@shgroup/dsh-serenity-hooks 1.77.1 → 1.78.0
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/{ccc-roots-rwgzt_0F.js → ccc-roots-D3ty1C8P.js} +1 -1
- package/lib/index.js +456 -2564
- package/lib/{message-source-CYa8LbbM.js → message-source-BOG0piL8.js} +240 -4
- package/lib/{profile-DplLGGqo.js → profile-C7BuyZK7.js} +63 -5
- package/lib/{route-COOFLTpU.js → route-Dr0SFD8A.js} +2 -279
- package/lib/service-DDmHSglV.js +282 -0
- package/lib/session-BxDVrxPH.js +2496 -0
- package/lib/settings-Bb4OnEdI.js +145 -0
- package/lib/trajectory/core.d.ts +4 -1
- package/lib/trajectory/model-runtime.d.ts +97 -0
- package/lib/trajectory/policy/profile.d.ts +50 -5
- package/lib/trajectory/policy/rules.d.ts +17 -4
- package/lib/trajectory/resident.d.ts +57 -0
- package/package.json +1 -1
- package/lib/settings-f9WZXcgi.js +0 -368
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS } from "./ccc-CyYJL4F2.js";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
//#region src/tools/localstore/service.ts
|
|
5
|
+
/**
|
|
6
|
+
* tools/localstore/service.ts — localstore 纯逻辑层(零 DSH 依赖,可独立单测)
|
|
7
|
+
*
|
|
8
|
+
* S134 重设计(v1.16.7):存储从 ~/.serenity/ 迁到 **CCC 根根目录 localstore.json**
|
|
9
|
+
* (JSON 格式——方便 MSM 直接 read + JSON.parse 读取,零解析依赖)。
|
|
10
|
+
*
|
|
11
|
+
* git 提交策略(可靠机制 × 用户自由):
|
|
12
|
+
* - 配置:.opencode/serenity.json `localstore.gitTrack`: "allow"(可提交)| "deny"(禁提交)
|
|
13
|
+
* - **缺省 deny(没配就是不提交)**;且 deny 的保证**不依赖 dsh 运行**——
|
|
14
|
+
* 写入时自动确保 .gitignore 含 localstore.json(物理保证:即使 dsh 不在、
|
|
15
|
+
* 用户手动 git commit 也不会误提交),container_git 检查为第二道防线(拒绝 + 提示)
|
|
16
|
+
* - allow:放行(文件可提交,用户自行管理 .gitignore)
|
|
17
|
+
*
|
|
18
|
+
* 存储结构(JSON 顶层分节):
|
|
19
|
+
* { "credentials": { "HOME_GITLAB_TOKEN": "xxx" }, "<config节>": { "<key>": "v" } }
|
|
20
|
+
* credentials 为保留节(credential 命名空间,key 大写蛇形);其余节归 config 命名空间
|
|
21
|
+
* (path = section.key,如 cheap-worker.models)。
|
|
22
|
+
*/
|
|
23
|
+
const LOCALSTORE_SCOPES = ["credential", "config"];
|
|
24
|
+
/** 存储文件名(CCC 根根目录) */
|
|
25
|
+
const LOCALSTORE_FILENAME = "localstore.json";
|
|
26
|
+
/** 凭据保留节名(JSON 顶层) */
|
|
27
|
+
const CREDENTIALS_SECTION = "credentials";
|
|
28
|
+
/** 凭据 key 规范:大写蛇形,如 HOME_GITLAB_TOKEN */
|
|
29
|
+
const CREDENTIAL_KEY_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
30
|
+
/** 配置节名规范:小写字母数字连字符 */
|
|
31
|
+
const CONFIG_SECTION_RE = /^[a-z][a-z0-9-]*$/;
|
|
32
|
+
/** 配置 key 规范(节内):小驼峰,如 defaultModel */
|
|
33
|
+
const CONFIG_KEY_RE = /^[a-z][a-zA-Z0-9_]*$/;
|
|
34
|
+
/** 存储文件绝对路径(CCC 根根目录) */
|
|
35
|
+
function localstorePath(root) {
|
|
36
|
+
return join(root, LOCALSTORE_FILENAME);
|
|
37
|
+
}
|
|
38
|
+
/** git 策略:serenity.json localstore.gitTrack,缺省 deny(不提交) */
|
|
39
|
+
function readGitTrack(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
|
|
40
|
+
return loadSerenityConfig(root, paths).localstore?.gitTrack === "allow" ? "allow" : "deny";
|
|
41
|
+
}
|
|
42
|
+
/** .gitignore 是否已覆盖 localstore 文件(非空非注释行含文件名即算) */
|
|
43
|
+
function isLocalstoreGitignored(root) {
|
|
44
|
+
const gi = join(root, ".gitignore");
|
|
45
|
+
if (!existsSync(gi)) return false;
|
|
46
|
+
return readFileSync(gi, "utf-8").split("\n").some((l) => {
|
|
47
|
+
const t = l.trim();
|
|
48
|
+
return t !== "" && !t.startsWith("#") && t.includes("localstore.json");
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 确保 .gitignore 含 localstore 文件(deny 时的物理保证,不依赖 dsh 运行):
|
|
53
|
+
* 写入 localstore 时调用——deny(含缺省)且 .gitignore 未覆盖 → 自动追加一行。
|
|
54
|
+
* allow → 不写入(放行,文件可提交)。
|
|
55
|
+
*/
|
|
56
|
+
function ensureLocalstoreGitignored(root) {
|
|
57
|
+
if (readGitTrack(root) === "allow") return { status: "allow" };
|
|
58
|
+
if (isLocalstoreGitignored(root)) return { status: "ignored" };
|
|
59
|
+
const gi = join(root, ".gitignore");
|
|
60
|
+
const existing = existsSync(gi) ? readFileSync(gi, "utf-8") : "";
|
|
61
|
+
const line = `${LOCALSTORE_FILENAME} # ACC localstore (localstore.gitTrack defaults to deny — do not commit)`;
|
|
62
|
+
writeFileSync(gi, (existing.endsWith("\n") || existing === "" ? existing : existing + "\n") + line + "\n", "utf-8");
|
|
63
|
+
return { status: "appended" };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* container_git 联动检查(第二道防线):文件存在 && deny && .gitignore 未覆盖 → 不通过。
|
|
67
|
+
* 调用方(container_git commit)据此拒绝提交;status 可输出 warning。
|
|
68
|
+
*/
|
|
69
|
+
function checkLocalstoreGitCompliance(root) {
|
|
70
|
+
if (!existsSync(localstorePath(root))) return { ok: true };
|
|
71
|
+
if (readGitTrack(root) === "allow") return { ok: true };
|
|
72
|
+
if (isLocalstoreGitignored(root)) return { ok: true };
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
reason: `localstore.json must not be committed (localstore.gitTrack=deny default) but .gitignore does not cover it — add ${LOCALSTORE_FILENAME} to .gitignore (or set localstore.gitTrack=allow to explicitly permit)`
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** 已告警的坏 localstore 路径(进程级去重;F-07:凭据文件损坏不得静默) */
|
|
79
|
+
const warnedBrokenStores = /* @__PURE__ */ new Set();
|
|
80
|
+
/** 读取全文件(顶层分节);文件不存在 → 空;**坏 JSON → 空 + 响亮告警**(每路径一次) */
|
|
81
|
+
function readAll(root) {
|
|
82
|
+
const p = localstorePath(root);
|
|
83
|
+
if (!existsSync(p)) return {};
|
|
84
|
+
try {
|
|
85
|
+
const v = JSON.parse(readFileSync(p, "utf-8").replace(/^\uFEFF/, ""));
|
|
86
|
+
if (v && typeof v === "object" && !Array.isArray(v)) return v;
|
|
87
|
+
return {};
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (!warnedBrokenStores.has(p)) {
|
|
90
|
+
warnedBrokenStores.add(p);
|
|
91
|
+
console.warn(`[serenity-hooks] ✗ localstore 解析失败,已按空存储继续(其中凭据/配置对工具表现为"未设置",请修 JSON): ${p} — ${String(err?.message ?? err)}`);
|
|
92
|
+
}
|
|
93
|
+
return {};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** 写回全文件(2 空格缩进 + 尾换行,方便 MSM 直接读取) */
|
|
97
|
+
function writeAll(root, data) {
|
|
98
|
+
writeFileSync(localstorePath(root), JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
99
|
+
}
|
|
100
|
+
/** 读取命名空间全部条目:credential → 扁平;config → 分节(剔除 credentials 保留节) */
|
|
101
|
+
function readStore(root, scope) {
|
|
102
|
+
const all = readAll(root);
|
|
103
|
+
if (scope === "credential") return all[CREDENTIALS_SECTION] ?? {};
|
|
104
|
+
const { [CREDENTIALS_SECTION]: _cred, ...rest } = all;
|
|
105
|
+
return rest;
|
|
106
|
+
}
|
|
107
|
+
/** 校验凭据 key 合法(大写蛇形);不合法抛错 */
|
|
108
|
+
function assertCredentialKey(key) {
|
|
109
|
+
if (!CREDENTIAL_KEY_RE.test(key)) throw new Error(`credential key "${key}" must match UPPER_SNAKE ^[A-Z][A-Z0-9_]*$ (e.g. HOME_GITLAB_TOKEN)`);
|
|
110
|
+
}
|
|
111
|
+
/** 校验 config 路径(节.key);不合法抛错 */
|
|
112
|
+
function assertConfigPath(path) {
|
|
113
|
+
const idx = path.indexOf(".");
|
|
114
|
+
if (idx <= 0 || idx === path.length - 1) throw new Error(`config path "${path}" must be section.key (e.g. cheap-worker.models)`);
|
|
115
|
+
const section = path.slice(0, idx);
|
|
116
|
+
const key = path.slice(idx + 1);
|
|
117
|
+
if (!CONFIG_SECTION_RE.test(section)) throw new Error(`config section "${section}" must match ^[a-z][a-z0-9-]*$`);
|
|
118
|
+
if (!CONFIG_KEY_RE.test(key)) throw new Error(`config key "${key}" must match lowerCamel ^[a-z][a-zA-Z0-9_]*$ (e.g. defaultModel)`);
|
|
119
|
+
return {
|
|
120
|
+
section,
|
|
121
|
+
key
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** 写入单个条目(自动建文件;deny 默认时同步确保 .gitignore 物理保证) */
|
|
125
|
+
function writeEntry(root, scope, name, value) {
|
|
126
|
+
const all = readAll(root);
|
|
127
|
+
if (scope === "credential") {
|
|
128
|
+
assertCredentialKey(name);
|
|
129
|
+
all[CREDENTIALS_SECTION] ??= {};
|
|
130
|
+
all[CREDENTIALS_SECTION][name] = value;
|
|
131
|
+
} else {
|
|
132
|
+
const { section, key } = assertConfigPath(name);
|
|
133
|
+
all[section] ??= {};
|
|
134
|
+
all[section][key] = value;
|
|
135
|
+
}
|
|
136
|
+
writeAll(root, all);
|
|
137
|
+
ensureLocalstoreGitignored(root);
|
|
138
|
+
}
|
|
139
|
+
/** 删除单个条目(不存在返回 false);空节自动移除 */
|
|
140
|
+
function unsetEntry(root, scope, name) {
|
|
141
|
+
const all = readAll(root);
|
|
142
|
+
if (scope === "credential") {
|
|
143
|
+
assertCredentialKey(name);
|
|
144
|
+
const sec = all[CREDENTIALS_SECTION];
|
|
145
|
+
if (!sec || !(name in sec)) return false;
|
|
146
|
+
delete sec[name];
|
|
147
|
+
if (Object.keys(sec).length === 0) delete all[CREDENTIALS_SECTION];
|
|
148
|
+
} else {
|
|
149
|
+
const { section, key } = assertConfigPath(name);
|
|
150
|
+
const sec = all[section];
|
|
151
|
+
if (!sec || !(key in sec)) return false;
|
|
152
|
+
delete sec[key];
|
|
153
|
+
if (Object.keys(sec).length === 0) delete all[section];
|
|
154
|
+
}
|
|
155
|
+
writeAll(root, all);
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
/** 读取单个条目值;不存在返回 null */
|
|
159
|
+
function getEntry(root, scope, name) {
|
|
160
|
+
const all = readAll(root);
|
|
161
|
+
if (scope === "credential") {
|
|
162
|
+
assertCredentialKey(name);
|
|
163
|
+
return all[CREDENTIALS_SECTION]?.[name] ?? null;
|
|
164
|
+
}
|
|
165
|
+
const { section, key } = assertConfigPath(name);
|
|
166
|
+
return all[section]?.[key] ?? null;
|
|
167
|
+
}
|
|
168
|
+
/** 列出 key(凭据只返回 key 名,不返回值) */
|
|
169
|
+
function listKeys(root, scope) {
|
|
170
|
+
const data = readStore(root, scope);
|
|
171
|
+
if (scope === "credential") return Object.keys(data);
|
|
172
|
+
return Object.entries(data).flatMap(([section, entries]) => Object.keys(entries).map((key) => `${section}.${key}`));
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* doc 说明文本:输出存储位置/格式/key 规范/git 策略/读写方法。
|
|
176
|
+
* agent 据此可直接用 fs 工具(read/write)自己读写凭据/配置。
|
|
177
|
+
*/
|
|
178
|
+
function docText(root) {
|
|
179
|
+
const path = localstorePath(root);
|
|
180
|
+
return [
|
|
181
|
+
"# localstore — ACC local credential/config storage (standard, S134 redesign)",
|
|
182
|
+
"",
|
|
183
|
+
"One tool manages two namespaces: credential (credentials) + config (preferences).",
|
|
184
|
+
`Stored at the CCC root in ${path} (JSON format; MSMs can read + JSON.parse directly).`,
|
|
185
|
+
"",
|
|
186
|
+
"## Git commit policy",
|
|
187
|
+
"- Config: .opencode/serenity.json `localstore.gitTrack`: `\"allow\"` (may commit) | `\"deny\"` (must not commit; .dsh fallback)",
|
|
188
|
+
"- **Default deny (unset = not committed)**; when deny, writes automatically ensure .gitignore contains localstore.json",
|
|
189
|
+
" (physical guarantee, independent of dsh runtime); container_git commit checks and refuses",
|
|
190
|
+
"- To commit: set `\"localstore\": { \"gitTrack\": \"allow\" }` and remove localstore.json from .gitignore",
|
|
191
|
+
"",
|
|
192
|
+
"## Format (JSON top-level sections)",
|
|
193
|
+
"```json",
|
|
194
|
+
"{",
|
|
195
|
+
" \"credentials\": {",
|
|
196
|
+
" \"HOME_GITLAB_TOKEN\": \"xxx\"",
|
|
197
|
+
" },",
|
|
198
|
+
" \"cheap-worker\": {",
|
|
199
|
+
" \"models\": [\"minimax-cn-coding-plan/MiniMax-M3\"]",
|
|
200
|
+
" }",
|
|
201
|
+
"}",
|
|
202
|
+
"```",
|
|
203
|
+
"- credentials is a reserved section (credentials, UPPER_SNAKE keys); other sections = config (path = section.key)",
|
|
204
|
+
"",
|
|
205
|
+
"## Key conventions",
|
|
206
|
+
"- credential key: ^[A-Z][A-Z0-9_]*$ (e.g. HOME_GITLAB_TOKEN)",
|
|
207
|
+
"- config path: section.key (section ^[a-z][a-z0-9-]*$, key lowerCamel ^[a-z][a-zA-Z0-9_]*$, e.g. cheap-worker.models)",
|
|
208
|
+
"",
|
|
209
|
+
"## Reading (agents may use fs tools directly)",
|
|
210
|
+
`- Read ${path} with the read tool → JSON.parse`,
|
|
211
|
+
"- Or use localstore get <name> [--scope credential|config]",
|
|
212
|
+
"",
|
|
213
|
+
"## Writing",
|
|
214
|
+
"- Recommended: localstore set <name> <value> [--scope ...] (auto-creates file / preserves other entries / syncs .gitignore)",
|
|
215
|
+
"- Or modify the file directly with write/edit, keeping the JSON valid",
|
|
216
|
+
"",
|
|
217
|
+
"## Security boundary",
|
|
218
|
+
"- list/show return only key names for credentials, never values",
|
|
219
|
+
"- Credential values should be used internally by the agent; never write them into conversation or logs",
|
|
220
|
+
"- Default deny: the file is not committed to git (.gitignore physical guarantee + container_git check fallback)",
|
|
221
|
+
""
|
|
222
|
+
].join("\n");
|
|
223
|
+
}
|
|
224
|
+
/** 运行 localstore 操作(纯逻辑;返回 JSON 值供工具 render) */
|
|
225
|
+
function runLocalStore(root, args) {
|
|
226
|
+
const scope = args.scope === "config" ? "config" : "credential";
|
|
227
|
+
switch (args.action) {
|
|
228
|
+
case "list": return {
|
|
229
|
+
scope,
|
|
230
|
+
keys: listKeys(root, scope)
|
|
231
|
+
};
|
|
232
|
+
case "get": {
|
|
233
|
+
if (!args.name) throw new Error("get requires name");
|
|
234
|
+
const value = getEntry(root, scope, args.name);
|
|
235
|
+
if (value === null) throw new Error(`not found: ${args.name} (scope=${scope})`);
|
|
236
|
+
return {
|
|
237
|
+
scope,
|
|
238
|
+
name: args.name,
|
|
239
|
+
value,
|
|
240
|
+
source: scope
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
case "set": {
|
|
244
|
+
if (!args.name) throw new Error("set requires name");
|
|
245
|
+
if (args.value === void 0) throw new Error("set requires value");
|
|
246
|
+
writeEntry(root, scope, args.name, args.value);
|
|
247
|
+
const git = checkLocalstoreGitCompliance(root);
|
|
248
|
+
return {
|
|
249
|
+
scope,
|
|
250
|
+
name: args.name,
|
|
251
|
+
set: true,
|
|
252
|
+
path: localstorePath(root),
|
|
253
|
+
gitTrack: readGitTrack(root),
|
|
254
|
+
gitOk: git.ok,
|
|
255
|
+
git: git.reason ? { warning: git.reason } : null
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
case "unset": {
|
|
259
|
+
if (!args.name) throw new Error("unset requires name");
|
|
260
|
+
const removed = unsetEntry(root, scope, args.name);
|
|
261
|
+
return {
|
|
262
|
+
scope,
|
|
263
|
+
name: args.name,
|
|
264
|
+
removed
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
case "show": {
|
|
268
|
+
if (!args.name) throw new Error("show requires name");
|
|
269
|
+
const exists = getEntry(root, scope, args.name) !== null;
|
|
270
|
+
return {
|
|
271
|
+
scope,
|
|
272
|
+
name: args.name,
|
|
273
|
+
exists,
|
|
274
|
+
path: localstorePath(root)
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
case "doc": return { doc: docText(root) };
|
|
278
|
+
default: throw new Error(`Unknown subcommand: ${args.action} (available: list/get/set/unset/show/doc)`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
//#endregion
|
|
282
|
+
export { readGitTrack as a, unsetEntry as c, localstorePath as i, writeEntry as l, checkLocalstoreGitCompliance as n, readStore as o, getEntry as r, runLocalStore as s, LOCALSTORE_SCOPES as t };
|