Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -289,3 +289,14 @@ ALLOW_UNVERIFIED_EMAILS=1
# inside it.
# HUB_SIDECAR_WEBSOCKET_URL=

# SIDECAR_ADAPTER_MANIFEST configures custom Interchange inference adapters
# for a sidecar process, overriding built-in adapters that share a provider
# key (@intx/inference's loadAdapterRegistry). Leave unset (the default) to
# run the built-ins only. The value is a JSON array of
# {"provider","specifier","export"} entries; each specifier must resolve
# from the sidecar's own module-resolution root (an installed package, not
# a bare file path), and every workflow-process child it spawns resolves
# the same manifest. Example activating @corbits/ollama-adapter for the
# "ollama" provider key:
# SIDECAR_ADAPTER_MANIFEST=[{"provider":"ollama","specifier":"@corbits/ollama-adapter","export":"createOllamaAdapter"}]

48 changes: 48 additions & 0 deletions apps/sidecar/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

import { type } from "arktype";

import { AdapterManifest } from "@intx/inference";

import { parseToolRegistries } from "./tool-materialization";

const WsURL = type("string").narrow((url, ctx) => {
Expand Down Expand Up @@ -38,6 +40,15 @@ const SidecarEnv = type({
// workflow-process child's spawn env so per-step tool
// materialization resolves the exact registries the operator pinned.
"SIDECAR_TOOL_REGISTRIES?": "string",
// Optional JSON-encoded custom inference adapter manifest
// (`AdapterManifestEntry[]`, `[{"provider","specifier","export"}]`).
// Unset means no custom adapters -- `loadAdapterRegistry` resolves the
// built-ins only. Validated here so a malformed manifest kills the boot
// with the variable named, and threaded (as its parsed form) into both
// this process's own adapter registry and every workflow-process
// child's `SIDECAR_ADAPTER_MANIFEST` substrate-config entry, so a child
// resolves the exact custom adapters this boot edge resolved.
"SIDECAR_ADAPTER_MANIFEST?": "string",
// Operator overrides for two workflow-supervisor timing bindings,
// threaded verbatim to every deployment's supervisor
// (`createSidecarWorkflowSupervisor`'s `consumedRetentionMs` /
Expand Down Expand Up @@ -83,6 +94,12 @@ export type SidecarConfig = {
* default).
*/
readonly toolRegistries: string | undefined;
/**
* The operator's custom inference adapter manifest, already validated
* against {@link AdapterManifest}. Empty when the operator configured
* none -- `loadAdapterRegistry([])` then resolves the built-ins only.
*/
readonly adapterManifest: AdapterManifest;
/**
* Consumed-dedup retention horizon (ms), forwarded verbatim to every
* deployment's supervisor. `undefined` means the operator did not
Expand All @@ -98,6 +115,34 @@ export type SidecarConfig = {
readonly readyTimeoutMs: number | undefined;
};

/**
* Parse the optional `SIDECAR_ADAPTER_MANIFEST` env value into a validated
* {@link AdapterManifest}. Unset resolves to `[]` (no custom adapters);
* a malformed value dies at boot with the variable named, rather than
* surfacing as a deep-stack `loadAdapterRegistry` import failure.
*/
export function parseSidecarAdapterManifest(
raw: string | undefined,
): AdapterManifest {
if (raw === undefined) return [];
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch (cause) {
throw new Error(
"invalid sidecar environment: SIDECAR_ADAPTER_MANIFEST is not valid JSON",
{ cause },
);
}
const validated = AdapterManifest(parsed);
if (validated instanceof type.errors) {
throw new Error(
`invalid sidecar environment: SIDECAR_ADAPTER_MANIFEST failed validation: ${validated.summary}`,
);
}
return validated;
}

/**
* Parse the sidecar's configuration out of an environment map. Throws at
* the call site when any variable is missing or malformed, naming the
Expand Down Expand Up @@ -126,6 +171,9 @@ export function readSidecarConfig(
home: parsed.HOME,
tmpdir: parsed.TMPDIR,
toolRegistries: parsed.SIDECAR_TOOL_REGISTRIES,
adapterManifest: parseSidecarAdapterManifest(
parsed.SIDECAR_ADAPTER_MANIFEST,
),
consumedRetentionMs: parsePositiveMsEnv(
parsed.CONSUMED_RETENTION_MS,
"CONSUMED_RETENTION_MS",
Expand Down
15 changes: 10 additions & 5 deletions apps/sidecar/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,12 @@ const CACHE_ROOT = path.join(config.dataDir, "cache", "tarballs");
const CACHE_MAX_BYTES = 10 * 1024 * 1024 * 1024;
const REGISTRY_MAX_TARBALL_BYTES = 10 * 1024 * 1024;

// Inference adapters are the statically-linked built-ins. Custom
// adapter modules are code; installing one means installing a package
// into this workspace, not naming a specifier in the environment.
const adapters = await loadAdapterRegistry([]);
// Built-in adapters merged with any operator-configured custom adapters
// named in `SIDECAR_ADAPTER_MANIFEST`. Installing a custom adapter still
// means installing its package into this workspace -- the manifest only
// names an already-installed module's specifier and export, it never
// carries code of its own.
const adapters = await loadAdapterRegistry(config.adapterManifest);

// Sweep any tmp staging directories left behind by a tarball put or
// extract that crashed between staging and the final rename on a
Expand Down Expand Up @@ -171,7 +173,10 @@ const multistepSubstrateEnv: Record<string, string> = {
PATH: config.path,
SIDECAR_CACHE_MAX_BYTES: String(CACHE_MAX_BYTES),
SIDECAR_REGISTRY_MAX_TARBALL_BYTES: String(REGISTRY_MAX_TARBALL_BYTES),
SIDECAR_ADAPTER_MANIFEST: JSON.stringify([]),
// Threaded verbatim from this boot edge's own resolved manifest so a
// workflow-process child resolves the exact custom adapters this
// process resolved -- never a default of its own.
SIDECAR_ADAPTER_MANIFEST: JSON.stringify(config.adapterManifest),
// Always serialized, defaulting to the public npmjs registry when the
// operator pinned none, so the child's per-step tool materialization
// resolves the exact registries this boot edge resolved — a child
Expand Down
31 changes: 31 additions & 0 deletions apps/sidecar/test/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,42 @@ test("parses a complete environment into config", () => {
home: undefined,
tmpdir: undefined,
toolRegistries: undefined,
adapterManifest: [],
consumedRetentionMs: undefined,
readyTimeoutMs: undefined,
});
});

test("carries a valid adapter manifest through as its parsed form", () => {
const manifest = [
{
provider: "ollama",
specifier: "@corbits/ollama-adapter",
export: "createOllamaAdapter",
},
];
const config = readSidecarConfig({
...VALID_ENV,
SIDECAR_ADAPTER_MANIFEST: JSON.stringify(manifest),
});
expect(config.adapterManifest).toEqual(manifest);
});

test("a malformed adapter manifest fails boot naming the variable", () => {
expect(() =>
readSidecarConfig({ ...VALID_ENV, SIDECAR_ADAPTER_MANIFEST: "{not json" }),
).toThrow(/SIDECAR_ADAPTER_MANIFEST/);
});

test("an adapter manifest entry missing a required field fails boot", () => {
expect(() =>
readSidecarConfig({
...VALID_ENV,
SIDECAR_ADAPTER_MANIFEST: JSON.stringify([{ provider: "ollama" }]),
}),
).toThrow(/SIDECAR_ADAPTER_MANIFEST/);
});

test("carries operator overrides for consumedRetentionMs/readyTimeoutMs through when present", () => {
const config = readSidecarConfig({
...VALID_ENV,
Expand Down
21 changes: 16 additions & 5 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading