Skip to content
Closed
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
115 changes: 75 additions & 40 deletions docs/VENDORING.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1514,13 +1514,13 @@ describe("buildOpenAISource", () => {
expect(source.baseURL).toBe("http://localhost:11434/v1");
});

test("substitutes a placeholder apiKey when none is provided (keyless)", () => {
test("substitutes a placeholder credentialId when none is provided (keyless)", () => {
const source = buildOpenAISource({
id: "local",
baseURL: "http://localhost:8080/v1",
model: "local-model",
});
expect(source.apiKey).toBe(KEYLESS_API_KEY);
expect(source.credentialId).toBe(KEYLESS_API_KEY);
});
});

Expand Down
9 changes: 9 additions & 0 deletions src/config/credential-material.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { CredentialMaterialResolver } from "@intx/types";

// Every source builder in `src/config` stores secret material directly in
// `credentialId` (API keys, OAuth access tokens, the keyless placeholder), so
// resolving a credential is an identity read. A live credential cell would
// replace this; until then the resolver echoes the id.
export const resolveInlineCredentialMaterial: CredentialMaterialResolver = (
credentialId,
) => ({ secret: credentialId });
20 changes: 10 additions & 10 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export const SOURCE_MAX_TOKENS = 16384;

// Placeholder sent in the Authorization header for keyless local providers
// (e.g. Ollama). The runtime's InferenceSource type requires a non-empty
// apiKey string; the value is injected as `Bearer <key>` by the harness but
// credentialId string; the value is injected as `Bearer <key>` by the harness but
// keyless servers ignore it entirely.
export const KEYLESS_API_KEY = "keyless";

Expand Down Expand Up @@ -259,7 +259,7 @@ export function buildOpenAISource(fields: {
baseURL: isOllamaProviderId(fields.id)
? ollamaOpenAIBaseURL(fields.baseURL)
: normalizeOpenAICompatibleBaseURL(fields.baseURL),
apiKey:
credentialId:
fields.apiKey !== undefined && fields.apiKey.length > 0
? fields.apiKey
: KEYLESS_API_KEY,
Expand Down Expand Up @@ -338,7 +338,7 @@ export function buildCodexSource(fields: {
id: fields.id,
provider: CODEX_RESPONSES_PROVIDER,
baseURL: CODEX_BASE_URL,
apiKey: fields.apiKey,
credentialId: fields.apiKey,
model: fields.model,
defaults: { maxTokens: SOURCE_MAX_TOKENS, providerOptions },
};
Expand Down Expand Up @@ -368,7 +368,7 @@ export function buildXaiSource(fields: {
id: fields.id,
provider: GROK_RESPONSES_PROVIDER,
baseURL: XAI_BASE_URL,
apiKey: fields.apiKey,
credentialId: fields.apiKey,
model: fields.model,
defaults: { maxTokens: SOURCE_MAX_TOKENS, providerOptions },
};
Expand All @@ -392,7 +392,7 @@ export function buildBifrostSource(fields: {
id: fields.id,
provider: BIFROST_PROVIDER,
baseURL: normalizeOpenAICompatibleBaseURL(fields.baseURL),
apiKey:
credentialId:
fields.apiKey !== undefined && fields.apiKey.length > 0
? fields.apiKey
: KEYLESS_API_KEY,
Expand All @@ -412,7 +412,7 @@ export function buildAnthropicSource(fields: {
id: fields.id,
provider: "anthropic",
baseURL: fields.baseURL.replace(/\/+$/, ""),
apiKey:
credentialId:
fields.apiKey !== undefined && fields.apiKey.length > 0
? fields.apiKey
: KEYLESS_API_KEY,
Expand Down Expand Up @@ -440,7 +440,7 @@ export function buildGoSource(fields: {
id: fields.id,
provider: OPENCODE_GO_MESSAGES_PROVIDER,
baseURL: endpoint.baseURL,
apiKey,
credentialId: apiKey,
model: fields.model,
defaults: {
maxTokens: SOURCE_MAX_TOKENS,
Expand All @@ -455,7 +455,7 @@ export function buildGoSource(fields: {
id: fields.id,
provider: OPENAI_RESPONSES_PROVIDER,
baseURL: endpoint.baseURL,
apiKey,
credentialId: apiKey,
model: fields.model,
defaults: {
maxTokens: SOURCE_MAX_TOKENS,
Expand Down Expand Up @@ -509,7 +509,7 @@ export function buildZenSource(fields: {
id: fields.id,
provider: ZEN_MESSAGES_PROVIDER,
baseURL: endpoint.baseURL,
apiKey,
credentialId: apiKey,
model: fields.model,
defaults: {
maxTokens: SOURCE_MAX_TOKENS,
Expand All @@ -524,7 +524,7 @@ export function buildZenSource(fields: {
id: fields.id,
provider: OPENAI_RESPONSES_PROVIDER,
baseURL: endpoint.baseURL,
apiKey,
credentialId: apiKey,
model: fields.model,
defaults: {
maxTokens: SOURCE_MAX_TOKENS,
Expand Down
2 changes: 1 addition & 1 deletion src/context-compactor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -953,7 +953,7 @@ describe("createPruningCompactor — consolidated handoff (CL-7521)", () => {
provider: "openai",
model: "test-model",
baseURL: "http://localhost:1",
apiKey: "k",
credentialId: "k",
};
let calls = 0;
const summarize = createModelSummarizer({
Expand Down
18 changes: 10 additions & 8 deletions src/exec/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -772,7 +772,7 @@ export async function runExec(config: Config): Promise<ExecResult> {
const { access } = await refreshSelectedProviderCredential(() =>
getValidCodexToken(initialCodexProfile),
);
liveSource = { ...liveSource, apiKey: access };
liveSource = { ...liveSource, credentialId: access };
liveSubAgentProvider.current = {
...liveSubAgentProvider.current,
apiKey: access,
Expand All @@ -782,7 +782,7 @@ export async function runExec(config: Config): Promise<ExecResult> {
const { access } = await refreshSelectedProviderCredential(() =>
getValidXaiToken(initialXaiProfile),
);
liveSource = { ...liveSource, apiKey: access };
liveSource = { ...liveSource, credentialId: access };
liveSubAgentProvider.current = {
...liveSubAgentProvider.current,
apiKey: access,
Expand All @@ -802,7 +802,7 @@ export async function runExec(config: Config): Promise<ExecResult> {
liveSource,
config.providers,
);
if (fresh.apiKey === liveSource.apiKey) return;
if (fresh.credentialId === liveSource.credentialId) return;
liveSource = fresh;
if (currentAgent !== null)
setAgentSourceUnlessClosed(currentAgent, fresh);
Expand Down Expand Up @@ -861,7 +861,9 @@ export async function runExec(config: Config): Promise<ExecResult> {
const sources = liveSources.length > 0 ? liveSources : [liveSource];
// Prefer liveSource credentials on the active id when OAuth was refreshed.
return sources.map((s) =>
s.id === liveSource.id ? { ...s, apiKey: liveSource.apiKey } : s,
s.id === liveSource.id
? { ...s, credentialId: liveSource.credentialId }
: s,
);
},
getDefaultSource: () =>
Expand Down Expand Up @@ -1005,15 +1007,15 @@ export async function runExec(config: Config): Promise<ExecResult> {
// Final OAuth refresh immediately before send (token may have aged during MCP).
if (initialCodexProfile !== undefined) {
const { access } = await getValidCodexToken(initialCodexProfile);
if (access !== liveSource.apiKey) {
liveSource = { ...liveSource, apiKey: access };
if (access !== liveSource.credentialId) {
liveSource = { ...liveSource, credentialId: access };
setAgentSourceUnlessClosed(activeAgent, liveSource);
}
}
if (initialXaiProfile !== undefined) {
const { access } = await getValidXaiToken(initialXaiProfile);
if (access !== liveSource.apiKey) {
liveSource = { ...liveSource, apiKey: access };
if (access !== liveSource.credentialId) {
liveSource = { ...liveSource, credentialId: access };
setAgentSourceUnlessClosed(activeAgent, liveSource);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/session/assemble-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ function stubChatAgentWiring(
id: "s",
provider: "test",
baseURL: "http://localhost",
apiKey: "k",
credentialId: "k",
model: "m",
},
],
Expand Down
2 changes: 2 additions & 0 deletions src/session/assemble-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
type LocalSettings,
} from "../config/settings.js";
import type { SessionMode } from "../config/session-mode.js";
import { resolveInlineCredentialMaterial } from "../config/credential-material.js";
import {
advertisedTools,
advertisedToolNamesForSessionMode,
Expand Down Expand Up @@ -578,6 +579,7 @@ export function assembleChatAgent(wiring: ChatAgentWiring): AssembledChatAgent {
const agent = await createAgentWithLiveToolDispatch(agentDef, {
sources: wiring.getSources(),
defaultSource: wiring.getDefaultSource(),
readCurrentMaterial: resolveInlineCredentialMaterial,
storage: storageForAgent,
workdir,
// contextTransforms ride deps: the published @intx/agent forwards deps
Expand Down
2 changes: 2 additions & 0 deletions src/session/summarizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
type RetryPolicy,
} from "@intx/types/runtime";
import { LOG_NAMESPACE_ROOT } from "../branding.js";
import { resolveInlineCredentialMaterial } from "../config/credential-material.js";
import { NOOP_TELEMETRY, type Telemetry } from "../telemetry/index.js";
import {
buildArchiveSummaryExcerpt,
Expand Down Expand Up @@ -200,6 +201,7 @@ function defaultComplete(deps: Dependencies, timeoutMs: number): CompletionFn {
signal,
nextSeq: () => seq++,
deps,
readMaterial: resolveInlineCredentialMaterial,
inferenceOptions: {
totalTimeoutMs: timeoutMs,
retryPolicy: NO_HARNESS_RETRY,
Expand Down
10 changes: 5 additions & 5 deletions src/subagent/refresh-inference-source.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import * as xaiSession from "../auth/xai/session.js";

import type { InferenceSource } from "@intx/types/runtime";

const baseSource = (id: string, apiKey = "stale"): InferenceSource => ({
const baseSource = (id: string, credentialId = "stale"): InferenceSource => ({
id,
provider: "openai",
baseURL: "https://api.openai.com/v1",
apiKey,
credentialId,
model: "gpt-4o",
});

Expand All @@ -18,15 +18,15 @@ describe("refresh-inference-source", () => {
spyOn(xaiSession, "getValidXaiToken").mockRestore();
});

test("ensureFreshInferenceSource replaces stale Codex apiKey after refresh", async () => {
test("ensureFreshInferenceSource replaces stale Codex credentialId after refresh", async () => {
spyOn(codexSession, "getValidCodexToken").mockResolvedValue({
access: "fresh-codex-token",
});
const { ensureFreshInferenceSource } =
await import("./refresh-inference-source.js");
const source = baseSource("codex/default", "stale");
const out = await ensureFreshInferenceSource(source, []);
expect(out.apiKey).toBe("fresh-codex-token");
expect(out.credentialId).toBe("fresh-codex-token");
});

test("refreshInferenceSourceBundle refreshes each leg", async () => {
Expand All @@ -53,6 +53,6 @@ describe("refresh-inference-source", () => {
apiKey: "key-abc",
},
]);
expect(out.apiKey).toBe("key-abc");
expect(out.credentialId).toBe("key-abc");
});
});
4 changes: 2 additions & 2 deletions src/subagent/refresh-inference-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ export async function ensureFreshInferenceSource(
entry?.codexProfile ?? codexProfileFromProviderName(source.id);
if (codexProfile !== undefined) {
const { access } = await getValidCodexToken(codexProfile);
return { ...source, apiKey: access };
return { ...source, credentialId: access };
}
const xaiProfile = entry?.xaiProfile ?? xaiProfileFromProviderName(source.id);
if (xaiProfile !== undefined) {
const { access } = await getValidXaiToken(xaiProfile);
return { ...source, apiKey: access };
return { ...source, credentialId: access };
}
return source;
}
Expand Down
4 changes: 2 additions & 2 deletions src/subagent/run-source.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe("buildSubAgentPrimarySource", () => {
id: "ollama/default",
provider: "openai-compatible",
baseURL: "http://localhost:11434/v1",
apiKey: KEYLESS_API_KEY,
credentialId: KEYLESS_API_KEY,
model: "qwen3",
});
});
Expand All @@ -43,7 +43,7 @@ describe("buildSubAgentPrimarySource", () => {
expect(source).toMatchObject({
id: providerName,
provider: "openai-responses",
apiKey: "sk-go",
credentialId: "sk-go",
model: "gpt-5.6-luna",
});
expect(typeof sessionId).toBe("string");
Expand Down
2 changes: 2 additions & 0 deletions src/subagent/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import {
import { createCodexReadRawFile } from "../agent/codex-read-raw-file.js";

import { isCodexProviderName } from "../config/codex-providers.js";
import { resolveInlineCredentialMaterial } from "../config/credential-material.js";
import { isOpenCodeGoProvider } from "../../packages/opencode-go/src/index.js";
import { createCompositeBlobReader } from "../agent/lazy-blob-reader.js";

Expand Down Expand Up @@ -1147,6 +1148,7 @@ async function runSubAgentInner(
agent = await createAgentWithLiveToolDispatch(def, {
sources: bundle.sources,
defaultSource: bundle.defaultSource,
readCurrentMaterial: resolveInlineCredentialMaterial,
storage,
workdir,
// contextTransforms ride deps: the published @intx/agent forwards deps
Expand Down
2 changes: 1 addition & 1 deletion src/tui/runner/exit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ const liveSource: InferenceSource = {
id: "codex/work",
provider: "openai",
baseURL: "https://example.test",
apiKey: "old-token",
credentialId: "old-token",
model: "m",
};

Expand Down
8 changes: 4 additions & 4 deletions src/tui/runner/exit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,9 +404,9 @@ export async function createRunLifecycle(
if (active === undefined) return;
const { access } = await getValidCodexToken(active.profile);
const source: InferenceSource =
access === active.source.apiKey
access === active.source.credentialId
? active.source
: { ...active.source, apiKey: access };
: { ...active.source, credentialId: access };
state.activeCodexSource = { profile: active.profile, source };
state.liveSource = source;
setAgentSourceUnlessClosed(liveAgent(state), source);
Expand All @@ -417,9 +417,9 @@ export async function createRunLifecycle(
if (active === undefined) return;
const { access } = await getValidXaiToken(active.profile);
const source: InferenceSource =
access === active.source.apiKey
access === active.source.credentialId
? active.source
: { ...active.source, apiKey: access };
: { ...active.source, credentialId: access };
state.activeXaiSource = { profile: active.profile, source };
state.liveSource = source;
setAgentSourceUnlessClosed(liveAgent(state), source);
Expand Down
2 changes: 1 addition & 1 deletion src/tui/runner/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ export async function assembleTUISession(
state.liveSource,
state.config.providers,
);
if (fresh.apiKey === state.liveSource.apiKey) return;
if (fresh.credentialId === state.liveSource.credentialId) return;
state.liveSource = fresh;
if (state.currentAgent !== undefined)
setAgentSourceUnlessClosed(state.currentAgent, fresh);
Expand Down
4 changes: 3 additions & 1 deletion tests/integration/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type {
import { type } from "arktype";

import { createAgentWithLiveToolDispatch } from "../../src/agent/live-tool-dispatch.js";
import { resolveInlineCredentialMaterial } from "../../src/config/credential-material.js";
import { createChatDirector } from "../../src/agent/director.js";
import { createAgentToolset } from "../../src/agent/tools.js";
import { ID_PREFIX } from "../../src/branding.js";
Expand Down Expand Up @@ -59,7 +60,7 @@ export const INTEGRATION_SOURCE: InferenceSource = {
id: "anthropic:claude-integration",
provider: "anthropic",
baseURL: "https://api.anthropic.com",
apiKey: "sk-integration-test",
credentialId: "sk-integration-test",
model: "claude-integration",
};

Expand Down Expand Up @@ -205,6 +206,7 @@ export async function openIntegrationSession(
const innerAgent = await startAgent(def, {
sources: [INTEGRATION_SOURCE],
defaultSource: INTEGRATION_SOURCE.id,
readCurrentMaterial: resolveInlineCredentialMaterial,
storage: storageForAgent,
workdir,
deps: {
Expand Down
2 changes: 2 additions & 0 deletions tests/integration/vendored-carry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type { ContextTransform } from "@intx/types/runtime";
import { type } from "arktype";

import { ID_PREFIX } from "../../src/branding.js";
import { resolveInlineCredentialMaterial } from "../../src/config/credential-material.js";
import { createPermissionGate } from "../../src/permission/gate.js";
import { createOptimizedContextStore } from "../../src/session/optimized-context-store.js";
import {
Expand Down Expand Up @@ -151,6 +152,7 @@ describe("integration — vendored feature carry", () => {
const agent = await createAgent(def, {
sources: [INTEGRATION_SOURCE],
defaultSource: INTEGRATION_SOURCE.id,
readCurrentMaterial: resolveInlineCredentialMaterial,
storage,
workdir,
deps: harness.deps,
Expand Down
Loading
Loading