@sweny-ai/core 0.1.123 → 0.1.125

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 (56) hide show
  1. package/dist/agent-env.d.ts +171 -0
  2. package/dist/agent-env.js +393 -0
  3. package/dist/browser.d.ts +2 -2
  4. package/dist/browser.js +1 -1
  5. package/dist/claude.d.ts +61 -3
  6. package/dist/claude.js +187 -27
  7. package/dist/cli/check.d.ts +42 -1
  8. package/dist/cli/check.js +130 -8
  9. package/dist/cli/cloud-lifecycle.d.ts +24 -29
  10. package/dist/cli/cloud-lifecycle.js +187 -12
  11. package/dist/cli/cloud-report.js +45 -2
  12. package/dist/cli/config-file.d.ts +8 -1
  13. package/dist/cli/config-file.js +42 -1
  14. package/dist/cli/config.js +11 -14
  15. package/dist/cli/e2e.d.ts +22 -0
  16. package/dist/cli/e2e.js +31 -4
  17. package/dist/cli/main.d.ts +1 -0
  18. package/dist/cli/main.js +138 -46
  19. package/dist/cli/marketplace.d.ts +3 -1
  20. package/dist/cli/marketplace.js +2 -2
  21. package/dist/cli/new.d.ts +15 -0
  22. package/dist/cli/new.js +139 -65
  23. package/dist/cli/publish.js +9 -0
  24. package/dist/cli/run-output.d.ts +59 -0
  25. package/dist/cli/run-output.js +189 -0
  26. package/dist/cli/skill-env.d.ts +34 -0
  27. package/dist/cli/skill-env.js +64 -0
  28. package/dist/cli/templates.js +587 -0
  29. package/dist/cli/workflow-input.d.ts +41 -0
  30. package/dist/cli/workflow-input.js +56 -0
  31. package/dist/executor.d.ts +17 -1
  32. package/dist/executor.js +119 -9
  33. package/dist/index.d.ts +3 -3
  34. package/dist/index.js +2 -2
  35. package/dist/mermaid.d.ts +6 -0
  36. package/dist/mermaid.js +10 -5
  37. package/dist/schema.d.ts +218 -188
  38. package/dist/schema.js +24 -3
  39. package/dist/skills/betterstack.js +4 -0
  40. package/dist/skills/datadog.js +3 -0
  41. package/dist/skills/github.js +8 -0
  42. package/dist/skills/linear.js +8 -0
  43. package/dist/skills/notification.js +3 -0
  44. package/dist/skills/sentry.js +4 -0
  45. package/dist/skills/slack.js +2 -0
  46. package/dist/skills/supabase.js +9 -0
  47. package/dist/testing.d.ts +3 -2
  48. package/dist/testing.js +9 -3
  49. package/dist/types.d.ts +99 -2
  50. package/dist/types.js +17 -0
  51. package/dist/untrusted.d.ts +32 -0
  52. package/dist/untrusted.js +46 -0
  53. package/dist/workflows/browser.js +2 -2
  54. package/dist/workflows/implement.yml +2 -0
  55. package/dist/workflows/triage.yml +3 -1
  56. package/package.json +1 -1
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Agent execution floor: scoped environment + SDK sandbox settings.
3
+ *
4
+ * Threat: the Claude Code subprocess runs model-chosen shell commands over
5
+ * attacker-influenceable input (issue bodies, alerts, fetched pages). Before
6
+ * this module it inherited the full `process.env` (every CI secret) and ran
7
+ * Bash with unrestricted network egress. Two controls live here:
8
+ *
9
+ * 1. {@link scopeAgentEnv}: the subprocess env is an allowlist, not a copy.
10
+ * On by default in CI, off locally (full env, as before); see
11
+ * {@link resolveEnvScope}.
12
+ * 2. {@link resolveAgentSandbox}: the SDK `sandbox` option is enabled with a
13
+ * network allowlist and the agent's own credentials denied to sandboxed
14
+ * commands. `auto` (default in CI) falls back to unsandboxed with one loud
15
+ * warning when the host cannot sandbox; `strict` fails closed.
16
+ *
17
+ * Configuration (env wins over `.sweny.yml`, see `applyAgentFileConfig` in
18
+ * cli/config-file.ts):
19
+ * SWENY_ENV_SCOPE / env-scope on | off (default: on in CI, off locally)
20
+ * SWENY_ENV_PASSTHROUGH / env-passthrough extra var names ("*" = inherit all)
21
+ * SWENY_SANDBOX / sandbox auto | strict | off (default: auto in CI, off locally)
22
+ * SWENY_SANDBOX_ALLOWED_DOMAINS / sandbox-allowed-domains extra hosts for sandboxed commands
23
+ */
24
+ import type { SandboxSettings } from "@anthropic-ai/claude-agent-sdk";
25
+ import type { Logger, Skill } from "./types.js";
26
+ /**
27
+ * Exact variable names always passed to the agent subprocess.
28
+ *
29
+ * Process basics (PATH, HOME, shell, temp), locale/TZ, proxy + CA settings
30
+ * (corporate egress), CI identity vars (no tokens), and Claude Code runtime
31
+ * knobs. Credentials are NOT in this list except via the ANTHROPIC_/CLAUDE_
32
+ * prefixes below, which the agent needs to authenticate at all.
33
+ */
34
+ export declare const AGENT_ENV_ALLOWLIST: readonly string[];
35
+ /**
36
+ * Prefixes always passed: locale (`LC_*`) and the agent's own backend config
37
+ * (`ANTHROPIC_*` incl. API key / auth token / base URL, `CLAUDE_*` incl.
38
+ * `CLAUDE_CODE_OAUTH_TOKEN`, `CLAUDE_CODE_USE_BEDROCK`, `CLAUDE_CONFIG_DIR`).
39
+ */
40
+ export declare const AGENT_ENV_PREFIXES: readonly string[];
41
+ /** The agent's own credentials. Needed by Claude Code, never by the commands it runs. */
42
+ export declare const AGENT_AUTH_VARS: readonly string[];
43
+ /** Split a comma/whitespace separated list, dropping empties. */
44
+ export declare function parseList(v: string | undefined): string[];
45
+ export interface BuildAgentEnvOpts {
46
+ /** Per-call names to include (a node's declared skill env vars). */
47
+ extraVars?: readonly string[];
48
+ /** Operator passthrough list (`env-passthrough`). `"*"` inherits everything. */
49
+ passthrough?: readonly string[];
50
+ logger?: Pick<Logger, "warn">;
51
+ }
52
+ /**
53
+ * Whether the agent env is scoped to the allowlist. `SWENY_ENV_SCOPE` (or the
54
+ * explicit value) `on|true|1` / `off|false|0` wins everywhere; otherwise on
55
+ * when `CI` is truthy and off locally, so local runs keep the full env that
56
+ * e2e/test workflows rely on (DATABASE_URL, BASE_URL, NODE_ENV, ...).
57
+ */
58
+ export declare function resolveEnvScope(env: Record<string, string | undefined>, explicit?: boolean, logger?: Pick<Logger, "warn">): boolean;
59
+ /** Max names listed in the withheld-vars warning before "and N more". */
60
+ export declare const WITHHELD_WARNING_CAP = 30;
61
+ /**
62
+ * One-line warning naming the withheld variables. Names only, never values;
63
+ * sorted; capped at {@link WITHHELD_WARNING_CAP}.
64
+ */
65
+ export declare function formatWithheldWarning(withheld: readonly string[]): string;
66
+ /**
67
+ * Scope the agent env and report which variable names were withheld.
68
+ * `withheld` holds names only; values never leave `source`.
69
+ */
70
+ export declare function scopeAgentEnv(source: Record<string, string | undefined>, opts?: BuildAgentEnvOpts): {
71
+ env: Record<string, string>;
72
+ withheld: string[];
73
+ };
74
+ /**
75
+ * Build the agent subprocess env from `source` using the allowlist.
76
+ *
77
+ * Kept: {@link AGENT_ENV_ALLOWLIST}, {@link AGENT_ENV_PREFIXES}, `AWS_*` when
78
+ * `CLAUDE_CODE_USE_BEDROCK` is set, Vertex vars when `CLAUDE_CODE_USE_VERTEX`
79
+ * is set, `extraVars`, and `passthrough`. Everything else is dropped. Nullish
80
+ * values are always dropped. Pure: never mutates `source`.
81
+ */
82
+ export declare function buildAgentEnv(source: Record<string, string | undefined>, opts?: BuildAgentEnvOpts): Record<string, string>;
83
+ /** What one node's agent call may see: env var names and network hosts. */
84
+ export interface AgentAccess {
85
+ envVars: string[];
86
+ domains: string[];
87
+ }
88
+ /** Hosts reachable by sandboxed commands in every node: source hosting + package registries. */
89
+ export declare const DEFAULT_SANDBOX_DOMAINS: readonly string[];
90
+ /** Provider hosts added when a node references the built-in skill. */
91
+ export declare const SKILL_SANDBOX_DOMAINS: Readonly<Record<string, readonly string[]>>;
92
+ /**
93
+ * Resolve a node's access from its `skills:` list: every env var named by a
94
+ * referenced skill's config fields, plus that skill's provider hosts.
95
+ * Unknown skill ids contribute nothing (the executor validates them).
96
+ */
97
+ export declare function resolveAgentAccess(skillIds: readonly string[], skills: Map<string, Skill>): AgentAccess;
98
+ /**
99
+ * `off`: never sandbox (default for local runs, so "it's your repo" keeps
100
+ * working: private registries, docker, cargo, go). `auto` (default when CI is
101
+ * truthy): sandbox when the host supports it, otherwise warn once and run
102
+ * unsandboxed so unattended CI never breaks on a missing dependency.
103
+ * `strict`: sandbox or fail closed.
104
+ */
105
+ export type SandboxMode = "off" | "auto" | "strict";
106
+ /**
107
+ * Parse the sandbox mode from the explicit `mode` or `SWENY_SANDBOX`.
108
+ * Accepts `off|false|0`, `auto`, `strict|on|true|1` (`on` is an alias for
109
+ * `strict`). Unset or unknown (with a warning) uses the default: `auto` when
110
+ * `CI` is truthy, `off` otherwise. An explicit value always wins.
111
+ */
112
+ export declare function resolveSandboxMode(env: Record<string, string | undefined>, mode?: string, logger?: Pick<Logger, "warn">): SandboxMode;
113
+ /**
114
+ * Probe whether the SDK sandbox can start on this host. Returns a reason
115
+ * string when it cannot, undefined when it looks supported.
116
+ *
117
+ * macOS uses the built-in `sandbox-exec`. Linux needs `bwrap` (bubblewrap)
118
+ * and `socat` on PATH, and bwrap must be able to create a namespace
119
+ * (`bwrapCheck`, a real `bwrap ... true` run by default). Anything else is
120
+ * unsupported.
121
+ */
122
+ export declare function checkSandboxSupport(platform?: NodeJS.Platform, pathEnv?: string, exists?: (p: string) => boolean, bwrapCheck?: () => string | undefined): string | undefined;
123
+ /**
124
+ * The SDK sandbox settings used when the sandbox is on.
125
+ *
126
+ * - `failIfUnavailable`: true under `strict` (the SDK errors instead of
127
+ * running unsandboxed); false under `auto`, so a host that passes the
128
+ * preflight but still cannot start the sandbox degrades instead of failing.
129
+ * - `allowUnsandboxedCommands: false`: the model cannot escape via the
130
+ * `dangerouslyDisableSandbox` parameter. Required, because nodes run under
131
+ * `bypassPermissions` (#365) which would otherwise auto-approve the escape.
132
+ * - `network.allowedDomains` + `strictAllowlist`: non-listed hosts are denied
133
+ * deterministically rather than prompted (no prompt exists headless).
134
+ * - `credentials`: the agent's own Anthropic credentials are unset for
135
+ * sandboxed commands, and the Claude Code credentials file is unreadable.
136
+ */
137
+ export declare function buildSandboxSettings(domains: readonly string[], opts?: {
138
+ failIfUnavailable?: boolean;
139
+ home?: string;
140
+ }): SandboxSettings;
141
+ export interface ResolveAgentSandboxOpts {
142
+ env: Record<string, string | undefined>;
143
+ /** Explicit mode (client option); falls back to `env.SWENY_SANDBOX`. */
144
+ mode?: SandboxMode;
145
+ /** Explicit extra hosts (client option); falls back to `env.SWENY_SANDBOX_ALLOWED_DOMAINS`. */
146
+ allowedDomains?: readonly string[];
147
+ /** Per-node hosts from {@link resolveAgentAccess}. */
148
+ nodeDomains?: readonly string[];
149
+ /** Preflight probe; test seam. Defaults to {@link checkSandboxSupport}, cached per process. */
150
+ probe?: () => string | undefined;
151
+ logger?: Pick<Logger, "warn">;
152
+ }
153
+ export interface AgentSandboxResolution {
154
+ mode: SandboxMode;
155
+ /** SDK `sandbox` option; absent when running unsandboxed. */
156
+ settings?: SandboxSettings;
157
+ /** `strict` only: the host cannot sandbox. Callers must fail the call. */
158
+ error?: string;
159
+ /** `auto` only: the host cannot sandbox, running unsandboxed. Log it loudly, once. */
160
+ warning?: string;
161
+ }
162
+ /**
163
+ * Resolve the `sandbox` query option for one agent call.
164
+ *
165
+ * - `off`: no settings.
166
+ * - supported host: `{ settings }`.
167
+ * - unsupported host under `auto`: `{ warning }`, run unsandboxed (back-compat:
168
+ * unattended CI must not break on a missing dependency).
169
+ * - unsupported host under `strict`: `{ error }`, fail closed.
170
+ */
171
+ export declare function resolveAgentSandbox(opts: ResolveAgentSandboxOpts): AgentSandboxResolution;
@@ -0,0 +1,393 @@
1
+ /**
2
+ * Agent execution floor: scoped environment + SDK sandbox settings.
3
+ *
4
+ * Threat: the Claude Code subprocess runs model-chosen shell commands over
5
+ * attacker-influenceable input (issue bodies, alerts, fetched pages). Before
6
+ * this module it inherited the full `process.env` (every CI secret) and ran
7
+ * Bash with unrestricted network egress. Two controls live here:
8
+ *
9
+ * 1. {@link scopeAgentEnv}: the subprocess env is an allowlist, not a copy.
10
+ * On by default in CI, off locally (full env, as before); see
11
+ * {@link resolveEnvScope}.
12
+ * 2. {@link resolveAgentSandbox}: the SDK `sandbox` option is enabled with a
13
+ * network allowlist and the agent's own credentials denied to sandboxed
14
+ * commands. `auto` (default in CI) falls back to unsandboxed with one loud
15
+ * warning when the host cannot sandbox; `strict` fails closed.
16
+ *
17
+ * Configuration (env wins over `.sweny.yml`, see `applyAgentFileConfig` in
18
+ * cli/config-file.ts):
19
+ * SWENY_ENV_SCOPE / env-scope on | off (default: on in CI, off locally)
20
+ * SWENY_ENV_PASSTHROUGH / env-passthrough extra var names ("*" = inherit all)
21
+ * SWENY_SANDBOX / sandbox auto | strict | off (default: auto in CI, off locally)
22
+ * SWENY_SANDBOX_ALLOWED_DOMAINS / sandbox-allowed-domains extra hosts for sandboxed commands
23
+ */
24
+ import { execFileSync } from "node:child_process";
25
+ import { existsSync } from "node:fs";
26
+ import { homedir } from "node:os";
27
+ import * as path from "node:path";
28
+ // ─── Scoped env ──────────────────────────────────────────────────
29
+ /**
30
+ * Exact variable names always passed to the agent subprocess.
31
+ *
32
+ * Process basics (PATH, HOME, shell, temp), locale/TZ, proxy + CA settings
33
+ * (corporate egress), CI identity vars (no tokens), and Claude Code runtime
34
+ * knobs. Credentials are NOT in this list except via the ANTHROPIC_/CLAUDE_
35
+ * prefixes below, which the agent needs to authenticate at all.
36
+ */
37
+ export const AGENT_ENV_ALLOWLIST = [
38
+ // process basics
39
+ "PATH",
40
+ "HOME",
41
+ "USER",
42
+ "LOGNAME",
43
+ "SHELL",
44
+ "TERM",
45
+ "TMPDIR",
46
+ "TMP",
47
+ "TEMP",
48
+ "XDG_CONFIG_HOME",
49
+ "XDG_CACHE_HOME",
50
+ "XDG_DATA_HOME",
51
+ "XDG_RUNTIME_DIR",
52
+ // Windows process basics
53
+ "USERPROFILE",
54
+ "APPDATA",
55
+ "LOCALAPPDATA",
56
+ "SystemRoot",
57
+ "ComSpec",
58
+ "PATHEXT",
59
+ // locale / time
60
+ "LANG",
61
+ "LANGUAGE",
62
+ "TZ",
63
+ // proxy + TLS trust
64
+ "HTTP_PROXY",
65
+ "HTTPS_PROXY",
66
+ "NO_PROXY",
67
+ "ALL_PROXY",
68
+ "http_proxy",
69
+ "https_proxy",
70
+ "no_proxy",
71
+ "all_proxy",
72
+ "NODE_EXTRA_CA_CERTS",
73
+ "SSL_CERT_FILE",
74
+ "SSL_CERT_DIR",
75
+ // CI identity (never tokens)
76
+ "CI",
77
+ "GITHUB_ACTIONS",
78
+ "GITHUB_REPOSITORY",
79
+ "GITHUB_REPOSITORY_OWNER",
80
+ "GITHUB_WORKSPACE",
81
+ "GITHUB_SHA",
82
+ "GITHUB_REF",
83
+ "GITHUB_REF_NAME",
84
+ "GITHUB_HEAD_REF",
85
+ "GITHUB_BASE_REF",
86
+ "GITHUB_RUN_ID",
87
+ "GITHUB_RUN_NUMBER",
88
+ "GITHUB_SERVER_URL",
89
+ "GITHUB_API_URL",
90
+ "GITHUB_EVENT_NAME",
91
+ "RUNNER_OS",
92
+ "RUNNER_TEMP",
93
+ "GITLAB_CI",
94
+ "CI_PROJECT_DIR",
95
+ "CI_COMMIT_SHA",
96
+ "BUILDKITE",
97
+ "CIRCLECI",
98
+ "JENKINS_URL",
99
+ "TF_BUILD",
100
+ // Claude Code runtime knobs (not credentials)
101
+ "MAX_THINKING_TOKENS",
102
+ "MCP_TIMEOUT",
103
+ "MCP_TOOL_TIMEOUT",
104
+ "BASH_DEFAULT_TIMEOUT_MS",
105
+ "BASH_MAX_TIMEOUT_MS",
106
+ "DISABLE_TELEMETRY",
107
+ "DISABLE_AUTOUPDATER",
108
+ "DISABLE_ERROR_REPORTING",
109
+ "DISABLE_NON_ESSENTIAL_MODEL_CALLS",
110
+ ];
111
+ /**
112
+ * Prefixes always passed: locale (`LC_*`) and the agent's own backend config
113
+ * (`ANTHROPIC_*` incl. API key / auth token / base URL, `CLAUDE_*` incl.
114
+ * `CLAUDE_CODE_OAUTH_TOKEN`, `CLAUDE_CODE_USE_BEDROCK`, `CLAUDE_CONFIG_DIR`).
115
+ */
116
+ export const AGENT_ENV_PREFIXES = ["LC_", "ANTHROPIC_", "CLAUDE_"];
117
+ /** Cloud-provider vars passed only when Claude Code is routed through that provider. */
118
+ const BEDROCK_PREFIXES = ["AWS_"];
119
+ const VERTEX_VARS = ["GOOGLE_APPLICATION_CREDENTIALS", "CLOUD_ML_REGION", "GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT"];
120
+ /** The agent's own credentials. Needed by Claude Code, never by the commands it runs. */
121
+ export const AGENT_AUTH_VARS = [
122
+ "ANTHROPIC_API_KEY",
123
+ "ANTHROPIC_AUTH_TOKEN",
124
+ "CLAUDE_CODE_OAUTH_TOKEN",
125
+ ];
126
+ function truthy(v) {
127
+ if (!v)
128
+ return false;
129
+ const s = v.trim().toLowerCase();
130
+ return s !== "" && s !== "0" && s !== "false" && s !== "no" && s !== "off";
131
+ }
132
+ /** Split a comma/whitespace separated list, dropping empties. */
133
+ export function parseList(v) {
134
+ if (!v)
135
+ return [];
136
+ return v
137
+ .split(/[,\s]+/)
138
+ .map((s) => s.trim())
139
+ .filter(Boolean);
140
+ }
141
+ /**
142
+ * Whether the agent env is scoped to the allowlist. `SWENY_ENV_SCOPE` (or the
143
+ * explicit value) `on|true|1` / `off|false|0` wins everywhere; otherwise on
144
+ * when `CI` is truthy and off locally, so local runs keep the full env that
145
+ * e2e/test workflows rely on (DATABASE_URL, BASE_URL, NODE_ENV, ...).
146
+ */
147
+ export function resolveEnvScope(env, explicit, logger) {
148
+ if (explicit !== undefined)
149
+ return explicit;
150
+ const raw = (env.SWENY_ENV_SCOPE ?? "").trim().toLowerCase();
151
+ if (raw === "on" || raw === "true" || raw === "1")
152
+ return true;
153
+ if (raw === "off" || raw === "false" || raw === "0")
154
+ return false;
155
+ const fallback = truthy(env.CI);
156
+ if (raw !== "")
157
+ logger?.warn(`SWENY_ENV_SCOPE="${raw}" is not one of on|off; using ${fallback ? "on" : "off"}`);
158
+ return fallback;
159
+ }
160
+ /** Max names listed in the withheld-vars warning before "and N more". */
161
+ export const WITHHELD_WARNING_CAP = 30;
162
+ /**
163
+ * One-line warning naming the withheld variables. Names only, never values;
164
+ * sorted; capped at {@link WITHHELD_WARNING_CAP}.
165
+ */
166
+ export function formatWithheldWarning(withheld) {
167
+ const names = [...withheld].sort();
168
+ const shown = names.slice(0, WITHHELD_WARNING_CAP).join(", ");
169
+ const more = names.length > WITHHELD_WARNING_CAP ? `, and ${names.length - WITHHELD_WARNING_CAP} more` : "";
170
+ return (`Agent env scoping withheld ${names.length} environment variable(s) from the agent: ${shown}${more}. ` +
171
+ `If a node's commands need any of them, add the names to env-passthrough ` +
172
+ `(SWENY_ENV_PASSTHROUGH), or set env-scope: off (SWENY_ENV_SCOPE=off).`);
173
+ }
174
+ /**
175
+ * Scope the agent env and report which variable names were withheld.
176
+ * `withheld` holds names only; values never leave `source`.
177
+ */
178
+ export function scopeAgentEnv(source, opts = {}) {
179
+ const env = buildAgentEnv(source, opts);
180
+ const withheld = Object.keys(source).filter((k) => source[k] != null && !(k in env));
181
+ return { env, withheld };
182
+ }
183
+ /**
184
+ * Build the agent subprocess env from `source` using the allowlist.
185
+ *
186
+ * Kept: {@link AGENT_ENV_ALLOWLIST}, {@link AGENT_ENV_PREFIXES}, `AWS_*` when
187
+ * `CLAUDE_CODE_USE_BEDROCK` is set, Vertex vars when `CLAUDE_CODE_USE_VERTEX`
188
+ * is set, `extraVars`, and `passthrough`. Everything else is dropped. Nullish
189
+ * values are always dropped. Pure: never mutates `source`.
190
+ */
191
+ export function buildAgentEnv(source, opts = {}) {
192
+ const passthrough = opts.passthrough ?? [];
193
+ if (passthrough.includes("*")) {
194
+ opts.logger?.warn("[sweny] env-passthrough contains '*': the agent inherits the full environment, including every secret.");
195
+ return Object.fromEntries(Object.entries(source).filter((e) => e[1] != null));
196
+ }
197
+ const exact = new Set([...AGENT_ENV_ALLOWLIST, ...(opts.extraVars ?? []), ...passthrough]);
198
+ const prefixes = [...AGENT_ENV_PREFIXES];
199
+ if (truthy(source.CLAUDE_CODE_USE_BEDROCK))
200
+ prefixes.push(...BEDROCK_PREFIXES);
201
+ if (truthy(source.CLAUDE_CODE_USE_VERTEX))
202
+ for (const v of VERTEX_VARS)
203
+ exact.add(v);
204
+ const out = {};
205
+ for (const [k, v] of Object.entries(source)) {
206
+ if (v == null)
207
+ continue;
208
+ if (exact.has(k) || prefixes.some((p) => k.startsWith(p)))
209
+ out[k] = v;
210
+ }
211
+ return out;
212
+ }
213
+ /** Hosts reachable by sandboxed commands in every node: source hosting + package registries. */
214
+ export const DEFAULT_SANDBOX_DOMAINS = [
215
+ "github.com",
216
+ "api.github.com",
217
+ "codeload.github.com",
218
+ "uploads.github.com",
219
+ "*.githubusercontent.com",
220
+ "registry.npmjs.org",
221
+ "registry.yarnpkg.com",
222
+ "pypi.org",
223
+ "files.pythonhosted.org",
224
+ "proxy.golang.org",
225
+ "sum.golang.org",
226
+ "crates.io",
227
+ "static.crates.io",
228
+ "index.crates.io",
229
+ "rubygems.org",
230
+ ];
231
+ /** Provider hosts added when a node references the built-in skill. */
232
+ export const SKILL_SANDBOX_DOMAINS = {
233
+ github: ["github.com", "api.github.com", "uploads.github.com", "*.githubusercontent.com"],
234
+ linear: ["api.linear.app", "mcp.linear.app"],
235
+ sentry: ["sentry.io", "*.sentry.io"],
236
+ datadog: ["*.datadoghq.com", "*.datadoghq.eu", "*.ddog-gov.com"],
237
+ betterstack: ["*.betterstack.com", "*.betterstackdata.com"],
238
+ slack: ["slack.com", "*.slack.com"],
239
+ supabase: ["*.supabase.co"],
240
+ gitlab: ["gitlab.com"],
241
+ };
242
+ /**
243
+ * Resolve a node's access from its `skills:` list: every env var named by a
244
+ * referenced skill's config fields, plus that skill's provider hosts.
245
+ * Unknown skill ids contribute nothing (the executor validates them).
246
+ */
247
+ export function resolveAgentAccess(skillIds, skills) {
248
+ const envVars = new Set();
249
+ const domains = new Set();
250
+ for (const id of skillIds) {
251
+ const skill = skills.get(id);
252
+ for (const field of Object.values(skill?.config ?? {})) {
253
+ if (field.env)
254
+ envVars.add(field.env);
255
+ }
256
+ for (const d of SKILL_SANDBOX_DOMAINS[id] ?? [])
257
+ domains.add(d);
258
+ }
259
+ return { envVars: [...envVars], domains: [...domains] };
260
+ }
261
+ /**
262
+ * Parse the sandbox mode from the explicit `mode` or `SWENY_SANDBOX`.
263
+ * Accepts `off|false|0`, `auto`, `strict|on|true|1` (`on` is an alias for
264
+ * `strict`). Unset or unknown (with a warning) uses the default: `auto` when
265
+ * `CI` is truthy, `off` otherwise. An explicit value always wins.
266
+ */
267
+ export function resolveSandboxMode(env, mode, logger) {
268
+ const raw = (mode ?? env.SWENY_SANDBOX ?? "").trim().toLowerCase();
269
+ if (raw === "off" || raw === "false" || raw === "0")
270
+ return "off";
271
+ if (raw === "strict" || raw === "on" || raw === "true" || raw === "1")
272
+ return "strict";
273
+ if (raw === "auto")
274
+ return "auto";
275
+ const fallback = truthy(env.CI) ? "auto" : "off";
276
+ if (raw !== "")
277
+ logger?.warn(`SWENY_SANDBOX="${raw}" is not one of off|auto|strict; using ${fallback}`);
278
+ return fallback;
279
+ }
280
+ /** Functional bubblewrap check: can it actually create a sandbox on this host? */
281
+ function bwrapWorks() {
282
+ try {
283
+ execFileSync("bwrap", ["--ro-bind", "/", "/", "--dev", "/dev", "--proc", "/proc", "true"], {
284
+ stdio: ["ignore", "ignore", "pipe"],
285
+ timeout: 10_000,
286
+ });
287
+ return undefined;
288
+ }
289
+ catch (err) {
290
+ const stderr = String(err.stderr ?? err.message ?? "").trim();
291
+ const userns = /user namespace|uid map|setting up uid|Operation not permitted|Permission denied/i.test(stderr);
292
+ return (`bwrap cannot create a sandbox (${stderr.split("\n")[0] || "unknown error"})` +
293
+ (userns
294
+ ? "; unprivileged user namespaces look restricted (Ubuntu 23.10+: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0)"
295
+ : ""));
296
+ }
297
+ }
298
+ /**
299
+ * Probe whether the SDK sandbox can start on this host. Returns a reason
300
+ * string when it cannot, undefined when it looks supported.
301
+ *
302
+ * macOS uses the built-in `sandbox-exec`. Linux needs `bwrap` (bubblewrap)
303
+ * and `socat` on PATH, and bwrap must be able to create a namespace
304
+ * (`bwrapCheck`, a real `bwrap ... true` run by default). Anything else is
305
+ * unsupported.
306
+ */
307
+ export function checkSandboxSupport(platform = process.platform, pathEnv = process.env.PATH ?? "", exists = existsSync, bwrapCheck = bwrapWorks) {
308
+ if (platform === "darwin")
309
+ return undefined;
310
+ if (platform !== "linux")
311
+ return `the Claude Code sandbox does not support platform "${platform}"`;
312
+ const dirs = pathEnv.split(path.delimiter).filter(Boolean);
313
+ const missing = ["bwrap", "socat"].filter((bin) => !dirs.some((d) => exists(path.join(d, bin))));
314
+ if (missing.length > 0) {
315
+ return `missing ${missing.join(" and ")} on PATH (install: sudo apt-get install -y bubblewrap socat)`;
316
+ }
317
+ return bwrapCheck();
318
+ }
319
+ let cachedProbe;
320
+ /** {@link checkSandboxSupport} for this host, computed once per process. */
321
+ function defaultProbe() {
322
+ if (!cachedProbe)
323
+ cachedProbe = { result: checkSandboxSupport() };
324
+ return cachedProbe.result;
325
+ }
326
+ /**
327
+ * The SDK sandbox settings used when the sandbox is on.
328
+ *
329
+ * - `failIfUnavailable`: true under `strict` (the SDK errors instead of
330
+ * running unsandboxed); false under `auto`, so a host that passes the
331
+ * preflight but still cannot start the sandbox degrades instead of failing.
332
+ * - `allowUnsandboxedCommands: false`: the model cannot escape via the
333
+ * `dangerouslyDisableSandbox` parameter. Required, because nodes run under
334
+ * `bypassPermissions` (#365) which would otherwise auto-approve the escape.
335
+ * - `network.allowedDomains` + `strictAllowlist`: non-listed hosts are denied
336
+ * deterministically rather than prompted (no prompt exists headless).
337
+ * - `credentials`: the agent's own Anthropic credentials are unset for
338
+ * sandboxed commands, and the Claude Code credentials file is unreadable.
339
+ */
340
+ export function buildSandboxSettings(domains, opts = {}) {
341
+ return {
342
+ enabled: true,
343
+ failIfUnavailable: opts.failIfUnavailable ?? true,
344
+ autoAllowBashIfSandboxed: true,
345
+ allowUnsandboxedCommands: false,
346
+ network: {
347
+ allowedDomains: [...new Set(domains)],
348
+ strictAllowlist: true,
349
+ },
350
+ credentials: {
351
+ envVars: AGENT_AUTH_VARS.map((name) => ({ name, mode: "deny" })),
352
+ files: [{ path: path.join(opts.home ?? homedir(), ".claude", ".credentials.json"), mode: "deny" }],
353
+ },
354
+ };
355
+ }
356
+ /**
357
+ * Resolve the `sandbox` query option for one agent call.
358
+ *
359
+ * - `off`: no settings.
360
+ * - supported host: `{ settings }`.
361
+ * - unsupported host under `auto`: `{ warning }`, run unsandboxed (back-compat:
362
+ * unattended CI must not break on a missing dependency).
363
+ * - unsupported host under `strict`: `{ error }`, fail closed.
364
+ */
365
+ export function resolveAgentSandbox(opts) {
366
+ const mode = resolveSandboxMode(opts.env, opts.mode, opts.logger);
367
+ if (mode === "off")
368
+ return { mode };
369
+ const reason = (opts.probe ?? defaultProbe)();
370
+ if (reason) {
371
+ if (mode === "strict") {
372
+ return {
373
+ mode,
374
+ error: `Agent sandbox is required (SWENY_SANDBOX=strict) but unavailable: ${reason}. ` +
375
+ `Refusing to run the agent unsandboxed. Fix the host, or set SWENY_SANDBOX=auto to fall back with a warning.`,
376
+ };
377
+ }
378
+ return {
379
+ mode,
380
+ warning: `Agent sandbox unavailable: ${reason}. Running agent shell commands UNSANDBOXED ` +
381
+ `(network and filesystem unrestricted; env scoping and untrusted-input fencing still apply). ` +
382
+ `Fix the host to sandbox, set SWENY_SANDBOX=strict (or \`sandbox: strict\` in .sweny.yml) to fail instead, ` +
383
+ `or SWENY_SANDBOX=off to silence this.`,
384
+ };
385
+ }
386
+ const extra = opts.allowedDomains ?? parseList(opts.env.SWENY_SANDBOX_ALLOWED_DOMAINS);
387
+ return {
388
+ mode,
389
+ settings: buildSandboxSettings([...DEFAULT_SANDBOX_DOMAINS, ...(opts.nodeDomains ?? []), ...extra], {
390
+ failIfUnavailable: mode === "strict",
391
+ }),
392
+ };
393
+ }
package/dist/browser.d.ts CHANGED
@@ -13,9 +13,9 @@
13
13
  * - `buildWorkflow` / `refineWorkflow` — depend on the Claude interface.
14
14
  * - Testing helpers — `MockClaude`, `createFileSkill` use `node:fs`.
15
15
  */
16
- export type { Skill, SkillCategory, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, Evaluator, EvaluatorRule, EvalResult, EvaluatorKind, NodeRequires, NodeRetry, NodeToolFilter, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, McpTransport, WorkflowType, SkillHarnessKey, } from "./types.js";
16
+ export type { Skill, SkillCategory, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, Evaluator, EvaluatorRule, EvalResult, EvaluatorKind, NodeRequires, NodeRetry, NodeToolFilter, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, NodeOnFail, McpTransport, WorkflowType, SkillHarnessKey, } from "./types.js";
17
17
  export { consoleLogger } from "./types.js";
18
- export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, MCP_TRANSPORTS, SKILL_CATEGORIES, SKILL_HARNESSES, SKILL_ID_PATTERN, SKILL_ID_MAX_LENGTH, isValidSkillId, } from "./types.js";
18
+ 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";
19
19
  export { github, linear, slack, sentry, datadog, betterstack, notification, supabase, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
20
20
  export type { SkillValidationResult } from "./skills/index.js";
21
21
  export { workflowZ, nodeZ, edgeZ, skillZ, parseWorkflow, validateWorkflow, workflowJsonSchema, skillJsonSchema, } from "./schema.js";
package/dist/browser.js CHANGED
@@ -18,7 +18,7 @@ export { consoleLogger } from "./types.js";
18
18
  // function from types.ts, which is already proven browser-safe (schema.ts
19
19
  // imports it). A Studio dropdown rendering evaluator kinds, or validating a
20
20
  // skill id, needs these without hardcoding the members.
21
- export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, MCP_TRANSPORTS, SKILL_CATEGORIES, SKILL_HARNESSES, SKILL_ID_PATTERN, SKILL_ID_MAX_LENGTH, isValidSkillId, } from "./types.js";
21
+ 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";
22
22
  // Skills (browser-safe — no filesystem access, no `process.env` reads)
23
23
  export { github, linear, slack, sentry, datadog, betterstack, notification, supabase, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
24
24
  // Schema & validation