@sweny-ai/core 0.1.142 → 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.
Files changed (45) hide show
  1. package/dist/agent-env.d.ts +24 -0
  2. package/dist/agent-env.js +40 -7
  3. package/dist/cli/config-file.d.ts +1 -1
  4. package/dist/cli/config-file.js +1 -1
  5. package/dist/cli/config.d.ts +2 -0
  6. package/dist/cli/config.js +15 -7
  7. package/dist/cli/main.js +30 -4
  8. package/dist/cli/run-history.d.ts +2 -0
  9. package/dist/cli/run-history.js +2 -0
  10. package/dist/cli/run-output.d.ts +13 -2
  11. package/dist/cli/run-output.js +27 -2
  12. package/dist/executor.d.ts +12 -0
  13. package/dist/executor.js +127 -7
  14. package/dist/harness/abort.d.ts +17 -0
  15. package/dist/harness/abort.js +53 -0
  16. package/dist/harness/agents.d.ts +11 -0
  17. package/dist/harness/agents.js +15 -0
  18. package/dist/harness/capabilities.d.ts +23 -0
  19. package/dist/harness/capabilities.js +35 -0
  20. package/dist/harness/claude-code.d.ts +20 -43
  21. package/dist/harness/claude-code.js +58 -312
  22. package/dist/harness/codex.d.ts +175 -0
  23. package/dist/harness/codex.js +921 -0
  24. package/dist/harness/index.d.ts +11 -3
  25. package/dist/harness/index.js +8 -8
  26. package/dist/harness/parse.d.ts +49 -0
  27. package/dist/harness/parse.js +253 -0
  28. package/dist/harness/policy.d.ts +23 -1
  29. package/dist/harness/policy.js +73 -15
  30. package/dist/harness/prompts.d.ts +7 -1
  31. package/dist/harness/prompts.js +16 -0
  32. package/dist/harness/types.d.ts +13 -2
  33. package/dist/harness/types.js +2 -2
  34. package/dist/index.d.ts +8 -4
  35. package/dist/index.js +6 -3
  36. package/dist/node-policy.d.ts +31 -0
  37. package/dist/node-policy.js +42 -0
  38. package/dist/safe-outputs.d.ts +103 -0
  39. package/dist/safe-outputs.js +460 -0
  40. package/dist/schema.d.ts +501 -12
  41. package/dist/schema.js +210 -4
  42. package/dist/skills/github.js +18 -0
  43. package/dist/types.d.ts +107 -1
  44. package/dist/types.js +29 -0
  45. package/package.json +1 -1
package/dist/schema.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * parsing untrusted input (JSON/YAML import, Studio editor).
8
8
  */
9
9
  import { z } from "zod";
10
- import { EVALUATOR_KINDS, EVAL_POLICIES, MCP_TRANSPORTS, NODE_ON_FAIL, REQUIRES_ON_FAIL, SKILL_CATEGORIES, SKILL_ID_MAX_LENGTH, SKILL_ID_PATTERN, TOOL_ACCESS, WORKFLOW_TYPES, } from "./types.js";
10
+ import { AUTHOR_ASSOCIATIONS, EVALUATOR_KINDS, EVAL_POLICIES, MCP_TRANSPORTS, NODE_ACCESS, NODE_ON_FAIL, REQUIRES_ON_FAIL, SAFE_OUTPUT_APPLIERS, SAFE_OUTPUT_EXPIRES_PATTERN, SAFE_OUTPUT_MAX_CEILING, SAFE_OUTPUT_TYPES, SKILL_CATEGORIES, SKILL_ID_MAX_LENGTH, SKILL_ID_PATTERN, TOOL_ACCESS, TOOL_CLASSES, WORKFLOW_TYPES, } from "./types.js";
11
11
  import { sourceZ } from "./sources.js";
12
12
  import { workflowInputsZ, WORKFLOW_INPUT_TYPES } from "./inputs.js";
13
13
  export { sourceZ };
@@ -244,6 +244,47 @@ export const nodeRetryZ = z
244
244
  instruction: z.union([z.string().min(1), retryInstructionAutoZ, retryInstructionReflectZ]).optional(),
245
245
  })
246
246
  .strict();
247
+ /**
248
+ * Node / workflow permissions (#365): `read` | `write`, or an object with
249
+ * `access`, `deny` (portable tool classes) and `strict`. The object form must
250
+ * declare at least one key.
251
+ */
252
+ export const nodePermissionsZ = z.union([
253
+ z.enum(NODE_ACCESS),
254
+ z
255
+ .object({
256
+ access: z.enum(NODE_ACCESS).optional(),
257
+ deny: z.array(z.enum(TOOL_CLASSES)).min(1).optional(),
258
+ strict: z.boolean().optional(),
259
+ })
260
+ .strict()
261
+ .refine((p) => p.access !== undefined || p.deny !== undefined || p.strict !== undefined, {
262
+ message: "permissions must declare at least one of access, deny, strict",
263
+ }),
264
+ ]);
265
+ /** One typed write intent a node may emit (#365). */
266
+ export const safeOutputDeclarationZ = z
267
+ .object({
268
+ type: z.enum(SAFE_OUTPUT_TYPES),
269
+ via: z.string().min(1).optional(),
270
+ max: z.number().int().min(1).max(SAFE_OUTPUT_MAX_CEILING).optional(),
271
+ target: z.string().min(1).optional(),
272
+ title_prefix: z.string().min(1).max(64).optional(),
273
+ labels: z.array(z.string().min(1)).min(1).optional(),
274
+ expires: z.string().regex(SAFE_OUTPUT_EXPIRES_PATTERN).optional(),
275
+ })
276
+ .strict();
277
+ /** Workflow-level safe-output policy (#365): the ceiling and run-wide limits. */
278
+ export const safeOutputsPolicyZ = z
279
+ .object({
280
+ allow: z.array(z.enum(SAFE_OUTPUT_TYPES)).min(1).optional(),
281
+ max: z.number().int().min(1).max(SAFE_OUTPUT_MAX_CEILING).optional(),
282
+ staged: z.boolean().optional(),
283
+ trusted_actors: z.array(z.string().min(1)).min(1).optional(),
284
+ trusted_associations: z.array(z.enum(AUTHOR_ASSOCIATIONS)).min(1).optional(),
285
+ screen: z.boolean().optional(),
286
+ })
287
+ .strict();
247
288
  export const nodeZ = z
248
289
  .object({
249
290
  name: z.string().min(1),
@@ -255,6 +296,8 @@ export const nodeZ = z
255
296
  tools: nodeToolsZ.optional(),
256
297
  fail_soft: z.boolean().optional(),
257
298
  on_fail: z.enum(NODE_ON_FAIL).optional(),
299
+ permissions: nodePermissionsZ.optional(),
300
+ outputs: z.array(safeOutputDeclarationZ).min(1).optional(),
258
301
  rules: nodeSourcesZ.optional(),
259
302
  context: nodeSourcesZ.optional(),
260
303
  eval: z.array(evaluatorZ).min(1).optional(),
@@ -307,6 +350,8 @@ export const workflowZ = z.object({
307
350
  model: z.string().min(1).optional(),
308
351
  judge_budget: z.number().int().min(0).optional(),
309
352
  inputs: workflowInputsZ.optional(),
353
+ permissions: nodePermissionsZ.optional(),
354
+ safe_outputs: safeOutputsPolicyZ.optional(),
310
355
  });
311
356
  const LEGACY_VERIFY_MESSAGE = "uses the legacy 'verify:' field. It was renamed to 'eval:' in @sweny-ai/core v0.2.0 with a different shape (named evaluators with kind: value | function | judge). " +
312
357
  "Migration guide: https://spec.sweny.ai/nodes/#eval";
@@ -548,6 +593,48 @@ export function validateWorkflow(workflow, knownSkills) {
548
593
  });
549
594
  }
550
595
  }
596
+ // Permissions and safe outputs (#365). The workflow's `permissions` and
597
+ // `safe_outputs.allow` are a ceiling every node inherits.
598
+ const wfAccess = typeof workflow.permissions === "string" ? workflow.permissions : workflow.permissions?.access;
599
+ const allowedOutputs = workflow.safe_outputs?.allow;
600
+ for (const [nodeId, node] of Object.entries(workflow.nodes)) {
601
+ const nodeAccess = typeof node.permissions === "string" ? node.permissions : node.permissions?.access;
602
+ if (wfAccess === "read" && nodeAccess === "write") {
603
+ errors.push({
604
+ code: "PERMISSION_CEILING",
605
+ message: `Node "${nodeId}" asks for permissions write, above the workflow's permissions read`,
606
+ nodeId,
607
+ });
608
+ }
609
+ const seenTypes = new Set();
610
+ for (const out of node.outputs ?? []) {
611
+ if (seenTypes.has(out.type)) {
612
+ errors.push({
613
+ code: "DUPLICATE_OUTPUT",
614
+ message: `Node "${nodeId}" declares output "${out.type}" more than once; merge them into one entry`,
615
+ nodeId,
616
+ });
617
+ }
618
+ seenTypes.add(out.type);
619
+ if (allowedOutputs && !allowedOutputs.includes(out.type)) {
620
+ errors.push({
621
+ code: "OUTPUT_NOT_ALLOWED",
622
+ message: `Node "${nodeId}" declares output "${out.type}", outside the workflow's safe_outputs.allow [${allowedOutputs.join(", ")}]`,
623
+ nodeId,
624
+ });
625
+ }
626
+ if (out.via && !SAFE_OUTPUT_APPLIERS[out.via]?.includes(out.type)) {
627
+ const supported = Object.entries(SAFE_OUTPUT_APPLIERS)
628
+ .filter(([, types]) => types.includes(out.type))
629
+ .map(([id]) => id);
630
+ errors.push({
631
+ code: "UNSUPPORTED_OUTPUT",
632
+ message: `Node "${nodeId}" output "${out.type}" names via "${out.via}", which cannot apply it (supported: ${supported.join(", ")})`,
633
+ nodeId,
634
+ });
635
+ }
636
+ }
637
+ }
551
638
  // Skill references
552
639
  if (knownSkills) {
553
640
  // Merge workflow inline skills into known set
@@ -705,6 +792,71 @@ export const workflowJsonSchema = {
705
792
  },
706
793
  ],
707
794
  },
795
+ Permissions: {
796
+ description: "What a node's agent may do. 'read' runs it read-only: only access: read skill tools, no external skill MCP servers, no shell, file-write, edit, fetch or subagent built-ins. On the workflow it is the default and the ceiling for every node.",
797
+ oneOf: [
798
+ { type: "string", enum: [...NODE_ACCESS] },
799
+ {
800
+ type: "object",
801
+ additionalProperties: false,
802
+ minProperties: 1,
803
+ properties: {
804
+ access: { type: "string", enum: [...NODE_ACCESS] },
805
+ deny: {
806
+ type: "array",
807
+ items: { type: "string", enum: [...TOOL_CLASSES] },
808
+ minItems: 1,
809
+ description: "Built-in tool classes the agent must not have, compiled per harness.",
810
+ },
811
+ strict: {
812
+ type: "boolean",
813
+ description: "Exclusive MCP (only the servers sweny injects), and refuse the node on a harness that cannot enforce the policy.",
814
+ },
815
+ },
816
+ },
817
+ ],
818
+ },
819
+ SafeOutput: {
820
+ type: "object",
821
+ required: ["type"],
822
+ additionalProperties: false,
823
+ properties: {
824
+ type: { type: "string", enum: [...SAFE_OUTPUT_TYPES] },
825
+ via: {
826
+ type: "string",
827
+ minLength: 1,
828
+ description: "Skill that applies the write. Default: the first node skill that supports the type.",
829
+ },
830
+ max: {
831
+ type: "integer",
832
+ minimum: 1,
833
+ maximum: SAFE_OUTPUT_MAX_CEILING,
834
+ description: "Max writes of this type from this node per run. Default 1.",
835
+ },
836
+ target: {
837
+ type: "string",
838
+ minLength: 1,
839
+ description: "Pinned target (GitHub owner/repo, Linear team id). GitHub default: GITHUB_REPOSITORY.",
840
+ },
841
+ title_prefix: {
842
+ type: "string",
843
+ minLength: 1,
844
+ maxLength: 64,
845
+ description: "Prepended to issue and PR titles that do not already start with it.",
846
+ },
847
+ labels: {
848
+ type: "array",
849
+ items: { type: "string", minLength: 1 },
850
+ minItems: 1,
851
+ description: "issue / pr: labels always added. label: the only labels the agent may add.",
852
+ },
853
+ expires: {
854
+ type: "string",
855
+ pattern: SAFE_OUTPUT_EXPIRES_PATTERN.source,
856
+ description: "Drop an intent older than this when the write stage runs (e.g. 30m, 2h, 7d).",
857
+ },
858
+ },
859
+ },
708
860
  },
709
861
  properties: {
710
862
  id: { type: "string", minLength: 1 },
@@ -739,6 +891,46 @@ export const workflowJsonSchema = {
739
891
  minLength: 1,
740
892
  description: "Default execution model for every node. Overridable per-node. Free-text passthrough (no registry); resolved as node.model ?? workflow.model ?? executor default.",
741
893
  },
894
+ permissions: {
895
+ $ref: "#/$defs/Permissions",
896
+ description: "Default and ceiling for every node's permissions. Absent: write.",
897
+ },
898
+ safe_outputs: {
899
+ type: "object",
900
+ description: "Workflow-level safe-output policy: the ceiling on output types and run-wide limits.",
901
+ additionalProperties: false,
902
+ properties: {
903
+ allow: {
904
+ type: "array",
905
+ items: { type: "string", enum: [...SAFE_OUTPUT_TYPES] },
906
+ minItems: 1,
907
+ description: "Output types any node may declare. Absent: all types.",
908
+ },
909
+ max: {
910
+ type: "integer",
911
+ minimum: 1,
912
+ maximum: SAFE_OUTPUT_MAX_CEILING,
913
+ description: "Total writes per run across all nodes.",
914
+ },
915
+ staged: { type: "boolean", description: "Preview every write and apply none." },
916
+ trusted_actors: {
917
+ type: "array",
918
+ items: { type: "string", minLength: 1 },
919
+ minItems: 1,
920
+ description: "GitHub logins whose runs may write.",
921
+ },
922
+ trusted_associations: {
923
+ type: "array",
924
+ items: { type: "string", enum: [...AUTHOR_ASSOCIATIONS] },
925
+ minItems: 1,
926
+ description: "Author associations (from the GitHub event payload) whose runs may write.",
927
+ },
928
+ screen: {
929
+ type: "boolean",
930
+ description: "One model call that may veto the writes after every deterministic check. It can never authorize one.",
931
+ },
932
+ },
933
+ },
742
934
  judge_budget: {
743
935
  type: "integer",
744
936
  minimum: 0,
@@ -860,11 +1052,11 @@ export const workflowJsonSchema = {
860
1052
  disallowed_tools: {
861
1053
  type: "array",
862
1054
  items: { type: "string", minLength: 1 },
863
- description: "Built-in tool names the agent cannot use at this node (e.g. ['Bash']). Forwarded to the Claude Agent SDK's disallowedTools, which removes the tools from the model context entirely.",
1055
+ description: "Built-in tool names the agent cannot use at this node, in the harness's own names (Claude Code: ['Bash']). Passed through to the harness: Claude Code removes them from the model context; Codex maps known names to tool classes and reports what it cannot deny as degraded. For portable workflows prefer tool classes in tools.deny.",
864
1056
  },
865
1057
  tools: {
866
1058
  type: "object",
867
- description: "Per-node filter over skill-provided tools. 'allow' keeps only the listed skill tools; 'deny' removes the listed skill tools (applied after 'allow'). Filtered tools are never registered for the node's run. Absent field = all skill tools exposed. Complements disallowed_tools, which covers built-in agent tools only.",
1059
+ description: "Per-node filter over skill-provided tools. 'allow' keeps only the listed skill tools; 'deny' removes the listed skill tools (applied after 'allow'). Filtered tools are never registered for the node's run. Absent field = all skill tools exposed. A 'deny' entry that names a portable tool class (shell, write, edit, net, subagent) also denies that class of built-in agent tools on every harness.",
868
1060
  additionalProperties: false,
869
1061
  anyOf: [{ required: ["allow"] }, { required: ["deny"] }],
870
1062
  properties: {
@@ -877,6 +1069,7 @@ export const workflowJsonSchema = {
877
1069
  type: "array",
878
1070
  items: { type: "string", minLength: 1 },
879
1071
  minItems: 1,
1072
+ description: "Skill tool names to remove, and/or portable tool classes (shell, write, edit, net, subagent) to deny for built-in agent tools. Each harness enforces a class natively or reports it as degraded; under strict harness policy an unenforceable class refuses the node.",
880
1073
  },
881
1074
  },
882
1075
  },
@@ -889,6 +1082,16 @@ export const workflowJsonSchema = {
889
1082
  enum: [...NODE_ON_FAIL],
890
1083
  description: "What to do when this node finishes 'failed' (agent-level failure, or an eval failure that exhausted retries and was not softened by fail_soft). 'halt' (default) stops the workflow with the failure surfaced so a broken node never advances down a conditional edge; 'continue' preserves the legacy fall-through where routing proceeds from the failed node. Distinct from requires.on_fail (the pre-condition gate).",
891
1084
  },
1085
+ permissions: {
1086
+ $ref: "#/$defs/Permissions",
1087
+ description: "What this node's agent may do. Absent: read when the node declares outputs, else the workflow's permissions, else write.",
1088
+ },
1089
+ outputs: {
1090
+ type: "array",
1091
+ items: { $ref: "#/$defs/SafeOutput" },
1092
+ minItems: 1,
1093
+ description: "Typed write intents this node may emit with the emit_output tool. sweny applies them after the node, within the declared caps.",
1094
+ },
892
1095
  rules: {
893
1096
  $ref: "#/$defs/NodeSources",
894
1097
  description: "Per-node rules. Additive by default; set { only: true } to block cascade.",
@@ -981,7 +1184,10 @@ export const workflowJsonSchema = {
981
1184
  properties: {
982
1185
  from: { type: "string", minLength: 1 },
983
1186
  to: { type: "string", minLength: 1 },
984
- when: { type: "string", description: "Natural language condition. Claude evaluates at runtime." },
1187
+ when: {
1188
+ type: "string",
1189
+ description: "Natural language condition, evaluated at runtime by the workflow's harness.",
1190
+ },
985
1191
  max_iterations: {
986
1192
  type: "integer",
987
1193
  minimum: 1,
@@ -229,6 +229,24 @@ export const github = {
229
229
  return reused ? { ...pr, reused: true } : pr;
230
230
  },
231
231
  },
232
+ {
233
+ name: "github_add_labels",
234
+ access: "write",
235
+ description: "Add labels to a GitHub issue or pull request",
236
+ input_schema: {
237
+ type: "object",
238
+ properties: {
239
+ repo: { type: "string", description: "owner/repo" },
240
+ issue_number: { type: "number", description: "Issue or PR number" },
241
+ labels: { type: "array", items: { type: "string" }, description: "Labels to add" },
242
+ },
243
+ required: ["repo", "issue_number", "labels"],
244
+ },
245
+ handler: async (input, ctx) => gh(`/repos/${input.repo}/issues/${input.issue_number}/labels`, ctx, {
246
+ method: "POST",
247
+ body: JSON.stringify({ labels: input.labels }),
248
+ }),
249
+ },
232
250
  {
233
251
  name: "github_list_recent_commits",
234
252
  access: "read",
package/dist/types.d.ts CHANGED
@@ -2,6 +2,7 @@ export type { Source, ResolvedSource, SourceKind, SourceResolutionMap } from "./
2
2
  export type { WorkflowInputs, WorkflowInputField, WorkflowInputType, InputValidationError, InputValidationResult, } from "./inputs.js";
3
3
  export { WORKFLOW_INPUT_TYPES } from "./inputs.js";
4
4
  import type { Source as _Source, ResolvedSource as _ResolvedSource } from "./sources.js";
5
+ import type { ToolClass } from "./harness/types.js";
5
6
  export type JSONSchema = Record<string, unknown>;
6
7
  /** Context passed to tool handlers at execution time */
7
8
  export interface ToolContext {
@@ -361,6 +362,93 @@ export interface Node {
361
362
  * Distinct from `requires.on_fail`, which is the pre-condition gate.
362
363
  */
363
364
  on_fail?: NodeOnFail;
365
+ /**
366
+ * What this node's agent may do (#365). `read` runs the node read-only:
367
+ * only `access: "read"` skill tools, no external skill MCP servers, no
368
+ * shell / file-write / edit / fetch / subagent built-ins. Absent: `read`
369
+ * when the node declares `outputs`, else the workflow's `permissions`,
370
+ * else `write` (today's behavior).
371
+ */
372
+ permissions?: NodePermissions;
373
+ /**
374
+ * Typed write intents this node may emit (#365). The agent records them with
375
+ * the `emit_output` tool; sweny applies them after the node, within the
376
+ * declared caps. See {@link SafeOutputDeclaration}.
377
+ */
378
+ outputs?: SafeOutputDeclaration[];
379
+ }
380
+ /** Node access level. `read` = no write-capable tool reaches the agent. */
381
+ export declare const NODE_ACCESS: readonly ["read", "write"];
382
+ export type NodeAccess = (typeof NODE_ACCESS)[number];
383
+ /** Portable built-in tool classes, compiled per harness (Claude Code: native tool names). */
384
+ export declare const TOOL_CLASSES: readonly ["shell", "write", "edit", "net", "subagent"];
385
+ /** Object form of {@link NodePermissions}. */
386
+ export interface NodePermissionsSpec {
387
+ access?: NodeAccess;
388
+ /** Built-in tool classes the agent must not have. */
389
+ deny?: (typeof TOOL_CLASSES)[number][];
390
+ /** Exclusive MCP, and refuse the node on a harness that cannot enforce the policy. */
391
+ strict?: boolean;
392
+ }
393
+ /** `read` / `write` shorthand, or the object form. */
394
+ export type NodePermissions = NodeAccess | NodePermissionsSpec;
395
+ /** Write operations a node may request through `emit_output`. */
396
+ export declare const SAFE_OUTPUT_TYPES: readonly ["comment", "issue", "pr", "label"];
397
+ export type SafeOutputType = (typeof SAFE_OUTPUT_TYPES)[number];
398
+ /**
399
+ * Skills that can apply each output type. The write stage calls the skill's
400
+ * own tool handler; a skill not listed here cannot apply safe outputs.
401
+ */
402
+ export declare const SAFE_OUTPUT_APPLIERS: Readonly<Record<string, readonly SafeOutputType[]>>;
403
+ /** GitHub author associations accepted by `safe_outputs.trusted_associations`. */
404
+ export declare const AUTHOR_ASSOCIATIONS: readonly ["OWNER", "MEMBER", "COLLABORATOR", "CONTRIBUTOR", "FIRST_TIME_CONTRIBUTOR", "FIRST_TIMER", "NONE"];
405
+ export type AuthorAssociation = (typeof AUTHOR_ASSOCIATIONS)[number];
406
+ /** Upper bound on a single output's `max` and on `safe_outputs.max`. */
407
+ export declare const SAFE_OUTPUT_MAX_CEILING = 100;
408
+ /** `expires` grammar: a positive integer and a unit (s, m, h, d). */
409
+ export declare const SAFE_OUTPUT_EXPIRES_PATTERN: RegExp;
410
+ /** One declared output on a node. */
411
+ export interface SafeOutputDeclaration {
412
+ type: SafeOutputType;
413
+ /** Skill that applies it. Default: the first node skill that supports the type. */
414
+ via?: string;
415
+ /** Max writes of this type from this node per run. Default 1. */
416
+ max?: number;
417
+ /** Pinned target (GitHub `owner/repo`, Linear team id). GitHub default: `GITHUB_REPOSITORY`. */
418
+ target?: string;
419
+ /** Prepended to issue / PR titles that do not already start with it. */
420
+ title_prefix?: string;
421
+ /** issue / pr: labels always added. label: the only labels the agent may add. */
422
+ labels?: string[];
423
+ /** Drop an intent older than this when the write stage runs (e.g. `30m`, `2h`, `7d`). */
424
+ expires?: string;
425
+ }
426
+ /** Workflow-level safe-output policy: the ceiling and run-wide limits. */
427
+ export interface SafeOutputsPolicy {
428
+ /** Output types any node may declare. Absent: all types. */
429
+ allow?: SafeOutputType[];
430
+ /** Total writes per run across all nodes. */
431
+ max?: number;
432
+ /** Preview every write and apply none. */
433
+ staged?: boolean;
434
+ /** GitHub logins whose runs may write. */
435
+ trusted_actors?: string[];
436
+ /** Author associations whose runs may write (from the GitHub event payload). */
437
+ trusted_associations?: AuthorAssociation[];
438
+ /** One model call that may veto the writes. It can never authorize one. */
439
+ screen?: boolean;
440
+ }
441
+ /** What the write stage did with one intent. Metadata only: never a title or body. */
442
+ export interface SafeOutputReceipt {
443
+ type: string;
444
+ /** Skill that applied (or would apply) it. */
445
+ via?: string;
446
+ status: "applied" | "staged" | "skipped" | "refused" | "vetoed" | "failed";
447
+ /** Why it was skipped, refused, vetoed or failed. Fixed wording, never model text. */
448
+ reason?: string;
449
+ target?: string;
450
+ /** Issue / PR / comment number or id the write produced. */
451
+ ref?: string | number;
364
452
  }
365
453
  /**
366
454
  * Per-node skill-tool filter. See {@link Node.tools}.
@@ -376,7 +464,7 @@ export interface NodeToolFilter {
376
464
  export interface Edge {
377
465
  from: string;
378
466
  to: string;
379
- /** Natural language condition. Claude evaluates at runtime. */
467
+ /** Natural language condition, evaluated at runtime by the workflow's harness. */
380
468
  when?: string;
381
469
  /** Max times this edge can be followed (enables retry loops). Default: unlimited. */
382
470
  max_iterations?: number;
@@ -423,6 +511,10 @@ export interface Workflow {
423
511
  * See `src/inputs.ts` for the field shape and validation rules.
424
512
  */
425
513
  inputs?: import("./inputs.js").WorkflowInputs;
514
+ /** Default and ceiling for every node's `permissions` (#365). Absent: `write`. */
515
+ permissions?: NodePermissions;
516
+ /** Workflow-level safe-output policy (#365). */
517
+ safe_outputs?: SafeOutputsPolicy;
426
518
  }
427
519
  /**
428
520
  * Inline skill definition in a workflow's `skills` block.
@@ -489,6 +581,8 @@ export interface NodeResult {
489
581
  };
490
582
  /** Opinions this run could not honor natively. Always empty for Claude Code. */
491
583
  degraded?: string[];
584
+ /** Safe outputs (#365): what the write stage did with each intent. Absent when the node declares none. */
585
+ outputs?: SafeOutputReceipt[];
492
586
  }
493
587
  export interface ToolCall {
494
588
  tool: string;
@@ -624,6 +718,12 @@ export interface Claude {
624
718
  maxTurns?: number;
625
719
  /** Built-in SDK tool names to disallow for this node (e.g. ["Bash"]). */
626
720
  disallowedTools?: string[];
721
+ /**
722
+ * Portable built-in tool classes denied at this node (`tools.deny` entries
723
+ * that name a class: shell, write, edit, net, subagent). Each harness
724
+ * compiles them to its native form, or reports them in `degraded`.
725
+ */
726
+ deny?: ToolClass[];
627
727
  /** Per-node execution model. Overrides the client default when set. Absent = no SDK model option emitted. */
628
728
  model?: string;
629
729
  /** Abort the query after this many ms. Default: no timeout (back-compat). */
@@ -645,6 +745,12 @@ export interface Claude {
645
745
  * (external MCP servers, shell, file-edit built-ins).
646
746
  */
647
747
  readOnly?: boolean;
748
+ /**
749
+ * The node's portable policy (#365), compiled per adapter. Legacy `Claude`
750
+ * implementations may ignore it; `readOnly` and `disallowedTools` above
751
+ * carry the same read-only and native-deny intent.
752
+ */
753
+ policy?: import("./harness/types.js").NodePolicy;
648
754
  }): Promise<NodeResult>;
649
755
  /**
650
756
  * Evaluate a routing condition — pick one of N choices.
package/dist/types.js CHANGED
@@ -67,6 +67,35 @@ export const REQUIRES_ON_FAIL = ["fail", "skip"];
67
67
  export const NODE_ON_FAIL = ["halt", "continue"];
68
68
  /** MCP server transport type. Inferred from command/url when omitted. */
69
69
  export const MCP_TRANSPORTS = ["stdio", "http"];
70
+ // ─── Permissions and safe outputs (#365) ─────────────────────────
71
+ /** Node access level. `read` = no write-capable tool reaches the agent. */
72
+ export const NODE_ACCESS = ["read", "write"];
73
+ /** Portable built-in tool classes, compiled per harness (Claude Code: native tool names). */
74
+ export const TOOL_CLASSES = ["shell", "write", "edit", "net", "subagent"];
75
+ /** Write operations a node may request through `emit_output`. */
76
+ export const SAFE_OUTPUT_TYPES = ["comment", "issue", "pr", "label"];
77
+ /**
78
+ * Skills that can apply each output type. The write stage calls the skill's
79
+ * own tool handler; a skill not listed here cannot apply safe outputs.
80
+ */
81
+ export const SAFE_OUTPUT_APPLIERS = {
82
+ github: ["comment", "issue", "pr", "label"],
83
+ linear: ["comment", "issue"],
84
+ };
85
+ /** GitHub author associations accepted by `safe_outputs.trusted_associations`. */
86
+ export const AUTHOR_ASSOCIATIONS = [
87
+ "OWNER",
88
+ "MEMBER",
89
+ "COLLABORATOR",
90
+ "CONTRIBUTOR",
91
+ "FIRST_TIME_CONTRIBUTOR",
92
+ "FIRST_TIMER",
93
+ "NONE",
94
+ ];
95
+ /** Upper bound on a single output's `max` and on `safe_outputs.max`. */
96
+ export const SAFE_OUTPUT_MAX_CEILING = 100;
97
+ /** `expires` grammar: a positive integer and a unit (s, m, h, d). */
98
+ export const SAFE_OUTPUT_EXPIRES_PATTERN = /^[1-9][0-9]*(s|m|h|d)$/;
70
99
  /**
71
100
  * Workflow type discriminator. Cloud uses this to render runs in a
72
101
  * type-specific way. Adding a value requires a corresponding renderer.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweny-ai/core",
3
- "version": "0.1.142",
3
+ "version": "0.1.144",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sweny": "./dist/cli/main.js"