A policy is a small decision table. You declare the actors and the resources, then write allow and deny rules over a fixed set of effects. The typed library only authors; the compiled IR decides. This page is the whole vocabulary — every word you can use, and what each one means.
This policy says: the coding agent may never force-push main, and may push main only with a human approval.
// fluent builder → canonical IR
il.Policy("repository-policy.v1").
Actor("agent").
Branch("main", "repo://branch/main").
Deny("deny-force-push-main").By("agent").
To(il.ForcePush).On("main").
Because("force-pushing main is not allowed").Add().
Allow("push-main").By("agent").
To(il.Push).On("main").
Requiring(il.HumanApproval("release-main")).
Because("pushing main needs human approval").Add().
Emit()
// @operatorstack/interlock
policy("repository-policy.v1")
.actor("agent")
.branch("main", "repo://branch/main")
.deny("deny-force-push-main").by("agent")
.to(ForcePush).on("main")
.because("force-pushing main is not allowed")
.allow("push-main").by("agent")
.to(Push).on("main")
.requiring(humanApproval("release-main"))
.because("pushing main needs human approval")
.emit();
# interlock
(Policy("repository-policy.v1")
.actor("agent")
.branch("main", "repo://branch/main")
.deny("deny-force-push-main").by("agent")
.to(FORCE_PUSH).on("main")
.because("force-pushing main is not allowed")
.allow("push-main").by("agent")
.to(PUSH).on("main")
.requiring(human_approval("release-main"))
.because("pushing main needs human approval")
.emit())
// interlock crate
Policy::new("repository-policy.v1")
.actor("agent")
.branch("main", "repo://branch/main")
.deny("deny-force-push-main").by("agent")
.to(ForcePush).on("main")
.because("force-pushing main is not allowed")
.allow("push-main").by("agent")
.to(Push).on("main")
.requiring(human_approval("release-main"))
.because("pushing main needs human approval")
.emit()?;
policy(id) — names the policy with a stable id.actor("agent") — declares who a rule is about. Actor ids are free-form.branch(id, "repo://…") — declares a resource. A branch matches exactly.deny(id) — a named rule that refuses an effect.allow(id) — a named rule that permits an effect.by(actor).to(effect).on(resource) — the rule's subject, verb, and object.because("…") — the reason, returned with every decision.requiring(humanApproval(…)) — an allow that needs evidence. Without it the decision is require, and the effect fails closed.emit() — compiles to the canonical IR: the decision table the engine runs.Every rule points at exactly one effect. The set is small and named; a request for anything outside it is a fault, not a silent allow.
You declare each resource once with an id and a URI, then reference it by id with .on(id). The kind sets how the URI matches.
URIs are a naming convention. repo://, net://, and secret:// just give each resource a stable name; the kind decides how it matches.
You author two kinds of rule. The engine returns one of four outcomes. Rules are tried in order, first match wins, and anything unmatched is denied.
Fail closed. Requirements attach only to allow rules. If an allow matches but its evidence is absent, the outcome is require and the effect does not happen.
Attach one or more to an allow with .requiring(…). Every requirement must be satisfied by the request's claimed evidence, or the effect fails closed.
Actors are free-form ids you declare with .actor(id) and bind to rules with .by(id). Give the coding agent one name and every trusted automation its own — then allow each only what it needs.
A "verified publisher" is just a separate actor whose allow rule carries strong requirements — a pattern, expressed in the same words.
Every policy below uses only the vocabulary above — more actors, more effects, more requirements.
The coding agent cannot change generated code. Only a verified generator may refresh it, and only with evidence.
il.Policy("generated.v1").
Actor("agent").Actor("sdk-generator").
Tree("generated", "repo://generated/**").
Deny("agent-no-write").By("agent").
To(il.Write).On("generated").
Because("the build owns generated files").Add().
Allow("generator-refresh").By("sdk-generator").
To(il.Publish).On("generated").
Requiring(il.VerifiedPublisher("sdk-generator"),
il.ReceiptStatus("codegen.v1", "green")).
Because("only a verified generator may refresh them").Add().
Emit()
policy("generated.v1")
.actor("agent").actor("sdk-generator")
.tree("generated", "repo://generated/**")
.deny("agent-no-write").by("agent")
.to(Write).on("generated")
.because("the build owns generated files")
.allow("generator-refresh").by("sdk-generator")
.to(Publish).on("generated")
.requiring(verifiedPublisher("sdk-generator"),
receiptStatus("codegen.v1", "green"))
.because("only a verified generator may refresh them")
.emit();
(Policy("generated.v1")
.actor("agent").actor("sdk-generator")
.tree("generated", "repo://generated/**")
.deny("agent-no-write").by("agent")
.to(WRITE).on("generated")
.because("the build owns generated files")
.allow("generator-refresh").by("sdk-generator")
.to(PUBLISH).on("generated")
.requiring(verified_publisher("sdk-generator"),
receipt_status("codegen.v1", "green"))
.because("only a verified generator may refresh them")
.emit())
Policy::new("generated.v1")
.actor("agent").actor("sdk-generator")
.tree("generated", "repo://generated/**")
.deny("agent-no-write").by("agent")
.to(Write).on("generated")
.because("the build owns generated files")
.allow("generator-refresh").by("sdk-generator")
.to(Publish).on("generated")
.requiring(verified_publisher("sdk-generator"),
receipt_status("codegen.v1", "green"))
.because("only a verified generator may refresh them")
.emit()?;
A release ships only through the release bot, with two approvals, inside work hours.
il.Policy("release.v1").
Actor("release-bot").
Release("ver", "repo://release/*").
Allow("guarded-release").By("release-bot").
To(il.Deploy).On("ver").
Requiring(il.TwoPersonApproval("ship", 2),
il.TimeWindow("09:00", "17:00")).
Because("releases need two approvals in work hours").Add().
Emit()
policy("release.v1")
.actor("release-bot")
.release("ver", "repo://release/*")
.allow("guarded-release").by("release-bot")
.to(Deploy).on("ver")
.requiring(twoPersonApproval("ship", 2),
timeWindow("09:00", "17:00"))
.because("releases need two approvals in work hours")
.emit();
(Policy("release.v1")
.actor("release-bot")
.release("ver", "repo://release/*")
.allow("guarded-release").by("release-bot")
.to(DEPLOY).on("ver")
.requiring(two_person_approval("ship", 2),
time_window("09:00", "17:00"))
.because("releases need two approvals in work hours")
.emit())
Policy::new("release.v1")
.actor("release-bot")
.release("ver", "repo://release/*")
.allow("guarded-release").by("release-bot")
.to(Deploy).on("ver")
.requiring(two_person_approval("ship", 2),
time_window("09:00", "17:00"))
.because("releases need two approvals in work hours")
.emit()?;
The coding agent may not read secrets or reach the network. Both fail closed.
il.Policy("egress.v1").
Actor("agent").
Endpoint("gh", "net://api.github.com").
Secret("key", "secret://OPENAI_API_KEY").
Deny("no-egress").By("agent").
To(il.NetworkEgress).On("gh").
Because("the agent may not call the network").Add().
Deny("no-secret-read").By("agent").
To(il.SecretRead).On("key").
Because("the agent may not read secrets").Add().
Emit()
policy("egress.v1")
.actor("agent")
.endpoint("gh", "net://api.github.com")
.secret("key", "secret://OPENAI_API_KEY")
.deny("no-egress").by("agent")
.to(NetworkEgress).on("gh")
.because("the agent may not call the network")
.deny("no-secret-read").by("agent")
.to(SecretRead).on("key")
.because("the agent may not read secrets")
.emit();
(Policy("egress.v1")
.actor("agent")
.endpoint("gh", "net://api.github.com")
.secret("key", "secret://OPENAI_API_KEY")
.deny("no-egress").by("agent")
.to(NETWORK_EGRESS).on("gh")
.because("the agent may not call the network")
.deny("no-secret-read").by("agent")
.to(SECRET_READ).on("key")
.because("the agent may not read secrets")
.emit())
Policy::new("egress.v1")
.actor("agent")
.endpoint("gh", "net://api.github.com")
.secret("key", "secret://OPENAI_API_KEY")
.deny("no-egress").by("agent")
.to(NetworkEgress).on("gh")
.because("the agent may not call the network")
.deny("no-secret-read").by("agent")
.to(SecretRead).on("key")
.because("the agent may not read secrets")
.emit()?;
init writes a runnable policy module. compile builds the canonical IR. decide runs any request through it.
$ interlock init --template main-branch
$ interlock compile -o policy.json
$ interlock decide --request request.json
$ interlock doctor