TypeScript SDK
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.Agentruntime — workload-side primitives:me(),trace(),withConstraints().keys.*— scoped sub-key lifecycle (derive,rotate,revoke).
// Operator sideconst created = await app.agents.create({ name: "research-agent" });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.delete(agentId);
// Workload sideconst me = await agent.me();await agent.trace({ runId: "run_42" }, async () => { await agent.request(...);});const narrowed = agent.withConstraints({ scopes: ["grants:read"] });
// Scoped key lifecycleconst 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 });app.agents namespace
Section titled “app.agents namespace”import { App } from "@alter-ai/alter-sdk";
const app = new App({ apiKey });const created = await app.agents.create({ name: "research-agent" });console.log(created.apiKey); // shown ONCEcreate
Section titled “create”Provision a managed agent and mint its initial API key.
async create(options: AgentCreateOptions): Promise<AgentCreateResult>| Option | Type | Description |
|---|---|---|
name | string | Stable per-app identifier. Required. |
displayName | string | Human-readable label. |
type | "agent" | "service" | Defaults to "agent". |
scopes | Record<string, string[]> | Per-provider scope allowlist. |
metadata | Record<string, unknown> | Free-form metadata (≤8 KB). |
policy | Record<string, unknown> | HITL config + policy attributes. |
idempotencyKey | string | Caller-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>| Option | Type | Default | Description |
|---|---|---|---|
includeRevoked | boolean | false | Include tombstoned agents. |
limit | number | 100 | Page size (1..1000). |
offset | number | 0 | Page offset. |
Returns AgentListResult.
Fetch a single agent by UUID.
async get(agentId: string): Promise<AgentInfo>Throws AgentNotFoundError.
getByName
Section titled “getByName”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
Section titled “update”Update label or scope/policy block on an existing agent.
async update( agentId: string, options?: AgentUpdateOptions,): Promise<AgentInfo>| Option | Type | Description |
|---|---|---|
displayName | string | New display name. |
metadata | Record<string, unknown> | Replacement metadata (pass {} to clear). |
scopes | Record<string, string[]> | Replacement per-provider scope allowlist. Narrowing rejected. |
policy | Record<string, unknown> | Replacement policy block. |
Passing no options is a no-op round-trip. Returns the updated AgentInfo.
Throws AgentScopeNarrowingNotSupportedError when scope updates drop providers or remove scopes from an existing provider.
delete
Section titled “delete”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.
Key rotation
Section titled “Key rotation”Each managed agent can hold multiple API keys, exactly one of which is active at any time after the initial mint. 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. Passforce: trueto override.
Throws KeyNotFoundError, KeyAlreadyRevokedError, LastActiveKeyError.
Agent runtime
Section titled “Agent runtime”Agent.me
Section titled “Agent.me”Return the calling agent’s own record.
async me(): Promise<AgentInfo>Calling me() on an App client throws MeRequiresAgentKeyError from the backend. The check is enforced server-side; the carve-out lets paused or revoked agents call me() for self-diagnosis (but a revoked individual key still throws KeyRevokedError).
const agent = new Agent({ apiKey: process.env.AGENT_API_KEY! });const info = await agent.me();console.log(info.status, info.scopes);Agent.trace
Section titled “Agent.trace”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>| Option | Type | Description |
|---|---|---|
runId | string | Stable per-run identifier (e.g. LangChain run_id). Threads through to the audit row. |
threadId | string | Stable per-conversation identifier. |
parent | string | null | Explicit parent identity. Overrides ambient detection. |
[metadataKey] | string | Free-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(...); });});Ambient parent detection
Section titled “Ambient parent detection”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.
Reserved metadata keys
Section titled “Reserved metadata keys”These keys are reserved and throw AlterValueError when passed via metadata:
agentparent_agentrun_id(use the explicitrunIdoption instead)thread_id(use the explicitthreadIdoption instead)tooltool_call_idframework
Agent.withConstraints
Section titled “Agent.withConstraints”Return a constrained sibling Agent. Pass scopes to narrow the scope set, rule to attach a one-off deny 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 one-off deny 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.
keys namespace
Section titled “keys namespace”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 });
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 ONCEderive
Section titled “derive”Mint a short-lived attenuated key under the calling key.
async derive(options: DeriveKeyOptions): Promise<MintedKey>| Option | Type | Description |
|---|---|---|
scopes | readonly 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. |
expiresIn | number | Lifetime in seconds. Capped by org policy (maxDerivedKeyTtlHours, default 24 h) and by the caller’s remaining TTL. |
cidrAllowlist | readonly string[] | Optional IP allowlist. Must be a subset of the caller’s allowlist. |
name | string | Optional label. |
metadata | Record<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:
AlterValueError—scopesempty,scopescontainskeys:derive,expiresInnon-positive.InsufficientScopeError— caller lackskeys:derive.BackendError— scope-not-subset failure or any other lifecycle failure.
rotate
Section titled “rotate”Rotate an existing key: mint a same-scope successor with an overlap window where both keys work.
async rotate(options: RotateKeyOptions): Promise<MintedKey>| Option | Type | Default | Description |
|---|---|---|---|
keyId | string | — | The key to rotate. Required. |
overlapDays | number | 7 | Days 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:
AlterValueError—keyIdmissing,overlapDaysout of range.InsufficientScopeError— caller lackskeys:adminon the target.BackendError— target not found, already revoked, or is a derived key.
revoke
Section titled “revoke”Revoke a key and cascade-revoke every derived (dk) descendant.
async revoke(options: RevokeKeyOptions): Promise<APIKeyInfo>| Option | Type | Default | Description |
|---|---|---|---|
keyId | string | — | The key to revoke. Required. |
force | boolean | false | Bypass 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:
AlterValueError—keyIdmissing.InsufficientScopeError— caller lackskeys:adminon the target.BackendError— target not found, last-active withoutforce.
isValidKey
Section titled “isValidKey”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 | falseisValidKey(undefined); // falsefunction isValidKey(plainKey: unknown): booleanReturns 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.