Repository navigation
rest: the 20 REST write routes answer a sandboxed hook's refusal in the hook's own words - #22763
Conversation
…he hook's own words Sharing rules, suggested bindings / overlay discard and approvals ask classifiedRefusalAnswer before their 500 arm relays .message, re-dressed in the nested envelope through nestedRefusalEnvelope. POST /packages/publish takes its fallback status from the same classification and its sentence from sandboxBusinessMessage. The external-datasource catch arms read the sentence through sandboxBusinessMessage with status and code unmoved. The pin's ANSWERS_THE_WRAPPER map is empty; its 20 rows sit in ANSWERS_THE_SENTENCE with the status and code they answer. Claude-Session: https://claude.ai/code/session_019SvPnd2bzECRNmAU9i6E4k Co-authored-by: Claude <noreply@anthropic.com>
Section 5 drives the twenty repaired rows with a plain fault (each keeps its own terminal), a declared 409 refusal (the three 500-arm families and the publish door answer it at 409; external datasources keep their own 400) and a sandboxed refusal carrying an unregistered code and a marked sentence (declaredCode and userMessage ride the nested envelope). Claude-Session: https://claude.ai/code/session_019SvPnd2bzECRNmAU9i6E4k Co-authored-by: Claude <noreply@anthropic.com>
…refusal answer Claude-Session: https://claude.ai/code/session_019SvPnd2bzECRNmAU9i6E4k Co-authored-by: Claude <noreply@anthropic.com>
…st-write-hook-sentence
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 17 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f1c23b9fc2ad596a82bd42ea7675bebeb1dd6570 && git checkout f1c23b9fc2ad596a82bd42ea7675bebeb1dd6570
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e86530088a137275d803db29cb7c3afbc0f9f10e 2525cfcf6f3c364bf09303a90d9902a13983bb8f && git checkout -B drift-repro e86530088a137275d803db29cb7c3afbc0f9f10e && git merge --no-ff 2525cfcf6f3c364bf09303a90d9902a13983bb8f
node scripts/docs-audit/affected-docs.mjs --json e86530088a137275d803db29cb7c3afbc0f9f10e
|
Fixes #22719
Clause-②: no
What this does
Twenty write routes in
@objectstack/restbuilt their error answer by hand from.message. For a sandboxed hook's refusal,.messageis the QuickJS debug wrapper (hook 'NAME' threw: Error: SENTENCE), so these routes shipped the wrapper while the other 24 write doors answer the hook's own sentence. Each family now answers through the shared read the conforming doors use (triage6101568631), with no local unwrap and no per-route text:rest-server.ts. Each family's own arms run first, unchanged. Where its 500 arm used to relay.message, it now asksclassifiedRefusalAnswerfirst. That is the/datadoor's classification, and it readssandboxBusinessMessage. The answer is re-dressed in the nested envelope through the family's own emitter. The re-dress is one function,nestedRefusalEnvelopeinerror-response.ts. It applies the same derivation the record-share family'srespondSharingErrorapplies inline:codefalls back to the catalog's floor for the status, anddeclaredCodeanduserMessagecome from the classification.POST /packages/publish(1), inpackage-routes.ts. This door runs the dispatcher twin's rule (resolveThrownHttpError). It now takes the status it falls back to fromclassifiedRefusalAnswer, and the sentence fromsandboxBusinessMessage.code,declaredCode,userMessageanddetailsstay this door's own mapping, so nothing it already answered moves.external-datasource-routes.ts. Both catch arms read the sentence throughsandboxBusinessMessage. Status and code do not move (see "Not chosen" below).The pin's
ANSWERS_THE_WRAPPERmap is empty. Its 20 rows sit inANSWERS_THE_SENTENCE, each pinned at the status and code it answers, and §3 now holds the map empty.Rows whose status or code changed (a sandboxed refusal that declares nothing)
POST /api/v1/sharing/rulesRULE_DEFINE_FAILED, flatVALIDATION_ERROR, nestedDELETE /api/v1/sharing/rules/:idOrNameRULE_DELETE_FAILED, flatVALIDATION_ERROR, nestedPOST /api/v1/sharing/rules/:idOrName/evaluateRULE_EVALUATE_FAILED, flatVALIDATION_ERROR, nestedPOST /api/v1/security/suggested-bindings/:id/confirmSUGGESTION_CONFIRM_FAILEDVALIDATION_ERRORPOST /api/v1/security/suggested-bindings/:id/dismissSUGGESTION_DISMISS_FAILEDVALIDATION_ERRORPOST /api/v1/security/permission-sets/:id/discard-overlayINTERNALVALIDATION_ERRORPOST /api/v1/approvals/requests/:id/approveAPPROVAL_APPROVE_FAILED, flatVALIDATION_ERROR, nestedPOST /api/v1/approvals/requests/:id/rejectAPPROVAL_REJECT_FAILED, flatVALIDATION_ERROR, nestedPOST /api/v1/approvals/requests/:id/recallAPPROVAL_RECALL_FAILED, flatVALIDATION_ERROR, nestedPOST /api/v1/approvals/requests/:id/reviseAPPROVAL_REVISE_FAILED, flatVALIDATION_ERROR, nestedPOST /api/v1/approvals/requests/:id/resubmitAPPROVAL_RESUBMIT_FAILED, flatVALIDATION_ERROR, nestedPOST /api/v1/approvals/requests/:id/reassignAPPROVAL_REASSIGN_FAILED, flatVALIDATION_ERROR, nestedPOST /api/v1/approvals/requests/:id/remindAPPROVAL_REMIND_FAILED, flatVALIDATION_ERROR, nestedPOST /api/v1/approvals/requests/:id/request-infoAPPROVAL_REQUEST_INFO_FAILED, flatVALIDATION_ERROR, nestedPOST /api/v1/approvals/requests/:id/commentAPPROVAL_COMMENT_FAILED, flatVALIDATION_ERROR, nestedPOST /api/v1/packages/publishINTERNAL_ERRORVALIDATION_ERROREXTERNAL_DATASOURCE_ERROR/EXTERNAL_IMPORT_ERROROn every row the message is now the hook's sentence instead of the wrapper.
VALIDATION_ERRORisstandardErrorCodeForHttpStatus(400), the same code the record-share family answers for an undeclared sandboxed refusal. A refusal that declares its own code keeps it, and an unregistered spelling ridesdeclaredCode. A declareduserMessagerides along too. §5 pins both on the 16 re-dressed rows. "Flat" means{ code, error: 'TEXT' }, and "nested" means{ success: false, error: { code, message } }. Security's own emitter writes{ error: { code, message } }on every arm, and still does.Ride-along (triage
6101568631: no decision owed)On the 15 sharing-rule, security and approval rows, a non-sandbox refusal can declare its own
status(orstatusCode) andcodewithout matching any route-specific arm. It is now answered at its declared status in the nested envelope, where it used to get500 FAMILY_CODE. §5 pins this with a declared409 RESOURCE_CONFLICT.POST /packages/publishalready answered a declared status, so it is unchanged.These are unchanged, and §5 pins them on all 20 rows: a plain fault keeps each route's own terminal (its status, its code and its words). The route-specific arms run first and answer exactly as before. Those are the sharing-rule prefixes, the approvals mapping and the security typed arm.
Not chosen: the external-datasource status (dispatch assumption 5)
These four routes answer every refusal at
400with their own code. Adopting the shared classification there would change a 4xx domain answer. The import route relays the metadata door's declared409 DESTRUCTIVE_CHANGEas400 EXTERNAL_IMPORT_ERROR, whichexternal-import-destructive-remedy.dogfood.test.tspins, and the classification would answer 409. So these rows take the shared read only. One consequence remains: a sandboxed refusal that declares, for example,409answers400 EXTERNAL_*here with the correct sentence, where the conforming doors answer 409. Reported, not chosen.Verification (head
2525cfcf6, after mergingorigin/main7aa8d51c0)rest-write-route-hook-refusal-sentence.ledger.test.ts,--project local): 105 passed (§1 1, §2 44, §3 1, §4 2, §5 57).b90ead477). Then the four sources were restored from the basee7e6544abinto the working tree only. On-disk proof: thenestedRefusalEnvelope/refusalSentence/classifiedRefusalAnswercounts in the reverted files read 0. The pin then went red on 51 of 105 cases: the 20 moved §2 rows, the 15 declared-409 ride-along cases and the 16 declared-channel cases. The other 54 stayed green: the 24 original §2 rows, the 20 plain-fault cases, the 5 publish and external declared-409 cases, §1, §3, §4 and §5's roster case. The direction was red, as expected. Restoring usedgit checkout HEAD, with a trap. After that,git hash-objectmatched the HEAD blob for all four files andgit diff HEADwas empty.@objectstack/rest:pnpm --filter @objectstack/rest test(thelocalproject) gave 272 files, 5239 passed and 326 skipped.test:repogave 5 files, 191 passed and 1 skipped.typecheck(tsc --noEmitpluscheck:test-typecheck) was green. Earlier, both projects together at150c886a6gave 277 files, 5373 passed and 327 skipped.dispatch-gates --commandswas re-derived on this change: 65 families, the dispatch's 57 plus 8 that the changeset brings. All 65 were run at2525cfcf6, and--ranreports 65 run, 0 NOT-MEASURED and 0 UNRUN. Selected verdict lines:check:route-envelope:rest-server.tsholds at stringError 39 and siblingCode 54, so no new dialect site was added.check:error-status-conformance: every derivable runtime status is documented.check:issue-citations: 9 citations, all resolve.check:nul-bytes: OK.check-adr-0087-registration: no declared-breaking changeset.eslint --no-inline-config --format jsonran over the 5 touched TS files: 5 results, 0 errors, 0 warnings, 0 ignored. The population is read from eslint's own config. The changeset.mdmatches no config object (--print-configanswersundefined). Invariance:eslint.config.mjsenables no type-aware linting (noparserOptions.project, as its own note states). Its only cross-file reads are two baseline JSONs this diff does not touch. So the diff cannot change the verdict on any untouched file.Acceptance notes
rest-server.tsfor the publish answer, but that answer is built inpackage-routes.ts, which this PR edits.error-response.tsgainsnestedRefusalEnvelope, which is not exported from the package index. Both files are inpackages/rest. No public export, error code or key is added.respondSharingErrorkeeps its inline copy of the derivation thatnestedRefusalEnvelopestates. Moving it onto the function is a byte-identical refactor of an arm outside this card's surface. Carrier: none.2525cfcf6through this pin's harness, at the route handler:TypeError: boom) getshook 'mz_guard' threw: TypeError: boomon all 20 rows: 500 with the family code on 16 rows, and 400EXTERNAL_*on the 4 external rows.503gets 500 with the family code and the wrapper on the 15 rest-server rows. It gets 503 with the sentence on publish, and 400 with the sentence on the external rows.origin/mainat7aa8d51c0brought no edit topackages/restfrom security(import): an import's reference resolution matches a lookup cell against a target whose exposure refuses reads (census row 7 of #22661) #22739's lane or any other lane.Generated by Claude Code