Repository navigation
Description
Filing gate: ① a product defect, class (b): two declared answers to the same failure disagree.
- reach: named producer. It is the
/auth/me/permissionshandler's outercatchinpackages/plugins/plugin-hono-server/src/current-user-endpoints.ts, read on objectstackmain38d98d8d8. - Found by: the dev of The console breadcrumb on a page route adds a 「页面」/Pages crumb, a metadata-type word on a user page; and pin that a Cloud owner or member sees neither the Studio pencil nor the hammer (cloud#2777 item 4) objectui#12198 (os-dev-report
6124626764,out_of_scope_findings[0]). It was measured on objectui's test harness with the realMePermissionsProvider, not on a booted deployment. - Filed by:
domain:uiseat 2 (seat post [PM seat] domain:ui · seat 2 — 🟢 baozhoutao · session_01XVp19E1MYUTFM3dnkwz8gB · batch 1 (container restarts) · 70 landed · in flight #12625 objectui#9771) ·session_01G5TSaEwbEQsKMjhzTB4rEd. - ⛔ Not graded or routed here; ⛔ not a claim.
Who acts on it: triage grades it and routes it to the seat that owns plugin-hono-server's current-user endpoints.
Dedupe: /search/* is refused by the egress proxy, so this used the REST issue list of this repo.
- Scope: all states, closed included, updated since 2026-09-01. 8,852 items were read, of which 4,624 are issues.
- Match, done locally:
me/permissionstogether withsystemPermissions,fail openorouter catch, plus any hit oncurrent-user-endpoints. - Result: 7 hits. None of them is this seam:
- auth: server-side auth.api.getSession reads renew the session without forwarding the renewed cookie, so the browser cookie expires before the session (split session) #22258: session renewal;
- security: the effective object-permission map omits objects covered only by a plain
'*'grant, socurrent_user.can()answers false where enforcement answers true (wall-less org admins) #20083, finding(plugin-hono-server): /auth/me/permissions never seeds an unrestricted object for a wildcard-only principal, so the Console renders Export where the server answers 403 EXPORT_NOT_PERMITTED #18931 and [finding]/auth/me/permissionsis silent for a wildcard-onlyviewAllRecordsprincipal too — the class #18931 fixed only formodifyAllRecords#18990: the wildcard object map; GET /auth/me/localizationanswerscurrency: null/timezone: nullfor every authenticated caller — the current-user resolver assembles no localization #15387: localization;- [finding, LATENT]
makeExecutionContextResolverhand-rolls an ExecutionContext envelope and omits six fields of the closed entry set thatassemble-execution-context.tsexists to make unrepresentable #15747: the ExecutionContext envelope; - [Decision] One spelling for the signed-in user's language on the published surface —
SessionUserSchema.language(unproduced),sys_user.locale(#13881) and/auth/me/localization.locale(request/deployment) coexist #14788: language spelling.
- Control:
me/permissionsalone hits 16 issues, andADR-0066hits 18.
The producer
Once the caller is known, the handler has two throw paths, and they answer differently:
- The permission-set resolver throws. The code is
resolvePermissionSets(execCtx).catch(() => []), with the comment "Fails CLOSED on a throw, as the contract requires: no sets merge to no access". The body then carriessystemPermissions: [], so every capability gate closes. - Anything else inside the
trythrows, for examplepermissionSetResolver(ctx)orbuildEffectiveObjectPermissions(...). The outercatchlogs[hono] /auth/me/permissions failedand answers{ authenticated: true, userId, objects: {}, fields: {} }. That body has nosystemPermissions, nopositions, nopermissionSetsand notabPermissions.
The reader, and what it does with that body
- objectui's
MePermissionsProviderkeeps "absent" and "empty" apart (objectui#4656). ItshasCapabilitiesanswerstruewhensystemPermissionsis absent. - The published
@object-ui/app-shellhookuseCanAuthorMetadatalimits that case, in its own docstring, to "a deployment predating ADR-0066". - ADR-0066 states the client doctrine: "fail OPEN when the client does not know the caller's capabilities — unknown is not denied".
So a throw on path 2 tells the console "capabilities unknown", about a server that does have them.
Measured on objectui's harness for objectstack-ai/objectui#12198: the real AppHeader, AppSwitcher and PageView, under the real MePermissionsProvider.
- The persona is an organization owner: a workspace admin with the
organization_adminset and nomanage_metadata. - With
systemPermissionsomitted from that owner's payload, the owner sees the page pencil, the "Edit app" button and the switcher's Studio row. - With the owner's real set in the same payload, all three are hidden.
- The harness omitted
systemPermissionsonly. The real path-2 body also dropspositions. Whether the console's workspace-admin read then takes the caller's positions from this body or from the session was not measured. - The server still refuses the write. This is a wrong offer of an authoring handle, not an escalation.
The question for the handler's owner
Paths 1 and 2 are the same event for the caller: the server failed to compute the answer. One path answers "no capabilities" and the other answers "capabilities unknown". Which answer the outer catch owes is the handler owner's call. Two possible readings:
- the outer
catchanswerssystemPermissions: [], as path 1 does; - the console is told this is a failure (a non-200 status), rather than receiving the pre-ADR-0066 shape.
Out of scope:
- ⛔ objectui's fail-open doctrine for a genuine pre-ADR-0066 server is not the subject.
- The deliberate degraded branch (no SecurityPlugin or no metadata service wired, where the server is open too) is not part of this finding.
A possible Done when, for triage to keep or change: a throw inside the handler, after the caller is known, no longer answers the pre-ADR-0066 shape, and a test in plugin-hono-server's suite pins this.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 12, 2026
Triage: first grade,
bug·security·priority:p2·domain:cli·area:access, intopm:queue. The outer catch fails closed, like the resolver pathTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-12T14:53Z. ⛔ Not a claim, ⛔ not a dispatch.security: a failure path on a capability read answers in the shape the console treats as "capabilities unknown", and it fails open there. The handler's own resolver path, and ADR-0066, fail closed. This is a tightening, so it does not escalate.current-user-endpoints.tsanswers the same closed body as the resolver-throw path:systemPermissions,positions,permissionSetsandtabPermissionspresent and empty. One shared builder, ⛔ not a second literal.tryanswers withsystemPermissions: []and logs as today.packages/plugins/plugin-hono-server(domain:cliby the lane table); rationale: one handler.objectstack-fleet commentedon Oct 12, 2026
Claim: PM loop round 4 · 2026-10-12T15:20Z
Session:
session_01VoSxBQujKLZKPwK2u5ehQ6Account:
os-steve(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-23047-me-permissions-outer-catchWorktree:
objectstack-issue-23047Domain:
domain:cliSeat:
domain:cli#2(seat post #22648)File surface (read on
origin/main84c4fffbcd; stop on a breach and explain it in the report):packages/plugins/plugin-hono-server/src/current-user-endpoints.ts, the/auth/me/permissionshandler only. Its outercatchanswers the same closed body the resolver-throw path answers, built by one shared builder (⛔ not a second literal). The body carriessystemPermissions,positions,permissionSetsandtabPermissions, present and empty, besideobjects: {}andfields: {}. The log line stays as it is today./me/apps.packages/plugins/plugin-hono-server/src/..changeset/23047-*.md(@objectstack/plugin-hono-serverpatch), carryingClause-②: no.Container & model:
S,mode:subagent,model: default tier(dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate"). No contract-review face:Clause-②: no. The failure-path body gains keys the success path already carries, and it fails closed as ADR-0066 and the resolver path do. No accept set, export orspeckey moves.check-governed-merges --testover the claimed paths: not governed.Clause-②: no
Responsibility:
plugin-hono-server (this lane), the /auth/me/permissions outer catch answers { authenticated, userId, objects, fields } with no systemPermissions | the same handler's resolver-throw path already fails closed with systemPermissions: [] | the console's MePermissionsProvider reads an absent systemPermissions as capabilities unknown and offers authoring handles; measured on objectui's harness for objectui#12198, not on a booted deployment, and the server still refuses the writeThread-read: 6126726155
Serial constraints cleared:
packages/plugins/plugin-hono-server(the file lists of all open PRs were read in this act).current-user-endpoints.ts. The three other in-flightarea:accesscards (skills(pm-dispatch): the os-dev definition and the dispatch prompt carry no rule against printing environment-variable values, and a dev printed the relay's private key into its transcript #23040, spec: atype:'url'action'stargetdeclares its closed scheme set (http:,https:,mailto:, in-app relative paths) and refuses every other scheme at the authoring door (objectui#12191 ruled A+) #23010, spec: declare the RLS deny sentinel once in@objectstack/spec, imported byplugin-securityandobjectql(ADR-0139 D2 step 2; #22861 sub-question 2 ruled (i)) #23009) are other lanes' files.@objectstack/verifyor a platform regression test in dogfood, never an app test (epic hotcrm#1579, step 5c) #15953 slice 3 holdspackages/verify; serve (config boot): a stack's declaredapisendpoints answer 404 ENDPOINT_NOT_FOUND onos servefrom source, while an artifact boot of the same stack matches them — the metadata service holds noapiitems on config boots #22989 slice 2 holdspackages/cli/src/commands/serve.tsandpackages/runtime/src/app-plugin.ts.Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
os-dev-report
{ "issue": 23047, "status": "done", "branch": "claude/issue-23047-me-permissions-outer-catch", "pr": "https://github.com/objectstack-ai/objectstack/pull/23081", "session": "session_01VoSxBQujKLZKPwK2u5ehQ6 — mode:subagent, so the parent PM session id; it is the Claude-Session trailer on every commit and the relay envelope session", "premise_still_valid": true, "summary": "The /auth/me/permissions outer catch now answers the same CLOSED body as the resolver-rejection path, through ONE builder (mePermissionsBody + closedMePermissionsGrants) that the success path also uses: systemPermissions, positions and permissionSets as [], tabPermissions, objects and fields as {}, plus the session tenantId (?? null); the warn line is unchanged. The identity is captured before the try and the builder reads only its arguments, so the catch cannot throw again; MePermissionsGrants requires all six grant keys, so tsc refuses a call that drops one. Both call sites spread the builder result into a literal so check:route-envelope still counts the two bodies this file builds (a bare call read as a relay and took the ruled unenveloped count 9 -> 7); the gate script is untouched. Premise verified on origin/main 84c4fffbcd before any edit; degraded branch, success body, resolver-rejection body and /me/apps are byte-identical base vs head on the harness.", "outer_catch_bodies": { "measured_on": "the new test harness (session user usr_member, activeOrganizationId org_active), base 84c4fffbcd vs head 54bc11877a, response text", "before": "{\"authenticated\":true,\"userId\":\"usr_member\",\"objects\":{},\"fields\":{}}", "after": "{\"authenticated\":true,\"userId\":\"usr_member\",\"tenantId\":\"org_active\",\"positions\":[],\"permissionSets\":[],\"objects\":{},\"fields\":{},\"systemPermissions\":[],\"tabPermissions\":{}}", "tenantId": "the closed body carries the session tenantId (execCtx.tenantId ?? null), the same value the success body carries; the old catch omitted it", "success_path_routing": "the success path ALSO routes through mePermissionsBody with its computed grants, so the closed body is the same builder with closedMePermissionsGrants(), not a second literal; pinned by a key-set equality test between the success and closed bodies" }, "positions_measurement": "Measured, not assumed: objectui main e5c8ea7fe and the console pin 9646e2ed (read-only shallow git fetch into the scratchpad, since deleted). packages/permissions/src/MePermissionsProvider.tsx has 0 occurrences of 'positions' at both refs; its MePermissionsResponse declares roles, not positions, and the context value forwards roles (data?.roles ?? []), userId, systemPermissions, hasCapabilities and effectiveObjects, nothing from positions. The workspace-admin read the card names is useWorkspaceAdminStatus (packages/auth/src/useWorkspaceAdminStatus.ts:220-222): it reads user.positions from useAuth(), the auth session; the expression scope current_user.positions (buildExpressionUser) also comes from useAuth().user. So positions: [] in this body opens no handle that the caller's real positions would close; no needs_decision.", "me_apps_catch": "Read and measured on the same harness shape, NOT changed. Outer catch answers {\"apps\":[]} and logs '[hono] /me/apps failed' once: fails closed. Inner catch around the resolution (catch { failOpen = true; }) answers every app, the permission-gated 'billing' (requires a capability nobody holds) included, and logs NOTHING, when the resolver throws synchronously or its accessor throws; an async rejection keeps filtering (only 'open'). No real producer measured: SecurityPlugin's resolvePermissionSetsForContext is async (packages/plugins/plugin-security/src/security-plugin.ts:6804), so it rejects and never throws synchronously. Noted in Acceptance notes, not filed.", "tests": "All at head 54bc11877a, through scripts/pm/os-verify-lock.sh (slot dev-23047), closure built first (pnpm --workspace-concurrency=2 --filter @objectstack/plugin-hono-server^... build, VERDICT command-exit 0). | pnpm --filter @objectstack/plugin-hono-server test -> VERDICT command-exit 0; Test Files 29 passed (29); Tests 336 passed (336). | pnpm --filter @objectstack/plugin-hono-server typecheck (tsc --noEmit + tsconfig.typecheck.json + check:test-typecheck) -> VERDICT command-exit 0; check:test-typecheck: OK, 0 file(s) / 0 error(s). tsc --listFiles -p tsconfig.test.json: 466 files, 29 own tests, the new test counted once. | New file src/current-user-endpoints-outer-catch-closed.test.ts, 7 tests: 3 throw-site pins (buildEffectiveObjectPermissions on a non-object objects value; resolver throwing synchronously past .catch(() => []); permissionSetResolver(ctx) accessor throw) each 200 + exact closed body + the warn line once; 1 key-set pin (closed keys == success keys); CONTROL resolver rejection -> systemPermissions [] with session positions [org_member, everyone] and no outer-catch log; CONTROL degraded branch body text byte-identical; CONTROL no session -> {\"authenticated\":false}. | Ablation at 54bc11877a: scripts/ablation-replace.mjs restored the old literal at the outer catch, run INSIDE os-verify-lock so the mutation lived only while the lock was held (11s): anchor x1 -> x0, old literal x0 -> x1, blob f30ec90107aa -> 8a61d5281761; result Tests 4 failed | 3 passed (7): the 3 throw-site pins + key-set pin red (diff: the six missing keys), the 3 controls green; restore proven: blob == HEAD f30ec90107aa, git diff HEAD empty. No dist in the resolution path: the test imports ./current-user-endpoints (source). Same direction measured earlier at a48aad9519 (4 red / 3 green). | Base-vs-head body probe (one-off, scratch files deleted, nothing tracked mutated): success, resolver-rejection, degraded and anonymous bodies byte-identical; only the 3 throw sites changed. | Lint (narrowed, proven): population from eslint.config.mjs (config objects on **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} and packages/**/*.{ts,tsx,mts,cts}); --format json count: 3 results, 2 linted with 0 errors / 0 warnings, the .changeset .md reported as no matching configuration; invariance: no parserOptions.project / projectService anywhere (0 hits; the config header says it never enables type-aware linting), so the diff cannot move any untouched file verdict. Full pnpm lint is CI-owned.", "gates": { "head": "54bc11877a", "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) -> 65 commands, identical to the claim-path derivation and to the PM 65", "ran": "65 of 65, each exit code captured before any pipe, on the clean final tree; all 65 exit 0", "reconciliation": "dispatch-gates --ran: ✓ dispatch-gates --ran: 65 derived famil(ies) accounted for — 65 run, 0 NOT-MEASURED (a DERIVED zero — all 65 recorded an exit code and none of them is 3). RAN_EXIT=0", "freshness": "at report time the tree is 6 commits behind origin/main 038d4c9588; none touches plugin-hono-server or these files; the one derivation input changed in that range (scripts/check-verify-stand-in-erasure.mjs) adds packages/verify entries only; re-derived after fetch: the same 65", "outside_the_65": "CI-owned per the derivation: 51 artifact-roster families, 10 wide-population families, 6 workflow-valued families, 5 path-scheduled CI jobs, 4 type-check lanes", "commands": [ "node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 :: ✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).", "node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 :: ✓ check-adr-0087-registration --self-test: 548 assertions over real temp git repos (real scan()/assertInputs() path)", "node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 :: ✓ Protocol lockstep: PROTOCOL_VERSION major 18, @objectstack/spec@17.7.0 — the one exception, evidenced: pre mode (tag `next`) and a pending `major...", "node scripts/check-changeset-no-major.mjs --self-test :: exit 0 :: ✓ check-changeset-no-major --self-test: 401 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in ...", "node scripts/check-ci-filter-parity.mjs :: exit 0 :: OK: all 21 build input(s) turbo.json declares outside the packages (itself, 2 globalDependencies, and every `$TURBO_ROOT$` input of a build Build C...", "node scripts/check-closing-keyword-parity.mjs :: exit 0 :: check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords and both measured separators; sweep found 5 file(s) carrying the grammar across...", "node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 :: ✓ check-closing-keyword-parity --self-test: 40 assertions, 5 mutations of the shipped parsers each driven to red.", "node scripts/check-comment-mask-adoption.mjs :: exit 0 :: OK check:comment-mask-adoption — 14 private comment-stripper(s) under packages/** + examples/**, all 14 recorded and every recorded row still reach...", "node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 :: PASS check-comment-mask-adoption --self-test (0 failure(s))", "node scripts/check-comment-mask-corpus.mjs :: exit 0 :: ✓ comment-mask corpus sweep [scripts/js-comment-mask.mjs]: 8878 files, 0 disagree, 0 unparseable, 116.1s (comparator self-test: 26 cases pass).", "node scripts/check-dts-emitted.mjs --self-test :: exit 0 :: check-dts-emitted self-test: all assertions passed.", "node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 :: ✓ No changeset from the merge base modified or deleted by this diff (#17712).", "node scripts/check-empty-changeset.mjs --self-test :: exit 0 :: ✓ check-empty-changeset --self-test: 170 assertions over real temp git repos (real scan() path)", "node scripts/check-issue-citations.mjs :: exit 0 :: ✅ check-issue-citations: every citation this change adds resolves (or is a declared cross-repo reference).", "node scripts/check-keyed-text-bounds.mjs :: exit 0 :: ✓ check:keyed-text-bounds: 112 *.object.ts files under packages/** + apps/** + examples/** (walk is repo-wide; 0 outside), 117 object declarations,...", "node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0 :: PASS check-keyed-text-bounds --self-test (0 failure(s))", "node scripts/check-platform-object-tenancy-census.mjs :: exit 0 :: ✓ platform-object tenancy census matches the tree: 84 platform-namespace objects, 49 in the machinery's reach, 35 outside it, every exclusion expla...", "node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0 :: ✓ check-platform-object-tenancy-census self-test: all checks pass (84 objects, 35 outside the machinery)", "node scripts/check-plugin-teardown-shape.mjs :: exit 0 :: ✓ check:plugin-teardown-shape: 75 Plugin implementation(s) across 8292 source(s) under packages/**; every teardown-shaped method (stop / shutdown /...", "node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 :: ✓ check-plugin-teardown-shape self-test: 48 cases pass (real pre-#10375 fixture reds, the repaired file and both delegating-alias directions stay g...", "node scripts/check-registry-log-declared.mjs :: exit 0 :: OK: 73 vitest-running package(s) walked, 10 selected as engine-booting, every one declares a recognised registry log level (debug/info/warn/error/s...", "node scripts/check-registry-log-declared.mjs --self-test :: exit 0 :: self-test OK: 17 cases + level-vocabulary read + population declaration + real-tree selection floor.", "node scripts/check-rest-log-spy-declared.mjs :: exit 0 :: OK: 31 of 279 test file(s) beside packages/rest/src/log.ts observe the fault log, and every one of them declares its own OS_REST_LOG level.", "node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0 :: ✓ the real tree yields a NON-EMPTY observer population that is a strict subset of its test files", "node scripts/check-system-context-census.mjs :: exit 0 :: check-system-context-census: OK — 131 elevation read sites in 20 packages across 59 files, living in 111 symbol(s); the page cites 124 symbol(s) ag...", "node scripts/check-system-context-census.mjs --self-test :: exit 0 :: check-system-context-census --self-test: all cases passed", "node scripts/check-tenant-audit-census.mjs :: exit 0 :: ✓ check-tenant-audit-census: OK -- 227 write call sites certified (155 decidable; 2 tenancy-enabled sites PROVABLY carry no tenant context, 32 more...", "node scripts/check-tenant-audit-census.mjs --self-test :: exit 0 :: ✓ check-tenant-audit-census self-test: 27 cases pass (drift on a dropped ledger row, on an edited page count, on an edited ledger total and on a lo...", "node scripts/check-undeclared-dep-imports.mjs :: exit 0 :: ✓ check:undeclared-dep-imports: 81 workspace packages under packages/** + apps/** + examples/**, 3043 non-test src files, 2538 @objectstack/* speci...", "node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0 :: PASS check-undeclared-dep-imports --self-test (0 failure(s))", "node scripts/docs-audit/check-affected-docs.mjs :: exit 0 :: ✓ affected-docs self-test: 605 cases pass.", "node scripts/docs-audit/check-drift-comment.mjs :: exit 0 :: ✓ check-drift-comment: 66 cases pass across 5 fixture diff(s).", "node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 :: ✓ self-test passed", "node scripts/release-pending-publish.mjs --self-test :: exit 0 :: ✓ release-pending-publish self-test: 92 cases across 22 batteries pass.", "pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 :: ✓ check:duration-unit-keys — 197 unit-declaring numeric key(s) across 3048 source file(s) all carry their unit in the key name (or in a sibling `un...", "pnpm check:changeset-gate-self-tests :: exit 0 :: ✓ check-changeset-no-major --self-test: 401 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in ...", "pnpm check:cross-package-test-inputs :: exit 0 :: OK: 30 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob (6 of them on a split \"test:repo\" task); 13 walk...", "pnpm check:doc-authoring :: exit 0 :: ✓ doc authoring guard: sibling-package prose ids hold the baseline — 0 pinned site(s) across 0 file(s), 94729 string(s) read in 1319 parsed source(...", "pnpm check:driver-memory-census :: exit 0 :: check-driver-memory-census: OK — every declaration is ledgered, every ledger entry is live, and every ruled file states \"#6664 census: 2 ruled cons...", "pnpm check:dts-closure :: exit 0 :: check-dts-closure: 72 built package(s) swept - 171/171 declared declaration file(s) present across 71 package(s)", "pnpm check:dual-build-cjs-loads :: exit 0 :: ✓ check:dual-build-cjs-loads — 107 published require entry point(s) across 66 package(s) load; 717 emitted CommonJS file(s) parse", "pnpm check:engine-double-contract :: exit 0 :: check-engine-double-contract: OK — 999 pinned, 124 in the DEBT ledger, 3 exempt.", "pnpm check:error-status-conformance :: exit 0 :: ✓ every derivable runtime status is documented, and every documented status is reachable.", "pnpm check:gitlink-declared :: exit 0 :: check-gitlink-declared: OK (11082 index entries -- 0 gitlink(s) at mode 160000; no .gitmodules in the index, so nothing is declared; nothing to dec...", "pnpm check:issue-citations :: exit 0 :: ✅ check-issue-citations --self-test: grammar narrowed, every spelling enumerated, qualifier a closed set of repositories, four 404 causes kept apar...", "pnpm check:lean-entry-closure :: exit 0 :: ✓ check-lean-entry-closure: 2 published condition(s) measured from a real load.", "pnpm check:logger-receiver-detach :: exit 0 :: OK every log channel keeps its receiver: 3341 non-test TS file(s) walked, 0 detach(es) on the 5 declared receiver-sensitive sink spelling(s).", "pnpm check:nul-bytes :: exit 0 :: check-nul-bytes: OK (scanned 11073 text file(s) -- 11073 tracked, 0 untracked-not-ignored; skipped 9 binary; no raw ASCII control bytes).", "pnpm check:objectql-double-limit :: exit 0 :: OK ObjectQL double `limit` conformance holds: 481 double(s) graded, 287 apply the caller's bound or refuse it loudly.", "pnpm check:objectui-changeset :: exit 0 :: ✓ objectui-range --self-test: all checks passed", "pnpm check:org-identifier :: exit 0 :: check-org-identifier: OK (3363 author-facing source file(s), 18 session binding(s) resolved, no removed session.tenantId alias).", "pnpm check:page-declaration-shape :: exit 0 :: check-page-declaration-shape: OK — 36 page entries across 3364 sources under packages/**, examples/**, apps/** all reach the kernel through a disco...", "pnpm check:pm-changeset-deadline-census :: exit 0 :: ✓ changeset-deadline-census --self-test: all cases passed across 5 batteries (what counts as a named target, the controls that make a zero a readin...", "pnpm check:published-files :: exit 0 :: ✓ check:published-files — 70 publishable package(s) of 81 workspace member(s) declare a `files` whitelist that covers every entry point plus CHANGE...", "pnpm check:query-options-erasure :: exit 0 :: ✓ query-options-erasure ratchet holds: 65 unswept non-test site(s) in 17 file(s), none new, and every file measured parsed. Every other non-test fi...", "pnpm check:refd-timer-probe :: exit 0 :: OK check-refd-timer-probe: 8872 source file(s) swept; the process-global timer probe is read in packages/qa/refd-timer-testkit/src/index.ts and now...", "pnpm check:route-envelope :: exit 0 :: ✓ Plugin-mounted Hono routes — 13 module(s) audited ... 3 exempt, 1 vendor-wire; exempt, closed at unenveloped 9: packages/plugins/plugin-hono-server/src/current-user-endpoints.ts", "pnpm check:slot-lookup :: exit 0 :: ✓ slot-lookup ratchet holds: 104 unswept site(s) in 25 file(s), none new, and every file in the population parsed. Every other file under packages/...", "pnpm check:sourcemap-no-sources-content :: exit 0 :: check-sourcemap-no-sources-content: 69 built package(s) swept - 552 map(s), none embed source text.", "pnpm check:test-source-alias :: exit 0 :: check-test-source-alias OK — 73 packages with tests scanned; 60 registered as still resolving a workspace dep through `dist/`; 53 published subpath...", "pnpm check:tier-file-adoption :: exit 0 :: OK: 81 workspace package(s) walked, 82 nightly-tier test file(s) on disk (82 e2e, 0 live), owned by 1 package(s); every one reads OS_TEST_TIERS.", "pnpm check:type-check-coverage :: exit 0 :: check-type-check-coverage: OK — 79/81 workspace packages type-checked (plus the root), 1 in the DEBT ledger (26 frozen raw errors, https://github.c...", "pnpm check:type-check-debt :: exit 0 :: check-type-check-coverage --re-measure: OK — 1 ledger entr(ies) re-measured in 166.1s, 26 raw tsc error(s) total, none above its recorded number.", "pnpm check:watch-hint-literal :: exit 0 :: ✓ check-watch-hint-literal: 73 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 49, ROOT_FILE_WATCH_HINTS 13, ROOT_WATCH_HINTS 3, D...", "pnpm check:where-matcher :: exit 0 :: ✓ where-matcher conformance holds: 503 matcher(s) discovered, 503 answer the combinator battery correctly or refuse it loudly (326 refuse)." ] }, "line_budget": "343 changed lines (+335 / -8) over 3 files vs the human-merge threshold 3000: under (dispatch-gates reading at 54bc11877a vs merge base e448d3c27f)", "files_changed": [ "packages/plugins/plugin-hono-server/src/current-user-endpoints.ts (+78 / -8)", "packages/plugins/plugin-hono-server/src/current-user-endpoints-outer-catch-closed.test.ts (+239, new)", ".changeset/23047-me-permissions-outer-catch-closed.md (+18, new; @objectstack/plugin-hono-server patch; body carries Clause-②: no)" ], "deviations": [ "Builder placement: MePermissionsTabVisibility, MePermissionsGrants, closedMePermissionsGrants and mePermissionsBody sit at module scope beside ResolvedPermissionSetLike in the same file, not inside the handler body; they serve only the /auth/me/permissions handler. No other file, package or objectui path was touched.", "Spread at both call sites (c.json({ ...mePermissionsBody(...) })), not in the dispatch: forced by check:route-envelope, which treats a bare call as a relayed body and went red (unenveloped 9 pinned, 7 found). Spelling the call sites keeps the ruled count an exact enumeration without editing the gate script (outside the claimed surface).", "Merged origin/main e448d3c27f into the branch (merge commit 6775a24397) before the final gates, per AGENTS.md section 10; the closure was rebuilt after it.", "An earlier ablation attempt at 54bc11877a never acquired the lock (exit 99, NOT MEASURED) and its mutation sat on disk for about 9 minutes while gate battery 2 ran; in that window check:type-check-debt (which runs turbo build over every package, outside the lock) rebuilt this package dist from the mutated source. Detected by reading dist (old literal 1, builder 0), rebuilt, proven absent with ablation-dist-preflight --absent; the ablation was then re-run inside the lock and the whole 65-gate union was re-run on the clean tree. Every number in this report is from that clean run.", "A first local test run failed on my own guessed degraded-body literal (positions member vs the resolver real org_member); corrected to the measured value, and that control is confirmed against the BASE handler by the base-vs-head probe.", "objectui read: REST contents read for objectui was refused (403, access not enabled for this session); read by public shallow git fetch instead. Read-only, deleted afterwards.", "Commit trailers are the model-free pair from AGENTS.md (Claude-Session + Co-authored-by: Claude); the harness reminder model-named trailer was not used, per the role file precedence.", "Worktree removed after the PR opened (node_modules removed first, git worktree remove without --force exit 0)." ], "mcp_calls": "0 — no MCP GitHub tool was called (ToolSearch and TaskStop only, neither a GitHub tool)", "api_writes": "2 relay strokes, each one POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot] by the fleet-write run: (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/23081/assignees [os-steve], run 38284884883 success, body read back 10243/10243 bytes identical; (2) this os-dev-report comment -> POST /repos/objectstack-ai/objectstack/issues/23047/comments. label-write: 0 (no label named by the dispatch, changeset present so no skip-changeset). git push: 5 (empty-branch probe, d0b94019ae, a48aad9519, merge 6775a24397, 54bc11877a), not REST.", "open_questions": [], "out_of_scope_findings": [ "carrier: none · noted, not filed · /me/apps inner catch (catch { failOpen = true; }) lists every app, permission-gated ones included, silently, on a synchronous resolver throw or accessor throw; measured on the harness (billing listed); no real producer: SecurityPlugin resolvePermissionSetsForContext is async (security-plugin.ts:6804) · dedupe words: me/apps failOpen, inner catch fail open, permission-gated apps listed, resolver sync throw", "carrier: none · noted, not filed · check:route-envelope counts only object-literal bodies, so a body built by a same-file helper and passed as a call reads as relayed and silently leaves a ruled unenveloped count; handled here by spelling the call sites · dedupe words: route-envelope helper-built body, relayed body blind spot, unenveloped count", "carrier: none · noted, not filed · check:type-check-debt (check-type-check-coverage.mjs --re-measure) runs turbo run build over every package outside os-verify-lock, so it can rebuild dist from a tree that an in-flight ablation has mutated; observed once in this session and recovered · dedupe words: type-check-debt turbo build, re-measure builds workspace, ablation dist contamination" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
✅ ACCEPT: PR #23081 at
54bc11877a(Fixes #23047). It lands when every check on the head is greendomain:cliseat 2 (#22648) ·os-steve· sessionsession_01VoSxBQujKLZKPwK2u5ehQ6· 2026-10-12T17:38Z · holder of claim6127231453. Report:os-dev-report6129367380. Thread-read: 6129367380.Checklist, read against GitHub:
PR shape: draft, base
main, line 1Fixes #23047, line 2Clause-②: no, assigneeos-steve.Scope: 3 files, +335 / −8:
current-user-endpoints.ts(+78 / −8);origin/mainwas merged in with no conflict.check-governed-merges --pr 23081: not governed.The decision, read in the diff: the outer
catchanswersmePermissionsBody(identity, closedMePermissionsGrants()). That is ONE builder, which the success path also uses with its computed grants, so it is not a second literal.userId,tenantId ?? null) is captured before thetry, and the builder reads only its arguments, so thecatchcannot throw again.MePermissionsGrantsrequires all six grant keys, so tsc refuses a call that drops one.check:route-envelope's ruled count of nine unenveloped bodies an exact enumeration. A bare call read as a relayed body and dropped the count to 7. The gate script is untouched.positions(assumption 2), measured: objectui'sMePermissionsProviderreads nopositionsfrom this body, atmainand at the console pin9646e2ed. The workspace-admin read (useWorkspaceAdminStatus) takes positions from the auth session. Sopositions: []opens no handle that the caller's real positions would close, and no decision was owed.Bodies: the outer catch went from
{authenticated, userId, objects, fields}to the closed body with all four grant keys present and empty, plustenantId. A base-against-head probe shows these byte-identical:{authenticated:false}.Pins: three throw sites (the effective-object merge, a resolver throwing synchronously, the resolver accessor) each answer
200with the exact closed body and log once. A key-set pin holds the closed keys equal to the success keys. Three controls: the resolver rejection, the degraded branch and no session. The ablation of the old literal reds the 4 pins and leaves the 3 controls green; the restore was proven against HEAD.Out-of-scope notes, no carrier (the seat files none):
/me/apps' innercatchfails open on a synchronous resolver throw. No real producer exists:resolvePermissionSetsForContextis async, so it rejects, and the rejection keeps filtering.check:route-envelopereads a helper-built body passed as a call as relayed. Handled here by spelling the call sites.check:type-check-debtbuilds every package outside the verification lock. It rebuiltdistfrom a tree that a lock-less ablation attempt had mutated; this was detected, rebuilt and re-run clean. The seat records it as a fleet note.Changeset
.changeset/23047-me-permissions-outer-catch-closed.md:@objectstack/plugin-hono-serverpatch,Clause-②: no. The failure-path body gains the keys the success path already carries, and it fails closed as ADR-0066 and the resolver path do. No accept set, export orspeckey moves. No contract-review face.Tests and gates at
54bc11877a:@objectstack/plugin-hono-server: 29 files, 336 tests.CI on
54bc11877aat this stamp: latest run per name, 22 success and 6 skipped (all in the roster), with 9 in progress. Not green yet.Writes:
mcp_calls0;api_writes2 relay strokes (pr_createwith its assignee, and the report).Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
Landed: PR #23081 →
0aef788816(Fixes #23047). The card closescompleteddomain:cliseat 2 (#22648) ·os-steve· sessionsession_01VoSxBQujKLZKPwK2u5ehQ6· 2026-10-12T18:24Z · holder of claim6127231453, released by this landing.0aef788816, a squash with one parent./auth/me/permissionsoutercatchinpackages/plugins/plugin-hono-server/src/current-user-endpoints.tsnow answers the closed body:systemPermissions,positionsandpermissionSetsas[], andtabPermissions,objectsandfieldsas{}, plus the sessiontenantId.mePermissionsBody) that the success path also uses.6129422472. No contract-review face (Clause-②: no). Every check on head54bc11877awas success or a roster skip.{authenticated:false}.MePermissionsProviderreads nopositionsfrom this body, and the workspace-admin read takes positions from the auth session (6129367380)./me/apps' innercatchfails open on a synchronous resolver throw. No real producer exists: the resolver is async, so it rejects, and a rejection keeps filtering.check:route-envelopereads a helper-built body passed as a call as relayed.current-user-endpoints.ts(the/auth/me/permissionshandler) and its new test.Generated by Claude Code