aic-agent — Quickstart (EN)EN

aic-agent quickstart

Three ways to talk to an AIC-protected API, from simplest to full-flow. The examples assume a running aic-verifier service; the pairing commands are in examples.md.

0. Install

go get github.com/varwof/aic-agent@latest

Requires Go 1.26+. Builds against published types and register; no local replace directives.

1. Locally-minted Bearer AIC-JWT (simplest)

The agent key mints a fresh aic+jwt per request — no issuer, no user signer, nothing but a key and a config:

package main

import (
    "log"

    aicagent "github.com/varwof/aic-agent"
    "github.com/varwof/aic-agent/identity"
    "github.com/varwof/types/aicjwt"
)

func main() {
    key, err := identity.GenerateKey(identity.ES256) // persist as agent-key.pem
    if err != nil {
        log.Fatal(err)
    }

    agent, err := aicagent.New(aicagent.Config{
        Mode:     aicagent.Bearer,
        Key:      key,
        Issuer:   "example-agent",
        Audience: []string{"myapi"},
        Subject:  "agent-001",
        Realm:    "example",
        ID:       "agent-001",
        Capabilities: []aicjwt.Capability{
            {Scheme: "demo", ID: "api:read"},
        },
    })
    if err != nil {
        log.Fatal(err)
    }
    defer agent.Close()

    resp, err := agent.Get("https://api.example/myapi")
    _ = resp
}

The token is minted per request (the verifier treats each jti as single-use by default). Token() still returns a cached token for inspection; the transport uses the per-request mint path.

2. mTLS client certificate

The agent presents a client certificate that carries the AIC X.509 extension:

agent, err := aicagent.New(aicagent.Config{
    Mode:     aicagent.MTLS,
    CertFile: "client-cert.pem", // carries the AIC extension
    KeyFile:  "client-key.pem",
    ServerCA: "ca-cert.pem",     // service CA; empty = system roots
})
resp, err := agent.Get("https://api.example/myapi")

3. Remote issuance via RFC 7523 exchange

The agent holds a DA assertion (minted by bearer.SignDA with the principal's key, or fetched from the user signer) and exchanges it at an issuer token endpoint for a short-lived token:

agent, err := aicagent.New(aicagent.Config{
    Mode:         aicagent.Bearer,
    RemoteIssuer: issuerURL,          // token endpoint (HTTPS required)
    DA:           daAssertion,        // or leave empty: fetched from UserSigner
    Audience:     []string{"myapi"},
    Subject:      "agent-001",
    Realm:        "example",
    ID:           "agent-001",
})
resp, err := agent.Get("https://api.example/myapi")

Tokens are fetched from the issuer and auto-refreshed on 401 (idempotent requests only; see api.md).

4. AIC issuance through the user signer (when the agent has no AIC yet)

Two-tier flow: submit a request, a human approves in the console, and core mints the AIC certificate for a key the agent generated locally — the agent never holds the principal's private key:

key, _ := identity.GenerateKey(identity.ES256)
agent, _ := aicagent.New(aicagent.Config{Mode: aicagent.Bearer, Key: key})

res, err := 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://example.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

ObtainAIC = SubmitAICRequestWaitForAICGenerateCSRIssueAICCertificate; each step is exported for interleaved logic.

5. Handling a refusal with a challenge

Non-2xx answers parse into a *RefusalError. To close an evidence-required denial, configure the EvidenceProvider — the SDK waits out Retry-After, asks your provider whether the facts now exist at the verifier's gate, and retries the byte-identical request once:

agent, _ := aicagent.New(aicagent.Config{
    Token:  "...",
    EvidenceProvider: func(ctx context.Context, ref *aicagent.RefusalError) (bool, error) {
        return deploymentHasEvidenceFacts(ref.Challenge), nil
    },
})

A second denial, a stale challenge, an unavailable provider or a declined signal all reach the caller unchanged — never a repeated hammering.

Next