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/src/config.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
import { isAbsolute } from 'node:path'
|
|
10
10
|
import z from '@deepseek-ai/schemastery'
|
|
11
11
|
import type { PatternMode } from './rules.ts'
|
|
12
|
+
import type { NetworkMode, UnlistedAction } from './network.ts'
|
|
13
|
+
import { NETWORK_MODES } from './network.ts'
|
|
12
14
|
import type { UiLanguage } from './prose.ts'
|
|
13
15
|
|
|
14
16
|
/** What happens when a discovered rule file exists but cannot be parsed or compiled. */
|
|
@@ -17,6 +19,44 @@ export type BadFilePolicy = 'fail' | 'ignore-with-warning'
|
|
|
17
19
|
/** Which decisions are audit-logged: every call, or only rule hits. */
|
|
18
20
|
export type AuditGranularity = 'all' | 'hits'
|
|
19
21
|
|
|
22
|
+
/** Loopback-target handling at the proxy layer. */
|
|
23
|
+
export type LoopbackPolicy = 'allow' | 'policy'
|
|
24
|
+
|
|
25
|
+
/** NO_PROXY handling for policy-injected subprocess environment. */
|
|
26
|
+
export type NoProxyPolicy = 'clear' | 'preserve'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The process-level network policy block. Every field is optional —
|
|
30
|
+
* {@link ResolvedConfig} supplies the defaults. `mode: 'auto'` maps the
|
|
31
|
+
* official sandbox preset onto the three network modes (read-only →
|
|
32
|
+
* deny-all, workspace-write → whitelist, danger-full-access → allow-all);
|
|
33
|
+
* on hosts without the sandbox-policy service `auto` resolves to
|
|
34
|
+
* `autoFallback` (`allow-all` by default, so pre-network hosts keep their
|
|
35
|
+
* permissive behavior until configured).
|
|
36
|
+
*/
|
|
37
|
+
export interface NetworkConfig {
|
|
38
|
+
/** Master switch: `false` disables the proxy, the env injection, and the web-tool mode defaults. */
|
|
39
|
+
enabled?: boolean
|
|
40
|
+
/** Policy mode: `auto` follows the sandbox preset, or an explicit mode. */
|
|
41
|
+
mode?: 'auto' | NetworkMode
|
|
42
|
+
/** Mode used when `mode: 'auto'` and the sandbox-policy service is absent or unknown. */
|
|
43
|
+
autoFallback?: NetworkMode
|
|
44
|
+
/** Whitelist-mode handling of targets no rule matched: `ask` (default) or `deny`. */
|
|
45
|
+
unlisted?: UnlistedAction
|
|
46
|
+
/** Local proxy bind address (loopback only — the proxy never listens publicly). */
|
|
47
|
+
proxyBind?: string
|
|
48
|
+
/** Local proxy port; `0` picks a free ephemeral port. */
|
|
49
|
+
proxyPort?: number
|
|
50
|
+
/** Cap on recent-block records kept for the settings page. */
|
|
51
|
+
proxyMaxRecent?: number
|
|
52
|
+
/** Loopback targets: `allow` (default, Codex parity) or `policy` (evaluated like any target). */
|
|
53
|
+
loopback?: LoopbackPolicy
|
|
54
|
+
/** Whether proxy environment variables are injected for subprocesses. */
|
|
55
|
+
injectEnv?: boolean
|
|
56
|
+
/** Subprocess NO_PROXY handling: `clear` enforces the policy (default) or `preserve` keeps ambient values. */
|
|
57
|
+
noProxy?: NoProxyPolicy
|
|
58
|
+
}
|
|
59
|
+
|
|
20
60
|
/** Raw plugin config — every field optional; {@link Config} supplies the defaults. */
|
|
21
61
|
export interface Config {
|
|
22
62
|
/**
|
|
@@ -75,6 +115,22 @@ export interface Config {
|
|
|
75
115
|
* `scripts/repair-session-logs.mjs` before loading on a newer harness).
|
|
76
116
|
*/
|
|
77
117
|
allowUnmarkedAudit?: boolean
|
|
118
|
+
/** Process-level network policy (all optional; defaults inside). */
|
|
119
|
+
network?: NetworkConfig
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Network config after {@link resolveConfig}: every optional field has its explicit default. */
|
|
123
|
+
export interface ResolvedNetworkConfig {
|
|
124
|
+
readonly enabled: boolean
|
|
125
|
+
readonly mode: 'auto' | NetworkMode
|
|
126
|
+
readonly autoFallback: NetworkMode
|
|
127
|
+
readonly unlisted: UnlistedAction
|
|
128
|
+
readonly proxyBind: string
|
|
129
|
+
readonly proxyPort: number
|
|
130
|
+
readonly proxyMaxRecent: number
|
|
131
|
+
readonly loopback: LoopbackPolicy
|
|
132
|
+
readonly injectEnv: boolean
|
|
133
|
+
readonly noProxy: NoProxyPolicy
|
|
78
134
|
}
|
|
79
135
|
|
|
80
136
|
/** Config after {@link resolveConfig}: every optional field has its explicit default. */
|
|
@@ -94,6 +150,7 @@ export interface ResolvedConfig {
|
|
|
94
150
|
readonly maxGlobStars: number
|
|
95
151
|
readonly enforce: boolean
|
|
96
152
|
readonly allowUnmarkedAudit: boolean
|
|
153
|
+
readonly network: ResolvedNetworkConfig
|
|
97
154
|
}
|
|
98
155
|
|
|
99
156
|
/** Schemastery schema: the loader validates and fills defaults before `apply`. */
|
|
@@ -113,6 +170,18 @@ export const Config: z<Config> = z.object({
|
|
|
113
170
|
maxGlobStars: z.number().default(2),
|
|
114
171
|
enforce: z.boolean().default(true),
|
|
115
172
|
allowUnmarkedAudit: z.boolean().default(false),
|
|
173
|
+
network: z.object({
|
|
174
|
+
enabled: z.boolean().default(true),
|
|
175
|
+
mode: z.union(['auto', ...NETWORK_MODES] as const).default('auto'),
|
|
176
|
+
autoFallback: z.union(NETWORK_MODES as [NetworkMode, ...NetworkMode[]]).default('allow-all'),
|
|
177
|
+
unlisted: z.union(['ask', 'deny'] as const).default('ask'),
|
|
178
|
+
proxyBind: z.string().default('127.0.0.1'),
|
|
179
|
+
proxyPort: z.number().default(0),
|
|
180
|
+
proxyMaxRecent: z.number().default(100),
|
|
181
|
+
loopback: z.union(['allow', 'policy'] as const).default('allow'),
|
|
182
|
+
injectEnv: z.boolean().default(true),
|
|
183
|
+
noProxy: z.union(['clear', 'preserve'] as const).default('clear'),
|
|
184
|
+
}),
|
|
116
185
|
})
|
|
117
186
|
|
|
118
187
|
/**
|
|
@@ -156,6 +225,7 @@ export function resolveConfig(config: Config = {}): ResolvedConfig {
|
|
|
156
225
|
assertBoolean('caseInsensitivePaths', config.caseInsensitivePaths ?? process.platform === 'win32')
|
|
157
226
|
assertBoolean('enforce', config.enforce ?? true)
|
|
158
227
|
assertBoolean('allowUnmarkedAudit', config.allowUnmarkedAudit ?? false)
|
|
228
|
+
const network = resolveNetworkConfig(config.network)
|
|
159
229
|
return {
|
|
160
230
|
rulesFile,
|
|
161
231
|
fallbackPath: config.fallbackPath,
|
|
@@ -172,6 +242,47 @@ export function resolveConfig(config: Config = {}): ResolvedConfig {
|
|
|
172
242
|
maxGlobStars,
|
|
173
243
|
enforce: config.enforce ?? true,
|
|
174
244
|
allowUnmarkedAudit: config.allowUnmarkedAudit ?? false,
|
|
245
|
+
network,
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Validate and default the optional `network` block; bad values fail the mount loudly. */
|
|
250
|
+
function resolveNetworkConfig(raw: NetworkConfig | undefined): ResolvedNetworkConfig {
|
|
251
|
+
const mode = raw?.mode ?? 'auto'
|
|
252
|
+
if (mode !== 'auto') assertEnum('network.mode', mode, NETWORK_MODES)
|
|
253
|
+
const autoFallback = raw?.autoFallback ?? 'allow-all'
|
|
254
|
+
assertEnum('network.autoFallback', autoFallback, NETWORK_MODES)
|
|
255
|
+
const unlisted = raw?.unlisted ?? 'ask'
|
|
256
|
+
assertEnum('network.unlisted', unlisted, ['ask', 'deny'])
|
|
257
|
+
const loopback = raw?.loopback ?? 'allow'
|
|
258
|
+
assertEnum('network.loopback', loopback, ['allow', 'policy'])
|
|
259
|
+
const noProxy = raw?.noProxy ?? 'clear'
|
|
260
|
+
assertEnum('network.noProxy', noProxy, ['clear', 'preserve'])
|
|
261
|
+
assertBoolean('network.enabled', raw?.enabled ?? true)
|
|
262
|
+
assertBoolean('network.injectEnv', raw?.injectEnv ?? true)
|
|
263
|
+
const proxyBind = raw?.proxyBind ?? '127.0.0.1'
|
|
264
|
+
if (typeof proxyBind !== 'string' || proxyBind.trim().length === 0) {
|
|
265
|
+
throw new TypeError(`network.proxyBind must be a non-empty string, got ${typeof proxyBind}`)
|
|
266
|
+
}
|
|
267
|
+
const proxyPort = raw?.proxyPort ?? 0
|
|
268
|
+
if (!Number.isSafeInteger(proxyPort) || proxyPort < 0 || proxyPort > 65535) {
|
|
269
|
+
throw new TypeError(`network.proxyPort must be an integer 0-65535, got ${String(raw?.proxyPort)}`)
|
|
270
|
+
}
|
|
271
|
+
const proxyMaxRecent = raw?.proxyMaxRecent ?? 100
|
|
272
|
+
if (!Number.isSafeInteger(proxyMaxRecent) || proxyMaxRecent <= 0) {
|
|
273
|
+
throw new TypeError(`network.proxyMaxRecent must be a positive safe integer, got ${String(raw?.proxyMaxRecent)}`)
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
enabled: raw?.enabled ?? true,
|
|
277
|
+
mode: mode as 'auto' | NetworkMode,
|
|
278
|
+
autoFallback,
|
|
279
|
+
unlisted,
|
|
280
|
+
proxyBind: proxyBind.trim(),
|
|
281
|
+
proxyPort,
|
|
282
|
+
proxyMaxRecent,
|
|
283
|
+
loopback,
|
|
284
|
+
injectEnv: raw?.injectEnv ?? true,
|
|
285
|
+
noProxy,
|
|
175
286
|
}
|
|
176
287
|
}
|
|
177
288
|
|
package/src/events.ts
CHANGED
|
@@ -57,6 +57,34 @@ declare module '@deepseek-ai/dsh-session/types' {
|
|
|
57
57
|
cwd: string
|
|
58
58
|
dryRun?: true
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* One process-level network block (proxy layer), appended when the
|
|
62
|
+
* block can be attributed to a session — log-only audit with the same
|
|
63
|
+
* `ignorable` marker discipline as `permissionRules/decision`. `tool`
|
|
64
|
+
* is the attributed shell tool (`bash`/`pwsh`) or `'subprocess'` when
|
|
65
|
+
* no in-flight shell execution could be named; `attributed` records
|
|
66
|
+
* which case. `domain`/`scheme`/`port` name the blocked target,
|
|
67
|
+
* `action` is `deny` or `ask` (an ask at the proxy cannot ride the
|
|
68
|
+
* approval seam — no session context — and blocks with a message),
|
|
69
|
+
* `mode` is the network policy mode in effect, and `ruleIndex`/`reason`
|
|
70
|
+
* appear only on rule hits.
|
|
71
|
+
*/
|
|
72
|
+
'permissionRules/network': {
|
|
73
|
+
kind: 'block'
|
|
74
|
+
tool: string
|
|
75
|
+
attributed: boolean
|
|
76
|
+
callId?: CallId
|
|
77
|
+
domain: string
|
|
78
|
+
scheme?: 'http' | 'https'
|
|
79
|
+
port?: number
|
|
80
|
+
action: 'deny' | 'ask'
|
|
81
|
+
mode: string
|
|
82
|
+
matched: boolean
|
|
83
|
+
source: string
|
|
84
|
+
ruleIndex?: number
|
|
85
|
+
reason?: string
|
|
86
|
+
time: number
|
|
87
|
+
}
|
|
60
88
|
}
|
|
61
89
|
}
|
|
62
90
|
|
|
@@ -81,6 +109,24 @@ export interface AuditDecision {
|
|
|
81
109
|
dryRun?: true
|
|
82
110
|
}
|
|
83
111
|
|
|
112
|
+
/** The payload of one `permissionRules/network` audit record. */
|
|
113
|
+
export interface AuditNetworkBlock {
|
|
114
|
+
kind: 'block'
|
|
115
|
+
tool: string
|
|
116
|
+
attributed: boolean
|
|
117
|
+
callId?: CallId
|
|
118
|
+
domain: string
|
|
119
|
+
scheme?: 'http' | 'https'
|
|
120
|
+
port?: number
|
|
121
|
+
action: 'deny' | 'ask'
|
|
122
|
+
mode: string
|
|
123
|
+
matched: boolean
|
|
124
|
+
source: string
|
|
125
|
+
ruleIndex?: number
|
|
126
|
+
reason?: string
|
|
127
|
+
time: number
|
|
128
|
+
}
|
|
129
|
+
|
|
84
130
|
/**
|
|
85
131
|
* `Session.append` narrowed to this plugin's audit event. The options bag
|
|
86
132
|
* exists only on host builds that expose the `ignorable` envelope-marker
|
package/src/index.ts
CHANGED
package/src/network.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure network-policy orchestration shared by the `tools/pre-execute` gate
|
|
3
|
+
* and the local HTTP/CONNECT proxy: the three policy modes mapped onto the
|
|
4
|
+
* official sandbox presets, proxy-layer first-match evaluation over the
|
|
5
|
+
* loaded rule chains, and the mode-default fallback decision. No I/O, no
|
|
6
|
+
* process state — unit-testable and replayable.
|
|
7
|
+
* @module dsh-permission-rules/network
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CompiledRule, CompiledRuleset, NetworkTarget } from './rules.ts'
|
|
11
|
+
import { matchWhen, matchTools, targetMatchesNetwork } from './rules.ts'
|
|
12
|
+
import type { MatchContext } from './rules.ts'
|
|
13
|
+
|
|
14
|
+
/** The three network policy modes. */
|
|
15
|
+
export type NetworkMode = 'deny-all' | 'whitelist' | 'allow-all'
|
|
16
|
+
|
|
17
|
+
/** Closed mode list for runtime normalization of untrusted values. */
|
|
18
|
+
export const NETWORK_MODES: readonly NetworkMode[] = ['deny-all', 'whitelist', 'allow-all']
|
|
19
|
+
|
|
20
|
+
/** The official file-sandbox presets the `auto` mode maps onto. */
|
|
21
|
+
export type SandboxModeName = 'read-only' | 'workspace-write' | 'danger-full-access'
|
|
22
|
+
|
|
23
|
+
/** How a whitelist-mode unlisted target is handled: ask (blocked at the proxy, approval seam for web tools) or deny. */
|
|
24
|
+
export type UnlistedAction = 'ask' | 'deny'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Map one official sandbox preset onto its network mode, Codex-style:
|
|
28
|
+
* `read-only` defaults to no network (whitelist only), `workspace-write`
|
|
29
|
+
* keeps a whitelist with ask/deny for unlisted targets, and
|
|
30
|
+
* `danger-full-access` opens everything (rules can still narrow it). Any
|
|
31
|
+
* other value falls back to `fallback` — hosts without the sandbox-policy
|
|
32
|
+
* service therefore stay permissive, preserving pre-network behavior.
|
|
33
|
+
* @param sandbox - the resolved sandbox mode, or undefined.
|
|
34
|
+
* @param fallback - the mode used when the preset is unknown/absent.
|
|
35
|
+
* @returns the effective network mode.
|
|
36
|
+
*/
|
|
37
|
+
export function networkModeForSandbox(sandbox: string | undefined, fallback: NetworkMode): NetworkMode {
|
|
38
|
+
switch (sandbox) {
|
|
39
|
+
case 'read-only': return 'deny-all'
|
|
40
|
+
case 'workspace-write': return 'whitelist'
|
|
41
|
+
case 'danger-full-access': return 'allow-all'
|
|
42
|
+
default: return fallback
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** One rule chain the proxy evaluates (ruleset + its source-file paths for attribution). */
|
|
47
|
+
export interface NetworkChain {
|
|
48
|
+
readonly ruleset: CompiledRuleset
|
|
49
|
+
readonly sources: readonly string[]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Proxy-layer evaluation options. */
|
|
53
|
+
export interface NetworkDecisionOptions {
|
|
54
|
+
/** The policy mode in effect (already resolved by the runtime). */
|
|
55
|
+
readonly mode: NetworkMode
|
|
56
|
+
/** Whitelist-mode handling of unlisted targets. */
|
|
57
|
+
readonly unlisted: UnlistedAction
|
|
58
|
+
/** `allow` short-circuits loopback targets before rules; `policy` evaluates them normally. */
|
|
59
|
+
readonly loopback: 'allow' | 'policy'
|
|
60
|
+
/** Host facts for `when` evaluation (proxy has no agent identity). */
|
|
61
|
+
readonly context?: MatchContext
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** One network decision: a rule hit or the mode-default fallback. */
|
|
65
|
+
export interface NetworkDecision {
|
|
66
|
+
readonly action: 'allow' | 'deny' | 'ask'
|
|
67
|
+
/** Whether a rule (true) or the mode default (false) produced the decision. */
|
|
68
|
+
readonly matched: boolean
|
|
69
|
+
/** The mode the fallback was computed from. */
|
|
70
|
+
readonly mode: NetworkMode
|
|
71
|
+
readonly ruleIndex?: number
|
|
72
|
+
readonly rule?: CompiledRule
|
|
73
|
+
/** Source file of the matched rule, for audit attribution. */
|
|
74
|
+
readonly source?: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The default decision a mode yields for a target no rule matched. */
|
|
78
|
+
export function defaultDecision(mode: NetworkMode, unlisted: UnlistedAction): NetworkDecision {
|
|
79
|
+
switch (mode) {
|
|
80
|
+
case 'deny-all':
|
|
81
|
+
return { action: 'deny', matched: false, mode }
|
|
82
|
+
case 'whitelist':
|
|
83
|
+
return { action: unlisted, matched: false, mode }
|
|
84
|
+
case 'allow-all':
|
|
85
|
+
return { action: 'allow', matched: false, mode }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Evaluate the network policy for one target across the loaded rule
|
|
91
|
+
* chains (nearest workspace first, first match wins). Proxy-layer tool
|
|
92
|
+
* attribution: a shell subprocess connection counts as `bash`/`pwsh`
|
|
93
|
+
* candidates, so a rule scoped to either shell tool can fire and one
|
|
94
|
+
* scoped to web tools never does. Agent identity is unknown at the proxy,
|
|
95
|
+
* so agent-scoped network rules fail closed there (the pre-execute path
|
|
96
|
+
* is where agents are known). Loopback targets short-circuit before any
|
|
97
|
+
* rule when `loopback: 'allow'` (Codex parity: local dev servers stay
|
|
98
|
+
* reachable in the read-only default).
|
|
99
|
+
* @param chains - the loaded chains, insertion (nearest) order first.
|
|
100
|
+
* @param target - the parsed connection/URL target.
|
|
101
|
+
* @param options - mode, unlisted handling, loopback policy, host facts.
|
|
102
|
+
* @returns the decision.
|
|
103
|
+
*/
|
|
104
|
+
export function decideNetworkTarget(chains: readonly NetworkChain[], target: NetworkTarget, options: NetworkDecisionOptions): NetworkDecision {
|
|
105
|
+
if (options.loopback === 'allow' && isLoopbackTarget(target)) {
|
|
106
|
+
return { action: 'allow', matched: false, mode: options.mode }
|
|
107
|
+
}
|
|
108
|
+
const context = options.context ?? {}
|
|
109
|
+
for (const chain of chains) {
|
|
110
|
+
for (const rule of chain.ruleset.rules) {
|
|
111
|
+
if (!rule.enabled) continue
|
|
112
|
+
if (rule.network === undefined) continue
|
|
113
|
+
if (!matchToolsProxy(rule)) continue
|
|
114
|
+
if (rule.when !== undefined && !matchWhen(rule, context)) continue
|
|
115
|
+
if (!targetMatchesNetwork(target, rule.network)) continue
|
|
116
|
+
return {
|
|
117
|
+
action: rule.action,
|
|
118
|
+
matched: true,
|
|
119
|
+
mode: options.mode,
|
|
120
|
+
ruleIndex: rule.index,
|
|
121
|
+
rule,
|
|
122
|
+
source: chain.sources[rule.sourceIndex] ?? '',
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return defaultDecision(options.mode, options.unlisted)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Tool-scope check for proxy traffic: shell subprocess connections carry
|
|
131
|
+
* the `bash`/`pwsh` identity candidates (ANY match satisfies, like the
|
|
132
|
+
* agents dimension); rules scoped to other tools never fire here.
|
|
133
|
+
*/
|
|
134
|
+
function matchToolsProxy(rule: CompiledRule): boolean {
|
|
135
|
+
if (rule.tools.length === 0) return true
|
|
136
|
+
return matchTools(rule, 'bash') || matchTools(rule, 'pwsh')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Whether a target addresses the loopback range. */
|
|
140
|
+
export function isLoopbackTarget(target: NetworkTarget): boolean {
|
|
141
|
+
const host = target.host
|
|
142
|
+
if (host === 'localhost' || host === '::1' || host === '0:0:0:0:0:0:0:1') return true
|
|
143
|
+
const parts = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host)
|
|
144
|
+
if (parts === null) return false
|
|
145
|
+
return parts.slice(1).every(octet => Number(octet) <= 255) && parts[1] === '127'
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The short structured message a blocked proxy connection receives (403
|
|
150
|
+
* body for plain HTTP, CONNECT error body for tunnels). Clients such as
|
|
151
|
+
* curl print it verbatim, so the model sees the reason through the shell
|
|
152
|
+
* tool output. An `ask` at the proxy layer is worded as a pending-approval
|
|
153
|
+
* block because the proxy has no session context for the interactive
|
|
154
|
+
* approval seam.
|
|
155
|
+
* @param decision - the decision that blocked the connection.
|
|
156
|
+
* @returns a short text/plain message.
|
|
157
|
+
*/
|
|
158
|
+
export function blockMessage(decision: NetworkDecision): string {
|
|
159
|
+
const reason = decision.rule?.reason ?? ''
|
|
160
|
+
if (decision.matched) {
|
|
161
|
+
const rulePart = decision.ruleIndex !== undefined ? ` by rule ${decision.ruleIndex + 1}` : ''
|
|
162
|
+
if (decision.action === 'ask') {
|
|
163
|
+
return `[network: blocked pending approval${rulePart}] ${reason}\n(subprocess connections cannot ride the interactive approval seam — ask your user or add an allow rule for this target)`
|
|
164
|
+
}
|
|
165
|
+
return `[network: denied${rulePart}] ${reason}`
|
|
166
|
+
}
|
|
167
|
+
switch (decision.mode) {
|
|
168
|
+
case 'deny-all':
|
|
169
|
+
return '[network: denied] network mode deny-all (read-only sandbox preset): only whitelisted targets are reachable'
|
|
170
|
+
case 'whitelist':
|
|
171
|
+
return decision.action === 'ask'
|
|
172
|
+
? '[network: blocked pending approval] whitelist mode: the target is not matched by an allow rule'
|
|
173
|
+
: '[network: denied] whitelist mode: the target is not matched by an allow rule'
|
|
174
|
+
case 'allow-all':
|
|
175
|
+
return '[network: denied]'
|
|
176
|
+
}
|
|
177
|
+
}
|
package/src/prose.ts
CHANGED
|
@@ -58,6 +58,16 @@ export interface UiProse {
|
|
|
58
58
|
unreachableWarning: (numbers: readonly number[]) => string
|
|
59
59
|
/** Empty-source placeholder in the rules header. */
|
|
60
60
|
emptySource: string
|
|
61
|
+
/** Line shown when the network policy is disabled (`network.enabled: false`). */
|
|
62
|
+
networkDisabled: string
|
|
63
|
+
/** Header naming the network mode, its sandbox preset, and proxy liveness. */
|
|
64
|
+
networkHeader: (mode: string, sandboxMode: string | undefined, configuredMode: string, proxyActive: boolean, proxyPort: number) => string
|
|
65
|
+
/** Cumulative block counters line. */
|
|
66
|
+
networkCounters: (denied: number, askBlocked: number) => string
|
|
67
|
+
/** Line shown when no proxy blocks were recorded yet. */
|
|
68
|
+
noNetworkBlocks: string
|
|
69
|
+
/** One recent proxy-block row. */
|
|
70
|
+
networkBlockLine: (time: number, tool: string, attributed: boolean, domain: string, scheme: string | undefined, port: number | undefined, action: string, matched: boolean, ruleIndex: number | undefined, reason: string | undefined) => string
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
/** Tokens `describeRule` uses for its match-dimension prefixes. */
|
|
@@ -70,6 +80,11 @@ export interface DescribeTokens {
|
|
|
70
80
|
absent: string
|
|
71
81
|
when: string
|
|
72
82
|
platform: string
|
|
83
|
+
network: string
|
|
84
|
+
domains: string
|
|
85
|
+
ips: string
|
|
86
|
+
ports: string
|
|
87
|
+
schemes: string
|
|
73
88
|
disabled: string
|
|
74
89
|
tags: string
|
|
75
90
|
/** Prefix of the per-rule source-file attribution. */
|
|
@@ -90,7 +105,7 @@ const EN: UiProse = {
|
|
|
90
105
|
lastReloadWarning: error => `Warning: the last reload failed (${error}); the rules listed above are the previous ones.`,
|
|
91
106
|
dryRunNotice: 'Dry-run mode (enforce: false): deny/ask hits are audit-logged only — every call is passed through.',
|
|
92
107
|
unknownArg: arg => `Unknown /rules argument "${arg}". ${EN.usage}`,
|
|
93
|
-
usage: 'Usage: /rules [list | reload | decisions [n] | test <tool> <json-args>]',
|
|
108
|
+
usage: 'Usage: /rules [list | reload | network | decisions [n] | test <tool> <json-args>]',
|
|
94
109
|
decisionsHeader: (shown, total) => `Last ${shown} of ${total} permission decision(s) for this session:`,
|
|
95
110
|
noDecisions: 'No permission decisions recorded in this session yet.',
|
|
96
111
|
auditDisabledNotice: 'Session-log audit is disabled on this host: its Session.append predates the ignorable marker and would make sessions unresumable elsewhere (set allowUnmarkedAudit: true to opt back in).',
|
|
@@ -110,6 +125,18 @@ const EN: UiProse = {
|
|
|
110
125
|
testUsage: 'Usage: /rules test [--cwd <dir>] [--env KEY=VALUE]... [--agent <selector>]... [--platform <name>] <tool> <json-args>, e.g. /rules test bash {"command":"git push origin main"}',
|
|
111
126
|
unreachableWarning: numbers => `Warning: rule${numbers.length > 1 ? 's' : ''} ${numbers.join(', ')} ${numbers.length > 1 ? 'are' : 'is'} unreachable (shadowed by an earlier catch-all rule).`,
|
|
112
127
|
emptySource: '(no rule file — empty rule set)',
|
|
128
|
+
networkDisabled: 'Network policy disabled (network.enabled: false): no proxy, no web-tool mode defaults.',
|
|
129
|
+
networkHeader: (mode, sandboxMode, configuredMode, proxyActive, proxyPort) => `Network policy: mode ${mode}${sandboxMode === undefined ? '' : ` (sandbox preset ${sandboxMode})`}${configuredMode === 'auto' ? '' : ` (configured ${configuredMode})`}; proxy ${proxyActive ? `active on 127.0.0.1:${proxyPort}` : 'INACTIVE (bind failed — shell network policy is not enforced)'}.`,
|
|
130
|
+
networkCounters: (denied, askBlocked) => `Blocks: ${denied} denied, ${askBlocked} ask-blocked.`,
|
|
131
|
+
noNetworkBlocks: 'No network blocks recorded yet.',
|
|
132
|
+
networkBlockLine: (time, tool, attributed, domain, scheme, port, action, matched, ruleIndex, reason) => {
|
|
133
|
+
const when = new Date(time).toISOString()
|
|
134
|
+
const who = `${tool}${attributed ? '' : ' (unattributed)'}`
|
|
135
|
+
const target = `${scheme ?? '?'}://${domain}${port === undefined ? '' : `:${port}`}`
|
|
136
|
+
const by = matched ? (ruleIndex === undefined ? '' : ` (rule ${ruleIndex + 1})`) : ' (mode default)'
|
|
137
|
+
const why = reason === undefined ? '' : `: ${short(reason)}`
|
|
138
|
+
return `- ${when} ${action} ${who} → ${target}${by}${why}`
|
|
139
|
+
},
|
|
113
140
|
}
|
|
114
141
|
|
|
115
142
|
const ZH: UiProse = {
|
|
@@ -121,7 +148,7 @@ const ZH: UiProse = {
|
|
|
121
148
|
lastReloadWarning: error => `警告:上次重载失败(${error});以上列出的是之前的规则。`,
|
|
122
149
|
dryRunNotice: '干跑模式(enforce: false):deny/ask 命中仅写入审计日志——所有调用照常透传。',
|
|
123
150
|
unknownArg: arg => `未知的 /rules 参数 "${arg}"。${ZH.usage}`,
|
|
124
|
-
usage: '用法:/rules [list | reload | decisions [n] | test <工具名> <json-参数>]',
|
|
151
|
+
usage: '用法:/rules [list | reload | network | decisions [n] | test <工具名> <json-参数>]',
|
|
125
152
|
decisionsHeader: (shown, total) => `本会话最近 ${shown}/${total} 条权限裁决:`,
|
|
126
153
|
noDecisions: '本会话还没有记录任何权限裁决。',
|
|
127
154
|
auditDisabledNotice: '本宿主上会话日志审计已停用:其 Session.append 早于 ignorable 标记支持,会令会话在其他宿主上无法恢复(设 allowUnmarkedAudit: true 可重新开启)。',
|
|
@@ -141,6 +168,18 @@ const ZH: UiProse = {
|
|
|
141
168
|
testUsage: '用法:/rules test [--cwd <目录>] [--env 键=值]... [--agent <选择器>]... [--platform <平台名>] <工具名> <json-参数>,例如 /rules test bash {"command":"git push origin main"}',
|
|
142
169
|
unreachableWarning: numbers => `警告:规则 ${numbers.join('、')} 不可达(被前面的通配规则遮蔽)。`,
|
|
143
170
|
emptySource: '(无规则文件——空规则集)',
|
|
171
|
+
networkDisabled: '网络策略已停用(network.enabled: false):无代理、无 web 工具模式默认裁决。',
|
|
172
|
+
networkHeader: (mode, sandboxMode, configuredMode, proxyActive, proxyPort) => `网络策略:模式 ${mode}${sandboxMode === undefined ? '' : `(沙箱预设 ${sandboxMode})`}${configuredMode === 'auto' ? '' : `(显式配置 ${configuredMode})`};代理${proxyActive ? `运行于 127.0.0.1:${proxyPort}` : '未激活(绑定失败——shell 网络策略未生效)'}。`,
|
|
173
|
+
networkCounters: (denied, askBlocked) => `拦截:拒绝 ${denied} 次,待审批阻断 ${askBlocked} 次。`,
|
|
174
|
+
noNetworkBlocks: '尚未记录任何网络拦截。',
|
|
175
|
+
networkBlockLine: (time, tool, attributed, domain, scheme, port, action, matched, ruleIndex, reason) => {
|
|
176
|
+
const when = new Date(time).toISOString()
|
|
177
|
+
const who = `${tool}${attributed ? '' : '(未归属)'}`
|
|
178
|
+
const target = `${scheme ?? '?'}://${domain}${port === undefined ? '' : `:${port}`}`
|
|
179
|
+
const by = matched ? (ruleIndex === undefined ? '' : `(规则 ${ruleIndex + 1})`) : '(模式默认)'
|
|
180
|
+
const why = reason === undefined ? '' : `:${short(reason)}`
|
|
181
|
+
return `- ${when} ${action} ${who} → ${target}${by}${why}`
|
|
182
|
+
},
|
|
144
183
|
}
|
|
145
184
|
|
|
146
185
|
const ES: UiProse = {
|
|
@@ -152,7 +191,7 @@ const ES: UiProse = {
|
|
|
152
191
|
lastReloadWarning: error => `Aviso: la última recarga falló (${error}); las reglas mostradas son las anteriores.`,
|
|
153
192
|
dryRunNotice: 'Modo simulación (enforce: false): los aciertos deny/ask solo se registran en auditoría — todas las llamadas pasan.',
|
|
154
193
|
unknownArg: arg => `Argumento de /rules desconocido "${arg}". ${ES.usage}`,
|
|
155
|
-
usage: 'Uso: /rules [list | reload | decisions [n] | test <herramienta> <json-args>]',
|
|
194
|
+
usage: 'Uso: /rules [list | reload | network | decisions [n] | test <herramienta> <json-args>]',
|
|
156
195
|
decisionsHeader: (shown, total) => `Últimas ${shown} de ${total} decisión(es) de permiso de esta sesión:`,
|
|
157
196
|
noDecisions: 'Esta sesión aún no registra decisiones de permiso.',
|
|
158
197
|
auditDisabledNotice: 'La auditoría del registro de sesión está desactivada en este host: su Session.append es anterior al marcador ignorable y haría las sesiones irrecuperables en otros hosts (establece allowUnmarkedAudit: true para reactivarla).',
|
|
@@ -172,6 +211,18 @@ const ES: UiProse = {
|
|
|
172
211
|
testUsage: 'Uso: /rules test [--cwd <dir>] [--env CLAVE=VALOR]... [--agent <selector>]... [--platform <nombre>] <herramienta> <json-args>, p. ej. /rules test bash {"command":"git push origin main"}',
|
|
173
212
|
unreachableWarning: numbers => `Aviso: ${numbers.length > 1 ? 'las reglas' : 'la regla'} ${numbers.join(', ')} ${numbers.length > 1 ? 'son inalcanzables' : 'es inalcanzable'} (tapada por una regla general anterior).`,
|
|
174
213
|
emptySource: '(sin archivo de reglas — conjunto vacío)',
|
|
214
|
+
networkDisabled: 'Política de red desactivada (network.enabled: false): sin proxy, sin valores predeterminados de modo para herramientas web.',
|
|
215
|
+
networkHeader: (mode, sandboxMode, configuredMode, proxyActive, proxyPort) => `Política de red: modo ${mode}${sandboxMode === undefined ? '' : ` (preset sandbox ${sandboxMode})`}${configuredMode === 'auto' ? '' : ` (configurado ${configuredMode})`}; proxy ${proxyActive ? `activo en 127.0.0.1:${proxyPort}` : 'INACTIVO (fallo de bind — la política de red de shell no se aplica)'}.`,
|
|
216
|
+
networkCounters: (denied, askBlocked) => `Bloqueos: ${denied} denegados, ${askBlocked} bloqueados por aprobación.`,
|
|
217
|
+
noNetworkBlocks: 'Aún no se han registrado bloqueos de red.',
|
|
218
|
+
networkBlockLine: (time, tool, attributed, domain, scheme, port, action, matched, ruleIndex, reason) => {
|
|
219
|
+
const when = new Date(time).toISOString()
|
|
220
|
+
const who = `${tool}${attributed ? '' : ' (sin atribuir)'}`
|
|
221
|
+
const target = `${scheme ?? '?'}://${domain}${port === undefined ? '' : `:${port}`}`
|
|
222
|
+
const by = matched ? (ruleIndex === undefined ? '' : ` (regla ${ruleIndex + 1})`) : ' (modo predeterminado)'
|
|
223
|
+
const why = reason === undefined ? '' : `: ${short(reason)}`
|
|
224
|
+
return `- ${when} ${action} ${who} → ${target}${by}${why}`
|
|
225
|
+
},
|
|
175
226
|
}
|
|
176
227
|
|
|
177
228
|
const PT: UiProse = {
|
|
@@ -183,7 +234,7 @@ const PT: UiProse = {
|
|
|
183
234
|
lastReloadWarning: error => `Aviso: a última recarga falhou (${error}); as regras listadas são as anteriores.`,
|
|
184
235
|
dryRunNotice: 'Modo simulação (enforce: false): acertos deny/ask são apenas registrados na auditoria — todas as chamadas passam.',
|
|
185
236
|
unknownArg: arg => `Argumento de /rules desconhecido "${arg}". ${PT.usage}`,
|
|
186
|
-
usage: 'Uso: /rules [list | reload | decisions [n] | test <ferramenta> <json-args>]',
|
|
237
|
+
usage: 'Uso: /rules [list | reload | network | decisions [n] | test <ferramenta> <json-args>]',
|
|
187
238
|
decisionsHeader: (shown, total) => `Últimas ${shown} de ${total} decisão(ões) de permissão desta sessão:`,
|
|
188
239
|
noDecisions: 'Esta sessão ainda não registrou decisões de permissão.',
|
|
189
240
|
auditDisabledNotice: 'A auditoria do registro da sessão está desativada neste host: seu Session.append é anterior ao marcador ignorable e tornaria as sessões irrecuperáveis em outros hosts (defina allowUnmarkedAudit: true para reativar).',
|
|
@@ -203,6 +254,18 @@ const PT: UiProse = {
|
|
|
203
254
|
testUsage: 'Uso: /rules test [--cwd <dir>] [--env CHAVE=VALOR]... [--agent <seletor>]... [--platform <nome>] <ferramenta> <json-args>, ex.: /rules test bash {"command":"git push origin main"}',
|
|
204
255
|
unreachableWarning: numbers => `Aviso: ${numbers.length > 1 ? 'as regras' : 'a regra'} ${numbers.join(', ')} ${numbers.length > 1 ? 'são inalcançáveis' : 'é inalcançável'} (encoberta por uma regra geral anterior).`,
|
|
205
256
|
emptySource: '(sem arquivo de regras — conjunto vazio)',
|
|
257
|
+
networkDisabled: 'Política de rede desativada (network.enabled: false): sem proxy, sem padrões de modo para ferramentas web.',
|
|
258
|
+
networkHeader: (mode, sandboxMode, configuredMode, proxyActive, proxyPort) => `Política de rede: modo ${mode}${sandboxMode === undefined ? '' : ` (preset sandbox ${sandboxMode})`}${configuredMode === 'auto' ? '' : ` (configurado ${configuredMode})`}; proxy ${proxyActive ? `ativo em 127.0.0.1:${proxyPort}` : 'INATIVO (falha de bind — a política de rede do shell não está ativa)'}.`,
|
|
259
|
+
networkCounters: (denied, askBlocked) => `Bloqueios: ${denied} negados, ${askBlocked} bloqueados por aprovação.`,
|
|
260
|
+
noNetworkBlocks: 'Nenhum bloqueio de rede registrado ainda.',
|
|
261
|
+
networkBlockLine: (time, tool, attributed, domain, scheme, port, action, matched, ruleIndex, reason) => {
|
|
262
|
+
const when = new Date(time).toISOString()
|
|
263
|
+
const who = `${tool}${attributed ? '' : ' (não atribuído)'}`
|
|
264
|
+
const target = `${scheme ?? '?'}://${domain}${port === undefined ? '' : `:${port}`}`
|
|
265
|
+
const by = matched ? (ruleIndex === undefined ? '' : ` (regra ${ruleIndex + 1})`) : ' (modo padrão)'
|
|
266
|
+
const why = reason === undefined ? '' : `: ${short(reason)}`
|
|
267
|
+
return `- ${when} ${action} ${who} → ${target}${by}${why}`
|
|
268
|
+
},
|
|
206
269
|
}
|
|
207
270
|
|
|
208
271
|
const HI: UiProse = {
|
|
@@ -214,7 +277,7 @@ const HI: UiProse = {
|
|
|
214
277
|
lastReloadWarning: error => `चेतावनी: अंतिम पुनः लोड विफल रहा (${error}); ऊपर सूचीबद्ध नियम पिछले वाले हैं।`,
|
|
215
278
|
dryRunNotice: 'ड्राई-रन मोड (enforce: false): deny/ask हिट केवल ऑडिट में दर्ज होते हैं — हर कॉल पास हो जाती है।',
|
|
216
279
|
unknownArg: arg => `अज्ञात /rules तर्क "${arg}"। ${HI.usage}`,
|
|
217
|
-
usage: 'उपयोग: /rules [list | reload | decisions [n] | test <टूल> <json-args>]',
|
|
280
|
+
usage: 'उपयोग: /rules [list | reload | network | decisions [n] | test <टूल> <json-args>]',
|
|
218
281
|
decisionsHeader: (shown, total) => `इस सत्र के अंतिम ${shown}/${total} अनुमति निर्णय:`,
|
|
219
282
|
noDecisions: 'इस सत्र में अभी कोई अनुमति निर्णय दर्ज नहीं है।',
|
|
220
283
|
auditDisabledNotice: 'इस होस्ट पर सत्र-लॉग ऑडिट अक्षम है: इसका Session.append ignorable मार्कर से पुराना है और सत्र अन्य होस्ट पर अप्राप्य बन जाते (पुनः चालू करने के लिए allowUnmarkedAudit: true सेट करें)।',
|
|
@@ -234,6 +297,18 @@ const HI: UiProse = {
|
|
|
234
297
|
testUsage: 'उपयोग: /rules test [--cwd <dir>] [--env KEY=मान]... [--agent <चयनकर्ता>]... [--platform <नाम>] <टूल> <json-args>, जैसे /rules test bash {"command":"git push origin main"}',
|
|
235
298
|
unreachableWarning: numbers => `चेतावनी: नियम ${numbers.join(', ')} अप्राप्य हैं (पहले वाले सर्व-मिलान नियम से ढके हुए)।`,
|
|
236
299
|
emptySource: '(कोई नियम फ़ाइल नहीं — खाली नियम-समूह)',
|
|
300
|
+
networkDisabled: 'नेटवर्क नीति अक्षम (network.enabled: false): कोई प्रॉक्सी नहीं, कोई वेब-टूल मोड डिफ़ॉल्ट नहीं।',
|
|
301
|
+
networkHeader: (mode, sandboxMode, configuredMode, proxyActive, proxyPort) => `नेटवर्क नीति: मोड ${mode}${sandboxMode === undefined ? '' : ` (सैंडबॉक्स प्रीसेट ${sandboxMode})`}${configuredMode === 'auto' ? '' : ` (कॉन्फ़िगर किया ${configuredMode})`}; प्रॉक्सी ${proxyActive ? `सक्रिय 127.0.0.1:${proxyPort} पर` : 'निष्क्रिय (बाइंड विफल — शेल नेटवर्क नीति लागू नहीं)'}।`,
|
|
302
|
+
networkCounters: (denied, askBlocked) => `ब्लॉक: ${denied} अस्वीकृत, ${askBlocked} अनुमोदन-ब्लॉक।`,
|
|
303
|
+
noNetworkBlocks: 'अभी तक कोई नेटवर्क ब्लॉक दर्ज नहीं हुआ।',
|
|
304
|
+
networkBlockLine: (time, tool, attributed, domain, scheme, port, action, matched, ruleIndex, reason) => {
|
|
305
|
+
const when = new Date(time).toISOString()
|
|
306
|
+
const who = `${tool}${attributed ? '' : ' (असाइन नहीं)'}`
|
|
307
|
+
const target = `${scheme ?? '?'}://${domain}${port === undefined ? '' : `:${port}`}`
|
|
308
|
+
const by = matched ? (ruleIndex === undefined ? '' : ` (नियम ${ruleIndex + 1})`) : ' (मोड डिफ़ॉल्ट)'
|
|
309
|
+
const why = reason === undefined ? '' : `: ${short(reason)}`
|
|
310
|
+
return `- ${when} ${action} ${who} → ${target}${by}${why}`
|
|
311
|
+
},
|
|
237
312
|
}
|
|
238
313
|
|
|
239
314
|
/** The localized prose tables by language. */
|
|
@@ -241,9 +316,9 @@ export const UI_PROSE: Readonly<Record<UiLanguage, UiProse>> = { en: EN, zh: ZH,
|
|
|
241
316
|
|
|
242
317
|
/** `describeRule` match-dimension tokens by language. */
|
|
243
318
|
export const DESCRIBE_TOKENS: Readonly<Record<UiLanguage, DescribeTokens>> = {
|
|
244
|
-
en: { allTools: 'all tools', tools: 'tools', agents: 'agents', params: 'params', paths: 'paths', absent: 'absent', when: 'when', platform: 'platform', disabled: 'disabled', tags: 'tags', src: 'src' },
|
|
245
|
-
zh: { allTools: '全部工具', tools: '工具', agents: '代理', params: '参数', paths: '路径', absent: '缺省键', when: '条件', platform: '平台', disabled: '已禁用', tags: '标签', src: '来源' },
|
|
246
|
-
es: { allTools: 'todas las herramientas', tools: 'herramientas', agents: 'agentes', params: 'parámetros', paths: 'rutas', absent: 'ausentes', when: 'cuándo', platform: 'plataforma', disabled: 'deshabilitada', tags: 'etiquetas', src: 'origen' },
|
|
247
|
-
pt: { allTools: 'todas as ferramentas', tools: 'ferramentas', agents: 'agentes', params: 'parâmetros', paths: 'caminhos', absent: 'ausentes', when: 'quando', platform: 'plataforma', disabled: 'desativada', tags: 'etiquetas', src: 'origem' },
|
|
248
|
-
hi: { allTools: 'सभी टूल', tools: 'टूल', agents: 'एजेंट', params: 'पैरामीटर', paths: 'पथ', absent: 'अनुपस्थित', when: 'शर्त', platform: 'प्लेटफ़ॉर्म', disabled: 'अक्षम', tags: 'टैग', src: 'स्रोत' },
|
|
319
|
+
en: { allTools: 'all tools', tools: 'tools', agents: 'agents', params: 'params', paths: 'paths', absent: 'absent', when: 'when', platform: 'platform', network: 'network', domains: 'domains', ips: 'ips', ports: 'ports', schemes: 'schemes', disabled: 'disabled', tags: 'tags', src: 'src' },
|
|
320
|
+
zh: { allTools: '全部工具', tools: '工具', agents: '代理', params: '参数', paths: '路径', absent: '缺省键', when: '条件', platform: '平台', network: '网络', domains: '域名', ips: 'IP', ports: '端口', schemes: '协议', disabled: '已禁用', tags: '标签', src: '来源' },
|
|
321
|
+
es: { allTools: 'todas las herramientas', tools: 'herramientas', agents: 'agentes', params: 'parámetros', paths: 'rutas', absent: 'ausentes', when: 'cuándo', platform: 'plataforma', network: 'red', domains: 'dominios', ips: 'IP', ports: 'puertos', schemes: 'esquemas', disabled: 'deshabilitada', tags: 'etiquetas', src: 'origen' },
|
|
322
|
+
pt: { allTools: 'todas as ferramentas', tools: 'ferramentas', agents: 'agentes', params: 'parâmetros', paths: 'caminhos', absent: 'ausentes', when: 'quando', platform: 'plataforma', network: 'rede', domains: 'domínios', ips: 'IP', ports: 'portas', schemes: 'esquemas', disabled: 'desativada', tags: 'etiquetas', src: 'origem' },
|
|
323
|
+
hi: { allTools: 'सभी टूल', tools: 'टूल', agents: 'एजेंट', params: 'पैरामीटर', paths: 'पथ', absent: 'अनुपस्थित', when: 'शर्त', platform: 'प्लेटफ़ॉर्म', network: 'नेटवर्क', domains: 'डोमेन', ips: 'IP', ports: 'पोर्ट', schemes: 'स्कीम', disabled: 'अक्षम', tags: 'टैग', src: 'स्रोत' },
|
|
249
324
|
}
|