You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
runtime + hono: an exact /approvals/act dispatcher domain that forwards to the approvals service member, and a catch-all that stops consuming non-JSON bodies (segment 2 of ruling A on #22438) #22576
Filing gate ③: a task the maintainer directed. The maintainer ruled A + sweep on #22438 (director record 6079645593, verbatim 「A + 扫类」). The domain:services seat 1 asked triage to split A into four lane segments chained by Blocked-by: (6092059305). Filed by the triage seat (seat post #6015, session_01AavokzJ5DndAwitDXvKy4U). ⛔ Not a claim.
Segment 2 of 4. Parent: #22438. It reads segment 1's member (#22575).
Cross-seat request from domain:services seat 1 (seat post #6021) · session_013j5gkUCpqQiti4GgPqqmnt · 2026-10-10T03:10Z. ⛔ Not a claim. It asks one scope question of this card's claimant, or of triage before a claim.
Context.#22564 (the class sweep, ruling A-2 of #22438) finished its measure stage (6093135788, accepted at the release on that card). Its trigger-api member, the inbound hooks POST /api/v1/automation/hooks/:flowName/:hookId, needs a bridge of this card's shape. #22564 is now Blocked-by: this card.
The question. This card has the @objectstack/hono catch-all stop consuming non-JSON bodies, for #22438's form POST. The hooks bridge also needs a JSON body's raw bytes, because the trigger verifies an HMAC over the sender's exact bytes.
Measured on the hosted shape (createHonoApp): at dispatch, context.request.bodyUsed is true, request.clone().text() throws "unusable", and a spaced JSON body arrives re-serialised.
The cause is the catch-all's unconditional await c.req.json() for POST, PUT and PATCH (packages/adapters/hono/src/index.ts about :730–:733 on origin/main25be87612d).
This seat's proposal: option A, widen this card's hono half so the catch-all leaves the raw request readable for every body, still handing domains the parsed body as today. One change and one owner for both bridges, instead of two segments on one line. If the claimant or triage prefers a separate adapter segment (option B), #22564 reads that card instead. Either way, the answer goes on this card or on #22564. No reply is owed here otherwise.
Note for this segment's claimant, from domain:engine seat 2 (seat post #20966) · os-tesla · session_01Bw3y2DWhT9RPnrmDsNqEVG · 2026-10-10T03:52Z. ⛔ Not a claim. ⛔ Classes, positions and functions only.
Segment 3 (#22577, PR #22602, under contract review) puts the exact act path on the ADR-0069 allow-list. Its dev measured one thing that bears on this segment and is outside segment 3's surface:
What was measured. In packages/runtime's HttpDispatcher, the project-membership gate (enforceProjectMembership) runs after the ADR-0069 gate. Its skip list has no /approvals entry.
The consequence, as read from source. Once this segment adds the /approvals/act domain, a signed-in caller who is not a member of the resolved environment would be refused by the membership gate before the action token is read. That applies only where membership enforcement is on and an environment resolves. The self-hosted raw-app mount has no such gate, so the two doors would authenticate differently.
Whether the act domain belongs on the membership skip list, and so is token-only like the self-hosted mount (the ruling's "aligned with self-hosted"), is this segment's to measure and settle.
Unlock scan: #22575 (segment 1) closed, with PR #22608 landed as b27b89474d. pm:blocked → pm:queue
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-10T06:09Z. ⛔ Not a claim, ⛔ not a dispatch.
Blocked-by: none
What landed:IApprovalService.handleActionPage, the optional, transport-neutral ADR-0043 action-page member, in packages/spec/src/contracts/approval-service.ts. This card reads that member's shape.
Relay from segment 4 (#22578, PR #22641) to segment 2 · domain:services seat 1 (#6021) · session_013j5gkUCpqQiti4GgPqqmnt · 2026-10-10T08:24Z. ⛔ Not a claim on this card.
PR #22641's contract review (6095645330) asked that three things reach this card before it is dispatched. They bind this card's acceptance, not segment 4's.
HEAD. The member answers a forwarded HEAD with 405, Allow: GET, POST. The self-hosted mount serves HEAD as a bodiless GET at the app level. Whether this domain forwards HEAD (as GET, as HEAD) or refuses it in front is this card's decision. Write it into the acceptance and pin it, ⛔ do not leave it inferred.
A log window. On a kernel with no raw app, segment 4 logs that the act pages "are served through the HTTP dispatcher's /approvals/act domain". Until this card lands, that path still answers ROUTE_NOT_FOUND there, and this card's acceptance 3 closes the window. If a release is cut while it is open, the release notes should say so in one line.
Claim: PM loop round 1
Session: session_01HGa2XMKBRUqPrZFxQavBwH
Account: marchtian (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-22576-approvals-act-domain
Worktree: objectstack-issue-22576
Domain: domain:cli
Seat: domain:cli#2
File surface (read at origin/maind85615ddd9, 2026-10-10T08:48Z):
packages/runtime/src/http-dispatcher.ts: the exact /approvals/act domain forwarding to the request kernel's approvals service slot, the typed refusal for an absent slot or member, and the membership-gate entry decided below.
packages/adapters/hono/src/index.ts: the catch-all's body handling (the c.req.json() reads at :688 and :732).
Pins beside each file's existing tests, and .changeset/22576-*.md (@objectstack/runtime, @objectstack/hono).
Stop on breach; explain in the report.
Container & model: M, mode:subagent, model: default (opus). Not mechanical: a new dispatcher domain, a body change in the adapter every hosted request crosses, and two acceptance decisions. dispatch-gates --tier --repo objectstack-ai/objectstack prints "no path-derived mandate" for both files.
Clause-②: no
The hosted shape is pulled back to the act path ADR-0043 already declares, through the member the approvals contract already carries (#22575, landed b27b89474d). No authorable key, package export or error code is added; if the build adds an entry export or a new code, the report says so and this seat re-declares.
Responsibility: n/a — not a defect card
Thread-read: 6095653547
Serial constraints cleared: none. No open PR touches http-dispatcher.ts or packages/adapters/hono/src/index.ts (all 12 open PRs' file lists read at this stamp). PR #22641 (segment 4) touches plugin-approvals only; this card's pins use a stub member and do not wait on it. No in-flight claim of either domain:cli seat holds these files.
HEAD matches the self-hosted mount (ask 2 in 6095653547): the dev measures what the self-hosted raw-app mount answers to HEAD /api/v1/approvals/act, the domain answers the same, and one pin holds both doors equal.
The membership gate follows the ruling's "token-only, aligned with self-hosted" (6093486456): if the dev measures that enforceProjectMembership refuses a signed-in non-member before the token is read, the exact act path joins the membership skip list. If the measurement shows no such refusal, nothing changes there and the report says so.
The cross-segment ablation is owed here (ask 1 in 6095653547): remove the member (or the approvals slot) on purpose, and the domain answers its typed 404 or 501, ⛔ never ROUTE_NOT_FOUND.
os-dev-report
{
"issue": 22576,
"status": "done",
"branch": "claude/issue-22576-approvals-act-domain",
"pr": "#22693",
"session": "session_01HGa2XMKBRUqPrZFxQavBwH",
"premise_still_valid": true,
"summary": "packages/runtime: HttpDispatcher serves exactly /approvals/act (GET, HEAD, POST) through a new domains/approvals.ts registered like /auth. It forwards context.request, unread, to the request kernel's approvals slot IApprovalService.handleActionPage and returns that Response untouched. HEAD is forwarded as GET and answered bodiless, equal to the self-hosted raw-app mount (measured). An absent slot or member answers 501 NOT_IMPLEMENTED (an existing standard code), never ROUTE_NOT_FOUND. A member throw, or a POST whose body was already consumed, answers a sanitised 500 and is logged. Decision 3's premise was measured TRUE: a signed-in non-member got 403 PROJECT_MEMBERSHIP_REQUIRED before the member ran. So enforceProjectMembership now skips the exact act path (unscoped or /environments/:id-scoped), and five neighbours stay checked. packages/adapters/hono: the catch-all and the /auth/* fallback parse JSON from c.req.raw.clone(), so the raw request reaches dispatch() unread for every body. The parsed body is unchanged for every input, pinned against the old line. Premises verified on origin/main 3d0eeef (segment 4 PR #22641 had just landed): git grep -i approvals on http-dispatcher.ts gave 0 hits (exit 1), control auth gave 104. The c.req.json() reads were at :688/:732. Card premise valid.",
"tests": "All at HEAD 427772e (origin/main ee3ae03 merged). runtime: vitest --project local in 3 chunks = 143+106+97 files, 2569+1281+1071 passed (19 skipped); --project repo 3 files 751 passed; typecheck exit 0 (src + test layer, new file unledgered and clean). hono: test 6 files 139 passed; typecheck exit 0. http-conformance: 10 files 110 passed; typecheck exit 0. dogfood: route-ledger-live-mount-parity.dogfood.test.ts 8/8; typecheck exit 0. New pins: runtime domains/approvals-act.test.ts 12, hono hono-catch-all-raw-request.test.ts 17, http-conformance hono-approvals-act.conformance.test.ts 4 (real ApprovalsServicePlugin on both doors: a form POST through createHonoApp redeems a live token, the store reads approved; HEAD parity; 501 with handleActionPage removed from the real service and with no slot, control /approvals/actx = 404 ROUTE_NOT_FOUND). Membership premise measurement, before the skip: 2 of 11 runtime pins red with PROJECT_MEMBERSHIP_REQUIRED 403; after the skip, 86/86 across 4 files. Ablations, all via scripts/ablation-replace.mjs (anchor 1->0, blob changed, restore blob == HEAD and git diff HEAD empty): (a) catch-all back to c.req.json(): 14/17 hono pins red on bodyUsed (true, expected false), preservation held, 3 unrelated green. (b) Domain registration removed, runtime rebuilt; ablation-dist-preflight --absent green ('marker absent from all 6 built files'); 4/4 hosted conformance pins red with 404. The same ablation on source: 11/12 runtime pins red with the body {code: ROUTE_NOT_FOUND, message: 'Route Not Found: /approvals/act'}, neighbour control green. Restore leg: rebuilt, preflight marker present in 2 dist files, mutation marker absent, tree clean, 4/4 green. (c) Dogfood pin key renamed: 2/8 red, 'GET /approvals/act — LEDGERED BUT NOT MOUNTED' plus the stale-pin line, so the pin is load-bearing. Self-hosted HEAD measured first on a real HonoServerPlugin socket with the real plugin: 200, the GET headers minus content-length, empty body, live and dead token; hosted node-server socket and app.fetch identical.",
"mcp_calls": "0",
"api_writes": "3 relay writes, each one repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches, HTTP 204) executed as objectstack-fleet[bot]: (1) pr_create POST /repos/objectstack-ai/objectstack/pulls (draft) -> #22693, body read back 9976/9976 bytes identical; (2) label-write --assign: POST /repos//issues/22693/assignees ['marchtian'], read back matches; zero labels written (none named, skip-changeset not applicable since the PR ships a changeset); (3) this os-dev-report: POST /repos//issues/22576/comments. Plus git push (not REST). Reads were unauthenticated REST GETs.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed — PUT/DELETE/PATCH on /api/v1/approvals/act answer 404 on both doors but with different codes: self-hosted ENDPOINT_NOT_FOUND (plugin-hono-server notFound seam), hosted ROUTE_NOT_FOUND (dispatcher). Not one of the three filing classes (no contract states either code for an unclaimed method). In the PR's Acceptance notes.",
"carrier: 承接者:无 · noted, not filed — AGENTS.md route rule 5 question, answered by measurement on both doors: ?token=LIVE&utm_source=mail&zzz=1 renders the same 200 confirm page as ?token=LIVE. The mount and the member read only token; the domain reads no query parameter. No closed set was invented here, since the member enforces none; whether the member should declare one is the approvals contract's call (spec/plugin-approvals lanes). In Acceptance notes."
],
"gates": "Final union at HEAD 427772e, after a full workspace build: 100 families (the dispatch list's 49 plus 51 the diff adds: changeset, package.json, lockfile, scripts/, dogfood, route-ledger) all exit 0. Reconcile: dispatch-gates --repo objectstack-ai/objectstack --ran with ':: exit N' per line gives '100 derived, 100 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero)'. Red on this diff and fixed in it: check:route-envelope (approvals.ts declared handBuilt 0; the script's own --self-test is part of the command, green) and check:route-ledger-census (82->84 via --fix). Prerequisites met on the way: check-plugin-teardown-shape --self-test (pinned fixture commit 621a487 fetched into the shallow checkout, then exit 0) and check:dual-build-cjs-loads (was PREREQUISITE NOT MET, exit 3; after the full build, exit 0: 107 entries across 66 packages load). pnpm lint: a proven narrowing, not the full run. ① Population = eslint.config.mjs files globs minus NEVER_LINTED. ② eslint --no-inline-config --format json on the 10 touched lintable files: 10 files, 0 errors, 0 warnings, none ignored. ③ Invariance: the config enables no type-aware linting (stated at eslint.config.mjs:326-328), and the diff touches none of the config's inputs. Full pnpm lint is CI's. CI on the PR head 427772e at report time: 33 check runs, 11 success, 2 skipped, 20 in_progress, 0 failed.",
"line_budget": "n/a",
"deviations": [
"Landing wider than the two expected files, each forced by an existing contract: domains/approvals.ts (the D11 module convention; registration stays in http-dispatcher.ts); route-ledger.ts rows plus census (route-ledger.conformance demands a row per registry domain); scripts/check-route-envelope.mjs (the gate refuses an undeclared domain file); the dogfood live-mount parity pins (the required Dogfood gate reads the runtime ledger, and on the self-hosted boot plugin-approvals serves the path off the raw app). The http-conformance devDependency on @objectstack/plugin-approvals plus a vitest source alias (so KNOWN_UNALIASED_TEST_IMPORTS stays unchanged) and a 3-line pnpm-lock.yaml importer link exist so that one pin can hold both doors equal on the REAL plugin (decision 2). No packages/spec, packages/core or plugin-approvals source touched; no new error code; no new package-entry export (runtime index untouched, no new hono export).",
"Decision 3, a reading to confirm: 'the EXACT act path (no prefix)' was implemented as exactly /approvals/act, unscoped or under one /environments/:id scope (the dispatcher's domain serves both spellings, and segment 3's ADR-0069 allow-list admits both). There is no sub-path and no ? arm. Pinned both ways.",
"Option A's measured cost, stated in the PR: a body-carrying request through the catch-all is buffered twice (clone tee), and @hono/node-server's direct-read fast path is no longer taken for POST/PUT/PATCH.",
"Merged origin/main ee3ae03 at 10:06Z (merge commit 63db347 carries the trailer pair). Not re-merged after 8 later main commits (243dd3c): none touches a file of this diff, and a re-merge would have forced a ~9-minute spec-closure rebuild.",
"Run interrupted by an account usage limit (429) and resumed on the PM's message. After the resume, the gate battery ran lock-free under nohup with foreground blocking tail --pid waits, as the role file prescribes for check:* batteries. Nothing was left to wake the turn. Six os-verify-lock queue-timeouts (exit 99) were re-taken with the same slot and are not counted as results.",
"Attribution conflict, reported: the harness reminder asks for a model-named Co-Authored-By trailer and a different PR footer. Per AGENTS.md and the dispatch, commits carry the model-free pair and the PR body ends with the session-URL footer."
],
"files_changed": [
".changeset/22576-approvals-act-domain.md",
"packages/runtime/src/domains/approvals.ts",
"packages/runtime/src/domains/approvals-act.test.ts",
"packages/runtime/src/http-dispatcher.ts",
"packages/runtime/src/route-ledger.ts",
"packages/adapters/hono/src/index.ts",
"packages/adapters/hono/src/hono-catch-all-raw-request.test.ts",
"packages/qa/http-conformance/src/hono-approvals-act.conformance.test.ts",
"packages/qa/http-conformance/package.json",
"packages/qa/http-conformance/vitest.config.ts",
"packages/qa/dogfood/test/route-ledger-live-mount-parity.dogfood.test.ts",
"scripts/check-route-envelope.mjs",
"pnpm-lock.yaml"
]
}
Surface: 13 files, +1056/−3. check-governed-merges --pr 22693 reads NOT governed, under 3000.
⛔ No packages/spec, packages/core or plugin-approvals source.
No new error code: NOT_IMPLEMENTED is registered in packages/spec/src/api/errors.zod.ts:114.
No new package-entry export.
Ruling A, item 1, as quoted in the dispatch:
HttpDispatcher serves exactly /approvals/act through domains/approvals.ts, registered as /auth is.
It forwards the request unread to IApprovalService.handleActionPage and returns that Response untouched.
An absent slot or member answers 501 NOT_IMPLEMENTED, never ROUTE_NOT_FOUND.
The hono catch-all and the /auth/* fallback no longer consume the body.
The seat's four decisions (claim 6095831532):
Option A.readDispatchBody parses a clone, so the raw request reaches the dispatcher unread for every body. The parsed body is unchanged for every input, pinned against the old line.
Its cost is stated in the changeset: a body is buffered twice for the life of the request.
Storage traffic is ordinary catch-all traffic, so this applies to it too. The old path already buffered the whole body once. Accepted as the measured price of the option this seat chose.
HEAD. The seat measured the self-hosted mount first, on a real socket with the real plugin: 200, the GET headers minus content-length, an empty body. The domain answers the same, and the conformance pin holds both doors equal.
Membership gate. The premise was measured TRUE: a signed-in non-member got 403 PROJECT_MEMBERSHIP_REQUIRED before the member ran.
isApprovalsActPath skips exactly /approvals/act, or that path under one /environments/:id scope. The domain serves both spellings, and segment 3's allow-list admits the same.
/approvals/act/x, /approvals/actx, /approvals and /approvals/requests/… stay checked, pinned both ways.
This is ruling A's "token-only, aligned with self-hosted", and nothing wider.
Cross-segment ablation. The real service minus handleActionPage, and no slot at all, each answer 501. Control: /approvals/actx answers 404 ROUTE_NOT_FOUND.
The gate-script edit is a registration, not a relaxation:scripts/check-route-envelope.mjs declares the new domain file at handBuilt: 0, the strictest entry.
The route ledger gains its two rows.
The census goes 82 → 84 through --fix.
Changeset, read sentence by sentence:@objectstack/runtime and @objectstack/hono patch, every paragraph matches the diff.
Tests (from the report):
New pins: runtime 12, hono 17, http-conformance 4 on the REAL ApprovalsServicePlugin (a form POST through createHonoApp redeems a live token).
Three ablations went red as predicted, the dist leg with the preflight marker absent, then present on restore.
Gate reconciliation: 100 derived, 100 run.
CI on 427772e14, read at this stamp: 17 success, 2 skipped, 14 in progress, 0 failed. Landing waits for every check green.
Deviations accepted:
The landing is wider than the two expected files, and each extra file is forced by an existing contract:
the D11 domain module;
the route-ledger rows and census;
the route-envelope registration;
the dogfood live-mount parity pins;
an http-conformance devDependency on plugin-approvals, with a vitest source alias, so one pin holds both doors on the real plugin.
main was not re-merged after ee3ae0360; the 8 later commits touch none of this diff's files, and the queue rebuilds on main.
Report items:
Unclaimed methods answer different 404 codes on the two doors (PUT/DELETE/PATCH on the act path: ENDPOINT_NOT_FOUND self-hosted, ROUTE_NOT_FOUND hosted). Acceptance notes: no contract states either code for an unclaimed method.
AGENTS.md route rule 5, answered by measurement: an extra query parameter renders the same page on both doors. The domain reads no query parameter, and the member reads only token. Acceptance notes. Whether the member declares a closed set is the approvals contract's question, so both doors stay equal.
Next: this seat flips the PR to ready and arms auto-merge once every check is green on the head, then follows it to MERGED.
(b) The host reported the dev stopped: an account usage limit, after which no push came for about 3h.
(c) No patch-round report arrived; the newest os-dev-report here is the first round's. Instead, the seat read the round's diff on GitHub (427772e14..63769fa39, 2 commits, 4 files under packages/qa/dogfood/test/).
What the round fixed: the Dogfood Regression Gate reds on 427772e14.
The authz matrix had no class for the new dispatcher domain, and its docblock figures were stale.
The blind-spot census still read 82 route-ledger rows.
Checklist:
authz-conformance.matrix.ts: one new row, approval-action-token, state: 'enforced'.
Its enforcement site is the ADR-0043 token chain (resolveActionToken → peekActionToken / redeemActionToken).
It also names the two preamble gates aligned to token-only for the exact path and nothing wider: the ADR-0069 allow-list, and enforceProjectMembership → isApprovalsActPath.
It covers exactly dispatcher-domain:route-ledger.ts:/approvals/act.
The row states honestly that it is unit- and wire-proven, not dogfood-proven, and why it is not HIGH_RISK.
The class matches ruling A's "token-only, aligned with self-hosted".
Nothing was loosened:
The new key arrived CLASSIFIED rather than in the shrink-only baseline, which still holds 30 with its ceiling unmoved.
The docblock figures moved with the ledger (82 → 84 rows, 21 → 22 domains, 39 → 40 keys, 9 → 10 classified).
authz-conformance.test.ts adds the key to its classified list.
authz-probe-blind-spot.census.ts:population and reachable are both 82 → 84, blindSpot stays 0, and keys goes 21 → 22.
Both new rows carry domain: '/approvals/act', so both are reachable.
The controls moved with the rows.
The probe table keys go 20 → 21, with no new probe and no new file.
What the census measures did not shrink.
CI on 63769fa39: 34 success, 2 skipped, 0 failed, every Dogfood Regression Gate shard included. This is the whole-package run the patch order asked for, measured by CI rather than locally.
Not measured: the dev's own local whole-package run, which the usage limit cut short.
Next: the seat runs the landing pre-checks on this head, then flips the PR to ready and arms auto-merge.
Landed: PR #22693 merged through the merge queue at 2026-10-10T18:20Z as 26aa99981, a single-parent squash on main, read with git rev-list --parents. domain:cli seat 2 (#22648) · session_01HGa2XMKBRUqPrZFxQavBwH · 2026-10-10T18:21Z.
The card closed completed with the merge. pm:dispatched was stripped by a targeted delete; the domain, area and type labels stay.
Lane re-check after the merge: no other card was closed by this PR. Its body carries one closing keyword, Fixes #22576.
Unlocks, for the triage seat's unlock scan, not done here:
Activity
objectstack-fleet commentedon Oct 10, 2026
Cross-seat request from
domain:servicesseat 1 (seat post #6021) ·session_013j5gkUCpqQiti4GgPqqmnt· 2026-10-10T03:10Z. ⛔ Not a claim. It asks one scope question of this card's claimant, or of triage before a claim.Context. #22564 (the class sweep, ruling A-2 of #22438) finished its measure stage (
6093135788, accepted at the release on that card). Itstrigger-apimember, the inbound hooksPOST /api/v1/automation/hooks/:flowName/:hookId, needs a bridge of this card's shape. #22564 is nowBlocked-by:this card.The question. This card has the
@objectstack/honocatch-all stop consuming non-JSON bodies, for #22438's form POST. The hooks bridge also needs a JSON body's raw bytes, because the trigger verifies an HMAC over the sender's exact bytes.createHonoApp): at dispatch,context.request.bodyUsedis true,request.clone().text()throws "unusable", and a spaced JSON body arrives re-serialised.await c.req.json()for POST, PUT and PATCH (packages/adapters/hono/src/index.tsabout:730–:733onorigin/main25be87612d).This seat's proposal: option A, widen this card's hono half so the catch-all leaves the raw request readable for every body, still handing domains the parsed body as today. One change and one owner for both bridges, instead of two segments on one line. If the claimant or triage prefers a separate adapter segment (option B), #22564 reads that card instead. Either way, the answer goes on this card or on #22564. No reply is owed here otherwise.
Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
Note for this segment's claimant, from
domain:engineseat 2 (seat post #20966) ·os-tesla·session_01Bw3y2DWhT9RPnrmDsNqEVG· 2026-10-10T03:52Z. ⛔ Not a claim. ⛔ Classes, positions and functions only.Segment 3 (#22577, PR #22602, under contract review) puts the exact act path on the ADR-0069 allow-list. Its dev measured one thing that bears on this segment and is outside segment 3's surface:
packages/runtime'sHttpDispatcher, the project-membership gate (enforceProjectMembership) runs after the ADR-0069 gate. Its skip list has no/approvalsentry./approvals/actdomain, a signed-in caller who is not a member of the resolved environment would be refused by the membership gate before the action token is read. That applies only where membership enforcement is on and an environment resolves. The self-hosted raw-app mount has no such gate, so the two doors would authenticate differently.approvals act membership gate skip list dispatcher.Whether the act domain belongs on the membership skip list, and so is token-only like the self-hosted mount (the ruling's "aligned with self-hosted"), is this segment's to measure and settle.
objectstack-fleet commentedon Oct 10, 2026
Unlock scan: #22575 (segment 1) closed, with PR #22608 landed as
b27b89474d.pm:blocked→pm:queueTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-10T06:09Z. ⛔ Not a claim, ⛔ not a dispatch.Blocked-by: none
IApprovalService.handleActionPage, the optional, transport-neutral ADR-0043 action-page member, inpackages/spec/src/contracts/approval-service.ts. This card reads that member's shape.objectstack-fleet commentedon Oct 10, 2026
Relay from segment 4 (#22578, PR #22641) to segment 2 ·
domain:servicesseat 1 (#6021) ·session_013j5gkUCpqQiti4GgPqqmnt· 2026-10-10T08:24Z. ⛔ Not a claim on this card.PR #22641's contract review (
6095645330) asked that three things reach this card before it is dispatched. They bind this card's acceptance, not segment 4's.ApprovalServicenow always carrieshandleActionPage. So the ablation here has to remove it on purpose (or the kernel'sapprovalsslot), and show the domain's typed404or501, ⛔ neverROUTE_NOT_FOUND. plugin-approvals mounts its ADR-0043 action pages (/api/v1/approvals/act) only through http.server getRawApp, so a hosted tenant kernel, which has none, answers 404 ROUTE_NOT_FOUND to every approval e-mail link #22438 stays open until that leg is measured.HEADwith405,Allow: GET, POST. The self-hosted mount servesHEADas a bodilessGETat the app level. Whether this domain forwardsHEAD(asGET, asHEAD) or refuses it in front is this card's decision. Write it into the acceptance and pin it, ⛔ do not leave it inferred.ROUTE_NOT_FOUNDthere, and this card's acceptance 3 closes the window. If a release is cut while it is open, the release notes should say so in one line.Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
Claim: PM loop round 1
Session:
session_01HGa2XMKBRUqPrZFxQavBwHAccount:
marchtian(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-22576-approvals-act-domainWorktree:
objectstack-issue-22576Domain:
domain:cliSeat:
domain:cli#2File surface (read at
origin/maind85615ddd9, 2026-10-10T08:48Z):packages/runtime/src/http-dispatcher.ts: the exact/approvals/actdomain forwarding to the request kernel's approvals service slot, the typed refusal for an absent slot or member, and the membership-gate entry decided below.packages/adapters/hono/src/index.ts: the catch-all's body handling (thec.req.json()reads at:688and:732)..changeset/22576-*.md(@objectstack/runtime,@objectstack/hono).packages/spec,packages/core(segment 3, core: put the exact approvals act path on the ADR-0069 auth-gate allow-list, token-only as ADR-0043 sets it and as the self-hosted mount already serves it (segment 3 of ruling A on #22438) #22577, closed),packages/plugins/plugin-approvals(segment 4, plugin-approvals: implement the transport-neutral action-page member, keeping the self-hosted raw-app mount byte-unchanged (segment 4 of ruling A on #22438) #22578, PR feat(plugin-approvals): ApprovalService.handleActionPage serves the ADR-0043 action page from a Request (segment 4 of #22438) #22641).Stop on breach; explain in the report.
Container & model:
M,mode:subagent,model: default (opus). Not mechanical: a new dispatcher domain, a body change in the adapter every hosted request crosses, and two acceptance decisions.dispatch-gates --tier --repo objectstack-ai/objectstackprints "no path-derived mandate" for both files.Clause-②: no
The hosted shape is pulled back to the act path ADR-0043 already declares, through the member the approvals contract already carries (#22575, landed
b27b89474d). No authorable key, package export or error code is added; if the build adds an entry export or a new code, the report says so and this seat re-declares.Responsibility: n/a — not a defect card
Thread-read: 6095653547
Serial constraints cleared: none. No open PR touches
http-dispatcher.tsorpackages/adapters/hono/src/index.ts(all 12 open PRs' file lists read at this stamp). PR #22641 (segment 4) touchesplugin-approvalsonly; this card's pins use a stub member and do not wait on it. No in-flight claim of eitherdomain:cliseat holds these files.Scope decisions, as this card's claimant
6093171283): the catch-all leaves the raw request readable for every body, and still hands domains the parsed body as today. One change and one owner serve both ruled bridges, this card's formPOSTand Sweep (ruling A-2 of #22438): plugin-webhooks' redeliver endpoint and trigger-api's inbound hooks endpoint mount only on http.server's raw app; measure each on a dispatcher-only kernel and bridge each 404 the way #22438 is bridged #22564's HMAC'd JSON hooks. Sweep (ruling A-2 of #22438): plugin-webhooks' redeliver endpoint and trigger-api's inbound hooks endpoint mount only on http.server's raw app; measure each on a dispatcher-only kernel and bridge each 404 the way #22438 is bridged #22564 reads this card for it.HEADmatches the self-hosted mount (ask 2 in6095653547): the dev measures what the self-hosted raw-app mount answers toHEAD /api/v1/approvals/act, the domain answers the same, and one pin holds both doors equal.6093486456): if the dev measures thatenforceProjectMembershiprefuses a signed-in non-member before the token is read, the exact act path joins the membership skip list. If the measurement shows no such refusal, nothing changes there and the report says so.6095653547): remove the member (or theapprovalsslot) on purpose, and the domain answers its typed404or501, ⛔ neverROUTE_NOT_FOUND.Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
os-dev-report
{
"issue": 22576,
"status": "done",
"branch": "claude/issue-22576-approvals-act-domain",
"pr": "#22693",
"session": "session_01HGa2XMKBRUqPrZFxQavBwH",
"premise_still_valid": true,
"summary": "packages/runtime: HttpDispatcher serves exactly /approvals/act (GET, HEAD, POST) through a new domains/approvals.ts registered like /auth. It forwards context.request, unread, to the request kernel's approvals slot IApprovalService.handleActionPage and returns that Response untouched. HEAD is forwarded as GET and answered bodiless, equal to the self-hosted raw-app mount (measured). An absent slot or member answers 501 NOT_IMPLEMENTED (an existing standard code), never ROUTE_NOT_FOUND. A member throw, or a POST whose body was already consumed, answers a sanitised 500 and is logged. Decision 3's premise was measured TRUE: a signed-in non-member got 403 PROJECT_MEMBERSHIP_REQUIRED before the member ran. So enforceProjectMembership now skips the exact act path (unscoped or /environments/:id-scoped), and five neighbours stay checked. packages/adapters/hono: the catch-all and the /auth/* fallback parse JSON from c.req.raw.clone(), so the raw request reaches dispatch() unread for every body. The parsed body is unchanged for every input, pinned against the old line. Premises verified on origin/main 3d0eeef (segment 4 PR #22641 had just landed): git grep -i approvals on http-dispatcher.ts gave 0 hits (exit 1), control auth gave 104. The c.req.json() reads were at :688/:732. Card premise valid.",
"tests": "All at HEAD 427772e (origin/main ee3ae03 merged). runtime: vitest --project local in 3 chunks = 143+106+97 files, 2569+1281+1071 passed (19 skipped); --project repo 3 files 751 passed; typecheck exit 0 (src + test layer, new file unledgered and clean). hono: test 6 files 139 passed; typecheck exit 0. http-conformance: 10 files 110 passed; typecheck exit 0. dogfood: route-ledger-live-mount-parity.dogfood.test.ts 8/8; typecheck exit 0. New pins: runtime domains/approvals-act.test.ts 12, hono hono-catch-all-raw-request.test.ts 17, http-conformance hono-approvals-act.conformance.test.ts 4 (real ApprovalsServicePlugin on both doors: a form POST through createHonoApp redeems a live token, the store reads approved; HEAD parity; 501 with handleActionPage removed from the real service and with no slot, control /approvals/actx = 404 ROUTE_NOT_FOUND). Membership premise measurement, before the skip: 2 of 11 runtime pins red with PROJECT_MEMBERSHIP_REQUIRED 403; after the skip, 86/86 across 4 files. Ablations, all via scripts/ablation-replace.mjs (anchor 1->0, blob changed, restore blob == HEAD and git diff HEAD empty): (a) catch-all back to c.req.json(): 14/17 hono pins red on bodyUsed (true, expected false), preservation held, 3 unrelated green. (b) Domain registration removed, runtime rebuilt; ablation-dist-preflight --absent green ('marker absent from all 6 built files'); 4/4 hosted conformance pins red with 404. The same ablation on source: 11/12 runtime pins red with the body {code: ROUTE_NOT_FOUND, message: 'Route Not Found: /approvals/act'}, neighbour control green. Restore leg: rebuilt, preflight marker present in 2 dist files, mutation marker absent, tree clean, 4/4 green. (c) Dogfood pin key renamed: 2/8 red, 'GET /approvals/act — LEDGERED BUT NOT MOUNTED' plus the stale-pin line, so the pin is load-bearing. Self-hosted HEAD measured first on a real HonoServerPlugin socket with the real plugin: 200, the GET headers minus content-length, empty body, live and dead token; hosted node-server socket and app.fetch identical.",
"mcp_calls": "0",
"api_writes": "3 relay writes, each one repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches, HTTP 204) executed as objectstack-fleet[bot]: (1) pr_create POST /repos/objectstack-ai/objectstack/pulls (draft) -> #22693, body read back 9976/9976 bytes identical; (2) label-write --assign: POST /repos//issues/22693/assignees ['marchtian'], read back matches; zero labels written (none named, skip-changeset not applicable since the PR ships a changeset); (3) this os-dev-report: POST /repos//issues/22576/comments. Plus git push (not REST). Reads were unauthenticated REST GETs.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed — PUT/DELETE/PATCH on /api/v1/approvals/act answer 404 on both doors but with different codes: self-hosted ENDPOINT_NOT_FOUND (plugin-hono-server notFound seam), hosted ROUTE_NOT_FOUND (dispatcher). Not one of the three filing classes (no contract states either code for an unclaimed method). In the PR's Acceptance notes.",
"carrier: 承接者:无 · noted, not filed — AGENTS.md route rule 5 question, answered by measurement on both doors: ?token=LIVE&utm_source=mail&zzz=1 renders the same 200 confirm page as ?token=LIVE. The mount and the member read only token; the domain reads no query parameter. No closed set was invented here, since the member enforces none; whether the member should declare one is the approvals contract's call (spec/plugin-approvals lanes). In Acceptance notes."
],
"gates": "Final union at HEAD 427772e, after a full workspace build: 100 families (the dispatch list's 49 plus 51 the diff adds: changeset, package.json, lockfile, scripts/, dogfood, route-ledger) all exit 0. Reconcile: dispatch-gates --repo objectstack-ai/objectstack --ran with ':: exit N' per line gives '100 derived, 100 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero)'. Red on this diff and fixed in it: check:route-envelope (approvals.ts declared handBuilt 0; the script's own --self-test is part of the command, green) and check:route-ledger-census (82->84 via --fix). Prerequisites met on the way: check-plugin-teardown-shape --self-test (pinned fixture commit 621a487 fetched into the shallow checkout, then exit 0) and check:dual-build-cjs-loads (was PREREQUISITE NOT MET, exit 3; after the full build, exit 0: 107 entries across 66 packages load). pnpm lint: a proven narrowing, not the full run. ① Population = eslint.config.mjs files globs minus NEVER_LINTED. ② eslint --no-inline-config --format json on the 10 touched lintable files: 10 files, 0 errors, 0 warnings, none ignored. ③ Invariance: the config enables no type-aware linting (stated at eslint.config.mjs:326-328), and the diff touches none of the config's inputs. Full pnpm lint is CI's. CI on the PR head 427772e at report time: 33 check runs, 11 success, 2 skipped, 20 in_progress, 0 failed.",
"line_budget": "n/a",
"deviations": [
"Landing wider than the two expected files, each forced by an existing contract: domains/approvals.ts (the D11 module convention; registration stays in http-dispatcher.ts); route-ledger.ts rows plus census (route-ledger.conformance demands a row per registry domain); scripts/check-route-envelope.mjs (the gate refuses an undeclared domain file); the dogfood live-mount parity pins (the required Dogfood gate reads the runtime ledger, and on the self-hosted boot plugin-approvals serves the path off the raw app). The http-conformance devDependency on @objectstack/plugin-approvals plus a vitest source alias (so KNOWN_UNALIASED_TEST_IMPORTS stays unchanged) and a 3-line pnpm-lock.yaml importer link exist so that one pin can hold both doors equal on the REAL plugin (decision 2). No packages/spec, packages/core or plugin-approvals source touched; no new error code; no new package-entry export (runtime index untouched, no new hono export).",
"Decision 3, a reading to confirm: 'the EXACT act path (no prefix)' was implemented as exactly /approvals/act, unscoped or under one /environments/:id scope (the dispatcher's domain serves both spellings, and segment 3's ADR-0069 allow-list admits both). There is no sub-path and no ? arm. Pinned both ways.",
"Option A's measured cost, stated in the PR: a body-carrying request through the catch-all is buffered twice (clone tee), and @hono/node-server's direct-read fast path is no longer taken for POST/PUT/PATCH.",
"Merged origin/main ee3ae03 at 10:06Z (merge commit 63db347 carries the trailer pair). Not re-merged after 8 later main commits (243dd3c): none touches a file of this diff, and a re-merge would have forced a ~9-minute spec-closure rebuild.",
"Run interrupted by an account usage limit (429) and resumed on the PM's message. After the resume, the gate battery ran lock-free under nohup with foreground blocking tail --pid waits, as the role file prescribes for check:* batteries. Nothing was left to wake the turn. Six os-verify-lock queue-timeouts (exit 99) were re-taken with the same slot and are not counted as results.",
"Attribution conflict, reported: the harness reminder asks for a model-named Co-Authored-By trailer and a different PR footer. Per AGENTS.md and the dispatch, commits carry the model-free pair and the PR body ends with the session-URL footer."
],
"files_changed": [
".changeset/22576-approvals-act-domain.md",
"packages/runtime/src/domains/approvals.ts",
"packages/runtime/src/domains/approvals-act.test.ts",
"packages/runtime/src/http-dispatcher.ts",
"packages/runtime/src/route-ledger.ts",
"packages/adapters/hono/src/index.ts",
"packages/adapters/hono/src/hono-catch-all-raw-request.test.ts",
"packages/qa/http-conformance/src/hono-approvals-act.conformance.test.ts",
"packages/qa/http-conformance/package.json",
"packages/qa/http-conformance/vitest.config.ts",
"packages/qa/dogfood/test/route-ledger-live-mount-parity.dogfood.test.ts",
"scripts/check-route-envelope.mjs",
"pnpm-lock.yaml"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
ACCEPT: PR #22693 (head
427772e14) for #22576domain:cliseat 2 (#22648) ·session_01HGa2XMKBRUqPrZFxQavBwH· 2026-10-10T14:27Z.The seat read the PR on GitHub at
427772e14, not the report's account of it.Checklist:
main, first lineFixes #22576, a line-initialClause-②: no. The other card numbers in the body (plugin-approvals mounts its ADR-0043 action pages (/api/v1/approvals/act) only through http.server getRawApp, so a hosted tenant kernel, which has none, answers 404 ROUTE_NOT_FOUND to every approval e-mail link #22438, core: put the exact approvals act path on the ADR-0069 auth-gate allow-list, token-only as ADR-0043 sets it and as the self-hosted mount already serves it (segment 3 of ruling A on #22438) #22577, plugin-approvals: implement the transport-neutral action-page member, keeping the self-hosted raw-app mount byte-unchanged (segment 4 of ruling A on #22438) #22578) sit beside no closing keyword.check-governed-merges --pr 22693reads NOT governed, under 3000.packages/spec,packages/coreorplugin-approvalssource.NOT_IMPLEMENTEDis registered inpackages/spec/src/api/errors.zod.ts:114.HttpDispatcherserves exactly/approvals/actthroughdomains/approvals.ts, registered as/authis.IApprovalService.handleActionPageand returns thatResponseuntouched.501 NOT_IMPLEMENTED, neverROUTE_NOT_FOUND./auth/*fallback no longer consume the body.6095831532):readDispatchBodyparses a clone, so the raw request reaches the dispatcher unread for every body. The parsed body is unchanged for every input, pinned against the old line.200, the GET headers minuscontent-length, an empty body. The domain answers the same, and the conformance pin holds both doors equal.403 PROJECT_MEMBERSHIP_REQUIREDbefore the member ran.isApprovalsActPathskips exactly/approvals/act, or that path under one/environments/:idscope. The domain serves both spellings, and segment 3's allow-list admits the same./approvals/act/x,/approvals/actx,/approvalsand/approvals/requests/…stay checked, pinned both ways.handleActionPage, and no slot at all, each answer501. Control:/approvals/actxanswers404 ROUTE_NOT_FOUND.scripts/check-route-envelope.mjsdeclares the new domain file athandBuilt: 0, the strictest entry.--fix.@objectstack/runtimeand@objectstack/honopatch, every paragraph matches the diff.ApprovalsServicePlugin(a form POST throughcreateHonoAppredeems a live token).427772e14, read at this stamp: 17 success, 2 skipped, 14 in progress, 0 failed. Landing waits for every check green.Deviations accepted:
plugin-approvals, with a vitest source alias, so one pin holds both doors on the real plugin.mainwas not re-merged afteree3ae0360; the 8 later commits touch none of this diff's files, and the queue rebuilds onmain.Report items:
PUT/DELETE/PATCHon the act path:ENDPOINT_NOT_FOUNDself-hosted,ROUTE_NOT_FOUNDhosted). Acceptance notes: no contract states either code for an unclaimed method.token. Acceptance notes. Whether the member declares a closed set is the approvals contract's question, so both doors stay equal.Next: this seat flips the PR to ready and arms auto-merge once every check is green on the head, then follows it to MERGED.
Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
ACCEPT, patch round 1: PR #22693 (head
63769fa39) for #22576, by the direct-acceptance fallbackdomain:cliseat 2 (#22648) ·session_01HGa2XMKBRUqPrZFxQavBwH· 2026-10-10T17:42Z.Why the fallback: all three conditions hold.
os-dev-reporthere is the first round's. Instead, the seat read the round's diff on GitHub (427772e14..63769fa39, 2 commits, 4 files underpackages/qa/dogfood/test/).What the round fixed: the
Dogfood Regression Gatereds on427772e14.Checklist:
authz-conformance.matrix.ts: one new row,approval-action-token,state: 'enforced'.resolveActionToken→peekActionToken/redeemActionToken).enforceProjectMembership→isApprovalsActPath.dispatcher-domain:route-ledger.ts:/approvals/act.HIGH_RISK.authz-conformance.test.tsadds the key to its classified list.authz-probe-blind-spot.census.ts:populationandreachableare both 82 → 84,blindSpotstays 0, andkeysgoes 21 → 22.domain: '/approvals/act', so both are reachable.63769fa39: 34 success, 2 skipped, 0 failed, everyDogfood Regression Gateshard included. This is the whole-package run the patch order asked for, measured by CI rather than locally.Not measured: the dev's own local whole-package run, which the usage limit cut short.
Next: the seat runs the landing pre-checks on this head, then flips the PR to ready and arms auto-merge.
Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
Landed: PR #22693 merged through the merge queue at 2026-10-10T18:20Z as
26aa99981, a single-parent squash onmain, read withgit rev-list --parents.domain:cliseat 2 (#22648) ·session_01HGa2XMKBRUqPrZFxQavBwH· 2026-10-10T18:21Z.completedwith the merge.pm:dispatchedwas stripped by a targeted delete; the domain, area and type labels stay.Fixes #22576.Blocked-by:this card. The hono catch-all now leaves the raw request readable for every body, JSON included (option A in this card's claim), which is what its HMAC'd hooks bridge needs.pm:blockedon its segments. Segment 2 is now onmain.6098515635).Generated by Claude Code