@gethmy/harness 1.4.0 → 1.6.0

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/index.js CHANGED
@@ -584,7 +584,6 @@ var init_stageHandoff = __esm(() => {
584
584
 
585
585
  // ../harmony-shared/dist/types.js
586
586
  var init_types = () => {};
587
-
588
587
  // ../harmony-shared/dist/index.js
589
588
  var init_dist = __esm(() => {
590
589
  init_agentStaleness();
@@ -610,6 +609,492 @@ var init_dist = __esm(() => {
610
609
  init_types();
611
610
  });
612
611
 
612
+ // src/confine-to-repo.ts
613
+ import { realpathSync } from "node:fs";
614
+ import { dirname, isAbsolute, parse, resolve, sep } from "node:path";
615
+ function isGitMetadata(repoRoot, candidate) {
616
+ const rel = candidate.startsWith(repoRoot) ? candidate.slice(repoRoot.length) : candidate;
617
+ return rel.split(/[\\/]/).some((segment) => segment.toLowerCase() === ".git");
618
+ }
619
+ function patternEscapes(pattern) {
620
+ if (isAbsolute(pattern))
621
+ return true;
622
+ if (/[{}[\]~()!+@]/.test(pattern))
623
+ return true;
624
+ return pattern.split(/[\\/]/).some((segment) => segment === "..");
625
+ }
626
+ function pathArgsFor(mode) {
627
+ return mode === "write" ? { ...READ_PATH_ARGS, ...WRITE_PATH_ARGS } : READ_PATH_ARGS;
628
+ }
629
+ function realPathOrNearest(p) {
630
+ const abs = isAbsolute(p) ? p : resolve(p);
631
+ const { root } = parse(abs);
632
+ let real = root;
633
+ for (const part of abs.slice(root.length).split(sep)) {
634
+ if (part === "" || part === ".")
635
+ continue;
636
+ if (part === "..") {
637
+ real = dirname(real);
638
+ continue;
639
+ }
640
+ const next = real.endsWith(sep) ? real + part : real + sep + part;
641
+ try {
642
+ real = realpathSync(next);
643
+ } catch {
644
+ real = next;
645
+ }
646
+ }
647
+ return real;
648
+ }
649
+ function isInsideTree(root, target) {
650
+ const normalizedRoot = realPathOrNearest(root);
651
+ const normalizedTarget = realPathOrNearest(target);
652
+ if (normalizedTarget === normalizedRoot)
653
+ return true;
654
+ return normalizedTarget.startsWith(normalizedRoot + sep);
655
+ }
656
+ function decideConfinedTool(repoRoot, toolName, input, mode = "read") {
657
+ const pathArgs = pathArgsFor(mode)[toolName];
658
+ if (!pathArgs) {
659
+ return {
660
+ behavior: "deny",
661
+ message: `${toolName} is not available to this run.`
662
+ };
663
+ }
664
+ const verb = toolName in WRITE_PATH_ARGS ? "write" : "read";
665
+ for (const key of PATTERN_ARG_BY_TOOL[toolName] ?? []) {
666
+ const value = input[key];
667
+ if (typeof value !== "string" || value.length === 0)
668
+ continue;
669
+ if (patternEscapes(value)) {
670
+ return {
671
+ behavior: "deny",
672
+ message: `${toolName} patterns must stay inside the repository — no absolute path and no "..". Refused: ${value}`
673
+ };
674
+ }
675
+ }
676
+ for (const key of pathArgs) {
677
+ const value = input[key];
678
+ if (typeof value !== "string" || value.length === 0)
679
+ continue;
680
+ const candidate = isAbsolute(value) ? value : `${repoRoot}${sep}${value}`;
681
+ if (isGitMetadata(repoRoot, candidate)) {
682
+ return {
683
+ behavior: "deny",
684
+ message: `${toolName} may not touch the repository's git metadata. Refused: ${value}`
685
+ };
686
+ }
687
+ if (!isInsideTree(repoRoot, candidate)) {
688
+ return {
689
+ behavior: "deny",
690
+ message: `${toolName} may only ${verb} inside the repository. Refused: ${value}`
691
+ };
692
+ }
693
+ }
694
+ return { behavior: "allow" };
695
+ }
696
+ function confineToRepo(repoRoot, mode = "read") {
697
+ return async (toolName, input) => decideConfinedTool(repoRoot, toolName, input, mode);
698
+ }
699
+ var READ_PATH_ARGS, WRITE_PATH_ARGS, CONFINED_READ_TOOLS, CONFINED_WRITE_TOOLS, PATTERN_ARG_BY_TOOL;
700
+ var init_confine_to_repo = __esm(() => {
701
+ READ_PATH_ARGS = {
702
+ Read: ["file_path", "path", "notebook_path"],
703
+ Grep: ["path"],
704
+ Glob: ["path"]
705
+ };
706
+ WRITE_PATH_ARGS = {
707
+ Write: ["file_path"],
708
+ Edit: ["file_path"],
709
+ MultiEdit: ["file_path"],
710
+ NotebookEdit: ["notebook_path"]
711
+ };
712
+ CONFINED_READ_TOOLS = Object.freeze(Object.keys(READ_PATH_ARGS));
713
+ CONFINED_WRITE_TOOLS = Object.freeze([
714
+ ...Object.keys(READ_PATH_ARGS),
715
+ ...Object.keys(WRITE_PATH_ARGS).filter((t) => t !== "MultiEdit")
716
+ ]);
717
+ PATTERN_ARG_BY_TOOL = {
718
+ Glob: ["pattern"],
719
+ Grep: ["glob"]
720
+ };
721
+ });
722
+
723
+ // src/runner.ts
724
+ import { getConfigDir } from "@gethmy/mcp/src/config.js";
725
+ function mayHoldCredentials(role) {
726
+ return role === "author" || role === "reviewer";
727
+ }
728
+ function credentialReadDeny() {
729
+ return `Read(/${getConfigDir()}/**)`;
730
+ }
731
+ function credentialAccessDeny() {
732
+ const dir = `/${getConfigDir()}/**`;
733
+ return [`Read(${dir})`, `Grep(${dir})`, `Glob(${dir})`];
734
+ }
735
+ function buildRoleLaunch(args) {
736
+ const role = normalizeStageRole(args.role);
737
+ const keep = mayHoldCredentials(role);
738
+ const env = {};
739
+ for (const [key, value] of Object.entries(args.parentEnv)) {
740
+ if (value === undefined)
741
+ continue;
742
+ if (!keep && HARMONY_CREDENTIAL_KEYS.includes(key))
743
+ continue;
744
+ env[key] = value;
745
+ }
746
+ return {
747
+ role,
748
+ prompt: args.prompt,
749
+ repoPath: args.repoPath,
750
+ env,
751
+ disallowedTools: keep ? [] : [credentialReadDeny()]
752
+ };
753
+ }
754
+ function envKeysDroppedByLaunch(parentEnv, launch) {
755
+ return Object.keys(parentEnv).filter((key) => parentEnv[key] !== undefined && !Object.hasOwn(launch.env, key));
756
+ }
757
+ var HARMONY_CREDENTIAL_KEYS;
758
+ var init_runner = __esm(() => {
759
+ init_dist();
760
+ HARMONY_CREDENTIAL_KEYS = [
761
+ "HARMONY_API_KEY",
762
+ "HARMONY_API_URL",
763
+ "HARMONY_WORKSPACE_ID",
764
+ "SUPABASE_ANON_KEY",
765
+ "SUPABASE_SERVICE_ROLE_KEY",
766
+ "SUPABASE_URL"
767
+ ];
768
+ });
769
+
770
+ // src/run-containment.ts
771
+ import { createHash } from "node:crypto";
772
+ import { readFileSync } from "node:fs";
773
+ import { createRequire as createRequire2 } from "node:module";
774
+ import { homedir, tmpdir } from "node:os";
775
+ import { dirname as dirname2, isAbsolute as isAbsolute2, join } from "node:path";
776
+ import { getConfigDir as getConfigDir2 } from "@gethmy/mcp/src/config.js";
777
+ function credentialDirectories() {
778
+ const home = homedir();
779
+ return [
780
+ getConfigDir2(),
781
+ join(home, ".claude"),
782
+ join(home, ".claude.json"),
783
+ join(home, ".ssh"),
784
+ join(home, ".gnupg"),
785
+ join(home, ".aws"),
786
+ join(home, ".codex"),
787
+ join(home, ".gemini"),
788
+ join(home, ".config", "gh"),
789
+ join(home, ".config", "gcloud"),
790
+ join(home, ".config", "anthropic"),
791
+ join(home, ".config", "op"),
792
+ join(home, ".docker"),
793
+ join(home, ".kube"),
794
+ join(home, ".netrc"),
795
+ join(home, ".npmrc"),
796
+ join(home, ".git-credentials")
797
+ ];
798
+ }
799
+ function writeOnlyDenyPaths() {
800
+ const paths = [join(homedir(), ".gitconfig")];
801
+ const xdg = process.env.XDG_CONFIG_HOME;
802
+ paths.push(xdg && isAbsolute2(xdg) ? join(xdg, "git") : join(homedir(), ".config", "git"));
803
+ return paths;
804
+ }
805
+ function credentialToolDeny() {
806
+ return credentialDirectories().flatMap((dir) => [
807
+ `Read(/${dir})`,
808
+ `Read(/${dir}/**)`
809
+ ]);
810
+ }
811
+ function toolchainCacheDirectories(worktree) {
812
+ const home = homedir();
813
+ const scratch = `harmony-run-${createHash("sha256").update(worktree).digest("hex").slice(0, 16)}`;
814
+ const candidate = process.env.TMPDIR ?? tmpdir();
815
+ const tmpRoot = isAbsolute2(candidate) ? candidate : "/tmp";
816
+ return [
817
+ join(home, ".bun", "install", "cache"),
818
+ join(home, ".npm", "_cacache"),
819
+ join(tmpRoot, scratch)
820
+ ];
821
+ }
822
+ function secretEnvKeysToStrip(parentEnv = process.env) {
823
+ const stripped = new Set(HARMONY_CREDENTIAL_KEYS);
824
+ for (const key of Object.keys(parentEnv)) {
825
+ if (KEEP_ENV_KEYS.has(key))
826
+ continue;
827
+ if (SECRET_ENV_PATTERN.test(key))
828
+ stripped.add(key);
829
+ }
830
+ return [...stripped];
831
+ }
832
+ function assertNoProjectSandboxOverride(worktree) {
833
+ for (const name of ["settings.json", "settings.local.json"]) {
834
+ const path = join(worktree, ".claude", name);
835
+ let raw;
836
+ try {
837
+ raw = readFileSync(path, "utf-8");
838
+ } catch {
839
+ continue;
840
+ }
841
+ let parsed;
842
+ try {
843
+ parsed = JSON.parse(raw);
844
+ } catch {
845
+ continue;
846
+ }
847
+ if (parsed === null || typeof parsed !== "object")
848
+ continue;
849
+ const offending = Object.keys(parsed).filter((key) => !INERT_PROJECT_SETTING_KEYS.has(key));
850
+ if (offending.length > 0) {
851
+ throw new ProjectSandboxOverrideError(path, offending);
852
+ }
853
+ }
854
+ }
855
+ function containedEnv(parentEnv = process.env) {
856
+ const strip = new Set(secretEnvKeysToStrip(parentEnv));
857
+ const out = {};
858
+ for (const [key, value] of Object.entries(parentEnv)) {
859
+ if (value === undefined || strip.has(key))
860
+ continue;
861
+ out[key] = value;
862
+ }
863
+ return out;
864
+ }
865
+ function gitMetadataDenyPaths(worktree) {
866
+ const paths = new Set;
867
+ const dotGit = join(worktree, ".git");
868
+ const require2 = createRequire2(import.meta.url);
869
+ const { execFileSync } = require2("node:child_process");
870
+ const { statSync } = require2("node:fs");
871
+ const gitDirs = new Set;
872
+ try {
873
+ const out = execFileSync("git", [...GIT_NO_HOOKS, "rev-parse", "--git-dir", "--git-common-dir"], { cwd: worktree, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
874
+ for (const line of out.split(`
875
+ `)) {
876
+ const trimmed = line.trim();
877
+ if (!trimmed)
878
+ continue;
879
+ gitDirs.add(isAbsolute2(trimmed) ? trimmed : join(worktree, trimmed));
880
+ }
881
+ } catch {}
882
+ gitDirs.add(dotGit);
883
+ for (const dir of gitDirs) {
884
+ paths.add(join(dir, "config"));
885
+ paths.add(join(dir, "config.worktree"));
886
+ paths.add(join(dir, "hooks"));
887
+ }
888
+ try {
889
+ if (statSync(dotGit).isFile())
890
+ paths.add(dotGit);
891
+ } catch {}
892
+ return [...paths];
893
+ }
894
+ function hostPersistencePaths() {
895
+ const home = homedir();
896
+ return [
897
+ join(home, ".zshenv"),
898
+ join(home, ".zprofile"),
899
+ join(home, ".zshrc"),
900
+ join(home, ".zlogin"),
901
+ join(home, ".bashrc"),
902
+ join(home, ".bash_profile"),
903
+ join(home, ".bash_login"),
904
+ join(home, ".profile"),
905
+ join(home, ".config", "fish", "config.fish"),
906
+ join(home, ".config", "fish", "conf.d"),
907
+ join(home, "Library", "LaunchAgents"),
908
+ join(home, ".config", "systemd", "user"),
909
+ join(home, ".config", "autostart")
910
+ ];
911
+ }
912
+ function credentialWriteToolDeny(worktree) {
913
+ return [
914
+ ...credentialDirectories(),
915
+ ...writeOnlyDenyPaths(),
916
+ ...hostPersistencePaths(),
917
+ ...gitMetadataDenyPaths(worktree)
918
+ ].flatMap((p) => [`Edit(/${p})`, `Edit(/${p}/**)`]);
919
+ }
920
+ function implementRunToolPolicy(args) {
921
+ const writable = [args.worktree, ...toolchainCacheDirectories(args.worktree)];
922
+ const gitMeta = gitMetadataDenyPaths(args.worktree);
923
+ const secrets = credentialDirectories();
924
+ return async (toolName, input) => {
925
+ const allow = { behavior: "allow", updatedInput: input };
926
+ const deny = (message) => ({ behavior: "deny", message });
927
+ if (toolName.startsWith("mcp__"))
928
+ return allow;
929
+ if (toolName === "Bash" || toolName === "BashOutput") {
930
+ return allow;
931
+ }
932
+ const writePaths = WRITE_TOOL_PATHS[toolName];
933
+ if (writePaths) {
934
+ if (args.readOnly === true) {
935
+ return deny(`${toolName} is not available to a review run.`);
936
+ }
937
+ for (const key of writePaths) {
938
+ const value = input[key];
939
+ if (typeof value !== "string" || value.length === 0)
940
+ continue;
941
+ const target = isAbsolute2(value) ? value : join(args.worktree, value);
942
+ if (gitMeta.some((p) => isInsideTree(p, target) || p === target)) {
943
+ return deny(`${toolName} may not touch git metadata. Refused: ${value}`);
944
+ }
945
+ if (!writable.some((root) => isInsideTree(root, target))) {
946
+ return deny(`${toolName} may only write inside this run's worktree. Refused: ${value}`);
947
+ }
948
+ }
949
+ return allow;
950
+ }
951
+ const readPaths = READ_TOOL_PATHS[toolName];
952
+ if (readPaths) {
953
+ for (const key of readPaths) {
954
+ const value = input[key];
955
+ if (typeof value !== "string" || value.length === 0)
956
+ continue;
957
+ const target = isAbsolute2(value) ? value : join(args.worktree, value);
958
+ if (secrets.some((dir) => isInsideTree(dir, target))) {
959
+ return deny(`${toolName} may not read the credential directories. Refused: ${value}`);
960
+ }
961
+ }
962
+ return allow;
963
+ }
964
+ return allow;
965
+ };
966
+ }
967
+ function harmonyMcpServer() {
968
+ const require2 = createRequire2(import.meta.url);
969
+ const cli = join(dirname2(require2.resolve("@gethmy/mcp")), "cli.js");
970
+ return {
971
+ harmony: {
972
+ type: "stdio",
973
+ command: process.execPath,
974
+ args: [cli, "serve"]
975
+ }
976
+ };
977
+ }
978
+ function implementRunContainment(args) {
979
+ assertNoProjectSandboxOverride(args.worktree);
980
+ return {
981
+ sandbox: {
982
+ enabled: true,
983
+ failIfUnavailable: true,
984
+ allowUnsandboxedCommands: false,
985
+ autoAllowBashIfSandboxed: args.readOnly !== true,
986
+ network: {
987
+ allowedDomains: [...IMPLEMENT_ALLOWED_DOMAINS],
988
+ allowLocalBinding: false
989
+ },
990
+ filesystem: {
991
+ allowWrite: [
992
+ args.worktree,
993
+ ...toolchainCacheDirectories(args.worktree)
994
+ ],
995
+ denyRead: credentialDirectories(),
996
+ denyWrite: [
997
+ ...credentialDirectories(),
998
+ ...writeOnlyDenyPaths(),
999
+ ...hostPersistencePaths(),
1000
+ ...gitMetadataDenyPaths(args.worktree)
1001
+ ]
1002
+ }
1003
+ },
1004
+ canUseTool: implementRunToolPolicy(args),
1005
+ gateEveryToolCall: true,
1006
+ settingSources: args.readOnly === true ? [] : ["project"],
1007
+ mcpServers: harmonyMcpServer(),
1008
+ strictMcpConfig: true,
1009
+ stripEnvKeys: secretEnvKeysToStrip(),
1010
+ disallowedTools: [
1011
+ ...args.extraDisallowedTools ?? [],
1012
+ ...credentialToolDeny(),
1013
+ ...credentialWriteToolDeny(args.worktree)
1014
+ ]
1015
+ };
1016
+ }
1017
+ function implementRunContainmentCliArgs(args) {
1018
+ const containment = implementRunContainment(args);
1019
+ return [
1020
+ "--settings",
1021
+ JSON.stringify({ sandbox: containment.sandbox }),
1022
+ "--setting-sources",
1023
+ containment.settingSources.join(","),
1024
+ "--mcp-config",
1025
+ JSON.stringify({ mcpServers: containment.mcpServers }),
1026
+ "--strict-mcp-config",
1027
+ "--disallowedTools",
1028
+ containment.disallowedTools.join(",")
1029
+ ];
1030
+ }
1031
+ var SECRET_ENV_PATTERN, KEEP_ENV_KEYS, ProjectSandboxOverrideError, INERT_PROJECT_SETTING_KEYS, WRITE_TOOL_PATHS, READ_TOOL_PATHS, GIT_NO_HOOKS, IMPLEMENT_ALLOWED_DOMAINS;
1032
+ var init_run_containment = __esm(() => {
1033
+ init_confine_to_repo();
1034
+ init_runner();
1035
+ SECRET_ENV_PATTERN = /(TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|API_?KEY|_PAT$|^PAT_|PRIVATE_KEY|ACCESS_KEY)/i;
1036
+ KEEP_ENV_KEYS = new Set([
1037
+ "ANTHROPIC_API_KEY",
1038
+ "ANTHROPIC_AUTH_TOKEN",
1039
+ "ANTHROPIC_BASE_URL",
1040
+ "CLAUDE_CODE_OAUTH_TOKEN",
1041
+ "SSH_AUTH_SOCK",
1042
+ "GIT_AUTHOR_NAME",
1043
+ "GIT_AUTHOR_EMAIL",
1044
+ "GIT_COMMITTER_NAME",
1045
+ "GIT_COMMITTER_EMAIL",
1046
+ "XAUTHORITY"
1047
+ ]);
1048
+ ProjectSandboxOverrideError = class ProjectSandboxOverrideError extends Error {
1049
+ settingsPath;
1050
+ offendingKeys;
1051
+ constructor(settingsPath, offendingKeys) {
1052
+ super(`Refusing to spawn: ${settingsPath} sets ${offendingKeys.map((k) => `"${k}"`).join(", ")}. ` + "Project settings are loaded so the run can read CLAUDE.md, and that same layer " + "can widen the sandbox (`sandbox.filesystem.allowWrite`) or execute commands " + "outside it (`hooks`, `env`). A run that can write this file could therefore " + "widen its own bounds, so only keys known not to affect execution are accepted. " + "Remove the key, or the run stays refused.");
1053
+ this.settingsPath = settingsPath;
1054
+ this.offendingKeys = offendingKeys;
1055
+ this.name = "ProjectSandboxOverrideError";
1056
+ }
1057
+ };
1058
+ INERT_PROJECT_SETTING_KEYS = new Set([
1059
+ "$schema",
1060
+ "cleanupPeriodDays",
1061
+ "includeCoAuthoredBy",
1062
+ "language",
1063
+ "outputStyle",
1064
+ "spinnerTipsEnabled",
1065
+ "theme",
1066
+ "verbose"
1067
+ ]);
1068
+ WRITE_TOOL_PATHS = {
1069
+ Write: ["file_path"],
1070
+ Edit: ["file_path"],
1071
+ MultiEdit: ["file_path"],
1072
+ NotebookEdit: ["notebook_path"]
1073
+ };
1074
+ READ_TOOL_PATHS = {
1075
+ Read: ["file_path", "path", "notebook_path"],
1076
+ Grep: ["path"],
1077
+ Glob: ["path"]
1078
+ };
1079
+ GIT_NO_HOOKS = [
1080
+ "-c",
1081
+ "core.hooksPath=",
1082
+ "-c",
1083
+ "core.fsmonitor=",
1084
+ "-c",
1085
+ "core.pager=cat"
1086
+ ];
1087
+ IMPLEMENT_ALLOWED_DOMAINS = [
1088
+ "api.anthropic.com",
1089
+ "*.anthropic.com",
1090
+ "registry.npmjs.org",
1091
+ "*.npmjs.org",
1092
+ "github.com",
1093
+ "*.github.com",
1094
+ "*.githubusercontent.com"
1095
+ ];
1096
+ });
1097
+
613
1098
  // src/git-pr.ts
614
1099
  var exports_git_pr = {};
615
1100
  __export(exports_git_pr, {
@@ -643,16 +1128,16 @@ __export(exports_git_pr, {
643
1128
  buildPrBody: () => buildPrBody
644
1129
  });
645
1130
  import { execFile as execFile2, execFileSync as execFileSync7 } from "node:child_process";
646
- import { promisify as promisify2 } from "node:util";
1131
+ import { promisify as promisify3 } from "node:util";
647
1132
  function createExecFileAsync2() {
648
- return promisify2(execFile2);
1133
+ return promisify3(execFile2);
649
1134
  }
650
1135
  function execFileAsync2() {
651
1136
  return cachedExecFileAsync2 ??= createExecFileAsync2();
652
1137
  }
653
1138
  function detectGitProvider(cwd) {
654
1139
  try {
655
- const url = execFileSync7("git", ["remote", "get-url", "origin"], {
1140
+ const url = execFileSync7("git", [...GIT_NO_HOOKS, "remote", "get-url", "origin"], {
656
1141
  cwd,
657
1142
  encoding: "utf-8"
658
1143
  }).trim();
@@ -702,7 +1187,7 @@ function validateGitProviderCli(provider, cwd) {
702
1187
  }
703
1188
  case "bitbucket":
704
1189
  case "unknown":
705
- log.warn(TAG13, `Git provider "${provider}" — PR creation will be skipped (no CLI support)`);
1190
+ log.warn(TAG14, `Git provider "${provider}" — PR creation will be skipped (no CLI support)`);
706
1191
  break;
707
1192
  }
708
1193
  }
@@ -950,7 +1435,7 @@ async function mergePullRequest(prUrl, cwd, provider, strategy, deleteBranch) {
950
1435
  }
951
1436
  function getHeadSha(cwd) {
952
1437
  try {
953
- return execFileSync7("git", ["rev-parse", "HEAD"], {
1438
+ return execFileSync7("git", [...GIT_NO_HOOKS, "rev-parse", "HEAD"], {
954
1439
  cwd,
955
1440
  encoding: "utf-8"
956
1441
  }).trim();
@@ -985,7 +1470,7 @@ async function checkPrMergeStatus(prUrl, cwd, provider) {
985
1470
  try {
986
1471
  parsed = JSON.parse(stdout.trim());
987
1472
  } catch {
988
- log.warn(TAG13, `Failed to parse glab JSON output for MR ${mrMatch[1]}`);
1473
+ log.warn(TAG14, `Failed to parse glab JSON output for MR ${mrMatch[1]}`);
989
1474
  return "unknown";
990
1475
  }
991
1476
  if (typeof parsed !== "object" || parsed === null)
@@ -1083,7 +1568,7 @@ async function resolvePrHeadBranch(prUrl, cwd, provider) {
1083
1568
  const { stdout } = await execFileAsync2()("gh", ["pr", "view", prUrl, "--json", "headRefName,isCrossRepository"], { cwd, encoding: "utf-8", timeout: 1e4 });
1084
1569
  return decidePrBranch("github", stdout);
1085
1570
  } catch (err) {
1086
- log.warn(TAG13, `gh pr view failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
1571
+ log.warn(TAG14, `gh pr view failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
1087
1572
  return decidePrBranch("github", null);
1088
1573
  }
1089
1574
  }
@@ -1099,7 +1584,7 @@ async function resolvePrHeadBranch(prUrl, cwd, provider) {
1099
1584
  const { stdout } = await execFileAsync2()("az", ["repos", "pr", "show", "--id", prId, "--output", "json"], { cwd, encoding: "utf-8", timeout: 1e4 });
1100
1585
  return decidePrBranch("azure", stdout);
1101
1586
  } catch (err) {
1102
- log.warn(TAG13, `az repos pr show failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
1587
+ log.warn(TAG14, `az repos pr show failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
1103
1588
  return decidePrBranch("azure", null);
1104
1589
  }
1105
1590
  }
@@ -1127,7 +1612,13 @@ function resolvePrUrl(description, branchName, cwd, provider) {
1127
1612
  }
1128
1613
  function remoteBranchExists(branchName, cwd) {
1129
1614
  try {
1130
- execFileSync7("git", ["ls-remote", "--exit-code", "origin", `refs/heads/${branchName}`], { cwd, stdio: "pipe" });
1615
+ execFileSync7("git", [
1616
+ ...GIT_NO_HOOKS,
1617
+ "ls-remote",
1618
+ "--exit-code",
1619
+ "origin",
1620
+ `refs/heads/${branchName}`
1621
+ ], { cwd, stdio: "pipe" });
1131
1622
  return true;
1132
1623
  } catch {
1133
1624
  return false;
@@ -1135,24 +1626,24 @@ function remoteBranchExists(branchName, cwd) {
1135
1626
  }
1136
1627
  function pushBranch(branchName, cwd) {
1137
1628
  if (remoteBranchExists(branchName, cwd)) {
1138
- log.info(TAG13, `Remote branch ${branchName} exists (rework), force-pushing`);
1629
+ log.info(TAG14, `Remote branch ${branchName} exists (rework), force-pushing`);
1139
1630
  let expectedSha = null;
1140
1631
  try {
1141
- execFileSync7("git", ["fetch", "origin", branchName], {
1632
+ execFileSync7("git", [...GIT_NO_HOOKS, "fetch", "origin", branchName], {
1142
1633
  cwd,
1143
1634
  stdio: "pipe"
1144
1635
  });
1145
- expectedSha = execFileSync7("git", ["rev-parse", `refs/remotes/origin/${branchName}`], { cwd, encoding: "utf-8" }).trim();
1636
+ expectedSha = execFileSync7("git", [...GIT_NO_HOOKS, "rev-parse", `refs/remotes/origin/${branchName}`], { cwd, encoding: "utf-8" }).trim();
1146
1637
  } catch (err) {
1147
- log.warn(TAG13, `could not resolve remote tip for ${branchName}, falling back to weak lease: ${err instanceof Error ? err.message : err}`);
1638
+ log.warn(TAG14, `could not resolve remote tip for ${branchName}, falling back to weak lease: ${err instanceof Error ? err.message : err}`);
1148
1639
  }
1149
1640
  const lease = expectedSha ? `--force-with-lease=refs/heads/${branchName}:${expectedSha}` : "--force-with-lease";
1150
- execFileSync7("git", ["push", lease, "-u", "origin", branchName], {
1641
+ execFileSync7("git", [...GIT_NO_HOOKS, "push", lease, "-u", "origin", branchName], {
1151
1642
  cwd,
1152
1643
  stdio: "pipe"
1153
1644
  });
1154
1645
  } else {
1155
- execFileSync7("git", ["push", "-u", "origin", branchName], {
1646
+ execFileSync7("git", [...GIT_NO_HOOKS, "push", "-u", "origin", branchName], {
1156
1647
  cwd,
1157
1648
  stdio: "pipe"
1158
1649
  });
@@ -1163,25 +1654,31 @@ function renameRemoteBranch(oldRef, newRef, cwd) {
1163
1654
  return;
1164
1655
  let sha;
1165
1656
  try {
1166
- sha = execFileSync7("git", ["rev-parse", "HEAD"], {
1657
+ sha = execFileSync7("git", [...GIT_NO_HOOKS, "rev-parse", "HEAD"], {
1167
1658
  cwd,
1168
1659
  encoding: "utf-8"
1169
1660
  }).trim();
1170
1661
  } catch (err) {
1171
1662
  throw new Error(`renameRemoteBranch: could not resolve HEAD: ${err instanceof Error ? err.message : err}`);
1172
1663
  }
1173
- log.info(TAG13, `Renaming remote ${oldRef} → ${newRef}`);
1174
- execFileSync7("git", ["push", "origin", `${sha}:refs/heads/${newRef}`, "--force-with-lease"], { cwd, stdio: "pipe" });
1664
+ log.info(TAG14, `Renaming remote ${oldRef} → ${newRef}`);
1665
+ execFileSync7("git", [
1666
+ ...GIT_NO_HOOKS,
1667
+ "push",
1668
+ "origin",
1669
+ `${sha}:refs/heads/${newRef}`,
1670
+ "--force-with-lease"
1671
+ ], { cwd, stdio: "pipe" });
1175
1672
  try {
1176
- execFileSync7("git", ["push", "origin", `:refs/heads/${oldRef}`], {
1673
+ execFileSync7("git", [...GIT_NO_HOOKS, "push", "origin", `:refs/heads/${oldRef}`], {
1177
1674
  cwd,
1178
1675
  stdio: "pipe"
1179
1676
  });
1180
1677
  } catch (err) {
1181
- log.warn(TAG13, `renameRemoteBranch: could not delete old ref ${oldRef}: ${err instanceof Error ? err.message : err}`);
1678
+ log.warn(TAG14, `renameRemoteBranch: could not delete old ref ${oldRef}: ${err instanceof Error ? err.message : err}`);
1182
1679
  }
1183
1680
  try {
1184
- execFileSync7("git", ["branch", "-m", oldRef, newRef], {
1681
+ execFileSync7("git", [...GIT_NO_HOOKS, "branch", "-m", oldRef, newRef], {
1185
1682
  cwd,
1186
1683
  stdio: "pipe"
1187
1684
  });
@@ -1189,7 +1686,7 @@ function renameRemoteBranch(oldRef, newRef, cwd) {
1189
1686
  }
1190
1687
  function getBranchWebUrl(branchName, cwd) {
1191
1688
  try {
1192
- const remoteUrl = execFileSync7("git", ["remote", "get-url", "origin"], {
1689
+ const remoteUrl = execFileSync7("git", [...GIT_NO_HOOKS, "remote", "get-url", "origin"], {
1193
1690
  cwd,
1194
1691
  encoding: "utf-8"
1195
1692
  }).trim();
@@ -1237,12 +1734,17 @@ function buildPrBody(card, commitLog) {
1237
1734
  }
1238
1735
  function createPullRequest(card, branchName, worktreePath, config, provider, existingPrUrl) {
1239
1736
  if (existingPrUrl) {
1240
- log.info(TAG13, `Reusing existing PR from card description: ${existingPrUrl}`);
1737
+ log.info(TAG14, `Reusing existing PR from card description: ${existingPrUrl}`);
1241
1738
  return existingPrUrl;
1242
1739
  }
1243
1740
  let commitLog = "";
1244
1741
  try {
1245
- commitLog = execFileSync7("git", ["log", "--oneline", `origin/${config.worktree.baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
1742
+ commitLog = execFileSync7("git", [
1743
+ ...GIT_NO_HOOKS,
1744
+ "log",
1745
+ "--oneline",
1746
+ `origin/${config.worktree.baseBranch}..HEAD`
1747
+ ], { cwd: worktreePath, encoding: "utf-8" }).trim();
1246
1748
  } catch {
1247
1749
  commitLog = "(unable to retrieve commit log)";
1248
1750
  }
@@ -1251,7 +1753,7 @@ function createPullRequest(card, branchName, worktreePath, config, provider, exi
1251
1753
  const base = config.worktree.baseBranch;
1252
1754
  const existingUrl = findExistingPr(branchName, worktreePath, provider);
1253
1755
  if (existingUrl) {
1254
- log.info(TAG13, `PR already exists for ${branchName}, updating body...`);
1756
+ log.info(TAG14, `PR already exists for ${branchName}, updating body...`);
1255
1757
  updateExistingPr(branchName, body, worktreePath, provider);
1256
1758
  return existingUrl;
1257
1759
  }
@@ -1301,13 +1803,13 @@ function createPullRequest(card, branchName, worktreePath, config, provider, exi
1301
1803
  ], { cwd: worktreePath, encoding: "utf-8" }).trim();
1302
1804
  break;
1303
1805
  default:
1304
- log.warn(TAG13, `No PR CLI for provider "${provider}" — branch pushed but no PR created`);
1806
+ log.warn(TAG14, `No PR CLI for provider "${provider}" — branch pushed but no PR created`);
1305
1807
  return null;
1306
1808
  }
1307
- log.info(TAG13, `PR created: ${result}`);
1809
+ log.info(TAG14, `PR created: ${result}`);
1308
1810
  return result;
1309
1811
  } catch (err) {
1310
- log.error(TAG13, `Failed to create PR: ${err instanceof Error ? err.message : err}`);
1812
+ log.error(TAG14, `Failed to create PR: ${err instanceof Error ? err.message : err}`);
1311
1813
  return null;
1312
1814
  }
1313
1815
  }
@@ -1341,15 +1843,16 @@ function updateExistingPr(branchName, body, worktreePath, provider) {
1341
1843
  execFileSync7("glab", ["mr", "update", branchName, "--description", body], { cwd: worktreePath, stdio: "pipe" });
1342
1844
  break;
1343
1845
  }
1344
- log.info(TAG13, `Updated existing PR body for ${branchName}`);
1846
+ log.info(TAG14, `Updated existing PR body for ${branchName}`);
1345
1847
  } catch (err) {
1346
- log.warn(TAG13, `Failed to update PR body: ${err instanceof Error ? err.message : err}`);
1848
+ log.warn(TAG14, `Failed to update PR body: ${err instanceof Error ? err.message : err}`);
1347
1849
  }
1348
1850
  }
1349
- var cachedExecFileAsync2, TAG13 = "git-pr", VALID_PR_URL_RE, PR_URL_RE, REVIEWED_SHA_RE, CI_REVIEW_REQUESTED_SHA_RE;
1851
+ var cachedExecFileAsync2, TAG14 = "git-pr", VALID_PR_URL_RE, PR_URL_RE, REVIEWED_SHA_RE, CI_REVIEW_REQUESTED_SHA_RE;
1350
1852
  var init_git_pr = __esm(() => {
1351
1853
  init_dist();
1352
1854
  init_log();
1855
+ init_run_containment();
1353
1856
  VALID_PR_URL_RE = /^https:\/\/(github\.com|gitlab\.com|dev\.azure\.com|bitbucket\.org)\//;
1354
1857
  PR_URL_RE = /PR:\s*(https?:\/\/[^\s)]+)/;
1355
1858
  REVIEWED_SHA_RE = /^Reviewed-SHA:\s*([0-9a-f]{7,40})\s*$/im;
@@ -1635,6 +2138,7 @@ class SdkAgentRunner {
1635
2138
  ...this.cfg.settingSources ? { settingSources: this.cfg.settingSources } : {},
1636
2139
  ...this.cfg.mcpServers ? { mcpServers: this.cfg.mcpServers } : {},
1637
2140
  ...this.cfg.strictMcpConfig ? { strictMcpConfig: true } : {},
2141
+ ...this.cfg.sandbox ? { sandbox: this.cfg.sandbox } : {},
1638
2142
  stderr: (data) => {
1639
2143
  this.capturedStderr += data;
1640
2144
  },
@@ -2051,6 +2555,7 @@ class ArtifactCollector {
2051
2555
  }
2052
2556
  // src/ci-failure.ts
2053
2557
  init_log();
2558
+ init_run_containment();
2054
2559
  import { execFile, execFileSync } from "node:child_process";
2055
2560
  import { promisify } from "node:util";
2056
2561
  function createExecFileAsync() {
@@ -2078,7 +2583,7 @@ function isPrOnOrigin(prUrl, cwd) {
2078
2583
  if (!prSlug)
2079
2584
  return false;
2080
2585
  try {
2081
- const remote = execFileSync("git", ["remote", "get-url", "origin"], {
2586
+ const remote = execFileSync("git", [...GIT_NO_HOOKS, "remote", "get-url", "origin"], {
2082
2587
  cwd,
2083
2588
  encoding: "utf-8",
2084
2589
  stdio: ["ignore", "pipe", "pipe"]
@@ -2216,6 +2721,7 @@ init_dist();
2216
2721
 
2217
2722
  // src/exec-types.ts
2218
2723
  var DEFAULT_METRIC_TIMEOUT_MS = 300000;
2724
+ var SANDBOX_STARTUP_GRACE_MS = 60000;
2219
2725
 
2220
2726
  // src/gate-config-error.ts
2221
2727
  init_dist();
@@ -2230,14 +2736,14 @@ var MAX_METRIC_TIMEOUT_MS = 900000;
2230
2736
  var METRIC_SIGINT_GRACE_MS = 2000;
2231
2737
  var METRIC_SIGTERM_GRACE_MS = 3000;
2232
2738
  var STDIO_DRAIN_GRACE_MS = 500;
2233
- function parseParseMode(parse) {
2234
- if (typeof parse !== "string" || parse.length === 0) {
2739
+ function parseParseMode(parse2) {
2740
+ if (typeof parse2 !== "string" || parse2.length === 0) {
2235
2741
  return { kind: "invalid", reason: "`parse` must be a non-empty string" };
2236
2742
  }
2237
- if (parse === "number")
2743
+ if (parse2 === "number")
2238
2744
  return { kind: "number" };
2239
- if (parse.startsWith("json:")) {
2240
- const path = parse.slice("json:".length).trim();
2745
+ if (parse2.startsWith("json:")) {
2746
+ const path = parse2.slice("json:".length).trim();
2241
2747
  if (!path) {
2242
2748
  return { kind: "invalid", reason: '`parse` "json:" is missing a path' };
2243
2749
  }
@@ -2245,7 +2751,7 @@ function parseParseMode(parse) {
2245
2751
  }
2246
2752
  return {
2247
2753
  kind: "invalid",
2248
- reason: `unknown \`parse\` mode "${parse}" (expected "number" or "json:<path>")`
2754
+ reason: `unknown \`parse\` mode "${parse2}" (expected "number" or "json:<path>")`
2249
2755
  };
2250
2756
  }
2251
2757
  function parseMetricValue(mode, stdout) {
@@ -2291,7 +2797,7 @@ function parseMetricValue(mode, stdout) {
2291
2797
  return { ok: true, value: resolved };
2292
2798
  }
2293
2799
  function runMetricCommand(args) {
2294
- return new Promise((resolve, reject) => {
2800
+ return new Promise((resolve2, reject) => {
2295
2801
  let child;
2296
2802
  try {
2297
2803
  child = spawnInGroup(args.command, args.args, {
@@ -2322,7 +2828,7 @@ function runMetricCommand(args) {
2322
2828
  if (failure)
2323
2829
  reject(failure);
2324
2830
  else
2325
- resolve(Buffer.concat(chunks).toString("utf8"));
2831
+ resolve2(Buffer.concat(chunks).toString("utf8"));
2326
2832
  };
2327
2833
  const killTree = (reason) => {
2328
2834
  if (settled || killReason)
@@ -2470,123 +2976,20 @@ function describeRunFailure(err, timeoutMs) {
2470
2976
  function truncate(value, max) {
2471
2977
  return value.length <= max ? value : `${value.slice(0, max)}…[truncated]`;
2472
2978
  }
2473
- // src/confine-to-repo.ts
2474
- import { realpathSync } from "node:fs";
2475
- import { dirname, isAbsolute, parse, resolve, sep } from "node:path";
2476
- var READ_PATH_ARGS = {
2477
- Read: ["file_path", "path", "notebook_path"],
2478
- Grep: ["path"],
2479
- Glob: ["path"]
2480
- };
2481
- var WRITE_PATH_ARGS = {
2482
- Write: ["file_path"],
2483
- Edit: ["file_path"],
2484
- MultiEdit: ["file_path"],
2485
- NotebookEdit: ["notebook_path"]
2486
- };
2487
- var CONFINED_READ_TOOLS = Object.freeze(Object.keys(READ_PATH_ARGS));
2488
- var CONFINED_WRITE_TOOLS = Object.freeze([
2489
- ...Object.keys(READ_PATH_ARGS),
2490
- ...Object.keys(WRITE_PATH_ARGS).filter((t) => t !== "MultiEdit")
2491
- ]);
2492
- var PATTERN_ARG_BY_TOOL = {
2493
- Glob: ["pattern"],
2494
- Grep: ["glob"]
2495
- };
2496
- function isGitMetadata(repoRoot, candidate) {
2497
- const rel = candidate.startsWith(repoRoot) ? candidate.slice(repoRoot.length) : candidate;
2498
- return rel.split(/[\\/]/).some((segment) => segment.toLowerCase() === ".git");
2499
- }
2500
- function patternEscapes(pattern) {
2501
- if (isAbsolute(pattern))
2502
- return true;
2503
- if (/[{}[\]~()!+@]/.test(pattern))
2504
- return true;
2505
- return pattern.split(/[\\/]/).some((segment) => segment === "..");
2506
- }
2507
- function pathArgsFor(mode) {
2508
- return mode === "write" ? { ...READ_PATH_ARGS, ...WRITE_PATH_ARGS } : READ_PATH_ARGS;
2509
- }
2510
- function realPathOrNearest(p) {
2511
- const abs = isAbsolute(p) ? p : resolve(p);
2512
- const { root } = parse(abs);
2513
- let real = root;
2514
- for (const part of abs.slice(root.length).split(sep)) {
2515
- if (part === "" || part === ".")
2516
- continue;
2517
- if (part === "..") {
2518
- real = dirname(real);
2519
- continue;
2520
- }
2521
- const next = real.endsWith(sep) ? real + part : real + sep + part;
2522
- try {
2523
- real = realpathSync(next);
2524
- } catch {
2525
- real = next;
2526
- }
2527
- }
2528
- return real;
2529
- }
2530
- function isInsideTree(root, target) {
2531
- const normalizedRoot = realPathOrNearest(root);
2532
- const normalizedTarget = realPathOrNearest(target);
2533
- if (normalizedTarget === normalizedRoot)
2534
- return true;
2535
- return normalizedTarget.startsWith(normalizedRoot + sep);
2536
- }
2537
- function decideConfinedTool(repoRoot, toolName, input, mode = "read") {
2538
- const pathArgs = pathArgsFor(mode)[toolName];
2539
- if (!pathArgs) {
2540
- return {
2541
- behavior: "deny",
2542
- message: `${toolName} is not available to this run.`
2543
- };
2544
- }
2545
- const verb = toolName in WRITE_PATH_ARGS ? "write" : "read";
2546
- for (const key of PATTERN_ARG_BY_TOOL[toolName] ?? []) {
2547
- const value = input[key];
2548
- if (typeof value !== "string" || value.length === 0)
2549
- continue;
2550
- if (patternEscapes(value)) {
2551
- return {
2552
- behavior: "deny",
2553
- message: `${toolName} patterns must stay inside the repository — no absolute path and no "..". Refused: ${value}`
2554
- };
2555
- }
2556
- }
2557
- for (const key of pathArgs) {
2558
- const value = input[key];
2559
- if (typeof value !== "string" || value.length === 0)
2560
- continue;
2561
- const candidate = isAbsolute(value) ? value : `${repoRoot}${sep}${value}`;
2562
- if (isGitMetadata(repoRoot, candidate)) {
2563
- return {
2564
- behavior: "deny",
2565
- message: `${toolName} may not touch the repository's git metadata. Refused: ${value}`
2566
- };
2567
- }
2568
- if (!isInsideTree(repoRoot, candidate)) {
2569
- return {
2570
- behavior: "deny",
2571
- message: `${toolName} may only ${verb} inside the repository. Refused: ${value}`
2572
- };
2573
- }
2574
- }
2575
- return { behavior: "allow" };
2576
- }
2577
- function confineToRepo(repoRoot, mode = "read") {
2578
- return async (toolName, input) => decideConfinedTool(repoRoot, toolName, input, mode);
2579
- }
2979
+
2980
+ // src/index.ts
2981
+ init_confine_to_repo();
2982
+
2580
2983
  // src/gate-collectors.ts
2581
2984
  init_dist();
2582
2985
  init_log();
2583
2986
 
2584
2987
  // src/oracle-collector.ts
2585
- import { createHash } from "node:crypto";
2988
+ import { createHash as createHash2 } from "node:crypto";
2586
2989
  init_log();
2587
2990
 
2588
2991
  // src/oracle.ts
2589
- import { lstatSync, readFileSync, statSync } from "node:fs";
2992
+ import { lstatSync, readFileSync as readFileSync2, statSync } from "node:fs";
2590
2993
  import {
2591
2994
  chmod,
2592
2995
  lstat,
@@ -2596,13 +2999,13 @@ import {
2596
2999
  rm,
2597
3000
  writeFile
2598
3001
  } from "node:fs/promises";
2599
- import { tmpdir } from "node:os";
2600
- import { dirname as dirname2, isAbsolute as isAbsolute2, join, resolve as resolve2, sep as sep2 } from "node:path";
3002
+ import { tmpdir as tmpdir2 } from "node:os";
3003
+ import { dirname as dirname3, isAbsolute as isAbsolute3, join as join2, resolve as resolve2, sep as sep2 } from "node:path";
2601
3004
  import { StringDecoder } from "node:string_decoder";
2602
3005
  init_log();
2603
3006
  var TAG5 = "oracle";
2604
3007
  async function resolveContained(repoPath, relativePath) {
2605
- if (isAbsolute2(relativePath)) {
3008
+ if (isAbsolute3(relativePath)) {
2606
3009
  throw new Error(`refusing to place an oracle at an absolute path: ${relativePath}`);
2607
3010
  }
2608
3011
  if (relativePath === "" || relativePath === ".") {
@@ -2625,7 +3028,7 @@ async function resolveContained(repoPath, relativePath) {
2625
3028
  }
2626
3029
  async function place(repoPath, oracle) {
2627
3030
  const target = await resolveContained(repoPath, oracle.path);
2628
- await mkdir(dirname2(target), { recursive: true });
3031
+ await mkdir(dirname3(target), { recursive: true });
2629
3032
  await writeFile(target, oracle.content, "utf8");
2630
3033
  }
2631
3034
  async function remove(repoPath, oracle) {
@@ -2733,8 +3136,8 @@ function argvPath(path) {
2733
3136
  }
2734
3137
  async function runHeldOracle(repoPath, oracle, timeoutMs = DEFAULT_METRIC_TIMEOUT_MS) {
2735
3138
  const spec = resolveOracleRunnerSpec(oracle);
2736
- const reportDir = await mkdtemp(join(tmpdir(), reportDirPrefix()));
2737
- const reportPath = join(reportDir, spec.report.file);
3139
+ const reportDir = await mkdtemp(join2(tmpdir2(), reportDirPrefix()));
3140
+ const reportPath = join2(reportDir, spec.report.file);
2738
3141
  try {
2739
3142
  return await spawnHeldOracle(spec, repoPath, oracle, reportPath, timeoutMs);
2740
3143
  } finally {
@@ -2745,7 +3148,7 @@ function reportDirPrefix() {
2745
3148
  return "harmony-oracle-report-";
2746
3149
  }
2747
3150
  function assertUntampered(reportPath) {
2748
- const dir = statSync(dirname2(reportPath));
3151
+ const dir = statSync(dirname3(reportPath));
2749
3152
  if ((dir.mode & 511) !== 448) {
2750
3153
  throw new Error(`the oracle report directory's mode changed to ${(dir.mode & 511).toString(8)} — refusing the report`);
2751
3154
  }
@@ -2808,7 +3211,7 @@ ${output}` : output;
2808
3211
  const captureReport = () => {
2809
3212
  try {
2810
3213
  assertUntampered(reportPath);
2811
- report = spec.report.parse(readFileSync(reportPath, "utf8"));
3214
+ report = spec.report.parse(readFileSync2(reportPath, "utf8"));
2812
3215
  } catch (err) {
2813
3216
  report = null;
2814
3217
  const message = err instanceof Error ? err.message : String(err);
@@ -2926,7 +3329,7 @@ class HeldOracleCollector {
2926
3329
  async runHeld(oracle) {
2927
3330
  const identity = {
2928
3331
  oracleId: oracle.id ?? null,
2929
- contentHash: createHash("sha256").update(oracle.content).digest("hex")
3332
+ contentHash: createHash2("sha256").update(oracle.content).digest("hex")
2930
3333
  };
2931
3334
  await this.deps.place(this.deps.repoPath, oracle);
2932
3335
  try {
@@ -3037,11 +3440,12 @@ function errText(err) {
3037
3440
  }
3038
3441
 
3039
3442
  // src/verification.ts
3040
- init_log();
3041
3443
  import { execFileSync as execFileSync5, spawn as spawn2 } from "node:child_process";
3444
+ init_log();
3042
3445
 
3043
3446
  // src/pm.ts
3044
3447
  init_log();
3448
+ init_run_containment();
3045
3449
  import { execFileSync as execFileSync2 } from "node:child_process";
3046
3450
  import { existsSync } from "node:fs";
3047
3451
  var TAG7 = "pm";
@@ -3051,7 +3455,7 @@ function detectPackageManager() {
3051
3455
  return cached;
3052
3456
  let repoRoot;
3053
3457
  try {
3054
- repoRoot = execFileSync2("git", ["rev-parse", "--show-toplevel"], {
3458
+ repoRoot = execFileSync2("git", [...GIT_NO_HOOKS, "rev-parse", "--show-toplevel"], {
3055
3459
  encoding: "utf-8"
3056
3460
  }).trim();
3057
3461
  } catch {
@@ -3094,7 +3498,7 @@ function spawnRunArgs(script, ...extra) {
3094
3498
  // src/project-type.ts
3095
3499
  init_log();
3096
3500
  import { execFileSync as execFileSync3 } from "node:child_process";
3097
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "node:fs";
3501
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "node:fs";
3098
3502
  var TAG8 = "project-type";
3099
3503
  var _cache = new Map;
3100
3504
  function _resetCache() {
@@ -3197,7 +3601,7 @@ var NPM_PLACEHOLDER_TEST = /no test specified/i;
3197
3601
  function hasNodeTestScript(dir) {
3198
3602
  let script;
3199
3603
  try {
3200
- const pkg = JSON.parse(readFileSync2(`${dir}/package.json`, "utf-8"));
3604
+ const pkg = JSON.parse(readFileSync3(`${dir}/package.json`, "utf-8"));
3201
3605
  script = pkg.scripts?.test;
3202
3606
  } catch (err) {
3203
3607
  log.warn(TAG8, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
@@ -3214,7 +3618,7 @@ function hasNodeTestScript(dir) {
3214
3618
  function firstNodeScript(dir, candidates) {
3215
3619
  let scripts;
3216
3620
  try {
3217
- const pkg = JSON.parse(readFileSync2(`${dir}/package.json`, "utf-8"));
3621
+ const pkg = JSON.parse(readFileSync3(`${dir}/package.json`, "utf-8"));
3218
3622
  scripts = pkg.scripts ?? {};
3219
3623
  } catch (err) {
3220
3624
  log.warn(TAG8, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
@@ -3269,10 +3673,108 @@ function resolveXcodeScheme(pt) {
3269
3673
  }
3270
3674
  }
3271
3675
 
3676
+ // src/repair-sandbox.ts
3677
+ init_log();
3678
+ import { randomUUID } from "node:crypto";
3679
+ import { promisify as promisify2 } from "node:util";
3680
+ var TAG9 = "repair-sandbox";
3681
+ async function dockerExec(argv, opts) {
3682
+ const { execFile: execFile2 } = await import("node:child_process");
3683
+ return promisify2(execFile2)("docker", argv, {
3684
+ encoding: "utf-8",
3685
+ ...opts
3686
+ });
3687
+ }
3688
+ var MAX_OUTPUT_BUFFER2 = 20971520;
3689
+ var PROBE_TIMEOUT_MS = 1e4;
3690
+ var SANDBOX_MOUNT = "/repo";
3691
+ var SANDBOX_MEMORY = "4g";
3692
+ var SANDBOX_PIDS = "512";
3693
+ var dockerProbe = null;
3694
+ async function sandboxAvailable() {
3695
+ if (dockerProbe === true)
3696
+ return true;
3697
+ try {
3698
+ await dockerExec(["version", "--format", "{{.Server.Version}}"], {
3699
+ timeout: PROBE_TIMEOUT_MS
3700
+ });
3701
+ dockerProbe = true;
3702
+ } catch {
3703
+ dockerProbe = false;
3704
+ }
3705
+ return dockerProbe;
3706
+ }
3707
+ function __resetSandboxProbe() {
3708
+ dockerProbe = null;
3709
+ }
3710
+ async function removeContainer(name) {
3711
+ try {
3712
+ await dockerExec(["rm", "--force", name], { timeout: PROBE_TIMEOUT_MS });
3713
+ log.warn(TAG9, `removed the timed-out sandbox container ${name}`);
3714
+ } catch {}
3715
+ }
3716
+ function sandboxRunArgs(image, worktree, command, name) {
3717
+ return [
3718
+ "run",
3719
+ "--rm",
3720
+ ...name ? ["--name", name] : [],
3721
+ "--network=none",
3722
+ "--cap-drop=ALL",
3723
+ "--security-opt=no-new-privileges",
3724
+ `--memory=${SANDBOX_MEMORY}`,
3725
+ `--pids-limit=${SANDBOX_PIDS}`,
3726
+ ...typeof process.getuid === "function" && typeof process.getgid === "function" ? ["--user", `${process.getuid()}:${process.getgid()}`] : [],
3727
+ "--env",
3728
+ "HOME=/tmp",
3729
+ "--volume",
3730
+ `${worktree}:${SANDBOX_MOUNT}`,
3731
+ "--workdir",
3732
+ SANDBOX_MOUNT,
3733
+ "--entrypoint",
3734
+ command.cmd,
3735
+ image,
3736
+ ...command.args
3737
+ ];
3738
+ }
3739
+ async function runInSandbox(args) {
3740
+ const name = `harmony-repair-${randomUUID()}`;
3741
+ const argv = sandboxRunArgs(args.image, args.worktree, args.command, name);
3742
+ log.info(TAG9, `sandbox: ${args.command.cmd} ${args.command.args.join(" ")} (image ${args.image})`);
3743
+ try {
3744
+ const { stdout } = await dockerExec(argv, {
3745
+ timeout: args.timeoutMs,
3746
+ maxBuffer: MAX_OUTPUT_BUFFER2
3747
+ });
3748
+ return { passed: true, output: stdout ?? "" };
3749
+ } catch (err) {
3750
+ const e = err;
3751
+ const output = `${e.stdout ?? ""}${e.stderr ?? ""}`;
3752
+ if (typeof e.code !== "number") {
3753
+ const timedOut = e.killed === true || e.signal != null;
3754
+ if (timedOut)
3755
+ await removeContainer(name);
3756
+ return {
3757
+ passed: false,
3758
+ output,
3759
+ sandboxError: timedOut ? `the sandbox timed out after ${args.timeoutMs}ms` : `the sandbox did not run: ${e.message ?? "unknown error"}`
3760
+ };
3761
+ }
3762
+ if (e.code === 125) {
3763
+ return {
3764
+ passed: false,
3765
+ output,
3766
+ sandboxError: `the sandbox could not start (image "${args.image}" missing or unusable)`
3767
+ };
3768
+ }
3769
+ return { passed: false, output };
3770
+ }
3771
+ }
3772
+
3272
3773
  // src/revert-guard.ts
3273
3774
  init_log();
3775
+ init_run_containment();
3274
3776
  import { execFileSync as execFileSync4 } from "node:child_process";
3275
- var TAG9 = "revert-guard";
3777
+ var TAG10 = "revert-guard";
3276
3778
  var TEST_FILE = /(?:^|\/)__tests__\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
3277
3779
  function isTestFile(path) {
3278
3780
  return TEST_FILE.test(path);
@@ -3282,21 +3784,27 @@ function filterTestFiles(paths) {
3282
3784
  }
3283
3785
  function refetchBase(worktreePath, baseBranch) {
3284
3786
  try {
3285
- execFileSync4("git", ["fetch", "origin", baseBranch], {
3787
+ execFileSync4("git", [...GIT_NO_HOOKS, "fetch", "origin", baseBranch], {
3286
3788
  cwd: worktreePath,
3287
3789
  stdio: "pipe"
3288
3790
  });
3289
3791
  } catch {
3290
- log.warn(TAG9, "Failed to re-fetch base for revert guard — using last fetch");
3792
+ log.warn(TAG10, "Failed to re-fetch base for revert guard — using last fetch");
3291
3793
  }
3292
3794
  }
3293
3795
  function listDeletedFilesAgainstBase(worktreePath, baseBranch) {
3294
3796
  try {
3295
- const out = execFileSync4("git", ["diff", "--diff-filter=D", "--name-only", `origin/${baseBranch}...HEAD`], { cwd: worktreePath, encoding: "utf-8" });
3797
+ const out = execFileSync4("git", [
3798
+ ...GIT_NO_HOOKS,
3799
+ "diff",
3800
+ "--diff-filter=D",
3801
+ "--name-only",
3802
+ `origin/${baseBranch}...HEAD`
3803
+ ], { cwd: worktreePath, encoding: "utf-8" });
3296
3804
  return out.split(`
3297
3805
  `).map((l) => l.trim()).filter((l) => l.length > 0);
3298
3806
  } catch (err) {
3299
- log.warn(TAG9, `Failed to list deleted files: ${err instanceof Error ? err.message : err}`);
3807
+ log.warn(TAG10, `Failed to list deleted files: ${err instanceof Error ? err.message : err}`);
3300
3808
  return [];
3301
3809
  }
3302
3810
  }
@@ -3306,8 +3814,9 @@ function findDeletedTestFiles(worktreePath, baseBranch) {
3306
3814
  }
3307
3815
 
3308
3816
  // src/verification.ts
3309
- var TAG10 = "verification";
3310
- var MAX_OUTPUT_BUFFER2 = 64 * 1024 * 1024;
3817
+ init_run_containment();
3818
+ var TAG11 = "verification";
3819
+ var MAX_OUTPUT_BUFFER3 = 64 * 1024 * 1024;
3311
3820
  async function runVerification(worktreePath, config, workerId) {
3312
3821
  const result = {
3313
3822
  passed: true,
@@ -3317,133 +3826,163 @@ async function runVerification(worktreePath, config, workerId) {
3317
3826
  reviewFindings: [],
3318
3827
  revertWarnings: []
3319
3828
  };
3829
+ const sandbox = verificationSandbox(config);
3320
3830
  if (config.verification.revertGuard) {
3321
- log.info(TAG10, `[worker:${workerId}] Checking for reverted merged work...`);
3831
+ log.info(TAG11, `[worker:${workerId}] Checking for reverted merged work...`);
3322
3832
  const deletedTests = findDeletedTestFiles(worktreePath, config.worktree.baseBranch);
3323
3833
  if (deletedTests.length > 0) {
3324
3834
  result.revertWarnings = deletedTests.map((f) => `Branch deletes test file '${f}' relative to current ${config.worktree.baseBranch} — ` + "likely an accidental revert of already-merged work. Restore the test or rebase on current main.");
3325
- log.warn(TAG10, `[worker:${workerId}] Revert guard tripped: ${deletedTests.length} deleted test file(s)`);
3835
+ log.warn(TAG11, `[worker:${workerId}] Revert guard tripped: ${deletedTests.length} deleted test file(s)`);
3326
3836
  result.passed = false;
3327
3837
  } else {
3328
- log.info(TAG10, `[worker:${workerId}] Revert guard passed`);
3838
+ log.info(TAG11, `[worker:${workerId}] Revert guard passed`);
3329
3839
  }
3330
3840
  }
3331
3841
  if (config.verification.build) {
3332
- log.info(TAG10, `[worker:${workerId}] Running build...`);
3333
- result.buildErrors = runBuild(worktreePath, config.verification.timeout);
3842
+ log.info(TAG11, `[worker:${workerId}] Running build...`);
3843
+ result.buildErrors = await runBuild(worktreePath, config.verification.timeout, sandbox);
3334
3844
  if (result.buildErrors.length > 0) {
3335
- log.warn(TAG10, `[worker:${workerId}] Build failed with ${result.buildErrors.length} error(s)`);
3845
+ log.warn(TAG11, `[worker:${workerId}] Build failed with ${result.buildErrors.length} error(s)`);
3336
3846
  result.passed = false;
3337
3847
  } else {
3338
- log.info(TAG10, `[worker:${workerId}] Build passed`);
3848
+ log.info(TAG11, `[worker:${workerId}] Build passed`);
3339
3849
  }
3340
3850
  }
3341
3851
  if (config.verification.test && result.buildErrors.length === 0) {
3342
- log.info(TAG10, `[worker:${workerId}] Running tests...`);
3343
- result.testFailures = runTests(worktreePath, config.verification.testTimeout);
3852
+ log.info(TAG11, `[worker:${workerId}] Running tests...`);
3853
+ result.testFailures = await runTests(worktreePath, config.verification.testTimeout, sandbox);
3344
3854
  if (result.testFailures.length > 0) {
3345
- log.warn(TAG10, `[worker:${workerId}] Tests failed with ${result.testFailures.length} failure(s)`);
3855
+ log.warn(TAG11, `[worker:${workerId}] Tests failed with ${result.testFailures.length} failure(s)`);
3346
3856
  result.passed = false;
3347
3857
  } else {
3348
- log.info(TAG10, `[worker:${workerId}] Tests passed`);
3858
+ log.info(TAG11, `[worker:${workerId}] Tests passed`);
3349
3859
  }
3350
3860
  }
3351
3861
  if (config.verification.lint) {
3352
- log.info(TAG10, `[worker:${workerId}] Running lint...`);
3353
- result.lintWarnings = runLint(worktreePath, config.verification.timeout);
3862
+ log.info(TAG11, `[worker:${workerId}] Running lint...`);
3863
+ result.lintWarnings = await runLint(worktreePath, config.verification.timeout, sandbox);
3354
3864
  if (result.lintWarnings.length > 0) {
3355
- log.warn(TAG10, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
3865
+ log.warn(TAG11, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
3356
3866
  } else {
3357
- log.info(TAG10, `[worker:${workerId}] Lint passed`);
3867
+ log.info(TAG11, `[worker:${workerId}] Lint passed`);
3358
3868
  }
3359
3869
  }
3360
3870
  if (config.verification.deepReview) {
3361
- log.info(TAG10, `[worker:${workerId}] Running deep review...`);
3871
+ log.info(TAG11, `[worker:${workerId}] Running deep review...`);
3362
3872
  result.reviewFindings = await runDeepReview(worktreePath, config, workerId);
3363
3873
  if (result.reviewFindings.length > 0) {
3364
- log.warn(TAG10, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
3874
+ log.warn(TAG11, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
3365
3875
  } else {
3366
- log.info(TAG10, `[worker:${workerId}] Deep review passed`);
3876
+ log.info(TAG11, `[worker:${workerId}] Deep review passed`);
3367
3877
  }
3368
3878
  }
3369
3879
  return result;
3370
3880
  }
3371
- function runBuild(worktreePath, timeout) {
3372
- const command = buildCommand(worktreePath);
3373
- if (!command) {
3374
- log.warn(TAG10, `No known build toolchain for ${worktreePath} — skipping build`);
3375
- return [];
3881
+ function verificationSandbox(config) {
3882
+ const image = config.verification.sandboxImage?.trim();
3883
+ if (!image)
3884
+ return;
3885
+ return { image };
3886
+ }
3887
+ async function execStep(command, args) {
3888
+ const { worktreePath, timeout } = args;
3889
+ const sandbox = args.sandbox?.image.trim() ? args.sandbox : undefined;
3890
+ if (sandbox) {
3891
+ if (!await sandboxAvailable()) {
3892
+ return {
3893
+ ok: false,
3894
+ sandboxError: `verification.sandboxImage is set to "${sandbox.image}" but no container runtime answered — ` + "start Docker or unset the image to verify on the host"
3895
+ };
3896
+ }
3897
+ const result = await runInSandbox({
3898
+ image: sandbox.image,
3899
+ worktree: worktreePath,
3900
+ command,
3901
+ timeoutMs: timeout + SANDBOX_STARTUP_GRACE_MS
3902
+ });
3903
+ if (result.sandboxError) {
3904
+ return { ok: false, sandboxError: result.sandboxError };
3905
+ }
3906
+ if (result.passed)
3907
+ return { ok: true };
3908
+ return { ok: false, err: { stdout: result.output, stderr: "" } };
3376
3909
  }
3377
3910
  try {
3378
3911
  execFileSync5(command.cmd, command.args, {
3379
3912
  cwd: worktreePath,
3380
3913
  timeout,
3381
3914
  stdio: "pipe",
3382
- maxBuffer: MAX_OUTPUT_BUFFER2
3915
+ maxBuffer: MAX_OUTPUT_BUFFER3,
3916
+ env: containedEnv()
3383
3917
  });
3384
- return [];
3918
+ return { ok: true };
3385
3919
  } catch (err) {
3386
- return parseErrorOutput(err);
3920
+ return { ok: false, err };
3387
3921
  }
3388
3922
  }
3389
- function runTests(worktreePath, timeout) {
3923
+ async function runBuild(worktreePath, timeout, sandbox) {
3924
+ const command = buildCommand(worktreePath);
3925
+ if (!command) {
3926
+ log.warn(TAG11, `No known build toolchain for ${worktreePath} — skipping build`);
3927
+ return [];
3928
+ }
3929
+ const outcome = await execStep(command, { worktreePath, timeout, sandbox });
3930
+ if (outcome.ok)
3931
+ return [];
3932
+ if (outcome.sandboxError) {
3933
+ log.error(TAG11, `Build not verified: ${outcome.sandboxError}`);
3934
+ return [`Build did not run: ${outcome.sandboxError}`];
3935
+ }
3936
+ return parseErrorOutput(outcome.err);
3937
+ }
3938
+ async function runTests(worktreePath, timeout, sandbox) {
3390
3939
  const command = testCommand(worktreePath);
3391
3940
  if (!command) {
3392
- log.warn(TAG10, `No test command for detected toolchain in ${worktreePath} — skipping tests`);
3941
+ log.warn(TAG11, `No test command for detected toolchain in ${worktreePath} — skipping tests`);
3393
3942
  return [];
3394
3943
  }
3395
- try {
3396
- execFileSync5(command.cmd, command.args, {
3397
- cwd: worktreePath,
3398
- timeout,
3399
- stdio: "pipe",
3400
- maxBuffer: MAX_OUTPUT_BUFFER2
3401
- });
3944
+ const outcome = await execStep(command, { worktreePath, timeout, sandbox });
3945
+ if (outcome.ok)
3402
3946
  return [];
3403
- } catch (err) {
3404
- const output = combineOutput(err);
3405
- log.warn(TAG10, `Test run failed:
3406
- ${output.slice(-4000) || "(no output captured)"}`);
3407
- return parseTestFailures(err, timeout);
3947
+ if (outcome.sandboxError) {
3948
+ log.error(TAG11, `Tests not verified: ${outcome.sandboxError}`);
3949
+ return [`Test run did not happen: ${outcome.sandboxError}`];
3408
3950
  }
3951
+ const output = combineOutput(outcome.err);
3952
+ log.warn(TAG11, `Test run failed:
3953
+ ${output.slice(-4000) || "(no output captured)"}`);
3954
+ return parseTestFailures(outcome.err, timeout);
3409
3955
  }
3410
- function runFormatFix(worktreePath, timeout, workerId) {
3956
+ async function runFormatFix(worktreePath, timeout, workerId, sandbox) {
3411
3957
  const command = formatFixCommand(worktreePath);
3412
3958
  if (!command)
3413
3959
  return;
3414
- try {
3415
- execFileSync5(command.cmd, command.args, {
3416
- cwd: worktreePath,
3417
- timeout,
3418
- stdio: "pipe",
3419
- maxBuffer: MAX_OUTPUT_BUFFER2
3420
- });
3421
- log.info(TAG10, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
3422
- } catch (err) {
3423
- log.warn(TAG10, `[worker:${workerId}] Auto-format step exited non-zero (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
3960
+ const outcome = await execStep(command, { worktreePath, timeout, sandbox });
3961
+ if (outcome.ok) {
3962
+ log.info(TAG11, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
3963
+ return;
3424
3964
  }
3965
+ const why = outcome.sandboxError ? outcome.sandboxError : outcome.err instanceof Error ? outcome.err.message : String(outcome.err);
3966
+ log.warn(TAG11, `[worker:${workerId}] Auto-format step did not complete (non-fatal): ${why}`);
3425
3967
  }
3426
- function runLint(worktreePath, timeout) {
3968
+ async function runLint(worktreePath, timeout, sandbox) {
3427
3969
  const command = lintCommand(worktreePath);
3428
3970
  if (!command) {
3429
- log.info(TAG10, `No lint step for detected toolchain in ${worktreePath} — skipping lint`);
3971
+ log.info(TAG11, `No lint step for detected toolchain in ${worktreePath} — skipping lint`);
3430
3972
  return [];
3431
3973
  }
3432
- try {
3433
- execFileSync5(command.cmd, command.args, {
3434
- cwd: worktreePath,
3435
- timeout,
3436
- stdio: "pipe",
3437
- maxBuffer: MAX_OUTPUT_BUFFER2
3438
- });
3974
+ const outcome = await execStep(command, { worktreePath, timeout, sandbox });
3975
+ if (outcome.ok)
3439
3976
  return [];
3440
- } catch (err) {
3441
- return parseErrorOutput(err);
3977
+ if (outcome.sandboxError) {
3978
+ log.error(TAG11, `Lint not verified: ${outcome.sandboxError}`);
3979
+ return [`Lint did not run: ${outcome.sandboxError}`];
3442
3980
  }
3981
+ return parseErrorOutput(outcome.err);
3443
3982
  }
3444
3983
  async function runDeepReview(worktreePath, config, workerId) {
3445
3984
  if (!supportsDevServer(worktreePath)) {
3446
- log.info(TAG10, `[worker:${workerId}] Detected non-web toolchain — skipping deep review`);
3985
+ log.info(TAG11, `[worker:${workerId}] Detected non-web toolchain — skipping deep review`);
3447
3986
  return [];
3448
3987
  }
3449
3988
  const port = config.verification.devServerBasePort + workerId;
@@ -3452,22 +3991,23 @@ async function runDeepReview(worktreePath, config, workerId) {
3452
3991
  const [cmd, args] = spawnRunArgs("dev", "--port", String(port));
3453
3992
  devServer = spawn2(cmd, args, {
3454
3993
  cwd: worktreePath,
3455
- stdio: ["ignore", "pipe", "pipe"]
3994
+ stdio: ["ignore", "pipe", "pipe"],
3995
+ env: containedEnv()
3456
3996
  });
3457
3997
  try {
3458
3998
  await waitForDevServer(devServer, 30000);
3459
3999
  await probeDevServer(port);
3460
4000
  } catch (err) {
3461
- log.error(TAG10, `Dev server did not become ready: ${err instanceof Error ? err.message : err}`);
4001
+ log.error(TAG11, `Dev server did not become ready: ${err instanceof Error ? err.message : err}`);
3462
4002
  return [];
3463
4003
  }
3464
4004
  let diff = "";
3465
4005
  try {
3466
- diff = execFileSync5("git", ["diff", `origin/${config.worktree.baseBranch}..HEAD`], {
4006
+ diff = execFileSync5("git", [...GIT_NO_HOOKS, "diff", `origin/${config.worktree.baseBranch}..HEAD`], {
3467
4007
  cwd: worktreePath,
3468
4008
  encoding: "utf-8",
3469
4009
  timeout: 30000,
3470
- maxBuffer: MAX_OUTPUT_BUFFER2
4010
+ maxBuffer: MAX_OUTPUT_BUFFER3
3471
4011
  });
3472
4012
  } catch {
3473
4013
  diff = "(unable to retrieve diff)";
@@ -3484,14 +4024,16 @@ async function runDeepReview(worktreePath, config, workerId) {
3484
4024
  "```"
3485
4025
  ].join(`
3486
4026
  `);
3487
- const leanSources = config.claude.leanSettingSources;
3488
4027
  const output = execFileSync5("claude", [
3489
4028
  "--print",
3490
4029
  "--model",
3491
4030
  "sonnet",
3492
4031
  "--max-turns",
3493
4032
  "10",
3494
- ...leanSources ? ["--setting-sources", leanSources] : [],
4033
+ ...implementRunContainmentCliArgs({
4034
+ worktree: worktreePath,
4035
+ readOnly: true
4036
+ }),
3495
4037
  "--",
3496
4038
  reviewPrompt
3497
4039
  ], {
@@ -3499,11 +4041,12 @@ async function runDeepReview(worktreePath, config, workerId) {
3499
4041
  encoding: "utf-8",
3500
4042
  timeout: config.verification.timeout,
3501
4043
  stdio: "pipe",
3502
- maxBuffer: MAX_OUTPUT_BUFFER2
4044
+ maxBuffer: MAX_OUTPUT_BUFFER3,
4045
+ env: containedEnv()
3503
4046
  });
3504
4047
  return parseReviewFindings(output);
3505
4048
  } catch (err) {
3506
- log.error(TAG10, `Deep review failed: ${err instanceof Error ? err.message : err}`);
4049
+ log.error(TAG11, `Deep review failed: ${err instanceof Error ? err.message : err}`);
3507
4050
  return [];
3508
4051
  } finally {
3509
4052
  if (devServer && !devServer.killed) {
@@ -3529,7 +4072,6 @@ function attemptAutoFix(worktreePath, config, errors) {
3529
4072
  "```"
3530
4073
  ].join(`
3531
4074
  `);
3532
- const leanSources = config.claude.leanSettingSources;
3533
4075
  const args = [
3534
4076
  "--print",
3535
4077
  "--model",
@@ -3538,16 +4080,17 @@ function attemptAutoFix(worktreePath, config, errors) {
3538
4080
  "50",
3539
4081
  "--allowedTools",
3540
4082
  "Bash,Read,Write,Edit,Glob,Grep",
3541
- ...leanSources ? ["--setting-sources", leanSources] : [],
4083
+ ...implementRunContainmentCliArgs({ worktree: worktreePath }),
3542
4084
  "--",
3543
4085
  fixPrompt
3544
4086
  ];
3545
- log.info(TAG10, "Spawning Claude for auto-fix...");
4087
+ log.info(TAG11, "Spawning Claude for auto-fix...");
3546
4088
  execFileSync5("claude", args, {
3547
4089
  cwd: worktreePath,
3548
4090
  timeout: config.verification.timeout,
3549
4091
  stdio: "pipe",
3550
- maxBuffer: MAX_OUTPUT_BUFFER2
4092
+ maxBuffer: MAX_OUTPUT_BUFFER3,
4093
+ env: containedEnv()
3551
4094
  });
3552
4095
  }
3553
4096
  async function reportFindings(client, cardId, result, recovery) {
@@ -3580,7 +4123,7 @@ async function reportFindings(client, cardId, result, recovery) {
3580
4123
  try {
3581
4124
  await client.createSubtask(cardId, title);
3582
4125
  } catch (err) {
3583
- log.error(TAG10, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
4126
+ log.error(TAG11, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
3584
4127
  }
3585
4128
  }));
3586
4129
  if (overflow > 0) {
@@ -3588,7 +4131,7 @@ async function reportFindings(client, cardId, result, recovery) {
3588
4131
  await client.createSubtask(cardId, `...and ${overflow} more issues`);
3589
4132
  } catch {}
3590
4133
  }
3591
- log.info(TAG10, `Reported ${Math.min(items.length, maxSubtasks)} finding(s) as subtasks on card ${cardId}`);
4134
+ log.info(TAG11, `Reported ${Math.min(items.length, maxSubtasks)} finding(s) as subtasks on card ${cardId}`);
3592
4135
  }
3593
4136
  function combineOutput(err) {
3594
4137
  const stderr = err?.stderr?.toString() ?? "";
@@ -3621,7 +4164,7 @@ function parseTestFailures(err, timeout) {
3621
4164
  }
3622
4165
  if (e?.code === "ENOBUFS") {
3623
4166
  return [
3624
- `Test output exceeded the ${MAX_OUTPUT_BUFFER2 / (1024 * 1024)}MB capture limit and the run was killed — ` + "the suite's real result is unknown. Quieten the reporter or raise the limit."
4167
+ `Test output exceeded the ${MAX_OUTPUT_BUFFER3 / (1024 * 1024)}MB capture limit and the run was killed — ` + "the suite's real result is unknown. Quieten the reporter or raise the limit."
3625
4168
  ];
3626
4169
  }
3627
4170
  const combined = combineOutput(err);
@@ -3712,7 +4255,7 @@ async function probeDevServer(port, timeoutMs = 5000) {
3712
4255
  }
3713
4256
 
3714
4257
  // src/gate-collectors.ts
3715
- var TAG11 = "gate-collectors";
4258
+ var TAG12 = "gate-collectors";
3716
4259
  async function resolveStageGate(client, card) {
3717
4260
  const currentStage = card.current_stage;
3718
4261
  const playbookId = card.playbook_id;
@@ -3730,7 +4273,7 @@ async function resolveStageGate(client, card) {
3730
4273
  return null;
3731
4274
  return { stage: resolution.stage, gate };
3732
4275
  } catch (err) {
3733
- log.warn(TAG11, `resolveStageGate failed for stage "${currentStage}": ${err instanceof Error ? err.message : err}`);
4276
+ log.warn(TAG12, `resolveStageGate failed for stage "${currentStage}": ${err instanceof Error ? err.message : err}`);
3734
4277
  return null;
3735
4278
  }
3736
4279
  }
@@ -3765,8 +4308,8 @@ class BuildGreenCollector {
3765
4308
  async collect(_context) {
3766
4309
  const doBuild = this.deps.runBuild ?? runBuild;
3767
4310
  const doLint = this.deps.runLint ?? runLint;
3768
- const buildErrors = doBuild(this.deps.worktreePath, this.deps.buildTimeout);
3769
- const lintWarnings = doLint(this.deps.worktreePath, this.deps.lintTimeout);
4311
+ const buildErrors = await doBuild(this.deps.worktreePath, this.deps.buildTimeout, this.deps.sandbox);
4312
+ const lintWarnings = await doLint(this.deps.worktreePath, this.deps.lintTimeout, this.deps.sandbox);
3770
4313
  const buildPassed = buildErrors.length === 0;
3771
4314
  const lintPassed = lintWarnings.length === 0;
3772
4315
  const result = buildPassed ? "passed" : "failed";
@@ -3862,7 +4405,7 @@ function buildGateCollectorRegistry(deps) {
3862
4405
  async function collectGateEvidence(registry, context) {
3863
4406
  const collector = registry[context.gate.kind];
3864
4407
  if (!collector) {
3865
- log.info(TAG11, `No collector for gate kind "${context.gate.kind}" — reporting blocked`);
4408
+ log.info(TAG12, `No collector for gate kind "${context.gate.kind}" — reporting blocked`);
3866
4409
  return {
3867
4410
  result: "blocked",
3868
4411
  structured: {
@@ -3874,14 +4417,15 @@ async function collectGateEvidence(registry, context) {
3874
4417
  return await collector.collect(context);
3875
4418
  } catch (err) {
3876
4419
  const msg = err instanceof Error ? err.message : String(err);
3877
- log.warn(TAG11, `Collector for "${context.gate.kind}" threw: ${msg} — reporting blocked`);
4420
+ log.warn(TAG12, `Collector for "${context.gate.kind}" threw: ${msg} — reporting blocked`);
3878
4421
  return { result: "blocked", structured: { error: msg } };
3879
4422
  }
3880
4423
  }
3881
4424
  // src/git-diff-stat.ts
3882
4425
  init_log();
4426
+ init_run_containment();
3883
4427
  import { execFileSync as execFileSync6 } from "node:child_process";
3884
- var TAG12 = "git-diff-stat";
4428
+ var TAG13 = "git-diff-stat";
3885
4429
  var MAX_CHANGED_FILES = 30;
3886
4430
  function parseNumstat(raw, maxFiles = MAX_CHANGED_FILES) {
3887
4431
  const files = [];
@@ -3960,10 +4504,10 @@ function formatDiffSummary(diff, maxFiles = 100) {
3960
4504
  }
3961
4505
  function captureDiffStat(worktreePath, baseBranch, maxFiles = MAX_CHANGED_FILES) {
3962
4506
  try {
3963
- const raw = execFileSync6("git", ["diff", "--numstat", `${baseBranch}...HEAD`], { cwd: worktreePath, encoding: "utf-8", timeout: 30000 });
4507
+ const raw = execFileSync6("git", [...GIT_NO_HOOKS, "diff", "--numstat", `${baseBranch}...HEAD`], { cwd: worktreePath, encoding: "utf-8", timeout: 30000 });
3964
4508
  return parseNumstat(raw, maxFiles);
3965
4509
  } catch (err) {
3966
- log.warn(TAG12, "git diff --numstat failed", {
4510
+ log.warn(TAG13, "git diff --numstat failed", {
3967
4511
  event: "diff_stat_failed",
3968
4512
  error: err instanceof Error ? err.message : String(err)
3969
4513
  });
@@ -3976,7 +4520,7 @@ init_git_pr();
3976
4520
 
3977
4521
  // src/harmony-client.ts
3978
4522
  init_log();
3979
- var TAG14 = "harmony-client";
4523
+ var TAG15 = "harmony-client";
3980
4524
  function readClientConfig(env) {
3981
4525
  const apiUrl = env.HARMONY_API_URL?.trim();
3982
4526
  const apiKey = env.HARMONY_API_KEY?.trim();
@@ -4028,7 +4572,7 @@ class HarmonyClient {
4028
4572
  purpose: "gate_evaluation"
4029
4573
  });
4030
4574
  if (!response.ok) {
4031
- log.warn(TAG14, `Oracle fetch for stage ${stageId} returned ${response.status} — no oracle read, the gate will report blocked`);
4575
+ log.warn(TAG15, `Oracle fetch for stage ${stageId} returned ${response.status} — no oracle read, the gate will report blocked`);
4032
4576
  return null;
4033
4577
  }
4034
4578
  const body = await response.json();
@@ -4108,150 +4652,14 @@ function relayAgentEvent(draft) {
4108
4652
  }
4109
4653
  return { type: "agent_event", event: draft };
4110
4654
  }
4111
- // src/repair-sandbox.ts
4112
- init_log();
4113
- import { randomUUID } from "node:crypto";
4114
- import { promisify as promisify3 } from "node:util";
4115
- var TAG15 = "repair-sandbox";
4116
- async function dockerExec(argv, opts) {
4117
- const { execFile: execFile3 } = await import("node:child_process");
4118
- return promisify3(execFile3)("docker", argv, {
4119
- encoding: "utf-8",
4120
- ...opts
4121
- });
4122
- }
4123
- var MAX_OUTPUT_BUFFER3 = 20971520;
4124
- var PROBE_TIMEOUT_MS = 1e4;
4125
- var SANDBOX_MOUNT = "/repo";
4126
- var SANDBOX_MEMORY = "4g";
4127
- var SANDBOX_PIDS = "512";
4128
- var dockerProbe = null;
4129
- async function sandboxAvailable() {
4130
- if (dockerProbe === true)
4131
- return true;
4132
- try {
4133
- await dockerExec(["version", "--format", "{{.Server.Version}}"], {
4134
- timeout: PROBE_TIMEOUT_MS
4135
- });
4136
- dockerProbe = true;
4137
- } catch {
4138
- dockerProbe = false;
4139
- }
4140
- return dockerProbe;
4141
- }
4142
- function __resetSandboxProbe() {
4143
- dockerProbe = null;
4144
- }
4145
- async function removeContainer(name) {
4146
- try {
4147
- await dockerExec(["rm", "--force", name], { timeout: PROBE_TIMEOUT_MS });
4148
- log.warn(TAG15, `removed the timed-out sandbox container ${name}`);
4149
- } catch {}
4150
- }
4151
- function sandboxRunArgs(image, worktree, command, name) {
4152
- return [
4153
- "run",
4154
- "--rm",
4155
- ...name ? ["--name", name] : [],
4156
- "--network=none",
4157
- "--cap-drop=ALL",
4158
- "--security-opt=no-new-privileges",
4159
- `--memory=${SANDBOX_MEMORY}`,
4160
- `--pids-limit=${SANDBOX_PIDS}`,
4161
- ...typeof process.getuid === "function" && typeof process.getgid === "function" ? ["--user", `${process.getuid()}:${process.getgid()}`] : [],
4162
- "--env",
4163
- "HOME=/tmp",
4164
- "--volume",
4165
- `${worktree}:${SANDBOX_MOUNT}`,
4166
- "--workdir",
4167
- SANDBOX_MOUNT,
4168
- "--entrypoint",
4169
- command.cmd,
4170
- image,
4171
- ...command.args
4172
- ];
4173
- }
4174
- async function runInSandbox(args) {
4175
- const name = `harmony-repair-${randomUUID()}`;
4176
- const argv = sandboxRunArgs(args.image, args.worktree, args.command, name);
4177
- log.info(TAG15, `sandbox: ${args.command.cmd} ${args.command.args.join(" ")} (image ${args.image})`);
4178
- try {
4179
- const { stdout } = await dockerExec(argv, {
4180
- timeout: args.timeoutMs,
4181
- maxBuffer: MAX_OUTPUT_BUFFER3
4182
- });
4183
- return { passed: true, output: stdout ?? "" };
4184
- } catch (err) {
4185
- const e = err;
4186
- const output = `${e.stdout ?? ""}${e.stderr ?? ""}`;
4187
- if (typeof e.code !== "number") {
4188
- const timedOut = e.killed === true || e.signal != null;
4189
- if (timedOut)
4190
- await removeContainer(name);
4191
- return {
4192
- passed: false,
4193
- output,
4194
- sandboxError: timedOut ? `the sandbox timed out after ${args.timeoutMs}ms` : `the sandbox did not run: ${e.message ?? "unknown error"}`
4195
- };
4196
- }
4197
- if (e.code === 125) {
4198
- return {
4199
- passed: false,
4200
- output,
4201
- sandboxError: `the sandbox could not start (image "${args.image}" missing or unusable)`
4202
- };
4203
- }
4204
- return { passed: false, output };
4205
- }
4206
- }
4207
- // src/run-sizing.ts
4208
- init_dist();
4209
4655
 
4210
- // src/runner.ts
4211
- init_dist();
4212
- import { getConfigDir } from "@gethmy/mcp/src/config.js";
4213
- var HARMONY_CREDENTIAL_KEYS = [
4214
- "HARMONY_API_KEY",
4215
- "HARMONY_API_URL",
4216
- "HARMONY_WORKSPACE_ID",
4217
- "SUPABASE_ANON_KEY",
4218
- "SUPABASE_SERVICE_ROLE_KEY",
4219
- "SUPABASE_URL"
4220
- ];
4221
- function mayHoldCredentials(role) {
4222
- return role === "author" || role === "reviewer";
4223
- }
4224
- function credentialReadDeny() {
4225
- return `Read(/${getConfigDir()}/**)`;
4226
- }
4227
- function credentialAccessDeny() {
4228
- const dir = `/${getConfigDir()}/**`;
4229
- return [`Read(${dir})`, `Grep(${dir})`, `Glob(${dir})`];
4230
- }
4231
- function buildRoleLaunch(args) {
4232
- const role = normalizeStageRole(args.role);
4233
- const keep = mayHoldCredentials(role);
4234
- const env = {};
4235
- for (const [key, value] of Object.entries(args.parentEnv)) {
4236
- if (value === undefined)
4237
- continue;
4238
- if (!keep && HARMONY_CREDENTIAL_KEYS.includes(key))
4239
- continue;
4240
- env[key] = value;
4241
- }
4242
- return {
4243
- role,
4244
- prompt: args.prompt,
4245
- repoPath: args.repoPath,
4246
- env,
4247
- disallowedTools: keep ? [] : [credentialReadDeny()]
4248
- };
4249
- }
4250
- function envKeysDroppedByLaunch(parentEnv, launch) {
4251
- return Object.keys(parentEnv).filter((key) => parentEnv[key] !== undefined && !Object.hasOwn(launch.env, key));
4252
- }
4656
+ // src/index.ts
4657
+ init_run_containment();
4253
4658
 
4254
4659
  // src/run-sizing.ts
4660
+ init_dist();
4661
+ init_confine_to_repo();
4662
+ init_runner();
4255
4663
  var SIZING_MODEL = "haiku";
4256
4664
  var SIZING_MAX_TURNS = 25;
4257
4665
  var SIZING_MAX_BUDGET_USD = 0.75;
@@ -4425,6 +4833,10 @@ async function sizeRun(deps) {
4425
4833
  clearTimeout(timer);
4426
4834
  }
4427
4835
  }
4836
+
4837
+ // src/index.ts
4838
+ init_runner();
4839
+
4428
4840
  // src/stage-run.ts
4429
4841
  async function runStage(request, deps) {
4430
4842
  const events = [];
@@ -4454,6 +4866,7 @@ init_log();
4454
4866
  import { execFileSync as execFileSync8, execSync } from "node:child_process";
4455
4867
  import { existsSync as existsSync3, readdirSync as readdirSync2, rmSync } from "node:fs";
4456
4868
  import { resolve as resolve3 } from "node:path";
4869
+ init_run_containment();
4457
4870
  var TAG16 = "worktree";
4458
4871
 
4459
4872
  class WorktreeBaseError extends Error {
@@ -4462,7 +4875,7 @@ class WorktreeBaseError extends Error {
4462
4875
  this.name = "WorktreeBaseError";
4463
4876
  }
4464
4877
  }
4465
- function fetchBaseBranch(repoRoot, baseBranch, attempts = 3, fetchImpl = (root, branch) => execFileSync8("git", ["fetch", "origin", branch], {
4878
+ function fetchBaseBranch(repoRoot, baseBranch, attempts = 3, fetchImpl = (root, branch) => execFileSync8("git", [...GIT_NO_HOOKS, "fetch", "origin", branch], {
4466
4879
  cwd: root,
4467
4880
  stdio: "pipe"
4468
4881
  })) {
@@ -4515,7 +4928,14 @@ function resolveContinuationTarget(branchName, continueRequested, failedBranchPr
4515
4928
  reason: "fresh"
4516
4929
  };
4517
4930
  }
4518
- function fetchExistingBranch(repoRoot, branchName, attempts = 3, lsRemoteImpl = (root, branch) => execFileSync8("git", ["ls-remote", "--exit-code", "origin", `refs/heads/${branch}`], { cwd: root, stdio: "pipe" }), fetchImpl = (root, branch) => execFileSync8("git", [
4931
+ function fetchExistingBranch(repoRoot, branchName, attempts = 3, lsRemoteImpl = (root, branch) => execFileSync8("git", [
4932
+ ...GIT_NO_HOOKS,
4933
+ "ls-remote",
4934
+ "--exit-code",
4935
+ "origin",
4936
+ `refs/heads/${branch}`
4937
+ ], { cwd: root, stdio: "pipe" }), fetchImpl = (root, branch) => execFileSync8("git", [
4938
+ ...GIT_NO_HOOKS,
4519
4939
  "fetch",
4520
4940
  "origin",
4521
4941
  `+refs/heads/${branch}:refs/remotes/origin/${branch}`
@@ -4549,7 +4969,7 @@ function fetchExistingBranch(repoRoot, branchName, attempts = 3, lsRemoteImpl =
4549
4969
  }
4550
4970
  function readWorktreeHead(worktreePath) {
4551
4971
  try {
4552
- return execFileSync8("git", ["rev-parse", "HEAD"], {
4972
+ return execFileSync8("git", [...GIT_NO_HOOKS, "rev-parse", "HEAD"], {
4553
4973
  cwd: worktreePath,
4554
4974
  encoding: "utf-8"
4555
4975
  }).trim();
@@ -4558,7 +4978,7 @@ function readWorktreeHead(worktreePath) {
4558
4978
  }
4559
4979
  }
4560
4980
  function createWorktree(basePath, baseBranch, branchName, opts = {}) {
4561
- const repoRoot = execFileSync8("git", ["rev-parse", "--show-toplevel"], {
4981
+ const repoRoot = execFileSync8("git", [...GIT_NO_HOOKS, "rev-parse", "--show-toplevel"], {
4562
4982
  encoding: "utf-8"
4563
4983
  }).trim();
4564
4984
  const worktreeDir = resolve3(repoRoot, basePath, branchName);
@@ -4567,7 +4987,7 @@ function createWorktree(basePath, baseBranch, branchName, opts = {}) {
4567
4987
  cleanupWorktree(worktreeDir, branchName);
4568
4988
  }
4569
4989
  try {
4570
- execFileSync8("git", ["worktree", "prune", "--expire=now"], {
4990
+ execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "prune", "--expire=now"], {
4571
4991
  cwd: repoRoot,
4572
4992
  stdio: "pipe"
4573
4993
  });
@@ -4576,37 +4996,54 @@ function createWorktree(basePath, baseBranch, branchName, opts = {}) {
4576
4996
  const startRef = resolveWorktreeStartRef(baseBranch, branchName, opts.continueExisting ?? false, () => opts.branchExistsOnOrigin ?? fetchExistingBranch(repoRoot, branchName));
4577
4997
  log.info(TAG16, `Creating worktree: ${worktreeDir} (branch: ${branchName}, base: ${startRef})`);
4578
4998
  try {
4579
- execFileSync8("git", ["worktree", "add", "-B", branchName, worktreeDir, startRef], { cwd: repoRoot, stdio: "pipe" });
4999
+ execFileSync8("git", [
5000
+ ...GIT_NO_HOOKS,
5001
+ "worktree",
5002
+ "add",
5003
+ "-B",
5004
+ branchName,
5005
+ worktreeDir,
5006
+ startRef
5007
+ ], { cwd: repoRoot, stdio: "pipe" });
4580
5008
  } catch (err) {
4581
5009
  const msg = err instanceof Error ? err.message : String(err);
4582
5010
  log.warn(TAG16, `worktree add failed, attempting forced recovery: ${msg}`);
4583
5011
  removeWorktreeHoldingBranch(repoRoot, branchName, worktreeDir);
4584
5012
  try {
4585
- execFileSync8("git", ["worktree", "remove", worktreeDir, "--force"], {
5013
+ execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "remove", worktreeDir, "--force"], {
4586
5014
  cwd: repoRoot,
4587
5015
  stdio: "pipe"
4588
5016
  });
4589
5017
  } catch {}
4590
5018
  try {
4591
- execFileSync8("git", ["worktree", "prune", "--expire=now"], {
5019
+ execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "prune", "--expire=now"], {
4592
5020
  cwd: repoRoot,
4593
5021
  stdio: "pipe"
4594
5022
  });
4595
5023
  } catch {}
4596
5024
  try {
4597
- execFileSync8("git", ["branch", "-D", branchName], {
5025
+ execFileSync8("git", [...GIT_NO_HOOKS, "branch", "-D", branchName], {
4598
5026
  cwd: repoRoot,
4599
5027
  stdio: "pipe"
4600
5028
  });
4601
5029
  } catch {}
4602
- execFileSync8("git", ["worktree", "add", "-B", branchName, worktreeDir, startRef], { cwd: repoRoot, stdio: "pipe" });
5030
+ execFileSync8("git", [
5031
+ ...GIT_NO_HOOKS,
5032
+ "worktree",
5033
+ "add",
5034
+ "-B",
5035
+ branchName,
5036
+ worktreeDir,
5037
+ startRef
5038
+ ], { cwd: repoRoot, stdio: "pipe" });
4603
5039
  }
4604
5040
  log.info(TAG16, "Installing dependencies in worktree...");
4605
5041
  try {
4606
- execSync(installCommand(), {
5042
+ execSync(installCommand(true), {
4607
5043
  cwd: worktreeDir,
4608
5044
  stdio: "pipe",
4609
- timeout: 60000
5045
+ timeout: 60000,
5046
+ env: containedEnv()
4610
5047
  });
4611
5048
  } catch {
4612
5049
  log.warn(TAG16, "Install failed (may be fine if deps are hoisted)");
@@ -4625,7 +5062,7 @@ function containsForeignWorktrees(dir) {
4625
5062
  return children.some((child) => existsSync3(resolve3(dir, child, ".git")));
4626
5063
  }
4627
5064
  function cleanupWorktree(worktreePath, branchName) {
4628
- const repoRoot = execFileSync8("git", ["rev-parse", "--show-toplevel"], {
5065
+ const repoRoot = execFileSync8("git", [...GIT_NO_HOOKS, "rev-parse", "--show-toplevel"], {
4629
5066
  encoding: "utf-8"
4630
5067
  }).trim();
4631
5068
  if (existsSync3(worktreePath)) {
@@ -4633,7 +5070,7 @@ function cleanupWorktree(worktreePath, branchName) {
4633
5070
  throw new Error(`Refusing to remove ${worktreePath}: it is not a git worktree itself, ` + `but git worktrees live directly beneath it. Removing it would ` + `destroy them — pass the individual worktree paths instead (#928).`);
4634
5071
  }
4635
5072
  try {
4636
- execFileSync8("git", ["worktree", "remove", worktreePath, "--force"], {
5073
+ execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "remove", worktreePath, "--force"], {
4637
5074
  cwd: repoRoot,
4638
5075
  stdio: "pipe"
4639
5076
  });
@@ -4644,7 +5081,7 @@ function cleanupWorktree(worktreePath, branchName) {
4644
5081
  rmSync(worktreePath, { recursive: true, force: true });
4645
5082
  }
4646
5083
  try {
4647
- execFileSync8("git", ["worktree", "prune", "--expire=now"], {
5084
+ execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "prune", "--expire=now"], {
4648
5085
  cwd: repoRoot,
4649
5086
  stdio: "pipe"
4650
5087
  });
@@ -4652,7 +5089,7 @@ function cleanupWorktree(worktreePath, branchName) {
4652
5089
  }
4653
5090
  } else {
4654
5091
  try {
4655
- execFileSync8("git", ["worktree", "prune", "--expire=now"], {
5092
+ execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "prune", "--expire=now"], {
4656
5093
  cwd: repoRoot,
4657
5094
  stdio: "pipe"
4658
5095
  });
@@ -4660,7 +5097,7 @@ function cleanupWorktree(worktreePath, branchName) {
4660
5097
  }
4661
5098
  if (branchName) {
4662
5099
  try {
4663
- execFileSync8("git", ["branch", "-D", branchName], {
5100
+ execFileSync8("git", [...GIT_NO_HOOKS, "branch", "-D", branchName], {
4664
5101
  cwd: repoRoot,
4665
5102
  stdio: "pipe"
4666
5103
  });
@@ -4670,7 +5107,7 @@ function cleanupWorktree(worktreePath, branchName) {
4670
5107
  function removeWorktreeHoldingBranch(repoRoot, branchName, exceptDir) {
4671
5108
  let listing;
4672
5109
  try {
4673
- listing = execFileSync8("git", ["worktree", "list", "--porcelain"], {
5110
+ listing = execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "list", "--porcelain"], {
4674
5111
  cwd: repoRoot,
4675
5112
  encoding: "utf-8",
4676
5113
  stdio: ["ignore", "pipe", "pipe"]
@@ -4698,7 +5135,7 @@ function removeWorktreeHoldingBranch(repoRoot, branchName, exceptDir) {
4698
5135
  if (exceptDir && resolve3(holderPath) === resolve3(exceptDir))
4699
5136
  return null;
4700
5137
  try {
4701
- execFileSync8("git", ["worktree", "remove", holderPath, "--force"], {
5138
+ execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "remove", holderPath, "--force"], {
4702
5139
  cwd: repoRoot,
4703
5140
  stdio: "pipe"
4704
5141
  });
@@ -4708,7 +5145,7 @@ function removeWorktreeHoldingBranch(repoRoot, branchName, exceptDir) {
4708
5145
  return null;
4709
5146
  }
4710
5147
  try {
4711
- execFileSync8("git", ["worktree", "prune", "--expire=now"], {
5148
+ execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "prune", "--expire=now"], {
4712
5149
  cwd: repoRoot,
4713
5150
  stdio: "pipe"
4714
5151
  });
@@ -4716,13 +5153,19 @@ function removeWorktreeHoldingBranch(repoRoot, branchName, exceptDir) {
4716
5153
  return holderPath;
4717
5154
  }
4718
5155
  function resolveRepoRoot() {
4719
- return execFileSync8("git", ["rev-parse", "--show-toplevel"], {
5156
+ return execFileSync8("git", [...GIT_NO_HOOKS, "rev-parse", "--show-toplevel"], {
4720
5157
  encoding: "utf-8"
4721
5158
  }).trim();
4722
5159
  }
4723
5160
  function localBranchExists(branchName, repoRoot) {
4724
5161
  try {
4725
- execFileSync8("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { cwd: repoRoot, stdio: "ignore" });
5162
+ execFileSync8("git", [
5163
+ ...GIT_NO_HOOKS,
5164
+ "show-ref",
5165
+ "--verify",
5166
+ "--quiet",
5167
+ `refs/heads/${branchName}`
5168
+ ], { cwd: repoRoot, stdio: "ignore" });
4726
5169
  return true;
4727
5170
  } catch {
4728
5171
  return false;
@@ -4732,7 +5175,7 @@ function branchAheadOfItsRemote(branchName, repoRoot = resolveRepoRoot()) {
4732
5175
  if (!localBranchExists(branchName, repoRoot))
4733
5176
  return false;
4734
5177
  try {
4735
- const out = execFileSync8("git", ["rev-list", branchName, "--not", "--remotes=origin"], { cwd: repoRoot, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
5178
+ const out = execFileSync8("git", [...GIT_NO_HOOKS, "rev-list", branchName, "--not", "--remotes=origin"], { cwd: repoRoot, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
4736
5179
  return out.length > 0;
4737
5180
  } catch {
4738
5181
  return false;
@@ -4785,11 +5228,14 @@ function makeBranchName(shortId, title, prefix = "agent-attempts/") {
4785
5228
  // src/index.ts
4786
5229
  var MOTOR_NAME = "harmony-harness";
4787
5230
  export {
5231
+ writeOnlyDenyPaths,
4788
5232
  waitForDevServer,
5233
+ verificationSandbox,
4789
5234
  validateGitProviderCli,
4790
5235
  upsertReviewedSha,
4791
5236
  upsertCiReviewRequestedSha,
4792
5237
  updateExistingPr,
5238
+ toolchainCacheDirectories,
4793
5239
  testCommand,
4794
5240
  terminateGroup,
4795
5241
  teardownWorktree,
@@ -4802,6 +5248,7 @@ export {
4802
5248
  sizingEventSource,
4803
5249
  sizeRun,
4804
5250
  signalGroup,
5251
+ secretEnvKeysToStrip,
4805
5252
  sanitizeCiText,
4806
5253
  sandboxRunArgs,
4807
5254
  sandboxAvailable,
@@ -4856,7 +5303,13 @@ export {
4856
5303
  isPrOnOrigin,
4857
5304
  isInsideTree,
4858
5305
  installCommand,
5306
+ implementRunToolPolicy,
5307
+ implementRunContainmentCliArgs,
5308
+ implementRunContainment,
5309
+ hostPersistencePaths,
5310
+ harmonyMcpServer,
4859
5311
  gradeOracleRed,
5312
+ gitMetadataDenyPaths,
4860
5313
  getPrStatus,
4861
5314
  getPrFailedChecks,
4862
5315
  getHeadSha,
@@ -4886,10 +5339,14 @@ export {
4886
5339
  deriveCiStatus,
4887
5340
  decidePrBranch,
4888
5341
  decideConfinedTool,
5342
+ credentialWriteToolDeny,
5343
+ credentialToolDeny,
5344
+ credentialDirectories,
4889
5345
  credentialAccessDeny,
4890
5346
  createWorktree,
4891
5347
  createPullRequest,
4892
5348
  cooldownMsFor,
5349
+ containedEnv,
4893
5350
  confineToRepo,
4894
5351
  collectSizingOutput,
4895
5352
  collectGateEvidence,
@@ -4908,14 +5365,17 @@ export {
4908
5365
  buildCommand,
4909
5366
  branchAheadOfItsRemote,
4910
5367
  attemptAutoFix,
5368
+ assertNoProjectSandboxOverride,
4911
5369
  _resetCache,
4912
5370
  __resetSandboxProbe,
4913
5371
  WorktreeBaseError,
4914
5372
  SdkAgentRunner,
4915
5373
  SIZING_MODEL,
4916
5374
  SDK_ALLOWED_TOOLS,
5375
+ SANDBOX_STARTUP_GRACE_MS,
4917
5376
  SANDBOX_MOUNT,
4918
5377
  ReviewPassedCollector,
5378
+ ProjectSandboxOverrideError,
4919
5379
  OracleRedCollector,
4920
5380
  OracleCollector,
4921
5381
  ORACLE_RUNNER_HINTS,
@@ -4925,8 +5385,10 @@ export {
4925
5385
  MAX_IMPLEMENT_MODEL,
4926
5386
  MAX_CHANGED_FILES,
4927
5387
  JUDGE_MODEL,
5388
+ IMPLEMENT_ALLOWED_DOMAINS,
4928
5389
  HarmonyClient,
4929
5390
  HARMONY_CREDENTIAL_KEYS,
5391
+ GIT_NO_HOOKS,
4930
5392
  GATE_CONFIG_ERROR_MARK,
4931
5393
  GATE_CONFIG_ERROR_KEY,
4932
5394
  DevServerReadinessError,