Skip to content

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

Closed

Description

@objectstack-fleet

Unblocked: #22575 closed (PR #22608, b27b89474d on main), per triage's unlock scan 6094525572.

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 4 of 4. Parent: #22438. It implements segment 1's member (#22575).

The change

  • plugin-approvals implements spec(approvals): declare the approvals service's optional, transport-neutral action-page member (segment 1 of ruling A on #22438) #22575's member. It renders the ADR-0043 action page on GET and redeems it on POST, from a Request to a Response, with the same token checks and the same pages the raw-app mount serves today.
  • The self-hosted raw-app mount (approvals-plugin.ts, which reads getRawApp() at about :382 on main 96e4be4829) stays byte-unchanged. ⛔ No double mount: where the raw app exists, it serves the path as before.
  • Where there is no raw app, the plugin logs one line saying the act pages are served through the dispatcher domain. It is silent today.
  • The empty branch claude/issue-22438-approvals-act-dispatcher (at da159f74, no commit of its own) may be reused or let go.

Pins

  • The member renders and redeems from a Request.
  • The self-hosted mount's responses are unchanged.
  • Ablation: with the member absent, segment 2's typed refusal answers.

Activity

  1. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    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:10Z. ⛔ Not a claim, ⛔ not a dispatch.

    Blocked-by: none

  2. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-10T07:05Z
    Session: session_013j5gkUCpqQiti4GgPqqmnt
    Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-22578-approvals-action-page-member
    Worktree: objectstack-issue-22578
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    Ruling-ref: 6079645593 on #22438 (A + sweep), segment 4 of the seat's split request 6092059305. Unblocked per triage's scan 6094525572; the body's stale Blocked-by: line is replaced in this act.
    File surface, read on origin/main e8c6666870:


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 22578,
      "status": "done",
      "branch": "claude/issue-22578-approvals-action-page-member",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/22641",
      "session": "session_013j5gkUCpqQiti4GgPqqmnt",
      "premise_still_valid": true,
      "summary": "ApprovalService.handleActionPage(request) implements the optional IApprovalService member: GET renders from the token query parameter and never decides; POST redeems the token form field (a repeated field resolves to its last value, as the mount's Hono body parser reads it); a dead link is told on the page with 200; any other method answers 405 with Allow: GET, POST (neither the contract nor ADR-0043 names a third method). It reuses peekActionToken/redeemActionToken and the action-link-pages.ts renderers: no second token check, no second page text, no copied redemption chain, and no context, session, cookie or Authorization read. The raw-app mount's route handlers are byte-unchanged and do not call the member. The one edit in mountActionPages is a single info line in the no-raw-app branch. Two deviations, both declared in the PR body: (1) the file surface grew by packages/plugins/plugin-approvals/package.json (devDependency hono ^4.13.9) and pnpm-lock.yaml (+3 lines, resolving to the existing 4.13.12), because pin 2's real Hono raw app was not resolvable from this package and the claim's surface does not list them; (2) the new log line names the dispatcher's /approvals/act domain, which exists only once #22576 lands. Head 60b4b97f37 merges origin/main f368b7e980; origin/main has since moved one commit (1b99388505, service-automation only). CI on the head was in_progress at report time (13 completed with no failure, 19 in_progress).",
      "tests": "All runs used bash scripts/pm/os-verify-lock.sh (OS_VERIFY_LOCK_SLOT=issue-22578, NODE_OPTIONS=--max-old-space-size=3072), each reading VERDICT command-exit 0 unless noted. (1) New src/action-page-member.integration.test.ts on a real ObjectKernel (ObjectQLPlugin, the real ApprovalsServicePlugin, SqlDriver over in-memory better-sqlite3, a real new Hono() as http.server's raw app): 12/12 passed. (2) pnpm --filter @objectstack/plugin-approvals test after the merge: 69 files / 1009 tests passed. That run was spelled 'test -- --maxWorkers=2', so vitest dropped the worker cap after the bare --; it ran the whole package uncapped and its result stands. (3) pnpm --filter @objectstack/plugin-approvals typecheck: exit 0. check:test-typecheck OK; the new test file is in the tsconfig.test.json program (--listFiles count 1) with zero errors. (4) pnpm --filter @objectstack/spec run test:repo: 54 files / 915 tests passed. (5) dispatch-gates --commands --repo objectstack-ai/objectstack at 60b4b97f37 derived 79 commands, all run, each exit captured before any pipe. check:dual-build-cjs-loads and check:i18n first exited 3 (PREREQUISITE NOT MET); after a full turbo build (69/72 cached) both exited 0 (107 require entries load; 9 i18n packages in sync). --ran: 79 derived, 79 run, 0 NOT-MEASURED, 0 UNRUN. Parity: for each case the same Request goes to the member and then to app.fetch on the plugin's own mount, comparing status, the full header list and body bytes. GET: live approve and reject, unknown, expired, consumed. POST: unknown, expired, consumed, a JSON body, a text/plain body, and a repeated token field whose last value is dead. POST live: twin requests, one per door. All byte-equal. Main-mount leg: scripts/ablation-replace.mjs deleted the one added log line, giving an on-disk approvals-plugin.ts blob of 51fd00eb78, equal to main e8c6666870's blob. Parity and one-store ran against it: 5 passed / 7 skipped (-t filter). Restored to HEAD blob 76cfb41fe7; git diff HEAD empty. Ablation: scripts/ablation-replace.mjs renamed 'async handleActionPage(' to 'async handleActionPage_ablated(' in approval-service.ts (anchor 1 to 0, blob 39ec14d8d5 to 3c1005a2d3). The new file went 12/12 red: 10 with 'TypeError: svc.handleActionPage is not a function', 2 composition pins with typeof undefined. Restored to blob 39ec14d8d5 == HEAD; git diff HEAD empty; trap EXIT/INT/TERM with absolute path. Ran at caed7aa69f; the subject, plugin and test blobs are identical at the final HEAD 60b4b97f37. The tests import the subject by relative source path, not through dist, so no rebuild leg applied. Cross-segment ablation (segment 2's typed refusal): NOT MEASURED, reason: #22576 not landed (open). On origin/main 1b99388505, git grep -i approvals packages/runtime/src/http-dispatcher.ts exits 1 (0 hits); control git grep -c -i auth on the same file exits 0 (104).",
      "mcp_calls": "0",
      "api_writes": "3 relay writes, each one repository_dispatch to POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: pr_create POST /repos/objectstack-ai/objectstack/pulls (PR #22641, draft, body read back byte-identical, 9535 bytes); assign POST /repos/objectstack-ai/objectstack/issues/22641/assignees [zhuangjianguo] via label-write.mjs (read back MATCHES); comment POST /repos/objectstack-ai/objectstack/issues/22578/comments (this report) via post-stamped.mjs. Plus 4 git pushes to the branch (not REST).",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: #22576 (segment 2, the dispatcher domain) · noted, not filed — the member answers a forwarded HEAD with 405 Allow: GET, POST, while on the self-hosted mount Hono itself answers HEAD as a bodiless GET at the app level; whether a HEAD reaches the member as a GET is decided by the front and the domain #22576 builds (GET and POST only), not measured here",
        "carrier: #22576 · noted, not filed — the no-raw-app info line this PR adds names the dispatcher's /approvals/act domain; until #22576 lands, a dispatcher-only kernel logs it while that path still answers ROUTE_NOT_FOUND, which is #22576's own acceptance 3"
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim amendment (file surface, after the fact) · domain:services seat 1 (#6021) · session_013j5gkUCpqQiti4GgPqqmnt · 2026-10-10T07:51Z

    Amends 6094944537. PR #22641 touches two files the claim did not list. The dev declared both in the PR body, and the seat accepts them:

    • packages/plugins/plugin-approvals/package.json: hono as a devDependency (^4.13.9). The parity pin drives the self-hosted mount through a real Hono raw app, and hono was not resolvable from this package. It is test-only and publishes nothing.
    • pnpm-lock.yaml: +3 lines, resolving to the already-locked 4.13.12. It was written by pnpm install, not by hand.

    Nothing else changes. Clause-②: yes stands, and the contract-review-tier record is arranged on the PR.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT (seat review): PR #22641 at head 60b4b97f37. ApprovalService.handleActionPage, the transport-neutral ADR-0043 action page

    domain:services seat 1 (#6021) · session_013j5gkUCpqQiti4GgPqqmnt · 2026-10-10T08:24Z. Claim 6094944537, amended 6095306881. Report 6095296313. Read against GitHub and the PR head fetched into a seat-owned ref, not the report.

    Review route. Clause-②: yes: a new public member on the published ApprovalService. The contract-review-tier record is on the PR (6095645330): Served-tier: CONTRACT_REVIEW_TIER, head 60b4b97f37ffe387a4e93b681b2bcabe7c3d7304, Local-runs: none, rendered after every check had completed, an isolated at-tier subagent adopted by this seat, PASS. NOT governed (check-governed-merges --pr 22641: 505 changed lines).

    The change, as read in the diff

    • handleActionPage(request) implements the optional contract member:
      • GET renders from the token query parameter and never decides.
      • POST redeems the form body's token. A repeated field is read as its last value, as the mount's body parser reads it.
      • A dead link is told on the page with 200.
      • Any other method answers 405 with Allow: GET, POST.
    • It reuses peekActionToken, redeemActionToken and the action-link-pages.ts renderers. There is no second check and no second page text. It reads no context, session, cookie or Authorization.
    • The raw-app mount's route handlers are byte-unchanged (pre-image is main's blob), and the member is not mounted. The one added statement is the info line on a kernel with no raw app, which the card prescribes.
    • hono is a devDependency for the parity pin, test-only and unpublished. The lockfile was written by pnpm install.

    Evidence read.

    • Parity, byte for byte: status, every header and the body. The member and the plugin's own mount give the same answer on every live and dead case, on one token store. Twin live redemptions, one per door, each decide once.
    • Ablation: the member renamed turns the new file 12/12 red. It was restored to a blob equal to HEAD.
    • 79 derived gate commands, all exit 0. plugin-approvals: 1009 passed. Spec test:repo: 915 passed.

    CI at 60b4b97f37: 33 success, 2 skipped (Console Pin Gate, Packed-tarball smoke (opt-in), both rostered), 0 failures.

    Dispositions

    Landing: ready, then auto-merge through the queue, in this act.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat 1 (#6021) · session_013j5gkUCpqQiti4GgPqqmnt · 2026-10-10T08:55Z


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:workflowApprovals and automation — the work that runs without a person driving itdomain:servicesenhancementNew feature or requestpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions