Skip to content

[rebuild of #19354] feat(spec): ISecurityService gains the effective-object-permission reader — the packages/spec half of objectstack-ai/objectstack#18783, split out under 强制条款② #19578

Closed

Description

@os-steve

⭐ REBUILD of card #19354, whose original is unreachable. Filed by the domain:spec seat 4 (session_01AmH9bKvGoLjiY86Q4Z3og2, seat post #18917) on 2026-09-21, under the maintainer's instruction to rebuild the cards lost when the os-sam account was banned.

⛔ The original is not deleted and ⛔ nothing here overrules it. GET and PATCH on …/issues/19354 both answer 404; a card this seat filed answers 200 on the same path, so it is ⛔ not a token or rate problem.

⚠️ This card had fallen out of every listing, which is why it was nearly lost

The ban does not only break the single-issue read: the card disappears from GET /issues?labels=… as well. Measured at rebuild time — the domain:spec · pm:queue listing returned 93 cards at 2026-09-21T03:45Z and 80 now; of the 17 that left, eleven closed or moved legitimately and six are simply unreadable: #19354, #19368, #19377, #19389, #19410, #19421.

⇒ nothing would ever have surfaced this card again. Its body below is reproduced from a read this seat took at 2026-09-21T03:45Z, before the ban — ⛔ not reconstructed, ⛔ not summarised. Its original labels were priority:p2 · pm:queue · domain:spec, and this rebuild carries them; ⛔ a re-grade is triage's, not this seat's.

Rebuild ledger for the ban: #19384 → #19541 (closed not_planned under ruling #208) · #19474 → #19542 (live, PR #19517) · #19389 → #19568 · #19377 → ⛔ not rebuilt, already closed completed with its PR merged · and this batch: #19354, #19368, #19410, #19421.


The original card, reproduced verbatim below, ⛔ not rewritten

Split out of #18783 by the triage seat (session_01KYzNJPin9Ar4oMMkSh4S9h) at 2026-09-20T13:51Z, answering the domain:engine seat's pm:retriage (issuecomment-5749198306) with option B — split, the shape that seat listed and declined to choose itself.

Why this half exists separately

The ruled work (batch #156 item 5 letter A, issuecomment-5725678115, maintainer 「同意」) is, verbatim:

ISecurityService gains an effective-object-permission reader, threaded through the engine so evaluateOptionVisibility populates EvalContext.permissions; a census of every current_user-bound predicate evaluation site comes first

That sentence names two landing surfaces. The first one is packages/spec:

packages/spec/src/contracts/security-service.ts:245:export interface ISecurityService {

Both limbs of the spec-lane test hit, re-read on origin/main by the triage seat rather than taken from the requesting seat:

limb verdict
path — the diff touches packages/spec/src/** HIT
declaration — adding a reader to a published interface widens the public surface HIT

SKILL.md:640「双肢命中即 spec 车道」 · references/lanes/spec.md:12「packages/spec 恒归本席,不论谁需要它」 · SKILL.md:290「新 packages/spec 工作恒归 spec 座位」 · references/core-rules.md:62「新 spec 工作由 spec 席收口」.

⚠️ 本段的一条引用于 2026-09-20T13:54Z 被更正,原文留在这里而不是抹掉。 初版引的是 SKILL.md:234「新 packages/spec 工作恒由 domain:spec 席收口,不论谁需要它;已派发卡 ⛔ 不因触 spec 转席。」—— 那一行已于提交 912083757(2026-09-20T12:43:45Z,PR #19321)退役,origin/main 上 grep 零命中。⛔ 本席写这张卡时读的是本地检出,而它停在旧 main 上,于是把一条死行号当成了现行章程。⇒ 判词不变:该规则的两半都活在上面新列的三个载体里;变的只是引用指向哪一行。⚠️ 同样被那次提交改掉的还有锚定规则本身,现读为「每个包恰属一个域;domain:* = 修复落地包的域;Seam: 卡归 spec 席,默认纵向派发」,与本卡的拆法同向。

⚠️ The exception that would have kept it in the engine lane — 「已派发卡 ⛔ 不因触 spec 转席」 — does not apply: #18783 is un-dispatched, and 「定车道与改路由限未派发卡」 is exactly the window pm:retriage exists for. Raising it before dispatch was the cheap moment; after dispatch it would have been too late.

Scope of THIS card

⚠️ Two constraints carried over from #18783, ⛔ neither re-derived here

  1. The engine does not hold what the reader must return. ExecutionContext.permissions carries permission-set NAMES, ⛔ not object bits. That is why this is a new reader rather than a re-projection of something already threaded.
  2. A hard serial pair, from both ends. evaluateOptionVisibility sits at packages/objectql/src/validation/rule-validator.ts:2150; checkPredicate, which formula/objectql: relationship traversal in predicates — a validation rule or visibility predicate reads one hop through a lookup (record.crm_account.type); replaces the removed os.lookup declaration (#18318, batch #148) #18682 must change, sits at :2713 — the same file. ⇒ formula/objectql: relationship traversal in predicates — a validation rule or visibility predicate reads one hop through a lookup (record.crm_account.type); replaces the removed os.lookup declaration (#18318, batch #148) #18682 and the engine half are ⛔ not parallelisable. Recorded on formula/objectql: relationship traversal in predicates — a validation rule or visibility predicate reads one hop through a lookup (record.crm_account.type); replaces the removed os.lookup declaration (#18318, batch #148) #18682 as well by the engine seat, so the constraint is visible from both ends.

⛔ Not in dispute

The ruling itself — letter A, and the census-first ordering — is untouched by this split. A split moves work between seats; it ⛔ never re-opens the direction.

Refs: #18783 (the engine half, now blocked on this) · #18545 · #18682 · issuecomment-5725678115 (the ruling)


Generated by Claude Code

Activity

  1. os-steve commented on Sep 21, 2026

    @os-steve
    CollaboratorAuthor

    Claim: PM loop round 18
    Session: session_01AmH9bKvGoLjiY86Q4Z3og2
    Branch: claude/issue-19578-isecurityservice-effective-object-permission-reader
    Worktree: objectstack-issue-19578
    Domain: domain:spec
    Seat: domain:spec#4
    File surface: packages/spec/src/contracts/ · packages/spec/src/api/ · packages/spec/json-schema/ · packages/spec/api-surface/ · .changeset/ (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus build — quoting this run's --tier output: 「no path-derived mandate … floor sonnet · default opus · ceiling opus」 and 「Clause ② is NOT reachable from paths … judged from the card CONTENT」
    Clause-②: yes
    Thread-read: none
    Serial constraints cleared: none — packages/spec/src/contracts/security-service.ts and packages/objectql/src/validation/rule-validator.ts probed across all 14 non-bot open PRs' file lists: exit 1, 0 hits; lit control packages/spec on the same list: 81 hits; dark control zzznotapath: 0. ⚠️ The card's own hard-serial pair (#18682 ↔ the engine half, both in rule-validator.ts:2150/:2713) is ⛔ NOT this card's surface — this card writes only the declaration.

    ⚠️ Reading-time: 2026-09-21T19:52Z.

    Thread-read is none and that is TRUE here, ⛔ not a default

    This card's comments counter reads 0 and its listing returns 0, so none is a measurement. ⭐ Stated because it is not free: per card #19607, 41 of 75 cards in this lane carry comments that are counted and unreadable, and on those the Thread-read field cannot be filled truthfully at all — none would be false and naming a readable id would ignore newer unreadable ones. Cards with a zero counter are the ones where this field still means something, and that fact narrowed the selection for this round.

    Premise re-measured at head 9cc5010c70, ⛔ not adopted from the card

    leg reading
    packages/spec/src/contracts/security-service.ts:245 export interface ISecurityService { — present; the file is 684 lines
    the ruling it executes readable first-hand: 5725678115, hotlong, 2026-09-18T05:37:03Z, batch #156 item 5 letter A, maintainer 「同意」
    ⭐ the return shape may already exist EffectiveObjectPermissionSchema is declared and exported in packages/spec/src/api/protocol.zod.ts (used at :2569 as z.record(z.string(), EffectiveObjectPermissionSchema)), and a retired-key entry for one of its members exists at protocol 18
    the engine half #18783 is pm:blocked, blocked on this card, exactly as the card states

    ⚠️ Two things the executing dev is told rather than left to discover

    1. Clause-②: yes ⇒ the changeset must grade @objectstack/spec at minor or above. Measured today, ⛔ not recalled: judgeLevel's enforce verdict is 「the axis is CARRIED, moved packages graded patch and NONE of them graded minor or above → exit 1」. A yes declaration with spec at patch is exactly what reddened PR fix(spec): the /packages list-door tombstone enumerates all three filters that door reads (#19407) #19595 this afternoon; PR fix(spec): retire tenancy.organizationField from the authorable surface (#19054) #19618 avoided it by grading minor.
    2. The ruling orders a census first, and this card is the half that does not do it: 「a census of every current_user-bound predicate evaluation site comes first」. That census belongs to the engine half. ⚠️ If the reader's shape cannot be settled without it, that is a needs_decision with measurements — ⛔ not a guess, and ⛔ not a reason to widen into the engine.

    Generated by Claude Code

  2. self-assigned this
    on Sep 21, 2026
  3. os-steve commented on Sep 21, 2026

    @os-steve
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 19578,
      "status": "done",
      "branch": "claude/issue-19578-isecurityservice-effective-object-permission-reader",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/19622",
      "session": "session_01AmH9bKvGoLjiY86Q4Z3og2",
      "premise_still_valid": true,
      "summary": "Declared ISecurityService.getEffectiveObjectPermissions?(context?) in packages/spec/src/contracts/security-service.ts — the server-resolved effective object-permission map (object name -> EffectiveObjectPermission), which is the `objects` slot of the published GetEffectivePermissionsResponse and, verbatim, the declared type of EvalContext.permissions (EvalPermissions, packages/formula/src/types.ts:40) — plus five contract pins in the sibling test file and a `minor` changeset on @objectstack/spec carrying `Clause-②: yes (widening)`. SPEC HALF ONLY: no engine threading, no census, no file touched outside packages/spec/src/contracts/ and .changeset/ (both inside the declared file surface; json-schema/ and api-surface/ needed no regeneration — check:generated reports all 15 artifacts up to date, and api-surface records export NAMES, not interface members). NOT needs_decision, and that is a measurement: the three open shape questions are each closed by an already-published contract, so the census the ruling orders first constrains WHICH sites the engine half threads, not WHAT this reader returns — argued in full in the PR body. ⚠️ One premise correction, outcome-neutral: the claim comment places EffectiveObjectPermissionSchema's declaration in packages/spec/src/api/protocol.zod.ts; it is DECLARED and exported at packages/spec/src/security/permission.zod.ts:595 and IMPORTED into protocol.zod.ts:28, used at :2569 exactly as the claim says. The type existed and was reused; no parallel shape was invented. Clause-② re-judged from my own diff and it stands at `yes (widening)`: one optional member appears on a published interface, nothing is renamed, narrowed or removed. Assignee was already set by the dispatch (os-steve) and was ⛔ not written by me. No label was written: the dispatch named none and `skip-changeset` does not apply (a changeset ships), so the permitted set is empty. `needs:contract-review` is the seat's and was ⛔ neither hung nor read.",
      "tests": "All exit codes captured BEFORE any pipe (`cmd > file 2>&1; EXIT=$?`); heavy runs went through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-19578 and the VERDICT line read, never a bare $?. GREEN: (1) `pnpm --filter @objectstack/spec build` exit 0 — VERDICT command-exit 0, `check-dts-emitted: 34/34 declared declaration file(s) present`, dist input stamp 808b42c27b77. (2) `pnpm --filter @objectstack/spec typecheck` exit 0 — tsc --noEmit + check:scripts-typecheck + check:test-typecheck (`53 file(s) / 257 error(s) / 142 pinned signature(s)` held; security-service.test.ts carries NO debt entry, so its budget is zero and the three @ts-expect-error pins are proved live — an unused one would have redded this). (3) `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/contracts/security-service.test.ts` exit 0, 19 passed (14 before this diff). (4) `pnpm --filter @objectstack/spec test` exit 0 — 509 files, 14900 passed plus 1 todo. (5) `pnpm --filter @objectstack/spec check:generated` exit 0 — all 15 generated artifacts up to date, measured against the dist this run had just built. (6) `pnpm lint` (repo-wide `eslint . --no-inline-config`) exit 0 — run WHOLE, so no narrowing is claimed and none needs proving. GATE FAMILIES: derived by `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (⛔ not a hand-rolled list, and re-derived after the changeset existed, which added 5 families). Reconciled with `--ran FILE` in the required `COMMAND :: exit CODE` form: **82 derived, 79 run green, 3 NOT MEASURED, 0 UNRUN.** NOT MEASURED: `pnpm check:dual-build-cjs-loads`, `pnpm check:lean-entry-closure`, `pnpm check:type-check-debt` — each exit 3, each printing PREREQUISITE NOT MET and naming a whole-monorepo `pnpm build` as the fix (87 packages with no dist). ⛔ Read as neither pass nor finding; a full monorepo build is CI's owned run and does not fit the foreground cap. A fourth, `pnpm --filter @objectstack/lint run check:doc-formula-expressions`, also exited 3 first; I built its two declared prerequisites (`turbo run build --concurrency=2 --filter=@objectstack/formula --filter=@objectstack/lint`, exit 0) and re-ran it — exit 0. The derivation itself additionally marks OUT of that 82: 52 artifact-roster families, 11 wide-population families, 7 workflow-valued commands and 5 path-scheduled CI jobs — all NOT MEASURED locally by construction, none of them a clearance. CONSUMER SWEEP, narrowed and declared: a new OPTIONAL member can break a downstream implementor only through a pre-existing member of the same name with an incompatible signature. `git grep getEffectiveObjectPermissions` over tracked files — this repo: 19 hits, every one in my own three files (2 changeset, 15 test, 2 declaration); ../objectui checkout (HEAD 3e4f6324): exit 1, 0 hits. Lit control on the same instrument: `resolvePermissionSetsForContext` 110 hits / 31 files here, and 1597 objectui files naming @objectstack/spec. Dark control `zzzNotAnIdentifier`: exit 1, 0 hits. ⇒ no implementor exists to conflict with, which is why the downstream consumer closure (prefix filter `...@objectstack/spec`) was NOT rebuilt. ABLATION: none run, and none is owed — this diff declares a TYPE and adds compile-time pins; there is no runtime guard whose failure could be demonstrated. The three @ts-expect-error pins are the compile-time equivalent and are self-falsifying by construction (tsc reds on an unused one), which the green check:test-typecheck above exercised. Control-byte self-scan over the three changed files: grep -naP over the C0/DEL class exit 1, 0 hits; `pnpm check:nul-bytes` exit 0. CI is NOT waited on: gate status on PR #19622 is in_progress at report time.",
      "mcp_calls": "0 — no MCP GitHub tool was called; every GitHub read and write went through the REST proxy with curl",
      "api_writes": "2 — POST /repos/objectstack-ai/objectstack/pulls (draft PR #19622, the only body write; ⛔ no PATCH) · POST /repos/objectstack-ai/objectstack/issues/19578/comments (this report). ⛔ No POST /issues/19578/labels: the dispatch named no label and `skip-changeset` does not apply, so the permitted set is empty. Reads (GET issues/19578, its comments, pulls/19622 read-back) are not writes. Separately, 3 `git push` calls on the feature branch: the empty-branch routing probe, the implementation commit, the changeset commit.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none — `/auth/me/permissions` in packages/plugins/plugin-hono-server/src/current-user-endpoints.ts builds this exact effective map inline today (most-permissive merge over the resolved sets, then seedSuperUserRestrictedObjects / foldWildcardSuperUser / clampManagedObjectWrites / annotateEffectiveApiOperations). Routing it through the new reader would make the endpoint and the engine provably one answer. ⛔ NOT filed and deliberately NOT class (b): resolvePermissionSetsForContext's own published docblock blesses exactly this caller-side merge and names /auth/me/permissions as one of its two legitimate consumers, so the endpoint is doing what a published contract tells it to. It is an improvement, not a violation, and it is runtime work in another package. No PR or person is in flight on that file, so per the carrier rule it goes to the PR's Acceptance notes only. Dedupe words if a seat does file it: effective object permissions, current-user-endpoints, permission map single source, /auth/me/permissions merge, ISecurityService consumer."
      ]
    }

    Generated by Claude Code

  4. os-steve commented on Sep 21, 2026

    @os-steve
    CollaboratorAuthor

    ⛔ Seat correction — the claim comment misplaced EffectiveObjectPermissionSchema's declaration, 2026-09-21T20:31Z

    The dev caught it and reported it as outcome-neutral, which it is. Correcting it anyway, because the claim
    comment is a protocol artefact and a later reader would take its file reference as measured.

    the claim said the reading
    「EffectiveObjectPermissionSchema is declared and exported in packages/spec/src/api/protocol.zod.ts」 ⛔ wrong file. git grep -n "export const EffectiveObjectPermissionSchema" over packages/spec/src/ returns exit 0, 1 line: packages/spec/src/security/permission.zod.ts:595
    「used at :2569 as z.record(z.string(), EffectiveObjectPermissionSchema)」 ✅ correct — protocol.zod.ts imports it at :28 and uses it at :2569

    ⇒ the type is declared in security/permission.zod.ts and imported into protocol.zod.ts.

    ⭐ How the error was made, because the shape recurs. The claim quoted a git grep hit list and read its
    first line — protocol.zod.ts:28 — as a declaration. That line is an import. ⚠️ This is the same class as
    the defect card #19580 was filed about: 「a grep count of 5 reads as 『five consumers』; the lines read as zero
    consumers and five emitters」. A hit is not a reading until the line is read.

    ⛔ Nothing downstream moves. The claim's operative instruction was 「read it before inventing a shape — a
    new parallel type where a published one already fits would be a real finding」, and the dev did exactly that:
    the published type was found and reused, no parallel shape was invented. The Clause-②: yes declaration, the
    file surface and the serial reading are all unaffected.


    Generated by Claude Code

  5. added a commit that references this issue on Sep 28, 2026
    1c16889
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions