@sweny-ai/core 0.1.149 → 0.1.150
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/builtin-workflows.d.ts +45 -0
- package/dist/cli/builtin-workflows.js +129 -0
- package/dist/cli/check.d.ts +2 -8
- package/dist/cli/check.js +2 -34
- package/dist/cli/config.js +2 -0
- package/dist/cli/main.js +46 -11
- package/dist/cli/packs.d.ts +9 -5
- package/dist/cli/packs.js +100 -50
- package/dist/executor.js +15 -3
- package/dist/harness/auth.d.ts +38 -0
- package/dist/harness/auth.js +93 -0
- package/dist/harness/claude-code.d.ts +8 -0
- package/dist/harness/claude-code.js +8 -1
- package/dist/harness/codex.d.ts +10 -1
- package/dist/harness/codex.js +17 -3
- package/dist/harness/policy.js +6 -0
- package/dist/harness/types.d.ts +6 -0
- package/dist/index.d.ts +1 -1
- package/dist/node-policy.js +1 -0
- package/dist/safe-outputs.d.ts +17 -5
- package/dist/safe-outputs.js +85 -11
- package/dist/schema.d.ts +80 -0
- package/dist/schema.js +23 -0
- package/dist/types.d.ts +13 -0
- package/dist/workflows/browser.js +2 -2
- package/dist/workflows/implement.yml +61 -19
- package/dist/workflows/triage.yml +171 -139
- package/package.json +1 -1
package/dist/harness/codex.js
CHANGED
|
@@ -37,6 +37,7 @@ import { parseToolResultContent, summarizeToolError, tryParseJSON } from "./pars
|
|
|
37
37
|
import { startToolBridge } from "./tool-bridge/server.js";
|
|
38
38
|
import { defaultSandboxWrapper, prepareAgentSpawn, wrappersFrom, } from "./sandbox-wrapper.js";
|
|
39
39
|
import { TOKEN_ENV } from "./tool-bridge/protocol.js";
|
|
40
|
+
import { codexAuth } from "./auth.js";
|
|
40
41
|
export { CODEX_CAPABILITIES };
|
|
41
42
|
/** Oldest Codex CLI whose flags this adapter uses (`--ignore-user-config`, `--ignore-rules`). */
|
|
42
43
|
export const MIN_CODEX_VERSION = "0.159.0";
|
|
@@ -288,6 +289,7 @@ export class CodexHarness {
|
|
|
288
289
|
killGraceMs;
|
|
289
290
|
preflightResult;
|
|
290
291
|
version = "unknown";
|
|
292
|
+
authProbe;
|
|
291
293
|
constructor(opts = {}) {
|
|
292
294
|
this.model = opts.model;
|
|
293
295
|
this.maxTurns = opts.maxTurns ?? 20;
|
|
@@ -305,12 +307,24 @@ export class CodexHarness {
|
|
|
305
307
|
this.codexCommand = opts.codexCommand ?? { command: process.env.SWENY_CODEX_PATH || "codex", args: [] };
|
|
306
308
|
this.toolBridgeShim = opts.toolBridgeShim;
|
|
307
309
|
this.killGraceMs = opts.killGraceMs ?? 2000;
|
|
310
|
+
this.authProbe = opts.authProbe ?? (() => codexAuth(process.env));
|
|
308
311
|
}
|
|
309
312
|
info() {
|
|
310
313
|
return { id: this.id, version: this.version };
|
|
311
314
|
}
|
|
315
|
+
/**
|
|
316
|
+
* Before any node runs (#339): the Codex CLI is installed and new enough,
|
|
317
|
+
* and Codex can authenticate (an API key, an access token, or `codex login`).
|
|
318
|
+
*/
|
|
319
|
+
async preflight() {
|
|
320
|
+
const cli = await this.checkCli();
|
|
321
|
+
if (!cli.ok)
|
|
322
|
+
return cli;
|
|
323
|
+
const auth = this.authProbe();
|
|
324
|
+
return auth.ok ? cli : { ok: false, reason: auth.reason };
|
|
325
|
+
}
|
|
312
326
|
/** `codex --version`, checked against {@link MIN_CODEX_VERSION}. Cached per instance. */
|
|
313
|
-
|
|
327
|
+
checkCli() {
|
|
314
328
|
this.preflightResult ??= new Promise((resolve) => {
|
|
315
329
|
const { command, args } = this.codexCommand;
|
|
316
330
|
execFile(command, [...args, "--version"], { timeout: 30_000 }, (err, stdout) => {
|
|
@@ -723,7 +737,7 @@ export class CodexHarness {
|
|
|
723
737
|
};
|
|
724
738
|
if (gate.refuse)
|
|
725
739
|
return refused(gate.refuse);
|
|
726
|
-
const pre = await this.
|
|
740
|
+
const pre = await this.checkCli();
|
|
727
741
|
if (!pre.ok) {
|
|
728
742
|
this.logger.error(pre.reason);
|
|
729
743
|
return tag({ status: "failed", data: { error: pre.reason }, toolCalls: [] });
|
|
@@ -922,7 +936,7 @@ export class CodexHarness {
|
|
|
922
936
|
: `codex.ask: ${why}. Returning empty string.`);
|
|
923
937
|
return null;
|
|
924
938
|
};
|
|
925
|
-
const pre = await this.
|
|
939
|
+
const pre = await this.checkCli();
|
|
926
940
|
if (!pre.ok)
|
|
927
941
|
return failClosed(pre.reason);
|
|
928
942
|
const args = [
|
package/dist/harness/policy.js
CHANGED
|
@@ -74,6 +74,12 @@ export function policyGate(caps, policy, wrappers = {}) {
|
|
|
74
74
|
unenforced.push(`disallowed_tools [${policy.nativeDeny.join(", ")}]: harness has no tools by these names`);
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
+
// Exclusive MCP (`permissions.strict`): only the servers sweny passes may
|
|
78
|
+
// load, never the user's own config. A harness that cannot exclude them
|
|
79
|
+
// cannot honor it, so strict refuses the node.
|
|
80
|
+
if (policy.exclusiveMcp && caps.mcp.exclusive === "none") {
|
|
81
|
+
unenforced.push("exclusive MCP: harness cannot keep the user's own MCP servers out of the run");
|
|
82
|
+
}
|
|
77
83
|
if (policy.egress.length > 0 && !caps.sandbox.network && !wrappers.egress) {
|
|
78
84
|
unenforced.push("egress allowlist: harness has no network sandbox and no egress wrapper is active");
|
|
79
85
|
}
|
package/dist/harness/types.d.ts
CHANGED
|
@@ -55,6 +55,12 @@ export interface NodePolicy {
|
|
|
55
55
|
egress: string[];
|
|
56
56
|
/** Refuse instead of degrade when an opinion cannot be enforced. */
|
|
57
57
|
strict: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Only the MCP servers sweny passes may load, never the user's own config
|
|
60
|
+
* (`permissions.strict`, #365). A harness whose `mcp.exclusive` is `none`
|
|
61
|
+
* cannot honor it: reported in `degraded`, refused under `strict`.
|
|
62
|
+
*/
|
|
63
|
+
exclusiveMcp?: boolean;
|
|
58
64
|
/**
|
|
59
65
|
* Turn budget for the run. Set by adapters whose `turnLimit` is not native,
|
|
60
66
|
* so the gate reports how the budget is kept (watchdog) or that it is not.
|
package/dist/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
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, NodeAccess, NodePermissions, NodePermissionsSpec, SafeOutputType, SafeOutputDeclaration, SafeOutputsPolicy, SafeOutputReceipt, AuthorAssociation, } 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, SafeOutputPin, AuthorAssociation, } from "./types.js";
|
|
25
25
|
export { WORKFLOW_INPUT_TYPES } from "./types.js";
|
|
26
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";
|
package/dist/node-policy.js
CHANGED
|
@@ -38,5 +38,6 @@ export function buildNodePolicy(opts) {
|
|
|
38
38
|
...(opts.disallowedTools && opts.disallowedTools.length > 0 ? { nativeDeny: opts.disallowedTools } : {}),
|
|
39
39
|
egress: opts.egress ?? [],
|
|
40
40
|
strict: opts.permissions.strict,
|
|
41
|
+
...(opts.permissions.strict ? { exclusiveMcp: true } : {}),
|
|
41
42
|
};
|
|
42
43
|
}
|
package/dist/safe-outputs.d.ts
CHANGED
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
* the write, so this holds on every harness.
|
|
10
10
|
*
|
|
11
11
|
* Order of checks per intent (all deterministic): declared type, workflow
|
|
12
|
-
* ceiling, actor trust, expiry, skill, target pin, shape and size,
|
|
13
|
-
* title prefix, dedupe, node cap, run cap. An optional model screen runs last
|
|
12
|
+
* ceiling, actor trust, expiry, skill, target pin, issue pin, shape and size,
|
|
13
|
+
* labels, title prefix, dedupe, node cap, run cap. An optional model screen runs last
|
|
14
14
|
* over the intents that passed and can only veto them.
|
|
15
15
|
*/
|
|
16
|
-
import type { Logger, SafeOutputDeclaration, SafeOutputReceipt, SafeOutputsPolicy, SafeOutputType, Skill, Tool } from "./types.js";
|
|
16
|
+
import type { Logger, SafeOutputDeclaration, SafeOutputPin, SafeOutputReceipt, SafeOutputsPolicy, SafeOutputType, Skill, Tool } from "./types.js";
|
|
17
17
|
/** Name of the intent-recording tool a node with `outputs` receives. */
|
|
18
18
|
export declare const EMIT_OUTPUT_TOOL = "emit_output";
|
|
19
19
|
/** Longest title the write stage accepts (after the prefix). */
|
|
@@ -43,7 +43,7 @@ export interface SafeOutputIntent {
|
|
|
43
43
|
*/
|
|
44
44
|
export declare function createEmitOutputTool(declarations: SafeOutputDeclaration[], buffer: SafeOutputIntent[], now?: () => number): Tool;
|
|
45
45
|
/** Instruction block that tells the agent how writes work at this node. */
|
|
46
|
-
export declare function safeOutputsInstruction(declarations: SafeOutputDeclaration[]): string;
|
|
46
|
+
export declare function safeOutputsInstruction(declarations: SafeOutputDeclaration[], input?: Record<string, unknown>): string;
|
|
47
47
|
export interface ActorInfo {
|
|
48
48
|
/** GitHub login of whoever triggered the run. */
|
|
49
49
|
login?: string;
|
|
@@ -54,9 +54,19 @@ export interface ActorInfo {
|
|
|
54
54
|
export declare function resolveActor(env: Record<string, string | undefined>, override?: ActorInfo): ActorInfo;
|
|
55
55
|
/** True when no trust list is declared, or the actor matches one. Unknown actor = untrusted. */
|
|
56
56
|
export declare function actorTrusted(policy: SafeOutputsPolicy | undefined, actor: ActorInfo): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* The issue or PR a declaration pins, as a string. `{ input: name }` reads the
|
|
59
|
+
* run input; an absent or empty value is `undefined`, which refuses the write.
|
|
60
|
+
*/
|
|
61
|
+
export declare function resolvePin(pin: SafeOutputPin, input?: Record<string, unknown>): string | undefined;
|
|
57
62
|
/** `30m` → 1_800_000. Undefined for anything the schema would reject. */
|
|
58
63
|
export declare function parseDuration(s: string | undefined): number | undefined;
|
|
59
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* The skill that applies a declaration: its `via`, else the first node skill
|
|
66
|
+
* that can and is configured, else the first node skill that can, else any
|
|
67
|
+
* configured skill that can. A node listing `[linear, github]` therefore files
|
|
68
|
+
* on GitHub when only GitHub has credentials.
|
|
69
|
+
*/
|
|
60
70
|
export declare function resolveOutputSkill(decl: SafeOutputDeclaration, nodeSkills: string[], skills: Map<string, Skill>): string | undefined;
|
|
61
71
|
/** Declarations whose applying skill is not configured (no credentials loaded). */
|
|
62
72
|
export declare function unresolvedOutputs(declarations: SafeOutputDeclaration[], nodeSkills: string[], skills: Map<string, Skill>): SafeOutputType[];
|
|
@@ -85,6 +95,8 @@ export interface ApplySafeOutputsOptions {
|
|
|
85
95
|
staged: boolean;
|
|
86
96
|
state: WriteStageState;
|
|
87
97
|
logger: Logger;
|
|
98
|
+
/** The run input, for `number: { input }` pins. */
|
|
99
|
+
input?: Record<string, unknown>;
|
|
88
100
|
/**
|
|
89
101
|
* Model screen (only called when `policy.screen` is on and at least one write
|
|
90
102
|
* passed every deterministic check). Anything but `ALLOW` is a veto.
|
package/dist/safe-outputs.js
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* the write, so this holds on every harness.
|
|
10
10
|
*
|
|
11
11
|
* Order of checks per intent (all deterministic): declared type, workflow
|
|
12
|
-
* ceiling, actor trust, expiry, skill, target pin, shape and size,
|
|
13
|
-
* title prefix, dedupe, node cap, run cap. An optional model screen runs last
|
|
12
|
+
* ceiling, actor trust, expiry, skill, target pin, issue pin, shape and size,
|
|
13
|
+
* labels, title prefix, dedupe, node cap, run cap. An optional model screen runs last
|
|
14
14
|
* over the intents that passed and can only veto them.
|
|
15
15
|
*/
|
|
16
16
|
import { createHash } from "node:crypto";
|
|
@@ -108,11 +108,15 @@ export function createEmitOutputTool(declarations, buffer, now = Date.now) {
|
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
110
|
/** Instruction block that tells the agent how writes work at this node. */
|
|
111
|
-
export function safeOutputsInstruction(declarations) {
|
|
111
|
+
export function safeOutputsInstruction(declarations, input) {
|
|
112
112
|
const lines = declarations.map((d) => {
|
|
113
113
|
const parts = [`at most ${d.max ?? 1}`];
|
|
114
114
|
if (d.target)
|
|
115
115
|
parts.push(`target ${d.target}`);
|
|
116
|
+
if (d.number !== undefined) {
|
|
117
|
+
const pin = resolvePin(d.number, input);
|
|
118
|
+
parts.push(pin ? `only issue or PR ${pin} (the number may be omitted)` : "pinned issue not set for this run");
|
|
119
|
+
}
|
|
116
120
|
if (d.title_prefix)
|
|
117
121
|
parts.push(`title prefix "${d.title_prefix}"`);
|
|
118
122
|
if (d.type === "label" && d.labels?.length)
|
|
@@ -167,18 +171,42 @@ export function actorTrusted(policy, actor) {
|
|
|
167
171
|
return false;
|
|
168
172
|
}
|
|
169
173
|
// ─── Helpers ──────────────────────────────────────────────────────
|
|
174
|
+
/**
|
|
175
|
+
* The issue or PR a declaration pins, as a string. `{ input: name }` reads the
|
|
176
|
+
* run input; an absent or empty value is `undefined`, which refuses the write.
|
|
177
|
+
*/
|
|
178
|
+
export function resolvePin(pin, input) {
|
|
179
|
+
const raw = typeof pin === "object" ? input?.[pin.input] : pin;
|
|
180
|
+
if (typeof raw === "number" && Number.isInteger(raw) && raw > 0)
|
|
181
|
+
return String(raw);
|
|
182
|
+
if (typeof raw !== "string")
|
|
183
|
+
return undefined;
|
|
184
|
+
const v = raw.trim().replace(/^#/, "");
|
|
185
|
+
return v.length > 0 ? v : undefined;
|
|
186
|
+
}
|
|
187
|
+
/** Same issue or PR: `#42` and `42`, `OFF-12` and `off-12` match. */
|
|
188
|
+
function sameIssue(a, b) {
|
|
189
|
+
const norm = (x) => x.trim().replace(/^#/, "").toLowerCase();
|
|
190
|
+
return norm(a) === norm(b);
|
|
191
|
+
}
|
|
170
192
|
const UNIT_MS = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 };
|
|
171
193
|
/** `30m` → 1_800_000. Undefined for anything the schema would reject. */
|
|
172
194
|
export function parseDuration(s) {
|
|
173
195
|
const m = s ? /^([1-9][0-9]*)(s|m|h|d)$/.exec(s) : null;
|
|
174
196
|
return m ? Number(m[1]) * UNIT_MS[m[2]] : undefined;
|
|
175
197
|
}
|
|
176
|
-
/**
|
|
198
|
+
/**
|
|
199
|
+
* The skill that applies a declaration: its `via`, else the first node skill
|
|
200
|
+
* that can and is configured, else the first node skill that can, else any
|
|
201
|
+
* configured skill that can. A node listing `[linear, github]` therefore files
|
|
202
|
+
* on GitHub when only GitHub has credentials.
|
|
203
|
+
*/
|
|
177
204
|
export function resolveOutputSkill(decl, nodeSkills, skills) {
|
|
178
205
|
if (decl.via)
|
|
179
206
|
return decl.via;
|
|
180
207
|
const supports = (id) => SAFE_OUTPUT_APPLIERS[id]?.includes(decl.type) === true;
|
|
181
|
-
return (nodeSkills.find((id) => supports(id)) ??
|
|
208
|
+
return (nodeSkills.find((id) => supports(id) && skills.has(id)) ??
|
|
209
|
+
nodeSkills.find((id) => supports(id)) ??
|
|
182
210
|
Object.keys(SAFE_OUTPUT_APPLIERS).find((id) => supports(id) && skills.has(id)));
|
|
183
211
|
}
|
|
184
212
|
/** Declarations whose applying skill is not configured (no credentials loaded). */
|
|
@@ -254,10 +282,29 @@ function dedupeKey(w, explicit) {
|
|
|
254
282
|
];
|
|
255
283
|
return createHash("sha256").update(JSON.stringify(parts)).digest("hex");
|
|
256
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* The object an apply call produced: the output itself (GitHub REST), or the
|
|
287
|
+
* record a GraphQL mutation wraps (`{ issueCreate: { issue: {...} } }`, Linear).
|
|
288
|
+
*/
|
|
289
|
+
function producedRecord(output) {
|
|
290
|
+
let o = output;
|
|
291
|
+
for (let depth = 0; depth < 3; depth++) {
|
|
292
|
+
if (!o || typeof o !== "object" || Array.isArray(o))
|
|
293
|
+
return undefined;
|
|
294
|
+
const rec = o;
|
|
295
|
+
if (["number", "identifier", "id"].some((k) => k in rec))
|
|
296
|
+
return rec;
|
|
297
|
+
const nested = Object.values(rec).filter((v) => v && typeof v === "object" && !Array.isArray(v));
|
|
298
|
+
if (nested.length !== 1)
|
|
299
|
+
return undefined;
|
|
300
|
+
o = nested[0];
|
|
301
|
+
}
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
257
304
|
function refOf(output) {
|
|
258
|
-
|
|
305
|
+
const o = producedRecord(output);
|
|
306
|
+
if (!o)
|
|
259
307
|
return undefined;
|
|
260
|
-
const o = output;
|
|
261
308
|
for (const k of ["number", "identifier", "id"]) {
|
|
262
309
|
const v = o[k];
|
|
263
310
|
if (typeof v === "number" || (typeof v === "string" && v.length > 0 && v.length <= 64))
|
|
@@ -265,6 +312,18 @@ function refOf(output) {
|
|
|
265
312
|
}
|
|
266
313
|
return undefined;
|
|
267
314
|
}
|
|
315
|
+
/** The web URL of what a write produced (`html_url` on GitHub, `url` on Linear). */
|
|
316
|
+
function urlOf(output) {
|
|
317
|
+
const o = producedRecord(output);
|
|
318
|
+
if (!o)
|
|
319
|
+
return undefined;
|
|
320
|
+
for (const k of ["html_url", "url"]) {
|
|
321
|
+
const v = o[k];
|
|
322
|
+
if (typeof v === "string" && /^https:\/\//.test(v) && v.length <= 512 && !/\/\/api\./.test(v))
|
|
323
|
+
return v;
|
|
324
|
+
}
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
268
327
|
export function createWriteStageState() {
|
|
269
328
|
return { counts: new Map(), total: 0, seen: new Set() };
|
|
270
329
|
}
|
|
@@ -318,11 +377,25 @@ export async function applySafeOutputs(o) {
|
|
|
318
377
|
refuse(type === "comment" && !body.trim() ? "missing body" : "body too long", at);
|
|
319
378
|
continue;
|
|
320
379
|
}
|
|
321
|
-
|
|
380
|
+
// Issue pin: a pinned comment or label may only land on that issue or PR.
|
|
381
|
+
let issueRef = intent.number?.trim().replace(/^#/, "") || undefined;
|
|
382
|
+
if ((type === "comment" || type === "label") && decl.number !== undefined) {
|
|
383
|
+
const pin = resolvePin(decl.number, o.input);
|
|
384
|
+
if (!pin) {
|
|
385
|
+
refuse("pinned issue is not set for this run", at);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (issueRef !== undefined && !sameIssue(issueRef, pin)) {
|
|
389
|
+
refuse("issue outside the declared number", at);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
issueRef = pin;
|
|
393
|
+
}
|
|
394
|
+
if ((type === "comment" || type === "label") && !issueRef) {
|
|
322
395
|
refuse("missing number", at);
|
|
323
396
|
continue;
|
|
324
397
|
}
|
|
325
|
-
if (via === "github" &&
|
|
398
|
+
if (via === "github" && issueRef !== undefined && !/^[1-9][0-9]*$/.test(issueRef)) {
|
|
326
399
|
refuse("number must be an issue or PR number", at);
|
|
327
400
|
continue;
|
|
328
401
|
}
|
|
@@ -362,7 +435,7 @@ export async function applySafeOutputs(o) {
|
|
|
362
435
|
// A Linear comment's issue ID does not encode or enforce its declared team.
|
|
363
436
|
// Resolve it through the configured read tool and carry its immutable ID to
|
|
364
437
|
// the write. Missing lookup support or unverifiable membership fails closed.
|
|
365
|
-
let number =
|
|
438
|
+
let number = issueRef;
|
|
366
439
|
if (via === "linear" && type === "comment" && target) {
|
|
367
440
|
const lookup = o.skills.get(via)?.tools.find((t) => t.name === "linear_get_issue" && t.access === "read");
|
|
368
441
|
let issue;
|
|
@@ -453,7 +526,8 @@ export async function applySafeOutputs(o) {
|
|
|
453
526
|
try {
|
|
454
527
|
const output = await tool.handler(applier.build(write), { config: o.config, logger: o.logger });
|
|
455
528
|
const ref = refOf(output);
|
|
456
|
-
|
|
529
|
+
const url = urlOf(output);
|
|
530
|
+
receipts[index] = { ...receipts[index], ...(ref !== undefined ? { ref } : {}), ...(url ? { url } : {}) };
|
|
457
531
|
o.logger.info(` safe output: ${write.type} via ${write.via} applied${ref !== undefined ? ` (${ref})` : ""}`, {
|
|
458
532
|
node: o.nodeId,
|
|
459
533
|
});
|
package/dist/schema.d.ts
CHANGED
|
@@ -857,8 +857,18 @@ export declare const safeOutputDeclarationZ: z.ZodObject<{
|
|
|
857
857
|
title_prefix: z.ZodOptional<z.ZodString>;
|
|
858
858
|
labels: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
859
859
|
expires: z.ZodOptional<z.ZodString>;
|
|
860
|
+
number: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodObject<{
|
|
861
|
+
input: z.ZodString;
|
|
862
|
+
}, "strict", z.ZodTypeAny, {
|
|
863
|
+
input: string;
|
|
864
|
+
}, {
|
|
865
|
+
input: string;
|
|
866
|
+
}>]>>;
|
|
860
867
|
}, "strict", z.ZodTypeAny, {
|
|
861
868
|
type: "comment" | "issue" | "pr" | "label";
|
|
869
|
+
number?: string | number | {
|
|
870
|
+
input: string;
|
|
871
|
+
} | undefined;
|
|
862
872
|
labels?: string[] | undefined;
|
|
863
873
|
max?: number | undefined;
|
|
864
874
|
via?: string | undefined;
|
|
@@ -867,6 +877,9 @@ export declare const safeOutputDeclarationZ: z.ZodObject<{
|
|
|
867
877
|
expires?: string | undefined;
|
|
868
878
|
}, {
|
|
869
879
|
type: "comment" | "issue" | "pr" | "label";
|
|
880
|
+
number?: string | number | {
|
|
881
|
+
input: string;
|
|
882
|
+
} | undefined;
|
|
870
883
|
labels?: string[] | undefined;
|
|
871
884
|
max?: number | undefined;
|
|
872
885
|
via?: string | undefined;
|
|
@@ -972,8 +985,18 @@ export declare const nodeZ: z.ZodObject<{
|
|
|
972
985
|
title_prefix: z.ZodOptional<z.ZodString>;
|
|
973
986
|
labels: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
974
987
|
expires: z.ZodOptional<z.ZodString>;
|
|
988
|
+
number: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodObject<{
|
|
989
|
+
input: z.ZodString;
|
|
990
|
+
}, "strict", z.ZodTypeAny, {
|
|
991
|
+
input: string;
|
|
992
|
+
}, {
|
|
993
|
+
input: string;
|
|
994
|
+
}>]>>;
|
|
975
995
|
}, "strict", z.ZodTypeAny, {
|
|
976
996
|
type: "comment" | "issue" | "pr" | "label";
|
|
997
|
+
number?: string | number | {
|
|
998
|
+
input: string;
|
|
999
|
+
} | undefined;
|
|
977
1000
|
labels?: string[] | undefined;
|
|
978
1001
|
max?: number | undefined;
|
|
979
1002
|
via?: string | undefined;
|
|
@@ -982,6 +1005,9 @@ export declare const nodeZ: z.ZodObject<{
|
|
|
982
1005
|
expires?: string | undefined;
|
|
983
1006
|
}, {
|
|
984
1007
|
type: "comment" | "issue" | "pr" | "label";
|
|
1008
|
+
number?: string | number | {
|
|
1009
|
+
input: string;
|
|
1010
|
+
} | undefined;
|
|
985
1011
|
labels?: string[] | undefined;
|
|
986
1012
|
max?: number | undefined;
|
|
987
1013
|
via?: string | undefined;
|
|
@@ -1403,6 +1429,9 @@ export declare const nodeZ: z.ZodObject<{
|
|
|
1403
1429
|
} | undefined;
|
|
1404
1430
|
outputs?: {
|
|
1405
1431
|
type: "comment" | "issue" | "pr" | "label";
|
|
1432
|
+
number?: string | number | {
|
|
1433
|
+
input: string;
|
|
1434
|
+
} | undefined;
|
|
1406
1435
|
labels?: string[] | undefined;
|
|
1407
1436
|
max?: number | undefined;
|
|
1408
1437
|
via?: string | undefined;
|
|
@@ -1513,6 +1542,9 @@ export declare const nodeZ: z.ZodObject<{
|
|
|
1513
1542
|
} | undefined;
|
|
1514
1543
|
outputs?: {
|
|
1515
1544
|
type: "comment" | "issue" | "pr" | "label";
|
|
1545
|
+
number?: string | number | {
|
|
1546
|
+
input: string;
|
|
1547
|
+
} | undefined;
|
|
1516
1548
|
labels?: string[] | undefined;
|
|
1517
1549
|
max?: number | undefined;
|
|
1518
1550
|
via?: string | undefined;
|
|
@@ -1706,8 +1738,18 @@ export declare const workflowZ: z.ZodObject<{
|
|
|
1706
1738
|
title_prefix: z.ZodOptional<z.ZodString>;
|
|
1707
1739
|
labels: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
1708
1740
|
expires: z.ZodOptional<z.ZodString>;
|
|
1741
|
+
number: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodObject<{
|
|
1742
|
+
input: z.ZodString;
|
|
1743
|
+
}, "strict", z.ZodTypeAny, {
|
|
1744
|
+
input: string;
|
|
1745
|
+
}, {
|
|
1746
|
+
input: string;
|
|
1747
|
+
}>]>>;
|
|
1709
1748
|
}, "strict", z.ZodTypeAny, {
|
|
1710
1749
|
type: "comment" | "issue" | "pr" | "label";
|
|
1750
|
+
number?: string | number | {
|
|
1751
|
+
input: string;
|
|
1752
|
+
} | undefined;
|
|
1711
1753
|
labels?: string[] | undefined;
|
|
1712
1754
|
max?: number | undefined;
|
|
1713
1755
|
via?: string | undefined;
|
|
@@ -1716,6 +1758,9 @@ export declare const workflowZ: z.ZodObject<{
|
|
|
1716
1758
|
expires?: string | undefined;
|
|
1717
1759
|
}, {
|
|
1718
1760
|
type: "comment" | "issue" | "pr" | "label";
|
|
1761
|
+
number?: string | number | {
|
|
1762
|
+
input: string;
|
|
1763
|
+
} | undefined;
|
|
1719
1764
|
labels?: string[] | undefined;
|
|
1720
1765
|
max?: number | undefined;
|
|
1721
1766
|
via?: string | undefined;
|
|
@@ -2137,6 +2182,9 @@ export declare const workflowZ: z.ZodObject<{
|
|
|
2137
2182
|
} | undefined;
|
|
2138
2183
|
outputs?: {
|
|
2139
2184
|
type: "comment" | "issue" | "pr" | "label";
|
|
2185
|
+
number?: string | number | {
|
|
2186
|
+
input: string;
|
|
2187
|
+
} | undefined;
|
|
2140
2188
|
labels?: string[] | undefined;
|
|
2141
2189
|
max?: number | undefined;
|
|
2142
2190
|
via?: string | undefined;
|
|
@@ -2247,6 +2295,9 @@ export declare const workflowZ: z.ZodObject<{
|
|
|
2247
2295
|
} | undefined;
|
|
2248
2296
|
outputs?: {
|
|
2249
2297
|
type: "comment" | "issue" | "pr" | "label";
|
|
2298
|
+
number?: string | number | {
|
|
2299
|
+
input: string;
|
|
2300
|
+
} | undefined;
|
|
2250
2301
|
labels?: string[] | undefined;
|
|
2251
2302
|
max?: number | undefined;
|
|
2252
2303
|
via?: string | undefined;
|
|
@@ -2602,6 +2653,9 @@ export declare const workflowZ: z.ZodObject<{
|
|
|
2602
2653
|
} | undefined;
|
|
2603
2654
|
outputs?: {
|
|
2604
2655
|
type: "comment" | "issue" | "pr" | "label";
|
|
2656
|
+
number?: string | number | {
|
|
2657
|
+
input: string;
|
|
2658
|
+
} | undefined;
|
|
2605
2659
|
labels?: string[] | undefined;
|
|
2606
2660
|
max?: number | undefined;
|
|
2607
2661
|
via?: string | undefined;
|
|
@@ -2763,6 +2817,9 @@ export declare const workflowZ: z.ZodObject<{
|
|
|
2763
2817
|
} | undefined;
|
|
2764
2818
|
outputs?: {
|
|
2765
2819
|
type: "comment" | "issue" | "pr" | "label";
|
|
2820
|
+
number?: string | number | {
|
|
2821
|
+
input: string;
|
|
2822
|
+
} | undefined;
|
|
2766
2823
|
labels?: string[] | undefined;
|
|
2767
2824
|
max?: number | undefined;
|
|
2768
2825
|
via?: string | undefined;
|
|
@@ -2966,6 +3023,9 @@ export declare function parseWorkflow(raw: unknown): {
|
|
|
2966
3023
|
} | undefined;
|
|
2967
3024
|
outputs?: {
|
|
2968
3025
|
type: "comment" | "issue" | "pr" | "label";
|
|
3026
|
+
number?: string | number | {
|
|
3027
|
+
input: string;
|
|
3028
|
+
} | undefined;
|
|
2969
3029
|
labels?: string[] | undefined;
|
|
2970
3030
|
max?: number | undefined;
|
|
2971
3031
|
via?: string | undefined;
|
|
@@ -3403,6 +3463,26 @@ export declare const workflowJsonSchema: {
|
|
|
3403
3463
|
readonly pattern: string;
|
|
3404
3464
|
readonly description: "Drop an intent older than this when the write stage runs (e.g. 30m, 2h, 7d).";
|
|
3405
3465
|
};
|
|
3466
|
+
readonly number: {
|
|
3467
|
+
readonly description: "comment / label: the only issue or PR this output may write to (GitHub number, Linear identifier), or { input: <name> } to pin it to a run input.";
|
|
3468
|
+
readonly oneOf: readonly [{
|
|
3469
|
+
readonly type: "string";
|
|
3470
|
+
readonly minLength: 1;
|
|
3471
|
+
}, {
|
|
3472
|
+
readonly type: "integer";
|
|
3473
|
+
readonly minimum: 1;
|
|
3474
|
+
}, {
|
|
3475
|
+
readonly type: "object";
|
|
3476
|
+
readonly required: readonly ["input"];
|
|
3477
|
+
readonly additionalProperties: false;
|
|
3478
|
+
readonly properties: {
|
|
3479
|
+
readonly input: {
|
|
3480
|
+
readonly type: "string";
|
|
3481
|
+
readonly minLength: 1;
|
|
3482
|
+
};
|
|
3483
|
+
};
|
|
3484
|
+
}];
|
|
3485
|
+
};
|
|
3406
3486
|
};
|
|
3407
3487
|
};
|
|
3408
3488
|
};
|
package/dist/schema.js
CHANGED
|
@@ -272,6 +272,9 @@ export const safeOutputDeclarationZ = z
|
|
|
272
272
|
title_prefix: z.string().min(1).max(64).optional(),
|
|
273
273
|
labels: z.array(z.string().min(1)).min(1).optional(),
|
|
274
274
|
expires: z.string().regex(SAFE_OUTPUT_EXPIRES_PATTERN).optional(),
|
|
275
|
+
number: z
|
|
276
|
+
.union([z.string().min(1), z.number().int().min(1), z.object({ input: z.string().min(1) }).strict()])
|
|
277
|
+
.optional(),
|
|
275
278
|
})
|
|
276
279
|
.strict();
|
|
277
280
|
/** Workflow-level safe-output policy (#365): the ceiling and run-wide limits. */
|
|
@@ -633,6 +636,13 @@ export function validateWorkflow(workflow, knownSkills) {
|
|
|
633
636
|
nodeId,
|
|
634
637
|
});
|
|
635
638
|
}
|
|
639
|
+
if (out.number !== undefined && out.type !== "comment" && out.type !== "label") {
|
|
640
|
+
errors.push({
|
|
641
|
+
code: "UNSUPPORTED_OUTPUT",
|
|
642
|
+
message: `Node "${nodeId}" output "${out.type}" pins number, which only applies to comment and label outputs`,
|
|
643
|
+
nodeId,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
636
646
|
}
|
|
637
647
|
}
|
|
638
648
|
// Skill references
|
|
@@ -855,6 +865,19 @@ export const workflowJsonSchema = {
|
|
|
855
865
|
pattern: SAFE_OUTPUT_EXPIRES_PATTERN.source,
|
|
856
866
|
description: "Drop an intent older than this when the write stage runs (e.g. 30m, 2h, 7d).",
|
|
857
867
|
},
|
|
868
|
+
number: {
|
|
869
|
+
description: "comment / label: the only issue or PR this output may write to (GitHub number, Linear identifier), or { input: <name> } to pin it to a run input.",
|
|
870
|
+
oneOf: [
|
|
871
|
+
{ type: "string", minLength: 1 },
|
|
872
|
+
{ type: "integer", minimum: 1 },
|
|
873
|
+
{
|
|
874
|
+
type: "object",
|
|
875
|
+
required: ["input"],
|
|
876
|
+
additionalProperties: false,
|
|
877
|
+
properties: { input: { type: "string", minLength: 1 } },
|
|
878
|
+
},
|
|
879
|
+
],
|
|
880
|
+
},
|
|
858
881
|
},
|
|
859
882
|
},
|
|
860
883
|
},
|
package/dist/types.d.ts
CHANGED
|
@@ -422,7 +422,18 @@ export interface SafeOutputDeclaration {
|
|
|
422
422
|
labels?: string[];
|
|
423
423
|
/** Drop an intent older than this when the write stage runs (e.g. `30m`, `2h`, `7d`). */
|
|
424
424
|
expires?: string;
|
|
425
|
+
/**
|
|
426
|
+
* comment / label: the only issue or PR this output may write to (GitHub
|
|
427
|
+
* issue or PR number, Linear issue identifier), or `{ input: <name> }` to pin
|
|
428
|
+
* it to a run input. An intent naming another is refused; an intent naming
|
|
429
|
+
* none uses the pin. An empty pinned input refuses the write.
|
|
430
|
+
*/
|
|
431
|
+
number?: SafeOutputPin;
|
|
425
432
|
}
|
|
433
|
+
/** A pinned issue / PR: a literal number or identifier, or the name of a run input that holds one. */
|
|
434
|
+
export type SafeOutputPin = string | number | {
|
|
435
|
+
input: string;
|
|
436
|
+
};
|
|
426
437
|
/** Workflow-level safe-output policy: the ceiling and run-wide limits. */
|
|
427
438
|
export interface SafeOutputsPolicy {
|
|
428
439
|
/** Output types any node may declare. Absent: all types. */
|
|
@@ -449,6 +460,8 @@ export interface SafeOutputReceipt {
|
|
|
449
460
|
target?: string;
|
|
450
461
|
/** Issue / PR / comment number or id the write produced. */
|
|
451
462
|
ref?: string | number;
|
|
463
|
+
/** Web URL of what the write produced, as the API returned it. */
|
|
464
|
+
url?: string;
|
|
452
465
|
}
|
|
453
466
|
/**
|
|
454
467
|
* Per-node skill-tool filter. See {@link Node.tools}.
|