Skip to content

Reference

Agents & Keys

Managed agents (app.agents namespace), Agent runtime (me/trace/withConstraints), scoped key lifecycle (keys namespace).

This page covers the full lifecycle of managed agents and the scoped-key primitives they’re built on:

  • app.agents.* — managed agent CRUD and per-agent key rotation. App-only.
  • Agent runtime — workload-side primitives: me(), trace(), withConstraints().
  • keys.* — scoped sub-key lifecycle (derive, rotate, revoke).
// Operator side
const created = await app.agents.create({
name: "research-agent",
scopes: { "<provider-id>": ["scope.read", "scope.write"] },
});
const page = await app.agents.list();
const info = await app.agents.get(agentId);
await app.agents.update(agentId, { displayName: "Research Agent v2" });
await app.agents.suspend(agentId);
await app.agents.resume(agentId);
await app.agents.delete(agentId);
// Workload side
const me = await agent.me();
await agent.trace({ runId: "run_42" }, async () => {
await agent.request(...);
});
const narrowed = agent.withConstraints({ scopes: ["grants:read"] });
// Scoped key lifecycle
const derived = await app.keys.derive({ scopes: ["tokens:retrieve"], expiresIn: 3600 });
const rotated = await app.keys.rotate({ keyId: old.id });
const revoked = await app.keys.revoke({ keyId: leaked.id });

import { App } from "@alter-ai/alter-sdk";
const app = new App({ apiKey });
try {
const created = await app.agents.create({
name: "research-agent",
scopes: { "<provider-id>": ["scope.read", "scope.write"] },
});
console.log(created.apiKey); // shown ONCE
} finally {
await app.close();
}

Provision a managed agent and mint its initial API key.

async create(options: AgentCreateOptions): Promise<AgentCreateResult>
OptionTypeDescription
namestringStable per-app identifier. Required.
displayNamestringHuman-readable label.
type"agent" | "service"Defaults to "agent".
scopesRecord<string, string[]>Required per-provider capability allowlist, maximum 12 KiB as compact UTF-8 JSON. Each listed provider must contain at least one scope; an empty object ({}) provisions a deny-all agent with no provider access until scopes are added.
metadataRecord<string, unknown>Free-form metadata (≤8 KB).
policyRecord<string, unknown>HITL config + policy attributes.
idempotencyKeystringCaller-supplied idempotency key. No CR/LF.

Returns AgentCreateResult. The plaintext apiKey is returned exactly once — store it securely on receipt. On an idempotency replay where the original key cannot be re-issued, apiKey is null; branch on result.apiKey === null to detect replay.

Throws AlterValueError, AgentNameExistsError, AgentCannotMintSubagentsError, IdempotencyKeyBodyMismatchError, IdempotencyKeyAgentRevokedError, IdempotencyKeyAgentInactiveError.

Paginated list of agents in the app.

async list(options?: AgentListOptions): Promise<AgentListResult>
OptionTypeDefaultDescription
includeRevokedbooleanfalseInclude tombstoned agents.
limitnumber100Requested page size (1..1000). The server returns at most 10 agents per page.
offsetnumber0Page offset.

Returns AgentListResult.

Fetch a single agent by UUID.

async get(agentId: string): Promise<AgentInfo>

Throws AgentNotFoundError.

Fetch an active agent by exact name. Returns null when no matching agent exists — doesn’t throw on the common “does it exist?” check.

async getByName(name: string): Promise<AgentInfo | null>
const info = await app.agents.getByName("research-agent");
if (info === null) {
throw new Error("Agent not provisioned");
}
const agent = app.getAgent(info.id);

Update label or scope/policy block on an existing agent.

async update(
agentId: string,
options?: AgentUpdateOptions,
): Promise<AgentInfo>
OptionTypeDescription
displayNamestringNew display name.
metadataRecord<string, unknown>Replacement metadata (pass {} to clear).
scopesRecord<string, string[]>Full desired per-provider allowlist, maximum 12 KiB as compact UTF-8 JSON. Requires expectedVersion. Broadening applies immediately; narrowing is staged.
expectedVersionnumberPositive AgentInfo.version from the snapshot used to build scopes. Required whenever scopes is supplied.
graceSecondsnumberNarrowing grace, 0604800. Requires scopes; 0 applies immediately. Omit for the 24-hour deployment default.
policyRecord<string, unknown>Replacement policy block.

Passing no options is a no-op round-trip. Returns the updated AgentInfo.

const snapshot = await app.agents.get(agentId);
await app.agents.update(agentId, {
scopes: { "<provider-id>": ["scope.read"] },
expectedVersion: snapshot.version,
graceSeconds: 3600,
});

If another operator changes the agent after snapshot was read, the backend throws AgentConcurrentUpdateError. Fetch the agent again, recompute the full desired allowlist from that fresh record, and retry with its version. Never blindly replay scopes built from the stale snapshot: doing so could restore access the other operator removed.

Pause and later restore an agent without revoking its identity.

async suspend(agentId: string): Promise<AgentInfo>
async resume(agentId: string): Promise<AgentInfo>
  • suspend() flips status to "inactive". The agent’s credential calls — agent.request() and agent.proxyRequest() — then throw AgentInactiveError; agent.me() keeps working so a paused workload can self-diagnose.
  • resume() flips status back to "active". Any overdue pending scope narrowing stays on the row and applies on the next auth check or sweeper pass.
await app.agents.suspend(agentId);
await app.agents.resume(agentId);

Hard-revoke an agent and cascade-revoke every API key under it.

async delete(agentId: string): Promise<AgentInfo>

Idempotent — calling delete() on an already-revoked agent returns the same row.

Each managed agent can hold multiple API keys. Multiple keys may remain active during a rotation window; the last-active-key guard prevents accidental lockout. Use the rotation surface for credential rolls without bricking the workload.

async mintKey(agentId: string): Promise<AgentKeyMintResult>
async listKeys(agentId: string): Promise<AgentKeyList>
async deprecateKey(agentId: string, keyId: string): Promise<AgentKey>
async undeprecateKey(agentId: string, keyId: string): Promise<AgentKey>
async revokeKey(
agentId: string,
keyId: string,
options?: { force?: boolean },
): Promise<AgentKey>
  • mintKey — mint a successor key. The plaintext is returned once.
  • listKeys — return every key for the agent (active, deprecated, revoked).
  • deprecateKey — keep the key working but mark it as the old key. The SDK surfaces a deprecation warning when a deprecated key is used so operations dashboards spot “time to roll the workload.”
  • undeprecateKey — clear the deprecation marker.
  • revokeKey — terminal revoke. Default-protective: refuses to revoke the agent’s last active key. Pass force: true to override.

Throws KeyNotFoundError, KeyAlreadyRevokedError, LastActiveKeyError, and — from mintKey, when the agent is at its active-key cap — AgentKeyLimitError (revoke an existing key to free a slot).


Return the calling agent’s own record.

async me(): Promise<AgentInfo>

Calling me() through an impersonating Agent returned by App.getAgent() uses the application’s key and throws MeRequiresAgentKeyError from the backend. The check is enforced server-side; the carve-out lets paused or revoked agents with genuine agent keys call me() for self-diagnosis (but a revoked individual key still throws KeyRevokedError).

const agent = new Agent({ apiKey: process.env.AGENT_API_KEY! });
try {
const info = await agent.me();
console.log(info.status, info.scopes);
} finally {
await agent.close();
}

Run a callback inside an audit-context scope. Every nested SDK call propagates the agent identity, run ID, and optional metadata for audit attribution.

async trace<T>(
options: {
runId?: string;
threadId?: string;
parent?: string | null;
[metadataKey: string]: unknown;
},
callback: () => Promise<T>,
): Promise<T>
OptionTypeDescription
runIdstringStable per-run identifier (e.g. LangChain run_id). Threads through to the audit row.
threadIdstringStable per-conversation identifier.
parentstring | nullExplicit parent identity. Overrides ambient detection.
[metadataKey]stringFree-form metadata. Values must be strings.

Inside the callback, every request(), proxyRequest(), or nested trace() on this agent — or on any other client constructed in the same async context — carries the scope automatically.

await agent.trace({ runId: "run_abc", role: "writer" }, async () => {
await agent.request("GET", url, { grantId });
await researcher.trace({}, async () => {
// parent_agent populated automatically via ambient detection.
await researcher.request(...);
});
});

When an outer trace() of a different agent is active, parent_agent is recorded automatically. Re-entering trace() for the same agent does not set parent_agent.

These keys are reserved and throw AlterValueError when passed via metadata:

  • agent
  • parent_agent
  • run_id (use the explicit runId option instead)
  • thread_id (use the explicit threadId option instead)
  • tool
  • tool_call_id
  • framework

Return a constrained sibling Agent. Pass scopes to narrow the scope set, rule to attach a request rule, or both; at least one is required. Calling it on a closed client throws AlterSDKError. See withConstraints on the Client page for the full surface, including the RequestRule shape.

const readOnly = agent.withConstraints({ scopes: ["grants:read"] });
const page = await readOnly.listGrants();
// Attach a request rule:
const restricted = agent.withConstraints({
rule: {
ruleType: "json_match",
ruleBody: { when: { method: "POST" }, effect: "deny" },
},
});

The constrained sibling preserves the parent agent’s trace() identity — the run ID, thread ID, and metadata propagate through the scope-narrowed client.


The keys namespace lifecycle-manages scoped API keys. Available on both App and Agent — agent callers typically only have permission to call derive().

import { App } from "@alter-ai/alter-sdk";
const app = new App({ apiKey });
try {
const derived = await app.keys.derive({
scopes: ["tokens:retrieve", "grants:read"],
expiresIn: 3600,
cidrAllowlist: ["10.0.0.0/8"],
metadata: { purpose: "ci-deploy" },
});
process.env.CHILD_KEY = derived.apiKey; // shown ONCE
} finally {
await app.close();
}

Mint a short-lived attenuated key under the calling key.

async derive(options: DeriveKeyOptions): Promise<MintedKey>
OptionTypeDescription
scopesreadonly string[]Scopes the derived key holds. Must be a non-empty subset of the caller’s effective scopes. keys:derive is rejected — derived keys cannot themselves derive.
expiresInnumberLifetime in seconds. Capped by org policy (maxDerivedKeyTtlHours, default 24 h) and by the caller’s remaining TTL.
cidrAllowlistreadonly string[]Optional IP allowlist. Must be a subset of the caller’s allowlist.
namestringOptional label.
metadataRecord<string, unknown>Free-form metadata stored on the key.

Returns MintedKey — extends APIKeyInfo with the plaintext apiKey. The plaintext is returned exactly once. Store it securely.

Throws:

Rotate an existing key: mint a same-scope successor with an overlap window where both keys work.

async rotate(options: RotateKeyOptions): Promise<MintedKey>
OptionTypeDefaultDescription
keyIdstring | { toString(): string }The key to rotate. UUID strings and UUID-like objects are accepted. Required.
overlapDaysnumber7Days the old key remains valid. Range 0..30.

Returns MintedKey — the successor’s plaintext is on apiKey, returned once.

Derived (dk) keys cannot be rotated. Derive a new sub-key from the parent instead.

Throws:

Revoke a key and cascade-revoke every derived (dk) descendant.

async revoke(options: RevokeKeyOptions): Promise<APIKeyInfo>
OptionTypeDefaultDescription
keyIdstring | { toString(): string }The key to revoke. UUID strings and UUID-like objects are accepted. Required.
forcebooleanfalseBypass the last-active-key guard.

Default-protective: refuses to revoke the last active key on a managed agent. Pass force: true to override (e.g. incident response on a leaked key without a successor).

Returns the post-revoke APIKeyInfo.

Throws:

Synchronous, no-network format check for a plaintext key. Re-derives the embedded CRC checksum and compares.

import { isValidKey } from "@alter-ai/alter-sdk";
isValidKey("alter_rk_…"); // true | false
isValidKey(undefined); // false
function isValidKey(plainKey: unknown): boolean

Returns false for non-strings, empty strings, unknown key-type discriminators, the wrong segment count, or a mismatched checksum. Returns true for legacy alter_key_… keys (which have no checksum) when the body is non-empty. Does not throw.

This is a usability gate that catches single-character typos before paying a network round-trip. It is not a security boundary — a forged checksum cannot pass backend authentication because the server verifies the key against its hashed fingerprint.

Report an issue with this page

Necessary

Required for sign-in, security, authorization, and remembering your choices.

Always active

Analytics

Helps us understand which product and documentation features are useful.

Performance diagnostics

Uses performance tracing and privacy-masked session replay to diagnose problems.

You can change these choices at any time from Cookie settings.