@warlock.js/ai-workspace
v4.8.2
Published
Policy-jailed filesystem + shell workspace for @warlock.js/ai agents
Maintainers
Readme
@warlock.js/ai-workspace
Policy-jailed filesystem + shell workspace for
@warlock.js/aiagents.
A workspace is the bounded place an agent reads, writes, and runs commands — a
policy-enforced handle to a filesystem + shell that (a) vends tools to agents and
(b) you can drive directly from code. One policy, one shared rule set, two ways to
touch it: the agent uses ws.tools.*; you use the direct methods (ws.readFile,
ws.exec, …). Importing the package registers the verb on the shared ai object,
so it is reached as ai.workspace(policy) per the ai.-namespace convention.
Security: a policy jail, not OS isolation
The workspace is a policy jail, and it is honest about that — it is not an
OS sandbox. Every path is realpath-resolved and must sit under cwd (or an
allowPaths root); denyPaths globs are blocked even inside cwd; shell commands
are gated by an executable allow/deny list; and process.env is never inherited
wholesale (a command can't even find node/npm unless the policy opts in via
shell.inheritEnv: ["PATH"]). What it does not do is contain what an allowed
process does once it runs — an allowed node can still open a socket or read a file
its own user can reach. For untrusted code you want real isolation (a container /
microVM backend), which is a later phase. Treat this as least-privilege guardrails
for a trusted agent, not a security boundary around a hostile one.
Install
npm install @warlock.js/ai-workspaceIt declares @warlock.js/ai as a peer and depends on @warlock.js/fs; beyond those
it uses only Node built-ins (node:child_process for the shell). Import the package
once for its side effect so ai.workspace(...) is registered:
import "@warlock.js/ai-workspace";Usage
1. Minimal build-loop agent
Hand the whole tool set to an agent and let it read, edit, and run tests until green. The failure comes back as tool data, so the agent self-corrects.
import "@warlock.js/ai-workspace";
import { ai } from "@warlock.js/ai";
const ws = ai.workspace({
cwd: "/srv/acme-api",
denyPaths: [".git/**", ".env*"],
shell: { allow: ["npm", "node"], inheritEnv: ["PATH"], timeoutMs: 60_000 },
});
const dev = ai.agent({
model: ai.openai.model({ name: "gpt-4o" }),
tools: ws.tools.all(),
maxTrips: 25,
});
await dev.execute("`npm test` fails on the cart-total suite. Make it green.");2. Read-only reviewer (least privilege via tools.pick)
A reviewer needs to read and search — never to write or run a shell. Pick exactly
those three tools, or take a readonly() projection (which omits every mutating tool
and rejects the mutating direct methods).
const ws = ai.workspace({ cwd: "/srv/acme-api" });
const reviewer = ai.agent({
model: ai.openai.model({ name: "gpt-4o" }),
tools: ws.tools.pick("readFile", "grep", "glob"),
});
await reviewer.execute("Review src/cart for off-by-one bugs and summarize.");
// Equivalent, as a projection of the same jail:
const ro = ws.readonly();
ai.agent({ model, tools: ro.tools.all() }); // read_file + grep + glob only3. Direct programmatic use (no LLM)
Drive the workspace from code — scaffold before an agent runs, or do a mechanical bulk migration under the read-before-edit stale-hash guard.
const ws = ai.workspace({
cwd: "/srv/acme-api",
shell: { allow: ["npm"], inheritEnv: ["PATH"] },
});
// Seed structure, install a dep, then hand off to an agent.
await ws.writeFile("src/routes.ts", ROUTES_BOILERPLATE);
await ws.exec("npm install zod");
// Bulk migration — no LLM needed.
for (const file of await ws.glob("src/models/**/*.ts")) {
const { content, hash } = await ws.readFile(file);
if (content.includes("legacyImport")) {
await ws.editFile({
path: file,
oldString: 'import { legacyImport } from "../legacy";',
newString: 'import { modernImport } from "../modern";',
replaceAll: true,
expectHash: hash, // rejected if the file moved underneath us
});
}
}What's in this release (W1)
- Policy jail —
cwdpath jail (realpath-resolved),allowPaths/denyPaths, a fail-closed shell allow/deny list (deny wins), per-command timeout + output cap, and opt-ininheritEnv(no blanketprocess.env). - Backends —
local(default;@warlock.js/fs+node:child_process) andmock(in-memory, for hermetic disk-free tests). - Seven tools under
ws.tools.*—read_file,edit_file,write_file,run_shell,run_tests,grep,glob— each aToolContractready forai.agent({ tools }). - Direct methods —
readFile/writeFile/editFile/exec/grep/glob/exists/mkdir/remove, sharing the exact same policy seam. tools.all()/tools.pick(...)— every tool, or a least-privilege subset.readonly()— a projection that vends only read/grep/glob and rejects every mutating direct method.scope(subdir)— a sub-jailed view rooted atsubdir(tighter blast radius).- Read-before-edit guard —
read_filereturns a SHA-256hash;edit_filerequires an exact, uniqueoldString(orreplaceAll) and rejects on anexpectHashmismatch.
Deferred to later phases (intentionally absent here): the transactional
lifecycle (snapshot / restore / diff / changes), on() hooks, dispose,
usage(), and the worktree / container backends plus provisioning helpers.
Skills
Progressive-disclosure, per-task docs live under skills/:
use-a-workspace/— build a jailed workspace and operate it (policy, tools, direct methods,readonly/scope).build-loop-agent/— wirews.tools.all()into a coding agent that reads → edits → runs tests until green.
