@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.
@@ -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
- preflight() {
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.preflight();
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.preflight();
939
+ const pre = await this.checkCli();
926
940
  if (!pre.ok)
927
941
  return failClosed(pre.reason);
928
942
  const args = [
@@ -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
  }
@@ -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";
@@ -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
  }
@@ -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, labels,
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
- /** The skill that applies a declaration: its `via`, else the first node skill (then any loaded skill) that can. */
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.
@@ -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, labels,
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
- /** The skill that applies a declaration: its `via`, else the first node skill (then any loaded skill) that can. */
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
- if (!output || typeof output !== "object")
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
- if ((type === "comment" || type === "label") && !intent.number) {
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" && intent.number !== undefined && !/^[1-9][0-9]*$/.test(intent.number)) {
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 = intent.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
- receipts[index] = { ...receipts[index], ...(ref !== undefined ? { ref } : {}) };
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}.