summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-05 00:50:05 +0900
committerAdam Malczewski <[email protected]>2026-06-05 00:50:05 +0900
commitab74946c3b6e1c2e55e47690d11ad838468d5c2f (patch)
tree7c12ea79b1817dbff939d85d1690866313d4b0f9
parentefb4737fd22572f757e7a9eb1034dd96ae1b6593 (diff)
downloaddispatch-ab74946c3b6e1c2e55e47690d11ad838468d5c2f.tar.gz
dispatch-ab74946c3b6e1c2e55e47690d11ad838468d5c2f.zip
feat(auth): provider resolves credentials via AuthContract, not config
- kernel HostAPI: add getAuthProviders()/getAuthProvider(id) read-views (mirrors getProviders) - provider-openai-compat: activate() resolves creds via host.getAuthProvider("apikey").resolve(); dependsOn auth-apikey; model stays config-driven - host-bin: mirror the new getters in post-activation HostAPI stub - auth-apikey is no longer vestigial; auth seam exercised end-to-end - 185 tests pass; typecheck + biome clean; verified live (curl returns real response)
-rw-r--r--packages/host-bin/src/main.ts5
-rw-r--r--packages/kernel/src/contracts/extension.ts6
-rw-r--r--packages/kernel/src/host/host.test.ts27
-rw-r--r--packages/kernel/src/host/host.ts6
-rw-r--r--packages/provider-openai-compat/src/extension.test.ts99
-rw-r--r--packages/provider-openai-compat/src/extension.ts35
-rw-r--r--tasks.md30
7 files changed, 195 insertions, 13 deletions
diff --git a/packages/host-bin/src/main.ts b/packages/host-bin/src/main.ts
index a3eb164..fe36e80 100644
--- a/packages/host-bin/src/main.ts
+++ b/packages/host-bin/src/main.ts
@@ -57,6 +57,8 @@ function buildPostActivationHostAPI(
host: {
getProviders: () => ReadonlyMap<string, unknown>;
getTools: () => ReadonlyMap<string, unknown>;
+ getAuthProviders: () => ReadonlyMap<string, unknown>;
+ getAuthProvider: (id: string) => unknown;
},
deps: HostDeps,
): HostAPI {
@@ -79,6 +81,9 @@ function buildPostActivationHostAPI(
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) },
};
}
diff --git a/packages/kernel/src/contracts/extension.ts b/packages/kernel/src/contracts/extension.ts
index 7078a9a..424f714 100644
--- a/packages/kernel/src/contracts/extension.ts
+++ b/packages/kernel/src/contracts/extension.ts
@@ -230,6 +230,12 @@ export interface HostAPI {
/** Read-only view of all registered tools. */
readonly getTools: () => ReadonlyMap<string, ToolContract>;
+ /** Read-only view of all registered auth providers. */
+ readonly getAuthProviders: () => ReadonlyMap<string, AuthContract>;
+
+ /** Look up a single auth provider by id. */
+ readonly getAuthProvider: (id: string) => AuthContract | undefined;
+
/** Register a scheduled job with the host's scheduler. */
readonly scheduler: {
readonly register: (job: ScheduledJob) => void;
diff --git a/packages/kernel/src/host/host.test.ts b/packages/kernel/src/host/host.test.ts
index f7b7eba..82d5177 100644
--- a/packages/kernel/src/host/host.test.ts
+++ b/packages/kernel/src/host/host.test.ts
@@ -632,6 +632,33 @@ describe("createHost", () => {
expect(capturedProviders?.size).toBe(1);
expect(capturedProviders?.get("anthropic")).toBe(provider);
});
+
+ it("getAuthProviders/getAuthProvider returns registered auth via HostAPI", async () => {
+ const auth = createFakeAuth("apikey");
+ let capturedAuth: ReadonlyMap<string, AuthContract> | undefined;
+ let capturedSingle: AuthContract | undefined;
+
+ const producer = createExtension("auth-apikey", {
+ activate: (host) => {
+ host.defineAuth(auth);
+ },
+ });
+ const consumer = createExtension("consumer", {
+ dependsOn: ["auth-apikey"],
+ activate: (host) => {
+ capturedAuth = host.getAuthProviders();
+ capturedSingle = host.getAuthProvider("apikey");
+ },
+ });
+
+ const host = createHost([producer, consumer], deps);
+ await host.activate();
+
+ expect(capturedAuth).toBeDefined();
+ expect(capturedAuth?.size).toBe(1);
+ expect(capturedAuth?.get("apikey")).toBe(auth);
+ expect(capturedSingle).toBe(auth);
+ });
});
describe("DAG errors", () => {
diff --git a/packages/kernel/src/host/host.ts b/packages/kernel/src/host/host.ts
index e0ca5d5..8592a16 100644
--- a/packages/kernel/src/host/host.ts
+++ b/packages/kernel/src/host/host.ts
@@ -132,6 +132,12 @@ export function createHost(extensions: readonly Extension[], deps: HostDeps): Ho
getTools() {
return tools;
},
+ getAuthProviders() {
+ return authProviders;
+ },
+ getAuthProvider(id: string) {
+ return authProviders.get(id);
+ },
scheduler: {
register(job: ScheduledJob) {
scheduledJobs.push(job);
diff --git a/packages/provider-openai-compat/src/extension.test.ts b/packages/provider-openai-compat/src/extension.test.ts
new file mode 100644
index 0000000..6dc409a
--- /dev/null
+++ b/packages/provider-openai-compat/src/extension.test.ts
@@ -0,0 +1,99 @@
+import type { ApiKeyCredentials, AuthContract, HostAPI } from "@dispatch/kernel";
+import { describe, expect, it, vi } from "vitest";
+import { activate, manifest } from "./extension.js";
+
+function makeFakeHost(overrides: {
+ getAuthProvider?: (id: string) => AuthContract | undefined;
+ configGet?: (key: string) => unknown;
+}): { host: HostAPI; defineProvider: ReturnType<typeof vi.fn> } {
+ const defineProvider = vi.fn();
+ const warn = vi.fn();
+ const info = vi.fn();
+ const host = {
+ defineProvider,
+ config: { get: overrides.configGet ?? (() => undefined) },
+ logger: { debug: vi.fn(), info, warn, error: vi.fn() },
+ getAuthProvider: overrides.getAuthProvider ?? (() => undefined),
+ } as unknown as HostAPI;
+ return { host, defineProvider };
+}
+
+describe("provider-openai-compat activation", () => {
+ it("registers provider when auth resolves ApiKeyCredentials", async () => {
+ const fakeCreds: ApiKeyCredentials = { type: "api-key", apiKey: "sk-test" };
+ const fakeAuth: AuthContract = {
+ id: "apikey",
+ resolve: async () => fakeCreds,
+ };
+
+ const { host, defineProvider } = makeFakeHost({
+ getAuthProvider: (id) => (id === "apikey" ? fakeAuth : undefined),
+ });
+
+ await activate(host);
+
+ expect(defineProvider).toHaveBeenCalledTimes(1);
+ expect(defineProvider.mock.calls[0]?.[0]?.id).toBe("openai-compat");
+ });
+
+ it("does not register provider when getAuthProvider returns undefined", async () => {
+ const { host, defineProvider } = makeFakeHost({
+ getAuthProvider: () => undefined,
+ });
+
+ await activate(host);
+
+ expect(defineProvider).not.toHaveBeenCalled();
+ });
+
+ it("does not register provider when resolve returns null", async () => {
+ const fakeAuth: AuthContract = {
+ id: "apikey",
+ resolve: async () => null,
+ };
+
+ const { host, defineProvider } = makeFakeHost({
+ getAuthProvider: (id) => (id === "apikey" ? fakeAuth : undefined),
+ });
+
+ await activate(host);
+
+ expect(defineProvider).not.toHaveBeenCalled();
+ });
+
+ it("does not register provider when credentials are not api-key type", async () => {
+ const fakeAuth: AuthContract = {
+ id: "apikey",
+ resolve: async () => ({ type: "bearer-token", token: "tok-123" }),
+ };
+
+ const { host, defineProvider } = makeFakeHost({
+ getAuthProvider: (id) => (id === "apikey" ? fakeAuth : undefined),
+ });
+
+ await activate(host);
+
+ expect(defineProvider).not.toHaveBeenCalled();
+ });
+
+ it("uses default model when config returns undefined", async () => {
+ const fakeCreds: ApiKeyCredentials = { type: "api-key", apiKey: "sk-test" };
+ const fakeAuth: AuthContract = {
+ id: "apikey",
+ resolve: async () => fakeCreds,
+ };
+
+ const { host, defineProvider } = makeFakeHost({
+ getAuthProvider: (id) => (id === "apikey" ? fakeAuth : undefined),
+ configGet: () => undefined,
+ });
+
+ await activate(host);
+
+ expect(defineProvider).toHaveBeenCalledTimes(1);
+ });
+
+ it("declares dependsOn auth-apikey", () => {
+ expect(manifest.dependsOn).toEqual(["auth-apikey"]);
+ });
+});
diff --git a/packages/provider-openai-compat/src/extension.ts b/packages/provider-openai-compat/src/extension.ts
index 1026381..4a580d3 100644
--- a/packages/provider-openai-compat/src/extension.ts
+++ b/packages/provider-openai-compat/src/extension.ts
@@ -1,4 +1,4 @@
-import type { ApiKeyCredentials, Extension, HostAPI, Manifest } from "@dispatch/kernel";
+import type { Extension, HostAPI, Manifest } from "@dispatch/kernel";
import { createOpenAICompatProvider } from "./provider.js";
export const manifest: Manifest = {
@@ -6,31 +6,40 @@ export const manifest: Manifest = {
name: "OpenAI-Compatible Provider",
version: "0.0.0",
apiVersion: "^0.1.0",
+ dependsOn: ["auth-apikey"],
trust: "bundled",
activation: "eager",
capabilities: { network: true },
contributes: { providers: ["openai-compat"] },
};
-export function activate(host: HostAPI): void {
- const apiKey = host.config.get<string>("provider.openai-compat.apiKey");
- const baseURL = host.config.get<string>("provider.openai-compat.baseURL");
- const model = host.config.get<string>("provider.openai-compat.model") ?? "deepseek-v4-flash";
+export async function activate(host: HostAPI): Promise<void> {
+ const auth = host.getAuthProvider("apikey");
+ if (!auth) {
+ host.logger.warn(
+ "provider-openai-compat: auth-apikey extension not available. Provider not registered.",
+ );
+ return;
+ }
- if (!apiKey) {
+ const creds = await auth.resolve();
+ if (!creds) {
host.logger.warn(
- "provider-openai-compat: no API key configured (provider.openai-compat.apiKey). Provider not registered.",
+ "provider-openai-compat: no credentials resolved from auth-apikey. Provider not registered.",
);
return;
}
- const credentials: ApiKeyCredentials = {
- type: "api-key",
- apiKey,
- ...(baseURL !== undefined ? { baseURL } : {}),
- };
+ if (creds.type !== "api-key") {
+ host.logger.warn(
+ `provider-openai-compat: expected api-key credentials but got "${creds.type}". Provider not registered.`,
+ );
+ return;
+ }
+
+ const model = host.config.get<string>("provider.openai-compat.model") ?? "deepseek-v4-flash";
- const provider = createOpenAICompatProvider({ credentials, model });
+ const provider = createOpenAICompatProvider({ credentials: creds, model });
host.defineProvider(provider);
host.logger.info(`provider-openai-compat: registered (model=${model})`);
}
diff --git a/tasks.md b/tasks.md
index f716026..e523923 100644
--- a/tasks.md
+++ b/tasks.md
@@ -54,3 +54,33 @@ drive fan-out); extension **loading is dynamic** (manifests via the host).
- host + conversation-store ran in parallel (disjoint). ✓
- session-orchestrator + transport-http ran in parallel (disjoint). ✓
- kernel-crs solo (touched shared contracts/extension.ts).
+
+---
+
+## Post-MVP backlog (orchestrator: mimo-v2.5-pro summons)
+
+### Step 1 — Wire auth → provider properly [x] DONE (verified live)
+Design: provider self-registers in `activate` (full-fidelity), so it must reach
+the `AuthContract` via the HostAPI. HostAPI exposes `getProviders`/`getTools` but
+NOT auth. Fix = small contract addition mirroring the existing provider/tool
+precedent.
+- **kernel-host** (owner of `host.ts`/`extension.ts`/`host.test.ts`): add
+ `getAuthProviders()`/`getAuthProvider(id)` to `HostAPI` + implement in
+ `buildHostAPI()` + tests. `lsp references` drives fan-out.
+- **provider-openai-compat** (owner): in `activate`, resolve creds via
+ `host.getAuthProvider("apikey").resolve()` (ApiKeyCredentials → baseURL/apiKey)
+ instead of reading `host.config` apiKey/baseURL. Add `dependsOn:["auth-apikey"]`.
+ Model stays config-driven (not a credential).
+- **host-bin** (orchestrator CR wiring): mirror the two getters in
+ `buildPostActivationHostAPI`.
+Sequence: kernel-host FIRST (adds contract surface), THEN provider (consumes it),
+THEN host-bin wiring. NOT parallel (provider depends on kernel surface).
+
+**Step 1 RESULT:** done + verified. kernel-host added `getAuthProviders`/
+`getAuthProvider` to HostAPI; provider-openai-compat `activate` now resolves creds
+via `host.getAuthProvider("apikey").resolve()` (`dependsOn:["auth-apikey"]`);
+host-bin stub mirrors the getters. 185 tests, typecheck+biome clean. Live curl
+returned a real response with auth-apikey on the path (boot log shows auth-apikey
+activates before provider; provider registered = creds resolved through contract).
+NOTE: host-bin's `buildPostActivationHostAPI` stub is slated for removal in Step 3
+(host CR-1). Summons: prompts/step1-kernel-host.md, prompts/step1-provider.md (mimo-v2.5-pro).