@gethmy/harness 1.3.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 +535 -69
- package/dist/index.js +697 -229
- 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 +263 -59
package/dist/index.js
CHANGED
|
@@ -556,6 +556,13 @@ var init_playbookStage = __esm(() => {
|
|
|
556
556
|
|
|
557
557
|
// ../harmony-shared/dist/projectTemplates.js
|
|
558
558
|
var init_projectTemplates = () => {};
|
|
559
|
+
|
|
560
|
+
// ../harmony-shared/dist/realtimeChannel.js
|
|
561
|
+
var inFlightDetach;
|
|
562
|
+
var init_realtimeChannel = __esm(() => {
|
|
563
|
+
init_logger();
|
|
564
|
+
inFlightDetach = new WeakMap;
|
|
565
|
+
});
|
|
559
566
|
// ../harmony-shared/dist/reviewTools.js
|
|
560
567
|
var REVIEW_DISALLOWED_TOOLS;
|
|
561
568
|
var init_reviewTools = __esm(() => {
|
|
@@ -577,7 +584,6 @@ var init_stageHandoff = __esm(() => {
|
|
|
577
584
|
|
|
578
585
|
// ../harmony-shared/dist/types.js
|
|
579
586
|
var init_types = () => {};
|
|
580
|
-
|
|
581
587
|
// ../harmony-shared/dist/index.js
|
|
582
588
|
var init_dist = __esm(() => {
|
|
583
589
|
init_agentStaleness();
|
|
@@ -597,11 +603,498 @@ var init_dist = __esm(() => {
|
|
|
597
603
|
init_playbookCatalog();
|
|
598
604
|
init_playbookStage();
|
|
599
605
|
init_projectTemplates();
|
|
606
|
+
init_realtimeChannel();
|
|
600
607
|
init_reviewTools();
|
|
601
608
|
init_stageHandoff();
|
|
602
609
|
init_types();
|
|
603
610
|
});
|
|
604
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
|
+
|
|
605
1098
|
// src/git-pr.ts
|
|
606
1099
|
var exports_git_pr = {};
|
|
607
1100
|
__export(exports_git_pr, {
|
|
@@ -644,7 +1137,7 @@ function execFileAsync2() {
|
|
|
644
1137
|
}
|
|
645
1138
|
function detectGitProvider(cwd) {
|
|
646
1139
|
try {
|
|
647
|
-
const url = execFileSync7("git", ["remote", "get-url", "origin"], {
|
|
1140
|
+
const url = execFileSync7("git", [...GIT_NO_HOOKS, "remote", "get-url", "origin"], {
|
|
648
1141
|
cwd,
|
|
649
1142
|
encoding: "utf-8"
|
|
650
1143
|
}).trim();
|
|
@@ -942,7 +1435,7 @@ async function mergePullRequest(prUrl, cwd, provider, strategy, deleteBranch) {
|
|
|
942
1435
|
}
|
|
943
1436
|
function getHeadSha(cwd) {
|
|
944
1437
|
try {
|
|
945
|
-
return execFileSync7("git", ["rev-parse", "HEAD"], {
|
|
1438
|
+
return execFileSync7("git", [...GIT_NO_HOOKS, "rev-parse", "HEAD"], {
|
|
946
1439
|
cwd,
|
|
947
1440
|
encoding: "utf-8"
|
|
948
1441
|
}).trim();
|
|
@@ -1119,7 +1612,13 @@ function resolvePrUrl(description, branchName, cwd, provider) {
|
|
|
1119
1612
|
}
|
|
1120
1613
|
function remoteBranchExists(branchName, cwd) {
|
|
1121
1614
|
try {
|
|
1122
|
-
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" });
|
|
1123
1622
|
return true;
|
|
1124
1623
|
} catch {
|
|
1125
1624
|
return false;
|
|
@@ -1130,21 +1629,21 @@ function pushBranch(branchName, cwd) {
|
|
|
1130
1629
|
log.info(TAG13, `Remote branch ${branchName} exists (rework), force-pushing`);
|
|
1131
1630
|
let expectedSha = null;
|
|
1132
1631
|
try {
|
|
1133
|
-
execFileSync7("git", ["fetch", "origin", branchName], {
|
|
1632
|
+
execFileSync7("git", [...GIT_NO_HOOKS, "fetch", "origin", branchName], {
|
|
1134
1633
|
cwd,
|
|
1135
1634
|
stdio: "pipe"
|
|
1136
1635
|
});
|
|
1137
|
-
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();
|
|
1138
1637
|
} catch (err) {
|
|
1139
1638
|
log.warn(TAG13, `could not resolve remote tip for ${branchName}, falling back to weak lease: ${err instanceof Error ? err.message : err}`);
|
|
1140
1639
|
}
|
|
1141
1640
|
const lease = expectedSha ? `--force-with-lease=refs/heads/${branchName}:${expectedSha}` : "--force-with-lease";
|
|
1142
|
-
execFileSync7("git", ["push", lease, "-u", "origin", branchName], {
|
|
1641
|
+
execFileSync7("git", [...GIT_NO_HOOKS, "push", lease, "-u", "origin", branchName], {
|
|
1143
1642
|
cwd,
|
|
1144
1643
|
stdio: "pipe"
|
|
1145
1644
|
});
|
|
1146
1645
|
} else {
|
|
1147
|
-
execFileSync7("git", ["push", "-u", "origin", branchName], {
|
|
1646
|
+
execFileSync7("git", [...GIT_NO_HOOKS, "push", "-u", "origin", branchName], {
|
|
1148
1647
|
cwd,
|
|
1149
1648
|
stdio: "pipe"
|
|
1150
1649
|
});
|
|
@@ -1155,7 +1654,7 @@ function renameRemoteBranch(oldRef, newRef, cwd) {
|
|
|
1155
1654
|
return;
|
|
1156
1655
|
let sha;
|
|
1157
1656
|
try {
|
|
1158
|
-
sha = execFileSync7("git", ["rev-parse", "HEAD"], {
|
|
1657
|
+
sha = execFileSync7("git", [...GIT_NO_HOOKS, "rev-parse", "HEAD"], {
|
|
1159
1658
|
cwd,
|
|
1160
1659
|
encoding: "utf-8"
|
|
1161
1660
|
}).trim();
|
|
@@ -1163,9 +1662,15 @@ function renameRemoteBranch(oldRef, newRef, cwd) {
|
|
|
1163
1662
|
throw new Error(`renameRemoteBranch: could not resolve HEAD: ${err instanceof Error ? err.message : err}`);
|
|
1164
1663
|
}
|
|
1165
1664
|
log.info(TAG13, `Renaming remote ${oldRef} → ${newRef}`);
|
|
1166
|
-
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" });
|
|
1167
1672
|
try {
|
|
1168
|
-
execFileSync7("git", ["push", "origin", `:refs/heads/${oldRef}`], {
|
|
1673
|
+
execFileSync7("git", [...GIT_NO_HOOKS, "push", "origin", `:refs/heads/${oldRef}`], {
|
|
1169
1674
|
cwd,
|
|
1170
1675
|
stdio: "pipe"
|
|
1171
1676
|
});
|
|
@@ -1173,7 +1678,7 @@ function renameRemoteBranch(oldRef, newRef, cwd) {
|
|
|
1173
1678
|
log.warn(TAG13, `renameRemoteBranch: could not delete old ref ${oldRef}: ${err instanceof Error ? err.message : err}`);
|
|
1174
1679
|
}
|
|
1175
1680
|
try {
|
|
1176
|
-
execFileSync7("git", ["branch", "-m", oldRef, newRef], {
|
|
1681
|
+
execFileSync7("git", [...GIT_NO_HOOKS, "branch", "-m", oldRef, newRef], {
|
|
1177
1682
|
cwd,
|
|
1178
1683
|
stdio: "pipe"
|
|
1179
1684
|
});
|
|
@@ -1181,7 +1686,7 @@ function renameRemoteBranch(oldRef, newRef, cwd) {
|
|
|
1181
1686
|
}
|
|
1182
1687
|
function getBranchWebUrl(branchName, cwd) {
|
|
1183
1688
|
try {
|
|
1184
|
-
const remoteUrl = execFileSync7("git", ["remote", "get-url", "origin"], {
|
|
1689
|
+
const remoteUrl = execFileSync7("git", [...GIT_NO_HOOKS, "remote", "get-url", "origin"], {
|
|
1185
1690
|
cwd,
|
|
1186
1691
|
encoding: "utf-8"
|
|
1187
1692
|
}).trim();
|
|
@@ -1234,7 +1739,12 @@ function createPullRequest(card, branchName, worktreePath, config, provider, exi
|
|
|
1234
1739
|
}
|
|
1235
1740
|
let commitLog = "";
|
|
1236
1741
|
try {
|
|
1237
|
-
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();
|
|
1238
1748
|
} catch {
|
|
1239
1749
|
commitLog = "(unable to retrieve commit log)";
|
|
1240
1750
|
}
|
|
@@ -1342,6 +1852,7 @@ var cachedExecFileAsync2, TAG13 = "git-pr", VALID_PR_URL_RE, PR_URL_RE, REVIEWED
|
|
|
1342
1852
|
var init_git_pr = __esm(() => {
|
|
1343
1853
|
init_dist();
|
|
1344
1854
|
init_log();
|
|
1855
|
+
init_run_containment();
|
|
1345
1856
|
VALID_PR_URL_RE = /^https:\/\/(github\.com|gitlab\.com|dev\.azure\.com|bitbucket\.org)\//;
|
|
1346
1857
|
PR_URL_RE = /PR:\s*(https?:\/\/[^\s)]+)/;
|
|
1347
1858
|
REVIEWED_SHA_RE = /^Reviewed-SHA:\s*([0-9a-f]{7,40})\s*$/im;
|
|
@@ -1627,6 +2138,7 @@ class SdkAgentRunner {
|
|
|
1627
2138
|
...this.cfg.settingSources ? { settingSources: this.cfg.settingSources } : {},
|
|
1628
2139
|
...this.cfg.mcpServers ? { mcpServers: this.cfg.mcpServers } : {},
|
|
1629
2140
|
...this.cfg.strictMcpConfig ? { strictMcpConfig: true } : {},
|
|
2141
|
+
...this.cfg.sandbox ? { sandbox: this.cfg.sandbox } : {},
|
|
1630
2142
|
stderr: (data) => {
|
|
1631
2143
|
this.capturedStderr += data;
|
|
1632
2144
|
},
|
|
@@ -2043,6 +2555,7 @@ class ArtifactCollector {
|
|
|
2043
2555
|
}
|
|
2044
2556
|
// src/ci-failure.ts
|
|
2045
2557
|
init_log();
|
|
2558
|
+
init_run_containment();
|
|
2046
2559
|
import { execFile, execFileSync } from "node:child_process";
|
|
2047
2560
|
import { promisify } from "node:util";
|
|
2048
2561
|
function createExecFileAsync() {
|
|
@@ -2070,7 +2583,7 @@ function isPrOnOrigin(prUrl, cwd) {
|
|
|
2070
2583
|
if (!prSlug)
|
|
2071
2584
|
return false;
|
|
2072
2585
|
try {
|
|
2073
|
-
const remote = execFileSync("git", ["remote", "get-url", "origin"], {
|
|
2586
|
+
const remote = execFileSync("git", [...GIT_NO_HOOKS, "remote", "get-url", "origin"], {
|
|
2074
2587
|
cwd,
|
|
2075
2588
|
encoding: "utf-8",
|
|
2076
2589
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -2222,14 +2735,14 @@ var MAX_METRIC_TIMEOUT_MS = 900000;
|
|
|
2222
2735
|
var METRIC_SIGINT_GRACE_MS = 2000;
|
|
2223
2736
|
var METRIC_SIGTERM_GRACE_MS = 3000;
|
|
2224
2737
|
var STDIO_DRAIN_GRACE_MS = 500;
|
|
2225
|
-
function parseParseMode(
|
|
2226
|
-
if (typeof
|
|
2738
|
+
function parseParseMode(parse2) {
|
|
2739
|
+
if (typeof parse2 !== "string" || parse2.length === 0) {
|
|
2227
2740
|
return { kind: "invalid", reason: "`parse` must be a non-empty string" };
|
|
2228
2741
|
}
|
|
2229
|
-
if (
|
|
2742
|
+
if (parse2 === "number")
|
|
2230
2743
|
return { kind: "number" };
|
|
2231
|
-
if (
|
|
2232
|
-
const path =
|
|
2744
|
+
if (parse2.startsWith("json:")) {
|
|
2745
|
+
const path = parse2.slice("json:".length).trim();
|
|
2233
2746
|
if (!path) {
|
|
2234
2747
|
return { kind: "invalid", reason: '`parse` "json:" is missing a path' };
|
|
2235
2748
|
}
|
|
@@ -2237,7 +2750,7 @@ function parseParseMode(parse) {
|
|
|
2237
2750
|
}
|
|
2238
2751
|
return {
|
|
2239
2752
|
kind: "invalid",
|
|
2240
|
-
reason: `unknown \`parse\` mode "${
|
|
2753
|
+
reason: `unknown \`parse\` mode "${parse2}" (expected "number" or "json:<path>")`
|
|
2241
2754
|
};
|
|
2242
2755
|
}
|
|
2243
2756
|
function parseMetricValue(mode, stdout) {
|
|
@@ -2283,7 +2796,7 @@ function parseMetricValue(mode, stdout) {
|
|
|
2283
2796
|
return { ok: true, value: resolved };
|
|
2284
2797
|
}
|
|
2285
2798
|
function runMetricCommand(args) {
|
|
2286
|
-
return new Promise((
|
|
2799
|
+
return new Promise((resolve2, reject) => {
|
|
2287
2800
|
let child;
|
|
2288
2801
|
try {
|
|
2289
2802
|
child = spawnInGroup(args.command, args.args, {
|
|
@@ -2314,7 +2827,7 @@ function runMetricCommand(args) {
|
|
|
2314
2827
|
if (failure)
|
|
2315
2828
|
reject(failure);
|
|
2316
2829
|
else
|
|
2317
|
-
|
|
2830
|
+
resolve2(Buffer.concat(chunks).toString("utf8"));
|
|
2318
2831
|
};
|
|
2319
2832
|
const killTree = (reason) => {
|
|
2320
2833
|
if (settled || killReason)
|
|
@@ -2462,123 +2975,20 @@ function describeRunFailure(err, timeoutMs) {
|
|
|
2462
2975
|
function truncate(value, max) {
|
|
2463
2976
|
return value.length <= max ? value : `${value.slice(0, max)}…[truncated]`;
|
|
2464
2977
|
}
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
Read: ["file_path", "path", "notebook_path"],
|
|
2470
|
-
Grep: ["path"],
|
|
2471
|
-
Glob: ["path"]
|
|
2472
|
-
};
|
|
2473
|
-
var WRITE_PATH_ARGS = {
|
|
2474
|
-
Write: ["file_path"],
|
|
2475
|
-
Edit: ["file_path"],
|
|
2476
|
-
MultiEdit: ["file_path"],
|
|
2477
|
-
NotebookEdit: ["notebook_path"]
|
|
2478
|
-
};
|
|
2479
|
-
var CONFINED_READ_TOOLS = Object.freeze(Object.keys(READ_PATH_ARGS));
|
|
2480
|
-
var CONFINED_WRITE_TOOLS = Object.freeze([
|
|
2481
|
-
...Object.keys(READ_PATH_ARGS),
|
|
2482
|
-
...Object.keys(WRITE_PATH_ARGS).filter((t) => t !== "MultiEdit")
|
|
2483
|
-
]);
|
|
2484
|
-
var PATTERN_ARG_BY_TOOL = {
|
|
2485
|
-
Glob: ["pattern"],
|
|
2486
|
-
Grep: ["glob"]
|
|
2487
|
-
};
|
|
2488
|
-
function isGitMetadata(repoRoot, candidate) {
|
|
2489
|
-
const rel = candidate.startsWith(repoRoot) ? candidate.slice(repoRoot.length) : candidate;
|
|
2490
|
-
return rel.split(/[\\/]/).some((segment) => segment.toLowerCase() === ".git");
|
|
2491
|
-
}
|
|
2492
|
-
function patternEscapes(pattern) {
|
|
2493
|
-
if (isAbsolute(pattern))
|
|
2494
|
-
return true;
|
|
2495
|
-
if (/[{}[\]~()!+@]/.test(pattern))
|
|
2496
|
-
return true;
|
|
2497
|
-
return pattern.split(/[\\/]/).some((segment) => segment === "..");
|
|
2498
|
-
}
|
|
2499
|
-
function pathArgsFor(mode) {
|
|
2500
|
-
return mode === "write" ? { ...READ_PATH_ARGS, ...WRITE_PATH_ARGS } : READ_PATH_ARGS;
|
|
2501
|
-
}
|
|
2502
|
-
function realPathOrNearest(p) {
|
|
2503
|
-
const abs = isAbsolute(p) ? p : resolve(p);
|
|
2504
|
-
const { root } = parse(abs);
|
|
2505
|
-
let real = root;
|
|
2506
|
-
for (const part of abs.slice(root.length).split(sep)) {
|
|
2507
|
-
if (part === "" || part === ".")
|
|
2508
|
-
continue;
|
|
2509
|
-
if (part === "..") {
|
|
2510
|
-
real = dirname(real);
|
|
2511
|
-
continue;
|
|
2512
|
-
}
|
|
2513
|
-
const next = real.endsWith(sep) ? real + part : real + sep + part;
|
|
2514
|
-
try {
|
|
2515
|
-
real = realpathSync(next);
|
|
2516
|
-
} catch {
|
|
2517
|
-
real = next;
|
|
2518
|
-
}
|
|
2519
|
-
}
|
|
2520
|
-
return real;
|
|
2521
|
-
}
|
|
2522
|
-
function isInsideTree(root, target) {
|
|
2523
|
-
const normalizedRoot = realPathOrNearest(root);
|
|
2524
|
-
const normalizedTarget = realPathOrNearest(target);
|
|
2525
|
-
if (normalizedTarget === normalizedRoot)
|
|
2526
|
-
return true;
|
|
2527
|
-
return normalizedTarget.startsWith(normalizedRoot + sep);
|
|
2528
|
-
}
|
|
2529
|
-
function decideConfinedTool(repoRoot, toolName, input, mode = "read") {
|
|
2530
|
-
const pathArgs = pathArgsFor(mode)[toolName];
|
|
2531
|
-
if (!pathArgs) {
|
|
2532
|
-
return {
|
|
2533
|
-
behavior: "deny",
|
|
2534
|
-
message: `${toolName} is not available to this run.`
|
|
2535
|
-
};
|
|
2536
|
-
}
|
|
2537
|
-
const verb = toolName in WRITE_PATH_ARGS ? "write" : "read";
|
|
2538
|
-
for (const key of PATTERN_ARG_BY_TOOL[toolName] ?? []) {
|
|
2539
|
-
const value = input[key];
|
|
2540
|
-
if (typeof value !== "string" || value.length === 0)
|
|
2541
|
-
continue;
|
|
2542
|
-
if (patternEscapes(value)) {
|
|
2543
|
-
return {
|
|
2544
|
-
behavior: "deny",
|
|
2545
|
-
message: `${toolName} patterns must stay inside the repository — no absolute path and no "..". Refused: ${value}`
|
|
2546
|
-
};
|
|
2547
|
-
}
|
|
2548
|
-
}
|
|
2549
|
-
for (const key of pathArgs) {
|
|
2550
|
-
const value = input[key];
|
|
2551
|
-
if (typeof value !== "string" || value.length === 0)
|
|
2552
|
-
continue;
|
|
2553
|
-
const candidate = isAbsolute(value) ? value : `${repoRoot}${sep}${value}`;
|
|
2554
|
-
if (isGitMetadata(repoRoot, candidate)) {
|
|
2555
|
-
return {
|
|
2556
|
-
behavior: "deny",
|
|
2557
|
-
message: `${toolName} may not touch the repository's git metadata. Refused: ${value}`
|
|
2558
|
-
};
|
|
2559
|
-
}
|
|
2560
|
-
if (!isInsideTree(repoRoot, candidate)) {
|
|
2561
|
-
return {
|
|
2562
|
-
behavior: "deny",
|
|
2563
|
-
message: `${toolName} may only ${verb} inside the repository. Refused: ${value}`
|
|
2564
|
-
};
|
|
2565
|
-
}
|
|
2566
|
-
}
|
|
2567
|
-
return { behavior: "allow" };
|
|
2568
|
-
}
|
|
2569
|
-
function confineToRepo(repoRoot, mode = "read") {
|
|
2570
|
-
return async (toolName, input) => decideConfinedTool(repoRoot, toolName, input, mode);
|
|
2571
|
-
}
|
|
2978
|
+
|
|
2979
|
+
// src/index.ts
|
|
2980
|
+
init_confine_to_repo();
|
|
2981
|
+
|
|
2572
2982
|
// src/gate-collectors.ts
|
|
2573
2983
|
init_dist();
|
|
2574
2984
|
init_log();
|
|
2575
2985
|
|
|
2576
2986
|
// src/oracle-collector.ts
|
|
2577
|
-
import { createHash } from "node:crypto";
|
|
2987
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
2578
2988
|
init_log();
|
|
2579
2989
|
|
|
2580
2990
|
// src/oracle.ts
|
|
2581
|
-
import { lstatSync, readFileSync, statSync } from "node:fs";
|
|
2991
|
+
import { lstatSync, readFileSync as readFileSync2, statSync } from "node:fs";
|
|
2582
2992
|
import {
|
|
2583
2993
|
chmod,
|
|
2584
2994
|
lstat,
|
|
@@ -2588,13 +2998,13 @@ import {
|
|
|
2588
2998
|
rm,
|
|
2589
2999
|
writeFile
|
|
2590
3000
|
} from "node:fs/promises";
|
|
2591
|
-
import { tmpdir } from "node:os";
|
|
2592
|
-
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";
|
|
2593
3003
|
import { StringDecoder } from "node:string_decoder";
|
|
2594
3004
|
init_log();
|
|
2595
3005
|
var TAG5 = "oracle";
|
|
2596
3006
|
async function resolveContained(repoPath, relativePath) {
|
|
2597
|
-
if (
|
|
3007
|
+
if (isAbsolute3(relativePath)) {
|
|
2598
3008
|
throw new Error(`refusing to place an oracle at an absolute path: ${relativePath}`);
|
|
2599
3009
|
}
|
|
2600
3010
|
if (relativePath === "" || relativePath === ".") {
|
|
@@ -2617,7 +3027,7 @@ async function resolveContained(repoPath, relativePath) {
|
|
|
2617
3027
|
}
|
|
2618
3028
|
async function place(repoPath, oracle) {
|
|
2619
3029
|
const target = await resolveContained(repoPath, oracle.path);
|
|
2620
|
-
await mkdir(
|
|
3030
|
+
await mkdir(dirname3(target), { recursive: true });
|
|
2621
3031
|
await writeFile(target, oracle.content, "utf8");
|
|
2622
3032
|
}
|
|
2623
3033
|
async function remove(repoPath, oracle) {
|
|
@@ -2725,8 +3135,8 @@ function argvPath(path) {
|
|
|
2725
3135
|
}
|
|
2726
3136
|
async function runHeldOracle(repoPath, oracle, timeoutMs = DEFAULT_METRIC_TIMEOUT_MS) {
|
|
2727
3137
|
const spec = resolveOracleRunnerSpec(oracle);
|
|
2728
|
-
const reportDir = await mkdtemp(
|
|
2729
|
-
const reportPath =
|
|
3138
|
+
const reportDir = await mkdtemp(join2(tmpdir2(), reportDirPrefix()));
|
|
3139
|
+
const reportPath = join2(reportDir, spec.report.file);
|
|
2730
3140
|
try {
|
|
2731
3141
|
return await spawnHeldOracle(spec, repoPath, oracle, reportPath, timeoutMs);
|
|
2732
3142
|
} finally {
|
|
@@ -2737,7 +3147,7 @@ function reportDirPrefix() {
|
|
|
2737
3147
|
return "harmony-oracle-report-";
|
|
2738
3148
|
}
|
|
2739
3149
|
function assertUntampered(reportPath) {
|
|
2740
|
-
const dir = statSync(
|
|
3150
|
+
const dir = statSync(dirname3(reportPath));
|
|
2741
3151
|
if ((dir.mode & 511) !== 448) {
|
|
2742
3152
|
throw new Error(`the oracle report directory's mode changed to ${(dir.mode & 511).toString(8)} — refusing the report`);
|
|
2743
3153
|
}
|
|
@@ -2800,7 +3210,7 @@ ${output}` : output;
|
|
|
2800
3210
|
const captureReport = () => {
|
|
2801
3211
|
try {
|
|
2802
3212
|
assertUntampered(reportPath);
|
|
2803
|
-
report = spec.report.parse(
|
|
3213
|
+
report = spec.report.parse(readFileSync2(reportPath, "utf8"));
|
|
2804
3214
|
} catch (err) {
|
|
2805
3215
|
report = null;
|
|
2806
3216
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -2918,7 +3328,7 @@ class HeldOracleCollector {
|
|
|
2918
3328
|
async runHeld(oracle) {
|
|
2919
3329
|
const identity = {
|
|
2920
3330
|
oracleId: oracle.id ?? null,
|
|
2921
|
-
contentHash:
|
|
3331
|
+
contentHash: createHash2("sha256").update(oracle.content).digest("hex")
|
|
2922
3332
|
};
|
|
2923
3333
|
await this.deps.place(this.deps.repoPath, oracle);
|
|
2924
3334
|
try {
|
|
@@ -3034,6 +3444,7 @@ import { execFileSync as execFileSync5, spawn as spawn2 } from "node:child_proce
|
|
|
3034
3444
|
|
|
3035
3445
|
// src/pm.ts
|
|
3036
3446
|
init_log();
|
|
3447
|
+
init_run_containment();
|
|
3037
3448
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
3038
3449
|
import { existsSync } from "node:fs";
|
|
3039
3450
|
var TAG7 = "pm";
|
|
@@ -3043,7 +3454,7 @@ function detectPackageManager() {
|
|
|
3043
3454
|
return cached;
|
|
3044
3455
|
let repoRoot;
|
|
3045
3456
|
try {
|
|
3046
|
-
repoRoot = execFileSync2("git", ["rev-parse", "--show-toplevel"], {
|
|
3457
|
+
repoRoot = execFileSync2("git", [...GIT_NO_HOOKS, "rev-parse", "--show-toplevel"], {
|
|
3047
3458
|
encoding: "utf-8"
|
|
3048
3459
|
}).trim();
|
|
3049
3460
|
} catch {
|
|
@@ -3086,7 +3497,7 @@ function spawnRunArgs(script, ...extra) {
|
|
|
3086
3497
|
// src/project-type.ts
|
|
3087
3498
|
init_log();
|
|
3088
3499
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
3089
|
-
import { existsSync as existsSync2, readdirSync, readFileSync as
|
|
3500
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "node:fs";
|
|
3090
3501
|
var TAG8 = "project-type";
|
|
3091
3502
|
var _cache = new Map;
|
|
3092
3503
|
function _resetCache() {
|
|
@@ -3189,7 +3600,7 @@ var NPM_PLACEHOLDER_TEST = /no test specified/i;
|
|
|
3189
3600
|
function hasNodeTestScript(dir) {
|
|
3190
3601
|
let script;
|
|
3191
3602
|
try {
|
|
3192
|
-
const pkg = JSON.parse(
|
|
3603
|
+
const pkg = JSON.parse(readFileSync3(`${dir}/package.json`, "utf-8"));
|
|
3193
3604
|
script = pkg.scripts?.test;
|
|
3194
3605
|
} catch (err) {
|
|
3195
3606
|
log.warn(TAG8, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
|
|
@@ -3206,7 +3617,7 @@ function hasNodeTestScript(dir) {
|
|
|
3206
3617
|
function firstNodeScript(dir, candidates) {
|
|
3207
3618
|
let scripts;
|
|
3208
3619
|
try {
|
|
3209
|
-
const pkg = JSON.parse(
|
|
3620
|
+
const pkg = JSON.parse(readFileSync3(`${dir}/package.json`, "utf-8"));
|
|
3210
3621
|
scripts = pkg.scripts ?? {};
|
|
3211
3622
|
} catch (err) {
|
|
3212
3623
|
log.warn(TAG8, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
|
|
@@ -3263,6 +3674,7 @@ function resolveXcodeScheme(pt) {
|
|
|
3263
3674
|
|
|
3264
3675
|
// src/revert-guard.ts
|
|
3265
3676
|
init_log();
|
|
3677
|
+
init_run_containment();
|
|
3266
3678
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
3267
3679
|
var TAG9 = "revert-guard";
|
|
3268
3680
|
var TEST_FILE = /(?:^|\/)__tests__\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
@@ -3274,7 +3686,7 @@ function filterTestFiles(paths) {
|
|
|
3274
3686
|
}
|
|
3275
3687
|
function refetchBase(worktreePath, baseBranch) {
|
|
3276
3688
|
try {
|
|
3277
|
-
execFileSync4("git", ["fetch", "origin", baseBranch], {
|
|
3689
|
+
execFileSync4("git", [...GIT_NO_HOOKS, "fetch", "origin", baseBranch], {
|
|
3278
3690
|
cwd: worktreePath,
|
|
3279
3691
|
stdio: "pipe"
|
|
3280
3692
|
});
|
|
@@ -3284,7 +3696,13 @@ function refetchBase(worktreePath, baseBranch) {
|
|
|
3284
3696
|
}
|
|
3285
3697
|
function listDeletedFilesAgainstBase(worktreePath, baseBranch) {
|
|
3286
3698
|
try {
|
|
3287
|
-
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" });
|
|
3288
3706
|
return out.split(`
|
|
3289
3707
|
`).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
3290
3708
|
} catch (err) {
|
|
@@ -3298,6 +3716,7 @@ function findDeletedTestFiles(worktreePath, baseBranch) {
|
|
|
3298
3716
|
}
|
|
3299
3717
|
|
|
3300
3718
|
// src/verification.ts
|
|
3719
|
+
init_run_containment();
|
|
3301
3720
|
var TAG10 = "verification";
|
|
3302
3721
|
var MAX_OUTPUT_BUFFER2 = 64 * 1024 * 1024;
|
|
3303
3722
|
async function runVerification(worktreePath, config, workerId) {
|
|
@@ -3371,7 +3790,8 @@ function runBuild(worktreePath, timeout) {
|
|
|
3371
3790
|
cwd: worktreePath,
|
|
3372
3791
|
timeout,
|
|
3373
3792
|
stdio: "pipe",
|
|
3374
|
-
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
3793
|
+
maxBuffer: MAX_OUTPUT_BUFFER2,
|
|
3794
|
+
env: containedEnv()
|
|
3375
3795
|
});
|
|
3376
3796
|
return [];
|
|
3377
3797
|
} catch (err) {
|
|
@@ -3389,7 +3809,8 @@ function runTests(worktreePath, timeout) {
|
|
|
3389
3809
|
cwd: worktreePath,
|
|
3390
3810
|
timeout,
|
|
3391
3811
|
stdio: "pipe",
|
|
3392
|
-
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
3812
|
+
maxBuffer: MAX_OUTPUT_BUFFER2,
|
|
3813
|
+
env: containedEnv()
|
|
3393
3814
|
});
|
|
3394
3815
|
return [];
|
|
3395
3816
|
} catch (err) {
|
|
@@ -3408,7 +3829,8 @@ function runFormatFix(worktreePath, timeout, workerId) {
|
|
|
3408
3829
|
cwd: worktreePath,
|
|
3409
3830
|
timeout,
|
|
3410
3831
|
stdio: "pipe",
|
|
3411
|
-
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
3832
|
+
maxBuffer: MAX_OUTPUT_BUFFER2,
|
|
3833
|
+
env: containedEnv()
|
|
3412
3834
|
});
|
|
3413
3835
|
log.info(TAG10, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
|
|
3414
3836
|
} catch (err) {
|
|
@@ -3426,7 +3848,8 @@ function runLint(worktreePath, timeout) {
|
|
|
3426
3848
|
cwd: worktreePath,
|
|
3427
3849
|
timeout,
|
|
3428
3850
|
stdio: "pipe",
|
|
3429
|
-
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
3851
|
+
maxBuffer: MAX_OUTPUT_BUFFER2,
|
|
3852
|
+
env: containedEnv()
|
|
3430
3853
|
});
|
|
3431
3854
|
return [];
|
|
3432
3855
|
} catch (err) {
|
|
@@ -3444,7 +3867,8 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
3444
3867
|
const [cmd, args] = spawnRunArgs("dev", "--port", String(port));
|
|
3445
3868
|
devServer = spawn2(cmd, args, {
|
|
3446
3869
|
cwd: worktreePath,
|
|
3447
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3870
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3871
|
+
env: containedEnv()
|
|
3448
3872
|
});
|
|
3449
3873
|
try {
|
|
3450
3874
|
await waitForDevServer(devServer, 30000);
|
|
@@ -3455,7 +3879,7 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
3455
3879
|
}
|
|
3456
3880
|
let diff = "";
|
|
3457
3881
|
try {
|
|
3458
|
-
diff = execFileSync5("git", ["diff", `origin/${config.worktree.baseBranch}..HEAD`], {
|
|
3882
|
+
diff = execFileSync5("git", [...GIT_NO_HOOKS, "diff", `origin/${config.worktree.baseBranch}..HEAD`], {
|
|
3459
3883
|
cwd: worktreePath,
|
|
3460
3884
|
encoding: "utf-8",
|
|
3461
3885
|
timeout: 30000,
|
|
@@ -3476,14 +3900,16 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
3476
3900
|
"```"
|
|
3477
3901
|
].join(`
|
|
3478
3902
|
`);
|
|
3479
|
-
const leanSources = config.claude.leanSettingSources;
|
|
3480
3903
|
const output = execFileSync5("claude", [
|
|
3481
3904
|
"--print",
|
|
3482
3905
|
"--model",
|
|
3483
3906
|
"sonnet",
|
|
3484
3907
|
"--max-turns",
|
|
3485
3908
|
"10",
|
|
3486
|
-
...
|
|
3909
|
+
...implementRunContainmentCliArgs({
|
|
3910
|
+
worktree: worktreePath,
|
|
3911
|
+
readOnly: true
|
|
3912
|
+
}),
|
|
3487
3913
|
"--",
|
|
3488
3914
|
reviewPrompt
|
|
3489
3915
|
], {
|
|
@@ -3491,7 +3917,8 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
3491
3917
|
encoding: "utf-8",
|
|
3492
3918
|
timeout: config.verification.timeout,
|
|
3493
3919
|
stdio: "pipe",
|
|
3494
|
-
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
3920
|
+
maxBuffer: MAX_OUTPUT_BUFFER2,
|
|
3921
|
+
env: containedEnv()
|
|
3495
3922
|
});
|
|
3496
3923
|
return parseReviewFindings(output);
|
|
3497
3924
|
} catch (err) {
|
|
@@ -3521,7 +3948,6 @@ function attemptAutoFix(worktreePath, config, errors) {
|
|
|
3521
3948
|
"```"
|
|
3522
3949
|
].join(`
|
|
3523
3950
|
`);
|
|
3524
|
-
const leanSources = config.claude.leanSettingSources;
|
|
3525
3951
|
const args = [
|
|
3526
3952
|
"--print",
|
|
3527
3953
|
"--model",
|
|
@@ -3530,7 +3956,7 @@ function attemptAutoFix(worktreePath, config, errors) {
|
|
|
3530
3956
|
"50",
|
|
3531
3957
|
"--allowedTools",
|
|
3532
3958
|
"Bash,Read,Write,Edit,Glob,Grep",
|
|
3533
|
-
...
|
|
3959
|
+
...implementRunContainmentCliArgs({ worktree: worktreePath }),
|
|
3534
3960
|
"--",
|
|
3535
3961
|
fixPrompt
|
|
3536
3962
|
];
|
|
@@ -3539,7 +3965,8 @@ function attemptAutoFix(worktreePath, config, errors) {
|
|
|
3539
3965
|
cwd: worktreePath,
|
|
3540
3966
|
timeout: config.verification.timeout,
|
|
3541
3967
|
stdio: "pipe",
|
|
3542
|
-
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
3968
|
+
maxBuffer: MAX_OUTPUT_BUFFER2,
|
|
3969
|
+
env: containedEnv()
|
|
3543
3970
|
});
|
|
3544
3971
|
}
|
|
3545
3972
|
async function reportFindings(client, cardId, result, recovery) {
|
|
@@ -3872,6 +4299,7 @@ async function collectGateEvidence(registry, context) {
|
|
|
3872
4299
|
}
|
|
3873
4300
|
// src/git-diff-stat.ts
|
|
3874
4301
|
init_log();
|
|
4302
|
+
init_run_containment();
|
|
3875
4303
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
3876
4304
|
var TAG12 = "git-diff-stat";
|
|
3877
4305
|
var MAX_CHANGED_FILES = 30;
|
|
@@ -3952,7 +4380,7 @@ function formatDiffSummary(diff, maxFiles = 100) {
|
|
|
3952
4380
|
}
|
|
3953
4381
|
function captureDiffStat(worktreePath, baseBranch, maxFiles = MAX_CHANGED_FILES) {
|
|
3954
4382
|
try {
|
|
3955
|
-
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 });
|
|
3956
4384
|
return parseNumstat(raw, maxFiles);
|
|
3957
4385
|
} catch (err) {
|
|
3958
4386
|
log.warn(TAG12, "git diff --numstat failed", {
|
|
@@ -4196,54 +4624,14 @@ async function runInSandbox(args) {
|
|
|
4196
4624
|
return { passed: false, output };
|
|
4197
4625
|
}
|
|
4198
4626
|
}
|
|
4199
|
-
// src/run-sizing.ts
|
|
4200
|
-
init_dist();
|
|
4201
4627
|
|
|
4202
|
-
// src/
|
|
4203
|
-
|
|
4204
|
-
import { getConfigDir } from "@gethmy/mcp/src/config.js";
|
|
4205
|
-
var HARMONY_CREDENTIAL_KEYS = [
|
|
4206
|
-
"HARMONY_API_KEY",
|
|
4207
|
-
"HARMONY_API_URL",
|
|
4208
|
-
"HARMONY_WORKSPACE_ID",
|
|
4209
|
-
"SUPABASE_ANON_KEY",
|
|
4210
|
-
"SUPABASE_SERVICE_ROLE_KEY",
|
|
4211
|
-
"SUPABASE_URL"
|
|
4212
|
-
];
|
|
4213
|
-
function mayHoldCredentials(role) {
|
|
4214
|
-
return role === "author" || role === "reviewer";
|
|
4215
|
-
}
|
|
4216
|
-
function credentialReadDeny() {
|
|
4217
|
-
return `Read(/${getConfigDir()}/**)`;
|
|
4218
|
-
}
|
|
4219
|
-
function credentialAccessDeny() {
|
|
4220
|
-
const dir = `/${getConfigDir()}/**`;
|
|
4221
|
-
return [`Read(${dir})`, `Grep(${dir})`, `Glob(${dir})`];
|
|
4222
|
-
}
|
|
4223
|
-
function buildRoleLaunch(args) {
|
|
4224
|
-
const role = normalizeStageRole(args.role);
|
|
4225
|
-
const keep = mayHoldCredentials(role);
|
|
4226
|
-
const env = {};
|
|
4227
|
-
for (const [key, value] of Object.entries(args.parentEnv)) {
|
|
4228
|
-
if (value === undefined)
|
|
4229
|
-
continue;
|
|
4230
|
-
if (!keep && HARMONY_CREDENTIAL_KEYS.includes(key))
|
|
4231
|
-
continue;
|
|
4232
|
-
env[key] = value;
|
|
4233
|
-
}
|
|
4234
|
-
return {
|
|
4235
|
-
role,
|
|
4236
|
-
prompt: args.prompt,
|
|
4237
|
-
repoPath: args.repoPath,
|
|
4238
|
-
env,
|
|
4239
|
-
disallowedTools: keep ? [] : [credentialReadDeny()]
|
|
4240
|
-
};
|
|
4241
|
-
}
|
|
4242
|
-
function envKeysDroppedByLaunch(parentEnv, launch) {
|
|
4243
|
-
return Object.keys(parentEnv).filter((key) => parentEnv[key] !== undefined && !Object.hasOwn(launch.env, key));
|
|
4244
|
-
}
|
|
4628
|
+
// src/index.ts
|
|
4629
|
+
init_run_containment();
|
|
4245
4630
|
|
|
4246
4631
|
// src/run-sizing.ts
|
|
4632
|
+
init_dist();
|
|
4633
|
+
init_confine_to_repo();
|
|
4634
|
+
init_runner();
|
|
4247
4635
|
var SIZING_MODEL = "haiku";
|
|
4248
4636
|
var SIZING_MAX_TURNS = 25;
|
|
4249
4637
|
var SIZING_MAX_BUDGET_USD = 0.75;
|
|
@@ -4417,6 +4805,10 @@ async function sizeRun(deps) {
|
|
|
4417
4805
|
clearTimeout(timer);
|
|
4418
4806
|
}
|
|
4419
4807
|
}
|
|
4808
|
+
|
|
4809
|
+
// src/index.ts
|
|
4810
|
+
init_runner();
|
|
4811
|
+
|
|
4420
4812
|
// src/stage-run.ts
|
|
4421
4813
|
async function runStage(request, deps) {
|
|
4422
4814
|
const events = [];
|
|
@@ -4446,6 +4838,7 @@ init_log();
|
|
|
4446
4838
|
import { execFileSync as execFileSync8, execSync } from "node:child_process";
|
|
4447
4839
|
import { existsSync as existsSync3, readdirSync as readdirSync2, rmSync } from "node:fs";
|
|
4448
4840
|
import { resolve as resolve3 } from "node:path";
|
|
4841
|
+
init_run_containment();
|
|
4449
4842
|
var TAG16 = "worktree";
|
|
4450
4843
|
|
|
4451
4844
|
class WorktreeBaseError extends Error {
|
|
@@ -4454,7 +4847,7 @@ class WorktreeBaseError extends Error {
|
|
|
4454
4847
|
this.name = "WorktreeBaseError";
|
|
4455
4848
|
}
|
|
4456
4849
|
}
|
|
4457
|
-
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], {
|
|
4458
4851
|
cwd: root,
|
|
4459
4852
|
stdio: "pipe"
|
|
4460
4853
|
})) {
|
|
@@ -4480,7 +4873,41 @@ function resolveWorktreeStartRef(baseBranch, branchName, continueExisting, branc
|
|
|
4480
4873
|
}
|
|
4481
4874
|
return `origin/${baseBranch}`;
|
|
4482
4875
|
}
|
|
4483
|
-
function
|
|
4876
|
+
function resolveContinuationTarget(branchName, continueRequested, failedBranchPrefix, approvedBranchPrefix, branchExistsOnOrigin) {
|
|
4877
|
+
if (branchExistsOnOrigin(branchName)) {
|
|
4878
|
+
return {
|
|
4879
|
+
branchName,
|
|
4880
|
+
continueExisting: true,
|
|
4881
|
+
existsOnOrigin: true,
|
|
4882
|
+
reason: continueRequested ? "requested" : "exists_on_origin"
|
|
4883
|
+
};
|
|
4884
|
+
}
|
|
4885
|
+
if (failedBranchPrefix && approvedBranchPrefix && branchName.startsWith(failedBranchPrefix)) {
|
|
4886
|
+
const sibling = approvedBranchPrefix + branchName.slice(failedBranchPrefix.length);
|
|
4887
|
+
if (branchExistsOnOrigin(sibling)) {
|
|
4888
|
+
return {
|
|
4889
|
+
branchName: sibling,
|
|
4890
|
+
continueExisting: true,
|
|
4891
|
+
existsOnOrigin: true,
|
|
4892
|
+
reason: "approved_rename"
|
|
4893
|
+
};
|
|
4894
|
+
}
|
|
4895
|
+
}
|
|
4896
|
+
return {
|
|
4897
|
+
branchName,
|
|
4898
|
+
continueExisting: continueRequested,
|
|
4899
|
+
existsOnOrigin: false,
|
|
4900
|
+
reason: "fresh"
|
|
4901
|
+
};
|
|
4902
|
+
}
|
|
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,
|
|
4484
4911
|
"fetch",
|
|
4485
4912
|
"origin",
|
|
4486
4913
|
`+refs/heads/${branch}:refs/remotes/origin/${branch}`
|
|
@@ -4514,7 +4941,7 @@ function fetchExistingBranch(repoRoot, branchName, attempts = 3, lsRemoteImpl =
|
|
|
4514
4941
|
}
|
|
4515
4942
|
function readWorktreeHead(worktreePath) {
|
|
4516
4943
|
try {
|
|
4517
|
-
return execFileSync8("git", ["rev-parse", "HEAD"], {
|
|
4944
|
+
return execFileSync8("git", [...GIT_NO_HOOKS, "rev-parse", "HEAD"], {
|
|
4518
4945
|
cwd: worktreePath,
|
|
4519
4946
|
encoding: "utf-8"
|
|
4520
4947
|
}).trim();
|
|
@@ -4523,7 +4950,7 @@ function readWorktreeHead(worktreePath) {
|
|
|
4523
4950
|
}
|
|
4524
4951
|
}
|
|
4525
4952
|
function createWorktree(basePath, baseBranch, branchName, opts = {}) {
|
|
4526
|
-
const repoRoot = execFileSync8("git", ["rev-parse", "--show-toplevel"], {
|
|
4953
|
+
const repoRoot = execFileSync8("git", [...GIT_NO_HOOKS, "rev-parse", "--show-toplevel"], {
|
|
4527
4954
|
encoding: "utf-8"
|
|
4528
4955
|
}).trim();
|
|
4529
4956
|
const worktreeDir = resolve3(repoRoot, basePath, branchName);
|
|
@@ -4532,46 +4959,63 @@ function createWorktree(basePath, baseBranch, branchName, opts = {}) {
|
|
|
4532
4959
|
cleanupWorktree(worktreeDir, branchName);
|
|
4533
4960
|
}
|
|
4534
4961
|
try {
|
|
4535
|
-
execFileSync8("git", ["worktree", "prune", "--expire=now"], {
|
|
4962
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "prune", "--expire=now"], {
|
|
4536
4963
|
cwd: repoRoot,
|
|
4537
4964
|
stdio: "pipe"
|
|
4538
4965
|
});
|
|
4539
4966
|
} catch {}
|
|
4540
4967
|
fetchBaseBranch(repoRoot, baseBranch);
|
|
4541
|
-
const startRef = resolveWorktreeStartRef(baseBranch, branchName, opts.continueExisting ?? false, () => fetchExistingBranch(repoRoot, branchName));
|
|
4968
|
+
const startRef = resolveWorktreeStartRef(baseBranch, branchName, opts.continueExisting ?? false, () => opts.branchExistsOnOrigin ?? fetchExistingBranch(repoRoot, branchName));
|
|
4542
4969
|
log.info(TAG16, `Creating worktree: ${worktreeDir} (branch: ${branchName}, base: ${startRef})`);
|
|
4543
4970
|
try {
|
|
4544
|
-
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" });
|
|
4545
4980
|
} catch (err) {
|
|
4546
4981
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4547
4982
|
log.warn(TAG16, `worktree add failed, attempting forced recovery: ${msg}`);
|
|
4548
4983
|
removeWorktreeHoldingBranch(repoRoot, branchName, worktreeDir);
|
|
4549
4984
|
try {
|
|
4550
|
-
execFileSync8("git", ["worktree", "remove", worktreeDir, "--force"], {
|
|
4985
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "remove", worktreeDir, "--force"], {
|
|
4551
4986
|
cwd: repoRoot,
|
|
4552
4987
|
stdio: "pipe"
|
|
4553
4988
|
});
|
|
4554
4989
|
} catch {}
|
|
4555
4990
|
try {
|
|
4556
|
-
execFileSync8("git", ["worktree", "prune", "--expire=now"], {
|
|
4991
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "prune", "--expire=now"], {
|
|
4557
4992
|
cwd: repoRoot,
|
|
4558
4993
|
stdio: "pipe"
|
|
4559
4994
|
});
|
|
4560
4995
|
} catch {}
|
|
4561
4996
|
try {
|
|
4562
|
-
execFileSync8("git", ["branch", "-D", branchName], {
|
|
4997
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "branch", "-D", branchName], {
|
|
4563
4998
|
cwd: repoRoot,
|
|
4564
4999
|
stdio: "pipe"
|
|
4565
5000
|
});
|
|
4566
5001
|
} catch {}
|
|
4567
|
-
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" });
|
|
4568
5011
|
}
|
|
4569
5012
|
log.info(TAG16, "Installing dependencies in worktree...");
|
|
4570
5013
|
try {
|
|
4571
|
-
execSync(installCommand(), {
|
|
5014
|
+
execSync(installCommand(true), {
|
|
4572
5015
|
cwd: worktreeDir,
|
|
4573
5016
|
stdio: "pipe",
|
|
4574
|
-
timeout: 60000
|
|
5017
|
+
timeout: 60000,
|
|
5018
|
+
env: containedEnv()
|
|
4575
5019
|
});
|
|
4576
5020
|
} catch {
|
|
4577
5021
|
log.warn(TAG16, "Install failed (may be fine if deps are hoisted)");
|
|
@@ -4590,7 +5034,7 @@ function containsForeignWorktrees(dir) {
|
|
|
4590
5034
|
return children.some((child) => existsSync3(resolve3(dir, child, ".git")));
|
|
4591
5035
|
}
|
|
4592
5036
|
function cleanupWorktree(worktreePath, branchName) {
|
|
4593
|
-
const repoRoot = execFileSync8("git", ["rev-parse", "--show-toplevel"], {
|
|
5037
|
+
const repoRoot = execFileSync8("git", [...GIT_NO_HOOKS, "rev-parse", "--show-toplevel"], {
|
|
4594
5038
|
encoding: "utf-8"
|
|
4595
5039
|
}).trim();
|
|
4596
5040
|
if (existsSync3(worktreePath)) {
|
|
@@ -4598,7 +5042,7 @@ function cleanupWorktree(worktreePath, branchName) {
|
|
|
4598
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).`);
|
|
4599
5043
|
}
|
|
4600
5044
|
try {
|
|
4601
|
-
execFileSync8("git", ["worktree", "remove", worktreePath, "--force"], {
|
|
5045
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "remove", worktreePath, "--force"], {
|
|
4602
5046
|
cwd: repoRoot,
|
|
4603
5047
|
stdio: "pipe"
|
|
4604
5048
|
});
|
|
@@ -4609,7 +5053,7 @@ function cleanupWorktree(worktreePath, branchName) {
|
|
|
4609
5053
|
rmSync(worktreePath, { recursive: true, force: true });
|
|
4610
5054
|
}
|
|
4611
5055
|
try {
|
|
4612
|
-
execFileSync8("git", ["worktree", "prune", "--expire=now"], {
|
|
5056
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "prune", "--expire=now"], {
|
|
4613
5057
|
cwd: repoRoot,
|
|
4614
5058
|
stdio: "pipe"
|
|
4615
5059
|
});
|
|
@@ -4617,7 +5061,7 @@ function cleanupWorktree(worktreePath, branchName) {
|
|
|
4617
5061
|
}
|
|
4618
5062
|
} else {
|
|
4619
5063
|
try {
|
|
4620
|
-
execFileSync8("git", ["worktree", "prune", "--expire=now"], {
|
|
5064
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "prune", "--expire=now"], {
|
|
4621
5065
|
cwd: repoRoot,
|
|
4622
5066
|
stdio: "pipe"
|
|
4623
5067
|
});
|
|
@@ -4625,7 +5069,7 @@ function cleanupWorktree(worktreePath, branchName) {
|
|
|
4625
5069
|
}
|
|
4626
5070
|
if (branchName) {
|
|
4627
5071
|
try {
|
|
4628
|
-
execFileSync8("git", ["branch", "-D", branchName], {
|
|
5072
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "branch", "-D", branchName], {
|
|
4629
5073
|
cwd: repoRoot,
|
|
4630
5074
|
stdio: "pipe"
|
|
4631
5075
|
});
|
|
@@ -4635,7 +5079,7 @@ function cleanupWorktree(worktreePath, branchName) {
|
|
|
4635
5079
|
function removeWorktreeHoldingBranch(repoRoot, branchName, exceptDir) {
|
|
4636
5080
|
let listing;
|
|
4637
5081
|
try {
|
|
4638
|
-
listing = execFileSync8("git", ["worktree", "list", "--porcelain"], {
|
|
5082
|
+
listing = execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "list", "--porcelain"], {
|
|
4639
5083
|
cwd: repoRoot,
|
|
4640
5084
|
encoding: "utf-8",
|
|
4641
5085
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -4663,7 +5107,7 @@ function removeWorktreeHoldingBranch(repoRoot, branchName, exceptDir) {
|
|
|
4663
5107
|
if (exceptDir && resolve3(holderPath) === resolve3(exceptDir))
|
|
4664
5108
|
return null;
|
|
4665
5109
|
try {
|
|
4666
|
-
execFileSync8("git", ["worktree", "remove", holderPath, "--force"], {
|
|
5110
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "remove", holderPath, "--force"], {
|
|
4667
5111
|
cwd: repoRoot,
|
|
4668
5112
|
stdio: "pipe"
|
|
4669
5113
|
});
|
|
@@ -4673,7 +5117,7 @@ function removeWorktreeHoldingBranch(repoRoot, branchName, exceptDir) {
|
|
|
4673
5117
|
return null;
|
|
4674
5118
|
}
|
|
4675
5119
|
try {
|
|
4676
|
-
execFileSync8("git", ["worktree", "prune", "--expire=now"], {
|
|
5120
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "prune", "--expire=now"], {
|
|
4677
5121
|
cwd: repoRoot,
|
|
4678
5122
|
stdio: "pipe"
|
|
4679
5123
|
});
|
|
@@ -4681,13 +5125,19 @@ function removeWorktreeHoldingBranch(repoRoot, branchName, exceptDir) {
|
|
|
4681
5125
|
return holderPath;
|
|
4682
5126
|
}
|
|
4683
5127
|
function resolveRepoRoot() {
|
|
4684
|
-
return execFileSync8("git", ["rev-parse", "--show-toplevel"], {
|
|
5128
|
+
return execFileSync8("git", [...GIT_NO_HOOKS, "rev-parse", "--show-toplevel"], {
|
|
4685
5129
|
encoding: "utf-8"
|
|
4686
5130
|
}).trim();
|
|
4687
5131
|
}
|
|
4688
5132
|
function localBranchExists(branchName, repoRoot) {
|
|
4689
5133
|
try {
|
|
4690
|
-
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" });
|
|
4691
5141
|
return true;
|
|
4692
5142
|
} catch {
|
|
4693
5143
|
return false;
|
|
@@ -4697,7 +5147,7 @@ function branchAheadOfItsRemote(branchName, repoRoot = resolveRepoRoot()) {
|
|
|
4697
5147
|
if (!localBranchExists(branchName, repoRoot))
|
|
4698
5148
|
return false;
|
|
4699
5149
|
try {
|
|
4700
|
-
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();
|
|
4701
5151
|
return out.length > 0;
|
|
4702
5152
|
} catch {
|
|
4703
5153
|
return false;
|
|
@@ -4750,11 +5200,13 @@ function makeBranchName(shortId, title, prefix = "agent-attempts/") {
|
|
|
4750
5200
|
// src/index.ts
|
|
4751
5201
|
var MOTOR_NAME = "harmony-harness";
|
|
4752
5202
|
export {
|
|
5203
|
+
writeOnlyDenyPaths,
|
|
4753
5204
|
waitForDevServer,
|
|
4754
5205
|
validateGitProviderCli,
|
|
4755
5206
|
upsertReviewedSha,
|
|
4756
5207
|
upsertCiReviewRequestedSha,
|
|
4757
5208
|
updateExistingPr,
|
|
5209
|
+
toolchainCacheDirectories,
|
|
4758
5210
|
testCommand,
|
|
4759
5211
|
terminateGroup,
|
|
4760
5212
|
teardownWorktree,
|
|
@@ -4767,6 +5219,7 @@ export {
|
|
|
4767
5219
|
sizingEventSource,
|
|
4768
5220
|
sizeRun,
|
|
4769
5221
|
signalGroup,
|
|
5222
|
+
secretEnvKeysToStrip,
|
|
4770
5223
|
sanitizeCiText,
|
|
4771
5224
|
sandboxRunArgs,
|
|
4772
5225
|
sandboxAvailable,
|
|
@@ -4787,6 +5240,7 @@ export {
|
|
|
4787
5240
|
resolvePrUrl,
|
|
4788
5241
|
resolvePrHeadBranch,
|
|
4789
5242
|
resolveOracleRunner,
|
|
5243
|
+
resolveContinuationTarget,
|
|
4790
5244
|
rescueUnpushedBranch,
|
|
4791
5245
|
rerunFailedJobs,
|
|
4792
5246
|
requestIndependentReview,
|
|
@@ -4820,7 +5274,13 @@ export {
|
|
|
4820
5274
|
isPrOnOrigin,
|
|
4821
5275
|
isInsideTree,
|
|
4822
5276
|
installCommand,
|
|
5277
|
+
implementRunToolPolicy,
|
|
5278
|
+
implementRunContainmentCliArgs,
|
|
5279
|
+
implementRunContainment,
|
|
5280
|
+
hostPersistencePaths,
|
|
5281
|
+
harmonyMcpServer,
|
|
4823
5282
|
gradeOracleRed,
|
|
5283
|
+
gitMetadataDenyPaths,
|
|
4824
5284
|
getPrStatus,
|
|
4825
5285
|
getPrFailedChecks,
|
|
4826
5286
|
getHeadSha,
|
|
@@ -4850,10 +5310,14 @@ export {
|
|
|
4850
5310
|
deriveCiStatus,
|
|
4851
5311
|
decidePrBranch,
|
|
4852
5312
|
decideConfinedTool,
|
|
5313
|
+
credentialWriteToolDeny,
|
|
5314
|
+
credentialToolDeny,
|
|
5315
|
+
credentialDirectories,
|
|
4853
5316
|
credentialAccessDeny,
|
|
4854
5317
|
createWorktree,
|
|
4855
5318
|
createPullRequest,
|
|
4856
5319
|
cooldownMsFor,
|
|
5320
|
+
containedEnv,
|
|
4857
5321
|
confineToRepo,
|
|
4858
5322
|
collectSizingOutput,
|
|
4859
5323
|
collectGateEvidence,
|
|
@@ -4872,6 +5336,7 @@ export {
|
|
|
4872
5336
|
buildCommand,
|
|
4873
5337
|
branchAheadOfItsRemote,
|
|
4874
5338
|
attemptAutoFix,
|
|
5339
|
+
assertNoProjectSandboxOverride,
|
|
4875
5340
|
_resetCache,
|
|
4876
5341
|
__resetSandboxProbe,
|
|
4877
5342
|
WorktreeBaseError,
|
|
@@ -4880,6 +5345,7 @@ export {
|
|
|
4880
5345
|
SDK_ALLOWED_TOOLS,
|
|
4881
5346
|
SANDBOX_MOUNT,
|
|
4882
5347
|
ReviewPassedCollector,
|
|
5348
|
+
ProjectSandboxOverrideError,
|
|
4883
5349
|
OracleRedCollector,
|
|
4884
5350
|
OracleCollector,
|
|
4885
5351
|
ORACLE_RUNNER_HINTS,
|
|
@@ -4889,8 +5355,10 @@ export {
|
|
|
4889
5355
|
MAX_IMPLEMENT_MODEL,
|
|
4890
5356
|
MAX_CHANGED_FILES,
|
|
4891
5357
|
JUDGE_MODEL,
|
|
5358
|
+
IMPLEMENT_ALLOWED_DOMAINS,
|
|
4892
5359
|
HarmonyClient,
|
|
4893
5360
|
HARMONY_CREDENTIAL_KEYS,
|
|
5361
|
+
GIT_NO_HOOKS,
|
|
4894
5362
|
GATE_CONFIG_ERROR_MARK,
|
|
4895
5363
|
GATE_CONFIG_ERROR_KEY,
|
|
4896
5364
|
DevServerReadinessError,
|