FabricFabricHarness
Reference

Policies and Approvals

Capability policies, approval flows, and where they live.

Policies are enforced at tool dispatch and, by default, at the sandbox boundary. Definition-level policies act as a security floor: invocation code may narrow access but cannot replace allowlists or remove denials.

Available controls

  • Scoped commands. Agents declare commands per call (commands: [...]). Anything else is a capability error.
  • Capability-aware tools. write and edit honor any filesystem write scope on the session.
  • Approvals. session.approval.request({ reason, risk, timeout }) waits durably (Temporal target) or in-process (inline runtime).

The capability policy block can be set on a finite job, init(), a session, or one prompt:

import { init, policiedFetch, type CapabilityPolicy } from '@fabric-harness/sdk';

const policy: CapabilityPolicy = {
    filesystem: {
      read:  ['/workspace/**'],
      write: ['/workspace/src/**', '/workspace/tests/**'],
      writeDeny: ['/etc/**', '/usr/**'],
      writeRequireApproval: ['**/.env*'],
    },
    commandPolicy: {
      allow: ['npm test', 'git diff', 'git status'],
      requireApproval: ['git push*', 'rm -rf*'],
    },
    network: {
      mode: 'allowlist',
      hosts: ['api.openai.com', '*.anthropic.com', 'api.github.com'],
      protocols: ['https:'], // defaults to ['http:', 'https:']
    },
    approvals: { defaultTimeoutMs: 5 * 60_000 },
};

const fabric = await init({ policy });

Network policy

The network block is enforced only for HTTP routed through the SDK's policiedFetch helper or a connector that uses it. It does not monkey-patch global fetch, Axios, third-party SDKs, raw sockets, DNS, or subprocess traffic. A custom tool using one of those paths can bypass the application policy unless you explicitly route it through the policy-aware transport.

Wrap every custom fetch-like transport:

const safeFetch = policiedFetch(fetch, policy);
await safeFetch('https://evil.example.com');
// → throws FabricError { code: 'POLICY_DENIED', ... }

For Axios, supply an adapter backed by a policy-aware Fetch implementation, or put the entire agent behind an egress proxy. For production workloads, pair application checks with a container, cluster, or cloud-provider network boundary and call assertEnforceableNetworkPolicy() during startup. See private networking and egress.

Modes:

  • 'allowlist' — only hosts matching the hosts glob list are permitted (default when hosts is set).
  • 'denylist' — listed hosts are blocked, everything else allowed.
  • 'none' — block all outbound network. Useful for sandboxed analysis agents.

Mount permissions

session.mount(mountAt, source, { mode: 'read' | 'write' }) (read by default) marks a mount as immutable. Write tools (write, edit, mkdir, rm) targeting paths under it are rejected by capability policy at the tool-call layer.

Sandbox-layer enforcement (v0.10+)

By default, init({ policy }) auto-wraps the session's SandboxEnv with policiedSandboxEnv() so that raw sandbox.exec / sandbox.writeFile / sandbox.readFile calls from inside agent code honor the same CapabilityPolicy as tool dispatch. Without this, agent code that called (await session.sandbox).exec('rm -rf /') would bypass the policy that would have blocked the equivalent session.shell() call.

Pass bypassPolicyEnforcement: true to init() if you have your own enforcement layered above (egress proxy, custom tools, etc.).

import { init, policiedSandboxEnv } from '@fabric-harness/sdk';

const fabric = await init({
  policy: {
    commandPolicy: { deny: ['rm -rf*'] },
    filesystem: { writeDeny: ['/etc/**'] },
  },
});
const session = await fabric.session();
const sandbox = await session.sandbox;

// This now throws COMMAND_DENIED — the same policy that gates session.shell().
await sandbox.exec('rm -rf /tmp/data');

The decorator throws COMMAND_DENIED for exec violations and POLICY_DENIED for filesystem violations. requireApproval patterns at the sandbox layer surface as denials with details.approvalRequired: true — the sandbox layer doesn't auto-resolve approvals because it has no session context. Use session.shell() / tool calls when you want approvals gated through the full machinery.

Where to put policy

  • Per promptsession.prompt(text, { commands, policy }).
  • Per sessionagent.session(id, { policy }).
  • Per finite agentdefineAgent({ policy, run }); this is a security floor for run() calls to init().
  • Per persistent agent — return { policy } from createAgent(({ id }) => ({ ... })).
  • Shared application default — pass policy to init() or merge a policy module from .fabricharness/policies/ in your own config.

See Enterprise controls for a complete definition-level example with tools, approvals, and cost budgets.

Approvals from the CLI

fh approvals <session-id> --pending
fh approve  <session-id> <approval-id> --actor preetham
fh reject   <session-id> <approval-id> --actor preetham --reason "Wrong branch"

# Deployed application: discover across the authenticated tenant
fh approvals --url "$FABRIC_HARNESS_APP_URL/api" --token-env FABRIC_HARNESS_REMOTE_TOKEN

When a model provider omits its own tool-call id, Harness creates a unique id for each logical call and reuses it across approval binding, lineage, tool context, and session history. Two calls to the same tool in one session therefore cannot share an approval grant or idempotency identity by accident.

See Approvals and fh approvals.