> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/aicr/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/aicr/_mcp/server.

# Using AICR as a Go library

AICR ships as both a CLI and a Go library. External projects that need
to resolve validated recipes, generate bundles, or collect observed
state can import AICR directly. This page is for those consumers.

## Which package to import

**Import the `github.com/NVIDIA/aicr/pkg/client/v1` package.** This is the
stable facade.

```go
import aicr "github.com/NVIDIA/aicr/pkg/client/v1"
```

The facade provides a single `Client` type with constructors for the
supported recipe sources. Internally it delegates to the functional
packages under `pkg/*`.

You _may_ also import `pkg/*` subpackages directly, but their APIs are
not covered by the same stability guarantees — see the [public API
surface](/aicr/integrator-guide/public-api-surface) for the details.

## Installing

```bash
go get github.com/NVIDIA/aicr@latest
```

For reproducibility in downstream projects, pin a specific tag:

```bash
go get github.com/NVIDIA/aicr@v0.11.1
```

## Quick start

```go
package main

import (
	"context"
	"log"
	"time"

	aicr "github.com/NVIDIA/aicr/pkg/client/v1"
)

func main() {
	// FilesystemSource layers an external recipe directory over the
	// embedded recipe data. Use this in production today; OCISource
	// is reserved but not yet implemented (NewClient returns
	// ErrCodeUnavailable when given one — see the constructor's
	// godoc for the current state).
	client, err := aicr.NewClient(
		aicr.WithRecipeSource(
			aicr.FilesystemSource("/etc/aicr/recipes"),
		),
	)
	if err != nil {
		log.Fatal(err)
	}
	// Always Close when done — releases this Client's cached
	// metadata store and component registry from the recipe
	// package's per-DataProvider caches.
	defer client.Close()

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	result, err := client.ResolveRecipe(ctx, aicr.RecipeRequest{
		Service:     "eks", // K8s flavour, not cloud vendor — map aws→eks etc. on your side
		Region:      "us-east-1",
		Accelerator: "h100",
		Nodes:       8, // worker-node count, not GPU count
		OS:          "ubuntu", // REQUIRED to reach the OS-pinned kubeflow overlay; see "Recipe sources" below
		Intent:      "training",
		Platform:    "kubeflow",
	})
	if err != nil {
		log.Fatalf("resolve recipe: %v", err)
	}

	log.Printf("resolved recipe %s (%d components)", result.Name, len(result.Components))
}
```

## Snapshotting and validation

Beyond recipe resolution, the facade exposes the rest of the
Snapshot → Validate workflow. Both methods are stateless w.r.t. the
Client's recipe source; they are surfaced through the Client only to
keep the facade uniform and leave room for future per-Client
telemetry hooks.

```go
// CollectSnapshot deploys a snapshotter Job to the target cluster and
// returns the resulting Snapshot. cfg is a facade-owned struct that
// mirrors most fields of the underlying pkg/snapshotter.AgentConfig
// (the in-pod network-discovery fields ClusterConfigPath and
// DiscoverNetwork are not surfaced on the facade).
snap, err := client.CollectSnapshot(ctx, &aicr.AgentConfig{
	Kubeconfig:         "/path/to/target-kubeconfig",
	Namespace:          "aicr-snapshot",
	Image:              "ghcr.io/nvidia/aicr:v0.11.1",
	ServiceAccountName: "aicr-agent",
	Timeout:            5 * time.Minute,
	Cleanup:            true,
})
if err != nil {
	log.Fatalf("collect snapshot: %v", err)
}

// ValidateState runs the validation phases against the resolved recipe +
// observed snapshot. With no WithValidationPhases option it runs all three
// phases (Deployment, Conformance, Performance) in canonical order.
results, err := client.ValidateState(ctx, result, snap)
if err != nil {
	log.Fatalf("validate state: %v", err)
}
for _, r := range results {
	log.Printf("phase=%s status=%s duration=%s", r.Phase, r.Status, r.Duration)
}
```

The `recipe` argument to `ValidateState` MUST be the `*RecipeResult`
returned by the same Client's `ResolveRecipe` (or `LoadRecipe`) call —
the unexported internal recipe state is required for constraint
evaluation.

To restrict the run to specific phases, pass `WithValidationPhases` in
the order you want them executed:

```go
results, err := client.ValidateState(ctx, result, snap,
	aicr.WithValidationPhases(aicr.PhaseDeployment, aicr.PhaseConformance))
```

Valid phase values are `PhaseDeployment`, `PhaseConformance`, and
`PhasePerformance` (canonical execution order). An unrecognized phase is rejected with
`ErrCodeInvalidRequest` before any cluster work, so a typo cannot
silently degrade to an empty run.

### Loading an existing recipe

When a recipe has already been resolved and persisted (for example a
recipe file checked into a GitOps repo, or a `cm://` ConfigMap URI), load
it back through the same Client with `LoadRecipe` instead of re-resolving
from criteria:

```go
result, err := client.LoadRecipe(ctx, "/etc/aicr/recipe.yaml", "")
if err != nil {
	log.Fatalf("load recipe: %v", err)
}
```

`LoadRecipe` hydrates overlay inputs (`kind: RecipeMetadata`) against the
Client's own data provider and returns a Client-owned `*RecipeResult`
ready for `ValidateState` / `BundleComponents` — it passes the same
ownership check as a `ResolveRecipe` result. An already-hydrated
`RecipeResult` file is returned with its provider bound to the Client.
The kubeconfig argument (third parameter) is only needed when the recipe
path (first argument) is a `cm://` ConfigMap URI.

For unit tests that exercise the facade surface without a live
cluster, pass `aicr.WithValidationNoCluster(true)`: every check
reports as "skipped - no-cluster mode" and no Kubernetes resources
are created. Other facade options
(`WithValidationNamespace`, `WithValidationRunID`,
`WithValidationCleanup`, `WithValidationImagePullSecrets`,
`WithValidationTolerations`, `WithValidationNodeSelector`) cover the
production-controller knobs.

## Recipe sources

AICR exposes one production recipe source today; pick it via
`aicr.WithRecipeSource`:

| Source | Constructor | Status |
|--------|-------------|--------|
| Embedded | `aicr.EmbeddedSource()` | Production. Uses only AICR's built-in recipe data with no external overlay. |
| Local filesystem | `aicr.FilesystemSource(path)` | Production. Use a directory containing a `registry.yaml` (layered over the embedded recipe data). |
| OCI registry | `aicr.OCISource(registry, tag)` | **Reserved — not yet implemented.** `NewClient` returns `ErrCodeUnavailable` when this source is selected. |

`EmbeddedSource` resolves against the recipe data compiled into the
AICR binary — no filesystem path required. Use it when you want AICR's
bundled recipe data and no local overrides. `FilesystemSource`
layers an external directory over that same embedded data, so files in
the directory override their embedded equivalents.

## Client options

Beyond `WithRecipeSource`, `NewClient` accepts these functional options:

```go
allowLists, err := aicr.ParseAllowListsFromEnv()
if err != nil {
	log.Fatal(err)
}

client, err := aicr.NewClient(
	aicr.WithRecipeSource(aicr.EmbeddedSource()),
	aicr.WithVersion("1.2.3"),
	aicr.WithAllowLists(allowLists),
)
```

- **`WithVersion(version string)`** stamps the given version string into
  resolved recipe metadata (accessible via `result.Resolved().Metadata.Version`).
  Typically the consuming binary's build version.
- **`WithAllowLists(al *AllowLists)`** fences which criteria values the
  Client's resolve path accepts. A resolve whose criteria fall outside
  the allowlist is rejected before the recipe is built. Pass `nil` (or
  omit the option) to allow all values.
- **`ParseAllowListsFromEnv()`** builds an `AllowLists` from the
  `AICR_ALLOWED_ACCELERATORS`, `AICR_ALLOWED_SERVICES`,
  `AICR_ALLOWED_INTENTS`, and `AICR_ALLOWED_OS` environment variables.
  It returns `nil` when none are set — `WithAllowLists` treats a `nil`
  `AllowLists` as allow-all, so the result is always safe to pass straight
  to `WithAllowLists`.

`AllowLists` is a facade-owned struct whose `Accelerators`, `Services`,
`Intents`, and `OSTypes` fields are plain `[]string` slices, so callers
can construct one directly without depending on `pkg/recipe`'s enum
identifiers. When you already hold a `pkg/recipe.AllowLists`, use
`aicr.WrapAllowLists` to project it onto the facade shape.

## Resolving from criteria

`ResolveRecipe` takes the stable `RecipeRequest` shape and returns the
facade `RecipeResult` — a deliberately small struct exposing the
`Name`, `Version`, and `Components` of the resolved recipe. When you
already hold an `*aicr.Criteria` value — for example, a REST handler
that parsed criteria from an incoming HTTP request and wrapped them with
`aicr.WrapCriteria` — use `ResolveRecipeFromCriteria`. It returns the
same facade `*RecipeResult`; call `result.Resolved()` when you need the
complete underlying `*pkg/recipe.RecipeResult` (constraints, deployment
order, validation config, metadata):

```go
rec, err := client.ResolveRecipeFromCriteria(ctx, aicr.WrapCriteria(criteria))
if err != nil {
	log.Fatalf("resolve recipe: %v", err)
}

// Facade surface — Name, Version, Components.
log.Printf("recipe %s components: %d", rec.Name, len(rec.Components))

// Full upstream shape, when needed.
resolved := rec.Resolved()
log.Printf("recipe constraints: %d", len(resolved.Constraints))
```

The returned `*RecipeResult` carries:

- `Name`, `Version`, `TranslatedAt` — stable identity
- `Components` — `[]ComponentRef` (Name, Kind, Version, Source, Chart, Namespace)
- `Resolved()` — the upstream `*pkg/recipe.RecipeResult` for callers that
  need constraints, deployment order, validation config, or metadata
  (e.g., evidence emission). Do not mutate; do not retain past the
  facade `RecipeResult`'s lifetime — marshal first if persistence is
  needed.

`Criteria` is a facade-owned struct whose enum-typed fields project to
plain strings, decoupling the public surface from `pkg/recipe`'s enum
identifiers. Construct one directly or wrap an upstream
`*pkg/recipe.Criteria` via `aicr.WrapCriteria`. Allowlist enforcement
(`WithAllowLists`) applies here just as it does on `ResolveRecipe`; a
`nil` Client, `nil` context, or `nil` criteria each return
`ErrCodeInvalidRequest`, and the same facade-level timeout bounds the
resolve.

To extract a single value from a resolved recipe, use
`SelectFromRecipe` with a dot-path selector. It hydrates the recipe's
component values and returns the value at the path; an empty selector
returns the entire hydrated structure, and a `nil` `*RecipeResult`
returns `ErrCodeInvalidRequest`. This mirrors the `aicr query` CLI
command:

```go
v, err := aicr.SelectFromRecipe(rec, "components.gpu-operator.values.driver.version")
if err != nil {
	log.Fatalf("select: %v", err)
}
log.Printf("driver version: %v", v)
```

## Errors

All errors returned by the facade are `*pkg/errors.StructuredError`
values carrying an `ErrorCode`. Use `errors.As` to inspect:

```go
import (
	stderrors "errors"
	aicr "github.com/NVIDIA/aicr/pkg/client/v1"
	aicrerrors "github.com/NVIDIA/aicr/pkg/errors"
)

_, err := client.ResolveRecipe(ctx, req)
var se *aicrerrors.StructuredError
if stderrors.As(err, &se) && se.Code == aicrerrors.ErrCodeInvalidRequest {
	// handle invalid input
}
```

## Context handling

`ResolveRecipe` (and every other context-aware facade method) honours
context cancellation. Each facade entry point unconditionally wraps the
caller's context with `context.WithTimeout` against its per-operation
cap. The effective deadline is the smaller of the caller's deadline
and the facade cap, per `context.WithTimeout` semantics — a caller
passing a tighter deadline keeps it; a caller passing
`context.Background()` gets the facade cap.

Per-operation caps:

- `ResolveRecipe` / `BundleComponents`: `defaults.RecipeOperationTimeout`
- `CollectSnapshot`: caller-controlled via `AgentConfig.Timeout`,
  falling back to `defaults.SnapshotOperationTimeout` when unset
- `ValidateState`: `defaults.ValidationOperationTimeout`
- `MakeBundle`: opt-in via `BundleOptions.Timeout`. When unset (`0`) the
  caller's context governs unchanged — large bundles, `--vendor-charts`,
  and attestation/signing can exceed any fixed cap. The REST `/v1/bundle`
  handler sets it to `defaults.BundleHandlerTimeout`; the CLI `bundle`
  command leaves it `0`.

Passing a `nil` `context.Context` returns `ErrCodeInvalidRequest`. Use
`context.Background()` (or a deadline-bounded child) for unbounded callers.

## Compatibility

The facade's exported API follows [Semantic Versioning][semver]:

- **Major** bumps may rename, remove, or change the shape of exported
  types and function signatures.
- **Minor** bumps may add new exported types, fields, or methods.
- **Patch** bumps are bug-fix-only.

Today AICR is pre-1.0. **Pin a patch version** in your `go.mod` and
audit diffs on upgrade.

## See also

- [Public API surface](/aicr/integrator-guide/public-api-surface) — stability matrix per package
- [Automation guide](/aicr/integrator-guide/automation) — CI integration patterns
- [Recipe development](/aicr/integrator-guide/recipe-development) — authoring recipes

[semver]: https://semver.org/spec/v2.0.0.html