wayang-ai 0.8.0 → 0.8.3
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 +1672 -379
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import * as
|
|
4
|
+
import * as path9 from "path";
|
|
5
5
|
|
|
6
6
|
// src/cli-args.ts
|
|
7
7
|
import meow from "meow";
|
|
@@ -463,11 +463,11 @@ var RemoteStateStore = class {
|
|
|
463
463
|
rpc;
|
|
464
464
|
entries = /* @__PURE__ */ new Map();
|
|
465
465
|
byId = /* @__PURE__ */ new Map();
|
|
466
|
-
subscribe(target,
|
|
467
|
-
const key = `${target}:${
|
|
466
|
+
subscribe(target, path10, callback) {
|
|
467
|
+
const key = `${target}:${path10}`;
|
|
468
468
|
let entry = this.entries.get(key);
|
|
469
469
|
if (!entry) {
|
|
470
|
-
entry = { subscriptionId: "", target, path:
|
|
470
|
+
entry = { subscriptionId: "", target, path: path10, data: void 0, listeners: /* @__PURE__ */ new Set(), settled: false };
|
|
471
471
|
this.entries.set(key, entry);
|
|
472
472
|
this.connect(entry, key);
|
|
473
473
|
}
|
|
@@ -477,8 +477,8 @@ var RemoteStateStore = class {
|
|
|
477
477
|
if (entry.listeners.size === 0) this.teardown(entry, key);
|
|
478
478
|
};
|
|
479
479
|
}
|
|
480
|
-
getSnapshot(target,
|
|
481
|
-
return this.entries.get(`${target}:${
|
|
480
|
+
getSnapshot(target, path10) {
|
|
481
|
+
return this.entries.get(`${target}:${path10}`)?.data;
|
|
482
482
|
}
|
|
483
483
|
connect(entry, key) {
|
|
484
484
|
this.rpc.request(METHOD.SUBSCRIBE, { target: entry.target, path: entry.path }).then((res) => {
|
|
@@ -516,11 +516,11 @@ var RemoteSubscribable = class {
|
|
|
516
516
|
}
|
|
517
517
|
store;
|
|
518
518
|
target;
|
|
519
|
-
subscribe(
|
|
520
|
-
return this.store.subscribe(this.target,
|
|
519
|
+
subscribe(path10, callback) {
|
|
520
|
+
return this.store.subscribe(this.target, path10, callback);
|
|
521
521
|
}
|
|
522
|
-
getSnapshot(
|
|
523
|
-
return this.store.getSnapshot(this.target,
|
|
522
|
+
getSnapshot(path10) {
|
|
523
|
+
return this.store.getSnapshot(this.target, path10);
|
|
524
524
|
}
|
|
525
525
|
};
|
|
526
526
|
|
|
@@ -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
|
}
|
|
@@ -1363,43 +1475,43 @@ var BaseWayangState = class {
|
|
|
1363
1475
|
}
|
|
1364
1476
|
// --- Read ---
|
|
1365
1477
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- public default generic: bare get() callers across the repo rely on it; per-call sites pass an explicit T
|
|
1366
|
-
get(
|
|
1367
|
-
return get(this.data,
|
|
1478
|
+
get(path10) {
|
|
1479
|
+
return get(this.data, path10);
|
|
1368
1480
|
}
|
|
1369
1481
|
// --- Write operations ---
|
|
1370
|
-
set(
|
|
1371
|
-
const prev = get(this.data,
|
|
1372
|
-
set(this.data,
|
|
1373
|
-
this.persist(
|
|
1374
|
-
this.notify(
|
|
1482
|
+
set(path10, value) {
|
|
1483
|
+
const prev = get(this.data, path10);
|
|
1484
|
+
set(this.data, path10, value);
|
|
1485
|
+
this.persist(path10, "set");
|
|
1486
|
+
this.notify(path10, "set", prev);
|
|
1375
1487
|
}
|
|
1376
1488
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- partial update merges an untyped object into the state tree root (see `data`)
|
|
1377
|
-
update(
|
|
1378
|
-
const prev = cloneDeep(get(this.data,
|
|
1379
|
-
const target = get(this.data,
|
|
1489
|
+
update(path10, partial) {
|
|
1490
|
+
const prev = cloneDeep(get(this.data, path10));
|
|
1491
|
+
const target = get(this.data, path10);
|
|
1380
1492
|
if (target && typeof target === "object") {
|
|
1381
1493
|
Object.assign(target, partial);
|
|
1382
1494
|
} else {
|
|
1383
|
-
set(this.data,
|
|
1495
|
+
set(this.data, path10, partial);
|
|
1384
1496
|
}
|
|
1385
|
-
this.persist(
|
|
1386
|
-
this.notify(
|
|
1497
|
+
this.persist(path10, "update");
|
|
1498
|
+
this.notify(path10, "update", prev);
|
|
1387
1499
|
}
|
|
1388
|
-
append(
|
|
1389
|
-
const arr = get(this.data,
|
|
1500
|
+
append(path10, entry) {
|
|
1501
|
+
const arr = get(this.data, path10);
|
|
1390
1502
|
if (!Array.isArray(arr)) {
|
|
1391
|
-
throw new Error(`State.append: path "${
|
|
1503
|
+
throw new Error(`State.append: path "${path10}" is not an array`);
|
|
1392
1504
|
}
|
|
1393
|
-
const stamped =
|
|
1505
|
+
const stamped = path10 === "conversation" ? applyAppendEnvelope(entry, arr) : entry;
|
|
1394
1506
|
arr.push(stamped);
|
|
1395
|
-
set(this.data,
|
|
1396
|
-
this.persist(
|
|
1397
|
-
this.notify(
|
|
1507
|
+
set(this.data, path10, [...arr]);
|
|
1508
|
+
this.persist(path10, "append", stamped);
|
|
1509
|
+
this.notify(path10, "append", void 0);
|
|
1398
1510
|
}
|
|
1399
|
-
remove(
|
|
1400
|
-
const arr = get(this.data,
|
|
1511
|
+
remove(path10, idx) {
|
|
1512
|
+
const arr = get(this.data, path10);
|
|
1401
1513
|
if (!Array.isArray(arr)) {
|
|
1402
|
-
throw new Error(`State.remove: path "${
|
|
1514
|
+
throw new Error(`State.remove: path "${path10}" is not an array`);
|
|
1403
1515
|
}
|
|
1404
1516
|
const prev = idx !== void 0 ? arr[idx] : arr[arr.length - 1];
|
|
1405
1517
|
if (idx === void 0) {
|
|
@@ -1407,16 +1519,16 @@ var BaseWayangState = class {
|
|
|
1407
1519
|
} else {
|
|
1408
1520
|
arr.splice(idx, 1);
|
|
1409
1521
|
}
|
|
1410
|
-
this.persist(
|
|
1411
|
-
this.notify(
|
|
1522
|
+
this.persist(path10, "remove");
|
|
1523
|
+
this.notify(path10, "remove", prev);
|
|
1412
1524
|
}
|
|
1413
1525
|
// --- Subscription ---
|
|
1414
|
-
on(
|
|
1415
|
-
if (!this.subscriptions.has(
|
|
1416
|
-
this.subscriptions.set(
|
|
1526
|
+
on(path10, callback) {
|
|
1527
|
+
if (!this.subscriptions.has(path10)) {
|
|
1528
|
+
this.subscriptions.set(path10, /* @__PURE__ */ new Set());
|
|
1417
1529
|
}
|
|
1418
|
-
this.subscriptions.get(
|
|
1419
|
-
return () => this.subscriptions.get(
|
|
1530
|
+
this.subscriptions.get(path10).add(callback);
|
|
1531
|
+
return () => this.subscriptions.get(path10)?.delete(callback);
|
|
1420
1532
|
}
|
|
1421
1533
|
// --- Persistence ---
|
|
1422
1534
|
/**
|
|
@@ -1424,8 +1536,8 @@ var BaseWayangState = class {
|
|
|
1424
1536
|
* for the helper to pick its own strategy. Snapshot helpers replace the
|
|
1425
1537
|
* file; append helpers write only the new entry and ignore window edits.
|
|
1426
1538
|
*/
|
|
1427
|
-
persist(
|
|
1428
|
-
const topKey =
|
|
1539
|
+
persist(path10, op, entry) {
|
|
1540
|
+
const topKey = path10.split(".")[0];
|
|
1429
1541
|
for (const mapping of this.persistMappings) {
|
|
1430
1542
|
if (mapping.path !== topKey) continue;
|
|
1431
1543
|
mapping.helper.write({
|
|
@@ -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 path10 = workspace?.path?.trim();
|
|
2173
|
+
return path10 ? workspaceSlugFromPath(path10) : 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);
|
|
@@ -2143,27 +2364,27 @@ function toolTarget(toolName, args, budget) {
|
|
|
2143
2364
|
return fitWithSuffix(lines[0].trim(), note, budget);
|
|
2144
2365
|
}
|
|
2145
2366
|
case "write_file": {
|
|
2146
|
-
const
|
|
2147
|
-
if (!
|
|
2367
|
+
const path10 = argString(args, ["path", "file_path"]);
|
|
2368
|
+
if (!path10) return "";
|
|
2148
2369
|
const lines = lineCount(args.content);
|
|
2149
2370
|
const note = lines > 0 ? ` (+${lines})` : "";
|
|
2150
|
-
return fitWithSuffix(shortenPath(
|
|
2371
|
+
return fitWithSuffix(shortenPath(path10), note, budget);
|
|
2151
2372
|
}
|
|
2152
2373
|
case "edit_file": {
|
|
2153
|
-
const
|
|
2154
|
-
if (!
|
|
2374
|
+
const path10 = argString(args, ["path", "file_path"]);
|
|
2375
|
+
if (!path10) return "";
|
|
2155
2376
|
const oldLines = lineCount(args.old_string);
|
|
2156
2377
|
const newLines = lineCount(args.new_string);
|
|
2157
2378
|
const note = oldLines > 0 || newLines > 0 ? ` (${oldLines}\u2192${newLines} \u884C)` : "";
|
|
2158
|
-
return fitWithSuffix(shortenPath(
|
|
2379
|
+
return fitWithSuffix(shortenPath(path10), note, budget);
|
|
2159
2380
|
}
|
|
2160
2381
|
case "read_file": {
|
|
2161
|
-
const
|
|
2162
|
-
if (!
|
|
2382
|
+
const path10 = argString(args, ["path", "file_path"]);
|
|
2383
|
+
if (!path10) return "";
|
|
2163
2384
|
const offset = argNumber(args, ["offset"]);
|
|
2164
2385
|
const limit = argNumber(args, ["limit"]);
|
|
2165
2386
|
const range = offset !== void 0 ? `:${offset}${limit !== void 0 ? `+${limit}` : ""}` : "";
|
|
2166
|
-
return truncateToWidth(shortenPath(
|
|
2387
|
+
return truncateToWidth(shortenPath(path10) + range, budget);
|
|
2167
2388
|
}
|
|
2168
2389
|
case "search_files":
|
|
2169
2390
|
return truncateToWidth(argString(args, ["pattern"]) ?? "", budget);
|
|
@@ -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
|
-
|
|
2982
|
-
|
|
2983
|
-
function formatWorkspaceSummary(workspaces) {
|
|
2984
|
-
if (!workspaces || workspaces.length === 0) return null;
|
|
2985
|
-
const slugs = workspaces.map((workspace) => workspace?.slug?.trim()).filter((slug) => !!slug);
|
|
2986
|
-
if (slugs.length === 0) return null;
|
|
2987
|
-
if (slugs.length === 1) return slugs[0];
|
|
2988
|
-
return `${slugs[0]} +${slugs.length - 1}`;
|
|
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);
|
|
2989
3236
|
}
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
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",
|
|
@@ -3768,15 +4063,15 @@ import { Box as Box8, Text as Text9, useStdout } from "ink";
|
|
|
3768
4063
|
|
|
3769
4064
|
// src/tui/hooks/use-wayang-state.ts
|
|
3770
4065
|
import { useState as useState4, useEffect as useEffect3 } from "react";
|
|
3771
|
-
function useWayangState(source,
|
|
3772
|
-
const [value, setValue] = useState4(() => source.getSnapshot(
|
|
4066
|
+
function useWayangState(source, path10) {
|
|
4067
|
+
const [value, setValue] = useState4(() => source.getSnapshot(path10));
|
|
3773
4068
|
useEffect3(() => {
|
|
3774
|
-
const unSub = source.subscribe(
|
|
3775
|
-
setValue(source.getSnapshot(
|
|
4069
|
+
const unSub = source.subscribe(path10, () => {
|
|
4070
|
+
setValue(source.getSnapshot(path10));
|
|
3776
4071
|
});
|
|
3777
|
-
setValue(source.getSnapshot(
|
|
4072
|
+
setValue(source.getSnapshot(path10));
|
|
3778
4073
|
return unSub;
|
|
3779
|
-
}, [source,
|
|
4074
|
+
}, [source, path10]);
|
|
3780
4075
|
return value;
|
|
3781
4076
|
}
|
|
3782
4077
|
|
|
@@ -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.3",
|
|
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}`);
|
|
@@ -5137,11 +5471,11 @@ var SignalQueue = class {
|
|
|
5137
5471
|
this.counter = Math.max(mark, this.maxIdInWindow());
|
|
5138
5472
|
}
|
|
5139
5473
|
// --- Subscribable ---
|
|
5140
|
-
subscribe(
|
|
5141
|
-
return this.state.on(
|
|
5474
|
+
subscribe(path10, callback) {
|
|
5475
|
+
return this.state.on(path10, callback);
|
|
5142
5476
|
}
|
|
5143
|
-
getSnapshot(
|
|
5144
|
-
return this.state.get(
|
|
5477
|
+
getSnapshot(path10) {
|
|
5478
|
+
return this.state.get(path10);
|
|
5145
5479
|
}
|
|
5146
5480
|
enqueue(sig) {
|
|
5147
5481
|
const id = `sig-${++this.counter}`;
|
|
@@ -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
|
|
@@ -5770,9 +6104,9 @@ function workerTranscriptPath(sessionDir, workerId) {
|
|
|
5770
6104
|
}
|
|
5771
6105
|
function readWorkerTranscript(sessionDir, workerId) {
|
|
5772
6106
|
if (!isValidWorkerId(workerId)) return null;
|
|
5773
|
-
const
|
|
5774
|
-
if (!existsSync8(
|
|
5775
|
-
return new JSONLFileHelper(
|
|
6107
|
+
const path10 = workerTranscriptPath(sessionDir, workerId);
|
|
6108
|
+
if (!existsSync8(path10)) return null;
|
|
6109
|
+
return new JSONLFileHelper(path10).read();
|
|
5776
6110
|
}
|
|
5777
6111
|
var MAX_TRANSCRIPT_WINDOW_BYTES = 2 * 1024 * 1024;
|
|
5778
6112
|
var MIN_WINDOW_BYTES = 64 * 1024;
|
|
@@ -5802,10 +6136,10 @@ function parseWindow(text, startsAtFileHead) {
|
|
|
5802
6136
|
}
|
|
5803
6137
|
function readWorkerTranscriptWindow(sessionDir, workerId, opts = {}) {
|
|
5804
6138
|
if (!isValidWorkerId(workerId)) return null;
|
|
5805
|
-
const
|
|
6139
|
+
const path10 = workerTranscriptPath(sessionDir, workerId);
|
|
5806
6140
|
let size;
|
|
5807
6141
|
try {
|
|
5808
|
-
size =
|
|
6142
|
+
size = statSync2(path10).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(path10, "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;
|
|
@@ -6723,6 +7057,23 @@ ${lines}`
|
|
|
6723
7057
|
);
|
|
6724
7058
|
}
|
|
6725
7059
|
|
|
7060
|
+
// src/infra/general-workspace.ts
|
|
7061
|
+
import { mkdirSync as mkdirSync6 } from "fs";
|
|
7062
|
+
import { homedir } from "os";
|
|
7063
|
+
import { join as join14 } from "path";
|
|
7064
|
+
var GENERAL_WORKSPACE_DIRNAME = "workspace";
|
|
7065
|
+
function wayangDirFor(ctx) {
|
|
7066
|
+
return wayangDirForSessionDir(ctx.sessionDir) ?? join14(homedir(), ".wayang");
|
|
7067
|
+
}
|
|
7068
|
+
function resolveGeneralWorkspace(ctx) {
|
|
7069
|
+
return join14(wayangDirFor(ctx), GENERAL_WORKSPACE_DIRNAME);
|
|
7070
|
+
}
|
|
7071
|
+
function ensureGeneralWorkspace(ctx) {
|
|
7072
|
+
const dir = resolveGeneralWorkspace(ctx);
|
|
7073
|
+
mkdirSync6(dir, { recursive: true });
|
|
7074
|
+
return dir;
|
|
7075
|
+
}
|
|
7076
|
+
|
|
6726
7077
|
// src/services/agents/prompts/controller-prompt.ts
|
|
6727
7078
|
var IDENTITY = section(
|
|
6728
7079
|
"Identity",
|
|
@@ -6999,13 +7350,17 @@ You resumed a previous session that had unfinished tasks. Workers were NOT autom
|
|
|
6999
7350
|
|
|
7000
7351
|
Do NOT blindly re-create everything. The user may have moved on. Prefer to summarize and ask unless the intent is obvious.`
|
|
7001
7352
|
);
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
`\u3010\u4EFB\u52A1\u62C6\u5206\u3011
|
|
7353
|
+
function buildWorkingPrinciples(generalWorkspace) {
|
|
7354
|
+
const body = `\u3010\u4EFB\u52A1\u62C6\u5206\u3011
|
|
7005
7355
|
- \u4E00\u4E2A\u4EFB\u52A1\u53EA\u4EA4\u4ED8\u4E00\u4E2A\u53EF\u72EC\u7ACB\u9A8C\u6536\u7684\u7ED3\u679C\uFF1B\u4E0D\u8981\u628A\u8BBE\u8BA1\u3001\u5B9E\u73B0\u3001\u6D4B\u8BD5\u585E\u8FDB\u540C\u4E00\u6BB5\u3002
|
|
7006
7356
|
- \u5148\u505A\u80FD\u88AB\u5355\u6D4B\u65AD\u8A00\u7684\u6838\u5FC3\u903B\u8F91\uFF0C\u628A\u8FB9\u754C\uFF08\u5BBD\u5B57\u7B26\u3001\u4EE3\u7406\u5BF9\u3001\u6298\u884C\u4F4D\u7F6E\u4E4B\u7C7B\uFF09\u56FA\u5316\u6210\u7528\u4F8B\uFF0C\u518D\u53E0\u52A0\u96C6\u6210\u4E0E\u4EA4\u4E92\u5C42\u3002
|
|
7007
7357
|
- \u4EFB\u52A1\u63CF\u8FF0\u5FC5\u987B\u81EA\u5305\u542B\uFF1A\u80CC\u666F\u3001\u76EE\u6807\u3001\u8F93\u5165\u8F93\u51FA\u3001\u7EA6\u675F\u3001\u9A8C\u6536\u6807\u51C6\u3002\u6267\u884C\u65B9\u770B\u4E0D\u5230\u4F60\u7684\u5BF9\u8BDD\u3002
|
|
7008
7358
|
|
|
7359
|
+
\u3010\u5DE5\u4F5C\u533A\u3011
|
|
7360
|
+
- \u7528\u6237\u7ED9\u4E86\u76EE\u5F55\uFF0C\u6216\u4EFB\u52A1\u660E\u663E\u5C5E\u4E8E\u67D0\u4E2A\u4EE3\u7801\u5E93\uFF1A\u7528\u90A3\u4E2A\u76EE\u5F55\u3002
|
|
7361
|
+
- \u7528\u6237\u6CA1\u6307\u5B9A\u3001\u6216\u4EFB\u52A1\u4E0E\u4EE3\u7801\u5E93\u65E0\u5173\uFF08\u8C03\u7814\u3001\u8D44\u6599\u6574\u7406\u3001\u4FE1\u606F\u67E5\u8BE2\u3001\u5199\u6587\u6863\u8349\u7A3F\uFF09\uFF1A\u7528\u901A\u7528\u5DE5\u4F5C\u533A ${generalWorkspace}\uFF0C\u5B83\u4E0D\u5C5E\u4E8E\u4EFB\u4F55\u4ED3\u5E93\uFF0C\u9996\u6B21\u4F7F\u7528\u81EA\u52A8\u521B\u5EFA\u3002
|
|
7362
|
+
- \u4EFB\u52A1\u4F1A\u7559\u4E0B\u4E2D\u95F4\u4EA7\u7269\u65F6\u5199\u8FDB\u63CF\u8FF0\uFF1A\u4E34\u65F6\u6587\u4EF6\u653E\u4EFB\u52A1 workspace \u5185\u7684 .agents/tmp/\uFF0C\u4E0D\u8981\u7528 /tmp\u3002
|
|
7363
|
+
|
|
7009
7364
|
\u3010\u5206\u5DE5\u4E0E\u5BA1\u6838\u3011
|
|
7010
7365
|
- \u8BBE\u8BA1\u5224\u65AD\u4E0E\u673A\u68B0\u5B9E\u73B0\u5206\u5F00\u6D3E\uFF1A\u8BBE\u8BA1\u65B9\u5148\u5B9E\u6D4B\u5173\u952E\u884C\u4E3A\uFF0C\u4EA7\u51FA\u300C\u6539\u54EA\u4E2A\u6587\u4EF6\u3001\u51FD\u6570\u7B7E\u540D\u3001\u6570\u636E\u7ED3\u6784\u3001\u8FB9\u754C\u60C5\u51B5\u3001\u6D4B\u8BD5\u6E05\u5355\u300D\u7684\u65B9\u6848\uFF1B\u6267\u884C\u65B9\u7167\u65B9\u6848\u7F16\u7801\uFF0C\u4E0D\u505A\u8BBE\u8BA1\u51B3\u7B56\u3002
|
|
7011
7366
|
- \u65B9\u6848\u91CC\u8981\u663E\u5F0F\u5199\u51FA\u300C\u4E0D\u8981\u505A\u4EC0\u4E48\u300D\uFF0C\u5426\u5219\u6267\u884C\u65B9\u4F1A\u987A\u624B\u91CD\u6784\u65E0\u5173\u4EE3\u7801\u3002
|
|
@@ -7030,8 +7385,9 @@ var WORKING_PRINCIPLES = section(
|
|
|
7030
7385
|
- \u540C\u533A\u4E92\u5377 \u2014\u2014 \u8BA9\u4E24\u4E2A\u6267\u884C\u8005\u5728\u540C\u4E00\u5DE5\u4F5C\u533A\u5E76\u53D1\u63D0\u4EA4\uFF0C\u4E92\u76F8\u628A\u5BF9\u65B9\u7684\u6539\u52A8\u5377\u8FDB\u81EA\u5DF1\u7684 commit\u3002
|
|
7031
7386
|
- \u9759\u9ED8\u5224\u6B7B \u2014\u2014 \u51ED\u8F93\u51FA\u9759\u9ED8\u5224\u65AD\u6B7B\u6D3B\uFF1A\u628A\u5728\u8DD1\u957F\u4EFB\u52A1\u5F53\u6210\u5361\u6B7B\u6740\u6389\uFF0C\u6216\u628A\u6D3B\u9501\u5F53\u6210\u6162\u4EFB\u52A1\u6B7B\u7B49\u3002
|
|
7032
7387
|
- \u7EFF\u706F\u5373\u5B8C\u6210 \u2014\u2014 \u5355\u6D4B\u6216\u6D41\u7A0B\u5168\u7EFF\u5C31\u5BA3\u5E03\u5B8C\u6210\uFF1B\u5076\u53D1\u5931\u8D25\u590D\u8DD1\u4E00\u6B21\u901A\u8FC7\u5C31\u5F53\u6CA1\u53D1\u751F\uFF0C\u4E0D\u505A\u8BB0\u5F55\u3002
|
|
7033
|
-
- \u96F6\u788E\u53D6\u8BC1 \u2014\u2014 \u628A\u4E00\u6B21\u53D6\u8BC1\u62C6\u6210\u51E0\u5341\u6761\u96F6\u788E\u53EA\u8BFB\u547D\u4EE4\u9010\u6761\u7B49\u6279\u51C6\uFF0C\u6392\u969C\u88AB\u6D41\u7A0B\u672C\u8EAB\u62D6\u6B7B\u3002
|
|
7034
|
-
);
|
|
7388
|
+
- \u96F6\u788E\u53D6\u8BC1 \u2014\u2014 \u628A\u4E00\u6B21\u53D6\u8BC1\u62C6\u6210\u51E0\u5341\u6761\u96F6\u788E\u53EA\u8BFB\u547D\u4EE4\u9010\u6761\u7B49\u6279\u51C6\uFF0C\u6392\u969C\u88AB\u6D41\u7A0B\u672C\u8EAB\u62D6\u6B7B\u3002`;
|
|
7389
|
+
return section("Working principles", body);
|
|
7390
|
+
}
|
|
7035
7391
|
var HARD_CONSTRAINTS = section(
|
|
7036
7392
|
"Hard constraints",
|
|
7037
7393
|
`- NEVER execute commands or write files yourself. Delegate via add_task\uFF08worker will execute\uFF09.
|
|
@@ -7050,24 +7406,93 @@ function buildWorkerList(workers) {
|
|
|
7050
7406
|
}
|
|
7051
7407
|
return section("Available workers", lines.join("\n"));
|
|
7052
7408
|
}
|
|
7053
|
-
function buildControllerSystemPrompt(ctx, skills) {
|
|
7409
|
+
function buildControllerSystemPrompt(ctx, skills, opts) {
|
|
7054
7410
|
return assemble(
|
|
7055
7411
|
IDENTITY,
|
|
7056
7412
|
RESPONSE_STYLE,
|
|
7057
7413
|
TOOL_USAGE,
|
|
7058
7414
|
PLANNING_TOOLS,
|
|
7059
7415
|
SIGNAL_HANDLING,
|
|
7060
|
-
|
|
7416
|
+
buildWorkingPrinciples(resolveGeneralWorkspace(ctx)),
|
|
7061
7417
|
HARD_CONSTRAINTS,
|
|
7062
7418
|
buildWorkerList(ctx.config.workers),
|
|
7063
7419
|
buildSkillCatalog(skills),
|
|
7064
|
-
buildEnvironment(ctx)
|
|
7420
|
+
buildEnvironment(ctx),
|
|
7421
|
+
// Project conventions go last: they are the most specific instruction and
|
|
7422
|
+
// the last word on conflicts with the generic sections above.
|
|
7423
|
+
opts?.rules
|
|
7065
7424
|
);
|
|
7066
7425
|
}
|
|
7067
7426
|
|
|
7427
|
+
// src/services/agents/prompts/controller-rules.ts
|
|
7428
|
+
import { readFileSync as readFileSync8, realpathSync as realpathSync2, statSync as statSync3 } from "fs";
|
|
7429
|
+
import { join as join15 } from "path";
|
|
7430
|
+
var CONTROLLER_RULES_FILENAME = "AGENTS.md";
|
|
7431
|
+
var CONTROLLER_RULES_MAX_BYTES = 32 * 1024;
|
|
7432
|
+
function loadControllerRules(opts) {
|
|
7433
|
+
const maxBytes = opts.maxBytes ?? CONTROLLER_RULES_MAX_BYTES;
|
|
7434
|
+
const files = [];
|
|
7435
|
+
for (const dir of [opts.homeDir, opts.cwd]) {
|
|
7436
|
+
if (!dir) continue;
|
|
7437
|
+
const file = readRulesFile(dir);
|
|
7438
|
+
if (!file) continue;
|
|
7439
|
+
if (files.some((seen) => seen.path === file.path)) continue;
|
|
7440
|
+
files.push(file);
|
|
7441
|
+
}
|
|
7442
|
+
if (files.length === 0) return "";
|
|
7443
|
+
const bodies = [];
|
|
7444
|
+
let usedBytes = 0;
|
|
7445
|
+
let truncated = false;
|
|
7446
|
+
for (const file of files) {
|
|
7447
|
+
const heading = `## Project rules (from ${file.path})`;
|
|
7448
|
+
const size = Buffer.byteLength(file.content, "utf8");
|
|
7449
|
+
if (usedBytes + size <= maxBytes) {
|
|
7450
|
+
bodies.push(`${heading}
|
|
7451
|
+
|
|
7452
|
+
${file.content.trimEnd()}`);
|
|
7453
|
+
usedBytes += size;
|
|
7454
|
+
continue;
|
|
7455
|
+
}
|
|
7456
|
+
const budget = maxBytes - usedBytes;
|
|
7457
|
+
const head = budget > 0 ? truncateToBytes(file.content, budget).trimEnd() : "";
|
|
7458
|
+
bodies.push(head ? `${heading}
|
|
7459
|
+
|
|
7460
|
+
${head}` : heading);
|
|
7461
|
+
truncated = true;
|
|
7462
|
+
break;
|
|
7463
|
+
}
|
|
7464
|
+
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.`;
|
|
7465
|
+
const parts = [`# Project rules
|
|
7466
|
+
|
|
7467
|
+
${intro}`, ...bodies];
|
|
7468
|
+
if (truncated) {
|
|
7469
|
+
parts.push(
|
|
7470
|
+
`...truncated at ${maxBytes} bytes (rules content exceeded the ${maxBytes}-byte total limit: the rest of the last file and any later files were dropped)`
|
|
7471
|
+
);
|
|
7472
|
+
}
|
|
7473
|
+
return parts.join("\n\n");
|
|
7474
|
+
}
|
|
7475
|
+
function readRulesFile(dir) {
|
|
7476
|
+
const candidate = join15(dir, CONTROLLER_RULES_FILENAME);
|
|
7477
|
+
try {
|
|
7478
|
+
const path10 = realpathSync2(candidate);
|
|
7479
|
+
if (!statSync3(path10).isFile()) return null;
|
|
7480
|
+
const content = readFileSync8(path10, "utf8");
|
|
7481
|
+
if (!content.trim()) return null;
|
|
7482
|
+
return { path: path10, content };
|
|
7483
|
+
} catch {
|
|
7484
|
+
return null;
|
|
7485
|
+
}
|
|
7486
|
+
}
|
|
7487
|
+
function truncateToBytes(text, maxBytes) {
|
|
7488
|
+
const buf = Buffer.from(text, "utf8");
|
|
7489
|
+
if (buf.length <= maxBytes) return text;
|
|
7490
|
+
return buf.subarray(0, maxBytes).toString("utf8").replace(/\uFFFD+$/, "");
|
|
7491
|
+
}
|
|
7492
|
+
|
|
7068
7493
|
// src/services/agents/prompts/project-rules.ts
|
|
7069
|
-
import { existsSync as existsSync9, readFileSync as
|
|
7070
|
-
import { basename as basename5, dirname as dirname6, join as
|
|
7494
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, statSync as statSync4 } from "fs";
|
|
7495
|
+
import { basename as basename5, dirname as dirname6, join as join16, resolve as resolve3 } from "path";
|
|
7071
7496
|
var RULE_FILE_NAMES = ["AGENTS.md", "CLAUDE.md"];
|
|
7072
7497
|
var MAX_LEVELS = 5;
|
|
7073
7498
|
var PROJECT_RULES_MAX_LENGTH = 8e3;
|
|
@@ -7076,7 +7501,7 @@ function findProjectRules(startDir) {
|
|
|
7076
7501
|
for (let level = 0; level < MAX_LEVELS; level++) {
|
|
7077
7502
|
const found = readRuleFile(current);
|
|
7078
7503
|
if (found) return found;
|
|
7079
|
-
if (existsSync9(
|
|
7504
|
+
if (existsSync9(join16(current, ".git"))) break;
|
|
7080
7505
|
const parent = dirname6(current);
|
|
7081
7506
|
if (parent === current) break;
|
|
7082
7507
|
current = parent;
|
|
@@ -7085,12 +7510,12 @@ function findProjectRules(startDir) {
|
|
|
7085
7510
|
}
|
|
7086
7511
|
function readRuleFile(dir) {
|
|
7087
7512
|
for (const name of RULE_FILE_NAMES) {
|
|
7088
|
-
const
|
|
7513
|
+
const path10 = join16(dir, name);
|
|
7089
7514
|
try {
|
|
7090
|
-
if (!
|
|
7091
|
-
const content =
|
|
7515
|
+
if (!statSync4(path10).isFile()) continue;
|
|
7516
|
+
const content = readFileSync9(path10, "utf8");
|
|
7092
7517
|
if (!content.trim()) continue;
|
|
7093
|
-
return { path:
|
|
7518
|
+
return { path: path10, content };
|
|
7094
7519
|
} catch {
|
|
7095
7520
|
}
|
|
7096
7521
|
}
|
|
@@ -7166,6 +7591,9 @@ Only use bash when no dedicated tool fits the task (e.g. running builds, install
|
|
|
7166
7591
|
- edit_file modifies an existing file by replacing a unique string. The old_string must appear exactly once. If it matches multiple times, the tool will tell you \u2014 expand the surrounding context to make it unique.
|
|
7167
7592
|
- Always verify changes: after write_file or edit_file, use read_file to confirm the content is correct. For large files, use offset and limit to read specific sections instead of the entire file.
|
|
7168
7593
|
- Use relative paths (relative to workspace). Accessing files outside workspace requires controller approval.
|
|
7594
|
+
- Scratch files and intermediate artifacts go to \`.agents/tmp/\` inside the current task workspace. Never \`/tmp\` \u2014 it sits outside the workspace, so every command there costs an approval \u2014 and never the home directory or the repository root.
|
|
7595
|
+
- Anything worth keeping goes to a normal path in the workspace (it is version-controlled); one-off intermediates stay in \`.agents/tmp/\`.
|
|
7596
|
+
- Before writing, confirm which workspace you are in: \`pwd\`, or the workspace named in the task description \u2014 that one outranks any path you assume.
|
|
7169
7597
|
|
|
7170
7598
|
## Web search
|
|
7171
7599
|
|
|
@@ -7285,6 +7713,7 @@ var RULES = [
|
|
|
7285
7713
|
"Report meaningful progress as you work (entering a new phase, completing a sub-step, recovering from an error).",
|
|
7286
7714
|
"Do NOT output a final summary right before completing \u2014 the system already captures the result separately.",
|
|
7287
7715
|
"Match the language of the task description.",
|
|
7716
|
+
"Keep scratch files and intermediate artifacts in `.agents/tmp/` inside the task workspace \u2014 never `/tmp`, the home directory, or the repository root.",
|
|
7288
7717
|
"</Rules>"
|
|
7289
7718
|
].join("\n");
|
|
7290
7719
|
function buildThirdPartyPrompt(skills) {
|
|
@@ -7477,11 +7906,11 @@ var WorkerAgent = class extends BaseAgent {
|
|
|
7477
7906
|
return this.state.get("conversation") ?? [];
|
|
7478
7907
|
}
|
|
7479
7908
|
// --- Subscribable ---
|
|
7480
|
-
subscribe(
|
|
7481
|
-
return this.state.on(
|
|
7909
|
+
subscribe(path10, callback) {
|
|
7910
|
+
return this.state.on(path10, callback);
|
|
7482
7911
|
}
|
|
7483
|
-
getSnapshot(
|
|
7484
|
-
return this.state.get(
|
|
7912
|
+
getSnapshot(path10) {
|
|
7913
|
+
return this.state.get(path10);
|
|
7485
7914
|
}
|
|
7486
7915
|
/**
|
|
7487
7916
|
* Accept a message from the controller — writes to internal inbox. The
|
|
@@ -7588,9 +8017,9 @@ function toolResultBlockToRecord(block, toolName, endedAt) {
|
|
|
7588
8017
|
import { resolve as resolve5 } from "path";
|
|
7589
8018
|
|
|
7590
8019
|
// src/infra/permission/policy.ts
|
|
7591
|
-
import { realpathSync as
|
|
7592
|
-
import { basename as basename6, dirname as dirname7, isAbsolute, join as
|
|
7593
|
-
var
|
|
8020
|
+
import { realpathSync as realpathSync3 } from "fs";
|
|
8021
|
+
import { basename as basename6, dirname as dirname7, isAbsolute, join as join17, resolve as resolve4, sep } from "path";
|
|
8022
|
+
var DEFAULT_SENSITIVE_RELATIVE_PATHS = [
|
|
7594
8023
|
".ssh",
|
|
7595
8024
|
".gnupg",
|
|
7596
8025
|
".aws",
|
|
@@ -7602,9 +8031,7 @@ var DEFAULT_DENIED_RELATIVE_PATHS = [
|
|
|
7602
8031
|
".git-credentials",
|
|
7603
8032
|
".npmrc",
|
|
7604
8033
|
".pypirc",
|
|
7605
|
-
".config/gh"
|
|
7606
|
-
".wayang.config.json",
|
|
7607
|
-
".wayang"
|
|
8034
|
+
".config/gh"
|
|
7608
8035
|
];
|
|
7609
8036
|
var SENSITIVE_SEGMENTS = /* @__PURE__ */ new Set([
|
|
7610
8037
|
".ssh",
|
|
@@ -7616,20 +8043,17 @@ var SENSITIVE_SEGMENTS = /* @__PURE__ */ new Set([
|
|
|
7616
8043
|
".netrc",
|
|
7617
8044
|
".git-credentials",
|
|
7618
8045
|
".npmrc",
|
|
7619
|
-
".pypirc"
|
|
7620
|
-
// The wayang data dir (sessions/conversations) — same intent as the
|
|
7621
|
-
// home-anchored `.wayang` entry, but applied wherever it appears.
|
|
7622
|
-
".wayang"
|
|
7623
|
-
]);
|
|
7624
|
-
var SENSITIVE_FILENAMES = /* @__PURE__ */ new Set([
|
|
7625
|
-
".wayang.config.json"
|
|
8046
|
+
".pypirc"
|
|
7626
8047
|
]);
|
|
8048
|
+
var SENSITIVE_SEGMENT_PAIRS = [
|
|
8049
|
+
[".docker", "config.json"]
|
|
8050
|
+
];
|
|
7627
8051
|
var MAX_ANCESTOR_DEPTH = 32;
|
|
7628
8052
|
function normalizePath(p, opts = {}) {
|
|
7629
8053
|
const base = opts.base ?? process.cwd();
|
|
7630
8054
|
const lexical = isAbsolute(p) ? resolve4(p) : resolve4(base, p);
|
|
7631
8055
|
try {
|
|
7632
|
-
return
|
|
8056
|
+
return realpathSync3(lexical);
|
|
7633
8057
|
} catch {
|
|
7634
8058
|
return resolveDeepestExistingAncestor(lexical);
|
|
7635
8059
|
}
|
|
@@ -7639,8 +8063,8 @@ function resolveDeepestExistingAncestor(lexical) {
|
|
|
7639
8063
|
let candidate = lexical;
|
|
7640
8064
|
for (let depth = 0; depth <= MAX_ANCESTOR_DEPTH; depth += 1) {
|
|
7641
8065
|
try {
|
|
7642
|
-
const resolved =
|
|
7643
|
-
return peeled.length === 0 ? resolved : resolve4(
|
|
8066
|
+
const resolved = realpathSync3(candidate);
|
|
8067
|
+
return peeled.length === 0 ? resolved : resolve4(join17(resolved, ...peeled));
|
|
7644
8068
|
} catch {
|
|
7645
8069
|
}
|
|
7646
8070
|
const parent = dirname7(candidate);
|
|
@@ -7650,8 +8074,8 @@ function resolveDeepestExistingAncestor(lexical) {
|
|
|
7650
8074
|
}
|
|
7651
8075
|
return lexical;
|
|
7652
8076
|
}
|
|
7653
|
-
function
|
|
7654
|
-
return
|
|
8077
|
+
function defaultSensitivePaths(homeDir2) {
|
|
8078
|
+
return DEFAULT_SENSITIVE_RELATIVE_PATHS.map((rel) => resolve4(homeDir2, rel));
|
|
7655
8079
|
}
|
|
7656
8080
|
function isPathWithin(parent, child) {
|
|
7657
8081
|
if (parent === sep) return child.startsWith(sep);
|
|
@@ -7659,21 +8083,38 @@ function isPathWithin(parent, child) {
|
|
|
7659
8083
|
const prefix = parent.endsWith(sep) ? parent : parent + sep;
|
|
7660
8084
|
return child.startsWith(prefix);
|
|
7661
8085
|
}
|
|
7662
|
-
function isDenied(absPath,
|
|
7663
|
-
return
|
|
8086
|
+
function isDenied(absPath, sensitivePaths) {
|
|
8087
|
+
return sensitivePaths.some((sensitive) => isPathWithin(sensitive, absPath));
|
|
7664
8088
|
}
|
|
7665
|
-
function
|
|
8089
|
+
function matchSensitiveEntry(absPath) {
|
|
7666
8090
|
const segments = absPath.split(sep).filter((s) => s.length > 0);
|
|
7667
8091
|
for (let i = 0; i < segments.length; i += 1) {
|
|
7668
8092
|
const segment = segments[i];
|
|
7669
|
-
if (SENSITIVE_SEGMENTS.has(segment)
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
8093
|
+
if (SENSITIVE_SEGMENTS.has(segment)) return { entry: segment, source: "segment" };
|
|
8094
|
+
for (const [dir, child] of SENSITIVE_SEGMENT_PAIRS) {
|
|
8095
|
+
if (segment === dir && segments[i + 1] === child) {
|
|
8096
|
+
return { entry: `${dir}/${child}`, source: "pair" };
|
|
8097
|
+
}
|
|
7674
8098
|
}
|
|
7675
8099
|
}
|
|
7676
|
-
return
|
|
8100
|
+
return void 0;
|
|
8101
|
+
}
|
|
8102
|
+
function sensitivePathEntries(homeDir2, extraSensitiveDirs) {
|
|
8103
|
+
const builtIn = DEFAULT_SENSITIVE_RELATIVE_PATHS.map((rel) => ({
|
|
8104
|
+
absPath: normalizePath(resolve4(homeDir2, rel)),
|
|
8105
|
+
entry: rel,
|
|
8106
|
+
source: "home-default"
|
|
8107
|
+
}));
|
|
8108
|
+
const configured = (extraSensitiveDirs ?? []).filter((d) => typeof d === "string" && d.length > 0).map((d) => ({
|
|
8109
|
+
absPath: normalizePath(d, { base: homeDir2 }),
|
|
8110
|
+
entry: d,
|
|
8111
|
+
source: "config-dir"
|
|
8112
|
+
}));
|
|
8113
|
+
return [...builtIn, ...configured];
|
|
8114
|
+
}
|
|
8115
|
+
function matchSensitivePath(homeDir2, absPath, extraSensitiveDirs) {
|
|
8116
|
+
const hit = sensitivePathEntries(homeDir2, extraSensitiveDirs).find((entry) => isPathWithin(entry.absPath, absPath));
|
|
8117
|
+
return hit ? { entry: hit.entry, source: hit.source } : void 0;
|
|
7677
8118
|
}
|
|
7678
8119
|
function normalizeDirs(dirs, base) {
|
|
7679
8120
|
if (!dirs) return [];
|
|
@@ -7689,15 +8130,17 @@ function decideAccess(input2) {
|
|
|
7689
8130
|
const taskAllowedDirs = normalizeDirs(input2.taskAllowedDirs, taskWorkspace ?? homeDir2);
|
|
7690
8131
|
const configAllowedDirs = normalizeDirs(input2.configAllowedDirs, homeDir2);
|
|
7691
8132
|
const autoAllowRoots = normalizeDirs(input2.autoAllowRoots, homeDir2);
|
|
7692
|
-
const
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
8133
|
+
const sensitiveMode = input2.sensitiveMode ?? "ask";
|
|
8134
|
+
const sensitive = matchSensitiveEntry(resolvedPath) ?? matchSensitivePath(homeDir2, resolvedPath, input2.extraSensitiveDirs);
|
|
8135
|
+
if (sensitive) {
|
|
8136
|
+
const hit = { matchedEntry: sensitive.entry, matchedSource: sensitive.source, sensitive: true };
|
|
8137
|
+
if (input2.access === "write") {
|
|
8138
|
+
return { decision: "deny", matchedRule: "sensitive-list-write", resolvedPath, ...hit };
|
|
8139
|
+
}
|
|
8140
|
+
if (sensitiveMode === "deny") {
|
|
8141
|
+
return { decision: "deny", matchedRule: "sensitive-list", resolvedPath, ...hit };
|
|
8142
|
+
}
|
|
8143
|
+
return { decision: "ask", matchedRule: "sensitive-list", resolvedPath, ...hit };
|
|
7701
8144
|
}
|
|
7702
8145
|
if (findContaining(configAllowedDirs, resolvedPath) !== void 0) {
|
|
7703
8146
|
return { decision: "allow", matchedRule: "config-allowed", resolvedPath };
|
|
@@ -7717,11 +8160,403 @@ function decideAccess(input2) {
|
|
|
7717
8160
|
return { decision: "ask", matchedRule: "outside-all", resolvedPath };
|
|
7718
8161
|
}
|
|
7719
8162
|
|
|
8163
|
+
// src/services/tools/shell-lexer.ts
|
|
8164
|
+
var CMDSUBST_PLACEHOLDER = "__cmdsubst__";
|
|
8165
|
+
var DISCARD_TARGETS = /* @__PURE__ */ new Set(["/dev/null", "/dev/stdout", "/dev/stderr"]);
|
|
8166
|
+
function isDiscardRedirectTarget(target) {
|
|
8167
|
+
return target !== void 0 && DISCARD_TARGETS.has(target);
|
|
8168
|
+
}
|
|
8169
|
+
function isWordTerminator(ch) {
|
|
8170
|
+
return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "|" || ch === ";" || ch === "&" || ch === "<" || ch === ">";
|
|
8171
|
+
}
|
|
8172
|
+
function findClosingDoubleQuote(command, start) {
|
|
8173
|
+
let i = start + 1;
|
|
8174
|
+
while (i < command.length) {
|
|
8175
|
+
const ch = command[i];
|
|
8176
|
+
if (ch === "\\") {
|
|
8177
|
+
i += 2;
|
|
8178
|
+
continue;
|
|
8179
|
+
}
|
|
8180
|
+
if (ch === '"') return i;
|
|
8181
|
+
i += 1;
|
|
8182
|
+
}
|
|
8183
|
+
return -1;
|
|
8184
|
+
}
|
|
8185
|
+
function findClosingBacktick(command, start) {
|
|
8186
|
+
let i = start + 1;
|
|
8187
|
+
while (i < command.length) {
|
|
8188
|
+
const ch = command[i];
|
|
8189
|
+
if (ch === "\\") {
|
|
8190
|
+
i += 2;
|
|
8191
|
+
continue;
|
|
8192
|
+
}
|
|
8193
|
+
if (ch === "`") return i;
|
|
8194
|
+
i += 1;
|
|
8195
|
+
}
|
|
8196
|
+
return -1;
|
|
8197
|
+
}
|
|
8198
|
+
function scanBacktickSubstitution(command, start) {
|
|
8199
|
+
const end = findClosingBacktick(command, start);
|
|
8200
|
+
if (end === -1) return { error: 'unbalanced backtick \u2014 no closing "`"' };
|
|
8201
|
+
return { inner: command.slice(start + 1, end), next: end + 1 };
|
|
8202
|
+
}
|
|
8203
|
+
function scanDollarSubstitution(command, start) {
|
|
8204
|
+
const n = command.length;
|
|
8205
|
+
let i = start + 2;
|
|
8206
|
+
let depth = 1;
|
|
8207
|
+
let inner = "";
|
|
8208
|
+
while (i < n) {
|
|
8209
|
+
const ch = command[i];
|
|
8210
|
+
if (ch === "\\") {
|
|
8211
|
+
inner += command.slice(i, i + 2);
|
|
8212
|
+
i += 2;
|
|
8213
|
+
continue;
|
|
8214
|
+
}
|
|
8215
|
+
if (ch === "'") {
|
|
8216
|
+
const end = command.indexOf("'", i + 1);
|
|
8217
|
+
if (end === -1) return { error: "unterminated single quote inside command substitution" };
|
|
8218
|
+
inner += command.slice(i, end + 1);
|
|
8219
|
+
i = end + 1;
|
|
8220
|
+
continue;
|
|
8221
|
+
}
|
|
8222
|
+
if (ch === '"') {
|
|
8223
|
+
const end = findClosingDoubleQuote(command, i);
|
|
8224
|
+
if (end === -1) return { error: "unterminated double quote inside command substitution" };
|
|
8225
|
+
inner += command.slice(i, end + 1);
|
|
8226
|
+
i = end + 1;
|
|
8227
|
+
continue;
|
|
8228
|
+
}
|
|
8229
|
+
if (ch === "`") {
|
|
8230
|
+
const end = findClosingBacktick(command, i);
|
|
8231
|
+
if (end === -1) return { error: "unterminated backtick inside command substitution" };
|
|
8232
|
+
inner += command.slice(i, end + 1);
|
|
8233
|
+
i = end + 1;
|
|
8234
|
+
continue;
|
|
8235
|
+
}
|
|
8236
|
+
if (ch === "(") {
|
|
8237
|
+
depth += 1;
|
|
8238
|
+
inner += ch;
|
|
8239
|
+
i += 1;
|
|
8240
|
+
continue;
|
|
8241
|
+
}
|
|
8242
|
+
if (ch === ")") {
|
|
8243
|
+
depth -= 1;
|
|
8244
|
+
if (depth === 0) return { inner, next: i + 1 };
|
|
8245
|
+
inner += ch;
|
|
8246
|
+
i += 1;
|
|
8247
|
+
continue;
|
|
8248
|
+
}
|
|
8249
|
+
inner += ch;
|
|
8250
|
+
i += 1;
|
|
8251
|
+
}
|
|
8252
|
+
return { error: 'unbalanced "$(" \u2014 no closing ")"' };
|
|
8253
|
+
}
|
|
8254
|
+
function scanSubstitutionAt(command, start) {
|
|
8255
|
+
const dollarForm = command[start] === "$" && command[start + 1] === "(";
|
|
8256
|
+
const backtickForm = command[start] === "`";
|
|
8257
|
+
if (!dollarForm && !backtickForm) return void 0;
|
|
8258
|
+
const scanned = dollarForm ? scanDollarSubstitution(command, start) : scanBacktickSubstitution(command, start);
|
|
8259
|
+
if ("error" in scanned) return scanned;
|
|
8260
|
+
return {
|
|
8261
|
+
substitution: { inner: scanned.inner, raw: command.slice(start, scanned.next) },
|
|
8262
|
+
next: scanned.next
|
|
8263
|
+
};
|
|
8264
|
+
}
|
|
8265
|
+
function scanWordLite(command, start) {
|
|
8266
|
+
const n = command.length;
|
|
8267
|
+
let i = start;
|
|
8268
|
+
let text = "";
|
|
8269
|
+
const substitutions = [];
|
|
8270
|
+
const takeSubstitution = () => {
|
|
8271
|
+
const scanned = scanSubstitutionAt(command, i);
|
|
8272
|
+
if (!scanned) return { ok: true, consumed: false };
|
|
8273
|
+
if ("error" in scanned) return { ok: false, error: scanned.error };
|
|
8274
|
+
substitutions.push(scanned.substitution);
|
|
8275
|
+
text += CMDSUBST_PLACEHOLDER;
|
|
8276
|
+
i = scanned.next;
|
|
8277
|
+
return { ok: true, consumed: true };
|
|
8278
|
+
};
|
|
8279
|
+
while (i < n) {
|
|
8280
|
+
const ch = command[i];
|
|
8281
|
+
if (ch === "\\") {
|
|
8282
|
+
if (i + 1 < n) {
|
|
8283
|
+
text += command[i + 1];
|
|
8284
|
+
i += 2;
|
|
8285
|
+
} else {
|
|
8286
|
+
text += ch;
|
|
8287
|
+
i += 1;
|
|
8288
|
+
}
|
|
8289
|
+
continue;
|
|
8290
|
+
}
|
|
8291
|
+
if (ch === "'") {
|
|
8292
|
+
const end = command.indexOf("'", i + 1);
|
|
8293
|
+
if (end === -1) return { error: "unterminated single quote" };
|
|
8294
|
+
text += command.slice(i + 1, end);
|
|
8295
|
+
i = end + 1;
|
|
8296
|
+
continue;
|
|
8297
|
+
}
|
|
8298
|
+
if (ch === '"') {
|
|
8299
|
+
i += 1;
|
|
8300
|
+
let closed = false;
|
|
8301
|
+
while (i < n) {
|
|
8302
|
+
const inner = command[i];
|
|
8303
|
+
if (inner === "\\") {
|
|
8304
|
+
if (i + 1 < n) {
|
|
8305
|
+
text += command[i + 1];
|
|
8306
|
+
i += 2;
|
|
8307
|
+
} else {
|
|
8308
|
+
text += inner;
|
|
8309
|
+
i += 1;
|
|
8310
|
+
}
|
|
8311
|
+
continue;
|
|
8312
|
+
}
|
|
8313
|
+
if (inner === '"') {
|
|
8314
|
+
i += 1;
|
|
8315
|
+
closed = true;
|
|
8316
|
+
break;
|
|
8317
|
+
}
|
|
8318
|
+
const taken2 = takeSubstitution();
|
|
8319
|
+
if (!taken2.ok) return { error: taken2.error };
|
|
8320
|
+
if (taken2.consumed) continue;
|
|
8321
|
+
text += inner;
|
|
8322
|
+
i += 1;
|
|
8323
|
+
}
|
|
8324
|
+
if (!closed) return { error: "unterminated double quote" };
|
|
8325
|
+
continue;
|
|
8326
|
+
}
|
|
8327
|
+
const taken = takeSubstitution();
|
|
8328
|
+
if (!taken.ok) return { error: taken.error };
|
|
8329
|
+
if (taken.consumed) continue;
|
|
8330
|
+
if (isWordTerminator(ch)) break;
|
|
8331
|
+
text += ch;
|
|
8332
|
+
i += 1;
|
|
8333
|
+
}
|
|
8334
|
+
return { word: { text, substitutions }, next: i };
|
|
8335
|
+
}
|
|
8336
|
+
function scanFdDupTail(command, start) {
|
|
8337
|
+
const match = /^&(\d+|-)/.exec(command.slice(start, start + 8));
|
|
8338
|
+
if (!match) return { error: "unrecognised file-descriptor redirection" };
|
|
8339
|
+
return { tail: match[0], next: start + match[0].length };
|
|
8340
|
+
}
|
|
8341
|
+
function scanRedirectTarget(command, start) {
|
|
8342
|
+
let i = start;
|
|
8343
|
+
while (i < command.length && (command[i] === " " || command[i] === " ")) i += 1;
|
|
8344
|
+
const ch = command[i];
|
|
8345
|
+
if (ch === void 0 || isWordTerminator(ch)) return { error: "redirection without a target" };
|
|
8346
|
+
const scanned = scanWordLite(command, i);
|
|
8347
|
+
if ("error" in scanned) return scanned;
|
|
8348
|
+
if (scanned.word.text.length === 0 && scanned.word.substitutions.length === 0) {
|
|
8349
|
+
return { error: "redirection without a target" };
|
|
8350
|
+
}
|
|
8351
|
+
return scanned;
|
|
8352
|
+
}
|
|
8353
|
+
function scanRedirect(command, start, fd) {
|
|
8354
|
+
const ch = command[start];
|
|
8355
|
+
if (ch === "&") {
|
|
8356
|
+
if (command[start + 1] !== ">") return { error: 'unexpected "&" in a redirection' };
|
|
8357
|
+
const append2 = command[start + 2] === ">";
|
|
8358
|
+
const target2 = scanRedirectTarget(command, start + (append2 ? 3 : 2));
|
|
8359
|
+
if ("error" in target2) return target2;
|
|
8360
|
+
return {
|
|
8361
|
+
redirect: { operator: append2 ? "&>>" : "&>", kind: "output", fd: 2, target: target2.word },
|
|
8362
|
+
next: target2.next
|
|
8363
|
+
};
|
|
8364
|
+
}
|
|
8365
|
+
if (ch === "<") {
|
|
8366
|
+
if (command[start + 1] === "<") {
|
|
8367
|
+
if (command[start + 2] === "<") {
|
|
8368
|
+
const target4 = scanRedirectTarget(command, start + 3);
|
|
8369
|
+
if ("error" in target4) return target4;
|
|
8370
|
+
return {
|
|
8371
|
+
redirect: { operator: "<<<", kind: "here-string", target: target4.word },
|
|
8372
|
+
next: target4.next
|
|
8373
|
+
};
|
|
8374
|
+
}
|
|
8375
|
+
const stripTabs = command[start + 2] === "-";
|
|
8376
|
+
const target3 = scanRedirectTarget(command, start + (stripTabs ? 3 : 2));
|
|
8377
|
+
if ("error" in target3) return target3;
|
|
8378
|
+
return {
|
|
8379
|
+
redirect: { operator: stripTabs ? "<<-" : "<<", kind: "here-doc", target: target3.word },
|
|
8380
|
+
next: target3.next,
|
|
8381
|
+
heredoc: { delimiter: target3.word.text, stripTabs }
|
|
8382
|
+
};
|
|
8383
|
+
}
|
|
8384
|
+
if (command[start + 1] === "&") {
|
|
8385
|
+
const dup = scanFdDupTail(command, start + 1);
|
|
8386
|
+
if ("error" in dup) return dup;
|
|
8387
|
+
return { redirect: { operator: `${fd ?? ""}<${dup.tail}`, kind: "fd-dup" }, next: dup.next };
|
|
8388
|
+
}
|
|
8389
|
+
if (command[start + 1] === ">") {
|
|
8390
|
+
const target3 = scanRedirectTarget(command, start + 2);
|
|
8391
|
+
if ("error" in target3) return target3;
|
|
8392
|
+
return {
|
|
8393
|
+
redirect: { operator: `${fd ?? ""}<>`, kind: "output", fd: fd ?? 0, target: target3.word },
|
|
8394
|
+
next: target3.next
|
|
8395
|
+
};
|
|
8396
|
+
}
|
|
8397
|
+
const target2 = scanRedirectTarget(command, start + 1);
|
|
8398
|
+
if ("error" in target2) return target2;
|
|
8399
|
+
return {
|
|
8400
|
+
redirect: { operator: `${fd ?? ""}<`, kind: "input", target: target2.word },
|
|
8401
|
+
next: target2.next
|
|
8402
|
+
};
|
|
8403
|
+
}
|
|
8404
|
+
const append = command[start + 1] === ">";
|
|
8405
|
+
let cursor = start + (append ? 2 : 1);
|
|
8406
|
+
const clobber = !append && command[cursor] === "|";
|
|
8407
|
+
if (clobber) cursor += 1;
|
|
8408
|
+
const operator = `${fd ?? ""}${append ? ">>" : ">"}${clobber ? "|" : ""}`;
|
|
8409
|
+
if (command[cursor] === "&") {
|
|
8410
|
+
const dup = scanFdDupTail(command, cursor);
|
|
8411
|
+
if ("error" in dup) return dup;
|
|
8412
|
+
return { redirect: { operator: `${operator}${dup.tail}`, kind: "fd-dup" }, next: dup.next };
|
|
8413
|
+
}
|
|
8414
|
+
const target = scanRedirectTarget(command, cursor);
|
|
8415
|
+
if ("error" in target) return target;
|
|
8416
|
+
return {
|
|
8417
|
+
redirect: { operator, kind: "output", fd: fd ?? 1, target: target.word },
|
|
8418
|
+
next: target.next
|
|
8419
|
+
};
|
|
8420
|
+
}
|
|
8421
|
+
function consumeHeredocBodies(command, start, heredocs) {
|
|
8422
|
+
let pos = start;
|
|
8423
|
+
for (const heredoc of heredocs) {
|
|
8424
|
+
let matched = false;
|
|
8425
|
+
while (pos < command.length) {
|
|
8426
|
+
const newline = command.indexOf("\n", pos);
|
|
8427
|
+
const lineEnd2 = newline === -1 ? command.length : newline;
|
|
8428
|
+
const raw = command.slice(pos, lineEnd2);
|
|
8429
|
+
const line = heredoc.stripTabs ? raw.replace(/^\t+/, "") : raw;
|
|
8430
|
+
pos = lineEnd2 + 1;
|
|
8431
|
+
if (line === heredoc.delimiter) {
|
|
8432
|
+
matched = true;
|
|
8433
|
+
break;
|
|
8434
|
+
}
|
|
8435
|
+
if (newline === -1) break;
|
|
8436
|
+
}
|
|
8437
|
+
if (!matched) return void 0;
|
|
8438
|
+
}
|
|
8439
|
+
return pos;
|
|
8440
|
+
}
|
|
8441
|
+
function tokenizeShellLite(command) {
|
|
8442
|
+
const n = command.length;
|
|
8443
|
+
const segments = [];
|
|
8444
|
+
let tokens = [];
|
|
8445
|
+
let pendingHeredocs = [];
|
|
8446
|
+
let i = 0;
|
|
8447
|
+
const endSegment = () => {
|
|
8448
|
+
if (tokens.length > 0) segments.push({ tokens });
|
|
8449
|
+
tokens = [];
|
|
8450
|
+
};
|
|
8451
|
+
const takeRedirect = (redirectStart, fd) => {
|
|
8452
|
+
const scanned = scanRedirect(command, redirectStart, fd);
|
|
8453
|
+
if ("error" in scanned) return { ok: false, error: scanned.error };
|
|
8454
|
+
tokens.push({ kind: "redirect", redirect: scanned.redirect });
|
|
8455
|
+
if (scanned.heredoc) pendingHeredocs.push(scanned.heredoc);
|
|
8456
|
+
i = scanned.next;
|
|
8457
|
+
return void 0;
|
|
8458
|
+
};
|
|
8459
|
+
while (i < n) {
|
|
8460
|
+
const ch = command[i];
|
|
8461
|
+
if (ch === " " || ch === " " || ch === "\r") {
|
|
8462
|
+
i += 1;
|
|
8463
|
+
continue;
|
|
8464
|
+
}
|
|
8465
|
+
if (ch === "\\" && command[i + 1] === "\n") {
|
|
8466
|
+
i += 2;
|
|
8467
|
+
continue;
|
|
8468
|
+
}
|
|
8469
|
+
if (ch === "\n") {
|
|
8470
|
+
endSegment();
|
|
8471
|
+
i += 1;
|
|
8472
|
+
if (pendingHeredocs.length > 0) {
|
|
8473
|
+
const after = consumeHeredocBodies(command, i, pendingHeredocs);
|
|
8474
|
+
if (after === void 0) return { ok: false, error: "here-document without its terminator line" };
|
|
8475
|
+
i = after;
|
|
8476
|
+
pendingHeredocs = [];
|
|
8477
|
+
}
|
|
8478
|
+
continue;
|
|
8479
|
+
}
|
|
8480
|
+
if (ch === "#") {
|
|
8481
|
+
while (i < n && command[i] !== "\n") i += 1;
|
|
8482
|
+
continue;
|
|
8483
|
+
}
|
|
8484
|
+
if (ch === "|" || ch === ";" || ch === "&") {
|
|
8485
|
+
if (ch === "&" && command[i + 1] === ">") {
|
|
8486
|
+
const failed = takeRedirect(i);
|
|
8487
|
+
if (failed) return failed;
|
|
8488
|
+
continue;
|
|
8489
|
+
}
|
|
8490
|
+
i += 1;
|
|
8491
|
+
while (i < n && (command[i] === "|" || command[i] === ";" || command[i] === "&")) i += 1;
|
|
8492
|
+
endSegment();
|
|
8493
|
+
continue;
|
|
8494
|
+
}
|
|
8495
|
+
const fdMatch = /^(\d+)(?=>>|>|<>|<)/.exec(command.slice(i, i + 24));
|
|
8496
|
+
const fd = fdMatch ? Number(fdMatch[1]) : void 0;
|
|
8497
|
+
const redirectStart = i + (fdMatch ? fdMatch[1].length : 0);
|
|
8498
|
+
const redirectChar = command[redirectStart];
|
|
8499
|
+
if (redirectChar === "<" || redirectChar === ">") {
|
|
8500
|
+
const failed = takeRedirect(redirectStart, fd);
|
|
8501
|
+
if (failed) return failed;
|
|
8502
|
+
continue;
|
|
8503
|
+
}
|
|
8504
|
+
const scanned = scanWordLite(command, i);
|
|
8505
|
+
if ("error" in scanned) return { ok: false, error: scanned.error };
|
|
8506
|
+
if (scanned.next === i) return { ok: false, error: `unexpected character "${ch}"` };
|
|
8507
|
+
tokens.push({ kind: "word", word: scanned.word });
|
|
8508
|
+
i = scanned.next;
|
|
8509
|
+
}
|
|
8510
|
+
if (pendingHeredocs.length > 0) {
|
|
8511
|
+
return { ok: false, error: "here-document without a body" };
|
|
8512
|
+
}
|
|
8513
|
+
endSegment();
|
|
8514
|
+
return { ok: true, segments };
|
|
8515
|
+
}
|
|
8516
|
+
function classifyRedirections(segments) {
|
|
8517
|
+
const result2 = {
|
|
8518
|
+
writeTargets: [],
|
|
8519
|
+
readTargets: [],
|
|
8520
|
+
duplicates: [],
|
|
8521
|
+
discarded: []
|
|
8522
|
+
};
|
|
8523
|
+
for (const segment of segments) {
|
|
8524
|
+
for (const token of segment.tokens) {
|
|
8525
|
+
if (token.kind !== "redirect") continue;
|
|
8526
|
+
const { redirect } = token;
|
|
8527
|
+
const target = redirect.target?.text;
|
|
8528
|
+
if (redirect.kind === "fd-dup") {
|
|
8529
|
+
result2.duplicates.push(redirect.operator);
|
|
8530
|
+
continue;
|
|
8531
|
+
}
|
|
8532
|
+
if (redirect.kind === "here-string" || redirect.kind === "here-doc") continue;
|
|
8533
|
+
if (target === void 0) continue;
|
|
8534
|
+
if (isDiscardRedirectTarget(target)) {
|
|
8535
|
+
result2.discarded.push(target);
|
|
8536
|
+
continue;
|
|
8537
|
+
}
|
|
8538
|
+
if (redirect.kind === "input") {
|
|
8539
|
+
if (target.includes(CMDSUBST_PLACEHOLDER)) {
|
|
8540
|
+
result2.unresolved ??= `${redirect.operator} ${target}`;
|
|
8541
|
+
continue;
|
|
8542
|
+
}
|
|
8543
|
+
result2.readTargets.push(target);
|
|
8544
|
+
continue;
|
|
8545
|
+
}
|
|
8546
|
+
if (target.includes(CMDSUBST_PLACEHOLDER)) {
|
|
8547
|
+
result2.unresolved ??= `${redirect.operator} ${target}`;
|
|
8548
|
+
continue;
|
|
8549
|
+
}
|
|
8550
|
+
result2.writeTargets.push(target);
|
|
8551
|
+
}
|
|
8552
|
+
}
|
|
8553
|
+
return result2;
|
|
8554
|
+
}
|
|
8555
|
+
|
|
7720
8556
|
// src/services/tools/permission-rules.ts
|
|
7721
8557
|
var FILE_TOOL_NAMES = ["read_file", "write_file", "edit_file", "search_files", "search_content"];
|
|
7722
8558
|
var READ_ONLY_FILE_TOOLS = /* @__PURE__ */ new Set(["read_file", "search_files", "search_content"]);
|
|
7723
8559
|
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
8560
|
function expandHome(p, homeDir2) {
|
|
7726
8561
|
if (p === "~") return { path: homeDir2, unresolvable: false };
|
|
7727
8562
|
if (p.startsWith("~/")) return { path: resolve5(homeDir2, p.slice(2)), unresolvable: false };
|
|
@@ -7731,37 +8566,139 @@ function expandHome(p, homeDir2) {
|
|
|
7731
8566
|
function expandHomeVars(command, homeDir2) {
|
|
7732
8567
|
return command.replace(/\$\{HOME\}/g, homeDir2).replace(/\$HOME(?![A-Za-z0-9_])/g, homeDir2);
|
|
7733
8568
|
}
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
8569
|
+
var CMDSUBST_READONLY_VERBS = /* @__PURE__ */ new Set([
|
|
8570
|
+
"cat",
|
|
8571
|
+
"ls",
|
|
8572
|
+
"grep",
|
|
8573
|
+
"head",
|
|
8574
|
+
"tail",
|
|
8575
|
+
"wc",
|
|
8576
|
+
"stat",
|
|
8577
|
+
"file",
|
|
8578
|
+
"which",
|
|
8579
|
+
"command",
|
|
8580
|
+
"readlink",
|
|
8581
|
+
"echo",
|
|
8582
|
+
"printf",
|
|
8583
|
+
"date",
|
|
8584
|
+
"pwd"
|
|
8585
|
+
]);
|
|
8586
|
+
var MAX_CMDSUBST_DEPTH = 5;
|
|
8587
|
+
var BASH_WRITE_VERBS = /* @__PURE__ */ new Set([
|
|
8588
|
+
"rm",
|
|
8589
|
+
"rmdir",
|
|
8590
|
+
"mv",
|
|
8591
|
+
"cp",
|
|
8592
|
+
"ln",
|
|
8593
|
+
"tee",
|
|
8594
|
+
"dd",
|
|
8595
|
+
"truncate",
|
|
8596
|
+
"install",
|
|
8597
|
+
"mkdir",
|
|
8598
|
+
"touch",
|
|
8599
|
+
"chmod",
|
|
8600
|
+
"chown",
|
|
8601
|
+
"chgrp",
|
|
8602
|
+
"shred",
|
|
8603
|
+
"unlink",
|
|
8604
|
+
"chattr",
|
|
8605
|
+
"mkfs",
|
|
8606
|
+
"mkswap",
|
|
8607
|
+
"mount",
|
|
8608
|
+
"umount"
|
|
8609
|
+
]);
|
|
8610
|
+
var BASH_FLAGGED_WRITE_VERBS = /* @__PURE__ */ new Map([
|
|
8611
|
+
["sed", ["-i", "--in-place"]],
|
|
8612
|
+
["perl", ["-i"]],
|
|
8613
|
+
["ruby", ["-i"]],
|
|
8614
|
+
["find", ["-delete", "-fprint", "-fls", "-fprintf", "-exec", "-execdir", "-ok"]]
|
|
8615
|
+
]);
|
|
8616
|
+
function isWriteSegment(segment) {
|
|
8617
|
+
const words = [];
|
|
8618
|
+
for (const token of segment.tokens) {
|
|
8619
|
+
if (token.kind === "word") {
|
|
8620
|
+
words.push(token.word.text);
|
|
8621
|
+
continue;
|
|
8622
|
+
}
|
|
8623
|
+
const { redirect } = token;
|
|
8624
|
+
if (redirect.kind !== "output") continue;
|
|
8625
|
+
if (isDiscardRedirectTarget(redirect.target?.text)) continue;
|
|
8626
|
+
return true;
|
|
8627
|
+
}
|
|
8628
|
+
for (const word of words) {
|
|
8629
|
+
if (BASH_WRITE_VERBS.has(word)) return true;
|
|
8630
|
+
const flags2 = BASH_FLAGGED_WRITE_VERBS.get(word);
|
|
8631
|
+
if (flags2 && flags2.some((flag) => words.includes(flag))) return true;
|
|
8632
|
+
}
|
|
8633
|
+
return false;
|
|
7739
8634
|
}
|
|
7740
|
-
function
|
|
7741
|
-
|
|
8635
|
+
function pathPieces(text, access) {
|
|
8636
|
+
const candidates = [];
|
|
8637
|
+
for (const piece of text.split("=")) {
|
|
8638
|
+
if (!piece.startsWith("/") && !piece.startsWith("~")) continue;
|
|
8639
|
+
if (isDiscardRedirectTarget(piece)) continue;
|
|
8640
|
+
candidates.push({ path: piece, access });
|
|
8641
|
+
}
|
|
8642
|
+
return candidates;
|
|
7742
8643
|
}
|
|
7743
|
-
function extractPathCandidates(
|
|
7744
|
-
const withoutUrls = command.replace(URL_RE, " ");
|
|
8644
|
+
function extractPathCandidates(segments) {
|
|
7745
8645
|
const candidates = [];
|
|
7746
|
-
for (const
|
|
7747
|
-
const
|
|
7748
|
-
|
|
8646
|
+
for (const segment of segments) {
|
|
8647
|
+
const segmentAccess = isWriteSegment(segment) ? "write" : "unknown";
|
|
8648
|
+
for (const token of segment.tokens) {
|
|
8649
|
+
if (token.kind === "word") {
|
|
8650
|
+
candidates.push(...pathPieces(token.word.text, segmentAccess));
|
|
8651
|
+
continue;
|
|
8652
|
+
}
|
|
8653
|
+
const { redirect } = token;
|
|
8654
|
+
if (redirect.kind === "fd-dup" || redirect.kind === "here-doc" || redirect.kind === "here-string") continue;
|
|
8655
|
+
const target = redirect.target?.text;
|
|
8656
|
+
if (target === void 0 || isDiscardRedirectTarget(target)) continue;
|
|
8657
|
+
if (target.includes(CMDSUBST_PLACEHOLDER)) continue;
|
|
8658
|
+
const access = redirect.kind === "output" ? "write" : segmentAccess;
|
|
8659
|
+
for (const candidate of pathPieces(target, access)) {
|
|
8660
|
+
candidates.push(
|
|
8661
|
+
redirect.kind === "output" ? { ...candidate, reason: `output redirection to "${target}"` } : candidate
|
|
8662
|
+
);
|
|
8663
|
+
}
|
|
8664
|
+
}
|
|
7749
8665
|
}
|
|
7750
8666
|
return candidates;
|
|
7751
8667
|
}
|
|
7752
|
-
function
|
|
8668
|
+
function commandAccess(candidates) {
|
|
8669
|
+
return candidates.some((candidate) => candidate.access === "write") ? "write" : "unknown";
|
|
8670
|
+
}
|
|
8671
|
+
function decidePath(scope, path10, access) {
|
|
8672
|
+
const permission = scope.permission;
|
|
7753
8673
|
return decideAccess({
|
|
7754
|
-
path:
|
|
8674
|
+
path: path10,
|
|
7755
8675
|
access,
|
|
7756
8676
|
cwd: scope.workspace,
|
|
7757
8677
|
homeDir: scope.homeDir,
|
|
7758
8678
|
taskWorkspace: scope.workspace,
|
|
7759
8679
|
taskAllowedDirs: scope.taskAllowedDirs,
|
|
7760
|
-
configAllowedDirs:
|
|
7761
|
-
autoAllowRoots:
|
|
7762
|
-
|
|
8680
|
+
configAllowedDirs: permission?.allowedDirs,
|
|
8681
|
+
autoAllowRoots: permission?.autoAllowRoots,
|
|
8682
|
+
// `sensitiveDirs` is the current key; `deniedDirs` is its deprecated alias
|
|
8683
|
+
// and is merged (deduped) here so a legacy config keeps protecting exactly
|
|
8684
|
+
// the directories it declares.
|
|
8685
|
+
extraSensitiveDirs: mergeSensitiveDirs(permission),
|
|
8686
|
+
sensitiveMode: resolveSensitiveMode(permission)
|
|
7763
8687
|
});
|
|
7764
8688
|
}
|
|
8689
|
+
function toToolDecision(verdict, access) {
|
|
8690
|
+
return {
|
|
8691
|
+
decision: verdict.decision,
|
|
8692
|
+
matchedRule: verdict.matchedRule,
|
|
8693
|
+
resolvedPath: verdict.resolvedPath,
|
|
8694
|
+
access,
|
|
8695
|
+
...verdict.matchedEntry !== void 0 && {
|
|
8696
|
+
matchedEntry: verdict.matchedEntry,
|
|
8697
|
+
matchedSource: verdict.matchedSource,
|
|
8698
|
+
sensitive: true
|
|
8699
|
+
}
|
|
8700
|
+
};
|
|
8701
|
+
}
|
|
7765
8702
|
function evaluateFileAccess(scope, toolName, args) {
|
|
7766
8703
|
const rawPath = args.path;
|
|
7767
8704
|
if (typeof rawPath !== "string" || !rawPath) return { decision: "allow", matchedRule: "no-path" };
|
|
@@ -7776,35 +8713,163 @@ function evaluateFileAccess(scope, toolName, args) {
|
|
|
7776
8713
|
}
|
|
7777
8714
|
const verdict = decidePath(scope, expanded.path, access);
|
|
7778
8715
|
if (verdict.decision === "allow" && verdict.withinReadScope) {
|
|
7779
|
-
return { decision: "ask", matchedRule: "read-within-home" };
|
|
8716
|
+
return { decision: "ask", matchedRule: "read-within-home", resolvedPath: verdict.resolvedPath, access };
|
|
8717
|
+
}
|
|
8718
|
+
return toToolDecision(verdict, access);
|
|
8719
|
+
}
|
|
8720
|
+
function collectSubstitutions(segments) {
|
|
8721
|
+
const substitutions = [];
|
|
8722
|
+
for (const segment of segments) {
|
|
8723
|
+
for (const token of segment.tokens) {
|
|
8724
|
+
const word = token.kind === "word" ? token.word : token.redirect.target;
|
|
8725
|
+
if (word) substitutions.push(...word.substitutions);
|
|
8726
|
+
}
|
|
8727
|
+
}
|
|
8728
|
+
return substitutions;
|
|
8729
|
+
}
|
|
8730
|
+
function checkSubstitutionInner(inner) {
|
|
8731
|
+
const tokenized = tokenizeShellLite(inner);
|
|
8732
|
+
if (!tokenized.ok) return { ok: false, reason: `cannot parse it (${tokenized.error})` };
|
|
8733
|
+
if (tokenized.segments.length === 0) return { ok: false, reason: "it is empty" };
|
|
8734
|
+
for (const segment of tokenized.segments) {
|
|
8735
|
+
const verb = segment.tokens.find((token) => token.kind === "word");
|
|
8736
|
+
if (!verb || verb.kind !== "word" || verb.word.text === CMDSUBST_PLACEHOLDER) {
|
|
8737
|
+
return { ok: false, reason: "it has no command word to judge" };
|
|
8738
|
+
}
|
|
8739
|
+
if (!CMDSUBST_READONLY_VERBS.has(verb.word.text)) {
|
|
8740
|
+
return { ok: false, reason: `\`${verb.word.text}\` is not a read-only command` };
|
|
8741
|
+
}
|
|
8742
|
+
if (isWriteSegment(segment)) return { ok: false, reason: `\`${verb.word.text}\` writes` };
|
|
8743
|
+
}
|
|
8744
|
+
return { ok: true };
|
|
8745
|
+
}
|
|
8746
|
+
function evaluateSubstitutions(scope, segments, depth) {
|
|
8747
|
+
const substitutions = collectSubstitutions(segments);
|
|
8748
|
+
if (substitutions.length === 0) return void 0;
|
|
8749
|
+
if (depth + 1 > MAX_CMDSUBST_DEPTH) {
|
|
8750
|
+
return {
|
|
8751
|
+
decision: "ask",
|
|
8752
|
+
matchedRule: "dynamic-shell",
|
|
8753
|
+
reason: `command substitution nested deeper than ${MAX_CMDSUBST_DEPTH} levels`
|
|
8754
|
+
};
|
|
8755
|
+
}
|
|
8756
|
+
for (const substitution of substitutions) {
|
|
8757
|
+
const inner = substitution.inner.trim();
|
|
8758
|
+
if (inner.length === 0) {
|
|
8759
|
+
return { decision: "ask", matchedRule: "dynamic-shell", reason: `empty command substitution ${substitution.raw}` };
|
|
8760
|
+
}
|
|
8761
|
+
const readOnly = checkSubstitutionInner(inner);
|
|
8762
|
+
if (!readOnly.ok) {
|
|
8763
|
+
return {
|
|
8764
|
+
decision: "ask",
|
|
8765
|
+
matchedRule: "dynamic-shell",
|
|
8766
|
+
reason: `command substitution ${substitution.raw}: ${readOnly.reason}`
|
|
8767
|
+
};
|
|
8768
|
+
}
|
|
8769
|
+
const verdict = evaluateBashCommand(scope, inner, depth + 1);
|
|
8770
|
+
if (verdict.decision === "deny") return verdict;
|
|
8771
|
+
if (verdict.decision === "ask") {
|
|
8772
|
+
const detail = verdict.reason ?? (verdict.resolvedPath ? `target ${verdict.resolvedPath}` : verdict.matchedRule);
|
|
8773
|
+
return {
|
|
8774
|
+
decision: "ask",
|
|
8775
|
+
matchedRule: "dynamic-shell",
|
|
8776
|
+
reason: `command substitution ${substitution.raw} needs approval: ${detail}`,
|
|
8777
|
+
// Carry the inner hit through, so an approval request still names the
|
|
8778
|
+
// path (and the sensitive entry) the substituted command would touch.
|
|
8779
|
+
...verdict.resolvedPath !== void 0 && { resolvedPath: verdict.resolvedPath },
|
|
8780
|
+
...verdict.matchedEntry !== void 0 && {
|
|
8781
|
+
matchedEntry: verdict.matchedEntry,
|
|
8782
|
+
matchedSource: verdict.matchedSource,
|
|
8783
|
+
sensitive: true
|
|
8784
|
+
}
|
|
8785
|
+
};
|
|
8786
|
+
}
|
|
7780
8787
|
}
|
|
7781
|
-
return
|
|
8788
|
+
return void 0;
|
|
8789
|
+
}
|
|
8790
|
+
function fallbackSensitiveWrite(scope, expandedCommand) {
|
|
8791
|
+
for (const raw of expandedCommand.split(/[\s=]+/)) {
|
|
8792
|
+
const word = raw.replace(/^['"]+/, "").replace(/['"]+$/, "");
|
|
8793
|
+
if (!word.startsWith("/") && !word.startsWith("~")) continue;
|
|
8794
|
+
const expanded = expandHome(word, scope.homeDir);
|
|
8795
|
+
if (expanded.unresolvable) continue;
|
|
8796
|
+
const verdict = decidePath(scope, expanded.path, "write");
|
|
8797
|
+
if (verdict.decision === "deny") return toToolDecision(verdict, "write");
|
|
8798
|
+
}
|
|
8799
|
+
return void 0;
|
|
7782
8800
|
}
|
|
7783
8801
|
function evaluateBashAccess(scope, command) {
|
|
8802
|
+
return evaluateBashCommand(scope, command, 0);
|
|
8803
|
+
}
|
|
8804
|
+
function evaluateBashCommand(scope, command, depth) {
|
|
7784
8805
|
const expandedCommand = expandHomeVars(command, scope.homeDir);
|
|
8806
|
+
const tokenized = tokenizeShellLite(expandedCommand);
|
|
8807
|
+
if (!tokenized.ok) {
|
|
8808
|
+
const refusal = fallbackSensitiveWrite(scope, expandedCommand);
|
|
8809
|
+
if (refusal) return refusal;
|
|
8810
|
+
return {
|
|
8811
|
+
decision: "ask",
|
|
8812
|
+
matchedRule: "dynamic-shell",
|
|
8813
|
+
reason: `could not parse the shell command (${tokenized.error})`
|
|
8814
|
+
};
|
|
8815
|
+
}
|
|
8816
|
+
const segments = tokenized.segments;
|
|
8817
|
+
const redirections = classifyRedirections(segments);
|
|
7785
8818
|
const candidates = [];
|
|
7786
8819
|
let hasUnresolvableHome = false;
|
|
7787
|
-
for (const
|
|
7788
|
-
const expanded = expandHome(
|
|
8820
|
+
for (const candidate of extractPathCandidates(segments)) {
|
|
8821
|
+
const expanded = expandHome(candidate.path, scope.homeDir);
|
|
7789
8822
|
if (expanded.unresolvable) {
|
|
7790
8823
|
hasUnresolvableHome = true;
|
|
7791
8824
|
continue;
|
|
7792
8825
|
}
|
|
7793
|
-
candidates.push(expanded.path);
|
|
8826
|
+
candidates.push({ ...candidate, path: expanded.path });
|
|
7794
8827
|
}
|
|
7795
8828
|
let firstAsk;
|
|
7796
8829
|
let firstAllow;
|
|
7797
8830
|
for (const candidate of candidates) {
|
|
7798
|
-
const verdict = decidePath(scope, candidate,
|
|
7799
|
-
if (verdict.decision === "deny") return verdict;
|
|
7800
|
-
if (verdict.decision === "ask")
|
|
7801
|
-
|
|
8831
|
+
const verdict = decidePath(scope, candidate.path, candidate.access);
|
|
8832
|
+
if (verdict.decision === "deny") return toToolDecision(verdict, candidate.access);
|
|
8833
|
+
if (verdict.decision === "ask") {
|
|
8834
|
+
firstAsk ??= {
|
|
8835
|
+
...toToolDecision(verdict, candidate.access),
|
|
8836
|
+
...candidate.reason !== void 0 && { reason: candidate.reason }
|
|
8837
|
+
};
|
|
8838
|
+
} else firstAllow ??= toToolDecision(verdict, candidate.access);
|
|
7802
8839
|
}
|
|
8840
|
+
const lineAccess = commandAccess(candidates);
|
|
8841
|
+
if (firstAsk) firstAsk = { ...firstAsk, access: lineAccess };
|
|
8842
|
+
if (firstAllow) firstAllow = { ...firstAllow, access: lineAccess };
|
|
7803
8843
|
if (DANGEROUS_BASH_PATTERN.test(expandedCommand)) {
|
|
7804
|
-
|
|
8844
|
+
const hit = firstAsk?.matchedEntry !== void 0 ? firstAsk : void 0;
|
|
8845
|
+
return {
|
|
8846
|
+
decision: "ask",
|
|
8847
|
+
matchedRule: "dangerous-bash",
|
|
8848
|
+
access: lineAccess,
|
|
8849
|
+
...hit !== void 0 && {
|
|
8850
|
+
matchedEntry: hit.matchedEntry,
|
|
8851
|
+
matchedSource: hit.matchedSource,
|
|
8852
|
+
resolvedPath: hit.resolvedPath,
|
|
8853
|
+
sensitive: true
|
|
8854
|
+
}
|
|
8855
|
+
};
|
|
8856
|
+
}
|
|
8857
|
+
const substitution = evaluateSubstitutions(scope, segments, depth);
|
|
8858
|
+
if (substitution) return substitution;
|
|
8859
|
+
if (redirections.unresolved !== void 0) {
|
|
8860
|
+
return {
|
|
8861
|
+
decision: "ask",
|
|
8862
|
+
matchedRule: "dynamic-shell",
|
|
8863
|
+
reason: `output redirection target cannot be determined statically (${redirections.unresolved})`
|
|
8864
|
+
};
|
|
8865
|
+
}
|
|
8866
|
+
if (redirections.writeTargets.length > 0 && !firstAsk) {
|
|
8867
|
+
return {
|
|
8868
|
+
decision: "ask",
|
|
8869
|
+
matchedRule: "dynamic-shell",
|
|
8870
|
+
reason: `output redirection writes a file ("${redirections.writeTargets.join('", "')}")`
|
|
8871
|
+
};
|
|
7805
8872
|
}
|
|
7806
|
-
const dynamic = detectUnsafeDynamicTarget(expandedCommand);
|
|
7807
|
-
if (dynamic) return { decision: "ask", matchedRule: "dynamic-shell", reason: dynamic };
|
|
7808
8873
|
if (hasUnresolvableHome) {
|
|
7809
8874
|
return {
|
|
7810
8875
|
decision: "ask",
|
|
@@ -7815,12 +8880,13 @@ function evaluateBashAccess(scope, command) {
|
|
|
7815
8880
|
if (firstAsk) return firstAsk;
|
|
7816
8881
|
return firstAllow ?? { decision: "allow", matchedRule: "no-path" };
|
|
7817
8882
|
}
|
|
7818
|
-
function denyMessage(toolName, requestedPath) {
|
|
8883
|
+
function denyMessage(toolName, requestedPath, matchedRule) {
|
|
8884
|
+
const write = matchedRule === "sensitive-list-write";
|
|
7819
8885
|
if (toolName === "bash") {
|
|
7820
|
-
return "[ERROR] bash: refusing to run this command \u2014 it
|
|
8886
|
+
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
8887
|
}
|
|
7822
8888
|
const target = requestedPath ? ` "${requestedPath}"` : "";
|
|
7823
|
-
return `[ERROR] ${toolName}: refusing access to${target} \u2014 the path is on the
|
|
8889
|
+
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
8890
|
}
|
|
7825
8891
|
function decideToolAccess(scope, toolName, args) {
|
|
7826
8892
|
let verdict;
|
|
@@ -7832,7 +8898,51 @@ function decideToolAccess(scope, toolName, args) {
|
|
|
7832
8898
|
return { decision: "allow", matchedRule: "no-path" };
|
|
7833
8899
|
}
|
|
7834
8900
|
if (verdict.decision !== "deny") return verdict;
|
|
7835
|
-
return {
|
|
8901
|
+
return {
|
|
8902
|
+
...verdict,
|
|
8903
|
+
message: denyMessage(toolName, typeof args.path === "string" ? args.path : void 0, verdict.matchedRule)
|
|
8904
|
+
};
|
|
8905
|
+
}
|
|
8906
|
+
|
|
8907
|
+
// src/services/permission/permission-audit.ts
|
|
8908
|
+
function operationForTool(toolName) {
|
|
8909
|
+
if (toolName === "bash") return "exec";
|
|
8910
|
+
if (toolName === "read_file" || toolName === "search_files" || toolName === "search_content") return "read";
|
|
8911
|
+
return "write";
|
|
8912
|
+
}
|
|
8913
|
+
function auditAccessFor(access) {
|
|
8914
|
+
return access ?? "unknown";
|
|
8915
|
+
}
|
|
8916
|
+
function recordPermissionAudit(logger, entry) {
|
|
8917
|
+
if (!logger) return;
|
|
8918
|
+
logger.info(
|
|
8919
|
+
{
|
|
8920
|
+
audit: "permission",
|
|
8921
|
+
decision: entry.decision,
|
|
8922
|
+
operation: entry.operation,
|
|
8923
|
+
...entry.access !== void 0 && { access: entry.access },
|
|
8924
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8925
|
+
...entry.resolvedPath !== void 0 && { resolvedPath: entry.resolvedPath },
|
|
8926
|
+
...entry.requestedPath !== void 0 && { requestedPath: entry.requestedPath },
|
|
8927
|
+
...entry.matchedEntry !== void 0 && { matchedEntry: entry.matchedEntry },
|
|
8928
|
+
...entry.matchedSource !== void 0 && { matchedSource: entry.matchedSource },
|
|
8929
|
+
...entry.matchedRule !== void 0 && { matchedRule: entry.matchedRule },
|
|
8930
|
+
...entry.taskId !== void 0 && { taskId: entry.taskId },
|
|
8931
|
+
...entry.workerId !== void 0 && { workerId: entry.workerId },
|
|
8932
|
+
...entry.toolName !== void 0 && { toolName: entry.toolName },
|
|
8933
|
+
...entry.source !== void 0 && { source: entry.source }
|
|
8934
|
+
},
|
|
8935
|
+
"permission audit"
|
|
8936
|
+
);
|
|
8937
|
+
}
|
|
8938
|
+
function verdictDetailSuffix(detail) {
|
|
8939
|
+
const parts = [];
|
|
8940
|
+
if (detail.matchedEntry) {
|
|
8941
|
+
parts.push(`hit: ${detail.matchedEntry}${detail.matchedSource ? ` (${detail.matchedSource})` : ""}`);
|
|
8942
|
+
}
|
|
8943
|
+
if (detail.matchedRule) parts.push(`rule: ${detail.matchedRule}`);
|
|
8944
|
+
if (detail.operation) parts.push(`op: ${detail.operation}`);
|
|
8945
|
+
return parts.length > 0 ? ` [${parts.join(" ; ")}]` : "";
|
|
7836
8946
|
}
|
|
7837
8947
|
|
|
7838
8948
|
// src/infra/blocking-requests.ts
|
|
@@ -7882,7 +8992,7 @@ var BlockingRequestMap = class {
|
|
|
7882
8992
|
|
|
7883
8993
|
// src/services/agents/claude-code-worker.ts
|
|
7884
8994
|
import { basename as basename7, dirname as dirname8 } from "path";
|
|
7885
|
-
import { homedir } from "os";
|
|
8995
|
+
import { homedir as homedir2 } from "os";
|
|
7886
8996
|
var INQUIRY_STREAM_CLOSE_TIMEOUT_MS = 36e4;
|
|
7887
8997
|
var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
7888
8998
|
id;
|
|
@@ -8146,11 +9256,11 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
8146
9256
|
return this.state.get("conversation") ?? [];
|
|
8147
9257
|
}
|
|
8148
9258
|
// --- Subscribable ---
|
|
8149
|
-
subscribe(
|
|
8150
|
-
return this.state.on(
|
|
9259
|
+
subscribe(path10, callback) {
|
|
9260
|
+
return this.state.on(path10, callback);
|
|
8151
9261
|
}
|
|
8152
|
-
getSnapshot(
|
|
8153
|
-
return this.state.get(
|
|
9262
|
+
getSnapshot(path10) {
|
|
9263
|
+
return this.state.get(path10);
|
|
8154
9264
|
}
|
|
8155
9265
|
/**
|
|
8156
9266
|
* Accept a message from the controller. The message is queued and handed
|
|
@@ -8238,15 +9348,16 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
8238
9348
|
effectiveAccessScope() {
|
|
8239
9349
|
if (this._accessScope) return this._accessScope;
|
|
8240
9350
|
const wayangDir = wayangDirForSessionDir(this.sessionDir);
|
|
8241
|
-
const homeDir2 = wayangDir && basename7(wayangDir) === ".wayang" ? dirname8(wayangDir) :
|
|
9351
|
+
const homeDir2 = wayangDir && basename7(wayangDir) === ".wayang" ? dirname8(wayangDir) : homedir2();
|
|
8242
9352
|
return { homeDir: homeDir2, workspace: this.workspaceDir };
|
|
8243
9353
|
}
|
|
8244
9354
|
/**
|
|
8245
9355
|
* 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
|
|
9356
|
+
* worker uses (`decideToolAccess`). A sensitive WRITE is denied outright
|
|
9357
|
+
* (no approval prompt), a sensitive READ asks with the matched entry named,
|
|
9358
|
+
* an ambiguous target asks the controller and a path inside the allowed scope
|
|
9359
|
+
* runs immediately. This closes the parity gap where the old regex-only gate
|
|
9360
|
+
* let sensitive paths through silently.
|
|
8250
9361
|
*/
|
|
8251
9362
|
async handleCanUseTool(toolName, input2) {
|
|
8252
9363
|
if (!this._signalQueue || !this._runContext) {
|
|
@@ -8255,42 +9366,91 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
8255
9366
|
const scope = this.effectiveAccessScope();
|
|
8256
9367
|
if (toolName === "Bash") {
|
|
8257
9368
|
const verdict = decideToolAccess(scope, "bash", { command: String(input2.command ?? "") });
|
|
8258
|
-
|
|
8259
|
-
|
|
8260
|
-
|
|
8261
|
-
if (verdict.decision === "ask") return this.requestPermission(toolName, input2, verdict.reason);
|
|
9369
|
+
const operation = operationForTool("bash");
|
|
9370
|
+
if (verdict.decision === "deny") return this.denyWithAudit(toolName, input2, verdict, operation);
|
|
9371
|
+
if (verdict.decision === "ask") return this.requestPermission(toolName, input2, this.askOf(verdict, operation));
|
|
8262
9372
|
return { behavior: "allow" };
|
|
8263
9373
|
}
|
|
8264
9374
|
const fileTool = _ClaudeCodeWorker.SDK_FILE_TOOL_MAP[toolName];
|
|
8265
9375
|
if (fileTool) {
|
|
9376
|
+
const operation = operationForTool(fileTool.policyTool);
|
|
8266
9377
|
const rawPath = input2[fileTool.pathKey];
|
|
8267
9378
|
if (typeof rawPath !== "string" || rawPath.length === 0) {
|
|
8268
9379
|
if (fileTool.pathRequired) {
|
|
8269
|
-
return this.requestPermission(toolName, input2,
|
|
9380
|
+
return this.requestPermission(toolName, input2, {
|
|
9381
|
+
reason: "the target path could not be determined",
|
|
9382
|
+
operation
|
|
9383
|
+
});
|
|
8270
9384
|
}
|
|
8271
9385
|
return { behavior: "allow" };
|
|
8272
9386
|
}
|
|
8273
9387
|
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);
|
|
9388
|
+
if (verdict.decision === "deny") return this.denyWithAudit(toolName, input2, verdict, operation);
|
|
9389
|
+
if (verdict.decision === "ask") return this.requestPermission(toolName, input2, this.askOf(verdict, operation));
|
|
8278
9390
|
return { behavior: "allow" };
|
|
8279
9391
|
}
|
|
8280
9392
|
if (_ClaudeCodeWorker.SDK_NON_PATH_TOOLS.has(toolName)) {
|
|
8281
9393
|
return { behavior: "allow" };
|
|
8282
9394
|
}
|
|
8283
|
-
return this.requestPermission(toolName, input2,
|
|
9395
|
+
return this.requestPermission(toolName, input2, {
|
|
9396
|
+
reason: "the tool is not recognized by the permission policy",
|
|
9397
|
+
operation: operationForTool("bash")
|
|
9398
|
+
});
|
|
9399
|
+
}
|
|
9400
|
+
/**
|
|
9401
|
+
* Refuse a call the policy hard-denied (sensitive write) and record it.
|
|
9402
|
+
*
|
|
9403
|
+
* The worker receives the policy's disclosure-safe message (its own path
|
|
9404
|
+
* spelling, never the matched entry); the audit trail gets the metadata.
|
|
9405
|
+
* Same shape as the puppet worker's audit, so both entries agree.
|
|
9406
|
+
*/
|
|
9407
|
+
denyWithAudit(toolName, input2, verdict, operation) {
|
|
9408
|
+
recordPermissionAudit(this.logger, {
|
|
9409
|
+
decision: "hard-denied",
|
|
9410
|
+
operation,
|
|
9411
|
+
access: auditAccessFor(verdict.access),
|
|
9412
|
+
resolvedPath: verdict.resolvedPath,
|
|
9413
|
+
requestedPath: this.requestedPathOf(input2),
|
|
9414
|
+
matchedEntry: verdict.matchedEntry,
|
|
9415
|
+
matchedSource: verdict.matchedSource,
|
|
9416
|
+
matchedRule: verdict.matchedRule,
|
|
9417
|
+
taskId: this._runContext?.taskId,
|
|
9418
|
+
workerId: this._runContext?.workerId,
|
|
9419
|
+
toolName
|
|
9420
|
+
});
|
|
9421
|
+
return { behavior: "deny", message: verdict.message ?? "Denied by the permission policy" };
|
|
9422
|
+
}
|
|
9423
|
+
/** Project a policy verdict onto the approval-request/audit fields. */
|
|
9424
|
+
askOf(verdict, operation) {
|
|
9425
|
+
return {
|
|
9426
|
+
reason: verdict.reason,
|
|
9427
|
+
matchedRule: verdict.matchedRule,
|
|
9428
|
+
matchedEntry: verdict.matchedEntry,
|
|
9429
|
+
matchedSource: verdict.matchedSource,
|
|
9430
|
+
resolvedPath: verdict.resolvedPath,
|
|
9431
|
+
operation,
|
|
9432
|
+
sensitive: verdict.sensitive,
|
|
9433
|
+
access: verdict.access
|
|
9434
|
+
};
|
|
9435
|
+
}
|
|
9436
|
+
/** The target as the worker spelled it (for auditing against the prompt). */
|
|
9437
|
+
requestedPathOf(input2) {
|
|
9438
|
+
for (const key of ["file_path", "notebook_path", "path"]) {
|
|
9439
|
+
const value = input2[key];
|
|
9440
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
9441
|
+
}
|
|
9442
|
+
return void 0;
|
|
8284
9443
|
}
|
|
8285
9444
|
/**
|
|
8286
9445
|
* Emit a permission_request and block until the controller answers (or it
|
|
8287
9446
|
* times out → deny). Shared by every `ask` verdict so the request flow stays
|
|
8288
9447
|
* identical to the puppet worker's.
|
|
8289
9448
|
*/
|
|
8290
|
-
async requestPermission(toolName, input2,
|
|
9449
|
+
async requestPermission(toolName, input2, ask) {
|
|
8291
9450
|
if (!this._signalQueue || !this._runContext) return { behavior: "allow" };
|
|
8292
9451
|
const requestId = generateId("perm");
|
|
8293
|
-
const description = this.describePermissionRequest(toolName, input2,
|
|
9452
|
+
const description = this.describePermissionRequest(toolName, input2, ask);
|
|
9453
|
+
const operation = ask.operation;
|
|
8294
9454
|
this._signalQueue.enqueue({
|
|
8295
9455
|
source: "worker",
|
|
8296
9456
|
type: "permission_request",
|
|
@@ -8303,16 +9463,43 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
8303
9463
|
emoji: this._runContext.emoji,
|
|
8304
9464
|
toolName,
|
|
8305
9465
|
toolArgs: input2,
|
|
8306
|
-
description
|
|
9466
|
+
description,
|
|
9467
|
+
...ask.matchedRule !== void 0 && { matchedRule: ask.matchedRule },
|
|
9468
|
+
...ask.matchedEntry !== void 0 && { matchedEntry: ask.matchedEntry },
|
|
9469
|
+
...ask.matchedSource !== void 0 && { matchedSource: ask.matchedSource },
|
|
9470
|
+
...ask.resolvedPath !== void 0 && { resolvedPath: ask.resolvedPath },
|
|
9471
|
+
...ask.sensitive !== void 0 && { sensitive: ask.sensitive },
|
|
9472
|
+
...ask.access !== void 0 && { access: ask.access },
|
|
9473
|
+
operation
|
|
8307
9474
|
},
|
|
8308
9475
|
...this._turnLineage
|
|
8309
9476
|
});
|
|
8310
|
-
|
|
8311
|
-
|
|
9477
|
+
const result2 = await this._pendingPermissions.wait(
|
|
9478
|
+
requestId,
|
|
9479
|
+
_ClaudeCodeWorker.PERMISSION_TIMEOUT_MS,
|
|
9480
|
+
() => ({ behavior: "deny", message: "Permission request timed out" })
|
|
9481
|
+
);
|
|
9482
|
+
const denied = result2.behavior === "deny";
|
|
9483
|
+
recordPermissionAudit(this.logger, {
|
|
9484
|
+
decision: denied ? "denied" : "approved",
|
|
9485
|
+
operation,
|
|
9486
|
+
access: auditAccessFor(ask.access),
|
|
9487
|
+
resolvedPath: ask.resolvedPath,
|
|
9488
|
+
requestedPath: this.requestedPathOf(input2),
|
|
9489
|
+
matchedEntry: ask.matchedEntry,
|
|
9490
|
+
matchedSource: ask.matchedSource,
|
|
9491
|
+
matchedRule: ask.matchedRule,
|
|
9492
|
+
taskId: this._runContext.taskId,
|
|
9493
|
+
workerId: this._runContext.workerId,
|
|
9494
|
+
toolName,
|
|
9495
|
+
...denied && {
|
|
9496
|
+
source: result2.message === "Permission request timed out" ? "timeout" : "controller"
|
|
9497
|
+
}
|
|
8312
9498
|
});
|
|
9499
|
+
return result2;
|
|
8313
9500
|
}
|
|
8314
9501
|
/** Human-readable summary of a gated call for the approval prompt. */
|
|
8315
|
-
describePermissionRequest(toolName, input2,
|
|
9502
|
+
describePermissionRequest(toolName, input2, ask) {
|
|
8316
9503
|
let detail;
|
|
8317
9504
|
if (toolName === "Bash") {
|
|
8318
9505
|
detail = `Execute shell command: ${String(input2.command ?? "")}`;
|
|
@@ -8325,7 +9512,15 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
8325
9512
|
} else {
|
|
8326
9513
|
detail = `${toolName}: ${JSON.stringify(input2)}`;
|
|
8327
9514
|
}
|
|
8328
|
-
|
|
9515
|
+
const hit = ask.sensitive ? verdictDetailSuffix({
|
|
9516
|
+
matchedRule: ask.matchedRule,
|
|
9517
|
+
matchedEntry: ask.matchedEntry,
|
|
9518
|
+
matchedSource: ask.matchedSource,
|
|
9519
|
+
resolvedPath: ask.resolvedPath,
|
|
9520
|
+
operation: ask.operation
|
|
9521
|
+
}) : "";
|
|
9522
|
+
const withHit = `${detail}${hit}`;
|
|
9523
|
+
return ask.reason ? `${withHit} (${ask.reason})` : withHit;
|
|
8329
9524
|
}
|
|
8330
9525
|
// --- Prompt as AsyncIterable (supports follow-up messages) ---
|
|
8331
9526
|
buildPromptIterable(initialPrompt) {
|
|
@@ -8630,11 +9825,14 @@ controller: ${answer}`,
|
|
|
8630
9825
|
};
|
|
8631
9826
|
|
|
8632
9827
|
// src/services/tools/add-task.ts
|
|
9828
|
+
import { mkdirSync as mkdirSync7 } from "fs";
|
|
9829
|
+
import { homedir as homedir3 } from "os";
|
|
9830
|
+
import * as path8 from "path";
|
|
8633
9831
|
import { z as z3 } from "zod";
|
|
8634
9832
|
|
|
8635
9833
|
// src/services/tools/workspace-rules.ts
|
|
8636
|
-
import { readFileSync as
|
|
8637
|
-
import { join as
|
|
9834
|
+
import { readFileSync as readFileSync10, statSync as statSync5 } from "fs";
|
|
9835
|
+
import { join as join18 } from "path";
|
|
8638
9836
|
var RULE_FILE_NAMES2 = ["AGENTS.md", "CLAUDE.md"];
|
|
8639
9837
|
var WORKSPACE_RULES_MAX_LINES = 40;
|
|
8640
9838
|
var WORKSPACE_RULES_MAX_CHARS = 2e3;
|
|
@@ -8642,7 +9840,7 @@ function buildWorkspaceRulesSummary(input2) {
|
|
|
8642
9840
|
const workspacePath = input2.workspacePath;
|
|
8643
9841
|
if (!workspacePath) return "";
|
|
8644
9842
|
for (const name of RULE_FILE_NAMES2) {
|
|
8645
|
-
const rulesPath =
|
|
9843
|
+
const rulesPath = join18(workspacePath, name);
|
|
8646
9844
|
if (!isRegularFile(rulesPath)) continue;
|
|
8647
9845
|
const header = `Workspace rules (from ${rulesPath}):`;
|
|
8648
9846
|
const refusal = describeRefusal(rulesPath, workspacePath, input2);
|
|
@@ -8664,16 +9862,17 @@ function describeRefusal(rulesPath, workspacePath, input2) {
|
|
|
8664
9862
|
if (!homeDir2) {
|
|
8665
9863
|
return "unavailable \u2014 the permission policy cannot be evaluated here (no home directory in this context), so the file was not read.";
|
|
8666
9864
|
}
|
|
8667
|
-
const { decision } = decideAccess({
|
|
9865
|
+
const { decision, matchedEntry } = decideAccess({
|
|
8668
9866
|
access: "read",
|
|
8669
9867
|
path: rulesPath,
|
|
8670
9868
|
cwd: workspacePath,
|
|
8671
9869
|
homeDir: homeDir2,
|
|
8672
9870
|
taskWorkspace: workspacePath,
|
|
8673
|
-
|
|
9871
|
+
extraSensitiveDirs: mergeSensitiveDirs(input2.permissionConfig),
|
|
9872
|
+
sensitiveMode: resolveSensitiveMode(input2.permissionConfig)
|
|
8674
9873
|
});
|
|
8675
|
-
if (decision === "deny") {
|
|
8676
|
-
return "unavailable \u2014 the file resolves to a
|
|
9874
|
+
if (matchedEntry !== void 0 || decision === "deny") {
|
|
9875
|
+
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
9876
|
}
|
|
8678
9877
|
if (decision === "ask") {
|
|
8679
9878
|
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,31 +9899,64 @@ function summarizeRules(content) {
|
|
|
8700
9899
|
}
|
|
8701
9900
|
function isRegularFile(p) {
|
|
8702
9901
|
try {
|
|
8703
|
-
return
|
|
9902
|
+
return statSync5(p).isFile();
|
|
8704
9903
|
} catch {
|
|
8705
9904
|
return false;
|
|
8706
9905
|
}
|
|
8707
9906
|
}
|
|
8708
9907
|
function readTextFile(p) {
|
|
8709
9908
|
try {
|
|
8710
|
-
return
|
|
9909
|
+
return readFileSync10(p, "utf8");
|
|
8711
9910
|
} catch {
|
|
8712
9911
|
return null;
|
|
8713
9912
|
}
|
|
8714
9913
|
}
|
|
8715
9914
|
|
|
8716
9915
|
// src/services/tools/add-task.ts
|
|
9916
|
+
var HOME_VAR_PREFIX = /^\$(?:\{HOME\}|HOME)(?=\/|$)/;
|
|
9917
|
+
function expandHomePrefix(raw, homeDir2) {
|
|
9918
|
+
if (raw === "~") return homeDir2;
|
|
9919
|
+
if (raw.startsWith("~/")) return path8.join(homeDir2, raw.slice(2));
|
|
9920
|
+
const match = HOME_VAR_PREFIX.exec(raw);
|
|
9921
|
+
if (match) {
|
|
9922
|
+
const rest = raw.slice(match[0].length);
|
|
9923
|
+
return rest === "" ? homeDir2 : path8.join(homeDir2, rest);
|
|
9924
|
+
}
|
|
9925
|
+
return raw.startsWith("~") ? null : raw;
|
|
9926
|
+
}
|
|
9927
|
+
function ensureWorkspaceDir(dirPath) {
|
|
9928
|
+
try {
|
|
9929
|
+
mkdirSync7(dirPath, { recursive: true });
|
|
9930
|
+
return null;
|
|
9931
|
+
} catch (err) {
|
|
9932
|
+
return `Cannot create workspace ${dirPath}: ${err.message}. The task was NOT created.`;
|
|
9933
|
+
}
|
|
9934
|
+
}
|
|
8717
9935
|
function resolveTaskWorkspace(requested, deps) {
|
|
8718
9936
|
const current = deps.workspaceDir;
|
|
8719
|
-
|
|
9937
|
+
let declared;
|
|
9938
|
+
if (requested !== void 0 && requested.trim() !== "") {
|
|
9939
|
+
const expanded = expandHomePrefix(requested.trim(), deps.homeDir ?? homedir3());
|
|
9940
|
+
if (expanded === null) {
|
|
9941
|
+
return {
|
|
9942
|
+
error: `Cannot resolve workspace "${requested}": only "~" and "$HOME" expand to the home directory, so no readable path was left. The task was NOT created.`
|
|
9943
|
+
};
|
|
9944
|
+
}
|
|
9945
|
+
declared = expanded;
|
|
9946
|
+
}
|
|
8720
9947
|
if (!current) {
|
|
8721
|
-
if (
|
|
9948
|
+
if (declared !== void 0) {
|
|
8722
9949
|
return {
|
|
8723
|
-
error: `Cannot resolve workspace ${
|
|
9950
|
+
error: `Cannot resolve workspace ${normalizeWorkspacePath(declared)}: the current workspace is unknown in this context, so the task was NOT created.`
|
|
8724
9951
|
};
|
|
8725
9952
|
}
|
|
8726
9953
|
return {};
|
|
8727
9954
|
}
|
|
9955
|
+
if (declared !== void 0 && path8.isAbsolute(declared)) {
|
|
9956
|
+
const dirError = ensureWorkspaceDir(declared);
|
|
9957
|
+
if (dirError !== null) return { error: dirError };
|
|
9958
|
+
}
|
|
9959
|
+
const requestedPath = declared !== void 0 ? normalizeWorkspacePath(declared) : void 0;
|
|
8728
9960
|
const targetPath = requestedPath ?? normalizeWorkspacePath(current);
|
|
8729
9961
|
const ref = resolveWorkspaceRef(deps.wayangDir ?? null, targetPath);
|
|
8730
9962
|
return { workspace: { path: ref.path, slug: ref.slug } };
|
|
@@ -8739,9 +9971,9 @@ function resolveTaskAllowedDirs(requested, workspacePath, deps) {
|
|
|
8739
9971
|
}
|
|
8740
9972
|
const home = normalizePath(homeDir2);
|
|
8741
9973
|
const base = workspacePath !== void 0 ? normalizePath(workspacePath, { base: home }) : home;
|
|
8742
|
-
const
|
|
8743
|
-
...
|
|
8744
|
-
...(deps.permissionConfig
|
|
9974
|
+
const sensitivePaths = [
|
|
9975
|
+
...defaultSensitivePaths(home),
|
|
9976
|
+
...mergeSensitiveDirs(deps.permissionConfig).map((d) => normalizePath(d, { base: home }))
|
|
8745
9977
|
].map((d) => normalizePath(d));
|
|
8746
9978
|
const seen = /* @__PURE__ */ new Set();
|
|
8747
9979
|
const allowedDirs = [];
|
|
@@ -8757,9 +9989,9 @@ function resolveTaskAllowedDirs(requested, workspacePath, deps) {
|
|
|
8757
9989
|
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
9990
|
};
|
|
8759
9991
|
}
|
|
8760
|
-
if (isDenied(resolved,
|
|
9992
|
+
if (isDenied(resolved, sensitivePaths)) {
|
|
8761
9993
|
return {
|
|
8762
|
-
error: `Invalid allowedDirs entry "${raw}" (resolved to ${resolved}): it matches the
|
|
9994
|
+
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
9995
|
};
|
|
8764
9996
|
}
|
|
8765
9997
|
if (!seen.has(resolved)) {
|
|
@@ -8780,6 +10012,7 @@ function withWorkspaceRules(resultText, workspacePath, deps) {
|
|
|
8780
10012
|
${summary}`;
|
|
8781
10013
|
}
|
|
8782
10014
|
function addTaskTool(deps) {
|
|
10015
|
+
const generalWorkspaceNote = deps.generalWorkspaceDir ? ` Tasks that belong to no codebase (research, collecting material, drafting a document) run in the general workspace ${deps.generalWorkspaceDir} \u2014 it is created on first use.` : "";
|
|
8783
10016
|
return defineTool({
|
|
8784
10017
|
description: "Add a new task to the queue. A Worker will execute it automatically.",
|
|
8785
10018
|
parameters: z3.object({
|
|
@@ -8794,7 +10027,7 @@ function addTaskTool(deps) {
|
|
|
8794
10027
|
"Dispatch this task to an existing idle worker (the workerId returned from a prior multiStage task). The worker resumes with its accumulated context. Omit for a fresh worker."
|
|
8795
10028
|
),
|
|
8796
10029
|
workspace: z3.string().optional().describe(
|
|
8797
|
-
|
|
10030
|
+
`Target workspace path; defaults to the current workspace. A different path runs this task there \u2014 the worker, its tools and its permission checks use it as their root. A leading ~ or $HOME expands to the home directory, and a missing directory is created.${generalWorkspaceNote}`
|
|
8798
10031
|
),
|
|
8799
10032
|
allowedDirs: z3.array(z3.string()).optional().describe(
|
|
8800
10033
|
"Extra directories this task may access, in addition to its workspace. Only this task is affected \u2014 the grant is NOT inherited by other tasks. Each entry must be inside the user home and off the denied-path list (credentials, tool config); an invalid entry fails the whole call and no task is created. Paths are normalized; relative entries resolve against the task workspace."
|
|
@@ -9362,7 +10595,7 @@ ${detail}`;
|
|
|
9362
10595
|
|
|
9363
10596
|
// src/services/tools/read-file.ts
|
|
9364
10597
|
import { z as z14 } from "zod";
|
|
9365
|
-
import { readFileSync as
|
|
10598
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
9366
10599
|
import { resolve as resolve6 } from "path";
|
|
9367
10600
|
var DEFAULT_MAX_LINES = 500;
|
|
9368
10601
|
function readFileTool(deps = {}) {
|
|
@@ -9374,9 +10607,9 @@ function readFileTool(deps = {}) {
|
|
|
9374
10607
|
offset: z14.number().int().min(1).optional().describe("Start line number (1-based)"),
|
|
9375
10608
|
limit: z14.number().int().min(1).optional().describe("Number of lines to read")
|
|
9376
10609
|
}),
|
|
9377
|
-
execute: safeExecute("read_file", async ({ path:
|
|
9378
|
-
const resolved = resolve6(workspace,
|
|
9379
|
-
const content =
|
|
10610
|
+
execute: safeExecute("read_file", async ({ path: path10, offset, limit }) => {
|
|
10611
|
+
const resolved = resolve6(workspace, path10);
|
|
10612
|
+
const content = readFileSync11(resolved, "utf-8");
|
|
9380
10613
|
const lines = content.split("\n");
|
|
9381
10614
|
const totalLines = lines.length;
|
|
9382
10615
|
const start = offset ? offset - 1 : 0;
|
|
@@ -9396,7 +10629,7 @@ function readFileTool(deps = {}) {
|
|
|
9396
10629
|
|
|
9397
10630
|
// src/services/tools/write-file.ts
|
|
9398
10631
|
import { z as z15 } from "zod";
|
|
9399
|
-
import { writeFileSync as writeFileSync4, mkdirSync as
|
|
10632
|
+
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync8, existsSync as existsSync10 } from "fs";
|
|
9400
10633
|
import { dirname as dirname9, resolve as resolve7 } from "path";
|
|
9401
10634
|
function writeFileTool(deps = {}) {
|
|
9402
10635
|
const workspace = deps.cwd ?? process.cwd();
|
|
@@ -9406,12 +10639,12 @@ function writeFileTool(deps = {}) {
|
|
|
9406
10639
|
path: z15.string().describe("File path (relative to workspace or absolute)"),
|
|
9407
10640
|
content: z15.string().describe("File content")
|
|
9408
10641
|
}),
|
|
9409
|
-
execute: safeExecute("write_file", async ({ path:
|
|
9410
|
-
const resolved = resolve7(workspace,
|
|
10642
|
+
execute: safeExecute("write_file", async ({ path: path10, content }) => {
|
|
10643
|
+
const resolved = resolve7(workspace, path10);
|
|
9411
10644
|
if (existsSync10(resolved)) {
|
|
9412
|
-
return `[ERROR] write_file: File already exists: ${
|
|
10645
|
+
return `[ERROR] write_file: File already exists: ${path10}. Use edit_file to modify existing files.`;
|
|
9413
10646
|
}
|
|
9414
|
-
|
|
10647
|
+
mkdirSync8(dirname9(resolved), { recursive: true });
|
|
9415
10648
|
writeFileSync4(resolved, content, "utf-8");
|
|
9416
10649
|
return `Written ${content.length} chars to ${resolved}`;
|
|
9417
10650
|
})
|
|
@@ -9419,7 +10652,7 @@ function writeFileTool(deps = {}) {
|
|
|
9419
10652
|
}
|
|
9420
10653
|
|
|
9421
10654
|
// src/services/tools/edit-file.ts
|
|
9422
|
-
import { readFileSync as
|
|
10655
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "fs";
|
|
9423
10656
|
import { resolve as resolve8 } from "path";
|
|
9424
10657
|
import { z as z16 } from "zod";
|
|
9425
10658
|
function editFileTool(deps) {
|
|
@@ -9431,22 +10664,22 @@ function editFileTool(deps) {
|
|
|
9431
10664
|
old_string: z16.string().describe("Exact text to find (must be unique in file)"),
|
|
9432
10665
|
new_string: z16.string().describe("Replacement text")
|
|
9433
10666
|
}),
|
|
9434
|
-
execute: safeExecute("edit_file", async ({ path:
|
|
9435
|
-
const resolved = resolve8(workspace,
|
|
10667
|
+
execute: safeExecute("edit_file", async ({ path: path10, old_string, new_string }) => {
|
|
10668
|
+
const resolved = resolve8(workspace, path10);
|
|
9436
10669
|
let content;
|
|
9437
10670
|
try {
|
|
9438
|
-
content =
|
|
10671
|
+
content = readFileSync12(resolved, "utf-8");
|
|
9439
10672
|
} catch {
|
|
9440
|
-
return `[ERROR] edit_file: File not found: ${
|
|
10673
|
+
return `[ERROR] edit_file: File not found: ${path10}`;
|
|
9441
10674
|
}
|
|
9442
10675
|
const count = content.split(old_string).length - 1;
|
|
9443
10676
|
if (count === 0)
|
|
9444
|
-
return `[ERROR] edit_file: old_string not found in ${
|
|
10677
|
+
return `[ERROR] edit_file: old_string not found in ${path10}, read the file content and provide a correct old_string.`;
|
|
9445
10678
|
if (count > 1)
|
|
9446
|
-
return `[ERROR] edit_file: old_string found ${count} times in ${
|
|
10679
|
+
return `[ERROR] edit_file: old_string found ${count} times in ${path10}. Provide more surrounding context to make it unique.`;
|
|
9447
10680
|
const newContent = content.replace(old_string, new_string);
|
|
9448
10681
|
writeFileSync5(resolved, newContent, "utf-8");
|
|
9449
|
-
return `Replaced ${old_string.length} chars with ${new_string.length} chars in ${
|
|
10682
|
+
return `Replaced ${old_string.length} chars with ${new_string.length} chars in ${path10}`;
|
|
9450
10683
|
})
|
|
9451
10684
|
});
|
|
9452
10685
|
}
|
|
@@ -9463,18 +10696,18 @@ function searchFilesTool(deps) {
|
|
|
9463
10696
|
pattern: z17.string().describe('Glob pattern (e.g. "**/*.ts", "src/**/*.test.ts")'),
|
|
9464
10697
|
path: z17.string().optional().describe("Base directory for search (default: workspace root)")
|
|
9465
10698
|
}),
|
|
9466
|
-
execute: safeExecute("search_files", async ({ pattern, path:
|
|
9467
|
-
const basePath =
|
|
10699
|
+
execute: safeExecute("search_files", async ({ pattern, path: path10 }) => {
|
|
10700
|
+
const basePath = path10 ? normalizePath(path10, { base: workspace }) : normalizePath(workspace);
|
|
9468
10701
|
if (homeDir2) {
|
|
9469
|
-
const { decision } = decideAccess({
|
|
10702
|
+
const { decision, matchedEntry } = decideAccess({
|
|
9470
10703
|
access: "read",
|
|
9471
10704
|
homeDir: homeDir2,
|
|
9472
10705
|
cwd: workspace,
|
|
9473
10706
|
path: basePath
|
|
9474
10707
|
});
|
|
9475
|
-
const requested =
|
|
9476
|
-
if (decision === "deny") {
|
|
9477
|
-
return `[ERROR] search_files: refusing to search "${requested}" \u2014 the path is on the
|
|
10708
|
+
const requested = path10 ?? workspace;
|
|
10709
|
+
if (matchedEntry !== void 0 || decision === "deny") {
|
|
10710
|
+
return `[ERROR] search_files: refusing to search "${requested}" \u2014 the path is on the sensitive list (a protected location).`;
|
|
9478
10711
|
}
|
|
9479
10712
|
if (decision === "ask") {
|
|
9480
10713
|
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.`;
|
|
@@ -9507,8 +10740,8 @@ function searchContentTool(deps) {
|
|
|
9507
10740
|
path: z18.string().optional().describe("Directory to search in (default: workspace root)"),
|
|
9508
10741
|
include: z18.string().optional().describe('File pattern to include (e.g. "*.ts", "*.py")')
|
|
9509
10742
|
}),
|
|
9510
|
-
execute: safeExecute("search_content", async ({ query: query2, path:
|
|
9511
|
-
const searchDir =
|
|
10743
|
+
execute: safeExecute("search_content", async ({ query: query2, path: path10, include }) => {
|
|
10744
|
+
const searchDir = path10 ? resolve9(workspace, path10) : workspace;
|
|
9512
10745
|
const cmd = `grep -rn${include ? ` --include=${shellQuote(include)}` : ""} -- ${shellQuote(query2)} ${shellQuote(searchDir)}`;
|
|
9513
10746
|
try {
|
|
9514
10747
|
const output = execSync3(cmd, {
|
|
@@ -9771,6 +11004,7 @@ function createControllerTools(deps) {
|
|
|
9771
11004
|
onTaskDispatched: deps.onTaskDispatched,
|
|
9772
11005
|
workspaceDir: deps.workspaceDir,
|
|
9773
11006
|
wayangDir: deps.wayangDir,
|
|
11007
|
+
generalWorkspaceDir: deps.generalWorkspaceDir,
|
|
9774
11008
|
homeDir: deps.homeDir,
|
|
9775
11009
|
permissionConfig: deps.permissionConfig
|
|
9776
11010
|
}),
|
|
@@ -9878,11 +11112,32 @@ function wrapWithPermissionMiddleware(tools, config, deps) {
|
|
|
9878
11112
|
return originalExecute(args, options);
|
|
9879
11113
|
}
|
|
9880
11114
|
if (verdict.decision === "deny") {
|
|
11115
|
+
recordPermissionAudit(deps.logger, {
|
|
11116
|
+
decision: "hard-denied",
|
|
11117
|
+
operation: verdict.operation ?? operationForTool(toolName),
|
|
11118
|
+
access: auditAccessFor(verdict.access),
|
|
11119
|
+
resolvedPath: verdict.resolvedPath,
|
|
11120
|
+
requestedPath: typeof args.path === "string" ? args.path : void 0,
|
|
11121
|
+
matchedEntry: verdict.matchedEntry,
|
|
11122
|
+
matchedSource: verdict.matchedSource,
|
|
11123
|
+
matchedRule: verdict.matchedRule,
|
|
11124
|
+
taskId: deps.taskId,
|
|
11125
|
+
workerId: deps.workerId,
|
|
11126
|
+
toolName
|
|
11127
|
+
});
|
|
9881
11128
|
return verdict.message ?? `[ERROR] ${toolName}: refused by the path permission policy`;
|
|
9882
11129
|
}
|
|
9883
11130
|
const requestId = generateId("perm");
|
|
11131
|
+
const operation = verdict.operation ?? operationForTool(toolName);
|
|
9884
11132
|
const baseDescription = config.describeCall(toolName, args);
|
|
9885
|
-
const
|
|
11133
|
+
const hitDetail = verdict.sensitive ? verdictDetailSuffix({
|
|
11134
|
+
matchedRule: verdict.matchedRule,
|
|
11135
|
+
matchedEntry: verdict.matchedEntry,
|
|
11136
|
+
matchedSource: verdict.matchedSource,
|
|
11137
|
+
resolvedPath: verdict.resolvedPath,
|
|
11138
|
+
operation
|
|
11139
|
+
}) : "";
|
|
11140
|
+
const description = `${baseDescription}${hitDetail}${verdict.reason ? ` [reason: ${verdict.reason}]` : ""}`;
|
|
9886
11141
|
deps.signalQueue.enqueue({
|
|
9887
11142
|
source: "worker",
|
|
9888
11143
|
type: "permission_request",
|
|
@@ -9895,7 +11150,14 @@ function wrapWithPermissionMiddleware(tools, config, deps) {
|
|
|
9895
11150
|
emoji: deps.emoji,
|
|
9896
11151
|
toolName,
|
|
9897
11152
|
toolArgs: args,
|
|
9898
|
-
description
|
|
11153
|
+
description,
|
|
11154
|
+
...verdict.matchedRule !== void 0 && { matchedRule: verdict.matchedRule },
|
|
11155
|
+
...verdict.matchedEntry !== void 0 && { matchedEntry: verdict.matchedEntry },
|
|
11156
|
+
...verdict.matchedSource !== void 0 && { matchedSource: verdict.matchedSource },
|
|
11157
|
+
...verdict.resolvedPath !== void 0 && { resolvedPath: verdict.resolvedPath },
|
|
11158
|
+
...verdict.sensitive !== void 0 && { sensitive: verdict.sensitive },
|
|
11159
|
+
...verdict.access !== void 0 && { access: verdict.access },
|
|
11160
|
+
operation
|
|
9899
11161
|
},
|
|
9900
11162
|
...deps.turnLineage ?? {}
|
|
9901
11163
|
});
|
|
@@ -9913,6 +11175,22 @@ function wrapWithPermissionMiddleware(tools, config, deps) {
|
|
|
9913
11175
|
timer
|
|
9914
11176
|
});
|
|
9915
11177
|
});
|
|
11178
|
+
recordPermissionAudit(deps.logger, {
|
|
11179
|
+
decision: result2.approved ? "approved" : "denied",
|
|
11180
|
+
operation,
|
|
11181
|
+
access: auditAccessFor(verdict.access),
|
|
11182
|
+
resolvedPath: verdict.resolvedPath,
|
|
11183
|
+
requestedPath: typeof args.path === "string" ? args.path : void 0,
|
|
11184
|
+
matchedEntry: verdict.matchedEntry,
|
|
11185
|
+
matchedSource: verdict.matchedSource,
|
|
11186
|
+
matchedRule: verdict.matchedRule,
|
|
11187
|
+
taskId: deps.taskId,
|
|
11188
|
+
workerId: deps.workerId,
|
|
11189
|
+
toolName,
|
|
11190
|
+
...!result2.approved && {
|
|
11191
|
+
source: result2.reason === "timeout" ? "timeout" : result2.reason === "Worker aborted" ? "aborted" : "controller"
|
|
11192
|
+
}
|
|
11193
|
+
});
|
|
9916
11194
|
if (!result2.approved) {
|
|
9917
11195
|
if (result2.reason === "timeout") {
|
|
9918
11196
|
return `[ERROR] Permission request timed out for ${toolName}`;
|
|
@@ -9985,7 +11263,7 @@ function formatApiCallError(err) {
|
|
|
9985
11263
|
|
|
9986
11264
|
// src/services/task-execute-engine.ts
|
|
9987
11265
|
import { basename as basename8, dirname as dirname10, resolve as resolve10 } from "path";
|
|
9988
|
-
import { homedir as
|
|
11266
|
+
import { homedir as homedir4 } from "os";
|
|
9989
11267
|
var RuntimeState = class extends BaseWayangState {
|
|
9990
11268
|
constructor(initialData) {
|
|
9991
11269
|
super(initialData, []);
|
|
@@ -9996,7 +11274,7 @@ var RuntimeState = class extends BaseWayangState {
|
|
|
9996
11274
|
var PUPPET_WORKER_TYPE = "puppet";
|
|
9997
11275
|
function resolveUserHomeDir(sessionDir) {
|
|
9998
11276
|
const wayangDir = wayangDirForSessionDir(sessionDir);
|
|
9999
|
-
return wayangDir && basename8(wayangDir) === ".wayang" ? dirname10(wayangDir) :
|
|
11277
|
+
return wayangDir && basename8(wayangDir) === ".wayang" ? dirname10(wayangDir) : homedir4();
|
|
10000
11278
|
}
|
|
10001
11279
|
var DEFAULT_IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
10002
11280
|
var INQUIRY_TIMEOUT_MS = 3e5;
|
|
@@ -10065,15 +11343,15 @@ var TaskExecuteEngine = class {
|
|
|
10065
11343
|
// ---------------------------------------------------------------------------
|
|
10066
11344
|
// Subscribable
|
|
10067
11345
|
// ---------------------------------------------------------------------------
|
|
10068
|
-
subscribe(
|
|
10069
|
-
if (
|
|
10070
|
-
if (
|
|
10071
|
-
throw new Error(`TaskExecuteEngine: unknown subscription path "${
|
|
11346
|
+
subscribe(path10, callback) {
|
|
11347
|
+
if (path10.startsWith("tasks.")) return this.taskState.on(path10, callback);
|
|
11348
|
+
if (path10 === "activeWorkers" || path10.startsWith("activeWorkers.")) return this.workerState.on(path10, callback);
|
|
11349
|
+
throw new Error(`TaskExecuteEngine: unknown subscription path "${path10}"`);
|
|
10072
11350
|
}
|
|
10073
|
-
getSnapshot(
|
|
10074
|
-
if (
|
|
10075
|
-
if (
|
|
10076
|
-
throw new Error(`TaskExecuteEngine: unknown snapshot path "${
|
|
11351
|
+
getSnapshot(path10) {
|
|
11352
|
+
if (path10.startsWith("tasks.")) return this.taskState.get(path10);
|
|
11353
|
+
if (path10 === "activeWorkers" || path10.startsWith("activeWorkers.")) return this.workerState.get(path10);
|
|
11354
|
+
throw new Error(`TaskExecuteEngine: unknown snapshot path "${path10}"`);
|
|
10077
11355
|
}
|
|
10078
11356
|
// ---------------------------------------------------------------------------
|
|
10079
11357
|
// Task management
|
|
@@ -10944,9 +12222,10 @@ var TaskExecuteEngine = class {
|
|
|
10944
12222
|
if (workerType === PUPPET_WORKER_TYPE) {
|
|
10945
12223
|
const middleware = wrapWithPermissionMiddleware(tools, {
|
|
10946
12224
|
gatedTools: ["bash", ...FILE_TOOL_NAMES],
|
|
10947
|
-
// Single decision source: the shared access policy (
|
|
12225
|
+
// Single decision source: the shared access policy (sensitive list →
|
|
10948
12226
|
// config roots → task allowedDirs → task workspace → read-in-home →
|
|
10949
|
-
// ask). `deny` refuses here without ever emitting a permission_request
|
|
12227
|
+
// ask). `deny` refuses here without ever emitting a permission_request;
|
|
12228
|
+
// both `deny` and every answered `ask` are audited (see the middleware).
|
|
10950
12229
|
decide: (toolName, args) => decideToolAccess(accessScope, toolName, args),
|
|
10951
12230
|
describeCall: (toolName, args) => {
|
|
10952
12231
|
if (toolName === "bash") return `Execute shell command: ${args.command}`;
|
|
@@ -10964,7 +12243,9 @@ var TaskExecuteEngine = class {
|
|
|
10964
12243
|
workerType,
|
|
10965
12244
|
emoji: workerMeta.emoji,
|
|
10966
12245
|
signalQueue: this.signalQueue,
|
|
10967
|
-
turnLineage: this.workerLineage(workerId)
|
|
12246
|
+
turnLineage: this.workerLineage(workerId),
|
|
12247
|
+
// Permission audit trail (metadata only) shares the session logger.
|
|
12248
|
+
logger: this.ctx.logger
|
|
10968
12249
|
});
|
|
10969
12250
|
tools = middleware.wrappedTools;
|
|
10970
12251
|
this.permissionResolvers.set(workerId, middleware);
|
|
@@ -11218,7 +12499,7 @@ var ControllerLoop = class {
|
|
|
11218
12499
|
};
|
|
11219
12500
|
|
|
11220
12501
|
// src/services/agents/controller-state.ts
|
|
11221
|
-
import { join as
|
|
12502
|
+
import { join as join20 } from "path";
|
|
11222
12503
|
|
|
11223
12504
|
// src/services/conversation/conversation-index.ts
|
|
11224
12505
|
function emptyUsage() {
|
|
@@ -11442,8 +12723,8 @@ var ControllerAgentState = class extends BaseWayangState {
|
|
|
11442
12723
|
/** In-memory resolve function for the current inquiry — never persisted. */
|
|
11443
12724
|
inquiryResolver = null;
|
|
11444
12725
|
constructor(ctx) {
|
|
11445
|
-
const jsonFile = new JSONFileHelper(
|
|
11446
|
-
const jsonlFile = new JSONLFileHelper(
|
|
12726
|
+
const jsonFile = new JSONFileHelper(join20(ctx.sessionDir, "runtime-state.json"));
|
|
12727
|
+
const jsonlFile = new JSONLFileHelper(join20(ctx.sessionDir, "conversation.jsonl"));
|
|
11447
12728
|
const initialData = {
|
|
11448
12729
|
runtimeState: {
|
|
11449
12730
|
session: { id: "", startedAt: 0 },
|
|
@@ -11468,9 +12749,9 @@ var ControllerAgentState = class extends BaseWayangState {
|
|
|
11468
12749
|
* shared conversation-envelope module — the single source of truth. This
|
|
11469
12750
|
* override only feeds the in-memory turn index incrementally.
|
|
11470
12751
|
*/
|
|
11471
|
-
append(
|
|
11472
|
-
super.append(
|
|
11473
|
-
if (
|
|
12752
|
+
append(path10, entry) {
|
|
12753
|
+
super.append(path10, entry);
|
|
12754
|
+
if (path10 === "conversation" && this.convIndex) {
|
|
11474
12755
|
this.convIndex.onAppend(entry);
|
|
11475
12756
|
}
|
|
11476
12757
|
}
|
|
@@ -11613,8 +12894,8 @@ function buildSummaryInput(entries, maxChars) {
|
|
|
11613
12894
|
lines = formatAll(entries, caps);
|
|
11614
12895
|
if (totalLength(lines) <= maxChars) return lines;
|
|
11615
12896
|
}
|
|
11616
|
-
const
|
|
11617
|
-
const reserve =
|
|
12897
|
+
const noteFor2 = (n) => `[${n} older entries omitted \u2014 over the summarizer input budget]`;
|
|
12898
|
+
const reserve = noteFor2(lines.length).length + 1;
|
|
11618
12899
|
const kept = [];
|
|
11619
12900
|
let used = 0;
|
|
11620
12901
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
@@ -11624,12 +12905,12 @@ function buildSummaryInput(entries, maxChars) {
|
|
|
11624
12905
|
used += size;
|
|
11625
12906
|
}
|
|
11626
12907
|
const omitted = lines.length - kept.length;
|
|
11627
|
-
return omitted > 0 ? [
|
|
12908
|
+
return omitted > 0 ? [noteFor2(omitted), ...kept] : kept;
|
|
11628
12909
|
}
|
|
11629
12910
|
|
|
11630
12911
|
// src/services/agents/controller-agent.ts
|
|
11631
12912
|
import { basename as basename9, dirname as dirname11 } from "path";
|
|
11632
|
-
import { homedir as
|
|
12913
|
+
import { homedir as homedir5 } from "os";
|
|
11633
12914
|
var SUMMARIZER_INPUT_RATIO = 0.5;
|
|
11634
12915
|
var ControllerAgent = class _ControllerAgent extends BaseAgent {
|
|
11635
12916
|
state;
|
|
@@ -11662,7 +12943,7 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
|
|
|
11662
12943
|
* cancelled while its workers are still running.
|
|
11663
12944
|
*/
|
|
11664
12945
|
turnInterruptRecorded = false;
|
|
11665
|
-
constructor(ctx, state, provider, tools, skills, recorder = new ToolCallRecorder()) {
|
|
12946
|
+
constructor(ctx, state, provider, tools, skills, recorder = new ToolCallRecorder(), controllerRules) {
|
|
11666
12947
|
super(provider);
|
|
11667
12948
|
this.ctx = ctx;
|
|
11668
12949
|
this.state = state;
|
|
@@ -11673,7 +12954,7 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
|
|
|
11673
12954
|
this.workspaceSlug = ctx.workspaceDir ? resolveWorkspaceRef(wayangDirForSessionDir(ctx.sessionDir), ctx.workspaceDir).slug : void 0;
|
|
11674
12955
|
this.contextManager = new ContextManager(
|
|
11675
12956
|
state,
|
|
11676
|
-
buildControllerSystemPrompt(this.ctx, skills),
|
|
12957
|
+
buildControllerSystemPrompt(this.ctx, skills, { rules: controllerRules }),
|
|
11677
12958
|
provider.contextWindowTokens
|
|
11678
12959
|
);
|
|
11679
12960
|
this.setHooks({
|
|
@@ -11695,7 +12976,17 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
|
|
|
11695
12976
|
const state = new ControllerAgentState(ctx);
|
|
11696
12977
|
const recorder = new ToolCallRecorder(ctx.logger);
|
|
11697
12978
|
const wayangDir = wayangDirForSessionDir(ctx.sessionDir);
|
|
11698
|
-
const homeDir2 = wayangDir && basename9(wayangDir) === ".wayang" ? dirname11(wayangDir) :
|
|
12979
|
+
const homeDir2 = wayangDir && basename9(wayangDir) === ".wayang" ? dirname11(wayangDir) : homedir5();
|
|
12980
|
+
const generalWorkspaceDir = resolveGeneralWorkspace(ctx);
|
|
12981
|
+
try {
|
|
12982
|
+
ensureGeneralWorkspace(ctx);
|
|
12983
|
+
} catch {
|
|
12984
|
+
ctx.logger.warn(`could not create the general workspace at ${generalWorkspaceDir}`);
|
|
12985
|
+
}
|
|
12986
|
+
const controllerRules = loadControllerRules({
|
|
12987
|
+
homeDir: homeDir2,
|
|
12988
|
+
cwd: ctx.workspaceDir || process.cwd()
|
|
12989
|
+
});
|
|
11699
12990
|
const tools = createControllerTools({
|
|
11700
12991
|
addTask: (task) => engine.add(task),
|
|
11701
12992
|
validateWorkerType: (type) => engine.validateWorkerType(type),
|
|
@@ -11725,7 +13016,8 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
|
|
|
11725
13016
|
cwd: ctx.workspaceDir,
|
|
11726
13017
|
homeDir: homeDir2,
|
|
11727
13018
|
// User-config permission section: add_task validates a task's allowedDirs
|
|
11728
|
-
// against the built-in
|
|
13019
|
+
// against the built-in sensitive list plus these configured
|
|
13020
|
+
// sensitiveDirs (the deprecated deniedDirs alias is merged in).
|
|
11729
13021
|
permissionConfig: opts.config.permission,
|
|
11730
13022
|
getNotebook: () => state.get("runtimeState.notebook") ?? "",
|
|
11731
13023
|
setNotebook: (content, mode) => {
|
|
@@ -11748,9 +13040,10 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
|
|
|
11748
13040
|
// Default workspace for tasks created by add_task; a declared workspace
|
|
11749
13041
|
// is stamped instead and the engine executes the task there.
|
|
11750
13042
|
workspaceDir: ctx.workspaceDir,
|
|
11751
|
-
wayangDir
|
|
13043
|
+
wayangDir,
|
|
13044
|
+
generalWorkspaceDir
|
|
11752
13045
|
});
|
|
11753
|
-
return new _ControllerAgent(ctx, state, provider, tools, skills, recorder);
|
|
13046
|
+
return new _ControllerAgent(ctx, state, provider, tools, skills, recorder, controllerRules);
|
|
11754
13047
|
}
|
|
11755
13048
|
/** Run the controller agent — streams text chunks while updating streamingEntries in state. */
|
|
11756
13049
|
async run(signals) {
|
|
@@ -12015,11 +13308,11 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
|
|
|
12015
13308
|
return this.state.getTurn(turnId);
|
|
12016
13309
|
}
|
|
12017
13310
|
// --- Subscribable ---
|
|
12018
|
-
subscribe(
|
|
12019
|
-
return this.state.on(
|
|
13311
|
+
subscribe(path10, callback) {
|
|
13312
|
+
return this.state.on(path10, callback);
|
|
12020
13313
|
}
|
|
12021
|
-
getSnapshot(
|
|
12022
|
-
return this.state.get(
|
|
13314
|
+
getSnapshot(path10) {
|
|
13315
|
+
return this.state.get(path10);
|
|
12023
13316
|
}
|
|
12024
13317
|
/** Check if context needs compaction. */
|
|
12025
13318
|
needsCompaction() {
|
|
@@ -12347,8 +13640,8 @@ function signalToEntry(sig, ts, sessionId) {
|
|
|
12347
13640
|
}
|
|
12348
13641
|
|
|
12349
13642
|
// src/services/skills/registry.ts
|
|
12350
|
-
import { readFileSync as
|
|
12351
|
-
import { join as
|
|
13643
|
+
import { readFileSync as readFileSync13, readdirSync as readdirSync2, statSync as statSync6 } from "fs";
|
|
13644
|
+
import { join as join21 } from "path";
|
|
12352
13645
|
import { parse as parseYaml } from "yaml";
|
|
12353
13646
|
var SKILL_FILE = "SKILL.md";
|
|
12354
13647
|
function parseFrontmatter(raw) {
|
|
@@ -12383,7 +13676,7 @@ function listResources(skillDir) {
|
|
|
12383
13676
|
const walk = (relDir) => {
|
|
12384
13677
|
let entries;
|
|
12385
13678
|
try {
|
|
12386
|
-
entries = readdirSync2(
|
|
13679
|
+
entries = readdirSync2(join21(skillDir, relDir), { withFileTypes: true });
|
|
12387
13680
|
} catch {
|
|
12388
13681
|
return;
|
|
12389
13682
|
}
|
|
@@ -12430,10 +13723,10 @@ var SkillRegistry = class {
|
|
|
12430
13723
|
if (existing) return existing;
|
|
12431
13724
|
const desc = this.descriptors.get(name);
|
|
12432
13725
|
if (!desc) return void 0;
|
|
12433
|
-
const filePath =
|
|
13726
|
+
const filePath = join21(desc.dir, SKILL_FILE);
|
|
12434
13727
|
let raw;
|
|
12435
13728
|
try {
|
|
12436
|
-
raw =
|
|
13729
|
+
raw = readFileSync13(filePath, "utf-8");
|
|
12437
13730
|
} catch (err) {
|
|
12438
13731
|
this.logger?.warn({ skill: name, error: err instanceof Error ? err.message : String(err) }, "Failed to read skill file");
|
|
12439
13732
|
return void 0;
|
|
@@ -12461,8 +13754,8 @@ var SkillRegistry = class {
|
|
|
12461
13754
|
for (const entry of entries) {
|
|
12462
13755
|
if (!entry.isDirectory()) continue;
|
|
12463
13756
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
12464
|
-
const skillDir =
|
|
12465
|
-
const skillFile =
|
|
13757
|
+
const skillDir = join21(dir, entry.name);
|
|
13758
|
+
const skillFile = join21(skillDir, SKILL_FILE);
|
|
12466
13759
|
if (!existsAsFile(skillFile)) continue;
|
|
12467
13760
|
const { frontmatter } = safeReadFrontmatter(skillFile);
|
|
12468
13761
|
const description = asString(frontmatter.description);
|
|
@@ -12475,16 +13768,16 @@ var SkillRegistry = class {
|
|
|
12475
13768
|
}
|
|
12476
13769
|
}
|
|
12477
13770
|
};
|
|
12478
|
-
function existsAsFile(
|
|
13771
|
+
function existsAsFile(path10) {
|
|
12479
13772
|
try {
|
|
12480
|
-
return
|
|
13773
|
+
return statSync6(path10).isFile();
|
|
12481
13774
|
} catch {
|
|
12482
13775
|
return false;
|
|
12483
13776
|
}
|
|
12484
13777
|
}
|
|
12485
|
-
function safeReadFrontmatter(
|
|
13778
|
+
function safeReadFrontmatter(path10) {
|
|
12486
13779
|
try {
|
|
12487
|
-
return parseFrontmatter(
|
|
13780
|
+
return parseFrontmatter(readFileSync13(path10, "utf-8"));
|
|
12488
13781
|
} catch {
|
|
12489
13782
|
return { frontmatter: {}, body: "" };
|
|
12490
13783
|
}
|
|
@@ -12492,12 +13785,12 @@ function safeReadFrontmatter(path9) {
|
|
|
12492
13785
|
|
|
12493
13786
|
// src/services/skills/skill-dir.ts
|
|
12494
13787
|
import { existsSync as existsSync11 } from "fs";
|
|
12495
|
-
import { homedir as
|
|
12496
|
-
import { join as
|
|
13788
|
+
import { homedir as homedir6 } from "os";
|
|
13789
|
+
import { join as join22, resolve as resolve11 } from "path";
|
|
12497
13790
|
function resolveSkillDirs(workspaceDir2, configDirs) {
|
|
12498
13791
|
const candidates = [
|
|
12499
|
-
|
|
12500
|
-
|
|
13792
|
+
join22(homedir6(), ".wayang", "skills"),
|
|
13793
|
+
join22(workspaceDir2, ".wayang", "skills"),
|
|
12501
13794
|
...(configDirs ?? []).map((d) => resolve11(d))
|
|
12502
13795
|
];
|
|
12503
13796
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -12824,11 +14117,11 @@ async function runWayangServer(options, io = process) {
|
|
|
12824
14117
|
let nextSubscriptionId = 1;
|
|
12825
14118
|
rpc.register(METHOD.SUBSCRIBE, (params) => {
|
|
12826
14119
|
const target = resolveTarget(params.target);
|
|
12827
|
-
const
|
|
12828
|
-
const data = target.getSnapshot(
|
|
14120
|
+
const path10 = requiredString(params, "path");
|
|
14121
|
+
const data = target.getSnapshot(path10);
|
|
12829
14122
|
const subscriptionId = `sub-${nextSubscriptionId++}`;
|
|
12830
|
-
subscriptions.set(subscriptionId, target.subscribe(
|
|
12831
|
-
rpc.notify(NOTIFY.STATE_EVENT, { subscriptionId, target: params.target, path:
|
|
14123
|
+
subscriptions.set(subscriptionId, target.subscribe(path10, (event) => {
|
|
14124
|
+
rpc.notify(NOTIFY.STATE_EVENT, { subscriptionId, target: params.target, path: path10, data: event.data });
|
|
12832
14125
|
}));
|
|
12833
14126
|
return { subscriptionId, data: data === void 0 ? null : data };
|
|
12834
14127
|
});
|
|
@@ -13009,9 +14302,9 @@ if (result.status === "error") {
|
|
|
13009
14302
|
process.exit(1);
|
|
13010
14303
|
}
|
|
13011
14304
|
var { input, flags } = result;
|
|
13012
|
-
var homeDir =
|
|
14305
|
+
var homeDir = path9.resolve(flags.homeDir ?? (process.env.HOME || "~"));
|
|
13013
14306
|
var workspaceDir = normalizeWorkspacePath(flags.workspaceDir ?? process.cwd());
|
|
13014
|
-
var configPath = flags.config ?
|
|
14307
|
+
var configPath = flags.config ? path9.resolve(flags.config) : path9.join(homeDir, ".wayang.config.json");
|
|
13015
14308
|
function fatal(err) {
|
|
13016
14309
|
if (err instanceof SessionLockedError) {
|
|
13017
14310
|
console.error(formatSessionLockHint(err));
|
|
@@ -13023,7 +14316,7 @@ function fatal(err) {
|
|
|
13023
14316
|
if (input[0] === "server") {
|
|
13024
14317
|
serverMain({
|
|
13025
14318
|
configPath,
|
|
13026
|
-
homeDir:
|
|
14319
|
+
homeDir: path9.join(homeDir, ".wayang"),
|
|
13027
14320
|
workspaceDir,
|
|
13028
14321
|
session: flags.session ?? "new",
|
|
13029
14322
|
injectPreviousTasks: flags.injectPreviousTasks,
|