@gethmy/harness 1.4.0 → 1.5.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/cli.js +527 -69
- package/dist/index.js +660 -228
- package/package.json +2 -2
- package/src/ci-failure.ts +10 -5
- package/src/exec-types.ts +5 -7
- package/src/git-diff-stat.ts +2 -1
- package/src/git-pr.ts +59 -25
- package/src/index.ts +1 -0
- package/src/pm.ts +8 -3
- package/src/revert-guard.ts +9 -2
- package/src/run-containment.ts +1292 -0
- package/src/sdk-agent-runner.ts +19 -0
- package/src/verification.ts +84 -7
- package/src/worktree.ts +149 -57
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, {
|
|
@@ -652,7 +1137,7 @@ function execFileAsync2() {
|
|
|
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();
|
|
@@ -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();
|
|
@@ -1127,7 +1612,13 @@ function resolvePrUrl(description, branchName, cwd, provider) {
|
|
|
1127
1612
|
}
|
|
1128
1613
|
function remoteBranchExists(branchName, cwd) {
|
|
1129
1614
|
try {
|
|
1130
|
-
execFileSync7("git", [
|
|
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;
|
|
@@ -1138,21 +1629,21 @@ function pushBranch(branchName, cwd) {
|
|
|
1138
1629
|
log.info(TAG13, `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
1638
|
log.warn(TAG13, `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,7 +1654,7 @@ 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();
|
|
@@ -1171,9 +1662,15 @@ function renameRemoteBranch(oldRef, newRef, cwd) {
|
|
|
1171
1662
|
throw new Error(`renameRemoteBranch: could not resolve HEAD: ${err instanceof Error ? err.message : err}`);
|
|
1172
1663
|
}
|
|
1173
1664
|
log.info(TAG13, `Renaming remote ${oldRef} → ${newRef}`);
|
|
1174
|
-
execFileSync7("git", [
|
|
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
|
});
|
|
@@ -1181,7 +1678,7 @@ function renameRemoteBranch(oldRef, newRef, cwd) {
|
|
|
1181
1678
|
log.warn(TAG13, `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();
|
|
@@ -1242,7 +1739,12 @@ function createPullRequest(card, branchName, worktreePath, config, provider, exi
|
|
|
1242
1739
|
}
|
|
1243
1740
|
let commitLog = "";
|
|
1244
1741
|
try {
|
|
1245
|
-
commitLog = execFileSync7("git", [
|
|
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
|
}
|
|
@@ -1350,6 +1852,7 @@ var cachedExecFileAsync2, TAG13 = "git-pr", VALID_PR_URL_RE, PR_URL_RE, REVIEWED
|
|
|
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"]
|
|
@@ -2230,14 +2735,14 @@ var MAX_METRIC_TIMEOUT_MS = 900000;
|
|
|
2230
2735
|
var METRIC_SIGINT_GRACE_MS = 2000;
|
|
2231
2736
|
var METRIC_SIGTERM_GRACE_MS = 3000;
|
|
2232
2737
|
var STDIO_DRAIN_GRACE_MS = 500;
|
|
2233
|
-
function parseParseMode(
|
|
2234
|
-
if (typeof
|
|
2738
|
+
function parseParseMode(parse2) {
|
|
2739
|
+
if (typeof parse2 !== "string" || parse2.length === 0) {
|
|
2235
2740
|
return { kind: "invalid", reason: "`parse` must be a non-empty string" };
|
|
2236
2741
|
}
|
|
2237
|
-
if (
|
|
2742
|
+
if (parse2 === "number")
|
|
2238
2743
|
return { kind: "number" };
|
|
2239
|
-
if (
|
|
2240
|
-
const path =
|
|
2744
|
+
if (parse2.startsWith("json:")) {
|
|
2745
|
+
const path = parse2.slice("json:".length).trim();
|
|
2241
2746
|
if (!path) {
|
|
2242
2747
|
return { kind: "invalid", reason: '`parse` "json:" is missing a path' };
|
|
2243
2748
|
}
|
|
@@ -2245,7 +2750,7 @@ function parseParseMode(parse) {
|
|
|
2245
2750
|
}
|
|
2246
2751
|
return {
|
|
2247
2752
|
kind: "invalid",
|
|
2248
|
-
reason: `unknown \`parse\` mode "${
|
|
2753
|
+
reason: `unknown \`parse\` mode "${parse2}" (expected "number" or "json:<path>")`
|
|
2249
2754
|
};
|
|
2250
2755
|
}
|
|
2251
2756
|
function parseMetricValue(mode, stdout) {
|
|
@@ -2291,7 +2796,7 @@ function parseMetricValue(mode, stdout) {
|
|
|
2291
2796
|
return { ok: true, value: resolved };
|
|
2292
2797
|
}
|
|
2293
2798
|
function runMetricCommand(args) {
|
|
2294
|
-
return new Promise((
|
|
2799
|
+
return new Promise((resolve2, reject) => {
|
|
2295
2800
|
let child;
|
|
2296
2801
|
try {
|
|
2297
2802
|
child = spawnInGroup(args.command, args.args, {
|
|
@@ -2322,7 +2827,7 @@ function runMetricCommand(args) {
|
|
|
2322
2827
|
if (failure)
|
|
2323
2828
|
reject(failure);
|
|
2324
2829
|
else
|
|
2325
|
-
|
|
2830
|
+
resolve2(Buffer.concat(chunks).toString("utf8"));
|
|
2326
2831
|
};
|
|
2327
2832
|
const killTree = (reason) => {
|
|
2328
2833
|
if (settled || killReason)
|
|
@@ -2470,123 +2975,20 @@ function describeRunFailure(err, timeoutMs) {
|
|
|
2470
2975
|
function truncate(value, max) {
|
|
2471
2976
|
return value.length <= max ? value : `${value.slice(0, max)}…[truncated]`;
|
|
2472
2977
|
}
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
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
|
-
}
|
|
2978
|
+
|
|
2979
|
+
// src/index.ts
|
|
2980
|
+
init_confine_to_repo();
|
|
2981
|
+
|
|
2580
2982
|
// src/gate-collectors.ts
|
|
2581
2983
|
init_dist();
|
|
2582
2984
|
init_log();
|
|
2583
2985
|
|
|
2584
2986
|
// src/oracle-collector.ts
|
|
2585
|
-
import { createHash } from "node:crypto";
|
|
2987
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
2586
2988
|
init_log();
|
|
2587
2989
|
|
|
2588
2990
|
// src/oracle.ts
|
|
2589
|
-
import { lstatSync, readFileSync, statSync } from "node:fs";
|
|
2991
|
+
import { lstatSync, readFileSync as readFileSync2, statSync } from "node:fs";
|
|
2590
2992
|
import {
|
|
2591
2993
|
chmod,
|
|
2592
2994
|
lstat,
|
|
@@ -2596,13 +2998,13 @@ import {
|
|
|
2596
2998
|
rm,
|
|
2597
2999
|
writeFile
|
|
2598
3000
|
} from "node:fs/promises";
|
|
2599
|
-
import { tmpdir } from "node:os";
|
|
2600
|
-
import { dirname as
|
|
3001
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
3002
|
+
import { dirname as dirname3, isAbsolute as isAbsolute3, join as join2, resolve as resolve2, sep as sep2 } from "node:path";
|
|
2601
3003
|
import { StringDecoder } from "node:string_decoder";
|
|
2602
3004
|
init_log();
|
|
2603
3005
|
var TAG5 = "oracle";
|
|
2604
3006
|
async function resolveContained(repoPath, relativePath) {
|
|
2605
|
-
if (
|
|
3007
|
+
if (isAbsolute3(relativePath)) {
|
|
2606
3008
|
throw new Error(`refusing to place an oracle at an absolute path: ${relativePath}`);
|
|
2607
3009
|
}
|
|
2608
3010
|
if (relativePath === "" || relativePath === ".") {
|
|
@@ -2625,7 +3027,7 @@ async function resolveContained(repoPath, relativePath) {
|
|
|
2625
3027
|
}
|
|
2626
3028
|
async function place(repoPath, oracle) {
|
|
2627
3029
|
const target = await resolveContained(repoPath, oracle.path);
|
|
2628
|
-
await mkdir(
|
|
3030
|
+
await mkdir(dirname3(target), { recursive: true });
|
|
2629
3031
|
await writeFile(target, oracle.content, "utf8");
|
|
2630
3032
|
}
|
|
2631
3033
|
async function remove(repoPath, oracle) {
|
|
@@ -2733,8 +3135,8 @@ function argvPath(path) {
|
|
|
2733
3135
|
}
|
|
2734
3136
|
async function runHeldOracle(repoPath, oracle, timeoutMs = DEFAULT_METRIC_TIMEOUT_MS) {
|
|
2735
3137
|
const spec = resolveOracleRunnerSpec(oracle);
|
|
2736
|
-
const reportDir = await mkdtemp(
|
|
2737
|
-
const reportPath =
|
|
3138
|
+
const reportDir = await mkdtemp(join2(tmpdir2(), reportDirPrefix()));
|
|
3139
|
+
const reportPath = join2(reportDir, spec.report.file);
|
|
2738
3140
|
try {
|
|
2739
3141
|
return await spawnHeldOracle(spec, repoPath, oracle, reportPath, timeoutMs);
|
|
2740
3142
|
} finally {
|
|
@@ -2745,7 +3147,7 @@ function reportDirPrefix() {
|
|
|
2745
3147
|
return "harmony-oracle-report-";
|
|
2746
3148
|
}
|
|
2747
3149
|
function assertUntampered(reportPath) {
|
|
2748
|
-
const dir = statSync(
|
|
3150
|
+
const dir = statSync(dirname3(reportPath));
|
|
2749
3151
|
if ((dir.mode & 511) !== 448) {
|
|
2750
3152
|
throw new Error(`the oracle report directory's mode changed to ${(dir.mode & 511).toString(8)} — refusing the report`);
|
|
2751
3153
|
}
|
|
@@ -2808,7 +3210,7 @@ ${output}` : output;
|
|
|
2808
3210
|
const captureReport = () => {
|
|
2809
3211
|
try {
|
|
2810
3212
|
assertUntampered(reportPath);
|
|
2811
|
-
report = spec.report.parse(
|
|
3213
|
+
report = spec.report.parse(readFileSync2(reportPath, "utf8"));
|
|
2812
3214
|
} catch (err) {
|
|
2813
3215
|
report = null;
|
|
2814
3216
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -2926,7 +3328,7 @@ class HeldOracleCollector {
|
|
|
2926
3328
|
async runHeld(oracle) {
|
|
2927
3329
|
const identity = {
|
|
2928
3330
|
oracleId: oracle.id ?? null,
|
|
2929
|
-
contentHash:
|
|
3331
|
+
contentHash: createHash2("sha256").update(oracle.content).digest("hex")
|
|
2930
3332
|
};
|
|
2931
3333
|
await this.deps.place(this.deps.repoPath, oracle);
|
|
2932
3334
|
try {
|
|
@@ -3042,6 +3444,7 @@ import { execFileSync as execFileSync5, spawn as spawn2 } from "node:child_proce
|
|
|
3042
3444
|
|
|
3043
3445
|
// src/pm.ts
|
|
3044
3446
|
init_log();
|
|
3447
|
+
init_run_containment();
|
|
3045
3448
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
3046
3449
|
import { existsSync } from "node:fs";
|
|
3047
3450
|
var TAG7 = "pm";
|
|
@@ -3051,7 +3454,7 @@ function detectPackageManager() {
|
|
|
3051
3454
|
return cached;
|
|
3052
3455
|
let repoRoot;
|
|
3053
3456
|
try {
|
|
3054
|
-
repoRoot = execFileSync2("git", ["rev-parse", "--show-toplevel"], {
|
|
3457
|
+
repoRoot = execFileSync2("git", [...GIT_NO_HOOKS, "rev-parse", "--show-toplevel"], {
|
|
3055
3458
|
encoding: "utf-8"
|
|
3056
3459
|
}).trim();
|
|
3057
3460
|
} catch {
|
|
@@ -3094,7 +3497,7 @@ function spawnRunArgs(script, ...extra) {
|
|
|
3094
3497
|
// src/project-type.ts
|
|
3095
3498
|
init_log();
|
|
3096
3499
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
3097
|
-
import { existsSync as existsSync2, readdirSync, readFileSync as
|
|
3500
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "node:fs";
|
|
3098
3501
|
var TAG8 = "project-type";
|
|
3099
3502
|
var _cache = new Map;
|
|
3100
3503
|
function _resetCache() {
|
|
@@ -3197,7 +3600,7 @@ var NPM_PLACEHOLDER_TEST = /no test specified/i;
|
|
|
3197
3600
|
function hasNodeTestScript(dir) {
|
|
3198
3601
|
let script;
|
|
3199
3602
|
try {
|
|
3200
|
-
const pkg = JSON.parse(
|
|
3603
|
+
const pkg = JSON.parse(readFileSync3(`${dir}/package.json`, "utf-8"));
|
|
3201
3604
|
script = pkg.scripts?.test;
|
|
3202
3605
|
} catch (err) {
|
|
3203
3606
|
log.warn(TAG8, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
|
|
@@ -3214,7 +3617,7 @@ function hasNodeTestScript(dir) {
|
|
|
3214
3617
|
function firstNodeScript(dir, candidates) {
|
|
3215
3618
|
let scripts;
|
|
3216
3619
|
try {
|
|
3217
|
-
const pkg = JSON.parse(
|
|
3620
|
+
const pkg = JSON.parse(readFileSync3(`${dir}/package.json`, "utf-8"));
|
|
3218
3621
|
scripts = pkg.scripts ?? {};
|
|
3219
3622
|
} catch (err) {
|
|
3220
3623
|
log.warn(TAG8, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
|
|
@@ -3271,6 +3674,7 @@ function resolveXcodeScheme(pt) {
|
|
|
3271
3674
|
|
|
3272
3675
|
// src/revert-guard.ts
|
|
3273
3676
|
init_log();
|
|
3677
|
+
init_run_containment();
|
|
3274
3678
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
3275
3679
|
var TAG9 = "revert-guard";
|
|
3276
3680
|
var TEST_FILE = /(?:^|\/)__tests__\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
@@ -3282,7 +3686,7 @@ function filterTestFiles(paths) {
|
|
|
3282
3686
|
}
|
|
3283
3687
|
function refetchBase(worktreePath, baseBranch) {
|
|
3284
3688
|
try {
|
|
3285
|
-
execFileSync4("git", ["fetch", "origin", baseBranch], {
|
|
3689
|
+
execFileSync4("git", [...GIT_NO_HOOKS, "fetch", "origin", baseBranch], {
|
|
3286
3690
|
cwd: worktreePath,
|
|
3287
3691
|
stdio: "pipe"
|
|
3288
3692
|
});
|
|
@@ -3292,7 +3696,13 @@ function refetchBase(worktreePath, baseBranch) {
|
|
|
3292
3696
|
}
|
|
3293
3697
|
function listDeletedFilesAgainstBase(worktreePath, baseBranch) {
|
|
3294
3698
|
try {
|
|
3295
|
-
const out = execFileSync4("git", [
|
|
3699
|
+
const out = execFileSync4("git", [
|
|
3700
|
+
...GIT_NO_HOOKS,
|
|
3701
|
+
"diff",
|
|
3702
|
+
"--diff-filter=D",
|
|
3703
|
+
"--name-only",
|
|
3704
|
+
`origin/${baseBranch}...HEAD`
|
|
3705
|
+
], { cwd: worktreePath, encoding: "utf-8" });
|
|
3296
3706
|
return out.split(`
|
|
3297
3707
|
`).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
3298
3708
|
} catch (err) {
|
|
@@ -3306,6 +3716,7 @@ function findDeletedTestFiles(worktreePath, baseBranch) {
|
|
|
3306
3716
|
}
|
|
3307
3717
|
|
|
3308
3718
|
// src/verification.ts
|
|
3719
|
+
init_run_containment();
|
|
3309
3720
|
var TAG10 = "verification";
|
|
3310
3721
|
var MAX_OUTPUT_BUFFER2 = 64 * 1024 * 1024;
|
|
3311
3722
|
async function runVerification(worktreePath, config, workerId) {
|
|
@@ -3379,7 +3790,8 @@ function runBuild(worktreePath, timeout) {
|
|
|
3379
3790
|
cwd: worktreePath,
|
|
3380
3791
|
timeout,
|
|
3381
3792
|
stdio: "pipe",
|
|
3382
|
-
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
3793
|
+
maxBuffer: MAX_OUTPUT_BUFFER2,
|
|
3794
|
+
env: containedEnv()
|
|
3383
3795
|
});
|
|
3384
3796
|
return [];
|
|
3385
3797
|
} catch (err) {
|
|
@@ -3397,7 +3809,8 @@ function runTests(worktreePath, timeout) {
|
|
|
3397
3809
|
cwd: worktreePath,
|
|
3398
3810
|
timeout,
|
|
3399
3811
|
stdio: "pipe",
|
|
3400
|
-
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
3812
|
+
maxBuffer: MAX_OUTPUT_BUFFER2,
|
|
3813
|
+
env: containedEnv()
|
|
3401
3814
|
});
|
|
3402
3815
|
return [];
|
|
3403
3816
|
} catch (err) {
|
|
@@ -3416,7 +3829,8 @@ function runFormatFix(worktreePath, timeout, workerId) {
|
|
|
3416
3829
|
cwd: worktreePath,
|
|
3417
3830
|
timeout,
|
|
3418
3831
|
stdio: "pipe",
|
|
3419
|
-
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
3832
|
+
maxBuffer: MAX_OUTPUT_BUFFER2,
|
|
3833
|
+
env: containedEnv()
|
|
3420
3834
|
});
|
|
3421
3835
|
log.info(TAG10, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
|
|
3422
3836
|
} catch (err) {
|
|
@@ -3434,7 +3848,8 @@ function runLint(worktreePath, timeout) {
|
|
|
3434
3848
|
cwd: worktreePath,
|
|
3435
3849
|
timeout,
|
|
3436
3850
|
stdio: "pipe",
|
|
3437
|
-
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
3851
|
+
maxBuffer: MAX_OUTPUT_BUFFER2,
|
|
3852
|
+
env: containedEnv()
|
|
3438
3853
|
});
|
|
3439
3854
|
return [];
|
|
3440
3855
|
} catch (err) {
|
|
@@ -3452,7 +3867,8 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
3452
3867
|
const [cmd, args] = spawnRunArgs("dev", "--port", String(port));
|
|
3453
3868
|
devServer = spawn2(cmd, args, {
|
|
3454
3869
|
cwd: worktreePath,
|
|
3455
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3870
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3871
|
+
env: containedEnv()
|
|
3456
3872
|
});
|
|
3457
3873
|
try {
|
|
3458
3874
|
await waitForDevServer(devServer, 30000);
|
|
@@ -3463,7 +3879,7 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
3463
3879
|
}
|
|
3464
3880
|
let diff = "";
|
|
3465
3881
|
try {
|
|
3466
|
-
diff = execFileSync5("git", ["diff", `origin/${config.worktree.baseBranch}..HEAD`], {
|
|
3882
|
+
diff = execFileSync5("git", [...GIT_NO_HOOKS, "diff", `origin/${config.worktree.baseBranch}..HEAD`], {
|
|
3467
3883
|
cwd: worktreePath,
|
|
3468
3884
|
encoding: "utf-8",
|
|
3469
3885
|
timeout: 30000,
|
|
@@ -3484,14 +3900,16 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
3484
3900
|
"```"
|
|
3485
3901
|
].join(`
|
|
3486
3902
|
`);
|
|
3487
|
-
const leanSources = config.claude.leanSettingSources;
|
|
3488
3903
|
const output = execFileSync5("claude", [
|
|
3489
3904
|
"--print",
|
|
3490
3905
|
"--model",
|
|
3491
3906
|
"sonnet",
|
|
3492
3907
|
"--max-turns",
|
|
3493
3908
|
"10",
|
|
3494
|
-
...
|
|
3909
|
+
...implementRunContainmentCliArgs({
|
|
3910
|
+
worktree: worktreePath,
|
|
3911
|
+
readOnly: true
|
|
3912
|
+
}),
|
|
3495
3913
|
"--",
|
|
3496
3914
|
reviewPrompt
|
|
3497
3915
|
], {
|
|
@@ -3499,7 +3917,8 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
3499
3917
|
encoding: "utf-8",
|
|
3500
3918
|
timeout: config.verification.timeout,
|
|
3501
3919
|
stdio: "pipe",
|
|
3502
|
-
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
3920
|
+
maxBuffer: MAX_OUTPUT_BUFFER2,
|
|
3921
|
+
env: containedEnv()
|
|
3503
3922
|
});
|
|
3504
3923
|
return parseReviewFindings(output);
|
|
3505
3924
|
} catch (err) {
|
|
@@ -3529,7 +3948,6 @@ function attemptAutoFix(worktreePath, config, errors) {
|
|
|
3529
3948
|
"```"
|
|
3530
3949
|
].join(`
|
|
3531
3950
|
`);
|
|
3532
|
-
const leanSources = config.claude.leanSettingSources;
|
|
3533
3951
|
const args = [
|
|
3534
3952
|
"--print",
|
|
3535
3953
|
"--model",
|
|
@@ -3538,7 +3956,7 @@ function attemptAutoFix(worktreePath, config, errors) {
|
|
|
3538
3956
|
"50",
|
|
3539
3957
|
"--allowedTools",
|
|
3540
3958
|
"Bash,Read,Write,Edit,Glob,Grep",
|
|
3541
|
-
...
|
|
3959
|
+
...implementRunContainmentCliArgs({ worktree: worktreePath }),
|
|
3542
3960
|
"--",
|
|
3543
3961
|
fixPrompt
|
|
3544
3962
|
];
|
|
@@ -3547,7 +3965,8 @@ function attemptAutoFix(worktreePath, config, errors) {
|
|
|
3547
3965
|
cwd: worktreePath,
|
|
3548
3966
|
timeout: config.verification.timeout,
|
|
3549
3967
|
stdio: "pipe",
|
|
3550
|
-
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
3968
|
+
maxBuffer: MAX_OUTPUT_BUFFER2,
|
|
3969
|
+
env: containedEnv()
|
|
3551
3970
|
});
|
|
3552
3971
|
}
|
|
3553
3972
|
async function reportFindings(client, cardId, result, recovery) {
|
|
@@ -3880,6 +4299,7 @@ async function collectGateEvidence(registry, context) {
|
|
|
3880
4299
|
}
|
|
3881
4300
|
// src/git-diff-stat.ts
|
|
3882
4301
|
init_log();
|
|
4302
|
+
init_run_containment();
|
|
3883
4303
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
3884
4304
|
var TAG12 = "git-diff-stat";
|
|
3885
4305
|
var MAX_CHANGED_FILES = 30;
|
|
@@ -3960,7 +4380,7 @@ function formatDiffSummary(diff, maxFiles = 100) {
|
|
|
3960
4380
|
}
|
|
3961
4381
|
function captureDiffStat(worktreePath, baseBranch, maxFiles = MAX_CHANGED_FILES) {
|
|
3962
4382
|
try {
|
|
3963
|
-
const raw = execFileSync6("git", ["diff", "--numstat", `${baseBranch}...HEAD`], { cwd: worktreePath, encoding: "utf-8", timeout: 30000 });
|
|
4383
|
+
const raw = execFileSync6("git", [...GIT_NO_HOOKS, "diff", "--numstat", `${baseBranch}...HEAD`], { cwd: worktreePath, encoding: "utf-8", timeout: 30000 });
|
|
3964
4384
|
return parseNumstat(raw, maxFiles);
|
|
3965
4385
|
} catch (err) {
|
|
3966
4386
|
log.warn(TAG12, "git diff --numstat failed", {
|
|
@@ -4204,54 +4624,14 @@ async function runInSandbox(args) {
|
|
|
4204
4624
|
return { passed: false, output };
|
|
4205
4625
|
}
|
|
4206
4626
|
}
|
|
4207
|
-
// src/run-sizing.ts
|
|
4208
|
-
init_dist();
|
|
4209
4627
|
|
|
4210
|
-
// src/
|
|
4211
|
-
|
|
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
|
-
}
|
|
4628
|
+
// src/index.ts
|
|
4629
|
+
init_run_containment();
|
|
4253
4630
|
|
|
4254
4631
|
// src/run-sizing.ts
|
|
4632
|
+
init_dist();
|
|
4633
|
+
init_confine_to_repo();
|
|
4634
|
+
init_runner();
|
|
4255
4635
|
var SIZING_MODEL = "haiku";
|
|
4256
4636
|
var SIZING_MAX_TURNS = 25;
|
|
4257
4637
|
var SIZING_MAX_BUDGET_USD = 0.75;
|
|
@@ -4425,6 +4805,10 @@ async function sizeRun(deps) {
|
|
|
4425
4805
|
clearTimeout(timer);
|
|
4426
4806
|
}
|
|
4427
4807
|
}
|
|
4808
|
+
|
|
4809
|
+
// src/index.ts
|
|
4810
|
+
init_runner();
|
|
4811
|
+
|
|
4428
4812
|
// src/stage-run.ts
|
|
4429
4813
|
async function runStage(request, deps) {
|
|
4430
4814
|
const events = [];
|
|
@@ -4454,6 +4838,7 @@ init_log();
|
|
|
4454
4838
|
import { execFileSync as execFileSync8, execSync } from "node:child_process";
|
|
4455
4839
|
import { existsSync as existsSync3, readdirSync as readdirSync2, rmSync } from "node:fs";
|
|
4456
4840
|
import { resolve as resolve3 } from "node:path";
|
|
4841
|
+
init_run_containment();
|
|
4457
4842
|
var TAG16 = "worktree";
|
|
4458
4843
|
|
|
4459
4844
|
class WorktreeBaseError extends Error {
|
|
@@ -4462,7 +4847,7 @@ class WorktreeBaseError extends Error {
|
|
|
4462
4847
|
this.name = "WorktreeBaseError";
|
|
4463
4848
|
}
|
|
4464
4849
|
}
|
|
4465
|
-
function fetchBaseBranch(repoRoot, baseBranch, attempts = 3, fetchImpl = (root, branch) => execFileSync8("git", ["fetch", "origin", branch], {
|
|
4850
|
+
function fetchBaseBranch(repoRoot, baseBranch, attempts = 3, fetchImpl = (root, branch) => execFileSync8("git", [...GIT_NO_HOOKS, "fetch", "origin", branch], {
|
|
4466
4851
|
cwd: root,
|
|
4467
4852
|
stdio: "pipe"
|
|
4468
4853
|
})) {
|
|
@@ -4515,7 +4900,14 @@ function resolveContinuationTarget(branchName, continueRequested, failedBranchPr
|
|
|
4515
4900
|
reason: "fresh"
|
|
4516
4901
|
};
|
|
4517
4902
|
}
|
|
4518
|
-
function fetchExistingBranch(repoRoot, branchName, attempts = 3, lsRemoteImpl = (root, branch) => execFileSync8("git", [
|
|
4903
|
+
function fetchExistingBranch(repoRoot, branchName, attempts = 3, lsRemoteImpl = (root, branch) => execFileSync8("git", [
|
|
4904
|
+
...GIT_NO_HOOKS,
|
|
4905
|
+
"ls-remote",
|
|
4906
|
+
"--exit-code",
|
|
4907
|
+
"origin",
|
|
4908
|
+
`refs/heads/${branch}`
|
|
4909
|
+
], { cwd: root, stdio: "pipe" }), fetchImpl = (root, branch) => execFileSync8("git", [
|
|
4910
|
+
...GIT_NO_HOOKS,
|
|
4519
4911
|
"fetch",
|
|
4520
4912
|
"origin",
|
|
4521
4913
|
`+refs/heads/${branch}:refs/remotes/origin/${branch}`
|
|
@@ -4549,7 +4941,7 @@ function fetchExistingBranch(repoRoot, branchName, attempts = 3, lsRemoteImpl =
|
|
|
4549
4941
|
}
|
|
4550
4942
|
function readWorktreeHead(worktreePath) {
|
|
4551
4943
|
try {
|
|
4552
|
-
return execFileSync8("git", ["rev-parse", "HEAD"], {
|
|
4944
|
+
return execFileSync8("git", [...GIT_NO_HOOKS, "rev-parse", "HEAD"], {
|
|
4553
4945
|
cwd: worktreePath,
|
|
4554
4946
|
encoding: "utf-8"
|
|
4555
4947
|
}).trim();
|
|
@@ -4558,7 +4950,7 @@ function readWorktreeHead(worktreePath) {
|
|
|
4558
4950
|
}
|
|
4559
4951
|
}
|
|
4560
4952
|
function createWorktree(basePath, baseBranch, branchName, opts = {}) {
|
|
4561
|
-
const repoRoot = execFileSync8("git", ["rev-parse", "--show-toplevel"], {
|
|
4953
|
+
const repoRoot = execFileSync8("git", [...GIT_NO_HOOKS, "rev-parse", "--show-toplevel"], {
|
|
4562
4954
|
encoding: "utf-8"
|
|
4563
4955
|
}).trim();
|
|
4564
4956
|
const worktreeDir = resolve3(repoRoot, basePath, branchName);
|
|
@@ -4567,7 +4959,7 @@ function createWorktree(basePath, baseBranch, branchName, opts = {}) {
|
|
|
4567
4959
|
cleanupWorktree(worktreeDir, branchName);
|
|
4568
4960
|
}
|
|
4569
4961
|
try {
|
|
4570
|
-
execFileSync8("git", ["worktree", "prune", "--expire=now"], {
|
|
4962
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "prune", "--expire=now"], {
|
|
4571
4963
|
cwd: repoRoot,
|
|
4572
4964
|
stdio: "pipe"
|
|
4573
4965
|
});
|
|
@@ -4576,37 +4968,54 @@ function createWorktree(basePath, baseBranch, branchName, opts = {}) {
|
|
|
4576
4968
|
const startRef = resolveWorktreeStartRef(baseBranch, branchName, opts.continueExisting ?? false, () => opts.branchExistsOnOrigin ?? fetchExistingBranch(repoRoot, branchName));
|
|
4577
4969
|
log.info(TAG16, `Creating worktree: ${worktreeDir} (branch: ${branchName}, base: ${startRef})`);
|
|
4578
4970
|
try {
|
|
4579
|
-
execFileSync8("git", [
|
|
4971
|
+
execFileSync8("git", [
|
|
4972
|
+
...GIT_NO_HOOKS,
|
|
4973
|
+
"worktree",
|
|
4974
|
+
"add",
|
|
4975
|
+
"-B",
|
|
4976
|
+
branchName,
|
|
4977
|
+
worktreeDir,
|
|
4978
|
+
startRef
|
|
4979
|
+
], { cwd: repoRoot, stdio: "pipe" });
|
|
4580
4980
|
} catch (err) {
|
|
4581
4981
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4582
4982
|
log.warn(TAG16, `worktree add failed, attempting forced recovery: ${msg}`);
|
|
4583
4983
|
removeWorktreeHoldingBranch(repoRoot, branchName, worktreeDir);
|
|
4584
4984
|
try {
|
|
4585
|
-
execFileSync8("git", ["worktree", "remove", worktreeDir, "--force"], {
|
|
4985
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "remove", worktreeDir, "--force"], {
|
|
4586
4986
|
cwd: repoRoot,
|
|
4587
4987
|
stdio: "pipe"
|
|
4588
4988
|
});
|
|
4589
4989
|
} catch {}
|
|
4590
4990
|
try {
|
|
4591
|
-
execFileSync8("git", ["worktree", "prune", "--expire=now"], {
|
|
4991
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "prune", "--expire=now"], {
|
|
4592
4992
|
cwd: repoRoot,
|
|
4593
4993
|
stdio: "pipe"
|
|
4594
4994
|
});
|
|
4595
4995
|
} catch {}
|
|
4596
4996
|
try {
|
|
4597
|
-
execFileSync8("git", ["branch", "-D", branchName], {
|
|
4997
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "branch", "-D", branchName], {
|
|
4598
4998
|
cwd: repoRoot,
|
|
4599
4999
|
stdio: "pipe"
|
|
4600
5000
|
});
|
|
4601
5001
|
} catch {}
|
|
4602
|
-
execFileSync8("git", [
|
|
5002
|
+
execFileSync8("git", [
|
|
5003
|
+
...GIT_NO_HOOKS,
|
|
5004
|
+
"worktree",
|
|
5005
|
+
"add",
|
|
5006
|
+
"-B",
|
|
5007
|
+
branchName,
|
|
5008
|
+
worktreeDir,
|
|
5009
|
+
startRef
|
|
5010
|
+
], { cwd: repoRoot, stdio: "pipe" });
|
|
4603
5011
|
}
|
|
4604
5012
|
log.info(TAG16, "Installing dependencies in worktree...");
|
|
4605
5013
|
try {
|
|
4606
|
-
execSync(installCommand(), {
|
|
5014
|
+
execSync(installCommand(true), {
|
|
4607
5015
|
cwd: worktreeDir,
|
|
4608
5016
|
stdio: "pipe",
|
|
4609
|
-
timeout: 60000
|
|
5017
|
+
timeout: 60000,
|
|
5018
|
+
env: containedEnv()
|
|
4610
5019
|
});
|
|
4611
5020
|
} catch {
|
|
4612
5021
|
log.warn(TAG16, "Install failed (may be fine if deps are hoisted)");
|
|
@@ -4625,7 +5034,7 @@ function containsForeignWorktrees(dir) {
|
|
|
4625
5034
|
return children.some((child) => existsSync3(resolve3(dir, child, ".git")));
|
|
4626
5035
|
}
|
|
4627
5036
|
function cleanupWorktree(worktreePath, branchName) {
|
|
4628
|
-
const repoRoot = execFileSync8("git", ["rev-parse", "--show-toplevel"], {
|
|
5037
|
+
const repoRoot = execFileSync8("git", [...GIT_NO_HOOKS, "rev-parse", "--show-toplevel"], {
|
|
4629
5038
|
encoding: "utf-8"
|
|
4630
5039
|
}).trim();
|
|
4631
5040
|
if (existsSync3(worktreePath)) {
|
|
@@ -4633,7 +5042,7 @@ function cleanupWorktree(worktreePath, branchName) {
|
|
|
4633
5042
|
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
5043
|
}
|
|
4635
5044
|
try {
|
|
4636
|
-
execFileSync8("git", ["worktree", "remove", worktreePath, "--force"], {
|
|
5045
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "remove", worktreePath, "--force"], {
|
|
4637
5046
|
cwd: repoRoot,
|
|
4638
5047
|
stdio: "pipe"
|
|
4639
5048
|
});
|
|
@@ -4644,7 +5053,7 @@ function cleanupWorktree(worktreePath, branchName) {
|
|
|
4644
5053
|
rmSync(worktreePath, { recursive: true, force: true });
|
|
4645
5054
|
}
|
|
4646
5055
|
try {
|
|
4647
|
-
execFileSync8("git", ["worktree", "prune", "--expire=now"], {
|
|
5056
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "prune", "--expire=now"], {
|
|
4648
5057
|
cwd: repoRoot,
|
|
4649
5058
|
stdio: "pipe"
|
|
4650
5059
|
});
|
|
@@ -4652,7 +5061,7 @@ function cleanupWorktree(worktreePath, branchName) {
|
|
|
4652
5061
|
}
|
|
4653
5062
|
} else {
|
|
4654
5063
|
try {
|
|
4655
|
-
execFileSync8("git", ["worktree", "prune", "--expire=now"], {
|
|
5064
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "prune", "--expire=now"], {
|
|
4656
5065
|
cwd: repoRoot,
|
|
4657
5066
|
stdio: "pipe"
|
|
4658
5067
|
});
|
|
@@ -4660,7 +5069,7 @@ function cleanupWorktree(worktreePath, branchName) {
|
|
|
4660
5069
|
}
|
|
4661
5070
|
if (branchName) {
|
|
4662
5071
|
try {
|
|
4663
|
-
execFileSync8("git", ["branch", "-D", branchName], {
|
|
5072
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "branch", "-D", branchName], {
|
|
4664
5073
|
cwd: repoRoot,
|
|
4665
5074
|
stdio: "pipe"
|
|
4666
5075
|
});
|
|
@@ -4670,7 +5079,7 @@ function cleanupWorktree(worktreePath, branchName) {
|
|
|
4670
5079
|
function removeWorktreeHoldingBranch(repoRoot, branchName, exceptDir) {
|
|
4671
5080
|
let listing;
|
|
4672
5081
|
try {
|
|
4673
|
-
listing = execFileSync8("git", ["worktree", "list", "--porcelain"], {
|
|
5082
|
+
listing = execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "list", "--porcelain"], {
|
|
4674
5083
|
cwd: repoRoot,
|
|
4675
5084
|
encoding: "utf-8",
|
|
4676
5085
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -4698,7 +5107,7 @@ function removeWorktreeHoldingBranch(repoRoot, branchName, exceptDir) {
|
|
|
4698
5107
|
if (exceptDir && resolve3(holderPath) === resolve3(exceptDir))
|
|
4699
5108
|
return null;
|
|
4700
5109
|
try {
|
|
4701
|
-
execFileSync8("git", ["worktree", "remove", holderPath, "--force"], {
|
|
5110
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "remove", holderPath, "--force"], {
|
|
4702
5111
|
cwd: repoRoot,
|
|
4703
5112
|
stdio: "pipe"
|
|
4704
5113
|
});
|
|
@@ -4708,7 +5117,7 @@ function removeWorktreeHoldingBranch(repoRoot, branchName, exceptDir) {
|
|
|
4708
5117
|
return null;
|
|
4709
5118
|
}
|
|
4710
5119
|
try {
|
|
4711
|
-
execFileSync8("git", ["worktree", "prune", "--expire=now"], {
|
|
5120
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "prune", "--expire=now"], {
|
|
4712
5121
|
cwd: repoRoot,
|
|
4713
5122
|
stdio: "pipe"
|
|
4714
5123
|
});
|
|
@@ -4716,13 +5125,19 @@ function removeWorktreeHoldingBranch(repoRoot, branchName, exceptDir) {
|
|
|
4716
5125
|
return holderPath;
|
|
4717
5126
|
}
|
|
4718
5127
|
function resolveRepoRoot() {
|
|
4719
|
-
return execFileSync8("git", ["rev-parse", "--show-toplevel"], {
|
|
5128
|
+
return execFileSync8("git", [...GIT_NO_HOOKS, "rev-parse", "--show-toplevel"], {
|
|
4720
5129
|
encoding: "utf-8"
|
|
4721
5130
|
}).trim();
|
|
4722
5131
|
}
|
|
4723
5132
|
function localBranchExists(branchName, repoRoot) {
|
|
4724
5133
|
try {
|
|
4725
|
-
execFileSync8("git", [
|
|
5134
|
+
execFileSync8("git", [
|
|
5135
|
+
...GIT_NO_HOOKS,
|
|
5136
|
+
"show-ref",
|
|
5137
|
+
"--verify",
|
|
5138
|
+
"--quiet",
|
|
5139
|
+
`refs/heads/${branchName}`
|
|
5140
|
+
], { cwd: repoRoot, stdio: "ignore" });
|
|
4726
5141
|
return true;
|
|
4727
5142
|
} catch {
|
|
4728
5143
|
return false;
|
|
@@ -4732,7 +5147,7 @@ function branchAheadOfItsRemote(branchName, repoRoot = resolveRepoRoot()) {
|
|
|
4732
5147
|
if (!localBranchExists(branchName, repoRoot))
|
|
4733
5148
|
return false;
|
|
4734
5149
|
try {
|
|
4735
|
-
const out = execFileSync8("git", ["rev-list", branchName, "--not", "--remotes=origin"], { cwd: repoRoot, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
5150
|
+
const out = execFileSync8("git", [...GIT_NO_HOOKS, "rev-list", branchName, "--not", "--remotes=origin"], { cwd: repoRoot, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
4736
5151
|
return out.length > 0;
|
|
4737
5152
|
} catch {
|
|
4738
5153
|
return false;
|
|
@@ -4785,11 +5200,13 @@ function makeBranchName(shortId, title, prefix = "agent-attempts/") {
|
|
|
4785
5200
|
// src/index.ts
|
|
4786
5201
|
var MOTOR_NAME = "harmony-harness";
|
|
4787
5202
|
export {
|
|
5203
|
+
writeOnlyDenyPaths,
|
|
4788
5204
|
waitForDevServer,
|
|
4789
5205
|
validateGitProviderCli,
|
|
4790
5206
|
upsertReviewedSha,
|
|
4791
5207
|
upsertCiReviewRequestedSha,
|
|
4792
5208
|
updateExistingPr,
|
|
5209
|
+
toolchainCacheDirectories,
|
|
4793
5210
|
testCommand,
|
|
4794
5211
|
terminateGroup,
|
|
4795
5212
|
teardownWorktree,
|
|
@@ -4802,6 +5219,7 @@ export {
|
|
|
4802
5219
|
sizingEventSource,
|
|
4803
5220
|
sizeRun,
|
|
4804
5221
|
signalGroup,
|
|
5222
|
+
secretEnvKeysToStrip,
|
|
4805
5223
|
sanitizeCiText,
|
|
4806
5224
|
sandboxRunArgs,
|
|
4807
5225
|
sandboxAvailable,
|
|
@@ -4856,7 +5274,13 @@ export {
|
|
|
4856
5274
|
isPrOnOrigin,
|
|
4857
5275
|
isInsideTree,
|
|
4858
5276
|
installCommand,
|
|
5277
|
+
implementRunToolPolicy,
|
|
5278
|
+
implementRunContainmentCliArgs,
|
|
5279
|
+
implementRunContainment,
|
|
5280
|
+
hostPersistencePaths,
|
|
5281
|
+
harmonyMcpServer,
|
|
4859
5282
|
gradeOracleRed,
|
|
5283
|
+
gitMetadataDenyPaths,
|
|
4860
5284
|
getPrStatus,
|
|
4861
5285
|
getPrFailedChecks,
|
|
4862
5286
|
getHeadSha,
|
|
@@ -4886,10 +5310,14 @@ export {
|
|
|
4886
5310
|
deriveCiStatus,
|
|
4887
5311
|
decidePrBranch,
|
|
4888
5312
|
decideConfinedTool,
|
|
5313
|
+
credentialWriteToolDeny,
|
|
5314
|
+
credentialToolDeny,
|
|
5315
|
+
credentialDirectories,
|
|
4889
5316
|
credentialAccessDeny,
|
|
4890
5317
|
createWorktree,
|
|
4891
5318
|
createPullRequest,
|
|
4892
5319
|
cooldownMsFor,
|
|
5320
|
+
containedEnv,
|
|
4893
5321
|
confineToRepo,
|
|
4894
5322
|
collectSizingOutput,
|
|
4895
5323
|
collectGateEvidence,
|
|
@@ -4908,6 +5336,7 @@ export {
|
|
|
4908
5336
|
buildCommand,
|
|
4909
5337
|
branchAheadOfItsRemote,
|
|
4910
5338
|
attemptAutoFix,
|
|
5339
|
+
assertNoProjectSandboxOverride,
|
|
4911
5340
|
_resetCache,
|
|
4912
5341
|
__resetSandboxProbe,
|
|
4913
5342
|
WorktreeBaseError,
|
|
@@ -4916,6 +5345,7 @@ export {
|
|
|
4916
5345
|
SDK_ALLOWED_TOOLS,
|
|
4917
5346
|
SANDBOX_MOUNT,
|
|
4918
5347
|
ReviewPassedCollector,
|
|
5348
|
+
ProjectSandboxOverrideError,
|
|
4919
5349
|
OracleRedCollector,
|
|
4920
5350
|
OracleCollector,
|
|
4921
5351
|
ORACLE_RUNNER_HINTS,
|
|
@@ -4925,8 +5355,10 @@ export {
|
|
|
4925
5355
|
MAX_IMPLEMENT_MODEL,
|
|
4926
5356
|
MAX_CHANGED_FILES,
|
|
4927
5357
|
JUDGE_MODEL,
|
|
5358
|
+
IMPLEMENT_ALLOWED_DOMAINS,
|
|
4928
5359
|
HarmonyClient,
|
|
4929
5360
|
HARMONY_CREDENTIAL_KEYS,
|
|
5361
|
+
GIT_NO_HOOKS,
|
|
4930
5362
|
GATE_CONFIG_ERROR_MARK,
|
|
4931
5363
|
GATE_CONFIG_ERROR_KEY,
|
|
4932
5364
|
DevServerReadinessError,
|