Skip to content

Commit 8e505db

Browse files
fix(pm): the relay reads no runner 403 as the egress proxy's, and a refused advisory row names its cause (#29505)
domain:skills#1 · huangyiirene · session_01J8VVAnMUWA9rufXzFaEh4J <!-- claude-projects-attribution: {"github_login":"huangyiirene"} --> _Requested by **Irene** · [project thread](https://claude.ai/code/project/chan_01TJXtQ1tVD26ecLDZxFS67x?thread=cmsg_01TJXtQ1tVD26ecLDZxFS67xCsADprEDxuFzAGDU2n4oTc)_ os-dev · domain:skills#1 dispatch · session_01J8VVAoLff4MC52HYARNBKP Part of #29402. The card's third bullet (the seat records the measured live 403 reason and its seat-side rule in `references/rest-channel.md`) stays open for the seat after a live reading. Clause-②: no Metadata-protocol: none ## 维护者速读 - 中继运行器上没有出口代理,所以 `write-pace` 在那里不再把 GitHub 的 403 当成代理拒绝;403 改走平台退避路径(规则 ③),不再退回名额。 - 代理识别只认正向标记:代理自己的提示语,或指向代理自身文档站点的 `documentation_url`。"不是 github.com" 不再算数。 - 安全公告行被拒时,行内打印平台的 `message` 和 `documentation_url`(各限 200 字符、压成一行),再接原有的处置建议,所以交给维护者的读数里有原因。 - ⛔ 载荷不进日志:若平台消息或链接复现了载荷或预检读到的公告正文中的任意 16 字符片段,就不打印,并在行内注明。标记测试仍为零泄漏。 - 实际原因(如每日报告上限)要等一次真跑读到后再写进 `rest-channel.md`,本 PR 不含。 ## What changes - **`scripts/pm/write-pace.mjs`** - New `inActionsRunner(env)` (`GITHUB_ACTIONS === 'true'`) and `proxyAnswered(body, env)`: never true on an Actions runner, `isProxyRefusal(body)` elsewhere. `stopSignalFrom` (now taking `env`, default `process.env`) and `noteResponse` (its `deps.env`) act on `proxyAnswered`, so a GitHub 403 inside the relay runner is judged by rule ③ and never gets the proxy's slot give-back and "No stop marker written" line. - `isProxyRefusal` stays pure (body only, so `label-write.mjs` and `fleet-token.mjs` behave the same in a seat container) but needs a positive mark: the proxy's own message, or a `documentation_url` on the proxy's docs hosts (`PROXY_DOC_HOSTS`: anthropic.com, claude.com, claude.ai). A URL that is merely not on github.com no longer counts. GitHub's own refusals can point at a short link or a changelog post on another host, which is the likely shape of the misreading the card measured. - **`scripts/pm/fleet-write/execute.mjs`** - New `advisoryRefusalCause(answer, known)`: the refusal's `message` and `documentation_url`, from its own answer only (never `errors`, never the rest of the body). The message is flattened to one table-safe line and capped at `ADVISORY_REFUSAL_MESSAGE_CAP` (200, cut with a note). The URL is printed only when https-shaped and within `ADVISORY_REFUSAL_URL_CAP` (200). No message gives "cause unread". - `known` is `advisoryProseOf` of the request body and, for the PATCH rows, of the advisory the preflight read. Ids, states, logins and URL-shaped words are left out. A message or URL (percent-decoded too) that reproduces any 16-character run of that prose is withheld, and the row says so. The message is judged raw as well as flattened, against the known prose as given and flattened the same way, so a quoted run that straddles a pipe, a backtick, a quote or a line break is still caught. - Status 0 (the fetch threw) never reads as the platform's reason: the row says the request did not reach the platform, and the transport's text is dropped if it quotes the payload. - `confirmAdvisory` and `advisoryPreflight` print that cause between the status and `ADVISORY_ACCESS_REMEDY`, so "put this reading to the maintainer" now hands over a reading that names the platform's reason. This replaces the six-string `ADVISORY_PLATFORM_MESSAGES` filter on these two paths. The header says the same. ## Why Card #29402 measured three defects on the live relay. After the first `advisory_report`, every later report answered 403. `write-pace` printed that the egress proxy had refused it, which cannot be true on a runner, so no stop marker was written and the slot was given back. The row printed no message because the platform's was not on the six-string list. So the "put this reading to the maintainer" advice went out with no cause in it, and nobody could tell a reporter limit from a missing permission. ## Verification At 3f3345a, in objectstack-ai/objectstack (the contract review's S2 and S3 are the second commit; 478c92a is the first): - `pnpm check:pm-write-pace`: 118 cases pass across 12 batteries (rule ③ battery 14 → 19 cases). It also passes under `GITHUB_ACTIONS=true`, the shape CI runs it in. - `pnpm check:pm-fleet-write-execute`: 242 cases pass across 21 batteries (advisory battery 27 → 32 cases). The marker sweep now covers 46 runs, up from 41, and finds the marker in no log line, row, summary, annotation or pace-log line. - New cases cover: - a runner 403 that takes rule ③ even with a proxy-shaped body; - a plain runner 403 that keeps its slot spent and prints no proxy line; - GitHub short-link, changelog and look-alike-host URLs that are not read as the proxy's; - an unlisted 403 message and its short link printed in the row, the summary and the log; - a message quoting the payload, and one quoting the preflight's advisory, withheld; - a percent-encoded quoting URL dropped while the message is kept; - no message; - the 200-character cap, and newline, pipe, backtick and quote flattening; - an ill-shaped URL not printed; - a quoted payload run straddling a pipe, withheld only because the raw message is judged too; - status 0 reading as a transport failure, never as the platform's reason. - Ablations, each made on the committed tree and restored with `git checkout HEAD --`, with `git status` clean afterwards: - At 478c92a: `quotesKnown` returning false turns 5 of 240 execute cases red, the marker sweep among them, and `proxyAnswered` ignoring the runner turns 3 of 118 write-pace cases red. - At 3f3345a: dropping the raw-message check turns 2 of 242 red (the straddle case and the marker sweep), and dropping the status-0 branch turns 1 of 242 red. After each, `git hash-object` of the file equals the head blob. - `node scripts/pm/dispatch-gates.mjs --commands` derives 36 commands for the two paths. At 478c92a all 36 exited 0, each exit captured before any pipe, and `--ran` reconciles 36 of 36. At 3f3345a, which changes `execute.mjs` alone, `check:pm-fleet-write-execute`, `check:pm-write-pace`, `check:nul-bytes`, `check:scripts-symbol-anchors`, `check:doc-authoring`, `check-self-test-wired` and eslint over the file were run again, all exit 0. Also clean: `pnpm check:doc-authoring`, eslint over both files, and the self-tests of `label-write`, `fleet-token`, `post-stamped`, `issue-create`, `close-cards`, `issue-transfer`, `sweep-closed-cards`, `sweep-stale-finding`, `fleet-write/dispatch` and `fleet-write/validate`. - No changeset: only `scripts/pm` changes, which publishes nothing, so the PR carries `skip-changeset`. ## Acceptance notes - **Not measured live.** The platform's actual 403 body for a second private report is still unread, because the relay used to hide it. The first refused report after this lands will print it. That reading, with its seat-side rule, goes into `references/rest-channel.md` as the card's third bullet asks. It is left out of this PR because the reason has not been measured. - **Whether that 403 writes a stop marker depends on what it carries.** Rule ③ fires on a 429, a ratelimit verdict, a body naming the secondary limit, a `retry-after` or `x-ratelimit-remaining: 0`. A bare 403 carrying none of them writes none, by rule ③'s own judgment ("a plain 403 does NOT"). The runner's pace log is fresh on every run anyway, so stopping repeat sends from seats rests on the row's cause and the remedy's ⛔ do-not-re-send. - **Doc drift outside this PR's surface:** - `ops.mjs`'s header still says a refusal quotes the platform's message only when it is one `ADVISORY_PLATFORM_MESSAGES` names. - `ADVISORY_ACCESS_REMEDY` still says what the App may do "is decided by the first live run". - `ADVISORY_PLATFORM_MESSAGES` and `advisoryPlatformText` now have no production caller; `validate.mjs`'s self-test is the only one left. - `label-write.mjs` line 420 still describes the proxy mark as "a `documentation_url` that is not GitHub's". - The surface was held to the two files, so all of these are left for the seat's follow-up. - `execute.mjs` is also being changed on #29420's branch. Whichever lands second merges `main` (no rebase), and the seat re-reviews the delta. - PR #29222's ablation B ("a refused answer quotes any platform message → marker test red") described the guard this PR replaces. The new guard is the prose-quote check, and its own ablation is above. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (Not touched.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; eslint over the changed files passes. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. Nothing publishes, so no changeset; both declaration lines are filled in. - [x] 6. `packages/spec` not touched. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English, except the 维护者速读 section, which the lane asks for in Chinese. - [ ] The *Before Submitting* list in CONTRIBUTING.md is done. (Left unchecked: the workspace-wide `pnpm lint` and `pnpm test` were not run; the scripts touched have no package tests, and eslint over the changed files was run.) 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01J8VVAoLff4MC52HYARNBKP --- _Generated by [Claude Code](https://claude.ai/code/session_01J8VVAoLff4MC52HYARNBKP)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 390bd2a commit 8e505db

2 files changed

Lines changed: 205 additions & 32 deletions

File tree

0 commit comments

Comments
 (0)