Skip to content

Commit 795e500

Browse files
dcramerclaude
andcommitted
feat(provider): Add Pi provider support across runtime and action
Thread provider selection through config, CLI, SDK, and GitHub Action flows so runs can target Claude or Pi consistently with provider-aware auth and telemetry.\n\nAdd provider-specific tool policy defaults for the core loop (including Pi write/shell tools), update output metadata, and expand regression coverage for provider behavior and wiring. Co-Authored-By: GPT-5 Codex <noreply@anthropic.com>
1 parent db9bffd commit 795e500

30 files changed

Lines changed: 365 additions & 82 deletions

action.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ branding:
77
color: 'purple'
88

99
inputs:
10+
provider:
11+
description: 'Provider to use (claude or pi). Overrides WARDEN_PROVIDER when set.'
12+
required: false
1013
anthropic-api-key:
1114
description: 'Anthropic API key (sk-ant-...) or OAuth token. Can also be set via ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN env vars.'
1215
required: false
@@ -61,6 +64,7 @@ runs:
6164
using: 'composite'
6265
steps:
6366
- name: Install Claude Code CLI
67+
if: ${{ inputs.provider != 'pi' && env.WARDEN_PROVIDER != 'pi' }}
6468
shell: bash
6569
run: |
6670
CLAUDE_CODE_VERSION="2.1.32"
@@ -84,6 +88,7 @@ runs:
8488
id: warden
8589
shell: bash
8690
env:
91+
INPUT_PROVIDER: ${{ inputs.provider }}
8792
INPUT_ANTHROPIC_API_KEY: ${{ inputs.anthropic-api-key }}
8893
INPUT_GITHUB_TOKEN: ${{ inputs.github-token }}
8994
INPUT_CONFIG_PATH: ${{ inputs.config-path }}
@@ -93,5 +98,6 @@ runs:
9398
INPUT_REQUEST_CHANGES: ${{ inputs.request-changes }}
9499
INPUT_FAIL_CHECK: ${{ inputs.fail-check }}
95100
INPUT_PARALLEL: ${{ inputs.parallel }}
101+
WARDEN_PROVIDER: ${{ inputs.provider || env.WARDEN_PROVIDER }}
96102
CLAUDE_CODE_PATH: ${{ env.HOME }}/.local/bin/claude
97103
run: node ${{ github.action_path }}/dist/action/index.js

src/action/fix-evaluation/judge.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { emptyUsage } from '../../sdk/usage.js';
77
import { FixJudgeVerdictSchema } from './types.js';
88
import type { FixJudgeResult } from './types.js';
99
import { fetchFileContent, fetchFileLines } from './github.js';
10+
import type { Provider } from '../../config/schema.js';
1011

1112
export interface FixJudgeInput {
1213
comment: ExistingComment;
@@ -212,7 +213,8 @@ export async function evaluateFix(
212213
input: FixJudgeInput,
213214
context: FixJudgeContext,
214215
apiKey: string,
215-
maxRetries?: number
216+
maxRetries?: number,
217+
provider: Provider = 'claude'
216218
): Promise<FixJudgeResult> {
217219
const fallback: FixJudgeResult = {
218220
verdict: { status: 'not_attempted', reasoning: 'Evaluation failed' },
@@ -225,6 +227,7 @@ export async function evaluateFix(
225227

226228
const result = await callHaikuWithTools({
227229
apiKey,
230+
provider,
228231
prompt,
229232
schema: FixJudgeVerdictSchema,
230233
tools: TOOL_DEFINITIONS,

src/action/inputs.test.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,19 @@ describe('parseActionInputs', () => {
3838
expect(inputs.anthropicApiKey).toBe('');
3939
});
4040

41-
it('throws when no auth token is found', () => {
41+
it('allows empty auth tokens (validated later by provider)', () => {
4242
delete process.env['ANTHROPIC_API_KEY'];
4343
delete process.env['WARDEN_ANTHROPIC_API_KEY'];
4444
delete process.env['CLAUDE_CODE_OAUTH_TOKEN'];
45-
expect(() => parseActionInputs()).toThrow('Authentication not found');
45+
const inputs = parseActionInputs();
46+
expect(inputs.anthropicApiKey).toBe('');
47+
expect(inputs.oauthToken).toBe('');
48+
});
49+
50+
it('parses provider from INPUT_PROVIDER', () => {
51+
process.env['INPUT_PROVIDER'] = 'pi';
52+
const inputs = parseActionInputs();
53+
expect(inputs.provider).toBe('pi');
4654
});
4755
});
4856

src/action/inputs.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,19 @@
77
import { SeverityThresholdSchema } from '../types/index.js';
88
import type { SeverityThreshold } from '../types/index.js';
99
import { DEFAULT_CONCURRENCY } from '../utils/index.js';
10+
import type { Provider } from '../config/schema.js';
1011

1112
// -----------------------------------------------------------------------------
1213
// Types
1314
// -----------------------------------------------------------------------------
1415

1516
export interface ActionInputs {
17+
/** Optional provider override (defaults to config/env) */
18+
provider?: Provider;
1619
/** API key for Anthropic API (empty if using OAuth) */
1720
anthropicApiKey: string;
21+
/** API key for Pi provider */
22+
piApiKey?: string;
1823
/** OAuth token for Claude Code (empty if using API key) */
1924
oauthToken: string;
2025
githubToken: string;
@@ -58,31 +63,32 @@ function parseBooleanInput(value: string): boolean | undefined {
5863
return undefined;
5964
}
6065

66+
function parseProviderInput(value: string): Provider | undefined {
67+
return value === 'claude' || value === 'pi' ? value : undefined;
68+
}
69+
6170
/**
6271
* Parse action inputs from the GitHub Actions environment.
6372
* Throws if required inputs are missing.
6473
*/
6574
export function parseActionInputs(): ActionInputs {
66-
// Check for auth token: supports both API keys and OAuth tokens
75+
const providerInput = getInput('provider') || process.env['WARDEN_PROVIDER'] || '';
76+
const provider = parseProviderInput(providerInput);
77+
78+
// Claude auth token: supports both API keys and OAuth tokens
6779
// Priority: input > WARDEN_ANTHROPIC_API_KEY > ANTHROPIC_API_KEY > CLAUDE_CODE_OAUTH_TOKEN
68-
const authToken =
80+
const claudeAuthToken =
6981
getInput('anthropic-api-key') ||
7082
process.env['WARDEN_ANTHROPIC_API_KEY'] ||
7183
process.env['ANTHROPIC_API_KEY'] ||
7284
process.env['CLAUDE_CODE_OAUTH_TOKEN'] ||
7385
'';
7486

75-
if (!authToken) {
76-
throw new Error(
77-
'Authentication not found. Provide an API key via anthropic-api-key input, ' +
78-
'ANTHROPIC_API_KEY env var, or OAuth token via CLAUDE_CODE_OAUTH_TOKEN env var.'
79-
);
80-
}
81-
8287
// Detect token type: OAuth tokens start with 'sk-ant-oat', API keys are other 'sk-ant-' prefixes
83-
const isOAuthToken = authToken.startsWith('sk-ant-oat');
84-
const anthropicApiKey = isOAuthToken ? '' : authToken;
85-
const oauthToken = isOAuthToken ? authToken : '';
88+
const isOAuthToken = claudeAuthToken.startsWith('sk-ant-oat');
89+
const anthropicApiKey = isOAuthToken ? '' : claudeAuthToken;
90+
const oauthToken = isOAuthToken ? claudeAuthToken : '';
91+
const piApiKey = process.env['WARDEN_PI_API_KEY'] || '';
8692

8793
const failOnInput = getInput('fail-on');
8894
const failOn = SeverityThresholdSchema.safeParse(failOnInput).success
@@ -101,7 +107,9 @@ export function parseActionInputs(): ActionInputs {
101107
const failCheck = parseBooleanInput(getInput('fail-check'));
102108

103109
return {
110+
provider,
104111
anthropicApiKey,
112+
piApiKey,
105113
oauthToken,
106114
githubToken: getInput('github-token') || process.env['GITHUB_TOKEN'] || '',
107115
configPath: getInput('config-path') || 'warden.toml',
@@ -129,10 +137,18 @@ export function validateInputs(inputs: ActionInputs): void {
129137
* Sets appropriate env vars based on token type (API key vs OAuth).
130138
*/
131139
export function setupAuthEnv(inputs: ActionInputs): void {
140+
if (inputs.provider) {
141+
process.env['WARDEN_PROVIDER'] = inputs.provider;
142+
}
143+
if (inputs.piApiKey) {
144+
process.env['WARDEN_PI_API_KEY'] = inputs.piApiKey;
145+
}
132146
if (inputs.oauthToken) {
133147
process.env['CLAUDE_CODE_OAUTH_TOKEN'] = inputs.oauthToken;
134148
} else {
135-
process.env['WARDEN_ANTHROPIC_API_KEY'] = inputs.anthropicApiKey;
136-
process.env['ANTHROPIC_API_KEY'] = inputs.anthropicApiKey;
149+
if (inputs.anthropicApiKey) {
150+
process.env['WARDEN_ANTHROPIC_API_KEY'] = inputs.anthropicApiKey;
151+
process.env['ANTHROPIC_API_KEY'] = inputs.anthropicApiKey;
152+
}
137153
}
138154
}

src/action/triggers/executor.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ describe('executeTrigger', () => {
7474
octokit: mockOctokit,
7575
context: mockContext,
7676
config: mockConfig,
77-
anthropicApiKey: 'test-key',
77+
apiKey: 'test-key',
78+
provider: 'claude',
7879
claudePath: '/test/claude',
7980
globalMaxFindings: 10,
8081
};

src/action/triggers/executor.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Sentry } from '../../sentry.js';
1010
import { ActionFailedError } from '../workflow/base.js';
1111
import type { ResolvedTrigger } from '../../config/loader.js';
1212
import type { WardenConfig } from '../../config/schema.js';
13+
import type { Provider } from '../../config/schema.js';
1314
import type { EventContext, SkillReport, SeverityThreshold, ConfidenceThreshold } from '../../types/index.js';
1415
import type { RenderResult } from '../../output/types.js';
1516
import type { OutputMode } from '../../cli/output/tty.js';
@@ -43,8 +44,9 @@ export interface TriggerExecutorDeps {
4344
octokit: Octokit;
4445
context: EventContext;
4546
config: WardenConfig;
46-
anthropicApiKey: string;
47-
claudePath: string;
47+
apiKey: string;
48+
provider: Provider;
49+
claudePath?: string;
4850
/** Global fail-on from action inputs (trigger-specific takes precedence) */
4951
globalFailOn?: SeverityThreshold;
5052
/** Global report-on from action inputs (trigger-specific takes precedence) */
@@ -97,7 +99,7 @@ export async function executeTrigger(
9799
{ op: 'trigger.execute', name: `execute ${trigger.name}` },
98100
async (span) => {
99101
span.setAttribute('skill.name', trigger.skill);
100-
const { octokit, context, config, anthropicApiKey, claudePath } = deps;
102+
const { octokit, context, config, apiKey, claudePath, provider } = deps;
101103

102104
logGroup(`Running trigger: ${trigger.name} (skill: ${trigger.skill})`);
103105

@@ -134,7 +136,8 @@ export async function executeTrigger(
134136
}),
135137
context: filterContextByPaths(context, trigger.filters),
136138
runnerOptions: {
137-
apiKey: anthropicApiKey,
139+
apiKey,
140+
provider,
138141
model: trigger.model,
139142
maxTurns: trigger.maxTurns,
140143
batchDelayMs: config.defaults?.batchDelayMs,

src/action/workflow/base.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { execFileNonInteractive } from '../../utils/exec.js';
1313
import type { EventContext, SkillReport } from '../../types/index.js';
1414
import { countSeverity } from '../../triggers/matcher.js';
1515
import type { TriggerResult } from '../triggers/executor.js';
16+
import type { Provider, WardenConfig } from '../../config/schema.js';
17+
import type { ActionInputs } from '../inputs.js';
1618

1719
/**
1820
* Sentinel error thrown by setFailed() so the top-level catch handler
@@ -187,6 +189,21 @@ export function setWorkflowOutputs(outputs: WorkflowOutputs): void {
187189
setOutput('summary', outputs.summary);
188190
}
189191

192+
export function resolveActionProvider(inputs: ActionInputs, config?: WardenConfig): Provider {
193+
if (inputs.provider) return inputs.provider;
194+
const envProvider = process.env['WARDEN_PROVIDER'];
195+
if (envProvider === 'claude' || envProvider === 'pi') return envProvider;
196+
const cfgProvider = config?.defaults?.provider;
197+
if (cfgProvider === 'claude' || cfgProvider === 'pi') return cfgProvider;
198+
return 'claude';
199+
}
200+
201+
export function getActionProviderApiKey(provider: Provider, inputs: ActionInputs): string {
202+
return provider === 'pi'
203+
? (inputs.piApiKey ?? inputs.anthropicApiKey)
204+
: inputs.anthropicApiKey;
205+
}
206+
190207
// -----------------------------------------------------------------------------
191208
// GitHub API Helpers
192209
// -----------------------------------------------------------------------------

src/action/workflow/pr-workflow.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ import {
4848
setWorkflowOutputs,
4949
getAuthenticatedBotLogin,
5050
writeFindingsOutput,
51+
resolveActionProvider,
52+
getActionProviderApiKey,
5153
} from './base.js';
5254

5355
// -----------------------------------------------------------------------------
@@ -148,7 +150,7 @@ async function initializeWorkflow(
148150
}
149151

150152
// Resolve skills into triggers and match
151-
const resolvedTriggers = resolveSkillConfigs(config);
153+
const resolvedTriggers = resolveSkillConfigs(config, undefined, inputs.provider);
152154
const matchedTriggers = resolvedTriggers.filter((t) => matchTrigger(t, context, 'github'));
153155

154156
if (matchedTriggers.length > 0) {
@@ -250,7 +252,9 @@ async function executeAllTriggers(
250252
inputs: ActionInputs
251253
): Promise<TriggerResult[]> {
252254
const concurrency = config.runner?.concurrency ?? inputs.parallel;
253-
const claudePath = await findClaudeCodeExecutable();
255+
const provider = resolveActionProvider(inputs, config);
256+
const apiKey = getActionProviderApiKey(provider, inputs);
257+
const claudePath = provider === 'claude' ? await findClaudeCodeExecutable() : undefined;
254258

255259
// Global semaphore gates file-level work across all triggers.
256260
// All triggers launch immediately; the semaphore limits concurrent file analyses.
@@ -264,7 +268,8 @@ async function executeAllTriggers(
264268
octokit,
265269
context,
266270
config,
267-
anthropicApiKey: inputs.anthropicApiKey,
271+
apiKey,
272+
provider,
268273
claudePath,
269274
globalFailOn: inputs.failOn,
270275
globalReportOn: inputs.reportOn,

src/action/workflow/schedule.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import {
2626
handleTriggerErrors,
2727
getDefaultBranchFromAPI,
2828
writeFindingsOutput,
29+
resolveActionProvider,
30+
getActionProviderApiKey,
2931
} from './base.js';
3032

3133
// -----------------------------------------------------------------------------
@@ -67,7 +69,9 @@ export async function runScheduleWorkflow(
6769
}
6870

6971
// Find schedule triggers
70-
const scheduleTriggers = resolveSkillConfigs(config).filter((t) => t.type === 'schedule');
72+
const scheduleTriggers = resolveSkillConfigs(config, undefined, inputs.provider).filter((t) => t.type === 'schedule');
73+
const provider = resolveActionProvider(inputs, config);
74+
const apiKey = getActionProviderApiKey(provider, inputs);
7175
if (scheduleTriggers.length === 0) {
7276
console.log('No schedule triggers configured');
7377
setOutput('findings-count', 0);
@@ -147,9 +151,10 @@ export async function runScheduleWorkflow(
147151
const skill = await resolveSkillAsync(resolved.skill, repoPath, {
148152
remote: resolved.remote,
149153
});
150-
const claudePath = await findClaudeCodeExecutable();
154+
const claudePath = provider === 'claude' ? await findClaudeCodeExecutable() : undefined;
151155
const report = await runSkill(skill, context, {
152-
apiKey: inputs.anthropicApiKey,
156+
apiKey,
157+
provider,
153158
model: resolved.model,
154159
maxTurns: resolved.maxTurns,
155160
batchDelayMs: config.defaults?.batchDelayMs,

src/cli/args.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ describe('parseCliArgs', () => {
6666
expect(result.options.config).toBe('./custom.toml');
6767
});
6868

69+
it('parses --provider option', () => {
70+
const result = parseCliArgs(['--provider', 'pi']);
71+
expect(result.options.provider).toBe('pi');
72+
});
73+
6974
it('parses --json flag', () => {
7075
const result = parseCliArgs(['--json']);
7176
expect(result.options.json).toBe(true);

0 commit comments

Comments
 (0)