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:
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.
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 ¶
func (AllowAllPDP) Evaluate(_ context.Context, _ *PDPRequest) PDPDecision
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 ¶
func (DenyAllPDP) Evaluate(_ context.Context, _ *PDPRequest) PDPDecision
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 ¶
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 ¶
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.
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 ¶
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 ¶
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).