From aa2bac835cc6d7ea7cd5d0202123ba23e7c9548f Mon Sep 17 00:00:00 2001 From: Aaron Trowbridge Date: Sat, 17 Oct 2026 06:10:01 -0400 Subject: [PATCH] fix(extension): the app-called fleet routes answer the documented no-fleet-peers absence (#1812) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend #1768's session-status discipline to the other fleet routes the app calls: /amicode/fleet/sessions (+ ?scope= / ?archived=), grants, creation-target, and peer-home-base only mounted WITH a staged fleet topology, so a no-topology machine (the unarmed hub) served them the generic no-route 404 the panel render gate refuses. createAmicodeService now pre-mounts the documented 404 {ok:false, reason:"no-fleet-peers"} — the exact body session-status serves — for those four routes on EVERY boot, BEFORE the fleet block; whichever staging branch arms re-adds the same route keys with the REAL handlers (server.add is last-wins), so the fleet-data paths are untouched and a topology-bearing boot is byte-identical. /amicode/fleet/status (not app-called) keeps the #391 base no-route 404, preserving the H3 discipline where it still holds. The two fleetless-boot assertions that pinned the old generic shape (sessions) are updated to the new absence contract — the byte-identity invariants are unchanged and still pass: base vs gated, and observation-only-zero-peers vs base, both answer the identical absence. Extension suite: 329 files, 5426 passed, 53 skipped, 0 failed. --- .../extension/src/amicode_service/index.ts | 35 +- .../amicode_service_fleet_absence.test.ts | 302 ++++++++++++++++++ .../amicode_service_fleet_data_plane.test.ts | 30 +- 3 files changed, 355 insertions(+), 12 deletions(-) create mode 100644 packages/extension/test/amicode_service_fleet_absence.test.ts diff --git a/packages/extension/src/amicode_service/index.ts b/packages/extension/src/amicode_service/index.ts index 3e02f2134..ba28abd99 100644 --- a/packages/extension/src/amicode_service/index.ts +++ b/packages/extension/src/amicode_service/index.ts @@ -425,12 +425,18 @@ export function registerModelRoutingRoutes( } // ── #391: the fleet routes — STAGED SURFACES ──────────────────────────────── -// Registered ONLY by the entitlement-staged path below (createAmicodeService's -// fleet option → stageFleetDataPlane). A boot without a staged fleet plane -// never mounts these: /amicode/fleet/* answers the base no-route 404, which -// is exactly the H3 byte-identity assertion. The routes are new fleet-class -// surfaces (ADR-0004 d.3's "data-plane routing" class) declared by the -// overlay manifest the staging gate validated. +// The FLEET-DATA handlers are registered ONLY by the entitlement-staged path +// below (createAmicodeService's fleet option → stageFleetDataPlane / +// baseStudioActivates). The routes the APP calls (sessions, grants, +// creation-target, peer-home-base) are ADDITIONALLY pre-mounted with the +// #1812 no-topology absence in createAmicodeService BEFORE this block, so a +// boot that never stages answers the documented {ok:false, +// reason:"no-fleet-peers"} 404 (the app's silent fallback — the panel render +// gate refuses the generic no-route 404 they used to serve) while a staged +// boot re-adds the same keys here with the REAL handlers (server.add is +// last-wins) — the fleet-data paths are untouched. The routes are new +// fleet-class surfaces (ADR-0004 d.3's "data-plane routing" class) declared +// by the overlay manifest the staging gate validated. export interface FleetRouteDeps { /** The late-bound routing mode (data-driven per request). */ @@ -1039,6 +1045,23 @@ export function createAmicodeService( // observation-only base peer). Left undefined where no fleet-peer provider / // owner map exists → the route answers its not-served 404. let fleetSessionStatusDeps: FleetSessionStatusRouteDeps | undefined; + // #1812: the app-called fleet routes' no-topology absence. #1768's + // discipline (session-status) extended: a machine without a fleet topology + // (the unarmed hub — hub_fleet_wiring resolves no server/standalone id, an + // unentitled staging refusal, a fleet-of-one observer) must answer the + // DOCUMENTED 404 {ok:false, reason:"no-fleet-peers"} — the body + // session-status serves, the app's silent-fallback signal — never the + // generic no-route 404 the panel render gate refuses. Registered FIRST so + // whichever boot branch stages below re-adds the same route keys with the + // REAL handlers (server.add is last-wins): the fleet-data paths are + // untouched, and a staged/topology-bearing boot is byte-identical. Only the + // routes the app calls (sessions + grants + creation-target + + // peer-home-base); /amicode/fleet/status — not app-called — keeps the #391 + // base no-route 404, preserving the H3 discipline where it still holds. + const noFleetPeersBody = JSON.stringify({ ok: false, reason: "no-fleet-peers" }); + for (const path of ["/amicode/fleet/sessions", "/amicode/fleet/grants", "/amicode/fleet/creation-target", "/amicode/fleet/peer-home-base"]) { + server.add("GET", path, () => ({ status: 404, body: noFleetPeersBody })); + } // #391: the fleet plane stages ONLY through the resolver's dispatch. No // entitlement → this block never arms anything → zero fleet surfaces, // byte-identical. diff --git a/packages/extension/test/amicode_service_fleet_absence.test.ts b/packages/extension/test/amicode_service_fleet_absence.test.ts new file mode 100644 index 000000000..8b2f2e74c --- /dev/null +++ b/packages/extension/test/amicode_service_fleet_absence.test.ts @@ -0,0 +1,302 @@ +// amicode_service_fleet_absence.test.ts — #1812: the fleet routes the APP +// calls answer the DOCUMENTED no-fleet absence {ok:false, reason:"no-fleet-peers"} +// on every boot, never the generic no-route 404 the panel render gate refuses. +// +// #1768's discipline, extended: GET /amicode/fleet/session-status already +// mounts unconditionally and answers the not-served 404 {ok:false, +// reason:"no-fleet-peers"} (the app's silent-fallback signal) wherever no +// fleet topology exists. But the OTHER app-called fleet routes — sessions +// (+ ?scope= / ?archived=), grants, creation-target, peer-home-base — only +// mounted WITH a staged/topology-bearing boot, so a no-topology machine (the +// hub: hub_fleet_wiring resolves no server/standalone machine id → the unarmed +// base service) served them the generic `no route` 404. This suite pins: +// +// AC1 — sessions (bare, ?scope=all, ?archived=true) answers 404 +// {ok:false, reason:"no-fleet-peers"} on EVERY fleetless boot +// (no fleet option; armed-but-unentitled staging refusal; an +// observation-only provider with ZERO serving peers); +// AC2 — the other app-called fleet routes (grants, creation-target, +// peer-home-base) answer the SAME absence, and the always-on family +// (session-status, attachment, effective-stream) keeps its documented +// answers; /amicode/fleet/status — NOT app-called — keeps the #391 +// base no-route 404 (H3 preserved where it still means something); +// AC3 — with a topology (an observation-only serving peer; a premium +// staged plane), the SAME routes serve REAL data — the always-on +// absence never shadows the staged registration; +// auth — the absence answers are credential-gated like every /amicode/* read. +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import * as http from "node:http"; +import { AddressInfo } from "node:net"; +import { createAmicodeService } from "../src/amicode_service"; +import { serverAuthToken, serverAuthHeader } from "../src/server_auth"; + +const LOCAL_ID = "test-laptop"; +const PEER_ID = "test-studio"; +const ENGINE_PW = "engine-mint-1812"; +const PEER_TOKEN = "tok-studio-1812"; +const PEER_HOME = "/Users/peer/harmoniqs"; +const ABSENCE = { ok: false, reason: "no-fleet-peers" }; + +interface Stub { + url: string; + stop(): Promise; +} + +/** An engine/peer stub: /experimental/session lists `sessions`, /global/health + * stamps the version, behind `credential` (Basic server-auth shape). */ +function startStub(sessions: unknown[], credential: string, peerWorkspace = false): Promise { + const server = http.createServer((req, res) => { + if (req.headers.authorization !== serverAuthHeader(credential)) { + res.writeHead(401, { "content-type": "application/json" }); + res.end(JSON.stringify({ ok: false, error: "unauthorized" })); + return; + } + res.setHeader("content-type", "application/json"); + const u = new URL(req.url ?? "/", "http://stub"); + if (req.method === "GET" && (u.pathname === "/session" || u.pathname === "/experimental/session")) { + const archived = u.searchParams.get("archived") === "true"; + res.end(JSON.stringify(archived ? [] : sessions)); + return; + } + if (req.method === "GET" && u.pathname === "/global/health") { + res.end(JSON.stringify({ healthy: true, version: "v1.18.29" })); + return; + } + // the peer's OWN workspace truth (#1643): what peer-home-base dials + if (peerWorkspace && req.method === "GET" && u.pathname === "/amicode/fleet/peer-workspace") { + res.end(JSON.stringify({ ok: true, home_base: PEER_HOME, projects: [] })); + return; + } + res.statusCode = 404; + res.end(JSON.stringify({ message: "not found" })); + }); + return new Promise((resolve) => { + server.listen(0, "127.0.0.1", () => { + resolve({ + url: `http://127.0.0.1:${(server.address() as AddressInfo).port}`, + stop: () => new Promise((r) => server.close(() => r())), + }); + }); + }); +} + +function writeDataPlaneManifest(sourceRoot: string): void { + const dir = join(sourceRoot, "fleet_overlay", "overlays"); + mkdirSync(dir, { recursive: true }); + writeFileSync( + join(dir, "fleet-data-plane.json"), + JSON.stringify({ + overlay_id: "fleet-data-plane", + overlay_version: 1, + base_version: "v1.18.29", + surfaces: [ + { + surface_id: "data-plane-routing", + fleet_class: "data-plane routing", + fields: [ + { name: "upstream_mode", base_default: "engine", description: "the routing mode" }, + { name: "hub_upstream", base_default: null, description: "the hub over the fleet tunnel" }, + { name: "hub_credential_entry", base_default: null, description: "the hub mint's store entry" }, + { name: "merged_projection", base_default: null, description: "the merged read path" }, + ], + }, + ], + }), + ); +} + +const auth = () => ({ Authorization: `Basic ${serverAuthToken(ENGINE_PW)}` }); + +/** Fetch one route on a booted origin, returning {status, body}. */ +async function get(origin: string, path: string): Promise<{ status: number; body: unknown }> { + const res = await fetch(`${origin}${path}`, { headers: auth() }); + return { status: res.status, body: await res.json() }; +} + +describe("#1812 — the app-called fleet routes answer the documented no-fleet absence", () => { + let root: string; + let overlaySource: string; + let engine: Stub; + let peer: Stub; + const savedEnv: Record = {}; + + const servingPeerProvider = () => ({ + localMachineId: LOCAL_ID, + getServingPeers: () => [{ machineId: PEER_ID }], + readPeerToken: (id: string) => + id === PEER_ID + ? { ok: true as const, credential: { baseUrl: peer.url, token: PEER_TOKEN } } + : { ok: false as const }, + rosterLookup: (id: string) => ({ name: id }), + }); + + const noPeerProvider = () => ({ + localMachineId: LOCAL_ID, + getServingPeers: () => [] as Array<{ machineId: string }>, + readPeerToken: () => ({ ok: false as const }), + rosterLookup: (id: string) => ({ name: id }), + }); + + function boot(fleet: Parameters[0]["fleet"]) { + return createAmicodeService({ + password: "service-own-mint", + engine: { password: ENGINE_PW, getUrl: () => engine.url }, + ...(fleet === undefined ? {} : { fleet }), + }); + } + + beforeAll(async () => { + root = mkdtempSync(join(tmpdir(), "amicode-1812-absence-")); + overlaySource = join(root, "overlay-source"); + writeDataPlaneManifest(overlaySource); + engine = await startStub([{ id: "ses-local", title: "local", time: { created: 1, updated: 2 } }], ENGINE_PW); + peer = await startStub([{ id: "ses-peer", title: "peer", time: { created: 3, updated: 4 } }], PEER_TOKEN, true); + // isolate every real store the routes read (hub credential, lifecycle + // grants, control requests) so the fleetless answers are deterministic + for (const k of ["AMICO_FLEET_HUB_FILE", "AMICO_FLEET_LIFECYCLE_GRANT_FILE", "AMICO_FLEET_CONTROL_REQUEST_FILE"]) { + savedEnv[k] = process.env[k]; + process.env[k] = join(root, `${k.toLowerCase()}-absent.json`); + } + }); + + afterAll(async () => { + await engine.stop(); + await peer.stop(); + for (const [k, v] of Object.entries(savedEnv)) { + if (v === undefined) delete process.env[k]; + else process.env[k] = v; + } + rmSync(root, { recursive: true, force: true }); + }); + + // the fleetless boot shapes from the wild: the hub's unarmed base service, + // the armed-but-unentitled staging refusal, and a fleet-of-one observer + const fleetlessBoots = () => [ + { name: "a base boot (no fleet option at all — the hub's unarmed shape)", fleet: undefined }, + { + name: "an armed-but-unentitled boot (the staging gate refuses)", + fleet: { entitlements: [] as string[], overlaySource, hub: { getUrl: () => undefined } }, + }, + { + name: "an observation-only boot whose provider resolves ZERO serving peers", + fleet: { hub: { getUrl: () => undefined }, observationOnly: true as const, fleetPeers: noPeerProvider() }, + }, + ]; + + // ── AC1: sessions (every variant the app calls) ───────────────────────────── + it.each(fleetlessBoots())( + "$name: GET /amicode/fleet/sessions (bare, ?scope=all, ?archived=true) answers 404 {ok:false, reason:'no-fleet-peers'}", + async ({ fleet }) => { + const svc = boot(fleet); + const origin = (await svc.start()).toString().replace(/\/$/, ""); + try { + for (const q of ["", "?scope=all", "?archived=true"]) { + const r = await get(origin, `/amicode/fleet/sessions${q}`); + expect(r.status).toBe(404); + expect(r.body).toEqual(ABSENCE); + } + } finally { + await svc.stop(); + } + }, + ); + + // ── AC2: the other app-called fleet routes + the always-on family ───────── + it.each(fleetlessBoots())( + "$name: grants, creation-target, and peer-home-base answer the SAME documented absence", + async ({ fleet }) => { + const svc = boot(fleet); + const origin = (await svc.start()).toString().replace(/\/$/, ""); + try { + for (const path of [ + "/amicode/fleet/grants", + `/amicode/fleet/creation-target?machine=${encodeURIComponent(PEER_ID)}`, + `/amicode/fleet/peer-home-base?machine=${encodeURIComponent(PEER_ID)}`, + ]) { + const r = await get(origin, path); + expect(r.status).toBe(404); + expect(r.body).toEqual(ABSENCE); + } + } finally { + await svc.stop(); + } + }, + ); + + it("a fleetless boot: session-status answers its documented absence and attachment/effective-stream stay served (the always-on family)", async () => { + const svc = boot(undefined); + const origin = (await svc.start()).toString().replace(/\/$/, ""); + try { + const status = await get(origin, "/amicode/fleet/session-status?ids=ses-peer"); + expect(status.status).toBe(404); + expect(status.body).toEqual(ABSENCE); + const attachment = await get(origin, "/amicode/fleet/attachment"); + expect(attachment.status).toBe(200); + const stream = await get(origin, "/amicode/fleet/effective-stream"); + expect(stream.status).toBe(200); + } finally { + await svc.stop(); + } + }); + + it("a fleetless boot: /amicode/fleet/status (NOT app-called) keeps the #391 base no-route 404 — H3 where it still means something", async () => { + const svc = boot(undefined); + const origin = (await svc.start()).toString().replace(/\/$/, ""); + try { + const r = await get(origin, "/amicode/fleet/status"); + expect(r.status).toBe(404); + expect(r.body).toEqual({ ok: false, error: "no route: GET /amicode/fleet/status" }); + } finally { + await svc.stop(); + } + }); + + it("the absence answers are credential-gated exactly like every other /amicode read", async () => { + const svc = boot(undefined); + const origin = (await svc.start()).toString().replace(/\/$/, ""); + try { + const res = await fetch(`${origin}/amicode/fleet/sessions`); + expect(res.status).toBe(401); + expect(res.headers.get("www-authenticate")).toBe('Basic realm="amicode-service"'); + } finally { + await svc.stop(); + } + }); + + // ── AC3: with a topology, the SAME routes serve REAL data ───────────────── + it.each([ + { name: "an observation-only boot with a serving peer", premium: false }, + { name: "a premium staged plane with a serving peer", premium: true }, + ])("$name: the routes serve real data — the always-on absence never shadows the staged registration", async ({ premium }) => { + const svc = boot({ + hub: { getUrl: () => undefined }, + fleetPeers: servingPeerProvider(), + ...(premium ? { entitlements: ["amicissimo"], overlaySource } : { observationOnly: true }), + }); + const origin = (await svc.start()).toString().replace(/\/$/, ""); + try { + const sessions = await get(origin, "/amicode/fleet/sessions"); + expect(sessions.status).toBe(200); + expect((sessions.body as { ok: boolean }).ok).toBe(true); + expect((sessions.body as { sources: Record }).sources[PEER_ID].present).toBe(true); + + const grants = await get(origin, "/amicode/fleet/grants"); + expect(grants.status).toBe(200); + expect((grants.body as { ok: boolean }).ok).toBe(true); + + const target = await get(origin, `/amicode/fleet/creation-target?machine=${encodeURIComponent(PEER_ID)}`); + expect(target.status).toBe(200); + expect((target.body as { ok: boolean }).ok).toBe(true); + expect((target.body as { target: { machineId?: string } }).target.machineId).toBe(PEER_ID); + + const homeBase = await get(origin, `/amicode/fleet/peer-home-base?machine=${encodeURIComponent(PEER_ID)}`); + expect(homeBase.status).toBe(200); + expect(homeBase.body).toEqual({ ok: true, directory: PEER_HOME }); + } finally { + await svc.stop(); + } + }); +}); diff --git a/packages/extension/test/amicode_service_fleet_data_plane.test.ts b/packages/extension/test/amicode_service_fleet_data_plane.test.ts index 33a6e22fb..f545670a7 100644 --- a/packages/extension/test/amicode_service_fleet_data_plane.test.ts +++ b/packages/extension/test/amicode_service_fleet_data_plane.test.ts @@ -252,12 +252,24 @@ describe("H3 staging fixture — zero fleet surfaces without the entitlement (by expect(gated).toEqual(base); }); - it("with no entitlement, the fleet paths answer the base no-route shape (the fleet mode does not exist)", async () => { - const res = await fetch(`${gatedOrigin}/amicode/fleet/sessions`, { + it("with no entitlement, the app-called fleet route answers the #1812 documented absence and status keeps the base no-route shape (the fleet mode does not exist)", async () => { + // #1812: sessions is app-called, so a fleetless boot answers the + // documented {ok:false, reason:"no-fleet-peers"} 404 (the app's silent + // fallback — the panel render gate refuses the generic no-route 404 it + // used to serve). NO real fleet data may leak: the absence is not a + // staged projection. + const sessions = await fetch(`${gatedOrigin}/amicode/fleet/sessions`, { headers: { Authorization: `Basic ${engineToken}` }, }); - expect(res.status).toBe(404); - expect(await res.json()).toEqual({ ok: false, error: "no route: GET /amicode/fleet/sessions" }); + expect(sessions.status).toBe(404); + expect(await sessions.json()).toEqual({ ok: false, reason: "no-fleet-peers" }); + // /amicode/fleet/status is NOT app-called — it keeps the #391 base + // no-route 404, so the staging gate lighting NOTHING stays observable. + const status = await fetch(`${gatedOrigin}/amicode/fleet/status`, { + headers: { Authorization: `Basic ${engineToken}` }, + }); + expect(status.status).toBe(404); + expect(await status.json()).toEqual({ ok: false, error: "no route: GET /amicode/fleet/status" }); }); }); @@ -2306,10 +2318,16 @@ describe("#1524 observation-only base peer-studio — routes mount without hub-a const obsCap = await cap(obsO); // H3: the observation-only-with-no-peers boot is byte-identical to base. expect(obsCap).toEqual(baseCap); - // and explicitly: the fleet routes 404 with the base no-route shape. + // and explicitly (#1812): the app-called sessions route 404s with the + // DOCUMENTED no-fleet absence (the app's silent fallback — both boots + // answer it identically, which is why the byte-identity above holds); + // status — not app-called — keeps the base no-route shape. const s = obsCap.find((x) => x.p === "/amicode/fleet/sessions")!; expect(s.status).toBe(404); - expect(JSON.parse(s.body)).toEqual({ ok: false, error: "no route: GET /amicode/fleet/sessions" }); + expect(JSON.parse(s.body)).toEqual({ ok: false, reason: "no-fleet-peers" }); + const st = obsCap.find((x) => x.p === "/amicode/fleet/status")!; + expect(st.status).toBe(404); + expect(JSON.parse(st.body)).toEqual({ ok: false, error: "no route: GET /amicode/fleet/status" }); } finally { await base.stop(); await obs.stop();