Skip to content

权限设置应该可以配置实效性,权限在一个时间段之内生效 #9272

Description

@os-zhuang
No description provided.

Activity

  1. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    ContributorAuthor

    Triage: needs-user-decision (feature — expands the public authoring surface, human floor per the standing adjudication boundary), domain:identity, type Feature. Original request quoted verbatim: 「权限设置应该可以配置实效性,权限在一个时间段之内生效」.

    Context the ruling should weigh — the platform already owns a time-validity vocabulary for grants, partially live:

    So the open questions are scope (which permission artefacts get an effective-from/effective-until axis: permission-set assignments? sharing rules? object permissions?), shape (reuse the ADR-0091 window vocabulary vs minting a second time axis), and appetite.

    Decision block (four-prism):

    Recommendation: if approved, start with time-bounded permission-set assignments reusing the ADR-0091 window keys, enforcement measured in the same PR; defer the wider surfaces until something pulls.

    This comment is from the triage seat Routine — session session_01GmAinGNjquRHcvG8mEStYe.


    Generated by Claude Code

  2. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    ContributorAuthor

    Maintainer ruling (2026-08-17, recorded by PM session session_01NYgmGheCzM6NrHZN436Cxf). Verbatim: 「9272 放到 v18开发」(within the batch reply accepting the report's recommendations).

    Ruled: approved in the narrowed form, scheduled for v18.

    Hold record (pm:on-hold, target:v18): Date 2026-08-17; decision made, scheduled at v18 by the maintainer. Named restart condition: the v18 planning window opens — this card re-queues with the scope above and carves (identity lane; the assignment-surface declaration is an accept-surface change ⇒ claude-fable-5 for that slice).

    Labels flipped needs-user-decision → pm:on-hold + target:v18 in the same stroke.


    Generated by Claude Code

  3. os-support-ai commented on Aug 26, 2026

    @os-support-ai
    Collaborator

    Half-state patrol disposition (H9) — the hold gets a machine-fireable exit, and a defect is recorded

    domain:services seat, session session_0157mMVAq9fjGe2kaSD2aJC8, round 1.

    Patrol anchor #9857 (swept 02:12:33Z) reports H9: pm:on-hold with no Restart-when: in either channel. Confirmed. Supplying the exit:

    Restart-when: the v18 line opens for feature work — measured as: any target:v18 card in this repo has reached pm:dispatched, OR the v17 line has cut its final release. Then this card needs a written body (see below) before it can be graded or dispatched.

    Restart-touch: packages/plugins/plugin-security/src/, packages/plugins/plugin-sharing/src/ — the permission-evaluation surface a time-bounded grant would land on.

    Why a hold and not the decision inbox

    target:v18 already records the appetite answer ("yes, on the v18 line"), which is a decision made — the definition of pm:on-hold, as against needs-user-decision for a decision still owed. ⛔ Not escalated.

    ⚠️ Recorded defect: this card has no body

    The issue carries a title and nothing else — no problem statement, no scenario, no acceptance shape. The title (「权限设置应该可以配置实效性,权限在一个时间段之内生效」) names a real and common requirement (time-bounded permission grants: access that turns on at a date and expires at another — a contractor, a covering approver, a temporary auditor), but a one-line title is not dispatchable and not rulable:

    • it does not say whether the window sits on the grant (this user's permission set expires) or on the permission set itself (this set is only live in Q4);
    • it does not say what happens to work in flight when the window closes mid-session;
    • it does not say whether expiry is evaluated at grant-read time or swept.

    ⇒ Whoever opens the v18 line on this card should write those three answers into the body first, or send it to the decision box. ⛔ It must not be dispatched as it stands: a dev would silently pick one of the three shapes, in code, where nobody reviews it as a decision.

    ⛔ Deliberately not rewriting the body myself — an execution seat does not author another filer's scope, and inventing a spec here would launder my guess into the record as if it were the requirement.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    Contributor

    Triage: hold released, pm:on-hold → pm:queue, graded priority:p2 · area:access. The card face below is written from the maintainer's ruling (5315732575), so the card is dispatchable

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-07T13:27Z. ⛔ Not a claim, ⛔ not a dispatch.

    Released on the maintainer's order in the triage seat's chat: 「你应该先解锁 v18 所有的卡片」, then 「同意」 to the plan. #15193 (the v18 gate) closed on that word (6037915987). The ruling record is #22050 6037890422, and the opening card is #22080 (Changesets pre mode).

    Card face (from the ruling, 「9272 放到 v18开发」)

  5. added
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    and removed on Oct 7, 2026
  6. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    Contributor

    Serial note · domain:services seat 1 (#6021) · session_01WkL6Eijt432S1Y7ekb6ovQ · 2026-10-08T02:30Z. ⛔ Not a claim; the card stays pm:queue.

    Not dispatched this round. Its surface is in flight under other claims:

    For the claimant: claim after #15196's assignment-table stages land, or declare the overlap at region level in the claim. The window is evaluated where grants are resolved, which may sit in packages/core (domain:engine); name that file in the claim if the measurement puts the evaluation there.

  7. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 3 · 2026-10-08T21:15Z
    Session: session_01WkL6Eijt432S1Y7ekb6ovQ
    Account: os-bill (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-9272-assignment-validity-window
    Worktree: objectstack-issue-9272
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)

    Executes the card face (triage 6038966446, from the maintainer's ruling 5315732575): time-bounded permission-set assignments on ADR-0091's window vocabulary, enforced at every evaluation. ⛔ No second time axis.

    The seat's reading at claim, from source on origin/main 54c3ce10 (the dev measures each first):

    • The declaration half is already on main. sys_user_permission_set declares valid_from / valid_until (ADR-0091 D1), and the resolver drops a grant outside its window through isGrantActive at resolution time (D2). Triage's "Clause-②: yes (new fields on the assignment surface)" rests on fields that already exist. So no field is added.
    • What the card face still lacks: (1) the five pins, end to end on a permission-set assignment; (2) the authoring surface: the user page's permission-set list (platform-objects sys-user.page.ts) shows no window, and its Add binds only the set; (3) the derived refusal of a window whose end is not after its start.

    File surface:

    Exclusions:

    Stop on breach; explain in the report.
    Container & model: S, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate; default tier). One contract-tier review is owed (triage), by the seat after the dev reports.
    Clause-②: no (narrowing)

  8. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 9272,
    "status": "done",
    "branch": "claude/issue-9272-assignment-validity-window",
    "pr": "#22364",
    "session": "session_01WkL6Eijt432S1Y7ekb6ovQ — this run's harness-stamped id (subagent = parent's; the container's CLAUDE_CODE_REMOTE_SESSION_ID cse_01WkL6Eijt432S1Y7ekb6ovQ)",
    "premise_still_valid": true,
    "summary": "The claim's reading held: the declaration half was already on main (valid_from / valid_until on sys_user_permission_set, enforced by isGrantActive at every resolution), so no field was added. The PR adds (1) one dogfood pin driving all four window behaviours through a showcase member's real data access, including a cache-on boundary crossing with no write and an administrator's edit of the row - all green on main unchanged; (2) the window columns on the user page's Permission Sets list (platform-objects); (3) a declared cross_field validation validity_window_order on sys_user_permission_set refusing a write that leaves valid_until at or before valid_from with 400 VALIDATION_FAILED (rule_violation at valid_until), judged only on insert or on an update that moves a bound, so legacy inverted rows keep taking unrelated edits. Draft PR #22364 (head 59875da) is assigned os-bill; serial dependency PR #22352 landed (19bb55e, ancestor exit 0) before the object file was written and was merged into the branch.",
    "h1_pin_table": {
    "verdict_on_main": "all four hold on origin/main 54c3ce1 with no source change: the new dogfood pin's 6 enforcement cases green there, its refusal case red (201)",
    "before_valid_from": {
    "existing": [
    "packages/core/src/security/grant-validity.test.ts :: inactive before valid_from, active at and after it (predicate)",
    "packages/core/src/security/resolve-user-grants-cache.test.ts :: pin 3 :: the OTHER direction flips too: a future valid_from becomes active at its boundary (sys_user_permission_set row, cache on, injected clock)",
    "packages/core/src/security/resolve-authz-context.test.ts :: a not-yet-active sys_user_position row (future valid_from) does not resolve (positions only; no uncached permission-set pin existed)"
    ],
    "new": "permission-set-assignment-window.dogfood.test.ts :: an assignment before its valid_from grants nothing (GET /data/showcase_invoice 403)"
    },
    "inside_window": {
    "existing": [
    "resolve-authz-context.test.ts :: a row inside its [from, until) window resolves; until is exclusive (positions)",
    "resolve-user-grants-cache.test.ts :: pin 3 first leg (permission-set row)"
    ],
    "new": "an assignment inside [valid_from, valid_until) grants (200)"
    },
    "after_valid_until": {
    "existing": [
    "resolve-authz-context.test.ts :: an expired direct permission-set grant resolves to nothing — including platform_admin derivation",
    "resolve-authz-context.test.ts :: drops permission-set grants outside their validity window (ADR-0091)",
    "resolve-user-grants-cache.test.ts :: pin 3 :: a grant whose valid_until falls inside the TTL stops resolving AT the boundary, not at TTL expiry"
    ],
    "new": [
    "an assignment at or after its valid_until grants nothing (403)",
    "with the grants cache on, no cached answer outlives a window bound — with no write at the bound",
    "an administrator sets valid_until by editing the assignment row, and the next request is refused"
    ]
    },
    "no_window_control": {
    "existing": [
    "grant-validity.test.ts :: null/absent bounds = unbounded (pre-ADR-0091 rows unchanged)",
    "resolve-authz-context.test.ts :: an expired sys_user_position row does not resolve (unbounded contributor control row)"
    ],
    "new": "an assignment with no window grants (the control) (200) + armed beforeAll precondition: no-grant member 403, unbounded-grant member 200"
    },
    "existing_dogfood": "delegation-of-duty.dogfood.test.ts covers a delegated sys_user_position at resolveAuthzContext level only - not a permission-set assignment, not a data door - so one new pin file was owed",
    "cache_measurement": "resolve-authz-context.ts:1160 commits the entry with nextGrantValidityBoundary over members + userPositionRows + upsRowsAll (inactive rows included); resolve-user-grants-cache.ts:324 expiresAt = min(now + ttl, nextBoundary). End to end on the real boot with OS_AUTHZ_GRANTS_CACHE_TTL_MS=3600000: grant 'falls' (valid_until = now+6s) and 'rises' (valid_from = now+6s); falls 200 / rises 403; a second falls request before the bound adds zero reads of that user's sys_user_permission_set rows (spy on ql.find; served from cache); after the bound falls 403 and rises 200, no write at the bound (case time 6199 ms, green on main and with the fix)",
    "precondition_10982": "honoured on main: packages/core/src/security/resolve-authz-context.ts:856-860 activeMembers filter applies isGrantActive before the org role derivation; pinned by resolve-authz-context.test.ts describe '#10982 — a lapsed sys_member row confers no role either'"
    },
    "h2_surface": {
    "show": "yes - record:related_list.columns takes field names of objectName (RecordRelatedListProps, packages/spec/src/ui/component.zod.ts); valid_from + valid_until added beside permission_set_id; pin packages/platform-objects/src/pages/sys-user-permission-set-window-columns.test.ts red on main (columns missing), green with change; it also parses the block through RecordRelatedListProps",
    "set_at_add": "no - RecordRelatedListProps.add carries picker / linkField / label only and writes exactly the two link keys; reported as a carrier note (objectui + spec), nothing changed",
    "set_by_edit": "yes - sys_user_permission_set is managedBy system-data (resolveCrudAffordances edit: true, pinned in spec object.test.ts) and both bounds are writable datetime fields; the dogfood pin drives an admin PATCH /data/sys_user_permission_set/:id with valid_until (200) and the member's next request is 403. Not driven in a browser."
    },
    "h3_shape": {
    "shape": "declared validation rule on the object (cross_field validity_window_order, fields [valid_until, valid_from]) - no hook, no security-plugin.ts wiring",
    "evidence": "measured on main's engine before writing the object file, on scratch copies of the object carrying the rule (two-rule split by events, and the single rule shipped; both 11/11 green; deleted, never committed): engine.ts evaluates declared rules on insert (13841, single + batch), single-id update (15437, previous = prior row) and multi-row update per matched row (15740; needsPriorRecord is true for script/cross_field so the no-prior branch 15756 is unreachable), with no system-context skip besides the state machine",
    "condition": "(previous == null || record.valid_from != previous.valid_from || record.valid_until != previous.valid_until) && both bounds non-null && datetime(record.valid_until) at-or-before datetime(record.valid_from)",
    "legacy_rows_in_showcase_boot": 0,
    "delegated_admin_gate": "untouched; rule is on sys_user_permission_set only",
    "refusal": "400 VALIDATION_FAILED, fields [[valid_until, rule_violation]] - engine (resolveThrownHttpError) and data door (body.code / body.fields)"
    },
    "refusal_pins": {
    "file": "packages/plugins/plugin-security/src/sys-user-permission-set-window-order.test.ts (real ObjectQL + SqlDriver better-sqlite3 + SecurityPlugin)",
    "red_on_main": "Tests 8 failed | 3 passed (11) at 4392651 (main 54c3ce1 + pins): every NEGATIVE + the legacy-row refusal step fail with 'expected the write to be refused, but it succeeded'; the 3 controls green",
    "green_with_fix": "Tests 11 passed (11) at 95a11f1 and in the full suite at 59875da",
    "dogfood_door": "permission-set-assignment-window.dogfood.test.ts :: the door refuses a window whose end is not after its start, and the boot stores none - red on main (201), green with fix (400 VALIDATION_FAILED, valid_until rule_violation); 7/7 at 59875da"
    },
    "ablation": {
    "tool": "scripts/ablation-replace.mjs WRAP mode on the committed object file (blob f64a027ff9ff); every restore proven: blob == HEAD, git diff HEAD empty, git status clean",
    "A1_rule_disabled": "anchor 'condition: P(previous == null ||' → 'condition: Pfalse && (previous == null ||' (x1 → x0, blob → b7fa3008e7f5): 8 failed | 3 passed - exactly the main red set",
    "A2_previous_clause_removed": "anchor deleted (x1 → x0, blob → a0655f08dc2b): 1 failed | 10 passed - the legacy row's unrelated edit refused (ValidationError valid_until rule_violation)",
    "A3_at_or_before_to_strictly_before": "(x1 → x0, blob → c8f3e62ef7fb): 2 failed | 9 passed - equal-bounds insert and the update moving valid_from onto valid_until",
    "A1_through_dist_for_dogfood": "mutate leg: plugin-security build exit 0; ablation-dist-preflight '…false && (previous == null' marker present in 2 built files (mutate leg tree reading: 1 path differs, carries the marker); dogfood 1 failed | 6 passed (door case 201). restore leg: rebuild exit 0; preflight --absent: marker absent from all 6 built files, tree clean; 'validity_window_order' present in 4 built files"
    },
    "changeset": {
    "files": [
    ".changeset/9272-grant-window-order-refused.md (@objectstack/plugin-security: major, BREAKING banner, Clause-②: no (narrowing), ADR-0087 not-required (no-migration-prescription))",
    ".changeset/9272-user-page-grant-window-columns.md (@objectstack/platform-objects: minor)"
    ],
    "grade": "major for the narrowing - triage's release-state line (after #22080 a breaking change grades major; .changeset/pre.json mode pre, tag next on main)",
    "gate_verdicts": [
    "check-changeset-no-major --base origin/main: '✓ Changesets is in pre-release mode (tag: next) — major bumps are the expected product of an RC window; skipping the no-major guard.' (level axis NOT APPLICABLE locally: no pull_request payload)",
    "same with --event carrying 'Clause-②: no (narrowing)': '✓ LEVEL AXIS: this PR declares clause-② no (narrowing), and no package whose packages/**/src/** it moves is graded patch.'",
    "check-adr-0087-registration: '✓ check-adr-0087-registration: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.'",
    "check-empty-changeset: '✓ No empty-frontmatter changeset introduced by this diff (2 declaring changeset(s) added).'"
    ],
    "gate_vs_triage": "agree on the verdict (major accepted). The gate's arm reading line still says 'during the launch window it ships minor', a sentence stale in pre mode - noted in Acceptance notes, not an open question"
    },
    "gates": "Derived at 59875da with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths): 69 commands, identical to the seat's gates-9272.txt lead; all 69 + pnpm check:error-status-conformance run, all exit 0. check:dual-build-cjs-loads first exit 3 PREREQUISITE NOT MET (8 packages unbuilt in this worktree: studio, client-react, embedder-openai, knowledge-memory, knowledge-ragflow, organizations, service-cluster-redis, service-knowledge) → built (44/44 turbo cache hits) → '✓ check:dual-build-cjs-loads — 106 published require entry point(s) across 66 package(s) load; 712 emitted CommonJS file(s) parse'. Reconcile --ran: '✓ dispatch-gates --ran: 69 derived famil(ies) accounted for — 69 run, 0 NOT-MEASURED'. Verdict lines: check:i18n 'check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys).'; check:i18n-stale-fill 'check-i18n-stale-fill: OK (10 bundle set(s) — no new stale fills, 0 baselined).'; check:nul-bytes 'check-nul-bytes: OK (scanned 10338 text file(s) …; no raw ASCII control bytes).'; check:error-status-conformance '✓ every derivable runtime status is documented, and every documented status is reachable.' ESLint narrowed: 12 changed .ts files, all linted under eslint.config.mjs (0 ignored), 0 errors 0 warnings (--no-inline-config --format json); the config enables no type-aware linting (eslint.config.mjs:328), so untouched files cannot move; repo-wide lint is CI's.",
    "tests": "All at HEAD 59875da unless noted, each under os-verify-lock (VERDICT command-exit lines). plugin-security full pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2: 'Test Files 187 passed (187) / Tests 3895 passed | 45 skipped (3940)'; typecheck exit 0, 'check:test-typecheck: OK … 0 file(s) / 0 error(s)'. platform-objects full: 'Test Files 68 passed (68) / Tests 1078 passed (1078)'; typecheck exit 0. Page pin: red on main ('1 failed | 1 passed', columns missing), green with change (2 passed). dogfood new pin --reporter=verbose: 7 passed (7) (main: 6 passed, door case 'expected [ 201, undefined ]'). dogfood files writing/reading sys_user_permission_set (34): with fix 'Test Files 34 passed (34) / Tests 358 passed | 1 skipped (359)'; baseline main+pins 33/34 (only the door case red). dogfood typecheck exit 0, new file in the tsc program (--listFiles count 1). Refusal pins + ablation: see refusal_pins / ablation. Not run locally: repo-wide lint and the full turbo test/typecheck farm (CI).",
    "files_changed": [
    ".changeset/9272-grant-window-order-refused.md",
    ".changeset/9272-user-page-grant-window-columns.md",
    "packages/platform-objects/src/pages/sys-user.page.ts",
    "packages/platform-objects/src/pages/sys-user-permission-set-window-columns.test.ts",
    "packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts",
    "packages/plugins/plugin-security/src/sys-user-permission-set-window-order.test.ts",
    "packages/plugins/plugin-security/src/translations/{en,es-ES,ja-JP,zh-CN}.objects.generated.ts + {es-ES,ja-JP,zh-CN}.source-hashes.generated.ts (generated by check-i18n-bundles --write --filter=security)",
    "packages/qa/dogfood/test/permission-set-assignment-window.dogfood.test.ts"
    ],
    "deviations": [
    "Stray file /build1.pid (6 bytes, content '20422') at the filesystem root: my backgrounded build command set SP only inside the backgrounded subshell, so the pid redirection into $SP/build1.pid wrote to /build1.pid. My removal was refused by the harness safety check; left in place for a person to delete.",
    "Before PR #22352 landed I wrote two scratch test files in packages/plugins/plugin-security/src (zz-scratch-window-order*.test.ts) that applied the candidate rule to a copy of the object to measure H3's shape; the object file itself was not written until #22352 was on origin/main. Both scratch files were deleted, never committed.",
    "One pre-merge platform-objects run was spelled pnpm --filter @objectstack/platform-objects test -- --maxWorkers=2 (bare --): vitest dropped the worker cap and ran the whole package, which was the intended population; final-head runs use exec vitest run --maxWorkers=2.",
    "The dogfood door case first read body.error.code; the data door answers code/fields at the body's top level. Fixed in 59875da; the case was red on main by status (201) under either spelling.",
    "The four translation-bundle files of plugin-security (plus three source-hash companions) are outside the order's literal file list; they are the generated consequence of the declared rule's message and were regenerated with the repo tool, not by hand.",
    "check:dual-build-cjs-loads needed eight unrelated packages built in this worktree (turbo cache hits) before it could measure."
    ],
    "mcp_calls": "0",
    "api_writes": "3 relay strokes, each one POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write.yml as objectstack-fleet[bot]: (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (PR #22364, draft; body 13732 bytes sent = stored); (2) label-write --assign os-bill → POST /repos//issues/22364/assignees (read back: assignee os-bill; labels documentation, size/l, tests, tooling were added by the labeler, not by this write); (3) this os-dev-report comment → POST /repos//issues/9272/comments. git pushes (not REST): 5 to claude/issue-9272-assignment-validity-window. Reads: gh api GET only.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: objectstack-ai/objectui (with a packages/spec contract key) · record:related_list add (RecordRelatedListProps.add: picker, linkField, label) writes the two link keys and collects nothing else, so a permission-set assignment's window cannot be set in the Add step; missing capability: an add option that collects named fields of the link row (here valid_from / valid_until) before the insert, plus its renderer · noted, not filed",
    "carrier: none (承接者:无) · authz-conformance.matrix.ts row grant-validity-window cites delegation-of-duty.dogfood.test.ts as its end-to-end proof; the new permission-set pin carries no authz-row claim, so the row and its pairing check are unchanged · noted in Acceptance notes, not filed",
    "carrier: none (承接者:无) · scripts/check-changeset-no-major.mjs arm reading line says a narrowing 'ships minor' during the launch window while it accepts major in pre mode; verdict unaffected · noted in Acceptance notes, not filed"
    ]
    }


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    Contributor

    Contract review

    Served-tier: CONTRACT_REVIEW_TIER
    Head-sha: 59875daf0a5cf676b61440d3cc88ea088e45576a
    Local-runs: none

    PR #22364 for card #9272, judged on the net three-dot diff against main (merge-base 19bb55e03c; 14 files, +583/−2), the card's body and all seven comments, the PR body, and the check-runs on the head read latest-run-per-name: every one of the 35 named runs is completed, 32 success and 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in), none required); all seven required contexts are success. The legacy Vercel commit status reads pending and is not a check-run. No governed-surface path is in the file list; the head repository is the base repository. No earlier ## Contract review record names this head on the PR or the card.

    ① Derived judgments

    1. Accept-set narrowing on sys_user_permission_set writes — right, and reaching every engine write path. The diff adds one declared cross_field rule, validity_window_order, to objects/sys-user-permission-set.object.ts and nothing else that changes what a write stores. On main's engine (packages/objectql/src/engine.ts, unchanged by the diff) declared rules are evaluated per row inside insert for single rows and batches alike (the rows[i] loop), insertMany delegates to insert, the by-id update evaluates with previous bound to the driver pre-image, and the multi-row update evaluates per matched row with previous bound to that row (the previous: null branch is unreachable for a cross_field rule because needsPriorRecord is true for it); the import preview path evaluates the same rules, so a preview reports the refusal an import would get. The only context skip at those sites is skipStateMachine, which does not reach a cross_field rule, so a system writer is judged like any other — the plugin-security pins cover both writers on insert and update, and the Test Core shards are green. A write below the engine (driver.create) is not judged, by construction; the pin that seeds a legacy inverted row uses exactly that seam and says so.
    2. The refusal envelope — right. 400 VALIDATION_FAILED with one field error, rule_violation at valid_until (the rule's first fields entry), read at the engine through resolveThrownHttpError and at the data door from the top-level code / fields of the body; the last commit on the branch corrected the dogfood case to the door's actual top-level shape, and the gate is green on that head.
    3. The half-open window semantics — right. ADR-0091 D1 declares valid_until inactive at and after the instant, and main's isGrantActive (packages/core/src/security/grant-validity.ts, untouched) implements the until bound as strictly-after-now; so equal bounds are an empty window and refusing at-or-before is the correct edge. The changeset, the rule's description and the message all state it the same way.
    4. datetime() across drivers — right, verified from source because the pins cannot show it. The refusal pins run on better-sqlite3 and the Temporal Conformance job runs driver-sql, core, formula and the non-SQL backends, not plugin-security, so the rule's behaviour on PG/MySQL rows is not CI-measured on this head. @objectstack/formula's stdlib registers datetime(dyn) over toDate, which accepts a Date instance, an epoch number or a string, so a driver-returned Date in previous cannot fault the predicate into a "could not be evaluated" refusal. No finding.
    5. The previous clause — right in intent, one bound on its sentence (carried to ③ f). On insert previous is null and the window is judged; on update record is the pre-image overlaid with the patch, so a patch that omits both bounds leaves record.valid_from == previous.valid_from and the rule is skipped. The test "moved" is a CEL != on the raw values, a spelling comparison, while the window test is an instant comparison. A patch that resends a bound unchanged in a different spelling counts as moved; a valid window is then judged and passes, so only a legacy inverted row receiving a full-row resubmit is affected, and it is refused with the accurate remedy. The pinned case (an update that omits both bounds) is exactly what the changeset's "moves neither bound" sentence describes.
    6. The user page columns — right, and not an accept-set change. valid_from and valid_until are fields declared on sys_user_permission_set on main (ADR-0091 D1, no field is added by this diff); RecordRelatedListProps.columns takes field names of objectName; the new platform-objects pin parses the block through that contract and asserts the two columns. The Add affordance is unchanged and writes the two link keys, as the PR says.
    7. The translation bundles — right. Four *.objects.generated.ts files gain _validations.validity_window_order.message and three *.source-hashes.generated.ts files gain its hash, the generator's output for a new declared message; the translated locales carry the English source text exactly as the sibling reserved_identity_name message does on main. check:i18n and the i18n ratchets sit inside the green Lint & Repo Gates.
    8. No other surface moves. No packages/spec path, no new export, no new authorable key, no packages/core change (the enforcement half pre-exists on main and is cited, not rewritten), no sys_user_position change, no sweep job, no second time axis: the file list matches the claim's exclusions (card comment 6069198090) and the maintainer's ruling (5315732575, 「9272 放到 v18开发」: assignments only, ADR-0091 vocabulary). The PR title's ! marker, the BREAKING banner and the major grade agree.
    9. The end-to-end pin — right as a proof of the card face's five pins. The four window states are read off one real data door on the showcase boot behind an armed precondition (no-grant member 403, unbounded-grant member 200); the cache-on case proves the grants cache was actually serving (the grant-table read count stays flat on the second request) and that its entry expires at the window bound with no write; the edit case drives an administrator's PATCH of valid_until; the refusal case asserts zero stored inverted rows in the boot. The grants cache defaults OFF (OS_AUTHZ_GRANTS_CACHE_TTL_MS unset), and the flat read count proves the env is honoured at request time. All three dogfood shards are green on the head.

    ② Semver level

    • .changeset/9272-grant-window-order-refused.md: @objectstack/plugin-security major, BREAKING banner, one ADR-0087 marker not-required (no-migration-prescription), and the declaration line. Right: refusing a stored value combination a published package used to accept is an accept-set narrowing, i.e. breaking; .changeset/pre.json on main is mode: pre, tag: next, and the card face's release-state line (triage 6038966446) grades a breaking change after release(v18): enter Changesets pre mode on main (changeset pre enter next) with one major marker, so the first v18 prerelease is 18.0.0-next.0 — the opening ruled B on #22050 #22080 as major. The disposition is honest: no metadata key, export or authorable surface moves, so objectstack migrate meta has nothing to rewrite, and the body's "to fix a refused write" sentence is a data remedy for an administrator, not a consumer code-rewrite prescription. Both ADR-0087 and no-major gates run inside the green Lint & Repo Gates.
    • .changeset/9272-user-page-grant-window-columns.md: @objectstack/platform-objects minor. Right: two additive columns on a shipped page, a functional improvement, no accept-set change.
    • @objectstack/dogfood is private: true; no changeset owed. No skip-changeset.
    • Clause-②: line: the PR body, the claim comment and the breaking changeset all read Clause-②: no (narrowing). Right. The diff widens no accept set and expands no public surface (no field, key, export or route), and it narrows one accept set; no with the (narrowing) arm is the one truthful spelling. Triage's earlier Clause-②: yes was a prediction of new fields; the claim measured on main that both fields already exist, and the diff confirms no field is added.

    ③ Boundary flags

    The dev report (6070549018) carries open_questions: []; its notes, deviations and one flag of my own, each answered:

    • a. Add-time collection of the window (carrier note, not filed). Answered: RecordRelatedListProps.add carries picker, linkField and label only and writes the two link keys, so the gap is a missing capability in packages/spec plus objectstack-ai/objectui, not a defect, a contract violation or an authoring trap; an acceptance note is the correct carrier under Prime Directive chore: version packages #10. The window is settable by editing the assignment row, which the card face's authoring requirement allows. Escalated to the dispatching seat only as a product choice: file the objectui carrier card if Add-time setting is wanted on v18; not a condition of this verdict.
    • b. The authz matrix row names delegation-of-duty.dogfood.test.ts as the window's end-to-end proof (carrier: none). Answered and accepted: the new pin is a second, permission-set-shaped proof carrying no authz-row: claim, so the row and its pairing check are unchanged; no gate asks for the claim.
    • c. check-changeset-no-major.mjs prints "during the launch window it ships minor" on a narrowing while accepting major in pre mode (carrier: none). Answered: the verdict is right and the sentence is stale; a gate line that misstates the grade it is about to accept is a reproducible tooling defect even with no verdict effect. Escalated to the dispatching seat to file as a tooling card (repro: run the gate in pre mode on a no (narrowing) declaration); not a condition of this verdict.
    • d. zh-CN / ja-JP / es-ES carry the English message. Answered and accepted: the generator's merge mode fills a new key with the source text, matching the sibling rules' messages on main; the i18n ratchets are green.
    • e. Deviations in the report. The stray /build1.pid is a container file, not in the diff; the two zz-scratch-* test files are absent from the diff; the translation bundles outside the order's literal file list are the declared rule's generated consequence and are in the diff as generated output. None touches the verdict.
    • f. My flag (from ① 5): the "moved a bound" test is spelling-sensitive while the window test is instant-sensitive. Answered, non-blocking: a bound resent unchanged in a different spelling (for example Z against .000Z) makes the rule judge the row; a valid window passes, so the only observable effect is that a legacy inverted row whose edit resends both bounds is refused with the prescribed remedy rather than silently kept inverted. The changeset's "an update that moves neither bound is not judged" holds for a patch that omits the bounds, which is the pinned shape. Recorded so the sentence is read with that bound; no change asked.

    Implemented-by: claude/issue-9272-assignment-validity-window
    Reviewed-by: session_01WkL6Eijt432S1Y7ekb6ovQ

    VERDICT: PASS


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    Contributor

    Seat ACCEPT: PR #22364 at 59875daf0a · seat domain:services#1 (#6021) · session_01WkL6Eijt432S1Y7ekb6ovQ · 2026-10-08T23:30Z

    Checked against GitHub and the branch, not the report's prose (os-dev-report 6070549018). The contract-tier review is PASS (6071079626): Served-tier: CONTRACT_REVIEW_TIER, Head-sha 59875daf0a…, Implemented-by the dev's branch, Reviewed-by this session, Local-runs: none.

  11. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    Contributor

    Landed · domain:services seat 1 (#6021) · session_01WkL6Eijt432S1Y7ekb6ovQ · 2026-10-09T00:04Z.

    PR #22364 merged through the merge queue as 83116502. The maintainer's ruling 「9272 放到 v18开发」 is met on origin/main: time-bounded permission-set assignments on ADR-0091's window, with no second time axis.

    • Enforcement (already on main, now pinned end to end on a real boot): an assignment grants nothing before valid_from, grants inside [valid_from, valid_until), and grants nothing at the next evaluation from valid_until on; the grants cache does not outlive a bound.
    • Authoring: the user page's Permission Sets list shows the window (@objectstack/platform-objects, minor), and the window is set by editing the assignment row.
    • Refusal: @objectstack/plugin-security (major, BREAKING) refuses an assignment whose window ends at or before it starts, with 400 VALIDATION_FAILED, through a declared validation rule.
    • The contract review passed (6071079626).

    The PR's Fixes line closed the card completed. This note also removes pm:dispatched and the assignee.

    • Setting the window at Add time needs a RecordRelatedListProps.add capability (a spec key plus an objectui renderer). It is not built here; raise it as its own card if a user pulls for it.
  12. added a commit that references this issue on Oct 9, 2026
    8311650
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:servicespriority:p2Medium: important, M3target:v18

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions