# Govern Resource usage

> Return typed Resource controls and hard admission limits from executable Policy packages.

## Before you begin {#before-you-begin}

Governance narrows an invokable Resource's immutable baseline. It does not change Resource configuration, provider selection, pricing, credentials, or wallet balance. Identify the target Resource's governance contract and decide which controls or admission limits the Policy must narrow. Unknown controls, meters, contracts, or unenforceable combinations deny before dispatch.

## Steps {#steps}

1. Use the contract's typed helpers from executable `policy()` code. This example applies sandbox controls and a customer-scoped concurrency limit:

   ```ts
   import { policy, sandboxControls, sandboxLimit } from "@constal/sdk";

   export default policy({
     id: "customer-sandbox-boundary",
     version: "1.0.0",
     evaluate(input) {
       const allowed = input.action === "resource:invoke"
         && input.context["resource.operation"] === "exec";
       return allowed
         ? {
             kind: "constrain" as const,
             constraints: [
               sandboxControls({
                 cpu: 2,
                 memory_gib: 4,
                 command_timeout_ms: 120_000,
                 network_mode: "domain-allow",
                 network_domains: ["api.example.com"],
               }),
               sandboxLimit({
                 id: "customer-concurrent-commands",
                 meter: "commands",
                 scope: "customer",
                 window: { kind: "concurrency" },
                 maximum: 3,
                 continuity: "target",
               }),
             ],
           }
         : { kind: "deny" as const, code: "operation-denied", reason: "operation denied" };
     },
   });
   ```

   `sandboxControls()` and `sandboxLimit()` pin the exact built-in sandbox governance contract. `modelControls()` and `modelLimit()` do the same for Models. A custom Resource package publishes its contract and typed helpers together.
2. Choose a trusted scope for each admission limit: `run`, `session`, `subject`, `customer`, `tenant`, `binding`, or `resource`. The platform derives the identity from authenticated authority and the accepted Run snapshot; Policy code and Agent arguments cannot supply it.
3. Choose a concurrency window for simultaneous work, a fixed window for bounded rates, or a lifetime window for a durable total. `continuity: "target"` preserves the limit authority across ordinary Policy and Resource revisions. Use `target-version` only when a Resource revision intentionally needs a separate counter.
4. Unit test an allowed invocation and failures for an unknown contract, control, meter, or unenforceable combination.

## Limit concurrent Runs {#run-limits}

`agentLimit()` pins the built-in `constal.agent` contract, whose `runs` meter counts the Runs an Agent is running at the same time. Return it from a Policy that targets the Agent and governs `agent:invoke`. The Session enforces it where each top-level Run starts, whatever sent the request:

```ts
import { agentLimit, policy } from "@constal/sdk";

export default policy({
  id: "plan-3",
  version: "1.0.0",
  evaluate(input) {
    if (input.action !== "agent:invoke") return { kind: "allow" as const };
    return {
      kind: "constrain" as const,
      constraints: [
        agentLimit({
          id: "worker-runs",
          meter: "runs",
          scope: "customer",
          window: { kind: "concurrency" },
          maximum: 3,
          continuity: "target",
          overLimit: "queue",
        }),
      ],
    };
  },
});
```

- `overLimit` says what happens when the limit is full. `reject`, the default when it is absent, refuses the start. `queue` makes the request wait in its Session's inbox, in an ordered [Run queue](/docs/api/run-controls.md#run-queues), until a slot frees. `queue` requires a concurrency window and `continuity: "target"`, so the queue survives Agent rollouts. `agentLimit()` accepts only a concurrency window.
- The supported scopes are `session`, `subject`, `customer`, `tenant`, and `resource`. A Run limit scoped to `run` or `binding` has no identity when the Run starts and is refused with `PolicyConstraintFailed`.
- A Run is governed by at most one Run limit. A start whose Policies state more than one is refused with 403 `PolicyConstraintFailed`.
- A slot is held for the Run's whole life. It is freed when the Run completes, fails, stops, or is cancelled, or when its Session is deleted. Pause, truncate, and rebind do not end a Run, so a paused Run keeps its slot. Branch Runs and child Runs are part of their parent's work and hold no slot.
- A Run stopped at its `maxTurns` or `maxMicroUsd` limit frees its slot too. A [policy control](/docs/api/run-controls.md) that resumes it takes a slot again first; it never waits, and while the limit is full it is refused with 429 `AdmissionLimitExceeded` and the Run stays stopped.
- A Session admits its inbox in order, so at most one request per Session waits. Requests that arrive behind it are decided `queued` and stay in the inbox.
- A limit is identified by its `id`, the Agent, the scope identity, and the window, not by the Policy that states it. Plan Policies that state the same `id` share one counter and one queue. A `session` identity is a Session within its Customer, so Customers whose Sessions have the same id never share a limit.

### Change a Customer's plan {#change-plan}

Bind each Customer to one plan Policy through [Customer Policy bindings](/docs/api/customers.md#steps). Every plan states the same limit, for example `worker-runs` with scope `customer` and `overLimit: "queue"`, and its own `maximum`: `plan-1` allows 1, `plan-3` allows 3, and `plan-10` allows 10. To change plans, bind the new plan Policy and delete the old binding.

- While two plans are bound, the lowest maximum applies.
- The limit uses the maximum carried by the most recent new request. Raising it grants waiting requests head-first up to the new maximum; an arriving request starts only if capacity is left and no request is waiting.
- Lowering it does not preempt. Held slots stay held until their Runs end, and nothing more is granted until fewer Runs than the new maximum hold slots.
- Waiting requests keep their positions across a change.
- Rebinding advances the Customer's `authorizationSeq`. A waiting Agent send (`ctx.send()` or a schedule) whose sender authority predates the change is refused when it starts with 403 `initiating authority is no longer current`, as sends queued behind a live Run already are. Channel and HTTP events keep their stored authority and start normally.
- An event whose stored authority predates the change applies the old maximum when it first acquires, and the next request carrying the new authority sets it back. Policies that state the same limit for one scope identity should state one maximum.

### What senders see {#run-limit-outcomes}

| Situation | Decision (`GET /events/:id`, HTTP invoke) | Agent send (`agent.result`) | Channel |
| --- | --- | --- | --- |
| Admitted | `accepted` | the Run's result | unchanged |
| Waiting | 202 `{ outcome: "queued", admission: { state: "waiting", queue, scope, position }, runId?, run? }` | nothing yet; the Handle stays pending | nothing |
| Rejected | 429 `{ error: "AdmissionLimitExceeded", queue, scope, maximum }` | `status: "failed"` | `channel.failed` with code `AdmissionLimitExceeded` |
| Dropped | 409 `{ error: "RunQueueItemDropped", detail, actor }` | `status: "stopped"`; the Handle is cancelled | `channel.failed` with code `RunQueueItemDropped` |

A waiting request keeps the Run id its sender reserved; a request that reserved none has no Run id while it waits.

## Verify {#verify}

Invoke the selected Resource and confirm the invocation records its effective governance hash and admission receipts. Confirm the provider receives only effective controls for the exact contract and returns enforcement evidence. The platform must reserve non-monetary admission limits and confirm a positive account balance before the external effect; exact settled usage is debited afterward under a stable ID. Provider-specific compute, network, filesystem, token, or connection enforcement stays inside the provider integration.

## Next steps {#next-steps}

Continue with [Author a Policy](/docs/policies/author.md) for the package and target-selector workflow, or [Operate Policies](/docs/policies/operate.md) to inspect selected Resources. Governance uses the existing selector, Policy, Resource invocation, and provider path; it does not introduce a second limit service or Resource-specific admission endpoint.
