Skip to content

Commit 296d734

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-21468-walled-form-withdrawal
2 parents b5e4b51 + aa46322 commit 296d734

344 files changed

Lines changed: 25747 additions & 2199 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: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/platform-objects': patch
4+
---
5+
6+
feat(spec)!: an agent's `memory` contract states exactly what the runtime honours — `maxEntries` and `reflectionInterval` are required once long-term memory is enabled, `longTerm.store` is retired, and the block is `live`, enforced by the cloud AI runtime (#20274)
7+
8+
**BREAKING** — `agent.memory` narrows to what the cloud AI runtime, the one runtime
9+
that executes agents, actually does with it. That runtime recalls the newest
10+
`maxEntries` distilled notes for the user before the first round, writes one note
11+
every `reflectionInterval` delivered interactions, evicts notes beyond `maxEntries`,
12+
and keeps them in its own database store. Before an agent's first turn it refused
13+
exactly the declarations this spec still accepted, so authoring now refuses them,
14+
by name, with a prescription (ADR-0049 enforce-or-remove):
15+
16+
- **`longTerm.maxEntries` and `reflectionInterval` are required when
17+
`longTerm.enabled` is true.** No default is declared for either: none has a
18+
measured basis, and the runtime adds none.
19+
- **`reflectionInterval` is refused without an enabled `longTerm`** — a reflection
20+
writes a long-term note, so with none enabled it would do nothing.
21+
- **`longTerm.store` is retired as a whole key.** The memory store is platform
22+
infrastructure, not agent metadata: the runtime keeps the notes in its own
23+
database store, and refused `vector` (the key's default, so what an omitted
24+
`store` parsed to) and `redis`. Its old spellings `backend`, `storage` and
25+
`provider` under `longTerm` are answered with the same prescription instead of
26+
being steered onto `store`.
27+
28+
`longTerm.enabled` is unchanged.
29+
30+
### FROM → TO
31+
32+
| before | what to write instead |
33+
| --- | --- |
34+
| `memory.longTerm.store` — any value, `database` included | delete the key; where the notes are kept is the platform's choice. |
35+
| `longTerm: { enabled: true, … }` without `maxEntries` | add `maxEntries`: how many distilled notes are kept for each user (an integer of at least 1). |
36+
| `longTerm: { enabled: true, … }` without `memory.reflectionInterval` | add `reflectionInterval`: how many delivered interactions pass between the reflections that write a note (an integer of at least 1). |
37+
| `memory.reflectionInterval` without `longTerm.enabled: true` | enable long-term memory with both numbers, or delete `reflectionInterval`. |
38+
39+
**The one-line fix: declare `maxEntries` and `reflectionInterval` when `longTerm.enabled`; delete `store`.**
40+
`os migrate meta --from 17` lists the mechanical edits for existing sources (the
41+
`store` deletion); the two numbers are the author's to choose.
42+
43+
Each refusal is a parse error at the key's own path, naming the key and the fix, and
44+
`store` also fails `tsc` (its input type is `never`).
45+
46+
### The retirement kit
47+
48+
- **Tombstone.** `longTerm.store` is a `retiredKey()` carrying the prescription; the
49+
three old alias spellings moved from `aliases` to `guidance`, because an alias may
50+
not steer an author onto a tombstone.
51+
- **The contract check** is a refinement on `memory` (`reflectionInterval` is
52+
`longTerm`'s sibling), one `custom` issue per missing or misplaced key. A JSON
53+
Schema cannot state a value-conditioned requirement in the closed projection list,
54+
so the published `ai/Agent` schema (and the four installed-package schemas that
55+
embed agents) names the site in `x-dropped-refinements`, recorded in
56+
`dropped-refinements.baseline.json`.
57+
- **D2 conversion `agent-memory-long-term-store-removed`** (step 18, retired from the
58+
load path): it deletes `store` from `memory.longTerm`, whatever it holds — the
59+
delete is lossless, because no value of it ever chose a backend. Stored
60+
`sys_metadata` agent rows and built artifacts replay it; one notice per agent. It
61+
supplies neither number.
62+
- **D3 entry `agent-memory-store-retired-and-limits-required`** carries the judgement
63+
the conversion cannot make: the two numbers an enabled `longTerm` now requires.
64+
- **`RETIRED_KEYS_BY_MAJOR[18]`** registers `ai/Agent:memory.longTerm.store`.
65+
- **No deprecation window**, per the project's startup-stage posture.
66+
67+
### Describes and the liveness ledger
68+
69+
- `agent.memory` drops `[EXPERIMENTAL — not enforced]`: it states that the cloud AI
70+
runtime enforces it and that the open framework edition does not run agents.
71+
`longTerm`, `enabled`, `maxEntries` and `reflectionInterval` each state what the
72+
runtime does with them.
73+
- The ledger row moves `experimental` → `live`, citing the cloud reader
74+
`agent-runtime.ts#compileAgentMemory` (via `AgentRuntime.resolveTurnGuardrails`),
75+
the enforcement in `ai-service.ts` and the store `agent-memory.ts#AgentMemoryStore`,
76+
as attested by the cloud seat's reading at cloud `ef5a4344`, `verifiedAt`
77+
2026-10-02. `os lint` / `os validate` no longer warn
78+
`liveness-experimental-property` on an agent that sets `memory`.
79+
- ⚠️ **The window, stated.** At `ef5a4344` the cloud reader still reads `store`: it
80+
honours `database` only and refuses `vector` and `redis`. Cloud drops `store` in
81+
that one reader once this release reaches its pin, and no earlier.
82+
83+
### The agent form's help texts
84+
85+
- The `memory` row's help text on the agent metadata form named short-term memory,
86+
a key the schema refuses. It now states what memory does and that `maxEntries`
87+
and `reflectionInterval` are required once long-term memory is enabled.
88+
- The neighbouring `planning` row named a strategy and a replan switch the schema
89+
does not declare; it now states the one key it has, the iteration cap.
90+
- The `platform-objects` metadata-form catalogs follow: the English leaves are
91+
regenerated, and the `zh-CN`, `ja-JP` and `es-ES` leaves are authored, not copied.
92+
93+
⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is
94+
published, and tenant-authored agents were not measured. This repo authors no
95+
`longTerm` outside `packages/spec`, and no cloud built-in agent declares one.
96+
97+
Clause-②: yes (narrowing)
98+
99+
<!-- adr-0087: registered agent-memory-long-term-store-removed, agent-memory-store-retired-and-limits-required -->
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
The list-view field-reference rule no longer walks a list view's own `tabs[].filter`
6+
7+
Clause-②: no
8+
9+
The list view's own `tabs` is a `retiredKey` tombstone on every list-view shape, and this rule judges the parsed stack, so the key could never reach the walk: the parse refuses it first, with its prescription. The dead branch is deleted. The rule still judges `filter` and `userFilters.tabs[].filter` exactly as before.
10+
11+
No finding changes for any stack that `os validate`, `os lint` or `os build` accepts.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@objectstack/metadata-protocol": patch
3+
---
4+
5+
`computeViewReferenceDiagnostics` no longer walks a list view's own `tabs[].filter`
6+
7+
Clause-②: no
8+
9+
The list view's own `tabs` is a `retiredKey` tombstone on every list-view shape. The write door refuses it, and a stored or artifact-shipped body has it stripped by the conversion replay before it is served, so the read could never see it. A served body that still carries it is already badged by the spec diagnostics (`computeMetadataDiagnostics`), with the tombstone's prescription. The `userFilters.tabs[].filter`, `filterableFields` and `kanban` checks are unchanged.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
Liveness ledger: the view container's body `name` row stays `dead`, and its note now states what the platform actually does with the key
6+
7+
Clause-②: no
8+
9+
- The old note said the body copy was "a copy nobody reads". Measured, the metadata door stamps the save name into every saved view body that has none, containers included (`normalizeViewMetadata` in `@objectstack/metadata-protocol`). Its overlay paths key on that stamped copy: `hydrateOverlayIntoRegistry` registers no body without a `name`, and `mergePackageAwareOverlay` slots an overlay row by it.
10+
- The verdict is unchanged, because the ledger's `live` means that authoring the key changes runtime behaviour. An authored container `name` only restates the key the container already registers under, or contradicts it. `os validate` and `os lint` keep warning `liveness-dead-property` ("drop it").
11+
- The note records why the key is kept rather than tombstoned: the door's own saves stamp it, so a tombstone would refuse the platform's own writes. A maintainer ruling also refused a spec-level forbid of a container's `name`.
12+
- It corrects the old attribution too. Artifact-shipped containers and the metadata-validation sweep author no `name`; what was read as theirs is the door's stamp.
13+
- The ledger README's `view` cell says the same. The `view.list.tabs` row's note now records that the two author-time walks that still read a list view's own `tabs` are deleted.
14+
- A comment in `system/i18n-resolver.ts` that still called the list view's own `tabs` a live carrier now says the key is a tombstone and `UserFiltersSchema.tabs` is the one carrier.
15+
- ⛔ No schema, parse, export, status or accept-set change.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/lint': patch
3+
---
4+
5+
Flow, hook, action, approval and expression rule findings no longer cite tracker numbers; each one states the decision behind it in words
6+
7+
Clause-②: no
8+
9+
Some findings these rules show to authors through `os validate`, `os lint` and `os build`, and the startup-registry findings a plugin author reads, pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does.
10+
11+
- Flow patterns: the record-change date-equality hint and the date-equality filter hint name the declarative alternative, a `schedule` flow whose start node carries a `config.timeRelative` descriptor; the unscoped `runAs` hint says `runAs` is enforced, so a run with no trigger user has its data operations refused rather than run unscoped; the unbounded bulk-write hint says `multi: true` is how a flow declares bulk intent and that the engine admits a whole-object write declared that way; the revise-target hint says the run-resume route continues a pause on a service-owned node type only through the service that owns it; the two interpolation hints say a flow node value is a string template in which only single-brace tokens resolve.
12+
- Startup-registry findings: the open-vocabulary notes say the engine judges node types only once the vocabulary is sealed at `kernel:bootstrapped`; the prescription describes the lazy cache resolution and the ADR-0104 attestation by what each does; the assertive-wording finding describes its two incidents, and how each was fixed, in words.
13+
- Expression findings: the field-level `visibleWhen` consequence names the `current_user` binding ADR-0089 D1 gives every runtime record surface; the retired `script` keys finding says spec 17 made `script` a call to a registered function and nothing else.
14+
- Trigger readiness: the array `triggerType` hint says multi-event arrays are deferred until two independent projects need a combination other than created-or-updated.
15+
- Body writes, readonly writes and approvals: the discarded `ctx.record` write says the snapshot stays read-only by design and an action writes through `ctx.api`; the `readonlyWhen` write finding says a bulk update strips the field from every matched row once any one of them is locked; the `queue` approver finding says the type was deprecated rather than built; the empty-slate hint says the admin override may act on any pending request, so that one nobody in its slate can decide never stays stuck.
16+
- The other findings drop a citation the sentence already explained.
17+
18+
Text only: no rule id, severity, condition or finding moves. A tool or test that matches the old text (for example a tracker-number suffix) needs the new spelling.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-automation': minor
4+
'@objectstack/metadata-protocol': minor
5+
'@objectstack/trigger-api': minor
6+
'@objectstack/runtime': minor
7+
---
8+
9+
feat(automation): a flow's credentials live in a write-only channel, not in its stored definition (#20790)
10+
11+
Clause-②: yes (widening)
12+
13+
A flow's two credentials, an inbound hook's `secret` on its start node and an `http` node's `signingSecret`, are no longer stored in the flow definition. The metadata save door moves each explicit value into a new platform object, `sys_flow_credential`, owned by `@objectstack/service-automation`. Its one field is `type: 'secret'`, so the engine encrypts it through the host crypto provider, masks it on every read, and dereferences it only through `resolveSecretField`. This is the same seam the webhook signing secret uses. The stored row, every new version-history row and the row's content hash carry no credential. The engine reads the value only when it verifies an inbound post or signs an outbound request. Authoring does not change: you still write the literal, a save that leaves the key out (the form every read serves) keeps the stored secret, `''` clears it, and only an explicit new value rotates it.
14+
15+
**⚠️ Rotate every inbound and outbound flow secret that existed before this release.** On the first boot with a crypto provider, or when a provider registers after a boot without one, each stored flow that still carries a credential is moved into the channel once, and the log prints one notice per flow: `[Automation] flow '<name>' (<state>): … was stored in cleartext … ROTATE: …`. The move guarantees no new copy, but the version-history rows and audit snapshots written before it stay as they were (both are append-only), so an administrator could have read those values. To rotate, save the flow with a new `config.secret` / `config.signingSecret`, then give the new value to whoever signs posts to the hook or verifies its deliveries. The run is recorded in `sys_migration` as `flow-credential-channel` (flow names only, never values). Packaged flows are not moved: a packaged flow's literal stays its source of truth, and where the channel holds a row for it, the row wins at verification.
16+
17+
What else changes:
18+
19+
- **`@objectstack/spec`**: `PLATFORM_OBJECTS_BY_PACKAGE['service-automation']` lists `sys_flow_credential`.
20+
- **`@objectstack/metadata-protocol`**: `registerCredentialChannel(type, channel)` registers a type's write-only credential channel (exported type `MetadataCredentialChannel`). `saveMetaItem` stores the body the channel returns, after the carry-forward and before the put. The runtime authoring gate reads the channel's held positions as present, on an active save and when a draft is published. `SysMetadataRepository.restoreVersion` takes `deriveRestoredBody`, shaped like `promoteDraft`'s `deriveActiveBody`. Rollback and revert pass the channel's strip, so restoring a version written before the move never puts its credential back at rest, and the channel keeps its current credential.
21+
- **`@objectstack/service-automation`**: exports `SysFlowCredential`, `FlowCredentialChannel` and `migrateFlowCredentialsIntoChannel`. `AutomationEngine` gains `setFlowCredentialSource`, `holdsFlowCredential`, `resolveFlowCredential` and `flowCredentialHoldings`. An `api` binding carries `resolveSecret()`, which reads the secret at verification time, so a rotation applies to the next post. A draft save never rotates the live secret; publishing the draft promotes it. Deleting a flow's stored row drops its credentials.
22+
- **`@objectstack/trigger-api`**: `FlowTriggerBinding.resolveSecret` arms a hook without a literal. A post whose secret cannot be read is answered `503 SERVICE_UNAVAILABLE` and is never verified against nothing.
23+
- **Refused now, loudly**:
24+
- With no crypto provider, a save that carries a flow credential is refused with `503 SERVICE_UNAVAILABLE` before anything is written. Register a provider (`setCryptoProvider`) and save again.
25+
- The clone door (`POST /api/v1/automation/:name/clone`) refuses a source that holds a credential, as a literal or in the channel, with `409 RESOURCE_CONFLICT`, because a copy would share it. ⚠️ Accepted cost: a packaged inbound flow can no longer be cloned in one step. Author the copy as a new flow under a new name, with its own secret.
26+
27+
<!-- adr-0087: not-required (no-migration-prescription) the one-time move rewrites stored flow rows through the metadata save door itself, at boot; no authorable key, spelling, export or stored shape is retired, so an author or an upgrading agent has nothing to rewrite. The operator's action is the rotation stated above, which is not a FROM to TO mapping. The gate reads this changeset as non-breaking; the disposition is stated for the migration the ruling named. -->
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/service-analytics': patch
4+
'@objectstack/formula': patch
5+
'@objectstack/objectql': patch
6+
---
7+
8+
Published comments that named `driver-memory`'s retired reference matcher as a live filter backend now name what replaced it
9+
10+
Clause-②: no
11+
12+
`driver-memory`'s reference matcher (`memory-matcher.ts`) was retired in commit `8fec76a2b`. Four published packages still described it as a live surface in text that ships:
13+
14+
- `@objectstack/spec`:
15+
- The backend table in the filter-logic conformance docblock, which ships in `data/index.d.ts` and `data/index.d.mts`, now lists the in-memory backend as `driver-memory`'s query path (`normalizeFilterCondition`, then mingo) where it listed `memory-matcher`, and says the matcher held that row until commit `8fec76a2b` retired it.
16+
- `src/data/filter.zod.ts` ships as source. In it, the `$icontains` implementation table lists `driver-memory`'s query path and analytics face, both on `asciiCaseInsensitiveRegexSource`. The `$like` / `$ilike` and `$empty` tables keep the matcher only in a note that commit `8fec76a2b` retired it. The `foldAsciiCase` docblock counts five JS evaluation faces where it counted six. The `asciiCaseInsensitiveContains` docblock names objectql's `having` and `formula` as its callers. The string-ordering note says `driver-memory`'s query path hands the comparison to mingo. Of these, the `foldAsciiCase`, `asciiCaseInsensitiveContains` and `FILTER_OPERATORS` docblocks also ship in the filter declaration chunk (`filter.zod-*.d.ts` / `.d.mts`).
17+
- `src/ui/view.zod.ts` ships as source. It now says that `driver-memory`'s query path runs `assertFilterConditionShape` through `convertToMongoQuery`, where it said `match()` did.
18+
- A comment inside `FILTER_TEXT_CASES` ships in `data/index.js` / `.mjs` and `browser/data/index.js` / `.mjs`. It now says the reference matcher measured case-exact until commit `8fec76a2b` retired it.
19+
- `@objectstack/service-analytics`: two comments in `ObjectQLStrategy`, which ship in the JavaScript output (the first also in `index.d.ts` / `index.d.cts`), changed. The first names `driver-memory`'s query path, not its matcher, as a face that pins `{$not: {}}` as the zero-row filter. The second says in the past tense that `memory-matcher.ts` read `$regex` as a real regex, until `$regex` was retired and commit `8fec76a2b` retired the matcher too.
20+
- `@objectstack/formula`: the comment over the `$icontains` arm in `matches-filter.ts` ships in `index.js` / `index.mjs`. It now names objectql's `having` as the other caller of `asciiCaseInsensitiveContains`. It says `driver-memory`'s reference matcher called it until commit `8fec76a2b` retired it, and that `driver-memory`'s query path folds through `asciiCaseInsensitiveRegexSource`.
21+
- `@objectstack/objectql`: the comment over the `having` walker's `$notContains` arm in `having-filter.ts` ships in `index.js` / `index.mjs` and `core.js` / `core.mjs`. It now says the record-at-a-time faces (`formula` and this walker) answer the predicate on a stored value that is not a string, as `driver-memory`'s reference matcher did until commit `8fec76a2b` retired it.
22+
23+
Comment only: no export, type, error code, status, message text or runtime behaviour changes.

0 commit comments

Comments
 (0)