@shgroup/dsh-serenity-hooks 1.16.6 → 1.16.8
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/dsh.plugin.json +1 -1
- package/lib/index.js +313 -319
- package/package.json +1 -1
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-serenity-hooks",
|
|
3
|
-
"version": "1.16.
|
|
3
|
+
"version": "1.16.8",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"description": "宁静号 ACC harness(Native Cordis 插件):真实 DSH 工具 cc_fs/session/acc_msm/eap/neat/cce/loop + 拦截缝机械约束(safe-mode/路径守卫/会话落盘)。适配 DSH 公开版(0.1.0-rc,deepseek-ai/deepseek-harness)。私有(dsh-external 组织)。",
|
|
6
6
|
"engines": {
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import z from "@deepseek-ai/schemastery";
|
|
2
2
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
3
|
-
import { appendFileSync,
|
|
3
|
+
import { appendFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
5
5
|
import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
|
|
6
6
|
import { homedir, platform } from "node:os";
|
|
@@ -12,7 +12,7 @@ import { randomBytes, randomUUID } from "node:crypto";
|
|
|
12
12
|
/**
|
|
13
13
|
* ccc.ts — CCC 纯逻辑层(零 DSH 依赖,可独立单测)
|
|
14
14
|
*
|
|
15
|
-
* 职责:CCC 根检测(P1)、git 检测(P2
|
|
15
|
+
* 职责:CCC 根检测(P1)、git 检测(P2)、serenity.json 配置读取(.opencode 规范位置,.dsh 回退)、
|
|
16
16
|
* 路径守卫(P3 语义)、安全模式黑名单匹配。
|
|
17
17
|
*
|
|
18
18
|
* 由 tools/ 与 seams/ 复用;逻辑移植自 dsh-serenity-plugin v0.1-v0.2
|
|
@@ -66,7 +66,11 @@ function resolveInside(root, p) {
|
|
|
66
66
|
if (classifyPath(abs, root) === "outside") throw new Error(`Path escape blocked: "${p}" resolves outside "${root}"`);
|
|
67
67
|
return abs;
|
|
68
68
|
}
|
|
69
|
-
|
|
69
|
+
/**
|
|
70
|
+
* ACC 依赖的 CCC 配置路径(S134 修正:兼容历史——历史在 .opencode,规范位置 = .opencode;
|
|
71
|
+
* .dsh 仅作 dsh 运行时回退,不优先)。
|
|
72
|
+
*/
|
|
73
|
+
const DEFAULT_SERENITY_CONFIG_PATHS = [".opencode/serenity.json", ".dsh/serenity.json"];
|
|
70
74
|
function loadSerenityConfig(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
|
|
71
75
|
for (const candidate of paths) {
|
|
72
76
|
const p = resolve(root, candidate);
|
|
@@ -301,7 +305,7 @@ function runCcFs(root, args) {
|
|
|
301
305
|
* 进程内注册(取代 v0.1 的 bash spawn runner):zod/schemastery 参数校验、
|
|
302
306
|
* 规范 JSON 输出、纯 render 投影。逻辑在 fs-ops.ts(可单测)。
|
|
303
307
|
*/
|
|
304
|
-
function agentCwd$
|
|
308
|
+
function agentCwd$7(exec) {
|
|
305
309
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
306
310
|
}
|
|
307
311
|
function renderText$9(value) {
|
|
@@ -359,7 +363,7 @@ const ccFsTool = defineTool({
|
|
|
359
363
|
render: (args, value) => renderText$9(value)
|
|
360
364
|
},
|
|
361
365
|
async execute(args, exec) {
|
|
362
|
-
const root = findSerenityRoot(agentCwd$
|
|
366
|
+
const root = findSerenityRoot(agentCwd$7(exec));
|
|
363
367
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
364
368
|
return runCcFs(root, args);
|
|
365
369
|
}
|
|
@@ -983,7 +987,7 @@ function appendHeartbeat(sessionMd) {
|
|
|
983
987
|
* AGENT_SESSIONS/ 全周期管理:list/show/create/health/qa/archive/summary。
|
|
984
988
|
* 逻辑在 session-ops.ts(可单测)。
|
|
985
989
|
*/
|
|
986
|
-
function agentCwd$
|
|
990
|
+
function agentCwd$6(exec) {
|
|
987
991
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
988
992
|
}
|
|
989
993
|
/** 当前 dsh 会话 id(use/close 按会话隔离的 scope) */
|
|
@@ -1024,7 +1028,7 @@ const sessionTool = defineTool({
|
|
|
1024
1028
|
render: (args, value) => renderText$8(value)
|
|
1025
1029
|
},
|
|
1026
1030
|
async execute(args, exec) {
|
|
1027
|
-
const root = findSerenityRoot(agentCwd$
|
|
1031
|
+
const root = findSerenityRoot(agentCwd$6(exec));
|
|
1028
1032
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
1029
1033
|
if (findEntry(root, "session-tool")) {
|
|
1030
1034
|
const r = runMsm(root, {
|
|
@@ -1346,7 +1350,7 @@ async function runKit(root, args) {
|
|
|
1346
1350
|
/**
|
|
1347
1351
|
* kit.ts — acc_kit 真实 DSH 工具定义(defineTool)
|
|
1348
1352
|
*/
|
|
1349
|
-
function agentCwd$
|
|
1353
|
+
function agentCwd$5(exec) {
|
|
1350
1354
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
1351
1355
|
}
|
|
1352
1356
|
function renderText$7(value) {
|
|
@@ -1375,12 +1379,284 @@ const kitTool = defineTool({
|
|
|
1375
1379
|
render: (args, value) => renderText$7(value)
|
|
1376
1380
|
},
|
|
1377
1381
|
async execute(args, exec) {
|
|
1378
|
-
const root = findSerenityRoot(agentCwd$
|
|
1382
|
+
const root = findSerenityRoot(agentCwd$5(exec));
|
|
1379
1383
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
1380
1384
|
return await runKit(root, args);
|
|
1381
1385
|
}
|
|
1382
1386
|
});
|
|
1383
1387
|
//#endregion
|
|
1388
|
+
//#region src/localstore-ops.ts
|
|
1389
|
+
/**
|
|
1390
|
+
* localstore-ops.ts — localstore 纯逻辑层(零 DSH 依赖,可独立单测)
|
|
1391
|
+
*
|
|
1392
|
+
* S134 重设计(v1.16.7):存储从 ~/.serenity/ 迁到 **CCC 根根目录 localstore.json**
|
|
1393
|
+
* (JSON 格式——方便 MSM 直接 read + JSON.parse 读取,零解析依赖)。
|
|
1394
|
+
*
|
|
1395
|
+
* git 提交策略(可靠机制 × 用户自由):
|
|
1396
|
+
* - 配置:.opencode/serenity.json `localstore.gitTrack`: "allow"(可提交)| "deny"(禁提交)
|
|
1397
|
+
* - **缺省 deny(没配就是不提交)**;且 deny 的保证**不依赖 dsh 运行**——
|
|
1398
|
+
* 写入时自动确保 .gitignore 含 localstore.json(物理保证:即使 dsh 不在、
|
|
1399
|
+
* 用户手动 git commit 也不会误提交),cc_git 检查为第二道防线(拒绝 + 提示)
|
|
1400
|
+
* - allow:放行(文件可提交,用户自行管理 .gitignore)
|
|
1401
|
+
*
|
|
1402
|
+
* 存储结构(JSON 顶层分节):
|
|
1403
|
+
* { "credentials": { "HOME_GITLAB_TOKEN": "xxx" }, "<config节>": { "<key>": "v" } }
|
|
1404
|
+
* credentials 为保留节(credential 命名空间,key 大写蛇形);其余节归 config 命名空间
|
|
1405
|
+
* (path = section.key,如 loop.defaultModel)。
|
|
1406
|
+
*/
|
|
1407
|
+
const LOCALSTORE_SCOPES = ["credential", "config"];
|
|
1408
|
+
/** 存储文件名(CCC 根根目录) */
|
|
1409
|
+
const LOCALSTORE_FILENAME = "localstore.json";
|
|
1410
|
+
/** 凭据保留节名(JSON 顶层) */
|
|
1411
|
+
const CREDENTIALS_SECTION = "credentials";
|
|
1412
|
+
/** 凭据 key 规范:大写蛇形,如 HOME_GITLAB_TOKEN */
|
|
1413
|
+
const CREDENTIAL_KEY_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
1414
|
+
/** 配置节名规范:小写字母数字连字符 */
|
|
1415
|
+
const CONFIG_SECTION_RE = /^[a-z][a-z0-9-]*$/;
|
|
1416
|
+
/** 配置 key 规范(节内):小驼峰,如 defaultModel */
|
|
1417
|
+
const CONFIG_KEY_RE = /^[a-z][a-zA-Z0-9_]*$/;
|
|
1418
|
+
/** 存储文件绝对路径(CCC 根根目录) */
|
|
1419
|
+
function localstorePath(root) {
|
|
1420
|
+
return join(root, LOCALSTORE_FILENAME);
|
|
1421
|
+
}
|
|
1422
|
+
/** git 策略:serenity.json localstore.gitTrack,缺省 deny(不提交) */
|
|
1423
|
+
function readGitTrack(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
|
|
1424
|
+
return loadSerenityConfig(root, paths).localstore?.gitTrack === "allow" ? "allow" : "deny";
|
|
1425
|
+
}
|
|
1426
|
+
/** .gitignore 是否已覆盖 localstore 文件(非空非注释行含文件名即算) */
|
|
1427
|
+
function isLocalstoreGitignored(root) {
|
|
1428
|
+
const gi = join(root, ".gitignore");
|
|
1429
|
+
if (!existsSync(gi)) return false;
|
|
1430
|
+
return readFileSync(gi, "utf-8").split("\n").some((l) => {
|
|
1431
|
+
const t = l.trim();
|
|
1432
|
+
return t !== "" && !t.startsWith("#") && t.includes("localstore.json");
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* 确保 .gitignore 含 localstore 文件(deny 时的物理保证,不依赖 dsh 运行):
|
|
1437
|
+
* 写入 localstore 时调用——deny(含缺省)且 .gitignore 未覆盖 → 自动追加一行。
|
|
1438
|
+
* allow → 不写入(放行,文件可提交)。
|
|
1439
|
+
*/
|
|
1440
|
+
function ensureLocalstoreGitignored(root) {
|
|
1441
|
+
if (readGitTrack(root) === "allow") return { status: "allow" };
|
|
1442
|
+
if (isLocalstoreGitignored(root)) return { status: "ignored" };
|
|
1443
|
+
const gi = join(root, ".gitignore");
|
|
1444
|
+
const existing = existsSync(gi) ? readFileSync(gi, "utf-8") : "";
|
|
1445
|
+
const line = `${LOCALSTORE_FILENAME} # ACC localstore(localstore.gitTrack 缺省 deny,禁提交)`;
|
|
1446
|
+
writeFileSync(gi, (existing.endsWith("\n") || existing === "" ? existing : existing + "\n") + line + "\n", "utf-8");
|
|
1447
|
+
return { status: "appended" };
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* cc_git 联动检查(第二道防线):文件存在 && deny && .gitignore 未覆盖 → 不通过。
|
|
1451
|
+
* 调用方(cc_git commit)据此拒绝提交;status 可输出 warning。
|
|
1452
|
+
*/
|
|
1453
|
+
function checkLocalstoreGitCompliance(root) {
|
|
1454
|
+
if (!existsSync(localstorePath(root))) return { ok: true };
|
|
1455
|
+
if (readGitTrack(root) === "allow") return { ok: true };
|
|
1456
|
+
if (isLocalstoreGitignored(root)) return { ok: true };
|
|
1457
|
+
return {
|
|
1458
|
+
ok: false,
|
|
1459
|
+
reason: `localstore.json 禁止提交(localstore.gitTrack=deny 缺省)但 .gitignore 未包含——请将 ${LOCALSTORE_FILENAME} 加入 .gitignore(或改配置 localstore.gitTrack=allow 显式放行)`
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
/** 读取全文件(顶层分节);文件不存在/坏 JSON 返回空 */
|
|
1463
|
+
function readAll(root) {
|
|
1464
|
+
const p = localstorePath(root);
|
|
1465
|
+
if (!existsSync(p)) return {};
|
|
1466
|
+
try {
|
|
1467
|
+
const v = JSON.parse(readFileSync(p, "utf-8"));
|
|
1468
|
+
if (v && typeof v === "object" && !Array.isArray(v)) return v;
|
|
1469
|
+
return {};
|
|
1470
|
+
} catch {
|
|
1471
|
+
return {};
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
/** 写回全文件(2 空格缩进 + 尾换行,方便 MSM 直接读取) */
|
|
1475
|
+
function writeAll(root, data) {
|
|
1476
|
+
writeFileSync(localstorePath(root), JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
1477
|
+
}
|
|
1478
|
+
/** 读取命名空间全部条目:credential → 扁平;config → 分节(剔除 credentials 保留节) */
|
|
1479
|
+
function readStore(root, scope) {
|
|
1480
|
+
const all = readAll(root);
|
|
1481
|
+
if (scope === "credential") return all["credentials"] ?? {};
|
|
1482
|
+
const { [CREDENTIALS_SECTION]: _cred, ...rest } = all;
|
|
1483
|
+
return rest;
|
|
1484
|
+
}
|
|
1485
|
+
/** 校验凭据 key 合法(大写蛇形);不合法抛错 */
|
|
1486
|
+
function assertCredentialKey(key) {
|
|
1487
|
+
if (!CREDENTIAL_KEY_RE.test(key)) throw new Error(`credential key "${key}" 必须匹配大写蛇形 ^[A-Z][A-Z0-9_]*$(如 HOME_GITLAB_TOKEN)`);
|
|
1488
|
+
}
|
|
1489
|
+
/** 校验 config 路径(节.key);不合法抛错 */
|
|
1490
|
+
function assertConfigPath(path) {
|
|
1491
|
+
const idx = path.indexOf(".");
|
|
1492
|
+
if (idx <= 0 || idx === path.length - 1) throw new Error(`config path "${path}" 必须为 section.key(如 loop.defaultModel)`);
|
|
1493
|
+
const section = path.slice(0, idx);
|
|
1494
|
+
const key = path.slice(idx + 1);
|
|
1495
|
+
if (!CONFIG_SECTION_RE.test(section)) throw new Error(`config 节 "${section}" 必须匹配 ^[a-z][a-z0-9-]*$`);
|
|
1496
|
+
if (!CONFIG_KEY_RE.test(key)) throw new Error(`config key "${key}" 必须匹配小驼峰 ^[a-z][a-zA-Z0-9_]*$(如 defaultModel)`);
|
|
1497
|
+
return {
|
|
1498
|
+
section,
|
|
1499
|
+
key
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
/** 写入单个条目(自动建文件;deny 默认时同步确保 .gitignore 物理保证) */
|
|
1503
|
+
function writeEntry(root, scope, name, value) {
|
|
1504
|
+
const all = readAll(root);
|
|
1505
|
+
if (scope === "credential") {
|
|
1506
|
+
assertCredentialKey(name);
|
|
1507
|
+
all[CREDENTIALS_SECTION] ??= {};
|
|
1508
|
+
all[CREDENTIALS_SECTION][name] = value;
|
|
1509
|
+
} else {
|
|
1510
|
+
const { section, key } = assertConfigPath(name);
|
|
1511
|
+
all[section] ??= {};
|
|
1512
|
+
all[section][key] = value;
|
|
1513
|
+
}
|
|
1514
|
+
writeAll(root, all);
|
|
1515
|
+
ensureLocalstoreGitignored(root);
|
|
1516
|
+
}
|
|
1517
|
+
/** 删除单个条目(不存在返回 false);空节自动移除 */
|
|
1518
|
+
function unsetEntry(root, scope, name) {
|
|
1519
|
+
const all = readAll(root);
|
|
1520
|
+
if (scope === "credential") {
|
|
1521
|
+
assertCredentialKey(name);
|
|
1522
|
+
const sec = all[CREDENTIALS_SECTION];
|
|
1523
|
+
if (!sec || !(name in sec)) return false;
|
|
1524
|
+
delete sec[name];
|
|
1525
|
+
if (Object.keys(sec).length === 0) delete all[CREDENTIALS_SECTION];
|
|
1526
|
+
} else {
|
|
1527
|
+
const { section, key } = assertConfigPath(name);
|
|
1528
|
+
const sec = all[section];
|
|
1529
|
+
if (!sec || !(key in sec)) return false;
|
|
1530
|
+
delete sec[key];
|
|
1531
|
+
if (Object.keys(sec).length === 0) delete all[section];
|
|
1532
|
+
}
|
|
1533
|
+
writeAll(root, all);
|
|
1534
|
+
return true;
|
|
1535
|
+
}
|
|
1536
|
+
/** 读取单个条目值;不存在返回 null */
|
|
1537
|
+
function getEntry(root, scope, name) {
|
|
1538
|
+
const all = readAll(root);
|
|
1539
|
+
if (scope === "credential") {
|
|
1540
|
+
assertCredentialKey(name);
|
|
1541
|
+
return all["credentials"]?.[name] ?? null;
|
|
1542
|
+
}
|
|
1543
|
+
const { section, key } = assertConfigPath(name);
|
|
1544
|
+
return all[section]?.[key] ?? null;
|
|
1545
|
+
}
|
|
1546
|
+
/** 列出 key(凭据只返回 key 名,不返回值) */
|
|
1547
|
+
function listKeys(root, scope) {
|
|
1548
|
+
const data = readStore(root, scope);
|
|
1549
|
+
if (scope === "credential") return Object.keys(data);
|
|
1550
|
+
return Object.entries(data).flatMap(([section, entries]) => Object.keys(entries).map((key) => `${section}.${key}`));
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* doc 说明文本:输出存储位置/格式/key 规范/git 策略/读写方法。
|
|
1554
|
+
* agent 据此可直接用 fs 工具(read/write)自己读写凭据/配置。
|
|
1555
|
+
*/
|
|
1556
|
+
function docText(root) {
|
|
1557
|
+
const path = localstorePath(root);
|
|
1558
|
+
return [
|
|
1559
|
+
"# localstore — ACC 本地凭据/配置存储(标准,S134 重设计)",
|
|
1560
|
+
"",
|
|
1561
|
+
"一个工具管理两个命名空间:credential(凭据)+ config(偏好)。",
|
|
1562
|
+
`存储于 CCC 根根目录 ${path}(JSON 格式,MSM 可直接 read + JSON.parse)。`,
|
|
1563
|
+
"",
|
|
1564
|
+
"## git 提交策略",
|
|
1565
|
+
"- 配置:.opencode/serenity.json `localstore.gitTrack`:`\"allow\"`(可提交)| `\"deny\"`(禁提交;.dsh 回退)",
|
|
1566
|
+
"- **缺省 deny(没配就是不提交)**;deny 时写入会自动确保 .gitignore 含 localstore.json",
|
|
1567
|
+
" (物理保证,不依赖 dsh 运行);cc_git commit 会检查拒绝",
|
|
1568
|
+
"- 想提交:配置 `\"localstore\": { \"gitTrack\": \"allow\" }`,并从 .gitignore 移除 localstore.json",
|
|
1569
|
+
"",
|
|
1570
|
+
"## 格式(JSON 顶层分节)",
|
|
1571
|
+
"```json",
|
|
1572
|
+
"{",
|
|
1573
|
+
" \"credentials\": {",
|
|
1574
|
+
" \"HOME_GITLAB_TOKEN\": \"xxx\"",
|
|
1575
|
+
" },",
|
|
1576
|
+
" \"loop\": {",
|
|
1577
|
+
" \"defaultModel\": \"minimax-cn-coding-plan/MiniMax-M3\"",
|
|
1578
|
+
" }",
|
|
1579
|
+
"}",
|
|
1580
|
+
"```",
|
|
1581
|
+
"- credentials 为保留节(凭据,key 大写蛇形);其余节 = config(path = section.key)",
|
|
1582
|
+
"",
|
|
1583
|
+
"## key 规范",
|
|
1584
|
+
"- credential key: ^[A-Z][A-Z0-9_]*$(如 HOME_GITLAB_TOKEN)",
|
|
1585
|
+
"- config path: section.key(节 ^[a-z][a-z0-9-]*$,key 小驼峰 ^[a-z][a-zA-Z0-9_]*$,如 loop.defaultModel)",
|
|
1586
|
+
"",
|
|
1587
|
+
"## 读取方法(agent 可直接用 fs 工具)",
|
|
1588
|
+
`- 用 read 工具读 ${path} → JSON.parse 解析`,
|
|
1589
|
+
"- 或用 localstore get <name> [--scope credential|config]",
|
|
1590
|
+
"",
|
|
1591
|
+
"## 写入方法",
|
|
1592
|
+
"- 推荐:localstore set <name> <value> [--scope ...](自动建文件/保留其他条目/联动 .gitignore)",
|
|
1593
|
+
"- 或直接用 write/edit 工具修改文件,保持 JSON 合法",
|
|
1594
|
+
"",
|
|
1595
|
+
"## 安全边界",
|
|
1596
|
+
"- list/show 对 credential 只返回 key 名,不返回值",
|
|
1597
|
+
"- 凭据值应在 agent 内部使用,不要写入对话/日志",
|
|
1598
|
+
"- 默认 deny:文件不会提交 git(.gitignore 物理保证 + cc_git 检查兜底)",
|
|
1599
|
+
""
|
|
1600
|
+
].join("\n");
|
|
1601
|
+
}
|
|
1602
|
+
/** 运行 localstore 操作(纯逻辑;返回 JSON 值供工具 render) */
|
|
1603
|
+
function runLocalStore(root, args) {
|
|
1604
|
+
const scope = args.scope === "config" ? "config" : "credential";
|
|
1605
|
+
switch (args.action) {
|
|
1606
|
+
case "list": return {
|
|
1607
|
+
scope,
|
|
1608
|
+
keys: listKeys(root, scope)
|
|
1609
|
+
};
|
|
1610
|
+
case "get": {
|
|
1611
|
+
if (!args.name) throw new Error("get 需要 name");
|
|
1612
|
+
const value = getEntry(root, scope, args.name);
|
|
1613
|
+
if (value === null) throw new Error(`not found: ${args.name}(scope=${scope})`);
|
|
1614
|
+
return {
|
|
1615
|
+
scope,
|
|
1616
|
+
name: args.name,
|
|
1617
|
+
value,
|
|
1618
|
+
source: scope
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
case "set": {
|
|
1622
|
+
if (!args.name) throw new Error("set 需要 name");
|
|
1623
|
+
if (args.value === void 0) throw new Error("set 需要 value");
|
|
1624
|
+
writeEntry(root, scope, args.name, args.value);
|
|
1625
|
+
const git = checkLocalstoreGitCompliance(root);
|
|
1626
|
+
return {
|
|
1627
|
+
scope,
|
|
1628
|
+
name: args.name,
|
|
1629
|
+
set: true,
|
|
1630
|
+
path: localstorePath(root),
|
|
1631
|
+
gitTrack: readGitTrack(root),
|
|
1632
|
+
gitOk: git.ok,
|
|
1633
|
+
git: git.reason ? { warning: git.reason } : null
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
case "unset": {
|
|
1637
|
+
if (!args.name) throw new Error("unset 需要 name");
|
|
1638
|
+
const removed = unsetEntry(root, scope, args.name);
|
|
1639
|
+
return {
|
|
1640
|
+
scope,
|
|
1641
|
+
name: args.name,
|
|
1642
|
+
removed
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
case "show": {
|
|
1646
|
+
if (!args.name) throw new Error("show 需要 name");
|
|
1647
|
+
const exists = getEntry(root, scope, args.name) !== null;
|
|
1648
|
+
return {
|
|
1649
|
+
scope,
|
|
1650
|
+
name: args.name,
|
|
1651
|
+
exists,
|
|
1652
|
+
path: localstorePath(root)
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
case "doc": return { doc: docText(root) };
|
|
1656
|
+
default: throw new Error(`未知子命令: ${args.action}(可用 list/get/set/unset/show/doc)`);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
//#endregion
|
|
1384
1660
|
//#region src/git-ops.ts
|
|
1385
1661
|
/**
|
|
1386
1662
|
* git-ops.ts — cc_git 纯操作层(零 DSH 依赖)
|
|
@@ -1426,12 +1702,17 @@ function runGit(root, args) {
|
|
|
1426
1702
|
"--porcelain"
|
|
1427
1703
|
]);
|
|
1428
1704
|
if (!r.ok) throw new Error(`status 失败:${r.stderr.trim()}`);
|
|
1429
|
-
|
|
1705
|
+
const out = {
|
|
1430
1706
|
clean: r.stdout.trim() === "",
|
|
1431
1707
|
entries: r.stdout.trim() ? r.stdout.trim().split("\n") : []
|
|
1432
1708
|
};
|
|
1709
|
+
const ls = checkLocalstoreGitCompliance(root);
|
|
1710
|
+
if (!ls.ok) out.warning = ls.reason;
|
|
1711
|
+
return out;
|
|
1433
1712
|
}
|
|
1434
1713
|
case "commit": {
|
|
1714
|
+
const ls = checkLocalstoreGitCompliance(root);
|
|
1715
|
+
if (!ls.ok) throw new Error(ls.reason);
|
|
1435
1716
|
if (!args.message) throw new Error("commit 需要 message");
|
|
1436
1717
|
const add = git(root, ["add", "-A"]);
|
|
1437
1718
|
if (!add.ok) throw new Error(`git add 失败:${add.stderr.trim()}`);
|
|
@@ -1489,7 +1770,7 @@ function runGit(root, args) {
|
|
|
1489
1770
|
/**
|
|
1490
1771
|
* git.ts — cc_git 真实 DSH 工具定义(defineTool)
|
|
1491
1772
|
*/
|
|
1492
|
-
function agentCwd$
|
|
1773
|
+
function agentCwd$4(exec) {
|
|
1493
1774
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
1494
1775
|
}
|
|
1495
1776
|
function renderText$6(value) {
|
|
@@ -1522,7 +1803,7 @@ const gitTool = defineTool({
|
|
|
1522
1803
|
render: (args, value) => renderText$6(value)
|
|
1523
1804
|
},
|
|
1524
1805
|
async execute(args, exec) {
|
|
1525
|
-
const root = findSerenityRoot(agentCwd$
|
|
1806
|
+
const root = findSerenityRoot(agentCwd$4(exec));
|
|
1526
1807
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
1527
1808
|
return runGit(root, args);
|
|
1528
1809
|
}
|
|
@@ -1532,7 +1813,7 @@ const gitTool = defineTool({
|
|
|
1532
1813
|
/**
|
|
1533
1814
|
* msm.ts — acc_msm 真实 DSH 工具定义(defineTool)
|
|
1534
1815
|
*/
|
|
1535
|
-
function agentCwd$
|
|
1816
|
+
function agentCwd$3(exec) {
|
|
1536
1817
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
1537
1818
|
}
|
|
1538
1819
|
function renderText$5(value) {
|
|
@@ -1582,7 +1863,7 @@ const msmTool = defineTool({
|
|
|
1582
1863
|
render: (args, value) => renderText$5(value)
|
|
1583
1864
|
},
|
|
1584
1865
|
async execute(args, exec) {
|
|
1585
|
-
const root = findSerenityRoot(agentCwd$
|
|
1866
|
+
const root = findSerenityRoot(agentCwd$3(exec));
|
|
1586
1867
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
1587
1868
|
return runMsmAsync(root, args);
|
|
1588
1869
|
}
|
|
@@ -1926,7 +2207,7 @@ ${resumeNote}
|
|
|
1926
2207
|
* 使 loop agent 继承发起方会话的 agent preset 工具(read/write/edit 等 preset 层工具)。
|
|
1927
2208
|
* agentPresets 是可选服务——无 preset 装配的环境(无 roster 部署)退化为空工具层(历史行为)。
|
|
1928
2209
|
*/
|
|
1929
|
-
function agentCwd$
|
|
2210
|
+
function agentCwd$2(exec) {
|
|
1930
2211
|
return (exec.agent?.session)?.header?.cwd ?? process.cwd();
|
|
1931
2212
|
}
|
|
1932
2213
|
function renderText$1(value) {
|
|
@@ -1968,7 +2249,7 @@ function lastAssistantText(agent) {
|
|
|
1968
2249
|
function createLoopTool(ctx) {
|
|
1969
2250
|
return defineTool({
|
|
1970
2251
|
name: "loop",
|
|
1971
|
-
description: "牛马循环(老 loop 等效):用指定模型创建专用 agent 反复执行任务直到完成。\n用法:loop 接受 task(要完成的目标)或依赖 session 上下文;模型缺省读 .
|
|
2252
|
+
description: "牛马循环(老 loop 等效):用指定模型创建专用 agent 反复执行任务直到完成。\n用法:loop 接受 task(要完成的目标)或依赖 session 上下文;模型缺省读 .opencode/serenity.json 的 loop.defaultModel(当前 minimax-cn-coding-plan/MiniMax-M3,廉价牛马)。\n行为:内部硬性 while 循环驱动 agent 逐轮推进任务,每轮等待无超时(agent 工作多久等多久)。唯一完成条件 = agent 精确回显本轮随机完成码(stop token),防止低智能模型提前结束。轮次不需要调用者指定——对话轮次**无上限**(不完成不返回),agent 非正常停止时自动重启(重启 ≤100 次,防死循环保险阀)。\n进度:写入 AGENT_SESSIONS/loop-<label>.md/.json;同 label 再次调用从上次轮次续跑(不重做)。\n约束:loop agent 受完整 Serenity 约束(ACC 身份/入口技能系统提示词/守卫/session-keeper)。\n示例:loop 执行「扫描 SQC 并修复 DC 问题」,label: sqc-scan",
|
|
1972
2253
|
parameters: {
|
|
1973
2254
|
task: {
|
|
1974
2255
|
type: "string",
|
|
@@ -1993,11 +2274,11 @@ function createLoopTool(ctx) {
|
|
|
1993
2274
|
render: (_args, value) => renderText$1(value)
|
|
1994
2275
|
},
|
|
1995
2276
|
async execute(args, exec) {
|
|
1996
|
-
const root = findSerenityRoot(agentCwd$
|
|
2277
|
+
const root = findSerenityRoot(agentCwd$2(exec));
|
|
1997
2278
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
1998
2279
|
const cfg = loadSerenityConfig(root, DEFAULT_SERENITY_CONFIG_PATHS);
|
|
1999
2280
|
const model = args.model ?? cfg.loop?.defaultModel;
|
|
2000
|
-
if (!model) throw new Error("loop 需要 model:传参或配置 .
|
|
2281
|
+
if (!model) throw new Error("loop 需要 model:传参或配置 .opencode/serenity.json loop.defaultModel");
|
|
2001
2282
|
const label = args.label;
|
|
2002
2283
|
if (!ctx.agentLoop) throw new Error("loop: ctx.agentLoop 不可用");
|
|
2003
2284
|
const { provider, model: modelName } = splitModel(model);
|
|
@@ -2109,307 +2390,13 @@ function createLoopTool(ctx) {
|
|
|
2109
2390
|
});
|
|
2110
2391
|
}
|
|
2111
2392
|
//#endregion
|
|
2112
|
-
//#region src/localstore-ops.ts
|
|
2113
|
-
/**
|
|
2114
|
-
* localstore-ops.ts — localstore 纯逻辑层(零 DSH 依赖,可独立单测)
|
|
2115
|
-
*
|
|
2116
|
-
* ACC 标准本地凭据/配置存储(S133 设计):
|
|
2117
|
-
* - 一个工具管理两个命名空间:credential(凭据,0600)+ config(偏好,0644)
|
|
2118
|
-
* - 存储于 ~/.serenity/(平台感知:win %USERPROFILE%\.serenity\)
|
|
2119
|
-
* - 两个 YAML 文件:credentials.yaml(扁平 REF→value)+ settings.yaml(命名空间分节)
|
|
2120
|
-
* - 目录 0700
|
|
2121
|
-
*
|
|
2122
|
-
* YAML 用轻量自实现子集(零依赖):扁平 `KEY: value` 映射 + `#` 注释。
|
|
2123
|
-
* 凭据/配置本质是给 MSM/agent 用的普通 YAML 文件——agent 可按 doc 子命令
|
|
2124
|
-
* 说明直接用 fs 工具(read/write)操作;本工具是管理入口 + 规范文档。
|
|
2125
|
-
*/
|
|
2126
|
-
const LOCALSTORE_SCOPES = ["credential", "config"];
|
|
2127
|
-
/** 根目录名(~/.serenity) */
|
|
2128
|
-
const STORE_DIR_NAME = ".serenity";
|
|
2129
|
-
/** 凭据文件(0600) */
|
|
2130
|
-
const CREDENTIALS_FILENAME = "credentials.yaml";
|
|
2131
|
-
/** 配置文件(0644) */
|
|
2132
|
-
const SETTINGS_FILENAME = "settings.yaml";
|
|
2133
|
-
/** 凭据 key 规范:大写蛇形(命名空间前缀),如 HOME_GITLAB_TOKEN */
|
|
2134
|
-
const CREDENTIAL_KEY_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
2135
|
-
/** 配置节名规范:小写字母数字连字符 */
|
|
2136
|
-
const CONFIG_SECTION_RE = /^[a-z][a-z0-9-]*$/;
|
|
2137
|
-
/** 配置 key 规范(节内):小驼峰(首字母小写,可含大写),如 defaultModel */
|
|
2138
|
-
const CONFIG_KEY_RE = /^[a-z][a-zA-Z0-9_]*$/;
|
|
2139
|
-
/** 解析 ~/.serenity 根(平台感知:os.homedir() 三平台统一) */
|
|
2140
|
-
function serenityDir() {
|
|
2141
|
-
return join(homedir(), STORE_DIR_NAME);
|
|
2142
|
-
}
|
|
2143
|
-
/** 各命名空间的文件绝对路径 */
|
|
2144
|
-
function storeFilePath(scope) {
|
|
2145
|
-
return scope === "credential" ? join(serenityDir(), CREDENTIALS_FILENAME) : join(serenityDir(), SETTINGS_FILENAME);
|
|
2146
|
-
}
|
|
2147
|
-
/** 确保目录存在并设 0700 */
|
|
2148
|
-
function ensureDir(dir) {
|
|
2149
|
-
mkdirSync(dir, { recursive: true });
|
|
2150
|
-
try {
|
|
2151
|
-
chmodSync(dir, 448);
|
|
2152
|
-
} catch {}
|
|
2153
|
-
}
|
|
2154
|
-
/** 设置文件权限:credential 0600 / config 0644 */
|
|
2155
|
-
function applyFileMode(scope, path) {
|
|
2156
|
-
try {
|
|
2157
|
-
chmodSync(path, scope === "credential" ? 384 : 420);
|
|
2158
|
-
} catch {}
|
|
2159
|
-
}
|
|
2160
|
-
/** 解析扁平映射 YAML → Record<string,string>(忽略 # 注释与空行;值去引号) */
|
|
2161
|
-
function parseFlatYaml(text) {
|
|
2162
|
-
const out = {};
|
|
2163
|
-
for (const rawLine of text.split("\n")) {
|
|
2164
|
-
const line = rawLine.trim();
|
|
2165
|
-
if (line === "" || line.startsWith("#")) continue;
|
|
2166
|
-
const idx = line.indexOf(":");
|
|
2167
|
-
if (idx <= 0) continue;
|
|
2168
|
-
const key = line.slice(0, idx).trim();
|
|
2169
|
-
let value = line.slice(idx + 1).trim();
|
|
2170
|
-
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
2171
|
-
out[key] = value;
|
|
2172
|
-
}
|
|
2173
|
-
return out;
|
|
2174
|
-
}
|
|
2175
|
-
/** 序列化扁平映射 YAML(保持 key 顺序,值引号包裹含特殊字符的) */
|
|
2176
|
-
function renderFlatYaml(entries) {
|
|
2177
|
-
const lines = [];
|
|
2178
|
-
for (const [key, value] of Object.entries(entries)) {
|
|
2179
|
-
const rendered = /[:#\n]/.test(value) || value === "" || /^\s/.test(value) || /\s$/.test(value) ? JSON.stringify(value) : value;
|
|
2180
|
-
lines.push(`${key}: ${rendered}`);
|
|
2181
|
-
}
|
|
2182
|
-
return lines.join("\n") + (lines.length > 0 ? "\n" : "");
|
|
2183
|
-
}
|
|
2184
|
-
/** 解析分节 YAML(config):节 → key → value */
|
|
2185
|
-
function parseSectionedYaml(text) {
|
|
2186
|
-
const out = {};
|
|
2187
|
-
let section = "";
|
|
2188
|
-
for (const rawLine of text.split("\n")) {
|
|
2189
|
-
const line = rawLine.trim();
|
|
2190
|
-
if (line === "" || line.startsWith("#")) continue;
|
|
2191
|
-
if (rawLine.length - rawLine.trimStart().length === 0 && line.endsWith(":")) {
|
|
2192
|
-
section = line.slice(0, -1).trim();
|
|
2193
|
-
if (!out[section]) out[section] = {};
|
|
2194
|
-
continue;
|
|
2195
|
-
}
|
|
2196
|
-
if (section === "") continue;
|
|
2197
|
-
const idx = line.indexOf(":");
|
|
2198
|
-
if (idx <= 0) continue;
|
|
2199
|
-
const key = line.slice(0, idx).trim();
|
|
2200
|
-
let value = line.slice(idx + 1).trim();
|
|
2201
|
-
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
2202
|
-
const sectionMap = out[section] ??= {};
|
|
2203
|
-
sectionMap[key] = value;
|
|
2204
|
-
}
|
|
2205
|
-
return out;
|
|
2206
|
-
}
|
|
2207
|
-
/** 序列化分节 YAML(config):节 → key → value */
|
|
2208
|
-
function renderSectionedYaml(sections) {
|
|
2209
|
-
const lines = [];
|
|
2210
|
-
for (const [section, entries] of Object.entries(sections)) {
|
|
2211
|
-
lines.push(`${section}:`);
|
|
2212
|
-
for (const [key, value] of Object.entries(entries)) {
|
|
2213
|
-
const rendered = /[:#\n]/.test(value) || value === "" || /^\s/.test(value) || /\s$/.test(value) ? JSON.stringify(value) : value;
|
|
2214
|
-
lines.push(` ${key}: ${rendered}`);
|
|
2215
|
-
}
|
|
2216
|
-
}
|
|
2217
|
-
return lines.join("\n") + (lines.length > 0 ? "\n" : "");
|
|
2218
|
-
}
|
|
2219
|
-
/** 读取命名空间全部条目(文件不存在返回空) */
|
|
2220
|
-
function readStore(scope) {
|
|
2221
|
-
const path = storeFilePath(scope);
|
|
2222
|
-
if (!existsSync(path)) return scope === "credential" ? {} : {};
|
|
2223
|
-
const text = readFileSync(path, "utf-8");
|
|
2224
|
-
return scope === "credential" ? parseFlatYaml(text) : parseSectionedYaml(text);
|
|
2225
|
-
}
|
|
2226
|
-
/** 校验凭据 key 合法(大写蛇形);不合法抛错 */
|
|
2227
|
-
function assertCredentialKey(key) {
|
|
2228
|
-
if (!CREDENTIAL_KEY_RE.test(key)) throw new Error(`credential key "${key}" 必须匹配大写蛇形 ^[A-Z][A-Z0-9_]*$(如 HOME_GITLAB_TOKEN)`);
|
|
2229
|
-
}
|
|
2230
|
-
/** 校验 config 路径(节.key);不合法抛错 */
|
|
2231
|
-
function assertConfigPath(path) {
|
|
2232
|
-
const idx = path.indexOf(".");
|
|
2233
|
-
if (idx <= 0 || idx === path.length - 1) throw new Error(`config path "${path}" 必须为 section.key(如 loop.defaultModel)`);
|
|
2234
|
-
const section = path.slice(0, idx);
|
|
2235
|
-
const key = path.slice(idx + 1);
|
|
2236
|
-
if (!CONFIG_SECTION_RE.test(section)) throw new Error(`config 节 "${section}" 必须匹配 ^[a-z][a-z0-9-]*$`);
|
|
2237
|
-
if (!CONFIG_KEY_RE.test(key)) throw new Error(`config key "${key}" 必须匹配小驼峰 ^[a-z][a-zA-Z0-9_]*$(如 defaultModel)`);
|
|
2238
|
-
return {
|
|
2239
|
-
section,
|
|
2240
|
-
key
|
|
2241
|
-
};
|
|
2242
|
-
}
|
|
2243
|
-
/** 写入单个条目(自动建目录/设权限;保留其他条目与注释行外内容) */
|
|
2244
|
-
function writeEntry(scope, name, value) {
|
|
2245
|
-
const path = storeFilePath(scope);
|
|
2246
|
-
ensureDir(serenityDir());
|
|
2247
|
-
if (scope === "credential") {
|
|
2248
|
-
assertCredentialKey(name);
|
|
2249
|
-
const entries = parseFlatYaml(existsSync(path) ? readFileSync(path, "utf-8") : "");
|
|
2250
|
-
entries[name] = value;
|
|
2251
|
-
writeFileSync(path, renderFlatYaml(entries), "utf-8");
|
|
2252
|
-
} else {
|
|
2253
|
-
const { section, key } = assertConfigPath(name);
|
|
2254
|
-
const sections = parseSectionedYaml(existsSync(path) ? readFileSync(path, "utf-8") : "");
|
|
2255
|
-
if (!sections[section]) sections[section] = {};
|
|
2256
|
-
sections[section][key] = value;
|
|
2257
|
-
writeFileSync(path, renderSectionedYaml(sections), "utf-8");
|
|
2258
|
-
}
|
|
2259
|
-
applyFileMode(scope, path);
|
|
2260
|
-
}
|
|
2261
|
-
/** 删除单个条目(不存在返回 false) */
|
|
2262
|
-
function unsetEntry(scope, name) {
|
|
2263
|
-
const path = storeFilePath(scope);
|
|
2264
|
-
if (!existsSync(path)) return false;
|
|
2265
|
-
if (scope === "credential") {
|
|
2266
|
-
assertCredentialKey(name);
|
|
2267
|
-
const entries = parseFlatYaml(readFileSync(path, "utf-8"));
|
|
2268
|
-
if (!(name in entries)) return false;
|
|
2269
|
-
delete entries[name];
|
|
2270
|
-
writeFileSync(path, renderFlatYaml(entries), "utf-8");
|
|
2271
|
-
return true;
|
|
2272
|
-
}
|
|
2273
|
-
const { section, key } = assertConfigPath(name);
|
|
2274
|
-
const sections = parseSectionedYaml(readFileSync(path, "utf-8"));
|
|
2275
|
-
const sectionMap = sections[section];
|
|
2276
|
-
if (!sectionMap || !(key in sectionMap)) return false;
|
|
2277
|
-
delete sectionMap[key];
|
|
2278
|
-
if (Object.keys(sectionMap).length === 0) delete sections[section];
|
|
2279
|
-
writeFileSync(path, renderSectionedYaml(sections), "utf-8");
|
|
2280
|
-
return true;
|
|
2281
|
-
}
|
|
2282
|
-
/** 读取单个条目值;不存在返回 null */
|
|
2283
|
-
function getEntry(scope, name) {
|
|
2284
|
-
const path = storeFilePath(scope);
|
|
2285
|
-
if (!existsSync(path)) return null;
|
|
2286
|
-
if (scope === "credential") {
|
|
2287
|
-
assertCredentialKey(name);
|
|
2288
|
-
return parseFlatYaml(readFileSync(path, "utf-8"))[name] ?? null;
|
|
2289
|
-
}
|
|
2290
|
-
const { section, key } = assertConfigPath(name);
|
|
2291
|
-
return parseSectionedYaml(readFileSync(path, "utf-8"))[section]?.[key] ?? null;
|
|
2292
|
-
}
|
|
2293
|
-
/** 列出 key(凭据只返回 key 名,不返回值) */
|
|
2294
|
-
function listKeys(scope) {
|
|
2295
|
-
const data = readStore(scope);
|
|
2296
|
-
if (scope === "credential") return Object.keys(data);
|
|
2297
|
-
return Object.entries(data).flatMap(([section, entries]) => Object.keys(entries).map((key) => `${section}.${key}`));
|
|
2298
|
-
}
|
|
2299
|
-
/**
|
|
2300
|
-
* doc 说明文本:输出存储位置/格式/key 规范/权限/读写方法/安全边界。
|
|
2301
|
-
* agent 据此可直接用 fs 工具(read/write)自己读写凭据/配置。
|
|
2302
|
-
*/
|
|
2303
|
-
function docText() {
|
|
2304
|
-
const dir = serenityDir();
|
|
2305
|
-
const credPath = storeFilePath("credential");
|
|
2306
|
-
const cfgPath = storeFilePath("config");
|
|
2307
|
-
return [
|
|
2308
|
-
"# localstore — ACC 本地凭据/配置存储(标准)",
|
|
2309
|
-
"",
|
|
2310
|
-
"一个工具管理两个命名空间:credential(凭据,0600)+ config(偏好,0644)。",
|
|
2311
|
-
"存储于用户主目录,不在任何 CCC git 仓库内。",
|
|
2312
|
-
"",
|
|
2313
|
-
"## 存储位置(平台感知)",
|
|
2314
|
-
`- 根目录: ${dir} (0700)`,
|
|
2315
|
-
`- 凭据: ${credPath} (0600)`,
|
|
2316
|
-
`- 配置: ${cfgPath} (0644)`,
|
|
2317
|
-
"- Windows: %USERPROFILE%\\.serenity\\...(与 Linux/macOS 的 $HOME/.serenity 同构)",
|
|
2318
|
-
"",
|
|
2319
|
-
"## 格式",
|
|
2320
|
-
"credentials.yaml(扁平映射,key = 大写蛇形命名空间前缀):",
|
|
2321
|
-
"```yaml",
|
|
2322
|
-
"HOME_GITLAB_TOKEN: xxx",
|
|
2323
|
-
"SSH_UBUNTU_PASSWORD: xxx",
|
|
2324
|
-
"ANYSEARCH_API_KEY: xxx",
|
|
2325
|
-
"```",
|
|
2326
|
-
"settings.yaml(命名空间分节):",
|
|
2327
|
-
"```yaml",
|
|
2328
|
-
"loop:",
|
|
2329
|
-
" defaultModel: minimax-cn-coding-plan/MiniMax-M3",
|
|
2330
|
-
"ui:",
|
|
2331
|
-
" theme: dark",
|
|
2332
|
-
"```",
|
|
2333
|
-
"",
|
|
2334
|
-
"## key 规范",
|
|
2335
|
-
"- credential key: ^[A-Z][A-Z0-9_]*$(如 HOME_GITLAB_TOKEN)",
|
|
2336
|
-
"- config path: section.key(节 ^[a-z][a-z0-9-]*$,key 小驼峰 ^[a-z][a-zA-Z0-9_]*$,如 loop.defaultModel)",
|
|
2337
|
-
"",
|
|
2338
|
-
"## 读取方法(agent 可直接用 fs 工具)",
|
|
2339
|
-
"- 用 read 工具读对应文件 → 按上面格式解析 YAML",
|
|
2340
|
-
"- 或用 localstore get <name> [--scope credential|config]",
|
|
2341
|
-
"",
|
|
2342
|
-
"## 写入方法",
|
|
2343
|
-
"- 推荐:localstore set <name> <value> [--scope ...](自动建目录/设权限/保留其他条目)",
|
|
2344
|
-
"- 或直接用 write/edit 工具修改文件,保持 YAML 合法(键值冒号分隔)",
|
|
2345
|
-
"",
|
|
2346
|
-
"## 安全边界",
|
|
2347
|
-
"- list/show 对 credential 只返回 key 名,不返回值",
|
|
2348
|
-
"- 凭据值应在 agent 内部使用,不要写入对话/日志",
|
|
2349
|
-
"- 文件权限不符(非 0600/0644)时 get/set 会提示 chmod 修复",
|
|
2350
|
-
"- 文件在用户主目录,天然不进入任何 git 仓库",
|
|
2351
|
-
""
|
|
2352
|
-
].join("\n");
|
|
2353
|
-
}
|
|
2354
|
-
/** 运行 localstore 操作(纯逻辑;返回 JSON 值供工具 render) */
|
|
2355
|
-
function runLocalStore(args) {
|
|
2356
|
-
const scope = args.scope === "config" ? "config" : "credential";
|
|
2357
|
-
switch (args.action) {
|
|
2358
|
-
case "list": return {
|
|
2359
|
-
scope,
|
|
2360
|
-
keys: listKeys(scope)
|
|
2361
|
-
};
|
|
2362
|
-
case "get": {
|
|
2363
|
-
if (!args.name) throw new Error("get 需要 name");
|
|
2364
|
-
const value = getEntry(scope, args.name);
|
|
2365
|
-
if (value === null) throw new Error(`not found: ${args.name}(scope=${scope})`);
|
|
2366
|
-
return {
|
|
2367
|
-
scope,
|
|
2368
|
-
name: args.name,
|
|
2369
|
-
value,
|
|
2370
|
-
source: scope
|
|
2371
|
-
};
|
|
2372
|
-
}
|
|
2373
|
-
case "set":
|
|
2374
|
-
if (!args.name) throw new Error("set 需要 name");
|
|
2375
|
-
if (args.value === void 0) throw new Error("set 需要 value");
|
|
2376
|
-
writeEntry(scope, args.name, args.value);
|
|
2377
|
-
return {
|
|
2378
|
-
scope,
|
|
2379
|
-
name: args.name,
|
|
2380
|
-
set: true
|
|
2381
|
-
};
|
|
2382
|
-
case "unset": {
|
|
2383
|
-
if (!args.name) throw new Error("unset 需要 name");
|
|
2384
|
-
const removed = unsetEntry(scope, args.name);
|
|
2385
|
-
return {
|
|
2386
|
-
scope,
|
|
2387
|
-
name: args.name,
|
|
2388
|
-
removed
|
|
2389
|
-
};
|
|
2390
|
-
}
|
|
2391
|
-
case "show": {
|
|
2392
|
-
if (!args.name) throw new Error("show 需要 name");
|
|
2393
|
-
const exists = getEntry(scope, args.name) !== null;
|
|
2394
|
-
return {
|
|
2395
|
-
scope,
|
|
2396
|
-
name: args.name,
|
|
2397
|
-
exists,
|
|
2398
|
-
path: storeFilePath(scope)
|
|
2399
|
-
};
|
|
2400
|
-
}
|
|
2401
|
-
case "doc": return { doc: docText() };
|
|
2402
|
-
default: throw new Error(`未知子命令: ${args.action}(可用 list/get/set/unset/show/doc)`);
|
|
2403
|
-
}
|
|
2404
|
-
}
|
|
2405
|
-
//#endregion
|
|
2406
2393
|
//#region src/tools/localstore.ts
|
|
2407
2394
|
/**
|
|
2408
2395
|
* localstore.ts — localstore 真实 DSH 工具定义(defineTool)
|
|
2409
2396
|
*
|
|
2410
|
-
* ACC 标准本地凭据/配置存储(S133
|
|
2411
|
-
* localstore-ops.ts(可单测)。
|
|
2412
|
-
* 工具(read/write)自己读写。
|
|
2397
|
+
* ACC 标准本地凭据/配置存储(S133 设计,S134 重设计:CCC 根 localstore.json)。
|
|
2398
|
+
* 进程内注册,零 DSH 依赖逻辑在 localstore-ops.ts(可单测)。
|
|
2399
|
+
* doc 子命令输出存储规范,agent 可直接用 fs 工具(read/write)自己读写。
|
|
2413
2400
|
*/
|
|
2414
2401
|
const ACTIONS = [
|
|
2415
2402
|
"list",
|
|
@@ -2419,6 +2406,9 @@ const ACTIONS = [
|
|
|
2419
2406
|
"show",
|
|
2420
2407
|
"doc"
|
|
2421
2408
|
];
|
|
2409
|
+
function agentCwd$1(exec) {
|
|
2410
|
+
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
2411
|
+
}
|
|
2422
2412
|
function renderText(value) {
|
|
2423
2413
|
return [{
|
|
2424
2414
|
type: "text",
|
|
@@ -2427,7 +2417,7 @@ function renderText(value) {
|
|
|
2427
2417
|
}
|
|
2428
2418
|
const localstoreTool = defineTool({
|
|
2429
2419
|
name: "localstore",
|
|
2430
|
-
description: "ACC 标准本地凭据/配置存储:一个工具管理两个命名空间 credential
|
|
2420
|
+
description: "ACC 标准本地凭据/配置存储:一个工具管理两个命名空间 credential(凭据)与 config(本地偏好)。存储于 CCC 根根目录 localstore.json(JSON 格式,MSM 可直接读取)。git 策略:.opencode/serenity.json localstore.gitTrack(allow 可提交 / deny 禁提交,缺省 deny;.dsh 回退)——deny 时写入自动确保 .gitignore 含该文件(物理保证),cc_git commit 会检查拒绝。子命令:list(列 key,凭据不返回值)/ get <name>(读值)/ set <name> <value>(写)/ unset <name>(删)/ show <name>(元数据,凭据不打印值)/ doc(输出存储规范——路径/格式/key 规范/git 策略,agent 可按说明直接用 read/write 操作文件)。默认 scope=credential;config 需传 --scope config(路径为 section.key,如 loop.defaultModel)。",
|
|
2431
2421
|
parameters: {
|
|
2432
2422
|
action: {
|
|
2433
2423
|
type: "string",
|
|
@@ -2453,8 +2443,10 @@ const localstoreTool = defineTool({
|
|
|
2453
2443
|
schema: { type: "json" },
|
|
2454
2444
|
render: (args, value) => renderText(value)
|
|
2455
2445
|
},
|
|
2456
|
-
async execute(args) {
|
|
2457
|
-
|
|
2446
|
+
async execute(args, exec) {
|
|
2447
|
+
const root = findSerenityRoot(agentCwd$1(exec));
|
|
2448
|
+
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
2449
|
+
return runLocalStore(root, args);
|
|
2458
2450
|
}
|
|
2459
2451
|
});
|
|
2460
2452
|
//#endregion
|
|
@@ -2668,7 +2660,9 @@ function accBlock(root) {
|
|
|
2668
2660
|
" neat — return the full Neat design collaboration protocol",
|
|
2669
2661
|
" cce — return the full Cognitive Continuity Engineering framework",
|
|
2670
2662
|
" loop — 牛马循环:指定模型专用 agent 反复执行",
|
|
2671
|
-
" localstore — ACC 本地凭据/配置存储(
|
|
2663
|
+
" localstore — ACC 本地凭据/配置存储(CCC 根 localstore.json,JSON 格式;git 策略 localstore.gitTrack 缺省 deny);doc 子命令输出规范",
|
|
2664
|
+
"",
|
|
2665
|
+
" ℹ️ CCC 内文件操作(read/write/edit/glob/grep 等)请使用相对 CCC 根的相对路径(如 AGENT_SESSIONS/2026-08-14--S134--x/SESSION.md);Root / SESSION.md path 等绝对路径仅作标识,不作工具入参",
|
|
2672
2666
|
"",
|
|
2673
2667
|
"The DSH platform tools remain available too (read/write/edit/glob/grep/web_search/ask_user_question/subagent/workflow/goal and more) — the ACC tools above are the serenity-native layer, not the only tools.",
|
|
2674
2668
|
"",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shgroup/dsh-serenity-hooks",
|
|
3
|
-
"version": "1.16.
|
|
3
|
+
"version": "1.16.8",
|
|
4
4
|
"description": "宁静号 ACC harness — Native Cordis 插件(DeepSeek Harness 运行时)。真实 DSH 工具注册(cc_fs/session/acc_msm 等 9 工具)+ 拦截缝机械约束(safe-mode/路径守卫/会话落盘)+ 系统提示词注入(ACC/CCE/Constraints/SKILL/Session 五块)。适配 DSH 公开版(deepseek-ai/deepseek-harness 0.1.0-rc)。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|