feat(apps): inject gateway origin into backends - #6519
Conversation
Hand each backend.entryPoint child two generic env vars so it can call back to THIS gateway (e.g. POST /api/notifications/push on a declared channel), without ever learning an app-specific value. - KIROCREW_GATEWAY_ORIGIN = http://127.0.0.1:<port>, where <port> is the port the gateway ACTUALLY bound via resolve_serving_port() (prefers the exported KIROCREW_BOUND_PORT over an inherited/guessed KIROCREW_PORT). Never the app's own PORT, a default, or a request-derived value, so a child can never be pointed at a sibling gateway. - KIROCREW_GATEWAY_ORIGIN_PROOF = HMAC-SHA256(app_secret, origin) hex, injected only when a .app_secret exists. The child recomputes it to confirm the origin was minted by the gateway that alone holds its secret, rather than an inherited or spoofed env value. - Re-enforce owner-only 0600 on .app_secret BEFORE reading it, closing the window where a secret that lost its mode is read while still group/world-readable. A missing secret stays a silent no-op (origin still set, no proof, no secret); only a real lockdown failure warns. Tests (test/test_app_backend.py): TestGatewayOriginInjection asserts the exact origin/proof off the bound port (distinct from the app PORT), proof validity under the right secret and rejection under a wrong key, the 0600-before-read order, missing-secret tolerance, and non-entryPoint apps getting no injection. TestAidlcNotificationsChannelOracle pins the one-channel AI-DLC fixture against the imported manifest schema. Docs: api-reference.md gains a Backend Environment Variables section and app-notifications.md documents reaching the push endpoint from an entryPoint backend. Verified: flake8 and mypy clean on the change; the 10 new tests pass; code, tests, and docs move together. No production deploy.
Merge kirodotdev/KiroCrew main (0934b80) into feat/aidlc-gateway-origin to fix forward the CI failures that came from a stale base. The PR branch was 47 commits behind main; the merge is conflict-free. Validated locally on the merged tree: - slot test test_slot_create_inherits_nearest_folder_project: pass - test/test_app_backend.py (patch-owned): 58 passed, 4 skipped - frontend: tsc -b clean; eslint src/ 0 errors, 664 warnings (= ceiling) - black baseline gate pass; flake8 and mypy on backend.py clean - semgrep custom-rule regression 11/11; diff-only SAST 0 findings Suppressed one patch-introduced semgrep false positive (python-logger-credential-disclosure) on the app-secret lockdown warning: it logs the app name and the OSError only, never the secret value. Uses the repo's standard nosemgrep convention (matches src/kiro_crew/auth/service.py). Remaining blocker is PR-description-owned, not code: the Fork PR Description check needs the template sections "Problem / Motivation", "Why it matters", and "What changed" added to the PR body.
Ready for maintainer review and workflow approvalUpdate 2026-08-28: Upstream All checks I can run locally are green:
Heavy CI has not run yet because it awaits maintainer workflow approval on this fork PR. I am not claiming upstream CI passed - those jobs are still pending your approval to start. Isolated end-to-end proof (issue 6777)To de-risk the change ahead of CI, I ran an isolated proof against a non-production, sandboxed environment:
RequestCould a maintainer please approve the workflow so full CI can run, and take a pass on the code review? Happy to address any feedback. Thanks! |
Merge origin/main (66b8035) into feat/aidlc-gateway-origin to clear the PR kirodotdev#6519 merge-conflict state. Upstream advanced past the branch's merge base; this merge makes the branch 0-behind and mergeable against the upstream base. Merge integrity verified: index-vs-origin/main delta is exactly the gateway-origin patch (4 files, 348 insertions, 4 deletions) - src/kiro_crew/apps/backend.py, test/test_app_backend.py, docs/app-kit/api-reference.md, docs/system-specs/features/app-notifications.md. No upstream change dropped, nothing unrelated swept in, no conflict markers in text files, no unmerged paths. Gates re-run in worktree .venv (py3.12): flake8 OK, mypy clean on backend.py, semgrep 0 findings, and the 14 merge-relevant tests (TestGatewayOriginInjection, TestAidlcNotificationsChannelOracle, TestTheCacheOnlyChildCanSeeTheCacheItMustBootFrom) pass. Change-completeness: backend.py behavior is covered by test_app_backend.py additions (chmod-before-read invariant and positive/negative HMAC proof), and both docs match the implemented behavior.
|
Superseded by #6599. That PR was cut fresh from current |
Hand each backend.entryPoint child two generic env vars so it can call back to THIS gateway (e.g. POST /api/notifications/push on a declared channel), without ever learning an app-specific value. - KIROCREW_GATEWAY_ORIGIN = http://127.0.0.1:<port>, where <port> is the port the gateway ACTUALLY bound via resolve_serving_port() (prefers the exported KIROCREW_BOUND_PORT over an inherited/guessed KIROCREW_PORT). Never the app's own PORT, a default, or a request-derived value, so a child can never be pointed at a sibling gateway. - KIROCREW_GATEWAY_ORIGIN_PROOF = HMAC-SHA256(app_secret, origin) hex, injected only when a .app_secret exists. The child recomputes it to confirm the origin was minted by the gateway that alone holds its secret, rather than an inherited or spoofed env value. - Re-enforce owner-only 0600 on .app_secret BEFORE reading it, closing the window where a secret that lost its mode is read while still group/world-readable. A missing secret stays a silent no-op (origin still set, no proof, no secret); only a real lockdown failure warns. Tests (test/test_app_backend.py): TestGatewayOriginInjection asserts the exact origin/proof off the bound port (distinct from the app PORT), proof validity under the right secret and rejection under a wrong key, the 0600-before-read order, missing-secret tolerance, and non-entryPoint apps getting no injection. TestAidlcNotificationsChannelOracle pins the one-channel AI-DLC fixture against the imported manifest schema. Docs: api-reference.md gains a Backend Environment Variables section and app-notifications.md documents reaching the push endpoint from an entryPoint backend. Verified on fresh origin/main (1129ef1): flake8 and mypy clean on the change, docs lint passes, diff-only semgrep 0 findings, and test/test_app_backend.py is 62 passed / 4 skipped. Code, tests, and docs move together. No production deploy. This is a single-commit replacement for PR kirodotdev#6519, which only failed the commit-count hygiene gate (3 commits from two upstream merges); the net diff is identical.
Problem / Motivation
backend.entryPointapps run as separate loopback processes, so they cannotpush notifications back to the gateway (for example
POST /api/notifications/pushon a declared channel) without first learning thegateway's own address. There was no safe way for a child process to discover
that address, and any app-specific, default, or request-derived value risked
pointing a child at a sibling gateway.
Why it matters
an inherited, guessed, or spoofed origin would let a child be pointed at the
wrong gateway.
read while still group/world-readable is a disclosure risk.
route apps must keep working unchanged.
What changed
Inject the gateway address at spawn time as two generic environment variables,
so no app-specific value is ever handed to a child:
KIROCREW_GATEWAY_ORIGIN=http://127.0.0.1:<port>, where<port>is theport the gateway ACTUALLY bound, via
resolve_serving_port()(which prefersthe exported
KIROCREW_BOUND_PORTover an inherited or guessedKIROCREW_PORT). It is never the app's ownPORT, a default, or arequest-derived value, so a child can never be pointed at a sibling gateway.
KIROCREW_GATEWAY_ORIGIN_PROOF=HMAC-SHA256(app_secret, origin)hex,injected only when the app has a
.app_secret. The child recomputes it withits secret to confirm the origin was minted by this gateway, rather than an
inherited or spoofed env value.
Both are recomputed on every spawn, so a gateway restarted on a different port
hands the backend the current origin.
Security:
<data-home>/apps/<name>/.app_secretis re-enforced toowner-only
0600BEFORE it is read, closing the window where a secret thatlost its mode is read while still group/world-readable. A missing secret
stays a silent no-op; only a real lockdown failure on an existing file warns.
mutated; only the child env dict is populated.
inherited or spoofed env value before trusting it as a callback base.
Compatibility:
the (non-secret) origin but neither the proof nor the proxy secret.
backend.routes) have no separate process and pushin-process, so they need neither variable.
docs/app-kit/api-reference.md(Backend Environment Variables)and
docs/system-specs/features/app-notifications.md.Tests
TestGatewayOriginInjection(test/test_app_backend.py): exact origin/proofoff the bound port (distinct from the app
PORT), proof accepted under theright secret and rejected under a wrong key, the 0600-before-read ordering,
missing-secret tolerance (origin only, no proof/secret), and non-entryPoint
apps getting no injection.
TestAidlcNotificationsChannelOracle: pins the one-channel AI-DLC fixtureagainst the imported manifest schema so a host-side schema change fails here
rather than at app runtime.
the gateway origin and a valid proof and pushed a notification back on its
declared channel. No production deploy - code, tests, and documentation only.