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 +1 -1
- package/lib/bridge.d.ts +34 -0
- package/lib/bridge.js +125 -0
- package/lib/cli.js +0 -0
- package/lib/client.js +246 -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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* debug-generation — T10b served 集合诊断端点(临时,上线前删,spec served 七步第 2 步)。
|
|
3
|
+
*
|
|
4
|
+
* GET /api/dsh-perm-gate/debug-generation
|
|
5
|
+
* 返回插件持有的 settings 服务实例自检:全量 describe 的 ns 列表 + 方法面。
|
|
6
|
+
* 判读:ns 列表若同时含 dsh-perm-gate / dsh-session-guard / dsh-free-search-settings
|
|
7
|
+
* 等其他插件命名空间 → 持有的是宿主共享单例(identitySame=TRUE,H3 孤儿假设死);
|
|
8
|
+
* 若只有自家 ns → 孤儿实例坐实(H3 成立)。
|
|
9
|
+
*/
|
|
10
|
+
export interface SettingsServiceLike {
|
|
11
|
+
update(ns: string, patch: object): Promise<void>;
|
|
12
|
+
register?(ns: string, schema: unknown, options?: {
|
|
13
|
+
base?: unknown;
|
|
14
|
+
}): unknown;
|
|
15
|
+
installSection?(owner: unknown, ns: string, schema: unknown, entry: unknown, hooks: unknown): unknown;
|
|
16
|
+
describe?(): Array<{
|
|
17
|
+
ns: unknown;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
export declare const DEBUG_GENERATION_ROUTE = "/api/dsh-perm-gate/debug-generation";
|
|
21
|
+
export declare function registerDebugGenerationRoute(server: unknown, getSettingsSvc: () => SettingsServiceLike | undefined): (() => void) | undefined;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export const DEBUG_GENERATION_ROUTE = '/api/dsh-perm-gate/debug-generation';
|
|
2
|
+
export function registerDebugGenerationRoute(server, getSettingsSvc) {
|
|
3
|
+
if (typeof server !== 'object' || server === null)
|
|
4
|
+
return undefined;
|
|
5
|
+
const ws = typeof server.register === 'function' ? server : undefined;
|
|
6
|
+
if (ws === undefined || typeof ws.register !== 'function')
|
|
7
|
+
return undefined;
|
|
8
|
+
return ws.register({
|
|
9
|
+
kind: 'exact',
|
|
10
|
+
path: DEBUG_GENERATION_ROUTE,
|
|
11
|
+
handler: (rawReq, rawRes) => {
|
|
12
|
+
const req = rawReq;
|
|
13
|
+
const res = rawRes;
|
|
14
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
15
|
+
res.writeHead(405);
|
|
16
|
+
res.end();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const svc = getSettingsSvc();
|
|
20
|
+
let namespaces = [];
|
|
21
|
+
let describeError;
|
|
22
|
+
if (svc !== undefined && typeof svc.describe === 'function') {
|
|
23
|
+
try {
|
|
24
|
+
const descriptors = svc.describe();
|
|
25
|
+
namespaces = descriptors.map((d) => String(d.ns));
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
describeError = e instanceof Error ? e.message : String(e);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
32
|
+
res.end(JSON.stringify({
|
|
33
|
+
hasSvc: svc !== undefined,
|
|
34
|
+
describeError,
|
|
35
|
+
namespaces,
|
|
36
|
+
methods: svc === undefined ? null : {
|
|
37
|
+
register: typeof svc.register === 'function',
|
|
38
|
+
installSection: typeof svc.installSection === 'function',
|
|
39
|
+
update: typeof svc.update === 'function',
|
|
40
|
+
},
|
|
41
|
+
}, null, 2));
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { SandboxController } from './appcontainer/index.js';
|
|
4
|
-
import { Config, ensureDataDir, readRulesFromSettings, readVolatileValue, resolveVolatileConfig, resolveDshHome, resolveDataDir, resolveGatePresets, resolvePermissiveStrategies, resolveRulesFile } from './config.js';
|
|
4
|
+
import { Config, ensureDataDir, readRulesFromSettings, readVolatileValue, resolveVolatileConfig, resolveConfig, resolveDshHome, resolveDataDir, resolveGatePresets, resolvePermissiveStrategies, resolveRulesFile } from './config.js';
|
|
5
5
|
import { appendAllowToSettings, readRulesFileDoc, replaceAllowInSettings } from './allowlist.js';
|
|
6
6
|
import { runDryRun } from './dry-run.js';
|
|
7
7
|
import { registerDryRunRoute, registerEventsRoute, registerHealthRoute, registerLearningRoute, registerNetworkRoute, registerReceiverRoute, registerReviewRoutes, registerRulesRoute } from './events.js';
|
|
8
|
+
// T12 设置数据桥(describe/mutate;client BridgeDocHandle 消费)
|
|
9
|
+
import { registerSettingsBridgeRoutes } from './bridge.js';
|
|
8
10
|
import { buildReceiverInfo } from './receiver-info.js';
|
|
9
11
|
import { readRulesView, readRulesViewFromSettings } from './rules-view.js';
|
|
10
12
|
import { PermGateRuntime } from './runtime.js';
|
|
@@ -188,7 +190,7 @@ export function makePreExecuteListener(runtime) {
|
|
|
188
190
|
export function apply(ctx, config = {}) {
|
|
189
191
|
// Runtime-adjustable config: 0.1.7 hands `.volatile()` fields to apply() as
|
|
190
192
|
// live refs; `current()` shallow-resolves one snapshot per read.
|
|
191
|
-
|
|
193
|
+
let current = () => resolveVolatileConfig(config);
|
|
192
194
|
// ─── Rules (volatile `rules` field on this entry) ─────────────────────────
|
|
193
195
|
// The rules document is a `.volatile()` whole-object field of the plugin's
|
|
194
196
|
// own entry (0.1.7 has no second projectable namespace). Unconfigured
|
|
@@ -207,17 +209,104 @@ export function apply(ctx, config = {}) {
|
|
|
207
209
|
ctx.inject(['settings'], (child) => {
|
|
208
210
|
const svc = child.settings;
|
|
209
211
|
settingsRef.svc = svc;
|
|
212
|
+
if (svc === undefined)
|
|
213
|
+
return;
|
|
214
|
+
// setSource 换绑运行时读取源:source 可能是 () => values(≤0.1.5 scope.get)
|
|
215
|
+
// 或普通对象;base 与运行时读分用两个 resolve(手册 §P1-3)。
|
|
216
|
+
const rebindFrom = (source) => {
|
|
217
|
+
current = () => resolveVolatileConfig((typeof source === 'function' ? source() : (source ?? config)));
|
|
218
|
+
};
|
|
219
|
+
const seedDoc = () => ({ ...readRulesFileDoc(rulesFilePath), initialized: true });
|
|
220
|
+
const seedUpdate = () => svc.update(ENTRY_ID, { rules: seedDoc() });
|
|
221
|
+
// F4 加固(spec served 段判「无论如何无条件做」):宿主 installSection 在
|
|
222
|
+
// register 之后同步调 setSource(dsh-settings 0.1.5 lib/index.js:332),换绑
|
|
223
|
+
// 或 onChange 抛错会从 installSection 一路抛穿插件加载、连带注册失败——
|
|
224
|
+
// 这里兜住异常保住注册(卡派发依赖 ns∈served,注册比换绑成败更金贵)。
|
|
225
|
+
const guardedSetSource = (source) => {
|
|
226
|
+
try {
|
|
227
|
+
rebindFrom(source);
|
|
228
|
+
hooks.onChange();
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
console.warn('[dsh-perm-gate] setSource rebind failed (registration kept):', e);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
const hooks = {
|
|
235
|
+
setSource: guardedSetSource,
|
|
236
|
+
onChange: () => { applyVolatileUpdate(); },
|
|
237
|
+
};
|
|
210
238
|
// Activation-time seed: a never-configured `rules` field is populated once
|
|
211
239
|
// — from the rules file when one exists (implicit migration), otherwise a
|
|
212
240
|
// bare starter document — and marked `initialized`, so the panel shows a
|
|
213
241
|
// live rules document from the first boot instead of a missing-file
|
|
214
242
|
// fallback. The field watcher below picks the seed up via reload.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
243
|
+
// ── 三代 waist:三分支执行体互斥(P1-1/P1-2),register-retry 仅存活于
|
|
244
|
+
// 兜底分支且在 rc.1+ 永不命中;rc.1+ 不引入 configure(P1-5)。
|
|
245
|
+
if (typeof svc.installSection === 'function') {
|
|
246
|
+
console.warn('[dsh-perm-gate] settings generation = service.installSection (0.1.2/0.1.5 line)');
|
|
247
|
+
svc.installSection(ctx, ENTRY_ID, Config, resolveConfig(config), {
|
|
248
|
+
setSource: guardedSetSource,
|
|
249
|
+
onChange: () => { applyVolatileUpdate(); },
|
|
219
250
|
});
|
|
251
|
+
if (readRulesDocument() === undefined) {
|
|
252
|
+
void seedUpdate().catch((e) => {
|
|
253
|
+
console.warn('[dsh-perm-gate] legacy seed update failed:', e);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
220
257
|
}
|
|
258
|
+
void (async () => {
|
|
259
|
+
try {
|
|
260
|
+
// 变量说明符:dsh-settings 现为 optional peer(T35,2026-10-07——dependencies
|
|
261
|
+
// 硬钉 0.1.1-rc.2 已删:六线运行时它本来就无效——0.1.5 分支互斥不加载、老线
|
|
262
|
+
// nested 链接悬空后动态 import 经 node 向上解析落宿主自身副本,见
|
|
263
|
+
// cross-version-adaptation-all/findings.md T10b 附录)。变量形式让 TS 按 any
|
|
264
|
+
// 解析不做模块查证;另注意 ctx.get 不跨插件隔离上下文(index.ts:44),
|
|
265
|
+
// legacy 分支只能动态 import,不能 ctx.get 直取宿主单例。
|
|
266
|
+
const legacySpecifier = '@deepseek-ai/dsh-settings';
|
|
267
|
+
const legacy = (await import(legacySpecifier));
|
|
268
|
+
if (typeof legacy.installSettingsSection === 'function' && typeof legacy.settingsNamespace === 'function') {
|
|
269
|
+
console.warn('[dsh-perm-gate] settings generation = module.installSettingsSection (0.1.0/0.1.1 line)');
|
|
270
|
+
legacy.installSettingsSection(ctx, legacy.settingsNamespace(ENTRY_ID), Config, resolveConfig(config), {
|
|
271
|
+
setSource: (source) => {
|
|
272
|
+
rebindFrom(source);
|
|
273
|
+
hooks.onChange();
|
|
274
|
+
},
|
|
275
|
+
onChange: () => { applyVolatileUpdate(); },
|
|
276
|
+
});
|
|
277
|
+
if (readRulesDocument() === undefined) {
|
|
278
|
+
await seedUpdate().catch((e) => {
|
|
279
|
+
console.warn('[dsh-perm-gate] legacy seed update failed:', e);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
// 模块级 legacy API 缺失/导入失败 → 落到 rc.1 兜底
|
|
287
|
+
}
|
|
288
|
+
console.warn('[dsh-perm-gate] settings generation = rc1.loader (rc.1+: loader 自动注册 ns,update 直接成功;register-retry 兜底永不命中)');
|
|
289
|
+
if (readRulesDocument() === undefined) {
|
|
290
|
+
void (async () => {
|
|
291
|
+
try {
|
|
292
|
+
await seedUpdate();
|
|
293
|
+
}
|
|
294
|
+
catch (e) {
|
|
295
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
296
|
+
// register-retry 兜底:rc.1+ 上 ns 已被 loader 占用,永不命中
|
|
297
|
+
if (!/not registered/i.test(message) || typeof svc.register !== 'function') {
|
|
298
|
+
throw e;
|
|
299
|
+
}
|
|
300
|
+
console.warn(`[dsh-perm-gate] legacy fallback: registering settings namespace ${ENTRY_ID}`);
|
|
301
|
+
svc.register(ENTRY_ID, Config, { base: { rules: seedDoc() } });
|
|
302
|
+
await seedUpdate();
|
|
303
|
+
console.warn('[dsh-perm-gate] legacy fallback registered + seeded');
|
|
304
|
+
}
|
|
305
|
+
})().catch((e) => {
|
|
306
|
+
console.warn('[dsh-perm-gate] rules seed write failed:', e);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
})();
|
|
221
310
|
});
|
|
222
311
|
const settingsWrite = (write) => {
|
|
223
312
|
const svc = settingsRef.svc;
|
|
@@ -478,7 +567,7 @@ export function apply(ctx, config = {}) {
|
|
|
478
567
|
// The host no longer seeds settings (there is no write path): an empty
|
|
479
568
|
// `allowlist` keeps the rules-file default via `runtime.allowlist()`.
|
|
480
569
|
let lastRulesDocKey;
|
|
481
|
-
|
|
570
|
+
function applyVolatileUpdate() {
|
|
482
571
|
const next = asSurface(current());
|
|
483
572
|
if (next !== undefined) {
|
|
484
573
|
if (Array.isArray(next.allowlist)) {
|
|
@@ -506,6 +595,10 @@ export function apply(ctx, config = {}) {
|
|
|
506
595
|
if (changed)
|
|
507
596
|
runtime.reload();
|
|
508
597
|
}
|
|
598
|
+
}
|
|
599
|
+
;
|
|
600
|
+
ctx.on('loader/volatile-update', () => {
|
|
601
|
+
applyVolatileUpdate();
|
|
509
602
|
});
|
|
510
603
|
// Event feed HTTP API (best effort): the dsh webServer service exposes the
|
|
511
604
|
// JSONL decision events to the browser half; without it events stay on disk.
|
|
@@ -546,6 +639,10 @@ export function apply(ctx, config = {}) {
|
|
|
546
639
|
const offHealth = registerHealthRoute(webServer, { check: () => runtime.healthCheck() });
|
|
547
640
|
if (offHealth !== undefined)
|
|
548
641
|
ctx.effect(() => () => { offHealth(); }, 'dsh-perm-gate: health route');
|
|
642
|
+
// T12: 设置数据桥(client 卡在 settingsScope 不解析的线上由此取数/写回)
|
|
643
|
+
for (const offBridge of registerSettingsBridgeRoutes(webServer, () => settingsRef.svc, ENTRY_ID)) {
|
|
644
|
+
ctx.effect(() => () => { offBridge(); }, 'dsh-perm-gate: settings bridge route');
|
|
645
|
+
}
|
|
549
646
|
// Network diagnostics: mode, bind, port, proxy liveness, env injection,
|
|
550
647
|
// block counters and recent blocks. Read-only.
|
|
551
648
|
const offNetwork = registerNetworkRoute(webServer, {
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-perm-gate",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.2-compat.7",
|
|
4
4
|
"publishConfig": {
|
|
5
|
-
"tag": "
|
|
5
|
+
"tag": "compat"
|
|
6
6
|
},
|
|
7
7
|
"description": "DSH permission-gate for the DeepSeek Harness 0.1.5 line: a single self-sufficient, deterministic-first, fail-closed gate covering P0 hard-deny -> P1 session grant -> P2 static rule (allow/deny) chain -> P3 optional LLM semantic classifier -> P4 ask, with command whitelist/blacklist.",
|
|
8
8
|
"type": "module",
|
|
@@ -73,7 +73,9 @@
|
|
|
73
73
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
74
74
|
"@deepseek-ai/dsh-client-locale": "0.1.0-rc.2 || 0.1.0-rc.3 || 0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2",
|
|
75
75
|
"@deepseek-ai/dsh-client-ui-renderer": "0.1.0-rc.2 || 0.1.0-rc.3 || 0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2",
|
|
76
|
-
"@deepseek-ai/dsh-client-ui-settings": "0.1.0-rc.2 || 0.1.0-rc.3 || 0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2"
|
|
76
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.1.0-rc.2 || 0.1.0-rc.3 || 0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2",
|
|
77
|
+
"@deepseek-ai/dsh-settings": "0.1.0-rc.2 || 0.1.0-rc.3 || 0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2",
|
|
78
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.1.0-rc.2 || 0.1.0-rc.3 || 0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2"
|
|
77
79
|
},
|
|
78
80
|
"dependencies": {
|
|
79
81
|
"@deepseek-ai/schemastery": "^3.18.4",
|
|
@@ -94,7 +96,25 @@
|
|
|
94
96
|
"tsdown": "^0.22.14",
|
|
95
97
|
"typescript": "^5.6.0",
|
|
96
98
|
"typescript-eslint": "^8.69.0",
|
|
97
|
-
"vitest": "^3.0.0"
|
|
99
|
+
"vitest": "^3.0.0",
|
|
100
|
+
"@deepseek-ai/dsh-settings": "0.1.1-rc.2"
|
|
101
|
+
},
|
|
102
|
+
"scripts": {
|
|
103
|
+
"build": "tsc -p tsconfig.build.json && tsdown",
|
|
104
|
+
"prepack": "npm run build",
|
|
105
|
+
"prepublishOnly": "npm run build",
|
|
106
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.client.json --noEmit",
|
|
107
|
+
"lint": "eslint src test scripts tsdown.config.ts eslint.config.mjs",
|
|
108
|
+
"test": "vitest run",
|
|
109
|
+
"patch:glyph": "node scripts/patch-permission-glyph.mjs",
|
|
110
|
+
"patch:glyph:check": "node scripts/patch-permission-glyph.mjs --check",
|
|
111
|
+
"verify:line": "node scripts/verify-line.mjs new",
|
|
112
|
+
"verify:lines": "node scripts/verify-line.mjs both",
|
|
113
|
+
"check:line": "node scripts/sync-0.1.5-line.mjs check",
|
|
114
|
+
"release:latest": "npm publish --tag latest",
|
|
115
|
+
"release:2x": "npm publish --tag 2.x",
|
|
116
|
+
"release:legacy": "npm publish --tag legacy",
|
|
117
|
+
"release:3x": "npm publish --tag dsh-0.1.5"
|
|
98
118
|
},
|
|
99
119
|
"license": "MIT",
|
|
100
120
|
"repository": {
|
|
@@ -138,21 +158,12 @@
|
|
|
138
158
|
},
|
|
139
159
|
"@deepseek-ai/dsh-client-ui-settings": {
|
|
140
160
|
"optional": true
|
|
161
|
+
},
|
|
162
|
+
"@deepseek-ai/dsh-client-ui-conversation": {
|
|
163
|
+
"optional": true
|
|
164
|
+
},
|
|
165
|
+
"@deepseek-ai/dsh-settings": {
|
|
166
|
+
"optional": true
|
|
141
167
|
}
|
|
142
|
-
},
|
|
143
|
-
"scripts": {
|
|
144
|
-
"build": "tsc -p tsconfig.build.json && tsdown",
|
|
145
|
-
"typecheck": "tsc --noEmit && tsc -p tsconfig.client.json --noEmit",
|
|
146
|
-
"lint": "eslint src test scripts tsdown.config.ts eslint.config.mjs",
|
|
147
|
-
"test": "vitest run",
|
|
148
|
-
"patch:glyph": "node scripts/patch-permission-glyph.mjs",
|
|
149
|
-
"patch:glyph:check": "node scripts/patch-permission-glyph.mjs --check",
|
|
150
|
-
"verify:line": "node scripts/verify-line.mjs new",
|
|
151
|
-
"verify:lines": "node scripts/verify-line.mjs both",
|
|
152
|
-
"check:line": "node scripts/sync-0.1.5-line.mjs check",
|
|
153
|
-
"release:latest": "npm publish --tag latest",
|
|
154
|
-
"release:2x": "npm publish --tag 2.x",
|
|
155
|
-
"release:legacy": "npm publish --tag legacy",
|
|
156
|
-
"release:3x": "npm publish --tag dsh-0.1.5"
|
|
157
168
|
}
|
|
158
|
-
}
|
|
169
|
+
}
|
|
File without changes
|