CloudWatch Logs PutLogEvents round-trip (both must be set, group + stream must exist):
August 2, 2026 · View on GitHub
███████╗████████╗██████╗ ██╗ ██╗██╗ ██╗███████╗
██╔════╝╚══██╔══╝██╔══██╗╚██╗ ██╔╝██║ ██╔╝██╔════╝
███████╗ ██║ ██████╔╝ ╚████╔╝ █████╔╝ █████╗
╚════██║ ██║ ██╔══██╗ ╚██╔╝ ██╔═██╗ ██╔══╝
███████║ ██║ ██║ ██║ ██║ ██║ ██╗███████╗
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝
[ a w s ]
[AWS CLIENT FOR STRYKE // S3 + DYNAMODB + SQS + LAMBDA + STS + SNS + SSM + SECRETS + SES + CLOUDWATCH + LOGS + EC2 + KMS + IAM + KINESIS + ECR + STEP FUNCTIONS]
"The cloud, one stryke pipe away."
AWS client for stryke — S3, DynamoDB, SQS, Lambda, STS, SNS, SSM Parameter Store, Secrets Manager, SES, CloudWatch, CloudWatch Logs, EC2, KMS, IAM, Kinesis, ECR, and Step Functions. Opt-in package tier, kept out of the stryke core binary so the daily-driver install stays slim.
strykelang · MenkeTechnologiesMeta · stryke-gcp · stryke-docker · stryke-k8s · stryke-demo
Read the Docs · Engineering Report
Table of Contents
- [0x00] Why this is a package, not a builtin
- [0x01] Install
- [0x02] Quick start
- [0x03] API reference
- [0x04] FFI layer
- [0x05] DynamoDB type encoding
- [0x06] LocalStack / MinIO
- [0x07] Tests
- [0x08] Dev workflow
- [0x09] Layout
- [0x0A] Roadmap
- [0xFF] License
[0x00] Why this is a package, not a builtin
The official aws-sdk-rust crates pull in tokio, hyper, rustls, and a fat chain of smithy / signing / endpoint-resolution support code. The 17 SDKs combined are way too much to bake into stryke core. This package ships them once, opt-in.
stryke-aws ships a thin stryke library plus a Rust cdylib
(libstryke_aws.{dylib,so}) that stryke's FFI bridge dlopens in-process
on first use AWS — no helper-binary fork per call (the v1 helper-binary
model was replaced in v0.2.0). Credentials and region come from the
standard AWS chain (env vars, ~/.aws/config|credentials, IMDS) — same
as the aws CLI.
[0x01] Install
From a release (no rustc on the consumer machine):
s pkg install -g github.com/MenkeTechnologies/stryke-aws
From a local checkout:
cd ~/projects/stryke-aws
cargo build --release # produces target/release/libstryke_aws.{dylib,so}
s pkg install -g . # cdylib lands in ~/.stryke/store/aws@<version>/
Or:
make install
The cdylib is dlopened in-process on first use AWS. A shared tokio
runtime + aws_config::SdkConfig cache per region is held in OnceCell
— no fork-per-call, no full IMDS/SSO/env creds chain on each call. The
cdylib covers S3, DynamoDB, SQS, Lambda, STS, SNS, SSM Parameter Store,
Secrets Manager, SES, CloudWatch, CloudWatch Logs, EC2, KMS, IAM, Kinesis,
ECR, and Step Functions; further ops can be added incrementally.
[0x02] Quick start
use AWS::S3
use AWS::Dynamo
use AWS::SQS
use AWS::Lambda
use AWS::STS
# Identity check.
p to_json AWS::STS::caller_identity()
# S3 — list, get, put, head, rm.
val @keys = AWS::S3::ls "s3://my-bucket/prefix/", delimiter => "/"
for val $e (@keys) {
p "$e->{type}: $e->{key}"
}
AWS::S3::put "s3://my-bucket/hello.txt", data => "hello from stryke"
p AWS::S3::get "s3://my-bucket/hello.txt"
p to_json AWS::S3::head "s3://my-bucket/hello.txt"
# DynamoDB — items. Plain JSON in/out (no AttributeValue).
AWS::Dynamo::put "users", { id => "u-42", name => "alice", score => 1.5 }
p to_json AWS::Dynamo::get "users", { id => "u-42" }
p for AWS::Dynamo::tables()
# SQS — send / long-poll / pump.
AWS::SQS::send $queue_url, "payload"
val @msgs = AWS::SQS::receive $queue_url, max => 10
# pump = receive → callback → delete-on-success
AWS::SQS::pump $queue_url, iterations => 5, callback => fn ($m) {
handle_message $m->{body}
}
# Lambda — invoke (sync or fire-and-forget).
val $reply = AWS::Lambda::call "my-fn", { hello => "world" }
p to_json $reply
Per-call connection overrides (named opts on every public fn):
AWS::S3::ls "s3://...",
region => "us-west-2",
profile => "prod",
endpoint => "http://localstack:4566" # for LocalStack / MinIO
[0x03] API reference
use AWS
Plumbing only — AWS::version() (cdylib package version) and
AWS::ping(%opts) (STS-backed connectivity probe), plus the flat
AWS::<service>_<op> fns that the namespaced wrappers below delegate to.
use AWS::S3
AWS::S3::ls $uri, %opts → @entries
AWS::S3::get $uri, %opts → $body (or $path when output=>"PATH")
AWS::S3::put $uri, %opts → \%resp # data=>$bytes | input=>"PATH"
AWS::S3::head $uri, %opts → \%resp
AWS::S3::rm $uri, %opts → \%resp
AWS::S3::presign $uri, %opts → dies # deferred in the cdylib
AWS::S3::buckets %opts → @buckets
AWS::S3::versions $uri, %opts → \%resp # { versions, delete_markers }
AWS::S3::location $uri, %opts → \%resp # { bucket, location_constraint, region }
AWS::S3::tags $uri, %opts → \%tags # { key => value }
AWS::S3::mb $uri, %opts → \%resp # CreateBucket; { bucket, location, created }
AWS::S3::rb $uri, %opts → 1 | 0 # DeleteBucket (bucket must be empty)
ls entries: {type=>"object", key, size, etag, last_modified, storage_class} or {type=>"prefix", key} when delimiter is set.
use AWS::Dynamo
AWS::Dynamo::get $table, $key, %opts → \%item | undef
AWS::Dynamo::put $table, $item, %opts → 1 | 0
AWS::Dynamo::delete $table, $key, %opts → 1 | 0
AWS::Dynamo::query $table, %opts → @items # opts: key_condition, values, names, filter, index, limit
AWS::Dynamo::scan $table, %opts → @items # opts: filter, values, limit
AWS::Dynamo::describe $table, %opts → \%info # status, item_count, key_schema, arn
AWS::Dynamo::tables %opts → @names
AWS::Dynamo::transact %opts → $count # opts: puts ([{table,item}]), deletes ([{table,key}]); atomic
AWS::Dynamo::ttl $table, %opts → \%info # { status, attribute_name }
AWS::Dynamo::create $table, $hash_key, %opts → \%resp # CreateTable (on-demand); opts: hash_key_type/range_key/range_key_type
AWS::Dynamo::drop $table, %opts → \%resp # DeleteTable; { table, status, deleted }
AWS::Dynamo::batch_write $table, $rows, %opts → dies # deferred in the cdylib
AWS::Dynamo::scan_stream $table, %opts → dies # deferred in the cdylib
query/scan take a DynamoDB expression plus a values hashref of
:placeholder => value bindings (and optional names for #alias). Nested
maps/lists round-trip recursively; binary attributes come back as
"base64:…". batch_write and scan_stream remain deferred.
use AWS::SQS
AWS::SQS::send $queue, $body, %opts → $message_id # delay_seconds / dedup_id / group_id deferred (die)
AWS::SQS::receive $queue, %opts → @messages # opts: max; wait / visibility deferred (die)
AWS::SQS::delete $queue, $receipt, %opts → { deleted: 1 | 0 }
AWS::SQS::list %opts → @urls # prefix filter deferred
AWS::SQS::purge $queue, %opts → 1 | 0 # delete all messages
AWS::SQS::attrs $queue, %opts → \%attributes # GetQueueAttributes (All)
AWS::SQS::set_attrs $queue, $attributes, %opts → 1 | 0 # SetQueueAttributes
AWS::SQS::change_visibility $queue, $receipt, $visibility_timeout, %opts → 1 | 0
AWS::SQS::pump $queue, %opts → $count # callback + auto-delete on success
use AWS::Lambda
AWS::Lambda::invoke $name, $payload, %opts → \%resp # { function, status_code, result }
AWS::Lambda::call $name, $payload, %opts → $result # convenience: just .result (undef unless status_code == 200)
AWS::Lambda::list %opts → @functions
AWS::Lambda::get $name, %opts → \%resp # GetFunction: { runtime, handler, arn, memory_size, timeout, … }
invocation_type => "event" (fire-and-forget) is deferred in the
cdylib.
use AWS::STS
AWS::STS::caller_identity %opts → { account, arn, user_id }
AWS::STS::assume_role $role_arn, %opts → \%creds # opts: session_name; temp credentials
use AWS::SNS
AWS::SNS::topics %opts → @topic_arns
AWS::SNS::create $name, %opts → $topic_arn
AWS::SNS::publish $message, %opts → $message_id # opts: topic_arn | target_arn | phone_number, subject
AWS::SNS::subscribe $topic_arn, $protocol, $endpoint, %opts → $subscription_arn
AWS::SNS::unsubscribe $subscription_arn, %opts → 1 | 0
AWS::SNS::subscriptions $topic_arn, %opts → @{ {subscription_arn, protocol, endpoint, owner} }
AWS::SNS::delete $topic_arn, %opts → 1 | 0 # DeleteTopic
use AWS::SSM (Parameter Store)
AWS::SSM::get $name, %opts → $value # opts: with_decryption
AWS::SSM::put $name, $value, %opts → $version # opts: type (String|StringList|SecureString), overwrite
AWS::SSM::by_path $path, %opts → @{ {name, value} } # opts: recursive, with_decryption
AWS::SSM::delete $name, %opts → 1 | 0
AWS::SSM::get_many $names_or_aref, %opts → \%resp # GetParameters (≤10): { parameters, invalid_parameters }
use AWS::Secrets (Secrets Manager)
AWS::Secrets::get $secret_id, %opts → $secret_string
AWS::Secrets::create $name, $secret_string, %opts → { arn, name }
AWS::Secrets::put $secret_id, $secret_string, %opts → $version_id
AWS::Secrets::list %opts → @{ {name, arn} }
use AWS::SES (email, v2)
AWS::SES::send $from, $to_or_aref, %opts → $message_id # opts: subject, body, html
use AWS::CloudWatch
AWS::CloudWatch::put $namespace, $metric_name, $value, %opts → 1 # opts: unit
AWS::CloudWatch::list %opts → @{ {namespace, metric_name, dimensions} } # opts: namespace, metric_name
use AWS::Logs (CloudWatch Logs)
AWS::Logs::groups %opts → @groups # DescribeLogGroups; opts: prefix
AWS::Logs::create $name, %opts → 1 # CreateLogGroup
AWS::Logs::filter $log_group, %opts → @events # opts: filter_pattern, start_time, end_time, limit
AWS::Logs::events $log_group, $log_stream, %opts → @events # opts: limit, start_from_head
AWS::Logs::put $log_group, $log_stream, $events, %opts → $next_token # PutLogEvents; events: [{message, timestamp(ms)}]; opts: sequence_token
use AWS::EC2
AWS::EC2::instances %opts → @instances # DescribeInstances; opts: instance_ids
AWS::EC2::start $ids, %opts → @changes # StartInstances
AWS::EC2::stop $ids, %opts → @changes # StopInstances; opts: force
AWS::EC2::security_groups %opts → @groups # opts: group_ids
AWS::EC2::vpcs %opts → @vpcs # opts: vpc_ids
AWS::EC2::reboot $ids, %opts → @ids # RebootInstances
use AWS::KMS (Key Management Service)
AWS::KMS::keys %opts → @{ {key_id, key_arn} } # ListKeys
AWS::KMS::describe $key_id, %opts → \%info # DescribeKey
AWS::KMS::encrypt $key_id, $plaintext, %opts → $ciphertext # base64
AWS::KMS::decrypt $ciphertext, %opts → $plaintext # opts: key_id
AWS::KMS::data_key $key_id, %opts → { key_id, plaintext, ciphertext } # opts: key_spec (default AES_256)
use AWS::IAM
AWS::IAM::users %opts → @users # ListUsers
AWS::IAM::roles %opts → @roles # ListRoles
AWS::IAM::user %opts → \%user # GetUser; opts: user_name
AWS::IAM::role_policies $role_name, %opts → @{ {policy_name, policy_arn} }
AWS::IAM::role $role_name, %opts → \%role # GetRole; { role_name, role_id, arn, path, description, max_session_duration }
use AWS::Kinesis
AWS::Kinesis::list %opts → @stream_names # ListStreams
AWS::Kinesis::describe $stream_name, %opts → \%info # DescribeStream
AWS::Kinesis::put $stream_name, $partition_key, $data, %opts → \%resp # PutRecord
use AWS::ECR (Elastic Container Registry)
AWS::ECR::repositories %opts → @repos # DescribeRepositories; opts: repository_names
AWS::ECR::images $repository_name, %opts → @{ {image_digest, image_tag} } # ListImages
use AWS::StepFunctions
AWS::StepFunctions::list %opts → @{ {name, arn, type, creation_date} } # ListStateMachines
AWS::StepFunctions::start $state_machine_arn, %opts → { execution_arn, start_date } # opts: name, input
AWS::StepFunctions::describe $execution_arn, %opts → \%info # DescribeExecution
Flat extras on use AWS
AWS::s3_copy_object $src_bucket, $src_key, $bucket, $key, %opts → \%resp
AWS::s3_delete_objects $bucket, \@keys, %opts → @deleted # batch delete (≤1000)
AWS::ddb_update_item $table, \%key, \%updates, %opts → 1 | 0 # SET each attr
AWS::ddb_batch_get_item $table, \@keys, %opts → @items # ≤100 keys
AWS::ddb_batch_write_item $table, %opts → $count # opts: puts (≤25), deletes
Pure helpers (no AWS)
AWS::parse_arn($arn) → { partition, service, region, account_id, resource, resource_type, resource_id }
AWS::build_arn(%opts) → $arn # parts → ARN; inverse of parse_arn
AWS::parse_s3_uri($uri) → { bucket, key }
AWS::build_s3_uri($b, $k?) → $uri # bucket+key → s3:// URI; inverse of parse_s3_uri
AWS::s3_uri_to_arn($uri, $partition?) → { arn, bucket, key } # s3://b/k → arn:aws:s3:::b/k
AWS::arn_to_s3_uri($arn) → { uri, bucket, key } # arn:aws:s3:::b/k → s3://b/k; inverse of s3_uri_to_arn
AWS::valid_bucket_name($n) → { name, valid, reason } # AWS bucket naming rules
AWS::valid_s3_key($key) → { key, valid, reason, bytes } # S3 object key: non-empty, ≤1024 UTF-8 bytes (any char, incl /)
AWS::valid_sqs_queue_name($n) → { name, valid, reason, fifo } # SQS queue name: ≤80 chars, alphanumeric/-/_, FIFO ends .fifo (counts toward 80); case-sensitive
AWS::valid_account_id($id) → { account_id, valid, reason } # exactly 12 decimal digits (leading zeros allowed)
AWS::valid_arn($arn) → { arn, valid, reason } # non-throwing structure check: 6 fields, arn prefix, non-empty partition/service/resource
AWS::arn_matches($pattern, $arn) → { pattern, arn, matches } # IAM policy resource matching: * (spans :/), ? (one char), anchored, case-sensitive; arn:aws:s3:::bucket/* matches every object key
AWS::partition_for_region($r) → { region, partition } # cn-*→aws-cn, us-gov-*→aws-us-gov, us-iso(b)-*→aws-iso(-b), else aws
AWS::valid_region($r) → { region, valid, partition, reason } # botocore regionRegex per partition; rejects malformed names (partition_for_region never does)
AWS::dns_suffix_for_partition($p) → { partition, dns_suffix } # aws→amazonaws.com, aws-cn→amazonaws.com.cn, aws-iso→c2s.ic.gov, … (botocore)
AWS::service_endpoint($svc, $region) → { service, region, partition, dns_suffix, endpoint, url } # <svc>.<region>.<dns_suffix> (s3.us-east-1.amazonaws.com)
AWS::parse_service_endpoint($endpoint) → { endpoint, service, region, partition, dns_suffix, url } # inverse of service_endpoint; host or URL → parts
AWS::s3_object_url($bucket, $region, $key?) → { url, bucket, region, partition, host } # virtual-hosted https URL: https://<bucket>.s3.<region>.<dns_suffix>/<key> (key percent-encoded)
AWS::parse_s3_url($url) → { url, bucket, key, region, partition, style, host, dns_suffix } # inverse of s3_object_url; virtual-hosted + path styles; key percent-decoded
AWS::region_for_az($az) → { az, region, zone_letter } # AZ → region (us-east-1a → us-east-1)
These open no client — pure string parsing/validation, so they run with no credentials.
[0x04] FFI layer
Each AWS::* wrapper builds a JSON args dict and calls a sibling
aws__* symbol resolved out of libstryke_aws.{dylib,so}. The cdylib
is dlopened in-process on first use AWS (via stryke's
pkg::commands::try_load_ffi_for resolver hook) and exposes the entry
points listed in the [ffi] exports table in stryke.toml, spanning
STS, S3, DynamoDB, SQS, Lambda, SNS, SSM, Secrets, SES, CloudWatch,
CloudWatch Logs, EC2, KMS, IAM, Kinesis, ECR, and Step Functions.
Persistent state: a shared tokio runtime + an aws_config::SdkConfig
cache per region held in OnceCell — no fork-per-call, no full
IMDS/SSO/env creds chain on each call.
Errors come back as {"error": "<msg>"} — the wrapper dies with it.
[0x05] DynamoDB type encoding
Plain JSON → AttributeValue:
| JSON | AV |
|---|---|
null | NULL(true) |
bool | BOOL |
number | N (stringified to preserve precision) |
string | S (or B when the string is "base64:...") |
array | L (heterogeneous) |
object | M |
Set types (SS / NS / BS) round-trip out as JSON arrays — on the
write path, pass them as L (an array) and DynamoDB will store as a list,
or extend the cdylib export list if you specifically need a typed set.
[0x06] LocalStack / MinIO
val %ls = (endpoint => "http://localhost:4566", region => "us-east-1")
AWS::S3::buckets(%ls) |> ep
AWS::SQS::list(%ls) |> ep
$ENV{AWS_ENDPOINT_URL} works as a global default.
[0x07] Tests
cargo test # compiles, no live calls
s test t/ # creds-aware end-to-end
# Opt into the per-service round-trips with a writable resource:
export STRYKE_AWS_TEST_BUCKET=my-test-bucket
export STRYKE_AWS_TEST_TABLE=stryke-aws-demo # PK `id: S`
export STRYKE_AWS_TEST_QUEUE=https://sqs.us-east-1.amazonaws.com/.../my-q
# CloudWatch Logs PutLogEvents round-trip (both must be set, group + stream must exist):
export STRYKE_AWS_TEST_LOG_GROUP=/stryke-aws/test
export STRYKE_AWS_TEST_LOG_STREAM=test-stream
# Opt into the create/delete lifecycle tests (creates + tears down a real
# bucket and DynamoDB table — off by default so a shared account is safe):
export STRYKE_AWS_TEST_MUTATE=1
s test t/
The suite skips cleanly when the cdylib isn't installed, when credentials are missing, or when the per-service env vars are unset.
[0x08] Dev workflow
make # release build
make debug
make test
make install
make clean
[0x09] Layout
stryke-aws/
stryke.toml # stryke package manifest
Cargo.toml # cdylib crate manifest
Makefile
src/
lib.rs # cdylib — aws__* extern "C" exports
lib/
AWS.stk # `use AWS` — plumbing + ping + flat ops + pure helpers
S3.stk # `use AWS::S3`
Dynamo.stk # `use AWS::Dynamo`
SQS.stk # `use AWS::SQS`
Lambda.stk # `use AWS::Lambda`
STS.stk # `use AWS::STS`
SNS.stk # `use AWS::SNS`
SSM.stk # `use AWS::SSM`
Secrets.stk # `use AWS::Secrets`
SES.stk # `use AWS::SES`
CloudWatch.stk # `use AWS::CloudWatch`
Logs.stk # `use AWS::Logs`
EC2.stk # `use AWS::EC2`
KMS.stk # `use AWS::KMS`
IAM.stk # `use AWS::IAM`
Kinesis.stk # `use AWS::Kinesis`
ECR.stk # `use AWS::ECR`
StepFunctions.stk # `use AWS::StepFunctions`
t/
test_aws.stk # end-to-end (gated on creds + opt-in env vars)
test_stryke_aws_surface.stk # wrapper-completeness pin
examples/
ddb_demo.stk
discover.stk
lambda_call.stk
s3_browse.stk
sqs_pump.stk
whoami.stk
.github/workflows/
ci.yml # cargo + compile-only (no live AWS)
release.yml # cross-compile + GH release on tag push
[0x0A] Roadmap
| Shipped | Later |
|---|---|
| S3 (incl. head/versions/location/tags), DynamoDB (incl. query/scan/delete/describe/transact/ttl), SQS (incl. purge/attrs/set_attrs/change_visibility), Lambda (incl. get), STS (incl. assume_role) | Deferred ops: DDB batch_write/scan_stream, S3 presign, Lambda invocation_type=event, SQS send delay_seconds/dedup_id/group_id, SQS receive wait/visibility, SQS list prefix |
| SNS, SSM, Secrets Manager, SES, CloudWatch, CloudWatch Logs, EC2, KMS, IAM, Kinesis, ECR, Step Functions | Typed-set passthrough, optimistic-locking helpers |
| In-process cdylib + persistent SdkConfig cache | Streaming multipart upload |
| Plain JSON DDB; buffered S3 put |
[0xFF] License
MIT.