Governance Examples
These examples use the policy fields Punk accepts today:
name,version, optionaldescriptionappliesTo.trustTiers,appliesTo.agentIds, orappliesTo.tags- ordered
ruleswitheffect,actions, and optionalrequiresApproval defaultEffect
Policies live in PUNK_POLICIES_DIR. Rules match actions shaped as <verb>:<resource> such as model:chat, read:crm, write:crm, write:web, and destroy:*.
Observe vs Optimize
Punk API keys have observe or optimize mode. There is no separate policy YAML field for enforcement mode.
| Key mode | Runtime behavior |
|---|---|
observe | Live provider serves the user. Policy denials and approvals are audited with enforcement: "skipped (observe mode)", route explanations can show a ghost blocked route, and optimized/cache routes are recorded as ghost savings instead of being served. Request-time approval verdicts do not create operator approval rows. |
optimize | Policy is enforced. deny and approval_required fail closed, approval work items can be created, and proven caches/artifacts may serve traffic. |
For stateful web actions, observe-mode reads can run, but web writes are refused because form submits and submit-button clicks are real remote writes.
Tool Actions and Side Effects
SDK and model-declared tools should declare sideEffectLevel:
| Level | Runtime meaning | Policy action shape for SDK-style tool names |
|---|---|---|
| 0 | Pure/local computation | read:<tool-namespace> when governed as a read |
| 1 | External read | read:<tool-namespace> |
| 2 | Reversible/idempotent write | write:<tool-namespace> |
| 3 | User-visible write | write:<tool-namespace> |
| 4 | High-impact write | destroy:<tool-namespace> |
For SDK-style names, Punk uses the prefix before the last dot as the namespace. For example, crm.lookup at level 1 maps to read:crm, while crm.updateAccount at level 3 maps to write:crm. Undeclared or unknown tools default to level 3, so they are treated conservatively.
Managed connectors publish their own domain policy actions in docs/CONNECTORS.md such as read:ticket, write:ticket, write:crm, and write:payment. Use those connector action names when governing managed connector tools.
Read-Only Support Agent
Use this when a support assistant may classify, summarize, and retrieve context, but must not change tickets, accounts, files, or execute commands.
name: support-readonly
version: "1.0"
description: Support agent can chat and read context, but cannot write.
appliesTo:
tags: [support-readonly]
rules:
- effect: allow
actions: ["model:chat", "read:*"]
- effect: deny
actions: ["write:*", "destroy:*", "execute:*"]
defaultEffect: deny
Expected outcomes:
| Runtime event | Optimize outcome |
|---|---|
Chat completion, model:chat | Allowed. The response may route live, exact-cache, semantic-cache, artifact, or model-substitution depending on available evidence. |
Support read, for example read:ticket or SDK tool zendesk.get_ticket declared level 1 | Allowed and auditable. Read-only tool results can be eligible for tool caching when configured with a TTL. |
Ticket comment, for example write:ticket or SDK tool zendesk.comment_ticket declared level 3 | Blocked with a policy deny; the tool/write should not execute. |
Shell or command tool, execute:* | Blocked with a policy deny. |
In observe mode, the model request still completes live. The blocked write policy verdict is recorded for review instead of being enforced at the request-routing layer.
Approval-Required CRM Write
Use this when CRM reads are safe, but account changes need a human exception before execution.
name: crm-write-approval
version: "1.0"
description: CRM writes need operator approval.
appliesTo:
trustTiers: [verified, trusted]
rules:
- effect: allow
actions: ["model:chat", "read:*"]
- effect: allow
actions: ["write:crm"]
requiresApproval: true
- effect: deny
actions: ["destroy:*", "execute:*"]
defaultEffect: deny
Expected outcomes:
| Runtime event | Optimize outcome |
|---|---|
read:crm or SDK tool crm.lookup declared level 1 | Allowed. |
write:crm from a managed connector or SDK tool crm.updateAccount declared level 2 or 3 | Fails closed as approval_required. Gateway requests return a 403 punk_approval_required response and the run route is blocked. |
| Pending approval | Punk creates or reuses a policy_exception approval tied to the tenant, app, agent, action, and run. |
| Approved exception | Matching future requests for the same agent/action are allowed until the exception expires. Hard deny rules are not softened by exception approval. |
In observe mode, the same approval verdict is recorded as a ghost blocked/held route, but the live model response is returned and no approval work item is created.
Blocked Private Web Fetch
This example allows public web context reads while blocking web writes. Private-network blocking is not a YAML policy field; it is the always-on URL guard used by SOM fetches, web sessions, workflow web fetches, and redirects.
name: public-web-context
version: "1.0"
description: Allow public web reads, block web writes.
appliesTo:
trustTiers: [unverified, verified, trusted, privileged]
rules:
- effect: allow
actions: ["model:chat", "read:web"]
- effect: deny
actions: ["write:web"]
defaultEffect: deny
Expected outcomes:
| Runtime event | Outcome |
|---|---|
POST /api/v1/web/fetch with https://example.com/pricing | Allowed when the URL is valid and publicly routable. The response includes SOM/context and may use the SOM cache. |
POST /api/v1/web/fetch with http://169.254.169.254/latest/meta-data, http://127.0.0.1:4100, http://10.0.0.1, .local, or .internal | Returns 403 with category: "url_guard". Punk audits action web.fetch with decision deny; the upstream request is not made. |
| Public page redirects to a private/link-local URL | Returns 403 with category: "url_guard" after the redirect target is checked. |
| Stateful web link click that navigates to a private destination | Returns 403 and audits web:action.blocked with governed action read:web. |
| Stateful form submit or submit-button click | Governed as write:web at level 3. In optimize mode, the snippet above blocks it. In observe mode, web writes are refused instead of ghost-allowed. |
Open dev mode is the exception for local demos: when there is no bootstrap API key, no login requirement, no authenticated caller, and no users, private/loopback fetches can be allowed. Authenticated deployments can only opt into private fetches with PUNK_ALLOW_PRIVATE_WEB_FETCH=true.
Level 3 and Level 4 Side-Effect Policy
Use this when user-visible writes should pause for approval and high-impact or destructive actions should be blocked outright.
name: side-effect-boundary
version: "1.0"
description: Human approval for level-3 writes; hard block level-4 destroy actions.
appliesTo:
trustTiers: [unverified, verified, trusted]
rules:
- effect: allow
actions: ["model:chat", "read:*"]
- effect: allow
actions: ["write:*"]
requiresApproval: true
- effect: deny
actions: ["destroy:*", "execute:*"]
defaultEffect: deny
Expected outcomes:
| Runtime event | Optimize outcome |
|---|---|
SDK tool email.send declared level 3 | Maps to write:email, matches requiresApproval, and fails closed as approval_required. |
SDK tool crm.lookup with no side-effect declaration | Defaults to level 3, maps to write:crm, and is held for approval. Declare read-only tools as level 1 when they are truly read-only. |
SDK tool billing.cancelSubscription declared level 4 | Maps to destroy:billing, matches destroy:*, and is denied. |
| Managed connector level-4 billing/payment tool | Govern its published action such as write:payment; level-4 connector tools require approval by default and are suppressed in replay/shadow. |
| Replay or shadow evaluation with a level 2, 3, or 4 side effect | The side effect is recorded as planned/suppressed and is not executed. |
The side-effect gate is conservative even without a matching YAML approval rule: level 4 live actions require approval unless the agent is privileged, and level 3 live actions require approval for unverified agents.
Observe-Mode Skipped Enforcement
This fixture-style policy denies chat for unverified agents. It is useful for proving observe mode before moving a tenant to optimize mode.
name: deny-unverified-chat
version: "1.0"
description: Block chat for unverified agents.
appliesTo:
trustTiers: [unverified]
rules:
- effect: deny
actions: ["model:chat"]
defaultEffect: allow
Expected outcomes:
| Key mode | Runtime outcome |
|---|---|
optimize | The chat request returns 403 with type: "punk_policy_blocked" and code: "policy_blocked". The run has route blocked; route explanation policy decision is deny; audit records the deny. |
observe | The chat request returns 200 from the live provider with x-punk-route: live. The run completes, route explanation keeps policy decision deny, and routeExplanation.ghost.route is blocked. Audit and policy.checked trace payloads include enforcement: "skipped (observe mode)". |
Observe mode also skips serving optimized routes: cache and artifact hits are recorded as ghost savings rather than used for the user response.