wezard 1.2.16 → 1.2.19
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/daemon/approval.js +80 -30
- package/dist/daemon/approval.js.map +1 -1
- package/dist/daemon/mirror-bridge.js +10 -7
- package/dist/daemon/mirror-bridge.js.map +1 -1
- package/dist/daemon/peers.js +6 -5
- package/dist/daemon/peers.js.map +1 -1
- package/dist/daemon/ws.js +11 -0
- package/dist/daemon/ws.js.map +1 -1
- package/dist/shared/claude-config-path.js +12 -2
- package/dist/shared/claude-config-path.js.map +1 -1
- package/package.json +1 -1
package/dist/daemon/approval.js
CHANGED
|
@@ -235,16 +235,18 @@ const bodyBlocks = (a, r) => {
|
|
|
235
235
|
const quoteArea = (text, url) => (url
|
|
236
236
|
? { type: 1, url, quote_text: text }
|
|
237
237
|
: { type: 0, quote_text: text });
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
|
|
238
|
+
// 必发卡 (危险名单 / askRules / `.claude/**` 守卫): 只有 ❌ / ✅ — 不给「N 分钟
|
|
239
|
+
// 全过」「✅总是」的入口, 否则一次点击就把后续所有这类操作也放行了, 名单等于失效。
|
|
240
|
+
// 判据是 forceSingle 而非 danger: 另两个来源的卡同样吃 handler 侧的 `!mustCard`
|
|
241
|
+
// 短路, 只按 danger 收按钮的话它们会画出点了没反应、也不 sweep 同批的死按钮。
|
|
242
|
+
const approveButtons = (a) => a.danger || a.forceSingle
|
|
241
243
|
? [
|
|
242
244
|
{ text: "❌", style: 4, key: encodeKey(a.reqId, "deny") },
|
|
243
245
|
{ text: "✅ 确认执行", style: 4, key: encodeKey(a.reqId, "allow") },
|
|
244
246
|
]
|
|
245
247
|
: [
|
|
246
248
|
{ text: "❌", style: 4, key: encodeKey(a.reqId, "deny") },
|
|
247
|
-
{ text:
|
|
249
|
+
{ text: windowLabel(a.windowMinutes), style: 1, key: encodeKey(a.reqId, "allow_window") },
|
|
248
250
|
// 「总是」= 由本次调用生成一条 allowRules 并落盘, 对齐 Claude Code 原生
|
|
249
251
|
// 弹窗的 Always allow。危险卡上没有这个入口 (与「⏱全过」同理)。
|
|
250
252
|
{ text: "✅总是", style: 4, key: encodeKey(a.reqId, "allow_always") },
|
|
@@ -287,6 +289,9 @@ const buildCard = (a) => {
|
|
|
287
289
|
};
|
|
288
290
|
};
|
|
289
291
|
const fmtWindow = (min) => min % 60 === 0 ? `${min / 60}h` : `${min}min`;
|
|
292
|
+
// 按钮上的时间窗文案: 裸「10h」看不出是放行, 补 ⏱ + 「自动过」点明语义。
|
|
293
|
+
// (style 也从 3 改成 1 — 企微 3 渲染成红色, 与「放行」相悖。)
|
|
294
|
+
const windowLabel = (min) => `⏱${fmtWindow(min)}自动过`;
|
|
290
295
|
const verbOf = (d, windowMinutes) => {
|
|
291
296
|
switch (d) {
|
|
292
297
|
case "deny": return "已拒绝";
|
|
@@ -413,7 +418,10 @@ const buildBatchCard = (batch, transcriptTail) => ({
|
|
|
413
418
|
task_id: batch.batchId,
|
|
414
419
|
button_list: [
|
|
415
420
|
{ text: "❌", style: 4, key: encodeBatchKey(batch.batchId, "deny") },
|
|
416
|
-
{ text:
|
|
421
|
+
{ text: windowLabel(batch.windowMinutes), style: 1, key: encodeBatchKey(batch.batchId, "allow_window") },
|
|
422
|
+
// 批量卡同样给「总是」: 合流的成员是同一个工具的 N 次调用, 逐个点「总是」与
|
|
423
|
+
// 点一次的结果相同(每位成员各自走一遍规则生成, 已被现有规则覆盖的不重复加)。
|
|
424
|
+
{ text: "✅总是", style: 4, key: encodeBatchKey(batch.batchId, "allow_always") },
|
|
417
425
|
{ text: `✅ ×${batch.members.length}`, style: 4, key: encodeBatchKey(batch.batchId, "allow") },
|
|
418
426
|
],
|
|
419
427
|
});
|
|
@@ -1232,9 +1240,14 @@ const handleAskUserQuestion = async ({ cfg, log, client, body, getMirrorTarget,
|
|
|
1232
1240
|
return { decision: "deny", reason };
|
|
1233
1241
|
};
|
|
1234
1242
|
const decisionToHook = (d) => (d === "deny" ? "deny" : "allow");
|
|
1235
|
-
// 必发卡的请求 (危险名单 / askRules
|
|
1236
|
-
//
|
|
1237
|
-
//
|
|
1243
|
+
// 必发卡的请求 (危险名单 / askRules) 永不走 fallbackOnError: "allow" — 超时/断线时
|
|
1244
|
+
// 降级为 ask, 交回本地 CLI 由人来确认, 而不是静默放行一次 rm。只判 danger 的话,
|
|
1245
|
+
// 用户自己配的 askRules 在 daemon 挂掉时反而失效。
|
|
1246
|
+
//
|
|
1247
|
+
// `.claude/**` 守卫虽然不进 mustCard, 但在**错误面**也要走同一条降级: 正常出口靠
|
|
1248
|
+
// settleGuard 事后按框, 而这三条 (no_approver / ws_disconnected / approver_timeout)
|
|
1249
|
+
// 要么没有 approver 可按、要么 pane 早已在等框 —— 静默 allow 就是把死锁原样放回来。
|
|
1250
|
+
// 所以调用点传的是 `mustCard || guardActive`, 见 handler 里那三处。
|
|
1238
1251
|
const fallback = (cfg, reason, forceSingle) => ({
|
|
1239
1252
|
decision: forceSingle && cfg.approval.fallbackOnError === "allow" ? "ask" : cfg.approval.fallbackOnError,
|
|
1240
1253
|
reason: forceSingle ? `${reason}:force_single` : reason,
|
|
@@ -1291,7 +1304,7 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1291
1304
|
// 按不掉时不干等: Esc 取消掉这次调用, 再把原因作为一条用户消息注入 pane。模型
|
|
1292
1305
|
// 收到的就是明确的"这条路走不通 + 该怎么绕", 而不是静默卡住 —— 等价于预拦截
|
|
1293
1306
|
// deny + reason, 只是走镜像通道传达。
|
|
1294
|
-
const
|
|
1307
|
+
const settleOne = async (sessionId, approver, hit) => {
|
|
1295
1308
|
const nm = nativeModal;
|
|
1296
1309
|
if (!nm)
|
|
1297
1310
|
return;
|
|
@@ -1320,6 +1333,22 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1320
1333
|
await notify(approver, `⚠️ 原生确认框未能代按(${r.status})。已${cancelled.ok ? "" : "尝试"}发 Esc 取消本次调用`
|
|
1321
1334
|
+ `${told.ok ? ",并把原因告知了会话" : `(原因注入失败:${told.reason ?? "unknown"},请到 tmux 里看一眼)`}。`);
|
|
1322
1335
|
};
|
|
1336
|
+
// 同一 pane 上的收尾必须串行。守卫退出 mustCard 之后, 一轮里 N 个 `.claude/**` 写
|
|
1337
|
+
// 会一起走窗口/缓存/规则的快路径、同刻返回 —— 以前每张卡等人点, 天然串成一列。
|
|
1338
|
+
// 并发进 answer() 的话两个协程会看见同一个框都按下去, 第二下落到下一个框或输入框上。
|
|
1339
|
+
// 按 sessionId 串一条 promise 链: 每次收尾等前一次跑完, 失败也不断链。
|
|
1340
|
+
const settleChains = new Map();
|
|
1341
|
+
const settleClaudeConfigModal = (sessionId, approver, hit) => {
|
|
1342
|
+
const next = (settleChains.get(sessionId) ?? Promise.resolve())
|
|
1343
|
+
.then(() => settleOne(sessionId, approver, hit));
|
|
1344
|
+
// 链尾自清: 只有还是自己时才删, 避免把后来者的链一起抹掉。
|
|
1345
|
+
const tail = next.catch(() => { }).finally(() => {
|
|
1346
|
+
if (settleChains.get(sessionId) === tail)
|
|
1347
|
+
settleChains.delete(sessionId);
|
|
1348
|
+
});
|
|
1349
|
+
settleChains.set(sessionId, tail);
|
|
1350
|
+
return next;
|
|
1351
|
+
};
|
|
1323
1352
|
// Flush 一个 batch: 单成员 → 普通卡 (与未启用聚合一致); 多成员 → 批量卡。
|
|
1324
1353
|
// 发送失败时调用 failPending 让每位成员的 handler 走 fallbackOnError 路径,
|
|
1325
1354
|
// 与单卡路径上 sendMessage 抛错时的语义一致。
|
|
@@ -1348,6 +1377,7 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1348
1377
|
transcriptTail: m.transcriptTail,
|
|
1349
1378
|
windowMinutes: batch.windowMinutes,
|
|
1350
1379
|
danger: batch.danger,
|
|
1380
|
+
forceSingle: batch.forceSingle,
|
|
1351
1381
|
sessionName: batch.sessionName,
|
|
1352
1382
|
denyReason: batch.denyReason,
|
|
1353
1383
|
detailUrl: detailUrlFor(m.reqId, batch.approver),
|
|
@@ -1440,8 +1470,24 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1440
1470
|
const guardHit = cfg.approval.claudeConfigGuard ? claudeConfigWrite(toolName, toolInput) : undefined;
|
|
1441
1471
|
const guardActive = Boolean(guardHit && nativeModal?.hasPane(sessionId));
|
|
1442
1472
|
if (guardHit) {
|
|
1443
|
-
log.info({ toolName, sessionId, path: guardHit.path, why: guardHit.why, guardActive }, guardActive ? "claude-config guard
|
|
1473
|
+
log.info({ toolName, sessionId, path: guardHit.path, why: guardHit.why, guardActive }, guardActive ? "claude-config guard armed" : "claude-config write detected (no live pane, passing through)");
|
|
1444
1474
|
}
|
|
1475
|
+
const approver = resolveApprover(cfg, sessionId, getMirrorTarget);
|
|
1476
|
+
// 守卫的收尾 = 把 CC 那个不过 hook 的原生确认框按掉。它挂在**每一条 allow 出口**
|
|
1477
|
+
// 上, 而不是靠"强制发卡 + 人工点"来触发 —— 死锁的成因是没人按框, 不是没人点卡。
|
|
1478
|
+
//
|
|
1479
|
+
// 这条区别是有代价的历史: 守卫曾经进 mustCard, 于是 ⏱窗口/缓存/合流/sweep 全被
|
|
1480
|
+
// 它短路, 卡上那颗「⏱10h自动过」点了等于一次性放行, 窗口从来没开过。现在窗口
|
|
1481
|
+
// 照常生效, 框照样有人按。
|
|
1482
|
+
//
|
|
1483
|
+
// 必须在 json(res) 之后调用: CC 要等 hook 进程退出才会把框渲染出来。
|
|
1484
|
+
const settleGuard = () => {
|
|
1485
|
+
if (!guardActive || !guardHit || !approver)
|
|
1486
|
+
return;
|
|
1487
|
+
void settleClaudeConfigModal(sessionId, approver, guardHit).catch((e) => {
|
|
1488
|
+
log.warn({ err: e.message, sessionId }, "settleClaudeConfigModal failed");
|
|
1489
|
+
});
|
|
1490
|
+
};
|
|
1445
1491
|
// 危险名单 (daemon/danger.ts): 内置的 rm / 强推 / DROP / 敏感路径等。语义上
|
|
1446
1492
|
// 是「出厂自带的 askRules」—— 所以它必须和用户写的 askRules 站在同一层, 排在
|
|
1447
1493
|
// allowRules 之前。放在后面的话, 一条 `Bash(git *)` 这样的宽 allow 规则 (尤其
|
|
@@ -1449,8 +1495,9 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1449
1495
|
const danger = dangerOf(cfg, toolName, toolInput);
|
|
1450
1496
|
if (danger)
|
|
1451
1497
|
log.info({ toolName, sessionId, rule: danger.rule }, "danger hit — forcing single approval");
|
|
1452
|
-
// 必发卡 = 危险名单 或 askRules
|
|
1453
|
-
|
|
1498
|
+
// 必发卡 = 危险名单 或 askRules 命中。这两者的语义是「每次都要人单独看一眼」,
|
|
1499
|
+
// 所以压过放行/窗口/缓存。守卫不在其列 —— 见上面 settleGuard 的注释。
|
|
1500
|
+
const mustCard = Boolean(danger) || Boolean(askHit);
|
|
1454
1501
|
// allowRules: matcher 拦下的工具里再挖细粒度豁免 (Bash 可按命令前缀区分)。
|
|
1455
1502
|
// 交互卡工具 (AskUserQuestion 等) 在引擎内部硬保护, 规则写了也不放行。
|
|
1456
1503
|
// 用 evaluateAllow 而非 ruleAllows: 未命中时要把"因为哪一段"带到卡上,
|
|
@@ -1460,6 +1507,7 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1460
1507
|
const ruleHit = [...new Set(verdict.hits)].join(" + ");
|
|
1461
1508
|
log.info({ toolName, sessionId, rule: ruleHit }, "allow-rule skip");
|
|
1462
1509
|
json(res, 200, { decision: "allow", reason: `allow_rule:${ruleHit}` });
|
|
1510
|
+
settleGuard();
|
|
1463
1511
|
return;
|
|
1464
1512
|
}
|
|
1465
1513
|
const denyReason = verdict.allowed ? undefined : verdict.reason;
|
|
@@ -1523,21 +1571,18 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1523
1571
|
json(res, 200, resp);
|
|
1524
1572
|
return;
|
|
1525
1573
|
}
|
|
1526
|
-
// Approver 提前解析: auto-window 现在按 chat 维度生效, isAutoWindowActive
|
|
1527
|
-
// 检查要拿到 chatKey (= approver principal) 才能查。approver 缺失时 window
|
|
1528
|
-
// 检查跳过 (window 需要一个 chat 才存在), 走后面的 no_approver fallback。
|
|
1529
|
-
const approver = resolveApprover(cfg, sessionId, getMirrorTarget);
|
|
1530
1574
|
// danger.skip / danger 模式的早退。第三个参数是「除 danger 外还有没有别的必发卡
|
|
1531
|
-
// 理由」—— askRules
|
|
1532
|
-
//
|
|
1533
|
-
const earlyExit = dangerEarlyExit(cfg, danger, Boolean(askHit)
|
|
1575
|
+
// 理由」—— askRules 不能被 danger 的开关顺带关掉。守卫不在其列: 它要的是事后按框,
|
|
1576
|
+
// 早退照样能给 (settleGuard), 拿它挡早退等于把 danger 的开关废掉。
|
|
1577
|
+
const earlyExit = dangerEarlyExit(cfg, danger, Boolean(askHit));
|
|
1534
1578
|
if (earlyExit) {
|
|
1535
1579
|
log.info({ toolName, sessionId, reason: earlyExit }, "danger switch early exit");
|
|
1536
1580
|
json(res, 200, { decision: "allow", reason: earlyExit });
|
|
1581
|
+
settleGuard();
|
|
1537
1582
|
return;
|
|
1538
1583
|
}
|
|
1539
1584
|
// Auto-approve window: while active for THIS chat, requests short-circuit to allow.
|
|
1540
|
-
// mustCard (危险名单 / askRules
|
|
1585
|
+
// mustCard (危险名单 / askRules) 的请求不吃窗口 — 即使开着 ⏱ 也逐条确认。
|
|
1541
1586
|
if (!mustCard && approver && isAutoWindowActive(approver)) {
|
|
1542
1587
|
const remainSec = Math.ceil(autoWindowRemainingMs(approver) / 1000);
|
|
1543
1588
|
log.info({ toolName, sessionId, chatKey: approver, remainSec }, "auto-window allow");
|
|
@@ -1545,11 +1590,12 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1545
1590
|
decision: "allow",
|
|
1546
1591
|
reason: `auto_window:${remainSec}s`,
|
|
1547
1592
|
});
|
|
1593
|
+
settleGuard();
|
|
1548
1594
|
return;
|
|
1549
1595
|
}
|
|
1550
1596
|
// Session cache (mustCard 的请求同样不吃缓存)
|
|
1551
1597
|
const ck = cacheKey(sessionId, toolName, toolInput);
|
|
1552
|
-
// mustCard(危险名单 / askRules
|
|
1598
|
+
// mustCard(危险名单 / askRules)一律不吃缓存 —— 缓存的语义是「这个调用批过一次
|
|
1553
1599
|
// 就不再问」, 与「每次都要单独确认」直接冲突。
|
|
1554
1600
|
const cached = mustCard ? undefined : cacheGet(ck);
|
|
1555
1601
|
if (cached) {
|
|
@@ -1558,16 +1604,18 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1558
1604
|
decision: decisionToHook(cached),
|
|
1559
1605
|
reason: `cached:${cached}`,
|
|
1560
1606
|
});
|
|
1607
|
+
if (decisionToHook(cached) === "allow")
|
|
1608
|
+
settleGuard();
|
|
1561
1609
|
return;
|
|
1562
1610
|
}
|
|
1563
1611
|
if (!approver) {
|
|
1564
1612
|
log.warn("no approver configured");
|
|
1565
|
-
json(res, 200, fallback(cfg, "no_approver", mustCard));
|
|
1613
|
+
json(res, 200, fallback(cfg, "no_approver", mustCard || guardActive));
|
|
1566
1614
|
return;
|
|
1567
1615
|
}
|
|
1568
1616
|
if (!client.isConnected) {
|
|
1569
1617
|
log.warn("ws not connected");
|
|
1570
|
-
json(res, 200, fallback(cfg, "ws_disconnected", mustCard));
|
|
1618
|
+
json(res, 200, fallback(cfg, "ws_disconnected", mustCard || guardActive));
|
|
1571
1619
|
return;
|
|
1572
1620
|
}
|
|
1573
1621
|
// Build pending + card
|
|
@@ -1638,6 +1686,7 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1638
1686
|
approver,
|
|
1639
1687
|
windowMinutes: cfg.approval.windowMinutes,
|
|
1640
1688
|
danger: danger?.rule,
|
|
1689
|
+
forceSingle: mustCard,
|
|
1641
1690
|
sessionName,
|
|
1642
1691
|
denyReason,
|
|
1643
1692
|
members: [member],
|
|
@@ -1667,7 +1716,7 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1667
1716
|
return;
|
|
1668
1717
|
}
|
|
1669
1718
|
log.warn({ err: e.message, reqId }, "approval timed out");
|
|
1670
|
-
json(res, 200, fallback(cfg, "approver_timeout", mustCard));
|
|
1719
|
+
json(res, 200, fallback(cfg, "approver_timeout", mustCard || guardActive));
|
|
1671
1720
|
return;
|
|
1672
1721
|
}
|
|
1673
1722
|
// 必发卡的决策一律不留痕: 不写 session cache、不开自动窗口 (卡上本就没这两个
|
|
@@ -1690,7 +1739,11 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1690
1739
|
await notify(approver, `⚠️ 该操作命中危险名单「${danger.rule}」,不支持「总是」:这类操作每次都要单独确认。`
|
|
1691
1740
|
+ `本次已放行。如确要长期免审,请在 config.jsonc 的 \`approval.danger.allowPatterns\` 里加豁免正则。`);
|
|
1692
1741
|
}
|
|
1693
|
-
|
|
1742
|
+
// `.claude/**` 守卫命中的调用现在可以正常存规则: 守卫不再靠"强制发卡"兜底,
|
|
1743
|
+
// settleGuard 挂在每条 allow 出口上 —— 规则放行的那一次同样会去按原生框。
|
|
1744
|
+
const gen = askHit || danger
|
|
1745
|
+
? []
|
|
1746
|
+
: alwaysAllowRulesFor(toolName, toolInput, cfg.approval.allowRules);
|
|
1694
1747
|
const added = gen.filter((r) => !cfg.approval.allowRules.includes(r));
|
|
1695
1748
|
if (added.length > 0) {
|
|
1696
1749
|
cfg.approval.allowRules.push(...added); // 先热生效; 文件写失败也不回滚内存
|
|
@@ -1761,11 +1814,8 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1761
1814
|
// 放行 `.claude/**` 写操作后, CC 会在 hook 退出后立起自己的原生确认框 —— 必须
|
|
1762
1815
|
// 等响应发出去才能去按 (框此刻还不存在)。fire-and-forget: 这条 HTTP 请求已经
|
|
1763
1816
|
// 结束, 失败也只能靠告知, 不能再改判决。
|
|
1764
|
-
if (
|
|
1765
|
-
|
|
1766
|
-
log.warn({ err: e.message, sessionId }, "settleClaudeConfigModal failed");
|
|
1767
|
-
});
|
|
1768
|
-
}
|
|
1817
|
+
if (decisionToHook(decision) === "allow")
|
|
1818
|
+
settleGuard();
|
|
1769
1819
|
};
|
|
1770
1820
|
};
|
|
1771
1821
|
// ── Card click event → resolvePending + update card in place ────────────
|