aic-exec — AIC / CLC meaning (EN)EN

What aic-exec tells us about AIC and CLC

Status: design note, local development (uncommitted).

Update. The convergence described here has landed: the command gate now projects argv into a varwof/exec-v1:<tool>:<action> CLC operation and decides it with the CLC core (package execclc/); the bespoke manifest constraint language is gone. The "Meaning for CLC" and "Work list" sections below are kept in their original pre-migration form as the record of why the refactor was necessary, followed by the design that was implemented.

The note records what the first working aic-exec implementation (CLI + MCP command executor gated by aic-verifier) proved, and what it exposed, about the AIC authorization model and the CLC-v1 constraint language. The pre-migration gate was a locally authored manifest decided by aicverifier.MatchCapability (glob) — that is the subject of the analysis below.

What is implemented

aic-exec gates command execution on an AIC credential:

  • Admission (who) — an AIC X.509 client certificate (mTLS) or a Bearer AIC-JWT is verified by aic-verifier; the identity and capability set come from the credential.
  • Command (what, CLC) — the requested argv is projected by the projection table into a CLC-v1 operation (id + params) and decided against the caller's AIC grants. The table only names and parameterizes a command; CLC decides it.
  • Executionexec.CommandContext, no shell, bounded timeout and output.
  • Evidence — one record per invocation over the aic-verifier audit sink, carrying the projected operation id, argv digest and CLC verdict.

Meaning for AIC

  1. The enforcement surface moves from the connection edge to the action edge. Until now AIC decided whether a caller could connect or call an RPC/MCP tool. aic-exec is the first case where AIC decides whether a local process may run a specific command. AIC is thereby demonstrated as an agent-runtime primitive, not only a gateway credential.

  2. One credential, any agent — demonstrated. claude, codex and opencode need no per-agent plugin: the control point is the credential, and any agent that can run a command or register an MCP server inherits it. This is the concrete advantage of AIC over per-agent plugin/permission systems.

  3. Capability granularity is the limiting factor. git:read cannot express "only status/log". The real authorization unit is the tuple (principal, capability, argv, target). AIC already carries a Capability.Parameters field that could hold this self-limiting detail (git:read + params:{subcommands:[status,log]}); the current implementation does not use it and puts the constraint in the external manifest instead.

  4. Issuance is the hard part, not enforcement. The CLI form bootstraps a private local CA; that is a placeholder trust root. AIC's value only holds on the real issuance chain (user-signer → core). The current implementation is honest about this but does not exercise it.

Meaning for CLC

  1. It was both the demand proof and the bypass evidence for CLC. Command execution is intrinsically an operation (capability id + parameters), which is exactly what CLC-v1 was built for (clc.go: "glob matching cannot see parameters: a grant of max 10 rows and a request for 50 rows are the same declaration"). The first aic-exec proved the demand — and, in the same breath, declined to use it: it invented a second parameter-constraint language (the manifest allowlist) instead of expressing exec as a CLC operation. That bypass is what the convergence removed.

  2. Policy fragmentation. At the time the stack had three parallel parameter-constraint mechanisms: CLC RequiredOperations, the aic-verifier mcp manifest ParameterConstraints (min/max/enum/pattern), and the aic-exec command allowlist. The convergence collapsed the third into the first, leaving CLC the single substrate for the command gate while the MCP manifest keeps only its coarse admission front door.

  3. Verdicts were flattened to binary. CLC returns allow / deny / allow_unresolved with normative reason codes and residual obligations, plus CLC-E evidence sufficiency. The gate now carries the three-valued verdict and the normative reason, and releases residual obligations (see execclc/).

  4. Evidence is now serial. The MCP layer records mcp_tools_call; the gate records exec with the projected CLC operation id, argv digest, verdict and deny reason, so CLC's decision → action → outcome closes into one chain per invocation.

Combined conclusion

The durable asset is three unifications: credential (AIC), policy (CLC), attribution (evidence). aic-exec shows this can reach the OS process boundary; it also showed that to hold, exec had to be re-expressed as a CLC operation.

Convergence (all landed):

  1. ✅ Express exec as a CLC operation (id = capability, params = projected argv); the projection table replaced the command allowlist.
  2. ✅ Put command constraints in Capability.Parameters; a grant may carry a fixed params schema and the operation always emits all keys (key closure).
  3. ✅ Carry the projected operation id, argv digest, verdict and normative reason codes in the exec evidence.
  4. ✅ Respect CLC's non-binary verdicts (allow_unresolved + residual obligations): execclc.Env discharges time:window / network:cidr.
  5. ✅ Wire the CLI form to the real issuance chain: the local run reuses a persistent issuer directory (-local-ca DIR, e.g. one produced by aic-exec mint -dir DIR); the claim identity (agent id, issuer/audience, realm) is configurable (run.local.*). The CLI no longer hardcodes its own identity.
  6. ✅ Retired the bespoke manifest constraint language (package manifest removed), so CLC is the only command-authorization language.

One line: for AIC, aic-exec is evidence that the enforcement surface can be extended to the action edge; for CLC, it proved the need and then converged on CLC as the single language of command-level authorization.

Convergence design: command authorization on CLC

This section records how the command gate was replaced by CLC, verified against the SDK surface. It is now the implemented design (execclc/), kept here as the rationale.

Why it is feasible: CLC decides per request already

The decision core is a pure function, not a static config lookup:

aicverifier.AuthorizeOperation(aic *AIC, pa *PrincipalAuthorization, opID string, params map[string]any) (semantics.Decision, error)
aicverifier.AuthorizeCapabilitiesWithConstraints(caps, constraints []Capability, opID string, params map[string]any) (semantics.Decision, error)

Given an AuthContext, aic-exec can decide the actual argv of this call with no request-level pipeline and no config rewrite. That is the whole enabler.

Mapping

Today On CLC
manifest git:read + argv allowlist capability id varwof/exec-v1:git:status; a grant may carry varwof/exec-v1:git:* (trailing wildcard is allowed by matchID)
operation id projected from argv: <exe> / <exe>:<subcommand>
argument rules operation params with a fixed schema: {"timeout":30}, {"flags":["--oneline"]}, {"target":"/srv/x"}
"command not authorized" Decision.Reason: capability_not_authorized, params_exceed_grant, params_missing, undeclared_param, not_in_enum
allow / deny allow / deny(reason) / allow_unresolved(obligations)
bespoke manifest policy manifest degrades to a projector/compiler (argv → opID + params); policy lives only in the AIC grants
exec evidence without verdict evidence carries OperationDecision / Grants / Verdict / Reason

Argument rules the enum/bound model cannot express (e.g. positional argv prefixes) can be enforced via a custom ConstraintEvaluator (aic-verifier's ConstraintRegistry interface; no ASN.1 or routing change). A registered type is evaluated by the executor before CLC and its outcome is authoritative; an unregistered type keeps the core's hard unknown_constraint deny. This is the fail-closed that matters: an unknown obligation is never silently dropped and never reaches allow_unresolved (the core recognizes only max_rows/time:window/network:cidr). Built-in user-signer example: varwof/constraint-v1:signer:operator prompts before running.

Semantics that shape the design

  • Strings compare by exact equality; numbers are upper bounds (op ≤ grant); arrays are enumeration sets (an op scalar must be a member; an op array is checked element-wise), not sequences.
  • Key closure: the grant's params keys must all be present in the operation and the operation's keys must be declared by the grant. The operation param schema is therefore fixed.
  • {} (or absent) means unconstrained; [] denies the whole class.
  • The verdict is three-valued and carries residual obligations; an UnresolvedEvaluator can release allow_unresolved (a natural hook for human confirmation of destructive commands).

Limits accepted

  1. Arrays are sets, not sequences. Order is not constrained, so raw argv must not be dumped into params. Decompose to the capability id (git:status) and keep params to bounded scalars + declared enums.
  2. Fixed operation schema (key closure) and maxParams size/depth caps.
  3. Scheme grammar is vendor/product-vN (^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+-v[0-9]+$). The current aic-exec:exec / git:read ids are invalid under CLC; ToGrant rejects them. Migration to varwof/exec-v1:* is mandatory — and the grants user-signer/core issue must be CLC-valid and carry params, so this is a cross-repo precondition, not an aic-exec-local change.
  4. Projection must match execution. Aliases, git -C dir status, --, etc. need a deterministic argv → operation projection or the system decides A and runs B. The vocabulary already exists: semantics.Match / ActionProjector / ActionMappingProfile.

Work list (aic-exec side)

  1. ✅ Migrate capability ids to varwof/exec-v1:* (MCP required_capability moved to the coarse varwof/exec-v1:* front-door glob).
  2. ✅ Add a deterministic projector: argv → (opID, params), same source as the execution path (execclc/projector.go).
  3. ✅ Replace manifest.Authorized with AuthorizeOperation; the table became the projection table (execclc/tooltable.go).
  4. ✅ Respect three-valued verdicts; wire the residual-obligation evaluator (execclc.Env) — time/network today.
  5. ✅ Record the projected operation id, argv digest, verdict and reason in evidence (evidence/).
  6. ✅ Register a custom ConstraintEvaluator; the built-in example is the user-signer approval hook: a claim that carries varwof/constraint-v1:signer:operator is evaluated by a registered evaluator before CLC (the core would otherwise hard-deny the unknown type), which prompts the operator on the terminal. Declined or interrupted approval denies fail-closed (run.local.signer, CLI form).

What does not move to CLC

"Authorization fully on CLC" is not "the component is only CLC":

Responsibility Owner Why it stays
Credential verification / identity (authn) AIC + aic-verifier CLC consumes grants; it does not verify the AIC. Authentication is not authorization.
argv → operation projection aic-exec CLC decides only the (id, params) it is handed. A wrong projection is a decide-A/run-B gap CLC cannot catch. This becomes the trusted component.
Enforcement (run the process, kill on timeout) execer CLC can declare ≤30s; it does not terminate the process. Runtime enforcement is separate.
Evidence sink (write / rotate) aic-verifier audit CLC produces the record content; persistence is not CLC.
Trust root / issuance aic-exec (dev) + user-signer/core The CLI's local CA is a placeholder; real authority comes from the issuance chain.
MCP-layer required_capability gate aic-verifier/mcp A glob (MatchCapability), not CLC — a coarse admission front door. In aic-exec it is now narrowed to varwof/exec-v1:*; the parameter decision belongs to CLC. Two engines still interpret the same capability id unless it is narrowed to admission-only.

So: is it "fully on CLC" afterwards?

  • Policy (authorization): yes — one language, three-valued verdicts, attributable.
  • Everything else (authn, projection, enforcement, evidence, trust root): no, and it should not. CLC does not authenticate, does not project, does not execute, does not persist.

The accurate statement: after the change, CLC is the single policy language of command-level authorization, and aic-exec is its argv projector + execution hand + evidence recorder. The projector is thereby promoted to a must-be-trusted component — which is the real work and risk of the refactor.