Skip to content

Commit d373752

Browse files
committed
Merge origin/main into claude/issue-21411-approval-actor-person (brings #21458's ApprovalActionRow.acted_as)
Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
2 parents 03bdc62 + cba4297 commit d373752

112 files changed

Lines changed: 3962 additions & 1409 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: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/service-automation': patch
3+
'@objectstack/plugin-audit': patch
4+
---
5+
6+
Automation refusals, prescriptions, log lines and run-object field help, and the activity type help, no longer cite tracker numbers; each one states the decision behind it in words
7+
8+
Clause-②: no
9+
10+
Some strings these two packages show to flow authors, operators and administrators 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.
11+
12+
- `@objectstack/service-automation`: the refusal for a `fieldValues` write map says a runtime alias for it was rejected by design, so the node keeps one strict `fields` key; the refusal for a screen field's `visibleIf` says a predicate under any other key is never read, so the field always shows, and a `required` field meant to stay hidden then blocks the screen from ever being submitted; the undeclared-config-key refusal says the built-in node types were reconciled so that every key their executors read is declared; the unknown-function error in a flow value expression says such a name is refused rather than evaluated to null, which would write the field as undefined; the inert-connector warning says entries without a `provider` are catalog descriptors, while an entry that names a `provider` is a connector instance that provider's installed executor materializes; the `sys_automation_run` field help says the paused node's type decides who may continue a run (an approval pause only through its owning service), that rows written before run history recorded its trigger were not backfilled, and that a finished run's bounded step log keeps its per-node detail across a restart; three bridge debug lines say what each bridge provides. The bulk-intent guidance, the degraded-connector dispatch error and retry lines, the user-less `runAs` warning and refusal, the unclaimed-branch warning, the script-function and node-config refusals and the `sys_flow_dispatch` description drop their citations.
13+
- `@objectstack/plugin-audit`: the `sys_activity` `type` help, whose English text all four shipped locale bundles carry, says the vocabulary is open by decision, not a gap awaiting enforcement.
14+
15+
Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/platform-objects': patch
4+
---
5+
6+
feat(spec)!: retire `agent.lifecycle`, the agent conversation state machine, and with it the XState `StateMachineSchema` family — a conversation phase is a skill with `triggerConditions`, orchestration is Flow, record transitions are the `state_machine` validation rule (#21320)
7+
8+
**BREAKING** — `agent.lifecycle` was parsed and never read. No runtime, in this
9+
repository or in the cloud AI runtime that executes agents, moved an agent through a
10+
declared state or refused an undeclared transition, so an authored machine changed
11+
nothing an agent did (ADR-0049 enforce-or-remove). Enforcing it would have meant a
12+
statechart interpreter beside Flow, the two-engine shape ADR-0020 rejected. Authoring
13+
now refuses the key by name, with a prescription, and TypeScript rejects it.
14+
15+
Its value schema had no other authorable door: ADR-0020 had already retired the XState
16+
shape as a record-lifecycle declaration and kept the file only for this key. So the
17+
family leaves the package with it.
18+
19+
### FROM → TO
20+
21+
| before | what to write instead |
22+
| --- | --- |
23+
| `agent.lifecycle` — any value | delete the key. |
24+
| a conversation phase in the machine (its own instructions and tools) | a skill with its own `instructions` and `tools`, selected by its `triggerConditions`, listed in the agent's `skills`. |
25+
| a multi-step process in the machine | a Flow. |
26+
| a record's status transitions in the machine | a `state_machine` validation rule in the object's `validations`: `{ type: 'state_machine', field, transitions: { from: [to, …] } }`. |
27+
| `StateMachineSchema`, `StateNodeSchema`, `TransitionSchema`, `ActionRefSchema`, `GuardRefSchema` and the types `StateMachineConfig`, `StateNode`, `StateNodeConfig`, `Transition`, `ActionRef`, `GuardRef` from `@objectstack/spec/automation` | no replacement: declare the shape your code needs itself, or drop it. For record transitions, `StateMachineValidationSchema` in `@objectstack/spec/data` is the enforced shape. |
28+
| `StateNodeConfig` from `@objectstack/spec` or `@objectstack/spec/ai` | removed with the family; nothing in those entries mentions it any more. |
29+
30+
**The one-line fix: delete `lifecycle`; put phase-scoped instructions and tools in
31+
skills with `triggerConditions`, and orchestration in Flow.** `os migrate meta --from 17`
32+
lists the mechanical edits for existing sources (the `lifecycle` deletion). Where each
33+
deleted machine's intent goes is the author's judgement.
34+
35+
The refusal is a parse error at `lifecycle` naming the key and the fix, and the key
36+
fails `tsc` (its input type is `never`).
37+
38+
### The retirement kit
39+
40+
- **Tombstone.** `lifecycle` is a `retiredKey()` on `AgentSchema` carrying the
41+
prescription; the agent metadata form no longer offers it.
42+
- **D2 conversion `agent-lifecycle-removed`** (step 18, retired from the load path):
43+
it deletes `lifecycle` from every agent, whatever it holds. The delete is lossless,
44+
because no value of it ever changed what an agent did. Stored `sys_metadata` agent
45+
rows and built artifacts replay it; one notice per agent. An object's ADR-0057
46+
`lifecycle` block shares the name and is not touched.
47+
- **D3 entry `agent-lifecycle-retired`** carries the judgement the conversion cannot
48+
make: which of the three destinations each deleted machine meant.
49+
- **`RETIRED_KEYS_BY_MAJOR[18]`** registers `ai/Agent:lifecycle`, and
50+
**`RETIRED_DEFS_BY_MAJOR[18]`** registers the five published defs
51+
`automation/StateMachine`, `automation/StateNode`, `automation/Transition`,
52+
`automation/ActionRef` and `automation/GuardRef`. Their reference page
53+
(`references/automation/state-machine`) is gone.
54+
- **No deprecation window**, per the project's startup-stage posture.
55+
56+
### The liveness ledger
57+
58+
The `agent.lifecycle` row moves `experimental` → `dead` with a REMOVED note
59+
(`verifiedAt` 2026-10-02); the tombstone keeps it in the walked shape. No `agent` row is
60+
`experimental` any more. `os validate` and every other parsing door refuse the key at
61+
parse, before any advisory runs. `os lint` reads the unparsed stack, so it now grades the
62+
key `liveness-dead-property` where it used to say `liveness-experimental-property`.
63+
64+
### `@objectstack/platform-objects`
65+
66+
The agent metadata-form catalogs drop the `lifecycle` row's label and help text in all
67+
four locales.
68+
69+
⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is
70+
published: tenant-authored agents, and code outside this repository importing the
71+
family's exports, were not measured. This repository authors no `agent.lifecycle`
72+
outside `packages/spec` and imports none of the family outside it; the pinned objectui
73+
checkout imports none of the family and reads no `agent.lifecycle`.
74+
75+
Clause-②: yes (narrowing)
76+
77+
<!-- adr-0087: registered agent-lifecycle-removed, agent-lifecycle-retired -->

‎.changeset/21326-secret-rewrap.md‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
'@objectstack/cli': minor
3+
'@objectstack/service-settings': minor
4+
---
5+
6+
feat(cli): `os secret rewrap` re-wraps version-1 `sys_secret` ciphertext under the current AAD derivation, each row under its holder's producer scope (ADR-0128 §4.2, #21326 stage 2)
7+
8+
Clause-②: yes (widening)
9+
10+
A ciphertext sealed before ADR-0128 D1–D3 carries the older binding over
11+
`(namespace, key)` alone, and still opens in this release. `os secret rewrap` moves
12+
the stored values to the current binding through `rotateKey`, the seam ADR-0128 §4
13+
names. It is an operator command: a dry run by default, `--apply` to write, and
14+
nothing on any boot or upgrade path invokes it. It has no HTTP surface.
15+
16+
- **The scope comes from the holder.** `sys_secret` records no producer, and a
17+
version-1 ciphertext binds no scope, so each row is re-sealed under the scope of
18+
the producer whose holder references it: `settings` for a `sys_setting.value_enc`
19+
handle, `object_secret_field` for a `secret:` ref on a business row,
20+
`datasource_credential` for a `sys_secret:` `credentialsRef`. The holders come from
21+
the same cross-producer reference union `os secret orphans` reads. A row nothing
22+
references, a row whose holders belong to different producers, and every row while
23+
a holder family could not be read are left as they are and counted, never re-sealed
24+
under a guessed scope. `--apply` refuses an incomplete union and names the family.
25+
- **Resumable.** A row already sealed under the current derivation is skipped as
26+
done, so a stopped run finishes the rest when re-run and a finished run writes
27+
nothing.
28+
- **Safe against a live deployment.** Each row is written by one conditional update,
29+
keyed on its id and the ciphertext the run read. A row a producer changed in
30+
between is not overwritten, and a re-run picks it up. A driver with no
31+
`updateMany` is refused before any row is opened.
32+
- **Fails closed.** A row that does not open, or whose re-seal does not open to the
33+
same plaintext under the same scope, is not written. The run finishes the rest and
34+
exits 1. The check happens before the write.
35+
- **Output is classes and counts only.** It never prints a plaintext, a ciphertext
36+
or a row id.
37+
38+
The command resolves its data key from `OS_SECRET_KEY`, `OS_DEV_CRYPTO_KEY` or the
39+
persisted key file, in the strict posture: it never mints a key, and it hands the
40+
settings service it boots the same provider so that service does not mint one
41+
either. With no key it refuses before opening any row.
42+
43+
`@objectstack/service-settings` publishes `ciphertextDerivationStatus` (and its
44+
`CiphertextDerivationStatus` type). It is `LocalCryptoProvider`'s own reading of
45+
which derivation sealed a stored ciphertext, read off its marker without opening it:
46+
`current`, `superseded` or `unknown`. The re-wrap classifies rows with it rather than
47+
restating the marker grammar.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@objectstack/service-analytics': minor
3+
---
4+
5+
fix(service-analytics)!: a caller-named analytics measure whose inferred source names no field (`_sum`, `*`, `*_sum`, an empty spelling) is refused with `INVALID_FIELD` / 400 at the analytics door, naming the spelling sent, on both strategies, before any statement is built (#21437)
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) a refusal at the analytics door of a caller-named measure spelling whose inferred source names no field. No authorable key, spelling, export type or stored shape moves: AnalyticsQuerySchema still parses every measures list it parsed (the refusal is a runtime rule on the request, not a schema change), CubeSchema and DatasetSchema are untouched, a member a cube declares is never minted and is served as before, the package index exports the same names with the same types, and no stored row is read or rewritten. A refused spelling had no answer to preserve: both strategies answered 500 for it, and which column a caller meant by an empty prefix is not something a ledger entry can rewrite. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers a caller-named measure spelling, and this diff adds none (not registered / already-registered); and the change is runtime behaviour, not a declaration (not runtime-interface-only / type-surface-only). -->
10+
11+
**BREAKING**: this narrows what `POST /api/v1/analytics/query` and its dry run `POST /api/v1/analytics/sql` accept in `measures`, on both strategies and every driver. It ships as `minor` under the launch-window convention for accept-set narrowings. No export, published type or error code changes.
12+
13+
**The rule.** A `measures` entry the cube does not declare is inferred: the bare `count` counts rows (`COUNT(*)`), and any other spelling aggregates one of the object's own fields, named before an aggregation suffix (`_sum`, `_avg`, `_average`, `_min`, `_max`, `_count_distinct`) or, with no suffix, by the whole spelling. The bare `count` is now the only spelling that reads the row wildcard `'*'`. A spelling whose source is empty or is `'*'` names no field, and it is refused with `400 INVALID_FIELD` before anything is executed. The error names the spelling as it was sent (`member`, with `param: 'measures'` and `cube`); a `<cube>.` qualifier is kept in the name.
14+
15+
**Before**, measured through `POST /api/v1/analytics/query` on SQLite, on the native-SQL and the ObjectQL strategy, on an ad-hoc cube and on an authored cube that does not declare the member:
16+
17+
- `_sum`, `_avg`, `_average`, `_min`, `_max`, their `<cube>.`-qualified forms, `*`, `*_sum`, `*_avg` and the empty spelling `''` answered `500 DATABASE_ERROR`, after a statement reached the database (`SUM(*)`, `AVG(*)`, `SUM()`).
18+
- `_count_distinct` and `*_count_distinct` answered `500 DATABASE_ERROR` on the native-SQL strategy (`COUNT(DISTINCT *)`). On the ObjectQL strategy the engine answered `400 INVALID_QUERY` after the aggregate was called.
19+
- The qualifier alone (`<cube>.`) answered `403 PERMISSION_DENIED` from the member-shape gate. It now answers the same `400 INVALID_FIELD`, because it names no field either.
20+
21+
**Now** each of those answers `400 INVALID_FIELD`, and no statement and no engine aggregate runs. `POST /api/v1/analytics/sql` refuses the same spellings instead of returning a statement that cannot run.
22+
23+
**What to write instead.** Ask for `count` to count rows, or put the field's name before the suffix: the sum of `amount` is `amount_sum`.
24+
25+
**Who is affected.** A caller that sent a measure spelling with nothing before the suffix, or the row wildcard itself. Every such request was already a 500. No example app, shipped dashboard, report, dataset, cube, doc or skill in this repository sends one. The console's analytics adapter composes a measure as the value field, an underscore and the aggregate function, so a widget whose value field is empty posts `_sum`. At the pinned `.objectui-sha` that adapter reads a 500 as an unknown failure and answers with its own client-side aggregation; it reads the 400 as a rejected request and surfaces it as an error.
26+
27+
**Unchanged.** The bare `count`; a field-prefixed spelling such as `amount_sum`; the no-suffix spelling of a field (`amount`); a measure a cube declares, including one declared under a key such as `_sum`, which is the cube's own vocabulary and is never inferred; and the authored-position twin of this rule, the `@objectstack/spec` parse refusal of `'*'` outside a `count` on a cube or dataset measure (#21409).
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): `ApprovalActionRow` declares `acted_as`, the pending-approver slot an approval action was taken as, beside the person in `actor_id`
6+
7+
Clause-②: yes
8+
9+
**What it declares.** One optional string member on `ApprovalActionRow` in `@objectstack/spec/contracts`, the row type of an approval request's action log (`IApprovalService.listActions`, served at `GET /api/v1/approvals/requests/:id/actions` and typed by the client SDK):
10+
11+
- `acted_as?: string` is the slot the action was admitted under, in the slot's stored spelling as it stood in the request's `pending_approvers`: a `position:<name>` address (or `role:<name>`, the deprecated pre-rename spelling), an email, or a user id.
12+
- It is never a person. The person who acted is `actor_id`, which under ADR-0118 D1 holds a `sys_user` id or nothing. A slot addressed by a user id carries that id in `acted_as` as the slot's address, which makes no claim about who acted.
13+
- Absent means the action was not admitted through a slot (a submitter's own action, a system action, or an admin override, which `via_override` marks), or the row was written before the slot was recorded. So absent alone never proves that no slot was involved.
14+
15+
**What moves for consumers.** Nothing in this package writes the member, and every existing export and member is unchanged: a row without `acted_as` conforms exactly as before. The approvals service is its producer, and that package's own changeset states when `listActions` starts returning it. Until then every row omits it, which is the member's declared absent case. A client that renders the action log can show `acted_as` beside the actor's name as the capacity the actor acted in.

‎content/docs/automation/workflows.mdx‎

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ ObjectStack no longer has a standalone Salesforce-style Workflow Rule authoring
88
type. Use:
99

1010
- **Flow** for event-triggered or scheduled automation.
11-
- **State machine metadata** for strict lifecycle transitions.
11+
- **A `state_machine` validation rule** for strict lifecycle transitions.
1212
- **Approval nodes** inside Flow for human approval pauses.
1313

1414
This page keeps the historical route but documents the current split.
@@ -60,42 +60,51 @@ registration/runtime.
6060

6161
## State machines for lifecycle constraints
6262

63-
Use `StateMachineSchema` when the core requirement is "this object can only move
64-
through these states by these events."
63+
Use a `state_machine` validation rule when the core requirement is "this record
64+
can only move through these states." It is one of the object's `validations`: a
65+
flat table of each state's allowed next states, enforced by the write path (see
66+
[State Machine](/docs/protocol/objectql/state-machine)).
6567

6668
{/* os:check */}
6769
```typescript
68-
import type { StateMachineConfig } from '@objectstack/spec/automation';
69-
70-
export const caseLifecycle: StateMachineConfig = {
71-
id: 'case_lifecycle',
72-
initial: 'new',
73-
states: {
74-
new: {
75-
on: {
76-
ASSIGN: { target: 'assigned' },
77-
},
78-
},
79-
assigned: {
80-
on: {
81-
RESOLVE: { target: 'resolved', cond: 'has_resolution' },
82-
ESCALATE: { target: 'escalated' },
83-
},
84-
},
85-
escalated: {
86-
on: {
87-
RESOLVE: { target: 'resolved', cond: 'has_resolution' },
70+
import { ObjectSchema, Field } from '@objectstack/spec/data';
71+
72+
export const SupportCase = ObjectSchema.create({
73+
name: 'support_case',
74+
label: 'Support Case',
75+
sharingModel: 'private',
76+
fields: {
77+
status: Field.select({
78+
label: 'Status',
79+
required: true,
80+
options: [
81+
{ label: 'New', value: 'new' },
82+
{ label: 'Assigned', value: 'assigned' },
83+
{ label: 'Escalated', value: 'escalated' },
84+
{ label: 'Resolved', value: 'resolved' },
85+
],
86+
}),
87+
},
88+
validations: [
89+
{
90+
type: 'state_machine',
91+
name: 'case_status_flow',
92+
field: 'status',
93+
events: ['update'],
94+
message: 'Invalid case status transition.',
95+
transitions: {
96+
new: ['assigned'],
97+
assigned: ['resolved', 'escalated'],
98+
escalated: ['resolved'],
99+
resolved: [],
88100
},
89101
},
90-
resolved: {
91-
type: 'final',
92-
},
93-
},
94-
};
102+
],
103+
});
95104
```
96105

97-
State machines describe valid transitions and guards. Use Flow nodes for side
98-
effects around those transitions when you need notifications, record updates, or
106+
The rule declares which transitions are legal. Use Flow nodes for side effects
107+
around those transitions when you need notifications, record updates, or
99108
external calls.
100109

101110
---

0 commit comments

Comments
 (0)