Repository navigation
refactor(plugin-sharing,plugin-audit): annotate enforcement on the full ExecutionContext and delete the double-casts (#7136) - #7140
Conversation
…ll ExecutionContext (#7136) Consumer half of #6523. The contracts converged onto the complete resolveAuthzContext envelope (#6206 ruling: no per-site subset contracts); the implementations behind them still named the six-field SharingExecutionContext, so nothing they could read had widened. - 27 enforcement parameters across SharingService, SharingRuleService and plugin-audit's comment-access gates now declare ExecutionContext, plus the two return types that produce the contexts feeding them. - exec-context-seam.testkit.ts returns the resolved envelope with no cast, deleting the `as unknown as` double cast on the value tests are meant to trust. - SharingRuleService's SYSTEM_CTX is typed as the envelope and passed as itself, retiring `SYSTEM_CTX as any` at all 11 call sites. - `(context as any).userId` / `.tenantId` in SharingService now read declared fields. Kept and documented in place: __readScope / __writeScope (middleware-private keys, not envelope fields) and organizationId (#5858 / check:org-identifier, held out of #7070 on purpose). No runtime behaviour changes — the values were always complete, the damage was type-side. Parameters only widen what they accept. Because a re-narrowed annotation would compile, ship and pass every test, exec-context-annotation.pin.ts pins the convergence at compile time via excess-property checks on envelope-only fields, with negatives so a parameter erased to `any` cannot pass either. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BM1tNf5U3nEbHKR4fo5qVQ
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 2 package(s): 9 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also reference the affected code. These are read-only:
|
The measured count was taken after the new doc comment — which itself quotes the string `SYSTEM_CTX as any` — had been inserted, so the doc comment counted itself. On origin/main the erasure appears at exactly 10 call sites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BM1tNf5U3nEbHKR4fo5qVQ
|
PM review — PASS. Marked ready and enqueued ( The evidence, re-measured by this seat rather than read off the reportThis card had no user-visible defect and no compiler pressure, so a green typecheck proves nothing — it was green before the branch existed. The load-bearing readings, each re-run here against
The specimen the card names — Reverse verification — the honest shape, accepted as suchThe template's before-green/after-red does not fit a type-only change, and the author did not force it. Prediction stated first: reverting should leave every runtime test green and only the new compile-time pin should move. Measured: typecheck red on exactly the six lines the pin was written to fail on (4× TS2353 on the positives, 2× TS2339 on the seam read, exit 2), and the same reverted tree still passed 410 + 137 tests. No TS2578, so all four negative cases stayed satisfied in both states. That is the card's own premise measured rather than asserted: 547 runtime tests cannot distinguish this change from its reversal. Which is precisely why The
|
…_comment sharing gates (objectstack-ai#7141) (objectstack-ai#7143) * fix(plugin-audit): forward the caller's execution envelope to the sys_comment sharing gates (objectstack-ai#7141) `callerContext()` rebuilt a five-field projection of the caller's `ExecutionContext` before handing it to `ISharingService.canEdit`, whose contract declares the full envelope and whose doc block forbids callers from rebuilding a subset of it (objectstack-ai#6523 / the objectstack-ai#6206 ruling). objectstack-ai#7136 (PR objectstack-ai#7140) widened the return annotation; this is the deferred body change. The projection was doing two jobs. Stripping the middleware-private keys is correct and is preserved — plugin-security stamps the access DEPTH resolved for `sys_comment` onto the context in place, while these gates ask the sharing service about the PARENT record's object — but it is now done by the `__` PREFIX, which also covers the engine's other operation-private markers (`__expandRead` waives object-level CRUD) and cannot go stale. Stripping the principal fields was the defect: `onBehalfOf` (which `ISecurityService.hasWriteBypass` reads to fail CLOSED on a delegated context) and `principalKind` (which `resolvePermissionSetsForContext` reads to cap an ADR-0090 D10 agent at its consented ceiling instead of appending the additive human baseline), plus `systemPermissions`, `accessible_org_ids`, `posture`, `audience` and `rlsMembership`. No access depth is synthesised for the parent object: absent depth leaves the owner-match at its narrowest (`own`), byte-for-byte what the projection produced. The read side's parent probe gets the same envelope-minus-private-keys treatment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BM1tNf5U3nEbHKR4fo5qVQ * test(plugin-audit): type the canEdit spy so tsc can read its call tuple (objectstack-ai#7141) `vi.fn(async () => true)` gives the mock a zero-length parameter tuple, so `calls[0]![2]` is a TS2493 under `tsc --noEmit` even though vitest records the argument at runtime. Declare the three parameters the contract passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BM1tNf5U3nEbHKR4fo5qVQ --------- Co-authored-by: os-zhuang <steve@objectstack.ai> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…Context` type (objectstack-ai#7218) (objectstack-ai#7325) * refactor(spec,plugin-sharing)!: retire the exported `SharingExecutionContext` type (objectstack-ai#7218) The deferred deletion recorded when objectstack-ai#7070 split the convergence in two. objectstack-ai#6523 / PR objectstack-ai#7068 converged 36 contract signatures onto the full `resolveAuthzContext` envelope (the objectstack-ai#6206 ruling: enforcement adjudicates on the whole envelope, never a per-site subset); PR objectstack-ai#7140 (identity: plugin-sharing, plugin-audit) and PR objectstack-ai#7206 (services: plugin-approvals, plugin-reports) re-annotated the implementations. Both halves landed with the type still exported because it is DEFINED in `packages/spec`, whose retirement is the spec seat's to make. Nothing declares it any more, so it goes — from `@objectstack/spec` and from `@objectstack/plugin-sharing`, which re-exported it. Public-surface removal on two packages, so both generated snapshots are regenerated (`api-surface/contracts.json`, `export-origins/contracts.json`), and the changeset carries the callout plus an ADR-0087 D3 semantic entry (`sharing-execution-context-retired`): a published TypeScript surface with no spec schema has no `retiredKey()` tombstone and no parse rejection, so the ledger is the only channel that reaches an upgrader. The pins needed the deliberate pass, not a find-and-delete. All three `exec-context-annotation.pin.ts` files told their failure story as "the parameter narrows back to `SharingExecutionContext`" — a type the deletion removes, while the failure mode it named survives: the six fields can be re-declared under any name, and the fresh-literal excess-property checks fire only on the fields a given literal happens to spell. Each pin now keeps the retired shape as a local, non-exported SPECIMEN and refutes every enforcement parameter against it by type identity, so a re-narrowing is red twice over and neither check depends on the retired export coming back. `packages/spec/src/contracts/sharing-service.test.ts` is re-anchored the same way, and its "twin unchanged in shape" case becomes "twin stays retired". The narrative the retired type's doc block carried — the measured `(context as any).posture` specimen, and why tsc cannot police narrowing at all — moves to the module doc of `contracts/sharing-service`, which the contracts and all three pins now point at. No runtime behaviour changes: the type was erased at compile time and no signature's accepted shape moved. Claude-Session: https://claude.ai/code/session_01ArkxxyvoyPaDhHiX5KhQBi Co-authored-by: os-zhuang <steve@objectstack.ai> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * chore(spec): regenerate ledger artifacts after merging main The textual merge kept this branch's side of `spec-changes.json` and `docs/protocol-upgrade-guide.md`, which predate PR objectstack-ai#7322's `action-global-nav-location-removed` conversion. Regenerated from the merged registry so both that entry and this branch's `sharing-execution-context-retired` semantic entry project into the artifacts. Generated files, never hand-merged. Claude-Session: https://claude.ai/code/session_01ArkxxyvoyPaDhHiX5KhQBi Co-authored-by: os-zhuang <steve@objectstack.ai> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: os-zhuang <steve@objectstack.ai>
Fixes #7136
Consumer half of #6523 / PR #7068, identity lane of the #7070 split. The contract half converged 36 signatures onto the full
ExecutionContext(the #6206 ruling: enforcement adjudicates on the wholeresolveAuthzContextenvelope, never a per-site subset). The implementations behind those contracts still annotated their own parameters withSharingExecutionContext, the six-field shape the contracts used to name — so nothing they could actually read had widened, and the casts that narrowness forced were all still there.What changed
SharingExecutionContextrefs, before → afterpackages/plugins/plugin-sharing/src/sharing-service.tspackages/plugins/plugin-sharing/src/sharing-rule-service.tspackages/plugins/plugin-sharing/src/exec-context-seam.testkit.tspackages/plugins/plugin-audit/src/comment-access-hooks.ts27 enforcement parameters now declare
ExecutionContext, plus the two return types that produce the contexts feeding them. The casts that go with them:exec-context-seam.testkit.ts:106— the specimen this card names. The helper resolved a REALresolveAuthzContextenvelope and then forced it into the narrow type:return { ...authz, isSystem: false } as unknown as SharingExecutionContext;. It now returns what it resolved. That matters beyond tidiness: this seam exists ([#5852 producer 半边]resolveOwnerScopeIds按权威字段填充 hierarchy resolver context + 补「resolver 收到的 org 非空」断言 #5859) so a drift in the resolver's output breaks the tests that trust it, and a double cast on that value is the seam lying about itself.SYSTEM_CTX as anyat all 10 of its call sites insharing-rule-service.ts— the system context is typed as the envelope and passed as itself. An erasure on an enforcement input switches checking off for the whole argument, not just for the readonly-array mismatch that provoked it.(context as any)reads ofuserId/tenantId— 3 insharing-service.ts, 3 insharing-rule-service.ts— now reading declared fields.Two cast families are deliberately kept, and are now documented where they sit rather than left looking like oversights:
__readScope/__writeScopeare not fields ofExecutionContext. They are private keys plugin-security's middleware stamps onto the context it forwards (sc.__readScope = …,security-plugin.ts). Widening the annotation does not make these deletable, and the fix is not to declare them on the envelope — that would publish a middleware seam as authorable, client-supplied vocabulary.organizationIdis not on the envelope at all. That spelling has its own history ([#5852 契约半边]HierarchyScopeContext未声明organizationId/tenantId哪个权威 —— producer 只填一个、consumer 只读另一个,两边都「符合契约」 #5858 /check:org-identifier) and was held out of finding: consumer half of #6523 — three plugin implementations still annotateSharingExecutionContext, so(context as any).postureis still needed to read a field the contract now declares #7070 on purpose, so the three reads of it stay cast. Only thetenantIdhalf of those expressions lost its cast, which makes the asymmetry the visible marker of which of the two names the contract actually knows.plugin-audit'scallerContextgets the widened annotation but keeps its five-field projection — see "deliberately not done" below.Verification — and why a green typecheck is not it
This card has no user-visible defect and no compiler pressure in either direction: the values were always complete at runtime (this family's damage was type-side), and PR #7068 already measured the dependents-direction typecheck green. The typecheck passed before this branch existed, so it is a regression check here and not evidence. The evidence is the cast counts and the pin.
Measured on this branch point (
0fd855624, not the card's445a0c2— the four per-file counts re-measured identical).SharingExecutionContextreferences, before → after (grep -cover the four files): 21 → 0, 8 → 0, 3 → 0, 2 → 0.Context casts, before → after (
grep -oF | wc -lsummed over the same four files):as unknown as SharingExecutionContextSYSTEM_CTX as any(context as any)— total occurrencesThe 6 surviving code reads are exactly the two families held out on purpose:
__readScope/__writeScope(3) andorganizationId(3). The 6 deleted ones were alluserId/tenantId— fields the envelope declares.Gates (regression checks, not evidence of the change):
The mechanical guard, and whether it earns its weight
Yes — because without it nothing detects a regression. A re-narrowed annotation compiles, ships and passes every test in these packages, which is exactly why this card had to be queued deliberately rather than discovered.
packages/plugins/plugin-sharing/src/exec-context-annotation.pin.tshands each enforcement parameter a fresh object literal naming envelope-only fields (posture,accessible_org_ids,org_user_ids). TypeScript's excess-property check rejects those the moment a parameter narrows back. That is the only direction available: a@ts-expect-errorasserting the reverse would be unsatisfied and fail the build, because a narrow context IS structurally assignable to a wide parameter — the boundarySharingExecutionContext's own doc block already records.It also carries four negative cases, because a parameter erased to
anywould swallow every positive just as happily.posture: 'SUPERUSER',userId: 42,accessible_org_ids: 'org_a'andorganizationId:must all still fail.It is a
.pin.ts, not a*.test.ts, and that is load-bearing:packages/plugins/plugin-sharing/tsconfig.jsonexcludes**/*.test.ts(a measuredTEST_DEBTof 3 inscripts/check-type-check-coverage.mjs), sotsc --noEmitnever reads this package's test files. A pin written there would be a phantom check that stays green however it is broken — AGENTS.md'sPINS_CHECKEDtrap, the same hole #6212 measured on driver-mongodb. Same convention as the existingpackages/objectql/src/register-object-authored-shape.pin.ts, and imported by nothing, so tsup (entrysrc/index.ts) never bundles it.Reverse verification
Prediction, recorded before running it: this change has no runtime behaviour, so reverting it would leave the entire suite green — the honest expectation here is not a red — while the pin, and only the pin, would go red. Both halves measured, by taking the fix out with
git checkout origin/main --on the four files and keepingexec-context-annotation.pin.ts:So: 547 runtime tests cannot tell this change from its own reversal, which is the card's premise measured rather than asserted, and the reason a fabricated red would have been the wrong thing to report. The pin fails on all six lines it was written to fail on. No
@ts-expect-errorbecame unsatisfied under the revert (tsc reports no TS2578), so the negative cases keep asserting in both states rather than turning into noise.Deliberately not done
plugin-approvalsandplugin-reports— the services-lane half (Re-annotate plugin-approvals + plugin-reports onto the fullExecutionContextand delete the now-redundant casts (services half of #7070) #7135), a different seat and a disjoint file set.packages/servicesis untouched.SharingExecutionContexttype stays. Its removal rides whichever of the two split halves lands second; Re-annotate plugin-approvals + plugin-reports onto the fullExecutionContextand delete the now-redundant casts (services half of #7070) #7135 is still open andpm:queueas of this PR, and finding:SharingExecutionContext是同族第四个窄 enforcement 契约类型(sharing / approval / report 三个服务共用),#6206 裁决的「不留 per-site 子集」默认尚未覆盖它 #6523 left the type exported precisely to keep those files compiling. Deleting it here would break the sibling half mid-flight. The coordination point (including theplugin-sharing/src/index.tsre-export, which is a public-surface removal) is recorded on Re-annotate plugin-approvals + plugin-reports onto the fullExecutionContextand delete the now-redundant casts (services half of #7070) #7135 for whichever seat lands second.callerContext's five-field projection inplugin-audit— filed as plugin-audit'scallerContextrebuilds a 5-field subset of the execution envelope, droppingonBehalfOfbefore the sharing gates that are documented to fail closed on it #7141. Widening its annotation is type-side and inert; forwardingexecwhole is a RUNTIME change with a real enforcement consequence (it dropsonBehalfOfbefore a probe documented to fail closed on it, and forwarding it whole would leak one object's access depth into another object's owner-match), so it is filed rather than folded in.Commits
Two, deliberately not squashed: the change, then a correction to a number in it. The
SYSTEM_CTXcast count was first written as 11 because the measurement was taken after the new doc comment — which itself quotes the stringSYSTEM_CTX as any— had been inserted, so the comment counted itself. Onorigin/mainthe erasure appears at exactly 10 call sites.Generated by Claude Code