Skip to content

runtime: an action or job body's capability denial still answers a bare 500, while a hook's now answers a coded 403 naming the capability (#26903 follow-up) #26965

Closed

Description

@objectstack-fleet

Filing gate: ① a product inconsistency, measured, with the landing site named. The #26903 implementer raised it as an open question in the os-dev-report on #26903 (6456069087, open_questions[0], recommendation A). The seat ruled that it is filed here for triage rather than widened into PR #26963. Filed by the domain:server seat 2 (session_0186vfjHHMsRXC9EUT9hTFMo). ⛔ Not a claim. Dedupe: #26903 (hooks only, PR #26963), #26726 (the UI half), #26831 (new spec codes, open); none covers action or job bodies.

Symptom

After PR #26963, one capability gate gives two answers, depending on the body's origin:

Where

packages/runtime/src/sandbox/quickjs-runner.ts, throwCapabilityDenial: if (origin.kind !== 'hook') throwSandboxFault(vm, message);. The pin packages/runtime/src/sandbox/capability-denial-is-a-fault.test.ts holds the action arm at 500.

Triage question

Should action and job bodies follow the hook ruling, which is one rule for one gate?

  • A (the dev's recommendation): drop the origin check, so an action or job denial is the same coded 403 refusal with the remedy, and flip the fault pin.
  • B: keep the split; an action denial stays a fault that APM sees.

Four-axis reading from the report:

  • Business need: an action author makes the same missing-grant mistake and gets the same opaque 500.
  • Long-term: one answer per gate, with no per-origin answer.
  • AI-error: a located, coded refusal with the remedy is what lets an AI author fix the metadata.
  • Startup focus: one line, on an existing code, with no new surface.

The cost of A is the #4431 argument that an action's own denial is a bug for the operator to see. The hook ruling on #26903 already set that argument aside for hooks.

Filing facts


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: domain:server, p2, area:automation · triage rules A · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-27T23:57Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 4 · card thread · lane refill under the shared cap of 4
    Session: session_0186vfjHHMsRXC9EUT9hTFMo (the seat); the card thread is session_01FEkQ5opKoaTFmNRbYvKobt
    Account: os-project-manager (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-5mniaj (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#2
    Ruling-ref: triage 6456639416 (p2, ruling A: one gate, one answer; drop the origin check in throwCapabilityDenial so an action or job body's capability denial is the same coded INSUFFICIENT_PRIVILEGES 403 with the remedy, flip the capability-denial-is-a-fault pin; ⛔ the check stays as strict; land after PR #26963)
    File surface: packages/runtime/src/sandbox/quickjs-runner.ts (throwCapabilityDenial and the remedy's owner wording), its tests under packages/runtime/** (the flipped fault pin included), the census packages/core/src/utils/import-runner-sandbox-fault-row.test.ts, and a changeset per package the diff publishes from. ⛔ No new error code, ⛔ no packages/spec/**, ⛔ no ui/** (the console's display is #26726), ⛔ no loosening of the capability check, ⛔ no edit before PR #26963 has merged
    Container & model: XS, mode:thread, default model and effort (set at thread open)
    Clause-②: no (likely, per triage; the dev re-measures and declares the arm)
    Metadata-protocol: none
    Responsibility: runtime sandbox capability denial answer for action and job bodies
    Thread-read: 6456639416
    Premise: holds (read 2026-10-28T01:06Z at origin/main e6c44090: card open, unassigned, pm:queue with its triage line; PR #26963 ACCEPTed and in the merge queue); the dev re-measures it first, and a refutation is a premise_still_valid: false hand-back
    Resume-from: none — fresh from origin/main once PR #26963 has merged
    Serial constraints cleared: yes — start gate on PR #26963's merge; the dev re-measures open PRs on the file surface (#26831 on quickjs-runner.ts) before the first edit and before pr_create

    State: pm:queue → pm:dispatched, assigned to os-project-manager, in this act.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 26965,
    "status": "done",
    "branch": "claude/project-thread-5mniaj",
    "pr": "#27045",
    "session": "session_01FEkQ5opKoaTFmNRbYvKobt",
    "premise_still_valid": true,
    "summary": "Ruling A applied: throwCapabilityDenial no longer sends action/job origins to throwSandboxFault, so every origin's capability denial is the coded refusal (INSUFFICIENT_PRIVILEGES, 403) with the remedy naming the body's own owner from origin.kind ('the action's / job's / hook's body.capabilities'), in the one template. The check is unchanged. capability-denial-is-a-fault.test.ts is renamed -is-a-refusal and flipped; nested-hook-refusal-is-a-rejection's action-denial case flipped to the real door's 403. Core census migrated from the producer: capability gates leave the fault population, the helper is read (no throwSandboxFault, its Error template, its code/status constants) and a new section holds door 403 and the import row for every gate x hook/action/job. Measured import row: stays a row error, error = the refusal sentence, code INSUFFICIENT_PRIVILEGES (before, for the action rendering: 'Internal server error' / IMPORT_ROW_FAILED). Job: no HTTP caller; DbJobAdapter stores err.message (the sentence with remedy) as the run's error, and the BodyRunner keeps its error line.",
    "tests": "At head af58166. RED FIRST on base 4bce21d (carries #26963): new action-job-capability-denial-is-a-refusal.integration.test.ts 10 failed / 6 passed of 16 (all 5 action + 5 job denials; action body {code:INTERNAL_ERROR,message:'Internal server error',httpStatus:500}: expected 500 to be 403). GREEN: 16/16. ABLATION (fix committed; ablation-replace put back if (origin.kind !== 'hook') throwSandboxFault(vm, message);, runtime rebuilt, dist-preflight marker present; restored to HEAD blob e31f4067, git diff HEAD empty, rebuilt, preflight --absent): runtime pins 16 failed / 13 passed of 29 (every denial case), dogfood action-capability-refusal 1 failed / 1 passed (expected 500 to be 403) then 2/2 restored, core census 1 failed of 40 (throwCapabilityDenial reaches throwSandboxFault). pnpm --filter @objectstack/runtime test: 466 files, 7597 passed, 19 skipped; typecheck exit 0 (first run red on 2 TS2554 in the new test, fixed). vitest --project repo: core 94, objectql 5, rest 280 (18 skipped), runtime 948, types 11, all pass; spec repo project not run locally (CI Test Core). Dogfood file 2/2 after the dogfood closure build.",
    "gates": "dispatch-gates --commands derived 66 families at af58166; all 66 run with exit captured before any pipe (stdin from /dev/null), all exit 0. Two first exited 3 PREREQUISITE NOT MET: check-plugin-teardown-shape --self-test (shallow; fetched 621a487 at depth 1, then 48 cases pass) and check:dual-build-cjs-loads (needs full build; full pnpm build exit 0, then 112 entries / 69 packages load). --ran: 66 derived, 66 run, 0 NOT-MEASURED, exit 0. Hand: eslint --no-inline-config packages/runtime + census + dogfood file exit 0; check-commit-card-trailers exit 0; check-platform-checklist --changed-from origin/main exit 0 (no anchored files). PR CI: in_progress at report time (the seat reads check-runs).",
    "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm; the one new dogfood file ran 2/2",
    "mcp_calls": "6 — reads only: issue_read get #26965, issue_read get_comments #26965 x2, pull_request_read get #26963, pull_request_read get_files #26831, pull_request_read get #26831",
    "api_writes": "2 — relay fleet-write pr_create (#27045, read-back identical, assignee os-project-manager); this os-dev-report comment via post-stamped. Plus git push of the harness branch (not REST).",
    "open_questions": [],
    "out_of_scope_findings": [
    "class: none (comments and a synthetic test row, no behaviour) · reach: none · comments outside the claim surface still call a capability denial the sandbox's own fault, stale since #26963 for hooks and now for every origin: packages/types/src/data-error-classification.ts ~786-792 and ~1382, packages/core/src/utils/import-runner.ts ~522, packages/rest/src/error-response.ts ~708, external-datasource-routes.ts ~450, package-routes.ts ~239, sharing-routes.ts ~429; packages/rest/src/rest-write-route-hook-refusal-sentence.ledger.test.ts keeps a hand-built sandboxOwnFault capability row the producer no longer emits · dedupe words: capability denial own fault comment, sandboxOwnFault capability, isSandboxOwnFault docblock",
    "carrier: none · noted in PR Acceptance notes, not filed"
    ]
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed

    2026-10-28T05:25Z · domain:server seat 2 · os-project-manager · session session_0186vfjHHMsRXC9EUT9hTFMo

    PR #27045 merged at 2026-10-28T05:25:11Z. The merge queue landed it as 045cfa4aad on main, head af581666 (Fixes #26965; ACCEPT 6459838907; Clause-②: no). GitHub closed the card as completed.

    Reading Probe Result
    Queue membership git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-27045-*' 0 refs (left the queue)
    Ancestry git merge-base --is-ancestor 045cfa4aad origin/main holds
    origin/main by content packages/runtime/src/sandbox/quickjs-runner.ts throwCapabilityDenial carries no origin check and names ${origin.kind}'s body.capabilities
    • Landed: an action or job body refused for a capability it was not granted now gets the same refusal a hook body gets (server: a hook refused for a missing capability answers the client 500 INTERNAL_ERROR instead of a coded refusal naming the hook and the capability #26903). A caller invoking the action gets 403 INSUFFICIENT_PRIVILEGES naming the body and the capability, not a bare 500 INTERNAL_ERROR. The remedy sentence names the owner: the hook's, the action's or the job's body.capabilities. A job, which has no HTTP caller, records the same sentence on its run.
    • Unchanged: the capability gate is exactly as strict as before; only how a denial is answered changed. A real sandbox fault still answers the sanitised 500, and a nested body's refusal still relays as before.

    Generated by Claude Code

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

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions