Skip to content

Commit 3f17d52

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-21504-retire-ai-chat-window
Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
2 parents 26ae681 + 88fb5e8 commit 3f17d52

61 files changed

Lines changed: 1734 additions & 404 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/core': patch
3+
---
4+
5+
Provenance comments in `@objectstack/core` cite the commits that decided them, not tracker numbers that no longer resolve
6+
7+
Clause-②: no
8+
9+
Docblocks and comments across the package cited issue-tracker numbers that now answer 404 on GitHub.
10+
Each now cites the commit in this repository's history that made the decision it describes, except
11+
three source comments: one in `resolve-authz-context.ts` that quotes a maintainer ruling now cites
12+
ADR-0131's 2026-09-17 amendment, which records that ruling verbatim, and two on the unpack-time
13+
integrity re-verification leg, which pointed at a tracker for work that was never built, now say in
14+
words that the leg is unbuilt. One test comment named a maintainer-ruling comment that also answers
15+
404; it now cites ADR-0025 §3.7, which records that ruling's effect. Some of these docblocks sit on
16+
exported members, so the reworded text appears in the published declaration files (`index.d.ts` /
17+
`index.d.cts`), and the comments esbuild keeps appear in the JavaScript output (`index.js` /
18+
`index.cjs`).
19+
20+
Comment only: no export, type, error code, status, message text or runtime behaviour changes.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
---
4+
5+
fix(metadata-protocol): a view a stored view container expands answers by name what the object door lists, on every kernel and for every container scope
6+
7+
Clause-②: no
8+
9+
- **What changed.** `GET /api/v1/meta/view?object=…` lists the views a stored view container expands, and the by-name read now answers each of those names with the same item. Before, `getMetaItem` expanded no container: it answered such a name only on an unscoped kernel and only for an environment-wide container, where the registry held a hydrated copy. On an environment-scoped kernel, and for an organization-scoped container on any kernel, it answered nothing. Where the name is one a package also ships, such as `<object>.default` under a tenant's overlay of that package's container, it answered the packaged view while the list served the overlay's.
10+
- **How.** The by-name read selects the stored containers in the caller's scope with the list read's own row selection, and expands them with the list read's own expansion. Nothing is persisted or registered, and a stored row of the name itself still answers first.
11+
- **Layers, history and diff for such a name.** `getMetaItemLayered` reports the container's own stored row as `overlay`, with the scope it was read from as `overlayScope`, and the expanded view as `effective`. `historyMetaItem` and `diffMetaItem` answer exactly what they answer under the container's own name, and say so: every event's `ref.name` and the diff's `name` are the container's. No history is made up for a name that was never stored.
12+
- **What does not change.** The container's own name still answers its stored row. The save door is unchanged, including a write by an expanded name. No response shape gains or loses a key.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@objectstack/plugin-approvals': patch
3+
---
4+
5+
Every `sys_user` lookup the approvals plugin writes now holds a user id or nothing: the SLA and dead-run sweeps record no actor instead of a `system:` placeholder, notifications name only the person who acted, and `reassign_from` / `reassign_to` become slot-address text columns; stored placeholders are cleared at the next boot
6+
7+
Clause-②: no
8+
9+
Under ADR-0118 D1 a lookup to `sys_user` holds a user id or null, never a placeholder value. Four writers broke that, and a lookup holding a non-id drops the row from every join and report on it, silently.
10+
11+
**This supersedes the "The SLA sweep keeps its reserved `system:sla` actor for now" sentence of the unreleased `21411-approval-actor-person` changeset.** Both ship in one release; from it, the sweep records no actor.
12+
13+
- **Machine actors record no actor.**
14+
- The SLA sweep's `escalate` row, and the `approve` / `reject` an `auto_approve` / `auto_reject` escalation then records, have `actor_id` empty. Before, both held `system:sla`. The `escalate` row's comment still names the configured action.
15+
- The dead-run sweep's `recall` row has `actor_id` empty. Before, it held `system:dead-run`. Its comment still names the dead run and its status, and a submitter's own recall still records the submitter.
16+
- **Notifications name only a person.** The actor the plugin hands to `sys_notification.actor_id` (and so to each `sys_inbox_message.actor_id`) is the user the action's context vouches for, or nothing.
17+
- Before, a reassign, reminder, request for information, comment or send-back taken under a named position or email forwarded that address as the actor.
18+
- Before, every SLA notification forwarded `system:sla`. It now forwards no actor, as the out-of-office notifications already did.
19+
- **`reassign_from` / `reassign_to` are slot addresses.** A reassignment moves a pending-approver slot, so both columns hold the slot's address in its stored spelling: a user id, an email, or `position:<name>`. They are now text columns (max 255 characters, like `acted_as`) instead of `sys_user` lookups, which matches what they already stored.
20+
- Existing values need no rewrite, and an existing database keeps its columns as they are. On SQLite and PostgreSQL 16, booting the new declaration over a table created by the old one issues no DDL, keeps every stored value, and reports no schema drift for either column. A new database creates them as `text`, as it does `acted_as`.
21+
- The action log still resolves `reassign_from_name` / `reassign_to_name` where an address names an account: a user id, or an email an account carries. A position address has no name.
22+
- **Stored rows.** The boot-time repair that moves slot literals out of `actor_id` now also clears `system:sla` and `system:dead-run` from it, in the same pass and in the same idempotent way. A cleared sentinel gets no `acted_as`, because a sweep takes no slot. The boot log line reports the count as `sentinelsCleared`.
23+
- **For a report or integration that read these values:**
24+
- To find the SLA sweep's actions, read the `escalate` rows, and the decision that directly follows an `escalate` row whose comment names `auto_approve` or `auto_reject`. Do not test `actor_id` for `system:sla`.
25+
- To find a dead-run release, read the `recall` row whose comment names the run. Do not test `actor_id` for `system:dead-run`.
26+
- Read `reassign_from` / `reassign_to` as slot addresses. Do not expand them as `sys_user` references.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/verify": patch
3+
---
4+
5+
`bootStack` (and so `os verify`) no longer creates a data key file in the key home, and no longer seals its fixtures under a key the host already holds (#21499)
6+
7+
Clause-②: no
8+
9+
The harness composed the settings service with no crypto provider and bound the engine to a default `LocalCryptoProvider`. `bootStack` forces a development posture. In that posture, with no `OS_SECRET_KEY`, no `OS_DEV_CRYPTO_KEY` and no key file, both providers wrote a new key file into the key home. So `os verify`, a one-shot command over an in-memory database, left key material behind, and the next development-posture process on that host adopted it. On a host that already had a key, the harness sealed its throwaway fixtures under that real key.
10+
11+
- **What the harness uses now.** One `LocalCryptoProvider` over a random key held in this process's memory only. It never reads `OS_SECRET_KEY`, `OS_DEV_CRYPTO_KEY` or the key file, and it never writes anywhere. The settings service and the engine get the same instance, so `secret` fields and encrypted settings still seal and open on a host with no key at all.
12+
- **One key per process, not per boot.** Two `bootStack` calls over one `databaseFile` in the same process (the harness's restart) still open each other's secrets.
13+
- **Unchanged.** `BootOptions` and the rest of the public API, and `os verify`'s stdout and `--json` report. The one stderr line announcing the minted key file is gone.

‎content/docs/automation/approvals.mdx‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,9 @@ not a pending approver`); a request that isn't pending returns 409
512512
(`INVALID_STATE`). The decision records two facts on its `sys_approval_action`
513513
row: `actor_id` is the user who decided, and `acted_as` is the slot the decision
514514
took, in that slot's stored spelling — the multi-approver tally counts approvals
515-
by matching `acted_as` against the slate. Always go through
515+
by matching `acted_as` against the slate. A `reassign` row records the slot it
516+
moved the same way: `reassign_from` and `reassign_to` hold slot addresses (a
517+
user id, an email or a position address), not users. Always go through
516518
these endpoints — never resume the flow run directly, and since #3801 you
517519
**cannot**: `POST /api/v1/automation/{flow}/runs/{runId}/resume` answers 403 for
518520
a run parked on an `approval` node (including via a `subflow` pause) and changes
@@ -672,8 +674,9 @@ trace that a slate had been bypassed was the designated approver's later
672674
reaches a terminal state without a decision — it failed, was cancelled, timed
673675
out, or the process hosting it crashed — nothing is left to decide the request,
674676
so a periodic sweep finalizes it as `recalled` and releases the record. The
675-
audit row records the actor `system:dead-run` and names the run and its status,
676-
so it reads distinctly from a submitter's own recall.
677+
audit row records no actor — a sweep is not a user, so `actor_id` is empty,
678+
never a placeholder value (ADR-0118 D1) — and names the run and its status, so
679+
it reads distinctly from a submitter's own recall, which records the submitter.
677680

678681
The sweep only ever acts on a run it can positively confirm is terminal: a
679682
paused run (the normal state of a live approval), an unknown run, or an
@@ -724,7 +727,10 @@ that request's drawer directly instead of a generic list.
724727
`escalation` is real, not decorative: set `enabled: true` and `timeoutHours`,
725728
and pick an `action` — `notify` (default), `reassign`, `auto_approve`, or
726729
`auto_reject`. Auto decisions run through the normal decide path, so the flow
727-
resumes exactly as if a human had clicked. Every escalation writes an audit row.
730+
resumes exactly as if a human had clicked. Every escalation writes an audit row
731+
of kind `escalate` naming the configured action; it, the auto decision that
732+
follows it, and the escalation's notifications record no actor, because no
733+
person acted (ADR-0118 D1).
728734
`timeoutHours` counts **calendar (wall-clock) hours** — nights, weekends and
729735
holidays included, because the platform ships no business-hours calendar — so a
730736
request opened at 17:00 on a Friday with `timeoutHours: 4` escalates at 21:00

‎packages/core/src/artifact-packages.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989
* not disagree.
9090
*
9191
* ⛔ The other half of that reason — "and Zod strips undeclared keys" — is GONE,
92-
* not merely reworded. `ManifestSchema` is `strictObject` since #14192 and
92+
* not merely reworded. `ManifestSchema` is `strictObject` since commit 4d0d9445a and
9393
* `AssembledPackageBodySchema` inherits the closed posture through `.extend()`,
9494
* so an undeclared key on an entry is REFUSED by this very parse, by name, and
9595
* never reaches a clone to be dropped from. Defaults are what still move bytes;

‎packages/core/src/hot-reload.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ describe('[#12340] stateStrategy refusal', () => {
260260
expect(m).toContain('were removed');
261261
expect(m).toContain("Use 'memory'");
262262
expect(m).toContain('p'); // locates the offending plugin
263-
// The negative twin (#13179's strip): the prescription anchors on the
263+
// The negative twin (commit fd289be45's strip): the prescription anchors on the
264264
// ADR and the version — never on a tracker id the refused author
265265
// cannot resolve. Mirrors the spec-side door's own pin.
266266
expect(m).not.toMatch(/(?<![#&])#\d{3,5}(?![0-9A-Za-z])/);
@@ -385,7 +385,7 @@ describe('[#12428] startWatching refusal and the watch-handle removal', () => {
385385
expect(m).toContain('never watched');
386386
expect(m).toContain('scheduleReload');
387387
expect(m).toContain('p'); // locates the offending plugin
388-
// The negative twin (#13179's strip, extended to this door's sibling id):
388+
// The negative twin (commit fd289be45's strip, extended to this door's sibling id):
389389
// anchored on the ADR and the migration call, never on a tracker id.
390390
expect(m).not.toMatch(/(?<![#&])#\d{3,5}(?![0-9A-Za-z])/);
391391
});

‎packages/core/src/lite-kernel.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,14 @@ export class LiteKernel extends ObjectKernelBase {
3838
* A plugin object the DECLARED plugin contract refuses is refused here,
3939
* with `PLUGIN_CONTRACT_VIOLATION` — the same check, the same envelope,
4040
* that `ObjectKernel.use()` runs through `PluginLoader` (`plugin-contract.ts`
41-
* is the one statement both kernels call; #16721, maintainer ruling
41+
* is the one statement both kernels call; commit 51ae73123 landed maintainer ruling
4242
* 2026-09-08, option A under #9864's precedent that the kernels converge).
4343
*
4444
* This method used to write the object straight into the registry, so the
4545
* same plugin was accepted by this kernel and refused by `ObjectKernel` —
4646
* and `AGENTS.md` names THIS kernel for tests, so a plugin could be green
4747
* in vitest and refused at production boot. Measured before converging
48-
* (#16721 step 1): of 813 `LiteKernel.use()` calls reachable in this
48+
* (step 1, before commit 51ae73123): of 813 `LiteKernel.use()` calls reachable in this
4949
* repository's suites, 807 were accepted by the schema unchanged and the
5050
* six refusals came from three test-local fixture objects, none of them
5151
* product code.
@@ -69,7 +69,7 @@ export class LiteKernel extends ObjectKernelBase {
6969
use(plugin: Plugin): this {
7070
this.validateIdle();
7171

72-
// Same check, same envelope, as `ObjectKernel.use()` (#16721).
72+
// Same check, same envelope, as `ObjectKernel.use()` (commit 51ae73123).
7373
assertPluginContract(plugin);
7474

7575
registerPluginByName(this.plugins, plugin, this.logger);

‎packages/core/src/metadata-service-contract.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
* - `check:meta-type-normalized` (`scripts/check-meta-type-normalized.mjs`)
4242
* is the CI gate whose whole job is to refuse a DECISION made on the
4343
* un-normalized `:type` — its header carries the three authorization
44-
* bypasses (#3984, #5881, #6241) that made the direction a rule. Its scan
44+
* bypasses (#3984, #5881, the one commit 83a3b1f2e closed) that made the direction a rule. Its scan
4545
* surface is `packages/rest/src`; what this module converges with is its
4646
* DIRECTION: normalize once, at the entry, and let every decision — here,
4747
* every store key — read the normalized value;
@@ -65,7 +65,7 @@
6565
* Row 3: a `data` that is not a plain object cannot be a metadata document.
6666
* The pre-ruling `MetadataFacade` accepted such a write and filed it under the
6767
* literal key `undefined` — readable back through no member (silent loss, the
68-
* #6725 family) — and the interim fix coerced it into a `{ name, content }`
68+
* same family as the defect commit 1507ba356 fixed) — and the interim fix coerced it into a `{ name, content }`
6969
* box, which collides with `content` being a REAL authorable field on live
7070
* metadata types (`doc`, `knowledge_document`). The ruling forbids both:
7171
* refuse, do not coerce into storability. `null` and arrays are refused with

‎packages/core/src/plugin-contract-enforcement.test.ts‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33
/**
44
* `kernel.use()` enforces the DECLARED plugin contract (#16049) — on BOTH
5-
* published kernels (#16721).
5+
* published kernels (commit 51ae73123).
66
*
77
* WHICH KERNEL. Groups A–F drive `ObjectKernel.use()`, the path #16049 wired
88
* (`PluginLoader.validatePluginContract`). Group G drives `LiteKernel.use()`,
9-
* which #16721 converged onto the SAME check — `assertPluginContract` in
9+
* which commit 51ae73123 converged onto the SAME check — `assertPluginContract` in
1010
* `plugin-contract.ts`, the one statement both kernels call. G is not a copy
1111
* of A–F: it pins the cases whose answer DIFFERED between the kernels before
12-
* #16721, the parity of the envelope for one input, and the two orderings
12+
* commit 51ae73123, the parity of the envelope for one input, and the two orderings
1313
* `LiteKernel.use()` owes (state before contract, contract before registry).
1414
*
1515
* WHY THIS FILE EXISTS. `PluginSchema` (`@objectstack/spec`,
@@ -424,7 +424,7 @@ describe('E — `version` is the NINTH enforced key, and admitting it refused no
424424

425425
describe('G — the SAME contract on LiteKernel.use() (#16721)', () => {
426426
/**
427-
* Before #16721 every refusal above had an accepting twin on this kernel:
427+
* Before commit 51ae73123 every refusal above had an accepting twin on this kernel:
428428
* `LiteKernel.use()` wrote the object straight into its registry, so the
429429
* object group A refuses mounted routes here. `AGENTS.md` names this
430430
* kernel for tests, so "green in vitest, refused at boot" was the shape
@@ -469,7 +469,7 @@ describe('G — the SAME contract on LiteKernel.use() (#16721)', () => {
469469
['staticPath', { name: '@os-fixture/lite-ui-no-static-path', type: 'ui', slug: 'lite-ui-no-static-path' }],
470470
['slug', { name: '@os-fixture/lite-ui-no-slug', type: 'ui', staticPath: UI_STATIC_PATH }],
471471
] as const)('refuses a `ui` plugin with no `%s`, naming the key and the spec code (#16334 reaches this kernel now)', (key, overrides) => {
472-
// The two inputs #16721 was filed on: refused by `ObjectKernel` (group F),
472+
// The two inputs behind commit 51ae73123: refused by `ObjectKernel` (group F),
473473
// and until now stored verbatim here — the hono auto-discovery pin's
474474
// group F carried the accepting readings and was rewritten with this.
475475
const kernel = makeLiteKernel();
@@ -542,7 +542,7 @@ describe('G — the SAME contract on LiteKernel.use() (#16721)', () => {
542542
// was excluded from the schema check it was the ONE declared key this
543543
// kernel did not judge at all: `version: 'v1.0.0'` registered here and
544544
// was refused by `ObjectKernel` at boot — precisely the green-in-vitest,
545-
// refused-in-production split #16721 converged the other eight keys to
545+
// refused-in-production split commit 51ae73123 converged the other eight keys to
546546
// close. It now travels the ordinary envelope.
547547
const kernel = makeLiteKernel();
548548
const bad = fixture({ name: 'com.example.lite-bad-version', version: 'v1.0.0' });
@@ -567,7 +567,7 @@ describe('G — the SAME contract on LiteKernel.use() (#16721)', () => {
567567
// "An author gets ONE refusal, with the same code and message shape,
568568
// from either kernel." `ObjectKernel.use()` re-wraps a failed load as
569569
// `Failed to load plugin: <name> - <message>` for EVERY load failure —
570-
// its existing wrapper, untouched by #16721 — so the parity to pin is
570+
// its existing wrapper, untouched by commit 51ae73123 — so the parity to pin is
571571
// that the LiteKernel message is exactly what follows that prefix.
572572
const make = () => fixture({ name: '@os-fixture/parity', type: 'ui', staticPath: UI_STATIC_PATH, slug: 'Not A Slug' });
573573

@@ -596,7 +596,7 @@ describe('G — the SAME contract on LiteKernel.use() (#16721)', () => {
596596
});
597597

598598
it('ORDER — state is checked before the contract: after bootstrap the refusal is the idle one', async () => {
599-
// `validateIdle()` first, then the contract — the wiring #16721 step 1
599+
// `validateIdle()` first, then the contract — the wiring step 1 (before commit 51ae73123)
600600
// measured with. A kernel that can no longer register plugins says so,
601601
// and does not run the schema over an object it would not store anyway.
602602
const kernel = makeLiteKernel();

0 commit comments

Comments
 (0)