@sweny-ai/core 0.1.123 → 0.1.124
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/agent-env.d.ts +171 -0
- package/dist/agent-env.js +393 -0
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +1 -1
- package/dist/claude.d.ts +61 -3
- package/dist/claude.js +187 -27
- package/dist/cli/check.d.ts +42 -1
- package/dist/cli/check.js +130 -8
- package/dist/cli/cloud-lifecycle.d.ts +24 -29
- package/dist/cli/cloud-lifecycle.js +187 -12
- package/dist/cli/cloud-report.js +45 -2
- package/dist/cli/config-file.d.ts +8 -1
- package/dist/cli/config-file.js +42 -1
- package/dist/cli/config.js +11 -14
- package/dist/cli/e2e.d.ts +22 -0
- package/dist/cli/e2e.js +31 -4
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +138 -46
- package/dist/cli/marketplace.d.ts +3 -1
- package/dist/cli/marketplace.js +2 -2
- package/dist/cli/new.d.ts +15 -0
- package/dist/cli/new.js +139 -65
- package/dist/cli/publish.js +9 -0
- package/dist/cli/run-output.d.ts +59 -0
- package/dist/cli/run-output.js +189 -0
- package/dist/cli/skill-env.d.ts +34 -0
- package/dist/cli/skill-env.js +64 -0
- package/dist/cli/templates.js +587 -0
- package/dist/cli/workflow-input.d.ts +41 -0
- package/dist/cli/workflow-input.js +56 -0
- package/dist/executor.d.ts +17 -1
- package/dist/executor.js +119 -9
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/mermaid.d.ts +6 -0
- package/dist/mermaid.js +10 -5
- package/dist/schema.d.ts +218 -188
- package/dist/schema.js +24 -3
- package/dist/skills/betterstack.js +4 -0
- package/dist/skills/datadog.js +3 -0
- package/dist/skills/github.js +8 -0
- package/dist/skills/linear.js +8 -0
- package/dist/skills/notification.js +3 -0
- package/dist/skills/sentry.js +4 -0
- package/dist/skills/slack.js +2 -0
- package/dist/skills/supabase.js +9 -0
- package/dist/testing.d.ts +3 -2
- package/dist/testing.js +9 -3
- package/dist/types.d.ts +99 -2
- package/dist/types.js +17 -0
- package/dist/untrusted.d.ts +32 -0
- package/dist/untrusted.js +46 -0
- package/dist/workflows/browser.js +2 -2
- package/dist/workflows/implement.yml +2 -0
- package/dist/workflows/triage.yml +3 -1
- 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
|