Skip to content

Commit ed4b655

Browse files
objectstack-fleet[bot]os-support-aiclaude
authored
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)
Fixes #19755 ## What changed `scripts/pm/**` only — the fleet's own write tools, not on the governed register. 1. **The seat session is derived from the container.** `sessionSource(env)` in `fleet-write/dispatch.mjs` reads `OS_FLEET_SESSION` first (explicit wins, validation unchanged; an explicit value that is malformed is refused, never replaced). When it is absent, the container's `CLAUDE_CODE_REMOTE_SESSION_ID` — `cse_ID` — is read and becomes `session_ID`; malformed or absent gives `null` as before. The route's reason names which source was read, and `--route` now prints the session and its source. The container variable's name is `CONTAINER_SESSION_ENV` in `fleet-write/ops.mjs`, beside `SESSION_ENV`, with its shape `CONTAINER_SESSION_SHAPE` beside `SESSION_SHAPE`. 2. **Condition ③ reads the workflow's Actions state.** Relay live = the file on `main` (today's read) AND `GET /repos/{board}/actions/workflows/fleet-write.yml` answering 200 with `state === 'active'`. Any other state, status or no answer is not live, the reason naming it — `disabled_manually` verbatim when that is the state, described as the maintainer's kill switch. Both reads are cached once per process and reached only after ① and ②. Explicit `OS_FLEET_TRANSPORT=dispatch` against a disabled workflow is a prerequisite refusal (exit 3), never a silent fall-back. 3. **Unchanged:** the two ceilings, the no-run fall-back under `auto`, the op table, the executor, the workflow file, every allow rule. Every `--help` line and header comment that told a seat to export `OS_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 and `OS_FLEET_SESSION` overrides it (a local checkout, a test). No documented spelling carries a prefix. 4. **Self-tests** for the derivation (well-formed / malformed / absent / explicit wins / malformed explicit never replaced / blank explicit counts as absent / the reason names the source), the state read (`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-run` and `--route` with the container variable alone). The dispatch and with-fleet CLI cases clear the inherited container variable, so both batteries hold inside a cloud container. 6. **Size-routed fall-back under `auto` (addendum, item 6).** `sizeRoute(route, body)` in `post-stamped.mjs` measures the RENDERED body in UTF-8 bytes against `MAX_BODY_BYTES` (60,000; the platform's `client_payload` cap is 64 KB). Over it under `auto`, THAT write takes `direct` and the transport line becomes one line naming the bytes, the cap and the identity used (the seat's own user, not `objectstack-fleet[bot]`). Explicit `OS_FLEET_TRANSPORT=dispatch` over 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-cards` are not size-routed; no compression codec (pinned by import shape). ## Verification record **`--route` in this cloud container, with NO `OS_FLEET_SESSION` set** (the seat's own id from the claude-code-remote `get_session` tool, field `ccr.id`, is `session_01GnJon4xkRvphn4w28xx3An`; the container carries `CLAUDE_CODE_REMOTE_SESSION_ID=cse_01GnJon4xkRvphn4w28xx3An` — same tail). Angle-bracket placeholders in the pasted reason are spelled `ID` here for the body sanitizer. ``` $ node scripts/pm/fleet-write/dispatch.mjs --route {"requested":"auto","transport":"dispatch","failed":null,"session":"session_01GnJon4xkRvphn4w28xx3An","session_source":"CLAUDE_CODE_REMOTE_SESSION_ID","reason":"OS_FLEET_TRANSPORT is auto → dispatch: CCR_AGENT_PROXY_ENABLED=1 (a cloud seat container, whose proxy replaces the Authorization header), the session was derived from the container's CLAUDE_CODE_REMOTE_SESSION_ID (cse_ID → session_ID; OS_FLEET_SESSION is absent), and the relay workflow .github/workflows/fleet-write.yml is on objectstack-ai/objectstack@main (GET answered 200) and its Actions state is active (GET /repos/objectstack-ai/objectstack/actions/workflows/fleet-write.yml answered 200)","error":null} exit 0 ``` 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: ``` post-stamped: transport direct — OS_FLEET_TRANSPORT is auto → direct for THIS write: the body is 60,159 UTF-8 bytes, 159 over the relay's 60,000-byte body cap (a repository_dispatch client_payload is capped by the platform at 64 KB), so it cannot take the relay. Written DIRECT as the seat's own user (the token this process holds), not as objectstack-fleet[bot]. ⛔ Not compressed: a body this size is the thing to shrink. ``` 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): | command | exit | verdict line | |---|---|---| | `pnpm check:pm-fleet-write-validate` | 0 | 69 cases pass across 7 batteries | | `pnpm check:pm-fleet-write-execute` | 0 | 48 cases pass across 9 batteries | | `pnpm check:pm-fleet-write-dispatch` | 0 | 90 cases pass across 10 batteries (was 64 across 9 at c120dbd) | | `pnpm check:pm-with-fleet` | 0 | 32 cases pass (was 31; floor 28 → 29) | | `pnpm check:pm-post-stamped` | 0 | 609 cases pass across 21 batteries (was 596 across 20) | | `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 | `npx eslint --no-inline-config` on the five changed `.mjs` files: exit 0, no findings. **Gates** — `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` on 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. `--ran` with 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-wide `pnpm lint` is CI's run; the changed files were linted directly (above). ## Changeset None: `scripts/pm/**` publishes 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 - `post-stamped --dry-run` skips 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-proxy` reads condition ③ as `HTTP 401` and reports `direct`. 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.mjs` and the sibling tools' self-test fixtures still spell `OS_FLEET_SESSION is absent` as a fixture error string; those are fixtures, not documented spellings, and were left as they are. - `.claude/skills/pm-dispatch/references/rest-channel.md` still describes the old opt-in sentence; that path is on the governed register and outside this card's `scripts/pm/**` scope. --- _Generated by [Claude Code](https://claude.ai/code/session_01GnJon4xkRvphn4w28xx3An)_ <sub>Dispatching seat (director, summon #27): `skip-changeset` applied by the seat — `scripts/pm/**` publishes nothing; the card's `Claim:` naming this branch is on #19755. Written 2026-09-23T00:48Z.</sub> Co-authored-by: objectstack-fleet <support@objectstack.ai> Co-authored-by: Claude <noreply@anthropic.com>
1 parent dc1b986 commit ed4b655

6 files changed

Lines changed: 355 additions & 94 deletions

File tree

‎scripts/pm/fleet-write/dispatch.mjs‎

Lines changed: 230 additions & 81 deletions
Large diffs are not rendered by default.

‎scripts/pm/fleet-write/ops.mjs‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,22 @@ export const TARGET_OWNER = 'objectstack-ai';
8484
export const TRANSPORT_ENV = 'OS_FLEET_TRANSPORT';
8585
export const TRANSPORTS = Object.freeze(['direct', 'dispatch', 'auto']);
8686

87-
/** The session id a dispatch carries; a seat sets it to its own `session_…`. */
87+
/**
88+
* The session id a dispatch carries. A cloud seat's is READ FROM THE CONTAINER
89+
* (`CONTAINER_SESSION_ENV` below); this variable OVERRIDES it — for a local
90+
* checkout, or a test — and an explicit value that is malformed is refused,
91+
* never replaced by the container's.
92+
*/
8893
export const SESSION_ENV = 'OS_FLEET_SESSION';
94+
/**
95+
* The container's own name for the seat's session: the cloud harness sets
96+
* `CLAUDE_CODE_REMOTE_SESSION_ID=cse_<id>` in every cloud seat container
97+
* measured, and `session_<id>` — the same tail — is the seat's id in the shape
98+
* `SESSION_SHAPE` takes. Read only when `SESSION_ENV` is absent; a subagent
99+
* dev runs in its PM's container and so inherits the DISPATCHING seat's id,
100+
* which is the identity the envelope should carry.
101+
*/
102+
export const CONTAINER_SESSION_ENV = 'CLAUDE_CODE_REMOTE_SESSION_ID';
89103

90104
/** The merge method the auto-merge op arms. ⛔ Never a free string. */
91105
export const MERGE_METHOD = 'SQUASH';
@@ -113,6 +127,8 @@ export const PAYLOAD_KEYS = Object.freeze(['request_id', 'repo', 'session', 'act
113127

114128
export const REQUEST_ID_SHAPE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
115129
export const SESSION_SHAPE = /^session_[A-Za-z0-9]{6,}$/;
130+
/** The container variable's shape; group 1 is the tail `session_` is prefixed to. */
131+
export const CONTAINER_SESSION_SHAPE = /^cse_([A-Za-z0-9]{6,})$/;
116132
export const TARGET_REPO_SHAPE = new RegExp(`^${TARGET_OWNER}/[A-Za-z0-9_.-]+$`);
117133

118134
/**

‎scripts/pm/issue-create.mjs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -625,7 +625,8 @@ const USAGE = [
625625
' The body comes from a file, always; the title from a file by preference. A batch of N ≥ 5 cards is',
626626
' announced first: node scripts/pm/write-pace.mjs --announce-batch N --kind "issue-create POST"',
627627
' OS_FLEET_TRANSPORT=direct|dispatch|auto (default auto): dispatch sends the create through the fleet-write relay as',
628-
' objectstack-fleet[bot]; auto takes it in a cloud seat container (OS_FLEET_SESSION must carry this seat\'s session_… id).',
628+
" objectstack-fleet[bot]; auto takes it in a cloud seat container (the seat's session is read from the container's",
629+
' CLAUDE_CODE_REMOTE_SESSION_ID; OS_FLEET_SESSION overrides it — a local checkout, a test) and direct elsewhere.',
629630
` Exits: 0 created and read back · ${EXIT_USAGE} usage · ${EXIT_PREREQUISITE} prerequisite · ${EXIT_READ_BACK_MISMATCH} read-back mismatch · ${EXIT_PLATFORM_REFUSAL} platform refused · ${EXIT_UNCONFIRMED} dispatched but UNCONFIRMED · ${EXIT_WRITE_PACE_REFUSED} throttle refused`,
630631
].join('\n');
631632

‎scripts/pm/label-write.mjs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -490,8 +490,9 @@ const USAGE = [
490490
' ⛔ Never `PUT .../labels` and ⛔ never MCP `issue_write` (lock 1 denies it).',
491491
'',
492492
' OS_FLEET_TRANSPORT=direct|dispatch|auto (default auto): dispatch packs step ③ into ONE repository_dispatch the',
493-
' fleet-write relay executes as objectstack-fleet[bot]; auto takes it in a cloud seat container (OS_FLEET_SESSION',
494-
' must carry this seat\'s session_… id there) and direct elsewhere. Steps ①, ② and ④ are the same either way.',
493+
' fleet-write relay executes as objectstack-fleet[bot]; auto takes it in a cloud seat container (the seat\'s session is',
494+
" read from the container's CLAUDE_CODE_REMOTE_SESSION_ID; OS_FLEET_SESSION overrides it — a local checkout, a test)",
495+
' and direct elsewhere. Steps ①, ② and ④ are the same either way.',
495496
'',
496497
' Exits: 0 landed+read-back · 2 usage · 3 prerequisite (no token/route, or rate-limit exhausted)',
497498
' 4 read-back disagrees with the target · 5 every channel refused · 6 dispatched but UNCONFIRMED.',

‎scripts/pm/post-stamped.mjs‎

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,7 @@ import {
867867
} from './check-half-states.mjs';
868868
import { readClause2Line } from './clause2-line.mjs';
869869
import { EXIT_UNCONFIRMED, fallbackText, packRequest, resolveRoute, sendFleetWrite, unconfirmedText } from './fleet-write/dispatch.mjs';
870+
import { MAX_BODY_BYTES, TRANSPORT_ENV } from './fleet-write/ops.mjs';
870871
import { refusalText as relayRefusalText } from './fleet-write/validate.mjs';
871872
import { isWriteMethod, noteResponse, paceWrite } from './write-pace.mjs';
872873

@@ -2828,6 +2829,53 @@ export function relayExitFor(result) {
28282829
return EXIT_UNCONFIRMED;
28292830
}
28302831

2832+
/**
2833+
* The size route: the relay's body cap decides the transport for THIS write,
2834+
* and nothing else does. Pure.
2835+
*
2836+
* The relay carries a body of at most `MAX_BODY_BYTES` UTF-8 bytes (a
2837+
* `repository_dispatch` `client_payload` is capped by the platform at 64 KB,
2838+
* hence the 60,000-byte body cap in `fleet-write/ops.mjs`), so a body over it
2839+
* cannot take the relay however live the relay is:
2840+
*
2841+
* - under `auto`, THIS write goes DIRECT — as the seat's own user, the token
2842+
* this process holds — and the route's reason becomes the ONE printed line
2843+
* that says so, naming the bytes, the cap and the identity. ⛔ Never silent.
2844+
* - under an explicit `OS_FLEET_TRANSPORT=dispatch` the route becomes a
2845+
* PREREQUISITE refusal (exit 3) naming the bytes: the operator asked for
2846+
* the relay, and a fall-back would change the identity the write is booked
2847+
* against.
2848+
*
2849+
* ⛔ Only the size re-routes. A refused op, a failed run and an UNCONFIRMED
2850+
* outcome keep the register `relayExitFor` gives them; and ⛔ there is no
2851+
* compression codec — a body this size is the thing to shrink, not to encode.
2852+
* A route that is not `dispatch`, one already carrying an error, or a body at
2853+
* or under the cap comes back unchanged (flagged `sizeRouted: false`).
2854+
*/
2855+
export function sizeRoute(route, body) {
2856+
const bytes = Buffer.byteLength(String(body ?? ''), 'utf8');
2857+
const cap = MAX_BODY_BYTES;
2858+
if (!route || route.transport !== 'dispatch' || route.error || bytes <= cap) return { ...route, bytes, cap, sizeRouted: false };
2859+
const size = `the body is ${grouped(bytes)} UTF-8 bytes, ${grouped(bytes - cap)} over the relay's ${grouped(cap)}-byte body cap (a repository_dispatch client_payload is capped by the platform at 64 KB)`;
2860+
if (route.requested === 'auto') {
2861+
return {
2862+
...route,
2863+
transport: 'direct',
2864+
bytes,
2865+
cap,
2866+
sizeRouted: true,
2867+
reason: `${TRANSPORT_ENV} is auto → direct for THIS write: ${size}, so it cannot take the relay. Written DIRECT as the seat's own user (the token this process holds), not as objectstack-fleet[bot]. ⛔ Not compressed: a body this size is the thing to shrink.`,
2868+
};
2869+
}
2870+
return {
2871+
...route,
2872+
bytes,
2873+
cap,
2874+
sizeRouted: true,
2875+
error: `${TRANSPORT_ENV}=dispatch but ${size} — the relay cannot carry it. ⛔ Not falling back to direct: the operator asked for the relay, and the fall-back would change the identity the write is booked against. Shrink the body, or run under ${TRANSPORT_ENV}=auto to let THIS write go direct as the seat's own user.`,
2876+
};
2877+
}
2878+
28312879
// ---------------------------------------------------------------------------
28322880
// Live layer
28332881
// ---------------------------------------------------------------------------
@@ -3074,7 +3122,10 @@ async function main(argv) {
30743122

30753123
// The transport, decided and SAID before anything leaves. A dry run makes no
30763124
// request, so a route that cannot be resolved is a note there, not a refusal.
3077-
const route = await resolveRoute(process.env);
3125+
// The size route runs on the RENDERED body (the stamp is part of what is
3126+
// sent): over the relay's cap, `auto` takes direct for this write and the
3127+
// line below says so; explicit dispatch is refused.
3128+
const route = sizeRoute(await resolveRoute(process.env), rendered.body);
30783129
if (route.error && !options.dryRun) {
30793130
console.error(`post-stamped: PREREQUISITE NOT MET — ${route.error}\n NOTHING WAS WRITTEN.`);
30803131
return EXIT_PREREQUISITE_NOT_MET;
@@ -3264,6 +3315,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
32643315
'the shared rule: this tool and H56 cannot come to disagree': 6,
32653316
'the keyed lines: a claim\'s exact-value fields, judged by the readers that own them': 20,
32663317
'the relay transport: the same act as one op, the comment found on the board, the exit register kept apart': 12,
3318+
"the size route: over the relay's body cap under auto THIS write goes direct with one line naming bytes, cap and identity; at or under it the relay; explicit dispatch refuses naming the bytes; nothing else re-routes": 12,
32673319
});
32683320
const SELF_TEST_BATTERY_FLOOR = 16;
32693321
const UNATTRIBUTED_BATTERY = '(unattributed)';
@@ -4356,6 +4408,33 @@ export function selfTest() {
43564408
t('structural: in main a dry run returns before the relay can be reached, and ONE read-back verdict serves both transports', mainSource.indexOf('if (options.dryRun) {') < mainSource.indexOf('await writeViaRelay(') && (mainSource.match(/readBackVerdict\(\{/g) ?? []).length === 1);
43574409
}
43584410

4411+
// ── the size route ───────────────────────────────────────────────────────
4412+
battery("the size route: over the relay's body cap under auto THIS write goes direct with one line naming bytes, cap and identity; at or under it the relay; explicit dispatch refuses naming the bytes; nothing else re-routes");
4413+
{
4414+
const AUTO = { requested: 'auto', transport: 'dispatch', reason: 'auto → dispatch (fixture)', error: null, failed: null, session: 'session_01ABCDEFGHJKMNPQRSTVWXYZ' };
4415+
const STRICT = { ...AUTO, requested: 'dispatch', reason: 'OS_FLEET_TRANSPORT=dispatch (fixture)' };
4416+
const ofBytes = (n) => 'x'.repeat(n);
4417+
const over = sizeRoute(AUTO, ofBytes(MAX_BODY_BYTES + 1));
4418+
t('one byte over the cap under auto: DIRECT for this write, flagged as size-routed, no error', over.transport === 'direct' && over.sizeRouted === true && over.error === null);
4419+
t('…and the ONE line names the bytes sent', over.reason.includes(`${grouped(MAX_BODY_BYTES + 1)} UTF-8 bytes`));
4420+
t('…the cap', over.reason.includes(`${grouped(MAX_BODY_BYTES)}-byte body cap`));
4421+
t("…and the identity used — the seat's own user, not the fleet bot", over.reason.includes("seat's own user") && over.reason.includes('not as objectstack-fleet[bot]'));
4422+
const under = sizeRoute(AUTO, ofBytes(MAX_BODY_BYTES - 1));
4423+
t('one byte under the cap under auto: the relay, the route untouched', under.transport === 'dispatch' && under.sizeRouted === false && under.reason === AUTO.reason && under.error === null);
4424+
t('…and exactly AT the cap is still the relay (the validator admits it)', sizeRoute(AUTO, ofBytes(MAX_BODY_BYTES)).transport === 'dispatch');
4425+
const strict = sizeRoute(STRICT, ofBytes(MAX_BODY_BYTES + 1));
4426+
t('⛔ explicit dispatch one byte over: a PREREQUISITE refusal naming the bytes and saying it will NOT fall back, the transport NOT changed', strict.transport === 'dispatch' && typeof strict.error === 'string' && strict.error.includes(grouped(MAX_BODY_BYTES + 1)) && strict.error.includes('Not falling back'));
4427+
t('bytes are UTF-8 BYTES, not characters: 20,001 three-byte characters are over the cap, 20,000 are not', sizeRoute(AUTO, '中'.repeat(20_001)).transport === 'direct' && sizeRoute(AUTO, '中'.repeat(20_000)).transport === 'dispatch');
4428+
t('a route already direct is never touched, whatever the size', sizeRoute({ ...AUTO, transport: 'direct' }, ofBytes(MAX_BODY_BYTES + 1)).sizeRouted === false);
4429+
t('a route that already carries an error is never touched — the size never masks a missing session', sizeRoute({ ...STRICT, error: 'no session' }, ofBytes(MAX_BODY_BYTES + 1)).error === 'no session');
4430+
t('⛔ nothing else re-routes: a refused dispatch, a failed run and UNCONFIRMED keep their register', relayExitFor({ state: 'refused' }) === EXIT_PREREQUISITE_NOT_MET && relayExitFor({ state: 'failure' }) === EXIT_NOT_STORED && relayExitFor({ state: 'timeout' }) === EXIT_UNCONFIRMED);
4431+
const ownSource = readFileSync(SELF_PATH, 'utf8');
4432+
const mainSource = ownSource.slice(ownSource.indexOf('async function main(argv)'), ownSource.indexOf('// --self-test — offline'));
4433+
t('structural: main hands the resolved route through sizeRoute on the RENDERED body before the transport line is printed', mainSource.includes('sizeRoute(await resolveRoute(process.env), rendered.body)') && mainSource.indexOf('sizeRoute(') < mainSource.indexOf('transport ${route.transport}'));
4434+
// An import SHAPE, not the module's bare name: the name is spelled in this very line, so a name test could never fail.
4435+
t('⛔ no compression codec: this file imports nothing from zlib', /from '(node:)?zlib'/.test(ownSource) === false && /require\('(node:)?zlib'\)/.test(ownSource) === false);
4436+
}
4437+
43594438
// The floor, evaluated last: a battery that stops running names itself here.
43604439
const failed = cases.filter((c) => !c.ok);
43614440
for (const c of cases) console.log(` ${c.ok ? '✓' : '✗'} ${c.name}`);

‎scripts/pm/with-fleet.sh‎

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@
2424
# --via dispatch `--repo owner/name --actions <file>`; never a command — the relay hands out no token.
2525
# --via auto (the default, or OS_FLEET_TRANSPORT) — asks `fleet-write/dispatch.mjs --route`,
2626
# the ONE selector every tool shares: `dispatch` only when the cloud discriminator,
27-
# a well-formed OS_FLEET_SESSION and a live relay on the board all hold, `direct`
28-
# otherwise (the line says which condition failed). A COMMAND under auto that
27+
# the seat's session (read from the container's CLAUDE_CODE_REMOTE_SESSION_ID;
28+
# OS_FLEET_SESSION overrides it — a local checkout, a test) and a live relay on the
29+
# board — the workflow file on main AND its Actions state `active` — all hold,
30+
# `direct` otherwise (the line says which condition failed). A COMMAND under auto that
2931
# resolves to `dispatch` is REFUSED (exit 3) with the actions-file spelling: it
3032
# cannot be run as the fleet there, and running it as the session's login instead
3133
# would be a silent change of identity.
@@ -95,7 +97,7 @@ log() { printf 'with-fleet: %s\n' "$*" >&2; }
9597

9698
ST_PASS=0
9799
ST_FAIL=0
98-
ST_MIN_CASES=28
100+
ST_MIN_CASES=29
99101
st_case() {
100102
local name="$1" got="$2" want="$3"
101103
if [[ "$got" == "$want" ]]; then
@@ -241,14 +243,26 @@ EOF
241243
run --via dispatch --repo objectstack-ai/objectstack --actions "$dir/actions.json" -- true || rc=$?
242244
st_case '--actions AND a command is usage (2)' "$rc" "$EXIT_USAGE"
243245
# A cloud seat container, under auto, with a command: the ONE shared selector decides. With no
244-
# OS_FLEET_SESSION the seat has not opted in, so it is direct — said out loud — and the command runs.
246+
# session from EITHER source (OS_FLEET_SESSION blank, the container variable unset — this self-test
247+
# runs inside cloud containers too, so the inherited one is dropped) it is direct — said out loud —
248+
# and the command runs.
245249
: > "$err.cloud"
246250
rc=0
247-
env -u GIT_DIR OS_FLEET_TOKEN_CACHE_FILE="$cache" OS_FLEET_APP_ID=1 OS_FLEET_INSTALLATION_ID=2 OS_PM_WRITE_PACE_FILE="$pace" HTTPS_PROXY= https_proxy= OS_FLEET_TRANSPORT= OS_FLEET_SESSION= \
251+
env -u GIT_DIR -u CLAUDE_CODE_REMOTE_SESSION_ID OS_FLEET_TOKEN_CACHE_FILE="$cache" OS_FLEET_APP_ID=1 OS_FLEET_INSTALLATION_ID=2 OS_PM_WRITE_PACE_FILE="$pace" HTTPS_PROXY= https_proxy= OS_FLEET_TRANSPORT= OS_FLEET_SESSION= \
248252
CCR_AGENT_PROXY_ENABLED=1 bash "$SELF" --read -- sh -c 'printf "%s" "$GITHUB_TOKEN"' > "$dir/cloud.out" 2> "$err.cloud" || rc=$?
249-
st_case 'a command under auto in a cloud container WITHOUT a session is direct (the seat has not opted in): it runs, and the line names OS_FLEET_SESSION' \
253+
st_case 'a command under auto in a cloud container WITHOUT a session from either source is direct: it runs, and the line names OS_FLEET_SESSION' \
250254
"$rc|$(grep -c 'auto → direct' "$err.cloud")|$(grep -c 'OS_FLEET_SESSION' "$err.cloud")|$(cat "$dir/cloud.out")" "0|1|1|$token"
251255
cat "$err.cloud" >> "$err"
256+
# …with NO OS_FLEET_SESSION but the container's own session variable (a cloud seat's default) and a live
257+
# relay (stood in by the test override): the selector DERIVES the session, all three hold, so a command
258+
# is refused with the relay spelling — the allowed command spelling routes through the relay with no prefix.
259+
: > "$err.cloud"
260+
rc=0
261+
env -u GIT_DIR OS_FLEET_TOKEN_CACHE_FILE="$cache" OS_FLEET_APP_ID=1 OS_FLEET_INSTALLATION_ID=2 OS_PM_WRITE_PACE_FILE="$pace" HTTPS_PROXY= https_proxy= OS_FLEET_TRANSPORT= OS_FLEET_SESSION= \
262+
CCR_AGENT_PROXY_ENABLED=1 CLAUDE_CODE_REMOTE_SESSION_ID=cse_01ABCDEFGHJKMNPQRSTVWXYZ OS_FLEET_RELAY_LIVE=1 bash "$SELF" --read -- sh -c 'printf "%s" "$GITHUB_TOKEN"' > "$dir/cloud.out" 2> "$err.cloud" || rc=$?
263+
st_case 'a command under auto with the session DERIVED from the container (no OS_FLEET_SESSION) is REFUSED (3) with the relay spelling, the line naming the container variable, and no token reached it' \
264+
"$rc|$(grep -c -- '--via dispatch' "$err.cloud")|$(grep -c 'CLAUDE_CODE_REMOTE_SESSION_ID' "$err.cloud")|$(cat "$dir/cloud.out")" '3|1|1|'
265+
cat "$err.cloud" >> "$err"
252266
# …with a session AND a live relay (stood in by the test override): dispatch, so a command is refused with the relay spelling.
253267
: > "$err.cloud"
254268
rc=0
@@ -367,8 +381,9 @@ if [[ "$VIA" == dispatch ]]; then
367381
exit "$EXIT_USAGE"
368382
fi
369383
if [[ "$VIA" == auto ]]; then
370-
# The ONE selector every tool shares decides: dispatch only when the cloud discriminator, a
371-
# well-formed OS_FLEET_SESSION and a live relay all hold. Its JSON line is read, never re-derived.
384+
# The ONE selector every tool shares decides: dispatch only when the cloud discriminator, the seat's
385+
# session (from the container, OS_FLEET_SESSION overriding it) and a live relay all hold. Its JSON
386+
# line is read, never re-derived.
372387
ROUTE_JSON="$(node "$HERE/fleet-write/dispatch.mjs" --route)" || {
373388
log "the transport selector refused (fleet-write/dispatch.mjs --route exit $?); nothing ran."
374389
exit 3

0 commit comments

Comments
 (0)