@sweny-ai/core 0.1.143 → 0.1.144
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/cli/main.js +1 -0
- package/dist/cli/run-output.js +4 -0
- package/dist/executor.d.ts +12 -0
- package/dist/executor.js +108 -6
- package/dist/harness/claude-code.d.ts +12 -4
- package/dist/harness/claude-code.js +41 -8
- package/dist/harness/codex.js +11 -7
- package/dist/index.d.ts +6 -2
- package/dist/index.js +4 -1
- package/dist/node-policy.d.ts +31 -0
- package/dist/node-policy.js +42 -0
- package/dist/safe-outputs.d.ts +103 -0
- package/dist/safe-outputs.js +460 -0
- package/dist/schema.d.ts +489 -1
- package/dist/schema.js +203 -1
- package/dist/skills/github.js +18 -0
- package/dist/types.d.ts +99 -0
- package/dist/types.js +29 -0
- package/package.json +1 -1
package/dist/cli/main.js
CHANGED
|
@@ -964,6 +964,7 @@ export async function workflowRunAction(file, options) {
|
|
|
964
964
|
fileRoot: config.fileRoot || undefined,
|
|
965
965
|
signal,
|
|
966
966
|
max_steps: wfMaxSteps,
|
|
967
|
+
stageOutputs: options.stage === true,
|
|
967
968
|
}), wfTimeoutMs, `Workflow ${workflow.name}`);
|
|
968
969
|
const wfDurationMs = Date.now() - runStart;
|
|
969
970
|
const wfHasFailed = [...results.values()].some((r) => r.status === "failed");
|
package/dist/cli/run-output.js
CHANGED
|
@@ -198,6 +198,10 @@ export const WORKFLOW_RUN_OPTIONS = [
|
|
|
198
198
|
"--dry-run",
|
|
199
199
|
"Run with read-only tools only: write tools, external MCP servers, and shell/file-edit tools are withheld, so nothing is created, posted, or sent. Stops at the first conditional edge. To inspect nodes without running, use --list-nodes.",
|
|
200
200
|
],
|
|
201
|
+
[
|
|
202
|
+
"--stage",
|
|
203
|
+
"Run normally, but preview every safe output (nodes with outputs:) instead of writing it: print what would be written and write nothing",
|
|
204
|
+
],
|
|
201
205
|
["--list-nodes", "Validate, print nodes and skills, and exit without running"],
|
|
202
206
|
["--json", "Output result as JSON on stdout; suppress progress output"],
|
|
203
207
|
["--stream", "Stream NDJSON events to stdout (for Studio / automation)"],
|
package/dist/executor.d.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import type { Workflow, Skill, Tool, Claude, Observer, Logger, ExecutionResult } from "./types.js";
|
|
16
16
|
import type { AgentHarness } from "./harness/types.js";
|
|
17
|
+
import { type ActorInfo } from "./safe-outputs.js";
|
|
17
18
|
export interface ExecuteOptions {
|
|
18
19
|
/** Registered skills (id → Skill) */
|
|
19
20
|
skills: Map<string, Skill>;
|
|
@@ -73,6 +74,17 @@ export interface ExecuteOptions {
|
|
|
73
74
|
* whole-run budget. Default: no timeout (back-compat).
|
|
74
75
|
*/
|
|
75
76
|
timeoutMs?: number;
|
|
77
|
+
/**
|
|
78
|
+
* Safe outputs (#365): preview every write and apply none (CLI `--stage`).
|
|
79
|
+
* A dry run always stages. Default: false.
|
|
80
|
+
*/
|
|
81
|
+
stageOutputs?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Who triggered the run, for `safe_outputs.trusted_actors` /
|
|
84
|
+
* `trusted_associations`. Default: `GITHUB_ACTOR` and the author
|
|
85
|
+
* association in the `GITHUB_EVENT_PATH` payload.
|
|
86
|
+
*/
|
|
87
|
+
actor?: ActorInfo;
|
|
76
88
|
}
|
|
77
89
|
/**
|
|
78
90
|
* Default workflow-level step budget. Counts total node executions across the
|
package/dist/executor.js
CHANGED
|
@@ -23,7 +23,9 @@ import { validateWorkflow } from "./schema.js";
|
|
|
23
23
|
import { resolveAgentAccess } from "./agent-env.js";
|
|
24
24
|
import { fenceUntrusted } from "./untrusted.js";
|
|
25
25
|
import { asClaude } from "./harness/compat.js";
|
|
26
|
-
import { isToolClass } from "./harness/policy.js";
|
|
26
|
+
import { isToolClass, policyGate } from "./harness/policy.js";
|
|
27
|
+
import { buildNodePolicy, resolveNodePermissions } from "./node-policy.js";
|
|
28
|
+
import { applySafeOutputs, createEmitOutputTool, createWriteStageState, resolveActor, safeOutputsInstruction, unresolvedOutputs, SAFE_OUTPUT_SCREEN_INSTRUCTION, } from "./safe-outputs.js";
|
|
27
29
|
/**
|
|
28
30
|
* Default workflow-level step budget. Counts total node executions across the
|
|
29
31
|
* whole run (loop iterations included). Generous enough for legitimate deep
|
|
@@ -71,6 +73,13 @@ export async function execute(workflow, input, options) {
|
|
|
71
73
|
let stepCount = 0; // total node executions across the run (loop iterations included)
|
|
72
74
|
const trace = { steps: [], edges: [], sources: {} };
|
|
73
75
|
validate(workflow, skills);
|
|
76
|
+
// Safe outputs (#365): run-wide caps and dedupe, the staged switch and the
|
|
77
|
+
// triggering actor, resolved once per run.
|
|
78
|
+
const writeState = createWriteStageState();
|
|
79
|
+
const stageOutputs = dryRun || options.stageOutputs === true || workflow.safe_outputs?.staged === true;
|
|
80
|
+
const runEnv = options.env ?? process.env;
|
|
81
|
+
const usesOutputs = Object.values(workflow.nodes).some((n) => (n.outputs?.length ?? 0) > 0);
|
|
82
|
+
const actor = usesOutputs ? resolveActor(runEnv, options.actor) : {};
|
|
74
83
|
// Build an eval-time alias table from the loaded skills. Each skill owns
|
|
75
84
|
// its own mapping between skill-tool names and equivalent MCP names. Core
|
|
76
85
|
// stays vendor-neutral; this call just unions what the skills declare.
|
|
@@ -150,11 +159,32 @@ export async function execute(workflow, input, options) {
|
|
|
150
159
|
// Dry run (#380): only `access: "read"` tools reach the node. Write and
|
|
151
160
|
// unclassified tools are withheld (never registered, so never callable)
|
|
152
161
|
// and recorded on the node result as `skippedWrites`.
|
|
153
|
-
|
|
162
|
+
// #365: `permissions: read` (explicit, inherited, or implied by `outputs`)
|
|
163
|
+
// gets the same tool gate as a dry run.
|
|
164
|
+
const permissions = resolveNodePermissions(node, workflow);
|
|
165
|
+
const declaresPolicy = node.permissions !== undefined || workflow.permissions !== undefined || (node.outputs?.length ?? 0) > 0;
|
|
166
|
+
const readOnlyNode = dryRun || permissions.access === "read";
|
|
167
|
+
const readTools = readOnlyNode ? filteredTools.filter(isReadTool) : filteredTools;
|
|
154
168
|
const skippedWrites = dryRun ? filteredTools.filter((t) => !isReadTool(t)).map((t) => t.name) : [];
|
|
155
169
|
if (skippedWrites.length > 0) {
|
|
156
170
|
logger.info(` dry run: withheld write tools: ${skippedWrites.join(", ")}`, { node: currentId });
|
|
157
171
|
}
|
|
172
|
+
else if (readOnlyNode && readTools.length < filteredTools.length) {
|
|
173
|
+
const withheld = filteredTools.filter((t) => !isReadTool(t)).map((t) => t.name);
|
|
174
|
+
logger.info(` permissions: read: withheld write tools: ${withheld.join(", ")}`, { node: currentId });
|
|
175
|
+
}
|
|
176
|
+
// Safe outputs (#365): the agent records write intents with emit_output;
|
|
177
|
+
// the write stage after the node applies them. One buffer per attempt.
|
|
178
|
+
const outputDecls = node.outputs ?? [];
|
|
179
|
+
const intents = [];
|
|
180
|
+
const tools = outputDecls.length > 0 ? [...readTools, createEmitOutputTool(outputDecls, intents)] : readTools;
|
|
181
|
+
if (outputDecls.length > 0 && !stageOutputs) {
|
|
182
|
+
const missing = unresolvedOutputs(outputDecls, node.skills, skills);
|
|
183
|
+
if (missing.length > 0) {
|
|
184
|
+
throw new Error(`Node "${currentId}" declares outputs [${missing.join(", ")}] but no configured skill can apply them ` +
|
|
185
|
+
`(github or linear). Set the skill's environment variables, or run with --stage to preview.`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
158
188
|
const skillInstructions = resolveSkillInstructions(node.skills, skills);
|
|
159
189
|
const skillMcpServers = resolveSkillMcpServers(node.skills, skills);
|
|
160
190
|
// Runtime guard: if this node declares skills but none resolved, the node
|
|
@@ -227,7 +257,8 @@ export async function execute(workflow, input, options) {
|
|
|
227
257
|
const effectiveRules = assembleCascaded("rules", currentId, workflow.nodes[currentId], inputRules.length, (workflow.rules ?? []).length, resolvedSources);
|
|
228
258
|
const effectiveContext = assembleCascaded("context", currentId, workflow.nodes[currentId], inputContext.length, (workflow.context ?? []).length, resolvedSources);
|
|
229
259
|
const baseInstruction = buildNodeInstruction(resolvedInstruction, effectiveRules, effectiveContext, input, skillInstructions);
|
|
230
|
-
const
|
|
260
|
+
const nodeInstruction = outputDecls.length > 0 ? `${baseInstruction}\n\n${safeOutputsInstruction(outputDecls)}` : baseInstruction;
|
|
261
|
+
const instruction = dryRun ? `${dryRunNotice(skippedWrites)}\n\n---\n\n${nodeInstruction}` : nodeInstruction;
|
|
231
262
|
// Run Claude on this node, with optional eval-failure retry loop.
|
|
232
263
|
let attempt = 0;
|
|
233
264
|
let result;
|
|
@@ -246,7 +277,38 @@ export async function execute(workflow, input, options) {
|
|
|
246
277
|
// compiles them or reports them in `degraded`.
|
|
247
278
|
const denyClasses = (node.tools?.deny ?? []).filter(isToolClass);
|
|
248
279
|
let degradedLogged = false;
|
|
280
|
+
const agentAccess = resolveAgentAccess(node.skills, skills);
|
|
281
|
+
// #365: a node or workflow that declares `permissions` or `outputs` gets
|
|
282
|
+
// one portable policy, compiled by each adapter. The gate also runs here so
|
|
283
|
+
// a strict refusal holds on every harness, before any spend. Nodes that
|
|
284
|
+
// declare neither pass no policy, so each harness keeps its own defaults.
|
|
285
|
+
const nodePolicy = declaresPolicy
|
|
286
|
+
? buildNodePolicy({
|
|
287
|
+
permissions: { ...permissions, deny: [...new Set([...permissions.deny, ...denyClasses])] },
|
|
288
|
+
dryRun,
|
|
289
|
+
disallowedTools: node.disallowed_tools,
|
|
290
|
+
egress: agentAccess.domains,
|
|
291
|
+
})
|
|
292
|
+
: undefined;
|
|
293
|
+
// Egress is left to the adapter: only it knows whether a process sandbox
|
|
294
|
+
// wrapper covers what its native sandbox cannot.
|
|
295
|
+
const gate = nodePolicy && options.harness
|
|
296
|
+
? policyGate(options.harness.capabilities, { ...nodePolicy, egress: [] })
|
|
297
|
+
: undefined;
|
|
249
298
|
while (true) {
|
|
299
|
+
if (gate?.refuse) {
|
|
300
|
+
// Not an agent failure: fail_soft never softens a policy refusal.
|
|
301
|
+
logger.warn(` harness refused the node: ${gate.refuse}`, { node: currentId });
|
|
302
|
+
result = {
|
|
303
|
+
status: "failed",
|
|
304
|
+
data: { error: gate.refuse, refused: true },
|
|
305
|
+
toolCalls: [],
|
|
306
|
+
degraded: gate.degraded,
|
|
307
|
+
};
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
// Only the final attempt's intents may be applied.
|
|
311
|
+
intents.length = 0;
|
|
250
312
|
result = await claude.run({
|
|
251
313
|
instruction: currentInstruction,
|
|
252
314
|
context,
|
|
@@ -258,9 +320,10 @@ export async function execute(workflow, input, options) {
|
|
|
258
320
|
model: nodeModel,
|
|
259
321
|
signal,
|
|
260
322
|
timeoutMs,
|
|
261
|
-
agentAccess
|
|
262
|
-
...(
|
|
263
|
-
...(
|
|
323
|
+
agentAccess,
|
|
324
|
+
...(nodePolicy ? { policy: nodePolicy } : {}),
|
|
325
|
+
...(readOnlyNode ? {} : { mcpServers: skillMcpServers }),
|
|
326
|
+
...(readOnlyNode ? { readOnly: true } : {}),
|
|
264
327
|
onProgress: (message) => {
|
|
265
328
|
safeObserve(observer, { type: "node:progress", node: currentId, message }, logger);
|
|
266
329
|
},
|
|
@@ -381,6 +444,45 @@ export async function execute(workflow, input, options) {
|
|
|
381
444
|
data: { ...(result.data ?? {}), fail_soft: true },
|
|
382
445
|
};
|
|
383
446
|
}
|
|
447
|
+
// Safe outputs write stage (#365). Runs only after the agent finished and
|
|
448
|
+
// every eval passed; deterministic checks first, an optional model screen
|
|
449
|
+
// may veto, and only then does sweny call the skill's own write handler.
|
|
450
|
+
if (outputDecls.length > 0) {
|
|
451
|
+
if (result.status === "success" && !agentRunFailed) {
|
|
452
|
+
const stage = await applySafeOutputs({
|
|
453
|
+
nodeId: currentId,
|
|
454
|
+
declarations: outputDecls,
|
|
455
|
+
intents,
|
|
456
|
+
policy: workflow.safe_outputs,
|
|
457
|
+
nodeSkills: node.skills,
|
|
458
|
+
skills,
|
|
459
|
+
config,
|
|
460
|
+
env: runEnv,
|
|
461
|
+
actor,
|
|
462
|
+
staged: stageOutputs,
|
|
463
|
+
state: writeState,
|
|
464
|
+
logger,
|
|
465
|
+
screen: async (writes) => claude.ask({
|
|
466
|
+
instruction: SAFE_OUTPUT_SCREEN_INSTRUCTION,
|
|
467
|
+
context: { writes },
|
|
468
|
+
model: nodeModel,
|
|
469
|
+
signal,
|
|
470
|
+
timeoutMs,
|
|
471
|
+
}),
|
|
472
|
+
});
|
|
473
|
+
result = { ...result, outputs: stage.receipts };
|
|
474
|
+
if (stage.error) {
|
|
475
|
+
logger.warn(` ${stage.error}`, { node: currentId });
|
|
476
|
+
result = { ...result, status: "failed", data: { ...result.data, error: stage.error } };
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
else if (intents.length > 0) {
|
|
480
|
+
result = {
|
|
481
|
+
...result,
|
|
482
|
+
outputs: intents.map((i) => ({ type: i.type, status: "skipped", reason: "node did not succeed" })),
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
}
|
|
384
486
|
if (skippedWrites.length > 0)
|
|
385
487
|
result = { ...result, skippedWrites };
|
|
386
488
|
results.set(currentId, result);
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { Claude, Tool, ToolContext, Logger, McpServerConfig } from "../types.js";
|
|
12
12
|
import { type SandboxMode } from "../agent-env.js";
|
|
13
|
-
import type { AgentHarness, HarnessCapabilities, HarnessCompleteRequest, HarnessInfo, HarnessRunRequest, HarnessRunResult, ToolClass } from "./types.js";
|
|
13
|
+
import type { AgentHarness, HarnessCapabilities, HarnessCompleteRequest, HarnessInfo, HarnessRunRequest, HarnessRunResult, NodePolicy, ToolClass } from "./types.js";
|
|
14
14
|
import { CLAUDE_CODE_CAPABILITIES } from "./capabilities.js";
|
|
15
15
|
import { buildEvaluatePrompt } from "./prompts.js";
|
|
16
16
|
import { parseToolResultContent, summarizeToolError } from "./parse.js";
|
|
@@ -42,6 +42,8 @@ export declare const READ_ONLY_DISALLOWED_TOOLS: readonly ["Bash", "Write", "Edi
|
|
|
42
42
|
* `tools.deny` classes compile to these names on `disallowedTools`.
|
|
43
43
|
*/
|
|
44
44
|
export declare const CLAUDE_TOOLS_BY_CLASS: Readonly<Record<ToolClass, readonly string[]>>;
|
|
45
|
+
/** Native `disallowedTools` for a policy: legacy names, compiled classes, and the read-only set. */
|
|
46
|
+
export declare function compileClaudeCodeDeny(policy: NodePolicy, legacy?: readonly string[]): string[];
|
|
45
47
|
/** How sweny resolves which credentials reach the Claude Code subprocess. */
|
|
46
48
|
export type SwenyAuthMode = "auto" | "api-key" | "oauth";
|
|
47
49
|
export interface ResolveAuthEnvOpts {
|
|
@@ -165,9 +167,15 @@ export declare class ClaudeCodeHarness implements Claude, AgentHarness {
|
|
|
165
167
|
version: string;
|
|
166
168
|
}>;
|
|
167
169
|
/**
|
|
168
|
-
* Run a node.
|
|
169
|
-
*
|
|
170
|
-
*
|
|
170
|
+
* Run a node. Runs {@link policyGate} (always `degraded: []` for Claude
|
|
171
|
+
* Code), compiles the policy to native options, and tags the result with the
|
|
172
|
+
* harness id and version.
|
|
173
|
+
*
|
|
174
|
+
* Policy compile (#365): `policy.readOnly` (or the legacy `readOnly` flag)
|
|
175
|
+
* is a read-only run; `policy.deny` classes become native `disallowedTools`
|
|
176
|
+
* names, merged with `nativeDeny` and the legacy `disallowedTools`; and
|
|
177
|
+
* `policy.strict` makes MCP exclusive (`strictMcpConfig`) even for a
|
|
178
|
+
* write-capable node.
|
|
171
179
|
*/
|
|
172
180
|
run(req: HarnessRunRequest): Promise<HarnessRunResult>;
|
|
173
181
|
private runQuery;
|
|
@@ -85,6 +85,17 @@ export const CLAUDE_TOOLS_BY_CLASS = {
|
|
|
85
85
|
net: ["WebFetch", "WebSearch"],
|
|
86
86
|
subagent: ["Task", "Agent"],
|
|
87
87
|
};
|
|
88
|
+
/** Native `disallowedTools` for a policy: legacy names, compiled classes, and the read-only set. */
|
|
89
|
+
export function compileClaudeCodeDeny(policy, legacy = []) {
|
|
90
|
+
return [
|
|
91
|
+
...new Set([
|
|
92
|
+
...legacy,
|
|
93
|
+
...(policy.nativeDeny ?? []),
|
|
94
|
+
...policy.deny.flatMap((c) => CLAUDE_TOOLS_BY_CLASS[c] ?? []),
|
|
95
|
+
...(policy.readOnly ? READ_ONLY_DISALLOWED_TOOLS : []),
|
|
96
|
+
]),
|
|
97
|
+
];
|
|
98
|
+
}
|
|
88
99
|
/**
|
|
89
100
|
* Decide which auth credentials survive into the Claude Code subprocess env.
|
|
90
101
|
*
|
|
@@ -253,9 +264,15 @@ export class ClaudeCodeHarness {
|
|
|
253
264
|
return { ok: true, version: claudeSdkVersion() };
|
|
254
265
|
}
|
|
255
266
|
/**
|
|
256
|
-
* Run a node.
|
|
257
|
-
*
|
|
258
|
-
*
|
|
267
|
+
* Run a node. Runs {@link policyGate} (always `degraded: []` for Claude
|
|
268
|
+
* Code), compiles the policy to native options, and tags the result with the
|
|
269
|
+
* harness id and version.
|
|
270
|
+
*
|
|
271
|
+
* Policy compile (#365): `policy.readOnly` (or the legacy `readOnly` flag)
|
|
272
|
+
* is a read-only run; `policy.deny` classes become native `disallowedTools`
|
|
273
|
+
* names, merged with `nativeDeny` and the legacy `disallowedTools`; and
|
|
274
|
+
* `policy.strict` makes MCP exclusive (`strictMcpConfig`) even for a
|
|
275
|
+
* write-capable node.
|
|
259
276
|
*/
|
|
260
277
|
async run(req) {
|
|
261
278
|
const policy = req.policy ?? {
|
|
@@ -266,10 +283,25 @@ export class ClaudeCodeHarness {
|
|
|
266
283
|
strict: false,
|
|
267
284
|
};
|
|
268
285
|
const gate = policyGate(this.capabilities, policy);
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
286
|
+
if (gate.refuse) {
|
|
287
|
+
this.logger.error(gate.refuse);
|
|
288
|
+
return {
|
|
289
|
+
status: "failed",
|
|
290
|
+
// `refused` keeps fail_soft from softening a policy refusal (executor.ts).
|
|
291
|
+
data: { error: gate.refuse, refused: true },
|
|
292
|
+
toolCalls: [],
|
|
293
|
+
harness: this.info(),
|
|
294
|
+
degraded: gate.degraded,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const readOnly = !!req.readOnly || policy.readOnly;
|
|
298
|
+
const disallowedTools = compileClaudeCodeDeny(policy, req.disallowedTools ?? []);
|
|
299
|
+
const result = await this.runQuery({
|
|
300
|
+
...req,
|
|
301
|
+
readOnly,
|
|
302
|
+
disallowedTools: disallowedTools.length > 0 ? disallowedTools : undefined,
|
|
303
|
+
strictMcp: readOnly || policy.strict,
|
|
304
|
+
});
|
|
273
305
|
return { ...result, harness: this.info(), degraded: gate.degraded };
|
|
274
306
|
}
|
|
275
307
|
async runQuery(opts) {
|
|
@@ -396,7 +428,8 @@ export class ClaudeCodeHarness {
|
|
|
396
428
|
// project and local settings, including their MCP servers (and
|
|
397
429
|
// project .mcp.json, plugins). strictMcpConfig limits MCP to the
|
|
398
430
|
// servers passed above, which under readOnly is only sweny-core.
|
|
399
|
-
|
|
431
|
+
// A strict policy (#365) makes it exclusive for write nodes too.
|
|
432
|
+
...(readOnly || opts.strictMcp ? { strictMcpConfig: true } : {}),
|
|
400
433
|
...(disallowedTools && disallowedTools.length > 0 ? { disallowedTools } : {}),
|
|
401
434
|
// CC-08: ask the SDK to produce validated structured output when the
|
|
402
435
|
// node declares an output schema. The SDK then returns the parsed
|
package/dist/harness/codex.js
CHANGED
|
@@ -338,13 +338,17 @@ export class CodexHarness {
|
|
|
338
338
|
}
|
|
339
339
|
/** The node policy after `disallowed_tools` names are translated into Codex classes. */
|
|
340
340
|
compilePolicy(req, maxTurns) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
341
|
+
// A node policy (#365) can make the run stricter, never looser than the
|
|
342
|
+
// harness policy mode.
|
|
343
|
+
const base = req.policy
|
|
344
|
+
? { ...req.policy, strict: req.policy.strict || this.policyMode === "strict" }
|
|
345
|
+
: {
|
|
346
|
+
readOnly: !!req.readOnly,
|
|
347
|
+
deny: req.deny ?? [],
|
|
348
|
+
nativeDeny: req.disallowedTools,
|
|
349
|
+
egress: req.agentAccess?.domains ?? [],
|
|
350
|
+
strict: this.policyMode === "strict",
|
|
351
|
+
};
|
|
348
352
|
const { classes, unknown } = translateDenyNames(base.nativeDeny);
|
|
349
353
|
return {
|
|
350
354
|
...base,
|
package/dist/index.d.ts
CHANGED
|
@@ -21,12 +21,16 @@
|
|
|
21
21
|
* })
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
|
-
export type { Skill, SkillCategory, SkillDefinition, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, TraceStep, TraceEdge, ExecutionTrace, ExecutionResult, Source, ResolvedSource, SourceKind, SourceResolutionMap, WorkflowInputs, WorkflowInputField, WorkflowInputType, InputValidationError, InputValidationResult, Evaluator, EvaluatorRule, EvalResult, EvaluatorKind, NodeRequires, NodeRetry, NodeToolFilter, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, NodeOnFail, McpTransport, WorkflowType, SkillHarnessKey, } from "./types.js";
|
|
24
|
+
export type { Skill, SkillCategory, SkillDefinition, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, TraceStep, TraceEdge, ExecutionTrace, ExecutionResult, Source, ResolvedSource, SourceKind, SourceResolutionMap, WorkflowInputs, WorkflowInputField, WorkflowInputType, InputValidationError, InputValidationResult, Evaluator, EvaluatorRule, EvalResult, EvaluatorKind, NodeRequires, NodeRetry, NodeToolFilter, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, NodeOnFail, McpTransport, WorkflowType, SkillHarnessKey, NodeAccess, NodePermissions, NodePermissionsSpec, SafeOutputType, SafeOutputDeclaration, SafeOutputsPolicy, SafeOutputReceipt, AuthorAssociation, } from "./types.js";
|
|
25
25
|
export { WORKFLOW_INPUT_TYPES } from "./types.js";
|
|
26
|
-
export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, NODE_ON_FAIL, MCP_TRANSPORTS, SKILL_CATEGORIES, SKILL_HARNESSES, SKILL_ID_PATTERN, SKILL_ID_MAX_LENGTH, isValidSkillId, } from "./types.js";
|
|
26
|
+
export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, NODE_ON_FAIL, MCP_TRANSPORTS, SKILL_CATEGORIES, SKILL_HARNESSES, SKILL_ID_PATTERN, SKILL_ID_MAX_LENGTH, isValidSkillId, NODE_ACCESS, TOOL_CLASSES, SAFE_OUTPUT_TYPES, SAFE_OUTPUT_APPLIERS, AUTHOR_ASSOCIATIONS, } from "./types.js";
|
|
27
27
|
export { consoleLogger } from "./types.js";
|
|
28
28
|
export { execute, RouteEvaluationError } from "./executor.js";
|
|
29
29
|
export type { ExecuteOptions } from "./executor.js";
|
|
30
|
+
export { resolveNodePermissions, buildNodePolicy } from "./node-policy.js";
|
|
31
|
+
export type { ResolvedPermissions } from "./node-policy.js";
|
|
32
|
+
export { applySafeOutputs, createEmitOutputTool, resolveActor, EMIT_OUTPUT_TOOL } from "./safe-outputs.js";
|
|
33
|
+
export type { SafeOutputIntent, ActorInfo, WriteStageState } from "./safe-outputs.js";
|
|
30
34
|
export { createHarness, claudeCompat, asClaude, policyGate, ClaudeCodeHarness, CodexHarness, SUPPORTED_AGENTS, } from "./harness/index.js";
|
|
31
35
|
export type { AgentHarness, HarnessCapabilities, HarnessId, HarnessInfo, HarnessRunRequest, HarnessRunResult, NodePolicy, PolicyGateResult, PolicyWrappers, ToolClass, ClaudeCodeHarnessOptions, CodexHarnessOptions, } from "./harness/index.js";
|
|
32
36
|
export { ClaudeClient, resolveAuthEnv } from "./harness/claude-code.js";
|
package/dist/index.js
CHANGED
|
@@ -23,10 +23,13 @@
|
|
|
23
23
|
*/
|
|
24
24
|
export { WORKFLOW_INPUT_TYPES } from "./types.js";
|
|
25
25
|
// Runtime enum constants + skill-id helpers
|
|
26
|
-
export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, NODE_ON_FAIL, MCP_TRANSPORTS, SKILL_CATEGORIES, SKILL_HARNESSES, SKILL_ID_PATTERN, SKILL_ID_MAX_LENGTH, isValidSkillId, } from "./types.js";
|
|
26
|
+
export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, NODE_ON_FAIL, MCP_TRANSPORTS, SKILL_CATEGORIES, SKILL_HARNESSES, SKILL_ID_PATTERN, SKILL_ID_MAX_LENGTH, isValidSkillId, NODE_ACCESS, TOOL_CLASSES, SAFE_OUTPUT_TYPES, SAFE_OUTPUT_APPLIERS, AUTHOR_ASSOCIATIONS, } from "./types.js";
|
|
27
27
|
export { consoleLogger } from "./types.js";
|
|
28
28
|
// Executor
|
|
29
29
|
export { execute, RouteEvaluationError } from "./executor.js";
|
|
30
|
+
// Least-privilege nodes and safe outputs (#365)
|
|
31
|
+
export { resolveNodePermissions, buildNodePolicy } from "./node-policy.js";
|
|
32
|
+
export { applySafeOutputs, createEmitOutputTool, resolveActor, EMIT_OUTPUT_TOOL } from "./safe-outputs.js";
|
|
30
33
|
// Agent harness (the seam) and its adapters: Claude Code and Codex
|
|
31
34
|
export { createHarness, claudeCompat, asClaude, policyGate, ClaudeCodeHarness, CodexHarness, SUPPORTED_AGENTS, } from "./harness/index.js";
|
|
32
35
|
// Claude client (back-compat: ClaudeClient is ClaudeCodeHarness, @deprecated)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node permissions (#365): resolve what a node's agent may do.
|
|
3
|
+
*
|
|
4
|
+
* Pure and browser-safe. The workflow's `permissions` is both the default and
|
|
5
|
+
* the ceiling for every node; a node may narrow it, never widen it.
|
|
6
|
+
*/
|
|
7
|
+
import type { NodePolicy, ToolClass } from "./harness/types.js";
|
|
8
|
+
import type { Node, NodeAccess, Workflow } from "./types.js";
|
|
9
|
+
export interface ResolvedPermissions {
|
|
10
|
+
access: NodeAccess;
|
|
11
|
+
deny: ToolClass[];
|
|
12
|
+
strict: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Effective permissions for a node.
|
|
16
|
+
*
|
|
17
|
+
* access: the node's own, else `read` when it declares `outputs`, else the
|
|
18
|
+
* workflow's, else `write` (today's behavior). A workflow `read` ceiling wins
|
|
19
|
+
* over a node `write` (the loader rejects that combination; this is the
|
|
20
|
+
* runtime backstop). deny: union. strict: either.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveNodePermissions(node: Pick<Node, "permissions" | "outputs">, workflow?: Pick<Workflow, "permissions">): ResolvedPermissions;
|
|
23
|
+
/** True when a node asks for more than the workflow ceiling allows. */
|
|
24
|
+
export declare function exceedsPermissionCeiling(node: Pick<Node, "permissions">, workflow: Pick<Workflow, "permissions">): boolean;
|
|
25
|
+
/** The portable policy handed to the harness for one node run. */
|
|
26
|
+
export declare function buildNodePolicy(opts: {
|
|
27
|
+
permissions: ResolvedPermissions;
|
|
28
|
+
dryRun: boolean;
|
|
29
|
+
disallowedTools?: string[];
|
|
30
|
+
egress?: string[];
|
|
31
|
+
}): NodePolicy;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node permissions (#365): resolve what a node's agent may do.
|
|
3
|
+
*
|
|
4
|
+
* Pure and browser-safe. The workflow's `permissions` is both the default and
|
|
5
|
+
* the ceiling for every node; a node may narrow it, never widen it.
|
|
6
|
+
*/
|
|
7
|
+
function spec(p) {
|
|
8
|
+
if (p === undefined)
|
|
9
|
+
return {};
|
|
10
|
+
return typeof p === "string" ? { access: p } : p;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Effective permissions for a node.
|
|
14
|
+
*
|
|
15
|
+
* access: the node's own, else `read` when it declares `outputs`, else the
|
|
16
|
+
* workflow's, else `write` (today's behavior). A workflow `read` ceiling wins
|
|
17
|
+
* over a node `write` (the loader rejects that combination; this is the
|
|
18
|
+
* runtime backstop). deny: union. strict: either.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveNodePermissions(node, workflow = {}) {
|
|
21
|
+
const wf = spec(workflow.permissions);
|
|
22
|
+
const own = spec(node.permissions);
|
|
23
|
+
let access = own.access ?? (node.outputs && node.outputs.length > 0 ? "read" : (wf.access ?? "write"));
|
|
24
|
+
if (wf.access === "read")
|
|
25
|
+
access = "read";
|
|
26
|
+
const deny = [...new Set([...(wf.deny ?? []), ...(own.deny ?? [])])];
|
|
27
|
+
return { access, deny, strict: wf.strict === true || own.strict === true };
|
|
28
|
+
}
|
|
29
|
+
/** True when a node asks for more than the workflow ceiling allows. */
|
|
30
|
+
export function exceedsPermissionCeiling(node, workflow) {
|
|
31
|
+
return spec(workflow.permissions).access === "read" && spec(node.permissions).access === "write";
|
|
32
|
+
}
|
|
33
|
+
/** The portable policy handed to the harness for one node run. */
|
|
34
|
+
export function buildNodePolicy(opts) {
|
|
35
|
+
return {
|
|
36
|
+
readOnly: opts.dryRun || opts.permissions.access === "read",
|
|
37
|
+
deny: opts.permissions.deny,
|
|
38
|
+
...(opts.disallowedTools && opts.disallowedTools.length > 0 ? { nativeDeny: opts.disallowedTools } : {}),
|
|
39
|
+
egress: opts.egress ?? [],
|
|
40
|
+
strict: opts.permissions.strict,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe outputs (#365): a deterministic write boundary for any agent.
|
|
3
|
+
*
|
|
4
|
+
* A node that declares `outputs` gets one extra tool, `emit_output`, which only
|
|
5
|
+
* records a typed write intent in memory. After the node succeeds, the write
|
|
6
|
+
* stage ({@link applySafeOutputs}) checks every intent against the node's
|
|
7
|
+
* declarations and the workflow's `safe_outputs` policy, then applies what
|
|
8
|
+
* passes by calling the owning skill's own tool handler. The agent never holds
|
|
9
|
+
* the write, so this holds on every harness.
|
|
10
|
+
*
|
|
11
|
+
* Order of checks per intent (all deterministic): declared type, workflow
|
|
12
|
+
* ceiling, actor trust, expiry, skill, target pin, shape and size, labels,
|
|
13
|
+
* title prefix, dedupe, node cap, run cap. An optional model screen runs last
|
|
14
|
+
* over the intents that passed and can only veto them.
|
|
15
|
+
*/
|
|
16
|
+
import type { Logger, SafeOutputDeclaration, SafeOutputReceipt, SafeOutputsPolicy, SafeOutputType, Skill, Tool } from "./types.js";
|
|
17
|
+
/** Name of the intent-recording tool a node with `outputs` receives. */
|
|
18
|
+
export declare const EMIT_OUTPUT_TOOL = "emit_output";
|
|
19
|
+
/** Longest title the write stage accepts (after the prefix). */
|
|
20
|
+
export declare const SAFE_OUTPUT_TITLE_MAX = 256;
|
|
21
|
+
/** Longest body the write stage accepts (GitHub's own limit). */
|
|
22
|
+
export declare const SAFE_OUTPUT_BODY_MAX = 65536;
|
|
23
|
+
/** One write the agent asked for. Recorded by `emit_output`, never applied by it. */
|
|
24
|
+
export interface SafeOutputIntent {
|
|
25
|
+
type: string;
|
|
26
|
+
title?: string;
|
|
27
|
+
body?: string;
|
|
28
|
+
target?: string;
|
|
29
|
+
/** Issue / PR number (GitHub) or issue id (Linear) for comment and label. */
|
|
30
|
+
number?: string;
|
|
31
|
+
labels?: string[];
|
|
32
|
+
head?: string;
|
|
33
|
+
base?: string;
|
|
34
|
+
dedupe_key?: string;
|
|
35
|
+
/** ms since epoch when the intent was recorded. */
|
|
36
|
+
recordedAt: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Build the `emit_output` tool for one node. `access: "read"`: it changes
|
|
40
|
+
* nothing outside the run, so it survives dry runs and read-only nodes.
|
|
41
|
+
* Its checks are advisory (they tell the agent early); the write stage
|
|
42
|
+
* re-checks everything.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createEmitOutputTool(declarations: SafeOutputDeclaration[], buffer: SafeOutputIntent[], now?: () => number): Tool;
|
|
45
|
+
/** Instruction block that tells the agent how writes work at this node. */
|
|
46
|
+
export declare function safeOutputsInstruction(declarations: SafeOutputDeclaration[]): string;
|
|
47
|
+
export interface ActorInfo {
|
|
48
|
+
/** GitHub login of whoever triggered the run. */
|
|
49
|
+
login?: string;
|
|
50
|
+
/** Author association of the event's comment, review, issue or PR author. */
|
|
51
|
+
association?: string;
|
|
52
|
+
}
|
|
53
|
+
/** Who triggered this run: an explicit override, else GitHub Actions' env and event payload. */
|
|
54
|
+
export declare function resolveActor(env: Record<string, string | undefined>, override?: ActorInfo): ActorInfo;
|
|
55
|
+
/** True when no trust list is declared, or the actor matches one. Unknown actor = untrusted. */
|
|
56
|
+
export declare function actorTrusted(policy: SafeOutputsPolicy | undefined, actor: ActorInfo): boolean;
|
|
57
|
+
/** `30m` → 1_800_000. Undefined for anything the schema would reject. */
|
|
58
|
+
export declare function parseDuration(s: string | undefined): number | undefined;
|
|
59
|
+
/** The skill that applies a declaration: its `via`, else the first node skill (then any loaded skill) that can. */
|
|
60
|
+
export declare function resolveOutputSkill(decl: SafeOutputDeclaration, nodeSkills: string[], skills: Map<string, Skill>): string | undefined;
|
|
61
|
+
/** Declarations whose applying skill is not configured (no credentials loaded). */
|
|
62
|
+
export declare function unresolvedOutputs(declarations: SafeOutputDeclaration[], nodeSkills: string[], skills: Map<string, Skill>): SafeOutputType[];
|
|
63
|
+
/** Run-wide counters shared by every node's write stage in one execute() call. */
|
|
64
|
+
export interface WriteStageState {
|
|
65
|
+
/** `${nodeId}:${type}` → writes accepted (applied or staged). */
|
|
66
|
+
counts: Map<string, number>;
|
|
67
|
+
/** Writes accepted across the run. */
|
|
68
|
+
total: number;
|
|
69
|
+
/** Dedupe keys already accepted this run. */
|
|
70
|
+
seen: Set<string>;
|
|
71
|
+
}
|
|
72
|
+
export declare function createWriteStageState(): WriteStageState;
|
|
73
|
+
export interface ApplySafeOutputsOptions {
|
|
74
|
+
nodeId: string;
|
|
75
|
+
declarations: SafeOutputDeclaration[];
|
|
76
|
+
intents: SafeOutputIntent[];
|
|
77
|
+
policy?: SafeOutputsPolicy;
|
|
78
|
+
nodeSkills: string[];
|
|
79
|
+
skills: Map<string, Skill>;
|
|
80
|
+
/** Resolved skill config (credentials), handed to the skill's tool handler. */
|
|
81
|
+
config: Record<string, string>;
|
|
82
|
+
env: Record<string, string | undefined>;
|
|
83
|
+
actor: ActorInfo;
|
|
84
|
+
/** Preview only: print what would be written, write nothing. */
|
|
85
|
+
staged: boolean;
|
|
86
|
+
state: WriteStageState;
|
|
87
|
+
logger: Logger;
|
|
88
|
+
/**
|
|
89
|
+
* Model screen (only called when `policy.screen` is on and at least one write
|
|
90
|
+
* passed every deterministic check). Anything but `ALLOW` is a veto.
|
|
91
|
+
*/
|
|
92
|
+
screen?: (writes: Record<string, unknown>[]) => Promise<string | null>;
|
|
93
|
+
now?: () => number;
|
|
94
|
+
}
|
|
95
|
+
export interface ApplySafeOutputsResult {
|
|
96
|
+
receipts: SafeOutputReceipt[];
|
|
97
|
+
/** Set when an apply call failed; the node fails with it. */
|
|
98
|
+
error?: string;
|
|
99
|
+
}
|
|
100
|
+
/** Check, then apply (or stage) one node's intents. See the module comment for the order of checks. */
|
|
101
|
+
export declare function applySafeOutputs(o: ApplySafeOutputsOptions): Promise<ApplySafeOutputsResult>;
|
|
102
|
+
/** Instruction for the optional screen. The writes arrive as fenced, untrusted context. */
|
|
103
|
+
export declare const SAFE_OUTPUT_SCREEN_INSTRUCTION: string;
|