Skip to content

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

Closed

Description

@objectstack-fleet

Filed by seat domain:ui#1 (seat post #25966, session_01JQwxbZeBv2y91towxasSUT) as the server half of #26726, per triage first grade 6447036960 ("domain:server, a child card filed on claim"). ⛔ Not a claim; left for triage. Parent: #26726 (ui half: the Studio hook editor gains a capabilities control).

Symptom (from #26726's QA reproduction)

A hook whose sandboxed-JS body calls ctx.api.find(…) without capabilities: ["api.read"] is refused by the capability check ("capability 'api.read' not granted"). With onError: abort, the user's record save gets a bare 500 INTERNAL_ERROR. With onError: log, a refused ctx.log is swallowed and nothing is logged.

Expected

A capability refusal reaches the client as a coded 4xx that names the hook and the missing capability. With onError: log, the refusal itself is recorded where hook errors are logged.

Ruled

Triage 6447036960, verbatim: "⛔ The capability check stays as strict as it is." Only the error mapping changes.

Landing site (the implementer verifies on current main)

The server hook runner's error mapping for capability refusals (HookBodyCapability, packages/spec/src/data/hook-body.zod.ts, names the tokens). #26831 (open) adds a hook refusal catalogue on the spec side; check whether it already covers this before adding a code.

Filing facts

  • reach: /api/v1/data writes on any object with such a hook; measured by the studio-qa-2026-10-27 data gap-fill card (QA id G11).
  • Dedupe words: capability not granted, hook 500, api.read, onError abort, refusal code.

Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: domain:server, p2, area:automation · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-27T21:09Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 4 · card thread · 18.2.0 wave-2 split with server seat 1
    Session: session_0186vfjHHMsRXC9EUT9hTFMo (the seat); the card thread is session_01FEkQ5o5GCE3vGxq6G24dZu
    Account: os-project-manager (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-ktf17y (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 6453651751 (p2: ⛔ the capability check stays as strict as it is, only the error mapping and the log arm change; a new spec code goes back to triage); seat ruling: an existing declared code (or #26831's if merged), one mapping on the refusal side of the sandbox classifier, ⛔ no new code
    File surface: the hook sandbox capability refusal and its HTTP / engine error mapping (packages/services/service-automation/**, the sandbox package, packages/runtime/** or packages/objectql/** where the hook error is mapped, found by the message), the onError: log sink, their tests, and a changeset per package. ⛔ No packages/spec/** (a needed code is a stop), ⛔ no Studio change (#26726), ⛔ no loosening of the capability check
    Container & model: S, mode:thread, default model and effort (set at thread open)
    Clause-②: no (likely: a 500 becomes a coded refusal; the dev re-measures and declares the arm)
    Metadata-protocol: none
    Responsibility: hook sandbox capability refusal mapping (server half of #26726)
    Thread-read: 6453651751
    Premise: holds (read 2026-10-27T22:01Z at origin/main e1ab32ed0b: card open, unassigned, pm:queue with its triage line; the split with server seat 1 gives this card to seat 2); the dev re-measures it first, and a refutation is a premise_still_valid: false hand-back
    Resume-from: none — fresh from origin/main
    Serial constraints cleared: yes — the dev re-measures open PRs on the file surface 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 27, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 26903,
    "status": "done",
    "branch": "claude/project-thread-ktf17y",
    "pr": "#26963",
    "session": "session_01FEkQ5o5GCE3vGxq6G24dZu",
    "premise_still_valid": true,
    "summary": "Abort half holds and is fixed; log half is refuted on main. In a HOOK body every capability gate (api.read, api.write, api.transaction, crypto.uuid, log, ctx.title(field)) now throws through one helper, throwCapabilityDenial (packages/runtime quickjs-runner.ts), which crosses the VM as a coded refusal (code INSUFFICIENT_PRIVILEGES, status 403, an existing StandardErrorCode member; no new code, no spec change; #26831 still open so none of its codes used). Every data door answers it through mapDataError's sandbox unwrap arm: 403 INSUFFICIENT_PRIVILEGES with the gate's text plus the remedy, naming the hook and the capability, nothing written. The gate is exactly as strict; action and job bodies keep the 500 fault contract. onError: log was measured live at e1ab32e: the denial is ALREADY logged (BodyRunner ERROR + the [hook] handler failed (onError=log; suppressing) line), so that half of the card did not reproduce; the PR pins that line (now with the remedy) and the save proceeding.",
    "tests": "New packages/runtime/src/sandbox/hook-capability-denial-is-a-refusal.integration.test.ts (real ObjectQL + SqlDriver better-sqlite3 + QuickJS + hook wrapper, read off mapDataError): 15/15 at 60488ef; with quickjs-runner.ts at its e1ab32e blob 5 failed / 10 passed, the 5 being exactly the onError: abort cases (expected 500 to be 403), log and control pass on main. New packages/qa/dogfood/test/hook-capability-refusal.dogfood.test.ts (real stack, POST /data): 4/4; red leg with runtime rebuilt from the base blob (ablation-dist-preflight --absent ok) 1 failed / 3 passed, body {error: Internal server error, code: INTERNAL_ERROR} expected 500 to be 403; restored (blob hash + clean git status), rebuilt, preflight marker present, 4/4. Live os dev showcase probe POST /api/v1/data/probe1_note: main 500 INTERNAL_ERROR, branch 403 INSUFFICIENT_PRIVILEGES naming hook probe1_guard and api.read, records unchanged; control with api.read 201; log variant 201 and logged. runtime vitest src/sandbox + src/domains: 188 files, 4590 tests passed. runtime typecheck exit 0. eslint --no-inline-config packages/runtime + new dogfood file exit 0.",
    "gates": "Local at 60488ef: dispatch-gates --commands --repo objectstack-ai/objectstack derived 71 families, 71 run, all exit 0 (codes captured before any pipe); --ran reconciliation 71 derived / 71 run / 0 NOT-MEASURED / 0 UNRUN, exit 0. Round 1 reds fixed in-PR: check:driver-memory-census (test moved from InMemoryDriver to SqlDriver, no ledger row) and check:error-status-conformance (INSUFFICIENT_PRIVILEGES ratcheted out of scripts/error-status-unpinned-baseline.json with --update). Full pnpm build exit 0 (116/116). CI check-runs on #26963: in_progress at report time, the seat reads them.",
    "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm; the one new dogfood file was run red and green as above",
    "mcp_calls": "5 — read-only: issue_read get x2 (26903, 26726), issue_read get_comments x2 (26903), pull_request_read get x1 (26831); no MCP write tool",
    "api_writes": "3 — git push of claude/project-thread-ktf17y (not REST); relay pr_create #26963 (fleet-write run 40601669954, read-back identical, assignee os-project-manager); this os-dev-report comment on #26903 via post-stamped.mjs",
    "open_questions": [
    {
    "question": "Should an ACTION or JOB body's capability denial also become the coded 403 refusal? This PR keeps them on the #4431 fault contract (500) because the seat ruling named hook bodies only, so the same denial now answers 403 from a hook and 500 from an action.",
    "options": [
    "A: align actions and jobs on the same refusal in a follow-up card (one helper line: drop the origin.kind check, flip capability-denial-is-a-fault.test.ts)",
    "B: keep the split; a capability denial in an action stays a fault visible to APM"
    ],
    "recommendation": "A. Business need: an action author hits the same missing-grant mistake and gets the same opaque 500. Long-term: one rule for one gate, no per-origin answer. AI-error axis: a coded, located refusal with the remedy is what lets an AI author fix the metadata; a 500 hides it. Startup focus: it is a one-line change on an existing code, no new surface. The only cost is the #4431 APM argument, which the hook ruling already set aside."
    }
    ],
    "out_of_scope_findings": [
    "class: a · reach: Studio record save on an object with such a hook answers 403 INSUFFICIENT_PRIVILEGES and the console's isPermissionError (ui/packages/react/src/utils/error-message.ts) shows its fixed no-permission line instead of the hook/capability sentence · evidence: memory-recorded UI 403 rule, not re-measured in the browser · carrier: #26726 (the UI half)",
    "carrier: #26831 — overlap on quickjs-runner.ts; git merge-tree with head e559783 auto-merges, and its host-relay marker sits in hostErrorToVm which this denial passes through · noted, not filed",
    "carrier: none — packages/types data-error-classification.ts comment cites a hook capability text as a sandbox own fault; still true for actions/jobs, outside the claim surface · noted in Acceptance notes only"
    ]
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed

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

    PR #26963 merged at 2026-10-28T01:56:24Z. The merge queue landed it as e46e25c0e1 on main (Fixes #26903; ACCEPT 6457859907; Clause-②: no). GitHub closed the card as completed.

    Reading Probe Result
    Queue membership git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-26963-*' 0 refs (left the queue)
    Ancestry git merge-base --is-ancestor e46e25c0e1 origin/main holds
    origin/main by content packages/runtime/src/sandbox/quickjs-runner.ts carries throwCapabilityDenial

    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