Repository navigation
Commit 4a87d09
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)
Fixes #23040
Clause-②: no
A dispatched dev printed the fleet relay's private-key variable into its
session transcript by running `env | grep` to 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 or `settings` deny is added — a
new gate defaults to no; if the maintainer wants one, it is a new card.
## What changed
| File | Added | Deleted | Lines before → after | Ceiling |
|:--|:--|:--|:--|:--|
| `.claude/agents/os-dev.md` | 3 lines in basic rule 3's GitHub-write
cluster | 3 restatements (homes below) | 402 → 402 | 402 (headroom 0) |
| `.claude/skills/pm-dispatch/SKILL.md` | 1 line in 〈全体座位的不变量〉 | 1
restatement (home below) | 319 → 319 | 319 (headroom 0) |
Both 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:
```text
- 写工具自读所需变量;⛔ 永不打印环境变量的值,转录是 AGENTS.md 禁落令牌的日志行。
- 拼法不限于 `env`、`printenv`、`set`、`export -p`、`echo $VAR`、`cat /proc/*/environ`。
- 判存在只用不印值形式 `[ -n "${X+x}" ] && echo set`;已印出 ⇒ `deviations` 只点名变量名。
```
`.claude/skills/pm-dispatch/SKILL.md`, after the `写侧恒为
objectstack-fleet[bot]` line in 〈全体座位的不变量〉:
```text
- 席位及其子代理 ⛔ 永不打印环境变量的值;判存在只用不印值的形式 `[ -n "${X+x}" ]`。
```
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 config`
value 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.mjs` derive 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
| File | Deleted line (verbatim) | Its home |
|:--|:--|:--|
| os-dev.md | `失效同签名:操作看着本地、报成功,唯一症状是 `git status` 里出现他人文件。` |
AGENTS.md, Multi-agent working discipline, the stash paragraph (`pop`
reports success — the only symptom is another agent's files in your `git
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/main` stages others' work)
stay. |
| os-dev.md | `恢复机制与字节一致性证明规则见 AGENTS.md;从已 commit 的状态重跑,红/绿数字才可信。` |
AGENTS.md, Multi-agent working discipline, the reverse-verification
paragraph (commit the fix first … then re-run from the committed state
so your red/green numbers are trustworthy); in os-dev.md itself the
commit-first line directly above it and hard lines ①–④ (restore from
`HEAD`, prove by `git diff HEAD`, absolute trap paths, blob-hash
comparison) remain. |
| os-dev.md | `trap 触发只证明 shell 跑了一个函数,不证明那个函数的效果。` | The line directly
above it, which states the trust order (trap is a convenience on the
crash path; the byte/hash comparison is the proof); the deleted line was
that order's rationale restated. |
| SKILL.md | `路径面干净的才转 ready → 入队;队列是唯一被认可的落地路径,⛔ 永不队列外合并。` | Red line
「永不绕过合并队列」 (SKILL.md 红线); the Tier S line directly above it (`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 to `main`). |
### Line budget
| File | before | after |
|:--|--:|--:|
| `.claude/agents/os-dev.md` | 402 | 402 |
| `.claude/skills/pm-dispatch/SKILL.md` | 319 | 319 |
## Premise check — the dispatch's H1–H5, measured on `origin/main` at
`38d98d8d8`
- **H1 holds.** `grep -c -i -E "secret|env
|printenv|private.key|credential" .claude/agents/os-dev.md` → 0 (control
`worktree` → 8). `grep -c -E "secret|printenv|private.key|credential|凭据"
.claude/skills/pm-dispatch/SKILL.md` → 0 (control `worktree` → 3). The
pm-dispatch references carry no env-printing rule either: a grep for
`printenv`, `env |`, `echo $`, `环境变量` over `.claude/skills/pm-dispatch/`
finds only unrelated readings (`platform-readings.md`,
`rest-channel.md`).
- **H2 adopted.** The os-dev line anchors on AGENTS.md's write-gate
sentence (the transcript is the log line it names); AGENTS.md is
untouched (Tier H, outside this card's file surface).
- **H3 adopted, with the reason.** The second half lands in SKILL.md
〈全体座位的不变量〉, not in `references/dispatch-runbook.md`:
`references/execution-duties.md` states 「无条件条款只住角色文件」 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.
- **H4 partly falsified.** SKILL.md is at 319/319 as the dispatch said —
and `.claude/agents/os-dev.md` is at 402/402 too (`CEILINGS` row in
`scripts/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-lint` holds: no issue number in either file's
operative text.
- **H5 adopted; no exception needed.** The one non-credential variable a
flow reads (the session id) is read by the write tools themselves:
`fleet-write/dispatch.mjs --route` derives it from the container and
reports the derived `session_…` 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/objectstack` derived (no paths passed; the tool
took the change set from git), run on `6aeede187`, exit code captured
before any pipe, reconciled with `--ran`
`--ran` verdict: `✓ 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.
| Command | exit | Gate's own verdict line |
|:--|--:|:--|
| `pnpm check:pm-skill-ratchet` | 0 | `✓ 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` | 0 | `✓ check-skill-id-lint: 87 file(s)
clean (pattern /#[0-9]{3,}/g).` |
| `pnpm check:agent-model-declared` | 0 | `✓ check-agent-model-declared:
1 agent definition(s) under .claude/agents/ all declare a model` |
| `pnpm check:skill-frame-sync` | 0 | `✓ check-skill-frame-sync: the one
declared copy of the decision frame is internally coherent
(.claude/skills/pm-dispatch/SKILL.md …)` |
| `pnpm check:nul-bytes` | 0 | `check-nul-bytes: OK (scanned 11047 text
file(s) … no raw ASCII control bytes).` |
| `pnpm check:pm-governed-prose` | 0 | `✓ check-governed-prose: 2
instruction surface(s) name all 6 registered governed surfaces` |
| `pnpm check:pm-governed-merges` | 0 | `live: the real generator
declared 9 output(s) and certified this tree` |
| `pnpm check:pm-expected-skips` | 0 | `✓ check-expected-skips
self-test: 154 cases pass` (+ roster truth on this checkout) |
| `pnpm check:commit-card-trailers` | 0 | `✓ check-commit-card-trailers
self-test: 81 cases pass.` |
| `pnpm check:agent-test-spelling` | 0 | green (sweep over the tree) |
| `pnpm check:cross-package-test-inputs` | 0 | `OK: 30 package(s) read
outside themselves, all declared, and turbo.json hashes every declared
glob` |
| `pnpm check:doc-authoring` | 0 | `✓ doc authoring guard:
sibling-package prose ids hold the baseline` |
| `pnpm check:driver-memory-census` | 0 | `check-driver-memory-census:
OK — every declaration is ledgered, every ledger entry is live` |
| `pnpm check:gitlink-declared` | 0 | `check-gitlink-declared: OK (11056
index entries -- 0 gitlink(s) at mode 160000 …)` |
| `pnpm check:refd-timer-probe` | 0 | `OK check-refd-timer-probe: 8856
source file(s) swept` |
| `pnpm check:watch-hint-literal` | 0 | `✓ check-watch-hint-literal: 73
declaration(s) across 4 rostered name(s)` |
| `node scripts/check-closing-keyword-parity.mjs` | 0 |
`check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords and
both measured separators …)` |
| `node scripts/check-closing-keyword-parity.mjs --self-test` | 0 | `✓
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` | 0 | `✓ comment-mask
corpus sweep [scripts/js-comment-mask.mjs]: 8862 files, 0 disagree, 0
unparseable` |
| `node scripts/pm/check-governed-queue-guard.mjs --self-test` | 0 | `✓
check-governed-queue-guard self-test: 296 cases pass` |
| `node scripts/pm/check-harness-current.mjs --self-test` | 0 |
`check-harness-current --self-test: all 26 cases passed.` |
| `pnpm --filter @objectstack/lint run check:doc-formula-expressions` |
0 (after `pnpm exec turbo run build --filter=@objectstack/formula
--filter=@objectstack/lint` under 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 lint` first 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, so `skip-changeset` is the declaration (applied through
`scripts/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
- Observed while opening this PR, noted and not filed (no class a/b/c:
not a credential, no declared contract):
`scripts/pm/fleet-write/dispatch.mjs --route` — and every tool that
re-execs itself with `--use-env-proxy` — prints the value of
`HTTPS_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-expressions`
exited 3 (PREREQUISITE NOT MET: `@objectstack/formula` and
`@objectstack/lint` not 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-ratchet` prints 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.
- The card body's suggested presence form `[ -n "$VAR" ] && echo set`
conflates 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](https://claude.ai/code/session_012dGTuJen61ECNHXYXKChoC)_
Co-authored-by: os-dev <noreply@anthropic.com>1 parent dc08c60 commit 4a87d09
2 files changed
Lines changed: 4 additions & 4 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
55 | 55 | | |
56 | 56 | | |
57 | 57 | | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
58 | 61 | | |
59 | 62 | | |
60 | 63 | | |
| |||
200 | 203 | | |
201 | 204 | | |
202 | 205 | | |
203 | | - | |
204 | 206 | | |
205 | 207 | | |
206 | 208 | | |
| |||
214 | 216 | | |
215 | 217 | | |
216 | 218 | | |
217 | | - | |
218 | 219 | | |
219 | 220 | | |
220 | 221 | | |
221 | 222 | | |
222 | 223 | | |
223 | 224 | | |
224 | 225 | | |
225 | | - | |
226 | 226 | | |
227 | 227 | | |
228 | 228 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
91 | 91 | | |
92 | 92 | | |
93 | 93 | | |
| 94 | + | |
94 | 95 | | |
95 | 96 | | |
96 | 97 | | |
| |||
201 | 202 | | |
202 | 203 | | |
203 | 204 | | |
204 | | - | |
205 | 205 | | |
206 | 206 | | |
207 | 207 | | |
| |||
0 commit comments