dsh-permission-rules 0.4.2 → 0.5.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/CHANGELOG.md +17 -0
- package/README.es.md +22 -0
- package/README.hi.md +22 -0
- package/README.md +22 -0
- package/README.pt.md +22 -0
- package/README.zh.md +22 -0
- package/lib/client.js +4696 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +1520 -172
- package/lib/typert.host.js +4305 -0
- package/lib/types/client/index.d.ts +28 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/locales.d.ts +9 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/remote.d.ts +181 -0
- package/lib/types/client/remote.d.ts.map +1 -0
- package/lib/types/client/section.d.ts +35 -0
- package/lib/types/client/section.d.ts.map +1 -0
- package/lib/types/config.d.ts +52 -0
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/events.d.ts +45 -0
- package/lib/types/events.d.ts.map +1 -1
- package/lib/types/index.d.ts +2 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/network.d.ts +90 -0
- package/lib/types/network.d.ts.map +1 -0
- package/lib/types/prose.d.ts +15 -0
- package/lib/types/prose.d.ts.map +1 -1
- package/lib/types/proxy.d.ts +128 -0
- package/lib/types/proxy.d.ts.map +1 -0
- package/lib/types/remote-service.d.ts +45 -0
- package/lib/types/remote-service.d.ts.map +1 -0
- package/lib/types/rules.d.ts +131 -0
- package/lib/types/rules.d.ts.map +1 -1
- package/lib/types/runtime.d.ts +141 -12
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/settings.d.ts +28 -0
- package/lib/types/settings.d.ts.map +1 -0
- package/lib/types/typert.host.d.ts +166 -0
- package/lib/types/typert.host.d.ts.map +1 -0
- package/lib/types/wire.d.ts +421 -0
- package/lib/types/wire.d.ts.map +1 -0
- package/package.json +40 -6
- package/src/client/index.ts +111 -0
- package/src/client/locales.ts +94 -0
- package/src/client/remote.ts +41 -0
- package/src/client/section.ts +201 -0
- package/src/config.ts +111 -0
- package/src/events.ts +46 -0
- package/src/index.ts +2 -0
- package/src/network.ts +177 -0
- package/src/prose.ts +85 -10
- package/src/proxy.ts +327 -0
- package/src/remote-service.ts +109 -0
- package/src/rules.ts +424 -5
- package/src/runtime.ts +454 -46
- package/src/settings.ts +74 -0
- package/src/typert.host.ts +25 -0
- package/src/wire.ts +239 -0
- package/lib/types/config.js +0 -99
- package/lib/types/config.js.map +0 -1
- package/lib/types/events.js +0 -44
- package/lib/types/events.js.map +0 -1
- package/lib/types/glob.js +0 -307
- package/lib/types/glob.js.map +0 -1
- package/lib/types/index.js +0 -27
- package/lib/types/index.js.map +0 -1
- package/lib/types/prose.js +0 -173
- package/lib/types/prose.js.map +0 -1
- package/lib/types/rules.js +0 -610
- package/lib/types/rules.js.map +0 -1
- package/lib/types/runtime.js +0 -818
- package/lib/types/runtime.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-permission-rules",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Declarative Claude Code-style permission rules for DeepSeek Harness: ordered allow/deny/ask rules with tool-name, argument (glob/regex),
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Declarative Claude Code-style permission rules plus a Codex-style process-level network policy for DeepSeek Harness: ordered allow/deny/ask rules with tool-name, argument (glob/regex), workspace-path, and network-target (domain/ip/port/scheme) matching on the tools/pre-execute waterfall, a built-in local HTTP/CONNECT proxy governing shell subprocess traffic, three network modes mapped onto the official sandbox presets, full session-log audit, HMR rule reload, and a settings page with a rule editor, block counters, and recent interception lists.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./lib/index.js",
|
|
@@ -11,6 +11,14 @@
|
|
|
11
11
|
"types": "./lib/types/index.d.ts",
|
|
12
12
|
"default": "./lib/index.js"
|
|
13
13
|
},
|
|
14
|
+
"./typert": {
|
|
15
|
+
"types": "./lib/types/typert.host.d.ts",
|
|
16
|
+
"default": "./lib/typert.host.js"
|
|
17
|
+
},
|
|
18
|
+
"./client": {
|
|
19
|
+
"types": "./lib/types/client/index.d.ts",
|
|
20
|
+
"default": "./lib/client.js"
|
|
21
|
+
},
|
|
14
22
|
"./package.json": "./package.json"
|
|
15
23
|
},
|
|
16
24
|
"files": [
|
|
@@ -31,6 +39,15 @@
|
|
|
31
39
|
"dsh": {
|
|
32
40
|
"bundle": {
|
|
33
41
|
"patch": "./cordis.patch.yml"
|
|
42
|
+
},
|
|
43
|
+
"client": {
|
|
44
|
+
"platform": "web",
|
|
45
|
+
"inject": [
|
|
46
|
+
"@deepseek-ai/dsh-client-connection",
|
|
47
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
48
|
+
"@deepseek-ai/dsh-client-locale",
|
|
49
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
50
|
+
]
|
|
34
51
|
}
|
|
35
52
|
},
|
|
36
53
|
"dshWorkshop": {
|
|
@@ -53,11 +70,13 @@
|
|
|
53
70
|
"permissions": [
|
|
54
71
|
"files:read",
|
|
55
72
|
"files:watch",
|
|
73
|
+
"files:write",
|
|
56
74
|
"session:append",
|
|
57
|
-
"network:
|
|
75
|
+
"network:outbound"
|
|
58
76
|
],
|
|
59
77
|
"compatibility": {
|
|
60
78
|
"dshVersions": [
|
|
79
|
+
"0.1.0-rc.5",
|
|
61
80
|
"0.1.0-rc.6"
|
|
62
81
|
]
|
|
63
82
|
},
|
|
@@ -82,7 +101,10 @@
|
|
|
82
101
|
"policy",
|
|
83
102
|
"allow-deny-ask",
|
|
84
103
|
"approval",
|
|
85
|
-
"safety"
|
|
104
|
+
"safety",
|
|
105
|
+
"network",
|
|
106
|
+
"network-policy",
|
|
107
|
+
"proxy"
|
|
86
108
|
],
|
|
87
109
|
"author": "dsh-permission-rules contributors",
|
|
88
110
|
"repository": {
|
|
@@ -103,22 +125,32 @@
|
|
|
103
125
|
"@deepseek-ai/dsh-llm": "0.1.0-rc.6",
|
|
104
126
|
"@deepseek-ai/dsh-session": "0.1.0-rc.6",
|
|
105
127
|
"@deepseek-ai/dsh-tools": "0.1.0-rc.6",
|
|
106
|
-
"@deepseek-ai/
|
|
128
|
+
"@deepseek-ai/dsh-typert-protocol": "0.1.0-rc.6",
|
|
129
|
+
"@deepseek-ai/schemastery": "^3.18.0",
|
|
130
|
+
"react": "^18.2.0"
|
|
107
131
|
},
|
|
108
132
|
"dependencies": {
|
|
133
|
+
"@types/react": "18.3.31",
|
|
109
134
|
"chokidar": "^5.0.0",
|
|
110
135
|
"tsdown": "^0.22.14",
|
|
111
136
|
"typescript": "^5.9.0",
|
|
112
|
-
"yaml": "^2.8.0"
|
|
137
|
+
"yaml": "^2.8.0",
|
|
138
|
+
"zod": "^4.4.3"
|
|
113
139
|
},
|
|
114
140
|
"devDependencies": {
|
|
115
141
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
116
142
|
"@deepseek-ai/dsh-agent": "0.1.0-rc.6",
|
|
143
|
+
"@deepseek-ai/dsh-client-connection": "0.1.0-rc.6",
|
|
144
|
+
"@deepseek-ai/dsh-client-locale": "0.1.0-rc.6",
|
|
145
|
+
"@deepseek-ai/dsh-client-runtime": "0.1.0-rc.6",
|
|
146
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.1.0-rc.6",
|
|
147
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.0-rc.6",
|
|
117
148
|
"@deepseek-ai/dsh-commands": "0.1.0-rc.6",
|
|
118
149
|
"@deepseek-ai/dsh-llm": "0.1.0-rc.6",
|
|
119
150
|
"@deepseek-ai/dsh-session": "0.1.0-rc.6",
|
|
120
151
|
"@deepseek-ai/dsh-subagent": "0.1.0-rc.6",
|
|
121
152
|
"@deepseek-ai/dsh-tools": "0.1.0-rc.6",
|
|
153
|
+
"@deepseek-ai/dsh-typert-protocol": "0.1.0-rc.6",
|
|
122
154
|
"@deepseek-ai/dsh-user-approval": "0.1.0-rc.6",
|
|
123
155
|
"@deepseek-ai/schemastery": "^3.18.0",
|
|
124
156
|
"@eslint/js": "^10.0.1",
|
|
@@ -127,6 +159,8 @@
|
|
|
127
159
|
"dsh-auto-review": "file:vendor/dsh-auto-review-0.4.0.tgz",
|
|
128
160
|
"eslint": "^10.8.1",
|
|
129
161
|
"globals": "^17.11.0",
|
|
162
|
+
"react": "18.3.1",
|
|
163
|
+
"react-dom": "18.3.1",
|
|
130
164
|
"typescript-eslint": "^8.67.0",
|
|
131
165
|
"vitest": "^3.2.0"
|
|
132
166
|
},
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dsh-permission-rules`, browser half: mounts the `permissionRules`
|
|
3
|
+
* Remote contribution, then registers the "Permission Rules" settings
|
|
4
|
+
* page (`settings.section`, id `permission-rules`) with the network-policy
|
|
5
|
+
* summary, block counters, the recent interception list, and the rule
|
|
6
|
+
* editor. All data arrives through the `remote.permissionRules`
|
|
7
|
+
* namespace — the page issues no other RPC.
|
|
8
|
+
*
|
|
9
|
+
* The slot and remote services are accessed structurally (through
|
|
10
|
+
* `ctx.get`/`ctx.inject`) so this bundle compiles against the published
|
|
11
|
+
* rc.6 client peers without hard value imports beyond the platform
|
|
12
|
+
* modules.
|
|
13
|
+
* @module dsh-permission-rules/client
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
17
|
+
import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
|
|
18
|
+
import { PERMISSION_RULES_REMOTE } from './remote.ts'
|
|
19
|
+
import { PermissionRulesSection } from './section.ts'
|
|
20
|
+
import type { PermissionRulesSectionInjected } from './section.ts'
|
|
21
|
+
import { en, zh, type PermissionRulesLocaleKey } from './locales.ts'
|
|
22
|
+
import type { PermissionRulesSnapshot, RulesReadResult, RulesReloadResult, RulesSaveResult } from '../wire.ts'
|
|
23
|
+
|
|
24
|
+
/** Dictionary namespace owned by this plugin. */
|
|
25
|
+
export const NS = 'settings.permissionRules'
|
|
26
|
+
|
|
27
|
+
/** Plugin name: matches the package name, the graph row id, and the bundle id. */
|
|
28
|
+
export const name = 'dsh-permission-rules'
|
|
29
|
+
|
|
30
|
+
/** Services the page reads; `remote.permissionRules` appears once this plugin mounts its contribution. */
|
|
31
|
+
export const inject = ['slots', 'locale', 'remote']
|
|
32
|
+
|
|
33
|
+
/** The locale registry face (structural; the real `ctx.locale` satisfies it). */
|
|
34
|
+
interface LocaleLike {
|
|
35
|
+
register(namespace: string, dictionaries: { en: Record<string, string>; zh: Record<string, string> }): void
|
|
36
|
+
bind(namespace: string): (key: PermissionRulesLocaleKey, vars?: Record<string, string | number>) => string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The remote-contribution host face (structural; the real `ctx.remote` satisfies it). */
|
|
40
|
+
interface RemoteLike {
|
|
41
|
+
$mount(contribution: unknown): Promise<void>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The slots registry face (structural; the real `ctx.slots` satisfies it). */
|
|
45
|
+
interface SlotsLike {
|
|
46
|
+
inject(slot: string, register: () => unknown): void
|
|
47
|
+
register(options: object, component: unknown): unknown
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Browser plugin body: dictionaries, the Remote contribution mount, and
|
|
52
|
+
* the settings-section registration.
|
|
53
|
+
* @param ctx - client root context.
|
|
54
|
+
*/
|
|
55
|
+
export async function apply(ctx: Context): Promise<void> {
|
|
56
|
+
const locale = ctx.get('locale') as LocaleLike | undefined
|
|
57
|
+
const remote = ctx.get('remote') as RemoteLike | undefined
|
|
58
|
+
const slots = ctx.get('slots') as SlotsLike | undefined
|
|
59
|
+
if (locale === undefined || remote === undefined || slots === undefined) return
|
|
60
|
+
|
|
61
|
+
ctx.effect(() => {
|
|
62
|
+
locale.register(NS, { zh, en })
|
|
63
|
+
return () => {}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// $mount registers the 'remote.permissionRules' namespace service and owns
|
|
67
|
+
// its removal for this fiber's lifetime.
|
|
68
|
+
await remote.$mount(PERMISSION_RULES_REMOTE)
|
|
69
|
+
|
|
70
|
+
ctx.inject(['remote.permissionRules'], (scope) => {
|
|
71
|
+
const t = locale.bind(NS)
|
|
72
|
+
const bridge = scope.get('remote.permissionRules') as {
|
|
73
|
+
networkStatus: () => Promise<RemoteResult<PermissionRulesSnapshot>>
|
|
74
|
+
rulesRead: (path: string) => Promise<RemoteResult<RulesReadResult>>
|
|
75
|
+
rulesSave: (path: string, text: string) => Promise<RemoteResult<RulesSaveResult>>
|
|
76
|
+
reload: () => Promise<RemoteResult<RulesReloadResult>>
|
|
77
|
+
} | undefined
|
|
78
|
+
if (bridge === undefined) return
|
|
79
|
+
const injected = (): PermissionRulesSectionInjected => ({
|
|
80
|
+
status: async () => {
|
|
81
|
+
const result = await bridge.networkStatus()
|
|
82
|
+
if (!result.ok) throw new Error(`networkStatus failed: ${result.error.code}: ${result.error.message}`)
|
|
83
|
+
return result.value
|
|
84
|
+
},
|
|
85
|
+
rulesRead: async (path) => {
|
|
86
|
+
const result = await bridge.rulesRead(path)
|
|
87
|
+
if (!result.ok) throw new Error(`rulesRead failed: ${result.error.code}: ${result.error.message}`)
|
|
88
|
+
return result.value
|
|
89
|
+
},
|
|
90
|
+
rulesSave: async (path, text) => {
|
|
91
|
+
const result = await bridge.rulesSave(path, text)
|
|
92
|
+
if (!result.ok) throw new Error(`rulesSave failed: ${result.error.code}: ${result.error.message}`)
|
|
93
|
+
return result.value
|
|
94
|
+
},
|
|
95
|
+
reload: async () => {
|
|
96
|
+
const result = await bridge.reload()
|
|
97
|
+
if (!result.ok) throw new Error(`reload failed: ${result.error.code}: ${result.error.message}`)
|
|
98
|
+
return result.value
|
|
99
|
+
},
|
|
100
|
+
t,
|
|
101
|
+
})
|
|
102
|
+
slots.inject('settings.section', () => slots.register({
|
|
103
|
+
name: 'settings.section',
|
|
104
|
+
id: 'permission-rules',
|
|
105
|
+
order: 20,
|
|
106
|
+
label: () => t('nav'),
|
|
107
|
+
locale: NS,
|
|
108
|
+
inject: injected,
|
|
109
|
+
}, PermissionRulesSection))
|
|
110
|
+
})
|
|
111
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings-page copy for `dsh-permission-rules`, `en`/`zh` (the two UI
|
|
3
|
+
* language codes the harness locale registry accepts today).
|
|
4
|
+
* @module dsh-permission-rules/client/locales
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type PermissionRulesLocaleKey =
|
|
8
|
+
| 'nav'
|
|
9
|
+
| 'policyTitle'
|
|
10
|
+
| 'mode'
|
|
11
|
+
| 'preset'
|
|
12
|
+
| 'configured'
|
|
13
|
+
| 'proxy'
|
|
14
|
+
| 'proxyActive'
|
|
15
|
+
| 'proxyInactive'
|
|
16
|
+
| 'proxyDisabled'
|
|
17
|
+
| 'counters'
|
|
18
|
+
| 'denied'
|
|
19
|
+
| 'askBlocked'
|
|
20
|
+
| 'recentTitle'
|
|
21
|
+
| 'noBlocks'
|
|
22
|
+
| 'blockBy'
|
|
23
|
+
| 'blockDefault'
|
|
24
|
+
| 'blockUnattributed'
|
|
25
|
+
| 'editorTitle'
|
|
26
|
+
| 'editorPath'
|
|
27
|
+
| 'editorLoad'
|
|
28
|
+
| 'editorSave'
|
|
29
|
+
| 'editorReload'
|
|
30
|
+
| 'editorEmpty'
|
|
31
|
+
| 'editorSaved'
|
|
32
|
+
| 'editorFailed'
|
|
33
|
+
| 'editorUntitled'
|
|
34
|
+
| 'unknown'
|
|
35
|
+
|
|
36
|
+
export const en: Record<PermissionRulesLocaleKey, string> = {
|
|
37
|
+
nav: 'Permission Rules',
|
|
38
|
+
policyTitle: 'Network policy',
|
|
39
|
+
mode: 'Mode',
|
|
40
|
+
preset: 'Sandbox preset',
|
|
41
|
+
configured: 'Configured',
|
|
42
|
+
proxy: 'Proxy',
|
|
43
|
+
proxyActive: 'active on 127.0.0.1:{port}',
|
|
44
|
+
proxyInactive: 'INACTIVE (bind failed — shell network policy is not enforced)',
|
|
45
|
+
proxyDisabled: 'network policy disabled',
|
|
46
|
+
counters: 'Blocks',
|
|
47
|
+
denied: 'denied',
|
|
48
|
+
askBlocked: 'ask-blocked',
|
|
49
|
+
recentTitle: 'Recent blocks',
|
|
50
|
+
noBlocks: 'No network blocks recorded yet.',
|
|
51
|
+
blockBy: 'rule {index}',
|
|
52
|
+
blockDefault: 'mode default',
|
|
53
|
+
blockUnattributed: 'unattributed',
|
|
54
|
+
editorTitle: 'Rule editor',
|
|
55
|
+
editorPath: 'Rule file',
|
|
56
|
+
editorLoad: 'Load',
|
|
57
|
+
editorSave: 'Save',
|
|
58
|
+
editorReload: 'Reload rules',
|
|
59
|
+
editorEmpty: 'Choose a rule file to edit. The editor only touches rule files the plugin already loads.',
|
|
60
|
+
editorSaved: 'Saved and reloaded ({reloaded} workspace(s)).',
|
|
61
|
+
editorFailed: 'Save rejected: {error}',
|
|
62
|
+
editorUntitled: '(new file)',
|
|
63
|
+
unknown: 'Unknown error',
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const zh: Record<PermissionRulesLocaleKey, string> = {
|
|
67
|
+
nav: '权限规则',
|
|
68
|
+
policyTitle: '网络策略',
|
|
69
|
+
mode: '模式',
|
|
70
|
+
preset: '沙箱预设',
|
|
71
|
+
configured: '显式配置',
|
|
72
|
+
proxy: '代理',
|
|
73
|
+
proxyActive: '运行于 127.0.0.1:{port}',
|
|
74
|
+
proxyInactive: '未激活(绑定失败——shell 网络策略未生效)',
|
|
75
|
+
proxyDisabled: '网络策略已停用',
|
|
76
|
+
counters: '拦截',
|
|
77
|
+
denied: '拒绝',
|
|
78
|
+
askBlocked: '待审批阻断',
|
|
79
|
+
recentTitle: '近期拦截',
|
|
80
|
+
noBlocks: '尚未记录任何网络拦截。',
|
|
81
|
+
blockBy: '规则 {index}',
|
|
82
|
+
blockDefault: '模式默认',
|
|
83
|
+
blockUnattributed: '未归属',
|
|
84
|
+
editorTitle: '规则编辑器',
|
|
85
|
+
editorPath: '规则文件',
|
|
86
|
+
editorLoad: '载入',
|
|
87
|
+
editorSave: '保存',
|
|
88
|
+
editorReload: '重载规则',
|
|
89
|
+
editorEmpty: '选择要编辑的规则文件。编辑器只能读写插件已加载的规则文件。',
|
|
90
|
+
editorSaved: '已保存并重载({reloaded} 个工作区)。',
|
|
91
|
+
editorFailed: '保存被拒绝:{error}',
|
|
92
|
+
editorUntitled: '(新文件)',
|
|
93
|
+
unknown: '未知错误',
|
|
94
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The client-side Remote face of the `permissionRules` namespace: the
|
|
3
|
+
* hand-written `TypertRemoteContribution` mounted through
|
|
4
|
+
* `ctx.remote.$mount`, plus the declaration merging that types
|
|
5
|
+
* `ctx.remote.permissionRules`. The descriptor list is shared with the
|
|
6
|
+
* host `./typert` manifest (`../wire.ts`), so the two faces can never
|
|
7
|
+
* drift.
|
|
8
|
+
* @module dsh-permission-rules/client/remote
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { RemoteResult, TypertRemoteContribution } from '@deepseek-ai/dsh-typert-protocol'
|
|
12
|
+
import { PERMISSION_RULES_INVOCATIONS } from '../wire.ts'
|
|
13
|
+
import type { PermissionRulesSnapshot, RulesReadResult, RulesReloadResult, RulesSaveResult } from '../wire.ts'
|
|
14
|
+
|
|
15
|
+
declare module '@deepseek-ai/dsh-typert-protocol' {
|
|
16
|
+
interface TypertRemoteNamespace$permissionRules {
|
|
17
|
+
/** Read the network-policy snapshot (mode, counters, recent blocks, editable rule sources). */
|
|
18
|
+
networkStatus: () => Promise<RemoteResult<PermissionRulesSnapshot>>
|
|
19
|
+
/** Read one known rule file for the editor. */
|
|
20
|
+
rulesRead: (path: string) => Promise<RemoteResult<RulesReadResult>>
|
|
21
|
+
/** Validate and write one known rule file. */
|
|
22
|
+
rulesSave: (path: string, text: string) => Promise<RemoteResult<RulesSaveResult>>
|
|
23
|
+
/** Re-read every cached workspace chain. */
|
|
24
|
+
reload: () => Promise<RemoteResult<RulesReloadResult>>
|
|
25
|
+
}
|
|
26
|
+
interface TypertRemoteMap {
|
|
27
|
+
'permissionRules/networkStatus': () => Promise<RemoteResult<PermissionRulesSnapshot>>
|
|
28
|
+
'permissionRules/rulesRead': (path: string) => Promise<RemoteResult<RulesReadResult>>
|
|
29
|
+
'permissionRules/rulesSave': (path: string, text: string) => Promise<RemoteResult<RulesSaveResult>>
|
|
30
|
+
'permissionRules/reload': () => Promise<RemoteResult<RulesReloadResult>>
|
|
31
|
+
}
|
|
32
|
+
interface TypertRemoteNamespaceMap {
|
|
33
|
+
permissionRules: TypertRemoteNamespace$permissionRules
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The client Remote contribution for the `permissionRules` namespace. */
|
|
38
|
+
export const PERMISSION_RULES_REMOTE = Object.freeze({
|
|
39
|
+
package: 'dsh-permission-rules',
|
|
40
|
+
descriptors: PERMISSION_RULES_INVOCATIONS,
|
|
41
|
+
} satisfies TypertRemoteContribution)
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "Permission Rules" settings page: network-policy summary (mode
|
|
3
|
+
* mapping, proxy liveness), block counters, the recent interception list
|
|
4
|
+
* (source tool + target domain), and the rule editor over known rule
|
|
5
|
+
* sources. All data arrives through the `permissionRules` Remote
|
|
6
|
+
* namespace; the page holds no state beyond the selected file and the
|
|
7
|
+
* draft text. Written with `React.createElement` only — the client
|
|
8
|
+
* bundle is plain JavaScript.
|
|
9
|
+
* @module dsh-permission-rules/client/section
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as React from 'react'
|
|
13
|
+
import type { ReactElement } from 'react'
|
|
14
|
+
import type { PermissionRulesLocaleKey } from './locales.ts'
|
|
15
|
+
import type { PermissionRulesSnapshot, RulesReadResult, RulesReloadResult, RulesSaveResult } from '../wire.ts'
|
|
16
|
+
|
|
17
|
+
/** Functions the registration injects (wired to the Remote namespace in `./index.ts`). */
|
|
18
|
+
export interface PermissionRulesSectionInjected {
|
|
19
|
+
/** Read the network-policy snapshot. */
|
|
20
|
+
status: () => Promise<PermissionRulesSnapshot>
|
|
21
|
+
/** Read one known rule file. */
|
|
22
|
+
rulesRead: (path: string) => Promise<RulesReadResult>
|
|
23
|
+
/** Validate and write one known rule file. */
|
|
24
|
+
rulesSave: (path: string, text: string) => Promise<RulesSaveResult>
|
|
25
|
+
/** Re-read every cached workspace chain. */
|
|
26
|
+
reload: () => Promise<RulesReloadResult>
|
|
27
|
+
/** The locale-bound translator. */
|
|
28
|
+
t: T
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** One bound dictionary. */
|
|
32
|
+
export type T = (key: PermissionRulesLocaleKey, vars?: Record<string, string | number>) => string
|
|
33
|
+
|
|
34
|
+
/** The shell supplies `close`; everything else arrives through the slot injection. */
|
|
35
|
+
export type PermissionRulesSectionProps = PermissionRulesSectionInjected & {
|
|
36
|
+
close: () => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** One select option for the rule-file picker. */
|
|
40
|
+
interface SourceOption {
|
|
41
|
+
path: string
|
|
42
|
+
label: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const HEADER: React.CSSProperties = { fontSize: 14, fontWeight: 700, margin: '16px 0 8px' }
|
|
46
|
+
const ROW: React.CSSProperties = { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }
|
|
47
|
+
const LABEL: React.CSSProperties = { fontSize: 13, color: 'var(--dsh-color-text-secondary, #666)', minWidth: 110 }
|
|
48
|
+
const VALUE: React.CSSProperties = { fontSize: 13 }
|
|
49
|
+
const MONO: React.CSSProperties = { fontFamily: 'var(--dsh-font-mono, monospace)', fontSize: 12 }
|
|
50
|
+
const TEXTAREA: React.CSSProperties = { width: '100%', minHeight: 260, fontFamily: 'var(--dsh-font-mono, monospace)', fontSize: 12, whiteSpace: 'pre', overflow: 'auto' }
|
|
51
|
+
const NOTICE_OK: React.CSSProperties = { fontSize: 13, color: 'var(--dsh-color-success, #1a7f37)' }
|
|
52
|
+
const NOTICE_ERR: React.CSSProperties = { fontSize: 13, color: 'var(--dsh-color-error, #c62828)' }
|
|
53
|
+
const TABLE: React.CSSProperties = { width: '100%', borderCollapse: 'collapse', fontSize: 12 }
|
|
54
|
+
const CELL: React.CSSProperties = { borderBottom: '1px solid var(--dsh-color-border, #ddd)', padding: '4px 6px', textAlign: 'left', verticalAlign: 'top' }
|
|
55
|
+
|
|
56
|
+
/** The settings section component. */
|
|
57
|
+
export function PermissionRulesSection({ close, t, ...injected }: PermissionRulesSectionProps): ReactElement {
|
|
58
|
+
void close
|
|
59
|
+
const [snapshot, setSnapshot] = React.useState<PermissionRulesSnapshot | undefined>(undefined)
|
|
60
|
+
const [loadError, setLoadError] = React.useState<string | undefined>(undefined)
|
|
61
|
+
const [sources, setSources] = React.useState<SourceOption[]>([])
|
|
62
|
+
const [selected, setSelected] = React.useState<string | undefined>(undefined)
|
|
63
|
+
const [text, setText] = React.useState('')
|
|
64
|
+
const [dirty, setDirty] = React.useState(false)
|
|
65
|
+
const [notice, setNotice] = React.useState<{ ok: boolean; text: string } | undefined>(undefined)
|
|
66
|
+
|
|
67
|
+
const refresh = React.useCallback(() => {
|
|
68
|
+
void injected.status().then(next => {
|
|
69
|
+
setSnapshot(next)
|
|
70
|
+
setLoadError(undefined)
|
|
71
|
+
setSources(next.sources.map(source => ({
|
|
72
|
+
path: source.path,
|
|
73
|
+
label: source.cwd === null ? source.path : source.path.replace(source.cwd, '.'),
|
|
74
|
+
})))
|
|
75
|
+
setSelected(current => current === undefined && next.sources.length > 0 ? next.sources[0]?.path : current)
|
|
76
|
+
}).catch(error => {
|
|
77
|
+
setLoadError(error instanceof Error ? error.message : String(error))
|
|
78
|
+
})
|
|
79
|
+
}, [injected.status])
|
|
80
|
+
|
|
81
|
+
React.useEffect(() => {
|
|
82
|
+
refresh()
|
|
83
|
+
const timer = setInterval(refresh, 5000)
|
|
84
|
+
return () => clearInterval(timer)
|
|
85
|
+
}, [refresh])
|
|
86
|
+
|
|
87
|
+
const loadFile = React.useCallback((path: string) => {
|
|
88
|
+
setSelected(path)
|
|
89
|
+
setDirty(false)
|
|
90
|
+
setNotice(undefined)
|
|
91
|
+
void injected.rulesRead(path).then(result => {
|
|
92
|
+
setText(result.error ?? result.text)
|
|
93
|
+
if (result.error !== null && result.error !== undefined) setNotice({ ok: false, text: result.error })
|
|
94
|
+
})
|
|
95
|
+
}, [injected])
|
|
96
|
+
|
|
97
|
+
const saveFile = React.useCallback(() => {
|
|
98
|
+
if (selected === undefined || !dirty) return
|
|
99
|
+
setNotice(undefined)
|
|
100
|
+
void injected.rulesSave(selected, text).then(result => {
|
|
101
|
+
setNotice(result.ok
|
|
102
|
+
? { ok: true, text: t('editorSaved', { reloaded: result.reloaded ?? 0 }) }
|
|
103
|
+
: { ok: false, text: t('editorFailed', { error: result.error ?? t('unknown') }) })
|
|
104
|
+
if (result.ok) {
|
|
105
|
+
setDirty(false)
|
|
106
|
+
refresh()
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
}, [injected, selected, text, dirty, t, refresh])
|
|
110
|
+
|
|
111
|
+
const reloadAll = React.useCallback(() => {
|
|
112
|
+
setNotice(undefined)
|
|
113
|
+
void injected.reload().then(result => {
|
|
114
|
+
setNotice(result.ok
|
|
115
|
+
? { ok: true, text: t('editorSaved', { reloaded: 0 }) }
|
|
116
|
+
: { ok: false, text: t('editorFailed', { error: result.error ?? t('unknown') }) })
|
|
117
|
+
refresh()
|
|
118
|
+
})
|
|
119
|
+
}, [injected, t, refresh])
|
|
120
|
+
|
|
121
|
+
const picker = React.createElement('select', {
|
|
122
|
+
style: { ...MONO, maxWidth: 420 },
|
|
123
|
+
value: selected ?? '',
|
|
124
|
+
onChange: (event: React.ChangeEvent<HTMLSelectElement>) => {
|
|
125
|
+
const path = event.target.value
|
|
126
|
+
if (path !== '') loadFile(path)
|
|
127
|
+
},
|
|
128
|
+
}, sources.map(source => React.createElement('option', { key: source.path, value: source.path }, source.label)))
|
|
129
|
+
|
|
130
|
+
const children: React.ReactNode[] = []
|
|
131
|
+
|
|
132
|
+
// Policy summary.
|
|
133
|
+
children.push(React.createElement('div', { key: 'policy-title', style: HEADER }, t('policyTitle')))
|
|
134
|
+
if (snapshot === undefined) {
|
|
135
|
+
children.push(React.createElement('div', { key: 'loading', style: VALUE }, loadError === undefined ? '…' : loadError))
|
|
136
|
+
} else {
|
|
137
|
+
const modeText = `${snapshot.mode}${snapshot.sandboxMode === null ? '' : ` (${t('preset')}: ${snapshot.sandboxMode})`}${snapshot.configuredMode === 'auto' ? '' : ` — ${t('configured')}: ${snapshot.configuredMode}`}`
|
|
138
|
+
children.push(React.createElement('div', { key: 'mode', style: ROW },
|
|
139
|
+
React.createElement('span', { style: LABEL }, t('mode')),
|
|
140
|
+
React.createElement('span', { style: VALUE }, modeText)))
|
|
141
|
+
const proxyText = snapshot.enabled
|
|
142
|
+
? snapshot.proxyActive ? t('proxyActive', { port: snapshot.proxyPort }) : t('proxyInactive')
|
|
143
|
+
: t('proxyDisabled')
|
|
144
|
+
children.push(React.createElement('div', { key: 'proxy', style: ROW },
|
|
145
|
+
React.createElement('span', { style: LABEL }, t('proxy')),
|
|
146
|
+
React.createElement('span', { style: VALUE }, proxyText)))
|
|
147
|
+
children.push(React.createElement('div', { key: 'counters', style: ROW },
|
|
148
|
+
React.createElement('span', { style: LABEL }, t('counters')),
|
|
149
|
+
React.createElement('span', { style: VALUE }, `${snapshot.denied} ${t('denied')} · ${snapshot.askBlocked} ${t('askBlocked')}`)))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Recent blocks.
|
|
153
|
+
children.push(React.createElement('div', { key: 'recent-title', style: HEADER }, t('recentTitle')))
|
|
154
|
+
if (snapshot === undefined || snapshot.recent.length === 0) {
|
|
155
|
+
children.push(React.createElement('div', { key: 'no-blocks', style: VALUE }, t('noBlocks')))
|
|
156
|
+
} else {
|
|
157
|
+
const rows = snapshot.recent.map(block => React.createElement('tr', { key: `${block.time}-${block.domain}` },
|
|
158
|
+
React.createElement('td', { style: CELL }, new Date(block.time).toLocaleTimeString()),
|
|
159
|
+
React.createElement('td', { style: CELL }, block.tool + (block.attributed ? '' : ` (${t('blockUnattributed')})`)),
|
|
160
|
+
React.createElement('td', { style: CELL }, `${block.scheme ?? '?'}://${block.domain}${block.port === null ? '' : `:${block.port}`}`),
|
|
161
|
+
React.createElement('td', { style: CELL }, block.matched
|
|
162
|
+
? t('blockBy', { index: (block.ruleIndex ?? 0) + 1 })
|
|
163
|
+
: t('blockDefault')),
|
|
164
|
+
React.createElement('td', { style: CELL }, block.action),
|
|
165
|
+
React.createElement('td', { style: CELL }, block.reason ?? '')))
|
|
166
|
+
children.push(React.createElement('table', { key: 'recent-table', style: TABLE },
|
|
167
|
+
React.createElement('tbody', undefined, rows)))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Rule editor.
|
|
171
|
+
children.push(React.createElement('div', { key: 'editor-title', style: HEADER }, t('editorTitle')))
|
|
172
|
+
children.push(React.createElement('div', { key: 'editor-picker', style: ROW },
|
|
173
|
+
React.createElement('span', { style: LABEL }, t('editorPath')),
|
|
174
|
+
picker,
|
|
175
|
+
React.createElement('button', { type: 'button', onClick: () => { if (selected !== undefined) loadFile(selected) } }, t('editorLoad')),
|
|
176
|
+
React.createElement('button', { type: 'button', onClick: reloadAll }, t('editorReload'))))
|
|
177
|
+
if (selected === undefined) {
|
|
178
|
+
children.push(React.createElement('div', { key: 'editor-empty', style: VALUE }, t('editorEmpty')))
|
|
179
|
+
} else {
|
|
180
|
+
children.push(React.createElement('textarea', {
|
|
181
|
+
key: 'editor-text',
|
|
182
|
+
style: TEXTAREA,
|
|
183
|
+
value: text,
|
|
184
|
+
spellCheck: false,
|
|
185
|
+
onChange: (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
186
|
+
setText(event.target.value)
|
|
187
|
+
setDirty(true)
|
|
188
|
+
setNotice(undefined)
|
|
189
|
+
},
|
|
190
|
+
}))
|
|
191
|
+
children.push(React.createElement('div', { key: 'editor-actions', style: { ...ROW, marginTop: 8 } },
|
|
192
|
+
React.createElement('button', { type: 'button', disabled: !dirty, onClick: saveFile }, t('editorSave')),
|
|
193
|
+
dirty === true ? React.createElement('span', { style: VALUE }, '●') : null,
|
|
194
|
+
notice === undefined ? null : React.createElement('span', {
|
|
195
|
+
key: 'notice',
|
|
196
|
+
style: notice.ok ? NOTICE_OK : NOTICE_ERR,
|
|
197
|
+
}, notice.text)))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return React.createElement('div', { style: { padding: '0 4px 24px' } }, ...children)
|
|
201
|
+
}
|