patchwork-os 1.2.0-beta.3.canary.889 → 1.2.0-beta.3.canary.890

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.
@@ -30,7 +30,9 @@ import { fileURLToPath } from "node:url";
30
30
  import { parse as parseYaml } from "yaml";
31
31
  import { approvalIdentityMatches, computeApprovedActionIdentity, executionEvidence, } from "../approvalIdentity.js";
32
32
  import { captureFixture } from "../connectors/fixtureRecorder.js";
33
- import { sanitizeEnv } from "../drivers/claude/envSanitizer.js";
33
+ import { allowlistEnv, sanitizeEnv } from "../drivers/claude/envSanitizer.js";
34
+ import { CLAUDE_PROVIDER_ENV_KEYS, containedPermissionArgs, } from "../drivers/claude/subprocess.js";
35
+ import { createSubprocessSettings, readWorkspaceDenies, } from "../drivers/claude/subprocessSettings.js";
34
36
  import { FLAG_CIRCUIT_BREAKER, FLAG_ENFORCE_ALLOWWRITES, isEnabled, } from "../featureFlags.js";
35
37
  import { computeEffectivePolicy } from "../governance/effectivePolicy.js";
36
38
  import { readKillSwitch } from "../governance/killSwitchPolicy.js";
@@ -3411,7 +3413,12 @@ export function defaultClaudeCodeFn(prompt, opts) {
3411
3413
  // silently falling back to no-MCP spawn — the recipe explicitly asked
3412
3414
  // for bridge tools and should be routed through SubprocessDriver via
3413
3415
  // the runtime injector instead.
3414
- if (opts?.mcpAccess === true) {
3416
+ // Only a STRICT (governed) containment takes the contained branch. A compat
3417
+ // `sandbox: true` opt-in keeps the legacy branch below, byte-identical.
3418
+ const containment = opts?.containment?.enforced === true && opts.containment.strict === true
3419
+ ? opts.containment
3420
+ : undefined;
3421
+ if (opts?.mcpAccess === true || containment?.mcpAccess === true) {
3415
3422
  return Promise.resolve("[agent step failed: recipe_mcp_unsupported — defaultClaudeCodeFn does not support mcpAccess:true; route via SubprocessDriver or unset the mcpAccess flag on this step]");
3416
3423
  }
3417
3424
  // P0-5 opt-in tool sandbox on the `recipe run --local` / non-bridge path.
@@ -3421,6 +3428,22 @@ export function defaultClaudeCodeFn(prompt, opts) {
3421
3428
  // and always apply --disallowed-tools regardless of mode.
3422
3429
  const sandboxAllowed = (Array.isArray(opts?.allowedTools) ? opts.allowedTools : []).filter((t) => typeof t === "string" && t.length > 0 && !t.startsWith("-"));
3423
3430
  const sandboxDenied = (Array.isArray(opts?.disallowedTools) ? opts.disallowedTools : []).filter((t) => typeof t === "string" && t.length > 0 && !t.startsWith("-"));
3431
+ // A contained run gets the same settings deny file the driver writes. If it
3432
+ // cannot be written, refuse rather than run the agent uncontained.
3433
+ let containedSettingsPath;
3434
+ // The workspace's own deny rules: applied by us, because a strict run does
3435
+ // not load the workspace's settings sources (their hooks and allows could
3436
+ // widen it).
3437
+ const workspaceDenies = containment
3438
+ ? readWorkspaceDenies(workspace.path)
3439
+ : [];
3440
+ if (containment) {
3441
+ const settings = createSubprocessSettings(() => { }, [...containment.deniedTools, ...workspaceDenies], { strict: true });
3442
+ if (!settings.write()) {
3443
+ return Promise.resolve("[agent step failed: contained_settings_unwritable — could not write the contained run's settings file; refusing to run the agent uncontained]");
3444
+ }
3445
+ containedSettingsPath = settings.path;
3446
+ }
3424
3447
  const localArgs = [
3425
3448
  "-p",
3426
3449
  prompt,
@@ -3440,13 +3463,22 @@ export function defaultClaudeCodeFn(prompt, opts) {
3440
3463
  : RECIPE_SYSTEM_PROMPT_COMPAT),
3441
3464
  "--no-session-persistence",
3442
3465
  ];
3443
- if (opts?.sandbox === true && sandboxAllowed.length > 0) {
3444
- localArgs.push("--permission-mode", "dontAsk");
3445
- localArgs.push("--allowed-tools", ...sandboxAllowed);
3466
+ if (containment && containedSettingsPath) {
3467
+ localArgs.push("--settings", containedSettingsPath);
3468
+ localArgs.push(...containedPermissionArgs(containment, [
3469
+ ...sandboxDenied,
3470
+ ...workspaceDenies,
3471
+ ]));
3446
3472
  }
3447
- // Deny rules apply in ANY mode.
3448
- if (sandboxDenied.length > 0) {
3449
- localArgs.push("--disallowed-tools", ...sandboxDenied);
3473
+ else {
3474
+ if (opts?.sandbox === true && sandboxAllowed.length > 0) {
3475
+ localArgs.push("--permission-mode", "dontAsk");
3476
+ localArgs.push("--allowed-tools", ...sandboxAllowed);
3477
+ }
3478
+ // Deny rules apply in ANY mode.
3479
+ if (sandboxDenied.length > 0) {
3480
+ localArgs.push("--disallowed-tools", ...sandboxDenied);
3481
+ }
3450
3482
  }
3451
3483
  try {
3452
3484
  const result = spawnSync(binary, localArgs, {
@@ -3455,7 +3487,11 @@ export function defaultClaudeCodeFn(prompt, opts) {
3455
3487
  // child so the spawn doesn't re-authenticate as, or nest under,
3456
3488
  // the parent Claude Code session. Mirrors SubprocessDriver.run
3457
3489
  // hygiene. Preserves CLAUDE_CODE_OAUTH_TOKEN (subscription auth).
3458
- env: sanitizeEnv(process.env),
3490
+ // Contained: the same env allowlist the driver uses (no connector
3491
+ // tokens, bridge secrets or PATCHWORK_* settings reach the child).
3492
+ env: containment?.envAllowlist
3493
+ ? allowlistEnv(process.env, { providerKeys: CLAUDE_PROVIDER_ENV_KEYS })
3494
+ : sanitizeEnv(process.env),
3459
3495
  encoding: "utf-8",
3460
3496
  timeout: 600_000,
3461
3497
  maxBuffer: 10 * 1024 * 1024,