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 +13 -0
- package/dsh.plugin.json +1 -1
- package/lib/bridge.d.ts +41 -0
- package/lib/bridge.js +128 -0
- package/lib/cli.js +0 -0
- package/lib/client.js +144 -7
- package/lib/client.js.map +1 -1
- package/lib/debug-generation.d.ts +21 -0
- package/lib/debug-generation.js +44 -0
- package/lib/index.js +104 -7
- package/package.json +31 -20
- package/scripts/patch-permission-glyph.mjs +0 -0
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.
|
|
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 否决、其余一律放行,规则未知或非法时直接报错,绝不静默失效。"
|
package/lib/bridge.d.ts
ADDED
|
@@ -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
|
|
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
|
-
|
|
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:
|
|
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:
|
|
4438
|
+
inject: cardInjected
|
|
4302
4439
|
}, PermissiveCard));
|
|
4303
4440
|
});
|
|
4304
4441
|
}
|