Skip to content

Commit 1f4c264

Browse files
andymac4182claude
andcommitted
Add ACP runtime support
Add an Agent Client Protocol runtime backed by @agentclientprotocol/sdk, including custom command and registry-based agent resolution. Thread ACP options through CLI, action, schedule, extraction repair, synthesis, fix-quality, and JSON repair paths so runtime=acp keeps model-backed execution on ACP. Document the new config and add coverage for runtime registration, config resolution, structured helper calls, and usage aggregation. Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
1 parent e785b6d commit 1f4c264

21 files changed

Lines changed: 933 additions & 28 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
},
5555
"homepage": "https://warden.sentry.dev",
5656
"dependencies": {
57+
"@agentclientprotocol/sdk": "^0.21.0",
5758
"@anthropic-ai/claude-agent-sdk": "^0.2.22",
5859
"@anthropic-ai/sdk": "^0.72.1",
5960
"@inquirer/select": "^5.0.4",

packages/docs/src/pages/config.astro

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,27 @@ model = "claude-opus-4-5"`}
223223
<p><strong>Model precedence:</strong> trigger &gt; skill &gt; defaults &gt; CLI flag (<code>-m</code>) &gt; env var (<code>WARDEN_MODEL</code>). Most specific wins.</p>
224224
<p><strong>Synthesis fallback:</strong> <code>defaults.synthesis.model</code> falls back to <code>defaults.auxiliary.model</code> when not set.</p>
225225

226+
<h3>Agent Client Protocol runtime</h3>
227+
228+
<p>Set <code>defaults.runtime</code> to <code>acp</code> to run hunk analysis through an ACP-compatible agent instead of Claude Code SDK. Use a custom command or a registry agent id.</p>
229+
230+
<Terminal showCopy={true}>
231+
<Code
232+
code={`[defaults]
233+
runtime = "acp"
234+
235+
[defaults.agent.acp]
236+
command = "atlas alta agent run"
237+
238+
# Or resolve an agent from the ACP registry:
239+
# registryId = "amp-acp"`}
240+
lang="toml"
241+
theme="vitesse-black"
242+
/>
243+
</Terminal>
244+
245+
<p>Registry agents are resolved from <code>https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json</code> by default. Analysis, extraction repair, consolidation, and fix-quality helper calls use the selected ACP agent.</p>
246+
226247
<h2 id="chunking">Chunking</h2>
227248

228249
<p>Control how files are split for analysis. By default, Warden analyzes each hunk separately.</p>

pnpm-lock.yaml

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/action/triggers/executor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ export async function executeTrigger(
136136
apiKey: anthropicApiKey,
137137
model: trigger.model,
138138
runtime: trigger.runtime,
139+
acp: trigger.acp,
139140
auxiliaryModel: trigger.auxiliaryModel,
140141
synthesisModel: trigger.synthesisModel,
141142
maxTurns: trigger.maxTurns,

src/action/workflow/schedule.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ export async function runScheduleWorkflow(
176176
apiKey: inputs.anthropicApiKey,
177177
model: resolved.model,
178178
runtime: resolved.runtime,
179+
acp: resolved.acp,
179180
auxiliaryModel: resolved.auxiliaryModel,
180181
synthesisModel: resolved.synthesisModel,
181182
maxTurns: resolved.maxTurns,

src/cli/main.ts

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,7 @@ interface SkillToRun {
482482
model?: string;
483483
maxTurns?: number;
484484
runtime?: SkillRunnerOptions['runtime'];
485+
acp?: SkillRunnerOptions['acp'];
485486
auxiliaryModel?: string;
486487
synthesisModel?: string;
487488
auxiliaryMaxRetries?: number;
@@ -507,7 +508,7 @@ interface ProcessedResults {
507508

508509
type SkillRunnerOptionOverrides = Pick<
509510
SkillRunnerOptions,
510-
'model' | 'maxTurns' | 'runtime' | 'auxiliaryModel' | 'synthesisModel' | 'auxiliaryMaxRetries'
511+
'model' | 'maxTurns' | 'runtime' | 'acp' | 'auxiliaryModel' | 'synthesisModel' | 'auxiliaryMaxRetries'
511512
>;
512513

513514
/** Apply per-skill runner overrides on top of the shared execution defaults. */
@@ -520,6 +521,7 @@ export function mergeSkillRunnerOptions(
520521
if (overrides.model !== undefined) merged.model = overrides.model;
521522
if (overrides.maxTurns !== undefined) merged.maxTurns = overrides.maxTurns;
522523
if (overrides.runtime !== undefined) merged.runtime = overrides.runtime;
524+
if (overrides.acp !== undefined) merged.acp = overrides.acp;
523525
if (overrides.auxiliaryModel !== undefined) merged.auxiliaryModel = overrides.auxiliaryModel;
524526
if (overrides.synthesisModel !== undefined) merged.synthesisModel = overrides.synthesisModel;
525527
if (overrides.auxiliaryMaxRetries !== undefined) {
@@ -819,20 +821,6 @@ export async function runSkills(
819821
// Not in a git repo - that's fine for file mode
820822
}
821823

822-
// Pre-flight: verify auth will work before starting analysis
823-
try {
824-
verifyAuth({ apiKey });
825-
} catch (error: unknown) {
826-
const message = (error as WardenAuthenticationError).message;
827-
reporter.error(message);
828-
emitEmptyRunLog(repoPath ?? cwd, options, {
829-
code: 'auth_failed',
830-
message,
831-
timestamp: new Date().toISOString(),
832-
});
833-
return 1;
834-
}
835-
836824
// Resolve config path
837825
let configPath: string | null = null;
838826
if (options.config) {
@@ -849,6 +837,22 @@ export async function runSkills(
849837
const defaultAuxiliaryModel = resolveCliDefaultAuxiliaryModel(config);
850838
const defaultSynthesisModel = resolveCliDefaultSynthesisModel(config);
851839

840+
// Pre-flight: verify Claude auth only when a Claude-backed run may execute.
841+
try {
842+
if ((config?.defaults?.runtime ?? 'claude') === 'claude') {
843+
verifyAuth({ apiKey });
844+
}
845+
} catch (error: unknown) {
846+
const message = (error as WardenAuthenticationError).message;
847+
reporter.error(message);
848+
emitEmptyRunLog(repoPath ?? cwd, options, {
849+
code: 'auth_failed',
850+
message,
851+
timestamp: new Date().toISOString(),
852+
});
853+
return 1;
854+
}
855+
852856
// Determine which triggers/skills to run
853857
let skillsToRun: SkillToRun[];
854858
if (options.skill) {
@@ -868,6 +872,7 @@ export async function runSkills(
868872
model: match?.model ?? defaultModel,
869873
maxTurns: match?.maxTurns ?? config?.defaults?.agent?.maxTurns ?? config?.defaults?.maxTurns,
870874
runtime: match?.runtime ?? config?.defaults?.runtime ?? 'claude',
875+
acp: match?.acp ?? config?.defaults?.agent?.acp,
871876
auxiliaryModel: match?.auxiliaryModel ?? defaultAuxiliaryModel,
872877
synthesisModel: match?.synthesisModel ?? defaultSynthesisModel,
873878
auxiliaryMaxRetries:
@@ -894,6 +899,7 @@ export async function runSkills(
894899
model: t.model,
895900
maxTurns: t.maxTurns,
896901
runtime: t.runtime,
902+
acp: t.acp,
897903
auxiliaryModel: t.auxiliaryModel,
898904
synthesisModel: t.synthesisModel,
899905
auxiliaryMaxRetries: t.auxiliaryMaxRetries,
@@ -926,6 +932,7 @@ export async function runSkills(
926932
apiKey,
927933
model: sdkModel,
928934
runtime: config?.defaults?.runtime ?? 'claude',
935+
acp: config?.defaults?.agent?.acp,
929936
auxiliaryModel: defaultAuxiliaryModel,
930937
synthesisModel: defaultSynthesisModel,
931938
abortController,
@@ -1231,7 +1238,9 @@ async function runConfigMode(options: CLIOptions, reporter: Reporter): Promise<n
12311238
}
12321239

12331240
try {
1234-
verifyAuth({ apiKey });
1241+
if ((config.defaults?.runtime ?? 'claude') === 'claude') {
1242+
verifyAuth({ apiKey });
1243+
}
12351244
} catch (error: unknown) {
12361245
const message = (error as WardenAuthenticationError).message;
12371246
reporter.error(message);
@@ -1257,6 +1266,7 @@ async function runConfigMode(options: CLIOptions, reporter: Reporter): Promise<n
12571266
apiKey,
12581267
model: trigger.model,
12591268
runtime: trigger.runtime,
1269+
acp: trigger.acp,
12601270
auxiliaryModel: trigger.auxiliaryModel,
12611271
synthesisModel: trigger.synthesisModel,
12621272
abortController,

src/cli/output/tasks.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,8 @@ export async function runSkillTask(
515515
runtime: runnerOptions.runtime,
516516
model: runnerOptions.synthesisModel,
517517
maxRetries: runnerOptions.auxiliaryMaxRetries,
518+
providerOptions: runnerOptions.runtime === 'acp' ? runnerOptions.acp : undefined,
519+
abortController: runnerOptions.abortController,
518520
});
519521
let mergedFindings = mergeResult.findings;
520522
if (mergeResult.usage) {
@@ -526,6 +528,8 @@ export async function runSkillTask(
526528
runtime: runnerOptions.runtime,
527529
model: runnerOptions.auxiliaryModel,
528530
maxRetries: runnerOptions.auxiliaryMaxRetries,
531+
providerOptions: runnerOptions.runtime === 'acp' ? runnerOptions.acp : undefined,
532+
abortController: runnerOptions.abortController,
529533
});
530534
mergedFindings = sanitized.findings;
531535
if (sanitized.usage) {

src/config/loader.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,23 @@ describe('resolveSkillConfigs', () => {
432432
expect(resolved?.auxiliaryMaxRetries).toBe(2);
433433
});
434434

435+
it('resolves ACP agent runtime options', () => {
436+
const config: WardenConfig = {
437+
...baseConfig,
438+
defaults: {
439+
runtime: 'acp',
440+
agent: {
441+
acp: { command: 'atlas alta agent run' },
442+
},
443+
},
444+
};
445+
446+
const [resolved] = resolveSkillConfigs(config);
447+
448+
expect(resolved?.runtime).toBe('acp');
449+
expect(resolved?.acp).toEqual({ command: 'atlas alta agent run' });
450+
});
451+
435452
it('falls back to auxiliary model when synthesis model is unset', () => {
436453
const config: WardenConfig = {
437454
...baseConfig,
@@ -853,6 +870,29 @@ describe('maxTurns config', () => {
853870
expect(result.data?.defaults?.synthesis?.model).toBe('claude-opus-4-5');
854871
});
855872

873+
it('accepts ACP custom command and registry defaults', () => {
874+
const customCommand = WardenConfigSchema.safeParse({
875+
version: 1,
876+
defaults: {
877+
runtime: 'acp',
878+
agent: { acp: { command: 'atlas alta agent run' } },
879+
},
880+
skills: [],
881+
});
882+
const registry = WardenConfigSchema.safeParse({
883+
version: 1,
884+
defaults: {
885+
runtime: 'acp',
886+
agent: { acp: { registryId: 'amp-acp', registryUrl: 'https://example.com/registry.json' } },
887+
},
888+
skills: [],
889+
});
890+
891+
expect(customCommand.success).toBe(true);
892+
expect(registry.success).toBe(true);
893+
expect(registry.data?.defaults?.agent?.acp?.registryUrl).toBe('https://example.com/registry.json');
894+
});
895+
856896
it('rejects unknown runtimes', () => {
857897
const config = {
858898
version: 1,

src/config/loader.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
type RunnerConfig,
1414
type LogsConfig,
1515
type RuntimeName,
16+
type AcpAgentRuntimeConfig,
1617
} from './schema.js';
1718
import type { SeverityThreshold, ConfidenceThreshold } from '../types/index.js';
1819

@@ -292,6 +293,8 @@ export interface ResolvedTrigger {
292293
maxTurns?: number;
293294
/** Runtime backend for all model-backed execution. */
294295
runtime?: RuntimeName;
296+
/** Agent Client Protocol runtime options. */
297+
acp?: AcpAgentRuntimeConfig;
295298
/** Model for auxiliary structured model calls. */
296299
auxiliaryModel?: string;
297300
/** Model for post-analysis synthesis/consolidation. */
@@ -340,6 +343,7 @@ export function resolveSkillConfigs(
340343
const envModel = emptyToUndefined(process.env['WARDEN_MODEL']);
341344
const result: ResolvedTrigger[] = [];
342345
const runtime = defaults?.runtime ?? 'claude';
346+
const acp = defaults?.agent?.acp;
343347
const auxiliaryModel = emptyToUndefined(defaults?.auxiliary?.model);
344348
const synthesisModel =
345349
emptyToUndefined(defaults?.synthesis?.model) ??
@@ -386,6 +390,7 @@ export function resolveSkillConfigs(
386390
model: baseModel,
387391
maxTurns: baseMaxTurns,
388392
runtime,
393+
acp,
389394
auxiliaryModel,
390395
synthesisModel,
391396
auxiliaryMaxRetries,
@@ -413,6 +418,7 @@ export function resolveSkillConfigs(
413418
model: emptyToUndefined(trigger.model) ?? baseModel,
414419
maxTurns: trigger.maxTurns ?? baseMaxTurns,
415420
runtime,
421+
acp,
416422
auxiliaryModel,
417423
synthesisModel,
418424
auxiliaryMaxRetries,

src/config/schema.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,27 @@ export type TriggerType = z.infer<typeof TriggerTypeSchema>;
5151
export { RuntimeNameSchema };
5252
export type { RuntimeName };
5353

54+
export const AcpAgentRuntimeConfigSchema = z.object({
55+
/** Custom ACP agent command, e.g. "atlas alta agent run". */
56+
command: z.string().min(1).optional(),
57+
/** Additional arguments appended to the custom ACP command. */
58+
args: z.array(z.string()).optional(),
59+
/** ACP registry agent id resolved from the public registry. */
60+
registryId: z.string().min(1).optional(),
61+
/** ACP registry URL. Defaults to the public latest registry. */
62+
registryUrl: z.string().url().optional(),
63+
/** Extra environment variables for the ACP agent process. */
64+
env: z.record(z.string(), z.string()).optional(),
65+
}).strict();
66+
export type AcpAgentRuntimeConfig = z.infer<typeof AcpAgentRuntimeConfigSchema>;
67+
5468
export const AgentRuntimeConfigSchema = z.object({
5569
/** Model for repo-aware skill execution. Overrides legacy defaults.model. */
5670
model: z.string().optional(),
5771
/** Maximum agentic turns for repo-aware skill execution. Overrides legacy defaults.maxTurns. */
5872
maxTurns: z.number().int().positive().optional(),
73+
/** Agent Client Protocol runtime options. Used when defaults.runtime = "acp". */
74+
acp: AcpAgentRuntimeConfigSchema.optional(),
5975
}).strict();
6076
export type AgentRuntimeConfig = z.infer<typeof AgentRuntimeConfigSchema>;
6177

0 commit comments

Comments
 (0)