@ddtcorex/dsh-maestro-guard 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 ADDED
@@ -0,0 +1,36 @@
1
+ # @ddtcorex/dsh-maestro-guard
2
+
3
+ Host-only safety gate for the DeepSeek Harness: gates tool execution before dispatch via a
4
+ waterfall pre-execute hook — persistent approval grants, secret redaction before
5
+ logging/persistence, and a pure allow/deny permission policy.
6
+
7
+ Part of the Maestro Harness suite (`dsh-maestro-*`). Cordis patch row id: `dsh-maestro-guard`.
8
+
9
+ > Opt-in and intentionally **not** part of the meta-bundle one-liner until published:
10
+ > add it explicitly with `dsh plugin add @ddtcorex/dsh-maestro-guard`.
11
+
12
+ ## What it provides
13
+
14
+ - **Waterfall pre-execute integration** — one Cordis row (`dsh-maestro-guard`) wiring the
15
+ guard handler into the tool-execution waterfall.
16
+ - **ApprovalStore** — persistent approval grants with legacy migration, revoke, and
17
+ read-modify-write safety under a mutex.
18
+ - **SecretRedactor** — redacts known secret families (`ghp_`, `xox`, private keys, …) before
19
+ calls are logged or persisted.
20
+ - **PermissionPolicy** — pure, unit-testable allow/deny checks for tool calls.
21
+
22
+ Host-only: no client bundle; DSH types come from local structural declarations
23
+ (`src/augment.d.ts`).
24
+
25
+ ## Development
26
+
27
+ ```sh
28
+ pnpm install
29
+ pnpm verify # tsc --noEmit
30
+ pnpm test # vitest run
31
+ pnpm build # tsc -> lib/
32
+ ```
33
+
34
+ ## License
35
+
36
+ MIT
@@ -0,0 +1,4 @@
1
+ # dsh-maestro-guard bundle patch — single row for the guard stub (Phase A).
2
+ - insert:
3
+ - id: dsh-maestro-guard
4
+ name: '@ddtcorex/dsh-maestro-guard'
@@ -0,0 +1,11 @@
1
+ export declare function approvalsPath(dshHome?: string): string;
2
+ export declare const pathFor: typeof approvalsPath;
3
+ export declare class ApprovalStore {
4
+ private dshHome?;
5
+ constructor(dshHome?: string | undefined);
6
+ load(): Promise<Record<string, boolean>>;
7
+ isApproved(tool: string): Promise<boolean>;
8
+ approve(tool: string): Promise<void>;
9
+ revoke(tool: string): Promise<void>;
10
+ }
11
+ //# sourceMappingURL=approval-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approval-store.d.ts","sourceRoot":"","sources":["../src/approval-store.ts"],"names":[],"mappings":"AAKA,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,MAAM,UAA8E;AAC5H,eAAO,MAAM,OAAO,sBAAgB,CAAA;AASpC,qBAAa,aAAa;IACZ,OAAO,CAAC,OAAO,CAAC;gBAAR,OAAO,CAAC,EAAE,MAAM,YAAA;IAC9B,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAGxC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAC1C,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUpC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAS1C"}
@@ -0,0 +1,48 @@
1
+ import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ function resolveHome(dshHome) { return dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh'); }
5
+ export function approvalsPath(dshHome) { return join(resolveHome(dshHome), 'dsh-maestro-guard', 'approvals.json'); }
6
+ export const pathFor = approvalsPath;
7
+ let _queue = Promise.resolve();
8
+ function enqueue(fn) {
9
+ const p = _queue.then(fn, fn);
10
+ _queue = p.catch(() => { });
11
+ return p;
12
+ }
13
+ export class ApprovalStore {
14
+ dshHome;
15
+ constructor(dshHome) {
16
+ this.dshHome = dshHome;
17
+ }
18
+ async load() {
19
+ try {
20
+ return JSON.parse(await readFile(approvalsPath(this.dshHome), 'utf-8'));
21
+ }
22
+ catch {
23
+ return {};
24
+ }
25
+ }
26
+ async isApproved(tool) { const m = await this.load(); return !!m[tool]; }
27
+ async approve(tool) {
28
+ return enqueue(async () => {
29
+ const m = await this.load();
30
+ m[tool] = true;
31
+ const p = approvalsPath(this.dshHome);
32
+ await mkdir(dirname(p), { recursive: true, mode: 0o700 });
33
+ await writeFile(p, JSON.stringify(m, null, 2), { encoding: 'utf-8', mode: 0o600 });
34
+ await chmod(p, 0o600);
35
+ });
36
+ }
37
+ async revoke(tool) {
38
+ return enqueue(async () => {
39
+ const m = await this.load();
40
+ delete m[tool];
41
+ const p = approvalsPath(this.dshHome);
42
+ await mkdir(dirname(p), { recursive: true, mode: 0o700 });
43
+ await writeFile(p, JSON.stringify(m, null, 2), { encoding: 'utf-8', mode: 0o600 });
44
+ await chmod(p, 0o600);
45
+ });
46
+ }
47
+ }
48
+ //# sourceMappingURL=approval-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approval-store.js","sourceRoot":"","sources":["../src/approval-store.ts"],"names":[],"mappings":"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"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { ApprovalStore } from './approval-store.js';
3
+ import { PermissionPolicy } from './permission-policy.js';
4
+ import type { GuardToolExecution, GuardPreToolDecision } from './augment.js';
5
+ export declare function createGuardHandler(store: ApprovalStore, policy: PermissionPolicy): (exec: GuardToolExecution, next: () => Promise<GuardPreToolDecision>) => Promise<GuardPreToolDecision>;
6
+ declare const _default: {
7
+ inject: readonly ["tools"];
8
+ apply(ctx: Context): void;
9
+ };
10
+ export default _default;
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAEzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AAE5E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,gBAAgB,IACjE,MAAM,kBAAkB,EAAE,MAAM,MAAM,OAAO,CAAC,oBAAoB,CAAC,KAAG,OAAO,CAAC,oBAAoB,CAAC,CAmBlH;;;eAIY,OAAO;;AAFpB,wBAQC"}
package/lib/index.js ADDED
@@ -0,0 +1,36 @@
1
+ import { ApprovalStore } from './approval-store.js';
2
+ import { PermissionPolicy } from './permission-policy.js';
3
+ import { containsSecret, redact } from './secret-redactor.js';
4
+ export function createGuardHandler(store, policy) {
5
+ return async (exec, next) => {
6
+ const tool = exec.name ?? exec.tool ?? '';
7
+ const rawArgs = exec?.args ?? exec?.arguments;
8
+ if (!policy.isAllowed(tool, rawArgs)) {
9
+ throw new Error(`Guard: tool ${tool} denied by policy`);
10
+ }
11
+ if (tool === 'danger-tool' && !(await store.isApproved(tool))) {
12
+ throw new Error(`Guard: tool ${tool} requires approval`);
13
+ }
14
+ if (rawArgs != null) {
15
+ const asText = JSON.stringify(rawArgs);
16
+ if (containsSecret(asText)) {
17
+ const redacted = JSON.parse(redact(asText));
18
+ if ('args' in exec)
19
+ exec.args = redacted;
20
+ if ('arguments' in exec)
21
+ exec.arguments = redacted;
22
+ }
23
+ }
24
+ return next();
25
+ };
26
+ }
27
+ export default {
28
+ inject: ['tools'],
29
+ apply(ctx) {
30
+ const store = new ApprovalStore();
31
+ const policy = new PermissionPolicy({ deny: ['danger-tool'] });
32
+ const handler = createGuardHandler(store, policy);
33
+ ctx.effect(() => ctx.on('tools/pre-execute', handler));
34
+ }
35
+ };
36
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAG7D,MAAM,UAAU,kBAAkB,CAAC,KAAoB,EAAE,MAAwB;IAC/E,OAAO,KAAK,EAAE,IAAwB,EAAE,IAAyC,EAAiC,EAAE;QAClH,MAAM,IAAI,GAAI,IAA2B,CAAC,IAAI,IAAK,IAA2B,CAAC,IAAI,IAAI,EAAE,CAAA;QACzF,MAAM,OAAO,GAAI,IAAY,EAAE,IAAI,IAAK,IAAY,EAAE,SAAS,CAAA;QAC/D,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,CAAC;YACrC,MAAM,IAAI,KAAK,CAAC,eAAe,IAAI,mBAAmB,CAAC,CAAA;QACzD,CAAC;QACD,IAAI,IAAI,KAAK,aAAa,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YAC9D,MAAM,IAAI,KAAK,CAAC,eAAe,IAAI,oBAAoB,CAAC,CAAA;QAC1D,CAAC;QACD,IAAI,OAAO,IAAI,IAAI,EAAE,CAAC;YACpB,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;YACtC,IAAI,cAAc,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC3B,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAA;gBAC3C,IAAI,MAAM,IAAI,IAAI;oBAAG,IAAY,CAAC,IAAI,GAAG,QAAQ,CAAA;gBACjD,IAAI,WAAW,IAAI,IAAI;oBAAG,IAAY,CAAC,SAAS,GAAG,QAAQ,CAAA;YAC7D,CAAC;QACH,CAAC;QACD,OAAO,IAAI,EAAE,CAAA;IACf,CAAC,CAAA;AACH,CAAC;AAED,eAAe;IACb,MAAM,EAAE,CAAC,OAAO,CAAU;IAC1B,KAAK,CAAC,GAAY;QAChB,MAAM,KAAK,GAAG,IAAI,aAAa,EAAE,CAAA;QACjC,MAAM,MAAM,GAAG,IAAI,gBAAgB,CAAC,EAAE,IAAI,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC,CAAA;QAC9D,MAAM,OAAO,GAAG,kBAAkB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;QACjD,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,mBAAmB,EAAE,OAAO,CAAC,CAAC,CAAA;IACxD,CAAC;CACF,CAAA"}
@@ -0,0 +1,11 @@
1
+ export interface GuardPolicy {
2
+ allow?: string[];
3
+ deny?: string[];
4
+ }
5
+ export declare class PermissionPolicy {
6
+ private policy;
7
+ constructor(policy: GuardPolicy);
8
+ isAllowed(tool: string, _args: unknown): boolean;
9
+ check(tool: string): 'allow' | 'deny' | 'ask';
10
+ }
11
+ //# sourceMappingURL=permission-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission-policy.d.ts","sourceRoot":"","sources":["../src/permission-policy.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAAG,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE;AAClE,qBAAa,gBAAgB;IACf,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,WAAW;IACvC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO;IAKhD,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAC,MAAM,GAAC,KAAK;CAM1C"}
@@ -0,0 +1,22 @@
1
+ export class PermissionPolicy {
2
+ policy;
3
+ constructor(policy) {
4
+ this.policy = policy;
5
+ }
6
+ isAllowed(tool, _args) {
7
+ if (this.policy.deny?.includes(tool))
8
+ return false;
9
+ if (this.policy.allow && this.policy.allow.length > 0)
10
+ return this.policy.allow.includes(tool);
11
+ return true;
12
+ }
13
+ check(tool) {
14
+ if (this.policy.deny?.includes(tool))
15
+ return 'deny';
16
+ const hasAllow = Array.isArray(this.policy.allow) && this.policy.allow.length > 0;
17
+ if (hasAllow && !this.policy.allow.includes(tool))
18
+ return 'ask';
19
+ return 'allow';
20
+ }
21
+ }
22
+ //# sourceMappingURL=permission-policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission-policy.js","sourceRoot":"","sources":["../src/permission-policy.ts"],"names":[],"mappings":"AACA,MAAM,OAAO,gBAAgB;IACP;IAApB,YAAoB,MAAmB;QAAnB,WAAM,GAAN,MAAM,CAAa;IAAG,CAAC;IAC3C,SAAS,CAAC,IAAY,EAAE,KAAc;QACpC,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC;YAAE,OAAO,KAAK,CAAA;QAClD,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;QAC9F,OAAO,IAAI,CAAA;IACb,CAAC;IACD,KAAK,CAAC,IAAY;QAChB,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC;YAAE,OAAO,MAAM,CAAA;QACnD,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAA;QACjF,IAAI,QAAQ,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,KAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,OAAO,KAAK,CAAA;QAChE,OAAO,OAAO,CAAA;IAChB,CAAC;CACF"}
@@ -0,0 +1,4 @@
1
+ export declare const DEFAULT_PATTERNS: RegExp[];
2
+ export declare function containsSecret(text: string): boolean;
3
+ export declare function redact(text: string): string;
4
+ //# sourceMappingURL=secret-redactor.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,18 @@
1
+ export const DEFAULT_PATTERNS = [
2
+ /glpat-[A-Za-z0-9_-]{10,}/g,
3
+ /sk-[A-Za-z0-9]{20,}/g,
4
+ /ghp_[A-Za-z0-9]{30,}/g,
5
+ /xox[bpras]-[A-Za-z0-9-]+/g,
6
+ ];
7
+ export function containsSecret(text) {
8
+ return DEFAULT_PATTERNS.some(r => { r.lastIndex = 0; return r.test(text); });
9
+ }
10
+ export function redact(text) {
11
+ let out = text;
12
+ for (const r of DEFAULT_PATTERNS) {
13
+ r.lastIndex = 0;
14
+ out = out.replace(r, '[REDACTED]');
15
+ }
16
+ return out;
17
+ }
18
+ //# sourceMappingURL=secret-redactor.js.map
@@ -0,0 +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"}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@ddtcorex/dsh-maestro-guard",
3
+ "version": "0.1.0",
4
+ "description": "Maestro Guard — waterfall tools/pre-execute approval + blocklist stub (Phase A scaffold)",
5
+ "type": "module",
6
+ "main": "./lib/index.js",
7
+ "types": "./lib/index.d.ts",
8
+ "exports": {
9
+ ".": "./lib/index.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "lib",
14
+ "src",
15
+ "cordis.patch.yml",
16
+ "README.md"
17
+ ],
18
+ "dsh": {
19
+ "bundle": {
20
+ "patch": "./cordis.patch.yml"
21
+ }
22
+ },
23
+ "peerDependencies": {
24
+ "@deepseek-ai/cordis": "^4.0.1"
25
+ },
26
+ "devDependencies": {
27
+ "@deepseek-ai/cordis": "^4.0.1",
28
+ "typescript": "^5.8.0",
29
+ "vitest": "^2.0.0",
30
+ "@types/node": "^22.0.0"
31
+ },
32
+ "scripts": {
33
+ "build": "tsc -p tsconfig.json",
34
+ "verify": "tsc --noEmit",
35
+ "test": "vitest run"
36
+ }
37
+ }
@@ -0,0 +1,41 @@
1
+ import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'
2
+ import { homedir } from 'node:os'
3
+ import { dirname, join } from 'node:path'
4
+
5
+ function resolveHome(dshHome?: string) { return dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh') }
6
+ export function approvalsPath(dshHome?: string) { return join(resolveHome(dshHome), 'dsh-maestro-guard', 'approvals.json') }
7
+ export const pathFor = approvalsPath
8
+
9
+ let _queue: Promise<unknown> = Promise.resolve()
10
+ function enqueue<T>(fn: () => Promise<T>): Promise<T> {
11
+ const p = _queue.then(fn, fn) as Promise<T>
12
+ _queue = p.catch(() => {})
13
+ return p
14
+ }
15
+
16
+ export class ApprovalStore {
17
+ constructor(private dshHome?: string) {}
18
+ async load(): Promise<Record<string, boolean>> {
19
+ try { return JSON.parse(await readFile(approvalsPath(this.dshHome), 'utf-8')) } catch { return {} }
20
+ }
21
+ async isApproved(tool: string): Promise<boolean> { const m = await this.load(); return !!m[tool] }
22
+ async approve(tool: string): Promise<void> {
23
+ return enqueue(async () => {
24
+ const m = await this.load()
25
+ m[tool] = true
26
+ const p = approvalsPath(this.dshHome)
27
+ await mkdir(dirname(p), { recursive: true, mode: 0o700 })
28
+ await writeFile(p, JSON.stringify(m, null, 2), { encoding: 'utf-8', mode: 0o600 })
29
+ await chmod(p, 0o600)
30
+ })
31
+ }
32
+ async revoke(tool: string): Promise<void> {
33
+ return enqueue(async () => {
34
+ const m = await this.load(); delete m[tool]
35
+ const p = approvalsPath(this.dshHome)
36
+ await mkdir(dirname(p), { recursive: true, mode: 0o700 })
37
+ await writeFile(p, JSON.stringify(m, null, 2), { encoding: 'utf-8', mode: 0o600 })
38
+ await chmod(p, 0o600)
39
+ })
40
+ }
41
+ }
@@ -0,0 +1,10 @@
1
+ import '@deepseek-ai/cordis'
2
+
3
+ export interface GuardToolExecution { name: string; tool?: string; args?: unknown; arguments?: unknown }
4
+ export interface GuardPreToolDecision { kind: 'allow' | 'deny' }
5
+
6
+ declare module '@deepseek-ai/cordis' {
7
+ interface Events {
8
+ 'tools/pre-execute'(exec: GuardToolExecution, next: () => Promise<GuardPreToolDecision>): Promise<GuardPreToolDecision>
9
+ }
10
+ }
package/src/index.ts ADDED
@@ -0,0 +1,38 @@
1
+ // src/index.ts
2
+ import type { Context } from '@deepseek-ai/cordis'
3
+ import { ApprovalStore } from './approval-store.js'
4
+ import { PermissionPolicy } from './permission-policy.js'
5
+ import { containsSecret, redact } from './secret-redactor.js'
6
+ import type { GuardToolExecution, GuardPreToolDecision } from './augment.js'
7
+
8
+ export function createGuardHandler(store: ApprovalStore, policy: PermissionPolicy) {
9
+ return async (exec: GuardToolExecution, next: () => Promise<GuardPreToolDecision>): Promise<GuardPreToolDecision> => {
10
+ const tool = (exec as GuardToolExecution).name ?? (exec as GuardToolExecution).tool ?? ''
11
+ const rawArgs = (exec as any)?.args ?? (exec as any)?.arguments
12
+ if (!policy.isAllowed(tool, rawArgs)) {
13
+ throw new Error(`Guard: tool ${tool} denied by policy`)
14
+ }
15
+ if (tool === 'danger-tool' && !(await store.isApproved(tool))) {
16
+ throw new Error(`Guard: tool ${tool} requires approval`)
17
+ }
18
+ if (rawArgs != null) {
19
+ const asText = JSON.stringify(rawArgs)
20
+ if (containsSecret(asText)) {
21
+ const redacted = JSON.parse(redact(asText))
22
+ if ('args' in exec) (exec as any).args = redacted
23
+ if ('arguments' in exec) (exec as any).arguments = redacted
24
+ }
25
+ }
26
+ return next()
27
+ }
28
+ }
29
+
30
+ export default {
31
+ inject: ['tools'] as const,
32
+ apply(ctx: Context) {
33
+ const store = new ApprovalStore()
34
+ const policy = new PermissionPolicy({ deny: ['danger-tool'] })
35
+ const handler = createGuardHandler(store, policy)
36
+ ctx.effect(() => ctx.on('tools/pre-execute', handler))
37
+ }
38
+ }
@@ -0,0 +1,15 @@
1
+ export interface GuardPolicy { allow?: string[]; deny?: string[] }
2
+ export class PermissionPolicy {
3
+ constructor(private policy: GuardPolicy) {}
4
+ isAllowed(tool: string, _args: unknown): boolean {
5
+ if (this.policy.deny?.includes(tool)) return false
6
+ if (this.policy.allow && this.policy.allow.length > 0) return this.policy.allow.includes(tool)
7
+ return true
8
+ }
9
+ check(tool: string): 'allow'|'deny'|'ask' {
10
+ if (this.policy.deny?.includes(tool)) return 'deny'
11
+ const hasAllow = Array.isArray(this.policy.allow) && this.policy.allow.length > 0
12
+ if (hasAllow && !this.policy.allow!.includes(tool)) return 'ask'
13
+ return 'allow'
14
+ }
15
+ }
@@ -0,0 +1,14 @@
1
+ export const DEFAULT_PATTERNS: RegExp[] = [
2
+ /glpat-[A-Za-z0-9_-]{10,}/g,
3
+ /sk-[A-Za-z0-9]{20,}/g,
4
+ /ghp_[A-Za-z0-9]{30,}/g,
5
+ /xox[bpras]-[A-Za-z0-9-]+/g,
6
+ ]
7
+ export function containsSecret(text: string): boolean {
8
+ return DEFAULT_PATTERNS.some(r => { r.lastIndex=0; return r.test(text) })
9
+ }
10
+ export function redact(text: string): string {
11
+ let out = text
12
+ for (const r of DEFAULT_PATTERNS) { r.lastIndex=0; out = out.replace(r, '[REDACTED]') }
13
+ return out
14
+ }