Agent projects and policy

An agent project pins one cruft agent run's whole authority into a single agent-policy.json file, alongside its code, inputs, and audit log. This page covers scaffolding a project, the policy schema, the commands that edit and validate it, and turning it back into a run.

An agent project is a directory that pins one cruft agent run's entire authority into a single file, agent-policy.json, alongside the code, inputs, audit log, and state it needs. Where the run model documents the run flags and agent tools documents what each tool can touch, this page is about the durable, reviewable form of a run: how a project is scaffolded, what the policy file declares, the commands that edit it in place, and how it turns back into a cruft agent run.

The point of a project is that the grant is data. A policy file, its integrity hashes, and its audit log can be checked into a repository, reviewed in a pull request, validated in CI, and replayed later, without ever re-deriving the run flags by hand.

cruft agent init review-proj --template=package-review
cruft agent policy validate --strict --project-confined review-proj
cruft agent run --project review-proj

Creating a project

cruft agent init <dir> [--template=<name>] [--blank] [--force]

init scaffolds a new project directory. --template=<name> selects one of the six templates below; --blank is the least-authority starting point (equivalent to --template=blank); --force allows initializing into a directory that already exists.

Scaffold layout

Every project has the same shape. This is the layout init produces for the package-review template:

.
├── agent-policy.json      # the pinned authority for this project
├── agent.js               # the agent entry script
├── README.md
├── run.sh                 # validate, run, replay in one script
├── audit/
│   └── agent.jsonl        # the run's JSONL audit log
├── examples/
├── hooks/                 # import-hook sources
├── modules/               # admitted module sources
│   └── package-input.js   # this template's required input module
├── packages/              # admitted package sources
├── repo/                  # fs-read repo slice (used by repo-review)
│   └── src/
└── state/                 # session file lives here

The generated run.sh is the canonical run recipe for a project:

#!/bin/sh
set -eu
cd "$(dirname "$0")"
CRUFT_BIN=${CRUFT_BIN:-cruft}
"$CRUFT_BIN" agent policy validate --strict --project-confined .
"$CRUFT_BIN" agent run --project .
"$CRUFT_BIN" agent replay --human audit/agent.jsonl

It validates the policy under the strict, project-confined rules, runs the project, then replays the resulting audit log in human-readable form.

Templates

List and inspect templates with cruft agent template list and cruft agent template describe <name>. There are six:

TemplateStatusToolsSecretsSummary
blankscaffoldnonenoneLeast-authority empty project for building your own agents
demoexampleechononePublic-surface demonstration with explicit echo/module/package/import-hook caps
package-reviewproduction-boundednonenoneBounded package source/metadata review through an explicit source module input
ci-triageproduction-boundednonenoneBounded CI log triage through an explicit source module input
repo-reviewproduction-boundedlistFiles, readFilenoneBounded repository review through an explicit fs-read repo slice
plugin-quarantineproduction-boundednonenoneBounded third-party plugin source review without executing the plugin

describe reports, for each template, its required tools, required secrets, required inputs, the budget keys it sets, its expected output events, and its explicit non-claims. The details that differ:

blank requires only agent.js. It sets timeout_ms, max_events, max_event_bytes, max_tool_arg_bytes, max_tool_result_bytes, max_microtasks, and max_steps. It grants no tools and claims no ambient fs, network, process, model, or package-graph authority.

demo grants echo and requires three inputs that show each admission path at once: modules/policy-module.js (a module), packages/left-pad.js (a package), and hooks/hooked.js (an import hook). It is explicitly not a production workflow and claims no process, shell, network, or model authority and no arbitrary npm graph execution.

package-review requires modules/package-input.js as its source input and emits package-review-start and package-review-finding events plus a session snapshot and a run artifact manifest. It does not do live npm/OSV or network lookup unless those tools are added separately, and claims no process/shell authority.

ci-triage requires modules/ci-log.js and emits ci-triage-start and ci-triage-finding events. It does not run CI commands and has no network/model authority by default.

repo-review is the one template that grants tools out of the box: listFiles and readFile. Its inputs are the repo/ slice plus fs_read_include and fs_read_exclude globs, and it emits repo-review-start and repo-review-finding events along with an fs-read source manifest. It reads a bounded, root-.gitignore-subset slice only, with no ambient fs and no git checkout or search.

plugin-quarantine requires modules/plugin-source.js and emits plugin-quarantine-start and plugin-quarantine-finding events. It reviews plugin source without executing the plugin's package graphs and has no ambient fs.

All the production-bounded templates set the same nine budget keys: timeout_ms, tool_timeout_ms, max_state_bytes, max_events, max_event_bytes, max_tool_arg_bytes, max_tool_result_bytes, max_microtasks, and max_steps.

The agent-policy.json schema

A policy is a flat JSON object. Here is the file init --template=package-review generates, which exercises every field:

{
  "schema_version": 1,
  "profile": "package-review",
  "agent": "agent.js",
  "worker": false,
  "audit_log": "audit/agent.jsonl",
  "context": {"package_name":"sample-package","review_goal":"bounded package review"},
  "state": {},
  "session_file": "state/session.json",
  "tools": [],
  "budgets": {
    "timeout_ms": 500,
    "tool_timeout_ms": 250,
    "max_state_bytes": 65536,
    "max_events": 16,
    "max_event_bytes": 8192,
    "max_tool_arg_bytes": 8192,
    "max_tool_result_bytes": 8192,
    "max_microtasks": 1000,
    "max_steps": 100000
  },
  "modules": {"./package-input.js":"modules/package-input.js"},
  "packages": {},
  "package_integrity": {},
  "import_hooks": {},
  "import_hook_integrity": {}
}
FieldMeaning
schema_versionPolicy format version. 1 today.
profileThe template this project was created from. Descriptive; ties the policy back to a known authority shape.
agentPath to the agent entry script, relative to the project root. Resolved to an absolute path at run time.
workerWhether to run under the worker host (--worker). See the worker note below.
audit_logPath where the JSONL audit log is written, relative to the project root.
contextThe frozen instruction object handed to the agent as context, the policy form of --context-json. Deeply frozen at run time; the agent cannot mutate it.
stateThe initial single-turn state object, the policy form of --state-json.
session_filePath to the turn-lineage session file, the policy form of --session-file.
toolsThe explicit tool registry for this run. Empty means no tools are granted; each name here becomes a --tool=<name> grant.
budgetsThe resource budgets, one key per --max-* / *-timeout-ms flag. See the table below.
modulesInline-admitted modules, a map of import specifier to source path (the policy form of --module specifier=path).
packagesAdmitted package graphs, a map of specifier to source path (--package specifier=path).
package_integrityPer-package fnv1a64 source hashes that pin each admitted package (--package-integrity specifier=fnv1a64:hash).
import_hooksAdmitted import hooks, a map of specifier to source path (--import-hook specifier=path).
import_hook_integrityPer-hook fnv1a64 source hashes that pin each admitted import hook (--import-hook-integrity specifier=fnv1a64:hash).

The budgets keys map one-to-one onto the run flags documented in the run model: timeout_ms (--timeout-ms), tool_timeout_ms (--tool-timeout-ms), max_state_bytes, max_events, max_event_bytes, max_tool_arg_bytes, max_tool_result_bytes, max_microtasks, and max_steps.

The integrity maps are why cruft agent hash (below) exists: to add a package or import hook to a policy safely, you compute its source hash once and pin it, so a later change to the source is a validation failure rather than a silent substitution.

Editing a policy in place

Each of these commands mutates a project's agent-policy.json. They are the reviewable alternative to editing JSON directly, and they keep the policy internally consistent.

CommandEffect
cruft agent add-tool <project> <tool>Add a tool name to tools.
cruft agent remove-tool <project> <tool>Remove a tool name from tools.
cruft agent add-module <project> <path> --specifier=<specifier>Admit a module: add specifier -> path to modules.
cruft agent remove-module <project> <specifier>Remove a module admission by specifier.
cruft agent add-package <project> <specifier> <path>Admit a package graph: add specifier -> path to packages.
cruft agent remove-package <project> <specifier>Remove a package admission by specifier.
cruft agent add-import-hook <project> <specifier> <path>Admit an import hook: add specifier -> path to import_hooks.
cruft agent remove-import-hook <project> <specifier>Remove an import-hook admission by specifier.
cruft agent set-context <project> <json-object>Replace context with the given JSON object.
cruft agent unset-context <project>Clear context.
cruft agent set-budget <project> key=value [key=value ...]Set one or more budgets keys.
cruft agent unset-budget <project> <key> [key ...]Remove one or more budgets keys.
cruft agent set-session <project> <session-file>Set session_file.
cruft agent unset-session <project>Clear session_file.
cruft agent set-worker <project> true/falseSet worker.
cruft agent unset-worker <project>Clear worker (back to default).

Validating, explaining, and diffing a policy

cruft agent policy validate [--strict] [--project-confined] <project|agent-policy.json>
cruft agent policy explain  [--json]                        <project|agent-policy.json>
cruft agent policy diff     [--json]                        <project|agent-policy.json>

validate resolves every path in the policy and reports whether the policy is well-formed and its inputs exist. --strict applies the stricter checks; --project-confined requires every resolved path to stay inside the project directory (no reaching outside the project root). This is the check run.sh runs first, and the one to run in CI:

policy valid: /tmp/agpr/agent-policy.json
agent=/tmp/agpr/agent.js
audit_log=/tmp/agpr/audit/agent.jsonl

explain prints what the policy actually grants, without running anything. With --json it reports authority counts (tools, approval-required tools, approved tools, modules, packages, import hooks, budgets, and the two integrity maps), the raw-to-resolved mapping of every path, the validation result, the run's non-claims, and a snapshot of the doctor control inventory. For the package-review policy above:

{
  "type": "agent_policy_explain",
  "policy": "/tmp/agpr/agent-policy.json",
  "authority_counts": {
    "tools": 0, "approval_required_tools": 0, "approved_tools": 0,
    "modules": 1, "packages": 0, "import_hooks": 0, "budgets": 9,
    "package_integrity": 0, "import_hook_integrity": 0
  },
  "resolved_paths": {
    "agent": {"raw": "agent.js", "resolved": "/tmp/agpr/agent.js"},
    "audit_log": {"raw": "audit/agent.jsonl", "resolved": "/tmp/agpr/audit/agent.jsonl"},
    "session_file": {"raw": "state/session.json", "resolved": "/tmp/agpr/state/session.json"},
    "modules": [{"specifier": "./package-input.js", "raw": "modules/package-input.js", "resolved": "/tmp/agpr/modules/package-input.js"}],
    "packages": [], "import_hooks": []
  },
  "validation": {"valid": true, "errors": [], "notes": ["..."]},
  "non_claims": ["OS/process sandboxing", "process/shell/exec/spawn tools", "network policy", "arbitrary npm/lockfile graphs", "direct allocator pre-kill", "external async/process cancellation"],
  "doctor_control_snapshot": { "...": "..." }
}

The non_claims list states the run's boundary up front: this compartment does not claim OS or process sandboxing, does not offer process/shell/exec/spawn tools, has no general network policy, does not run arbitrary npm or lockfile graphs, cannot pre-kill on allocation, and cannot cancel external async or process work.

diff compares a project's current policy against what it would be if re-derived, so you can see drift between a checked-in policy and its inputs. --json gives the machine-readable form.

Bundling, history, and reset

cruft agent bundle <project|agent-policy.json> --out <dir> [--run-id=<id>|--failed]
cruft agent history [--json|--human] <project|agent-policy.json|audit.jsonl>
cruft agent reset   [--dry-run] [--rollback] [--json|--human] <project|agent-policy.json>

bundle exports a run's evidence into --out <dir>: the policy, the audit log, and the run's artifact manifest, scoped to a single run. --run-id=<id> selects a specific run; --failed selects the failed run. For a repo-review project, the bundle carries the fs-read source manifest and its dispositions without copying the underlying source.

history summarizes the runs recorded in a project's audit log: run ids and what each did. It reads the project's audit_log, so it reports an error if no run has been recorded yet:

cruft agent history: cannot read /tmp/agpr/audit/agent.jsonl: No such file or directory

reset clears the project's session file so the next run starts fresh. It keeps exactly one backup, so the reset can be undone. --dry-run reports what it would do without touching anything; --rollback restores the last backup. The dry-run form shows the full plan:

{"type":"agent_reset","policy":"/tmp/agpr/agent-policy.json","session_file":"/tmp/agpr/state/session.json","backup_file":"/tmp/agpr/state/session.json.cruft-reset-backup","mode":"reset","existed":false,"removed":false,"restored":false,"dry_run":true,"scope":"session_file_only","rollback":"one_backup"}

Note scope: "session_file_only": reset touches turn lineage, not the policy, the audit log, or any inputs.

Computing integrity hashes

cruft agent hash <path> [--kind=source|module|import-hook|package] [--specifier=<specifier>] [--json]

hash computes the fnv1a64 source hash of a file, the value that goes into a policy's package_integrity or import_hook_integrity map. --kind selects how the source is treated (plain source, a module, an import-hook, or a package), and --specifier supplies the specifier when the hash is specifier-scoped. The output is the pinnable hash:

fnv1a64:be6350e58a98cbe3
# source_hash=fnv1a64:be6350e58a98cbe3

Pin a package by pairing add-package with the hash from hash --kind=package, recorded in package_integrity; pin an import hook the same way with hash --kind=import-hook and import_hook_integrity. When validate later re-hashes the source and the value differs, the policy fails closed.

Running a project

cruft agent run --project <dir>
cruft agent run --policy <agent-policy.json>

Either form loads a policy and expands it into the underlying run. --project points at a project directory (and uses its agent-policy.json); --policy points directly at a policy file. Both are equivalent to assembling the run flags documented in the run model by hand:

Policy fieldMaps to run flag
agentthe positional agent.js (or --entry-module for a static module entry)
worker--worker
audit_log--audit-log=<path>
context--context-json=<json>
state--state-json=<json>
session_file--session-file=<path>
toolsone --tool=<name> per entry
budgets.timeout_ms--timeout-ms
budgets.tool_timeout_ms--tool-timeout-ms
budgets.max_state_bytes--max-state-bytes
budgets.max_events / max_event_bytes--max-events / --max-event-bytes
budgets.max_tool_arg_bytes / max_tool_result_bytes--max-tool-arg-bytes / --max-tool-result-bytes
budgets.max_microtasks / max_steps--max-microtasks / --max-steps
modulesone --module specifier=path per entry
packagesone --package specifier=path per entry
package_integrityone --package-integrity specifier=fnv1a64:hash per entry
import_hooksone --import-hook specifier=path per entry
import_hook_integrityone --import-hook-integrity specifier=fnv1a64:hash per entry

A note on worker: setting worker: true requests the worker host, but the full worker membrane is not available yet, so a policy that depends on worker isolation is depending on a control that is not finished. Keep worker: false for now unless you are exercising the worker path deliberately. The doctor inventory (the run model) always states which controls are enforced versus not available, and the same rule governs what a policy may claim.

See also