diff --git a/.changeset/docs-audit-4161-service-automation.md b/.changeset/docs-audit-4161-service-automation.md
new file mode 100644
index 0000000000..060a42a733
--- /dev/null
+++ b/.changeset/docs-audit-4161-service-automation.md
@@ -0,0 +1,40 @@
+---
+---
+
+Docs only — no package changes, nothing to release.
+
+Back-fills the implementation-accuracy audit that #4161 never got. Its drift
+comment was computed by the mapper bug fixed in #4206: the change to
+`packages/services/service-automation` collapsed to the container directory, so
+the four docs that actually reference `@objectstack/service-automation` were
+either attributed to the wrong package or (on a service-automation-only diff)
+not reported at all. Re-derived with the fixed mapper and audited:
+`automation/flows.mdx`, `plugins/packages.mdx`, `releases/implementation-status.mdx`,
+`releases/v9.mdx`.
+
+44 evidence-backed fixes, 5 repaired by the adversarial verifier. The
+substantive ones:
+
+- **`flows.mdx`** — `runAs` now states that a `'user'` run which resolved no
+ trigger user has its data operations refused (the axis #4161 tightened);
+ `node.type` is documented as an open `string` checked against the live action
+ registry at `registerFlow()` (ADR-0018), not a closed enum; the `script`
+ executor is described as naming a callable, with `'email'` / `'slack'` called
+ out as logger-backed markers that record intent without delivering; the
+ `.strict()` flow/node/edge/variable shells and the previously undocumented
+ `timeoutMs`, `inputSchema`, `waitEventConfig`, `boundaryConfig`,
+ `successMessage`, `errorMessage` keys are added.
+- **`implementation-status.mdx`** — REST rows carry their real `/api/v1` prefix;
+ the memory driver's capability list is replaced with what `InMemoryDriver.supports`
+ actually declares; Cache/Encryption/Dataset/Sorting statuses corrected;
+ the `msw` column dropped (not a workspace package); `Role` → `Position`
+ (ADR-0090 D3), which ratchets `scripts/role-word-baseline.json` down by one —
+ that gate fails on a *decrease* too, so the baseline update ships with it.
+- **`packages.mdx`** — package inventory and the `create*` plugin-factory claim
+ corrected against the real exports.
+- **`v9.mdx`** — notes that `os package publish --visibility` has since shipped
+ (`private` / `org` / `marketplace`, default `org`).
+
+18 residual items the agents could not resolve without a code-owner decision are
+recorded in the PR body rather than guessed at; two are genuine code/doc
+contradictions worth their own issues.
diff --git a/content/docs/automation/flows.mdx b/content/docs/automation/flows.mdx
index b866045dac..df40e3f0e9 100644
--- a/content/docs/automation/flows.mdx
+++ b/content/docs/automation/flows.mdx
@@ -73,11 +73,13 @@ const approvalFlow = {
| `version` | `number` | optional | Version number (defaults to `1`) |
| `status` | `enum` | optional | `'draft'`, `'active'`, `'obsolete'`, `'invalid'` (defaults to `'draft'`) |
| `type` | `FlowType` | ✅ | Flow trigger type (see below) |
-| `runAs` | `enum` | optional | `'system'` or `'user'` execution context (defaults to `'user'`) |
+| `runAs` | `enum` | optional | `'system'` (elevated, bypasses RLS) or `'user'` (the triggering user, RLS-respecting; the default). A `'user'` run that resolved **no** trigger user has nothing to scope to, so its data operations are refused — declare `'system'` for schedule / time-relative / API triggers and for record-change flows fired by a write that carried no user |
| `variables` | `FlowVariable[]` | optional | Input/output variables |
| `nodes` | `FlowNode[]` | ✅ | Flow nodes |
| `edges` | `FlowEdge[]` | ✅ | Connections between nodes |
| `errorHandling` | `object` | optional | Error handling strategy |
+| `successMessage` | `string` | optional | Toast a screen-flow runner shows on terminal success (plain string — `{var}` is **not** interpolated) |
+| `errorMessage` | `string` | optional | Toast a screen-flow runner shows on failure, instead of the raw error |
### Flow Types
@@ -110,7 +112,7 @@ Each node performs a specific action in the flow.
| `get_record` | Query records |
| `http` | Make an HTTP API call |
| `notify` | Send an outbound notification via the messaging service |
-| `script` | Run a custom script action (dispatched by `config.actionType`) |
+| `script` | Call a named callable — a registered function (`config.function`) or a built-in side-effect marker (`config.actionType`) |
| `screen` | Display a user form/screen (durable pause) |
| `wait` | Pause for a timer or named signal (durable pause; timers auto-resume) |
| `subflow` | Invoke another flow — a pause inside the child suspends both runs as a linked chain |
@@ -134,11 +136,21 @@ Each node performs a specific action in the flow.
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| `id` | `string` | ✅ | Unique node identifier |
-| `type` | `FlowNodeAction` | ✅ | Node type |
+| `type` | `string` | ✅ | Node type — a built-in id from the table above **or** a plugin-registered one. Per ADR-0018 the spec does not gate this with a closed enum; it is checked against the live action registry at `registerFlow()` |
| `label` | `string` | ✅ | Display label |
-| `config` | `object` | optional | Type-specific configuration |
-| `connectorConfig` | `object` | optional | External connector settings |
+| `config` | `object` | optional | Type-specific configuration (an open record — the registered executor's `configSchema` owns its shape) |
+| `connectorConfig` | `object` | optional | `{ connectorId, actionId, input }` for a `connector_action` node |
| `position` | `{ x, y }` | optional | Visual position on canvas |
+| `timeoutMs` | `number` | optional | Per-node execution timeout |
+| `inputSchema` | `object` | optional | Declared input parameter types, for Studio form generation and runtime validation |
+| `waitEventConfig` | `object` | optional | `wait`-node event descriptor (`eventType`, `timerDuration`, `signalName`, `timeoutMs`, `onTimeout`) |
+| `boundaryConfig` | `object` | optional | BPMN boundary-event descriptor (interop) |
+
+
+The flow, node, edge, and variable **shells are `.strict()`** — a key they do not
+declare is a parse error naming the likely intended key, not a silent strip. (A
+node's `config` stays open, so plugin node types keep their own vocabulary.)
+
### Node Examples
@@ -193,18 +205,36 @@ Each node performs a specific action in the flow.
**Script:**
-The built-in `script` executor dispatches on `config.actionType` (e.g. `email`)
-rather than evaluating an arbitrary JavaScript string:
+The built-in `script` executor never evaluates an arbitrary JavaScript string —
+it **names a callable**. Two forms:
+
+- **`config.function`** — a function registered through
+ `defineStack({ functions })`. `config.inputs` is `{var}`-interpolated and
+ handed to it; `config.outputVariable` binds the returned value as a flow
+ variable, so a later declarative node persists it. This is the supported way
+ to run server logic.
+- **`config.actionType`** — one of the two built-in side-effect markers,
+ `'email'` or `'slack'`. These are **logger-backed**: they record the intent
+ and succeed, they do not deliver anything — reach for a `notify` node when you
+ want real delivery. Any other `actionType` value is treated as a
+ registered-function name — except the marker `'invoke_function'`, which means
+ "call the function named in `config.function`" and errors if that key is
+ missing.
+
+Inline `config.script` (a JS source body) is *recognized* but **not executed** —
+the built-in runtime has no server-side JS sandbox, so such a node warns and
+no-ops. A script node that names neither a built-in action nor a registered
+function fails the step loudly rather than passing silently.
```typescript
{
- id: 'send_welcome',
+ id: 'score_lead',
type: 'script',
- label: 'Send Welcome Email',
+ label: 'Score Lead',
config: {
- actionType: 'email',
- template: 'welcome',
- recipients: '{record.email}',
+ function: 'scoreLead', // registered via defineStack({ functions })
+ inputs: { rating: '{record.rating}' }, // {var} templates resolve against flow variables
+ outputVariable: 'leadScore', // later: fields: { score: '{leadScore}' }
},
}
```
@@ -247,8 +277,11 @@ Two things worth knowing:
value at all, which counts as unanswered — without `defaultValue: false` the
user cannot express "no" by leaving it clear.
- **A broken predicate fails open** (the field stays visible) rather than hiding
- an input the flow may be waiting on. Nothing validates the expression at
- author time yet, so a typo shows up as a field that never hides.
+ an input the flow may be waiting on. `registerFlow()` does check the predicate
+ as bare CEL — a `{var}` template or a syntax error is a loud registration
+ failure that quotes the source and locates it as
+ `config.fields[N].visibleWhen` — but a predicate that *parses* and simply
+ names the wrong field still shows up as a field that never hides.
**Screen (object form):**
@@ -561,8 +594,9 @@ Edges connect nodes and define the execution path:
| `source` | `string` | ✅ | Source node ID |
| `target` | `string` | ✅ | Target node ID |
| `type` | `enum` | optional | `'default'` (success), `'fault'` (error), `'conditional'` (expression-guarded), or `'back'` (declared back-edge, ADR-0044); defaults to `'default'` |
-| `condition` | `string` | optional | Boolean expression for branching |
+| `condition` | `string` | optional | Boolean CEL predicate for branching (a bare string is stored as `{ dialect: 'cel', source }`) |
| `label` | `string` | optional | Label displayed on the connector — cosmetic only. It does **not** select a path except on a branching node (`decision` / `approval`), which picks its out-edge by label. |
+| `isDefault` | `boolean` | optional | BPMN default-flow marker (interop). Accepted by the schema, but **the engine does not read it** — traversal selects by `condition` and by `label`. On a `decision` node, the fallback is the out-edge labelled `default`: when no `conditions[]` entry matches, the node emits `branchLabel: 'default'` |
### Fault edges — handling a failed node
@@ -582,7 +616,7 @@ edges: [
the type leaves an ordinary edge, and every unconditional out-edge is traversed
(in parallel) on **success**. The handler then runs whenever the node
*succeeds*, never when it fails, and the run still aborts on failure. Both
-halves are silent, so `objectstack validate` reports this as
+halves are silent, so `os validate` reports this as
`flow-error-label-not-fault`.
@@ -621,7 +655,7 @@ The split is deliberate. A dropped filter condition does not narrow a query, it
widens it — so if guards were routable, one `fault` edge on a `delete_record`
would turn off the protection against emptying the object while the run still
reported success. Re-running changes nothing either: the fix is to correct the
-metadata, which `objectstack validate` will point at.
+metadata, which `os validate` will point at.
#### Fault edges vs. `errorHandling.retry`
@@ -673,9 +707,12 @@ errorHandling: {
| Property | Type | Description |
| :--- | :--- | :--- |
-| `strategy` | `enum` | `'fail'` (stop), `'retry'` (retry), `'continue'` (skip) |
+| `strategy` | `enum` | `'fail'` (stop) or `'retry'` (re-run the whole flow). `'continue'` parses but the engine branches only on `'retry'`, so it behaves exactly like `'fail'` — use a `fault` edge to keep going past a failed node |
| `maxRetries` | `number` | Maximum retry attempts (0-10) |
| `retryDelayMs` | `number` | Delay between retries (ms) |
+| `backoffMultiplier` | `number` | Exponential backoff multiplier (default `1`) |
+| `maxRetryDelayMs` | `number` | Ceiling on the backed-off delay (default `30000`) |
+| `jitter` | `boolean` | Randomize the delay to avoid a thundering herd (default `false`) |
## Discovery & Registration
@@ -782,30 +819,43 @@ credentials.
**`success: true` doesn't always mean "it ran".** If the start condition isn't
-met, the response is a *successful* skip:
-`{ "success": true, "data": { "skipped": true, "reason": "condition_not_met" } }`.
+met, the response is a *successful* skip — the run result rides in `data`, and
+the skip marker sits on its `output`:
+`{ "success": true, "data": { "success": true, "output": { "skipped": true, "reason": "condition_not_met" } } }`.
A self-triggering flow caught by the loop guard reports
-`reason: "reentrancy_loop_guard"` the same way. Check `skipped` before assuming
-the work happened.
+`reason: "reentrancy_loop_guard"` the same way. Check `data.output.skipped`
+before assuming the work happened.
## Console Flow Viewer & Test Runner
-Every flow can surface in the Console metadata browser under `/_console/`.
-ObjectUI's flow viewer replaces the default JSON inspector with rich tabs when
-the flow viewer plugin is installed:
+Every flow surfaces in the Console metadata browser under `/_console/`, where
+ObjectUI's app-shell registers `FlowPreview` as the `flow` metadata preview
+(`registerMetadataPreview('flow', FlowPreview)`) in place of the default JSON
+inspector. It renders the graph on a canvas — editable (add a node, patch a
+selection) only in the designer's edit mode, read-only otherwise. Opening a flow
+shows the **full-width canvas with no side panel**; the toolbar toggles one
+collapsible panel at a time:
-| Tab | Component | Purpose |
+| Side panel | Component | Purpose |
|:---|:---|:---|
-| **Overview** | `FlowViewer` | Renders trigger type, variables, nodes, edges, and `errorHandling` as inspector cards |
-| **Run** | `FlowTestRunner` | Auto-generates a form for every `isInput: true` variable (with type-aware coercion for `number` / `boolean` / `object` / `list`), executes the flow against the per-project kernel, and shows the returned outputs + run id |
-| **Runs** | `FlowRunsPanel` | Lists historical executions for the selected flow with status, duration, and a deep-link to the run record |
-
-The runner posts to the same
-[`POST /api/v1/automation/:name/trigger`](#run-a-flow-via-api) endpoint,
-scoped to the active project. All three components participate in the Studio
-authentication / project-scope context, so they work identically in
-single-project mode and in cloud mode.
+| **Variables** | — | The flow's declared variables with their types and input/output flags. Opt-in — it is the fallback panel, not one that opens on its own |
+| **Problems** | `ProblemsPanel` | Validation findings; selecting one reveals the offending node or edge on the canvas |
+| **Debug** | `FlowSimulatorPanel` | Steps the graph client-side, highlighting the active node, the visited nodes, and the traversed edges |
+| **Runs** | `FlowRunsPanel` | Real run history from [`GET /api/v1/automation/:name/runs`](#observing-runs), each run's step log nested by loop iteration / parallel branch / try-catch handler |
+
+Actually *running* a flow is a separate Console page — **Developer › Flow Runs**
+(`developer/flow-runs`). Its test runner auto-generates a form for every
+`isInput: true` variable (with type-aware coercion for `number` / `boolean` /
+`object` / `list`) and calls `client.automation.execute(name, { params })` —
+the same [`POST /api/v1/automation/:name/trigger`](#run-a-flow-via-api)
+endpoint — then renders the returned result. A screen flow that comes back
+`paused` is handed to the same `FlowRunner` the record and list surfaces use,
+so multi-step wizards (and `object-form` steps) can be driven to completion
+from here instead of orphaning a suspended run. A run-history panel beside it
+lists recent runs and expands one for its step detail. The Console is not
+project-scoped: the client comes from the app adapter, with no `projectId` in
+the route.
## Flows in practice
@@ -1040,6 +1090,6 @@ To branch on **which** event fired, test `previous` — it is empty on create
## Related
-- [Workflow Metadata](/docs/automation/workflows) — Event-triggered automation rules
+- [Workflow Metadata](/docs/automation/workflows) — why there is no standalone Workflow Rule type, and what replaces it
- [Object Metadata](/docs/data-modeling/objects) — Objects that flows operate on
- [Validation Metadata](/docs/data-modeling/validation) — Data validation rules
diff --git a/content/docs/plugins/packages.mdx b/content/docs/plugins/packages.mdx
index 8201a58c19..7b6ca62e8d 100644
--- a/content/docs/plugins/packages.mdx
+++ b/content/docs/plugins/packages.mdx
@@ -5,7 +5,7 @@ description: Complete guide to all ObjectStack packages, services, drivers, plug
# Package Overview
-ObjectStack is organized into **71 package manifests** across multiple categories. This guide provides an overview of the framework packages, services, drivers, plugins, and adapters in the [framework repository](https://github.com/objectstack-ai/objectstack/tree/main/packages).
+ObjectStack is organized into **72 package manifests** across multiple categories. This guide provides an overview of the framework packages, services, drivers, plugins, and adapters in the [framework repository](https://github.com/objectstack-ai/objectstack/tree/main/packages).
### Package categories at a glance
@@ -198,9 +198,9 @@ All services implement contracts from `@objectstack/spec/contracts` and are kern
### @objectstack/service-analytics
-**Analytics Service** — Multi-driver analytics with NativeSQL, ObjectQL, InMemory strategies.
+**Analytics Service** — Multi-driver analytics with built-in NativeSQL and ObjectQL strategies (the lowest-priority `InMemoryStrategy` is not built in — it ships in `@objectstack/driver-memory`).
-- **Features**: Aggregations, time series, funnels, dashboards
+- **Features**: Aggregations (measures/dimensions), time dimensions with granularity, dashboard/report widget queries
- **When to use**: Business intelligence, reporting, metrics dashboards
- **README**: [View README](https://github.com/objectstack-ai/objectstack/blob/main/packages/services/service-analytics/README.md)
@@ -214,10 +214,10 @@ All services implement contracts from `@objectstack/spec/contracts` and are kern
### @objectstack/service-cache
-**Cache Service** — In-memory and Redis caching.
+**Cache Service** — In-memory caching behind the `ICacheService` contract.
-- **Adapters**: Memory (dev), Redis (production)
-- **Features**: TTL, namespaces, pattern matching, statistics
+- **Adapters**: Memory (the only working adapter). `RedisCacheAdapter` is a **skeleton** — every method throws `not yet implemented`, and `CacheServicePlugin({ adapter: 'redis' })` refuses to start. For a real distributed cache, register your own `ICacheService` via `ctx.registerService('cache', impl)`.
+- **Features**: `get` / `set` / `delete` / `has` / `clear` / `stats`, per-entry TTL, `maxSize` eviction, lookup & write metrics
- **When to use**: Performance optimization, reduce database load
- **README**: [View README](https://github.com/objectstack-ai/objectstack/blob/main/packages/services/service-cache/README.md)
@@ -253,18 +253,19 @@ All services implement contracts from `@objectstack/spec/contracts` and are kern
### @objectstack/service-queue
-**Queue Service** — Job queues with in-memory and BullMQ adapters.
+**Queue Service** — Job queues with an in-memory adapter and a durable, database-backed adapter (`sys_job_queue`). No BullMQ/Redis adapter is shipped.
-- **Features**: Priority queues, retry, rate limiting, worker pools, job events
+- **Features**: Priority, delayed / scheduled delivery, `maxAttempts` + fixed/exponential backoff, dead-letter queue and replay, idempotency keys; multi-node claim via a lease on the `db` adapter
- **When to use**: Async processing, email sending, report generation, webhooks
- **README**: [View README](https://github.com/objectstack-ai/objectstack/blob/main/packages/services/service-queue/README.md)
### @objectstack/service-realtime
-**Realtime Service** — WebSocket pub/sub for live updates.
+**Realtime Service** — in-process pub/sub implementing `IRealtimeService`, backed by an in-memory adapter.
-- **Features**: Channels, presence, broadcasting, typing indicators, cursor tracking
-- **When to use**: Real-time dashboards, collaborative editing, live notifications
+- **Features**: `publish` / `subscribe` / `unsubscribe`, channel routing, per-subscription filtering by object name and event type, a `maxSubscriptions` cap; registers the `sys_presence` object
+- **Not included**: there is **no WebSocket/SSE endpoint and no client transport** — `IRealtimeService.handleUpgrade` is deliberately unimplemented platform-wide until the identity-admission seam exists. The adapter is process-local (single-instance only).
+- **When to use**: Trusted, server-internal subscribers (webhook auto-enqueuer, knowledge sync)
- **README**: [View README](https://github.com/objectstack-ai/objectstack/blob/main/packages/services/service-realtime/README.md)
### @objectstack/service-storage
@@ -299,8 +300,8 @@ All services implement contracts from `@objectstack/spec/contracts` and are kern
**Authentication Plugin** — Better-auth integration with ObjectQL.
-- **Features**: Email/password, OAuth providers, session management, RBAC
-- **When to use**: User authentication and authorization
+- **Features**: Email/password, OAuth providers, session management, bearer-token auth, password reset and email verification (authorization itself lives in `plugin-security`)
+- **When to use**: User authentication and identity
- **README**: [View README](https://github.com/objectstack-ai/objectstack/blob/main/packages/plugins/plugin-auth/README.md)
### @objectstack/plugin-security
@@ -354,7 +355,7 @@ All services implement contracts from `@objectstack/spec/contracts` and are kern
**Approvals Plugin** — Contributes the `approval` flow node (ADR-0019): an approval runs on the one automation engine as a durable-pause node, backed by `sys_approval_request` / `sys_approval_action`.
-- **Features**: Approver resolution (user/org_membership_level/position/team/department/manager/field/queue), `first_response` / `unanimous`, record lock, status mirror, per-node SLA escalation, audit trail
+- **Features**: Approver resolution (manager/position/department/team/field/expression/org_membership_level/user), `first_response` / `unanimous` / `quorum` / `per_group`, record lock, status mirror, per-node SLA escalation, audit trail
- **When to use**: Any flow that needs human sign-off (expense, quote, contract, …) — add an `approval` node and branch on `approve` / `reject`
### @objectstack/plugin-sharing
@@ -368,7 +369,7 @@ All services implement contracts from `@objectstack/spec/contracts` and are kern
**Email Plugin** — Outbound email channel and templates.
-- **Features**: Provider adapters, MJML templates, delivery tracking
+- **Features**: Provider transports (Postmark, Resend), a minimal mustache-style `{{placeholder}}` template renderer, delivery tracking on `sys_email`
- **When to use**: Transactional and workflow-driven email
### @objectstack/plugin-webhooks
@@ -440,9 +441,9 @@ npx create-objectstack my-app
### @objectstack/types
-**Shared Type Utilities** — Common TypeScript types and utilities.
+**Shared Type Utilities** — Dependency-light runtime contracts that break circular imports between packages.
-- **Exports**: Type helpers, utility types, branded types
+- **Exports**: `IKernel`, `RuntimePlugin`, `RuntimeContext`, plus env-reading (`readEnvWithDeprecation`), degraded-boot, error-leak, and module-not-found helpers
- **When to use**: Imported automatically by other packages
- **README**: [View README](https://github.com/objectstack-ai/objectstack/blob/main/packages/types/README.md)
@@ -460,13 +461,16 @@ The snippets below illustrate which **runtime** packages you typically reach for
### For New Projects (edge / AI-native)
```typescript
-// Host bootstrap (pseudocode — exact shape depends on adapter)
-import { ObjectKernel } from '@objectstack/core';
+// Host bootstrap (exact shape depends on adapter)
+import { ObjectKernel, DriverPlugin } from '@objectstack/runtime';
+import { ObjectQLPlugin } from '@objectstack/objectql';
import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
// Open edition: AI is exposed via @objectstack/mcp (BYO-AI), not an in-process plugin.
const kernel = new ObjectKernel();
-await kernel.use(new SqliteWasmDriver({ filename: ':memory:' }));
+await kernel.use(new ObjectQLPlugin());
+// A driver is not a plugin — wrap it in `DriverPlugin` before `kernel.use()`.
+await kernel.use(new DriverPlugin(new SqliteWasmDriver({ filename: ':memory:' })));
// Point your own AI (Claude/Cursor/local model) at objects, queries & actions over MCP.
await kernel.bootstrap();
```
@@ -474,28 +478,29 @@ await kernel.bootstrap();
### For Traditional Web Apps
```typescript
-import { createDriverPlugin } from '@objectstack/driver-sql';
-import { createAuthPlugin } from '@objectstack/plugin-auth';
-import { createServiceQueuePlugin } from '@objectstack/service-queue';
+import { DriverPlugin } from '@objectstack/runtime';
+import { SqlDriver } from '@objectstack/driver-sql';
+import { AuthPlugin } from '@objectstack/plugin-auth';
+import { QueueServicePlugin } from '@objectstack/service-queue';
-await kernel.use(createDriverPlugin({ client: 'pg', /* … */ }));
-await kernel.use(createAuthPlugin({ /* … */ }));
-await kernel.use(createServiceQueuePlugin({ /* … */ }));
+await kernel.use(new DriverPlugin(new SqlDriver({ client: 'pg', connection: { /* … */ } })));
+await kernel.use(new AuthPlugin({ /* … */ }));
+await kernel.use(new QueueServicePlugin({ adapter: 'auto' }));
```
### For Enterprise Applications
```typescript
-import { createSecurityPlugin } from '@objectstack/plugin-security';
-import { createAuditPlugin } from '@objectstack/plugin-audit';
-import { createServiceAnalyticsPlugin } from '@objectstack/service-analytics';
+import { SecurityPlugin } from '@objectstack/plugin-security';
+import { AuditPlugin } from '@objectstack/plugin-audit';
+import { AnalyticsServicePlugin } from '@objectstack/service-analytics';
-await kernel.use(createSecurityPlugin({ /* … */ }));
-await kernel.use(createAuditPlugin({ /* … */ }));
-await kernel.use(createServiceAnalyticsPlugin({ /* … */ }));
+await kernel.use(new SecurityPlugin({ /* … */ }));
+await kernel.use(new AuditPlugin());
+await kernel.use(new AnalyticsServicePlugin({ /* … */ }));
```
-> The exact factory names follow each package's `README.md` — they all return a plugin object compatible with `kernel.use()`.
+> The official plugin and service packages above export plugin **classes** you instantiate (`new XPlugin(options)`) — check each package's `README.md` for its option shape. Some infrastructure packages ship a `create*Plugin` factory instead (`createRestApiPlugin`, `createDispatcherPlugin`, `createPlatformObjectsPlugin`, …). Either way the result implements the `Plugin` interface `kernel.use()` accepts.
---
diff --git a/content/docs/releases/implementation-status.mdx b/content/docs/releases/implementation-status.mdx
index 71270ef03a..61688480d6 100644
--- a/content/docs/releases/implementation-status.mdx
+++ b/content/docs/releases/implementation-status.mdx
@@ -58,8 +58,8 @@ This matrix is generated from actual codebase analysis and represents the curren
**Notes:**
- ObjectQL implements complete IDataEngine interface
-- Memory driver is reference implementation (basic CRUD only)
-- Advanced query features (aggregations, window functions) require production drivers
+- Memory driver is the reference implementation: filters, sorting, pagination, aggregations, snapshot transactions, streaming and `syncSchema()` are on; joins, window functions, subqueries, full-text and vector search are off (`InMemoryDriver.supports`)
+- Advanced query features (joins, window functions, subqueries) require production drivers
- Memory driver supports aggregations (groupBy, count, sum, avg, min, max)
### Metadata Framework
@@ -70,8 +70,9 @@ This matrix is generated from actual codebase analysis and represents the curren
- Metadata API (list types, list items, get item) is fully functional
- Metadata is loaded from config files or `dist/objectstack.json` at startup and held in memory
-- `MetadataPlugin` no longer registers `sys_metadata` / `sys_metadata_history` into the runtime or auto-bridges ObjectQL to `DatabaseLoader`
-- Future: production Artifact API loader and durable local artifact cache
+- `MetadataPlugin` auto-provisions the `sys_metadata` / `sys_metadata_history` storage objects (plus the ADR-0067 commit log and the metadata audit trail) by default, so `PUT /api/v1/meta/{type}/{name}` always has somewhere to write. Per-project (cloud) kernels opt out with `registerSystemObjects: false`, because there the control plane owns those tables
+- `MetadataPlugin` does **not** auto-bridge ObjectQL to a `DatabaseLoader` — there is no automatic runtime project-DB bridge, and nothing in the packages calls the bridge during boot. Database-backed metadata is an explicit `MetadataManager` opt-in (`setDataEngine()` / `setDatabaseDriver()`, or a `datasource` + `driver` pair on the manager config), and even then it is control-plane only: both methods return early whenever an `environmentId` is supplied, so a per-project kernel never gets a database-backed metadata loader
+- Future: production Artifact API loader (`artifactSource: { mode: 'artifact-api' }` is reserved) and durable local artifact cache
### System Services
@@ -83,10 +84,10 @@ This matrix is generated from actual codebase analysis and represents the curren
| **Tracing** | @objectstack/observability | ⚠️ | Error/exporter pipeline shipped (`error-exporters.ts`); full distributed tracing in progress |
| **Audit** | @objectstack/plugin-audit | ✅ | Audit writers + audit objects shipped |
| **Job** | @objectstack/service-job | ✅ | Job service with cron/db/interval adapters |
-| **Cache** | ⚠️ | ⚠️ | HTTP caching implemented, in-memory cache partial |
+| **Cache** | @objectstack/service-cache | ✅ | `ICacheService` with in-memory and Redis adapters (`cache-service-plugin.ts`); HTTP caching ships separately in `@objectstack/rest` |
| **Translation** | @objectstack/service-i18n | ✅ | i18n/translation service with file adapter |
| **Feature Flags** | ❌ | 📋 | Planned |
-| **Encryption** | ❌ | 📋 | Planned |
+| **Encryption** | @objectstack/service-settings | ⚠️ | `ICryptoProvider` seam with a local AES-256-GCM provider (`local-crypto-provider.ts`); `secret` fields are encrypted at rest into `sys_secret` (`objectql/secret-fields.ts`). Managed KMS/Vault providers not shipped |
| **Compliance** | ❌ | 📋 | Planned |
| **Masking** | ❌ | 📋 | Planned |
| **Notification** | @objectstack/service-messaging | 🟡 | Framework pipeline shipped; objectui bell cut-over remains |
@@ -105,7 +106,7 @@ This matrix is generated from actual codebase analysis and represents the curren
| **Object** | ✅ | ✅ | ✅ | ✅ Full |
| **Validation** | ✅ | ⚠️ | ❌ | ⚠️ Partial |
| **Hook** | ✅ | ✅ | ❌ | ✅ Full |
-| **Dataset** | ✅ | ❌ | ❌ | ❌ Not Impl |
+| **Dataset** | ✅ | ❌ | ✅ | ✅ `@objectstack/service-analytics` |
| **Mapping** | ✅ | ❌ | ❌ | ❌ Not Impl |
| **Document** | ✅ | ❌ | ❌ | ❌ Not Impl |
| **External Lookup** | ✅ | ❌ | ❌ | ❌ Not Impl |
@@ -116,14 +117,14 @@ This matrix is generated from actual codebase analysis and represents the curren
|:-----------|:-------------:|:------|
| text, textarea, email, url, phone | ✅ | Full support |
| number, currency, percent | ✅ | Full support |
-| boolean, checkbox | ✅ | Full support |
+| boolean, toggle | ✅ | Full support (there is no `checkbox` type — a checkbox group is `checkboxes`) |
| date, datetime, time | ✅ | Full support |
| select, multiselect | ✅ | Full support |
| lookup, master_detail | ✅ | Full support in spec, partial in memory driver |
| formula | ✅ | CEL-backed formula fields are implemented |
| summary | ✅ | Server-side rollups for count/sum/min/max/avg on child records |
-| json, array | ✅ | Full support |
-| file, image | 📋 | Protocol defined, not implemented |
+| json, tags | ✅ | Full support (there is no `array` type — use `json`, `tags`, or the `repeater` embedded type) |
+| file, image | ✅ | File-class fields (`image`/`file`/`avatar`/`video`/`audio`) hold an opaque `sys_file` id; ownership + release are tracked by `@objectstack/service-storage` |
### Query Engine
@@ -133,8 +134,8 @@ This matrix is generated from actual codebase analysis and represents the curren
| **Query Builder** | ❌ | ✅ | ❌ | ✅ Client |
| **Filter Operators** | ✅ | ✅ | ⚠️ | ⚠️ Partial |
| **Aggregations** | ✅ | ✅ | ✅ | ✅ Full |
-| **Joins** | ✅ | ✅ | ❌ | ⚠️ Spec only |
-| **Sorting** | ✅ | ✅ | ❌ | ⚠️ Spec only |
+| **Joins** | ✅ | ✅ | ❌ | ⚠️ Driver-dependent |
+| **Sorting** | ✅ | ✅ | ✅ | ✅ Full |
| **Pagination** | ✅ | ✅ | ✅ | ✅ Full |
| **Window Functions** | ✅ | ❌ | ❌ | ❌ Spec only |
| **Subqueries** | ✅ | ❌ | ❌ | ❌ Spec only |
@@ -142,8 +143,8 @@ This matrix is generated from actual codebase analysis and represents the curren
**Notes:**
- Full query protocol is defined and implemented in ObjectQL engine
- Actual query capability depends on driver implementation
-- Memory driver supports basic queries only
-- Client SDK provides query builder utilities
+- Memory driver covers filters, sorting, pagination and aggregations; joins, window functions and subqueries are off
+- Client SDK provides query builder utilities (`QueryBuilder` / `FilterBuilder`, `createQuery` / `createFilter`)
---
@@ -164,27 +165,29 @@ This matrix is generated from actual codebase analysis and represents the curren
| **HTTP Cache** | ✅ | ✅ | ✅ | ✅ | ✅ Full |
| **Batch** | ✅ | ✅ | ✅ | ✅ | ✅ Full |
-The data (`/data`), metadata (`/meta`), and batch endpoints are implemented in `@objectstack/rest` (`rest-server.ts`); `@objectstack/runtime` re-exports `RestServer` from that package and provides the underlying HTTP server/dispatcher.
+The data (`/api/v1/data`), metadata (`/api/v1/meta`), and batch endpoints are implemented in `@objectstack/rest` (`rest-server.ts`); `@objectstack/runtime` re-exports `RestServer` from that package and provides the underlying HTTP server/dispatcher.
**REST Endpoints Implemented:**
| Endpoint Pattern | Method | Purpose | Status |
|:----------------|:------:|:--------|:------:|
-| `/api/v1` | GET | API discovery | ✅ |
-| `/meta` | GET | List metadata types | ✅ |
-| `/meta/{type}` | GET | List items of type | ✅ |
-| `/meta/{type}/{name}` | GET | Get specific metadata item | ✅ |
-| `/data/{object}` | GET | List records | ✅ |
-| `/data/{object}/{id}` | GET | Get single record | ✅ |
-| `/data/{object}` | POST | Create record | ✅ |
-| `/data/{object}/{id}` | PATCH | Update record | ✅ |
-| `/data/{object}/{id}` | DELETE | Delete record | ✅ |
-| `/data/{object}/createMany` | POST | Batch create | ✅ |
-| `/data/{object}/updateMany` | POST | Batch update | ✅ |
-| `/data/{object}/deleteMany` | POST | Batch delete | ✅ |
-| `/data/{object}/{id}/clone` | POST | Clone a record (gated by `enable.clone`) | ✅ |
-| `/data/{object}/batch` | POST | Atomic batch operations | ✅ |
-| `/batch` | POST | Cross-object atomic batch (parent + children in one transaction, `$ref`) | ✅ |
+| `/api/v1` | GET | API discovery (alias; the SDK connects via `/api/v1/discovery`) | ✅ |
+| `/api/v1/meta` | GET | List metadata types | ✅ |
+| `/api/v1/meta/{type}` | GET | List items of type | ✅ |
+| `/api/v1/meta/{type}/{name}` | GET | Get specific metadata item | ✅ |
+| `/api/v1/data/{object}` | GET | List records | ✅ |
+| `/api/v1/data/{object}/{id}` | GET | Get single record | ✅ |
+| `/api/v1/data/{object}` | POST | Create record | ✅ |
+| `/api/v1/data/{object}/{id}` | PATCH | Update record | ✅ |
+| `/api/v1/data/{object}/{id}` | DELETE | Delete record | ✅ |
+| `/api/v1/data/{object}/createMany` | POST | Batch create | ✅ |
+| `/api/v1/data/{object}/updateMany` | POST | Batch update | ✅ |
+| `/api/v1/data/{object}/deleteMany` | POST | Batch delete | ✅ |
+| `/api/v1/data/{object}/{id}/clone` | POST | Clone a record (gated by `enable.clone`) | ✅ |
+| `/api/v1/data/{object}/batch` | POST | Atomic batch operations | ✅ |
+| `/api/v1/batch` | POST | Cross-object atomic batch (parent + children in one transaction, `$ref`) | ✅ |
+
+Every route carries the `/api/v1` prefix. When project scoping is enabled each row is additionally mirrored under `/api/v1/environments/{environmentId}`. The full audited route list lives in `packages/rest/src/rest-route-ledger.ts`.
### Advanced Protocols
@@ -249,11 +252,11 @@ The data (`/data`), metadata (`/meta`), and batch endpoints are implemented in `
| **Webhook** | ✅ | ❌ | ✅ | ✅ `@objectstack/plugin-webhooks` |
| **ETL** | ✅ | ❌ | ✅ | 📋 Plugin |
| **Sync** | ✅ | ❌ | ✅ | 📋 Plugin |
-| **Trigger Registry** | ✅ | ❌ | ✅ | ✅ `trigger-record-change` / `trigger-schedule` |
+| **Trigger Registry** | ✅ | ❌ | ✅ | ✅ `trigger-api` / `trigger-record-change` / `trigger-schedule` |
**Notes:**
- Hook system is implemented in ObjectQL (beforeFind, afterInsert, etc.) — this is data-layer eventing, not workflow automation
-- The flow/workflow engine ships in `@objectstack/service-automation` (`engine.ts`, builtin nodes, `plugin.ts`); approvals, webhooks, and triggers ship as `plugin-approvals`, `plugin-webhooks`, `trigger-record-change`, and `trigger-schedule`
+- The flow/workflow engine ships in `@objectstack/service-automation` (`engine.ts`, builtin nodes, `plugin.ts`); approvals, webhooks, and triggers ship as `@objectstack/plugin-approvals`, `@objectstack/plugin-webhooks`, `@objectstack/trigger-api`, `@objectstack/trigger-record-change`, and `@objectstack/trigger-schedule`
- ETL and Sync protocols are defined but not yet implemented as plugins
- Discovery API reports automation service as `unavailable` until a plugin is registered
@@ -267,26 +270,26 @@ The data (`/data`), metadata (`/meta`), and batch endpoints are implemented in `
### Auth Service (`plugin-auth`)
-The `auth` service in `CoreServiceName` covers both **authentication** (identity) and **authorization** (permissions). There is no separate `permission` service — it is part of `auth`.
+`CoreServiceName` names a single `auth` service — there is no `permission` entry in that enum. Authentication/identity is what `@objectstack/plugin-auth` registers under that name. Authorization is registered *outside* `CoreServiceName`: `@objectstack/plugin-security` registers `security` (plus `security.permissions`, `security.rls`, `security.fieldMasker`) and `@objectstack/plugin-sharing` registers `sharing`, `sharingRules`, and `shareLinks`.
| Protocol | Area | @objectstack/spec | Kernel | Plugin Required | Status |
|:---------|:-----|:-----------------:|:------:|:---------------:|:------:|
-| **Identity** | Authentication | ✅ | ❌ | ✅ | 📋 Plugin |
-| **Auth Config** | Authentication | ✅ | ❌ | ✅ | 📋 Plugin |
-| **Role** | Authentication | ✅ | ❌ | ✅ | 📋 Plugin |
-| **Organization** | Authentication | ✅ | ❌ | ✅ | 📋 Plugin |
+| **Identity** | Authentication | ✅ | ❌ | ✅ | ✅ `plugin-auth` (better-auth) |
+| **Auth Config** | Authentication | ✅ | ❌ | ✅ | ✅ `plugin-auth` |
+| **Position** | Authentication | ✅ | ❌ | ✅ | ✅ `plugin-auth` + `plugin-security` |
+| **Organization** | Authentication | ✅ | ❌ | ✅ | ✅ `plugin-auth` (`tenancy` service) |
| **Policy** | Authentication | ✅ | ❌ | ✅ | 📋 Plugin |
-| **SCIM** | Authentication | ✅ | ❌ | ✅ | 📋 Plugin |
+| **SCIM** | Authentication | ✅ | ❌ | ✅ | ⚠️ `plugin-auth`, opt-in via `OS_SCIM_ENABLED` |
| **Permission** | Authorization | ✅ | ✅ Phase-1 | ✅ | ✅ `plugin-security` |
-| **Sharing** | Authorization | ✅ | ❌ | ✅ | 📋 Plugin |
+| **Sharing** | Authorization | ✅ | ❌ | ✅ | ✅ `plugin-sharing` |
| **RLS** | Authorization | ✅ | ✅ Phase-1 (tenant + owner) | ✅ | ✅ `plugin-security` |
**Notes:**
-- All security protocols (identity + permission) are delivered by a single `auth` plugin — matching `CoreServiceName`
+- Security is delivered by three cooperating plugins, not one: `plugin-auth` (identity, `auth` + `tenancy` services), `plugin-security` (permissions, FLS, RLS, tenant wall), and `plugin-sharing` (OWD, sharing rules, per-record shares, share links)
- Client SDK supports bearer token header — but token validation requires the auth plugin
- Auth route (`/auth/*`) only appears in Discovery when the auth plugin is registered
-- Fine-grained authorization (RLS, sharing, territory) is internal to the auth plugin
-- **Phase-1 RBAC enforcement is live end-to-end**: REST → ObjectQL → SecurityPlugin middleware now receives a populated `ExecutionContext` (userId, tenantId, positions, permissions). Tenant isolation is enforced as a Layer 0 tenant wall (`plugin-security/tenant-layer.ts`, ADR-0095 D1) that AND-composes `organization_id == current_user.organization_id` ahead of and independently of business RLS — the earlier wildcard `tenant_isolation` RLS policy on `member_default` was retired (an OR-merged business policy could widen it). The default `member_default` set still ships per-object overrides `sys_organization_self` (`id == current_user.organization_id`) and `sys_user_self` (`id == current_user.id`) for the global tables that lack an `organization_id` column. The earlier `tenantField` indirection (RLS expressions written against an abstract `tenant_id` column then rewritten to the configured physical column at compile time) was removed — the placeholder, the column name, and `RLSUserContext.organization_id` are now the same name end-to-end. The legacy `objectql.registerTenantMiddleware` (hardcoded `where.tenant_id` injection that pre-dated SecurityPlugin) has been removed; SecurityPlugin is the sole authority for tenant isolation. Analytics now uses the same reusable read scope via `security.getReadFilter`, so dataset-bound dashboards/reports do not bypass RLS. Verified cross-organization isolation on `pnpm dev:crm` across `sys_organization`, `sys_member`, `sys_user`, `sys_user_permission_set`, `sys_position_permission_set`. **Anonymous traffic is denied by default** (the ADR-0056 D2 default-deny flip landed: `requireAuth` defaults to `true`); an explicit `requireAuth: false` opt-out logs a boot-time warning, and public forms do not depend on any fall-open — they carry a declaration-derived `publicFormGrant` (ADR-0056 Option A).
+- Fine-grained authorization (RLS, sharing) lives in `plugin-security` / `plugin-sharing`, not in the auth plugin. Territory-style access is expressed as an RLS dynamic-membership set (`ExecutionContext.rlsMembership`, e.g. `id in current_user.territory_account_ids`) rather than as a dedicated territory module
+- **Phase-1 RBAC enforcement is live end-to-end**: REST → ObjectQL → SecurityPlugin middleware now receives a populated `ExecutionContext` (userId, tenantId, positions, permissions). Tenant isolation is enforced as a Layer 0 tenant wall (`plugin-security/tenant-layer.ts`, ADR-0095 D1) that AND-composes `organization_id == current_user.organization_id` ahead of and independently of business RLS — the earlier wildcard `tenant_isolation` RLS policy on `member_default` was retired (an OR-merged business policy could widen it). The default `member_default` set still ships per-object overrides `sys_organization_self` (`id == current_user.organization_id`) and `sys_user_self` (`id == current_user.id`) for the global tables that lack an `organization_id` column. The earlier `tenantField` indirection (RLS expressions written against an abstract `tenant_id` column then rewritten to the configured physical column at compile time) was removed — the placeholder, the column name, and `RLSUserContext.organization_id` are now the same name end-to-end. The legacy `objectql.registerTenantMiddleware` (hardcoded `where.tenant_id` injection that pre-dated SecurityPlugin) has been removed; SecurityPlugin is the sole authority for tenant isolation. Analytics now uses the same reusable read scope via `security.getReadFilter`, so dataset-bound dashboards/reports do not bypass RLS. Verified cross-organization isolation on `pnpm dev:crm` across `sys_organization`, `sys_member`, `sys_user`, `sys_user_permission_set`, `sys_position_permission_set`. **Anonymous traffic is always denied** (ADR-0056 D2). The deployment-wide opt-out is gone: `api.requireAuth` was retired in `@objectstack/spec` 18 (#3963) and is now a tombstoned key that fails validation rather than reopening the data plane. The single decision lives in `@objectstack/core` (`security/anonymous-deny.ts`, 401 `UNAUTHENTICATED`), and every surface that legitimately serves a session-less caller derives its own narrow authorization from a declaration instead: control-plane paths via the auth-gate allowlist, public forms via `publicFormGrant` (ADR-0056 Option A), share links via a capability token validated then read as SYSTEM, `book.audience: 'public'` reads via the audience gate, and MCP via an OAuth token or API key.
- **OWD / sharing-model enforcement is live and proven end-to-end (ADR-0056)**: `private`, `public_read`, `public_read_write`, and `controlled_by_parent` are enforced through `plugin-sharing` + `plugin-security` and verified by dogfood proofs over the real HTTP stack. `object.sharingModel` accepts the canonical OWD vocabulary only (`private` / `public_read` / `public_read_write` / `controlled_by_parent`) — the legacy `read` / `read_write` / `full` aliases were removed from the enum (ADR-0090 D4), and an unset `sharingModel` on a custom object resolves to `private` (ADR-0090 D1). RLS owner policies resolve `current_user.email` in addition to `id` / `organization_id` / `positions` (#2054). Permission sets may declare `isDefault: true` as the install-time suggestion to bind the set to the built-in `everyone` position (ADR-0090 D5, superseding the ADR-0056 D7 fallback-profile mechanism). **A sharing rule must state its criteria** (#3896): all three write paths reject a match-all shape, a stored criteria-less rule matches nothing, and its materialised grants are revoked on the next reconcile.
---
@@ -345,9 +348,11 @@ describe that separate runtime.
**Features Implemented:**
- TestSuite, TestScenario, TestStep schemas ✅
-- HTTP adapter for testing ✅
-- MSW integration for browser mocking ✅
-- QA runner in core ✅
+- HTTP adapter for testing ✅ (`packages/core/src/qa/http-adapter.ts`)
+- QA runner in core ✅ (`packages/core/src/qa/runner.ts`)
+- Conformance/dogfood harnesses in `@objectstack/verify` and `packages/qa` (`dogfood`, `downstream-contract`, `http-conformance`) ✅
+
+There is no MSW package in this repo — browser API mocking is a devDependency of the sibling `objectui` frontend, and `@objectstack/driver-memory` is what backs those in-browser fixtures.
---
@@ -413,29 +418,31 @@ describe that separate runtime.
### Phase 8: Automation (Plugin) 🟡 **IN PROGRESS**
- [x] Flow Engine Plugin
- [x] Approval Node Plugin
+- [x] Trigger Registry Plugins — `@objectstack/trigger-api`, `@objectstack/trigger-record-change`, `@objectstack/trigger-schedule` (including the time-relative trigger)
- [ ] ETL Pipeline Plugin
-- [ ] Trigger Registry Plugin
+- [ ] Sync Plugin
### Phase 9: Security (Plugin) 🟡 **PHASE-1 LANDED**
- [x] Authentication Plugin (`@objectstack/plugin-auth`, better-auth)
- [x] Authorization Plugin — `@objectstack/plugin-security` enforces CRUD/FLS/RLS in the ObjectQL middleware chain; REST → ObjectQL now propagates `ExecutionContext` end-to-end (Phase-1)
-- [x] Row-Level Security — tenant isolation is a Layer 0 tenant wall (`plugin-security/tenant-layer.ts`, ADR-0095 D1) that AND-composes `organization_id == current_user.organization_id` independently of business RLS (the earlier wildcard `tenant_isolation` policy on `member_default` was retired); `member_default` still applies per-object overrides `sys_organization_self` / `sys_user_self`. The earlier `tenantField` rewrite indirection was removed: RLS column, placeholder, and `RLSUserContext.organization_id` use the same canonical name end-to-end. Policy `enabled: false` is enforced at the compiler choke point — a disabled policy no longer contributes its OR-branch grant (#3980) — and the void `priority` key is removed (#3990)
+- [x] Row-Level Security — tenant isolation is a Layer 0 tenant wall (`plugin-security/tenant-layer.ts`, ADR-0095 D1) that AND-composes `organization_id == current_user.organization_id` independently of business RLS (the earlier wildcard `tenant_isolation` policy on `member_default` was retired); `member_default` still applies per-object overrides `sys_organization_self` / `sys_user_self`. The earlier `tenantField` rewrite indirection was removed: RLS column, placeholder, and `RLSUserContext.organization_id` use the same canonical name end-to-end. Policy `enabled: false` is enforced at the compiler choke point — a disabled policy no longer contributes its OR-branch grant (#3980) — and the void `rowLevelSecurity[].priority` key was removed in `@objectstack/spec` 17.0.0 (#3896 security audit)
- [x] Analytics RLS bridge — `@objectstack/service-analytics` auto-bridges to `security.getReadFilter(object, context)` and fails closed when read-scope resolution cannot be safely applied
- [x] Multi-tenancy — verified cross-organization isolation on `pnpm dev:crm` (Alice@OrgAlpha vs. Bob@OrgBeta only see their own records across `sys_organization`, `sys_member`, `sys_user`, and `sys_*_permission_set` link tables)
- [x] Legacy `objectql.registerTenantMiddleware` removed — SecurityPlugin is now the sole tenant-isolation authority
- [x] Organization-Wide Defaults / sharing model — `private`, `public_read`, `public_read_write`, and `controlled_by_parent` enforced via `plugin-sharing` + `plugin-security`, proven by dogfood over the real HTTP stack (ADR-0056). Canonical vocabulary only — legacy aliases removed from the enum (ADR-0090 D4); unset custom-object OWD resolves to `private` (ADR-0090 D1)
- [x] Sharing Rule evaluator — criteria rules re-evaluated on `afterInsert` / `afterUpdate` (`plugin-sharing/rule-hooks.ts`); enforced recipients are user / position / `unit_and_subordinates` (business-unit-subtree widening, ADR-0057 D5 / ADR-0090 D3); owner-type rules and group/guest recipients remain `[experimental — not enforced]`
- [x] Everyone-baseline suggestion — a permission set may set `isDefault: true` as the install-time suggestion to bind it to the built-in `everyone` position; resolved per-request as an additive baseline, no fallback cliff (ADR-0090 D5)
-- [x] Default-deny for anonymous traffic — the global default-deny **flip landed** (ADR-0056 D2): `requireAuth` defaults to `true`, explicit `requireAuth: false` opt-outs warn at boot, and public forms self-authorize via `publicFormGrant` (Option A)
+- [x] Default-deny for anonymous traffic — the global default-deny landed (ADR-0056 D2) and the `api.requireAuth` opt-out was then **removed** in `@objectstack/spec` 18 (#3963): the key is tombstoned and rejected at parse time, the deny decision is centralised in `@objectstack/core` `security/anonymous-deny.ts`, and public forms self-authorize via `publicFormGrant` (Option A)
- [ ] Studio RLS visual editor
- [ ] Per-user×org permission cache
- [ ] Audit UI / denied-access logging
-### Phase 10: AI Integration 📋 **PLANNED**
-- [ ] Agent Framework
-- [ ] RAG Pipeline
-- [ ] NLQ Engine
-- [ ] Model Registry
+### Phase 10: AI Integration 🟡 **IN PROGRESS**
+- [x] RAG Pipeline — `@objectstack/service-knowledge` plus the `knowledge-memory`, `knowledge-ragflow`, and `embedder-openai` plugins ship in this repo
+- [x] MCP surface — `@objectstack/mcp` exposes the platform to any BYO-AI client
+- [ ] Agent Framework (ObjectOS runtime; not in the open-source framework)
+- [ ] NLQ Engine (ObjectOS runtime)
+- [ ] Model Registry (ObjectOS runtime)
---
@@ -448,8 +455,8 @@ describe that separate runtime.
| **Data** | 16 | Core modeling, hooks, and query engine fully implemented; document/mapping/external-lookup still pending |
| **UI** | 10 | Studio/ObjectUI render most authored surfaces (🟡); full cross-surface renderer parity in progress |
| **API** | 14 | REST/HTTP/discovery/batch fully implemented; OData pending; GraphQL removed from the plan |
-| **System** | 39 | Logging, audit, job, translation, metrics, notification implemented; several governance services pending |
-| **Auth** (plugin) | 10 | Permission + RLS live (`plugin-security`); identity/sharing/territory still plugin-pending |
+| **System** | 39 | Logging, audit, job, translation, metrics, cache, notification implemented; encryption partial (local crypto provider); several governance services pending |
+| **Auth** (plugin) | 10 | Identity/organizations live (`plugin-auth`), permission + RLS live (`plugin-security`), OWD + sharing rules live (`plugin-sharing`); SCIM is opt-in |
| **Automation** (plugin) | 7 | Flow, workflow, approval, webhook, and triggers implemented; ETL/Sync pending |
| **AI** | 12 | Agents, model registry, conversation, tools, RAG implemented (ObjectOS runtime + open knowledge plugins); cost/predictive/DevOps-agent pending |
| **Integration** | 7 | REST/OpenAPI/MCP/Slack connectors, file storage, and queue implemented; GitHub/Vercel connectors pending |
@@ -457,7 +464,7 @@ describe that separate runtime.
### Implementation Coverage
-Implementation spans every layer of the platform. Core infrastructure, data modeling, the REST API, client SDKs, security (Phase-1), automation, AI, and integration all have shipping implementations. Remaining gaps are concentrated in specific protocols (OData, cross-surface UI renderer parity, sharing/territory authorization, ETL/Sync, and a handful of governance and AI-cost services) rather than entire layers. Refer to the per-layer tables above for protocol-level status.
+Implementation spans every layer of the platform. Core infrastructure, data modeling, the REST API, client SDKs, security (Phase-1), automation, AI, and integration all have shipping implementations. Remaining gaps are concentrated in specific protocols (OData, cross-surface UI renderer parity, ETL/Sync, the mapping/document/external-lookup data protocols, and a handful of governance and AI-cost services) rather than entire layers. Refer to the per-layer tables above for protocol-level status.
### Core Functionality Status
@@ -471,38 +478,37 @@ Implementation spans every layer of the platform. Core infrastructure, data mode
| **Metadata System** | ⚠️ | Partial (`sys_metadata` persistence via `MetadataPlugin`; without it the kernel's in-memory fallback fills the slot and discovery reports it `degraded`) |
| **HTTP Caching** | ✅ | Yes |
| **Testing Tools** | ✅ | Yes |
-| **UI Rendering** | ❌ | No |
+| **UI Rendering** | 🟡 | Partial (Studio/ObjectUI render authored surfaces; cross-surface renderer parity in progress) |
| **Workflows** | ✅ | Yes (plugin: `service-automation`) |
-| **Security** | ⚠️ | Partial (Phase-1 RBAC/RLS via `plugin-security`) |
+| **Security** | ⚠️ | Partial (Phase-1 RBAC/FLS/RLS via `plugin-security`; OWD + sharing rules via `plugin-sharing`; identity via `plugin-auth`) |
| **AI Features** | ⚠️ | Partial (ObjectOS runtime + open knowledge plugins) |
---
## Package Feature Matrix
-| Feature | spec | core | objectql | runtime | client | client-react | cli | metadata | hono | memory | msw |
-|:--------|:----:|:----:|:--------:|:-------:|:------:|:------------:|:---:|:--------:|:----:|:------:|:---:|
-| **Protocol Definitions** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
-| **Microkernel** | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
-| **Plugin System** | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
-| **Service Registry** | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
-| **Event Bus** | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
-| **Logging** | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
-| **Schema Registry** | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
-| **ObjectQL Engine** | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
-| **Protocol Implementation** | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
-| **REST Server** | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
-| **Endpoint Generation** | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
-| **HTTP Server** | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
-| **Client SDK** | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
-| **Query Builder** | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
-| **React Hooks** | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
-| **CLI Commands** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
-| **Config Validation** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
-| **Metadata Loading** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
-| **File Watching** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
-| **Database Driver** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ⚠️ | ❌ |
-| **API Mocking** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
+| Feature | spec | core | objectql | runtime | client | client-react | cli | metadata | hono | memory |
+|:--------|:----:|:----:|:--------:|:-------:|:------:|:------------:|:---:|:--------:|:----:|:------:|
+| **Protocol Definitions** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
+| **Microkernel** | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
+| **Plugin System** | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
+| **Service Registry** | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
+| **Event Bus** | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
+| **Logging** | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
+| **Schema Registry** | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
+| **ObjectQL Engine** | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
+| **Protocol Implementation** | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
+| **REST Server** | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
+| **Endpoint Generation** | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
+| **HTTP Server** | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
+| **Client SDK** | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
+| **Query Builder** | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
+| **React Hooks** | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
+| **CLI Commands** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
+| **Config Validation** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
+| **Metadata Loading** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
+| **File Watching** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
+| **Database Driver** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ⚠️ |
> The REST server, endpoint generation, and data/meta/batch endpoints are implemented in the `@objectstack/rest` package (not shown as a column above); `@objectstack/runtime` re-exports `RestServer` from it.
diff --git a/content/docs/releases/v9.mdx b/content/docs/releases/v9.mdx
index 6ebd3cbd26..6dbaf4c579 100644
--- a/content/docs/releases/v9.mdx
+++ b/content/docs/releases/v9.mdx
@@ -204,6 +204,11 @@ Coming in the next framework release: published packages default to
Marketplace immediately, with private visibility reserved for targeted
distribution.
+> Note: this has since shipped — `os package publish --visibility` accepts
+> `private` / `org` / `marketplace` and defaults to `org` ("auto-visible /
+> installable across your organization's environments"). See the
+> [Publish & preview guide](/docs/deployment/publish-and-preview).
+
## Notable fixes
- Publishing pending drafts by ref so package-less drafts can't get stuck in
diff --git a/scripts/role-word-baseline.json b/scripts/role-word-baseline.json
index aa9c9e8817..ad6ee3a2ae 100644
--- a/scripts/role-word-baseline.json
+++ b/scripts/role-word-baseline.json
@@ -30,7 +30,6 @@
"content/docs/protocol/objectql/security.mdx": 1,
"content/docs/protocol/objectui/record-alert.mdx": 1,
"content/docs/protocol/objectui/widget-contract.mdx": 3,
- "content/docs/releases/implementation-status.mdx": 1,
"content/docs/releases/index.mdx": 1,
"content/docs/releases/v13.mdx": 15,
"content/docs/releases/v14.mdx": 5,