Skip to content

Commit 0600163

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-9767-empty-ledger-green-line
2 parents 71a90d2 + be49304 commit 0600163

234 files changed

Lines changed: 12657 additions & 1744 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: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
"@objectstack/core": minor
3+
---
4+
5+
feat(security): the REST 401 anonymous-deny body carries `code: "UNAUTHENTICATED"` alongside the existing `error` / `message` keys (#9487)
6+
7+
Every other REST error family answers `{ error, code }`, with the machine code
8+
in `code` — the 401 family was the one outlier, answering
9+
`{ error: "UNAUTHENTICATED", message }` with no `code` key at all. A client
10+
keying on `body.code` (the shape the other families teach, and the first read
11+
of `@objectstack/client`'s `err.code`) read `undefined` for every
12+
authentication failure.
13+
14+
`ANONYMOUS_DENY_BODY` now carries `code: "UNAUTHENTICATED"` as well.
15+
**Additive only** (maintainer-ruled): no key is removed or moved — `error`
16+
keeps holding the same code value it always has, so every existing reader
17+
keeps working. The wire effect surfaces through `@objectstack/rest`'s
18+
`enforceAuth`, which writes this constant verbatim on every `/data`, `/meta`
19+
and `/reports` 401. This does not settle ADR-0112 D5 (flat vs nested envelope
20+
convergence); both declared envelope families are unchanged in kind.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
"@objectstack/hono": minor
3+
---
4+
5+
feat(adapters): the hono adapter's two discovery bodies join the response envelope (#9436)
6+
7+
<!-- adr-0087: not-required (no-migration-prescription) Additive wire change:
8+
`GET {prefix}` and `GET {prefix}/discovery` gain `success: true` beside the
9+
existing `data` key — nothing authorable and no key is renamed, retired or
10+
removed, so there is no conversion to register. Every measured reader
11+
(`@objectstack/client` `connect()`'s `body.data || body`, the QA http-adapter's
12+
`'routes' in body` discriminator, objectui's `typeof body.success === 'boolean'
13+
&& 'data' in body` unwrap) resolves the new shape to the same document. -->
14+
15+
`GET {prefix}` and `GET {prefix}/discovery` answered `{ data: <discovery> }`
16+
with no `success` flag — one key short of the declared `BaseResponseSchema`
17+
envelope. They now answer `{ success: true, data: <discovery> }`.
18+
19+
Maintainer ruling on #9436 (2026-08-18, option A), deliberately not inheriting
20+
#9389's pre-auth exemption: these bodies are read by SDKs, codegen and AI
21+
clients — the envelope's core constituency — rather than by our own shells,
22+
and the migration is one key. Readers that unwrapped `body.data` keep working
23+
unchanged; envelope-aware readers that discriminate on `success` now unwrap
24+
this mount correctly.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
"@objectstack/plugin-hono-server": minor
3+
---
4+
5+
fix(hono-server): `http_requests_total` is emitted by the transport, so every inbound mount is counted (#9650)
6+
7+
The counter had exactly one emitter: a `Proxy` the runtime dispatcher built
8+
over its **own** `IHttpServer` handle. It therefore saw only the routes the
9+
dispatcher itself registered — and nothing else on the same server.
10+
11+
Measured, that left at least **14** inbound surfaces uncounted, in two
12+
structurally different classes:
13+
14+
- plugins that mount through `getRawApp()` — auth (`/api/v1/auth/*`),
15+
metadata HMR, cloud-connection, marketplace, runtime-config, trigger-api,
16+
webhooks, approvals, the console SPA. These bypass `IHttpServer` entirely,
17+
so **no** wrapper at that level can ever reach them.
18+
- plugins that resolve `http.server` themselves and mount through the verb
19+
methods — the REST data API via `RouteManager`, storage, i18n, settings,
20+
datasource admin.
21+
22+
The two highest-traffic surfaces an operator actually cares about, auth and
23+
the REST data API, were both in that set. The documented guidance is to alert
24+
on the 5xx rate derived from this counter, so a deployment could be melting
25+
down on `/api/v1/*` with the counter flat.
26+
27+
The counter is now emitted from the Hono adapter itself, as a raw-app
28+
middleware installed at the end of `HonoServerPlugin.init()` beside
29+
`installMiddlewareSeam()` — the one layer every inbound request converges on,
30+
whatever registered the handler.
31+
32+
**The route label is the matched PATTERN, never the concrete path.**
33+
`/api/v1/data/:id`, not one series per record id; `/api/v1/auth/*`, not one
34+
per sign-in endpoint. Cardinality has to stay bounded or the counter is
35+
unusable for the alerting it exists for, and the label is unfixable in place
36+
once dashboards are wired against the first shipped one.
37+
38+
**Wiring.** `HonoServerPlugin` takes a new `observability.metrics` option and
39+
otherwise follows the canonical chain `ObservabilityServicePlugin` documents:
40+
explicit option, then the `observability:metrics` service, then **nothing** —
41+
with no backend configured no middleware is installed at all, so an
42+
unconfigured deployment pays no per-request cost.
43+
44+
**Two consequences, stated rather than left to be discovered:**
45+
46+
- **A transport that does not implement this seam reports no HTTP metrics.**
47+
The seam is Hono's. Another `IHttpServer` implementation emits nothing until
48+
it grows its own, and a zero there means "not instrumented", never "no
49+
traffic". A response-observing hook on the `IHttpServer` contract is the
50+
transport-agnostic successor and is filed separately.
51+
- **A request the `use()` chain refuses is still counted** — the seam is
52+
installed before the middleware seam, so the inbound rate limiter's `429`
53+
appears with `status="429"`. A preflight `OPTIONS` that the transport's own
54+
CORS built-in answers is **not** counted: it short-circuits earlier and
55+
never reaches a route.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/core": patch
4+
"@objectstack/observability": patch
5+
"@objectstack/plugin-hono-server": patch
6+
"@objectstack/runtime": patch
7+
"@objectstack/http-conformance": patch
8+
---
9+
10+
feat(spec): `IHttpServer` gains an optional `afterResponse` response-observing
11+
hook so HTTP metrics are transport-agnostic instead of Hono-only (#9835)
12+
13+
The contract addition (additive — a new optional member plus the
14+
`HttpResponseObservation` / `HttpResponseObserver` types and the reserved
15+
`UNMATCHED_ROUTE_PATTERN` label): a transport invokes each registered observer
16+
exactly once per answered request with `{ method, routePattern, status,
17+
elapsedMs }`, after the response exists — the observation point the `use()`
18+
middleware contract cannot express (it runs before dispatch and never sees a
19+
status). `routePattern` is REQUIRED to be the registered route pattern
20+
(`/api/v1/data/:id`), never the concrete path, so no adapter re-decides metric
21+
cardinality. Optionality is feature-detected runtime-real
22+
(`typeof server.afterResponse === 'function'`); a transport that does not
23+
implement the seam reports **no** HTTP metrics — zero there means "not
24+
instrumented", never "no traffic".
25+
26+
Implementations and consumers in the same change:
27+
28+
- `@objectstack/plugin-hono-server`: `HonoHttpServer` implements the seam (the
29+
ruled #9650 raw-app middleware becomes its delivery path — same reach,
30+
including `getRawApp()` mounts and middleware-refused 429s); unrouted
31+
requests are now labelled with the reserved `unmatched` pattern (previously
32+
they could surface as `/*`).
33+
- `@objectstack/observability`: new `armHttpRequestCounter(server, metrics)`
34+
arms the `http_requests_total` counter through the seam at most once per
35+
server (first caller wins), which is what makes "exactly one counter per
36+
server" structural.
37+
- `@objectstack/runtime`: the dispatcher offers its `observability.metrics`
38+
registry to the seam (a host that wires only the dispatcher now counts every
39+
inbound surface) and suppresses its own per-route copy of
40+
`http_requests_total` when the transport implements the seam — retiring the
41+
#9833 double count. Request-id echo, the duration histogram, the error
42+
counter and the error reporter are unchanged.
43+
- `@objectstack/http-conformance`: `NodeHttpServer` implements the seam, and a
44+
new cross-adapter conformance suite locks the semantics for both adapters.
45+
- `@objectstack/core`: re-exports the new contract types/constant.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
---
2+
"@objectstack/mcp": patch
3+
---
4+
5+
docs(mcp): rewrite the published README to the shipped host-extension surface (#9579)
6+
7+
`packages/mcp/README.md` is in the package's `files` array with `private` unset,
8+
so it is the page npm renders. It told the reader to extend the server
9+
imperatively at six call sites:
10+
11+
```ts
12+
kernel.getService('mcp').registerTool(calculateRevenueTool);
13+
kernel.getService('mcp').registerResource({ … });
14+
kernel.getService('mcp').registerPrompt({ … });
15+
```
16+
17+
`MCPServerRuntime` has never had any of those members. Measured against the
18+
built `dist/index.d.ts`, a consumer who copies those lines gets three
19+
`TS2339 Property … does not exist on type 'MCPServerRuntime'`. The receiver is a
20+
local variable, so `check:published-readme-exports` is structurally blind to
21+
them — both of its halves key on a name the fence *imported*, and this one is
22+
neither imported nor a bare identifier.
23+
24+
Ruled 2026-08-18: **document the shipped surface; do not grow the API to match
25+
the docs.** So the imperative narrative is gone and the page now documents what
26+
actually ships — the bridge methods (`bridgeTools`, `bridgeDataTools`,
27+
`bridgeResources`, `bridgePrompts`), `handleHttpRequest` / `renderSkill`, and the
28+
exported `registerObjectTools` / `registerActionTools` / `registerSkillPrompts`
29+
helpers driving an `McpServer`. Every row is probed against the built type entry
30+
the `exports` map resolves, and the page's one host-extension example compiles
31+
clean against it.
32+
33+
Neighbouring fabrications the audit turned up, all corrected in the same pass —
34+
each of them was reachable only through prose or an unimported receiver, which is
35+
why nothing had read them:
36+
37+
- **A tool family that does not exist.** The page listed
38+
`objectstack_find` / `objectstack_findOne` / `objectstack_create` /
39+
`objectstack_update` / `objectstack_delete` / `objectstack_describeObject` /
40+
`objectstack_listObjects` / `objectstack_listFields` as "auto-registered". No
41+
such tool name occurs anywhere in the repo. The real names are the
42+
`list_objects` … `run_action` set the page listed separately, one section down.
43+
- **`aggregate_records` was missing** from the list that *was* correct, along
44+
with the fact that it registers only when the bridge implements `aggregate`.
45+
- **Resource URIs were wrong in both directions.** The page taught
46+
`objectstack://objects/{name}/records` (no such resource) and
47+
`objectstack://objects/{name}/{id}` (real shape is
48+
`…/{name}/records/{id}`), and omitted `objectstack://objects` and
49+
`objectstack://metadata/types` entirely.
50+
- **The advertised capability block was invented.** It claimed
51+
`tools.listChanged`, `resources.subscribe`, `resources.listChanged`,
52+
`prompts.listChanged` and `experimental.streaming`. The server hand-declares
53+
only `logging`; everything else is *derived* from what was actually registered,
54+
which is the ADR-0076 D12 contract the README was contradicting. The
55+
"Streaming Support" feature bullet and the streaming-resource example went with
56+
it — neither names anything that ships.
57+
- **The stdio transport could not be started by following the page.** Neither
58+
`OS_MCP_STDIO_ENABLED` nor `OS_MCP_STDIO_API_KEY` was documented, and stdio
59+
auto-start refuses to boot without the key (ADR-0101, fail-closed). The three
60+
client config blocks now carry both. The Debugging section also taught
61+
`OS_MCP_SERVER_ENABLED=true` as the stdio switch, which is the deprecated path
62+
that logs a warning.
63+
- **A broken relative link.** `../../spec/src/ai/` resolves above the repo root
64+
from `packages/mcp/`; the target is `../spec/src/ai/`.
65+
66+
Docs only — no runtime code changed, and no API was added. `registerTool` /
67+
`registerResource` / `registerPrompt` remain unbuilt by ruling; a future
68+
imperative API is its own card on measured pull.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
"@objectstack/metadata": patch
3+
---
4+
5+
fix(metadata): `MetadataPlugin`'s `watch` option defaults to `false`, as its own doc comment documents (#9770)
6+
7+
`MetadataPluginOptions.watch` documents its default as ``Default: `false` (post PR-10e —
8+
was previously `true`)``, directly above the field. The constructor implemented the
9+
opposite, and it did so in **two** places, so both entry shapes resolved `true`:
10+
11+
- the options literal `{ watch: true, ...options }` — covering a caller who **omits** the key;
12+
- the fallback `this.options.watch ?? true` — covering a caller who passes an explicit
13+
`undefined`.
14+
15+
Both non-test construction sites in this repo pass `watch: false` explicitly and are
16+
unaffected either way, which is exactly why the drift was invisible to every test and
17+
gate: no in-repo configuration exercised the default. `MetadataPlugin` is a public export
18+
(`@objectstack/metadata`, `@objectstack/metadata/node`), so the consumers who did reach it
19+
were **external** ones — and they reached it by doing the documented-safe thing and not
20+
naming the key at all. What they got was the configuration both internal call sites go out
21+
of their way to refuse, citing an **EMFILE** hazard at both: a recursive chokidar poll
22+
(`usePolling: true, interval: 1000`) over the entire project root, with `node_modules`
23+
excluded only by chokidar's default `ignored`.
24+
25+
The default now resolves `false`. The flag is normalized once in the constructor
26+
(`watch: options.watch ?? false`) rather than spelled `{ watch: false, ...options }`,
27+
because a spread preserves an explicitly-passed `undefined` verbatim and not every read of
28+
the flag routes through a nullish fallback — the `start()`-time `FileSystemRepository`
29+
`disableWatch` keys on `=== false`. Coercing once makes an omitted key and an explicit
30+
`undefined` resolve identically at every downstream read, instead of trading one
31+
two-spelling divergence for another.
32+
33+
This is a default flip, **not** a capability removal: an explicit `watch: true` still
34+
attaches the scanner and its watcher, and the sealed-runtime carve-out
35+
(`bootstrap: 'artifact-only'` forces watching off even against an explicit `watch: true`)
36+
is untouched. Pins cover all four shapes, asserting on the **observable** — whether a
37+
watcher object exists on the manager — rather than on the resolved options value alone.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
"@objectstack/lint": minor
3+
---
4+
5+
feat(lint): the five gating object rules cross the runtime publish gate — `object` writes are now judged by `validateFunctionalCompleteness`, `validateManagedApiMethods`, `lintAutonumberFormats`, `validateRuleCompilability` and `validateRuleSchemaFormats` (#4716)
6+
7+
An `active`-state `object` save through `saveMetaItem` (Studio's field editor,
8+
REST `/meta` item CRUD, an MCP/AI author) is now refused with the existing 422
9+
`invalid_metadata` envelope when it carries a defect these five rules judge:
10+
an inert `summary`/`lookup`/`select` shape, a managed-API verb the object's own
11+
affordances refuse, an autonumber format referencing an unknown field, a
12+
`format` regex or `json_schema` schema the runtime's own compilers reject, or a
13+
`json_schema` `format` name ajv would silently drop. All five already gated
14+
`os validate` / `os build` / `os lint`; the runtime door — the only door a
15+
tenant overlay row has — ran none of them.
16+
17+
Scope is deliberately the five **gating** rules only (the #4716 adjudication):
18+
the six advisory-tier object rules stay off the runtime surface, so a clean
19+
save's response is byte-identical and no new advisory volume reaches Studio's
20+
designer. Draft saves are untouched (D1), stored rows keep being served
21+
(ADR-0087 asymmetry — the gate's differential blames a write only for what it
22+
adds), and `OS_ALLOW_UNLINTED_METADATA_WRITES=1` still degrades the refusal to
23+
a loud log for migration windows.
24+
25+
Boot-path note: the two schema-judging rules load ajv lazily, only when the
26+
judged snapshot actually carries a `json_schema` validation — an ordinary
27+
field edit still loads no compiler, which `runtime-lazy-deps.test.ts` now pins
28+
as a three-tier contract (parsers never; ajv never without a schema; ajv
29+
required, on demand, when one is present).
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@objectstack/service-storage': patch
3+
'@objectstack/plugin-audit': patch
4+
---
5+
6+
Attachment access hooks: read the caller's org under the blessed `organizationId` name
7+
8+
`callerContext()` in the `sys_attachment` access kit built its fallback
9+
execution envelope from `session.tenantId` — an alias removed from the
10+
hook/action session surface in v11 (#3290). `HookContextSchema` strips a
11+
`tenantId` key and the engine's `buildSession` only ever emits
12+
`organizationId`, so on every call that reached the session fallback (no
13+
execution context riding along) the envelope handed to
14+
`ISharingService.canEdit` carried **no organization at all**. Parent-record
15+
access for attachments was therefore evaluated without the caller's active
16+
org on that path. It now reads `session.organizationId`, matching the
17+
`sys_comment` kit, which already did.
18+
19+
The `sys_comment` kit's own `callerContext()` had the same read as a dead
20+
first arm (`s.tenantId ?? s.organizationId`); the arm is removed. That half
21+
is behaviour-neutral — the fallback already carried the value.
22+
23+
Both kits gain coverage of the session-fallback path in both directions: the
24+
blessed name is read, and a stray removed-alias key does not become the org.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
"@objectstack/objectql": patch
3+
---
4+
5+
fix(objectql): a failed `sys_organization` probe stops reading as "this install has no organizations" (#9261)
6+
7+
`probeInstallOrganizations` — the read the #8844 system-write organization
8+
resolution decides on — sat behind a bare `} catch { ids = [] }`. Every failure
9+
answered with the count that means *none*, and `resolveSystemWriteOrganization`
10+
maps 0 / 1 / 2+ organizations to *proceed unstamped* / *stamp the derived id* /
11+
*refuse*. So one transient probe failure silently skipped **both** halves of the
12+
2026-08-15 ruling:
13+
14+
- on a `single`-posture install that really has one organization, system-context
15+
inserts (a hook, a cron tick, a `runAs: system` flow) landed **unstamped** —
16+
filing the row under the `__global__` pseudo-tenant and forking exactly the
17+
per-organization autonumber counter and partitioned unique index the ruling
18+
exists to protect;
19+
- on a multi-organization install, the refusal the ruling mandates
20+
(`ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED`) **never fired** — fail-open on a
21+
guard that must be loud.
22+
23+
Aggravated by the memo: the invented answer was cached in
24+
`organizationProbeMemo`, so the outage's consequence outlived the outage — every
25+
later system write inherited "no organizations" until an organization write
26+
happened to clear it.
27+
28+
The probe now discriminates by error TYPE, the disposition ADR-0110 D3 requires
29+
("the probe found nothing" and "the probe could not run" are different facts):
30+
31+
- **benign** — `sys_organization` routes but its table was never provisioned
32+
(schema sync not run yet). It cannot hold a row, so zero really is the count,
33+
and first boot still proceeds unstamped. Asked through the shared
34+
`isMissingTableError` predicate (`@objectstack/metadata/errors`), the same call
35+
the file's sibling read seams make, never a hand-rolled code test.
36+
- **everything else** — connection loss, pool exhaustion, a timeout mid-boot, a
37+
datasource that never connected, a permission denial — propagates with its
38+
envelope intact, and is **not memoised**. The write that asked fails loudly
39+
instead of being filed under a guessed topology, and the next write re-probes
40+
rather than inheriting the guess. No new error code and no new response field.
41+
42+
Measured rather than assumed: the old comment's "`sys_organization` may not be
43+
registered at all (a lean embedding, a bare-kernel test)" is not a second benign
44+
cause. An object missing from the registry does not fail the read at all on a
45+
driver that tolerates an unknown table (`find` returns `[]` through the normal
46+
path), a strict driver surfaces it as the missing table above, and an engine
47+
with no driver fails the write on its own object one frame before the probe runs.

0 commit comments

Comments
 (0)