diff options
| author | Adam Malczewski <[email protected]> | 2026-06-05 01:22:21 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-05 01:22:21 +0900 |
| commit | 977ca522736bba53172e010494de5ac59fdb2a4a (patch) | |
| tree | b4e4a34fe7319e050e231e3bd7a840dc32562838 | |
| parent | 64e9688cc27ceea6eba442d156868d82d7aafb75 (diff) | |
| download | dispatch-977ca522736bba53172e010494de5ac59fdb2a4a.tar.gz dispatch-977ca522736bba53172e010494de5ac59fdb2a4a.zip | |
refactor(host): expose getHostAPI(); host-bin drops duplicate adapter; storage-sqlite manifest honesty
host CR-1: createHost.getHostAPI() returns the canonical post-activation HostAPI
(registration closed) via a single builder — host-bin deletes its
buildPostActivationHostAPI duplicate and calls host.getHostAPI().
storage-sqlite CR-2: remove false contributes.services:["storage"] (backend is a
kernel bootstrap dep injected as HostDeps.storageFactory, not a bus service);
document the intentional no-op activate.
typecheck clean, 218 tests pass, biome clean; live boot + curl verified.
| -rw-r--r-- | packages/host-bin/src/main.ts | 38 | ||||
| -rw-r--r-- | packages/kernel/src/host/host.test.ts | 63 | ||||
| -rw-r--r-- | packages/kernel/src/host/host.ts | 10 | ||||
| -rw-r--r-- | packages/storage-sqlite/src/extension.ts | 2 | ||||
| -rw-r--r-- | tasks.md | 22 |
5 files changed, 96 insertions, 39 deletions
diff --git a/packages/host-bin/src/main.ts b/packages/host-bin/src/main.ts index e26b8c6..0cb0d37 100644 --- a/packages/host-bin/src/main.ts +++ b/packages/host-bin/src/main.ts @@ -8,7 +8,6 @@ import { createHost, type EventsEmitter, type Extension, - type HostAPI, type HostDeps, type Logger, type PermissionGate, @@ -54,41 +53,6 @@ function createNoopEvents(): EventsEmitter { return { emit: () => {} }; } -function buildPostActivationHostAPI( - host: { - getProviders: () => ReadonlyMap<string, unknown>; - getTools: () => ReadonlyMap<string, unknown>; - getAuthProviders: () => ReadonlyMap<string, unknown>; - getAuthProvider: (id: string) => unknown; - }, - deps: HostDeps, -): HostAPI { - const notAvailable = () => { - throw new Error("Registration not available after activation"); - }; - return { - defineTool: notAvailable, - defineProvider: notAvailable, - defineAuth: notAvailable, - on: (hook, handler) => deps.bus.on(hook, handler), - addFilter: (hook, fn) => deps.bus.addFilter(hook, fn), - provideService: (handle, impl) => deps.bus.provideService(handle, impl), - getService: (handle) => deps.bus.getService(handle), - storage: (namespace: string) => deps.storageFactory(namespace), - config: deps.config, - secrets: deps.secrets, - permissions: deps.permissions, - events: deps.events, - logger: deps.logger, - getProviders: () => host.getProviders() as ReturnType<HostAPI["getProviders"]>, - getTools: () => host.getTools() as ReturnType<HostAPI["getTools"]>, - getAuthProviders: () => host.getAuthProviders() as ReturnType<HostAPI["getAuthProviders"]>, - getAuthProvider: (id: string) => - host.getAuthProvider(id) as ReturnType<HostAPI["getAuthProvider"]>, - scheduler: { register: (job: ScheduledJob) => deps.scheduler.register(job) }, - }; -} - const CORE_EXTENSIONS: readonly Extension[] = [ storageSqliteExt, conversationStoreExt, @@ -131,7 +95,7 @@ async function boot(): Promise<void> { } } - const hostAPI = buildPostActivationHostAPI(host, deps); + const hostAPI = host.getHostAPI(); const app = createServer(hostAPI); // Port precedence: BACKEND_PORT (the rewrite's assigned port) → PORT → default. diff --git a/packages/kernel/src/host/host.test.ts b/packages/kernel/src/host/host.test.ts index 82d5177..11c2356 100644 --- a/packages/kernel/src/host/host.test.ts +++ b/packages/kernel/src/host/host.test.ts @@ -687,4 +687,67 @@ describe("createHost", () => { expect(host.getDisabled()).toHaveLength(0); }); }); + + describe("getHostAPI", () => { + it("returns a HostAPI whose read-views reflect registrations from activation", async () => { + const tool = createFakeTool("read-file"); + const provider = createFakeProvider("anthropic"); + const auth = createFakeAuth("apikey"); + + const ext = createExtension("multi-ext", { + activate: (host) => { + host.defineTool(tool); + host.defineProvider(provider); + host.defineAuth(auth); + }, + }); + + const host = createHost([ext], deps); + await host.activate(); + + const api = host.getHostAPI(); + + expect(api.getTools().size).toBe(1); + expect(api.getTools().get("read-file")).toBe(tool); + + expect(api.getProviders().size).toBe(1); + expect(api.getProviders().get("anthropic")).toBe(provider); + + expect(api.getAuthProviders().size).toBe(1); + expect(api.getAuthProvider("apikey")).toBe(auth); + }); + + it("throws on defineTool after activation", async () => { + const ext = createExtension("ext", { activate: () => {} }); + const host = createHost([ext], deps); + await host.activate(); + + const api = host.getHostAPI(); + expect(() => api.defineTool(createFakeTool("late"))).toThrow( + "Registration not available after activation", + ); + }); + + it("throws on defineProvider after activation", async () => { + const ext = createExtension("ext", { activate: () => {} }); + const host = createHost([ext], deps); + await host.activate(); + + const api = host.getHostAPI(); + expect(() => api.defineProvider(createFakeProvider("late"))).toThrow( + "Registration not available after activation", + ); + }); + + it("throws on defineAuth after activation", async () => { + const ext = createExtension("ext", { activate: () => {} }); + const host = createHost([ext], deps); + await host.activate(); + + const api = host.getHostAPI(); + expect(() => api.defineAuth(createFakeAuth("late"))).toThrow( + "Registration not available after activation", + ); + }); + }); }); diff --git a/packages/kernel/src/host/host.ts b/packages/kernel/src/host/host.ts index 8592a16..dd61f9f 100644 --- a/packages/kernel/src/host/host.ts +++ b/packages/kernel/src/host/host.ts @@ -54,6 +54,7 @@ export interface Host { readonly getScheduledJobs: () => readonly ScheduledJob[]; readonly getMigrations: () => readonly string[]; readonly getDisabled: () => readonly DisabledExtension[]; + readonly getHostAPI: () => HostAPI; } export function createHost(extensions: readonly Extension[], deps: HostDeps): Host { @@ -95,15 +96,19 @@ export function createHost(extensions: readonly Extension[], deps: HostDeps): Ho } } - function buildHostAPI(): HostAPI { + function buildHostAPI(opts?: { readonly registrationClosed?: boolean }): HostAPI { + const closed = opts?.registrationClosed ?? false; return { defineTool(tool: ToolContract) { + if (closed) throw new Error("Registration not available after activation"); tools.set(tool.name, tool); }, defineProvider(provider: ProviderContract) { + if (closed) throw new Error("Registration not available after activation"); providers.set(provider.id, provider); }, defineAuth(auth: AuthContract) { + if (closed) throw new Error("Registration not available after activation"); authProviders.set(auth.id, auth); }, on<TPayload>(hook: EventHookDescriptor<TPayload>, handler: EventHandler<TPayload>) { @@ -201,5 +206,8 @@ export function createHost(extensions: readonly Extension[], deps: HostDeps): Ho getDisabled() { return disabled; }, + getHostAPI() { + return buildHostAPI({ registrationClosed: true }); + }, }; } diff --git a/packages/storage-sqlite/src/extension.ts b/packages/storage-sqlite/src/extension.ts index 63a71af..32297e5 100644 --- a/packages/storage-sqlite/src/extension.ts +++ b/packages/storage-sqlite/src/extension.ts @@ -7,11 +7,11 @@ export const manifest: Manifest = { apiVersion: "^0.1.0", trust: "bundled", capabilities: { db: true }, - contributes: { services: ["storage"] }, activation: "eager", }; export const extension: Extension = { manifest, + // No-op: the SQLite backend is a kernel bootstrap dep injected via HostDeps.storageFactory, not a bus service. activate: async (_host: HostAPI) => {}, }; @@ -100,3 +100,25 @@ contained a real **`tool-call` + `tool-result`** round-trip and the final answer quoted the file's secret passphrase (MAGENTA-OTTER-42) correctly. The kernel tool-dispatch loop is now proven end-to-end against a live model (§3.3). Summon: prompts/step2-tool-read-file.md, report: reports/step2-tool-read-file.md. + +### Step 3 — Small CRs / hygiene [x] DONE (verified live) +Parallel summons (disjoint file sets, mimo-v2.5-pro; output→reports/*.run.log): +- **host CR-1** (kernel-host owner): `createHost` exposes `getHostAPI()` (registration + closed post-activation) so host-bin drops its `buildPostActivationHostAPI` duplicate. + prompts/step3-kernel-host.md. Files: packages/kernel/src/host/{host.ts,host.test.ts}. +- **storage-sqlite CR-2** (owner): remove false `contributes.services:["storage"]` + from manifest (backend is a kernel bootstrap dep via HostDeps.storageFactory, not a + bus service); document the intentional no-op activate. prompts/step3-storage-sqlite.md. + Files: packages/storage-sqlite/src/extension.ts. +After both land: orchestrator wires host-bin to use `host.getHostAPI()` (deletes adapter). + +**Step 3 RESULT:** done + verified. (1) kernel-host: `createHost` now exposes +`getHostAPI()` returning the canonical post-activation HostAPI with registration +methods closed (single builder w/ `registrationClosed` flag — zero duplication); ++4 host tests. (2) storage-sqlite: removed false `contributes.services:["storage"]`; +documented the intentional no-op activate (backend is a kernel bootstrap dep via +HostDeps.storageFactory). (3) host-bin wiring (orchestrator): deleted the +`buildPostActivationHostAPI` adapter, now calls `host.getHostAPI()`; dropped the +now-unused `HostAPI` import. typecheck clean, **218 tests pass**, biome clean. +Live boot: all 7 extensions activate, curl returns real responses. Summons: +prompts/step3-{kernel-host,storage-sqlite}.md (mimo-v2.5-pro, parallel, disjoint). |
