Skip to content

core/src/permission-rules

The permission policy pi runs under (#446, #496): ENSO_HOME/permissions.json, in @gotgenes/pi-permission-system’s format, never a copy of another tool’s settings.

gotgenes reads its policy from one place only, <agentDir>/extensions/pi-permission-system/ config.json, and there is no setting to point it elsewhere. So preparePermissionPolicy makes that path a symlink to the user’s file. The file stays where #446 put it, and gotgenes’ reload on change still works, since it follows the link and stats the target.

It also writes the mode agent files (permission-modes.ts) beside pi’s other agent files, because gotgenes reads a mode’s preset from there.

⚠ gotgenes reads a file it cannot parse as an EMPTY policy, and an empty policy has no deny rules at all: every call asks. That is why an unreadable file refuses the launch here rather than being passed on. A file the user breaks while a session runs still reaches gotgenes that way; nothing here can stop that.

Only plain JSON is accepted. gotgenes also strips JavaScript-style comments before it parses, so every file this module accepts, it reads the same way. A commented file is refused here even though gotgenes would read it; that is the safe direction.

PermissionPolicyPreparation = { catchAllAdded: readonly string[]; kind: "kept"; movedAside?: string; path: string; surfaces: number; } | { kind: "seeded"; movedAside?: string; path: string; piccBackup?: string; was: "absent" | "without a permission block" | "a picc rules file"; } | { kind: "unreadable"; path: string; reason: string; }

Defined in: core/src/permission-rules.ts:165

What preparePermissionPolicy found and did with the user’s file.

  • kept: the file holds a permission block; it is used as it is.
  • seeded: the file now holds ENSO_DEFAULT_PERMISSION_POLICY. It was absent, held no permission block, or was a picc rules file (#496; that file is kept beside it, at piccBackup, and its rules are not carried over).
  • unreadable: the file is there but is not a JSON object (or is a link to nothing); nothing was written, and the caller refuses to start.

movedAside names where a regular file found at the engine’s config path was moved to (see linkEngineConfig), so the launch log says so. catchAllAdded names the surfaces of a kept file that got Enso’s catch-all put first (withCatchAllFirst), so the log says that too.


const ENSO_DEFAULT_PERMISSION_POLICY: object

Defined in: core/src/permission-rules.ts:79

The policy a fresh ENSO_HOME starts with.

  • Tools: reads and searches run, as do Enso’s ask_user and web_search; edits and writes ask; anything unnamed asks ("*"), web_fetch included.
  • Catch-alls: every map starts with Enso’s catch-all, ** (ENSO_CATCH_ALL): the one key a mode changes, and the pattern auto may answer. A rule a user adds after it is theirs, and beats every mode (#498, #499).
  • Bash: asks, except gotgenes’ documented read-only allowlist (its “Read-Only Bash Command Allowlist” recipe, trimmed of less/more, which can escape to a shell). The allowlist is safe to seed because gotgenes resolves a chain to its most restrictive part and floors wrappers (sh -c, sudo, xargs) to ask.
  • Git: the escapes Enso’s guards do not already refuse are denied (force-push is cc-safety-net’s git.push-force).
  • .env files: denied for every tool and every bash command, through symlinks too.
  • Outside the project: asks.
  • auto mode: authorizerChain names Enso’s classifier link. The link defers in every other mode, and a user who removes the name gets prompts in auto, never fewer.

⚠ path_write is allow inside the project on purpose. gotgenes checks a bash path argument against BOTH directions unless the command is one of its pure readers, so ask here made git diff src/a.ts ask (probed). The cost: an allowlisted command’s redirect (cat a > b) writes inside the project without asking. Writes outside still ask through external_directory.

⚠ git commit * -n* is deliberately absent: it would also deny a commit whose MESSAGE contains -n. -n right after commit is still denied.

readonly authorizerChain: readonly ["enso-auto"]

readonly permission: object

readonly *: "ask" = 'ask'

readonly ask_user: "allow" = 'allow'

readonly bash: object

readonly **: "ask" = 'ask'

readonly cat *: "allow" = 'allow'

readonly cmp *: "allow" = 'allow'

readonly date: "allow" = 'allow'

readonly df *: "allow" = 'allow'

readonly diff *: "allow" = 'allow'

readonly du *: "allow" = 'allow'

readonly fd *: "allow" = 'allow'

readonly find *: "allow" = 'allow'

readonly git blame *: "allow" = 'allow'

readonly git branch: "allow" = 'allow'

readonly git commit –no-verify*: "deny" = 'deny'

readonly git commit -n*: "deny" = 'deny'

permission.bash.git commit * –no-verify*
Section titled “permission.bash.git commit * –no-verify*”

readonly git commit * –no-verify*: "deny" = 'deny'

readonly git diff: "allow" = 'allow'

readonly git diff *: "allow" = 'allow'

readonly git log: "allow" = 'allow'

readonly git log *: "allow" = 'allow'

readonly git ls-files *: "allow" = 'allow'

readonly git push –delete *: "deny" = 'deny'

readonly git push * –delete *: "deny" = 'deny'

readonly git remote -v: "allow" = 'allow'

readonly git show *: "allow" = 'allow'

readonly git status: "allow" = 'allow'

readonly git status *: "allow" = 'allow'

readonly grep *: "allow" = 'allow'

readonly head *: "allow" = 'allow'

readonly ls: "allow" = 'allow'

readonly ls *: "allow" = 'allow'

readonly pwd: "allow" = 'allow'

readonly rg *: "allow" = 'allow'

readonly sha256sum *: "allow" = 'allow'

readonly stat *: "allow" = 'allow'

readonly tail *: "allow" = 'allow'

readonly tree *: "allow" = 'allow'

readonly uname *: "allow" = 'allow'

readonly wc *: "allow" = 'allow'

readonly which *: "allow" = 'allow'

readonly whoami: "allow" = 'allow'

readonly edit: object

readonly **: "ask" = 'ask'

readonly external_directory: object

readonly **: "ask" = 'ask'

readonly find: "allow" = 'allow'

readonly grep: "allow" = 'allow'

readonly ls: "allow" = 'allow'

readonly path_read: object

readonly *.env: "deny" = 'deny'

readonly *.env.*: "deny" = 'deny'

readonly *.env.example: "allow" = 'allow'

readonly **: "allow" = 'allow'

readonly path_write: object

readonly *.env: "deny" = 'deny'

readonly *.env.*: "deny" = 'deny'

readonly **: "allow" = 'allow'

readonly read: "allow" = 'allow'

readonly web_search: "allow" = 'allow'

readonly write: object

readonly **: "ask" = 'ask'


describePermissionPolicy(preparation): string

Defined in: core/src/permission-rules.ts:445

The line both launch surfaces log for a preparation that did not refuse, in the same words on the web host and the terminal launcher, so a reader finds one in the logs by the other.

{ catchAllAdded: readonly string[]; kind: "kept"; movedAside?: string; path: string; surfaces: number; } | { kind: "seeded"; movedAside?: string; path: string; piccBackup?: string; was: "absent" | "without a permission block" | "a picc rules file"; }

string


permissionEngineConfigPath(agentDir): string

Defined in: core/src/permission-rules.ts:145

Where gotgenes reads its policy, under pi’s agent directory.

string

string


preparePermissionPolicy(policyPath, agentDir): PermissionPolicyPreparation

Defined in: core/src/permission-rules.ts:395

Make policyPath a policy gotgenes will use as it is, and put it and the mode presets where gotgenes reads them:

  • keep a file that holds a permission block;
  • seed one that is absent, holds none, or is a picc rules file;
  • refuse one that is not a JSON object, touching nothing.

Seeding keeps every other top-level key the file had, except a picc file, which is replaced whole (its copy is kept beside it; replacePiccRulesFile names it).

Call it before the extension loads, on both launch surfaces.

string

string

PermissionPolicyPreparation


replacePiccRulesFile(policyPath): string | undefined

Defined in: core/src/permission-rules.ts:269

Move a picc rules file aside and write the seed in its place, returning the backup’s path, or undefined when the file was already gone.

⚠ TWO LAUNCHES MAY BOTH DECIDE TO MIGRATE (#497 review): the web host and the terminal, on a home not yet migrated, each read the picc file before either moved it. So this step runs with a decision that may be stale, and whatever it moves, moveAside never overwrites an earlier backup: the user’s rules stay in the first one (.picc-backup), and a late launch’s copy of the seed, or of the same rules, lands in .picc-backup.1. When the file is already gone (the other launch moved it and has not written the seed yet), the caller reads again rather than failing.

string

string | undefined