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
argvinto avarwof/exec-v1:<tool>:<action>CLC operation and decides it with the CLC core (packageexecclc/); the bespokemanifestconstraint 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
argvis 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. - Execution —
exec.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
-
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-execis 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. -
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.
-
Capability granularity is the limiting factor.
git:readcannot express "only status/log". The real authorization unit is the tuple(principal, capability, argv, target). AIC already carries aCapability.Parametersfield 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. -
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
-
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 expressingexecas a CLC operation. That bypass is what the convergence removed. -
Policy fragmentation. At the time the stack had three parallel parameter-constraint mechanisms: CLC
RequiredOperations, the aic-verifiermcpmanifestParameterConstraints(min/max/enum/pattern), and theaic-execcommand 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. -
Verdicts were flattened to binary. CLC returns
allow / deny / allow_unresolvedwith 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 (seeexecclc/). -
Evidence is now serial. The MCP layer records
mcp_tools_call; the gate recordsexecwith 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):
- ✅ Express
execas a CLC operation (id = capability, params = projected argv); the projection table replaced the command allowlist. - ✅ Put command constraints in
Capability.Parameters; a grant may carry a fixed params schema and the operation always emits all keys (key closure). - ✅ Carry the projected operation id, argv digest, verdict and normative reason codes in the exec evidence.
- ✅ Respect CLC's non-binary verdicts (
allow_unresolved+ residual obligations):execclc.Envdischargestime:window/network:cidr. - ✅ Wire the CLI form to the real issuance chain: the local run reuses a
persistent issuer directory (
-local-ca DIR, e.g. one produced byaic-exec mint -dir DIR); the claim identity (agent id, issuer/audience, realm) is configurable (run.local.*). The CLI no longer hardcodes its own identity. - ✅ Retired the bespoke manifest constraint language (package
manifestremoved), 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
UnresolvedEvaluatorcan releaseallow_unresolved(a natural hook for human confirmation of destructive commands).
Limits accepted
- 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. - Fixed operation schema (key closure) and
maxParamssize/depth caps. - Scheme grammar is
vendor/product-vN(^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+-v[0-9]+$). The currentaic-exec:exec/git:readids are invalid under CLC;ToGrantrejects them. Migration tovarwof/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. - Projection must match execution. Aliases,
git -C dir status,--, etc. need a deterministic argv → operation projection or the system decidesAand runsB. The vocabulary already exists:semantics.Match/ActionProjector/ActionMappingProfile.
Work list (aic-exec side)
- ✅ Migrate capability ids to
varwof/exec-v1:*(MCPrequired_capabilitymoved to the coarsevarwof/exec-v1:*front-door glob). - ✅ Add a deterministic projector: argv → (opID, params), same source as the
execution path (
execclc/projector.go). - ✅ Replace
manifest.AuthorizedwithAuthorizeOperation; the table became the projection table (execclc/tooltable.go). - ✅ Respect three-valued verdicts; wire the residual-obligation evaluator
(
execclc.Env) — time/network today. - ✅ Record the projected operation id, argv digest, verdict and reason in
evidence (
evidence/). - ✅ Register a custom
ConstraintEvaluator; the built-in example is the user-signer approval hook: a claim that carriesvarwof/constraint-v1:signer:operatoris 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.