@warlock.js/ai-workspace 4.5.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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/cjs/index.cjs +1609 -0
- package/cjs/index.cjs.map +1 -0
- package/esm/backends/local.d.mts +22 -0
- package/esm/backends/local.d.mts.map +1 -0
- package/esm/backends/local.mjs +208 -0
- package/esm/backends/local.mjs.map +1 -0
- package/esm/backends/mock.d.mts +62 -0
- package/esm/backends/mock.d.mts.map +1 -0
- package/esm/backends/mock.mjs +167 -0
- package/esm/backends/mock.mjs.map +1 -0
- package/esm/contracts/index.d.mts +5 -0
- package/esm/contracts/tool-io.type.d.mts +149 -0
- package/esm/contracts/tool-io.type.d.mts.map +1 -0
- package/esm/contracts/workspace-backend.contract.d.mts +69 -0
- package/esm/contracts/workspace-backend.contract.d.mts.map +1 -0
- package/esm/contracts/workspace-ops.contract.d.mts +72 -0
- package/esm/contracts/workspace-ops.contract.d.mts.map +1 -0
- package/esm/contracts/workspace-policy.type.d.mts +86 -0
- package/esm/contracts/workspace-policy.type.d.mts.map +1 -0
- package/esm/contracts/workspace.contract.d.mts +131 -0
- package/esm/contracts/workspace.contract.d.mts.map +1 -0
- package/esm/errors.d.mts +100 -0
- package/esm/errors.d.mts.map +1 -0
- package/esm/errors.mjs +58 -0
- package/esm/errors.mjs.map +1 -0
- package/esm/index.d.mts +20 -0
- package/esm/index.mjs +15 -0
- package/esm/ops.d.mts +25 -0
- package/esm/ops.d.mts.map +1 -0
- package/esm/ops.mjs +294 -0
- package/esm/ops.mjs.map +1 -0
- package/esm/policy/policy.d.mts +71 -0
- package/esm/policy/policy.d.mts.map +1 -0
- package/esm/policy/policy.mjs +184 -0
- package/esm/policy/policy.mjs.map +1 -0
- package/esm/tools/edit-file.d.mts +40 -0
- package/esm/tools/edit-file.d.mts.map +1 -0
- package/esm/tools/edit-file.mjs +57 -0
- package/esm/tools/edit-file.mjs.map +1 -0
- package/esm/tools/glob.d.mts +37 -0
- package/esm/tools/glob.d.mts.map +1 -0
- package/esm/tools/glob.mjs +45 -0
- package/esm/tools/glob.mjs.map +1 -0
- package/esm/tools/grep.d.mts +36 -0
- package/esm/tools/grep.d.mts.map +1 -0
- package/esm/tools/grep.mjs +51 -0
- package/esm/tools/grep.mjs.map +1 -0
- package/esm/tools/read-file.d.mts +35 -0
- package/esm/tools/read-file.d.mts.map +1 -0
- package/esm/tools/read-file.mjs +64 -0
- package/esm/tools/read-file.mjs.map +1 -0
- package/esm/tools/run-shell.d.mts +35 -0
- package/esm/tools/run-shell.d.mts.map +1 -0
- package/esm/tools/run-shell.mjs +65 -0
- package/esm/tools/run-shell.mjs.map +1 -0
- package/esm/tools/run-tests.d.mts +40 -0
- package/esm/tools/run-tests.d.mts.map +1 -0
- package/esm/tools/run-tests.mjs +67 -0
- package/esm/tools/run-tests.mjs.map +1 -0
- package/esm/tools/schema.mjs +111 -0
- package/esm/tools/schema.mjs.map +1 -0
- package/esm/tools/write-file.d.mts +33 -0
- package/esm/tools/write-file.d.mts.map +1 -0
- package/esm/tools/write-file.mjs +52 -0
- package/esm/tools/write-file.mjs.map +1 -0
- package/esm/workspace.d.mts +54 -0
- package/esm/workspace.d.mts.map +1 -0
- package/esm/workspace.mjs +210 -0
- package/esm/workspace.mjs.map +1 -0
- package/llms-full.txt +231 -0
- package/llms.txt +10 -0
- package/package.json +42 -0
- package/skills/README.md +13 -0
- package/skills/build-loop-agent/SKILL.md +100 -0
- package/skills/use-a-workspace/SKILL.md +117 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
//#region ../@warlock.js/ai-workspace/src/backends/mock.ts
|
|
2
|
+
/**
|
|
3
|
+
* Normalize an absolute path to a stable in-memory key: forward slashes,
|
|
4
|
+
* collapsed duplicate separators, and resolved `.` / `..` segments. There
|
|
5
|
+
* are no symlinks in memory, so this is a pure lexical canonicalization —
|
|
6
|
+
* exactly what the backend's `realpath` promises.
|
|
7
|
+
*/
|
|
8
|
+
function canonicalize(absPath) {
|
|
9
|
+
const unified = absPath.replace(/\\/g, "/");
|
|
10
|
+
const driveMatch = unified.match(/^([a-zA-Z]:)?\/?/);
|
|
11
|
+
const prefix = driveMatch ? driveMatch[0] : "";
|
|
12
|
+
const rest = unified.slice(prefix.length);
|
|
13
|
+
const resolved = [];
|
|
14
|
+
for (const segment of rest.split("/")) {
|
|
15
|
+
if (segment === "" || segment === ".") continue;
|
|
16
|
+
if (segment === "..") {
|
|
17
|
+
resolved.pop();
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
resolved.push(segment);
|
|
21
|
+
}
|
|
22
|
+
const joined = resolved.join("/");
|
|
23
|
+
const normalizedPrefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
24
|
+
return joined.length > 0 ? `${normalizedPrefix}${joined}` : normalizedPrefix;
|
|
25
|
+
}
|
|
26
|
+
/** All ancestor directory keys of a canonical path, root-first. */
|
|
27
|
+
function ancestorsOf(canonicalPath) {
|
|
28
|
+
const lastSlash = canonicalPath.lastIndexOf("/");
|
|
29
|
+
if (lastSlash <= 0) return [];
|
|
30
|
+
const parent = canonicalPath.slice(0, lastSlash);
|
|
31
|
+
const result = ancestorsOf(parent);
|
|
32
|
+
result.push(parent);
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The in-memory executor behind {@link createMockBackend}. Holds the file
|
|
37
|
+
* tree in a `Map`, the directory set in a `Set`, and the scripted command
|
|
38
|
+
* table in a second `Map` — no disk, no child processes, fully
|
|
39
|
+
* deterministic. Construct it via the factory, never directly.
|
|
40
|
+
*/
|
|
41
|
+
var MockBackend = class {
|
|
42
|
+
constructor(seed) {
|
|
43
|
+
this.files = /* @__PURE__ */ new Map();
|
|
44
|
+
this.directories = /* @__PURE__ */ new Set();
|
|
45
|
+
this.commands = /* @__PURE__ */ new Map();
|
|
46
|
+
for (const [path, content] of Object.entries(seed?.files ?? {})) {
|
|
47
|
+
const canonical = canonicalize(path);
|
|
48
|
+
this.files.set(canonical, content);
|
|
49
|
+
this.registerAncestorDirectories(canonical);
|
|
50
|
+
}
|
|
51
|
+
for (const [command, result] of Object.entries(seed?.commands ?? {})) this.commands.set(command, result);
|
|
52
|
+
}
|
|
53
|
+
/** Record every ancestor directory of a path as existing. */
|
|
54
|
+
registerAncestorDirectories(canonicalPath) {
|
|
55
|
+
for (const ancestor of ancestorsOf(canonicalPath)) this.directories.add(ancestor);
|
|
56
|
+
}
|
|
57
|
+
async readFile(absPath) {
|
|
58
|
+
const canonical = canonicalize(absPath);
|
|
59
|
+
const content = this.files.get(canonical);
|
|
60
|
+
if (content === void 0) throw new Error(`Mock backend: no such file: ${canonical}`);
|
|
61
|
+
return content;
|
|
62
|
+
}
|
|
63
|
+
async writeFile(absPath, content) {
|
|
64
|
+
const canonical = canonicalize(absPath);
|
|
65
|
+
this.files.set(canonical, content);
|
|
66
|
+
this.registerAncestorDirectories(canonical);
|
|
67
|
+
}
|
|
68
|
+
async exists(absPath) {
|
|
69
|
+
const canonical = canonicalize(absPath);
|
|
70
|
+
return this.files.has(canonical) || this.directories.has(canonical);
|
|
71
|
+
}
|
|
72
|
+
async mkdir(absPath) {
|
|
73
|
+
const canonical = canonicalize(absPath);
|
|
74
|
+
this.directories.add(canonical);
|
|
75
|
+
this.registerAncestorDirectories(canonical);
|
|
76
|
+
}
|
|
77
|
+
async remove(absPath) {
|
|
78
|
+
const canonical = canonicalize(absPath);
|
|
79
|
+
const prefix = `${canonical}/`;
|
|
80
|
+
for (const file of [...this.files.keys()]) if (file === canonical || file.startsWith(prefix)) this.files.delete(file);
|
|
81
|
+
for (const directory of [...this.directories]) if (directory === canonical || directory.startsWith(prefix)) this.directories.delete(directory);
|
|
82
|
+
}
|
|
83
|
+
async list(absDir) {
|
|
84
|
+
const canonical = canonicalize(absDir);
|
|
85
|
+
const prefix = canonical === "/" ? "/" : `${canonical}/`;
|
|
86
|
+
const children = /* @__PURE__ */ new Set();
|
|
87
|
+
const collect = (key) => {
|
|
88
|
+
if (!key.startsWith(prefix) || key === canonical) return;
|
|
89
|
+
const remainder = key.slice(prefix.length);
|
|
90
|
+
const nextSlash = remainder.indexOf("/");
|
|
91
|
+
const childName = nextSlash === -1 ? remainder : remainder.slice(0, nextSlash);
|
|
92
|
+
if (childName.length > 0) children.add(`${prefix}${childName}`);
|
|
93
|
+
};
|
|
94
|
+
for (const file of this.files.keys()) collect(file);
|
|
95
|
+
for (const directory of this.directories) collect(directory);
|
|
96
|
+
return [...children].sort();
|
|
97
|
+
}
|
|
98
|
+
async realpath(absPath) {
|
|
99
|
+
return canonicalize(absPath);
|
|
100
|
+
}
|
|
101
|
+
async exec(command, _opts) {
|
|
102
|
+
const scripted = this.commands.get(command);
|
|
103
|
+
return {
|
|
104
|
+
exitCode: scripted?.exitCode ?? 0,
|
|
105
|
+
stdout: scripted?.stdout ?? "",
|
|
106
|
+
stderr: scripted?.stderr ?? "",
|
|
107
|
+
timedOut: scripted?.timedOut ?? false
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Create an in-memory {@link WorkspaceBackend} for fast, disk-free tests.
|
|
113
|
+
*
|
|
114
|
+
* Every IO method operates on a `Map` of `absolutePath -> content` (with a
|
|
115
|
+
* companion directory set), so reads, writes, existence checks, `mkdir`,
|
|
116
|
+
* recursive `remove`, `list`, and `realpath` all run synchronously in
|
|
117
|
+
* memory with no filesystem access. `exec` is **scripted**: a registered
|
|
118
|
+
* `command -> result` table is consulted by exact command line, and any
|
|
119
|
+
* unregistered command resolves to a successful `0`-exit no-op with empty
|
|
120
|
+
* output.
|
|
121
|
+
*
|
|
122
|
+
* `realpath` is a pure lexical canonicalization (forward slashes,
|
|
123
|
+
* collapsed separators, resolved `.`/`..`) — there are no symlinks in
|
|
124
|
+
* memory — which is exactly what the jail resolver expects.
|
|
125
|
+
*
|
|
126
|
+
* The single positional `seed` accepts either the shorthand
|
|
127
|
+
* `Record<string, string>` of file contents (matching the design's
|
|
128
|
+
* `ai.workspace.mock(seed)` signature) or the richer {@link MockBackendSeed}
|
|
129
|
+
* with both `files` and scripted `commands`.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* // Shorthand: seed files only.
|
|
133
|
+
* const backend = createMockBackend({ "/srv/app/src/index.ts": "export const x = 1;" });
|
|
134
|
+
* await backend.readFile("/srv/app/src/index.ts"); // "export const x = 1;"
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* // Full seed: files plus a scripted command.
|
|
138
|
+
* const backend = createMockBackend({
|
|
139
|
+
* files: { "/srv/app/package.json": "{}" },
|
|
140
|
+
* commands: { "npm test": { exitCode: 1, stderr: "1 failing" } },
|
|
141
|
+
* });
|
|
142
|
+
* await backend.exec("npm test"); // { exitCode: 1, stderr: "1 failing", ... }
|
|
143
|
+
* await backend.exec("echo hi"); // { exitCode: 0, stdout: "", ... } (no-op)
|
|
144
|
+
*/
|
|
145
|
+
function createMockBackend(seed) {
|
|
146
|
+
return new MockBackend(normalizeSeed(seed));
|
|
147
|
+
}
|
|
148
|
+
/** Coerce the dual-shaped `seed` argument into a {@link MockBackendSeed}. */
|
|
149
|
+
function normalizeSeed(seed) {
|
|
150
|
+
if (seed === void 0) return;
|
|
151
|
+
if (isMockBackendSeed(seed)) return seed;
|
|
152
|
+
return { files: seed };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Whether `seed` is the structured {@link MockBackendSeed} (has a `files`
|
|
156
|
+
* or `commands` key) rather than the flat `path -> content` shorthand. A
|
|
157
|
+
* plain shorthand map whose only key happens to be named `files` is
|
|
158
|
+
* treated as structured — callers wanting that literal path should use the
|
|
159
|
+
* explicit `{ files: { files: "..." } }` form.
|
|
160
|
+
*/
|
|
161
|
+
function isMockBackendSeed(seed) {
|
|
162
|
+
return typeof seed.files === "object" || typeof seed.commands === "object";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
//#endregion
|
|
166
|
+
export { createMockBackend };
|
|
167
|
+
//# sourceMappingURL=mock.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock.mjs","names":[],"sources":["../../../../../../../@warlock.js/ai-workspace/src/backends/mock.ts"],"sourcesContent":["import type {\n WorkspaceBackend,\n WorkspaceBackendExecOptions,\n WorkspaceBackendExecResult,\n} from \"../contracts/workspace-backend.contract\";\n\n/**\n * A scripted outcome for the mock backend's `exec`. Mirrors\n * {@link WorkspaceBackendExecResult} but every field is optional so a\n * registration can specify only what it cares about — the rest fall back\n * to a successful, empty-output, non-timed-out run.\n */\nexport type MockExecResult = Partial<WorkspaceBackendExecResult>;\n\n/**\n * The two seedable inputs of a mock backend:\n *\n * - `files` — initial file tree as `absolutePath -> content`. Parent\n * directories of each seeded file are implicitly created.\n * - `commands` — scripted `exec` outcomes keyed on the **exact** command\n * line. Any command not registered here resolves to a `0`-exit no-op.\n */\nexport interface MockBackendSeed {\n /** Initial file contents, keyed by absolute path. */\n files?: Record<string, string>;\n /** Scripted `exec` results, keyed by exact command line. */\n commands?: Record<string, MockExecResult>;\n}\n\n/**\n * Normalize an absolute path to a stable in-memory key: forward slashes,\n * collapsed duplicate separators, and resolved `.` / `..` segments. There\n * are no symlinks in memory, so this is a pure lexical canonicalization —\n * exactly what the backend's `realpath` promises.\n */\nfunction canonicalize(absPath: string): string {\n const unified = absPath.replace(/\\\\/g, \"/\");\n // Preserve a leading slash (POSIX root) or a `C:`-style Windows drive\n // prefix; everything else is a normal segment.\n const driveMatch = unified.match(/^([a-zA-Z]:)?\\/?/);\n const prefix = driveMatch ? driveMatch[0] : \"\";\n const rest = unified.slice(prefix.length);\n\n const resolved: string[] = [];\n\n for (const segment of rest.split(\"/\")) {\n if (segment === \"\" || segment === \".\") {\n continue;\n }\n\n if (segment === \"..\") {\n resolved.pop();\n\n continue;\n }\n\n resolved.push(segment);\n }\n\n const joined = resolved.join(\"/\");\n\n // A trailing slash on the prefix means it was absolute; keep it so the\n // result stays absolute even when the body is empty (the root itself).\n const normalizedPrefix = prefix.endsWith(\"/\") ? prefix : `${prefix}/`;\n\n return joined.length > 0 ? `${normalizedPrefix}${joined}` : normalizedPrefix;\n}\n\n/** All ancestor directory keys of a canonical path, root-first. */\nfunction ancestorsOf(canonicalPath: string): string[] {\n const lastSlash = canonicalPath.lastIndexOf(\"/\");\n\n if (lastSlash <= 0) {\n return [];\n }\n\n const parent = canonicalPath.slice(0, lastSlash);\n const result = ancestorsOf(parent);\n\n result.push(parent);\n\n return result;\n}\n\n/**\n * The in-memory executor behind {@link createMockBackend}. Holds the file\n * tree in a `Map`, the directory set in a `Set`, and the scripted command\n * table in a second `Map` — no disk, no child processes, fully\n * deterministic. Construct it via the factory, never directly.\n */\nclass MockBackend implements WorkspaceBackend {\n /** Canonical file path -> content. */\n private readonly files = new Map<string, string>();\n\n /** Canonical directory paths that exist (including implicit parents). */\n private readonly directories = new Set<string>();\n\n /** Exact command line -> scripted outcome. */\n private readonly commands = new Map<string, MockExecResult>();\n\n public constructor(seed?: MockBackendSeed) {\n for (const [path, content] of Object.entries(seed?.files ?? {})) {\n const canonical = canonicalize(path);\n\n this.files.set(canonical, content);\n this.registerAncestorDirectories(canonical);\n }\n\n for (const [command, result] of Object.entries(seed?.commands ?? {})) {\n this.commands.set(command, result);\n }\n }\n\n /** Record every ancestor directory of a path as existing. */\n private registerAncestorDirectories(canonicalPath: string): void {\n for (const ancestor of ancestorsOf(canonicalPath)) {\n this.directories.add(ancestor);\n }\n }\n\n public async readFile(absPath: string): Promise<string> {\n const canonical = canonicalize(absPath);\n const content = this.files.get(canonical);\n\n if (content === undefined) {\n throw new Error(`Mock backend: no such file: ${canonical}`);\n }\n\n return content;\n }\n\n public async writeFile(absPath: string, content: string): Promise<void> {\n const canonical = canonicalize(absPath);\n\n this.files.set(canonical, content);\n this.registerAncestorDirectories(canonical);\n }\n\n public async exists(absPath: string): Promise<boolean> {\n const canonical = canonicalize(absPath);\n\n return this.files.has(canonical) || this.directories.has(canonical);\n }\n\n public async mkdir(absPath: string): Promise<void> {\n const canonical = canonicalize(absPath);\n\n this.directories.add(canonical);\n this.registerAncestorDirectories(canonical);\n }\n\n public async remove(absPath: string): Promise<void> {\n const canonical = canonicalize(absPath);\n const prefix = `${canonical}/`;\n\n // Drop the entry itself and any descendant files/directories — a\n // recursive tree removal, matching the contract's \"file or directory\n // tree\" wording.\n for (const file of [...this.files.keys()]) {\n if (file === canonical || file.startsWith(prefix)) {\n this.files.delete(file);\n }\n }\n\n for (const directory of [...this.directories]) {\n if (directory === canonical || directory.startsWith(prefix)) {\n this.directories.delete(directory);\n }\n }\n }\n\n public async list(absDir: string): Promise<string[]> {\n const canonical = canonicalize(absDir);\n const prefix = canonical === \"/\" ? \"/\" : `${canonical}/`;\n const children = new Set<string>();\n\n const collect = (key: string): void => {\n if (!key.startsWith(prefix) || key === canonical) {\n return;\n }\n\n const remainder = key.slice(prefix.length);\n const nextSlash = remainder.indexOf(\"/\");\n const childName =\n nextSlash === -1 ? remainder : remainder.slice(0, nextSlash);\n\n if (childName.length > 0) {\n children.add(`${prefix}${childName}`);\n }\n };\n\n for (const file of this.files.keys()) {\n collect(file);\n }\n\n for (const directory of this.directories) {\n collect(directory);\n }\n\n return [...children].sort();\n }\n\n public async realpath(absPath: string): Promise<string> {\n return canonicalize(absPath);\n }\n\n public async exec(\n command: string,\n _opts?: WorkspaceBackendExecOptions,\n ): Promise<WorkspaceBackendExecResult> {\n const scripted = this.commands.get(command);\n\n return {\n exitCode: scripted?.exitCode ?? 0,\n stdout: scripted?.stdout ?? \"\",\n stderr: scripted?.stderr ?? \"\",\n timedOut: scripted?.timedOut ?? false,\n };\n }\n}\n\n/**\n * Create an in-memory {@link WorkspaceBackend} for fast, disk-free tests.\n *\n * Every IO method operates on a `Map` of `absolutePath -> content` (with a\n * companion directory set), so reads, writes, existence checks, `mkdir`,\n * recursive `remove`, `list`, and `realpath` all run synchronously in\n * memory with no filesystem access. `exec` is **scripted**: a registered\n * `command -> result` table is consulted by exact command line, and any\n * unregistered command resolves to a successful `0`-exit no-op with empty\n * output.\n *\n * `realpath` is a pure lexical canonicalization (forward slashes,\n * collapsed separators, resolved `.`/`..`) — there are no symlinks in\n * memory — which is exactly what the jail resolver expects.\n *\n * The single positional `seed` accepts either the shorthand\n * `Record<string, string>` of file contents (matching the design's\n * `ai.workspace.mock(seed)` signature) or the richer {@link MockBackendSeed}\n * with both `files` and scripted `commands`.\n *\n * @example\n * // Shorthand: seed files only.\n * const backend = createMockBackend({ \"/srv/app/src/index.ts\": \"export const x = 1;\" });\n * await backend.readFile(\"/srv/app/src/index.ts\"); // \"export const x = 1;\"\n *\n * @example\n * // Full seed: files plus a scripted command.\n * const backend = createMockBackend({\n * files: { \"/srv/app/package.json\": \"{}\" },\n * commands: { \"npm test\": { exitCode: 1, stderr: \"1 failing\" } },\n * });\n * await backend.exec(\"npm test\"); // { exitCode: 1, stderr: \"1 failing\", ... }\n * await backend.exec(\"echo hi\"); // { exitCode: 0, stdout: \"\", ... } (no-op)\n */\nexport function createMockBackend(\n seed?: Record<string, string> | MockBackendSeed,\n): WorkspaceBackend {\n return new MockBackend(normalizeSeed(seed));\n}\n\n/** Coerce the dual-shaped `seed` argument into a {@link MockBackendSeed}. */\nfunction normalizeSeed(\n seed?: Record<string, string> | MockBackendSeed,\n): MockBackendSeed | undefined {\n if (seed === undefined) {\n return undefined;\n }\n\n if (isMockBackendSeed(seed)) {\n return seed;\n }\n\n return { files: seed };\n}\n\n/**\n * Whether `seed` is the structured {@link MockBackendSeed} (has a `files`\n * or `commands` key) rather than the flat `path -> content` shorthand. A\n * plain shorthand map whose only key happens to be named `files` is\n * treated as structured — callers wanting that literal path should use the\n * explicit `{ files: { files: \"...\" } }` form.\n */\nfunction isMockBackendSeed(\n seed: Record<string, string> | MockBackendSeed,\n): seed is MockBackendSeed {\n return (\n typeof (seed as MockBackendSeed).files === \"object\" ||\n typeof (seed as MockBackendSeed).commands === \"object\"\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { EditFileInput, EditFileResult, GlobInput, GlobResult, GrepInput, GrepMatch, GrepResult, ReadFileInput, ReadFileResult, RunShellInput, RunShellResult, RunTestsInput, WorkspaceToolName, WriteFileInput, WriteFileResult } from "./tool-io.type.mjs";
|
|
2
|
+
import { WorkspaceBackend, WorkspaceBackendExecOptions, WorkspaceBackendExecResult } from "./workspace-backend.contract.mjs";
|
|
3
|
+
import { WorkspaceOps } from "./workspace-ops.contract.mjs";
|
|
4
|
+
import { WorkspaceBackendType, WorkspacePolicy, WorkspaceReadPolicy, WorkspaceShellPolicy } from "./workspace-policy.type.mjs";
|
|
5
|
+
import { Workspace, WorkspaceTools } from "./workspace.contract.mjs";
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
//#region ../@warlock.js/ai-workspace/src/contracts/tool-io.type.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Input/output shapes for the seven workspace tools and the direct
|
|
4
|
+
* methods that back them. These are the wire contracts the agent sees
|
|
5
|
+
* (via each tool's schema) and the values the policy-enforced ops layer
|
|
6
|
+
* returns. Paths in every `*Input` are workspace-relative; the policy
|
|
7
|
+
* engine resolves them against `cwd` and rejects escapes.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* The discriminating set of tool names a workspace can vend, used by
|
|
11
|
+
* `tools.pick(...)` to select a least-privilege subset.
|
|
12
|
+
*/
|
|
13
|
+
type WorkspaceToolName = "readFile" | "editFile" | "writeFile" | "runShell" | "runTests" | "grep" | "glob";
|
|
14
|
+
/** Input to the `read_file` tool / `readFile()` direct method. */
|
|
15
|
+
interface ReadFileInput {
|
|
16
|
+
/** Workspace-relative path to read. */
|
|
17
|
+
path: string;
|
|
18
|
+
/** 1-based line to start from. Omit to start at the top. */
|
|
19
|
+
startLine?: number;
|
|
20
|
+
/** Max lines to return from `startLine`. Omit for the policy default. */
|
|
21
|
+
limit?: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Result of a read — the requested line window plus the metadata an
|
|
25
|
+
* agent needs to read-before-edit (the `hash` feeds `editFile`'s
|
|
26
|
+
* `expectHash` stale-guard).
|
|
27
|
+
*/
|
|
28
|
+
interface ReadFileResult {
|
|
29
|
+
/** The returned file content for the requested window. */
|
|
30
|
+
content: string;
|
|
31
|
+
/** 1-based first line included in `content`. */
|
|
32
|
+
startLine: number;
|
|
33
|
+
/** 1-based last line included in `content`. */
|
|
34
|
+
endLine: number;
|
|
35
|
+
/** Total line count of the underlying file. */
|
|
36
|
+
totalLines: number;
|
|
37
|
+
/** True when the window stopped short of the file end (policy cap or `limit`). */
|
|
38
|
+
truncated: boolean;
|
|
39
|
+
/** SHA-256 of the full file content at read time (stale-edit guard). */
|
|
40
|
+
hash: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Input to the `edit_file` tool / `editFile()` direct method — an
|
|
44
|
+
* exact-string replacement with a read-before-edit stale guard.
|
|
45
|
+
*/
|
|
46
|
+
interface EditFileInput {
|
|
47
|
+
/** Workspace-relative path to edit. */
|
|
48
|
+
path: string;
|
|
49
|
+
/** Exact substring to replace; must be unique unless `replaceAll`. */
|
|
50
|
+
oldString: string;
|
|
51
|
+
/** Replacement text. */
|
|
52
|
+
newString: string;
|
|
53
|
+
/** Replace every occurrence instead of requiring a single unique match. */
|
|
54
|
+
replaceAll?: boolean;
|
|
55
|
+
/** Expected current SHA-256; a mismatch rejects the edit as stale. */
|
|
56
|
+
expectHash?: string;
|
|
57
|
+
}
|
|
58
|
+
/** Result of a successful edit. */
|
|
59
|
+
interface EditFileResult {
|
|
60
|
+
/** Workspace-relative path that was edited. */
|
|
61
|
+
path: string;
|
|
62
|
+
/** Number of occurrences replaced. */
|
|
63
|
+
replacements: number;
|
|
64
|
+
/** SHA-256 of the file content after the edit. */
|
|
65
|
+
hash: string;
|
|
66
|
+
}
|
|
67
|
+
/** Input to the `write_file` tool / `writeFile()` direct method. */
|
|
68
|
+
interface WriteFileInput {
|
|
69
|
+
/** Workspace-relative path to write (created if absent). */
|
|
70
|
+
path: string;
|
|
71
|
+
/** Full file content to write atomically. */
|
|
72
|
+
content: string;
|
|
73
|
+
}
|
|
74
|
+
/** Result of a write. */
|
|
75
|
+
interface WriteFileResult {
|
|
76
|
+
/** Workspace-relative path that was written. */
|
|
77
|
+
path: string;
|
|
78
|
+
/** Number of bytes written. */
|
|
79
|
+
bytesWritten: number;
|
|
80
|
+
/** SHA-256 of the written content. */
|
|
81
|
+
hash: string;
|
|
82
|
+
}
|
|
83
|
+
/** Input to the `run_shell` tool / `exec()` direct method. */
|
|
84
|
+
interface RunShellInput {
|
|
85
|
+
/** The command line to run; its leading basename is policy-checked. */
|
|
86
|
+
command: string;
|
|
87
|
+
/** Per-call wall-clock cap (overrides the policy default if lower). */
|
|
88
|
+
timeoutMs?: number;
|
|
89
|
+
}
|
|
90
|
+
/** Result of a shell command — captured streams plus how it terminated. */
|
|
91
|
+
interface RunShellResult {
|
|
92
|
+
/** Process exit code (non-zero ⇒ the command failed, not the tool). */
|
|
93
|
+
exitCode: number;
|
|
94
|
+
/** Captured standard output (byte-capped per policy). */
|
|
95
|
+
stdout: string;
|
|
96
|
+
/** Captured standard error (byte-capped per policy). */
|
|
97
|
+
stderr: string;
|
|
98
|
+
/** True when stdout/stderr was clipped at the policy byte cap. */
|
|
99
|
+
truncated: boolean;
|
|
100
|
+
/** True when the command was SIGKILLed for exceeding its timeout. */
|
|
101
|
+
timedOut: boolean;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Input to the `run_tests` tool. Convenience over `run_shell` that runs
|
|
105
|
+
* the workspace's configured test command, optionally narrowed to a
|
|
106
|
+
* pattern (a path/suite filter passed through to the runner).
|
|
107
|
+
*/
|
|
108
|
+
interface RunTestsInput {
|
|
109
|
+
/** Optional path/name filter forwarded to the test runner. */
|
|
110
|
+
pattern?: string;
|
|
111
|
+
}
|
|
112
|
+
/** Input to the `grep` tool / `grep()` direct method. */
|
|
113
|
+
interface GrepInput {
|
|
114
|
+
/** Regular-expression pattern to search file contents for. */
|
|
115
|
+
pattern: string;
|
|
116
|
+
/** Glob narrowing which files are scanned (e.g. `"src/api/*.ts"`). */
|
|
117
|
+
glob?: string;
|
|
118
|
+
/** Case-insensitive matching when true. */
|
|
119
|
+
ignoreCase?: boolean;
|
|
120
|
+
}
|
|
121
|
+
/** A single matched line within a file. */
|
|
122
|
+
interface GrepMatch {
|
|
123
|
+
/** Workspace-relative path of the file containing the match. */
|
|
124
|
+
path: string;
|
|
125
|
+
/** 1-based line number of the match. */
|
|
126
|
+
line: number;
|
|
127
|
+
/** Full text of the matching line. */
|
|
128
|
+
text: string;
|
|
129
|
+
}
|
|
130
|
+
/** Result of a content search across the jailed file set. */
|
|
131
|
+
interface GrepResult {
|
|
132
|
+
/** Every matching line found, in file/line order. */
|
|
133
|
+
matches: GrepMatch[];
|
|
134
|
+
/** Total number of matches (== `matches.length` unless capped). */
|
|
135
|
+
total: number;
|
|
136
|
+
}
|
|
137
|
+
/** Input to the `glob` tool / `glob()` direct method. */
|
|
138
|
+
interface GlobInput {
|
|
139
|
+
/** Glob pattern resolved relative to `cwd` (e.g. `"src/api/*.ts"`). */
|
|
140
|
+
pattern: string;
|
|
141
|
+
}
|
|
142
|
+
/** Result of a glob — the matched workspace-relative paths. */
|
|
143
|
+
interface GlobResult {
|
|
144
|
+
/** Workspace-relative paths matching the pattern, sorted. */
|
|
145
|
+
paths: string[];
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
export { EditFileInput, EditFileResult, GlobInput, GlobResult, GrepInput, GrepMatch, GrepResult, ReadFileInput, ReadFileResult, RunShellInput, RunShellResult, RunTestsInput, WorkspaceToolName, WriteFileInput, WriteFileResult };
|
|
149
|
+
//# sourceMappingURL=tool-io.type.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-io.type.d.mts","names":[],"sources":["../../../../../../../@warlock.js/ai-workspace/src/contracts/tool-io.type.ts"],"mappings":";;AAYA;;;;AAA6B;AAU7B;;;;;KAVY,iBAAA;;UAUK,aAAA;EAMV;EAJL,IAAA;EAY6B;EAV7B,SAAA;EAU6B;EAR7B,KAAA;AAAA;;;;;;UAQe,cAAA;EAmBA;EAjBf,OAAA;;EAEA,SAAA;EAiBA;EAfA,OAAA;EAmBA;EAjBA,UAAA;EAqBA;EAnBA,SAAA;EAmBU;EAjBV,IAAA;AAAA;;;;;UAOe,aAAA;EAoBX;EAlBJ,IAAA;EAsBe;EApBf,SAAA;;EAEA,SAAA;EAsBO;EApBP,UAAA;EAwB8B;EAtB9B,UAAA;AAAA;;UAIe,cAAA;EAwBf;EAtBA,IAAA;EAsBI;EApBJ,YAAA;EAwB4B;EAtB5B,IAAA;AAAA;AA0BS;AAAA,UAtBM,cAAA;EA0Bc;EAxB7B,IAAA;EAwB6B;EAtB7B,OAAO;AAAA;;UAIQ,eAAA;EA4Bf;EA1BA,IAAA;EA0BQ;EAxBR,YAAA;EAgC4B;EA9B5B,IAAA;AAAA;AAgCO;AAAA,UA5BQ,aAAA;EAgCS;EA9BxB,OAAA;EA8BwB;EA5BxB,SAAS;AAAA;;UAIM,cAAA;EA8BL;EA5BV,QAAA;EAgCwB;EA9BxB,MAAA;EA8BwB;EA5BxB,MAAA;EAgCA;EA9BA,SAAA;EAgCI;EA9BJ,QAAA;AAAA;;;;;;UAQe,aAAA;EA8BV;EA5BL,OAAO;AAAA;;UAIQ,SAAA;EA8BR;EA5BP,OAAA;EAgCe;EA9Bf,IAAA;;EAEA,UAAA;AAAA;;UAIe,SAAA;;EAEf,IAAA;;EAEA,IAAA;;EAEA,IAAA;AAAA;;UAIe,UAAA;;EAEf,OAAA,EAAS,SAAS;;EAElB,KAAA;AAAA;;UAIe,SAAA;;EAEf,OAAO;AAAA;;UAIQ,UAAA;;EAEf,KAAK;AAAA"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
//#region ../@warlock.js/ai-workspace/src/contracts/workspace-backend.contract.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Low-level result of a backend command execution. Mirrors the shape the
|
|
4
|
+
* policy-enforced ops layer surfaces as `RunShellResult`, minus the
|
|
5
|
+
* byte-cap `truncated` flag (capping is an ops-layer concern, not the
|
|
6
|
+
* raw executor's).
|
|
7
|
+
*/
|
|
8
|
+
interface WorkspaceBackendExecResult {
|
|
9
|
+
/** Process exit code. */
|
|
10
|
+
exitCode: number;
|
|
11
|
+
/** Captured standard output. */
|
|
12
|
+
stdout: string;
|
|
13
|
+
/** Captured standard error. */
|
|
14
|
+
stderr: string;
|
|
15
|
+
/** True when the command was killed for exceeding its timeout. */
|
|
16
|
+
timedOut: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Options threaded into a backend `exec` call. The ops layer resolves
|
|
20
|
+
* the effective environment and timeout from policy before handing them
|
|
21
|
+
* down — the backend just runs what it is told.
|
|
22
|
+
*/
|
|
23
|
+
interface WorkspaceBackendExecOptions {
|
|
24
|
+
/** Working directory for the spawned process (already jail-resolved). */
|
|
25
|
+
cwd?: string;
|
|
26
|
+
/** Wall-clock cap in ms; on expiry the process is SIGKILLed. */
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
/** Exact environment for the spawned process (NOT merged with process.env). */
|
|
29
|
+
env?: Record<string, string>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The swappable executor a workspace runs on — the thin, **policy-agnostic**
|
|
33
|
+
* IO seam the ops layer calls. A backend takes **already-resolved absolute
|
|
34
|
+
* paths** and performs the side effect; it does NOT know about `cwd` jails,
|
|
35
|
+
* allow/deny lists, hashes, or line windows — that enforcement lives one
|
|
36
|
+
* layer up in {@link WorkspaceOps}.
|
|
37
|
+
*
|
|
38
|
+
* Implementations:
|
|
39
|
+
* - **local** — `@warlock.js/fs` for IO + `node:child_process` for `exec`.
|
|
40
|
+
* - **mock** — an in-memory `Map` + scripted `exec`, for hermetic tests.
|
|
41
|
+
*
|
|
42
|
+
* Keeping the backend dumb is what lets the same ops/policy layer run
|
|
43
|
+
* unchanged over a real disk, an in-memory fake, or (later) a container.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* const out = await backend.readFile("/srv/acme-api/src/index.ts");
|
|
47
|
+
* const { exitCode } = await backend.exec("npm test", { cwd: "/srv/acme-api" });
|
|
48
|
+
*/
|
|
49
|
+
interface WorkspaceBackend {
|
|
50
|
+
/** Read a file's full UTF-8 content at an absolute path. */
|
|
51
|
+
readFile(absPath: string): Promise<string>;
|
|
52
|
+
/** Write full content to an absolute path (atomically where supported). */
|
|
53
|
+
writeFile(absPath: string, content: string): Promise<void>;
|
|
54
|
+
/** Whether anything exists at an absolute path (file or directory). */
|
|
55
|
+
exists(absPath: string): Promise<boolean>;
|
|
56
|
+
/** Create a directory (and parents) at an absolute path; idempotent. */
|
|
57
|
+
mkdir(absPath: string): Promise<void>;
|
|
58
|
+
/** Remove a file or directory tree at an absolute path. */
|
|
59
|
+
remove(absPath: string): Promise<void>;
|
|
60
|
+
/** List immediate children of an absolute directory as absolute paths. */
|
|
61
|
+
list(absDir: string): Promise<string[]>;
|
|
62
|
+
/** Resolve symlinks/`..` to a canonical absolute path (jail enforcement). */
|
|
63
|
+
realpath(absPath: string): Promise<string>;
|
|
64
|
+
/** Run a command and capture its outcome. */
|
|
65
|
+
exec(command: string, opts?: WorkspaceBackendExecOptions): Promise<WorkspaceBackendExecResult>;
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
export { WorkspaceBackend, WorkspaceBackendExecOptions, WorkspaceBackendExecResult };
|
|
69
|
+
//# sourceMappingURL=workspace-backend.contract.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-backend.contract.d.mts","names":[],"sources":["../../../../../../../@warlock.js/ai-workspace/src/contracts/workspace-backend.contract.ts"],"mappings":";;AAMA;;;;;UAAiB,0BAAA;EAMf;EAJA,QAAA;EAMQ;EAJR,MAAA;EAYe;EAVf,MAAA;;EAEA,QAAA;AAAA;;;;;AAcY;UANG,2BAAA;EA2BgB;EAzB/B,GAAA;EA2B2B;EAzB3B,SAAA;EA6ByB;EA3BzB,GAAA,GAAM,MAAM;AAAA;;;;;;;;;;;;;;;;;;;UAqBG,gBAAA;EAQS;EANxB,QAAA,CAAS,OAAA,WAAkB,OAAA;EAQpB;EANP,SAAA,CAAU,OAAA,UAAiB,OAAA,WAAkB,OAAA;EAQ7C;EANA,MAAA,CAAO,OAAA,WAAkB,OAAA;EAMH;EAJtB,KAAA,CAAM,OAAA,WAAkB,OAAA;EAMf;EAJT,MAAA,CAAO,OAAA,WAAkB,OAAA;EAMzB;EAJA,IAAA,CAAK,MAAA,WAAiB,OAAA;EAMb;EAJT,QAAA,CAAS,OAAA,WAAkB,OAAA;EAKxB;EAHH,IAAA,CACE,OAAA,UACA,IAAA,GAAO,2BAAA,GACN,OAAA,CAAQ,0BAAA;AAAA"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { EditFileInput, EditFileResult, GrepResult, RunShellResult } from "./tool-io.type.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../@warlock.js/ai-workspace/src/contracts/workspace-ops.contract.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The **policy-enforced** operation layer that sits between the dumb
|
|
6
|
+
* {@link WorkspaceBackend} and the two public callers — the agent-facing
|
|
7
|
+
* `.tools.*` factories and the human-facing direct methods. Both share
|
|
8
|
+
* this single instance, so there is exactly one jail and one set of
|
|
9
|
+
* rules regardless of who calls.
|
|
10
|
+
*
|
|
11
|
+
* Every method here resolves workspace-relative paths against `cwd`,
|
|
12
|
+
* applies the path allow/deny jail, gates shell commands, enforces read
|
|
13
|
+
* caps and the read-before-edit stale-hash guard, then delegates the raw
|
|
14
|
+
* IO to the backend. Policy violations surface as typed errors
|
|
15
|
+
* (`WorkspacePolicyError` / `WorkspaceEditError`), which the tool layer
|
|
16
|
+
* turns into agent-visible tool-error data rather than thrown
|
|
17
|
+
* run-killers.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* const { content, hash } = await ops.readFile("src/index.ts");
|
|
21
|
+
* await ops.editFile({ path: "src/index.ts", oldString: "x", newString: "y", expectHash: hash });
|
|
22
|
+
*/
|
|
23
|
+
interface WorkspaceOps {
|
|
24
|
+
/**
|
|
25
|
+
* Read a jailed file, returning its content, content hash, and total
|
|
26
|
+
* line count. `offset`/`limit` select a 1-based line window; the
|
|
27
|
+
* policy `read` caps still apply.
|
|
28
|
+
*/
|
|
29
|
+
readFile(path: string, opts?: {
|
|
30
|
+
offset?: number;
|
|
31
|
+
limit?: number;
|
|
32
|
+
}): Promise<{
|
|
33
|
+
content: string;
|
|
34
|
+
hash: string;
|
|
35
|
+
totalLines: number;
|
|
36
|
+
}>;
|
|
37
|
+
/** Atomically write full content to a jailed path, returning size + hash. */
|
|
38
|
+
writeFile(path: string, content: string): Promise<{
|
|
39
|
+
hash: string;
|
|
40
|
+
bytesWritten: number;
|
|
41
|
+
}>;
|
|
42
|
+
/**
|
|
43
|
+
* Apply an exact-string edit under the read-before-edit guard:
|
|
44
|
+
* rejects on a non-unique `oldString` (unless `replaceAll`) or a
|
|
45
|
+
* stale `expectHash`.
|
|
46
|
+
*/
|
|
47
|
+
editFile(input: EditFileInput): Promise<EditFileResult>;
|
|
48
|
+
/**
|
|
49
|
+
* Run a shell command after gating its leading executable basename
|
|
50
|
+
* against the shell allow/deny policy, with the policy environment,
|
|
51
|
+
* timeout, and output byte cap applied.
|
|
52
|
+
*/
|
|
53
|
+
exec(command: string, opts?: {
|
|
54
|
+
timeoutMs?: number;
|
|
55
|
+
}): Promise<RunShellResult>;
|
|
56
|
+
/** Search jailed file contents for a regex pattern. */
|
|
57
|
+
grep(pattern: string, opts?: {
|
|
58
|
+
glob?: string;
|
|
59
|
+
ignoreCase?: boolean;
|
|
60
|
+
}): Promise<GrepResult>;
|
|
61
|
+
/** Resolve a glob to matching workspace-relative paths within the jail. */
|
|
62
|
+
glob(pattern: string): Promise<string[]>;
|
|
63
|
+
/** Whether a jailed path exists (file or directory). */
|
|
64
|
+
exists(path: string): Promise<boolean>;
|
|
65
|
+
/** Create a directory (and parents) at a jailed path; idempotent. */
|
|
66
|
+
mkdir(path: string): Promise<void>;
|
|
67
|
+
/** Remove a file or directory tree at a jailed path. */
|
|
68
|
+
remove(path: string): Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
export { WorkspaceOps };
|
|
72
|
+
//# sourceMappingURL=workspace-ops.contract.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-ops.contract.d.mts","names":[],"sources":["../../../../../../../@warlock.js/ai-workspace/src/contracts/workspace-ops.contract.ts"],"mappings":";;;;;AA0BA;;;;;;;;;;;;;;;;;UAAiB,YAAA;EAMf;;;;;EAAA,QAAA,CACE,IAAA,UACA,IAAA;IAAS,MAAA;IAAiB,KAAA;EAAA,IACzB,OAAA;IAAU,OAAA;IAAiB,IAAA;IAAc,UAAA;EAAA;EAM/B;EAHb,SAAA,CACE,IAAA,UACA,OAAA,WACC,OAAA;IAAU,IAAA;IAAc,YAAA;EAAA;EAOK;;;;;EAAhC,QAAA,CAAS,KAAA,EAAO,aAAA,GAAgB,OAAA,CAAQ,cAAA;EAUrC;;;;;EAHH,IAAA,CACE,OAAA,UACA,IAAA;IAAS,SAAA;EAAA,IACR,OAAA,CAAQ,cAAA;EAMA;EAHX,IAAA,CACE,OAAA,UACA,IAAA;IAAS,IAAA;IAAe,UAAA;EAAA,IACvB,OAAA,CAAQ,UAAA;EAMJ;EAHP,IAAA,CAAK,OAAA,WAAkB,OAAA;EAMvB;EAHA,MAAA,CAAO,IAAA,WAAe,OAAA;EAGD;EAArB,KAAA,CAAM,IAAA,WAAe,OAAA;EAGd;EAAP,MAAA,CAAO,IAAA,WAAe,OAAA;AAAA"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
//#region ../@warlock.js/ai-workspace/src/contracts/workspace-policy.type.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The executor a workspace runs on. `"local"` uses `@warlock.js/fs` +
|
|
4
|
+
* `node:child_process` against the real filesystem under `cwd`;
|
|
5
|
+
* `"mock"` uses an in-memory backend (for hermetic, disk-free tests).
|
|
6
|
+
*
|
|
7
|
+
* The worktree and container backends from the design doc are deferred
|
|
8
|
+
* past the W1 surface and intentionally absent from this union.
|
|
9
|
+
*/
|
|
10
|
+
type WorkspaceBackendType = "local" | "mock";
|
|
11
|
+
/**
|
|
12
|
+
* Shell-execution sub-policy for `run_shell` / `run_tests` and the
|
|
13
|
+
* `exec()` direct method.
|
|
14
|
+
*
|
|
15
|
+
* **Command gating.** Each command's leading executable basename is
|
|
16
|
+
* matched against `allow` / `deny`; **deny wins** when a name appears
|
|
17
|
+
* in both. An empty/absent `allow` means no command is permitted unless
|
|
18
|
+
* the policy explicitly opts in — the workspace is fail-closed.
|
|
19
|
+
*
|
|
20
|
+
* **Environment.** The spawned process does NOT inherit `process.env`.
|
|
21
|
+
* The effective environment is `{ ...pick(process.env, inheritEnv), ...env }`
|
|
22
|
+
* — so `run_shell` cannot find `node`/`npm` unless `inheritEnv` includes
|
|
23
|
+
* `"PATH"`. This is deliberate isolation, documented loudly.
|
|
24
|
+
*/
|
|
25
|
+
interface WorkspaceShellPolicy {
|
|
26
|
+
/** Allowed executable basenames (e.g. `["npm", "node"]`). */
|
|
27
|
+
allow?: string[];
|
|
28
|
+
/** Denied executable basenames — wins over `allow`. */
|
|
29
|
+
deny?: string[];
|
|
30
|
+
/** Per-command wall-clock cap; on expiry the process is SIGKILLed. */
|
|
31
|
+
timeoutMs?: number;
|
|
32
|
+
/** Max bytes captured from stdout/stderr before output is truncated. */
|
|
33
|
+
maxOutputBytes?: number;
|
|
34
|
+
/** Explicit environment variables injected into every spawned process. */
|
|
35
|
+
env?: Record<string, string>;
|
|
36
|
+
/**
|
|
37
|
+
* Opt-in passthrough of named `process.env` keys (e.g. `["PATH"]`).
|
|
38
|
+
* Nothing from `process.env` leaks in unless listed here.
|
|
39
|
+
*/
|
|
40
|
+
inheritEnv?: string[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Read sub-policy controlling how much of a file a single read returns.
|
|
44
|
+
*/
|
|
45
|
+
interface WorkspaceReadPolicy {
|
|
46
|
+
/** Hard cap on bytes returned by a single read; larger reads truncate. */
|
|
47
|
+
maxBytes?: number;
|
|
48
|
+
/** Default number of lines a read returns when no explicit limit is given. */
|
|
49
|
+
defaultLines?: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The full policy that bounds a workspace — the single source of truth
|
|
53
|
+
* the policy engine enforces for both `.tools.*` and the direct methods.
|
|
54
|
+
*
|
|
55
|
+
* **Path jail.** Every path is `realpath`-resolved and must sit under
|
|
56
|
+
* `cwd` (or an `allowPaths` root); `denyPaths` globs are blocked even
|
|
57
|
+
* inside `cwd` (e.g. `.git/**`, `.env*`). A path that escapes the jail
|
|
58
|
+
* surfaces as a `WorkspacePolicyError`, returned to the agent as tool
|
|
59
|
+
* data — never a thrown run-killer.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* const policy: WorkspacePolicy = {
|
|
63
|
+
* cwd: "/srv/acme-api",
|
|
64
|
+
* denyPaths: [".git/**", ".env*"],
|
|
65
|
+
* shell: { allow: ["npm", "node"], inheritEnv: ["PATH"], timeoutMs: 60_000 },
|
|
66
|
+
* read: { maxBytes: 256_000, defaultLines: 2_000 },
|
|
67
|
+
* backend: "local",
|
|
68
|
+
* };
|
|
69
|
+
*/
|
|
70
|
+
interface WorkspacePolicy {
|
|
71
|
+
/** Absolute jail root — the workspace cannot read or write outside it. */
|
|
72
|
+
cwd: string;
|
|
73
|
+
/** Extra readable roots outside `cwd` (resolved absolute paths). */
|
|
74
|
+
allowPaths?: string[];
|
|
75
|
+
/** Globs blocked even inside `cwd` (deny wins over allow). */
|
|
76
|
+
denyPaths?: string[];
|
|
77
|
+
/** Shell-execution sub-policy. Absent ⇒ no command may run. */
|
|
78
|
+
shell?: WorkspaceShellPolicy;
|
|
79
|
+
/** Read sub-policy (byte cap + default line window). */
|
|
80
|
+
read?: WorkspaceReadPolicy;
|
|
81
|
+
/** Executor the workspace runs on. Defaults to `"local"`. */
|
|
82
|
+
backend?: WorkspaceBackendType;
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
export { WorkspaceBackendType, WorkspacePolicy, WorkspaceReadPolicy, WorkspaceShellPolicy };
|
|
86
|
+
//# sourceMappingURL=workspace-policy.type.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-policy.type.d.mts","names":[],"sources":["../../../../../../../@warlock.js/ai-workspace/src/contracts/workspace-policy.type.ts"],"mappings":";;AAQA;;;;AAAgC;AAgBhC;;KAhBY,oBAAA;;;;;;;;;;AA+BA;AAMZ;;;;UArBiB,oBAAA;EA+CA;EA7Cf,KAAA;;EAEA,IAAA;EAqDO;EAnDP,SAAA;EAqD8B;EAnD9B,cAAA;EAyCA;EAvCA,GAAA,GAAM,MAAM;EA2CZ;;;;EAtCA,UAAA;AAAA;;;AA4C8B;UAtCf,mBAAA;;EAEf,QAAA;;EAEA,YAAY;AAAA;;;;;;;;;;;;;;;;;;;;UAsBG,eAAA;;EAEf,GAAA;;EAEA,UAAA;;EAEA,SAAA;;EAEA,KAAA,GAAQ,oBAAA;;EAER,IAAA,GAAO,mBAAA;;EAEP,OAAA,GAAU,oBAAA;AAAA"}
|