Repository navigation
docs(os-dev, pm-dispatch): a dev, a seat and its subagents never print an environment variable's value; presence is tested in a form that prints none - #23049
Conversation
…t an environment variable's value; presence is tested in a form that prints none The os-dev definition gains one three-line rule in the GitHub-write cluster of basic rule 3: the write tools read the variables they need, a dev never prints an environment variable's value (the transcript is the log line AGENTS.md already forbids a token in), the spellings that print one are named, presence is tested only in a value-free form, and a value already printed is reported by variable name only. The pm-dispatch all-seat invariants gain the same rule for seats and every subagent they spawn. Both files sit at ratchet headroom 0, so each addition is paid by a same-file deletion of a restatement whose home is elsewhere: os-dev.md loses the shared-ref symptom sentence (AGENTS.md multi-agent discipline), the restore-mechanism pointer (AGENTS.md and the file's own hard lines), and the trap rationale (the adjacent trust-order line); SKILL.md loses the clean-path landing line (red line on the queue, the Tier S line beside it, and landing-operations.md). No mechanical guard is added. Claude-Session: https://claude.ai/code/session_012dGTuJen61ECNHXYXKChoC Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #23040 (body; comments 6124767320 triage, 6125031917 claim, 6125439378 os-dev-report), PR #23049 (body, file list, the net diff against ① Derived judgmentsAccept-set and public surface: the diff publishes nothing. No released package, no Zod schema, no export, no REST route, no metadata key changes; the whole change is governed agent-instruction text.
Check-runs on the head, read 2026-10-12T23:10Z: 38 check-runs, all ② Semver level
③ Boundary flags
Dev flags (report deviations, out-of-scope findings and the PR's Acceptance notes), each answered:
Escalation: the only maintainer question raised anywhere in this thread is whether the exposed relay key is rotated; the filer and the triage grade already carry it to the maintainer, this PR decides nothing about it, and this record adds nothing new to escalate. Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #23040
Clause-②: no
A dispatched dev printed the fleet relay's private-key variable into its session transcript by running
env | grepto read the relay's routing variables. Neither the dev-agent definition nor the PM skill said a word about it. This PR adds the rule to both, in the triage grade's direction: a dev, a seat and every subagent a seat spawns never print an environment variable's value, and test presence only in a form that prints none. No mechanical guard, hook orsettingsdeny is added — a new gate defaults to no; if the maintainer wants one, it is a new card.What changed
.claude/agents/os-dev.md.claude/skills/pm-dispatch/SKILL.mdBoth files sit at ratchet headroom 0, so every added line is paid by a same-file deletion. No re-wrap, no ceiling moved, no cross-file move declared. Every added line is under the ratchet's 120-byte line cap (115 / 102 / 114 / 113 bytes).
Lines added, verbatim
.claude/agents/os-dev.md, after the line归属 = 文本里的 session ID …in basic rule 3:.claude/skills/pm-dispatch/SKILL.md, after the写侧恒为 objectstack-fleet[bot]line in 〈全体座位的不变量〉:Why these words: the rule is a flat one — never print any environment variable's value — rather than "only the ones that look like credentials", because the flat form is the one an agent cannot misjudge (the measured instance was a dev reading routing variables). The os-dev line anchors on the rule AGENTS.md already states in its write-gate paragraph (never a token in a remote URL, a
git configvalue on disk or a log line): a session transcript is a log line, so this is that rule applied, not a second vocabulary. The write tools read the variables they need themselves (fleet-token.mjs,fleet-write/dispatch.mjsderive the session and the route from the container), so a dev never needs a value. The presence form uses${X+x}, which answers set-versus-unset without expanding the value; the card body's[ -n "$VAR" ]form conflates unset with empty and expands the value into the test, so it is not the form taught.Lines deleted, and where each one lives
失效同签名:操作看着本地、报成功,唯一症状是git status里出现他人文件。popreports success — the only symptom is another agent's files in yourgit status) and the shared-ref paragraph beneath it; os-dev.md's own line 19 binds AGENTS.md in full. The two neighbours that carry the rule (what a worktree shares;git reset --soft origin/mainstages others' work) stay.恢复机制与字节一致性证明规则见 AGENTS.md;从已 commit 的状态重跑,红/绿数字才可信。HEAD, prove bygit diff HEAD, absolute trap paths, blob-hash comparison) remain.trap 触发只证明 shell 跑了一个函数,不证明那个函数的效果。路径面干净的才转 ready → 入队;队列是唯一被认可的落地路径,⛔ 永不队列外合并。S 经达档复核 PASS 后 ready → 入队) and the landing line in 路径分叉 (席位落地 = 过落地前检、清标、ready、auto-merge);references/landing-operations.md入队与落地 (ready/queue only after the diff read, PASS ⇒ ready + auto-merge, the governed-merges pre-check); AGENTS.md Multi-agent discipline §7 (land through the merge queue, never straight tomain).Line budget
.claude/agents/os-dev.md.claude/skills/pm-dispatch/SKILL.mdPremise check — the dispatch's H1–H5, measured on
origin/mainat38d98d8d8grep -c -i -E "secret|env |printenv|private.key|credential" .claude/agents/os-dev.md→ 0 (controlworktree→ 8).grep -c -E "secret|printenv|private.key|credential|凭据" .claude/skills/pm-dispatch/SKILL.md→ 0 (controlworktree→ 3). The pm-dispatch references carry no env-printing rule either: a grep forprintenv,env |,echo $,环境变量over.claude/skills/pm-dispatch/finds only unrelated readings (platform-readings.md,rest-channel.md).references/dispatch-runbook.md:references/execution-duties.mdstates 「无条件条款只住角色文件」 and 「标准非协商条款 ⛔ 不抄进派发词」, so the os-dev constraint lives only in os-dev.md and the dispatch prompt carries no second copy; what the dispatch template cannot reach is the seat itself and the non-os-dev subagents it spawns (a contract-review general-purpose subagent reads neither os-dev.md nor a dispatch prompt), and the one text every seat and every subagent is bound by is the all-seat invariants. That line is the "dispatch template" half of the triage direction..claude/agents/os-dev.mdis at 402/402 too (CEILINGSrow inscripts/pm/check-skill-line-ratchet.mjs), so the dispatch's "≤ +3 lines" for os-dev.md also had to be paid by three same-file deletions, which the dispatch did not budget. The same ratchet caps every line at 120 bytes (MAX_LINE_BYTES), which bounded the wording.check:pm-skill-id-lintholds: no issue number in either file's operative text.fleet-write/dispatch.mjs --routederives it from the container and reports the derivedsession_…id, which is already the attribution text written to GitHub. No current step needs a dev or a seat to print a value.Gates — every command
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived (no paths passed; the tool took the change set from git), run on6aeede187, exit code captured before any pipe, reconciled with--ran--ranverdict:✓ dispatch-gates --ran: 22 derived famil(ies) accounted for — 21 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)— then the exit-3 family was re-run after its stated prerequisite build; its final line is in the table.pnpm check:pm-skill-ratchet✓ check-skill-line-ratchet: .claude/agents/os-dev.md is 402 lines (ceiling 402; headroom 0).·✓ check-skill-line-ratchet: .claude/skills/pm-dispatch/SKILL.md is 319 lines (ceiling 319; headroom 0).pnpm check:pm-skill-id-lint✓ check-skill-id-lint: 87 file(s) clean (pattern /#[0-9]{3,}/g).pnpm check:agent-model-declared✓ check-agent-model-declared: 1 agent definition(s) under .claude/agents/ all declare a modelpnpm check:skill-frame-sync✓ check-skill-frame-sync: the one declared copy of the decision frame is internally coherent (.claude/skills/pm-dispatch/SKILL.md …)pnpm check:nul-bytescheck-nul-bytes: OK (scanned 11047 text file(s) … no raw ASCII control bytes).pnpm check:pm-governed-prose✓ check-governed-prose: 2 instruction surface(s) name all 6 registered governed surfacespnpm check:pm-governed-mergeslive: the real generator declared 9 output(s) and certified this treepnpm check:pm-expected-skips✓ check-expected-skips self-test: 154 cases pass(+ roster truth on this checkout)pnpm check:commit-card-trailers✓ check-commit-card-trailers self-test: 81 cases pass.pnpm check:agent-test-spellingpnpm check:cross-package-test-inputsOK: 30 package(s) read outside themselves, all declared, and turbo.json hashes every declared globpnpm check:doc-authoring✓ doc authoring guard: sibling-package prose ids hold the baselinepnpm check:driver-memory-censuscheck-driver-memory-census: OK — every declaration is ledgered, every ledger entry is livepnpm check:gitlink-declaredcheck-gitlink-declared: OK (11056 index entries -- 0 gitlink(s) at mode 160000 …)pnpm check:refd-timer-probeOK check-refd-timer-probe: 8856 source file(s) sweptpnpm check:watch-hint-literal✓ check-watch-hint-literal: 73 declaration(s) across 4 rostered name(s)node scripts/check-closing-keyword-parity.mjscheck-closing-keyword-parity: OK (3 parsers agree on all 9 keywords and both measured separators …)node scripts/check-closing-keyword-parity.mjs --self-test✓ check-closing-keyword-parity --self-test: 40 assertions, 5 mutations of the shipped parsers each driven to red.node scripts/check-comment-mask-corpus.mjs✓ comment-mask corpus sweep [scripts/js-comment-mask.mjs]: 8862 files, 0 disagree, 0 unparseablenode scripts/pm/check-governed-queue-guard.mjs --self-test✓ check-governed-queue-guard self-test: 296 cases passnode scripts/pm/check-harness-current.mjs --self-testcheck-harness-current --self-test: all 26 cases passed.pnpm --filter @objectstack/lint run check:doc-formula-expressionspnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lintunder the verify lock; first run exit 3, PREREQUISITE NOT MET)✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 473 files / 1388 TS blocks judged clean by @objectstack/formula.Not run locally, by design: no package is touched (the diff is
.claude/**only), so there is no ① dependency-closure build and no ② package test/typecheck; the repo-wide scans (pnpm lintfirst among them), the 51 artifact-roster families, the 11 wide-population families, the 15 pending-changeset families and the type-check lanes the derivation lists are CI's runs. No changeset:.claude/**publishes nothing, soskip-changesetis the declaration (applied throughscripts/pm/label-write.mjs).维护者速读(草稿)
改了什么:两份 agent 指令文本各加一条规则。
.claude/agents/os-dev.md(派发的开发 agent 定义)在「GitHub 写」那一簇加三行:写工具自己读所需变量;永不打印任何环境变量的值(列出env、printenv、set、export -p、echo $VAR、cat /proc/*/environ这些拼法);判断变量是否设置只用不印值的形式;万一已印出,报告里只点名变量名。.claude/skills/pm-dispatch/SKILL.md(席位技能)在「全体座位的不变量」加一行:席位及其子代理永不打印环境变量的值,判存在只用不印值的形式。两份文件都在只降不升的行数棘轮顶上,所以每加一行就同文件删一行在别处有家的复述:os-dev.md 删三行,SKILL.md 删一行,行数前后不变(402 / 319)。不加任何门禁、钩子或 settings 拒绝项。为什么改:一次实测事故——一个派发的开发 agent 为了看中继的路由变量跑了
env | grep,把中继私钥的值印进了它的会话转录(GitHub 上的报告与 PR 正文都没带值)。此前这两份文本对「不要打印环境变量」零字,一个要判断变量是否设置的 agent 没有被告知安全写法,自然伸手去拿env。分诊定级给的方向就是:两处各加一条规则,不加机械门禁。风险与代价(含回滚):纯指令文本,零运行时影响、零发布面。代价是删掉的四行复述——每一行在 AGENTS.md 或同文件相邻行里都有家,正文表格逐行列出了出处。残余风险:这是约定不是机械强制;分诊方向是新门禁默认不加,要加另立卡。回滚 = revert 本 PR,无副作用。
席位意见:(留空,席位定稿)
你要做的:一个动作——决定那把已暴露的中继私钥是否轮换;这不在本 PR 范围内,也不由任何席位决定。本 PR 本身是 Tier S(
.claude/**全树),由席位在达档契约复核 PASS 在案后落地,不需要您合并。Acceptance notes
scripts/pm/fleet-write/dispatch.mjs --route— and every tool that re-execs itself with--use-env-proxy— prints the value ofHTTPS_PROXY(a loopback proxy URL with a port) in its ℹ️ re-exec notice. Under the flat rule this PR adds, a tool that echoes a variable's value is the next thing a transcript picks up; the value here is not secret. Carrier: none; a candidate for the lane if the maintainer wants tools held to the same rule. Dedupe words:use-env-proxy,HTTPS_PROXY,re-exec notice,prints value.pnpm --filter @objectstack/lint run check:doc-formula-expressionsexited 3 (PREREQUISITE NOT MET:@objectstack/formulaand@objectstack/lintnot built) on the first run; the build it prescribes was run under the shared verify lock and the gate re-run — the table above carries the final reading. Not a finding about the tree.check:pm-skill-ratchetprints a pre-existing ℹ️ about.claude/skills/spec-property-retirement/SKILL.md(table-row pin headroom 2, could be lowered to 324). Not this card's file; untouched.[ -n "$VAR" ] && echo setconflates unset with empty and expands the value into the test; the rule teaches[ -n "${X+x}" ]instead. A wording note, not a finding.Generated by Claude Code