Skip to content

Commit be755de

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-20790-flow-hook-secret-seam
2 parents bd9a133 + 11905a4 commit be755de

129 files changed

Lines changed: 6396 additions & 1201 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/driver-sql': patch
3+
---
4+
5+
Provenance comments in `@objectstack/driver-sql` cite the commits and ADR 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 one now cites the commit in this repository's history that made the decision it describes, or the
11+
ADR that records it (ADR-0104's 2026-09-05 addendum). Some of these docblocks sit on exported members,
12+
so the reworded text appears in the published `index.d.ts` / `index.d.mts`, and comments that esbuild
13+
keeps appear in the JavaScript output.
14+
15+
Comment only: no export, type, error code, status, message text or runtime behaviour changes.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
"@objectstack/formula": minor
3+
---
4+
5+
fix(formula)!: `matchesFilterCondition` compares a bare-day upper bound as written; its own whole-day copy is deleted (ADR-0053 D-D1 items 5 and 9)
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) a change of how one runtime evaluator answers an ordering comparison, not of anything an author writes: no spec key, spelling, export or stored shape moves. FilterConditionSchema, every RLS policy, object and query definition parse and save as before, @objectstack/formula exports the same names with the same types, and no stored row is read or rewritten. What moves is the answer for a bare-day upper bound that reaches the evaluator without the shared lowering, which the seams already apply, so there is nothing for objectstack migrate meta to rewrite. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers a filter's bound semantics 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 the RLS write check admits on columns that are not `datetime`, and moves a few `engine.aggregate` answers that no seam lowers. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes.
12+
13+
**What is deleted.** `matchesFilterCondition` no longer reads a bare `YYYY-MM-DD` `$lte`, or a `$between` maximum, as "through that whole day", and no longer drops the bound on `9999-12-31`. It compares the value as written, as every other ordering operator here does, and as `driver-sql` compares it on the read. The whole day is applied once, at the seams that feed this evaluator, by the shared `lowerFilterCondition` (`@objectstack/spec/data`): the RLS compile seam lowers every policy filter on the object's declared `datetime` columns, the engine lowers `having` and `aggregations[i].filter` the same way, and the RLS write check judges a declared `date`, `datetime` or `time` column in its stored form. So a `check` on a `date` or `datetime` column answers exactly as before.
14+
15+
**The RLS write check now agrees with the read on other columns.** Measured through `ObjectQL.insert` and `SecurityPlugin` on `SqlDriver` (better-sqlite3), as a member whose policy has the same `using` and `check`:
16+
17+
- a `text` column under `record.title <= '2026-01-05'`, written as `'2026-01-05T15:00:00Z'` or `'2026-01-05 noon'`: the write was admitted while the read hid the stored row. It is now refused `PERMISSION_DENIED` / 403, and the read still hides it;
18+
- two `text` columns, `record.title <= record.code`, with `code` holding `'2026-01-05'`: the same, admitted before and 403 now, with the read hiding the row;
19+
- a `number` column under `record.amount <= '9999-12-31'`: the write was admitted because an epoch number read as an instant on the last supported day. A number is not less than a day string, so it is now 403. The engine refuses the same comparison in a `where` (`INVALID_FILTER` / 400: a day string is not a number).
20+
21+
The access explanation (`explain`) judges a stored row with this evaluator, so its row verdict moves the same way: for the two `text` cells it now says hidden, as the read does.
22+
23+
**`engine.aggregate` answers that no seam lowers.** A `{ $field }` referent is per row, so no seam can lower it. These positions are now compared as written:
24+
25+
- two declared `text` columns of one class, at a per-aggregation `filter` or between two `having` group columns: `'2026-01-05 noon'` against `'2026-01-05'` is no longer counted or kept, which is what the same comparison answers in a `where`;
26+
- the pairs the class rule cannot judge because a side has no declaration: an object the registry does not declare, and an audit-opt-out object's row-carried `created_at` / `updated_at` against a `date`. An instant on the due day is no longer counted against that bare day;
27+
- a direct `applyInMemoryAggregation` call, which applies no class rule.
28+
29+
**The remedy.** Compare a `datetime` with a `datetime` and a `date` with a `date`. A `datetime` against a calendar day has no single answer across SQL and memory, and a declared pair of the two is already refused. A number compared with a day string has no answer at all: compare a number with a number. A caller that evaluates a filter on a `datetime` column without passing a seam lowers it first with `lowerFilterCondition(filter, { isDatetimeColumn })` to get the whole-day reading.
30+
31+
**Unchanged.** A `check` on a declared `date`, `datetime` or `time` column, a `{ $field }` pair of two `date` or two `datetime` columns (with or without `addDays`), a full-ISO bound, `$gte` / `$gt` / `$lt` and `$eq`.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
"@objectstack/plugin-security": minor
3+
---
4+
5+
fix(plugin-security)!: a row-level policy that compares a numeric column with a comparand that is not a number is refused at the RLS compile seam, read and write alike, as the engine's `where` refuses the same comparison
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) a refusal of a compiled policy comparand at the RLS compile seam, the same comparand the engine's where door already refuses: no authorable key, spelling, export or stored shape moves. RowLevelSecurityPolicySchema and every permission set parse and save as before, the predicate's text is untouched, @objectstack/plugin-security exports the same names, and no stored row is read or rewritten. Which number the author meant is not something a ledger entry can decide, so there is nothing for objectstack migrate meta to rewrite. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers a filter comparand's type and this diff adds none (not registered / already-registered); and the change is runtime behaviour plus one ADDITIVE optional member (number) on the published RlsFieldGuard type, with no published interface or type narrowed or removed (not runtime-interface-only / type-surface-only). -->
10+
11+
**BREAKING**: this narrows which row-level policies the RLS compile seam hands to its two consumers, the read and the write check. It ships as `minor` under the launch-window convention for accept-set narrowings. No export is added or removed. One published type gains a member: `RlsFieldGuard`, the type of the optional `fieldGuard` argument of the root-exported `RLSCompiler.compileFilter`, gains the optional `number` member (each declared column's `type`, and a formula's `returnType`). It is an additive optional member, not a change of what is accepted.
12+
13+
**What was accepted before.** A policy such as `record.amount <= '9999-12-31'` on a `number` column compiled, and its `using` and `check` both reached their consumers unjudged. Measured through `ObjectQL.insert` and `SecurityPlugin` on `SqlDriver` (better-sqlite3), as a member: the write of `amount: 5` was admitted (`@objectstack/formula`'s deleted whole-day copy read the number as an instant), and the read showed the stored row, because SQLite orders an integer before any text. That read was measured on SQLite only; PostgreSQL was not run for this change. The same comparison in a caller's `where` is refused `INVALID_FILTER` / 400 by the engine's number-comparand door.
14+
15+
**What is refused now.** The seam runs the spec's number-comparand verdict (`numberComparandDoorVerdict`, `@objectstack/spec/data`), the one the engine's `where` door consults, on every compiled policy filter, after the shape door and before the comparand-type door. On a column the object declares numeric, a comparand that is not a number (a string the platform's numeric grammar does not read, such as `'9999-12-31'` or `'abc'`, a boolean, a `Date` or a list) drops the policy through the existing fail-closed route: the read is filtered by the deny sentinel and returns no rows, the write is refused `PERMISSION_DENIED` / 403, and a WARN line names the policy, the clause and the comparand. The line's detail is written for the clause it refused: for `check`, which the write check evaluates in-process, it names no driver bind. A granting sibling policy still grants.
16+
17+
**Narrowed, as in `where`.** A numeric string (`'10'`, `'1e3'`) is replaced by the number it names before either consumer runs. So `record.amount == '10'` now matches a stored `10` on the write check, which compared the text with the number and refused it, while the read showed the row.
18+
19+
**The remedy.** Compare a numeric column with a number: `record.amount <= 9999`, not `record.amount <= '9999-12-31'`.
20+
21+
**Unchanged.** A numeric literal, a column that is not numeric, a `{ $field }` reference, and an object whose declaration cannot be read (nothing is judged without one).
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): a bound action's translation is read only under its own object, never from `globalActions`
6+
7+
Clause-②: no
8+
9+
The i18n resolver reads an action's translated copy at one address, chosen by the action's own `objectName`. This covers `translateAction`, `resolveActionLabel`, `resolveActionConfirm`, `resolveActionSuccess`, `resolveActionResultDialog`, and `translateObject` for an object's inline actions.
10+
11+
- An action with an `objectName` reads only `objects.OBJECT._actions.ACTION`.
12+
- An action with no `objectName` reads only `globalActions.ACTION`.
13+
14+
Before this, a bound action with no object-scoped copy fell back to `globalActions.ACTION`. The fallback covered its label, description, confirm text, success message, outcome messages, params and result dialog. `TranslationDataSchema.globalActions` declares that group for object-less actions only. `os validate` already refuses, at error level, a `globalActions` key that names a bound action, and says the key is never read. The resolver now matches both.
15+
16+
**What changes for a project.** A bundle that passes `os validate` is not affected. A bundle that keeps a bound action's copy under `globalActions` now shows that action's source text instead of the translation. `os validate` does not check a translation stored at runtime, so such a translation changes the same way. The fix is to move the keys from `globalActions.ACTION` to `objects.OBJECT._actions.ACTION`, where OBJECT is the action's `objectName`. The example apps under `examples/` and the translation bundles shipped in this repository's packages have no such key.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: an agent's `structuredOutput` is JSON-only — the `regex` / `grammar` / `xml` formats and the `coerce_types` step are retired, and the block is `live`, enforced by the cloud AI runtime (#21277)
6+
7+
**BREAKING** — four members leave the agent's structured-output vocabulary:
8+
`regex`, `grammar` and `xml` from `StructuredOutputFormat` (so from
9+
`agent.structuredOutput.format` and `agent.structuredOutput.fallbackFormat`), and
10+
`coerce_types` from `TransformPipelineStep` (so from
11+
`agent.structuredOutput.transformPipeline`). ADR-0049 enforce-or-remove, ruled
12+
retire. The cloud AI runtime, the one runtime that executes agents, enforces
13+
`structuredOutput` on every final answer and refused an agent declaring any of the
14+
four before its first turn: the spec never had a key to carry the pattern or
15+
grammar a `regex` / `grammar` answer would be checked against, an answer is checked
16+
only as JSON, and no coercion engine exists. So no authored value of the four ever
17+
did what it named, and authoring now refuses them by name instead of the first
18+
live turn refusing the agent. `json_object`, `json_schema`, `trim`, `parse_json`
19+
and `validate` are unchanged.
20+
21+
### FROM → TO
22+
23+
| removed | what to write instead |
24+
| --- | --- |
25+
| `structuredOutput.format: 'regex'`, `'grammar'` or `'xml'` | `format: 'json_schema'` with a JSON Schema in `schema` when the answer must have a shape, or `format: 'json_object'`; or delete the `structuredOutput` block if the agent needs no output contract. |
26+
| `structuredOutput.fallbackFormat: 'regex'`, `'grammar'` or `'xml'` | `'json_object'` or `'json_schema'`, or delete the key. |
27+
| `'coerce_types'` in `structuredOutput.transformPipeline` | delete the step, and declare the exact types in `schema` so the answer is validated as the model wrote it. |
28+
29+
**The one-line fix: use `json_schema` with a JSON Schema; drop `coerce_types`.**
30+
`os migrate meta --from 17` lists the mechanical edits for existing sources.
31+
32+
Each retired member is refused at parse with a prescription naming the JSON
33+
formats, and in `tsc` (the members are gone from the `StructuredOutputFormat` /
34+
`TransformPipelineStep` types). Any other unknown value keeps zod's own message.
35+
36+
### The retirement kit
37+
38+
- **Value-level retirement.** Both enums are declared through
39+
`enumWithRetiredValues` (`shared/retired-key.ts`), the house mechanism for a
40+
narrowed vocabulary, with the prescriptions module-private. No authorable KEY and
41+
no def changed, so nothing lands in `RETIRED_KEYS_BY_MAJOR` and the four surface
42+
ratchets (`api-surface`, `authorable-surface`, `json-schema.manifest`,
43+
`api-surface-signatures`) are byte-identical.
44+
- **D2 conversion `agent-structured-output-refused-members-removed`** (step 18,
45+
retired from the load path): it deletes a `structuredOutput` block whose `format`
46+
was retired (the format is required, and no rewrite can say which JSON contract
47+
was meant), deletes a retired `fallbackFormat`, and drops `coerce_types` from the
48+
pipeline, keeping the other steps in order. Stored `sys_metadata` agent rows replay
49+
it at rehydration; one notice per edit.
50+
- **D3 entry `agent-structured-output-refused-members-retired`** carries the
51+
judgement the conversion cannot make: whether an agent whose block was deleted
52+
should now carry a `json_schema` contract.
53+
- **No deprecation window**, per the project's startup-stage posture.
54+
55+
### Describes and the liveness ledger
56+
57+
- `agent.structuredOutput` drops `[EXPERIMENTAL — not enforced]`: it states that the
58+
cloud AI runtime enforces it on every final answer and that the open framework
59+
edition does not run agents. Its ledger row moves `experimental` → `live`, citing
60+
the cloud readers (`agent-runtime.ts#compileStructuredOutput`,
61+
`ai-service.ts#AIService.settleFinalAnswer`) as attested by the cloud seat's
62+
reading at cloud `cb62c3ea`, `verifiedAt` 2026-10-02. `os lint` / `os validate` no
63+
longer warn `liveness-experimental-property` on an agent that sets it.
64+
- `fallbackFormat`'s describe states what the runtime does with it: once the primary
65+
format's retries are spent, the last answer is checked against the fallback.
66+
- `guardrails.blockedTopics`'s describe states the enforced match: an exact,
67+
case-sensitive match on the tool name, on `action_` plus the action type, or on
68+
the tool category.
69+
- The generated agent reference page follows.
70+
71+
⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is
72+
published, and tenant-authored agents were not measured. This repo authors no
73+
`structuredOutput` outside `packages/spec`, and the cloud seat's reading found no
74+
producer in cloud.
75+
76+
Clause-②: no (narrowing)
77+
78+
<!-- adr-0087: registered agent-structured-output-refused-members-removed, agent-structured-output-refused-members-retired -->
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
The published README now describes the `os` that ships. Five things it said were false.
6+
7+
Clause-②: no
8+
9+
- **Short flags.** The README listed `-v, --version` and `-h, --help` as global options. `os -v` and `os -h` exit 2 with `command -v not found` / `command -h not found`, because only `--version` and `--help` are registered. It now lists `--version` and `--help` alone and says there is no short form. `-v` already belongs to commands of their own: it is `--verbose` on `os dev`, `os serve`, `os start` and `os doctor`, and `--version` on `os package publish` and `os package install`.
10+
- **The `os plugin` group.** The README said there is no `os plugin` command group. `os plugin build`, `os plugin sign` and `os plugin publish` are registered, and the README now lists them. It also says the group has no `install`, and that `os plugin` is a different thing from `os plugins`, which is not a command.
11+
- **Two command rows.** `os init [name]` creates a new directory of that name when a name is given, so it no longer says "in the current directory" for every case. `os dev` restarts the server after each rebuild, so it no longer says "with hot reload".
12+
- **Cloud credentials and flags.** The README said every cloud command takes its credentials from `os cloud login` or from `--token` / `OS_CLOUD_API_KEY` and `--server` / `OS_CLOUD_URL`. That holds only for `os package publish` and `os plugin publish`. `os environments list`, `show`, `create`, `bind` and `switch` take `-u, --url` (env `OS_CLOUD_URL`) and `-t, --token` (env `OS_TOKEN`), and otherwise use the `os login` session in `~/.objectstack/credentials.json` — never the `os cloud login` session. With only `os cloud login` done they exit 1 with `Authentication required`. The README now has a per-command table, and its typical publish flow says so at the `os environments create` step.
13+
- **`os serve --ui`.** The README said it enables "Studio UI". It enables the bundled Console portal at `/_console/` when `@object-ui/console` is installed, which is what `os serve --help` says.
14+
15+
**What changes for an operator.** Nothing at runtime. No command, flag, environment variable, exit code or help page changes.

0 commit comments

Comments
 (0)