Skip to content

feat: CLI Completeness: Implement agentspec register + Document Full CLI Surface #13

@iliassjabali

Description

@iliassjabali

Problem

Three CLI operations — register, deploy, push — are referenced throughout the codebase but are not first-class documented commands:

Command Current State Impact
agentspec register Not implemented at all. Referenced in 6+ places as a command users should run to obtain an API key. Broken user workflow: agentspec generate --push writes .env.agentspec telling users to run agentspec register, which doesn't exist.
agentspec deploy Implemented as agentspec generate --deploy <k8s|helm>. Not a standalone command, but works. Missing from CLAUDE.md CLI table.
agentspec push Implemented as agentspec generate --push. Not a standalone command, but works. Missing from CLAUDE.md CLI table.

Additionally, CLAUDE.md only documents 6 of the 12 implemented CLI subcommands. The missing commands (migrate, scan, diff, generate-policy, evaluate, probe) and notable flags (--deploy, --push) are undocumented in the project guide.


Scope

Part 1: Implement agentspec register CLI command

Part 2: Update CLAUDE.md CLI commands table to reflect full CLI surface

Part 3: Minor doc comment fix in SDK push types


Part 1: Implement agentspec register

Context

The server-side API already exists and is fully functional:

  • Endpoint: POST /api/v1/register (implemented in packages/control-plane/api/register.py)
  • Auth: X-Admin-Key header (verified against $AGENTSPEC_ADMIN_KEY on the server)
  • Request body:
    {
      "agentName": "string (1-63 chars, k8s DNS label pattern: ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$)",
      "runtime": "docker | local | k8s",
      "manifest": "object | null (optional, full manifest dict)"
    }
  • Response body:
    {
      "agentId": "agt_<uuid4_hex>",
      "apiKey": "<JWT HS256, 30-day expiry>",
      "expiresAt": "2026-04-14T00:00:00Z"
    }
  • Behavior: Idempotent by agentName. Re-registering rotates the API key (revokes old one).
  • Error codes: 403 (bad admin key), 422 (invalid name/runtime), 503 (server not configured)

The CLI wrapper is the missing piece.

Note on --runtime values

The runtime field is a deployment target label — it describes where the agent runs, not which LLM provider it uses. The allowed values are:

Value Meaning
docker Agent runs in a Docker container
local Agent runs locally (dev machine)
k8s Agent runs in Kubernetes

This field is purely informational — it is stored at registration, displayed in the k9s :ao table and API responses, and written as a Kubernetes annotation on AgentObservation CRs. No code path in the codebase makes any behavioral decision based on the runtime value. All agents use the same heartbeat protocol, CR structure, and operator reconciliation logic regardless of runtime. Remote runtimes (e.g. Bedrock, Vertex) can be added later when there is actual differentiated behavior.

1.1 New file: packages/cli/src/commands/register.ts

Command signature:

agentspec register <agent-name> --runtime <runtime> [options]

Options:

Flag Required Default Description
--runtime <rt> Yes Deployment target: docker, local, k8s
--url <url> No http://localhost:8000 Control plane URL. Overridden by $AGENTSPEC_URL. TODO: replace default with production domain once acquired.
--admin-key <key> No Admin key. Resolution order: flag > $AGENTSPEC_ADMIN_KEY env > interactive prompt.
--manifest <file> No Path to agent.yaml to attach to registration.
--json No false Output raw JSON response (for CI/scripting).

Architecture (thin orchestrator + named helpers pattern per CLAUDE.md):

// -- Internal interfaces --
interface RegisterResult {
  agentId: string
  apiKey: string
  expiresAt: string | null
}

// -- Private helpers --
function resolveControlPlaneUrl(flagValue?: string): string
async function resolveAdminKey(flagValue?: string): Promise<string>
function buildRequestBody(agentName, runtime, manifestFile?): { agentName; runtime; manifest? }
async function postRegister(url, adminKey, body): Promise<RegisterResult>
function isRegisterResult(value: unknown): value is RegisterResult
function tryUpdateEnvFile(apiKey: string): boolean
function printRegistrationResult(result, envUpdated, jsonMode): void

// -- Public orchestrator --
export function registerRegisterCommand(program: Command): void

Orchestrator flow:

export function registerRegisterCommand(program: Command): void {
  program
    .command('register <agent-name>')
    .description('Register an agent with the control plane and obtain an API key')
    .requiredOption('--runtime <runtime>', 'Deployment target: docker, local, k8s')
    .option('--url <url>', ...)
    .option('--admin-key <key>', ...)
    .option('--manifest <file>', ...)
    .option('--json', ...)
    .action(async (agentName, opts) => {
      const url = resolveControlPlaneUrl(opts.url)
      try {
        const adminKey = await resolveAdminKey(opts.adminKey)
        const body = buildRequestBody(agentName, opts.runtime, opts.manifest)
        const result = await postRegister(url, adminKey, body)
        const envUpdated = tryUpdateEnvFile(result.apiKey)
        printRegistrationResult(result, envUpdated, opts.json ?? false)
      } catch (err) { ... }
    })
}

Expected output (default mode):

✓ Agent registered: agt_a1b2c3d4e5f6

  API Key (save this — it won't be shown again):

    agentspec_eyJhbGciOiJIUzI1NiIs...

  ✓ Updated .env.agentspec with AGENTSPEC_KEY

  Expires: 2026-04-14T00:00:00Z

Expected output (when .env.agentspec doesn't exist):

✓ Agent registered: agt_a1b2c3d4e5f6

  API Key (save this — it won't be shown again):

    agentspec_eyJhbGciOiJIUzI1NiIs...

  To configure push mode, set these environment variables:
    export AGENTSPEC_KEY=agentspec_eyJhbGciOiJIUzI1NiIs...
    export AGENTSPEC_URL=http://localhost:8000

Expected output (--json):

{
  "agentId": "agt_a1b2c3d4e5f6",
  "apiKey": "agentspec_eyJhbGciOiJIUzI1NiIs...",
  "expiresAt": "2026-04-14T00:00:00Z"
}

Error output examples:

✗ Invalid admin key. Check --admin-key flag or $AGENTSPEC_ADMIN_KEY environment variable.
✗ Cannot reach control plane at http://localhost:8000. Check --url or $AGENTSPEC_URL.
✗ Registration failed (422): agentName must match pattern ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$

Interactive admin key prompt (when no flag and no env var):

Uses @clack/prompts password() input:

◆ Enter your admin key (AGENTSPEC_ADMIN_KEY):
│ ••••••••••••••••••••
└

1.2 Edit: packages/cli/src/cli.ts

Add import and registration:

import { registerRegisterCommand } from './commands/register.js'
// ...
registerRegisterCommand(program)

This adds register as the 13th subcommand alongside the existing 12.


1.3 New file: packages/cli/src/__tests__/register.test.ts

Test suite using vitest with fetch mocking (same pattern as packages/cli/src/__tests__/generate.test.ts).

Test cases:

# Test Setup Assertion
1 Default URL fallback No flag, no env Fetch called with http://localhost:8000
2 URL from $AGENTSPEC_URL env Set env var Fetch called with env var URL
3 URL from --url flag over env Set both Fetch called with flag URL
4 Admin key from --admin-key flag Pass flag X-Admin-Key header uses flag value
5 Admin key from $AGENTSPEC_ADMIN_KEY env Set env var X-Admin-Key header uses env value
6 Happy path: prints API key Mock 200 Prints agent ID, API key, expiry
7 --json flag: raw JSON output Mock 200 stdout is valid JSON
8 HTTP 403: clear error message Mock 403 Prints "Invalid admin key"
9 HTTP 422: forwards validation error Mock 422 Prints forwarded message
10 HTTP 503: server config message Mock 503 Prints "Control plane not configured"
11 Network error: cannot reach message Mock throws Prints "Cannot reach control plane"
12 --manifest flag: includes manifest Valid YAML file Request body contains manifest
13 .env.agentspec exists: replaces placeholder Write placeholder file File updated with real key
14 .env.agentspec absent: prints export instruction No file Prints export AGENTSPEC_KEY=...

Part 2: Update CLAUDE.md CLI Commands Table

Edit: CLAUDE.md — replace the CLI Commands section

Updated (13 commands + notable flags):

Command Description
agentspec validate <file> Schema validation only (no I/O)
agentspec health <file> Runtime health checks (env, model, MCP, memory, services)
agentspec audit <file> Compliance scoring against rule packs
agentspec init [dir] Interactive manifest wizard
agentspec generate <file> --framework <fw> Code generation via framework adapter
agentspec generate <file> --deploy <k8s|helm> Deployment manifest generation (k8s is deterministic, helm uses Claude)
agentspec generate <file> --push Write .env.agentspec with push-mode env var placeholders
agentspec export <file> --format <fmt> Export to A2A AgentCard or AGENTS.md
agentspec register <name> --runtime <rt> Register agent with control plane, obtain API key
agentspec migrate <file> Migrate older manifest versions
agentspec scan <file> Security scan of agent source code
agentspec diff <file> Diff manifest against previous version
agentspec generate-policy <file> Generate security policy from manifest
agentspec evaluate <file> Run evaluation suite against live agent
agentspec probe <file> Probe agent runtime capabilities

Part 3: SDK Doc Comment Fix

Edit: packages/sdk/src/agent/push.ts line 11

Before:

/** Bearer token obtained from `agentspec register` or POST /api/v1/register */

After:

/** Bearer token obtained from `agentspec register` */

Since agentspec register is now a real command, the fallback reference to the raw API endpoint is unnecessary.


Files Changed Summary

File Action Lines (est.)
packages/cli/src/commands/register.ts New ~177
packages/cli/src/__tests__/register.test.ts New ~350
packages/cli/src/cli.ts Edit (add import + registration call) +2
CLAUDE.md Edit (replace CLI commands table) ~+15 net
packages/sdk/src/agent/push.ts Edit (simplify doc comment) 1 line change

Total: 2 new files, 3 edits.


Dependencies

  • @clack/prompts — already a dependency of the CLI package (used in init.ts)
  • commander — already a dependency
  • chalk — already a dependency
  • Native fetch — already used throughout (Node 18+ built-in)
  • No new dependencies required.

Verification

# Unit tests pass
pnpm --filter @agentspec/cli test register

# CLI help shows the new command
node packages/cli/dist/cli.js register --help

# Full workspace tests still pass
pnpm test

# Build succeeds
pnpm --filter @agentspec/cli build

Labels

cli, enhancement, developer-experience

Milestone

CLI Completeness

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions