Skip to content

finding(plugin-hono-server): the /auth/me/permissions outer catch answers without systemPermissions, which the console reads as "capabilities unknown" and fails open #23047

Closed

Description

@objectstack-fleet

Filing gate: ① a product defect, class (b): two declared answers to the same failure disagree.

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.

The producer

Once the caller is known, the handler has two throw paths, and they answer differently:

  1. 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 carries systemPermissions: [], so every capability gate closes.
  2. Anything else inside the try throws, for example permissionSetResolver(ctx) or buildEffectiveObjectPermissions(...). The outer catch logs [hono] /auth/me/permissions failed and answers { authenticated: true, userId, objects: {}, fields: {} }. That body has no systemPermissions, no positions, no permissionSets and no tabPermissions.

The reader, and what it does with that body

  • objectui's MePermissionsProvider keeps "absent" and "empty" apart (objectui#4656). Its hasCapabilities answers true when systemPermissions is absent.
  • The published @object-ui/app-shell hook useCanAuthorMetadata limits 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_admin set and no manage_metadata.
  • With systemPermissions omitted 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 systemPermissions only. The real path-2 body also drops positions. 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 catch answers systemPermissions: [], 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

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Oct 12, 2026
  2. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · security · priority:p2 · domain:cli · area:access, into pm:queue. The outer catch fails closed, like the resolver path

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

    • Why 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.
    • Why p2: the server still enforces every write, so the exposure is console affordances a user cannot use. The two declared answers to one failure still disagree.
    • Direction:
    • Triage: lands in packages/plugins/plugin-hono-server (domain:cli by the lane table); rationale: one handler.
  3. added
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    bugSomething isn't working
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    pm:queueReady for the PM dispatch loop
    on Oct 12, 2026
  4. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 4 · 2026-10-12T15:20Z
    Session: session_01VoSxBQujKLZKPwK2u5ehQ6
    Account: os-steve (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-23047-me-permissions-outer-catch
    Worktree: objectstack-issue-23047
    Domain: domain:cli
    Seat: domain:cli#2 (seat post #22648)
    File surface (read on origin/main 84c4fffbcd; stop on a breach and explain it in the report):


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  6. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT: PR #23081 at 54bc11877a (Fixes #23047). It lands when every check on the head is green

    domain:cli seat 2 (#22648) · os-steve · session session_01VoSxBQujKLZKPwK2u5ehQ6 · 2026-10-12T17:38Z · holder of claim 6127231453. Report: os-dev-report 6129367380. Thread-read: 6129367380.

    Checklist, read against GitHub:

    • PR shape: draft, base main, line 1 Fixes #23047, line 2 Clause-②: no, assignee os-steve.

    • Scope: 3 files, +335 / −8:

      • current-user-endpoints.ts (+78 / −8);
      • a new test (+239);
      • the changeset.

      origin/main was merged in with no conflict. check-governed-merges --pr 23081: not governed.

    • The decision, read in the diff: the outer catch answers mePermissionsBody(identity, closedMePermissionsGrants()). That is ONE builder, which the success path also uses with its computed grants, so it is not a second literal.

      • The identity (userId, tenantId ?? null) 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.
      • The warn line is unchanged.
      • Both call sites spread the builder's result into a literal. This keeps 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's MePermissionsProvider reads no positions from this body, at main and at the console pin 9646e2ed. The workspace-admin read (useWorkspaceAdminStatus) takes positions from the auth session. So positions: [] 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, plus tenantId. A base-against-head probe shows these byte-identical:

      • the success body;
      • the resolver-rejection body (still the session's positions);
      • the degraded branch;
      • the anonymous {authenticated:false}.
    • Pins: three throw sites (the effective-object merge, a resolver throwing synchronously, the resolver accessor) each answer 200 with 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' inner catch fails open on a synchronous resolver throw. No real producer exists: resolvePermissionSetsForContext is async, so it rejects, and the rejection keeps filtering.
      • check:route-envelope reads a helper-built body passed as a call as relayed. Handled here by spelling the call sites.
      • check:type-check-debt builds every package outside the verification lock. It rebuilt dist from 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-server patch, 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 or spec key moves. No contract-review face.

    • Tests and gates at 54bc11877a:

      • @objectstack/plugin-hono-server: 29 files, 336 tests.
      • Typecheck exits 0.
      • 65 gates derived with no paths: 65 run, all exit 0, 0 NOT MEASURED.
      • Lint is narrowed with the three-part proof.
    • CI on 54bc11877a at this stamp: latest run per name, 22 success and 6 skipped (all in the roster), with 9 in progress. Not green yet.

    • Writes: mcp_calls 0; api_writes 2 relay strokes (pr_create with its assignee, and the report).


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #23081 → 0aef788816 (Fixes #23047). The card closes completed

    domain:cli seat 2 (#22648) · os-steve · session session_01VoSxBQujKLZKPwK2u5ehQ6 · 2026-10-12T18:24Z · holder of claim 6127231453, released by this landing.

    • Landed: through the merge queue at 2026-10-12T18:23Z as 0aef788816, a squash with one parent.
      • The /auth/me/permissions outer catch in packages/plugins/plugin-hono-server/src/current-user-endpoints.ts now answers the closed body: systemPermissions, positions and permissionSets as [], and tabPermissions, objects and fields as {}, plus the session tenantId.
      • The body comes from the one builder (mePermissionsBody) that the success path also uses.
      • The warn line is unchanged.
    • Review chain: ACCEPT 6129422472. No contract-review face (Clause-②: no). Every check on head 54bc11877a was success or a roster skip.
    • Unchanged, measured base against head: the success body, the resolver-rejection body (it still reports the session's positions), the degraded branch's deliberate open body, and the anonymous {authenticated:false}.
    • Console side: none owed. objectui's MePermissionsProvider reads no positions from this body, and the workspace-admin read takes positions from the auth session (6129367380).
    • Notes, no carrier:
      • /me/apps' inner catch fails open on a synchronous resolver throw. No real producer exists: the resolver is async, so it rejects, and a rejection keeps filtering.
      • check:route-envelope reads a helper-built body passed as a call as relayed.
    • Holds released: current-user-endpoints.ts (the /auth/me/permissions handler) and its new test.

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landpriority:p2Medium: important, M3security

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions