fix: give sub-agents the provider their pinned model needs

Sub-agent sessions are created with `noExtensions: true`, which also
removes every provider an extension registered through
`pi.registerProvider` from the sub-session's own ModelRuntime. Since
bff187d pinned sub-agents to the invoking session's model, every check
depends on a provider registration it deliberately does not load, so
scans died in about 20ms with "No API key found for neuralwatt" on a
perfectly healthy login — the key was in auth.json, the provider simply
did not exist in that session's runtime.

Thread the invoking session's registry down to the runner and
re-register just the pinned model's provider there, through an inline
extension factory: inline factories load regardless of `noExtensions`,
so the sub-agent authenticates exactly as the host does while every
other extension stays out of its scope. Registrations are narrowed once
at the extension entry point (`asProviderRegistry`) and relayed
untouched — a native provider keeps its own stream and auth wiring.

The generated omp port keeps its isolation: it disables extension
discovery and has no extension registrations to forward, so it carries
the same shapes with a stub for the helper.

Tests: new tests/provider-forwarding.test.ts pins the narrowing and the
relay (native wins over config-only, nothing forwarded for a built-in);
it fails on the previous tree, where the helper does not exist. Full
suite 162 pass with one pre-existing failure (MISC-46, worktree-only).
bun run typecheck clean; port-to-omp regenerates and typechecks.

MISC-45

Co-Authored-By: nw-flash-flex <noreply@neuralwatt.com>
This commit is contained in:
2026-10-20 17:43:54 -04:00
parent d29ee5ce46
commit 6213281b3c
8 changed files with 269 additions and 3 deletions

View File

@@ -0,0 +1,105 @@
/**
* provider-forwarding.test.ts — the provider hand-off from the invoking session
* into a sub-agent session.
*
* A sub-agent session is created with `noExtensions: true`, so a provider that
* exists only because an extension called `pi.registerProvider` is absent from
* its `ModelRuntime` and its credentials cannot be resolved — pinning the
* invoking session's model then fails with `No API key found for <provider>`
* before a single token, on a perfectly healthy login. These tests pin the two
* halves of the fix: narrowing the host registry at the boundary, and the inline
* extension that re-registers the pinned model's provider inside the sub-session
* (inline factories load even under `noExtensions`).
*/
import { describe, expect, it } from "bun:test";
import {
asProviderRegistry,
providerForwardingExtension,
type ProviderRegistry,
} from "../src/agent-runner.js";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
/** Record `registerProvider` calls so the relay can be asserted on. */
function recorder(): {
pi: ExtensionAPI;
calls: unknown[][];
} {
const calls: unknown[][] = [];
const pi = {
registerProvider: (...args: unknown[]) => {
calls.push(args);
},
} as unknown as ExtensionAPI;
return { pi, calls };
}
/** Registry stub holding one provider id, native and/or config form. */
function registry(entry: {
native?: unknown;
config?: unknown;
}): ProviderRegistry {
return {
getRegisteredNativeProvider: (_id) =>
entry.native === undefined ? undefined : entry.native,
getRegisteredProviderConfig: (_id) =>
entry.config === undefined ? undefined : entry.config,
};
}
describe("asProviderRegistry", () => {
it("accepts a registry that exposes both registration lookups", () => {
const host = registry({ native: { id: "neuralwatt" } });
expect(asProviderRegistry(host)).toBe(host);
});
it("rejects values that cannot answer both lookups", () => {
expect(asProviderRegistry(undefined)).toBeUndefined();
expect(asProviderRegistry(null)).toBeUndefined();
expect(asProviderRegistry("modelRegistry")).toBeUndefined();
// A registry from a host that never records extension providers, such as
// the omp port, degrades to "nothing to forward" instead of throwing.
expect(asProviderRegistry({})).toBeUndefined();
expect(
asProviderRegistry({ getRegisteredNativeProvider: () => undefined }),
).toBeUndefined();
});
});
describe("providerForwardingExtension", () => {
it("is a no-op without a registry, a pinned model, or a registered provider", () => {
// Built-in and models.json providers need no forwarding: pi's own
// catalogue already answers for them.
const empty = registry({});
expect(providerForwardingExtension(undefined, "neuralwatt")).toBeUndefined();
expect(providerForwardingExtension(empty, undefined)).toBeUndefined();
expect(providerForwardingExtension(empty, "neuralwatt")).toBeUndefined();
});
it("re-registers a native provider, keeping its own stream and auth wiring", () => {
const native = { id: "neuralwatt" };
const extension = providerForwardingExtension(
registry({ native, config: { baseUrl: "ignored-when-native" } }),
"neuralwatt",
);
if (!extension) throw new Error("expected a forwarding extension");
expect(extension.name).toBe("pygienium-provider");
// Hidden: the sub-agent's extension list must not advertise an extension
// the user never installed for it.
expect(extension.hidden).toBe(true);
const { pi, calls } = recorder();
extension.factory(pi);
// The native provider wins over a config-only registration, which would
// only augment a built-in provider and lose the wrapped stream.
expect(calls).toEqual([[native]]);
});
it("re-registers a config-only provider under its provider id", () => {
const config = { baseUrl: "config-only-provider" };
const extension = providerForwardingExtension(registry({ config }), "probe");
if (!extension) throw new Error("expected a forwarding extension");
const { pi, calls } = recorder();
extension.factory(pi);
expect(calls).toEqual([["probe", config]]);
});
});