Skip to content

fix(rest): require the resolved environment to belong to the caller at GET /ui/view/:object/:type - #13625

Merged
os-steve merged 7 commits into
mainfrom
claude/issue-13214-ui-view-environment-ownership
Aug 31, 2026
Merged

fix(rest): require the resolved environment to belong to the caller at GET /ui/view/:object/:type#13625
os-steve merged 7 commits into
mainfrom
claude/issue-13214-ui-view-environment-ownership

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#13214

Maintainer ruling 2026-08-30T12:20:51Z (comment id 5468648876, 第 5 场总监席决裁批 #1, verbatim 「第一批其余同意」), option C: the seam must require that the resolved environment belong to the caller — identity resolution plus an ownership check — on both named-environment channels, with a signalled refusal on an envRegistry.resolveById validation failure rather than a silent fallback to the default environment.

⛔ Draft on purpose. Review and landing are the PM's; auto-merge is not armed.

What was wrong

GET /api/v1/ui/view/:object/:type was the one metadata-touching route in RestServer's table that resolved no identity at all — straight from resolveProtocol to getUiView. Measured across two prior measurement-only PRs (#13244, #13258, both merged):

  • 200 to an anonymous caller, byte-identical to an entitled one, with resolveExecCtx called zero times, while all 52 identity-touching siblings answered 401 under an absent context;
  • because the unscoped mount lets the REQUEST name its environment, an anonymous caller naming another environment received that environment's UI view — object label plus every field's name / label / type / required — through both channels (X-Environment-Id header and bound hostname), with the foreign kernel acquired;
  • an object-existence oracle for the named environment, and an environment-id oracle: an unresolvable X-Environment-Id was not refused but fell through to the default environment and answered 200 with that environment's view, so two 200s with different bytes told a real environment id from an invented one.

Why anonymous-deny alone is not the repair

The ruling names it: adding resolveExecCtx + enforceAuth and stopping is option B, and it was measured not to fix the crossing — an authenticated caller could still name a foreign environment, because nothing downstream compared the environment that was resolved with the environment the caller is entitled to.

That claim is now demonstrated rather than quoted. The ablation below removes only the ownership comparison, keeping resolveExecCtx + enforceAuth — i.e. builds exactly option B — and 12 pins go red while every anonymous-deny pin stays green.

What the seam does now

Four steps, in this order, and the order is load-bearing:

  1. resolve the environment once, through the shared entry point resolveRequestEnvironmentId;
  2. resolve identity in that environment;
  3. refuse anonymity (enforceAuth);
  4. compare (enforceEnvironmentOwnership).

Resolving identity before the environment is decided would authenticate the caller somewhere other than where the answer comes from — which is the very mismatch step 4 exists to catch.

There was no ownership predicate to reuse. Searched before writing one: no environmentOwnership / assertEnvironmentAccess / entitlement predicate exists in this package or its siblings, the scoped /environments/:environmentId/... mounts carry no such check either, and ExecutionContext declares no environment field. So the fact had to come from where identity is actually established.

computeExecCtx validates the caller against an auth service it looks up in a kernel, and it now records which environment that kernel belongs to on __authEnvironmentId. A credential is good for the environment whose auth service accepted it. The value differs from the resolved environment in exactly the branch that crosses: when the resolved environment's kernel carries no auth service, the lookup falls back to the default environment's, and a session minted there authenticated a request naming another one.

Two refusable shapes: named but not served (the caller named an environment through the header and the chain resolved a different one — which is what an unresolvable id does), and anchored elsewhere.

⚠️ Why the refusal is the anonymous-deny response, verbatim

Deliberate, and the reason is the oracle rather than tidiness. A caller naming a real foreign environment is already refused by enforceAuth — their session is not valid there, so no context resolves. Giving either shape above a different status (a 403, or a 404 of this seam's own) would leave "this environment id exists" distinguishable from "it does not" by status alone, rebuilding the oracle one layer up. One shape, byte for byte, for every way a caller can fail to be entitled to the environment it named.

The cost is diagnosability, named rather than discovered: an operator whose environment genuinely lacks an auth service sees the anonymous 401, not a wiring error. That is the same trade the sibling seams already make.

undefined and 'platform' are not environments — a control-plane boot resolves none, so the anonymous floor is the whole gate there.

⚠️ Changeset grade — proposed, not decided

Proposed minor, with the migration note already written into the changeset. Please confirm or override; the ruling did not grade this card.

Reasoning: this changes a published route's behaviour from "anonymous read" to "authenticated and ownership-checked", which plausibly breaks a caller that relied on the old behaviour (an anonymous consumer, a caller sending a contradictory X-Environment-Id, a caller sending an unresolvable one, or a deployment whose environment kernel carries no auth service of its own — all four are enumerated with remedies in the changeset). The nearest precedent on this lane is #13347, graded minor with a migration note for a purely additive published-envelope change, on the reasoning that a published shape change is minor even when additive. This one is a narrowing, so minor looks like the floor rather than a generous read — patch seems wrong to me and I did not want to choose it quietly.

Verification — at 2a4e9098e, the final commit

All exit codes captured before any pipe (cmd > file 2>&1; EXIT=$?), every gate quoted from its own printed verdict line. Authored in session https://claude.ai/code/session_01UngCYXF98BVpYA9hfz6NYk — recorded here in prose because editing a PR body rewrites the footer's session URL to the bare form.

Tests.pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2Test Files 163 passed (163) / Tests 2740 passed (2740).

Dogfood suite (@objectstack/dogfood, 129 files, sharded 3 ways exactly as CI does) → 980 passed, 3 skipped, 0 failed: shard 1 Test Files 43 passed (43) / Tests 313 passed (313), shard 2 43 passed (43) / 288 passed | 1 skipped (289), shard 3 42 passed | 1 skipped (43) / 379 passed | 2 skipped (381). 43+43+43 = 129 = the real file count, which is how I know --shard took effect rather than being silently dropped. The case that was RED in CI now prints its own verdict line: ✓ authz probe blind-spot census (#13260) > packages/rest/src/rest-server.ts — every positive control is still present in THAT file.

Typecheck.pnpm --filter @objectstack/rest typecheck → exit 0; check:test-typecheck: OK — @objectstack/rest's test layer compiles under packages/rest/tsconfig.test.json; 1 file(s) / 2 error(s) / 1 pinned signature(s) held in test-typecheck-debt.json. ⛔ Not a NOT-MEASURED green: tsc -p tsconfig.test.json --listFiles puts a 660-file program on the table and all six changed files are in it (1 hit each), and the two ledgered errors are both in src/rest.test.ts — none is mine.

pnpm lint (repo-wide eslint . --no-inline-config) → exit 0, run in full. ⛔ No narrowing claimed. Run because this lane compensates by hand for dispatch-gates.mjs not naming the lint family.

Gate union, re-derived after merging origin/main with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (change set taken from the merge base by the script itself). The first derivation printed a STALE TREE banner on stderr while still exiting 0 — origin/main had moved 5 commits — so it was re-derived after the merge and that second answer governs. Green, each read from its own verdict line: check:authz-resolver · check:route-envelope · check-system-context-census · check:nul-bytes · check:cross-package-test-inputs · check:dispatcher-error-vocabulary · check:doc-authoring · check:logger-receiver-detach · check:objectql-double-limit · check:page-declaration-shape · check:published-files · check:slot-lookup · check:test-source-alias · check:type-source-resolution · check:where-matcher · check:changeset-gate-self-tests · check:objectui-changeset · check:pm-half-states · check-adr-0087-registration · check-changeset-no-major · check-empty-changeset · check-ci-filter-parity · check-comment-mask-adoption · check-keyed-text-bounds · check-plugin-teardown-shape · check-shard-attestation · check-undeclared-dep-imports · docs-audit/check-affected-docs · docs-audit/check-drift-comment · release-rehearsal-clone --self-test.

Refusals, reported separately and NOT folded into the green list: none. No gate exited "PREREQUISITE NOT MET" in this run.

Patch round 2 — the census table conflict, resolved by REGENERATION

The PR went mergeable_state: dirty behind 7 commits. Exactly one file conflicted — content/docs/permissions/system-context.mdx, the census anchor table — while packages/rest/src/rest-server.ts auto-merged clean.

Not hand-resolved, and not resolved by picking a side. Both sides had run the gate's --fix against different trees, so each side's line numbers were right for its own tree and neither was right for the merged one. Resolution was: clear the marker with an arbitrary side as a placeholder, then run node scripts/check-system-context-census.mjs --fix against the merged tree and let it rewrite the table.

The regeneration is what proves hand-merging would have been wrong, and by more than the conflict showed. It rewrote 18 anchors, not 5:

  • Row 50's five rest-server.ts anchors resolved to 4408, 5771, 6019, 6382, 6575 — matching neither side (mine said 4407, 5770, 6002, 6365, 6558; main said 4284, 5647, 5895, 6258, 6451). Either side taken whole is internally plausible and wrong.
  • 13 further anchors were silently stale with no conflict marker at allplugin-sharing/src/sharing-service.ts shifted +29 (625→654, 891→920, 978→1007, 1568→1597, 1309→1338) and four more rest-server.ts citations moved +1. Measured cause: main's 7 commits are +35 / -6 in sharing-service.ts. Git auto-merged those rows without a marker, so resolving only the marked conflict — by any method — still leaves the file wrong. Only regeneration reaches them.

Verdict line, quoted from the gate run without--fix at the merged head: check-system-context-census: OK — 109 elevation read sites in 20 packages across 45 files, all anchored; 145 anchors resolve, 27 declared non-read.

scripts/pm/os-regen-merge.sh does not cover this path, confirmed independently rather than taken on report: git check-attr merge answers unspecified for content/docs/permissions/system-context.mdx and os-regen for content/docs/references/**. This file has no merge driver.

Every anchored count re-derived at the merged head (main touched rest-server.ts, so none was carried forward): census 73 sites / 92 mentions / 20 caught / 53 bare / 53 bare-guarded / 0 caught-guarded; dogfood control enforceAuth 64, routeManager.register 80, registrars 17; ownership guard call sites 1. All unchanged. The gate union re-derived to the same 45 families as the previous head, with no stale-tree banner.

Dogfood suite, all three shards re-run at this head (⛔ no narrowing this round — the previous round's declared narrowing does not carry across a 7-commit merge): shard 1 Test Files 43 passed (43) / Tests 313 passed (313); shard 2 43 passed (43) / 288 passed | 1 skipped (289); shard 3 42 passed | 1 skipped (43) / 379 passed | 2 skipped (381). 43+43+43 = 129 = the real file count, so --shard took effect.

Patch round — the fourth count anchor, and why a package-scoped run could not see it

CI went red on Dogfood Regression Gate (3/3): control "enforceAuth" moved in packages/rest/src/rest-server.ts: expected 64 to be 61.

Re-anchored, not relaxed.PROBE_FILE_CENSUS's control exists to prove that census is still reading the file it thinks it is, and a rising enforceAuth is exactly what this card is supposed to cause. The recorded number is now the measured one and it is still an exact count — a range or a floor would stop the row noticing the next move, which is the only thing it is for.

The move is +3 over the whole file, because the derivation counts the bare term with comments included: one new call site (if (this.enforceAuth(req, res, context)) return;, 52 → 53) plus two prose mentions in the new doc-comments. The three sibling numbers were re-derived and did not move — population 80, reachable 19, private register*Endpoints( 17, this.routeManager.register( 80 — which is what says this is a guard change and not a surface change, so blindSpot stays 61.

Why it was missed, which is the transferable part:packages/qa/dogfood holds anchors citing other packages' files and is not reached by pnpm --filter @objectstack/rest .... A package-scoped suite structurally cannot see it; only the Dogfood Regression Gate runs it. Any change to rest-server.ts owes the dogfood suite a local run.

A second miss, found by re-deriving rather than by CI: my first-round gate union was derived before the system-context.mdx repair was applied, so it never listed the 11 documentation families that file pulls in. Re-derived after the merge, they are now run and green: check-doc-frontmatter · check-doc-route-spelling · check-docs-section-name · check-section-landing-index · check:doc-anchors · check:docs-audit-scope · check:docs-redirects · check:docs-single-h1 · check:published-readme-links · check:react-page-adapter-contract · check:role-word, plus check-cross-package-test-inputs. That is the derivation rule earning its keep: the gate list must be re-derived against the diff you actually have, not the one you started with.

Merge collision, resolved and verified. PR #13614 landed mid-round and edits execctx-consumer-census.test.ts — the same file I re-anchored. The merge auto-resolved; its §9 and my numbers coexist, and the census was re-derived on top of it at the final head rather than carried forward: 73 sites / 92 mentions / 20 caught / 53 bare / 53 bare-guarded / 0 caught-guarded, all unchanged.

One declared narrowing. Dogfood shards 2 and 3 were measured at the immediately preceding tree, not re-run at 898b1d705. That is a measurement rather than an omission: the delta between the two trees is exactly six files (git diff --name-only), zero of which are referenced anywhere under packages/qa/dogfood, and the same grep finds rest-server.ts in 5 dogfood files, so the zero is a reading and not a broken instrument. Shard 1 and the previously-red file were both re-run at the final head. The lock had two waiters, one at 750s, and re-running the other two shards would have starved them for ~8 minutes to re-measure a tree delta that provably does not reach them.

⭐ Ablation — direction predicted before running, and the first attempt was a no-op

Prediction, recorded before the run: removing only the ownership comparison builds option B exactly, so the result should be a split, not a uniform flip — red on every leg whose caller is authenticated but anchored elsewhere or names an environment the chain did not serve; green on every anonymous leg and on the whole identity suite, because enforceAuth still fires there. Predicted magnitude 10–14 red, 0 in ui-view-route-identity.measurement.test.ts.

Result: 12 failed / 41 passed, 11 in the tenancy suite and 1 in the ownership suite, 0 in the identity suite. Every red is an authenticated-outsider, ghost-id, oracle-closed, precedence or registrar-source leg. The prediction held on direction, on split and on magnitude.

⛔ The first ablation attempt measured nothing and this records it rather than quietly re-running. It reported 53/53 green — which my own pre-registered third branch ("if nothing flips, the pins are not reading the guard") flagged. Cause: the mutation lived in a script with trap restore EXIT, and the vitest run was chained after that script with &&, so the trap restored the file before vitest ever loaded it. The run was re-done with the suite invoked inside the trap's scope.

Mutation proven on disk, never from an editor exit code: anchor asserted unique before mutating (1), anchor after 0, injected text after 1, blob moved 19e33da029921cee; the script aborts if any of those fails. The tree was re-hashed after the run and still read 29921cee, so the reds were taken on the mutated tree. No rebuild needed, and here is why rather than an assumption: the mutated subject is rest-server.ts, imported in-package as ./rest-server.js and resolved by vitest to the TS source, not through a dependency's exports to dist/ — the 12 reds with no rebuild are themselves the proof the mutation reached the code under test.

Restore leg given equal treatment, proven by observation and not by an exit code:git checkout HEAD -- "$REPO_ROOT/packages/rest/src/rest-server.ts" (absolute path, HEAD-pinned to bypass the index) → restored blob 19e33da0equals the HEAD blob, injected text count 0, anchor count 1, git diff HEAD empty, git status clean.

The two existing measurement suites: inverted, ⛔ not deleted and ⛔ not weakened

Both were on main pinning the leak as present. Each reading that said "B's view crosses to a caller with no claim on B" now says "it is refused", against the same instrument, the same fixtures and the same controls, with the pre-repair reading kept in the comment beside its inverse — an assertion with no history cannot tell a later reader whether the defect ever existed.

One method change is called out in the file rather than hidden: the identity suite's §3 used to compare an absent caller against an entitled one through the seam. The seam no longer serves the absent one, so that comparison is unavailable and has ⛔ not been faked by relaxing an assertion. The producer question is unchanged and still answered — by calling the producer directly, plus a new reading the old method could not take: when the seam refuses, the producer is never reached (recorder sees 0 calls, with the same recorder reached once by an entitled caller as the control).

ui-view-environment-ownership.test.ts is new and it is the load-bearing half. Both existing suites stub resolveExecCtx wholesale, so __authEnvironmentId is synthetic there — they are complete readings about the gate and no reading at all about the fact. If computeExecCtx stamped the resolved environment unconditionally (the tempting shortcut), the gate would compare a value to itself, refuse nobody, and every assertion in those two files would stay green. The new file drives the unstubbed method and pins that the value is truthful, including the cross-environment fallback branch where it deliberately does not match.

⚠️ Prediction miss, reported rather than adjusted away

I predicted the foreign kernel would still be ACQUIRED before the refusal on the tenancy instrument (matching the sibling /data/:object ordering the ruling recorded as input knowledge). It measured empty. The prediction is right about production and wrong about that instrument: instrument() replaces resolveExecCtx, so the only getOrCreate caller left is resolveProtocol, which the repair moved to after the refusal. Unstubbed, computeExecCtx does acquire the named environment's kernel, and that is now pinned in the new file (authLookups[0] is the foreign environment, acquired contains it). ⛔ The zero must not be read as "the ordering property has been repaired" — it has not been, and repairing it is a separate card.

Fences

⚠️ Surface breach, declared

The diff leaves packages/rest/ in exactly one file: content/docs/permissions/system-context.mdx. check-system-context-census went red on my first run with 10 problems, all pure line rot — my edits shifted five rest-server.ts anchors by exactly +124 lines each. The repair is the gate's own prescribed --fix, it rewrote one table row and five line numbers and nothing else, and the gate then printed OK — 109 elevation read sites in 20 packages across 45 files, all anchored. Any edit to rest-server.ts above line 4283 causes this. Flagged for the PM rather than passed over.

Residual, measured and not closed here

For an authenticated caller the two refusals are byte-identical, so the id oracle is closed for them. The remaining discriminator is narrower and worth naming: a caller naming a real foreign environment is refused by enforceAuth on the identity step, whereas one naming an invented id is refused by the ownership comparison — today both answer the same 401 with the same body, which is why the oracle is closed. It stays closed only as long as those two answers remain identical, which is the reason the shape is asserted rather than described (see the id-oracle case in §4 of the tenancy suite). ⛔ Nothing here is left silently unrefused.


Generated by Claude Code


Generated by Claude Code

os-steveand others added 3 commits August 31, 2026 02:48
…t GET /ui/view/:object/:type
Maintainer ruling 2026-08-30 (option C): the seam must resolve identity AND
compare the resolved environment against the one the caller is entitled to, on
both naming channels (X-Environment-Id header and bound hostname), with a
signalled refusal on an envRegistry.resolveById validation failure rather than a
silent fallback to the default environment.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UngCYXF98BVpYA9hfz6NYk
- execctx-consumer-census: the UI-view route joins as BARE site 53 behind the
shared anonymous floor (72->73 sites, 89->92 mentions, 52->53 bare); the seam
drops its local .catch so it does not become the only site that is both
locally caught AND behind the floor.
- rest-exec-ctx-principal-kind: __authEnvironmentId named in the golden key set.
- system-context-census: 5 doc anchors re-anchored by the gate's own --fix,
pure line rot (+124 on every one).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UngCYXF98BVpYA9hfz6NYk
@github-actionsgithub-actionsBot added size/xl documentation Improvements or additions to documentation tests tooling labels Aug 31, 2026
@github-actions

github-actionsBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/rest, touching 7 documentable anchor(s).

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx(via getView (sdk), meta.getView (sdk))
  • content/docs/api/metadata-api.mdx(via /view/:object/:type (route))
  • content/docs/api/plugin-endpoints.mdx(via /view/:object/:type (route))
  • content/docs/kernel/contracts/metadata-service.mdx(via getView (sdk))
  • content/docs/kernel/services-checklist.mdx(via getView (sdk), /view/:object/:type (route))
  • content/docs/protocol/objectui/concept.mdx(via /api/v1/ui/view/:object/:type (route), /view/:object/:type (route))

4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx(via RestServer (symbol))
  • content/docs/releases/v12.mdx(via RestServer (symbol))
  • content/docs/releases/v16.mdx(via RestServer (symbol))
  • content/docs/releases/v17.mdx(via getView (sdk), meta.getView (sdk), /view/:object/:type (route))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json eaba72e48cc294038e74ece5bf7677568d55b038packageMentionDocs.

Which tree this was computed on

This run read content/docs from 48728f27ceaf82b3b09d6312b807758c1331dfc9 — the merge of head 2a4e9098e8248b080914610baf9909248590800e into base eaba72e48cc294038e74ece5bf7677568d55b038, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 48728f27ceaf82b3b09d6312b807758c1331dfc9 && git checkout 48728f27ceaf82b3b09d6312b807758c1331dfc9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin eaba72e48cc294038e74ece5bf7677568d55b038 2a4e9098e8248b080914610baf9909248590800e && git checkout -B drift-repro eaba72e48cc294038e74ece5bf7677568d55b038 && git merge --no-ff 2a4e9098e8248b080914610baf9909248590800e
node scripts/docs-audit/affected-docs.mjs --json eaba72e48cc294038e74ece5bf7677568d55b038

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs eaba72e48cc294038e74ece5bf7677568d55b038 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

os-steveand others added 2 commits August 31, 2026 04:15
…1 -> 64
The fourth count anchor this seam repair moves, and the one a package-scoped
run cannot see: packages/qa/dogfood cites other packages' files and is not
reached by `pnpm --filter @objectstack/rest ...` -- only the Dogfood
Regression Gate runs it.
Re-anchored, NOT relaxed: the control proves the census still reads the file
it thinks it does, and a rising enforceAuth is exactly what this card causes.
Measured +3 over the whole file (the derivation counts the bare term, comments
included): one new call site (52 -> 53) plus two prose mentions. The sibling
numbers were re-derived and did not move -- population 80, reachable 19,
registrars 17, mounts 80 -- which is what says this is a guard change and not
a surface change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UngCYXF98BVpYA9hfz6NYk
…-view-environment-ownership
# Conflicts:
#	content/docs/permissions/system-context.mdx
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xlteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] GET /api/v1/ui/view/:object/:type resolves a UI view with no identity resolution and no anonymous-deny at the REST seam

2 participants

@os-steve@claude