feat(apps): inject gateway origin into backends - #6599
Conversation
GPT 5.6 Review (fork) — 🔴 changes requested (blocking)Reviewed 1 of 1 blocking finding(s) are security-class and were withheld from adjudication, so the blocking verdict stands. BLOCKING -- src/kiro_crew/apps/backend.py:1005 -- symlinked secret escapes the app sandbox FINDING -- src/kiro_crew/apps/backend.py:1012 -- a lockdown failure still reaches FINDING -- src/kiro_crew/apps/backend.py:986 -- only [BLOCK-MERGE] cb42102 Adjudication (Opus 4.8) — is blocking on each finding proportionate?The adjudicable block is empty (0 findings). One fenced finding, F1. F1 — |
Opus 4.8 Review (fork) — ✅ no blocking findingsReviewed Review detailsI've verified the key technical claim mentally: No grounded new finding emerges from the diff: for every realistic value of No findings. [OPUS-REVIEWED] cb42102 |
Design Review (Fable 5, fork) — 🟡 CONCERNSDesign-level review of All spawn call sites ( Design-Verdict: CONCERNS Sound fail-closed origin injection, but the description documents a proof HMAC, a test class, and a resolution path that do not exist in this diff. Watch
Suggestions
[DESIGN-REVIEWED] cb42102 |
First Principles Review (Fable 5, fork) — 🟡 CONCERNSPremise-level review of All evidence gathered. The base confirms First-Principles-Verdict: CONCERNS The description sells an HMAC origin proof ( Not justified as shipped
What this change shipsIntent: let a spawned
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] cb42102 |
|
Status update and one request for a maintainer. Every CI/test check on this PR passes or is skipped except a single Windows test shard: Backend Tests (Windows) (3) (run 33216936703, job 99006265019). Windows shards 1, 2, and 4 pass. That failure is not in this patch. It is a worker crash / timeout in the git-subprocess-heavy test/test_md_notebook.py (worker gw0 crashed with worker restart disabled, e.g. test_sync_refuses_rather_than_pushing_a_subset), none of which this change touches. The diff only modifies src/kiro_crew/apps/backend.py, test/test_app_backend.py, and two docs files - there is no patch-owned test failure. I cannot rerun it as a fork contributor: the re-run-failed-jobs API returns 403 "Must have admin rights to Repository". Could a maintainer please rerun just that one job (Backend Tests (Windows) (3)) once? A single rerun should clear the flake. I am not asking for a rerun-all and I am not claiming CI is green. For reference: the PR is mergeable, head 36abf95, two commits. PR Readiness is still red on one blocking item because maintainer review has not happened yet - review is still required. Thanks. |
bolichen97
left a comment
There was a problem hiding this comment.
Description ↔ code mismatch: the KIROCREW_GATEWAY_ORIGIN_PROOF mechanism is not in this diff
Blocking finding from a Description-vs-code consistency audit. Criterion: the description claims a behaviour that is absent from the code, and the absent behaviour is the security property this PR is advertised on.
The claim
The PR body describes a proof mechanism in six places, including an end-to-end verification claim:
KIROCREW_GATEWAY_ORIGIN_PROOF=HMAC-SHA256(app_secret, origin)hex, injected only when the app has a.app_secret. The child recomputes it with its secret …
- The proof lets a backend distinguish a gateway-minted origin from an inherited or spoofed env value before trusting it as a callback base.
TestGatewayOriginInjection(test/test_app_backend.py): exact origin/proof off the bound port (distinct from the appPORT), proof accepted under the right secret and rejected under a wrong key …
- Verified end to end with an isolated app on port 6777: the backend received the gateway origin and a valid proof …
What the code actually does
src/kiro_crew/apps/backend.py injects exactly two variables:
+ env["KIROCREW_GATEWAY_ORIGIN"] = f"http://127.0.0.1:{int(bound_port)}"
...
+ _proxy_secret = secret_path.read_text().strip()
if _proxy_secret:
env["KIROCREW_PROXY_SECRET"] = _proxy_secret
Across all four changed files, the string KIROCREW_GATEWAY_ORIGIN_PROOF appears 0 times, and there is no HMAC computation. The only hmac token in src/kiro_crew/apps/backend.py is a pre-existing comment about the X-KiroCrew-Proxy header the gateway signs on forwarded requests — a different mechanism, unrelated to this env var.
test/test_app_backend.py's TestGatewayOriginInjection asserts on KIROCREW_GATEWAY_ORIGIN and KIROCREW_PROXY_SECRET only. There is no "accepted under the right secret / rejected under a wrong key" case, because there is nothing to accept or reject.
Why this blocks
The stated security property — that a child can tell a gateway-minted origin from an inherited or spoofed one — does not hold as implemented. KIROCREW_GATEWAY_ORIGIN arrives unauthenticated, so a spoofed env value is indistinguishable from a real one, which is precisely the case the description says is covered. A reviewer reading the body will approve on the assumption that an HMAC binding exists, and the "verified end to end … a valid proof" line asserts observed behaviour that the diff cannot produce.
The docs in this same PR (docs/app-kit/api-reference.md, docs/system-specs/features/app-notifications.md) are consistent with the code — they document KIROCREW_GATEWAY_ORIGIN + KIROCREW_PROXY_SECRET and never mention a proof. So the PR body contradicts both the code and its own docs.
Required change — pick one
- Implement it: inject
KIROCREW_GATEWAY_ORIGIN_PROOF = HMAC-SHA256(app_secret, origin)gated on.app_secretexisting, document it alongside the other two variables, and add the right-secret/wrong-key test cases the body already claims exist. - Or correct the description: delete the proof/HMAC bullets, the
rejected under a wrong keytest claim, and theand a valid proofend-to-end line, so the body matches the actual change — inject the bound-port origin, plus the raw proxy secret when one exists.
Everything else in the description checks out and is not at issue: the bound-port-only guard on KIROCREW_BOUND_PORT, the restrict_to_owner-before-read_text (0600-before-read) ordering, fail-closed omission of the origin, missing-secret silent tolerance, and no-injection for non-entryPoint apps all match the diff and its tests.
Automated Description↔code consistency audit. Only these five classes are treated as blocking: described behaviour absent from code; undisclosed change to user-visible behaviour/API/permissions/security boundary; test-pass claims contradicted by evidence; undisclosed scope beyond what the description says; conflict with spec/docs/an invariant. Wording and style are never flagged.
Open PR relationship auditThis is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion. Relationship findings
No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit. |
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.
Derive KIROCREW_GATEWAY_ORIGIN only from KIROCREW_BOUND_PORT, the port this gateway actually bound, requiring it to parse as an integer in 1..65535. Drop the resolve_serving_port() fallback so an inherited KIROCREW_PORT guess, the app's own PORT, a config value, or a default can never point a backend at a sibling gateway. Absent or invalid evidence now omits the origin entirely, so a callback-dependent backend fails closed rather than trusting a guessed address. Remove KIROCREW_GATEWAY_ORIGIN_PROOF and its HMAC machinery (hashlib/hmac imports). The bound-port-only origin no longer needs a proof value for the backend to recompute; the per-app proxy secret still authenticates forwarded requests. Gate the secret block on secret_path.exists(), and re-tighten owner-only mode (0600) before reading so a secret that lost its mode in transit is never read while group/world-readable. A missing .app_secret is a silent no-op: no lockdown attempt and no per-spawn warning. Verified: test/test_app_backend.py updated to cover bound-port-only origin, omission on absent/invalid port, and the exists/0600-before-read path; docs/app-kit/api-reference.md and docs/system-specs/features/app-notifications.md updated to match.
36abf95 to
cb42102
Compare
|
Rebased onto main Conflicts resolved:
Gates run locally on changed files: isort, flake8, black (both files are in Please review the resolution. A maintainer push makes the maintainer the last pusher, so a second approver is needed under the repo's last-push rule. Reply if anything looks wrong. |
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.