Add runtime event telemetry and auth-mode config hardening

This commit is contained in:
2026-02-23 17:30:53 -05:00
parent 3ca9bd3db8
commit 94c79d9dd7
10 changed files with 853 additions and 3 deletions

View File

@@ -1,6 +1,11 @@
import test from "node:test";
import assert from "node:assert/strict";
import { buildClaudeAuthEnv, loadConfig, resolveAnthropicToken } from "../src/config.js";
import {
buildClaudeAuthEnv,
loadConfig,
resolveAnthropicToken,
resolveOpenAiApiKey,
} from "../src/config.js";
test("loads defaults and freezes config", () => {
const config = loadConfig({});
@@ -11,10 +16,25 @@ test("loads defaults and freezes config", () => {
assert.equal(config.discovery.fileRelativePath, ".agent-context/resources.json");
assert.equal(config.security.violationHandling, "hard_abort");
assert.equal(config.security.commandTimeoutMs, 120000);
assert.equal(config.runtimeEvents.logPath, ".ai_ops/events/runtime-events.ndjson");
assert.equal(config.runtimeEvents.discordMinSeverity, "critical");
assert.deepEqual(config.runtimeEvents.discordAlwaysNotifyTypes, [
"session.started",
"session.completed",
"session.failed",
]);
assert.equal(config.provider.openAiAuthMode, "auto");
assert.equal(Object.isFrozen(config), true);
assert.equal(Object.isFrozen(config.orchestration), true);
});
test("validates OPENAI_AUTH_MODE values", () => {
assert.throws(
() => loadConfig({ OPENAI_AUTH_MODE: "oauth" }),
/OPENAI_AUTH_MODE must be one of/,
);
});
test("validates boolean env values", () => {
assert.throws(
() => loadConfig({ CODEX_SKIP_GIT_CHECK: "maybe" }),
@@ -29,6 +49,13 @@ test("validates security violation mode", () => {
);
});
test("validates runtime discord severity mode", () => {
assert.throws(
() => loadConfig({ AGENT_RUNTIME_DISCORD_MIN_SEVERITY: "verbose" }),
/Runtime event severity/,
);
});
test("prefers CLAUDE_CODE_OAUTH_TOKEN over ANTHROPIC_API_KEY", () => {
const config = loadConfig({
CLAUDE_CODE_OAUTH_TOKEN: "oauth-token",
@@ -57,3 +84,23 @@ test("falls back to ANTHROPIC_API_KEY when oauth token is absent", () => {
assert.equal(authEnv.CLAUDE_CODE_OAUTH_TOKEN, undefined);
assert.equal(authEnv.ANTHROPIC_API_KEY, "api-key");
});
test("resolveOpenAiApiKey respects chatgpt auth mode", () => {
const config = loadConfig({
OPENAI_AUTH_MODE: "chatgpt",
CODEX_API_KEY: "codex-key",
OPENAI_API_KEY: "openai-key",
});
assert.equal(resolveOpenAiApiKey(config.provider), undefined);
});
test("resolveOpenAiApiKey prefers CODEX_API_KEY in auto mode", () => {
const config = loadConfig({
OPENAI_AUTH_MODE: "auto",
CODEX_API_KEY: "codex-key",
OPENAI_API_KEY: "openai-key",
});
assert.equal(resolveOpenAiApiKey(config.provider), "codex-key");
});

View File

@@ -107,6 +107,53 @@ test("runCodexPrompt wires client options and parses final output", async () =>
assert.equal(closed, true);
});
test("runCodexPrompt omits apiKey when OPENAI_AUTH_MODE=chatgpt", async () => {
const config = loadConfig({
OPENAI_AUTH_MODE: "chatgpt",
CODEX_API_KEY: "codex-token",
OPENAI_API_KEY: "openai-token",
OPENAI_BASE_URL: "https://api.example.com/v1",
});
let capturedClientInput: Record<string, unknown> | undefined;
const sessionContext: SessionContext = {
provider: "codex",
sessionId: "session-codex-chatgpt",
mcp: {},
promptWithContext: "prompt with context",
runtimeInjection: {
workingDirectory: "/tmp/worktree",
env: {
HOME: "/home/tester",
},
discoveryFilePath: "/tmp/worktree/.agent-context/resources.json",
},
runInSession: async <T>(run: () => Promise<T>) => run(),
close: async () => {},
};
await runCodexPrompt("ignored", {
config,
createSessionContextFn: async () => sessionContext,
createCodexClient: (input) => {
capturedClientInput = input as Record<string, unknown>;
return {
startThread: () => ({
run: async () => ({
finalResponse: "ok",
}),
}),
};
},
writeOutput: () => {},
});
assert.equal(capturedClientInput?.["apiKey"], undefined);
assert.equal(capturedClientInput?.["baseUrl"], "https://api.example.com/v1");
assert.deepEqual(capturedClientInput?.["env"], sessionContext.runtimeInjection.env);
});
test("runClaudePrompt wires auth env, stream parsing, and output", async () => {
const config = loadConfig({
CLAUDE_CODE_OAUTH_TOKEN: "oauth-token",
@@ -193,6 +240,68 @@ test("runClaudePrompt wires auth env, stream parsing, and output", async () => {
assert.equal(closed, true);
});
test("runClaudePrompt uses ambient Claude login when no token env is configured", async () => {
const config = loadConfig({});
let queryInput:
| {
prompt: string;
options?: Record<string, unknown>;
}
| undefined;
const sessionContext: SessionContext = {
provider: "claude",
sessionId: "session-claude-no-key",
mcp: {},
promptWithContext: "augmented prompt",
runtimeInjection: {
workingDirectory: "/tmp/claude-worktree",
env: {
HOME: "/home/tester",
PATH: "/usr/bin",
},
discoveryFilePath: "/tmp/claude-worktree/.agent-context/resources.json",
},
runInSession: async <T>(run: () => Promise<T>) => run(),
close: async () => {},
};
const queryFn: ClaudeQueryFunction = ((input: {
prompt: string;
options?: Record<string, unknown>;
}) => {
queryInput = input;
const stream = createMessageStream([
{
type: "result",
subtype: "success",
result: "ok",
} as SDKMessage,
]);
return {
...stream,
close: () => {},
} as ReturnType<ClaudeQueryFunction>;
}) as ClaudeQueryFunction;
await runClaudePrompt("ignored", {
config,
createSessionContextFn: async () => sessionContext,
queryFn,
writeOutput: () => {},
});
assert.equal(queryInput?.options?.apiKey, undefined);
assert.equal(queryInput?.options?.authToken, undefined);
const env = queryInput?.options?.env as Record<string, string | undefined> | undefined;
assert.equal(env?.HOME, "/home/tester");
assert.equal(env?.CLAUDE_CODE_OAUTH_TOKEN, undefined);
assert.equal(env?.ANTHROPIC_API_KEY, undefined);
});
test("readClaudeResult throws on non-success result events", async () => {
const stream = createMessageStream([
{

View File

@@ -0,0 +1,94 @@
import test from "node:test";
import assert from "node:assert/strict";
import { mkdtemp, readFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { resolve } from "node:path";
import {
RuntimeEventPublisher,
createDiscordWebhookRuntimeEventSink,
createFileRuntimeEventSink,
} from "../src/telemetry/index.js";
test("runtime event file sink writes ndjson events", async () => {
const root = await mkdtemp(resolve(tmpdir(), "ai-ops-runtime-events-"));
const logPath = resolve(root, "runtime-events.ndjson");
const publisher = new RuntimeEventPublisher({
sinks: [createFileRuntimeEventSink(logPath)],
});
await publisher.publish({
type: "session.started",
severity: "info",
sessionId: "session-1",
message: "Session started.",
metadata: {
entryNodeId: "entry",
},
});
const lines = (await readFile(logPath, "utf8"))
.trim()
.split("\n")
.filter((line) => line.length > 0);
assert.equal(lines.length, 1);
const parsed = JSON.parse(lines[0] ?? "{}") as Record<string, unknown>;
assert.equal(parsed.type, "session.started");
assert.equal(parsed.severity, "info");
assert.equal(parsed.sessionId, "session-1");
});
test("discord runtime sink supports severity threshold and always-notify types", async () => {
const requests: Array<{
url: string;
body: Record<string, unknown>;
}> = [];
const discordSink = createDiscordWebhookRuntimeEventSink({
webhookUrl: "https://discord.example/webhook",
minSeverity: "critical",
alwaysNotifyTypes: ["session.started", "session.completed"],
fetchFn: async (url, init) => {
requests.push({
url: String(url),
body: JSON.parse(String(init?.body ?? "{}")) as Record<string, unknown>,
});
return new Response(null, { status: 204 });
},
});
const publisher = new RuntimeEventPublisher({
sinks: [discordSink],
});
await publisher.publish({
type: "session.started",
severity: "info",
sessionId: "session-1",
message: "Session started.",
});
await publisher.publish({
type: "node.attempt.completed",
severity: "warning",
sessionId: "session-1",
nodeId: "node-1",
attempt: 1,
message: "Validation failed.",
});
await publisher.publish({
type: "session.failed",
severity: "critical",
sessionId: "session-1",
message: "Session failed.",
});
assert.equal(requests.length, 2);
assert.equal(requests[0]?.url, "https://discord.example/webhook");
const firstPayload = requests[0]?.body;
assert.ok(firstPayload);
const firstEmbeds = firstPayload.embeds as Array<Record<string, unknown>>;
assert.equal(firstEmbeds[0]?.title, "session.started");
const secondPayload = requests[1]?.body;
assert.ok(secondPayload);
const secondEmbeds = secondPayload.embeds as Array<Record<string, unknown>>;
assert.equal(secondEmbeds[0]?.title, "session.failed");
});