Skip to content

Commit e278aff

Browse files
committed
refactor: take each run's route from its program and drop the global debug sink
Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589
1 parent dd2c148 commit e278aff

78 files changed

Lines changed: 855 additions & 1008 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎src/agent/agent-interface.ts‎

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,24 @@ import { randomUUID } from 'node:crypto';
66

77
import path from 'path';
88
import * as os from 'os';
9+
// eslint-disable-next-line no-restricted-imports -- resolves the SDK's bundled CLI path where ESM has no bare require
910
import { createRequire } from 'node:module';
1011
import type {
1112
ProgressEmitter,
1213
SpinnerHandle,
1314
TokenUsageDelta,
1415
} from './progress';
15-
import { debug, logToFile, initLogFile, getLogFilePath } from '@utils/debug';
16+
import {
17+
formatLogLine,
18+
logToFile,
19+
initLogFile,
20+
getLogFilePath,
21+
} from '@utils/debug';
1622
import type { WizardRunOptions } from '@utils/types';
1723
import { analytics } from '@utils/analytics';
1824
import { isTemplateEnvFileName } from '@utils/env-scan';
1925
import { runtimeEnv } from '@env';
20-
import type { AioCapture } from '@agent/aio-capture';
26+
import type { AioCapture } from './aio-capture';
2127
import {
2228
Harness,
2329
CallType,
@@ -36,14 +42,14 @@ import {
3642
gatewayAuth,
3743
isPastRefresh,
3844
type GatewayAuth,
39-
} from '@agent/gateway-session';
45+
} from './gateway-session';
4046
import { evaluateBashCommand } from './bash-fence';
41-
import { createWizardToolsServer, WIZARD_TOOL_NAMES } from '@agent/tools';
47+
import { createWizardToolsServer, WIZARD_TOOL_NAMES } from './tools';
4248
import {
4349
createPreToolUseYaraHooks,
4450
createPostToolUseYaraHooks,
4551
prewarmYaraScanner,
46-
} from '@agent/yara-hooks';
52+
} from './yara-hooks';
4753
import { createTriageLLMProvider } from './triage-provider';
4854
import type { LLMProvider } from '@posthog/warlock';
4955
import { assembleCommandments } from './runner/switchboard/commandments';
@@ -91,11 +97,8 @@ async function getSDKModule(): Promise<any> {
9197
* This ensures we use the SDK's bundled version rather than the user's installed Claude Code.
9298
*/
9399
function getClaudeCodeExecutablePath(): string {
94-
// Bare `require` is undefined in ESM (tsx dev runs) — fall back to createRequire.
95-
const resolver =
96-
typeof require !== 'undefined'
97-
? require
98-
: createRequire(process.argv[1] ?? `${process.cwd()}/`);
100+
// ESM has no bare `require`: resolve from the entry script.
101+
const resolver = createRequire(process.argv[1] ?? `${process.cwd()}/`);
99102
// resolve finds the package's main entry, then we get cli.js from same dir
100103
const sdkPackagePath = resolver.resolve('@anthropic-ai/claude-agent-sdk');
101104
return path.join(path.dirname(sdkPackagePath), 'cli.js');
@@ -221,7 +224,7 @@ export type AgentConfig = {
221224
/** Schema for callers that consume a structured SDK result. */
222225
outputFormat?: import('@anthropic-ai/claude-agent-sdk').Options['outputFormat'];
223226
/** Bridge that drives the `wizard_ask` overlay. Omit in non-interactive hosts. */
224-
askBridge?: import('@agent/wizard-ask-bridge').WizardAskBridge;
227+
askBridge?: import('./wizard-ask-bridge').WizardAskBridge;
225228
/** Per-run cap on `wizard_ask` invocations. Defaults to 10. */
226229
askMaxQuestions?: number;
227230
/** Extra tools added on top of BASE_ALLOWED_TOOLS for this run. */
@@ -239,7 +242,7 @@ export type AgentConfig = {
239242
* flag routes the run here; threaded into wizard-tools so the orchestrator
240243
* tools register.
241244
*/
242-
orchestrator?: import('@agent/runner/sequence/orchestrator/queue-tools').OrchestratorToolsContext;
245+
orchestrator?: import('./runner/sequence/orchestrator/queue-tools').OrchestratorToolsContext;
243246
/**
244247
* Optional AIO capture — mirrors each assistant SDK message into the
245248
* authenticated project as `$ai_generation`. No-op instance when
@@ -417,6 +420,16 @@ export function buildAgentEnv(
417420
// Re-export for backwards compatibility — canonical source is skill-install.ts
418421
export { isSkillInstallCommand } from '@shared/skill-install';
419422

423+
/** A debug run's diagnostic line, as info log progress; a run without `debug` emits nothing. */
424+
function debugLine(
425+
emit: ProgressEmitter,
426+
options: Pick<WizardRunOptions, 'debug'>,
427+
...args: unknown[]
428+
): void {
429+
if (!options.debug) return;
430+
emit({ kind: 'log', level: 'info', message: formatLogLine(...args) });
431+
}
432+
420433
/**
421434
* Permission hook that allows only safe commands. Bash commands are gated by
422435
* the exact per-manager fence in bash-fence.ts (install/build/typecheck/lint
@@ -435,6 +448,8 @@ export function wizardCanUseTool(
435448
readOnly?: boolean;
436449
wizardAskPending?: boolean;
437450
disallowedTools?: readonly string[];
451+
/** A debug run's line for each Bash decision. */
452+
onDebug?: (line: string) => void;
438453
} = {},
439454
):
440455
| { behavior: 'allow'; updatedInput: Record<string, unknown> }
@@ -516,12 +531,14 @@ export function wizardCanUseTool(
516531
const decision = evaluateBashCommand(command);
517532
if (decision.allowed) {
518533
logToFile(`Allowing bash command: ${command}`);
519-
debug(`Allowing bash command: ${command}`);
534+
context.onDebug?.(`Allowing bash command: ${command}`);
520535
return { behavior: 'allow', updatedInput: input };
521536
}
522537

523538
logToFile(`Denying bash command (${decision.analyticsReason}): ${command}`);
524-
debug(`Denying bash command (${decision.analyticsReason}): ${command}`);
539+
context.onDebug?.(
540+
`Denying bash command (${decision.analyticsReason}): ${command}`,
541+
);
525542
analytics.wizardCapture('bash denied', {
526543
reason: decision.analyticsReason,
527544
command,
@@ -689,14 +706,12 @@ export async function initializeAgent(
689706
apiKeyPresent: !!config.posthogApiKey,
690707
});
691708

692-
if (options.debug) {
693-
debug('Agent config:', {
694-
workingDirectory: agentRunConfig.workingDirectory,
695-
posthogMcpUrl: config.posthogMcpUrl,
696-
gatewayUrl,
697-
apiKeyPresent: !!config.posthogApiKey,
698-
});
699-
}
709+
debugLine(emit, options, 'Agent config:', {
710+
workingDirectory: agentRunConfig.workingDirectory,
711+
posthogMcpUrl: config.posthogMcpUrl,
712+
gatewayUrl,
713+
apiKeyPresent: !!config.posthogApiKey,
714+
});
700715

701716
// Pre-warm the warlock scanner (WASM init + rule compile) off the hook path
702717
// so the first tool-call scan doesn't pay cold-start under a hook timeout.
@@ -714,7 +729,7 @@ export async function initializeAgent(
714729
message: `Failed to initialize agent: ${(error as Error).message}`,
715730
});
716731
logToFile('Agent initialization error:', error);
717-
debug('Agent initialization error:', error);
732+
debugLine(emit, options, 'Agent initialization error:', error);
718733
throw error;
719734
}
720735
}
@@ -1181,6 +1196,7 @@ export async function runAgent(
11811196
readOnly: agentConfig.readOnly,
11821197
wizardAskPending: agentConfig.getPendingQuestion?.() != null,
11831198
disallowedTools: agentConfig.disallowedTools,
1199+
onDebug: (line) => debugLine(emit, options, line),
11841200
},
11851201
);
11861202
logToFile('canUseTool result:', result);
@@ -1204,9 +1220,7 @@ export async function runAgent(
12041220
// Capture stderr from CLI subprocess for debugging
12051221
stderr: (data: string) => {
12061222
logToFile('CLI stderr:', data);
1207-
if (options.debug) {
1208-
debug('CLI stderr:', data);
1209-
}
1223+
debugLine(emit, options, 'CLI stderr:', data);
12101224
},
12111225
// Stop hook: collect remark, then allow stop
12121226
hooks: {
@@ -1672,7 +1686,7 @@ export async function runAgent(
16721686
message: `Error: ${(error as Error).message}`,
16731687
});
16741688
logToFile('Agent run failed:', error);
1675-
debug('Full error:', error);
1689+
debugLine(emit, options, 'Full error:', error);
16761690
throw error;
16771691
} finally {
16781692
agentConfig.signal?.removeEventListener('abort', onExternalAbort);
@@ -2042,9 +2056,7 @@ function handleSDKMessage(
20422056
};
20432057
logToFile(`SDK Message: ${message.type}`, JSON.stringify(message, null, 2));
20442058

2045-
if (options.debug) {
2046-
debug(`SDK Message type: ${message.type}`);
2047-
}
2059+
debugLine(emit, options, `SDK Message type: ${message.type}`);
20482060

20492061
switch (message.type) {
20502062
case 'assistant': {
@@ -2227,9 +2239,7 @@ function handleSDKMessage(
22272239

22282240
default:
22292241
// Log other message types for debugging
2230-
if (options.debug) {
2231-
debug(`Unhandled message type: ${message.type}`);
2232-
}
2242+
debugLine(emit, options, `Unhandled message type: ${message.type}`);
22332243
break;
22342244
}
22352245
}

‎src/agent/agent-prompt-loader.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
import { logToFile } from '@utils/debug';
2929
import { analytics } from '@utils/analytics';
3030
import { fetchWithRetry } from '@shared/fetch-retry';
31-
import { WIZARD_TOOL_NAMES } from '@agent/tools/tools';
31+
import { WIZARD_TOOL_NAMES } from './tools/tools';
3232

3333
/**
3434
* The basics the client injects around every agent-prompt body. The `/agents/`

‎src/agent/bash-fence.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* (`npx <anything>` downloads and runs it), and shell injection. Matching is
1414
* token-exact per manager — keyword prefixes admitted `npm publish` via `pub`.
1515
*/
16-
import { LINTING_TOOLS } from '@agent/safe-tools';
16+
import { LINTING_TOOLS } from './safe-tools';
1717

1818
export type BashFenceDecision =
1919
| { allowed: true }

‎src/agent/gateway-session.ts‎

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,15 @@
66
* unattributed money to hide an outage.
77
*/
88

9-
import { readFileSync } from 'node:fs';
109
import { logToFile } from '@utils/debug';
1110
import { analytics } from '@utils/analytics';
1211
import { ErrorCodes, WizardError } from '@shared/errors';
12+
import type { GatewayCredential } from '@shared/api';
1313
import type { HostResolution } from '@shared/host-resolution';
1414
import { oauthLoginKey } from '@shared/oauth-session';
1515
import { checkLlmGatewayHealth } from '@shared/health-checks/endpoints';
1616
import { ServiceHealthStatus } from '@shared/health-checks/types';
17-
import { IS_PRODUCTION_BUILD, runtimeEnv } from '@env';
18-
import type { CloudRegion } from '@utils/types';
17+
import { IS_PRODUCTION_BUILD } from '@env';
1918

2019
export interface GatewayAuth {
2120
/** Base URL for model calls (no `/v1`; transports append their route). */
@@ -73,25 +72,16 @@ export function configureGatewayCredentialsForCI(
7372
};
7473
}
7574

76-
// TODO: CI credential loading belongs outside the agent. It leaves with the rest
77-
// of this module once RunInput carries resolved inference auth, later in the
78-
// refactor.
79-
export function configureGatewayFromCIEnvironment(
75+
/** Use this run's pre-issued gateway token, or mint when it has none; the keyed mint cache stays. */
76+
export function useRunGatewayCredential(
77+
gateway: GatewayCredential | undefined,
8078
projectId: number,
81-
region: CloudRegion,
8279
): void {
83-
if (IS_PRODUCTION_BUILD)
84-
throw new Error('CI gateway auth requires a non-production build');
85-
const path = runtimeEnv('WIZARD_CI_GATEWAY_TOKEN_FILE');
86-
if (!path) throw new Error('WIZARD_CI_GATEWAY_TOKEN_FILE is required for CI');
87-
const token = readFileSync(path, 'utf8');
88-
delete process.env.WIZARD_CI_GATEWAY_TOKEN_FILE;
89-
configureGatewayCredentialsForCI(
90-
token,
91-
projectId,
92-
runtimeEnv('WIZARD_CI_GATEWAY_URL') ||
93-
`https://ai-gateway.${region}.posthog.com`,
94-
);
80+
if (gateway) {
81+
configureGatewayCredentialsForCI(gateway.token, projectId, gateway.url);
82+
return;
83+
}
84+
ciAuth = null;
9585
}
9686

9787
/**

‎src/agent/index.ts‎

Lines changed: 28 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
/**
22
* Public entry of the agent. Code outside `src/agent` imports runtime values
3-
* from here and types from `./types`; deeper imports fail lint and the
4-
* architecture test. Keep this list to what callers use, and keep it cheap:
3+
* from here and types from `./types`; deeper imports fail `pnpm typecheck`.
4+
* Keep this list to what callers use, and keep it cheap:
55
* the startup chunk imports this module, so anything re-exported here loads
66
* before the wizard does any work. Heavy paths stay behind a lazy import.
7-
*
8-
* Grouped by fate, per the stack plan (sections 4.1 to 4.5 and 7).
97
*/
8+
import type { LLMProvider } from '@posthog/warlock';
109

1110
/**
12-
* Stays. The agent's contract: the one way to run it, the marker strings
11+
* The agent's contract: the one way to run it, the marker strings
1312
* program prompts embed, and the tool ids programs put in allowedTools and
1413
* disallowedTools.
1514
*/
@@ -18,45 +17,35 @@ export { runAgent, RunOutcome } from './runner';
1817
export { AgentSignals } from './agent-interface';
1918
export { WIZARD_TOOL_NAMES } from './tools';
2019

21-
/**
22-
* Leaves when the bindings move to programs. Bindings and program data move to
23-
* programs: resolveBinding
24-
* is keyed by PROGRAM_BINDINGS and the agent keeps only "run from an
25-
* already-resolved binding"; shouldDisableAsk is a flags policy programs
26-
* decide and pass in; LONGER_ASK_TIMEOUT_MS is a tuning number programs own
27-
* as askTimeoutMs.
28-
*/
29-
export {
30-
resolveBinding,
31-
resolveScanBindings,
32-
shouldDisableAsk,
33-
} from './runner';
34-
export { captureSwitchboardDecision } from './runner/switchboard/resolve-run';
35-
export { LONGER_ASK_TIMEOUT_MS } from './wizard-ask-bridge';
20+
/** The binding a program gets when it declares none. */
21+
export { DEFAULT_BINDING } from './runner';
22+
23+
/** What a scan's matches decide: end the session or only warn. The Warlock release gate reads it. */
24+
export { scanVerdict } from './yara-policy';
3625

3726
/**
38-
* Leaves later in the refactor. buildRunTags builds the trace tags runProgram
39-
* and agentic detection send. configureGatewayFromCIEnvironment loads the CI
40-
* gateway token. flushScanReport becomes a progress event rather than a call.
41-
* downloadSkill leaves once skill install becomes shared.
27+
* The scan a freshly installed skill directory goes through: a returned reason
28+
* means it is poisoned. Callers of `downloadSkill` inject it, since @shared
29+
* cannot import @agent. It loads the scanner on first call so the startup
30+
* chunk does not grow.
4231
*/
43-
export { buildRunTags } from './agent-interface';
44-
export { configureGatewayFromCIEnvironment } from './gateway-session';
45-
export { flushScanReport } from './yara-hooks';
46-
export { downloadSkill } from './tools';
47-
/** The frameworkContext slot the legacy adapter fills for the e2e harness. */
48-
export { TASK_OUTCOMES_KEY } from './runner';
32+
export async function scanInstalledSkill(
33+
skillDir: string,
34+
llmProvider: LLMProvider | undefined,
35+
): Promise<string | null> {
36+
const yara = await import('./yara-hooks');
37+
return yara.scanInstalledSkill(skillDir, llmProvider);
38+
}
4939

5040
/**
51-
* Leaves in C2. The TUI receives agent data through program state. Until
52-
* then the suggested-prompts screen streams through this wrapper, which loads
53-
* the streaming module on first call so the startup chunk does not grow.
41+
* The MCP tutorial's prompt stream: one prompt against the PostHog MCP server,
42+
* streamed as chunks. `@tools` re-exports it unchanged, and the TUI reaches it
43+
* only through there. It loads the streaming module on first call so the
44+
* startup chunk does not grow.
5445
*/
55-
export async function* runMcpPromptViaSdk(
56-
args: Parameters<
57-
typeof import('./mcp-prompt-streaming').runMcpPromptViaSdk
58-
>[0],
59-
): AsyncIterable<import('./types').AgentChunk> {
46+
export async function* streamMcpPrompt(
47+
args: Parameters<typeof import('./mcp-prompt-streaming').streamMcpPrompt>[0],
48+
): AsyncIterable<import('./types').McpPromptChunk> {
6049
const streaming = await import('./mcp-prompt-streaming');
61-
yield* streaming.runMcpPromptViaSdk(args);
50+
yield* streaming.streamMcpPrompt(args);
6251
}

0 commit comments

Comments
 (0)