Skip to content

Commit 134ee9b

Browse files
os-zhuangclaude
andauthored
docs(audit): the control-plane surface is guarded now — from the other side of the boundary (#3655) (#3707)
* docs(audit): the control-plane surface is guarded now — from the other side of the boundary (#3655) §10's capstone exempts `/api/v1/cloud/*` because nothing in this repo serves those routes, and the comment said only "tracked separately (#3655)". The ledger now exists, in `cloud` — `packages/service-cloud/src/cloud-route-ledger.ts`, 90 routes with reviewed dispositions, plus a client half that drives THIS SDK with a recording fetch and matches every `projects.*` URL against it. It had to live there: cloud depends on this repo, never the reverse, so it is the only place the mounted route set and `@objectstack/client` are both in scope. The exemption stays — this suite still cannot see those routes — but it is now a statement about where the coverage lives, not that there is none. That guard immediately found `projects.listTemplates` building `/api/v1/cloud/templates`, which no registrar in either repo mounts (#3702): the sixth instance of the #3584 / #3611 / #3636 class, and the first one only a cross-repo guard could have seen. Adds §13 with the disposition counts, both findings, and the one consequence of pinning: cloud asserts against `.objectstack-sha`, so a `projects.*` change here is not verified against the control plane until that pin moves. Co-Authored-By: Claude <noreply@anthropic.com> * chore(changeset): empty changeset — #3707 is comments and docs only The Check Changeset gate requires every PR to add one; an empty-frontmatter changeset is its own sanctioned "this PR releases nothing" declaration, which is what a comment-and-docs change should say rather than minting a version bump for prose. Same form as .changeset/adr-0104-design-doc-only.md. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a629074 commit 134ee9b

3 files changed

Lines changed: 73 additions & 2 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
---
3+
4+
docs(audit): record that the control-plane surface is guarded from the `cloud` side (#3655) — comments and docs only, releases nothing.

docs/audits/2026-07-dispatcher-client-route-coverage.md

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,59 @@ questions, and the second one needs the contract imported into the assertion —
302302
`BaseResponseSchema.safeParse(body)`, not a hand-copied restatement that drifts
303303
from the schema it claims to check.
304304

305+
## 13. The control plane, from the other side (#3655)
306+
307+
The one surface this repo could never audit. §10's capstone drives every SDK
308+
method and matches its URL against the union of the in-repo ledgers; 196 of
309+
~219 matched, and **every** unmatched one was in `projects.*` — 23 methods on
310+
`/api/v1/cloud/*`, served by the sibling `cloud` repo. The dispatcher here does
311+
not merely lack those routes, it refuses them
312+
(`if (path.includes('/cloud/environments/')) return undefined;`), so no ledger
313+
here could honestly carry a row and the capstone exempts the prefix.
314+
315+
The ledger therefore lives in `cloud`
316+
(`packages/service-cloud/src/cloud-route-ledger.ts`), and it had to: **cloud
317+
depends on this repo, never the reverse**, so it is the only place where the
318+
mounted route set and `@objectstack/client` are both in scope. Same shape as
319+
tranche 3 — 90 routes, each with a reviewed disposition, enumerated for real by
320+
driving all 17 registrars against a capturing mock `IHttpServer`.
321+
322+
The client half is the part that actually closes the boundary. Without it a
323+
route could be renamed there, the ledger updated to match, and all 23 methods
324+
404 with the server-side guard still green — so
325+
`projects-namespace-coverage.test.ts` drives the real SDK with a recording
326+
`fetch` and matches each URL against the ledger. Mutation-checked against
327+
exactly that scenario.
328+
329+
| Disposition | Count |
330+
|---|---|
331+
| `sdk` | 22 |
332+
| `gap` | 14 |
333+
| `mismatch` | 6 |
334+
| `server-only` / `public` | 48 |
335+
336+
Two findings, both pinned by tests rather than prose:
337+
338+
- **`projects.listTemplates` is dead** — it builds `/api/v1/cloud/templates`,
339+
which the string search finds exactly once in each repo: the call itself.
340+
Templates exist as a filtered `sys_package` view, never as a route (#3702).
341+
The sixth instance of the `#3584 / #3611 / #3636` class, and the first one
342+
only a cross-repo guard could have seen.
343+
- **A duplicate route registration**`POST /actions/sys_environment/:actionName`
344+
mounted twice with an identical path, the second commented "legacy alias"
345+
and aliasing nothing (cloud#887).
346+
347+
The six `mismatch` rows are all one operation with two live spellings
348+
(`change-hostname` / `hostname`, `rotate-credential` / `credentials/rotate`,
349+
`install-package` / `packages`, a `PUT`/`POST` hostname twin, two
350+
installation-scoped routes). Each is the spelling the SDK does *not* use, so
351+
none is a live break — but each is a rename waiting to pick the wrong one.
352+
353+
The SDK is pinned by cloud's `.objectstack-sha`, so the contract is asserted
354+
against the framework revision cloud builds and ships against. That is the
355+
right revision to check — but it means a `projects.*` change landing here is
356+
not verified against the control plane until that pin moves.
357+
305358
## Follow-up slicing (proposed)
306359

307360
1. **`client.actions.invoke(...)`** — closes the largest hole (3 routes).
@@ -314,7 +367,8 @@ from the schema it claims to check.
314367
8. **REST-surface tranche** (§8) with the same ledger+guard treatment — done in #3587.
315368
9. **Autonomous service mounts** (§9) — done in #3636.
316369
10. **Cross-surface URL conformance** (§10, the reverse direction) — done in #3642.
317-
11. **Control-plane surface** (§10) — #3655, needs a ledger in the `cloud` repo.
370+
11. **Control-plane surface** (§10) — done in #3655; the ledger lives in `cloud`
371+
(§13), which is the only repo where both halves are in scope.
318372
12. **Enumerate `/auth/**`** (§11) — done in #3656; wildcard ratchet 60 → 3.
319373
13. **Enumerate `/ai/**`** — the last dynamic family, 3 SDK methods.
320374
14. **Response-shape conformance** (§12) — error path done in #3675 for both

packages/client/src/client-url-conformance.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,20 @@ function matches(verb: string, path: string): Pattern | undefined {
126126
* not by anything in this one — this repo's dispatcher explicitly REFUSES those
127127
* paths (`http-dispatcher.ts`: "Guard against matching control-plane routes
128128
* like /cloud/environments"). No in-repo ledger can vouch for them, so they are
129-
* exempt here and tracked separately (#3655).
129+
* exempt here.
130+
*
131+
* NO LONGER UNGUARDED — guarded on the OTHER side of the boundary (#3655).
132+
* `cloud`'s `packages/service-cloud/src/cloud-route-ledger.ts` gives all 90
133+
* routes its artifact API plugin mounts a reviewed disposition, and
134+
* `projects-namespace-coverage.test.ts` there drives this very SDK with a
135+
* recording `fetch` and matches every `projects.*` URL against it. That had to
136+
* live in `cloud`: it depends on this repo, never the reverse, so the cloud
137+
* repo is the only place the mounted route set and the SDK are both in scope.
138+
* The exemption below stays because THIS suite still cannot see those routes —
139+
* it is a statement about where the coverage lives, not that there is none.
140+
*
141+
* That guard immediately found `projects.listTemplates` building
142+
* `/api/v1/cloud/templates`, which no registrar in either repo mounts (#3702).
130143
*
131144
* Exempt by PREFIX, and bounded from both ends: the assertions below pin which
132145
* methods are allowed to use it, so the hole cannot quietly widen into a place

0 commit comments

Comments
 (0)