summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-05 01:22:21 +0900
committerAdam Malczewski <[email protected]>2026-06-05 01:22:21 +0900
commit977ca522736bba53172e010494de5ac59fdb2a4a (patch)
treeb4e4a34fe7319e050e231e3bd7a840dc32562838
parent64e9688cc27ceea6eba442d156868d82d7aafb75 (diff)
downloaddispatch-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.ts38
-rw-r--r--packages/kernel/src/host/host.test.ts63
-rw-r--r--packages/kernel/src/host/host.ts10
-rw-r--r--packages/storage-sqlite/src/extension.ts2
-rw-r--r--tasks.md22
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) => {},
};
diff --git a/tasks.md b/tasks.md
index 83e9bf7..3777b23 100644
--- a/tasks.md
+++ b/tasks.md
@@ -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).