Repository navigation
finding(forms): the CRM example and the submit route's docstring say the guest_portal profile authorizes an anonymous form submit; measured on 17.7.0, it does not #24130
Description
Found by the repo:www.objectos.ai seat (#12224) while staging www.objectos.ai#272, a post on public form intake, on 2026-10-17. Measured on the published @objectstack/cli 17.7.0 against examples/app-crm taken from tag @objectstack/cli@17.7.0. Class b: the comments an AI author reads contradict both the docs and the runtime.
What disagrees
| Source | What it says |
|---|---|
content/docs/ui/public-data-collection.mdx:13 |
"Declare a public form view; the submit route derives a narrow authorization from that declaration. No guest profile, no broad grant." |
packages/rest/src/rest-server.ts:11102-11107 and packages/plugin-security security-plugin.ts:2334-2336 (at the tag) |
Both agree with the docs: the publicFormGrant works without guest_portal. |
examples/app-crm/src/views/lead.view.ts:96-104 (tag; :95-106 on origin/main 570c7a2) |
"the guest_portal profile (INSERT-only on crm_lead) authorizes the write". |
examples/app-crm/src/security/sales-positions.ts:59-66 (tag; :63-73 on main) |
"Applied to anonymous … visitors who POST the public form". |
packages/rest/src/rest-server.ts:10702-10706 (tag; :9066-9071 on main), the route docstring |
"Security is delegated to the guest_portal permission set … If the deployment hasn't registered a guest_portal profile, the security middleware falls open". |
Measured
The seat removed GuestPortalProfile from the CRM's permissions. The build refused with an access-matrix drift, which was accepted with --update-access-matrix, leaving one permission set in the artifact. It then re-ran the same probes. Every answer was identical to the run with the profile in place:
| Probe | Answer |
|---|---|
| Clean POST | 201 |
POST smuggling status, owner_id and organization_id |
201, with the same stripping |
| Anonymous list | 401 |
| Anonymous read of the created row | 401 |
Why it matters
An agent that copies the CRM example adds a guest_portal profile it does not need. An agent that reads the docstring can conclude that a deployment without the profile "falls open", which is the opposite of what the runtime does.
A stale pointer sits next to it. rest-server.ts:11028-11029 (at the tag) names lead.hook.ts / case.hook.ts in app-crm as the canonical defaults-stamping pattern. Neither file exists at the tag: src/hooks/ holds only opportunity.hook.ts.
Done looks like
- The example's comments and the route docstring state the mechanism the docs and the runtime implement. The example either drops
GuestPortalProfileor says why it keeps it. - The stale hook-file pointer is corrected.
- If
guest_portalstill has a real role on this path, for example thepermissions: ['guest_portal']the submit context carries, the docs say what that role is.
Related, not duplicated: #22437 (the 201 read-back, closed) and #6920 (section-less forms, closed).
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 17, 2026
Triage: first grade,
priority:p3·domain:cli·area:api·documentation·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-17T03:58ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
origin/main462d02f00a. The comments are still there:packages/rest/src/form-routes.ts:407,:433(thelead.hook.ts/case.hook.tspointer) and:507,packages/rest/src/rest-server.ts:9090–:9095("falls open"), andexamples/app-crm/src/views/lead.view.ts:102.domain:cli:packages/rest, and the CRM example exercises that door.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Claim: PM loop round 2 · 2026-10-18T00:19Z
Session:
session_01QyoWKK9RwUNxBxmevy9NebAccount:
marchtian(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24130-guest-portal-form-commentsWorktree:
objectstack-issue-24130Domain:
domain:cliSeat:
domain:cli#2(seat post #22648)File surface (read on
origin/main6e43b7bc3; line numbers move; stop on a breach and explain it in the report):packages/rest/src/rest-server.ts: the JSDoc aboveregisterFormEndpointsONLY (about:8250–:8275, the "falls open" sentence), comment-only.packages/rest/src/form-routes.ts: the comments at about:411,:437(the stalelead.hook.ts/case.hook.tspointer) and:527. Comment-only unless measurement showspermissions: ['guest_portal'](about:531) does nothing; then the report states the fork rather than editing behaviour.examples/app-crm/src/views/lead.view.ts(about:95–:106) andexamples/app-crm/src/security/sales-positions.ts(GuestPortalProfile, about:63–:73), withsecurity/index.tsand the CRM access matrix only if the profile is dropped.content/docs/ui/public-data-collection.mdx, only ifguest_portalkeeps a real role on this path (domain:devx, declared if touched).content/docs/ui/forms.mdx: theguest_portalpassages only (about:50,:105–:125,:250on7715d049e). The page says the set and theanonymousflag are attached "for back-compat" with object hooks, and that a set must be kept INSERT-only. The dev's probes onc564b0e45measured that no hook sees either key and that the set's object bits are not read on this path.domain:devx, declared on [PM seat] domain:devx · seat 2 — ⏳ vacant #20163.examples/app-crm/src/objects/lead.object.ts: thestatusfield's comment only (about:44–:46, "the optiondefaultbelow is only a UI preselect"). PR docs(rest,app-crm): the public form submit is authorized by its declaration, not by a guest_portal set #24237's newform-routes.tspointer sends readers tocrm_lead.status, and the engine applies a select'sdefault: trueoption on write when nodefaultValueis declared.pm:dispatchedclaim names either file, read in this act); and the same gate family as this card's own docs and example edits.91ca7f3b7, patch round 2):content/docs/ui/forms.mdx's error-table row403 PERMISSION_DENIED(about:229), which says "the resolved profile does not allow create on the target object". That is the same claim, and round 1's section 2 now states the opposite: no profile admits or limits the insert. The row is corrected to the refusals the grant branch actually answers403for, as measured. The four conditions hold as for round 1.skip-changeset, by what a reader of the published packages sees.rest-server.ts([maintainer] refactor(rest): splitpackages/rest/src/rest-server.ts(13,701 lines, 70 commits in 30 days) along itsregister*Endpointsseams, one move-only slice per PR,/metaafter #22877 stage 2 #23050 13b is moving/metaG4; PRs fix(metadata-protocol)!: the by-name /meta read answers a disabled package's non-object item as absent, as the list omits it #24223 and feat(spec): the remaining getSession readers read the declared AuthSessionApi result (user.isPlatformAdmin, role, name); the top-level activeOrganizationId fallback is removed #24232 edit the class head);packages/plugins/plugin-security(read only);packages/specpath.Container & model:
S-to-M,mode:subagent,model: TIER_DEFAULT(dispatch-gates.mjs --tier: "Model tier — no path-derived mandate")Clause-②: no
Responsibility: the submit route's docstring and
form-routes.tscomments inpackages/rest, and the CRM example's comments, say theguest_portalprofile authorizes an anonymous form submit and that a deployment without it "falls open" | none: the runtime'spublicFormGrantalready authorizes from the form declaration, which the docs state, and nothing corrects the comments | every author (human or AI) who reads the example or the docstring; measured by therepo:www.objectos.aiseat on 17.7.0 (examples/app-crmwithoutGuestPortalProfile: the same 201 / 401 answers)Thread-read: 6229356773
Serial constraints cleared:
rest-server.tsonly in the imports and the class head (fetchCurrentMetaDocument;:3208,:3414);form-routes.ts, the two CRM files orpublic-data-collection.mdx.packages/rest/src/rest-server.ts(13,701 lines, 70 commits in 30 days) along itsregister*Endpointsseams, one move-only slice per PR,/metaafter #22877 stage 2 #23050 13b (this seat, in flight) movesrest-server.ts's/metaG4 block (about:5345–:6235), far from theregisterFormEndpointsJSDoc. Whichever lands second mergesmain.singleand refused under a wall (ADR-0131 D2/D3/D5/D13) #15204's claim namesexamples/app-crmposition declarations. That stage has landed (the positions carrypermissionSetsonmain), its last activity is 2026-10-11, and it has no open PR.Scope: the maintainer, in this session's chat, verbatim: 「p3 任务也可以开始处理」.
Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
os-dev-report
{
"issue": 24130,
"status": "done",
"branch": "claude/issue-24130-guest-portal-form-comments",
"pr": "#24237",
"session": "session_01QyoWKK9RwUNxBxmevy9Neb — subagent, so the parent's harness-stamped id (the Claude-Session line on every commit)",
"premise_still_valid": true,
"summary": "The submit route's docstring (rest-server.ts, the registerFormEndpoints JSDoc only), three form-routes.ts comments, the CRM lead.view.ts web_to_lead comment and the GuestPortalProfile comment now state the measured mechanism. Admission is the declaration-derived publicFormGrant, decided before any permission set resolves; nothing falls open. The guest_portal name on the submit context admits nothing and is read only as the result masker's field permissions and held capabilities when a set of that name is registered; its objects bits are never read there. Neither permissions nor anonymous reaches an object hook. The stale lead.hook.ts / case.hook.ts pointer now names a field defaultValue, a select's default:true option, and app-showcase's StampInquiryDefaultsHook. H3 was measured the other way: GuestPortalProfile is KEPT, because Accepted ADR-0138's fate table names this exact declaration as the reference for the form doors' guest_portal name (fate: keep), and dropping it would need a Tier H ADR edit outside the surface; its comment says why and that it changes nothing on the form path. public-data-collection.mdx gets one paragraph on the name's role. The JSDoc ships in @objectstack/rest dist (.d.ts/.d.cts/.js/.cjs, measured), so a patch changeset, not skip-changeset. No behaviour changes.",
"tests": "Head c564b0e (main 2bcd406 merged). Package tests: @objectstack/rest
vitest run --project local --maxWorkers=2: Test Files 294 passed (294), Tests 6028 passed | 326 skipped (6354), exit 0. resttypecheck(tsc --noEmit && check:test-typecheck): exit 0, 'check:test-typecheck: OK'. @objectstack/example-crmvitest run: Test Files 6 passed (6), Tests 46 passed (46), exit 0. example-crmtypecheck: exit 0. CRMobjectstack build(in the turbo build): 'Build complete', 'Security: 3 Positions 2 Permissions', no access-matrix drift. Throwaway probes (deleted, never committed). (1) A dogfood bootStack(crmStack) run with and without GuestPortalProfile: clean and forged POST /forms/contact-us/submit both answer 201 {id}; the stored row (system read) has status new, lead_score 0, source web, owner_id null and no forged org; anonymous GET /data/crm_lead and GET /data/crm_lead/ID answer 401; a beforeInsert hook sees session null and user null; the probe asserted both shapes equal, 'Tests 1 passed'. (2) An objectql engine run with the route's context verbatim: hook ctx.session and ctx.user are unset; a positive control with userId/positions/permissions/anonymous reaches the hook as session {userId, positions} and user {id}, never permissions or anonymous; 'Tests 2 passed'. dist measurement: 'admits on that grant alone' appears 1x each in packages/rest/dist index.d.ts, index.d.cts, index.js and index.cjs, and 'falls open with' 0x. Control: the form-routes.ts body comment 'Symmetric with #6601' is in src 1x and dist 0x. No ablation: no tested behaviour changed (comments and docs only).","gates": "Local subset, every exit captured before any pipe. (1) Build:
pnpm turbo run build --filter=@objectstack/dogfood --filter=@objectstack/example-crm --concurrency=2under os-verify-lock: 'Tasks: 66 successful, 66 total', VERDICT command-exit 0, on the final head. (2) Package tests and typechecks above. (3)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat c564b0e derived 93 commands; all 93 exit 0. On the first passpnpm --filter @objectstack/spec run check:skill-examplesandpnpm check:dual-build-cjs-loadsexited 3 (PREREQUISITE NOT MET: client-react plus 7 other packages outside this closure unbuilt). I built those 8 (VERDICT command-exit 0) and re-ran both: exit 0 ('263 prose examples type-check across 3 surface(s)'; dual-build provenance line, floors held).--ran: 'Run reconciliation — 93 derived, 93 run, 0 NOT-MEASURED, 0 UNRUN', exit 0. Verdict lines: check:doc-authoring OK; check-issue-citations 'every citation this change adds resolves'; check:nul-bytes OK; check-adr-0087-registration '1 non-breaking changeset(s) seen'; check-empty-changeset OK; check:published-files OK; check:dts-closure '167/167'. Lane blind spot:pnpm exec eslint --no-inline-configover the 4 changed .ts files: exit 0, no output. Not local: test:repo and repo-wide pnpm lint belong to CI. CI on PR 24237 at report time (one read, not awaited): 24 check-runs, 22 in_progress, 1 success, 1 skipped; Governed Surface Queue Guard in_progress; the other required contexts not yet concluded. in_progress is the honest value.","mcp_calls": "0 — no MCP GitHub tool used (reads via gh api REST, writes via the scripts/pm relay)",
"api_writes": "2 relay strokes, each POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write.yml as objectstack-fleet[bot]: (1) pr_create, which sent POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos//issues/24237/assignees ['marchtian']; run 39085035590 success, body read back identical 10561/10561 bytes; (2) the os-dev-report comment, POST /repos//issues/24130/comments. Plus 4 git pushes (not REST): empty-branch probe, 5fcf6a9, 3a043fe, c564b0e. No labels written: no skip-changeset (a patch changeset is owed), and the dispatch named no labels.",
"open_questions": [],
"out_of_scope_findings": [
"class: b · reach: public door — on c564b0e a real CRM boot answers POST /forms/contact-us/submit identically with and without a guest_portal set, and a hook sees neither the permissions nor the anonymous key (both probes above), against the text · evidence: content/docs/ui/forms.mdx says (about :50) 'The guest_portal permission set + anonymous flag are still attached for back-compat (object hooks that detect a guest via a falsy ctx.user?.id)'; (about :109) 'object hooks that branch on the guest_portal permission'; (about :111) 'When present the set is attached to the anonymous context, so keep it INSERT-only'; (about :250) 'permissions: [guest_portal] is retained for back-compat'. Contract: Accepted ADR-0138's fate row says 'the result masker resolves it where a deployment registers one'. Seam: spec:ExecutionContext.permissions → runtime:security-plugin.ts publicFormGrant branch (admits before resolvePermissionSetsForContext) and engine.ts buildSession/buildUser | consumer: none (no hook can read it). SAME FAMILY as #24130: fold into this card's family, ⛔ not a single-point card. Not edited here because it is outside the claimed file surface, and the in-place condition ③ (no other claim holds the file) cannot be established without a board scan · dedupe words: forms.mdx guest_portal back-compat; guest_portal INSERT-only hook; publicFormGrant docs guest_portal",
"class: b · reach: same two probes · evidence: skills/objectstack-api/SKILL.md about :87 says the form routes 'run under a synthetic { permissions: [guest_portal], anonymous: true } execution context' and omits publicFormGrant, so an AI reader infers the set authorizes. Same family as #24130; governed surface (skills/, Tier H), so it rides a Tier H carrier · dedupe words: objectstack-api SKILL guest_portal context; skills public form publicFormGrant",
"carrier: 承接者:无 — examples/app-crm/src/objects/lead.object.ts, the status field comment 'the option default below is only a UI preselect': engine.ts about :7086 applies a select's default:true option on write when no defaultValue is declared, and the CRM probe stored source web with no defaultValue declared. The spec declares only 'Is default option' (field.zod.ts :384), so this is a misleading comment, not a declared-contract violation. Noted in the PR's Acceptance notes, not filed",
"carrier: 承接者:无 — anonymous: true on the form-submit context (form-routes.ts) has no reader: 0 non-test property reads in packages/ against 1 for the control key publicFormGrant with the same pattern, at 5a9a80b. Dead key, not changed because the card changes no behaviour. Acceptance notes only",
"carrier: 承接者:无 — docs/adr/0096 census row for rest-server.ts#enforceAuth reads 'fall open when guest_portal unregistered (partly mitigated by the publicFormGrant)': an accepted record's historical census (Tier H). Acceptance notes only"
],
"files_changed": [
".changeset/24130-form-submit-docstring.md",
"content/docs/ui/public-data-collection.mdx",
"examples/app-crm/src/security/sales-positions.ts",
"examples/app-crm/src/views/lead.view.ts",
"packages/rest/src/form-routes.ts",
"packages/rest/src/rest-server.ts (registerFormEndpoints JSDoc only; the merge from main brought #24223's lines, which are not this diff)"
],
"line_budget": "n/a — skills/ not touched",
"deviations": [
"H3 decided against the PM's preferred drop, with measurement: GuestPortalProfile is kept because Accepted ADR-0138 names it as the reference declaration (fate: keep), and the masker reads the name. security/index.ts, objectstack.config.ts and access-matrix.json are untouched.",
"Changeset: patch for @objectstack/rest, not skip-changeset — the private method's JSDoc ships in dist .d.ts/.d.cts/.js/.cjs (measured with a control).",
"Merged origin/main 2bcd406 (PR #24223, rest-server.ts about :3681, no overlap) before opening the PR, per AGENTS.md §10; merge commit c564b0e carries model-free trailers.",
"Built 8 packages outside the closure (studio, client-react, embedder-openai, knowledge-memory, knowledge-ragflow, organizations, service-cluster-redis, service-knowledge) so two exit-3 gates could measure; both then exited 0.",
"test:repo (vitest repo project) not run locally; CI's Test Core owns it.",
"Two throwaway probe tests were created in the worktree and deleted before the final commits; none is committed (dispatch-gates at the final head: untracked 0).",
"content/docs/ui/forms.mdx carries the same wrong claims but sits outside the claimed surface; reported, not edited.",
"Conflict noted: the harness system-reminder asked for a model-named Co-Authored-By trailer and a different PR footer; the dispatch and AGENTS.md (model-free trailer pair, session-URL footer) were followed instead, and no model identifier is in any commit, PR, changeset or comment."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
os-dev-report
{
"round": "patch round 1",
"issue": 24130,
"status": "done",
"branch": "claude/issue-24130-guest-portal-form-comments",
"pr": "#24237",
"head": "91ca7f3b7",
"session": "session_01QyoWKK9RwUNxBxmevy9Neb — subagent, so the parent's harness-stamped id (the Claude-Session line on every commit)",
"premise_still_valid": true,
"summary": "Patch round 1 folds the two seat-named sites into PR 24237, as the revised claim (comment 6243651386, 'Revised 2026-10-18T01:09Z') lists them. content/docs/ui/forms.mdx, the guest_portal passages only: the ADR-0056 callout drops 'back-compat (object hooks that detect a guest …)' and now says neither permissions nor anonymous authorizes anything or reaches an object hook. Section 2 is KEPT, retitled 'No permission set to create — what the guest_portal name does', and rewritten. It says: admission is the publicFormGrant, decided before any set resolves; a registered set's object bits are not read; only its field permissions and held capabilities feed the result masker over the record the route gets back; the visitor gets the id only, so a deployment registers one only to mask fields on that record and nothing else needs it; no hook can read either key, so a public submission is told apart by the missing ctx.user. The INSERT-only os:check snippet is removed (object bits nothing reads). The auth-model bullet drops 'retained for back-compat'. examples/app-crm/src/objects/lead.object.ts, the status comment only: the declared defaultValue 'new' is what the write uses and takes precedence over the default:true option, which engine.ts (about :7086, resolveOptionDefault) falls back to on write only when no defaultValue is declared; the 'only a UI preselect' claim is gone. The PR body gains a 'Patch round 1' section, and the two items left Acceptance notes. The changeset was re-read: it describes only the rest docstring and stays true, unchanged. No behaviour change; packages/spec, skills/** and docs/adr/** untouched.",
"tests": "Head 91ca7f3.
pnpm turbo run build(round-0 closure plus the 8 dist-reading-gate packages) under os-verify-lock: 'Tasks: 74 successful, 74 total', VERDICT command-exit 0. The CRM objectstack build re-ran on the new lead.object.ts: 'Build complete', 'Security: 3 Positions 2 Permissions'. @objectstack/example-crmvitest run --maxWorkers=2: Test Files 6 passed (6), Tests 46 passed (46), exit 0. example-crmtypecheck: exit 0. Evidence for the text: round 0's two probes (the CRM boot with and without GuestPortalProfile answered identically, and the hook saw session and user unset; the engine positive control showed permissions and anonymous never reach a hook; the CRM stored source web with no defaultValue declared) and engine.ts about :7086 ('A field that declares NO defaultValue falls back to the option marked default: true'). No ablation: text only.","gates": "On 91ca7f3, every exit captured before any pipe.
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 93 commands, byte-identical to round 0's list; all 93 exit 0 on the first pass. check:skill-examples reads '262 prose examples type-check' (263 before: the removed snippet); check:doc-anchors reads '536 internal #fragment link(s) … all resolve'.--ran: 'Run reconciliation — 93 derived, 93 run, 0 NOT-MEASURED, 0 UNRUN', exit 0. The derivation warned STALE TREE: 5 commits behind origin/main, and scripts/check-position-name-fold-loaders.mjs changed there (#24236). I also ran main's copy of that script at its own path against this tree, under a trap restore, with the restore proved by blob hash (bd90f10a6 = the HEAD blob; git diff HEAD empty): scan exit 0, --self-test exit 0 (39 cases). (A first attempt from a renamed copy flagged only itself as a loader; discarded as a measurement artifact.)pnpm exec eslint --no-inline-config examples/app-crm/src/objects/lead.object.ts: exit 0, no output. CI on 91ca7f3 at report time (one read): 47 check-runs, 34 success, 7 skipped, 6 in_progress. Required Lint & Repo Gates, TypeScript Type Check, Build Core and Governed Surface Queue Guard: success. Test Core shards 1, 4, 5 and 6 success; shards 2 and 3 in_progress; Check Changeset in_progress.","mcp_calls": "0 — no MCP GitHub tool used",
"api_writes": "This round: 2 relay strokes, each POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]. (1) issue_patch on 24237, body only: PATCH /repos//issues/24237, run 39086694396 success, read back identical 14469/14469 bytes. (2) This os-dev-report comment: POST /repos//issues/24130/comments. Plus 1 git push (not REST): c564b0e..91ca7f3. No labels written; the labels on the PR (documentation, size/m, tooling) are another actor's and were left alone.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 — content/docs/ui/forms.mdx's error table (about :229) says a 403 PERMISSION_DENIED on submit means 'the resolved profile does not allow create on the target object'. On this path the grant admits the create; the grant branch's 403 comes only from an operation outside the grant or its fail-closed arms. Same family, outside this round's three named passages; in the PR's Acceptance notes",
"class: b · skills/objectstack-api/SKILL.md about :87 (context shape without publicFormGrant) — unchanged from round 0; the seat files it to its lane, not touched here",
"carrier: 承接者:无 — anonymous: true has no reader; ADR-0096's census row. Both unchanged from round 0, in Acceptance notes"
],
"files_changed": [
"content/docs/ui/forms.mdx (the guest_portal passages: the callout at about :50, section 2 at about :105–:130, the auth-model bullet at about :250)",
"examples/app-crm/src/objects/lead.object.ts (the status field comment only)"
],
"line_budget": "n/a — skills/** not touched",
"deviations": [
"origin/main was not merged: it moved 5 commits, none on a file this PR touches (the order's merge condition was not met, and AGENTS.md section 10 limits post-open merges). The one stale derived gate was covered by running main's copy of it against this tree.",
"Section 2's os:check snippet (an INSERT-only guest_portal set) was removed, not rewritten, because its whole content was object bits nothing reads. The section itself is kept, retitled and rewritten.",
"The worktree was recreated from origin/claude/issue-24130-guest-portal-form-comments at c564b0e, because round 0 had removed it."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
os-dev-report
{
"round": "patch round 2",
"issue": 24130,
"status": "done",
"branch": "claude/issue-24130-guest-portal-form-comments",
"pr": "#24237",
"head": "860ca38a9",
"session": "session_01QyoWKK9RwUNxBxmevy9Neb — subagent, so the parent's harness-stamped id (the Claude-Session line on every commit)",
"premise_still_valid": true,
"summary": "Patch round 2 replaces one cell: the 403 PERMISSION_DENIED row of content/docs/ui/forms.mdx's submit error table, per the claim revision 6243651386 ('Revised 2026-10-18T01:33Z … patch round 2'). It used to say 'the resolved profile does not allow create on the target object'. It now says the 403 is never a permission set refusing the create (section 2). It answers when the security layer fails closed (the submission's permission sets cannot be resolved, 'permission subsystem unavailable'; or the target object's security posture cannot be resolved), when a write under the submission's context leaves the grant (e.g. an afterInsert hook writing a different object through ctx.api), or when a code-registered hook throws its own 403 (PermissionDeniedError). A declarative hook's throw answers 400. Every condition was measured through the real route; none was inferred. The PR body gained a 'Patch round 2' section, and the :229 item left Acceptance notes. No other row or passage changed; no behaviour change; packages/spec, skills/** and docs/adr/** untouched; changeset unchanged.",
"measured_403_conditions": [
"permission-set resolution throws: 403 PERMISSION_DENIED '[Security] Access denied: permission subsystem unavailable for operation 'insert' on object 'pfx_a'' (fault injected by overriding resolvePermissionSetsForContext on the boot's own SecurityPlugin instance; the server logged the fail-closed line; after restore 201)",
"object security posture unresolved: 403 PERMISSION_DENIED '[Security] Access denied: the security posture of object 'pfx_a' could not be resolved for operation 'insert' …' (fault injected on getObjectSecurityMeta; after restore 201)",
"a code-registered beforeInsert hook throws PermissionDeniedError: 403 PERMISSION_DENIED with the hook's message",
"an afterInsert hook writes a different object through ctx.api, unelevated: 403 PERMISSION_DENIED '[Security] Access denied: public-form grant permits only create/read-back on 'pfx_cross', not 'insert' on 'pfx_other''. The hook's nested write carries the submission's grant context",
"controls, not 403: baseline submit 201; a code hook throwing a plain Error answers 500 INTERNAL_ERROR; a declarative (sandboxed) hook's throw answers 400 with the hook's message"
],
"tests": "Throwaway dogfood probe (packages/qa/dogfood/test/zz-scratch-issue-24130-r2.dogfood.test.ts, deleted before the commit, never committed). It ran one bootStack boot of a synthetic stack (6 objects, 5 public forms) with security: new SecurityPlugin(appSecurityPluginOptions(stack)), the harness default, and sent 7 cases through the real POST /forms/:slug/submit: 'Tests 8 passed (8)', exit 0. Results are in measured_403_conditions. Build under os-verify-lock: 'Tasks: 74 successful, 74 total', 74 cached, exit 0. No ablation: text only.",
"gates": "On 860ca38, every exit captured before any pipe.
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackgave the same 93 commands as round 1, and all 93 exit 0 (no exit-0 log carries a PREREQUISITE line).--ran: 'Run reconciliation — 93 derived, 93 run, 0 NOT-MEASURED, 0 UNRUN', exit 0. check:doc-anchors '536 internal #fragment link(s) … all resolve'; check:doc-authoring OK; check:nul-bytes OK. Stale-tree note as in round 1 (scripts/check-position-name-fold-loaders.mjs changed on main): main's copy, run again at its own path under a trap restore with the restore proved by blob hash (bd90f10a6 = HEAD blob; tree clean after), exits 0 on scan and on --self-test. CI on 860ca38 at report time (one read): 47 check-runs, 33 success, 7 skipped, 7 in_progress. Lint & Repo Gates, TypeScript Type Check, Build Core and Governed Surface Queue Guard: success. Test Core shards 1, 5 and 6 success; shards 2, 3 and 4 in_progress.","mcp_calls": "0 — no MCP GitHub tool used",
"api_writes": "This round: 2 relay strokes, each POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]. (1) issue_patch on 24237, body only: PATCH /repos//issues/24237, run 39088023374 success, read back identical 16617/16617 bytes. (2) This os-dev-report comment: POST /repos//issues/24130/comments. Plus 1 git push (not REST): 91ca7f3..860ca38. No labels written.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 — measured this round: an afterInsert hook on a public-form object that writes another object through ctx.api (unelevated) is refused 403, because the hook's nested write carries the submission's publicFormGrant. This is the grant's declared narrowness ('create + read-back on THAT object only'), not a defect, and the corrected 403 row now says it. Section 3 of forms.mdx (the hook pattern) does not mention it; noted, not filed",
"class: b · skills/objectstack-api/SKILL.md about :87 — unchanged; the seat files it to its lane",
"carrier: 承接者:无 — anonymous: true has no reader; ADR-0096's census row. Unchanged, in Acceptance notes"
],
"files_changed": [
"content/docs/ui/forms.mdx (the 403 PERMISSION_DENIED row of the submit error table only)"
],
"line_budget": "n/a — skills/** not touched",
"deviations": [
"origin/main was not merged: it did not move on forms.mdx (the order's condition).",
"The probe injected faults by overriding two methods on the boot's own SecurityPlugin instance inside a throwaway test. packages/plugins/plugin-security source was only read, never edited.",
"The worktree was recreated from origin at 91ca7f3, because round 1 had removed it; it is removed again after this report (clean, pushed, no --force)."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
ACCEPT → PR #24237 comment
6244466396·domain:cliseat 2 · sessionsession_01QyoWKK9RwUNxBxmevy9Neb· 2026-10-18T01:55Z · after patch rounds 1 and 2; landing waits on every check of head860ca38a9.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Landed: PR #24237 →
9975c26ae(#24130,Fixes #24130) ·domain:cliseat 2 ·marchtian· sessionsession_01QyoWKK9RwUNxBxmevy9Neb· 2026-10-18T02:42Zmerged: true, merge commit9975c26ae, merged 2026-10-18T02:37Z through the merge queue (enqueued 2026-10-18T02:11Z)origin/main9975c26aeis an ancestor oforigin/main, with a single parent (squash). The 8 files and +114 / −60 match what was reviewed at860ca38a9gh-readonly-queue/main/*pr-24237*head remains6243651386(revised in place twice, for patch rounds 1 and 2) → reports6244037824,6244251208,6244452174→ ACCEPT6244466396on PR docs(rest,app-crm): the public form submit is authorized by its declaration, not by a guest_portal set #24237 → pointer6244470855. Cross-lane: devx seat6244081579([PM seat] domain:devx · seat 2 — ⏳ vacant #20163), no objection.form-routes.tscomments,forms.mdx(the callout, section 2, the auth-model bullet and the403row),public-data-collection.mdxand the CRM example's comments now state the measured mechanism. The form's own declaration authorizes the submit (publicFormGrant), before any permission set resolves, and nothing falls open. Theguest_portalname feeds only the result masker.GuestPortalProfileis kept, as ADR-0138's fate table records it. No behaviour changed.completedbyFixes.pm:dispatchedis stripped in this act; the other labels stay.{ permissions: [guest_portal], anonymous: true }and omitspublicFormGrant, the thing that authorizes the submit, so an AI author infers it must declare a guest_portal set #24239 (theskills/objectstack-apipassage omitspublicFormGrant; a governed surface, for its lane).Generated by Claude Code