@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/cli.js
CHANGED
|
@@ -501,7 +501,6 @@ var init_stageHandoff = __esm(() => {
|
|
|
501
501
|
|
|
502
502
|
// ../harmony-shared/dist/types.js
|
|
503
503
|
var init_types = () => {};
|
|
504
|
-
|
|
505
504
|
// ../harmony-shared/dist/index.js
|
|
506
505
|
var init_dist = __esm(() => {
|
|
507
506
|
init_agentStaleness();
|
|
@@ -611,6 +610,492 @@ var init_log = __esm(() => {
|
|
|
611
610
|
};
|
|
612
611
|
});
|
|
613
612
|
|
|
613
|
+
// src/confine-to-repo.ts
|
|
614
|
+
import { realpathSync } from "node:fs";
|
|
615
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, parse, resolve as resolve2, sep as sep2 } from "node:path";
|
|
616
|
+
function isGitMetadata(repoRoot, candidate) {
|
|
617
|
+
const rel = candidate.startsWith(repoRoot) ? candidate.slice(repoRoot.length) : candidate;
|
|
618
|
+
return rel.split(/[\\/]/).some((segment) => segment.toLowerCase() === ".git");
|
|
619
|
+
}
|
|
620
|
+
function patternEscapes(pattern) {
|
|
621
|
+
if (isAbsolute2(pattern))
|
|
622
|
+
return true;
|
|
623
|
+
if (/[{}[\]~()!+@]/.test(pattern))
|
|
624
|
+
return true;
|
|
625
|
+
return pattern.split(/[\\/]/).some((segment) => segment === "..");
|
|
626
|
+
}
|
|
627
|
+
function pathArgsFor(mode) {
|
|
628
|
+
return mode === "write" ? { ...READ_PATH_ARGS, ...WRITE_PATH_ARGS } : READ_PATH_ARGS;
|
|
629
|
+
}
|
|
630
|
+
function realPathOrNearest(p) {
|
|
631
|
+
const abs = isAbsolute2(p) ? p : resolve2(p);
|
|
632
|
+
const { root } = parse(abs);
|
|
633
|
+
let real = root;
|
|
634
|
+
for (const part of abs.slice(root.length).split(sep2)) {
|
|
635
|
+
if (part === "" || part === ".")
|
|
636
|
+
continue;
|
|
637
|
+
if (part === "..") {
|
|
638
|
+
real = dirname2(real);
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
const next = real.endsWith(sep2) ? real + part : real + sep2 + part;
|
|
642
|
+
try {
|
|
643
|
+
real = realpathSync(next);
|
|
644
|
+
} catch {
|
|
645
|
+
real = next;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return real;
|
|
649
|
+
}
|
|
650
|
+
function isInsideTree(root, target) {
|
|
651
|
+
const normalizedRoot = realPathOrNearest(root);
|
|
652
|
+
const normalizedTarget = realPathOrNearest(target);
|
|
653
|
+
if (normalizedTarget === normalizedRoot)
|
|
654
|
+
return true;
|
|
655
|
+
return normalizedTarget.startsWith(normalizedRoot + sep2);
|
|
656
|
+
}
|
|
657
|
+
function decideConfinedTool(repoRoot, toolName, input, mode = "read") {
|
|
658
|
+
const pathArgs = pathArgsFor(mode)[toolName];
|
|
659
|
+
if (!pathArgs) {
|
|
660
|
+
return {
|
|
661
|
+
behavior: "deny",
|
|
662
|
+
message: `${toolName} is not available to this run.`
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
const verb = toolName in WRITE_PATH_ARGS ? "write" : "read";
|
|
666
|
+
for (const key of PATTERN_ARG_BY_TOOL[toolName] ?? []) {
|
|
667
|
+
const value = input[key];
|
|
668
|
+
if (typeof value !== "string" || value.length === 0)
|
|
669
|
+
continue;
|
|
670
|
+
if (patternEscapes(value)) {
|
|
671
|
+
return {
|
|
672
|
+
behavior: "deny",
|
|
673
|
+
message: `${toolName} patterns must stay inside the repository — no absolute path and no "..". Refused: ${value}`
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
for (const key of pathArgs) {
|
|
678
|
+
const value = input[key];
|
|
679
|
+
if (typeof value !== "string" || value.length === 0)
|
|
680
|
+
continue;
|
|
681
|
+
const candidate = isAbsolute2(value) ? value : `${repoRoot}${sep2}${value}`;
|
|
682
|
+
if (isGitMetadata(repoRoot, candidate)) {
|
|
683
|
+
return {
|
|
684
|
+
behavior: "deny",
|
|
685
|
+
message: `${toolName} may not touch the repository's git metadata. Refused: ${value}`
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
if (!isInsideTree(repoRoot, candidate)) {
|
|
689
|
+
return {
|
|
690
|
+
behavior: "deny",
|
|
691
|
+
message: `${toolName} may only ${verb} inside the repository. Refused: ${value}`
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return { behavior: "allow" };
|
|
696
|
+
}
|
|
697
|
+
function confineToRepo(repoRoot, mode = "read") {
|
|
698
|
+
return async (toolName, input) => decideConfinedTool(repoRoot, toolName, input, mode);
|
|
699
|
+
}
|
|
700
|
+
var READ_PATH_ARGS, WRITE_PATH_ARGS, CONFINED_READ_TOOLS, CONFINED_WRITE_TOOLS, PATTERN_ARG_BY_TOOL;
|
|
701
|
+
var init_confine_to_repo = __esm(() => {
|
|
702
|
+
READ_PATH_ARGS = {
|
|
703
|
+
Read: ["file_path", "path", "notebook_path"],
|
|
704
|
+
Grep: ["path"],
|
|
705
|
+
Glob: ["path"]
|
|
706
|
+
};
|
|
707
|
+
WRITE_PATH_ARGS = {
|
|
708
|
+
Write: ["file_path"],
|
|
709
|
+
Edit: ["file_path"],
|
|
710
|
+
MultiEdit: ["file_path"],
|
|
711
|
+
NotebookEdit: ["notebook_path"]
|
|
712
|
+
};
|
|
713
|
+
CONFINED_READ_TOOLS = Object.freeze(Object.keys(READ_PATH_ARGS));
|
|
714
|
+
CONFINED_WRITE_TOOLS = Object.freeze([
|
|
715
|
+
...Object.keys(READ_PATH_ARGS),
|
|
716
|
+
...Object.keys(WRITE_PATH_ARGS).filter((t) => t !== "MultiEdit")
|
|
717
|
+
]);
|
|
718
|
+
PATTERN_ARG_BY_TOOL = {
|
|
719
|
+
Glob: ["pattern"],
|
|
720
|
+
Grep: ["glob"]
|
|
721
|
+
};
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
// src/runner.ts
|
|
725
|
+
import { getConfigDir } from "@gethmy/mcp/src/config.js";
|
|
726
|
+
function mayHoldCredentials(role) {
|
|
727
|
+
return role === "author" || role === "reviewer";
|
|
728
|
+
}
|
|
729
|
+
function credentialReadDeny() {
|
|
730
|
+
return `Read(/${getConfigDir()}/**)`;
|
|
731
|
+
}
|
|
732
|
+
function credentialAccessDeny() {
|
|
733
|
+
const dir = `/${getConfigDir()}/**`;
|
|
734
|
+
return [`Read(${dir})`, `Grep(${dir})`, `Glob(${dir})`];
|
|
735
|
+
}
|
|
736
|
+
function buildRoleLaunch(args) {
|
|
737
|
+
const role = normalizeStageRole(args.role);
|
|
738
|
+
const keep = mayHoldCredentials(role);
|
|
739
|
+
const env = {};
|
|
740
|
+
for (const [key, value] of Object.entries(args.parentEnv)) {
|
|
741
|
+
if (value === undefined)
|
|
742
|
+
continue;
|
|
743
|
+
if (!keep && HARMONY_CREDENTIAL_KEYS.includes(key))
|
|
744
|
+
continue;
|
|
745
|
+
env[key] = value;
|
|
746
|
+
}
|
|
747
|
+
return {
|
|
748
|
+
role,
|
|
749
|
+
prompt: args.prompt,
|
|
750
|
+
repoPath: args.repoPath,
|
|
751
|
+
env,
|
|
752
|
+
disallowedTools: keep ? [] : [credentialReadDeny()]
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
function envKeysDroppedByLaunch(parentEnv, launch) {
|
|
756
|
+
return Object.keys(parentEnv).filter((key) => parentEnv[key] !== undefined && !Object.hasOwn(launch.env, key));
|
|
757
|
+
}
|
|
758
|
+
var HARMONY_CREDENTIAL_KEYS;
|
|
759
|
+
var init_runner = __esm(() => {
|
|
760
|
+
init_dist();
|
|
761
|
+
HARMONY_CREDENTIAL_KEYS = [
|
|
762
|
+
"HARMONY_API_KEY",
|
|
763
|
+
"HARMONY_API_URL",
|
|
764
|
+
"HARMONY_WORKSPACE_ID",
|
|
765
|
+
"SUPABASE_ANON_KEY",
|
|
766
|
+
"SUPABASE_SERVICE_ROLE_KEY",
|
|
767
|
+
"SUPABASE_URL"
|
|
768
|
+
];
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
// src/run-containment.ts
|
|
772
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
773
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
774
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
775
|
+
import { homedir, tmpdir as tmpdir2 } from "node:os";
|
|
776
|
+
import { dirname as dirname3, isAbsolute as isAbsolute3, join as join2 } from "node:path";
|
|
777
|
+
import { getConfigDir as getConfigDir2 } from "@gethmy/mcp/src/config.js";
|
|
778
|
+
function credentialDirectories() {
|
|
779
|
+
const home = homedir();
|
|
780
|
+
return [
|
|
781
|
+
getConfigDir2(),
|
|
782
|
+
join2(home, ".claude"),
|
|
783
|
+
join2(home, ".claude.json"),
|
|
784
|
+
join2(home, ".ssh"),
|
|
785
|
+
join2(home, ".gnupg"),
|
|
786
|
+
join2(home, ".aws"),
|
|
787
|
+
join2(home, ".codex"),
|
|
788
|
+
join2(home, ".gemini"),
|
|
789
|
+
join2(home, ".config", "gh"),
|
|
790
|
+
join2(home, ".config", "gcloud"),
|
|
791
|
+
join2(home, ".config", "anthropic"),
|
|
792
|
+
join2(home, ".config", "op"),
|
|
793
|
+
join2(home, ".docker"),
|
|
794
|
+
join2(home, ".kube"),
|
|
795
|
+
join2(home, ".netrc"),
|
|
796
|
+
join2(home, ".npmrc"),
|
|
797
|
+
join2(home, ".git-credentials")
|
|
798
|
+
];
|
|
799
|
+
}
|
|
800
|
+
function writeOnlyDenyPaths() {
|
|
801
|
+
const paths = [join2(homedir(), ".gitconfig")];
|
|
802
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
803
|
+
paths.push(xdg && isAbsolute3(xdg) ? join2(xdg, "git") : join2(homedir(), ".config", "git"));
|
|
804
|
+
return paths;
|
|
805
|
+
}
|
|
806
|
+
function credentialToolDeny() {
|
|
807
|
+
return credentialDirectories().flatMap((dir) => [
|
|
808
|
+
`Read(/${dir})`,
|
|
809
|
+
`Read(/${dir}/**)`
|
|
810
|
+
]);
|
|
811
|
+
}
|
|
812
|
+
function toolchainCacheDirectories(worktree) {
|
|
813
|
+
const home = homedir();
|
|
814
|
+
const scratch = `harmony-run-${createHash2("sha256").update(worktree).digest("hex").slice(0, 16)}`;
|
|
815
|
+
const candidate = process.env.TMPDIR ?? tmpdir2();
|
|
816
|
+
const tmpRoot = isAbsolute3(candidate) ? candidate : "/tmp";
|
|
817
|
+
return [
|
|
818
|
+
join2(home, ".bun", "install", "cache"),
|
|
819
|
+
join2(home, ".npm", "_cacache"),
|
|
820
|
+
join2(tmpRoot, scratch)
|
|
821
|
+
];
|
|
822
|
+
}
|
|
823
|
+
function secretEnvKeysToStrip(parentEnv = process.env) {
|
|
824
|
+
const stripped = new Set(HARMONY_CREDENTIAL_KEYS);
|
|
825
|
+
for (const key of Object.keys(parentEnv)) {
|
|
826
|
+
if (KEEP_ENV_KEYS.has(key))
|
|
827
|
+
continue;
|
|
828
|
+
if (SECRET_ENV_PATTERN.test(key))
|
|
829
|
+
stripped.add(key);
|
|
830
|
+
}
|
|
831
|
+
return [...stripped];
|
|
832
|
+
}
|
|
833
|
+
function assertNoProjectSandboxOverride(worktree) {
|
|
834
|
+
for (const name of ["settings.json", "settings.local.json"]) {
|
|
835
|
+
const path = join2(worktree, ".claude", name);
|
|
836
|
+
let raw;
|
|
837
|
+
try {
|
|
838
|
+
raw = readFileSync2(path, "utf-8");
|
|
839
|
+
} catch {
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
let parsed;
|
|
843
|
+
try {
|
|
844
|
+
parsed = JSON.parse(raw);
|
|
845
|
+
} catch {
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
if (parsed === null || typeof parsed !== "object")
|
|
849
|
+
continue;
|
|
850
|
+
const offending = Object.keys(parsed).filter((key) => !INERT_PROJECT_SETTING_KEYS.has(key));
|
|
851
|
+
if (offending.length > 0) {
|
|
852
|
+
throw new ProjectSandboxOverrideError(path, offending);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
function containedEnv(parentEnv = process.env) {
|
|
857
|
+
const strip = new Set(secretEnvKeysToStrip(parentEnv));
|
|
858
|
+
const out = {};
|
|
859
|
+
for (const [key, value] of Object.entries(parentEnv)) {
|
|
860
|
+
if (value === undefined || strip.has(key))
|
|
861
|
+
continue;
|
|
862
|
+
out[key] = value;
|
|
863
|
+
}
|
|
864
|
+
return out;
|
|
865
|
+
}
|
|
866
|
+
function gitMetadataDenyPaths(worktree) {
|
|
867
|
+
const paths = new Set;
|
|
868
|
+
const dotGit = join2(worktree, ".git");
|
|
869
|
+
const require2 = createRequire2(import.meta.url);
|
|
870
|
+
const { execFileSync } = require2("node:child_process");
|
|
871
|
+
const { statSync: statSync2 } = require2("node:fs");
|
|
872
|
+
const gitDirs = new Set;
|
|
873
|
+
try {
|
|
874
|
+
const out = execFileSync("git", [...GIT_NO_HOOKS, "rev-parse", "--git-dir", "--git-common-dir"], { cwd: worktree, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
875
|
+
for (const line of out.split(`
|
|
876
|
+
`)) {
|
|
877
|
+
const trimmed = line.trim();
|
|
878
|
+
if (!trimmed)
|
|
879
|
+
continue;
|
|
880
|
+
gitDirs.add(isAbsolute3(trimmed) ? trimmed : join2(worktree, trimmed));
|
|
881
|
+
}
|
|
882
|
+
} catch {}
|
|
883
|
+
gitDirs.add(dotGit);
|
|
884
|
+
for (const dir of gitDirs) {
|
|
885
|
+
paths.add(join2(dir, "config"));
|
|
886
|
+
paths.add(join2(dir, "config.worktree"));
|
|
887
|
+
paths.add(join2(dir, "hooks"));
|
|
888
|
+
}
|
|
889
|
+
try {
|
|
890
|
+
if (statSync2(dotGit).isFile())
|
|
891
|
+
paths.add(dotGit);
|
|
892
|
+
} catch {}
|
|
893
|
+
return [...paths];
|
|
894
|
+
}
|
|
895
|
+
function hostPersistencePaths() {
|
|
896
|
+
const home = homedir();
|
|
897
|
+
return [
|
|
898
|
+
join2(home, ".zshenv"),
|
|
899
|
+
join2(home, ".zprofile"),
|
|
900
|
+
join2(home, ".zshrc"),
|
|
901
|
+
join2(home, ".zlogin"),
|
|
902
|
+
join2(home, ".bashrc"),
|
|
903
|
+
join2(home, ".bash_profile"),
|
|
904
|
+
join2(home, ".bash_login"),
|
|
905
|
+
join2(home, ".profile"),
|
|
906
|
+
join2(home, ".config", "fish", "config.fish"),
|
|
907
|
+
join2(home, ".config", "fish", "conf.d"),
|
|
908
|
+
join2(home, "Library", "LaunchAgents"),
|
|
909
|
+
join2(home, ".config", "systemd", "user"),
|
|
910
|
+
join2(home, ".config", "autostart")
|
|
911
|
+
];
|
|
912
|
+
}
|
|
913
|
+
function credentialWriteToolDeny(worktree) {
|
|
914
|
+
return [
|
|
915
|
+
...credentialDirectories(),
|
|
916
|
+
...writeOnlyDenyPaths(),
|
|
917
|
+
...hostPersistencePaths(),
|
|
918
|
+
...gitMetadataDenyPaths(worktree)
|
|
919
|
+
].flatMap((p) => [`Edit(/${p})`, `Edit(/${p}/**)`]);
|
|
920
|
+
}
|
|
921
|
+
function implementRunToolPolicy(args) {
|
|
922
|
+
const writable = [args.worktree, ...toolchainCacheDirectories(args.worktree)];
|
|
923
|
+
const gitMeta = gitMetadataDenyPaths(args.worktree);
|
|
924
|
+
const secrets = credentialDirectories();
|
|
925
|
+
return async (toolName, input) => {
|
|
926
|
+
const allow = { behavior: "allow", updatedInput: input };
|
|
927
|
+
const deny = (message) => ({ behavior: "deny", message });
|
|
928
|
+
if (toolName.startsWith("mcp__"))
|
|
929
|
+
return allow;
|
|
930
|
+
if (toolName === "Bash" || toolName === "BashOutput") {
|
|
931
|
+
return allow;
|
|
932
|
+
}
|
|
933
|
+
const writePaths = WRITE_TOOL_PATHS[toolName];
|
|
934
|
+
if (writePaths) {
|
|
935
|
+
if (args.readOnly === true) {
|
|
936
|
+
return deny(`${toolName} is not available to a review run.`);
|
|
937
|
+
}
|
|
938
|
+
for (const key of writePaths) {
|
|
939
|
+
const value = input[key];
|
|
940
|
+
if (typeof value !== "string" || value.length === 0)
|
|
941
|
+
continue;
|
|
942
|
+
const target = isAbsolute3(value) ? value : join2(args.worktree, value);
|
|
943
|
+
if (gitMeta.some((p) => isInsideTree(p, target) || p === target)) {
|
|
944
|
+
return deny(`${toolName} may not touch git metadata. Refused: ${value}`);
|
|
945
|
+
}
|
|
946
|
+
if (!writable.some((root) => isInsideTree(root, target))) {
|
|
947
|
+
return deny(`${toolName} may only write inside this run's worktree. Refused: ${value}`);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return allow;
|
|
951
|
+
}
|
|
952
|
+
const readPaths = READ_TOOL_PATHS[toolName];
|
|
953
|
+
if (readPaths) {
|
|
954
|
+
for (const key of readPaths) {
|
|
955
|
+
const value = input[key];
|
|
956
|
+
if (typeof value !== "string" || value.length === 0)
|
|
957
|
+
continue;
|
|
958
|
+
const target = isAbsolute3(value) ? value : join2(args.worktree, value);
|
|
959
|
+
if (secrets.some((dir) => isInsideTree(dir, target))) {
|
|
960
|
+
return deny(`${toolName} may not read the credential directories. Refused: ${value}`);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return allow;
|
|
964
|
+
}
|
|
965
|
+
return allow;
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
function harmonyMcpServer() {
|
|
969
|
+
const require2 = createRequire2(import.meta.url);
|
|
970
|
+
const cli = join2(dirname3(require2.resolve("@gethmy/mcp")), "cli.js");
|
|
971
|
+
return {
|
|
972
|
+
harmony: {
|
|
973
|
+
type: "stdio",
|
|
974
|
+
command: process.execPath,
|
|
975
|
+
args: [cli, "serve"]
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
function implementRunContainment(args) {
|
|
980
|
+
assertNoProjectSandboxOverride(args.worktree);
|
|
981
|
+
return {
|
|
982
|
+
sandbox: {
|
|
983
|
+
enabled: true,
|
|
984
|
+
failIfUnavailable: true,
|
|
985
|
+
allowUnsandboxedCommands: false,
|
|
986
|
+
autoAllowBashIfSandboxed: args.readOnly !== true,
|
|
987
|
+
network: {
|
|
988
|
+
allowedDomains: [...IMPLEMENT_ALLOWED_DOMAINS],
|
|
989
|
+
allowLocalBinding: false
|
|
990
|
+
},
|
|
991
|
+
filesystem: {
|
|
992
|
+
allowWrite: [
|
|
993
|
+
args.worktree,
|
|
994
|
+
...toolchainCacheDirectories(args.worktree)
|
|
995
|
+
],
|
|
996
|
+
denyRead: credentialDirectories(),
|
|
997
|
+
denyWrite: [
|
|
998
|
+
...credentialDirectories(),
|
|
999
|
+
...writeOnlyDenyPaths(),
|
|
1000
|
+
...hostPersistencePaths(),
|
|
1001
|
+
...gitMetadataDenyPaths(args.worktree)
|
|
1002
|
+
]
|
|
1003
|
+
}
|
|
1004
|
+
},
|
|
1005
|
+
canUseTool: implementRunToolPolicy(args),
|
|
1006
|
+
gateEveryToolCall: true,
|
|
1007
|
+
settingSources: args.readOnly === true ? [] : ["project"],
|
|
1008
|
+
mcpServers: harmonyMcpServer(),
|
|
1009
|
+
strictMcpConfig: true,
|
|
1010
|
+
stripEnvKeys: secretEnvKeysToStrip(),
|
|
1011
|
+
disallowedTools: [
|
|
1012
|
+
...args.extraDisallowedTools ?? [],
|
|
1013
|
+
...credentialToolDeny(),
|
|
1014
|
+
...credentialWriteToolDeny(args.worktree)
|
|
1015
|
+
]
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
function implementRunContainmentCliArgs(args) {
|
|
1019
|
+
const containment = implementRunContainment(args);
|
|
1020
|
+
return [
|
|
1021
|
+
"--settings",
|
|
1022
|
+
JSON.stringify({ sandbox: containment.sandbox }),
|
|
1023
|
+
"--setting-sources",
|
|
1024
|
+
containment.settingSources.join(","),
|
|
1025
|
+
"--mcp-config",
|
|
1026
|
+
JSON.stringify({ mcpServers: containment.mcpServers }),
|
|
1027
|
+
"--strict-mcp-config",
|
|
1028
|
+
"--disallowedTools",
|
|
1029
|
+
containment.disallowedTools.join(",")
|
|
1030
|
+
];
|
|
1031
|
+
}
|
|
1032
|
+
var SECRET_ENV_PATTERN, KEEP_ENV_KEYS, ProjectSandboxOverrideError, INERT_PROJECT_SETTING_KEYS, WRITE_TOOL_PATHS, READ_TOOL_PATHS, GIT_NO_HOOKS, IMPLEMENT_ALLOWED_DOMAINS;
|
|
1033
|
+
var init_run_containment = __esm(() => {
|
|
1034
|
+
init_confine_to_repo();
|
|
1035
|
+
init_runner();
|
|
1036
|
+
SECRET_ENV_PATTERN = /(TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|API_?KEY|_PAT$|^PAT_|PRIVATE_KEY|ACCESS_KEY)/i;
|
|
1037
|
+
KEEP_ENV_KEYS = new Set([
|
|
1038
|
+
"ANTHROPIC_API_KEY",
|
|
1039
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
1040
|
+
"ANTHROPIC_BASE_URL",
|
|
1041
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
1042
|
+
"SSH_AUTH_SOCK",
|
|
1043
|
+
"GIT_AUTHOR_NAME",
|
|
1044
|
+
"GIT_AUTHOR_EMAIL",
|
|
1045
|
+
"GIT_COMMITTER_NAME",
|
|
1046
|
+
"GIT_COMMITTER_EMAIL",
|
|
1047
|
+
"XAUTHORITY"
|
|
1048
|
+
]);
|
|
1049
|
+
ProjectSandboxOverrideError = class ProjectSandboxOverrideError extends Error {
|
|
1050
|
+
settingsPath;
|
|
1051
|
+
offendingKeys;
|
|
1052
|
+
constructor(settingsPath, offendingKeys) {
|
|
1053
|
+
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.");
|
|
1054
|
+
this.settingsPath = settingsPath;
|
|
1055
|
+
this.offendingKeys = offendingKeys;
|
|
1056
|
+
this.name = "ProjectSandboxOverrideError";
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
INERT_PROJECT_SETTING_KEYS = new Set([
|
|
1060
|
+
"$schema",
|
|
1061
|
+
"cleanupPeriodDays",
|
|
1062
|
+
"includeCoAuthoredBy",
|
|
1063
|
+
"language",
|
|
1064
|
+
"outputStyle",
|
|
1065
|
+
"spinnerTipsEnabled",
|
|
1066
|
+
"theme",
|
|
1067
|
+
"verbose"
|
|
1068
|
+
]);
|
|
1069
|
+
WRITE_TOOL_PATHS = {
|
|
1070
|
+
Write: ["file_path"],
|
|
1071
|
+
Edit: ["file_path"],
|
|
1072
|
+
MultiEdit: ["file_path"],
|
|
1073
|
+
NotebookEdit: ["notebook_path"]
|
|
1074
|
+
};
|
|
1075
|
+
READ_TOOL_PATHS = {
|
|
1076
|
+
Read: ["file_path", "path", "notebook_path"],
|
|
1077
|
+
Grep: ["path"],
|
|
1078
|
+
Glob: ["path"]
|
|
1079
|
+
};
|
|
1080
|
+
GIT_NO_HOOKS = [
|
|
1081
|
+
"-c",
|
|
1082
|
+
"core.hooksPath=",
|
|
1083
|
+
"-c",
|
|
1084
|
+
"core.fsmonitor=",
|
|
1085
|
+
"-c",
|
|
1086
|
+
"core.pager=cat"
|
|
1087
|
+
];
|
|
1088
|
+
IMPLEMENT_ALLOWED_DOMAINS = [
|
|
1089
|
+
"api.anthropic.com",
|
|
1090
|
+
"*.anthropic.com",
|
|
1091
|
+
"registry.npmjs.org",
|
|
1092
|
+
"*.npmjs.org",
|
|
1093
|
+
"github.com",
|
|
1094
|
+
"*.github.com",
|
|
1095
|
+
"*.githubusercontent.com"
|
|
1096
|
+
];
|
|
1097
|
+
});
|
|
1098
|
+
|
|
614
1099
|
// src/cli.ts
|
|
615
1100
|
init_dist();
|
|
616
1101
|
|
|
@@ -896,6 +1381,7 @@ class SdkAgentRunner {
|
|
|
896
1381
|
...this.cfg.settingSources ? { settingSources: this.cfg.settingSources } : {},
|
|
897
1382
|
...this.cfg.mcpServers ? { mcpServers: this.cfg.mcpServers } : {},
|
|
898
1383
|
...this.cfg.strictMcpConfig ? { strictMcpConfig: true } : {},
|
|
1384
|
+
...this.cfg.sandbox ? { sandbox: this.cfg.sandbox } : {},
|
|
899
1385
|
stderr: (data) => {
|
|
900
1386
|
this.capturedStderr += data;
|
|
901
1387
|
},
|
|
@@ -1316,6 +1802,7 @@ init_dist();
|
|
|
1316
1802
|
|
|
1317
1803
|
// src/exec-types.ts
|
|
1318
1804
|
var DEFAULT_METRIC_TIMEOUT_MS = 300000;
|
|
1805
|
+
var SANDBOX_STARTUP_GRACE_MS = 60000;
|
|
1319
1806
|
|
|
1320
1807
|
// src/gate-config-error.ts
|
|
1321
1808
|
init_dist();
|
|
@@ -2030,11 +2517,12 @@ function errText(err) {
|
|
|
2030
2517
|
}
|
|
2031
2518
|
|
|
2032
2519
|
// src/verification.ts
|
|
2033
|
-
init_log();
|
|
2034
2520
|
import { execFileSync as execFileSync4, spawn as spawn2 } from "node:child_process";
|
|
2521
|
+
init_log();
|
|
2035
2522
|
|
|
2036
2523
|
// src/pm.ts
|
|
2037
2524
|
init_log();
|
|
2525
|
+
init_run_containment();
|
|
2038
2526
|
import { execFileSync } from "node:child_process";
|
|
2039
2527
|
import { existsSync } from "node:fs";
|
|
2040
2528
|
var TAG6 = "pm";
|
|
@@ -2044,7 +2532,7 @@ function detectPackageManager() {
|
|
|
2044
2532
|
return cached;
|
|
2045
2533
|
let repoRoot;
|
|
2046
2534
|
try {
|
|
2047
|
-
repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
2535
|
+
repoRoot = execFileSync("git", [...GIT_NO_HOOKS, "rev-parse", "--show-toplevel"], {
|
|
2048
2536
|
encoding: "utf-8"
|
|
2049
2537
|
}).trim();
|
|
2050
2538
|
} catch {
|
|
@@ -2087,7 +2575,7 @@ function spawnRunArgs(script, ...extra) {
|
|
|
2087
2575
|
// src/project-type.ts
|
|
2088
2576
|
init_log();
|
|
2089
2577
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
2090
|
-
import { existsSync as existsSync2, readdirSync, readFileSync as
|
|
2578
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "node:fs";
|
|
2091
2579
|
var TAG7 = "project-type";
|
|
2092
2580
|
var _cache = new Map;
|
|
2093
2581
|
function _resetCache() {
|
|
@@ -2190,7 +2678,7 @@ var NPM_PLACEHOLDER_TEST = /no test specified/i;
|
|
|
2190
2678
|
function hasNodeTestScript(dir) {
|
|
2191
2679
|
let script;
|
|
2192
2680
|
try {
|
|
2193
|
-
const pkg = JSON.parse(
|
|
2681
|
+
const pkg = JSON.parse(readFileSync3(`${dir}/package.json`, "utf-8"));
|
|
2194
2682
|
script = pkg.scripts?.test;
|
|
2195
2683
|
} catch (err) {
|
|
2196
2684
|
log.warn(TAG7, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
|
|
@@ -2207,7 +2695,7 @@ function hasNodeTestScript(dir) {
|
|
|
2207
2695
|
function firstNodeScript(dir, candidates) {
|
|
2208
2696
|
let scripts;
|
|
2209
2697
|
try {
|
|
2210
|
-
const pkg = JSON.parse(
|
|
2698
|
+
const pkg = JSON.parse(readFileSync3(`${dir}/package.json`, "utf-8"));
|
|
2211
2699
|
scripts = pkg.scripts ?? {};
|
|
2212
2700
|
} catch (err) {
|
|
2213
2701
|
log.warn(TAG7, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
|
|
@@ -2262,10 +2750,108 @@ function resolveXcodeScheme(pt) {
|
|
|
2262
2750
|
}
|
|
2263
2751
|
}
|
|
2264
2752
|
|
|
2753
|
+
// src/repair-sandbox.ts
|
|
2754
|
+
init_log();
|
|
2755
|
+
import { randomUUID } from "node:crypto";
|
|
2756
|
+
import { promisify } from "node:util";
|
|
2757
|
+
var TAG8 = "repair-sandbox";
|
|
2758
|
+
async function dockerExec(argv, opts) {
|
|
2759
|
+
const { execFile } = await import("node:child_process");
|
|
2760
|
+
return promisify(execFile)("docker", argv, {
|
|
2761
|
+
encoding: "utf-8",
|
|
2762
|
+
...opts
|
|
2763
|
+
});
|
|
2764
|
+
}
|
|
2765
|
+
var MAX_OUTPUT_BUFFER2 = 20971520;
|
|
2766
|
+
var PROBE_TIMEOUT_MS = 1e4;
|
|
2767
|
+
var SANDBOX_MOUNT = "/repo";
|
|
2768
|
+
var SANDBOX_MEMORY = "4g";
|
|
2769
|
+
var SANDBOX_PIDS = "512";
|
|
2770
|
+
var dockerProbe = null;
|
|
2771
|
+
async function sandboxAvailable() {
|
|
2772
|
+
if (dockerProbe === true)
|
|
2773
|
+
return true;
|
|
2774
|
+
try {
|
|
2775
|
+
await dockerExec(["version", "--format", "{{.Server.Version}}"], {
|
|
2776
|
+
timeout: PROBE_TIMEOUT_MS
|
|
2777
|
+
});
|
|
2778
|
+
dockerProbe = true;
|
|
2779
|
+
} catch {
|
|
2780
|
+
dockerProbe = false;
|
|
2781
|
+
}
|
|
2782
|
+
return dockerProbe;
|
|
2783
|
+
}
|
|
2784
|
+
function __resetSandboxProbe() {
|
|
2785
|
+
dockerProbe = null;
|
|
2786
|
+
}
|
|
2787
|
+
async function removeContainer(name) {
|
|
2788
|
+
try {
|
|
2789
|
+
await dockerExec(["rm", "--force", name], { timeout: PROBE_TIMEOUT_MS });
|
|
2790
|
+
log.warn(TAG8, `removed the timed-out sandbox container ${name}`);
|
|
2791
|
+
} catch {}
|
|
2792
|
+
}
|
|
2793
|
+
function sandboxRunArgs(image, worktree, command, name) {
|
|
2794
|
+
return [
|
|
2795
|
+
"run",
|
|
2796
|
+
"--rm",
|
|
2797
|
+
...name ? ["--name", name] : [],
|
|
2798
|
+
"--network=none",
|
|
2799
|
+
"--cap-drop=ALL",
|
|
2800
|
+
"--security-opt=no-new-privileges",
|
|
2801
|
+
`--memory=${SANDBOX_MEMORY}`,
|
|
2802
|
+
`--pids-limit=${SANDBOX_PIDS}`,
|
|
2803
|
+
...typeof process.getuid === "function" && typeof process.getgid === "function" ? ["--user", `${process.getuid()}:${process.getgid()}`] : [],
|
|
2804
|
+
"--env",
|
|
2805
|
+
"HOME=/tmp",
|
|
2806
|
+
"--volume",
|
|
2807
|
+
`${worktree}:${SANDBOX_MOUNT}`,
|
|
2808
|
+
"--workdir",
|
|
2809
|
+
SANDBOX_MOUNT,
|
|
2810
|
+
"--entrypoint",
|
|
2811
|
+
command.cmd,
|
|
2812
|
+
image,
|
|
2813
|
+
...command.args
|
|
2814
|
+
];
|
|
2815
|
+
}
|
|
2816
|
+
async function runInSandbox(args) {
|
|
2817
|
+
const name = `harmony-repair-${randomUUID()}`;
|
|
2818
|
+
const argv = sandboxRunArgs(args.image, args.worktree, args.command, name);
|
|
2819
|
+
log.info(TAG8, `sandbox: ${args.command.cmd} ${args.command.args.join(" ")} (image ${args.image})`);
|
|
2820
|
+
try {
|
|
2821
|
+
const { stdout } = await dockerExec(argv, {
|
|
2822
|
+
timeout: args.timeoutMs,
|
|
2823
|
+
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
2824
|
+
});
|
|
2825
|
+
return { passed: true, output: stdout ?? "" };
|
|
2826
|
+
} catch (err) {
|
|
2827
|
+
const e = err;
|
|
2828
|
+
const output = `${e.stdout ?? ""}${e.stderr ?? ""}`;
|
|
2829
|
+
if (typeof e.code !== "number") {
|
|
2830
|
+
const timedOut = e.killed === true || e.signal != null;
|
|
2831
|
+
if (timedOut)
|
|
2832
|
+
await removeContainer(name);
|
|
2833
|
+
return {
|
|
2834
|
+
passed: false,
|
|
2835
|
+
output,
|
|
2836
|
+
sandboxError: timedOut ? `the sandbox timed out after ${args.timeoutMs}ms` : `the sandbox did not run: ${e.message ?? "unknown error"}`
|
|
2837
|
+
};
|
|
2838
|
+
}
|
|
2839
|
+
if (e.code === 125) {
|
|
2840
|
+
return {
|
|
2841
|
+
passed: false,
|
|
2842
|
+
output,
|
|
2843
|
+
sandboxError: `the sandbox could not start (image "${args.image}" missing or unusable)`
|
|
2844
|
+
};
|
|
2845
|
+
}
|
|
2846
|
+
return { passed: false, output };
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2265
2850
|
// src/revert-guard.ts
|
|
2266
2851
|
init_log();
|
|
2852
|
+
init_run_containment();
|
|
2267
2853
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
2268
|
-
var
|
|
2854
|
+
var TAG9 = "revert-guard";
|
|
2269
2855
|
var TEST_FILE = /(?:^|\/)__tests__\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
2270
2856
|
function isTestFile(path) {
|
|
2271
2857
|
return TEST_FILE.test(path);
|
|
@@ -2275,21 +2861,27 @@ function filterTestFiles(paths) {
|
|
|
2275
2861
|
}
|
|
2276
2862
|
function refetchBase(worktreePath, baseBranch) {
|
|
2277
2863
|
try {
|
|
2278
|
-
execFileSync3("git", ["fetch", "origin", baseBranch], {
|
|
2864
|
+
execFileSync3("git", [...GIT_NO_HOOKS, "fetch", "origin", baseBranch], {
|
|
2279
2865
|
cwd: worktreePath,
|
|
2280
2866
|
stdio: "pipe"
|
|
2281
2867
|
});
|
|
2282
2868
|
} catch {
|
|
2283
|
-
log.warn(
|
|
2869
|
+
log.warn(TAG9, "Failed to re-fetch base for revert guard — using last fetch");
|
|
2284
2870
|
}
|
|
2285
2871
|
}
|
|
2286
2872
|
function listDeletedFilesAgainstBase(worktreePath, baseBranch) {
|
|
2287
2873
|
try {
|
|
2288
|
-
const out = execFileSync3("git", [
|
|
2874
|
+
const out = execFileSync3("git", [
|
|
2875
|
+
...GIT_NO_HOOKS,
|
|
2876
|
+
"diff",
|
|
2877
|
+
"--diff-filter=D",
|
|
2878
|
+
"--name-only",
|
|
2879
|
+
`origin/${baseBranch}...HEAD`
|
|
2880
|
+
], { cwd: worktreePath, encoding: "utf-8" });
|
|
2289
2881
|
return out.split(`
|
|
2290
2882
|
`).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
2291
2883
|
} catch (err) {
|
|
2292
|
-
log.warn(
|
|
2884
|
+
log.warn(TAG9, `Failed to list deleted files: ${err instanceof Error ? err.message : err}`);
|
|
2293
2885
|
return [];
|
|
2294
2886
|
}
|
|
2295
2887
|
}
|
|
@@ -2299,8 +2891,9 @@ function findDeletedTestFiles(worktreePath, baseBranch) {
|
|
|
2299
2891
|
}
|
|
2300
2892
|
|
|
2301
2893
|
// src/verification.ts
|
|
2302
|
-
|
|
2303
|
-
var
|
|
2894
|
+
init_run_containment();
|
|
2895
|
+
var TAG10 = "verification";
|
|
2896
|
+
var MAX_OUTPUT_BUFFER3 = 64 * 1024 * 1024;
|
|
2304
2897
|
async function runVerification(worktreePath, config, workerId) {
|
|
2305
2898
|
const result = {
|
|
2306
2899
|
passed: true,
|
|
@@ -2310,133 +2903,163 @@ async function runVerification(worktreePath, config, workerId) {
|
|
|
2310
2903
|
reviewFindings: [],
|
|
2311
2904
|
revertWarnings: []
|
|
2312
2905
|
};
|
|
2906
|
+
const sandbox = verificationSandbox(config);
|
|
2313
2907
|
if (config.verification.revertGuard) {
|
|
2314
|
-
log.info(
|
|
2908
|
+
log.info(TAG10, `[worker:${workerId}] Checking for reverted merged work...`);
|
|
2315
2909
|
const deletedTests = findDeletedTestFiles(worktreePath, config.worktree.baseBranch);
|
|
2316
2910
|
if (deletedTests.length > 0) {
|
|
2317
2911
|
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.");
|
|
2318
|
-
log.warn(
|
|
2912
|
+
log.warn(TAG10, `[worker:${workerId}] Revert guard tripped: ${deletedTests.length} deleted test file(s)`);
|
|
2319
2913
|
result.passed = false;
|
|
2320
2914
|
} else {
|
|
2321
|
-
log.info(
|
|
2915
|
+
log.info(TAG10, `[worker:${workerId}] Revert guard passed`);
|
|
2322
2916
|
}
|
|
2323
2917
|
}
|
|
2324
2918
|
if (config.verification.build) {
|
|
2325
|
-
log.info(
|
|
2326
|
-
result.buildErrors = runBuild(worktreePath, config.verification.timeout);
|
|
2919
|
+
log.info(TAG10, `[worker:${workerId}] Running build...`);
|
|
2920
|
+
result.buildErrors = await runBuild(worktreePath, config.verification.timeout, sandbox);
|
|
2327
2921
|
if (result.buildErrors.length > 0) {
|
|
2328
|
-
log.warn(
|
|
2922
|
+
log.warn(TAG10, `[worker:${workerId}] Build failed with ${result.buildErrors.length} error(s)`);
|
|
2329
2923
|
result.passed = false;
|
|
2330
2924
|
} else {
|
|
2331
|
-
log.info(
|
|
2925
|
+
log.info(TAG10, `[worker:${workerId}] Build passed`);
|
|
2332
2926
|
}
|
|
2333
2927
|
}
|
|
2334
2928
|
if (config.verification.test && result.buildErrors.length === 0) {
|
|
2335
|
-
log.info(
|
|
2336
|
-
result.testFailures = runTests(worktreePath, config.verification.testTimeout);
|
|
2929
|
+
log.info(TAG10, `[worker:${workerId}] Running tests...`);
|
|
2930
|
+
result.testFailures = await runTests(worktreePath, config.verification.testTimeout, sandbox);
|
|
2337
2931
|
if (result.testFailures.length > 0) {
|
|
2338
|
-
log.warn(
|
|
2932
|
+
log.warn(TAG10, `[worker:${workerId}] Tests failed with ${result.testFailures.length} failure(s)`);
|
|
2339
2933
|
result.passed = false;
|
|
2340
2934
|
} else {
|
|
2341
|
-
log.info(
|
|
2935
|
+
log.info(TAG10, `[worker:${workerId}] Tests passed`);
|
|
2342
2936
|
}
|
|
2343
2937
|
}
|
|
2344
2938
|
if (config.verification.lint) {
|
|
2345
|
-
log.info(
|
|
2346
|
-
result.lintWarnings = runLint(worktreePath, config.verification.timeout);
|
|
2939
|
+
log.info(TAG10, `[worker:${workerId}] Running lint...`);
|
|
2940
|
+
result.lintWarnings = await runLint(worktreePath, config.verification.timeout, sandbox);
|
|
2347
2941
|
if (result.lintWarnings.length > 0) {
|
|
2348
|
-
log.warn(
|
|
2942
|
+
log.warn(TAG10, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
|
|
2349
2943
|
} else {
|
|
2350
|
-
log.info(
|
|
2944
|
+
log.info(TAG10, `[worker:${workerId}] Lint passed`);
|
|
2351
2945
|
}
|
|
2352
2946
|
}
|
|
2353
2947
|
if (config.verification.deepReview) {
|
|
2354
|
-
log.info(
|
|
2948
|
+
log.info(TAG10, `[worker:${workerId}] Running deep review...`);
|
|
2355
2949
|
result.reviewFindings = await runDeepReview(worktreePath, config, workerId);
|
|
2356
2950
|
if (result.reviewFindings.length > 0) {
|
|
2357
|
-
log.warn(
|
|
2951
|
+
log.warn(TAG10, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
|
|
2358
2952
|
} else {
|
|
2359
|
-
log.info(
|
|
2953
|
+
log.info(TAG10, `[worker:${workerId}] Deep review passed`);
|
|
2360
2954
|
}
|
|
2361
2955
|
}
|
|
2362
2956
|
return result;
|
|
2363
2957
|
}
|
|
2364
|
-
function
|
|
2365
|
-
const
|
|
2366
|
-
if (!
|
|
2367
|
-
|
|
2368
|
-
|
|
2958
|
+
function verificationSandbox(config) {
|
|
2959
|
+
const image = config.verification.sandboxImage?.trim();
|
|
2960
|
+
if (!image)
|
|
2961
|
+
return;
|
|
2962
|
+
return { image };
|
|
2963
|
+
}
|
|
2964
|
+
async function execStep(command, args) {
|
|
2965
|
+
const { worktreePath, timeout } = args;
|
|
2966
|
+
const sandbox = args.sandbox?.image.trim() ? args.sandbox : undefined;
|
|
2967
|
+
if (sandbox) {
|
|
2968
|
+
if (!await sandboxAvailable()) {
|
|
2969
|
+
return {
|
|
2970
|
+
ok: false,
|
|
2971
|
+
sandboxError: `verification.sandboxImage is set to "${sandbox.image}" but no container runtime answered — ` + "start Docker or unset the image to verify on the host"
|
|
2972
|
+
};
|
|
2973
|
+
}
|
|
2974
|
+
const result = await runInSandbox({
|
|
2975
|
+
image: sandbox.image,
|
|
2976
|
+
worktree: worktreePath,
|
|
2977
|
+
command,
|
|
2978
|
+
timeoutMs: timeout + SANDBOX_STARTUP_GRACE_MS
|
|
2979
|
+
});
|
|
2980
|
+
if (result.sandboxError) {
|
|
2981
|
+
return { ok: false, sandboxError: result.sandboxError };
|
|
2982
|
+
}
|
|
2983
|
+
if (result.passed)
|
|
2984
|
+
return { ok: true };
|
|
2985
|
+
return { ok: false, err: { stdout: result.output, stderr: "" } };
|
|
2369
2986
|
}
|
|
2370
2987
|
try {
|
|
2371
2988
|
execFileSync4(command.cmd, command.args, {
|
|
2372
2989
|
cwd: worktreePath,
|
|
2373
2990
|
timeout,
|
|
2374
2991
|
stdio: "pipe",
|
|
2375
|
-
maxBuffer:
|
|
2992
|
+
maxBuffer: MAX_OUTPUT_BUFFER3,
|
|
2993
|
+
env: containedEnv()
|
|
2376
2994
|
});
|
|
2377
|
-
return
|
|
2995
|
+
return { ok: true };
|
|
2378
2996
|
} catch (err) {
|
|
2379
|
-
return
|
|
2997
|
+
return { ok: false, err };
|
|
2380
2998
|
}
|
|
2381
2999
|
}
|
|
2382
|
-
function
|
|
3000
|
+
async function runBuild(worktreePath, timeout, sandbox) {
|
|
3001
|
+
const command = buildCommand(worktreePath);
|
|
3002
|
+
if (!command) {
|
|
3003
|
+
log.warn(TAG10, `No known build toolchain for ${worktreePath} — skipping build`);
|
|
3004
|
+
return [];
|
|
3005
|
+
}
|
|
3006
|
+
const outcome = await execStep(command, { worktreePath, timeout, sandbox });
|
|
3007
|
+
if (outcome.ok)
|
|
3008
|
+
return [];
|
|
3009
|
+
if (outcome.sandboxError) {
|
|
3010
|
+
log.error(TAG10, `Build not verified: ${outcome.sandboxError}`);
|
|
3011
|
+
return [`Build did not run: ${outcome.sandboxError}`];
|
|
3012
|
+
}
|
|
3013
|
+
return parseErrorOutput(outcome.err);
|
|
3014
|
+
}
|
|
3015
|
+
async function runTests(worktreePath, timeout, sandbox) {
|
|
2383
3016
|
const command = testCommand(worktreePath);
|
|
2384
3017
|
if (!command) {
|
|
2385
|
-
log.warn(
|
|
3018
|
+
log.warn(TAG10, `No test command for detected toolchain in ${worktreePath} — skipping tests`);
|
|
2386
3019
|
return [];
|
|
2387
3020
|
}
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
cwd: worktreePath,
|
|
2391
|
-
timeout,
|
|
2392
|
-
stdio: "pipe",
|
|
2393
|
-
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
2394
|
-
});
|
|
3021
|
+
const outcome = await execStep(command, { worktreePath, timeout, sandbox });
|
|
3022
|
+
if (outcome.ok)
|
|
2395
3023
|
return [];
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
${output.slice(-4000) || "(no output captured)"}`);
|
|
2400
|
-
return parseTestFailures(err, timeout);
|
|
3024
|
+
if (outcome.sandboxError) {
|
|
3025
|
+
log.error(TAG10, `Tests not verified: ${outcome.sandboxError}`);
|
|
3026
|
+
return [`Test run did not happen: ${outcome.sandboxError}`];
|
|
2401
3027
|
}
|
|
3028
|
+
const output = combineOutput(outcome.err);
|
|
3029
|
+
log.warn(TAG10, `Test run failed:
|
|
3030
|
+
${output.slice(-4000) || "(no output captured)"}`);
|
|
3031
|
+
return parseTestFailures(outcome.err, timeout);
|
|
2402
3032
|
}
|
|
2403
|
-
function runFormatFix(worktreePath, timeout, workerId) {
|
|
3033
|
+
async function runFormatFix(worktreePath, timeout, workerId, sandbox) {
|
|
2404
3034
|
const command = formatFixCommand(worktreePath);
|
|
2405
3035
|
if (!command)
|
|
2406
3036
|
return;
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
stdio: "pipe",
|
|
2412
|
-
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
2413
|
-
});
|
|
2414
|
-
log.info(TAG9, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
|
|
2415
|
-
} catch (err) {
|
|
2416
|
-
log.warn(TAG9, `[worker:${workerId}] Auto-format step exited non-zero (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
3037
|
+
const outcome = await execStep(command, { worktreePath, timeout, sandbox });
|
|
3038
|
+
if (outcome.ok) {
|
|
3039
|
+
log.info(TAG10, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
|
|
3040
|
+
return;
|
|
2417
3041
|
}
|
|
3042
|
+
const why = outcome.sandboxError ? outcome.sandboxError : outcome.err instanceof Error ? outcome.err.message : String(outcome.err);
|
|
3043
|
+
log.warn(TAG10, `[worker:${workerId}] Auto-format step did not complete (non-fatal): ${why}`);
|
|
2418
3044
|
}
|
|
2419
|
-
function runLint(worktreePath, timeout) {
|
|
3045
|
+
async function runLint(worktreePath, timeout, sandbox) {
|
|
2420
3046
|
const command = lintCommand(worktreePath);
|
|
2421
3047
|
if (!command) {
|
|
2422
|
-
log.info(
|
|
3048
|
+
log.info(TAG10, `No lint step for detected toolchain in ${worktreePath} — skipping lint`);
|
|
2423
3049
|
return [];
|
|
2424
3050
|
}
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
cwd: worktreePath,
|
|
2428
|
-
timeout,
|
|
2429
|
-
stdio: "pipe",
|
|
2430
|
-
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
2431
|
-
});
|
|
3051
|
+
const outcome = await execStep(command, { worktreePath, timeout, sandbox });
|
|
3052
|
+
if (outcome.ok)
|
|
2432
3053
|
return [];
|
|
2433
|
-
|
|
2434
|
-
|
|
3054
|
+
if (outcome.sandboxError) {
|
|
3055
|
+
log.error(TAG10, `Lint not verified: ${outcome.sandboxError}`);
|
|
3056
|
+
return [`Lint did not run: ${outcome.sandboxError}`];
|
|
2435
3057
|
}
|
|
3058
|
+
return parseErrorOutput(outcome.err);
|
|
2436
3059
|
}
|
|
2437
3060
|
async function runDeepReview(worktreePath, config, workerId) {
|
|
2438
3061
|
if (!supportsDevServer(worktreePath)) {
|
|
2439
|
-
log.info(
|
|
3062
|
+
log.info(TAG10, `[worker:${workerId}] Detected non-web toolchain — skipping deep review`);
|
|
2440
3063
|
return [];
|
|
2441
3064
|
}
|
|
2442
3065
|
const port = config.verification.devServerBasePort + workerId;
|
|
@@ -2445,22 +3068,23 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
2445
3068
|
const [cmd, args] = spawnRunArgs("dev", "--port", String(port));
|
|
2446
3069
|
devServer = spawn2(cmd, args, {
|
|
2447
3070
|
cwd: worktreePath,
|
|
2448
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3071
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3072
|
+
env: containedEnv()
|
|
2449
3073
|
});
|
|
2450
3074
|
try {
|
|
2451
3075
|
await waitForDevServer(devServer, 30000);
|
|
2452
3076
|
await probeDevServer(port);
|
|
2453
3077
|
} catch (err) {
|
|
2454
|
-
log.error(
|
|
3078
|
+
log.error(TAG10, `Dev server did not become ready: ${err instanceof Error ? err.message : err}`);
|
|
2455
3079
|
return [];
|
|
2456
3080
|
}
|
|
2457
3081
|
let diff = "";
|
|
2458
3082
|
try {
|
|
2459
|
-
diff = execFileSync4("git", ["diff", `origin/${config.worktree.baseBranch}..HEAD`], {
|
|
3083
|
+
diff = execFileSync4("git", [...GIT_NO_HOOKS, "diff", `origin/${config.worktree.baseBranch}..HEAD`], {
|
|
2460
3084
|
cwd: worktreePath,
|
|
2461
3085
|
encoding: "utf-8",
|
|
2462
3086
|
timeout: 30000,
|
|
2463
|
-
maxBuffer:
|
|
3087
|
+
maxBuffer: MAX_OUTPUT_BUFFER3
|
|
2464
3088
|
});
|
|
2465
3089
|
} catch {
|
|
2466
3090
|
diff = "(unable to retrieve diff)";
|
|
@@ -2477,14 +3101,16 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
2477
3101
|
"```"
|
|
2478
3102
|
].join(`
|
|
2479
3103
|
`);
|
|
2480
|
-
const leanSources = config.claude.leanSettingSources;
|
|
2481
3104
|
const output = execFileSync4("claude", [
|
|
2482
3105
|
"--print",
|
|
2483
3106
|
"--model",
|
|
2484
3107
|
"sonnet",
|
|
2485
3108
|
"--max-turns",
|
|
2486
3109
|
"10",
|
|
2487
|
-
...
|
|
3110
|
+
...implementRunContainmentCliArgs({
|
|
3111
|
+
worktree: worktreePath,
|
|
3112
|
+
readOnly: true
|
|
3113
|
+
}),
|
|
2488
3114
|
"--",
|
|
2489
3115
|
reviewPrompt
|
|
2490
3116
|
], {
|
|
@@ -2492,11 +3118,12 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
2492
3118
|
encoding: "utf-8",
|
|
2493
3119
|
timeout: config.verification.timeout,
|
|
2494
3120
|
stdio: "pipe",
|
|
2495
|
-
maxBuffer:
|
|
3121
|
+
maxBuffer: MAX_OUTPUT_BUFFER3,
|
|
3122
|
+
env: containedEnv()
|
|
2496
3123
|
});
|
|
2497
3124
|
return parseReviewFindings(output);
|
|
2498
3125
|
} catch (err) {
|
|
2499
|
-
log.error(
|
|
3126
|
+
log.error(TAG10, `Deep review failed: ${err instanceof Error ? err.message : err}`);
|
|
2500
3127
|
return [];
|
|
2501
3128
|
} finally {
|
|
2502
3129
|
if (devServer && !devServer.killed) {
|
|
@@ -2522,7 +3149,6 @@ function attemptAutoFix(worktreePath, config, errors) {
|
|
|
2522
3149
|
"```"
|
|
2523
3150
|
].join(`
|
|
2524
3151
|
`);
|
|
2525
|
-
const leanSources = config.claude.leanSettingSources;
|
|
2526
3152
|
const args = [
|
|
2527
3153
|
"--print",
|
|
2528
3154
|
"--model",
|
|
@@ -2531,16 +3157,17 @@ function attemptAutoFix(worktreePath, config, errors) {
|
|
|
2531
3157
|
"50",
|
|
2532
3158
|
"--allowedTools",
|
|
2533
3159
|
"Bash,Read,Write,Edit,Glob,Grep",
|
|
2534
|
-
...
|
|
3160
|
+
...implementRunContainmentCliArgs({ worktree: worktreePath }),
|
|
2535
3161
|
"--",
|
|
2536
3162
|
fixPrompt
|
|
2537
3163
|
];
|
|
2538
|
-
log.info(
|
|
3164
|
+
log.info(TAG10, "Spawning Claude for auto-fix...");
|
|
2539
3165
|
execFileSync4("claude", args, {
|
|
2540
3166
|
cwd: worktreePath,
|
|
2541
3167
|
timeout: config.verification.timeout,
|
|
2542
3168
|
stdio: "pipe",
|
|
2543
|
-
maxBuffer:
|
|
3169
|
+
maxBuffer: MAX_OUTPUT_BUFFER3,
|
|
3170
|
+
env: containedEnv()
|
|
2544
3171
|
});
|
|
2545
3172
|
}
|
|
2546
3173
|
async function reportFindings(client, cardId, result, recovery) {
|
|
@@ -2573,7 +3200,7 @@ async function reportFindings(client, cardId, result, recovery) {
|
|
|
2573
3200
|
try {
|
|
2574
3201
|
await client.createSubtask(cardId, title);
|
|
2575
3202
|
} catch (err) {
|
|
2576
|
-
log.error(
|
|
3203
|
+
log.error(TAG10, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
|
|
2577
3204
|
}
|
|
2578
3205
|
}));
|
|
2579
3206
|
if (overflow > 0) {
|
|
@@ -2581,7 +3208,7 @@ async function reportFindings(client, cardId, result, recovery) {
|
|
|
2581
3208
|
await client.createSubtask(cardId, `...and ${overflow} more issues`);
|
|
2582
3209
|
} catch {}
|
|
2583
3210
|
}
|
|
2584
|
-
log.info(
|
|
3211
|
+
log.info(TAG10, `Reported ${Math.min(items.length, maxSubtasks)} finding(s) as subtasks on card ${cardId}`);
|
|
2585
3212
|
}
|
|
2586
3213
|
function combineOutput(err) {
|
|
2587
3214
|
const stderr = err?.stderr?.toString() ?? "";
|
|
@@ -2614,7 +3241,7 @@ function parseTestFailures(err, timeout) {
|
|
|
2614
3241
|
}
|
|
2615
3242
|
if (e?.code === "ENOBUFS") {
|
|
2616
3243
|
return [
|
|
2617
|
-
`Test output exceeded the ${
|
|
3244
|
+
`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."
|
|
2618
3245
|
];
|
|
2619
3246
|
}
|
|
2620
3247
|
const combined = combineOutput(err);
|
|
@@ -2641,7 +3268,7 @@ class DevServerReadinessError extends Error {
|
|
|
2641
3268
|
}
|
|
2642
3269
|
}
|
|
2643
3270
|
function waitForDevServer(proc, timeout) {
|
|
2644
|
-
return new Promise((
|
|
3271
|
+
return new Promise((resolve3, reject) => {
|
|
2645
3272
|
let settled = false;
|
|
2646
3273
|
const cleanup = () => {
|
|
2647
3274
|
proc.stdout?.off("data", onData);
|
|
@@ -2655,7 +3282,7 @@ function waitForDevServer(proc, timeout) {
|
|
|
2655
3282
|
return;
|
|
2656
3283
|
settled = true;
|
|
2657
3284
|
cleanup();
|
|
2658
|
-
|
|
3285
|
+
resolve3();
|
|
2659
3286
|
};
|
|
2660
3287
|
const settleReject = (err) => {
|
|
2661
3288
|
if (settled)
|
|
@@ -2705,7 +3332,7 @@ async function probeDevServer(port, timeoutMs = 5000) {
|
|
|
2705
3332
|
}
|
|
2706
3333
|
|
|
2707
3334
|
// src/gate-collectors.ts
|
|
2708
|
-
var
|
|
3335
|
+
var TAG11 = "gate-collectors";
|
|
2709
3336
|
async function resolveStageGate(client, card) {
|
|
2710
3337
|
const currentStage = card.current_stage;
|
|
2711
3338
|
const playbookId = card.playbook_id;
|
|
@@ -2723,7 +3350,7 @@ async function resolveStageGate(client, card) {
|
|
|
2723
3350
|
return null;
|
|
2724
3351
|
return { stage: resolution.stage, gate };
|
|
2725
3352
|
} catch (err) {
|
|
2726
|
-
log.warn(
|
|
3353
|
+
log.warn(TAG11, `resolveStageGate failed for stage "${currentStage}": ${err instanceof Error ? err.message : err}`);
|
|
2727
3354
|
return null;
|
|
2728
3355
|
}
|
|
2729
3356
|
}
|
|
@@ -2758,8 +3385,8 @@ class BuildGreenCollector {
|
|
|
2758
3385
|
async collect(_context) {
|
|
2759
3386
|
const doBuild = this.deps.runBuild ?? runBuild;
|
|
2760
3387
|
const doLint = this.deps.runLint ?? runLint;
|
|
2761
|
-
const buildErrors = doBuild(this.deps.worktreePath, this.deps.buildTimeout);
|
|
2762
|
-
const lintWarnings = doLint(this.deps.worktreePath, this.deps.lintTimeout);
|
|
3388
|
+
const buildErrors = await doBuild(this.deps.worktreePath, this.deps.buildTimeout, this.deps.sandbox);
|
|
3389
|
+
const lintWarnings = await doLint(this.deps.worktreePath, this.deps.lintTimeout, this.deps.sandbox);
|
|
2763
3390
|
const buildPassed = buildErrors.length === 0;
|
|
2764
3391
|
const lintPassed = lintWarnings.length === 0;
|
|
2765
3392
|
const result = buildPassed ? "passed" : "failed";
|
|
@@ -2855,7 +3482,7 @@ function buildGateCollectorRegistry(deps) {
|
|
|
2855
3482
|
async function collectGateEvidence(registry, context) {
|
|
2856
3483
|
const collector = registry[context.gate.kind];
|
|
2857
3484
|
if (!collector) {
|
|
2858
|
-
log.info(
|
|
3485
|
+
log.info(TAG11, `No collector for gate kind "${context.gate.kind}" — reporting blocked`);
|
|
2859
3486
|
return {
|
|
2860
3487
|
result: "blocked",
|
|
2861
3488
|
structured: {
|
|
@@ -2867,14 +3494,14 @@ async function collectGateEvidence(registry, context) {
|
|
|
2867
3494
|
return await collector.collect(context);
|
|
2868
3495
|
} catch (err) {
|
|
2869
3496
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2870
|
-
log.warn(
|
|
3497
|
+
log.warn(TAG11, `Collector for "${context.gate.kind}" threw: ${msg} — reporting blocked`);
|
|
2871
3498
|
return { result: "blocked", structured: { error: msg } };
|
|
2872
3499
|
}
|
|
2873
3500
|
}
|
|
2874
3501
|
|
|
2875
3502
|
// src/harmony-client.ts
|
|
2876
3503
|
init_log();
|
|
2877
|
-
var
|
|
3504
|
+
var TAG12 = "harmony-client";
|
|
2878
3505
|
function readClientConfig(env) {
|
|
2879
3506
|
const apiUrl = env.HARMONY_API_URL?.trim();
|
|
2880
3507
|
const apiKey = env.HARMONY_API_KEY?.trim();
|
|
@@ -2926,7 +3553,7 @@ class HarmonyClient {
|
|
|
2926
3553
|
purpose: "gate_evaluation"
|
|
2927
3554
|
});
|
|
2928
3555
|
if (!response.ok) {
|
|
2929
|
-
log.warn(
|
|
3556
|
+
log.warn(TAG12, `Oracle fetch for stage ${stageId} returned ${response.status} — no oracle read, the gate will report blocked`);
|
|
2930
3557
|
return null;
|
|
2931
3558
|
}
|
|
2932
3559
|
const body = await response.json();
|
|
@@ -3009,52 +3636,7 @@ function relayAgentEvent(draft) {
|
|
|
3009
3636
|
|
|
3010
3637
|
// src/stage-cli.ts
|
|
3011
3638
|
init_dist();
|
|
3012
|
-
|
|
3013
|
-
// src/runner.ts
|
|
3014
|
-
init_dist();
|
|
3015
|
-
import { getConfigDir } from "@gethmy/mcp/src/config.js";
|
|
3016
|
-
var HARMONY_CREDENTIAL_KEYS = [
|
|
3017
|
-
"HARMONY_API_KEY",
|
|
3018
|
-
"HARMONY_API_URL",
|
|
3019
|
-
"HARMONY_WORKSPACE_ID",
|
|
3020
|
-
"SUPABASE_ANON_KEY",
|
|
3021
|
-
"SUPABASE_SERVICE_ROLE_KEY",
|
|
3022
|
-
"SUPABASE_URL"
|
|
3023
|
-
];
|
|
3024
|
-
function mayHoldCredentials(role) {
|
|
3025
|
-
return role === "author" || role === "reviewer";
|
|
3026
|
-
}
|
|
3027
|
-
function credentialReadDeny() {
|
|
3028
|
-
return `Read(/${getConfigDir()}/**)`;
|
|
3029
|
-
}
|
|
3030
|
-
function credentialAccessDeny() {
|
|
3031
|
-
const dir = `/${getConfigDir()}/**`;
|
|
3032
|
-
return [`Read(${dir})`, `Grep(${dir})`, `Glob(${dir})`];
|
|
3033
|
-
}
|
|
3034
|
-
function buildRoleLaunch(args) {
|
|
3035
|
-
const role = normalizeStageRole(args.role);
|
|
3036
|
-
const keep = mayHoldCredentials(role);
|
|
3037
|
-
const env = {};
|
|
3038
|
-
for (const [key, value] of Object.entries(args.parentEnv)) {
|
|
3039
|
-
if (value === undefined)
|
|
3040
|
-
continue;
|
|
3041
|
-
if (!keep && HARMONY_CREDENTIAL_KEYS.includes(key))
|
|
3042
|
-
continue;
|
|
3043
|
-
env[key] = value;
|
|
3044
|
-
}
|
|
3045
|
-
return {
|
|
3046
|
-
role,
|
|
3047
|
-
prompt: args.prompt,
|
|
3048
|
-
repoPath: args.repoPath,
|
|
3049
|
-
env,
|
|
3050
|
-
disallowedTools: keep ? [] : [credentialReadDeny()]
|
|
3051
|
-
};
|
|
3052
|
-
}
|
|
3053
|
-
function envKeysDroppedByLaunch(parentEnv, launch) {
|
|
3054
|
-
return Object.keys(parentEnv).filter((key) => parentEnv[key] !== undefined && !Object.hasOwn(launch.env, key));
|
|
3055
|
-
}
|
|
3056
|
-
|
|
3057
|
-
// src/stage-cli.ts
|
|
3639
|
+
init_runner();
|
|
3058
3640
|
var STAGE_RUN_USAGE = "usage: harmony-harness stage run --card <id> --stage <id> --workspace <id> --repo <path> --session <id> [--metrics <json-path>]";
|
|
3059
3641
|
function readFlag(argv, name) {
|
|
3060
3642
|
const index = argv.indexOf(`--${name}`);
|
|
@@ -3255,7 +3837,7 @@ async function runStage(request, deps) {
|
|
|
3255
3837
|
}
|
|
3256
3838
|
|
|
3257
3839
|
// src/cli.ts
|
|
3258
|
-
var
|
|
3840
|
+
var TAG13 = "cli";
|
|
3259
3841
|
var GATE_VERIFICATION_TIMEOUT_MS = 600000;
|
|
3260
3842
|
var SHUTDOWN_HARD_EXIT_MS = 15000;
|
|
3261
3843
|
var activeRunner = null;
|
|
@@ -3286,12 +3868,12 @@ async function runRole(request, prompt, emit2) {
|
|
|
3286
3868
|
});
|
|
3287
3869
|
const runner = new SdkAgentRunner(launch.config);
|
|
3288
3870
|
activeRunner = runner;
|
|
3289
|
-
log.info(
|
|
3871
|
+
log.info(TAG13, `Running stage ${request.stageId} as role ${launch.role ?? "(none — fail-closed)"}`);
|
|
3290
3872
|
const timeoutMs = stageTimeoutMs(process.env);
|
|
3291
3873
|
let timedOut = false;
|
|
3292
3874
|
const clock = timeoutMs > 0 ? setTimeout(() => {
|
|
3293
3875
|
timedOut = true;
|
|
3294
|
-
log.warn(
|
|
3876
|
+
log.warn(TAG13, `stage ${request.stageId} exceeded ${timeoutMs}ms — stopping the subagent`);
|
|
3295
3877
|
runner.stop("timeout");
|
|
3296
3878
|
}, timeoutMs) : null;
|
|
3297
3879
|
clock?.unref?.();
|
|
@@ -3307,9 +3889,9 @@ async function runRole(request, prompt, emit2) {
|
|
|
3307
3889
|
if (relayed)
|
|
3308
3890
|
emit2(relayed);
|
|
3309
3891
|
if (event.kind === "error") {
|
|
3310
|
-
log.warn(
|
|
3892
|
+
log.warn(TAG13, `subagent error: ${event.payload.message}`);
|
|
3311
3893
|
} else {
|
|
3312
|
-
log.event(
|
|
3894
|
+
log.event(TAG13, `subagent ${event.kind}`);
|
|
3313
3895
|
}
|
|
3314
3896
|
}
|
|
3315
3897
|
} finally {
|
|
@@ -3340,8 +3922,8 @@ ${STAGE_RUN_USAGE}
|
|
|
3340
3922
|
const { cardId, stageId, workspaceId, repoPath, sessionId, metricsPath } = parsed.args;
|
|
3341
3923
|
let driverMetrics = {};
|
|
3342
3924
|
if (metricsPath !== null) {
|
|
3343
|
-
const { readFileSync:
|
|
3344
|
-
driverMetrics = parseMetricsAllowlist(
|
|
3925
|
+
const { readFileSync: readFileSync4 } = await import("node:fs");
|
|
3926
|
+
driverMetrics = parseMetricsAllowlist(readFileSync4(metricsPath, "utf8"), metricsPath);
|
|
3345
3927
|
}
|
|
3346
3928
|
const client = new HarmonyClient(readClientConfig(process.env));
|
|
3347
3929
|
const card = await client.fetchStageCard(cardId);
|