@dawn-ai/permissions

Use this when

Use this package to evaluate Dawn permission patterns or inject a PermissionsStore into an integration. The root is the portable matching and contract surface. Import /node only when the application itself owns the local .dawn/permissions.json store.

Install and import

bash
pnpm add @dawn-ai/permissions
ts
import { matchPermission, type PermissionsStore } from "@dawn-ai/permissions"
import { createPermissionsStore } from "@dawn-ai/permissions/node"

Compatibility and audience

SurfaceRuntimePurityAudienceStability
@dawn-ai/permissionsedge-safenot-claimedintegrationsupported
@dawn-ai/permissions/nodenode-onlynot-claimedintegrationsupported

The root keeps Node filesystem APIs out of its import graph. The /node store reads and writes application-local files and therefore requires Node.

Public exports

@dawn-ai/permissions

ExportResponsibility
matchPermissionMatch one candidate against allow and deny pattern maps.
subagentPermissionPatternSerialize a parent-route and subagent-name identity.
suggestedCommandPatternSuggest the first two command tokens.
suggestedMemoryPatternSuggest a terminated workspace-and-route namespace prefix.
suggestedPathPatternSuggest a path's parent directory prefix.
CommandDetailDescribe a command permission request.
MemoryDetailDescribe a memory supersede request.
PathDetailDescribe a filesystem permission request.
PermissionDecisionName an interrupt resume decision.
PermissionModeName the active permission mode.
PermissionRequestDescribe the discriminated interrupt payload.
PermissionsFileDescribe config and runtime permission maps.
PermissionsStoreDefine permission loading, matching, and persistence.
SubagentDetailDescribe a subagent dispatch request.
ToolDetailDescribe an arbitrary tool request.

@dawn-ai/permissions/node

The /node entry does not re-export the root. Import shared types and matching helpers from @dawn-ai/permissions independently.

ExportResponsibility
createPermissionsStoreCreate the application-local disk-backed store.

Key contracts

ts
export type PermissionMode = "interactive" | "non-interactive" | "bypass"
ts
export type PermissionDecision = "once" | "always" | "deny"
ts
export interface PermissionsFile {
  readonly version: 1
  readonly allow: Readonly<Record<string, readonly string[]>>
  readonly deny: Readonly<Record<string, readonly string[]>>
}

Fields: @dawn-ai/permissions#.:PermissionsFile

FieldTypeRequiredDescription
readonly version1yesIdentify the current file format.
readonly allowReadonly<Record<string, readonly string[]>>yesMap operation keys to allow patterns.
readonly denyReadonly<Record<string, readonly string[]>>yesMap operation keys to deny patterns.
ts
export type PermissionRequest = PermissionRequestBase &
  (
    | { readonly kind: "command"; readonly detail: CommandDetail }
    | { readonly kind: "path"; readonly detail: PathDetail }
    | { readonly kind: "tool"; readonly detail: ToolDetail }
    | { readonly kind: "memory"; readonly detail: MemoryDetail }
    | { readonly kind: "subagent"; readonly detail: SubagentDetail }
  )
ts
export interface PermissionsStore {
  load(): Promise<void>
  match(tool: string, candidate: string): "allow" | "deny" | "unknown"
  addAllow(tool: string, pattern: string): Promise<void>
  readonly mode: PermissionMode
}
ts
export declare function matchPermission(
  tool: string,
  candidate: string,
  allow: Readonly<Record<string, readonly string[]>>,
  deny: Readonly<Record<string, readonly string[]>>,
): "allow" | "deny" | "unknown"
ts
export declare function createPermissionsStore(opts: {
  readonly appRoot: string
  readonly config: PermissionsFile | undefined
  readonly mode: PermissionMode
}): PermissionsStore

The inline input object is public; CreateOptions is not exported.

Behavior contract permissions.match.prefix

Non-reserved command, path, and memory candidates use prefix matching, and deny wins over allow.

Behavior contract permissions.tool.exact

Reserved tool names match exactly rather than by prefix.

Behavior contract permissions.subagent.exact

Reserved subagent identities match exactly rather than by prefix.

Pattern boundaries

Reserved tool and subagent keys use exact matching. Other keys use candidate.startsWith(pattern), so callers must choose boundary-safe patterns: paths normally end in /, and memory namespaces use the terminating | returned by suggestedMemoryPattern().

Behavior contract permissions.store.noninteractive

Non-interactive mode ignores the runtime permissions file.

Store lifecycle and trust boundary

Call and await store.load() before any store use, especially before addAllow() or other persistence. Calling addAllow() first can overwrite grants already present in the runtime file. In interactive mode load() reads .dawn/permissions.json; non-interactive mode uses config maps but intentionally ignores that runtime file; bypass mode returns unknown for every match. A malformed interactive runtime file rejects load().

Only addAllow() persists a runtime decision. It serializes concurrent writes, creates .dawn/permissions.json, updates the in-memory allow map, and adds .dawn/ to .gitignore. The store has no deny-write or close method. Configuration ownership remains with the caller: pass the resolved config and mode when constructing it.

Patterns are policy inputs, not authentication or filesystem containment. Prefixes can be broad, and permission-map keys are open strings except for the reserved exact-match behavior described above.

ts
import { createPermissionsStore } from "@dawn-ai/permissions/node"
 
const store = createPermissionsStore({
  appRoot: process.cwd(),
  config: { version: 1, allow: { bash: ["pnpm test"] }, deny: {} },
  mode: "non-interactive",
})
await store.load()
 
if (store.match("bash", "pnpm test packages/sdk") !== "allow") {
  throw new Error("Command is not allowed")
}

Continue with Permissions, Access Control, and Workspace API.