@epoch-agent/infra 0.1.0 → 0.3.1
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/README.md +20 -20
- package/dist/index.d.ts +765 -48
- package/dist/index.js +849 -393
- package/dist/locales/en.yaml +3328 -2400
- package/dist/locales/zh.yaml +3489 -2740
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { platform, homedir, tmpdir } from 'os';
|
|
2
|
-
import { join, dirname, delimiter, resolve as resolve$1, isAbsolute,
|
|
2
|
+
import { join, dirname, basename, delimiter, resolve as resolve$1, isAbsolute, win32, posix, relative } from 'path';
|
|
3
3
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, openSync, closeSync, writeSync, renameSync, rmSync, chmodSync, lstatSync, realpathSync, accessSync, constants } from 'fs';
|
|
4
4
|
import Database from 'better-sqlite3';
|
|
5
5
|
import { execFileSync, execFile, spawn } from 'child_process';
|
|
6
6
|
import { TextDecoder } from 'util';
|
|
7
|
-
import { createRequire } from 'module';
|
|
8
7
|
import { fileURLToPath } from 'url';
|
|
9
8
|
import { load } from 'js-yaml';
|
|
9
|
+
import { createRequire } from 'module';
|
|
10
10
|
import { randomBytes, createCipheriv, createDecipheriv, createHash } from 'crypto';
|
|
11
11
|
|
|
12
12
|
// src/paths.ts
|
|
@@ -38,6 +38,9 @@ function skillsDir(homeDir = resolveHomeDir()) {
|
|
|
38
38
|
function policiesDir(homeDir = resolveHomeDir()) {
|
|
39
39
|
return join(homeDir, "policies");
|
|
40
40
|
}
|
|
41
|
+
function complianceDir(homeDir = resolveHomeDir()) {
|
|
42
|
+
return join(homeDir, "compliance");
|
|
43
|
+
}
|
|
41
44
|
function hooksPath(homeDir = resolveHomeDir()) {
|
|
42
45
|
return join(homeDir, "hooks.json");
|
|
43
46
|
}
|
|
@@ -132,6 +135,19 @@ function projectHooksPath(projectRoot) {
|
|
|
132
135
|
function projectPoliciesDir(projectRoot) {
|
|
133
136
|
return join(projectRoot, PROJECT_DIR_NAME, "policies");
|
|
134
137
|
}
|
|
138
|
+
function projectComplianceDir(projectRoot) {
|
|
139
|
+
return join(projectRoot, PROJECT_DIR_NAME, "compliance");
|
|
140
|
+
}
|
|
141
|
+
var CLAUDE_DIR_NAME = ".claude";
|
|
142
|
+
function claudeUserSettingsPath(home = homedir()) {
|
|
143
|
+
return join(home, CLAUDE_DIR_NAME, "settings.json");
|
|
144
|
+
}
|
|
145
|
+
function projectClaudeSettingsPath(projectRoot) {
|
|
146
|
+
return join(projectRoot, CLAUDE_DIR_NAME, "settings.json");
|
|
147
|
+
}
|
|
148
|
+
function projectClaudeLocalSettingsPath(projectRoot) {
|
|
149
|
+
return join(projectRoot, CLAUDE_DIR_NAME, "settings.local.json");
|
|
150
|
+
}
|
|
135
151
|
function managedSettingsPath(os = platform()) {
|
|
136
152
|
const FILE = "managed-settings.json";
|
|
137
153
|
if (os === "win32") {
|
|
@@ -152,8 +168,9 @@ function setSectionField(yaml, section, field, value) {
|
|
|
152
168
|
const entry = ` ${field}: ${value}`;
|
|
153
169
|
const header = new RegExp(`^${section}:[ \\t]*$`, "m");
|
|
154
170
|
if (!header.test(yaml)) {
|
|
155
|
-
|
|
156
|
-
|
|
171
|
+
const head = yaml.trimEnd();
|
|
172
|
+
return `${head === "" ? "" : `${head}
|
|
173
|
+
`}${section}:
|
|
157
174
|
${entry}
|
|
158
175
|
`;
|
|
159
176
|
}
|
|
@@ -269,7 +286,6 @@ function writeArtifact(dir, name, extension, data, onExisting = "unique") {
|
|
|
269
286
|
handle.close();
|
|
270
287
|
return handle.path;
|
|
271
288
|
}
|
|
272
|
-
|
|
273
289
|
// src/text-slice.ts
|
|
274
290
|
function isHighSurrogate(code) {
|
|
275
291
|
return code >= 55296 && code <= 56319;
|
|
@@ -459,7 +475,6 @@ function schemaVersion(db, namespace) {
|
|
|
459
475
|
const row = db.prepare("SELECT MAX(version) AS v FROM _epoch_migrations WHERE namespace = ?").get(namespace);
|
|
460
476
|
return row?.v ?? 0;
|
|
461
477
|
}
|
|
462
|
-
|
|
463
478
|
// src/mask.ts
|
|
464
479
|
function maskApiKey(key) {
|
|
465
480
|
if (!key || key.length < 8) return "***";
|
|
@@ -491,7 +506,6 @@ function maskSensitive(data) {
|
|
|
491
506
|
}
|
|
492
507
|
return masked;
|
|
493
508
|
}
|
|
494
|
-
|
|
495
509
|
// src/logger.ts
|
|
496
510
|
var currentLevel = "info";
|
|
497
511
|
function setLogLevel(level) {
|
|
@@ -529,6 +543,177 @@ function createLogger(module) {
|
|
|
529
543
|
}
|
|
530
544
|
};
|
|
531
545
|
}
|
|
546
|
+
var LANGS = ["zh", "en"];
|
|
547
|
+
var DEFAULT_LANG = "zh";
|
|
548
|
+
function isLang(value) {
|
|
549
|
+
return LANGS.includes(value);
|
|
550
|
+
}
|
|
551
|
+
var catalogs = /* @__PURE__ */ new Map();
|
|
552
|
+
var pending = [];
|
|
553
|
+
var localesDirCache;
|
|
554
|
+
var currentLangValue;
|
|
555
|
+
function localesDir() {
|
|
556
|
+
if (localesDirCache !== void 0) return localesDirCache ?? void 0;
|
|
557
|
+
const override = process.env.EPOCH_LOCALES_DIR;
|
|
558
|
+
if (override) {
|
|
559
|
+
localesDirCache = existsSync(override) ? override : null;
|
|
560
|
+
return localesDirCache ?? void 0;
|
|
561
|
+
}
|
|
562
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
563
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
564
|
+
if (basename(dir) === "node_modules") break;
|
|
565
|
+
const candidate = join(dir, "locales");
|
|
566
|
+
if (existsSync(join(candidate, `${DEFAULT_LANG}.yaml`))) {
|
|
567
|
+
localesDirCache = candidate;
|
|
568
|
+
return candidate;
|
|
569
|
+
}
|
|
570
|
+
const parent = dirname(dir);
|
|
571
|
+
if (parent === dir) break;
|
|
572
|
+
dir = parent;
|
|
573
|
+
}
|
|
574
|
+
localesDirCache = null;
|
|
575
|
+
return void 0;
|
|
576
|
+
}
|
|
577
|
+
function flatten(value, prefix, out) {
|
|
578
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
579
|
+
for (const [key, child] of Object.entries(value)) {
|
|
580
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
581
|
+
if (typeof child === "string") out[path] = child;
|
|
582
|
+
else flatten(child, path, out);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
function fallbackDetail(key, vars) {
|
|
586
|
+
const text = t(key, vars);
|
|
587
|
+
if (text !== key) return text;
|
|
588
|
+
switch (key) {
|
|
589
|
+
case "i18n.locales_missing":
|
|
590
|
+
return "locales directory not found; UI text will show as key paths";
|
|
591
|
+
case "i18n.catalog_missing":
|
|
592
|
+
return `locales/${vars["file"]} is missing; ${vars["lang"]} text falls back to the default language`;
|
|
593
|
+
case "i18n.catalog_invalid":
|
|
594
|
+
return `locales/${vars["file"]} is not valid YAML (${vars["reason"]}); fell back to the built-in default`;
|
|
595
|
+
default:
|
|
596
|
+
return key;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function loadCatalog(lang) {
|
|
600
|
+
const cached2 = catalogs.get(lang);
|
|
601
|
+
if (cached2) return cached2;
|
|
602
|
+
const result = readCatalog(lang);
|
|
603
|
+
catalogs.set(lang, result);
|
|
604
|
+
return result;
|
|
605
|
+
}
|
|
606
|
+
function readCatalog(lang) {
|
|
607
|
+
const dir = localesDir();
|
|
608
|
+
if (!dir) {
|
|
609
|
+
pending.push({
|
|
610
|
+
code: "locales-missing",
|
|
611
|
+
render: () => fallbackDetail("i18n.locales_missing", {})
|
|
612
|
+
});
|
|
613
|
+
return { entries: {}, error: "\u627E\u4E0D\u5230 locales \u76EE\u5F55" };
|
|
614
|
+
}
|
|
615
|
+
const file = `${lang}.yaml`;
|
|
616
|
+
const path = join(dir, file);
|
|
617
|
+
if (!existsSync(path)) {
|
|
618
|
+
pending.push({
|
|
619
|
+
code: "catalog-missing",
|
|
620
|
+
render: () => fallbackDetail("i18n.catalog_missing", { file, lang })
|
|
621
|
+
});
|
|
622
|
+
return { entries: {}, error: `${path} \u4E0D\u5B58\u5728` };
|
|
623
|
+
}
|
|
624
|
+
let parsed;
|
|
625
|
+
try {
|
|
626
|
+
parsed = load(readFileSync(path, "utf-8"));
|
|
627
|
+
} catch (err) {
|
|
628
|
+
const reason = (err instanceof Error ? err.message : String(err)).split("\n")[0] ?? "";
|
|
629
|
+
pending.push({
|
|
630
|
+
code: "catalog-invalid",
|
|
631
|
+
render: () => fallbackDetail("i18n.catalog_invalid", { file, reason })
|
|
632
|
+
});
|
|
633
|
+
return { entries: {}, error: reason };
|
|
634
|
+
}
|
|
635
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
636
|
+
const reason = "\u9876\u5C42\u4E0D\u662F\u952E\u503C\u5BF9";
|
|
637
|
+
pending.push({
|
|
638
|
+
code: "catalog-invalid",
|
|
639
|
+
render: () => fallbackDetail("i18n.catalog_invalid", { file, reason })
|
|
640
|
+
});
|
|
641
|
+
return { entries: {}, error: reason };
|
|
642
|
+
}
|
|
643
|
+
const entries = {};
|
|
644
|
+
flatten(parsed, "", entries);
|
|
645
|
+
return { entries };
|
|
646
|
+
}
|
|
647
|
+
function resetI18n() {
|
|
648
|
+
catalogs.clear();
|
|
649
|
+
pending = [];
|
|
650
|
+
localesDirCache = void 0;
|
|
651
|
+
currentLangValue = void 0;
|
|
652
|
+
}
|
|
653
|
+
var PLACEHOLDER = /\{(\w+)\}/g;
|
|
654
|
+
function placeholdersOf(text) {
|
|
655
|
+
const found = [];
|
|
656
|
+
for (const match of text.matchAll(PLACEHOLDER)) {
|
|
657
|
+
const name = match[1];
|
|
658
|
+
if (name && !found.includes(name)) found.push(name);
|
|
659
|
+
}
|
|
660
|
+
return found;
|
|
661
|
+
}
|
|
662
|
+
function t(key, vars, lang) {
|
|
663
|
+
const target = lang ?? currentLang();
|
|
664
|
+
const text = loadCatalog(target).entries[key] ?? loadCatalog(DEFAULT_LANG).entries[key] ?? key;
|
|
665
|
+
if (!vars) return text;
|
|
666
|
+
return text.replace(PLACEHOLDER, (whole, name) => {
|
|
667
|
+
const value = vars[name];
|
|
668
|
+
return value === void 0 ? whole : String(value);
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
function currentLang() {
|
|
672
|
+
currentLangValue ??= resolveLang();
|
|
673
|
+
return currentLangValue;
|
|
674
|
+
}
|
|
675
|
+
function setLang(lang) {
|
|
676
|
+
currentLangValue = lang;
|
|
677
|
+
}
|
|
678
|
+
function uiDateLocale() {
|
|
679
|
+
switch (currentLang()) {
|
|
680
|
+
case "zh":
|
|
681
|
+
return "zh-CN";
|
|
682
|
+
case "en":
|
|
683
|
+
return "en-US";
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
function systemLocaleSignals() {
|
|
687
|
+
const env = process.env;
|
|
688
|
+
const signals = [env.LC_ALL, env.LC_MESSAGES, env.LANG, env.LANGUAGE].filter(
|
|
689
|
+
(v) => typeof v === "string" && v.length > 0
|
|
690
|
+
);
|
|
691
|
+
try {
|
|
692
|
+
signals.push(new Intl.DateTimeFormat().resolvedOptions().locale);
|
|
693
|
+
} catch {
|
|
694
|
+
}
|
|
695
|
+
return signals;
|
|
696
|
+
}
|
|
697
|
+
function langFromLocale(tag) {
|
|
698
|
+
const primary = tag.split(/[.@]/)[0]?.replace("_", "-").toLowerCase() ?? "";
|
|
699
|
+
if (!primary || primary === "c" || primary === "posix") return void 0;
|
|
700
|
+
if (primary === "zh" || primary.startsWith("zh-")) return "zh";
|
|
701
|
+
return "en";
|
|
702
|
+
}
|
|
703
|
+
function resolveLang(configured, signals) {
|
|
704
|
+
const fromEnv = process.env.EPOCH_LANGUAGE;
|
|
705
|
+
if (fromEnv && isLang(fromEnv)) return fromEnv;
|
|
706
|
+
if (configured && isLang(configured)) return configured;
|
|
707
|
+
for (const tag of signals ?? systemLocaleSignals()) {
|
|
708
|
+
const lang = langFromLocale(tag);
|
|
709
|
+
if (lang) return lang;
|
|
710
|
+
}
|
|
711
|
+
return DEFAULT_LANG;
|
|
712
|
+
}
|
|
713
|
+
function i18nDiagnostics() {
|
|
714
|
+
return pending.map((item) => ({ code: item.code, detail: item.render() }));
|
|
715
|
+
}
|
|
716
|
+
// src/platform.ts
|
|
532
717
|
var IS_WINDOWS = platform() === "win32";
|
|
533
718
|
var WINDOWS_HIDE_FLAGS = IS_WINDOWS ? { windowsHide: true } : {};
|
|
534
719
|
function getPythonCommand() {
|
|
@@ -538,9 +723,7 @@ function getPythonCommand() {
|
|
|
538
723
|
var SHELL_KINDS = ["cmd", "powershell", "pwsh"];
|
|
539
724
|
var SHELL_EXECUTABLES = {
|
|
540
725
|
cmd: "cmd.exe",
|
|
541
|
-
// Windows PowerShell 5.1,随系统自带
|
|
542
726
|
powershell: "powershell.exe",
|
|
543
|
-
// PowerShell 7+,要自己装
|
|
544
727
|
pwsh: "pwsh.exe"
|
|
545
728
|
};
|
|
546
729
|
var configuredShell = null;
|
|
@@ -563,7 +746,7 @@ function probeShell(kind) {
|
|
|
563
746
|
for (const dir of dirs) {
|
|
564
747
|
if (existsSync(join(dir, exe))) return null;
|
|
565
748
|
}
|
|
566
|
-
return
|
|
749
|
+
return t("infra_misc.shell_not_on_path", { kind, exe });
|
|
567
750
|
}
|
|
568
751
|
var PS_EXIT_TRAILER = [
|
|
569
752
|
"$__epochOk = $?",
|
|
@@ -605,15 +788,10 @@ function encodingForCodePage(codePage) {
|
|
|
605
788
|
const table = {
|
|
606
789
|
65001: "utf-8",
|
|
607
790
|
936: "gbk",
|
|
608
|
-
// 简体中文
|
|
609
791
|
950: "big5",
|
|
610
|
-
// 繁体中文
|
|
611
792
|
932: "shift_jis",
|
|
612
|
-
// 日文
|
|
613
793
|
949: "euc-kr",
|
|
614
|
-
// 韩文
|
|
615
794
|
866: "ibm866",
|
|
616
|
-
// 西里尔 OEM
|
|
617
795
|
874: "windows-874",
|
|
618
796
|
1250: "windows-1250",
|
|
619
797
|
1251: "windows-1251",
|
|
@@ -695,7 +873,10 @@ function createStreamDecoder(fallbackEncoding = detectConsoleEncoding()) {
|
|
|
695
873
|
}
|
|
696
874
|
};
|
|
697
875
|
}
|
|
698
|
-
|
|
876
|
+
function ripgrepInstallHint() {
|
|
877
|
+
if (IS_WINDOWS) return t("infra_misc.ripgrep_install_windows");
|
|
878
|
+
return process.platform === "darwin" ? "brew install ripgrep" : "apt install ripgrep / dnf install ripgrep / pacman -S ripgrep";
|
|
879
|
+
}
|
|
699
880
|
function ensureExecutable(path) {
|
|
700
881
|
try {
|
|
701
882
|
accessSync(path, constants.X_OK);
|
|
@@ -745,6 +926,7 @@ function defaultResolveVendor(specifier) {
|
|
|
745
926
|
function readVendorVersion(manifestPath) {
|
|
746
927
|
try {
|
|
747
928
|
const pkg = require_(manifestPath);
|
|
929
|
+
if (typeof pkg.ripgrepVersion === "string") return pkg.ripgrepVersion;
|
|
748
930
|
return typeof pkg.version === "string" ? pkg.version : void 0;
|
|
749
931
|
} catch {
|
|
750
932
|
return void 0;
|
|
@@ -771,13 +953,20 @@ function resolve(options) {
|
|
|
771
953
|
if (manifest) {
|
|
772
954
|
const api = platform10 === "win32" ? win32 : posix;
|
|
773
955
|
const binary = api.join(api.dirname(manifest), "bin", platform10 === "win32" ? "rg.exe" : "rg");
|
|
774
|
-
if (exists(binary)
|
|
775
|
-
|
|
776
|
-
|
|
956
|
+
if (exists(binary)) {
|
|
957
|
+
if (platform10 === "win32" || makeExecutable(binary)) {
|
|
958
|
+
const version = readVendorVersion(manifest);
|
|
959
|
+
return { mode: "builtin", command: binary, ...version ? { version } : {} };
|
|
960
|
+
}
|
|
961
|
+
return {
|
|
962
|
+
mode: "not-executable",
|
|
963
|
+
command: "rg",
|
|
964
|
+
hint: t("infra_misc.ripgrep_chmod_hint", { path: binary })
|
|
965
|
+
};
|
|
777
966
|
}
|
|
778
967
|
}
|
|
779
968
|
}
|
|
780
|
-
return { mode: "missing", command: "rg", hint:
|
|
969
|
+
return { mode: "missing", command: "rg", hint: ripgrepInstallHint() };
|
|
781
970
|
}
|
|
782
971
|
var cached = null;
|
|
783
972
|
function resolveRipgrep(options) {
|
|
@@ -812,24 +1001,55 @@ async function killProcessTree(options) {
|
|
|
812
1001
|
const { pid, pty, detached = false, platform: platform10 = platform() } = options;
|
|
813
1002
|
if (!isRealChildPid(pid)) return;
|
|
814
1003
|
if (platform10 === "win32") {
|
|
815
|
-
const
|
|
1004
|
+
const snapshot = await windowsSnapshot();
|
|
1005
|
+
if (snapshot.pids.size > 0 && !snapshot.pids.has(pid)) {
|
|
1006
|
+
tryPtyKill(pty);
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
const descendants2 = walkTree(pid, (parent) => snapshot.childrenOf.get(parent) ?? []);
|
|
1010
|
+
await killPids(descendants2, { signal: "SIGKILL" });
|
|
1011
|
+
const swept = isAlive(pid) ? await runProbe("taskkill", ["/pid", String(pid), "/f", "/t"]) : "";
|
|
816
1012
|
tryPtyKill(pty);
|
|
817
|
-
await killPids([
|
|
818
|
-
await runProbe("taskkill", ["/pid", String(pid), "/f", "/t"]);
|
|
1013
|
+
if (!swept && isAlive(pid)) await killPids([pid], { signal: "SIGKILL" });
|
|
819
1014
|
return;
|
|
820
1015
|
}
|
|
821
1016
|
const descendants = await collectProcessTree(pid, platform10);
|
|
822
1017
|
await killPids([...descendants, pid], options, {
|
|
823
|
-
// 只有 detached 才允许碰 -pid —— 这一行就是文件头那条约束的落点
|
|
824
1018
|
...detached ? { groupLeaderPid: pid } : {},
|
|
825
1019
|
...pty ? { pty } : {}
|
|
826
1020
|
});
|
|
827
1021
|
}
|
|
1022
|
+
async function collectProcessTreeStamped(rootPid, platform10 = platform()) {
|
|
1023
|
+
if (!isRealChildPid(rootPid)) return [];
|
|
1024
|
+
if (platform10 !== "win32") {
|
|
1025
|
+
const pids = await collectProcessTree(rootPid, platform10);
|
|
1026
|
+
return pids.map((pid) => ({ pid, born: "" }));
|
|
1027
|
+
}
|
|
1028
|
+
const snapshot = await windowsSnapshot();
|
|
1029
|
+
return walkTree(rootPid, (parent) => snapshot.childrenOf.get(parent) ?? []).map((pid) => ({
|
|
1030
|
+
pid,
|
|
1031
|
+
born: snapshot.bornOf.get(pid) ?? ""
|
|
1032
|
+
}));
|
|
1033
|
+
}
|
|
1034
|
+
async function killStampedPids(stamped, options = {}, platform10 = platform()) {
|
|
1035
|
+
if (stamped.length === 0) return;
|
|
1036
|
+
if (platform10 !== "win32") {
|
|
1037
|
+
await killPids(
|
|
1038
|
+
stamped.map((s) => s.pid),
|
|
1039
|
+
options
|
|
1040
|
+
);
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
const snapshot = await windowsSnapshot();
|
|
1044
|
+
const targets = snapshot.pids.size === 0 ? stamped.map((s) => s.pid) : stamped.filter((s) => s.born === "" || snapshot.bornOf.get(s.pid) === s.born).map((s) => s.pid);
|
|
1045
|
+
if (targets.length > 0) await killPids(targets, options);
|
|
1046
|
+
}
|
|
828
1047
|
async function killPids(pids, options = {}, extra = {}) {
|
|
829
1048
|
const { escalate = false, isExited = () => false, killTimeoutMs = SIGKILL_TIMEOUT_MS } = options;
|
|
830
1049
|
const targets = pids.filter(isRealChildPid);
|
|
831
1050
|
const first = options.signal ?? (escalate ? "SIGTERM" : "SIGKILL");
|
|
832
1051
|
sweep(targets, first, extra);
|
|
1052
|
+
tryPtyKill(extra.pty, first);
|
|
833
1053
|
if (!escalate || isExited()) return;
|
|
834
1054
|
await delay(killTimeoutMs);
|
|
835
1055
|
if (isExited()) return;
|
|
@@ -838,7 +1058,14 @@ async function killPids(pids, options = {}, extra = {}) {
|
|
|
838
1058
|
function sweep(targets, signal, extra) {
|
|
839
1059
|
if (extra.groupLeaderPid !== void 0) sendSignal(-extra.groupLeaderPid, signal);
|
|
840
1060
|
for (const target of targets) sendSignal(target, signal);
|
|
841
|
-
|
|
1061
|
+
}
|
|
1062
|
+
function isAlive(pid) {
|
|
1063
|
+
try {
|
|
1064
|
+
process.kill(pid, 0);
|
|
1065
|
+
return true;
|
|
1066
|
+
} catch (err) {
|
|
1067
|
+
return err.code === "EPERM";
|
|
1068
|
+
}
|
|
842
1069
|
}
|
|
843
1070
|
function sendSignal(target, signal) {
|
|
844
1071
|
try {
|
|
@@ -855,14 +1082,35 @@ function tryPtyKill(pty, signal) {
|
|
|
855
1082
|
}
|
|
856
1083
|
async function collectProcessTree(rootPid, platform10 = platform()) {
|
|
857
1084
|
if (!isRealChildPid(rootPid)) return [];
|
|
858
|
-
|
|
1085
|
+
if (platform10 === "win32") {
|
|
1086
|
+
const snapshot = await windowsSnapshot();
|
|
1087
|
+
return walkTree(rootPid, (parent) => snapshot.childrenOf.get(parent) ?? []);
|
|
1088
|
+
}
|
|
859
1089
|
const found = [];
|
|
860
1090
|
const seen = /* @__PURE__ */ new Set([rootPid]);
|
|
861
1091
|
let frontier = [rootPid];
|
|
862
1092
|
for (let depth = 0; depth < MAX_TREE_DEPTH && frontier.length > 0; depth++) {
|
|
863
1093
|
const next = [];
|
|
864
1094
|
for (const parent of frontier) {
|
|
865
|
-
for (const child of await
|
|
1095
|
+
for (const child of await listChildrenViaPgrep(parent)) {
|
|
1096
|
+
if (seen.has(child) || found.length >= MAX_TREE_SIZE) continue;
|
|
1097
|
+
seen.add(child);
|
|
1098
|
+
found.push(child);
|
|
1099
|
+
next.push(child);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
frontier = next;
|
|
1103
|
+
}
|
|
1104
|
+
return found.reverse();
|
|
1105
|
+
}
|
|
1106
|
+
function walkTree(rootPid, childrenOf) {
|
|
1107
|
+
const found = [];
|
|
1108
|
+
const seen = /* @__PURE__ */ new Set([rootPid]);
|
|
1109
|
+
let frontier = [rootPid];
|
|
1110
|
+
for (let depth = 0; depth < MAX_TREE_DEPTH && frontier.length > 0; depth++) {
|
|
1111
|
+
const next = [];
|
|
1112
|
+
for (const parent of frontier) {
|
|
1113
|
+
for (const child of childrenOf(parent)) {
|
|
866
1114
|
if (seen.has(child) || found.length >= MAX_TREE_SIZE) continue;
|
|
867
1115
|
seen.add(child);
|
|
868
1116
|
found.push(child);
|
|
@@ -877,9 +1125,13 @@ var listChildrenViaPgrep = async (parentPid) => {
|
|
|
877
1125
|
const stdout = await runProbe("pgrep", ["-P", String(parentPid)]);
|
|
878
1126
|
return stdout.split("\n").map((line) => Number.parseInt(line.trim(), 10)).filter(isRealChildPid);
|
|
879
1127
|
};
|
|
880
|
-
async function
|
|
881
|
-
const
|
|
882
|
-
return
|
|
1128
|
+
async function windowsSnapshot() {
|
|
1129
|
+
const records = await readWindowsSnapshot();
|
|
1130
|
+
return {
|
|
1131
|
+
pids: new Set(records.map((record) => record.pid)),
|
|
1132
|
+
childrenOf: buildChildrenMap(records),
|
|
1133
|
+
bornOf: new Map(records.map((record) => [record.pid, record.born]))
|
|
1134
|
+
};
|
|
883
1135
|
}
|
|
884
1136
|
async function readWindowsSnapshot() {
|
|
885
1137
|
const fast = parseWmicCsv(await runProbe("wmic", WMIC_SNAPSHOT_ARGS, SNAPSHOT_MAX_BUFFER));
|
|
@@ -917,7 +1169,6 @@ function parseWmicCsv(stdout) {
|
|
|
917
1169
|
return {
|
|
918
1170
|
pid: Number.parseInt(cells[atPid] ?? "", 10),
|
|
919
1171
|
ppid: Number.parseInt(cells[atPpid] ?? "", 10),
|
|
920
|
-
// `20260807071417.130820+480` → 留到秒的那 14 位就够比大小了
|
|
921
1172
|
born: normalizeBorn((cells[atBorn] ?? "").slice(0, 14))
|
|
922
1173
|
};
|
|
923
1174
|
});
|
|
@@ -958,7 +1209,6 @@ function runProbe(file, args2, maxBuffer) {
|
|
|
958
1209
|
function delay(ms) {
|
|
959
1210
|
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
960
1211
|
}
|
|
961
|
-
|
|
962
1212
|
// src/shell-parse.ts
|
|
963
1213
|
function defaultFlavor() {
|
|
964
1214
|
return IS_WINDOWS ? "windows" : "posix";
|
|
@@ -1111,7 +1361,7 @@ function parseShellCommand(command, flavor) {
|
|
|
1111
1361
|
const hasSubstitution = kind === "windows" ? detectWindowsSubstitution(stripped) : detectPosixSubstitution(stripped);
|
|
1112
1362
|
const tokens = tokenize(stripped, kind);
|
|
1113
1363
|
if (tokens === null) {
|
|
1114
|
-
return { segments: [], hasSubstitution, parseError: "
|
|
1364
|
+
return { segments: [], hasSubstitution, parseError: t("infra_misc.unclosed_quote"), stripped };
|
|
1115
1365
|
}
|
|
1116
1366
|
const segments = [];
|
|
1117
1367
|
let words = [];
|
|
@@ -1156,7 +1406,6 @@ function hasSideEffectChannel(parsed) {
|
|
|
1156
1406
|
if (parsed.segments.length > 1) return true;
|
|
1157
1407
|
return parsed.segments.some((s) => s.redirects.length > 0);
|
|
1158
1408
|
}
|
|
1159
|
-
|
|
1160
1409
|
// src/command-safety.ts
|
|
1161
1410
|
function rootMatches(rule, root) {
|
|
1162
1411
|
if (typeof rule === "string") return root === rule;
|
|
@@ -1206,7 +1455,6 @@ function inlineCodeExecOf(seg) {
|
|
|
1206
1455
|
return args(seg).some((a) => entry.flags.includes(a)) ? seg.root : null;
|
|
1207
1456
|
}
|
|
1208
1457
|
var CODE_EXEC_ROOTS = [
|
|
1209
|
-
// 解释器
|
|
1210
1458
|
"python",
|
|
1211
1459
|
"python2",
|
|
1212
1460
|
"python3",
|
|
@@ -1218,25 +1466,21 @@ var CODE_EXEC_ROOTS = [
|
|
|
1218
1466
|
"perl",
|
|
1219
1467
|
"php",
|
|
1220
1468
|
"lua",
|
|
1221
|
-
// 包运行器 —— 后面跟什么都能跑
|
|
1222
1469
|
"npx",
|
|
1223
1470
|
"bunx",
|
|
1224
1471
|
"npm run",
|
|
1225
1472
|
"yarn run",
|
|
1226
1473
|
"pnpm run",
|
|
1227
1474
|
"bun run",
|
|
1228
|
-
// shell 与远程执行
|
|
1229
1475
|
...SHELLS,
|
|
1230
1476
|
"ssh",
|
|
1231
1477
|
"eval",
|
|
1232
1478
|
"exec",
|
|
1233
1479
|
"env",
|
|
1234
1480
|
"xargs",
|
|
1235
|
-
// 提权
|
|
1236
1481
|
"sudo",
|
|
1237
1482
|
"doas",
|
|
1238
1483
|
"su",
|
|
1239
|
-
// PowerShell
|
|
1240
1484
|
"powershell",
|
|
1241
1485
|
"pwsh"
|
|
1242
1486
|
];
|
|
@@ -1279,69 +1523,70 @@ var ENCODED_COMMAND_RULE = {
|
|
|
1279
1523
|
const got = m[1].toLowerCase();
|
|
1280
1524
|
return got.length > 0 && got.startsWith("e") && "encodedcommand".startsWith(got);
|
|
1281
1525
|
}),
|
|
1282
|
-
desc:
|
|
1526
|
+
desc: () => t("danger.ps_encoded_command")
|
|
1283
1527
|
};
|
|
1284
1528
|
var POSIX_RULES = [
|
|
1285
1529
|
{
|
|
1286
1530
|
root: "rm",
|
|
1287
1531
|
when: (s) => hasRecursiveForce(s) && args(s).some(isDangerousRmTarget),
|
|
1288
|
-
desc: "
|
|
1532
|
+
desc: () => t("danger.rm_system_root")
|
|
1289
1533
|
},
|
|
1290
1534
|
{
|
|
1291
1535
|
root: "rm",
|
|
1292
1536
|
when: (s) => hasRecursiveForce(s) && args(s).some(isEmptyVarSlash),
|
|
1293
|
-
desc:
|
|
1537
|
+
desc: () => t("danger.rm_empty_var")
|
|
1294
1538
|
},
|
|
1295
1539
|
{
|
|
1296
|
-
// `find / -delete` / `find / -exec rm` —— 老实现整条漏了
|
|
1297
1540
|
root: "find",
|
|
1298
1541
|
when: (s) => args(s).some(isDangerousRmTarget) && args(s).some((a) => a === "-delete" || a === "-exec" || a === "-execdir"),
|
|
1299
|
-
desc:
|
|
1542
|
+
desc: () => t("danger.find_delete_system")
|
|
1300
1543
|
},
|
|
1301
|
-
{ root: /^mkfs(\.\w+)?$/, desc: "
|
|
1544
|
+
{ root: /^mkfs(\.\w+)?$/, desc: () => t("danger.mkfs") },
|
|
1302
1545
|
{
|
|
1303
1546
|
root: "dd",
|
|
1304
1547
|
when: (s) => args(s).some((a) => a.startsWith("of=/dev/")),
|
|
1305
|
-
desc: "
|
|
1548
|
+
desc: () => t("danger.dd_raw_device")
|
|
1549
|
+
},
|
|
1550
|
+
{
|
|
1551
|
+
root: "dd",
|
|
1552
|
+
when: (s) => args(s).some((a) => a.startsWith("if=")),
|
|
1553
|
+
desc: () => t("danger.dd_disk_io")
|
|
1306
1554
|
},
|
|
1307
|
-
{ root: "
|
|
1308
|
-
{ root: "
|
|
1309
|
-
{ root: ["fdisk", "parted"], desc: "\u78C1\u76D8\u5206\u533A\u64CD\u4F5C" },
|
|
1555
|
+
{ root: "shred", desc: () => t("danger.shred") },
|
|
1556
|
+
{ root: ["fdisk", "parted"], desc: () => t("danger.partition") },
|
|
1310
1557
|
{
|
|
1311
1558
|
root: "diskutil",
|
|
1312
1559
|
when: (s) => (args(s)[0] ?? "").toLowerCase().startsWith("erase"),
|
|
1313
|
-
desc: "
|
|
1560
|
+
desc: () => t("danger.partition")
|
|
1314
1561
|
},
|
|
1315
|
-
{ root: "chmod", when: (s) => args(s).includes("777"), desc: "
|
|
1562
|
+
{ root: "chmod", when: (s) => args(s).includes("777"), desc: () => t("danger.chmod_777") },
|
|
1316
1563
|
{
|
|
1317
1564
|
root: "chown",
|
|
1318
1565
|
when: (s) => args(s).some((a) => a === "root" || a.startsWith("root:")),
|
|
1319
|
-
desc:
|
|
1566
|
+
desc: () => t("danger.chown_root")
|
|
1320
1567
|
},
|
|
1321
1568
|
ENCODED_COMMAND_RULE,
|
|
1322
|
-
{ root: ["sudo", "doas"], desc:
|
|
1323
|
-
{ root: "su", when: (s) => args(s).includes("-"), desc:
|
|
1569
|
+
{ root: ["sudo", "doas"], desc: () => t("danger.privilege_posix") },
|
|
1570
|
+
{ root: "su", when: (s) => args(s).includes("-"), desc: () => t("danger.privilege_posix") },
|
|
1324
1571
|
{
|
|
1325
|
-
// 下载后直接执行:上一段是下载工具,这一段是被管道喂进来的 shell
|
|
1326
1572
|
root: SHELLS,
|
|
1327
1573
|
when: (s, prev) => s.pipedInto && prev !== void 0 && FETCHERS.includes(prev.root),
|
|
1328
|
-
desc:
|
|
1574
|
+
desc: () => t("danger.curl_pipe_sh")
|
|
1329
1575
|
},
|
|
1330
1576
|
{
|
|
1331
1577
|
root: INTERPRETERS,
|
|
1332
1578
|
when: (s, prev) => s.pipedInto && prev !== void 0 && FETCHERS.includes(prev.root),
|
|
1333
|
-
desc: "
|
|
1579
|
+
desc: () => t("danger.download_pipe_interpreter")
|
|
1334
1580
|
},
|
|
1335
1581
|
{
|
|
1336
|
-
// 通用「管道进 shell」。放在上面两条之后,让来源明确的先给出更准确的描述
|
|
1337
1582
|
root: SHELLS,
|
|
1338
1583
|
when: (s) => s.pipedInto,
|
|
1339
|
-
desc:
|
|
1584
|
+
desc: () => t("danger.pipe_to_shell")
|
|
1340
1585
|
},
|
|
1341
1586
|
{
|
|
1342
1587
|
root: "git",
|
|
1343
1588
|
when: (s) => args(s)[0] === "push" && args(s).some((a) => a === "--force" || a === "-f" || a === "+"),
|
|
1344
|
-
desc: "
|
|
1589
|
+
desc: () => t("danger.force_push")
|
|
1345
1590
|
},
|
|
1346
1591
|
{
|
|
1347
1592
|
root: "git",
|
|
@@ -1351,77 +1596,80 @@ var POSIX_RULES = [
|
|
|
1351
1596
|
if (sub === "clean") return rest.some((a) => isShortCluster(a) && a.includes("f"));
|
|
1352
1597
|
return false;
|
|
1353
1598
|
},
|
|
1354
|
-
desc: "
|
|
1599
|
+
desc: () => t("danger.discard_changes")
|
|
1355
1600
|
},
|
|
1356
|
-
{ root: ["shutdown", "reboot", "halt", "poweroff"], desc:
|
|
1601
|
+
{ root: ["shutdown", "reboot", "halt", "poweroff"], desc: () => t("danger.shutdown") },
|
|
1357
1602
|
{
|
|
1358
1603
|
root: ["kill", "killall"],
|
|
1359
1604
|
when: (s) => args(s).includes("-9") && args(s).some((a) => a === "-1" || a === "1"),
|
|
1360
|
-
desc:
|
|
1605
|
+
desc: () => t("danger.kill_all")
|
|
1361
1606
|
},
|
|
1362
1607
|
{
|
|
1363
1608
|
root: ["iptables", "ip6tables", "nft", "pfctl"],
|
|
1364
1609
|
when: (s) => args(s).some((a) => a === "-F" || a === "--flush"),
|
|
1365
|
-
desc: "
|
|
1610
|
+
desc: () => t("danger.flush_firewall")
|
|
1366
1611
|
},
|
|
1367
1612
|
{
|
|
1368
1613
|
root: ["launchctl", "systemctl"],
|
|
1369
1614
|
when: (s) => ["disable", "unload"].includes(args(s)[0] ?? ""),
|
|
1370
|
-
desc: "
|
|
1615
|
+
desc: () => t("danger.disable_service")
|
|
1371
1616
|
},
|
|
1372
|
-
{ root: "history", when: (s) => args(s).includes("-c"), desc: "
|
|
1617
|
+
{ root: "history", when: (s) => args(s).includes("-c"), desc: () => t("danger.clear_history") },
|
|
1373
1618
|
{
|
|
1374
1619
|
root: "tee",
|
|
1375
1620
|
when: (s) => args(s).some((a) => a.startsWith("/etc/")),
|
|
1376
|
-
desc:
|
|
1621
|
+
desc: () => t("danger.write_etc")
|
|
1377
1622
|
},
|
|
1378
1623
|
{
|
|
1379
|
-
// 重定向到裸设备 / 历史文件。这条不看 root,任何命令都可能这么写
|
|
1380
1624
|
root: /.*/,
|
|
1381
1625
|
when: (s) => s.redirects.some((r) => isRawDevice(r) || /(^|\/)\.?\w*history$/.test(r)),
|
|
1382
|
-
desc: "
|
|
1626
|
+
desc: () => t("danger.write_device_or_history")
|
|
1383
1627
|
}
|
|
1384
1628
|
];
|
|
1385
1629
|
var WINDOWS_RULES = [
|
|
1386
1630
|
{
|
|
1387
1631
|
root: ["del", "erase", "rd", "rmdir"],
|
|
1388
1632
|
when: hasDangerousWinTarget,
|
|
1389
|
-
desc:
|
|
1633
|
+
desc: () => t("danger.win_rm_system")
|
|
1390
1634
|
},
|
|
1391
1635
|
{
|
|
1392
1636
|
root: "remove-item",
|
|
1393
1637
|
when: (s) => hasWinSwitch(s, "recurse") && hasDangerousWinTarget(s),
|
|
1394
|
-
desc:
|
|
1638
|
+
desc: () => t("danger.ps_rm_system")
|
|
1395
1639
|
},
|
|
1396
1640
|
{
|
|
1397
1641
|
root: "format",
|
|
1398
1642
|
when: (s) => args(s).some((a) => /^[a-z]:$/i.test(a)),
|
|
1399
|
-
desc: "
|
|
1643
|
+
desc: () => t("danger.win_format")
|
|
1400
1644
|
},
|
|
1401
|
-
{ root: "diskpart", desc: "
|
|
1402
|
-
{ root: "cipher", when: (s) => hasWinSwitch(s, "w", true), desc: "
|
|
1645
|
+
{ root: "diskpart", desc: () => t("danger.partition") },
|
|
1646
|
+
{ root: "cipher", when: (s) => hasWinSwitch(s, "w", true), desc: () => t("danger.cipher_wipe") },
|
|
1403
1647
|
{
|
|
1404
1648
|
root: "vssadmin",
|
|
1405
1649
|
when: (s) => (args(s)[0] ?? "").toLowerCase() === "delete",
|
|
1406
|
-
desc: "
|
|
1650
|
+
desc: () => t("danger.delete_shadow_copies")
|
|
1407
1651
|
},
|
|
1408
1652
|
{
|
|
1409
1653
|
root: "wmic",
|
|
1410
1654
|
when: (s) => args(s).map((a) => a.toLowerCase()).join(" ").includes("shadowcopy delete"),
|
|
1411
|
-
desc: "
|
|
1655
|
+
desc: () => t("danger.delete_shadow_copies")
|
|
1412
1656
|
},
|
|
1413
1657
|
{
|
|
1414
1658
|
root: "reg",
|
|
1415
1659
|
when: (s) => (args(s)[0] ?? "").toLowerCase() === "delete" && hasWinSwitch(s, "f", true),
|
|
1416
|
-
desc: "
|
|
1660
|
+
desc: () => t("danger.reg_delete")
|
|
1417
1661
|
},
|
|
1418
|
-
{ root: "bcdedit", desc: "
|
|
1419
|
-
{
|
|
1420
|
-
|
|
1662
|
+
{ root: "bcdedit", desc: () => t("danger.bcdedit") },
|
|
1663
|
+
{
|
|
1664
|
+
root: "sc",
|
|
1665
|
+
when: (s) => (args(s)[0] ?? "").toLowerCase() === "delete",
|
|
1666
|
+
desc: () => t("danger.sc_delete")
|
|
1667
|
+
},
|
|
1668
|
+
{ root: "takeown", when: (s) => hasWinSwitch(s, "r", true), desc: () => t("danger.takeown") },
|
|
1421
1669
|
{
|
|
1422
1670
|
root: "icacls",
|
|
1423
1671
|
when: (s) => hasWinSwitch(s, "grant") && args(s).some((a) => /^(everyone|users)(:|$)/i.test(a)),
|
|
1424
|
-
desc:
|
|
1672
|
+
desc: () => t("danger.icacls_everyone")
|
|
1425
1673
|
},
|
|
1426
1674
|
{
|
|
1427
1675
|
root: "netsh",
|
|
@@ -1429,75 +1677,82 @@ var WINDOWS_RULES = [
|
|
|
1429
1677
|
const lower = args(s).map((a) => a.toLowerCase());
|
|
1430
1678
|
return (lower[0] === "advfirewall" || lower[0] === "firewall") && lower.some((a) => a === "off" || a === "disable");
|
|
1431
1679
|
},
|
|
1432
|
-
desc: "
|
|
1680
|
+
desc: () => t("danger.disable_firewall")
|
|
1433
1681
|
},
|
|
1434
1682
|
{
|
|
1435
1683
|
root: "set-mppreference",
|
|
1436
1684
|
when: (s) => hasWinSwitch(s, "disablerealtimemonitoring"),
|
|
1437
|
-
desc:
|
|
1685
|
+
desc: () => t("danger.disable_defender")
|
|
1438
1686
|
},
|
|
1439
1687
|
{
|
|
1440
1688
|
root: "net",
|
|
1441
1689
|
when: (s) => ["user", "localgroup"].includes((args(s)[0] ?? "").toLowerCase()) && hasWinSwitch(s, "add", true),
|
|
1442
|
-
desc:
|
|
1690
|
+
desc: () => t("danger.add_admin_user")
|
|
1443
1691
|
},
|
|
1444
|
-
{ root: "runas", desc: "
|
|
1692
|
+
{ root: "runas", desc: () => t("danger.privilege_runas") },
|
|
1445
1693
|
{
|
|
1446
1694
|
root: "start-process",
|
|
1447
1695
|
when: (s) => (winSwitchValue(s, "verb") ?? "").toLowerCase() === "runas",
|
|
1448
|
-
desc:
|
|
1696
|
+
desc: () => t("danger.privilege_start_process")
|
|
1449
1697
|
},
|
|
1450
1698
|
{
|
|
1451
1699
|
root: ["iex", "invoke-expression"],
|
|
1452
1700
|
when: (s) => s.pipedInto,
|
|
1453
|
-
desc:
|
|
1701
|
+
desc: () => t("danger.iwr_iex")
|
|
1454
1702
|
},
|
|
1455
1703
|
{
|
|
1456
1704
|
root: ["powershell", "pwsh", "cmd"],
|
|
1457
1705
|
when: (s, prev) => s.pipedInto && prev !== void 0 && ["curl", "wget", "iwr", "irm", "invoke-webrequest", "invoke-restmethod"].includes(prev.root),
|
|
1458
|
-
desc:
|
|
1706
|
+
desc: () => t("danger.download_pipe_shell")
|
|
1459
1707
|
},
|
|
1460
1708
|
{
|
|
1461
1709
|
root: "certutil",
|
|
1462
1710
|
when: (s) => hasWinSwitch(s, "urlcache"),
|
|
1463
|
-
desc:
|
|
1711
|
+
desc: () => t("danger.certutil_download")
|
|
1712
|
+
},
|
|
1713
|
+
{
|
|
1714
|
+
root: "bitsadmin",
|
|
1715
|
+
when: (s) => hasWinSwitch(s, "transfer"),
|
|
1716
|
+
desc: () => t("danger.bitsadmin")
|
|
1464
1717
|
},
|
|
1465
|
-
{ root: "bitsadmin", when: (s) => hasWinSwitch(s, "transfer"), desc: "bitsadmin \u540E\u53F0\u4E0B\u8F7D" },
|
|
1466
1718
|
ENCODED_COMMAND_RULE,
|
|
1467
1719
|
{
|
|
1468
1720
|
root: ["powershell", "pwsh"],
|
|
1469
1721
|
when: (s) => hasWinSwitch(s, "noprofile") || (winSwitchValue(s, "windowstyle") ?? "").toLowerCase() === "hidden" || (winSwitchValue(s, "w") ?? "").toLowerCase() === "hidden",
|
|
1470
|
-
desc:
|
|
1722
|
+
desc: () => t("danger.ps_hidden_window")
|
|
1471
1723
|
},
|
|
1472
1724
|
{
|
|
1473
1725
|
root: ["powershell", "pwsh"],
|
|
1474
1726
|
when: (s) => ["bypass", "unrestricted"].includes(
|
|
1475
1727
|
(winSwitchValue(s, "executionpolicy") ?? "").toLowerCase()
|
|
1476
1728
|
),
|
|
1477
|
-
desc:
|
|
1729
|
+
desc: () => t("danger.ps_bypass_policy")
|
|
1478
1730
|
},
|
|
1479
|
-
{ root: "mshta", desc: "mshta
|
|
1480
|
-
{ root: "rundll32", desc:
|
|
1731
|
+
{ root: "mshta", desc: () => t("danger.mshta") },
|
|
1732
|
+
{ root: "rundll32", desc: () => t("danger.rundll32") },
|
|
1481
1733
|
{
|
|
1482
1734
|
root: "regsvr32",
|
|
1483
1735
|
when: (s) => args(s).some((a) => /^[/-]i:/i.test(a)),
|
|
1484
|
-
desc:
|
|
1736
|
+
desc: () => t("danger.regsvr32")
|
|
1485
1737
|
},
|
|
1486
1738
|
{
|
|
1487
1739
|
root: "shutdown",
|
|
1488
1740
|
when: (s) => hasWinSwitch(s, "s", true) || hasWinSwitch(s, "r", true),
|
|
1489
|
-
desc:
|
|
1741
|
+
desc: () => t("danger.shutdown")
|
|
1490
1742
|
},
|
|
1491
|
-
{ root: ["restart-computer", "stop-computer"], desc:
|
|
1743
|
+
{ root: ["restart-computer", "stop-computer"], desc: () => t("danger.shutdown") },
|
|
1492
1744
|
{
|
|
1493
1745
|
root: "schtasks",
|
|
1494
1746
|
when: (s) => hasWinSwitch(s, "create", true),
|
|
1495
|
-
desc: "
|
|
1747
|
+
desc: () => t("danger.schtasks_create")
|
|
1496
1748
|
},
|
|
1497
|
-
{
|
|
1749
|
+
{
|
|
1750
|
+
root: ["new-scheduledtask", "register-scheduledtask"],
|
|
1751
|
+
desc: () => t("danger.register_scheduled_task")
|
|
1752
|
+
}
|
|
1498
1753
|
];
|
|
1499
1754
|
var WHOLE_STRING_RULES = [
|
|
1500
|
-
{ pattern: /:\s*\(\s*\)\s*\{.*\}\s*;?\s*:/, desc:
|
|
1755
|
+
{ pattern: /:\s*\(\s*\)\s*\{.*\}\s*;?\s*:/, desc: () => t("danger.fork_bomb") }
|
|
1501
1756
|
];
|
|
1502
1757
|
var WINDOWS_TABLE = [...WINDOWS_RULES, ...POSIX_RULES];
|
|
1503
1758
|
function parseForSafety(command, platform10) {
|
|
@@ -1511,7 +1766,7 @@ function checkDangerousCommand(command, opts) {
|
|
|
1511
1766
|
const platform10 = opts?.platform ?? currentPlatform();
|
|
1512
1767
|
const parsed = parseForSafety(command, platform10);
|
|
1513
1768
|
for (const { pattern, desc } of WHOLE_STRING_RULES) {
|
|
1514
|
-
if (pattern.test(parsed.stripped)) return { dangerous: true, desc };
|
|
1769
|
+
if (pattern.test(parsed.stripped)) return { dangerous: true, desc: desc() };
|
|
1515
1770
|
}
|
|
1516
1771
|
const table = platform10 === "win32" ? WINDOWS_TABLE : POSIX_RULES;
|
|
1517
1772
|
for (let i = 0; i < parsed.segments.length; i++) {
|
|
@@ -1520,7 +1775,7 @@ function checkDangerousCommand(command, opts) {
|
|
|
1520
1775
|
for (const rule of table) {
|
|
1521
1776
|
if (!rootMatches(rule.root, seg.root)) continue;
|
|
1522
1777
|
if (rule.when && !rule.when(seg, prev)) continue;
|
|
1523
|
-
return { dangerous: true, desc: rule.desc };
|
|
1778
|
+
return { dangerous: true, desc: rule.desc() };
|
|
1524
1779
|
}
|
|
1525
1780
|
}
|
|
1526
1781
|
return { dangerous: false };
|
|
@@ -1598,29 +1853,31 @@ function isReadOnlyCommand(command) {
|
|
|
1598
1853
|
});
|
|
1599
1854
|
}
|
|
1600
1855
|
var OBFUSCATION_PATTERNS = [
|
|
1601
|
-
{ pattern: /\beval\b/, desc:
|
|
1602
|
-
{ pattern: /\bexec\s+\$/, desc:
|
|
1603
|
-
{ pattern: /\bbase64\s+(-d|--decode)/, desc:
|
|
1604
|
-
{ pattern: /\b(xxd|od)\s+-r/, desc: "
|
|
1605
|
-
{ pattern: /\|\s*(ba|z|k|da)?sh\b/, desc:
|
|
1606
|
-
{ pattern: /\\x[0-9a-f]{2}/i, desc: "
|
|
1607
|
-
{ pattern: /\$\{[^}]*[:#%/][^}]*\}/, desc: "
|
|
1608
|
-
{ pattern: /\bprintf\b[^|;&]*\|/, desc:
|
|
1609
|
-
{ pattern: /\bcurl\b[^|;&]*\|/, desc: "
|
|
1856
|
+
{ pattern: /\beval\b/, desc: () => t("danger.obf_eval") },
|
|
1857
|
+
{ pattern: /\bexec\s+\$/, desc: () => t("danger.obf_exec_var") },
|
|
1858
|
+
{ pattern: /\bbase64\s+(-d|--decode)/, desc: () => t("danger.obf_base64") },
|
|
1859
|
+
{ pattern: /\b(xxd|od)\s+-r/, desc: () => t("danger.obf_hex_restore") },
|
|
1860
|
+
{ pattern: /\|\s*(ba|z|k|da)?sh\b/, desc: () => t("danger.obf_pipe_shell") },
|
|
1861
|
+
{ pattern: /\\x[0-9a-f]{2}/i, desc: () => t("danger.obf_hex_escape") },
|
|
1862
|
+
{ pattern: /\$\{[^}]*[:#%/][^}]*\}/, desc: () => t("danger.obf_var_expansion") },
|
|
1863
|
+
{ pattern: /\bprintf\b[^|;&]*\|/, desc: () => t("danger.obf_printf_pipe") },
|
|
1864
|
+
{ pattern: /\bcurl\b[^|;&]*\|/, desc: () => t("danger.obf_curl_pipe") }
|
|
1610
1865
|
];
|
|
1611
1866
|
function checkObfuscation(command) {
|
|
1612
1867
|
const parsed = parseForSafety(command, currentPlatform());
|
|
1613
|
-
if (parsed.parseError !== void 0)
|
|
1868
|
+
if (parsed.parseError !== void 0) {
|
|
1869
|
+
return t("infra_misc.parse_failed", { reason: parsed.parseError });
|
|
1870
|
+
}
|
|
1614
1871
|
if (parsed.hasSubstitution) {
|
|
1615
|
-
return /`/.test(parsed.stripped) && !/\$\(/.test(parsed.stripped) ? "
|
|
1872
|
+
return /`/.test(parsed.stripped) && !/\$\(/.test(parsed.stripped) ? t("infra_misc.backtick_substitution") : t("infra_misc.dollar_substitution");
|
|
1616
1873
|
}
|
|
1617
1874
|
for (const seg of parsed.segments) {
|
|
1618
1875
|
const interpreter = inlineCodeExecOf(seg);
|
|
1619
|
-
if (interpreter !== null) return
|
|
1876
|
+
if (interpreter !== null) return t("infra_misc.inline_code", { interpreter });
|
|
1620
1877
|
}
|
|
1621
1878
|
const norm = command.replace(/\s+/g, " ").trim();
|
|
1622
1879
|
for (const { pattern, desc } of OBFUSCATION_PATTERNS) {
|
|
1623
|
-
if (pattern.test(norm)) return desc;
|
|
1880
|
+
if (pattern.test(norm)) return desc();
|
|
1624
1881
|
}
|
|
1625
1882
|
return null;
|
|
1626
1883
|
}
|
|
@@ -1705,11 +1962,7 @@ var SYSTEM_READ_PATHS = [
|
|
|
1705
1962
|
"/private/var/db",
|
|
1706
1963
|
"/private/var/folders",
|
|
1707
1964
|
"/private/var/select",
|
|
1708
|
-
// xcode-select 要读它,否则 macOS 自带 python3 起不来
|
|
1709
1965
|
"/private/etc",
|
|
1710
|
-
// macOS 自带的 /usr/bin/python3 是个 Xcode shim:它会去跑 xcodebuild,
|
|
1711
|
-
// 需要读整个 Xcode.app(Info.plist + SharedFrameworks)。
|
|
1712
|
-
// Xcode.app 是公开的应用包,放行读取不泄露任何用户数据。
|
|
1713
1966
|
"/Applications/Xcode.app",
|
|
1714
1967
|
"/Library/Developer"
|
|
1715
1968
|
];
|
|
@@ -1745,9 +1998,6 @@ var SENSITIVE_HOME_PATHS = [
|
|
|
1745
1998
|
"Library/Application Support/Firefox",
|
|
1746
1999
|
"Library/Cookies",
|
|
1747
2000
|
"Library/Messages",
|
|
1748
|
-
// agent 自己的数据:会话、记忆、审批、API key 都在这。
|
|
1749
|
-
// 注意沙箱目录本身就在 ~/.epoch/sandbox 下,所以 buildProfile 必须
|
|
1750
|
-
// 在这条 deny **之后**重新 allow writableDirs,否则连自己的 code.js 都读不到。
|
|
1751
2001
|
".epoch"
|
|
1752
2002
|
];
|
|
1753
2003
|
var SENSITIVE_ABS_PATHS = ["/etc/shadow", "/etc/sudoers", "/private/etc/master.passwd"];
|
|
@@ -1809,7 +2059,6 @@ function networkSection(allowNetwork) {
|
|
|
1809
2059
|
return [
|
|
1810
2060
|
";; ---- \u7F51\u7EDC ----",
|
|
1811
2061
|
"(deny network*)",
|
|
1812
|
-
// 留一个本机 unix socket,否则解析库初始化就可能失败
|
|
1813
2062
|
'(allow network-outbound (literal "/private/var/run/mDNSResponder"))'
|
|
1814
2063
|
];
|
|
1815
2064
|
}
|
|
@@ -1885,7 +2134,6 @@ function isolateWithSeatbelt(command, args2, opts) {
|
|
|
1885
2134
|
]
|
|
1886
2135
|
};
|
|
1887
2136
|
}
|
|
1888
|
-
|
|
1889
2137
|
// src/sandbox/backend.ts
|
|
1890
2138
|
function detectBackend() {
|
|
1891
2139
|
if (platform() === "darwin" && probeSeatbelt()) return "seatbelt";
|
|
@@ -1902,7 +2150,6 @@ function isolate(command, args2, opts) {
|
|
|
1902
2150
|
return null;
|
|
1903
2151
|
}
|
|
1904
2152
|
}
|
|
1905
|
-
|
|
1906
2153
|
// src/sandbox/classify.ts
|
|
1907
2154
|
var DENIAL_SIGNATURES = {
|
|
1908
2155
|
seatbelt: [/Operation not permitted/i, /\bEPERM\b/],
|
|
@@ -1912,13 +2159,10 @@ var DENIAL_SIGNATURES = {
|
|
|
1912
2159
|
var RUNNER_FAILURE_RULES = {
|
|
1913
2160
|
seatbelt: {
|
|
1914
2161
|
fatal: [/^sandbox-exec:/],
|
|
1915
|
-
// sandbox-exec 在 macOS 上已标记 deprecated,它可能(现在或将来)
|
|
1916
|
-
// 往 stderr 打一句提示。那是**信息**不是失败,见方案 46 验收 7
|
|
1917
2162
|
benign: [/^sandbox-exec:.*\bdeprecat/i]
|
|
1918
2163
|
},
|
|
1919
2164
|
bubblewrap: {
|
|
1920
2165
|
fatal: [/^bwrap:/],
|
|
1921
|
-
// bwrap 在部分内核上会为无害的降级打一行提示(比如某个 unshare 跳过了)
|
|
1922
2166
|
benign: [/^bwrap:.*\b(?:skipping|ignoring|deprecat)/i]
|
|
1923
2167
|
},
|
|
1924
2168
|
none: { fatal: [], benign: [] }
|
|
@@ -1979,6 +2223,9 @@ function writableDirsFor(policy) {
|
|
|
1979
2223
|
}
|
|
1980
2224
|
function confine(command, args2, policy) {
|
|
1981
2225
|
const backend = detectBackend();
|
|
2226
|
+
if (policy.enabled === false) {
|
|
2227
|
+
return { confined: false, backend, mode: policy.mode, reason: "config-disabled" };
|
|
2228
|
+
}
|
|
1982
2229
|
if (policy.mode === "danger-full-access") {
|
|
1983
2230
|
return { confined: false, backend, mode: policy.mode, reason: "mode-disabled" };
|
|
1984
2231
|
}
|
|
@@ -2004,40 +2251,299 @@ function confine(command, args2, policy) {
|
|
|
2004
2251
|
runnerFailureRules: RUNNER_FAILURE_RULES[backend]
|
|
2005
2252
|
};
|
|
2006
2253
|
}
|
|
2254
|
+
// src/jobs.ts
|
|
2255
|
+
var bySession2 = /* @__PURE__ */ new Map();
|
|
2256
|
+
var PREFIX = { command: "t", shell: "s", agent: "a" };
|
|
2257
|
+
var counters = { command: 0, shell: 0, agent: 0 };
|
|
2258
|
+
function nextId(kind) {
|
|
2259
|
+
counters[kind] += 1;
|
|
2260
|
+
return `${PREFIX[kind]}${counters[kind]}`;
|
|
2261
|
+
}
|
|
2262
|
+
function bucketOf(sessionId) {
|
|
2263
|
+
const existing = bySession2.get(sessionId);
|
|
2264
|
+
if (existing) return existing;
|
|
2265
|
+
const fresh = /* @__PURE__ */ new Map();
|
|
2266
|
+
bySession2.set(sessionId, fresh);
|
|
2267
|
+
return fresh;
|
|
2268
|
+
}
|
|
2269
|
+
function jobIn(sessionId, id) {
|
|
2270
|
+
return bySession2.get(sessionId)?.get(id);
|
|
2271
|
+
}
|
|
2272
|
+
function append(job, chunk) {
|
|
2273
|
+
job.info.outputBytes += chunk.length;
|
|
2274
|
+
job.buffer += chunk;
|
|
2275
|
+
if (job.buffer.length <= job.spec.maxOutput) return;
|
|
2276
|
+
if (job.spill === void 0) {
|
|
2277
|
+
job.spill = openArtifact(job.spec.artifactsDir, `${job.spec.artifactLabel}-${job.info.id}`, ".txt") ?? null;
|
|
2278
|
+
job.spill?.append(job.buffer);
|
|
2279
|
+
if (job.spill) job.info.artifact = job.spill.path;
|
|
2280
|
+
} else {
|
|
2281
|
+
job.spill?.append(chunk);
|
|
2282
|
+
}
|
|
2283
|
+
const drop = job.buffer.length - job.spec.maxOutput;
|
|
2284
|
+
job.buffer = job.buffer.slice(drop);
|
|
2285
|
+
job.droppedBytes += drop;
|
|
2286
|
+
job.info.truncated = true;
|
|
2287
|
+
}
|
|
2288
|
+
function endJob(job, status, exitCode) {
|
|
2289
|
+
if (job.info.status !== "running") return;
|
|
2290
|
+
job.info.status = status;
|
|
2291
|
+
if (exitCode !== void 0) job.info.exitCode = exitCode;
|
|
2292
|
+
job.info.endedAt = Date.now();
|
|
2293
|
+
job.spill?.close();
|
|
2294
|
+
job.settle();
|
|
2295
|
+
}
|
|
2296
|
+
function registerJob(spec) {
|
|
2297
|
+
const now = Date.now();
|
|
2298
|
+
let settle;
|
|
2299
|
+
const done = new Promise((resolve5) => {
|
|
2300
|
+
settle = resolve5;
|
|
2301
|
+
});
|
|
2302
|
+
const job = {
|
|
2303
|
+
spec,
|
|
2304
|
+
info: {
|
|
2305
|
+
id: nextId(spec.kind),
|
|
2306
|
+
kind: spec.kind,
|
|
2307
|
+
label: spec.label,
|
|
2308
|
+
...spec.cwd ? { cwd: spec.cwd } : {},
|
|
2309
|
+
pid: 0,
|
|
2310
|
+
status: "running",
|
|
2311
|
+
startedAt: now,
|
|
2312
|
+
lastActivity: now,
|
|
2313
|
+
outputBytes: 0,
|
|
2314
|
+
truncated: false
|
|
2315
|
+
},
|
|
2316
|
+
buffer: "",
|
|
2317
|
+
droppedBytes: 0,
|
|
2318
|
+
done,
|
|
2319
|
+
settle
|
|
2320
|
+
};
|
|
2321
|
+
bucketOf(spec.sessionId).set(job.info.id, job);
|
|
2322
|
+
return {
|
|
2323
|
+
id: job.info.id,
|
|
2324
|
+
detail: spec.detail,
|
|
2325
|
+
info: () => ({ ...job.info }),
|
|
2326
|
+
status: () => job.info.status,
|
|
2327
|
+
setPid: (pid) => {
|
|
2328
|
+
job.info.pid = pid;
|
|
2329
|
+
},
|
|
2330
|
+
append: (chunk) => append(job, chunk),
|
|
2331
|
+
touch: () => {
|
|
2332
|
+
job.info.lastActivity = Date.now();
|
|
2333
|
+
},
|
|
2334
|
+
cursor: () => job.info.outputBytes,
|
|
2335
|
+
finish: (status, exitCode) => endJob(job, status, exitCode)
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
function listJobs(sessionId, kind) {
|
|
2339
|
+
const bucket = bySession2.get(sessionId);
|
|
2340
|
+
if (!bucket) return [];
|
|
2341
|
+
const all = [...bucket.values()].map((job) => ({ ...job.info }));
|
|
2342
|
+
return kind ? all.filter((info) => info.kind === kind) : all;
|
|
2343
|
+
}
|
|
2344
|
+
function getJob(sessionId, id) {
|
|
2345
|
+
const job = jobIn(sessionId, id);
|
|
2346
|
+
return job ? { ...job.info } : void 0;
|
|
2347
|
+
}
|
|
2348
|
+
function jobDetail(sessionId, id) {
|
|
2349
|
+
return jobIn(sessionId, id)?.spec.detail;
|
|
2350
|
+
}
|
|
2351
|
+
function allJobs(kind) {
|
|
2352
|
+
const out = [];
|
|
2353
|
+
for (const [sessionId, bucket] of bySession2) {
|
|
2354
|
+
for (const job of bucket.values()) {
|
|
2355
|
+
if (kind && job.info.kind !== kind) continue;
|
|
2356
|
+
out.push({ sessionId, info: { ...job.info } });
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
return out;
|
|
2360
|
+
}
|
|
2361
|
+
function readJob(sessionId, id, since, maxChunk) {
|
|
2362
|
+
const job = jobIn(sessionId, id);
|
|
2363
|
+
return job ? sliceFrom(job, since, maxChunk) : void 0;
|
|
2364
|
+
}
|
|
2365
|
+
function sliceFrom(job, since, maxChunk) {
|
|
2366
|
+
const from = Math.max(since, job.droppedBytes);
|
|
2367
|
+
const missed = Math.max(0, job.droppedBytes - since);
|
|
2368
|
+
const slice = job.buffer.slice(from - job.droppedBytes);
|
|
2369
|
+
const output = slice.slice(0, maxChunk);
|
|
2370
|
+
return {
|
|
2371
|
+
info: { ...job.info },
|
|
2372
|
+
output,
|
|
2373
|
+
nextCursor: from + output.length,
|
|
2374
|
+
missed,
|
|
2375
|
+
hasMore: output.length < slice.length
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
function touchJob(sessionId, id) {
|
|
2379
|
+
const job = jobIn(sessionId, id);
|
|
2380
|
+
if (job) job.info.lastActivity = Date.now();
|
|
2381
|
+
}
|
|
2382
|
+
function appendJob(sessionId, id, chunk) {
|
|
2383
|
+
const job = jobIn(sessionId, id);
|
|
2384
|
+
if (job) append(job, chunk);
|
|
2385
|
+
}
|
|
2386
|
+
async function waitJob(sessionId, id, timeoutMs) {
|
|
2387
|
+
const job = jobIn(sessionId, id);
|
|
2388
|
+
if (!job) return void 0;
|
|
2389
|
+
const snapshot = () => ({ ...job.info });
|
|
2390
|
+
if (job.info.status !== "running") {
|
|
2391
|
+
return { info: snapshot(), timedOut: false, neverEnds: false };
|
|
2392
|
+
}
|
|
2393
|
+
if (!job.spec.terminates) return { info: snapshot(), timedOut: false, neverEnds: true };
|
|
2394
|
+
let timer;
|
|
2395
|
+
const timeout = new Promise((resolve5) => {
|
|
2396
|
+
timer = setTimeout(() => resolve5("timeout"), timeoutMs);
|
|
2397
|
+
timer.unref?.();
|
|
2398
|
+
});
|
|
2399
|
+
try {
|
|
2400
|
+
const winner = await Promise.race([job.done.then(() => "done"), timeout]);
|
|
2401
|
+
return { info: snapshot(), timedOut: winner === "timeout", neverEnds: false };
|
|
2402
|
+
} finally {
|
|
2403
|
+
if (timer) clearTimeout(timer);
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
function stopJob(sessionId, id, status) {
|
|
2407
|
+
const job = jobIn(sessionId, id);
|
|
2408
|
+
if (!job || job.info.status !== "running") return Promise.resolve(false);
|
|
2409
|
+
const stop = job.spec.stop;
|
|
2410
|
+
endJob(job, status ?? job.spec.stoppedStatus ?? "killed");
|
|
2411
|
+
return stop().then(() => true);
|
|
2412
|
+
}
|
|
2413
|
+
function rekeyJobs(from, to, kinds) {
|
|
2414
|
+
if (from === to) return;
|
|
2415
|
+
const moving = bySession2.get(from);
|
|
2416
|
+
if (!moving) return;
|
|
2417
|
+
const wanted = [...moving].filter(([, job]) => kinds.includes(job.info.kind));
|
|
2418
|
+
if (wanted.length === 0) return;
|
|
2419
|
+
for (const [id] of wanted) moving.delete(id);
|
|
2420
|
+
if (moving.size === 0) bySession2.delete(from);
|
|
2421
|
+
const target = bucketOf(to);
|
|
2422
|
+
for (const [id, job] of wanted) {
|
|
2423
|
+
job.spec.sessionId = to;
|
|
2424
|
+
target.set(id, job);
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
function clearAllJobs(kinds) {
|
|
2428
|
+
for (const [sessionId, bucket] of bySession2) {
|
|
2429
|
+
for (const [id, job] of bucket) {
|
|
2430
|
+
if (kinds && !kinds.includes(job.info.kind)) continue;
|
|
2431
|
+
job.spill?.close();
|
|
2432
|
+
job.settle();
|
|
2433
|
+
bucket.delete(id);
|
|
2434
|
+
}
|
|
2435
|
+
if (bucket.size === 0) bySession2.delete(sessionId);
|
|
2436
|
+
}
|
|
2437
|
+
for (const kind of kinds ?? ["command", "shell", "agent"]) counters[kind] = 0;
|
|
2438
|
+
}
|
|
2007
2439
|
var EXEC_PATH_AS_NODE_ENV = {
|
|
2008
2440
|
ELECTRON_RUN_AS_NODE: "1"
|
|
2009
2441
|
};
|
|
2010
|
-
var
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2442
|
+
var owners = /* @__PURE__ */ new Map();
|
|
2443
|
+
var bySession3 = /* @__PURE__ */ new Map();
|
|
2444
|
+
var TableImpl = class {
|
|
2445
|
+
constructor(fallback) {
|
|
2446
|
+
this.fallback = fallback;
|
|
2447
|
+
}
|
|
2448
|
+
fallback;
|
|
2449
|
+
entries = /* @__PURE__ */ new Map();
|
|
2450
|
+
sessions = /* @__PURE__ */ new Set();
|
|
2451
|
+
released = false;
|
|
2452
|
+
adopt(pid, entry) {
|
|
2453
|
+
this.entries.set(pid, entry);
|
|
2454
|
+
owners.set(pid, this);
|
|
2455
|
+
}
|
|
2456
|
+
forget(pid) {
|
|
2457
|
+
this.entries.delete(pid);
|
|
2458
|
+
if (owners.get(pid) === this) owners.delete(pid);
|
|
2459
|
+
}
|
|
2460
|
+
take(pid) {
|
|
2461
|
+
const entry = this.entries.get(pid);
|
|
2462
|
+
this.forget(pid);
|
|
2463
|
+
return entry;
|
|
2464
|
+
}
|
|
2465
|
+
start(opts) {
|
|
2466
|
+
const child = spawn(opts.file, [...opts.args], {
|
|
2467
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2468
|
+
detached: false,
|
|
2469
|
+
...opts.cwd ? { cwd: opts.cwd } : {},
|
|
2470
|
+
...opts.spawnOptions
|
|
2024
2471
|
});
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2472
|
+
const pid = child.pid ?? 0;
|
|
2473
|
+
if (pid > 0) this.adopt(pid, { kind: "child", child });
|
|
2474
|
+
const pump = (stream) => {
|
|
2475
|
+
child[stream]?.on("data", (buf) => {
|
|
2476
|
+
opts.onOutput?.(buf.toString("utf8"), stream);
|
|
2477
|
+
});
|
|
2478
|
+
};
|
|
2479
|
+
pump("stdout");
|
|
2480
|
+
pump("stderr");
|
|
2481
|
+
child.on("exit", (code, signal) => {
|
|
2482
|
+
this.forget(pid);
|
|
2483
|
+
opts.onExit?.(code, signal);
|
|
2484
|
+
});
|
|
2485
|
+
child.on("error", () => {
|
|
2486
|
+
this.forget(pid);
|
|
2487
|
+
opts.onExit?.(null, null);
|
|
2488
|
+
});
|
|
2489
|
+
return { pid, child };
|
|
2490
|
+
}
|
|
2491
|
+
trackForeign(pid, kill) {
|
|
2492
|
+
if (pid <= 0) return () => {
|
|
2493
|
+
};
|
|
2494
|
+
this.adopt(pid, { kind: "foreign", kill });
|
|
2495
|
+
return () => {
|
|
2496
|
+
if (this.entries.get(pid)?.kind === "foreign") this.forget(pid);
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
count() {
|
|
2500
|
+
return this.entries.size;
|
|
2501
|
+
}
|
|
2502
|
+
async killAll() {
|
|
2503
|
+
const pids = [...this.entries.keys()];
|
|
2504
|
+
await Promise.all(pids.map((pid) => killTrackedProcess(pid)));
|
|
2505
|
+
}
|
|
2506
|
+
claim(sessionId) {
|
|
2507
|
+
if (this.fallback || !sessionId) return;
|
|
2508
|
+
bySession3.set(sessionId, this);
|
|
2509
|
+
this.sessions.add(sessionId);
|
|
2510
|
+
}
|
|
2511
|
+
async release() {
|
|
2512
|
+
if (this.fallback || this.released) return;
|
|
2513
|
+
this.released = true;
|
|
2514
|
+
for (const sid of this.sessions) if (bySession3.get(sid) === this) bySession3.delete(sid);
|
|
2515
|
+
this.sessions.clear();
|
|
2516
|
+
const last = --fallbackRefs <= 0;
|
|
2517
|
+
if (last) fallbackRefs = 0;
|
|
2518
|
+
await Promise.all(last ? [this.killAll(), fallbackTable.killAll()] : [this.killAll()]);
|
|
2519
|
+
}
|
|
2520
|
+
};
|
|
2521
|
+
var fallbackTable = new TableImpl(true);
|
|
2522
|
+
var fallbackRefs = 0;
|
|
2523
|
+
function createProcessTable() {
|
|
2524
|
+
fallbackRefs++;
|
|
2525
|
+
return new TableImpl(false);
|
|
2526
|
+
}
|
|
2527
|
+
function processTableFor(sessionId) {
|
|
2528
|
+
if (!sessionId) return fallbackTable;
|
|
2529
|
+
return bySession3.get(sessionId) ?? fallbackTable;
|
|
2530
|
+
}
|
|
2531
|
+
function processFallbackTable() {
|
|
2532
|
+
return fallbackTable;
|
|
2533
|
+
}
|
|
2534
|
+
function startLongLivedProcess(opts) {
|
|
2535
|
+
return fallbackTable.start(opts);
|
|
2536
|
+
}
|
|
2537
|
+
function trackForeignProcess(pid, kill) {
|
|
2538
|
+
return fallbackTable.trackForeign(pid, kill);
|
|
2037
2539
|
}
|
|
2038
2540
|
async function killTrackedProcess(pid) {
|
|
2039
|
-
const
|
|
2040
|
-
|
|
2541
|
+
const entry = owners.get(pid)?.take(pid);
|
|
2542
|
+
if (entry?.kind === "foreign") {
|
|
2543
|
+
await entry.kill();
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
const child = entry?.child;
|
|
2041
2547
|
await killProcessTree({
|
|
2042
2548
|
pid,
|
|
2043
2549
|
detached: false,
|
|
@@ -2046,10 +2552,10 @@ async function killTrackedProcess(pid) {
|
|
|
2046
2552
|
});
|
|
2047
2553
|
}
|
|
2048
2554
|
function trackedProcessCount() {
|
|
2049
|
-
return
|
|
2555
|
+
return owners.size;
|
|
2050
2556
|
}
|
|
2051
2557
|
async function killAllTrackedProcesses() {
|
|
2052
|
-
const pids = [...
|
|
2558
|
+
const pids = [...owners.keys()];
|
|
2053
2559
|
await Promise.all(pids.map((pid) => killTrackedProcess(pid)));
|
|
2054
2560
|
}
|
|
2055
2561
|
function within(root, abs) {
|
|
@@ -2063,7 +2569,6 @@ function isInWorkspace(target, workDir, extraRoots = []) {
|
|
|
2063
2569
|
if (within(root, abs)) return true;
|
|
2064
2570
|
return extraRoots.some((r) => within(resolve$1(r), abs));
|
|
2065
2571
|
}
|
|
2066
|
-
|
|
2067
2572
|
// src/schema.ts
|
|
2068
2573
|
function parseLenient(schema, input, fallback) {
|
|
2069
2574
|
const result = schema.safeParse(input);
|
|
@@ -2075,8 +2580,7 @@ function parseStrict(schema, input, label) {
|
|
|
2075
2580
|
if (result.success) return result.data;
|
|
2076
2581
|
const issues = toIssues(result.error.issues);
|
|
2077
2582
|
const detail = issues.map((i) => ` - ${i.path}: ${i.message}`).join("\n");
|
|
2078
|
-
throw new Error(
|
|
2079
|
-
${detail}`);
|
|
2583
|
+
throw new Error(t("infra_misc.schema_failed", { path: label, count: issues.length, detail }));
|
|
2080
2584
|
}
|
|
2081
2585
|
var DEFAULT_ISSUE_LIMIT = 8;
|
|
2082
2586
|
function formatIssues(label, issues, limit = DEFAULT_ISSUE_LIMIT) {
|
|
@@ -2086,7 +2590,7 @@ function issueDetails(issues, limit = DEFAULT_ISSUE_LIMIT) {
|
|
|
2086
2590
|
if (issues.length === 0) return [];
|
|
2087
2591
|
const shown = issues.slice(0, limit).map((i) => `${i.path} \u2014\u2014 ${i.message}`);
|
|
2088
2592
|
const rest = issues.length - shown.length;
|
|
2089
|
-
if (rest > 0) shown.push(
|
|
2593
|
+
if (rest > 0) shown.push(t("infra_misc.schema_more", { count: rest }));
|
|
2090
2594
|
return shown;
|
|
2091
2595
|
}
|
|
2092
2596
|
function unknownKeyIssues(known, input, prefix = "") {
|
|
@@ -2094,7 +2598,7 @@ function unknownKeyIssues(known, input, prefix = "") {
|
|
|
2094
2598
|
const knownSet = new Set(known);
|
|
2095
2599
|
return Object.keys(input).filter((key) => !knownSet.has(key)).map((key) => ({
|
|
2096
2600
|
path: prefix ? `${prefix}.${key}` : key,
|
|
2097
|
-
message: "
|
|
2601
|
+
message: t("infra_misc.schema_unknown_key")
|
|
2098
2602
|
}));
|
|
2099
2603
|
}
|
|
2100
2604
|
var MAX_NEST_DEPTH = 5;
|
|
@@ -2129,7 +2633,6 @@ function dedupe(issues) {
|
|
|
2129
2633
|
return true;
|
|
2130
2634
|
});
|
|
2131
2635
|
}
|
|
2132
|
-
|
|
2133
2636
|
// src/async.ts
|
|
2134
2637
|
async function withTimeout(promise, ms, fallback) {
|
|
2135
2638
|
let timer;
|
|
@@ -2158,151 +2661,6 @@ function safeInit(name, factory, diags) {
|
|
|
2158
2661
|
return null;
|
|
2159
2662
|
}
|
|
2160
2663
|
}
|
|
2161
|
-
var LANGS = ["zh", "en"];
|
|
2162
|
-
var DEFAULT_LANG = "zh";
|
|
2163
|
-
function isLang(value) {
|
|
2164
|
-
return LANGS.includes(value);
|
|
2165
|
-
}
|
|
2166
|
-
var catalogs = /* @__PURE__ */ new Map();
|
|
2167
|
-
var pending = [];
|
|
2168
|
-
var localesDirCache;
|
|
2169
|
-
var currentLangValue;
|
|
2170
|
-
function localesDir() {
|
|
2171
|
-
if (localesDirCache !== void 0) return localesDirCache ?? void 0;
|
|
2172
|
-
const override = process.env.EPOCH_LOCALES_DIR;
|
|
2173
|
-
if (override) {
|
|
2174
|
-
localesDirCache = existsSync(override) ? override : null;
|
|
2175
|
-
return localesDirCache ?? void 0;
|
|
2176
|
-
}
|
|
2177
|
-
let dir = dirname(fileURLToPath(import.meta.url));
|
|
2178
|
-
for (let depth = 0; depth < 8; depth += 1) {
|
|
2179
|
-
if (basename(dir) === "node_modules") break;
|
|
2180
|
-
const candidate = join(dir, "locales");
|
|
2181
|
-
if (existsSync(join(candidate, `${DEFAULT_LANG}.yaml`))) {
|
|
2182
|
-
localesDirCache = candidate;
|
|
2183
|
-
return candidate;
|
|
2184
|
-
}
|
|
2185
|
-
const parent = dirname(dir);
|
|
2186
|
-
if (parent === dir) break;
|
|
2187
|
-
dir = parent;
|
|
2188
|
-
}
|
|
2189
|
-
localesDirCache = null;
|
|
2190
|
-
return void 0;
|
|
2191
|
-
}
|
|
2192
|
-
function flatten(value, prefix, out) {
|
|
2193
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
2194
|
-
for (const [key, child] of Object.entries(value)) {
|
|
2195
|
-
const path = prefix ? `${prefix}.${key}` : key;
|
|
2196
|
-
if (typeof child === "string") out[path] = child;
|
|
2197
|
-
else flatten(child, path, out);
|
|
2198
|
-
}
|
|
2199
|
-
}
|
|
2200
|
-
function loadCatalog(lang) {
|
|
2201
|
-
const cached2 = catalogs.get(lang);
|
|
2202
|
-
if (cached2) return cached2;
|
|
2203
|
-
const result = readCatalog(lang);
|
|
2204
|
-
catalogs.set(lang, result);
|
|
2205
|
-
return result;
|
|
2206
|
-
}
|
|
2207
|
-
function readCatalog(lang) {
|
|
2208
|
-
const dir = localesDir();
|
|
2209
|
-
if (!dir) {
|
|
2210
|
-
pending.push({ code: "locales-missing", render: () => t("i18n.locales_missing") });
|
|
2211
|
-
return { entries: {}, error: "\u627E\u4E0D\u5230 locales \u76EE\u5F55" };
|
|
2212
|
-
}
|
|
2213
|
-
const file = `${lang}.yaml`;
|
|
2214
|
-
const path = join(dir, file);
|
|
2215
|
-
if (!existsSync(path)) {
|
|
2216
|
-
pending.push({
|
|
2217
|
-
code: "catalog-missing",
|
|
2218
|
-
render: () => t("i18n.catalog_missing", { file, lang })
|
|
2219
|
-
});
|
|
2220
|
-
return { entries: {}, error: `${path} \u4E0D\u5B58\u5728` };
|
|
2221
|
-
}
|
|
2222
|
-
let parsed;
|
|
2223
|
-
try {
|
|
2224
|
-
parsed = load(readFileSync(path, "utf-8"));
|
|
2225
|
-
} catch (err) {
|
|
2226
|
-
const reason = (err instanceof Error ? err.message : String(err)).split("\n")[0] ?? "";
|
|
2227
|
-
pending.push({
|
|
2228
|
-
code: "catalog-invalid",
|
|
2229
|
-
render: () => t("i18n.catalog_invalid", { file, reason })
|
|
2230
|
-
});
|
|
2231
|
-
return { entries: {}, error: reason };
|
|
2232
|
-
}
|
|
2233
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2234
|
-
const reason = "\u9876\u5C42\u4E0D\u662F\u952E\u503C\u5BF9";
|
|
2235
|
-
pending.push({
|
|
2236
|
-
code: "catalog-invalid",
|
|
2237
|
-
render: () => t("i18n.catalog_invalid", { file, reason })
|
|
2238
|
-
});
|
|
2239
|
-
return { entries: {}, error: reason };
|
|
2240
|
-
}
|
|
2241
|
-
const entries = {};
|
|
2242
|
-
flatten(parsed, "", entries);
|
|
2243
|
-
return { entries };
|
|
2244
|
-
}
|
|
2245
|
-
function resetI18n() {
|
|
2246
|
-
catalogs.clear();
|
|
2247
|
-
pending = [];
|
|
2248
|
-
localesDirCache = void 0;
|
|
2249
|
-
currentLangValue = void 0;
|
|
2250
|
-
}
|
|
2251
|
-
var PLACEHOLDER = /\{(\w+)\}/g;
|
|
2252
|
-
function placeholdersOf(text) {
|
|
2253
|
-
const found = [];
|
|
2254
|
-
for (const match of text.matchAll(PLACEHOLDER)) {
|
|
2255
|
-
const name = match[1];
|
|
2256
|
-
if (name && !found.includes(name)) found.push(name);
|
|
2257
|
-
}
|
|
2258
|
-
return found;
|
|
2259
|
-
}
|
|
2260
|
-
function t(key, vars, lang) {
|
|
2261
|
-
const target = lang ?? currentLang();
|
|
2262
|
-
const text = loadCatalog(target).entries[key] ?? loadCatalog(DEFAULT_LANG).entries[key] ?? key;
|
|
2263
|
-
if (!vars) return text;
|
|
2264
|
-
return text.replace(PLACEHOLDER, (whole, name) => {
|
|
2265
|
-
const value = vars[name];
|
|
2266
|
-
return value === void 0 ? whole : String(value);
|
|
2267
|
-
});
|
|
2268
|
-
}
|
|
2269
|
-
function currentLang() {
|
|
2270
|
-
currentLangValue ??= resolveLang();
|
|
2271
|
-
return currentLangValue;
|
|
2272
|
-
}
|
|
2273
|
-
function setLang(lang) {
|
|
2274
|
-
currentLangValue = lang;
|
|
2275
|
-
}
|
|
2276
|
-
function systemLocaleSignals() {
|
|
2277
|
-
const env = process.env;
|
|
2278
|
-
const signals = [env.LC_ALL, env.LC_MESSAGES, env.LANG, env.LANGUAGE].filter(
|
|
2279
|
-
(v) => typeof v === "string" && v.length > 0
|
|
2280
|
-
);
|
|
2281
|
-
try {
|
|
2282
|
-
signals.push(new Intl.DateTimeFormat().resolvedOptions().locale);
|
|
2283
|
-
} catch {
|
|
2284
|
-
}
|
|
2285
|
-
return signals;
|
|
2286
|
-
}
|
|
2287
|
-
function langFromLocale(tag) {
|
|
2288
|
-
const primary = tag.split(/[.@]/)[0]?.replace("_", "-").toLowerCase() ?? "";
|
|
2289
|
-
if (!primary || primary === "c" || primary === "posix") return void 0;
|
|
2290
|
-
if (primary === "zh" || primary.startsWith("zh-")) return "zh";
|
|
2291
|
-
return "en";
|
|
2292
|
-
}
|
|
2293
|
-
function resolveLang(configured, signals) {
|
|
2294
|
-
const fromEnv = process.env.EPOCH_LANGUAGE;
|
|
2295
|
-
if (fromEnv && isLang(fromEnv)) return fromEnv;
|
|
2296
|
-
if (configured && isLang(configured)) return configured;
|
|
2297
|
-
for (const tag of signals ?? systemLocaleSignals()) {
|
|
2298
|
-
const lang = langFromLocale(tag);
|
|
2299
|
-
if (lang) return lang;
|
|
2300
|
-
}
|
|
2301
|
-
return DEFAULT_LANG;
|
|
2302
|
-
}
|
|
2303
|
-
function i18nDiagnostics() {
|
|
2304
|
-
return pending.map((item) => ({ code: item.code, detail: item.render() }));
|
|
2305
|
-
}
|
|
2306
2664
|
var SECRET_SERVICE = "epoch-agent";
|
|
2307
2665
|
function secretServiceFor(homeDir) {
|
|
2308
2666
|
if (homeDir === join(homedir(), ".epoch")) return SECRET_SERVICE;
|
|
@@ -2320,7 +2678,7 @@ var OP_TIMEOUT_MS = 1e4;
|
|
|
2320
2678
|
var BACKEND_ENV_VAR = "EPOCH_SECRET_BACKEND";
|
|
2321
2679
|
function assertValidSecretName(name) {
|
|
2322
2680
|
if (!/^[A-Za-z0-9._:-]{1,128}$/.test(name)) {
|
|
2323
|
-
throw new Error(
|
|
2681
|
+
throw new Error(t("secret.bad_name", { name }));
|
|
2324
2682
|
}
|
|
2325
2683
|
}
|
|
2326
2684
|
function secretIndexPath(homeDir) {
|
|
@@ -2329,7 +2687,6 @@ function secretIndexPath(homeDir) {
|
|
|
2329
2687
|
function dpapiStorePath(homeDir) {
|
|
2330
2688
|
return join(homeDir, "secrets.dpapi.json");
|
|
2331
2689
|
}
|
|
2332
|
-
|
|
2333
2690
|
// src/secret/catalog.ts
|
|
2334
2691
|
var FILE_VERSION = 1;
|
|
2335
2692
|
var SecretCatalog = class {
|
|
@@ -2338,7 +2695,6 @@ var SecretCatalog = class {
|
|
|
2338
2695
|
}
|
|
2339
2696
|
filePath;
|
|
2340
2697
|
cache = null;
|
|
2341
|
-
/** 已登记的名字,顺序稳定(排序过),方便 `epoch config secret list` 输出稳定 */
|
|
2342
2698
|
read() {
|
|
2343
2699
|
if (this.cache) return [...this.cache];
|
|
2344
2700
|
this.cache = parse(this.filePath);
|
|
@@ -2354,22 +2710,14 @@ var SecretCatalog = class {
|
|
|
2354
2710
|
if (!names.includes(name)) return;
|
|
2355
2711
|
this.write(names.filter((n) => n !== name));
|
|
2356
2712
|
}
|
|
2357
|
-
/** 整体替换。预取自愈用:把「钥匙串里真读得出来」的那份写回去 */
|
|
2358
2713
|
replace(names) {
|
|
2359
2714
|
const next = [...new Set(names)].sort();
|
|
2360
2715
|
if (sameList(this.read(), next)) return;
|
|
2361
2716
|
this.write(next);
|
|
2362
2717
|
}
|
|
2363
|
-
/** 仅供测试:丢掉内存缓存,强制重新读盘 */
|
|
2364
2718
|
invalidate() {
|
|
2365
2719
|
this.cache = null;
|
|
2366
2720
|
}
|
|
2367
|
-
/**
|
|
2368
|
-
* 原子写 + 建文件时就 0600。
|
|
2369
|
-
*
|
|
2370
|
-
* mode 在**创建时**生效,写完再 chmod 会留 TOCTOU 窗口
|
|
2371
|
-
* [对标 hermes mcp_oauth.py:388 的 `_write_secure_json`]。
|
|
2372
|
-
*/
|
|
2373
2721
|
write(names) {
|
|
2374
2722
|
const sorted = [...names].sort();
|
|
2375
2723
|
const file = { version: FILE_VERSION, names: sorted };
|
|
@@ -2384,7 +2732,11 @@ var SecretCatalog = class {
|
|
|
2384
2732
|
} catch (err) {
|
|
2385
2733
|
rmSync(tmp, { force: true });
|
|
2386
2734
|
throw new Error(
|
|
2387
|
-
|
|
2735
|
+
t("secret.op_failed", {
|
|
2736
|
+
op: t("secret.op_write"),
|
|
2737
|
+
name: t("secret.catalog_label"),
|
|
2738
|
+
detail: `${this.filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
2739
|
+
})
|
|
2388
2740
|
);
|
|
2389
2741
|
}
|
|
2390
2742
|
this.cache = sorted;
|
|
@@ -2411,10 +2763,8 @@ function run(file, args2, opts) {
|
|
|
2411
2763
|
args2,
|
|
2412
2764
|
{
|
|
2413
2765
|
timeout: opts.timeoutMs,
|
|
2414
|
-
// 凭据值可能几 KB(OAuth token 串),默认 1MB 够用但显式写出来更清楚
|
|
2415
2766
|
maxBuffer: 1024 * 1024,
|
|
2416
2767
|
encoding: "utf-8",
|
|
2417
|
-
// Windows 上别弹黑框:PowerShell 冷启动会闪一个控制台窗口
|
|
2418
2768
|
...WINDOWS_HIDE_FLAGS
|
|
2419
2769
|
},
|
|
2420
2770
|
(err, stdout, stderr) => {
|
|
@@ -2444,12 +2794,15 @@ function classify(err) {
|
|
|
2444
2794
|
};
|
|
2445
2795
|
}
|
|
2446
2796
|
function describeFailure(file, r) {
|
|
2447
|
-
if (r.missing) return
|
|
2448
|
-
if (r.timedOut) return
|
|
2797
|
+
if (r.missing) return t("secret.exec_missing", { file });
|
|
2798
|
+
if (r.timedOut) return t("secret.exec_timeout", { file });
|
|
2449
2799
|
const detail = r.stderr.trim().split("\n")[0]?.slice(0, 200);
|
|
2450
|
-
return
|
|
2800
|
+
return t("secret.exec_exit", {
|
|
2801
|
+
file,
|
|
2802
|
+
code: r.code ?? -1,
|
|
2803
|
+
detail: detail ? t("secret.exec_exit_detail", { detail }) : ""
|
|
2804
|
+
});
|
|
2451
2805
|
}
|
|
2452
|
-
|
|
2453
2806
|
// src/secret/dpapi.ts
|
|
2454
2807
|
var FILE_VERSION2 = 1;
|
|
2455
2808
|
var SHELLS2 = ["powershell.exe", "pwsh"];
|
|
@@ -2476,20 +2829,38 @@ var DpapiStore = class {
|
|
|
2476
2829
|
shell;
|
|
2477
2830
|
backend = "dpapi";
|
|
2478
2831
|
encrypted = true;
|
|
2479
|
-
detail
|
|
2832
|
+
get detail() {
|
|
2833
|
+
return t("secret.backend_dpapi");
|
|
2834
|
+
}
|
|
2480
2835
|
cache = null;
|
|
2481
2836
|
async get(name) {
|
|
2482
2837
|
assertValidSecretName(name);
|
|
2483
2838
|
const blob = this.load().secrets[name];
|
|
2484
2839
|
if (!blob) return void 0;
|
|
2485
2840
|
const r = await this.ps(UNPROTECT_SCRIPT, blob);
|
|
2486
|
-
if (r.code !== 0)
|
|
2841
|
+
if (r.code !== 0) {
|
|
2842
|
+
throw new Error(
|
|
2843
|
+
t("secret.op_failed", {
|
|
2844
|
+
op: t("secret.op_read"),
|
|
2845
|
+
name,
|
|
2846
|
+
detail: describeFailure(this.shell, r)
|
|
2847
|
+
})
|
|
2848
|
+
);
|
|
2849
|
+
}
|
|
2487
2850
|
return Buffer.from(r.stdout.trim(), "base64").toString("utf-8");
|
|
2488
2851
|
}
|
|
2489
2852
|
async set(name, value) {
|
|
2490
2853
|
assertValidSecretName(name);
|
|
2491
2854
|
const r = await this.ps(PROTECT_SCRIPT, Buffer.from(value, "utf-8").toString("base64"));
|
|
2492
|
-
if (r.code !== 0)
|
|
2855
|
+
if (r.code !== 0) {
|
|
2856
|
+
throw new Error(
|
|
2857
|
+
t("secret.op_failed", {
|
|
2858
|
+
op: t("secret.op_write"),
|
|
2859
|
+
name,
|
|
2860
|
+
detail: describeFailure(this.shell, r)
|
|
2861
|
+
})
|
|
2862
|
+
);
|
|
2863
|
+
}
|
|
2493
2864
|
const file = this.load();
|
|
2494
2865
|
file.secrets[name] = stripBlank(r.stdout);
|
|
2495
2866
|
this.save(file);
|
|
@@ -2503,7 +2874,6 @@ var DpapiStore = class {
|
|
|
2503
2874
|
}
|
|
2504
2875
|
return Promise.resolve();
|
|
2505
2876
|
}
|
|
2506
|
-
/** 密文就在自己的文件里,名字直接是 key —— 这个后端不需要 catalog.ts 的索引 */
|
|
2507
2877
|
list() {
|
|
2508
2878
|
return Promise.resolve(Object.keys(this.load().secrets).sort());
|
|
2509
2879
|
}
|
|
@@ -2515,7 +2885,6 @@ var DpapiStore = class {
|
|
|
2515
2885
|
this.cache = readDpapiFile(this.filePath);
|
|
2516
2886
|
return this.cache;
|
|
2517
2887
|
}
|
|
2518
|
-
/** 原子写 + 建文件时就 0600(Windows 上 mode 是空操作,但这份代码也跑在测试里) */
|
|
2519
2888
|
save(file) {
|
|
2520
2889
|
mkdirSync(dirname(this.filePath), { recursive: true });
|
|
2521
2890
|
const tmp = `${this.filePath}.${process.pid}.tmp`;
|
|
@@ -2525,7 +2894,11 @@ var DpapiStore = class {
|
|
|
2525
2894
|
} catch (err) {
|
|
2526
2895
|
rmSync(tmp, { force: true });
|
|
2527
2896
|
throw new Error(
|
|
2528
|
-
|
|
2897
|
+
t("secret.op_failed", {
|
|
2898
|
+
op: t("secret.op_write"),
|
|
2899
|
+
name: t("secret.dpapi_file_label"),
|
|
2900
|
+
detail: `${this.filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
2901
|
+
})
|
|
2529
2902
|
);
|
|
2530
2903
|
}
|
|
2531
2904
|
this.cache = file;
|
|
@@ -2548,10 +2921,10 @@ function readDpapiFile(filePath) {
|
|
|
2548
2921
|
}
|
|
2549
2922
|
}
|
|
2550
2923
|
async function probeDpapi() {
|
|
2551
|
-
if (platform() !== "win32") return { reason: "
|
|
2924
|
+
if (platform() !== "win32") return { reason: t("secret.not_windows") };
|
|
2552
2925
|
const expect = randomBytes(8).toString("hex");
|
|
2553
2926
|
const payload = Buffer.from(expect, "utf-8").toString("base64");
|
|
2554
|
-
let last = "
|
|
2927
|
+
let last = t("secret.no_powershell");
|
|
2555
2928
|
for (const shell of SHELLS2) {
|
|
2556
2929
|
const sealed = await run(shell, [...PS_FLAGS, PROTECT_SCRIPT], {
|
|
2557
2930
|
timeoutMs: POWERSHELL_TIMEOUT_MS,
|
|
@@ -2559,7 +2932,7 @@ async function probeDpapi() {
|
|
|
2559
2932
|
});
|
|
2560
2933
|
if (sealed.missing) continue;
|
|
2561
2934
|
if (sealed.code !== 0) {
|
|
2562
|
-
last =
|
|
2935
|
+
last = t("secret.probe_encrypt_failed", { detail: describeFailure(shell, sealed) });
|
|
2563
2936
|
continue;
|
|
2564
2937
|
}
|
|
2565
2938
|
const back = await run(shell, [...PS_FLAGS, UNPROTECT_SCRIPT], {
|
|
@@ -2567,11 +2940,11 @@ async function probeDpapi() {
|
|
|
2567
2940
|
stdin: stripBlank(sealed.stdout)
|
|
2568
2941
|
});
|
|
2569
2942
|
if (back.code !== 0) {
|
|
2570
|
-
last =
|
|
2943
|
+
last = t("secret.probe_decrypt_failed", { detail: describeFailure(shell, back) });
|
|
2571
2944
|
continue;
|
|
2572
2945
|
}
|
|
2573
2946
|
if (Buffer.from(back.stdout.trim(), "base64").toString("utf-8") !== expect) {
|
|
2574
|
-
last = "
|
|
2947
|
+
last = t("secret.probe_crypto_mismatch");
|
|
2575
2948
|
continue;
|
|
2576
2949
|
}
|
|
2577
2950
|
return { shell };
|
|
@@ -2584,11 +2957,6 @@ function stripBlank(s) {
|
|
|
2584
2957
|
var SECURITY = "/usr/bin/security";
|
|
2585
2958
|
var NOT_FOUND = 44;
|
|
2586
2959
|
var KeychainStore = class {
|
|
2587
|
-
/**
|
|
2588
|
-
* @param service 钥匙串里的服务名。**按家目录分命名空间**,见
|
|
2589
|
-
* [types.ts](./types.js) 的 `secretServiceFor()` ——
|
|
2590
|
-
* 钥匙串是全局的,写死一个名字会让多 profile 共用同一批凭据
|
|
2591
|
-
*/
|
|
2592
2960
|
constructor(catalog, service) {
|
|
2593
2961
|
this.catalog = catalog;
|
|
2594
2962
|
this.service = service;
|
|
@@ -2597,14 +2965,24 @@ var KeychainStore = class {
|
|
|
2597
2965
|
service;
|
|
2598
2966
|
backend = "keychain";
|
|
2599
2967
|
encrypted = true;
|
|
2600
|
-
detail
|
|
2968
|
+
get detail() {
|
|
2969
|
+
return t("secret.backend_keychain");
|
|
2970
|
+
}
|
|
2601
2971
|
async get(name) {
|
|
2602
2972
|
assertValidSecretName(name);
|
|
2603
2973
|
const r = await run(SECURITY, ["find-generic-password", "-s", this.service, "-a", name, "-w"], {
|
|
2604
2974
|
timeoutMs: OP_TIMEOUT_MS
|
|
2605
2975
|
});
|
|
2606
2976
|
if (r.code === NOT_FOUND) return void 0;
|
|
2607
|
-
if (r.code !== 0)
|
|
2977
|
+
if (r.code !== 0) {
|
|
2978
|
+
throw new Error(
|
|
2979
|
+
t("secret.op_failed", {
|
|
2980
|
+
op: t("secret.op_read"),
|
|
2981
|
+
name,
|
|
2982
|
+
detail: describeFailure(SECURITY, r)
|
|
2983
|
+
})
|
|
2984
|
+
);
|
|
2985
|
+
}
|
|
2608
2986
|
return decode(r.stdout);
|
|
2609
2987
|
}
|
|
2610
2988
|
async set(name, value) {
|
|
@@ -2625,7 +3003,15 @@ var KeychainStore = class {
|
|
|
2625
3003
|
],
|
|
2626
3004
|
{ timeoutMs: OP_TIMEOUT_MS }
|
|
2627
3005
|
);
|
|
2628
|
-
if (r.code !== 0)
|
|
3006
|
+
if (r.code !== 0) {
|
|
3007
|
+
throw new Error(
|
|
3008
|
+
t("secret.op_failed", {
|
|
3009
|
+
op: t("secret.op_write"),
|
|
3010
|
+
name,
|
|
3011
|
+
detail: describeFailure(SECURITY, r)
|
|
3012
|
+
})
|
|
3013
|
+
);
|
|
3014
|
+
}
|
|
2629
3015
|
this.catalog.add(name);
|
|
2630
3016
|
}
|
|
2631
3017
|
async delete(name) {
|
|
@@ -2634,24 +3020,28 @@ var KeychainStore = class {
|
|
|
2634
3020
|
timeoutMs: OP_TIMEOUT_MS
|
|
2635
3021
|
});
|
|
2636
3022
|
if (r.code !== 0 && r.code !== NOT_FOUND) {
|
|
2637
|
-
throw new Error(
|
|
3023
|
+
throw new Error(
|
|
3024
|
+
t("secret.op_failed", {
|
|
3025
|
+
op: t("secret.op_delete"),
|
|
3026
|
+
name,
|
|
3027
|
+
detail: describeFailure(SECURITY, r)
|
|
3028
|
+
})
|
|
3029
|
+
);
|
|
2638
3030
|
}
|
|
2639
3031
|
this.catalog.remove(name);
|
|
2640
3032
|
}
|
|
2641
|
-
/** 见 catalog.ts:钥匙串本身没有便宜的列举方式,索引是提示、`get` 才是权威 */
|
|
2642
3033
|
list() {
|
|
2643
3034
|
return Promise.resolve(this.catalog.read());
|
|
2644
3035
|
}
|
|
2645
|
-
/** 预取时发现索引里有、钥匙串里没有的名字,就地剔掉(自愈) */
|
|
2646
3036
|
healCatalog(available) {
|
|
2647
3037
|
this.catalog.replace(available);
|
|
2648
3038
|
}
|
|
2649
3039
|
};
|
|
2650
3040
|
async function probeKeychain(service) {
|
|
2651
|
-
if (platform() !== "darwin") return "
|
|
3041
|
+
if (platform() !== "darwin") return t("secret.not_macos");
|
|
2652
3042
|
const dk = await run(SECURITY, ["default-keychain"], { timeoutMs: PROBE_TIMEOUT_MS2 });
|
|
2653
3043
|
if (dk.code !== 0 || dk.stdout.trim() === "") {
|
|
2654
|
-
return
|
|
3044
|
+
return t("secret.no_default_keychain", { detail: describeFailure(SECURITY, dk) });
|
|
2655
3045
|
}
|
|
2656
3046
|
const account = `__probe__${randomBytes(8).toString("hex")}`;
|
|
2657
3047
|
const expect = randomBytes(8).toString("hex");
|
|
@@ -2659,13 +3049,17 @@ async function probeKeychain(service) {
|
|
|
2659
3049
|
const added = await run(SECURITY, ["add-generic-password", "-U", ...args2, "-w", encode(expect)], {
|
|
2660
3050
|
timeoutMs: PROBE_TIMEOUT_MS2
|
|
2661
3051
|
});
|
|
2662
|
-
if (added.code !== 0)
|
|
3052
|
+
if (added.code !== 0) {
|
|
3053
|
+
return t("secret.probe_write_failed", { detail: describeFailure(SECURITY, added) });
|
|
3054
|
+
}
|
|
2663
3055
|
const read = await run(SECURITY, ["find-generic-password", ...args2, "-w"], {
|
|
2664
3056
|
timeoutMs: PROBE_TIMEOUT_MS2
|
|
2665
3057
|
});
|
|
2666
3058
|
await run(SECURITY, ["delete-generic-password", ...args2], { timeoutMs: PROBE_TIMEOUT_MS2 });
|
|
2667
|
-
if (read.code !== 0)
|
|
2668
|
-
|
|
3059
|
+
if (read.code !== 0) {
|
|
3060
|
+
return t("secret.probe_read_failed", { detail: describeFailure(SECURITY, read) });
|
|
3061
|
+
}
|
|
3062
|
+
if (decode(read.stdout) !== expect) return t("secret.probe_mismatch");
|
|
2669
3063
|
return null;
|
|
2670
3064
|
}
|
|
2671
3065
|
function encode(value) {
|
|
@@ -2676,10 +3070,6 @@ function decode(stdout) {
|
|
|
2676
3070
|
}
|
|
2677
3071
|
var SECRET_TOOL = "secret-tool";
|
|
2678
3072
|
var LibsecretStore = class {
|
|
2679
|
-
/**
|
|
2680
|
-
* @param service Secret Service 里的 `service` 属性值。**按家目录分命名空间**,
|
|
2681
|
-
* 见 [types.ts](./types.js) 的 `secretServiceFor()`
|
|
2682
|
-
*/
|
|
2683
3073
|
constructor(catalog, service) {
|
|
2684
3074
|
this.catalog = catalog;
|
|
2685
3075
|
this.service = service;
|
|
@@ -2688,8 +3078,9 @@ var LibsecretStore = class {
|
|
|
2688
3078
|
service;
|
|
2689
3079
|
backend = "libsecret";
|
|
2690
3080
|
encrypted = true;
|
|
2691
|
-
detail
|
|
2692
|
-
|
|
3081
|
+
get detail() {
|
|
3082
|
+
return t("secret.backend_libsecret");
|
|
3083
|
+
}
|
|
2693
3084
|
attrs(name) {
|
|
2694
3085
|
return ["service", this.service, "account", name];
|
|
2695
3086
|
}
|
|
@@ -2698,7 +3089,13 @@ var LibsecretStore = class {
|
|
|
2698
3089
|
const r = await run(SECRET_TOOL, ["lookup", ...this.attrs(name)], { timeoutMs: OP_TIMEOUT_MS });
|
|
2699
3090
|
if (r.code === 0) return decode2(r.stdout);
|
|
2700
3091
|
if (r.stdout.trim() === "" && !r.timedOut && !r.missing) return void 0;
|
|
2701
|
-
throw new Error(
|
|
3092
|
+
throw new Error(
|
|
3093
|
+
t("secret.op_failed", {
|
|
3094
|
+
op: t("secret.op_read"),
|
|
3095
|
+
name,
|
|
3096
|
+
detail: describeFailure(SECRET_TOOL, r)
|
|
3097
|
+
})
|
|
3098
|
+
);
|
|
2702
3099
|
}
|
|
2703
3100
|
async set(name, value) {
|
|
2704
3101
|
assertValidSecretName(name);
|
|
@@ -2707,31 +3104,40 @@ var LibsecretStore = class {
|
|
|
2707
3104
|
["store", `--label=${this.service}: ${name}`, ...this.attrs(name)],
|
|
2708
3105
|
{ timeoutMs: OP_TIMEOUT_MS, stdin: encode2(value) }
|
|
2709
3106
|
);
|
|
2710
|
-
if (r.code !== 0)
|
|
3107
|
+
if (r.code !== 0) {
|
|
3108
|
+
throw new Error(
|
|
3109
|
+
t("secret.op_failed", {
|
|
3110
|
+
op: t("secret.op_write"),
|
|
3111
|
+
name,
|
|
3112
|
+
detail: describeFailure(SECRET_TOOL, r)
|
|
3113
|
+
})
|
|
3114
|
+
);
|
|
3115
|
+
}
|
|
2711
3116
|
this.catalog.add(name);
|
|
2712
3117
|
}
|
|
2713
3118
|
async delete(name) {
|
|
2714
3119
|
assertValidSecretName(name);
|
|
2715
3120
|
const r = await run(SECRET_TOOL, ["clear", ...this.attrs(name)], { timeoutMs: OP_TIMEOUT_MS });
|
|
2716
3121
|
if (r.code !== 0 && (r.timedOut || r.missing)) {
|
|
2717
|
-
throw new Error(
|
|
3122
|
+
throw new Error(
|
|
3123
|
+
t("secret.op_failed", {
|
|
3124
|
+
op: t("secret.op_delete"),
|
|
3125
|
+
name,
|
|
3126
|
+
detail: describeFailure(SECRET_TOOL, r)
|
|
3127
|
+
})
|
|
3128
|
+
);
|
|
2718
3129
|
}
|
|
2719
3130
|
this.catalog.remove(name);
|
|
2720
3131
|
}
|
|
2721
|
-
/**
|
|
2722
|
-
* `secret-tool search --all` 能列,但输出格式随 libsecret 版本变,
|
|
2723
|
-
* 而且要多走一次 D-Bus 往返。索引更便宜也更稳定 —— 见 catalog.ts。
|
|
2724
|
-
*/
|
|
2725
3132
|
list() {
|
|
2726
3133
|
return Promise.resolve(this.catalog.read());
|
|
2727
3134
|
}
|
|
2728
|
-
/** 预取时发现索引里有、keyring 里没有的名字,就地剔掉(自愈) */
|
|
2729
3135
|
healCatalog(available) {
|
|
2730
3136
|
this.catalog.replace(available);
|
|
2731
3137
|
}
|
|
2732
3138
|
};
|
|
2733
3139
|
async function probeLibsecret(service) {
|
|
2734
|
-
if (platform() === "darwin" || platform() === "win32") return "
|
|
3140
|
+
if (platform() === "darwin" || platform() === "win32") return t("secret.not_linux");
|
|
2735
3141
|
const account = `__probe__${randomBytes(8).toString("hex")}`;
|
|
2736
3142
|
const expect = randomBytes(8).toString("hex");
|
|
2737
3143
|
const args2 = ["service", service, "account", account];
|
|
@@ -2740,16 +3146,20 @@ async function probeLibsecret(service) {
|
|
|
2740
3146
|
stdin: encode2(expect)
|
|
2741
3147
|
});
|
|
2742
3148
|
if (stored.missing) {
|
|
2743
|
-
return "
|
|
3149
|
+
return t("secret.no_secret_tool");
|
|
2744
3150
|
}
|
|
2745
3151
|
if (stored.timedOut) {
|
|
2746
|
-
return "secret
|
|
3152
|
+
return t("secret.secret_tool_hang");
|
|
3153
|
+
}
|
|
3154
|
+
if (stored.code !== 0) {
|
|
3155
|
+
return t("secret.probe_write_failed", { detail: describeFailure(SECRET_TOOL, stored) });
|
|
2747
3156
|
}
|
|
2748
|
-
if (stored.code !== 0) return `\u5199\u5165\u63A2\u6D4B\u5931\u8D25\uFF08${describeFailure(SECRET_TOOL, stored)}\uFF09`;
|
|
2749
3157
|
const read = await run(SECRET_TOOL, ["lookup", ...args2], { timeoutMs: PROBE_TIMEOUT_MS2 });
|
|
2750
3158
|
await run(SECRET_TOOL, ["clear", ...args2], { timeoutMs: PROBE_TIMEOUT_MS2 });
|
|
2751
|
-
if (read.code !== 0)
|
|
2752
|
-
|
|
3159
|
+
if (read.code !== 0) {
|
|
3160
|
+
return t("secret.probe_read_failed", { detail: describeFailure(SECRET_TOOL, read) });
|
|
3161
|
+
}
|
|
3162
|
+
if (decode2(read.stdout) !== expect) return t("secret.probe_mismatch");
|
|
2753
3163
|
return null;
|
|
2754
3164
|
}
|
|
2755
3165
|
function encode2(value) {
|
|
@@ -2759,18 +3169,17 @@ function decode2(stdout) {
|
|
|
2759
3169
|
return Buffer.from(stdout.trim(), "base64").toString("utf-8");
|
|
2760
3170
|
}
|
|
2761
3171
|
var PlaintextStore = class {
|
|
2762
|
-
/**
|
|
2763
|
-
* @param reason 为什么降级。**必须带修复方法** —— 只说「明文」而不说怎么修,
|
|
2764
|
-
* 用户除了忍着没有别的选择
|
|
2765
|
-
*/
|
|
2766
3172
|
constructor(envFilePath, reason) {
|
|
2767
3173
|
this.envFilePath = envFilePath;
|
|
2768
|
-
this.
|
|
3174
|
+
this.reason = reason;
|
|
2769
3175
|
}
|
|
2770
3176
|
envFilePath;
|
|
3177
|
+
reason;
|
|
2771
3178
|
backend = "plaintext";
|
|
2772
3179
|
encrypted = false;
|
|
2773
|
-
detail
|
|
3180
|
+
get detail() {
|
|
3181
|
+
return t("secret.plaintext_note", { reason: this.reason, path: this.envFilePath });
|
|
3182
|
+
}
|
|
2774
3183
|
get(name) {
|
|
2775
3184
|
return Promise.resolve(parseEnvText(readText(this.envFilePath))[name]);
|
|
2776
3185
|
}
|
|
@@ -2850,7 +3259,7 @@ var TAG_BYTES = 16;
|
|
|
2850
3259
|
var dataKey = null;
|
|
2851
3260
|
function setDataKey(key) {
|
|
2852
3261
|
if (key !== null && key.length !== KEY_BYTES) {
|
|
2853
|
-
throw new Error(
|
|
3262
|
+
throw new Error(t("secret.bad_key_length", { expected: KEY_BYTES, actual: key.length }));
|
|
2854
3263
|
}
|
|
2855
3264
|
dataKey = key;
|
|
2856
3265
|
}
|
|
@@ -2879,9 +3288,9 @@ function isEnvelope(text) {
|
|
|
2879
3288
|
return text.startsWith(ENVELOPE_PREFIX);
|
|
2880
3289
|
}
|
|
2881
3290
|
function openEnvelope(key, blob) {
|
|
2882
|
-
if (!isEnvelope(blob)) throw new Error("
|
|
3291
|
+
if (!isEnvelope(blob)) throw new Error(t("secret.not_envelope"));
|
|
2883
3292
|
const raw = Buffer.from(blob.slice(ENVELOPE_PREFIX.length), "base64");
|
|
2884
|
-
if (raw.length < IV_BYTES + TAG_BYTES) throw new Error("
|
|
3293
|
+
if (raw.length < IV_BYTES + TAG_BYTES) throw new Error(t("secret.envelope_too_short"));
|
|
2885
3294
|
const iv = raw.subarray(0, IV_BYTES);
|
|
2886
3295
|
const tag = raw.subarray(IV_BYTES, IV_BYTES + TAG_BYTES);
|
|
2887
3296
|
const body = raw.subarray(IV_BYTES + TAG_BYTES);
|
|
@@ -2899,7 +3308,10 @@ async function migrateEnvSecrets(opts) {
|
|
|
2899
3308
|
try {
|
|
2900
3309
|
existing = new Set(await store.list());
|
|
2901
3310
|
} catch (err) {
|
|
2902
|
-
return {
|
|
3311
|
+
return {
|
|
3312
|
+
migrated: [],
|
|
3313
|
+
warning: t("secret.migrate_read_failed", { detail: message(err) })
|
|
3314
|
+
};
|
|
2903
3315
|
}
|
|
2904
3316
|
const todo = candidates.filter((n) => !existing.has(n));
|
|
2905
3317
|
if (todo.length === 0) return { migrated: [] };
|
|
@@ -2913,7 +3325,11 @@ async function migrateEnvSecrets(opts) {
|
|
|
2913
3325
|
} catch (err) {
|
|
2914
3326
|
return {
|
|
2915
3327
|
migrated,
|
|
2916
|
-
warning:
|
|
3328
|
+
warning: t("secret.migrate_failed", {
|
|
3329
|
+
name,
|
|
3330
|
+
path: envFilePath,
|
|
3331
|
+
detail: message(err)
|
|
3332
|
+
})
|
|
2917
3333
|
};
|
|
2918
3334
|
}
|
|
2919
3335
|
}
|
|
@@ -2921,7 +3337,10 @@ async function migrateEnvSecrets(opts) {
|
|
|
2921
3337
|
try {
|
|
2922
3338
|
renameSync(envFilePath, backupPath);
|
|
2923
3339
|
} catch (err) {
|
|
2924
|
-
return {
|
|
3340
|
+
return {
|
|
3341
|
+
migrated,
|
|
3342
|
+
warning: t("secret.migrate_partial", { path: envFilePath, detail: message(err) })
|
|
3343
|
+
};
|
|
2925
3344
|
}
|
|
2926
3345
|
return { migrated, backupPath };
|
|
2927
3346
|
}
|
|
@@ -2943,14 +3362,16 @@ function readOrEmpty(path) {
|
|
|
2943
3362
|
function message(err) {
|
|
2944
3363
|
return err instanceof Error ? err.message : String(err);
|
|
2945
3364
|
}
|
|
2946
|
-
|
|
2947
3365
|
// src/secret/index.ts
|
|
2948
3366
|
async function createSecretStore(opts = {}) {
|
|
2949
3367
|
if (opts.inject) return opts.inject;
|
|
2950
3368
|
const homeDir = opts.homeDir ?? resolveHomeDir();
|
|
2951
3369
|
const forced = readForcedBackend();
|
|
2952
3370
|
if (forced === "plaintext") {
|
|
2953
|
-
return new PlaintextStore(
|
|
3371
|
+
return new PlaintextStore(
|
|
3372
|
+
envPath(homeDir),
|
|
3373
|
+
t("secret.backend_plaintext_forced", { envVar: BACKEND_ENV_VAR })
|
|
3374
|
+
);
|
|
2954
3375
|
}
|
|
2955
3376
|
const reason = await tryBackends(homeDir, forced);
|
|
2956
3377
|
if (typeof reason !== "string") return reason;
|
|
@@ -2968,14 +3389,18 @@ async function tryBackends(homeDir, forced) {
|
|
|
2968
3389
|
if (want("dpapi") && platform() === "win32") {
|
|
2969
3390
|
const { shell, reason } = await probeDpapi();
|
|
2970
3391
|
if (shell) return new DpapiStore(dpapiStorePath(homeDir), shell);
|
|
2971
|
-
return reason ?? "
|
|
3392
|
+
return reason ?? t("secret.dpapi_unavailable");
|
|
2972
3393
|
}
|
|
2973
3394
|
if (want("libsecret") && platform() !== "darwin" && platform() !== "win32") {
|
|
2974
3395
|
const why = await probeLibsecret(service);
|
|
2975
3396
|
if (why === null) return new LibsecretStore(catalog, service);
|
|
2976
3397
|
return why;
|
|
2977
3398
|
}
|
|
2978
|
-
return forced === null ?
|
|
3399
|
+
return forced === null ? t("secret.no_backend", { platform: platform() }) : t("secret.forced_unavailable", {
|
|
3400
|
+
envVar: BACKEND_ENV_VAR,
|
|
3401
|
+
forced,
|
|
3402
|
+
platform: platform()
|
|
3403
|
+
});
|
|
2979
3404
|
}
|
|
2980
3405
|
function readForcedBackend() {
|
|
2981
3406
|
const raw = process.env[BACKEND_ENV_VAR]?.trim().toLowerCase();
|
|
@@ -2988,7 +3413,7 @@ async function prefetchProviderSecrets(store, names) {
|
|
|
2988
3413
|
try {
|
|
2989
3414
|
stored = await store.list();
|
|
2990
3415
|
} catch (err) {
|
|
2991
|
-
return { values: {}, warning:
|
|
3416
|
+
return { values: {}, warning: t("secret.list_failed", { detail: message2(err) }) };
|
|
2992
3417
|
}
|
|
2993
3418
|
const wanted = stored.filter((n) => names.includes(n));
|
|
2994
3419
|
const values2 = {};
|
|
@@ -2999,7 +3424,7 @@ async function prefetchProviderSecrets(store, names) {
|
|
|
2999
3424
|
if (value) values2[name] = value;
|
|
3000
3425
|
else missing.push(name);
|
|
3001
3426
|
} catch (err) {
|
|
3002
|
-
return { values: values2, warning:
|
|
3427
|
+
return { values: values2, warning: t("secret.read_failed", { name, detail: message2(err) }) };
|
|
3003
3428
|
}
|
|
3004
3429
|
}
|
|
3005
3430
|
if (missing.length > 0) {
|
|
@@ -3038,6 +3463,37 @@ function resetSecretState() {
|
|
|
3038
3463
|
current = null;
|
|
3039
3464
|
values = {};
|
|
3040
3465
|
}
|
|
3466
|
+
async function writeProviderSecret(name, value, fallbackEnvPath) {
|
|
3467
|
+
const store = getSecretStore();
|
|
3468
|
+
const result = await writeThrough(store, name, value, fallbackEnvPath);
|
|
3469
|
+
setSecretValues({ ...getSecretValues(), [name]: value });
|
|
3470
|
+
return result;
|
|
3471
|
+
}
|
|
3472
|
+
async function writeThrough(store, name, value, fallbackEnvPath) {
|
|
3473
|
+
if (!store) {
|
|
3474
|
+
writeEnvFile(fallbackEnvPath, setEnvVarText(readEnvOrEmpty(fallbackEnvPath), name, value));
|
|
3475
|
+
return {
|
|
3476
|
+
backend: "plaintext",
|
|
3477
|
+
encrypted: false,
|
|
3478
|
+
detail: t("cli.config.secret_plaintext_write", { path: fallbackEnvPath }),
|
|
3479
|
+
envPath: fallbackEnvPath
|
|
3480
|
+
};
|
|
3481
|
+
}
|
|
3482
|
+
await store.set(name, value);
|
|
3483
|
+
return {
|
|
3484
|
+
backend: store.backend,
|
|
3485
|
+
encrypted: store.encrypted,
|
|
3486
|
+
detail: store.detail,
|
|
3487
|
+
envPath: store.backend === "plaintext" ? fallbackEnvPath : null
|
|
3488
|
+
};
|
|
3489
|
+
}
|
|
3490
|
+
function readEnvOrEmpty(path) {
|
|
3491
|
+
try {
|
|
3492
|
+
return readFileSync(path, "utf-8");
|
|
3493
|
+
} catch {
|
|
3494
|
+
return "";
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3041
3497
|
function message2(err) {
|
|
3042
3498
|
return err instanceof Error ? err.message : String(err);
|
|
3043
3499
|
}
|
|
@@ -3096,4 +3552,4 @@ function message2(err) {
|
|
|
3096
3552
|
* Modifications Copyright 2024-2026 bowen
|
|
3097
3553
|
*/
|
|
3098
3554
|
|
|
3099
|
-
export { CODE_EXEC_ROOTS, DEFAULT_LANG, DEFAULT_YAML, DENIAL_SIGNATURES, ENVELOPE_PREFIX, EXEC_PATH_AS_NODE_ENV, IS_WINDOWS, LANGS, PROJECT_DIR_NAME, PlaintextStore,
|
|
3555
|
+
export { CLAUDE_DIR_NAME, CODE_EXEC_ROOTS, DEFAULT_LANG, DEFAULT_YAML, DENIAL_SIGNATURES, ENVELOPE_PREFIX, EXEC_PATH_AS_NODE_ENV, IS_WINDOWS, LANGS, PROJECT_DIR_NAME, PlaintextStore, RUNNER_FAILURE_RULES, SECRET_MODE, SECRET_SERVICE, SHELL_KINDS, SIGKILL_TIMEOUT_MS, SecretCatalog, TOOLCHAIN_CACHE_DIRS, WINDOWS_HIDE_FLAGS, agentsDir, allJobs, appendJob, approvalsPath, artifactsDir, assertValidSecretName, automationDir, automationLogsDir, automationWorkDir, budgetStatePath, buildBwrapArgs, buildProfile, checkDangerousCommand, checkObfuscation, checkpointsDir, classifyFailure, claudeUserSettingsPath, clearAllJobs, closeAllDatabases, collectProcessTree, collectProcessTreeStamped, commandsDir, complianceDir, configPath, confine, countCodePoints, createLogger, createProcessTable, createSecretStore, createStreamDecoder, currentLang, dbPath, detectBackend, detectConsoleEncoding, encodingForCodePage, envPath, formatIssues, generateDataKey, getDataKey, getDefaultShell, getJob, getPythonCommand, getSecretStore, getSecretValues, hasSideEffectChannel, headCodePoints, hooksPath, i18nDiagnostics, isEnvelope, isInWorkspace, isLang, isReadOnlyCommand, isSensitiveKey, isolate, issueDetails, jobDetail, keepHeadAndTail, keybindingsPath, killAllTrackedProcesses, killPids, killProcessTree, killStampedPids, killTrackedProcess, listJobs, listStoredSecretNames, loadCatalog, localesDir, managedSettingsPath, marketplacesPath, maskApiKey, maskSensitive, mcpAuthPath, mcpConfigPath, mcpSchemaCachePath, memoriesDir, migrate, migrateEnvSecrets, normalizeForMatch, openArtifact, openDatabase, openEnvelope, parseDataKey, parseEnvText, parseLenient, parseShellCommand, parseStrict, placeholdersOf, pluginsDir, pluginsStatePath, policiesDir, powershellCommandArg, prefetchProviderSecrets, probeBubblewrap, probeSeatbelt, probeShell, processFallbackTable, processTableFor, projectClaudeLocalSettingsPath, projectClaudeSettingsPath, projectCommandsDir, projectComplianceDir, projectHooksPath, projectLocalSettingsPath, projectPoliciesDir, projectSchemasDir, projectSettingsPath, projectSkillsDir, readConfigYaml, readJob, readKey, refCount, registerJob, rekeyJobs, releaseDatabase, removeEnvVarText, resetI18n, resetRipgrepCache, resetSecretState, resolveHomeDir, resolveLang, resolveProfile, resolveRipgrep, resolveShellKind, ripgrepInstallHint, safeInit, sandboxModeForLevel, schemaVersion, sealEnvelope, setDataKey, setEnvVarText, setLang, setLogLevel, setScalar, setSecretStore, setSecretValues, setSectionField, setShell, shellPtyArgs, shellSpawnArgs, skillsDir, splitWords, startLongLivedProcess, stopJob, stripShellWrapper, systemLocaleSignals, t, tailCodePoints, touchJob, trackForeignProcess, trackedProcessCount, trustPath, trustedImportsPath, uiDateLocale, unknownKeyIssues, unsetScalar, unsetSectionField, waitJob, withTimeout, workspacesPath, worktreesDir, writeArtifact, writeConfigYaml, writeEnvFile, writeProviderSecret };
|