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

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 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.7",
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,34 @@
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
+ interface SettingsServiceLike {
13
+ update(ns: string, patch: object): Promise<void>;
14
+ register?(ns: string, schema: unknown, options?: {
15
+ base?: unknown;
16
+ }): unknown;
17
+ installSection?(owner: unknown, ns: string, schema: unknown, entry: unknown, hooks: unknown): unknown;
18
+ describe?(o?: {
19
+ redactSecrets?: boolean;
20
+ }): Array<{
21
+ ns: unknown;
22
+ [k: string]: unknown;
23
+ }>;
24
+ mutate?(ns: string, ops: Array<{
25
+ op: string;
26
+ path: string[];
27
+ value?: unknown;
28
+ }>, expectedRevision?: number): Promise<unknown>;
29
+ writable?: boolean;
30
+ }
31
+ export declare const BRIDGE_DESCRIBE_ROUTE = "/api/dsh-perm-gate/settings/describe";
32
+ export declare const BRIDGE_MUTATE_ROUTE = "/api/dsh-perm-gate/settings/mutate";
33
+ export declare function registerSettingsBridgeRoutes(server: unknown, getSettingsSvc: () => SettingsServiceLike | undefined, ns: string): Array<() => void>;
34
+ export {};
package/lib/bridge.js ADDED
@@ -0,0 +1,125 @@
1
+ export const BRIDGE_DESCRIBE_ROUTE = '/api/dsh-perm-gate/settings/describe';
2
+ export const BRIDGE_MUTATE_ROUTE = '/api/dsh-perm-gate/settings/mutate';
3
+ function readJsonBody(reqRaw, limit = 256 * 1024) {
4
+ const req = reqRaw;
5
+ return new Promise((resolve, reject) => {
6
+ const chunks = [];
7
+ let size = 0;
8
+ req.on('data', (chunk) => {
9
+ const buf = chunk;
10
+ size += buf.length;
11
+ if (size > limit) {
12
+ reject(new Error('bridge body too large'));
13
+ req.resume?.();
14
+ return;
15
+ }
16
+ chunks.push(buf);
17
+ });
18
+ req.on('end', () => {
19
+ const raw = Buffer.concat(chunks).toString('utf8');
20
+ if (raw === '') {
21
+ resolve({});
22
+ return;
23
+ }
24
+ try {
25
+ resolve(JSON.parse(raw));
26
+ }
27
+ catch (e) {
28
+ reject(e instanceof Error ? e : new Error(String(e)));
29
+ }
30
+ });
31
+ req.on('error', reject);
32
+ });
33
+ }
34
+ function sendJson(res, code, body) {
35
+ res.writeHead(code, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-cache' });
36
+ res.end(JSON.stringify(body));
37
+ }
38
+ function isConflict(e) {
39
+ return e instanceof Error && /conflict|revision/i.test(e.message);
40
+ }
41
+ export function registerSettingsBridgeRoutes(server, getSettingsSvc, ns) {
42
+ if (typeof server !== 'object' || server === null)
43
+ return [];
44
+ const ws = typeof server.register === 'function' ? server : undefined;
45
+ if (ws === undefined)
46
+ return [];
47
+ const offs = [];
48
+ offs.push(ws.register({
49
+ kind: 'exact',
50
+ path: BRIDGE_DESCRIBE_ROUTE,
51
+ handler: (rawReq, rawRes) => {
52
+ const req = rawReq;
53
+ const res = rawRes;
54
+ if (req.method !== 'POST') {
55
+ sendJson(res, 405, { ok: false, code: 'method-not-allowed' });
56
+ return;
57
+ }
58
+ const svc = getSettingsSvc();
59
+ if (svc === undefined || typeof svc.describe !== 'function') {
60
+ sendJson(res, 200, { ok: false, code: 'settings-unavailable', message: 'settings service not resolved yet' });
61
+ return;
62
+ }
63
+ try {
64
+ const descriptor = svc
65
+ .describe({ redactSecrets: true })
66
+ .find((d) => d.ns === ns);
67
+ if (descriptor === undefined) {
68
+ sendJson(res, 200, { ok: false, code: 'ns-not-registered', message: `settings namespace "${ns}" is not registered` });
69
+ return;
70
+ }
71
+ sendJson(res, 200, { ok: true, value: { descriptor, writable: svc.writable !== false } });
72
+ }
73
+ catch (e) {
74
+ sendJson(res, 200, { ok: false, code: 'internal', message: e instanceof Error ? e.message : String(e) });
75
+ }
76
+ },
77
+ }));
78
+ offs.push(ws.register({
79
+ kind: 'exact',
80
+ path: BRIDGE_MUTATE_ROUTE,
81
+ handler: (rawReq, rawRes) => {
82
+ const req = rawReq;
83
+ const res = rawRes;
84
+ if (req.method !== 'POST') {
85
+ sendJson(res, 405, { ok: false, code: 'method-not-allowed' });
86
+ return;
87
+ }
88
+ const svc = getSettingsSvc();
89
+ const mutate = svc === undefined ? undefined : svc.mutate;
90
+ if (typeof mutate !== 'function') {
91
+ sendJson(res, 200, { ok: false, code: 'settings-unavailable', message: 'settings service mutate not available' });
92
+ return;
93
+ }
94
+ readJsonBody(req).then((body) => {
95
+ const ops = body.ops;
96
+ if (!Array.isArray(ops) || ops.some((op) => !op || typeof op !== 'object' || !(op.op in { set: 1, unset: 1 }))) {
97
+ sendJson(res, 200, { ok: false, code: 'settings-rejected', message: 'malformed bridge mutate ops' });
98
+ return;
99
+ }
100
+ const expectedRevision = typeof body.expectedRevision === 'number' ? body.expectedRevision : undefined;
101
+ mutate
102
+ .call(svc, ns, ops, expectedRevision)
103
+ .then(() => {
104
+ // mutate 成功即 describe 回填(规范:写路径仍归口宿主服务,无第二份状态)
105
+ const descriptor = svc
106
+ .describe({ redactSecrets: true })
107
+ .find((d) => d.ns === ns);
108
+ if (descriptor === undefined) {
109
+ sendJson(res, 200, { ok: false, code: 'internal', message: `namespace "${ns}" disposed after mutate` });
110
+ return;
111
+ }
112
+ sendJson(res, 200, { ok: true, value: { descriptor } });
113
+ })
114
+ .catch((e) => {
115
+ sendJson(res, 200, isConflict(e)
116
+ ? { ok: false, code: 'settings-conflict', message: e instanceof Error ? e.message : String(e) }
117
+ : { ok: false, code: 'internal', message: e instanceof Error ? e.message : String(e) });
118
+ });
119
+ }).catch((e) => {
120
+ sendJson(res, 200, { ok: false, code: 'bad-request', message: e instanceof Error ? e.message : String(e) });
121
+ });
122
+ },
123
+ }));
124
+ return offs;
125
+ }
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;
@@ -3087,6 +3100,83 @@ window.__ModuleLoader__.load({
3087
3100
  }) : null]
3088
3101
  });
3089
3102
  }
3103
+ /** 浮窗齿轮 + 固定定位浮层承载完整审批门卡。自绘定位,不依赖宿主容器布局。 */
3104
+ function FloatingPermissiveGate(props) {
3105
+ const [open, setOpen] = (0, react.useState)(false);
3106
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3107
+ style: {
3108
+ position: "fixed",
3109
+ right: 16,
3110
+ bottom: 64,
3111
+ zIndex: 1e4
3112
+ },
3113
+ children: [
3114
+ open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3115
+ onClick: () => setOpen(false),
3116
+ style: {
3117
+ position: "fixed",
3118
+ top: 0,
3119
+ left: 0,
3120
+ right: 0,
3121
+ bottom: 0,
3122
+ background: "rgba(0,0,0,.45)",
3123
+ zIndex: 1e4
3124
+ }
3125
+ }) : null,
3126
+ open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3127
+ style: {
3128
+ position: "fixed",
3129
+ top: "6vh",
3130
+ left: "50%",
3131
+ transform: "translateX(-50%)",
3132
+ width: "min(760px, 92vw)",
3133
+ maxHeight: "84vh",
3134
+ overflowY: "auto",
3135
+ zIndex: 10001
3136
+ },
3137
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3138
+ type: "button",
3139
+ onClick: () => setOpen(false),
3140
+ style: {
3141
+ position: "sticky",
3142
+ top: 6,
3143
+ float: "right",
3144
+ marginRight: 8,
3145
+ zIndex: 2,
3146
+ font: "inherit",
3147
+ cursor: "pointer",
3148
+ borderRadius: 8,
3149
+ border: "1px solid rgba(127,127,127,.4)",
3150
+ background: "rgba(30,30,32,.85)",
3151
+ color: "inherit",
3152
+ padding: "2px 10px"
3153
+ },
3154
+ children: "✕ 关闭"
3155
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PermissiveCard, {
3156
+ t: props.t,
3157
+ scope: props.scope
3158
+ })]
3159
+ }) : null,
3160
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3161
+ type: "button",
3162
+ title: "审批门设置",
3163
+ onClick: () => setOpen(!open),
3164
+ style: {
3165
+ width: 40,
3166
+ height: 40,
3167
+ borderRadius: "50%",
3168
+ border: "1px solid rgba(127,127,127,.4)",
3169
+ background: "rgba(30,30,32,.85)",
3170
+ color: "inherit",
3171
+ cursor: "pointer",
3172
+ fontSize: 16,
3173
+ boxShadow: "0 4px 16px rgba(0,0,0,.35)"
3174
+ },
3175
+ children: "🛡"
3176
+ })
3177
+ ]
3178
+ });
3179
+ }
3090
3180
  //#endregion
3091
3181
  //#region src/client/feed.ts
3092
3182
  /** Fetch decision events for one session; `since > 0` returns only newer ones. */
@@ -4234,17 +4324,118 @@ window.__ModuleLoader__.load({
4234
4324
  *
4235
4325
  * Both variants are scoped sub-injects: on a host without the service the fiber
4236
4326
  * 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).
4327
+ * notice strip's slot-inject when its slot holder is absent). session-guard
4328
+ * 4.1.0 proves the `settingsScope` branch resolves on 0.1.5-rc.3 (its
4329
+ * shell.overlay gear registers inside this callback and renders there).
4238
4330
  */
4239
4331
  function resolveSettingsScope(ctx, namespace, onScope) {
4240
4332
  ctx.inject(["configForms"], (configForms) => {
4241
- onScope(configForms.get(namespace));
4333
+ onScope(configForms.get(namespace), "configForms");
4242
4334
  });
4243
4335
  ctx.inject(["settingsScope"], (settingsScope) => {
4244
- onScope(settingsScope.bind({ namespace }));
4336
+ onScope(settingsScope.bind({ namespace }), "settingsScope");
4245
4337
  });
4246
4338
  }
4247
4339
  //#endregion
4340
+ //#region src/client/bridge-scope.ts
4341
+ const DESCRIBE = "/api/dsh-perm-gate/settings/describe";
4342
+ const MUTATE = "/api/dsh-perm-gate/settings/mutate";
4343
+ var BridgeDocHandle = class {
4344
+ snapshot = {
4345
+ status: "pending",
4346
+ value: void 0,
4347
+ writable: true
4348
+ };
4349
+ listeners = /* @__PURE__ */ new Set();
4350
+ revision;
4351
+ inflight;
4352
+ constructor() {
4353
+ this.refresh();
4354
+ }
4355
+ getSnapshot() {
4356
+ return this.snapshot;
4357
+ }
4358
+ subscribe(listener) {
4359
+ this.listeners.add(listener);
4360
+ return () => {
4361
+ this.listeners.delete(listener);
4362
+ };
4363
+ }
4364
+ async set(field, value) {
4365
+ await this.mutate([{
4366
+ op: "set",
4367
+ path: [field],
4368
+ value
4369
+ }]);
4370
+ }
4371
+ async unset(field) {
4372
+ await this.mutate([{
4373
+ op: "unset",
4374
+ path: [field]
4375
+ }]);
4376
+ }
4377
+ notify() {
4378
+ for (const l of [...this.listeners]) l();
4379
+ }
4380
+ async request(path, body) {
4381
+ const response = await fetch(path, {
4382
+ method: "POST",
4383
+ headers: { "content-type": "application/json" },
4384
+ body: JSON.stringify(body ?? {})
4385
+ });
4386
+ if (!response.ok) throw new Error(`bridge ${path} HTTP ${response.status}`);
4387
+ return await response.json();
4388
+ }
4389
+ async refresh() {
4390
+ if (this.inflight !== void 0) return this.inflight;
4391
+ this.inflight = (async () => {
4392
+ try {
4393
+ const body = await this.request(DESCRIBE);
4394
+ if (body.ok && body.value?.descriptor !== void 0) {
4395
+ const d = body.value.descriptor;
4396
+ this.revision = typeof d.revision === "number" ? d.revision : void 0;
4397
+ this.snapshot = {
4398
+ status: "ready",
4399
+ value: d.value,
4400
+ writable: body.value.writable !== false
4401
+ };
4402
+ } else this.snapshot = {
4403
+ status: "error",
4404
+ value: void 0,
4405
+ writable: false
4406
+ };
4407
+ } catch {
4408
+ this.snapshot = {
4409
+ status: "error",
4410
+ value: void 0,
4411
+ writable: false
4412
+ };
4413
+ } finally {
4414
+ this.inflight = void 0;
4415
+ }
4416
+ this.notify();
4417
+ })();
4418
+ return this.inflight;
4419
+ }
4420
+ async mutate(ops) {
4421
+ const body = await this.request(MUTATE, {
4422
+ ops,
4423
+ expectedRevision: this.revision
4424
+ });
4425
+ if (body.ok && body.value?.descriptor !== void 0) {
4426
+ const d = body.value.descriptor;
4427
+ this.revision = typeof d.revision === "number" ? d.revision : void 0;
4428
+ this.snapshot = {
4429
+ status: "ready",
4430
+ value: d.value,
4431
+ writable: this.snapshot.writable
4432
+ };
4433
+ } else if (body.code === "settings-conflict") await this.refresh();
4434
+ else throw new Error(`bridge mutate failed: ${body.code ?? "unknown"} ${body.message ?? ""}`);
4435
+ this.notify();
4436
+ }
4437
+ };
4438
+ //#endregion
4248
4439
  //#region src/client/index.ts
4249
4440
  /** The profile entry id of this plugin — the `configForms` key (kept in lockstep with cordis.patch.yml). */
4250
4441
  const PERMISSIVE_NS = "dsh-perm-gate";
@@ -4283,7 +4474,21 @@ window.__ModuleLoader__.load({
4283
4474
  }, HistoryView);
4284
4475
  });
4285
4476
  const t = ctx.locale.bind(NS);
4286
- resolveSettingsScope(ctx, PERMISSIVE_NS, (scope) => {
4477
+ const safe = (tag, fn) => {
4478
+ try {
4479
+ fn();
4480
+ } catch (e) {
4481
+ (globalThis.__pgSurf ??= []).push(`${tag}: ${e?.message ?? String(e)}`);
4482
+ console.warn(`[dsh-perm-gate] client surface '${tag}' failed:`, e);
4483
+ }
4484
+ };
4485
+ const scopeRef = {};
4486
+ resolveSettingsScope(ctx, PERMISSIVE_NS, (scope, _generation) => {
4487
+ scopeRef.scope = scope;
4488
+ });
4489
+ const bridgeScope = new BridgeDocHandle();
4490
+ const cardInjected = () => ({ scope: scopeRef.scope ?? bridgeScope });
4491
+ safe("settings.section", () => {
4287
4492
  ctx.slots.inject("settings.section", function* () {
4288
4493
  yield ctx.slots.register({
4289
4494
  name: "settings.section",
@@ -4291,16 +4496,50 @@ window.__ModuleLoader__.load({
4291
4496
  order: 30,
4292
4497
  label: () => t("section.title"),
4293
4498
  locale: NS,
4294
- inject: () => ({ scope })
4499
+ inject: cardInjected
4295
4500
  }, PermissiveCard);
4296
4501
  });
4502
+ });
4503
+ safe("plugins.bundle.config", () => {
4297
4504
  ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
4298
4505
  name: "plugins.bundle.config",
4299
4506
  key: "dsh-perm-gate",
4300
4507
  locale: NS,
4301
- inject: () => ({ scope })
4508
+ inject: cardInjected
4509
+ }, PermissiveCard));
4510
+ });
4511
+ safe("settings.plugins.tab", () => {
4512
+ ctx.slots.inject("settings.plugins.tab", () => ctx.slots.register({
4513
+ name: "settings.plugins.tab",
4514
+ id: PERMISSIVE_NS,
4515
+ order: 50,
4516
+ label: () => t("section.title"),
4517
+ locale: NS,
4518
+ inject: cardInjected
4519
+ }, PermissiveCard));
4520
+ });
4521
+ safe("settings.plugin.item", () => {
4522
+ const itemSlots = ctx.slots;
4523
+ itemSlots.inject("settings.plugin.item", () => itemSlots.register({
4524
+ name: "settings.plugin.item",
4525
+ id: PERMISSIVE_NS,
4526
+ key: PERMISSIVE_NS,
4527
+ order: 40,
4528
+ locale: NS,
4529
+ inject: cardInjected
4302
4530
  }, PermissiveCard));
4303
4531
  });
4532
+ safe("shell.overlay", () => {
4533
+ const overlaySlots = ctx.slots;
4534
+ overlaySlots.inject("shell.overlay", () => overlaySlots.register({
4535
+ name: "shell.overlay",
4536
+ id: PERMISSIVE_NS,
4537
+ inject: () => ({
4538
+ t,
4539
+ scope: scopeRef.scope ?? bridgeScope
4540
+ })
4541
+ }, FloatingPermissiveGate));
4542
+ });
4304
4543
  }
4305
4544
  //#endregion
4306
4545
  exports.apply = apply;