Skip to content

Commit 3de1abe

Browse files
committed
Merge origin/main: keep both halves of the formula suite's tail
Both sides appended to the end of matches-filter-temporal-conformance.ts: main added the note explaining why the token axis cannot apply to an RLS `check` (it is a CEL expression, so a `{token}` string never reaches this evaluator), and this branch added the Field.time sweep. They are complementary, so both are kept — with one sentence added to the note recording that the same reasoning covers the wall-clock cases, which carry no token spelling at all because no date macro resolves to a time of day. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqqZmPS5a4gJGBoCTwipFr
2 parents b0d840c + 41dcda3 commit 3de1abe

52 files changed

Lines changed: 3264 additions & 1840 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
"@objectstack/runtime": patch
3+
---
4+
5+
fix(runtime): the `/ai/agents` degraded fallback answers in the declared envelope (#4053)
6+
7+
`GET /ai/agents` was the last unenveloped SDK-addressable route. The framework's
8+
degraded fallback — what an open-source runtime with no `service-ai` answers —
9+
now returns `{ success: true, data: { agents: [] } }` via `deps.success`, and the
10+
route-envelope guard's last ratchet retires with it: **0 ratcheted on both
11+
surfaces.**
12+
13+
## Why `data: { agents }` and not `data: []`
14+
15+
#3983 set the precedent that `data` carries the payload directly, and following it
16+
here would have looked consistent. It would also have been wrong, and silently so.
17+
18+
`AiAgentsResponseSchema` is a **declared** payload schema; share-links' `{ links }`
19+
was an ad-hoc wrapper with none. So this is the #3843 relocation — the declared
20+
payload moves under `data` unchanged, the way `SettingsNamespacePayload` did —
21+
rather than a reshape.
22+
23+
That distinction decides the blast radius. `unwrapResponse` returns `body.data`
24+
when a body has a boolean `success` **and** a `data` key, so:
25+
26+
| conversion | `client.ai.agents.list()` |
27+
|---|---|
28+
| `data: { agents }` (this one) | reads `.agents` off it — **works** |
29+
| `data: [...]` (flattened) | `.agents` is `undefined`**`[]`** |
30+
31+
An empty list is not a visible failure on this route. `useAiSurfaceEnabled` gates
32+
the entire AI surface on `agents.length > 0`, and an empty catalog is the *correct*
33+
answer for a seat-less user (ADR-0068) or a Community-Edition deployment. The
34+
broken state and the legitimate one are indistinguishable — no error, no 403, no
35+
log.
36+
37+
## Consequence: no lockstep
38+
39+
Because the SDK reads both shapes identically, **each surface converts on its own
40+
schedule**. Cloud's `service-ai` still answers unenveloped and keeps working
41+
unchanged; objectui already reads all four shapes (objectui#2992). The
42+
"three repos in one batch" framing #4053 opened with does not apply to this
43+
variant.
44+
45+
Five tests in `@objectstack/client` pin it, including the road not taken: the
46+
flattened body asserts `[]`, so the cost of choosing it is recorded rather than
47+
rediscovered.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
**A config-booted app no longer loses its `onEnable` — every `script` action's
6+
handler reaches the engine again instead of 404'ing at dispatch (#4095).**
7+
8+
`os serve <config>` calls `createStandaloneStack()`, which reads
9+
`dist/objectstack.json` and returns a ready-made `AppPlugin` for the app. That
10+
satisfied serve's "does the host already wrap itself with an AppPlugin?" guard,
11+
so the `new AppPlugin(config)` built from the LOADED MODULE — the only one
12+
carrying the module's `onEnable` — was skipped. A JSON artifact cannot hold a
13+
function, so the app booted with all of its metadata and none of its code.
14+
15+
On `examples/app-todo` that meant eight declared `script` actions, zero
16+
registered handlers, and every button answering
17+
`404 Action 'complete_task' on object 'todo_task' not found`. The example is
18+
correctly authored: it declares `target: 'completeTask'`, registers
19+
`todo_task:completeTask`, and exports `onEnable`. serve carried that hook intact
20+
all the way to the branch that discarded it.
21+
22+
Serve now grafts the module's executable members onto the app bundle already
23+
registered, rather than dropping them with the wrap:
24+
25+
- Only members `AppPlugin` actually executes travel — `onEnable` and the
26+
`functions` map that string-named hook/job handlers resolve against. (`onDisable`
27+
is deliberately excluded: it is declared in `packages/spec` but no kernel,
28+
runtime or service ever calls it, so grafting it would wire a hook nothing
29+
runs.)
30+
- The artifact stays the metadata source of truth. Neither side is a superset —
31+
the artifact carries compile-time enrichment the config never has (ADR-0046
32+
packaged docs, which serve already grafts the other way) — so this moves code
33+
only, and never metadata.
34+
- Targeting is by `manifest.id`, so a host composing several `AppPlugin`s can
35+
never have one app's handlers attached to another. With no id to match, it
36+
falls back to the single app bundle present and refuses when there are several.
37+
- A bundle's own value always wins, so a host that wrapped itself on purpose is
38+
untouched.
39+
- Code that finds no bundle to land on is now reported with a boot warning naming
40+
the consequence ("they 404 at dispatch") instead of vanishing. That silent drop
41+
is what hid this.
42+
43+
Verified end to end on `examples/app-todo`: `POST /api/v1/actions/todo_task/complete_task`
44+
went from `404 RESOURCE_NOT_FOUND` to `{"success":true}`, `export_csv` now returns
45+
real CSV, and the `[action-governance]` boot warning naming all eight actions is
46+
gone. 14 unit cases pin the graft and — as importantly — the cases where it must
47+
refuse; one end-to-end case boots a real stack through `bin/run-dev.js` and fails
48+
against the pre-fix command.
49+
50+
Note that `os serve <config>` still cannot boot at all when `dist/objectstack.json`
51+
is absent (#4085, `Service 'manifest' is async - use await`). That was verified to
52+
be a **separate** defect on the other side of the same fork, not this one: the
53+
failure reproduces unchanged with this fix applied.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/service-automation": patch
4+
"@objectstack/runtime": patch
5+
---
6+
7+
fix(spec,runtime,service-automation): `IAutomationService` declares the connector registry it already serves (#4127)
8+
9+
The fourth and last of the dispatcher call sites #4127 found calling a method its
10+
contract never declared. The first three shipped in #4143; this one was held back
11+
because the fix is a **type move**, not a type addition — `ConnectorDescriptor`
12+
was declared in `@objectstack/service-automation`'s engine, which is one
13+
*implementation* of `IAutomationService`. A contract cannot name a type that
14+
lives inside its own implementation, so `getConnectorDescriptors` could not be
15+
declared at all until the type had a home in the spec.
16+
17+
**`IAutomationService` += `getConnectorDescriptors?()`.** It is the sibling of
18+
`getActionDescriptors`, which the contract has declared since ADR-0018: the two
19+
fill the flow designer's `connector_action` node together — node vocabulary from
20+
one, the connector → action → input pickers from the other. Only one of them was
21+
written down. `GET /api/v1/automation/connectors` has served the other since
22+
ADR-0022 by probing for the method and then re-typing its own result as `any` to
23+
filter on `?type=`, which is a filter on a field the type system did not know
24+
existed — one typo from silently matching nothing and answering an empty
25+
registry, which is also what this route legitimately returns when the method is
26+
absent, so the failure had no distinguishable symptom.
27+
28+
Optional for the same reason `getActionDescriptors` is: a connector registry is a
29+
capability of the flow-engine implementation, not a property of every automation
30+
slot. A script-runner filling the slot has no connectors to describe, and the
31+
route answers an empty registry rather than a 404 — the `handlerReady` posture
32+
does not apply, since the slot is serveable and only this capability is absent.
33+
34+
**`ConnectorDescriptor` / `ConnectorActionDescriptor` / `ConnectorOrigin` /
35+
`ConnectorState` move to `@objectstack/spec/integration`**, beside the ADR-0097
36+
provider contract, for the reason that file already states about itself: they are
37+
pure types, so a connector plugin — or a designer client, or the dispatcher —
38+
speaks about registered connectors depending only on the spec, with no runtime
39+
coupling to the engine. `ConnectorOrigin` is ADR-0097 §4 vocabulary and
40+
`ConnectorState` is #3017 vocabulary; neither was ever engine-private in meaning,
41+
only in location.
42+
43+
Nothing is renamed and no shape changes. `@objectstack/service-automation`
44+
imports the four back and re-exports them from its index — the same names, from
45+
the same entry point — so every existing importer compiles unchanged.
46+
`ConnectorState` joins that re-export, which it should have been in all along: it
47+
is a required field of the descriptor the index has always exported.
48+
49+
**The test fixture had already drifted, which is the concrete cost.** The
50+
dispatcher's connector mock declared `{ name, label, type, actions }` and omitted
51+
`origin` and `state` — both **required** on `ConnectorDescriptor`, and both the
52+
fields a designer reads to tell a live declarative instance from a plugin one
53+
(ADR-0097 §4), or a dispatchable connector from a degraded one that is listed
54+
honestly rather than hidden (#3017). Nothing caught it, because an undeclared
55+
return type cannot be checked against. The fixture is typed now, so it cannot
56+
drift again, and a new test pins that `origin` / `state` / `degradedReason`
57+
survive the hop through the route rather than only `name` and `type`.
58+
59+
Verified: `@objectstack/spec` **7089 tests / 272 files** (2 new contract tests),
60+
`@objectstack/service-automation` **457 / 41**, `@objectstack/runtime`
61+
**218 http-dispatcher tests** (1 new), `tsc --noEmit`, `pnpm lint`, the liveness
62+
and empty-state gates, and the three generated-artifact gates — all clean.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/runtime": patch
4+
"@objectstack/service-i18n": patch
5+
---
6+
7+
fix(spec,runtime,service-i18n): the dispatcher domains and their service contracts describe the same surface (#4127)
8+
9+
#4087 retired a `/storage` bridge that called `upload(key, data, options?)` as
10+
`upload(file, { request })` — a shape no implementation has. Sweeping the other
11+
dispatcher domains against `packages/spec/src/contracts/*` found the mirror-image
12+
gap in three places: the call site and the implementation agreed, and the
13+
**contract** was the thing that had never been written down. Each one was worked
14+
around at the call site with `typeof x.foo === 'function'` — a duck-type is what
15+
"the contract does not cover this" looks like when nobody fixes the contract.
16+
17+
Fixed at the contract, per Prime Directive #12.
18+
19+
**`INotificationService` — the inbox half.** `listInbox` / `markRead` /
20+
`markAllRead` now exist, with `InboxQuery` / `InboxNotification` /
21+
`InboxListResult` / `MarkReadResult`. Three SDK-expressed routes
22+
(`notifications.list` / `.markRead` / `.markAllRead`) have rested on them all
23+
along, implemented by `service-messaging`, while this contract described only
24+
`send`. The cost was not theoretical: the dev notification stub implements
25+
exactly `send` and `sendBatch` **because it followed the contract**, so the one
26+
implementation written to spec was the one the dispatcher had to duck-type past.
27+
28+
They are optional, and the probe stays: an inbox needs a durable store, and a
29+
send-only provider (SMTP, Twilio, a Slack webhook) fills the slot legitimately
30+
without one. `handlerReady` cannot express that — the slot is serveable, one
31+
capability of it is absent. The `/notifications` domain now takes
32+
`INotificationService` instead of `as any`, and each write route probes its own
33+
method rather than riding the entry `listInbox` check (they are separately
34+
optional, so "has an inbox to read" never implied "has read-state to write").
35+
36+
**`II18nService.getFieldLabels`.** Both serving surfaces — the dispatcher's
37+
`/i18n/labels/:object/:locale` and service-i18n's own mount — probed for it and
38+
both documented it as "optional on `II18nService`", which was not true. It is
39+
now. service-i18n's probe loses two casts with it (one through
40+
`Record<string, unknown>`, one re-declaring the signature inline).
41+
42+
**`IAutomationService.getFlowRuntimeStates`** + the `FlowRuntimeState` type.
43+
`GET /automation/_status` (and the CLI boot summary, and the
44+
`kernel:bootstrapped` audit) already called it while the contract stopped at
45+
`listFlows(): string[]`. The dispatcher's inline cast declared it as
46+
`{ name, enabled, bound }` — a third copy of the shape and a narrower one than
47+
the engine returns, dropping the `status` / `triggerType` / `object` fields that
48+
say WHY a flow is unbound.
49+
50+
Two runtime fixes fell out of the same sweep:
51+
52+
- **`POST /automation/trigger/:name` now builds a real `AutomationContext`.**
53+
It passed the raw HTTP body to `execute(name, body)`, so the
54+
`{ recordId, objectName, params }` translation never ran and — the sharper
55+
half — no caller identity was forwarded. A flow's default `runAs` is `'user'`,
56+
and a `runAs:'user'` run whose trigger resolved no user has its data
57+
operations REFUSED (#3760, fail-closed), so `client.automation.trigger()`
58+
could not run a data-touching flow at all while `POST /:name/trigger` could.
59+
service-automation's own comment claims "most trigger surfaces (REST action /
60+
trigger endpoint) already resolve the full envelope"; for this endpoint it was
61+
not true. Both routes share one context builder now.
62+
- **The dead `automationService.trigger(...)` probe is gone.** Nothing in the
63+
repo has ever implemented `trigger` on the automation slot and the contract
64+
never declared it, so the branch was unreachable on every deployment and its
65+
`execute` "fallback" was the route. Declaring `trigger?` would have blessed a
66+
second name for `execute`; the dead branch is deleted instead.
67+
68+
No migration. Every added contract member is optional, so existing
69+
implementations stay valid; the two runtime fixes only make routes that were
70+
failing or degraded behave like their working twins.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
"@objectstack/plugin-hono-server": minor
3+
---
4+
5+
feat(plugin-hono-server): export `registerCurrentUserEndpoints` so a host without the plugin can still supply them (cloud#924)
6+
7+
`GET /api/v1/auth/me/permissions`, `/api/v1/auth/me/localization` and
8+
`/api/v1/me/apps` are the platform's **sole** supply — neither
9+
`@objectstack/rest` nor `@objectstack/runtime` registers any `/me/*` route, the
10+
objectui console reads the first for its whole permission layer and the second
11+
for regional defaults, and `core`'s auth gate allow-lists the last two as
12+
endpoints a gated user MUST still reach. #4073/#4079 freed them from the
13+
`registerStandardEndpoints` flag, but left the supply welded to
14+
`HonoServerPlugin`: a host that stands up a bare `HonoHttpServer` and registers
15+
it as `http.server` itself — rather than mounting the plugin — got no provider at
16+
all, and the console's FLS / `apiOperations` had no server-side answer on that
17+
startup path.
18+
19+
Registration needs a Hono app and a service locator, not ownership of the
20+
listening socket, so it is now a standalone module (`./current-user-endpoints`)
21+
that both shapes call:
22+
23+
```ts
24+
import { registerCurrentUserEndpoints } from '@objectstack/plugin-hono-server';
25+
26+
const httpServer = new HonoHttpServer();
27+
kernel.registerService('http.server', httpServer);
28+
registerCurrentUserEndpoints({
29+
rawApp: httpServer.getRawApp(),
30+
// any { getService, logger } — a PluginContext satisfies it structurally
31+
ctx: { getService: (n) => { try { return kernel.getService(n); } catch { return undefined; } } },
32+
});
33+
```
34+
35+
It is **idempotent**: it returns `false` and registers nothing when all three
36+
paths are already served, so a host may both call it eagerly on the raw app AND
37+
mount the plugin — the plugin's `kernel:ready` registration then no-ops instead
38+
of shadowing the host's routes with dead duplicates. Registering early matters,
39+
because Hono's only route precedence is first-registration-wins and plugin-auth
40+
mounts a `/api/v1/auth/*` wildcard that `/auth/me/*` must outrank.
41+
42+
**No behaviour change for existing hosts.** `os serve` and every host that mounts
43+
`HonoServerPlugin` register the same three routes, in the same `kernel:ready`
44+
position, with the same response shapes — the plugin now delegates to the shared
45+
registrar instead of owning a private method.
46+
47+
**Moved exports (same package, same names, no rename).** `foldWildcardSuperUser`,
48+
`clampManagedObjectWrites`, `seedSuperUserRestrictedObjects`,
49+
`annotateEffectiveApiOperations`, `ManagedSchemaLike` and `ApiExposureSchemaLike`
50+
now live in `./current-user-endpoints` alongside the endpoint they shape. Importing
51+
them from the package root (`@objectstack/plugin-hono-server`) is unchanged; only a
52+
deep import of `.../dist/hono-plugin` would need updating, and the package exposes
53+
no such subpath.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
'@objectstack/plugin-dev': minor
3+
---
4+
5+
feat(plugin-dev)!: the stub table is retired — DevPlugin assembles real plugins and registers no service implementations of its own (ADR-0115, #4093, #4104).
6+
7+
DevPlugin used to fill every core-service slot no real plugin occupied with a dev stub. Every one of those stubs is gone. A slot nothing fills now stays EMPTY, exactly as in production: routes answer 404/501, discovery reports `unavailable`, and in-process consumers must handle absence — which production already required of them. FROM → TO per retired slot:
8+
9+
| Slot | The stub did | Instead |
10+
|:---|:---|:---|
11+
| `security.permissions` | allow-all `checkObjectPermission()` | install `@objectstack/plugin-security` (already part of the default assembly) |
12+
| `security.rls` | compiled no row filter | same — `plugin-security` |
13+
| `security.fieldMasker` | returned results unmasked | same — `plugin-security` |
14+
| `auth` | `verify()` accepted everyone as admin | install `@objectstack/plugin-auth` (already part of the default assembly) |
15+
| `data` | accepted writes, stored nothing | install `@objectstack/objectql` (already part of the default assembly) |
16+
| `ui` | shapeless `{}` placeholder | nothing consumed it; handle the absent slot |
17+
| `ai` | placeholder chat/complete answers | install a real AI service |
18+
| `automation` | `execute()` reported success without running | install an automation engine plugin |
19+
| `notification` | claimed "sent", delivered nothing | install a notification service |
20+
| `file-storage` | in-memory files lost on restart | `@objectstack/service-storage` — now auto-wired by DevPlugin when installed (local-disk adapter) |
21+
| `realtime` | in-process pub/sub copy | `@objectstack/service-realtime` — now auto-wired by DevPlugin when installed (its default in-memory adapter) |
22+
| `search` | in-memory substring index | no consumer resolves this slot; a future search service ships its own dev strategy |
23+
| `workflow` | unvalidated state transitions | no consumer resolves this slot; a future workflow service ships its own dev strategy |
24+
| `metadata` | a second hand-written copy of core's `createMemoryMetadata` | no behavior change — the kernel pre-injects core's fallback for empty core slots (`CORE_FALLBACK_FACTORIES`), and ObjectQL registers the real metadata service in the default assembly |
25+
| `cache` / `queue` / `job` / `i18n` | re-registered core's `createMemory*` fallbacks | no behavior change — the kernel pre-injects the same core fallbacks automatically; install `@objectstack/service-cache` / `service-queue` / `service-job` for real engines, and i18n auto-wires from the stack's translations (unchanged) |
26+
27+
Also new, from the same ADR:
28+
29+
- **Production guard** (first shipped with the security-trio subset): `DevPlugin.init()` throws when `NODE_ENV === 'production'` — the assembly is built around a well-known default auth secret and a seeded dev admin. Escape hatch: `OS_ALLOW_DEV_PLUGIN=1`.
30+
- **Assembly auto-wire**: `@objectstack/service-storage` and `@objectstack/service-realtime` are wired as optional child plugins when installed (both ship with DevPlugin's dependencies), so dev keeps working file storage and realtime through real implementations.
31+
- `options.services` keys for the retired stubs are accepted and ignored; `'file-storage'` / `'realtime'` now toggle the real service wiring.
32+
33+
One-line fix for an upgrading stack: if something you called in dev now throws "service not found" or 404s, that call was consuming a fabricated answer — install the real service for that slot (table above), or make the caller tolerate absence the way it already must in production.

0 commit comments

Comments
 (0)