Repository navigation
fix(pm): fleet-write derives the seat session from the container; the workflow’s Actions state is the kill switch; size-routed direct for post-stamped - #19760
Merged
os-support-ai merged 1 commit intoSep 23, 2026
Conversation
…reads the relay workflow's Actions state
The seats' allow rules are literal command prefixes, so a write spelled with
a leading OS_FLEET_SESSION=… assignment matched none of them and waited on a
permission it never got. sessionSource() now reads OS_FLEET_SESSION first
(explicit wins, validation unchanged) and, when it is absent, derives the
session from the container's CLAUDE_CODE_REMOTE_SESSION_ID (cse_<id> →
session_<id>); the route's reason names which source was read, and --route
prints the session and its source.
Condition ③ (relay live) is now the workflow file on main AND a 200 from
GET /repos/{board}/actions/workflows/fleet-write.yml with state active; any
other state (disabled_manually verbatim — the maintainer's kill switch),
status or no answer is not live, named. Cached once per process, reached only
after ① and ②; an explicit OS_FLEET_TRANSPORT=dispatch against a disabled
workflow is a prerequisite refusal, never a silent fall-back.
post-stamped gains the size route: a rendered body over MAX_BODY_BYTES under
auto takes direct for THAT write with one printed line naming the bytes, the
cap and the identity; explicit dispatch over the cap is refused naming the
bytes. Nothing else re-routes and there is no compression codec.
Every --help line and header comment that told a seat to export
OS_FLEET_SESSION now says the session is read from the container and the
variable overrides it. Self-tests cover the derivation, the state read and the
size route; the with-fleet and dispatch CLI cases clear the inherited
container variable so they hold inside cloud containers too.
Claude-Session: https://claude.ai/code/session_01GnJon4xkRvphn4w28xx3An
Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Sep 23, 2026
os-support-ai
enabled auto-merge
September 23, 2026 00:52
os-support-ai
deleted the
claude/issue-19755-fleet-write-derive-session
branch
September 23, 2026 01:19
This was referenced Sep 23, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
… the relay can arm auto-merge (objectstack-ai#19767) Fixes objectstack-ai#19762 ## What changed Touches `.github/workflows/**`, so this is a draft the maintainer merges by hand. 1. **The mint step grants `permission-contents: write`.** In `.github/workflows/fleet-write.yml`, `actions/create-github-app-token@v2` gains `permission-contents: write` beside the three existing `permission-*` inputs, with a comment naming the one consumer — GitHub's `enablePullRequestAutoMerge` / `disablePullRequestAutoMerge` mutations require `contents: write` on the token (measured on the relay's first live `automerge_enable`: "Resource not accessible by integration") — and stating that the op table has no contents op, so the grant is spent only by those two mutations. The header's permission list says the same. 2. **The op table spends it truthfully.** `PERMISSIONS` in `scripts/pm/fleet-write/ops.mjs` gains `contents: 'write'`; the `automerge_enable` and `automerge_disable` rows carry `permission: 'contents'`, so the validator's permission read stays a true statement of what each row spends. The header paragraph on permissions is updated. 3. **Self-tests** (`validate.mjs --self-test`, the refused-by-construction battery, 9 → 14 pinned cases): the map is exactly `issues` / `pull-requests` / `contents` write and `metadata` read; the two auto-merge rows spend `contents`; no other row does; every `contents` spender is an auto-merge GraphQL mutation, never a REST path; and the workflow's `permission-*` inputs, read from the file on disk, mirror `PERMISSIONS` exactly — a grant added on one side without the other reds. 4. **Nothing else.** No new op, no change to the sender gate, caps, ceilings, the executor or the four write tools. ## Verification record **Batteries, each run alone with its exit captured before any pipe** (worktree at 182209b, base `origin/main` dc1b986): | command | exit | verdict line | |---|---|---| | `pnpm check:pm-fleet-write-validate` | 0 | 74 cases pass across 7 batteries (was 69) | | `pnpm check:pm-fleet-write-execute` | 0 | 48 cases pass across 9 batteries | | `pnpm check:pm-fleet-write-dispatch` | 0 | 64 cases pass across 9 batteries | | `pnpm check:pm-with-fleet` | 0 | 31 cases pass | | `pnpm check:pm-post-stamped` | 0 | 596 cases pass across 20 batteries | | `pnpm check:pm-label-write` | 0 | 88 cases pass across 10 batteries | | `pnpm check:pm-issue-create` | 0 | 42 cases pass across 6 batteries | | `pnpm check:pm-close-cards` | 0 | 111 cases pass across 12 batteries | | `pnpm check:pm-write-pace` | 0 | 109 cases pass across 12 batteries | | `pnpm check:workflow-status-functions` | 0 | OK (39 workflow files) | | `pnpm check:workflow-step-name-quoting` | 0 | OK (39 workflow files + 2 composite actions) | | `pnpm check:required-contexts` | 0 | OK | `npx eslint --no-inline-config` on the two changed `.mjs` files: exit 0. **Gates** — `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` on the edited tree derived 54 families (3 paths vs merge base dc1b986; 52 changed lines, under the human-merge threshold). Each was run alone with its exit code captured before any pipe; all 54 exited 0. `--ran` with the codes recorded: `54 derived famil(ies) accounted for — 54 run, 0 NOT-MEASURED (a DERIVED zero — all 54 recorded an exit code and none of them is 3)`. Repo-wide `pnpm lint` is CI's run; the changed files were linted directly (above). The live grant itself cannot be proven before merge (a workflow change runs only once it is on `main`); the card's post-merge verification is the director seat's: a two-action relay request (`pr_draft` + `pr_ready`, or `automerge_enable` on the next queued PR) reading back `enabledBy` = `objectstack-fleet[bot]`. ## Changeset None: `.github/workflows/**` and `scripts/pm/**` publish nothing from any released package, so `skip-changeset` applies. This dispatch forbids label writes, so the label is the seat's to apply. ## Acceptance notes - Branch cut from `origin/main` at dc1b986, not on top of the objectstack-ai#19755 branch; PR objectstack-ai#19760 is still landing and is not touched here. Both edit `scripts/pm/fleet-write/ops.mjs` in disjoint regions (the session constants there, the permission map and two rows here), so no textual conflict is expected once objectstack-ai#19760 merges; if one appears, this branch rebases onto `main`. --- _Generated by [Claude Code](https://claude.ai/code/session_01GnJon4xkRvphn4w28xx3An)_ Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…the App identity (objectstack-ai#19770) Part of objectstack-ai#19739 Clause-②: no ## What changed One file, `.claude/skills/pm-dispatch/references/rest-channel.md`, section 〈通道边界〉: one paragraph of nine rule lines stating how the App identity is reached. - `objectstack-fleet[bot]` is reached through the `fleet-write` relay; the four write tools (`post-stamped`, `label-write`, `issue-create`, `close-cards`) and `with-fleet.sh --via auto` share one selector. - `dispatch` only when three conditions hold — `CCR_AGENT_PROXY_ENABLED=1`, a session id, a live relay — otherwise `direct`, with one printed line. - The session id is derived from the container's `CLAUDE_CODE_REMOTE_SESSION_ID`; `OS_FLEET_SESSION` overrides it for local checkouts and tests only. - ⛔ Never prefix a command with it: the allow rules are literal prefixes, a prefixed write drops onto the classifier and the seat stalls. - Live = the workflow file on `main` AND its Actions state `active`; the maintainer disabling it in the Actions UI turns the whole fleet back to `direct`. - The dispatch goes to objectstack with the session token; the run executes on the runner with a per-run App token narrowed to the target repo. - The closed op list lives in `scripts/pm/fleet-write/ops.mjs`; reads stay direct. - A `post-stamped` body over 60,000 bytes takes `direct` for that one write, with one printed line, as the seat's own user. - A failed run (`post-stamped`/`close-cards` 4, `label-write`/`issue-create` 5) and UNCONFIRMED (6) are read, ⛔ never retried. Spellings are taken from the objectstack-ai#19755 branch `claude/issue-19755-fleet-write-derive-session` at 6470e4e (PR objectstack-ai#19760): `sessionSource` in `scripts/pm/fleet-write/dispatch.mjs`, `CONTAINER_SESSION_ENV` in `ops.mjs`, `relayLive`'s Actions-state read, the `with-fleet.sh --via auto` paragraph, and `sizeRoute` / `MAX_BODY_BYTES` in `post-stamped.mjs`. Everything else is read from `main` at 544767d (`.github/workflows/fleet-write.yml`: `repositories:` = the validated target, `permission-issues: write`, `permission-pull-requests: write`, `permission-metadata: read`). **One correction to the card's item 3 wording.** The failed-run exit is not `4` fleet-wide. Each tool books it in its own register on that branch: `post-stamped` 4 (`EXIT_NOT_STORED`, via `relayExitFor`), `close-cards` 4 (`EXIT_HALF_WRITE`, and the same 4 for an unconfirmed close), `label-write` and `issue-create` 5 (`exitForResult` → `EXIT_PLATFORM_REFUSAL`). UNCONFIRMED is 6 in every tool that takes the relay. The paragraph states the registers as they are on that branch; the rule — read, never retry — is the same in all four. ## Line budget — ceiling 82, headroom 0, paid in place (+9 / −9) Nine lines are retired. Each is a restatement; the line that keeps the rule is named beside it (line numbers on `main` at 544767d). | retired (`rest-channel.md` line) | the rule survives at | |---|---| | 9 — closed session gate: repo-scoped 403, no `gh`, MCP and git fine | `platform-readings.md` 108 (the 403 shape), 114–115 (git first, MCP third), 142 (no `gh` in the container) | | 10 — run one repo-scoped probe before choosing a channel | `platform-readings.md` 107, 127 | | 13 — a rate-limit refusal binds to the refused identity; switching channels is a retry | `platform-readings.md` 122, 125 | | 14 — the other side is a way out only under a different identity (`GET /user` vs the refused id) | `platform-readings.md` 125 (the `GET /user` test), 96 (one identity inside a seat) | | 38 — label/assignee writes go through `label-write.mjs` (four steps, read-back) | `SKILL.md` 394; `os-dev.md` 304; the tool's own header | | 44 — every write carries `Content-Type: application/json`; the 415 and its discriminant | `platform-readings.md` 119–120 | | 54 — never MCP `update_pull_request`; ready/draft and auto-merge take the ccr routes | `.claude/settings.json` deny list; `SKILL.md` 610; the ccr rows at 48 and 52 of this file | | 60 — a REST-gated seat falls back to one targeted MCP `search_issues` | `platform-readings.md` 115, 211 | | 72 — whether auto-merge goes through the queue is answered by trying the action, not a field | `platform-readings.md` 9; rows 73–75 of this file keep the three criteria | No line was re-wrapped; every line is ≤ 120 bytes (`check:pm-skill-ratchet` green on the head). ## Landing order ⛔ This PR lands only after objectstack-ai#19755 (PR objectstack-ai#19760) merges: the paragraph states what that PR makes true (the container-derived session, the Actions-state half of the liveness read, the size route). Not enqueued by this dispatch. Tier S (`.claude/**`), `CONTRACT_REVIEW_TIER` — the director seat renders the `## Contract review` of record. `skip-changeset`: nothing published from any released package; this dispatch forbids label writes, so the label is the seat's to apply. ## Verification Gates derived by `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` on the edited tree at 9f49c33; each run alone, exit captured before any pipe (`cmd > log 2>&1; ec=$?`). | command | exit | verdict line | |---|---|---| | `node scripts/check-closing-keyword-parity.mjs` | 0 | • packages/spec/CHANGELOG.md -- 6080503 bytes exceeds the sweep's 2097152-byte cutoff for UNREGISTERED files | | `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 r | | `node scripts/check-comment-mask-corpus.mjs` | 0 | ✓ comment-mask corpus sweep [scripts/js-comment-mask.mjs]: 7011 files, 0 disagree, 0 unparseable, 92.9s (compa | | `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 | objectstack-ai#11673). | | `pnpm check:agent-test-spelling` | 0 | which drives this same sweep RED over a temp tree on disk. | | `pnpm check:cross-package-test-inputs` | 0 | OK: 29 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob (6 of them | | `pnpm check:doc-authoring` | 0 | ✓ doc authoring guard: sibling-package prose ids hold the baseline — 819 pinned site(s) across 231 file(s), 90 | | `pnpm check:driver-memory-census` | 0 | check-driver-memory-census: OK — every declaration is ledgered, every ledger entry is live, and every ruled fi | | `pnpm check:gitlink-declared` | 0 | check-gitlink-declared: OK (9246 index entries -- 0 gitlink(s) at mode 160000; no .gitmodules in the index, so | | `pnpm check:nul-bytes` | 0 | check-nul-bytes: OK (scanned 9239 text file(s) -- 9239 tracked, 0 untracked-not-ignored; skipped 7 binary; no | | `pnpm check:pm-governed-merges` | 0 | live: the real generator declared 9 output(s) and certified this tree | | `pnpm check:pm-skill-id-lint` | 0 | ✓ check-skill-id-lint: 30 file(s) clean (pattern /#[0-9]{3,}/g). | | `pnpm check:pm-skill-ratchet` | 0 | ✓ check-skill-line-ratchet: declared cross-file moves: 1, total ceilings down 9 lines. | | `pnpm check:refd-timer-probe` | 0 | 1 code site(s), all inside the approved module, which is present and still reads it. | | `pnpm check:skill-frame-sync` | 0 | binding sentence present in it; 1 count mention(s) agree; 77 markdown files scanned for undeclared copies. | | `pnpm check:watch-hint-literal` | 0 | ✓ check-watch-hint-literal: 71 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 47, ROOT_FILE_ | Also run on this tree, named by the dispatch and not in the derived list: `pnpm check:skill-frame-freshness` exit 0 · `pnpm check:pm-governed-prose` exit 0. `--ran` reconciliation: ✓ dispatch-gates --ran: 17 derived famil(ies) accounted for — 17 run, 0 NOT-MEASURED (a DERIVED zero — all 17 recorded an exit code and none of them is 3). ## Acceptance notes - `scripts/pm/with-fleet.sh --read -- true` in this cloud container (a probe of the direct path before opening the PRs): the mint `POST /app/installations/{id}/access_tokens` is refused by the egress proxy (HTTP 403 "not permitted through this proxy"), `classifyHttp` read that 403 as `ratelimit`, and `write-pace` wrote a 30-minute STOP MARKER for the App token's key (`f665bb8f6447`). A proxy path refusal is not a back-off signal; the marker blocks nothing this session writes (the session token's key `f07d7417ce88` carries no marker), but it would block a seat that can mint. Reported on the card for the seat to file; not fixed here (`scripts/pm/**`, outside this diff). - `objectstack-ai#19755 remains open`; its PR objectstack-ai#19760 is the head this text was read from. If that branch changes the reason text, exit registers or the cap before it merges, this paragraph is re-read against the merged text before the contract review. --- _Generated by [Claude Code](https://claude.ai/code/session_01XPUwWGnmky4PXPofNsf2w7)_ Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #19755
What changed
scripts/pm/**only — the fleet's own write tools, not on the governed register.sessionSource(env)infleet-write/dispatch.mjsreadsOS_FLEET_SESSIONfirst (explicit wins, validation unchanged; an explicit value that is malformed is refused, never replaced). When it is absent, the container'sCLAUDE_CODE_REMOTE_SESSION_ID—cse_ID— is read and becomessession_ID; malformed or absent givesnullas before. The route's reason names which source was read, and--routenow prints the session and its source. The container variable's name isCONTAINER_SESSION_ENVinfleet-write/ops.mjs, besideSESSION_ENV, with its shapeCONTAINER_SESSION_SHAPEbesideSESSION_SHAPE.main(today's read) ANDGET /repos/{board}/actions/workflows/fleet-write.ymlanswering 200 withstate === 'active'. Any other state, status or no answer is not live, the reason naming it —disabled_manuallyverbatim when that is the state, described as the maintainer's kill switch. Both reads are cached once per process and reached only after ① and ②. ExplicitOS_FLEET_TRANSPORT=dispatchagainst a disabled workflow is a prerequisite refusal (exit 3), never a silent fall-back.auto, the op table, the executor, the workflow file, every allow rule. Every--helpline and header comment that told a seat to exportOS_FLEET_SESSION(dispatch.mjs, label-write.mjs, issue-create.mjs, with-fleet.sh) now says a cloud seat's session is read from the container andOS_FLEET_SESSIONoverrides it (a local checkout, a test). No documented spelling carries a prefix.active/disabled_manually/ another state / a 200 without a state / 404 / 500 / no answer / cached once / reached only after the file read / strict refusal), and the CLI (--dry-runand--routewith the container variable alone). The dispatch and with-fleet CLI cases clear the inherited container variable, so both batteries hold inside a cloud container.auto(addendum, item 6).sizeRoute(route, body)inpost-stamped.mjsmeasures the RENDERED body in UTF-8 bytes againstMAX_BODY_BYTES(60,000; the platform'sclient_payloadcap is 64 KB). Over it underauto, THAT write takesdirectand the transport line becomes one line naming the bytes, the cap and the identity used (the seat's own user, notobjectstack-fleet[bot]). ExplicitOS_FLEET_TRANSPORT=dispatchover the cap is refused (exit 3) naming the bytes. A route already direct, or already carrying an error, is untouched; a refused op, a failed run and UNCONFIRMED keep their register;label-write,issue-create,close-cardsare not size-routed; no compression codec (pinned by import shape).Verification record
--routein this cloud container, with NOOS_FLEET_SESSIONset (the seat's own id from the claude-code-remoteget_sessiontool, fieldccr.id, issession_01GnJon4xkRvphn4w28xx3An; the container carriesCLAUDE_CODE_REMOTE_SESSION_ID=cse_01GnJon4xkRvphn4w28xx3An— same tail). Angle-bracket placeholders in the pasted reason are spelledIDhere for the body sanitizer.Before this change the same command in the same container answered
"transport":"direct","failed":"session".Size route, live dry runs in this container (
node --use-env-proxy scripts/pm/post-stamped.mjs --comment=19755 --file=… --dry-run; nothing written). A 60,159-byte rendered body:A small body from the same tree:
post-stamped: transport dispatch — … the session was derived from the container's CLAUDE_CODE_REMOTE_SESSION_ID … its Actions state is active …. The disabled-workflow case is proven in the dispatch self-test's fake platform; the live workflow was not touched.Batteries, each run alone with its exit captured before any pipe (worktree at 6470e4e):
pnpm check:pm-fleet-write-validatepnpm check:pm-fleet-write-executepnpm check:pm-fleet-write-dispatchpnpm check:pm-with-fleetpnpm check:pm-post-stampedpnpm check:pm-label-writepnpm check:pm-issue-createpnpm check:pm-close-cardspnpm check:pm-write-pacenpx eslint --no-inline-configon the five changed.mjsfiles: exit 0, no findings.Gates —
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandson the edited tree derived 38 families (6 paths vs merge base c120dbd; 449 changed lines, under the human-merge threshold). Each was run alone with its exit code captured before any pipe; all 38 exited 0.--ranwith the codes recorded:38 derived famil(ies) accounted for — 38 run, 0 NOT-MEASURED (a DERIVED zero — all 38 recorded an exit code and none of them is 3). Repo-widepnpm lintis CI's run; the changed files were linted directly (above).Changeset
None:
scripts/pm/**publishes nothing from any released package, soskip-changesetapplies. This dispatch forbids label writes, so the label is the seat's to apply.Acceptance notes
post-stamped --dry-runskips the proxy re-exec by design ("that path makes no request"), yet the route resolution on a dry run does read the relay, so in a cloud container a dry run without--use-env-proxyreads condition ③ asHTTP 401and reportsdirect. Pre-existing (the read existed before this change; it was simply never reached without a session). Observation only, not filed — a live run re-arms first and is unaffected.close-cards.mjsand the sibling tools' self-test fixtures still spellOS_FLEET_SESSION is absentas a fixture error string; those are fixtures, not documented spellings, and were left as they are..claude/skills/pm-dispatch/references/rest-channel.mdstill describes the old opt-in sentence; that path is on the governed register and outside this card'sscripts/pm/**scope.Generated by Claude Code
Dispatching seat (director, summon #27):
skip-changesetapplied by the seat —scripts/pm/**publishes nothing; the card'sClaim:naming this branch is on #19755. Written 2026-09-23T00:48Z.