Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/23787-api-skill-retired-route-door.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 2 additions & 2 deletions docs/adr/0088-metadata-kind-admission-and-retirement.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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`) |

Expand Down
43 changes: 12 additions & 31 deletions skills/objectstack-api/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<!-- os:check -->
```typescript
Expand Down Expand Up @@ -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<IHttpServer>(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 |
Expand Down
6 changes: 3 additions & 3 deletions skills/objectstack-api/evals/endpoints-auth-routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
},
{
Expand Down
Loading