@scalequality/cli 0.4.9-rc.3 → 0.4.10
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/connect.build.json +2 -2
- package/dist/connect.cjs +1990 -1168
- package/package.json +1 -1
package/dist/connect.cjs
CHANGED
|
@@ -685,684 +685,1577 @@ function credentialAlias(raw) {
|
|
|
685
685
|
};
|
|
686
686
|
}
|
|
687
687
|
|
|
688
|
-
// src/application/services/workspaceSandbox/
|
|
689
|
-
var
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
688
|
+
// src/application/services/workspaceSandbox/toolPolicy.ts
|
|
689
|
+
var import_fs3 = require("fs");
|
|
690
|
+
var import_promises4 = require("fs/promises");
|
|
691
|
+
var import_path3 = require("path");
|
|
692
|
+
|
|
693
|
+
// src/application/services/workspaceSandbox/workspaceGit.ts
|
|
694
|
+
var import_child_process = require("child_process");
|
|
695
|
+
var import_util = require("util");
|
|
696
|
+
var import_crypto = require("crypto");
|
|
697
|
+
var import_promises2 = require("fs/promises");
|
|
698
|
+
var import_fs2 = require("fs");
|
|
699
|
+
var import_os = require("os");
|
|
700
|
+
var import_path2 = require("path");
|
|
701
|
+
|
|
702
|
+
// src/application/services/workspaceSandbox/checkpointMap.ts
|
|
703
|
+
var CHECKPOINT_MAP_VERSION = 2;
|
|
704
|
+
var LOCAL_FOLDER_KEY = ".";
|
|
705
|
+
function parseCheckpoints(raw, legacyRepo) {
|
|
706
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
707
|
+
if (!raw) return out2;
|
|
708
|
+
const trimmed = raw.trimStart();
|
|
709
|
+
if (trimmed.startsWith("{")) {
|
|
710
|
+
try {
|
|
711
|
+
const parsed = JSON.parse(trimmed);
|
|
712
|
+
if (parsed && parsed.version === CHECKPOINT_MAP_VERSION && parsed.repos && typeof parsed.repos === "object" && !Array.isArray(parsed.repos)) {
|
|
713
|
+
for (const [repo2, patch] of Object.entries(parsed.repos)) {
|
|
714
|
+
if (repo2 && typeof patch === "string" && patch) out2.set(repo2, patch);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
} catch {
|
|
718
|
+
}
|
|
719
|
+
return out2;
|
|
694
720
|
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
721
|
+
out2.set(legacyRepo ?? LOCAL_FOLDER_KEY, raw);
|
|
722
|
+
return out2;
|
|
723
|
+
}
|
|
724
|
+
function serializeCheckpoints(map) {
|
|
725
|
+
const repos = {};
|
|
726
|
+
for (const key of [...map.keys()].sort()) {
|
|
727
|
+
const patch = map.get(key);
|
|
728
|
+
if (patch) repos[key] = patch;
|
|
703
729
|
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
/** The API's error code (e.g. REPOSITORY_NOT_IN_SCOPE), when it sent one. */
|
|
707
|
-
code;
|
|
708
|
-
};
|
|
730
|
+
return Object.keys(repos).length ? JSON.stringify({ version: CHECKPOINT_MAP_VERSION, repos }) : "";
|
|
731
|
+
}
|
|
709
732
|
|
|
710
|
-
// src/application/services/workspaceSandbox/
|
|
711
|
-
var
|
|
712
|
-
var
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
733
|
+
// src/application/services/workspaceSandbox/workspaceGit.ts
|
|
734
|
+
var run = (0, import_util.promisify)(import_child_process.execFile);
|
|
735
|
+
var CHECKPOINT_BASE_HEADER = "ScaleQuality-Base:";
|
|
736
|
+
var DIFF_CAPS = { perFileBytes: 200 * 1024, totalBytes: 2 * 1024 * 1024, maxFiles: 500 };
|
|
737
|
+
async function git(args, opts) {
|
|
738
|
+
const { stdout } = await run("git", args, {
|
|
739
|
+
cwd: opts.cwd,
|
|
740
|
+
timeout: opts.timeoutMs ?? 6e4,
|
|
741
|
+
maxBuffer: opts.maxBuffer ?? 64 * 1024 * 1024,
|
|
742
|
+
env: gitEnv(opts.env),
|
|
743
|
+
windowsHide: true
|
|
744
|
+
});
|
|
745
|
+
return stdout;
|
|
746
|
+
}
|
|
747
|
+
async function gitBuffer(args, opts) {
|
|
748
|
+
const { stdout } = await run("git", args, {
|
|
749
|
+
cwd: opts.cwd,
|
|
750
|
+
timeout: opts.timeoutMs ?? 6e4,
|
|
751
|
+
maxBuffer: opts.maxBuffer ?? 64 * 1024 * 1024,
|
|
752
|
+
env: gitEnv(opts.env),
|
|
753
|
+
windowsHide: true,
|
|
754
|
+
encoding: "buffer"
|
|
755
|
+
});
|
|
756
|
+
return stdout;
|
|
757
|
+
}
|
|
758
|
+
function gitEnv(extra) {
|
|
759
|
+
const env = {};
|
|
760
|
+
for (const k of ["PATH", "HOME", "LANG", "LC_ALL", "TMPDIR", "GIT_CONFIG_GLOBAL"]) if (process.env[k] != null) env[k] = process.env[k];
|
|
761
|
+
env.GIT_TERMINAL_PROMPT = "0";
|
|
762
|
+
env.GIT_CONFIG_NOSYSTEM = "1";
|
|
763
|
+
return { ...env, ...extra ?? {} };
|
|
764
|
+
}
|
|
765
|
+
function scopeSpec(scope) {
|
|
766
|
+
return scope?.prefix ? ["--", scope.prefix] : [];
|
|
767
|
+
}
|
|
768
|
+
async function repoIndexFile(root) {
|
|
769
|
+
const dotGit = (0, import_path2.join)(root, ".git");
|
|
770
|
+
const st = await (0, import_promises2.stat)(dotGit).catch(() => null);
|
|
771
|
+
if (!st || st.isDirectory()) return (0, import_path2.join)(dotGit, "index");
|
|
772
|
+
const out2 = await git(["rev-parse", "--git-path", "index"], { cwd: root }).catch(() => "");
|
|
773
|
+
return out2.trim() ? (0, import_path2.resolve)(root, out2.trim()) : (0, import_path2.join)(dotGit, "index");
|
|
774
|
+
}
|
|
775
|
+
async function withWorktreeIndex(root, fn, scope) {
|
|
776
|
+
const tag = `${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
777
|
+
const idx = (0, import_path2.join)((0, import_os.tmpdir)(), `sq-ws-index-${tag}`);
|
|
778
|
+
const real = scope?.index !== void 0 ? scope.index : await repoIndexFile(root);
|
|
779
|
+
if (real && (0, import_fs2.existsSync)(real)) {
|
|
780
|
+
await (0, import_promises2.copyFile)(real, idx);
|
|
781
|
+
const st = await (0, import_promises2.stat)(real).catch(() => null);
|
|
782
|
+
if (st) await (0, import_promises2.utimes)(idx, st.atime, st.mtime).catch(() => void 0);
|
|
719
783
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
784
|
+
const env = { ...scope?.env ?? {}, GIT_INDEX_FILE: idx, ...scope?.prefix || scope?.paths ? { GIT_LITERAL_PATHSPECS: "1" } : {} };
|
|
785
|
+
const list = (0, import_path2.join)((0, import_os.tmpdir)(), `sq-ws-paths-${tag}`);
|
|
786
|
+
try {
|
|
787
|
+
if (scope?.paths) {
|
|
788
|
+
if (scope.paths.length) {
|
|
789
|
+
await (0, import_promises2.writeFile)(list, scope.paths.join("\0"), "utf8");
|
|
790
|
+
await git(["add", "-A", "-f", `--pathspec-from-file=${list}`, "--pathspec-file-nul"], { cwd: root, env, timeoutMs: 5 * 6e4 });
|
|
791
|
+
}
|
|
792
|
+
} else {
|
|
793
|
+
await git(["add", "-A", ...scopeSpec(scope)], { cwd: root, env, timeoutMs: 5 * 6e4 });
|
|
794
|
+
}
|
|
795
|
+
return await fn(env);
|
|
796
|
+
} finally {
|
|
797
|
+
await (0, import_promises2.rm)(idx, { force: true }).catch(() => void 0);
|
|
798
|
+
await (0, import_promises2.rm)(list, { force: true }).catch(() => void 0);
|
|
727
799
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
800
|
+
}
|
|
801
|
+
async function listChanges(root, base, scope) {
|
|
802
|
+
return withWorktreeIndex(root, (env) => listChangesWithEnv(root, base, env, scope), scope);
|
|
803
|
+
}
|
|
804
|
+
async function listChangesWithEnv(root, base, env, scope) {
|
|
805
|
+
const nameStatus = await git(["diff", "--cached", "--no-renames", "--name-status", "-z", base, ...scopeSpec(scope)], { cwd: root, env });
|
|
806
|
+
const numstat = await git(["diff", "--cached", "--no-renames", "--numstat", "-z", base, ...scopeSpec(scope)], { cwd: root, env });
|
|
807
|
+
const counts = /* @__PURE__ */ new Map();
|
|
808
|
+
for (const rec of numstat.split("\0")) {
|
|
809
|
+
if (!rec) continue;
|
|
810
|
+
const m = /^(-|\d+)\t(-|\d+)\t([\s\S]*)$/.exec(rec);
|
|
811
|
+
if (!m) continue;
|
|
812
|
+
counts.set(m[3], { a: m[1] === "-" ? 0 : Number(m[1]), d: m[2] === "-" ? 0 : Number(m[2]), bin: m[1] === "-" });
|
|
813
|
+
}
|
|
814
|
+
const parts = nameStatus.split("\0");
|
|
815
|
+
const out2 = [];
|
|
816
|
+
for (let i = 0; i + 1 < parts.length; i += 2) {
|
|
817
|
+
const code = parts[i];
|
|
818
|
+
const path = parts[i + 1];
|
|
819
|
+
if (!code || !path) continue;
|
|
820
|
+
const status = code.startsWith("A") ? "added" : code.startsWith("D") ? "deleted" : "modified";
|
|
821
|
+
const c = counts.get(path) ?? { a: 0, d: 0, bin: false };
|
|
822
|
+
out2.push({ path, status, additions: c.a, deletions: c.d, binary: c.bin });
|
|
823
|
+
}
|
|
824
|
+
return out2;
|
|
825
|
+
}
|
|
826
|
+
async function computeDiff(root, base, caps = DIFF_CAPS, scope) {
|
|
827
|
+
return withWorktreeIndex(root, async (env) => {
|
|
828
|
+
const changes = await listChangesWithEnv(root, base, env, scope);
|
|
829
|
+
const files = [];
|
|
830
|
+
let total = 0;
|
|
831
|
+
for (const [i, c] of changes.entries()) {
|
|
832
|
+
const entry = { path: c.path, status: c.status, additions: c.additions, deletions: c.deletions, patch: "" };
|
|
833
|
+
if (c.binary) {
|
|
834
|
+
entry.binary = true;
|
|
835
|
+
} else if (i >= caps.maxFiles || total >= caps.totalBytes) {
|
|
836
|
+
entry.truncated = true;
|
|
837
|
+
} else {
|
|
838
|
+
let patch = await git(["diff", "--cached", "--no-renames", base, "--", c.path], { cwd: root, env }).catch(() => "");
|
|
839
|
+
const room = Math.min(caps.perFileBytes, caps.totalBytes - total);
|
|
840
|
+
if (Buffer.byteLength(patch, "utf8") > room) {
|
|
841
|
+
patch = truncateUtf8(patch, room);
|
|
842
|
+
entry.truncated = true;
|
|
843
|
+
}
|
|
844
|
+
total += Buffer.byteLength(patch, "utf8");
|
|
845
|
+
entry.patch = patch;
|
|
846
|
+
if (!entry.truncated) {
|
|
847
|
+
const hunks = parseHunks(c.path, patch).map(({ lines: _l, ...h }) => h);
|
|
848
|
+
if (hunks.length) entry.hunks = hunks;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
files.push(entry);
|
|
852
|
+
}
|
|
853
|
+
return files;
|
|
854
|
+
}, scope);
|
|
855
|
+
}
|
|
856
|
+
async function worktreeTreeId(root, scope) {
|
|
857
|
+
return withWorktreeIndex(root, async (env) => (await git(["write-tree"], { cwd: root, env })).trim(), scope);
|
|
858
|
+
}
|
|
859
|
+
async function checkpointPatch(root, base, scope) {
|
|
860
|
+
return withWorktreeIndex(root, async (env) => {
|
|
861
|
+
const patch = await git(["diff", "--cached", "--no-renames", "--binary", base, ...scopeSpec(scope)], { cwd: root, env, maxBuffer: 512 * 1024 * 1024 });
|
|
862
|
+
return patch ? `${CHECKPOINT_BASE_HEADER} ${base}
|
|
863
|
+
${patch}` : "";
|
|
864
|
+
}, scope);
|
|
865
|
+
}
|
|
866
|
+
var PR_FILE_MAX_BYTES = 2 * 1024 * 1024;
|
|
867
|
+
async function prText(abs) {
|
|
868
|
+
const st = await (0, import_promises2.stat)(abs).catch(() => null);
|
|
869
|
+
if (!st || !st.isFile()) return { reason: "missing" };
|
|
870
|
+
if (st.size > PR_FILE_MAX_BYTES) return { reason: "too_large" };
|
|
871
|
+
const buf = await (0, import_promises2.readFile)(abs);
|
|
872
|
+
if (buf.includes(0)) return { reason: "binary" };
|
|
873
|
+
const content = buf.toString("utf8");
|
|
874
|
+
if (Buffer.byteLength(content, "utf8") !== buf.length || content.includes("\uFFFD")) return { reason: "not_utf8" };
|
|
875
|
+
return { content };
|
|
876
|
+
}
|
|
877
|
+
async function filesForPullRequest(root, base, published = [], scope) {
|
|
878
|
+
const changes = await listChanges(root, base, scope);
|
|
879
|
+
const out2 = { files: [], skipped: [] };
|
|
880
|
+
for (const c of changes) {
|
|
881
|
+
if (c.status === "deleted") {
|
|
882
|
+
out2.files.push({ path: c.path, content: "", deleted: true });
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
if (c.binary) {
|
|
886
|
+
out2.skipped.push({ path: c.path, reason: "binary" });
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
const r = await prText((0, import_path2.join)(root, c.path));
|
|
890
|
+
if ("content" in r) out2.files.push({ path: c.path, content: r.content });
|
|
891
|
+
else if (r.reason === "missing") out2.files.push({ path: c.path, content: "", deleted: true });
|
|
892
|
+
else out2.skipped.push({ path: c.path, reason: r.reason });
|
|
893
|
+
}
|
|
894
|
+
const changed = new Set(changes.map((c) => c.path));
|
|
895
|
+
for (const path of published) {
|
|
896
|
+
if (changed.has(path) || !path || path.startsWith("/") || path.split("/").includes("..")) continue;
|
|
897
|
+
if (scope?.prefix && !path.startsWith(`${scope.prefix}/`)) continue;
|
|
898
|
+
const buf = await gitBuffer(["show", `${base}:${path}`], { cwd: root, maxBuffer: 8 * 1024 * 1024, env: scope?.env }).catch(() => null);
|
|
899
|
+
if (buf === null) {
|
|
900
|
+
out2.files.push({ path, content: "", deleted: true });
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
const content = buf.toString("utf8");
|
|
904
|
+
if (buf.includes(0) || Buffer.byteLength(content, "utf8") !== buf.length) {
|
|
905
|
+
out2.skipped.push({ path, reason: "binary" });
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
out2.files.push({ path, content });
|
|
909
|
+
}
|
|
910
|
+
return out2;
|
|
911
|
+
}
|
|
912
|
+
async function resolveInside(root, p) {
|
|
913
|
+
if (!p || p.includes("\0")) return null;
|
|
914
|
+
const abs = (0, import_path2.resolve)(root, p);
|
|
915
|
+
const realRoot = await (0, import_promises2.realpath)(root).catch(() => (0, import_path2.resolve)(root));
|
|
916
|
+
let probe = abs;
|
|
917
|
+
while (!(0, import_fs2.existsSync)(probe) && (0, import_path2.dirname)(probe) !== probe) probe = (0, import_path2.dirname)(probe);
|
|
918
|
+
const realProbe = await (0, import_promises2.realpath)(probe).catch(() => probe);
|
|
919
|
+
const rest = (0, import_path2.relative)(probe, abs);
|
|
920
|
+
const finalPath = rest ? (0, import_path2.join)(realProbe, rest) : realProbe;
|
|
921
|
+
if (finalPath !== realRoot && !finalPath.startsWith(realRoot + import_path2.sep)) return null;
|
|
922
|
+
return finalPath;
|
|
923
|
+
}
|
|
924
|
+
async function discardPath(root, base, relPath) {
|
|
925
|
+
if ((0, import_path2.isAbsolute)(relPath)) relPath = (0, import_path2.relative)(root, relPath);
|
|
926
|
+
const abs = await resolveInside(root, relPath);
|
|
927
|
+
if (!abs) throw new Error("PATH_OUTSIDE_WORKSPACE");
|
|
928
|
+
const rel = (0, import_path2.relative)(await (0, import_promises2.realpath)(root).catch(() => root), abs).split(import_path2.sep).join("/");
|
|
929
|
+
if (rel === "" || rel === ".git" || rel.startsWith(".git/")) throw new Error("PATH_NOT_DISCARDABLE");
|
|
930
|
+
const existed = await git(["cat-file", "-e", `${base}:${rel}`], { cwd: root }).then(() => true, () => false);
|
|
931
|
+
if (existed) {
|
|
932
|
+
await git(["checkout", base, "--", rel], { cwd: root });
|
|
933
|
+
await git(["reset", "-q", "--", rel], { cwd: root }).catch(() => void 0);
|
|
934
|
+
return "restored";
|
|
732
935
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
936
|
+
await (0, import_promises2.rm)(abs, { recursive: true, force: true });
|
|
937
|
+
await git(["rm", "-q", "--cached", "--ignore-unmatch", "--", rel], { cwd: root }).catch(() => void 0);
|
|
938
|
+
return "removed";
|
|
939
|
+
}
|
|
940
|
+
function truncateUtf8(s, maxBytes) {
|
|
941
|
+
if (maxBytes <= 0) return "";
|
|
942
|
+
const buf = Buffer.from(s, "utf8");
|
|
943
|
+
if (buf.length <= maxBytes) return s;
|
|
944
|
+
let end = maxBytes;
|
|
945
|
+
while (end > 0 && (buf[end] & 192) === 128) end--;
|
|
946
|
+
return buf.subarray(0, end).toString("utf8");
|
|
947
|
+
}
|
|
948
|
+
function tailUtf8(s, maxBytes) {
|
|
949
|
+
const buf = Buffer.from(s, "utf8");
|
|
950
|
+
if (buf.length <= maxBytes) return s;
|
|
951
|
+
let start = buf.length - maxBytes;
|
|
952
|
+
while (start < buf.length && (buf[start] & 192) === 128) start++;
|
|
953
|
+
return buf.subarray(start).toString("utf8");
|
|
954
|
+
}
|
|
955
|
+
function parseHunks(path, patch) {
|
|
956
|
+
const out2 = [];
|
|
957
|
+
let current = null;
|
|
958
|
+
for (const line of patch.split("\n")) {
|
|
959
|
+
const m = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/.exec(line);
|
|
960
|
+
if (m) {
|
|
961
|
+
current = {
|
|
962
|
+
hash: "",
|
|
963
|
+
header: line,
|
|
964
|
+
lines: [],
|
|
965
|
+
oldStart: Number(m[1]),
|
|
966
|
+
oldLines: m[2] === void 0 ? 1 : Number(m[2]),
|
|
967
|
+
newStart: Number(m[3]),
|
|
968
|
+
newLines: m[4] === void 0 ? 1 : Number(m[4])
|
|
969
|
+
};
|
|
970
|
+
out2.push(current);
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
if (!current) continue;
|
|
974
|
+
if (line.startsWith(" ") || line.startsWith("+") || line.startsWith("-") || line.startsWith("\\")) current.lines.push(line);
|
|
736
975
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
976
|
+
const seen = /* @__PURE__ */ new Map();
|
|
977
|
+
for (const h of out2) {
|
|
978
|
+
const base = (0, import_crypto.createHash)("sha256").update(`${path}
|
|
979
|
+
${h.lines.join("\n")}`).digest("hex").slice(0, 16);
|
|
980
|
+
const n = (seen.get(base) ?? 0) + 1;
|
|
981
|
+
seen.set(base, n);
|
|
982
|
+
h.hash = n === 1 ? base : `${base}-${n}`;
|
|
740
983
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
984
|
+
return out2;
|
|
985
|
+
}
|
|
986
|
+
async function discardHunk(root, base, relPath, hash, scope) {
|
|
987
|
+
const abs = await resolveInside(root, relPath);
|
|
988
|
+
if (!abs) throw new Error("PATH_OUTSIDE_WORKSPACE");
|
|
989
|
+
const rel = (0, import_path2.relative)(await (0, import_promises2.realpath)(root).catch(() => root), abs).split(import_path2.sep).join("/");
|
|
990
|
+
if (rel === "" || rel === ".git" || rel.startsWith(".git/")) throw new Error("PATH_NOT_DISCARDABLE");
|
|
991
|
+
return withWorktreeIndex(root, async (env) => {
|
|
992
|
+
const patch = await git(["diff", "--cached", "--no-renames", "--no-color", base, "--", rel], { cwd: root, env });
|
|
993
|
+
if (!patch) return "not_found";
|
|
994
|
+
const hunk = parseHunks(rel, patch).find((h) => h.hash === hash);
|
|
995
|
+
if (!hunk) return "not_found";
|
|
996
|
+
const headerEnd = patch.indexOf("\n@@");
|
|
997
|
+
const fileHeader = patch.slice(0, headerEnd + 1);
|
|
998
|
+
const one = `${fileHeader}${hunk.header}
|
|
999
|
+
${hunk.lines.join("\n")}
|
|
1000
|
+
`;
|
|
1001
|
+
const file = (0, import_path2.join)((0, import_os.tmpdir)(), `sq-hunk-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.patch`);
|
|
1002
|
+
await (0, import_promises2.writeFile)(file, one, "utf8");
|
|
1003
|
+
try {
|
|
1004
|
+
await git(["apply", "-R", "--whitespace=nowarn", file], { cwd: root, env: scope?.env });
|
|
1005
|
+
} finally {
|
|
1006
|
+
await (0, import_promises2.rm)(file, { force: true }).catch(() => void 0);
|
|
1007
|
+
}
|
|
1008
|
+
return "discarded";
|
|
1009
|
+
}, scope);
|
|
1010
|
+
}
|
|
1011
|
+
async function restoreTree(root, treeId, scope) {
|
|
1012
|
+
if (!/^[0-9a-f]{40}([0-9a-f]{24})?$/.test(treeId)) throw new Error("INVALID_TREE");
|
|
1013
|
+
await withWorktreeIndex(root, async (env) => {
|
|
1014
|
+
const now = (await git(["write-tree"], { cwd: root, env })).trim();
|
|
1015
|
+
const added = await git(["diff", "--no-renames", "--name-only", "-z", "--diff-filter=A", treeId, now, ...scopeSpec(scope)], { cwd: root, env });
|
|
1016
|
+
for (const rel of added.split("\0").filter(Boolean)) {
|
|
1017
|
+
const abs = await resolveInside(root, rel);
|
|
1018
|
+
if (abs) await (0, import_promises2.rm)(abs, { force: true });
|
|
1019
|
+
}
|
|
1020
|
+
if (scope?.prefix) {
|
|
1021
|
+
await git(["checkout", treeId, "--", scope.prefix], { cwd: root, env }).catch(() => void 0);
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
await git(["read-tree", treeId], { cwd: root, env });
|
|
1025
|
+
await git(["checkout-index", "-a", "-f"], { cwd: root, env });
|
|
1026
|
+
}, scope);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// src/application/services/workspaceSandbox/webFetchGuard.ts
|
|
1030
|
+
var import_promises3 = require("dns/promises");
|
|
1031
|
+
var import_net = require("net");
|
|
1032
|
+
var defaultLookup = (host) => (0, import_promises3.lookup)(host, { all: true, verbatim: true });
|
|
1033
|
+
var ALLOWED_PORTS = /* @__PURE__ */ new Set(["", "80", "443", "8080", "8443"]);
|
|
1034
|
+
function v4Private(ip) {
|
|
1035
|
+
const [a, b] = ip.split(".").map(Number);
|
|
1036
|
+
return a === 0 || a === 10 || a === 127 || a >= 224 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 192 && b === 0 || a === 198 && (b === 18 || b === 19);
|
|
1037
|
+
}
|
|
1038
|
+
function v6Private(ip) {
|
|
1039
|
+
const s = ip.toLowerCase().replace(/^\[|\]$/g, "");
|
|
1040
|
+
if (s === "::" || s === "::1") return true;
|
|
1041
|
+
const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(s);
|
|
1042
|
+
if (mapped) return v4Private(mapped[1]);
|
|
1043
|
+
const head = parseInt(s.split(":")[0] || "0", 16);
|
|
1044
|
+
return (head & 65024) === 64512 || (head & 65472) === 65152 || (head & 65280) === 65280 || s.startsWith("fd00:ec2::");
|
|
1045
|
+
}
|
|
1046
|
+
function privateAddress(ip) {
|
|
1047
|
+
const kind = (0, import_net.isIP)(ip.replace(/^\[|\]$/g, ""));
|
|
1048
|
+
if (kind === 4) return v4Private(ip);
|
|
1049
|
+
if (kind === 6) return v6Private(ip);
|
|
1050
|
+
return true;
|
|
1051
|
+
}
|
|
1052
|
+
async function webFetchDenial(raw, resolve8 = defaultLookup) {
|
|
1053
|
+
if (typeof raw !== "string" || !raw.trim()) return "WebFetch needs a URL.";
|
|
1054
|
+
let url;
|
|
1055
|
+
try {
|
|
1056
|
+
url = new URL(raw.trim());
|
|
1057
|
+
} catch {
|
|
1058
|
+
return "WebFetch needs a valid http or https URL.";
|
|
744
1059
|
}
|
|
745
|
-
|
|
746
|
-
|
|
1060
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return "WebFetch reads only http and https pages.";
|
|
1061
|
+
if (url.username || url.password) return "URLs with credentials cannot be fetched from the workspace.";
|
|
1062
|
+
if (!ALLOWED_PORTS.has(url.port)) return "WebFetch reads public pages on the standard web ports only.";
|
|
1063
|
+
const host = url.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
1064
|
+
if (!host || host === "localhost" || host.endsWith(".localhost") || host.endsWith(".internal") || host.endsWith(".local")) {
|
|
1065
|
+
return "Addresses of this machine or its private network cannot be fetched from the workspace.";
|
|
747
1066
|
}
|
|
748
|
-
|
|
749
|
-
|
|
1067
|
+
if ((0, import_net.isIP)(host)) return privateAddress(host) ? "Addresses of this machine or its private network cannot be fetched from the workspace." : null;
|
|
1068
|
+
let addresses;
|
|
1069
|
+
try {
|
|
1070
|
+
addresses = await resolve8(host);
|
|
1071
|
+
} catch {
|
|
1072
|
+
return `The host ${host} could not be resolved.`;
|
|
750
1073
|
}
|
|
751
|
-
|
|
752
|
-
|
|
1074
|
+
if (!addresses.length || addresses.some((a) => privateAddress(a.address))) {
|
|
1075
|
+
return "Addresses of this machine or its private network cannot be fetched from the workspace.";
|
|
753
1076
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
1077
|
+
return null;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// src/application/services/workspaceSandbox/toolPolicy.ts
|
|
1081
|
+
var SQ_MCP_SERVER = "scalequality";
|
|
1082
|
+
var SQ_MCP_PREFIX = `mcp__${SQ_MCP_SERVER}__`;
|
|
1083
|
+
var DENIED_TOOLS = ["WebSearch", "RemoteTrigger", "CronCreate", "CronDelete", "CronList", "ScheduleWakeup", "PushNotification", "EnterWorktree", "ExitWorktree", "Artifact", "Workflow", "SendFeedback", "ClaudeDesign", "Projects"];
|
|
1084
|
+
var READ_TOOLS = { Read: "file_path", Glob: "path", Grep: "path", LS: "path" };
|
|
1085
|
+
var WRITE_TOOLS = { Write: "file_path", Edit: "file_path", MultiEdit: "file_path", NotebookEdit: "notebook_path" };
|
|
1086
|
+
var WRITE_TOOL_PATHS = WRITE_TOOLS;
|
|
1087
|
+
var HARMLESS = /* @__PURE__ */ new Set(["TodoWrite", "BashOutput", "KillShell", "TaskStop", "TaskOutput", "Skill"]);
|
|
1088
|
+
var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
|
|
1089
|
+
var SQ_READ_ONLY = /* @__PURE__ */ new Set([
|
|
1090
|
+
"get_project_measurement",
|
|
1091
|
+
"get_measurement_findings",
|
|
1092
|
+
"get_agent_options",
|
|
1093
|
+
"get_agent_quote",
|
|
1094
|
+
"get_pr_followup_status",
|
|
1095
|
+
"get_agent_request",
|
|
1096
|
+
"get_measurement_request",
|
|
1097
|
+
"get_measurement_setup",
|
|
1098
|
+
"explain_change_request",
|
|
1099
|
+
"read_scalequality_guide",
|
|
1100
|
+
"list_repositories",
|
|
1101
|
+
"open_repository",
|
|
1102
|
+
"measure_change"
|
|
1103
|
+
]);
|
|
1104
|
+
var BASH_MAX_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
1105
|
+
var CLOUD_BASH_DENY = [
|
|
1106
|
+
{ re: /\bgit\b[^\n;&|]*\s(push|send-pack|send-email|request-pull)\b/, why: "Publishing from the workspace is not allowed. Use the open_pull_request tool; it asks the user for approval." },
|
|
1107
|
+
{ re: /\bgit\b[^\n;&|]*\sremote\b/, why: "Git remotes are managed by ScaleQuality and cannot be read or changed from the workspace." }
|
|
1108
|
+
];
|
|
1109
|
+
var BASH_DENY = [
|
|
1110
|
+
{ re: /\bgit\b[^\n;&|]*\scredential\b/, why: "Git credentials are not available in the workspace." },
|
|
1111
|
+
{ re: /\bgit\b[^\n;&|]*\sconfig\b[^\n;&|]*(credential|insteadof|pushinsteadof|extraheader|\bremote\.|\burl\.)/, why: "Git credential and remote settings cannot be changed from the workspace." },
|
|
1112
|
+
{ re: /credential[._-]?helper/, why: "Git credential helpers cannot be configured in the workspace." },
|
|
1113
|
+
{ re: /\/proc\/[^\s'"]*\/(environ|mem)\b/, why: "Process environments are not readable from the workspace." },
|
|
1114
|
+
{ re: /\b(INTERNAL_API_SECRET|WORKSPACE_SESSION_SECRET|ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|ANTHROPIC_BASE_URL)\b/, why: "Workspace credentials are not available to commands." },
|
|
1115
|
+
{ re: /(^|[;&|(`]|\$\()\s*(env|printenv|export\s+-p|declare\s+-x|set)\s*($|[;&|)>`])/, why: "Dumping the whole environment is not allowed; read the specific variable your task needs." }
|
|
1116
|
+
];
|
|
1117
|
+
var HOME_PREFIX = String.raw`(~|\$\{?home\}?|\$\{?userprofile\}?|/users/[^/\s]+|/home/[^/\s]+|/root)/`;
|
|
1118
|
+
var LOCAL_BASH_DENY = [
|
|
1119
|
+
{ re: /(^|[\s=:/~])\.(ssh|aws|gnupg)(\/|\s|$)/i, why: "Credential directories on this machine are not readable from the workspace." },
|
|
1120
|
+
{ re: /(^|[\s=:/~])\.(netrc|git-credentials|pgpass)\b/i, why: "Credential files on this machine are not readable from the workspace." },
|
|
1121
|
+
// Projects have their own .npmrc or .docker folder; only the ones in the home directory hold credentials.
|
|
1122
|
+
{ re: new RegExp(`${HOME_PREFIX}\\.(npmrc|yarnrc\\.yml|pypirc|docker|kube|azure|config/(gh|hub|gcloud|op))\\b`, "i"), why: "Credential files on this machine are not readable from the workspace." },
|
|
1123
|
+
{ re: /(^|[;&|(`]|\$\()\s*(security\s+(find|dump|export)-|gh\s+auth\s+(token|status\s+-t\b|status\s+--show-token)|op\s+(read|item\s+get)|pass\s+show|secret-tool\s+lookup|aws\s+configure\s+(get|export-credentials)|gcloud\s+auth\s+print-)/i, why: "Credential stores on this machine are not readable from the workspace." }
|
|
1124
|
+
];
|
|
1125
|
+
function normalizeCommand(command) {
|
|
1126
|
+
return command.replace(/\\\n/g, " ").replace(/['"\\]/g, "").replace(/[ \t]+/g, " ").toLowerCase();
|
|
1127
|
+
}
|
|
1128
|
+
function bashDenial(command, opts = {}) {
|
|
1129
|
+
const n = normalizeCommand(command);
|
|
1130
|
+
for (const { re, why } of BASH_DENY) if (re.test(n) || re.test(command)) return why;
|
|
1131
|
+
if (!opts.local) {
|
|
1132
|
+
for (const { re, why } of CLOUD_BASH_DENY) if (re.test(n) || re.test(command)) return why;
|
|
767
1133
|
}
|
|
768
|
-
|
|
769
|
-
const
|
|
770
|
-
const messages = Array.isArray(raw?.messages) ? raw.messages : [];
|
|
771
|
-
return {
|
|
772
|
-
messages: messages.filter((m) => !!m && typeof m === "object" && (m.role === "user" || m.role === "assistant")).map((m) => ({
|
|
773
|
-
role: m.role,
|
|
774
|
-
text: typeof m.text === "string" ? m.text : "",
|
|
775
|
-
at: typeof m.at === "string" ? m.at : null,
|
|
776
|
-
...Array.isArray(m.tools) ? { tools: m.tools.filter((t) => !!t && typeof t.name === "string" && typeof t.summary === "string") } : {}
|
|
777
|
-
}))
|
|
778
|
-
};
|
|
1134
|
+
if (opts.local) {
|
|
1135
|
+
for (const { re, why } of LOCAL_BASH_DENY) if (re.test(n) || re.test(command)) return why;
|
|
779
1136
|
}
|
|
780
|
-
|
|
781
|
-
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
async function realpathLoose(p) {
|
|
1140
|
+
let probe = (0, import_path3.resolve)(p);
|
|
1141
|
+
while (!(0, import_fs3.existsSync)(probe) && (0, import_path3.dirname)(probe) !== probe) probe = (0, import_path3.dirname)(probe);
|
|
1142
|
+
const real = await (0, import_promises4.realpath)(probe).catch(() => probe);
|
|
1143
|
+
const rest = (0, import_path3.relative)(probe, (0, import_path3.resolve)(p));
|
|
1144
|
+
return rest ? (0, import_path3.join)(real, rest) : real;
|
|
1145
|
+
}
|
|
1146
|
+
function inside(root, abs) {
|
|
1147
|
+
return abs === root || abs.startsWith(root + import_path3.sep);
|
|
1148
|
+
}
|
|
1149
|
+
var PLAN_ONLY_READS = "Plan mode is read-only: read and search the code, write the plan to the plan file, then call ExitPlanMode. Changes start after the user approves the plan.";
|
|
1150
|
+
var READ_ONLY_PROGRAMS = /* @__PURE__ */ new Set([
|
|
1151
|
+
"ls",
|
|
1152
|
+
"cat",
|
|
1153
|
+
"head",
|
|
1154
|
+
"tail",
|
|
1155
|
+
"wc",
|
|
1156
|
+
"grep",
|
|
1157
|
+
"rg",
|
|
1158
|
+
"ag",
|
|
1159
|
+
"tree",
|
|
1160
|
+
"pwd",
|
|
1161
|
+
"file",
|
|
1162
|
+
"stat",
|
|
1163
|
+
"du",
|
|
1164
|
+
"df",
|
|
1165
|
+
"sort",
|
|
1166
|
+
"uniq",
|
|
1167
|
+
"cut",
|
|
1168
|
+
"tr",
|
|
1169
|
+
"which",
|
|
1170
|
+
"echo",
|
|
1171
|
+
"basename",
|
|
1172
|
+
"dirname",
|
|
1173
|
+
"realpath",
|
|
1174
|
+
"diff",
|
|
1175
|
+
"cloc",
|
|
1176
|
+
"jq",
|
|
1177
|
+
"less",
|
|
1178
|
+
"nl",
|
|
1179
|
+
"column",
|
|
1180
|
+
"date",
|
|
1181
|
+
"uname",
|
|
1182
|
+
"true"
|
|
1183
|
+
]);
|
|
1184
|
+
var READ_ONLY_GIT = /* @__PURE__ */ new Set(["status", "log", "show", "diff", "blame", "branch", "ls-files", "ls-tree", "grep", "rev-parse", "describe", "shortlog", "tag", "cat-file", "reflog"]);
|
|
1185
|
+
function readOnlyCommand(command) {
|
|
1186
|
+
if (/[<>`]|\$\(|\$\{/.test(command)) return false;
|
|
1187
|
+
const segments2 = command.split(/\|\||&&|\||;/).map((s) => s.trim()).filter(Boolean);
|
|
1188
|
+
if (!segments2.length) return false;
|
|
1189
|
+
return segments2.every((seg) => {
|
|
1190
|
+
const words = seg.split(/\s+/);
|
|
1191
|
+
let i = 0;
|
|
1192
|
+
while (i < words.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[i])) i++;
|
|
1193
|
+
const program = words[i] ?? "";
|
|
1194
|
+
const rest = words.slice(i + 1);
|
|
1195
|
+
if (program === "git") {
|
|
1196
|
+
const sub = rest.find((w) => !w.startsWith("-"));
|
|
1197
|
+
if (!sub || !READ_ONLY_GIT.has(sub)) return false;
|
|
1198
|
+
if (sub === "branch" || sub === "tag") return rest.slice(rest.indexOf(sub) + 1).every((w) => /^(-a|-r|-v|-vv|-l|--list|--all|--show-current|--sort=\S+|--contains|--merged|--no-merged)$/.test(w));
|
|
1199
|
+
return !rest.some((w) => /^(--output|--open-files-in-pager|-O|--ext-diff)(=|$)/.test(w) || /^-O\S/.test(w));
|
|
1200
|
+
}
|
|
1201
|
+
if (program === "find") return !rest.some((w) => /^-(delete|exec|execdir|ok|okdir|fprint|fprint0|fprintf|fls)$/.test(w));
|
|
1202
|
+
if (program === "rg") return !rest.some((w) => /^--pre(=|$)/.test(w));
|
|
1203
|
+
if (program === "sort" || program === "tree") return !rest.some((w) => /^(-o|--output)/.test(w));
|
|
1204
|
+
if (program === "uniq") return rest.filter((w) => !w.startsWith("-")).length <= 1;
|
|
1205
|
+
return READ_ONLY_PROGRAMS.has(program);
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
async function decideToolUse(toolName, input, ctx) {
|
|
1209
|
+
if (toolName.startsWith("mcp__") && ctx.orgMcpTools?.all.has(toolName)) {
|
|
1210
|
+
if (ctx.plan && !ctx.orgMcpTools.readOnly.has(toolName)) return { behavior: "deny", message: PLAN_ONLY_READS };
|
|
1211
|
+
return { behavior: "allow", updatedInput: input };
|
|
782
1212
|
}
|
|
783
|
-
|
|
784
|
-
|
|
1213
|
+
if (toolName.startsWith("mcp__")) {
|
|
1214
|
+
if (!toolName.startsWith(SQ_MCP_PREFIX)) return { behavior: "deny", message: "Only ScaleQuality tools and the MCP servers your organization approved are available in this workspace." };
|
|
1215
|
+
if (ctx.plan && !SQ_READ_ONLY.has(toolName.slice(SQ_MCP_PREFIX.length))) return { behavior: "deny", message: PLAN_ONLY_READS };
|
|
1216
|
+
return { behavior: "allow", updatedInput: input };
|
|
785
1217
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
1218
|
+
if (HARMLESS.has(toolName)) return { behavior: "allow", updatedInput: input };
|
|
1219
|
+
if (SUBAGENT_TOOLS.has(toolName)) {
|
|
1220
|
+
if (typeof input.prompt !== "string" || !input.prompt.trim()) return { behavior: "deny", message: "A subagent needs a task (prompt)." };
|
|
1221
|
+
const updated = { ...input, run_in_background: false };
|
|
1222
|
+
for (const k of ["isolation", "model", "name", "team_name", "mode"]) delete updated[k];
|
|
1223
|
+
return { behavior: "allow", updatedInput: updated };
|
|
789
1224
|
}
|
|
790
|
-
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
return { model: boot.model, runtime: boot.runtime, reasoning: boot.reasoning ?? null };
|
|
1225
|
+
if (toolName === "WebFetch") {
|
|
1226
|
+
const why = await webFetchDenial(input.url, ctx.lookup);
|
|
1227
|
+
return why ? { behavior: "deny", message: why } : { behavior: "allow", updatedInput: input };
|
|
794
1228
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
accept: "application/json",
|
|
799
|
-
...this.opts.userAgent ? { "user-agent": this.opts.userAgent } : {},
|
|
800
|
-
...json ? { "content-type": "application/json" } : {}
|
|
801
|
-
};
|
|
1229
|
+
if (ctx.protections?.secretFiles && (toolName in READ_TOOLS || toolName in WRITE_TOOLS)) {
|
|
1230
|
+
const why = secretFileToolDenial(toolName, input);
|
|
1231
|
+
if (why) return { behavior: "deny", message: why, reason: "PROTECTION_SECRET_FILE" };
|
|
802
1232
|
}
|
|
803
|
-
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
if (
|
|
809
|
-
try {
|
|
810
|
-
return await this.request("GET", path, void 0, timeoutMs, outer);
|
|
811
|
-
} catch (e) {
|
|
812
|
-
last = e;
|
|
813
|
-
if (e instanceof SessionGoneError) throw e;
|
|
814
|
-
if (e instanceof TransportError && !e.retryable) throw e;
|
|
815
|
-
if (i < attempts - 1) await sleep(delay * 2 ** i, outer);
|
|
816
|
-
}
|
|
1233
|
+
if (toolName in WRITE_TOOLS && ctx.planDir) {
|
|
1234
|
+
const raw = input[WRITE_TOOLS[toolName]];
|
|
1235
|
+
if (typeof raw === "string" && raw.endsWith(".md")) {
|
|
1236
|
+
const plans = await realpathLoose(ctx.planDir);
|
|
1237
|
+
const abs = await realpathLoose((0, import_path3.resolve)(ctx.planDir, raw));
|
|
1238
|
+
if (inside(plans, abs) && abs !== plans) return { behavior: "allow", updatedInput: input };
|
|
817
1239
|
}
|
|
818
|
-
throw last;
|
|
819
|
-
}
|
|
820
|
-
post(path, body, timeoutMs) {
|
|
821
|
-
return this.request("POST", path, body, timeoutMs);
|
|
822
1240
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
const
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
method,
|
|
830
|
-
headers: this.headers(body !== void 0),
|
|
831
|
-
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
832
|
-
signal
|
|
833
|
-
});
|
|
834
|
-
} catch (e) {
|
|
835
|
-
throw new TransportError(`${method} ${routeLabel(path)} failed: ${e.name}`, null, true);
|
|
1241
|
+
if (ctx.plan && toolName in WRITE_TOOLS) return { behavior: "deny", message: PLAN_ONLY_READS };
|
|
1242
|
+
if (toolName in READ_TOOLS || toolName in WRITE_TOOLS) {
|
|
1243
|
+
const field = READ_TOOLS[toolName] ?? WRITE_TOOLS[toolName];
|
|
1244
|
+
const raw = input[field];
|
|
1245
|
+
if ((raw === void 0 || raw === null || raw === "") && toolName in READ_TOOLS && toolName !== "Read") {
|
|
1246
|
+
return { behavior: "allow", updatedInput: input };
|
|
836
1247
|
}
|
|
837
|
-
if (
|
|
838
|
-
|
|
839
|
-
|
|
1248
|
+
if (typeof raw !== "string" || raw.length === 0) return { behavior: "deny", message: `${toolName} needs a path inside the repository.` };
|
|
1249
|
+
const abs = await resolveInside(ctx.root, raw);
|
|
1250
|
+
let denied = false;
|
|
1251
|
+
for (const d of ctx.deniedRoots ?? []) {
|
|
1252
|
+
const realDenied = await (0, import_promises4.realpath)(d).catch(() => (0, import_path3.resolve)(d));
|
|
1253
|
+
if (abs && inside(realDenied, abs)) denied = true;
|
|
840
1254
|
}
|
|
841
|
-
if (!
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1255
|
+
if (abs && !denied) {
|
|
1256
|
+
if (toolName in WRITE_TOOLS) {
|
|
1257
|
+
const realRoot = await (0, import_promises4.realpath)(ctx.root).catch(() => (0, import_path3.resolve)(ctx.root));
|
|
1258
|
+
const rel = (0, import_path3.relative)(realRoot, abs).split(import_path3.sep);
|
|
1259
|
+
if (rel.includes(".git")) return { behavior: "deny", message: "Files under .git cannot be written from the workspace." };
|
|
1260
|
+
}
|
|
1261
|
+
return { behavior: "allow", updatedInput: input };
|
|
847
1262
|
}
|
|
848
|
-
if (
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
throw new TransportError(`${method} ${routeLabel(path)} returned invalid JSON`, res.status, false);
|
|
1263
|
+
if (toolName in READ_TOOLS) {
|
|
1264
|
+
for (const extra of ctx.extraReadRoots ?? []) {
|
|
1265
|
+
const e = await resolveInside(extra, raw);
|
|
1266
|
+
const realExtra = await (0, import_promises4.realpath)(extra).catch(() => (0, import_path3.resolve)(extra));
|
|
1267
|
+
if (e && inside(realExtra, e)) return { behavior: "allow", updatedInput: input };
|
|
1268
|
+
}
|
|
855
1269
|
}
|
|
1270
|
+
return { behavior: "deny", message: `${toolName} is limited to files inside the repository (${ctx.root}).` };
|
|
856
1271
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
if (
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1272
|
+
if (toolName === "Bash") {
|
|
1273
|
+
const command = typeof input.command === "string" ? input.command : "";
|
|
1274
|
+
if (!command.trim()) return { behavior: "deny", message: "Empty command." };
|
|
1275
|
+
const why = bashDenial(command, { local: ctx.local });
|
|
1276
|
+
if (why) return { behavior: "deny", message: why };
|
|
1277
|
+
const secret = ctx.protections?.secretFiles ? secretFileCommandDenial(command) : null;
|
|
1278
|
+
if (secret) return { behavior: "deny", message: secret, reason: "PROTECTION_SECRET_FILE" };
|
|
1279
|
+
if (ctx.plan && !readOnlyCommand(command)) return { behavior: "deny", message: PLAN_ONLY_READS };
|
|
1280
|
+
const updated = { ...input };
|
|
1281
|
+
delete updated.dangerouslyDisableSandbox;
|
|
1282
|
+
const t = typeof input.timeout === "number" && Number.isFinite(input.timeout) ? input.timeout : void 0;
|
|
1283
|
+
if (t !== void 0) updated.timeout = Math.max(1e3, Math.min(BASH_MAX_TIMEOUT_MS, t));
|
|
1284
|
+
return { behavior: "allow", updatedInput: updated };
|
|
868
1285
|
}
|
|
869
|
-
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
return
|
|
873
|
-
}
|
|
874
|
-
function sleep(ms, signal) {
|
|
875
|
-
return new Promise((resolve8) => {
|
|
876
|
-
const t = setTimeout(resolve8, ms);
|
|
877
|
-
signal?.addEventListener("abort", () => {
|
|
878
|
-
clearTimeout(t);
|
|
879
|
-
resolve8();
|
|
880
|
-
}, { once: true });
|
|
881
|
-
});
|
|
1286
|
+
if (toolName === "WebSearch") {
|
|
1287
|
+
return { behavior: "deny", message: "Web search is not available in the ScaleQuality workspace. Fetch a specific page with WebFetch." };
|
|
1288
|
+
}
|
|
1289
|
+
return { behavior: "deny", message: `${toolName} is not available in the ScaleQuality workspace.` };
|
|
882
1290
|
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
const
|
|
1291
|
+
|
|
1292
|
+
// src/application/services/workspaceSandbox/protections.ts
|
|
1293
|
+
var NO_PROTECTIONS = Object.freeze({ secretFiles: false, protectedPush: false, deployApproval: false, destructiveCommands: false, unknownPackage: null });
|
|
1294
|
+
var KNOWN_RULES = {
|
|
1295
|
+
"secret-files": ["block"],
|
|
1296
|
+
"protected-push": ["require"],
|
|
1297
|
+
"deploy-approval": ["require"],
|
|
1298
|
+
"destructive-commands": ["require"],
|
|
1299
|
+
"unknown-package": ["block", "warn"]
|
|
1300
|
+
};
|
|
1301
|
+
function protectionsWireOf(raw) {
|
|
1302
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
1303
|
+
const r = raw;
|
|
1304
|
+
if (!r.rules || typeof r.rules !== "object" || Array.isArray(r.rules)) return null;
|
|
1305
|
+
const rules = {};
|
|
1306
|
+
for (const [key, value] of Object.entries(r.rules)) {
|
|
1307
|
+
if (typeof value === "string" && KNOWN_RULES[key]?.includes(value)) rules[key] = value;
|
|
1308
|
+
}
|
|
1309
|
+
const version = typeof r.version === "number" && Number.isFinite(r.version) ? r.version : 0;
|
|
1310
|
+
return Object.keys(rules).length ? { version, rules } : null;
|
|
1311
|
+
}
|
|
1312
|
+
function parseProtections(raw) {
|
|
1313
|
+
const wire = protectionsWireOf(raw);
|
|
1314
|
+
if (!wire) return NO_PROTECTIONS;
|
|
1315
|
+
const r = wire.rules;
|
|
902
1316
|
return {
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
projectIds: Array.isArray(rawScope?.projectIds) ? rawScope.projectIds.filter((p) => typeof p === "string") : typeof session.projectId === "string" && session.projectId ? [session.projectId] : [],
|
|
909
|
-
repos: repositories
|
|
910
|
-
},
|
|
911
|
-
branch: text3(session.branch, repo2 ? defaultBranch : ""),
|
|
912
|
-
projectId: text3(session.projectId),
|
|
913
|
-
model: text3(session.model, "sq-auto"),
|
|
914
|
-
runtime: {
|
|
915
|
-
baseUrl: text3(runtime.baseUrl),
|
|
916
|
-
token: text3(runtime.token),
|
|
917
|
-
models: runtime.models,
|
|
918
|
-
primaryModel: typeof runtime.primaryModel === "string" ? runtime.primaryModel : null,
|
|
919
|
-
fastModel: typeof runtime.fastModel === "string" ? runtime.fastModel : null,
|
|
920
|
-
maxOutputTokens: typeof runtime.maxOutputTokens === "number" ? runtime.maxOutputTokens : null,
|
|
921
|
-
maxOutputTokensCeiling: typeof runtime.maxOutputTokensCeiling === "number" ? runtime.maxOutputTokensCeiling : null,
|
|
922
|
-
reasoning: parseReasoningCapability(runtime.reasoning),
|
|
923
|
-
aliases: Array.isArray(runtime.aliases) ? runtime.aliases.filter((a) => typeof a?.alias === "string").map(credentialAlias) : void 0
|
|
924
|
-
},
|
|
925
|
-
reasoning: isReasoningLevel(session.reasoning) ? session.reasoning : null,
|
|
926
|
-
imported: importedInfo(session.imported),
|
|
927
|
-
checkpointPatch: typeof raw?.checkpointPatch === "string" ? raw.checkpointPatch : null,
|
|
928
|
-
sdkSessionId: typeof session.sdkSessionId === "string" ? session.sdkSessionId : null,
|
|
929
|
-
workspaceKind: session.workspaceKind === "LOCAL" ? "LOCAL" : "CLOUD",
|
|
930
|
-
projectName: typeof session.projectName === "string" ? session.projectName : typeof raw?.project?.name === "string" ? raw.project.name : null,
|
|
931
|
-
folderLink: typeof session.folderLink?.projectId === "string" && session.folderLink.projectId ? { projectId: session.folderLink.projectId } : null,
|
|
932
|
-
transcript: transcriptOf(raw?.transcript),
|
|
933
|
-
baseMeasurements: raw?.baseMeasurements && typeof raw.baseMeasurements === "object" && !Array.isArray(raw.baseMeasurements) ? raw.baseMeasurements : {},
|
|
934
|
-
pullRequests: pullRequestsOf(raw?.pullRequests),
|
|
935
|
-
skills: skillsOf(raw?.skills),
|
|
936
|
-
plugins: pluginsOf(raw?.plugins),
|
|
937
|
-
environments: environmentsOf(raw?.environments)
|
|
1317
|
+
secretFiles: r["secret-files"] === "block",
|
|
1318
|
+
protectedPush: r["protected-push"] === "require",
|
|
1319
|
+
deployApproval: r["deploy-approval"] === "require",
|
|
1320
|
+
destructiveCommands: r["destructive-commands"] === "require",
|
|
1321
|
+
unknownPackage: r["unknown-package"] === "block" || r["unknown-package"] === "warn" ? r["unknown-package"] : null
|
|
938
1322
|
};
|
|
939
1323
|
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1324
|
+
var SECRET_FILE_MESSAGE = "Blocked by your organization's protection: secret files are never sent to the model. Do not try to read this file another way; ask the user for the specific value your task needs, or use the example file (such as .env.example).";
|
|
1325
|
+
var ENV_TEMPLATES = /* @__PURE__ */ new Set(["example", "sample", "template", "dist"]);
|
|
1326
|
+
var SECRET_EXTENSIONS = /\.(pem|key|p12|pfx|jks|keystore)$/;
|
|
1327
|
+
var SECRET_NAMES = /* @__PURE__ */ new Set([".npmrc", ".pypirc", ".netrc", ".git-credentials", "credentials.json", "secrets.yml", "secrets.yaml", "terraform.tfvars"]);
|
|
1328
|
+
function isSecretPath(path) {
|
|
1329
|
+
const clean = path.trim().replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
1330
|
+
if (!clean) return false;
|
|
1331
|
+
const parts = clean.split("/");
|
|
1332
|
+
const name = parts[parts.length - 1];
|
|
1333
|
+
if (name === ".env") return true;
|
|
1334
|
+
if (name.startsWith(".env.")) return !name.slice(5).split(".").some((part) => ENV_TEMPLATES.has(part));
|
|
1335
|
+
if (SECRET_EXTENSIONS.test(name)) return true;
|
|
1336
|
+
if (/^id_(rsa|dsa|ecdsa|ed25519)/.test(name) && !name.endsWith(".pub")) return true;
|
|
1337
|
+
if (SECRET_NAMES.has(name) || name.endsWith(".auto.tfvars")) return true;
|
|
1338
|
+
if (/^service-account.*\.json$/.test(name)) return true;
|
|
1339
|
+
return name === "credentials" && parts[parts.length - 2] === ".aws";
|
|
1340
|
+
}
|
|
1341
|
+
var SECRET_SAMPLES = [
|
|
1342
|
+
".env",
|
|
1343
|
+
".env.local",
|
|
1344
|
+
".env.production",
|
|
1345
|
+
"server.pem",
|
|
1346
|
+
"tls.key",
|
|
1347
|
+
"cert.p12",
|
|
1348
|
+
"cert.pfx",
|
|
1349
|
+
"release.jks",
|
|
1350
|
+
"app.keystore",
|
|
1351
|
+
"id_rsa",
|
|
1352
|
+
"id_ed25519",
|
|
1353
|
+
".npmrc",
|
|
1354
|
+
".pypirc",
|
|
1355
|
+
".netrc",
|
|
1356
|
+
".git-credentials",
|
|
1357
|
+
"credentials.json",
|
|
1358
|
+
"service-account.json",
|
|
1359
|
+
"service-account-prod.json",
|
|
1360
|
+
"prod.auto.tfvars",
|
|
1361
|
+
"terraform.tfvars",
|
|
1362
|
+
".aws/credentials",
|
|
1363
|
+
"secrets.yml",
|
|
1364
|
+
"secrets.yaml"
|
|
1365
|
+
];
|
|
1366
|
+
var ORDINARY_SAMPLES = ["package.json", "index.ts", "main.py", "README.md", "app.yml", "app.yaml", "main.tf", "Main.java", "config"];
|
|
1367
|
+
function globRegex(glob) {
|
|
1368
|
+
let re = "";
|
|
1369
|
+
for (let i = 0; i < glob.length; i++) {
|
|
1370
|
+
const c = glob[i];
|
|
1371
|
+
if (c === "*") {
|
|
1372
|
+
if (glob[i + 1] === "*") {
|
|
1373
|
+
re += ".*";
|
|
1374
|
+
i++;
|
|
1375
|
+
if (glob[i + 1] === "/") i++;
|
|
1376
|
+
} else re += "[^/]*";
|
|
1377
|
+
} else if (c === "?") re += "[^/]";
|
|
1378
|
+
else if (c === "{") re += "(";
|
|
1379
|
+
else if (c === "}") re += ")";
|
|
1380
|
+
else if (c === ",") re += "|";
|
|
1381
|
+
else re += c.replace(/[.+^$()|[\]\\]/g, "\\$&");
|
|
1382
|
+
}
|
|
1383
|
+
return new RegExp(`^${re}$`, "i");
|
|
1384
|
+
}
|
|
1385
|
+
function globTargetsSecrets(glob) {
|
|
1386
|
+
const patterns = glob.split(/\s+/).filter(Boolean).filter((g) => !g.startsWith("!"));
|
|
1387
|
+
return patterns.some((g) => {
|
|
1388
|
+
let re;
|
|
1389
|
+
try {
|
|
1390
|
+
re = globRegex(g.replace(/^\.\//, ""));
|
|
1391
|
+
} catch {
|
|
1392
|
+
return false;
|
|
1393
|
+
}
|
|
1394
|
+
const test = (name) => re.test(name) || !g.includes("/") && re.test(name.split("/").pop());
|
|
1395
|
+
return SECRET_SAMPLES.some(test) && !ORDINARY_SAMPLES.some(test);
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
var SECRET_READ_DENY_RULES = [
|
|
1399
|
+
...[
|
|
1400
|
+
".env",
|
|
1401
|
+
".env.local",
|
|
1402
|
+
".env.*.local",
|
|
1403
|
+
".env.development",
|
|
1404
|
+
".env.dev",
|
|
1405
|
+
".env.production",
|
|
1406
|
+
".env.prod",
|
|
1407
|
+
".env.staging",
|
|
1408
|
+
".env.stage",
|
|
1409
|
+
".env.test",
|
|
1410
|
+
".env.testing",
|
|
1411
|
+
".env.qa",
|
|
1412
|
+
".env.uat",
|
|
1413
|
+
".env.preview",
|
|
1414
|
+
".env.ci",
|
|
1415
|
+
".env.docker",
|
|
1416
|
+
".env.vault",
|
|
1417
|
+
".env.keys",
|
|
1418
|
+
".env.backup",
|
|
1419
|
+
".env.bak",
|
|
1420
|
+
".env.old",
|
|
1421
|
+
".env.secret",
|
|
1422
|
+
".env.secrets",
|
|
1423
|
+
".env.override",
|
|
1424
|
+
"*.pem",
|
|
1425
|
+
"*.key",
|
|
1426
|
+
"*.p12",
|
|
1427
|
+
"*.pfx",
|
|
1428
|
+
"*.jks",
|
|
1429
|
+
"*.keystore",
|
|
1430
|
+
"id_rsa",
|
|
1431
|
+
"id_rsa_*",
|
|
1432
|
+
"id_dsa",
|
|
1433
|
+
"id_dsa_*",
|
|
1434
|
+
"id_ecdsa",
|
|
1435
|
+
"id_ecdsa_*",
|
|
1436
|
+
"id_ed25519",
|
|
1437
|
+
"id_ed25519_*",
|
|
1438
|
+
".npmrc",
|
|
1439
|
+
".pypirc",
|
|
1440
|
+
".netrc",
|
|
1441
|
+
".git-credentials",
|
|
1442
|
+
"credentials.json",
|
|
1443
|
+
"service-account*.json",
|
|
1444
|
+
"*.auto.tfvars",
|
|
1445
|
+
"terraform.tfvars",
|
|
1446
|
+
".aws/credentials",
|
|
1447
|
+
"secrets.yml",
|
|
1448
|
+
"secrets.yaml"
|
|
1449
|
+
].map((p) => `Read(**/${p})`)
|
|
1450
|
+
];
|
|
1451
|
+
var FILE_TOOL_PATHS = {
|
|
1452
|
+
Read: ["file_path"],
|
|
1453
|
+
Write: ["file_path"],
|
|
1454
|
+
Edit: ["file_path"],
|
|
1455
|
+
MultiEdit: ["file_path"],
|
|
1456
|
+
NotebookEdit: ["notebook_path"],
|
|
1457
|
+
Grep: ["path"],
|
|
1458
|
+
Glob: ["path"],
|
|
1459
|
+
LS: ["path"]
|
|
1460
|
+
};
|
|
1461
|
+
function secretFileToolDenial(toolName, input) {
|
|
1462
|
+
for (const field of FILE_TOOL_PATHS[toolName] ?? []) {
|
|
1463
|
+
const v = input[field];
|
|
1464
|
+
if (typeof v === "string" && isSecretPath(v)) return SECRET_FILE_MESSAGE;
|
|
1465
|
+
}
|
|
1466
|
+
if (toolName === "Grep" && typeof input.glob === "string" && globTargetsSecrets(input.glob)) return SECRET_FILE_MESSAGE;
|
|
1467
|
+
return null;
|
|
944
1468
|
}
|
|
945
|
-
var
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1469
|
+
var HARMLESS_WITH_SECRET = /* @__PURE__ */ new Set(["ls", "stat", "test", "[", "[[", "file", "wc", "rm", "touch", "echo", "printf", "chmod", "mkdir", "realpath", "basename", "dirname", "du", "true", "cd", "pwd"]);
|
|
1470
|
+
var HARMLESS_GIT_WITH_SECRET = /* @__PURE__ */ new Set(["status", "add", "rm", "check-ignore", "ls-files", "update-index"]);
|
|
1471
|
+
var RECURSIVE_SEARCH = /(^|\s)(grep|egrep|fgrep)\s(.*\s)?(-[a-z]*r[a-z]*|--recursive|--dereference-recursive)(\s|$)|(^|\s)(rg|ag|ack|ack-grep)(\s|$)|(^|\s)git\s+(-c\s+\S+\s+)*grep(\s|$)/;
|
|
1472
|
+
function commandSegments(command) {
|
|
1473
|
+
return command.replace(/\\\n/g, " ").split(/\|\||&&|[|;&\n\r`]|\$\(|<\(|>\(|[()]/).map((s) => s.trim()).filter(Boolean);
|
|
949
1474
|
}
|
|
950
|
-
function
|
|
951
|
-
|
|
952
|
-
|
|
1475
|
+
function programWords(segment) {
|
|
1476
|
+
const words = segment.split(/\s+/).filter(Boolean);
|
|
1477
|
+
let i = 0;
|
|
1478
|
+
for (; ; ) {
|
|
1479
|
+
const w = words[i];
|
|
1480
|
+
if (w === void 0) break;
|
|
1481
|
+
if (/^[a-z_][a-z0-9_]*=/i.test(w)) {
|
|
1482
|
+
i++;
|
|
1483
|
+
continue;
|
|
1484
|
+
}
|
|
1485
|
+
if (["sudo", "doas", "command", "builtin", "exec", "nohup", "time", "nice", "env", "xargs"].includes(w.replace(/^.*\//, ""))) {
|
|
1486
|
+
i++;
|
|
1487
|
+
while (words[i]?.startsWith("-")) i++;
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
break;
|
|
1491
|
+
}
|
|
1492
|
+
return words.slice(i);
|
|
953
1493
|
}
|
|
954
|
-
function
|
|
955
|
-
|
|
956
|
-
return raw.filter((s) => !!s && typeof s === "object").filter((s) => typeof s.name === "string" && typeof s.body === "string").slice(0, 200).map((s) => ({
|
|
957
|
-
name: s.name,
|
|
958
|
-
description: typeof s.description === "string" ? s.description : "",
|
|
959
|
-
body: s.body,
|
|
960
|
-
argumentHint: typeof s.argumentHint === "string" ? s.argumentHint : null
|
|
961
|
-
}));
|
|
1494
|
+
function pathPieces(word) {
|
|
1495
|
+
return word.replace(/^[<>]+/, "").split(/[=:@,]/).map((p) => p.replace(/^[<>]+/, "")).filter(Boolean);
|
|
962
1496
|
}
|
|
963
|
-
function
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1497
|
+
function secretFileCommandDenial(command) {
|
|
1498
|
+
const n = normalizeCommand(command);
|
|
1499
|
+
const segments2 = commandSegments(n);
|
|
1500
|
+
const words = n.split(/[\s|;&()`]+/).filter(Boolean);
|
|
1501
|
+
const namesSecret = words.some((w) => pathPieces(w).some(isSecretPath)) || /(^|\s)(source|\.)\s+\S*\.env(\s|$)/.test(n);
|
|
1502
|
+
if (namesSecret) {
|
|
1503
|
+
const onlyHarmless = segments2.every((seg) => {
|
|
1504
|
+
const w = programWords(seg);
|
|
1505
|
+
const p = (w[0] ?? "").replace(/^.*\//, "");
|
|
1506
|
+
if (p === "git") {
|
|
1507
|
+
const sub = w.slice(1).find((x) => !x.startsWith("-"));
|
|
1508
|
+
return !!sub && HARMLESS_GIT_WITH_SECRET.has(sub);
|
|
1509
|
+
}
|
|
1510
|
+
if (p === "cp" || p === "mv" || p === "install") {
|
|
1511
|
+
const args = w.slice(1).filter((x) => !x.startsWith("-"));
|
|
1512
|
+
return args.length >= 2 && !args.slice(0, -1).some((a) => pathPieces(a).some(isSecretPath));
|
|
1513
|
+
}
|
|
1514
|
+
return HARMLESS_WITH_SECRET.has(p);
|
|
1515
|
+
});
|
|
1516
|
+
if (!onlyHarmless || /<\s*[^<\s]/.test(n.replace(/<<-?\s*\S+/g, ""))) return SECRET_FILE_MESSAGE;
|
|
1517
|
+
}
|
|
1518
|
+
const pipelineStarts = n.replace(/\\\n/g, " ").split(/&&|\|\||[;&\n\r`]|\$\(|<\(|>\(|[()]/).map((c) => c.split("|")[0].trim()).filter(Boolean);
|
|
1519
|
+
if (pipelineStarts.some((seg) => RECURSIVE_SEARCH.test(programWords(seg).join(" ")))) {
|
|
1520
|
+
return "Blocked by your organization's protection: secret files are never sent to the model, and a recursive search from a command would read them. Use the Grep tool for this search (it skips secret files), or search specific files by name.";
|
|
1521
|
+
}
|
|
1522
|
+
return null;
|
|
1523
|
+
}
|
|
1524
|
+
var RECURSIVE_FLAG = /(^|\s)(-[a-z]*r[a-z]*|--recursive)(\s|$)/;
|
|
1525
|
+
var DESTRUCTIVE_SEGMENT = [
|
|
1526
|
+
(w) => w[0] === "rm" && RECURSIVE_FLAG.test(w.slice(1).join(" ")),
|
|
1527
|
+
(w, seg) => w[0] === "find" && /\s-delete(\s|$)/.test(seg),
|
|
1528
|
+
(w, seg) => w[0] === "find" && /\s-exec(dir)?\s+(\S*\/)?rm\s/.test(seg),
|
|
1529
|
+
(w, seg) => w[0] === "git" && /(^|\s)reset\s+(.*\s)?--hard(\s|$)/.test(seg),
|
|
1530
|
+
(w, seg) => w[0] === "git" && /(^|\s)clean\s+(.*\s)?(-[a-z]*f[a-z]*|--force)(\s|$)/.test(seg),
|
|
1531
|
+
(w, seg) => w[0] === "git" && /(^|\s)checkout\s+(.*\s)?(--\s+)?(\.|:\/|\*)(\s|$)/.test(seg),
|
|
1532
|
+
(w, seg) => w[0] === "git" && /(^|\s)restore\s+(.*\s)?(\.|:\/|\*)(\s|$)/.test(seg),
|
|
1533
|
+
(w) => w[0] === "dd",
|
|
1534
|
+
(w) => !!w[0]?.startsWith("mkfs"),
|
|
1535
|
+
(w) => w[0] === "shred",
|
|
1536
|
+
(w, seg) => w[0] === "chmod" && RECURSIVE_FLAG.test(seg) && /(^|\s)(0?777|a\+rwx|ugo\+rwx)(\s|$)/.test(seg),
|
|
1537
|
+
(w, seg) => w[0] === "chown" && RECURSIVE_FLAG.test(seg),
|
|
1538
|
+
(w) => w[0] === "dropdb"
|
|
1539
|
+
];
|
|
1540
|
+
var DOWNLOAD_TO_SHELL = [
|
|
1541
|
+
/\b(curl|wget|fetch)\b[^|;\n]*\|\s*(sudo\s+(-\S+\s+)*)?(env\s+)?(\S*\/)?(sh|bash|zsh|dash|ksh|fish|python3?|perl|ruby|node)(\s|$)/,
|
|
1542
|
+
/\b(sh|bash|zsh|dash|ksh|fish|source|\.)\s+<\(\s*(curl|wget)\b/,
|
|
1543
|
+
/\b(sh|bash|zsh|dash|ksh|fish)\s+-c\s+\$\(\s*(curl|wget)\b/,
|
|
1544
|
+
/\beval\s+\$\(\s*(curl|wget)\b/
|
|
1545
|
+
];
|
|
1546
|
+
function isDestructiveCommand(command) {
|
|
1547
|
+
const n = normalizeCommand(command);
|
|
1548
|
+
if (DOWNLOAD_TO_SHELL.some((re) => re.test(n))) return true;
|
|
1549
|
+
if (/\bdrop\s+(database|table|schema)\b/.test(n)) return true;
|
|
1550
|
+
if (/(^|[\s;&|(])(psql|mysql|mariadb|sqlite3|clickhouse-client|cockroach)\b.*\btruncate\b/.test(n)) return true;
|
|
1551
|
+
return commandSegments(n).some((seg) => {
|
|
1552
|
+
const words = programWords(seg).map((w, i) => i === 0 ? w.replace(/^.*\//, "") : w);
|
|
1553
|
+
return DESTRUCTIVE_SEGMENT.some((test) => test(words, words.join(" ")));
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
var DEPLOY = [
|
|
1557
|
+
/\b(npm|pnpm|yarn|bun)\s+(.*\s)?publish(\s|$)/,
|
|
1558
|
+
/\bcargo\s+publish\b/,
|
|
1559
|
+
/\bgem\s+push\b/,
|
|
1560
|
+
/\bpoetry\s+publish\b/,
|
|
1561
|
+
/\btwine\s+upload\b/,
|
|
1562
|
+
/\b(docker|podman)\s+(image\s+)?push\b/,
|
|
1563
|
+
/\b(terraform|tofu)\s+(.*\s)?(apply|destroy)\b/,
|
|
1564
|
+
/\bpulumi\s+(.*\s)?(up|destroy|update)\b/,
|
|
1565
|
+
/\bcdk\s+(.*\s)?(deploy|destroy)\b/,
|
|
1566
|
+
/\b(serverless|sls)\s+(.*\s)?deploy\b/,
|
|
1567
|
+
/\bkubectl\s+(.*\s)?(apply|delete|rollout)\b/,
|
|
1568
|
+
/\bhelm\s+(.*\s)?(install|upgrade|uninstall)\b/,
|
|
1569
|
+
/(^|\s)vercel(\s+.*)?\s(--prod|deploy)\b/,
|
|
1570
|
+
/\bnetlify\s+(.*\s)?deploy\b/,
|
|
1571
|
+
/\b(fly|flyctl)\s+(.*\s)?deploy\b/,
|
|
1572
|
+
/\bwrangler\s+(.*\s)?(deploy|publish)\b/,
|
|
1573
|
+
/\bgcloud\s+(.*\s)?deploy\b/,
|
|
1574
|
+
/\baz\s+(.*\s)?(deploy|deployment|webapp\s+up)\b/,
|
|
1575
|
+
/\baws\s+(.*\s)?cloudformation\s+(.*\s)?deploy\b/,
|
|
1576
|
+
/\baws\s+(.*\s)?ecs\s+(.*\s)?update-service\b/,
|
|
1577
|
+
/\baws\s+(.*\s)?lambda\s+(.*\s)?update-function-code\b/,
|
|
1578
|
+
/\baws\s+(.*\s)?s3\s+sync\s+(.*\s)?\S+\s+s3:\/\/\S+(\s+-.*)?$/,
|
|
1579
|
+
/\bgh\s+release\s+create\b/,
|
|
1580
|
+
/\beas\s+submit\b/,
|
|
1581
|
+
/\bmvn\b.*\bdeploy\b/,
|
|
1582
|
+
/\bgradlew?\b.*\bpublish/,
|
|
1583
|
+
/\bdotnet\s+nuget\s+push\b/,
|
|
1584
|
+
/\bfirebase\s+(.*\s)?deploy\b/,
|
|
1585
|
+
/\beb\s+deploy\b/
|
|
1586
|
+
];
|
|
1587
|
+
function isDeployCommand(command) {
|
|
1588
|
+
return commandSegments(normalizeCommand(command)).some((seg) => {
|
|
1589
|
+
const words = programWords(seg);
|
|
1590
|
+
const text3 = words.join(" ");
|
|
1591
|
+
return DEPLOY.some((re) => re.test(text3));
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
var PROTECTED_BRANCH = /^(main|master|production|prod|develop|development|release\/.+)$/;
|
|
1595
|
+
function isProtectedBranch(branch2) {
|
|
1596
|
+
return PROTECTED_BRANCH.test(branch2.replace(/^refs\/heads\//, "").toLowerCase());
|
|
1597
|
+
}
|
|
1598
|
+
var PUSH_VALUE_FLAGS = /* @__PURE__ */ new Set(["-o", "--push-option", "--repo", "--receive-pack", "--exec", "--signed"]);
|
|
1599
|
+
function gitPushes(command) {
|
|
1600
|
+
const out2 = [];
|
|
1601
|
+
for (const seg of commandSegments(normalizeCommand(command))) {
|
|
1602
|
+
const words = programWords(seg);
|
|
1603
|
+
if ((words[0] ?? "").replace(/^.*\//, "") !== "git") continue;
|
|
1604
|
+
let i = 1;
|
|
1605
|
+
while (i < words.length && words[i].startsWith("-")) i += words[i] === "-C" || words[i] === "-c" ? 2 : 1;
|
|
1606
|
+
if (words[i] !== "push") continue;
|
|
1607
|
+
const t = { branches: [], current: false, force: false, all: false };
|
|
1608
|
+
let tagsOnly = false;
|
|
1609
|
+
const positional = [];
|
|
1610
|
+
for (let j = i + 1; j < words.length; j++) {
|
|
1611
|
+
const w = words[j];
|
|
1612
|
+
if (w === "--") {
|
|
1613
|
+
positional.push(...words.slice(j + 1));
|
|
1614
|
+
break;
|
|
1615
|
+
}
|
|
1616
|
+
if (PUSH_VALUE_FLAGS.has(w)) {
|
|
1617
|
+
j++;
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1620
|
+
if (/^--force(-with-lease|-if-includes)?(=|$)/.test(w) || w === "--mirror") t.force = true;
|
|
1621
|
+
if (w === "--all" || w === "--mirror" || w === "--branches") t.all = true;
|
|
1622
|
+
if (w === "--tags") tagsOnly = true;
|
|
1623
|
+
if (/^-[a-z]+$/.test(w) && !w.startsWith("--") && w.includes("f")) t.force = true;
|
|
1624
|
+
if (w.startsWith("-")) continue;
|
|
1625
|
+
positional.push(w);
|
|
1626
|
+
}
|
|
1627
|
+
const refspecs = positional.slice(1);
|
|
1628
|
+
if (!refspecs.length && !tagsOnly) t.current = true;
|
|
1629
|
+
for (const spec of refspecs) {
|
|
1630
|
+
if (spec.startsWith("+")) t.force = true;
|
|
1631
|
+
const s = spec.replace(/^\+/, "");
|
|
1632
|
+
const dst = s.includes(":") ? s.slice(s.indexOf(":") + 1) : s;
|
|
1633
|
+
const name = dst.replace(/^refs\/heads\//, "");
|
|
1634
|
+
if (!name) continue;
|
|
1635
|
+
if (name === "head" || name === "@") t.current = true;
|
|
1636
|
+
else t.branches.push(name);
|
|
1637
|
+
}
|
|
1638
|
+
out2.push(t);
|
|
974
1639
|
}
|
|
975
1640
|
return out2;
|
|
976
1641
|
}
|
|
977
|
-
function
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1642
|
+
async function isProtectedPush(command, currentBranch) {
|
|
1643
|
+
const pushes = gitPushes(command);
|
|
1644
|
+
if (!pushes.length) return false;
|
|
1645
|
+
let current;
|
|
1646
|
+
for (const p of pushes) {
|
|
1647
|
+
if (p.force || p.all || p.branches.some(isProtectedBranch)) return true;
|
|
1648
|
+
if (p.current) {
|
|
1649
|
+
if (current === void 0) current = await currentBranch().catch(() => null);
|
|
1650
|
+
if (!current || current === "head" || current === "HEAD" || isProtectedBranch(current)) return true;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
return false;
|
|
989
1654
|
}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1655
|
+
var NPM_NAME = /^(@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
|
|
1656
|
+
var PYPI_NAME = /^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/;
|
|
1657
|
+
var CRATE_NAME = /^[a-z0-9][a-z0-9_-]*$/;
|
|
1658
|
+
var NPM_VALUE_FLAGS = /* @__PURE__ */ new Set(["--registry", "--prefix", "-w", "--workspace", "--tag", "--omit", "--include", "--cache", "--userconfig", "-c", "--filter", "-F", "--dir", "-C", "--cwd", "-p", "--package"]);
|
|
1659
|
+
var PIP_VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
1660
|
+
"-r",
|
|
1661
|
+
"--requirement",
|
|
1662
|
+
"-c",
|
|
1663
|
+
"--constraint",
|
|
1664
|
+
"-e",
|
|
1665
|
+
"--editable",
|
|
1666
|
+
"-i",
|
|
1667
|
+
"--index-url",
|
|
1668
|
+
"--extra-index-url",
|
|
1669
|
+
"-f",
|
|
1670
|
+
"--find-links",
|
|
1671
|
+
"-t",
|
|
1672
|
+
"--target",
|
|
1673
|
+
"--prefix",
|
|
1674
|
+
"--root",
|
|
1675
|
+
"--python",
|
|
1676
|
+
"-p",
|
|
1677
|
+
"--group",
|
|
1678
|
+
"--optional",
|
|
1679
|
+
"-G",
|
|
1680
|
+
"--source",
|
|
1681
|
+
"--platform",
|
|
1682
|
+
"--python-version",
|
|
1683
|
+
"--implementation",
|
|
1684
|
+
"--abi",
|
|
1685
|
+
"--trusted-host",
|
|
1686
|
+
"--cert",
|
|
1687
|
+
"--client-cert",
|
|
1688
|
+
"--cache-dir",
|
|
1689
|
+
"--src",
|
|
1690
|
+
"--upgrade-strategy",
|
|
1691
|
+
"--progress-bar",
|
|
1692
|
+
"--log",
|
|
1693
|
+
"--timeout",
|
|
1694
|
+
"--retries",
|
|
1695
|
+
"--proxy",
|
|
1696
|
+
"-E",
|
|
1697
|
+
"--extras",
|
|
1698
|
+
"--with",
|
|
1699
|
+
"--dev-group"
|
|
1700
|
+
]);
|
|
1701
|
+
var CARGO_VALUE_FLAGS = /* @__PURE__ */ new Set(["-F", "--features", "--rename", "-p", "--package", "--registry", "--manifest-path", "--target", "--branch", "--tag", "--rev"]);
|
|
1702
|
+
function notRegistryTarget(t) {
|
|
1703
|
+
return /^(\.|\/|~|file:|link:|workspace:|portal:|patch:|git[+:]|github:|gitlab:|bitbucket:|https?:|ssh:)/.test(t) || /\.(tgz|tar\.gz|tar|zip|whl)$/.test(t) || t.includes("://");
|
|
1704
|
+
}
|
|
1705
|
+
function npmName(target) {
|
|
1706
|
+
if (notRegistryTarget(target)) return null;
|
|
1707
|
+
const alias = /^[^@]+@npm:(.+)$/.exec(target) ?? /^@[^/]+\/[^@]+@npm:(.+)$/.exec(target);
|
|
1708
|
+
const t = alias ? alias[1] : target;
|
|
1709
|
+
const scoped = t.startsWith("@");
|
|
1710
|
+
const at = t.indexOf("@", scoped ? 1 : 0);
|
|
1711
|
+
const name = at > 0 ? t.slice(0, at) : t;
|
|
1712
|
+
if (!scoped && name.includes("/")) return null;
|
|
1713
|
+
return NPM_NAME.test(name) ? name : null;
|
|
1714
|
+
}
|
|
1715
|
+
function pypiName(target) {
|
|
1716
|
+
if (notRegistryTarget(target) || target.includes(" @ ")) return null;
|
|
1717
|
+
const name = target.split(/[\[<>=!~;@\s]/)[0];
|
|
1718
|
+
return PYPI_NAME.test(name) ? name : null;
|
|
1719
|
+
}
|
|
1720
|
+
function crateName(target) {
|
|
1721
|
+
const name = target.split("@")[0];
|
|
1722
|
+
return CRATE_NAME.test(name) ? name : null;
|
|
1723
|
+
}
|
|
1724
|
+
function targets(words, valueFlags) {
|
|
1725
|
+
const out2 = [];
|
|
1726
|
+
for (let i = 0; i < words.length; i++) {
|
|
1727
|
+
const w = words[i];
|
|
1728
|
+
if (valueFlags.has(w)) {
|
|
1729
|
+
i++;
|
|
1730
|
+
continue;
|
|
1731
|
+
}
|
|
1732
|
+
if (w.startsWith("-")) continue;
|
|
1733
|
+
out2.push(w);
|
|
1734
|
+
}
|
|
1735
|
+
return out2;
|
|
998
1736
|
}
|
|
999
|
-
function
|
|
1000
|
-
const
|
|
1001
|
-
const
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1737
|
+
function packageInstalls(command, opts = {}) {
|
|
1738
|
+
const out2 = [];
|
|
1739
|
+
const add = (ecosystem, name) => {
|
|
1740
|
+
if (name && !out2.some((p) => p.ecosystem === ecosystem && p.name === name)) out2.push({ ecosystem, name });
|
|
1741
|
+
};
|
|
1742
|
+
for (const seg of commandSegments(normalizeCommand(command))) {
|
|
1743
|
+
let w = programWords(seg);
|
|
1744
|
+
const p = (w[0] ?? "").replace(/^.*\//, "");
|
|
1745
|
+
const rest = w.slice(1);
|
|
1746
|
+
const otherRegistry = rest.some((x) => /^(--registry|--index-url|-i)(=|$)/.test(x));
|
|
1747
|
+
if (otherRegistry) continue;
|
|
1748
|
+
if (["npm", "pnpm", "yarn", "bun"].includes(p)) {
|
|
1749
|
+
const args = targets(rest, NPM_VALUE_FLAGS);
|
|
1750
|
+
let k = 0;
|
|
1751
|
+
if (args[0] === "global") k = 1;
|
|
1752
|
+
const sub = args[k];
|
|
1753
|
+
const installs = sub === "add" || (sub === "install" || sub === "i" || sub === "in") && p !== "yarn";
|
|
1754
|
+
if (installs) for (const t of args.slice(k + 1)) add("npm", npmName(t));
|
|
1755
|
+
if ((p === "pnpm" || p === "yarn") && sub === "dlx" && args[k + 1]) add("npm", npmName(args[k + 1]));
|
|
1756
|
+
if (p === "npm" && (sub === "exec" || sub === "x") && args[k + 1]) {
|
|
1757
|
+
const name = npmName(args[k + 1]);
|
|
1758
|
+
if (name && !opts.installedLocally?.(name)) add("npm", name);
|
|
1759
|
+
}
|
|
1760
|
+
continue;
|
|
1761
|
+
}
|
|
1762
|
+
if (p === "npx" || p === "bunx" || p === "pnpx") {
|
|
1763
|
+
const pkgFlag = rest.findIndex((x) => x === "-p" || x === "--package");
|
|
1764
|
+
if (pkgFlag >= 0 && rest[pkgFlag + 1]) {
|
|
1765
|
+
add("npm", npmName(rest[pkgFlag + 1]));
|
|
1766
|
+
continue;
|
|
1767
|
+
}
|
|
1768
|
+
const first = targets(rest, /* @__PURE__ */ new Set(["--registry", "--cache", "--prefix"]))[0];
|
|
1769
|
+
const name = first ? npmName(first) : null;
|
|
1770
|
+
if (name && !opts.installedLocally?.(name)) add("npm", name);
|
|
1771
|
+
continue;
|
|
1772
|
+
}
|
|
1773
|
+
if (/^python[0-9.]*$/.test(p) && rest[0] === "-m" && /^pip3?$/.test(rest[1] ?? "")) w = [rest[1], ...rest.slice(2)];
|
|
1774
|
+
else if (p === "uv" && rest[0] === "pip") w = ["pip", ...rest.slice(1)];
|
|
1775
|
+
const q = (w[0] ?? "").replace(/^.*\//, "");
|
|
1776
|
+
const r = w.slice(1);
|
|
1777
|
+
if (/^pip3?(\.\d+)?$/.test(q) && r[0] === "install") {
|
|
1778
|
+
for (const t of targets(r.slice(1), PIP_VALUE_FLAGS)) add("pypi", pypiName(t));
|
|
1779
|
+
continue;
|
|
1780
|
+
}
|
|
1781
|
+
if ((q === "uv" || q === "poetry") && r[0] === "add") {
|
|
1782
|
+
for (const t of targets(r.slice(1), PIP_VALUE_FLAGS)) add("pypi", pypiName(t));
|
|
1783
|
+
continue;
|
|
1784
|
+
}
|
|
1785
|
+
if (q === "cargo" && r[0] === "add") {
|
|
1786
|
+
if (r.some((x) => /^--(git|path)(=|$)/.test(x))) continue;
|
|
1787
|
+
for (const t of targets(r.slice(1), CARGO_VALUE_FLAGS)) add("crates", crateName(t));
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
return out2;
|
|
1014
1791
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
var import_os = require("os");
|
|
1035
|
-
var import_path2 = require("path");
|
|
1036
|
-
|
|
1037
|
-
// src/application/services/workspaceSandbox/checkpointMap.ts
|
|
1038
|
-
var CHECKPOINT_MAP_VERSION = 2;
|
|
1039
|
-
var LOCAL_FOLDER_KEY = ".";
|
|
1040
|
-
function parseCheckpoints(raw, legacyRepo) {
|
|
1041
|
-
const out2 = /* @__PURE__ */ new Map();
|
|
1042
|
-
if (!raw) return out2;
|
|
1043
|
-
const trimmed = raw.trimStart();
|
|
1044
|
-
if (trimmed.startsWith("{")) {
|
|
1045
|
-
try {
|
|
1046
|
-
const parsed = JSON.parse(trimmed);
|
|
1047
|
-
if (parsed && parsed.version === CHECKPOINT_MAP_VERSION && parsed.repos && typeof parsed.repos === "object" && !Array.isArray(parsed.repos)) {
|
|
1048
|
-
for (const [repo2, patch] of Object.entries(parsed.repos)) {
|
|
1049
|
-
if (repo2 && typeof patch === "string" && patch) out2.set(repo2, patch);
|
|
1050
|
-
}
|
|
1792
|
+
var NEW_PACKAGE_DAYS = 30;
|
|
1793
|
+
var REGISTRY_TIMEOUT_MS = 3e3;
|
|
1794
|
+
function registryUrl(p) {
|
|
1795
|
+
if (p.ecosystem === "npm") return `https://registry.npmjs.org/${p.name.replace("/", "%2F")}`;
|
|
1796
|
+
if (p.ecosystem === "pypi") return `https://pypi.org/pypi/${encodeURIComponent(p.name)}/json`;
|
|
1797
|
+
return `https://crates.io/api/v1/crates/${encodeURIComponent(p.name)}`;
|
|
1798
|
+
}
|
|
1799
|
+
function firstPublished(p, body) {
|
|
1800
|
+
let iso3 = null;
|
|
1801
|
+
if (p.ecosystem === "npm") iso3 = body?.time?.created;
|
|
1802
|
+
else if (p.ecosystem === "crates") iso3 = body?.crate?.created_at;
|
|
1803
|
+
else {
|
|
1804
|
+
let min = null;
|
|
1805
|
+
const releases = body?.releases && typeof body.releases === "object" ? Object.values(body.releases) : [];
|
|
1806
|
+
for (const files of releases) {
|
|
1807
|
+
if (!Array.isArray(files)) continue;
|
|
1808
|
+
for (const f of files) {
|
|
1809
|
+
const t2 = Date.parse(String(f?.upload_time_iso_8601 ?? f?.upload_time ?? ""));
|
|
1810
|
+
if (Number.isFinite(t2) && (min === null || t2 < min)) min = t2;
|
|
1051
1811
|
}
|
|
1052
|
-
} catch {
|
|
1053
1812
|
}
|
|
1813
|
+
return min === null ? null : new Date(min);
|
|
1814
|
+
}
|
|
1815
|
+
const t = typeof iso3 === "string" ? Date.parse(iso3) : NaN;
|
|
1816
|
+
return Number.isFinite(t) ? new Date(t) : null;
|
|
1817
|
+
}
|
|
1818
|
+
var RegistryChecker = class {
|
|
1819
|
+
constructor(o = {}) {
|
|
1820
|
+
this.o = o;
|
|
1821
|
+
}
|
|
1822
|
+
o;
|
|
1823
|
+
cache = /* @__PURE__ */ new Map();
|
|
1824
|
+
async check(refs) {
|
|
1825
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
1826
|
+
const signal = AbortSignal.timeout(this.o.timeoutMs ?? REGISTRY_TIMEOUT_MS);
|
|
1827
|
+
await Promise.all(refs.map(async (p) => {
|
|
1828
|
+
const key = `${p.ecosystem}:${p.name}`;
|
|
1829
|
+
const cached = this.cache.get(key);
|
|
1830
|
+
if (cached) {
|
|
1831
|
+
out2.set(key, cached);
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
const status = await this.lookup(p, signal);
|
|
1835
|
+
if (status !== "UNVERIFIED") this.cache.set(key, status);
|
|
1836
|
+
out2.set(key, status);
|
|
1837
|
+
}));
|
|
1054
1838
|
return out2;
|
|
1055
1839
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1840
|
+
async lookup(p, signal) {
|
|
1841
|
+
try {
|
|
1842
|
+
const res = await (this.o.fetchImpl ?? fetch)(registryUrl(p), {
|
|
1843
|
+
headers: { accept: "application/json", "user-agent": this.o.userAgent ?? "scalequality-code (+https://scalequality.io)" },
|
|
1844
|
+
signal,
|
|
1845
|
+
redirect: "follow"
|
|
1846
|
+
});
|
|
1847
|
+
if (res.status === 404) return "NOT_FOUND";
|
|
1848
|
+
if (!res.ok) return "UNVERIFIED";
|
|
1849
|
+
const body = await res.json();
|
|
1850
|
+
const first = firstPublished(p, body);
|
|
1851
|
+
if (!first) return "UNVERIFIED";
|
|
1852
|
+
const ageDays = ((this.o.now?.() ?? /* @__PURE__ */ new Date()).getTime() - first.getTime()) / 864e5;
|
|
1853
|
+
return ageDays < NEW_PACKAGE_DAYS ? "TOO_NEW" : "OK";
|
|
1854
|
+
} catch {
|
|
1855
|
+
return "UNVERIFIED";
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
};
|
|
1859
|
+
function mayNeedProtection(command, p) {
|
|
1860
|
+
if (p.destructiveCommands && isDestructiveCommand(command)) return true;
|
|
1861
|
+
if (p.deployApproval && isDeployCommand(command)) return true;
|
|
1862
|
+
if (p.protectedPush && gitPushes(command).length > 0) return true;
|
|
1863
|
+
return p.unknownPackage !== null && packageInstalls(command).length > 0;
|
|
1864
|
+
}
|
|
1865
|
+
function packageMessage(name, ecosystem, reason) {
|
|
1866
|
+
const registry = ecosystem === "npm" ? "npm" : ecosystem === "pypi" ? "PyPI" : "crates.io";
|
|
1867
|
+
return reason === "NOT_FOUND" ? `Blocked by your organization's protection: the package "${name}" does not exist in the public ${registry} registry. It was not installed. Check the name (it may be a typo or an invented package) and ask the user before trying another one.` : `Blocked by your organization's protection: the package "${name}" was first published on ${registry} less than ${NEW_PACKAGE_DAYS} days ago. It was not installed. Use an established package or ask the user.`;
|
|
1868
|
+
}
|
|
1869
|
+
async function protectionVerdict(command, p, deps) {
|
|
1870
|
+
const reasons = [];
|
|
1871
|
+
let pkg = null;
|
|
1872
|
+
if (p.unknownPackage) {
|
|
1873
|
+
const refs = packageInstalls(command, { installedLocally: deps.installedLocally });
|
|
1874
|
+
if (refs.length) {
|
|
1875
|
+
const results = await deps.registry.check(refs);
|
|
1876
|
+
const flagged = refs.map((r) => ({ r, status: results.get(`${r.ecosystem}:${r.name}`) ?? "UNVERIFIED" })).filter((x) => x.status !== "OK");
|
|
1877
|
+
const definite = flagged.find((x) => x.status === "NOT_FOUND" || x.status === "TOO_NEW");
|
|
1878
|
+
if (definite && p.unknownPackage === "block") {
|
|
1879
|
+
const reason = definite.status;
|
|
1880
|
+
return { kind: "deny", message: packageMessage(definite.r.name, definite.r.ecosystem, reason), reason: "PROTECTION_UNKNOWN_PACKAGE", package: definite.r.name, packageReason: reason };
|
|
1881
|
+
}
|
|
1882
|
+
const ask = definite ?? (p.unknownPackage === "block" ? flagged[0] : void 0);
|
|
1883
|
+
if (ask) {
|
|
1884
|
+
reasons.push("PROTECTION_UNKNOWN_PACKAGE");
|
|
1885
|
+
pkg = { name: ask.r.name, reason: ask.status };
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
if (p.destructiveCommands && isDestructiveCommand(command)) reasons.push("PROTECTION_DESTRUCTIVE_COMMAND");
|
|
1890
|
+
if (p.deployApproval && isDeployCommand(command)) reasons.push("PROTECTION_DEPLOY");
|
|
1891
|
+
if (p.protectedPush && await isProtectedPush(command, deps.currentBranch)) reasons.push("PROTECTION_PROTECTED_PUSH");
|
|
1892
|
+
if (!reasons.length) return { kind: "allow" };
|
|
1893
|
+
return { kind: "ask", reasons, ...pkg ? { package: pkg.name, packageReason: pkg.reason } : {} };
|
|
1894
|
+
}
|
|
1895
|
+
var PROTECTION_MESSAGES = {
|
|
1896
|
+
rejected: "Your organization's protection requires a person to approve this command, and the user denied it. It did not run. Do not run it again or another way unless the user asks.",
|
|
1897
|
+
timedOut: (minutes) => `Your organization's protection requires a person to approve this command, and nobody answered within ${minutes} minutes. It did not run. Ask the user before trying it again.`,
|
|
1898
|
+
stopped: "The request was stopped before the command ran.",
|
|
1899
|
+
unavailable: "Your organization's protection requires a person to approve this command, and the approval could not be requested. It did not run. Tell the user; do not try to run it another way."
|
|
1900
|
+
};
|
|
1901
|
+
|
|
1902
|
+
// src/application/services/workspaceSandbox/SessionTransport.ts
|
|
1903
|
+
var SessionGoneError = class extends Error {
|
|
1904
|
+
constructor(status) {
|
|
1905
|
+
super(`session unavailable (${status})`);
|
|
1906
|
+
this.status = status;
|
|
1907
|
+
this.name = "SessionGoneError";
|
|
1908
|
+
}
|
|
1909
|
+
status;
|
|
1910
|
+
};
|
|
1911
|
+
var TransportError = class extends Error {
|
|
1912
|
+
constructor(message, status, retryable) {
|
|
1913
|
+
super(message);
|
|
1914
|
+
this.status = status;
|
|
1915
|
+
this.retryable = retryable;
|
|
1916
|
+
this.name = "TransportError";
|
|
1917
|
+
}
|
|
1918
|
+
status;
|
|
1919
|
+
retryable;
|
|
1920
|
+
/** The API's error code (e.g. REPOSITORY_NOT_IN_SCOPE), when it sent one. */
|
|
1921
|
+
code;
|
|
1922
|
+
};
|
|
1923
|
+
|
|
1924
|
+
// src/application/services/workspaceSandbox/HttpSessionTransport.ts
|
|
1925
|
+
var GONE = [401, 403, 404, 410];
|
|
1926
|
+
var HttpSessionTransport = class {
|
|
1927
|
+
constructor(opts) {
|
|
1928
|
+
this.opts = opts;
|
|
1929
|
+
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
1930
|
+
this.gone = new Set(opts.goneStatuses ?? GONE);
|
|
1931
|
+
const base = opts.baseUrl.replace(/\/+$/, "");
|
|
1932
|
+
this.root = `${base}/api/ai-governance/internal/workspace-sessions/${encodeURIComponent(opts.sessionId)}`;
|
|
1933
|
+
}
|
|
1934
|
+
opts;
|
|
1935
|
+
fetchImpl;
|
|
1936
|
+
root;
|
|
1937
|
+
gone;
|
|
1938
|
+
async bootstrap() {
|
|
1939
|
+
const raw = await this.get(`/bootstrap?features=${ENGINE_FEATURES.join(",")}`, this.opts.requestTimeoutMs ?? 3e4);
|
|
1940
|
+
return toSessionBootstrap(raw);
|
|
1941
|
+
}
|
|
1942
|
+
async pollCommands(waitSeconds, signal) {
|
|
1943
|
+
const wait = Math.max(0, Math.min(25, Math.floor(waitSeconds)));
|
|
1944
|
+
const body = await this.get(`/commands?wait=${wait}`, (wait + 15) * 1e3, signal);
|
|
1945
|
+
return Array.isArray(body?.commands) ? body.commands : [];
|
|
1946
|
+
}
|
|
1947
|
+
async postEvents(events) {
|
|
1948
|
+
if (events.length === 0) return;
|
|
1949
|
+
await this.post("/events", { events }, this.opts.requestTimeoutMs ?? 3e4);
|
|
1950
|
+
}
|
|
1951
|
+
callTool(name, args, approvalId) {
|
|
1952
|
+
if (!/^[a-z][a-z0-9_]{0,63}$/.test(name)) throw new TransportError("invalid tool name", null, false);
|
|
1953
|
+
return this.post(`/tools/${name}`, approvalId ? { args, approvalId } : { args }, this.opts.toolTimeoutMs ?? 12e4);
|
|
1954
|
+
}
|
|
1955
|
+
async mcpTools() {
|
|
1956
|
+
const raw = await this.get("/mcp/tools", this.opts.requestTimeoutMs ?? 6e4);
|
|
1957
|
+
return orgMcpCatalogOf(raw);
|
|
1958
|
+
}
|
|
1959
|
+
mcpCall(req) {
|
|
1960
|
+
return this.post("/mcp/call", req, this.opts.toolTimeoutMs ?? 18e4);
|
|
1961
|
+
}
|
|
1962
|
+
openPullRequest(req) {
|
|
1963
|
+
return this.post("/pull-request", req, this.opts.toolTimeoutMs ?? 18e4);
|
|
1964
|
+
}
|
|
1965
|
+
async checkpoint(req) {
|
|
1966
|
+
await this.post("/checkpoint", req, this.opts.requestTimeoutMs ?? 6e4);
|
|
1967
|
+
}
|
|
1968
|
+
async openRepository(repoFullName) {
|
|
1969
|
+
const raw = await this.post("/repositories/open", { repoFullName }, this.opts.requestTimeoutMs ?? 3e4);
|
|
1970
|
+
const text3 = (v, fallback = "") => typeof v === "string" ? v : fallback;
|
|
1971
|
+
const defaultBranch = text3(raw?.defaultBranch, "main");
|
|
1972
|
+
return {
|
|
1973
|
+
cloneUrl: text3(raw?.cloneUrl),
|
|
1974
|
+
scheme: text3(raw?.scheme),
|
|
1975
|
+
token: text3(raw?.token),
|
|
1976
|
+
provider: text3(raw?.provider),
|
|
1977
|
+
repoFullName: text3(raw?.repoFullName, repoFullName),
|
|
1978
|
+
defaultBranch,
|
|
1979
|
+
branch: typeof raw?.branch === "string" ? raw.branch : defaultBranch
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
async importedHistory() {
|
|
1983
|
+
const raw = await this.get("/imported", this.opts.requestTimeoutMs ?? 6e4);
|
|
1984
|
+
const messages = Array.isArray(raw?.messages) ? raw.messages : [];
|
|
1985
|
+
return {
|
|
1986
|
+
messages: messages.filter((m) => !!m && typeof m === "object" && (m.role === "user" || m.role === "assistant")).map((m) => ({
|
|
1987
|
+
role: m.role,
|
|
1988
|
+
text: typeof m.text === "string" ? m.text : "",
|
|
1989
|
+
at: typeof m.at === "string" ? m.at : null,
|
|
1990
|
+
...Array.isArray(m.tools) ? { tools: m.tools.filter((t) => !!t && typeof t.name === "string" && typeof t.summary === "string") } : {}
|
|
1991
|
+
}))
|
|
1992
|
+
};
|
|
1064
1993
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
// src/application/services/workspaceSandbox/workspaceGit.ts
|
|
1069
|
-
var run = (0, import_util.promisify)(import_child_process.execFile);
|
|
1070
|
-
var CHECKPOINT_BASE_HEADER = "ScaleQuality-Base:";
|
|
1071
|
-
var DIFF_CAPS = { perFileBytes: 200 * 1024, totalBytes: 2 * 1024 * 1024, maxFiles: 500 };
|
|
1072
|
-
async function git(args, opts) {
|
|
1073
|
-
const { stdout } = await run("git", args, {
|
|
1074
|
-
cwd: opts.cwd,
|
|
1075
|
-
timeout: opts.timeoutMs ?? 6e4,
|
|
1076
|
-
maxBuffer: opts.maxBuffer ?? 64 * 1024 * 1024,
|
|
1077
|
-
env: gitEnv(opts.env),
|
|
1078
|
-
windowsHide: true
|
|
1079
|
-
});
|
|
1080
|
-
return stdout;
|
|
1081
|
-
}
|
|
1082
|
-
async function gitBuffer(args, opts) {
|
|
1083
|
-
const { stdout } = await run("git", args, {
|
|
1084
|
-
cwd: opts.cwd,
|
|
1085
|
-
timeout: opts.timeoutMs ?? 6e4,
|
|
1086
|
-
maxBuffer: opts.maxBuffer ?? 64 * 1024 * 1024,
|
|
1087
|
-
env: gitEnv(opts.env),
|
|
1088
|
-
windowsHide: true,
|
|
1089
|
-
encoding: "buffer"
|
|
1090
|
-
});
|
|
1091
|
-
return stdout;
|
|
1092
|
-
}
|
|
1093
|
-
function gitEnv(extra) {
|
|
1094
|
-
const env = {};
|
|
1095
|
-
for (const k of ["PATH", "HOME", "LANG", "LC_ALL", "TMPDIR", "GIT_CONFIG_GLOBAL"]) if (process.env[k] != null) env[k] = process.env[k];
|
|
1096
|
-
env.GIT_TERMINAL_PROMPT = "0";
|
|
1097
|
-
env.GIT_CONFIG_NOSYSTEM = "1";
|
|
1098
|
-
return { ...env, ...extra ?? {} };
|
|
1099
|
-
}
|
|
1100
|
-
function scopeSpec(scope) {
|
|
1101
|
-
return scope?.prefix ? ["--", scope.prefix] : [];
|
|
1102
|
-
}
|
|
1103
|
-
async function repoIndexFile(root) {
|
|
1104
|
-
const dotGit = (0, import_path2.join)(root, ".git");
|
|
1105
|
-
const st = await (0, import_promises2.stat)(dotGit).catch(() => null);
|
|
1106
|
-
if (!st || st.isDirectory()) return (0, import_path2.join)(dotGit, "index");
|
|
1107
|
-
const out2 = await git(["rev-parse", "--git-path", "index"], { cwd: root }).catch(() => "");
|
|
1108
|
-
return out2.trim() ? (0, import_path2.resolve)(root, out2.trim()) : (0, import_path2.join)(dotGit, "index");
|
|
1109
|
-
}
|
|
1110
|
-
async function withWorktreeIndex(root, fn, scope) {
|
|
1111
|
-
const tag = `${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1112
|
-
const idx = (0, import_path2.join)((0, import_os.tmpdir)(), `sq-ws-index-${tag}`);
|
|
1113
|
-
const real = scope?.index !== void 0 ? scope.index : await repoIndexFile(root);
|
|
1114
|
-
if (real && (0, import_fs2.existsSync)(real)) {
|
|
1115
|
-
await (0, import_promises2.copyFile)(real, idx);
|
|
1116
|
-
const st = await (0, import_promises2.stat)(real).catch(() => null);
|
|
1117
|
-
if (st) await (0, import_promises2.utimes)(idx, st.atime, st.mtime).catch(() => void 0);
|
|
1994
|
+
async saveTranscript(t) {
|
|
1995
|
+
await this.post("/transcript", t, this.opts.requestTimeoutMs ?? 6e4);
|
|
1118
1996
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
try {
|
|
1122
|
-
if (scope?.paths) {
|
|
1123
|
-
if (scope.paths.length) {
|
|
1124
|
-
await (0, import_promises2.writeFile)(list, scope.paths.join("\0"), "utf8");
|
|
1125
|
-
await git(["add", "-A", "-f", `--pathspec-from-file=${list}`, "--pathspec-file-nul"], { cwd: root, env, timeoutMs: 5 * 6e4 });
|
|
1126
|
-
}
|
|
1127
|
-
} else {
|
|
1128
|
-
await git(["add", "-A", ...scopeSpec(scope)], { cwd: root, env, timeoutMs: 5 * 6e4 });
|
|
1129
|
-
}
|
|
1130
|
-
return await fn(env);
|
|
1131
|
-
} finally {
|
|
1132
|
-
await (0, import_promises2.rm)(idx, { force: true }).catch(() => void 0);
|
|
1133
|
-
await (0, import_promises2.rm)(list, { force: true }).catch(() => void 0);
|
|
1997
|
+
async saveBaseMeasurement(r) {
|
|
1998
|
+
await this.post("/measurements/base", r, this.opts.requestTimeoutMs ?? 3e4);
|
|
1134
1999
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
}
|
|
1139
|
-
async function listChangesWithEnv(root, base, env, scope) {
|
|
1140
|
-
const nameStatus = await git(["diff", "--cached", "--no-renames", "--name-status", "-z", base, ...scopeSpec(scope)], { cwd: root, env });
|
|
1141
|
-
const numstat = await git(["diff", "--cached", "--no-renames", "--numstat", "-z", base, ...scopeSpec(scope)], { cwd: root, env });
|
|
1142
|
-
const counts = /* @__PURE__ */ new Map();
|
|
1143
|
-
for (const rec of numstat.split("\0")) {
|
|
1144
|
-
if (!rec) continue;
|
|
1145
|
-
const m = /^(-|\d+)\t(-|\d+)\t([\s\S]*)$/.exec(rec);
|
|
1146
|
-
if (!m) continue;
|
|
1147
|
-
counts.set(m[3], { a: m[1] === "-" ? 0 : Number(m[1]), d: m[2] === "-" ? 0 : Number(m[2]), bin: m[1] === "-" });
|
|
2000
|
+
async reply(commandId, reply) {
|
|
2001
|
+
if (!/^[A-Za-z0-9-]{1,80}$/.test(commandId)) throw new TransportError("invalid command id", null, false);
|
|
2002
|
+
await this.post(`/replies/${commandId}`, reply, this.opts.requestTimeoutMs ?? 3e4);
|
|
1148
2003
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
if (!code || !path) continue;
|
|
1155
|
-
const status = code.startsWith("A") ? "added" : code.startsWith("D") ? "deleted" : "modified";
|
|
1156
|
-
const c = counts.get(path) ?? { a: 0, d: 0, bin: false };
|
|
1157
|
-
out2.push({ path, status, additions: c.a, deletions: c.d, binary: c.bin });
|
|
2004
|
+
async requestCommandApproval(body) {
|
|
2005
|
+
const raw = await this.post("/command-approvals", body, this.opts.requestTimeoutMs ?? 3e4);
|
|
2006
|
+
const approvalId = typeof raw?.approvalId === "string" ? raw.approvalId : "";
|
|
2007
|
+
if (!approvalId) throw new TransportError("invalid command approval response", 502, false);
|
|
2008
|
+
return { approvalId };
|
|
1158
2009
|
}
|
|
1159
|
-
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
const
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
2010
|
+
async cancelCommandApproval(approvalId, reason) {
|
|
2011
|
+
await this.post(`/command-approvals/${encodeURIComponent(approvalId)}/cancel`, { reason }, this.opts.requestTimeoutMs ?? 3e4);
|
|
2012
|
+
}
|
|
2013
|
+
/** `current`: the model this engine works with now, which the API keeps for the session when the new one is refused. */
|
|
2014
|
+
async refreshRuntime(current) {
|
|
2015
|
+
const raw = await this.post("/runtime", current?.model ? { current: current.model } : {}, this.opts.requestTimeoutMs ?? 3e4);
|
|
2016
|
+
const boot = toSessionBootstrap({ session: { model: raw?.model, reasoning: raw?.reasoning }, runtime: raw?.runtime, repository: null });
|
|
2017
|
+
return { model: boot.model, runtime: boot.runtime, reasoning: boot.reasoning ?? null };
|
|
2018
|
+
}
|
|
2019
|
+
headers(json) {
|
|
2020
|
+
return {
|
|
2021
|
+
"x-workspace-session-secret": this.opts.secret,
|
|
2022
|
+
accept: "application/json",
|
|
2023
|
+
...this.opts.userAgent ? { "user-agent": this.opts.userAgent } : {},
|
|
2024
|
+
...json ? { "content-type": "application/json" } : {}
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
async get(path, timeoutMs, outer) {
|
|
2028
|
+
const attempts = Math.max(1, this.opts.getAttempts ?? 4);
|
|
2029
|
+
const delay = this.opts.retryDelayMs ?? 1e3;
|
|
2030
|
+
let last;
|
|
2031
|
+
for (let i = 0; i < attempts; i++) {
|
|
2032
|
+
if (outer?.aborted) throw new TransportError("aborted", null, false);
|
|
2033
|
+
try {
|
|
2034
|
+
return await this.request("GET", path, void 0, timeoutMs, outer);
|
|
2035
|
+
} catch (e) {
|
|
2036
|
+
last = e;
|
|
2037
|
+
if (e instanceof SessionGoneError) throw e;
|
|
2038
|
+
if (e instanceof TransportError && !e.retryable) throw e;
|
|
2039
|
+
if (i < attempts - 1) await sleep(delay * 2 ** i, outer);
|
|
1185
2040
|
}
|
|
1186
|
-
files.push(entry);
|
|
1187
2041
|
}
|
|
1188
|
-
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
const buf = await (0, import_promises2.readFile)(abs);
|
|
1207
|
-
if (buf.includes(0)) return { reason: "binary" };
|
|
1208
|
-
const content = buf.toString("utf8");
|
|
1209
|
-
if (Buffer.byteLength(content, "utf8") !== buf.length || content.includes("\uFFFD")) return { reason: "not_utf8" };
|
|
1210
|
-
return { content };
|
|
1211
|
-
}
|
|
1212
|
-
async function filesForPullRequest(root, base, published = [], scope) {
|
|
1213
|
-
const changes = await listChanges(root, base, scope);
|
|
1214
|
-
const out2 = { files: [], skipped: [] };
|
|
1215
|
-
for (const c of changes) {
|
|
1216
|
-
if (c.status === "deleted") {
|
|
1217
|
-
out2.files.push({ path: c.path, content: "", deleted: true });
|
|
1218
|
-
continue;
|
|
2042
|
+
throw last;
|
|
2043
|
+
}
|
|
2044
|
+
post(path, body, timeoutMs) {
|
|
2045
|
+
return this.request("POST", path, body, timeoutMs);
|
|
2046
|
+
}
|
|
2047
|
+
async request(method, path, body, timeoutMs, outer) {
|
|
2048
|
+
const signals = [AbortSignal.timeout(timeoutMs), ...outer ? [outer] : []];
|
|
2049
|
+
const signal = signals.length > 1 ? anySignal(signals) : signals[0];
|
|
2050
|
+
let res;
|
|
2051
|
+
try {
|
|
2052
|
+
res = await this.fetchImpl(this.root + path, {
|
|
2053
|
+
method,
|
|
2054
|
+
headers: this.headers(body !== void 0),
|
|
2055
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
2056
|
+
signal
|
|
2057
|
+
});
|
|
2058
|
+
} catch (e) {
|
|
2059
|
+
throw new TransportError(`${method} ${routeLabel(path)} failed: ${e.name}`, null, true);
|
|
1219
2060
|
}
|
|
1220
|
-
if (
|
|
1221
|
-
|
|
1222
|
-
|
|
2061
|
+
if (this.gone.has(res.status)) {
|
|
2062
|
+
await res.body?.cancel().catch(() => void 0);
|
|
2063
|
+
throw new SessionGoneError(res.status);
|
|
1223
2064
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
if (
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
2065
|
+
if (!res.ok) {
|
|
2066
|
+
const code = await res.text().then((t) => JSON.parse(t)?.code, () => void 0).catch(() => void 0);
|
|
2067
|
+
const retryable = res.status === 429 || res.status >= 500;
|
|
2068
|
+
const error = new TransportError(`${method} ${routeLabel(path)} answered ${res.status}`, res.status, retryable);
|
|
2069
|
+
if (typeof code === "string" && /^[A-Z_]{3,80}$/.test(code)) error.code = code;
|
|
2070
|
+
throw error;
|
|
2071
|
+
}
|
|
2072
|
+
if (res.status === 204) return void 0;
|
|
2073
|
+
const text3 = await res.text();
|
|
2074
|
+
if (!text3) return void 0;
|
|
2075
|
+
try {
|
|
2076
|
+
return JSON.parse(text3);
|
|
2077
|
+
} catch {
|
|
2078
|
+
throw new TransportError(`${method} ${routeLabel(path)} returned invalid JSON`, res.status, false);
|
|
1237
2079
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
2080
|
+
}
|
|
2081
|
+
};
|
|
2082
|
+
function anySignal(signals) {
|
|
2083
|
+
const native = AbortSignal.any;
|
|
2084
|
+
if (typeof native === "function") return native.call(AbortSignal, signals);
|
|
2085
|
+
const ac = new AbortController();
|
|
2086
|
+
for (const s of signals) {
|
|
2087
|
+
if (s.aborted) {
|
|
2088
|
+
ac.abort(s.reason);
|
|
2089
|
+
return ac.signal;
|
|
1242
2090
|
}
|
|
1243
|
-
|
|
2091
|
+
s.addEventListener("abort", () => ac.abort(s.reason), { once: true });
|
|
1244
2092
|
}
|
|
1245
|
-
return
|
|
2093
|
+
return ac.signal;
|
|
1246
2094
|
}
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
const abs = (0, import_path2.resolve)(root, p);
|
|
1250
|
-
const realRoot = await (0, import_promises2.realpath)(root).catch(() => (0, import_path2.resolve)(root));
|
|
1251
|
-
let probe = abs;
|
|
1252
|
-
while (!(0, import_fs2.existsSync)(probe) && (0, import_path2.dirname)(probe) !== probe) probe = (0, import_path2.dirname)(probe);
|
|
1253
|
-
const realProbe = await (0, import_promises2.realpath)(probe).catch(() => probe);
|
|
1254
|
-
const rest = (0, import_path2.relative)(probe, abs);
|
|
1255
|
-
const finalPath = rest ? (0, import_path2.join)(realProbe, rest) : realProbe;
|
|
1256
|
-
if (finalPath !== realRoot && !finalPath.startsWith(realRoot + import_path2.sep)) return null;
|
|
1257
|
-
return finalPath;
|
|
2095
|
+
function routeLabel(path) {
|
|
2096
|
+
return path.split("?")[0];
|
|
1258
2097
|
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
await git(["checkout", base, "--", rel], { cwd: root });
|
|
1268
|
-
await git(["reset", "-q", "--", rel], { cwd: root }).catch(() => void 0);
|
|
1269
|
-
return "restored";
|
|
1270
|
-
}
|
|
1271
|
-
await (0, import_promises2.rm)(abs, { recursive: true, force: true });
|
|
1272
|
-
await git(["rm", "-q", "--cached", "--ignore-unmatch", "--", rel], { cwd: root }).catch(() => void 0);
|
|
1273
|
-
return "removed";
|
|
2098
|
+
function sleep(ms, signal) {
|
|
2099
|
+
return new Promise((resolve8) => {
|
|
2100
|
+
const t = setTimeout(resolve8, ms);
|
|
2101
|
+
signal?.addEventListener("abort", () => {
|
|
2102
|
+
clearTimeout(t);
|
|
2103
|
+
resolve8();
|
|
2104
|
+
}, { once: true });
|
|
2105
|
+
});
|
|
1274
2106
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
2107
|
+
var ENGINE_FEATURES = ["message_queue_v1"];
|
|
2108
|
+
function toSessionBootstrap(raw) {
|
|
2109
|
+
if (raw && raw.repo && !raw.repository) return raw;
|
|
2110
|
+
const session = raw?.session ?? {};
|
|
2111
|
+
const repository = raw?.repository && typeof raw.repository === "object" ? raw.repository : null;
|
|
2112
|
+
const runtime = raw?.runtime ?? {};
|
|
2113
|
+
const text3 = (v, fallback = "") => typeof v === "string" ? v : fallback;
|
|
2114
|
+
const defaultBranch = text3(repository?.defaultBranch, "main");
|
|
2115
|
+
const repo2 = repository ? {
|
|
2116
|
+
cloneUrl: text3(repository.cloneUrl),
|
|
2117
|
+
scheme: text3(repository.scheme),
|
|
2118
|
+
token: text3(repository.token),
|
|
2119
|
+
provider: text3(repository.provider, text3(session.provider)),
|
|
2120
|
+
repoFullName: text3(repository.repoFullName, text3(session.repoFullName)),
|
|
2121
|
+
defaultBranch
|
|
2122
|
+
} : null;
|
|
2123
|
+
const repositories = scopeRepos(raw?.repositories) ?? scopeRepos(session.scope?.repos) ?? (repo2 ? [{ repoFullName: repo2.repoFullName, provider: repo2.provider, projectId: text3(session.projectId), defaultBranch: repo2.defaultBranch }] : []);
|
|
2124
|
+
const rawScope = session.scope && typeof session.scope === "object" ? session.scope : null;
|
|
2125
|
+
const kind = rawScope?.kind === "ALL" || rawScope?.kind === "TEAM" || rawScope?.kind === "BUSINESS_AREA" ? rawScope.kind : "PROJECTS";
|
|
2126
|
+
return {
|
|
2127
|
+
repo: repo2,
|
|
2128
|
+
repositories,
|
|
2129
|
+
scope: {
|
|
2130
|
+
kind,
|
|
2131
|
+
teamId: typeof rawScope?.teamId === "string" ? rawScope.teamId : null,
|
|
2132
|
+
projectIds: Array.isArray(rawScope?.projectIds) ? rawScope.projectIds.filter((p) => typeof p === "string") : typeof session.projectId === "string" && session.projectId ? [session.projectId] : [],
|
|
2133
|
+
repos: repositories,
|
|
2134
|
+
// An area: its name, the chosen initiative's name and the project that measures it (an older API sends none).
|
|
2135
|
+
...kind === "BUSINESS_AREA" ? {
|
|
2136
|
+
areaName: text3(rawScope?.areaName) || null,
|
|
2137
|
+
initiativeName: text3(rawScope?.initiativeName) || null,
|
|
2138
|
+
initiativeProjectId: text3(rawScope?.initiativeProjectId) || null
|
|
2139
|
+
} : {}
|
|
2140
|
+
},
|
|
2141
|
+
branch: text3(session.branch, repo2 ? defaultBranch : ""),
|
|
2142
|
+
projectId: text3(session.projectId),
|
|
2143
|
+
model: text3(session.model, "sq-auto"),
|
|
2144
|
+
runtime: {
|
|
2145
|
+
baseUrl: text3(runtime.baseUrl),
|
|
2146
|
+
token: text3(runtime.token),
|
|
2147
|
+
models: runtime.models,
|
|
2148
|
+
primaryModel: typeof runtime.primaryModel === "string" ? runtime.primaryModel : null,
|
|
2149
|
+
fastModel: typeof runtime.fastModel === "string" ? runtime.fastModel : null,
|
|
2150
|
+
maxOutputTokens: typeof runtime.maxOutputTokens === "number" ? runtime.maxOutputTokens : null,
|
|
2151
|
+
maxOutputTokensCeiling: typeof runtime.maxOutputTokensCeiling === "number" ? runtime.maxOutputTokensCeiling : null,
|
|
2152
|
+
reasoning: parseReasoningCapability(runtime.reasoning),
|
|
2153
|
+
aliases: Array.isArray(runtime.aliases) ? runtime.aliases.filter((a) => typeof a?.alias === "string").map(credentialAlias) : void 0,
|
|
2154
|
+
protections: protectionsWireOf(runtime.protections)
|
|
2155
|
+
},
|
|
2156
|
+
reasoning: isReasoningLevel(session.reasoning) ? session.reasoning : null,
|
|
2157
|
+
imported: importedInfo(session.imported),
|
|
2158
|
+
checkpointPatch: typeof raw?.checkpointPatch === "string" ? raw.checkpointPatch : null,
|
|
2159
|
+
sdkSessionId: typeof session.sdkSessionId === "string" ? session.sdkSessionId : null,
|
|
2160
|
+
workspaceKind: session.workspaceKind === "LOCAL" ? "LOCAL" : "CLOUD",
|
|
2161
|
+
projectName: typeof session.projectName === "string" ? session.projectName : typeof raw?.project?.name === "string" ? raw.project.name : null,
|
|
2162
|
+
folderLink: typeof session.folderLink?.projectId === "string" && session.folderLink.projectId ? { projectId: session.folderLink.projectId } : null,
|
|
2163
|
+
transcript: transcriptOf(raw?.transcript),
|
|
2164
|
+
baseMeasurements: raw?.baseMeasurements && typeof raw.baseMeasurements === "object" && !Array.isArray(raw.baseMeasurements) ? raw.baseMeasurements : {},
|
|
2165
|
+
pullRequests: pullRequestsOf(raw?.pullRequests),
|
|
2166
|
+
skills: skillsOf(raw?.skills),
|
|
2167
|
+
plugins: pluginsOf(raw?.plugins),
|
|
2168
|
+
environments: environmentsOf(raw?.environments)
|
|
2169
|
+
};
|
|
1282
2170
|
}
|
|
1283
|
-
function
|
|
1284
|
-
const
|
|
1285
|
-
if (
|
|
1286
|
-
|
|
1287
|
-
while (start < buf.length && (buf[start] & 192) === 128) start++;
|
|
1288
|
-
return buf.subarray(start).toString("utf8");
|
|
2171
|
+
function transcriptOf(raw) {
|
|
2172
|
+
const t = raw;
|
|
2173
|
+
if (!t || typeof t !== "object" || typeof t.sdkSessionId !== "string" || t.encoding !== "gzip-base64" || typeof t.data !== "string") return null;
|
|
2174
|
+
return { sdkSessionId: t.sdkSessionId, encoding: "gzip-base64", data: t.data, bytes: typeof t.bytes === "number" ? t.bytes : 0, sha256: typeof t.sha256 === "string" ? t.sha256 : "" };
|
|
1289
2175
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
const
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
2176
|
+
var strings = (v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).filter(([, x]) => typeof x === "string")) : {};
|
|
2177
|
+
function environmentsOf(raw) {
|
|
2178
|
+
if (!Array.isArray(raw)) return [];
|
|
2179
|
+
return raw.filter((e) => !!e && typeof e === "object" && typeof e.projectId === "string").slice(0, 50).map((e) => ({ projectId: e.projectId, setupCommand: typeof e.setupCommand === "string" && e.setupCommand.trim() ? e.setupCommand : null, env: strings(e.env), secrets: strings(e.secrets) }));
|
|
2180
|
+
}
|
|
2181
|
+
function pluginsOf(raw) {
|
|
2182
|
+
if (!Array.isArray(raw)) return [];
|
|
2183
|
+
return raw.filter((p) => !!p && typeof p === "object" && typeof p.name === "string" && Array.isArray(p.files)).slice(0, 20).map((p) => ({ name: p.name, files: p.files.filter((f) => !!f && typeof f.path === "string" && typeof f.content === "string").slice(0, 300) }));
|
|
2184
|
+
}
|
|
2185
|
+
function skillsOf(raw) {
|
|
2186
|
+
if (!Array.isArray(raw)) return [];
|
|
2187
|
+
return raw.filter((s) => !!s && typeof s === "object").filter((s) => typeof s.name === "string" && typeof s.body === "string").slice(0, 200).map((s) => ({
|
|
2188
|
+
name: s.name,
|
|
2189
|
+
description: typeof s.description === "string" ? s.description : "",
|
|
2190
|
+
body: s.body,
|
|
2191
|
+
argumentHint: typeof s.argumentHint === "string" ? s.argumentHint : null
|
|
2192
|
+
}));
|
|
2193
|
+
}
|
|
2194
|
+
function pullRequestsOf(raw) {
|
|
2195
|
+
const out2 = {};
|
|
2196
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return out2;
|
|
2197
|
+
for (const [repo2, v] of Object.entries(raw)) {
|
|
2198
|
+
if (!v || typeof v.branch !== "string") continue;
|
|
2199
|
+
out2[repo2] = {
|
|
2200
|
+
branch: v.branch,
|
|
2201
|
+
number: typeof v.number === "number" ? v.number : null,
|
|
2202
|
+
url: typeof v.url === "string" ? v.url : null,
|
|
2203
|
+
files: Array.isArray(v.files) ? v.files.filter((f) => typeof f === "string") : []
|
|
2204
|
+
};
|
|
1318
2205
|
}
|
|
1319
2206
|
return out2;
|
|
1320
2207
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
const one = `${fileHeader}${hunk.header}
|
|
1334
|
-
${hunk.lines.join("\n")}
|
|
1335
|
-
`;
|
|
1336
|
-
const file = (0, import_path2.join)((0, import_os.tmpdir)(), `sq-hunk-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.patch`);
|
|
1337
|
-
await (0, import_promises2.writeFile)(file, one, "utf8");
|
|
1338
|
-
try {
|
|
1339
|
-
await git(["apply", "-R", "--whitespace=nowarn", file], { cwd: root, env: scope?.env });
|
|
1340
|
-
} finally {
|
|
1341
|
-
await (0, import_promises2.rm)(file, { force: true }).catch(() => void 0);
|
|
1342
|
-
}
|
|
1343
|
-
return "discarded";
|
|
1344
|
-
}, scope);
|
|
2208
|
+
function importedInfo(raw) {
|
|
2209
|
+
if (!raw || typeof raw !== "object") return null;
|
|
2210
|
+
const r = raw;
|
|
2211
|
+
if (r.source !== "CLAUDE_CODE" && r.source !== "CODEX" || !isExternalId(r.externalId)) return null;
|
|
2212
|
+
return {
|
|
2213
|
+
source: r.source,
|
|
2214
|
+
externalId: r.externalId,
|
|
2215
|
+
title: typeof r.title === "string" ? r.title : "",
|
|
2216
|
+
folder: typeof r.folder === "string" ? r.folder : null,
|
|
2217
|
+
messageCount: typeof r.messageCount === "number" ? r.messageCount : 0,
|
|
2218
|
+
nativeResume: r.nativeResume === true
|
|
2219
|
+
};
|
|
1345
2220
|
}
|
|
1346
|
-
|
|
1347
|
-
if (
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
2221
|
+
function scopeRepos(raw) {
|
|
2222
|
+
if (!Array.isArray(raw)) return null;
|
|
2223
|
+
return raw.filter((r) => r && typeof r === "object" && typeof r.repoFullName === "string").map((r) => ({
|
|
2224
|
+
repoFullName: r.repoFullName,
|
|
2225
|
+
provider: typeof r.provider === "string" ? r.provider : "",
|
|
2226
|
+
projectId: typeof r.projectId === "string" ? r.projectId : "",
|
|
2227
|
+
defaultBranch: typeof r.defaultBranch === "string" ? r.defaultBranch : null
|
|
2228
|
+
}));
|
|
2229
|
+
}
|
|
2230
|
+
function orgMcpCatalogOf(raw) {
|
|
2231
|
+
const r = raw ?? {};
|
|
2232
|
+
const servers = (Array.isArray(r.servers) ? r.servers : []).filter((s) => !!s && typeof s === "object" && typeof s.server === "string" && /^[a-z0-9][a-z0-9-]{0,31}$/.test(s.server)).slice(0, 20).map((s) => ({
|
|
2233
|
+
server: s.server,
|
|
2234
|
+
description: typeof s.description === "string" ? s.description : null,
|
|
2235
|
+
approval: typeof s.approval === "string" ? s.approval : "WRITE",
|
|
2236
|
+
tools: (Array.isArray(s.tools) ? s.tools : []).filter((t) => !!t && typeof t === "object" && typeof t.name === "string" && /^[A-Za-z0-9_-]{1,64}$/.test(t.name)).slice(0, 100).map((t) => ({
|
|
2237
|
+
name: t.name,
|
|
2238
|
+
description: typeof t.description === "string" ? t.description : "",
|
|
2239
|
+
inputSchema: t.inputSchema && typeof t.inputSchema === "object" && !Array.isArray(t.inputSchema) ? t.inputSchema : { type: "object" },
|
|
2240
|
+
readOnly: t.readOnly === true
|
|
2241
|
+
}))
|
|
2242
|
+
}));
|
|
2243
|
+
const unavailable = (Array.isArray(r.unavailable) ? r.unavailable : []).filter((u) => !!u && typeof u.server === "string").map((u) => ({ server: String(u.server).slice(0, 32), error: typeof u.error === "string" ? u.error.slice(0, 60) : "MCP_FAILED" }));
|
|
2244
|
+
return { servers, unavailable };
|
|
1362
2245
|
}
|
|
1363
2246
|
|
|
2247
|
+
// src/application/services/workspaceSandbox/localWorkspace.ts
|
|
2248
|
+
var import_promises6 = require("fs/promises");
|
|
2249
|
+
var import_os3 = require("os");
|
|
2250
|
+
var import_path5 = require("path");
|
|
2251
|
+
|
|
1364
2252
|
// src/application/services/workspaceSandbox/localTracking.ts
|
|
1365
|
-
var
|
|
2253
|
+
var import_crypto2 = require("crypto");
|
|
2254
|
+
var import_fs4 = require("fs");
|
|
2255
|
+
var import_promises5 = require("fs/promises");
|
|
2256
|
+
var import_os2 = require("os");
|
|
2257
|
+
var import_path4 = require("path");
|
|
2258
|
+
var posix = (p) => p.split(import_path4.sep).join("/");
|
|
1366
2259
|
function underPrefix(path, prefix) {
|
|
1367
2260
|
if (!prefix) return true;
|
|
1368
2261
|
const clean = path.replace(/^\/+/, "");
|
|
@@ -1428,14 +2321,14 @@ async function discoverRepositories(root, limits = {}) {
|
|
|
1428
2321
|
const children = [];
|
|
1429
2322
|
let isRepo = false;
|
|
1430
2323
|
try {
|
|
1431
|
-
const d = await (0,
|
|
2324
|
+
const d = await (0, import_promises5.opendir)(dir);
|
|
1432
2325
|
for await (const e of d) {
|
|
1433
2326
|
if (e.name === ".git") {
|
|
1434
2327
|
isRepo = true;
|
|
1435
2328
|
continue;
|
|
1436
2329
|
}
|
|
1437
2330
|
if (!e.isDirectory() || e.name.startsWith(".") || DISCOVERY_SKIP.has(e.name)) continue;
|
|
1438
|
-
children.push((0,
|
|
2331
|
+
children.push((0, import_path4.join)(dir, e.name));
|
|
1439
2332
|
}
|
|
1440
2333
|
} catch {
|
|
1441
2334
|
continue;
|
|
@@ -1476,7 +2369,7 @@ async function repoFingerprint(root) {
|
|
|
1476
2369
|
const r = recs[i];
|
|
1477
2370
|
if (!r || r.length < 4) continue;
|
|
1478
2371
|
n++;
|
|
1479
|
-
const st = await (0,
|
|
2372
|
+
const st = await (0, import_promises5.stat)((0, import_path4.join)(root, r.slice(3))).catch(() => null);
|
|
1480
2373
|
h.update(`\0${st ? `${st.size}:${st.mtimeMs}` : "-"}`);
|
|
1481
2374
|
if (r[0] === "R" || r[0] === "C") i++;
|
|
1482
2375
|
}
|
|
@@ -1497,13 +2390,13 @@ var LooseFileTracker = class {
|
|
|
1497
2390
|
return this.o.root;
|
|
1498
2391
|
}
|
|
1499
2392
|
get gitDir() {
|
|
1500
|
-
return (0,
|
|
2393
|
+
return (0, import_path4.join)(this.o.stateDir, "shadow.git");
|
|
1501
2394
|
}
|
|
1502
2395
|
get baseIndex() {
|
|
1503
|
-
return (0,
|
|
2396
|
+
return (0, import_path4.join)(this.o.stateDir, "base.index");
|
|
1504
2397
|
}
|
|
1505
2398
|
get manifest() {
|
|
1506
|
-
return (0,
|
|
2399
|
+
return (0, import_path4.join)(this.o.stateDir, "manifest.json");
|
|
1507
2400
|
}
|
|
1508
2401
|
get env() {
|
|
1509
2402
|
return { GIT_DIR: this.gitDir, GIT_WORK_TREE: this.o.root };
|
|
@@ -1513,8 +2406,8 @@ var LooseFileTracker = class {
|
|
|
1513
2406
|
return this.entries.map((e) => e.path);
|
|
1514
2407
|
}
|
|
1515
2408
|
async init() {
|
|
1516
|
-
await (0,
|
|
1517
|
-
if (!(0,
|
|
2409
|
+
await (0, import_promises5.mkdir)(this.o.stateDir, { recursive: true, mode: 448 });
|
|
2410
|
+
if (!(0, import_fs4.existsSync)((0, import_path4.join)(this.gitDir, "HEAD"))) {
|
|
1518
2411
|
await git(["init", "-q", "--bare", this.gitDir], { cwd: this.o.stateDir });
|
|
1519
2412
|
for (const [k, v] of [
|
|
1520
2413
|
["core.bare", "false"],
|
|
@@ -1527,7 +2420,7 @@ var LooseFileTracker = class {
|
|
|
1527
2420
|
await git(["config", k, v], { cwd: this.o.stateDir, env: { GIT_DIR: this.gitDir } });
|
|
1528
2421
|
}
|
|
1529
2422
|
}
|
|
1530
|
-
const saved = await (0,
|
|
2423
|
+
const saved = await (0, import_promises5.readFile)(this.manifest, "utf8").then((t) => JSON.parse(t), () => null);
|
|
1531
2424
|
if (saved?.version === 1 && saved.root === this.o.root && Array.isArray(saved.entries)) {
|
|
1532
2425
|
this.entries = saved.entries.filter((e) => e && typeof e.path === "string" && typeof e.existed === "boolean");
|
|
1533
2426
|
}
|
|
@@ -1539,12 +2432,12 @@ var LooseFileTracker = class {
|
|
|
1539
2432
|
return next;
|
|
1540
2433
|
}
|
|
1541
2434
|
async track(abs) {
|
|
1542
|
-
const rel = (0,
|
|
1543
|
-
if (!rel || rel.startsWith("..") || (0,
|
|
2435
|
+
const rel = (0, import_path4.relative)(this.o.root, abs);
|
|
2436
|
+
if (!rel || rel.startsWith("..") || (0, import_path4.isAbsolute)(rel)) return;
|
|
1544
2437
|
const path = posix(rel);
|
|
1545
2438
|
if (path.split("/").includes(".git") || this.entries.some((e) => e.path === path) || this.skipped.has(path)) return;
|
|
1546
|
-
if (this.o.excluded().some((r) => abs === r || abs.startsWith(r +
|
|
1547
|
-
const st = await (0,
|
|
2439
|
+
if (this.o.excluded().some((r) => abs === r || abs.startsWith(r + import_path4.sep))) return;
|
|
2440
|
+
const st = await (0, import_promises5.lstat)(abs).catch(() => null);
|
|
1548
2441
|
if (st && !st.isFile()) return;
|
|
1549
2442
|
if (st && st.size > (this.o.maxBytes ?? LOOSE_FILE_MAX_BYTES)) {
|
|
1550
2443
|
this.skipped.add(path);
|
|
@@ -1562,8 +2455,8 @@ var LooseFileTracker = class {
|
|
|
1562
2455
|
async persist() {
|
|
1563
2456
|
const tmp = `${this.manifest}.${process.pid}.tmp`;
|
|
1564
2457
|
const data = { version: 1, root: this.o.root, entries: this.entries };
|
|
1565
|
-
await (0,
|
|
1566
|
-
await (0,
|
|
2458
|
+
await (0, import_promises5.writeFile)(tmp, JSON.stringify(data), { mode: 384 });
|
|
2459
|
+
await (0, import_promises5.rename)(tmp, this.manifest);
|
|
1567
2460
|
}
|
|
1568
2461
|
/** Waits for the copies in progress. */
|
|
1569
2462
|
settled() {
|
|
@@ -1577,8 +2470,8 @@ var LooseFileTracker = class {
|
|
|
1577
2470
|
scope() {
|
|
1578
2471
|
return {
|
|
1579
2472
|
env: this.env,
|
|
1580
|
-
index: (0,
|
|
1581
|
-
paths: this.entries.filter((e) => e.existed || (0,
|
|
2473
|
+
index: (0, import_fs4.existsSync)(this.baseIndex) ? this.baseIndex : null,
|
|
2474
|
+
paths: this.entries.filter((e) => e.existed || (0, import_fs4.existsSync)((0, import_path4.join)(this.o.root, e.path))).map((e) => e.path)
|
|
1582
2475
|
};
|
|
1583
2476
|
}
|
|
1584
2477
|
entry(path) {
|
|
@@ -1600,546 +2493,284 @@ var LooseFileTracker = class {
|
|
|
1600
2493
|
if (e.existed) {
|
|
1601
2494
|
await git(["checkout-index", "-f", "--", e.path], { cwd: this.o.root, env: { ...this.env, GIT_INDEX_FILE: this.baseIndex, GIT_LITERAL_PATHSPECS: "1" } });
|
|
1602
2495
|
} else {
|
|
1603
|
-
await (0,
|
|
2496
|
+
await (0, import_promises5.rm)(abs, { force: true });
|
|
1604
2497
|
}
|
|
1605
2498
|
}
|
|
1606
2499
|
async discardHunk(path, hash) {
|
|
1607
|
-
await this.chain;
|
|
1608
|
-
const e = this.entry(path);
|
|
1609
|
-
return discardHunk(this.o.root, await this.baseTree(), e.path, hash, this.scope());
|
|
1610
|
-
}
|
|
1611
|
-
/** The files as they are now, and how many files were tracked then. */
|
|
1612
|
-
async snapshot() {
|
|
1613
|
-
await this.chain;
|
|
1614
|
-
if (!this.entries.length) return JSON.stringify({ t: null, n: 0 });
|
|
1615
|
-
return JSON.stringify({ t: await worktreeTreeId(this.o.root, this.scope()), n: this.entries.length });
|
|
1616
|
-
}
|
|
1617
|
-
/**
|
|
1618
|
-
* Back to a snapshot: the files tracked then return to their content then;
|
|
1619
|
-
* the ones first changed later return to their copy (that was their content
|
|
1620
|
-
* then, since the session had not touched them).
|
|
1621
|
-
*/
|
|
1622
|
-
async restore(snapshot) {
|
|
1623
|
-
await this.chain;
|
|
1624
|
-
const snap = JSON.parse(snapshot);
|
|
1625
|
-
if (!this.entries.length) return;
|
|
1626
|
-
const idx = (0, import_path3.join)((0, import_os2.tmpdir)(), `sq-loose-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`);
|
|
1627
|
-
const env = { ...this.env, GIT_INDEX_FILE: idx };
|
|
1628
|
-
let target;
|
|
1629
|
-
try {
|
|
1630
|
-
if (snap.t) await git(["read-tree", snap.t], { cwd: this.o.root, env });
|
|
1631
|
-
else await git(["read-tree", "--empty"], { cwd: this.o.root, env });
|
|
1632
|
-
for (const e of this.entries.slice(snap.n)) {
|
|
1633
|
-
if (!e.existed) continue;
|
|
1634
|
-
const line = (await git(["ls-files", "-s", "--", e.path], { cwd: this.o.root, env: { ...this.env, GIT_INDEX_FILE: this.baseIndex, GIT_LITERAL_PATHSPECS: "1" } })).trim();
|
|
1635
|
-
const m = /^(\d+) ([0-9a-f]+) \d\t/.exec(line);
|
|
1636
|
-
if (m) await git(["update-index", "--add", "--cacheinfo", `${m[1]},${m[2]},${e.path}`], { cwd: this.o.root, env });
|
|
1637
|
-
}
|
|
1638
|
-
target = (await git(["write-tree"], { cwd: this.o.root, env })).trim();
|
|
1639
|
-
} finally {
|
|
1640
|
-
await (0, import_promises3.rm)(idx, { force: true }).catch(() => void 0);
|
|
1641
|
-
}
|
|
1642
|
-
await restoreTree(this.o.root, target, this.scope());
|
|
1643
|
-
}
|
|
1644
|
-
async checkpoint() {
|
|
1645
|
-
await this.chain;
|
|
1646
|
-
if (!this.entries.length) return "";
|
|
1647
|
-
return checkpointPatch(this.o.root, await this.baseTree(), this.scope());
|
|
1648
|
-
}
|
|
1649
|
-
async prFiles(published) {
|
|
1650
|
-
await this.chain;
|
|
1651
|
-
if (!this.entries.length && !published.length) return { files: [], skipped: [] };
|
|
1652
|
-
return filesForPullRequest(this.o.root, await this.baseTree(), published, this.scope());
|
|
1653
|
-
}
|
|
1654
|
-
async treeId() {
|
|
1655
|
-
await this.chain;
|
|
1656
|
-
return this.entries.length ? worktreeTreeId(this.o.root, this.scope()) : null;
|
|
1657
|
-
}
|
|
1658
|
-
};
|
|
1659
|
-
var SESSION_STATE_MAX_AGE_MS = 30 * 24 * 60 * 6e4;
|
|
1660
|
-
async function pruneSessionStates(parent, keep, now = Date.now(), maxAgeMs = SESSION_STATE_MAX_AGE_MS) {
|
|
1661
|
-
const removed = [];
|
|
1662
|
-
for (const name of await (0, import_promises3.readdir)(parent).catch(() => [])) {
|
|
1663
|
-
const dir = (0, import_path3.join)(parent, name);
|
|
1664
|
-
if (dir === keep) continue;
|
|
1665
|
-
const st = await (0, import_promises3.stat)((0, import_path3.join)(dir, "manifest.json")).catch(() => null) ?? await (0, import_promises3.stat)(dir).catch(() => null);
|
|
1666
|
-
if (!st || now - st.mtimeMs < maxAgeMs) continue;
|
|
1667
|
-
await (0, import_promises3.rm)(dir, { recursive: true, force: true }).catch(() => void 0);
|
|
1668
|
-
removed.push(name);
|
|
1669
|
-
}
|
|
1670
|
-
return removed;
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
// src/application/services/workspaceSandbox/localWorkspace.ts
|
|
1674
|
-
var EMPTY_TREE = {
|
|
1675
|
-
sha1: "4b825dc642cb6eb9a060e54bf8d69288fbee4904",
|
|
1676
|
-
sha256: "6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321"
|
|
1677
|
-
};
|
|
1678
|
-
var LocalWorkspaceError = class extends Error {
|
|
1679
|
-
constructor(code, publicMessage) {
|
|
1680
|
-
super(publicMessage);
|
|
1681
|
-
this.code = code;
|
|
1682
|
-
this.publicMessage = publicMessage;
|
|
1683
|
-
this.name = "LocalWorkspaceError";
|
|
1684
|
-
}
|
|
1685
|
-
code;
|
|
1686
|
-
publicMessage;
|
|
1687
|
-
};
|
|
1688
|
-
async function localFolderRoot(dir) {
|
|
1689
|
-
const abs = (0, import_path4.resolve)(dir);
|
|
1690
|
-
const st = await (0, import_promises4.stat)(abs).catch(() => null);
|
|
1691
|
-
if (!st || !st.isDirectory()) throw new LocalWorkspaceError("FOLDER_NOT_FOUND", `The folder ${abs} does not exist.`);
|
|
1692
|
-
return (0, import_promises4.realpath)(abs);
|
|
1693
|
-
}
|
|
1694
|
-
var posix2 = (p) => p.split(import_path4.sep).join("/");
|
|
1695
|
-
async function enclosingRepository(root) {
|
|
1696
|
-
let missing = false;
|
|
1697
|
-
const top = await git(["rev-parse", "--show-toplevel"], { cwd: root }).then((o) => o.trim(), (e) => {
|
|
1698
|
-
if (e?.code === "ENOENT") missing = true;
|
|
1699
|
-
return "";
|
|
1700
|
-
});
|
|
1701
|
-
if (missing) return "gitMissing";
|
|
1702
|
-
if (!top) return null;
|
|
1703
|
-
const realTop = await (0, import_promises4.realpath)(top).catch(() => top);
|
|
1704
|
-
if (realTop === root) return { top: realTop, prefix: "" };
|
|
1705
|
-
const prefix = posix2((0, import_path4.relative)(realTop, root));
|
|
1706
|
-
if (!prefix || prefix.startsWith("..")) return null;
|
|
1707
|
-
const env = { GIT_LITERAL_PATHSPECS: "1" };
|
|
1708
|
-
const inHead = (await git(["ls-tree", "-d", "--name-only", "HEAD", "--", prefix], { cwd: realTop, env }).catch(() => "")).trim().length > 0;
|
|
1709
|
-
if (inHead) return { top: realTop, prefix };
|
|
1710
|
-
const home = await (0, import_promises4.realpath)((0, import_os3.homedir)()).catch(() => (0, import_os3.homedir)());
|
|
1711
|
-
if (realTop === home || (0, import_path4.relative)(realTop, home).split(import_path4.sep)[0] !== "..") return null;
|
|
1712
|
-
const ignored = await git(["check-ignore", "-q", "--", prefix], { cwd: realTop, env }).then(() => true, () => false);
|
|
1713
|
-
return ignored ? null : { top: realTop, prefix };
|
|
1714
|
-
}
|
|
1715
|
-
async function repoHead(top) {
|
|
1716
|
-
const head = (await git(["rev-parse", "--verify", "-q", "HEAD^{commit}"], { cwd: top }).catch(() => "")).trim();
|
|
1717
|
-
let baseRevision = head;
|
|
1718
|
-
let baseKind = "commit";
|
|
1719
|
-
if (!head) {
|
|
1720
|
-
const format = (await git(["rev-parse", "--show-object-format"], { cwd: top }).catch(() => "sha1")).trim();
|
|
1721
|
-
baseRevision = EMPTY_TREE[format] ?? EMPTY_TREE.sha1;
|
|
1722
|
-
baseKind = "empty-tree";
|
|
1723
|
-
}
|
|
1724
|
-
const branch2 = (await git(["symbolic-ref", "--short", "-q", "HEAD"], { cwd: top }).catch(() => "")).trim() || null;
|
|
1725
|
-
const originUrl = (await git(["config", "--get", "remote.origin.url"], { cwd: top }).catch(() => "")).trim() || null;
|
|
1726
|
-
return { baseRevision, baseKind, branch: branch2, originUrl: originUrl ? stripUserinfo(originUrl) : null };
|
|
1727
|
-
}
|
|
1728
|
-
async function nestedRepo(folder, dir) {
|
|
1729
|
-
const real = await (0, import_promises4.realpath)(dir).catch(() => null);
|
|
1730
|
-
if (!real) return null;
|
|
1731
|
-
const top = (await git(["rev-parse", "--show-toplevel"], { cwd: real }).catch(() => "")).trim();
|
|
1732
|
-
if (!top || await (0, import_promises4.realpath)(top).catch(() => top) !== real) return null;
|
|
1733
|
-
return { root: real, rel: posix2((0, import_path4.relative)(folder, real)), ...await repoHead(real) };
|
|
1734
|
-
}
|
|
1735
|
-
async function inspectLocalFolder(dir, opts = {}) {
|
|
1736
|
-
const root = await localFolderRoot(dir);
|
|
1737
|
-
const owner = await enclosingRepository(root);
|
|
1738
|
-
if (owner === null || owner === "gitMissing") {
|
|
1739
|
-
const gitAvailable = owner !== "gitMissing";
|
|
1740
|
-
const found = gitAvailable ? await discoverRepositories(root, opts.discovery) : { repos: [], truncated: false, scannedDirs: 0 };
|
|
1741
|
-
const repos = (await mapLimit(found.repos, 8, (d) => nestedRepo(root, d).catch(() => null))).filter((r) => !!r);
|
|
1742
|
-
return {
|
|
1743
|
-
kind: "folder",
|
|
1744
|
-
root,
|
|
1745
|
-
repoRoot: null,
|
|
1746
|
-
prefix: "",
|
|
1747
|
-
baseRevision: "",
|
|
1748
|
-
baseKind: "commit",
|
|
1749
|
-
branch: null,
|
|
1750
|
-
changedAtStart: 0,
|
|
1751
|
-
originUrl: null,
|
|
1752
|
-
headOnRemote: null,
|
|
1753
|
-
repos,
|
|
1754
|
-
reposTruncated: found.truncated,
|
|
1755
|
-
gitAvailable
|
|
1756
|
-
};
|
|
1757
|
-
}
|
|
1758
|
-
const { top, prefix } = owner;
|
|
1759
|
-
const head = await repoHead(top);
|
|
1760
|
-
const status = await git(
|
|
1761
|
-
["--no-optional-locks", "status", "--porcelain", "-z", "--untracked-files=all", ...prefix ? ["--", prefix] : []],
|
|
1762
|
-
{ cwd: top, timeoutMs: 5 * 6e4, env: { GIT_LITERAL_PATHSPECS: "1" } }
|
|
1763
|
-
).catch(() => "");
|
|
1764
|
-
const changedAtStart = countPorcelainZ(status);
|
|
1765
|
-
let headOnRemote = null;
|
|
1766
|
-
const anyRemoteBranch = (await git(["branch", "-r"], { cwd: top }).catch(() => "")).trim().length > 0;
|
|
1767
|
-
if (head.baseKind === "commit" && anyRemoteBranch) {
|
|
1768
|
-
const remotes = await git(["branch", "-r", "--contains", head.baseRevision], { cwd: top }).catch(() => null);
|
|
1769
|
-
headOnRemote = remotes === null ? null : remotes.trim().length > 0;
|
|
1770
|
-
}
|
|
1771
|
-
return { kind: prefix ? "subfolder" : "repo", root, repoRoot: top, prefix, ...head, changedAtStart, headOnRemote, repos: [], reposTruncated: false, gitAvailable: true };
|
|
1772
|
-
}
|
|
1773
|
-
function countPorcelainZ(out2) {
|
|
1774
|
-
const recs = out2.split("\0");
|
|
1775
|
-
let n = 0;
|
|
1776
|
-
for (let i = 0; i < recs.length; i++) {
|
|
1777
|
-
const r = recs[i];
|
|
1778
|
-
if (!r || r.length < 4) continue;
|
|
1779
|
-
n++;
|
|
1780
|
-
if (r[0] === "R" || r[0] === "C") i++;
|
|
1781
|
-
}
|
|
1782
|
-
return n;
|
|
1783
|
-
}
|
|
1784
|
-
function remotePathSegments(url) {
|
|
1785
|
-
const u = url.trim();
|
|
1786
|
-
const scp = /^(?:[^@\s/]+@)?([^:\s/]+):(?!\/\/)(.+)$/.exec(u);
|
|
1787
|
-
let host = "";
|
|
1788
|
-
let path = u;
|
|
1789
|
-
if (scp && !/^[a-z][a-z0-9+.-]*:\/\//i.test(u)) {
|
|
1790
|
-
host = scp[1];
|
|
1791
|
-
path = scp[2];
|
|
1792
|
-
} else {
|
|
1793
|
-
try {
|
|
1794
|
-
const parsed = new URL(u);
|
|
1795
|
-
host = parsed.hostname;
|
|
1796
|
-
path = parsed.pathname;
|
|
1797
|
-
} catch {
|
|
1798
|
-
path = u;
|
|
1799
|
-
}
|
|
1800
|
-
}
|
|
1801
|
-
const segments2 = path.split("/").map((s) => {
|
|
1802
|
-
try {
|
|
1803
|
-
return decodeURIComponent(s);
|
|
1804
|
-
} catch {
|
|
1805
|
-
return s;
|
|
1806
|
-
}
|
|
1807
|
-
}).map((s) => s.toLowerCase().replace(/\.git$/, "")).filter((s) => s && s !== "_git" && s !== "v3");
|
|
1808
|
-
return { host: host.toLowerCase(), segments: segments2 };
|
|
1809
|
-
}
|
|
1810
|
-
var PROVIDER_HOSTS = [[/github/, "GITHUB"], [/gitlab/, "GITLAB"], [/bitbucket/, "BITBUCKET"], [/(dev\.azure|visualstudio)/, "AZURE"]];
|
|
1811
|
-
function matchRemoteToScope(originUrl, repos) {
|
|
1812
|
-
if (!originUrl) return null;
|
|
1813
|
-
const { host, segments: segments2 } = remotePathSegments(originUrl);
|
|
1814
|
-
if (!segments2.length) return null;
|
|
1815
|
-
const provider = PROVIDER_HOSTS.find(([re]) => re.test(host))?.[1] ?? null;
|
|
1816
|
-
let best = [];
|
|
1817
|
-
let bestLen = 0;
|
|
1818
|
-
for (const r of repos) {
|
|
1819
|
-
const rs = r.repoFullName.toLowerCase().split("/").map((s) => s.replace(/\.git$/, "")).filter(Boolean);
|
|
1820
|
-
if (!rs.length || rs.length > segments2.length) continue;
|
|
1821
|
-
const offset = segments2.length - rs.length;
|
|
1822
|
-
if (!rs.every((s, i) => s === segments2[offset + i])) continue;
|
|
1823
|
-
if (rs.length > bestLen) {
|
|
1824
|
-
best = [r];
|
|
1825
|
-
bestLen = rs.length;
|
|
1826
|
-
} else if (rs.length === bestLen) best.push(r);
|
|
1827
|
-
}
|
|
1828
|
-
if (best.length > 1 && provider) best = best.filter((r) => r.provider.toUpperCase().startsWith(provider));
|
|
1829
|
-
return best.length === 1 ? best[0] : null;
|
|
1830
|
-
}
|
|
1831
|
-
function localFolderRepo(originUrl, repos, linkedProjectId) {
|
|
1832
|
-
const linked = linkedProjectId ? repos.filter((r) => r.projectId === linkedProjectId) : [];
|
|
1833
|
-
if (linked.length) return { repo: matchRemoteToScope(originUrl, linked) ?? (linked.length === 1 ? linked[0] : null), linked };
|
|
1834
|
-
return { repo: matchRemoteToScope(originUrl, repos), linked: [] };
|
|
1835
|
-
}
|
|
1836
|
-
function bootScopeRepos(boot) {
|
|
1837
|
-
if (boot.scope?.repos) return boot.scope.repos;
|
|
1838
|
-
if (boot.repositories) return boot.repositories;
|
|
1839
|
-
return boot.repo ? [{ repoFullName: boot.repo.repoFullName, provider: boot.repo.provider, projectId: boot.projectId, defaultBranch: boot.repo.defaultBranch }] : [];
|
|
1840
|
-
}
|
|
1841
|
-
function stripUserinfo(url) {
|
|
1842
|
-
return url.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^@/]+@/i, "$1");
|
|
1843
|
-
}
|
|
1844
|
-
async function prepareLocalWorkspace(dir, _boot, onStep) {
|
|
1845
|
-
const t0 = Date.now();
|
|
1846
|
-
onStep?.("Reading the local folder");
|
|
1847
|
-
const local = await inspectLocalFolder(dir);
|
|
1848
|
-
return {
|
|
1849
|
-
baseRevision: local.baseRevision,
|
|
1850
|
-
baseKind: local.baseKind,
|
|
1851
|
-
branch: local.branch ?? "HEAD",
|
|
1852
|
-
restore: "none",
|
|
1853
|
-
originUrl: local.originUrl,
|
|
1854
|
-
timings: { local_folder: Date.now() - t0 },
|
|
1855
|
-
local
|
|
1856
|
-
};
|
|
1857
|
-
}
|
|
1858
|
-
function localWarnings(local, boot) {
|
|
1859
|
-
const out2 = [];
|
|
1860
|
-
if (local.kind === "folder") {
|
|
1861
|
-
if (local.reposTruncated) out2.push(`The folder is large: the repositories inside it were looked for up to ${DISCOVERY_LIMITS.maxDepth} levels down, for a few seconds. Others are still available; their changes are tracked when you edit them here.`);
|
|
1862
|
-
return out2;
|
|
1863
|
-
}
|
|
1864
|
-
const { repo: match, linked } = localFolderRepo(local.originUrl, bootScopeRepos(boot), boot.folderLink?.projectId);
|
|
1865
|
-
const sessionBranch = match ? boot.repo?.repoFullName === match.repoFullName ? boot.branch || boot.repo.defaultBranch : match.defaultBranch : null;
|
|
1866
|
-
if (sessionBranch && local.branch && local.branch !== sessionBranch) {
|
|
1867
|
-
out2.push(`This folder is on branch "${local.branch}", and the session targets "${sessionBranch}". A pull request is opened against "${sessionBranch}" and only when it is at the same commit as this folder.`);
|
|
2500
|
+
await this.chain;
|
|
2501
|
+
const e = this.entry(path);
|
|
2502
|
+
return discardHunk(this.o.root, await this.baseTree(), e.path, hash, this.scope());
|
|
1868
2503
|
}
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
return out2;
|
|
2504
|
+
/** The files as they are now, and how many files were tracked then. */
|
|
2505
|
+
async snapshot() {
|
|
2506
|
+
await this.chain;
|
|
2507
|
+
if (!this.entries.length) return JSON.stringify({ t: null, n: 0 });
|
|
2508
|
+
return JSON.stringify({ t: await worktreeTreeId(this.o.root, this.scope()), n: this.entries.length });
|
|
1875
2509
|
}
|
|
1876
|
-
|
|
1877
|
-
|
|
2510
|
+
/**
|
|
2511
|
+
* Back to a snapshot: the files tracked then return to their content then;
|
|
2512
|
+
* the ones first changed later return to their copy (that was their content
|
|
2513
|
+
* then, since the session had not touched them).
|
|
2514
|
+
*/
|
|
2515
|
+
async restore(snapshot) {
|
|
2516
|
+
await this.chain;
|
|
2517
|
+
const snap = JSON.parse(snapshot);
|
|
2518
|
+
if (!this.entries.length) return;
|
|
2519
|
+
const idx = (0, import_path4.join)((0, import_os2.tmpdir)(), `sq-loose-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`);
|
|
2520
|
+
const env = { ...this.env, GIT_INDEX_FILE: idx };
|
|
2521
|
+
let target;
|
|
2522
|
+
try {
|
|
2523
|
+
if (snap.t) await git(["read-tree", snap.t], { cwd: this.o.root, env });
|
|
2524
|
+
else await git(["read-tree", "--empty"], { cwd: this.o.root, env });
|
|
2525
|
+
for (const e of this.entries.slice(snap.n)) {
|
|
2526
|
+
if (!e.existed) continue;
|
|
2527
|
+
const line = (await git(["ls-files", "-s", "--", e.path], { cwd: this.o.root, env: { ...this.env, GIT_INDEX_FILE: this.baseIndex, GIT_LITERAL_PATHSPECS: "1" } })).trim();
|
|
2528
|
+
const m = /^(\d+) ([0-9a-f]+) \d\t/.exec(line);
|
|
2529
|
+
if (m) await git(["update-index", "--add", "--cacheinfo", `${m[1]},${m[2]},${e.path}`], { cwd: this.o.root, env });
|
|
2530
|
+
}
|
|
2531
|
+
target = (await git(["write-tree"], { cwd: this.o.root, env })).trim();
|
|
2532
|
+
} finally {
|
|
2533
|
+
await (0, import_promises5.rm)(idx, { force: true }).catch(() => void 0);
|
|
2534
|
+
}
|
|
2535
|
+
await restoreTree(this.o.root, target, this.scope());
|
|
1878
2536
|
}
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
// src/application/services/workspaceSandbox/toolPolicy.ts
|
|
1884
|
-
var import_fs4 = require("fs");
|
|
1885
|
-
var import_promises6 = require("fs/promises");
|
|
1886
|
-
var import_path5 = require("path");
|
|
1887
|
-
|
|
1888
|
-
// src/application/services/workspaceSandbox/webFetchGuard.ts
|
|
1889
|
-
var import_promises5 = require("dns/promises");
|
|
1890
|
-
var import_net = require("net");
|
|
1891
|
-
var defaultLookup = (host) => (0, import_promises5.lookup)(host, { all: true, verbatim: true });
|
|
1892
|
-
var ALLOWED_PORTS = /* @__PURE__ */ new Set(["", "80", "443", "8080", "8443"]);
|
|
1893
|
-
function v4Private(ip) {
|
|
1894
|
-
const [a, b] = ip.split(".").map(Number);
|
|
1895
|
-
return a === 0 || a === 10 || a === 127 || a >= 224 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 192 && b === 0 || a === 198 && (b === 18 || b === 19);
|
|
1896
|
-
}
|
|
1897
|
-
function v6Private(ip) {
|
|
1898
|
-
const s = ip.toLowerCase().replace(/^\[|\]$/g, "");
|
|
1899
|
-
if (s === "::" || s === "::1") return true;
|
|
1900
|
-
const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(s);
|
|
1901
|
-
if (mapped) return v4Private(mapped[1]);
|
|
1902
|
-
const head = parseInt(s.split(":")[0] || "0", 16);
|
|
1903
|
-
return (head & 65024) === 64512 || (head & 65472) === 65152 || (head & 65280) === 65280 || s.startsWith("fd00:ec2::");
|
|
1904
|
-
}
|
|
1905
|
-
function privateAddress(ip) {
|
|
1906
|
-
const kind = (0, import_net.isIP)(ip.replace(/^\[|\]$/g, ""));
|
|
1907
|
-
if (kind === 4) return v4Private(ip);
|
|
1908
|
-
if (kind === 6) return v6Private(ip);
|
|
1909
|
-
return true;
|
|
1910
|
-
}
|
|
1911
|
-
async function webFetchDenial(raw, resolve8 = defaultLookup) {
|
|
1912
|
-
if (typeof raw !== "string" || !raw.trim()) return "WebFetch needs a URL.";
|
|
1913
|
-
let url;
|
|
1914
|
-
try {
|
|
1915
|
-
url = new URL(raw.trim());
|
|
1916
|
-
} catch {
|
|
1917
|
-
return "WebFetch needs a valid http or https URL.";
|
|
2537
|
+
async checkpoint() {
|
|
2538
|
+
await this.chain;
|
|
2539
|
+
if (!this.entries.length) return "";
|
|
2540
|
+
return checkpointPatch(this.o.root, await this.baseTree(), this.scope());
|
|
1918
2541
|
}
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
if (!host || host === "localhost" || host.endsWith(".localhost") || host.endsWith(".internal") || host.endsWith(".local")) {
|
|
1924
|
-
return "Addresses of this machine or its private network cannot be fetched from the workspace.";
|
|
2542
|
+
async prFiles(published) {
|
|
2543
|
+
await this.chain;
|
|
2544
|
+
if (!this.entries.length && !published.length) return { files: [], skipped: [] };
|
|
2545
|
+
return filesForPullRequest(this.o.root, await this.baseTree(), published, this.scope());
|
|
1925
2546
|
}
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
addresses = await resolve8(host);
|
|
1930
|
-
} catch {
|
|
1931
|
-
return `The host ${host} could not be resolved.`;
|
|
2547
|
+
async treeId() {
|
|
2548
|
+
await this.chain;
|
|
2549
|
+
return this.entries.length ? worktreeTreeId(this.o.root, this.scope()) : null;
|
|
1932
2550
|
}
|
|
1933
|
-
|
|
1934
|
-
|
|
2551
|
+
};
|
|
2552
|
+
var SESSION_STATE_MAX_AGE_MS = 30 * 24 * 60 * 6e4;
|
|
2553
|
+
async function pruneSessionStates(parent, keep, now = Date.now(), maxAgeMs = SESSION_STATE_MAX_AGE_MS) {
|
|
2554
|
+
const removed = [];
|
|
2555
|
+
for (const name of await (0, import_promises5.readdir)(parent).catch(() => [])) {
|
|
2556
|
+
const dir = (0, import_path4.join)(parent, name);
|
|
2557
|
+
if (dir === keep) continue;
|
|
2558
|
+
const st = await (0, import_promises5.stat)((0, import_path4.join)(dir, "manifest.json")).catch(() => null) ?? await (0, import_promises5.stat)(dir).catch(() => null);
|
|
2559
|
+
if (!st || now - st.mtimeMs < maxAgeMs) continue;
|
|
2560
|
+
await (0, import_promises5.rm)(dir, { recursive: true, force: true }).catch(() => void 0);
|
|
2561
|
+
removed.push(name);
|
|
1935
2562
|
}
|
|
1936
|
-
return
|
|
2563
|
+
return removed;
|
|
1937
2564
|
}
|
|
1938
2565
|
|
|
1939
|
-
// src/application/services/workspaceSandbox/
|
|
1940
|
-
var
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
var
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
"get_measurement_findings",
|
|
1951
|
-
"get_agent_options",
|
|
1952
|
-
"get_agent_quote",
|
|
1953
|
-
"get_pr_followup_status",
|
|
1954
|
-
"get_agent_request",
|
|
1955
|
-
"get_measurement_request",
|
|
1956
|
-
"get_measurement_setup",
|
|
1957
|
-
"explain_change_request",
|
|
1958
|
-
"read_scalequality_guide",
|
|
1959
|
-
"list_repositories",
|
|
1960
|
-
"open_repository",
|
|
1961
|
-
"measure_change"
|
|
1962
|
-
]);
|
|
1963
|
-
var BASH_MAX_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
1964
|
-
var CLOUD_BASH_DENY = [
|
|
1965
|
-
{ re: /\bgit\b[^\n;&|]*\s(push|send-pack|send-email|request-pull)\b/, why: "Publishing from the workspace is not allowed. Use the open_pull_request tool; it asks the user for approval." },
|
|
1966
|
-
{ re: /\bgit\b[^\n;&|]*\sremote\b/, why: "Git remotes are managed by ScaleQuality and cannot be read or changed from the workspace." }
|
|
1967
|
-
];
|
|
1968
|
-
var BASH_DENY = [
|
|
1969
|
-
{ re: /\bgit\b[^\n;&|]*\scredential\b/, why: "Git credentials are not available in the workspace." },
|
|
1970
|
-
{ re: /\bgit\b[^\n;&|]*\sconfig\b[^\n;&|]*(credential|insteadof|pushinsteadof|extraheader|\bremote\.|\burl\.)/, why: "Git credential and remote settings cannot be changed from the workspace." },
|
|
1971
|
-
{ re: /credential[._-]?helper/, why: "Git credential helpers cannot be configured in the workspace." },
|
|
1972
|
-
{ re: /\/proc\/[^\s'"]*\/(environ|mem)\b/, why: "Process environments are not readable from the workspace." },
|
|
1973
|
-
{ re: /\b(INTERNAL_API_SECRET|WORKSPACE_SESSION_SECRET|ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|ANTHROPIC_BASE_URL)\b/, why: "Workspace credentials are not available to commands." },
|
|
1974
|
-
{ re: /(^|[;&|(`]|\$\()\s*(env|printenv|export\s+-p|declare\s+-x|set)\s*($|[;&|)>`])/, why: "Dumping the whole environment is not allowed; read the specific variable your task needs." }
|
|
1975
|
-
];
|
|
1976
|
-
var HOME_PREFIX = String.raw`(~|\$\{?home\}?|\$\{?userprofile\}?|/users/[^/\s]+|/home/[^/\s]+|/root)/`;
|
|
1977
|
-
var LOCAL_BASH_DENY = [
|
|
1978
|
-
{ re: /(^|[\s=:/~])\.(ssh|aws|gnupg)(\/|\s|$)/i, why: "Credential directories on this machine are not readable from the workspace." },
|
|
1979
|
-
{ re: /(^|[\s=:/~])\.(netrc|git-credentials|pgpass)\b/i, why: "Credential files on this machine are not readable from the workspace." },
|
|
1980
|
-
// Projects have their own .npmrc or .docker folder; only the ones in the home directory hold credentials.
|
|
1981
|
-
{ re: new RegExp(`${HOME_PREFIX}\\.(npmrc|yarnrc\\.yml|pypirc|docker|kube|azure|config/(gh|hub|gcloud|op))\\b`, "i"), why: "Credential files on this machine are not readable from the workspace." },
|
|
1982
|
-
{ re: /(^|[;&|(`]|\$\()\s*(security\s+(find|dump|export)-|gh\s+auth\s+(token|status\s+-t\b|status\s+--show-token)|op\s+(read|item\s+get)|pass\s+show|secret-tool\s+lookup|aws\s+configure\s+(get|export-credentials)|gcloud\s+auth\s+print-)/i, why: "Credential stores on this machine are not readable from the workspace." }
|
|
1983
|
-
];
|
|
1984
|
-
function normalizeCommand(command) {
|
|
1985
|
-
return command.replace(/\\\n/g, " ").replace(/['"\\]/g, "").replace(/[ \t]+/g, " ").toLowerCase();
|
|
1986
|
-
}
|
|
1987
|
-
function bashDenial(command, opts = {}) {
|
|
1988
|
-
const n = normalizeCommand(command);
|
|
1989
|
-
for (const { re, why } of BASH_DENY) if (re.test(n) || re.test(command)) return why;
|
|
1990
|
-
if (!opts.local) {
|
|
1991
|
-
for (const { re, why } of CLOUD_BASH_DENY) if (re.test(n) || re.test(command)) return why;
|
|
1992
|
-
}
|
|
1993
|
-
if (opts.local) {
|
|
1994
|
-
for (const { re, why } of LOCAL_BASH_DENY) if (re.test(n) || re.test(command)) return why;
|
|
2566
|
+
// src/application/services/workspaceSandbox/localWorkspace.ts
|
|
2567
|
+
var EMPTY_TREE = {
|
|
2568
|
+
sha1: "4b825dc642cb6eb9a060e54bf8d69288fbee4904",
|
|
2569
|
+
sha256: "6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321"
|
|
2570
|
+
};
|
|
2571
|
+
var LocalWorkspaceError = class extends Error {
|
|
2572
|
+
constructor(code, publicMessage) {
|
|
2573
|
+
super(publicMessage);
|
|
2574
|
+
this.code = code;
|
|
2575
|
+
this.publicMessage = publicMessage;
|
|
2576
|
+
this.name = "LocalWorkspaceError";
|
|
1995
2577
|
}
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
const
|
|
2002
|
-
|
|
2003
|
-
return
|
|
2004
|
-
}
|
|
2005
|
-
function inside(root, abs) {
|
|
2006
|
-
return abs === root || abs.startsWith(root + import_path5.sep);
|
|
2578
|
+
code;
|
|
2579
|
+
publicMessage;
|
|
2580
|
+
};
|
|
2581
|
+
async function localFolderRoot(dir) {
|
|
2582
|
+
const abs = (0, import_path5.resolve)(dir);
|
|
2583
|
+
const st = await (0, import_promises6.stat)(abs).catch(() => null);
|
|
2584
|
+
if (!st || !st.isDirectory()) throw new LocalWorkspaceError("FOLDER_NOT_FOUND", `The folder ${abs} does not exist.`);
|
|
2585
|
+
return (0, import_promises6.realpath)(abs);
|
|
2007
2586
|
}
|
|
2008
|
-
var
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
"
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
"wc",
|
|
2015
|
-
"grep",
|
|
2016
|
-
"rg",
|
|
2017
|
-
"ag",
|
|
2018
|
-
"tree",
|
|
2019
|
-
"pwd",
|
|
2020
|
-
"file",
|
|
2021
|
-
"stat",
|
|
2022
|
-
"du",
|
|
2023
|
-
"df",
|
|
2024
|
-
"sort",
|
|
2025
|
-
"uniq",
|
|
2026
|
-
"cut",
|
|
2027
|
-
"tr",
|
|
2028
|
-
"which",
|
|
2029
|
-
"echo",
|
|
2030
|
-
"basename",
|
|
2031
|
-
"dirname",
|
|
2032
|
-
"realpath",
|
|
2033
|
-
"diff",
|
|
2034
|
-
"cloc",
|
|
2035
|
-
"jq",
|
|
2036
|
-
"less",
|
|
2037
|
-
"nl",
|
|
2038
|
-
"column",
|
|
2039
|
-
"date",
|
|
2040
|
-
"uname",
|
|
2041
|
-
"true"
|
|
2042
|
-
]);
|
|
2043
|
-
var READ_ONLY_GIT = /* @__PURE__ */ new Set(["status", "log", "show", "diff", "blame", "branch", "ls-files", "ls-tree", "grep", "rev-parse", "describe", "shortlog", "tag", "cat-file", "reflog"]);
|
|
2044
|
-
function readOnlyCommand(command) {
|
|
2045
|
-
if (/[<>`]|\$\(|\$\{/.test(command)) return false;
|
|
2046
|
-
const segments2 = command.split(/\|\||&&|\||;/).map((s) => s.trim()).filter(Boolean);
|
|
2047
|
-
if (!segments2.length) return false;
|
|
2048
|
-
return segments2.every((seg) => {
|
|
2049
|
-
const words = seg.split(/\s+/);
|
|
2050
|
-
let i = 0;
|
|
2051
|
-
while (i < words.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[i])) i++;
|
|
2052
|
-
const program = words[i] ?? "";
|
|
2053
|
-
const rest = words.slice(i + 1);
|
|
2054
|
-
if (program === "git") {
|
|
2055
|
-
const sub = rest.find((w) => !w.startsWith("-"));
|
|
2056
|
-
if (!sub || !READ_ONLY_GIT.has(sub)) return false;
|
|
2057
|
-
if (sub === "branch" || sub === "tag") return rest.slice(rest.indexOf(sub) + 1).every((w) => /^(-a|-r|-v|-vv|-l|--list|--all|--show-current|--sort=\S+|--contains|--merged|--no-merged)$/.test(w));
|
|
2058
|
-
return !rest.some((w) => /^(--output|--open-files-in-pager|-O|--ext-diff)(=|$)/.test(w) || /^-O\S/.test(w));
|
|
2059
|
-
}
|
|
2060
|
-
if (program === "find") return !rest.some((w) => /^-(delete|exec|execdir|ok|okdir|fprint|fprint0|fprintf|fls)$/.test(w));
|
|
2061
|
-
if (program === "rg") return !rest.some((w) => /^--pre(=|$)/.test(w));
|
|
2062
|
-
if (program === "sort" || program === "tree") return !rest.some((w) => /^(-o|--output)/.test(w));
|
|
2063
|
-
if (program === "uniq") return rest.filter((w) => !w.startsWith("-")).length <= 1;
|
|
2064
|
-
return READ_ONLY_PROGRAMS.has(program);
|
|
2587
|
+
var posix2 = (p) => p.split(import_path5.sep).join("/");
|
|
2588
|
+
async function enclosingRepository(root) {
|
|
2589
|
+
let missing = false;
|
|
2590
|
+
const top = await git(["rev-parse", "--show-toplevel"], { cwd: root }).then((o) => o.trim(), (e) => {
|
|
2591
|
+
if (e?.code === "ENOENT") missing = true;
|
|
2592
|
+
return "";
|
|
2065
2593
|
});
|
|
2594
|
+
if (missing) return "gitMissing";
|
|
2595
|
+
if (!top) return null;
|
|
2596
|
+
const realTop = await (0, import_promises6.realpath)(top).catch(() => top);
|
|
2597
|
+
if (realTop === root) return { top: realTop, prefix: "" };
|
|
2598
|
+
const prefix = posix2((0, import_path5.relative)(realTop, root));
|
|
2599
|
+
if (!prefix || prefix.startsWith("..")) return null;
|
|
2600
|
+
const env = { GIT_LITERAL_PATHSPECS: "1" };
|
|
2601
|
+
const inHead = (await git(["ls-tree", "-d", "--name-only", "HEAD", "--", prefix], { cwd: realTop, env }).catch(() => "")).trim().length > 0;
|
|
2602
|
+
if (inHead) return { top: realTop, prefix };
|
|
2603
|
+
const home = await (0, import_promises6.realpath)((0, import_os3.homedir)()).catch(() => (0, import_os3.homedir)());
|
|
2604
|
+
if (realTop === home || (0, import_path5.relative)(realTop, home).split(import_path5.sep)[0] !== "..") return null;
|
|
2605
|
+
const ignored = await git(["check-ignore", "-q", "--", prefix], { cwd: realTop, env }).then(() => true, () => false);
|
|
2606
|
+
return ignored ? null : { top: realTop, prefix };
|
|
2066
2607
|
}
|
|
2067
|
-
async function
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2608
|
+
async function repoHead(top) {
|
|
2609
|
+
const head = (await git(["rev-parse", "--verify", "-q", "HEAD^{commit}"], { cwd: top }).catch(() => "")).trim();
|
|
2610
|
+
let baseRevision = head;
|
|
2611
|
+
let baseKind = "commit";
|
|
2612
|
+
if (!head) {
|
|
2613
|
+
const format = (await git(["rev-parse", "--show-object-format"], { cwd: top }).catch(() => "sha1")).trim();
|
|
2614
|
+
baseRevision = EMPTY_TREE[format] ?? EMPTY_TREE.sha1;
|
|
2615
|
+
baseKind = "empty-tree";
|
|
2071
2616
|
}
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2617
|
+
const branch2 = (await git(["symbolic-ref", "--short", "-q", "HEAD"], { cwd: top }).catch(() => "")).trim() || null;
|
|
2618
|
+
const originUrl = (await git(["config", "--get", "remote.origin.url"], { cwd: top }).catch(() => "")).trim() || null;
|
|
2619
|
+
return { baseRevision, baseKind, branch: branch2, originUrl: originUrl ? stripUserinfo(originUrl) : null };
|
|
2620
|
+
}
|
|
2621
|
+
async function nestedRepo(folder, dir) {
|
|
2622
|
+
const real = await (0, import_promises6.realpath)(dir).catch(() => null);
|
|
2623
|
+
if (!real) return null;
|
|
2624
|
+
const top = (await git(["rev-parse", "--show-toplevel"], { cwd: real }).catch(() => "")).trim();
|
|
2625
|
+
if (!top || await (0, import_promises6.realpath)(top).catch(() => top) !== real) return null;
|
|
2626
|
+
return { root: real, rel: posix2((0, import_path5.relative)(folder, real)), ...await repoHead(real) };
|
|
2627
|
+
}
|
|
2628
|
+
async function inspectLocalFolder(dir, opts = {}) {
|
|
2629
|
+
const root = await localFolderRoot(dir);
|
|
2630
|
+
const owner = await enclosingRepository(root);
|
|
2631
|
+
if (owner === null || owner === "gitMissing") {
|
|
2632
|
+
const gitAvailable = owner !== "gitMissing";
|
|
2633
|
+
const found = gitAvailable ? await discoverRepositories(root, opts.discovery) : { repos: [], truncated: false, scannedDirs: 0 };
|
|
2634
|
+
const repos = (await mapLimit(found.repos, 8, (d) => nestedRepo(root, d).catch(() => null))).filter((r) => !!r);
|
|
2635
|
+
return {
|
|
2636
|
+
kind: "folder",
|
|
2637
|
+
root,
|
|
2638
|
+
repoRoot: null,
|
|
2639
|
+
prefix: "",
|
|
2640
|
+
baseRevision: "",
|
|
2641
|
+
baseKind: "commit",
|
|
2642
|
+
branch: null,
|
|
2643
|
+
changedAtStart: 0,
|
|
2644
|
+
originUrl: null,
|
|
2645
|
+
headOnRemote: null,
|
|
2646
|
+
repos,
|
|
2647
|
+
reposTruncated: found.truncated,
|
|
2648
|
+
gitAvailable
|
|
2649
|
+
};
|
|
2076
2650
|
}
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2651
|
+
const { top, prefix } = owner;
|
|
2652
|
+
const head = await repoHead(top);
|
|
2653
|
+
const status = await git(
|
|
2654
|
+
["--no-optional-locks", "status", "--porcelain", "-z", "--untracked-files=all", ...prefix ? ["--", prefix] : []],
|
|
2655
|
+
{ cwd: top, timeoutMs: 5 * 6e4, env: { GIT_LITERAL_PATHSPECS: "1" } }
|
|
2656
|
+
).catch(() => "");
|
|
2657
|
+
const changedAtStart = countPorcelainZ(status);
|
|
2658
|
+
let headOnRemote = null;
|
|
2659
|
+
const anyRemoteBranch = (await git(["branch", "-r"], { cwd: top }).catch(() => "")).trim().length > 0;
|
|
2660
|
+
if (head.baseKind === "commit" && anyRemoteBranch) {
|
|
2661
|
+
const remotes = await git(["branch", "-r", "--contains", head.baseRevision], { cwd: top }).catch(() => null);
|
|
2662
|
+
headOnRemote = remotes === null ? null : remotes.trim().length > 0;
|
|
2083
2663
|
}
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2664
|
+
return { kind: prefix ? "subfolder" : "repo", root, repoRoot: top, prefix, ...head, changedAtStart, headOnRemote, repos: [], reposTruncated: false, gitAvailable: true };
|
|
2665
|
+
}
|
|
2666
|
+
function countPorcelainZ(out2) {
|
|
2667
|
+
const recs = out2.split("\0");
|
|
2668
|
+
let n = 0;
|
|
2669
|
+
for (let i = 0; i < recs.length; i++) {
|
|
2670
|
+
const r = recs[i];
|
|
2671
|
+
if (!r || r.length < 4) continue;
|
|
2672
|
+
n++;
|
|
2673
|
+
if (r[0] === "R" || r[0] === "C") i++;
|
|
2087
2674
|
}
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2675
|
+
return n;
|
|
2676
|
+
}
|
|
2677
|
+
function remotePathSegments(url) {
|
|
2678
|
+
const u = url.trim();
|
|
2679
|
+
const scp = /^(?:[^@\s/]+@)?([^:\s/]+):(?!\/\/)(.+)$/.exec(u);
|
|
2680
|
+
let host = "";
|
|
2681
|
+
let path = u;
|
|
2682
|
+
if (scp && !/^[a-z][a-z0-9+.-]*:\/\//i.test(u)) {
|
|
2683
|
+
host = scp[1];
|
|
2684
|
+
path = scp[2];
|
|
2685
|
+
} else {
|
|
2686
|
+
try {
|
|
2687
|
+
const parsed = new URL(u);
|
|
2688
|
+
host = parsed.hostname;
|
|
2689
|
+
path = parsed.pathname;
|
|
2690
|
+
} catch {
|
|
2691
|
+
path = u;
|
|
2094
2692
|
}
|
|
2095
2693
|
}
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
return { behavior: "allow", updatedInput: input };
|
|
2102
|
-
}
|
|
2103
|
-
if (typeof raw !== "string" || raw.length === 0) return { behavior: "deny", message: `${toolName} needs a path inside the repository.` };
|
|
2104
|
-
const abs = await resolveInside(ctx.root, raw);
|
|
2105
|
-
let denied = false;
|
|
2106
|
-
for (const d of ctx.deniedRoots ?? []) {
|
|
2107
|
-
const realDenied = await (0, import_promises6.realpath)(d).catch(() => (0, import_path5.resolve)(d));
|
|
2108
|
-
if (abs && inside(realDenied, abs)) denied = true;
|
|
2109
|
-
}
|
|
2110
|
-
if (abs && !denied) {
|
|
2111
|
-
if (toolName in WRITE_TOOLS) {
|
|
2112
|
-
const realRoot = await (0, import_promises6.realpath)(ctx.root).catch(() => (0, import_path5.resolve)(ctx.root));
|
|
2113
|
-
const rel = (0, import_path5.relative)(realRoot, abs).split(import_path5.sep);
|
|
2114
|
-
if (rel.includes(".git")) return { behavior: "deny", message: "Files under .git cannot be written from the workspace." };
|
|
2115
|
-
}
|
|
2116
|
-
return { behavior: "allow", updatedInput: input };
|
|
2117
|
-
}
|
|
2118
|
-
if (toolName in READ_TOOLS) {
|
|
2119
|
-
for (const extra of ctx.extraReadRoots ?? []) {
|
|
2120
|
-
const e = await resolveInside(extra, raw);
|
|
2121
|
-
const realExtra = await (0, import_promises6.realpath)(extra).catch(() => (0, import_path5.resolve)(extra));
|
|
2122
|
-
if (e && inside(realExtra, e)) return { behavior: "allow", updatedInput: input };
|
|
2123
|
-
}
|
|
2694
|
+
const segments2 = path.split("/").map((s) => {
|
|
2695
|
+
try {
|
|
2696
|
+
return decodeURIComponent(s);
|
|
2697
|
+
} catch {
|
|
2698
|
+
return s;
|
|
2124
2699
|
}
|
|
2125
|
-
|
|
2700
|
+
}).map((s) => s.toLowerCase().replace(/\.git$/, "")).filter((s) => s && s !== "_git" && s !== "v3");
|
|
2701
|
+
return { host: host.toLowerCase(), segments: segments2 };
|
|
2702
|
+
}
|
|
2703
|
+
var PROVIDER_HOSTS = [[/github/, "GITHUB"], [/gitlab/, "GITLAB"], [/bitbucket/, "BITBUCKET"], [/(dev\.azure|visualstudio)/, "AZURE"]];
|
|
2704
|
+
function matchRemoteToScope(originUrl, repos) {
|
|
2705
|
+
if (!originUrl) return null;
|
|
2706
|
+
const { host, segments: segments2 } = remotePathSegments(originUrl);
|
|
2707
|
+
if (!segments2.length) return null;
|
|
2708
|
+
const provider = PROVIDER_HOSTS.find(([re]) => re.test(host))?.[1] ?? null;
|
|
2709
|
+
let best = [];
|
|
2710
|
+
let bestLen = 0;
|
|
2711
|
+
for (const r of repos) {
|
|
2712
|
+
const rs = r.repoFullName.toLowerCase().split("/").map((s) => s.replace(/\.git$/, "")).filter(Boolean);
|
|
2713
|
+
if (!rs.length || rs.length > segments2.length) continue;
|
|
2714
|
+
const offset = segments2.length - rs.length;
|
|
2715
|
+
if (!rs.every((s, i) => s === segments2[offset + i])) continue;
|
|
2716
|
+
if (rs.length > bestLen) {
|
|
2717
|
+
best = [r];
|
|
2718
|
+
bestLen = rs.length;
|
|
2719
|
+
} else if (rs.length === bestLen) best.push(r);
|
|
2126
2720
|
}
|
|
2127
|
-
if (
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2721
|
+
if (best.length > 1 && provider) best = best.filter((r) => r.provider.toUpperCase().startsWith(provider));
|
|
2722
|
+
return best.length === 1 ? best[0] : null;
|
|
2723
|
+
}
|
|
2724
|
+
function localFolderRepo(originUrl, repos, linkedProjectId) {
|
|
2725
|
+
const linked = linkedProjectId ? repos.filter((r) => r.projectId === linkedProjectId) : [];
|
|
2726
|
+
if (linked.length) return { repo: matchRemoteToScope(originUrl, linked) ?? (linked.length === 1 ? linked[0] : null), linked };
|
|
2727
|
+
return { repo: matchRemoteToScope(originUrl, repos), linked: [] };
|
|
2728
|
+
}
|
|
2729
|
+
function bootScopeRepos(boot) {
|
|
2730
|
+
if (boot.scope?.repos) return boot.scope.repos;
|
|
2731
|
+
if (boot.repositories) return boot.repositories;
|
|
2732
|
+
return boot.repo ? [{ repoFullName: boot.repo.repoFullName, provider: boot.repo.provider, projectId: boot.projectId, defaultBranch: boot.repo.defaultBranch }] : [];
|
|
2733
|
+
}
|
|
2734
|
+
function stripUserinfo(url) {
|
|
2735
|
+
return url.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^@/]+@/i, "$1");
|
|
2736
|
+
}
|
|
2737
|
+
async function prepareLocalWorkspace(dir, _boot, onStep) {
|
|
2738
|
+
const t0 = Date.now();
|
|
2739
|
+
onStep?.("Reading the local folder");
|
|
2740
|
+
const local = await inspectLocalFolder(dir);
|
|
2741
|
+
return {
|
|
2742
|
+
baseRevision: local.baseRevision,
|
|
2743
|
+
baseKind: local.baseKind,
|
|
2744
|
+
branch: local.branch ?? "HEAD",
|
|
2745
|
+
restore: "none",
|
|
2746
|
+
originUrl: local.originUrl,
|
|
2747
|
+
timings: { local_folder: Date.now() - t0 },
|
|
2748
|
+
local
|
|
2749
|
+
};
|
|
2750
|
+
}
|
|
2751
|
+
function localWarnings(local, boot) {
|
|
2752
|
+
const out2 = [];
|
|
2753
|
+
if (local.kind === "folder") {
|
|
2754
|
+
if (local.reposTruncated) out2.push(`The folder is large: the repositories inside it were looked for up to ${DISCOVERY_LIMITS.maxDepth} levels down, for a few seconds. Others are still available; their changes are tracked when you edit them here.`);
|
|
2755
|
+
return out2;
|
|
2138
2756
|
}
|
|
2139
|
-
|
|
2140
|
-
|
|
2757
|
+
const { repo: match, linked } = localFolderRepo(local.originUrl, bootScopeRepos(boot), boot.folderLink?.projectId);
|
|
2758
|
+
const sessionBranch = match ? boot.repo?.repoFullName === match.repoFullName ? boot.branch || boot.repo.defaultBranch : match.defaultBranch : null;
|
|
2759
|
+
if (sessionBranch && local.branch && local.branch !== sessionBranch) {
|
|
2760
|
+
out2.push(`This folder is on branch "${local.branch}", and the session targets "${sessionBranch}". A pull request is opened against "${sessionBranch}" and only when it is at the same commit as this folder.`);
|
|
2141
2761
|
}
|
|
2142
|
-
|
|
2762
|
+
if (!local.branch) out2.push("This folder is on a detached HEAD.");
|
|
2763
|
+
if (local.headOnRemote === false) out2.push("HEAD has commits that are not on any remote branch this folder knows about. Push them first if you plan to open a pull request from this session.");
|
|
2764
|
+
if (local.changedAtStart > 0) out2.push(`${local.changedAtStart} file(s) already differ from HEAD. They are part of this session's change.`);
|
|
2765
|
+
if (linked.length) {
|
|
2766
|
+
if (!match) out2.push(`This folder is linked to a project with several repositories (${linked.map((r) => r.repoFullName).join(", ")}). A pull request names the one it goes to.`);
|
|
2767
|
+
return out2;
|
|
2768
|
+
}
|
|
2769
|
+
if (local.originUrl && !match) {
|
|
2770
|
+
out2.push(`The origin remote (${local.originUrl}) is not a repository in this session's scope. You can work on the code here; a pull request cannot be opened from this folder.`);
|
|
2771
|
+
}
|
|
2772
|
+
if (!local.originUrl) out2.push("This folder has no origin remote, so it is not matched to a repository in this session's scope. You can work on the code here; a pull request cannot be opened from this folder.");
|
|
2773
|
+
return out2;
|
|
2143
2774
|
}
|
|
2144
2775
|
|
|
2145
2776
|
// src/application/services/workspaceSandbox/commandRules.ts
|
|
@@ -2395,13 +3026,19 @@ var BrowserCommandGate = class {
|
|
|
2395
3026
|
this.chain = next.catch(() => void 0);
|
|
2396
3027
|
return next;
|
|
2397
3028
|
}
|
|
3029
|
+
checkProtected(command, opts) {
|
|
3030
|
+
const next = this.chain.then(() => this.ask(command, { description: opts.description, signal: opts.signal, protection: opts }));
|
|
3031
|
+
this.chain = next.catch(() => void 0);
|
|
3032
|
+
return next;
|
|
3033
|
+
}
|
|
2398
3034
|
checkEdit(path, opts = {}) {
|
|
2399
3035
|
const next = this.chain.then(() => this.ask(`edit ${path}`, { signal: opts.signal, edit: { path, ...opts.preview ? { preview: opts.preview } : {} } }));
|
|
2400
3036
|
this.chain = next.catch(() => void 0);
|
|
2401
3037
|
return next;
|
|
2402
3038
|
}
|
|
2403
3039
|
async ask(command, opts) {
|
|
2404
|
-
|
|
3040
|
+
const protection = opts.protection;
|
|
3041
|
+
if (!opts.edit && !protection && matchingRule(command, this.allRules())) return { allow: true };
|
|
2405
3042
|
if (opts.signal?.aborted) return { allow: false, message: STOPPED };
|
|
2406
3043
|
let approvalId;
|
|
2407
3044
|
let approved = false;
|
|
@@ -2410,19 +3047,21 @@ var BrowserCommandGate = class {
|
|
|
2410
3047
|
command,
|
|
2411
3048
|
cwd: this.o.root,
|
|
2412
3049
|
...opts.description ? { description: opts.description.slice(0, 1e3) } : {},
|
|
2413
|
-
reasons: opts.edit ? [] : approvalReasons(command),
|
|
2414
|
-
suggestedRule: opts.edit ? null : suggestRule(command),
|
|
2415
|
-
...opts.edit ? { edit: { path: opts.edit.path, ...opts.edit.preview ? { preview: opts.edit.preview.slice(0, 4e3) } : {} } } : {}
|
|
3050
|
+
reasons: opts.edit ? [] : protection ? [...protection.reasons, ...approvalReasons(command)] : approvalReasons(command),
|
|
3051
|
+
suggestedRule: opts.edit || protection ? null : suggestRule(command),
|
|
3052
|
+
...opts.edit ? { edit: { path: opts.edit.path, ...opts.edit.preview ? { preview: opts.edit.preview.slice(0, 4e3) } : {} } } : {},
|
|
3053
|
+
...protection?.package ? { package: protection.package, ...protection.packageReason ? { packageReason: protection.packageReason } : {} } : {}
|
|
2416
3054
|
}));
|
|
2417
3055
|
} catch (e) {
|
|
2418
3056
|
this.o.log?.(`command approval could not be requested: ${e.message}`);
|
|
2419
3057
|
const code = e instanceof CommandApprovalError ? e.code : null;
|
|
3058
|
+
if (protection && code !== "COMMAND_NOT_ALLOWED") return { allow: false, message: PROTECTION_MESSAGES.unavailable };
|
|
2420
3059
|
return {
|
|
2421
3060
|
allow: false,
|
|
2422
3061
|
message: code === "COMMAND_NOT_ALLOWED" ? "This command is not allowed in the ScaleQuality workspace. It did not run; do not try to run it another way." : "The command could not be sent to the user for approval, so it did not run. Tell the user; do not try to run it another way."
|
|
2423
3062
|
};
|
|
2424
3063
|
}
|
|
2425
|
-
if (approved) return { allow: true };
|
|
3064
|
+
if (approved && !protection) return { allow: true };
|
|
2426
3065
|
this.o.onWaiting?.(command);
|
|
2427
3066
|
const timeoutMs = this.o.timeoutMs ?? COMMAND_APPROVAL_TIMEOUT_MS;
|
|
2428
3067
|
const timer = new AbortController();
|
|
@@ -2433,12 +3072,14 @@ var BrowserCommandGate = class {
|
|
|
2433
3072
|
if (!decision) {
|
|
2434
3073
|
const reason = opts.signal?.aborted ? "STOPPED" : "TIMED_OUT";
|
|
2435
3074
|
void this.o.api.cancel(approvalId, reason).catch(() => void 0);
|
|
3075
|
+
if (protection) return { allow: false, message: reason === "STOPPED" ? PROTECTION_MESSAGES.stopped : PROTECTION_MESSAGES.timedOut(Math.round(timeoutMs / 6e4)) };
|
|
2436
3076
|
return reason === "STOPPED" ? { allow: false, message: STOPPED } : { allow: false, message: `The user did not answer the approval for this command within ${Math.round(timeoutMs / 6e4)} minutes, so it did not run. Ask the user before trying it again.` };
|
|
2437
3077
|
}
|
|
2438
3078
|
if (decision.decision === "REJECT") {
|
|
3079
|
+
if (protection) return { allow: false, message: PROTECTION_MESSAGES.rejected };
|
|
2439
3080
|
return opts.edit ? { allow: false, message: `The user declined this change to ${opts.edit.path} in the browser. The file was not changed. Ask the user what to do differently.` } : { allow: false, message: "The user denied this command in the browser. It did not run. Do not run the same command again unless the user asks; continue another way or ask the user." };
|
|
2440
3081
|
}
|
|
2441
|
-
if (decision.remember) {
|
|
3082
|
+
if (decision.remember && !protection) {
|
|
2442
3083
|
if (this.o.onRemember) this.o.onRemember(decision.remember);
|
|
2443
3084
|
else this.remembered.push(decision.remember);
|
|
2444
3085
|
}
|
|
@@ -4277,6 +4918,9 @@ var ENGINE_MESSAGES = {
|
|
|
4277
4918
|
MODEL_SWITCH_FAILED: "The model could not be changed now; this request uses {model}.",
|
|
4278
4919
|
MODEL_SWITCHED: "Now using {model}.",
|
|
4279
4920
|
GATEWAY_REFUSED: "The AI gateway refused this request ({code}).",
|
|
4921
|
+
// The AI Workspace's own credit (gateway BUDGET codes): said as credit, not as a budget the organization set.
|
|
4922
|
+
MANAGED_ALLOWANCE_EXHAUSTED: "The AI Workspace credit ran out ({code}).",
|
|
4923
|
+
MANAGED_PREPAID_REQUIRED: "This model uses purchased credit, and the purchased credit ran out ({code}).",
|
|
4280
4924
|
// Files, rewind and hunks.
|
|
4281
4925
|
DISCARD_FAILED: "The change to that file could not be discarded.",
|
|
4282
4926
|
DISCARD_REPOSITORY_REQUIRED: "Say which repository the file belongs to.",
|
|
@@ -4324,10 +4968,66 @@ function engineMessage(code, params) {
|
|
|
4324
4968
|
return text3.replace(/\{(\w+)\}/g, (_m, k) => k === "code" && !(params && k in params) ? code : params && params[k] !== void 0 && params[k] !== null ? String(params[k]) : "");
|
|
4325
4969
|
}
|
|
4326
4970
|
var SQ_CODE = /"scalequality_code"\s*:\s*"([A-Z][A-Z0-9_]{2,79})"/;
|
|
4971
|
+
var GATEWAY_CODE_CATEGORIES = Object.freeze({
|
|
4972
|
+
...Object.fromEntries([
|
|
4973
|
+
"BUDGET_EXHAUSTED",
|
|
4974
|
+
"ORGANIZATION_BUDGET_EXHAUSTED",
|
|
4975
|
+
"ENVIRONMENT_BUDGET_EXHAUSTED",
|
|
4976
|
+
"TEAM_BUDGET_EXHAUSTED",
|
|
4977
|
+
"BUSINESS_UNIT_BUDGET_EXHAUSTED",
|
|
4978
|
+
"AREA_BUDGET_EXHAUSTED",
|
|
4979
|
+
"MODEL_BUDGET_EXHAUSTED",
|
|
4980
|
+
"MANAGED_ALLOWANCE_EXHAUSTED",
|
|
4981
|
+
"MANAGED_PREPAID_REQUIRED"
|
|
4982
|
+
].map((c) => [c, "BUDGET"])),
|
|
4983
|
+
PERSON_BUDGET_EXHAUSTED: "PERSON_BUDGET",
|
|
4984
|
+
CONCURRENCY_LIMIT: "CONCURRENCY",
|
|
4985
|
+
ORGANIZATION_CONCURRENCY_LIMIT: "CONCURRENCY",
|
|
4986
|
+
...Object.fromEntries([
|
|
4987
|
+
"INPUT_POLICY_BLOCK",
|
|
4988
|
+
"OUTPUT_POLICY_BLOCK",
|
|
4989
|
+
"SEMANTIC_INPUT_BLOCK",
|
|
4990
|
+
"SEMANTIC_OUTPUT_BLOCK",
|
|
4991
|
+
"TOOL_OUTPUT_POLICY_BLOCK",
|
|
4992
|
+
"TOOL_OUTPUT_NOT_ALLOWED",
|
|
4993
|
+
"TOOL_OUTPUT_INVALID",
|
|
4994
|
+
"PROTECTION_PROMPT_ATTACK",
|
|
4995
|
+
"PROTECTION_INDIRECT_INJECTION",
|
|
4996
|
+
"PROTECTION_HARMFUL_CONTENT",
|
|
4997
|
+
"PROTECTION_PROHIBITED_PRACTICE",
|
|
4998
|
+
"PROTECTION_SENSITIVE_INFERENCE",
|
|
4999
|
+
"PROTECTION_COMPANY_TOPIC",
|
|
5000
|
+
"PROTECTION_UNGROUNDED_ANSWER"
|
|
5001
|
+
].map((c) => [c, "POLICY"])),
|
|
5002
|
+
...Object.fromEntries([
|
|
5003
|
+
"MODEL_ALIAS_NOT_ALLOWED",
|
|
5004
|
+
"NO_PUBLISHED_POLICY",
|
|
5005
|
+
"MODEL_DISABLED_BY_ORGANIZATION",
|
|
5006
|
+
"MODEL_EXCLUDED_FROM_AUTO",
|
|
5007
|
+
"MANAGED_MODEL_PLAN_RESTRICTED",
|
|
5008
|
+
"MANAGED_ROUTE_NOT_ALLOWED",
|
|
5009
|
+
"MANAGED_CODE_PROFILE_REQUIRED",
|
|
5010
|
+
"CONNECTION_NOT_AVAILABLE"
|
|
5011
|
+
].map((c) => [c, "MODEL_ACCESS"])),
|
|
5012
|
+
...Object.fromEntries([
|
|
5013
|
+
"MANAGED_NOT_AVAILABLE",
|
|
5014
|
+
"WORKSPACE_ENTITLEMENT_REQUIRED",
|
|
5015
|
+
"MANAGED_ENTITLEMENT_REQUIRED",
|
|
5016
|
+
"TRIAL_UPGRADE_REQUIRED",
|
|
5017
|
+
"WORKSPACE_ACTIVE_PEOPLE_LIMIT"
|
|
5018
|
+
].map((c) => [c, "ENTITLEMENT"])),
|
|
5019
|
+
PROVIDER_RATE_LIMITED: "PROVIDER"
|
|
5020
|
+
});
|
|
5021
|
+
var BARE_CODE = /API Error:\s*(\d{3})\s+([A-Z][A-Z0-9_]{2,79})(?![A-Za-z0-9_])/;
|
|
5022
|
+
function bareGatewayError(text3) {
|
|
5023
|
+
const m = BARE_CODE.exec(text3);
|
|
5024
|
+
const category = m ? GATEWAY_CODE_CATEGORIES[m[2]] : void 0;
|
|
5025
|
+
return m && category ? { code: m[2], params: { status: Number(m[1]), category } } : null;
|
|
5026
|
+
}
|
|
4327
5027
|
function gatewayError(text3) {
|
|
4328
5028
|
if (!text3) return null;
|
|
4329
5029
|
const m = SQ_CODE.exec(text3);
|
|
4330
|
-
if (!m) return
|
|
5030
|
+
if (!m) return bareGatewayError(text3);
|
|
4331
5031
|
const params = {};
|
|
4332
5032
|
const status = /API Error:\s*(\d{3})/.exec(text3);
|
|
4333
5033
|
if (status) params.status = Number(status[1]);
|
|
@@ -4476,7 +5176,7 @@ function limitInEngineText(text3) {
|
|
|
4476
5176
|
// src/application/services/workspaceSandbox/scalequalityTools.ts
|
|
4477
5177
|
var import_crypto4 = require("crypto");
|
|
4478
5178
|
|
|
4479
|
-
//
|
|
5179
|
+
// node_modules/zod/v3/external.js
|
|
4480
5180
|
var external_exports = {};
|
|
4481
5181
|
__export(external_exports, {
|
|
4482
5182
|
BRAND: () => BRAND,
|
|
@@ -4588,7 +5288,7 @@ __export(external_exports, {
|
|
|
4588
5288
|
void: () => voidType
|
|
4589
5289
|
});
|
|
4590
5290
|
|
|
4591
|
-
//
|
|
5291
|
+
// node_modules/zod/v3/helpers/util.js
|
|
4592
5292
|
var util;
|
|
4593
5293
|
(function(util2) {
|
|
4594
5294
|
util2.assertEqual = (_) => {
|
|
@@ -4722,7 +5422,7 @@ var getParsedType = (data) => {
|
|
|
4722
5422
|
}
|
|
4723
5423
|
};
|
|
4724
5424
|
|
|
4725
|
-
//
|
|
5425
|
+
// node_modules/zod/v3/ZodError.js
|
|
4726
5426
|
var ZodIssueCode = util.arrayToEnum([
|
|
4727
5427
|
"invalid_type",
|
|
4728
5428
|
"invalid_literal",
|
|
@@ -4840,7 +5540,7 @@ ZodError.create = (issues) => {
|
|
|
4840
5540
|
return error;
|
|
4841
5541
|
};
|
|
4842
5542
|
|
|
4843
|
-
//
|
|
5543
|
+
// node_modules/zod/v3/locales/en.js
|
|
4844
5544
|
var errorMap = (issue, _ctx) => {
|
|
4845
5545
|
let message;
|
|
4846
5546
|
switch (issue.code) {
|
|
@@ -4943,7 +5643,7 @@ var errorMap = (issue, _ctx) => {
|
|
|
4943
5643
|
};
|
|
4944
5644
|
var en_default = errorMap;
|
|
4945
5645
|
|
|
4946
|
-
//
|
|
5646
|
+
// node_modules/zod/v3/errors.js
|
|
4947
5647
|
var overrideErrorMap = en_default;
|
|
4948
5648
|
function setErrorMap(map) {
|
|
4949
5649
|
overrideErrorMap = map;
|
|
@@ -4952,7 +5652,7 @@ function getErrorMap() {
|
|
|
4952
5652
|
return overrideErrorMap;
|
|
4953
5653
|
}
|
|
4954
5654
|
|
|
4955
|
-
//
|
|
5655
|
+
// node_modules/zod/v3/helpers/parseUtil.js
|
|
4956
5656
|
var makeIssue = (params) => {
|
|
4957
5657
|
const { data, path, errorMaps, issueData } = params;
|
|
4958
5658
|
const fullPath = [...path, ...issueData.path || []];
|
|
@@ -5062,14 +5762,14 @@ var isDirty = (x) => x.status === "dirty";
|
|
|
5062
5762
|
var isValid = (x) => x.status === "valid";
|
|
5063
5763
|
var isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise;
|
|
5064
5764
|
|
|
5065
|
-
//
|
|
5765
|
+
// node_modules/zod/v3/helpers/errorUtil.js
|
|
5066
5766
|
var errorUtil;
|
|
5067
5767
|
(function(errorUtil2) {
|
|
5068
5768
|
errorUtil2.errToObj = (message) => typeof message === "string" ? { message } : message || {};
|
|
5069
5769
|
errorUtil2.toString = (message) => typeof message === "string" ? message : message?.message;
|
|
5070
5770
|
})(errorUtil || (errorUtil = {}));
|
|
5071
5771
|
|
|
5072
|
-
//
|
|
5772
|
+
// node_modules/zod/v3/types.js
|
|
5073
5773
|
var ParseInputLazyPath = class {
|
|
5074
5774
|
constructor(parent, value, path, key) {
|
|
5075
5775
|
this._cachedPath = [];
|
|
@@ -9297,6 +9997,51 @@ var TurnTiming = class {
|
|
|
9297
9997
|
}
|
|
9298
9998
|
};
|
|
9299
9999
|
|
|
10000
|
+
// src/application/services/workspaceSandbox/protectionApproval.ts
|
|
10001
|
+
function gateProtectionApprover(gate) {
|
|
10002
|
+
if (!gate.checkProtected) return null;
|
|
10003
|
+
return { ask: (command, check) => gate.checkProtected(command, check) };
|
|
10004
|
+
}
|
|
10005
|
+
function cloudProtectionApprover(o) {
|
|
10006
|
+
let chain = Promise.resolve();
|
|
10007
|
+
const ask = async (command, check) => {
|
|
10008
|
+
if (check.signal?.aborted) return { allow: false, message: PROTECTION_MESSAGES.stopped };
|
|
10009
|
+
let approvalId;
|
|
10010
|
+
try {
|
|
10011
|
+
({ approvalId } = await o.calls.requestCommandApproval({
|
|
10012
|
+
command,
|
|
10013
|
+
cwd: o.cwd,
|
|
10014
|
+
...check.description ? { description: check.description.slice(0, 1e3) } : {},
|
|
10015
|
+
reasons: [...check.reasons],
|
|
10016
|
+
suggestedRule: null,
|
|
10017
|
+
...check.package ? { package: check.package, ...check.packageReason ? { packageReason: check.packageReason } : {} } : {}
|
|
10018
|
+
}));
|
|
10019
|
+
} catch (e) {
|
|
10020
|
+
o.log?.(`protection approval could not be requested: ${e.message}`);
|
|
10021
|
+
return { allow: false, message: PROTECTION_MESSAGES.unavailable };
|
|
10022
|
+
}
|
|
10023
|
+
const timeoutMs = o.timeoutMs ?? COMMAND_APPROVAL_TIMEOUT_MS;
|
|
10024
|
+
const timer = new AbortController();
|
|
10025
|
+
const t = setTimeout(() => timer.abort(), timeoutMs);
|
|
10026
|
+
t.unref?.();
|
|
10027
|
+
const decision = await o.broker.wait(approvalId, check.signal ? anySignal([check.signal, timer.signal]) : timer.signal);
|
|
10028
|
+
clearTimeout(t);
|
|
10029
|
+
if (!decision) {
|
|
10030
|
+
const reason = check.signal?.aborted ? "STOPPED" : "TIMED_OUT";
|
|
10031
|
+
void o.calls.cancelCommandApproval(approvalId, reason).catch(() => void 0);
|
|
10032
|
+
return { allow: false, message: reason === "STOPPED" ? PROTECTION_MESSAGES.stopped : PROTECTION_MESSAGES.timedOut(Math.round(timeoutMs / 6e4)) };
|
|
10033
|
+
}
|
|
10034
|
+
return decision.decision === "APPROVE" ? { allow: true } : { allow: false, message: PROTECTION_MESSAGES.rejected };
|
|
10035
|
+
};
|
|
10036
|
+
return {
|
|
10037
|
+
ask(command, check) {
|
|
10038
|
+
const next = chain.then(() => ask(command, check));
|
|
10039
|
+
chain = next.catch(() => void 0);
|
|
10040
|
+
return next;
|
|
10041
|
+
}
|
|
10042
|
+
};
|
|
10043
|
+
}
|
|
10044
|
+
|
|
9300
10045
|
// src/application/services/workspaceSandbox/systemPrompt.ts
|
|
9301
10046
|
var FOLDER_LISTED = 60;
|
|
9302
10047
|
function layoutLines(root, l) {
|
|
@@ -9318,10 +10063,15 @@ function layoutLines(root, l) {
|
|
|
9318
10063
|
}
|
|
9319
10064
|
var LISTED = 30;
|
|
9320
10065
|
function scopeLine(c) {
|
|
9321
|
-
const what = c.scope.kind === "ALL" ? "everything the user can access in the organization" : c.scope.kind === "TEAM" ? "the projects of one team" : c.scope.kind === "BUSINESS_AREA" ?
|
|
10066
|
+
const what = c.scope.kind === "ALL" ? "everything the user can access in the organization" : c.scope.kind === "TEAM" ? "the projects of one team" : c.scope.kind === "BUSINESS_AREA" ? `the projects of the business area${c.scope.areaName ? ` "${c.scope.areaName}"` : ""} (the ones associated with it and the ones that measure its initiatives)` : c.scope.projectIds.length === 1 ? `project ${c.scope.projectIds[0]}` : `${c.scope.projectIds.length} projects`;
|
|
9322
10067
|
const n = c.scope.repos.length;
|
|
9323
10068
|
const names = c.scope.repos.slice(0, LISTED).map((r) => `${r.repoFullName} (${r.provider})`).join(", ");
|
|
9324
|
-
return `The session scope is ${what}: ${n === 0 ? "no repository" : `${n} repositor${n === 1 ? "y" : "ies"}: ${names}${n > LISTED ? ", ... (call list_repositories for all)" : ""}`}. ScaleQuality tools only act on projects and repositories of this scope
|
|
10069
|
+
return `The session scope is ${what}: ${n === 0 ? "no repository" : `${n} repositor${n === 1 ? "y" : "ies"}: ${names}${n > LISTED ? ", ... (call list_repositories for all)" : ""}`}. ScaleQuality tools only act on projects and repositories of this scope.${initiativeLine(c)}`;
|
|
10070
|
+
}
|
|
10071
|
+
function initiativeLine(c) {
|
|
10072
|
+
if (c.scope.kind !== "BUSINESS_AREA" || !c.scope.initiativeName) return "";
|
|
10073
|
+
const own = c.scope.initiativeProjectId ? c.scope.repos.filter((r) => r.projectId === c.scope.initiativeProjectId).map((r) => r.repoFullName) : [];
|
|
10074
|
+
return ` The user is working on the initiative "${c.scope.initiativeName}"${own.length ? `; its code is ${own.join(", ")} (the project that measures the initiative). Start there when the user talks about the initiative's code.` : "; no project measures this initiative yet."}`;
|
|
9325
10075
|
}
|
|
9326
10076
|
function openLine(c) {
|
|
9327
10077
|
if (!c.open.length) return "No repository is open yet.";
|
|
@@ -10041,6 +10791,9 @@ var WorkspaceEngine = class {
|
|
|
10041
10791
|
redactor = new Redactor();
|
|
10042
10792
|
sink;
|
|
10043
10793
|
broker = new ApprovalBroker();
|
|
10794
|
+
/** Registry answers of unknown-package, kept for the session. */
|
|
10795
|
+
registry = new RegistryChecker();
|
|
10796
|
+
approver;
|
|
10044
10797
|
boot = null;
|
|
10045
10798
|
scope = { kind: "PROJECTS", teamId: null, projectIds: [], repos: [] };
|
|
10046
10799
|
/** Open repositories by folder. */
|
|
@@ -10560,11 +11313,15 @@ var WorkspaceEngine = class {
|
|
|
10560
11313
|
})) : null;
|
|
10561
11314
|
if (!repos) return;
|
|
10562
11315
|
const kind = p.kind === "ALL" || p.kind === "TEAM" || p.kind === "BUSINESS_AREA" ? p.kind : "PROJECTS";
|
|
11316
|
+
const named = (v) => typeof v === "string" && v ? v : null;
|
|
10563
11317
|
this.scope = {
|
|
10564
11318
|
kind,
|
|
10565
11319
|
teamId: typeof p.teamId === "string" ? p.teamId : null,
|
|
10566
11320
|
projectIds: Array.isArray(p.projectIds) ? p.projectIds.filter((x) => typeof x === "string") : [],
|
|
10567
|
-
repos
|
|
11321
|
+
repos,
|
|
11322
|
+
areaName: named(p.areaName),
|
|
11323
|
+
initiativeName: named(p.initiativeName),
|
|
11324
|
+
initiativeProjectId: named(p.initiativeProjectId)
|
|
10568
11325
|
};
|
|
10569
11326
|
for (const repo2 of this.repos.values()) {
|
|
10570
11327
|
if (repo2.localId) {
|
|
@@ -10893,7 +11650,7 @@ ${String(item.content)}` : String(item.content);
|
|
|
10893
11650
|
if (!model || model === boot.model || !this.deps.transport.refreshRuntime) return;
|
|
10894
11651
|
const step = this.ownStep("model", "tool", `Switching to ${model}`, "STEP_MODEL_SWITCH", { model });
|
|
10895
11652
|
try {
|
|
10896
|
-
const fresh = await this.deps.transport.refreshRuntime();
|
|
11653
|
+
const fresh = await this.deps.transport.refreshRuntime({ model: boot.model });
|
|
10897
11654
|
if (!fresh.runtime.token || !fresh.runtime.baseUrl) throw new Error("RUNTIME_INCOMPLETE");
|
|
10898
11655
|
this.redactor.add(fresh.runtime.token);
|
|
10899
11656
|
boot.runtime = fresh.runtime;
|
|
@@ -10907,6 +11664,42 @@ ${String(item.content)}` : String(item.content);
|
|
|
10907
11664
|
this.error("MODEL_SWITCH_FAILED", { model: boot.runtime.primaryModel || boot.model });
|
|
10908
11665
|
}
|
|
10909
11666
|
}
|
|
11667
|
+
/**
|
|
11668
|
+
* The organization's protection rules in force: the ones the latest credential brought (bootstrap, model switch,
|
|
11669
|
+
* renewal). None when the Runtime sent none.
|
|
11670
|
+
*/
|
|
11671
|
+
protections() {
|
|
11672
|
+
const raw = this.boot?.runtime?.protections;
|
|
11673
|
+
return raw ? parseProtections(raw) : NO_PROTECTIONS;
|
|
11674
|
+
}
|
|
11675
|
+
/** What the protections read from the workspace: the registries, the checked out branch, the installed packages. */
|
|
11676
|
+
protectionDeps() {
|
|
11677
|
+
return {
|
|
11678
|
+
registry: this.registry,
|
|
11679
|
+
currentBranch: async () => (await git(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: this.deps.root, timeoutMs: 3e3 })).trim() || null,
|
|
11680
|
+
installedLocally: (name) => [this.deps.root, ...[...this.repos.values()].map((r) => r.root)].some((dir) => (0, import_fs9.existsSync)((0, import_path15.join)(dir, "node_modules", name, "package.json")) || (0, import_fs9.existsSync)((0, import_path15.join)(dir, "node_modules", ".bin", name)))
|
|
11681
|
+
};
|
|
11682
|
+
}
|
|
11683
|
+
/**
|
|
11684
|
+
* Who approves a command a protection holds: on a connected computer the browser gate (its decisions come through
|
|
11685
|
+
* the CLI), in the cloud workspace the same card through the internal route, decided on this engine's poll.
|
|
11686
|
+
*/
|
|
11687
|
+
protectionApprover() {
|
|
11688
|
+
if (this.approver !== void 0) return this.approver;
|
|
11689
|
+
const gate = this.deps.commandGate;
|
|
11690
|
+
const transport = this.deps.transport;
|
|
11691
|
+
if (gate) {
|
|
11692
|
+
this.approver = gateProtectionApprover(timedGate(gate, (ms) => this.live?.timing.permission(ms)));
|
|
11693
|
+
} else if (transport.requestCommandApproval && transport.cancelCommandApproval) {
|
|
11694
|
+
this.approver = cloudProtectionApprover({
|
|
11695
|
+
calls: { requestCommandApproval: (b) => transport.requestCommandApproval(b), cancelCommandApproval: (id, r) => transport.cancelCommandApproval(id, r) },
|
|
11696
|
+
broker: this.broker,
|
|
11697
|
+
cwd: this.deps.root,
|
|
11698
|
+
log: (msg) => this.deps.log.warn(this.redactor.text(msg))
|
|
11699
|
+
});
|
|
11700
|
+
} else this.approver = null;
|
|
11701
|
+
return this.approver;
|
|
11702
|
+
}
|
|
10910
11703
|
/**
|
|
10911
11704
|
* A fresh gateway credential for the same model (silent): the limits and protections of now. A failure keeps
|
|
10912
11705
|
* the current one; the turn runs as before.
|
|
@@ -10914,10 +11707,11 @@ ${String(item.content)}` : String(item.content);
|
|
|
10914
11707
|
async renewCredential() {
|
|
10915
11708
|
if (!this.deps.transport.refreshRuntime) return;
|
|
10916
11709
|
try {
|
|
10917
|
-
const fresh = await this.deps.transport.refreshRuntime();
|
|
11710
|
+
const fresh = await this.deps.transport.refreshRuntime({ model: this.boot.model });
|
|
10918
11711
|
if (!fresh.runtime.token || !fresh.runtime.baseUrl) throw new Error("RUNTIME_INCOMPLETE");
|
|
10919
11712
|
this.redactor.add(fresh.runtime.token);
|
|
10920
11713
|
this.boot.runtime = fresh.runtime;
|
|
11714
|
+
if (fresh.model) this.boot.model = fresh.model;
|
|
10921
11715
|
this.reasoningCapability = parseReasoningCapability(fresh.runtime.reasoning ?? null);
|
|
10922
11716
|
this.reasoningLevel = effectiveReasoning(fresh.reasoning ?? this.reasoningLevel, this.reasoningCapability);
|
|
10923
11717
|
} catch (e) {
|
|
@@ -11750,7 +12544,8 @@ ${text3}`;
|
|
|
11750
12544
|
skills: { dir: this.skillsDir(), enabled: enabledSkillNames(t.off.skillEntries), pluginDirs: t.off.pluginDirs },
|
|
11751
12545
|
skillsKey: this.skillsSent,
|
|
11752
12546
|
orgMcpServers: org ? t.off.mcpServers : {},
|
|
11753
|
-
folderWrites: this.localFolder?.kind === "folder"
|
|
12547
|
+
folderWrites: this.localFolder?.kind === "folder",
|
|
12548
|
+
secretFiles: this.protections().secretFiles
|
|
11754
12549
|
};
|
|
11755
12550
|
}
|
|
11756
12551
|
/**
|
|
@@ -11780,6 +12575,9 @@ ${text3}`;
|
|
|
11780
12575
|
...Object.keys(i.orgMcpServers).length ? { orgMcpServers: i.orgMcpServers } : {},
|
|
11781
12576
|
pathToClaudeCodeExecutable: this.deps.pathToClaudeCodeExecutable,
|
|
11782
12577
|
commandGate: gate ? timedGate(gate, (ms) => this.live?.timing.permission(ms)) : void 0,
|
|
12578
|
+
secretFiles: i.secretFiles,
|
|
12579
|
+
protections: () => this.protections(),
|
|
12580
|
+
protection: { deps: this.protectionDeps(), approver: this.protectionApprover() },
|
|
11783
12581
|
beforeTool: async () => {
|
|
11784
12582
|
const t0 = Date.now();
|
|
11785
12583
|
await this.toolGate;
|
|
@@ -12512,11 +13310,13 @@ function buildQueryOptions(o) {
|
|
|
12512
13310
|
if (name.startsWith("mcp__") && provenance?.source && provenance.source !== "sdk") {
|
|
12513
13311
|
return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: "Only ScaleQuality tools are available in this workspace." } };
|
|
12514
13312
|
}
|
|
12515
|
-
const
|
|
12516
|
-
const
|
|
13313
|
+
const protections = o.protections?.() ?? NO_PROTECTIONS;
|
|
13314
|
+
const d = await decideToolUse(name, input.tool_input ?? {}, { ...o.policy, protections });
|
|
13315
|
+
const guarded = d.behavior === "allow" && name === "Bash" && mayNeedProtection(String(d.updatedInput.command ?? ""), protections);
|
|
13316
|
+
const gated = guarded || d.behavior === "allow" && !!gate && (name === "Bash" || !!gate.checkEdit && !o.plan && name in WRITE_TOOL_PATHS);
|
|
12517
13317
|
if (d.behavior === "allow" && !gated) await beforeWrite(name, d.updatedInput);
|
|
12518
13318
|
if (gated) {
|
|
12519
|
-
return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "ask", permissionDecisionReason: "Commands and changes on this machine follow the person's permission mode." } };
|
|
13319
|
+
return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "ask", permissionDecisionReason: guarded ? "This command follows the organization's protections." : "Commands and changes on this machine follow the person's permission mode." } };
|
|
12520
13320
|
}
|
|
12521
13321
|
return d.behavior === "allow" ? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: d.updatedInput } } : { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: d.message } };
|
|
12522
13322
|
};
|
|
@@ -12532,8 +13332,28 @@ function buildQueryOptions(o) {
|
|
|
12532
13332
|
o.onPlan?.(text3);
|
|
12533
13333
|
return { behavior: "deny", message: "The plan is now with the user for review in ScaleQuality. Do not continue and do not call more tools: end your turn with one short sentence saying the plan is ready for review." };
|
|
12534
13334
|
}
|
|
12535
|
-
const
|
|
13335
|
+
const protections = o.protections?.() ?? NO_PROTECTIONS;
|
|
13336
|
+
const d = await decideToolUse(toolName, input, { ...o.policy, protections });
|
|
12536
13337
|
if (d.behavior !== "allow") return { behavior: "deny", message: d.message };
|
|
13338
|
+
let protectionApproved = false;
|
|
13339
|
+
if (toolName === "Bash" && mayNeedProtection(String(d.updatedInput.command ?? ""), protections)) {
|
|
13340
|
+
const command = String(d.updatedInput.command ?? "");
|
|
13341
|
+
const signal = opts?.signal ?? o.abortController.signal;
|
|
13342
|
+
if (!o.protection) return { behavior: "deny", message: PROTECTION_MESSAGES.unavailable };
|
|
13343
|
+
const v = await protectionVerdict(command, protections, o.protection.deps);
|
|
13344
|
+
if (v.kind === "deny") return { behavior: "deny", message: v.message };
|
|
13345
|
+
if (v.kind === "ask") {
|
|
13346
|
+
const approver = o.protection.approver;
|
|
13347
|
+
const r = approver ? await approver.ask(command, {
|
|
13348
|
+
reasons: v.reasons,
|
|
13349
|
+
...v.package ? { package: v.package, packageReason: v.packageReason } : {},
|
|
13350
|
+
description: typeof input.description === "string" ? input.description : void 0,
|
|
13351
|
+
signal
|
|
13352
|
+
}) : { allow: false, message: PROTECTION_MESSAGES.unavailable };
|
|
13353
|
+
if (!r.allow) return { behavior: "deny", message: r.message };
|
|
13354
|
+
protectionApproved = true;
|
|
13355
|
+
}
|
|
13356
|
+
}
|
|
12537
13357
|
if (gate?.checkEdit && !o.plan && toolName in WRITE_TOOL_PATHS) {
|
|
12538
13358
|
const field = WRITE_TOOL_PATHS[toolName];
|
|
12539
13359
|
const path = typeof d.updatedInput[field] === "string" ? (0, import_path15.relative)(o.root, d.updatedInput[field]) || String(d.updatedInput[field]) : "";
|
|
@@ -12541,7 +13361,7 @@ function buildQueryOptions(o) {
|
|
|
12541
13361
|
if (!r.allow) return { behavior: "deny", message: r.message };
|
|
12542
13362
|
}
|
|
12543
13363
|
await beforeWrite(toolName, d.updatedInput);
|
|
12544
|
-
if (gate && toolName === "Bash") {
|
|
13364
|
+
if (gate && toolName === "Bash" && !protectionApproved) {
|
|
12545
13365
|
const r = await gate.check(String(d.updatedInput.command ?? ""), {
|
|
12546
13366
|
description: typeof input.description === "string" ? input.description : void 0,
|
|
12547
13367
|
signal: opts?.signal ?? o.abortController.signal
|
|
@@ -12570,7 +13390,8 @@ function buildQueryOptions(o) {
|
|
|
12570
13390
|
tools: o.plan ? PLAN_TOOLS : MODEL_TOOLS,
|
|
12571
13391
|
// No settings file is read (settingSources above); these are ours: WebFetch asks no outside service
|
|
12572
13392
|
// before fetching (the policy already decided), and plan files stay in the engine's own folder.
|
|
12573
|
-
|
|
13393
|
+
// secret-files: Read deny rules, which the engine also applies to its searches (Grep and Glob skip those files).
|
|
13394
|
+
settings: { skipWebFetchPreflight: true, ...o.secretFiles ? { permissions: { deny: [...SECRET_READ_DENY_RULES] } } : {} },
|
|
12574
13395
|
// Skills: the built-in ones that fit the workspace and the plugin the engine built (never the repository's .claude settings).
|
|
12575
13396
|
...o.skills ? { plugins: [o.skills.dir, ...o.skills.pluginDirs ?? []].map((path) => ({ type: "local", path, skipMcpDiscovery: true })), skills: o.skills.enabled } : { skills: [] },
|
|
12576
13397
|
disallowedTools: DENIED_TOOLS,
|
|
@@ -12598,7 +13419,8 @@ function timedGate(gate, took) {
|
|
|
12598
13419
|
};
|
|
12599
13420
|
return {
|
|
12600
13421
|
check: (command, opts) => timed(() => gate.check(command, opts)),
|
|
12601
|
-
...gate.checkEdit ? { checkEdit: (path, opts) => timed(() => gate.checkEdit(path, opts)) } : {}
|
|
13422
|
+
...gate.checkEdit ? { checkEdit: (path, opts) => timed(() => gate.checkEdit(path, opts)) } : {},
|
|
13423
|
+
...gate.checkProtected ? { checkProtected: (command, opts) => timed(() => gate.checkProtected(command, opts)) } : {}
|
|
12602
13424
|
};
|
|
12603
13425
|
}
|
|
12604
13426
|
async function hasLocalTranscript(configDir, sessionId) {
|