Skip to content

Commit 0d59704

Browse files
committed
docs(adr): ADR-0148 names ADR-0087's kit rule, the ref a snapshot would hold, and resolve-at-sweep as the rejected alternative
Delta review 6649866889 on #30263 passed with four should-fixes, all text: - "Aged out of the load path" named the wrong concept: both conversions are retiredFromLoadPath. The difference main carries is ADR-0087's kit rule — the enable.trash kit's conversion was removed when 18 was cut (its tombstone stays), the allowRestore/allowPurge kit is carried at 18 and ages out at 19, and neither replays at load. ADR-0049's Status note and ADR-0148's Builds on, Leaves unchanged, Context and D5 now say "kit". - D4's reason for keeping secrets out of the snapshot no longer conflates the value with the ref: under ADR-0100 the row carries only the secret ref, so the snapshot would hold a ref the committed delete released, and encrypting the value into the entry would open a second store outside sys_secret. - D7 gains the two choices it omitted: no object-level authoring key (by rule, not by a flag) and the privacy rules as standing rules that bind v1 and everything after it; D5 and D4 mark them where they are made. - D3 names resolve-at-sweep (the LifecycleService's lifecycle-namespace windows, global and per tenant; the sys_file reap's deleted_at plus a static expireAfter) as the alternative the record rejects, and leaves the reap shape on the stamped column to #30237. docs/adr/README.md is unchanged by this commit (the Status lines did not move). Claude-Session: https://claude.ai/code/session_01WGufzfC3wLXmeCXwxa963a Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9b45718 commit 0d59704

2 files changed

Lines changed: 44 additions & 26 deletions

File tree

‎docs/adr/0049-no-unenforced-security-properties.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ADR-0049: Spec must not declare security properties the runtime does not enforce (enforce-or-remove gate)
22

3-
**Status**: Accepted (2026-06-15) — implemented: fail-closed `DESTRUCTIVE_OPERATIONS` (`packages/plugins/plugin-security/src/permission-evaluator.ts#DESTRUCTIVE_OPERATIONS`), lifecycle bits RBAC-gated, `apiEnabled` enforced (`runtime/src/api-exposure.ts`), `PolicySchema` removed, EXPERIMENTAL tag convention live. Two gate-valid disposition deviations: agent access-control shipped experimental-tagged (not enforced), `flow.runAs` kept + enforced (not removed). `action.disabled` CEL enforcement to confirm in objectui. **Amended 2026-09-04** (#14402) — recording that an author-written validation rule's `message` became translatable in 17.3.0 (#14381) under the object-scoped bundle key `objects.<object_name>._validations.<rule_name>.message`. That is this gate being **enforced, not reversed**: the 17.0.0 retirement of the top-level `validationMessages` group stands, and the two are not the same route. See **"Amendment (2026-09-04): validation-rule messages are translatable again — enforced, not reversed"** at the end. **Amended 2026-11-05** ([ADR-0148](./0148-recycle-bin-separate-store-restore-under-original-ids-retention-in-days.md), #30234, ruling record 6629858351 on #3146) — the recycle bin returns as a **new** surface whose readers that record names (a separate `sys_recycle_bin` store, restore under the original ids, retention in days through a host hook); the keys this gate removed — `enable.trash` and `softDelete` (16.x, #2377), `allowRestore` and `allowPurge` (#12497) — **stay removed**: their tombstones stand; the `allowRestore` / `allowPurge` conversion (`packages/spec/src/conversions/entries/18/permission-allow-restore-purge-removed.ts`) stands; the 16.x `enable.trash` conversion has aged out of the load path as ADR-0087 provides (`object-enable-trash-mru-removed`, retired in `packages/spec/src/conversions/retired-after.census.json`, no entry file); `softDelete` never had one; and none returns under its old spelling. The gate is enforced, not reversed; this decision text is unchanged.
3+
**Status**: Accepted (2026-06-15) — implemented: fail-closed `DESTRUCTIVE_OPERATIONS` (`packages/plugins/plugin-security/src/permission-evaluator.ts#DESTRUCTIVE_OPERATIONS`), lifecycle bits RBAC-gated, `apiEnabled` enforced (`runtime/src/api-exposure.ts`), `PolicySchema` removed, EXPERIMENTAL tag convention live. Two gate-valid disposition deviations: agent access-control shipped experimental-tagged (not enforced), `flow.runAs` kept + enforced (not removed). `action.disabled` CEL enforcement to confirm in objectui. **Amended 2026-09-04** (#14402) — recording that an author-written validation rule's `message` became translatable in 17.3.0 (#14381) under the object-scoped bundle key `objects.<object_name>._validations.<rule_name>.message`. That is this gate being **enforced, not reversed**: the 17.0.0 retirement of the top-level `validationMessages` group stands, and the two are not the same route. See **"Amendment (2026-09-04): validation-rule messages are translatable again — enforced, not reversed"** at the end. **Amended 2026-11-05** ([ADR-0148](./0148-recycle-bin-separate-store-restore-under-original-ids-retention-in-days.md), #30234, ruling record 6629858351 on #3146) — the recycle bin returns as a **new** surface whose readers that record names (a separate `sys_recycle_bin` store, restore under the original ids, retention in days through a host hook); the keys this gate removed — `enable.trash` and `softDelete` (16.x, #2377), `allowRestore` and `allowPurge` (#12497) — **stay removed**: their tombstones stand; under ADR-0087's kit rule the `allowRestore` / `allowPurge` kit (`packages/spec/src/conversions/entries/18/permission-allow-restore-purge-removed.ts`, retired after 17.2.0) is carried at 18 and ages out at 19, while the `enable.trash` kit's conversion (`object-enable-trash-mru-removed`, 16.x) was removed when 18 was cut and only its tombstone remains; neither conversion replays at load (both `retiredFromLoadPath`, `packages/spec/src/conversions/retired-after.census.json`); `softDelete` never had one; and none returns under its old spelling. The gate is enforced, not reversed; this decision text is unchanged.
44
**Deciders**: ObjectStack Protocol Architects
55
**Builds on**: [ADR-0005](./0005-metadata-customization-overlay.md) (artifact vs runtime overlay), [ADR-0010](./0010-metadata-protection-model.md) (package provenance), [ADR-0027](./0027-metadata-authoring-lifecycle.md) (authoring lifecycle)
66
**Consumers**: `@objectstack/spec` (security/identity schemas), `@objectstack/plugin-security` (`PermissionEvaluator`, `SecurityPlugin`), spec authors, the metadata-property liveness audit follow-ups (#1878 P0 cluster).

‎docs/adr/0148-recycle-bin-separate-store-restore-under-original-ids-retention-in-days.md‎

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818
cascade a delete removes and a restore brings back together),
1919
[ADR-0132](./0132-multi-organization-runtime-is-open-core.md) D2 (mechanism open, entitlement commercial),
2020
[ADR-0112](./0112-error-code-vocabulary-and-ledger.md) (the envelope every restore refusal answers in),
21-
[ADR-0087](./0087-metadata-protocol-upgrade-contract.md) (the retired keys' tombstones stand, and their conversions
22-
age out on its schedule — [D5](#d5--the-readers-that-will-exist-and-the-keys-that-stay-retired) says which one is
23-
still carried; the new keys arrive as a protocol addition), [ADR-0120](./0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md) (why the
21+
[ADR-0087](./0087-metadata-protocol-upgrade-contract.md) (the retired keys' tombstones stand, and their kits age out
22+
on its schedule — [D5](#d5--the-readers-that-will-exist-and-the-keys-that-stay-retired) says which kit this major
23+
still carries; the new keys arrive as a protocol addition), [ADR-0120](./0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md) (why the
2424
shape frees a unique value at delete, and why a restore can find it taken),
2525
[ADR-0100](./0100-credential-field-channels.md) (the `secret` channel the snapshot keeps out),
2626
[ADR-0041](./0041-flow-trigger-family.md) (record triggers — why a restore is its own event, never a create).
@@ -29,8 +29,8 @@
2929
([D5](#d5--the-readers-that-will-exist-and-the-keys-that-stay-retired)).
3030
- **Leaves unchanged**: every read path (query, aggregate, REST, the analytics engine's SQL, rollups, lookup pickers,
3131
search, AI tools, MCP and export); the physical delete the engine performs today and its cascade order; the
32-
tombstones of `enable.trash`, `softDelete`, `allowRestore` and `allowPurge`, and the one ADR-0087 conversion `main`
33-
still carries for them (`allowRestore` / `allowPurge`); the `sys_file` tombstone and its reap; the release of
32+
tombstones of `enable.trash`, `softDelete`, `allowRestore` and `allowPurge`, and the one ADR-0087 kit `main` still
33+
carries for them (`allowRestore` / `allowPurge`, carried at 18, gone at 19); the `sys_file` tombstone and its reap; the release of
3434
`sys_secret` handles on a committed delete.
3535
- **Consumers**: `@objectstack/spec` (what #30235's body lists — the recycle-bin object, the retention hook's contract,
3636
the refusal codes — and what this record adds to that card, the `restored` event's name and the two grant keys, both
@@ -111,10 +111,14 @@ affordance: authors wrote `trash: false` believing they were opting out of a sof
111111
removed in the 16.x line (#2377) and tombstoned — `enable.trash` in
112112
`packages/spec/src/data/object.zod.ts#CAPABILITIES_RETIRED_KEY_GUIDANCE`, `softDelete` in the same file's top-level
113113
unknown-key guidance, whose text is the standing fact: 「there is no soft-delete/recycle-bin runtime … Deletes are hard
114-
deletes」. What `main` carries for them under ADR-0087: the 16.x conversion `object-enable-trash-mru-removed` has aged
115-
out — it is retired from the load path (listed as such in `packages/spec/src/conversions/retired-after.census.json`)
116-
and no entry file remains, which is why the `enable.trash` tombstone tells an author to apply the edit by hand or run
117-
the tool on a release that still carries it; `softDelete` never had a conversion entry, only the tombstone. The
114+
deletes」. What `main` carries for them is set by ADR-0087's kit rule (its addendum of 2026-10-26: a retired key's
115+
tombstone and conversion — the kit — live one major past the one they retired in): the `enable.trash` kit's conversion,
116+
`object-enable-trash-mru-removed` (retired at 16.x), was removed when 18 was cut, so no entry file remains and the
117+
tombstone that stays tells an author to apply the edit by hand or run the tool on a release that still carries it; the
118+
`allowRestore` / `allowPurge` kit (`packages/spec/src/conversions/entries/18/permission-allow-restore-purge-removed.ts`,
119+
retired after 17.2.0) is carried at 18 and ages out at 19. Neither conversion replays at load: both are
120+
`retiredFromLoadPath`, listed as such in `packages/spec/src/conversions/retired-after.census.json`, and the permission
121+
one runs under `os migrate meta` only. `softDelete` never had a conversion entry, only the tombstone. The
118122
permission lifecycle bits `allowRestore` / `allowPurge` claimed to gate `restore` / `purge` operations
119123
that never entered the engine's dispatch vocabulary (pinned by
120124
`packages/objectql/src/engine-middleware-operation-vocabulary.test.ts`); they were removed with #12497 (the maintainer
@@ -215,9 +219,14 @@ number — zero and anything that is not such a number are refused as a configur
215219
**When the days become a deadline — stamped at delete time.** The ruling decided days-through-a-hook and no more; this
216220
record decides that the resolver is asked once, at delete, and its answer is written on the entry as the purge
217221
deadline. Reasons: the toast and the admin page show the person a date, and a later change of the host's answer (a plan
218-
change, a new default) must not shorten a window a person was already shown; the sweep then reads one stored column,
219-
as the `sys_file` reap reads `deleted_at`, and calls no resolver per entry. The consequence is accepted: a longer window
220-
applies to deletes made after the change, not before. No entry is purged before its stamped deadline.
222+
change, a new default) must not shorten a window a person was already shown. The alternative the record rejects is
223+
the one the LifecycleService already runs: resolving an object's window at sweep time from the `lifecycle` settings
224+
namespace, globally and per tenant (ADR-0057 §3.2, `packages/objectql/src/lifecycle/lifecycle-service.ts`), as the
225+
`sys_file` reap does with `deleted_at` plus a static `ttl.expireAfter` — a window resolved at sweep can move after the
226+
person was shown it, and a host's days are not a per-object setting. With the deadline stamped, the sweep reads one
227+
stored column and asks no resolver; how the reap is declared on that column (a `ttl` on the deadline with no added
228+
window, or a reap guard) is #30237's. The consequence is accepted: a longer window applies to deletes made after the
229+
change, not before. No entry is purged before its stamped deadline.
221230

222231
**The TTL purge runs on the existing LifecycleService** (ADR-0057 §3.3: one sweeper, never a bespoke one), following
223232
the `sys_file` tombstone pattern: an entry is purged once its stamped deadline has passed, by the platform sweep, under
@@ -252,15 +261,17 @@ slice say so.
252261
the rows held (#27581), and v1 leaves that as it is;
253262
- a member-facing 「我删除的」 page.
254263

255-
**The privacy rules**, which bind v1 and everything after it:
264+
**The privacy rules**, which bind v1 and everything after it (the ruling lists them among the risks to plan for;
265+
making them standing rules is the record's choice, [D7](#d7--what-this-record-fixes-that-the-ruling-left-open)):
256266

257267
- **FLS applies when listing.** A snapshot holds every field; the bin shows a caller a field only where the caller may
258268
read it on the live object.
259269
- **Secret fields stay out of the snapshot, or are encrypted.** The ruling allows either; this record chooses **out**
260-
for v1, because the ruling's later item (holding attachments and `sys_secret` rows until purge) leaves the committed delete
261-
releasing the handles in v1, so an encrypted copy of a value would point at a handle that no longer exists. So the
262-
`secret` channel's values (ADR-0100) are not written into the entry, and a restored record's secret fields come back
263-
empty, to be re-entered. Holding them until purge is the later item, and it may revisit this choice.
270+
for v1. Under ADR-0100 a business row carries only the opaque `secret:<id>` ref and the value lives in `sys_secret`;
271+
the committed delete releases those refs in v1 (#27581 — holding `sys_secret` rows until purge is the later item), so
272+
a snapshot of the row would hold a ref that dangles, and encrypting the value into the entry would open a second
273+
store for a secret outside `sys_secret`. So the `secret` channel's refs are not written into the entry, and a restored
274+
record's secret fields come back empty, to be re-entered. The later item may revisit this choice.
264275
- **Purge on organization or environment deletion.** An organization's or environment's entries go with it.
265276
- **Bin storage counts toward quota and is shown separately.**
266277

@@ -287,18 +298,20 @@ with the card that lands it. Every row below is new; none is a tombstoned spelli
287298

288299
**The old keys stay retired.** `enable.trash`, `softDelete`, `allowRestore` and `allowPurge` are tombstones and none of
289300
them returns: an author who writes one is refused with the prescription the tombstone carries. What `main` carries for
290-
them under ADR-0087 is exactly this: the `allowRestore` / `allowPurge` conversion
291-
(`packages/spec/src/conversions/entries/18/permission-allow-restore-purge-removed.ts`) stands; the 16.x conversion
292-
`object-enable-trash-mru-removed` has aged out of the load path as ADR-0087 provides (retired in
293-
`packages/spec/src/conversions/retired-after.census.json`, no entry file), so a 15.x artifact that still writes
294-
`enable.trash` is refused by the tombstone and migrated by hand or on a release that still carries it; `softDelete` never
295-
had a conversion. This record changes none of that. The new surface uses new spellings on purpose, so that a key which
301+
them is what ADR-0087's kit rule leaves at this major: the `allowRestore` / `allowPurge` kit
302+
(`packages/spec/src/conversions/entries/18/permission-allow-restore-purge-removed.ts`, retired after 17.2.0) is carried
303+
at 18 and ages out at 19; the `enable.trash` kit's conversion (`object-enable-trash-mru-removed`, 16.x) was removed when
304+
18 was cut and only its tombstone remains, so a 15.x artifact that still writes `enable.trash` is refused by the
305+
tombstone and migrated by hand or on a release that still carries it; neither conversion replays at load (both are
306+
`retiredFromLoadPath` in `packages/spec/src/conversions/retired-after.census.json`); `softDelete` never had a
307+
conversion. This record changes none of that. The new surface uses new spellings on purpose, so that a key which
296308
once meant "nothing happens" never comes to mean "a snapshot is kept". The retired `apiMethods` values `restore` and
297309
`purge` (`packages/spec/src/data/api-derivation.ts`) are likewise unchanged. ADR-0049's Status line carries the dated
298310
note.
299311

300312
**No object-level authoring key is named by this record.** v1 applies to every business object and to no `sys_*`
301-
object by rule, not by a flag. Per-object retention is enterprise and lives in cloud; if an open per-object key is
313+
object by rule, not by a flag — the ruling said only "business objects, not `sys_*`", and that no key carries it is the
314+
record's choice ([D7](#d7--what-this-record-fixes-that-the-ruling-left-open)). Per-object retention is enterprise and lives in cloud; if an open per-object key is
302315
ever ruled, it arrives with its reader in the same change, as ADR-0049 requires.
303316

304317
### D6 — What this record does not decide
@@ -326,7 +339,12 @@ Each of these is the record's choice, not the ruling's, and is marked as such wh
326339
- secret fields kept out of the snapshot in v1, of the two the ruling allowed
327340
([D4](#d4--v1-scope-forward-only-later-items-and-the-privacy-rules));
328341
- the `restored` event's name and the grant keys declared by #30235, whose body lists neither
329-
([D5](#d5--the-readers-that-will-exist-and-the-keys-that-stay-retired)).
342+
([D5](#d5--the-readers-that-will-exist-and-the-keys-that-stay-retired));
343+
- no object-level authoring key — v1 reaches every business object and no `sys_*` object by rule, not by a flag; the
344+
ruling said only "business objects, not `sys_*`", and #30235's body expects any object-level key the record names,
345+
so "none" is the record's answer ([D5](#d5--the-readers-that-will-exist-and-the-keys-that-stay-retired));
346+
- the privacy rules as standing rules that bind v1 and everything after it — the ruling listed them among the risks
347+
to plan for ([D4](#d4--v1-scope-forward-only-later-items-and-the-privacy-rules)).
330348

331349
The ruling itself gives a person the restore of what they deleted and gives an admin everything (list, restore, empty);
332350
this record adds no permission beyond that.

0 commit comments

Comments
 (0)