@ddtcorex/dsh-maestro-guard 0.1.0 → 0.2.1
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/cordis.patch.yml +1 -0
- package/lib/approval-store.d.ts.map +1 -1
- package/lib/approval-store.js.map +1 -1
- package/lib/approve-tool.d.ts +25 -0
- package/lib/approve-tool.d.ts.map +1 -0
- package/lib/approve-tool.js +54 -0
- package/lib/approve-tool.js.map +1 -0
- package/lib/full-scan-tool.d.ts +7 -0
- package/lib/full-scan-tool.d.ts.map +1 -0
- package/lib/full-scan-tool.js +79 -0
- package/lib/full-scan-tool.js.map +1 -0
- package/lib/index.d.ts +2 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +144 -2
- package/lib/index.js.map +1 -1
- package/lib/pending.d.ts +44 -0
- package/lib/pending.d.ts.map +1 -0
- package/lib/pending.js +133 -0
- package/lib/pending.js.map +1 -0
- package/lib/permission-policy.d.ts.map +1 -1
- package/lib/permission-policy.js.map +1 -1
- package/lib/sandbox.d.ts +95 -0
- package/lib/sandbox.d.ts.map +1 -0
- package/lib/sandbox.js +321 -0
- package/lib/sandbox.js.map +1 -0
- package/lib/secret-redactor.d.ts.map +1 -1
- package/lib/secret-redactor.js.map +1 -1
- package/package.json +6 -1
- package/src/host/approve-tool.ts +61 -0
- package/src/host/full-scan-tool.ts +71 -0
- package/src/host/index.ts +184 -0
- package/src/host/pending.ts +144 -0
- package/src/host/sandbox.ts +322 -0
- package/src/sandbox.ts +5 -0
- package/src/index.ts +0 -38
- /package/src/{approval-store.ts → host/approval-store.ts} +0 -0
- /package/src/{augment.d.ts → host/augment.d.ts} +0 -0
- /package/src/{permission-policy.ts → host/permission-policy.ts} +0 -0
- /package/src/{secret-redactor.ts → host/secret-redactor.ts} +0 -0
package/lib/sandbox.d.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True if path points to a blocked credential / secret location.
|
|
3
|
+
* Covers: ~/.dsh/.credentials.yaml (any expansion), ~/.cloudflared, NPM_TOKEN, .credentials.yaml substring
|
|
4
|
+
* credentialPaths: additional custom paths from config (guard.credentialPaths) — merged with always-blocked defaults.
|
|
5
|
+
* The 3 always-blocked substrings stay blocked even when credentialPaths is empty.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isBlockedPath(input: string, credentialPaths?: string[]): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* True if command string is a publish command (pnpm|npm publish)
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the working directory a command actually executes in, when it names
|
|
13
|
+
* one explicitly (cd <dir> / git -C <dir>). Falls back to the passed cwd when
|
|
14
|
+
* the command has no explicit target — preserving the historical session-cwd
|
|
15
|
+
* semantics for commands that run in place.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getCommandWorkingDir(command: string | undefined, cwd: string | undefined): string | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Choose the branch used for protected-branch detection: the branch of the repo
|
|
20
|
+
* the command targets (via `getCommandWorkingDir`), falling back to the session
|
|
21
|
+
* cwd when the command runs in place. If the command cd's into a directory that
|
|
22
|
+
* is not a git repo, no protected branch applies (the push would fail there
|
|
23
|
+
* anyway) — do NOT fall back to the session cwd, that reintroduces the
|
|
24
|
+
* false-positive where a feature-branch push inside a sub-repo is blocked
|
|
25
|
+
* because the session cwd repo happens to sit on master.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveCurrentBranch(command: string | undefined, sessionCwd: string | undefined, branchOf: (dir: string) => string | undefined): string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* The executed command surface of a tool call. Shell-style tools carry their
|
|
30
|
+
* script in `args.command` (bash/exec/shell/govard_shell); bare string args are
|
|
31
|
+
* the command itself. Tools with no command field (read/write/memory/...) have
|
|
32
|
+
* no execution surface, so protected-op detection must not apply to their
|
|
33
|
+
* content — that was the source of the analysis-tool false positives.
|
|
34
|
+
*/
|
|
35
|
+
export declare function extractCommandText(args: unknown): string | undefined;
|
|
36
|
+
export declare function isBlockedCommand(cmd: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* True when the command executes a protected git operation. Detection is
|
|
39
|
+
* per-command-segment (split on && / ; / | / newline) so a `gh pr create
|
|
40
|
+
* --base master` mention after a feature push — or quoted text anywhere — does
|
|
41
|
+
* not turn a safe push into a blocked one. Hard rules (gh pr merge, gh release,
|
|
42
|
+
* protection deletion, protected branch words in the push segment, being
|
|
43
|
+
* checked out on a protected branch) keep their unconditional coverage.
|
|
44
|
+
*/
|
|
45
|
+
export declare function isBlockedGitCommand(cmd: string, currentBranch?: string, branches?: string[]): boolean;
|
|
46
|
+
export declare function isPublishBlocked(cmd: string, approved: boolean): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* True if target path is outside cwd (strict containment).
|
|
49
|
+
* Uses resolve for absolute comparison; expanded ~/ handled.
|
|
50
|
+
*/
|
|
51
|
+
export declare function isOutsideCwd(target: string, cwd: string): boolean;
|
|
52
|
+
export interface SandboxCheckResult {
|
|
53
|
+
blocked: boolean;
|
|
54
|
+
reason?: string;
|
|
55
|
+
}
|
|
56
|
+
export interface CheckSandboxOpts {
|
|
57
|
+
cwd?: string;
|
|
58
|
+
currentBranch?: string;
|
|
59
|
+
approved?: boolean;
|
|
60
|
+
credentialPaths?: string[];
|
|
61
|
+
gitProtection?: {
|
|
62
|
+
enabled: boolean;
|
|
63
|
+
branches: string[];
|
|
64
|
+
};
|
|
65
|
+
publishBlocked?: boolean;
|
|
66
|
+
cwdContainment?: boolean;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Central sandbox check. Combines credential-path, git-protection, publish, and cwd containment.
|
|
70
|
+
* @param tool tool name (e.g. maestro_read_file, exec, bash)
|
|
71
|
+
* @param args tool arguments (object, string, or unknown)
|
|
72
|
+
* @param opts.cwd session cwd (exec.agent.session.header.cwd)
|
|
73
|
+
* @param opts.currentBranch git current branch (from getCurrentBranch)
|
|
74
|
+
* @param opts.approved whether publish/git is APPROVED (via ApprovalStore)
|
|
75
|
+
* @param opts.credentialPaths additional blocked credential paths from guard config
|
|
76
|
+
* @param opts.gitProtection git protection toggle + branches from guard config
|
|
77
|
+
* @param opts.publishBlocked whether publish is blocked (default true)
|
|
78
|
+
* @param opts.cwdContainment whether cwd containment is enforced (default true)
|
|
79
|
+
*/
|
|
80
|
+
export declare function checkSandbox(tool: string, args: unknown, opts?: CheckSandboxOpts): SandboxCheckResult;
|
|
81
|
+
/**
|
|
82
|
+
* Simple guard per task 5 snippet: guard(p) => false if blocked, true if allowed.
|
|
83
|
+
* For publish, second arg approved indicates APPROVED.
|
|
84
|
+
*/
|
|
85
|
+
export declare function guard(p: string, approved?: boolean): boolean;
|
|
86
|
+
export declare const sandbox: {
|
|
87
|
+
isBlockedPath: typeof isBlockedPath;
|
|
88
|
+
isBlockedCommand: typeof isBlockedCommand;
|
|
89
|
+
isBlockedGitCommand: typeof isBlockedGitCommand;
|
|
90
|
+
isPublishBlocked: typeof isPublishBlocked;
|
|
91
|
+
isOutsideCwd: typeof isOutsideCwd;
|
|
92
|
+
checkSandbox: typeof checkSandbox;
|
|
93
|
+
guard: typeof guard;
|
|
94
|
+
};
|
|
95
|
+
//# sourceMappingURL=sandbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../src/host/sandbox.ts"],"names":[],"mappings":"AAoBA;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAoChF;AAED;;GAEG;AACH;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAkB7G;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GAC5C,MAAM,GAAG,SAAS,CAIpB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAOpE;AAOD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAIrD;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAsCrG;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CAGxE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CASjE;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;IAC1B,aAAa,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;KAAE,CAAA;IACxD,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,EACb,IAAI,CAAC,EAAE,gBAAgB,GACtB,kBAAkB,CAsEpB;AAED;;;GAGG;AACH,wBAAgB,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,OAAO,CAK5D;AAED,eAAO,MAAM,OAAO;;;;;;;;CAAgH,CAAA"}
|
package/lib/sandbox.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join, resolve, normalize } from 'node:path';
|
|
3
|
+
function expandHome(p) {
|
|
4
|
+
if (!p)
|
|
5
|
+
return p;
|
|
6
|
+
if (p === '~')
|
|
7
|
+
return homedir();
|
|
8
|
+
if (p.startsWith('~/'))
|
|
9
|
+
return join(homedir(), p.slice(2));
|
|
10
|
+
if (p.startsWith('$HOME/'))
|
|
11
|
+
return join(homedir(), p.slice(6));
|
|
12
|
+
if (p.startsWith('${HOME}/'))
|
|
13
|
+
return join(homedir(), p.slice(8));
|
|
14
|
+
return p;
|
|
15
|
+
}
|
|
16
|
+
function normalizePath(p) {
|
|
17
|
+
try {
|
|
18
|
+
return normalize(expandHome(p));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return expandHome(p);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* True if path points to a blocked credential / secret location.
|
|
26
|
+
* Covers: ~/.dsh/.credentials.yaml (any expansion), ~/.cloudflared, NPM_TOKEN, .credentials.yaml substring
|
|
27
|
+
* credentialPaths: additional custom paths from config (guard.credentialPaths) — merged with always-blocked defaults.
|
|
28
|
+
* The 3 always-blocked substrings stay blocked even when credentialPaths is empty.
|
|
29
|
+
*/
|
|
30
|
+
export function isBlockedPath(input, credentialPaths) {
|
|
31
|
+
if (!input || typeof input !== 'string')
|
|
32
|
+
return false;
|
|
33
|
+
const trimmed = input.trim();
|
|
34
|
+
if (!trimmed)
|
|
35
|
+
return false;
|
|
36
|
+
// Direct substring checks (covers JSON-stringified args, env leakage, etc.) — always blocked
|
|
37
|
+
if (trimmed.includes('.credentials.yaml'))
|
|
38
|
+
return true;
|
|
39
|
+
if (trimmed.includes('.cloudflared'))
|
|
40
|
+
return true;
|
|
41
|
+
if (trimmed.includes('NPM_TOKEN'))
|
|
42
|
+
return true;
|
|
43
|
+
if (trimmed.includes('.dsh') && trimmed.includes('credentials'))
|
|
44
|
+
return true;
|
|
45
|
+
// Normalized expanded check for defaults
|
|
46
|
+
const norm = normalizePath(trimmed);
|
|
47
|
+
if (norm.includes('.credentials.yaml'))
|
|
48
|
+
return true;
|
|
49
|
+
if (norm.includes('.cloudflared'))
|
|
50
|
+
return true;
|
|
51
|
+
// Check absolute homedir variant
|
|
52
|
+
const absCred = join(homedir(), '.dsh', '.credentials.yaml');
|
|
53
|
+
if (norm === absCred || norm.startsWith(absCred))
|
|
54
|
+
return true;
|
|
55
|
+
const absCf = join(homedir(), '.cloudflared');
|
|
56
|
+
if (norm === absCf || norm.startsWith(absCf + '/') || norm.includes('.cloudflared'))
|
|
57
|
+
return true;
|
|
58
|
+
// Injected credentialPaths from config (additional) — substring + normalized + prefix
|
|
59
|
+
if (credentialPaths && credentialPaths.length > 0) {
|
|
60
|
+
for (const p of credentialPaths) {
|
|
61
|
+
if (!p || typeof p !== 'string')
|
|
62
|
+
continue;
|
|
63
|
+
const t = p.trim();
|
|
64
|
+
if (!t)
|
|
65
|
+
continue;
|
|
66
|
+
if (trimmed.includes(t))
|
|
67
|
+
return true;
|
|
68
|
+
const normP = normalizePath(t);
|
|
69
|
+
if (norm.includes(normP))
|
|
70
|
+
return true;
|
|
71
|
+
if (norm === normP)
|
|
72
|
+
return true;
|
|
73
|
+
if (norm.startsWith(normP + '/'))
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* True if command string is a publish command (pnpm|npm publish)
|
|
81
|
+
*/
|
|
82
|
+
/**
|
|
83
|
+
* Resolve the working directory a command actually executes in, when it names
|
|
84
|
+
* one explicitly (cd <dir> / git -C <dir>). Falls back to the passed cwd when
|
|
85
|
+
* the command has no explicit target — preserving the historical session-cwd
|
|
86
|
+
* semantics for commands that run in place.
|
|
87
|
+
*/
|
|
88
|
+
export function getCommandWorkingDir(command, cwd) {
|
|
89
|
+
if (!command || !cwd)
|
|
90
|
+
return cwd ?? undefined;
|
|
91
|
+
const hasCdVerb = /\bcd\b/.test(command) || /\bgit\s+-C\b/.test(command);
|
|
92
|
+
const cd = /\bcd\s+([^\s;&|"'`${}]+)(?:\s*(?:[;&|]|$))/.exec(command);
|
|
93
|
+
const c = /\bgit\s+-C\s+([^\s;&|"'`${}]+)/.exec(command);
|
|
94
|
+
const dir = cd?.[1] ?? c?.[1];
|
|
95
|
+
if (!dir) {
|
|
96
|
+
// A cd/-C verb is present but its target cannot be parsed (quoted, $VAR,
|
|
97
|
+
// wildcard, bare `cd`): do NOT assume the session cwd — that reintroduces
|
|
98
|
+
// the false positive when the session cwd repo sits on a protected branch.
|
|
99
|
+
// Unknown target means no protected-branch assumption (segment word checks
|
|
100
|
+
// still apply); commands with no cd verb keep the session-cwd semantics.
|
|
101
|
+
if (hasCdVerb)
|
|
102
|
+
return undefined;
|
|
103
|
+
return cwd;
|
|
104
|
+
}
|
|
105
|
+
if (dir === '~')
|
|
106
|
+
return homedir();
|
|
107
|
+
if (dir.startsWith('~/'))
|
|
108
|
+
return join(homedir(), dir.slice(2));
|
|
109
|
+
return resolve(cwd, dir);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Choose the branch used for protected-branch detection: the branch of the repo
|
|
113
|
+
* the command targets (via `getCommandWorkingDir`), falling back to the session
|
|
114
|
+
* cwd when the command runs in place. If the command cd's into a directory that
|
|
115
|
+
* is not a git repo, no protected branch applies (the push would fail there
|
|
116
|
+
* anyway) — do NOT fall back to the session cwd, that reintroduces the
|
|
117
|
+
* false-positive where a feature-branch push inside a sub-repo is blocked
|
|
118
|
+
* because the session cwd repo happens to sit on master.
|
|
119
|
+
*/
|
|
120
|
+
export function resolveCurrentBranch(command, sessionCwd, branchOf) {
|
|
121
|
+
const dir = getCommandWorkingDir(command, sessionCwd);
|
|
122
|
+
if (!dir)
|
|
123
|
+
return undefined;
|
|
124
|
+
return branchOf(dir);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The executed command surface of a tool call. Shell-style tools carry their
|
|
128
|
+
* script in `args.command` (bash/exec/shell/govard_shell); bare string args are
|
|
129
|
+
* the command itself. Tools with no command field (read/write/memory/...) have
|
|
130
|
+
* no execution surface, so protected-op detection must not apply to their
|
|
131
|
+
* content — that was the source of the analysis-tool false positives.
|
|
132
|
+
*/
|
|
133
|
+
export function extractCommandText(args) {
|
|
134
|
+
if (args == null)
|
|
135
|
+
return undefined;
|
|
136
|
+
if (typeof args === 'string')
|
|
137
|
+
return args;
|
|
138
|
+
if (typeof args === 'object' && typeof args.command === 'string') {
|
|
139
|
+
return args.command;
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
/** Collapse quoted spans — text inside quotes is data (echo/printf/script bodies), not argv. */
|
|
144
|
+
function stripQuoted(cmd) {
|
|
145
|
+
return cmd.replace(/"[^"]*"/g, ' ').replace(/'[^']*'/g, ' ');
|
|
146
|
+
}
|
|
147
|
+
export function isBlockedCommand(cmd) {
|
|
148
|
+
if (!cmd || typeof cmd !== 'string')
|
|
149
|
+
return false;
|
|
150
|
+
// package-manager publish verbs as a whole-word sequence
|
|
151
|
+
return /\b(pnpm|npm)\s+publish\b/.test(stripQuoted(cmd));
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* True when the command executes a protected git operation. Detection is
|
|
155
|
+
* per-command-segment (split on && / ; / | / newline) so a `gh pr create
|
|
156
|
+
* --base master` mention after a feature push — or quoted text anywhere — does
|
|
157
|
+
* not turn a safe push into a blocked one. Hard rules (gh pr merge, gh release,
|
|
158
|
+
* protection deletion, protected branch words in the push segment, being
|
|
159
|
+
* checked out on a protected branch) keep their unconditional coverage.
|
|
160
|
+
*/
|
|
161
|
+
export function isBlockedGitCommand(cmd, currentBranch, branches) {
|
|
162
|
+
if (!cmd || typeof cmd !== 'string')
|
|
163
|
+
return false;
|
|
164
|
+
const effectiveBranches = branches && branches.length > 0 ? branches : ['master', 'main'];
|
|
165
|
+
const lowerBranches = effectiveBranches.map((b) => b.toLowerCase());
|
|
166
|
+
const escBranch = (b) => b.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
167
|
+
// Strip quoted spans on the FULL text BEFORE segmenting: a quoted block (an
|
|
168
|
+
// instruction prompt, a multiline -e body) legitimately spans &&/;|/newline
|
|
169
|
+
// separators, and segment-then-strip would fragment it, exposing the words.
|
|
170
|
+
const stripped = stripQuoted(cmd);
|
|
171
|
+
const segments = stripped.split(/\s*(?:&&|\|\||;|\||\r?\n)+\s*/);
|
|
172
|
+
for (const seg of segments) {
|
|
173
|
+
const lower = seg.toLowerCase();
|
|
174
|
+
// hard rules are unconditional per segment
|
|
175
|
+
if (/\bgh\s+pr\s+merge\b/.test(lower))
|
|
176
|
+
return true;
|
|
177
|
+
if (/\bgh\s+release\s+(create|publish)\b/.test(lower))
|
|
178
|
+
return true;
|
|
179
|
+
for (const b of lowerBranches) {
|
|
180
|
+
const re = new RegExp(`gh\\s+api\\b.*delete.*\\/branches\\/${escBranch(b)}\\/protection`);
|
|
181
|
+
if (re.test(lower))
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
// git push: protected branch word in THIS segment, checked out on one, or
|
|
185
|
+
// pushing a release tag (version tags trigger the CI publish workflow and
|
|
186
|
+
// must follow the same human-approval rule as gh release create)
|
|
187
|
+
if (/\bgit\s+push\b/.test(lower)) {
|
|
188
|
+
for (const b of lowerBranches) {
|
|
189
|
+
if (new RegExp(`\\b${escBranch(b)}\\b`).test(lower))
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
if (/refs\/tags\//.test(lower))
|
|
193
|
+
return true;
|
|
194
|
+
// semver-like tag as a push refspec: standalone token (space/start
|
|
195
|
+
// preceded), so a branch name like feat/1.2.3 is not a false positive
|
|
196
|
+
if (/(?:^|\s)v?\d+\.\d+\.\d+(?:[-+][0-9a-z.]+)?\b/.test(lower))
|
|
197
|
+
return true;
|
|
198
|
+
if (currentBranch) {
|
|
199
|
+
const curLower = currentBranch.toLowerCase();
|
|
200
|
+
if (lowerBranches.includes(curLower))
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// git tag push that includes protected branch (rare) — already covered by push regex
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
export function isPublishBlocked(cmd, approved) {
|
|
209
|
+
if (!isBlockedCommand(cmd))
|
|
210
|
+
return false;
|
|
211
|
+
return !approved;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* True if target path is outside cwd (strict containment).
|
|
215
|
+
* Uses resolve for absolute comparison; expanded ~/ handled.
|
|
216
|
+
*/
|
|
217
|
+
export function isOutsideCwd(target, cwd) {
|
|
218
|
+
if (!target || !cwd)
|
|
219
|
+
return false;
|
|
220
|
+
const expTarget = expandHome(target);
|
|
221
|
+
const expCwd = expandHome(cwd);
|
|
222
|
+
const resolvedTarget = resolve(expTarget);
|
|
223
|
+
const resolvedCwd = resolve(expCwd);
|
|
224
|
+
if (resolvedTarget === resolvedCwd)
|
|
225
|
+
return false;
|
|
226
|
+
// Ensure cwd prefix with separator to avoid /tmp/proj matching /tmp/proj2
|
|
227
|
+
return !resolvedTarget.startsWith(resolvedCwd + '/');
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Central sandbox check. Combines credential-path, git-protection, publish, and cwd containment.
|
|
231
|
+
* @param tool tool name (e.g. maestro_read_file, exec, bash)
|
|
232
|
+
* @param args tool arguments (object, string, or unknown)
|
|
233
|
+
* @param opts.cwd session cwd (exec.agent.session.header.cwd)
|
|
234
|
+
* @param opts.currentBranch git current branch (from getCurrentBranch)
|
|
235
|
+
* @param opts.approved whether publish/git is APPROVED (via ApprovalStore)
|
|
236
|
+
* @param opts.credentialPaths additional blocked credential paths from guard config
|
|
237
|
+
* @param opts.gitProtection git protection toggle + branches from guard config
|
|
238
|
+
* @param opts.publishBlocked whether publish is blocked (default true)
|
|
239
|
+
* @param opts.cwdContainment whether cwd containment is enforced (default true)
|
|
240
|
+
*/
|
|
241
|
+
export function checkSandbox(tool, args, opts) {
|
|
242
|
+
const approved = !!opts?.approved;
|
|
243
|
+
const cwd = opts?.cwd;
|
|
244
|
+
const currentBranch = opts?.currentBranch;
|
|
245
|
+
const credentialPaths = opts?.credentialPaths;
|
|
246
|
+
const gitProtection = opts?.gitProtection;
|
|
247
|
+
const publishBlocked = opts?.publishBlocked ?? true;
|
|
248
|
+
const cwdContainment = opts?.cwdContainment ?? true;
|
|
249
|
+
// Serialize args for generic substring checks
|
|
250
|
+
const asText = args != null ? (typeof args === 'string' ? args : JSON.stringify(args)) : '';
|
|
251
|
+
const combined = `${tool ?? ''} ${asText}`;
|
|
252
|
+
// Protected-op detection (git/publish) runs on the executed command surface
|
|
253
|
+
// only — non-shell tools (write/read/memory/...) have no command and must not
|
|
254
|
+
// be flagged for text that merely mentions a protected phrase.
|
|
255
|
+
const execText = extractCommandText(args);
|
|
256
|
+
// 1) Block credential paths anywhere in tool+args (always, with injected list)
|
|
257
|
+
if (isBlockedPath(tool, credentialPaths) || isBlockedPath(asText, credentialPaths) || isBlockedPath(combined, credentialPaths)) {
|
|
258
|
+
return { blocked: true, reason: 'credential path blocked: ~/.dsh/.credentials.yaml or ~/.cloudflared or NPM_TOKEN' };
|
|
259
|
+
}
|
|
260
|
+
// 2) Block git push to protected branches without APPROVED (branch-aware, toggle-aware)
|
|
261
|
+
const gitEnabled = gitProtection?.enabled ?? true;
|
|
262
|
+
const branches = gitProtection?.branches ?? ['master', 'main'];
|
|
263
|
+
if (gitEnabled && execText && isBlockedGitCommand(execText, currentBranch, branches) && !approved) {
|
|
264
|
+
return { blocked: true, reason: 'git push to master/main blocked without APPROVED: ' + execText.slice(0, 300) };
|
|
265
|
+
}
|
|
266
|
+
// 3) Block publish without APPROVED (toggle-aware)
|
|
267
|
+
if (publishBlocked && execText && isBlockedCommand(execText) && !approved) {
|
|
268
|
+
return { blocked: true, reason: 'publish blocked without APPROVED: pnpm publish requires approval' };
|
|
269
|
+
}
|
|
270
|
+
// 4) Block maestro file tools outside cwd (toggle-aware)
|
|
271
|
+
const fileTools = new Set(['maestro_read_file', 'maestro_write_file', 'fs_read', 'fs_write', 'read_file', 'write_file']);
|
|
272
|
+
if (cwdContainment && cwd && fileTools.has(tool)) {
|
|
273
|
+
let pathVal;
|
|
274
|
+
if (typeof args === 'object' && args !== null) {
|
|
275
|
+
const a = args;
|
|
276
|
+
// common keys
|
|
277
|
+
pathVal = a.path ?? a.file ?? a.file_path ?? a.filePath;
|
|
278
|
+
// array-like args: {0: 'path'}
|
|
279
|
+
if (!pathVal && typeof a[0] === 'string')
|
|
280
|
+
pathVal = a[0];
|
|
281
|
+
// if args itself has nested command with path
|
|
282
|
+
if (!pathVal && typeof a.command === 'string') {
|
|
283
|
+
// command may contain path; fallback to blocked path check already done, but also cwd check if command contains path
|
|
284
|
+
// no explicit path to check
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
else if (typeof args === 'string') {
|
|
288
|
+
pathVal = args;
|
|
289
|
+
}
|
|
290
|
+
if (pathVal) {
|
|
291
|
+
if (isBlockedPath(pathVal, credentialPaths)) {
|
|
292
|
+
return { blocked: true, reason: `credential path blocked: ${pathVal}` };
|
|
293
|
+
}
|
|
294
|
+
if (isOutsideCwd(pathVal, cwd)) {
|
|
295
|
+
return { blocked: true, reason: `path outside cwd: ${pathVal} not in ${cwd}` };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// Also check generic exec/bash tool with cwd containment if it looks like a file path arg
|
|
300
|
+
// (optional: not strictly required for credential/publish but supports broader sandbox)
|
|
301
|
+
if (cwd && (tool === 'exec' || tool === 'bash' || tool === 'shell')) {
|
|
302
|
+
// already covered publish; for credential path we already blocked above via substring
|
|
303
|
+
// No additional cwd check for shell commands unless they are obvious file paths
|
|
304
|
+
}
|
|
305
|
+
return { blocked: false };
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Simple guard per task 5 snippet: guard(p) => false if blocked, true if allowed.
|
|
309
|
+
* For publish, second arg approved indicates APPROVED.
|
|
310
|
+
*/
|
|
311
|
+
export function guard(p, approved) {
|
|
312
|
+
if (!p || typeof p !== 'string')
|
|
313
|
+
return true;
|
|
314
|
+
if (isBlockedPath(p))
|
|
315
|
+
return false;
|
|
316
|
+
if (isBlockedCommand(p) && !approved)
|
|
317
|
+
return false;
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
export const sandbox = { isBlockedPath, isBlockedCommand, isBlockedGitCommand, isPublishBlocked, isOutsideCwd, checkSandbox, guard };
|
|
321
|
+
//# sourceMappingURL=sandbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../src/host/sandbox.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-redactor.d.ts","sourceRoot":"","sources":["../src/secret-redactor.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,EAAE,MAAM,EAKpC,CAAA;AACD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD;AACD,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI3C"}
|
|
1
|
+
{"version":3,"file":"secret-redactor.d.ts","sourceRoot":"","sources":["../src/host/secret-redactor.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,EAAE,MAAM,EAKpC,CAAA;AACD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD;AACD,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI3C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-redactor.js","sourceRoot":"","sources":["../src/secret-redactor.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,gBAAgB,GAAa;IACxC,2BAA2B;IAC3B,sBAAsB;IACtB,uBAAuB;IACvB,2BAA2B;CAC5B,CAAA;AACD,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,SAAS,GAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA,CAAC,CAAC,CAAC,CAAA;AAC3E,CAAC;AACD,MAAM,UAAU,MAAM,CAAC,IAAY;IACjC,IAAI,GAAG,GAAG,IAAI,CAAA;IACd,KAAK,MAAM,CAAC,IAAI,gBAAgB,EAAE,CAAC;QAAC,CAAC,CAAC,SAAS,GAAC,CAAC,CAAC;QAAC,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC,EAAE,YAAY,CAAC,CAAA;IAAC,CAAC;IACvF,OAAO,GAAG,CAAA;AACZ,CAAC"}
|
|
1
|
+
{"version":3,"file":"secret-redactor.js","sourceRoot":"","sources":["../src/host/secret-redactor.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,gBAAgB,GAAa;IACxC,2BAA2B;IAC3B,sBAAsB;IACtB,uBAAuB;IACvB,2BAA2B;CAC5B,CAAA;AACD,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,SAAS,GAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA,CAAC,CAAC,CAAC,CAAA;AAC3E,CAAC;AACD,MAAM,UAAU,MAAM,CAAC,IAAY;IACjC,IAAI,GAAG,GAAG,IAAI,CAAA;IACd,KAAK,MAAM,CAAC,IAAI,gBAAgB,EAAE,CAAC;QAAC,CAAC,CAAC,SAAS,GAAC,CAAC,CAAC;QAAC,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC,EAAE,YAAY,CAAC,CAAA;IAAC,CAAC;IACvF,OAAO,GAAG,CAAA;AACZ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ddtcorex/dsh-maestro-guard",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"private": false,
|
|
4
5
|
"description": "Maestro Guard — waterfall tools/pre-execute approval + blocklist stub (Phase A scaffold)",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"main": "./lib/index.js",
|
|
@@ -20,11 +21,15 @@
|
|
|
20
21
|
"patch": "./cordis.patch.yml"
|
|
21
22
|
}
|
|
22
23
|
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@ddtcorex/dsh-maestro-config-lib": "^0.1.2"
|
|
26
|
+
},
|
|
23
27
|
"peerDependencies": {
|
|
24
28
|
"@deepseek-ai/cordis": "^4.0.1"
|
|
25
29
|
},
|
|
26
30
|
"devDependencies": {
|
|
27
31
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
32
|
+
"@deepseek-ai/dsh-tools": "0.1.2-alpha.2",
|
|
28
33
|
"typescript": "^5.8.0",
|
|
29
34
|
"vitest": "^2.0.0",
|
|
30
35
|
"@types/node": "^22.0.0"
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
2
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
3
|
+
import type { PendingRequest, PendingStore } from './pending.js'
|
|
4
|
+
|
|
5
|
+
export const name = 'maestro-guard-approve-tools'
|
|
6
|
+
export const inject = ['tools'] as const
|
|
7
|
+
|
|
8
|
+
export interface ApproveResult { ok: boolean; requestId: string; scope: string; status: string; message?: string }
|
|
9
|
+
export interface ListResult { ok: true; requests: PendingRequest[] }
|
|
10
|
+
|
|
11
|
+
export function createApproveTools(deps: { pending: PendingStore }) {
|
|
12
|
+
return {
|
|
13
|
+
async approve(requestId: string): Promise<ApproveResult> {
|
|
14
|
+
if (typeof requestId !== 'string' || requestId.length === 0) {
|
|
15
|
+
return { ok: false, requestId: '', scope: '', status: 'error', message: 'requestId (string) is required' }
|
|
16
|
+
}
|
|
17
|
+
const req = await deps.pending.approve(requestId)
|
|
18
|
+
if (!req) {
|
|
19
|
+
return { ok: false, requestId, scope: '', status: 'error', message: `no pending request with id ${requestId}` }
|
|
20
|
+
}
|
|
21
|
+
return { ok: true, requestId: req.id, scope: req.scope, status: req.status }
|
|
22
|
+
},
|
|
23
|
+
async list(): Promise<ListResult> {
|
|
24
|
+
return { ok: true, requests: await deps.pending.list() }
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function applyApproveTools(ctx: Context, deps: { pending: PendingStore }): void {
|
|
30
|
+
const tools = createApproveTools(deps)
|
|
31
|
+
ctx.effect(() => ctx.tools.register(defineTool({
|
|
32
|
+
name: 'maestro_guard_approve',
|
|
33
|
+
description: 'Approve one recorded guard request so its exact blocked operation may run once. Call after the human approves the operation in the conversation.',
|
|
34
|
+
parameters: { requestId: { type: 'string', description: 'the request id from the guard block message or maestro_guard_list' } },
|
|
35
|
+
output: {
|
|
36
|
+
schema: {
|
|
37
|
+
type: 'object', additionalProperties: true,
|
|
38
|
+
properties: { ok: { type: 'boolean', required: true }, requestId: { type: 'string', required: true }, scope: { type: 'string', required: true }, status: { type: 'string', required: true }, message: { type: 'string' } },
|
|
39
|
+
},
|
|
40
|
+
render: (_a: unknown, v: any) => [{ type: 'text', text: v.ok ? `approved request ${v.requestId} (scope ${v.scope})` : `approve failed: ${v.message ?? v.status}` }],
|
|
41
|
+
},
|
|
42
|
+
async execute(args: any) {
|
|
43
|
+
return tools.approve((args as any)?.requestId) as any
|
|
44
|
+
},
|
|
45
|
+
})))
|
|
46
|
+
ctx.effect(() => ctx.tools.register(defineTool({
|
|
47
|
+
name: 'maestro_guard_list',
|
|
48
|
+
description: 'List guard tickets (pending/approved/consumed) with redacted commands. Read-only.',
|
|
49
|
+
parameters: {},
|
|
50
|
+
output: {
|
|
51
|
+
schema: {
|
|
52
|
+
type: 'object', additionalProperties: true,
|
|
53
|
+
properties: { ok: { type: 'boolean', required: true }, requests: { type: 'array', required: true, items: { type: 'object', additionalProperties: true } } },
|
|
54
|
+
},
|
|
55
|
+
render: (_a: unknown, v: any) => [{ type: 'text', text: (v.requests ?? []).map((r: any) => `${r.status} ${r.id} ${r.scope} @${r.requestedAt} ${String(r.command ?? '').slice(0, 80)}`).join('\n') || 'no guard requests' }],
|
|
56
|
+
},
|
|
57
|
+
async execute() {
|
|
58
|
+
return tools.list() as any
|
|
59
|
+
},
|
|
60
|
+
})))
|
|
61
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { resolve, join } from 'node:path'
|
|
4
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
5
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
6
|
+
export const name = 'maestro-full-scan-tool'
|
|
7
|
+
export const inject = ['tools'] as const
|
|
8
|
+
function workspaceRootFor(c:string|undefined, e:unknown): string {
|
|
9
|
+
if (c) return c
|
|
10
|
+
const cwd=(e as any)?.agent?.session?.header?.cwd
|
|
11
|
+
return typeof cwd==='string'&&cwd?cwd:process.cwd()
|
|
12
|
+
}
|
|
13
|
+
function resolveScript(root:string): string {
|
|
14
|
+
const direct = join(root, 'scripts/enforce-rules.mjs')
|
|
15
|
+
if (existsSync(direct)) return direct
|
|
16
|
+
// fallback: walk up from root and from process.cwd() to find workspace root containing scripts/enforce-rules.mjs
|
|
17
|
+
const candidates: string[] = []
|
|
18
|
+
// try ancestors of root
|
|
19
|
+
let cur = resolve(root)
|
|
20
|
+
for (let i=0;i<8;i++) {
|
|
21
|
+
const cand = join(cur, 'scripts/enforce-rules.mjs')
|
|
22
|
+
if (existsSync(cand)) return cand
|
|
23
|
+
candidates.push(cand)
|
|
24
|
+
const parent = resolve(cur, '..')
|
|
25
|
+
if (parent===cur) break
|
|
26
|
+
cur = parent
|
|
27
|
+
}
|
|
28
|
+
// try ancestors of process.cwd()
|
|
29
|
+
cur = process.cwd()
|
|
30
|
+
for (let i=0;i<8;i++) {
|
|
31
|
+
const cand = join(cur, 'scripts/enforce-rules.mjs')
|
|
32
|
+
if (existsSync(cand)) return cand
|
|
33
|
+
const parent = resolve(cur, '..')
|
|
34
|
+
if (parent===cur) break
|
|
35
|
+
cur = parent
|
|
36
|
+
}
|
|
37
|
+
// try maestro-harness workspace root resolved via __dirname ancestor search for packages/dsh-maestro-guard
|
|
38
|
+
// last resort: return direct (will error but report will contain command)
|
|
39
|
+
return direct
|
|
40
|
+
}
|
|
41
|
+
export function apply(ctx: Context, config:{rootPath?:string}={}) {
|
|
42
|
+
ctx.effect(()=> ctx.tools.register(defineTool({
|
|
43
|
+
name:'maestro_full_scan',
|
|
44
|
+
description:'Run full enforce-rules scan (blacklist + protection + publish gate) and return report. No approval needed; read-only.',
|
|
45
|
+
parameters:{ rootPath:{type:'string', description:'workspace root override'} },
|
|
46
|
+
output:{ schema:{type:'object', additionalProperties:true, properties:{ok:{type:'boolean', required:true}, report:{type:'string', required:true}, code:{type:'number', required:true}}}, render:(_a:any,v:any)=>[{type:'text', text: v.report.slice(0,4000)}] },
|
|
47
|
+
async execute(args:any, exec:unknown) {
|
|
48
|
+
const root = workspaceRootFor((args as any)?.rootPath ?? config.rootPath, exec)
|
|
49
|
+
const script = resolveScript(root)
|
|
50
|
+
if (!existsSync(script)) {
|
|
51
|
+
return {ok: true, report: `enforce-rules: no script found at ${script} (standalone repo, skipping)`, code: 0}
|
|
52
|
+
}
|
|
53
|
+
const run = (extra:string[])=>{
|
|
54
|
+
const r=spawnSync('node', [script, ...extra], {encoding:'utf-8', timeout: 30000})
|
|
55
|
+
return {code: r.status??0, out: (r.stdout??'')+(r.stderr??'')}
|
|
56
|
+
}
|
|
57
|
+
let combined=''
|
|
58
|
+
let ok=true
|
|
59
|
+
const a=run([])
|
|
60
|
+
combined+=`$ node scripts/enforce-rules.mjs\n${a.out}\n`
|
|
61
|
+
if (a.code!==0) ok=false
|
|
62
|
+
const b=run(['--check-protection'])
|
|
63
|
+
combined+=`$ node scripts/enforce-rules.mjs --check-protection\n${b.out}\n`
|
|
64
|
+
if (b.code!==0) combined+=`[warn] --check-protection non-zero (may need gh auth)\n`
|
|
65
|
+
const c=run(['--check-publish'])
|
|
66
|
+
combined+=`$ node scripts/enforce-rules.mjs --check-publish\n${c.out}\n`
|
|
67
|
+
// do not fail on --check-publish when no tag
|
|
68
|
+
return {ok, report: combined.slice(0,12000), code: ok?0:1}
|
|
69
|
+
}
|
|
70
|
+
})))
|
|
71
|
+
}
|