@sweny-ai/core 0.1.122 → 0.1.124
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-env.d.ts +171 -0
- package/dist/agent-env.js +393 -0
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +1 -1
- package/dist/claude.d.ts +61 -3
- package/dist/claude.js +191 -27
- package/dist/cli/check.d.ts +42 -1
- package/dist/cli/check.js +130 -8
- package/dist/cli/cloud-lifecycle.d.ts +24 -29
- package/dist/cli/cloud-lifecycle.js +187 -12
- package/dist/cli/cloud-report.js +45 -2
- package/dist/cli/config-file.d.ts +8 -1
- package/dist/cli/config-file.js +42 -1
- package/dist/cli/config.js +11 -14
- package/dist/cli/e2e.d.ts +22 -0
- package/dist/cli/e2e.js +31 -4
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +138 -46
- package/dist/cli/marketplace.d.ts +3 -1
- package/dist/cli/marketplace.js +2 -2
- package/dist/cli/new.d.ts +15 -0
- package/dist/cli/new.js +139 -65
- package/dist/cli/publish.js +9 -0
- package/dist/cli/run-output.d.ts +59 -0
- package/dist/cli/run-output.js +189 -0
- package/dist/cli/skill-env.d.ts +34 -0
- package/dist/cli/skill-env.js +64 -0
- package/dist/cli/templates.js +587 -0
- package/dist/cli/workflow-input.d.ts +41 -0
- package/dist/cli/workflow-input.js +56 -0
- package/dist/executor.d.ts +17 -1
- package/dist/executor.js +119 -9
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/mermaid.d.ts +6 -0
- package/dist/mermaid.js +10 -5
- package/dist/schema.d.ts +218 -188
- package/dist/schema.js +24 -3
- package/dist/skills/betterstack.js +4 -0
- package/dist/skills/datadog.js +3 -0
- package/dist/skills/github.js +8 -0
- package/dist/skills/linear.js +8 -0
- package/dist/skills/notification.js +3 -0
- package/dist/skills/sentry.js +4 -0
- package/dist/skills/slack.js +2 -0
- package/dist/skills/supabase.js +9 -0
- package/dist/testing.d.ts +3 -2
- package/dist/testing.js +9 -3
- package/dist/types.d.ts +99 -2
- package/dist/types.js +17 -0
- package/dist/untrusted.d.ts +32 -0
- package/dist/untrusted.js +46 -0
- package/dist/workflows/browser.js +2 -2
- package/dist/workflows/implement.yml +2 -0
- package/dist/workflows/triage.yml +3 -1
- package/package.json +2 -2
package/dist/claude.d.ts
CHANGED
|
@@ -9,6 +9,27 @@
|
|
|
9
9
|
* is to use headless Claude Code — never the raw Anthropic API.
|
|
10
10
|
*/
|
|
11
11
|
import type { Claude, Tool, ToolContext, NodeResult, JSONSchema, Logger, McpServerConfig } from "./types.js";
|
|
12
|
+
import { type AgentAccess, type SandboxMode } from "./agent-env.js";
|
|
13
|
+
/**
|
|
14
|
+
* Built-in tools disallowed for the route-evaluation (`evaluate`) and
|
|
15
|
+
* reflection/judge (`ask`) calls.
|
|
16
|
+
*
|
|
17
|
+
* These are pure classification calls over prior-node data that can be
|
|
18
|
+
* attacker-influenceable (a prior node summarizes an untrusted issue body,
|
|
19
|
+
* PR diff, log line, etc.). They must never be able to mutate the workspace
|
|
20
|
+
* or shell out, regardless of any node's own `disallowed_tools` policy.
|
|
21
|
+
* `maxTurns: 1` already bounds them; this removes the powerful built-ins
|
|
22
|
+
* from the model's context entirely as structural defense-in-depth.
|
|
23
|
+
*/
|
|
24
|
+
export declare const CLASSIFICATION_DISALLOWED_TOOLS: readonly ["Bash", "Write", "Edit", "MultiEdit", "NotebookEdit", "WebFetch", "WebSearch"];
|
|
25
|
+
/**
|
|
26
|
+
* Built-in tools disallowed for a node run with `readOnly: true` (dry run,
|
|
27
|
+
* #380): everything that can edit the workspace, shell out (and so reach
|
|
28
|
+
* `gh`, `curl`, `git push`), spawn a subagent that could, or fetch an
|
|
29
|
+
* arbitrary URL (a GET can still trigger a webhook or exfiltrate data).
|
|
30
|
+
* Read, Grep, Glob and WebSearch stay available for analysis.
|
|
31
|
+
*/
|
|
32
|
+
export declare const READ_ONLY_DISALLOWED_TOOLS: readonly ["Bash", "Write", "Edit", "MultiEdit", "NotebookEdit", "Task", "Agent", "WebFetch"];
|
|
12
33
|
/** How sweny resolves which credentials reach the Claude Code subprocess. */
|
|
13
34
|
export type SwenyAuthMode = "auto" | "api-key" | "oauth";
|
|
14
35
|
export interface ResolveAuthEnvOpts {
|
|
@@ -59,6 +80,27 @@ export interface ClaudeClientOptions {
|
|
|
59
80
|
defaultContext?: ToolContext;
|
|
60
81
|
/** External MCP servers (GitHub, Linear, Sentry, etc.) — merged with core skill tools */
|
|
61
82
|
mcpServers?: Record<string, McpServerConfig>;
|
|
83
|
+
/**
|
|
84
|
+
* Extra env var names passed to the agent subprocess on top of the
|
|
85
|
+
* allowlist (see agent-env.ts). `"*"` inherits the full environment.
|
|
86
|
+
* Default: `SWENY_ENV_PASSTHROUGH` (comma-separated).
|
|
87
|
+
*/
|
|
88
|
+
envPassthrough?: string[];
|
|
89
|
+
/**
|
|
90
|
+
* Scope the agent env to the allowlist. Default: `SWENY_ENV_SCOPE`, else on
|
|
91
|
+
* when CI is truthy and off locally (full env).
|
|
92
|
+
*/
|
|
93
|
+
envScope?: boolean;
|
|
94
|
+
/**
|
|
95
|
+
* SDK sandbox for agent commands: `auto` (sandbox when the host supports
|
|
96
|
+
* it, else warn and run unsandboxed), `strict` (sandbox or fail), `off`.
|
|
97
|
+
* Default: `SWENY_SANDBOX`, else `auto` when CI is truthy, `off` otherwise.
|
|
98
|
+
*/
|
|
99
|
+
sandbox?: SandboxMode;
|
|
100
|
+
/** Extra hosts sandboxed commands may reach. Default: `SWENY_SANDBOX_ALLOWED_DOMAINS`. */
|
|
101
|
+
sandboxAllowedDomains?: string[];
|
|
102
|
+
/** Sandbox preflight probe (test seam). Returns a reason when the sandbox cannot run. */
|
|
103
|
+
sandboxProbe?: () => string | undefined;
|
|
62
104
|
}
|
|
63
105
|
/**
|
|
64
106
|
* Wire an optional timeout + caller signal onto a single AbortController.
|
|
@@ -81,10 +123,22 @@ export declare class ClaudeClient implements Claude {
|
|
|
81
123
|
private logger;
|
|
82
124
|
private defaultContext;
|
|
83
125
|
private mcpServers;
|
|
126
|
+
private envPassthrough;
|
|
127
|
+
private sandboxMode;
|
|
128
|
+
private sandboxAllowedDomains;
|
|
129
|
+
private sandboxProbe;
|
|
130
|
+
private sandboxWarned;
|
|
131
|
+
private envScope;
|
|
132
|
+
private envWarned;
|
|
84
133
|
constructor(opts?: ClaudeClientOptions);
|
|
85
134
|
/**
|
|
86
|
-
* Build env for the Claude Code subprocess.
|
|
87
|
-
*
|
|
135
|
+
* Build env for the Claude Code subprocess (#360). Applies auth precedence
|
|
136
|
+
* via {@link resolveAuthEnv} on the full process env (it reads
|
|
137
|
+
* `SWENY_AUTH`). When env scoping is on (default in CI, see
|
|
138
|
+
* {@link resolveEnvScope}) the result is narrowed to the allowlist plus
|
|
139
|
+
* `extraVars` (the node's declared skill env vars) and the operator
|
|
140
|
+
* passthrough list, and the withheld names (never values) are warned once
|
|
141
|
+
* per client. When off, the full env passes through as before.
|
|
88
142
|
*/
|
|
89
143
|
private buildEnv;
|
|
90
144
|
run(opts: {
|
|
@@ -100,6 +154,10 @@ export declare class ClaudeClient implements Claude {
|
|
|
100
154
|
timeoutMs?: number;
|
|
101
155
|
/** Caller-supplied abort signal. Aborting it interrupts the query. */
|
|
102
156
|
signal?: AbortSignal;
|
|
157
|
+
/** Dry run (#380): no external MCP servers, no write-capable built-ins. */
|
|
158
|
+
readOnly?: boolean;
|
|
159
|
+
/** Env var names + sandbox hosts this node's skills need (#360). */
|
|
160
|
+
agentAccess?: AgentAccess;
|
|
103
161
|
}): Promise<NodeResult>;
|
|
104
162
|
evaluate(opts: {
|
|
105
163
|
question: string;
|
|
@@ -112,7 +170,7 @@ export declare class ClaudeClient implements Claude {
|
|
|
112
170
|
timeoutMs?: number;
|
|
113
171
|
/** Caller-supplied abort signal. Aborting it interrupts the query. */
|
|
114
172
|
signal?: AbortSignal;
|
|
115
|
-
}): Promise<string>;
|
|
173
|
+
}): Promise<string | null>;
|
|
116
174
|
ask(opts: {
|
|
117
175
|
instruction: string;
|
|
118
176
|
context: Record<string, unknown>;
|
package/dist/claude.js
CHANGED
|
@@ -11,7 +11,46 @@
|
|
|
11
11
|
import { query, createSdkMcpServer, tool as sdkTool } from "@anthropic-ai/claude-agent-sdk";
|
|
12
12
|
import { z } from "zod";
|
|
13
13
|
import { consoleLogger } from "./types.js";
|
|
14
|
+
import { formatWithheldWarning, parseList, resolveEnvScope, scopeAgentEnv, resolveAgentSandbox, } from "./agent-env.js";
|
|
15
|
+
import { fenceUntrustedJson } from "./untrusted.js";
|
|
14
16
|
const SYSTEM_PROMPT = `You are a step in an automated workflow. Execute the instruction precisely using the tools available to you. Be thorough but concise. When you're done, summarize your findings and results.`;
|
|
17
|
+
/**
|
|
18
|
+
* Built-in tools disallowed for the route-evaluation (`evaluate`) and
|
|
19
|
+
* reflection/judge (`ask`) calls.
|
|
20
|
+
*
|
|
21
|
+
* These are pure classification calls over prior-node data that can be
|
|
22
|
+
* attacker-influenceable (a prior node summarizes an untrusted issue body,
|
|
23
|
+
* PR diff, log line, etc.). They must never be able to mutate the workspace
|
|
24
|
+
* or shell out, regardless of any node's own `disallowed_tools` policy.
|
|
25
|
+
* `maxTurns: 1` already bounds them; this removes the powerful built-ins
|
|
26
|
+
* from the model's context entirely as structural defense-in-depth.
|
|
27
|
+
*/
|
|
28
|
+
export const CLASSIFICATION_DISALLOWED_TOOLS = [
|
|
29
|
+
"Bash",
|
|
30
|
+
"Write",
|
|
31
|
+
"Edit",
|
|
32
|
+
"MultiEdit",
|
|
33
|
+
"NotebookEdit",
|
|
34
|
+
"WebFetch",
|
|
35
|
+
"WebSearch",
|
|
36
|
+
];
|
|
37
|
+
/**
|
|
38
|
+
* Built-in tools disallowed for a node run with `readOnly: true` (dry run,
|
|
39
|
+
* #380): everything that can edit the workspace, shell out (and so reach
|
|
40
|
+
* `gh`, `curl`, `git push`), spawn a subagent that could, or fetch an
|
|
41
|
+
* arbitrary URL (a GET can still trigger a webhook or exfiltrate data).
|
|
42
|
+
* Read, Grep, Glob and WebSearch stay available for analysis.
|
|
43
|
+
*/
|
|
44
|
+
export const READ_ONLY_DISALLOWED_TOOLS = [
|
|
45
|
+
"Bash",
|
|
46
|
+
"Write",
|
|
47
|
+
"Edit",
|
|
48
|
+
"MultiEdit",
|
|
49
|
+
"NotebookEdit",
|
|
50
|
+
"Task",
|
|
51
|
+
"Agent",
|
|
52
|
+
"WebFetch",
|
|
53
|
+
];
|
|
15
54
|
/**
|
|
16
55
|
* Decide which auth credentials survive into the Claude Code subprocess env.
|
|
17
56
|
*
|
|
@@ -127,6 +166,10 @@ export function makeAbort(timeoutMs, signal) {
|
|
|
127
166
|
* Swallows errors: the stream may already be done, and interrupt is only
|
|
128
167
|
* supported in streaming-input mode.
|
|
129
168
|
*/
|
|
169
|
+
// The return type is deliberately `Promise<unknown>`, not `Promise<void>`:
|
|
170
|
+
// agent-sdk 0.3 changed `Query.interrupt()` to resolve with an
|
|
171
|
+
// SDKControlInterruptResponse. We discard the value either way, and a `void`
|
|
172
|
+
// parameter type would reject any SDK that resolves with something.
|
|
130
173
|
async function interruptStream(stream) {
|
|
131
174
|
if (!stream || typeof stream.interrupt !== "function")
|
|
132
175
|
return;
|
|
@@ -137,6 +180,30 @@ async function interruptStream(stream) {
|
|
|
137
180
|
/* already finished or not interruptible — nothing to clean up */
|
|
138
181
|
}
|
|
139
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Extract shape-only usage/cost from an SDK `result` message.
|
|
185
|
+
*
|
|
186
|
+
* Reads `total_cost_usd`, `usage` (in/out/cache tokens), and `num_turns` off
|
|
187
|
+
* the terminal result. Returns undefined when the message carries none of
|
|
188
|
+
* them (mocks, older SDKs) so callers leave `NodeResult.usage` absent rather
|
|
189
|
+
* than shipping an all-zero object. NEVER touches `result`/prompt text.
|
|
190
|
+
*/
|
|
191
|
+
function extractUsage(resultMsg) {
|
|
192
|
+
const m = resultMsg;
|
|
193
|
+
const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : undefined);
|
|
194
|
+
const u = m.usage;
|
|
195
|
+
const usage = {
|
|
196
|
+
costUsd: num(m.total_cost_usd),
|
|
197
|
+
inputTokens: num(u?.input_tokens),
|
|
198
|
+
outputTokens: num(u?.output_tokens),
|
|
199
|
+
cacheReadTokens: num(u?.cache_read_input_tokens),
|
|
200
|
+
cacheCreationTokens: num(u?.cache_creation_input_tokens),
|
|
201
|
+
numTurns: num(m.num_turns),
|
|
202
|
+
};
|
|
203
|
+
// Drop the object entirely when the SDK gave us nothing usable, so a
|
|
204
|
+
// mocked/legacy run reports no usage instead of a misleading zero-cost run.
|
|
205
|
+
return Object.values(usage).some((v) => v !== undefined) ? usage : undefined;
|
|
206
|
+
}
|
|
140
207
|
export class ClaudeClient {
|
|
141
208
|
model;
|
|
142
209
|
maxTurns;
|
|
@@ -144,6 +211,13 @@ export class ClaudeClient {
|
|
|
144
211
|
logger;
|
|
145
212
|
defaultContext;
|
|
146
213
|
mcpServers;
|
|
214
|
+
envPassthrough;
|
|
215
|
+
sandboxMode;
|
|
216
|
+
sandboxAllowedDomains;
|
|
217
|
+
sandboxProbe;
|
|
218
|
+
sandboxWarned = false;
|
|
219
|
+
envScope;
|
|
220
|
+
envWarned = false;
|
|
147
221
|
constructor(opts = {}) {
|
|
148
222
|
this.model = opts.model;
|
|
149
223
|
this.maxTurns = opts.maxTurns ?? 20;
|
|
@@ -151,18 +225,68 @@ export class ClaudeClient {
|
|
|
151
225
|
this.logger = opts.logger ?? consoleLogger;
|
|
152
226
|
this.defaultContext = opts.defaultContext ?? { config: {}, logger: this.logger };
|
|
153
227
|
this.mcpServers = opts.mcpServers ?? {};
|
|
228
|
+
this.envPassthrough = opts.envPassthrough;
|
|
229
|
+
this.envScope = opts.envScope;
|
|
230
|
+
this.sandboxMode = opts.sandbox;
|
|
231
|
+
this.sandboxAllowedDomains = opts.sandboxAllowedDomains;
|
|
232
|
+
this.sandboxProbe = opts.sandboxProbe;
|
|
154
233
|
}
|
|
155
234
|
/**
|
|
156
|
-
* Build env for the Claude Code subprocess.
|
|
157
|
-
*
|
|
235
|
+
* Build env for the Claude Code subprocess (#360). Applies auth precedence
|
|
236
|
+
* via {@link resolveAuthEnv} on the full process env (it reads
|
|
237
|
+
* `SWENY_AUTH`). When env scoping is on (default in CI, see
|
|
238
|
+
* {@link resolveEnvScope}) the result is narrowed to the allowlist plus
|
|
239
|
+
* `extraVars` (the node's declared skill env vars) and the operator
|
|
240
|
+
* passthrough list, and the withheld names (never values) are warned once
|
|
241
|
+
* per client. When off, the full env passes through as before.
|
|
158
242
|
*/
|
|
159
|
-
buildEnv() {
|
|
160
|
-
const
|
|
161
|
-
|
|
243
|
+
buildEnv(extraVars = []) {
|
|
244
|
+
const full = Object.fromEntries(Object.entries(process.env).filter((e) => e[1] != null));
|
|
245
|
+
const authed = resolveAuthEnv(full, { logger: this.logger });
|
|
246
|
+
if (!resolveEnvScope(process.env, this.envScope, this.logger))
|
|
247
|
+
return authed;
|
|
248
|
+
const { env, withheld } = scopeAgentEnv(authed, {
|
|
249
|
+
extraVars,
|
|
250
|
+
passthrough: this.envPassthrough ?? parseList(process.env.SWENY_ENV_PASSTHROUGH),
|
|
251
|
+
logger: this.logger,
|
|
252
|
+
});
|
|
253
|
+
if (withheld.length > 0 && !this.envWarned) {
|
|
254
|
+
this.envWarned = true;
|
|
255
|
+
const prefix = process.env.GITHUB_ACTIONS === "true" ? "::warning title=SWEny agent env::" : "";
|
|
256
|
+
this.logger.warn(`${prefix}${formatWithheldWarning(withheld)}`);
|
|
257
|
+
}
|
|
258
|
+
return env;
|
|
162
259
|
}
|
|
163
260
|
async run(opts) {
|
|
164
|
-
const { instruction, context, tools, outputSchema, onProgress, maxTurns,
|
|
261
|
+
const { instruction, context, tools, outputSchema, onProgress, maxTurns, model, timeoutMs, signal, readOnly, agentAccess, } = opts;
|
|
262
|
+
// Dry run (#380): external MCP servers cannot be classified per tool, so
|
|
263
|
+
// they are unknown, and unknown means write. Drop them, and disallow the
|
|
264
|
+
// built-ins that can change the workspace or shell out.
|
|
265
|
+
const disallowedTools = readOnly
|
|
266
|
+
? [...new Set([...(opts.disallowedTools ?? []), ...READ_ONLY_DISALLOWED_TOOLS])]
|
|
267
|
+
: opts.disallowedTools;
|
|
165
268
|
const effectiveModel = model ?? this.model;
|
|
269
|
+
// #360: run agent commands in the SDK sandbox. `auto` (default in CI) falls
|
|
270
|
+
// back to unsandboxed with one loud warning when the host cannot sandbox;
|
|
271
|
+
// `strict` fails the node here instead.
|
|
272
|
+
const sandbox = resolveAgentSandbox({
|
|
273
|
+
env: process.env,
|
|
274
|
+
mode: this.sandboxMode,
|
|
275
|
+
allowedDomains: this.sandboxAllowedDomains,
|
|
276
|
+
nodeDomains: agentAccess?.domains,
|
|
277
|
+
probe: this.sandboxProbe,
|
|
278
|
+
logger: this.logger,
|
|
279
|
+
});
|
|
280
|
+
if (sandbox.error) {
|
|
281
|
+
this.logger.error(sandbox.error);
|
|
282
|
+
return { status: "failed", data: { error: sandbox.error }, toolCalls: [] };
|
|
283
|
+
}
|
|
284
|
+
if (sandbox.warning && !this.sandboxWarned) {
|
|
285
|
+
this.sandboxWarned = true;
|
|
286
|
+
// GitHub Actions renders `::warning::` as a run annotation.
|
|
287
|
+
const prefix = process.env.GITHUB_ACTIONS === "true" ? "::warning title=SWEny agent sandbox::" : "";
|
|
288
|
+
this.logger.warn(`${prefix}${sandbox.warning}`);
|
|
289
|
+
}
|
|
166
290
|
// Tool-call accounting (Fix #1).
|
|
167
291
|
//
|
|
168
292
|
// Stream-driven recording, correlated by tool_use_id:
|
|
@@ -190,30 +314,37 @@ export class ClaudeClient {
|
|
|
190
314
|
name: "sweny-core",
|
|
191
315
|
tools: sdkTools,
|
|
192
316
|
});
|
|
193
|
-
// Build prompt
|
|
317
|
+
// Build prompt. Context (workflow input such as issues/alerts, plus prior
|
|
318
|
+
// node outputs) is fenced as untrusted data (#360).
|
|
194
319
|
const prompt = [
|
|
195
320
|
`## Instruction\n\n${instruction}`,
|
|
196
|
-
`## Context\n\n
|
|
321
|
+
`## Context\n\n${fenceUntrustedJson(context, "context")}`,
|
|
197
322
|
outputSchema
|
|
198
323
|
? `## Required Output\n\nYou MUST end with a JSON object matching this schema:\n\`\`\`json\n${JSON.stringify(outputSchema, null, 2)}\n\`\`\``
|
|
199
324
|
: "",
|
|
200
325
|
]
|
|
201
326
|
.filter(Boolean)
|
|
202
327
|
.join("\n\n");
|
|
203
|
-
const env = this.buildEnv();
|
|
328
|
+
const env = this.buildEnv(agentAccess?.envVars);
|
|
204
329
|
let response = "";
|
|
205
330
|
// CC-08: when the SDK populates typed structured output (because we passed
|
|
206
331
|
// an `outputFormat`), prefer it over re-parsing JSON out of the free-text
|
|
207
332
|
// result. Left undefined when the SDK didn't provide it, so the
|
|
208
333
|
// tryParseJSON heuristic remains the fallback (e.g. mocks, older results).
|
|
209
334
|
let structuredOutput;
|
|
335
|
+
// Token + cost accounting. Read off the SDK's terminal `result` message
|
|
336
|
+
// (present on both success and error subtypes). Shape-only: counts and
|
|
337
|
+
// cost, never any prompt/response text. Left undefined when the SDK
|
|
338
|
+
// emitted no usage (mocks, older SDKs), so `usage` stays absent rather
|
|
339
|
+
// than shipping zeroes that read as a real (free) run.
|
|
340
|
+
let usage;
|
|
210
341
|
// Timeout / abort wiring (back-compat: only armed when requested).
|
|
211
342
|
// A single AbortController drives both an optional caller signal and an
|
|
212
343
|
// optional timeout timer. The SDK aborts the subprocess when this fires.
|
|
213
344
|
const abort = makeAbort(timeoutMs, signal);
|
|
214
345
|
let stream;
|
|
215
346
|
try {
|
|
216
|
-
const allMcpServers = { ...this.mcpServers };
|
|
347
|
+
const allMcpServers = readOnly ? {} : { ...this.mcpServers };
|
|
217
348
|
if (sdkTools.length > 0)
|
|
218
349
|
allMcpServers["sweny-core"] = mcpServer;
|
|
219
350
|
stream = query({
|
|
@@ -225,10 +356,16 @@ export class ClaudeClient {
|
|
|
225
356
|
env,
|
|
226
357
|
permissionMode: "bypassPermissions",
|
|
227
358
|
allowDangerouslySkipPermissions: true,
|
|
359
|
+
...(sandbox.settings ? { sandbox: sandbox.settings } : {}),
|
|
228
360
|
stderr: (data) => this.logger.debug(`[claude-code] ${data}`),
|
|
229
361
|
...(abort ? { abortController: abort.controller } : {}),
|
|
230
362
|
...(effectiveModel ? { model: effectiveModel } : {}),
|
|
231
363
|
...(Object.keys(allMcpServers).length > 0 ? { mcpServers: allMcpServers } : {}),
|
|
364
|
+
// Dry run (#380): settingSources is omitted, so the SDK loads user,
|
|
365
|
+
// project and local settings, including their MCP servers (and
|
|
366
|
+
// project .mcp.json, plugins). strictMcpConfig limits MCP to the
|
|
367
|
+
// servers passed above, which under readOnly is only sweny-core.
|
|
368
|
+
...(readOnly ? { strictMcpConfig: true } : {}),
|
|
232
369
|
...(disallowedTools && disallowedTools.length > 0 ? { disallowedTools } : {}),
|
|
233
370
|
// CC-08: ask the SDK to produce validated structured output when the
|
|
234
371
|
// node declares an output schema. The SDK then returns the parsed
|
|
@@ -310,6 +447,10 @@ export class ClaudeClient {
|
|
|
310
447
|
}
|
|
311
448
|
else if (message.type === "result") {
|
|
312
449
|
const resultMsg = message;
|
|
450
|
+
// Capture token/cost accounting off the terminal result. Present on
|
|
451
|
+
// both success and error subtypes; shape-only. Attached to every
|
|
452
|
+
// return below (including early-termination and error paths).
|
|
453
|
+
usage = extractUsage(resultMsg);
|
|
313
454
|
if (resultMsg.subtype === "success" && "result" in resultMsg) {
|
|
314
455
|
// terminal_reason was added in @anthropic-ai/claude-agent-sdk v0.2.91.
|
|
315
456
|
// When the turn budget is exhausted the SDK still emits subtype='success'
|
|
@@ -330,6 +471,7 @@ export class ClaudeClient {
|
|
|
330
471
|
...(partial !== "" ? { summary: partial } : {}),
|
|
331
472
|
},
|
|
332
473
|
toolCalls,
|
|
474
|
+
...(usage ? { usage } : {}),
|
|
333
475
|
};
|
|
334
476
|
}
|
|
335
477
|
response = resultMsg.result;
|
|
@@ -350,6 +492,7 @@ export class ClaudeClient {
|
|
|
350
492
|
status: "failed",
|
|
351
493
|
data: { error: prefix + (errors?.join("\n") ?? "Execution failed") },
|
|
352
494
|
toolCalls,
|
|
495
|
+
...(usage ? { usage } : {}),
|
|
353
496
|
};
|
|
354
497
|
}
|
|
355
498
|
}
|
|
@@ -385,6 +528,7 @@ export class ClaudeClient {
|
|
|
385
528
|
status: "success",
|
|
386
529
|
data: { summary: response, ...parsedData },
|
|
387
530
|
toolCalls,
|
|
531
|
+
...(usage ? { usage } : {}),
|
|
388
532
|
};
|
|
389
533
|
}
|
|
390
534
|
async evaluate(opts) {
|
|
@@ -404,6 +548,15 @@ export class ClaudeClient {
|
|
|
404
548
|
env,
|
|
405
549
|
permissionMode: "bypassPermissions",
|
|
406
550
|
allowDangerouslySkipPermissions: true,
|
|
551
|
+
// Route evaluation is a pure classification call over
|
|
552
|
+
// possibly-attacker-influenceable prior-node data. Never let it
|
|
553
|
+
// shell out or mutate, regardless of the node's own policy.
|
|
554
|
+
// Disable ALL built-in tools (SDK `tools: []`); no MCP servers are passed.
|
|
555
|
+
// The disallow list below stays as a second layer.
|
|
556
|
+
tools: [],
|
|
557
|
+
mcpServers: {},
|
|
558
|
+
strictMcpConfig: true,
|
|
559
|
+
disallowedTools: [...CLASSIFICATION_DISALLOWED_TOOLS],
|
|
407
560
|
stderr: (data) => this.logger.debug(`[claude-code] ${data}`),
|
|
408
561
|
...(abort ? { abortController: abort.controller } : {}),
|
|
409
562
|
...(this.model ? { model: this.model } : {}),
|
|
@@ -416,34 +569,33 @@ export class ClaudeClient {
|
|
|
416
569
|
response = resultMsg.result;
|
|
417
570
|
}
|
|
418
571
|
else {
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
//
|
|
572
|
+
// Fail closed. An SDK-level failure (non-success subtype) is NOT a
|
|
573
|
+
// routing decision. Signal it to the caller (null) so the executor
|
|
574
|
+
// takes an explicit default edge or terminates — never the old
|
|
575
|
+
// fall-through to the first choice, which silently routed an outage
|
|
576
|
+
// down the first conditional edge (e.g. filing a real issue/PR).
|
|
424
577
|
sdkFailed = true;
|
|
425
|
-
this.logger.warn(`claude.evaluate: SDK returned non-success subtype "${resultMsg.subtype}" —
|
|
578
|
+
this.logger.warn(`claude.evaluate: SDK returned non-success subtype "${resultMsg.subtype}" — failing closed (no route decision).`);
|
|
426
579
|
}
|
|
427
580
|
}
|
|
428
581
|
}
|
|
429
582
|
}
|
|
430
583
|
catch (err) {
|
|
431
584
|
if (abort?.reason() === "timeout") {
|
|
432
|
-
this.logger.warn(`Evaluate query timed out after ${timeoutMs}ms.
|
|
585
|
+
this.logger.warn(`Evaluate query timed out after ${timeoutMs}ms. Failing closed (no route decision).`);
|
|
433
586
|
}
|
|
434
587
|
else {
|
|
435
|
-
this.logger.warn(`Evaluate query failed: ${err.message}.
|
|
588
|
+
this.logger.warn(`Evaluate query failed: ${err.message}. Failing closed (no route decision).`);
|
|
436
589
|
}
|
|
437
|
-
return
|
|
590
|
+
return null;
|
|
438
591
|
}
|
|
439
592
|
finally {
|
|
440
593
|
abort?.clear();
|
|
441
594
|
await interruptStream(stream);
|
|
442
595
|
}
|
|
443
|
-
// An SDK-level failure already logged a distinct message;
|
|
444
|
-
// the ambiguous-answer warning, which would misattribute the cause.
|
|
596
|
+
// An SDK-level failure already logged a distinct message; fail closed.
|
|
445
597
|
if (sdkFailed)
|
|
446
|
-
return
|
|
598
|
+
return null;
|
|
447
599
|
const text = response.trim().replace(/^["']|["']$/g, "");
|
|
448
600
|
const validIds = choices.map((c) => c.id);
|
|
449
601
|
// Exact match
|
|
@@ -453,15 +605,18 @@ export class ClaudeClient {
|
|
|
453
605
|
const match = validIds.find((id) => text.includes(id));
|
|
454
606
|
if (match)
|
|
455
607
|
return match;
|
|
456
|
-
//
|
|
457
|
-
|
|
458
|
-
|
|
608
|
+
// Fail closed. An unparseable answer is not a decision. Returning
|
|
609
|
+
// validIds[0] here is the fail-open bug: on a node with a single
|
|
610
|
+
// conditional out-edge, the "first choice" is always that edge, so a
|
|
611
|
+
// garbled model answer would always take it. Signal no-decision instead.
|
|
612
|
+
this.logger.warn(`Could not parse route choice from: "${text.slice(0, 100)}". Failing closed (no route decision).`);
|
|
613
|
+
return null;
|
|
459
614
|
}
|
|
460
615
|
async ask(opts) {
|
|
461
616
|
const { instruction, context, model, timeoutMs, signal } = opts;
|
|
462
617
|
const prompt = [
|
|
463
618
|
instruction,
|
|
464
|
-
Object.keys(context).length > 0 ? `\nContext:\n
|
|
619
|
+
Object.keys(context).length > 0 ? `\nContext:\n${fenceUntrustedJson(context, "context")}` : "",
|
|
465
620
|
]
|
|
466
621
|
.filter(Boolean)
|
|
467
622
|
.join("\n");
|
|
@@ -479,6 +634,15 @@ export class ClaudeClient {
|
|
|
479
634
|
env,
|
|
480
635
|
permissionMode: "bypassPermissions",
|
|
481
636
|
allowDangerouslySkipPermissions: true,
|
|
637
|
+
// Reflection and judge scoring are pure classification calls over
|
|
638
|
+
// possibly-attacker-influenceable prior-node data. Deny the powerful
|
|
639
|
+
// built-ins so they can never shell out or mutate the workspace.
|
|
640
|
+
// Disable ALL built-in tools (SDK `tools: []`); no MCP servers are passed.
|
|
641
|
+
// The disallow list below stays as a second layer.
|
|
642
|
+
tools: [],
|
|
643
|
+
mcpServers: {},
|
|
644
|
+
strictMcpConfig: true,
|
|
645
|
+
disallowedTools: [...CLASSIFICATION_DISALLOWED_TOOLS],
|
|
482
646
|
stderr: (data) => this.logger.debug(`[claude-code] ${data}`),
|
|
483
647
|
...(abort ? { abortController: abort.controller } : {}),
|
|
484
648
|
...(effectiveModel ? { model: effectiveModel } : {}),
|
|
@@ -578,7 +742,7 @@ export function buildEvaluatePrompt(question, context, choices) {
|
|
|
578
742
|
const choiceList = choices.map((c) => `- "${c.id}": ${c.description}`).join("\n");
|
|
579
743
|
return [
|
|
580
744
|
question,
|
|
581
|
-
`\nContext:\n
|
|
745
|
+
`\nContext:\n${fenceUntrustedJson(context, "context")}`,
|
|
582
746
|
`\nChoices:\n${choiceList}`,
|
|
583
747
|
`\nEvaluation rules:`,
|
|
584
748
|
`1. Read each choice's condition literally and match against the structured fields in the context (e.g. status, counts, enum values, boolean flags).`,
|
package/dist/cli/check.d.ts
CHANGED
|
@@ -8,7 +8,17 @@ export interface CheckResult {
|
|
|
8
8
|
* Run lightweight connectivity checks for all configured providers.
|
|
9
9
|
* Uses raw fetch — does NOT import provider packages.
|
|
10
10
|
*/
|
|
11
|
-
export
|
|
11
|
+
export interface CheckOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Skill ids the configured workflows use. When set, only providers those
|
|
14
|
+
* skills need are checked; everything else is omitted. When undefined
|
|
15
|
+
* (no workflows found), the legacy config-driven checks run.
|
|
16
|
+
*/
|
|
17
|
+
scope?: ReadonlySet<string>;
|
|
18
|
+
/** A local Claude Code login exists, so no API key is required. */
|
|
19
|
+
claudeCodeLogin?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare function checkProviderConnectivity(config: CliConfig, opts?: CheckOptions): Promise<CheckResult[]>;
|
|
12
22
|
/** Which credential `sweny check` should validate, mirroring resolveAuthEnv intent. */
|
|
13
23
|
export type CheckAuthMode = "oauth" | "api-key" | "auth-token" | "none";
|
|
14
24
|
export declare function resolveCheckAuthMode(config: Pick<CliConfig, "anthropicApiKey" | "anthropicAuthToken" | "claudeOauthToken" | "swenyAuth">): CheckAuthMode;
|
|
@@ -16,3 +26,34 @@ export declare function resolveCheckAuthMode(config: Pick<CliConfig, "anthropicA
|
|
|
16
26
|
export declare function redactUrl(raw: string): string;
|
|
17
27
|
export declare function checkAnthropicGateway(base: string, config: Pick<CliConfig, "anthropicApiKey" | "anthropicAuthToken" | "claudeOauthToken">, mode: CheckAuthMode): Promise<CheckResult>;
|
|
18
28
|
export declare function checkDatadog(creds: Record<string, string>): Promise<CheckResult>;
|
|
29
|
+
/**
|
|
30
|
+
* Detect a local Claude Code login the way `sweny workflow run` benefits from
|
|
31
|
+
* it: the spawned Claude Code agent finds its own login, so no env credential
|
|
32
|
+
* is needed. Checks `~/.claude/.credentials.json` (or CLAUDE_CONFIG_DIR) and,
|
|
33
|
+
* on macOS, the Keychain entry (attributes only, the secret is never read).
|
|
34
|
+
*/
|
|
35
|
+
export declare function detectClaudeCodeLogin(opts?: {
|
|
36
|
+
env?: Record<string, string | undefined>;
|
|
37
|
+
home?: string;
|
|
38
|
+
platform?: NodeJS.Platform;
|
|
39
|
+
keychainHasLogin?: () => boolean;
|
|
40
|
+
}): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Skill ids used by the workflows in `.sweny/workflows/`. `found` is false when
|
|
43
|
+
* there are no workflow files, in which case callers keep the legacy checks.
|
|
44
|
+
*/
|
|
45
|
+
export declare function discoverWorkflowSkillIds(cwd: string): {
|
|
46
|
+
found: boolean;
|
|
47
|
+
skillIds: Set<string>;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Errors `sweny check` should report. With a workflow scope, only credentials
|
|
51
|
+
* those workflows need (plus agent auth) are required. A local Claude Code
|
|
52
|
+
* login satisfies agent auth.
|
|
53
|
+
*/
|
|
54
|
+
export declare function validateCheckInputs(config: CliConfig, opts?: {
|
|
55
|
+
scope?: ReadonlySet<string>;
|
|
56
|
+
claudeCodeLogin?: boolean;
|
|
57
|
+
env?: Record<string, string | undefined>;
|
|
58
|
+
cwd?: string;
|
|
59
|
+
}): string[];
|