@oscharko-dev/keiko-tools 0.2.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.
Files changed (59) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/browser/cdp-client.d.ts +36 -0
  3. package/dist/browser/cdp-client.d.ts.map +1 -0
  4. package/dist/browser/cdp-client.js +218 -0
  5. package/dist/browser/errors.d.ts +27 -0
  6. package/dist/browser/errors.d.ts.map +1 -0
  7. package/dist/browser/errors.js +61 -0
  8. package/dist/browser/session.d.ts +46 -0
  9. package/dist/browser/session.d.ts.map +1 -0
  10. package/dist/browser/session.js +759 -0
  11. package/dist/browser/types.d.ts +49 -0
  12. package/dist/browser/types.d.ts.map +1 -0
  13. package/dist/browser/types.js +2 -0
  14. package/dist/browser/validators.d.ts +6 -0
  15. package/dist/browser/validators.d.ts.map +1 -0
  16. package/dist/browser/validators.js +97 -0
  17. package/dist/errors.d.ts +3 -0
  18. package/dist/errors.d.ts.map +1 -0
  19. package/dist/errors.js +4 -0
  20. package/dist/exec.d.ts +45 -0
  21. package/dist/exec.d.ts.map +1 -0
  22. package/dist/exec.js +372 -0
  23. package/dist/index.d.ts +20 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +33 -0
  26. package/dist/patch-content.d.ts +11 -0
  27. package/dist/patch-content.d.ts.map +1 -0
  28. package/dist/patch-content.js +130 -0
  29. package/dist/patch-normalize.d.ts +2 -0
  30. package/dist/patch-normalize.d.ts.map +1 -0
  31. package/dist/patch-normalize.js +85 -0
  32. package/dist/patch-parse.d.ts +9 -0
  33. package/dist/patch-parse.d.ts.map +1 -0
  34. package/dist/patch-parse.js +201 -0
  35. package/dist/patch.d.ts +22 -0
  36. package/dist/patch.d.ts.map +1 -0
  37. package/dist/patch.js +469 -0
  38. package/dist/registry.d.ts +35 -0
  39. package/dist/registry.d.ts.map +1 -0
  40. package/dist/registry.js +240 -0
  41. package/dist/sandbox.d.ts +9 -0
  42. package/dist/sandbox.d.ts.map +1 -0
  43. package/dist/sandbox.js +131 -0
  44. package/dist/schemas.d.ts +3 -0
  45. package/dist/schemas.d.ts.map +1 -0
  46. package/dist/schemas.js +51 -0
  47. package/dist/terminal-policy.d.ts +10 -0
  48. package/dist/terminal-policy.d.ts.map +1 -0
  49. package/dist/terminal-policy.js +306 -0
  50. package/dist/types.d.ts +5 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +18 -0
  53. package/dist/version.d.ts +2 -0
  54. package/dist/version.d.ts.map +1 -0
  55. package/dist/version.js +4 -0
  56. package/dist/writer.d.ts +8 -0
  57. package/dist/writer.d.ts.map +1 -0
  58. package/dist/writer.js +20 -0
  59. package/package.json +42 -0
@@ -0,0 +1,240 @@
1
+ // The tool host: WorkspaceToolHost implements the harness ToolPort. It narrows untrusted
2
+ // `Record<string,unknown>` arguments, dispatches the 6 tools through a handler map, and returns
3
+ // a ToolCallResult whose `output` is already redacted. run_command is the only tool that sets
4
+ // commandExecuted:true (the executor counts those against maxCommandExecutions). All filesystem
5
+ // reads go through the workspace layer; all writes through the WorkspaceWriter; all spawns through
6
+ // the injected SpawnFn — so a unit test needs no real secrets and no real processes.
7
+ import { discoverWithStats, readWorkspaceFile, } from "@oscharko-dev/keiko-workspace";
8
+ import { nodeWorkspaceFs } from "@oscharko-dev/keiko-workspace/internal/fs";
9
+ import { nodeSpawnFn, runCommand } from "./exec.js";
10
+ import { CommandCancelledError, ToolArgumentError, UnknownToolError } from "./errors.js";
11
+ import { applyPatch, renderDryRun, validatePatch } from "./patch.js";
12
+ import { TOOL_DEFINITIONS } from "./schemas.js";
13
+ import { nodeWorkspaceWriter } from "./writer.js";
14
+ import { resolveToolHostConfig, } from "./types.js";
15
+ function requireString(args, key, tool) {
16
+ const value = args[key];
17
+ if (typeof value !== "string" || value.length === 0) {
18
+ throw new ToolArgumentError(`argument '${key}' must be a non-empty string`, tool);
19
+ }
20
+ return value;
21
+ }
22
+ function optionalString(args, key, tool) {
23
+ const value = args[key];
24
+ if (value === undefined) {
25
+ return undefined;
26
+ }
27
+ if (typeof value !== "string") {
28
+ throw new ToolArgumentError(`argument '${key}' must be a string`, tool);
29
+ }
30
+ return value;
31
+ }
32
+ function optionalNumber(args, key, tool) {
33
+ const value = args[key];
34
+ if (value === undefined) {
35
+ return undefined;
36
+ }
37
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
38
+ throw new ToolArgumentError(`argument '${key}' must be a non-negative number`, tool);
39
+ }
40
+ return value;
41
+ }
42
+ function optionalBoolean(args, key, tool) {
43
+ const value = args[key];
44
+ if (value === undefined) {
45
+ return undefined;
46
+ }
47
+ if (typeof value !== "boolean") {
48
+ throw new ToolArgumentError(`argument '${key}' must be a boolean`, tool);
49
+ }
50
+ return value;
51
+ }
52
+ function optionalStringArray(args, key, tool) {
53
+ const value = args[key];
54
+ if (value === undefined) {
55
+ return undefined;
56
+ }
57
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
58
+ throw new ToolArgumentError(`argument '${key}' must be a string array`, tool);
59
+ }
60
+ return value;
61
+ }
62
+ function summarizeCommand(result) {
63
+ return JSON.stringify({
64
+ exitCode: result.exitCode,
65
+ signal: result.signal,
66
+ timedOut: result.timedOut,
67
+ truncated: result.truncated,
68
+ stdout: result.stdout,
69
+ stderr: result.stderr,
70
+ });
71
+ }
72
+ export class WorkspaceToolHost {
73
+ workspace;
74
+ fs;
75
+ writer;
76
+ spawn;
77
+ resolveExecutable;
78
+ config;
79
+ processEnv;
80
+ now;
81
+ constructor(deps) {
82
+ this.workspace = deps.workspace;
83
+ this.fs = deps.fs ?? nodeWorkspaceFs;
84
+ this.writer = deps.writer ?? nodeWorkspaceWriter;
85
+ this.spawn = deps.spawn ?? nodeSpawnFn;
86
+ this.resolveExecutable = deps.resolveExecutable;
87
+ this.config = resolveToolHostConfig(deps.config);
88
+ this.processEnv = deps.processEnv ?? process.env;
89
+ this.now = deps.now ?? Date.now;
90
+ }
91
+ listTools() {
92
+ return TOOL_DEFINITIONS;
93
+ }
94
+ async execute(request) {
95
+ if (request.signal.aborted) {
96
+ throw new CommandCancelledError("tool call cancelled before dispatch");
97
+ }
98
+ const startedAt = this.now();
99
+ const handled = await this.dispatch(request);
100
+ return {
101
+ toolCallId: request.toolCallId,
102
+ output: handled.output,
103
+ durationMs: this.now() - startedAt,
104
+ commandExecuted: handled.commandExecuted,
105
+ ...(handled.metadata === undefined ? {} : { metadata: handled.metadata }),
106
+ };
107
+ }
108
+ dispatch(request) {
109
+ const args = request.arguments;
110
+ switch (request.toolName) {
111
+ case "read_file":
112
+ return Promise.resolve(this.readFile(args));
113
+ case "list_files":
114
+ return Promise.resolve(this.listFiles(args));
115
+ case "inspect_package_scripts":
116
+ return Promise.resolve(this.inspectScripts(args));
117
+ case "run_command":
118
+ return this.runCommandTool(args, request.signal);
119
+ case "propose_patch":
120
+ return Promise.resolve(this.proposePatch(args));
121
+ case "apply_patch":
122
+ return Promise.resolve(this.applyPatchTool(args, request.signal));
123
+ default:
124
+ throw new UnknownToolError(`no such tool: ${request.toolName}`, request.toolName);
125
+ }
126
+ }
127
+ readFile(args) {
128
+ const path = requireString(args, "path", "read_file");
129
+ const maxBytes = optionalNumber(args, "maxBytes", "read_file") ?? this.config.maxReadBytes;
130
+ const content = readWorkspaceFile(this.workspace, path, { maxBytes }, this.fs);
131
+ return { output: JSON.stringify(content), commandExecuted: false };
132
+ }
133
+ listFiles(args) {
134
+ const maxDepth = optionalNumber(args, "maxDepth", "list_files");
135
+ const maxFiles = optionalNumber(args, "maxFiles", "list_files");
136
+ const applyGitignore = optionalBoolean(args, "applyGitignore", "list_files");
137
+ const result = discoverWithStats(this.workspace, {
138
+ maxDepth: maxDepth ?? 12,
139
+ maxFiles: maxFiles ?? 5_000,
140
+ applyGitignore: applyGitignore ?? true,
141
+ }, this.fs);
142
+ return { output: JSON.stringify(result), commandExecuted: false };
143
+ }
144
+ inspectScripts(args) {
145
+ const path = optionalString(args, "path", "inspect_package_scripts") ?? "package.json";
146
+ const content = readWorkspaceFile(this.workspace, path, { maxBytes: this.config.maxReadBytes }, this.fs);
147
+ const scripts = parseScripts(content.text, path);
148
+ return { output: JSON.stringify({ path, scripts }), commandExecuted: false };
149
+ }
150
+ async runCommandTool(args, signal) {
151
+ const command = requireString(args, "command", "run_command");
152
+ const cmdArgs = optionalStringArray(args, "args", "run_command") ?? [];
153
+ const cwd = optionalString(args, "cwd", "run_command");
154
+ const timeoutMs = optionalNumber(args, "timeoutMs", "run_command");
155
+ const result = await runCommand({ command, args: cmdArgs, cwd, timeoutMs, signal }, {
156
+ workspace: this.workspace,
157
+ policy: this.config.sandbox,
158
+ commandRules: this.config.commandRules,
159
+ spawn: this.spawn,
160
+ resolveExecutable: this.resolveExecutable,
161
+ processEnv: this.processEnv,
162
+ now: this.now,
163
+ });
164
+ return {
165
+ output: summarizeCommand(result),
166
+ commandExecuted: true,
167
+ metadata: {
168
+ kind: "command",
169
+ executable: command,
170
+ argCount: cmdArgs.length,
171
+ exitCode: result.exitCode,
172
+ timedOut: result.timedOut,
173
+ sandbox: {
174
+ envAllowlist: this.config.sandbox.envAllowlist,
175
+ network: this.config.sandbox.network,
176
+ maxOutputBytes: this.config.sandbox.maxOutputBytes,
177
+ timeoutMs: timeoutMs ?? this.config.sandbox.defaultTimeoutMs,
178
+ terminationGraceMs: this.config.sandbox.terminationGraceMs,
179
+ cwdRequested: cwd !== undefined,
180
+ filesystem: this.config.sandbox.filesystem,
181
+ // ADR-0043: record how (and whether) egress was enforced for a network:"none" run.
182
+ ...(result.attestation === undefined
183
+ ? {}
184
+ : {
185
+ networkEnforced: result.attestation.networkEnforced,
186
+ filesystemEnforced: result.attestation.filesystemEnforced,
187
+ backend: result.attestation.backend,
188
+ }),
189
+ },
190
+ },
191
+ };
192
+ }
193
+ proposePatch(args) {
194
+ const diff = requireString(args, "diff", "propose_patch");
195
+ const validation = validatePatch(this.workspace, diff, {
196
+ fs: this.fs,
197
+ limits: this.config.patchLimits,
198
+ });
199
+ return {
200
+ output: JSON.stringify({ validation, preview: renderDryRun(validation) }),
201
+ commandExecuted: false,
202
+ };
203
+ }
204
+ applyPatchTool(args, signal) {
205
+ const diff = requireString(args, "diff", "apply_patch");
206
+ const result = applyPatch(this.workspace, diff, {
207
+ applyEnabled: this.config.applyEnabled,
208
+ signal,
209
+ fs: this.fs,
210
+ writer: this.writer,
211
+ limits: this.config.patchLimits,
212
+ });
213
+ return {
214
+ output: JSON.stringify(result),
215
+ commandExecuted: false,
216
+ metadata: {
217
+ kind: "patch-apply",
218
+ changedFiles: result.changedFiles.length,
219
+ created: result.created.length,
220
+ deleted: result.deleted.length,
221
+ },
222
+ };
223
+ }
224
+ }
225
+ function parseScripts(text, path) {
226
+ let parsed;
227
+ try {
228
+ parsed = JSON.parse(text);
229
+ }
230
+ catch {
231
+ throw new ToolArgumentError(`${path} is not valid JSON`, "inspect_package_scripts");
232
+ }
233
+ if (typeof parsed !== "object" || parsed === null) {
234
+ return {};
235
+ }
236
+ const scripts = parsed.scripts;
237
+ return typeof scripts === "object" && scripts !== null
238
+ ? scripts
239
+ : {};
240
+ }
@@ -0,0 +1,9 @@
1
+ import type { CommandRule } from "./types.js";
2
+ export declare function buildSandboxEnv(processEnv: NodeJS.ProcessEnv, allowlist: readonly string[]): Record<string, string>;
3
+ export declare function collectSensitiveEnvValues(processEnv: NodeJS.ProcessEnv, allowlist: readonly string[]): readonly string[];
4
+ export interface CommandDecision {
5
+ readonly allowed: boolean;
6
+ readonly reason?: string | undefined;
7
+ }
8
+ export declare function isCommandAllowed(rules: readonly CommandRule[], executable: string, args: readonly string[]): CommandDecision;
9
+ //# sourceMappingURL=sandbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAI9C,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,CAAC,UAAU,EAC7B,SAAS,EAAE,SAAS,MAAM,EAAE,GAC3B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASxB;AAMD,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,MAAM,CAAC,UAAU,EAC7B,SAAS,EAAE,SAAS,MAAM,EAAE,GAC3B,SAAS,MAAM,EAAE,CAYnB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACtC;AAqGD,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,SAAS,WAAW,EAAE,EAC7B,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,SAAS,MAAM,EAAE,GACtB,eAAe,CAajB"}
@@ -0,0 +1,131 @@
1
+ // PURE sandbox logic: the trust boundary's decision functions. No filesystem, no spawn, no
2
+ // node:child_process imports — every effect lives in exec.ts/writer.ts. These functions are
3
+ // individually unit-testable so the security invariants (env isolation, deny-by-default) are
4
+ // pinned down. Only node:path (a pure string utility) is imported here.
5
+ import { basename } from "node:path";
6
+ // Builds the child env by copying ONLY allowlisted names that are present in the parent.
7
+ // NEVER spreads `...processEnv`, so no credential-bearing variable can leak into the child.
8
+ export function buildSandboxEnv(processEnv, allowlist) {
9
+ const env = {};
10
+ for (const name of allowlist) {
11
+ const value = processEnv[name];
12
+ if (value !== undefined) {
13
+ env[name] = value;
14
+ }
15
+ }
16
+ return env;
17
+ }
18
+ // Collects the values of every parent env var that is NOT on the allowlist, so the command's
19
+ // captured stdout/stderr can be scrubbed of any secret a child still managed to print (e.g. a
20
+ // tool that reads a token from its own config and echoes it). Empty/short values are skipped to
21
+ // avoid over-redaction. The allowlisted, non-secret values (PATH, HOME, …) are deliberately kept.
22
+ export function collectSensitiveEnvValues(processEnv, allowlist) {
23
+ const allowed = new Set(allowlist);
24
+ const values = [];
25
+ for (const [name, value] of Object.entries(processEnv)) {
26
+ if (allowed.has(name)) {
27
+ continue;
28
+ }
29
+ if (value !== undefined && value.length >= 6) {
30
+ values.push(value);
31
+ }
32
+ }
33
+ return values;
34
+ }
35
+ function hasPathSeparator(value) {
36
+ return value.includes("/") || value.includes("\\");
37
+ }
38
+ function hasNul(value) {
39
+ return value.includes("\u0000");
40
+ }
41
+ // Resolves the subcommand: the first non-flag token, skipping leading flags AND the value of any
42
+ // value-taking flag (`--prefix DIR`, `-C DIR`). This is the S-H2 fix — a value can no longer
43
+ // masquerade as the subcommand. `--flag=value` carries its value inline, so only the flag token is
44
+ // consumed. Returns undefined when no subcommand token is present.
45
+ function resolveSubcommand(rule, args) {
46
+ const valueFlags = new Set(rule.valueFlags ?? []);
47
+ let skipNext = false;
48
+ for (const arg of args) {
49
+ if (skipNext) {
50
+ skipNext = false; // this token is the value of the preceding value-flag; skip it
51
+ continue;
52
+ }
53
+ if (!arg.startsWith("-")) {
54
+ return arg;
55
+ }
56
+ // A `-f=value` / `--flag=value` token carries its own value; consume just this token.
57
+ if (!arg.includes("=") && valueFlags.has(arg)) {
58
+ skipNext = true; // the following token is this flag's value
59
+ }
60
+ }
61
+ return undefined;
62
+ }
63
+ // Denies the whole invocation if any denied flag (e.g. npm/npx `-c`/`--call`) appears anywhere in
64
+ // args, in either `--call x` or `--call=x` form. These execute a transitive shell (S-H2).
65
+ function hasDeniedFlag(rule, args) {
66
+ const denied = rule.denyFlags;
67
+ if (denied === undefined) {
68
+ return false;
69
+ }
70
+ return args.some((arg) => {
71
+ const flag = arg.includes("=") ? arg.slice(0, arg.indexOf("=")) : arg;
72
+ return denied.includes(flag);
73
+ });
74
+ }
75
+ function hasRequiredLeadingFlags(rule, args) {
76
+ const required = rule.requiredLeadingFlags;
77
+ if (required === undefined || required.length === 0) {
78
+ return true;
79
+ }
80
+ return required.every((flag, index) => args[index] === flag);
81
+ }
82
+ function checkAllowlistMode(rule, allowed, sub) {
83
+ if (sub === undefined || !allowed.includes(sub)) {
84
+ return { allowed: false, reason: `subcommand not allowed: ${rule.executable} ${sub ?? ""}` };
85
+ }
86
+ return { allowed: true };
87
+ }
88
+ function checkDenylistMode(rule, sub) {
89
+ // Deny-by-default on the subcommand: when a known-subcommand set is declared, an unrecognized
90
+ // first non-flag token (e.g. a stray path from a value-flag bypass) is denied.
91
+ if (rule.knownSubcommands !== undefined &&
92
+ (sub === undefined || !rule.knownSubcommands.includes(sub))) {
93
+ return { allowed: false, reason: `unrecognized subcommand: ${rule.executable} ${sub ?? ""}` };
94
+ }
95
+ if (rule.deniedSubcommands !== undefined &&
96
+ sub !== undefined &&
97
+ rule.deniedSubcommands.includes(sub)) {
98
+ return { allowed: false, reason: `subcommand denied: ${rule.executable} ${sub}` };
99
+ }
100
+ return { allowed: true };
101
+ }
102
+ function checkSubcommand(rule, args) {
103
+ if (hasDeniedFlag(rule, args)) {
104
+ return { allowed: false, reason: `denied flag for ${rule.executable}` };
105
+ }
106
+ if (!hasRequiredLeadingFlags(rule, args)) {
107
+ return { allowed: false, reason: `missing required leading flag for ${rule.executable}` };
108
+ }
109
+ const sub = resolveSubcommand(rule, args);
110
+ if (rule.allowedSubcommands !== undefined) {
111
+ return checkAllowlistMode(rule, rule.allowedSubcommands, sub);
112
+ }
113
+ return checkDenylistMode(rule, sub);
114
+ }
115
+ // PURE deny-by-default decision. The executable must be a BARE name (no path separators, no
116
+ // NUL): we match by basename against the rules and reject anything unlisted. This is evaluated
117
+ // BEFORE any spawn, so a denied command never reaches child_process.
118
+ export function isCommandAllowed(rules, executable, args) {
119
+ if (executable.length === 0 || hasNul(executable)) {
120
+ return { allowed: false, reason: "empty or NUL-containing executable" };
121
+ }
122
+ if (hasPathSeparator(executable)) {
123
+ return { allowed: false, reason: "executable must be a bare PATH-resolved name" };
124
+ }
125
+ const name = basename(executable);
126
+ const rule = rules.find((candidate) => candidate.executable === name);
127
+ if (rule === undefined) {
128
+ return { allowed: false, reason: `executable not allowlisted: ${name}` };
129
+ }
130
+ return checkSubcommand(rule, args);
131
+ }
@@ -0,0 +1,3 @@
1
+ import type { ToolDefinition } from "@oscharko-dev/keiko-contracts";
2
+ export declare const TOOL_DEFINITIONS: readonly ToolDefinition[];
3
+ //# sourceMappingURL=schemas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AASpE,eAAO,MAAM,gBAAgB,EAAE,SAAS,cAAc,EA2DpD,CAAC"}
@@ -0,0 +1,51 @@
1
+ // The model-facing tool contract: 6 ToolDefinitions with JSON-Schema `parameters`. Kept apart
2
+ // from registry.ts so the dispatch logic stays small and the schema table is a single frozen
3
+ // surface the gateway/model see. No runtime logic — just the frozen definitions.
4
+ function obj(properties, required) {
5
+ return { type: "object", properties, required, additionalProperties: false };
6
+ }
7
+ export const TOOL_DEFINITIONS = Object.freeze([
8
+ {
9
+ name: "read_file",
10
+ description: "Read a UTF-8 file inside the workspace. Output is redacted; files above the byte cap are rejected.",
11
+ parameters: obj({
12
+ path: { type: "string", description: "Workspace-relative file path." },
13
+ maxBytes: { type: "number", description: "Optional read cap in bytes." },
14
+ }, ["path"]),
15
+ },
16
+ {
17
+ name: "list_files",
18
+ description: "List workspace files (deny-list and optional .gitignore applied).",
19
+ parameters: obj({
20
+ maxDepth: { type: "number", description: "Optional recursion depth cap." },
21
+ maxFiles: { type: "number", description: "Optional result count cap." },
22
+ applyGitignore: { type: "boolean", description: "Apply the .gitignore subset." },
23
+ }, []),
24
+ },
25
+ {
26
+ name: "inspect_package_scripts",
27
+ description: "Return the `scripts` object from a package.json inside the workspace.",
28
+ parameters: obj({ path: { type: "string", description: "Optional path; defaults to package.json." } }, []),
29
+ },
30
+ {
31
+ name: "run_command",
32
+ description: "Run an allowlisted read-only command (npm/git by default) with no shell, a clean env, " +
33
+ "a trusted executable path, a workspace cwd, a timeout, and capped redacted output.",
34
+ parameters: obj({
35
+ command: { type: "string", description: "Bare executable name (PATH-resolved)." },
36
+ args: { type: "array", items: { type: "string" }, description: "Argument vector." },
37
+ cwd: { type: "string", description: "Optional workspace-relative working directory." },
38
+ timeoutMs: { type: "number", description: "Optional wall-time budget in ms." },
39
+ }, ["command"]),
40
+ },
41
+ {
42
+ name: "propose_patch",
43
+ description: "Validate a unified diff and return a dry-run preview. Never writes to disk.",
44
+ parameters: obj({ diff: { type: "string", description: "Unified diff text." } }, ["diff"]),
45
+ },
46
+ {
47
+ name: "apply_patch",
48
+ description: "Apply a validated unified diff atomically. Fail-closed: refuses unless apply is enabled.",
49
+ parameters: obj({ diff: { type: "string", description: "Unified diff text." } }, ["diff"]),
50
+ },
51
+ ]);
@@ -0,0 +1,10 @@
1
+ import type { CommandRule } from "./types.js";
2
+ declare const FROZEN_NONE: readonly string[];
3
+ export declare const TERMINAL_COMMAND_RULES: readonly CommandRule[];
4
+ export interface TerminalCommandDecision {
5
+ readonly allowed: boolean;
6
+ readonly reason?: string | undefined;
7
+ }
8
+ export declare function isTerminalCommandAllowed(command: string, args: readonly string[]): TerminalCommandDecision;
9
+ export { FROZEN_NONE as TERMINAL_NO_FLAGS };
10
+ //# sourceMappingURL=terminal-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminal-policy.d.ts","sourceRoot":"","sources":["../src/terminal-policy.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,QAAA,MAAM,WAAW,EAAE,SAAS,MAAM,EAAsB,CAAC;AAIzD,eAAO,MAAM,sBAAsB,EAAE,SAAS,WAAW,EAyEvD,CAAC;AA4DH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACtC;AAuKD,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,SAAS,MAAM,EAAE,GACtB,uBAAuB,CAkBzB;AAGD,OAAO,EAAE,WAAW,IAAI,iBAAiB,EAAE,CAAC"}