@gethmy/harness 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +736 -154
- package/dist/index.js +886 -424
- package/package.json +2 -2
- package/src/ci-failure.ts +10 -5
- package/src/exec-types.ts +69 -7
- package/src/gate-collectors.ts +30 -4
- 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 +1312 -0
- package/src/sdk-agent-runner.ts +19 -0
- package/src/verification.ts +295 -66
- 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, {
|
|
@@ -643,16 +1128,16 @@ __export(exports_git_pr, {
|
|
|
643
1128
|
buildPrBody: () => buildPrBody
|
|
644
1129
|
});
|
|
645
1130
|
import { execFile as execFile2, execFileSync as execFileSync7 } from "node:child_process";
|
|
646
|
-
import { promisify as
|
|
1131
|
+
import { promisify as promisify3 } from "node:util";
|
|
647
1132
|
function createExecFileAsync2() {
|
|
648
|
-
return
|
|
1133
|
+
return promisify3(execFile2);
|
|
649
1134
|
}
|
|
650
1135
|
function execFileAsync2() {
|
|
651
1136
|
return cachedExecFileAsync2 ??= createExecFileAsync2();
|
|
652
1137
|
}
|
|
653
1138
|
function detectGitProvider(cwd) {
|
|
654
1139
|
try {
|
|
655
|
-
const url = execFileSync7("git", ["remote", "get-url", "origin"], {
|
|
1140
|
+
const url = execFileSync7("git", [...GIT_NO_HOOKS, "remote", "get-url", "origin"], {
|
|
656
1141
|
cwd,
|
|
657
1142
|
encoding: "utf-8"
|
|
658
1143
|
}).trim();
|
|
@@ -702,7 +1187,7 @@ function validateGitProviderCli(provider, cwd) {
|
|
|
702
1187
|
}
|
|
703
1188
|
case "bitbucket":
|
|
704
1189
|
case "unknown":
|
|
705
|
-
log.warn(
|
|
1190
|
+
log.warn(TAG14, `Git provider "${provider}" — PR creation will be skipped (no CLI support)`);
|
|
706
1191
|
break;
|
|
707
1192
|
}
|
|
708
1193
|
}
|
|
@@ -950,7 +1435,7 @@ async function mergePullRequest(prUrl, cwd, provider, strategy, deleteBranch) {
|
|
|
950
1435
|
}
|
|
951
1436
|
function getHeadSha(cwd) {
|
|
952
1437
|
try {
|
|
953
|
-
return execFileSync7("git", ["rev-parse", "HEAD"], {
|
|
1438
|
+
return execFileSync7("git", [...GIT_NO_HOOKS, "rev-parse", "HEAD"], {
|
|
954
1439
|
cwd,
|
|
955
1440
|
encoding: "utf-8"
|
|
956
1441
|
}).trim();
|
|
@@ -985,7 +1470,7 @@ async function checkPrMergeStatus(prUrl, cwd, provider) {
|
|
|
985
1470
|
try {
|
|
986
1471
|
parsed = JSON.parse(stdout.trim());
|
|
987
1472
|
} catch {
|
|
988
|
-
log.warn(
|
|
1473
|
+
log.warn(TAG14, `Failed to parse glab JSON output for MR ${mrMatch[1]}`);
|
|
989
1474
|
return "unknown";
|
|
990
1475
|
}
|
|
991
1476
|
if (typeof parsed !== "object" || parsed === null)
|
|
@@ -1083,7 +1568,7 @@ async function resolvePrHeadBranch(prUrl, cwd, provider) {
|
|
|
1083
1568
|
const { stdout } = await execFileAsync2()("gh", ["pr", "view", prUrl, "--json", "headRefName,isCrossRepository"], { cwd, encoding: "utf-8", timeout: 1e4 });
|
|
1084
1569
|
return decidePrBranch("github", stdout);
|
|
1085
1570
|
} catch (err) {
|
|
1086
|
-
log.warn(
|
|
1571
|
+
log.warn(TAG14, `gh pr view failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1087
1572
|
return decidePrBranch("github", null);
|
|
1088
1573
|
}
|
|
1089
1574
|
}
|
|
@@ -1099,7 +1584,7 @@ async function resolvePrHeadBranch(prUrl, cwd, provider) {
|
|
|
1099
1584
|
const { stdout } = await execFileAsync2()("az", ["repos", "pr", "show", "--id", prId, "--output", "json"], { cwd, encoding: "utf-8", timeout: 1e4 });
|
|
1100
1585
|
return decidePrBranch("azure", stdout);
|
|
1101
1586
|
} catch (err) {
|
|
1102
|
-
log.warn(
|
|
1587
|
+
log.warn(TAG14, `az repos pr show failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1103
1588
|
return decidePrBranch("azure", null);
|
|
1104
1589
|
}
|
|
1105
1590
|
}
|
|
@@ -1127,7 +1612,13 @@ function resolvePrUrl(description, branchName, cwd, provider) {
|
|
|
1127
1612
|
}
|
|
1128
1613
|
function remoteBranchExists(branchName, cwd) {
|
|
1129
1614
|
try {
|
|
1130
|
-
execFileSync7("git", [
|
|
1615
|
+
execFileSync7("git", [
|
|
1616
|
+
...GIT_NO_HOOKS,
|
|
1617
|
+
"ls-remote",
|
|
1618
|
+
"--exit-code",
|
|
1619
|
+
"origin",
|
|
1620
|
+
`refs/heads/${branchName}`
|
|
1621
|
+
], { cwd, stdio: "pipe" });
|
|
1131
1622
|
return true;
|
|
1132
1623
|
} catch {
|
|
1133
1624
|
return false;
|
|
@@ -1135,24 +1626,24 @@ function remoteBranchExists(branchName, cwd) {
|
|
|
1135
1626
|
}
|
|
1136
1627
|
function pushBranch(branchName, cwd) {
|
|
1137
1628
|
if (remoteBranchExists(branchName, cwd)) {
|
|
1138
|
-
log.info(
|
|
1629
|
+
log.info(TAG14, `Remote branch ${branchName} exists (rework), force-pushing`);
|
|
1139
1630
|
let expectedSha = null;
|
|
1140
1631
|
try {
|
|
1141
|
-
execFileSync7("git", ["fetch", "origin", branchName], {
|
|
1632
|
+
execFileSync7("git", [...GIT_NO_HOOKS, "fetch", "origin", branchName], {
|
|
1142
1633
|
cwd,
|
|
1143
1634
|
stdio: "pipe"
|
|
1144
1635
|
});
|
|
1145
|
-
expectedSha = execFileSync7("git", ["rev-parse", `refs/remotes/origin/${branchName}`], { cwd, encoding: "utf-8" }).trim();
|
|
1636
|
+
expectedSha = execFileSync7("git", [...GIT_NO_HOOKS, "rev-parse", `refs/remotes/origin/${branchName}`], { cwd, encoding: "utf-8" }).trim();
|
|
1146
1637
|
} catch (err) {
|
|
1147
|
-
log.warn(
|
|
1638
|
+
log.warn(TAG14, `could not resolve remote tip for ${branchName}, falling back to weak lease: ${err instanceof Error ? err.message : err}`);
|
|
1148
1639
|
}
|
|
1149
1640
|
const lease = expectedSha ? `--force-with-lease=refs/heads/${branchName}:${expectedSha}` : "--force-with-lease";
|
|
1150
|
-
execFileSync7("git", ["push", lease, "-u", "origin", branchName], {
|
|
1641
|
+
execFileSync7("git", [...GIT_NO_HOOKS, "push", lease, "-u", "origin", branchName], {
|
|
1151
1642
|
cwd,
|
|
1152
1643
|
stdio: "pipe"
|
|
1153
1644
|
});
|
|
1154
1645
|
} else {
|
|
1155
|
-
execFileSync7("git", ["push", "-u", "origin", branchName], {
|
|
1646
|
+
execFileSync7("git", [...GIT_NO_HOOKS, "push", "-u", "origin", branchName], {
|
|
1156
1647
|
cwd,
|
|
1157
1648
|
stdio: "pipe"
|
|
1158
1649
|
});
|
|
@@ -1163,25 +1654,31 @@ function renameRemoteBranch(oldRef, newRef, cwd) {
|
|
|
1163
1654
|
return;
|
|
1164
1655
|
let sha;
|
|
1165
1656
|
try {
|
|
1166
|
-
sha = execFileSync7("git", ["rev-parse", "HEAD"], {
|
|
1657
|
+
sha = execFileSync7("git", [...GIT_NO_HOOKS, "rev-parse", "HEAD"], {
|
|
1167
1658
|
cwd,
|
|
1168
1659
|
encoding: "utf-8"
|
|
1169
1660
|
}).trim();
|
|
1170
1661
|
} catch (err) {
|
|
1171
1662
|
throw new Error(`renameRemoteBranch: could not resolve HEAD: ${err instanceof Error ? err.message : err}`);
|
|
1172
1663
|
}
|
|
1173
|
-
log.info(
|
|
1174
|
-
execFileSync7("git", [
|
|
1664
|
+
log.info(TAG14, `Renaming remote ${oldRef} → ${newRef}`);
|
|
1665
|
+
execFileSync7("git", [
|
|
1666
|
+
...GIT_NO_HOOKS,
|
|
1667
|
+
"push",
|
|
1668
|
+
"origin",
|
|
1669
|
+
`${sha}:refs/heads/${newRef}`,
|
|
1670
|
+
"--force-with-lease"
|
|
1671
|
+
], { cwd, stdio: "pipe" });
|
|
1175
1672
|
try {
|
|
1176
|
-
execFileSync7("git", ["push", "origin", `:refs/heads/${oldRef}`], {
|
|
1673
|
+
execFileSync7("git", [...GIT_NO_HOOKS, "push", "origin", `:refs/heads/${oldRef}`], {
|
|
1177
1674
|
cwd,
|
|
1178
1675
|
stdio: "pipe"
|
|
1179
1676
|
});
|
|
1180
1677
|
} catch (err) {
|
|
1181
|
-
log.warn(
|
|
1678
|
+
log.warn(TAG14, `renameRemoteBranch: could not delete old ref ${oldRef}: ${err instanceof Error ? err.message : err}`);
|
|
1182
1679
|
}
|
|
1183
1680
|
try {
|
|
1184
|
-
execFileSync7("git", ["branch", "-m", oldRef, newRef], {
|
|
1681
|
+
execFileSync7("git", [...GIT_NO_HOOKS, "branch", "-m", oldRef, newRef], {
|
|
1185
1682
|
cwd,
|
|
1186
1683
|
stdio: "pipe"
|
|
1187
1684
|
});
|
|
@@ -1189,7 +1686,7 @@ function renameRemoteBranch(oldRef, newRef, cwd) {
|
|
|
1189
1686
|
}
|
|
1190
1687
|
function getBranchWebUrl(branchName, cwd) {
|
|
1191
1688
|
try {
|
|
1192
|
-
const remoteUrl = execFileSync7("git", ["remote", "get-url", "origin"], {
|
|
1689
|
+
const remoteUrl = execFileSync7("git", [...GIT_NO_HOOKS, "remote", "get-url", "origin"], {
|
|
1193
1690
|
cwd,
|
|
1194
1691
|
encoding: "utf-8"
|
|
1195
1692
|
}).trim();
|
|
@@ -1237,12 +1734,17 @@ function buildPrBody(card, commitLog) {
|
|
|
1237
1734
|
}
|
|
1238
1735
|
function createPullRequest(card, branchName, worktreePath, config, provider, existingPrUrl) {
|
|
1239
1736
|
if (existingPrUrl) {
|
|
1240
|
-
log.info(
|
|
1737
|
+
log.info(TAG14, `Reusing existing PR from card description: ${existingPrUrl}`);
|
|
1241
1738
|
return existingPrUrl;
|
|
1242
1739
|
}
|
|
1243
1740
|
let commitLog = "";
|
|
1244
1741
|
try {
|
|
1245
|
-
commitLog = execFileSync7("git", [
|
|
1742
|
+
commitLog = execFileSync7("git", [
|
|
1743
|
+
...GIT_NO_HOOKS,
|
|
1744
|
+
"log",
|
|
1745
|
+
"--oneline",
|
|
1746
|
+
`origin/${config.worktree.baseBranch}..HEAD`
|
|
1747
|
+
], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
1246
1748
|
} catch {
|
|
1247
1749
|
commitLog = "(unable to retrieve commit log)";
|
|
1248
1750
|
}
|
|
@@ -1251,7 +1753,7 @@ function createPullRequest(card, branchName, worktreePath, config, provider, exi
|
|
|
1251
1753
|
const base = config.worktree.baseBranch;
|
|
1252
1754
|
const existingUrl = findExistingPr(branchName, worktreePath, provider);
|
|
1253
1755
|
if (existingUrl) {
|
|
1254
|
-
log.info(
|
|
1756
|
+
log.info(TAG14, `PR already exists for ${branchName}, updating body...`);
|
|
1255
1757
|
updateExistingPr(branchName, body, worktreePath, provider);
|
|
1256
1758
|
return existingUrl;
|
|
1257
1759
|
}
|
|
@@ -1301,13 +1803,13 @@ function createPullRequest(card, branchName, worktreePath, config, provider, exi
|
|
|
1301
1803
|
], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
1302
1804
|
break;
|
|
1303
1805
|
default:
|
|
1304
|
-
log.warn(
|
|
1806
|
+
log.warn(TAG14, `No PR CLI for provider "${provider}" — branch pushed but no PR created`);
|
|
1305
1807
|
return null;
|
|
1306
1808
|
}
|
|
1307
|
-
log.info(
|
|
1809
|
+
log.info(TAG14, `PR created: ${result}`);
|
|
1308
1810
|
return result;
|
|
1309
1811
|
} catch (err) {
|
|
1310
|
-
log.error(
|
|
1812
|
+
log.error(TAG14, `Failed to create PR: ${err instanceof Error ? err.message : err}`);
|
|
1311
1813
|
return null;
|
|
1312
1814
|
}
|
|
1313
1815
|
}
|
|
@@ -1341,15 +1843,16 @@ function updateExistingPr(branchName, body, worktreePath, provider) {
|
|
|
1341
1843
|
execFileSync7("glab", ["mr", "update", branchName, "--description", body], { cwd: worktreePath, stdio: "pipe" });
|
|
1342
1844
|
break;
|
|
1343
1845
|
}
|
|
1344
|
-
log.info(
|
|
1846
|
+
log.info(TAG14, `Updated existing PR body for ${branchName}`);
|
|
1345
1847
|
} catch (err) {
|
|
1346
|
-
log.warn(
|
|
1848
|
+
log.warn(TAG14, `Failed to update PR body: ${err instanceof Error ? err.message : err}`);
|
|
1347
1849
|
}
|
|
1348
1850
|
}
|
|
1349
|
-
var cachedExecFileAsync2,
|
|
1851
|
+
var cachedExecFileAsync2, TAG14 = "git-pr", VALID_PR_URL_RE, PR_URL_RE, REVIEWED_SHA_RE, CI_REVIEW_REQUESTED_SHA_RE;
|
|
1350
1852
|
var init_git_pr = __esm(() => {
|
|
1351
1853
|
init_dist();
|
|
1352
1854
|
init_log();
|
|
1855
|
+
init_run_containment();
|
|
1353
1856
|
VALID_PR_URL_RE = /^https:\/\/(github\.com|gitlab\.com|dev\.azure\.com|bitbucket\.org)\//;
|
|
1354
1857
|
PR_URL_RE = /PR:\s*(https?:\/\/[^\s)]+)/;
|
|
1355
1858
|
REVIEWED_SHA_RE = /^Reviewed-SHA:\s*([0-9a-f]{7,40})\s*$/im;
|
|
@@ -1635,6 +2138,7 @@ class SdkAgentRunner {
|
|
|
1635
2138
|
...this.cfg.settingSources ? { settingSources: this.cfg.settingSources } : {},
|
|
1636
2139
|
...this.cfg.mcpServers ? { mcpServers: this.cfg.mcpServers } : {},
|
|
1637
2140
|
...this.cfg.strictMcpConfig ? { strictMcpConfig: true } : {},
|
|
2141
|
+
...this.cfg.sandbox ? { sandbox: this.cfg.sandbox } : {},
|
|
1638
2142
|
stderr: (data) => {
|
|
1639
2143
|
this.capturedStderr += data;
|
|
1640
2144
|
},
|
|
@@ -2051,6 +2555,7 @@ class ArtifactCollector {
|
|
|
2051
2555
|
}
|
|
2052
2556
|
// src/ci-failure.ts
|
|
2053
2557
|
init_log();
|
|
2558
|
+
init_run_containment();
|
|
2054
2559
|
import { execFile, execFileSync } from "node:child_process";
|
|
2055
2560
|
import { promisify } from "node:util";
|
|
2056
2561
|
function createExecFileAsync() {
|
|
@@ -2078,7 +2583,7 @@ function isPrOnOrigin(prUrl, cwd) {
|
|
|
2078
2583
|
if (!prSlug)
|
|
2079
2584
|
return false;
|
|
2080
2585
|
try {
|
|
2081
|
-
const remote = execFileSync("git", ["remote", "get-url", "origin"], {
|
|
2586
|
+
const remote = execFileSync("git", [...GIT_NO_HOOKS, "remote", "get-url", "origin"], {
|
|
2082
2587
|
cwd,
|
|
2083
2588
|
encoding: "utf-8",
|
|
2084
2589
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -2216,6 +2721,7 @@ init_dist();
|
|
|
2216
2721
|
|
|
2217
2722
|
// src/exec-types.ts
|
|
2218
2723
|
var DEFAULT_METRIC_TIMEOUT_MS = 300000;
|
|
2724
|
+
var SANDBOX_STARTUP_GRACE_MS = 60000;
|
|
2219
2725
|
|
|
2220
2726
|
// src/gate-config-error.ts
|
|
2221
2727
|
init_dist();
|
|
@@ -2230,14 +2736,14 @@ var MAX_METRIC_TIMEOUT_MS = 900000;
|
|
|
2230
2736
|
var METRIC_SIGINT_GRACE_MS = 2000;
|
|
2231
2737
|
var METRIC_SIGTERM_GRACE_MS = 3000;
|
|
2232
2738
|
var STDIO_DRAIN_GRACE_MS = 500;
|
|
2233
|
-
function parseParseMode(
|
|
2234
|
-
if (typeof
|
|
2739
|
+
function parseParseMode(parse2) {
|
|
2740
|
+
if (typeof parse2 !== "string" || parse2.length === 0) {
|
|
2235
2741
|
return { kind: "invalid", reason: "`parse` must be a non-empty string" };
|
|
2236
2742
|
}
|
|
2237
|
-
if (
|
|
2743
|
+
if (parse2 === "number")
|
|
2238
2744
|
return { kind: "number" };
|
|
2239
|
-
if (
|
|
2240
|
-
const path =
|
|
2745
|
+
if (parse2.startsWith("json:")) {
|
|
2746
|
+
const path = parse2.slice("json:".length).trim();
|
|
2241
2747
|
if (!path) {
|
|
2242
2748
|
return { kind: "invalid", reason: '`parse` "json:" is missing a path' };
|
|
2243
2749
|
}
|
|
@@ -2245,7 +2751,7 @@ function parseParseMode(parse) {
|
|
|
2245
2751
|
}
|
|
2246
2752
|
return {
|
|
2247
2753
|
kind: "invalid",
|
|
2248
|
-
reason: `unknown \`parse\` mode "${
|
|
2754
|
+
reason: `unknown \`parse\` mode "${parse2}" (expected "number" or "json:<path>")`
|
|
2249
2755
|
};
|
|
2250
2756
|
}
|
|
2251
2757
|
function parseMetricValue(mode, stdout) {
|
|
@@ -2291,7 +2797,7 @@ function parseMetricValue(mode, stdout) {
|
|
|
2291
2797
|
return { ok: true, value: resolved };
|
|
2292
2798
|
}
|
|
2293
2799
|
function runMetricCommand(args) {
|
|
2294
|
-
return new Promise((
|
|
2800
|
+
return new Promise((resolve2, reject) => {
|
|
2295
2801
|
let child;
|
|
2296
2802
|
try {
|
|
2297
2803
|
child = spawnInGroup(args.command, args.args, {
|
|
@@ -2322,7 +2828,7 @@ function runMetricCommand(args) {
|
|
|
2322
2828
|
if (failure)
|
|
2323
2829
|
reject(failure);
|
|
2324
2830
|
else
|
|
2325
|
-
|
|
2831
|
+
resolve2(Buffer.concat(chunks).toString("utf8"));
|
|
2326
2832
|
};
|
|
2327
2833
|
const killTree = (reason) => {
|
|
2328
2834
|
if (settled || killReason)
|
|
@@ -2470,123 +2976,20 @@ function describeRunFailure(err, timeoutMs) {
|
|
|
2470
2976
|
function truncate(value, max) {
|
|
2471
2977
|
return value.length <= max ? value : `${value.slice(0, max)}…[truncated]`;
|
|
2472
2978
|
}
|
|
2473
|
-
|
|
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
|
-
}
|
|
2979
|
+
|
|
2980
|
+
// src/index.ts
|
|
2981
|
+
init_confine_to_repo();
|
|
2982
|
+
|
|
2580
2983
|
// src/gate-collectors.ts
|
|
2581
2984
|
init_dist();
|
|
2582
2985
|
init_log();
|
|
2583
2986
|
|
|
2584
2987
|
// src/oracle-collector.ts
|
|
2585
|
-
import { createHash } from "node:crypto";
|
|
2988
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
2586
2989
|
init_log();
|
|
2587
2990
|
|
|
2588
2991
|
// src/oracle.ts
|
|
2589
|
-
import { lstatSync, readFileSync, statSync } from "node:fs";
|
|
2992
|
+
import { lstatSync, readFileSync as readFileSync2, statSync } from "node:fs";
|
|
2590
2993
|
import {
|
|
2591
2994
|
chmod,
|
|
2592
2995
|
lstat,
|
|
@@ -2596,13 +2999,13 @@ import {
|
|
|
2596
2999
|
rm,
|
|
2597
3000
|
writeFile
|
|
2598
3001
|
} from "node:fs/promises";
|
|
2599
|
-
import { tmpdir } from "node:os";
|
|
2600
|
-
import { dirname as
|
|
3002
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
3003
|
+
import { dirname as dirname3, isAbsolute as isAbsolute3, join as join2, resolve as resolve2, sep as sep2 } from "node:path";
|
|
2601
3004
|
import { StringDecoder } from "node:string_decoder";
|
|
2602
3005
|
init_log();
|
|
2603
3006
|
var TAG5 = "oracle";
|
|
2604
3007
|
async function resolveContained(repoPath, relativePath) {
|
|
2605
|
-
if (
|
|
3008
|
+
if (isAbsolute3(relativePath)) {
|
|
2606
3009
|
throw new Error(`refusing to place an oracle at an absolute path: ${relativePath}`);
|
|
2607
3010
|
}
|
|
2608
3011
|
if (relativePath === "" || relativePath === ".") {
|
|
@@ -2625,7 +3028,7 @@ async function resolveContained(repoPath, relativePath) {
|
|
|
2625
3028
|
}
|
|
2626
3029
|
async function place(repoPath, oracle) {
|
|
2627
3030
|
const target = await resolveContained(repoPath, oracle.path);
|
|
2628
|
-
await mkdir(
|
|
3031
|
+
await mkdir(dirname3(target), { recursive: true });
|
|
2629
3032
|
await writeFile(target, oracle.content, "utf8");
|
|
2630
3033
|
}
|
|
2631
3034
|
async function remove(repoPath, oracle) {
|
|
@@ -2733,8 +3136,8 @@ function argvPath(path) {
|
|
|
2733
3136
|
}
|
|
2734
3137
|
async function runHeldOracle(repoPath, oracle, timeoutMs = DEFAULT_METRIC_TIMEOUT_MS) {
|
|
2735
3138
|
const spec = resolveOracleRunnerSpec(oracle);
|
|
2736
|
-
const reportDir = await mkdtemp(
|
|
2737
|
-
const reportPath =
|
|
3139
|
+
const reportDir = await mkdtemp(join2(tmpdir2(), reportDirPrefix()));
|
|
3140
|
+
const reportPath = join2(reportDir, spec.report.file);
|
|
2738
3141
|
try {
|
|
2739
3142
|
return await spawnHeldOracle(spec, repoPath, oracle, reportPath, timeoutMs);
|
|
2740
3143
|
} finally {
|
|
@@ -2745,7 +3148,7 @@ function reportDirPrefix() {
|
|
|
2745
3148
|
return "harmony-oracle-report-";
|
|
2746
3149
|
}
|
|
2747
3150
|
function assertUntampered(reportPath) {
|
|
2748
|
-
const dir = statSync(
|
|
3151
|
+
const dir = statSync(dirname3(reportPath));
|
|
2749
3152
|
if ((dir.mode & 511) !== 448) {
|
|
2750
3153
|
throw new Error(`the oracle report directory's mode changed to ${(dir.mode & 511).toString(8)} — refusing the report`);
|
|
2751
3154
|
}
|
|
@@ -2808,7 +3211,7 @@ ${output}` : output;
|
|
|
2808
3211
|
const captureReport = () => {
|
|
2809
3212
|
try {
|
|
2810
3213
|
assertUntampered(reportPath);
|
|
2811
|
-
report = spec.report.parse(
|
|
3214
|
+
report = spec.report.parse(readFileSync2(reportPath, "utf8"));
|
|
2812
3215
|
} catch (err) {
|
|
2813
3216
|
report = null;
|
|
2814
3217
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -2926,7 +3329,7 @@ class HeldOracleCollector {
|
|
|
2926
3329
|
async runHeld(oracle) {
|
|
2927
3330
|
const identity = {
|
|
2928
3331
|
oracleId: oracle.id ?? null,
|
|
2929
|
-
contentHash:
|
|
3332
|
+
contentHash: createHash2("sha256").update(oracle.content).digest("hex")
|
|
2930
3333
|
};
|
|
2931
3334
|
await this.deps.place(this.deps.repoPath, oracle);
|
|
2932
3335
|
try {
|
|
@@ -3037,11 +3440,12 @@ function errText(err) {
|
|
|
3037
3440
|
}
|
|
3038
3441
|
|
|
3039
3442
|
// src/verification.ts
|
|
3040
|
-
init_log();
|
|
3041
3443
|
import { execFileSync as execFileSync5, spawn as spawn2 } from "node:child_process";
|
|
3444
|
+
init_log();
|
|
3042
3445
|
|
|
3043
3446
|
// src/pm.ts
|
|
3044
3447
|
init_log();
|
|
3448
|
+
init_run_containment();
|
|
3045
3449
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
3046
3450
|
import { existsSync } from "node:fs";
|
|
3047
3451
|
var TAG7 = "pm";
|
|
@@ -3051,7 +3455,7 @@ function detectPackageManager() {
|
|
|
3051
3455
|
return cached;
|
|
3052
3456
|
let repoRoot;
|
|
3053
3457
|
try {
|
|
3054
|
-
repoRoot = execFileSync2("git", ["rev-parse", "--show-toplevel"], {
|
|
3458
|
+
repoRoot = execFileSync2("git", [...GIT_NO_HOOKS, "rev-parse", "--show-toplevel"], {
|
|
3055
3459
|
encoding: "utf-8"
|
|
3056
3460
|
}).trim();
|
|
3057
3461
|
} catch {
|
|
@@ -3094,7 +3498,7 @@ function spawnRunArgs(script, ...extra) {
|
|
|
3094
3498
|
// src/project-type.ts
|
|
3095
3499
|
init_log();
|
|
3096
3500
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
3097
|
-
import { existsSync as existsSync2, readdirSync, readFileSync as
|
|
3501
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "node:fs";
|
|
3098
3502
|
var TAG8 = "project-type";
|
|
3099
3503
|
var _cache = new Map;
|
|
3100
3504
|
function _resetCache() {
|
|
@@ -3197,7 +3601,7 @@ var NPM_PLACEHOLDER_TEST = /no test specified/i;
|
|
|
3197
3601
|
function hasNodeTestScript(dir) {
|
|
3198
3602
|
let script;
|
|
3199
3603
|
try {
|
|
3200
|
-
const pkg = JSON.parse(
|
|
3604
|
+
const pkg = JSON.parse(readFileSync3(`${dir}/package.json`, "utf-8"));
|
|
3201
3605
|
script = pkg.scripts?.test;
|
|
3202
3606
|
} catch (err) {
|
|
3203
3607
|
log.warn(TAG8, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
|
|
@@ -3214,7 +3618,7 @@ function hasNodeTestScript(dir) {
|
|
|
3214
3618
|
function firstNodeScript(dir, candidates) {
|
|
3215
3619
|
let scripts;
|
|
3216
3620
|
try {
|
|
3217
|
-
const pkg = JSON.parse(
|
|
3621
|
+
const pkg = JSON.parse(readFileSync3(`${dir}/package.json`, "utf-8"));
|
|
3218
3622
|
scripts = pkg.scripts ?? {};
|
|
3219
3623
|
} catch (err) {
|
|
3220
3624
|
log.warn(TAG8, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
|
|
@@ -3269,10 +3673,108 @@ function resolveXcodeScheme(pt) {
|
|
|
3269
3673
|
}
|
|
3270
3674
|
}
|
|
3271
3675
|
|
|
3676
|
+
// src/repair-sandbox.ts
|
|
3677
|
+
init_log();
|
|
3678
|
+
import { randomUUID } from "node:crypto";
|
|
3679
|
+
import { promisify as promisify2 } from "node:util";
|
|
3680
|
+
var TAG9 = "repair-sandbox";
|
|
3681
|
+
async function dockerExec(argv, opts) {
|
|
3682
|
+
const { execFile: execFile2 } = await import("node:child_process");
|
|
3683
|
+
return promisify2(execFile2)("docker", argv, {
|
|
3684
|
+
encoding: "utf-8",
|
|
3685
|
+
...opts
|
|
3686
|
+
});
|
|
3687
|
+
}
|
|
3688
|
+
var MAX_OUTPUT_BUFFER2 = 20971520;
|
|
3689
|
+
var PROBE_TIMEOUT_MS = 1e4;
|
|
3690
|
+
var SANDBOX_MOUNT = "/repo";
|
|
3691
|
+
var SANDBOX_MEMORY = "4g";
|
|
3692
|
+
var SANDBOX_PIDS = "512";
|
|
3693
|
+
var dockerProbe = null;
|
|
3694
|
+
async function sandboxAvailable() {
|
|
3695
|
+
if (dockerProbe === true)
|
|
3696
|
+
return true;
|
|
3697
|
+
try {
|
|
3698
|
+
await dockerExec(["version", "--format", "{{.Server.Version}}"], {
|
|
3699
|
+
timeout: PROBE_TIMEOUT_MS
|
|
3700
|
+
});
|
|
3701
|
+
dockerProbe = true;
|
|
3702
|
+
} catch {
|
|
3703
|
+
dockerProbe = false;
|
|
3704
|
+
}
|
|
3705
|
+
return dockerProbe;
|
|
3706
|
+
}
|
|
3707
|
+
function __resetSandboxProbe() {
|
|
3708
|
+
dockerProbe = null;
|
|
3709
|
+
}
|
|
3710
|
+
async function removeContainer(name) {
|
|
3711
|
+
try {
|
|
3712
|
+
await dockerExec(["rm", "--force", name], { timeout: PROBE_TIMEOUT_MS });
|
|
3713
|
+
log.warn(TAG9, `removed the timed-out sandbox container ${name}`);
|
|
3714
|
+
} catch {}
|
|
3715
|
+
}
|
|
3716
|
+
function sandboxRunArgs(image, worktree, command, name) {
|
|
3717
|
+
return [
|
|
3718
|
+
"run",
|
|
3719
|
+
"--rm",
|
|
3720
|
+
...name ? ["--name", name] : [],
|
|
3721
|
+
"--network=none",
|
|
3722
|
+
"--cap-drop=ALL",
|
|
3723
|
+
"--security-opt=no-new-privileges",
|
|
3724
|
+
`--memory=${SANDBOX_MEMORY}`,
|
|
3725
|
+
`--pids-limit=${SANDBOX_PIDS}`,
|
|
3726
|
+
...typeof process.getuid === "function" && typeof process.getgid === "function" ? ["--user", `${process.getuid()}:${process.getgid()}`] : [],
|
|
3727
|
+
"--env",
|
|
3728
|
+
"HOME=/tmp",
|
|
3729
|
+
"--volume",
|
|
3730
|
+
`${worktree}:${SANDBOX_MOUNT}`,
|
|
3731
|
+
"--workdir",
|
|
3732
|
+
SANDBOX_MOUNT,
|
|
3733
|
+
"--entrypoint",
|
|
3734
|
+
command.cmd,
|
|
3735
|
+
image,
|
|
3736
|
+
...command.args
|
|
3737
|
+
];
|
|
3738
|
+
}
|
|
3739
|
+
async function runInSandbox(args) {
|
|
3740
|
+
const name = `harmony-repair-${randomUUID()}`;
|
|
3741
|
+
const argv = sandboxRunArgs(args.image, args.worktree, args.command, name);
|
|
3742
|
+
log.info(TAG9, `sandbox: ${args.command.cmd} ${args.command.args.join(" ")} (image ${args.image})`);
|
|
3743
|
+
try {
|
|
3744
|
+
const { stdout } = await dockerExec(argv, {
|
|
3745
|
+
timeout: args.timeoutMs,
|
|
3746
|
+
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
3747
|
+
});
|
|
3748
|
+
return { passed: true, output: stdout ?? "" };
|
|
3749
|
+
} catch (err) {
|
|
3750
|
+
const e = err;
|
|
3751
|
+
const output = `${e.stdout ?? ""}${e.stderr ?? ""}`;
|
|
3752
|
+
if (typeof e.code !== "number") {
|
|
3753
|
+
const timedOut = e.killed === true || e.signal != null;
|
|
3754
|
+
if (timedOut)
|
|
3755
|
+
await removeContainer(name);
|
|
3756
|
+
return {
|
|
3757
|
+
passed: false,
|
|
3758
|
+
output,
|
|
3759
|
+
sandboxError: timedOut ? `the sandbox timed out after ${args.timeoutMs}ms` : `the sandbox did not run: ${e.message ?? "unknown error"}`
|
|
3760
|
+
};
|
|
3761
|
+
}
|
|
3762
|
+
if (e.code === 125) {
|
|
3763
|
+
return {
|
|
3764
|
+
passed: false,
|
|
3765
|
+
output,
|
|
3766
|
+
sandboxError: `the sandbox could not start (image "${args.image}" missing or unusable)`
|
|
3767
|
+
};
|
|
3768
|
+
}
|
|
3769
|
+
return { passed: false, output };
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3272
3773
|
// src/revert-guard.ts
|
|
3273
3774
|
init_log();
|
|
3775
|
+
init_run_containment();
|
|
3274
3776
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
3275
|
-
var
|
|
3777
|
+
var TAG10 = "revert-guard";
|
|
3276
3778
|
var TEST_FILE = /(?:^|\/)__tests__\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
3277
3779
|
function isTestFile(path) {
|
|
3278
3780
|
return TEST_FILE.test(path);
|
|
@@ -3282,21 +3784,27 @@ function filterTestFiles(paths) {
|
|
|
3282
3784
|
}
|
|
3283
3785
|
function refetchBase(worktreePath, baseBranch) {
|
|
3284
3786
|
try {
|
|
3285
|
-
execFileSync4("git", ["fetch", "origin", baseBranch], {
|
|
3787
|
+
execFileSync4("git", [...GIT_NO_HOOKS, "fetch", "origin", baseBranch], {
|
|
3286
3788
|
cwd: worktreePath,
|
|
3287
3789
|
stdio: "pipe"
|
|
3288
3790
|
});
|
|
3289
3791
|
} catch {
|
|
3290
|
-
log.warn(
|
|
3792
|
+
log.warn(TAG10, "Failed to re-fetch base for revert guard — using last fetch");
|
|
3291
3793
|
}
|
|
3292
3794
|
}
|
|
3293
3795
|
function listDeletedFilesAgainstBase(worktreePath, baseBranch) {
|
|
3294
3796
|
try {
|
|
3295
|
-
const out = execFileSync4("git", [
|
|
3797
|
+
const out = execFileSync4("git", [
|
|
3798
|
+
...GIT_NO_HOOKS,
|
|
3799
|
+
"diff",
|
|
3800
|
+
"--diff-filter=D",
|
|
3801
|
+
"--name-only",
|
|
3802
|
+
`origin/${baseBranch}...HEAD`
|
|
3803
|
+
], { cwd: worktreePath, encoding: "utf-8" });
|
|
3296
3804
|
return out.split(`
|
|
3297
3805
|
`).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
3298
3806
|
} catch (err) {
|
|
3299
|
-
log.warn(
|
|
3807
|
+
log.warn(TAG10, `Failed to list deleted files: ${err instanceof Error ? err.message : err}`);
|
|
3300
3808
|
return [];
|
|
3301
3809
|
}
|
|
3302
3810
|
}
|
|
@@ -3306,8 +3814,9 @@ function findDeletedTestFiles(worktreePath, baseBranch) {
|
|
|
3306
3814
|
}
|
|
3307
3815
|
|
|
3308
3816
|
// src/verification.ts
|
|
3309
|
-
|
|
3310
|
-
var
|
|
3817
|
+
init_run_containment();
|
|
3818
|
+
var TAG11 = "verification";
|
|
3819
|
+
var MAX_OUTPUT_BUFFER3 = 64 * 1024 * 1024;
|
|
3311
3820
|
async function runVerification(worktreePath, config, workerId) {
|
|
3312
3821
|
const result = {
|
|
3313
3822
|
passed: true,
|
|
@@ -3317,133 +3826,163 @@ async function runVerification(worktreePath, config, workerId) {
|
|
|
3317
3826
|
reviewFindings: [],
|
|
3318
3827
|
revertWarnings: []
|
|
3319
3828
|
};
|
|
3829
|
+
const sandbox = verificationSandbox(config);
|
|
3320
3830
|
if (config.verification.revertGuard) {
|
|
3321
|
-
log.info(
|
|
3831
|
+
log.info(TAG11, `[worker:${workerId}] Checking for reverted merged work...`);
|
|
3322
3832
|
const deletedTests = findDeletedTestFiles(worktreePath, config.worktree.baseBranch);
|
|
3323
3833
|
if (deletedTests.length > 0) {
|
|
3324
3834
|
result.revertWarnings = deletedTests.map((f) => `Branch deletes test file '${f}' relative to current ${config.worktree.baseBranch} — ` + "likely an accidental revert of already-merged work. Restore the test or rebase on current main.");
|
|
3325
|
-
log.warn(
|
|
3835
|
+
log.warn(TAG11, `[worker:${workerId}] Revert guard tripped: ${deletedTests.length} deleted test file(s)`);
|
|
3326
3836
|
result.passed = false;
|
|
3327
3837
|
} else {
|
|
3328
|
-
log.info(
|
|
3838
|
+
log.info(TAG11, `[worker:${workerId}] Revert guard passed`);
|
|
3329
3839
|
}
|
|
3330
3840
|
}
|
|
3331
3841
|
if (config.verification.build) {
|
|
3332
|
-
log.info(
|
|
3333
|
-
result.buildErrors = runBuild(worktreePath, config.verification.timeout);
|
|
3842
|
+
log.info(TAG11, `[worker:${workerId}] Running build...`);
|
|
3843
|
+
result.buildErrors = await runBuild(worktreePath, config.verification.timeout, sandbox);
|
|
3334
3844
|
if (result.buildErrors.length > 0) {
|
|
3335
|
-
log.warn(
|
|
3845
|
+
log.warn(TAG11, `[worker:${workerId}] Build failed with ${result.buildErrors.length} error(s)`);
|
|
3336
3846
|
result.passed = false;
|
|
3337
3847
|
} else {
|
|
3338
|
-
log.info(
|
|
3848
|
+
log.info(TAG11, `[worker:${workerId}] Build passed`);
|
|
3339
3849
|
}
|
|
3340
3850
|
}
|
|
3341
3851
|
if (config.verification.test && result.buildErrors.length === 0) {
|
|
3342
|
-
log.info(
|
|
3343
|
-
result.testFailures = runTests(worktreePath, config.verification.testTimeout);
|
|
3852
|
+
log.info(TAG11, `[worker:${workerId}] Running tests...`);
|
|
3853
|
+
result.testFailures = await runTests(worktreePath, config.verification.testTimeout, sandbox);
|
|
3344
3854
|
if (result.testFailures.length > 0) {
|
|
3345
|
-
log.warn(
|
|
3855
|
+
log.warn(TAG11, `[worker:${workerId}] Tests failed with ${result.testFailures.length} failure(s)`);
|
|
3346
3856
|
result.passed = false;
|
|
3347
3857
|
} else {
|
|
3348
|
-
log.info(
|
|
3858
|
+
log.info(TAG11, `[worker:${workerId}] Tests passed`);
|
|
3349
3859
|
}
|
|
3350
3860
|
}
|
|
3351
3861
|
if (config.verification.lint) {
|
|
3352
|
-
log.info(
|
|
3353
|
-
result.lintWarnings = runLint(worktreePath, config.verification.timeout);
|
|
3862
|
+
log.info(TAG11, `[worker:${workerId}] Running lint...`);
|
|
3863
|
+
result.lintWarnings = await runLint(worktreePath, config.verification.timeout, sandbox);
|
|
3354
3864
|
if (result.lintWarnings.length > 0) {
|
|
3355
|
-
log.warn(
|
|
3865
|
+
log.warn(TAG11, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
|
|
3356
3866
|
} else {
|
|
3357
|
-
log.info(
|
|
3867
|
+
log.info(TAG11, `[worker:${workerId}] Lint passed`);
|
|
3358
3868
|
}
|
|
3359
3869
|
}
|
|
3360
3870
|
if (config.verification.deepReview) {
|
|
3361
|
-
log.info(
|
|
3871
|
+
log.info(TAG11, `[worker:${workerId}] Running deep review...`);
|
|
3362
3872
|
result.reviewFindings = await runDeepReview(worktreePath, config, workerId);
|
|
3363
3873
|
if (result.reviewFindings.length > 0) {
|
|
3364
|
-
log.warn(
|
|
3874
|
+
log.warn(TAG11, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
|
|
3365
3875
|
} else {
|
|
3366
|
-
log.info(
|
|
3876
|
+
log.info(TAG11, `[worker:${workerId}] Deep review passed`);
|
|
3367
3877
|
}
|
|
3368
3878
|
}
|
|
3369
3879
|
return result;
|
|
3370
3880
|
}
|
|
3371
|
-
function
|
|
3372
|
-
const
|
|
3373
|
-
if (!
|
|
3374
|
-
|
|
3375
|
-
|
|
3881
|
+
function verificationSandbox(config) {
|
|
3882
|
+
const image = config.verification.sandboxImage?.trim();
|
|
3883
|
+
if (!image)
|
|
3884
|
+
return;
|
|
3885
|
+
return { image };
|
|
3886
|
+
}
|
|
3887
|
+
async function execStep(command, args) {
|
|
3888
|
+
const { worktreePath, timeout } = args;
|
|
3889
|
+
const sandbox = args.sandbox?.image.trim() ? args.sandbox : undefined;
|
|
3890
|
+
if (sandbox) {
|
|
3891
|
+
if (!await sandboxAvailable()) {
|
|
3892
|
+
return {
|
|
3893
|
+
ok: false,
|
|
3894
|
+
sandboxError: `verification.sandboxImage is set to "${sandbox.image}" but no container runtime answered — ` + "start Docker or unset the image to verify on the host"
|
|
3895
|
+
};
|
|
3896
|
+
}
|
|
3897
|
+
const result = await runInSandbox({
|
|
3898
|
+
image: sandbox.image,
|
|
3899
|
+
worktree: worktreePath,
|
|
3900
|
+
command,
|
|
3901
|
+
timeoutMs: timeout + SANDBOX_STARTUP_GRACE_MS
|
|
3902
|
+
});
|
|
3903
|
+
if (result.sandboxError) {
|
|
3904
|
+
return { ok: false, sandboxError: result.sandboxError };
|
|
3905
|
+
}
|
|
3906
|
+
if (result.passed)
|
|
3907
|
+
return { ok: true };
|
|
3908
|
+
return { ok: false, err: { stdout: result.output, stderr: "" } };
|
|
3376
3909
|
}
|
|
3377
3910
|
try {
|
|
3378
3911
|
execFileSync5(command.cmd, command.args, {
|
|
3379
3912
|
cwd: worktreePath,
|
|
3380
3913
|
timeout,
|
|
3381
3914
|
stdio: "pipe",
|
|
3382
|
-
maxBuffer:
|
|
3915
|
+
maxBuffer: MAX_OUTPUT_BUFFER3,
|
|
3916
|
+
env: containedEnv()
|
|
3383
3917
|
});
|
|
3384
|
-
return
|
|
3918
|
+
return { ok: true };
|
|
3385
3919
|
} catch (err) {
|
|
3386
|
-
return
|
|
3920
|
+
return { ok: false, err };
|
|
3387
3921
|
}
|
|
3388
3922
|
}
|
|
3389
|
-
function
|
|
3923
|
+
async function runBuild(worktreePath, timeout, sandbox) {
|
|
3924
|
+
const command = buildCommand(worktreePath);
|
|
3925
|
+
if (!command) {
|
|
3926
|
+
log.warn(TAG11, `No known build toolchain for ${worktreePath} — skipping build`);
|
|
3927
|
+
return [];
|
|
3928
|
+
}
|
|
3929
|
+
const outcome = await execStep(command, { worktreePath, timeout, sandbox });
|
|
3930
|
+
if (outcome.ok)
|
|
3931
|
+
return [];
|
|
3932
|
+
if (outcome.sandboxError) {
|
|
3933
|
+
log.error(TAG11, `Build not verified: ${outcome.sandboxError}`);
|
|
3934
|
+
return [`Build did not run: ${outcome.sandboxError}`];
|
|
3935
|
+
}
|
|
3936
|
+
return parseErrorOutput(outcome.err);
|
|
3937
|
+
}
|
|
3938
|
+
async function runTests(worktreePath, timeout, sandbox) {
|
|
3390
3939
|
const command = testCommand(worktreePath);
|
|
3391
3940
|
if (!command) {
|
|
3392
|
-
log.warn(
|
|
3941
|
+
log.warn(TAG11, `No test command for detected toolchain in ${worktreePath} — skipping tests`);
|
|
3393
3942
|
return [];
|
|
3394
3943
|
}
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
cwd: worktreePath,
|
|
3398
|
-
timeout,
|
|
3399
|
-
stdio: "pipe",
|
|
3400
|
-
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
3401
|
-
});
|
|
3944
|
+
const outcome = await execStep(command, { worktreePath, timeout, sandbox });
|
|
3945
|
+
if (outcome.ok)
|
|
3402
3946
|
return [];
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
${output.slice(-4000) || "(no output captured)"}`);
|
|
3407
|
-
return parseTestFailures(err, timeout);
|
|
3947
|
+
if (outcome.sandboxError) {
|
|
3948
|
+
log.error(TAG11, `Tests not verified: ${outcome.sandboxError}`);
|
|
3949
|
+
return [`Test run did not happen: ${outcome.sandboxError}`];
|
|
3408
3950
|
}
|
|
3951
|
+
const output = combineOutput(outcome.err);
|
|
3952
|
+
log.warn(TAG11, `Test run failed:
|
|
3953
|
+
${output.slice(-4000) || "(no output captured)"}`);
|
|
3954
|
+
return parseTestFailures(outcome.err, timeout);
|
|
3409
3955
|
}
|
|
3410
|
-
function runFormatFix(worktreePath, timeout, workerId) {
|
|
3956
|
+
async function runFormatFix(worktreePath, timeout, workerId, sandbox) {
|
|
3411
3957
|
const command = formatFixCommand(worktreePath);
|
|
3412
3958
|
if (!command)
|
|
3413
3959
|
return;
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
stdio: "pipe",
|
|
3419
|
-
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
3420
|
-
});
|
|
3421
|
-
log.info(TAG10, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
|
|
3422
|
-
} catch (err) {
|
|
3423
|
-
log.warn(TAG10, `[worker:${workerId}] Auto-format step exited non-zero (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
3960
|
+
const outcome = await execStep(command, { worktreePath, timeout, sandbox });
|
|
3961
|
+
if (outcome.ok) {
|
|
3962
|
+
log.info(TAG11, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
|
|
3963
|
+
return;
|
|
3424
3964
|
}
|
|
3965
|
+
const why = outcome.sandboxError ? outcome.sandboxError : outcome.err instanceof Error ? outcome.err.message : String(outcome.err);
|
|
3966
|
+
log.warn(TAG11, `[worker:${workerId}] Auto-format step did not complete (non-fatal): ${why}`);
|
|
3425
3967
|
}
|
|
3426
|
-
function runLint(worktreePath, timeout) {
|
|
3968
|
+
async function runLint(worktreePath, timeout, sandbox) {
|
|
3427
3969
|
const command = lintCommand(worktreePath);
|
|
3428
3970
|
if (!command) {
|
|
3429
|
-
log.info(
|
|
3971
|
+
log.info(TAG11, `No lint step for detected toolchain in ${worktreePath} — skipping lint`);
|
|
3430
3972
|
return [];
|
|
3431
3973
|
}
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
cwd: worktreePath,
|
|
3435
|
-
timeout,
|
|
3436
|
-
stdio: "pipe",
|
|
3437
|
-
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
3438
|
-
});
|
|
3974
|
+
const outcome = await execStep(command, { worktreePath, timeout, sandbox });
|
|
3975
|
+
if (outcome.ok)
|
|
3439
3976
|
return [];
|
|
3440
|
-
|
|
3441
|
-
|
|
3977
|
+
if (outcome.sandboxError) {
|
|
3978
|
+
log.error(TAG11, `Lint not verified: ${outcome.sandboxError}`);
|
|
3979
|
+
return [`Lint did not run: ${outcome.sandboxError}`];
|
|
3442
3980
|
}
|
|
3981
|
+
return parseErrorOutput(outcome.err);
|
|
3443
3982
|
}
|
|
3444
3983
|
async function runDeepReview(worktreePath, config, workerId) {
|
|
3445
3984
|
if (!supportsDevServer(worktreePath)) {
|
|
3446
|
-
log.info(
|
|
3985
|
+
log.info(TAG11, `[worker:${workerId}] Detected non-web toolchain — skipping deep review`);
|
|
3447
3986
|
return [];
|
|
3448
3987
|
}
|
|
3449
3988
|
const port = config.verification.devServerBasePort + workerId;
|
|
@@ -3452,22 +3991,23 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
3452
3991
|
const [cmd, args] = spawnRunArgs("dev", "--port", String(port));
|
|
3453
3992
|
devServer = spawn2(cmd, args, {
|
|
3454
3993
|
cwd: worktreePath,
|
|
3455
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3994
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3995
|
+
env: containedEnv()
|
|
3456
3996
|
});
|
|
3457
3997
|
try {
|
|
3458
3998
|
await waitForDevServer(devServer, 30000);
|
|
3459
3999
|
await probeDevServer(port);
|
|
3460
4000
|
} catch (err) {
|
|
3461
|
-
log.error(
|
|
4001
|
+
log.error(TAG11, `Dev server did not become ready: ${err instanceof Error ? err.message : err}`);
|
|
3462
4002
|
return [];
|
|
3463
4003
|
}
|
|
3464
4004
|
let diff = "";
|
|
3465
4005
|
try {
|
|
3466
|
-
diff = execFileSync5("git", ["diff", `origin/${config.worktree.baseBranch}..HEAD`], {
|
|
4006
|
+
diff = execFileSync5("git", [...GIT_NO_HOOKS, "diff", `origin/${config.worktree.baseBranch}..HEAD`], {
|
|
3467
4007
|
cwd: worktreePath,
|
|
3468
4008
|
encoding: "utf-8",
|
|
3469
4009
|
timeout: 30000,
|
|
3470
|
-
maxBuffer:
|
|
4010
|
+
maxBuffer: MAX_OUTPUT_BUFFER3
|
|
3471
4011
|
});
|
|
3472
4012
|
} catch {
|
|
3473
4013
|
diff = "(unable to retrieve diff)";
|
|
@@ -3484,14 +4024,16 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
3484
4024
|
"```"
|
|
3485
4025
|
].join(`
|
|
3486
4026
|
`);
|
|
3487
|
-
const leanSources = config.claude.leanSettingSources;
|
|
3488
4027
|
const output = execFileSync5("claude", [
|
|
3489
4028
|
"--print",
|
|
3490
4029
|
"--model",
|
|
3491
4030
|
"sonnet",
|
|
3492
4031
|
"--max-turns",
|
|
3493
4032
|
"10",
|
|
3494
|
-
...
|
|
4033
|
+
...implementRunContainmentCliArgs({
|
|
4034
|
+
worktree: worktreePath,
|
|
4035
|
+
readOnly: true
|
|
4036
|
+
}),
|
|
3495
4037
|
"--",
|
|
3496
4038
|
reviewPrompt
|
|
3497
4039
|
], {
|
|
@@ -3499,11 +4041,12 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
3499
4041
|
encoding: "utf-8",
|
|
3500
4042
|
timeout: config.verification.timeout,
|
|
3501
4043
|
stdio: "pipe",
|
|
3502
|
-
maxBuffer:
|
|
4044
|
+
maxBuffer: MAX_OUTPUT_BUFFER3,
|
|
4045
|
+
env: containedEnv()
|
|
3503
4046
|
});
|
|
3504
4047
|
return parseReviewFindings(output);
|
|
3505
4048
|
} catch (err) {
|
|
3506
|
-
log.error(
|
|
4049
|
+
log.error(TAG11, `Deep review failed: ${err instanceof Error ? err.message : err}`);
|
|
3507
4050
|
return [];
|
|
3508
4051
|
} finally {
|
|
3509
4052
|
if (devServer && !devServer.killed) {
|
|
@@ -3529,7 +4072,6 @@ function attemptAutoFix(worktreePath, config, errors) {
|
|
|
3529
4072
|
"```"
|
|
3530
4073
|
].join(`
|
|
3531
4074
|
`);
|
|
3532
|
-
const leanSources = config.claude.leanSettingSources;
|
|
3533
4075
|
const args = [
|
|
3534
4076
|
"--print",
|
|
3535
4077
|
"--model",
|
|
@@ -3538,16 +4080,17 @@ function attemptAutoFix(worktreePath, config, errors) {
|
|
|
3538
4080
|
"50",
|
|
3539
4081
|
"--allowedTools",
|
|
3540
4082
|
"Bash,Read,Write,Edit,Glob,Grep",
|
|
3541
|
-
...
|
|
4083
|
+
...implementRunContainmentCliArgs({ worktree: worktreePath }),
|
|
3542
4084
|
"--",
|
|
3543
4085
|
fixPrompt
|
|
3544
4086
|
];
|
|
3545
|
-
log.info(
|
|
4087
|
+
log.info(TAG11, "Spawning Claude for auto-fix...");
|
|
3546
4088
|
execFileSync5("claude", args, {
|
|
3547
4089
|
cwd: worktreePath,
|
|
3548
4090
|
timeout: config.verification.timeout,
|
|
3549
4091
|
stdio: "pipe",
|
|
3550
|
-
maxBuffer:
|
|
4092
|
+
maxBuffer: MAX_OUTPUT_BUFFER3,
|
|
4093
|
+
env: containedEnv()
|
|
3551
4094
|
});
|
|
3552
4095
|
}
|
|
3553
4096
|
async function reportFindings(client, cardId, result, recovery) {
|
|
@@ -3580,7 +4123,7 @@ async function reportFindings(client, cardId, result, recovery) {
|
|
|
3580
4123
|
try {
|
|
3581
4124
|
await client.createSubtask(cardId, title);
|
|
3582
4125
|
} catch (err) {
|
|
3583
|
-
log.error(
|
|
4126
|
+
log.error(TAG11, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
|
|
3584
4127
|
}
|
|
3585
4128
|
}));
|
|
3586
4129
|
if (overflow > 0) {
|
|
@@ -3588,7 +4131,7 @@ async function reportFindings(client, cardId, result, recovery) {
|
|
|
3588
4131
|
await client.createSubtask(cardId, `...and ${overflow} more issues`);
|
|
3589
4132
|
} catch {}
|
|
3590
4133
|
}
|
|
3591
|
-
log.info(
|
|
4134
|
+
log.info(TAG11, `Reported ${Math.min(items.length, maxSubtasks)} finding(s) as subtasks on card ${cardId}`);
|
|
3592
4135
|
}
|
|
3593
4136
|
function combineOutput(err) {
|
|
3594
4137
|
const stderr = err?.stderr?.toString() ?? "";
|
|
@@ -3621,7 +4164,7 @@ function parseTestFailures(err, timeout) {
|
|
|
3621
4164
|
}
|
|
3622
4165
|
if (e?.code === "ENOBUFS") {
|
|
3623
4166
|
return [
|
|
3624
|
-
`Test output exceeded the ${
|
|
4167
|
+
`Test output exceeded the ${MAX_OUTPUT_BUFFER3 / (1024 * 1024)}MB capture limit and the run was killed — ` + "the suite's real result is unknown. Quieten the reporter or raise the limit."
|
|
3625
4168
|
];
|
|
3626
4169
|
}
|
|
3627
4170
|
const combined = combineOutput(err);
|
|
@@ -3712,7 +4255,7 @@ async function probeDevServer(port, timeoutMs = 5000) {
|
|
|
3712
4255
|
}
|
|
3713
4256
|
|
|
3714
4257
|
// src/gate-collectors.ts
|
|
3715
|
-
var
|
|
4258
|
+
var TAG12 = "gate-collectors";
|
|
3716
4259
|
async function resolveStageGate(client, card) {
|
|
3717
4260
|
const currentStage = card.current_stage;
|
|
3718
4261
|
const playbookId = card.playbook_id;
|
|
@@ -3730,7 +4273,7 @@ async function resolveStageGate(client, card) {
|
|
|
3730
4273
|
return null;
|
|
3731
4274
|
return { stage: resolution.stage, gate };
|
|
3732
4275
|
} catch (err) {
|
|
3733
|
-
log.warn(
|
|
4276
|
+
log.warn(TAG12, `resolveStageGate failed for stage "${currentStage}": ${err instanceof Error ? err.message : err}`);
|
|
3734
4277
|
return null;
|
|
3735
4278
|
}
|
|
3736
4279
|
}
|
|
@@ -3765,8 +4308,8 @@ class BuildGreenCollector {
|
|
|
3765
4308
|
async collect(_context) {
|
|
3766
4309
|
const doBuild = this.deps.runBuild ?? runBuild;
|
|
3767
4310
|
const doLint = this.deps.runLint ?? runLint;
|
|
3768
|
-
const buildErrors = doBuild(this.deps.worktreePath, this.deps.buildTimeout);
|
|
3769
|
-
const lintWarnings = doLint(this.deps.worktreePath, this.deps.lintTimeout);
|
|
4311
|
+
const buildErrors = await doBuild(this.deps.worktreePath, this.deps.buildTimeout, this.deps.sandbox);
|
|
4312
|
+
const lintWarnings = await doLint(this.deps.worktreePath, this.deps.lintTimeout, this.deps.sandbox);
|
|
3770
4313
|
const buildPassed = buildErrors.length === 0;
|
|
3771
4314
|
const lintPassed = lintWarnings.length === 0;
|
|
3772
4315
|
const result = buildPassed ? "passed" : "failed";
|
|
@@ -3862,7 +4405,7 @@ function buildGateCollectorRegistry(deps) {
|
|
|
3862
4405
|
async function collectGateEvidence(registry, context) {
|
|
3863
4406
|
const collector = registry[context.gate.kind];
|
|
3864
4407
|
if (!collector) {
|
|
3865
|
-
log.info(
|
|
4408
|
+
log.info(TAG12, `No collector for gate kind "${context.gate.kind}" — reporting blocked`);
|
|
3866
4409
|
return {
|
|
3867
4410
|
result: "blocked",
|
|
3868
4411
|
structured: {
|
|
@@ -3874,14 +4417,15 @@ async function collectGateEvidence(registry, context) {
|
|
|
3874
4417
|
return await collector.collect(context);
|
|
3875
4418
|
} catch (err) {
|
|
3876
4419
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3877
|
-
log.warn(
|
|
4420
|
+
log.warn(TAG12, `Collector for "${context.gate.kind}" threw: ${msg} — reporting blocked`);
|
|
3878
4421
|
return { result: "blocked", structured: { error: msg } };
|
|
3879
4422
|
}
|
|
3880
4423
|
}
|
|
3881
4424
|
// src/git-diff-stat.ts
|
|
3882
4425
|
init_log();
|
|
4426
|
+
init_run_containment();
|
|
3883
4427
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
3884
|
-
var
|
|
4428
|
+
var TAG13 = "git-diff-stat";
|
|
3885
4429
|
var MAX_CHANGED_FILES = 30;
|
|
3886
4430
|
function parseNumstat(raw, maxFiles = MAX_CHANGED_FILES) {
|
|
3887
4431
|
const files = [];
|
|
@@ -3960,10 +4504,10 @@ function formatDiffSummary(diff, maxFiles = 100) {
|
|
|
3960
4504
|
}
|
|
3961
4505
|
function captureDiffStat(worktreePath, baseBranch, maxFiles = MAX_CHANGED_FILES) {
|
|
3962
4506
|
try {
|
|
3963
|
-
const raw = execFileSync6("git", ["diff", "--numstat", `${baseBranch}...HEAD`], { cwd: worktreePath, encoding: "utf-8", timeout: 30000 });
|
|
4507
|
+
const raw = execFileSync6("git", [...GIT_NO_HOOKS, "diff", "--numstat", `${baseBranch}...HEAD`], { cwd: worktreePath, encoding: "utf-8", timeout: 30000 });
|
|
3964
4508
|
return parseNumstat(raw, maxFiles);
|
|
3965
4509
|
} catch (err) {
|
|
3966
|
-
log.warn(
|
|
4510
|
+
log.warn(TAG13, "git diff --numstat failed", {
|
|
3967
4511
|
event: "diff_stat_failed",
|
|
3968
4512
|
error: err instanceof Error ? err.message : String(err)
|
|
3969
4513
|
});
|
|
@@ -3976,7 +4520,7 @@ init_git_pr();
|
|
|
3976
4520
|
|
|
3977
4521
|
// src/harmony-client.ts
|
|
3978
4522
|
init_log();
|
|
3979
|
-
var
|
|
4523
|
+
var TAG15 = "harmony-client";
|
|
3980
4524
|
function readClientConfig(env) {
|
|
3981
4525
|
const apiUrl = env.HARMONY_API_URL?.trim();
|
|
3982
4526
|
const apiKey = env.HARMONY_API_KEY?.trim();
|
|
@@ -4028,7 +4572,7 @@ class HarmonyClient {
|
|
|
4028
4572
|
purpose: "gate_evaluation"
|
|
4029
4573
|
});
|
|
4030
4574
|
if (!response.ok) {
|
|
4031
|
-
log.warn(
|
|
4575
|
+
log.warn(TAG15, `Oracle fetch for stage ${stageId} returned ${response.status} — no oracle read, the gate will report blocked`);
|
|
4032
4576
|
return null;
|
|
4033
4577
|
}
|
|
4034
4578
|
const body = await response.json();
|
|
@@ -4108,150 +4652,14 @@ function relayAgentEvent(draft) {
|
|
|
4108
4652
|
}
|
|
4109
4653
|
return { type: "agent_event", event: draft };
|
|
4110
4654
|
}
|
|
4111
|
-
// src/repair-sandbox.ts
|
|
4112
|
-
init_log();
|
|
4113
|
-
import { randomUUID } from "node:crypto";
|
|
4114
|
-
import { promisify as promisify3 } from "node:util";
|
|
4115
|
-
var TAG15 = "repair-sandbox";
|
|
4116
|
-
async function dockerExec(argv, opts) {
|
|
4117
|
-
const { execFile: execFile3 } = await import("node:child_process");
|
|
4118
|
-
return promisify3(execFile3)("docker", argv, {
|
|
4119
|
-
encoding: "utf-8",
|
|
4120
|
-
...opts
|
|
4121
|
-
});
|
|
4122
|
-
}
|
|
4123
|
-
var MAX_OUTPUT_BUFFER3 = 20971520;
|
|
4124
|
-
var PROBE_TIMEOUT_MS = 1e4;
|
|
4125
|
-
var SANDBOX_MOUNT = "/repo";
|
|
4126
|
-
var SANDBOX_MEMORY = "4g";
|
|
4127
|
-
var SANDBOX_PIDS = "512";
|
|
4128
|
-
var dockerProbe = null;
|
|
4129
|
-
async function sandboxAvailable() {
|
|
4130
|
-
if (dockerProbe === true)
|
|
4131
|
-
return true;
|
|
4132
|
-
try {
|
|
4133
|
-
await dockerExec(["version", "--format", "{{.Server.Version}}"], {
|
|
4134
|
-
timeout: PROBE_TIMEOUT_MS
|
|
4135
|
-
});
|
|
4136
|
-
dockerProbe = true;
|
|
4137
|
-
} catch {
|
|
4138
|
-
dockerProbe = false;
|
|
4139
|
-
}
|
|
4140
|
-
return dockerProbe;
|
|
4141
|
-
}
|
|
4142
|
-
function __resetSandboxProbe() {
|
|
4143
|
-
dockerProbe = null;
|
|
4144
|
-
}
|
|
4145
|
-
async function removeContainer(name) {
|
|
4146
|
-
try {
|
|
4147
|
-
await dockerExec(["rm", "--force", name], { timeout: PROBE_TIMEOUT_MS });
|
|
4148
|
-
log.warn(TAG15, `removed the timed-out sandbox container ${name}`);
|
|
4149
|
-
} catch {}
|
|
4150
|
-
}
|
|
4151
|
-
function sandboxRunArgs(image, worktree, command, name) {
|
|
4152
|
-
return [
|
|
4153
|
-
"run",
|
|
4154
|
-
"--rm",
|
|
4155
|
-
...name ? ["--name", name] : [],
|
|
4156
|
-
"--network=none",
|
|
4157
|
-
"--cap-drop=ALL",
|
|
4158
|
-
"--security-opt=no-new-privileges",
|
|
4159
|
-
`--memory=${SANDBOX_MEMORY}`,
|
|
4160
|
-
`--pids-limit=${SANDBOX_PIDS}`,
|
|
4161
|
-
...typeof process.getuid === "function" && typeof process.getgid === "function" ? ["--user", `${process.getuid()}:${process.getgid()}`] : [],
|
|
4162
|
-
"--env",
|
|
4163
|
-
"HOME=/tmp",
|
|
4164
|
-
"--volume",
|
|
4165
|
-
`${worktree}:${SANDBOX_MOUNT}`,
|
|
4166
|
-
"--workdir",
|
|
4167
|
-
SANDBOX_MOUNT,
|
|
4168
|
-
"--entrypoint",
|
|
4169
|
-
command.cmd,
|
|
4170
|
-
image,
|
|
4171
|
-
...command.args
|
|
4172
|
-
];
|
|
4173
|
-
}
|
|
4174
|
-
async function runInSandbox(args) {
|
|
4175
|
-
const name = `harmony-repair-${randomUUID()}`;
|
|
4176
|
-
const argv = sandboxRunArgs(args.image, args.worktree, args.command, name);
|
|
4177
|
-
log.info(TAG15, `sandbox: ${args.command.cmd} ${args.command.args.join(" ")} (image ${args.image})`);
|
|
4178
|
-
try {
|
|
4179
|
-
const { stdout } = await dockerExec(argv, {
|
|
4180
|
-
timeout: args.timeoutMs,
|
|
4181
|
-
maxBuffer: MAX_OUTPUT_BUFFER3
|
|
4182
|
-
});
|
|
4183
|
-
return { passed: true, output: stdout ?? "" };
|
|
4184
|
-
} catch (err) {
|
|
4185
|
-
const e = err;
|
|
4186
|
-
const output = `${e.stdout ?? ""}${e.stderr ?? ""}`;
|
|
4187
|
-
if (typeof e.code !== "number") {
|
|
4188
|
-
const timedOut = e.killed === true || e.signal != null;
|
|
4189
|
-
if (timedOut)
|
|
4190
|
-
await removeContainer(name);
|
|
4191
|
-
return {
|
|
4192
|
-
passed: false,
|
|
4193
|
-
output,
|
|
4194
|
-
sandboxError: timedOut ? `the sandbox timed out after ${args.timeoutMs}ms` : `the sandbox did not run: ${e.message ?? "unknown error"}`
|
|
4195
|
-
};
|
|
4196
|
-
}
|
|
4197
|
-
if (e.code === 125) {
|
|
4198
|
-
return {
|
|
4199
|
-
passed: false,
|
|
4200
|
-
output,
|
|
4201
|
-
sandboxError: `the sandbox could not start (image "${args.image}" missing or unusable)`
|
|
4202
|
-
};
|
|
4203
|
-
}
|
|
4204
|
-
return { passed: false, output };
|
|
4205
|
-
}
|
|
4206
|
-
}
|
|
4207
|
-
// src/run-sizing.ts
|
|
4208
|
-
init_dist();
|
|
4209
4655
|
|
|
4210
|
-
// src/
|
|
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
|
-
}
|
|
4656
|
+
// src/index.ts
|
|
4657
|
+
init_run_containment();
|
|
4253
4658
|
|
|
4254
4659
|
// src/run-sizing.ts
|
|
4660
|
+
init_dist();
|
|
4661
|
+
init_confine_to_repo();
|
|
4662
|
+
init_runner();
|
|
4255
4663
|
var SIZING_MODEL = "haiku";
|
|
4256
4664
|
var SIZING_MAX_TURNS = 25;
|
|
4257
4665
|
var SIZING_MAX_BUDGET_USD = 0.75;
|
|
@@ -4425,6 +4833,10 @@ async function sizeRun(deps) {
|
|
|
4425
4833
|
clearTimeout(timer);
|
|
4426
4834
|
}
|
|
4427
4835
|
}
|
|
4836
|
+
|
|
4837
|
+
// src/index.ts
|
|
4838
|
+
init_runner();
|
|
4839
|
+
|
|
4428
4840
|
// src/stage-run.ts
|
|
4429
4841
|
async function runStage(request, deps) {
|
|
4430
4842
|
const events = [];
|
|
@@ -4454,6 +4866,7 @@ init_log();
|
|
|
4454
4866
|
import { execFileSync as execFileSync8, execSync } from "node:child_process";
|
|
4455
4867
|
import { existsSync as existsSync3, readdirSync as readdirSync2, rmSync } from "node:fs";
|
|
4456
4868
|
import { resolve as resolve3 } from "node:path";
|
|
4869
|
+
init_run_containment();
|
|
4457
4870
|
var TAG16 = "worktree";
|
|
4458
4871
|
|
|
4459
4872
|
class WorktreeBaseError extends Error {
|
|
@@ -4462,7 +4875,7 @@ class WorktreeBaseError extends Error {
|
|
|
4462
4875
|
this.name = "WorktreeBaseError";
|
|
4463
4876
|
}
|
|
4464
4877
|
}
|
|
4465
|
-
function fetchBaseBranch(repoRoot, baseBranch, attempts = 3, fetchImpl = (root, branch) => execFileSync8("git", ["fetch", "origin", branch], {
|
|
4878
|
+
function fetchBaseBranch(repoRoot, baseBranch, attempts = 3, fetchImpl = (root, branch) => execFileSync8("git", [...GIT_NO_HOOKS, "fetch", "origin", branch], {
|
|
4466
4879
|
cwd: root,
|
|
4467
4880
|
stdio: "pipe"
|
|
4468
4881
|
})) {
|
|
@@ -4515,7 +4928,14 @@ function resolveContinuationTarget(branchName, continueRequested, failedBranchPr
|
|
|
4515
4928
|
reason: "fresh"
|
|
4516
4929
|
};
|
|
4517
4930
|
}
|
|
4518
|
-
function fetchExistingBranch(repoRoot, branchName, attempts = 3, lsRemoteImpl = (root, branch) => execFileSync8("git", [
|
|
4931
|
+
function fetchExistingBranch(repoRoot, branchName, attempts = 3, lsRemoteImpl = (root, branch) => execFileSync8("git", [
|
|
4932
|
+
...GIT_NO_HOOKS,
|
|
4933
|
+
"ls-remote",
|
|
4934
|
+
"--exit-code",
|
|
4935
|
+
"origin",
|
|
4936
|
+
`refs/heads/${branch}`
|
|
4937
|
+
], { cwd: root, stdio: "pipe" }), fetchImpl = (root, branch) => execFileSync8("git", [
|
|
4938
|
+
...GIT_NO_HOOKS,
|
|
4519
4939
|
"fetch",
|
|
4520
4940
|
"origin",
|
|
4521
4941
|
`+refs/heads/${branch}:refs/remotes/origin/${branch}`
|
|
@@ -4549,7 +4969,7 @@ function fetchExistingBranch(repoRoot, branchName, attempts = 3, lsRemoteImpl =
|
|
|
4549
4969
|
}
|
|
4550
4970
|
function readWorktreeHead(worktreePath) {
|
|
4551
4971
|
try {
|
|
4552
|
-
return execFileSync8("git", ["rev-parse", "HEAD"], {
|
|
4972
|
+
return execFileSync8("git", [...GIT_NO_HOOKS, "rev-parse", "HEAD"], {
|
|
4553
4973
|
cwd: worktreePath,
|
|
4554
4974
|
encoding: "utf-8"
|
|
4555
4975
|
}).trim();
|
|
@@ -4558,7 +4978,7 @@ function readWorktreeHead(worktreePath) {
|
|
|
4558
4978
|
}
|
|
4559
4979
|
}
|
|
4560
4980
|
function createWorktree(basePath, baseBranch, branchName, opts = {}) {
|
|
4561
|
-
const repoRoot = execFileSync8("git", ["rev-parse", "--show-toplevel"], {
|
|
4981
|
+
const repoRoot = execFileSync8("git", [...GIT_NO_HOOKS, "rev-parse", "--show-toplevel"], {
|
|
4562
4982
|
encoding: "utf-8"
|
|
4563
4983
|
}).trim();
|
|
4564
4984
|
const worktreeDir = resolve3(repoRoot, basePath, branchName);
|
|
@@ -4567,7 +4987,7 @@ function createWorktree(basePath, baseBranch, branchName, opts = {}) {
|
|
|
4567
4987
|
cleanupWorktree(worktreeDir, branchName);
|
|
4568
4988
|
}
|
|
4569
4989
|
try {
|
|
4570
|
-
execFileSync8("git", ["worktree", "prune", "--expire=now"], {
|
|
4990
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "prune", "--expire=now"], {
|
|
4571
4991
|
cwd: repoRoot,
|
|
4572
4992
|
stdio: "pipe"
|
|
4573
4993
|
});
|
|
@@ -4576,37 +4996,54 @@ function createWorktree(basePath, baseBranch, branchName, opts = {}) {
|
|
|
4576
4996
|
const startRef = resolveWorktreeStartRef(baseBranch, branchName, opts.continueExisting ?? false, () => opts.branchExistsOnOrigin ?? fetchExistingBranch(repoRoot, branchName));
|
|
4577
4997
|
log.info(TAG16, `Creating worktree: ${worktreeDir} (branch: ${branchName}, base: ${startRef})`);
|
|
4578
4998
|
try {
|
|
4579
|
-
execFileSync8("git", [
|
|
4999
|
+
execFileSync8("git", [
|
|
5000
|
+
...GIT_NO_HOOKS,
|
|
5001
|
+
"worktree",
|
|
5002
|
+
"add",
|
|
5003
|
+
"-B",
|
|
5004
|
+
branchName,
|
|
5005
|
+
worktreeDir,
|
|
5006
|
+
startRef
|
|
5007
|
+
], { cwd: repoRoot, stdio: "pipe" });
|
|
4580
5008
|
} catch (err) {
|
|
4581
5009
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4582
5010
|
log.warn(TAG16, `worktree add failed, attempting forced recovery: ${msg}`);
|
|
4583
5011
|
removeWorktreeHoldingBranch(repoRoot, branchName, worktreeDir);
|
|
4584
5012
|
try {
|
|
4585
|
-
execFileSync8("git", ["worktree", "remove", worktreeDir, "--force"], {
|
|
5013
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "remove", worktreeDir, "--force"], {
|
|
4586
5014
|
cwd: repoRoot,
|
|
4587
5015
|
stdio: "pipe"
|
|
4588
5016
|
});
|
|
4589
5017
|
} catch {}
|
|
4590
5018
|
try {
|
|
4591
|
-
execFileSync8("git", ["worktree", "prune", "--expire=now"], {
|
|
5019
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "prune", "--expire=now"], {
|
|
4592
5020
|
cwd: repoRoot,
|
|
4593
5021
|
stdio: "pipe"
|
|
4594
5022
|
});
|
|
4595
5023
|
} catch {}
|
|
4596
5024
|
try {
|
|
4597
|
-
execFileSync8("git", ["branch", "-D", branchName], {
|
|
5025
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "branch", "-D", branchName], {
|
|
4598
5026
|
cwd: repoRoot,
|
|
4599
5027
|
stdio: "pipe"
|
|
4600
5028
|
});
|
|
4601
5029
|
} catch {}
|
|
4602
|
-
execFileSync8("git", [
|
|
5030
|
+
execFileSync8("git", [
|
|
5031
|
+
...GIT_NO_HOOKS,
|
|
5032
|
+
"worktree",
|
|
5033
|
+
"add",
|
|
5034
|
+
"-B",
|
|
5035
|
+
branchName,
|
|
5036
|
+
worktreeDir,
|
|
5037
|
+
startRef
|
|
5038
|
+
], { cwd: repoRoot, stdio: "pipe" });
|
|
4603
5039
|
}
|
|
4604
5040
|
log.info(TAG16, "Installing dependencies in worktree...");
|
|
4605
5041
|
try {
|
|
4606
|
-
execSync(installCommand(), {
|
|
5042
|
+
execSync(installCommand(true), {
|
|
4607
5043
|
cwd: worktreeDir,
|
|
4608
5044
|
stdio: "pipe",
|
|
4609
|
-
timeout: 60000
|
|
5045
|
+
timeout: 60000,
|
|
5046
|
+
env: containedEnv()
|
|
4610
5047
|
});
|
|
4611
5048
|
} catch {
|
|
4612
5049
|
log.warn(TAG16, "Install failed (may be fine if deps are hoisted)");
|
|
@@ -4625,7 +5062,7 @@ function containsForeignWorktrees(dir) {
|
|
|
4625
5062
|
return children.some((child) => existsSync3(resolve3(dir, child, ".git")));
|
|
4626
5063
|
}
|
|
4627
5064
|
function cleanupWorktree(worktreePath, branchName) {
|
|
4628
|
-
const repoRoot = execFileSync8("git", ["rev-parse", "--show-toplevel"], {
|
|
5065
|
+
const repoRoot = execFileSync8("git", [...GIT_NO_HOOKS, "rev-parse", "--show-toplevel"], {
|
|
4629
5066
|
encoding: "utf-8"
|
|
4630
5067
|
}).trim();
|
|
4631
5068
|
if (existsSync3(worktreePath)) {
|
|
@@ -4633,7 +5070,7 @@ function cleanupWorktree(worktreePath, branchName) {
|
|
|
4633
5070
|
throw new Error(`Refusing to remove ${worktreePath}: it is not a git worktree itself, ` + `but git worktrees live directly beneath it. Removing it would ` + `destroy them — pass the individual worktree paths instead (#928).`);
|
|
4634
5071
|
}
|
|
4635
5072
|
try {
|
|
4636
|
-
execFileSync8("git", ["worktree", "remove", worktreePath, "--force"], {
|
|
5073
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "remove", worktreePath, "--force"], {
|
|
4637
5074
|
cwd: repoRoot,
|
|
4638
5075
|
stdio: "pipe"
|
|
4639
5076
|
});
|
|
@@ -4644,7 +5081,7 @@ function cleanupWorktree(worktreePath, branchName) {
|
|
|
4644
5081
|
rmSync(worktreePath, { recursive: true, force: true });
|
|
4645
5082
|
}
|
|
4646
5083
|
try {
|
|
4647
|
-
execFileSync8("git", ["worktree", "prune", "--expire=now"], {
|
|
5084
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "prune", "--expire=now"], {
|
|
4648
5085
|
cwd: repoRoot,
|
|
4649
5086
|
stdio: "pipe"
|
|
4650
5087
|
});
|
|
@@ -4652,7 +5089,7 @@ function cleanupWorktree(worktreePath, branchName) {
|
|
|
4652
5089
|
}
|
|
4653
5090
|
} else {
|
|
4654
5091
|
try {
|
|
4655
|
-
execFileSync8("git", ["worktree", "prune", "--expire=now"], {
|
|
5092
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "prune", "--expire=now"], {
|
|
4656
5093
|
cwd: repoRoot,
|
|
4657
5094
|
stdio: "pipe"
|
|
4658
5095
|
});
|
|
@@ -4660,7 +5097,7 @@ function cleanupWorktree(worktreePath, branchName) {
|
|
|
4660
5097
|
}
|
|
4661
5098
|
if (branchName) {
|
|
4662
5099
|
try {
|
|
4663
|
-
execFileSync8("git", ["branch", "-D", branchName], {
|
|
5100
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "branch", "-D", branchName], {
|
|
4664
5101
|
cwd: repoRoot,
|
|
4665
5102
|
stdio: "pipe"
|
|
4666
5103
|
});
|
|
@@ -4670,7 +5107,7 @@ function cleanupWorktree(worktreePath, branchName) {
|
|
|
4670
5107
|
function removeWorktreeHoldingBranch(repoRoot, branchName, exceptDir) {
|
|
4671
5108
|
let listing;
|
|
4672
5109
|
try {
|
|
4673
|
-
listing = execFileSync8("git", ["worktree", "list", "--porcelain"], {
|
|
5110
|
+
listing = execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "list", "--porcelain"], {
|
|
4674
5111
|
cwd: repoRoot,
|
|
4675
5112
|
encoding: "utf-8",
|
|
4676
5113
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -4698,7 +5135,7 @@ function removeWorktreeHoldingBranch(repoRoot, branchName, exceptDir) {
|
|
|
4698
5135
|
if (exceptDir && resolve3(holderPath) === resolve3(exceptDir))
|
|
4699
5136
|
return null;
|
|
4700
5137
|
try {
|
|
4701
|
-
execFileSync8("git", ["worktree", "remove", holderPath, "--force"], {
|
|
5138
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "remove", holderPath, "--force"], {
|
|
4702
5139
|
cwd: repoRoot,
|
|
4703
5140
|
stdio: "pipe"
|
|
4704
5141
|
});
|
|
@@ -4708,7 +5145,7 @@ function removeWorktreeHoldingBranch(repoRoot, branchName, exceptDir) {
|
|
|
4708
5145
|
return null;
|
|
4709
5146
|
}
|
|
4710
5147
|
try {
|
|
4711
|
-
execFileSync8("git", ["worktree", "prune", "--expire=now"], {
|
|
5148
|
+
execFileSync8("git", [...GIT_NO_HOOKS, "worktree", "prune", "--expire=now"], {
|
|
4712
5149
|
cwd: repoRoot,
|
|
4713
5150
|
stdio: "pipe"
|
|
4714
5151
|
});
|
|
@@ -4716,13 +5153,19 @@ function removeWorktreeHoldingBranch(repoRoot, branchName, exceptDir) {
|
|
|
4716
5153
|
return holderPath;
|
|
4717
5154
|
}
|
|
4718
5155
|
function resolveRepoRoot() {
|
|
4719
|
-
return execFileSync8("git", ["rev-parse", "--show-toplevel"], {
|
|
5156
|
+
return execFileSync8("git", [...GIT_NO_HOOKS, "rev-parse", "--show-toplevel"], {
|
|
4720
5157
|
encoding: "utf-8"
|
|
4721
5158
|
}).trim();
|
|
4722
5159
|
}
|
|
4723
5160
|
function localBranchExists(branchName, repoRoot) {
|
|
4724
5161
|
try {
|
|
4725
|
-
execFileSync8("git", [
|
|
5162
|
+
execFileSync8("git", [
|
|
5163
|
+
...GIT_NO_HOOKS,
|
|
5164
|
+
"show-ref",
|
|
5165
|
+
"--verify",
|
|
5166
|
+
"--quiet",
|
|
5167
|
+
`refs/heads/${branchName}`
|
|
5168
|
+
], { cwd: repoRoot, stdio: "ignore" });
|
|
4726
5169
|
return true;
|
|
4727
5170
|
} catch {
|
|
4728
5171
|
return false;
|
|
@@ -4732,7 +5175,7 @@ function branchAheadOfItsRemote(branchName, repoRoot = resolveRepoRoot()) {
|
|
|
4732
5175
|
if (!localBranchExists(branchName, repoRoot))
|
|
4733
5176
|
return false;
|
|
4734
5177
|
try {
|
|
4735
|
-
const out = execFileSync8("git", ["rev-list", branchName, "--not", "--remotes=origin"], { cwd: repoRoot, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
5178
|
+
const out = execFileSync8("git", [...GIT_NO_HOOKS, "rev-list", branchName, "--not", "--remotes=origin"], { cwd: repoRoot, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
4736
5179
|
return out.length > 0;
|
|
4737
5180
|
} catch {
|
|
4738
5181
|
return false;
|
|
@@ -4785,11 +5228,14 @@ function makeBranchName(shortId, title, prefix = "agent-attempts/") {
|
|
|
4785
5228
|
// src/index.ts
|
|
4786
5229
|
var MOTOR_NAME = "harmony-harness";
|
|
4787
5230
|
export {
|
|
5231
|
+
writeOnlyDenyPaths,
|
|
4788
5232
|
waitForDevServer,
|
|
5233
|
+
verificationSandbox,
|
|
4789
5234
|
validateGitProviderCli,
|
|
4790
5235
|
upsertReviewedSha,
|
|
4791
5236
|
upsertCiReviewRequestedSha,
|
|
4792
5237
|
updateExistingPr,
|
|
5238
|
+
toolchainCacheDirectories,
|
|
4793
5239
|
testCommand,
|
|
4794
5240
|
terminateGroup,
|
|
4795
5241
|
teardownWorktree,
|
|
@@ -4802,6 +5248,7 @@ export {
|
|
|
4802
5248
|
sizingEventSource,
|
|
4803
5249
|
sizeRun,
|
|
4804
5250
|
signalGroup,
|
|
5251
|
+
secretEnvKeysToStrip,
|
|
4805
5252
|
sanitizeCiText,
|
|
4806
5253
|
sandboxRunArgs,
|
|
4807
5254
|
sandboxAvailable,
|
|
@@ -4856,7 +5303,13 @@ export {
|
|
|
4856
5303
|
isPrOnOrigin,
|
|
4857
5304
|
isInsideTree,
|
|
4858
5305
|
installCommand,
|
|
5306
|
+
implementRunToolPolicy,
|
|
5307
|
+
implementRunContainmentCliArgs,
|
|
5308
|
+
implementRunContainment,
|
|
5309
|
+
hostPersistencePaths,
|
|
5310
|
+
harmonyMcpServer,
|
|
4859
5311
|
gradeOracleRed,
|
|
5312
|
+
gitMetadataDenyPaths,
|
|
4860
5313
|
getPrStatus,
|
|
4861
5314
|
getPrFailedChecks,
|
|
4862
5315
|
getHeadSha,
|
|
@@ -4886,10 +5339,14 @@ export {
|
|
|
4886
5339
|
deriveCiStatus,
|
|
4887
5340
|
decidePrBranch,
|
|
4888
5341
|
decideConfinedTool,
|
|
5342
|
+
credentialWriteToolDeny,
|
|
5343
|
+
credentialToolDeny,
|
|
5344
|
+
credentialDirectories,
|
|
4889
5345
|
credentialAccessDeny,
|
|
4890
5346
|
createWorktree,
|
|
4891
5347
|
createPullRequest,
|
|
4892
5348
|
cooldownMsFor,
|
|
5349
|
+
containedEnv,
|
|
4893
5350
|
confineToRepo,
|
|
4894
5351
|
collectSizingOutput,
|
|
4895
5352
|
collectGateEvidence,
|
|
@@ -4908,14 +5365,17 @@ export {
|
|
|
4908
5365
|
buildCommand,
|
|
4909
5366
|
branchAheadOfItsRemote,
|
|
4910
5367
|
attemptAutoFix,
|
|
5368
|
+
assertNoProjectSandboxOverride,
|
|
4911
5369
|
_resetCache,
|
|
4912
5370
|
__resetSandboxProbe,
|
|
4913
5371
|
WorktreeBaseError,
|
|
4914
5372
|
SdkAgentRunner,
|
|
4915
5373
|
SIZING_MODEL,
|
|
4916
5374
|
SDK_ALLOWED_TOOLS,
|
|
5375
|
+
SANDBOX_STARTUP_GRACE_MS,
|
|
4917
5376
|
SANDBOX_MOUNT,
|
|
4918
5377
|
ReviewPassedCollector,
|
|
5378
|
+
ProjectSandboxOverrideError,
|
|
4919
5379
|
OracleRedCollector,
|
|
4920
5380
|
OracleCollector,
|
|
4921
5381
|
ORACLE_RUNNER_HINTS,
|
|
@@ -4925,8 +5385,10 @@ export {
|
|
|
4925
5385
|
MAX_IMPLEMENT_MODEL,
|
|
4926
5386
|
MAX_CHANGED_FILES,
|
|
4927
5387
|
JUDGE_MODEL,
|
|
5388
|
+
IMPLEMENT_ALLOWED_DOMAINS,
|
|
4928
5389
|
HarmonyClient,
|
|
4929
5390
|
HARMONY_CREDENTIAL_KEYS,
|
|
5391
|
+
GIT_NO_HOOKS,
|
|
4930
5392
|
GATE_CONFIG_ERROR_MARK,
|
|
4931
5393
|
GATE_CONFIG_ERROR_KEY,
|
|
4932
5394
|
DevServerReadinessError,
|