-
Notifications
You must be signed in to change notification settings - Fork 1
feat: CLI Completeness: Implement agentspec register + Document Full CLI Surface #13
Description
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 inpackages/control-plane/api/register.py) - Auth:
X-Admin-Keyheader (verified against$AGENTSPEC_ADMIN_KEYon 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): voidOrchestrator 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 ininit.ts)commander— already a dependencychalk— 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 buildLabels
cli, enhancement, developer-experience
Milestone
CLI Completeness