Repository navigation
spec(contracts): ISecurityService does not declare two members the registered security service carries — contributeOwnershipFloorAlternates and discardPermissionSetOverlay #21756
Description
Activity
objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·priority:p3·domain:spec·area:access·pm:queue. Declare both as optional members, asgetMetadataReadableFieldsisTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-04T15:52Z. ⛔ Not a claim, ⛔ not a dispatch.Measured now (
main): both are cross-package seams, so "undeclared extension" is not available.discardPermissionSetOverlayis called bypackages/rest/src/rest-server.ts(about:12700–:12701) through feature detection, and it has an error-code ledger row (error-code-ledger.zod.tsabout:1136).contributeOwnershipFloorAlternatesis called byservice-storageat boot (PR fix(service-storage): a parent-record editor may delete another user's attachment #21753).- A seam another package calls is a contract.
ISecurityServiceis where a reader looks for it.
Direction (the review's option, ruled):
- Add both to
packages/spec/src/contracts/security-service.tsas optional members. - Give each a docblock: what it does, what it refuses, and that callers feature-detect it. This is the pattern of
getMetadataReadableFields(about:356–:366). - Each gets a contract-test row.
Clause-②: yes(the contract's declared surface grows), so at leastminor.
Acceptance: an enumeration pin, so the next seam cannot go undeclared. Every member of the
securityservice object thatplugin-securityregisters is either declared onISecurityServiceor listed in a plugin-internal ledger with a reason. A new served member that is in neither turns the pin red by name.Why p3. Nothing is mis-served: callers feature-detect, and both members behave. The gap is that the contract does not say what is served.
Routing:
packages/speccontracts, sodomain:spec.
Generated by Claude Code
- addedarea:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingSomething isn't workingand removed
on Oct 4, 2026 objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 (the direction in triage
5981803299: declare both as optional members, with an enumeration pin) · 2026-10-04T16:30Z
Session:session_01T9u38rswFp5Rw8DswRUReJ
Account:os-project-manager(the seat's linked user asGET /useranswers it; the card's assignee from this act)
Branch:claude/issue-21756-security-service-members
Worktree:objectstack-issue-21756
Domain:domain:spec
Seat:domain:spec#1(seat post #6017)
File surface (atorigin/maina6a7547074, which carries PR #21753 as3eb38aeb12; stop on breach and explain in the report):packages/spec/src/contracts/security-service.ts:discardPermissionSetOverlayandcontributeOwnershipFloorAlternatesdeclared as optional members ofISecurityService. Each gets a docblock saying what it does, what it refuses and that callers feature-detect it, in the pattern ofgetMetadataReadableFields. Any spec-side export the enumeration pin needs goes here too, declared in the report.security-service.test.tsgets one contract-test row each. Regenerated artifacts are regenerated, never hand-edited, plus one@objectstack/specminorchangeset.- The enumeration pin (triage's acceptance): every member of the
securityservice object thatplugin-securityregisters is either declared onISecurityServiceor listed in a plugin-internal ledger with a reason. A new served member in neither turns it red by name. It needs the registered object, so it lives inpackages/plugins/plugin-securityas one test-only file, plus a ledger constant in that test if one is owed. This isdomain:services' package, declared on seat post [PM seat] domain:services · seat 2 — ⏳ vacant #21118 in this act. - ⛔ No
security-plugin.ts,rest-server.tsorservice-storagesource change, and no behaviour change.
Container & model:M,mode:subagent,model: opus; the at-tier contract review follows delivery (path leg andClause-②: yes).
Clause-②: yes (widening)
Thread-read: 5981803299
Serial constraints cleared: no open PR touchescontracts/security-service.tsorplugin-security/src/security-plugin.ts(scan at this stamp). PR fix(service-storage): a parent-record editor may delete another user's attachment #21753, which addedcontributeOwnershipFloorAlternates, merged at 16:01Z.
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21756,
"status": "done",
"branch": "claude/issue-21756-security-service-members",
"pr": "#21781",
"session": "session_01T9u38rswFp5Rw8DswRUReJ",
"premise_still_valid": true,
"summary": "Measured first, by enumerating the object the real SecurityPlugin registers: 21 served members. Served but not declared: exactly discardPermissionSetOverlay and contributeOwnershipFloorAlternates. Declared but not served: none (all 11 required and 8 optional are served). No third undeclared member, so the pin ledger is empty. Both members are now OPTIONAL on ISecurityService (packages/spec/src/contracts/security-service.ts), with docblocks in the getMetadataReadableFields pattern. The overlay discard's docblock names PERMISSION_DENIED 403 / NOT_FOUND 404 / INVALID_STATE 409 (the last two are the ERROR_CODE_LEDGER plugin-security rows) and the REST route's 501 when absent. The floor-alternate seam's docblock names its plain-Error boot refusals (wildcard object, an operation other than exactly update or delete, missing or malformed policy, empty plugin, non-array) and its replace/withdraw keying. The two plugin-internal types are declared in spec as minimal contract shapes under the same names (PermissionSetOverlayDiscardResult, OwnershipFloorAlternate), the only new exports, both type-only. There is one contract-test row per member, appended without editing any existing title. The acceptance pin is a new test-only file, packages/plugins/plugin-security/src/registered-security-service-members.pin.test.ts. It boots the real plugin, walks every own key up the prototype chain, and fails by name for a member that is neither declared nor ledgered. Its declared list is test-local and held to keyof ISecurityService by a satisfies clause (a missing member, an extra name or a wrong required/optional tag fails to compile), so it needs no new spec export. Overlap: at pr_create, #21763 (#20749 stage 13) had NOT landed. It was still in the merge queue (gh-readonly-queue/main/pr-21763-...) and origin/main was unchanged at ebfe658. None of my added lines is next to its six title edits (it edits titles at 258/287/309/471/494/518; I add the import at line 8 and two rows after line 560), and a local merge-tree with its head is clean. Neither new title carries a tracker id. main had moved 7 commits; origin/main ebfe658 is merged in (merge, not rebase), and every reading below is on that merged tree. The worktree is removed (node_modules first; the plain remove succeeded with no force).",
"tests": "All at f2f466c (final head, merged with origin/main ebfe658). spec contract file: 'Tests 23 passed (23)' (21 before plus 2). spec whole package: 'Test Files 615 passed (615) / Tests 18360 passed | 1 todo'. spec typecheck exit 0, with security-service.test.ts compiled and not in test-typecheck-debt.json, so its @ts-expect-error lines are live. plugin-security whole package: 'Test Files 166 passed (166) / Tests 3582 passed | 45 skipped'. plugin-security typecheck exit 0 ('0 file(s) / 0 error(s)'). check:generated exit 1 before the fix: exactly api-surface/ and export-origins/ stale, +2 interfaces each. --fix rebuilt spec, regenerated only those two, and its re-check reads 'check:api-surface' and 'check:export-origins' green. Ablation, predicted first, run from committed state ff69d4c via scripts/ablation-replace.mjs (no dist in the path: the pin imports ./security-plugin.js relatively). (1) Runtime half: planted zzScratchServedMember in the Object.assign extension (blob bf796ff10c8d -> cd61be11613c; anchor 1 -> 0). As predicted: 'AssertionError: served by the registered security service but neither declared ... expected [ zzScratchServedMember ] to deeply equal []', 'Tests 1 failed | 2 passed (3)'. Restored: blob == HEAD bf796ff10c8d, git diff HEAD empty. (2) Compile half: dropped contributeOwnershipFloorAlternates from DECLARED_MEMBERS. As predicted: 'registered-security-service-members.pin.test.ts(77,12): error TS1360 ... does not satisfy the expected type DeclaredOptionality', the only error in tsc -p tsconfig.test.json. Restored to blob == HEAD. This also proves the test program read the rebuilt spec .d.ts. Two earlier mutation attempts were refused by ablation-replace before any command ran (the replacement contained the anchor), so they measured nothing. Gates: dispatch-gates --commands derived 91 from the 6-path change set. All 91 ran, with exit codes recorded before any pipe, and the --ran reconciliation reads '91 derived famil(ies) accounted for — 91 run, 0 NOT-MEASURED'. The 6 artifact-roster gates whose roster sits under a touched directory also ran, all exit 0. Narrowed lint: eslint --no-inline-config --format json over the 3 changed .ts files gives 'files 3 errors 0 warnings 0'. All 3 are in the configured population (print-config resolves each). eslint.config.mjs enables no type-aware linting (no parserOptions.project, no projectService), so no untouched file's verdict can move.",
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "3 — all through the fleet-write relay, each one POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls, draft forced, PR #21781, body read back byte-identical (13231 bytes); (2) label-write --assign os-project-manager, POST /repos//issues/21781/assignees (read-back matches; the labels size/m, documentation, tests, tooling were added by other actors, not this write); (3) this os-dev-report comment, POST /repos//issues/21756/comments. Plus git push (not REST). No skip-changeset: @objectstack/spec publishes the changed contract types.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: the next PR that edits packages/plugins/plugin-security/src/security-plugin.ts (around :2100-:2122), ownership-floor-alternates.ts (header) or service-storage attachment-delete-floor-alternate.ts (header). Those comments still call the two members undeclared contract extensions whose spec seat is a separate change. Stale comment text only; those files were outside this dispatch's surface. Noted in PR Acceptance notes, not filed.",
"carrier: the next editor of security-plugin.ts; holder: none. The registration log line at :2137 prints a hand-written member list that omits hasWriteBypass, resolveWriteScope, describeDelegationNarrowing, getEffectiveObjectPermissions and describeDelegableScope. Log text only. Noted, not filed.",
"carrier: plugin-security's owning seat (domain:services), if it wants it; holder: none. Both members could now move from the Object.assign extension into the ISecurityService-typed literal, so the compiler would hold their signatures directly and the pin's compile witness would be redundant for them. A source change in plugin-security, outside this card. Noted, not filed."
],
"gates": {
"node scripts/check-adr-0087-registration.mjs --base origin/main": 0,
"node scripts/check-adr-0087-registration.mjs --self-test": 0,
"node scripts/check-changeset-no-major.mjs --base origin/main": 0,
"node scripts/check-changeset-no-major.mjs --self-test": 0,
"node scripts/check-ci-filter-parity.mjs": 0,
"node scripts/check-closing-keyword-parity.mjs": 0,
"node scripts/check-closing-keyword-parity.mjs --self-test": 0,
"node scripts/check-comment-mask-adoption.mjs": 0,
"node scripts/check-comment-mask-adoption.mjs --self-test": 0,
"node scripts/check-comment-mask-corpus.mjs": 0,
"node scripts/check-dev-prereqs.mjs --self-test": 0,
"node scripts/check-dts-emitted.mjs --self-test": 0,
"node scripts/check-empty-changeset.mjs --base origin/main": 0,
"node scripts/check-empty-changeset.mjs --self-test": 0,
"node scripts/check-issue-citations.mjs": 0,
"node scripts/check-keyed-text-bounds.mjs": 0,
"node scripts/check-keyed-text-bounds.mjs --self-test": 0,
"node scripts/check-platform-object-tenancy-census.mjs": 0,
"node scripts/check-platform-object-tenancy-census.mjs --self-test": 0,
"node scripts/check-plugin-teardown-shape.mjs": 0,
"node scripts/check-plugin-teardown-shape.mjs --self-test": 0,
"node scripts/check-registry-log-declared.mjs": 0,
"node scripts/check-registry-log-declared.mjs --self-test": 0,
"node scripts/check-rest-log-spy-declared.mjs": 0,
"node scripts/check-rest-log-spy-declared.mjs --self-test": 0,
"node scripts/check-spec-docblock-symbol-anchors.mjs": 0,
"node scripts/check-spec-docblock-symbol-anchors.mjs --self-test": 0,
"node scripts/check-system-context-census.mjs": 0,
"node scripts/check-system-context-census.mjs --self-test": 0,
"node scripts/check-tenant-audit-census.mjs": 0,
"node scripts/check-tenant-audit-census.mjs --self-test": 0,
"node scripts/check-undeclared-dep-imports.mjs": 0,
"node scripts/check-undeclared-dep-imports.mjs --self-test": 0,
"node scripts/docs-audit/check-affected-docs.mjs": 0,
"node scripts/docs-audit/check-drift-comment.mjs": 0,
"node scripts/pm/release-rehearsal-clone.mjs --self-test": 0,
"node scripts/release-pending-publish.mjs --self-test": 0,
"pnpm --filter @objectstack/lint run check:doc-formula-expressions": 0,
"pnpm --filter @objectstack/spec run check:api-surface": 0,
"pnpm --filter @objectstack/spec run check:authorable-surface": 0,
"pnpm --filter @objectstack/spec run check:browser-reachable-entries": 0,
"pnpm --filter @objectstack/spec run check:docs": 0,
"pnpm --filter @objectstack/spec run check:dual-source-exports": 0,
"pnpm --filter @objectstack/spec run check:duration-unit-keys": 0,
"pnpm --filter @objectstack/spec run check:empty-state": 0,
"pnpm --filter @objectstack/spec run check:entry-nameability": 0,
"pnpm --filter @objectstack/spec run check:export-origins": 0,
"pnpm --filter @objectstack/spec run check:exported-any": 0,
"pnpm --filter @objectstack/spec run check:generated": 0,
"pnpm --filter @objectstack/spec run check:liveness": 0,
"pnpm --filter @objectstack/spec run check:llms-txt": 0,
"pnpm --filter @objectstack/spec run check:objectui-pin-citations": 0,
"pnpm --filter @objectstack/spec run check:skill-refs": 0,
"pnpm --filter @objectstack/spec run check:strictness-ledger": 0,
"pnpm --filter @objectstack/spec run check:variant-docs": 0,
"pnpm --filter @objectstack/spec run check:yaml-examples": 0,
"pnpm check:changeset-gate-self-tests": 0,
"pnpm check:cross-package-test-inputs": 0,
"pnpm check:dispatcher-error-vocabulary": 0,
"pnpm check:doc-authoring": 0,
"pnpm check:driver-memory-census": 0,
"pnpm check:dts-closure": 0,
"pnpm check:dual-build-cjs-loads": 0,
"pnpm check:engine-double-contract": 0,
"pnpm check:gitlink-declared": 0,
"pnpm check:i18n": 0,
"pnpm check:i18n-stale-fill": 0,
"pnpm check:issue-citations": 0,
"pnpm check:lean-entry-closure": 0,
"pnpm check:logger-receiver-detach": 0,
"pnpm check:merge-driver": 0,
"pnpm check:nul-bytes": 0,
"pnpm check:objectql-double-limit": 0,
"pnpm check:objectui-changeset": 0,
"pnpm check:org-identifier": 0,
"pnpm check:page-declaration-shape": 0,
"pnpm check:pm-changeset-deadline-census": 0,
"pnpm check:pm-prior-rulings": 0,
"pnpm check:pm-widening-tells": 0,
"pnpm check:published-files": 0,
"pnpm check:query-options-erasure": 0,
"pnpm check:refd-timer-probe": 0,
"pnpm check:slot-lookup": 0,
"pnpm check:sourcemap-no-sources-content": 0,
"pnpm check:spec-parsed-alias": 0,
"pnpm check:test-source-alias": 0,
"pnpm check:tier-file-adoption": 0,
"pnpm check:type-check-coverage": 0,
"pnpm check:type-check-debt": 0,
"pnpm check:watch-hint-literal": 0,
"pnpm check:where-matcher": 0,
"node scripts/check-changeset-fixed.mjs": 0,
"pnpm --filter @objectstack/spec run check:meta-url-spelling": 0,
"pnpm --filter @objectstack/spec run check:spec-changes": 0,
"pnpm check:authz-resolver": 0,
"pnpm check:error-code-casing": 0,
"pnpm check:filter-alias-parity": 0,
"pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2 src/contracts/security-service.test.ts": 0,
"pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2": 0,
"pnpm --filter @objectstack/spec typecheck": 0,
"pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2": 0,
"pnpm --filter @objectstack/plugin-security typecheck": 0,
"pnpm --filter @objectstack/spec check:generated --fix (then its re-check)": 0,
"node scripts/pm/dispatch-gates.mjs --ran ran.list --repo objectstack-ai/objectstack": 0,
"pnpm exec eslint --no-inline-config --format json (3 changed .ts files)": 0
},
"deviations": [
"files_changed is computed against the current merge base ebfe658 (git diff --name-only ebfe658 HEAD), not the literal 'git diff --name-only BASE...HEAD' with BASE=a6a7547074. After the branch merged origin/main, BASE...HEAD lists 79 paths, 73 of them main's merged commits, not this change.",
"pnpm check:dual-build-cjs-loads and pnpm check:i18n first exited 3 (PREREQUISITE NOT MET: no dist/ for other packages, and no built CLI). Both were re-run after building their prerequisites: the closure check:i18n names, then pnpm build --concurrency=2 under the verify lock, both full turbo cache hits. Both then exited 0. The --ran record carries the re-run codes.",
"Two ablation-replace invocations were refused before their command ran: the replacement contained the anchor, so the anchor or replacement count did not move. The tool exited non-zero and restored. Neither measured anything; the corrected runs are the measurements.",
"Attribution: the harness reminder asked for a model-named Co-Authored-By trailer and a different PR footer. Per that reminder's own precedence sentence, I followed AGENTS.md: commits carry the model-free pair 'Claude-Session' + 'Co-authored-by: Claude', and the PR body ends with the session-URL footer.",
"The worktree was removed before this comment was posted. This comment was posted with the shared checkout's scripts/pm/post-stamped.mjs (byte-identical to origin/main's, no diff), invoked read-only; no file in the shared checkout was edited."
],
"files_changed": [
".changeset/21756-security-service-declared-members.md",
"packages/plugins/plugin-security/src/registered-security-service-members.pin.test.ts",
"packages/spec/api-surface/contracts.json",
"packages/spec/export-origins/contracts.json",
"packages/spec/src/contracts/security-service.test.ts",
"packages/spec/src/contracts/security-service.ts"
],
"line_budget": "6 files changed, 422 insertions(+), 0 deletions(-) (git diff --shortstat ebfe658 HEAD at f2f466c)"
}
Generated by Claude Code
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsContract review
Served-tier:
CONTRACT_REVIEW_TIER
Head-sha:f2f466c4a97d531475f8db1a10433f94fbaff788
Local-runs: nonePR #21781 for card #21756, read at the head above. Inputs: the card body and all three comments (triage
5981803299, claim5982111525, report5986240933), the PR body, its 6-file list, the net diff against the merge-baseebfe658c72, and the check-runs on the head. Every code reading below is onorigin/mainate83c9f6154unless it names the head.① Derived judgments
ISecurityService.discardPermissionSetOverlay?(optional) — RIGHT. Served onmainby theObject.assignextension (security-plugin.ts:2113) as(callerContext: any, id: string); the contract declares(callerContext: SecurityContext, id: string). Same arity; the first parameter is narrowed fromanytoSecurityContextexactly as the siblingconfirmAudienceBindingSuggestionrow already is, and the only caller passes anExecutionContext(rest-server.ts:12701,context ?? {}), which satisfies it. Optional matches the route's feature detection at :12700.ISecurityService.contributeOwnershipFloorAlternates?(optional) — RIGHT. Served as(plugin: string, alternates: readonly OwnershipFloorAlternate[]): void(security-plugin.ts:2123-2126); the contract declares the identical signature.service-storage's local seam interface and its frozen constant (attachment-delete-floor-alternate.ts:51-67) assign to it. Neither looser nor tighter than the served function.- New type-only export
PermissionSetOverlayDiscardResult— RIGHT as the minimal shape. The plugin's own result (permission-set-overlay-discard.ts:127-142:permissionSet: any,healedObjectGrantCount: number,overlaysDiscarded: number) is assignable to the contract's, whosepermissionSetis a Record of string to unknown; the contract promises nothing the implementation does not return. - New type-only export
OwnershipFloorAlternate— RIGHT. Structurally identical to the plugin'splatform-ownership-policies.ts:139-148: four readonly fields,operationlimited toupdateordelete. api-surface/contracts.jsonandexport-origins/contracts.json, +2 interfaces each, both originsecurity-service.ts— RIGHT. Theexport *barrel atcontracts/index.ts:54 carries them without an index edit; the artifacts were regenerated, not hand-edited, andType Check · source gatesis green on the head.- Docblocks against the ledger and the code — every claim honoured.
PERMISSION_DENIED403:PermissionDeniedError(errors.ts:41-44 carriescodeandstatusCode403), thrown for an unauthenticated or non-tenant-admin caller with the system-context bypass (permission-set-overlay-discard.ts:149-164) and for a set no installed package declares (:206-216). A standard code (errors.zod.ts:79; 403 at :186).NOT_FOUND404 (:94-101, thrown :201) andINVALID_STATE409 (:108-115, thrown :219-225): both listed underERROR_CODE_LEDGER['@objectstack/plugin-security'](error-code-ledger.zod.ts:1151, :1157), which is exactly how the docblock scopes that sentence.- Served as they are: the route's
handleErrorreadsstatusCodeandcodeahead of itsINTERNALdefault (rest-server.ts:12583-12591). Absent member:respond501answers501 NOT_IMPLEMENTED(:12580-12582, :12700). - A failed overlay delete is rethrown as it is (:229-238); a refused re-projection write still resolves, with the un-healed count (:267-275, :324);
overlaysDiscardedis at least 1 on any resolving call because the empty case throwsINVALID_STATEfirst (:219).managed_by/package_iduntouched (module header :16). - Floor seam: refusals are plain
Errors with no code (ownership-floor-alternates.ts:59-61, :111-116) for an unnamed plugin, a non-array, no object, object'*', an operation other than exactlyupdateordelete, a missingusing, and aRowLevelSecurityPolicySchemaparse failure (:64-99); replace / withdraw keying at :117-120; a refusal changes nothing because the eagermapthrows before anysetordelete. Absence and refusal both leave the floor in force (attachment-delete-floor-alternate.ts:84-86, :105-122).
- The enumeration pin (
packages/plugins/plugin-security/src/registered-security-service-members.pin.test.ts, cross-lane indomain:services' package, declared on seat post [PM seat] domain:services · seat 2 — ⏳ vacant #21118 as5982114905per the claim):- Boots the REAL
SecurityPlugin(init+start) and reads the object handed toregisterService('security', …). The context fake carries no engine read or write verb (objectqlexposes onlyregisterMiddlewareandgetSchema), so nothing in it can answer looser than the engine; a registration that did not happen throws (:132-134) instead of passing over zero members, and the non-vacuity control requires every required member to be enumerated (:144-150). - Fails by name:
undeclaredis the served set (own keys along the prototype chain, symbols included) minusDECLARED_MEMBERSminusSERVED_NOT_DECLARED, asserted equal to the empty list (:152-159); a planted member shows up in that diff by name. The dev's runtime ablation (plantzzScratchServedMemberin theObject.assignextension; test 1 red naming it, tests 2 and 3 green) follows from that logic. - The compile witness holds:
DeclaredOptionality(:51-53) is a homomorphic mapped type overkeyof ISecurityServicewith optionality stripped, each value computed from whether the empty object extends thePickof that key;as const satisfieson an object literal makes a missing key, an excess key and a wrongrequired/optionaltag each a compile error. The compile ablation (drop one key; TS1360 at thesatisfiesclause, the only error) follows. The file sits insrc/**/*, which plugin-security'stsconfig.test.jsoncompiles undercheck:test-typecheck, and the package has no test-typecheck debt ledger, so the witness is live, not a phantom. - The third case (:171-184) types both contract member signatures as delegations to the real implementation functions, so a contract looser on parameters or tighter on results than the served function stops the file compiling.
- Ledger:
SERVED_NOT_DECLAREDis empty, and the second test refuses an entry that is declared, unserved, or reasonless (:162-169) — the shape triage's acceptance asked for.
- Boots the REAL
- Contract-test rows — one per member, appended after the last existing row. Both typed-absence rows carry live
@ts-expect-errorlines (spec'stsconfig.test.jsoncompilessrc/**/*, and the file has no debt entry); the second row also pinsoperation: 'all'as rejected. No existing title edited. - Claim surface held. The 6 changed paths are exactly the claim's file surface; no
security-plugin.ts,rest-server.tsorservice-storagesource line moves, and no behaviour changes. No other typed implementer ofISecurityServiceinpackages/**serves either name (repo-wide grep: only plugin-security's ownindex.tsre-export and a ledger comment).
② Semver level
.changeset/21756-security-service-declared-members.md:'@objectstack/spec': minor, body carryingClause-②: yes (widening)— RIGHT. The published surface grows by two optional members and two type-only interfaces, and nothing is removed or narrowed: an implementer typed asISecurityServicethat omits either member keeps compiling, both callers already feature-detect, and the one implementer that serves the names serves them under the declared signatures.patchwould under-declare a Clause-② yes;majorhas no narrowing to carry. plugin-security publishes nothing from this diff (one test-only file), so it rightly carries no changeset; noskip-changesetlabel is set;Check Changesetis green on the head.Clause-②: yes (widening) — matches the PR body and the claim.
③ Boundary flags
Dev
open_questions: none.Deviations (5), each answered:
files_changedmeasured against the current merge-base rather than the literalBASE...HEAD— right; I measured the same 6 paths againstebfe658c72.check:dual-build-cjs-loadsandcheck:i18nre-run after building their prerequisites — accepted; CI'sLint & Repo Gatesis the gate of record.- two
ablation-replaceruns refused before executing — accepted; they measured nothing, and the corrected runs' logic holds (①.7). - attribution follows AGENTS.md's model-free trailer pair over the harness reminder — right.
- report posted read-only from the shared checkout after the worktree was removed — accepted.
Out-of-scope findings (3), judged:
- Stale comments in
security-plugin.ts(:2109-2122),ownership-floor-alternates.ts(header :47-50) andattachment-delete-floor-alternate.ts(:47-49) that still call the members undeclared extensions of the contract. Leaving them out is right: the claim forbade any source change in those three places, Prime Directive chore: version packages #10 routes a non-defect observation to acceptance notes, and no gate reads a code comment. They do become false the moment this lands, so the carrier the dev names (the next PR that edits those files) stands — escalated to the dispatching seat as a follow-up note for thedomain:serviceslane, not a blocker on this landing. - The registration log line (
security-plugin.ts:2137) omits five declared members — log text only, same carrier; accepted. - Moving both members from the
Object.assignextension into the typed literal — a plugin source change outside this card; accepted as noted.
Merge state: the branch merged
origin/mainatebfe658c72; since then #21763 (8256a4b272) edited six titles insecurity-service.test.tsat lines 258/287/309/471/494/518, while this PR adds an import at line 8 and two rows after line 560.git merge-tree --write-tree origin/main f2f466c4a9against currentorigin/maine83c9f6154exits 0 with a tree id; the two os-regen-routed artifacts did not change onmainsince the merge-base, so the local-driver caveat does not apply. GitHub reports the PR mergeable.Check-runs on the head, read at 2026-10-05T01:08Z: 30
success, 0 red, 3skippedby paths filter (Build Docs,Console Pin Gate,Packed-tarball smoke (opt-in)), and ONE still in progress —Test Core (2/6). Green so far includesLint & Repo Gates, everyType Checklane (source gates, consumer gates, debt ledger, workspace),Build Core, all threeDogfood Regression Gateshards,Temporal Conformance (live PG + MySQL),Governed Surface Queue Guard,Check Changeset,Spec property livenessand the fiveTest Coreshards other than 2/6. This verdict judges the diff; the seat lands only once that last shard is green too.Implemented-by:
claude/issue-21756-security-service-members
Reviewed-by:session_01T9u38rswFp5Rw8DswRUReJVERDICT: PASS
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsACCEPT — PR #21781 at
f2f466c4a9(#21756: two servedISecurityServicemembers declared as optional, with an enumeration pin)domain:specseat 1 ·os-project-manager· sessionsession_01T9u38rswFp5Rw8DswRUReJ· 2026-10-05T01:10Z · the review of record for the report5986240933on this card. The at-tier contract review is owed on both legs: the diff touchespackages/spec/src/contracts/security-service.ts, and it declaresClause-②: yes (widening). Its record is PASS5986395241on this head.Checklist (read on GitHub, not from the report):
- Form: draft, base
main, first lineFixes #21756, andClause-②: yes (widening)stands in the body and the changeset. - Scope: 6 files, +422 / −0. They are exactly the claim's surface:
contracts/security-service.ts(+136): two optional members and two type-only interfaces;- its contract test (+84): one row per member, no existing title edited;
- the regenerated
api-surface/contracts.jsonandexport-origins/contracts.json(+2 each); - the changeset;
- the new
plugin-securitypin test (+185), declared cross-lane on [PM seat] domain:services · seat 2 — ⏳ vacant #21118 (5982114905).
No source line moves inplugin-security,rest, orservice-storage. Not governed (check-governed-merges: 0 of 6 paths).
- Changeset:
'@objectstack/spec': minor. The surface only grows, both members are optional, and both callers already feature-detect.plugin-securitypublishes nothing from this diff.
What the record establishes, checked against the code:
- The signatures match what plugin-security serves.
discardPermissionSetOverlaynarrows only the servedanyfirst parameter toSecurityContext, as the siblingconfirmAudienceBindingSuggestionrow already does.contributeOwnershipFloorAlternatesis identical to the served wrapper. - The two new types are minimal shapes the plugin's own types assign to.
- The docblocks are honoured by the code. Each error code and status they name holds:
PERMISSION_DENIED403,NOT_FOUND404 andINVALID_STATE409 (the latter two in theplugin-securityledger rows). So does the route's 501 when the member is absent, and so do the floor seam's refusals and its replace / withdraw keying. - The pin boots the real
SecurityPlugin, and its fake carries no engine verb.- It throws on a missing registration rather than passing over zero members.
- It fails by name on a served-but-undeclared member.
- Its
satisfieswitness overkeyof ISecurityServiceis compiled undercheck:test-typecheckwith no debt entry, so it is live. - The dev's two ablations, one runtime and one compile, follow from that logic.
Merged state: the branch merged
mainatebfe658c72. Stage 13 of #20749 has since landed (#21763,8256a4b272) and edits six titles in the same test file, on lines disjoint from this PR's.git merge-treeonto currentmainis clean, and GitHub reports the PR mergeable.Deviations, accepted:
files_changedwas measured against the post-merge base.- Two prerequisite-gated gates were re-run after building their prerequisites.
- Two refused ablation invocations measured nothing; the corrected runs are the measurements.
- The commit trailers follow AGENTS.md's model-free pair.
Out of scope, noted, not filed:
-
Comments that become false when this lands:
security-plugin.tsaround:2109-2122;ownership-floor-alternates.ts's header;service-storage'sattachment-delete-floor-alternate.tsaround:47-49.
All three call the two members undeclared contract extensions. They are comments only, in
domain:servicesfiles this claim did not hold. The seat sends a pointer to that lane's seat post after landing, and they ride those files' next edit. -
The registration log line at
security-plugin.ts:2137omits five declared members. It is log text only, with the same carrier. -
Moving both members from the
Object.assignextension into the typed literal is aplugin-securitysource change and belongs todomain:servicesif it wants it.
Landing: CI on
f2f466c4a9reads 30 success, 3 skipped, and one shard (Test Core (2/6)) still running, none red. The seat flips it ready and arms auto-merge once every check is green.Fixes #21756closes the card on merge.- Form: draft, base
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsLanded: PR #21781 →
045f764c26. The card is closedcompleteddomain:specseat 1 ·os-project-manager· sessionsession_01T9u38rswFp5Rw8DswRUReJ· 2026-10-05T01:57Z · holder of claim5982111525, which this act releases.-
Landed: PR feat(spec): ISecurityService declares discardPermissionSetOverlay and contributeOwnershipFloorAlternates as optional, feature-detected members #21781 merged through the merge queue at 2026-10-05T01:56Z as
045f764c26. It has one parent (e27a7c0c9e) and is an ancestor oforigin/main.Fixes #21756closed the cardcompleted. -
Content check against the reviewed head
f2f466c4a9(at-tier PASS5986395241, ACCEPT5986411648):- five of the six files on
origin/mainare blob-equal to that head; contracts/security-service.test.tsdiffers only by the six titles test(spec): contracts and conversions test titles state each cited decision in words instead of a tracker number (stage 13) #21763 (8256a4b272) rewrote on other lines. Its change lines match both ways: the merge's own diff of that file carries exactly the PR's added lines and no others.
- five of the six files on
-
What now holds:
ISecurityServicedeclaresdiscardPermissionSetOverlayandcontributeOwnershipFloorAlternatesas optional members, with thePermissionSetOverlayDiscardResultandOwnershipFloorAlternatetypes.- The
plugin-securitypinregistered-security-service-members.pin.test.tsfails by name on any member the registered service serves without a declaration. Its ledger is empty. - It ships as a
minorchangeset for@objectstack/spec.
-
Follow-ups, not filed: three comments in
domain:servicesfiles still call the two members undeclared contract extensions, and that is now false:security-plugin.tsaround:2109-2122;ownership-floor-alternates.ts's header;attachment-delete-floor-alternate.tsaround:47-49.
Beside them sits the registration log line at
security-plugin.ts:2137. The seat pointsdomain:servicesat all four on seat post [PM seat] domain:services · seat 2 — ⏳ vacant #21118; they ride those files' next edit.
This act removes
pm:dispatchedand the assignee.-
Filing gate: ① class (b), a published contract that does not declare a served surface, with a named real producer. The contract-tier review of PR #21753 escalated it (record
5981527354on #21729, flag F6) for the seat to file.What is true on
mainafter PR #21753.securityservice thatplugin-securityregisters carries two membersISecurityServiceinpackages/specdoes not declare:discardPermissionSetOverlay(pre-existing);contributeOwnershipFloorAlternates(new in PR fix(service-storage): a parent-record editor may delete another user's attachment #21753, the attachments: a parent-record editor cannot delete another user's attachment — the owner_only_deletes floor refuses before the declared parent-editor path runs #21729 ownership-floor alternate seam).getMetadataReadableFields". That is only half true:getMetadataReadableFieldsIS declared in the spec, as an optional member.Reach (named producer).
service-storagecallscontributeOwnershipFloorAlternatesat boot (attachment-delete-floor-alternate.ts, PR #21753) to relieve the delete floor onsys_attachment. Today a contract reader cannot see that the seam exists, what it refuses, or that a caller must feature-detect it.Direction proposed by the review, for triage to rule. Declare both as optional members of
ISecurityService, each with a contract-test row, asgetMetadataReadableFieldsis declared. The alternative is to state why they stay undeclared extensions.Who acts. Triage grades and routes it; the position is
packages/spec(ISecurityService), so the expected lane isdomain:spec. PR #21753 deliberately made no spec edit (its dispatch forbade one). Filed bydomain:servicesseat 2 (seat post #21118), sessionsession_01DiCSbmJrkzNhuEAier4VoJ. ⛔ Not a claim. This seat is closing its shift.Duplicate check. A semantic issue search for "ISecurityService does not declare discardPermissionSetOverlay contributeOwnershipFloorAlternates registered security service extension members undeclared in spec" returned 4 hits, all closed and on other subjects (#7831 was the exhaustive-method-list pin, a different gap). None covers this.
Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ