feat(smoke): read-only IAM-authenticated status probe + trailing-blank message fix - #8
Conversation
Includes tests/smoke.test.ts shell harness that stubs aws/curl/sleep on a temp PATH and drives bash scripts/smoke.sh, asserting every branch and the read-only invariant (no lambda invoke, no fetch payload).
Implements the message-formatting design (trailing \\n\\n appended to buildFinalMessageForDiscord so adjacent Discord posts have a visible bottom edge). All truncating paths still end with the blank — two chars reserved from the 2000-char budget. Tests assert the trailing blank on every shape (no-match, full-suffix, clipped-suffix, oversized-preMessage, custom-limit snowball regression).
…+ plan Captures the spec/planning docs for two changes shipped in this branch: - trailing-blank-line message formatting (design only; implemented in the prior commit's src/agent/fetch.ts change). - smoke-status-iam: rewrite scripts/smoke.sh as a read-only IAM-authenticated status probe (design + implementation plan).
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 8 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (21)
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe Function URL now uses same-account ChangesFunction URL IAM boundary
Read-only smoke probe
Discord message formatting
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant SmokeScript
participant AWSCLI
participant FunctionURL
participant Handler
SmokeScript->>AWSCLI: Resolve URL and AWS credentials
SmokeScript->>FunctionURL: Send unsigned status request
FunctionURL-->>SmokeScript: Return 403
SmokeScript->>FunctionURL: Send SigV4-signed status request
FunctionURL->>Handler: Deliver authorized status request
Handler-->>SmokeScript: Return status JSON
SmokeScript->>SmokeScript: Validate schema and side-effect constraints
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Comment |
Mark the spec Implemented and check off the acceptance criteria. Add clarifications for the implementation details that diverged from the original spec prose: - §3.2 Step 5: schema-check must use jq has() to distinguish a missing field from snapshotVersion: null; the // fallback collapses both and would falsely reject the pre-tick empty-state response. - §3.3: pin the verbatim handler comment text that was added. - §5.2: pin Match.anyValue() for TargetFunctionArn (CDK logical id drifts with hash changes) and add a 5.2.1 subsection listing the test infrastructure prerequisites (export AgentStack, globalSetup + vitest.config.ts to set DISCORD_WEBHOOK_URL before the module-load synth, long per-test timeout for DockerImageCode, and a note about cdk.out cleanup between manual synth and the test run).
Documents the split-brain risk inherent in the SQLite-rehydrated-by-S3 pattern (S3 has no partial file locking, so concurrent Lambdas silently overwrite each other's /tmp writes on PutObject) and lays out three upgrade paths: EFS mount, Litestream, or a serverless DB migration. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 15
🧹 Nitpick comments (1)
tests/smoke.test.ts (1)
231-250: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winUse a deterministic clock for the retry-exhaustion test.
SLEEP_OKreturns immediately, butsmoke.shuses real wall-clock time for its deadline. This test therefore spins through signedcurlcalls for about 75 seconds. Add adateshim and make thesleepshim advance that clock. Assert the expected number of retry attempts.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/smoke.test.ts` around lines 231 - 250, Update the retry-exhaustion test around runSmoke to use a deterministic date shim and have the sleep shim advance the mocked clock instead of relying on wall-clock time. Preserve the 429 response behavior, then assert the expected bounded number of signed retry attempts in addition to the existing failure-message checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/superpowers/plans/2026-08-09-smoke-status-iam.md`:
- Around line 523-536: Update runSmoke and the exhaustion test setup so the
subprocess timeout exceeds the smoke script’s full 75-second retry window, or
inject a configurable fake clock/retry window for tests. Ensure the exhaustion
scenario completes naturally and emits its expected failure message instead of
being killed early.
- Around line 523-536: Update runSmoke to preserve the real toolchain by
constructing PATH from env.binDir followed by the captured originalPath, rather
than replacing PATH with only env.binDir. Reuse the existing originalPath value
while keeping the shim directory first so shims continue to take precedence.
- Around line 663-674: Update the success-scenario assertions around runSmoke
and parseInvocations to require exactly two curl probes in order: the unsigned
probe first and the signed probe second. Validate each call’s signing
configuration and expected status mapping, while preserving the existing
success-status and read-only AWS assertions.
- Around line 1013-1016: Update the Step 2 CDK synthesis command so it preserves
the exit status from npx cdk synth instead of head. Capture output while
checking the synth status first, or use a pipefail-safe approach that still
treats successful synthesis as success when truncating output.
- Around line 472-514: The smoke-test shim setup must create usable command
wrappers and dispatch to the selected scenario fragment. Update setupShims to
create binDir and install wrappers for aws, curl, and sleep regardless of spec,
then update shimSource and the runSmoke/withBehaviorFile flow to use the
scenario fragment selected via SHIM_BEHAVIOR_FILE_DIR without recursively
invoking the fragment itself; preserve invocation logging and pass the command
name and arguments to the fragment.
- Around line 290-299: Update the unsigned and signed status probe assignments
around UNSIGNED_STATUS and the corresponding signed-status variable to wrap each
curl command in an if ! ...; then failure block, add --show-error, and emit the
promised actionable transport-error message before exiting. Preserve the
existing HTTP status validation branches for successful curl requests.
- Around line 188-199: Update the fallback instructions in Step 3 to import
Match from aws-cdk-lib/assertions instead of aws-cdk-lib, matching the import
used by tests/infra.test.ts; leave the generic AWS::Lambda::Url assertion
unchanged.
- Around line 371-397: Update the status-response validation around
SNAPSHOT_VERSION, SOURCES_LEN, and RECENT_LEN to use a single jq -e predicate
that requires an object with present snapshotVersion, sources, and
recentNotifications fields, with the latter two being arrays. Read
snapshotVersion while preserving JSON null rather than converting it to
"__missing__", so the existing empty-state branch can accept null; retain the
failure output for invalid shapes and the later weather-source validation.
- Around line 451-452: Update the path setup defining REPO_ROOT and SMOKE_SCRIPT
to derive the current module directory from import.meta.url instead of
__dirname, preserving the existing repository-root and smoke-script locations.
In `@infra/stack.ts`:
- Around line 25-32: Remove the module-level AgentStack construction from
infra/stack.ts so importing the AgentStack class has no side effects. Move the
CDK CLI bootstrap currently at the bottom of the module into a separate
entrypoint that creates the app and instantiates AgentStack for deployment,
while preserving the existing exported AgentStack definition for tests.
In `@README.md`:
- Line 82: Update README.md lines 82-82 to replace “The daily fetch run” with
wording reflecting the scheduled five-minute cadence. Also update
docs/01-architecture.md lines 3-5, including the later paragraph at lines 21-23,
so it no longer says the bot runs once a day and consistently describes the
five-minute schedule.
- Around line 83-87: Update the on-demand invocation example to use a
SigV4-signed request to the AWS_IAM-protected Function URL, including the
session token when credentials provide one. State that invocation requires both
FETCH_TRIGGER_TOKEN and an authorized same-account IAM principal, and revise the
warning so it no longer claims anyone with a valid token can invoke.
In `@scripts/smoke.sh`:
- Around line 145-161: Update the status validation near SNAPSHOT_PRESENT,
SOURCES_PRESENT, and RECENT_PRESENT to use a single jq -e schema predicate
requiring snapshotVersion to be string or null and both sources and
recentNotifications to be arrays. Fail with the existing diagnostic when the
predicate fails, then preserve the subsequent .snapshotVersion and .sources
processing for valid responses.
- Around line 58-62: Update both curl requests in the smoke script, including
the unsigned status request and the signed request, to specify --connect-timeout
and --max-time. For the signed request, calculate --max-time from the remaining
retry budget so an individual attempt cannot exceed the overall 75-second
window; preserve the existing retry behavior and request arguments.
In `@src/agent/fetch.ts`:
- Around line 71-73: Validate limit before calculating effectiveLimit in the
function containing this truncation logic, requiring an integer at least
TRAILING_BLANK.length and rejecting smaller values. Preserve the existing
truncation behavior for valid limits, and add boundary tests covering limits
below and equal to 2.
---
Nitpick comments:
In `@tests/smoke.test.ts`:
- Around line 231-250: Update the retry-exhaustion test around runSmoke to use a
deterministic date shim and have the sleep shim advance the mocked clock instead
of relying on wall-clock time. Preserve the 429 response behavior, then assert
the expected bounded number of signed retry attempts in addition to the existing
failure-message checks.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 87f5aad5-b140-4e9b-8796-236f28b7a034
📒 Files selected for processing (16)
README.mddocs/01-architecture.mddocs/02-rehydration.mddocs/07-budget-protection.mddocs/superpowers/plans/2026-08-09-smoke-status-iam.mddocs/superpowers/specs/2026-08-09-message-formatting-design.mddocs/superpowers/specs/2026-08-09-smoke-status-iam-design.mdinfra/stack.tsscripts/smoke.shsrc/agent/fetch.tssrc/handler.tstests/fetch.test.tstests/globalSetup.tstests/infra.test.tstests/smoke.test.tsvitest.config.ts
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/superpowers/specs/2026-08-09-smoke-status-iam-design.md`:
- Line 226: Change the checklist item in the smoke status acceptance criteria
from completed to pending, since the live post-deploy smoke check has not yet
been performed. Keep the existing pre-deploy verification note unchanged, or
separate the automated and operator checks so only verified work is marked
complete.
In `@README.md`:
- Line 123: Correct the spelling in the “How it works” description by changing
“repligates” to “replicates,” without modifying the surrounding text.
- Around line 115-119: Revise the “EFS Mount: The Zero-Server Alternative”
section to remove the claim that EFS safely supports one SQLite file across
hundreds of concurrent Lambda instances and that EFS natively handles SQLite
locking. Either document and validate a compatible non-WAL journal mode, locking
behavior, failure recovery, and backups, or replace the recommendation with a
client/server database for concurrent writers.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 6a757922-adc8-4540-896a-39cb8bd50ac0
📒 Files selected for processing (2)
README.mddocs/superpowers/specs/2026-08-09-smoke-status-iam-design.md
…k RAG) Loop mode removed dedup and moved RAG from per-source/prompt-injected to per-tick/mechanical, but the docs (and a couple of code comments) still described the old design. Rewrites README, docs/01, 03, 05-09, and stale comments in infra/stack.ts and src/agent/status.ts to match current behavior: no dedup, one format+embed call per tick (not per source), global (not same-source) KNN matching, and the mechanical base_message- based suffix that prevents snowballing. Also fixes a broken on-demand fetch curl example (needed SigV4 signing) and a few stale line/typo references. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- src/agent/fetch.ts: validate limit >= TRAILING_BLANK.length on the buildFinalMessageForDiscord helper and throw RangeError otherwise. Boundary tests cover limits < 2, = 2, and non-integer values. - scripts/smoke.sh: add --connect-timeout / --max-time to both curl calls (unsigned probe + signed retry loop), with --max-time set from the remaining retry budget on the signed probe. Wrap both calls in if ! ...; then blocks with --show-error so a transport error emits the actionable failure message instead of set -e aborting silently. - scripts/smoke.sh: replace the has() + per-field read schema check with one jq -e predicate that validates object shape, field presence, AND array types (snapshotVersion string|null, sources + recentNotifications arrays). Read snapshotVersion without collapsing null. - infra/stack.ts: remove the module-level cdk.App + new AgentStack bootstrap so importing the class has no side effects. Move the CLI entrypoint to a new infra/app.ts. - infra/cdk.json + scripts/deploy.sh: point at the new infra/app.ts entrypoint instead of infra/stack.ts. - README.md: replace the on-demand fetch example with a proper SigV4 signing flow that mirrors scripts/smoke.sh (export-credentials, netrc, session-token file). Update the warning to require both FETCH_TRIGGER_TOKEN and an authorized same-account IAM principal. - README.md: qualify the EFS recommendation — EFS is not a true multi-writer substitute; SQLite WAL on NFS is unsafe, and the hydrating-Lambda pattern is the only SQLite-on-Lambda shape the tutorial guarantees. Recommend a client/server database for genuine concurrent writers. - docs/superpowers/specs/2026-08-09-smoke-status-iam-design.md: split the live post-deploy acceptance criterion into a pending operator action so the checklist no longer marks it complete. - docs/superpowers/plans/2026-08-09-smoke-status-iam.md: sync the plan body to reflect the actual code (curl transport handling, schema predicate, Match import path, runSmoke timeout + PATH, success-scenario curl ordering, cdk synth pipefail-safe pattern). Co-Authored-By: Claude <noreply@anthropic.com>
/fix-pr follow-upCommit: Review resolution
Verification
All 13 previously unresolved CodeRabbit threads are now resolved. |
There was a problem hiding this comment.
Pull request overview
This PR tightens the Lambda Function URL security boundary by switching to IAM-authenticated (SigV4) access and rewrites the smoke script into a strictly read-only status probe, while also adjusting Discord message formatting to always end with a trailing blank line for clearer visual separation.
Changes:
- Lock the Function URL to
AWS_IAMand grant same-account invocation permissions; update handler/docs to reflect the SigV4 boundary. - Rewrite
scripts/smoke.shinto a read-only status probe (unsigned403assertion + signed200with bounded429retries + schema validation) and add deterministic test coverage. - Append
\n\nto all Discord final messages (with limit-budget reservation) and update tests/docs accordingly.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
vitest.config.ts |
Adds global setup for synth-related env seeding (comment currently misstates why). |
tests/smoke.test.ts |
New deterministic harness for scripts/smoke.sh (currently has ESM + shim-script generation bugs). |
tests/infra.test.ts |
New CDK synth assertions pinning Function URL IAM auth + grants. |
tests/globalSetup.ts |
Seeds DISCORD_WEBHOOK_URL for tests (comment currently misstates when the check runs). |
tests/fetch.test.ts |
Updates expectations to include trailing blank line in formatted messages. |
src/handler.ts |
Comment update clarifying SigV4/IAM boundary vs FETCH_TRIGGER_TOKEN. |
src/agent/status.ts |
Documentation/comment tweaks around nearest-match semantics and bootstrap naming. |
src/agent/fetch.ts |
Adds trailing blank line behavior and limit-budget handling in buildFinalMessageForDiscord. |
scripts/smoke.sh |
Rewritten as read-only IAM-authenticated status probe with bounded retry + schema checks. |
scripts/deploy.sh |
Updates CDK app entrypoint to infra/app.ts. |
README.md |
Updates operator docs: IAM-auth Function URL, read-only smoke, on-demand fetch guidance. |
infra/stack.ts |
Switches Function URL to AWS_IAM, adds same-account URL grant; exports AgentStack (JSDoc currently stale). |
infra/cdk.json |
Points CDK CLI to infra/app.ts. |
infra/app.ts |
New CDK CLI entrypoint, separating stack definition from app instantiation. |
docs/superpowers/specs/2026-08-09-smoke-status-iam-design.md |
Captures IAM smoke/status design rationale and acceptance criteria. |
docs/superpowers/specs/2026-08-09-message-formatting-design.md |
Captures trailing-blank message design rationale. |
docs/superpowers/plans/2026-08-09-smoke-status-iam.md |
Detailed implementation plan for the IAM smoke/status work. |
docs/09-lesson-script.md |
Updates teaching material to match loop + RAG mechanics as implemented. |
docs/08-rag-vector-search.md |
Updates RAG documentation to match tick-level embedding/search/store and global matching. |
docs/07-budget-protection.md |
Clarifies cost-risk surface now requires IAM auth + token for on-demand fetch. |
docs/06-discord-webhook-setup.md |
Clarifies synth-time env usage wording. |
docs/05-from-tutorial-to-prod.md |
Updates cost framing for 5-minute cadence and wording tweaks. |
docs/03-schema.md |
Updates schema docs for base_message + nearest match fields + “no dedup” explanation. |
docs/02-rehydration.md |
Notes IAM-auth Function URL and smoke probe behavior for status reads. |
docs/01-architecture.md |
Updates architecture description: IAM-auth URL, loop cadence, Bedrock call shape. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- tests/smoke.test.ts: replace ESM-undefined __dirname with
fileURLToPath(new URL('.', import.meta.url)) so the test can locate
scripts/smoke.sh at module load (package.json has "type": "module").
- infra/stack.ts: collapse the duplicated JSDoc on AgentStack and
remove the stale claim that the CDK CLI bootstrap still lives at
the bottom of this file (it moved to infra/app.ts in the previous
commit).
- vitest.config.ts + tests/globalSetup.ts: update comments to
describe the DISCORD_WEBHOOK_URL check happening in the AgentStack
constructor, not at module load.
Co-Authored-By: Claude <noreply@anthropic.com>
/fix-pr follow-upCommit: Review resolution
Verification
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated no new comments.
Suppressed comments (8)
README.md:135
- This example writes the AWS secret key and optional session token to temp files but never removes them. Anyone copying the documented command leaves live credentials on disk after the request; add cleanup (ideally a trap as in
scripts/smoke.sh) before the snippet ends.
--data '{"op":"fetch"}'
scripts/smoke.sh:141
- This branch handles every non-429 status, but the message diagnoses all of them as a missing IAM grant. A signed
500means authentication succeeded and the function failed, so this directs the operator to redeploy instead of checking Lambda logs. Give403the IAM guidance and use a function-error message for other statuses.
if [ "$STATUS_CODE" != "429" ]; then
echo "FAIL: signed status probe returned $STATUS_CODE (expected 200 after retries). The Function URL grant may be missing — re-run \`npm run deploy\` so \`functionUrl.grantInvokeUrl\` is in place, and verify your IAM principal has lambda:InvokeFunctionUrl / lambda:InvokeFunction on the URL." >&2
exit 1
scripts/smoke.sh:168
- The documented empty state requires both arrays to be empty, but this predicate accepts any arrays when
snapshotVersionis null. The later branch also treats null alone as empty state, so malformed responses such as{"snapshotVersion":null,"sources":[{"name":"crypto"}],"recentNotifications":[]}pass the smoke test. Enforce the null-state array lengths here.
and has("snapshotVersion")
and (.snapshotVersion == null or (.snapshotVersion | type) == "string")
and has("sources") and ((.sources | type) == "array")
and has("recentNotifications")
and ((.recentNotifications | type) == "array")
tests/smoke.test.ts:162
- The read-only invariant is only checked in the first test and in a final test that reruns the success path. The 429-exhaustion, signed-403, public-URL, and malformed-schema scenarios never inspect their invocation logs, despite the PR stating every scenario does. Checking the log in shared teardown ensures a write added to any failure branch is caught.
afterEach(() => {
if (env) {
process.env.PATH = env.originalPath;
process.chdir(env.originalCwd);
rmSync(env.dir, { recursive: true, force: true });
vitest.config.ts:13
- The new
infra/stack.tsis side-effect-free, andtests/infra.test.tsalready sets this variable immediately before constructingAgentStack, so this global hook is no longer needed. Vitest propagates environment mutations from global setup to test workers; contrary to the comment, it makes the fake webhook available to every test file and can mask code that unintentionally reads the ambient environment. Remove the registration and the now-unused setup file.
// `AgentStack`'s constructor reads DISCORD_WEBHOOK_URL (via loadConfig at synth
// time), so the env var must be set before tests/infra.test.ts instantiates the
// stack. Setting it in globalSetup guarantees it's present for every test file
// without leaking the webhook URL into other test files' environments.
globalSetup: ['./tests/globalSetup.ts'],
docs/08-rag-vector-search.md:77
- This describes storage failures as tick-level and says they remove the suffix, but
runFetchstores embeddings after posting and catches eachinsertEmbeddingfailure per source (src/agent/fetch.ts:291-300). Such a failure preserves the already-posted suffix and notification row; only embed/match failure degrades the post to no suffix. Update this section to distinguish the two boundaries.
Both the search and the store lookups are wrapped in the same tick-level error isolation
`runFetch` already has for formatter/post failures: a Titan failure at the search-or-store
step is caught, logged into `agent_runs.error`, and the tick still posts to Discord with no
suffix — it never blocks the post. See [03-schema.md](03-schema.md)'s explanation of why
docs/superpowers/specs/2026-08-09-smoke-status-iam-design.md:175
- This verification command no longer synthesizes a stack: this PR moved the CDK entrypoint to
infra/app.tsand madeinfra/stack.tsside-effect-free. Following the new design document as written therefore cannot verify the IAM resources.
- `npx cdk synth --app "npx tsx infra/stack.ts"` — synthesize only, no deploy required for CI.
docs/09-lesson-script.md:328
- The final sentence contradicts the preceding explanation and the status query:
nearestMatch.formattedMessageis read from the matched row'sformatted_message, so it is exactly the text that appeared in Discord, not one suffix different. The difference is between the current post's quotedbase_messageand this status field.
**1a. A subtlety worth naming: `nearestMatch.formattedMessage` is the matched tick's *posted* text, suffix included — not the `base_message` that tick actually used to build its own suffix.** The writer's suffix-building step (Lesson 6) always reads `base_message`, but the reader's join reads `formatted_message` for the matched row, because that's the human-readable text a status consumer wants to see (the same text that appeared in Discord). If you're comparing what the channel showed against what the JSON shows for a matched notification, expect them to differ by exactly one suffix.
Summary
Two related changes shipped together:
scripts/smoke.shis now a read-only IAM-authenticated status probe. It no longer invokesfetch, posts to Discord, or calls Bedrock. It proves two things:AWS_IAM(asserts an unsigned probe returns403).429s within a bounded window, asserts the documented status schema).Trailing blank line on every Discord message. A one-line writer change in
src/agent/fetch.tsso adjacent messages in the channel have a visible bottom edge; truncating paths still end with the blank (two chars reserved from the 2000-char budget).Changes
Stack + handler (SigV4 boundary):
infra/stack.ts—addFunctionUrlswitched toauthType: AWS_IAM;functionUrl.grantInvokeUrl(new iam.AccountPrincipal(this.account))synthesizes bothlambda:InvokeFunctionUrland the URL-scopedlambda:InvokeFunctionpermission.src/handler.ts— comment in the HTTP-triggeredfetchgating block now describes the SigV4 boundary and the relationship to the on-demandFETCH_TRIGGER_TOKEN(defense in depth, not a substitute).Smoke script (read-only invariant):
scripts/smoke.sh— full rewrite: unsigned probe → assert403; signed probe with--aws-sigv4 aws:amz:$REGION:lambda→ retry429s only for ~75s, otherwise fail; schema validation accepts empty-state (snapshotVersion: null) and populated (weather.lastValue) responses.Tests:
tests/infra.test.ts(new) — synth suite pinningAuthType: AWS_IAM, both URL invocation permissions, and the unchanged EventBridge state.tests/smoke.test.ts(new) — shell harness that stubsaws/curl/sleepon a tempPATHand drivesbash scripts/smoke.sh. Every scenario assertsaws lambda invokeand{"op":"fetch"}are never sent.tests/globalSetup.ts(new) +vitest.config.ts— setsDISCORD_WEBHOOK_URLbefore any test importsinfra/stack.ts, which synthesizes the stack at module load.Docs:
README.md,docs/01-architecture.md,docs/02-rehydration.md,docs/07-budget-protection.md— describe the Function URL as IAM-authenticated; clarify that the on-demandFETCH_TRIGGER_TOKENis application-level defense in depth on top of the IAM grant.Message formatting:
src/agent/fetch.ts+tests/fetch.test.ts— append+ "\n\n"to every return path ofbuildFinalMessageForDiscord; tests assert the trailing blank on every shape.Captured design/plan docs (for context):
docs/superpowers/specs/2026-08-09-message-formatting-design.mddocs/superpowers/specs/2026-08-09-smoke-status-iam-design.mddocs/superpowers/plans/2026-08-09-smoke-status-iam.mdVerification
npm test— 134/134 pass (16 test files, including newtests/infra.test.ts+tests/smoke.test.ts).npm run typecheckandnpm run build— clean.bash -n scripts/smoke.sh— clean.cdk synth— synthesizesAuthType: AWS_IAMand bothlambda:InvokeFunctionUrl/lambda:InvokeFunctionpermissions.Operator-facing manual check (post-
npm run deploy)Both runs perform zero writes, zero Discord posts, zero Bedrock calls.
🤖 Generated with Claude Code