dsh-cc-permissions 0.1.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/README.md +43 -0
- package/cordis.patch.yml +6 -0
- package/package.json +48 -0
- package/src/index.js +159 -0
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# dsh-cc-permissions
|
|
2
|
+
|
|
3
|
+
Enforce Claude Code permission rules in DSH.
|
|
4
|
+
|
|
5
|
+
Reads `.claude/settings.json` (user `~/.claude`, project, and `settings.local.json`)
|
|
6
|
+
through the shared [dsh-cc-loader](../cc-loader) parse layer and folds the rules
|
|
7
|
+
**deny → ask → allow** on every tool call, exactly like Claude Code:
|
|
8
|
+
|
|
9
|
+
- `Tool` bare deny removes the tool from the model's context (per-agent hide)
|
|
10
|
+
- `Tool(spec)` scoped rules match commands, paths, domains, params, MCP tools
|
|
11
|
+
- Bash compound commands are split; wrappers (`timeout`, `nice`, `nohup`…) and
|
|
12
|
+
leading env assignments are stripped before matching
|
|
13
|
+
- Read deny rules also gate Edit/Write tools and Bash file commands
|
|
14
|
+
(`cat`, `head`, `tail`, `sed`…)
|
|
15
|
+
- `ask` rules route through DSH's approval service
|
|
16
|
+
|
|
17
|
+
## Read-only bridge
|
|
18
|
+
|
|
19
|
+
CC `settings.json` is a **read-only source**. DSH-side approvals ("don't ask
|
|
20
|
+
again") are stored in DSH's own config and are never written back to
|
|
21
|
+
`.claude/settings.local.json` — CC, Codex and DSH have different permission
|
|
22
|
+
semantics, so cross-tool permission sync is intentionally out of scope
|
|
23
|
+
(community `agents` / `cc-suite` do not sync permissions either).
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
dsh plugin --profile <name> add dsh-cc-permissions
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Config
|
|
32
|
+
|
|
33
|
+
| Key | Default | Meaning |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| `enabled` | `true` | Master switch |
|
|
36
|
+
| `hideDeniedTools` | `true` | Bare-name deny → per-agent `ctx.tools.restrict` |
|
|
37
|
+
| `enableDefaultMode` | `true` | `defaultMode=dontAsk` → approval policy `never` |
|
|
38
|
+
| `homeDir` | `<home>` | Home dir for `~/.claude/settings.json` |
|
|
39
|
+
| `projectRootMarkers` | `['.git']` | Project root discovery |
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
MIT.
|
package/cordis.patch.yml
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-cc-permissions",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Enforce Claude Code permission rules (.claude/settings.json allow/deny/ask, deny>ask>allow) in DSH via a tools/pre-execute gate. Reads CC settings as a read-only source; DSH-side approvals never write back to .claude.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"dsh": {
|
|
11
|
+
"bundle": {
|
|
12
|
+
"patch": "./cordis.patch.yml"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"README.md",
|
|
18
|
+
"cordis.patch.yml"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"dsh",
|
|
22
|
+
"deepseek-harness",
|
|
23
|
+
"claude-code",
|
|
24
|
+
"permissions",
|
|
25
|
+
"security"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"dsh-cc-loader": "^0.1.0"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@deepseek-ai/cordis": "*",
|
|
33
|
+
"@deepseek-ai/schemastery": "*"
|
|
34
|
+
},
|
|
35
|
+
"peerDependenciesMeta": {
|
|
36
|
+
"@deepseek-ai/cordis": {
|
|
37
|
+
"optional": true
|
|
38
|
+
},
|
|
39
|
+
"@deepseek-ai/schemastery": {
|
|
40
|
+
"optional": true
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=20"
|
|
45
|
+
},
|
|
46
|
+
"repository": "git+https://github.com/Bcy2020/dsh-cc-ecosystem.git",
|
|
47
|
+
"homepage": "https://github.com/Bcy2020/dsh-cc-ecosystem"
|
|
48
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// cc-permissions — enforce CC permission rules in DSH.
|
|
2
|
+
//
|
|
3
|
+
// Read-only bridge: `.claude/settings.json` (user/project/local) is parsed by
|
|
4
|
+
// dsh-cc-loader into rules, and a `tools/pre-execute` listener folds them
|
|
5
|
+
// deny → ask → allow exactly like Claude Code. DSH-side approvals never write
|
|
6
|
+
// back to `.claude` (no cross-tool permission sync — CC/Codex/DSH permission
|
|
7
|
+
// semantics differ).
|
|
8
|
+
//
|
|
9
|
+
// Bare-name deny rules additionally hide the tool per-agent via
|
|
10
|
+
// `agent.ctx.tools.restrict({ deny })`, so the model does not see removed
|
|
11
|
+
// tools (falls back to always-deny when a name is unknown to the registry).
|
|
12
|
+
|
|
13
|
+
import { stat } from 'node:fs/promises'
|
|
14
|
+
import { homedir } from 'node:os'
|
|
15
|
+
import z from '@deepseek-ai/schemastery'
|
|
16
|
+
import { loadPermissions, evaluateCall } from 'dsh-cc-loader'
|
|
17
|
+
|
|
18
|
+
export const name = 'cc-permissions'
|
|
19
|
+
|
|
20
|
+
export const Config = z.object({
|
|
21
|
+
enabled: z.boolean().default(true),
|
|
22
|
+
// Bare-name deny → per-agent tool hiding (restrict). Pre-execute always
|
|
23
|
+
// denies regardless; this only affects what the model sees.
|
|
24
|
+
hideDeniedTools: z.boolean().default(true),
|
|
25
|
+
// Map CC defaultMode: dontAsk → approval policy never (per session).
|
|
26
|
+
enableDefaultMode: z.boolean().default(true),
|
|
27
|
+
homeDir: z.string(),
|
|
28
|
+
projectRootMarkers: z.array(z.string()).default(['.git']),
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
export function apply(ctx, config = {}) {
|
|
32
|
+
if (config.enabled === false) return
|
|
33
|
+
|
|
34
|
+
const homeDir = config.homeDir ?? homedir()
|
|
35
|
+
// Per-cwd cache of loaded permissions; invalidated when any settings file's
|
|
36
|
+
// mtime changes, so edits take effect immediately (fresher than CC's
|
|
37
|
+
// load-at-startup semantics).
|
|
38
|
+
const cache = new Map()
|
|
39
|
+
|
|
40
|
+
async function permissionsFor(cwd) {
|
|
41
|
+
const hit = cache.get(cwd)
|
|
42
|
+
if (hit !== undefined) {
|
|
43
|
+
let changed = false
|
|
44
|
+
for (const src of hit.sources) {
|
|
45
|
+
try {
|
|
46
|
+
const s = await stat(src.path)
|
|
47
|
+
if (s.mtimeMs !== src.mtimeMs) { changed = true; break }
|
|
48
|
+
} catch { changed = true; break }
|
|
49
|
+
}
|
|
50
|
+
if (!changed) return hit
|
|
51
|
+
cache.delete(cwd)
|
|
52
|
+
}
|
|
53
|
+
const loaded = await loadPermissions({ cwd, homeDir, projectRootMarkers: config.projectRootMarkers })
|
|
54
|
+
const sources = (loaded.permissions.sources ?? []).map((src) => ({ path: src.path, mtimeMs: 0 }))
|
|
55
|
+
for (const src of sources) {
|
|
56
|
+
try { src.mtimeMs = (await stat(src.path)).mtimeMs } catch { /* keep 0 → always reload */ }
|
|
57
|
+
}
|
|
58
|
+
const entry = { ...loaded, sources, at: Date.now() }
|
|
59
|
+
cache.set(cwd, entry)
|
|
60
|
+
return entry
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── gate: tools/pre-execute ────────────────────────────────────────────────
|
|
64
|
+
ctx.logger.info('cc-permissions: gate registered (reads .claude/settings.json allow/deny/ask)')
|
|
65
|
+
ctx.on('tools/pre-execute', async (exec, next) => {
|
|
66
|
+
const cwd = exec.agent?.session.header.cwd ?? process.cwd()
|
|
67
|
+
let loaded
|
|
68
|
+
try { loaded = await permissionsFor(cwd) } catch (error) {
|
|
69
|
+
ctx.logger.warn(`cc-permissions: load failed: ${String(error)}`)
|
|
70
|
+
return next()
|
|
71
|
+
}
|
|
72
|
+
const perm = loaded.permissions
|
|
73
|
+
if (perm === undefined) return next()
|
|
74
|
+
// Proceed when there are rules to fold OR when the MCP auto-approve toggle
|
|
75
|
+
// is on (it is meaningful even with an otherwise-empty settings file).
|
|
76
|
+
if (perm.status !== 'DIRECT' && perm.enableAllProjectMcpServers !== true) return next()
|
|
77
|
+
const result = evaluateCall(perm.parsed, { tool: exec.name, args: exec.arguments }, {
|
|
78
|
+
cwd: loaded.cwd,
|
|
79
|
+
homeDir,
|
|
80
|
+
projectRoot: loaded.projectRoot,
|
|
81
|
+
})
|
|
82
|
+
if (result.decision === 'deny') {
|
|
83
|
+
ctx.logger.info(`cc-permissions: DENY ${exec.name} (cwd=${cwd}) — ${result.reason}`)
|
|
84
|
+
return { kind: 'deny', reason: result.reason ?? 'Denied by a Claude Code permission rule.' }
|
|
85
|
+
}
|
|
86
|
+
if (result.decision === 'ask') {
|
|
87
|
+
ctx.logger.info(`cc-permissions: ASK ${exec.name} (cwd=${cwd}) — ${result.reason}`)
|
|
88
|
+
return { kind: 'ask', reason: result.reason ?? 'A Claude Code permission rule requests confirmation.' }
|
|
89
|
+
}
|
|
90
|
+
// CC settings `enableAllProjectMcpServers: true` auto-approves tools from
|
|
91
|
+
// PROJECT MCP servers (mcp__<server>__<tool>); plugin MCP tools keep their
|
|
92
|
+
// mcp__plugin_ namespace and are NOT covered. Slots in at the bottom of
|
|
93
|
+
// the deny → ask → allow fold: explicit deny/ask rules still win.
|
|
94
|
+
if (result.decision === 'none'
|
|
95
|
+
&& perm.enableAllProjectMcpServers === true
|
|
96
|
+
&& exec.name.startsWith('mcp__') && !exec.name.startsWith('mcp__plugin_')) {
|
|
97
|
+
ctx.logger.info(`cc-permissions: ALLOW ${exec.name} (cwd=${cwd}) — enableAllProjectMcpServers=true auto-approves project MCP tools`)
|
|
98
|
+
return { kind: 'allow', reason: 'enableAllProjectMcpServers=true auto-approves project MCP server tools.' }
|
|
99
|
+
}
|
|
100
|
+
return next()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// ── bare-name deny → hide the tool from the model ─────────────────────────
|
|
104
|
+
if (config.hideDeniedTools !== false) {
|
|
105
|
+
ctx.on('agent/created', ({ agent }) => {
|
|
106
|
+
const cwd = agent.session?.header?.cwd
|
|
107
|
+
if (cwd === undefined) return
|
|
108
|
+
void (async () => {
|
|
109
|
+
try {
|
|
110
|
+
const loaded = await permissionsFor(cwd)
|
|
111
|
+
const perm = loaded.permissions
|
|
112
|
+
if (perm === undefined || perm.status !== 'DIRECT') return
|
|
113
|
+
const { names } = perm.removed
|
|
114
|
+
if (names.length === 0) return
|
|
115
|
+
const tools = agent.ctx.tools
|
|
116
|
+
try {
|
|
117
|
+
tools.restrict({ deny: names })
|
|
118
|
+
} catch {
|
|
119
|
+
// Some names may not be in the registry yet / never registered —
|
|
120
|
+
// restrict per name so the known ones still hide.
|
|
121
|
+
for (const name of names) {
|
|
122
|
+
try { tools.restrict({ deny: [name] }) } catch { /* unknown tool: pre-execute still denies */ }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
ctx.logger.warn(`cc-permissions: restrict failed: ${String(error)}`)
|
|
127
|
+
}
|
|
128
|
+
})()
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── defaultMode → approval policy / guidance ──────────────────────────────
|
|
133
|
+
if (config.enableDefaultMode !== false) {
|
|
134
|
+
ctx.on('agent/session-start', ({ agent }) => {
|
|
135
|
+
const cwd = agent.session.header?.cwd
|
|
136
|
+
if (cwd === undefined) return
|
|
137
|
+
void (async () => {
|
|
138
|
+
try {
|
|
139
|
+
const loaded = await permissionsFor(cwd)
|
|
140
|
+
const mode = loaded.permissions?.defaultMode
|
|
141
|
+
if (mode === undefined) return
|
|
142
|
+
const approval = ctx.get('approval')
|
|
143
|
+
if (mode === 'dontAsk' && approval !== undefined) {
|
|
144
|
+
approval.setPolicy(agent, 'never')
|
|
145
|
+
ctx.logger.info(`cc-permissions: defaultMode=dontAsk → approval policy never for session`)
|
|
146
|
+
} else if (mode === 'bypassPermissions') {
|
|
147
|
+
ctx.logger.warn(`cc-permissions: defaultMode=bypassPermissions detected — DSH does NOT auto-map this to danger-full-access; enable it explicitly if intended`)
|
|
148
|
+
} else if (mode !== 'default' && mode !== 'manual') {
|
|
149
|
+
ctx.logger.info(`cc-permissions: defaultMode=${mode} noted (no direct DSH mapping; sandbox/approval presets govern)`)
|
|
150
|
+
}
|
|
151
|
+
} catch (error) {
|
|
152
|
+
ctx.logger.warn(`cc-permissions: defaultMode handling failed: ${String(error)}`)
|
|
153
|
+
}
|
|
154
|
+
})()
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
ctx.effect(() => () => cache.clear())
|
|
159
|
+
}
|