Skip to content

Commit 49700e4

Browse files
docs(skills): objectstack-api stops teaching an app to mount routes on http.server (#23787) (#23807)
Fixes #23787 Clause-②: no ## What changed Ruling C′ item 2 on #23128 (record 6142279866, the maintainer's 「同意」) retired the imperative `http.server` mount as an app-author surface: an app declares `apis:`, computes in a flow, and registers code as a stack function or a connector; the `http.server` service itself stays the platform plugins' own door. This PR is the skill half of that item plus the ADR-0088 row the filer added. The spec half is PR #23785 (#23216); none of its files are touched here, and the skill wording follows its retire texts where they say the same thing. - `skills/objectstack-api/SKILL.md` - Table "Choosing between `apis:` and a code handler": the `http.server mount (plugin code)` row is replaced by **A flow that runs your 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 `apis:` row is unchanged. - The paragraph under the table now names the inbound `type: 'api'` flow hook (a pointer to objectstack-automation → Inbound webhook (`api`) triggers, which already teaches `hookId` / `secret`), states that an app never mounts on `http.server` (same reasons as PR #23785's retire texts: no caller identity, auth gate, rate limit, namespace carve-out or OpenAPI entry), and names the three cases with no app surface today instead of inventing a door. - Section "Code routes: mounting on `http.server`" (26 lines including its leading separator) is deleted, with the deprecated-alias, `kernel:listening` and Hono-sealing notes. - `skills/objectstack-api/evals/endpoints-auth-routes.json`, eval id 3. The Stripe prompt is **kept** (it is the realistic lure for the retired door) and the eval now expects the skill's statement of the gap plus what holds for code: `expected_output` rewritten; `must_contain` = `x-objectstack-signature`, `functions`; `must_not_contain` = `http.server`, `kernel:ready`, `getService`. Chosen over re-targeting at a served case because the eval's job is to catch an authoring AI reaching for the retired door on exactly this prompt; evals 1 and 2 already cover the served `apis:` cases. - `docs/adr/0088-metadata-kind-admission-and-retirement.md`: one `**Amended**` entry on the Status line (2026-10-16, #23787, citing #23128 ruling C′ item 2, record 6142279866), and the `router` row points at it. The `router` KIND's retirement is unchanged; the amendment narrows only the delivered form "imperative `http.server` mounts" to the platform plugins. No other ADR edit. - `.changeset/23787-api-skill-retired-route-door.md`: `@objectstack/skills` patch with `Clause-②: no`. Measured: `skills/**` ships through `packages/skills` (`files[]` = `dist`, `README.md`, `CHANGELOG.md`; `scripts/sync-catalog.mjs` copies `skills/**` into `dist/skills/`), so the text edit publishes. `docs/adr/**` ships in no package's `files[]` (every tracked `package.json` scanned at b485122: zero `adr`/`docs` entries), so the ADR edit owes nothing. ## Measurements behind the new text (base b485122) - `apis:` types: `packages/spec/src/api/endpoint.zod.ts:174` — only `object_operation` and `flow` execute; `script` / `proxy` are refused at publish (`endpoint-publish-gate.ts:370-371`). The describe text already sends script logic to "a flow whose script node runs your registered function" and outbound calls to "a flow using a declared connector". - Stack function from a flow: the `script` node's `config.function` resolves through `engine.resolveFunction()` to `defineStack({ functions })` (`packages/services/service-automation/src/builtin/screen-nodes.ts:31-33, 409-419`; `engine.ts:4266-4275`; `packages/spec/src/stack.zod.ts:992-1047`). A `type: 'flow'` endpoint runs that flow (`packages/runtime/src/endpoint-executor.ts:253-262, 484-585`). - Connectors: all five at this base (`connector-imap`, `-mcp`, `-openapi`, `-rest`, `-slack`) are outbound only — they `registerConnector` / `registerConnectorProvider` with the automation service so a flow's `connector_action` node can dispatch to them; imap is a read-only session that registers no `connector_action` handler. Zero hits for `http.server`, `IHttpServer` or `.post(` under `packages/connectors/*/src`. - Inbound hook (PM reading b4, confirmed): `packages/triggers/trigger-api/src/api-trigger.ts` arms `POST /api/v1/automation/hooks/:flowName/:hookId` for a `type: 'api'` flow; the start node's `config.secret` is required (`engine.ts:10807-10825`); it verifies only ObjectStack's `x-objectstack-signature` in the timestamped form through core's `verifyHttpSignature` (`api-trigger.ts:142-170, 346-348`), requires a JSON object body (400 otherwise) and answers 202 queued. It is app-authorable (a flow is app metadata; `skills/objectstack-automation/SKILL.md:340-354` teaches it), so the skill names it — for a sender that signs with ObjectStack's own scheme. - Streaming: `endpoint-executor.ts` answers `{ status, body: { success: true, data } }` for both executing types (`:291`, `:567-585`); there is no streaming path. - Real-app precedent: `examples/app-showcase` mounts no route handler of its own; its recalc is a declared flow endpoint (`src/coverage.ts:133`, `objectstack.config.ts:259`). ## Three cases with no app surface today — open questions for the maintainer The seat puts these on the card; they are stated in the skill as gaps, not routed to the retired door. 1. A third-party callback verified by its own signature over the raw body (the eval's Stripe case): trigger-api answers 401 `INVALID_SIGNATURE` to any post without a valid `x-objectstack-signature` (`api-trigger.ts:321-348`), and a flow — whether started by the hook or by a `type: 'flow'` endpoint — receives the parsed JSON body as its trigger record, never the raw bytes or the request headers (`api-trigger.ts:357-366`; `endpoint-executor.ts:567` passes `ctx.body` to `buildAutomationContext`). A stack function therefore cannot verify `Stripe-Signature`. 2. A streaming response: declared endpoints answer one JSON envelope (`endpoint-executor.ts:291, 585`). 3. A protocol the platform does not speak: no declared surface exists by definition, and `http.server` is an HTTP door in any case. ## `skills/**` readings | File | Lines before → after | Tokens before → after (ceiling) | |:--|:--|:--| | `skills/objectstack-api/SKILL.md` | 434 → 415 (−19) | 4754 → 4642 (6314; headroom 1560 → 1672) | | `skills/objectstack-api/evals/endpoints-auth-routes.json` | 55 → 55 (0) | 1042 → 1039 (1042; headroom 0 → 3) | | Whole package (sum of every `skills/*/SKILL.md`) | 4408 → 4389 (−19) | bundle total (whole shipped tree) 155016 → 154901 | `node scripts/check-skills-token-ratchet.mjs` verdict lines: - before, at b485122: `✓ check-skills-token-ratchet: skills/objectstack-api/SKILL.md is 4754 tokens (ceiling 6314; headroom 1560).` · `✓ check-skills-token-ratchet: skills/objectstack-api/evals/endpoints-auth-routes.json is 1042 tokens (ceiling 1042; headroom 0).` - after, at b4ea190: `✓ check-skills-token-ratchet: skills/objectstack-api/SKILL.md is 4642 tokens (ceiling 6314; headroom 1672).` · `✓ check-skills-token-ratchet: skills/objectstack-api/evals/endpoints-auth-routes.json is 1039 tokens (ceiling 1042; headroom 3).` No `CEILINGS` row moves. The eval's growth is paid inside the file by content, not re-wrapping (its line count is unchanged; the byte count fell 4165 → 4156). ## Verification (local, head b4ea190) Derived after the final commit with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths): 49 commands — the PM's 37 at 46ca793 plus 12 changeset-family gates the new `.changeset/*.md` pulls in. Every command was run with its exit code captured before any pipe and reconciled: `dispatch-gates --ran` → "49 derived famil(ies) accounted for — 45 run, 4 NOT-MEASURED (4 DERIVED from a recorded exit 3)", exit 0, 0 UNRUN. - 45 × exit 0, among them `check-skills-token-ratchet` (+ `--self-test`), `check:skill-docs`, `check:adr-anchors`, `check-adr-links` (+ `--self-test`), `check-adr-symbol-anchors`, `check:skill-identifier-liveness`, `check:skill-frame-sync`, `check:skill-compatibility`, `check:role-word`, `check:doc-authoring`, `check:pm-skill-id-lint`, `check:nul-bytes`, `check:published-files`, `check-empty-changeset`, `check-changeset-no-major`, `check-adr-0087-registration`, and `check:doc-formula-expressions` (first exit 3 for an unbuilt `@objectstack/lint`; rebuilt with `pnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lint --concurrency=2` under `os-verify-lock.sh`, `VERDICT command-exit 0`, 88 s held; rerun exit 0). - 4 NOT MEASURED locally (exit 3, PREREQUISITE NOT MET): `check:dts-closure`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:sourcemap-no-sources-content`. They read the whole repository's `dist/`; a fresh worktree has none, and this diff touches no package, so the whole-repo build is not owed here. They enter the derivation only through ci.yml's Build Core job filter on `skills/**`, and CI measures them after its full build. - No package source changed, so no package test suite or typecheck is owed; no tested behaviour changed, so no ablation. ## Acceptance notes - Observation, not filed (class none; carrier: none): `skills/objectstack-platform/rules/plugin-lifecycle.md:134` reads `ctx.getService('http-server')`, the deprecated alias; the api-skill text deleted here recorded that the alias is absent on the provider path that registers no alias. Platform-plugin teaching outside this card's file list. - The remaining `kernel:ready` / `kernel:listening` mentions in `skills/` (objectstack-platform, objectstack-i18n) teach plugin lifecycle to platform-plugin authors, the primitive the ruling leaves untouched. Out of scope. - The spec's describe texts still spell the executing-type statement as "17.x" (`endpoint.zod.ts:174-175`, `openapi-endpoints.ts:218`); the skill keeps that spelling at its L154 for consistency with the spec. Not this card. ## 维护者速读(草稿) **改了什么**:`objectstack-api` 技能不再教应用作者把路由挂到 `http.server` 上。决策表的「需要代码」一行改为:声明 `type: 'flow'` 端点,代码以 stack function(`defineStack({ functions })`)由 flow 的 `script` 节点运行,或以 connector 由 `connector_action` 节点调用;新增一句指向 `type: 'api'` 入站 webhook;整节「Code routes」删除。eval 3 改为期待这套说法。ADR-0088 状态行加一条 Amended,`router` 行指向它。附 `@objectstack/skills` patch changeset。 **为什么改**:#23128 裁决 C′ 第 2 条(记录 6142279866)已把 `http.server` 的命令式挂载从应用作者面退役;发布的技能文本和它的 eval 仍在教这扇门,写元数据的 AI 会照表执行。spec 半边在 PR #23785,本 PR 是技能半边加 ADR 行。 **风险与代价(含回滚)**:只改文本,运行时零变化;技能行数与 token 都下降(SKILL.md 434→415 行、4754→4642 tokens;eval 1042→1039 tokens,天花板不动)。风险在于三类场景(第三方自带签名的回调、流式响应、平台不说的协议)今天确实没有应用可声明的落点,技能如实写明「没有」而不发明门;若维护者另有裁定,只需改那一句。回滚 = revert 本 PR 一个 commit。 **席位意见**:(留空) **你要做的**:审批 Tier H(`skills/**` + `docs/adr/**`);并回答一个问题 —— 上述三类无落点场景(尤其 Stripe 这类自带签名、需原始请求体的回调)是否要给应用一个声明式入口,还是维持「平台插件自建门」的现状。 --- _Generated by [Claude Code](https://claude.ai/code/session_01VqGhCg9wdsBXPz54uAEWg7)_ Co-authored-by: objectstack-fleet[bot] <objectstack-fleet[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent f7a5ebe commit 49700e4

4 files changed

Lines changed: 26 additions & 36 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
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)
6+
7+
Clause-②: no
8+
9+
`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.

‎docs/adr/0088-metadata-kind-admission-and-retirement.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ADR-0088: Metadata-kind admission test; retire `trigger`, `router`, `function`, `service`
22

3-
**Status**: Accepted (2026-07-05)
3+
**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)
44
**Deciders**: ObjectStack Protocol Architects
55
**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)
66
**Closes**: framework#2613
@@ -37,7 +37,7 @@ Their real, consumed forms already live elsewhere, and all are **code contributi
3737

3838
| Retired kind | Delivered form |
3939
|---|---|
40-
| `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. |
40+
| `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. |
4141
| `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. |
4242
| `service` | the plugin/service registry itself (`registerService`) |
4343

‎skills/objectstack-api/SKILL.md‎

Lines changed: 12 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,18 @@ policy layer over an existing pipeline, never a second execution dialect.
116116
| Use | When |
117117
|:---|:---|
118118
| **`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.** |
119-
| **`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`. |
120-
121-
If the logic is "a bit of computation, then a record write", express it as a
122-
**flow** and point a `type: 'flow'` endpoint at it — that keeps the URL
123-
declarative and the logic in the automation surface that already runs it.
119+
| **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. |
120+
121+
An inbound webhook from a sender you control is a `type: 'api'` flow
122+
(objectstack-automation → **Inbound webhook (`api`) triggers**): it verifies
123+
ObjectStack's own `x-objectstack-signature`, enqueues, and answers `202`.
124+
**An app never mounts on `http.server`** — that service is the platform
125+
plugins' own door-building primitive, and a code-mounted route gets no caller
126+
identity, auth gate, rate limit, namespace carve-out or OpenAPI entry. Three
127+
cases have no app surface today; say so rather than invent one: a third-party
128+
callback verified by its own signature over the raw body (the hook checks only
129+
`x-objectstack-signature`), a streaming response (a declared endpoint answers
130+
one JSON envelope), and a protocol the platform does not speak.
124131

125132
<!-- os:check -->
126133
```typescript
@@ -198,32 +205,6 @@ for `authRequired: false`.
198205

199206
---
200207

201-
## Code routes: mounting on `http.server`
202-
203-
The one code-route pattern the platform ships:
204-
205-
```ts
206-
import type { IHttpServer } from '@objectstack/spec/contracts';
207-
208-
// in a plugin's `start(ctx)`. ONE `try` PER NAME: `getService` is SYNCHRONOUS
209-
// and THROWS on an empty slot, so `a() ?? b()` in one `try` never reaches `b`.
210-
const read = (n: string): IHttpServer | null => {
211-
try { return ctx.getService<IHttpServer>(n); } catch { return null; }
212-
};
213-
ctx.hook('kernel:ready', () => { // NOT later — see below
214-
const http = read('http.server') ?? read('http-server'); // canonical FIRST
215-
http?.post('/api/v1/apps/acme/recalc', (req, res) => { res.status(200).json({}); });
216-
});
217-
```
218-
219-
`http-server` is the deprecated alias and is absent on the provider path that
220-
registers no alias, so an alias-only read mounts nothing there. `listen()` is
221-
deferred to `kernel:listening` so late registration still lands — Hono seals
222-
its matcher on the first matched request and a later `post()` throws. Service
223-
resolution itself is objectstack-platform → `rules/service-registry.md`.
224-
225-
---
226-
227208
## Endpoint Naming Conventions
228209

229210
| Pattern | Use Case | Example |

‎skills/objectstack-api/evals/endpoints-auth-routes.json‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@
2424
{
2525
"id": 3,
2626
"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.",
27-
"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.",
27+
"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 })`).",
2828
"files": [],
2929
"assertions": {
30-
"must_contain": ["http.server", "kernel:ready", "getService"],
31-
"must_not_contain": ["type: 'script'", "type: 'proxy'"]
30+
"must_contain": ["x-objectstack-signature", "functions"],
31+
"must_not_contain": ["http.server", "kernel:ready", "getService"]
3232
}
3333
},
3434
{

0 commit comments

Comments
 (0)