@warlock.js/ai-workspace 5.30.0 → 5.31.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 +46 -2
- package/README.md +34 -21
- package/cjs/index.cjs +3116 -309
- package/cjs/index.cjs.map +1 -1
- package/esm/backends/local.d.mts.map +1 -1
- package/esm/backends/local.mjs +165 -27
- package/esm/backends/local.mjs.map +1 -1
- package/esm/backends/mock.d.mts +11 -5
- package/esm/backends/mock.d.mts.map +1 -1
- package/esm/backends/mock.mjs +34 -0
- package/esm/backends/mock.mjs.map +1 -1
- package/esm/contracts/index.d.mts +3 -3
- package/esm/contracts/tool-io.type.d.mts +202 -4
- package/esm/contracts/tool-io.type.d.mts.map +1 -1
- package/esm/contracts/workspace-backend.contract.d.mts +61 -1
- package/esm/contracts/workspace-backend.contract.d.mts.map +1 -1
- package/esm/contracts/workspace-ops.contract.d.mts +32 -3
- package/esm/contracts/workspace-ops.contract.d.mts.map +1 -1
- package/esm/contracts/workspace-policy.type.d.mts +133 -12
- package/esm/contracts/workspace-policy.type.d.mts.map +1 -1
- package/esm/contracts/workspace.contract.d.mts +63 -9
- package/esm/contracts/workspace.contract.d.mts.map +1 -1
- package/esm/errors.d.mts +79 -5
- package/esm/errors.d.mts.map +1 -1
- package/esm/errors.mjs +22 -1
- package/esm/errors.mjs.map +1 -1
- package/esm/index.d.mts +13 -5
- package/esm/index.mjs +10 -2
- package/esm/ops.d.mts +21 -2
- package/esm/ops.d.mts.map +1 -1
- package/esm/ops.mjs +230 -64
- package/esm/ops.mjs.map +1 -1
- package/esm/policy/jail.mjs +73 -0
- package/esm/policy/jail.mjs.map +1 -0
- package/esm/policy/policy.d.mts +25 -15
- package/esm/policy/policy.d.mts.map +1 -1
- package/esm/policy/policy.mjs +61 -85
- package/esm/policy/policy.mjs.map +1 -1
- package/esm/policy/shell-check.d.mts +64 -0
- package/esm/policy/shell-check.d.mts.map +1 -0
- package/esm/policy/shell-check.mjs +627 -0
- package/esm/policy/shell-check.mjs.map +1 -0
- package/esm/policy/shell-parser.mjs +802 -0
- package/esm/policy/shell-parser.mjs.map +1 -0
- package/esm/policy/tokenize-command.d.mts +63 -0
- package/esm/policy/tokenize-command.d.mts.map +1 -0
- package/esm/policy/tokenize-command.mjs +75 -23
- package/esm/policy/tokenize-command.mjs.map +1 -1
- package/esm/shell-jobs.d.mts +45 -0
- package/esm/shell-jobs.d.mts.map +1 -0
- package/esm/shell-jobs.mjs +247 -0
- package/esm/shell-jobs.mjs.map +1 -0
- package/esm/todos.d.mts +22 -0
- package/esm/todos.d.mts.map +1 -0
- package/esm/todos.mjs +73 -0
- package/esm/todos.mjs.map +1 -0
- package/esm/tools/edit-file.d.mts.map +1 -1
- package/esm/tools/edit-file.mjs +2 -1
- package/esm/tools/edit-file.mjs.map +1 -1
- package/esm/tools/glob.d.mts.map +1 -1
- package/esm/tools/glob.mjs +2 -1
- package/esm/tools/glob.mjs.map +1 -1
- package/esm/tools/grep.d.mts.map +1 -1
- package/esm/tools/grep.mjs +2 -1
- package/esm/tools/grep.mjs.map +1 -1
- package/esm/tools/json-schemas.mjs +175 -0
- package/esm/tools/json-schemas.mjs.map +1 -0
- package/esm/tools/list-dir.d.mts +32 -0
- package/esm/tools/list-dir.d.mts.map +1 -0
- package/esm/tools/list-dir.mjs +50 -0
- package/esm/tools/list-dir.mjs.map +1 -0
- package/esm/tools/multi-edit.d.mts +38 -0
- package/esm/tools/multi-edit.d.mts.map +1 -0
- package/esm/tools/multi-edit.mjs +121 -0
- package/esm/tools/multi-edit.mjs.map +1 -0
- package/esm/tools/read-file.d.mts.map +1 -1
- package/esm/tools/read-file.mjs +2 -1
- package/esm/tools/read-file.mjs.map +1 -1
- package/esm/tools/run-shell.d.mts +34 -12
- package/esm/tools/run-shell.d.mts.map +1 -1
- package/esm/tools/run-shell.mjs +59 -53
- package/esm/tools/run-shell.mjs.map +1 -1
- package/esm/tools/run-tests.d.mts +8 -0
- package/esm/tools/run-tests.d.mts.map +1 -1
- package/esm/tools/run-tests.mjs +10 -3
- package/esm/tools/run-tests.mjs.map +1 -1
- package/esm/tools/schema.mjs +23 -18
- package/esm/tools/schema.mjs.map +1 -1
- package/esm/tools/shell-kill.d.mts +25 -0
- package/esm/tools/shell-kill.d.mts.map +1 -0
- package/esm/tools/shell-kill.mjs +36 -0
- package/esm/tools/shell-kill.mjs.map +1 -0
- package/esm/tools/shell-output.d.mts +30 -0
- package/esm/tools/shell-output.d.mts.map +1 -0
- package/esm/tools/shell-output.mjs +49 -0
- package/esm/tools/shell-output.mjs.map +1 -0
- package/esm/tools/todo.d.mts +43 -0
- package/esm/tools/todo.d.mts.map +1 -0
- package/esm/tools/todo.mjs +126 -0
- package/esm/tools/todo.mjs.map +1 -0
- package/esm/tools/write-file.d.mts.map +1 -1
- package/esm/tools/write-file.mjs +2 -1
- package/esm/tools/write-file.mjs.map +1 -1
- package/esm/workspace.d.mts +11 -8
- package/esm/workspace.d.mts.map +1 -1
- package/esm/workspace.mjs +108 -27
- package/esm/workspace.mjs.map +1 -1
- package/llms-full.txt +155 -34
- package/llms.txt +1 -1
- package/package.json +3 -3
- package/skills/README.md +2 -2
- package/skills/build-loop-agent/SKILL.md +7 -7
- package/skills/index.md +6 -1
- package/skills/use-a-workspace/SKILL.md +148 -27
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { withJsonSchema } from "./schema.mjs";
|
|
2
|
+
import { TODO_READ_JSON_SCHEMA, TODO_WRITE_JSON_SCHEMA } from "./json-schemas.mjs";
|
|
3
|
+
import { TODO_STATUSES, todoListProblems } from "../todos.mjs";
|
|
4
|
+
import { tool } from "@warlock.js/ai";
|
|
5
|
+
|
|
6
|
+
//#region ../../packages/ai-workspace/src/tools/todo.ts
|
|
7
|
+
/** Whether `value` is a todo status. */
|
|
8
|
+
function isTodoStatus(value) {
|
|
9
|
+
return typeof value === "string" && TODO_STATUSES.includes(value);
|
|
10
|
+
}
|
|
11
|
+
/** Input schema for the `todo_write` tool. */
|
|
12
|
+
const todoWriteSchema = withJsonSchema({ "~standard": {
|
|
13
|
+
version: 1,
|
|
14
|
+
vendor: "ai-workspace",
|
|
15
|
+
validate(value) {
|
|
16
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return { issues: [{ message: "input must be an object" }] };
|
|
17
|
+
const raw = value.todos;
|
|
18
|
+
if (!Array.isArray(raw) || raw.length > 100) return { issues: [{
|
|
19
|
+
message: `"todos" must be an array of at most ${100} items`,
|
|
20
|
+
path: ["todos"]
|
|
21
|
+
}] };
|
|
22
|
+
const issues = [];
|
|
23
|
+
const todos = [];
|
|
24
|
+
raw.forEach((item, index) => {
|
|
25
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
26
|
+
issues.push({
|
|
27
|
+
message: `todos[${index}] must be an object`,
|
|
28
|
+
path: ["todos", index]
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const candidate = item;
|
|
33
|
+
if (typeof candidate.content !== "string") issues.push({
|
|
34
|
+
message: `todos[${index}].content must be a string`,
|
|
35
|
+
path: [
|
|
36
|
+
"todos",
|
|
37
|
+
index,
|
|
38
|
+
"content"
|
|
39
|
+
]
|
|
40
|
+
});
|
|
41
|
+
if (!isTodoStatus(candidate.status)) issues.push({
|
|
42
|
+
message: `todos[${index}].status must be one of ${TODO_STATUSES.join(", ")}`,
|
|
43
|
+
path: [
|
|
44
|
+
"todos",
|
|
45
|
+
index,
|
|
46
|
+
"status"
|
|
47
|
+
]
|
|
48
|
+
});
|
|
49
|
+
if (candidate.id !== void 0 && typeof candidate.id !== "string") issues.push({
|
|
50
|
+
message: `todos[${index}].id must be a string when provided`,
|
|
51
|
+
path: [
|
|
52
|
+
"todos",
|
|
53
|
+
index,
|
|
54
|
+
"id"
|
|
55
|
+
]
|
|
56
|
+
});
|
|
57
|
+
if (typeof candidate.content === "string" && isTodoStatus(candidate.status)) todos.push({
|
|
58
|
+
content: candidate.content,
|
|
59
|
+
status: candidate.status,
|
|
60
|
+
...typeof candidate.id === "string" ? { id: candidate.id } : {}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
if (issues.length === 0) for (const problem of todoListProblems(todos)) issues.push({
|
|
64
|
+
message: problem,
|
|
65
|
+
path: ["todos"]
|
|
66
|
+
});
|
|
67
|
+
return issues.length > 0 ? { issues } : { value: { todos } };
|
|
68
|
+
}
|
|
69
|
+
} }, TODO_WRITE_JSON_SCHEMA);
|
|
70
|
+
/** Input schema for the `todo_read` tool (no arguments). */
|
|
71
|
+
const todoReadSchema = withJsonSchema({ "~standard": {
|
|
72
|
+
version: 1,
|
|
73
|
+
vendor: "ai-workspace",
|
|
74
|
+
validate: () => ({ value: {} })
|
|
75
|
+
} }, TODO_READ_JSON_SCHEMA);
|
|
76
|
+
/**
|
|
77
|
+
* Build the agent-facing `todo_write` tool over a workspace's todo list:
|
|
78
|
+
* the model sends the **complete** updated list (each item `content` +
|
|
79
|
+
* `status` of `pending` / `in_progress` / `completed`, optional `id`) and it
|
|
80
|
+
* replaces the old one. The list lives in memory, per workspace (shared by
|
|
81
|
+
* its `readonly()` / `scope()` projections), for the length of the run.
|
|
82
|
+
*
|
|
83
|
+
* @param store - The workspace's todo list.
|
|
84
|
+
* @param options - Optional overrides; `name` renames the LLM-visible tool.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* await makeTodoWriteTool(store).invoke({
|
|
88
|
+
* todos: [
|
|
89
|
+
* { content: "Reproduce the failing test", status: "completed" },
|
|
90
|
+
* { content: "Fix the rounding bug", status: "in_progress" },
|
|
91
|
+
* ],
|
|
92
|
+
* });
|
|
93
|
+
*/
|
|
94
|
+
function makeTodoWriteTool(store, options) {
|
|
95
|
+
return tool({
|
|
96
|
+
name: options?.name ?? "todo_write",
|
|
97
|
+
description: "Replace your todo list for this task with the complete updated list (each item: content and status pending | in_progress | completed). Use it to plan multi-step work and to mark progress.",
|
|
98
|
+
action: "Updating the todo list",
|
|
99
|
+
input: todoWriteSchema,
|
|
100
|
+
execute: async (input) => store.write(input.todos)
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Build the agent-facing `todo_read` tool: the workspace's current todo
|
|
105
|
+
* list and a count per status.
|
|
106
|
+
*
|
|
107
|
+
* @param store - The workspace's todo list.
|
|
108
|
+
* @param options - Optional overrides; `name` renames the LLM-visible tool.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* const { data } = await makeTodoReadTool(store).invoke({});
|
|
112
|
+
* console.log(data?.counts.pending);
|
|
113
|
+
*/
|
|
114
|
+
function makeTodoReadTool(store, options) {
|
|
115
|
+
return tool({
|
|
116
|
+
name: options?.name ?? "todo_read",
|
|
117
|
+
description: "Read your current todo list for this task.",
|
|
118
|
+
action: "Reading the todo list",
|
|
119
|
+
input: todoReadSchema,
|
|
120
|
+
execute: async () => store.read()
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
//#endregion
|
|
125
|
+
export { makeTodoReadTool, makeTodoWriteTool };
|
|
126
|
+
//# sourceMappingURL=todo.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"todo.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/todo.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { tool, type ToolContract } from \"@warlock.js/ai\";\nimport { MAX_TODOS, TODO_STATUSES, type TodoStore, todoListProblems } from \"../todos\";\nimport type {\n TodoListResult,\n TodoWriteInput,\n WorkspaceTodoInput,\n WorkspaceTodoStatus,\n} from \"../contracts\";\nimport { TODO_READ_JSON_SCHEMA, TODO_WRITE_JSON_SCHEMA } from \"./json-schemas\";\nimport { withJsonSchema } from \"./schema\";\n\n/** Whether `value` is a todo status. */\nfunction isTodoStatus(value: unknown): value is WorkspaceTodoStatus {\n return typeof value === \"string\" && (TODO_STATUSES as readonly string[]).includes(value);\n}\n\n/** Input schema for the `todo_write` tool. */\nconst todoWriteSchema = withJsonSchema<TodoWriteInput>(\n {\n \"~standard\": {\n version: 1,\n vendor: \"ai-workspace\",\n validate(value) {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return { issues: [{ message: \"input must be an object\" }] };\n }\n\n const raw = (value as Record<string, unknown>).todos;\n\n if (!Array.isArray(raw) || raw.length > MAX_TODOS) {\n return {\n issues: [\n {\n message: `\"todos\" must be an array of at most ${MAX_TODOS} items`,\n path: [\"todos\"],\n },\n ],\n };\n }\n\n const issues: StandardSchemaV1.Issue[] = [];\n const todos: WorkspaceTodoInput[] = [];\n\n raw.forEach((item: unknown, index: number) => {\n if (typeof item !== \"object\" || item === null || Array.isArray(item)) {\n issues.push({ message: `todos[${index}] must be an object`, path: [\"todos\", index] });\n\n return;\n }\n\n const candidate = item as Record<string, unknown>;\n\n if (typeof candidate.content !== \"string\") {\n issues.push({\n message: `todos[${index}].content must be a string`,\n path: [\"todos\", index, \"content\"],\n });\n }\n\n if (!isTodoStatus(candidate.status)) {\n issues.push({\n message: `todos[${index}].status must be one of ${TODO_STATUSES.join(\", \")}`,\n path: [\"todos\", index, \"status\"],\n });\n }\n\n if (candidate.id !== undefined && typeof candidate.id !== \"string\") {\n issues.push({\n message: `todos[${index}].id must be a string when provided`,\n path: [\"todos\", index, \"id\"],\n });\n }\n\n if (typeof candidate.content === \"string\" && isTodoStatus(candidate.status)) {\n todos.push({\n content: candidate.content,\n status: candidate.status,\n ...(typeof candidate.id === \"string\" ? { id: candidate.id } : {}),\n });\n }\n });\n\n if (issues.length === 0) {\n for (const problem of todoListProblems(todos)) {\n issues.push({ message: problem, path: [\"todos\"] });\n }\n }\n\n return issues.length > 0 ? { issues } : { value: { todos } };\n },\n },\n },\n TODO_WRITE_JSON_SCHEMA,\n);\n\n/** Input schema for the `todo_read` tool (no arguments). */\nconst todoReadSchema = withJsonSchema<Record<string, never>>(\n {\n \"~standard\": {\n version: 1,\n vendor: \"ai-workspace\",\n validate: () => ({ value: {} }),\n },\n },\n TODO_READ_JSON_SCHEMA,\n);\n\n/**\n * Build the agent-facing `todo_write` tool over a workspace's todo list:\n * the model sends the **complete** updated list (each item `content` +\n * `status` of `pending` / `in_progress` / `completed`, optional `id`) and it\n * replaces the old one. The list lives in memory, per workspace (shared by\n * its `readonly()` / `scope()` projections), for the length of the run.\n *\n * @param store - The workspace's todo list.\n * @param options - Optional overrides; `name` renames the LLM-visible tool.\n *\n * @example\n * await makeTodoWriteTool(store).invoke({\n * todos: [\n * { content: \"Reproduce the failing test\", status: \"completed\" },\n * { content: \"Fix the rounding bug\", status: \"in_progress\" },\n * ],\n * });\n */\nexport function makeTodoWriteTool(\n store: TodoStore,\n options?: { name?: string },\n): ToolContract<TodoWriteInput, TodoListResult> {\n return tool<TodoWriteInput, TodoListResult>({\n name: options?.name ?? \"todo_write\",\n description:\n \"Replace your todo list for this task with the complete updated list \" +\n \"(each item: content and status pending | in_progress | completed). \" +\n \"Use it to plan multi-step work and to mark progress.\",\n action: \"Updating the todo list\",\n input: todoWriteSchema,\n execute: async (input) => store.write(input.todos),\n });\n}\n\n/**\n * Build the agent-facing `todo_read` tool: the workspace's current todo\n * list and a count per status.\n *\n * @param store - The workspace's todo list.\n * @param options - Optional overrides; `name` renames the LLM-visible tool.\n *\n * @example\n * const { data } = await makeTodoReadTool(store).invoke({});\n * console.log(data?.counts.pending);\n */\nexport function makeTodoReadTool(\n store: TodoStore,\n options?: { name?: string },\n): ToolContract<Record<string, never>, TodoListResult> {\n return tool<Record<string, never>, TodoListResult>({\n name: options?.name ?? \"todo_read\",\n description: \"Read your current todo list for this task.\",\n action: \"Reading the todo list\",\n input: todoReadSchema,\n execute: async () => store.read(),\n });\n}\n"],"mappings":";;;;;;;AAaA,SAAS,aAAa,OAA8C;CAClE,OAAO,OAAO,UAAU,YAAa,cAAoC,SAAS,KAAK;AACzF;;AAGA,MAAM,kBAAkB,eACtB,EACE,aAAa;CACX,SAAS;CACT,QAAQ;CACR,SAAS,OAAO;EACd,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GACpE,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,0BAA0B,CAAC,EAAE;EAG5D,MAAM,MAAO,MAAkC;EAE/C,IAAI,CAAC,MAAM,QAAQ,GAAG,KAAK,IAAI,cAC7B,OAAO,EACL,QAAQ,CACN;GACE,SAAS,2CAAiD;GAC1D,MAAM,CAAC,OAAO;EAChB,CACF,EACF;EAGF,MAAM,SAAmC,CAAC;EAC1C,MAAM,QAA8B,CAAC;EAErC,IAAI,SAAS,MAAe,UAAkB;GAC5C,IAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,MAAM,QAAQ,IAAI,GAAG;IACpE,OAAO,KAAK;KAAE,SAAS,SAAS,MAAM;KAAsB,MAAM,CAAC,SAAS,KAAK;IAAE,CAAC;IAEpF;GACF;GAEA,MAAM,YAAY;GAElB,IAAI,OAAO,UAAU,YAAY,UAC/B,OAAO,KAAK;IACV,SAAS,SAAS,MAAM;IACxB,MAAM;KAAC;KAAS;KAAO;IAAS;GAClC,CAAC;GAGH,IAAI,CAAC,aAAa,UAAU,MAAM,GAChC,OAAO,KAAK;IACV,SAAS,SAAS,MAAM,0BAA0B,cAAc,KAAK,IAAI;IACzE,MAAM;KAAC;KAAS;KAAO;IAAQ;GACjC,CAAC;GAGH,IAAI,UAAU,OAAO,UAAa,OAAO,UAAU,OAAO,UACxD,OAAO,KAAK;IACV,SAAS,SAAS,MAAM;IACxB,MAAM;KAAC;KAAS;KAAO;IAAI;GAC7B,CAAC;GAGH,IAAI,OAAO,UAAU,YAAY,YAAY,aAAa,UAAU,MAAM,GACxE,MAAM,KAAK;IACT,SAAS,UAAU;IACnB,QAAQ,UAAU;IAClB,GAAI,OAAO,UAAU,OAAO,WAAW,EAAE,IAAI,UAAU,GAAG,IAAI,CAAC;GACjE,CAAC;EAEL,CAAC;EAED,IAAI,OAAO,WAAW,GACpB,KAAK,MAAM,WAAW,iBAAiB,KAAK,GAC1C,OAAO,KAAK;GAAE,SAAS;GAAS,MAAM,CAAC,OAAO;EAAE,CAAC;EAIrD,OAAO,OAAO,SAAS,IAAI,EAAE,OAAO,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE;CAC7D;AACF,EACF,GACA,sBACF;;AAGA,MAAM,iBAAiB,eACrB,EACE,aAAa;CACX,SAAS;CACT,QAAQ;CACR,iBAAiB,EAAE,OAAO,CAAC,EAAE;AAC/B,EACF,GACA,qBACF;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,kBACd,OACA,SAC8C;CAC9C,OAAO,KAAqC;EAC1C,MAAM,SAAS,QAAQ;EACvB,aACE;EAGF,QAAQ;EACR,OAAO;EACP,SAAS,OAAO,UAAU,MAAM,MAAM,MAAM,KAAK;CACnD,CAAC;AACH;;;;;;;;;;;;AAaA,SAAgB,iBACd,OACA,SACqD;CACrD,OAAO,KAA4C;EACjD,MAAM,SAAS,QAAQ;EACvB,aAAa;EACb,QAAQ;EACR,OAAO;EACP,SAAS,YAAY,MAAM,KAAK;CAClC,CAAC;AACH"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"write-file.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/write-file.ts"],"mappings":";;;;;;;;;
|
|
1
|
+
{"version":3,"file":"write-file.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/write-file.ts"],"mappings":";;;;;;;;;AAuCA;;;;;;;;;;;;;;;;;AAG+C;iBAH/B,iBAAA,CACd,GAAA,EAAK,YAAA,EACL,OAAA;EAAY,IAAA;AAAA,IACX,YAAA,CAAa,cAAA,EAAgB,eAAA"}
|
package/esm/tools/write-file.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { objectSchema, stringField } from "./schema.mjs";
|
|
2
|
+
import { WRITE_FILE_JSON_SCHEMA } from "./json-schemas.mjs";
|
|
2
3
|
import { tool } from "@warlock.js/ai";
|
|
3
4
|
|
|
4
5
|
//#region ../../packages/ai-workspace/src/tools/write-file.ts
|
|
@@ -8,7 +9,7 @@ const DEFAULT_NAME = "write_file";
|
|
|
8
9
|
const inputSchema = objectSchema({
|
|
9
10
|
path: stringField(),
|
|
10
11
|
content: stringField()
|
|
11
|
-
});
|
|
12
|
+
}, WRITE_FILE_JSON_SCHEMA);
|
|
12
13
|
/**
|
|
13
14
|
* Build the agent-facing `write_file` tool over a workspace's policy-
|
|
14
15
|
* enforced {@link WorkspaceOps}.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"write-file.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/write-file.ts"],"sourcesContent":["import { tool, type ToolContract } from \"@warlock.js/ai\";\nimport { objectSchema, stringField } from \"./schema\";\nimport type { WorkspaceOps, WriteFileInput, WriteFileResult } from \"../contracts\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"write_file\";\n\n/** Input schema for the `write_file` tool. */\nconst inputSchema = objectSchema<WriteFileInput>({\n
|
|
1
|
+
{"version":3,"file":"write-file.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/write-file.ts"],"sourcesContent":["import { tool, type ToolContract } from \"@warlock.js/ai\";\nimport { objectSchema, stringField } from \"./schema\";\nimport { WRITE_FILE_JSON_SCHEMA } from \"./json-schemas\";\nimport type { WorkspaceOps, WriteFileInput, WriteFileResult } from \"../contracts\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"write_file\";\n\n/** Input schema for the `write_file` tool. */\nconst inputSchema = objectSchema<WriteFileInput>(\n {\n path: stringField(),\n content: stringField(),\n },\n WRITE_FILE_JSON_SCHEMA,\n);\n\n/**\n * Build the agent-facing `write_file` tool over a workspace's policy-\n * enforced {@link WorkspaceOps}.\n *\n * The tool validates `{ path, content }` against a Standard Schema, then\n * delegates to `ops.writeFile`, which atomically writes the full content\n * (creating parent directories) and returns the byte count and content\n * `hash`. The tool re-attaches the workspace-relative `path` so the\n * result matches the {@link WriteFileResult} wire shape.\n *\n * **Errors flow as data.** A jail escape is thrown by `ops`; the\n * `tool()` wrapper catches it and surfaces it in the returned `{ error }`\n * field — `invoke()` never throws.\n *\n * @param ops - The shared, policy-enforced operation layer.\n * @param options - Optional overrides; `name` renames the LLM-visible tool.\n *\n * @example\n * const writeTool = makeWriteFileTool(ops);\n * const { data } = await writeTool.invoke({ path: \"src/new.ts\", content: \"export {};\" });\n * console.log(data.bytesWritten, data.hash);\n */\nexport function makeWriteFileTool(\n ops: WorkspaceOps,\n options?: { name?: string },\n): ToolContract<WriteFileInput, WriteFileResult> {\n return tool<WriteFileInput, WriteFileResult>({\n name: options?.name ?? DEFAULT_NAME,\n description:\n \"Write full content to a workspace file, creating it (and any parent \" +\n \"directories) if absent and overwriting it otherwise. The write is \" +\n \"atomic. Returns the bytes written and the new content hash.\",\n input: inputSchema,\n async execute(input) {\n const { hash, bytesWritten } = await ops.writeFile(input.path, input.content);\n\n return { path: input.path, bytesWritten, hash };\n },\n });\n}\n"],"mappings":";;;;;;AAMA,MAAM,eAAe;;AAGrB,MAAM,cAAc,aAClB;CACE,MAAM,YAAY;CAClB,SAAS,YAAY;AACvB,GACA,sBACF;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,kBACd,KACA,SAC+C;CAC/C,OAAO,KAAsC;EAC3C,MAAM,SAAS,QAAQ;EACvB,aACE;EAGF,OAAO;EACP,MAAM,QAAQ,OAAO;GACnB,MAAM,EAAE,MAAM,iBAAiB,MAAM,IAAI,UAAU,MAAM,MAAM,MAAM,OAAO;GAE5E,OAAO;IAAE,MAAM,MAAM;IAAM;IAAc;GAAK;EAChD;CACF,CAAC;AACH"}
|
package/esm/workspace.d.mts
CHANGED
|
@@ -4,20 +4,23 @@ import { Workspace } from "./contracts/workspace.contract.mjs";
|
|
|
4
4
|
/**
|
|
5
5
|
* Build a {@link Workspace} — the integrator that wires a
|
|
6
6
|
* {@link WorkspacePolicy} to a backend, the shared policy-enforced ops
|
|
7
|
-
* layer, and the
|
|
7
|
+
* layer, and the agent-facing tool factories.
|
|
8
8
|
*
|
|
9
9
|
* The backend is chosen from `policy.backend`: `"mock"` runs in memory
|
|
10
10
|
* (hermetic tests); the `"local"` default (and any absent value) runs
|
|
11
11
|
* over the real disk via `@warlock.js/fs` + `node:child_process`. The
|
|
12
12
|
* returned workspace exposes:
|
|
13
13
|
*
|
|
14
|
-
* - **`tools.*`** — `readFile` / `editFile` / `
|
|
15
|
-
* `
|
|
16
|
-
* `
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
14
|
+
* - **`tools.*`** — `readFile` / `editFile` / `multiEdit` / `writeFile` /
|
|
15
|
+
* `listDir` / `runShell` / `shellOutput` / `shellKill` / `runTests` /
|
|
16
|
+
* `grep` / `glob` / `todoWrite` / `todoRead`, plus `all()` (every tool)
|
|
17
|
+
* and `pick(...)` (a least-privilege subset).
|
|
18
|
+
* - **direct methods** — `readFile` / `writeFile` / `editFile` /
|
|
19
|
+
* `multiEdit` / `listDir` / `exec` / `startShell` / `shellOutput` /
|
|
20
|
+
* `shellKill` / `listShells` / `grep` / `glob` / `exists` / `mkdir` /
|
|
21
|
+
* `remove` / `readTodos` / `writeTodos`, each delegating to the shared
|
|
22
|
+
* ops layer, and `dispose()`.
|
|
23
|
+
* - **`readonly()`** — a projection that vends only the read-side tools and
|
|
21
24
|
* rejects every mutating direct method.
|
|
22
25
|
* - **`scope(subdir)`** — a sub-jailed workspace rooted at `subdir`.
|
|
23
26
|
*
|
package/esm/workspace.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace.d.mts","names":[],"sources":["../../../../../../../packages/ai-workspace/src/workspace.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"workspace.d.mts","names":[],"sources":["../../../../../../../packages/ai-workspace/src/workspace.ts"],"mappings":";;;;;;;AAuZA;;;;;;;;AAA6D;AAE5D;;;;;;;;;;;AAWgD;;;;;;;;;;;;;;iBAbjC,SAAA,CAAU,MAAA,EAAQ,eAAA,GAAkB,SAAS;;;;;;;;YAWjD,EAAA;;IAER,SAAA,CAAU,MAAA,EAAQ,eAAA,GAAkB,SAAS;EAAA;AAAA"}
|
package/esm/workspace.mjs
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { WorkspacePolicyError } from "./errors.mjs";
|
|
2
|
+
import { ShellJobs } from "./shell-jobs.mjs";
|
|
2
3
|
import { createOps } from "./ops.mjs";
|
|
3
4
|
import { createLocalBackend } from "./backends/local.mjs";
|
|
4
5
|
import { createMockBackend } from "./backends/mock.mjs";
|
|
5
6
|
import { makeEditFileTool } from "./tools/edit-file.mjs";
|
|
6
7
|
import { makeGlobTool } from "./tools/glob.mjs";
|
|
7
8
|
import { makeGrepTool } from "./tools/grep.mjs";
|
|
9
|
+
import { makeListDirTool } from "./tools/list-dir.mjs";
|
|
10
|
+
import { makeMultiEditTool } from "./tools/multi-edit.mjs";
|
|
8
11
|
import { makeReadFileTool } from "./tools/read-file.mjs";
|
|
9
12
|
import { makeRunShellTool } from "./tools/run-shell.mjs";
|
|
10
13
|
import { makeRunTestsTool } from "./tools/run-tests.mjs";
|
|
14
|
+
import { makeShellKillTool } from "./tools/shell-kill.mjs";
|
|
15
|
+
import { makeShellOutputTool } from "./tools/shell-output.mjs";
|
|
16
|
+
import { TodoStore } from "./todos.mjs";
|
|
17
|
+
import { makeTodoReadTool, makeTodoWriteTool } from "./tools/todo.mjs";
|
|
11
18
|
import { makeWriteFileTool } from "./tools/write-file.mjs";
|
|
12
19
|
import { ai } from "@warlock.js/ai";
|
|
13
20
|
import path from "node:path";
|
|
@@ -20,23 +27,35 @@ import path from "node:path";
|
|
|
20
27
|
const ALL_TOOL_NAMES = [
|
|
21
28
|
"readFile",
|
|
22
29
|
"editFile",
|
|
30
|
+
"multiEdit",
|
|
23
31
|
"writeFile",
|
|
32
|
+
"listDir",
|
|
24
33
|
"runShell",
|
|
34
|
+
"shellOutput",
|
|
35
|
+
"shellKill",
|
|
25
36
|
"runTests",
|
|
26
37
|
"grep",
|
|
27
|
-
"glob"
|
|
38
|
+
"glob",
|
|
39
|
+
"todoWrite",
|
|
40
|
+
"todoRead"
|
|
28
41
|
];
|
|
29
42
|
/**
|
|
30
43
|
* The subset a {@link Workspace.readonly} projection exposes — the
|
|
31
|
-
*
|
|
32
|
-
* `runTests` are deliberately omitted so a
|
|
33
|
-
* change
|
|
44
|
+
* tools that cannot change the tree. `editFile` / `multiEdit` /
|
|
45
|
+
* `writeFile` / the shell tools / `runTests` are deliberately omitted so a
|
|
46
|
+
* reviewer agent has no path to change it. The todo tools stay: they only
|
|
47
|
+
* touch the in-memory plan.
|
|
34
48
|
*/
|
|
35
49
|
const READONLY_TOOL_NAMES = [
|
|
36
50
|
"readFile",
|
|
51
|
+
"listDir",
|
|
37
52
|
"grep",
|
|
38
|
-
"glob"
|
|
53
|
+
"glob",
|
|
54
|
+
"todoWrite",
|
|
55
|
+
"todoRead"
|
|
39
56
|
];
|
|
57
|
+
/** Tools vended only when the policy enables background commands. */
|
|
58
|
+
const BACKGROUND_TOOL_NAMES = new Set(["shellOutput", "shellKill"]);
|
|
40
59
|
/**
|
|
41
60
|
* Choose the dumb IO executor for a policy. `"mock"` selects the
|
|
42
61
|
* in-memory backend (hermetic tests); anything else — including the
|
|
@@ -61,11 +80,15 @@ function selectBackend(policy) {
|
|
|
61
80
|
* Constructed via {@link workspace}; the class itself is internal.
|
|
62
81
|
*/
|
|
63
82
|
var WorkspaceImpl = class WorkspaceImpl {
|
|
64
|
-
constructor(policy, allowedTools = ALL_TOOL_NAMES) {
|
|
83
|
+
constructor(policy, allowedTools = ALL_TOOL_NAMES, state) {
|
|
65
84
|
this.policy = policy;
|
|
66
85
|
this.allowedTools = new Set(allowedTools);
|
|
86
|
+
this.state = state ?? {
|
|
87
|
+
jobs: new ShellJobs(),
|
|
88
|
+
todos: new TodoStore()
|
|
89
|
+
};
|
|
67
90
|
const backend = selectBackend(policy);
|
|
68
|
-
this.ops = createOps(backend, policy);
|
|
91
|
+
this.ops = createOps(backend, policy, { jobs: this.state.jobs });
|
|
69
92
|
this.tools = this.buildTools();
|
|
70
93
|
}
|
|
71
94
|
/**
|
|
@@ -77,26 +100,49 @@ var WorkspaceImpl = class WorkspaceImpl {
|
|
|
77
100
|
*/
|
|
78
101
|
buildTools() {
|
|
79
102
|
const erase = (contract) => contract;
|
|
103
|
+
const shell = this.policy.shell;
|
|
104
|
+
const background = shell?.background !== void 0;
|
|
105
|
+
const interpreter = shell?.interpreter ?? "none";
|
|
80
106
|
const factories = {
|
|
81
107
|
readFile: (opts) => erase(makeReadFileTool(this.ops, opts)),
|
|
82
108
|
editFile: (opts) => erase(makeEditFileTool(this.ops, opts)),
|
|
109
|
+
multiEdit: (opts) => erase(makeMultiEditTool(this.ops, opts)),
|
|
83
110
|
writeFile: (opts) => erase(makeWriteFileTool(this.ops, opts)),
|
|
84
|
-
|
|
85
|
-
|
|
111
|
+
listDir: (opts) => erase(makeListDirTool(this.ops, opts)),
|
|
112
|
+
runShell: (opts) => erase(makeRunShellTool(this.ops, {
|
|
113
|
+
...opts?.name !== void 0 ? { name: opts.name } : {},
|
|
114
|
+
...shell?.timeoutMs !== void 0 ? { maxTimeoutMs: shell.timeoutMs } : {},
|
|
115
|
+
background
|
|
116
|
+
})),
|
|
117
|
+
shellOutput: (opts) => erase(makeShellOutputTool(this.ops, opts)),
|
|
118
|
+
shellKill: (opts) => erase(makeShellKillTool(this.ops, opts)),
|
|
119
|
+
runTests: (opts) => erase(makeRunTestsTool(this.ops, {
|
|
120
|
+
...opts,
|
|
121
|
+
interpreter
|
|
122
|
+
})),
|
|
86
123
|
grep: (opts) => erase(makeGrepTool(this.ops, opts)),
|
|
87
|
-
glob: (opts) => erase(makeGlobTool(this.ops, opts))
|
|
124
|
+
glob: (opts) => erase(makeGlobTool(this.ops, opts)),
|
|
125
|
+
todoWrite: (opts) => erase(makeTodoWriteTool(this.state.todos, opts)),
|
|
126
|
+
todoRead: (opts) => erase(makeTodoReadTool(this.state.todos, opts))
|
|
88
127
|
};
|
|
89
128
|
const build = (name, opts) => factories[name](opts);
|
|
129
|
+
const vendedByAll = (name) => this.allowedTools.has(name) && (background || !BACKGROUND_TOOL_NAMES.has(name));
|
|
90
130
|
return {
|
|
91
|
-
all: () => ALL_TOOL_NAMES.filter(
|
|
131
|
+
all: () => ALL_TOOL_NAMES.filter(vendedByAll).map((name) => build(name)),
|
|
92
132
|
pick: (...names) => names.filter((name) => this.allowedTools.has(name)).map((name) => build(name)),
|
|
93
133
|
readFile: (opts) => build("readFile", opts),
|
|
94
134
|
editFile: (opts) => build("editFile", opts),
|
|
135
|
+
multiEdit: (opts) => build("multiEdit", opts),
|
|
95
136
|
writeFile: (opts) => build("writeFile", opts),
|
|
137
|
+
listDir: (opts) => build("listDir", opts),
|
|
96
138
|
runShell: (opts) => build("runShell", opts),
|
|
139
|
+
shellOutput: (opts) => build("shellOutput", opts),
|
|
140
|
+
shellKill: (opts) => build("shellKill", opts),
|
|
97
141
|
runTests: (opts) => build("runTests", opts),
|
|
98
142
|
grep: (opts) => build("grep", opts),
|
|
99
|
-
glob: (opts) => build("glob", opts)
|
|
143
|
+
glob: (opts) => build("glob", opts),
|
|
144
|
+
todoWrite: (opts) => build("todoWrite", opts),
|
|
145
|
+
todoRead: (opts) => build("todoRead", opts)
|
|
100
146
|
};
|
|
101
147
|
}
|
|
102
148
|
/**
|
|
@@ -122,10 +168,40 @@ var WorkspaceImpl = class WorkspaceImpl {
|
|
|
122
168
|
this.assertWritable("editFile");
|
|
123
169
|
return this.ops.editFile(input);
|
|
124
170
|
}
|
|
171
|
+
async multiEdit(input) {
|
|
172
|
+
this.assertWritable("multiEdit");
|
|
173
|
+
return this.ops.multiEdit(input);
|
|
174
|
+
}
|
|
175
|
+
listDir(dirPath, opts) {
|
|
176
|
+
return this.ops.listDir(dirPath, opts);
|
|
177
|
+
}
|
|
125
178
|
async exec(command, opts) {
|
|
126
179
|
this.assertWritable("exec");
|
|
127
180
|
return this.ops.exec(command, opts);
|
|
128
181
|
}
|
|
182
|
+
async startShell(command, opts) {
|
|
183
|
+
this.assertWritable("startShell");
|
|
184
|
+
return this.ops.startShell(command, opts);
|
|
185
|
+
}
|
|
186
|
+
shellOutput(id, opts) {
|
|
187
|
+
return this.ops.shellOutput(id, opts);
|
|
188
|
+
}
|
|
189
|
+
async shellKill(id) {
|
|
190
|
+
this.assertWritable("shellKill");
|
|
191
|
+
return this.ops.shellKill(id);
|
|
192
|
+
}
|
|
193
|
+
listShells() {
|
|
194
|
+
return this.ops.listShells();
|
|
195
|
+
}
|
|
196
|
+
readTodos() {
|
|
197
|
+
return this.state.todos.read();
|
|
198
|
+
}
|
|
199
|
+
writeTodos(todos) {
|
|
200
|
+
return this.state.todos.write(todos);
|
|
201
|
+
}
|
|
202
|
+
dispose() {
|
|
203
|
+
return this.ops.dispose();
|
|
204
|
+
}
|
|
129
205
|
grep(pattern, opts) {
|
|
130
206
|
return this.ops.grep(pattern, opts);
|
|
131
207
|
}
|
|
@@ -144,44 +220,49 @@ var WorkspaceImpl = class WorkspaceImpl {
|
|
|
144
220
|
return this.ops.remove(filePath);
|
|
145
221
|
}
|
|
146
222
|
/**
|
|
147
|
-
* A read-only projection over the SAME policy — only the read/
|
|
148
|
-
* tools are vended and every mutating direct method
|
|
149
|
-
* {@link WorkspacePolicyError}. A fresh ops/backend is
|
|
150
|
-
* identical policy, so the projection sees the same jailed
|
|
223
|
+
* A read-only projection over the SAME policy — only the read / list /
|
|
224
|
+
* grep / glob / todo tools are vended and every mutating direct method
|
|
225
|
+
* rejects with a {@link WorkspacePolicyError}. A fresh ops/backend is
|
|
226
|
+
* built from the identical policy, so the projection sees the same jailed
|
|
227
|
+
* tree; it shares this workspace's background commands and todo list.
|
|
151
228
|
*/
|
|
152
229
|
readonly() {
|
|
153
|
-
return new WorkspaceImpl(this.policy, READONLY_TOOL_NAMES);
|
|
230
|
+
return new WorkspaceImpl(this.policy, READONLY_TOOL_NAMES.filter((name) => this.allowedTools.has(name)), this.state);
|
|
154
231
|
}
|
|
155
232
|
/**
|
|
156
233
|
* A sub-jailed view rooted at `subdir` (relative to this workspace's
|
|
157
234
|
* `cwd`). Returns a brand-new workspace whose policy is this policy
|
|
158
235
|
* with `cwd` narrowed to `join(cwd, subdir)` — same backend selection,
|
|
159
|
-
* same allow/deny/shell/read sub-policies, but a tighter jail root.
|
|
236
|
+
* same allow/deny/shell/read sub-policies, but a tighter jail root. It
|
|
237
|
+
* shares this workspace's background commands and todo list.
|
|
160
238
|
*/
|
|
161
239
|
scope(subdir) {
|
|
162
240
|
return new WorkspaceImpl({
|
|
163
241
|
...this.policy,
|
|
164
242
|
cwd: path.join(this.policy.cwd, subdir)
|
|
165
|
-
}, [...this.allowedTools]);
|
|
243
|
+
}, [...this.allowedTools], this.state);
|
|
166
244
|
}
|
|
167
245
|
};
|
|
168
246
|
/**
|
|
169
247
|
* Build a {@link Workspace} — the integrator that wires a
|
|
170
248
|
* {@link WorkspacePolicy} to a backend, the shared policy-enforced ops
|
|
171
|
-
* layer, and the
|
|
249
|
+
* layer, and the agent-facing tool factories.
|
|
172
250
|
*
|
|
173
251
|
* The backend is chosen from `policy.backend`: `"mock"` runs in memory
|
|
174
252
|
* (hermetic tests); the `"local"` default (and any absent value) runs
|
|
175
253
|
* over the real disk via `@warlock.js/fs` + `node:child_process`. The
|
|
176
254
|
* returned workspace exposes:
|
|
177
255
|
*
|
|
178
|
-
* - **`tools.*`** — `readFile` / `editFile` / `
|
|
179
|
-
* `
|
|
180
|
-
* `
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
256
|
+
* - **`tools.*`** — `readFile` / `editFile` / `multiEdit` / `writeFile` /
|
|
257
|
+
* `listDir` / `runShell` / `shellOutput` / `shellKill` / `runTests` /
|
|
258
|
+
* `grep` / `glob` / `todoWrite` / `todoRead`, plus `all()` (every tool)
|
|
259
|
+
* and `pick(...)` (a least-privilege subset).
|
|
260
|
+
* - **direct methods** — `readFile` / `writeFile` / `editFile` /
|
|
261
|
+
* `multiEdit` / `listDir` / `exec` / `startShell` / `shellOutput` /
|
|
262
|
+
* `shellKill` / `listShells` / `grep` / `glob` / `exists` / `mkdir` /
|
|
263
|
+
* `remove` / `readTodos` / `writeTodos`, each delegating to the shared
|
|
264
|
+
* ops layer, and `dispose()`.
|
|
265
|
+
* - **`readonly()`** — a projection that vends only the read-side tools and
|
|
185
266
|
* rejects every mutating direct method.
|
|
186
267
|
* - **`scope(subdir)`** — a sub-jailed workspace rooted at `subdir`.
|
|
187
268
|
*
|
package/esm/workspace.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace.mjs","names":[],"sources":["../../../../../../../packages/ai-workspace/src/workspace.ts"],"sourcesContent":["import path from \"node:path\";\nimport { ai, type ToolContract } from \"@warlock.js/ai\";\nimport { createLocalBackend } from \"./backends/local\";\nimport { createMockBackend } from \"./backends/mock\";\nimport { WorkspacePolicyError } from \"./errors\";\nimport { createOps } from \"./ops\";\nimport { makeEditFileTool } from \"./tools/edit-file\";\nimport { makeGlobTool } from \"./tools/glob\";\nimport { makeGrepTool } from \"./tools/grep\";\nimport { makeReadFileTool } from \"./tools/read-file\";\nimport { makeRunShellTool } from \"./tools/run-shell\";\nimport { makeRunTestsTool } from \"./tools/run-tests\";\nimport { makeWriteFileTool } from \"./tools/write-file\";\nimport type {\n EditFileInput,\n EditFileResult,\n ExecOptions,\n GrepResult,\n ReadFileResult,\n RunShellResult,\n Workspace,\n WorkspaceBackend,\n WorkspaceOps,\n WorkspacePolicy,\n WorkspaceToolName,\n WorkspaceTools,\n} from \"./contracts\";\n\n/**\n * The full set of tool names a writable workspace vends, in a stable\n * canonical order so `tools.all()` is deterministic.\n */\nconst ALL_TOOL_NAMES: readonly WorkspaceToolName[] = [\n \"readFile\",\n \"editFile\",\n \"writeFile\",\n \"runShell\",\n \"runTests\",\n \"grep\",\n \"glob\",\n];\n\n/**\n * The subset a {@link Workspace.readonly} projection exposes — the\n * non-mutating tools only. `editFile` / `writeFile` / `runShell` /\n * `runTests` are deliberately omitted so a reviewer agent has no path to\n * change the tree.\n */\nconst READONLY_TOOL_NAMES: readonly WorkspaceToolName[] = [\n \"readFile\",\n \"grep\",\n \"glob\",\n];\n\n/**\n * Choose the dumb IO executor for a policy. `\"mock\"` selects the\n * in-memory backend (hermetic tests); anything else — including the\n * `\"local\"` default and an absent `backend` — selects the real-disk\n * local backend.\n */\nfunction selectBackend(policy: WorkspacePolicy): WorkspaceBackend {\n if (policy.backend === \"mock\") {\n return createMockBackend();\n }\n\n return createLocalBackend();\n}\n\n/**\n * The internal {@link Workspace} implementation. Holds the resolved\n * backend, the policy, and the single shared {@link WorkspaceOps} seam\n * that both the agent-facing `.tools.*` factories and the human-facing\n * direct methods funnel through — one jail, one rule set, two callers.\n *\n * The `allowedTools` set narrows what `tools.*` will vend and which\n * mutating direct methods are permitted: a full workspace allows every\n * name; a {@link WorkspaceImpl.readonly} projection allows only the\n * read/grep/glob subset and rejects writes/edits/shell/mkdir/remove.\n *\n * Constructed via {@link workspace}; the class itself is internal.\n */\nclass WorkspaceImpl implements Workspace {\n /** The shared, policy-enforced operation layer (jail + guards). */\n private readonly ops: WorkspaceOps;\n\n /** Tool names this projection is permitted to vend / mutate through. */\n private readonly allowedTools: ReadonlySet<WorkspaceToolName>;\n\n public readonly policy: WorkspacePolicy;\n\n public readonly tools: WorkspaceTools;\n\n public constructor(\n policy: WorkspacePolicy,\n allowedTools: readonly WorkspaceToolName[] = ALL_TOOL_NAMES,\n ) {\n this.policy = policy;\n this.allowedTools = new Set(allowedTools);\n\n const backend = selectBackend(policy);\n this.ops = createOps(backend, policy);\n this.tools = this.buildTools();\n }\n\n /**\n * Assemble the agent-facing tool namespace. Each factory builds its\n * tool over the shared `ops`; `all()` returns every *allowed* tool in\n * canonical order and `pick(...)` returns the named subset (silently\n * dropping any name this projection does not allow, so a `readonly()`\n * workspace can never be coaxed into vending a mutating tool).\n */\n private buildTools(): WorkspaceTools {\n // Each `make*Tool` returns a precisely-typed\n // `ToolContract<SpecificInput, SpecificOutput>`, but the agent-facing\n // `WorkspaceTools` surface vends the type-erased `ToolContract`\n // (`ToolContract<unknown, unknown>`). Because `ToolContract` puts its\n // input in a contravariant position (`execute(input)` / `action(input)`),\n // a specific contract is not assignable to the erased one — so erase it\n // once, here, through `unknown`. The runtime object is identical; only\n // the static input type is widened for the shared surface.\n const erase = <TInput, TOutput>(\n contract: ToolContract<TInput, TOutput>,\n ): ToolContract => contract as unknown as ToolContract;\n\n const factories: Record<\n WorkspaceToolName,\n (opts?: { name?: string; command?: string }) => ToolContract\n > = {\n readFile: (opts) => erase(makeReadFileTool(this.ops, opts)),\n editFile: (opts) => erase(makeEditFileTool(this.ops, opts)),\n writeFile: (opts) => erase(makeWriteFileTool(this.ops, opts)),\n runShell: (opts) => erase(makeRunShellTool(this.ops, opts)),\n runTests: (opts) => erase(makeRunTestsTool(this.ops, opts)),\n grep: (opts) => erase(makeGrepTool(this.ops, opts)),\n glob: (opts) => erase(makeGlobTool(this.ops, opts)),\n };\n\n const build = (name: WorkspaceToolName, opts?: { name?: string; command?: string }) =>\n factories[name](opts);\n\n return {\n all: () =>\n ALL_TOOL_NAMES.filter((name) => this.allowedTools.has(name)).map((name) =>\n build(name),\n ),\n pick: (...names: WorkspaceToolName[]) =>\n names.filter((name) => this.allowedTools.has(name)).map((name) => build(name)),\n readFile: (opts) => build(\"readFile\", opts),\n editFile: (opts) => build(\"editFile\", opts),\n writeFile: (opts) => build(\"writeFile\", opts),\n runShell: (opts) => build(\"runShell\", opts),\n runTests: (opts) => build(\"runTests\", opts),\n grep: (opts) => build(\"grep\", opts),\n glob: (opts) => build(\"glob\", opts),\n };\n }\n\n /**\n * Reject a mutating direct method on a read-only projection — surfaced\n * as a {@link WorkspacePolicyError} (the same typed error a denied\n * command produces) so a caller branches on `error.type`.\n */\n private assertWritable(operation: string): void {\n if (this.allowedTools.has(\"writeFile\")) {\n return;\n }\n\n throw new WorkspacePolicyError(\n `Operation \"${operation}\" is not permitted on a read-only workspace.`,\n { type: \"denied-command\", command: operation },\n );\n }\n\n public readFile(\n filePath: string,\n opts?: { offset?: number; limit?: number },\n ): Promise<ReadFileResult> {\n return this.ops.readFile(filePath, opts);\n }\n\n public async writeFile(\n filePath: string,\n content: string,\n ): Promise<{ hash: string; bytesWritten: number }> {\n this.assertWritable(\"writeFile\");\n\n return this.ops.writeFile(filePath, content);\n }\n\n public async editFile(input: EditFileInput): Promise<EditFileResult> {\n this.assertWritable(\"editFile\");\n\n return this.ops.editFile(input);\n }\n\n public async exec(command: string, opts?: ExecOptions): Promise<RunShellResult> {\n this.assertWritable(\"exec\");\n\n return this.ops.exec(command, opts);\n }\n\n public grep(\n pattern: string,\n opts?: { glob?: string; ignoreCase?: boolean },\n ): Promise<GrepResult> {\n return this.ops.grep(pattern, opts);\n }\n\n public glob(pattern: string): Promise<string[]> {\n return this.ops.glob(pattern);\n }\n\n public exists(filePath: string): Promise<boolean> {\n return this.ops.exists(filePath);\n }\n\n public async mkdir(filePath: string): Promise<void> {\n this.assertWritable(\"mkdir\");\n\n return this.ops.mkdir(filePath);\n }\n\n public async remove(filePath: string): Promise<void> {\n this.assertWritable(\"remove\");\n\n return this.ops.remove(filePath);\n }\n\n /**\n * A read-only projection over the SAME policy — only the read/grep/glob\n * tools are vended and every mutating direct method rejects with a\n * {@link WorkspacePolicyError}. A fresh ops/backend is built from the\n * identical policy, so the projection sees the same jailed tree.\n */\n public readonly(): Workspace {\n return new WorkspaceImpl(this.policy, READONLY_TOOL_NAMES);\n }\n\n /**\n * A sub-jailed view rooted at `subdir` (relative to this workspace's\n * `cwd`). Returns a brand-new workspace whose policy is this policy\n * with `cwd` narrowed to `join(cwd, subdir)` — same backend selection,\n * same allow/deny/shell/read sub-policies, but a tighter jail root.\n */\n public scope(subdir: string): Workspace {\n return new WorkspaceImpl(\n { ...this.policy, cwd: path.join(this.policy.cwd, subdir) },\n [...this.allowedTools],\n );\n }\n}\n\n/**\n * Build a {@link Workspace} — the integrator that wires a\n * {@link WorkspacePolicy} to a backend, the shared policy-enforced ops\n * layer, and the seven agent-facing tool factories.\n *\n * The backend is chosen from `policy.backend`: `\"mock\"` runs in memory\n * (hermetic tests); the `\"local\"` default (and any absent value) runs\n * over the real disk via `@warlock.js/fs` + `node:child_process`. The\n * returned workspace exposes:\n *\n * - **`tools.*`** — `readFile` / `editFile` / `writeFile` / `runShell` /\n * `runTests` / `grep` / `glob`, plus `all()` (every tool) and\n * `pick(...)` (a least-privilege subset).\n * - **direct methods** — `readFile` / `writeFile` / `editFile` / `exec` /\n * `grep` / `glob` / `exists` / `mkdir` / `remove`, each delegating 1:1\n * to the shared ops layer.\n * - **`readonly()`** — a projection that vends only read/grep/glob and\n * rejects every mutating direct method.\n * - **`scope(subdir)`** — a sub-jailed workspace rooted at `subdir`.\n *\n * Available at runtime as `ai.workspace(policy)` once this module is\n * imported (it registers the verb on the shared `ai` object).\n *\n * @param policy - The policy bounding the workspace (its `cwd` is the jail root).\n * @returns A fully-wired {@link Workspace}.\n *\n * @example\n * const ws = workspace({ cwd: \"/srv/acme-api\", shell: { allow: [\"npm\"], inheritEnv: [\"PATH\"] } });\n * const dev = ai.agent({ model, tools: ws.tools.all() });\n * await dev.execute(\"Make the failing cart-total suite green.\");\n *\n * @example\n * // Least-privilege reviewer — no write, no shell.\n * const reviewer = ai.agent({ model, tools: ws.readonly().tools.all() });\n */\nexport function workspace(policy: WorkspacePolicy): Workspace {\n return new WorkspaceImpl(policy);\n}\n\n/**\n * Attach the `workspace` verb to the `ai` namespace via module augmentation,\n * per the `ai.`-namespace convention. `@warlock.js/ai` now exposes a named `Ai`\n * interface for exactly this, so after a bare `import \"@warlock.js/ai-workspace\"`,\n * `ai.workspace(...)` is globally typed — no view/cast needed.\n */\ndeclare module \"@warlock.js/ai\" {\n interface Ai {\n /** Build a policy-jailed filesystem + shell {@link Workspace}. */\n workspace(policy: WorkspacePolicy): Workspace;\n }\n}\n\n// Runtime registration: attach `workspace` onto the shared `ai` object the\n// moment this package is imported (the augmentation above types it).\nai.workspace = workspace;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"workspace.mjs","names":[],"sources":["../../../../../../../packages/ai-workspace/src/workspace.ts"],"sourcesContent":["import path from \"node:path\";\nimport { ai, type ToolContract } from \"@warlock.js/ai\";\nimport { createLocalBackend } from \"./backends/local\";\nimport { createMockBackend } from \"./backends/mock\";\nimport { WorkspacePolicyError } from \"./errors\";\nimport { createOps } from \"./ops\";\nimport { ShellJobs } from \"./shell-jobs\";\nimport { TodoStore } from \"./todos\";\nimport { makeEditFileTool } from \"./tools/edit-file\";\nimport { makeGlobTool } from \"./tools/glob\";\nimport { makeGrepTool } from \"./tools/grep\";\nimport { makeListDirTool } from \"./tools/list-dir\";\nimport { makeMultiEditTool } from \"./tools/multi-edit\";\nimport { makeReadFileTool } from \"./tools/read-file\";\nimport { makeRunShellTool } from \"./tools/run-shell\";\nimport { makeRunTestsTool } from \"./tools/run-tests\";\nimport { makeShellKillTool } from \"./tools/shell-kill\";\nimport { makeShellOutputTool } from \"./tools/shell-output\";\nimport { makeTodoReadTool, makeTodoWriteTool } from \"./tools/todo\";\nimport { makeWriteFileTool } from \"./tools/write-file\";\nimport type {\n EditFileInput,\n EditFileResult,\n ExecOptions,\n GrepResult,\n ListDirOptions,\n ListDirResult,\n MultiEditInput,\n MultiEditResult,\n ReadFileResult,\n RunShellResult,\n ShellJobInfo,\n ShellOutputOptions,\n ShellOutputResult,\n StartShellOptions,\n TodoListResult,\n Workspace,\n WorkspaceBackend,\n WorkspaceOps,\n WorkspacePolicy,\n WorkspaceTodoInput,\n WorkspaceToolName,\n WorkspaceTools,\n} from \"./contracts\";\n\n/**\n * The full set of tool names a writable workspace vends, in a stable\n * canonical order so `tools.all()` is deterministic.\n */\nconst ALL_TOOL_NAMES: readonly WorkspaceToolName[] = [\n \"readFile\",\n \"editFile\",\n \"multiEdit\",\n \"writeFile\",\n \"listDir\",\n \"runShell\",\n \"shellOutput\",\n \"shellKill\",\n \"runTests\",\n \"grep\",\n \"glob\",\n \"todoWrite\",\n \"todoRead\",\n];\n\n/**\n * The subset a {@link Workspace.readonly} projection exposes — the\n * tools that cannot change the tree. `editFile` / `multiEdit` /\n * `writeFile` / the shell tools / `runTests` are deliberately omitted so a\n * reviewer agent has no path to change it. The todo tools stay: they only\n * touch the in-memory plan.\n */\nconst READONLY_TOOL_NAMES: readonly WorkspaceToolName[] = [\n \"readFile\",\n \"listDir\",\n \"grep\",\n \"glob\",\n \"todoWrite\",\n \"todoRead\",\n];\n\n/** Tools vended only when the policy enables background commands. */\nconst BACKGROUND_TOOL_NAMES: ReadonlySet<WorkspaceToolName> = new Set([\"shellOutput\", \"shellKill\"]);\n\n/**\n * State one workspace shares with every projection made from it: its\n * background commands and its todo list.\n */\ninterface WorkspaceState {\n jobs: ShellJobs;\n todos: TodoStore;\n}\n\n/**\n * Choose the dumb IO executor for a policy. `\"mock\"` selects the\n * in-memory backend (hermetic tests); anything else — including the\n * `\"local\"` default and an absent `backend` — selects the real-disk\n * local backend.\n */\nfunction selectBackend(policy: WorkspacePolicy): WorkspaceBackend {\n if (policy.backend === \"mock\") {\n return createMockBackend();\n }\n\n return createLocalBackend();\n}\n\n/**\n * The internal {@link Workspace} implementation. Holds the resolved\n * backend, the policy, and the single shared {@link WorkspaceOps} seam\n * that both the agent-facing `.tools.*` factories and the human-facing\n * direct methods funnel through — one jail, one rule set, two callers.\n *\n * The `allowedTools` set narrows what `tools.*` will vend and which\n * mutating direct methods are permitted: a full workspace allows every\n * name; a {@link WorkspaceImpl.readonly} projection allows only the\n * read/grep/glob subset and rejects writes/edits/shell/mkdir/remove.\n *\n * Constructed via {@link workspace}; the class itself is internal.\n */\nclass WorkspaceImpl implements Workspace {\n /** The shared, policy-enforced operation layer (jail + guards). */\n private readonly ops: WorkspaceOps;\n\n /** Tool names this projection is permitted to vend / mutate through. */\n private readonly allowedTools: ReadonlySet<WorkspaceToolName>;\n\n public readonly policy: WorkspacePolicy;\n\n public readonly tools: WorkspaceTools;\n\n /** Background commands + todo list, shared with projections. */\n private readonly state: WorkspaceState;\n\n public constructor(\n policy: WorkspacePolicy,\n allowedTools: readonly WorkspaceToolName[] = ALL_TOOL_NAMES,\n state?: WorkspaceState,\n ) {\n this.policy = policy;\n this.allowedTools = new Set(allowedTools);\n this.state = state ?? { jobs: new ShellJobs(), todos: new TodoStore() };\n\n const backend = selectBackend(policy);\n this.ops = createOps(backend, policy, { jobs: this.state.jobs });\n this.tools = this.buildTools();\n }\n\n /**\n * Assemble the agent-facing tool namespace. Each factory builds its\n * tool over the shared `ops`; `all()` returns every *allowed* tool in\n * canonical order and `pick(...)` returns the named subset (silently\n * dropping any name this projection does not allow, so a `readonly()`\n * workspace can never be coaxed into vending a mutating tool).\n */\n private buildTools(): WorkspaceTools {\n // Each `make*Tool` returns a precisely-typed\n // `ToolContract<SpecificInput, SpecificOutput>`, but the agent-facing\n // `WorkspaceTools` surface vends the type-erased `ToolContract`\n // (`ToolContract<unknown, unknown>`). Because `ToolContract` puts its\n // input in a contravariant position (`execute(input)` / `action(input)`),\n // a specific contract is not assignable to the erased one — so erase it\n // once, here, through `unknown`. The runtime object is identical; only\n // the static input type is widened for the shared surface.\n const erase = <TInput, TOutput>(contract: ToolContract<TInput, TOutput>): ToolContract =>\n contract as unknown as ToolContract;\n\n const shell = this.policy.shell;\n const background = shell?.background !== undefined;\n const interpreter = shell?.interpreter ?? \"none\";\n\n const factories: Record<\n WorkspaceToolName,\n (opts?: { name?: string; command?: string }) => ToolContract\n > = {\n readFile: (opts) => erase(makeReadFileTool(this.ops, opts)),\n editFile: (opts) => erase(makeEditFileTool(this.ops, opts)),\n multiEdit: (opts) => erase(makeMultiEditTool(this.ops, opts)),\n writeFile: (opts) => erase(makeWriteFileTool(this.ops, opts)),\n listDir: (opts) => erase(makeListDirTool(this.ops, opts)),\n runShell: (opts) =>\n erase(\n makeRunShellTool(this.ops, {\n ...(opts?.name !== undefined ? { name: opts.name } : {}),\n ...(shell?.timeoutMs !== undefined ? { maxTimeoutMs: shell.timeoutMs } : {}),\n background,\n }),\n ),\n shellOutput: (opts) => erase(makeShellOutputTool(this.ops, opts)),\n shellKill: (opts) => erase(makeShellKillTool(this.ops, opts)),\n runTests: (opts) => erase(makeRunTestsTool(this.ops, { ...opts, interpreter })),\n grep: (opts) => erase(makeGrepTool(this.ops, opts)),\n glob: (opts) => erase(makeGlobTool(this.ops, opts)),\n todoWrite: (opts) => erase(makeTodoWriteTool(this.state.todos, opts)),\n todoRead: (opts) => erase(makeTodoReadTool(this.state.todos, opts)),\n };\n\n const build = (name: WorkspaceToolName, opts?: { name?: string; command?: string }) =>\n factories[name](opts);\n\n // `shell_output` / `shell_kill` only mean something when background\n // commands can be started, so `all()` leaves them out otherwise.\n const vendedByAll = (name: WorkspaceToolName): boolean =>\n this.allowedTools.has(name) && (background || !BACKGROUND_TOOL_NAMES.has(name));\n\n return {\n all: () => ALL_TOOL_NAMES.filter(vendedByAll).map((name) => build(name)),\n pick: (...names: WorkspaceToolName[]) =>\n names.filter((name) => this.allowedTools.has(name)).map((name) => build(name)),\n readFile: (opts) => build(\"readFile\", opts),\n editFile: (opts) => build(\"editFile\", opts),\n multiEdit: (opts) => build(\"multiEdit\", opts),\n writeFile: (opts) => build(\"writeFile\", opts),\n listDir: (opts) => build(\"listDir\", opts),\n runShell: (opts) => build(\"runShell\", opts),\n shellOutput: (opts) => build(\"shellOutput\", opts),\n shellKill: (opts) => build(\"shellKill\", opts),\n runTests: (opts) => build(\"runTests\", opts),\n grep: (opts) => build(\"grep\", opts),\n glob: (opts) => build(\"glob\", opts),\n todoWrite: (opts) => build(\"todoWrite\", opts),\n todoRead: (opts) => build(\"todoRead\", opts),\n };\n }\n\n /**\n * Reject a mutating direct method on a read-only projection — surfaced\n * as a {@link WorkspacePolicyError} (the same typed error a denied\n * command produces) so a caller branches on `error.type`.\n */\n private assertWritable(operation: string): void {\n if (this.allowedTools.has(\"writeFile\")) {\n return;\n }\n\n throw new WorkspacePolicyError(\n `Operation \"${operation}\" is not permitted on a read-only workspace.`,\n { type: \"denied-command\", command: operation },\n );\n }\n\n public readFile(\n filePath: string,\n opts?: { offset?: number; limit?: number },\n ): Promise<ReadFileResult> {\n return this.ops.readFile(filePath, opts);\n }\n\n public async writeFile(\n filePath: string,\n content: string,\n ): Promise<{ hash: string; bytesWritten: number }> {\n this.assertWritable(\"writeFile\");\n\n return this.ops.writeFile(filePath, content);\n }\n\n public async editFile(input: EditFileInput): Promise<EditFileResult> {\n this.assertWritable(\"editFile\");\n\n return this.ops.editFile(input);\n }\n\n public async multiEdit(input: MultiEditInput): Promise<MultiEditResult> {\n this.assertWritable(\"multiEdit\");\n\n return this.ops.multiEdit(input);\n }\n\n public listDir(dirPath?: string, opts?: ListDirOptions): Promise<ListDirResult> {\n return this.ops.listDir(dirPath, opts);\n }\n\n public async exec(command: string, opts?: ExecOptions): Promise<RunShellResult> {\n this.assertWritable(\"exec\");\n\n return this.ops.exec(command, opts);\n }\n\n public async startShell(command: string, opts?: StartShellOptions): Promise<ShellJobInfo> {\n this.assertWritable(\"startShell\");\n\n return this.ops.startShell(command, opts);\n }\n\n public shellOutput(id: string, opts?: ShellOutputOptions): Promise<ShellOutputResult> {\n return this.ops.shellOutput(id, opts);\n }\n\n public async shellKill(id: string): Promise<ShellJobInfo> {\n this.assertWritable(\"shellKill\");\n\n return this.ops.shellKill(id);\n }\n\n public listShells(): ShellJobInfo[] {\n return this.ops.listShells();\n }\n\n public readTodos(): TodoListResult {\n return this.state.todos.read();\n }\n\n public writeTodos(todos: WorkspaceTodoInput[]): TodoListResult {\n return this.state.todos.write(todos);\n }\n\n public dispose(): Promise<void> {\n return this.ops.dispose();\n }\n\n public grep(\n pattern: string,\n opts?: { glob?: string; ignoreCase?: boolean },\n ): Promise<GrepResult> {\n return this.ops.grep(pattern, opts);\n }\n\n public glob(pattern: string): Promise<string[]> {\n return this.ops.glob(pattern);\n }\n\n public exists(filePath: string): Promise<boolean> {\n return this.ops.exists(filePath);\n }\n\n public async mkdir(filePath: string): Promise<void> {\n this.assertWritable(\"mkdir\");\n\n return this.ops.mkdir(filePath);\n }\n\n public async remove(filePath: string): Promise<void> {\n this.assertWritable(\"remove\");\n\n return this.ops.remove(filePath);\n }\n\n /**\n * A read-only projection over the SAME policy — only the read / list /\n * grep / glob / todo tools are vended and every mutating direct method\n * rejects with a {@link WorkspacePolicyError}. A fresh ops/backend is\n * built from the identical policy, so the projection sees the same jailed\n * tree; it shares this workspace's background commands and todo list.\n */\n public readonly(): Workspace {\n return new WorkspaceImpl(\n this.policy,\n READONLY_TOOL_NAMES.filter((name) => this.allowedTools.has(name)),\n this.state,\n );\n }\n\n /**\n * A sub-jailed view rooted at `subdir` (relative to this workspace's\n * `cwd`). Returns a brand-new workspace whose policy is this policy\n * with `cwd` narrowed to `join(cwd, subdir)` — same backend selection,\n * same allow/deny/shell/read sub-policies, but a tighter jail root. It\n * shares this workspace's background commands and todo list.\n */\n public scope(subdir: string): Workspace {\n return new WorkspaceImpl(\n { ...this.policy, cwd: path.join(this.policy.cwd, subdir) },\n [...this.allowedTools],\n this.state,\n );\n }\n}\n\n/**\n * Build a {@link Workspace} — the integrator that wires a\n * {@link WorkspacePolicy} to a backend, the shared policy-enforced ops\n * layer, and the agent-facing tool factories.\n *\n * The backend is chosen from `policy.backend`: `\"mock\"` runs in memory\n * (hermetic tests); the `\"local\"` default (and any absent value) runs\n * over the real disk via `@warlock.js/fs` + `node:child_process`. The\n * returned workspace exposes:\n *\n * - **`tools.*`** — `readFile` / `editFile` / `multiEdit` / `writeFile` /\n * `listDir` / `runShell` / `shellOutput` / `shellKill` / `runTests` /\n * `grep` / `glob` / `todoWrite` / `todoRead`, plus `all()` (every tool)\n * and `pick(...)` (a least-privilege subset).\n * - **direct methods** — `readFile` / `writeFile` / `editFile` /\n * `multiEdit` / `listDir` / `exec` / `startShell` / `shellOutput` /\n * `shellKill` / `listShells` / `grep` / `glob` / `exists` / `mkdir` /\n * `remove` / `readTodos` / `writeTodos`, each delegating to the shared\n * ops layer, and `dispose()`.\n * - **`readonly()`** — a projection that vends only the read-side tools and\n * rejects every mutating direct method.\n * - **`scope(subdir)`** — a sub-jailed workspace rooted at `subdir`.\n *\n * Available at runtime as `ai.workspace(policy)` once this module is\n * imported (it registers the verb on the shared `ai` object).\n *\n * @param policy - The policy bounding the workspace (its `cwd` is the jail root).\n * @returns A fully-wired {@link Workspace}.\n *\n * @example\n * const ws = workspace({ cwd: \"/srv/acme-api\", shell: { allow: [\"npm\"], inheritEnv: [\"PATH\"] } });\n * const dev = ai.agent({ model, tools: ws.tools.all() });\n * await dev.execute(\"Make the failing cart-total suite green.\");\n *\n * @example\n * // Least-privilege reviewer — no write, no shell.\n * const reviewer = ai.agent({ model, tools: ws.readonly().tools.all() });\n */\nexport function workspace(policy: WorkspacePolicy): Workspace {\n return new WorkspaceImpl(policy);\n}\n\n/**\n * Attach the `workspace` verb to the `ai` namespace via module augmentation,\n * per the `ai.`-namespace convention. `@warlock.js/ai` now exposes a named `Ai`\n * interface for exactly this, so after a bare `import \"@warlock.js/ai-workspace\"`,\n * `ai.workspace(...)` is globally typed — no view/cast needed.\n */\ndeclare module \"@warlock.js/ai\" {\n interface Ai {\n /** Build a policy-jailed filesystem + shell {@link Workspace}. */\n workspace(policy: WorkspacePolicy): Workspace;\n }\n}\n\n// Runtime registration: attach `workspace` onto the shared `ai` object the\n// moment this package is imported (the augmentation above types it).\nai.workspace = workspace;\n"],"mappings":"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"}
|