dsh-perm-gate 5.1.0 → 5.1.2-compat.10

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/CHANGELOG.md CHANGED
@@ -9,6 +9,19 @@ undefined
9
9
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
10
10
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
11
11
 
12
+ ## [5.1.2-compat.9] - 2026-10-08
13
+
14
+ ### Changed
15
+
16
+ - **回退家族 tab 收拢(用户定案:自动审查门不在 tab 化范围内)**:顶级
17
+ `settings.section`(侧栏「自动审查门」独立节,order 30)恢复为唯一设置导航
18
+ 入口;同日上午加入的 `dsh-family.tab`(order 45)撤除。
19
+ - `settings.plugins.tab`(插件页 tab,0.1.0 实测与独立节双渲染)与
20
+ `settings.plugin.item`(≤0.1.5 派发卡)维持退役——与独立节重复;
21
+ `plugins.bundle.config` 详情卡与 `shell.overlay` 浮窗齿轮不变。
22
+ - 席位契约测试(test/client-settings-seat.spec.ts)改钉单节形态:
23
+ settings.section 恰一次(order 30 / PermissiveCard),三个退役席位断言缺席。
24
+
12
25
  ## [2.6.0] - 2026-09-17
13
26
 
14
27
  ### Added
package/dsh.plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "dsh-perm-gate",
3
3
  "name": "dsh-perm-gate",
4
- "version": "5.1.0",
4
+ "version": "5.1.2-compat.9",
5
5
  "description": {
6
6
  "en": "Deterministic-first, fail-closed permission gate for DeepSeek Harness 0.1.2+: a P0 hard-deny -> P1 precise session grant -> P2 static allow/deny rule chain -> P3 optional LLM classifier -> P4 ask pipeline over the tools/pre-execute waterfall, with a standalone CLI dry-run evaluator and an auditable decision log. It only vetoes with deny/ask and always delegates otherwise, and unknown or malformed rules fail loud instead of silently standing the gate down.",
7
7
  "zh": "面向 DeepSeek Harness 0.1.2+ 的确定性优先、失败即拒的权限闸门:在 tools/pre-execute 瀑布上串起 P0 硬拒绝 → P1 精确会话授权 → P2 静态 allow/deny 规则链 → P3 可选 LLM 语义分类 → P4 ask,并附带独立 CLI 干跑评估器与可审计的决策日志。只以 deny/ask 否决、其余一律放行,规则未知或非法时直接报错,绝不静默失效。"
@@ -0,0 +1,41 @@
1
+ /**
2
+ * settings bridge — T12(spec webServer 数据桥规范):describe/mutate 两端点。
3
+ *
4
+ * 桥只是 HTTP 皮:读 = svc.describe 取自家 ns 描述符;写 = svc.mutate 转 ops 落
5
+ * 宿主 settings 服务(不落盘不缓存第二份,天然规避双写/锁竞争)。mutate 成功即
6
+ * describe 回填,带 expectedRevision 乐观并发。守卫:POST-only + ns 硬编码自家
7
+ * allowlist + 秘密不回读(redactSecrets)。
8
+ *
9
+ * 数据面消费方 = client BridgeDocHandle(T13b 双轨数据源:0.1.7+ 原生句柄优先,
10
+ * ≤0.1.5 settingsScope 不解析时落到本桥——free-search 同架构)。
11
+ *
12
+ * T20-b 泛化(2026-10-08):路由从 ns 参数派生(bridgeRoutesFor),一个宿主进程
13
+ * 可为多个 ns 各注册一对端点(steward 双 ns:会话管家 + 搜索索引)——客户端与
14
+ * 服务端各自持同一派生式,测试钉死两半一致,URL 契约不可能漂移。
15
+ */
16
+ interface SettingsServiceLike {
17
+ update(ns: string, patch: object): Promise<void>;
18
+ register?(ns: string, schema: unknown, options?: {
19
+ base?: unknown;
20
+ }): unknown;
21
+ installSection?(owner: unknown, ns: string, schema: unknown, entry: unknown, hooks: unknown): unknown;
22
+ describe?(o?: {
23
+ redactSecrets?: boolean;
24
+ }): Array<{
25
+ ns: unknown;
26
+ [k: string]: unknown;
27
+ }>;
28
+ mutate?(ns: string, ops: Array<{
29
+ op: string;
30
+ path: string[];
31
+ value?: unknown;
32
+ }>, expectedRevision?: number): Promise<unknown>;
33
+ writable?: boolean;
34
+ }
35
+ /** 一个 settings ns 的桥路由对——client 与 server 必须同式派生(测试钉死一致)。 */
36
+ export declare function bridgeRoutesFor(ns: string): {
37
+ describe: string;
38
+ mutate: string;
39
+ };
40
+ export declare function registerSettingsBridgeRoutes(server: unknown, getSettingsSvc: () => SettingsServiceLike | undefined, ns: string): Array<() => void>;
41
+ export {};
package/lib/bridge.js ADDED
@@ -0,0 +1,128 @@
1
+ /** 一个 settings ns 的桥路由对——client 与 server 必须同式派生(测试钉死一致)。 */
2
+ export function bridgeRoutesFor(ns) {
3
+ return { describe: `/api/${ns}/settings/describe`, mutate: `/api/${ns}/settings/mutate` };
4
+ }
5
+ function readJsonBody(reqRaw, limit = 256 * 1024) {
6
+ const req = reqRaw;
7
+ return new Promise((resolve, reject) => {
8
+ const chunks = [];
9
+ let size = 0;
10
+ req.on('data', (chunk) => {
11
+ const buf = chunk;
12
+ size += buf.length;
13
+ if (size > limit) {
14
+ reject(new Error('bridge body too large'));
15
+ req.resume?.();
16
+ return;
17
+ }
18
+ chunks.push(buf);
19
+ });
20
+ req.on('end', () => {
21
+ const raw = Buffer.concat(chunks).toString('utf8');
22
+ if (raw === '') {
23
+ resolve({});
24
+ return;
25
+ }
26
+ try {
27
+ resolve(JSON.parse(raw));
28
+ }
29
+ catch (e) {
30
+ reject(e instanceof Error ? e : new Error(String(e)));
31
+ }
32
+ });
33
+ req.on('error', reject);
34
+ });
35
+ }
36
+ function sendJson(res, code, body) {
37
+ res.writeHead(code, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-cache' });
38
+ res.end(JSON.stringify(body));
39
+ }
40
+ function isConflict(e) {
41
+ return e instanceof Error && /conflict|revision/i.test(e.message);
42
+ }
43
+ export function registerSettingsBridgeRoutes(server, getSettingsSvc, ns) {
44
+ if (typeof server !== 'object' || server === null)
45
+ return [];
46
+ const ws = typeof server.register === 'function' ? server : undefined;
47
+ if (ws === undefined)
48
+ return [];
49
+ const offs = [];
50
+ const routes = bridgeRoutesFor(ns);
51
+ offs.push(ws.register({
52
+ kind: 'exact',
53
+ path: routes.describe,
54
+ handler: (rawReq, rawRes) => {
55
+ const req = rawReq;
56
+ const res = rawRes;
57
+ if (req.method !== 'POST') {
58
+ sendJson(res, 405, { ok: false, code: 'method-not-allowed' });
59
+ return;
60
+ }
61
+ const svc = getSettingsSvc();
62
+ if (svc === undefined || typeof svc.describe !== 'function') {
63
+ sendJson(res, 200, { ok: false, code: 'settings-unavailable', message: 'settings service not resolved yet' });
64
+ return;
65
+ }
66
+ try {
67
+ const descriptor = svc
68
+ .describe({ redactSecrets: true })
69
+ .find((d) => d.ns === ns);
70
+ if (descriptor === undefined) {
71
+ sendJson(res, 200, { ok: false, code: 'ns-not-registered', message: `settings namespace "${ns}" is not registered` });
72
+ return;
73
+ }
74
+ sendJson(res, 200, { ok: true, value: { descriptor, writable: svc.writable !== false } });
75
+ }
76
+ catch (e) {
77
+ sendJson(res, 200, { ok: false, code: 'internal', message: e instanceof Error ? e.message : String(e) });
78
+ }
79
+ },
80
+ }));
81
+ offs.push(ws.register({
82
+ kind: 'exact',
83
+ path: routes.mutate,
84
+ handler: (rawReq, rawRes) => {
85
+ const req = rawReq;
86
+ const res = rawRes;
87
+ if (req.method !== 'POST') {
88
+ sendJson(res, 405, { ok: false, code: 'method-not-allowed' });
89
+ return;
90
+ }
91
+ const svc = getSettingsSvc();
92
+ const mutate = svc === undefined ? undefined : svc.mutate;
93
+ if (typeof mutate !== 'function') {
94
+ sendJson(res, 200, { ok: false, code: 'settings-unavailable', message: 'settings service mutate not available' });
95
+ return;
96
+ }
97
+ readJsonBody(req).then((body) => {
98
+ const ops = body.ops;
99
+ if (!Array.isArray(ops) || ops.some((op) => !op || typeof op !== 'object' || !(op.op in { set: 1, unset: 1 }))) {
100
+ sendJson(res, 200, { ok: false, code: 'settings-rejected', message: 'malformed bridge mutate ops' });
101
+ return;
102
+ }
103
+ const expectedRevision = typeof body.expectedRevision === 'number' ? body.expectedRevision : undefined;
104
+ mutate
105
+ .call(svc, ns, ops, expectedRevision)
106
+ .then(() => {
107
+ // mutate 成功即 describe 回填(规范:写路径仍归口宿主服务,无第二份状态)
108
+ const descriptor = svc
109
+ .describe({ redactSecrets: true })
110
+ .find((d) => d.ns === ns);
111
+ if (descriptor === undefined) {
112
+ sendJson(res, 200, { ok: false, code: 'internal', message: `namespace "${ns}" disposed after mutate` });
113
+ return;
114
+ }
115
+ sendJson(res, 200, { ok: true, value: { descriptor } });
116
+ })
117
+ .catch((e) => {
118
+ sendJson(res, 200, isConflict(e)
119
+ ? { ok: false, code: 'settings-conflict', message: e instanceof Error ? e.message : String(e) }
120
+ : { ok: false, code: 'internal', message: e instanceof Error ? e.message : String(e) });
121
+ });
122
+ }).catch((e) => {
123
+ sendJson(res, 200, { ok: false, code: 'bad-request', message: e instanceof Error ? e.message : String(e) });
124
+ });
125
+ },
126
+ }));
127
+ return offs;
128
+ }
package/lib/cli.js CHANGED
File without changes
package/lib/client.js CHANGED
@@ -1701,6 +1701,13 @@ window.__ModuleLoader__.load({
1701
1701
  * default so the section reads as an open page of drawers.
1702
1702
  */
1703
1703
  function PermissiveCard({ t, scope }) {
1704
+ if (!scope) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1705
+ style: {
1706
+ padding: 16,
1707
+ color: "var(--dsw-alias-label-tertiary, #888)"
1708
+ },
1709
+ children: [t("card.unavailable"), "(scope 未解析 — 诊断占位)"]
1710
+ });
1704
1711
  const [open, setOpen] = (0, react.useState)(true);
1705
1712
  const [apiKeyDraft, setApiKeyDraft] = (0, react.useState)("");
1706
1713
  const [allowText, setAllowText] = (0, react.useState)("");
@@ -1708,7 +1715,13 @@ window.__ModuleLoader__.load({
1708
1715
  const snapshot = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
1709
1716
  const unavailable = snapshot.status === "unavailable";
1710
1717
  const readonly = unavailable || !snapshot.writable;
1711
- const value = snapshot.value ?? {};
1718
+ const raw = snapshot.value ?? {};
1719
+ const listOf = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
1720
+ const value = {
1721
+ ...raw,
1722
+ allowlist: listOf(raw.allowlist),
1723
+ denyKeywords: listOf(raw.denyKeywords)
1724
+ };
1712
1725
  const strategies = value.permissiveStrategies ?? {};
1713
1726
  const hasApiKey = typeof value.classifierApiKey === "string" && value.classifierApiKey !== "";
1714
1727
  const effective = value.permissive === true;
@@ -4234,17 +4247,125 @@ window.__ModuleLoader__.load({
4234
4247
  *
4235
4248
  * Both variants are scoped sub-injects: on a host without the service the fiber
4236
4249
  * waits forever WITHOUT blocking the plugin's other faces (same posture as the
4237
- * notice strip's slot-inject when its slot holder is absent).
4250
+ * notice strip's slot-inject when its slot holder is absent). session-guard
4251
+ * 4.1.0 proves the `settingsScope` branch resolves on 0.1.5-rc.3 (its
4252
+ * shell.overlay gear registers inside this callback and renders there).
4238
4253
  */
4239
4254
  function resolveSettingsScope(ctx, namespace, onScope) {
4240
4255
  ctx.inject(["configForms"], (configForms) => {
4241
- onScope(configForms.get(namespace));
4256
+ onScope(configForms.get(namespace), "configForms");
4242
4257
  });
4243
4258
  ctx.inject(["settingsScope"], (settingsScope) => {
4244
- onScope(settingsScope.bind({ namespace }));
4259
+ onScope(settingsScope.bind({ namespace }), "settingsScope");
4245
4260
  });
4246
4261
  }
4247
4262
  //#endregion
4263
+ //#region src/client/bridge-scope.ts
4264
+ /** 与 server 侧 bridgeRoutesFor(src/bridge.ts)必须逐字符一致。 */
4265
+ function bridgeRoutesFor(ns) {
4266
+ return {
4267
+ describe: `/api/${ns}/settings/describe`,
4268
+ mutate: `/api/${ns}/settings/mutate`
4269
+ };
4270
+ }
4271
+ var BridgeDocHandle = class {
4272
+ snapshot = {
4273
+ status: "pending",
4274
+ value: void 0,
4275
+ writable: true
4276
+ };
4277
+ listeners = /* @__PURE__ */ new Set();
4278
+ revision;
4279
+ inflight;
4280
+ routes;
4281
+ constructor(ns) {
4282
+ this.routes = bridgeRoutesFor(ns);
4283
+ this.refresh();
4284
+ }
4285
+ getSnapshot() {
4286
+ return this.snapshot;
4287
+ }
4288
+ subscribe(listener) {
4289
+ this.listeners.add(listener);
4290
+ return () => {
4291
+ this.listeners.delete(listener);
4292
+ };
4293
+ }
4294
+ async set(field, value) {
4295
+ await this.mutate([{
4296
+ op: "set",
4297
+ path: [field],
4298
+ value
4299
+ }]);
4300
+ }
4301
+ async unset(field) {
4302
+ await this.mutate([{
4303
+ op: "unset",
4304
+ path: [field]
4305
+ }]);
4306
+ }
4307
+ notify() {
4308
+ for (const l of [...this.listeners]) l();
4309
+ }
4310
+ async request(path, body) {
4311
+ const response = await fetch(path, {
4312
+ method: "POST",
4313
+ headers: { "content-type": "application/json" },
4314
+ body: JSON.stringify(body ?? {})
4315
+ });
4316
+ if (!response.ok) throw new Error(`bridge ${path} HTTP ${response.status}`);
4317
+ return await response.json();
4318
+ }
4319
+ async refresh() {
4320
+ if (this.inflight !== void 0) return this.inflight;
4321
+ this.inflight = (async () => {
4322
+ try {
4323
+ const body = await this.request(this.routes.describe);
4324
+ if (body.ok && body.value?.descriptor !== void 0) {
4325
+ const d = body.value.descriptor;
4326
+ this.revision = typeof d.revision === "number" ? d.revision : void 0;
4327
+ this.snapshot = {
4328
+ status: "ready",
4329
+ value: d.value,
4330
+ writable: body.value.writable !== false
4331
+ };
4332
+ } else this.snapshot = {
4333
+ status: "error",
4334
+ value: void 0,
4335
+ writable: false
4336
+ };
4337
+ } catch {
4338
+ this.snapshot = {
4339
+ status: "error",
4340
+ value: void 0,
4341
+ writable: false
4342
+ };
4343
+ } finally {
4344
+ this.inflight = void 0;
4345
+ }
4346
+ this.notify();
4347
+ })();
4348
+ return this.inflight;
4349
+ }
4350
+ async mutate(ops) {
4351
+ const body = await this.request(this.routes.mutate, {
4352
+ ops,
4353
+ expectedRevision: this.revision
4354
+ });
4355
+ if (body.ok && body.value?.descriptor !== void 0) {
4356
+ const d = body.value.descriptor;
4357
+ this.revision = typeof d.revision === "number" ? d.revision : void 0;
4358
+ this.snapshot = {
4359
+ status: "ready",
4360
+ value: d.value,
4361
+ writable: this.snapshot.writable
4362
+ };
4363
+ } else if (body.code === "settings-conflict") await this.refresh();
4364
+ else throw new Error(`bridge mutate failed: ${body.code ?? "unknown"} ${body.message ?? ""}`);
4365
+ this.notify();
4366
+ }
4367
+ };
4368
+ //#endregion
4248
4369
  //#region src/client/index.ts
4249
4370
  /** The profile entry id of this plugin — the `configForms` key (kept in lockstep with cordis.patch.yml). */
4250
4371
  const PERMISSIVE_NS = "dsh-perm-gate";
@@ -4283,7 +4404,21 @@ window.__ModuleLoader__.load({
4283
4404
  }, HistoryView);
4284
4405
  });
4285
4406
  const t = ctx.locale.bind(NS);
4286
- resolveSettingsScope(ctx, PERMISSIVE_NS, (scope) => {
4407
+ const safe = (tag, fn) => {
4408
+ try {
4409
+ fn();
4410
+ } catch (e) {
4411
+ (globalThis.__pgSurf ??= []).push(`${tag}: ${e?.message ?? String(e)}`);
4412
+ console.warn(`[dsh-perm-gate] client surface '${tag}' failed:`, e);
4413
+ }
4414
+ };
4415
+ const scopeRef = {};
4416
+ resolveSettingsScope(ctx, PERMISSIVE_NS, (scope, _generation) => {
4417
+ scopeRef.scope = scope;
4418
+ });
4419
+ const bridgeScope = new BridgeDocHandle(PERMISSIVE_NS);
4420
+ const cardInjected = () => ({ scope: scopeRef.scope ?? bridgeScope });
4421
+ safe("settings.section", () => {
4287
4422
  ctx.slots.inject("settings.section", function* () {
4288
4423
  yield ctx.slots.register({
4289
4424
  name: "settings.section",
@@ -4291,14 +4426,16 @@ window.__ModuleLoader__.load({
4291
4426
  order: 30,
4292
4427
  label: () => t("section.title"),
4293
4428
  locale: NS,
4294
- inject: () => ({ scope })
4429
+ inject: cardInjected
4295
4430
  }, PermissiveCard);
4296
4431
  });
4432
+ });
4433
+ safe("plugins.bundle.config", () => {
4297
4434
  ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
4298
4435
  name: "plugins.bundle.config",
4299
4436
  key: "dsh-perm-gate",
4300
4437
  locale: NS,
4301
- inject: () => ({ scope })
4438
+ inject: cardInjected
4302
4439
  }, PermissiveCard));
4303
4440
  });
4304
4441
  }