Skip to content

Releases

v2.7.1

Choose a tag to compare

@jeffyaw jeffyaw released this 11 Nov 14:21
v2.7.1
90a251f

Changed

  • A tools/call whose undeclared arguments total more than 64 KiB is refused with JSON-RPC -32602 instead of being processed with those arguments silently dropped. Undeclared arguments are never read by any tool, so no legitimate input is lost: declared arguments are never measured, and large ones (an aws_lambda_invoke payload, aws_call params, aws_script code, an aws_resource_* desired state) go through as before. A small stray argument is still ignored. The refusal names the undeclared keys (at most 10, each cut to 64 characters) and the tool's declared ones. This clears the security-oversized-input warning from @yawlabs/mcp-compliance on both the Node and oam legs.

Fixed

  • A client cancel now stops the aws CLI child already in flight, instead of leaving it to run to its per-call timeout (60s by default). runAwsCall takes an AbortSignal: already aborted, nothing is spawned; aborted mid-call, the child is killed through the same path the timeout uses and the call settles at once as cancelled. Wired into aws_sso_list_assignments / aws_sso_generate_profiles discovery, aws_multi_region and aws_multi_account, where an in-flight entry now comes back as errorKind: "cancelled".
  • Concurrent writes to ~/.aws/credentials or the AWS config file no longer fail spuriously on Windows. Under contention, Windows can fail the exclusive create of <file>.lock with EPERM/EACCES instead of EEXIST (likely a lock file whose delete is still pending), and the writer treated that as fatal, so a concurrent aws_assume_role or aws_sso_generate_profiles could fail with EPERM ... credentials.lock. On Windows an unbroken run of those errors is now retried as contention for up to 1 s, after which the real error is reported, so a genuinely read-only ~/.aws still fails fast with its true cause. Other platforms are unchanged.

v2.7.0

Choose a tag to compare

@jeffyaw jeffyaw released this 11 Nov 12:12
v2.7.0
5457509

Added

  • IAM Identity Center bootstrap for a user with no profile yet (#65). aws_login_start takes ssoSession + startUrl + ssoRegion: it writes a managed [sso-session] block to the AWS config file and runs aws sso login --sso-session (AWS CLI 2.9.0+, with AWS_PROFILE stripped from the child so a stale one cannot fail it). aws_login_complete on such a login returns the token status instead of running an identity check there is no profile for. Two new tools follow it: aws_sso_list_assignments (read-only; every account and role the login can use, flagging which already have a profile) and aws_sso_generate_profiles (writes [profile ...] sections for all assignments, up to 200, or a selection; dryRun previews). Generated sections carry a managed-by: aws-mcp marker and are the only sections aws-mcp ever changes -- a hand-written section is never modified, and every other byte of the file is preserved. The SSO bearer token reaches the CLI only through a 0600 temp file and never appears in a tool result. aws_list_profiles gains ssoAccountId, ssoRoleName and managed.

Changed

  • The oam floor moves from 0.19.1 to 0.19.5, so the aws-mcp launcher no longer runs the server on oam 0.19.1-0.19.4. 0.19.5 is verified on the installed aarch64-pc-windows-msvc binary through the launcher with OAM_BIN pointing at it and AWS_MCP_RUNTIME=oam: a full MCP handshake listing all 30 tools, aws_list_profiles served, and the documented aws_script sandbox divergence re-measured on it (eval('1+1') returns 2 and Function('return 7')() returns 7 under oam; Function('return this')() yields a global with no process and no require; both raise the code-generation error under Node).
  • The locked atomic writer behind aws_assume_role's credentials file moved to src/ini-file.ts and is shared with the config-file writer. No behaviour change for aws_assume_role.
  • README: new section "Multi-user gateways: one process per user" (#65) -- run one aws-mcp process per user, each with its own HOME/USERPROFILE, AWS_CONFIG_FILE, AWS_SHARED_CREDENTIALS_FILE and TMPDIR, routed by the gateway's verified principal.

Fixed

  • AWS_CONFIG_FILE and AWS_SHARED_CREDENTIALS_FILE are honoured everywhere the server reads those files itself (#67). aws_list_profiles, the profile start-URL lookup that picks the cached SSO token for aws_whoami, aws_login_complete and aws_refresh_if_expiring_soon, and aws_assume_role read ~/.aws/config / ~/.aws/credentials directly, so with either variable set they disagreed with the aws CLI they spawn. Paths now resolve as botocore resolves them: $NAME / ${NAME} (and %NAME% on Windows) are expanded, then ~; an unset reference is left as written; the value is not trimmed; and a set-but-empty value means "no file", which aws_list_profiles reports as an error and aws_assume_role refuses, instead of falling back to the default path.
  • SSO device login recognizes the newer verification URLs (#66, thanks @SolomonHD). aws_login_start now accepts the IAM Identity Center portal (https://<id>.awsapps.com/start/#/device) and AWS sign-in (https://...signin.aws/.../device) forms. A signin.aws URL must end in /device, so an unrelated sign-in URL cannot suppress the PKCE fallback, and the URL is re-read from the full output when the login settles, so a stdout chunk split inside ?user_code=... no longer drops the code.

Security

  • Lockfile-only bumps, within existing ranges, clearing all open Dependabot alerts and npm audit findings (#70): undici 8.12.4 (3 high), fast-uri 3.1.9, hono 4.13.18, @hono/node-server 2.1.7, zod 4.6.7, body-parser 2.4.0, qs 6.17.1, ip-address 10.7.6.

v2.6.1

Choose a tag to compare

@jeffyaw jeffyaw released this 30 Oct 22:56
v2.6.1
68f5450

Changed

  • The oam floor moves from 0.18.0 to 0.19.1, so the aws-mcp launcher no longer runs the server on oam 0.18.x or 0.19.0. 0.19.1 is verified on the installed aarch64-pc-windows-msvc binary through the launcher with OAM_BIN pointing at it and AWS_MCP_RUNTIME=oam: a full MCP handshake listing all 28 tools, aws_list_profiles served, and the documented aws_script sandbox divergence re-measured on it (eval('1+1') returns 2 and Function('return 7')() returns 7 under oam; Function('return this')() still yields a global with no process and no require; the host process.versions.oam read through the bridge was 0.19.1). An OAM_BIN at oam 0.19.0 is now refused under AWS_MCP_RUNTIME=oam with Run oam self-update, and falls back to Node under the default auto. The self-update advice's ssh-keygen note now names only an oam that predates 0.18.0, the first signed release, since a below-floor oam may now be a signed one.
  • Source comments that said oam drops the creation mode of open / writeFileSync now say that was true before 0.19.1: oam 0.19.1 honours it like Node (re-measured on Windows: a file created 0o400 comes back read-only). The fchmod / chmod / private-mode checks stay, as defense in depth for mounts that ignore modes and for a client that runs oam run directly and bypasses the launcher's floor. No behaviour change.

Fixed

  • scripts/check-oam-floor.mjs (run by release.sh and npm run check:oam-floor) now runs from a junctioned or symlinked checkout. It decided whether it was the main module by comparing process.argv[1] with its own URL, which Node realpaths and argv[1] does not, so from such a checkout it printed nothing and exited 0 -- a silent pass of the release gate. Both sides are now realpath'd. Release tooling only.

v2.6.0

Choose a tag to compare

@jeffyaw jeffyaw released this 11 Oct 15:28
v2.6.0
d157486

Added

  • The server now sends MCP instructions: a short, plain-ASCII routing guide that names which tool to reach for (start with aws_whoami, one operation with aws_call, fan-out with aws_multi_region / aws_multi_account, resources by CloudFormation type, logs, metrics, docs). It is held under 2000 bytes, the ceiling yaw-mcp cuts upstream instructions at, by a test that also fails if it names a tool that does not exist or leaves one out. src/server-instructions.ts had promised this block since 2.5.0 without it ever being wired.
  • release.sh grades the server with @yawlabs/mcp-compliance before every release, pinned at ^0.20.4, the current release line (yaw-mcp's own yaw-mcp audit still pins ^0.17.1). npm run check:compliance runs mcp-compliance test --strict against the published launcher under AWS_MCP_RUNTIME=node and again under oam; a required-test failure stops the release, and a leg that could not run (the package not installed, no oam on PATH) is reported as a warning, never as a pass. Both legs grade A here. Release tooling only.

Changed

  • The launcher's "no usable oam" advice names the fix for what it actually found. Under AWS_MCP_RUNTIME=oam an oam below the floor now gets Run oam self-update, a binary that would not run gets "check that it is an executable oam binary for this platform", a missing OAM_BIN gets "point OAM_BIN at an existing oam binary", and only a machine with no oam at all is sent to https://oamjs.org (never on linux-arm64, where oam publishes no build). The oam self-update advice also says that updating from an oam older than 0.18.0 needs ssh-keygen 8.1 or later, because that update installs the first signed release and verifies it.
  • The launcher finds an oam installed to OAM_INSTALL_DIR. That is the oam installer's own target directory, and an oam installed there and not on PATH was never found; it is now searched first.
  • On an oam host, the launcher takes oam's inherited --permission / --allow-* flags out of NODE_OPTIONS before it starts the server in a child (Node after a handoff, or a newer oam). oam 0.18.0 hands those flags to every child through NODE_OPTIONS, and this server is never meant to run sandboxed. The flags are matched the way Node tokenizes NODE_OPTIONS (double quotes, backslash escapes), so a quoted option is kept whole, and a permission path holding a space, which oam appends unquoted, is dropped with its flag. On Node, NODE_OPTIONS is passed through untouched.
  • A docs fetch under oam that fails with no error code no longer blames an untrusted certificate. Since oam 0.16.3 a refused certificate carries Node's code and gets the NODE_EXTRA_CA_CERTS advice as it does on Node; the uncoded failure that remains (measured on oam 0.18.0 with a server that answers TLS with garbage) now points at a TLS-intercepting proxy or an unreachable HTTPS_PROXY.
  • npm run build:binary:oam verifies the cross-build carrier against the release's signed RELEASE-MANIFEST (ssh-keygen -Y verify against oam's release keys, vendored in scripts/oam-release-keys/, with the manifest's tag checked against the one requested), not against the unsigned SHA256SUMS served beside the binary. It defaults to the launcher's oam floor instead of latest. A tag before v0.18.0, which has no manifest, is checked as oam's installers check it: its SHA256SUMS must match the pinned digest for that tag (oam's release-keys/presigning-sums, vendored beside the keys), and a tag with no pin is refused. Every failure aborts the build. Release tooling only.
  • Source comments about oam behaviour are brought up to the 0.18.0 floor: writeFileSync's wx flag, mkdtemp names and the child's pick between case-duplicate environment names all now match Node on oam 0.18.0. No behaviour change.

Fixed

  • scripts/update-manifests.mjs escapes every value it writes into a Ruby string in the Homebrew formula (CodeQL js/incomplete-sanitization). The desc escaped " but not \, so a description holding \" came out as \\" -- an escaped backslash and then a closing quote -- and it left #{...} alone, which Ruby interpolates inside double quotes, so such a description would have run as code when brew loaded the formula. A new rubyString() escapes the backslash first, then ", a # that starts interpolation (#{, #@, #$; a plain # stays as written so brew style does not flag it), CR and LF, and is applied to desc, homepage, version, license, the release URLs, the sha256s and the command name. For the real package.json the generated formula and Scoop manifest are byte-identical to the old script's (checked on v1.5.3). Release tooling only; the server itself is unchanged.

v2.5.2

Choose a tag to compare

@jeffyaw jeffyaw released this 06 Oct 20:51
v2.5.2
eb9ef28

Changed

  • The oam floor moves from 0.17.0 to 0.18.0, so the aws-mcp launcher no longer runs the server on an older oam. This server is verified on one oam release at a time, and the floor keeps the launcher off anything older than that release. 0.18.0 is verified on the published aarch64-pc-windows-msvc binary, checksum matched against the release SHA256SUMS, through the launcher with OAM_BIN pointing at it: a full MCP handshake listing all 28 tools (the same 28 as on Node), aws_list_profiles served, and the documented aws_script sandbox divergence re-measured on it (inside the sandbox eval('1+1') returns 2 and Function('return 7')() returns 7 under oam, while both raise EvalError under Node; Function('return this')() still yields a global with no process and no require; the host process.versions.oam read through the bridge was 0.18.0). If the launcher finds only oam 0.17.x or older, it now falls back to Node under the default AWS_MCP_RUNTIME=auto, and exits with an error under AWS_MCP_RUNTIME=oam -- it says so on stderr, naming the version it found and the floor (measured with the published 0.17.0 as OAM_BIN). On Node the tools and the AWS CLI they run are the same; the one difference is in aws_script, where eval and Function raise EvalError instead of working (see Runtime in the README). A client that runs oam run /path/to/aws-mcp/dist/index.js directly bypasses the launcher and keeps the oam it names. Run oam self-update, or set AWS_MCP_RUNTIME=node to make the choice explicit.

Security

  • The bundled MCP SDK moves from 1.30.0 to 1.32.1 (GHSA-6qxp-vccf-f47h, high: the SDK's OAuth client could send credentials to an authorization server chosen by the MCP server). The SDK is bundled into dist, so this ships in the package; the dependency floor is now ^1.32.1. Transitive dev dependencies are patched alongside it: proxy-addr 2.0.8 (GHSA-jqcg-44mw-7w3h), fast-uri 3.1.8 (GHSA-hrr3-gc8f-f4qj) and ip-address 10.7.3 (GHSA-j6r3-76f7-8jcv). npm audit reports 0 vulnerabilities.

Fixed

  • release.sh runs every mcp-publisher call to the MCP Registry, each login and each publish attempt, under coreutils timeout (MCP_PUBLISH_TIMEOUT_S, default 90 s) where one (or Homebrew's gtimeout) is on PATH -- without one the call runs unbounded, and a warning says so -- because mcp-publisher waits for the registry's answer with no limit of its own and a registry that never answered would have hung the release. A publish attempt that gets no answer is retried on the same 30, 60 and 90 s clock as the registry's own 429-504: one the limit stopped, which used to hang the step, and one whose connection failed or dropped (the client's error sending request or error reading response), which used to fail it at once. A login the limit stopped says the registry did not answer; one that failed any other way says how to read mcp-publisher's output (a 401 is the registry refusing the token exchange; a 429, a 5xx or a connection error is the registry or the network) instead of naming token scopes, which never fail a login: the registry reads the org role at login and refuses only at publish. A publish refused with a 403 says what the io.github.YawLabs namespace takes: a YawLabs org Owner whose token can read org roles. Release tooling only; the server itself is unchanged.

v2.5.1

Choose a tag to compare

@jeffyaw jeffyaw released this 30 Sep 02:33
v2.5.1
818b39f

Changed

  • The oam floor moves from 0.16.3 to 0.17.0, so the aws-mcp launcher no longer runs the server on an older oam. This server is verified on one oam release at a time, and the floor keeps the launcher off anything older than that release. 0.17.0 is verified on the published aarch64-pc-windows-msvc binary, checksum matched against the release SHA256SUMS: a full MCP handshake listing all 28 tools, aws_list_profiles served, and the documented aws_script sandbox divergence re-measured on it (inside the sandbox eval('1+1') returns 2 and Function('return 7')() returns 7 under oam, while both raise EvalError under Node; Function('return this')() still yields a global whose process and require are undefined). If the launcher finds only oam 0.16.3 or 0.16.4, it now falls back to Node under the default AWS_MCP_RUNTIME=auto, and exits with an error under AWS_MCP_RUNTIME=oam -- it says so on stderr, naming the version it found and the floor. On Node the tools and the AWS CLI they run are the same; the one difference is in aws_script, where eval and Function raise EvalError instead of working (see Runtime in the README). A client that runs oam run /path/to/aws-mcp/dist/index.js directly, as the README's Runtime section shows, bypasses the launcher and keeps the oam it names. Run oam self-update, or set AWS_MCP_RUNTIME=node to make the choice explicit.

Fixed

  • release.sh waits up to 600 s, not 300, for npm to serve a new version before the MCP Registry step, gives the post-publish npx smoke 60 attempts 10 s apart, not 30, and polls npm up to 120 times 5 s apart in its final check instead of reading once after 3 s. On 2026-09-29 the @yawlabs/fetch-mcp 0.8.2 release spent 295 s of its 300 s gate waiting for npm to serve the new version, and npx needed 313 s, 24 of its 30 attempts, to install @yawlabs/lemonsqueezy-mcp 1.0.1. Release tooling only; the server itself is unchanged.

v2.5.0

Choose a tag to compare

@jeffyaw jeffyaw released this 20 Sep 23:20
v2.5.0
bbb737a

Added

  • commandArgv: the exact argv beside every command string. Every envelope that carries command now also carries commandArgv -- the same call as an array of unquoted tokens, entry 0 the binary as displayed and one entry per argument, redacted identically. This is the fix for something no amount of quoting could solve. 2.4.0 stopped command from EXECUTING when pasted into PowerShell, but it could not make one string correct everywhere: in cmd.exe single quotes are not quoting characters at all, so &, |, ^ and a raw newline stay live whatever is done to the string (measured: 0 of 24 character-class probes correct, unchanged by the 2.4.0 fix -- --query 'x & echo PWNED_CMD' printed PWNED_CMD, and a value carrying a newline ran its tail as a fresh command line). It also could not serve Git Bash on Windows, where the PowerShell-correct doubled-quote form reads as concatenation and silently drops the quotes, turning Buckets[?Name=='prod'].Name into a different, invalid expression. And fish treats \\ and \' as escapes inside single quotes, so a backslash-bearing value arrived corrupted there -- the one POSIX shell the string form is wrong for. An array has none of these problems: there is nothing for a shell to re-interpret. The string is now RENDERED from the array rather than built beside it, so the two cannot drift and a redaction cannot apply to one and not the other. command remains, unchanged in shape, as the form for a human to read; parse commandArgv instead of it.

Changed

  • The oam floor moves from 0.15.2 to 0.16.3, so an oam older than that now falls back to Node. This server is verified on one oam release at a time, and the floor is what enforces that. 0.16.3 is verified: full MCP handshake, all 28 tools, and the documented aws_script sandbox divergence re-measured on it (inside the sandbox eval('1+1') returns 2 and Function('return 7')() returns 7 under oam, while both raise EvalError under Node). If you are on oam 0.15.2 through 0.16.2, the launcher will now hand off to Node instead of serving on your oam -- it says so on stderr, naming the version it found and the floor. Nothing about the server's behaviour changes when that happens; oam and Node run the same code and the same resolved AWS CLI. Update oam, or set AWS_MCP_RUNTIME=node to make the choice explicit.
  • AWS_MCP_AWS_CLI pointing at a Windows aws.exe is now refused when the server is not on Windows. This is the WSL case, and it was a trap rather than a configuration: /mnt/c/.../aws.exe passes every other check (the path is absolute, the .exe rule is Windows-only, and DrvFs reports 0777 so the executable test is satisfied), and WSL interop really does run it -- so it worked until it didn't. The server is a POSIX process: it writes /tmp/aws-mcp-input-*/params.json and passes file:///tmp/..., and aws_lambda_invoke hands the CLI a POSIX outfile path. A Windows CLI resolves both against the current drive, so params over the inline cap cannot be read and an invoke response is written where nothing looks for it. The error names the binary, the platform, and which transports break. Install the Linux build and point this at that. (The override is the only way a Windows executable can be reached on POSIX -- the PATH walk looks for a file named aws, never aws.exe.)

Fixed

  • A mis-spelled AWS_MCP_AWS_CLI no longer passes in silence on macOS and Linux. Environment variables are case-sensitive there, so aws_mcp_aws_cli=/usr/local/bin/aws was not "the override set wrong", it was "no override at all": the PATH walk answered instead and returned ok, leaving the operator with a working server running a different binary than the one they configured. Measured end to end -- an override naming a real executable plus a PATH holding another aws resolved to the PATH one, with no diagnostic. That defeats the whole design of this variable, which is to fail loudly rather than fall back, because it picks which binary handles your credentials. A near-miss now prints one line naming the spelling found and the canonical one, once per spelling rather than once per call. Windows is unaffected: the lookup case-folds there, so the variable is honoured as written.
  • The aws child processes pass windowsHide, matching the launcher. bin/aws-mcp.mjs set it on its version probe and both launch paths; the three spawns that run the actual AWS CLI did not, so libuv never added CREATE_NO_WINDOW for them. Bounded honestly: a console application inherits its parent's console, so a window could only appear where the server itself has none -- a GUI-subsystem MCP host launching it detached. Asserted rather than measured, because discriminating it needs a console-less parent plus a window probe. Inert on macOS and Linux.

v2.4.0

Choose a tag to compare

@jeffyaw jeffyaw released this 20 Sep 21:29
v2.4.0
f18b99b

Security

  • The display command no longer runs anything when pasted into PowerShell. Every result carries a command string built for a reader to paste, and a value containing a single quote made it executable. shellQuoteArg emitted the POSIX close-escape-reopen idiom; PowerShell reads '...' as literal text the way POSIX does but has no backslash escape inside it, so that form tokenises into separate words -- a ; in the value ends the statement, whatever follows it RUNS, and a # comments out the dangling quote that would otherwise be a parse error. Measured on win32/arm64 with PowerShell 5.1, driving the real call against a loopback stub: a --query value of x'; echo PWNED # printed PWNED. The other 24 character-class probes were delivered byte-identically in PowerShell and in Git Bash, so a single quote in the value is the whole trigger -- and a --query such as Buckets[?Name=='prod'].Name is an ordinary way to get one. Quoting is now chosen by the platform the reader is on: Windows emits PowerShell's own escape, a doubled quote, which is also inert in bash ('a''b' is concatenation), so a Git Bash paste yields a wrong value rather than a running command. command is still a display string, not a universal one: it is correct in a POSIX shell and in PowerShell, and NOT in cmd.exe, where &, | and a newline are live whatever the quoting (measured 0 of 24 probes correct, and unchanged by this fix). A %VAR% is now quoted rather than emitted bare, so a batch file no longer expands it away. Re-quote the string yourself before pasting it into a shell other than the server's own.
  • The 0600 on the two files that can hold credentials is verified now rather than assumed, and a filesystem that cannot honour it fails the call instead of leaking. The --cli-input-json params file (which can carry a SecureString) and ~/.aws/credentials (which carries an access key, a secret key and a session token in plaintext) were each opened at 0600 and then fchmod-ed at 0600 -- open's mode because Node honours it, the fchmod because oam drops it. Neither is sufficient: a filesystem may accept a chmod, report success, and change nothing. Measured on WSL Ubuntu (linux/arm64, Node 22.23.2) against a Windows drive under /mnt (v9fs/DrvFs): mkdtemp returns 0777, the exclusive create returns 0777, and fchmodSync(fd, 0o600) succeeds while changing nothing -- so both files landed world-readable and world-writable, with nothing able to detect it. World-writable is the worse half: the CLI opens the params path after this server has closed it, so another local user can substitute the payload in between. Neither configuration is exotic -- TMPDIR into a Windows drive is how you share one scratch directory between a WSL distro and its Windows host, and AWS_SHARED_CREDENTIALS_FILE (botocore's own variable, named in aws_assume_role's description) pointed at the Windows-side ~/.aws is how you share one credentials file and SSO cache. The mode is now checked with one fstat, and a file that cannot be made private is not written at all: the call fails naming the variable to change (TMPDIR/TMP/TEMP, or AWS_SHARED_CREDENTIALS_FILE) before any secret byte reaches disk. This is a failure you can newly hit: with either path on such a mount, calls that used to appear to succeed now fail with that message. That is the intended trade, because the previous behaviour was a silent exposure. CIFS/SMB, FAT-family and NTFS mounts under fmask/dmask, and Docker Desktop bind mounts, are expected to behave the same way -- reasoned, not measured. No POSIX measurement of oam exists anywhere, since oam publishes no linux-arm64 build and this work had no Mac, so every oam filesystem row in this release was measured on Windows.
  • The AWS CLI is resolved to an absolute path before every call, and never from the working directory, on any platform. 2.3.3 closed the Windows case by setting NoDefaultCurrentDirectoryInExePath on the server process, which left the protection resting on the server's own environment and did nothing for a PATH that names the working directory itself: on macOS and Linux an empty PATH entry or . means the current directory to execvp. The server now walks the absolute directories on the child environment's PATH itself -- aws.exe only on Windows, a regular executable file elsewhere -- and spawns the path it found. Every entry that would resolve against a directory the server does not control is skipped rather than searched: empty, ., a relative bin, a drive-relative C:foo, a root-relative \tools, an unexpanded %X%\bin. Each Windows aws child also gets NoDefaultCurrentDirectoryInExePath=1 explicitly, so the CLI's own helpers do not resolve there either -- aws ssm start-session hands off to session-manager-plugin by bare name, and a credential_process can name a bare command too. There is no bare-name fallback: when no CLI is found nothing runs, because falling back is exactly the planted-checkout case on a machine without the CLI. Resolving the path also settles a runtime difference -- oam searches its own install directory before the parent PATH while Node searches the working directory -- so Node and oam now run the same binary. The walk is not cached and does not need to be: 0.47-1.16 ms over this host's 58-entry PATH (20 runs, median 0.56) against a CLI start of hundreds of milliseconds, and a cache would go stale after an install or an aws update.

Fixed

  • aws_logs_tail returns structured events against a real AWS CLI -- for the first time since it shipped. It wrapped aws logs tail --format json on the premise that the output is NDJSON. It never was: that flag selects the CLI's pretty-print formatter, which prints <iso-timestamp> <stream> <message> lines and re-indents any message that is entirely JSON across several lines (awscli/customizations/logs/tail.py, byte-identical from 2.34.3 to the v2 head). The first line never parsed, so every non-empty call took the raw-string fallback: events was one text blob, eventCount and totalEvents were null, truncated was false, and the maxEvents bound added in 2.2.0 never applied -- on aws-cli 2.34.3 against a loopback endpoint a six-event window came back as a 637-character string, and a 2,000-event window with maxEvents: 5 as 329,998 characters. Only an empty window was right. Nor can that text be parsed back into events: a message beginning with a timestamp reads as a new event, a stream name with a space cannot be told from the message after it, a traceback's continuation lines carry no prefix, and a JSON message has already been re-serialized. The tool now calls FilterLogEvents through aws logs filter-log-events -- the same API tail calls -- whose JSON output is a single document, and returns events as {timestamp, logStreamName, message}: timestamp in ISO 8601 UTC with milliseconds, message verbatim rather than re-indented and right-stripped. The parser is strict now, which is the pin that keeps this from recurring: stdout that is not the document the fixed --query asked for is ok: false naming what arrived, where the old parser turned text into ok: true. That removes an arm the Stability section documented -- events as the raw blob with eventCount and totalEvents both null -- so events is always an array now. It is not held for a major because every non-empty call against a real CLI landed there: the shape being removed is the bug itself, not an output any caller could have gotten a real event out of. The tail scenarios in the fake are replaced by payloads captured from the real CLI, and a new suite drives the handler through the INSTALLED aws binary against an in-process endpoint on every npm test -- ten cases, four of which also assert the fake returns the identical envelope -- so a fake that drifts from the real format fails instead of hiding the next one.
  • A log-group ARN now addresses the group it names. The ARN used to be cut down to its bare name, so a source-account ARN silently read the same-named group in the caller's own account, and an ARN for another region read the call's region instead. It is now sent as FilterLogEvents' logGroupIdentifier (a trailing :* removed), so a cross-account read works from a CloudWatch cross-account monitoring account; an ARN whose region differs from the call's region is refused before anything spawns, with both regions named, because FilterLogEvents is regional; and an AWS CLI older than 2.9.15, which cannot address a group by ARN, gets an error saying so -- naming the ARN's own account and the bare-name workaround -- instead of a silent retarget.
  • Every aws call pins the CLI settings that change what this server reads. AWS CLI 2.34.0 added cli_error_format; set to json, yaml, text or table, it removes the An error occurred (Code) when calling ... line the error classifier reads, so an expired or rejected credential came back as nonzero_exit with no remedy instead of expired_creds / invalid_creds (measured on 2.34.3 against a loopback stub answering 403). cli_auto_prompt = on failed every call with exit 255 before a connection was opened -- aws sso login included, so aws_login_start could not work at all. Both are now pinned through the environment (AWS_CLI_ERROR_FORMAT=enhanced, the CLI's own default, and AWS_CLI_AUTO_PROMPT=off), not with flags: AWS CLI 2.22.0 answers --cli-error-format with Unknown options and exit 252, while an environment variable it does not know is ignored. The pins go on top of whatever environment the caller built rather than instead of it, so aws_multi_account's per-account credentials and the AWS_MAX_ATTEMPTS=1 that keeps aws_lambda_invoke to one attempt both survive them.
  • Non-ASCII output no longer breaks calls on Windows. Writing to a pipe, the Windows CLI uses the ANSI code page...
Read more

v2.3.4

Choose a tag to compare

@jeffyaw jeffyaw released this 20 Sep 10:19
v2.3.4
6acf810

Fixed

  • aws_iam_simulate answered per action when it promises per resource, and reported resources as denied that AWS allows. On 2026-07-30 IAM changed SimulatePrincipalPolicy server-side, so every CLI version is affected: it now returns ONE EvaluationResult per action, whose top-level EvalDecision is the most restrictive decision across every resource and whose EvalResourceName is an ARN TEMPLATE -- literally arn:${Partition}:s3:::${BucketName}/${KeyName}. The per-resource answers live only in ResourceSpecificResults, which this tool dropped. So "can this role read these three buckets" came back as one row that named none of them and said deny for all three because one was denied: safe and unreadable, and the opposite of the tool's job -- it is meant to stop a model attempting work that will 403, and instead it stopped the model attempting work that would have succeeded, sending the user hunting for permissions they already have. Rows now come from ResourceSpecificResults, one per resource, each with that resource's own decision, matched statement ids and missing context keys, so summary counts (action, resource) pairs again -- which is what the description and the README have promised all along. No field is added or removed. AWS's reference says the older one-result-per-resource shape carried the same aggregate decision on every result, so reading ResourceSpecificResults is right whichever shape AWS serves: one code path, no version sniffing. An action AWS does not break down per resource falls back to its own top-level result, so no action can drop out of results because AWS sent a shape we had not seen. Three corollaries, each a wrong answer on its own: a call that names no resources reports resource * again rather than the ARN template (that is the commonest call shape, "can I do this at all?", and AWS applies ['*'] server-side); missingContextValues survives that call, because the API reports those keys on the TOP-LEVEL result for a * simulation and puts them per resource only when the call named resources -- only a * entry inherits them, since the top-level list is a union across resources and copying it onto a specific ARN would blame that ARN for another's keys; and a decision outside allowed / explicitDeny / implicitDeny now reads unknown, as the README always said, where before only a MISSING decision landed there and every other string, including "" and a miscased "Allowed", passed through verbatim and was counted in denied. organizationsDecision and permissionsBoundaryDecision are reported by AWS per ACTION, so a resource row that was allowed now reads "allowed" instead of the action's aggregate "denied" -- an SCP or boundary deny cannot coexist with an allow -- while a row that is not allowed still carries the action's value, which the description now says rather than leaving the caller to guess. Every iam_simulate fixture the suite had was pre-change shape, which is why nothing caught this; the four new ones were transcribed from captures the real aws-cli 2.34.3 rendered from the documented XML through a loopback stub, and the 2-actions-by-3-resources call now yields 4 rows with no ${ in any resource.
  • aws_iam_simulate rejects an STS session ARN up front, with the lookup that fixes it. aws_whoami reports arn:aws:sts::<account>:assumed-role/<role>/<session> for every SSO session, which makes it the ARN a model reaches for first, but the simulator takes a user, group or role ARN only -- so the first call from an SSO session went to IAM and came back with nothing to act on. The handler now refuses it before anything spawns and names the iam get-role lookup for the role it parsed out. It deliberately does not rebuild the role ARN, because the session ARN drops the role's path and SSO roles live under aws-reserved/sso.amazonaws.com/. The description and the README's rows also state what the simulator cannot see: SCP statements never appear in matchedStatementIds, keys only an SCP references are never reported missing (so a region-locking SCP needs aws:RequestedRegion passed in contextEntries yourself), and "allowed" is necessary but not sufficient -- RCPs, the target resource's own policy, session policies and VPC endpoint policies are not evaluated. The pagination wording now matches what the CLI does: aws iam simulate-principal-policy follows IAM's IsTruncated/Marker itself and prints the merged pages, so hasMore is false and marker null on every first call, and only a call that resumed from marker can carry either. The fake was emitting a truncated FIRST page the real CLI never produces -- the same fake-vs-real gap that hid the aws_logs_tail bug -- and now models the CLI both ways. The response shape is unchanged.
  • aws_docs_read could not open the index.html pages aws_docs_search hands out. docs.aws.amazon.com answers every <path>/index.html with a 301 to <path>/, and the post-redirect check added in 2.0.0 re-used the input pattern, which demands a .html suffix. So the canonical form of a URL the site itself returns came back as "redirected to '/', which is not an 'https://docs.aws.amazon.com/...html' page" -- an error worded like a blocked off-site redirect. In a live census of eight queries, 126 of 800 search hits were index.html URLs, among them the AWS CLI's own per-service command indexes (cli/latest/reference/s3api/index.html), which are the most relevant family of pages there is for a server that wraps the CLI. The landing URL is now judged three ways instead of one, and the 2.0.0 hardening is kept whole: it must still be https, on docs.aws.amazon.com, on the default port, with no credentials, and an off-site redirect is refused exactly as before.
  • A guessed page name now says it does not exist, instead of coming back as another page's content. The site does not 404 a missing page inside a guide that exists: it 302s to the guide's landing page, and a missing CLI command to that service's command index. So the redirect fix on its own would have answered a guessed command name with the whole s3api index as the content of a command nobody can run -- which is what accepting every same-host redirect, the behavior an origin-only check gives you, amounts to. A PAGE request that lands on some other directory is now ok: false with "does not exist" and the landing URL, so a model that guessed learns that it guessed. Every shape here was re-verified against the live site on 2026-09-19, and the fixtures are trimmed captures of those responses rather than invented HTML.
  • A docs fetch blocked by a corporate gateway blamed AWS's backend for it. Node's fetch rejects with a bare TypeError: fetch failed and puts the real verdict in err.cause, which no production file here read -- so behind a TLS-inspecting gateway or a private CA (the ordinary shape of a corporate network, and these are the only two tools in this server that reach the internet without the aws CLI) both docs tools reported "fetch failed" and nothing else, and aws_docs_search went on to say the undocumented backend at proxy.search.docs.aws.com "may have changed or be unreachable" about a request that never left the machine. The unit test meant to cover this threw new Error("ECONNREFUSED"), a shape fetch never produces, so the suite stayed green over it. All four fetch branches now append the cause -- read defensively, a non-empty string code only so a numeric errno cannot print as a bare "0", with any userinfo@ redacted out of both halves -- to the LAST @ of the authority and with no : required, because WHATWG userinfo runs to the final @ (new URL("http://svc:Pa@ss@h:3128") gives password Pa%40ss on 22.22.2, so an unencoded @ in a password is a legitimate value) and because http://<token>@proxy is auth too; ? and # end the authority like / does, so a docs URL whose query or fragment holds an @ is untouched -- and name the remedy where the verdict has one: an untrusted certificate chain asks for NODE_EXTRA_CA_CERTS or NODE_USE_SYSTEM_CA=1 (Node 22.19+) and says both are read at process start, so they belong in the MCP config's env block rather than your shell; an expired certificate or a hostname mismatch deliberately gets no CA advice, because a CA fixes neither; HTTPS_PROXY set without NODE_USE_ENV_PROXY=1 (Node 22.21+) gets the fact that Node's fetch ignores it, which is why the aws CLI -- which reads HTTPS_PROXY itself -- can keep working while these two tools cannot, and that hint also reaches the 30s timeout branch, because on a proxy-only network the likelier symptom is nothing answering at all; and an uncoded transport failure under oam, which reports no code for one, gets the certificate remedy plus a note that an unreachable proxy looks identical from there. Every shape was reproduced locally on 2026-09-19 rather than read out of documentation: a self-signed HTTPS server on a loopback port (Node 22.22.2 gives DEPTH_ZERO_SELF_SIGNED_CERT, oam 0.16.2 gives no code at all, and NODE_EXTRA_CA_CERTS fixes both), a dead port and an unresolvable host, and HTTPS_PROXY pointed at a dead port with the opt-in present, absent, set to 0, and via NODE_OPTIONS.
  • aws_call no longer blames you for the params you did supply. aws_call with service: 's3api', operation: 'get-object', params: {Bucket, Key} exited 252 with the following arguments are required: --bucket, --key -- naming the two values that were in params -- and no suggestion at all, so a model reads it as its own mistake and retries with the same params. Same for bedrock-runtime invoke-model, bedrock-agentcore invoke-agent-runtime, lambda invoke, logs tail, s3 cp/ls/sync and configure get. The cause is not the parameters: the AWS CLI never registers --cli-input-json on an operation whose argument table carries an outfile (awscli/customizations/cliinput.py...
Read more

v2.3.3

Choose a tag to compare

@jeffyaw jeffyaw released this 20 Sep 05:18
v2.3.3
681590f

Security

  • A Cloud Control identifier, token or pagination cursor, or an aws_logs_tail filter pattern, that starts with file:// or fileb:// no longer makes the AWS CLI read a local file and send it to AWS. The CLI's paramfile loader (awscli/paramfile.py) replaces any command-line parameter value that begins with exactly file:// or fileb:// with the contents of that local file -- after expanding ~, $VAR and %VAR% -- before the request is signed, and v2 stores its no_paramfile setting without ever reading it, so there is no per-parameter opt-out. aws_resource_*'s identifier, requestToken, clientToken and nextToken, aws_paginate's startingToken and aws_logs_tail's filterPattern reached the command line unguarded, on tools annotated read-only that hosts commonly auto-approve: a prompt-injected identifier of file://~/.aws/credentials sent that file to AWS as the identifier, and the fileb:// form echoed the file's bytes back to the model in the error. The Cloud Control and pagination fields now reject the prefix before any process starts, in the same class as the existing leading-- reject and with errorKind absent the same way. As a backstop, runAwsCall refuses any such value among the CLI-level arguments a tool hands it (errorKind: "bad_input", naming the flag it belongs to) -- that is what closes filterPattern, which has no field-level reject of its own, and it also covers the values that come back from AWS, such as the request token awaitCompletion polls with. The match is exactly the loader's own case-sensitive, whitespace-free startswith, so nothing that reached AWS as itself before is refused now: FILE://, a leading space and file:/ with one slash all still pass through, verified against aws-cli 2.34.3 and 2.22.0 through a loopback stub. aws_lambda_invoke's own --payload fileb://<temp file> is exempted by its exact value, so --qualifier and the rest of that call stay guarded.
  • On Windows, an aws.exe in the server's working directory no longer stands in for the AWS CLI. Node resolves the bare command aws by looking in the current directory before PATH unless NoDefaultCurrentDirectoryInExePath is set. Claude Code sets it; hosts on the MCP SDK's default stdio transport pass only a short list of variables that does not include it, and start the server in their own working directory -- often the open project. So a repository with an aws.exe at its root ran that binary, with your AWS environment, on the first tool call: reproduced over MCP stdio on Node 22.22.2 against a build of this release with the fix removed, where a copy of node.exe planted as aws.exe answered aws_call sts get-caller-identity and the stub endpoint saw no requests at all. oam 0.16.2 never searched the working directory either way. The server now sets the variable for itself at startup when the host has not, as the first statement of the entry point, before any tool can spawn anything; an existing value is left alone whatever case the host spelled the key in. Windows evaluates the setting in the parent at spawn time, so the aws children are covered too, and the CLI inherits it, which also stops the CLI's own helpers resolving there: a planted session-manager-plugin.exe otherwise ran under aws ssm start-session and was handed the session's token. A credential_process configured as a bare program name that exists only in the working directory stops resolving; name it by absolute path.

Fixed

  • aws_lambda_invoke could run a function three times and still return nothing. A function slower than 60 s hit the AWS CLI's default socket read timeout, and the CLI's standard retry mode then sent Invoke again -- three attempts by default, five for a profile with max_attempts = 5. The tool's own description told callers to raise timeoutMs for a long function, which is exactly what set it off: through the v2.3.2 handler, aws-cli 2.34.3 against a local stub, a 70 s function with timeoutMs: 200000 received three Invoke POSTs 60 s apart and the call came back after 183 s as nonzero_exit Read timeout on endpoint URL, with no payload and no log tail. The invoke now runs with AWS_MAX_ATTEMPTS=1, which overrides any max_attempts in the caller's profile -- every spelling of the variable is dropped from the child environment first, because Node hands a child the upper-case key while oam hands it the last one -- and with a --cli-read-timeout derived from timeoutMs, so the request is sent once and the CLI waits as long as the caller asked. timeoutMs now means how long to wait for the function: the CLI gets 10 s more, enough for a cold start's init phase, so a function that hits its own timeout still comes back as a functionError with its log tail, and the tool's own kill timer fires 5 s after the CLI's as a backstop. Values above 900000, Lambda's synchronous maximum, are treated as 900000, which also stops a huge timeoutMs timing the call out at once -- Node fires a timer longer than 2^31-1 ms after 1 ms. No answer in time is errorKind: "timeout" whichever clock fired -- the CLI's read timeout used to surface as nonzero_exit -- and the message says whether the invoke was sent. That distinction is not cosmetic: --cli-read-timeout also bounds the CLI's credential calls, and a hung STS endpoint for a role_arn profile produces the identical Read timeout text with nothing sent to Lambda, so the handler reads the URL in the message rather than assuming the function ran.
  • Error codes and remedies survive the CLI giving up on its retries. Once botocore stops retrying it marks the error MaxAttemptsReached and writes ... when calling the X operation (reached max retries: N): ..., and the parser required operation: straight after the operation name, so every retry-exhausted error lost its code, its operation and its suggestion -- throttling and 5xx on every tool, which is exactly the case the backoff suggestion was written for. It matters more now that aws_lambda_invoke runs with AWS_MAX_ATTEMPTS=1: botocore marks the FIRST failure the same way when max_attempts is 1, so the CLI writes (reached max retries: 0) on every Lambda service error, and a missing function would have lost "Verify the resource identifier and region." On Windows the same pattern also swallowed the CLI's Additional error details: block, because the terminator looked for \n\n and the CLI writes \r\n\r\n. Both are fixed by the one regex, and the throttling suggestion now says how many times the CLI had already retried -- nothing is appended when it had not.
  • parseAwsError has a remedy for three transport failures that had none: Read timeout on endpoint URL and Connection was closed before we received a valid response (the request was sent, so a call that changes state may have taken effect -- check before retrying), and Connect timeout on endpoint URL (check network access, proxy and region). The remedies are deliberately generic about retry safety -- every tool but aws_lambda_invoke reaches them with the CLI's retries still on -- and the request they say was sent is the one to the endpoint the message names, which for a role_arn profile whose STS endpoint hangs is the credential call rather than the caller's operation.
  • aws_lambda_invoke accepts the qualifier $LATEST.PUBLISHED and function ARNs up to 256 characters. Both limits predate Lambda Managed Instances, and both rejected valid input here before anything spawned: FunctionName now allows the 256 characters the Invoke API documents (it was 170, which a 64-character name with a 59-character alias already exceeds), and the qualifier charset gains . for $LATEST.PUBLISHED -- sent as ?Qualifier=%24LATEST.PUBLISHED, verified through the installed CLI. : and / stay out, so a qualifier still cannot spell file://. The qualifier description no longer says an unqualified invoke always runs $LATEST -- on Managed Instances it runs $LATEST.PUBLISHED, and durable functions need an explicit qualifier -- the tool description says Managed Instances functions do not support the log tail, and invocationType no longer offers aws_iam_simulate as a DryRun substitute: the simulator evaluates the caller's identity policies and never fetches the function's resource-based policy.
  • Pagination cursors are accepted up to 8,192 characters, up from 2,048. Cloud Control documents ListResources NextToken at up to 4,096 characters, and aws-cli returned and accepted one that long against a local stub, but aws_resource_list rejected it locally before any process spawned. aws_paginate needs more headroom still: the CLI wraps a raw token as base64 JSON for --starting-token, so a 4,096-character cursor arrives as 5,484. It is the 128-character bug 2.0.0 fixed, one limit higher. The identifier cap (2,048) and the requestToken / clientToken cap (128) are unchanged.

Changed

  • aws_lambda_invoke no longer lets the AWS CLI retry a throttle, a 5xx or a dropped connection. This is a behavior change, not merely a fix. The CLI re-sent all three, and every re-send of an Invoke that reached the function is another run -- a dropped connection, a 429 TooManyRequestsException and a 500 ServiceException were each sent three times on the CLI's defaults. They now reach the caller on the first failure. For a TooManyRequestsException this gives something up: Lambda refused before running anything, so the old silent retry was safe, but no environment variable or flag limits retries to some error codes and not others. The tool description now says a throttle is safe to retry, and that after a dropped connection or a 5xx the function may have run, so check its logs first.
  • aws_lambda_invoke's timeoutMs now bounds the wait for the function, not the whole call. This is a behavior change, not merely a fix: the CLI is allowed 10 s beyond the value in force ...
Read more