Skip to content

docs(configuration): define agent-child environment boundary - #886

Merged
moncher-dev merged 4 commits into
mainfrom
symphony/hojinzs-github-symphony-871
Sep 5, 2026
Merged

docs(configuration): define agent-child environment boundary#886
moncher-dev merged 4 commits into
mainfrom
symphony/hojinzs-github-symphony-871

Conversation

@moncher-dev

@moncher-dev moncher-dev commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Issues

Summary

  • Document the least-privilege agent-child environment boundary and the project-.env-only hook allowlist.
  • Validate malformed and undefined allowlist names only when a configured, trusted hook will execute.
  • Preserve skipped hooks for absent/untrusted commands, while normalizing empty separators and duplicate names.

Change-point diagram

  • Managed project .env → execution guard → allowlist validation → approved hook environment
  • Worker environment → runtime-specific least-privilege constructor → coding-agent child

Start here

  • packages/orchestrator/src/service.ts:5053 — execution guard around allowlist validation
  • packages/orchestrator/src/service.test.ts:16107 — skipped-path regressions
  • docs/configuration.md:89 — operator contract
  • docs/configuration.md:397 — agent-child boundary

User-Visible Behavior / Operational Impact

  • Malformed or undefined names fail only when their configured hook is trusted and about to execute.
  • Invalid allowlists remain inert when no hook exists or hook execution is trust-disabled.
  • Empty separators are ignored and duplicate names are folded together.
  • Coding-agent child inputs are unchanged.

Validation

  • pnpm exec vitest run packages/orchestrator/src/service.test.ts — pass (221 tests)
  • pnpm lint — pass
  • pnpm test — pass
  • pnpm typecheck — pass
  • pnpm build — pass
  • ./e2e/run-standalone-project-e2e.sh — unavailable twice at Docker preflight (exit 69; daemon socket unreachable); tracked in test(e2e): restore Docker daemon access in Symphony workers #889

Changeset

  • .changeset/quiet-hooks-validate.md (patch)

Risks & rollback

  • Risk is limited to allowlist parsing immediately before approved hook execution. Revert this PR to restore permissive behavior.

Changed files

  • packages/orchestrator/src/service.ts — validate only executing hooks and canonicalize entries
  • packages/orchestrator/src/service.test.ts — cover parser rejection/normalization and both skip paths
  • docs/configuration.md — document hook validation timing and the agent-child boundary
  • docs/architecture.md — map validation to the orchestrator boundary
  • AGENT_TEST.md — record authoritative coverage
  • .changeset/quiet-hooks-validate.md — patch release note

Post-merge / human validation

  • None beyond normal review and CI.

Security

  • No real tokens, private keys, .env files, or generated installation tokens are committed

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-05T10:34:41.198564Z 6f62160 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 40.97% 45074 / 110003
🔵 Statements 40.97% 45074 / 110003
🔵 Functions 72.68% 2225 / 3061
🔵 Branches 78.85% 10368 / 13148
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/orchestrator/src/service.ts 92.08% 85.16% 95.65% 92.08% 267-268, 313, 315, 327-329, 386, 535-541, 558, 666, 692-693, 698-699, 721, 725, 729, 766-770, 793-811, 840-854, 987-988, 993-994, 1019-1020, 1091-1092, 1128-1129, 1141-1142, 1147-1158, 1160-1161, 1193-1210, 1232-1239, 1261-1262, 1274-1275, 1282, 1289-1293, 1309, 1319, 1350, 1376-1377, 1427-1429, 1441-1442, 1449, 1502-1503, 1665, 1667, 1821-1822, 1834-1836, 1978, 1993, 2008, 2033-2037, 2087, 2159-2161, 2166-2167, 2181-2191, 2198, 2223, 2250-2252, 2257-2258, 2285, 2334, 2366-2370, 2441, 2472-2473, 2477-2481, 2485-2486, 2495-2496, 2511, 2538, 2563, 2566-2569, 2692-2708, 2784-2789, 2791-2796, 2860-2861, 2880-2881, 2928-2931, 3007-3008, 3015-3016, 3045-3046, 3080-3083, 3117-3130, 3188-3191, 3223-3226, 3234-3237, 3286-3289, 3505-3515, 3530, 3538-3539, 3558-3559, 3578-3582, 3590-3591, 3621, 3633-3638, 3669-3671, 3674-3675, 3718, 3721, 3818, 4119, 4155, 4244, 4246, 4249, 4251-4253, 4256-4257, 4276, 4341-4342, 4347-4351, 4355, 4368-4369, 4418, 4516, 4540-4544, 4554-4556, 4602-4603, 4633-4639, 4699-4703, 4825-4826, 4881-4882, 4912-4913, 5042-5050, 5083-5090, 5108-5111, 5266, 5280-5286, 5349-5355, 5382, 5392, 5432-5435, 5442-5448, 5598-5601, 5741-5742, 5754-5763, 5823-5824, 5857-5858, 5914-5915, 6092-6093, 6167-6174, 6222-6223, 6327, 6392-6393, 6412-6413, 6415-6416, 6418-6419, 6421-6422, 6429-6430, 6451-6452, 6556-6557, 6560-6561, 6581, 6589-6590, 6598-6599, 6660-6663, 6693, 6800-6801, 6869-6871, 6877-6878, 6891-6892, 6897-6903, 6964-6966, 7100-7101
Generated in workflow #1409 for commit 68974fd by the Vitest Coverage Report Action

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6f621603c8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/configuration.md Outdated

@moncher-dev moncher-dev left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Tested at head 6f621603. pnpm install --frozen-lockfile && pnpm build && pnpm test all pass (14 packages, 0 failures), and CI is green. Requesting changes on content, not on the build.

This PR's whole value is that the documentation describes what actually runs. One of the two new paragraphs does not.

The hook allowlist paragraph is wrong about the daemon environment

The new text says:

To expose additional variables from the effective project/daemon environment, set SYMPHONY_WORKFLOW_HOOK_ENV_ALLOWLIST to a comma-separated list of names

The daemon environment is not a usable source for either the allowlist variable or its targets. hookEnv is built by buildProjectExecutionEnv (packages/orchestrator/src/service.ts:5200) as:

{ ...projectEnv, ...inheritedEnv, ...explicitEnv }
  • projectEnv is readProjectEnv — the project .env file, and nothing else (service.ts:5137).
  • inheritedEnv is process.env filtered by shouldInheritProcessEnvKey, i.e. INHERITED_ENV_ALLOWLIST = CI, HOME, LANG, LOGNAME, PATH, PWD, SHELL, TERM, TMPDIR, USER plus LC_* (service.ts:118).
  • explicitEnv is Symphony's injected hook context.

SYMPHONY_WORKFLOW_HOOK_ENV_ALLOWLIST is not in that allowlist, so setting it in the daemon environment never reaches hookEnv and is silently ignored. A daemon-only target is worse than ignored — this PR turns it into a hard failure. Using the parser this PR exports:

parseWorkflowHookEnvAllowlist("STAGING_API_HOST", {HOME, PATH, SYMPHONY_WORKFLOW_HOOK_ENV_ALLOWLIST:"STAGING_API_HOST"})
  -> THROWS: names an environment variable that is not defined: STAGING_API_HOST

parseWorkflowHookEnvAllowlist("STAGING_API_HOST", {HOME, STAGING_API_HOST:"x", ...})
  -> ["STAGING_API_HOST"]        # project .env only

So an operator who follows this paragraph and puts the variable in the daemon environment does not get a no-op — they get a failing hook. The Codex reviewer flagged this and is right. Either source allowlisted names from process.env, or say plainly that this is a project-.env-only mechanism.

Worth calling out because this repo's own daemon runs with SYMPHONY_ALLOW_WORKFLOW_HOOKS=1 and an after_create hook.

Secondary, and documented so I am not blocking on it: a trailing comma now throws ("A_VAR," -> invalid environment variable name: "") where it was previously filtered out. The new prose does say malformed entries fail, so this is intentional — just an easy accident to make.

The child-boundary table

I checked the table against the code as it stands after #868/#869/#870/#877 and it reads accurately today, including the #agent-visible-symphony-context anchor, which resolves. One dependency worth noting: the "Tracker-declared secrets ... always stripped" row is only unconditionally true while CUSTOM_RUNTIME_RESERVED_AUTH_ENVIRONMENT_NAMES exists in core. PR #885 removes it, which would make seven of those names conditional on the active tracker's declaration. I have asked for changes on #885 for that reason; flagging it here so the two do not land in an order that leaves this table describing something that no longer runs.

Scope note

The diff for docs/configuration.md is 29 additions and 0 deletions. The issue asked for four disagreeing sections to be replaced by one child-boundary section; this appends a fifth statement and leaves the existing four in place. If the intent was consolidation, the old text still needs to be reconciled or removed — otherwise the next reader still has five places to compare.

Moving #871 back to Ready.

@moncher-dev moncher-dev left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@hojinzs re-reviewed at head 2ade8c02. The substantive finding is fixed and verified. Ready for your review.

Build

pnpm install --frozen-lockfile && pnpm build && pnpm test on a clean worktree of this head: all 14 packages pass, 0 failures. CI green on both jobs.

The hook paragraph now matches the code

Previous text claimed the allowlist exposed variables from the "effective project/daemon environment". New text:

set SYMPHONY_WORKFLOW_HOOK_ENV_ALLOWLIST in the managed project .env ... and define those names in the same file. A daemon-only allowlist setting or target variable is not a source for hook forwarding because hooks inherit only the portable process variables listed above from the daemon.

That is exactly what buildProjectExecutionEnv does. Re-verified against the parser this PR exports, built from 2ade8c02:

allowlist + target in project .env   -> ["STAGING_API_HOST"]
target daemon-only (filtered out)    -> THROWS: names an environment variable that is not defined: STAGING_API_HOST
empty / malformed entry              -> THROWS: contains an invalid environment variable name: ""

The doc now warns about the throwing case instead of recommending it. The wording change from "unknown entry" to "names a variable absent from the effective hook environment" is also more accurate, since that is the condition actually tested.

Rejection-path coverage is present in packages/orchestrator/src/service.test.ts for both branches (malformed not-valid, and absent MISSPELLED_NAME), which is the issue's second acceptance criterion.

Both acceptance criteria on #871 are met

  • docs/configuration.md documents the agent-child boundary and the hook allowlist variable ✅
  • SYMPHONY_WORKFLOW_HOOK_ENV_ALLOWLIST is validated with test coverage for the rejection path ✅

I re-checked the child-boundary table against the code after #868/#869/#870/#877 and it still reads accurately, anchor included.

One scope question for you, and a correction to my own last review

I previously flagged that the docs/configuration.md diff is additions-only (now 31/0) and suggested it should have replaced the four overlapping sections. Re-reading #871, that is not what the issue asks. Its scope line is "Document the agent-child boundary in docs/configuration.md, next to the existing precedence tables" — explicitly additive — and its out-of-scope list is about behaviour, not consolidation. So I do not think this PR should be bounced for it, and I am not treating it as a blocker.

If you do want the four sections consolidated, it is worth its own issue: deleting them is a judgement call about which wording survives, and it is easy to lose accurate detail (the broker table's per-variable audience column, for instance, has no equivalent in the new table).

One small pre-existing inaccuracy I noticed while comparing, not introduced here: the GITHUB_TOKEN_BROKER_SECRET row in Credential Brokers And Git Access says it "is declared as a tracker secret and removed from agent children by default". On main today it is removed by CUSTOM_RUNTIME_RESERVED_AUTH_ENVIRONMENT_NAMES unconditionally, not by the declaration. That row describes the world #885 is trying to create — and #885 is currently failing on exactly that point, so the row is ahead of the code either way. Worth a follow-up once #885 settles.

Ordering note

The child-boundary table's "Tracker-declared secrets ... always stripped" row is unconditionally true only while the core constant exists. #885 removes it and is still red on the compatibility-mode e2e (re-reviewed at d5bd4298 just now). This PR is safe to land first; #885 is the one that would need this table revisited.

@moncher-dev
moncher-dev requested a review from hojinzs September 5, 2026 11:23

@hojinzs hojinzs left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Review 1/6 — one P1 blocker, otherwise a well-scoped PR

Requirements vs. #871

Acceptance criterion Status
docs/configuration.md documents the agent-child boundary docs/configuration.md:397-414, table + the explicit "a project .env entry does not reach the agent child" statement
docs/configuration.md documents the hook allowlist variable docs/configuration.md:92-104
SYMPHONY_WORKFLOW_HOOK_ENV_ALLOWLIST validated ⚠️ implemented, but fires on paths where no hook runs — see P1
Rejection path has test coverage ⚠️ parser-level only; neither skip path is covered
No behaviour change to what the child receives (out of scope) ✅ documentation only, confirmed against agent-child-env.ts / custom-child-env.ts

I spot-checked the doc claims rather than taking them on trust: the portable hook set matches DEFAULT_HOOK_ENV_KEYS (packages/core/src/workspace/hooks.ts:272-282); the #agent-visible-symphony-context anchor resolves to the heading at docs/configuration.md:634; and "Codex and Claude also receive a private DOCKER_CONFIG" is right — agent-child-env.ts:20 sets it, custom-child-env.ts:152-153 deliberately does not. No over-engineering or unrequested decisions found — the diff is the issue's scope and nothing more, and the changeset is correctly scoped to patch.

The earlier Codex P2 thread is genuinely resolved, not just marked resolved: 2ade8c0 rewrote the paragraph to describe the mechanism as project-.env-only, which is what the reviewer asked for.

Smoke test

No preview environment on this PR, so I ran it locally against the real modules at 2ade8c0: built the workspace, exercised the runHookexecuteWorkspaceHook composition directly, and re-ran the touched suite.

  • npx vitest run packages/orchestrator/src/service.test.ts217 passed, matching the PR body
  • Targeted reproduction of the composed call path — surfaced the P1 below

Findings

Finding
P1 Allowlist validation is evaluated as a call argument, so it runs before executeWorkspaceHook's two skipped returns. A typo'd entry now fails dispatch for a project with no hooks configured, and for the default-off SYMPHONY_ALLOW_WORKFLOW_HOOKS case. Via after_create that also rm -rfs the freshly created workspace.
P2 A trailing comma throws invalid environment variable name: "" — neither of the two conditions #871 asks you to reject, and undiagnosable from the message.
P3 The new describe block is inserted between two import statements in service.test.ts, and packs two rejection paths into one it.
nit Duplicate entries are returned undeduplicated.

What I need for approval

Just the P1 — a guard so the parser runs only when the hook will actually execute, plus a test pinning "bad allowlist + no hook configured stays skipped". The P2 is a small follow-on in the same function. P3 and the nit are yours to take or decline; a reasoned "no" on the thread is fine and I won't re-raise them.

Everything else here is ready. The documentation is accurate and the boundary it describes is the right one to write down.


Generated by Claude Code

Comment thread packages/orchestrator/src/service.ts Outdated
Comment thread packages/orchestrator/src/service.ts Outdated
Comment thread packages/orchestrator/src/service.test.ts
Comment thread packages/orchestrator/src/service.ts
@moncher-dev
moncher-dev requested a review from hojinzs September 5, 2026 11:57

@hojinzs hojinzs left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Review 2/6 — Approved

All four findings from review 1/6 are fixed at e7aaa5b, and I verified each against the real modules rather than taking the thread replies on trust.

Finding disposition

Finding Status
P1 Allowlist parsed as a call argument, firing before both skipped returns ✅ Fixed — service.ts:5056-5079 hoists hookCommand/trusted and gates the parse on hookCommand && trusted
P2 Trailing/doubled comma threw invalid environment variable name: "" ✅ Fixed — .filter((entry) => entry.length > 0) precedes the regex check
P3 describe wedged between imports; two rejection paths in one it ✅ Fixed — import list is contiguous again, and rejects a malformed name / rejects an undefined name are separate
nit Duplicates returned undeduplicated ✅ Accepted — [...new Set(...)]

The P1 guard is exactly equivalent to executeWorkspaceHook's two skip returns (packages/core/src/workspace/hooks.ts:247-266): same resolveHookCommand(hooks, kind) on the same object, and the same trusted value that is then passed through. No third skip path exists, so there is no case where validation fires on a path that skips.

Smoke test

No preview environment, so I ran this locally at e7aaa5b — clean pnpm install --frozen-lockfile && pnpm build (exit 0), then a direct reproduction of my original P1 repro against the real executeWorkspaceHook composed with the new call-site expression:

no hooks configured      + bad allowlist -> skipped   ✅ (was: throws)
hook configured/untrusted + bad allowlist -> skipped   ✅ (was: throws)
hook configured + trusted + bad allowlist -> throws    ✅ validation still has teeth
"A_VAR,,A_VAR, "                          -> ["A_VAR"] ✅ P2 + nit

The third case matters: gating the parse could have quietly disabled the feature this issue exists to add, and it does not.

  • npx vitest run packages/orchestrator/src/service.test.ts221 passed (was 217; +4 from this commit), matching the PR body
  • pnpm lint — pass · pnpm typecheck — pass · pnpm build — pass
  • CI green on both Test and Container Smoke
  • The positive path stays covered end-to-end by applies allowlisted project .env to approved script hooks (service.test.ts:16070), which runs a real hook script reading the forwarded variables — so the wiring is pinned in both directions now
  • delete process.env.SYMPHONY_ALLOW_WORKFLOW_HOOKS in the new test is restored by the suite's afterEach (service.test.ts:579-590), so no cross-test leakage

Requirements vs. #871

Acceptance criterion Status
docs/configuration.md documents the agent-child boundary :397-414
docs/configuration.md documents the hook allowlist variable :92-106, now including the validation-timing sentence
SYMPHONY_WORKFLOW_HOOK_ENV_ALLOWLIST validated
Rejection path has test coverage ✅ parser-level plus both skip paths as an integration test
No behaviour change to what the child receives (out of scope) ✅ documentation only

The doc followed the code on the P2 change rather than being left stale — empty separators are ignored and duplicate names are folded together replaces the old an empty or malformed entry wording. That is the detail that most often gets missed in a fix like this.

No over-engineering or unrequested decisions. The diff is the issue's scope and nothing beyond it; the changeset is correctly patch.

One non-blocking note — no response needed

You took the call-site gate, which is the option I offered, so this is an observation and not a request. resolveHookCommand is now evaluated twice for every hook — once in runHook, once inside executeWorkspaceHook — which means service.ts has to stay in step with a skip condition owned by core. It is correct today. If a third skip condition is ever added to executeWorkspaceHook, moving the parse inside it (after its guards) is the version that cannot drift. Not worth a change now.

The #885 ordering note from the earlier review still stands: this PR is safe to land first, and #885 is the one that would need the child-boundary table revisited.


Generated by Claude Code

@moncher-dev
moncher-dev merged commit 5bc1374 into main Sep 5, 2026
2 checks passed
@moncher-dev
moncher-dev deleted the symphony/hojinzs-github-symphony-871 branch September 5, 2026 12:21
@moncher-dev
moncher-dev restored the symphony/hojinzs-github-symphony-871 branch September 5, 2026 12:22
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.

docs(configuration): document the agent-child environment boundary and validate the hook allowlist

2 participants