Permissions

Permissions are Dawn's human-in-the-loop gate for workspace operations. The runtime checks two kinds of operations before they proceed:

  • runBash commands (kind: "command") — shell commands are matched against allow and deny lists before executing.
  • Filesystem paths outside workspace/ (kind: "path") — file reads, writes, and directory listings that would escape the workspace root are permission-gated.

Unknown operations pause the run and ask the human rather than failing or silently proceeding.

Configuration

Set allow and deny lists in dawn.config.ts:

dawn.config.ts
export default {
  permissions: {
    // mode?: "interactive" | "non-interactive" | "bypass"  (default "interactive")
    allow: { bash: ["ls", "cat"] },
    deny: { bash: ["rm -rf", "sudo"] },
  },
}

allow and deny each map a tool name (bash) to an array of pattern strings. The research scaffold, for example, allows safe read-only commands and denies destructive ones — but leaves the network-fetch script off the allow list so the first run surfaces a prompt.

How matching works

For every runBash call the runtime runs this sequence:

  1. Deny first — if any deny pattern is a prefix of the command string, the call is rejected immediately.
  2. Then allow — if any allow pattern is a prefix of the command string, the call proceeds.
  3. No match → "unknown" — the outcome depends on the mode.

Prefix matching means "ls" covers ls, ls -la, and ls workspace/corpus. Use longer patterns to be more specific.

Modes

ModeUnknown command behavior
interactive (default)Run pauses; an interrupt is sent to the client for human decision
non-interactiveUnknown commands are denied immediately (fail-closed)
bypassAll commands are allowed without checking — dev/test only

Override the mode for a single run without touching the config by setting the DAWN_PERMISSIONS_MODE environment variable:

bash
DAWN_PERMISSIONS_MODE=non-interactive dawn dev

The env var takes precedence over permissions.mode in dawn.config.ts.

The interrupt payload

When a command or path is "unknown" in interactive mode, the agent run pauses and the runtime emits an SSE event. The kind field tells you which gate fired.

kind: "command" — a runBash command was not on the allow list:

text
event: interrupt
data: {
  "interruptId": "perm-abc123",
  "type": "permission-request",
  "kind": "command",
  "detail": {
    "command": "node scripts/fetch-source.mjs https://example.com/api",
    "suggestedPattern": "node scripts/fetch-source.mjs"
  }
}

kind: "path" — a filesystem operation targeted a path outside workspace/:

text
event: interrupt
data: {
  "interruptId": "perm-def456",
  "type": "permission-request",
  "kind": "path",
  "detail": {
    "operation": "readFile",
    "path": "/Users/me/private/notes.md",
    "suggestedPattern": "/Users/me/private/"
  }
}

detail.suggestedPattern is the prefix Dawn suggests you add to the allow list so the operation is approved automatically on future runs. The run stays paused until you resume it.

Resuming an interrupted run

Send a POST /threads/:thread_id/resume request with the interrupt_id from the SSE event and a decision:

DecisionEffect
onceAllow this command for this invocation only
alwaysAllow and persist the suggestedPattern to .dawn/permissions.json
denyReject the command; the run continues with an error result

Here is the full sequence using curl (start the server with dawn dev --port 2024):

bash
# 1. Create a thread
THREAD=$(curl -sX POST http://127.0.0.1:2024/threads \
  -H 'Content-Type: application/json' \
  -d '{}' | jq -r .thread_id)
 
# 2. Start a run — stream until the interrupt fires
curl -N http://127.0.0.1:2024/threads/$THREAD/runs/stream \
  -H 'Content-Type: application/json' \
  -d '{"input":{"messages":[{"role":"user","content":"fetch the API docs"}]},"route":"/research#agent"}'
# ...SSE output...
# event: interrupt
# data: {"interruptId":"perm-abc123","type":"permission-request","kind":"command","detail":{"command":"node scripts/fetch-source.mjs ...","suggestedPattern":"node scripts/fetch-source.mjs"}}
 
# 3. Resume with a decision
curl -X POST http://127.0.0.1:2024/threads/$THREAD/resume \
  -H 'Content-Type: application/json' \
  -d '{"interrupt_id":"perm-abc123","decision":"once"}'
# The response streams the continuation as SSE

Choosing "always" persists the allow entry to .dawn/permissions.json. On subsequent runs the command is matched by the allow list and proceeds without prompting.

Testing

In @dawn-ai/testing, use expectInterrupt and harness.resume to drive the approval flow in automated tests without a live server. See the testing docs for the full pattern.

Related