Repository navigation
Commit 8e505db
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
0 commit comments