Repository navigation
feat(pm): advisory_read relays an advisory's text sealed to a one-time key the seat alone holds - #29536
Conversation
… seal Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J8VVAng9QxNxoEzyWkqbW6
…ttery Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J8VVAng9QxNxoEzyWkqbW6
…k and --reveal Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J8VVAng9QxNxoEzyWkqbW6
… seat's advisory read at it Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J8VVAng9QxNxoEzyWkqbW6
…a failing case, not a crash Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J8VVAng9QxNxoEzyWkqbW6
|
domain:skills#1 · huangyiirene · session_01J8VVAnMUWA9rufXzFaEh4J Contract-review-claim: session_01J8VVAnMUWA9rufXzFaEh4J · head d2ee86c · 2026-11-02T18:12Z The seat review runs at CONTRACT_REVIEW_TIER in an isolated subagent (the seat session is not at that tier). |
|
domain:skills#1 · huangyiirene · session_01J8VVAnMUWA9rufXzFaEh4J Contract reviewServed-tier: Reviewer: an isolated contract reviewer at CONTRACT_REVIEW_TIER, read-only on GitHub, fed the card body and comments, the PR body, the net diff against the merge-base, the head's check-runs, contract-review.md and review-checklist.md, and the shape of cr15/cr17. The dev report was read as input, not as conclusions; every count below is this review's own measurement. This record carries no finding, no vulnerability mechanism, no advisory id and no advisory title; the self-test fixtures use made-up ids in GitHub's shape and generic marker sentences, and none is repeated here. No environment variable value is printed anywhere in this record. ① Derived judgmentsThe card's six change items, the five pinned cases, the marker test and the PR's claims, one at a time:
② Semver levelNone. No published package, no ③ Boundary flags
④ Design judgmentPer hits file, against the card's scenario (a seat that cannot read an advisory under its own account reads it through the public relay run without any field of it reaching a public surface):
Must-fix: none. Should-fix:
Check-runs at record time (2026-11-02T18:31Z): 46 check-runs on d2ee86c, all completed, 26 success, 20 skipped, 0 failure, 0 in progress. Success: Lint & Repo Gates, TypeScript Type Check, Test Core (and shards 1/8, 2/8), Governed Surface Queue Guard, Test UI, Lint · ESLint, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · tooling self-tests, Lint · scoped · slot lookup & corpus, Lint · scoped · query-options erasure, Lint · PM dispatch-gates, Lint · CI, release & engine gates, Type Check · workspace / source gates / debt ledger / consumer gates, PR-body guards, Check Changeset, Check PR Size, Dogfood Regression Gate, Plan Test Core shards, Auto Label, filter. Skipped by path filter: Build Core, Build Docs, Build UI Deps, Lint UI, Type Check UI, Build & E2E UI, Live E2E UI, Dogfood Verify CLI, Temporal Conformance, UI Published Artifacts and the opt-in lanes. Combined status Implemented-by: VERDICT: PASS with should-fixes (S1 PR-body Fixes/Part-of and the follow-up card; S2 the Generated by Claude Code |
… not, not "HTTP 0" On status 0 (`rest`'s word for a call that threw before any answer) the read row now says the request did not reach the platform, with the row status `not reached`. It no longer prints "HTTP 0" or appends the read-grant remedy, because the platform judged nothing. The exception's message is still printed nowhere, since it can carry the path and so the id. A self-test case pins this, and the run joins the marker and id sweep (19 runs). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J8VVAng9QxNxoEzyWkqbW6
Conflicts in scripts/pm/fleet-write/execute.mjs, resolved as follows: - the ops import list takes both sides: advisory_read's names and #29505's ADVISORY_STATE_SHAPE; - the battery map keeps #29505's advisory rows count and advisory_read's row; - advisoryPlatformText is imported again. #29505 moved the other advisory rows onto advisoryRefusalCause and dropped the import, but advisory_read's refused arm still quotes only the messages ADVISORY_PLATFORM_MESSAGES names (review S2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J8VVAng9QxNxoEzyWkqbW6
|
domain:skills#1 · huangyiirene · session_01J8VVAnMUWA9rufXzFaEh4J Contract reviewServed-tier: Reviewer: an isolated contract reviewer at CONTRACT_REVIEW_TIER, read-only on GitHub, fed cr20's PASS on d2ee86c, the dev's two commits, the replayed merge, the PR body, the head's check-runs, and the shape of cr16. This record judges only what moved since d2ee86c; cr20's judgments stand where the blobs are unchanged, and every count below is this review's own measurement. The record carries no finding, no vulnerability mechanism, no advisory id and no advisory title; the fixtures use made-up ids in GitHub's shape and generic marker sentences, none repeated here. No environment variable value is printed anywhere in this record. ① Derived judgmentsThe two commits, one claim at a time:
② Semver levelNone, unchanged from cr20. No published package, no ③ Boundary flags
④ Design judgmentPer hits file, only execute.mjs and ops.mjs moved in substance:
Must-fix: none. Should-fix: none. Check-runs at record time (2026-11-02T19:11Z): 49 check-runs on 453d4e1, all completed, 26 success, 22 skipped, 1 failure, 0 in progress. The one failure is the superseded Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:skills#1 · huangyiirene · session_01J8VVAnMUWA9rufXzFaEh4J 维护者速读
席位核对 — ACCEPT · domain:skills#1 · head
|
domain:skills#1 · huangyiirene · session_01J8VVAnMUWA9rufXzFaEh4J
Requested by Irene · project thread
Part of #29420
Clause-②: no
Metadata-protocol: none
维护者速读
advisory_read。席位进程每读一次就在内存里现生成一对一次性 X25519 密钥,只把公钥随请求送出;中继用本笔独有的repository-advisories: read令牌读公告,当场用这把公钥封装(只用 Node 自带crypto),只把密文经运行通知回传;席位用内存里的私钥解开,加--reveal才把明文印到自己的 stdout。scripts/pm/fleet-write/四个脚本、两份技能参考各一行和行数棘轮,不发布任何包,不改工作流;回滚即 revert 本 PR。公开运行上看得到的只有状态码、字节数、分片数和密文本身。scripts/pm/fleet-write/execute.mjs的advisoryReadAction(先过闸再封装再分片,封装失败什么都不发);scripts/pm/fleet-write/ops.mjs的sealAdvisoryRead和advisory_read行(行上 ⚖ 注写明发送者闸的两种选择);scripts/pm/fleet-write/dispatch.mjs的armAdvisoryReads与--reveal。node scripts/pm/fleet-write/execute.mjs --self-test,预期打印 259 个用例在 22 组里全部通过,其中 advisory_read 一组 17 例,标记扫描覆盖 19 次运行无一命中。collaborator(推荐:发送者必须在该公告的协作者名单里),或把行上的senderGate改为target-write(对目标仓有写权限即可)。What changes
scripts/pm/fleet-write/ops.mjs: theadvisory_readrow and everything it needs.ghsa_idandpublic_key, nothing optional, travelsalone, spendsrepository-advisoriesat its owngrant: 'read', and carriessenderGate: 'collaborator'with a ⚖ comment recording the choice for the maintainer. Its one request is a GET of the advisory, and it carries anadvisoryReadmarker so the executor takes the sealed path.public_keyfield kind (x25519): base64 SPKI DER of exactly 60 characters, the X25519 prefix pinned by a regular expression. A trial derivation refuses low-order points.sealAdvisoryRead/openAdvisoryRead, Nodecryptoonly:advisoryReadPartscuts the sealed text into at most 10 notice parts of 4,000 characters each, inside the runner's measured caps (10 notices a step, messages cut at 4,096 characters). That is 40,000 sealed characters, or 29,939 bytes of answer.advisoryReadFenceFailures) cover the read itself, the advisory, and the sender. An unknown gate value fails closed.advisoryReadCallprints a placeholder path where every other row prints its real one. The landed and over-cap texts carry byte and part counts alone.scripts/pm/fleet-write/validate.mjs: thex25519kind's refusal describes the value by type and byte count and never echoes it. Grants are now per row (OPS[op].grant, falling back to the permission's default level), so a read's stroke mintsrepository-advisories=readand every other advisory stroke still mints write. The stroke's token-permission map gives write precedence when two rows meet, whichalonealready prevents. A new battery holds 28 cases.scripts/pm/fleet-write/execute.mjs:advisoryReadActionruns these steps in order:rest's status 0) is FAILED saying the request did not reach the platform, row statusnot reached, with no HTTP status, no grant remedy and never the exception's message;The row and the summary carry the placeholder call, the status, and the counts. A new battery holds 17 cases.
scripts/pm/fleet-write/dispatch.mjs: arming and opening at the seat.armAdvisoryReadsmakes one key pair per read in this process and puts the public half into the action. Apublic_keysupplied in the actions file is refused (exit 2) without being echoed.openRunAdvisoryReadsreassembles the run's notice parts and opens them with the private half. A read that does not open is UNVERIFIED (exit 6).--revealprints the opened text to stdout, and nothing else ever prints it.--revealwithout a read is exit 2.--jsoncarries state, byte count, field names and parts, never text..claude/skills/pm-dispatch/references/rest-channel.md: one line namingadvisory_read..claude/skills/pm-dispatch/references/security-findings.md: one line pointing a seat's advisory read at the relay.scripts/pm/check-skill-line-ratchet.mjs: those two rows, 81 → 82 and 55 → 56, the measured delta of +1 each.Why
A seat's own account is refused when it reads an advisory, and the relay's run is public. Without a seal, any read through the relay puts the advisory's text on the run's log and annotations. The card's design keeps the text in plaintext only inside the seat process that asked: the private key exists for one read, in memory, and is never written anywhere.
Tests
origin/main8e505dbvalidate.mjs --self-testexecute.mjs --self-testdispatch.mjs --self-testEach new battery raises its file's battery floor by one.
The card's pinned cases each have a case:
ghsa_idorpublic_keyis refused before any request. Validate covers wrong length, wrong prefix, not base64, a low-order point and a non-string, each refused without an echo. The executor's refused payloads make zero requests.advisory_readbeside another action is refused. This holds beside a comment, beside another advisory row, and for two reads together.repository-advisories=read, never write and never actions. Every other stroke mints no advisory grant. Both are checked through the validator's real process output.not reached: no "HTTP 0", no grant remedy, and the exception's message printed nowhere.Ablations. Every mutation was re-run at head 453d4e1 (after the merge of main) with
scripts/ablation-replace.mjs, in a clean worktree, against the committed blobs: opsce0656b3454b, validate8ea5abcdd17a, executec328a12cd284, dispatch51075987d172. In every run the anchor read 1 → 0, the self-test exited 1, and the restore was proved (blob == HEAD,git diff HEADempty). The worktree had 0 changed files afterwards.x25519refusal echoes the value--revealalone: trueremoved from the roweslinton the four relay scripts and the ratchet: exit 0.Acceptance notes
collaborator, which the card recommends: the sender must appear, matched case-insensitively, in the advisory's owncollaborating_users. Teams are not read, so a sender who reaches the advisory only through a team is refused. The alternative istarget-write, set by changing the row'ssenderGate: write on the target repository is enough, the same gate every other relay row uses. Both are tested, and a value that is neither fails closed. The default holds until the ruling.repository-advisories: read. GitHub documents this GET under the read grant, but only the write grant's read was measured (run 41415522766, recorded in the card). The first live read is the measurement of record. If the platform refuses there, the action fails carrying a remedy that says raising the row to write is the maintainer's call, never a retry under a wider token. The live read is not part of this PR.permission-repository-advisoriesinput already passes whatever level the validator writes. Its comment near line 154 of.github/workflows/fleet-write.ymlis now stale: it still says "THREE consumers" and namespermission_repository_advisories=write. The file is Tier H and outside this card's surface, so the fix is a later PR of its own.execute.mjs. The rule was that the second to land either keepsadvisoryPlatformTextimported or moves the read's refused arm onto fix(pm): the relay reads no runner 403 as the egress proxy's, and a refused advisory row names its cause #29505'sadvisoryRefusalCause, then re-runsnode scripts/pm/fleet-write/execute.mjs --self-teston the merged tree. fix(pm): the relay reads no runner 403 as the egress proxy's, and a refused advisory row names its cause #29505 landed first, so this PR mergedmain(merge commit 453d4e1, no rebase) and resolved it there.advisoryPlatformTextis imported again.ADVISORY_PLATFORM_MESSAGESnames it.advisoryRefusalCausewithholds a message only when it quotes prose the run already knows, and a refused read knows none: its payload is the id and the key, and no advisory was read. So the move would print whatever message the platform sent, and the read battery's 404-with-marker fixture would reach the row.execute.mjs --self-testis 259 cases across 22 batteries, and validate and dispatch are unchanged at 303 and 315.KeyObjectin a Map). No key and no plaintext is written to disk, env, the payload or GitHub. No dependency was added.Co-Authored-Bynaming the model, against the model-free trailer rule (check:commit-card-trailers --range origin/main..HEADflags both). This clone had no pre-push hook installed, so nothing refused the push. The branch is notclaude/issue-*, which makes it shared under AGENTS.md §3, so it was not rewritten. The squash message is taken from this body, so neither trailer lands onmain.skip-changeset.scripts/pm/**and.claude/**do not publish.Verification
dispatch-gates at 453d4e1: 47 derived, 47 run, all exit 0
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 47 families from the 7 changed paths (1,262 changed lines against merge base 8e505db, under the 3,000 human-merge threshold). Each was run as printed and recorded with its exit code before any pipe. Reconciliation: "✓ dispatch-gates --ran: 47 derived famil(ies) accounted for — 47 run, 0 NOT-MEASURED (a DERIVED zero — all 47 recorded an exit code and none of them is 3)."Human Minimum
packages/specholds no runtime logic (not touched).pnpm lintpasses (eslint on the changed scripts: exit 0; the repo-wide run is CI's).main, one pull request.packages/spectouched: no.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Dispatched by
domain:skills#1 · huangyiirene · session_01J8VVAnMUWA9rufXzFaEh4J; dev = card threadsession_01J8VVAng9QxNxoEzyWkqbW6.🤖 Generated with Claude Code
https://claude.ai/code/session_01J8VVAng9QxNxoEzyWkqbW6
Generated by Claude Code