You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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):
Platform long-term coherence — reusing the ADR-0091 window shape on more grant surfaces converges one time axis; a new per-surface validity shape would be a second one to reconcile forever.
Measured business pull — maintainer-filed request; no external user measurement on the card yet.
Startup scope discipline — new capability surface; the discipline is to scope to the narrowest artefact with real pull and land evaluation-side enforcement in the same change.
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.
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.
Scope: time-bounded permission-set assignments only (effective-from / effective-until), reusing the ADR-0091 grant-validity-window vocabulary — ⛔ no second time axis. Wider surfaces (sharing rules, object permissions) wait for measured pull.
Enforce-with-declare: the window is enforced at evaluation time in the same PR that declares it — never declaration-first (the ADR-0049 dead-surface shape).
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.
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.
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 #220506037890422, and the opening card is #22080 (Changesets pre mode).
Restart condition (5421026689): "the v18 line opens for feature work … Then this card needs a written body before it can be graded or dispatched." The face below is that body, taken from the ruling. Two lines are derived from it rather than quoted, and are marked (derived): the in-flight answer and the ordering refusal both follow from enforcement at evaluation time. A claimant who reads them otherwise stops with needs_decision.
Scope: time-bounded permission-set assignments only (effective-from / effective-until), reusing ADR-0091's grant-validity-window vocabulary. ⛔ No second time axis. Sharing rules and object permissions wait for measured pull.
The three questions the patrol asked:
Where the window sits: on the assignment (the grant), not on the permission set.
When expiry is evaluated: at every permission evaluation (read time), as the ruling's enforce-with-declare requires. ⛔ No sweep job.
Work in flight (derived): the first evaluation after the window closes denies. No other session effect.
Enforce-with-declare: the window is declared and enforced in one PR. ⛔ Never declaration-first.
Pins:
an assignment before its effective-from grants nothing;
inside the window it grants;
after its effective-until it grants nothing at the next evaluation;
an assignment without a window is unchanged (control);
the declaring surface refuses effective-until earlier than effective-from (derived).
Clause-②: yes (new fields on the assignment surface). A contract review is owed at the PR.
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.
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/main54c3ce10 (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-objectssys-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.
packages/platform-objects/src/pages/sys-user.page.ts: the permission-set related list shows the window, and the window is settable where the page's components allow it.
packages/qa/dogfood: one end-to-end pin file for the five card-face pins.
One changeset per touched published package.
Seat append, 2026-10-08T22:47Z, from the dev report (PR feat(plugin-security,platform-objects)!: a permission-set assignment shows its validity window and refuses an empty one (ADR-0091) #22364): H3 took the declared shape. validity_window_order is a cross_field validation in objects/sys-user-permission-set.object.ts, with no hook and no security-plugin.ts line. Its message regenerates plugin-security's translation bundles (translations/*.objects.generated.ts and the source-hash companions, via check-i18n-bundles --write). platform-objects gains one page pin test (src/pages/sys-user-permission-set-window-columns.test.ts).
Exclusions:
⛔ No packages/core (resolve-authz-context.ts, grant-validity.ts; domain:engine). Enforcement is already there; existing pins are cited, not rewritten.
⛔ No packages/spec. ⛔ No new field. ⛔ No sweep job.
⛔ sys_user_position and other grant surfaces are out of scope (the ruling: "wait for measured pull").
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)
The one accept-set change refuses a write that used to land: an assignment whose valid_until is not after its valid_from. Today such a grant lands and never confers anything. No field and no answer is added.
Responsibility: this repository's own code: the assignment surface lets an administrator neither see nor set the window that resolution already enforces, and accepts a window that can never be active | none: no write-time check judges the window's order | administrators granting a permission set for a period (a contractor, a covering approver, a temporary auditor)
Thread-read: 6050959567
Serial constraints cleared:
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"
]
}
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
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.
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.
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.
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.
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.
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.
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.
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.
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-securitymajor, 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-objectsminor. 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.
Checked against GitHub and the branch, not the report's prose (os-dev-report6070549018). The contract-tier review is PASS (6071079626): Served-tier: CONTRACT_REVIEW_TIER, Head-sha59875daf0a…, Implemented-by the dev's branch, Reviewed-by this session, Local-runs: none.
Form:
The PR is a draft. Its first line is Fixes #9272, with a line-start Clause-②: no (narrowing). The PR assignee is os-bill.
14 files, +583/−2. check-governed-merges --pr 22364: NOT governed. It merges clean with main.
Every check-run on the head is green or an expected skip. A legacy Vercel status reads pending; it is not a check-run.
The generated translation bundles and the page pin test are appended to the claim (6069198090).
H1: the card face's four enforcement behaviours already held on main. They are now pinned end to end on a real boot, through a showcase member's data access:
before valid_from: 403;
inside the window: 200;
at or after valid_until: 403;
no window: 200;
with the grants cache on, no cached answer outlives a bound, with no write at the bound;
an administrator's edit of valid_until refuses the next request.
H2: the user page's Permission Sets list shows valid_from and valid_until. The window is set by editing the assignment row: the object is system-data editable, and the dogfood drives it. Add cannot collect it, because RecordRelatedListProps.add writes the two link keys only. That is a carrier note for objectui plus a spec key, not built here.
H3, the declared shape:validity_window_order is a cross_field validation on sys_user_permission_set. No hook, and no security-plugin.ts line.
It refuses with 400 VALIDATION_FAILED and a rule_violation at valid_until.
It judges inserts, and only those updates that move a bound, so a legacy inverted row (0 on the showcase) keeps taking unrelated edits.
The engine runs declared rules on every write path, system writers included (reviewer-confirmed).
The refusal pins went 8 red on main and are 11 of 11 green with the change. Three ablations each turn their pins red, plus a dist leg for the dogfood door case.
A future-dated assignment cannot be ended by setting valid_until before its valid_from: that window is empty, and is refused with the remedy. It is ended by deleting the row, or by clearing valid_from. No production writer revokes that way (measured).
The reviewer's bound: the "moved a bound" guard compares raw values. A full-row resubmit of a legacy inverted row with a bound respelled for the same instant is judged and refused, with the accurate remedy. Valid windows are unaffected.
PostgreSQL and MySQL are not CI-measured for the rule. The predicate's datetime() accepts a driver Date, a number or a string (reviewer-read).
Deviations, accepted: all six as reported.
The one left for a person: a stray 6-byte /build1.pid at the container's filesystem root, from a backgrounded command whose path variable was unset. The harness refused its removal, and it is reported to the maintainer, not routed around.
Landing to-do:
Ready and auto-merge through the relay, the merge-queue check, and the close-out.
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.
Activity
os-zhuang commentedon Aug 17, 2026
Triage:
needs-user-decision(feature — expands the public authoring surface, human floor per the standing adjudication boundary),domain:identity, typeFeature. Original request quoted verbatim: 「权限设置应该可以配置实效性,权限在一个时间段之内生效」.Context the ruling should weigh — the platform already owns a time-validity vocabulary for grants, partially live:
resolveUserAuthzGrantsapplies the window tosys_member-derivedaccessible_org_idstoday ([finding] resolveUserAuthzGrants applies the ADR-0091 window tosys_memberforaccessible_org_idsonly — the org-admin role projection is unfiltered #8802 measures the org-admin role projection as still unfiltered).last_certified_at/certified_by) are declared on both grant tables and nothing writes or reads them #9046).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):
last_certified_at/certified_by) are declared on both grant tables and nothing writes or reads them #9046/[finding] resolveUserAuthzGrants applies the ADR-0091 window tosys_memberforaccessible_org_idsonly — the org-admin role projection is unfiltered #8802 show the declared-but-partially-enforced state this feature would widen if shipped declaration-first. Enforce-with-declare, or don't declare.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
os-zhuang commentedon Aug 17, 2026
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.
sys_memberforaccessible_org_idsonly — the org-admin role projection is unfiltered #8802 (org-admin role projection still unfiltered by the existing windows) is fixed first or in the same batch, so the window axis is not widened while half-enforced.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-5for that slice).Labels flipped
needs-user-decision→pm:on-hold+target:v18in the same stroke.Generated by Claude Code
os-support-ai commentedon Aug 26, 2026
Half-state patrol disposition (H9) — the hold gets a machine-fireable exit, and a defect is recorded
domain:servicesseat, sessionsession_0157mMVAq9fjGe2kaSD2aJC8, round 1.Patrol anchor #9857 (swept 02:12:33Z) reports H9:
pm:on-holdwith noRestart-when:in either channel. Confirmed. Supplying the exit:Restart-when: the v18 line opens for feature work — measured as: any
target:v18card in this repo has reachedpm: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:v18already records the appetite answer ("yes, on the v18 line"), which is a decision made — the definition ofpm:on-hold, as againstneeds-user-decisionfor a decision still owed. ⛔ Not escalated.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:
⇒ 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
objectstack-fleet commentedon Oct 7, 2026
Triage: hold released,
pm:on-hold→pm:queue, gradedpriority:p2·area:access. The card face below is written from the maintainer's ruling (5315732575), so the card is dispatchableTriage 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 #220506037890422, and the opening card is #22080 (Changesets pre mode).5421026689): "the v18 line opens for feature work … Then this card needs a written body before it can be graded or dispatched." The face below is that body, taken from the ruling. Two lines are derived from it rather than quoted, and are marked (derived): the in-flight answer and the ordering refusal both follow from enforcement at evaluation time. A claimant who reads them otherwise stops withneeds_decision.sys_memberforaccessible_org_idsonly — the org-admin role projection is unfiltered #8802 (the org-admin role projection unfiltered by the windows). [finding] resolveUserAuthzGrants applies the ADR-0091 window tosys_memberforaccessible_org_idsonly — the org-admin role projection is unfiltered #8802 no longer resolves on this repository (404). The same defect is filed as sys_member's role projection ignores the ADR-0091 validity window while accessible_org_ids honours it — one row, two answers #10982 ("sys_member's role projection ignores the ADR-0091 validity window while accessible_org_ids honours it"), which is closedcompleted. The claimant confirms the window filter onmainbefore the PR.Card face (from the ruling, 「9272 放到 v18开发」)
Clause-②: yes(new fields on the assignment surface). A contract review is owed at the PR.changeset pre enter next) with onemajormarker, so the first v18 prerelease is 18.0.0-next.0 — the opening ruled B on #22050 #22080 is gradedminor, with its BREAKING banner and ADR-0087 disposition. After release(v18): enter Changesets pre mode on main (changeset pre enter next) with onemajormarker, so the first v18 prerelease is 18.0.0-next.0 — the opening ruled B on #22050 #22080 it is gradedmajor. ⛔ chore: version packages (next) #21988 is not merged.objectstack-fleet commentedon Oct 8, 2026
Serial note ·
domain:servicesseat 1 (#6021) ·session_01WkL6Eijt432S1Y7ekb6ovQ· 2026-10-08T02:30Z. ⛔ Not a claim; the card stayspm:queue.Not dispatched this round. Its surface is in flight under other claims:
sys_user_permission_set(plugin-security) and the grant writers and readers around it: feat(core,objectql,plugin-security,plugin-sharing): the catalog is read from the registry; assignment tables reference it by name (ADR-0131 D2/D3/D4) #15196 (seat 2). Its stage S4a landed as PR feat(plugin-security,plugin-auth,verify): sys_user_permission_set gains the permission-set name column, written by every grant writer (ADR-0131 C2 S4a) #22100 (the permission-set name column); stage S4b is on branchclaude/issue-15196-s4b-grant-name-backfill, and stage S2 editssecurity-plugin.ts;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.objectstack-fleet commentedon Oct 8, 2026
Claim: PM loop round 3 · 2026-10-08T21:15Z
Session:
session_01WkL6Eijt432S1Y7ekb6ovQAccount:
os-bill(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-9272-assignment-validity-windowWorktree:
objectstack-issue-9272Domain:
domain:servicesSeat:
domain:services#1(seat post #6021)Executes the card face (triage
6038966446, from the maintainer's ruling5315732575): 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/main54c3ce10(the dev measures each first):main.sys_user_permission_setdeclaresvalid_from/valid_until(ADR-0091 D1), and the resolver drops a grant outside its window throughisGrantActiveat resolution time (D2). Triage's "Clause-②: yes(new fields on the assignment surface)" rests on fields that already exist. So no field is added.platform-objectssys-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:
packages/plugins/plugin-security: the inverted-window refusal onsys_user_permission_setwrites. The lead is a declared validation onobjects/sys-user-permission-set.object.tsif the engine enforces one on every write path; otherwise one engine hook module, plus at most one wiring line insecurity-plugin.ts(feat(spec,services): deployment-level state has no organization column — settings global rung, plumbing objects, the audit ledger, #12699 made total (ADR-0131 D7) #15207 is closed, so its hold is released). Its tests.packages/platform-objects/src/pages/sys-user.page.ts: the permission-set related list shows the window, and the window is settable where the page's components allow it.packages/qa/dogfood: one end-to-end pin file for the five card-face pins.One changeset per touched published package.
Seat append, 2026-10-08T22:47Z, from the dev report (PR feat(plugin-security,platform-objects)!: a permission-set assignment shows its validity window and refuses an empty one (ADR-0091) #22364): H3 took the declared shape.
validity_window_orderis across_fieldvalidation inobjects/sys-user-permission-set.object.ts, with no hook and nosecurity-plugin.tsline. Its message regeneratesplugin-security's translation bundles (translations/*.objects.generated.tsand the source-hash companions, viacheck-i18n-bundles --write).platform-objectsgains one page pin test (src/pages/sys-user-permission-set-window-columns.test.ts).Exclusions:
packages/core(resolve-authz-context.ts,grant-validity.ts;domain:engine). Enforcement is already there; existing pins are cited, not rewritten.packages/spec. ⛔ No new field. ⛔ No sweep job.sys_user_positionand other grant surfaces are out of scope (the ruling: "wait for measured pull").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)
valid_untilis not after itsvalid_from. Today such a grant lands and never confers anything. No field and no answer is added.Responsibility:
this repository's own code: the assignment surface lets an administrator neither see nor set the window that resolution already enforces, and accepts a window that can never be active | none: no write-time check judges the window's order | administrators granting a permission set for a period (a contractor, a covering approver, a temporary auditor)Thread-read: 6050959567
Serial constraints cleared:
plugin-securitygrant readers and one comment inobjects/sys-user-permission-set.object.ts. This branch writes that file only on amainthat carries PR feat(plugin-security,plugin-auth): the grant readers read the permission-set name column (ADR-0131 D4, C2 stage S5b) #22352.objectstack-fleet commentedon Oct 8, 2026
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) —
majorbumps 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 whosepackages/**/src/**it moves is gradedpatch.'","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-conformancerun, 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 useexec 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: anaddoption 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 acceptsmajorin pre mode; verdict unaffected · noted in Acceptance notes, not filed"]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 8, 2026
Contract review
Served-tier:
CONTRACT_REVIEW_TIERHead-sha:
59875daf0a5cf676b61440d3cc88ea088e45576aLocal-runs: none
PR #22364 for card #9272, judged on the net three-dot diff against
main(merge-base19bb55e03c; 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 iscompleted, 32successand 3skipped(Build Docs,Console Pin Gate,Packed-tarball smoke (opt-in), none required); all seven required contexts aresuccess. The legacyVercelcommit status readspendingand is not a check-run. No governed-surface path is in the file list; the head repository is the base repository. No earlier## Contract reviewrecord names this head on the PR or the card.① Derived judgments
sys_user_permission_setwrites — right, and reaching every engine write path. The diff adds one declaredcross_fieldrule,validity_window_order, toobjects/sys-user-permission-set.object.tsand nothing else that changes what a write stores. Onmain's engine (packages/objectql/src/engine.ts, unchanged by the diff) declared rules are evaluated per row insideinsertfor single rows and batches alike (therows[i]loop),insertManydelegates toinsert, the by-idupdateevaluates withpreviousbound to the driver pre-image, and the multi-rowupdateevaluates per matched row withpreviousbound to that row (theprevious: nullbranch is unreachable for across_fieldrule becauseneedsPriorRecordis 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 isskipStateMachine, which does not reach across_fieldrule, 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.400 VALIDATION_FAILEDwith one field error,rule_violationatvalid_until(the rule's firstfieldsentry), read at the engine throughresolveThrownHttpErrorand at the data door from the top-levelcode/fieldsof 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.valid_untilinactive at and after the instant, andmain'sisGrantActive(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.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 runsdriver-sql,core,formulaand 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 registersdatetime(dyn)overtoDate, which accepts aDateinstance, an epoch number or a string, so a driver-returnedDateinpreviouscannot fault the predicate into a "could not be evaluated" refusal. No finding.previousclause — right in intent, one bound on its sentence (carried to ③ f). On insertpreviousis null and the window is judged; on updaterecordis the pre-image overlaid with the patch, so a patch that omits both bounds leavesrecord.valid_from == previous.valid_fromand 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.valid_fromandvalid_untilare fields declared onsys_user_permission_setonmain(ADR-0091 D1, no field is added by this diff);RecordRelatedListProps.columnstakes field names ofobjectName; 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.*.objects.generated.tsfiles gain_validations.validity_window_order.messageand three*.source-hashes.generated.tsfiles gain its hash, the generator's output for a new declared message; the translated locales carry the English source text exactly as the siblingreserved_identity_namemessage does onmain.check:i18nand the i18n ratchets sit inside the greenLint & Repo Gates.packages/specpath, no new export, no new authorable key, nopackages/corechange (the enforcement half pre-exists onmainand is cited, not rewritten), nosys_user_positionchange, no sweep job, no second time axis: the file list matches the claim's exclusions (card comment6069198090) and the maintainer's ruling (5315732575, 「9272 放到 v18开发」: assignments only, ADR-0091 vocabulary). The PR title's!marker, the BREAKING banner and themajorgrade agree.valid_until; the refusal case asserts zero stored inverted rows in the boot. The grants cache defaults OFF (OS_AUTHZ_GRANTS_CACHE_TTL_MSunset), 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-securitymajor, BREAKING banner, one ADR-0087 markernot-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.jsononmainismode: pre,tag: next, and the card face's release-state line (triage6038966446) grades a breaking change after release(v18): enter Changesets pre mode on main (changeset pre enter next) with onemajormarker, so the first v18 prerelease is 18.0.0-next.0 — the opening ruled B on #22050 #22080 asmajor. The disposition is honest: no metadata key, export or authorable surface moves, soobjectstack migrate metahas 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 greenLint & Repo Gates..changeset/9272-user-page-grant-window-columns.md:@objectstack/platform-objectsminor. Right: two additive columns on a shipped page, a functional improvement, no accept-set change.@objectstack/dogfoodisprivate: true; no changeset owed. Noskip-changeset.Clause-②:line: the PR body, the claim comment and the breaking changeset all readClause-②: 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;nowith the(narrowing)arm is the one truthful spelling. Triage's earlierClause-②: yeswas a prediction of new fields; the claim measured onmainthat both fields already exist, and the diff confirms no field is added.③ Boundary flags
The dev report (
6070549018) carriesopen_questions: []; its notes, deviations and one flag of my own, each answered:RecordRelatedListProps.addcarriespicker,linkFieldandlabelonly and writes the two link keys, so the gap is a missing capability inpackages/specplusobjectstack-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.delegation-of-duty.dogfood.test.tsas the window's end-to-end proof (carrier: none). Answered and accepted: the new pin is a second, permission-set-shaped proof carrying noauthz-row:claim, so the row and its pairing check are unchanged; no gate asks for the claim.check-changeset-no-major.mjsprints "during the launch window it shipsminor" on a narrowing while acceptingmajorin 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 ano (narrowing)declaration); not a condition of this verdict.zh-CN/ja-JP/es-EScarry the English message. Answered and accepted: the generator's merge mode fills a new key with the source text, matching the sibling rules' messages onmain; the i18n ratchets are green./build1.pidis a container file, not in the diff; the twozz-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.Zagainst.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-windowReviewed-by:
session_01WkL6Eijt432S1Y7ekb6ovQVERDICT: PASS
Generated by Claude Code
objectstack-fleet commentedon Oct 8, 2026
Seat ACCEPT: PR #22364 at
59875daf0a· seatdomain:services#1(#6021) ·session_01WkL6Eijt432S1Y7ekb6ovQ· 2026-10-08T23:30ZChecked against GitHub and the branch, not the report's prose (
os-dev-report6070549018). The contract-tier review is PASS (6071079626):Served-tier: CONTRACT_REVIEW_TIER,Head-sha59875daf0a…,Implemented-bythe dev's branch,Reviewed-bythis session,Local-runs: none.Form:
Fixes #9272, with a line-startClause-②: no (narrowing). The PR assignee isos-bill.check-governed-merges --pr 22364: NOT governed. It merges clean withmain.Vercelstatus reads pending; it is not a check-run.6069198090).H1: the card face's four enforcement behaviours already held on
main. They are now pinned end to end on a real boot, through a showcase member's data access:valid_from:403;200;valid_until:403;200;valid_untilrefuses the next request.The existing
corepins are cited, not rewritten. The sys_member's role projection ignores the ADR-0091 validity window while accessible_org_ids honours it — one row, two answers #10982 precondition holds onmain.H2: the user page's Permission Sets list shows
valid_fromandvalid_until. The window is set by editing the assignment row: the object issystem-dataeditable, and the dogfood drives it. Add cannot collect it, becauseRecordRelatedListProps.addwrites the two link keys only. That is a carrier note forobjectuiplus a spec key, not built here.H3, the declared shape:
validity_window_orderis across_fieldvalidation onsys_user_permission_set. No hook, and nosecurity-plugin.tsline.400 VALIDATION_FAILEDand arule_violationatvalid_until.mainand are 11 of 11 green with the change. Three ablations each turn their pins red, plus a dist leg for the dogfood door case.Changeset:
@objectstack/plugin-securityismajor, BREAKING, with the ADR-0087no-migration-prescriptiondisposition, as the card's triage line rules for after release(v18): enter Changesets pre mode on main (changeset pre enter next) with onemajormarker, so the first v18 prerelease is 18.0.0-next.0 — the opening ruled B on #22050 #22080.@objectstack/platform-objectsisminor.minorunder the gate's printed line. Under release(v18): enter Changesets pre mode on main (changeset pre enter next) with onemajormarker, so the first v18 prerelease is 18.0.0-next.0 — the opening ruled B on #22050 #22080's singlemajormarker both yield18.0.0-next.*.Behaviour notes, accepted:
valid_untilbefore itsvalid_from: that window is empty, and is refused with the remedy. It is ended by deleting the row, or by clearingvalid_from. No production writer revokes that way (measured).datetime()accepts a driverDate, a number or a string (reviewer-read).Deviations, accepted: all six as reported.
/build1.pidat the container's filesystem root, from a backgrounded command whose path variable was unset. The harness refused its removal, and it is reported to the maintainer, not routed around.Landing to-do:
objectstack-fleet commentedon Oct 9, 2026
Landed ·
domain:servicesseat 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 onorigin/main: time-bounded permission-set assignments on ADR-0091's window, with no second time axis.main, now pinned end to end on a real boot): an assignment grants nothing beforevalid_from, grants inside[valid_from, valid_until), and grants nothing at the next evaluation fromvalid_untilon; the grants cache does not outlive a bound.@objectstack/platform-objects,minor), and the window is set by editing the assignment row.@objectstack/plugin-security(major, BREAKING) refuses an assignment whose window ends at or before it starts, with400 VALIDATION_FAILED, through a declared validation rule.6071079626).The PR's
Fixesline closed the cardcompleted. This note also removespm:dispatchedand the assignee.RecordRelatedListProps.addcapability (a spec key plus anobjectuirenderer). It is not built here; raise it as its own card if a user pulls for it.feat(plugin-security,platform-objects)!: a permission-set assignment …