aic-agent — README (EN)EN
aic-agent
Go client SDK for AIC-protected services. The consumer-side counterpart of
aic-verifier: it mints and carries the
agent's AIC credential and calls services protected by AIC authorization.
Status: early. The API, and the CLC revision it evaluates (currently CLC-1.8), may change before the first stable release.
Documentation: start at docs/index.md — quickstart, API, reference, architecture, threat model, deployment, examples.
Stability. Frozen until v1.0 (additive-only): Agent and its Do/Close
lifecycle, Config, the identity/bearer/transport package entry points,
and the request/response types they exchange. Experimental (may change in a
minor release): the evidence-transport helpers and anything under examples/.
What it does
An agent (a service-to-service caller or an autonomous agent) needs to prove
an AIC credential to reach an API protected by aic-verifier. This SDK covers
the whole consumer side:
- identity — generate/load the agent key (P-256 ECDSA or Ed25519),
derive SPKI (kid) and RFC 7638 JWK thumbprints (jkt for
cnf), and build the AIC principal. - bearer — mint a local Bearer AIC-JWT (
aic+jwt) signed with the agent key, and exchange a delegation-authorization (DA) assertion for a short-lived token at an issuer token endpoint (RFC 7523 jwt-bearer grant). - transport —
http.RoundTrippers that attach the credential: a Bearer header transport with refresh-on-401, and an mTLS transport with an AIC-bearing client certificate. - aicagent — one-stop
Agentclient combining identity, minting and the HTTP transports.
Install
go get github.com/varwof/aic-agent@latest
Requires Go 1.26+. It builds against the published github.com/varwof/types
v0.6.0 and github.com/varwof/register v0.6.0; no local replace
directives are needed.
Quickstart (Bearer AIC-JWT)
key, _ := identity.GenerateKey(identity.ES256) // persisted as agent-key.pem
agent, _ := aicagent.New(aicagent.Config{
Mode: aicagent.Bearer,
Key: key,
Issuer: "aic-verifier-example",
Audience: []string{"myapi"},
Subject: "agent-001",
Realm: "example",
ID: "agent-001",
Capabilities: []aicjwt.Capability{{Scheme: "demo", ID: "api:read"}},
})
resp, _ := agent.Get("https://api.example/myapi")
Locally-signed bearer credentials are minted fresh per request: aic-verifier
treats each token's jti as single-use by default (ReplayProtection), so the
agent never reuses a cached token for local Key mode. Token() still returns
a cached token for inspection; the transport calls the per-request mint path.
A pre-minted Config.Token is necessarily single-use under the same policy.
Quickstart (mTLS client certificate)
agent, _ := aicagent.New(aicagent.Config{
Mode: aicagent.MTLS,
CertFile: "client-cert.pem", // carries the AIC X.509 extension
KeyFile: "client-key.pem",
ServerCA: "ca-cert.pem", // service CA; empty = system roots
})
resp, _ := agent.Get("https://api.example/myapi")
Exchange from an issuer (RFC 7523)
agent, _ := aicagent.New(aicagent.Config{
Mode: aicagent.Bearer,
RemoteIssuer: issuerURL, // token endpoint
DA: daAssertion, // minted with bearer.SignDA
Audience: []string{"myapi"},
Subject: "agent-001",
Realm: "example",
ID: "agent-001",
})
// tokens are fetched from the issuer and auto-refreshed on 401
AIC issuance through user-signer
When the agent has no AIC certificate yet, it obtains one through a
human-in-the-loop flow: the agent never holds the principal's private key, so
it asks user-signer for a Delegation Authorization, a human approves the
request in the console, and the resulting evidence is presented to varwof
core, which mints the AIC certificate for a key the agent generated locally.
submit AIC request ──▶ user-signer ──▶ human approves (console)
◀── DA evidence {da, user_cert_pem, constraints}
build CSR (own key) + evidence ──▶ core POST /api/v1/certs (agent-proxy)
◀── AIC certificate
One-shot:
key, _ := identity.GenerateKey(identity.ES256)
agent, _ := aicagent.New(aicagent.Config{Mode: aicagent.Bearer, Key: key})
res, _ := agent.ObtainAIC(ctx, aicagent.AICRequestOptions{
SignerURL: "https://127.0.0.1:8444", // user-signer mTLS
CoreURL: "https://127.0.0.1:4433", // varwof core
AgentID: "spiffe://varwof.com/agent/ci-bot",
PrincipalUID: "pki:alice@example.com",
Capabilities: []aicagent.AICCapability{
{SchemeID: "varwof/demo-mysql-v1", CapabilityID: "SELECT:*"},
},
ReasonCode: "operator-request",
Description: "CI bot needs one-time DB read access",
LifetimeSec: 900,
})
// res.CertPEM is the issued AIC certificate; res.Constraints mirrors the
// effective constraint set written into the AIC extension.
ObtainAIC is SubmitAICRequest → WaitForAIC → GenerateCSR →
IssueAICCertificate; each step is exported when you need to interleave your
own logic (e.g. surface the request id to an operator before waiting).
SubmitAICRequest(ctx, opt) (string, error)— enqueue the request, returns its id. A configuredConfig.Keymakes the delegation a v2 DA bound to the agent's SPKI (DAVersion: 1opts into legacy v1).WaitForAIC(ctx, opt, id, timeout, poll) (*AICRequestStatus, error)— poll until a human approves or rejects.IssueAICCertificate(ctx, opt, status, csrPEM) (*AICResult, error)— present the evidence plus the agent's CSR to core; the effective constraints the DA was signed with are forwarded asauthorization_constraints, so the AIC extension stays aligned with the delegation.
MCP client over the issued AIC (mcpclient/)
mcpclient.New runs the flow above and presents the minted (short-lived,
memory-only) AIC certificate as the mTLS identity on every JSON-RPC request to
an AIC-gated MCP server. The certificate is re-obtained as its validity runs
out; no on-disk cache.
c, _ := mcpclient.New(ctx, mcpclient.Options{
SignerURL: "https://127.0.0.1:8444", // user-signer mTLS
CoreURL: "https://127.0.0.1:4433", // varwof core
ServerURL: "https://127.0.0.1:9443/mcp",
ServerCA: "/data/ca.pem", // pin the MCP server certificate
AgentID: "mcp-agent",
PrincipalUID: "pki:alice@example.com",
Capabilities: []aicagent.AICCapability{
{SchemeID: "varwof/demo-mysql-v1", CapabilityID: "SELECT:*"},
},
ReasonCode: "operator-request", Description: "MCP DB reads",
})
defer c.Close()
sess, _ := c.Connect(ctx) // initialize over mTLS
tools, _ := sess.Tools(ctx) // tools/list
res, _ := sess.Call(ctx, "echo", map[string]any{"message": "hi"}) // tools/call
examples/mcp-client drives the chain from the CLI (list tools, then call
one). The certificate is bound to the agent's own key via the CSR, so the
server's IdentityAIC admission sees the same agent identity that obtained
the delegation.
To present an already-issued AIC (operator-provisioned path — e.g. the AIC X.509
cert the mcp-behind-proxy --mtls demo mints), set Options.AICCertPEM /
Options.AICKeyPEM; mcpclient then skips the user-signer/core pipeline and uses
the pair as its mTLS identity.
CLC client-side pre-check
aic-agent/clc.go lets an agent decide an operation against its declared
capabilities before spending a request, using the same CLC-v1 core
(register/semantics) that aic-verifier decides with, so client and server
verdicts agree by construction:
cfg := aicagent.Config{Capabilities: []aicjwt.Capability{
{Scheme: "std/database-v1", ID: "query:SELECT", Params: json.RawMessage(`{"limit":100}`)},
}}
dec, _ := cfg.Authorize("std/database-v1:query:SELECT", map[string]any{"limit": 50})
switch dec.Verdict {
case semantics.VerdictAllow:
// proceed
case semantics.VerdictAllowUR:
// recognized residual obligation (dec.Unresolved) still needs confirming
default: // VerdictDeny — dec.Reason carries the normative code
}
Server denials: plain, or evidence-challenge-bearing
aic-verifier (v0.2.0) denies in two shapes, and aic-agent parses both into
a *RefusalError that is matchable with errors.As:
- plain denial —
Content-Type: application/json, body{"code":"access_denied","message":…}; - remediable denial —
Content-Type: application/problem+json(RFC 9457), body carriestype/title/status/detailplus an extendedchallengemember (aCLC-CHALLENGE-v1), and the response may also carry aRetry-Afterheader (seconds).
Reading the challenge is one errors.As:
var ref *aicagent.RefusalError
if errors.As(err, &ref) && ref.Challenge != nil {
// ref.Challenge.Required[].id/constraint/reason says what evidence is still
// missing; ref.RetryAfterSec says when a corrected presentation may retry.
// Non-2xx that cannot possibly be remediated is no challenge — but a
// challenge that was announced yet unparseable sets
// ref.ChallengeUnavailable, never a silent "no challenge".
}
allow_unresolved is not allow: neither the client pre-check above nor the
server's challenge is a verdict. An operation granted allow outright is safe
to run; one that comes back allow_unresolved (or with a challenge) still has
an outstanding obligation the caller must discharge first.
Consuming the challenge: closing an evidence-required denial
There is no wire channel for evidence — the agent never uploads recorded
records to the verifier. Evidence is presented by the deployment: the
verifier's EvidenceRequirement / EvidenceFacts hook decides admission from
facts the deployment itself established (e.g. human authorization receipts the
deployment recorded and stored). The challenge tells the agent what is missing
(required[].id/constraint) and when it may retry (Retry-After, an integer
seconds header; the client treats it as retry_timing.not_before).
aic-agent implements the client half of that mechanism. Configure a provider:
a, err := aicagent.New(aicagent.Config{
Token: "…",
// after an evidence-required denial and any Retry-After wait, the SDK asks
// the deployment whether the facts are now in place for the verifier gate.
EvidenceProvider: func(ctx context.Context, ref *aicagent.RefusalError) (bool, error) {
if !recordHumanAuthorization(ref.Challenge) { // deployment glue
return false, nil
}
return true, nil // retry the identical request
},
})
When the core (or a peer reached through coreDo) answers an evidence-required
challenge, the SDK:
- rejects the retry if the challenge is already stale —
Challenge.Usable: expired challenges are never retried (that is the load amplification the challenge specification forbids); - waits out
Retry-After(orretry_timing.not_before), never pastexpires_at; - asks your
EvidenceProviderafter the wait (the challenge must still be live the instant it retries); - retries the byte-identical request exactly once.
A second evidence-required denial is returned as-is. A plain denial, a
challenge that was announced but unparseable (ChallengeUnavailable), a stale
challenge, a declined provider, or a provider error all reach the caller as the
*RefusalError (or its error) — never as a blind retry.
The same machine guards data-plane requests through Agent.Do: with an
EvidenceProvider configured, Do closes an evidence-required denial with the
identical wait → provider → single byte-identical retry loop, and returns the
refusal when it cannot be closed. Without a provider (or for a denial with no
usable challenge), Do hands the response back unchanged — the request is
replayed verbatim and never more than once.
Smoke tests
go test -tags smoke -v ./smoke/
smoke/ drives the normative CLC-v1 corpus (the register vectors) through
the SDK glue and exercises the user-signer → core issuance flow against stub
servers.
Structure
aicagent.go Agent client aggregating identity + transports
aicrequest.go user-signer submission → approval → core AIC issuance
mcpclient/ MCP client presenting the issued AIC over mTLS
clc.go CLC-v1 client-side capability/operation decision glue
identity/ agent key material and fingerprints
bearer/ local minting (Sign) and DA exchange (SignDA, IssuerClient)
transport/ Bearer and mTLS http.RoundTrippers
smoke/ CLC corpus conformance + issuance-flow smoke (build tag smoke)
examples/
call-bearer/ calls aic-verifier/examples/bearer-jwt-backend
call-mtls/ calls aic-verifier/examples/mtls-backend
mcp-client/ lists/calls tools on an AIC-gated MCP server
smoke-call/ presents an issued AIC cert to an aic-verifier service
See examples/ headers for the exact run commands that pair with the
aic-verifier service examples.
Related repositories
| Repository | Role |
|---|---|
| varwof/types | Shared Go types: AIC, AIC-JWT, capabilities |
| varwof/register | Capability registry, PKCS#7 signing, CLC semantics |
| varwof/capability | Capability definitions, CLC spec and conformance corpus |
| varwof/core | CA and AIC issuance |
| varwof/client | CLI for issuance and delegation signing |
| varwof/gateway-core · varwof/gateway | Full gateway; this SDK is its embeddable admission core |
| varwof/aic-verifier | Service-side SDK that verifies the credentials this agent presents |
License
Apache-2.0. See LICENSE.
See also SECURITY.md (how the credential is held, vulnerability reporting, hardening), CONTRIBUTING.md (development and the check list), and CHANGELOG.md.