Sdk cli

TypeScript SDK

The @armoriq/sdk package for Node.js agents. ArmorIQClient, capturePlan / getIntentToken / invoke loop, per-call user scoping, and known parity gaps with the Python SDK.

The TypeScript SDK ships as @armoriq/sdk on npm. Use it from any Node.js 18+ runtime — Express servers, serverless functions, Deno, Bun.

npm install @armoriq/sdk
# or pnpm add / yarn add

There is no TypeScript CLI. Use the Python CLI (pipx install armoriq-sdk) for armoriq login, armoriq orgs, and armoriq switch-org. Both SDKs read the same ~/.armoriq/credentials.json — a single login works for both runtimes.

Minimum setup

import { ArmorIQClient } from '@armoriq/sdk';

const client = new ArmorIQClient({
  apiKey: process.env.ARMORIQ_API_KEY!,
  userId: 'my-service',
  agentId: 'my-agent',
});

Three required options:

  • apiKey — your ArmorIQ API key, typically from an env var. Falls back to ARMORIQ_API_KEY and then the credentials file if omitted.
  • userId — an identifier for the service or worker. Auto-default is not applied on TS (unlike Python).
  • agentId — an identifier for the agent persona. Also required.

All other options (endpoints, proxy mapping, timeouts) auto-resolve to platform defaults.

Full constructor

new ArmorIQClient({
  apiKey: string,                              // required
  userId: string,                              // required
  agentId: string,                             // required
  contextId?: string,                          // default: 'default'
  iapEndpoint?: string,                        // auto-resolves
  proxyEndpoint?: string,
  backendEndpoint?: string,
  proxyEndpoints?: Record<string, string>,     // per-MCP overrides
  timeout?: number,                            // ms, default 30000
  verifySsl?: boolean,                         // default true
  useProduction?: boolean,                     // default true
  mcpCredentials?: Record<string, any>,
})

The plan → token → invoke loop

Same three-step flow as Python. All network operations are async.

capturePlan(llm, prompt, plan, metadata?)

Capture the LLM's planned tool calls. The plan object is required — it must contain a steps array:

const plan = client.capturePlan(
  'gpt-4o',
  "What's the weather in Paris?",
  {
    goal: 'Weather lookup',
    steps: [
      {
        action: 'get_weather',
        mcp: 'weather-mcp',
        params: { city: 'Paris', units: 'celsius' },
      },
    ],
  },
);

console.log(plan.planHash);
console.log(plan.merkleRoot);

getIntentToken(plan, options?)

Mint a short-lived signed token:

const token = await client.getIntentToken(plan, { validitySeconds: 300 });

console.log(token.tokenId);
console.log(token.expiresAt);

invoke(mcp, action, token, params?, merkleProof?, userEmail?)

Execute the tool call through the proxy:

const result = await client.invoke(
  'weather-mcp',
  'get_weather',
  token,
  { city: 'Paris', units: 'celsius' },
  undefined,                     // merkleProof — optional
  'alice@customer.com',          // userEmail — the end user on whose behalf
);

console.log(result.status);
console.log(result.data);

invokeWithPolicy(mcp, action, token, params, options?)

The richer variant. Takes options for user email, hold-approval polling, and delegation:

const result = await client.invokeWithPolicy(
  'weather-mcp',
  'get_weather',
  token,
  { city: 'Paris' },
  {
    userEmail: 'alice@customer.com',
    waitForApproval: true,        // poll on PolicyHold
    holdPollIntervalMs: 3000,
    holdPollAttempts: 20,         // 60s total
  },
);

Use this when the tool call might require human approval — the SDK handles delegation creation and approval-polling automatically.

Email scoping

TypeScript passes the end-user email per call, on invoke() or in invokeWithPolicy options. Every tool call tagged with userEmail appears in audit logs under that user, and policies scoped per-email / per-membership are evaluated against them.

// Per-user invocations
for (const email of ['alice@acme.com', 'bob@acme.com']) {
  const plan  = client.capturePlan(llm, prompt, planDefinition);
  const token = await client.getIntentToken(plan);
  await client.invoke('weather-mcp', 'get_weather', token, {...}, undefined, email);
}

Exception handling

All exceptions extend ArmorIQException. Catch broadly or specifically.

import {
  InvalidTokenException,
  TokenExpiredException,
  IntentMismatchException,
  PolicyBlockedException,
  PolicyHoldException,
  MCPInvocationException,
  ConfigurationException,
} from '@armoriq/sdk';

try {
  const result = await client.invoke('mcp', 'action', token, params);
} catch (err) {
  if (err instanceof TokenExpiredException) {
    const fresh = await client.getIntentToken(plan);
    // retry...
  } else if (err instanceof PolicyBlockedException) {
    console.error(`Blocked: ${err.message}`);
  } else if (err instanceof PolicyHoldException) {
    console.error(`Approval required: delegation=${err.delegationId}`);
  }
}
ExceptionYou'll see it when…What to do
ConfigurationExceptionNo API key, or missing userId/agentIdSet env vars, or pass them to the constructor
InvalidTokenExceptionSignature mismatchCapture a fresh plan + token
TokenExpiredExceptionToken past validitySecondsCall getIntentToken again
IntentMismatchExceptioninvoke args don't match plan.stepsAlign params, or capture a new plan
PolicyBlockedExceptionTool not in user's allow-listUpdate policy in the dashboard
PolicyHoldExceptionApproval requiredUse invokeWithPolicy({ waitForApproval: true })
MCPInvocationExceptionMCP server itself erroredInspect MCP logs; not an ArmorIQ issue

Known gaps vs the Python SDK

The TypeScript SDK is feature-complete for core use cases but has a few parity gaps. All are documented here so you're not surprised.

FeaturePythonTypeScript
ArmorIQClient (core)
capturePlan / getIntentToken / invoke
invokeWithPolicy (with hold auto-retry)
Per-call userEmail on invoke
for_user(email) scope wrappernot yet
from_config("armoriq.yaml")not yet
Session mode (ArmorIQSession)not yet
CLI (armoriq login etc.)not shipped — use Python CLI
Google ADK integrationPython only

For any gap on the TS side, use one of these workarounds:

  • for_user(email) / session mode → pass userEmail per-call on invoke / invokeWithPolicy.
  • from_config → parse the YAML manually with yaml or js-yaml, then pass fields to the constructor.
  • CLI → install and use the Python CLI; the TS runtime reads the same credentials file.

Full worked example

import { ArmorIQClient, TokenExpiredException } from '@armoriq/sdk';

const client = new ArmorIQClient({
  apiKey: process.env.ARMORIQ_API_KEY!,
  userId: 'weather-service',
  agentId: 'weather-bot',
});

async function getWeather(userEmail: string, city: string) {
  const plan = client.capturePlan(
    'gpt-4o',
    `What's the weather in ${city}?`,
    {
      goal: 'Weather lookup',
      steps: [
        {
          action: 'get_weather',
          mcp: 'weather-mcp',
          params: { city, units: 'celsius' },
        },
      ],
    },
  );

  let token = await client.getIntentToken(plan, { validitySeconds: 300 });

  try {
    const result = await client.invoke(
      'weather-mcp',
      'get_weather',
      token,
      { city, units: 'celsius' },
      undefined,
      userEmail,
    );
    return result.data;
  } catch (err) {
    if (err instanceof TokenExpiredException) {
      token = await client.getIntentToken(plan);
      const result = await client.invoke(
        'weather-mcp',
        'get_weather',
        token,
        { city, units: 'celsius' },
        undefined,
        userEmail,
      );
      return result.data;
    }
    throw err;
  }
}

// Usage
const data = await getWeather('alice@customer.com', 'Paris');
console.log(data);

Next steps

On this page