dsh-desktop-kit 2.2.1 → 2.7.3
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 +86 -0
- package/README.md +114 -27
- package/client.js +3270 -32
- package/docs/AGENT-HARNESS.md +144 -0
- package/docs/ARCHITECTURE.md +38 -7
- package/docs/COMPATIBILITY.md +4 -2
- package/docs/DEVELOPMENT.md +56 -0
- package/docs/INSTALL.md +20 -11
- package/docs/LAPTOP-INSTALL.md +96 -0
- package/docs/ROADMAP-3.0.md +353 -0
- package/docs/ROADMAP.md +1 -1
- package/docs/SUBAGENTS.md +25 -0
- package/docs/TEST-0.2.2.md +5 -5
- package/docs/TEST-2.3.0.md +49 -0
- package/docs/TEST-2.7.2.md +46 -0
- package/docs/TEST-2.7.3.md +20 -0
- package/docs/TOKENS.md +2 -2
- package/docs/TROUBLESHOOTING.md +27 -0
- package/docs/UI-QA.md +28 -0
- package/docs/images/peak-toast-peak.png +0 -0
- package/docs/images/peak-toast-soon.png +0 -0
- package/docs/preview/peak-toast.html +155 -0
- package/index.js +18 -1
- package/package.json +26 -6
- package/plugins/dsh-agent-extensions/README.md +107 -0
- package/plugins/dsh-agent-extensions/client.js +157 -0
- package/plugins/dsh-agent-extensions/index.js +174 -0
- package/plugins/dsh-agent-extensions/lib/agents.js +76 -0
- package/plugins/dsh-agent-extensions/lib/api.js +53 -0
- package/plugins/dsh-agent-extensions/lib/checkpoints.js +103 -0
- package/plugins/dsh-agent-extensions/lib/commands.js +255 -0
- package/plugins/dsh-agent-extensions/lib/hooks.js +182 -0
- package/plugins/dsh-agent-extensions/lib/policy.js +84 -0
- package/plugins/dsh-agent-extensions/lib/project.js +144 -0
- package/plugins/dsh-agent-extensions/lib/scaffold.js +151 -0
- package/plugins/dsh-agent-extensions/package.json +20 -0
- package/plugins/dsh-agent-extensions/screenshots/agents-1440.png +0 -0
- package/plugins/dsh-agent-extensions/screenshots/agents-390.png +0 -0
- package/plugins/dsh-browser-qa/README.md +221 -0
- package/plugins/dsh-browser-qa/cordis.patch.yml +4 -0
- package/plugins/dsh-browser-qa/index.js +165 -0
- package/plugins/dsh-browser-qa/lib/cdp.js +224 -0
- package/plugins/dsh-browser-qa/lib/chrome.js +466 -0
- package/plugins/dsh-browser-qa/lib/pages.js +552 -0
- package/plugins/dsh-browser-qa/lib/qa.js +827 -0
- package/plugins/dsh-browser-qa/lib/ui-gate.js +256 -0
- package/plugins/dsh-browser-qa/package.json +31 -0
- package/plugins/dsh-checkpoints/README.md +22 -0
- package/plugins/dsh-checkpoints/cordis.patch.yml +3 -0
- package/plugins/dsh-checkpoints/index.js +80 -0
- package/plugins/dsh-checkpoints/lib/checkpoints.js +111 -0
- package/plugins/dsh-checkpoints/package.json +25 -0
- package/plugins/dsh-github-review/README.md +131 -0
- package/plugins/dsh-github-review/cordis.patch.yml +3 -0
- package/plugins/dsh-github-review/index.js +536 -0
- package/plugins/dsh-github-review/lib/gh.js +494 -0
- package/plugins/dsh-github-review/lib/normalize.js +662 -0
- package/plugins/dsh-github-review/package.json +28 -0
- package/plugins/dsh-guardrails/README.md +264 -0
- package/plugins/dsh-guardrails/cordis.patch.yml +5 -0
- package/plugins/dsh-guardrails/index.js +389 -0
- package/plugins/dsh-guardrails/lib/hooks.js +262 -0
- package/plugins/dsh-guardrails/lib/modes.js +77 -0
- package/plugins/dsh-guardrails/lib/policy.js +592 -0
- package/plugins/dsh-guardrails/lib/scan.js +451 -0
- package/plugins/dsh-guardrails/lib/secrets.js +274 -0
- package/plugins/dsh-guardrails/package.json +26 -0
- package/plugins/dsh-mcp-manager/README.md +78 -0
- package/plugins/dsh-mcp-manager/client.js +373 -0
- package/plugins/dsh-mcp-manager/index.js +159 -0
- package/plugins/dsh-mcp-manager/lib/api.js +250 -0
- package/plugins/dsh-mcp-manager/lib/client.js +219 -0
- package/plugins/dsh-mcp-manager/lib/config.js +217 -0
- package/plugins/dsh-mcp-manager/lib/manager.js +146 -0
- package/plugins/dsh-mcp-manager/package.json +18 -0
- package/plugins/dsh-mcp-manager/screenshots/modal-390.png +0 -0
- package/plugins/dsh-mcp-manager/screenshots/settings-1440.png +0 -0
- package/plugins/dsh-mcp-manager/screenshots/settings-390.png +0 -0
- package/plugins/dsh-memory-bridge/README.md +161 -0
- package/plugins/dsh-memory-bridge/client.js +2264 -0
- package/plugins/dsh-memory-bridge/cordis.patch.yml +5 -0
- package/plugins/dsh-memory-bridge/docs/images/conflicts.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/hero.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/settings-dark.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/settings-light.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/settings-mobile.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/setup.png +0 -0
- package/plugins/dsh-memory-bridge/icon.svg +13 -0
- package/plugins/dsh-memory-bridge/index.js +378 -0
- package/plugins/dsh-memory-bridge/lib/bridge.js +636 -0
- package/plugins/dsh-memory-bridge/lib/github.js +473 -0
- package/plugins/dsh-memory-bridge/lib/merge.js +205 -0
- package/plugins/dsh-memory-bridge/lib/portable.js +314 -0
- package/plugins/dsh-memory-bridge/lib/state.js +565 -0
- package/plugins/dsh-memory-bridge/lib/sync.js +984 -0
- package/plugins/dsh-memory-bridge/locale/en.json +6 -0
- package/plugins/dsh-memory-bridge/locale/ru.json +6 -0
- package/plugins/dsh-memory-bridge/package.json +67 -0
- package/plugins/dsh-peak-guard/README.md +16 -3
- package/plugins/dsh-peak-guard/client.js +223 -8
- package/plugins/dsh-peak-guard/package.json +1 -1
- package/plugins/dsh-plan-mode/README.md +24 -0
- package/plugins/dsh-plan-mode/cordis.patch.yml +3 -0
- package/plugins/dsh-plan-mode/index.js +103 -0
- package/plugins/dsh-plan-mode/lib/plan.js +89 -0
- package/plugins/dsh-plan-mode/package.json +18 -0
- package/plugins/dsh-profile-transfer/client.js +277 -17
- package/plugins/dsh-profile-transfer/index.js +10 -0
- package/plugins/dsh-profile-transfer/package.json +3 -1
- package/plugins/dsh-profile-transfer/transfer.js +101 -2
- package/plugins/dsh-project-memory/index.js +92 -2
- package/plugins/dsh-project-memory/package.json +1 -1
- package/plugins/dsh-repo-map/README.md +26 -0
- package/plugins/dsh-repo-map/cordis.patch.yml +3 -0
- package/plugins/dsh-repo-map/index.js +99 -0
- package/plugins/dsh-repo-map/lib/repo-map.js +170 -0
- package/plugins/dsh-repo-map/package.json +25 -0
- package/plugins/dsh-skill-autopilot/index.js +135 -11
- package/skills/delivery-gate/SKILL.md +31 -0
- package/skills/git-and-ci/SKILL.md +5 -1
- package/skills/manifest.json +31 -6
- package/skills/requirements-and-planning/SKILL.md +3 -0
- package/skills/research-first/SKILL.md +35 -0
- package/skills/research-first/references/query-patterns.md +20 -0
- package/skills/subagent-playbook/SKILL.md +36 -0
- package/skills/subagent-playbook/references/prompts.md +39 -0
- package/skills/ui-craft/SKILL.md +43 -0
- package/skills/visual-qa/SKILL.md +29 -0
- package/skills/visual-qa/references/commands.md +32 -0
- package/skills-baseline.json +219 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real Host hooks for the guardrails plugin.
|
|
3
|
+
*
|
|
4
|
+
* Two extension points are used, both verified against the live DSH Event
|
|
5
|
+
* contracts (see README "Event contracts"):
|
|
6
|
+
*
|
|
7
|
+
* - `tools/pre-execute` is a waterfall `(exec, next) => PreToolDecision`.
|
|
8
|
+
* Returning `next()` delegates; a `deny`/`ask` decision short-circuits.
|
|
9
|
+
* The plugin uses it for shell commands (bash/pwsh, …) and as defence in
|
|
10
|
+
* depth for file-mutating tool arguments (write/edit/apply_patch).
|
|
11
|
+
*
|
|
12
|
+
* - `fs/write-intent`, `fs/edit-intent`, `fs/delete-intent` are single-slot
|
|
13
|
+
* waterfalls `(target, actor, next) => guard | undefined`. Their contract
|
|
14
|
+
* has no deny decision: `undefined`/`next()` allows and an intent owns the
|
|
15
|
+
* mutation. A refusal is therefore a thrown `Error` — the same mechanism the
|
|
16
|
+
* shipped `dsh-fs-observation-policy` uses (`FS_NOT_OBSERVED`). The plugin
|
|
17
|
+
* only throws for a path matched by the policy and delegates otherwise, so
|
|
18
|
+
* normal editing keeps working (documented conservative-allow fallback).
|
|
19
|
+
*
|
|
20
|
+
* Node built-ins only. Logs never include command text, target paths or secret
|
|
21
|
+
* values: they carry the rule id and the tool/event name only.
|
|
22
|
+
*/
|
|
23
|
+
import { evaluateCommand, loadPolicy, matchProtectedPath } from "./policy.js";
|
|
24
|
+
import { modeDecision, resolveMode } from "./modes.js";
|
|
25
|
+
|
|
26
|
+
/** Tool names whose command text is checked by the shell-command rules. */
|
|
27
|
+
export const SHELL_TOOL_NAMES = new Set(["bash", "pwsh", "powershell", "shell", "sh", "zsh", "cmd", "terminal"]);
|
|
28
|
+
|
|
29
|
+
/** File-mutating tool names whose path arguments are checked as defence in depth. */
|
|
30
|
+
export const PROTECTED_PATH_TOOLS = new Set(["write", "edit", "apply_patch", "patch"]);
|
|
31
|
+
|
|
32
|
+
const MAX_PATHS_PER_CALL = 200;
|
|
33
|
+
|
|
34
|
+
function errorMessage(error) {
|
|
35
|
+
return error instanceof Error ? error.message : String(error);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Fresh hook registration state, reported by the `status` action. */
|
|
39
|
+
export function createHookState() {
|
|
40
|
+
return {
|
|
41
|
+
toolsPreExecute: false,
|
|
42
|
+
fsWriteIntent: false,
|
|
43
|
+
fsEditIntent: false,
|
|
44
|
+
fsDeleteIntent: false,
|
|
45
|
+
errors: [],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extract the command text from shell-tool arguments. Both the shipped bash and
|
|
51
|
+
* pwsh tools use the `command` field; `cmd`/`script` are accepted for other
|
|
52
|
+
* shell providers.
|
|
53
|
+
*
|
|
54
|
+
* @param {unknown} args - parsed tool arguments.
|
|
55
|
+
* @returns {string} command text, or an empty string.
|
|
56
|
+
*/
|
|
57
|
+
export function extractShellCommand(args) {
|
|
58
|
+
if (typeof args === "string") return args;
|
|
59
|
+
if (args === null || typeof args !== "object") return "";
|
|
60
|
+
for (const key of ["command", "cmd", "script"]) {
|
|
61
|
+
const value = args[key];
|
|
62
|
+
if (typeof value === "string" && value.trim().length > 0) return value;
|
|
63
|
+
}
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Extract the paths a file-mutating tool plans to touch. For `apply_patch` the
|
|
69
|
+
* Codex-style patch headers are read; the mutation itself is still enforced by
|
|
70
|
+
* the authoritative `fs/*-intent` hooks.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} toolName - tool name from `tools/pre-execute`.
|
|
73
|
+
* @param {unknown} args - parsed tool arguments.
|
|
74
|
+
* @returns {string[]} unique requested paths, bounded per call.
|
|
75
|
+
*/
|
|
76
|
+
export function requestedWritePaths(toolName, args) {
|
|
77
|
+
const paths = [];
|
|
78
|
+
if (args === null || typeof args !== "object") return paths;
|
|
79
|
+
if (toolName === "write" || toolName === "edit") {
|
|
80
|
+
for (const key of ["file_path", "path"]) {
|
|
81
|
+
const value = args[key];
|
|
82
|
+
if (typeof value === "string" && value.trim().length > 0) paths.push(value.trim());
|
|
83
|
+
}
|
|
84
|
+
} else if (PROTECTED_PATH_TOOLS.has(toolName)) {
|
|
85
|
+
const patchText = typeof args.input === "string" ? args.input : typeof args.patch === "string" ? args.patch : "";
|
|
86
|
+
for (const line of patchText.split(/\r?\n/u)) {
|
|
87
|
+
if (paths.length >= MAX_PATHS_PER_CALL) break;
|
|
88
|
+
const match = /^\*\*\*\s+(?:Add File|Update File|Delete File|Move to):\s*(.+)$/u.exec(line);
|
|
89
|
+
if (match !== null) paths.push(match[1].trim());
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return [...new Set(paths)];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function policyHint() {
|
|
96
|
+
return "(bypass: DSH_GUARDRAILS=off, or edit ~/.dsh/guardrails.json)";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function denyDecision(reason, ruleId) {
|
|
100
|
+
return {
|
|
101
|
+
kind: "deny",
|
|
102
|
+
reason,
|
|
103
|
+
info: {
|
|
104
|
+
name: "GuardrailsDenied",
|
|
105
|
+
code: "GUARDRAILS_DENIED",
|
|
106
|
+
...(typeof ruleId === "string" ? { reason: ruleId } : {}),
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function askDecision(reason, ruleId) {
|
|
112
|
+
return {
|
|
113
|
+
kind: "ask",
|
|
114
|
+
reason,
|
|
115
|
+
displayReason: {
|
|
116
|
+
en: reason,
|
|
117
|
+
ru: `Guardrails: команда требует подтверждения (правило ${ruleId}).`,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Build the decision functions used by the event hooks. Pure state is read from
|
|
124
|
+
* `env` and from the policy file; the returned object is safe to share between
|
|
125
|
+
* listeners.
|
|
126
|
+
*
|
|
127
|
+
* @param {{env?: object, policyFile?: string, logger?: object}} [options] - environment, optional policy path override and logger.
|
|
128
|
+
* @returns {{load: () => object, preExecute: Function, fsIntent: Function}} guard.
|
|
129
|
+
*/
|
|
130
|
+
export function createGuard(options = {}) {
|
|
131
|
+
const env = options.env ?? process.env;
|
|
132
|
+
const policyFile = options.policyFile;
|
|
133
|
+
const logger = options.logger;
|
|
134
|
+
const load = () => loadPolicy({ env, ...(policyFile === undefined ? {} : { filePath: policyFile }) });
|
|
135
|
+
async function allowPreTool(next) {
|
|
136
|
+
return typeof next === "function" ? next() : { kind: "allow" };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function preExecute(exec, next) {
|
|
140
|
+
let loaded;
|
|
141
|
+
try {
|
|
142
|
+
loaded = load();
|
|
143
|
+
} catch (error) {
|
|
144
|
+
logger?.warn?.(`[guardrails] policy load failed; allowing call: ${errorMessage(error)}`);
|
|
145
|
+
return allowPreTool(next);
|
|
146
|
+
}
|
|
147
|
+
if (!loaded.enabled || loaded.policy.hooks.toolsPreExecute === false) return allowPreTool(next);
|
|
148
|
+
try {
|
|
149
|
+
const toolName = typeof exec?.name === "string" ? exec.name : "";
|
|
150
|
+
if (SHELL_TOOL_NAMES.has(toolName)) {
|
|
151
|
+
const command = extractShellCommand(exec?.arguments);
|
|
152
|
+
if (command.length > 0) {
|
|
153
|
+
const verdict = evaluateCommand(command, { policy: loaded.policy });
|
|
154
|
+
if (verdict.action === "deny" || verdict.action === "ask") {
|
|
155
|
+
const ruleId = verdict.primary?.id ?? "policy";
|
|
156
|
+
logger?.debug?.(`[guardrails] pre-execute ${verdict.action} tool=${toolName} rule=${ruleId}`);
|
|
157
|
+
if (verdict.action === "deny") {
|
|
158
|
+
return denyDecision(
|
|
159
|
+
`guardrails: command denied by rule "${ruleId}" (${verdict.primary?.description ?? "policy"}). ${policyHint()}`,
|
|
160
|
+
ruleId,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return askDecision(
|
|
164
|
+
`guardrails: command requires confirmation by rule "${ruleId}" (${verdict.primary?.description ?? "policy"})`,
|
|
165
|
+
ruleId,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (PROTECTED_PATH_TOOLS.has(toolName)) {
|
|
171
|
+
for (const requestedPath of requestedWritePaths(toolName, exec?.arguments)) {
|
|
172
|
+
const hit = matchProtectedPath(requestedPath, { policy: loaded.policy, env });
|
|
173
|
+
if (hit === undefined) continue;
|
|
174
|
+
logger?.debug?.(`[guardrails] pre-execute deny tool=${toolName} rule=${hit.id}`);
|
|
175
|
+
return denyDecision(
|
|
176
|
+
`guardrails: modifying a protected path is not allowed: ${requestedPath} (rule: ${hit.id}). ${policyHint()}`,
|
|
177
|
+
hit.id,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const mode = typeof options.modeFor === "function" ? options.modeFor(exec?.agent) : undefined;
|
|
182
|
+
const modeHit = modeDecision(mode, exec, exec?.agent?.session?.header?.cwd ?? process.cwd());
|
|
183
|
+
if (modeHit) {
|
|
184
|
+
logger?.debug?.(`[guardrails] pre-execute mode=${mode?.name ?? "?"} action=${modeHit.kind} tool=${toolName}`);
|
|
185
|
+
if (modeHit.kind === "ask") return askDecision(modeHit.reason, "permission-mode");
|
|
186
|
+
return denyDecision(modeHit.reason, "permission-mode");
|
|
187
|
+
}
|
|
188
|
+
return allowPreTool(next);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
// Conservative allow: a guard failure must never break a normal call.
|
|
191
|
+
logger?.warn?.(`[guardrails] pre-execute check skipped: ${errorMessage(error)}`);
|
|
192
|
+
return allowPreTool(next);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function allowIntent(next) {
|
|
197
|
+
return typeof next === "function" ? next() : undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function fsIntent(kind, target, next) {
|
|
201
|
+
let loaded;
|
|
202
|
+
try {
|
|
203
|
+
loaded = load();
|
|
204
|
+
} catch (error) {
|
|
205
|
+
logger?.warn?.(`[guardrails] policy load failed; allowing fs/${kind}: ${errorMessage(error)}`);
|
|
206
|
+
return allowIntent(next);
|
|
207
|
+
}
|
|
208
|
+
if (!loaded.enabled || loaded.policy.hooks.fsIntents === false) return allowIntent(next);
|
|
209
|
+
try {
|
|
210
|
+
const displayPath = typeof target?.displayPath === "string" ? target.displayPath : "";
|
|
211
|
+
if (displayPath.length === 0) return allowIntent(next);
|
|
212
|
+
const hit = matchProtectedPath(displayPath, { policy: loaded.policy, env });
|
|
213
|
+
if (hit === undefined) return allowIntent(next);
|
|
214
|
+
logger?.debug?.(`[guardrails] fs/${kind} protected rule=${hit.id}`);
|
|
215
|
+
const error = new Error(
|
|
216
|
+
`guardrails: ${kind} of a protected path is not allowed (rule: ${hit.id}). ${policyHint()}`,
|
|
217
|
+
);
|
|
218
|
+
error.name = "GuardrailsProtectedPathError";
|
|
219
|
+
error.code = "GUARDRAILS_PROTECTED_PATH";
|
|
220
|
+
error.ruleId = hit.id;
|
|
221
|
+
throw error;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (error?.code === "GUARDRAILS_PROTECTED_PATH") throw error;
|
|
224
|
+
logger?.warn?.(`[guardrails] fs/${kind} check skipped: ${errorMessage(error)}`);
|
|
225
|
+
return allowIntent(next);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return { load, preExecute, fsIntent };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Register all four Host listeners on a Cordis context. Registration failures
|
|
234
|
+
* are recorded in `state.errors` instead of taking the plugin down; the
|
|
235
|
+
* `status` action reports what is actually installed.
|
|
236
|
+
*
|
|
237
|
+
* @param {object} ctx - Cordis context with `on()`.
|
|
238
|
+
* @param {ReturnType<typeof createGuard>} guard - guard from `createGuard`.
|
|
239
|
+
* @param {ReturnType<typeof createHookState>} state - mutable registration state.
|
|
240
|
+
* @returns {ReturnType<typeof createHookState>} the same state.
|
|
241
|
+
*/
|
|
242
|
+
export function registerHooks(ctx, guard, state) {
|
|
243
|
+
const on = (event, handler) => {
|
|
244
|
+
try {
|
|
245
|
+
ctx.on(event, handler);
|
|
246
|
+
return true;
|
|
247
|
+
} catch (error) {
|
|
248
|
+
state.errors.push({ event, message: errorMessage(error) });
|
|
249
|
+
try {
|
|
250
|
+
ctx.logger?.warn?.(`[guardrails] hook ${event} was not registered: ${errorMessage(error)}`);
|
|
251
|
+
} catch {
|
|
252
|
+
/* logging must never break activation */
|
|
253
|
+
}
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
state.toolsPreExecute = on("tools/pre-execute", (exec, next) => guard.preExecute(exec, next));
|
|
258
|
+
state.fsWriteIntent = on("fs/write-intent", (target, actor, next) => guard.fsIntent("write", target, next));
|
|
259
|
+
state.fsEditIntent = on("fs/edit-intent", (target, actor, next) => guard.fsIntent("edit", target, next));
|
|
260
|
+
state.fsDeleteIntent = on("fs/delete-intent", (target, actor, next) => guard.fsIntent("delete", target, next));
|
|
261
|
+
return state;
|
|
262
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge DSH permission presets into the kit's tool guard.
|
|
3
|
+
*
|
|
4
|
+
* DSH already owns the picker and the approval card. The kit must not create a
|
|
5
|
+
* second one; it only enforces hard limits for the effective preset.
|
|
6
|
+
*/
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
|
|
9
|
+
const MUTATING_TOOLS = new Set(["write", "edit", "apply_patch", "delete", "move"]);
|
|
10
|
+
const SHELL_TOOLS = new Set(["bash", "pwsh", "shell"]);
|
|
11
|
+
const MUTATING_SHELL = /(?:^|[;&|]\s*|\s)(?:rm|rmdir|mv|cp|mkdir|touch|truncate|tee|dd|chmod|chown|Set-Content|Add-Content|Out-File|New-Item|Remove-Item|Move-Item|Copy-Item|git\s+(?:commit|push|reset|checkout|clean|merge|rebase)|npm\s+(?:install|uninstall|update)|pnpm\s+(?:add|remove|install)|composer\s+(?:install|update)|php\s+artisan\s+(?:migrate|db:)|dropdb|createdb|psql\b[^\n]*(?:insert|update|delete|drop|truncate|alter))/iu;
|
|
12
|
+
|
|
13
|
+
export function requestedPaths(toolName, args) {
|
|
14
|
+
if (!args || typeof args !== "object") return [];
|
|
15
|
+
const values = [];
|
|
16
|
+
for (const key of ["file_path", "path", "cwd", "target", "source", "destination"]) {
|
|
17
|
+
if (typeof args[key] === "string" && args[key].length > 0) values.push(args[key]);
|
|
18
|
+
}
|
|
19
|
+
if (Array.isArray(args.paths)) values.push(...args.paths.filter((value) => typeof value === "string"));
|
|
20
|
+
return values;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isMutatingShell(command) {
|
|
24
|
+
return typeof command === "string" && MUTATING_SHELL.test(command);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isInside(cwd, target) {
|
|
28
|
+
if (!cwd || !target) return false;
|
|
29
|
+
try {
|
|
30
|
+
const root = path.resolve(cwd);
|
|
31
|
+
const value = path.isAbsolute(target) ? path.resolve(target) : path.resolve(root, target);
|
|
32
|
+
return value === root || value.startsWith(root.endsWith(path.sep) ? root : `${root}${path.sep}`);
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {{name?: string, sandbox?: string, approval?: string}|null} mode
|
|
40
|
+
* @param {{name?: string, arguments?: unknown}} exec
|
|
41
|
+
* @param {string} cwd
|
|
42
|
+
* @returns {{kind:'deny'|'ask', reason:string}|null}
|
|
43
|
+
*/
|
|
44
|
+
export function modeDecision(mode, exec, cwd) {
|
|
45
|
+
if (!mode || mode.sandbox === "danger-full-access") return null;
|
|
46
|
+
const tool = String(exec?.name ?? "");
|
|
47
|
+
const args = exec?.arguments && typeof exec.arguments === "object" ? exec.arguments : {};
|
|
48
|
+
if (mode.sandbox === "read-only") {
|
|
49
|
+
if (MUTATING_TOOLS.has(tool)) {
|
|
50
|
+
return { kind: "deny", reason: `permission ${mode.name ?? "read-only"}: изменение файлов запрещено` };
|
|
51
|
+
}
|
|
52
|
+
if (SHELL_TOOLS.has(tool)) {
|
|
53
|
+
const command = typeof args.command === "string" ? args.command : "";
|
|
54
|
+
if (isMutatingShell(command)) return { kind: "deny", reason: `permission ${mode.name ?? "read-only"}: изменяющая shell-команда запрещена` };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (mode.sandbox === "workspace-write") {
|
|
58
|
+
if (MUTATING_TOOLS.has(tool)) {
|
|
59
|
+
const paths = requestedPaths(tool, args);
|
|
60
|
+
const outside = paths.find((target) => !isInside(cwd, target));
|
|
61
|
+
if (outside) return { kind: "ask", reason: `permission ${mode.name ?? "workspace-write"}: запись вне рабочей папки — ${outside}` };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function resolveMode(ctx, agent) {
|
|
68
|
+
try {
|
|
69
|
+
const service = ctx?.get?.("permissionPresets");
|
|
70
|
+
const session = agent?.session;
|
|
71
|
+
if (!service || !session) return null;
|
|
72
|
+
const name = service.current(session);
|
|
73
|
+
return { name, ...service.resolve(name) };
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|