wayang-ai 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1464 -233
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -683,6 +683,7 @@ ${getStderrTail()}`));
|
|
|
683
683
|
|
|
684
684
|
// src/infra/session-helpers.ts
|
|
685
685
|
import * as fs3 from "fs";
|
|
686
|
+
import { closeSync as closeSync2, openSync as openSync2, readSync, statSync } from "fs";
|
|
686
687
|
import * as path3 from "path";
|
|
687
688
|
|
|
688
689
|
// src/infra/session-index.ts
|
|
@@ -1021,8 +1022,115 @@ function listSessions(homeDir2, query2 = {}) {
|
|
|
1021
1022
|
function getSession(homeDir2, sessionId) {
|
|
1022
1023
|
return createSessionIndex(homeDir2).get(sessionId);
|
|
1023
1024
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1025
|
+
var CONVERSATION_FILENAME = "conversation.jsonl";
|
|
1026
|
+
var ACTIVITY_WINDOW_BYTES = 64 * 1024;
|
|
1027
|
+
var MAX_ACTIVITY_WINDOW_BYTES = 2 * 1024 * 1024;
|
|
1028
|
+
function entryTimestamp(entry) {
|
|
1029
|
+
if (entry === null || typeof entry !== "object") return null;
|
|
1030
|
+
const e = entry;
|
|
1031
|
+
for (const raw of [e.timestamp, e.endedAt, e.startedAt]) {
|
|
1032
|
+
if (typeof raw === "string") {
|
|
1033
|
+
const ms = Date.parse(raw);
|
|
1034
|
+
if (Number.isFinite(ms)) return ms;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
function readTailEntryTimestamp(filePath) {
|
|
1040
|
+
let size;
|
|
1041
|
+
try {
|
|
1042
|
+
size = statSync(filePath).size;
|
|
1043
|
+
} catch {
|
|
1044
|
+
return null;
|
|
1045
|
+
}
|
|
1046
|
+
if (size === 0) return null;
|
|
1047
|
+
let windowBytes = Math.min(size, ACTIVITY_WINDOW_BYTES);
|
|
1048
|
+
for (; ; ) {
|
|
1049
|
+
const start = size - windowBytes;
|
|
1050
|
+
const chunk = Buffer.alloc(windowBytes);
|
|
1051
|
+
let fd = null;
|
|
1052
|
+
try {
|
|
1053
|
+
fd = openSync2(filePath, "r");
|
|
1054
|
+
readSync(fd, chunk, 0, windowBytes, start);
|
|
1055
|
+
} catch {
|
|
1056
|
+
return null;
|
|
1057
|
+
} finally {
|
|
1058
|
+
if (fd !== null) closeSync2(fd);
|
|
1059
|
+
}
|
|
1060
|
+
let body = chunk.toString("utf-8");
|
|
1061
|
+
if (start > 0) {
|
|
1062
|
+
const nl = body.indexOf("\n");
|
|
1063
|
+
if (nl === -1) {
|
|
1064
|
+
if (windowBytes >= MAX_ACTIVITY_WINDOW_BYTES) return null;
|
|
1065
|
+
windowBytes = Math.min(size, windowBytes * 2);
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
body = body.slice(nl + 1);
|
|
1069
|
+
}
|
|
1070
|
+
let latest = null;
|
|
1071
|
+
for (const line of body.split("\n")) {
|
|
1072
|
+
if (line.length === 0) continue;
|
|
1073
|
+
let parsed;
|
|
1074
|
+
try {
|
|
1075
|
+
parsed = JSON.parse(line);
|
|
1076
|
+
} catch {
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
const ts = entryTimestamp(parsed);
|
|
1080
|
+
if (ts !== null && (latest === null || ts > latest)) latest = ts;
|
|
1081
|
+
}
|
|
1082
|
+
if (latest !== null) return latest;
|
|
1083
|
+
if (start === 0 || windowBytes >= MAX_ACTIVITY_WINDOW_BYTES) return null;
|
|
1084
|
+
windowBytes = Math.min(size, windowBytes * 2);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
function readMetaForActivity(sessionDir) {
|
|
1088
|
+
try {
|
|
1089
|
+
const raw = fs3.readFileSync(path3.join(sessionDir, "meta.json"), "utf-8");
|
|
1090
|
+
const meta = JSON.parse(raw);
|
|
1091
|
+
return meta !== null && typeof meta === "object" ? meta : null;
|
|
1092
|
+
} catch {
|
|
1093
|
+
return null;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
function usableTimestamp(value) {
|
|
1097
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
|
|
1098
|
+
}
|
|
1099
|
+
function readSessionLastActivity(sessionDir) {
|
|
1100
|
+
const meta = readMetaForActivity(sessionDir);
|
|
1101
|
+
const candidates = [];
|
|
1102
|
+
const conversationAt = readTailEntryTimestamp(path3.join(sessionDir, CONVERSATION_FILENAME));
|
|
1103
|
+
if (conversationAt !== null) candidates.push({ at: conversationAt, source: "conversation" });
|
|
1104
|
+
const lastActiveAt = usableTimestamp(meta?.lastActiveAt);
|
|
1105
|
+
if (lastActiveAt !== null) candidates.push({ at: lastActiveAt, source: "meta.lastActiveAt" });
|
|
1106
|
+
const createdAt = usableTimestamp(meta?.createdAt);
|
|
1107
|
+
if (createdAt !== null) candidates.push({ at: createdAt, source: "meta.createdAt" });
|
|
1108
|
+
if (candidates.length === 0) return null;
|
|
1109
|
+
return candidates.reduce((best, candidate) => candidate.at > best.at ? candidate : best);
|
|
1110
|
+
}
|
|
1111
|
+
function pickMostRecentlyActive(candidates, activityOf) {
|
|
1112
|
+
let best = null;
|
|
1113
|
+
let bestAt = 0;
|
|
1114
|
+
for (const candidate of candidates) {
|
|
1115
|
+
const at = activityOf(candidate);
|
|
1116
|
+
if (best === null || at > bestAt || at === bestAt && candidate.sessionId > best.sessionId) {
|
|
1117
|
+
best = candidate;
|
|
1118
|
+
bestAt = at;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
return best;
|
|
1122
|
+
}
|
|
1123
|
+
function activityAtForEntry(entry) {
|
|
1124
|
+
const activity = readSessionLastActivity(entry.sessionDir);
|
|
1125
|
+
if (activity) return activity.at;
|
|
1126
|
+
return usableTimestamp(entry.meta.createdAt) ?? 0;
|
|
1127
|
+
}
|
|
1128
|
+
function getLatestSessionForWorkspace(homeDir2, workspaceDir2, opts = {}) {
|
|
1129
|
+
const candidates = createSessionIndex(homeDir2).list({
|
|
1130
|
+
workspace: workspaceDir2,
|
|
1131
|
+
includeArchived: opts.includeArchived
|
|
1132
|
+
});
|
|
1133
|
+
return pickMostRecentlyActive(candidates, activityAtForEntry);
|
|
1026
1134
|
}
|
|
1027
1135
|
function readSessionUnfinishedTasks(sessionDir) {
|
|
1028
1136
|
const tasksPath = path3.join(sessionDir, "tasks.json");
|
|
@@ -1303,7 +1411,7 @@ function validateConfig(config) {
|
|
|
1303
1411
|
if (!config.providers[wp]) return `worker.provider "${wp}" not found`;
|
|
1304
1412
|
const permission = config.permission;
|
|
1305
1413
|
if (permission !== void 0 && permission !== null) {
|
|
1306
|
-
const keys = ["allowedDirs", "autoAllowRoots", "deniedDirs"];
|
|
1414
|
+
const keys = ["allowedDirs", "autoAllowRoots", "sensitiveDirs", "deniedDirs"];
|
|
1307
1415
|
for (const key of keys) {
|
|
1308
1416
|
const value = permission[key];
|
|
1309
1417
|
if (value === void 0) continue;
|
|
@@ -1311,6 +1419,10 @@ function validateConfig(config) {
|
|
|
1311
1419
|
return `permission.${key} must be an array of strings (got ${JSON.stringify(value)})`;
|
|
1312
1420
|
}
|
|
1313
1421
|
}
|
|
1422
|
+
const mode = permission.sensitiveMode;
|
|
1423
|
+
if (mode !== void 0 && mode !== "ask" && mode !== "deny") {
|
|
1424
|
+
return `permission.sensitiveMode must be 'ask' or 'deny' (got ${JSON.stringify(mode)})`;
|
|
1425
|
+
}
|
|
1314
1426
|
}
|
|
1315
1427
|
return null;
|
|
1316
1428
|
}
|
|
@@ -1466,10 +1578,10 @@ import {
|
|
|
1466
1578
|
existsSync as existsSync4,
|
|
1467
1579
|
mkdirSync as mkdirSync3,
|
|
1468
1580
|
renameSync as renameSync4,
|
|
1469
|
-
openSync as
|
|
1581
|
+
openSync as openSync3,
|
|
1470
1582
|
writeSync as writeSync2,
|
|
1471
1583
|
fsyncSync as fsyncSync2,
|
|
1472
|
-
closeSync as
|
|
1584
|
+
closeSync as closeSync3,
|
|
1473
1585
|
unlinkSync as unlinkSync3,
|
|
1474
1586
|
copyFileSync
|
|
1475
1587
|
} from "fs";
|
|
@@ -1498,16 +1610,16 @@ var JSONFileHelper = class {
|
|
|
1498
1610
|
const tmp = this.tempPath();
|
|
1499
1611
|
let fd = null;
|
|
1500
1612
|
try {
|
|
1501
|
-
fd =
|
|
1613
|
+
fd = openSync3(tmp, "w");
|
|
1502
1614
|
writeSync2(fd, JSON.stringify(value, null, 2));
|
|
1503
1615
|
fsyncSync2(fd);
|
|
1504
|
-
|
|
1616
|
+
closeSync3(fd);
|
|
1505
1617
|
fd = null;
|
|
1506
1618
|
renameSync4(tmp, this.filePath);
|
|
1507
1619
|
} catch (err) {
|
|
1508
1620
|
if (fd !== null) {
|
|
1509
1621
|
try {
|
|
1510
|
-
|
|
1622
|
+
closeSync3(fd);
|
|
1511
1623
|
} catch {
|
|
1512
1624
|
}
|
|
1513
1625
|
}
|
|
@@ -1958,56 +2070,6 @@ var theme = {
|
|
|
1958
2070
|
}
|
|
1959
2071
|
};
|
|
1960
2072
|
|
|
1961
|
-
// src/tui/utils/chat-lookups.ts
|
|
1962
|
-
var EMPTY_CHAT_LOOKUPS = {
|
|
1963
|
-
permissionRequests: /* @__PURE__ */ new Map(),
|
|
1964
|
-
inquiries: /* @__PURE__ */ new Map(),
|
|
1965
|
-
workers: /* @__PURE__ */ new Map(),
|
|
1966
|
-
workerTypeEmoji: /* @__PURE__ */ new Map()
|
|
1967
|
-
};
|
|
1968
|
-
function buildChatLookups(items) {
|
|
1969
|
-
const permissionRequests = /* @__PURE__ */ new Map();
|
|
1970
|
-
const inquiries = /* @__PURE__ */ new Map();
|
|
1971
|
-
const workers = /* @__PURE__ */ new Map();
|
|
1972
|
-
const workerTypeEmoji = /* @__PURE__ */ new Map();
|
|
1973
|
-
for (const item of items) {
|
|
1974
|
-
if (item.role !== "signal") continue;
|
|
1975
|
-
if (item.workerType && item.emoji && !workerTypeEmoji.has(item.workerType)) {
|
|
1976
|
-
workerTypeEmoji.set(item.workerType, item.emoji);
|
|
1977
|
-
}
|
|
1978
|
-
let worker;
|
|
1979
|
-
if (item.workerId) {
|
|
1980
|
-
const prev = workers.get(item.workerId);
|
|
1981
|
-
worker = {
|
|
1982
|
-
workerId: item.workerId,
|
|
1983
|
-
emoji: item.emoji ?? prev?.emoji,
|
|
1984
|
-
workerType: item.workerType ?? prev?.workerType,
|
|
1985
|
-
taskTitle: item.taskTitle ?? prev?.taskTitle
|
|
1986
|
-
};
|
|
1987
|
-
workers.set(item.workerId, worker);
|
|
1988
|
-
}
|
|
1989
|
-
const requestId = item.permissionMeta?.requestId;
|
|
1990
|
-
if (item.permissionMeta && requestId) {
|
|
1991
|
-
permissionRequests.set(requestId, { meta: item.permissionMeta, ...worker && { worker } });
|
|
1992
|
-
}
|
|
1993
|
-
const inquiryId = item.inquiryMeta?.inquiryId;
|
|
1994
|
-
if (item.inquiryMeta && inquiryId) {
|
|
1995
|
-
inquiries.set(inquiryId, { meta: item.inquiryMeta, ...worker && { worker } });
|
|
1996
|
-
}
|
|
1997
|
-
}
|
|
1998
|
-
return { permissionRequests, inquiries, workers, workerTypeEmoji };
|
|
1999
|
-
}
|
|
2000
|
-
function formatWorkerTag(ctx) {
|
|
2001
|
-
const parts = [];
|
|
2002
|
-
if (ctx.taskTitle) parts.push(ctx.taskTitle);
|
|
2003
|
-
if (ctx.workerId) parts.push(`w:${ctx.workerId.slice(-4)}`);
|
|
2004
|
-
const body = parts.join(" \xB7 ");
|
|
2005
|
-
return [ctx.emoji, body].filter(Boolean).join(" ");
|
|
2006
|
-
}
|
|
2007
|
-
|
|
2008
|
-
// src/tui/components/chat-area/user-row.tsx
|
|
2009
|
-
import { Box, Text } from "ink";
|
|
2010
|
-
|
|
2011
2073
|
// src/tui/utils/text-width.ts
|
|
2012
2074
|
function charWidth(cp) {
|
|
2013
2075
|
if (cp >= 768 && cp <= 879) return 0;
|
|
@@ -2049,6 +2111,28 @@ function sliceToWidth(s, maxWidth) {
|
|
|
2049
2111
|
}
|
|
2050
2112
|
return out;
|
|
2051
2113
|
}
|
|
2114
|
+
function sliceToWidthFromEnd(s, maxWidth) {
|
|
2115
|
+
if (maxWidth <= 0) return "";
|
|
2116
|
+
const chars = Array.from(s);
|
|
2117
|
+
let w = 0;
|
|
2118
|
+
let out = "";
|
|
2119
|
+
for (let i = chars.length - 1; i >= 0; i -= 1) {
|
|
2120
|
+
const cw = charWidth(chars[i].codePointAt(0) ?? 0);
|
|
2121
|
+
if (w + cw > maxWidth) break;
|
|
2122
|
+
out = chars[i] + out;
|
|
2123
|
+
w += cw;
|
|
2124
|
+
}
|
|
2125
|
+
return out;
|
|
2126
|
+
}
|
|
2127
|
+
function truncateMiddleToWidth(s, maxWidth) {
|
|
2128
|
+
if (maxWidth <= 0) return "";
|
|
2129
|
+
if (stringWidth(s) <= maxWidth) return s;
|
|
2130
|
+
if (maxWidth === 1) return "\u2026";
|
|
2131
|
+
const keep = maxWidth - 1;
|
|
2132
|
+
const headBudget = Math.ceil(keep / 2);
|
|
2133
|
+
const tailBudget = keep - headBudget;
|
|
2134
|
+
return sliceToWidth(s, headBudget) + "\u2026" + sliceToWidthFromEnd(s, tailBudget);
|
|
2135
|
+
}
|
|
2052
2136
|
function collapseWhitespace(s) {
|
|
2053
2137
|
return s.replace(/\s+/g, " ").trim();
|
|
2054
2138
|
}
|
|
@@ -2063,7 +2147,144 @@ function shortId(id) {
|
|
|
2063
2147
|
return id.slice(-4);
|
|
2064
2148
|
}
|
|
2065
2149
|
|
|
2150
|
+
// src/tui/utils/workspace-display.ts
|
|
2151
|
+
var EMPTY_WORKSPACE_CONTEXT = { workspaces: [] };
|
|
2152
|
+
function formatWorkspaceSlug(workspace) {
|
|
2153
|
+
const slug = workspace?.slug?.trim();
|
|
2154
|
+
return slug ? slug : null;
|
|
2155
|
+
}
|
|
2156
|
+
function formatWorkspaceSummary(workspaces) {
|
|
2157
|
+
if (!workspaces || workspaces.length === 0) return null;
|
|
2158
|
+
const slugs = workspaces.map((workspace) => workspace?.slug?.trim()).filter((slug) => !!slug);
|
|
2159
|
+
if (slugs.length === 0) return null;
|
|
2160
|
+
if (slugs.length === 1) return slugs[0];
|
|
2161
|
+
return `${slugs[0]} +${slugs.length - 1}`;
|
|
2162
|
+
}
|
|
2163
|
+
function isSameWorkspacePath(a, b) {
|
|
2164
|
+
const left = a?.trim();
|
|
2165
|
+
const right = b?.trim();
|
|
2166
|
+
if (!left || !right) return false;
|
|
2167
|
+
return normalizeWorkspacePath(left) === normalizeWorkspacePath(right);
|
|
2168
|
+
}
|
|
2169
|
+
function resolveWorkspaceSlug(workspace) {
|
|
2170
|
+
const slug = workspace?.slug?.trim();
|
|
2171
|
+
if (slug) return slug;
|
|
2172
|
+
const path9 = workspace?.path?.trim();
|
|
2173
|
+
return path9 ? workspaceSlugFromPath(path9) : null;
|
|
2174
|
+
}
|
|
2175
|
+
function formatTaskWorkspaceMark(workspace, defaultWorkspacePath) {
|
|
2176
|
+
const slug = resolveWorkspaceSlug(workspace);
|
|
2177
|
+
if (!slug) return "";
|
|
2178
|
+
if (defaultWorkspacePath && isSameWorkspacePath(workspace?.path, defaultWorkspacePath)) {
|
|
2179
|
+
return "";
|
|
2180
|
+
}
|
|
2181
|
+
return ` \u2014 ${slug}`;
|
|
2182
|
+
}
|
|
2183
|
+
function resolveDeclaredWorkspaceSlug(declaredPath, options = {}) {
|
|
2184
|
+
const raw = declaredPath?.trim();
|
|
2185
|
+
if (!raw) return null;
|
|
2186
|
+
if (options.defaultWorkspacePath && isSameWorkspacePath(raw, options.defaultWorkspacePath)) {
|
|
2187
|
+
return null;
|
|
2188
|
+
}
|
|
2189
|
+
const known = (options.workspaces ?? []).find(
|
|
2190
|
+
(workspace) => workspace?.path && isSameWorkspacePath(workspace.path, raw)
|
|
2191
|
+
);
|
|
2192
|
+
if (known) return resolveWorkspaceSlug(known);
|
|
2193
|
+
return resolveWorkspaceSlug({ path: normalizeWorkspacePath(raw) });
|
|
2194
|
+
}
|
|
2195
|
+
var PROJECT_LIST_MAX_WIDTH = 80;
|
|
2196
|
+
var PROJECT_DEFAULT_MARKER = "*";
|
|
2197
|
+
var PROJECT_DEFAULT_LEGEND = "* = default workspace";
|
|
2198
|
+
function collectProjectEntries(input2) {
|
|
2199
|
+
const defaultPath = input2.defaultWorkspacePath?.trim();
|
|
2200
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2201
|
+
const entries = [];
|
|
2202
|
+
const candidates = [
|
|
2203
|
+
...input2.workspaces ?? [],
|
|
2204
|
+
...input2.taskWorkspaces ?? [],
|
|
2205
|
+
...defaultPath ? [{ path: defaultPath }] : []
|
|
2206
|
+
];
|
|
2207
|
+
for (const candidate of candidates) {
|
|
2208
|
+
const rawPath = candidate?.path?.trim();
|
|
2209
|
+
if (!rawPath) continue;
|
|
2210
|
+
const canonical = normalizeWorkspacePath(rawPath);
|
|
2211
|
+
if (seen.has(canonical)) continue;
|
|
2212
|
+
const slug = resolveWorkspaceSlug({ ...candidate, path: rawPath });
|
|
2213
|
+
if (!slug) continue;
|
|
2214
|
+
seen.add(canonical);
|
|
2215
|
+
entries.push({
|
|
2216
|
+
slug,
|
|
2217
|
+
path: rawPath,
|
|
2218
|
+
isDefault: defaultPath !== void 0 && isSameWorkspacePath(rawPath, defaultPath)
|
|
2219
|
+
});
|
|
2220
|
+
}
|
|
2221
|
+
const defaultEntry = entries.find((entry) => entry.isDefault);
|
|
2222
|
+
return defaultEntry ? [defaultEntry, ...entries.filter((entry) => entry !== defaultEntry)] : entries;
|
|
2223
|
+
}
|
|
2224
|
+
function formatProjectList(input2, options = {}) {
|
|
2225
|
+
const maxWidth = options.maxWidth ?? PROJECT_LIST_MAX_WIDTH;
|
|
2226
|
+
const entries = collectProjectEntries(input2);
|
|
2227
|
+
if (entries.length === 0) return "No projects";
|
|
2228
|
+
const lines = entries.map((entry) => {
|
|
2229
|
+
const suffix = entry.isDefault ? ` ${PROJECT_DEFAULT_MARKER}` : "";
|
|
2230
|
+
const prefix = `${entry.slug} `;
|
|
2231
|
+
const pathBudget = Math.max(1, maxWidth - stringWidth(prefix) - stringWidth(suffix));
|
|
2232
|
+
return prefix + truncateMiddleToWidth(entry.path, pathBudget) + suffix;
|
|
2233
|
+
});
|
|
2234
|
+
if (entries.some((entry) => entry.isDefault)) lines.push(PROJECT_DEFAULT_LEGEND);
|
|
2235
|
+
return lines.join("\n");
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
// src/tui/utils/chat-lookups.ts
|
|
2239
|
+
var EMPTY_CHAT_LOOKUPS = {
|
|
2240
|
+
permissionRequests: /* @__PURE__ */ new Map(),
|
|
2241
|
+
inquiries: /* @__PURE__ */ new Map(),
|
|
2242
|
+
workers: /* @__PURE__ */ new Map(),
|
|
2243
|
+
workerTypeEmoji: /* @__PURE__ */ new Map(),
|
|
2244
|
+
workspace: EMPTY_WORKSPACE_CONTEXT
|
|
2245
|
+
};
|
|
2246
|
+
function buildChatLookups(items, workspace = EMPTY_WORKSPACE_CONTEXT) {
|
|
2247
|
+
const permissionRequests = /* @__PURE__ */ new Map();
|
|
2248
|
+
const inquiries = /* @__PURE__ */ new Map();
|
|
2249
|
+
const workers = /* @__PURE__ */ new Map();
|
|
2250
|
+
const workerTypeEmoji = /* @__PURE__ */ new Map();
|
|
2251
|
+
for (const item of items) {
|
|
2252
|
+
if (item.role !== "signal") continue;
|
|
2253
|
+
if (item.workerType && item.emoji && !workerTypeEmoji.has(item.workerType)) {
|
|
2254
|
+
workerTypeEmoji.set(item.workerType, item.emoji);
|
|
2255
|
+
}
|
|
2256
|
+
let worker;
|
|
2257
|
+
if (item.workerId) {
|
|
2258
|
+
const prev = workers.get(item.workerId);
|
|
2259
|
+
worker = {
|
|
2260
|
+
workerId: item.workerId,
|
|
2261
|
+
emoji: item.emoji ?? prev?.emoji,
|
|
2262
|
+
workerType: item.workerType ?? prev?.workerType,
|
|
2263
|
+
taskTitle: item.taskTitle ?? prev?.taskTitle
|
|
2264
|
+
};
|
|
2265
|
+
workers.set(item.workerId, worker);
|
|
2266
|
+
}
|
|
2267
|
+
const requestId = item.permissionMeta?.requestId;
|
|
2268
|
+
if (item.permissionMeta && requestId) {
|
|
2269
|
+
permissionRequests.set(requestId, { meta: item.permissionMeta, ...worker && { worker } });
|
|
2270
|
+
}
|
|
2271
|
+
const inquiryId = item.inquiryMeta?.inquiryId;
|
|
2272
|
+
if (item.inquiryMeta && inquiryId) {
|
|
2273
|
+
inquiries.set(inquiryId, { meta: item.inquiryMeta, ...worker && { worker } });
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
return { permissionRequests, inquiries, workers, workerTypeEmoji, workspace };
|
|
2277
|
+
}
|
|
2278
|
+
function formatWorkerTag(ctx) {
|
|
2279
|
+
const parts = [];
|
|
2280
|
+
if (ctx.taskTitle) parts.push(ctx.taskTitle);
|
|
2281
|
+
if (ctx.workerId) parts.push(`w:${ctx.workerId.slice(-4)}`);
|
|
2282
|
+
const body = parts.join(" \xB7 ");
|
|
2283
|
+
return [ctx.emoji, body].filter(Boolean).join(" ");
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2066
2286
|
// src/tui/components/chat-area/user-row.tsx
|
|
2287
|
+
import { Box, Text } from "ink";
|
|
2067
2288
|
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
2068
2289
|
var PREFIX = `${theme.user.prefix} `;
|
|
2069
2290
|
var PREFIX_WIDTH = stringWidth(PREFIX);
|
|
@@ -2199,15 +2420,18 @@ function toolTarget(toolName, args, budget) {
|
|
|
2199
2420
|
function requestArgs(meta) {
|
|
2200
2421
|
return meta.toolArgs && typeof meta.toolArgs === "object" && !Array.isArray(meta.toolArgs) ? meta.toolArgs : {};
|
|
2201
2422
|
}
|
|
2202
|
-
var RESOLVED_PATH_RE = /^\S+: (.*) \(resolved: (.*)\)
|
|
2423
|
+
var RESOLVED_PATH_RE = /^\S+: (.*) \(resolved: (.*?)\)(?: \[(.*)\])?$/s;
|
|
2203
2424
|
function meaningfulDescription(description) {
|
|
2204
2425
|
if (!description) return "";
|
|
2205
2426
|
const text = description.trim();
|
|
2206
2427
|
if (/^execute shell command:/i.test(text)) return "";
|
|
2207
2428
|
const m = RESOLVED_PATH_RE.exec(text);
|
|
2208
2429
|
if (m) {
|
|
2209
|
-
const [, rawPath, resolved] = m;
|
|
2210
|
-
|
|
2430
|
+
const [, rawPath, resolved, hit] = m;
|
|
2431
|
+
const parts = [];
|
|
2432
|
+
if (resolved && resolved !== rawPath) parts.push(`\u89E3\u6790\u4E3A ${resolved}`);
|
|
2433
|
+
if (hit) parts.push(`[${hit}]`);
|
|
2434
|
+
return collapseWhitespace(parts.join(" "));
|
|
2211
2435
|
}
|
|
2212
2436
|
return collapseWhitespace(text);
|
|
2213
2437
|
}
|
|
@@ -2318,11 +2542,25 @@ function summarizeWorkerMessage(argsJson, width, result2, maxLines = 3) {
|
|
|
2318
2542
|
};
|
|
2319
2543
|
}
|
|
2320
2544
|
var TASK_ID_RE = /^Task (\S+) (?:added|assigned)/;
|
|
2321
|
-
|
|
2545
|
+
var ADD_TASK_HEADER_CHROME = 27;
|
|
2546
|
+
var ADD_TASK_TAG_INSET = 1;
|
|
2547
|
+
var ADD_TASK_WORKSPACE_TAG_MAX_WIDTH = 16;
|
|
2548
|
+
function formatAddTaskWorkspaceTag(slug, maxWidth = ADD_TASK_WORKSPACE_TAG_MAX_WIDTH) {
|
|
2549
|
+
const label = slug?.trim();
|
|
2550
|
+
if (!label) return "";
|
|
2551
|
+
return truncateToWidth(` \u2014 ${label}`, maxWidth);
|
|
2552
|
+
}
|
|
2553
|
+
function summarizeAddTask(argsJson, width, result2, options = {}) {
|
|
2322
2554
|
const args = parseArgs(argsJson) ?? {};
|
|
2555
|
+
const maxLines = options.maxLines ?? 2;
|
|
2323
2556
|
const title = str(args.title) ?? "(untitled)";
|
|
2324
2557
|
const description = str(args.description) ?? "";
|
|
2325
2558
|
const { lines, hidden } = previewLines(description, maxLines, width);
|
|
2559
|
+
const declaredWorkspaceSlug = resolveDeclaredWorkspaceSlug(str(args.workspace), {
|
|
2560
|
+
defaultWorkspacePath: options.defaultWorkspacePath,
|
|
2561
|
+
workspaces: options.workspaces
|
|
2562
|
+
});
|
|
2563
|
+
const workspaceTag = formatAddTaskWorkspaceTag(declaredWorkspaceSlug);
|
|
2326
2564
|
let outcome = "pending";
|
|
2327
2565
|
let taskShortId;
|
|
2328
2566
|
let failure = "";
|
|
@@ -2336,8 +2574,11 @@ function summarizeAddTask(argsJson, width, result2, maxLines = 2) {
|
|
|
2336
2574
|
failure = failureText(result2.text, width);
|
|
2337
2575
|
}
|
|
2338
2576
|
}
|
|
2577
|
+
const tagCost = workspaceTag ? stringWidth(workspaceTag) + ADD_TASK_TAG_INSET : 0;
|
|
2578
|
+
const titleBudget = Math.max(1, width - ADD_TASK_HEADER_CHROME - tagCost);
|
|
2339
2579
|
return {
|
|
2340
|
-
title: fitWithSuffix(collapseWhitespace(title), "",
|
|
2580
|
+
title: fitWithSuffix(collapseWhitespace(title), "", titleBudget),
|
|
2581
|
+
...declaredWorkspaceSlug && { workspaceSlug: declaredWorkspaceSlug },
|
|
2341
2582
|
workerType: str(args.workerType) ?? str(args.worker_type) ?? "puppet",
|
|
2342
2583
|
...str(args.priority) && { priority: str(args.priority) },
|
|
2343
2584
|
multiStage: args.multiStage === true,
|
|
@@ -2597,8 +2838,13 @@ function AddTaskRow({ section: section2, result: result2, lookups }) {
|
|
|
2597
2838
|
const summary = summarizeAddTask(
|
|
2598
2839
|
section2.text,
|
|
2599
2840
|
SECONDARY_WIDTH,
|
|
2600
|
-
result2 ? { text: result2.text, isError: result2.isError } : void 0
|
|
2841
|
+
result2 ? { text: result2.text, isError: result2.isError } : void 0,
|
|
2842
|
+
{
|
|
2843
|
+
defaultWorkspacePath: lookups.workspace.defaultWorkspacePath,
|
|
2844
|
+
workspaces: lookups.workspace.workspaces
|
|
2845
|
+
}
|
|
2601
2846
|
);
|
|
2847
|
+
const workspaceTag = formatAddTaskWorkspaceTag(summary.workspaceSlug);
|
|
2602
2848
|
const reuse = summary.assignToWorker !== void 0;
|
|
2603
2849
|
const iconColor = summary.outcome === "failed" ? theme.action.failColor : theme.action.color;
|
|
2604
2850
|
const reusedWorker = reuse ? lookups.workers.get(summary.assignToWorker) : void 0;
|
|
@@ -2623,6 +2869,7 @@ function AddTaskRow({ section: section2, result: result2, lookups }) {
|
|
|
2623
2869
|
" #",
|
|
2624
2870
|
summary.taskShortId
|
|
2625
2871
|
] }),
|
|
2872
|
+
workspaceTag ? /* @__PURE__ */ jsx3(Text2, { color: theme.secondary.color, children: workspaceTag }) : null,
|
|
2626
2873
|
/* @__PURE__ */ jsx3(OutcomeMark, { outcome: summary.outcome, ok: "\u5DF2\u6D3E\u53D1", failed: "\u6D3E\u53D1\u5931\u8D25" })
|
|
2627
2874
|
] }),
|
|
2628
2875
|
/* @__PURE__ */ jsx3(SecondaryLine, { children: metaLine }),
|
|
@@ -2817,14 +3064,20 @@ function shouldShowGap(prev, _current) {
|
|
|
2817
3064
|
if (!prev) return false;
|
|
2818
3065
|
return true;
|
|
2819
3066
|
}
|
|
2820
|
-
var ChatArea = React2.memo(function ChatArea2({
|
|
3067
|
+
var ChatArea = React2.memo(function ChatArea2({
|
|
3068
|
+
items,
|
|
3069
|
+
busy,
|
|
3070
|
+
streamingItems,
|
|
3071
|
+
contentWidth,
|
|
3072
|
+
workspace = EMPTY_WORKSPACE_CONTEXT
|
|
3073
|
+
}) {
|
|
2821
3074
|
const width = contentWidth ?? 78;
|
|
2822
3075
|
const { staticItems, dynamicItems } = useMemo(() => {
|
|
2823
3076
|
const read = items.filter((i) => i.read);
|
|
2824
3077
|
const unread = items.filter((i) => !i.read);
|
|
2825
3078
|
return { staticItems: read, dynamicItems: unread };
|
|
2826
3079
|
}, [items]);
|
|
2827
|
-
const lookups = useMemo(() => buildChatLookups(items), [items]);
|
|
3080
|
+
const lookups = useMemo(() => buildChatLookups(items, workspace), [items, workspace]);
|
|
2828
3081
|
return /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
2829
3082
|
/* @__PURE__ */ jsx8(Static, { items: staticItems, children: (item, idx) => /* @__PURE__ */ jsx8(
|
|
2830
3083
|
Box5,
|
|
@@ -2975,29 +3228,61 @@ function renderShortcutList() {
|
|
|
2975
3228
|
return lines.join("\n");
|
|
2976
3229
|
}
|
|
2977
3230
|
|
|
2978
|
-
// src/tui/utils/
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
3231
|
+
// src/tui/utils/task-list.ts
|
|
3232
|
+
var TASK_LIST_DESCRIPTION_MAX_WIDTH = 80;
|
|
3233
|
+
var TASK_LIST_MAX_ITEMS = 20;
|
|
3234
|
+
function truncateForList(text, maxWidth = TASK_LIST_DESCRIPTION_MAX_WIDTH) {
|
|
3235
|
+
return truncateToWidth(collapseWhitespace(text), maxWidth);
|
|
2982
3236
|
}
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
return
|
|
2989
|
-
}
|
|
2990
|
-
function
|
|
2991
|
-
const
|
|
2992
|
-
const
|
|
2993
|
-
if (
|
|
2994
|
-
|
|
3237
|
+
var ACTIVE_TASK_STATUSES = /* @__PURE__ */ new Set([
|
|
3238
|
+
"pending",
|
|
3239
|
+
"running"
|
|
3240
|
+
]);
|
|
3241
|
+
function isActiveTask(task) {
|
|
3242
|
+
return ACTIVE_TASK_STATUSES.has(task.status);
|
|
3243
|
+
}
|
|
3244
|
+
function limitForList(items, max = TASK_LIST_MAX_ITEMS, options = {}) {
|
|
3245
|
+
const isActive = options.isActive ?? (() => false);
|
|
3246
|
+
const total = items.length;
|
|
3247
|
+
if (total === 0) {
|
|
3248
|
+
return { items: [], total, hidden: 0, note: null };
|
|
3249
|
+
}
|
|
3250
|
+
const activeIndices = [];
|
|
3251
|
+
const historyIndices = [];
|
|
3252
|
+
items.forEach((item, index) => {
|
|
3253
|
+
(isActive(item) ? activeIndices : historyIndices).push(index);
|
|
3254
|
+
});
|
|
3255
|
+
const historyBudget = Math.max(0, max - activeIndices.length);
|
|
3256
|
+
const keptHistory = historyBudget === 0 ? [] : historyIndices.slice(-historyBudget);
|
|
3257
|
+
const keep = /* @__PURE__ */ new Set([...activeIndices, ...keptHistory]);
|
|
3258
|
+
const kept = items.filter((_, index) => keep.has(index));
|
|
3259
|
+
const hidden = total - kept.length;
|
|
3260
|
+
if (hidden === 0) {
|
|
3261
|
+
return { items: kept, total, hidden: 0, note: null };
|
|
3262
|
+
}
|
|
3263
|
+
const note = activeIndices.length > 0 ? `${noteFor(total, kept.length)}\uFF08\u53E6\u6709 ${hidden} \u6761\u5386\u53F2\u672A\u663E\u793A\uFF09` : noteFor(total, kept.length);
|
|
3264
|
+
return { items: kept, total, hidden, note };
|
|
3265
|
+
}
|
|
3266
|
+
function noteFor(total, shown) {
|
|
3267
|
+
return `\u2026 \u663E\u793A\u6700\u8FD1 ${shown} / \u5171 ${total} \u6761`;
|
|
3268
|
+
}
|
|
3269
|
+
function formatTaskRow(task, options = {}) {
|
|
3270
|
+
const workspacePart = formatTaskWorkspaceMark(task.workspace, options.defaultWorkspacePath);
|
|
3271
|
+
const description = truncateForList(task.description, options.descriptionMaxWidth);
|
|
3272
|
+
return `[${task.status}] ${task.id}: ${description}${workspacePart}${task.result ? ` \u2192 ${task.result}` : ""}`;
|
|
3273
|
+
}
|
|
3274
|
+
function formatTaskList(tasks, options = {}) {
|
|
3275
|
+
const limited = limitForList(tasks, options.maxItems, { isActive: isActiveTask });
|
|
3276
|
+
const lines = limited.items.map((task) => formatTaskRow(task, options));
|
|
3277
|
+
if (limited.note) lines.push(limited.note);
|
|
3278
|
+
return lines.join("\n");
|
|
2995
3279
|
}
|
|
2996
3280
|
|
|
2997
3281
|
// src/tui/hooks/use-slash-commands.ts
|
|
2998
3282
|
var SLASH_COMMANDS = [
|
|
2999
3283
|
{ name: "tasks", args: "[status]", description: "List tasks" },
|
|
3000
3284
|
{ name: "workers", description: "List active workers" },
|
|
3285
|
+
{ name: "projects", description: "List session workspaces" },
|
|
3001
3286
|
{ name: "cancel", args: "<id>", description: "Cancel a task" },
|
|
3002
3287
|
{ name: "context", description: "Show context token breakdown" },
|
|
3003
3288
|
{ name: "compact", description: "Compact context" },
|
|
@@ -3018,12 +3303,21 @@ function useSlashCommands(supervisor) {
|
|
|
3018
3303
|
const status = parts[1];
|
|
3019
3304
|
const tasks = await supervisor.engine.list(status);
|
|
3020
3305
|
if (tasks.length === 0) return { handled: true, output: "No tasks" };
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3306
|
+
return {
|
|
3307
|
+
handled: true,
|
|
3308
|
+
output: formatTaskList(tasks, { defaultWorkspacePath: supervisor.ctx.workspaceDir })
|
|
3309
|
+
};
|
|
3310
|
+
}
|
|
3311
|
+
case "projects": {
|
|
3312
|
+
const tasks = await supervisor.engine.list();
|
|
3313
|
+
return {
|
|
3314
|
+
handled: true,
|
|
3315
|
+
output: formatProjectList({
|
|
3316
|
+
workspaces: supervisor.ctx.workspaces,
|
|
3317
|
+
taskWorkspaces: tasks.map((task) => task.workspace ?? {}),
|
|
3318
|
+
defaultWorkspacePath: supervisor.ctx.workspaceDir
|
|
3319
|
+
})
|
|
3320
|
+
};
|
|
3027
3321
|
}
|
|
3028
3322
|
case "workers": {
|
|
3029
3323
|
const activeWorkers = supervisor.engine.getActiveWorkers();
|
|
@@ -3057,8 +3351,9 @@ function useSlashCommands(supervisor) {
|
|
|
3057
3351
|
handled: true,
|
|
3058
3352
|
output: [
|
|
3059
3353
|
"Commands:",
|
|
3060
|
-
" /tasks [status] \u2014 List tasks",
|
|
3354
|
+
" /tasks [status] \u2014 List tasks (workspace slug when not the default)",
|
|
3061
3355
|
" /workers \u2014 List active workers",
|
|
3356
|
+
" /projects \u2014 List session workspaces (* = default workspace)",
|
|
3062
3357
|
" /cancel <id> \u2014 Cancel a task and abort its worker",
|
|
3063
3358
|
" /context \u2014 Show context token breakdown",
|
|
3064
3359
|
" /compact \u2014 Compact conversation context",
|
|
@@ -4162,6 +4457,13 @@ function ControllerPage({ onExit }) {
|
|
|
4162
4457
|
break;
|
|
4163
4458
|
}
|
|
4164
4459
|
});
|
|
4460
|
+
const workspaceContext = useMemo4(
|
|
4461
|
+
() => ({
|
|
4462
|
+
defaultWorkspacePath: supervisor.ctx.workspaceDir,
|
|
4463
|
+
workspaces: supervisor.ctx.workspaces
|
|
4464
|
+
}),
|
|
4465
|
+
[supervisor]
|
|
4466
|
+
);
|
|
4165
4467
|
const { handleCommand } = useSlashCommands(supervisor);
|
|
4166
4468
|
const handleSubmit = useMemoizedFn(async (input2) => {
|
|
4167
4469
|
let cmdResult;
|
|
@@ -4201,7 +4503,8 @@ function ControllerPage({ onExit }) {
|
|
|
4201
4503
|
items: displayItems,
|
|
4202
4504
|
busy,
|
|
4203
4505
|
streamingItems,
|
|
4204
|
-
contentWidth: chatContentWidth
|
|
4506
|
+
contentWidth: chatContentWidth,
|
|
4507
|
+
workspace: workspaceContext
|
|
4205
4508
|
}
|
|
4206
4509
|
) }),
|
|
4207
4510
|
overlay && /* @__PURE__ */ jsx13(OverlayPanel, { overlay }),
|
|
@@ -4268,7 +4571,7 @@ function AppContent() {
|
|
|
4268
4571
|
// package.json
|
|
4269
4572
|
var package_default = {
|
|
4270
4573
|
name: "wayang-ai",
|
|
4271
|
-
version: "0.8.
|
|
4574
|
+
version: "0.8.2",
|
|
4272
4575
|
description: "Multi-agent orchestration platform",
|
|
4273
4576
|
type: "module",
|
|
4274
4577
|
license: "MIT",
|
|
@@ -4395,6 +4698,36 @@ import * as fs5 from "fs";
|
|
|
4395
4698
|
import * as path6 from "path";
|
|
4396
4699
|
import * as childProcess from "child_process";
|
|
4397
4700
|
import prompts from "prompts";
|
|
4701
|
+
|
|
4702
|
+
// src/infra/permission/permission-config.ts
|
|
4703
|
+
var DEFAULT_SENSITIVE_MODE = "ask";
|
|
4704
|
+
function mergeSensitiveDirs(permission) {
|
|
4705
|
+
const entries = [...permission?.sensitiveDirs ?? [], ...permission?.deniedDirs ?? []];
|
|
4706
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4707
|
+
const merged = [];
|
|
4708
|
+
for (const entry of entries) {
|
|
4709
|
+
if (typeof entry !== "string" || entry.length === 0) continue;
|
|
4710
|
+
if (seen.has(entry)) continue;
|
|
4711
|
+
seen.add(entry);
|
|
4712
|
+
merged.push(entry);
|
|
4713
|
+
}
|
|
4714
|
+
return merged;
|
|
4715
|
+
}
|
|
4716
|
+
function resolveSensitiveMode(permission) {
|
|
4717
|
+
return permission?.sensitiveMode ?? DEFAULT_SENSITIVE_MODE;
|
|
4718
|
+
}
|
|
4719
|
+
var deprecatedDeniedDirsWarned = false;
|
|
4720
|
+
function warnDeprecatedPermissionKeys(permission, warn) {
|
|
4721
|
+
if (deprecatedDeniedDirsWarned) return;
|
|
4722
|
+
const legacy = permission?.deniedDirs;
|
|
4723
|
+
if (!Array.isArray(legacy) || legacy.length === 0) return;
|
|
4724
|
+
deprecatedDeniedDirsWarned = true;
|
|
4725
|
+
warn(
|
|
4726
|
+
"permission.deniedDirs is deprecated and will be removed in a future release \u2014 use permission.sensitiveDirs instead (both keys are read and merged)."
|
|
4727
|
+
);
|
|
4728
|
+
}
|
|
4729
|
+
|
|
4730
|
+
// src/onboard.ts
|
|
4398
4731
|
function fillApiKeys(config) {
|
|
4399
4732
|
const envKey = process.env.WAYANG_LLM_API_KEY || "";
|
|
4400
4733
|
for (const provider of Object.values(config.providers)) {
|
|
@@ -4549,6 +4882,7 @@ async function loadConfig(configPath2, options = {}) {
|
|
|
4549
4882
|
}
|
|
4550
4883
|
}
|
|
4551
4884
|
fillApiKeys(config);
|
|
4885
|
+
warnDeprecatedPermissionKeys(config.permission, (message) => console.warn(`Warning: ${message}`));
|
|
4552
4886
|
const err = validateConfig(config);
|
|
4553
4887
|
if (err) {
|
|
4554
4888
|
if (nonInteractive) throw new Error(`Invalid config: ${err}`);
|
|
@@ -5261,7 +5595,7 @@ var SignalQueue = class {
|
|
|
5261
5595
|
};
|
|
5262
5596
|
|
|
5263
5597
|
// src/services/worker-transcript.ts
|
|
5264
|
-
import { closeSync as
|
|
5598
|
+
import { closeSync as closeSync4, existsSync as existsSync8, openSync as openSync4, readSync as readSync2, statSync as statSync2 } from "fs";
|
|
5265
5599
|
import { join as join12 } from "path";
|
|
5266
5600
|
|
|
5267
5601
|
// src/services/agents/worker-state.ts
|
|
@@ -5805,7 +6139,7 @@ function readWorkerTranscriptWindow(sessionDir, workerId, opts = {}) {
|
|
|
5805
6139
|
const path9 = workerTranscriptPath(sessionDir, workerId);
|
|
5806
6140
|
let size;
|
|
5807
6141
|
try {
|
|
5808
|
-
size =
|
|
6142
|
+
size = statSync2(path9).size;
|
|
5809
6143
|
} catch {
|
|
5810
6144
|
return null;
|
|
5811
6145
|
}
|
|
@@ -5819,11 +6153,11 @@ function readWorkerTranscriptWindow(sessionDir, workerId, opts = {}) {
|
|
|
5819
6153
|
const readLen = Math.min(size, windowBytes);
|
|
5820
6154
|
const start = size - readLen;
|
|
5821
6155
|
const chunk = Buffer.alloc(readLen);
|
|
5822
|
-
const fd =
|
|
6156
|
+
const fd = openSync4(path9, "r");
|
|
5823
6157
|
try {
|
|
5824
|
-
|
|
6158
|
+
readSync2(fd, chunk, 0, readLen, start);
|
|
5825
6159
|
} finally {
|
|
5826
|
-
|
|
6160
|
+
closeSync4(fd);
|
|
5827
6161
|
}
|
|
5828
6162
|
const parsed = parseWindow(chunk.toString("utf8"), start === 0);
|
|
5829
6163
|
const enough = parsed.entries.filter(matches).length >= need;
|
|
@@ -5873,7 +6207,7 @@ function entryLabel(entry) {
|
|
|
5873
6207
|
return subtype ? `${entry.type}/${subtype}` : entry.type;
|
|
5874
6208
|
}
|
|
5875
6209
|
var TERMINAL_SUBTYPES = /* @__PURE__ */ new Set(["task_result" /* TaskResult */, "error" /* Error */]);
|
|
5876
|
-
function
|
|
6210
|
+
function entryTimestamp2(entry) {
|
|
5877
6211
|
const e = entry;
|
|
5878
6212
|
for (const raw of [e.timestamp, e.endedAt, e.startedAt]) {
|
|
5879
6213
|
if (typeof raw === "string") {
|
|
@@ -5886,7 +6220,7 @@ function entryTimestamp(entry) {
|
|
|
5886
6220
|
function summarizeTranscript(entries) {
|
|
5887
6221
|
let lastActivityAt = null;
|
|
5888
6222
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
5889
|
-
lastActivityAt =
|
|
6223
|
+
lastActivityAt = entryTimestamp2(entries[i]);
|
|
5890
6224
|
if (lastActivityAt !== null) break;
|
|
5891
6225
|
}
|
|
5892
6226
|
let currentTool = null;
|
|
@@ -7050,7 +7384,7 @@ function buildWorkerList(workers) {
|
|
|
7050
7384
|
}
|
|
7051
7385
|
return section("Available workers", lines.join("\n"));
|
|
7052
7386
|
}
|
|
7053
|
-
function buildControllerSystemPrompt(ctx, skills) {
|
|
7387
|
+
function buildControllerSystemPrompt(ctx, skills, opts) {
|
|
7054
7388
|
return assemble(
|
|
7055
7389
|
IDENTITY,
|
|
7056
7390
|
RESPONSE_STYLE,
|
|
@@ -7061,13 +7395,82 @@ function buildControllerSystemPrompt(ctx, skills) {
|
|
|
7061
7395
|
HARD_CONSTRAINTS,
|
|
7062
7396
|
buildWorkerList(ctx.config.workers),
|
|
7063
7397
|
buildSkillCatalog(skills),
|
|
7064
|
-
buildEnvironment(ctx)
|
|
7398
|
+
buildEnvironment(ctx),
|
|
7399
|
+
// Project conventions go last: they are the most specific instruction and
|
|
7400
|
+
// the last word on conflicts with the generic sections above.
|
|
7401
|
+
opts?.rules
|
|
7065
7402
|
);
|
|
7066
7403
|
}
|
|
7067
7404
|
|
|
7405
|
+
// src/services/agents/prompts/controller-rules.ts
|
|
7406
|
+
import { readFileSync as readFileSync8, realpathSync as realpathSync2, statSync as statSync3 } from "fs";
|
|
7407
|
+
import { join as join14 } from "path";
|
|
7408
|
+
var CONTROLLER_RULES_FILENAME = "AGENTS.md";
|
|
7409
|
+
var CONTROLLER_RULES_MAX_BYTES = 32 * 1024;
|
|
7410
|
+
function loadControllerRules(opts) {
|
|
7411
|
+
const maxBytes = opts.maxBytes ?? CONTROLLER_RULES_MAX_BYTES;
|
|
7412
|
+
const files = [];
|
|
7413
|
+
for (const dir of [opts.homeDir, opts.cwd]) {
|
|
7414
|
+
if (!dir) continue;
|
|
7415
|
+
const file = readRulesFile(dir);
|
|
7416
|
+
if (!file) continue;
|
|
7417
|
+
if (files.some((seen) => seen.path === file.path)) continue;
|
|
7418
|
+
files.push(file);
|
|
7419
|
+
}
|
|
7420
|
+
if (files.length === 0) return "";
|
|
7421
|
+
const bodies = [];
|
|
7422
|
+
let usedBytes = 0;
|
|
7423
|
+
let truncated = false;
|
|
7424
|
+
for (const file of files) {
|
|
7425
|
+
const heading = `## Project rules (from ${file.path})`;
|
|
7426
|
+
const size = Buffer.byteLength(file.content, "utf8");
|
|
7427
|
+
if (usedBytes + size <= maxBytes) {
|
|
7428
|
+
bodies.push(`${heading}
|
|
7429
|
+
|
|
7430
|
+
${file.content.trimEnd()}`);
|
|
7431
|
+
usedBytes += size;
|
|
7432
|
+
continue;
|
|
7433
|
+
}
|
|
7434
|
+
const budget = maxBytes - usedBytes;
|
|
7435
|
+
const head = budget > 0 ? truncateToBytes(file.content, budget).trimEnd() : "";
|
|
7436
|
+
bodies.push(head ? `${heading}
|
|
7437
|
+
|
|
7438
|
+
${head}` : heading);
|
|
7439
|
+
truncated = true;
|
|
7440
|
+
break;
|
|
7441
|
+
}
|
|
7442
|
+
const intro = `${CONTROLLER_RULES_FILENAME} content loaded once at startup. The home directory file comes first as the baseline; the working directory file comes second and takes precedence wherever the two disagree. Follow these rules unless the user explicitly overrides them.`;
|
|
7443
|
+
const parts = [`# Project rules
|
|
7444
|
+
|
|
7445
|
+
${intro}`, ...bodies];
|
|
7446
|
+
if (truncated) {
|
|
7447
|
+
parts.push(
|
|
7448
|
+
`...truncated at ${maxBytes} bytes (rules content exceeded the ${maxBytes}-byte total limit: the rest of the last file and any later files were dropped)`
|
|
7449
|
+
);
|
|
7450
|
+
}
|
|
7451
|
+
return parts.join("\n\n");
|
|
7452
|
+
}
|
|
7453
|
+
function readRulesFile(dir) {
|
|
7454
|
+
const candidate = join14(dir, CONTROLLER_RULES_FILENAME);
|
|
7455
|
+
try {
|
|
7456
|
+
const path9 = realpathSync2(candidate);
|
|
7457
|
+
if (!statSync3(path9).isFile()) return null;
|
|
7458
|
+
const content = readFileSync8(path9, "utf8");
|
|
7459
|
+
if (!content.trim()) return null;
|
|
7460
|
+
return { path: path9, content };
|
|
7461
|
+
} catch {
|
|
7462
|
+
return null;
|
|
7463
|
+
}
|
|
7464
|
+
}
|
|
7465
|
+
function truncateToBytes(text, maxBytes) {
|
|
7466
|
+
const buf = Buffer.from(text, "utf8");
|
|
7467
|
+
if (buf.length <= maxBytes) return text;
|
|
7468
|
+
return buf.subarray(0, maxBytes).toString("utf8").replace(/\uFFFD+$/, "");
|
|
7469
|
+
}
|
|
7470
|
+
|
|
7068
7471
|
// src/services/agents/prompts/project-rules.ts
|
|
7069
|
-
import { existsSync as existsSync9, readFileSync as
|
|
7070
|
-
import { basename as basename5, dirname as dirname6, join as
|
|
7472
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, statSync as statSync4 } from "fs";
|
|
7473
|
+
import { basename as basename5, dirname as dirname6, join as join15, resolve as resolve3 } from "path";
|
|
7071
7474
|
var RULE_FILE_NAMES = ["AGENTS.md", "CLAUDE.md"];
|
|
7072
7475
|
var MAX_LEVELS = 5;
|
|
7073
7476
|
var PROJECT_RULES_MAX_LENGTH = 8e3;
|
|
@@ -7076,7 +7479,7 @@ function findProjectRules(startDir) {
|
|
|
7076
7479
|
for (let level = 0; level < MAX_LEVELS; level++) {
|
|
7077
7480
|
const found = readRuleFile(current);
|
|
7078
7481
|
if (found) return found;
|
|
7079
|
-
if (existsSync9(
|
|
7482
|
+
if (existsSync9(join15(current, ".git"))) break;
|
|
7080
7483
|
const parent = dirname6(current);
|
|
7081
7484
|
if (parent === current) break;
|
|
7082
7485
|
current = parent;
|
|
@@ -7085,10 +7488,10 @@ function findProjectRules(startDir) {
|
|
|
7085
7488
|
}
|
|
7086
7489
|
function readRuleFile(dir) {
|
|
7087
7490
|
for (const name of RULE_FILE_NAMES) {
|
|
7088
|
-
const path9 =
|
|
7491
|
+
const path9 = join15(dir, name);
|
|
7089
7492
|
try {
|
|
7090
|
-
if (!
|
|
7091
|
-
const content =
|
|
7493
|
+
if (!statSync4(path9).isFile()) continue;
|
|
7494
|
+
const content = readFileSync9(path9, "utf8");
|
|
7092
7495
|
if (!content.trim()) continue;
|
|
7093
7496
|
return { path: path9, content };
|
|
7094
7497
|
} catch {
|
|
@@ -7588,9 +7991,9 @@ function toolResultBlockToRecord(block, toolName, endedAt) {
|
|
|
7588
7991
|
import { resolve as resolve5 } from "path";
|
|
7589
7992
|
|
|
7590
7993
|
// src/infra/permission/policy.ts
|
|
7591
|
-
import { realpathSync as
|
|
7592
|
-
import { basename as basename6, dirname as dirname7, isAbsolute, join as
|
|
7593
|
-
var
|
|
7994
|
+
import { realpathSync as realpathSync3 } from "fs";
|
|
7995
|
+
import { basename as basename6, dirname as dirname7, isAbsolute, join as join16, resolve as resolve4, sep } from "path";
|
|
7996
|
+
var DEFAULT_SENSITIVE_RELATIVE_PATHS = [
|
|
7594
7997
|
".ssh",
|
|
7595
7998
|
".gnupg",
|
|
7596
7999
|
".aws",
|
|
@@ -7624,12 +8027,15 @@ var SENSITIVE_SEGMENTS = /* @__PURE__ */ new Set([
|
|
|
7624
8027
|
var SENSITIVE_FILENAMES = /* @__PURE__ */ new Set([
|
|
7625
8028
|
".wayang.config.json"
|
|
7626
8029
|
]);
|
|
8030
|
+
var SENSITIVE_SEGMENT_PAIRS = [
|
|
8031
|
+
[".docker", "config.json"]
|
|
8032
|
+
];
|
|
7627
8033
|
var MAX_ANCESTOR_DEPTH = 32;
|
|
7628
8034
|
function normalizePath(p, opts = {}) {
|
|
7629
8035
|
const base = opts.base ?? process.cwd();
|
|
7630
8036
|
const lexical = isAbsolute(p) ? resolve4(p) : resolve4(base, p);
|
|
7631
8037
|
try {
|
|
7632
|
-
return
|
|
8038
|
+
return realpathSync3(lexical);
|
|
7633
8039
|
} catch {
|
|
7634
8040
|
return resolveDeepestExistingAncestor(lexical);
|
|
7635
8041
|
}
|
|
@@ -7639,8 +8045,8 @@ function resolveDeepestExistingAncestor(lexical) {
|
|
|
7639
8045
|
let candidate = lexical;
|
|
7640
8046
|
for (let depth = 0; depth <= MAX_ANCESTOR_DEPTH; depth += 1) {
|
|
7641
8047
|
try {
|
|
7642
|
-
const resolved =
|
|
7643
|
-
return peeled.length === 0 ? resolved : resolve4(
|
|
8048
|
+
const resolved = realpathSync3(candidate);
|
|
8049
|
+
return peeled.length === 0 ? resolved : resolve4(join16(resolved, ...peeled));
|
|
7644
8050
|
} catch {
|
|
7645
8051
|
}
|
|
7646
8052
|
const parent = dirname7(candidate);
|
|
@@ -7650,8 +8056,8 @@ function resolveDeepestExistingAncestor(lexical) {
|
|
|
7650
8056
|
}
|
|
7651
8057
|
return lexical;
|
|
7652
8058
|
}
|
|
7653
|
-
function
|
|
7654
|
-
return
|
|
8059
|
+
function defaultSensitivePaths(homeDir2) {
|
|
8060
|
+
return DEFAULT_SENSITIVE_RELATIVE_PATHS.map((rel) => resolve4(homeDir2, rel));
|
|
7655
8061
|
}
|
|
7656
8062
|
function isPathWithin(parent, child) {
|
|
7657
8063
|
if (parent === sep) return child.startsWith(sep);
|
|
@@ -7659,21 +8065,39 @@ function isPathWithin(parent, child) {
|
|
|
7659
8065
|
const prefix = parent.endsWith(sep) ? parent : parent + sep;
|
|
7660
8066
|
return child.startsWith(prefix);
|
|
7661
8067
|
}
|
|
7662
|
-
function isDenied(absPath,
|
|
7663
|
-
return
|
|
8068
|
+
function isDenied(absPath, sensitivePaths) {
|
|
8069
|
+
return sensitivePaths.some((sensitive) => isPathWithin(sensitive, absPath));
|
|
7664
8070
|
}
|
|
7665
|
-
function
|
|
8071
|
+
function matchSensitiveEntry(absPath) {
|
|
7666
8072
|
const segments = absPath.split(sep).filter((s) => s.length > 0);
|
|
7667
8073
|
for (let i = 0; i < segments.length; i += 1) {
|
|
7668
8074
|
const segment = segments[i];
|
|
7669
|
-
if (SENSITIVE_SEGMENTS.has(segment)
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
8075
|
+
if (SENSITIVE_SEGMENTS.has(segment)) return { entry: segment, source: "segment" };
|
|
8076
|
+
if (SENSITIVE_FILENAMES.has(segment)) return { entry: segment, source: "filename" };
|
|
8077
|
+
for (const [dir, child] of SENSITIVE_SEGMENT_PAIRS) {
|
|
8078
|
+
if (segment === dir && segments[i + 1] === child) {
|
|
8079
|
+
return { entry: `${dir}/${child}`, source: "pair" };
|
|
8080
|
+
}
|
|
7674
8081
|
}
|
|
7675
8082
|
}
|
|
7676
|
-
return
|
|
8083
|
+
return void 0;
|
|
8084
|
+
}
|
|
8085
|
+
function sensitivePathEntries(homeDir2, extraSensitiveDirs) {
|
|
8086
|
+
const builtIn = DEFAULT_SENSITIVE_RELATIVE_PATHS.map((rel) => ({
|
|
8087
|
+
absPath: normalizePath(resolve4(homeDir2, rel)),
|
|
8088
|
+
entry: rel,
|
|
8089
|
+
source: "home-default"
|
|
8090
|
+
}));
|
|
8091
|
+
const configured = (extraSensitiveDirs ?? []).filter((d) => typeof d === "string" && d.length > 0).map((d) => ({
|
|
8092
|
+
absPath: normalizePath(d, { base: homeDir2 }),
|
|
8093
|
+
entry: d,
|
|
8094
|
+
source: "config-dir"
|
|
8095
|
+
}));
|
|
8096
|
+
return [...builtIn, ...configured];
|
|
8097
|
+
}
|
|
8098
|
+
function matchSensitivePath(homeDir2, absPath, extraSensitiveDirs) {
|
|
8099
|
+
const hit = sensitivePathEntries(homeDir2, extraSensitiveDirs).find((entry) => isPathWithin(entry.absPath, absPath));
|
|
8100
|
+
return hit ? { entry: hit.entry, source: hit.source } : void 0;
|
|
7677
8101
|
}
|
|
7678
8102
|
function normalizeDirs(dirs, base) {
|
|
7679
8103
|
if (!dirs) return [];
|
|
@@ -7689,15 +8113,17 @@ function decideAccess(input2) {
|
|
|
7689
8113
|
const taskAllowedDirs = normalizeDirs(input2.taskAllowedDirs, taskWorkspace ?? homeDir2);
|
|
7690
8114
|
const configAllowedDirs = normalizeDirs(input2.configAllowedDirs, homeDir2);
|
|
7691
8115
|
const autoAllowRoots = normalizeDirs(input2.autoAllowRoots, homeDir2);
|
|
7692
|
-
const
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
8116
|
+
const sensitiveMode = input2.sensitiveMode ?? "ask";
|
|
8117
|
+
const sensitive = matchSensitiveEntry(resolvedPath) ?? matchSensitivePath(homeDir2, resolvedPath, input2.extraSensitiveDirs);
|
|
8118
|
+
if (sensitive) {
|
|
8119
|
+
const hit = { matchedEntry: sensitive.entry, matchedSource: sensitive.source, sensitive: true };
|
|
8120
|
+
if (input2.access === "write") {
|
|
8121
|
+
return { decision: "deny", matchedRule: "sensitive-list-write", resolvedPath, ...hit };
|
|
8122
|
+
}
|
|
8123
|
+
if (sensitiveMode === "deny") {
|
|
8124
|
+
return { decision: "deny", matchedRule: "sensitive-list", resolvedPath, ...hit };
|
|
8125
|
+
}
|
|
8126
|
+
return { decision: "ask", matchedRule: "sensitive-list", resolvedPath, ...hit };
|
|
7701
8127
|
}
|
|
7702
8128
|
if (findContaining(configAllowedDirs, resolvedPath) !== void 0) {
|
|
7703
8129
|
return { decision: "allow", matchedRule: "config-allowed", resolvedPath };
|
|
@@ -7717,11 +8143,403 @@ function decideAccess(input2) {
|
|
|
7717
8143
|
return { decision: "ask", matchedRule: "outside-all", resolvedPath };
|
|
7718
8144
|
}
|
|
7719
8145
|
|
|
8146
|
+
// src/services/tools/shell-lexer.ts
|
|
8147
|
+
var CMDSUBST_PLACEHOLDER = "__cmdsubst__";
|
|
8148
|
+
var DISCARD_TARGETS = /* @__PURE__ */ new Set(["/dev/null", "/dev/stdout", "/dev/stderr"]);
|
|
8149
|
+
function isDiscardRedirectTarget(target) {
|
|
8150
|
+
return target !== void 0 && DISCARD_TARGETS.has(target);
|
|
8151
|
+
}
|
|
8152
|
+
function isWordTerminator(ch) {
|
|
8153
|
+
return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "|" || ch === ";" || ch === "&" || ch === "<" || ch === ">";
|
|
8154
|
+
}
|
|
8155
|
+
function findClosingDoubleQuote(command, start) {
|
|
8156
|
+
let i = start + 1;
|
|
8157
|
+
while (i < command.length) {
|
|
8158
|
+
const ch = command[i];
|
|
8159
|
+
if (ch === "\\") {
|
|
8160
|
+
i += 2;
|
|
8161
|
+
continue;
|
|
8162
|
+
}
|
|
8163
|
+
if (ch === '"') return i;
|
|
8164
|
+
i += 1;
|
|
8165
|
+
}
|
|
8166
|
+
return -1;
|
|
8167
|
+
}
|
|
8168
|
+
function findClosingBacktick(command, start) {
|
|
8169
|
+
let i = start + 1;
|
|
8170
|
+
while (i < command.length) {
|
|
8171
|
+
const ch = command[i];
|
|
8172
|
+
if (ch === "\\") {
|
|
8173
|
+
i += 2;
|
|
8174
|
+
continue;
|
|
8175
|
+
}
|
|
8176
|
+
if (ch === "`") return i;
|
|
8177
|
+
i += 1;
|
|
8178
|
+
}
|
|
8179
|
+
return -1;
|
|
8180
|
+
}
|
|
8181
|
+
function scanBacktickSubstitution(command, start) {
|
|
8182
|
+
const end = findClosingBacktick(command, start);
|
|
8183
|
+
if (end === -1) return { error: 'unbalanced backtick \u2014 no closing "`"' };
|
|
8184
|
+
return { inner: command.slice(start + 1, end), next: end + 1 };
|
|
8185
|
+
}
|
|
8186
|
+
function scanDollarSubstitution(command, start) {
|
|
8187
|
+
const n = command.length;
|
|
8188
|
+
let i = start + 2;
|
|
8189
|
+
let depth = 1;
|
|
8190
|
+
let inner = "";
|
|
8191
|
+
while (i < n) {
|
|
8192
|
+
const ch = command[i];
|
|
8193
|
+
if (ch === "\\") {
|
|
8194
|
+
inner += command.slice(i, i + 2);
|
|
8195
|
+
i += 2;
|
|
8196
|
+
continue;
|
|
8197
|
+
}
|
|
8198
|
+
if (ch === "'") {
|
|
8199
|
+
const end = command.indexOf("'", i + 1);
|
|
8200
|
+
if (end === -1) return { error: "unterminated single quote inside command substitution" };
|
|
8201
|
+
inner += command.slice(i, end + 1);
|
|
8202
|
+
i = end + 1;
|
|
8203
|
+
continue;
|
|
8204
|
+
}
|
|
8205
|
+
if (ch === '"') {
|
|
8206
|
+
const end = findClosingDoubleQuote(command, i);
|
|
8207
|
+
if (end === -1) return { error: "unterminated double quote inside command substitution" };
|
|
8208
|
+
inner += command.slice(i, end + 1);
|
|
8209
|
+
i = end + 1;
|
|
8210
|
+
continue;
|
|
8211
|
+
}
|
|
8212
|
+
if (ch === "`") {
|
|
8213
|
+
const end = findClosingBacktick(command, i);
|
|
8214
|
+
if (end === -1) return { error: "unterminated backtick inside command substitution" };
|
|
8215
|
+
inner += command.slice(i, end + 1);
|
|
8216
|
+
i = end + 1;
|
|
8217
|
+
continue;
|
|
8218
|
+
}
|
|
8219
|
+
if (ch === "(") {
|
|
8220
|
+
depth += 1;
|
|
8221
|
+
inner += ch;
|
|
8222
|
+
i += 1;
|
|
8223
|
+
continue;
|
|
8224
|
+
}
|
|
8225
|
+
if (ch === ")") {
|
|
8226
|
+
depth -= 1;
|
|
8227
|
+
if (depth === 0) return { inner, next: i + 1 };
|
|
8228
|
+
inner += ch;
|
|
8229
|
+
i += 1;
|
|
8230
|
+
continue;
|
|
8231
|
+
}
|
|
8232
|
+
inner += ch;
|
|
8233
|
+
i += 1;
|
|
8234
|
+
}
|
|
8235
|
+
return { error: 'unbalanced "$(" \u2014 no closing ")"' };
|
|
8236
|
+
}
|
|
8237
|
+
function scanSubstitutionAt(command, start) {
|
|
8238
|
+
const dollarForm = command[start] === "$" && command[start + 1] === "(";
|
|
8239
|
+
const backtickForm = command[start] === "`";
|
|
8240
|
+
if (!dollarForm && !backtickForm) return void 0;
|
|
8241
|
+
const scanned = dollarForm ? scanDollarSubstitution(command, start) : scanBacktickSubstitution(command, start);
|
|
8242
|
+
if ("error" in scanned) return scanned;
|
|
8243
|
+
return {
|
|
8244
|
+
substitution: { inner: scanned.inner, raw: command.slice(start, scanned.next) },
|
|
8245
|
+
next: scanned.next
|
|
8246
|
+
};
|
|
8247
|
+
}
|
|
8248
|
+
function scanWordLite(command, start) {
|
|
8249
|
+
const n = command.length;
|
|
8250
|
+
let i = start;
|
|
8251
|
+
let text = "";
|
|
8252
|
+
const substitutions = [];
|
|
8253
|
+
const takeSubstitution = () => {
|
|
8254
|
+
const scanned = scanSubstitutionAt(command, i);
|
|
8255
|
+
if (!scanned) return { ok: true, consumed: false };
|
|
8256
|
+
if ("error" in scanned) return { ok: false, error: scanned.error };
|
|
8257
|
+
substitutions.push(scanned.substitution);
|
|
8258
|
+
text += CMDSUBST_PLACEHOLDER;
|
|
8259
|
+
i = scanned.next;
|
|
8260
|
+
return { ok: true, consumed: true };
|
|
8261
|
+
};
|
|
8262
|
+
while (i < n) {
|
|
8263
|
+
const ch = command[i];
|
|
8264
|
+
if (ch === "\\") {
|
|
8265
|
+
if (i + 1 < n) {
|
|
8266
|
+
text += command[i + 1];
|
|
8267
|
+
i += 2;
|
|
8268
|
+
} else {
|
|
8269
|
+
text += ch;
|
|
8270
|
+
i += 1;
|
|
8271
|
+
}
|
|
8272
|
+
continue;
|
|
8273
|
+
}
|
|
8274
|
+
if (ch === "'") {
|
|
8275
|
+
const end = command.indexOf("'", i + 1);
|
|
8276
|
+
if (end === -1) return { error: "unterminated single quote" };
|
|
8277
|
+
text += command.slice(i + 1, end);
|
|
8278
|
+
i = end + 1;
|
|
8279
|
+
continue;
|
|
8280
|
+
}
|
|
8281
|
+
if (ch === '"') {
|
|
8282
|
+
i += 1;
|
|
8283
|
+
let closed = false;
|
|
8284
|
+
while (i < n) {
|
|
8285
|
+
const inner = command[i];
|
|
8286
|
+
if (inner === "\\") {
|
|
8287
|
+
if (i + 1 < n) {
|
|
8288
|
+
text += command[i + 1];
|
|
8289
|
+
i += 2;
|
|
8290
|
+
} else {
|
|
8291
|
+
text += inner;
|
|
8292
|
+
i += 1;
|
|
8293
|
+
}
|
|
8294
|
+
continue;
|
|
8295
|
+
}
|
|
8296
|
+
if (inner === '"') {
|
|
8297
|
+
i += 1;
|
|
8298
|
+
closed = true;
|
|
8299
|
+
break;
|
|
8300
|
+
}
|
|
8301
|
+
const taken2 = takeSubstitution();
|
|
8302
|
+
if (!taken2.ok) return { error: taken2.error };
|
|
8303
|
+
if (taken2.consumed) continue;
|
|
8304
|
+
text += inner;
|
|
8305
|
+
i += 1;
|
|
8306
|
+
}
|
|
8307
|
+
if (!closed) return { error: "unterminated double quote" };
|
|
8308
|
+
continue;
|
|
8309
|
+
}
|
|
8310
|
+
const taken = takeSubstitution();
|
|
8311
|
+
if (!taken.ok) return { error: taken.error };
|
|
8312
|
+
if (taken.consumed) continue;
|
|
8313
|
+
if (isWordTerminator(ch)) break;
|
|
8314
|
+
text += ch;
|
|
8315
|
+
i += 1;
|
|
8316
|
+
}
|
|
8317
|
+
return { word: { text, substitutions }, next: i };
|
|
8318
|
+
}
|
|
8319
|
+
function scanFdDupTail(command, start) {
|
|
8320
|
+
const match = /^&(\d+|-)/.exec(command.slice(start, start + 8));
|
|
8321
|
+
if (!match) return { error: "unrecognised file-descriptor redirection" };
|
|
8322
|
+
return { tail: match[0], next: start + match[0].length };
|
|
8323
|
+
}
|
|
8324
|
+
function scanRedirectTarget(command, start) {
|
|
8325
|
+
let i = start;
|
|
8326
|
+
while (i < command.length && (command[i] === " " || command[i] === " ")) i += 1;
|
|
8327
|
+
const ch = command[i];
|
|
8328
|
+
if (ch === void 0 || isWordTerminator(ch)) return { error: "redirection without a target" };
|
|
8329
|
+
const scanned = scanWordLite(command, i);
|
|
8330
|
+
if ("error" in scanned) return scanned;
|
|
8331
|
+
if (scanned.word.text.length === 0 && scanned.word.substitutions.length === 0) {
|
|
8332
|
+
return { error: "redirection without a target" };
|
|
8333
|
+
}
|
|
8334
|
+
return scanned;
|
|
8335
|
+
}
|
|
8336
|
+
function scanRedirect(command, start, fd) {
|
|
8337
|
+
const ch = command[start];
|
|
8338
|
+
if (ch === "&") {
|
|
8339
|
+
if (command[start + 1] !== ">") return { error: 'unexpected "&" in a redirection' };
|
|
8340
|
+
const append2 = command[start + 2] === ">";
|
|
8341
|
+
const target2 = scanRedirectTarget(command, start + (append2 ? 3 : 2));
|
|
8342
|
+
if ("error" in target2) return target2;
|
|
8343
|
+
return {
|
|
8344
|
+
redirect: { operator: append2 ? "&>>" : "&>", kind: "output", fd: 2, target: target2.word },
|
|
8345
|
+
next: target2.next
|
|
8346
|
+
};
|
|
8347
|
+
}
|
|
8348
|
+
if (ch === "<") {
|
|
8349
|
+
if (command[start + 1] === "<") {
|
|
8350
|
+
if (command[start + 2] === "<") {
|
|
8351
|
+
const target4 = scanRedirectTarget(command, start + 3);
|
|
8352
|
+
if ("error" in target4) return target4;
|
|
8353
|
+
return {
|
|
8354
|
+
redirect: { operator: "<<<", kind: "here-string", target: target4.word },
|
|
8355
|
+
next: target4.next
|
|
8356
|
+
};
|
|
8357
|
+
}
|
|
8358
|
+
const stripTabs = command[start + 2] === "-";
|
|
8359
|
+
const target3 = scanRedirectTarget(command, start + (stripTabs ? 3 : 2));
|
|
8360
|
+
if ("error" in target3) return target3;
|
|
8361
|
+
return {
|
|
8362
|
+
redirect: { operator: stripTabs ? "<<-" : "<<", kind: "here-doc", target: target3.word },
|
|
8363
|
+
next: target3.next,
|
|
8364
|
+
heredoc: { delimiter: target3.word.text, stripTabs }
|
|
8365
|
+
};
|
|
8366
|
+
}
|
|
8367
|
+
if (command[start + 1] === "&") {
|
|
8368
|
+
const dup = scanFdDupTail(command, start + 1);
|
|
8369
|
+
if ("error" in dup) return dup;
|
|
8370
|
+
return { redirect: { operator: `${fd ?? ""}<${dup.tail}`, kind: "fd-dup" }, next: dup.next };
|
|
8371
|
+
}
|
|
8372
|
+
if (command[start + 1] === ">") {
|
|
8373
|
+
const target3 = scanRedirectTarget(command, start + 2);
|
|
8374
|
+
if ("error" in target3) return target3;
|
|
8375
|
+
return {
|
|
8376
|
+
redirect: { operator: `${fd ?? ""}<>`, kind: "output", fd: fd ?? 0, target: target3.word },
|
|
8377
|
+
next: target3.next
|
|
8378
|
+
};
|
|
8379
|
+
}
|
|
8380
|
+
const target2 = scanRedirectTarget(command, start + 1);
|
|
8381
|
+
if ("error" in target2) return target2;
|
|
8382
|
+
return {
|
|
8383
|
+
redirect: { operator: `${fd ?? ""}<`, kind: "input", target: target2.word },
|
|
8384
|
+
next: target2.next
|
|
8385
|
+
};
|
|
8386
|
+
}
|
|
8387
|
+
const append = command[start + 1] === ">";
|
|
8388
|
+
let cursor = start + (append ? 2 : 1);
|
|
8389
|
+
const clobber = !append && command[cursor] === "|";
|
|
8390
|
+
if (clobber) cursor += 1;
|
|
8391
|
+
const operator = `${fd ?? ""}${append ? ">>" : ">"}${clobber ? "|" : ""}`;
|
|
8392
|
+
if (command[cursor] === "&") {
|
|
8393
|
+
const dup = scanFdDupTail(command, cursor);
|
|
8394
|
+
if ("error" in dup) return dup;
|
|
8395
|
+
return { redirect: { operator: `${operator}${dup.tail}`, kind: "fd-dup" }, next: dup.next };
|
|
8396
|
+
}
|
|
8397
|
+
const target = scanRedirectTarget(command, cursor);
|
|
8398
|
+
if ("error" in target) return target;
|
|
8399
|
+
return {
|
|
8400
|
+
redirect: { operator, kind: "output", fd: fd ?? 1, target: target.word },
|
|
8401
|
+
next: target.next
|
|
8402
|
+
};
|
|
8403
|
+
}
|
|
8404
|
+
function consumeHeredocBodies(command, start, heredocs) {
|
|
8405
|
+
let pos = start;
|
|
8406
|
+
for (const heredoc of heredocs) {
|
|
8407
|
+
let matched = false;
|
|
8408
|
+
while (pos < command.length) {
|
|
8409
|
+
const newline = command.indexOf("\n", pos);
|
|
8410
|
+
const lineEnd2 = newline === -1 ? command.length : newline;
|
|
8411
|
+
const raw = command.slice(pos, lineEnd2);
|
|
8412
|
+
const line = heredoc.stripTabs ? raw.replace(/^\t+/, "") : raw;
|
|
8413
|
+
pos = lineEnd2 + 1;
|
|
8414
|
+
if (line === heredoc.delimiter) {
|
|
8415
|
+
matched = true;
|
|
8416
|
+
break;
|
|
8417
|
+
}
|
|
8418
|
+
if (newline === -1) break;
|
|
8419
|
+
}
|
|
8420
|
+
if (!matched) return void 0;
|
|
8421
|
+
}
|
|
8422
|
+
return pos;
|
|
8423
|
+
}
|
|
8424
|
+
function tokenizeShellLite(command) {
|
|
8425
|
+
const n = command.length;
|
|
8426
|
+
const segments = [];
|
|
8427
|
+
let tokens = [];
|
|
8428
|
+
let pendingHeredocs = [];
|
|
8429
|
+
let i = 0;
|
|
8430
|
+
const endSegment = () => {
|
|
8431
|
+
if (tokens.length > 0) segments.push({ tokens });
|
|
8432
|
+
tokens = [];
|
|
8433
|
+
};
|
|
8434
|
+
const takeRedirect = (redirectStart, fd) => {
|
|
8435
|
+
const scanned = scanRedirect(command, redirectStart, fd);
|
|
8436
|
+
if ("error" in scanned) return { ok: false, error: scanned.error };
|
|
8437
|
+
tokens.push({ kind: "redirect", redirect: scanned.redirect });
|
|
8438
|
+
if (scanned.heredoc) pendingHeredocs.push(scanned.heredoc);
|
|
8439
|
+
i = scanned.next;
|
|
8440
|
+
return void 0;
|
|
8441
|
+
};
|
|
8442
|
+
while (i < n) {
|
|
8443
|
+
const ch = command[i];
|
|
8444
|
+
if (ch === " " || ch === " " || ch === "\r") {
|
|
8445
|
+
i += 1;
|
|
8446
|
+
continue;
|
|
8447
|
+
}
|
|
8448
|
+
if (ch === "\\" && command[i + 1] === "\n") {
|
|
8449
|
+
i += 2;
|
|
8450
|
+
continue;
|
|
8451
|
+
}
|
|
8452
|
+
if (ch === "\n") {
|
|
8453
|
+
endSegment();
|
|
8454
|
+
i += 1;
|
|
8455
|
+
if (pendingHeredocs.length > 0) {
|
|
8456
|
+
const after = consumeHeredocBodies(command, i, pendingHeredocs);
|
|
8457
|
+
if (after === void 0) return { ok: false, error: "here-document without its terminator line" };
|
|
8458
|
+
i = after;
|
|
8459
|
+
pendingHeredocs = [];
|
|
8460
|
+
}
|
|
8461
|
+
continue;
|
|
8462
|
+
}
|
|
8463
|
+
if (ch === "#") {
|
|
8464
|
+
while (i < n && command[i] !== "\n") i += 1;
|
|
8465
|
+
continue;
|
|
8466
|
+
}
|
|
8467
|
+
if (ch === "|" || ch === ";" || ch === "&") {
|
|
8468
|
+
if (ch === "&" && command[i + 1] === ">") {
|
|
8469
|
+
const failed = takeRedirect(i);
|
|
8470
|
+
if (failed) return failed;
|
|
8471
|
+
continue;
|
|
8472
|
+
}
|
|
8473
|
+
i += 1;
|
|
8474
|
+
while (i < n && (command[i] === "|" || command[i] === ";" || command[i] === "&")) i += 1;
|
|
8475
|
+
endSegment();
|
|
8476
|
+
continue;
|
|
8477
|
+
}
|
|
8478
|
+
const fdMatch = /^(\d+)(?=>>|>|<>|<)/.exec(command.slice(i, i + 24));
|
|
8479
|
+
const fd = fdMatch ? Number(fdMatch[1]) : void 0;
|
|
8480
|
+
const redirectStart = i + (fdMatch ? fdMatch[1].length : 0);
|
|
8481
|
+
const redirectChar = command[redirectStart];
|
|
8482
|
+
if (redirectChar === "<" || redirectChar === ">") {
|
|
8483
|
+
const failed = takeRedirect(redirectStart, fd);
|
|
8484
|
+
if (failed) return failed;
|
|
8485
|
+
continue;
|
|
8486
|
+
}
|
|
8487
|
+
const scanned = scanWordLite(command, i);
|
|
8488
|
+
if ("error" in scanned) return { ok: false, error: scanned.error };
|
|
8489
|
+
if (scanned.next === i) return { ok: false, error: `unexpected character "${ch}"` };
|
|
8490
|
+
tokens.push({ kind: "word", word: scanned.word });
|
|
8491
|
+
i = scanned.next;
|
|
8492
|
+
}
|
|
8493
|
+
if (pendingHeredocs.length > 0) {
|
|
8494
|
+
return { ok: false, error: "here-document without a body" };
|
|
8495
|
+
}
|
|
8496
|
+
endSegment();
|
|
8497
|
+
return { ok: true, segments };
|
|
8498
|
+
}
|
|
8499
|
+
function classifyRedirections(segments) {
|
|
8500
|
+
const result2 = {
|
|
8501
|
+
writeTargets: [],
|
|
8502
|
+
readTargets: [],
|
|
8503
|
+
duplicates: [],
|
|
8504
|
+
discarded: []
|
|
8505
|
+
};
|
|
8506
|
+
for (const segment of segments) {
|
|
8507
|
+
for (const token of segment.tokens) {
|
|
8508
|
+
if (token.kind !== "redirect") continue;
|
|
8509
|
+
const { redirect } = token;
|
|
8510
|
+
const target = redirect.target?.text;
|
|
8511
|
+
if (redirect.kind === "fd-dup") {
|
|
8512
|
+
result2.duplicates.push(redirect.operator);
|
|
8513
|
+
continue;
|
|
8514
|
+
}
|
|
8515
|
+
if (redirect.kind === "here-string" || redirect.kind === "here-doc") continue;
|
|
8516
|
+
if (target === void 0) continue;
|
|
8517
|
+
if (isDiscardRedirectTarget(target)) {
|
|
8518
|
+
result2.discarded.push(target);
|
|
8519
|
+
continue;
|
|
8520
|
+
}
|
|
8521
|
+
if (redirect.kind === "input") {
|
|
8522
|
+
if (target.includes(CMDSUBST_PLACEHOLDER)) {
|
|
8523
|
+
result2.unresolved ??= `${redirect.operator} ${target}`;
|
|
8524
|
+
continue;
|
|
8525
|
+
}
|
|
8526
|
+
result2.readTargets.push(target);
|
|
8527
|
+
continue;
|
|
8528
|
+
}
|
|
8529
|
+
if (target.includes(CMDSUBST_PLACEHOLDER)) {
|
|
8530
|
+
result2.unresolved ??= `${redirect.operator} ${target}`;
|
|
8531
|
+
continue;
|
|
8532
|
+
}
|
|
8533
|
+
result2.writeTargets.push(target);
|
|
8534
|
+
}
|
|
8535
|
+
}
|
|
8536
|
+
return result2;
|
|
8537
|
+
}
|
|
8538
|
+
|
|
7720
8539
|
// src/services/tools/permission-rules.ts
|
|
7721
8540
|
var FILE_TOOL_NAMES = ["read_file", "write_file", "edit_file", "search_files", "search_content"];
|
|
7722
8541
|
var READ_ONLY_FILE_TOOLS = /* @__PURE__ */ new Set(["read_file", "search_files", "search_content"]);
|
|
7723
8542
|
var DANGEROUS_BASH_PATTERN = /(?:^|\||;|&&)\s*(?:rm\s|sudo\b|chmod\b|chown\b|mkfs\b|dd\s|shutdown\b|reboot\b|kill\s+-9)/;
|
|
7724
|
-
var URL_RE = /[a-zA-Z][a-zA-Z0-9+.-]*:\/\/\S+/g;
|
|
7725
8543
|
function expandHome(p, homeDir2) {
|
|
7726
8544
|
if (p === "~") return { path: homeDir2, unresolvable: false };
|
|
7727
8545
|
if (p.startsWith("~/")) return { path: resolve5(homeDir2, p.slice(2)), unresolvable: false };
|
|
@@ -7731,25 +8549,110 @@ function expandHome(p, homeDir2) {
|
|
|
7731
8549
|
function expandHomeVars(command, homeDir2) {
|
|
7732
8550
|
return command.replace(/\$\{HOME\}/g, homeDir2).replace(/\$HOME(?![A-Za-z0-9_])/g, homeDir2);
|
|
7733
8551
|
}
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
8552
|
+
var CMDSUBST_READONLY_VERBS = /* @__PURE__ */ new Set([
|
|
8553
|
+
"cat",
|
|
8554
|
+
"ls",
|
|
8555
|
+
"grep",
|
|
8556
|
+
"head",
|
|
8557
|
+
"tail",
|
|
8558
|
+
"wc",
|
|
8559
|
+
"stat",
|
|
8560
|
+
"file",
|
|
8561
|
+
"which",
|
|
8562
|
+
"command",
|
|
8563
|
+
"readlink",
|
|
8564
|
+
"echo",
|
|
8565
|
+
"printf",
|
|
8566
|
+
"date",
|
|
8567
|
+
"pwd"
|
|
8568
|
+
]);
|
|
8569
|
+
var MAX_CMDSUBST_DEPTH = 5;
|
|
8570
|
+
var BASH_WRITE_VERBS = /* @__PURE__ */ new Set([
|
|
8571
|
+
"rm",
|
|
8572
|
+
"rmdir",
|
|
8573
|
+
"mv",
|
|
8574
|
+
"cp",
|
|
8575
|
+
"ln",
|
|
8576
|
+
"tee",
|
|
8577
|
+
"dd",
|
|
8578
|
+
"truncate",
|
|
8579
|
+
"install",
|
|
8580
|
+
"mkdir",
|
|
8581
|
+
"touch",
|
|
8582
|
+
"chmod",
|
|
8583
|
+
"chown",
|
|
8584
|
+
"chgrp",
|
|
8585
|
+
"shred",
|
|
8586
|
+
"unlink",
|
|
8587
|
+
"chattr",
|
|
8588
|
+
"mkfs",
|
|
8589
|
+
"mkswap",
|
|
8590
|
+
"mount",
|
|
8591
|
+
"umount"
|
|
8592
|
+
]);
|
|
8593
|
+
var BASH_FLAGGED_WRITE_VERBS = /* @__PURE__ */ new Map([
|
|
8594
|
+
["sed", ["-i", "--in-place"]],
|
|
8595
|
+
["perl", ["-i"]],
|
|
8596
|
+
["ruby", ["-i"]],
|
|
8597
|
+
["find", ["-delete", "-fprint", "-fls", "-fprintf", "-exec", "-execdir", "-ok"]]
|
|
8598
|
+
]);
|
|
8599
|
+
function isWriteSegment(segment) {
|
|
8600
|
+
const words = [];
|
|
8601
|
+
for (const token of segment.tokens) {
|
|
8602
|
+
if (token.kind === "word") {
|
|
8603
|
+
words.push(token.word.text);
|
|
8604
|
+
continue;
|
|
8605
|
+
}
|
|
8606
|
+
const { redirect } = token;
|
|
8607
|
+
if (redirect.kind !== "output") continue;
|
|
8608
|
+
if (isDiscardRedirectTarget(redirect.target?.text)) continue;
|
|
8609
|
+
return true;
|
|
8610
|
+
}
|
|
8611
|
+
for (const word of words) {
|
|
8612
|
+
if (BASH_WRITE_VERBS.has(word)) return true;
|
|
8613
|
+
const flags2 = BASH_FLAGGED_WRITE_VERBS.get(word);
|
|
8614
|
+
if (flags2 && flags2.some((flag) => words.includes(flag))) return true;
|
|
8615
|
+
}
|
|
8616
|
+
return false;
|
|
7739
8617
|
}
|
|
7740
|
-
function
|
|
7741
|
-
|
|
8618
|
+
function pathPieces(text, access) {
|
|
8619
|
+
const candidates = [];
|
|
8620
|
+
for (const piece of text.split("=")) {
|
|
8621
|
+
if (!piece.startsWith("/") && !piece.startsWith("~")) continue;
|
|
8622
|
+
if (isDiscardRedirectTarget(piece)) continue;
|
|
8623
|
+
candidates.push({ path: piece, access });
|
|
8624
|
+
}
|
|
8625
|
+
return candidates;
|
|
7742
8626
|
}
|
|
7743
|
-
function extractPathCandidates(
|
|
7744
|
-
const withoutUrls = command.replace(URL_RE, " ");
|
|
8627
|
+
function extractPathCandidates(segments) {
|
|
7745
8628
|
const candidates = [];
|
|
7746
|
-
for (const
|
|
7747
|
-
const
|
|
7748
|
-
|
|
8629
|
+
for (const segment of segments) {
|
|
8630
|
+
const segmentAccess = isWriteSegment(segment) ? "write" : "unknown";
|
|
8631
|
+
for (const token of segment.tokens) {
|
|
8632
|
+
if (token.kind === "word") {
|
|
8633
|
+
candidates.push(...pathPieces(token.word.text, segmentAccess));
|
|
8634
|
+
continue;
|
|
8635
|
+
}
|
|
8636
|
+
const { redirect } = token;
|
|
8637
|
+
if (redirect.kind === "fd-dup" || redirect.kind === "here-doc" || redirect.kind === "here-string") continue;
|
|
8638
|
+
const target = redirect.target?.text;
|
|
8639
|
+
if (target === void 0 || isDiscardRedirectTarget(target)) continue;
|
|
8640
|
+
if (target.includes(CMDSUBST_PLACEHOLDER)) continue;
|
|
8641
|
+
const access = redirect.kind === "output" ? "write" : segmentAccess;
|
|
8642
|
+
for (const candidate of pathPieces(target, access)) {
|
|
8643
|
+
candidates.push(
|
|
8644
|
+
redirect.kind === "output" ? { ...candidate, reason: `output redirection to "${target}"` } : candidate
|
|
8645
|
+
);
|
|
8646
|
+
}
|
|
8647
|
+
}
|
|
7749
8648
|
}
|
|
7750
8649
|
return candidates;
|
|
7751
8650
|
}
|
|
8651
|
+
function commandAccess(candidates) {
|
|
8652
|
+
return candidates.some((candidate) => candidate.access === "write") ? "write" : "unknown";
|
|
8653
|
+
}
|
|
7752
8654
|
function decidePath(scope, path9, access) {
|
|
8655
|
+
const permission = scope.permission;
|
|
7753
8656
|
return decideAccess({
|
|
7754
8657
|
path: path9,
|
|
7755
8658
|
access,
|
|
@@ -7757,11 +8660,28 @@ function decidePath(scope, path9, access) {
|
|
|
7757
8660
|
homeDir: scope.homeDir,
|
|
7758
8661
|
taskWorkspace: scope.workspace,
|
|
7759
8662
|
taskAllowedDirs: scope.taskAllowedDirs,
|
|
7760
|
-
configAllowedDirs:
|
|
7761
|
-
autoAllowRoots:
|
|
7762
|
-
|
|
8663
|
+
configAllowedDirs: permission?.allowedDirs,
|
|
8664
|
+
autoAllowRoots: permission?.autoAllowRoots,
|
|
8665
|
+
// `sensitiveDirs` is the current key; `deniedDirs` is its deprecated alias
|
|
8666
|
+
// and is merged (deduped) here so a legacy config keeps protecting exactly
|
|
8667
|
+
// the directories it declares.
|
|
8668
|
+
extraSensitiveDirs: mergeSensitiveDirs(permission),
|
|
8669
|
+
sensitiveMode: resolveSensitiveMode(permission)
|
|
7763
8670
|
});
|
|
7764
8671
|
}
|
|
8672
|
+
function toToolDecision(verdict, access) {
|
|
8673
|
+
return {
|
|
8674
|
+
decision: verdict.decision,
|
|
8675
|
+
matchedRule: verdict.matchedRule,
|
|
8676
|
+
resolvedPath: verdict.resolvedPath,
|
|
8677
|
+
access,
|
|
8678
|
+
...verdict.matchedEntry !== void 0 && {
|
|
8679
|
+
matchedEntry: verdict.matchedEntry,
|
|
8680
|
+
matchedSource: verdict.matchedSource,
|
|
8681
|
+
sensitive: true
|
|
8682
|
+
}
|
|
8683
|
+
};
|
|
8684
|
+
}
|
|
7765
8685
|
function evaluateFileAccess(scope, toolName, args) {
|
|
7766
8686
|
const rawPath = args.path;
|
|
7767
8687
|
if (typeof rawPath !== "string" || !rawPath) return { decision: "allow", matchedRule: "no-path" };
|
|
@@ -7776,35 +8696,163 @@ function evaluateFileAccess(scope, toolName, args) {
|
|
|
7776
8696
|
}
|
|
7777
8697
|
const verdict = decidePath(scope, expanded.path, access);
|
|
7778
8698
|
if (verdict.decision === "allow" && verdict.withinReadScope) {
|
|
7779
|
-
return { decision: "ask", matchedRule: "read-within-home" };
|
|
8699
|
+
return { decision: "ask", matchedRule: "read-within-home", resolvedPath: verdict.resolvedPath, access };
|
|
7780
8700
|
}
|
|
7781
|
-
return
|
|
8701
|
+
return toToolDecision(verdict, access);
|
|
8702
|
+
}
|
|
8703
|
+
function collectSubstitutions(segments) {
|
|
8704
|
+
const substitutions = [];
|
|
8705
|
+
for (const segment of segments) {
|
|
8706
|
+
for (const token of segment.tokens) {
|
|
8707
|
+
const word = token.kind === "word" ? token.word : token.redirect.target;
|
|
8708
|
+
if (word) substitutions.push(...word.substitutions);
|
|
8709
|
+
}
|
|
8710
|
+
}
|
|
8711
|
+
return substitutions;
|
|
8712
|
+
}
|
|
8713
|
+
function checkSubstitutionInner(inner) {
|
|
8714
|
+
const tokenized = tokenizeShellLite(inner);
|
|
8715
|
+
if (!tokenized.ok) return { ok: false, reason: `cannot parse it (${tokenized.error})` };
|
|
8716
|
+
if (tokenized.segments.length === 0) return { ok: false, reason: "it is empty" };
|
|
8717
|
+
for (const segment of tokenized.segments) {
|
|
8718
|
+
const verb = segment.tokens.find((token) => token.kind === "word");
|
|
8719
|
+
if (!verb || verb.kind !== "word" || verb.word.text === CMDSUBST_PLACEHOLDER) {
|
|
8720
|
+
return { ok: false, reason: "it has no command word to judge" };
|
|
8721
|
+
}
|
|
8722
|
+
if (!CMDSUBST_READONLY_VERBS.has(verb.word.text)) {
|
|
8723
|
+
return { ok: false, reason: `\`${verb.word.text}\` is not a read-only command` };
|
|
8724
|
+
}
|
|
8725
|
+
if (isWriteSegment(segment)) return { ok: false, reason: `\`${verb.word.text}\` writes` };
|
|
8726
|
+
}
|
|
8727
|
+
return { ok: true };
|
|
8728
|
+
}
|
|
8729
|
+
function evaluateSubstitutions(scope, segments, depth) {
|
|
8730
|
+
const substitutions = collectSubstitutions(segments);
|
|
8731
|
+
if (substitutions.length === 0) return void 0;
|
|
8732
|
+
if (depth + 1 > MAX_CMDSUBST_DEPTH) {
|
|
8733
|
+
return {
|
|
8734
|
+
decision: "ask",
|
|
8735
|
+
matchedRule: "dynamic-shell",
|
|
8736
|
+
reason: `command substitution nested deeper than ${MAX_CMDSUBST_DEPTH} levels`
|
|
8737
|
+
};
|
|
8738
|
+
}
|
|
8739
|
+
for (const substitution of substitutions) {
|
|
8740
|
+
const inner = substitution.inner.trim();
|
|
8741
|
+
if (inner.length === 0) {
|
|
8742
|
+
return { decision: "ask", matchedRule: "dynamic-shell", reason: `empty command substitution ${substitution.raw}` };
|
|
8743
|
+
}
|
|
8744
|
+
const readOnly = checkSubstitutionInner(inner);
|
|
8745
|
+
if (!readOnly.ok) {
|
|
8746
|
+
return {
|
|
8747
|
+
decision: "ask",
|
|
8748
|
+
matchedRule: "dynamic-shell",
|
|
8749
|
+
reason: `command substitution ${substitution.raw}: ${readOnly.reason}`
|
|
8750
|
+
};
|
|
8751
|
+
}
|
|
8752
|
+
const verdict = evaluateBashCommand(scope, inner, depth + 1);
|
|
8753
|
+
if (verdict.decision === "deny") return verdict;
|
|
8754
|
+
if (verdict.decision === "ask") {
|
|
8755
|
+
const detail = verdict.reason ?? (verdict.resolvedPath ? `target ${verdict.resolvedPath}` : verdict.matchedRule);
|
|
8756
|
+
return {
|
|
8757
|
+
decision: "ask",
|
|
8758
|
+
matchedRule: "dynamic-shell",
|
|
8759
|
+
reason: `command substitution ${substitution.raw} needs approval: ${detail}`,
|
|
8760
|
+
// Carry the inner hit through, so an approval request still names the
|
|
8761
|
+
// path (and the sensitive entry) the substituted command would touch.
|
|
8762
|
+
...verdict.resolvedPath !== void 0 && { resolvedPath: verdict.resolvedPath },
|
|
8763
|
+
...verdict.matchedEntry !== void 0 && {
|
|
8764
|
+
matchedEntry: verdict.matchedEntry,
|
|
8765
|
+
matchedSource: verdict.matchedSource,
|
|
8766
|
+
sensitive: true
|
|
8767
|
+
}
|
|
8768
|
+
};
|
|
8769
|
+
}
|
|
8770
|
+
}
|
|
8771
|
+
return void 0;
|
|
8772
|
+
}
|
|
8773
|
+
function fallbackSensitiveWrite(scope, expandedCommand) {
|
|
8774
|
+
for (const raw of expandedCommand.split(/[\s=]+/)) {
|
|
8775
|
+
const word = raw.replace(/^['"]+/, "").replace(/['"]+$/, "");
|
|
8776
|
+
if (!word.startsWith("/") && !word.startsWith("~")) continue;
|
|
8777
|
+
const expanded = expandHome(word, scope.homeDir);
|
|
8778
|
+
if (expanded.unresolvable) continue;
|
|
8779
|
+
const verdict = decidePath(scope, expanded.path, "write");
|
|
8780
|
+
if (verdict.decision === "deny") return toToolDecision(verdict, "write");
|
|
8781
|
+
}
|
|
8782
|
+
return void 0;
|
|
7782
8783
|
}
|
|
7783
8784
|
function evaluateBashAccess(scope, command) {
|
|
8785
|
+
return evaluateBashCommand(scope, command, 0);
|
|
8786
|
+
}
|
|
8787
|
+
function evaluateBashCommand(scope, command, depth) {
|
|
7784
8788
|
const expandedCommand = expandHomeVars(command, scope.homeDir);
|
|
8789
|
+
const tokenized = tokenizeShellLite(expandedCommand);
|
|
8790
|
+
if (!tokenized.ok) {
|
|
8791
|
+
const refusal = fallbackSensitiveWrite(scope, expandedCommand);
|
|
8792
|
+
if (refusal) return refusal;
|
|
8793
|
+
return {
|
|
8794
|
+
decision: "ask",
|
|
8795
|
+
matchedRule: "dynamic-shell",
|
|
8796
|
+
reason: `could not parse the shell command (${tokenized.error})`
|
|
8797
|
+
};
|
|
8798
|
+
}
|
|
8799
|
+
const segments = tokenized.segments;
|
|
8800
|
+
const redirections = classifyRedirections(segments);
|
|
7785
8801
|
const candidates = [];
|
|
7786
8802
|
let hasUnresolvableHome = false;
|
|
7787
|
-
for (const
|
|
7788
|
-
const expanded = expandHome(
|
|
8803
|
+
for (const candidate of extractPathCandidates(segments)) {
|
|
8804
|
+
const expanded = expandHome(candidate.path, scope.homeDir);
|
|
7789
8805
|
if (expanded.unresolvable) {
|
|
7790
8806
|
hasUnresolvableHome = true;
|
|
7791
8807
|
continue;
|
|
7792
8808
|
}
|
|
7793
|
-
candidates.push(expanded.path);
|
|
8809
|
+
candidates.push({ ...candidate, path: expanded.path });
|
|
7794
8810
|
}
|
|
7795
8811
|
let firstAsk;
|
|
7796
8812
|
let firstAllow;
|
|
7797
8813
|
for (const candidate of candidates) {
|
|
7798
|
-
const verdict = decidePath(scope, candidate,
|
|
7799
|
-
if (verdict.decision === "deny") return verdict;
|
|
7800
|
-
if (verdict.decision === "ask")
|
|
7801
|
-
|
|
8814
|
+
const verdict = decidePath(scope, candidate.path, candidate.access);
|
|
8815
|
+
if (verdict.decision === "deny") return toToolDecision(verdict, candidate.access);
|
|
8816
|
+
if (verdict.decision === "ask") {
|
|
8817
|
+
firstAsk ??= {
|
|
8818
|
+
...toToolDecision(verdict, candidate.access),
|
|
8819
|
+
...candidate.reason !== void 0 && { reason: candidate.reason }
|
|
8820
|
+
};
|
|
8821
|
+
} else firstAllow ??= toToolDecision(verdict, candidate.access);
|
|
7802
8822
|
}
|
|
8823
|
+
const lineAccess = commandAccess(candidates);
|
|
8824
|
+
if (firstAsk) firstAsk = { ...firstAsk, access: lineAccess };
|
|
8825
|
+
if (firstAllow) firstAllow = { ...firstAllow, access: lineAccess };
|
|
7803
8826
|
if (DANGEROUS_BASH_PATTERN.test(expandedCommand)) {
|
|
7804
|
-
|
|
8827
|
+
const hit = firstAsk?.matchedEntry !== void 0 ? firstAsk : void 0;
|
|
8828
|
+
return {
|
|
8829
|
+
decision: "ask",
|
|
8830
|
+
matchedRule: "dangerous-bash",
|
|
8831
|
+
access: lineAccess,
|
|
8832
|
+
...hit !== void 0 && {
|
|
8833
|
+
matchedEntry: hit.matchedEntry,
|
|
8834
|
+
matchedSource: hit.matchedSource,
|
|
8835
|
+
resolvedPath: hit.resolvedPath,
|
|
8836
|
+
sensitive: true
|
|
8837
|
+
}
|
|
8838
|
+
};
|
|
8839
|
+
}
|
|
8840
|
+
const substitution = evaluateSubstitutions(scope, segments, depth);
|
|
8841
|
+
if (substitution) return substitution;
|
|
8842
|
+
if (redirections.unresolved !== void 0) {
|
|
8843
|
+
return {
|
|
8844
|
+
decision: "ask",
|
|
8845
|
+
matchedRule: "dynamic-shell",
|
|
8846
|
+
reason: `output redirection target cannot be determined statically (${redirections.unresolved})`
|
|
8847
|
+
};
|
|
8848
|
+
}
|
|
8849
|
+
if (redirections.writeTargets.length > 0 && !firstAsk) {
|
|
8850
|
+
return {
|
|
8851
|
+
decision: "ask",
|
|
8852
|
+
matchedRule: "dynamic-shell",
|
|
8853
|
+
reason: `output redirection writes a file ("${redirections.writeTargets.join('", "')}")`
|
|
8854
|
+
};
|
|
7805
8855
|
}
|
|
7806
|
-
const dynamic = detectUnsafeDynamicTarget(expandedCommand);
|
|
7807
|
-
if (dynamic) return { decision: "ask", matchedRule: "dynamic-shell", reason: dynamic };
|
|
7808
8856
|
if (hasUnresolvableHome) {
|
|
7809
8857
|
return {
|
|
7810
8858
|
decision: "ask",
|
|
@@ -7815,12 +8863,13 @@ function evaluateBashAccess(scope, command) {
|
|
|
7815
8863
|
if (firstAsk) return firstAsk;
|
|
7816
8864
|
return firstAllow ?? { decision: "allow", matchedRule: "no-path" };
|
|
7817
8865
|
}
|
|
7818
|
-
function denyMessage(toolName, requestedPath) {
|
|
8866
|
+
function denyMessage(toolName, requestedPath, matchedRule) {
|
|
8867
|
+
const write = matchedRule === "sensitive-list-write";
|
|
7819
8868
|
if (toolName === "bash") {
|
|
7820
|
-
return "[ERROR] bash: refusing to run this command \u2014 it
|
|
8869
|
+
return write ? "[ERROR] bash: refusing to run this command \u2014 it would write to a path on the sensitive list (sensitive paths are never writable)." : "[ERROR] bash: refusing to run this command \u2014 it touches a path on the sensitive list (a protected location).";
|
|
7821
8870
|
}
|
|
7822
8871
|
const target = requestedPath ? ` "${requestedPath}"` : "";
|
|
7823
|
-
return `[ERROR] ${toolName}: refusing access to${target} \u2014 the path is on the
|
|
8872
|
+
return write ? `[ERROR] ${toolName}: refusing to write${target} \u2014 the path is on the sensitive list (sensitive paths are never writable).` : `[ERROR] ${toolName}: refusing access to${target} \u2014 the path is on the sensitive list (a protected location).`;
|
|
7824
8873
|
}
|
|
7825
8874
|
function decideToolAccess(scope, toolName, args) {
|
|
7826
8875
|
let verdict;
|
|
@@ -7832,7 +8881,51 @@ function decideToolAccess(scope, toolName, args) {
|
|
|
7832
8881
|
return { decision: "allow", matchedRule: "no-path" };
|
|
7833
8882
|
}
|
|
7834
8883
|
if (verdict.decision !== "deny") return verdict;
|
|
7835
|
-
return {
|
|
8884
|
+
return {
|
|
8885
|
+
...verdict,
|
|
8886
|
+
message: denyMessage(toolName, typeof args.path === "string" ? args.path : void 0, verdict.matchedRule)
|
|
8887
|
+
};
|
|
8888
|
+
}
|
|
8889
|
+
|
|
8890
|
+
// src/services/permission/permission-audit.ts
|
|
8891
|
+
function operationForTool(toolName) {
|
|
8892
|
+
if (toolName === "bash") return "exec";
|
|
8893
|
+
if (toolName === "read_file" || toolName === "search_files" || toolName === "search_content") return "read";
|
|
8894
|
+
return "write";
|
|
8895
|
+
}
|
|
8896
|
+
function auditAccessFor(access) {
|
|
8897
|
+
return access ?? "unknown";
|
|
8898
|
+
}
|
|
8899
|
+
function recordPermissionAudit(logger, entry) {
|
|
8900
|
+
if (!logger) return;
|
|
8901
|
+
logger.info(
|
|
8902
|
+
{
|
|
8903
|
+
audit: "permission",
|
|
8904
|
+
decision: entry.decision,
|
|
8905
|
+
operation: entry.operation,
|
|
8906
|
+
...entry.access !== void 0 && { access: entry.access },
|
|
8907
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8908
|
+
...entry.resolvedPath !== void 0 && { resolvedPath: entry.resolvedPath },
|
|
8909
|
+
...entry.requestedPath !== void 0 && { requestedPath: entry.requestedPath },
|
|
8910
|
+
...entry.matchedEntry !== void 0 && { matchedEntry: entry.matchedEntry },
|
|
8911
|
+
...entry.matchedSource !== void 0 && { matchedSource: entry.matchedSource },
|
|
8912
|
+
...entry.matchedRule !== void 0 && { matchedRule: entry.matchedRule },
|
|
8913
|
+
...entry.taskId !== void 0 && { taskId: entry.taskId },
|
|
8914
|
+
...entry.workerId !== void 0 && { workerId: entry.workerId },
|
|
8915
|
+
...entry.toolName !== void 0 && { toolName: entry.toolName },
|
|
8916
|
+
...entry.source !== void 0 && { source: entry.source }
|
|
8917
|
+
},
|
|
8918
|
+
"permission audit"
|
|
8919
|
+
);
|
|
8920
|
+
}
|
|
8921
|
+
function verdictDetailSuffix(detail) {
|
|
8922
|
+
const parts = [];
|
|
8923
|
+
if (detail.matchedEntry) {
|
|
8924
|
+
parts.push(`hit: ${detail.matchedEntry}${detail.matchedSource ? ` (${detail.matchedSource})` : ""}`);
|
|
8925
|
+
}
|
|
8926
|
+
if (detail.matchedRule) parts.push(`rule: ${detail.matchedRule}`);
|
|
8927
|
+
if (detail.operation) parts.push(`op: ${detail.operation}`);
|
|
8928
|
+
return parts.length > 0 ? ` [${parts.join(" ; ")}]` : "";
|
|
7836
8929
|
}
|
|
7837
8930
|
|
|
7838
8931
|
// src/infra/blocking-requests.ts
|
|
@@ -8243,10 +9336,11 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
8243
9336
|
}
|
|
8244
9337
|
/**
|
|
8245
9338
|
* Route a Claude Code tool call through the SAME access policy a puppet
|
|
8246
|
-
* worker uses (`decideToolAccess`). A
|
|
8247
|
-
* (no approval prompt)
|
|
8248
|
-
*
|
|
8249
|
-
* the old regex-only gate
|
|
9339
|
+
* worker uses (`decideToolAccess`). A sensitive WRITE is denied outright
|
|
9340
|
+
* (no approval prompt), a sensitive READ asks with the matched entry named,
|
|
9341
|
+
* an ambiguous target asks the controller and a path inside the allowed scope
|
|
9342
|
+
* runs immediately. This closes the parity gap where the old regex-only gate
|
|
9343
|
+
* let sensitive paths through silently.
|
|
8250
9344
|
*/
|
|
8251
9345
|
async handleCanUseTool(toolName, input2) {
|
|
8252
9346
|
if (!this._signalQueue || !this._runContext) {
|
|
@@ -8255,42 +9349,91 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
8255
9349
|
const scope = this.effectiveAccessScope();
|
|
8256
9350
|
if (toolName === "Bash") {
|
|
8257
9351
|
const verdict = decideToolAccess(scope, "bash", { command: String(input2.command ?? "") });
|
|
8258
|
-
|
|
8259
|
-
|
|
8260
|
-
|
|
8261
|
-
if (verdict.decision === "ask") return this.requestPermission(toolName, input2, verdict.reason);
|
|
9352
|
+
const operation = operationForTool("bash");
|
|
9353
|
+
if (verdict.decision === "deny") return this.denyWithAudit(toolName, input2, verdict, operation);
|
|
9354
|
+
if (verdict.decision === "ask") return this.requestPermission(toolName, input2, this.askOf(verdict, operation));
|
|
8262
9355
|
return { behavior: "allow" };
|
|
8263
9356
|
}
|
|
8264
9357
|
const fileTool = _ClaudeCodeWorker.SDK_FILE_TOOL_MAP[toolName];
|
|
8265
9358
|
if (fileTool) {
|
|
9359
|
+
const operation = operationForTool(fileTool.policyTool);
|
|
8266
9360
|
const rawPath = input2[fileTool.pathKey];
|
|
8267
9361
|
if (typeof rawPath !== "string" || rawPath.length === 0) {
|
|
8268
9362
|
if (fileTool.pathRequired) {
|
|
8269
|
-
return this.requestPermission(toolName, input2,
|
|
9363
|
+
return this.requestPermission(toolName, input2, {
|
|
9364
|
+
reason: "the target path could not be determined",
|
|
9365
|
+
operation
|
|
9366
|
+
});
|
|
8270
9367
|
}
|
|
8271
9368
|
return { behavior: "allow" };
|
|
8272
9369
|
}
|
|
8273
9370
|
const verdict = decideToolAccess(scope, fileTool.policyTool, { path: rawPath });
|
|
8274
|
-
if (verdict.decision === "deny")
|
|
8275
|
-
|
|
8276
|
-
}
|
|
8277
|
-
if (verdict.decision === "ask") return this.requestPermission(toolName, input2, verdict.reason);
|
|
9371
|
+
if (verdict.decision === "deny") return this.denyWithAudit(toolName, input2, verdict, operation);
|
|
9372
|
+
if (verdict.decision === "ask") return this.requestPermission(toolName, input2, this.askOf(verdict, operation));
|
|
8278
9373
|
return { behavior: "allow" };
|
|
8279
9374
|
}
|
|
8280
9375
|
if (_ClaudeCodeWorker.SDK_NON_PATH_TOOLS.has(toolName)) {
|
|
8281
9376
|
return { behavior: "allow" };
|
|
8282
9377
|
}
|
|
8283
|
-
return this.requestPermission(toolName, input2,
|
|
9378
|
+
return this.requestPermission(toolName, input2, {
|
|
9379
|
+
reason: "the tool is not recognized by the permission policy",
|
|
9380
|
+
operation: operationForTool("bash")
|
|
9381
|
+
});
|
|
9382
|
+
}
|
|
9383
|
+
/**
|
|
9384
|
+
* Refuse a call the policy hard-denied (sensitive write) and record it.
|
|
9385
|
+
*
|
|
9386
|
+
* The worker receives the policy's disclosure-safe message (its own path
|
|
9387
|
+
* spelling, never the matched entry); the audit trail gets the metadata.
|
|
9388
|
+
* Same shape as the puppet worker's audit, so both entries agree.
|
|
9389
|
+
*/
|
|
9390
|
+
denyWithAudit(toolName, input2, verdict, operation) {
|
|
9391
|
+
recordPermissionAudit(this.logger, {
|
|
9392
|
+
decision: "hard-denied",
|
|
9393
|
+
operation,
|
|
9394
|
+
access: auditAccessFor(verdict.access),
|
|
9395
|
+
resolvedPath: verdict.resolvedPath,
|
|
9396
|
+
requestedPath: this.requestedPathOf(input2),
|
|
9397
|
+
matchedEntry: verdict.matchedEntry,
|
|
9398
|
+
matchedSource: verdict.matchedSource,
|
|
9399
|
+
matchedRule: verdict.matchedRule,
|
|
9400
|
+
taskId: this._runContext?.taskId,
|
|
9401
|
+
workerId: this._runContext?.workerId,
|
|
9402
|
+
toolName
|
|
9403
|
+
});
|
|
9404
|
+
return { behavior: "deny", message: verdict.message ?? "Denied by the permission policy" };
|
|
9405
|
+
}
|
|
9406
|
+
/** Project a policy verdict onto the approval-request/audit fields. */
|
|
9407
|
+
askOf(verdict, operation) {
|
|
9408
|
+
return {
|
|
9409
|
+
reason: verdict.reason,
|
|
9410
|
+
matchedRule: verdict.matchedRule,
|
|
9411
|
+
matchedEntry: verdict.matchedEntry,
|
|
9412
|
+
matchedSource: verdict.matchedSource,
|
|
9413
|
+
resolvedPath: verdict.resolvedPath,
|
|
9414
|
+
operation,
|
|
9415
|
+
sensitive: verdict.sensitive,
|
|
9416
|
+
access: verdict.access
|
|
9417
|
+
};
|
|
9418
|
+
}
|
|
9419
|
+
/** The target as the worker spelled it (for auditing against the prompt). */
|
|
9420
|
+
requestedPathOf(input2) {
|
|
9421
|
+
for (const key of ["file_path", "notebook_path", "path"]) {
|
|
9422
|
+
const value = input2[key];
|
|
9423
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
9424
|
+
}
|
|
9425
|
+
return void 0;
|
|
8284
9426
|
}
|
|
8285
9427
|
/**
|
|
8286
9428
|
* Emit a permission_request and block until the controller answers (or it
|
|
8287
9429
|
* times out → deny). Shared by every `ask` verdict so the request flow stays
|
|
8288
9430
|
* identical to the puppet worker's.
|
|
8289
9431
|
*/
|
|
8290
|
-
async requestPermission(toolName, input2,
|
|
9432
|
+
async requestPermission(toolName, input2, ask) {
|
|
8291
9433
|
if (!this._signalQueue || !this._runContext) return { behavior: "allow" };
|
|
8292
9434
|
const requestId = generateId("perm");
|
|
8293
|
-
const description = this.describePermissionRequest(toolName, input2,
|
|
9435
|
+
const description = this.describePermissionRequest(toolName, input2, ask);
|
|
9436
|
+
const operation = ask.operation;
|
|
8294
9437
|
this._signalQueue.enqueue({
|
|
8295
9438
|
source: "worker",
|
|
8296
9439
|
type: "permission_request",
|
|
@@ -8303,16 +9446,43 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
8303
9446
|
emoji: this._runContext.emoji,
|
|
8304
9447
|
toolName,
|
|
8305
9448
|
toolArgs: input2,
|
|
8306
|
-
description
|
|
9449
|
+
description,
|
|
9450
|
+
...ask.matchedRule !== void 0 && { matchedRule: ask.matchedRule },
|
|
9451
|
+
...ask.matchedEntry !== void 0 && { matchedEntry: ask.matchedEntry },
|
|
9452
|
+
...ask.matchedSource !== void 0 && { matchedSource: ask.matchedSource },
|
|
9453
|
+
...ask.resolvedPath !== void 0 && { resolvedPath: ask.resolvedPath },
|
|
9454
|
+
...ask.sensitive !== void 0 && { sensitive: ask.sensitive },
|
|
9455
|
+
...ask.access !== void 0 && { access: ask.access },
|
|
9456
|
+
operation
|
|
8307
9457
|
},
|
|
8308
9458
|
...this._turnLineage
|
|
8309
9459
|
});
|
|
8310
|
-
|
|
8311
|
-
|
|
9460
|
+
const result2 = await this._pendingPermissions.wait(
|
|
9461
|
+
requestId,
|
|
9462
|
+
_ClaudeCodeWorker.PERMISSION_TIMEOUT_MS,
|
|
9463
|
+
() => ({ behavior: "deny", message: "Permission request timed out" })
|
|
9464
|
+
);
|
|
9465
|
+
const denied = result2.behavior === "deny";
|
|
9466
|
+
recordPermissionAudit(this.logger, {
|
|
9467
|
+
decision: denied ? "denied" : "approved",
|
|
9468
|
+
operation,
|
|
9469
|
+
access: auditAccessFor(ask.access),
|
|
9470
|
+
resolvedPath: ask.resolvedPath,
|
|
9471
|
+
requestedPath: this.requestedPathOf(input2),
|
|
9472
|
+
matchedEntry: ask.matchedEntry,
|
|
9473
|
+
matchedSource: ask.matchedSource,
|
|
9474
|
+
matchedRule: ask.matchedRule,
|
|
9475
|
+
taskId: this._runContext.taskId,
|
|
9476
|
+
workerId: this._runContext.workerId,
|
|
9477
|
+
toolName,
|
|
9478
|
+
...denied && {
|
|
9479
|
+
source: result2.message === "Permission request timed out" ? "timeout" : "controller"
|
|
9480
|
+
}
|
|
8312
9481
|
});
|
|
9482
|
+
return result2;
|
|
8313
9483
|
}
|
|
8314
9484
|
/** Human-readable summary of a gated call for the approval prompt. */
|
|
8315
|
-
describePermissionRequest(toolName, input2,
|
|
9485
|
+
describePermissionRequest(toolName, input2, ask) {
|
|
8316
9486
|
let detail;
|
|
8317
9487
|
if (toolName === "Bash") {
|
|
8318
9488
|
detail = `Execute shell command: ${String(input2.command ?? "")}`;
|
|
@@ -8325,7 +9495,15 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
8325
9495
|
} else {
|
|
8326
9496
|
detail = `${toolName}: ${JSON.stringify(input2)}`;
|
|
8327
9497
|
}
|
|
8328
|
-
|
|
9498
|
+
const hit = ask.sensitive ? verdictDetailSuffix({
|
|
9499
|
+
matchedRule: ask.matchedRule,
|
|
9500
|
+
matchedEntry: ask.matchedEntry,
|
|
9501
|
+
matchedSource: ask.matchedSource,
|
|
9502
|
+
resolvedPath: ask.resolvedPath,
|
|
9503
|
+
operation: ask.operation
|
|
9504
|
+
}) : "";
|
|
9505
|
+
const withHit = `${detail}${hit}`;
|
|
9506
|
+
return ask.reason ? `${withHit} (${ask.reason})` : withHit;
|
|
8329
9507
|
}
|
|
8330
9508
|
// --- Prompt as AsyncIterable (supports follow-up messages) ---
|
|
8331
9509
|
buildPromptIterable(initialPrompt) {
|
|
@@ -8633,8 +9811,8 @@ controller: ${answer}`,
|
|
|
8633
9811
|
import { z as z3 } from "zod";
|
|
8634
9812
|
|
|
8635
9813
|
// src/services/tools/workspace-rules.ts
|
|
8636
|
-
import { readFileSync as
|
|
8637
|
-
import { join as
|
|
9814
|
+
import { readFileSync as readFileSync10, statSync as statSync5 } from "fs";
|
|
9815
|
+
import { join as join17 } from "path";
|
|
8638
9816
|
var RULE_FILE_NAMES2 = ["AGENTS.md", "CLAUDE.md"];
|
|
8639
9817
|
var WORKSPACE_RULES_MAX_LINES = 40;
|
|
8640
9818
|
var WORKSPACE_RULES_MAX_CHARS = 2e3;
|
|
@@ -8642,7 +9820,7 @@ function buildWorkspaceRulesSummary(input2) {
|
|
|
8642
9820
|
const workspacePath = input2.workspacePath;
|
|
8643
9821
|
if (!workspacePath) return "";
|
|
8644
9822
|
for (const name of RULE_FILE_NAMES2) {
|
|
8645
|
-
const rulesPath =
|
|
9823
|
+
const rulesPath = join17(workspacePath, name);
|
|
8646
9824
|
if (!isRegularFile(rulesPath)) continue;
|
|
8647
9825
|
const header = `Workspace rules (from ${rulesPath}):`;
|
|
8648
9826
|
const refusal = describeRefusal(rulesPath, workspacePath, input2);
|
|
@@ -8664,16 +9842,17 @@ function describeRefusal(rulesPath, workspacePath, input2) {
|
|
|
8664
9842
|
if (!homeDir2) {
|
|
8665
9843
|
return "unavailable \u2014 the permission policy cannot be evaluated here (no home directory in this context), so the file was not read.";
|
|
8666
9844
|
}
|
|
8667
|
-
const { decision } = decideAccess({
|
|
9845
|
+
const { decision, matchedEntry } = decideAccess({
|
|
8668
9846
|
access: "read",
|
|
8669
9847
|
path: rulesPath,
|
|
8670
9848
|
cwd: workspacePath,
|
|
8671
9849
|
homeDir: homeDir2,
|
|
8672
9850
|
taskWorkspace: workspacePath,
|
|
8673
|
-
|
|
9851
|
+
extraSensitiveDirs: mergeSensitiveDirs(input2.permissionConfig),
|
|
9852
|
+
sensitiveMode: resolveSensitiveMode(input2.permissionConfig)
|
|
8674
9853
|
});
|
|
8675
|
-
if (decision === "deny") {
|
|
8676
|
-
return "unavailable \u2014 the file resolves to a
|
|
9854
|
+
if (matchedEntry !== void 0 || decision === "deny") {
|
|
9855
|
+
return "unavailable \u2014 the file resolves to a sensitive location (a protected path such as credentials or tool config), so its content was not read.";
|
|
8677
9856
|
}
|
|
8678
9857
|
if (decision === "ask") {
|
|
8679
9858
|
return "unavailable \u2014 reading it was not approved by the permission policy (the path is outside the readable scope), so its content was not read.";
|
|
@@ -8700,14 +9879,14 @@ function summarizeRules(content) {
|
|
|
8700
9879
|
}
|
|
8701
9880
|
function isRegularFile(p) {
|
|
8702
9881
|
try {
|
|
8703
|
-
return
|
|
9882
|
+
return statSync5(p).isFile();
|
|
8704
9883
|
} catch {
|
|
8705
9884
|
return false;
|
|
8706
9885
|
}
|
|
8707
9886
|
}
|
|
8708
9887
|
function readTextFile(p) {
|
|
8709
9888
|
try {
|
|
8710
|
-
return
|
|
9889
|
+
return readFileSync10(p, "utf8");
|
|
8711
9890
|
} catch {
|
|
8712
9891
|
return null;
|
|
8713
9892
|
}
|
|
@@ -8739,9 +9918,9 @@ function resolveTaskAllowedDirs(requested, workspacePath, deps) {
|
|
|
8739
9918
|
}
|
|
8740
9919
|
const home = normalizePath(homeDir2);
|
|
8741
9920
|
const base = workspacePath !== void 0 ? normalizePath(workspacePath, { base: home }) : home;
|
|
8742
|
-
const
|
|
8743
|
-
...
|
|
8744
|
-
...(deps.permissionConfig
|
|
9921
|
+
const sensitivePaths = [
|
|
9922
|
+
...defaultSensitivePaths(home),
|
|
9923
|
+
...mergeSensitiveDirs(deps.permissionConfig).map((d) => normalizePath(d, { base: home }))
|
|
8745
9924
|
].map((d) => normalizePath(d));
|
|
8746
9925
|
const seen = /* @__PURE__ */ new Set();
|
|
8747
9926
|
const allowedDirs = [];
|
|
@@ -8757,9 +9936,9 @@ function resolveTaskAllowedDirs(requested, workspacePath, deps) {
|
|
|
8757
9936
|
error: `Invalid allowedDirs entry "${raw}" (resolved to ${resolved}): it is outside the home directory (${home}). Only directories under home can be granted to a task. The task was NOT created.`
|
|
8758
9937
|
};
|
|
8759
9938
|
}
|
|
8760
|
-
if (isDenied(resolved,
|
|
9939
|
+
if (isDenied(resolved, sensitivePaths)) {
|
|
8761
9940
|
return {
|
|
8762
|
-
error: `Invalid allowedDirs entry "${raw}" (resolved to ${resolved}): it matches the
|
|
9941
|
+
error: `Invalid allowedDirs entry "${raw}" (resolved to ${resolved}): it matches the sensitive list (a protected location such as credentials or tool config). The task was NOT created.`
|
|
8763
9942
|
};
|
|
8764
9943
|
}
|
|
8765
9944
|
if (!seen.has(resolved)) {
|
|
@@ -9362,7 +10541,7 @@ ${detail}`;
|
|
|
9362
10541
|
|
|
9363
10542
|
// src/services/tools/read-file.ts
|
|
9364
10543
|
import { z as z14 } from "zod";
|
|
9365
|
-
import { readFileSync as
|
|
10544
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
9366
10545
|
import { resolve as resolve6 } from "path";
|
|
9367
10546
|
var DEFAULT_MAX_LINES = 500;
|
|
9368
10547
|
function readFileTool(deps = {}) {
|
|
@@ -9376,7 +10555,7 @@ function readFileTool(deps = {}) {
|
|
|
9376
10555
|
}),
|
|
9377
10556
|
execute: safeExecute("read_file", async ({ path: path9, offset, limit }) => {
|
|
9378
10557
|
const resolved = resolve6(workspace, path9);
|
|
9379
|
-
const content =
|
|
10558
|
+
const content = readFileSync11(resolved, "utf-8");
|
|
9380
10559
|
const lines = content.split("\n");
|
|
9381
10560
|
const totalLines = lines.length;
|
|
9382
10561
|
const start = offset ? offset - 1 : 0;
|
|
@@ -9419,7 +10598,7 @@ function writeFileTool(deps = {}) {
|
|
|
9419
10598
|
}
|
|
9420
10599
|
|
|
9421
10600
|
// src/services/tools/edit-file.ts
|
|
9422
|
-
import { readFileSync as
|
|
10601
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "fs";
|
|
9423
10602
|
import { resolve as resolve8 } from "path";
|
|
9424
10603
|
import { z as z16 } from "zod";
|
|
9425
10604
|
function editFileTool(deps) {
|
|
@@ -9435,7 +10614,7 @@ function editFileTool(deps) {
|
|
|
9435
10614
|
const resolved = resolve8(workspace, path9);
|
|
9436
10615
|
let content;
|
|
9437
10616
|
try {
|
|
9438
|
-
content =
|
|
10617
|
+
content = readFileSync12(resolved, "utf-8");
|
|
9439
10618
|
} catch {
|
|
9440
10619
|
return `[ERROR] edit_file: File not found: ${path9}`;
|
|
9441
10620
|
}
|
|
@@ -9466,15 +10645,15 @@ function searchFilesTool(deps) {
|
|
|
9466
10645
|
execute: safeExecute("search_files", async ({ pattern, path: path9 }) => {
|
|
9467
10646
|
const basePath = path9 ? normalizePath(path9, { base: workspace }) : normalizePath(workspace);
|
|
9468
10647
|
if (homeDir2) {
|
|
9469
|
-
const { decision } = decideAccess({
|
|
10648
|
+
const { decision, matchedEntry } = decideAccess({
|
|
9470
10649
|
access: "read",
|
|
9471
10650
|
homeDir: homeDir2,
|
|
9472
10651
|
cwd: workspace,
|
|
9473
10652
|
path: basePath
|
|
9474
10653
|
});
|
|
9475
10654
|
const requested = path9 ?? workspace;
|
|
9476
|
-
if (decision === "deny") {
|
|
9477
|
-
return `[ERROR] search_files: refusing to search "${requested}" \u2014 the path is on the
|
|
10655
|
+
if (matchedEntry !== void 0 || decision === "deny") {
|
|
10656
|
+
return `[ERROR] search_files: refusing to search "${requested}" \u2014 the path is on the sensitive list (a protected location).`;
|
|
9478
10657
|
}
|
|
9479
10658
|
if (decision === "ask") {
|
|
9480
10659
|
return `[ERROR] search_files: refusing to search "${requested}" \u2014 the path is outside the home boundary (${homeDir2}). The controller can only read files under the home directory.`;
|
|
@@ -9878,11 +11057,32 @@ function wrapWithPermissionMiddleware(tools, config, deps) {
|
|
|
9878
11057
|
return originalExecute(args, options);
|
|
9879
11058
|
}
|
|
9880
11059
|
if (verdict.decision === "deny") {
|
|
11060
|
+
recordPermissionAudit(deps.logger, {
|
|
11061
|
+
decision: "hard-denied",
|
|
11062
|
+
operation: verdict.operation ?? operationForTool(toolName),
|
|
11063
|
+
access: auditAccessFor(verdict.access),
|
|
11064
|
+
resolvedPath: verdict.resolvedPath,
|
|
11065
|
+
requestedPath: typeof args.path === "string" ? args.path : void 0,
|
|
11066
|
+
matchedEntry: verdict.matchedEntry,
|
|
11067
|
+
matchedSource: verdict.matchedSource,
|
|
11068
|
+
matchedRule: verdict.matchedRule,
|
|
11069
|
+
taskId: deps.taskId,
|
|
11070
|
+
workerId: deps.workerId,
|
|
11071
|
+
toolName
|
|
11072
|
+
});
|
|
9881
11073
|
return verdict.message ?? `[ERROR] ${toolName}: refused by the path permission policy`;
|
|
9882
11074
|
}
|
|
9883
11075
|
const requestId = generateId("perm");
|
|
11076
|
+
const operation = verdict.operation ?? operationForTool(toolName);
|
|
9884
11077
|
const baseDescription = config.describeCall(toolName, args);
|
|
9885
|
-
const
|
|
11078
|
+
const hitDetail = verdict.sensitive ? verdictDetailSuffix({
|
|
11079
|
+
matchedRule: verdict.matchedRule,
|
|
11080
|
+
matchedEntry: verdict.matchedEntry,
|
|
11081
|
+
matchedSource: verdict.matchedSource,
|
|
11082
|
+
resolvedPath: verdict.resolvedPath,
|
|
11083
|
+
operation
|
|
11084
|
+
}) : "";
|
|
11085
|
+
const description = `${baseDescription}${hitDetail}${verdict.reason ? ` [reason: ${verdict.reason}]` : ""}`;
|
|
9886
11086
|
deps.signalQueue.enqueue({
|
|
9887
11087
|
source: "worker",
|
|
9888
11088
|
type: "permission_request",
|
|
@@ -9895,7 +11095,14 @@ function wrapWithPermissionMiddleware(tools, config, deps) {
|
|
|
9895
11095
|
emoji: deps.emoji,
|
|
9896
11096
|
toolName,
|
|
9897
11097
|
toolArgs: args,
|
|
9898
|
-
description
|
|
11098
|
+
description,
|
|
11099
|
+
...verdict.matchedRule !== void 0 && { matchedRule: verdict.matchedRule },
|
|
11100
|
+
...verdict.matchedEntry !== void 0 && { matchedEntry: verdict.matchedEntry },
|
|
11101
|
+
...verdict.matchedSource !== void 0 && { matchedSource: verdict.matchedSource },
|
|
11102
|
+
...verdict.resolvedPath !== void 0 && { resolvedPath: verdict.resolvedPath },
|
|
11103
|
+
...verdict.sensitive !== void 0 && { sensitive: verdict.sensitive },
|
|
11104
|
+
...verdict.access !== void 0 && { access: verdict.access },
|
|
11105
|
+
operation
|
|
9899
11106
|
},
|
|
9900
11107
|
...deps.turnLineage ?? {}
|
|
9901
11108
|
});
|
|
@@ -9913,6 +11120,22 @@ function wrapWithPermissionMiddleware(tools, config, deps) {
|
|
|
9913
11120
|
timer
|
|
9914
11121
|
});
|
|
9915
11122
|
});
|
|
11123
|
+
recordPermissionAudit(deps.logger, {
|
|
11124
|
+
decision: result2.approved ? "approved" : "denied",
|
|
11125
|
+
operation,
|
|
11126
|
+
access: auditAccessFor(verdict.access),
|
|
11127
|
+
resolvedPath: verdict.resolvedPath,
|
|
11128
|
+
requestedPath: typeof args.path === "string" ? args.path : void 0,
|
|
11129
|
+
matchedEntry: verdict.matchedEntry,
|
|
11130
|
+
matchedSource: verdict.matchedSource,
|
|
11131
|
+
matchedRule: verdict.matchedRule,
|
|
11132
|
+
taskId: deps.taskId,
|
|
11133
|
+
workerId: deps.workerId,
|
|
11134
|
+
toolName,
|
|
11135
|
+
...!result2.approved && {
|
|
11136
|
+
source: result2.reason === "timeout" ? "timeout" : result2.reason === "Worker aborted" ? "aborted" : "controller"
|
|
11137
|
+
}
|
|
11138
|
+
});
|
|
9916
11139
|
if (!result2.approved) {
|
|
9917
11140
|
if (result2.reason === "timeout") {
|
|
9918
11141
|
return `[ERROR] Permission request timed out for ${toolName}`;
|
|
@@ -10944,9 +12167,10 @@ var TaskExecuteEngine = class {
|
|
|
10944
12167
|
if (workerType === PUPPET_WORKER_TYPE) {
|
|
10945
12168
|
const middleware = wrapWithPermissionMiddleware(tools, {
|
|
10946
12169
|
gatedTools: ["bash", ...FILE_TOOL_NAMES],
|
|
10947
|
-
// Single decision source: the shared access policy (
|
|
12170
|
+
// Single decision source: the shared access policy (sensitive list →
|
|
10948
12171
|
// config roots → task allowedDirs → task workspace → read-in-home →
|
|
10949
|
-
// ask). `deny` refuses here without ever emitting a permission_request
|
|
12172
|
+
// ask). `deny` refuses here without ever emitting a permission_request;
|
|
12173
|
+
// both `deny` and every answered `ask` are audited (see the middleware).
|
|
10950
12174
|
decide: (toolName, args) => decideToolAccess(accessScope, toolName, args),
|
|
10951
12175
|
describeCall: (toolName, args) => {
|
|
10952
12176
|
if (toolName === "bash") return `Execute shell command: ${args.command}`;
|
|
@@ -10964,7 +12188,9 @@ var TaskExecuteEngine = class {
|
|
|
10964
12188
|
workerType,
|
|
10965
12189
|
emoji: workerMeta.emoji,
|
|
10966
12190
|
signalQueue: this.signalQueue,
|
|
10967
|
-
turnLineage: this.workerLineage(workerId)
|
|
12191
|
+
turnLineage: this.workerLineage(workerId),
|
|
12192
|
+
// Permission audit trail (metadata only) shares the session logger.
|
|
12193
|
+
logger: this.ctx.logger
|
|
10968
12194
|
});
|
|
10969
12195
|
tools = middleware.wrappedTools;
|
|
10970
12196
|
this.permissionResolvers.set(workerId, middleware);
|
|
@@ -11218,7 +12444,7 @@ var ControllerLoop = class {
|
|
|
11218
12444
|
};
|
|
11219
12445
|
|
|
11220
12446
|
// src/services/agents/controller-state.ts
|
|
11221
|
-
import { join as
|
|
12447
|
+
import { join as join18 } from "path";
|
|
11222
12448
|
|
|
11223
12449
|
// src/services/conversation/conversation-index.ts
|
|
11224
12450
|
function emptyUsage() {
|
|
@@ -11442,8 +12668,8 @@ var ControllerAgentState = class extends BaseWayangState {
|
|
|
11442
12668
|
/** In-memory resolve function for the current inquiry — never persisted. */
|
|
11443
12669
|
inquiryResolver = null;
|
|
11444
12670
|
constructor(ctx) {
|
|
11445
|
-
const jsonFile = new JSONFileHelper(
|
|
11446
|
-
const jsonlFile = new JSONLFileHelper(
|
|
12671
|
+
const jsonFile = new JSONFileHelper(join18(ctx.sessionDir, "runtime-state.json"));
|
|
12672
|
+
const jsonlFile = new JSONLFileHelper(join18(ctx.sessionDir, "conversation.jsonl"));
|
|
11447
12673
|
const initialData = {
|
|
11448
12674
|
runtimeState: {
|
|
11449
12675
|
session: { id: "", startedAt: 0 },
|
|
@@ -11613,8 +12839,8 @@ function buildSummaryInput(entries, maxChars) {
|
|
|
11613
12839
|
lines = formatAll(entries, caps);
|
|
11614
12840
|
if (totalLength(lines) <= maxChars) return lines;
|
|
11615
12841
|
}
|
|
11616
|
-
const
|
|
11617
|
-
const reserve =
|
|
12842
|
+
const noteFor2 = (n) => `[${n} older entries omitted \u2014 over the summarizer input budget]`;
|
|
12843
|
+
const reserve = noteFor2(lines.length).length + 1;
|
|
11618
12844
|
const kept = [];
|
|
11619
12845
|
let used = 0;
|
|
11620
12846
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
@@ -11624,7 +12850,7 @@ function buildSummaryInput(entries, maxChars) {
|
|
|
11624
12850
|
used += size;
|
|
11625
12851
|
}
|
|
11626
12852
|
const omitted = lines.length - kept.length;
|
|
11627
|
-
return omitted > 0 ? [
|
|
12853
|
+
return omitted > 0 ? [noteFor2(omitted), ...kept] : kept;
|
|
11628
12854
|
}
|
|
11629
12855
|
|
|
11630
12856
|
// src/services/agents/controller-agent.ts
|
|
@@ -11662,7 +12888,7 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
|
|
|
11662
12888
|
* cancelled while its workers are still running.
|
|
11663
12889
|
*/
|
|
11664
12890
|
turnInterruptRecorded = false;
|
|
11665
|
-
constructor(ctx, state, provider, tools, skills, recorder = new ToolCallRecorder()) {
|
|
12891
|
+
constructor(ctx, state, provider, tools, skills, recorder = new ToolCallRecorder(), controllerRules) {
|
|
11666
12892
|
super(provider);
|
|
11667
12893
|
this.ctx = ctx;
|
|
11668
12894
|
this.state = state;
|
|
@@ -11673,7 +12899,7 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
|
|
|
11673
12899
|
this.workspaceSlug = ctx.workspaceDir ? resolveWorkspaceRef(wayangDirForSessionDir(ctx.sessionDir), ctx.workspaceDir).slug : void 0;
|
|
11674
12900
|
this.contextManager = new ContextManager(
|
|
11675
12901
|
state,
|
|
11676
|
-
buildControllerSystemPrompt(this.ctx, skills),
|
|
12902
|
+
buildControllerSystemPrompt(this.ctx, skills, { rules: controllerRules }),
|
|
11677
12903
|
provider.contextWindowTokens
|
|
11678
12904
|
);
|
|
11679
12905
|
this.setHooks({
|
|
@@ -11696,6 +12922,10 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
|
|
|
11696
12922
|
const recorder = new ToolCallRecorder(ctx.logger);
|
|
11697
12923
|
const wayangDir = wayangDirForSessionDir(ctx.sessionDir);
|
|
11698
12924
|
const homeDir2 = wayangDir && basename9(wayangDir) === ".wayang" ? dirname11(wayangDir) : homedir3();
|
|
12925
|
+
const controllerRules = loadControllerRules({
|
|
12926
|
+
homeDir: homeDir2,
|
|
12927
|
+
cwd: ctx.workspaceDir || process.cwd()
|
|
12928
|
+
});
|
|
11699
12929
|
const tools = createControllerTools({
|
|
11700
12930
|
addTask: (task) => engine.add(task),
|
|
11701
12931
|
validateWorkerType: (type) => engine.validateWorkerType(type),
|
|
@@ -11725,7 +12955,8 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
|
|
|
11725
12955
|
cwd: ctx.workspaceDir,
|
|
11726
12956
|
homeDir: homeDir2,
|
|
11727
12957
|
// User-config permission section: add_task validates a task's allowedDirs
|
|
11728
|
-
// against the built-in
|
|
12958
|
+
// against the built-in sensitive list plus these configured
|
|
12959
|
+
// sensitiveDirs (the deprecated deniedDirs alias is merged in).
|
|
11729
12960
|
permissionConfig: opts.config.permission,
|
|
11730
12961
|
getNotebook: () => state.get("runtimeState.notebook") ?? "",
|
|
11731
12962
|
setNotebook: (content, mode) => {
|
|
@@ -11750,7 +12981,7 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
|
|
|
11750
12981
|
workspaceDir: ctx.workspaceDir,
|
|
11751
12982
|
wayangDir
|
|
11752
12983
|
});
|
|
11753
|
-
return new _ControllerAgent(ctx, state, provider, tools, skills, recorder);
|
|
12984
|
+
return new _ControllerAgent(ctx, state, provider, tools, skills, recorder, controllerRules);
|
|
11754
12985
|
}
|
|
11755
12986
|
/** Run the controller agent — streams text chunks while updating streamingEntries in state. */
|
|
11756
12987
|
async run(signals) {
|
|
@@ -12347,8 +13578,8 @@ function signalToEntry(sig, ts, sessionId) {
|
|
|
12347
13578
|
}
|
|
12348
13579
|
|
|
12349
13580
|
// src/services/skills/registry.ts
|
|
12350
|
-
import { readFileSync as
|
|
12351
|
-
import { join as
|
|
13581
|
+
import { readFileSync as readFileSync13, readdirSync as readdirSync2, statSync as statSync6 } from "fs";
|
|
13582
|
+
import { join as join19 } from "path";
|
|
12352
13583
|
import { parse as parseYaml } from "yaml";
|
|
12353
13584
|
var SKILL_FILE = "SKILL.md";
|
|
12354
13585
|
function parseFrontmatter(raw) {
|
|
@@ -12383,7 +13614,7 @@ function listResources(skillDir) {
|
|
|
12383
13614
|
const walk = (relDir) => {
|
|
12384
13615
|
let entries;
|
|
12385
13616
|
try {
|
|
12386
|
-
entries = readdirSync2(
|
|
13617
|
+
entries = readdirSync2(join19(skillDir, relDir), { withFileTypes: true });
|
|
12387
13618
|
} catch {
|
|
12388
13619
|
return;
|
|
12389
13620
|
}
|
|
@@ -12430,10 +13661,10 @@ var SkillRegistry = class {
|
|
|
12430
13661
|
if (existing) return existing;
|
|
12431
13662
|
const desc = this.descriptors.get(name);
|
|
12432
13663
|
if (!desc) return void 0;
|
|
12433
|
-
const filePath =
|
|
13664
|
+
const filePath = join19(desc.dir, SKILL_FILE);
|
|
12434
13665
|
let raw;
|
|
12435
13666
|
try {
|
|
12436
|
-
raw =
|
|
13667
|
+
raw = readFileSync13(filePath, "utf-8");
|
|
12437
13668
|
} catch (err) {
|
|
12438
13669
|
this.logger?.warn({ skill: name, error: err instanceof Error ? err.message : String(err) }, "Failed to read skill file");
|
|
12439
13670
|
return void 0;
|
|
@@ -12461,8 +13692,8 @@ var SkillRegistry = class {
|
|
|
12461
13692
|
for (const entry of entries) {
|
|
12462
13693
|
if (!entry.isDirectory()) continue;
|
|
12463
13694
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
12464
|
-
const skillDir =
|
|
12465
|
-
const skillFile =
|
|
13695
|
+
const skillDir = join19(dir, entry.name);
|
|
13696
|
+
const skillFile = join19(skillDir, SKILL_FILE);
|
|
12466
13697
|
if (!existsAsFile(skillFile)) continue;
|
|
12467
13698
|
const { frontmatter } = safeReadFrontmatter(skillFile);
|
|
12468
13699
|
const description = asString(frontmatter.description);
|
|
@@ -12477,14 +13708,14 @@ var SkillRegistry = class {
|
|
|
12477
13708
|
};
|
|
12478
13709
|
function existsAsFile(path9) {
|
|
12479
13710
|
try {
|
|
12480
|
-
return
|
|
13711
|
+
return statSync6(path9).isFile();
|
|
12481
13712
|
} catch {
|
|
12482
13713
|
return false;
|
|
12483
13714
|
}
|
|
12484
13715
|
}
|
|
12485
13716
|
function safeReadFrontmatter(path9) {
|
|
12486
13717
|
try {
|
|
12487
|
-
return parseFrontmatter(
|
|
13718
|
+
return parseFrontmatter(readFileSync13(path9, "utf-8"));
|
|
12488
13719
|
} catch {
|
|
12489
13720
|
return { frontmatter: {}, body: "" };
|
|
12490
13721
|
}
|
|
@@ -12493,11 +13724,11 @@ function safeReadFrontmatter(path9) {
|
|
|
12493
13724
|
// src/services/skills/skill-dir.ts
|
|
12494
13725
|
import { existsSync as existsSync11 } from "fs";
|
|
12495
13726
|
import { homedir as homedir4 } from "os";
|
|
12496
|
-
import { join as
|
|
13727
|
+
import { join as join20, resolve as resolve11 } from "path";
|
|
12497
13728
|
function resolveSkillDirs(workspaceDir2, configDirs) {
|
|
12498
13729
|
const candidates = [
|
|
12499
|
-
|
|
12500
|
-
|
|
13730
|
+
join20(homedir4(), ".wayang", "skills"),
|
|
13731
|
+
join20(workspaceDir2, ".wayang", "skills"),
|
|
12501
13732
|
...(configDirs ?? []).map((d) => resolve11(d))
|
|
12502
13733
|
];
|
|
12503
13734
|
const seen = /* @__PURE__ */ new Set();
|