You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
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
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/cli17.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-securitysecurity-plugin.ts:2334-2336 (at the tag)
Both agree with the docs: the publicFormGrant works withoutguest_portal.
examples/app-crm/src/views/lead.view.ts:96-104 (tag; :95-106 on origin/main570c7a2)
"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 GuestPortalProfile or says why it keeps it.
The stale hook-file pointer is corrected.
If guest_portal still has a real role on this path, for example the permissions: ['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).
Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
Still on the 18 line: measured on 17.7.0, which ships no further release; re-read on origin/main462d02f00a. The comments are still there: packages/rest/src/form-routes.ts:407, :433 (the lead.hook.ts / case.hook.ts pointer) and :507, packages/rest/src/rest-server.ts:9090–:9095 ("falls open"), and examples/app-crm/src/views/lead.view.ts:102.
Why p3: comments and docs only; the runtime is right. The "falls open" sentence is the misleading part an AI author reads, and it goes in the same change.
Why domain:cli:packages/rest, and the CRM example exercises that door.
Claim: PM loop round 2 · 2026-10-18T00:19Z
Session: session_01QyoWKK9RwUNxBxmevy9Neb
Account: marchtian (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-24130-guest-portal-form-comments
Worktree: objectstack-issue-24130
Domain: domain:cli
Seat: 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 above registerFormEndpoints ONLY (about :8250–:8275, the "falls open" sentence), comment-only.
packages/rest/src/form-routes.ts: the comments at about :411, :437 (the stale lead.hook.ts / case.hook.ts pointer) and :527. Comment-only unless measurement shows permissions: ['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) and examples/app-crm/src/security/sales-positions.ts (GuestPortalProfile, about :63–:73), with security/index.ts and the CRM access matrix only if the profile is dropped.
content/docs/ui/public-data-collection.mdx, only if guest_portal keeps a real role on this path (domain:devx, declared if touched).
content/docs/ui/forms.mdx: the guest_portal passages only (about :50, :105–:125, :250 on 7715d049e). The page says the set and the anonymous flag are attached "for back-compat" with object hooks, and that a set must be kept INSERT-only. The dev's probes on c564b0e45 measured 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 — 🟢 os-sales · session_016PVjjuVPte5XEJvaQx1E4t · serial (maintainer) #20163.
In-place fix of adjacent defects, by the four conditions: the same defect class (text that misstates what authorizes or fills the public submit); mechanical (text only, no behaviour); unclaimed (no open PR's file list and no pm:dispatched claim names either file, read in this act); and the same gate family as this card's own docs and example edits.
Revised 2026-10-18T01:33Z (seat review of PR docs(rest,app-crm): the public form submit is authorized by its declaration, not by a guest_portal set #24237 at 91ca7f3b7, patch round 2):content/docs/ui/forms.mdx's error-table row 403 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 answers 403 for, as measured. The four conditions hold as for round 1.
A changeset or skip-changeset, by what a reader of the published packages sees.
every packages/spec path.
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.ts comments in packages/rest, and the CRM example's comments, say the guest_portal profile authorizes an anonymous form submit and that a deployment without it "falls open" | none: the runtime's publicFormGrant already 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 the repo:www.objectos.ai seat on 17.7.0 (examples/app-crm without GuestPortalProfile: the same 201 / 401 answers)
Thread-read: 6229356773
Serial constraints cleared:
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. rest typecheck (tsc --noEmit && check:test-typecheck): exit 0, 'check:test-typecheck: OK'. @objectstack/example-crm vitest run: Test Files 6 passed (6), Tests 46 passed (46), exit 0. example-crm typecheck: exit 0. CRM objectstack 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=2 under 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/objectstack at c564b0e derived 93 commands; all 93 exit 0. On the first pass pnpm --filter @objectstack/spec run check:skill-examples and pnpm check:dual-build-cjs-loads exited 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-config over 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."
]
}
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-crm vitest run --maxWorkers=2: Test Files 6 passed (6), Tests 46 passed (46), exit 0. example-crm typecheck: 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/objectstack derived 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."
]
}
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/objectstack gave 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)."
]
}
ACCEPT → PR #24237 comment 6244466396 · domain:cli seat 2 · session session_01QyoWKK9RwUNxBxmevy9Neb · 2026-10-18T01:55Z · after patch rounds 1 and 2; landing waits on every check of head 860ca38a9.
What landed: the submit route's docstring, the form-routes.ts comments, forms.mdx (the callout, section 2, the auth-model bullet and the 403 row), public-data-collection.mdx and 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. The guest_portal name feeds only the result masker. GuestPortalProfile is kept, as ADR-0138's fate table records it. No behaviour changed.
State change: the card was closed completed by Fixes. pm:dispatched is stripped in this act; the other labels stay.
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 — 🟢 os-sales · session_016PVjjuVPte5XEJvaQx1E4t · serial (maintainer) #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 — 🟢 os-sales · session_016PVjjuVPte5XEJvaQx1E4t · serial (maintainer) #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