policy

package
v0.1.159 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 3, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package policy is the codefly permission and capability layer.

It encodes who-can-do-what across plugins:

  • SandboxPolicy: the read/write/network/socket policy a plugin declares in its manifest. Translates 1:1 to a runners/sandbox configuration.

  • CanonicalRegistry: the binary→toolbox map used to enforce "every plugin's Bash that tries to run `git` is denied; route through the Git toolbox." Computed from the union of plugins' `canonical_for:` declarations plus a small built-in fallback for binaries no toolbox has claimed yet (so that even before the Git toolbox ships, `bash -c git` is blocked).

Enforcement happens at two layers in concert:

  1. AST-aware bash parsing (mvdan/sh) splits on &&/||/;/| and evaluates each command independently against the registry. Defeats the canonical "git status && git push" chaining bypass that every other coding agent permission system has.

  2. OS sandbox (runners/sandbox): even if the parser is fooled, the bash executor itself runs inside bwrap/sandbox-exec with no access to git/docker/nix binaries. Belt-and-suspenders — both layers must fail for an agent to break out.

Permissions live IN THE PLUGIN MANIFEST (declarative), not in codefly host code. The host reads, validates, builds the registry, and enforces. Plugins are the source of truth.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type AllowAllPDP

type AllowAllPDP struct{}

AllowAllPDP allows every call. Identity surface for "no policy configured."

func (AllowAllPDP) Evaluate

type CanonicalRegistry

type CanonicalRegistry struct {
	// contains filtered or unexported fields
}

CanonicalRegistry maps a binary name to the toolbox that owns it.

When a plugin's Bash toolbox parses a command and finds the leaf program is in this registry, it MUST refuse and tell the agent to invoke the canonical toolbox instead. The canonical-tool routing is the high-level enforcement layer — the OS sandbox is the belt-and-suspenders below it.

Population:

  • At plugin manifest load time, every plugin's `canonical_for:` list contributes to the registry. Each binary name may have exactly one canonical owner; a conflict is a load-time error (caught early, not at first invocation).

  • A built-in fallback covers binaries no plugin has claimed yet (`git`, `docker`, `nix`, `kubectl`, `helm`, `curl`, `wget`). Default behavior for the fallback set: DenyMissingToolbox — refuse with a clear "install the X toolbox" hint, instead of silently letting bash run the binary unsupervised.

func NewCanonicalRegistry

func NewCanonicalRegistry() *CanonicalRegistry

NewCanonicalRegistry returns a registry seeded with the built-in fallback. Plugin claims are added via Claim.

func (*CanonicalRegistry) Claim

func (r *CanonicalRegistry) Claim(owner string, binaries ...string) error

Claim records that `owner` is the canonical toolbox for each binary in `binaries`. Returns an error if any binary already has a non-fallback owner — two plugins both claiming `git` is a configuration error that must surface at load time, not at first invocation.

If an existing entry is a built-in fallback (owner == ""), the claim wins silently — the Git toolbox plugin claims `git`, replacing the unclaimed-fallback entry.

func (*CanonicalRegistry) Lookup

func (r *CanonicalRegistry) Lookup(bin string) *Decision

Lookup returns the routing decision for a binary name. A nil decision means the binary is not routed and bash may execute it (subject to whatever other policy layers apply). The lookup strips a leading path: `/usr/bin/git` resolves to `git`.

func (*CanonicalRegistry) Owners

func (r *CanonicalRegistry) Owners() []OwnerEntry

Owners returns a sorted snapshot of (binary, owner) pairs for diagnostic display (`codefly policy show` style commands).

type Decision

type Decision struct {
	// Routed indicates the binary has a canonical toolbox; the bash
	// executor must refuse and direct the caller there.
	Routed bool

	// Owner is the plugin name that owns the canonical surface, or ""
	// for the built-in fallback (no plugin yet ships the toolbox; the
	// binary is denied with a hint to install one).
	Owner string

	// Reason is the human-readable explanation for the routing —
	// suitable for surfacing verbatim in the bash-toolbox error.
	Reason string
}

Decision is the result of looking up a binary in the registry.

type DenyAllPDP

type DenyAllPDP struct{}

DenyAllPDP denies every call. Useful in tests + as a sanity check.

func (DenyAllPDP) Evaluate

type JSONPDP

type JSONPDP struct {
	// contains filtered or unexported fields
}

JSONPDP evaluates a JSONPolicy. Construct with NewJSONPDPFromFile or directly via NewJSONPDP for in-memory use (tests).

func NewJSONPDP

func NewJSONPDP(p JSONPolicy) *JSONPDP

NewJSONPDP returns a PDP backed by the given parsed policy.

func NewJSONPDPFromFile

func NewJSONPDPFromFile(path string) (*JSONPDP, error)

NewJSONPDPFromFile reads a JSON file and constructs a JSONPDP. The file shape mirrors JSONPolicy directly; example:

{
  "default": "deny",
  "rules": [
    {"toolbox": "git", "allow": true},
    {"toolbox": "docker", "tool": "docker.list_containers", "allow": true},
    {"toolbox": "web", "allow": false, "reason": "no outbound HTTP from this workspace"}
  ]
}

func (*JSONPDP) Evaluate

func (j *JSONPDP) Evaluate(_ context.Context, req *PDPRequest) PDPDecision

Evaluate runs the rules in order. First match wins; the rule's Allow field is the decision. If no rule matches, the policy's Default applies.

type JSONPolicy

type JSONPolicy struct {
	Default string       `json:"default"` // "allow" or "deny"
	Rules   []PolicyRule `json:"rules"`
}

JSONPolicy is a minimal-but-real allow-list. Each rule names a toolbox + tool (or "*" for any) and a verdict. First match wins; the default if no rule matches is Default (typically "deny" for safety, "allow" for development).

Why this exists alongside a Rego target: Rego is great for expressive policies (group membership, attribute-based access), but it's heavy and operators often start with "git is allowed, docker is not." The JSON shape covers that without dragging in the OPA Go library.

Migrate to Rego when you find yourself writing predicates the JSON shape can't express (boolean logic across attributes, time-of-day rules, etc.). Same PDP interface; different evaluator.

type MapExpander

type MapExpander map[string]string

MapExpander is the standard PathExpander backed by an explicit map of placeholder name (without the ${}) → expansion. Use NewExpander to construct one with sensible defaults seeded from the current process (HOME, TMPDIR) plus a caller-supplied WORKSPACE.

Lookups for placeholders not in the map fail loud — that's the contract: a typo in a manifest must surface at load time, never silently expand to "".

func NewExpander

func NewExpander(workspace string) MapExpander

NewExpander returns a MapExpander seeded with HOME (from os.UserHomeDir, falling back to $HOME), TMPDIR (from os.TempDir), and WORKSPACE (from the supplied argument). Empty workspace means "no ${WORKSPACE} expansion available" — manifests using it will fail loudly. Callers that have additional placeholders (CACHE_DIR, AGENT_ROOT, …) can copy the map and add to it.

func (MapExpander) Expand

func (e MapExpander) Expand(s string) (string, error)

Expand replaces ${KEY} tokens with their mapped expansions. Strings without "${" pass through unchanged. Unknown placeholders return an error naming the offending input.

type NetworkPolicy

type NetworkPolicy string

NetworkPolicy mirrors sandbox.NetworkPolicy but is the YAML-facing type. Translation is one-way at policy.Apply time.

const (
	// NetworkDeny severs all network access. Default for new
	// manifests; explicit zero value avoids "what does empty mean?"
	// ambiguity.
	NetworkDeny NetworkPolicy = "deny"

	// NetworkOpen leaves network unrestricted. Explicit opt-in only.
	// Auditors should grep for `network: open` in manifests.
	NetworkOpen NetworkPolicy = "open"

	// NetworkLoopback allows 127.0.0.1 only — required for the
	// agent loader's gRPC handshake to reach the plugin's
	// loopback listener, while denying every external connection.
	// Recommended secure default for plugin manifests.
	NetworkLoopback NetworkPolicy = "loopback"
)

type OwnerEntry

type OwnerEntry struct {
	Binary string
	Owner  string
	Reason string
}

OwnerEntry is one row in a registry snapshot.

type PDP

type PDP interface {
	Evaluate(ctx context.Context, req *PDPRequest) PDPDecision
}

PDP is the policy decision point a toolbox dispatch layer consults before invoking a tool. Implementations:

  • AllowAll: always allows; the codefly default while operators migrate. Useful for development.
  • DenyAll: always denies; useful in tests + as a sanity check that the wrap is wired (a flipped flag should refuse every call, surfacing the wrap's effect).
  • JSONPDP: reads a JSON allow-list from disk. Working default for production until a real Rego policy is in place.
  • (operator-supplied) RegoPDP: github.com/open-policy-agent/opa/rego.Rego. Registered via a small adapter the operator writes; codefly does NOT depend on OPA directly (heavy transitive surface).

The interface is intentionally tiny so any of the above is a drop-in replacement.

type PDPDecision

type PDPDecision struct {
	// Allow is the load-bearing field. When false, the toolbox
	// dispatch layer short-circuits with Reason as the user-visible
	// refusal.
	Allow bool

	// Reason is required when Allow is false; ignored when true.
	// Surface verbatim — the model uses it to decide whether to
	// retry with different arguments, escalate to the user, or
	// abandon the path.
	Reason string
}

PDPDecision is what the PDP returns. Allow/Deny are exhaustive — "no decision" maps to Deny per zero-trust. Reason carries a human- readable explanation that surfaces back to the agent so the model understands WHY it was refused (and can plan around it).

type PDPRequest

type PDPRequest struct {
	Toolbox  string         // identity from manifest, e.g. "git"
	Tool     string         // dotted tool name, e.g. "git.status"
	Args     map[string]any // structured arguments (post-decoded)
	Identity map[string]any // caller attribution (agent id, user id, ...)
}

PDPRequest is everything a policy decision point needs to make a call. Mirrors the toolbox CallTool surface so a Rego policy can reason about exactly what's about to happen — toolbox name, tool name, structured arguments, and the calling identity.

Identity is intentionally a free-form map so callers can route whatever attribution context the host has (an agent ID, a user ID, the parent session). The PDP is responsible for interpreting the keys it cares about; unknown keys are ignored.

type PathExpander

type PathExpander interface {
	Expand(s string) (string, error)
}

PathExpander resolves placeholders in path strings: ${WORKSPACE}, ${TMPDIR}, ${HOME}. Implementations are caller-provided so the policy package doesn't need to know how to find the workspace.

type PolicyRule

type PolicyRule struct {
	Toolbox string `json:"toolbox,omitempty"` // "" = any
	Tool    string `json:"tool,omitempty"`    // "" = any (matches with or without dotted prefix)
	Allow   bool   `json:"allow"`
	Reason  string `json:"reason,omitempty"` // surfaced when Allow=false; ignored when true
}

PolicyRule matches a tool call. Empty fields are wildcards (treat as "any"). Allow controls the decision when the rule matches.

type SandboxPolicy

type SandboxPolicy struct {
	ReadPaths   []string      `yaml:"read_paths,omitempty" json:"read_paths,omitempty"`
	WritePaths  []string      `yaml:"write_paths,omitempty" json:"write_paths,omitempty"`
	Network     NetworkPolicy `yaml:"network,omitempty" json:"network,omitempty"`
	UnixSockets []string      `yaml:"unix_sockets,omitempty" json:"unix_sockets,omitempty"`
}

SandboxPolicy is the YAML-shaped permission block a plugin manifest declares. Example:

sandbox:
  read_paths:
    - "${WORKSPACE}"
  write_paths:
    - "${WORKSPACE}"
    - "${TMPDIR}"
  network: deny
  unix_sockets:
    - "/var/run/docker.sock"  # if this plugin needs docker access

Path strings may use ${WORKSPACE}, ${TMPDIR}, ${HOME} placeholders that are expanded at Apply time. Absolute paths pass through.

func (*SandboxPolicy) Apply

func (p *SandboxPolicy) Apply(sb sandbox.Sandbox, expand PathExpander) error

Apply translates a SandboxPolicy into a configured sandbox.Sandbox. The resulting sandbox is ready to Wrap() commands that should run under the policy.

**Mutation contract.** Apply MUTATES sb in-place — it calls the fluent With* setters on the passed-in sandbox. Callers who Apply a policy and then call sb.WithNetwork(NetworkOpen) afterward will override the manifest's intent silently. Recommended pattern:

sb, _ := sandbox.New()
policy.Apply(&pol, sb, expand)   // configure
cmd := exec.Command("...")
sb.Wrap(cmd)                     // use; no further With* calls

Don't share a sandbox across goroutines after Apply unless every goroutine treats it as immutable.

expand is called on every path entry; it should fail loudly when a referenced placeholder is unset rather than silently substituting "" (which would create an unintended catch-all subpath rule).

Paths are expanded then handed to the sandbox in single batched calls per category — the underlying With* methods are variadic and storing many slices' worth of one-element appends is wasteful.

func (*SandboxPolicy) Validate

func (p *SandboxPolicy) Validate() error

Validate checks the policy is internally consistent. Empty policies are allowed (zero-trust default applied at Apply time).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL