@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.
Files changed (78) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +149 -0
  4. package/cjs/index.cjs +1609 -0
  5. package/cjs/index.cjs.map +1 -0
  6. package/esm/backends/local.d.mts +22 -0
  7. package/esm/backends/local.d.mts.map +1 -0
  8. package/esm/backends/local.mjs +208 -0
  9. package/esm/backends/local.mjs.map +1 -0
  10. package/esm/backends/mock.d.mts +62 -0
  11. package/esm/backends/mock.d.mts.map +1 -0
  12. package/esm/backends/mock.mjs +167 -0
  13. package/esm/backends/mock.mjs.map +1 -0
  14. package/esm/contracts/index.d.mts +5 -0
  15. package/esm/contracts/tool-io.type.d.mts +149 -0
  16. package/esm/contracts/tool-io.type.d.mts.map +1 -0
  17. package/esm/contracts/workspace-backend.contract.d.mts +69 -0
  18. package/esm/contracts/workspace-backend.contract.d.mts.map +1 -0
  19. package/esm/contracts/workspace-ops.contract.d.mts +72 -0
  20. package/esm/contracts/workspace-ops.contract.d.mts.map +1 -0
  21. package/esm/contracts/workspace-policy.type.d.mts +86 -0
  22. package/esm/contracts/workspace-policy.type.d.mts.map +1 -0
  23. package/esm/contracts/workspace.contract.d.mts +131 -0
  24. package/esm/contracts/workspace.contract.d.mts.map +1 -0
  25. package/esm/errors.d.mts +100 -0
  26. package/esm/errors.d.mts.map +1 -0
  27. package/esm/errors.mjs +58 -0
  28. package/esm/errors.mjs.map +1 -0
  29. package/esm/index.d.mts +20 -0
  30. package/esm/index.mjs +15 -0
  31. package/esm/ops.d.mts +25 -0
  32. package/esm/ops.d.mts.map +1 -0
  33. package/esm/ops.mjs +294 -0
  34. package/esm/ops.mjs.map +1 -0
  35. package/esm/policy/policy.d.mts +71 -0
  36. package/esm/policy/policy.d.mts.map +1 -0
  37. package/esm/policy/policy.mjs +184 -0
  38. package/esm/policy/policy.mjs.map +1 -0
  39. package/esm/tools/edit-file.d.mts +40 -0
  40. package/esm/tools/edit-file.d.mts.map +1 -0
  41. package/esm/tools/edit-file.mjs +57 -0
  42. package/esm/tools/edit-file.mjs.map +1 -0
  43. package/esm/tools/glob.d.mts +37 -0
  44. package/esm/tools/glob.d.mts.map +1 -0
  45. package/esm/tools/glob.mjs +45 -0
  46. package/esm/tools/glob.mjs.map +1 -0
  47. package/esm/tools/grep.d.mts +36 -0
  48. package/esm/tools/grep.d.mts.map +1 -0
  49. package/esm/tools/grep.mjs +51 -0
  50. package/esm/tools/grep.mjs.map +1 -0
  51. package/esm/tools/read-file.d.mts +35 -0
  52. package/esm/tools/read-file.d.mts.map +1 -0
  53. package/esm/tools/read-file.mjs +64 -0
  54. package/esm/tools/read-file.mjs.map +1 -0
  55. package/esm/tools/run-shell.d.mts +35 -0
  56. package/esm/tools/run-shell.d.mts.map +1 -0
  57. package/esm/tools/run-shell.mjs +65 -0
  58. package/esm/tools/run-shell.mjs.map +1 -0
  59. package/esm/tools/run-tests.d.mts +40 -0
  60. package/esm/tools/run-tests.d.mts.map +1 -0
  61. package/esm/tools/run-tests.mjs +67 -0
  62. package/esm/tools/run-tests.mjs.map +1 -0
  63. package/esm/tools/schema.mjs +111 -0
  64. package/esm/tools/schema.mjs.map +1 -0
  65. package/esm/tools/write-file.d.mts +33 -0
  66. package/esm/tools/write-file.d.mts.map +1 -0
  67. package/esm/tools/write-file.mjs +52 -0
  68. package/esm/tools/write-file.mjs.map +1 -0
  69. package/esm/workspace.d.mts +54 -0
  70. package/esm/workspace.d.mts.map +1 -0
  71. package/esm/workspace.mjs +210 -0
  72. package/esm/workspace.mjs.map +1 -0
  73. package/llms-full.txt +231 -0
  74. package/llms.txt +10 -0
  75. package/package.json +42 -0
  76. package/skills/README.md +13 -0
  77. package/skills/build-loop-agent/SKILL.md +100 -0
  78. 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"}