Documentation
¶
Overview ¶
THE comparator for "which ceiling grant bounds this proposed grant".
It lives here, in composer, because this package holds the RUNTIME clamp (clampGrants) and internal/api — which holds the WRITE-TIME comparator (governanceGrantWithinCeiling) — imports composer rather than the other way round. One definition, two callers: the write-time comparator refuses a profile grant no ceiling grant dominates, the runtime clamp narrows a run's grant to the bound of the ceiling grant that covers it, and both ask THIS file which ceiling grant that is.
They used to ask differently, and disagreed on the axis that matters. The comparator searched per-grant ("some single ceiling grant dominates on every axis"); the clamp built `map[GrantKind]GrantSpec` and let the LAST same-kind ceiling grant win, then took RequiresApproval and TTL from that arbitrary grant. A ceiling listing two ssh_key grants — the normal shape for two forges, and equally normal for api_key and git_pat — therefore clamped a proposal naming the STRICT forge's pairing to the PERMISSIVE forge's approval posture and TTL, and returned different answers for the same ceiling SET depending on slice order. A stripped requires_approval auto-mints the injection at proxy boot with no human in the loop, which is the widening the write-time comparator was written to refuse and the runtime clamp was quietly allowing.
Index ¶
- Constants
- func CapDiskMiB(disk, ceil int) int
- func CeilingGrantsCovering(g types.GrantSpec, ceiling []types.GrantSpec) []types.GrantSpec
- func Clamp(proposed, ceiling types.RunPolicySpec, maxEphemeralDiskMiB int) (types.RunPolicySpec, []string)
- func ClampRunConfinement(runClass string, floor types.ConfinementClass) (string, string)
- func EffectiveConfinementFloor(policyMin, floor, cap types.ConfinementClass) types.ConfinementClass
- func GitHubScopeWithin(proposed, ceiling json.RawMessage) error
- func GrantPairing(g types.GrantSpec) (host, secretRef, knownHostsRef string, covered, ok bool)
- func PairingInCeiling(g types.GrantSpec, ceiling []types.GrantSpec) bool
- func RequiredConfinementFloor(spec types.RunPolicySpec) types.ConfinementClass
- type RiskItem
- type RiskLevel
- type RunInput
Constants ¶
const WarnResourcesCapped = "resources capped to operator maximum"
ClampRunConfinement raises a proposed run's confinement class up to the clamped policy floor so the composer never emits a self-inconsistent proposal: a run advertising a WEAKER class than its inline_policy's MinConfinementClass would be rejected 422 by handleCreateRun (invariant 5, fail closed). It ONLY strengthens — a run that legitimately asked for a class STRONGER than the floor is left as-is — and an empty/unknown run class ranks 0, so it too is raised to the floor. Returns the (possibly raised) class and a non-empty warning when it tightened. WarnResourcesCapped is what a caller is told when a resource field was bounded by the operator's ceiling. It is a const because dispatch's own ephemeral-disk clamp says the SAME thing about the SAME number (runs_dispatch_ceiling.go) — two hand-typed copies of one sentence is how a preview stops matching the run.
Variables ¶
This section is empty.
Functions ¶
func CapDiskMiB ¶ added in v0.7.2
CapDiskMiB bounds a NON-ZERO ephemeral-disk size by ONE operator ceiling, with 0 on either side meaning "no bound". It is ONE ceiling's min(), in one place, for the callers that apply a ceiling: Clamp above (what POST /runs/preflight and the Review rail show) and api.ephemeralDiskFor (the fill + BOTH ceilings the sandbox gets, which the preview now calls too).
Sharing this function is NOT what makes the preview and the run agree, and the comment that said so was how they came to disagree: the preview applied the profile ceiling and dispatch applied the org's as well, so a 100000 MiB policy under a 4096 MiB org maximum previewed 100000 and ran on 4096. The agreement is pinned by api.TestPreflightAndDispatchAgreeOnEphemeralDisk, not argued.
A ZERO disk STAYS ZERO. A ceiling bounds a request; it does not invent one.
func CeilingGrantsCovering ¶ added in v0.7.0
CeilingGrantsCovering returns the ceiling grants whose IDENTITY covers g: the same kind, and — for the kinds that name a stored secret — the same (host, secret, known_hosts) pairing.
This is the SELECTION half of "is this grant within the ceiling", and it is exported because internal/api's write-time comparator (governanceGrantWithinCeiling) must select from the same set the runtime clamp bounds against. F014 was that selection existing twice; round 1 shared only the pairing PREDICATE (PairingInCeiling) and left the two searches standing, and they drifted again within the round — on github_token, whose scope names no pairing at all.
Identity, never bounds: approval, TTL and github scope are what a clamp NARROWS and a comparator REFUSES, so they are the caller's question, not this one's. An undecodable proposal scope names no pairing and is therefore covered by NOTHING (fail closed); github_token and cloud_sts name no stored secret, so same-kind membership is the whole of their identity and the repo/permission subset is a bound axis handled by the caller.
func Clamp ¶
func Clamp(proposed, ceiling types.RunPolicySpec, maxEphemeralDiskMiB int) (types.RunPolicySpec, []string)
Clamp tightens a proposed RunPolicySpec to the operator's policy CEILING so the composer can never propose something more permissive than the operator allows. It returns the clamped spec and a human-readable warning for every tightening it performed. This is defense-in-depth on top of the deterministic risk grade: the grade informs the human, the clamp enforces the operator's hard limits regardless of what the (untrusted-input-driven) analyzer OR a member's own hand-authored inline_policy proposed.
The returned spec OWNS its memory: it shares no backing array and no pointee with EITHER argument, so an in-place mutation of the caller's proposal (or of the clamped spec) can never move a ceiling this function already enforced. See cloneProposal.
Clamps applied:
- confinement raised to the operator's minimum class if the proposal is weaker;
- allow_all_egress forced off unless the ceiling permits it;
- allowed_domains intersected down to the ceiling's allowlist (unless the ceiling itself allows all egress);
- the ceiling's denied_domains unioned in (deny always wins);
- first_use_approval raised to the STRICTER of the two (always_deny > deny_with_review > wait_for_review) — an unset ceiling ranks as always_deny, its own documented fail-closed default (FirstUseMode.Normalize);
- llm_inspection: when the ceiling sets one, the proposal unconditionally inherits it — nil means OFF, so an omitted/weaker proposal is exactly the "disable the operator's prompt-inspection guardrail" escalation, not "no opinion". A ceiling that sets NONE is likewise the FLOOR (nil), not "no opinion" — symmetric with the workspace_mounts drop below: a member's own hand-authored llm_inspection (detector_sidecar_url, intercept_tls, ...) must never survive a nil ceiling unclamped. The inherited copy's workspace_secret_values is always zeroed — a proposal/audit event never needs resolved secret values, only the names (see types.LLMInspectionSpec);
- allowed_methods: empty means "all" (unlike allowed_domains' default-deny), so an empty proposal ADOPTS the ceiling's list when the ceiling sets one, and a non-empty proposal is intersected down to it;
- ui_apps: intersected down to the ceiling's (name, port) pairs when the ceiling declares any, and untouched when it declares none — an empty proposal is the NARROWEST state for this field, so it never adopts;
- resources: each of cpu/memory/pids/disk capped at the ceiling's when the ceiling sets one — an unset (zero) proposed field is the PERMISSIVE state here (filled in by the driver's own default later), so it is capped down exactly like an explicit value that exceeds the ceiling;
- auto_stop_after_sec capped at the ceiling's maximum when the ceiling sets one — 0 (platform default) and negative (never reap) both rank as MORE permissive than a real cap and are capped down too. A ceiling with NO positive maximum leaves the field untouched: 0 and negative are the same run to internal/lifecycle's reaper (it skips every value <= 0), so a run under such a ceiling is never idle-reaped whichever one it carries;
- grants of a kind the ceiling does not list are dropped; github permissions intersected down to the ceiling's github permissions; TTL capped; and requires_approval forced on when the ceiling requires it;
- workspace_mounts dropped entirely — host mounts are operator-authored and a composer (fed untrusted input) must never be able to introduce one.
maxEphemeralDiskMiB is the acting principal's GovernanceLimits. MaxEphemeralDiskMiB (0 = unlimited), and it is a separate argument rather than a field of the ceiling SPEC because that is what it is: a limit beside the ceiling, not inside it. It exists here for PREVIEW PARITY — dispatch is the one site that binds the limit for every lane (runs_dispatch_ceiling.go's applyEphemeralDisk), so without this argument POST /runs/preflight and the New Run Review rail would show a size the run then silently does not get. The two share one min() (CapDiskMiB) so they cannot drift.
It CLAMPS A NON-ZERO REQUEST ONLY — deliberately not capField's zero-fill idiom. A zero disk_mib means "unbounded scratch" and must stay that way: the only thing that fills a zero is the org's own storage.ephemeral. default_disk_mib, at dispatch, because filling from a MAXIMUM would hand every request-less run the ceiling as its size and the docker driver fails a create closed on overlay2-over-ext4 (every laptop) the moment DiskMiB is non-zero.
func ClampRunConfinement ¶
func ClampRunConfinement(runClass string, floor types.ConfinementClass) (string, string)
func EffectiveConfinementFloor ¶
func EffectiveConfinementFloor(policyMin, floor, cap types.ConfinementClass) types.ConfinementClass
EffectiveConfinementFloor combines the operator policy's minimum confinement class with the operator's per-run compose floor (the Getting Started default tier, sent RAISE-ONLY on the request) and returns the class to floor the proposal at: the PER-RUN floor is first capped at the strongest class this host's runner can actually enforce, THEN max(policyMin, cappedFloor). The cap degrades the PER-RUN request floor ONLY — it keeps a CC3 default tier on a Fence-only host from flooring a composed run into a launch-time 422 at the confinement gate (internal/api/runs.go). It must NEVER lower the operator's configured policy minimum: an unenforceable POLICY min still fails closed at launch (the manual create-run path 422s it — invariant 5), and silently degrading it here would make compose the one path that bypasses an operator security control. cap=="" (unknown runner caps) means "do not cap". Feeding the result as Clamp's ceiling MinConfinementClass makes the raise flow through Clamp's EXISTING "confinement raised ..." warning — no new channel, so the review's "Tightened by policy" panel renders it with zero new UI.
func GitHubScopeWithin ¶ added in v0.7.0
func GitHubScopeWithin(proposed, ceiling json.RawMessage) error
GitHubScopeWithin reports whether a PROPOSED github_token scope stays inside a CEILING's, by the exact rules clampGitHubScope enforces above: the ceiling's repo list is an ALLOWLIST (an empty one is deny-all, W23-S1-3), a permission absent from the ceiling map is not grantable at any level (M6), and a present one bounds the level by permRank.
It lives HERE, beside the clamp it must agree with, and calls that clamp's own permRank and toSet — because its caller is a comparator that ACCEPTS a scope the clamp will later honor at mint. A second copy of these rules in the caller's package drifts in exactly the direction that matters: accept there, widen here. There is nothing to keep in sync because there is one copy.
It differs from the clamp in one deliberate way: an UNDECODABLE scope is an error, not an empty one. The clamp tolerates it because it intersects toward empty (fail-closed); a comparator reading it as "no constraint" fails OPEN.
func GrantPairing ¶ added in v0.7.0
GrantPairing returns the pairing a grant names and whether its kind names a stored secret at all.
covered=false is the honest answer for github_token and cloud_sts: they name no stored secret (github_token mints an App installation token, cloud_sts is refused by the embedded IdP), so same-kind membership IS their identity test and the github repo/permission subset is checked separately.
ok=false means the scope did not decode into a usable pairing. It is deliberately NOT an error return: this file answers "does this ceiling entry cover this grant", and an unreadable scope simply cannot be said to cover anything — so it never MATCHES, which is the fail-closed direction. Whether an unreadable scope is a REQUEST error is a different question, answered where a grant is validated for delivery (internal/api's storedSecretGrantPairing, which fails such a grant closed).
env_secret puts the env var NAME in the host slot, so the ceiling comparison is an exact (name, secret) pairing rather than a secret a member could re-home to a variable the operator never wrote.
func PairingInCeiling ¶ added in v0.7.0
PairingInCeiling reports whether some ceiling grant of the SAME kind names the same stored-secret pairing. This is the identity axis of the one comparator — internal/api's storedSecretPairingInCeiling forwards to it, so the member grant filter, the governance profile bound and the runtime clamp cannot drift into three answers for one question.
It takes the whole proposed grant rather than a destructured pairing so the pairing rule lives in exactly ONE decoder (grantPairingOf). It used to take (kind, host, secretRef, knownHostsRef), which is why an api_key axis added to the pairing — header/format, F097 — could not reach this caller at all. A grant whose kind names no stored secret, or whose scope does not decode, is in no pairing (fail closed); its kind-level ceiling membership is the caller's question (CeilingGrantsCovering).
func RequiredConfinementFloor ¶
func RequiredConfinementFloor(spec types.RunPolicySpec) types.ConfinementClass
RequiredConfinementFloor returns the DETERMINISTIC minimum confinement class a run's BLAST RADIUS requires — independent of what the model proposed or the operator picked. A run that holds POWERFUL credentials is itself a high-value compromise target: if a prompt-injected agent escapes the sandbox, it takes those credentials (and your host) with it. Such a run must therefore run in the STRONGEST sandbox (Vault / CC3) so an escape is contained. "Powerful" means the run can mutate external/production systems or authenticate to third-party services:
- a WRITE-CAPABLE grant (cloud STS, or a GitHub token with write/admin), or
- an api_key to a host OUTSIDE the safe coding-agent baseline — i.e. a database, deploy API, or other third-party production credential (the agent's own model/VCS api_keys are baseline and do NOT floor).
Returns "" when no floor above the policy default applies. Enforced BOTH in the composer proposal and (defense-in-depth) at run.create, where a host that can't provide CC3 then fails closed rather than running the workload under-confined.
Types ¶
type RiskItem ¶
type RiskItem struct {
Field string `json:"field"`
Value string `json:"value"`
Level RiskLevel `json:"risk_level"`
Rationale string `json:"rationale"`
InvariantRef string `json:"invariant_ref,omitempty"`
}
RiskItem is one graded config choice. Field/Value identify what was graded, Level is Wardyn's deterministic grade, Rationale explains it to the human, and InvariantRef (optional) cites the security invariant the choice bears on.
func Grade ¶
func Grade(run RunInput, spec types.RunPolicySpec) []RiskItem
Grade computes the deterministic risk assessment of a proposed run setup PURELY from its fields. It NEVER consults any LLM self-assessment — a prompt-injected attachment cannot lower the grade because the grade is a function of the spec, not of anything the model claims about it. It emits one item per notable choice (including LOW ones) so the human sees the full picture, sorted riskiest-first.
type RiskLevel ¶
type RiskLevel string
RiskLevel is Wardyn's deterministic grade for a single config choice.
func OverallLevel ¶
OverallLevel returns the highest level among items (low if none).
type RunInput ¶
type RunInput struct {
Agent string `json:"agent"`
Repo string `json:"repo"`
Task string `json:"task"`
ConfinementClass string `json:"confinement_class,omitempty"`
Interactive bool `json:"interactive,omitempty"`
DevcontainerRepo string `json:"devcontainer_repo,omitempty"`
}
RunInput is the scalar create-run fields graded by Grade — the same fields the wizard's buildSpec puts on `run`. Mapped onto the create-run request by the API layer (this package must not import internal/api).