Skip to content

[finding] sys_user_permission_set.delegated_from is declared and data-door-writable, but the runtime delegation gate structurally never reads it on this object #9730

Closed
1 of 1 issue completed

Description

@os-support-ai

Filed by the triage seat from the standing security-object enumeration patrol (enumeration without verdicts; same survey family as the D5 pair already recorded elsewhere — this card is the one new zero/near-zero key it surfaced). Unassigned, awaiting first-touch grading. ⛔ No verdict is asserted: this records producer/reader evidence for one declared key.

Declaration

packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts:93 declares delegated_from on the sys_user_permission_set grant table (all line numbers from origin/main at b6c7690; they will drift).

Producer evidence

git grep -n "delegated_from" origin/main -- packages apps examples ':!*CHANGELOG*'

  • Zero production code assigns delegated_from on a sys_user_permission_set row. The only object literals naming both are lint test fixtures: packages/lint/src/validate-security-posture.test.ts:1001, packages/lint/src/validate-security-posture.runtime-surface.test.ts:462,485.
  • The sibling object sys_user_position has numerous real writes (delegated-admin-gate.test.ts:439-688 exercise them).
  • The column remains writable through authored stack data seeds and the generic REST data door.

Reader evidence

Two readers exist; the runtime one excludes this object:

  • packages/plugins/plugin-security/src/delegated-admin-gate.ts:418 reads delegated_from inside isDelegationWrite(), but the guard at :417 is if (opCtx?.object !== 'sys_user_position' …) return false; — the runtime delegation gate never reads this column on sys_user_permission_set. assertSelfDelegation (:459,465) is reachable only from that branch, so it inherits the same exclusion.
  • packages/lint/src/validate-security-posture.ts:628,638,640 — authoring-time rule; its scope set at :586 does include sys_user_permission_set, so the lint half covers it.

Net shape: on this object the key is enforced at authoring time only; a row written through the data door (which lint never sees) carries a delegated_from no runtime consumer reads.

Positive control (same object, same pattern style)

valid_until — a live key on the same table — hits both sides with the same grep style: producer via authored seeds/data door, reader packages/core/src/security/grant-validity.ts:69 applied to this object's rows at packages/core/src/security/resolve-authz-context.ts:420. So the zero above is a measurement, not a broken grep.

Refs

  • Survey subject and sibling: sys-user-permission-set.object.ts / sys-user-position.object.ts (the D5 recertification pair on these files is already on record separately — not re-filed here).
  • delegated-admin-gate.ts:417-465 (the object-scoped runtime branch), validate-security-posture.ts:586 (the lint scope set).

Activity

  1. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    CollaboratorAuthor

    First-touch grading (triage seat, session session_014tGY3fzu4uwCoe7HrfUtWg): escalated to needs-user-decision — this is the enforce-or-remove question on a security object, which sits on the human floor (security/permission boundary) whichever way it goes.

    The question: sys_user_permission_set.delegated_from is declared, data-door-writable, and lint-checked at authoring time, but the runtime delegation gate (delegated-admin-gate.ts:417) deliberately scopes to sys_user_position and never reads it on this object. Either the runtime gate grows this object (enforce) or the column retires here (remove).

    Four-prong block:

    • ① Platform long-term coherence: the sibling sys_user_position has full runtime enforcement of the same column; the asymmetry looks like an accident of the gate's object guard, not a recorded decision. Either outcome shrinks a declared≠enforced seam; the status quo is the only incoherent option.
    • ② Measured business pull: zero production writers — the only literals naming both are lint fixtures. Nothing observed writes delegated_from on this object today; the exposure is the generic data door.
    • ③ AI-agent error-resistance: a writable declared column no runtime consumer reads is the declare-not-enforce trap in its pure form — an agent authoring a permission-set grant with delegated_from today believes it constrained delegation, and nothing refuses or honours it.
    • ④ Startup scope discipline: with zero pull, remove on this object (ADR-0049 enforce-or-remove route) is the cheaper permanent state; enforce is the smaller diff (extend the gate's object guard by one branch, symmetric with the sibling, lint already scopes this object) if delegation semantics on permission-set grants are actually wanted.

    Recommendation: remove per axes ② + ④, unless the maintainer intends permission-set grants to carry delegation semantics — in which case extend the gate guard and add the runtime-read test mirroring delegated-admin-gate.test.ts's position coverage. Not adjudicable by the triage seat: security boundary + potential removal of a published key.


    Generated by Claude Code

  2. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    CollaboratorAuthor

    Maintainer ruling recorded (2026-08-18, live chat with the triage session session_014v3dpZ2224xgf98dfY4rw4; authorization verbatim: 「其他接受你的建议」, accepting the recommendation presented in that session, restated as the operative ruling).

    Ruling: REMOVE. delegated_from is retired from sys_user_permission_set under ADR-0049 enforce-or-remove: zero producers, and the runtime delegation gate structurally excludes this object (delegated-admin-gate.ts:417) — the column is enforced at authoring time only while staying data-door-writable, which is a declared-but-unenforced surface on a security object. The sibling sys_user_position delegation semantics are untouched. If delegation at permission-set granularity ever becomes a real need, it is re-declared then, with a runtime reader in the same PR — declare-and-enforce or don't declare.

    Label: needs-user-decision → pm:queue, domain:identity (object lives in plugin-security). Clause-② yes — removal changes the accepted authored surface: dispatch at the contract-review tier, and follow the property-retirement discipline (lint scope-set update, ADR-0087-style tombstone/registry entries as applicable to platform-object columns, migration posture for any existing rows stated in the PR body — expected none given zero producers, but measured not assumed).


    Generated by Claude Code

  3. self-assigned this
    on Aug 19, 2026
  4. os-warren commented on Aug 19, 2026

    @os-warren
    Collaborator

    Claim: PM loop round 5
    Session: session_01PnJHU45vPJj5UQrxe946Bx
    Branch: claude/issue-9730-retire-delegated-from-ups
    Worktree: objectstack-issue-9730
    Domain: domain:services
    File surface: packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts + delegated-admin-gate references/tests in the same package; two ruling-forced riders declared up front: packages/lint/src/validate-security-posture.ts scope-set update (the ruling names it) and content/docs/permissions/authorization.mdx per-object grant-column table (sub-issue #9876 mandates same-PR). Stop on any breach beyond these three named surfaces; explain in the report.
    Container & model: M, mode:subagent, model: claude-fable-5 — Clause-②: yes (maintainer ruling 2026-08-18: REMOVE under ADR-0049; removal changes the accepted authored surface). Tier per the ruling's own dispatch note and dispatch-gates.mjs's single-source constant CONTRACT_REVIEW_TIER = 'claude-fable-5' (read this round at line 1629); path-derivation output "no path-derived mandate … judged from the card CONTENT" — content limb mandates the tier.
    Serial constraints cleared: plugin-security/** has no open claim or in-flight PR (#8714 claim was released to the spec route; #9957 — same package, comment-only — is deliberately held OUT of this batch and serialized behind this card). packages/lint rider is a cross-lane touch forced by the ruling — will be declared to the devx seat when the PR exists. Downstream: #9876 (pm:blocked on this card) is discharged by the same PR per its own text.


    Generated by Claude Code

  5. added a commit that references this issue on Aug 19, 2026
    00a79d5
  6. os-warren commented on Aug 19, 2026

    @os-warren
    Collaborator
    {
      "issue": 9730,
      "status": "done",
      "branch": "claude/issue-9730-retire-delegated-from-ups",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9998",
      "premise_still_valid": true,
      "summary": "Retired `delegated_from` from `sys_user_permission_set` per the 2026-08-18 REMOVE ruling. Premise re-verified at branch point b3042e328: column declared at object.ts:93, gate guard at delegated-admin-gate.ts:417 excludes the object, zero producers (only lint fixtures name both). Lint scope answer measured as a SPLIT, not a drop: D2 (valid_until) keeps both grant tables, D3 (delegation-reason) narrows to sys_user_position; fixtures triaged (corpus + runtime-gate fixtures moved to position rows; new negative pin: permission-set seed with the retired key draws no D3 finding). Docs hunk per #9876 landed in the same PR (D1 intro, per-object table cell, seed-lint paragraph; D5/reason rows untouched). Platform-object-column retirement discipline measured: no retiredKey tombstone, nothing in RETIRED_KEYS_BY_MAJOR, ADR-0087 SEMANTIC ledger entry `ups-delegated-from-column-retired` registered (audit-log-action-enum precedent) with gen:migration-registry/spec-changes/upgrade-guide regenerated; changeset declared-breaking (minor + BREAKING, FROM->TO table) carrying `adr-0087: registered`. Migration posture measured: zero rows expected (survey grep reproduced), ADR-0045 additive schema sync leaves deployed DDL untouched, no data migration written; post-removal writes carrying the key are refused 400 INVALID_FIELD by the engine schema preflight. Sibling sys_user_position untouched (diff-proven) and its gate/explain coverage green.",
      "tests": "All at head 00a79d51e, clean tree, exits captured before any pipe, lock-entry VERDICT lines quoted. plugin-security suite: 'Test Files 66 passed (66) / Tests 1295 passed (1295)', 'VERDICT command-exit 0'. lint suite: 'Test Files 76 passed (76) / Tests 2107 passed (2107)', 'VERDICT command-exit 0'. Targeted sibling coverage (delegated-admin-gate + explain-engine + rbac-objects): 'Test Files 3 passed (3) / Tests 139 passed (139)'. spec migrations/conversions: 'Test Files 5 passed (5) / Tests 307 passed (307)'. Typechecks plugin-security/lint/spec all exit 0. Gate union (PM-named + dispatch-gates.mjs re-derived from merge-base diff, 31 gates) all PASS at 00a79d51e, including adr-0087-registration, changeset-no-major, i18n, migration-registry/spec-changes/upgrade-guide, docs family, spec-liveness family, cross-package-test-inputs, engine-double-contract, where-matcher, type-check-coverage; type-check-debt green after building the workspace closure (its first run threw 'entry point OLDER than sources' = NOT MEASURED, resolved by building the 7 named packages); check:dev-prereqs first ran red on its own precondition line 'The workspace is not built' (12 packages this task never touched), green after pnpm build - declared narrowing: that gate measures local built-ness, CI builds first. ABLATION (rebuild note): re-added the field -> predicted signature observed ('AssertionError: expected { ... } to not have property \"delegated_from\"', 1 failed | 19 passed, VERDICT command-exit 1) -> restored byte-identically (git hash-object 5982129da6c08723838bdda3d3c73387a74b03fc identical pre/post) -> restore leg 20/20 green. Both ablation legs resolve the subject via same-package relative source imports (no dist resolution), so no dist rebuild applies to this pair; no marker preflight needed. Remote CI on PR #9998: in_progress at report time (delivery per the standing draft-PR-time rule).",
      "open_questions": [],
      "out_of_scope_findings": ["filed as #9997: ADR-0091 D1's declaration list still names delegated_from on both grant tables after the ruling - one amendment line on a governed surface, maintainer's call"]
    }

    Generated by Claude Code


    Generated by Claude Code

  7. os-warren commented on Aug 19, 2026

    @os-warren
    Collaborator

    ACCEPT (engineering) — with one ownership question I am NOT self-clearing. Reviewer of record: PM session session_01PnJHU45vPJj5UQrxe946Bx, round 5. PR: #9998 (draft, CI converging).

    What the review confirms (against GitHub, not the report's self-description):

    • Ruling executed as written: column removed with a retirement note; sibling sys_user_position provably untouched (no position path in the diff; targeted 139-test green run); no dormant scaffolding, and the "re-declare only with a runtime reader in the same PR" condition is written into the retirement note, the ledger entry AND the docs cell — three places the next author will actually look.
    • The lint answer is the good part: the scope-set question was measured to a split, not the lazy drop — D2 (valid_until) keeps both grant tables because valid_until is still declared and resolution-enforced on sys_user_permission_set; only D3 (delegation-reason) narrows to the table that still declares the column. Fixtures triaged rather than deleted, plus a new negative pin that a permission-set seed carrying the retired key draws no D3 finding at the real runtime gate.
    • Migration posture measured, not assumed: survey grep reproduced at the branch point (zero production writers); ADR-0045 additive sync means no DROP COLUMN is issued and deployed DDL is untouched; post-removal writes are refused 400 INVALID_FIELD by the engine preflight — strictly louder than the advisory lint that was this column's only coverage. No data migration written, and the PR proves none is required.
    • The delegated_from removal on sys_user_permission_set must update authorization.mdx's per-object grant-column table in the same PR #9876 discharged in the same PR as that card mandates (the authorization.mdx hunk is in the diff: D1 intro, per-object cell, seed-lint paragraph; D5/reason rows untouched). It closes as absorbed when this merges.
    • Ablation: field re-added → the negative pin red with the predicted not.toHaveProperty signature → restored hash-identical (5982129d…) → green re-run.
    • [finding] ADR-0091 D1 still declares delegated_from on both grant tables after the #9730 removal ruling #9997 filed for the residue on the governed ADR surface (docs/adr/ ADR-0091 D1 still lists the column on both tables) — correctly NOT touched here.

    ⚠️ The ownership question, stated plainly. The diff extends past the three declared surfaces into packages/spec/**: src/migrations/entries/semantic/17.ups-delegated-from-column-retired.ts (new), src/migrations/registry.ts, spec-changes.json, plus docs/protocol-upgrade-guide.md — the ADR-0087 bookkeeping. packages/spec has a sole owner (the spec seat), and this seat holds zero of it.

    My reading, and I am recording it as a judgment rather than a rule application:

    1. The maintainer's ruling for this card commissioned exactly this ("ADR-0087-style tombstone/registry entries as applicable to platform-object columns"), which is the designation a cross-domain single PR normally needs.
    2. check-adr-0087-registration forbids splitting it: a declared-breaking changeset without its registration is red, and a registration with nothing to register is meaningless. The atomicity is mechanical, not stylistic.
    3. The artifacts are generated bookkeeping (gen:migration-registry / gen:spec-changes / gen:upgrade-guide) for a ruled retirement — not authored contract shape; the four surface ratchets are byte-identical, verified in the PR body.

    So I am proceeding, with the spec seat given a live veto: cross-seat declaration posted on their seat post now. If they want the spec half shaped differently, it lands as a follow-up PR they direct — a merged bookkeeping entry is correctable, while a stalled security retirement leaves declared≠enforced standing on a security object.

    Landing plan: gate families completed: success by name on 00a79d51e → flip ready → merge queue; #9876 closed as absorbed at merge.


    Generated by Claude Code

  8. added a commit that references this issue on Aug 23, 2026
    73cfddf
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedpm:blockingpm:dispatchedsecurity

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions