cclawd 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +3 -3
- package/dist/plugin-sdk/active-listener-CN-tMEvN.js +35 -0
- package/dist/plugin-sdk/api-key-rotation-CimGYMBc.js +176 -0
- package/dist/plugin-sdk/audio-preflight-C-xXBoE2.js +51 -0
- package/dist/plugin-sdk/audio-transcription-runner-CTIHpebA.js +2173 -0
- package/dist/plugin-sdk/audit-membership-runtime-BFatB2LJ.js +58 -0
- package/dist/plugin-sdk/channel-activity-DO0FEzyj.js +95 -0
- package/dist/plugin-sdk/channel-web-Da-__nUF.js +2238 -0
- package/dist/plugin-sdk/commands-registry-6no2NNrY.js +1118 -0
- package/dist/plugin-sdk/compact.runtime-CCoclu5e.js +35 -0
- package/dist/plugin-sdk/config-B9ODwgpz.js +37426 -0
- package/dist/plugin-sdk/deliver-B1fFpKjV.js +1757 -0
- package/dist/plugin-sdk/deliver-runtime-DB-VRMe1.js +15 -0
- package/dist/plugin-sdk/deps-send-discord.runtime-DklqycYG.js +15 -0
- package/dist/plugin-sdk/deps-send-imessage.runtime-Chs8zeon.js +14 -0
- package/dist/plugin-sdk/deps-send-signal.runtime-clW9aSJP.js +13 -0
- package/dist/plugin-sdk/deps-send-slack.runtime-BUx0LYY1.js +13 -0
- package/dist/plugin-sdk/deps-send-telegram.runtime-LECSHgMG.js +16 -0
- package/dist/plugin-sdk/deps-send-whatsapp.runtime-D2d65fw0.js +40 -0
- package/dist/plugin-sdk/diagnostic-CxIvS-C2.js +315 -0
- package/dist/plugin-sdk/dispatch-BqlR4dPx.js +105863 -0
- package/dist/plugin-sdk/env-b9k1PHMI.js +34 -0
- package/dist/plugin-sdk/fetch-PoxzAANT.js +326 -0
- package/dist/plugin-sdk/fetch-guard-4UVSZ0uS.js +164 -0
- package/dist/plugin-sdk/image-Ch6M4tnJ.js +2420 -0
- package/dist/plugin-sdk/image-runtime-CSh2o5wY.js +8 -0
- package/dist/plugin-sdk/index.js +35 -35
- package/dist/plugin-sdk/ir-CugsqGH8.js +1312 -0
- package/dist/plugin-sdk/local-roots-adnEg9zb.js +217 -0
- package/dist/plugin-sdk/logger-D6zRubj0.js +1164 -0
- package/dist/plugin-sdk/login-CYvkQ0At.js +54 -0
- package/dist/plugin-sdk/login-qr-ll4NtaT5.js +316 -0
- package/dist/plugin-sdk/manager-CHy8IclH.js +3959 -0
- package/dist/plugin-sdk/manager-runtime-C70EkEr7.js +11 -0
- package/dist/plugin-sdk/outbound-Wzs2iN7X.js +216 -0
- package/dist/plugin-sdk/outbound-attachment-khXJwucX.js +17 -0
- package/dist/plugin-sdk/paths-BtVqCdw4.js +3063 -0
- package/dist/plugin-sdk/pi-model-discovery-Dh4ziodY.js +131 -0
- package/dist/plugin-sdk/pi-model-discovery-runtime-b83Xe-HT.js +8 -0
- package/dist/plugin-sdk/pi-tools.before-tool-call.runtime-C1z5CDBF.js +349 -0
- package/dist/plugin-sdk/proxy-fetch-CJEmoBxi.js +54 -0
- package/dist/plugin-sdk/pw-ai-Dj3Cvlzl.js +1990 -0
- package/dist/plugin-sdk/qmd-manager-egHUAseQ.js +1581 -0
- package/dist/plugin-sdk/resolve-outbound-target-BiICvIKs.js +38 -0
- package/dist/plugin-sdk/runtime-whatsapp-login.runtime-DNApufzW.js +9 -0
- package/dist/plugin-sdk/runtime-whatsapp-outbound.runtime-CBmtfIQ8.js +13 -0
- package/dist/plugin-sdk/send-CScblaI4.js +532 -0
- package/dist/plugin-sdk/send-CeHhnld6.js +407 -0
- package/dist/plugin-sdk/send-DP_c8JfR.js +3277 -0
- package/dist/plugin-sdk/send-Dc5fI6e8.js +495 -0
- package/dist/plugin-sdk/send-l-77_s1_.js +2507 -0
- package/dist/plugin-sdk/session-CkOKZaqa.js +166 -0
- package/dist/plugin-sdk/signal.js +2 -2
- package/dist/plugin-sdk/skill-commands-BohYCgkq.js +336 -0
- package/dist/plugin-sdk/slash-commands.runtime-DpLfVTM6.js +8 -0
- package/dist/plugin-sdk/slash-dispatch.runtime-CASMHwpm.js +35 -0
- package/dist/plugin-sdk/slash-skill-commands.runtime-D7rrJEci.js +9 -0
- package/dist/plugin-sdk/sqlite-CJE3X7Mv.js +1005 -0
- package/dist/plugin-sdk/subagent-registry-runtime-B1oo5bih.js +35 -0
- package/dist/plugin-sdk/tables-D5VgpTmm.js +53 -0
- package/dist/plugin-sdk/target-errors-C6zZ_OpA.js +191 -0
- package/dist/plugin-sdk/tokens-DUnJnpMS.js +50 -0
- package/dist/plugin-sdk/web-TfUM1nSi.js +39 -0
- package/dist/plugin-sdk/whatsapp-actions-DuWJ0j1r.js +71 -0
- package/extensions/mfa-auth/index.ts +36 -17
- package/extensions/mfa-auth/src/auth-manager.ts +4 -0
- package/extensions/mfa-auth/src/notification-service.ts +5 -1
- package/package.json +1 -1
|
@@ -0,0 +1,3063 @@
|
|
|
1
|
+
import { c as resolveStateDir, d as resolveRequiredHomeDir, l as expandHomePrefix } from "./paths-eFexkPEh.js";
|
|
2
|
+
import { B as shouldLogVerbose, D as resolveUserPath, I as danger, Y as resolveNodeRequireFromMeta, a as createSubsystemLogger, i as logWarn, n as logError, t as logDebug, w as pathExists$1 } from "./logger-D6zRubj0.js";
|
|
3
|
+
import fs, { constants, readFileSync, statSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import fs$1 from "node:fs/promises";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
import { execFile, spawn } from "node:child_process";
|
|
10
|
+
import process$1 from "node:process";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { pipeline } from "node:stream/promises";
|
|
13
|
+
//#region src/sessions/session-key-utils.ts
|
|
14
|
+
/**
|
|
15
|
+
* Parse agent-scoped session keys in a canonical, case-insensitive way.
|
|
16
|
+
* Returned values are normalized to lowercase for stable comparisons/routing.
|
|
17
|
+
*/
|
|
18
|
+
function parseAgentSessionKey(sessionKey) {
|
|
19
|
+
const raw = (sessionKey ?? "").trim().toLowerCase();
|
|
20
|
+
if (!raw) return null;
|
|
21
|
+
const parts = raw.split(":").filter(Boolean);
|
|
22
|
+
if (parts.length < 3) return null;
|
|
23
|
+
if (parts[0] !== "agent") return null;
|
|
24
|
+
const agentId = parts[1]?.trim();
|
|
25
|
+
const rest = parts.slice(2).join(":");
|
|
26
|
+
if (!agentId || !rest) return null;
|
|
27
|
+
return {
|
|
28
|
+
agentId,
|
|
29
|
+
rest
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Best-effort chat-type extraction from session keys across canonical and legacy formats.
|
|
34
|
+
*/
|
|
35
|
+
function deriveSessionChatType(sessionKey) {
|
|
36
|
+
const raw = (sessionKey ?? "").trim().toLowerCase();
|
|
37
|
+
if (!raw) return "unknown";
|
|
38
|
+
const scoped = parseAgentSessionKey(raw)?.rest ?? raw;
|
|
39
|
+
const tokens = new Set(scoped.split(":").filter(Boolean));
|
|
40
|
+
if (tokens.has("group")) return "group";
|
|
41
|
+
if (tokens.has("channel")) return "channel";
|
|
42
|
+
if (tokens.has("direct") || tokens.has("dm")) return "direct";
|
|
43
|
+
if (/^discord:(?:[^:]+:)?guild-[^:]+:channel-[^:]+$/.test(scoped)) return "channel";
|
|
44
|
+
return "unknown";
|
|
45
|
+
}
|
|
46
|
+
function isCronSessionKey(sessionKey) {
|
|
47
|
+
const parsed = parseAgentSessionKey(sessionKey);
|
|
48
|
+
if (!parsed) return false;
|
|
49
|
+
return parsed.rest.toLowerCase().startsWith("cron:");
|
|
50
|
+
}
|
|
51
|
+
function isSubagentSessionKey(sessionKey) {
|
|
52
|
+
const raw = (sessionKey ?? "").trim();
|
|
53
|
+
if (!raw) return false;
|
|
54
|
+
if (raw.toLowerCase().startsWith("subagent:")) return true;
|
|
55
|
+
const parsed = parseAgentSessionKey(raw);
|
|
56
|
+
return Boolean((parsed?.rest ?? "").toLowerCase().startsWith("subagent:"));
|
|
57
|
+
}
|
|
58
|
+
function getSubagentDepth(sessionKey) {
|
|
59
|
+
const raw = (sessionKey ?? "").trim().toLowerCase();
|
|
60
|
+
if (!raw) return 0;
|
|
61
|
+
return raw.split(":subagent:").length - 1;
|
|
62
|
+
}
|
|
63
|
+
function isAcpSessionKey(sessionKey) {
|
|
64
|
+
const raw = (sessionKey ?? "").trim();
|
|
65
|
+
if (!raw) return false;
|
|
66
|
+
if (raw.toLowerCase().startsWith("acp:")) return true;
|
|
67
|
+
const parsed = parseAgentSessionKey(raw);
|
|
68
|
+
return Boolean((parsed?.rest ?? "").toLowerCase().startsWith("acp:"));
|
|
69
|
+
}
|
|
70
|
+
const THREAD_SESSION_MARKERS = [":thread:", ":topic:"];
|
|
71
|
+
function resolveThreadParentSessionKey(sessionKey) {
|
|
72
|
+
const raw = (sessionKey ?? "").trim();
|
|
73
|
+
if (!raw) return null;
|
|
74
|
+
const normalized = raw.toLowerCase();
|
|
75
|
+
let idx = -1;
|
|
76
|
+
for (const marker of THREAD_SESSION_MARKERS) {
|
|
77
|
+
const candidate = normalized.lastIndexOf(marker);
|
|
78
|
+
if (candidate > idx) idx = candidate;
|
|
79
|
+
}
|
|
80
|
+
if (idx <= 0) return null;
|
|
81
|
+
const parent = raw.slice(0, idx).trim();
|
|
82
|
+
return parent ? parent : null;
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region src/infra/prototype-keys.ts
|
|
86
|
+
const BLOCKED_OBJECT_KEYS = new Set([
|
|
87
|
+
"__proto__",
|
|
88
|
+
"prototype",
|
|
89
|
+
"constructor"
|
|
90
|
+
]);
|
|
91
|
+
function isBlockedObjectKey(key) {
|
|
92
|
+
return BLOCKED_OBJECT_KEYS.has(key);
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/routing/account-id.ts
|
|
96
|
+
const DEFAULT_ACCOUNT_ID = "default";
|
|
97
|
+
const VALID_ID_RE$1 = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
|
|
98
|
+
const INVALID_CHARS_RE$1 = /[^a-z0-9_-]+/g;
|
|
99
|
+
const LEADING_DASH_RE$1 = /^-+/;
|
|
100
|
+
const TRAILING_DASH_RE$1 = /-+$/;
|
|
101
|
+
const ACCOUNT_ID_CACHE_MAX = 512;
|
|
102
|
+
const normalizeAccountIdCache = /* @__PURE__ */ new Map();
|
|
103
|
+
const normalizeOptionalAccountIdCache = /* @__PURE__ */ new Map();
|
|
104
|
+
function canonicalizeAccountId(value) {
|
|
105
|
+
if (VALID_ID_RE$1.test(value)) return value.toLowerCase();
|
|
106
|
+
return value.toLowerCase().replace(INVALID_CHARS_RE$1, "-").replace(LEADING_DASH_RE$1, "").replace(TRAILING_DASH_RE$1, "").slice(0, 64);
|
|
107
|
+
}
|
|
108
|
+
function normalizeCanonicalAccountId(value) {
|
|
109
|
+
const canonical = canonicalizeAccountId(value);
|
|
110
|
+
if (!canonical || isBlockedObjectKey(canonical)) return;
|
|
111
|
+
return canonical;
|
|
112
|
+
}
|
|
113
|
+
function normalizeAccountId(value) {
|
|
114
|
+
const trimmed = (value ?? "").trim();
|
|
115
|
+
if (!trimmed) return DEFAULT_ACCOUNT_ID;
|
|
116
|
+
const cached = normalizeAccountIdCache.get(trimmed);
|
|
117
|
+
if (cached) return cached;
|
|
118
|
+
const normalized = normalizeCanonicalAccountId(trimmed) || "default";
|
|
119
|
+
setNormalizeCache(normalizeAccountIdCache, trimmed, normalized);
|
|
120
|
+
return normalized;
|
|
121
|
+
}
|
|
122
|
+
function normalizeOptionalAccountId(value) {
|
|
123
|
+
const trimmed = (value ?? "").trim();
|
|
124
|
+
if (!trimmed) return;
|
|
125
|
+
if (normalizeOptionalAccountIdCache.has(trimmed)) return normalizeOptionalAccountIdCache.get(trimmed);
|
|
126
|
+
const normalized = normalizeCanonicalAccountId(trimmed) || void 0;
|
|
127
|
+
setNormalizeCache(normalizeOptionalAccountIdCache, trimmed, normalized);
|
|
128
|
+
return normalized;
|
|
129
|
+
}
|
|
130
|
+
function setNormalizeCache(cache, key, value) {
|
|
131
|
+
cache.set(key, value);
|
|
132
|
+
if (cache.size <= ACCOUNT_ID_CACHE_MAX) return;
|
|
133
|
+
const oldest = cache.keys().next();
|
|
134
|
+
if (!oldest.done) cache.delete(oldest.value);
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/routing/session-key.ts
|
|
138
|
+
const DEFAULT_AGENT_ID = "main";
|
|
139
|
+
const DEFAULT_MAIN_KEY = "main";
|
|
140
|
+
const VALID_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
|
|
141
|
+
const INVALID_CHARS_RE = /[^a-z0-9_-]+/g;
|
|
142
|
+
const LEADING_DASH_RE = /^-+/;
|
|
143
|
+
const TRAILING_DASH_RE = /-+$/;
|
|
144
|
+
function normalizeToken(value) {
|
|
145
|
+
return (value ?? "").trim().toLowerCase();
|
|
146
|
+
}
|
|
147
|
+
function scopedHeartbeatWakeOptions(sessionKey, wakeOptions) {
|
|
148
|
+
return parseAgentSessionKey(sessionKey) ? {
|
|
149
|
+
...wakeOptions,
|
|
150
|
+
sessionKey
|
|
151
|
+
} : wakeOptions;
|
|
152
|
+
}
|
|
153
|
+
function normalizeMainKey(value) {
|
|
154
|
+
const trimmed = (value ?? "").trim();
|
|
155
|
+
return trimmed ? trimmed.toLowerCase() : DEFAULT_MAIN_KEY;
|
|
156
|
+
}
|
|
157
|
+
function resolveAgentIdFromSessionKey(sessionKey) {
|
|
158
|
+
return normalizeAgentId(parseAgentSessionKey(sessionKey)?.agentId ?? "main");
|
|
159
|
+
}
|
|
160
|
+
function classifySessionKeyShape(sessionKey) {
|
|
161
|
+
const raw = (sessionKey ?? "").trim();
|
|
162
|
+
if (!raw) return "missing";
|
|
163
|
+
if (parseAgentSessionKey(raw)) return "agent";
|
|
164
|
+
return raw.toLowerCase().startsWith("agent:") ? "malformed_agent" : "legacy_or_alias";
|
|
165
|
+
}
|
|
166
|
+
function normalizeAgentId(value) {
|
|
167
|
+
const trimmed = (value ?? "").trim();
|
|
168
|
+
if (!trimmed) return DEFAULT_AGENT_ID;
|
|
169
|
+
if (VALID_ID_RE.test(trimmed)) return trimmed.toLowerCase();
|
|
170
|
+
return trimmed.toLowerCase().replace(INVALID_CHARS_RE, "-").replace(LEADING_DASH_RE, "").replace(TRAILING_DASH_RE, "").slice(0, 64) || "main";
|
|
171
|
+
}
|
|
172
|
+
function isValidAgentId(value) {
|
|
173
|
+
const trimmed = (value ?? "").trim();
|
|
174
|
+
return Boolean(trimmed) && VALID_ID_RE.test(trimmed);
|
|
175
|
+
}
|
|
176
|
+
function sanitizeAgentId(value) {
|
|
177
|
+
return normalizeAgentId(value);
|
|
178
|
+
}
|
|
179
|
+
function buildAgentMainSessionKey(params) {
|
|
180
|
+
return `agent:${normalizeAgentId(params.agentId)}:${normalizeMainKey(params.mainKey)}`;
|
|
181
|
+
}
|
|
182
|
+
function buildAgentPeerSessionKey(params) {
|
|
183
|
+
const peerKind = params.peerKind ?? "direct";
|
|
184
|
+
if (peerKind === "direct") {
|
|
185
|
+
const dmScope = params.dmScope ?? "main";
|
|
186
|
+
let peerId = (params.peerId ?? "").trim();
|
|
187
|
+
const linkedPeerId = dmScope === "main" ? null : resolveLinkedPeerId({
|
|
188
|
+
identityLinks: params.identityLinks,
|
|
189
|
+
channel: params.channel,
|
|
190
|
+
peerId
|
|
191
|
+
});
|
|
192
|
+
if (linkedPeerId) peerId = linkedPeerId;
|
|
193
|
+
peerId = peerId.toLowerCase();
|
|
194
|
+
if (dmScope === "per-account-channel-peer" && peerId) {
|
|
195
|
+
const channel = (params.channel ?? "").trim().toLowerCase() || "unknown";
|
|
196
|
+
const accountId = normalizeAccountId(params.accountId);
|
|
197
|
+
return `agent:${normalizeAgentId(params.agentId)}:${channel}:${accountId}:direct:${peerId}`;
|
|
198
|
+
}
|
|
199
|
+
if (dmScope === "per-channel-peer" && peerId) {
|
|
200
|
+
const channel = (params.channel ?? "").trim().toLowerCase() || "unknown";
|
|
201
|
+
return `agent:${normalizeAgentId(params.agentId)}:${channel}:direct:${peerId}`;
|
|
202
|
+
}
|
|
203
|
+
if (dmScope === "per-peer" && peerId) return `agent:${normalizeAgentId(params.agentId)}:direct:${peerId}`;
|
|
204
|
+
return buildAgentMainSessionKey({
|
|
205
|
+
agentId: params.agentId,
|
|
206
|
+
mainKey: params.mainKey
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
const channel = (params.channel ?? "").trim().toLowerCase() || "unknown";
|
|
210
|
+
const peerId = ((params.peerId ?? "").trim() || "unknown").toLowerCase();
|
|
211
|
+
return `agent:${normalizeAgentId(params.agentId)}:${channel}:${peerKind}:${peerId}`;
|
|
212
|
+
}
|
|
213
|
+
function resolveLinkedPeerId(params) {
|
|
214
|
+
const identityLinks = params.identityLinks;
|
|
215
|
+
if (!identityLinks) return null;
|
|
216
|
+
const peerId = params.peerId.trim();
|
|
217
|
+
if (!peerId) return null;
|
|
218
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
219
|
+
const rawCandidate = normalizeToken(peerId);
|
|
220
|
+
if (rawCandidate) candidates.add(rawCandidate);
|
|
221
|
+
const channel = normalizeToken(params.channel);
|
|
222
|
+
if (channel) {
|
|
223
|
+
const scopedCandidate = normalizeToken(`${channel}:${peerId}`);
|
|
224
|
+
if (scopedCandidate) candidates.add(scopedCandidate);
|
|
225
|
+
}
|
|
226
|
+
if (candidates.size === 0) return null;
|
|
227
|
+
for (const [canonical, ids] of Object.entries(identityLinks)) {
|
|
228
|
+
const canonicalName = canonical.trim();
|
|
229
|
+
if (!canonicalName) continue;
|
|
230
|
+
if (!Array.isArray(ids)) continue;
|
|
231
|
+
for (const id of ids) {
|
|
232
|
+
const normalized = normalizeToken(id);
|
|
233
|
+
if (normalized && candidates.has(normalized)) return canonicalName;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
function buildGroupHistoryKey(params) {
|
|
239
|
+
const channel = normalizeToken(params.channel) || "unknown";
|
|
240
|
+
const accountId = normalizeAccountId(params.accountId);
|
|
241
|
+
const peerId = params.peerId.trim().toLowerCase() || "unknown";
|
|
242
|
+
return `${channel}:${accountId}:${params.peerKind}:${peerId}`;
|
|
243
|
+
}
|
|
244
|
+
function resolveThreadSessionKeys(params) {
|
|
245
|
+
const threadId = (params.threadId ?? "").trim();
|
|
246
|
+
if (!threadId) return {
|
|
247
|
+
sessionKey: params.baseSessionKey,
|
|
248
|
+
parentSessionKey: void 0
|
|
249
|
+
};
|
|
250
|
+
const normalizedThreadId = (params.normalizeThreadId ?? ((value) => value.toLowerCase()))(threadId);
|
|
251
|
+
return {
|
|
252
|
+
sessionKey: params.useSuffix ?? true ? `${params.baseSessionKey}:thread:${normalizedThreadId}` : params.baseSessionKey,
|
|
253
|
+
parentSessionKey: params.parentSessionKey
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region src/infra/openclaw-exec-env.ts
|
|
258
|
+
const OPENCLAW_CLI_ENV_VAR = "OPENCLAW_CLI";
|
|
259
|
+
function markOpenClawExecEnv(env) {
|
|
260
|
+
return {
|
|
261
|
+
...env,
|
|
262
|
+
[OPENCLAW_CLI_ENV_VAR]: "1"
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
//#endregion
|
|
266
|
+
//#region src/config/model-input.ts
|
|
267
|
+
function resolveAgentModelPrimaryValue(model) {
|
|
268
|
+
if (typeof model === "string") return model.trim() || void 0;
|
|
269
|
+
if (!model || typeof model !== "object") return;
|
|
270
|
+
return model.primary?.trim() || void 0;
|
|
271
|
+
}
|
|
272
|
+
function resolveAgentModelFallbackValues(model) {
|
|
273
|
+
if (!model || typeof model !== "object") return [];
|
|
274
|
+
return Array.isArray(model.fallbacks) ? model.fallbacks : [];
|
|
275
|
+
}
|
|
276
|
+
function toAgentModelListLike(model) {
|
|
277
|
+
if (typeof model === "string") {
|
|
278
|
+
const primary = model.trim();
|
|
279
|
+
return primary ? { primary } : void 0;
|
|
280
|
+
}
|
|
281
|
+
if (!model || typeof model !== "object") return;
|
|
282
|
+
return model;
|
|
283
|
+
}
|
|
284
|
+
//#endregion
|
|
285
|
+
//#region src/shared/string-normalization.ts
|
|
286
|
+
function normalizeStringEntries(list) {
|
|
287
|
+
return (list ?? []).map((entry) => String(entry).trim()).filter(Boolean);
|
|
288
|
+
}
|
|
289
|
+
function normalizeStringEntriesLower(list) {
|
|
290
|
+
return normalizeStringEntries(list).map((entry) => entry.toLowerCase());
|
|
291
|
+
}
|
|
292
|
+
function normalizeHyphenSlug(raw) {
|
|
293
|
+
const trimmed = raw?.trim().toLowerCase() ?? "";
|
|
294
|
+
if (!trimmed) return "";
|
|
295
|
+
return trimmed.replace(/\s+/g, "-").replace(/[^a-z0-9#@._+-]+/g, "-").replace(/-{2,}/g, "-").replace(/^[-.]+|[-.]+$/g, "");
|
|
296
|
+
}
|
|
297
|
+
function normalizeAtHashSlug(raw) {
|
|
298
|
+
const trimmed = raw?.trim().toLowerCase() ?? "";
|
|
299
|
+
if (!trimmed) return "";
|
|
300
|
+
return trimmed.replace(/^[@#]+/, "").replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]+/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
|
|
301
|
+
}
|
|
302
|
+
//#endregion
|
|
303
|
+
//#region src/agents/skills/filter.ts
|
|
304
|
+
function normalizeSkillFilter(skillFilter) {
|
|
305
|
+
if (skillFilter === void 0) return;
|
|
306
|
+
return normalizeStringEntries(skillFilter);
|
|
307
|
+
}
|
|
308
|
+
//#endregion
|
|
309
|
+
//#region src/infra/path-guards.ts
|
|
310
|
+
const NOT_FOUND_CODES = new Set(["ENOENT", "ENOTDIR"]);
|
|
311
|
+
const SYMLINK_OPEN_CODES = new Set([
|
|
312
|
+
"ELOOP",
|
|
313
|
+
"EINVAL",
|
|
314
|
+
"ENOTSUP"
|
|
315
|
+
]);
|
|
316
|
+
function normalizeWindowsPathForComparison(input) {
|
|
317
|
+
let normalized = path.win32.normalize(input);
|
|
318
|
+
if (normalized.startsWith("\\\\?\\")) {
|
|
319
|
+
normalized = normalized.slice(4);
|
|
320
|
+
if (normalized.toUpperCase().startsWith("UNC\\")) normalized = `\\\\${normalized.slice(4)}`;
|
|
321
|
+
}
|
|
322
|
+
return normalized.replaceAll("/", "\\").toLowerCase();
|
|
323
|
+
}
|
|
324
|
+
function isNodeError(value) {
|
|
325
|
+
return Boolean(value && typeof value === "object" && "code" in value);
|
|
326
|
+
}
|
|
327
|
+
function hasNodeErrorCode(value, code) {
|
|
328
|
+
return isNodeError(value) && value.code === code;
|
|
329
|
+
}
|
|
330
|
+
function isNotFoundPathError(value) {
|
|
331
|
+
return isNodeError(value) && typeof value.code === "string" && NOT_FOUND_CODES.has(value.code);
|
|
332
|
+
}
|
|
333
|
+
function isSymlinkOpenError(value) {
|
|
334
|
+
return isNodeError(value) && typeof value.code === "string" && SYMLINK_OPEN_CODES.has(value.code);
|
|
335
|
+
}
|
|
336
|
+
function isPathInside(root, target) {
|
|
337
|
+
const resolvedRoot = path.resolve(root);
|
|
338
|
+
const resolvedTarget = path.resolve(target);
|
|
339
|
+
if (process.platform === "win32") {
|
|
340
|
+
const rootForCompare = normalizeWindowsPathForComparison(resolvedRoot);
|
|
341
|
+
const targetForCompare = normalizeWindowsPathForComparison(resolvedTarget);
|
|
342
|
+
const relative = path.win32.relative(rootForCompare, targetForCompare);
|
|
343
|
+
return relative === "" || !relative.startsWith("..") && !path.win32.isAbsolute(relative);
|
|
344
|
+
}
|
|
345
|
+
const relative = path.relative(resolvedRoot, resolvedTarget);
|
|
346
|
+
return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
347
|
+
}
|
|
348
|
+
//#endregion
|
|
349
|
+
//#region src/infra/boundary-path.ts
|
|
350
|
+
const BOUNDARY_PATH_ALIAS_POLICIES = {
|
|
351
|
+
strict: Object.freeze({
|
|
352
|
+
allowFinalSymlinkForUnlink: false,
|
|
353
|
+
allowFinalHardlinkForUnlink: false
|
|
354
|
+
}),
|
|
355
|
+
unlinkTarget: Object.freeze({
|
|
356
|
+
allowFinalSymlinkForUnlink: true,
|
|
357
|
+
allowFinalHardlinkForUnlink: true
|
|
358
|
+
})
|
|
359
|
+
};
|
|
360
|
+
async function resolveBoundaryPath(params) {
|
|
361
|
+
const rootPath = path.resolve(params.rootPath);
|
|
362
|
+
const absolutePath = path.resolve(params.absolutePath);
|
|
363
|
+
const context = createBoundaryResolutionContext({
|
|
364
|
+
resolveParams: params,
|
|
365
|
+
rootPath,
|
|
366
|
+
absolutePath,
|
|
367
|
+
rootCanonicalPath: params.rootCanonicalPath ? path.resolve(params.rootCanonicalPath) : await resolvePathViaExistingAncestor(rootPath),
|
|
368
|
+
outsideLexicalCanonicalPath: await resolveOutsideLexicalCanonicalPathAsync({
|
|
369
|
+
rootPath,
|
|
370
|
+
absolutePath
|
|
371
|
+
})
|
|
372
|
+
});
|
|
373
|
+
const outsideResult = await resolveOutsideBoundaryPathAsync({
|
|
374
|
+
boundaryLabel: params.boundaryLabel,
|
|
375
|
+
context
|
|
376
|
+
});
|
|
377
|
+
if (outsideResult) return outsideResult;
|
|
378
|
+
return resolveBoundaryPathLexicalAsync({
|
|
379
|
+
params,
|
|
380
|
+
absolutePath: context.absolutePath,
|
|
381
|
+
rootPath: context.rootPath,
|
|
382
|
+
rootCanonicalPath: context.rootCanonicalPath
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
function resolveBoundaryPathSync(params) {
|
|
386
|
+
const rootPath = path.resolve(params.rootPath);
|
|
387
|
+
const absolutePath = path.resolve(params.absolutePath);
|
|
388
|
+
const context = createBoundaryResolutionContext({
|
|
389
|
+
resolveParams: params,
|
|
390
|
+
rootPath,
|
|
391
|
+
absolutePath,
|
|
392
|
+
rootCanonicalPath: params.rootCanonicalPath ? path.resolve(params.rootCanonicalPath) : resolvePathViaExistingAncestorSync(rootPath),
|
|
393
|
+
outsideLexicalCanonicalPath: resolveOutsideLexicalCanonicalPathSync({
|
|
394
|
+
rootPath,
|
|
395
|
+
absolutePath
|
|
396
|
+
})
|
|
397
|
+
});
|
|
398
|
+
const outsideResult = resolveOutsideBoundaryPathSync({
|
|
399
|
+
boundaryLabel: params.boundaryLabel,
|
|
400
|
+
context
|
|
401
|
+
});
|
|
402
|
+
if (outsideResult) return outsideResult;
|
|
403
|
+
return resolveBoundaryPathLexicalSync({
|
|
404
|
+
params,
|
|
405
|
+
absolutePath: context.absolutePath,
|
|
406
|
+
rootPath: context.rootPath,
|
|
407
|
+
rootCanonicalPath: context.rootCanonicalPath
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
function isPromiseLike(value) {
|
|
411
|
+
return Boolean(value && (typeof value === "object" || typeof value === "function") && "then" in value && typeof value.then === "function");
|
|
412
|
+
}
|
|
413
|
+
function createLexicalTraversalState(params) {
|
|
414
|
+
return {
|
|
415
|
+
segments: path.relative(params.rootPath, params.absolutePath).split(path.sep).filter(Boolean),
|
|
416
|
+
allowFinalSymlink: params.params.policy?.allowFinalSymlinkForUnlink === true,
|
|
417
|
+
canonicalCursor: params.rootCanonicalPath,
|
|
418
|
+
lexicalCursor: params.rootPath,
|
|
419
|
+
preserveFinalSymlink: false
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function assertLexicalCursorInsideBoundary(params) {
|
|
423
|
+
assertInsideBoundary({
|
|
424
|
+
boundaryLabel: params.params.boundaryLabel,
|
|
425
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
426
|
+
candidatePath: params.candidatePath,
|
|
427
|
+
absolutePath: params.absolutePath
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
function applyMissingSuffixToCanonicalCursor(params) {
|
|
431
|
+
const missingSuffix = params.state.segments.slice(params.missingFromIndex);
|
|
432
|
+
params.state.canonicalCursor = path.resolve(params.state.canonicalCursor, ...missingSuffix);
|
|
433
|
+
assertLexicalCursorInsideBoundary({
|
|
434
|
+
params: params.params,
|
|
435
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
436
|
+
candidatePath: params.state.canonicalCursor,
|
|
437
|
+
absolutePath: params.absolutePath
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
function advanceCanonicalCursorForSegment(params) {
|
|
441
|
+
params.state.canonicalCursor = path.resolve(params.state.canonicalCursor, params.segment);
|
|
442
|
+
assertLexicalCursorInsideBoundary({
|
|
443
|
+
params: params.params,
|
|
444
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
445
|
+
candidatePath: params.state.canonicalCursor,
|
|
446
|
+
absolutePath: params.absolutePath
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
function finalizeLexicalResolution(params) {
|
|
450
|
+
assertLexicalCursorInsideBoundary({
|
|
451
|
+
params: params.params,
|
|
452
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
453
|
+
candidatePath: params.state.canonicalCursor,
|
|
454
|
+
absolutePath: params.absolutePath
|
|
455
|
+
});
|
|
456
|
+
return buildResolvedBoundaryPath({
|
|
457
|
+
absolutePath: params.absolutePath,
|
|
458
|
+
canonicalPath: params.state.canonicalCursor,
|
|
459
|
+
rootPath: params.rootPath,
|
|
460
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
461
|
+
kind: params.kind
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
function handleLexicalLstatFailure(params) {
|
|
465
|
+
if (!isNotFoundPathError(params.error)) return false;
|
|
466
|
+
applyMissingSuffixToCanonicalCursor({
|
|
467
|
+
state: params.state,
|
|
468
|
+
missingFromIndex: params.missingFromIndex,
|
|
469
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
470
|
+
params: params.resolveParams,
|
|
471
|
+
absolutePath: params.absolutePath
|
|
472
|
+
});
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
function handleLexicalStatReadFailure(params) {
|
|
476
|
+
if (handleLexicalLstatFailure({
|
|
477
|
+
error: params.error,
|
|
478
|
+
state: params.state,
|
|
479
|
+
missingFromIndex: params.missingFromIndex,
|
|
480
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
481
|
+
resolveParams: params.resolveParams,
|
|
482
|
+
absolutePath: params.absolutePath
|
|
483
|
+
})) return null;
|
|
484
|
+
throw params.error;
|
|
485
|
+
}
|
|
486
|
+
function handleLexicalStatDisposition(params) {
|
|
487
|
+
if (!params.isSymbolicLink) {
|
|
488
|
+
advanceCanonicalCursorForSegment({
|
|
489
|
+
state: params.state,
|
|
490
|
+
segment: params.segment,
|
|
491
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
492
|
+
params: params.resolveParams,
|
|
493
|
+
absolutePath: params.absolutePath
|
|
494
|
+
});
|
|
495
|
+
return "continue";
|
|
496
|
+
}
|
|
497
|
+
if (params.state.allowFinalSymlink && params.isLast) {
|
|
498
|
+
params.state.preserveFinalSymlink = true;
|
|
499
|
+
advanceCanonicalCursorForSegment({
|
|
500
|
+
state: params.state,
|
|
501
|
+
segment: params.segment,
|
|
502
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
503
|
+
params: params.resolveParams,
|
|
504
|
+
absolutePath: params.absolutePath
|
|
505
|
+
});
|
|
506
|
+
return "break";
|
|
507
|
+
}
|
|
508
|
+
return "resolve-link";
|
|
509
|
+
}
|
|
510
|
+
function applyResolvedSymlinkHop(params) {
|
|
511
|
+
if (!isPathInside(params.rootCanonicalPath, params.linkCanonical)) throw symlinkEscapeError({
|
|
512
|
+
boundaryLabel: params.boundaryLabel,
|
|
513
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
514
|
+
symlinkPath: params.state.lexicalCursor
|
|
515
|
+
});
|
|
516
|
+
params.state.canonicalCursor = params.linkCanonical;
|
|
517
|
+
params.state.lexicalCursor = params.linkCanonical;
|
|
518
|
+
}
|
|
519
|
+
function readLexicalStat(params) {
|
|
520
|
+
try {
|
|
521
|
+
const stat = params.read(params.state.lexicalCursor);
|
|
522
|
+
if (isPromiseLike(stat)) return Promise.resolve(stat).catch((error) => handleLexicalStatReadFailure({
|
|
523
|
+
...params,
|
|
524
|
+
error
|
|
525
|
+
}));
|
|
526
|
+
return stat;
|
|
527
|
+
} catch (error) {
|
|
528
|
+
return handleLexicalStatReadFailure({
|
|
529
|
+
...params,
|
|
530
|
+
error
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
function resolveAndApplySymlinkHop(params) {
|
|
535
|
+
const linkCanonical = params.resolveLinkCanonical(params.state.lexicalCursor);
|
|
536
|
+
if (isPromiseLike(linkCanonical)) return Promise.resolve(linkCanonical).then((value) => applyResolvedSymlinkHop({
|
|
537
|
+
state: params.state,
|
|
538
|
+
linkCanonical: value,
|
|
539
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
540
|
+
boundaryLabel: params.boundaryLabel
|
|
541
|
+
}));
|
|
542
|
+
applyResolvedSymlinkHop({
|
|
543
|
+
state: params.state,
|
|
544
|
+
linkCanonical,
|
|
545
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
546
|
+
boundaryLabel: params.boundaryLabel
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
function* iterateLexicalTraversal(state) {
|
|
550
|
+
for (let idx = 0; idx < state.segments.length; idx += 1) {
|
|
551
|
+
const segment = state.segments[idx] ?? "";
|
|
552
|
+
const isLast = idx === state.segments.length - 1;
|
|
553
|
+
state.lexicalCursor = path.join(state.lexicalCursor, segment);
|
|
554
|
+
yield {
|
|
555
|
+
idx,
|
|
556
|
+
segment,
|
|
557
|
+
isLast
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
async function resolveBoundaryPathLexicalAsync(params) {
|
|
562
|
+
const state = createLexicalTraversalState(params);
|
|
563
|
+
const sharedStepParams = {
|
|
564
|
+
state,
|
|
565
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
566
|
+
resolveParams: params.params,
|
|
567
|
+
absolutePath: params.absolutePath
|
|
568
|
+
};
|
|
569
|
+
for (const { idx, segment, isLast } of iterateLexicalTraversal(state)) {
|
|
570
|
+
const stat = await readLexicalStat({
|
|
571
|
+
...sharedStepParams,
|
|
572
|
+
missingFromIndex: idx,
|
|
573
|
+
read: (cursor) => fs$1.lstat(cursor)
|
|
574
|
+
});
|
|
575
|
+
if (!stat) break;
|
|
576
|
+
const disposition = handleLexicalStatDisposition({
|
|
577
|
+
...sharedStepParams,
|
|
578
|
+
isSymbolicLink: stat.isSymbolicLink(),
|
|
579
|
+
segment,
|
|
580
|
+
isLast
|
|
581
|
+
});
|
|
582
|
+
if (disposition === "continue") continue;
|
|
583
|
+
if (disposition === "break") break;
|
|
584
|
+
await resolveAndApplySymlinkHop({
|
|
585
|
+
state,
|
|
586
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
587
|
+
boundaryLabel: params.params.boundaryLabel,
|
|
588
|
+
resolveLinkCanonical: (cursor) => resolveSymlinkHopPath(cursor)
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
const kind = await getPathKind(params.absolutePath, state.preserveFinalSymlink);
|
|
592
|
+
return finalizeLexicalResolution({
|
|
593
|
+
...params,
|
|
594
|
+
state,
|
|
595
|
+
kind
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
function resolveBoundaryPathLexicalSync(params) {
|
|
599
|
+
const state = createLexicalTraversalState(params);
|
|
600
|
+
for (let idx = 0; idx < state.segments.length; idx += 1) {
|
|
601
|
+
const segment = state.segments[idx] ?? "";
|
|
602
|
+
const isLast = idx === state.segments.length - 1;
|
|
603
|
+
state.lexicalCursor = path.join(state.lexicalCursor, segment);
|
|
604
|
+
const maybeStat = readLexicalStat({
|
|
605
|
+
state,
|
|
606
|
+
missingFromIndex: idx,
|
|
607
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
608
|
+
resolveParams: params.params,
|
|
609
|
+
absolutePath: params.absolutePath,
|
|
610
|
+
read: (cursor) => fs.lstatSync(cursor)
|
|
611
|
+
});
|
|
612
|
+
if (isPromiseLike(maybeStat)) throw new Error("Unexpected async lexical stat");
|
|
613
|
+
const stat = maybeStat;
|
|
614
|
+
if (!stat) break;
|
|
615
|
+
const disposition = handleLexicalStatDisposition({
|
|
616
|
+
state,
|
|
617
|
+
isSymbolicLink: stat.isSymbolicLink(),
|
|
618
|
+
segment,
|
|
619
|
+
isLast,
|
|
620
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
621
|
+
resolveParams: params.params,
|
|
622
|
+
absolutePath: params.absolutePath
|
|
623
|
+
});
|
|
624
|
+
if (disposition === "continue") continue;
|
|
625
|
+
if (disposition === "break") break;
|
|
626
|
+
if (isPromiseLike(resolveAndApplySymlinkHop({
|
|
627
|
+
state,
|
|
628
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
629
|
+
boundaryLabel: params.params.boundaryLabel,
|
|
630
|
+
resolveLinkCanonical: (cursor) => resolveSymlinkHopPathSync(cursor)
|
|
631
|
+
}))) throw new Error("Unexpected async symlink resolution");
|
|
632
|
+
}
|
|
633
|
+
const kind = getPathKindSync(params.absolutePath, state.preserveFinalSymlink);
|
|
634
|
+
return finalizeLexicalResolution({
|
|
635
|
+
...params,
|
|
636
|
+
state,
|
|
637
|
+
kind
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
function resolveCanonicalOutsideLexicalPath(params) {
|
|
641
|
+
return params.outsideLexicalCanonicalPath ?? params.absolutePath;
|
|
642
|
+
}
|
|
643
|
+
function createBoundaryResolutionContext(params) {
|
|
644
|
+
const lexicalInside = isPathInside(params.rootPath, params.absolutePath);
|
|
645
|
+
const canonicalOutsideLexicalPath = resolveCanonicalOutsideLexicalPath({
|
|
646
|
+
absolutePath: params.absolutePath,
|
|
647
|
+
outsideLexicalCanonicalPath: params.outsideLexicalCanonicalPath
|
|
648
|
+
});
|
|
649
|
+
assertLexicalBoundaryOrCanonicalAlias({
|
|
650
|
+
skipLexicalRootCheck: params.resolveParams.skipLexicalRootCheck,
|
|
651
|
+
lexicalInside,
|
|
652
|
+
canonicalOutsideLexicalPath,
|
|
653
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
654
|
+
boundaryLabel: params.resolveParams.boundaryLabel,
|
|
655
|
+
rootPath: params.rootPath,
|
|
656
|
+
absolutePath: params.absolutePath
|
|
657
|
+
});
|
|
658
|
+
return {
|
|
659
|
+
rootPath: params.rootPath,
|
|
660
|
+
absolutePath: params.absolutePath,
|
|
661
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
662
|
+
lexicalInside,
|
|
663
|
+
canonicalOutsideLexicalPath
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
async function resolveOutsideBoundaryPathAsync(params) {
|
|
667
|
+
if (params.context.lexicalInside) return null;
|
|
668
|
+
const kind = await getPathKind(params.context.absolutePath, false);
|
|
669
|
+
return buildOutsideBoundaryPathFromContext({
|
|
670
|
+
boundaryLabel: params.boundaryLabel,
|
|
671
|
+
context: params.context,
|
|
672
|
+
kind
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
function resolveOutsideBoundaryPathSync(params) {
|
|
676
|
+
if (params.context.lexicalInside) return null;
|
|
677
|
+
const kind = getPathKindSync(params.context.absolutePath, false);
|
|
678
|
+
return buildOutsideBoundaryPathFromContext({
|
|
679
|
+
boundaryLabel: params.boundaryLabel,
|
|
680
|
+
context: params.context,
|
|
681
|
+
kind
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
function buildOutsideBoundaryPathFromContext(params) {
|
|
685
|
+
return buildOutsideLexicalBoundaryPath({
|
|
686
|
+
boundaryLabel: params.boundaryLabel,
|
|
687
|
+
rootCanonicalPath: params.context.rootCanonicalPath,
|
|
688
|
+
absolutePath: params.context.absolutePath,
|
|
689
|
+
canonicalOutsideLexicalPath: params.context.canonicalOutsideLexicalPath,
|
|
690
|
+
rootPath: params.context.rootPath,
|
|
691
|
+
kind: params.kind
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
async function resolveOutsideLexicalCanonicalPathAsync(params) {
|
|
695
|
+
if (isPathInside(params.rootPath, params.absolutePath)) return;
|
|
696
|
+
return await resolvePathViaExistingAncestor(params.absolutePath);
|
|
697
|
+
}
|
|
698
|
+
function resolveOutsideLexicalCanonicalPathSync(params) {
|
|
699
|
+
if (isPathInside(params.rootPath, params.absolutePath)) return;
|
|
700
|
+
return resolvePathViaExistingAncestorSync(params.absolutePath);
|
|
701
|
+
}
|
|
702
|
+
function buildOutsideLexicalBoundaryPath(params) {
|
|
703
|
+
assertInsideBoundary({
|
|
704
|
+
boundaryLabel: params.boundaryLabel,
|
|
705
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
706
|
+
candidatePath: params.canonicalOutsideLexicalPath,
|
|
707
|
+
absolutePath: params.absolutePath
|
|
708
|
+
});
|
|
709
|
+
return buildResolvedBoundaryPath({
|
|
710
|
+
absolutePath: params.absolutePath,
|
|
711
|
+
canonicalPath: params.canonicalOutsideLexicalPath,
|
|
712
|
+
rootPath: params.rootPath,
|
|
713
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
714
|
+
kind: params.kind
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
function assertLexicalBoundaryOrCanonicalAlias(params) {
|
|
718
|
+
if (params.skipLexicalRootCheck || params.lexicalInside) return;
|
|
719
|
+
if (isPathInside(params.rootCanonicalPath, params.canonicalOutsideLexicalPath)) return;
|
|
720
|
+
throw pathEscapeError({
|
|
721
|
+
boundaryLabel: params.boundaryLabel,
|
|
722
|
+
rootPath: params.rootPath,
|
|
723
|
+
absolutePath: params.absolutePath
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
function buildResolvedBoundaryPath(params) {
|
|
727
|
+
return {
|
|
728
|
+
absolutePath: params.absolutePath,
|
|
729
|
+
canonicalPath: params.canonicalPath,
|
|
730
|
+
rootPath: params.rootPath,
|
|
731
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
732
|
+
relativePath: relativeInsideRoot(params.rootCanonicalPath, params.canonicalPath),
|
|
733
|
+
exists: params.kind.exists,
|
|
734
|
+
kind: params.kind.kind
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
async function resolvePathViaExistingAncestor(targetPath) {
|
|
738
|
+
const normalized = path.resolve(targetPath);
|
|
739
|
+
let cursor = normalized;
|
|
740
|
+
const missingSuffix = [];
|
|
741
|
+
while (!isFilesystemRoot(cursor) && !await pathExists(cursor)) {
|
|
742
|
+
missingSuffix.unshift(path.basename(cursor));
|
|
743
|
+
const parent = path.dirname(cursor);
|
|
744
|
+
if (parent === cursor) break;
|
|
745
|
+
cursor = parent;
|
|
746
|
+
}
|
|
747
|
+
if (!await pathExists(cursor)) return normalized;
|
|
748
|
+
try {
|
|
749
|
+
const resolvedAncestor = path.resolve(await fs$1.realpath(cursor));
|
|
750
|
+
if (missingSuffix.length === 0) return resolvedAncestor;
|
|
751
|
+
return path.resolve(resolvedAncestor, ...missingSuffix);
|
|
752
|
+
} catch {
|
|
753
|
+
return normalized;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
function resolvePathViaExistingAncestorSync(targetPath) {
|
|
757
|
+
const normalized = path.resolve(targetPath);
|
|
758
|
+
let cursor = normalized;
|
|
759
|
+
const missingSuffix = [];
|
|
760
|
+
while (!isFilesystemRoot(cursor) && !fs.existsSync(cursor)) {
|
|
761
|
+
missingSuffix.unshift(path.basename(cursor));
|
|
762
|
+
const parent = path.dirname(cursor);
|
|
763
|
+
if (parent === cursor) break;
|
|
764
|
+
cursor = parent;
|
|
765
|
+
}
|
|
766
|
+
if (!fs.existsSync(cursor)) return normalized;
|
|
767
|
+
try {
|
|
768
|
+
const resolvedAncestor = path.resolve(fs.realpathSync(cursor));
|
|
769
|
+
if (missingSuffix.length === 0) return resolvedAncestor;
|
|
770
|
+
return path.resolve(resolvedAncestor, ...missingSuffix);
|
|
771
|
+
} catch {
|
|
772
|
+
return normalized;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
async function getPathKind(absolutePath, preserveFinalSymlink) {
|
|
776
|
+
try {
|
|
777
|
+
return {
|
|
778
|
+
exists: true,
|
|
779
|
+
kind: toResolvedKind(preserveFinalSymlink ? await fs$1.lstat(absolutePath) : await fs$1.stat(absolutePath))
|
|
780
|
+
};
|
|
781
|
+
} catch (error) {
|
|
782
|
+
if (isNotFoundPathError(error)) return {
|
|
783
|
+
exists: false,
|
|
784
|
+
kind: "missing"
|
|
785
|
+
};
|
|
786
|
+
throw error;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
function getPathKindSync(absolutePath, preserveFinalSymlink) {
|
|
790
|
+
try {
|
|
791
|
+
return {
|
|
792
|
+
exists: true,
|
|
793
|
+
kind: toResolvedKind(preserveFinalSymlink ? fs.lstatSync(absolutePath) : fs.statSync(absolutePath))
|
|
794
|
+
};
|
|
795
|
+
} catch (error) {
|
|
796
|
+
if (isNotFoundPathError(error)) return {
|
|
797
|
+
exists: false,
|
|
798
|
+
kind: "missing"
|
|
799
|
+
};
|
|
800
|
+
throw error;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
function toResolvedKind(stat) {
|
|
804
|
+
if (stat.isFile()) return "file";
|
|
805
|
+
if (stat.isDirectory()) return "directory";
|
|
806
|
+
if (stat.isSymbolicLink()) return "symlink";
|
|
807
|
+
return "other";
|
|
808
|
+
}
|
|
809
|
+
function relativeInsideRoot(rootPath, targetPath) {
|
|
810
|
+
const relative = path.relative(path.resolve(rootPath), path.resolve(targetPath));
|
|
811
|
+
if (!relative || relative === ".") return "";
|
|
812
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) return "";
|
|
813
|
+
return relative;
|
|
814
|
+
}
|
|
815
|
+
function assertInsideBoundary(params) {
|
|
816
|
+
if (isPathInside(params.rootCanonicalPath, params.candidatePath)) return;
|
|
817
|
+
throw new Error(`Path resolves outside ${params.boundaryLabel} (${shortPath$1(params.rootCanonicalPath)}): ${shortPath$1(params.absolutePath)}`);
|
|
818
|
+
}
|
|
819
|
+
function pathEscapeError(params) {
|
|
820
|
+
return /* @__PURE__ */ new Error(`Path escapes ${params.boundaryLabel} (${shortPath$1(params.rootPath)}): ${shortPath$1(params.absolutePath)}`);
|
|
821
|
+
}
|
|
822
|
+
function symlinkEscapeError(params) {
|
|
823
|
+
return /* @__PURE__ */ new Error(`Symlink escapes ${params.boundaryLabel} (${shortPath$1(params.rootCanonicalPath)}): ${shortPath$1(params.symlinkPath)}`);
|
|
824
|
+
}
|
|
825
|
+
function shortPath$1(value) {
|
|
826
|
+
const home = os.homedir();
|
|
827
|
+
if (value.startsWith(home)) return `~${value.slice(home.length)}`;
|
|
828
|
+
return value;
|
|
829
|
+
}
|
|
830
|
+
function isFilesystemRoot(candidate) {
|
|
831
|
+
return path.parse(candidate).root === candidate;
|
|
832
|
+
}
|
|
833
|
+
async function pathExists(targetPath) {
|
|
834
|
+
try {
|
|
835
|
+
await fs$1.lstat(targetPath);
|
|
836
|
+
return true;
|
|
837
|
+
} catch (error) {
|
|
838
|
+
if (isNotFoundPathError(error)) return false;
|
|
839
|
+
throw error;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
async function resolveSymlinkHopPath(symlinkPath) {
|
|
843
|
+
try {
|
|
844
|
+
return path.resolve(await fs$1.realpath(symlinkPath));
|
|
845
|
+
} catch (error) {
|
|
846
|
+
if (!isNotFoundPathError(error)) throw error;
|
|
847
|
+
const linkTarget = await fs$1.readlink(symlinkPath);
|
|
848
|
+
return resolvePathViaExistingAncestor(path.resolve(path.dirname(symlinkPath), linkTarget));
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
function resolveSymlinkHopPathSync(symlinkPath) {
|
|
852
|
+
try {
|
|
853
|
+
return path.resolve(fs.realpathSync(symlinkPath));
|
|
854
|
+
} catch (error) {
|
|
855
|
+
if (!isNotFoundPathError(error)) throw error;
|
|
856
|
+
const linkTarget = fs.readlinkSync(symlinkPath);
|
|
857
|
+
return resolvePathViaExistingAncestorSync(path.resolve(path.dirname(symlinkPath), linkTarget));
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
//#endregion
|
|
861
|
+
//#region src/infra/file-identity.ts
|
|
862
|
+
function isZero(value) {
|
|
863
|
+
return value === 0 || value === 0n;
|
|
864
|
+
}
|
|
865
|
+
function sameFileIdentity$1(left, right, platform = process.platform) {
|
|
866
|
+
if (left.ino !== right.ino) return false;
|
|
867
|
+
if (left.dev === right.dev) return true;
|
|
868
|
+
return platform === "win32" && (isZero(left.dev) || isZero(right.dev));
|
|
869
|
+
}
|
|
870
|
+
//#endregion
|
|
871
|
+
//#region src/infra/safe-open-sync.ts
|
|
872
|
+
function isExpectedPathError(error) {
|
|
873
|
+
const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
|
|
874
|
+
return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP";
|
|
875
|
+
}
|
|
876
|
+
function sameFileIdentity(left, right) {
|
|
877
|
+
return sameFileIdentity$1(left, right);
|
|
878
|
+
}
|
|
879
|
+
function openVerifiedFileSync(params) {
|
|
880
|
+
const ioFs = params.ioFs ?? fs;
|
|
881
|
+
const allowedType = params.allowedType ?? "file";
|
|
882
|
+
const openReadFlags = ioFs.constants.O_RDONLY | (typeof ioFs.constants.O_NOFOLLOW === "number" ? ioFs.constants.O_NOFOLLOW : 0);
|
|
883
|
+
let fd = null;
|
|
884
|
+
try {
|
|
885
|
+
if (params.rejectPathSymlink) {
|
|
886
|
+
if (ioFs.lstatSync(params.filePath).isSymbolicLink()) return {
|
|
887
|
+
ok: false,
|
|
888
|
+
reason: "validation"
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
const realPath = params.resolvedPath ?? ioFs.realpathSync(params.filePath);
|
|
892
|
+
const preOpenStat = ioFs.lstatSync(realPath);
|
|
893
|
+
if (!isAllowedType(preOpenStat, allowedType)) return {
|
|
894
|
+
ok: false,
|
|
895
|
+
reason: "validation"
|
|
896
|
+
};
|
|
897
|
+
if (params.rejectHardlinks && preOpenStat.isFile() && preOpenStat.nlink > 1) return {
|
|
898
|
+
ok: false,
|
|
899
|
+
reason: "validation"
|
|
900
|
+
};
|
|
901
|
+
if (params.maxBytes !== void 0 && preOpenStat.isFile() && preOpenStat.size > params.maxBytes) return {
|
|
902
|
+
ok: false,
|
|
903
|
+
reason: "validation"
|
|
904
|
+
};
|
|
905
|
+
fd = ioFs.openSync(realPath, openReadFlags);
|
|
906
|
+
const openedStat = ioFs.fstatSync(fd);
|
|
907
|
+
if (!isAllowedType(openedStat, allowedType)) return {
|
|
908
|
+
ok: false,
|
|
909
|
+
reason: "validation"
|
|
910
|
+
};
|
|
911
|
+
if (params.rejectHardlinks && openedStat.isFile() && openedStat.nlink > 1) return {
|
|
912
|
+
ok: false,
|
|
913
|
+
reason: "validation"
|
|
914
|
+
};
|
|
915
|
+
if (params.maxBytes !== void 0 && openedStat.isFile() && openedStat.size > params.maxBytes) return {
|
|
916
|
+
ok: false,
|
|
917
|
+
reason: "validation"
|
|
918
|
+
};
|
|
919
|
+
if (!sameFileIdentity(preOpenStat, openedStat)) return {
|
|
920
|
+
ok: false,
|
|
921
|
+
reason: "validation"
|
|
922
|
+
};
|
|
923
|
+
const opened = {
|
|
924
|
+
ok: true,
|
|
925
|
+
path: realPath,
|
|
926
|
+
fd,
|
|
927
|
+
stat: openedStat
|
|
928
|
+
};
|
|
929
|
+
fd = null;
|
|
930
|
+
return opened;
|
|
931
|
+
} catch (error) {
|
|
932
|
+
if (isExpectedPathError(error)) return {
|
|
933
|
+
ok: false,
|
|
934
|
+
reason: "path",
|
|
935
|
+
error
|
|
936
|
+
};
|
|
937
|
+
return {
|
|
938
|
+
ok: false,
|
|
939
|
+
reason: "io",
|
|
940
|
+
error
|
|
941
|
+
};
|
|
942
|
+
} finally {
|
|
943
|
+
if (fd !== null) ioFs.closeSync(fd);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
function isAllowedType(stat, allowedType) {
|
|
947
|
+
if (allowedType === "directory") return stat.isDirectory();
|
|
948
|
+
return stat.isFile();
|
|
949
|
+
}
|
|
950
|
+
//#endregion
|
|
951
|
+
//#region src/infra/boundary-file-read.ts
|
|
952
|
+
function canUseBoundaryFileOpen(ioFs) {
|
|
953
|
+
return typeof ioFs.openSync === "function" && typeof ioFs.closeSync === "function" && typeof ioFs.fstatSync === "function" && typeof ioFs.lstatSync === "function" && typeof ioFs.realpathSync === "function" && typeof ioFs.readFileSync === "function" && typeof ioFs.constants === "object" && ioFs.constants !== null;
|
|
954
|
+
}
|
|
955
|
+
function openBoundaryFileSync(params) {
|
|
956
|
+
const ioFs = params.ioFs ?? fs;
|
|
957
|
+
const resolved = resolveBoundaryFilePathGeneric({
|
|
958
|
+
absolutePath: params.absolutePath,
|
|
959
|
+
resolve: (absolutePath) => resolveBoundaryPathSync({
|
|
960
|
+
absolutePath,
|
|
961
|
+
rootPath: params.rootPath,
|
|
962
|
+
rootCanonicalPath: params.rootRealPath,
|
|
963
|
+
boundaryLabel: params.boundaryLabel,
|
|
964
|
+
skipLexicalRootCheck: params.skipLexicalRootCheck
|
|
965
|
+
})
|
|
966
|
+
});
|
|
967
|
+
if (resolved instanceof Promise) return toBoundaryValidationError(/* @__PURE__ */ new Error("Unexpected async boundary resolution"));
|
|
968
|
+
return finalizeBoundaryFileOpen({
|
|
969
|
+
resolved,
|
|
970
|
+
maxBytes: params.maxBytes,
|
|
971
|
+
rejectHardlinks: params.rejectHardlinks,
|
|
972
|
+
allowedType: params.allowedType,
|
|
973
|
+
ioFs
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
function openBoundaryFileResolved(params) {
|
|
977
|
+
const opened = openVerifiedFileSync({
|
|
978
|
+
filePath: params.absolutePath,
|
|
979
|
+
resolvedPath: params.resolvedPath,
|
|
980
|
+
rejectHardlinks: params.rejectHardlinks ?? true,
|
|
981
|
+
maxBytes: params.maxBytes,
|
|
982
|
+
allowedType: params.allowedType,
|
|
983
|
+
ioFs: params.ioFs
|
|
984
|
+
});
|
|
985
|
+
if (!opened.ok) return opened;
|
|
986
|
+
return {
|
|
987
|
+
ok: true,
|
|
988
|
+
path: opened.path,
|
|
989
|
+
fd: opened.fd,
|
|
990
|
+
stat: opened.stat,
|
|
991
|
+
rootRealPath: params.rootRealPath
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
function finalizeBoundaryFileOpen(params) {
|
|
995
|
+
if ("ok" in params.resolved) return params.resolved;
|
|
996
|
+
return openBoundaryFileResolved({
|
|
997
|
+
absolutePath: params.resolved.absolutePath,
|
|
998
|
+
resolvedPath: params.resolved.resolvedPath,
|
|
999
|
+
rootRealPath: params.resolved.rootRealPath,
|
|
1000
|
+
maxBytes: params.maxBytes,
|
|
1001
|
+
rejectHardlinks: params.rejectHardlinks,
|
|
1002
|
+
allowedType: params.allowedType,
|
|
1003
|
+
ioFs: params.ioFs
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
async function openBoundaryFile(params) {
|
|
1007
|
+
const ioFs = params.ioFs ?? fs;
|
|
1008
|
+
const maybeResolved = resolveBoundaryFilePathGeneric({
|
|
1009
|
+
absolutePath: params.absolutePath,
|
|
1010
|
+
resolve: (absolutePath) => resolveBoundaryPath({
|
|
1011
|
+
absolutePath,
|
|
1012
|
+
rootPath: params.rootPath,
|
|
1013
|
+
rootCanonicalPath: params.rootRealPath,
|
|
1014
|
+
boundaryLabel: params.boundaryLabel,
|
|
1015
|
+
policy: params.aliasPolicy,
|
|
1016
|
+
skipLexicalRootCheck: params.skipLexicalRootCheck
|
|
1017
|
+
})
|
|
1018
|
+
});
|
|
1019
|
+
return finalizeBoundaryFileOpen({
|
|
1020
|
+
resolved: maybeResolved instanceof Promise ? await maybeResolved : maybeResolved,
|
|
1021
|
+
maxBytes: params.maxBytes,
|
|
1022
|
+
rejectHardlinks: params.rejectHardlinks,
|
|
1023
|
+
allowedType: params.allowedType,
|
|
1024
|
+
ioFs
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
function toBoundaryValidationError(error) {
|
|
1028
|
+
return {
|
|
1029
|
+
ok: false,
|
|
1030
|
+
reason: "validation",
|
|
1031
|
+
error
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
function mapResolvedBoundaryPath(absolutePath, resolved) {
|
|
1035
|
+
return {
|
|
1036
|
+
absolutePath,
|
|
1037
|
+
resolvedPath: resolved.canonicalPath,
|
|
1038
|
+
rootRealPath: resolved.rootCanonicalPath
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
function resolveBoundaryFilePathGeneric(params) {
|
|
1042
|
+
const absolutePath = path.resolve(params.absolutePath);
|
|
1043
|
+
try {
|
|
1044
|
+
const resolved = params.resolve(absolutePath);
|
|
1045
|
+
if (resolved instanceof Promise) return resolved.then((value) => mapResolvedBoundaryPath(absolutePath, value)).catch((error) => toBoundaryValidationError(error));
|
|
1046
|
+
return mapResolvedBoundaryPath(absolutePath, resolved);
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
return toBoundaryValidationError(error);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
//#endregion
|
|
1052
|
+
//#region src/process/spawn-utils.ts
|
|
1053
|
+
const DEFAULT_RETRY_CODES = ["EBADF"];
|
|
1054
|
+
function resolveCommandStdio(params) {
|
|
1055
|
+
return [
|
|
1056
|
+
params.hasInput ? "pipe" : params.preferInherit ? "inherit" : "pipe",
|
|
1057
|
+
"pipe",
|
|
1058
|
+
"pipe"
|
|
1059
|
+
];
|
|
1060
|
+
}
|
|
1061
|
+
function shouldRetry(err, codes) {
|
|
1062
|
+
const code = err && typeof err === "object" && "code" in err ? String(err.code) : "";
|
|
1063
|
+
return code.length > 0 && codes.includes(code);
|
|
1064
|
+
}
|
|
1065
|
+
async function spawnAndWaitForSpawn(spawnImpl, argv, options) {
|
|
1066
|
+
const child = spawnImpl(argv[0], argv.slice(1), options);
|
|
1067
|
+
return await new Promise((resolve, reject) => {
|
|
1068
|
+
let settled = false;
|
|
1069
|
+
const cleanup = () => {
|
|
1070
|
+
child.removeListener("error", onError);
|
|
1071
|
+
child.removeListener("spawn", onSpawn);
|
|
1072
|
+
};
|
|
1073
|
+
const finishResolve = () => {
|
|
1074
|
+
if (settled) return;
|
|
1075
|
+
settled = true;
|
|
1076
|
+
cleanup();
|
|
1077
|
+
resolve(child);
|
|
1078
|
+
};
|
|
1079
|
+
const onError = (err) => {
|
|
1080
|
+
if (settled) return;
|
|
1081
|
+
settled = true;
|
|
1082
|
+
cleanup();
|
|
1083
|
+
reject(err);
|
|
1084
|
+
};
|
|
1085
|
+
const onSpawn = () => {
|
|
1086
|
+
finishResolve();
|
|
1087
|
+
};
|
|
1088
|
+
child.once("error", onError);
|
|
1089
|
+
child.once("spawn", onSpawn);
|
|
1090
|
+
process.nextTick(() => {
|
|
1091
|
+
if (typeof child.pid === "number") finishResolve();
|
|
1092
|
+
});
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
async function spawnWithFallback(params) {
|
|
1096
|
+
const spawnImpl = params.spawnImpl ?? spawn;
|
|
1097
|
+
const retryCodes = params.retryCodes ?? DEFAULT_RETRY_CODES;
|
|
1098
|
+
const baseOptions = { ...params.options };
|
|
1099
|
+
const fallbacks = params.fallbacks ?? [];
|
|
1100
|
+
const attempts = [{ options: baseOptions }, ...fallbacks.map((fallback) => ({
|
|
1101
|
+
label: fallback.label,
|
|
1102
|
+
options: {
|
|
1103
|
+
...baseOptions,
|
|
1104
|
+
...fallback.options
|
|
1105
|
+
}
|
|
1106
|
+
}))];
|
|
1107
|
+
let lastError;
|
|
1108
|
+
for (let index = 0; index < attempts.length; index += 1) {
|
|
1109
|
+
const attempt = attempts[index];
|
|
1110
|
+
try {
|
|
1111
|
+
return {
|
|
1112
|
+
child: await spawnAndWaitForSpawn(spawnImpl, params.argv, attempt.options),
|
|
1113
|
+
usedFallback: index > 0,
|
|
1114
|
+
fallbackLabel: attempt.label
|
|
1115
|
+
};
|
|
1116
|
+
} catch (err) {
|
|
1117
|
+
lastError = err;
|
|
1118
|
+
const nextFallback = fallbacks[index];
|
|
1119
|
+
if (!nextFallback || !shouldRetry(err, retryCodes)) throw err;
|
|
1120
|
+
params.onFallback?.(err, nextFallback);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
throw lastError;
|
|
1124
|
+
}
|
|
1125
|
+
//#endregion
|
|
1126
|
+
//#region src/process/exec.ts
|
|
1127
|
+
const execFileAsync = promisify(execFile);
|
|
1128
|
+
const WINDOWS_UNSAFE_CMD_CHARS_RE = /[&|<>^%\r\n]/;
|
|
1129
|
+
function isWindowsBatchCommand(resolvedCommand) {
|
|
1130
|
+
if (process$1.platform !== "win32") return false;
|
|
1131
|
+
const ext = path.extname(resolvedCommand).toLowerCase();
|
|
1132
|
+
return ext === ".cmd" || ext === ".bat";
|
|
1133
|
+
}
|
|
1134
|
+
function escapeForCmdExe(arg) {
|
|
1135
|
+
if (WINDOWS_UNSAFE_CMD_CHARS_RE.test(arg)) throw new Error(`Unsafe Windows cmd.exe argument detected: ${JSON.stringify(arg)}. Pass an explicit shell-wrapper argv at the call site instead.`);
|
|
1136
|
+
if (!arg.includes(" ") && !arg.includes("\"")) return arg;
|
|
1137
|
+
return `"${arg.replace(/"/g, "\"\"")}"`;
|
|
1138
|
+
}
|
|
1139
|
+
function buildCmdExeCommandLine(resolvedCommand, args) {
|
|
1140
|
+
return [escapeForCmdExe(resolvedCommand), ...args.map(escapeForCmdExe)].join(" ");
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* On Windows, Node 18.20.2+ (CVE-2024-27980) rejects spawning .cmd/.bat directly
|
|
1144
|
+
* without shell, causing EINVAL. Resolve npm/npx to node + cli script so we
|
|
1145
|
+
* spawn node.exe instead of npm.cmd.
|
|
1146
|
+
*/
|
|
1147
|
+
function resolveNpmArgvForWindows(argv) {
|
|
1148
|
+
if (process$1.platform !== "win32" || argv.length === 0) return null;
|
|
1149
|
+
const basename = path.basename(argv[0]).toLowerCase().replace(/\.(cmd|exe|bat)$/, "");
|
|
1150
|
+
const cliName = basename === "npx" ? "npx-cli.js" : basename === "npm" ? "npm-cli.js" : null;
|
|
1151
|
+
if (!cliName) return null;
|
|
1152
|
+
const nodeDir = path.dirname(process$1.execPath);
|
|
1153
|
+
const cliPath = path.join(nodeDir, "node_modules", "npm", "bin", cliName);
|
|
1154
|
+
if (!fs.existsSync(cliPath)) {
|
|
1155
|
+
const command = argv[0] ?? "";
|
|
1156
|
+
return [path.extname(command).toLowerCase() ? command : `${command}.cmd`, ...argv.slice(1)];
|
|
1157
|
+
}
|
|
1158
|
+
return [
|
|
1159
|
+
process$1.execPath,
|
|
1160
|
+
cliPath,
|
|
1161
|
+
...argv.slice(1)
|
|
1162
|
+
];
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Resolves a command for Windows compatibility.
|
|
1166
|
+
* On Windows, non-.exe commands (like pnpm, yarn) are resolved to .cmd; npm/npx
|
|
1167
|
+
* are handled by resolveNpmArgvForWindows to avoid spawn EINVAL (no direct .cmd).
|
|
1168
|
+
*/
|
|
1169
|
+
function resolveCommand(command) {
|
|
1170
|
+
if (process$1.platform !== "win32") return command;
|
|
1171
|
+
const basename = path.basename(command).toLowerCase();
|
|
1172
|
+
if (path.extname(basename)) return command;
|
|
1173
|
+
if (["pnpm", "yarn"].includes(basename)) return `${command}.cmd`;
|
|
1174
|
+
return command;
|
|
1175
|
+
}
|
|
1176
|
+
function shouldSpawnWithShell(params) {
|
|
1177
|
+
return false;
|
|
1178
|
+
}
|
|
1179
|
+
async function runExec(command, args, opts = 1e4) {
|
|
1180
|
+
const options = typeof opts === "number" ? {
|
|
1181
|
+
timeout: opts,
|
|
1182
|
+
encoding: "utf8"
|
|
1183
|
+
} : {
|
|
1184
|
+
timeout: opts.timeoutMs,
|
|
1185
|
+
maxBuffer: opts.maxBuffer,
|
|
1186
|
+
cwd: opts.cwd,
|
|
1187
|
+
encoding: "utf8"
|
|
1188
|
+
};
|
|
1189
|
+
try {
|
|
1190
|
+
const argv = [command, ...args];
|
|
1191
|
+
let execCommand;
|
|
1192
|
+
let execArgs;
|
|
1193
|
+
if (process$1.platform === "win32") {
|
|
1194
|
+
const resolved = resolveNpmArgvForWindows(argv);
|
|
1195
|
+
if (resolved) {
|
|
1196
|
+
execCommand = resolved[0] ?? "";
|
|
1197
|
+
execArgs = resolved.slice(1);
|
|
1198
|
+
} else {
|
|
1199
|
+
execCommand = resolveCommand(command);
|
|
1200
|
+
execArgs = args;
|
|
1201
|
+
}
|
|
1202
|
+
} else {
|
|
1203
|
+
execCommand = resolveCommand(command);
|
|
1204
|
+
execArgs = args;
|
|
1205
|
+
}
|
|
1206
|
+
const { stdout, stderr } = isWindowsBatchCommand(execCommand) ? await execFileAsync(process$1.env.ComSpec ?? "cmd.exe", [
|
|
1207
|
+
"/d",
|
|
1208
|
+
"/s",
|
|
1209
|
+
"/c",
|
|
1210
|
+
buildCmdExeCommandLine(execCommand, execArgs)
|
|
1211
|
+
], {
|
|
1212
|
+
...options,
|
|
1213
|
+
windowsVerbatimArguments: true
|
|
1214
|
+
}) : await execFileAsync(execCommand, execArgs, options);
|
|
1215
|
+
if (shouldLogVerbose()) {
|
|
1216
|
+
if (stdout.trim()) logDebug(stdout.trim());
|
|
1217
|
+
if (stderr.trim()) logError(stderr.trim());
|
|
1218
|
+
}
|
|
1219
|
+
return {
|
|
1220
|
+
stdout,
|
|
1221
|
+
stderr
|
|
1222
|
+
};
|
|
1223
|
+
} catch (err) {
|
|
1224
|
+
if (shouldLogVerbose()) logError(danger(`Command failed: ${command} ${args.join(" ")}`));
|
|
1225
|
+
throw err;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
function resolveCommandEnv(params) {
|
|
1229
|
+
const baseEnv = params.baseEnv ?? process$1.env;
|
|
1230
|
+
const argv = params.argv;
|
|
1231
|
+
const shouldSuppressNpmFund = (() => {
|
|
1232
|
+
const cmd = path.basename(argv[0] ?? "");
|
|
1233
|
+
if (cmd === "npm" || cmd === "npm.cmd" || cmd === "npm.exe") return true;
|
|
1234
|
+
if (cmd === "node" || cmd === "node.exe") return (argv[1] ?? "").includes("npm-cli.js");
|
|
1235
|
+
return false;
|
|
1236
|
+
})();
|
|
1237
|
+
const mergedEnv = params.env ? {
|
|
1238
|
+
...baseEnv,
|
|
1239
|
+
...params.env
|
|
1240
|
+
} : { ...baseEnv };
|
|
1241
|
+
const resolvedEnv = Object.fromEntries(Object.entries(mergedEnv).filter(([, value]) => value !== void 0).map(([key, value]) => [key, String(value)]));
|
|
1242
|
+
if (shouldSuppressNpmFund) {
|
|
1243
|
+
if (resolvedEnv.NPM_CONFIG_FUND == null) resolvedEnv.NPM_CONFIG_FUND = "false";
|
|
1244
|
+
if (resolvedEnv.npm_config_fund == null) resolvedEnv.npm_config_fund = "false";
|
|
1245
|
+
}
|
|
1246
|
+
return markOpenClawExecEnv(resolvedEnv);
|
|
1247
|
+
}
|
|
1248
|
+
async function runCommandWithTimeout(argv, optionsOrTimeout) {
|
|
1249
|
+
const options = typeof optionsOrTimeout === "number" ? { timeoutMs: optionsOrTimeout } : optionsOrTimeout;
|
|
1250
|
+
const { timeoutMs, cwd, input, env, noOutputTimeoutMs } = options;
|
|
1251
|
+
const { windowsVerbatimArguments } = options;
|
|
1252
|
+
const hasInput = input !== void 0;
|
|
1253
|
+
const resolvedEnv = resolveCommandEnv({
|
|
1254
|
+
argv,
|
|
1255
|
+
env
|
|
1256
|
+
});
|
|
1257
|
+
const stdio = resolveCommandStdio({
|
|
1258
|
+
hasInput,
|
|
1259
|
+
preferInherit: true
|
|
1260
|
+
});
|
|
1261
|
+
const finalArgv = process$1.platform === "win32" ? resolveNpmArgvForWindows(argv) ?? argv : argv;
|
|
1262
|
+
const resolvedCommand = finalArgv !== argv ? finalArgv[0] ?? "" : resolveCommand(argv[0] ?? "");
|
|
1263
|
+
const useCmdWrapper = isWindowsBatchCommand(resolvedCommand);
|
|
1264
|
+
const child = spawn(useCmdWrapper ? process$1.env.ComSpec ?? "cmd.exe" : resolvedCommand, useCmdWrapper ? [
|
|
1265
|
+
"/d",
|
|
1266
|
+
"/s",
|
|
1267
|
+
"/c",
|
|
1268
|
+
buildCmdExeCommandLine(resolvedCommand, finalArgv.slice(1))
|
|
1269
|
+
] : finalArgv.slice(1), {
|
|
1270
|
+
stdio,
|
|
1271
|
+
cwd,
|
|
1272
|
+
env: resolvedEnv,
|
|
1273
|
+
windowsVerbatimArguments: useCmdWrapper ? true : windowsVerbatimArguments,
|
|
1274
|
+
...shouldSpawnWithShell({
|
|
1275
|
+
resolvedCommand,
|
|
1276
|
+
platform: process$1.platform
|
|
1277
|
+
}) ? { shell: true } : {}
|
|
1278
|
+
});
|
|
1279
|
+
return await new Promise((resolve, reject) => {
|
|
1280
|
+
let stdout = "";
|
|
1281
|
+
let stderr = "";
|
|
1282
|
+
let settled = false;
|
|
1283
|
+
let timedOut = false;
|
|
1284
|
+
let noOutputTimedOut = false;
|
|
1285
|
+
let noOutputTimer = null;
|
|
1286
|
+
const shouldTrackOutputTimeout = typeof noOutputTimeoutMs === "number" && Number.isFinite(noOutputTimeoutMs) && noOutputTimeoutMs > 0;
|
|
1287
|
+
const clearNoOutputTimer = () => {
|
|
1288
|
+
if (!noOutputTimer) return;
|
|
1289
|
+
clearTimeout(noOutputTimer);
|
|
1290
|
+
noOutputTimer = null;
|
|
1291
|
+
};
|
|
1292
|
+
const armNoOutputTimer = () => {
|
|
1293
|
+
if (!shouldTrackOutputTimeout || settled) return;
|
|
1294
|
+
clearNoOutputTimer();
|
|
1295
|
+
noOutputTimer = setTimeout(() => {
|
|
1296
|
+
if (settled) return;
|
|
1297
|
+
noOutputTimedOut = true;
|
|
1298
|
+
if (typeof child.kill === "function") child.kill("SIGKILL");
|
|
1299
|
+
}, Math.floor(noOutputTimeoutMs));
|
|
1300
|
+
};
|
|
1301
|
+
const timer = setTimeout(() => {
|
|
1302
|
+
timedOut = true;
|
|
1303
|
+
if (typeof child.kill === "function") child.kill("SIGKILL");
|
|
1304
|
+
}, timeoutMs);
|
|
1305
|
+
armNoOutputTimer();
|
|
1306
|
+
if (hasInput && child.stdin) {
|
|
1307
|
+
child.stdin.write(input ?? "");
|
|
1308
|
+
child.stdin.end();
|
|
1309
|
+
}
|
|
1310
|
+
child.stdout?.on("data", (d) => {
|
|
1311
|
+
stdout += d.toString();
|
|
1312
|
+
armNoOutputTimer();
|
|
1313
|
+
});
|
|
1314
|
+
child.stderr?.on("data", (d) => {
|
|
1315
|
+
stderr += d.toString();
|
|
1316
|
+
armNoOutputTimer();
|
|
1317
|
+
});
|
|
1318
|
+
child.on("error", (err) => {
|
|
1319
|
+
if (settled) return;
|
|
1320
|
+
settled = true;
|
|
1321
|
+
clearTimeout(timer);
|
|
1322
|
+
clearNoOutputTimer();
|
|
1323
|
+
reject(err);
|
|
1324
|
+
});
|
|
1325
|
+
child.on("close", (code, signal) => {
|
|
1326
|
+
if (settled) return;
|
|
1327
|
+
settled = true;
|
|
1328
|
+
clearTimeout(timer);
|
|
1329
|
+
clearNoOutputTimer();
|
|
1330
|
+
const termination = noOutputTimedOut ? "no-output-timeout" : timedOut ? "timeout" : signal != null ? "signal" : "exit";
|
|
1331
|
+
resolve({
|
|
1332
|
+
pid: child.pid ?? void 0,
|
|
1333
|
+
stdout,
|
|
1334
|
+
stderr,
|
|
1335
|
+
code,
|
|
1336
|
+
signal,
|
|
1337
|
+
killed: child.killed,
|
|
1338
|
+
termination,
|
|
1339
|
+
noOutputTimedOut
|
|
1340
|
+
});
|
|
1341
|
+
});
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
//#endregion
|
|
1345
|
+
//#region src/infra/openclaw-root.ts
|
|
1346
|
+
const CORE_PACKAGE_NAMES = new Set(["openclaw"]);
|
|
1347
|
+
async function readPackageName(dir) {
|
|
1348
|
+
try {
|
|
1349
|
+
const raw = await fs$1.readFile(path.join(dir, "package.json"), "utf-8");
|
|
1350
|
+
const parsed = JSON.parse(raw);
|
|
1351
|
+
return typeof parsed.name === "string" ? parsed.name : null;
|
|
1352
|
+
} catch {
|
|
1353
|
+
return null;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
function readPackageNameSync(dir) {
|
|
1357
|
+
try {
|
|
1358
|
+
const raw = fs.readFileSync(path.join(dir, "package.json"), "utf-8");
|
|
1359
|
+
const parsed = JSON.parse(raw);
|
|
1360
|
+
return typeof parsed.name === "string" ? parsed.name : null;
|
|
1361
|
+
} catch {
|
|
1362
|
+
return null;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
async function findPackageRoot(startDir, maxDepth = 12) {
|
|
1366
|
+
for (const current of iterAncestorDirs(startDir, maxDepth)) {
|
|
1367
|
+
const name = await readPackageName(current);
|
|
1368
|
+
if (name && CORE_PACKAGE_NAMES.has(name)) return current;
|
|
1369
|
+
}
|
|
1370
|
+
return null;
|
|
1371
|
+
}
|
|
1372
|
+
function findPackageRootSync(startDir, maxDepth = 12) {
|
|
1373
|
+
for (const current of iterAncestorDirs(startDir, maxDepth)) {
|
|
1374
|
+
const name = readPackageNameSync(current);
|
|
1375
|
+
if (name && CORE_PACKAGE_NAMES.has(name)) return current;
|
|
1376
|
+
}
|
|
1377
|
+
return null;
|
|
1378
|
+
}
|
|
1379
|
+
function* iterAncestorDirs(startDir, maxDepth) {
|
|
1380
|
+
let current = path.resolve(startDir);
|
|
1381
|
+
for (let i = 0; i < maxDepth; i += 1) {
|
|
1382
|
+
yield current;
|
|
1383
|
+
const parent = path.dirname(current);
|
|
1384
|
+
if (parent === current) break;
|
|
1385
|
+
current = parent;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
function candidateDirsFromArgv1(argv1) {
|
|
1389
|
+
const normalized = path.resolve(argv1);
|
|
1390
|
+
const candidates = [path.dirname(normalized)];
|
|
1391
|
+
try {
|
|
1392
|
+
const resolved = fs.realpathSync(normalized);
|
|
1393
|
+
if (resolved !== normalized) candidates.push(path.dirname(resolved));
|
|
1394
|
+
} catch {}
|
|
1395
|
+
const parts = normalized.split(path.sep);
|
|
1396
|
+
const binIndex = parts.lastIndexOf(".bin");
|
|
1397
|
+
if (binIndex > 0 && parts[binIndex - 1] === "node_modules") {
|
|
1398
|
+
const binName = path.basename(normalized);
|
|
1399
|
+
const nodeModulesDir = parts.slice(0, binIndex).join(path.sep);
|
|
1400
|
+
candidates.push(path.join(nodeModulesDir, binName));
|
|
1401
|
+
}
|
|
1402
|
+
return candidates;
|
|
1403
|
+
}
|
|
1404
|
+
async function resolveOpenClawPackageRoot(opts) {
|
|
1405
|
+
for (const candidate of buildCandidates(opts)) {
|
|
1406
|
+
const found = await findPackageRoot(candidate);
|
|
1407
|
+
if (found) return found;
|
|
1408
|
+
}
|
|
1409
|
+
return null;
|
|
1410
|
+
}
|
|
1411
|
+
function resolveOpenClawPackageRootSync(opts) {
|
|
1412
|
+
for (const candidate of buildCandidates(opts)) {
|
|
1413
|
+
const found = findPackageRootSync(candidate);
|
|
1414
|
+
if (found) return found;
|
|
1415
|
+
}
|
|
1416
|
+
return null;
|
|
1417
|
+
}
|
|
1418
|
+
function buildCandidates(opts) {
|
|
1419
|
+
const candidates = [];
|
|
1420
|
+
if (opts.moduleUrl) try {
|
|
1421
|
+
candidates.push(path.dirname(fileURLToPath(opts.moduleUrl)));
|
|
1422
|
+
} catch {}
|
|
1423
|
+
if (opts.argv1) candidates.push(...candidateDirsFromArgv1(opts.argv1));
|
|
1424
|
+
if (opts.cwd) candidates.push(opts.cwd);
|
|
1425
|
+
return candidates;
|
|
1426
|
+
}
|
|
1427
|
+
//#endregion
|
|
1428
|
+
//#region src/agents/workspace-templates.ts
|
|
1429
|
+
const FALLBACK_TEMPLATE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../docs/reference/templates");
|
|
1430
|
+
let cachedTemplateDir;
|
|
1431
|
+
let resolvingTemplateDir;
|
|
1432
|
+
async function resolveWorkspaceTemplateDir(opts) {
|
|
1433
|
+
if (cachedTemplateDir) return cachedTemplateDir;
|
|
1434
|
+
if (resolvingTemplateDir) return resolvingTemplateDir;
|
|
1435
|
+
resolvingTemplateDir = (async () => {
|
|
1436
|
+
const moduleUrl = opts?.moduleUrl ?? import.meta.url;
|
|
1437
|
+
const argv1 = opts?.argv1 ?? process.argv[1];
|
|
1438
|
+
const cwd = opts?.cwd ?? process.cwd();
|
|
1439
|
+
const packageRoot = await resolveOpenClawPackageRoot({
|
|
1440
|
+
moduleUrl,
|
|
1441
|
+
argv1,
|
|
1442
|
+
cwd
|
|
1443
|
+
});
|
|
1444
|
+
const candidates = [
|
|
1445
|
+
packageRoot ? path.join(packageRoot, "docs", "reference", "templates") : null,
|
|
1446
|
+
cwd ? path.resolve(cwd, "docs", "reference", "templates") : null,
|
|
1447
|
+
FALLBACK_TEMPLATE_DIR
|
|
1448
|
+
].filter(Boolean);
|
|
1449
|
+
for (const candidate of candidates) if (await pathExists$1(candidate)) {
|
|
1450
|
+
cachedTemplateDir = candidate;
|
|
1451
|
+
return candidate;
|
|
1452
|
+
}
|
|
1453
|
+
cachedTemplateDir = candidates[0] ?? FALLBACK_TEMPLATE_DIR;
|
|
1454
|
+
return cachedTemplateDir;
|
|
1455
|
+
})();
|
|
1456
|
+
try {
|
|
1457
|
+
return await resolvingTemplateDir;
|
|
1458
|
+
} finally {
|
|
1459
|
+
resolvingTemplateDir = void 0;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
//#endregion
|
|
1463
|
+
//#region src/agents/workspace.ts
|
|
1464
|
+
function resolveDefaultAgentWorkspaceDir(env = process.env, homedir = os.homedir) {
|
|
1465
|
+
const home = resolveRequiredHomeDir(env, homedir);
|
|
1466
|
+
const profile = env.OPENCLAW_PROFILE?.trim();
|
|
1467
|
+
if (profile && profile.toLowerCase() !== "default") return path.join(home, ".openclaw", `workspace-${profile}`);
|
|
1468
|
+
return path.join(home, ".openclaw", "workspace");
|
|
1469
|
+
}
|
|
1470
|
+
const DEFAULT_AGENT_WORKSPACE_DIR = resolveDefaultAgentWorkspaceDir();
|
|
1471
|
+
const DEFAULT_AGENTS_FILENAME = "AGENTS.md";
|
|
1472
|
+
const DEFAULT_SOUL_FILENAME = "SOUL.md";
|
|
1473
|
+
const DEFAULT_TOOLS_FILENAME = "TOOLS.md";
|
|
1474
|
+
const DEFAULT_IDENTITY_FILENAME = "IDENTITY.md";
|
|
1475
|
+
const DEFAULT_USER_FILENAME = "USER.md";
|
|
1476
|
+
const DEFAULT_HEARTBEAT_FILENAME = "HEARTBEAT.md";
|
|
1477
|
+
const DEFAULT_BOOTSTRAP_FILENAME = "BOOTSTRAP.md";
|
|
1478
|
+
const DEFAULT_MEMORY_FILENAME = "MEMORY.md";
|
|
1479
|
+
const DEFAULT_MEMORY_ALT_FILENAME = "memory.md";
|
|
1480
|
+
const WORKSPACE_STATE_DIRNAME = ".openclaw";
|
|
1481
|
+
const WORKSPACE_STATE_FILENAME = "workspace-state.json";
|
|
1482
|
+
const WORKSPACE_STATE_VERSION = 1;
|
|
1483
|
+
const workspaceTemplateCache = /* @__PURE__ */ new Map();
|
|
1484
|
+
let gitAvailabilityPromise = null;
|
|
1485
|
+
const MAX_WORKSPACE_BOOTSTRAP_FILE_BYTES = 2 * 1024 * 1024;
|
|
1486
|
+
const workspaceFileCache = /* @__PURE__ */ new Map();
|
|
1487
|
+
function workspaceFileIdentity(stat, canonicalPath) {
|
|
1488
|
+
return `${canonicalPath}|${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}`;
|
|
1489
|
+
}
|
|
1490
|
+
async function readWorkspaceFileWithGuards(params) {
|
|
1491
|
+
const opened = await openBoundaryFile({
|
|
1492
|
+
absolutePath: params.filePath,
|
|
1493
|
+
rootPath: params.workspaceDir,
|
|
1494
|
+
boundaryLabel: "workspace root",
|
|
1495
|
+
maxBytes: MAX_WORKSPACE_BOOTSTRAP_FILE_BYTES
|
|
1496
|
+
});
|
|
1497
|
+
if (!opened.ok) {
|
|
1498
|
+
workspaceFileCache.delete(params.filePath);
|
|
1499
|
+
return opened;
|
|
1500
|
+
}
|
|
1501
|
+
const identity = workspaceFileIdentity(opened.stat, opened.path);
|
|
1502
|
+
const cached = workspaceFileCache.get(params.filePath);
|
|
1503
|
+
if (cached && cached.identity === identity) {
|
|
1504
|
+
fs.closeSync(opened.fd);
|
|
1505
|
+
return {
|
|
1506
|
+
ok: true,
|
|
1507
|
+
content: cached.content
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
try {
|
|
1511
|
+
const content = fs.readFileSync(opened.fd, "utf-8");
|
|
1512
|
+
workspaceFileCache.set(params.filePath, {
|
|
1513
|
+
content,
|
|
1514
|
+
identity
|
|
1515
|
+
});
|
|
1516
|
+
return {
|
|
1517
|
+
ok: true,
|
|
1518
|
+
content
|
|
1519
|
+
};
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
workspaceFileCache.delete(params.filePath);
|
|
1522
|
+
return {
|
|
1523
|
+
ok: false,
|
|
1524
|
+
reason: "io",
|
|
1525
|
+
error
|
|
1526
|
+
};
|
|
1527
|
+
} finally {
|
|
1528
|
+
fs.closeSync(opened.fd);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
function stripFrontMatter(content) {
|
|
1532
|
+
if (!content.startsWith("---")) return content;
|
|
1533
|
+
const endIndex = content.indexOf("\n---", 3);
|
|
1534
|
+
if (endIndex === -1) return content;
|
|
1535
|
+
const start = endIndex + 4;
|
|
1536
|
+
let trimmed = content.slice(start);
|
|
1537
|
+
trimmed = trimmed.replace(/^\s+/, "");
|
|
1538
|
+
return trimmed;
|
|
1539
|
+
}
|
|
1540
|
+
async function loadTemplate(name) {
|
|
1541
|
+
const cached = workspaceTemplateCache.get(name);
|
|
1542
|
+
if (cached) return cached;
|
|
1543
|
+
const pending = (async () => {
|
|
1544
|
+
const templateDir = await resolveWorkspaceTemplateDir();
|
|
1545
|
+
const templatePath = path.join(templateDir, name);
|
|
1546
|
+
try {
|
|
1547
|
+
return stripFrontMatter(await fs$1.readFile(templatePath, "utf-8"));
|
|
1548
|
+
} catch {
|
|
1549
|
+
throw new Error(`Missing workspace template: ${name} (${templatePath}). Ensure docs/reference/templates are packaged.`);
|
|
1550
|
+
}
|
|
1551
|
+
})();
|
|
1552
|
+
workspaceTemplateCache.set(name, pending);
|
|
1553
|
+
try {
|
|
1554
|
+
return await pending;
|
|
1555
|
+
} catch (error) {
|
|
1556
|
+
workspaceTemplateCache.delete(name);
|
|
1557
|
+
throw error;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
async function writeFileIfMissing(filePath, content) {
|
|
1561
|
+
try {
|
|
1562
|
+
await fs$1.writeFile(filePath, content, {
|
|
1563
|
+
encoding: "utf-8",
|
|
1564
|
+
flag: "wx"
|
|
1565
|
+
});
|
|
1566
|
+
return true;
|
|
1567
|
+
} catch (err) {
|
|
1568
|
+
if (err.code !== "EEXIST") throw err;
|
|
1569
|
+
return false;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
async function fileExists(filePath) {
|
|
1573
|
+
try {
|
|
1574
|
+
await fs$1.access(filePath);
|
|
1575
|
+
return true;
|
|
1576
|
+
} catch {
|
|
1577
|
+
return false;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
function resolveWorkspaceStatePath(dir) {
|
|
1581
|
+
return path.join(dir, WORKSPACE_STATE_DIRNAME, WORKSPACE_STATE_FILENAME);
|
|
1582
|
+
}
|
|
1583
|
+
function parseWorkspaceOnboardingState(raw) {
|
|
1584
|
+
try {
|
|
1585
|
+
const parsed = JSON.parse(raw);
|
|
1586
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
1587
|
+
return {
|
|
1588
|
+
version: WORKSPACE_STATE_VERSION,
|
|
1589
|
+
bootstrapSeededAt: typeof parsed.bootstrapSeededAt === "string" ? parsed.bootstrapSeededAt : void 0,
|
|
1590
|
+
onboardingCompletedAt: typeof parsed.onboardingCompletedAt === "string" ? parsed.onboardingCompletedAt : void 0
|
|
1591
|
+
};
|
|
1592
|
+
} catch {
|
|
1593
|
+
return null;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
async function readWorkspaceOnboardingState(statePath) {
|
|
1597
|
+
try {
|
|
1598
|
+
return parseWorkspaceOnboardingState(await fs$1.readFile(statePath, "utf-8")) ?? { version: WORKSPACE_STATE_VERSION };
|
|
1599
|
+
} catch (err) {
|
|
1600
|
+
if (err.code !== "ENOENT") throw err;
|
|
1601
|
+
return { version: WORKSPACE_STATE_VERSION };
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
async function writeWorkspaceOnboardingState(statePath, state) {
|
|
1605
|
+
await fs$1.mkdir(path.dirname(statePath), { recursive: true });
|
|
1606
|
+
const payload = `${JSON.stringify(state, null, 2)}\n`;
|
|
1607
|
+
const tmpPath = `${statePath}.tmp-${process.pid}-${Date.now().toString(36)}`;
|
|
1608
|
+
try {
|
|
1609
|
+
await fs$1.writeFile(tmpPath, payload, { encoding: "utf-8" });
|
|
1610
|
+
await fs$1.rename(tmpPath, statePath);
|
|
1611
|
+
} catch (err) {
|
|
1612
|
+
await fs$1.unlink(tmpPath).catch(() => {});
|
|
1613
|
+
throw err;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
async function hasGitRepo(dir) {
|
|
1617
|
+
try {
|
|
1618
|
+
await fs$1.stat(path.join(dir, ".git"));
|
|
1619
|
+
return true;
|
|
1620
|
+
} catch {
|
|
1621
|
+
return false;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
async function isGitAvailable() {
|
|
1625
|
+
if (gitAvailabilityPromise) return gitAvailabilityPromise;
|
|
1626
|
+
gitAvailabilityPromise = (async () => {
|
|
1627
|
+
try {
|
|
1628
|
+
return (await runCommandWithTimeout(["git", "--version"], { timeoutMs: 2e3 })).code === 0;
|
|
1629
|
+
} catch {
|
|
1630
|
+
return false;
|
|
1631
|
+
}
|
|
1632
|
+
})();
|
|
1633
|
+
return gitAvailabilityPromise;
|
|
1634
|
+
}
|
|
1635
|
+
async function ensureGitRepo(dir, isBrandNewWorkspace) {
|
|
1636
|
+
if (!isBrandNewWorkspace) return;
|
|
1637
|
+
if (await hasGitRepo(dir)) return;
|
|
1638
|
+
if (!await isGitAvailable()) return;
|
|
1639
|
+
try {
|
|
1640
|
+
await runCommandWithTimeout(["git", "init"], {
|
|
1641
|
+
cwd: dir,
|
|
1642
|
+
timeoutMs: 1e4
|
|
1643
|
+
});
|
|
1644
|
+
} catch {}
|
|
1645
|
+
}
|
|
1646
|
+
async function ensureAgentWorkspace(params) {
|
|
1647
|
+
const dir = resolveUserPath(params?.dir?.trim() ? params.dir.trim() : DEFAULT_AGENT_WORKSPACE_DIR);
|
|
1648
|
+
await fs$1.mkdir(dir, { recursive: true });
|
|
1649
|
+
if (!params?.ensureBootstrapFiles) return { dir };
|
|
1650
|
+
const agentsPath = path.join(dir, DEFAULT_AGENTS_FILENAME);
|
|
1651
|
+
const soulPath = path.join(dir, DEFAULT_SOUL_FILENAME);
|
|
1652
|
+
const toolsPath = path.join(dir, DEFAULT_TOOLS_FILENAME);
|
|
1653
|
+
const identityPath = path.join(dir, DEFAULT_IDENTITY_FILENAME);
|
|
1654
|
+
const userPath = path.join(dir, DEFAULT_USER_FILENAME);
|
|
1655
|
+
const heartbeatPath = path.join(dir, DEFAULT_HEARTBEAT_FILENAME);
|
|
1656
|
+
const bootstrapPath = path.join(dir, DEFAULT_BOOTSTRAP_FILENAME);
|
|
1657
|
+
const statePath = resolveWorkspaceStatePath(dir);
|
|
1658
|
+
const isBrandNewWorkspace = await (async () => {
|
|
1659
|
+
const templatePaths = [
|
|
1660
|
+
agentsPath,
|
|
1661
|
+
soulPath,
|
|
1662
|
+
toolsPath,
|
|
1663
|
+
identityPath,
|
|
1664
|
+
userPath,
|
|
1665
|
+
heartbeatPath
|
|
1666
|
+
];
|
|
1667
|
+
const userContentPaths = [
|
|
1668
|
+
path.join(dir, "memory"),
|
|
1669
|
+
path.join(dir, DEFAULT_MEMORY_FILENAME),
|
|
1670
|
+
path.join(dir, ".git")
|
|
1671
|
+
];
|
|
1672
|
+
const paths = [...templatePaths, ...userContentPaths];
|
|
1673
|
+
return (await Promise.all(paths.map(async (p) => {
|
|
1674
|
+
try {
|
|
1675
|
+
await fs$1.access(p);
|
|
1676
|
+
return true;
|
|
1677
|
+
} catch {
|
|
1678
|
+
return false;
|
|
1679
|
+
}
|
|
1680
|
+
}))).every((v) => !v);
|
|
1681
|
+
})();
|
|
1682
|
+
const agentsTemplate = await loadTemplate(DEFAULT_AGENTS_FILENAME);
|
|
1683
|
+
const soulTemplate = await loadTemplate(DEFAULT_SOUL_FILENAME);
|
|
1684
|
+
const toolsTemplate = await loadTemplate(DEFAULT_TOOLS_FILENAME);
|
|
1685
|
+
const identityTemplate = await loadTemplate(DEFAULT_IDENTITY_FILENAME);
|
|
1686
|
+
const userTemplate = await loadTemplate(DEFAULT_USER_FILENAME);
|
|
1687
|
+
const heartbeatTemplate = await loadTemplate(DEFAULT_HEARTBEAT_FILENAME);
|
|
1688
|
+
await writeFileIfMissing(agentsPath, agentsTemplate);
|
|
1689
|
+
await writeFileIfMissing(soulPath, soulTemplate);
|
|
1690
|
+
await writeFileIfMissing(toolsPath, toolsTemplate);
|
|
1691
|
+
await writeFileIfMissing(identityPath, identityTemplate);
|
|
1692
|
+
await writeFileIfMissing(userPath, userTemplate);
|
|
1693
|
+
await writeFileIfMissing(heartbeatPath, heartbeatTemplate);
|
|
1694
|
+
let state = await readWorkspaceOnboardingState(statePath);
|
|
1695
|
+
let stateDirty = false;
|
|
1696
|
+
const markState = (next) => {
|
|
1697
|
+
state = {
|
|
1698
|
+
...state,
|
|
1699
|
+
...next
|
|
1700
|
+
};
|
|
1701
|
+
stateDirty = true;
|
|
1702
|
+
};
|
|
1703
|
+
const nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
1704
|
+
let bootstrapExists = await fileExists(bootstrapPath);
|
|
1705
|
+
if (!state.bootstrapSeededAt && bootstrapExists) markState({ bootstrapSeededAt: nowIso() });
|
|
1706
|
+
if (!state.onboardingCompletedAt && state.bootstrapSeededAt && !bootstrapExists) markState({ onboardingCompletedAt: nowIso() });
|
|
1707
|
+
if (!state.bootstrapSeededAt && !state.onboardingCompletedAt && !bootstrapExists) {
|
|
1708
|
+
const [identityContent, userContent] = await Promise.all([fs$1.readFile(identityPath, "utf-8"), fs$1.readFile(userPath, "utf-8")]);
|
|
1709
|
+
const hasUserContent = await (async () => {
|
|
1710
|
+
const indicators = [
|
|
1711
|
+
path.join(dir, "memory"),
|
|
1712
|
+
path.join(dir, DEFAULT_MEMORY_FILENAME),
|
|
1713
|
+
path.join(dir, ".git")
|
|
1714
|
+
];
|
|
1715
|
+
for (const indicator of indicators) try {
|
|
1716
|
+
await fs$1.access(indicator);
|
|
1717
|
+
return true;
|
|
1718
|
+
} catch {}
|
|
1719
|
+
return false;
|
|
1720
|
+
})();
|
|
1721
|
+
if (identityContent !== identityTemplate || userContent !== userTemplate || hasUserContent) markState({ onboardingCompletedAt: nowIso() });
|
|
1722
|
+
else {
|
|
1723
|
+
if (!await writeFileIfMissing(bootstrapPath, await loadTemplate("BOOTSTRAP.md"))) bootstrapExists = await fileExists(bootstrapPath);
|
|
1724
|
+
else bootstrapExists = true;
|
|
1725
|
+
if (bootstrapExists && !state.bootstrapSeededAt) markState({ bootstrapSeededAt: nowIso() });
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
if (stateDirty) await writeWorkspaceOnboardingState(statePath, state);
|
|
1729
|
+
await ensureGitRepo(dir, isBrandNewWorkspace);
|
|
1730
|
+
return {
|
|
1731
|
+
dir,
|
|
1732
|
+
agentsPath,
|
|
1733
|
+
soulPath,
|
|
1734
|
+
toolsPath,
|
|
1735
|
+
identityPath,
|
|
1736
|
+
userPath,
|
|
1737
|
+
heartbeatPath,
|
|
1738
|
+
bootstrapPath
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
async function resolveMemoryBootstrapEntries(resolvedDir) {
|
|
1742
|
+
const candidates = [DEFAULT_MEMORY_FILENAME, DEFAULT_MEMORY_ALT_FILENAME];
|
|
1743
|
+
const entries = [];
|
|
1744
|
+
for (const name of candidates) {
|
|
1745
|
+
const filePath = path.join(resolvedDir, name);
|
|
1746
|
+
try {
|
|
1747
|
+
await fs$1.access(filePath);
|
|
1748
|
+
entries.push({
|
|
1749
|
+
name,
|
|
1750
|
+
filePath
|
|
1751
|
+
});
|
|
1752
|
+
} catch {}
|
|
1753
|
+
}
|
|
1754
|
+
if (entries.length <= 1) return entries;
|
|
1755
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1756
|
+
const deduped = [];
|
|
1757
|
+
for (const entry of entries) {
|
|
1758
|
+
let key = entry.filePath;
|
|
1759
|
+
try {
|
|
1760
|
+
key = await fs$1.realpath(entry.filePath);
|
|
1761
|
+
} catch {}
|
|
1762
|
+
if (seen.has(key)) continue;
|
|
1763
|
+
seen.add(key);
|
|
1764
|
+
deduped.push(entry);
|
|
1765
|
+
}
|
|
1766
|
+
return deduped;
|
|
1767
|
+
}
|
|
1768
|
+
async function loadWorkspaceBootstrapFiles(dir) {
|
|
1769
|
+
const resolvedDir = resolveUserPath(dir);
|
|
1770
|
+
const entries = [
|
|
1771
|
+
{
|
|
1772
|
+
name: DEFAULT_AGENTS_FILENAME,
|
|
1773
|
+
filePath: path.join(resolvedDir, DEFAULT_AGENTS_FILENAME)
|
|
1774
|
+
},
|
|
1775
|
+
{
|
|
1776
|
+
name: DEFAULT_SOUL_FILENAME,
|
|
1777
|
+
filePath: path.join(resolvedDir, DEFAULT_SOUL_FILENAME)
|
|
1778
|
+
},
|
|
1779
|
+
{
|
|
1780
|
+
name: DEFAULT_TOOLS_FILENAME,
|
|
1781
|
+
filePath: path.join(resolvedDir, DEFAULT_TOOLS_FILENAME)
|
|
1782
|
+
},
|
|
1783
|
+
{
|
|
1784
|
+
name: DEFAULT_IDENTITY_FILENAME,
|
|
1785
|
+
filePath: path.join(resolvedDir, DEFAULT_IDENTITY_FILENAME)
|
|
1786
|
+
},
|
|
1787
|
+
{
|
|
1788
|
+
name: DEFAULT_USER_FILENAME,
|
|
1789
|
+
filePath: path.join(resolvedDir, DEFAULT_USER_FILENAME)
|
|
1790
|
+
},
|
|
1791
|
+
{
|
|
1792
|
+
name: DEFAULT_HEARTBEAT_FILENAME,
|
|
1793
|
+
filePath: path.join(resolvedDir, DEFAULT_HEARTBEAT_FILENAME)
|
|
1794
|
+
},
|
|
1795
|
+
{
|
|
1796
|
+
name: DEFAULT_BOOTSTRAP_FILENAME,
|
|
1797
|
+
filePath: path.join(resolvedDir, DEFAULT_BOOTSTRAP_FILENAME)
|
|
1798
|
+
}
|
|
1799
|
+
];
|
|
1800
|
+
entries.push(...await resolveMemoryBootstrapEntries(resolvedDir));
|
|
1801
|
+
const result = [];
|
|
1802
|
+
for (const entry of entries) {
|
|
1803
|
+
const loaded = await readWorkspaceFileWithGuards({
|
|
1804
|
+
filePath: entry.filePath,
|
|
1805
|
+
workspaceDir: resolvedDir
|
|
1806
|
+
});
|
|
1807
|
+
if (loaded.ok) result.push({
|
|
1808
|
+
name: entry.name,
|
|
1809
|
+
path: entry.filePath,
|
|
1810
|
+
content: loaded.content,
|
|
1811
|
+
missing: false
|
|
1812
|
+
});
|
|
1813
|
+
else result.push({
|
|
1814
|
+
name: entry.name,
|
|
1815
|
+
path: entry.filePath,
|
|
1816
|
+
missing: true
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
return result;
|
|
1820
|
+
}
|
|
1821
|
+
const MINIMAL_BOOTSTRAP_ALLOWLIST = new Set([
|
|
1822
|
+
DEFAULT_AGENTS_FILENAME,
|
|
1823
|
+
DEFAULT_TOOLS_FILENAME,
|
|
1824
|
+
DEFAULT_SOUL_FILENAME,
|
|
1825
|
+
DEFAULT_IDENTITY_FILENAME,
|
|
1826
|
+
DEFAULT_USER_FILENAME
|
|
1827
|
+
]);
|
|
1828
|
+
function filterBootstrapFilesForSession(files, sessionKey) {
|
|
1829
|
+
if (!sessionKey || !isSubagentSessionKey(sessionKey) && !isCronSessionKey(sessionKey)) return files;
|
|
1830
|
+
return files.filter((file) => MINIMAL_BOOTSTRAP_ALLOWLIST.has(file.name));
|
|
1831
|
+
}
|
|
1832
|
+
//#endregion
|
|
1833
|
+
//#region src/agents/agent-scope.ts
|
|
1834
|
+
const log = createSubsystemLogger("agent-scope");
|
|
1835
|
+
/** Strip null bytes from paths to prevent ENOTDIR errors. */
|
|
1836
|
+
function stripNullBytes(s) {
|
|
1837
|
+
return s.replace(/\0/g, "");
|
|
1838
|
+
}
|
|
1839
|
+
let defaultAgentWarned = false;
|
|
1840
|
+
function listAgentEntries(cfg) {
|
|
1841
|
+
const list = cfg.agents?.list;
|
|
1842
|
+
if (!Array.isArray(list)) return [];
|
|
1843
|
+
return list.filter((entry) => Boolean(entry && typeof entry === "object"));
|
|
1844
|
+
}
|
|
1845
|
+
function listAgentIds(cfg) {
|
|
1846
|
+
const agents = listAgentEntries(cfg);
|
|
1847
|
+
if (agents.length === 0) return [DEFAULT_AGENT_ID];
|
|
1848
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1849
|
+
const ids = [];
|
|
1850
|
+
for (const entry of agents) {
|
|
1851
|
+
const id = normalizeAgentId(entry?.id);
|
|
1852
|
+
if (seen.has(id)) continue;
|
|
1853
|
+
seen.add(id);
|
|
1854
|
+
ids.push(id);
|
|
1855
|
+
}
|
|
1856
|
+
return ids.length > 0 ? ids : [DEFAULT_AGENT_ID];
|
|
1857
|
+
}
|
|
1858
|
+
function resolveDefaultAgentId(cfg) {
|
|
1859
|
+
const agents = listAgentEntries(cfg);
|
|
1860
|
+
if (agents.length === 0) return DEFAULT_AGENT_ID;
|
|
1861
|
+
const defaults = agents.filter((agent) => agent?.default);
|
|
1862
|
+
if (defaults.length > 1 && !defaultAgentWarned) {
|
|
1863
|
+
defaultAgentWarned = true;
|
|
1864
|
+
log.warn("Multiple agents marked default=true; using the first entry as default.");
|
|
1865
|
+
}
|
|
1866
|
+
const chosen = (defaults[0] ?? agents[0])?.id?.trim();
|
|
1867
|
+
return normalizeAgentId(chosen || "main");
|
|
1868
|
+
}
|
|
1869
|
+
function resolveSessionAgentIds(params) {
|
|
1870
|
+
const defaultAgentId = resolveDefaultAgentId(params.config ?? {});
|
|
1871
|
+
const explicitAgentIdRaw = typeof params.agentId === "string" ? params.agentId.trim().toLowerCase() : "";
|
|
1872
|
+
const explicitAgentId = explicitAgentIdRaw ? normalizeAgentId(explicitAgentIdRaw) : null;
|
|
1873
|
+
const sessionKey = params.sessionKey?.trim();
|
|
1874
|
+
const normalizedSessionKey = sessionKey ? sessionKey.toLowerCase() : void 0;
|
|
1875
|
+
const parsed = normalizedSessionKey ? parseAgentSessionKey(normalizedSessionKey) : null;
|
|
1876
|
+
return {
|
|
1877
|
+
defaultAgentId,
|
|
1878
|
+
sessionAgentId: explicitAgentId ?? (parsed?.agentId ? normalizeAgentId(parsed.agentId) : defaultAgentId)
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
function resolveSessionAgentId(params) {
|
|
1882
|
+
return resolveSessionAgentIds(params).sessionAgentId;
|
|
1883
|
+
}
|
|
1884
|
+
function resolveAgentEntry(cfg, agentId) {
|
|
1885
|
+
const id = normalizeAgentId(agentId);
|
|
1886
|
+
return listAgentEntries(cfg).find((entry) => normalizeAgentId(entry.id) === id);
|
|
1887
|
+
}
|
|
1888
|
+
function resolveAgentConfig(cfg, agentId) {
|
|
1889
|
+
const entry = resolveAgentEntry(cfg, normalizeAgentId(agentId));
|
|
1890
|
+
if (!entry) return;
|
|
1891
|
+
return {
|
|
1892
|
+
name: typeof entry.name === "string" ? entry.name : void 0,
|
|
1893
|
+
workspace: typeof entry.workspace === "string" ? entry.workspace : void 0,
|
|
1894
|
+
agentDir: typeof entry.agentDir === "string" ? entry.agentDir : void 0,
|
|
1895
|
+
model: typeof entry.model === "string" || entry.model && typeof entry.model === "object" ? entry.model : void 0,
|
|
1896
|
+
skills: Array.isArray(entry.skills) ? entry.skills : void 0,
|
|
1897
|
+
memorySearch: entry.memorySearch,
|
|
1898
|
+
humanDelay: entry.humanDelay,
|
|
1899
|
+
heartbeat: entry.heartbeat,
|
|
1900
|
+
identity: entry.identity,
|
|
1901
|
+
groupChat: entry.groupChat,
|
|
1902
|
+
subagents: typeof entry.subagents === "object" && entry.subagents ? entry.subagents : void 0,
|
|
1903
|
+
sandbox: entry.sandbox,
|
|
1904
|
+
tools: entry.tools
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
function resolveAgentSkillsFilter(cfg, agentId) {
|
|
1908
|
+
return normalizeSkillFilter(resolveAgentConfig(cfg, agentId)?.skills);
|
|
1909
|
+
}
|
|
1910
|
+
function resolveModelPrimary(raw) {
|
|
1911
|
+
if (typeof raw === "string") return raw.trim() || void 0;
|
|
1912
|
+
if (!raw || typeof raw !== "object") return;
|
|
1913
|
+
const primary = raw.primary;
|
|
1914
|
+
if (typeof primary !== "string") return;
|
|
1915
|
+
return primary.trim() || void 0;
|
|
1916
|
+
}
|
|
1917
|
+
function resolveAgentExplicitModelPrimary(cfg, agentId) {
|
|
1918
|
+
const raw = resolveAgentConfig(cfg, agentId)?.model;
|
|
1919
|
+
return resolveModelPrimary(raw);
|
|
1920
|
+
}
|
|
1921
|
+
function resolveAgentEffectiveModelPrimary(cfg, agentId) {
|
|
1922
|
+
return resolveAgentExplicitModelPrimary(cfg, agentId) ?? resolveModelPrimary(cfg.agents?.defaults?.model);
|
|
1923
|
+
}
|
|
1924
|
+
function resolveAgentModelFallbacksOverride(cfg, agentId) {
|
|
1925
|
+
const raw = resolveAgentConfig(cfg, agentId)?.model;
|
|
1926
|
+
if (!raw || typeof raw === "string") return;
|
|
1927
|
+
if (!Object.hasOwn(raw, "fallbacks")) return;
|
|
1928
|
+
return Array.isArray(raw.fallbacks) ? raw.fallbacks : void 0;
|
|
1929
|
+
}
|
|
1930
|
+
function resolveFallbackAgentId(params) {
|
|
1931
|
+
const explicitAgentId = typeof params.agentId === "string" ? params.agentId.trim() : "";
|
|
1932
|
+
if (explicitAgentId) return normalizeAgentId(explicitAgentId);
|
|
1933
|
+
return resolveAgentIdFromSessionKey(params.sessionKey);
|
|
1934
|
+
}
|
|
1935
|
+
function resolveRunModelFallbacksOverride(params) {
|
|
1936
|
+
if (!params.cfg) return;
|
|
1937
|
+
return resolveAgentModelFallbacksOverride(params.cfg, resolveFallbackAgentId({
|
|
1938
|
+
agentId: params.agentId,
|
|
1939
|
+
sessionKey: params.sessionKey
|
|
1940
|
+
}));
|
|
1941
|
+
}
|
|
1942
|
+
function hasConfiguredModelFallbacks(params) {
|
|
1943
|
+
const fallbacksOverride = resolveRunModelFallbacksOverride(params);
|
|
1944
|
+
const defaultFallbacks = resolveAgentModelFallbackValues(params.cfg?.agents?.defaults?.model);
|
|
1945
|
+
return (fallbacksOverride ?? defaultFallbacks).length > 0;
|
|
1946
|
+
}
|
|
1947
|
+
function resolveEffectiveModelFallbacks(params) {
|
|
1948
|
+
const agentFallbacksOverride = resolveAgentModelFallbacksOverride(params.cfg, params.agentId);
|
|
1949
|
+
if (!params.hasSessionModelOverride) return agentFallbacksOverride;
|
|
1950
|
+
const defaultFallbacks = resolveAgentModelFallbackValues(params.cfg.agents?.defaults?.model);
|
|
1951
|
+
return agentFallbacksOverride ?? defaultFallbacks;
|
|
1952
|
+
}
|
|
1953
|
+
function resolveAgentWorkspaceDir(cfg, agentId) {
|
|
1954
|
+
const id = normalizeAgentId(agentId);
|
|
1955
|
+
const configured = resolveAgentConfig(cfg, id)?.workspace?.trim();
|
|
1956
|
+
if (configured) return stripNullBytes(resolveUserPath(configured));
|
|
1957
|
+
if (id === resolveDefaultAgentId(cfg)) {
|
|
1958
|
+
const fallback = cfg.agents?.defaults?.workspace?.trim();
|
|
1959
|
+
if (fallback) return stripNullBytes(resolveUserPath(fallback));
|
|
1960
|
+
return stripNullBytes(resolveDefaultAgentWorkspaceDir(process.env));
|
|
1961
|
+
}
|
|
1962
|
+
const stateDir = resolveStateDir(process.env);
|
|
1963
|
+
return stripNullBytes(path.join(stateDir, `workspace-${id}`));
|
|
1964
|
+
}
|
|
1965
|
+
function resolveAgentDir(cfg, agentId) {
|
|
1966
|
+
const id = normalizeAgentId(agentId);
|
|
1967
|
+
const configured = resolveAgentConfig(cfg, id)?.agentDir?.trim();
|
|
1968
|
+
if (configured) return resolveUserPath(configured);
|
|
1969
|
+
const root = resolveStateDir(process.env);
|
|
1970
|
+
return path.join(root, "agents", id, "agent");
|
|
1971
|
+
}
|
|
1972
|
+
//#endregion
|
|
1973
|
+
//#region src/utils/run-with-concurrency.ts
|
|
1974
|
+
async function runTasksWithConcurrency(params) {
|
|
1975
|
+
const { tasks, limit, onTaskError } = params;
|
|
1976
|
+
const errorMode = params.errorMode ?? "continue";
|
|
1977
|
+
if (tasks.length === 0) return {
|
|
1978
|
+
results: [],
|
|
1979
|
+
firstError: void 0,
|
|
1980
|
+
hasError: false
|
|
1981
|
+
};
|
|
1982
|
+
const resolvedLimit = Math.max(1, Math.min(limit, tasks.length));
|
|
1983
|
+
const results = Array.from({ length: tasks.length });
|
|
1984
|
+
let next = 0;
|
|
1985
|
+
let firstError = void 0;
|
|
1986
|
+
let hasError = false;
|
|
1987
|
+
const workers = Array.from({ length: resolvedLimit }, async () => {
|
|
1988
|
+
while (true) {
|
|
1989
|
+
if (errorMode === "stop" && hasError) return;
|
|
1990
|
+
const index = next;
|
|
1991
|
+
next += 1;
|
|
1992
|
+
if (index >= tasks.length) return;
|
|
1993
|
+
try {
|
|
1994
|
+
results[index] = await tasks[index]();
|
|
1995
|
+
} catch (error) {
|
|
1996
|
+
if (!hasError) {
|
|
1997
|
+
firstError = error;
|
|
1998
|
+
hasError = true;
|
|
1999
|
+
}
|
|
2000
|
+
onTaskError?.(error, index);
|
|
2001
|
+
if (errorMode === "stop") return;
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
});
|
|
2005
|
+
await Promise.allSettled(workers);
|
|
2006
|
+
return {
|
|
2007
|
+
results,
|
|
2008
|
+
firstError,
|
|
2009
|
+
hasError
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
//#endregion
|
|
2013
|
+
//#region src/security/safe-regex.ts
|
|
2014
|
+
const SAFE_REGEX_CACHE_MAX = 256;
|
|
2015
|
+
const SAFE_REGEX_TEST_WINDOW = 2048;
|
|
2016
|
+
const safeRegexCache = /* @__PURE__ */ new Map();
|
|
2017
|
+
function createParseFrame() {
|
|
2018
|
+
return {
|
|
2019
|
+
lastToken: null,
|
|
2020
|
+
containsRepetition: false,
|
|
2021
|
+
hasAlternation: false,
|
|
2022
|
+
branchMinLength: 0,
|
|
2023
|
+
branchMaxLength: 0,
|
|
2024
|
+
altMinLength: null,
|
|
2025
|
+
altMaxLength: null
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
function addLength(left, right) {
|
|
2029
|
+
if (!Number.isFinite(left) || !Number.isFinite(right)) return Number.POSITIVE_INFINITY;
|
|
2030
|
+
return left + right;
|
|
2031
|
+
}
|
|
2032
|
+
function multiplyLength(length, factor) {
|
|
2033
|
+
if (!Number.isFinite(length)) return factor === 0 ? 0 : Number.POSITIVE_INFINITY;
|
|
2034
|
+
return length * factor;
|
|
2035
|
+
}
|
|
2036
|
+
function recordAlternative(frame) {
|
|
2037
|
+
if (frame.altMinLength === null || frame.altMaxLength === null) {
|
|
2038
|
+
frame.altMinLength = frame.branchMinLength;
|
|
2039
|
+
frame.altMaxLength = frame.branchMaxLength;
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
frame.altMinLength = Math.min(frame.altMinLength, frame.branchMinLength);
|
|
2043
|
+
frame.altMaxLength = Math.max(frame.altMaxLength, frame.branchMaxLength);
|
|
2044
|
+
}
|
|
2045
|
+
function readQuantifier(source, index) {
|
|
2046
|
+
const ch = source[index];
|
|
2047
|
+
const consumed = source[index + 1] === "?" ? 2 : 1;
|
|
2048
|
+
if (ch === "*") return {
|
|
2049
|
+
consumed,
|
|
2050
|
+
minRepeat: 0,
|
|
2051
|
+
maxRepeat: null
|
|
2052
|
+
};
|
|
2053
|
+
if (ch === "+") return {
|
|
2054
|
+
consumed,
|
|
2055
|
+
minRepeat: 1,
|
|
2056
|
+
maxRepeat: null
|
|
2057
|
+
};
|
|
2058
|
+
if (ch === "?") return {
|
|
2059
|
+
consumed,
|
|
2060
|
+
minRepeat: 0,
|
|
2061
|
+
maxRepeat: 1
|
|
2062
|
+
};
|
|
2063
|
+
if (ch !== "{") return null;
|
|
2064
|
+
let i = index + 1;
|
|
2065
|
+
while (i < source.length && /\d/.test(source[i])) i += 1;
|
|
2066
|
+
if (i === index + 1) return null;
|
|
2067
|
+
const minRepeat = Number.parseInt(source.slice(index + 1, i), 10);
|
|
2068
|
+
let maxRepeat = minRepeat;
|
|
2069
|
+
if (source[i] === ",") {
|
|
2070
|
+
i += 1;
|
|
2071
|
+
const maxStart = i;
|
|
2072
|
+
while (i < source.length && /\d/.test(source[i])) i += 1;
|
|
2073
|
+
maxRepeat = i === maxStart ? null : Number.parseInt(source.slice(maxStart, i), 10);
|
|
2074
|
+
}
|
|
2075
|
+
if (source[i] !== "}") return null;
|
|
2076
|
+
i += 1;
|
|
2077
|
+
if (source[i] === "?") i += 1;
|
|
2078
|
+
if (maxRepeat !== null && maxRepeat < minRepeat) return null;
|
|
2079
|
+
return {
|
|
2080
|
+
consumed: i - index,
|
|
2081
|
+
minRepeat,
|
|
2082
|
+
maxRepeat
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
function tokenizePattern(source) {
|
|
2086
|
+
const tokens = [];
|
|
2087
|
+
let inCharClass = false;
|
|
2088
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
2089
|
+
const ch = source[i];
|
|
2090
|
+
if (ch === "\\") {
|
|
2091
|
+
i += 1;
|
|
2092
|
+
tokens.push({ kind: "simple-token" });
|
|
2093
|
+
continue;
|
|
2094
|
+
}
|
|
2095
|
+
if (inCharClass) {
|
|
2096
|
+
if (ch === "]") inCharClass = false;
|
|
2097
|
+
continue;
|
|
2098
|
+
}
|
|
2099
|
+
if (ch === "[") {
|
|
2100
|
+
inCharClass = true;
|
|
2101
|
+
tokens.push({ kind: "simple-token" });
|
|
2102
|
+
continue;
|
|
2103
|
+
}
|
|
2104
|
+
if (ch === "(") {
|
|
2105
|
+
tokens.push({ kind: "group-open" });
|
|
2106
|
+
continue;
|
|
2107
|
+
}
|
|
2108
|
+
if (ch === ")") {
|
|
2109
|
+
tokens.push({ kind: "group-close" });
|
|
2110
|
+
continue;
|
|
2111
|
+
}
|
|
2112
|
+
if (ch === "|") {
|
|
2113
|
+
tokens.push({ kind: "alternation" });
|
|
2114
|
+
continue;
|
|
2115
|
+
}
|
|
2116
|
+
const quantifier = readQuantifier(source, i);
|
|
2117
|
+
if (quantifier) {
|
|
2118
|
+
tokens.push({
|
|
2119
|
+
kind: "quantifier",
|
|
2120
|
+
quantifier
|
|
2121
|
+
});
|
|
2122
|
+
i += quantifier.consumed - 1;
|
|
2123
|
+
continue;
|
|
2124
|
+
}
|
|
2125
|
+
tokens.push({ kind: "simple-token" });
|
|
2126
|
+
}
|
|
2127
|
+
return tokens;
|
|
2128
|
+
}
|
|
2129
|
+
function analyzeTokensForNestedRepetition(tokens) {
|
|
2130
|
+
const frames = [createParseFrame()];
|
|
2131
|
+
const emitToken = (token) => {
|
|
2132
|
+
const frame = frames[frames.length - 1];
|
|
2133
|
+
frame.lastToken = token;
|
|
2134
|
+
if (token.containsRepetition) frame.containsRepetition = true;
|
|
2135
|
+
frame.branchMinLength = addLength(frame.branchMinLength, token.minLength);
|
|
2136
|
+
frame.branchMaxLength = addLength(frame.branchMaxLength, token.maxLength);
|
|
2137
|
+
};
|
|
2138
|
+
const emitSimpleToken = () => {
|
|
2139
|
+
emitToken({
|
|
2140
|
+
containsRepetition: false,
|
|
2141
|
+
hasAmbiguousAlternation: false,
|
|
2142
|
+
minLength: 1,
|
|
2143
|
+
maxLength: 1
|
|
2144
|
+
});
|
|
2145
|
+
};
|
|
2146
|
+
for (const token of tokens) {
|
|
2147
|
+
if (token.kind === "simple-token") {
|
|
2148
|
+
emitSimpleToken();
|
|
2149
|
+
continue;
|
|
2150
|
+
}
|
|
2151
|
+
if (token.kind === "group-open") {
|
|
2152
|
+
frames.push(createParseFrame());
|
|
2153
|
+
continue;
|
|
2154
|
+
}
|
|
2155
|
+
if (token.kind === "group-close") {
|
|
2156
|
+
if (frames.length > 1) {
|
|
2157
|
+
const frame = frames.pop();
|
|
2158
|
+
if (frame.hasAlternation) recordAlternative(frame);
|
|
2159
|
+
const groupMinLength = frame.hasAlternation ? frame.altMinLength ?? 0 : frame.branchMinLength;
|
|
2160
|
+
const groupMaxLength = frame.hasAlternation ? frame.altMaxLength ?? 0 : frame.branchMaxLength;
|
|
2161
|
+
emitToken({
|
|
2162
|
+
containsRepetition: frame.containsRepetition,
|
|
2163
|
+
hasAmbiguousAlternation: frame.hasAlternation && frame.altMinLength !== null && frame.altMaxLength !== null && frame.altMinLength !== frame.altMaxLength,
|
|
2164
|
+
minLength: groupMinLength,
|
|
2165
|
+
maxLength: groupMaxLength
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
continue;
|
|
2169
|
+
}
|
|
2170
|
+
if (token.kind === "alternation") {
|
|
2171
|
+
const frame = frames[frames.length - 1];
|
|
2172
|
+
frame.hasAlternation = true;
|
|
2173
|
+
recordAlternative(frame);
|
|
2174
|
+
frame.branchMinLength = 0;
|
|
2175
|
+
frame.branchMaxLength = 0;
|
|
2176
|
+
frame.lastToken = null;
|
|
2177
|
+
continue;
|
|
2178
|
+
}
|
|
2179
|
+
const frame = frames[frames.length - 1];
|
|
2180
|
+
const previousToken = frame.lastToken;
|
|
2181
|
+
if (!previousToken) continue;
|
|
2182
|
+
if (previousToken.containsRepetition) return true;
|
|
2183
|
+
if (previousToken.hasAmbiguousAlternation && token.quantifier.maxRepeat === null) return true;
|
|
2184
|
+
const previousMinLength = previousToken.minLength;
|
|
2185
|
+
const previousMaxLength = previousToken.maxLength;
|
|
2186
|
+
previousToken.minLength = multiplyLength(previousToken.minLength, token.quantifier.minRepeat);
|
|
2187
|
+
previousToken.maxLength = token.quantifier.maxRepeat === null ? Number.POSITIVE_INFINITY : multiplyLength(previousToken.maxLength, token.quantifier.maxRepeat);
|
|
2188
|
+
previousToken.containsRepetition = true;
|
|
2189
|
+
frame.containsRepetition = true;
|
|
2190
|
+
frame.branchMinLength = frame.branchMinLength - previousMinLength + previousToken.minLength;
|
|
2191
|
+
frame.branchMaxLength = addLength(Number.isFinite(frame.branchMaxLength) && Number.isFinite(previousMaxLength) ? frame.branchMaxLength - previousMaxLength : Number.POSITIVE_INFINITY, previousToken.maxLength);
|
|
2192
|
+
}
|
|
2193
|
+
return false;
|
|
2194
|
+
}
|
|
2195
|
+
function testRegexFromStart(regex, value) {
|
|
2196
|
+
regex.lastIndex = 0;
|
|
2197
|
+
return regex.test(value);
|
|
2198
|
+
}
|
|
2199
|
+
function testRegexWithBoundedInput(regex, input, maxWindow = SAFE_REGEX_TEST_WINDOW) {
|
|
2200
|
+
if (maxWindow <= 0) return false;
|
|
2201
|
+
if (input.length <= maxWindow) return testRegexFromStart(regex, input);
|
|
2202
|
+
if (testRegexFromStart(regex, input.slice(0, maxWindow))) return true;
|
|
2203
|
+
return testRegexFromStart(regex, input.slice(-maxWindow));
|
|
2204
|
+
}
|
|
2205
|
+
function hasNestedRepetition(source) {
|
|
2206
|
+
return analyzeTokensForNestedRepetition(tokenizePattern(source));
|
|
2207
|
+
}
|
|
2208
|
+
function compileSafeRegex(source, flags = "") {
|
|
2209
|
+
const trimmed = source.trim();
|
|
2210
|
+
if (!trimmed) return null;
|
|
2211
|
+
const cacheKey = `${flags}::${trimmed}`;
|
|
2212
|
+
if (safeRegexCache.has(cacheKey)) return safeRegexCache.get(cacheKey) ?? null;
|
|
2213
|
+
let compiled = null;
|
|
2214
|
+
if (!hasNestedRepetition(trimmed)) try {
|
|
2215
|
+
compiled = new RegExp(trimmed, flags);
|
|
2216
|
+
} catch {
|
|
2217
|
+
compiled = null;
|
|
2218
|
+
}
|
|
2219
|
+
safeRegexCache.set(cacheKey, compiled);
|
|
2220
|
+
if (safeRegexCache.size > SAFE_REGEX_CACHE_MAX) {
|
|
2221
|
+
const oldestKey = safeRegexCache.keys().next().value;
|
|
2222
|
+
if (oldestKey) safeRegexCache.delete(oldestKey);
|
|
2223
|
+
}
|
|
2224
|
+
return compiled;
|
|
2225
|
+
}
|
|
2226
|
+
function replacePatternBounded(text, pattern, replacer, options) {
|
|
2227
|
+
const chunkThreshold = options?.chunkThreshold ?? 32768;
|
|
2228
|
+
const chunkSize = options?.chunkSize ?? 16384;
|
|
2229
|
+
if (chunkThreshold <= 0 || chunkSize <= 0 || text.length <= chunkThreshold) return text.replace(pattern, replacer);
|
|
2230
|
+
let output = "";
|
|
2231
|
+
for (let index = 0; index < text.length; index += chunkSize) output += text.slice(index, index + chunkSize).replace(pattern, replacer);
|
|
2232
|
+
return output;
|
|
2233
|
+
}
|
|
2234
|
+
//#endregion
|
|
2235
|
+
//#region src/logging/redact.ts
|
|
2236
|
+
const requireConfig = resolveNodeRequireFromMeta(import.meta.url);
|
|
2237
|
+
const DEFAULT_REDACT_MODE = "tools";
|
|
2238
|
+
const DEFAULT_REDACT_MIN_LENGTH = 18;
|
|
2239
|
+
const DEFAULT_REDACT_KEEP_START = 6;
|
|
2240
|
+
const DEFAULT_REDACT_KEEP_END = 4;
|
|
2241
|
+
const DEFAULT_REDACT_PATTERNS = [
|
|
2242
|
+
String.raw`\b[A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD)\b\s*[=:]\s*(["']?)([^\s"'\\]+)\1`,
|
|
2243
|
+
String.raw`"(?:apiKey|token|secret|password|passwd|accessToken|refreshToken)"\s*:\s*"([^"]+)"`,
|
|
2244
|
+
String.raw`--(?:api[-_]?key|token|secret|password|passwd)\s+(["']?)([^\s"']+)\1`,
|
|
2245
|
+
String.raw`Authorization\s*[:=]\s*Bearer\s+([A-Za-z0-9._\-+=]+)`,
|
|
2246
|
+
String.raw`\bBearer\s+([A-Za-z0-9._\-+=]{18,})\b`,
|
|
2247
|
+
String.raw`-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----`,
|
|
2248
|
+
String.raw`\b(sk-[A-Za-z0-9_-]{8,})\b`,
|
|
2249
|
+
String.raw`\b(ghp_[A-Za-z0-9]{20,})\b`,
|
|
2250
|
+
String.raw`\b(github_pat_[A-Za-z0-9_]{20,})\b`,
|
|
2251
|
+
String.raw`\b(xox[baprs]-[A-Za-z0-9-]{10,})\b`,
|
|
2252
|
+
String.raw`\b(xapp-[A-Za-z0-9-]{10,})\b`,
|
|
2253
|
+
String.raw`\b(gsk_[A-Za-z0-9_-]{10,})\b`,
|
|
2254
|
+
String.raw`\b(AIza[0-9A-Za-z\-_]{20,})\b`,
|
|
2255
|
+
String.raw`\b(pplx-[A-Za-z0-9_-]{10,})\b`,
|
|
2256
|
+
String.raw`\b(npm_[A-Za-z0-9]{10,})\b`,
|
|
2257
|
+
String.raw`\bbot(\d{6,}:[A-Za-z0-9_-]{20,})\b`,
|
|
2258
|
+
String.raw`\b(\d{6,}:[A-Za-z0-9_-]{20,})\b`
|
|
2259
|
+
];
|
|
2260
|
+
function normalizeMode(value) {
|
|
2261
|
+
return value === "off" ? "off" : DEFAULT_REDACT_MODE;
|
|
2262
|
+
}
|
|
2263
|
+
function parsePattern(raw) {
|
|
2264
|
+
if (!raw.trim()) return null;
|
|
2265
|
+
const match = raw.match(/^\/(.+)\/([gimsuy]*)$/);
|
|
2266
|
+
if (match) {
|
|
2267
|
+
const flags = match[2].includes("g") ? match[2] : `${match[2]}g`;
|
|
2268
|
+
return compileSafeRegex(match[1], flags);
|
|
2269
|
+
}
|
|
2270
|
+
return compileSafeRegex(raw, "gi");
|
|
2271
|
+
}
|
|
2272
|
+
function resolvePatterns(value) {
|
|
2273
|
+
return (value?.length ? value : DEFAULT_REDACT_PATTERNS).map(parsePattern).filter((re) => Boolean(re));
|
|
2274
|
+
}
|
|
2275
|
+
function maskToken(token) {
|
|
2276
|
+
if (token.length < DEFAULT_REDACT_MIN_LENGTH) return "***";
|
|
2277
|
+
return `${token.slice(0, DEFAULT_REDACT_KEEP_START)}…${token.slice(-DEFAULT_REDACT_KEEP_END)}`;
|
|
2278
|
+
}
|
|
2279
|
+
function redactPemBlock(block) {
|
|
2280
|
+
const lines = block.split(/\r?\n/).filter(Boolean);
|
|
2281
|
+
if (lines.length < 2) return "***";
|
|
2282
|
+
return `${lines[0]}\n…redacted…\n${lines[lines.length - 1]}`;
|
|
2283
|
+
}
|
|
2284
|
+
function redactMatch(match, groups) {
|
|
2285
|
+
if (match.includes("PRIVATE KEY-----")) return redactPemBlock(match);
|
|
2286
|
+
const token = groups.filter((value) => typeof value === "string" && value.length > 0).at(-1) ?? match;
|
|
2287
|
+
const masked = maskToken(token);
|
|
2288
|
+
if (token === match) return masked;
|
|
2289
|
+
return match.replace(token, masked);
|
|
2290
|
+
}
|
|
2291
|
+
function redactText(text, patterns) {
|
|
2292
|
+
let next = text;
|
|
2293
|
+
for (const pattern of patterns) next = replacePatternBounded(next, pattern, (...args) => redactMatch(args[0], args.slice(1, args.length - 2)));
|
|
2294
|
+
return next;
|
|
2295
|
+
}
|
|
2296
|
+
function resolveConfigRedaction() {
|
|
2297
|
+
let cfg;
|
|
2298
|
+
try {
|
|
2299
|
+
cfg = (requireConfig?.("../config/config.js"))?.loadConfig?.().logging;
|
|
2300
|
+
} catch {
|
|
2301
|
+
cfg = void 0;
|
|
2302
|
+
}
|
|
2303
|
+
return {
|
|
2304
|
+
mode: normalizeMode(cfg?.redactSensitive),
|
|
2305
|
+
patterns: cfg?.redactPatterns
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
function redactSensitiveText(text, options) {
|
|
2309
|
+
if (!text) return text;
|
|
2310
|
+
const resolved = options ?? resolveConfigRedaction();
|
|
2311
|
+
if (normalizeMode(resolved.mode) === "off") return text;
|
|
2312
|
+
const patterns = resolvePatterns(resolved.patterns);
|
|
2313
|
+
if (!patterns.length) return text;
|
|
2314
|
+
return redactText(text, patterns);
|
|
2315
|
+
}
|
|
2316
|
+
function redactToolDetail(detail) {
|
|
2317
|
+
const resolved = resolveConfigRedaction();
|
|
2318
|
+
if (normalizeMode(resolved.mode) !== "tools") return detail;
|
|
2319
|
+
return redactSensitiveText(detail, resolved);
|
|
2320
|
+
}
|
|
2321
|
+
function getDefaultRedactPatterns() {
|
|
2322
|
+
return [...DEFAULT_REDACT_PATTERNS];
|
|
2323
|
+
}
|
|
2324
|
+
//#endregion
|
|
2325
|
+
//#region src/infra/hardlink-guards.ts
|
|
2326
|
+
async function assertNoHardlinkedFinalPath(params) {
|
|
2327
|
+
if (params.allowFinalHardlinkForUnlink) return;
|
|
2328
|
+
let stat;
|
|
2329
|
+
try {
|
|
2330
|
+
stat = await fs$1.stat(params.filePath);
|
|
2331
|
+
} catch (err) {
|
|
2332
|
+
if (isNotFoundPathError(err)) return;
|
|
2333
|
+
throw err;
|
|
2334
|
+
}
|
|
2335
|
+
if (!stat.isFile()) return;
|
|
2336
|
+
if (stat.nlink > 1) throw new Error(`Hardlinked path is not allowed under ${params.boundaryLabel} (${shortPath(params.root)}): ${shortPath(params.filePath)}`);
|
|
2337
|
+
}
|
|
2338
|
+
function shortPath(value) {
|
|
2339
|
+
if (value.startsWith(os.homedir())) return `~${value.slice(os.homedir().length)}`;
|
|
2340
|
+
return value;
|
|
2341
|
+
}
|
|
2342
|
+
//#endregion
|
|
2343
|
+
//#region src/infra/path-alias-guards.ts
|
|
2344
|
+
const PATH_ALIAS_POLICIES = BOUNDARY_PATH_ALIAS_POLICIES;
|
|
2345
|
+
async function assertNoPathAliasEscape(params) {
|
|
2346
|
+
const resolved = await resolveBoundaryPath({
|
|
2347
|
+
absolutePath: params.absolutePath,
|
|
2348
|
+
rootPath: params.rootPath,
|
|
2349
|
+
boundaryLabel: params.boundaryLabel,
|
|
2350
|
+
policy: params.policy
|
|
2351
|
+
});
|
|
2352
|
+
if (params.policy?.allowFinalSymlinkForUnlink === true && resolved.kind === "symlink") return;
|
|
2353
|
+
await assertNoHardlinkedFinalPath({
|
|
2354
|
+
filePath: resolved.absolutePath,
|
|
2355
|
+
root: resolved.rootPath,
|
|
2356
|
+
boundaryLabel: params.boundaryLabel,
|
|
2357
|
+
allowFinalHardlinkForUnlink: params.policy?.allowFinalHardlinkForUnlink
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
//#endregion
|
|
2361
|
+
//#region src/infra/fs-safe.ts
|
|
2362
|
+
var SafeOpenError = class extends Error {
|
|
2363
|
+
constructor(code, message, options) {
|
|
2364
|
+
super(message, options);
|
|
2365
|
+
this.code = code;
|
|
2366
|
+
this.name = "SafeOpenError";
|
|
2367
|
+
}
|
|
2368
|
+
};
|
|
2369
|
+
const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in constants;
|
|
2370
|
+
const OPEN_READ_FLAGS = constants.O_RDONLY | (SUPPORTS_NOFOLLOW ? constants.O_NOFOLLOW : 0);
|
|
2371
|
+
const OPEN_WRITE_EXISTING_FLAGS = constants.O_WRONLY | (SUPPORTS_NOFOLLOW ? constants.O_NOFOLLOW : 0);
|
|
2372
|
+
const OPEN_WRITE_CREATE_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (SUPPORTS_NOFOLLOW ? constants.O_NOFOLLOW : 0);
|
|
2373
|
+
const OPEN_APPEND_EXISTING_FLAGS = constants.O_RDWR | constants.O_APPEND | (SUPPORTS_NOFOLLOW ? constants.O_NOFOLLOW : 0);
|
|
2374
|
+
const OPEN_APPEND_CREATE_FLAGS = constants.O_RDWR | constants.O_APPEND | constants.O_CREAT | constants.O_EXCL | (SUPPORTS_NOFOLLOW ? constants.O_NOFOLLOW : 0);
|
|
2375
|
+
const ensureTrailingSep = (value) => value.endsWith(path.sep) ? value : value + path.sep;
|
|
2376
|
+
async function expandRelativePathWithHome(relativePath) {
|
|
2377
|
+
let home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
2378
|
+
try {
|
|
2379
|
+
home = await fs$1.realpath(home);
|
|
2380
|
+
} catch {}
|
|
2381
|
+
return expandHomePrefix(relativePath, { home });
|
|
2382
|
+
}
|
|
2383
|
+
async function openVerifiedLocalFile(filePath, options) {
|
|
2384
|
+
try {
|
|
2385
|
+
if ((await fs$1.lstat(filePath)).isDirectory()) throw new SafeOpenError("not-file", "not a file");
|
|
2386
|
+
} catch (err) {
|
|
2387
|
+
if (err instanceof SafeOpenError) throw err;
|
|
2388
|
+
}
|
|
2389
|
+
let handle;
|
|
2390
|
+
try {
|
|
2391
|
+
handle = await fs$1.open(filePath, OPEN_READ_FLAGS);
|
|
2392
|
+
} catch (err) {
|
|
2393
|
+
if (isNotFoundPathError(err)) throw new SafeOpenError("not-found", "file not found");
|
|
2394
|
+
if (isSymlinkOpenError(err)) throw new SafeOpenError("symlink", "symlink open blocked", { cause: err });
|
|
2395
|
+
if (hasNodeErrorCode(err, "EISDIR")) throw new SafeOpenError("not-file", "not a file");
|
|
2396
|
+
throw err;
|
|
2397
|
+
}
|
|
2398
|
+
try {
|
|
2399
|
+
const [stat, lstat] = await Promise.all([handle.stat(), fs$1.lstat(filePath)]);
|
|
2400
|
+
if (lstat.isSymbolicLink()) throw new SafeOpenError("symlink", "symlink not allowed");
|
|
2401
|
+
if (!stat.isFile()) throw new SafeOpenError("not-file", "not a file");
|
|
2402
|
+
if (options?.rejectHardlinks && stat.nlink > 1) throw new SafeOpenError("invalid-path", "hardlinked path not allowed");
|
|
2403
|
+
if (!sameFileIdentity$1(stat, lstat)) throw new SafeOpenError("path-mismatch", "path changed during read");
|
|
2404
|
+
const realPath = await fs$1.realpath(filePath);
|
|
2405
|
+
const realStat = await fs$1.stat(realPath);
|
|
2406
|
+
if (options?.rejectHardlinks && realStat.nlink > 1) throw new SafeOpenError("invalid-path", "hardlinked path not allowed");
|
|
2407
|
+
if (!sameFileIdentity$1(stat, realStat)) throw new SafeOpenError("path-mismatch", "path mismatch");
|
|
2408
|
+
return {
|
|
2409
|
+
handle,
|
|
2410
|
+
realPath,
|
|
2411
|
+
stat
|
|
2412
|
+
};
|
|
2413
|
+
} catch (err) {
|
|
2414
|
+
await handle.close().catch(() => {});
|
|
2415
|
+
if (err instanceof SafeOpenError) throw err;
|
|
2416
|
+
if (isNotFoundPathError(err)) throw new SafeOpenError("not-found", "file not found");
|
|
2417
|
+
throw err;
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
async function resolvePathWithinRoot(params) {
|
|
2421
|
+
let rootReal;
|
|
2422
|
+
try {
|
|
2423
|
+
rootReal = await fs$1.realpath(params.rootDir);
|
|
2424
|
+
} catch (err) {
|
|
2425
|
+
if (isNotFoundPathError(err)) throw new SafeOpenError("not-found", "root dir not found");
|
|
2426
|
+
throw err;
|
|
2427
|
+
}
|
|
2428
|
+
const rootWithSep = ensureTrailingSep(rootReal);
|
|
2429
|
+
const expanded = await expandRelativePathWithHome(params.relativePath);
|
|
2430
|
+
const resolved = path.resolve(rootWithSep, expanded);
|
|
2431
|
+
if (!isPathInside(rootWithSep, resolved)) throw new SafeOpenError("outside-workspace", "file is outside workspace root");
|
|
2432
|
+
return {
|
|
2433
|
+
rootReal,
|
|
2434
|
+
rootWithSep,
|
|
2435
|
+
resolved
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
async function openFileWithinRoot(params) {
|
|
2439
|
+
const { rootWithSep, resolved } = await resolvePathWithinRoot(params);
|
|
2440
|
+
let opened;
|
|
2441
|
+
try {
|
|
2442
|
+
opened = await openVerifiedLocalFile(resolved);
|
|
2443
|
+
} catch (err) {
|
|
2444
|
+
if (err instanceof SafeOpenError) {
|
|
2445
|
+
if (err.code === "not-found") throw err;
|
|
2446
|
+
throw new SafeOpenError("invalid-path", "path is not a regular file under root", { cause: err });
|
|
2447
|
+
}
|
|
2448
|
+
throw err;
|
|
2449
|
+
}
|
|
2450
|
+
if (params.rejectHardlinks !== false && opened.stat.nlink > 1) {
|
|
2451
|
+
await opened.handle.close().catch(() => {});
|
|
2452
|
+
throw new SafeOpenError("invalid-path", "hardlinked path not allowed");
|
|
2453
|
+
}
|
|
2454
|
+
if (!isPathInside(rootWithSep, opened.realPath)) {
|
|
2455
|
+
await opened.handle.close().catch(() => {});
|
|
2456
|
+
throw new SafeOpenError("outside-workspace", "file is outside workspace root");
|
|
2457
|
+
}
|
|
2458
|
+
return opened;
|
|
2459
|
+
}
|
|
2460
|
+
async function readFileWithinRoot(params) {
|
|
2461
|
+
const opened = await openFileWithinRoot({
|
|
2462
|
+
rootDir: params.rootDir,
|
|
2463
|
+
relativePath: params.relativePath,
|
|
2464
|
+
rejectHardlinks: params.rejectHardlinks
|
|
2465
|
+
});
|
|
2466
|
+
try {
|
|
2467
|
+
return await readOpenedFileSafely({
|
|
2468
|
+
opened,
|
|
2469
|
+
maxBytes: params.maxBytes
|
|
2470
|
+
});
|
|
2471
|
+
} finally {
|
|
2472
|
+
await opened.handle.close().catch(() => {});
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
async function readPathWithinRoot(params) {
|
|
2476
|
+
const rootDir = path.resolve(params.rootDir);
|
|
2477
|
+
const candidatePath = path.isAbsolute(params.filePath) ? path.resolve(params.filePath) : path.resolve(rootDir, params.filePath);
|
|
2478
|
+
return await readFileWithinRoot({
|
|
2479
|
+
rootDir,
|
|
2480
|
+
relativePath: path.relative(rootDir, candidatePath),
|
|
2481
|
+
rejectHardlinks: params.rejectHardlinks,
|
|
2482
|
+
maxBytes: params.maxBytes
|
|
2483
|
+
});
|
|
2484
|
+
}
|
|
2485
|
+
function createRootScopedReadFile(params) {
|
|
2486
|
+
const rootDir = path.resolve(params.rootDir);
|
|
2487
|
+
return async (filePath) => {
|
|
2488
|
+
return (await readPathWithinRoot({
|
|
2489
|
+
rootDir,
|
|
2490
|
+
filePath,
|
|
2491
|
+
rejectHardlinks: params.rejectHardlinks,
|
|
2492
|
+
maxBytes: params.maxBytes
|
|
2493
|
+
})).buffer;
|
|
2494
|
+
};
|
|
2495
|
+
}
|
|
2496
|
+
async function readLocalFileSafely(params) {
|
|
2497
|
+
const opened = await openVerifiedLocalFile(params.filePath);
|
|
2498
|
+
try {
|
|
2499
|
+
return await readOpenedFileSafely({
|
|
2500
|
+
opened,
|
|
2501
|
+
maxBytes: params.maxBytes
|
|
2502
|
+
});
|
|
2503
|
+
} finally {
|
|
2504
|
+
await opened.handle.close().catch(() => {});
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
async function readOpenedFileSafely(params) {
|
|
2508
|
+
if (params.maxBytes !== void 0 && params.opened.stat.size > params.maxBytes) throw new SafeOpenError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${params.opened.stat.size})`);
|
|
2509
|
+
return {
|
|
2510
|
+
buffer: await params.opened.handle.readFile(),
|
|
2511
|
+
realPath: params.opened.realPath,
|
|
2512
|
+
stat: params.opened.stat
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
function emitWriteBoundaryWarning(reason) {
|
|
2516
|
+
logWarn(`security: fs-safe write boundary warning (${reason})`);
|
|
2517
|
+
}
|
|
2518
|
+
function buildAtomicWriteTempPath(targetPath) {
|
|
2519
|
+
const dir = path.dirname(targetPath);
|
|
2520
|
+
const base = path.basename(targetPath);
|
|
2521
|
+
return path.join(dir, `.${base}.${process.pid}.${randomUUID()}.tmp`);
|
|
2522
|
+
}
|
|
2523
|
+
async function writeTempFileForAtomicReplace(params) {
|
|
2524
|
+
const tempHandle = await fs$1.open(params.tempPath, OPEN_WRITE_CREATE_FLAGS, params.mode);
|
|
2525
|
+
try {
|
|
2526
|
+
if (typeof params.data === "string") await tempHandle.writeFile(params.data, params.encoding ?? "utf8");
|
|
2527
|
+
else await tempHandle.writeFile(params.data);
|
|
2528
|
+
return await tempHandle.stat();
|
|
2529
|
+
} finally {
|
|
2530
|
+
await tempHandle.close().catch(() => {});
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
async function verifyAtomicWriteResult(params) {
|
|
2534
|
+
const rootWithSep = ensureTrailingSep(await fs$1.realpath(params.rootDir));
|
|
2535
|
+
const opened = await openVerifiedLocalFile(params.targetPath, { rejectHardlinks: true });
|
|
2536
|
+
try {
|
|
2537
|
+
if (!sameFileIdentity$1(opened.stat, params.expectedStat)) throw new SafeOpenError("path-mismatch", "path changed during write");
|
|
2538
|
+
if (!isPathInside(rootWithSep, opened.realPath)) throw new SafeOpenError("outside-workspace", "file is outside workspace root");
|
|
2539
|
+
} finally {
|
|
2540
|
+
await opened.handle.close().catch(() => {});
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
async function resolveOpenedFileRealPathForHandle(handle, ioPath) {
|
|
2544
|
+
try {
|
|
2545
|
+
return await fs$1.realpath(ioPath);
|
|
2546
|
+
} catch (err) {
|
|
2547
|
+
if (!isNotFoundPathError(err)) throw err;
|
|
2548
|
+
}
|
|
2549
|
+
const fdCandidates = process.platform === "linux" ? [`/proc/self/fd/${handle.fd}`, `/dev/fd/${handle.fd}`] : process.platform === "win32" ? [] : [`/dev/fd/${handle.fd}`];
|
|
2550
|
+
for (const fdPath of fdCandidates) try {
|
|
2551
|
+
return await fs$1.realpath(fdPath);
|
|
2552
|
+
} catch {}
|
|
2553
|
+
throw new SafeOpenError("path-mismatch", "unable to resolve opened file path");
|
|
2554
|
+
}
|
|
2555
|
+
async function openWritableFileWithinRoot(params) {
|
|
2556
|
+
const { rootReal, rootWithSep, resolved } = await resolvePathWithinRoot(params);
|
|
2557
|
+
try {
|
|
2558
|
+
await assertNoPathAliasEscape({
|
|
2559
|
+
absolutePath: resolved,
|
|
2560
|
+
rootPath: rootReal,
|
|
2561
|
+
boundaryLabel: "root"
|
|
2562
|
+
});
|
|
2563
|
+
} catch (err) {
|
|
2564
|
+
throw new SafeOpenError("invalid-path", "path alias escape blocked", { cause: err });
|
|
2565
|
+
}
|
|
2566
|
+
if (params.mkdir !== false) await fs$1.mkdir(path.dirname(resolved), { recursive: true });
|
|
2567
|
+
let ioPath = resolved;
|
|
2568
|
+
try {
|
|
2569
|
+
const resolvedRealPath = await fs$1.realpath(resolved);
|
|
2570
|
+
if (!isPathInside(rootWithSep, resolvedRealPath)) throw new SafeOpenError("outside-workspace", "file is outside workspace root");
|
|
2571
|
+
ioPath = resolvedRealPath;
|
|
2572
|
+
} catch (err) {
|
|
2573
|
+
if (err instanceof SafeOpenError) throw err;
|
|
2574
|
+
if (!isNotFoundPathError(err)) throw err;
|
|
2575
|
+
}
|
|
2576
|
+
const fileMode = params.mode ?? 384;
|
|
2577
|
+
let handle;
|
|
2578
|
+
let createdForWrite = false;
|
|
2579
|
+
const existingFlags = params.append ? OPEN_APPEND_EXISTING_FLAGS : OPEN_WRITE_EXISTING_FLAGS;
|
|
2580
|
+
const createFlags = params.append ? OPEN_APPEND_CREATE_FLAGS : OPEN_WRITE_CREATE_FLAGS;
|
|
2581
|
+
try {
|
|
2582
|
+
try {
|
|
2583
|
+
handle = await fs$1.open(ioPath, existingFlags, fileMode);
|
|
2584
|
+
} catch (err) {
|
|
2585
|
+
if (!isNotFoundPathError(err)) throw err;
|
|
2586
|
+
handle = await fs$1.open(ioPath, createFlags, fileMode);
|
|
2587
|
+
createdForWrite = true;
|
|
2588
|
+
}
|
|
2589
|
+
} catch (err) {
|
|
2590
|
+
if (isNotFoundPathError(err)) throw new SafeOpenError("not-found", "file not found");
|
|
2591
|
+
if (isSymlinkOpenError(err)) throw new SafeOpenError("invalid-path", "symlink open blocked", { cause: err });
|
|
2592
|
+
throw err;
|
|
2593
|
+
}
|
|
2594
|
+
let openedRealPath = null;
|
|
2595
|
+
try {
|
|
2596
|
+
const stat = await handle.stat();
|
|
2597
|
+
if (!stat.isFile()) throw new SafeOpenError("invalid-path", "path is not a regular file under root");
|
|
2598
|
+
if (stat.nlink > 1) throw new SafeOpenError("invalid-path", "hardlinked path not allowed");
|
|
2599
|
+
try {
|
|
2600
|
+
const lstat = await fs$1.lstat(ioPath);
|
|
2601
|
+
if (lstat.isSymbolicLink() || !lstat.isFile()) throw new SafeOpenError("invalid-path", "path is not a regular file under root");
|
|
2602
|
+
if (!sameFileIdentity$1(stat, lstat)) throw new SafeOpenError("path-mismatch", "path changed during write");
|
|
2603
|
+
} catch (err) {
|
|
2604
|
+
if (!isNotFoundPathError(err)) throw err;
|
|
2605
|
+
}
|
|
2606
|
+
const realPath = await resolveOpenedFileRealPathForHandle(handle, ioPath);
|
|
2607
|
+
openedRealPath = realPath;
|
|
2608
|
+
const realStat = await fs$1.stat(realPath);
|
|
2609
|
+
if (!sameFileIdentity$1(stat, realStat)) throw new SafeOpenError("path-mismatch", "path mismatch");
|
|
2610
|
+
if (realStat.nlink > 1) throw new SafeOpenError("invalid-path", "hardlinked path not allowed");
|
|
2611
|
+
if (!isPathInside(rootWithSep, realPath)) throw new SafeOpenError("outside-workspace", "file is outside workspace root");
|
|
2612
|
+
if (params.append !== true && params.truncateExisting !== false && !createdForWrite) await handle.truncate(0);
|
|
2613
|
+
return {
|
|
2614
|
+
handle,
|
|
2615
|
+
createdForWrite,
|
|
2616
|
+
openedRealPath: realPath,
|
|
2617
|
+
openedStat: stat
|
|
2618
|
+
};
|
|
2619
|
+
} catch (err) {
|
|
2620
|
+
const cleanupCreatedPath = createdForWrite && err instanceof SafeOpenError;
|
|
2621
|
+
const cleanupPath = openedRealPath ?? ioPath;
|
|
2622
|
+
await handle.close().catch(() => {});
|
|
2623
|
+
if (cleanupCreatedPath) await fs$1.rm(cleanupPath, { force: true }).catch(() => {});
|
|
2624
|
+
throw err;
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
async function appendFileWithinRoot(params) {
|
|
2628
|
+
const target = await openWritableFileWithinRoot({
|
|
2629
|
+
rootDir: params.rootDir,
|
|
2630
|
+
relativePath: params.relativePath,
|
|
2631
|
+
mkdir: params.mkdir,
|
|
2632
|
+
truncateExisting: false,
|
|
2633
|
+
append: true
|
|
2634
|
+
});
|
|
2635
|
+
try {
|
|
2636
|
+
let prefix = "";
|
|
2637
|
+
if (params.prependNewlineIfNeeded === true && !target.createdForWrite && target.openedStat.size > 0 && (typeof params.data === "string" && !params.data.startsWith("\n") || Buffer.isBuffer(params.data) && params.data.length > 0 && params.data[0] !== 10)) {
|
|
2638
|
+
const lastByte = Buffer.alloc(1);
|
|
2639
|
+
const { bytesRead } = await target.handle.read(lastByte, 0, 1, target.openedStat.size - 1);
|
|
2640
|
+
if (bytesRead === 1 && lastByte[0] !== 10) prefix = "\n";
|
|
2641
|
+
}
|
|
2642
|
+
if (typeof params.data === "string") {
|
|
2643
|
+
await target.handle.appendFile(`${prefix}${params.data}`, params.encoding ?? "utf8");
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
const payload = prefix.length > 0 ? Buffer.concat([Buffer.from(prefix, "utf8"), params.data]) : params.data;
|
|
2647
|
+
await target.handle.appendFile(payload);
|
|
2648
|
+
} finally {
|
|
2649
|
+
await target.handle.close().catch(() => {});
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
async function writeFileWithinRoot(params) {
|
|
2653
|
+
const target = await openWritableFileWithinRoot({
|
|
2654
|
+
rootDir: params.rootDir,
|
|
2655
|
+
relativePath: params.relativePath,
|
|
2656
|
+
mkdir: params.mkdir,
|
|
2657
|
+
truncateExisting: false
|
|
2658
|
+
});
|
|
2659
|
+
const destinationPath = target.openedRealPath;
|
|
2660
|
+
const targetMode = target.openedStat.mode & 511;
|
|
2661
|
+
await target.handle.close().catch(() => {});
|
|
2662
|
+
let tempPath = null;
|
|
2663
|
+
try {
|
|
2664
|
+
tempPath = buildAtomicWriteTempPath(destinationPath);
|
|
2665
|
+
const writtenStat = await writeTempFileForAtomicReplace({
|
|
2666
|
+
tempPath,
|
|
2667
|
+
data: params.data,
|
|
2668
|
+
encoding: params.encoding,
|
|
2669
|
+
mode: targetMode || 384
|
|
2670
|
+
});
|
|
2671
|
+
await fs$1.rename(tempPath, destinationPath);
|
|
2672
|
+
tempPath = null;
|
|
2673
|
+
try {
|
|
2674
|
+
await verifyAtomicWriteResult({
|
|
2675
|
+
rootDir: params.rootDir,
|
|
2676
|
+
targetPath: destinationPath,
|
|
2677
|
+
expectedStat: writtenStat
|
|
2678
|
+
});
|
|
2679
|
+
} catch (err) {
|
|
2680
|
+
emitWriteBoundaryWarning(`post-write verification failed: ${String(err)}`);
|
|
2681
|
+
throw err;
|
|
2682
|
+
}
|
|
2683
|
+
} finally {
|
|
2684
|
+
if (tempPath) await fs$1.rm(tempPath, { force: true }).catch(() => {});
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
async function copyFileWithinRoot(params) {
|
|
2688
|
+
const source = await openVerifiedLocalFile(params.sourcePath, { rejectHardlinks: params.rejectSourceHardlinks });
|
|
2689
|
+
if (params.maxBytes !== void 0 && source.stat.size > params.maxBytes) {
|
|
2690
|
+
await source.handle.close().catch(() => {});
|
|
2691
|
+
throw new SafeOpenError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${source.stat.size})`);
|
|
2692
|
+
}
|
|
2693
|
+
let target = null;
|
|
2694
|
+
let sourceClosedByStream = false;
|
|
2695
|
+
let targetClosedByUs = false;
|
|
2696
|
+
let tempHandle = null;
|
|
2697
|
+
let tempPath = null;
|
|
2698
|
+
let tempClosedByStream = false;
|
|
2699
|
+
try {
|
|
2700
|
+
target = await openWritableFileWithinRoot({
|
|
2701
|
+
rootDir: params.rootDir,
|
|
2702
|
+
relativePath: params.relativePath,
|
|
2703
|
+
mkdir: params.mkdir,
|
|
2704
|
+
truncateExisting: false
|
|
2705
|
+
});
|
|
2706
|
+
const destinationPath = target.openedRealPath;
|
|
2707
|
+
const targetMode = target.openedStat.mode & 511;
|
|
2708
|
+
await target.handle.close().catch(() => {});
|
|
2709
|
+
targetClosedByUs = true;
|
|
2710
|
+
tempPath = buildAtomicWriteTempPath(destinationPath);
|
|
2711
|
+
tempHandle = await fs$1.open(tempPath, OPEN_WRITE_CREATE_FLAGS, targetMode || 384);
|
|
2712
|
+
const sourceStream = source.handle.createReadStream();
|
|
2713
|
+
const targetStream = tempHandle.createWriteStream();
|
|
2714
|
+
sourceStream.once("close", () => {
|
|
2715
|
+
sourceClosedByStream = true;
|
|
2716
|
+
});
|
|
2717
|
+
targetStream.once("close", () => {
|
|
2718
|
+
tempClosedByStream = true;
|
|
2719
|
+
});
|
|
2720
|
+
await pipeline(sourceStream, targetStream);
|
|
2721
|
+
const writtenStat = await fs$1.stat(tempPath);
|
|
2722
|
+
if (!tempClosedByStream) {
|
|
2723
|
+
await tempHandle.close().catch(() => {});
|
|
2724
|
+
tempClosedByStream = true;
|
|
2725
|
+
}
|
|
2726
|
+
tempHandle = null;
|
|
2727
|
+
await fs$1.rename(tempPath, destinationPath);
|
|
2728
|
+
tempPath = null;
|
|
2729
|
+
try {
|
|
2730
|
+
await verifyAtomicWriteResult({
|
|
2731
|
+
rootDir: params.rootDir,
|
|
2732
|
+
targetPath: destinationPath,
|
|
2733
|
+
expectedStat: writtenStat
|
|
2734
|
+
});
|
|
2735
|
+
} catch (err) {
|
|
2736
|
+
emitWriteBoundaryWarning(`post-copy verification failed: ${String(err)}`);
|
|
2737
|
+
throw err;
|
|
2738
|
+
}
|
|
2739
|
+
} catch (err) {
|
|
2740
|
+
if (target?.createdForWrite) await fs$1.rm(target.openedRealPath, { force: true }).catch(() => {});
|
|
2741
|
+
throw err;
|
|
2742
|
+
} finally {
|
|
2743
|
+
if (tempPath) await fs$1.rm(tempPath, { force: true }).catch(() => {});
|
|
2744
|
+
if (!sourceClosedByStream) await source.handle.close().catch(() => {});
|
|
2745
|
+
if (tempHandle && !tempClosedByStream) await tempHandle.close().catch(() => {});
|
|
2746
|
+
if (target && !targetClosedByUs) await target.handle.close().catch(() => {});
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
async function writeFileFromPathWithinRoot(params) {
|
|
2750
|
+
await copyFileWithinRoot({
|
|
2751
|
+
sourcePath: params.sourcePath,
|
|
2752
|
+
rootDir: params.rootDir,
|
|
2753
|
+
relativePath: params.relativePath,
|
|
2754
|
+
mkdir: params.mkdir,
|
|
2755
|
+
rejectSourceHardlinks: true
|
|
2756
|
+
});
|
|
2757
|
+
}
|
|
2758
|
+
//#endregion
|
|
2759
|
+
//#region src/plugin-sdk/windows-spawn.ts
|
|
2760
|
+
function isFilePath(candidate) {
|
|
2761
|
+
try {
|
|
2762
|
+
return statSync(candidate).isFile();
|
|
2763
|
+
} catch {
|
|
2764
|
+
return false;
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
function resolveWindowsExecutablePath(command, env) {
|
|
2768
|
+
if (command.includes("/") || command.includes("\\") || path.isAbsolute(command)) return command;
|
|
2769
|
+
const pathEntries = (env.PATH ?? env.Path ?? process.env.PATH ?? process.env.Path ?? "").split(";").map((entry) => entry.trim()).filter(Boolean);
|
|
2770
|
+
const hasExtension = path.extname(command).length > 0;
|
|
2771
|
+
const pathExtRaw = env.PATHEXT ?? env.Pathext ?? process.env.PATHEXT ?? process.env.Pathext ?? ".EXE;.CMD;.BAT;.COM";
|
|
2772
|
+
const pathExt = hasExtension ? [""] : pathExtRaw.split(";").map((ext) => ext.trim()).filter(Boolean).map((ext) => ext.startsWith(".") ? ext : `.${ext}`);
|
|
2773
|
+
for (const dir of pathEntries) for (const ext of pathExt) for (const candidateExt of [
|
|
2774
|
+
ext,
|
|
2775
|
+
ext.toLowerCase(),
|
|
2776
|
+
ext.toUpperCase()
|
|
2777
|
+
]) {
|
|
2778
|
+
const candidate = path.join(dir, `${command}${candidateExt}`);
|
|
2779
|
+
if (isFilePath(candidate)) return candidate;
|
|
2780
|
+
}
|
|
2781
|
+
return command;
|
|
2782
|
+
}
|
|
2783
|
+
function resolveEntrypointFromCmdShim(wrapperPath) {
|
|
2784
|
+
if (!isFilePath(wrapperPath)) return null;
|
|
2785
|
+
try {
|
|
2786
|
+
const content = readFileSync(wrapperPath, "utf8");
|
|
2787
|
+
const candidates = [];
|
|
2788
|
+
for (const match of content.matchAll(/"([^"\r\n]*)"/g)) {
|
|
2789
|
+
const relative = (match[1] ?? "").match(/%~?dp0%?\s*[\\/]*(.*)$/i)?.[1]?.trim();
|
|
2790
|
+
if (!relative) continue;
|
|
2791
|
+
const normalizedRelative = relative.replace(/[\\/]+/g, path.sep).replace(/^[\\/]+/, "");
|
|
2792
|
+
const candidate = path.resolve(path.dirname(wrapperPath), normalizedRelative);
|
|
2793
|
+
if (isFilePath(candidate)) candidates.push(candidate);
|
|
2794
|
+
}
|
|
2795
|
+
return candidates.find((candidate) => {
|
|
2796
|
+
const base = path.basename(candidate).toLowerCase();
|
|
2797
|
+
return base !== "node.exe" && base !== "node";
|
|
2798
|
+
}) ?? null;
|
|
2799
|
+
} catch {
|
|
2800
|
+
return null;
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
function resolveBinEntry(packageName, binField) {
|
|
2804
|
+
if (typeof binField === "string") return binField.trim() || null;
|
|
2805
|
+
if (!binField || typeof binField !== "object") return null;
|
|
2806
|
+
if (packageName) {
|
|
2807
|
+
const preferred = binField[packageName];
|
|
2808
|
+
if (typeof preferred === "string" && preferred.trim()) return preferred.trim();
|
|
2809
|
+
}
|
|
2810
|
+
for (const value of Object.values(binField)) if (typeof value === "string" && value.trim()) return value.trim();
|
|
2811
|
+
return null;
|
|
2812
|
+
}
|
|
2813
|
+
function resolveEntrypointFromPackageJson(wrapperPath, packageName) {
|
|
2814
|
+
if (!packageName) return null;
|
|
2815
|
+
const wrapperDir = path.dirname(wrapperPath);
|
|
2816
|
+
const packageDirs = [path.resolve(wrapperDir, "..", packageName), path.resolve(wrapperDir, "node_modules", packageName)];
|
|
2817
|
+
for (const packageDir of packageDirs) {
|
|
2818
|
+
const packageJsonPath = path.join(packageDir, "package.json");
|
|
2819
|
+
if (!isFilePath(packageJsonPath)) continue;
|
|
2820
|
+
try {
|
|
2821
|
+
const entryRel = resolveBinEntry(packageName, JSON.parse(readFileSync(packageJsonPath, "utf8")).bin);
|
|
2822
|
+
if (!entryRel) continue;
|
|
2823
|
+
const entryPath = path.resolve(packageDir, entryRel);
|
|
2824
|
+
if (isFilePath(entryPath)) return entryPath;
|
|
2825
|
+
} catch {}
|
|
2826
|
+
}
|
|
2827
|
+
return null;
|
|
2828
|
+
}
|
|
2829
|
+
function resolveWindowsSpawnProgramCandidate(params) {
|
|
2830
|
+
const platform = params.platform ?? process.platform;
|
|
2831
|
+
const env = params.env ?? process.env;
|
|
2832
|
+
const execPath = params.execPath ?? process.execPath;
|
|
2833
|
+
if (platform !== "win32") return {
|
|
2834
|
+
command: params.command,
|
|
2835
|
+
leadingArgv: [],
|
|
2836
|
+
resolution: "direct"
|
|
2837
|
+
};
|
|
2838
|
+
const resolvedCommand = resolveWindowsExecutablePath(params.command, env);
|
|
2839
|
+
const ext = path.extname(resolvedCommand).toLowerCase();
|
|
2840
|
+
if (ext === ".js" || ext === ".cjs" || ext === ".mjs") return {
|
|
2841
|
+
command: execPath,
|
|
2842
|
+
leadingArgv: [resolvedCommand],
|
|
2843
|
+
resolution: "node-entrypoint",
|
|
2844
|
+
windowsHide: true
|
|
2845
|
+
};
|
|
2846
|
+
if (ext === ".cmd" || ext === ".bat") {
|
|
2847
|
+
const entrypoint = resolveEntrypointFromCmdShim(resolvedCommand) ?? resolveEntrypointFromPackageJson(resolvedCommand, params.packageName);
|
|
2848
|
+
if (entrypoint) {
|
|
2849
|
+
if (path.extname(entrypoint).toLowerCase() === ".exe") return {
|
|
2850
|
+
command: entrypoint,
|
|
2851
|
+
leadingArgv: [],
|
|
2852
|
+
resolution: "exe-entrypoint",
|
|
2853
|
+
windowsHide: true
|
|
2854
|
+
};
|
|
2855
|
+
return {
|
|
2856
|
+
command: execPath,
|
|
2857
|
+
leadingArgv: [entrypoint],
|
|
2858
|
+
resolution: "node-entrypoint",
|
|
2859
|
+
windowsHide: true
|
|
2860
|
+
};
|
|
2861
|
+
}
|
|
2862
|
+
return {
|
|
2863
|
+
command: resolvedCommand,
|
|
2864
|
+
leadingArgv: [],
|
|
2865
|
+
resolution: "unresolved-wrapper"
|
|
2866
|
+
};
|
|
2867
|
+
}
|
|
2868
|
+
return {
|
|
2869
|
+
command: resolvedCommand,
|
|
2870
|
+
leadingArgv: [],
|
|
2871
|
+
resolution: "direct"
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
function applyWindowsSpawnProgramPolicy(params) {
|
|
2875
|
+
if (params.candidate.resolution !== "unresolved-wrapper") return {
|
|
2876
|
+
command: params.candidate.command,
|
|
2877
|
+
leadingArgv: params.candidate.leadingArgv,
|
|
2878
|
+
resolution: params.candidate.resolution,
|
|
2879
|
+
windowsHide: params.candidate.windowsHide
|
|
2880
|
+
};
|
|
2881
|
+
if (params.allowShellFallback !== false) return {
|
|
2882
|
+
command: params.candidate.command,
|
|
2883
|
+
leadingArgv: [],
|
|
2884
|
+
resolution: "shell-fallback",
|
|
2885
|
+
shell: true
|
|
2886
|
+
};
|
|
2887
|
+
throw new Error(`${path.basename(params.candidate.command)} wrapper resolved, but no executable/Node entrypoint could be resolved without shell execution.`);
|
|
2888
|
+
}
|
|
2889
|
+
function resolveWindowsSpawnProgram(params) {
|
|
2890
|
+
return applyWindowsSpawnProgramPolicy({
|
|
2891
|
+
candidate: resolveWindowsSpawnProgramCandidate(params),
|
|
2892
|
+
allowShellFallback: params.allowShellFallback
|
|
2893
|
+
});
|
|
2894
|
+
}
|
|
2895
|
+
function materializeWindowsSpawnProgram(program, argv) {
|
|
2896
|
+
return {
|
|
2897
|
+
command: program.command,
|
|
2898
|
+
argv: [...program.leadingArgv, ...argv],
|
|
2899
|
+
resolution: program.resolution,
|
|
2900
|
+
shell: program.shell,
|
|
2901
|
+
windowsHide: program.windowsHide
|
|
2902
|
+
};
|
|
2903
|
+
}
|
|
2904
|
+
//#endregion
|
|
2905
|
+
//#region src/config/sessions/paths.ts
|
|
2906
|
+
function resolveAgentSessionsDir(agentId, env = process.env, homedir = () => resolveRequiredHomeDir(env, os.homedir)) {
|
|
2907
|
+
const root = resolveStateDir(env, homedir);
|
|
2908
|
+
const id = normalizeAgentId(agentId ?? "main");
|
|
2909
|
+
return path.join(root, "agents", id, "sessions");
|
|
2910
|
+
}
|
|
2911
|
+
function resolveSessionTranscriptsDirForAgent(agentId, env = process.env, homedir = () => resolveRequiredHomeDir(env, os.homedir)) {
|
|
2912
|
+
return resolveAgentSessionsDir(agentId, env, homedir);
|
|
2913
|
+
}
|
|
2914
|
+
function resolveDefaultSessionStorePath(agentId) {
|
|
2915
|
+
return path.join(resolveAgentSessionsDir(agentId), "sessions.json");
|
|
2916
|
+
}
|
|
2917
|
+
const MULTI_STORE_PATH_SENTINEL = "(multiple)";
|
|
2918
|
+
function resolveSessionFilePathOptions(params) {
|
|
2919
|
+
const agentId = params.agentId?.trim();
|
|
2920
|
+
const storePath = params.storePath?.trim();
|
|
2921
|
+
if (storePath && storePath !== MULTI_STORE_PATH_SENTINEL) {
|
|
2922
|
+
const sessionsDir = path.dirname(path.resolve(storePath));
|
|
2923
|
+
return agentId ? {
|
|
2924
|
+
sessionsDir,
|
|
2925
|
+
agentId
|
|
2926
|
+
} : { sessionsDir };
|
|
2927
|
+
}
|
|
2928
|
+
if (agentId) return { agentId };
|
|
2929
|
+
}
|
|
2930
|
+
const SAFE_SESSION_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
|
|
2931
|
+
function validateSessionId(sessionId) {
|
|
2932
|
+
const trimmed = sessionId.trim();
|
|
2933
|
+
if (!SAFE_SESSION_ID_RE.test(trimmed)) throw new Error(`Invalid session ID: ${sessionId}`);
|
|
2934
|
+
return trimmed;
|
|
2935
|
+
}
|
|
2936
|
+
function resolveSessionsDir(opts) {
|
|
2937
|
+
const sessionsDir = opts?.sessionsDir?.trim();
|
|
2938
|
+
if (sessionsDir) return path.resolve(sessionsDir);
|
|
2939
|
+
return resolveAgentSessionsDir(opts?.agentId);
|
|
2940
|
+
}
|
|
2941
|
+
function resolvePathFromAgentSessionsDir(agentSessionsDir, candidateAbsPath) {
|
|
2942
|
+
const agentBase = safeRealpathSync(path.resolve(agentSessionsDir)) ?? path.resolve(agentSessionsDir);
|
|
2943
|
+
const realCandidate = safeRealpathSync(candidateAbsPath) ?? candidateAbsPath;
|
|
2944
|
+
const relative = path.relative(agentBase, realCandidate);
|
|
2945
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) return;
|
|
2946
|
+
return path.resolve(agentBase, relative);
|
|
2947
|
+
}
|
|
2948
|
+
function resolveSiblingAgentSessionsDir(baseSessionsDir, agentId) {
|
|
2949
|
+
const resolvedBase = path.resolve(baseSessionsDir);
|
|
2950
|
+
if (path.basename(resolvedBase) !== "sessions") return;
|
|
2951
|
+
const baseAgentDir = path.dirname(resolvedBase);
|
|
2952
|
+
const baseAgentsDir = path.dirname(baseAgentDir);
|
|
2953
|
+
if (path.basename(baseAgentsDir) !== "agents") return;
|
|
2954
|
+
const rootDir = path.dirname(baseAgentsDir);
|
|
2955
|
+
return path.join(rootDir, "agents", normalizeAgentId(agentId), "sessions");
|
|
2956
|
+
}
|
|
2957
|
+
function resolveAgentSessionsPathParts(candidateAbsPath) {
|
|
2958
|
+
const parts = path.normalize(path.resolve(candidateAbsPath)).split(path.sep).filter(Boolean);
|
|
2959
|
+
const sessionsIndex = parts.lastIndexOf("sessions");
|
|
2960
|
+
if (sessionsIndex < 2 || parts[sessionsIndex - 2] !== "agents") return null;
|
|
2961
|
+
return {
|
|
2962
|
+
parts,
|
|
2963
|
+
sessionsIndex
|
|
2964
|
+
};
|
|
2965
|
+
}
|
|
2966
|
+
function extractAgentIdFromAbsoluteSessionPath(candidateAbsPath) {
|
|
2967
|
+
const parsed = resolveAgentSessionsPathParts(candidateAbsPath);
|
|
2968
|
+
if (!parsed) return;
|
|
2969
|
+
const { parts, sessionsIndex } = parsed;
|
|
2970
|
+
return parts[sessionsIndex - 1] || void 0;
|
|
2971
|
+
}
|
|
2972
|
+
function resolveStructuralSessionFallbackPath(candidateAbsPath, expectedAgentId) {
|
|
2973
|
+
const parsed = resolveAgentSessionsPathParts(candidateAbsPath);
|
|
2974
|
+
if (!parsed) return;
|
|
2975
|
+
const { parts, sessionsIndex } = parsed;
|
|
2976
|
+
const agentIdPart = parts[sessionsIndex - 1];
|
|
2977
|
+
if (!agentIdPart) return;
|
|
2978
|
+
const normalizedAgentId = normalizeAgentId(agentIdPart);
|
|
2979
|
+
if (normalizedAgentId !== agentIdPart.toLowerCase()) return;
|
|
2980
|
+
if (normalizedAgentId !== normalizeAgentId(expectedAgentId)) return;
|
|
2981
|
+
const relativeSegments = parts.slice(sessionsIndex + 1);
|
|
2982
|
+
if (relativeSegments.length !== 1) return;
|
|
2983
|
+
const fileName = relativeSegments[0];
|
|
2984
|
+
if (!fileName || fileName === "." || fileName === "..") return;
|
|
2985
|
+
return path.normalize(path.resolve(candidateAbsPath));
|
|
2986
|
+
}
|
|
2987
|
+
function safeRealpathSync(filePath) {
|
|
2988
|
+
try {
|
|
2989
|
+
return fs.realpathSync(filePath);
|
|
2990
|
+
} catch {
|
|
2991
|
+
return;
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
function resolvePathWithinSessionsDir(sessionsDir, candidate, opts) {
|
|
2995
|
+
const trimmed = candidate.trim();
|
|
2996
|
+
if (!trimmed) throw new Error("Session file path must not be empty");
|
|
2997
|
+
const resolvedBase = path.resolve(sessionsDir);
|
|
2998
|
+
const realBase = safeRealpathSync(resolvedBase) ?? resolvedBase;
|
|
2999
|
+
const realTrimmed = path.isAbsolute(trimmed) ? safeRealpathSync(trimmed) ?? trimmed : trimmed;
|
|
3000
|
+
const normalized = path.isAbsolute(realTrimmed) ? path.relative(realBase, realTrimmed) : realTrimmed;
|
|
3001
|
+
if (normalized.startsWith("..") && path.isAbsolute(realTrimmed)) {
|
|
3002
|
+
const tryAgentFallback = (agentId) => {
|
|
3003
|
+
const normalizedAgentId = normalizeAgentId(agentId);
|
|
3004
|
+
const siblingSessionsDir = resolveSiblingAgentSessionsDir(realBase, normalizedAgentId);
|
|
3005
|
+
if (siblingSessionsDir) {
|
|
3006
|
+
const siblingResolved = resolvePathFromAgentSessionsDir(siblingSessionsDir, realTrimmed);
|
|
3007
|
+
if (siblingResolved) return siblingResolved;
|
|
3008
|
+
}
|
|
3009
|
+
return resolvePathFromAgentSessionsDir(resolveAgentSessionsDir(normalizedAgentId), realTrimmed);
|
|
3010
|
+
};
|
|
3011
|
+
const explicitAgentId = opts?.agentId?.trim();
|
|
3012
|
+
if (explicitAgentId) {
|
|
3013
|
+
const resolvedFromAgent = tryAgentFallback(explicitAgentId);
|
|
3014
|
+
if (resolvedFromAgent) return resolvedFromAgent;
|
|
3015
|
+
}
|
|
3016
|
+
const extractedAgentId = extractAgentIdFromAbsoluteSessionPath(realTrimmed);
|
|
3017
|
+
if (extractedAgentId) {
|
|
3018
|
+
const resolvedFromPath = tryAgentFallback(extractedAgentId);
|
|
3019
|
+
if (resolvedFromPath) return resolvedFromPath;
|
|
3020
|
+
const structuralFallback = resolveStructuralSessionFallbackPath(realTrimmed, extractedAgentId);
|
|
3021
|
+
if (structuralFallback) return structuralFallback;
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
if (!normalized || normalized.startsWith("..") || path.isAbsolute(normalized)) throw new Error("Session file path must be within sessions directory");
|
|
3025
|
+
return path.resolve(realBase, normalized);
|
|
3026
|
+
}
|
|
3027
|
+
function resolveSessionTranscriptPathInDir(sessionId, sessionsDir, topicId) {
|
|
3028
|
+
const safeSessionId = validateSessionId(sessionId);
|
|
3029
|
+
const safeTopicId = typeof topicId === "string" ? encodeURIComponent(topicId) : typeof topicId === "number" ? String(topicId) : void 0;
|
|
3030
|
+
return resolvePathWithinSessionsDir(sessionsDir, safeTopicId !== void 0 ? `${safeSessionId}-topic-${safeTopicId}.jsonl` : `${safeSessionId}.jsonl`);
|
|
3031
|
+
}
|
|
3032
|
+
function resolveSessionTranscriptPath(sessionId, agentId, topicId) {
|
|
3033
|
+
return resolveSessionTranscriptPathInDir(sessionId, resolveAgentSessionsDir(agentId), topicId);
|
|
3034
|
+
}
|
|
3035
|
+
function resolveSessionFilePath(sessionId, entry, opts) {
|
|
3036
|
+
const sessionsDir = resolveSessionsDir(opts);
|
|
3037
|
+
const candidate = entry?.sessionFile?.trim();
|
|
3038
|
+
if (candidate) try {
|
|
3039
|
+
return resolvePathWithinSessionsDir(sessionsDir, candidate, { agentId: opts?.agentId });
|
|
3040
|
+
} catch {}
|
|
3041
|
+
return resolveSessionTranscriptPathInDir(sessionId, sessionsDir);
|
|
3042
|
+
}
|
|
3043
|
+
function resolveStorePath(store, opts) {
|
|
3044
|
+
const agentId = normalizeAgentId(opts?.agentId ?? "main");
|
|
3045
|
+
if (!store) return resolveDefaultSessionStorePath(agentId);
|
|
3046
|
+
if (store.includes("{agentId}")) {
|
|
3047
|
+
const expanded = store.replaceAll("{agentId}", agentId);
|
|
3048
|
+
if (expanded.startsWith("~")) return path.resolve(expandHomePrefix(expanded, {
|
|
3049
|
+
home: resolveRequiredHomeDir(process.env, os.homedir),
|
|
3050
|
+
env: process.env,
|
|
3051
|
+
homedir: os.homedir
|
|
3052
|
+
}));
|
|
3053
|
+
return path.resolve(expanded);
|
|
3054
|
+
}
|
|
3055
|
+
if (store.startsWith("~")) return path.resolve(expandHomePrefix(store, {
|
|
3056
|
+
home: resolveRequiredHomeDir(process.env, os.homedir),
|
|
3057
|
+
env: process.env,
|
|
3058
|
+
homedir: os.homedir
|
|
3059
|
+
}));
|
|
3060
|
+
return path.resolve(store);
|
|
3061
|
+
}
|
|
3062
|
+
//#endregion
|
|
3063
|
+
export { loadWorkspaceBootstrapFiles as $, runTasksWithConcurrency as A, normalizeMainKey as At, resolveRunModelFallbacksOverride as B, getSubagentDepth as Bt, PATH_ALIAS_POLICIES as C, DEFAULT_MAIN_KEY as Ct, redactToolDetail as D, classifySessionKeyShape as Dt, redactSensitiveText as E, buildGroupHistoryKey as Et, resolveAgentEffectiveModelPrimary as F, DEFAULT_ACCOUNT_ID as Ft, DEFAULT_BOOTSTRAP_FILENAME as G, resolveThreadParentSessionKey as Gt, resolveSessionAgentIds as H, isCronSessionKey as Ht, resolveAgentSkillsFilter as I, normalizeAccountId as It, DEFAULT_SOUL_FILENAME as J, DEFAULT_HEARTBEAT_FILENAME as K, resolveAgentWorkspaceDir as L, normalizeOptionalAccountId as Lt, listAgentIds as M, resolveThreadSessionKeys as Mt, resolveAgentConfig as N, sanitizeAgentId as Nt, compileSafeRegex as O, isValidAgentId as Ot, resolveAgentDir as P, scopedHeartbeatWakeOptions as Pt, filterBootstrapFilesForSession as Q, resolveDefaultAgentId as R, isBlockedObjectKey as Rt, writeFileWithinRoot as S, DEFAULT_AGENT_ID as St, getDefaultRedactPatterns as T, buildAgentPeerSessionKey as Tt, DEFAULT_AGENTS_FILENAME as U, isSubagentSessionKey as Ut, resolveSessionAgentId as V, isAcpSessionKey as Vt, DEFAULT_AGENT_WORKSPACE_DIR as W, parseAgentSessionKey as Wt, DEFAULT_USER_FILENAME as X, DEFAULT_TOOLS_FILENAME as Y, ensureAgentWorkspace as Z, openFileWithinRoot as _, normalizeStringEntriesLower as _t, resolveSessionTranscriptPathInDir as a, canUseBoundaryFileOpen as at, readLocalFileSafely as b, toAgentModelListLike as bt, applyWindowsSpawnProgramPolicy as c, sameFileIdentity$1 as ct, resolveWindowsSpawnProgram as d, isPathInside as dt, resolveOpenClawPackageRoot as et, resolveWindowsSpawnProgramCandidate as f, normalizeWindowsPathForComparison as ft, createRootScopedReadFile as g, normalizeStringEntries as gt, copyFileWithinRoot as h, normalizeHyphenSlug as ht, resolveSessionTranscriptPath as i, spawnWithFallback as it, hasConfiguredModelFallbacks as j, resolveAgentIdFromSessionKey as jt, testRegexWithBoundedInput as k, normalizeAgentId as kt, materializeWindowsSpawnProgram as l, resolvePathViaExistingAncestorSync as lt, appendFileWithinRoot as m, normalizeAtHashSlug as mt, resolveSessionFilePath as n, runCommandWithTimeout as nt, resolveSessionTranscriptsDirForAgent as o, openBoundaryFile as ot, SafeOpenError as p, normalizeSkillFilter as pt, DEFAULT_IDENTITY_FILENAME as q, resolveSessionFilePathOptions as r, runExec as rt, resolveStorePath as s, openBoundaryFileSync as st, resolveDefaultSessionStorePath as t, resolveOpenClawPackageRootSync as tt, resolveWindowsExecutablePath as u, isNotFoundPathError as ut, openWritableFileWithinRoot as v, resolveAgentModelFallbackValues as vt, assertNoPathAliasEscape as w, buildAgentMainSessionKey as wt, writeFileFromPathWithinRoot as x, markOpenClawExecEnv as xt, readFileWithinRoot as y, resolveAgentModelPrimaryValue as yt, resolveEffectiveModelFallbacks as z, deriveSessionChatType as zt };
|