@gethmy/harness 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +535 -69
- package/dist/index.js +697 -229
- package/package.json +2 -2
- package/src/ci-failure.ts +10 -5
- package/src/exec-types.ts +5 -7
- package/src/git-diff-stat.ts +2 -1
- package/src/git-pr.ts +59 -25
- package/src/index.ts +1 -0
- package/src/pm.ts +8 -3
- package/src/revert-guard.ts +9 -2
- package/src/run-containment.ts +1292 -0
- package/src/sdk-agent-runner.ts +19 -0
- package/src/verification.ts +84 -7
- package/src/worktree.ts +263 -59
package/dist/cli.js
CHANGED
|
@@ -473,6 +473,13 @@ var init_playbookStage = __esm(() => {
|
|
|
473
473
|
|
|
474
474
|
// ../harmony-shared/dist/projectTemplates.js
|
|
475
475
|
var init_projectTemplates = () => {};
|
|
476
|
+
|
|
477
|
+
// ../harmony-shared/dist/realtimeChannel.js
|
|
478
|
+
var inFlightDetach;
|
|
479
|
+
var init_realtimeChannel = __esm(() => {
|
|
480
|
+
init_logger();
|
|
481
|
+
inFlightDetach = new WeakMap;
|
|
482
|
+
});
|
|
476
483
|
// ../harmony-shared/dist/reviewTools.js
|
|
477
484
|
var REVIEW_DISALLOWED_TOOLS;
|
|
478
485
|
var init_reviewTools = __esm(() => {
|
|
@@ -494,7 +501,6 @@ var init_stageHandoff = __esm(() => {
|
|
|
494
501
|
|
|
495
502
|
// ../harmony-shared/dist/types.js
|
|
496
503
|
var init_types = () => {};
|
|
497
|
-
|
|
498
504
|
// ../harmony-shared/dist/index.js
|
|
499
505
|
var init_dist = __esm(() => {
|
|
500
506
|
init_agentStaleness();
|
|
@@ -514,6 +520,7 @@ var init_dist = __esm(() => {
|
|
|
514
520
|
init_playbookCatalog();
|
|
515
521
|
init_playbookStage();
|
|
516
522
|
init_projectTemplates();
|
|
523
|
+
init_realtimeChannel();
|
|
517
524
|
init_reviewTools();
|
|
518
525
|
init_stageHandoff();
|
|
519
526
|
init_types();
|
|
@@ -603,6 +610,492 @@ var init_log = __esm(() => {
|
|
|
603
610
|
};
|
|
604
611
|
});
|
|
605
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
|
+
|
|
606
1099
|
// src/cli.ts
|
|
607
1100
|
init_dist();
|
|
608
1101
|
|
|
@@ -888,6 +1381,7 @@ class SdkAgentRunner {
|
|
|
888
1381
|
...this.cfg.settingSources ? { settingSources: this.cfg.settingSources } : {},
|
|
889
1382
|
...this.cfg.mcpServers ? { mcpServers: this.cfg.mcpServers } : {},
|
|
890
1383
|
...this.cfg.strictMcpConfig ? { strictMcpConfig: true } : {},
|
|
1384
|
+
...this.cfg.sandbox ? { sandbox: this.cfg.sandbox } : {},
|
|
891
1385
|
stderr: (data) => {
|
|
892
1386
|
this.capturedStderr += data;
|
|
893
1387
|
},
|
|
@@ -2027,6 +2521,7 @@ import { execFileSync as execFileSync4, spawn as spawn2 } from "node:child_proce
|
|
|
2027
2521
|
|
|
2028
2522
|
// src/pm.ts
|
|
2029
2523
|
init_log();
|
|
2524
|
+
init_run_containment();
|
|
2030
2525
|
import { execFileSync } from "node:child_process";
|
|
2031
2526
|
import { existsSync } from "node:fs";
|
|
2032
2527
|
var TAG6 = "pm";
|
|
@@ -2036,7 +2531,7 @@ function detectPackageManager() {
|
|
|
2036
2531
|
return cached;
|
|
2037
2532
|
let repoRoot;
|
|
2038
2533
|
try {
|
|
2039
|
-
repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
2534
|
+
repoRoot = execFileSync("git", [...GIT_NO_HOOKS, "rev-parse", "--show-toplevel"], {
|
|
2040
2535
|
encoding: "utf-8"
|
|
2041
2536
|
}).trim();
|
|
2042
2537
|
} catch {
|
|
@@ -2079,7 +2574,7 @@ function spawnRunArgs(script, ...extra) {
|
|
|
2079
2574
|
// src/project-type.ts
|
|
2080
2575
|
init_log();
|
|
2081
2576
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
2082
|
-
import { existsSync as existsSync2, readdirSync, readFileSync as
|
|
2577
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "node:fs";
|
|
2083
2578
|
var TAG7 = "project-type";
|
|
2084
2579
|
var _cache = new Map;
|
|
2085
2580
|
function _resetCache() {
|
|
@@ -2182,7 +2677,7 @@ var NPM_PLACEHOLDER_TEST = /no test specified/i;
|
|
|
2182
2677
|
function hasNodeTestScript(dir) {
|
|
2183
2678
|
let script;
|
|
2184
2679
|
try {
|
|
2185
|
-
const pkg = JSON.parse(
|
|
2680
|
+
const pkg = JSON.parse(readFileSync3(`${dir}/package.json`, "utf-8"));
|
|
2186
2681
|
script = pkg.scripts?.test;
|
|
2187
2682
|
} catch (err) {
|
|
2188
2683
|
log.warn(TAG7, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
|
|
@@ -2199,7 +2694,7 @@ function hasNodeTestScript(dir) {
|
|
|
2199
2694
|
function firstNodeScript(dir, candidates) {
|
|
2200
2695
|
let scripts;
|
|
2201
2696
|
try {
|
|
2202
|
-
const pkg = JSON.parse(
|
|
2697
|
+
const pkg = JSON.parse(readFileSync3(`${dir}/package.json`, "utf-8"));
|
|
2203
2698
|
scripts = pkg.scripts ?? {};
|
|
2204
2699
|
} catch (err) {
|
|
2205
2700
|
log.warn(TAG7, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
|
|
@@ -2256,6 +2751,7 @@ function resolveXcodeScheme(pt) {
|
|
|
2256
2751
|
|
|
2257
2752
|
// src/revert-guard.ts
|
|
2258
2753
|
init_log();
|
|
2754
|
+
init_run_containment();
|
|
2259
2755
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
2260
2756
|
var TAG8 = "revert-guard";
|
|
2261
2757
|
var TEST_FILE = /(?:^|\/)__tests__\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
@@ -2267,7 +2763,7 @@ function filterTestFiles(paths) {
|
|
|
2267
2763
|
}
|
|
2268
2764
|
function refetchBase(worktreePath, baseBranch) {
|
|
2269
2765
|
try {
|
|
2270
|
-
execFileSync3("git", ["fetch", "origin", baseBranch], {
|
|
2766
|
+
execFileSync3("git", [...GIT_NO_HOOKS, "fetch", "origin", baseBranch], {
|
|
2271
2767
|
cwd: worktreePath,
|
|
2272
2768
|
stdio: "pipe"
|
|
2273
2769
|
});
|
|
@@ -2277,7 +2773,13 @@ function refetchBase(worktreePath, baseBranch) {
|
|
|
2277
2773
|
}
|
|
2278
2774
|
function listDeletedFilesAgainstBase(worktreePath, baseBranch) {
|
|
2279
2775
|
try {
|
|
2280
|
-
const out = execFileSync3("git", [
|
|
2776
|
+
const out = execFileSync3("git", [
|
|
2777
|
+
...GIT_NO_HOOKS,
|
|
2778
|
+
"diff",
|
|
2779
|
+
"--diff-filter=D",
|
|
2780
|
+
"--name-only",
|
|
2781
|
+
`origin/${baseBranch}...HEAD`
|
|
2782
|
+
], { cwd: worktreePath, encoding: "utf-8" });
|
|
2281
2783
|
return out.split(`
|
|
2282
2784
|
`).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
2283
2785
|
} catch (err) {
|
|
@@ -2291,6 +2793,7 @@ function findDeletedTestFiles(worktreePath, baseBranch) {
|
|
|
2291
2793
|
}
|
|
2292
2794
|
|
|
2293
2795
|
// src/verification.ts
|
|
2796
|
+
init_run_containment();
|
|
2294
2797
|
var TAG9 = "verification";
|
|
2295
2798
|
var MAX_OUTPUT_BUFFER2 = 64 * 1024 * 1024;
|
|
2296
2799
|
async function runVerification(worktreePath, config, workerId) {
|
|
@@ -2364,7 +2867,8 @@ function runBuild(worktreePath, timeout) {
|
|
|
2364
2867
|
cwd: worktreePath,
|
|
2365
2868
|
timeout,
|
|
2366
2869
|
stdio: "pipe",
|
|
2367
|
-
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
2870
|
+
maxBuffer: MAX_OUTPUT_BUFFER2,
|
|
2871
|
+
env: containedEnv()
|
|
2368
2872
|
});
|
|
2369
2873
|
return [];
|
|
2370
2874
|
} catch (err) {
|
|
@@ -2382,7 +2886,8 @@ function runTests(worktreePath, timeout) {
|
|
|
2382
2886
|
cwd: worktreePath,
|
|
2383
2887
|
timeout,
|
|
2384
2888
|
stdio: "pipe",
|
|
2385
|
-
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
2889
|
+
maxBuffer: MAX_OUTPUT_BUFFER2,
|
|
2890
|
+
env: containedEnv()
|
|
2386
2891
|
});
|
|
2387
2892
|
return [];
|
|
2388
2893
|
} catch (err) {
|
|
@@ -2401,7 +2906,8 @@ function runFormatFix(worktreePath, timeout, workerId) {
|
|
|
2401
2906
|
cwd: worktreePath,
|
|
2402
2907
|
timeout,
|
|
2403
2908
|
stdio: "pipe",
|
|
2404
|
-
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
2909
|
+
maxBuffer: MAX_OUTPUT_BUFFER2,
|
|
2910
|
+
env: containedEnv()
|
|
2405
2911
|
});
|
|
2406
2912
|
log.info(TAG9, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
|
|
2407
2913
|
} catch (err) {
|
|
@@ -2419,7 +2925,8 @@ function runLint(worktreePath, timeout) {
|
|
|
2419
2925
|
cwd: worktreePath,
|
|
2420
2926
|
timeout,
|
|
2421
2927
|
stdio: "pipe",
|
|
2422
|
-
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
2928
|
+
maxBuffer: MAX_OUTPUT_BUFFER2,
|
|
2929
|
+
env: containedEnv()
|
|
2423
2930
|
});
|
|
2424
2931
|
return [];
|
|
2425
2932
|
} catch (err) {
|
|
@@ -2437,7 +2944,8 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
2437
2944
|
const [cmd, args] = spawnRunArgs("dev", "--port", String(port));
|
|
2438
2945
|
devServer = spawn2(cmd, args, {
|
|
2439
2946
|
cwd: worktreePath,
|
|
2440
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
2947
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2948
|
+
env: containedEnv()
|
|
2441
2949
|
});
|
|
2442
2950
|
try {
|
|
2443
2951
|
await waitForDevServer(devServer, 30000);
|
|
@@ -2448,7 +2956,7 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
2448
2956
|
}
|
|
2449
2957
|
let diff = "";
|
|
2450
2958
|
try {
|
|
2451
|
-
diff = execFileSync4("git", ["diff", `origin/${config.worktree.baseBranch}..HEAD`], {
|
|
2959
|
+
diff = execFileSync4("git", [...GIT_NO_HOOKS, "diff", `origin/${config.worktree.baseBranch}..HEAD`], {
|
|
2452
2960
|
cwd: worktreePath,
|
|
2453
2961
|
encoding: "utf-8",
|
|
2454
2962
|
timeout: 30000,
|
|
@@ -2469,14 +2977,16 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
2469
2977
|
"```"
|
|
2470
2978
|
].join(`
|
|
2471
2979
|
`);
|
|
2472
|
-
const leanSources = config.claude.leanSettingSources;
|
|
2473
2980
|
const output = execFileSync4("claude", [
|
|
2474
2981
|
"--print",
|
|
2475
2982
|
"--model",
|
|
2476
2983
|
"sonnet",
|
|
2477
2984
|
"--max-turns",
|
|
2478
2985
|
"10",
|
|
2479
|
-
...
|
|
2986
|
+
...implementRunContainmentCliArgs({
|
|
2987
|
+
worktree: worktreePath,
|
|
2988
|
+
readOnly: true
|
|
2989
|
+
}),
|
|
2480
2990
|
"--",
|
|
2481
2991
|
reviewPrompt
|
|
2482
2992
|
], {
|
|
@@ -2484,7 +2994,8 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
2484
2994
|
encoding: "utf-8",
|
|
2485
2995
|
timeout: config.verification.timeout,
|
|
2486
2996
|
stdio: "pipe",
|
|
2487
|
-
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
2997
|
+
maxBuffer: MAX_OUTPUT_BUFFER2,
|
|
2998
|
+
env: containedEnv()
|
|
2488
2999
|
});
|
|
2489
3000
|
return parseReviewFindings(output);
|
|
2490
3001
|
} catch (err) {
|
|
@@ -2514,7 +3025,6 @@ function attemptAutoFix(worktreePath, config, errors) {
|
|
|
2514
3025
|
"```"
|
|
2515
3026
|
].join(`
|
|
2516
3027
|
`);
|
|
2517
|
-
const leanSources = config.claude.leanSettingSources;
|
|
2518
3028
|
const args = [
|
|
2519
3029
|
"--print",
|
|
2520
3030
|
"--model",
|
|
@@ -2523,7 +3033,7 @@ function attemptAutoFix(worktreePath, config, errors) {
|
|
|
2523
3033
|
"50",
|
|
2524
3034
|
"--allowedTools",
|
|
2525
3035
|
"Bash,Read,Write,Edit,Glob,Grep",
|
|
2526
|
-
...
|
|
3036
|
+
...implementRunContainmentCliArgs({ worktree: worktreePath }),
|
|
2527
3037
|
"--",
|
|
2528
3038
|
fixPrompt
|
|
2529
3039
|
];
|
|
@@ -2532,7 +3042,8 @@ function attemptAutoFix(worktreePath, config, errors) {
|
|
|
2532
3042
|
cwd: worktreePath,
|
|
2533
3043
|
timeout: config.verification.timeout,
|
|
2534
3044
|
stdio: "pipe",
|
|
2535
|
-
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
3045
|
+
maxBuffer: MAX_OUTPUT_BUFFER2,
|
|
3046
|
+
env: containedEnv()
|
|
2536
3047
|
});
|
|
2537
3048
|
}
|
|
2538
3049
|
async function reportFindings(client, cardId, result, recovery) {
|
|
@@ -2633,7 +3144,7 @@ class DevServerReadinessError extends Error {
|
|
|
2633
3144
|
}
|
|
2634
3145
|
}
|
|
2635
3146
|
function waitForDevServer(proc, timeout) {
|
|
2636
|
-
return new Promise((
|
|
3147
|
+
return new Promise((resolve3, reject) => {
|
|
2637
3148
|
let settled = false;
|
|
2638
3149
|
const cleanup = () => {
|
|
2639
3150
|
proc.stdout?.off("data", onData);
|
|
@@ -2647,7 +3158,7 @@ function waitForDevServer(proc, timeout) {
|
|
|
2647
3158
|
return;
|
|
2648
3159
|
settled = true;
|
|
2649
3160
|
cleanup();
|
|
2650
|
-
|
|
3161
|
+
resolve3();
|
|
2651
3162
|
};
|
|
2652
3163
|
const settleReject = (err) => {
|
|
2653
3164
|
if (settled)
|
|
@@ -3001,52 +3512,7 @@ function relayAgentEvent(draft) {
|
|
|
3001
3512
|
|
|
3002
3513
|
// src/stage-cli.ts
|
|
3003
3514
|
init_dist();
|
|
3004
|
-
|
|
3005
|
-
// src/runner.ts
|
|
3006
|
-
init_dist();
|
|
3007
|
-
import { getConfigDir } from "@gethmy/mcp/src/config.js";
|
|
3008
|
-
var HARMONY_CREDENTIAL_KEYS = [
|
|
3009
|
-
"HARMONY_API_KEY",
|
|
3010
|
-
"HARMONY_API_URL",
|
|
3011
|
-
"HARMONY_WORKSPACE_ID",
|
|
3012
|
-
"SUPABASE_ANON_KEY",
|
|
3013
|
-
"SUPABASE_SERVICE_ROLE_KEY",
|
|
3014
|
-
"SUPABASE_URL"
|
|
3015
|
-
];
|
|
3016
|
-
function mayHoldCredentials(role) {
|
|
3017
|
-
return role === "author" || role === "reviewer";
|
|
3018
|
-
}
|
|
3019
|
-
function credentialReadDeny() {
|
|
3020
|
-
return `Read(/${getConfigDir()}/**)`;
|
|
3021
|
-
}
|
|
3022
|
-
function credentialAccessDeny() {
|
|
3023
|
-
const dir = `/${getConfigDir()}/**`;
|
|
3024
|
-
return [`Read(${dir})`, `Grep(${dir})`, `Glob(${dir})`];
|
|
3025
|
-
}
|
|
3026
|
-
function buildRoleLaunch(args) {
|
|
3027
|
-
const role = normalizeStageRole(args.role);
|
|
3028
|
-
const keep = mayHoldCredentials(role);
|
|
3029
|
-
const env = {};
|
|
3030
|
-
for (const [key, value] of Object.entries(args.parentEnv)) {
|
|
3031
|
-
if (value === undefined)
|
|
3032
|
-
continue;
|
|
3033
|
-
if (!keep && HARMONY_CREDENTIAL_KEYS.includes(key))
|
|
3034
|
-
continue;
|
|
3035
|
-
env[key] = value;
|
|
3036
|
-
}
|
|
3037
|
-
return {
|
|
3038
|
-
role,
|
|
3039
|
-
prompt: args.prompt,
|
|
3040
|
-
repoPath: args.repoPath,
|
|
3041
|
-
env,
|
|
3042
|
-
disallowedTools: keep ? [] : [credentialReadDeny()]
|
|
3043
|
-
};
|
|
3044
|
-
}
|
|
3045
|
-
function envKeysDroppedByLaunch(parentEnv, launch) {
|
|
3046
|
-
return Object.keys(parentEnv).filter((key) => parentEnv[key] !== undefined && !Object.hasOwn(launch.env, key));
|
|
3047
|
-
}
|
|
3048
|
-
|
|
3049
|
-
// src/stage-cli.ts
|
|
3515
|
+
init_runner();
|
|
3050
3516
|
var STAGE_RUN_USAGE = "usage: harmony-harness stage run --card <id> --stage <id> --workspace <id> --repo <path> --session <id> [--metrics <json-path>]";
|
|
3051
3517
|
function readFlag(argv, name) {
|
|
3052
3518
|
const index = argv.indexOf(`--${name}`);
|
|
@@ -3332,8 +3798,8 @@ ${STAGE_RUN_USAGE}
|
|
|
3332
3798
|
const { cardId, stageId, workspaceId, repoPath, sessionId, metricsPath } = parsed.args;
|
|
3333
3799
|
let driverMetrics = {};
|
|
3334
3800
|
if (metricsPath !== null) {
|
|
3335
|
-
const { readFileSync:
|
|
3336
|
-
driverMetrics = parseMetricsAllowlist(
|
|
3801
|
+
const { readFileSync: readFileSync4 } = await import("node:fs");
|
|
3802
|
+
driverMetrics = parseMetricsAllowlist(readFileSync4(metricsPath, "utf8"), metricsPath);
|
|
3337
3803
|
}
|
|
3338
3804
|
const client = new HarmonyClient(readClientConfig(process.env));
|
|
3339
3805
|
const card = await client.fetchStageCard(cardId);
|