@sweny-ai/core 0.1.123 → 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 +187 -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 +1 -1
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
|
*
|
|
@@ -141,6 +180,30 @@ async function interruptStream(stream) {
|
|
|
141
180
|
/* already finished or not interruptible — nothing to clean up */
|
|
142
181
|
}
|
|
143
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
|
+
}
|
|
144
207
|
export class ClaudeClient {
|
|
145
208
|
model;
|
|
146
209
|
maxTurns;
|
|
@@ -148,6 +211,13 @@ export class ClaudeClient {
|
|
|
148
211
|
logger;
|
|
149
212
|
defaultContext;
|
|
150
213
|
mcpServers;
|
|
214
|
+
envPassthrough;
|
|
215
|
+
sandboxMode;
|
|
216
|
+
sandboxAllowedDomains;
|
|
217
|
+
sandboxProbe;
|
|
218
|
+
sandboxWarned = false;
|
|
219
|
+
envScope;
|
|
220
|
+
envWarned = false;
|
|
151
221
|
constructor(opts = {}) {
|
|
152
222
|
this.model = opts.model;
|
|
153
223
|
this.maxTurns = opts.maxTurns ?? 20;
|
|
@@ -155,18 +225,68 @@ export class ClaudeClient {
|
|
|
155
225
|
this.logger = opts.logger ?? consoleLogger;
|
|
156
226
|
this.defaultContext = opts.defaultContext ?? { config: {}, logger: this.logger };
|
|
157
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;
|
|
158
233
|
}
|
|
159
234
|
/**
|
|
160
|
-
* Build env for the Claude Code subprocess.
|
|
161
|
-
*
|
|
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.
|
|
162
242
|
*/
|
|
163
|
-
buildEnv() {
|
|
164
|
-
const
|
|
165
|
-
|
|
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;
|
|
166
259
|
}
|
|
167
260
|
async run(opts) {
|
|
168
|
-
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;
|
|
169
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
|
+
}
|
|
170
290
|
// Tool-call accounting (Fix #1).
|
|
171
291
|
//
|
|
172
292
|
// Stream-driven recording, correlated by tool_use_id:
|
|
@@ -194,30 +314,37 @@ export class ClaudeClient {
|
|
|
194
314
|
name: "sweny-core",
|
|
195
315
|
tools: sdkTools,
|
|
196
316
|
});
|
|
197
|
-
// Build prompt
|
|
317
|
+
// Build prompt. Context (workflow input such as issues/alerts, plus prior
|
|
318
|
+
// node outputs) is fenced as untrusted data (#360).
|
|
198
319
|
const prompt = [
|
|
199
320
|
`## Instruction\n\n${instruction}`,
|
|
200
|
-
`## Context\n\n
|
|
321
|
+
`## Context\n\n${fenceUntrustedJson(context, "context")}`,
|
|
201
322
|
outputSchema
|
|
202
323
|
? `## Required Output\n\nYou MUST end with a JSON object matching this schema:\n\`\`\`json\n${JSON.stringify(outputSchema, null, 2)}\n\`\`\``
|
|
203
324
|
: "",
|
|
204
325
|
]
|
|
205
326
|
.filter(Boolean)
|
|
206
327
|
.join("\n\n");
|
|
207
|
-
const env = this.buildEnv();
|
|
328
|
+
const env = this.buildEnv(agentAccess?.envVars);
|
|
208
329
|
let response = "";
|
|
209
330
|
// CC-08: when the SDK populates typed structured output (because we passed
|
|
210
331
|
// an `outputFormat`), prefer it over re-parsing JSON out of the free-text
|
|
211
332
|
// result. Left undefined when the SDK didn't provide it, so the
|
|
212
333
|
// tryParseJSON heuristic remains the fallback (e.g. mocks, older results).
|
|
213
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;
|
|
214
341
|
// Timeout / abort wiring (back-compat: only armed when requested).
|
|
215
342
|
// A single AbortController drives both an optional caller signal and an
|
|
216
343
|
// optional timeout timer. The SDK aborts the subprocess when this fires.
|
|
217
344
|
const abort = makeAbort(timeoutMs, signal);
|
|
218
345
|
let stream;
|
|
219
346
|
try {
|
|
220
|
-
const allMcpServers = { ...this.mcpServers };
|
|
347
|
+
const allMcpServers = readOnly ? {} : { ...this.mcpServers };
|
|
221
348
|
if (sdkTools.length > 0)
|
|
222
349
|
allMcpServers["sweny-core"] = mcpServer;
|
|
223
350
|
stream = query({
|
|
@@ -229,10 +356,16 @@ export class ClaudeClient {
|
|
|
229
356
|
env,
|
|
230
357
|
permissionMode: "bypassPermissions",
|
|
231
358
|
allowDangerouslySkipPermissions: true,
|
|
359
|
+
...(sandbox.settings ? { sandbox: sandbox.settings } : {}),
|
|
232
360
|
stderr: (data) => this.logger.debug(`[claude-code] ${data}`),
|
|
233
361
|
...(abort ? { abortController: abort.controller } : {}),
|
|
234
362
|
...(effectiveModel ? { model: effectiveModel } : {}),
|
|
235
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 } : {}),
|
|
236
369
|
...(disallowedTools && disallowedTools.length > 0 ? { disallowedTools } : {}),
|
|
237
370
|
// CC-08: ask the SDK to produce validated structured output when the
|
|
238
371
|
// node declares an output schema. The SDK then returns the parsed
|
|
@@ -314,6 +447,10 @@ export class ClaudeClient {
|
|
|
314
447
|
}
|
|
315
448
|
else if (message.type === "result") {
|
|
316
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);
|
|
317
454
|
if (resultMsg.subtype === "success" && "result" in resultMsg) {
|
|
318
455
|
// terminal_reason was added in @anthropic-ai/claude-agent-sdk v0.2.91.
|
|
319
456
|
// When the turn budget is exhausted the SDK still emits subtype='success'
|
|
@@ -334,6 +471,7 @@ export class ClaudeClient {
|
|
|
334
471
|
...(partial !== "" ? { summary: partial } : {}),
|
|
335
472
|
},
|
|
336
473
|
toolCalls,
|
|
474
|
+
...(usage ? { usage } : {}),
|
|
337
475
|
};
|
|
338
476
|
}
|
|
339
477
|
response = resultMsg.result;
|
|
@@ -354,6 +492,7 @@ export class ClaudeClient {
|
|
|
354
492
|
status: "failed",
|
|
355
493
|
data: { error: prefix + (errors?.join("\n") ?? "Execution failed") },
|
|
356
494
|
toolCalls,
|
|
495
|
+
...(usage ? { usage } : {}),
|
|
357
496
|
};
|
|
358
497
|
}
|
|
359
498
|
}
|
|
@@ -389,6 +528,7 @@ export class ClaudeClient {
|
|
|
389
528
|
status: "success",
|
|
390
529
|
data: { summary: response, ...parsedData },
|
|
391
530
|
toolCalls,
|
|
531
|
+
...(usage ? { usage } : {}),
|
|
392
532
|
};
|
|
393
533
|
}
|
|
394
534
|
async evaluate(opts) {
|
|
@@ -408,6 +548,15 @@ export class ClaudeClient {
|
|
|
408
548
|
env,
|
|
409
549
|
permissionMode: "bypassPermissions",
|
|
410
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],
|
|
411
560
|
stderr: (data) => this.logger.debug(`[claude-code] ${data}`),
|
|
412
561
|
...(abort ? { abortController: abort.controller } : {}),
|
|
413
562
|
...(this.model ? { model: this.model } : {}),
|
|
@@ -420,34 +569,33 @@ export class ClaudeClient {
|
|
|
420
569
|
response = resultMsg.result;
|
|
421
570
|
}
|
|
422
571
|
else {
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
//
|
|
427
|
-
//
|
|
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).
|
|
428
577
|
sdkFailed = true;
|
|
429
|
-
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).`);
|
|
430
579
|
}
|
|
431
580
|
}
|
|
432
581
|
}
|
|
433
582
|
}
|
|
434
583
|
catch (err) {
|
|
435
584
|
if (abort?.reason() === "timeout") {
|
|
436
|
-
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).`);
|
|
437
586
|
}
|
|
438
587
|
else {
|
|
439
|
-
this.logger.warn(`Evaluate query failed: ${err.message}.
|
|
588
|
+
this.logger.warn(`Evaluate query failed: ${err.message}. Failing closed (no route decision).`);
|
|
440
589
|
}
|
|
441
|
-
return
|
|
590
|
+
return null;
|
|
442
591
|
}
|
|
443
592
|
finally {
|
|
444
593
|
abort?.clear();
|
|
445
594
|
await interruptStream(stream);
|
|
446
595
|
}
|
|
447
|
-
// An SDK-level failure already logged a distinct message;
|
|
448
|
-
// the ambiguous-answer warning, which would misattribute the cause.
|
|
596
|
+
// An SDK-level failure already logged a distinct message; fail closed.
|
|
449
597
|
if (sdkFailed)
|
|
450
|
-
return
|
|
598
|
+
return null;
|
|
451
599
|
const text = response.trim().replace(/^["']|["']$/g, "");
|
|
452
600
|
const validIds = choices.map((c) => c.id);
|
|
453
601
|
// Exact match
|
|
@@ -457,15 +605,18 @@ export class ClaudeClient {
|
|
|
457
605
|
const match = validIds.find((id) => text.includes(id));
|
|
458
606
|
if (match)
|
|
459
607
|
return match;
|
|
460
|
-
//
|
|
461
|
-
|
|
462
|
-
|
|
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;
|
|
463
614
|
}
|
|
464
615
|
async ask(opts) {
|
|
465
616
|
const { instruction, context, model, timeoutMs, signal } = opts;
|
|
466
617
|
const prompt = [
|
|
467
618
|
instruction,
|
|
468
|
-
Object.keys(context).length > 0 ? `\nContext:\n
|
|
619
|
+
Object.keys(context).length > 0 ? `\nContext:\n${fenceUntrustedJson(context, "context")}` : "",
|
|
469
620
|
]
|
|
470
621
|
.filter(Boolean)
|
|
471
622
|
.join("\n");
|
|
@@ -483,6 +634,15 @@ export class ClaudeClient {
|
|
|
483
634
|
env,
|
|
484
635
|
permissionMode: "bypassPermissions",
|
|
485
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],
|
|
486
646
|
stderr: (data) => this.logger.debug(`[claude-code] ${data}`),
|
|
487
647
|
...(abort ? { abortController: abort.controller } : {}),
|
|
488
648
|
...(effectiveModel ? { model: effectiveModel } : {}),
|
|
@@ -582,7 +742,7 @@ export function buildEvaluatePrompt(question, context, choices) {
|
|
|
582
742
|
const choiceList = choices.map((c) => `- "${c.id}": ${c.description}`).join("\n");
|
|
583
743
|
return [
|
|
584
744
|
question,
|
|
585
|
-
`\nContext:\n
|
|
745
|
+
`\nContext:\n${fenceUntrustedJson(context, "context")}`,
|
|
586
746
|
`\nChoices:\n${choiceList}`,
|
|
587
747
|
`\nEvaluation rules:`,
|
|
588
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[];
|