Skip to content

feat(smoke): read-only IAM-authenticated status probe + trailing-blank message fix - #8

Merged
equationalapplications merged 12 commits into
mainfrom
feat/smoke-status-iam
Aug 9, 2026
Merged

feat(smoke): read-only IAM-authenticated status probe + trailing-blank message fix#8
equationalapplications merged 12 commits into
mainfrom
feat/smoke-status-iam

Conversation

@equationalapplications

Copy link
Copy Markdown
Owner

Summary

Two related changes shipped together:

  1. scripts/smoke.sh is now a read-only IAM-authenticated status probe. It no longer invokes fetch, posts to Discord, or calls Bedrock. It proves two things:

    • The Function URL actually enforces AWS_IAM (asserts an unsigned probe returns 403).
    • An authorized same-account principal can read the status (signed probe, retries only 429s within a bounded window, asserts the documented status schema).
  2. Trailing blank line on every Discord message. A one-line writer change in src/agent/fetch.ts so 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.tsaddFunctionUrl switched to authType: AWS_IAM; functionUrl.grantInvokeUrl(new iam.AccountPrincipal(this.account)) synthesizes both lambda:InvokeFunctionUrl and the URL-scoped lambda:InvokeFunction permission.
  • src/handler.ts — comment in the HTTP-triggered fetch gating block now describes the SigV4 boundary and the relationship to the on-demand FETCH_TRIGGER_TOKEN (defense in depth, not a substitute).

Smoke script (read-only invariant):

  • scripts/smoke.sh — full rewrite: unsigned probe → assert 403; signed probe with --aws-sigv4 aws:amz:$REGION:lambda → retry 429s 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 pinning AuthType: AWS_IAM, both URL invocation permissions, and the unchanged EventBridge state.
  • tests/smoke.test.ts (new) — shell harness that stubs aws/curl/sleep on a temp PATH and drives bash scripts/smoke.sh. Every scenario asserts aws lambda invoke and {"op":"fetch"} are never sent.
  • tests/globalSetup.ts (new) + vitest.config.ts — sets DISCORD_WEBHOOK_URL before any test imports infra/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-demand FETCH_TRIGGER_TOKEN is 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 of buildFinalMessageForDiscord; tests assert the trailing blank on every shape.

Captured design/plan docs (for context):

  • docs/superpowers/specs/2026-08-09-message-formatting-design.md
  • docs/superpowers/specs/2026-08-09-smoke-status-iam-design.md
  • docs/superpowers/plans/2026-08-09-smoke-status-iam.md

Verification

  • npm test — 134/134 pass (16 test files, including new tests/infra.test.ts + tests/smoke.test.ts).
  • npm run typecheck and npm run build — clean.
  • bash -n scripts/smoke.sh — clean.
  • cdk synth — synthesizes AuthType: AWS_IAM and both lambda:InvokeFunctionUrl / lambda:InvokeFunction permissions.

Operator-facing manual check (post-npm run deploy)

npm run smoke     # empty-state read path (200, snapshotVersion: null)
# while a loop tick is in flight:
npm run smoke     # 429 retry path (200 after retries)

Both runs perform zero writes, zero Discord posts, zero Bedrock calls.

🤖 Generated with Claude Code

claude added 7 commits August 9, 2026 11:10
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).
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 8 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fc9fe6b6-5e02-4320-8e4b-fd42bec18230

📥 Commits

Reviewing files that changed from the base of the PR and between e85e6aa and c8bacab.

📒 Files selected for processing (21)
  • README.md
  • docs/01-architecture.md
  • docs/03-schema.md
  • docs/05-from-tutorial-to-prod.md
  • docs/06-discord-webhook-setup.md
  • docs/07-budget-protection.md
  • docs/08-rag-vector-search.md
  • docs/09-lesson-script.md
  • docs/superpowers/plans/2026-08-09-smoke-status-iam.md
  • docs/superpowers/specs/2026-08-09-smoke-status-iam-design.md
  • infra/app.ts
  • infra/cdk.json
  • infra/stack.ts
  • scripts/deploy.sh
  • scripts/smoke.sh
  • src/agent/fetch.ts
  • src/agent/status.ts
  • tests/fetch.test.ts
  • tests/globalSetup.ts
  • tests/smoke.test.ts
  • vitest.config.ts
📝 Walkthrough

Summary by CodeRabbit

  • Security

    • Function URL access now requires AWS IAM authentication and SigV4-signed requests.
    • On-demand fetches require both IAM authorization and the configured application token.
  • Monitoring

    • Smoke checks are read-only, validate authorization and status responses, and safely retry temporary rate limits.
  • User Experience

    • Generated Discord messages now consistently include a trailing blank line, including truncated messages.
  • Documentation

    • Updated architecture, security, rehydration, budget, and smoke-test guidance.
    • Documented five-minute scheduled updates, SQLite/S3 single-writer limitations, and scaling alternatives.

Walkthrough

The Function URL now uses same-account AWS_IAM authorization. scripts/smoke.sh performs signed, read-only status checks with bounded retries and schema validation. Infrastructure and shell tests cover authorization and side-effect constraints. Discord messages now end with trailing blank lines.

Changes

Function URL IAM boundary

Layer / File(s) Summary
IAM authentication and permissions
infra/stack.ts, tests/infra.test.ts
The Function URL uses AWS_IAM. Same-account permissions cover Function URL invocation. Tests verify permissions, outputs, and the five-minute schedule.
Authorization documentation
README.md, docs/01-architecture.md, docs/02-rehydration.md, docs/07-budget-protection.md, src/handler.ts
Documentation and handler comments describe SigV4 access and FETCH_TRIGGER_TOKEN as defense in depth.

Read-only smoke probe

Layer / File(s) Summary
Signed status validation
scripts/smoke.sh
The script checks unsigned 403, signs status requests, retries 429 responses until a deadline, validates JSON fields, and rejects invalid responses.
Deterministic smoke tests
tests/smoke.test.ts, tests/globalSetup.ts, vitest.config.ts
The tests use command shims to cover success, retry, authorization, schema, regression, and no-fetch paths. Global setup provides the webhook placeholder needed during imports.
Implementation plan and design
docs/superpowers/plans/2026-08-09-smoke-status-iam.md, docs/superpowers/specs/2026-08-09-smoke-status-iam-design.md
The plan and design record the IAM boundary, smoke flow, harness, acceptance criteria, and verification steps.

Discord message formatting

Layer / File(s) Summary
Trailing blank lines
src/agent/fetch.ts, tests/fetch.test.ts, docs/superpowers/specs/2026-08-09-message-formatting-design.md
Generated Discord messages append \n\n. Truncation and reminder-suffix behavior remain within the configured limit, with updated assertions.

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
Loading

Possibly related PRs

Suggested reviewers: claude

Poem

A rabbit signs requests at dawn,
Checks the gate, then hops along.
No fetch, no webhook, no cloudy call,
Just status fields, checked one and all.
Two blank lines trail each message bright—
IAM keeps the burrow right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes both primary changes: the read-only IAM-authenticated smoke probe and the trailing-blank message fix.
Description check ✅ Passed The description directly explains the smoke probe, IAM configuration, tests, documentation, and Discord message formatting changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

claude added 2 commits August 9, 2026 11:40
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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

🧹 Nitpick comments (1)
tests/smoke.test.ts (1)

231-250: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use a deterministic clock for the retry-exhaustion test.

SLEEP_OK returns immediately, but smoke.sh uses real wall-clock time for its deadline. This test therefore spins through signed curl calls for about 75 seconds. Add a date shim and make the sleep shim 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

📥 Commits

Reviewing files that changed from the base of the PR and between fb1cf14 and 831af5d.

📒 Files selected for processing (16)
  • README.md
  • docs/01-architecture.md
  • docs/02-rehydration.md
  • docs/07-budget-protection.md
  • docs/superpowers/plans/2026-08-09-smoke-status-iam.md
  • docs/superpowers/specs/2026-08-09-message-formatting-design.md
  • docs/superpowers/specs/2026-08-09-smoke-status-iam-design.md
  • infra/stack.ts
  • scripts/smoke.sh
  • src/agent/fetch.ts
  • src/handler.ts
  • tests/fetch.test.ts
  • tests/globalSetup.ts
  • tests/infra.test.ts
  • tests/smoke.test.ts
  • vitest.config.ts

Comment thread docs/superpowers/plans/2026-08-09-smoke-status-iam.md Outdated
Comment thread docs/superpowers/plans/2026-08-09-smoke-status-iam.md Outdated
Comment thread docs/superpowers/plans/2026-08-09-smoke-status-iam.md Outdated
Comment thread docs/superpowers/plans/2026-08-09-smoke-status-iam.md
Comment thread docs/superpowers/plans/2026-08-09-smoke-status-iam.md Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread scripts/smoke.sh Outdated
Comment thread scripts/smoke.sh Outdated
Comment thread src/agent/fetch.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 831af5d and e85e6aa.

📒 Files selected for processing (2)
  • README.md
  • docs/superpowers/specs/2026-08-09-smoke-status-iam-design.md

Comment thread docs/superpowers/specs/2026-08-09-smoke-status-iam-design.md Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
claude added 2 commits August 9, 2026 11:56
…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>
@equationalapplications

Copy link
Copy Markdown
Owner Author

/fix-pr follow-up

Commit: 71f190a56358b04f319d23a518898e4dc28922c6

Review resolution

  • src/agent/fetch.ts limit validationFixed: Reject limit < TRAILING_BLANK.length with RangeError; added boundary tests for limits < 2, = 2, and non-integer values.
  • scripts/smoke.sh curl timeoutsFixed: Added --connect-timeout 5 and --max-time to both unsigned and signed probes; signed probe --max-time is computed from the remaining retry budget. Wrapped both calls in if ! ...; then with --show-error so transport errors emit the actionable failure message.
  • scripts/smoke.sh schema validationFixed: Replaced the has() + per-field read with a single jq -e predicate that validates object shape, field presence, AND array types (snapshotVersion string|null, sources + recentNotifications arrays). snapshotVersion is now read without collapsing null.
  • infra/stack.ts module-level AgentStackFixed: Removed the module-level cdk.App + new AgentStack bootstrap. CLI entrypoint moved to new infra/app.ts; infra/cdk.json and scripts/deploy.sh point at the new entry. Importing AgentStack now has no side effects.
  • README.md SigV4 invocation exampleFixed: Replaced the bare --user curl with the same SigV4 flow scripts/smoke.sh uses (export-credentials → netrc + session-token file). Updated the warning to require both FETCH_TRIGGER_TOKEN and an authorized same-account IAM principal.
  • README.md EFS recommendationFixed: Removed the unsupported claim that EFS safely scales one SQLite file across hundreds of concurrent Lambda instances and natively handles SQLite locking. SQLite WAL on NFS is unsafe; the hydrating-Lambda pattern is the only SQLite-on-Lambda shape the tutorial guarantees. Genuine concurrent writers should use a client/server database.
  • README.md spellingAlready fixed: The file already said "replicates" (commit d0edd43); no change needed.
  • smoke-status-iam spec acceptance checklistFixed: 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.mdFixed: Synced 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).

Verification

  • typecheck — pass (npm run typecheck)
  • build — pass (npm run build)
  • tests — pass (npm test, 137/137 across 16 test files)
  • smoke.sh syntax — pass (bash -n scripts/smoke.sh)

All 13 previously unresolved CodeRabbit threads are now resolved.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_IAM and grant same-account invocation permissions; update handler/docs to reflect the SigV4 boundary.
  • Rewrite scripts/smoke.sh into a read-only status probe (unsigned 403 assertion + signed 200 with bounded 429 retries + schema validation) and add deterministic test coverage.
  • Append \n\n to 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.

Comment thread tests/smoke.test.ts
Comment thread tests/smoke.test.ts
Comment thread infra/stack.ts Outdated
Comment thread vitest.config.ts Outdated
Comment thread tests/globalSetup.ts Outdated
- 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>
@equationalapplications

Copy link
Copy Markdown
Owner Author

/fix-pr follow-up

Commit: c8bacab

Review resolution

  • tests/smoke.test.ts__dirname undefined in ESMFixed. Replaced with fileURLToPath(new URL('.', import.meta.url)) and a THIS_DIR constant; REPO_ROOT now derives from that. (package.json has "type": "module", so __dirname was throwing ReferenceError at module load.)
  • tests/smoke.test.ts — shim scripts escape $, breaking shell syntaxNot applied. Verified empirically that JS template literals drop the backslash on \$ and \${ escape sequences, so the bash files written to disk contain plain $(...) and ${VAR} rather than escaped variants. The printf '%s' "$*" "unescaped quote" concern is a misread of the JS template source: bash parses "$*" independently inside the $(...) subshell. Ran tests/smoke.test.ts after the __dirname fix alone — 7/7 scenarios pass, including ones that round-trip args through parseInvocations via the log.
  • infra/stack.ts — JSDoc still references module-level bootstrapFixed. Collapsed the duplicated docblock into one JSDoc that points readers to infra/app.ts and notes this module is side-effect-free on import.
  • vitest.config.ts — comment claims module-load-time synthFixed. Updated to describe the check happening in the AgentStack constructor.
  • tests/globalSetup.ts — comment claims module-load-time synth checkFixed. Same wording update as above.

Verification

  • typecheck — pass (npm run typecheck)
  • lint — N/A (no lint script in package.json)
  • tests — pass (npm test, 137/137 in 16 files; tests/smoke.test.ts 7/7)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 500 means authentication succeeded and the function failed, so this directs the operator to redeploy instead of checking Lambda logs. Give 403 the 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 snapshotVersion is 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.ts is side-effect-free, and tests/infra.test.ts already sets this variable immediately before constructing AgentStack, 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 runFetch stores embeddings after posting and catches each insertEmbedding failure 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.ts and made infra/stack.ts side-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.formattedMessage is read from the matched row's formatted_message, so it is exactly the text that appeared in Discord, not one suffix different. The difference is between the current post's quoted base_message and 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.

@equationalapplications
equationalapplications merged commit dc2aeb2 into main Aug 9, 2026
2 checks passed
@equationalapplications
equationalapplications deleted the feat/smoke-status-iam branch August 9, 2026 16:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants