Skip to content

Latest commit

 

History

History
1751 lines (1467 loc) · 51.2 KB

File metadata and controls

1751 lines (1467 loc) · 51.2 KB

Identity Setup Guide

Overview

Identity profiles tell agentcli who a task runs as and what credentials it carries. When agentcli executes a v0.2 manifest, it resolves the task's identity profile, acquires credentials from the configured provider, materializes those credentials into the subprocess environment, and records a redacted audit trail.

When you need identity profiles:

  • Your tool needs an API token, OAuth access token, or service credential at runtime.
  • You need audit records that attribute executions to a specific principal.
  • You need trust-level enforcement on tasks (e.g. production tasks require supervised trust).
  • You need verifiable execution evidence signed against a declared identity.

When you do not need identity profiles:

  • The task has no authentication requirements and you do not need principal attribution. Use "provider": "none" or omit the identity block entirely.

Choosing a Provider

Situation Provider Auth mode
Bearer token already in an environment variable env-bearer service
Bearer token stored in a file on disk file-bearer service
Service-to-service OAuth (client credentials grant) oidc-client-credentials service
Exchange one token for another (RFC 8693) oidc-token-exchange exchange
Running on an Azure VM, App Service, or Container Instance azure-managed-identity service
Running on AWS EC2, Lambda, ECS, or EKS aws-sts-assume-role service
Running on GCP Compute Engine, Cloud Run, or GKE gcp-workload-identity service
Running in a SPIFFE-enabled Kubernetes cluster spiffe-jwt-svid service
Running as an Entra Agent ID in Microsoft Entra entra-agent-id service
No credentials needed none none

List available providers at any time:

agentcli identity providers

Quick Setup: env-bearer

The most common case. You have a bearer token in an environment variable and want agentcli to pass it to the tool process.

Manifest

{
  "version": "0.2",
  "identity_profiles": [
    {
      "id": "api-service",
      "provider": "env-bearer",
      "subject": {
        "kind": "service",
        "principal": "agent://myorg/api-service",
        "delegation_mode": "none"
      },
      "auth": {
        "mode": "service",
        "scopes": ["read", "write"],
        "required": true,
        "provider_config": {
          "token_env": "MY_API_TOKEN"
        }
      },
      "trust": {
        "level": "supervised"
      },
      "presentation": {
        "bindings": [
          {
            "source": "credentials.access_token.value",
            "target": { "kind": "env", "name": "TOOL_ACCESS_TOKEN" },
            "required": true,
            "redact": true
          }
        ],
        "handoff": "none",
        "cleanup": "always"
      }
    }
  ],
  "workflows": [
    {
      "id": "deploy",
      "name": "Deploy Workflow",
      "contract": {
        "sandbox": "permissive",
        "network": "unrestricted",
        "audit": "always"
      },
      "tasks": [
        {
          "id": "call-api",
          "name": "Call API",
          "shell": {
            "program": "sh",
            "args": ["-lc", "curl -H \"Authorization: Bearer $TOOL_ACCESS_TOKEN\" https://api.example.com/deploy"]
          },
          "target": { "session_target": "shell" },
          "schedule": { "cron": "0 0 * * *", "tz": "UTC" },
          "identity": { "ref": "api-service" }
        }
      ]
    }
  ]
}

This example uses sh -lc intentionally. Structured shell.args are passed literally by agentcli, so shell variable expansion only happens when you opt into an explicit shell wrapper. For tools that can read credentials directly from the environment, prefer invoking them without sh -lc.

Run it

export MY_API_TOKEN="your-token-here"
agentcli exec manifest.json call-api

The flow:

  1. agentcli reads MY_API_TOKEN from the environment.
  2. The presentation binding copies the token value into TOOL_ACCESS_TOKEN in the subprocess environment.
  3. The curl command receives the token via its environment.
  4. After execution, credentials are cleaned up and an audit record is written.

Dry run

Preview what agentcli will do without executing the command:

agentcli exec manifest.json call-api --dry-run

Dry-run does not resolve a provider or materialize credentials. It shows the merged, audit-safe identity declaration and marks live phases skipped. Use the dedicated identity command when you intentionally want provider-backed resolution without running the task:

agentcli identity resolve manifest.json call-api

Making auth optional

Set "required": false in the auth block. If the environment variable is not set, agentcli produces an empty credential session instead of failing:

"auth": {
  "mode": "service",
  "required": false,
  "provider_config": {
    "token_env": "MY_API_TOKEN"
  }
}

Quick Setup: oidc-client-credentials

Use this when you need to obtain a fresh access token from an OAuth 2.0 / OIDC token endpoint using the client credentials grant (RFC 6749 Section 4.4).

Manifest

{
  "version": "0.2",
  "identity_profiles": [
    {
      "id": "oidc-service",
      "provider": "oidc-client-credentials",
      "subject": {
        "kind": "service",
        "principal": "agent://myorg/oidc-worker",
        "delegation_mode": "none"
      },
      "auth": {
        "mode": "service",
        "scopes": ["api.read", "api.write"],
        "audience": "https://api.example.com",
        "required": true,
        "provider_config": {
          "token_endpoint": "https://auth.example.com/oauth/token",
          "client_id": "my-client-id",
          "client_secret": {
            "value_from": { "env": "OIDC_CLIENT_SECRET" }
          }
        }
      },
      "trust": {
        "level": "supervised"
      },
      "presentation": {
        "bindings": [
          {
            "source": "credentials.access_token.value",
            "target": { "kind": "env", "name": "ACCESS_TOKEN" },
            "required": true,
            "redact": true
          }
        ],
        "handoff": "none",
        "cleanup": "always"
      }
    }
  ],
  "workflows": [
    {
      "id": "sync",
      "name": "Data Sync",
      "contract": {
        "sandbox": "permissive",
        "network": "unrestricted",
        "audit": "always"
      },
      "tasks": [
        {
          "id": "sync-data",
          "name": "Sync Data",
          "shell": {
            "program": "python3",
            "args": ["sync.py"]
          },
          "target": { "session_target": "shell" },
          "schedule": { "cron": "0 0 * * *", "tz": "UTC" },
          "identity": { "ref": "oidc-service" }
        }
      ]
    }
  ]
}

Run it

export OIDC_CLIENT_SECRET="your-client-secret"
agentcli exec manifest.json sync-data

The flow:

  1. agentcli reads the client secret from OIDC_CLIENT_SECRET via the value_from indirection.
  2. It performs a POST to the token endpoint with grant_type=client_credentials, client_id, client_secret, scope, and audience.
  3. The returned access token is placed into the ACCESS_TOKEN env var for the subprocess.
  4. If the token endpoint returns expires_in, the session records the expiration time.

Client secret sources

The client secret supports three resolution methods:

Inline string (not recommended for production):

"client_secret": "literal-secret-value"

Environment variable (recommended):

"client_secret": {
  "value_from": { "env": "OIDC_CLIENT_SECRET" }
}

File on disk:

"client_secret": {
  "value_from": { "file": "/run/secrets/oidc-client-secret" }
}

You can also place the secret in auth.inputs instead of provider_config:

"auth": {
  "inputs": {
    "client_secret": {
      "value_from": { "env": "OIDC_CLIENT_SECRET" }
    }
  },
  "provider_config": {
    "token_endpoint": "https://auth.example.com/oauth/token",
    "client_id": "my-client-id"
  }
}

Required fields

Field Location Required
token_endpoint auth.provider_config.token_endpoint Yes
client_id auth.provider_config.client_id Yes
client_secret auth.provider_config.client_secret or auth.inputs.client_secret Yes
scopes auth.scopes No
audience auth.audience No
resource auth.resource No

The token endpoint must use https:// unless you explicitly pass allowInsecure in the resolution context.

Quick Setup: file-bearer

Use this when a bearer token is stored in a file, for example a Kubernetes service account token at /var/run/secrets/kubernetes.io/serviceaccount/token.

Manifest

{
  "version": "0.2",
  "identity_profiles": [
    {
      "id": "k8s-service",
      "provider": "file-bearer",
      "subject": {
        "kind": "service",
        "principal": "agent://myorg/k8s-worker",
        "delegation_mode": "none"
      },
      "auth": {
        "mode": "service",
        "required": true,
        "provider_config": {
          "token_file": "/var/run/secrets/kubernetes.io/serviceaccount/token"
        }
      },
      "trust": {
        "level": "supervised"
      },
      "presentation": {
        "bindings": [
          {
            "source": "credentials.access_token.value",
            "target": { "kind": "env", "name": "K8S_TOKEN" },
            "required": true,
            "redact": true
          }
        ],
        "handoff": "none",
        "cleanup": "always"
      }
    }
  ],
  "workflows": [
    {
      "id": "k8s-ops",
      "name": "Kubernetes Operations",
      "contract": {
        "sandbox": "permissive",
        "network": "unrestricted",
        "audit": "always"
      },
      "tasks": [
        {
          "id": "list-pods",
          "name": "List Pods",
          "shell": {
            "program": "kubectl",
            "args": ["get", "pods"]
          },
          "target": { "session_target": "shell" },
          "schedule": { "cron": "0 0 * * *", "tz": "UTC" },
          "identity": { "ref": "k8s-service" }
        }
      ]
    }
  ]
}

Run it

agentcli exec manifest.json list-pods

Indirect file path via environment variable

Instead of hardcoding the file path, resolve it from an environment variable:

"auth": {
  "mode": "service",
  "required": true,
  "inputs": {
    "token_file": {
      "value_from": { "env": "TOKEN_FILE_PATH" }
    }
  }
}
export TOKEN_FILE_PATH="/var/run/secrets/kubernetes.io/serviceaccount/token"
agentcli exec manifest.json list-pods

Security note

The file-bearer provider checks file permissions at resolution time. If the token file is world-readable, a warning is included in provider_assertions.permission_warning and appears in the audit record. Restrict token files to mode 0600.

Quick Setup: oidc-token-exchange

Use this when you have an existing token (from another identity provider, CI system, or upstream service) and need to exchange it for a new token with different scope, audience, or type. Implements OAuth 2.0 Token Exchange (RFC 8693).

Manifest

{
  "version": "0.2",
  "identity_profiles": [
    {
      "id": "exchange-service",
      "provider": "oidc-token-exchange",
      "subject": {
        "kind": "service",
        "principal": "agent://myorg/mesh-worker",
        "delegation_mode": "none"
      },
      "auth": {
        "mode": "exchange",
        "scopes": ["api.read"],
        "audience": "https://downstream.example.com",
        "required": true,
        "provider_config": {
          "token_endpoint": "https://auth.example.com/oauth/token",
          "subject_token_env": "UPSTREAM_TOKEN"
        }
      },
      "trust": {
        "level": "supervised"
      },
      "presentation": {
        "bindings": [
          {
            "source": "credentials.access_token.value",
            "target": { "kind": "env", "name": "EXCHANGED_TOKEN" },
            "required": true,
            "redact": true
          }
        ],
        "cleanup": "always"
      }
    }
  ],
  "workflows": [
    {
      "id": "mesh-call",
      "name": "Service Mesh Call",
      "contract": {
        "sandbox": "permissive",
        "network": "unrestricted",
        "audit": "always"
      },
      "tasks": [
        {
          "id": "call-downstream",
          "name": "Call Downstream Service",
          "shell": {
            "program": "sh",
            "args": ["-lc", "curl -H \"Authorization: Bearer $EXCHANGED_TOKEN\" https://downstream.example.com/api"]
          },
          "target": { "session_target": "shell" },
          "schedule": { "cron": "0 0 * * *", "tz": "UTC" },
          "identity": { "ref": "exchange-service" }
        }
      ]
    }
  ]
}

Required fields

Field Location Required
token_endpoint auth.provider_config.token_endpoint Yes
subject_token_env auth.provider_config.subject_token_env Yes (or use auth.inputs.subject_token.value_from)
client_id auth.provider_config.client_id No
client_secret auth.provider_config.client_secret or auth.inputs.client_secret No
subject_token_type auth.provider_config.subject_token_type No (defaults to urn:ietf:params:oauth:token-type:access_token)
scopes auth.scopes No
audience auth.audience No

Run it

export UPSTREAM_TOKEN="eyJhbGciOi..."
agentcli exec manifest.json call-downstream

Resolve the identity without executing the task:

agentcli identity resolve manifest.json call-downstream

When to use this provider

Use oidc-token-exchange for service mesh token exchange, cross-tenant delegation, or scope reduction. The token endpoint must support the RFC 8693 urn:ietf:params:oauth:grant-type:token-exchange grant type. The provider also supports downscope handoff for passing a reduced-privilege token to downstream tasks.

Quick Setup: azure-managed-identity

Use this when running on Azure VMs, App Service, or Container Instances with managed identity enabled. The provider acquires tokens from the Azure Instance Metadata Service (IMDS) at 169.254.169.254.

Manifest

{
  "version": "0.2",
  "identity_profiles": [
    {
      "id": "azure-service",
      "provider": "azure-managed-identity",
      "subject": {
        "kind": "service",
        "principal": "agent://myorg/azure-worker",
        "delegation_mode": "none"
      },
      "auth": {
        "mode": "service",
        "required": true,
        "provider_config": {
          "resource": "https://management.azure.com/"
        }
      },
      "trust": {
        "level": "supervised"
      },
      "presentation": {
        "bindings": [
          {
            "source": "credentials.access_token.value",
            "target": { "kind": "env", "name": "AZURE_ACCESS_TOKEN" },
            "required": true,
            "redact": true
          }
        ],
        "cleanup": "always"
      }
    }
  ],
  "workflows": [
    {
      "id": "azure-ops",
      "name": "Azure Operations",
      "contract": {
        "sandbox": "permissive",
        "network": "unrestricted",
        "audit": "always"
      },
      "tasks": [
        {
          "id": "list-resources",
          "name": "List Resources",
          "shell": {
            "program": "sh",
            "args": ["-lc", "curl -H \"Authorization: Bearer $AZURE_ACCESS_TOKEN\" \"https://management.azure.com/subscriptions?api-version=2022-12-01\""]
          },
          "target": { "session_target": "shell" },
          "schedule": { "cron": "0 0 * * *", "tz": "UTC" },
          "identity": { "ref": "azure-service" }
        }
      ]
    }
  ]
}

Required fields

Field Location Required
resource auth.provider_config.resource Yes (e.g. https://management.azure.com/, https://vault.azure.net/, https://graph.microsoft.com/)
client_id auth.provider_config.client_id No (required for user-assigned managed identities)

Run it

agentcli exec manifest.json list-resources

Preview the identity resolution:

agentcli identity resolve manifest.json list-resources

When to use this provider

This provider only works inside Azure environments where managed identity is enabled. Outside Azure, the IMDS endpoint at 169.254.169.254 is not reachable and the provider fails with: Azure IMDS endpoint not reachable. This provider requires an Azure environment with managed identity enabled. For user-assigned managed identities, set client_id to the managed identity's client ID. For system-assigned identities, omit client_id.

Quick Setup: aws-sts-assume-role

Use this to assume an IAM role via AWS Security Token Service. The provider implements AWS Signature Version 4 signing with no external dependencies.

Manifest

{
  "version": "0.2",
  "identity_profiles": [
    {
      "id": "aws-service",
      "provider": "aws-sts-assume-role",
      "subject": {
        "kind": "service",
        "principal": "agent://myorg/aws-deployer",
        "delegation_mode": "none"
      },
      "auth": {
        "mode": "service",
        "required": true,
        "provider_config": {
          "role_arn": "arn:aws:iam::123456789012:role/AgentDeployRole"
        }
      },
      "trust": {
        "level": "supervised"
      },
      "presentation": {
        "bindings": [
          {
            "source": "credentials.access_key_id.value",
            "target": { "kind": "env", "name": "AWS_ACCESS_KEY_ID" },
            "required": true,
            "redact": true
          },
          {
            "source": "credentials.secret_access_key.value",
            "target": { "kind": "env", "name": "AWS_SECRET_ACCESS_KEY" },
            "required": true,
            "redact": true
          },
          {
            "source": "credentials.access_token.value",
            "target": { "kind": "env", "name": "AWS_SESSION_TOKEN" },
            "required": true,
            "redact": true
          }
        ],
        "cleanup": "always"
      }
    }
  ],
  "workflows": [
    {
      "id": "aws-deploy",
      "name": "AWS Deploy",
      "contract": {
        "sandbox": "permissive",
        "network": "unrestricted",
        "audit": "always"
      },
      "tasks": [
        {
          "id": "list-buckets",
          "name": "List S3 Buckets",
          "shell": {
            "program": "aws",
            "args": ["s3", "ls"]
          },
          "target": { "session_target": "shell" },
          "schedule": { "cron": "0 0 * * *", "tz": "UTC" },
          "identity": { "ref": "aws-service" }
        }
      ]
    }
  ]
}

Required fields

Field Location Required
role_arn auth.provider_config.role_arn Yes
region auth.provider_config.region No (defaults to AWS_DEFAULT_REGION, AWS_REGION, or us-east-1)
session_name auth.provider_config.session_name No (defaults to agentcli-session)
duration_seconds auth.provider_config.duration_seconds No (defaults to 3600)
external_id auth.provider_config.external_id No

Requires AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in the environment. If you are chaining from an existing session, AWS_SESSION_TOKEN is also accepted.

Run it

export AWS_ACCESS_KEY_ID="AKIA..."
export AWS_SECRET_ACCESS_KEY="wJalr..."
agentcli exec manifest.json list-buckets

Resolve the assumed role session without executing the task:

agentcli identity resolve manifest.json list-buckets

When to use this provider

Use aws-sts-assume-role for cross-account access, limited-privilege role assumption, or when you need temporary credentials scoped to a specific role. The provider includes full AWS Signature V4 signing and does not require the AWS SDK. The three credential values (access key, secret key, session token) are materialized into the subprocess environment via separate presentation bindings, matching the standard AWS credential environment variables.

Quick Setup: gcp-workload-identity

Use this when running on GCP Compute Engine, Cloud Run, or GKE with a service account attached. The provider acquires tokens from the GCP metadata server at metadata.google.internal.

Manifest

{
  "version": "0.2",
  "identity_profiles": [
    {
      "id": "gcp-service",
      "provider": "gcp-workload-identity",
      "subject": {
        "kind": "service",
        "principal": "agent://myorg/gcp-worker",
        "delegation_mode": "none"
      },
      "auth": {
        "mode": "service",
        "scopes": ["https://www.googleapis.com/auth/cloud-platform"],
        "required": true
      },
      "trust": {
        "level": "supervised"
      },
      "presentation": {
        "bindings": [
          {
            "source": "credentials.access_token.value",
            "target": { "kind": "env", "name": "GCP_ACCESS_TOKEN" },
            "required": true,
            "redact": true
          }
        ],
        "cleanup": "always"
      }
    }
  ],
  "workflows": [
    {
      "id": "gcp-ops",
      "name": "GCP Operations",
      "contract": {
        "sandbox": "permissive",
        "network": "unrestricted",
        "audit": "always"
      },
      "tasks": [
        {
          "id": "list-instances",
          "name": "List Compute Instances",
          "shell": {
            "program": "sh",
            "args": ["-lc", "curl -H \"Authorization: Bearer $GCP_ACCESS_TOKEN\" \"https://compute.googleapis.com/compute/v1/projects/my-project/zones/us-central1-a/instances\""]
          },
          "target": { "session_target": "shell" },
          "schedule": { "cron": "0 0 * * *", "tz": "UTC" },
          "identity": { "ref": "gcp-service" }
        }
      ]
    }
  ]
}

Required fields

Field Location Required
scopes auth.scopes or auth.provider_config.scopes Yes (non-empty array, e.g. ["https://www.googleapis.com/auth/cloud-platform"])
service_account_email auth.provider_config.service_account_email No (for impersonating a specific service account)

Run it

agentcli exec manifest.json list-instances

Resolve identity without executing:

agentcli identity resolve manifest.json list-instances

When to use this provider

This provider only works inside GCP environments where the metadata server is reachable. Outside GCP, the provider fails with: GCP metadata server not reachable. This provider requires a GCP environment with workload identity enabled. When service_account_email is specified, the metadata server returns a token for that specific service account (impersonation). Otherwise it uses the default service account attached to the instance.

Quick Setup: spiffe-jwt-svid

Use this in SPIFFE-enabled Kubernetes clusters running SPIRE or Istio when a JWT-SVID and its verification key or JWKS are mounted as local files. The provider does not implement the gRPC SPIFFE Workload API.

Manifest

{
  "version": "0.2",
  "identity_profiles": [
    {
      "id": "spiffe-service",
      "provider": "spiffe-jwt-svid",
      "subject": {
        "kind": "service",
        "principal": "spiffe://example.org/my-agent",
        "delegation_mode": "none"
      },
      "auth": {
        "mode": "service",
        "audience": "spiffe://example.org/downstream",
        "required": true,
        "provider_config": {
          "svid_file": "/var/run/secrets/spiffe/svid.jwt",
          "public_key_file": "/var/run/secrets/spiffe/jwt-svid-public.pem"
        }
      },
      "trust": {
        "level": "supervised"
      },
      "presentation": {
        "bindings": [
          {
            "source": "credentials.jwt_svid.value",
            "target": { "kind": "env", "name": "SPIFFE_JWT_SVID" },
            "required": true,
            "redact": true
          }
        ],
        "cleanup": "always"
      }
    }
  ],
  "workflows": [
    {
      "id": "mesh-ops",
      "name": "Service Mesh Operations",
      "contract": {
        "sandbox": "permissive",
        "network": "unrestricted",
        "audit": "always"
      },
      "tasks": [
        {
          "id": "call-peer",
          "name": "Call Peer Service",
          "shell": {
            "program": "sh",
            "args": ["-lc", "curl -H \"Authorization: Bearer $SPIFFE_JWT_SVID\" https://peer.example.svc.cluster.local/api"]
          },
          "target": { "session_target": "shell" },
          "schedule": { "cron": "0 0 * * *", "tz": "UTC" },
          "identity": { "ref": "spiffe-service" }
        }
      ]
    }
  ]
}

Required fields

Field Location Required
audience auth.audience or auth.provider_config.audience Yes
svid_file auth.provider_config.svid_file Yes when auth.required is true
One trust source auth.provider_config.public_key_pem, public_key_file, jwks, or jwks_file Exactly one when auth.required is true

workload_api_socket and remote jwks_uri values are rejected. Project the JWT-SVID and a local public key or JWKS into the workload instead.

Run it

agentcli exec manifest.json call-peer

Resolve identity without executing the task:

agentcli identity resolve manifest.json call-peer

When to use this provider

Use spiffe-jwt-svid in Kubernetes clusters with SPIRE agent or Istio that project JWT-SVIDs and trust material into pod volumes. The declared principal, when present, must exactly match the cryptographically verified sub claim. The provider verifies the signature, issuer, requested audience, activation time, expiration, and SPIFFE ID shape before returning a session.

Quick Setup: entra-agent-id

Use this for enterprise agent identities registered in the Microsoft Entra Agent Registry. This is distinct from azure-managed-identity: Entra Agent ID uses JWT bearer client assertions and supports agent-specific Conditional Access policies and lifecycle governance.

Manifest

{
  "version": "0.2",
  "identity_profiles": [
    {
      "id": "entra-agent",
      "provider": "entra-agent-id",
      "subject": {
        "kind": "service",
        "principal": "agent://entra/contoso/my-agent",
        "delegation_mode": "none"
      },
      "auth": {
        "mode": "service",
        "scopes": ["https://graph.microsoft.com/.default"],
        "required": true,
        "provider_config": {
          "tenant_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
          "blueprint_app_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
          "agent_identity_id": "c3d4e5f6-a7b8-9012-cdef-123456789012"
        }
      },
      "trust": {
        "level": "supervised"
      },
      "presentation": {
        "bindings": [
          {
            "source": "credentials.access_token.value",
            "target": { "kind": "env", "name": "ENTRA_ACCESS_TOKEN" },
            "required": true,
            "redact": true
          }
        ],
        "cleanup": "always"
      }
    }
  ],
  "workflows": [
    {
      "id": "entra-ops",
      "name": "Entra Operations",
      "contract": {
        "sandbox": "permissive",
        "network": "unrestricted",
        "audit": "always"
      },
      "tasks": [
        {
          "id": "query-graph",
          "name": "Query Microsoft Graph",
          "shell": {
            "program": "sh",
            "args": ["-lc", "curl -H \"Authorization: Bearer $ENTRA_ACCESS_TOKEN\" https://graph.microsoft.com/v1.0/me"]
          },
          "target": { "session_target": "shell" },
          "schedule": { "cron": "0 0 * * *", "tz": "UTC" },
          "identity": { "ref": "entra-agent" }
        }
      ]
    }
  ]
}

Required fields

Field Location Required
tenant_id auth.provider_config.tenant_id Yes (Entra tenant GUID)
blueprint_app_id auth.provider_config.blueprint_app_id Yes (blueprint application GUID, used as client_id)
agent_identity_id auth.provider_config.agent_identity_id Yes (agent identity GUID)
authority auth.provider_config.authority No (defaults to https://login.microsoftonline.com/{tenant_id})

The client assertion (a platform-issued JWT) is resolved in this order:

  1. AGENTCLI_ENTRA_CLIENT_ASSERTION environment variable
  2. auth.inputs.client_assertion.value_from (env, file, or command indirection)
  3. auth.provider_config.client_assertion (inline string or value_from)
  4. IMDS fallback (acquires a managed identity token for the blueprint app)

Run it

export AGENTCLI_ENTRA_CLIENT_ASSERTION="eyJhbGciOi..."
agentcli exec manifest.json query-graph

Resolve the identity session without executing the task:

agentcli identity resolve manifest.json query-graph

When to use this provider

Use entra-agent-id when your agent is registered in the Microsoft Entra Agent Registry and needs to authenticate using agent-specific credentials. The provider uses client credentials flow with a JWT bearer assertion (urn:ietf:params:oauth:client-assertion-type:jwt-bearer), which is distinct from the IMDS-based flow used by azure-managed-identity. On Azure infrastructure, the provider can fall back to IMDS to acquire the client assertion automatically. Outside Azure, you must provide the assertion via environment variable, file, or command source.

Dynamic Credential Acquisition

The value_from pattern supports four sources for resolving sensitive values without embedding them in the manifest:

Source Usage Example
env Read from an environment variable { "env": "MY_SECRET" }
file Read from a file on disk { "file": "/run/secrets/token" }
literal Inline value (use sparingly) { "literal": "static-value" }
command Run a shell command and capture stdout { "command": "vault kv get -field=token secret/app" }

The command source runs the specified string through the platform shell with a 30-second timeout. On Unix-like hosts this uses sh -c; on Windows it uses cmd.exe /d /s /c. It captures stdout, trims whitespace, resolves relative paths from the current working directory, and returns the result. If the command fails (non-zero exit), the value resolves to null.

When to use command

Use command when credentials are managed by an external tool that exposes them via CLI:

"inputs": {
  "client_secret": {
    "value_from": {
      "command": "vault kv get -field=api_key secret/myapp"
    }
  }
}

Common patterns:

Tool Command
HashiCorp Vault vault kv get -field=token secret/path
1Password CLI op item get "API Key" --fields credential
AWS SSM aws ssm get-parameter --name /app/secret --with-decryption --query Parameter.Value --output text
Stripe Projects stripe projects env --pull --format env 2>/dev/null | grep STRIPE_API_KEY | cut -d= -f2
macOS Keychain security find-generic-password -a account -s service -w
Doppler doppler secrets get API_KEY --plain

Security considerations

  • The command inherits the current environment, so tools that use env-based auth (like VAULT_TOKEN for Vault) will work transparently.
  • The command runs with the same permissions as the agentcli process.
  • Stdout is captured and trimmed. Stderr is discarded.
  • The 30-second timeout prevents hanging on interactive prompts.
  • Command values are NOT persisted in audit records. Only the fact that a command source was used is recorded.

Example: Stripe Projects credential sync

stripe-projects.json demonstrates using stripe projects env --pull as a credential source alongside direct STRIPE_API_KEY binding. The workflow syncs project credentials, checks project status, and runs database migrations with strict trust enforcement and failure triage.

stripe projects env --pull          # populate local .env
agentcli validate examples/stripe-projects.json
agentcli exec examples/stripe-projects.json check-project-status --signer none

Trust Levels

Trust levels declare how much autonomy a task's identity is granted. There are four levels, from least to most privileged:

Level Meaning
untrusted No trust. Suitable for sandboxed or throwaway operations.
restricted Limited trust. May read but not modify sensitive resources.
supervised Standard operating trust. Human oversight assumed. Default if not specified.
autonomous Full autonomy. The task can act independently without human oversight.

Setting the trust level

Set the trust level on the identity profile:

"trust": {
  "level": "supervised"
}

Requiring a trust level on a contract

Set required_trust_level and trust_enforcement on the workflow or task contract:

"contract": {
  "sandbox": "permissive",
  "network": "unrestricted",
  "audit": "always",
  "required_trust_level": "supervised",
  "trust_enforcement": "strict"
}

Enforcement modes

Mode Behavior when trust is below required level
none Recorded in audit but execution proceeds. No warning.
advisory A warning is emitted and recorded. Execution proceeds.
strict Execution fails with error code trust_level_insufficient.

Sandbox and Network Contract Modes

The trust settings above answer "is this identity trusted enough to run this task?"

The contract block also answers "what execution boundary is this task supposed to run inside?"

Sandbox modes

Mode Meaning
none No sandboxing intent is declared.
permissive The task may run locally without strong isolation, but the manifest still records boundary intent.
strict The task is intended to run in a stronger sandbox.

Network modes

Mode Meaning
unrestricted Normal network access is allowed.
restricted Network access should be narrowed by the runtime or environment.
none The task should run without network access.

What local agentcli exec enforces today

Local agentcli exec enforces the contract before spawning:

  • allowed_paths: verifies the effective working directory, resolves symlinks, and requires an enforceable filesystem sandbox
  • required_trust_level + trust_enforcement: enforced
  • audit: enforced
  • sandbox: strict: requires supported operating-system isolation
  • network: restricted or network: none: requires supported network isolation

If a restrictive sandbox, allowed-path, or network boundary cannot be enforced, execution fails with a sandbox or contract error. It never warns and proceeds outside the requested boundary. sandbox: permissive with network: unrestricted and no allowed_paths explicitly accepts execution without strong isolation.

Use this rule of thumb:

  • local static preview: use exec --dry-run; it does not probe sandbox support
  • live local execution without isolation: explicitly use permissive and unrestricted with no allowed_paths
  • restrictive execution: use a supported macOS sandbox boundary or run agentcli inside a container or another operating-system isolation layer
  • other OSes: keep the contract declaration, but rely on a backend or environment that can enforce it until an OS-specific adapter is available
  • if you want no warning during local runs on an unsupported machine, use sandbox: "none" and network: "unrestricted"

For production-grade isolation on Linux or Windows, the recommended path today is run agentcli exec inside a container and let the container's namespace / AppContainer boundary provide the sandbox. The manifest declaration remains valid metadata; the OS-level boundary comes from the execution environment rather than an in-process sandbox adapter. Native Linux (bubblewrap / seccomp-bpf) and Windows (AppContainer / Job Objects) adapters are on the roadmap but not in the current release.

Example: require supervised trust for production tasks

{
  "version": "0.2",
  "identity_profiles": [
    {
      "id": "prod-agent",
      "provider": "env-bearer",
      "subject": {
        "kind": "service",
        "principal": "agent://myorg/prod-deployer"
      },
      "auth": {
        "mode": "service",
        "required": true,
        "provider_config": { "token_env": "DEPLOY_TOKEN" }
      },
      "trust": { "level": "supervised" }
    }
  ],
  "workflows": [
    {
      "id": "prod-deploy",
      "name": "Production Deploy",
      "contract": {
        "sandbox": "permissive",
        "network": "unrestricted",
        "audit": "always",
        "required_trust_level": "supervised",
        "trust_enforcement": "strict"
      },
      "tasks": [
        {
          "id": "deploy",
          "name": "Deploy",
          "shell": { "program": "deploy.sh", "args": [] },
          "target": { "session_target": "shell" },
          "schedule": { "cron": "0 0 * * *", "tz": "UTC" },
          "identity": { "ref": "prod-agent" }
        }
      ]
    }
  ]
}

If a task references an identity with "level": "restricted" and the contract enforces "trust_enforcement": "strict" with "required_trust_level": "supervised", execution will fail:

Error: Trust level "restricted" is below required "supervised"

Credential Presentation

Presentation bindings control how resolved credentials are delivered to the subprocess.

Environment variable binding (most common)

The binding reads a value from the credential session using a dot-path and writes it to a named environment variable in the subprocess:

"presentation": {
  "bindings": [
    {
      "source": "credentials.access_token.value",
      "target": { "kind": "env", "name": "AZURE_ACCESS_TOKEN" },
      "required": true,
      "redact": true
    }
  ]
}

After resolution, the subprocess will have AZURE_ACCESS_TOKEN set to the token value.

File binding

For tools that read credentials from a file path, use a file target. agentcli writes the credential to a temporary file with mode 0600 and cleans it up after execution:

"presentation": {
  "bindings": [
    {
      "source": "credentials.access_token.value",
      "target": { "kind": "file", "prefix": "my-cred" },
      "format": "raw"
    }
  ]
}

The temporary file is created under the system temp directory. Use --presentation-debug to see the materialization summary including temp file counts:

agentcli exec manifest.json my-task --dry-run --presentation-debug

Multiple bindings

You can bind multiple values from the same session. For example, bind both the token and the token type:

"bindings": [
  {
    "source": "credentials.access_token.value",
    "target": { "kind": "env", "name": "API_TOKEN" }
  },
  {
    "source": "credentials.access_token.audience",
    "target": { "kind": "env", "name": "API_AUDIENCE" }
  }
]

Source paths

The source field is a dot-delimited path into the credential session object. Common paths:

Path Value
credentials.access_token.value The raw token string
credentials.access_token.audience The audience claim
credentials.access_token.scopes Array of granted scopes
credentials.access_token.expires_at ISO 8601 expiration timestamp
subject.principal The resolved principal
provider_assertions.token_endpoint The token endpoint used (OIDC providers)

Format options

The format field on a binding controls how the value is serialized before writing:

Format Behavior
raw String conversion (default)
json JSON-encoded
base64 Base64-encoded

Cleanup

Presentation supports a cleanup field that controls when temporary files are deleted:

"presentation": {
  "cleanup": "always",
  "bindings": [...]
}

Cleanup runs after live inspection or execution completes. Static dry-runs never materialize credentials and therefore have no provider artifacts to clean up.

Evidence and Attestation

Evidence profiles produce cryptographically signed records that bind an execution to a declared identity, command, and result. This is separate from the signing-based attestation in v0.1 manifests.

Configure an evidence profile

Define an evidence profile at the top level of the manifest:

"evidence_profiles": [
  {
    "id": "ssh-evidence",
    "provider": "ssh",
    "payload": {
      "bind": ["execution_id", "declared_identity", "contract", "command", "result"],
      "format": "canonical-json"
    },
    "verify": { "required": false }
  }
]

The evidence declaration selects additional sections and context, while the stored envelope always retains the complete versioned binding needed for verification. That canonical binding includes the manifest digest, effective task hash, execution id, audit-safe identity and command descriptors, result, and postcondition. Raw credentials, stdin, stdout, and stderr are excluded.

Reference the evidence profile from a task

"tasks": [
  {
    "id": "secured-task",
    "name": "Secured Task",
    "shell": { "program": "echo", "args": ["hello"] },
    "target": { "session_target": "shell" },
    "identity": { "ref": "my-identity" },
    "evidence": { "ref": "ssh-evidence" }
  }
]

Run with evidence

agentcli exec manifest.json secured-task

The evidence provider signs the payload using your SSH key (discovered automatically from ~/.ssh/id_ed25519, ~/.ssh/id_ecdsa, or ~/.ssh/id_rsa, or set explicitly via AGENTCLI_SIGNING_KEY).

To require evidence and fail if signing is not possible:

agentcli exec manifest.json secured-task --require-evidence

Verify after execution

Use the execution ID from the result to verify:

agentcli verify <execution-id>

Provide an explicit allowed signers file if needed:

agentcli verify <execution-id> --allowed-signers ~/.ssh/allowed_signers

The verify command reads the audit log, finds the record with the matching execution ID, and checks the attestation signature against the recorded principal.

Full example with identity and evidence

{
  "version": "0.2",
  "identity_profiles": [
    {
      "id": "build-agent",
      "provider": "env-bearer",
      "subject": {
        "kind": "service",
        "principal": "agent://myorg/build-agent"
      },
      "auth": {
        "mode": "service",
        "required": true,
        "provider_config": { "token_env": "BUILD_TOKEN" }
      },
      "trust": { "level": "supervised" },
      "presentation": {
        "bindings": [
          {
            "source": "credentials.access_token.value",
            "target": { "kind": "env", "name": "CI_TOKEN" },
            "redact": true
          }
        ],
        "cleanup": "always"
      }
    }
  ],
  "evidence_profiles": [
    {
      "id": "ssh-evidence",
      "provider": "ssh",
      "payload": {
        "bind": ["execution_id", "declared_identity", "contract", "command", "result"],
        "format": "canonical-json"
      },
      "verify": { "required": false }
    }
  ],
  "workflows": [
    {
      "id": "ci",
      "name": "CI Pipeline",
      "contract": {
        "sandbox": "permissive",
        "network": "unrestricted",
        "audit": "always"
      },
      "tasks": [
        {
          "id": "build",
          "name": "Build",
          "shell": { "program": "make", "args": ["build"] },
          "target": { "session_target": "shell" },
          "schedule": { "cron": "0 0 * * *", "tz": "UTC" },
          "identity": { "ref": "build-agent" },
          "evidence": { "ref": "ssh-evidence" }
        }
      ]
    }
  ]
}
export BUILD_TOKEN="ghp_xxxxxxxxxxxx"
agentcli exec manifest.json build

Audit Records

Every v0.2 execution with "audit": "always" writes an append-only audit record. Records are stored as newline-delimited JSON in ~/.agentcli/audit.ndjson (or the path set by AGENTCLI_HOME).

Reading audit records

agentcli audit
agentcli audit --limit 5

What an audit record contains

A v0.2 audit record includes:

{
  "execution_id": "a1b2c3d4e5f6...",
  "timestamp": "2026-03-22T10:30:00.000Z",
  "source": {
    "workflow_id": "deploy",
    "task_id": "call-api"
  },
  "declared_identity": {
    "provider": "env-bearer",
    "subject": {
      "principal": "agent://myorg/api-service",
      "kind": "service",
      "issuer": null
    },
    "trust_level": "supervised"
  },
  "resolved_identity": {
    "provider": "env-bearer",
    "subject": { "principal": "agent://myorg/api-service" },
    "trust": { "declared_level": "supervised", "effective_level": "supervised" },
    "credentials": {
      "access_token": {
        "kind": "bearer",
        "value": "[REDACTED]",
        "audience": null,
        "scopes": ["read", "write"],
        "expires_at": null
      }
    },
    "credential_summary": {
      "credential_types": ["bearer"],
      "expires_at": null
    }
  },
  "principal_used": "agent://myorg/api-service",
  "trust": {
    "declared_level": "supervised",
    "effective_level": "supervised"
  },
  "contract": {
    "sandbox": "permissive",
    "network": "unrestricted",
    "audit": "always"
  },
  "command": {
    "program": "sh",
    "args": ["-lc", "curl -H \"Authorization: Bearer $TOOL_ACCESS_TOKEN\" https://api.example.com/deploy"],
    "cwd": "/home/user/project",
    "env_keys": ["TOOL_ACCESS_TOKEN"],
    "stdin_present": false
  },
  "hashes": {
    "command": "sha256:...",
    "result": "sha256:..."
  },
  "result": {
    "exit_code": 0,
    "signal": null,
    "timed_out": false,
    "duration_ms": 1234,
    "stdout_bytes": 42,
    "stderr_bytes": 0,
    "output_hash": "sha256:...",
    "structured_present": false
  },
  "warnings": [],
  "dry_run": false
}

What is redacted

All credential values in resolved_identity.credentials are replaced with [REDACTED]. The audit record never contains raw tokens, secrets, or access credentials. The credential_summary field provides a safe overview of what credential types were present and when they expire.

Stdout and stderr content is not stored in the audit record. Only byte counts and a SHA-256 hash of the combined output are recorded.

Troubleshooting

"Identity provider not found"

The provider name in the identity profile does not match a registered provider. Check the spelling and run agentcli identity providers to see all available providers.

agentcli identity providers

"Bearer token not found: environment variable ... is not set or is empty"

The token_env environment variable is not set in the current shell. Export it before running:

export MY_API_TOKEN="your-token"
agentcli exec manifest.json my-task

"Token file not found" / "Token file is empty"

The file path in provider_config.token_file does not exist or the file contains only whitespace. Verify the file exists and contains a token:

ls -la /path/to/token/file
cat /path/to/token/file | wc -c

"Trust level ... is below required ..."

The task's identity trust level is lower than the contract's required_trust_level and trust_enforcement is strict. Either raise the trust level on the identity profile or lower the contract requirement:

"trust": { "level": "supervised" }

"Client secret could not be resolved"

The OIDC client credentials provider could not find the client secret. Check that:

  • The value_from.env variable is exported in your shell.
  • The value_from.file path exists and is readable.
  • The secret is defined in either auth.provider_config.client_secret or auth.inputs.client_secret.

"Token endpoint returned HTTP 4xx/5xx"

The OAuth token endpoint rejected the request. Common causes:

  • Wrong client_id or client_secret.
  • The audience or scopes are not configured on the OAuth server.
  • The token endpoint URL is incorrect.

Test the token endpoint directly:

curl -X POST https://auth.example.com/oauth/token \
  -d "grant_type=client_credentials" \
  -d "client_id=my-client-id" \
  -d "client_secret=$OIDC_CLIENT_SECRET" \
  -d "scope=api.read api.write"

"Authorization proof verification failed"

The authorization proof (JWT, detached signature, or certificate) did not pass verification. Check that the proof value is current, cryptographically valid, bound to the canonical manifest, and matches the expected claims. Use the dedicated verification command without executing the task:

agentcli authorization-proof verify manifest.json my-task

"Authorization denied"

An external authorization provider rejected the request. Check the authorization policy configuration and ensure the principal has the required permissions.

Debug flags

Use these flags to get more detail during troubleshooting:

Flag What it shows
--dry-run Static execution plan; all live phases are skipped
--identity-debug Redacted identity session and credential summary during a live or dedicated identity operation
--presentation-debug Materialization summary during a live operation

Example:

agentcli exec manifest.json my-task --dry-run
agentcli identity resolve manifest.json my-task

Validating identity resolution without execution

Resolve and display the identity session for a task without executing it:

agentcli identity resolve manifest.json my-task

Validate the delegation chain:

agentcli identity validate-delegation manifest.json my-task

Stripe Identity Step-Up For Sensitive Commands

Use Stripe Identity as an additional verification signal, not as the task's primary runtime credential.

Recommended pattern:

  1. Put the normal CLI or service credential in an identity_profile.
  2. Put org, delegation, run, and non-secret verification references in identity.subject.attributes.
  3. Require a short-lived signed JWT in authorization_proof for sensitive tasks.
  4. Use jwks_uri or public_key; every JWT proof requires signature verification and a canonical manifest digest claim regardless of verify.required.
  5. If you use OPA, request the actor and step_up sections so policy can see the actor chain and verification summary without reading raw tokens.

The dedicated example manifest is:

That example shows:

  • normal runtime auth via identity_profiles
  • a sensitive task gated by authorization_proof
  • OPA authorization using the actor and step_up request sections
  • actor metadata flowing from identity.subject.attributes
  • evidence payload binding with actor_context and authorization_proof

Example Manifests

The examples/ directory contains complete, runnable manifests for common use cases:

Example Use case
identity-v2.json Basic v0.2 identity with none and env-bearer providers
oidc-service-auth.json OIDC client credentials with token materialization
trust-enforcement.json Graduated trust levels with strict and advisory enforcement
authorization-proof.json JWT-based manifest authorization proof with signature-backed verification
stripe-identity-step-up.json Step-up proof for sensitive commands plus actor-context audit metadata
cloud-workload.json Azure managed identity for cloud workloads

Run any example locally:

agentcli validate examples/trust-enforcement.json
agentcli exec examples/trust-enforcement.json collect-data --dry-run --signer none
agentcli identity resolve examples/cloud-workload.json fetch-prices