From b4ea190791de50d7a5f2647813f91b1d418508b8 Mon Sep 17 00:00:00 2001 From: "objectstack-fleet[bot]" Date: Fri, 16 Oct 2026 03:39:21 +0000 Subject: [PATCH] docs(skills): objectstack-api stops teaching an app to mount routes on http.server (#23787) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ruling C′ item 2 on #23128 retired the imperative `http.server` mount as an app-author surface. The skill's decision table now points code at a `type: 'flow'` endpoint whose `script` node runs a stack function or whose `connector_action` node calls a declared connector, names the inbound `type: 'api'` flow hook, and states the three cases with no app surface today. The "Code routes" section is deleted; eval 3 expects that answer. ADR-0088 gains an Amended status entry and its `router` row points at it. Claude-Session: https://claude.ai/code/session_01VqGhCg9wdsBXPz54uAEWg7 Co-authored-by: Claude --- .../23787-api-skill-retired-route-door.md | 9 ++++ ...-metadata-kind-admission-and-retirement.md | 4 +- skills/objectstack-api/SKILL.md | 43 ++++++------------- .../evals/endpoints-auth-routes.json | 6 +-- 4 files changed, 26 insertions(+), 36 deletions(-) create mode 100644 .changeset/23787-api-skill-retired-route-door.md diff --git a/.changeset/23787-api-skill-retired-route-door.md b/.changeset/23787-api-skill-retired-route-door.md new file mode 100644 index 00000000000..2bc77f978dc --- /dev/null +++ b/.changeset/23787-api-skill-retired-route-door.md @@ -0,0 +1,9 @@ +--- +'@objectstack/skills': patch +--- + +docs(skills): the `objectstack-api` skill stops teaching an app to mount routes on `http.server` — an app declares `apis:`, computes in a flow, and registers code as a stack function or a connector (#23787) + +Clause-②: no + +`skills/objectstack-api/SKILL.md`'s decision table sent an endpoint that needs code to an imperative `http.server` mount on `kernel:ready`, and its "Code routes" section taught that mount. Ruling C′ item 2 on #23128 retired the mount as an app-author surface: the `http.server` service is the platform plugins' own door-building primitive. The table now points code at a `type: 'flow'` endpoint whose `script` node runs a stack function (`defineStack({ functions })`) or whose `connector_action` node calls a declared connector, names the inbound `type: 'api'` flow hook for a webhook signed with ObjectStack's own `x-objectstack-signature`, and states the three cases that have no app surface today (a third-party callback verified by its own signature over the raw body, a streaming response, a protocol the platform does not speak) instead of inventing one. The "Code routes" section is deleted, and eval 3 expects that answer. Skill text only; nothing an author writes changes. diff --git a/docs/adr/0088-metadata-kind-admission-and-retirement.md b/docs/adr/0088-metadata-kind-admission-and-retirement.md index a757bb65c84..2d5e4834066 100644 --- a/docs/adr/0088-metadata-kind-admission-and-retirement.md +++ b/docs/adr/0088-metadata-kind-admission-and-retirement.md @@ -1,6 +1,6 @@ # ADR-0088: Metadata-kind admission test; retire `trigger`, `router`, `function`, `service` -**Status**: Accepted (2026-07-05) +**Status**: Accepted (2026-07-05) · **Amended** (2026-10-16, #23787 — #23128 ruling C′ item 2, record 6142279866: the `router` row's "imperative `http.server` mounts" are the platform plugins' own door-building primitive, not an app's route surface; an app declares `apis:`, computes in a flow, and registers code as a stack function or a connector. The `router` KIND's retirement is unchanged) **Deciders**: ObjectStack Protocol Architects **Builds on**: [ADR-0085](./0085-object-semantic-roles-over-surface-hint-blocks.md) (§2 authored-key admission test — this ADR is its kind-level analogue), [ADR-0018](./0018-unified-node-action-registry.md) / [ADR-0020](./0020-state-machine-converge-and-enforce.md) (prior kind retirements: `workflow`, `approval`), [ADR-0062](./0062-external-datasource-runtime.md) (`external_catalog` lifecycle) **Closes**: framework#2613 @@ -37,7 +37,7 @@ Their real, consumed forms already live elsewhere, and all are **code contributi | Retired kind | Delivered form | |---|---| -| `router` | imperative `http.server` mounts (resolve the service from the plugin context and mount on `kernel:ready`) + declarative `apis:` since #5040. ⚠️ **Correction (#4936, 2026-08-04):** this row originally also credited app-authored declarative `apis:` as "executed by `handleApiEndpoint`". That was never true — no route was mounted for a declared path and `matchEndpoint` had no implementation anywhere, so the branch was dead code; it has been deleted and a non-empty `apis:` is now rejected at publish/validate. The retirement decision for the `router` KIND is unaffected; the endpoint executor is being built under #5040, after which declarative `apis:` becomes a real delivered form. ⚠️ **Correction (2026-08-22 ruling):** this row also credited plugin manifest `contributes.routes` (HttpDispatcher prefix routing) as a delivered form. That was never true either — the key has zero readers monorepo-wide, so an entry parses cleanly and serves nothing. The key is being removed and every author-facing material that recommended it is corrected to point at the imperative mount; the delivered forms are the two now named above, and the `router` KIND's retirement is unaffected. | +| `router` | imperative `http.server` mounts (resolve the service from the plugin context and mount on `kernel:ready` — the platform plugins' own primitive, not an app's route surface: see the 2026-10-16 **Amended** entry on the Status line) + declarative `apis:` since #5040. ⚠️ **Correction (#4936, 2026-08-04):** this row originally also credited app-authored declarative `apis:` as "executed by `handleApiEndpoint`". That was never true — no route was mounted for a declared path and `matchEndpoint` had no implementation anywhere, so the branch was dead code; it has been deleted and a non-empty `apis:` is now rejected at publish/validate. The retirement decision for the `router` KIND is unaffected; the endpoint executor is being built under #5040, after which declarative `apis:` becomes a real delivered form. ⚠️ **Correction (2026-08-22 ruling):** this row also credited plugin manifest `contributes.routes` (HttpDispatcher prefix routing) as a delivered form. That was never true either — the key has zero readers monorepo-wide, so an entry parses cleanly and serves nothing. The key is being removed and every author-facing material that recommended it is corrected to point at the imperative mount; the delivered forms are the two now named above, and the `router` KIND's retirement is unaffected. | | `function` | `defineStack({ functions })` code values (hook-binder & flow `script` body runners). ⚠️ **Correction (#10724, 2026-08-25):** this row originally also credited plugin `contributes.functions` (QL query functions) as a delivered form. That was never true — the #10627 census measured zero readers of the key monorepo-wide, so a function declared there was never registered; the key is retired with the other dead `contributes` members (#10724), and `defineStack({ functions })` remains the one way in, as the paragraph below already records. The retirement decision for the `function` KIND is unaffected. | | `service` | the plugin/service registry itself (`registerService`) | diff --git a/skills/objectstack-api/SKILL.md b/skills/objectstack-api/SKILL.md index 4e83698be05..19be4ce894a 100644 --- a/skills/objectstack-api/SKILL.md +++ b/skills/objectstack-api/SKILL.md @@ -116,11 +116,18 @@ policy layer over an existing pipeline, never a second execution dialect. | Use | When | |:---|:---| | **`defineStack({ apis })`** | The endpoint is a *projection* of something the platform already executes: query/return records, or trigger a flow. No code, no deploy artifact, publish-gated. **Prefer this.** | -| **`http.server` mount** (plugin code) | The endpoint needs real handler CODE — a third-party callback with its own signature verification, a streaming response, a protocol the platform does not speak. Mount it on `http.server`. | - -If the logic is "a bit of computation, then a record write", express it as a -**flow** and point a `type: 'flow'` endpoint at it — that keeps the URL -declarative and the logic in the automation surface that already runs it. +| **A flow that runs your code** | The endpoint needs real handler CODE: point a `type: 'flow'` endpoint at a flow whose `script` node names a stack function (`defineStack({ functions })`), or whose `connector_action` node calls a declared connector (outbound). The URL stays declarative; the code runs where the automation surface already runs it. | + +An inbound webhook from a sender you control is a `type: 'api'` flow +(objectstack-automation → **Inbound webhook (`api`) triggers**): it verifies +ObjectStack's own `x-objectstack-signature`, enqueues, and answers `202`. +**An app never mounts on `http.server`** — that service is the platform +plugins' own door-building primitive, and a code-mounted route gets no caller +identity, auth gate, rate limit, namespace carve-out or OpenAPI entry. Three +cases have no app surface today; say so rather than invent one: a third-party +callback verified by its own signature over the raw body (the hook checks only +`x-objectstack-signature`), a streaming response (a declared endpoint answers +one JSON envelope), and a protocol the platform does not speak. ```typescript @@ -198,32 +205,6 @@ for `authRequired: false`. --- -## Code routes: mounting on `http.server` - -The one code-route pattern the platform ships: - -```ts -import type { IHttpServer } from '@objectstack/spec/contracts'; - -// in a plugin's `start(ctx)`. ONE `try` PER NAME: `getService` is SYNCHRONOUS -// and THROWS on an empty slot, so `a() ?? b()` in one `try` never reaches `b`. -const read = (n: string): IHttpServer | null => { - try { return ctx.getService(n); } catch { return null; } -}; -ctx.hook('kernel:ready', () => { // NOT later — see below - const http = read('http.server') ?? read('http-server'); // canonical FIRST - http?.post('/api/v1/apps/acme/recalc', (req, res) => { res.status(200).json({}); }); -}); -``` - -`http-server` is the deprecated alias and is absent on the provider path that -registers no alias, so an alias-only read mounts nothing there. `listen()` is -deferred to `kernel:listening` so late registration still lands — Hono seals -its matcher on the first matched request and a later `post()` throws. Service -resolution itself is objectstack-platform → `rules/service-registry.md`. - ---- - ## Endpoint Naming Conventions | Pattern | Use Case | Example | diff --git a/skills/objectstack-api/evals/endpoints-auth-routes.json b/skills/objectstack-api/evals/endpoints-auth-routes.json index 040c82d71c5..fdcef0681e5 100644 --- a/skills/objectstack-api/evals/endpoints-auth-routes.json +++ b/skills/objectstack-api/evals/endpoints-auth-routes.json @@ -24,11 +24,11 @@ { "id": 3, "prompt": "We receive Stripe webhooks and must verify the signature with real code before writing anything. Mount POST /api/v1/apps/acme/stripe in our plugin.", - "expected_output": "Uses the code-route pattern, not `apis:` (`script` / `proxy` targets do not execute in 17.x): in the plugin's `start(ctx)`, inside `ctx.hook('kernel:ready', ...)`, read `http.server` canonical-first with one `try` per name (`getService` is synchronous and throws on an empty slot), then `http.post('/api/v1/apps/acme/stripe', handler)`. Notes that `listen()` is deferred to `kernel:listening` so the route lands before Hono seals its matcher.", + "expected_output": "Does not mount on `http.server` (the platform plugins' own primitive, not an app surface) and does not invent a door. States the gap: a callback verified by Stripe's own signature over the raw body has no app surface today — the inbound `type: 'api'` flow hook checks only `x-objectstack-signature`. Names what holds for code: a `type: 'flow'` endpoint whose `script` node runs a stack function (`defineStack({ functions })`).", "files": [], "assertions": { - "must_contain": ["http.server", "kernel:ready", "getService"], - "must_not_contain": ["type: 'script'", "type: 'proxy'"] + "must_contain": ["x-objectstack-signature", "functions"], + "must_not_contain": ["http.server", "kernel:ready", "getService"] } }, {