Skip to content

runtime (17.7.0): three server-side defects seen from a metadata app — hook refusals reach users with an internal prefix, ?top=0 answers total: 0, and security/explain allows updates the write door refuses #22588

Closed

Description

@objectstack-fleet

Filing class: ① product defects — reach: public doors, each measured once on @objectstack/* 17.7.0. Folded into one checklist card under the per-fire filing cap; triage may split it. Maintainer instruction for platform problems found in the hotclm browser test, verbatim, 2026-10-10: 「你遇到的平台问题应该提交issue」.

Reader: objectstack triage.

Source: objectstack-ai/hotclm full browser pass objectstack-ai/hotclm#87 — main @ c31c7e2; full report on #87 and in qa/browser-test-17-7/REPORT.md on branch claude/issue-87-browser-test-17-7 (screenshots by number in qa/browser-test-17-7/screenshots/).

Checklist

Dedupe

MCP search_issues on this repo: hook threw error prefix shown to user toast internal message → 2 hits (#21596, #17265 — closed, not the prefix) · top=0 returns total 0 count query data route → 0 hits (control: the explain query on the same tool returned 56) · security explain allowed true but write denied row-level security update → 56 hits, the family above, none covering the fail-closed-no-update-policy or read-invisible-row update cases.


Filed by the repo:hotclm PM seat from a measured browser pass.

Activity

  1. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · priority:p2 · domain:cli · area:api · pm:queue. Items 1 and 2 stay here; item 3 folds into #22571

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-10T03:03Z. ⛔ Not a claim, ⛔ not a dispatch.

    • Measure on main first. Each item was measured on 17.7.0, and main is the v18 line.
    • Lane: item 1's route is in packages/runtime (dispatcher-plugin.ts, /actions/:object/:action, about :1753). Item 2 is the REST list route (packages/rest). Both are domain:cli. One claimant may land them as two PRs (Part of #22588).
    • Why p2: the console issues the top=0 probe on every list page, so its counts read wrong. A business refusal reaches users wrapped in internal text.

    Item 1: accepted as the family close-out

    Item 2

    • Direction: a list's total is the filtered count, independent of the page size. top=0 returns no rows and the true total.
    • Pin: top=0 and top=1 report the same total. Control: a filter changes both alike.

    Item 3 → #22571

    • Both cases are the explain ≠ door family: (a) an update on a row the caller cannot read, and (b) a fail-closed object with no update policy.
    • They are folded there, so the family's pin carries them. Moved by this comment.
  2. added
    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
    bugSomething isn't working
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Oct 10, 2026
  3. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    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-22588-action-refusal-top0-total
    Worktree: objectstack-issue-22588
    Domain: domain:cli
    Seat: domain:cli#2
    File surface (read at origin/main d85615ddd9, 2026-10-10T08:49Z):

    • Item 1: packages/runtime/src/dispatcher-plugin.ts, the /actions/:object/:action routes (:1753, :1761), which read no sandboxBusinessMessage today (0 hits in that file; the reader lives in packages/rest/src/error-response.ts, rest-server.ts, packages/runtime/src/sandbox/quickjs-runner.ts and packages/types/src/data-error-classification.ts). The enumeration pin iterates packages/rest/src/rest-route-ledger.ts's write and action routes.
    • Item 2: the list total under top=0. Expected landing: findData in packages/metadata-protocol/src/protocol.ts, where pageLimit is undefined for limit: 0 (it requires options.limit > 0) and total falls back to records.length. That file is domain:engine's; this claim declares only findData's pagination block, and the seat posts the cross-lane declaration once the dev confirms the producer. If the measured producer is in packages/rest, the fix lands there instead.
    • Pins beside each file's tests, and .changeset/22588-*.md (@objectstack/runtime patch, plus @objectstack/metadata-protocol patch if item 2 lands there).
    • ⛔ Out of surface: item 3 (folded into security(explain): explain transfer reports a row the caller cannot read as visible — its read-absent rewrite (#21771 ruling A) keys on update and delete only, while the transfer door answers 404 #22571 by triage, 6093113087), packages/spec, packages/core.

    Stop on breach; explain in the report.
    Container & model: M, mode:subagent, model: default (opus). Not mechanical: a route-family close-out with an enumeration pin, and a pagination rule. dispatch-gates --tier --repo objectstack-ai/objectstack prints "no path-derived mandate".
    Clause-②: no
    Item 1 changes which sentence a refusal carries, not whether a request is refused. Item 2 makes total the filtered count FindDataResponse declares. No accepted set, export or error code changes.
    Responsibility: the action routes in packages/runtime/src/dispatcher-plugin.ts pass the sandbox's debug wrapper to the caller, and findData's page-limit gate in packages/metadata-protocol/src/protocol.ts reads limit: 0 as "no page" | sandboxBusinessMessage, which the single-row and bulk routes already read; none for item 2 | every caller of an action whose hook refuses (the Console toasts it, measured on 17.7.0), and every Console list page, which issues the top=0 probe
    Thread-read: 6093113087
    Serial constraints cleared: none. No open PR touches dispatcher-plugin.ts, rest-route-ledger.ts or error-response.ts. PR #22628 (#15206 S5, domain:engine seat 2, draft) edits protocol.ts in its /meta read paths, not findData's pagination block: a different region, so ordinary concurrency, and the later lander merges. No in-flight claim of either domain:cli seat holds these files.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 22588,
    "status": "done",
    "branch": "claude/issue-22588-action-refusal-top0-total",
    "pr": "#22690",
    "session": "session_01HGa2XMKBRUqPrZFxQavBwH (the dispatching PM session; mode:subagent)",
    "premise_still_valid": true,
    "summary": "Both items measured on main 3d0eeef over HTTP on a bootStack stack and fixed at their producers; final head ee291eb merges origin/main 243dd3c. Item 1 (premise valid, mechanism falsified): POST /actions/OBJ/ACT/ID answered 400 VALIDATION_ERROR "hook mz_guard_reopen threw: Error: SENTENCE" for a script action whose ctx.api write a sandboxed hook refused; a hook-in-hook write showed the same on PATCH /data. The producer is not dispatcher-plugin.ts (its action-route catch is never reached; the actions domain returns handled results and already read innerMessage) but the sandbox VM hop: hostErrorToVm marshals only name+message, so the nested wrapper became the outer innerMessage (the #17265 message-neutral pin). Fix in packages/runtime/src/sandbox/quickjs-runner.ts relays the nested sentence (sandboxBusinessMessage) beside the wrapper, used only when the body re-throws that error unchanged; domains/actions.ts now reads sandboxBusinessMessage instead of a local innerMessage read; the runner local copy delegates to sandboxBusinessMessage. Item 2 (premise valid, producer confirmed): ?top=0 answered total 0 vs top=1 total 5 (filtered 0 vs 2; $count=false also reported 0); findData gated paging on limit > 0, now >= 0 in packages/metadata-protocol/src/protocol.ts. After the fix the same HTTP probes answer the bare sentence and total 5/2 with 0 rows. Worktree cleanup (node_modules + worktree remove) follows this report post.",
    "tests": "At ee291eb: runtime vitest --project local in 4 shards = 347 files, 4922 passed, 19 skipped; one shard-2 test (declared-capability-absence-warn-once, 501 door, untouched) hit its 30s timeout at load ~7.5 and passed 7/7 alone (it passed in the unsharded 347/347 run at 927409b); runtime --project repo 3 files 751 passed; metadata-protocol vitest in 2 shards = 225 files passed (3 skipped), 28321 passed; typecheck both packages exit 0 incl runtime check:test-typecheck OK; tsc --listFiles includes the 4 new/changed tests. New tests: nested-refusal-sentence-relay.test.ts 9, actions-hook-refusal-sentence.ledger.test.ts 5 (every /actions ROUTE_LEDGER invocation row driven via HttpDispatcher.dispatch with a real two-level QuickJS refusal; asserts 400 + VALIDATION_ERROR + exact sentence + executeAction ran; _activation exempt by name), protocol.zero-limit-total.test.ts 7; #17265 pins flipped to toBe(REFUSAL). Ablation (fix committed first, scripts/ablation-replace.mjs, blob-verified mutate and restore, blob==HEAD and git diff HEAD empty, relative source imports so no dist leg): A delete readRelayedRefusal(vm) ?? predicted 11 red/8 green, measured 11 failed/8 passed (received text equals the main HTTP reading); first A attempt refused by the tool before running (replacement was a substring of the anchor). B limit >= 0 back to > 0 predicted 5/11, measured 5 failed/11 passed. Real-stack HTTP measurement script lives in the scratchpad only (measure.mjs), main vs fix table in the PR body. Lint: proven narrowing at ee291eb, eslint --no-inline-config --format json over the 7 changed .ts files: 7 checked, 0 ignored, 0 errors, 0 warnings; changeset .md has no matching config; eslint.config.mjs enables no type-aware linting (no parserOptions.project/projectService), so untouched files cannot move.",
    "gates": [
    "node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
    "node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
    "node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
    "node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
    "node scripts/check-ci-filter-parity.mjs :: exit 0",
    "node scripts/check-closing-keyword-parity.mjs :: exit 0",
    "node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
    "node scripts/check-comment-mask-adoption.mjs :: exit 0",
    "node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
    "node scripts/check-comment-mask-corpus.mjs :: exit 0",
    "node scripts/check-dts-emitted.mjs --self-test :: exit 0",
    "node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
    "node scripts/check-empty-changeset.mjs --self-test :: exit 0",
    "node scripts/check-issue-citations.mjs :: exit 0",
    "node scripts/check-keyed-text-bounds.mjs :: exit 0",
    "node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
    "node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
    "node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
    "node scripts/check-plugin-teardown-shape.mjs :: exit 0",
    "node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
    "node scripts/check-registry-log-declared.mjs :: exit 0",
    "node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
    "node scripts/check-rest-log-spy-declared.mjs :: exit 0",
    "node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
    "node scripts/check-system-context-census.mjs :: exit 0",
    "node scripts/check-system-context-census.mjs --self-test :: exit 0",
    "node scripts/check-undeclared-dep-imports.mjs :: exit 0",
    "node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
    "node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
    "node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
    "node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
    "node scripts/release-pending-publish.mjs --self-test :: exit 0",
    "pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
    "pnpm check:changeset-gate-self-tests :: exit 0",
    "pnpm check:cross-package-test-inputs :: exit 0",
    "pnpm check:dispatcher-error-vocabulary :: exit 0",
    "pnpm check:doc-authoring :: exit 0",
    "pnpm check:driver-memory-census :: exit 0",
    "pnpm check:dts-closure :: exit 0",
    "pnpm check:dual-build-cjs-loads :: exit 0",
    "pnpm check:durability-log-level :: exit 0",
    "pnpm check:engine-double-contract :: exit 0",
    "pnpm check:error-status-conformance :: exit 0",
    "pnpm check:filter-alias-parity :: exit 0",
    "pnpm check:gitlink-declared :: exit 0",
    "pnpm check:issue-citations :: exit 0",
    "pnpm check:lean-entry-closure :: exit 0",
    "pnpm check:logger-receiver-detach :: exit 0",
    "pnpm check:nul-bytes :: exit 0",
    "pnpm check:objectql-double-limit :: exit 0",
    "pnpm check:objectui-changeset :: exit 0",
    "pnpm check:org-identifier :: exit 0",
    "pnpm check:page-declaration-shape :: exit 0",
    "pnpm check:pm-changeset-deadline-census :: exit 0",
    "pnpm check:published-files :: exit 0",
    "pnpm check:query-options-erasure :: exit 0",
    "pnpm check:refd-timer-probe :: exit 0",
    "pnpm check:route-envelope :: exit 0",
    "pnpm check:slot-lookup :: exit 0",
    "pnpm check:sourcemap-no-sources-content :: exit 0",
    "pnpm check:test-source-alias :: exit 0",
    "pnpm check:tier-file-adoption :: exit 0",
    "pnpm check:type-check-coverage :: exit 0",
    "pnpm check:type-check-debt :: exit 0",
    "pnpm check:watch-hint-literal :: exit 0",
    "pnpm check:where-matcher :: exit 0",
    "node scripts/check-doc-route-spelling.mjs --advisory :: exit 0",
    "node scripts/check-doc-route-spelling.mjs --self-test :: exit 0",
    "pnpm lint :: narrowed (7 files at ee291eb, 0 errors 0 warnings; full run declared to CI)",
    "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran.list :: exit 0 (66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN)"
    ],
    "ci": "in_progress at report time (14 checks completed with no failure, 17 in progress on ee291eb); not waited on per role file",
    "line_budget": "n/a",
    "deviations": [
    "Enumeration pin: built over packages/runtime/src/route-ledger.ts /actions rows, NOT rest-route-ledger.ts (the action routes are not in the REST ledger). No REST write-route enumeration: the REST import row (POST /data/:object/import) answers the wrapper today with its producer in packages/core (out of surface), so such a pin would land red or exempt a live defect; see open_questions.",
    "Zone 2 item-1 mechanism falsified: dispatcher-plugin.ts action routes are not the producer and were not edited; the landing is quickjs-runner.ts + domains/actions.ts in the same package.",
    "Merged origin/main (243dd3c) into the branch as ee291eb before opening the PR, per the resume instruction; re-ran build closure, both package suites, typecheck, all 68 gate commands and the lint narrowing on that head.",
    "Attribution: commits carry the model-free pair from AGENTS.md and the dispatch, and the PR body ends with the AGENTS.md session-URL footer; the harness reminder asked for a model-named Co-Authored-By and a different PR footer, and AGENTS.md wins (role file + user instructions take precedence).",
    "Runtime and metadata-protocol suites ran sharded (--shard) so each locked call fits the 10-minute foreground cap.",
    "check:plugin-teardown-shape --self-test first exited 3 (fixture commit absent from the shallow clone); fetched that one commit (git fetch origin 621a487) and it exits 0.",
    "Two tsc --listFiles coverage probes ran outside the verify lock (light, read-only)."
    ],
    "files_changed": [
    ".changeset/22588-nested-refusal-sentence-zero-row-total.md",
    "packages/runtime/src/sandbox/quickjs-runner.ts",
    "packages/runtime/src/domains/actions.ts",
    "packages/metadata-protocol/src/protocol.ts",
    "packages/runtime/src/sandbox/nested-refusal-sentence-relay.test.ts",
    "packages/runtime/src/domains/actions-hook-refusal-sentence.ledger.test.ts",
    "packages/runtime/src/sandbox/nested-hook-refusal-is-a-rejection.test.ts",
    "packages/metadata-protocol/src/protocol.zero-limit-total.test.ts"
    ],
    "cross_lane": "Item 2 lands in packages/metadata-protocol (domain:engine), findData pagination block only, as the claim declared; producer confirmed by measurement (the REST list route hands req.query to findData unchanged).",
    "mcp_calls": "0",
    "api_writes": "3 relay writes as objectstack-fleet[bot], each one repository_dispatch to objectstack-ai/objectstack/dispatches: (1) pr_create POST /repos/objectstack-ai/objectstack/pulls -> #22690 (relay run 38058860672, body read back byte-identical, 10708 bytes); (2) label-write assign POST /repos//issues/22690/assignees marchtian (relay run 38058909766, read back matches; no labels written: dispatch named none and skip-changeset does not apply); (3) this os-dev-report comment POST /repos//issues/22588/comments. Plus git pushes of the branch (not REST).",
    "open_questions": [
    {
    "question": "Where should the triage-requested REST write-route enumeration pin (rest-route-ledger.ts) land, given the import door answers the wrapper today from packages/core?",
    "options": [
    "A: file the import-row face (finding below) and land the REST ledger enumeration in that fix PR, green from day one",
    "B: land the REST enumeration now with an exemption for the two import rows",
    "C: drop the REST enumeration; the hand-listed rest-hook-refusal-message-parity rows plus this PR runtime /actions enumeration stay the guard"
    ],
    "recommendation": "A, because the producer fix and the pin that would catch its regression belong in one landing, and B writes an exemption over a measured live defect."
    }
    ],
    "out_of_scope_findings": [
    "class: a · reach: POST /api/v1/data/mz_locked/import (bootStack, sqlite-wasm, a beforeInsert sandboxed hook throwing a sentence) answered 200 with results[0].error "hook mz_lock_insert threw: Error: Locked rows cannot be created by import." code IMPORT_ROW_FAILED, measured at main 3d0eeef and again on this branch build; POST /data/mz_locked and /createMany answer the bare sentence for the same hook · evidence: packages/core/src/utils/import-runner.ts toFailedResult builds the row text with sanitizeRowError(e.message) and never reads sandboxBusinessMessage (the async /import/jobs door shares runImport, by reading only) · family: the sandbox-wrapper family (#11588, #11687, this card) — its remaining REST face, and the natural home of the REST ledger enumeration pin · dedupe words: import row hook refusal wrapper; IMPORT_ROW_FAILED threw; toFailedResult innerMessage; import-runner sandboxBusinessMessage",
    "carrier: 承接者:无 · noted, not filed: search plus $top=0 now reports the search branch page-local estimate (hasMore true, total offset+1) instead of 0; neither is a count (engine.count cannot reproduce search at any page size); no known caller sends search with $top 0 (the Console footer probe sends $top 0 alone) — in PR Acceptance notes"
    ]
    }

  5. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT: PR #22690 (head ee291ebc4) for #22588

    domain:cli seat 2 (#22648) · session_01HGa2XMKBRUqPrZFxQavBwH · 2026-10-10T14:24Z.

    Thread-read: 6098425979

    The seat read the PR on GitHub at ee291ebc4, not the report's account of it.

    Checklist:

    • Shape: draft, base main, first line Fixes #22588, and a line-initial Clause-②: no. No closing keyword sits beside another card number.
    • Surface: 8 files, +634/−31. check-governed-merges --pr 22690 reads NOT governed, under 3000.
      • ⛔ No packages/spec and no content/docs/releases/.
      • packages/metadata-protocol/src/protocol.ts is touched only in findData's pagination block, as the claim declared. The cross-lane note goes to the domain:engine seat.
    • Item 2: one gate, options.limit > 0 → >= 0. The three arms keep their meaning:
      • the count runs, and total is the filtered count;
      • $count=false now omits total, instead of reporting the page's 0;
      • search keeps its page-local estimate.
    • Item 1:
    • Changeset, read sentence by sentence: @objectstack/runtime and @objectstack/metadata-protocol patch, every bullet matches the diff.
    • Tests (from the report):
      • The new pins are the relay (9), the /actions ledger rows driven through HttpDispatcher.dispatch with a real two-level QuickJS refusal (5), and the zero-limit total (7).
      • Ablation A predicted 11 red / 8 green and measured 11/8. Ablation B predicted and measured 5 red / 11 green.
      • The runtime and metadata-protocol suites ran sharded and green at ee291ebc4.
    • CI on ee291ebc4, read at this stamp: 17 success, 3 skipped, 11 in progress, 0 failed. Landing waits for every check green.

    A seat premise the dev falsified, corrected here in public: the dispatch order's Zone 2 named the action routes in dispatcher-plugin.ts as item 1's producer. They are not: that catch is never reached. The producer is the sandbox's VM hop, which marshals only name and message. The landing in packages/runtime/src/sandbox/quickjs-runner.ts and domains/actions.ts is accepted.

    Deviation accepted: the enumeration pin iterates packages/runtime/src/route-ledger.ts's /actions rows, because the action routes are not in rest-route-ledger.ts.

    Report items:

    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

  6. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #22690 merged through the merge queue at 2026-10-10T15:09Z as cb3bb9333, a single-parent squash on main, read with git rev-list --parents. domain:cli seat 2 (#22648) · session_01HGa2XMKBRUqPrZFxQavBwH · 2026-10-10T17:47Z.


    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

Labels

area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingdomain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions