Skip to content

Commit 49e940a

Browse files
committed
Merge origin/main into claude/implementation-accuracy-audit-eavdgx
Bring the docs-audit branch up to date with main (29 commits) before merge. Conflict resolutions: - content/docs/automation/flows.mdx: took main's newer run-history paragraph (sys_automation_run durable history / bounded step log) which superseded this branch's audit wording of the same section. - content/docs/references/automation/meta.json: regenerated; added main's new `time-relative-trigger` schema to the automation "Flow & Execution" sidebar grouping in build-docs.ts. `gen:docs --check` green (254 files in sync). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012urEihGTAsQP2xizAqfCwt
2 parents 9d9de01 + 86d30af commit 49e940a

162 files changed

Lines changed: 8057 additions & 490 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: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
ADR-0099 P1 (#3211 M2): the Layer 0 cross-tenant exemption gate now reads the carried `ctx.posture` rung (#2956) as authoritative, with the platform-admin capability probe demoted to a fallback for resolver-less contexts (delegated-admin bridge, sharing service, `getReadFilter`). The read and write (insert/update post-image) tenant checks share one decision (`computeLayeredRlsFilter`), so they cannot drift. A probe↔rung disagreement logs a defect breadcrumb and enforces the narrower rung verdict.
6+
7+
**Behavior change (security narrowing, multi-org / `@objectstack/organizations` only):** a principal whose carried rung is not `PLATFORM_ADMIN` no longer crosses the tenant wall on private / platform-global / better-auth-managed objects, even when its resolved permission sets carry a platform-exclusive capability. Two shapes are affected: (a) a **scoped** `admin_full_access` grant (`sys_user_permission_set.organization_id` non-null), and (b) a custom set granting a platform capability (e.g. `studio.access`) piecemeal alongside a superuser bit. Both are now walled to their own org — the fail-safe direction (the carried rung is a strict subset of the probe). Single-org / env-per-database deployments are unaffected (Layer 0 is inert).
8+
9+
**Upgrade check:** before upgrading, scan `sys_user_permission_set` for `admin_full_access` rows with a non-null `organization_id`, and custom permission sets whose `systemPermissions` intersect `{manage_metadata, manage_platform_settings, studio.access, manage_users}`. To restore cross-tenant operator access for such a principal, grant the **unscoped** `admin_full_access` instead. The `[authz/ADR-0099]` warn log names any principal hitting the divergence at runtime.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
ADR-0099 P2′ (#3211 M3′): pin the two-axis Amendment in the authz matrix. The original P2 (collapse the Layer 1 tier onto posture) was rejected — Layer 1's tier input is the per-object super-bit, a per-principal × per-object delegation primitive posture cannot represent. New cells pin: seeded-face agreement (seeded super-bit holders are already ≥ TENANT_ADMIN), the load-bearing delegation cell (a MEMBER with a delegated per-object `viewAllRecords`/`modifyAllRecords` short-circuits Layer 1 yet stays walled by Layer 0 — the auditor pattern), invariant I7 (the scope axis never crosses a boundary posture has not opened), and the contrast that the bit is a real grantable capability, not conditionally inert. Test-only; zero behavior change.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
'@objectstack/rest': patch
3+
'@objectstack/spec': minor
4+
---
5+
6+
fix(rest): gate the cross-object transactional batch by the same per-object API rules as single-record writes (#1604)
7+
8+
The `POST {basePath}/batch` route (issue #1604 / ADR-0034) wraps N cross-object
9+
create/update/delete ops in one engine transaction, but it skipped the
10+
per-object API-exposure gate every single-record route applies — an
11+
authenticated caller could write to an `apiEnabled: false` object, or run an
12+
operation outside an object's `apiMethods` whitelist, straight through the batch
13+
surface (ADR-0049 / #1889 — the same "declared ≠ enforced" hole closed for the
14+
generic write path in #3220 / #3213).
15+
16+
The route now:
17+
18+
- validates the body against a new `CrossObjectBatchRequestSchema`
19+
(`@objectstack/spec/api`, Zod-First) — a malformed op, an unknown action, or a
20+
missing `object` is a `400` instead of a `500`;
21+
- enforces `enable.apiEnabled` / `enable.apiMethods` for **every** op (metadata
22+
fetched once, each distinct `(object, action)` checked) BEFORE opening the
23+
transaction — `404 OBJECT_API_DISABLED` / `405 OBJECT_API_METHOD_NOT_ALLOWED`;
24+
- requires an `id` for `update` / `delete` (`400`);
25+
- rejects an unresolvable `{ $ref }` with `400 BATCH_UNRESOLVED_REF` instead of
26+
silently writing a `null` FK;
27+
- rejects an explicit `atomic: false` (`400 BATCH_NOT_ATOMIC`) rather than
28+
silently applying atomically — non-atomic per-object batches stay on
29+
`POST /data/:object/batch`.
30+
31+
`enforceApiAccess` is refactored to share the pure `apiAccessDenialFromEnable`
32+
check + a `loadObjectItems` helper with the batch route (single-record behavior
33+
unchanged). Adds `rest-batch-endpoint.test.ts` — the REST-boundary coverage
34+
ADR-0034 flagged as missing (commit, `$ref`, rollback surfacing, API-access
35+
denial, request validation).
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'@objectstack/core': minor
3+
'@objectstack/service-analytics': minor
4+
---
5+
6+
feat(analytics): scope a datetime date-bucket drill to the reference-tz midnight instants (#1752 follow-up)
7+
8+
Closes the one gap left by the initial #1752 change: a `datetime` date dimension
9+
bucketed under a **non-UTC reference timezone** previously fell back to a superset
10+
drill (its bucket boundary is that tz's midnight *instant*, which `YYYY-MM-DD`
11+
calendar bounds can't express).
12+
13+
- **`@objectstack/core`** adds `zonedDateStartToUtcMs(ymd, tz)` — the UTC instant
14+
at which a calendar day begins in a reference timezone (the inverse of
15+
`calendarPartsInTz`). DST-safe: the offset is read from the platform tz
16+
database via `Intl`, with a two-pass resolution for the rare offset-boundary
17+
case; an unset/`'UTC'`/invalid zone returns plain UTC midnight.
18+
- **`@objectstack/service-analytics`** now emits `drillRanges` bounds per the
19+
field's temporal type (ADR-0053): a `datetime` field → ISO **instant** bounds
20+
at the reference tz's midnight (works under any tz, incl. DST); a `date` field
21+
`YYYY-MM-DD` calendar bounds (tz-naive, exact under any tz). An unknown field
22+
type is still emitted only under UTC and omitted (superset) under a non-UTC tz.
23+
24+
No objectui change is needed — the client already forwards whatever bound values
25+
the server sends into the drill filter and the `filter[field][gte|lt]` URL.
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): declare `summaryOperations` sub-fields in the Field metadata form (#3257)
6+
7+
`fieldForm` (the registered metadata form for editing a Field) previously
8+
declared `summaryOperations` as a bare `composite` with no sub-fields, so a
9+
protocol-driven renderer had to fall back to a raw JSON editor. It now declares
10+
the inner shape explicitly — `object` (`ref:object`), `function` (select),
11+
`field`, `relationshipField`, and `filter` (bound to `widget: 'filter-condition'`)
12+
— mirroring the `summaryOperations` Zod schema and surfacing the roll-up `filter`
13+
added in #1868. Also gates the block to `data.type == 'summary'`.
14+
15+
Small step toward #3257 (making the Studio field designer metadata-driven rather
16+
than hand-coded); the live objectui inspector already edits these fields.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@objectstack/runtime': patch
3+
---
4+
5+
fix(runtime): honor a hook body's declared `timeoutMs` so nested cross-object writes aren't clamped to 250ms (#1867)
6+
7+
Hook bodies run in the QuickJS sandbox with a default 250ms timeout. The runner
8+
folded that engine default straight into `Math.min(...)` when resolving the
9+
effective timeout, so it *always* dominated for hooks: a body that declared a
10+
larger `timeoutMs` (the spec permits up to 30_000ms — `ScriptBody.timeoutMs`) to
11+
give a legitimate nested write — "when a child changes, update the parent" —
12+
room to settle was silently clamped back to 250ms and killed mid-flight. The
13+
declared knob was never enforced.
14+
15+
The engine default is now a FALLBACK used only when no explicit timeout is
16+
supplied, not a hard ceiling. An explicit `body.timeoutMs` (and/or an enclosing
17+
hook/action timeout) is honored; when both are present the smaller wins. Bodies
18+
that declare nothing still get the 250ms hook / 5000ms action default, and a
19+
body may still LOWER its own timeout below the default.
20+
21+
This clears the last reliability blocker for nested cross-object writes from
22+
hooks — the sandbox crash itself (`memory access out of bounds`) was already
23+
fixed by the deferred-promise host-call model — so header/rollup fields no
24+
longer need denormalized, hand-maintained workarounds.

.changeset/mcp-dev-connect-hint.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
feat(cli): surface the MCP endpoint in the server-ready banner (#3167)
6+
7+
The MCP server (`/api/v1/mcp`) is a default-on core capability, but nothing in
8+
the `os dev` / `os serve` boot output pointed to it — a developer had to already
9+
know it was there to connect an AI client. The server-ready banner now prints
10+
the MCP URL and the `SKILL.md` pointer whenever the surface is enabled
11+
(`isMcpServerEnabled()`, the same switch that auto-loads the plugin and gates
12+
the route), so an agent can operate the running app straight from the dev loop.
13+
Hidden when `OS_MCP_SERVER_ENABLED=false`.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@objectstack/mcp': minor
3+
---
4+
5+
feat(mcp)!: stdio transport requires an API-key principal — fail-closed, no unscoped bridge (ADR-0101, #3246)
6+
7+
The long-lived MCP **stdio** transport no longer reads data unscoped. It now runs
8+
under an env-supplied identity, closing the platform's last identity-less
9+
execution surface (the `mcp-stdio-authority` conformance row graduates
10+
`experimental``enforced`).
11+
12+
- `OS_MCP_STDIO_API_KEY=osk_...` supplies the stdio identity, resolved through
13+
the SAME `@objectstack/core` verify + authorization chain as the HTTP/REST
14+
surfaces; the `record_by_id` resource reads via `ql.find(obj, { where:{id},
15+
context })`, so RLS/FLS/tenant apply exactly as on REST `/data`. Re-resolved
16+
per read, so a revoked/expired key stops working on a live session.
17+
- **Fail-closed** — enabling stdio auto-start (`OS_MCP_STDIO_ENABLED=true` /
18+
`autoStart`) without a resolvable key throws and refuses to start. There is no
19+
unscoped fallback and deliberately no `system` bypass; full authority is a key
20+
minted on a platform-admin or dedicated service identity.
21+
22+
**BREAKING (stdio auto-start only):** previously `OS_MCP_STDIO_ENABLED=true`
23+
(or the plugin `autoStart` option) started stdio with full, unscoped authority
24+
and no credential. It now requires `OS_MCP_STDIO_API_KEY`; without it, boot
25+
fails closed. The default-on HTTP surface and any deployment that never enables
26+
stdio auto-start are unaffected.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/types': minor
3+
'@objectstack/mcp': minor
4+
'@objectstack/cli': minor
5+
'create-objectstack': patch
6+
---
7+
8+
feat(mcp): decouple the stdio auto-start switch from the HTTP surface + surface the MCP endpoint on `os dev` boot (#3167)
9+
10+
The MCP HTTP surface (`/api/v1/mcp`) and the long-lived stdio transport used to
11+
share one env var: `OS_MCP_SERVER_ENABLED=true` turned the HTTP surface on **and**
12+
silently auto-started the stdio transport — which bridges the raw metadata service
13+
+ data engine with no per-request principal (unscoped). An operator setting it to
14+
"make sure MCP is on" got an unscoped transport as a side effect.
15+
16+
- **`@objectstack/types`** — new `resolveMcpStdioAutoStart()`. Stdio auto-start is
17+
now its own switch, `OS_MCP_STDIO_ENABLED` (default off); `OS_MCP_SERVER_ENABLED`
18+
governs only the HTTP surface. The legacy `OS_MCP_SERVER_ENABLED=true` trigger
19+
still starts stdio for one release, flagged as deprecated. `=false` is unchanged
20+
(it only ever gated HTTP).
21+
- **`@objectstack/mcp`**`MCPServerPlugin.start()` gates stdio on the new switch
22+
and logs a one-time deprecation warning when started via the legacy alias.
23+
- **`@objectstack/cli`**`os dev` now prints the MCP endpoint, the agent-skill
24+
URL, and a ready-to-paste `claude mcp add` command on boot (gated on the HTTP
25+
surface being on), so the "an agent operates the app it's building" loop is
26+
discoverable at dev time.
27+
- **`create-objectstack`** — the blank scaffold README documents that the app is
28+
itself an MCP server (the serve side), distinct from the consume-side connector.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
---
4+
5+
fix(metadata-protocol): unscoped metadata list dedupes package-aware, not by bare name (ADR-0048 #1828)
6+
7+
`getMetaItems` merged registry items, `sys_metadata` overlay rows, draft-preview
8+
rows, and MetadataService items into `Map`s keyed by bare `name`, so two installed
9+
packages shipping the same `type/name` (e.g. `page/home`) collapsed to one row
10+
(last-write-wins) on an unscoped `GET /meta/:type` whenever either package had an
11+
overlay — and the frontend prefer-local resolution, which reads that list, could
12+
no longer tell the two packages' rows apart.
13+
14+
The three merge sites (plus the env/org pre-merge) now key by `(package, name)`,
15+
mirroring `getMetaItem`'s scoped-then-global-fallback resolution: colliding rows
16+
stay distinct each with its own `_packageId`, a package-less (env-wide) overlay
17+
still wins over the single artifact it customizes (ADR-0005 precedence and
18+
single-package behaviour unchanged), and the registry-hydration artifact graft is
19+
scoped to each row's own `package_id` so a collision no longer mislabels provenance.

0 commit comments

Comments
 (0)