feat: bidirectional agent handoff with sub-agent lifecycle#25
Merged
Conversation
Comprehensive design for agent live migration (local ↔ cloud): - Identity succession protocol (Ed25519 signed, no key transfer) - Reclamation protocol (co-signed reverse handoff) - Sub-agent re-spawn with state injection - Three-layer handoff endpoint auth (handoff token + no localhost bypass + mutual attestation) - Security review: 11 threat findings with mitigations - Handoff trigger security (confirmation token, time delay, AGT policy gate) - UX design across webchat, TUI, and Telegram - Demo script and implementation phases (H1-H4) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Router-side handoff infrastructure for agent live migration (local ↔ cloud): ## New module: handoff.rs (1368 lines) - HandoffState, SubAgentSnapshot, HandoffMetadata, CredentialRef structs - HandoffTokenStore: in-memory, TTL-based, one-at-a-time token management - 32-byte random tokens, max 10min TTL, constant-time comparison - Token hash logged for audit (never the token value) - HandoffSession: phase tracking across the full handoff lifecycle (idle → initialized → draining → snapshotting → transferring → restoring → verifying → decommissioning → complete | failed | aborted) - DrainState: stops new work during handoff, tracks duration - State serialization: JSON + gzip compression - State encryption: AES-256-GCM with HKDF-SHA256 key derivation Key derived from shared secret + salt using 'azureclaw-handoff-v1' info - Verification: SHA-256 hash of plaintext for integrity checking - 21 unit tests covering token store, serialization, encryption, sessions ## New endpoints (8 routes, three auth tiers) 1. POST /agt/handoff/init — admin token only, NO localhost bypass 2. POST /agt/handoff/snapshot — creates encrypted state blob 3. POST /agt/handoff/restore — decrypts, validates, restores state 4. POST /agt/handoff/verify — returns verification digest 5. POST /agt/handoff/drain — enters drain mode 6. POST /agt/handoff/decommission — agent goes dormant 7. POST /agt/handoff/abort — cancels in-progress handoff 8. GET /agt/handoff/status — read-only (localhost allowed) ## Security: three-layer authentication - Layer 1: Handoff token (one-time, short-lived, CLI-only) Token exists only in CLI process memory — never in pod env - Layer 2: NO localhost bypass for mutation endpoints Prevents prompt injection from exfiltrating state via localhost - Layer 3: Mutual attestation via DH-encrypted state blob (Phase H2 adds Ed25519 succession signature verification) ## All endpoints audit-logged with: - Caller IP, timestamp, endpoint, success/failure - Token hash (not value), state blob size, item counts ## Dependencies added: aes-gcm 0.10, hkdf 0.12, sha2 0.10, rand 0.9, base64 0.22, flate2 1 ## Test results: - 77 unit tests pass (21 new handoff tests) - 26 integration tests pass (updated for new AppState fields) - 74 controller tests pass (unaffected) - clippy clean (zero warnings) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…n, and reclamation
Phase H2 of the agent handoff feature:
Registry topology (local vs global):
- Add RegistryMode enum to router config (AGT_REGISTRY_MODE env var)
- Handoff init returns 409 in local mode with clear guidance
- Global mode does startup health check on AGT_REGISTRY_URL
- Handoff status endpoint exposes registry_mode + handoff_available
Identity succession (A→B):
- SQL migration 008_succession.sql with succession_log table
- POST /v1/registry/succession endpoint with Ed25519 sig verification
- Canonical message format: succession:{pred}:{succ}:{timestamp}
- One-shot rule (unique index on active predecessor)
- Copies reputation A→B, marks predecessor dormant
Identity reclamation (B→A, co-signed):
- POST /v1/registry/reclamation with dual signature verification
- Original succession ref must match active event_hash
- Deactivates succession redirect, copies reputation back
- Sets original online, departing offline
Lookup follows succession redirects:
- lookup_agent checks succession_log for dormant predecessors
- Returns successor with succeeded_from + succession_hash metadata
- Max redirect depth = 1 (no chains)
Dormant presence status:
- New PresenceStatus::Dormant variant in registry
- Ghost cleanup skips dormant agents (preserves succession chains)
- Capability search excludes dormant agents
CLI --global-registry flag:
- azureclaw dev --global-registry <url> skips local registry stack
- Passes AGT_REGISTRY_MODE=global to router
- Health check on global registry at startup
- Status display shows "handoff enabled" for global mode
Tests: 177 Rust (77 unit + 26 integration + 74 controller) + 159 CLI
All passing, clippy clean.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CLI command (cli/src/commands/handoff.ts): - azureclaw handoff <name> --to cloud (forward: local → AKS) - azureclaw handoff <name> --to local (reverse: AKS → local) - azureclaw handoff <name> --status (check progress) - azureclaw handoff <name> --abort (cancel in-progress) - 7-step orchestration: verify → init → snapshot → drain → transfer → succession → summary - Registry mode guard (blocks with clear guidance if not --global-registry) - Shared secret derivation for snapshot encryption - Full error recovery (abort on failure) Plugin tools (cli/src/plugin.ts): - azureclaw_handoff_status: check handoff availability and state - azureclaw_handoff_request: LLM can request handoff, but returns CLI command (tokens are CLI-only to prevent prompt injection attacks) Security design: - Handoff tokens live in CLI process memory only (never LLM-accessible) - LLM tool returns the CLI command to run, not the token itself - This prevents prompt injection from triggering unauthorized handoff Registered in cli.ts under 'Agent mobility' section. Tests: 177 Rust + 159 CLI, all passing, clippy clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ction protection, rate limits §9.9.9 — Two-stage confirmation gate (HIGH priority): - PendingHandoffStore with confirmation token (4-byte random hex) - POST /agt/handoff/pending — creates pending request (rate limited: 1 per 5min) - POST /agt/handoff/confirm — validates token, enforces 3s minimum delay to prevent LLM self-confirm, consumes pending on success - Plugin azureclaw_handoff_request now calls /pending (Stage 1) - New plugin azureclaw_handoff_confirm tool (Stage 2) - 6 unit tests for PendingHandoffStore (create, confirm, delay, rate limit, cancel, wrong token) §9.9.1 — State blob prompt injection protections: - sanitize_chat_snapshot() strips messages matching 17 injection patterns (system prompt override, handoff commands, instruction ignoring) - User messages always preserved (legitimate user content) - Non-UTF8 chat snapshots rejected entirely - Trust scores capped at 750 on restore (cannot import max trust) - 4 unit tests for chat sanitization §9.9.4 — State blob size/DoS limits: - 50MB blob size cap on both snapshot and restore - MAX_WORKSPACE_FILES (100) and MAX_WORKSPACE_FILE_SIZE (10MB) constants - PAYLOAD_TOO_LARGE (413) returned on violation §9.9.3/§9.9.8 — Rate limits: - Succession rate limit: 1 per AMID per 5 minutes (DB-backed) - Reclamation rate limit: 1 per AMID per hour (DB-backed) - check_succession_rate_limit() queries succession_log timestamps §9.9.9 — AGT policy rule (belt-and-suspenders): - handoff-tool-approval rule in azureclaw-default.yaml - type: approval, priority: 75 (higher than tool-allow at 70) - Requires operator approval for tool:azureclaw_handoff_request:* and tool:azureclaw_handoff_confirm:* Tests: 188 Rust (74 controller + 88 router + 26 integration) + 159 CLI All passing, clippy clean, registry cargo check clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lay auth, CLI Phase G1 implementation: - G1a: AGIC Ingress manifest (deploy/agentmesh-ingress.yaml) with NetworkPolicy (postgres locked to registry, registry/relay to AppGW), Azure-managed TLS, WAF rate limiting, WebSocket support for relay - G1b: Entra ID OAuth provider added to agentmesh-registry (authorize, callback, token validation via Microsoft Graph). Existing GitHub + Google providers untouched. - G1c: Deployment manifest updated with OAuth secret references (agentmesh-oauth-credentials), REGISTRY_URL for relay verification - G1d: CLI 'azureclaw mesh auth' command — generates Ed25519 keypair, runs browser-based OAuth flow, stores encrypted identity in ~/.azureclaw/mesh-identity.json (AES-256-GCM, machine-bound key). Subcommands: auth, status, reset. - G1e: CLI 'azureclaw up --global-registry <url>' skips local registry deployment. '--expose-registry' deploys AGIC Ingress to make this cluster's registry the global endpoint. Context persists registry mode. - G1f: Relay registration verification — after Ed25519 signature check, relay calls registry /v1/registry/lookup to confirm AMID is registered. Unregistered/revoked agents rejected. Fails open on registry errors (avoids cascading failures). Gated by REQUIRE_REGISTRATION=true. Security: 4-layer auth chain (WAF → Ed25519 → registry check → OAuth). PostgreSQL never exposed externally (NetworkPolicy enforced). Private keys encrypted at rest (AES-256-GCM). Tests: 188 Rust + 159 CLI passing, all clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ation Tests: - 28 new CLI mesh tests (mesh.test.ts): base58 encoding, Ed25519 keypair generation, AMID derivation, encrypt/decrypt roundtrip, tamper detection, command structure verification - 3 relay registry verifier tests (registry_verify.rs): disabled verifier passthrough, env-based construction, enable logic (compile-gated by pre-existing ed25519-dalek API mismatch in relay) - All 188 Rust + 187 CLI tests passing Documentation: - architecture.md: new 'Global Registry Deployment' section — deployment modes table, 4-layer auth chain diagram, NetworkPolicy enforcement, identity management overview - security.md: new 'Layer 9: Global Registry & Handoff Security' section — relay auth layers table, handoff threat/mitigation matrix, NetworkPolicy diagram, identity-at-rest encryption details Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
In local registry mode, only azureclaw_handoff_status is registered. The request and confirm tools are hidden from the LLM, preventing unnecessary AGT governance prompts for tools that would 409 anyway. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
azureclaw mesh promote — deploys AGIC Ingress + NetworkPolicies to expose the cluster's AgentMesh registry and relay as public endpoints. Updates deployment context to global mode. azureclaw mesh demote — removes Ingress resources and reverts to cluster-local registry. Disables cross-environment handoff. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
mesh promote auto-detects your public IP (via ifconfig.me) and injects the AGIC whitelist-source-range annotation into both Ingress resources. Override with --allow-ip <cidr>. If detection fails, warns and leaves the registry open. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
mesh promote now queries the AGIC Application Gateway for its public IP and generates sslip.io hostnames (e.g. registry.20-30-40-50.sslip.io). No DNS setup needed for testing. TLS is disabled for sslip.io domains (secured by IP allowlist instead). Use --domain for custom domains. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace Ingress-based approach with direct LoadBalancer Service patching. No ingress controller needed. promote patches registry + relay services to LoadBalancer with loadBalancerSourceRanges for IP restriction, waits for external IPs, builds sslip.io URLs. demote reverts to ClusterIP. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the CLI-only handoff confirm flow with full LLM-driven orchestration. After the user confirms the handoff code, the plugin now executes the entire transfer autonomously: 1. Confirm → router creates handoff token (stays in plugin memory) 2. Snapshot → encrypted AES-256-GCM state blob 3. Drain → stop accepting new work 4. Spawn → create cloud target on AKS (or find existing) 5. Transfer → send state blob via E2E encrypted mesh (Signal Protocol) 6. Verify → target restores, sends verification digest back via mesh 7. Succession → registry identity chain update 8. Decommission → local agent enters dormant state Key changes: - handoff_confirm tool: full orchestration instead of returning CLI cmd - onMessage handler: new handoff_transfer message type for target agent to auto-restore state and send verification back - _routerCallStrict: new helper that rejects on HTTP >= 400 - _readAdminToken: reads admin token from filesystem paths - _routerCall: added extraHeaders parameter (backward compatible) - agtReconnect: disconnect before connect to clear stale SDK state Security model (§9.9.9): the LLM can REQUEST a handoff but never EXECUTE one. The handoff token stays in plugin memory — the LLM never sees it. All router calls use this token. Human confirmation via the 2-stage code flow is the gate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When registry is already in global mode, 'azureclaw mesh promote' now: - Checks registry HTTP health (/v1/health) - Checks relay TCP connectivity - If both healthy: reports status and exits - If either dead: kills stale PIDs, clears held ports, restarts fresh port-forward tunnels, verifies connectivity Previously it just said 'already global' and exited, even when the port-forwards had died (e.g. after IP change or sleep). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
_readAdminToken used require('node:fs') which is unavailable in ESM.
Changed to async function with await import('node:fs') and updated
both call sites to await the result.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
In dev mode, the router's /sandbox/spawn endpoint was creating Docker containers. For handoff (local→cloud), we need actual AKS pods. Changes: - Add HandoffMeta struct to SpawnRequest (mode + predecessor fields) - When handoff.mode='restore' in dev mode, bypass Docker path and use K8s CRD creation via kube-rs (kubeconfig mounted from host) - Mount ~/.kube/config into dev container at /run/secrets/kubeconfig so the router can reach the K8s API for handoff spawns The controller already sets AGT_RELAY_URL and AGT_REGISTRY_URL on spawned pods, and NetworkPolicy allows mesh egress — so the handoff target automatically joins the global mesh. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The handoff target was rejecting the source's KNOCK (trust score 0 < threshold 500) because AGT_TRUSTED_PEERS wasn't propagated. Also, AGT_REGISTRY_MODE wasn't set, so handoff tools were skipped. Changes: - CRD: add trusted_peers and registry_mode fields to GovernanceConfig - Controller: propagate AGT_TRUSTED_PEERS and AGT_REGISTRY_MODE to the openclaw container env vars - Spawn: write trusted_peers and registry_mode='global' into CRD governance spec for handoff targets - Spawn: use 'handoff'/'predecessor' labels instead of 'agent'/'parent' for handoff-spawned CRDs (not sub-agents) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sequence diagram covering all 5 phases: two-stage confirm, snapshot/drain, spawn on AKS, E2E mesh transfer, succession/decommission. Includes security model diagram and current vs future (Entra OAuth) trust flow comparison. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Refactor handoff_confirm to return immediately and run orchestration in the background via _runHandoffOrchestration(). The LLM polls handoff_status every 3-5s and relays emoji step updates to the user in real-time instead of blocking for 2-3 minutes. Key changes: - HandoffProgress interface tracks phase, steps[], status, error - _hp() helper updates progress + logs at each step - _runHandoffOrchestration() contains the full 7-step flow: snapshot → drain → spawn → mesh-wait → transfer → verify → succession → decommission - handoff_status returns rich progress with active polling instruction - Module-level _log set during register() for background access Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…udit
sec-6: Fix message filter AND→OR — verification now rejects messages
unless BOTH from_amid AND from_agent match the expected target
sec-1: Propagate AGT_TRUSTED_PEERS and AGT_REGISTRY_MODE to router
container (was only on openclaw container)
sec-2: Validate trusted_peers — reject values with control chars
sec-3: Validate registry_mode — only accept 'local'|'global'
rel-7: Wrap _runHandoffOrchestration in top-level try-catch
rel-6: Replace non-null assertions with explicit guard at completion
rel-3: Bump snapshot timeout 15s→60s, drain timeout 15s→30s
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace Section 11 with 7 comprehensive diagrams:
- 11.1: End-to-end sequence (both source + target sides, live progress)
- 11.2: Handoff state machine with known limitations noted
- 11.3: 7-layer security model (gate → isolation → auth → encryption →
injection hardening → identity → infrastructure)
- 11.4: Env var propagation showing both containers receive vars
- 11.5: Two orchestration paths (LLM vs CLI) and their differences
- 11.6: Trust flow (current unauthenticated vs future Entra OAuth)
- 11.7: Error recovery and planned improvements
Also add nohup.out to .gitignore (stale port-forward logs).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tests
rel-1: Clean up orphaned CRDs on abort when mesh transfer or discovery
fails after spawn (DELETE /sandbox/spawn/<name>)
rel-8: Concurrent handoff guard — reject confirm if handoff already running
sec-4: Respect $KUBECONFIG env var with fallback to ~/.kube/config
test-3: Fix 4 failing spawn error tests — tools handle unreachable router
gracefully (return status JSON), update assertions accordingly
(187/187 tests now pass)
dup-1: Document dual orchestration paths (CLI operator-mode vs plugin
LLM-mode) in handoff.ts header comment + architecture-diagrams.md
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rel-2: Add try_transition() to enforce handoff phase ordering
(Idle→Init→Snapshot→Drain→Transfer→Restore→Verify→Decom→Complete)
rel-4: Add resume() + POST /agt/handoff/resume endpoint to cancel
drain state after abort (Aborted|Draining → Idle)
rel-5: Change body.unwrap() to expect() with message in spawn.rs
dead-1: Remove unused _used field from ActiveToken
test-1: Add 10 state machine transition tests (valid sequence,
invalid skip, abort, fail, resume, restart after complete)
test-2: Add 4 auth token tests (wrong value, no active, after
revoke, wrong pending confirmation code)
201 Rust tests pass (74 controller + 101 router + 26 integration)
Clippy clean with -D warnings
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- handoff_status tool: add since_step param for incremental polling, returns only new_steps since last call so LLM relays one step at a time - vendor SDK patch #9: AgentMeshClient.connect() no longer sets connected=true when transport.connect() returns false, allowing retry - Policy: remove handoff-tool-approval gate (two-step confirmation code mechanism is sufficient; approval gate can return once native UI exists) - config: add promoteMode to DeploymentContext for mesh promote tracking All tests pass: 201 Rust (74+101+26), 187 CLI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The K8s API server was silently stripping these fields because the Helm CRD template only defined enabled/toolPolicy/trustThreshold. spawn.rs wrote the fields and reconciler.rs read them, but the schema validation layer dropped them in between. Root cause of handoff mesh registration failure: target pods never received AGT_TRUSTED_PEERS or AGT_REGISTRY_MODE env vars because the CRD never stored the values. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
AppState::new() only checked /run/secrets/admin-token but the controller mounts the secret at /etc/azureclaw/secrets/admin-token. This caused 'Server misconfiguration: no admin token' on target pods during handoff verification. main.rs had the correct path but its token was only used for the admin_auth_middleware, not the handoff middleware which reads from state.admin_token. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…n, relay URLs Three defensive fixes from comprehensive flow audit: 1. Snapshot endpoint now uses _routerCallStrict (was _routerCall) — if snapshot fails, error surfaces immediately instead of continuing with undefined blob data 2. Target-side handoff_transfer handler now reads direction from the mesh message instead of hardcoding 'local_to_aks' — enables reverse (aks_to_local) handoffs 3. Controller propagates AGT_RELAY_URL and AGT_REGISTRY_URL to the openclaw container (was only on router container) — plugin no longer relies on fallback to router proxy for relay connection All tests pass: 201 Rust (74+101+26), 187 CLI, clippy clean Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nsition All 5 handoff route handlers now use try_transition() instead of set_phase(), returning 409 Conflict on invalid phase transitions. Also fixed the transition rules: Decommissioning is now allowed from Draining (source-side flow: Init→Snapshot→Drain→Decommission skips Verify/Restore which happen on the target router, not the source). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two fixes: 1. Verification hash mismatch: verify endpoint was rebuilding a fresh snapshot (new timestamp, nonce, hostname) instead of using the hash from the restored data. Now restore stores the hash of the decrypted compressed bytes, and verify reuses it. Falls back to build_snapshot for source-side verify (where no restore happened). 2. No proactive progress: LLMs don't autonomously poll tools, so the handoff_confirm tool now awaits _runHandoffOrchestration() and returns all steps when complete, instead of firing-and-forgetting and expecting the LLM to poll handoff_status. All tests pass: 201 Rust (74+101+26), 187 CLI, clippy clean Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The docker_api helper only checked curl's exit code, not the HTTP response status from Docker Engine. This caused silent failures — e.g. container start returning HTTP 404 (network not found) was swallowed and spawn reported success even though the container never started. Add -w flag to capture HTTP status code and return Err for 4xx/5xx responses with the Docker error message extracted from the JSON body. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Workspaces > 512KB are split into chunks sent via separate mesh messages, then reassembled on the receiver side. This lifts the practical workspace transfer limit from ~768KB to ~40MB (80 chunks × 512KB). Sender (sub-agent): - Small workspace (≤512KB): single handoff:workspace_response (fast path) - Large workspace: N handoff:workspace_chunk messages + completion marker - Max 80 chunks (leaves headroom in relay's 100-message offline queue) Receiver (parent): - Collects workspace_chunk messages into a Map keyed by chunk_index - Reassembles in order when all chunks received or completion marker arrives - 30s timeout with partial-chunk recovery (uses what was received) - Backwards compatible: single-message responses still work unchanged Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements a general-purpose auto-chunking transport layer for the E2E encrypted mesh. Any payload exceeding 512KB is transparently split into chunks with per-chunk SHA-256 integrity verification, then reassembled on the receiver side before reaching application logic. Transport layer (meshSend + meshHandleTransportMessage): - meshSend(): auto-chunks large payloads into mesh:transfer_manifest + N mesh:transfer_chunk messages. Small messages pass through directly. - meshHandleTransportMessage(): intercepts transport messages in onMessage, accumulates chunks, verifies SHA-256 hashes, reassembles, and delivers the original message to the application layer. - Per-chunk + manifest-level SHA-256 integrity verification - 2-minute TTL with automatic stale transfer cleanup - Max ~40MB per transfer (80 chunks × 512KB) New tool — azureclaw_mesh_transfer_file: - Agents can send files to each other via E2E encrypted mesh - Files up to 30MB supported (auto-chunked transparently) - Received files auto-saved to /sandbox/.openclaw/workspace/incoming/ - Path traversal protection (must be within /sandbox) Consumers updated to use unified transport: - mesh_send tool: auto-chunks large task messages - Handoff blob transfer: auto-chunks encrypted snapshots - Sub-agent workspace collection: auto-chunks workspace tars - Handoff re-send loop: uses meshSend for retransmit Limits raised: - Router MAX_BLOB_SIZE_BYTES: 50MB → 200MB (sub-agent workspaces) - Plugin workspace tar cap: 5MB → 50MB - CLI workspace tar cap: 5MB → 50MB Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t tests - Make ROUTER and ROUTER_BASE configurable via AZURECLAW_ROUTER_URL env var (defaults to http://127.0.0.1:8443 for backward compat) - Fix 5 pre-existing spawn tool test timeouts caused by local dev router on port 8443 — tests now point to unused port 19876 for immediate ECONNREFUSED instead of 45s polling loop - Fix test assertions to match actual error behavior (plain text errors, not JSON, when router is unreachable) - Add 11 new tests (198 total, up from 187): - mesh_transfer_file: registration, schema, path traversal, abs path, mesh-not-connected - mesh_send: registration, params, error when disconnected - handoff_status: registration, returns status JSON - AZURECLAW_ROUTER_URL: spawn + spawn_status use configurable URL Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1. Plugin reverse: retry with 60s backoff when discovering local target (local agent may be waking from dormant state after CLI runs wakeDormantDocker) 2. Direction validation: both plugin and router now validate that the incoming handoff direction matches the environment (AZURECLAW_DEV_MODE). Warn-only on mismatch — doesn't block to avoid false positives. 3. Forward credential rehydration: collect actual credential VALUES from Docker (not just refs), create K8s secret on AKS target before pod starts so envFrom can mount them. Closes the credential gap where forward handoff lost Telegram/Slack/Brave tokens. 4. Symmetric cleanup: reverse handoff now scales deployment to 0 instead of deleting the CRD. This preserves the sandbox definition for instant re-forward handoff while freeing all compute resources. Falls back to CRD deletion if scale fails. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add handoff:interrupt protocol so sub-agents can save in-progress work before their workspace is collected during a handoff. Plugin path (mesh-based): - Parent sends handoff:interrupt to all sub-agents concurrently - Sub-agents set handoffInterruptRequested flag - processTaskWithTools checks the flag between LLM rounds - On interrupt: saves task progress to .task-in-progress.json (round, messages, last content, original task) - Sends handoff:interrupt_ack back to parent - Parent waits up to 10s for acks, then proceeds with workspace collection CLI path (exec-based): - CLI writes .handoff-interrupt sentinel file into each sub-agent container - processTaskWithTools also checks for this file between rounds - Same progress save behavior (.task-in-progress.json) Both paths ensure the workspace tar includes the progress checkpoint, so it survives the handoff and is available on the target for resumption. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Complete the sub-agent handoff lifecycle — after re-spawn on the target,
sub-agents now receive their workspace and resume interrupted work.
Router changes:
- Restore response now includes sub_agent_workspaces array with each
sub-agent's workspace_tar, task_context, status, and checkpoint
Plugin — target side (post-restore):
- Waits up to 60s for each re-spawned sub-agent to register in mesh
- Sends workspace tar via meshSend (auto-chunked for large workspaces)
- Sends handoff:resume with task context and checkpoint info
Plugin — sub-agent side (new message handlers):
- handoff:workspace_inject: extracts received workspace tar into /sandbox/
with path traversal validation and size guard
- handoff:resume: reads .task-in-progress.json, sends resume_ack to parent
with status report ('Successfully restored in cloud. Resuming interrupted
work from round N: <task>'), then re-enters processTaskWithTools with a
contextual prompt that includes the original task, progress, and last output
The full sub-agent handoff lifecycle is now:
interrupt → save progress → collect workspace → transfer →
re-spawn → inject workspace → resume task → report to parent
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…leanup
Three bugs prevented sub-agents from properly migrating during handoff:
1. collect_sub_agent_snapshots_docker passed raw JSON braces in the
Docker API URL — curl treated {} as glob patterns and the Docker
daemon couldn't parse the filter. Result: always returned 0
sub-agents, so the snapshot blob had no sub-agents to respawn.
Now uses URL-encoded filter via the docker_api() helper (matching
list_sandboxes_docker's pattern).
2. Same function used the Docker container name (azureclaw-{name})
as the agent name, causing respawn to create
azureclaw-azureclaw-{name} on the target. Now strips the prefix.
3. Source decommission only put the main agent dormant — sub-agent
containers kept running. Now destroys all source sub-agents via
the spawn API before decommissioning.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Four fixes for sub-agent handoff orchestration:
1. Sub-agent list used GET /sandbox/spawn (wrong) — now GET /sandbox/list
2. Sub-agent delete used DELETE /sandbox/spawn/{name} (wrong) — now
DELETE /sandbox/{name} (matches actual router routes)
3. Response field was 'sub_agents' but endpoint returns 'sandboxes'
4. No sub-agent info in handoff progress report — added _hp() calls
for: discovery count, interrupt/checkpoint status, workspace
collection count, snapshot inclusion, cleanup status, and
sub_agents_transferred in the final result object
Also fixed orphan target cleanup URLs (2 places) that had the same
/sandbox/spawn/{name} → /sandbox/{name} issue.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The trusted_peers remapping in handoff_restore was dead code — Docker snapshots had trusted_peers=None, so the if-let guard never fired. Re-spawned sub-agents had no trusted parent AMID and rejected handoff:workspace_inject + handoff:resume messages from the new parent. This caused workspace injection to silently fail and task resumption to never trigger. Fix: always set trusted_peers to include the new parent's AMID, regardless of whether the original snapshot had it set: - If peers existed: remap old parent → new parent (existing logic) - If peers existed but old parent absent: append new parent - If peers was None: set to new parent entry (new case) This ensures the sub-agent trusts the new parent on first KNOCK and accepts workspace/resume messages immediately after spawn. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Restructure the restore flow so sub-agent workspace injection and resume happens BEFORE the Telegram greeting. After sending resume signals, wait up to 8s for sub-agents to send resume_ack messages. The Telegram greeting now includes a sub-agent status section showing each agent's name, state (resumed/ready/starting/failed), and a task preview. The handoff_ready mesh message back to the predecessor also now includes sub_agents_restored count, sub_agents_resumed count, and per-agent details. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The operator's unified view fetches both Docker and AKS agents, but the routerExec/fetchAgtQuick/fetchEgressDomains functions used the global devMode flag to choose between docker-exec and kubectl-exec. This meant Docker agents were queried via kubectl (which fails — no K8s namespace) when the operator ran in non-dev mode, and vice versa. Fix: use sb.runtime === 'docker' per-sandbox instead of the global devMode flag in all 4 places that exec into agent containers: - fetchSecurityState (routerExec + k8sCheck) - fetchEgressDomains (routerCurl) - fetchAgtQuick (docker/kubectl exec) - seccomp profile inference Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two fixes for broken parent↔sub-agent communication after handoff: 1. Stale trusted_peers: After handoff, re-spawned sub-agents get new AMIDs but the parent's parentTrustedAmids set contains old AMIDs. KNOCK handler rejects all incoming sub-agent messages (score=0 < threshold=500). Fix: when the plugin discovers a sub-agent's new AMID during workspace injection, register it in amidToName, nameToAmid, parentTrustedAmids, and push baseline trust to router. 2. Silent from_value failure: serde_json::from_value for sub-agent snapshots at POST /handoff/snapshot was wrapped in `if let Ok` which silently swallowed deserialization errors, potentially losing all sub-agent workspace data. Changed to match with tracing::warn that logs the exact error + JSON preview for debugging. Also adds roundtrip test for SubAgentSnapshot workspace_tar through the full serialize→compress→encrypt→decrypt→decompress→deserialize chain, and a test for the JS↔Rust base64 round-trip. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…wlist clawhub.com had a 12% malware rate (Atomic Stealer was #1 skill). openclaw.ai enables `curl | bash` install vectors from inside sandboxes. Both were in the default Helm values and example CRD. Removed from: - deploy/helm/azureclaw/values.yaml (default egress allowlist) - examples/basic-agent/clawsandbox.yaml (example CRD) - PLAN.md (policy presets documentation) Defense is now three layers deep: 1. Egress proxy blocks clawhub.com/openclaw.ai (network) 2. Skills directory is root-owned, chmod 640/750 (filesystem) 3. Plugin code is root-owned, read-only for sandbox (code integrity) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…_workspaces Root cause: the post-restore trust registration and resume signals were gated on restoreResp.sub_agent_workspaces (workspace data), which could be empty even when sub-agents were successfully spawned. This caused the entire block to be skipped — no trust registration, no resume signals, no Telegram sub-agent status. Fix: use restoreResp.sub_agent_results (always populated when sub-agents spawn) as the primary loop driver. Workspace data is looked up by name from sub_agent_workspaces as a secondary source. Also: - Added workspace_inject_ack: sub-agent confirms extraction success/fail with file_count + error back to parent before resume is sent - Parent waits up to 15s for ack, logs result, passes workspace_delivered flag in resume payload - handoff_ready report now includes sub_agents_workspace_delivered count - Telegram greeting shows 📦 icon per sub-agent when workspace arrived - Increased mesh registration wait from 60s to 90s (AKS pods need boot) - Router logs snapshot details when building sub_agent_workspaces Tests added: - Rust: sub_agent_workspaces builder filter (empty/non-empty workspace_tar) - Rust: full encrypt→decrypt→restore round-trip with 2 sub-agents - Rust: edge case — empty workspace + empty task_context filtered out - TS: sub_agent_results drives loop even when sub_agent_workspaces empty - TS: workspace_inject_ack protocol (success + failure paths) - TS: handoff_ready includes workspace delivery status - TS: only spawned sub-agents enter trust loop - TS: missing sub_agent_results graceful fallback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Vendor patch #10: SessionManager.initiateSession() threw 'Active session already exists' when the crypto layer had a session established via incoming KNOCK but client.activeSessions wasn't synced. This broke mesh_transfer_file and any second send to the same peer. Fix: return existing session info with reused=true flag instead of throwing. establishSession detects reuse, syncs activeSessions, and skips redundant KNOCK/activate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…iagnostics Security fix: The handoff confirmation token was returned in the tool result, allowing the LLM to self-confirm without user input. Now the code is sent directly to Telegram (side-channel) and printed to console for TUI users — the LLM never sees it. Changes: - Remove confirmation_token from azureclaw_handoff_request tool response - Send code via Telegram sendMessage as a side-channel delivery - Print code to console.log (visible in kubectl logs, not to LLM) - Update tool descriptions to emphasize code comes from user input - Bump CONFIRMATION_MIN_DELAY_SECS from 3s to 8s - Add console.log diagnostics in post-restore IIFE for handoff debugging - Add test: tool response must not contain confirmation_token Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
10 rounds was too tight for data-heavy tasks — sub-agents hit the cap and returned truncated results. 25 gives enough room for multi-step research/collection while still preventing runaway loops. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…send Sub-agents take 20-30s+ after pod is Running to upload prekeys (gateway start → plugin load → SDK init → relay connect → prekey upload). The previous 8×2s=16s window wasn't enough, causing 'Cannot get prekeys' failures on task dispatch. Now: 15 attempts × 3s = 45s max wait with clearer hint message. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The mesh_transfer_file tool had no delivery confirmation — it reported 'sent' but couldn't verify the file was actually written to disk on the target agent. This caused silent failures where coordinator-notes.txt appeared to transfer but never landed. Now: - Receiver sends file_transfer_ack with success/saved_to/error - Sender waits up to 15s for ack - Tool returns 'delivered' (with path) or 'sent_no_ack' (no confirmation) - Write is verified with stat after writeFileSync Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…reliability mesh_inbox now filters out internal protocol messages (handoff blobs, acks, workspace inject/resume) so the LLM only sees actual sub-agent replies. Shows filtered_protocol_messages count for visibility. file_transfer now retries up to 3 times with ack verification — sends, waits 15s for file_transfer_ack, retries with 3s backoff if no ack. Returns 'delivered' with exact path or 'sent_no_ack' after all retries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
file_transfer messages now show decoded text content (or a binary placeholder) instead of raw base64 blobs. Uses null-byte detection to distinguish text vs binary files. Text files are fully readable in the inbox; binary files show filename and size. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Root cause: trust+resume loop found OLD sub-agent AMIDs still in the registry (Docker containers hadn't timed out yet) and cached them. New AKS sub-agents registered with different AMIDs 26s later, but the parent never discovered them — all messages went to dead relay connections and were silently dropped. Three-layer fix: 1. Stale AMID rejection: collect original_amid from handoff snapshots, reject matching registry results, wait for NEW AMIDs to appear 2. Prekey readiness gate: verify E2E session is established before sending workspace_inject (20 attempts × 3s = 60s max) 3. Workspace inject retry: 3 attempts with 20s ack wait each, catches send errors and retries instead of fire-and-forget Also filters protocol messages from mesh_inbox and auto-decodes file_transfer base64 content so LLM sees readable text. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
After extracting the workspace tar, writes a manifest listing restored user-facing files to /sandbox/.openclaw/workspace/HANDOFF_FILES.md. This makes injected files discoverable when the agent is asked about its workspace contents post-handoff. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Policy: 120 → 240 max_calls/60s for inference:* actions. Router: 100 → 200 global req/s, 10 → 20 per-agent req/s. Handoff with 3+ agents doing workspace inject + resume + relay traffic was hitting the old limits. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copies files from incoming/ to the workspace root so the agent sees them immediately when listing files, without needing to know about the incoming/ directory convention. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
After scaling the parent to 0, also scale down all sub-agent deployments that were snapshotted. Prevents orphaned sub-agent pods running on AKS after reverse handoff. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- New section 12 in architecture-diagrams.md: agent lifecycle across handoff, forward/reverse flows with sub-agents, stale AMID cache poisoning problem & fix, workspace injection detail - CHANGELOG: add handoff features, sub-agent support, rate limit bump - README: add handoff to features list and CLI reference table Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
pallakatos
added a commit
that referenced
this pull request
May 12, 2026
* docs: add global AgentMesh handoff design document
Comprehensive design for agent live migration (local ↔ cloud):
- Identity succession protocol (Ed25519 signed, no key transfer)
- Reclamation protocol (co-signed reverse handoff)
- Sub-agent re-spawn with state injection
- Three-layer handoff endpoint auth (handoff token + no localhost bypass + mutual attestation)
- Security review: 11 threat findings with mitigations
- Handoff trigger security (confirmation token, time delay, AGT policy gate)
- UX design across webchat, TUI, and Telegram
- Demo script and implementation phases (H1-H4)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(handoff): implement Phase H1 — handoff module with three-layer auth
Router-side handoff infrastructure for agent live migration (local ↔ cloud):
## New module: handoff.rs (1368 lines)
- HandoffState, SubAgentSnapshot, HandoffMetadata, CredentialRef structs
- HandoffTokenStore: in-memory, TTL-based, one-at-a-time token management
- 32-byte random tokens, max 10min TTL, constant-time comparison
- Token hash logged for audit (never the token value)
- HandoffSession: phase tracking across the full handoff lifecycle
(idle → initialized → draining → snapshotting → transferring → restoring
→ verifying → decommissioning → complete | failed | aborted)
- DrainState: stops new work during handoff, tracks duration
- State serialization: JSON + gzip compression
- State encryption: AES-256-GCM with HKDF-SHA256 key derivation
Key derived from shared secret + salt using 'azureclaw-handoff-v1' info
- Verification: SHA-256 hash of plaintext for integrity checking
- 21 unit tests covering token store, serialization, encryption, sessions
## New endpoints (8 routes, three auth tiers)
1. POST /agt/handoff/init — admin token only, NO localhost bypass
2. POST /agt/handoff/snapshot — creates encrypted state blob
3. POST /agt/handoff/restore — decrypts, validates, restores state
4. POST /agt/handoff/verify — returns verification digest
5. POST /agt/handoff/drain — enters drain mode
6. POST /agt/handoff/decommission — agent goes dormant
7. POST /agt/handoff/abort — cancels in-progress handoff
8. GET /agt/handoff/status — read-only (localhost allowed)
## Security: three-layer authentication
- Layer 1: Handoff token (one-time, short-lived, CLI-only)
Token exists only in CLI process memory — never in pod env
- Layer 2: NO localhost bypass for mutation endpoints
Prevents prompt injection from exfiltrating state via localhost
- Layer 3: Mutual attestation via DH-encrypted state blob
(Phase H2 adds Ed25519 succession signature verification)
## All endpoints audit-logged with:
- Caller IP, timestamp, endpoint, success/failure
- Token hash (not value), state blob size, item counts
## Dependencies added:
aes-gcm 0.10, hkdf 0.12, sha2 0.10, rand 0.9, base64 0.22, flate2 1
## Test results:
- 77 unit tests pass (21 new handoff tests)
- 26 integration tests pass (updated for new AppState fields)
- 74 controller tests pass (unaffected)
- clippy clean (zero warnings)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(handoff): implement Phase H2 — registry mode, identity succession, and reclamation
Phase H2 of the agent handoff feature:
Registry topology (local vs global):
- Add RegistryMode enum to router config (AGT_REGISTRY_MODE env var)
- Handoff init returns 409 in local mode with clear guidance
- Global mode does startup health check on AGT_REGISTRY_URL
- Handoff status endpoint exposes registry_mode + handoff_available
Identity succession (A→B):
- SQL migration 008_succession.sql with succession_log table
- POST /v1/registry/succession endpoint with Ed25519 sig verification
- Canonical message format: succession:{pred}:{succ}:{timestamp}
- One-shot rule (unique index on active predecessor)
- Copies reputation A→B, marks predecessor dormant
Identity reclamation (B→A, co-signed):
- POST /v1/registry/reclamation with dual signature verification
- Original succession ref must match active event_hash
- Deactivates succession redirect, copies reputation back
- Sets original online, departing offline
Lookup follows succession redirects:
- lookup_agent checks succession_log for dormant predecessors
- Returns successor with succeeded_from + succession_hash metadata
- Max redirect depth = 1 (no chains)
Dormant presence status:
- New PresenceStatus::Dormant variant in registry
- Ghost cleanup skips dormant agents (preserves succession chains)
- Capability search excludes dormant agents
CLI --global-registry flag:
- azureclaw dev --global-registry <url> skips local registry stack
- Passes AGT_REGISTRY_MODE=global to router
- Health check on global registry at startup
- Status display shows "handoff enabled" for global mode
Tests: 177 Rust (77 unit + 26 integration + 74 controller) + 159 CLI
All passing, clippy clean.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(handoff): implement Phase H3 — CLI command and plugin tools
CLI command (cli/src/commands/handoff.ts):
- azureclaw handoff <name> --to cloud (forward: local → AKS)
- azureclaw handoff <name> --to local (reverse: AKS → local)
- azureclaw handoff <name> --status (check progress)
- azureclaw handoff <name> --abort (cancel in-progress)
- 7-step orchestration: verify → init → snapshot → drain → transfer → succession → summary
- Registry mode guard (blocks with clear guidance if not --global-registry)
- Shared secret derivation for snapshot encryption
- Full error recovery (abort on failure)
Plugin tools (cli/src/plugin.ts):
- azureclaw_handoff_status: check handoff availability and state
- azureclaw_handoff_request: LLM can request handoff, but returns CLI
command (tokens are CLI-only to prevent prompt injection attacks)
Security design:
- Handoff tokens live in CLI process memory only (never LLM-accessible)
- LLM tool returns the CLI command to run, not the token itself
- This prevents prompt injection from triggering unauthorized handoff
Registered in cli.ts under 'Agent mobility' section.
Tests: 177 Rust + 159 CLI, all passing, clippy clean.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* security(handoff): close §9.9 security gaps — confirmation gate, injection protection, rate limits
§9.9.9 — Two-stage confirmation gate (HIGH priority):
- PendingHandoffStore with confirmation token (4-byte random hex)
- POST /agt/handoff/pending — creates pending request (rate limited: 1 per 5min)
- POST /agt/handoff/confirm — validates token, enforces 3s minimum delay
to prevent LLM self-confirm, consumes pending on success
- Plugin azureclaw_handoff_request now calls /pending (Stage 1)
- New plugin azureclaw_handoff_confirm tool (Stage 2)
- 6 unit tests for PendingHandoffStore (create, confirm, delay, rate limit,
cancel, wrong token)
§9.9.1 — State blob prompt injection protections:
- sanitize_chat_snapshot() strips messages matching 17 injection patterns
(system prompt override, handoff commands, instruction ignoring)
- User messages always preserved (legitimate user content)
- Non-UTF8 chat snapshots rejected entirely
- Trust scores capped at 750 on restore (cannot import max trust)
- 4 unit tests for chat sanitization
§9.9.4 — State blob size/DoS limits:
- 50MB blob size cap on both snapshot and restore
- MAX_WORKSPACE_FILES (100) and MAX_WORKSPACE_FILE_SIZE (10MB) constants
- PAYLOAD_TOO_LARGE (413) returned on violation
§9.9.3/§9.9.8 — Rate limits:
- Succession rate limit: 1 per AMID per 5 minutes (DB-backed)
- Reclamation rate limit: 1 per AMID per hour (DB-backed)
- check_succession_rate_limit() queries succession_log timestamps
§9.9.9 — AGT policy rule (belt-and-suspenders):
- handoff-tool-approval rule in azureclaw-default.yaml
- type: approval, priority: 75 (higher than tool-allow at 70)
- Requires operator approval for tool:azureclaw_handoff_request:*
and tool:azureclaw_handoff_confirm:*
Tests: 188 Rust (74 controller + 88 router + 26 integration) + 159 CLI
All passing, clippy clean, registry cargo check clean.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(mesh): implement global registry deployment — Ingress, OAuth, relay auth, CLI
Phase G1 implementation:
- G1a: AGIC Ingress manifest (deploy/agentmesh-ingress.yaml) with
NetworkPolicy (postgres locked to registry, registry/relay to AppGW),
Azure-managed TLS, WAF rate limiting, WebSocket support for relay
- G1b: Entra ID OAuth provider added to agentmesh-registry
(authorize, callback, token validation via Microsoft Graph).
Existing GitHub + Google providers untouched.
- G1c: Deployment manifest updated with OAuth secret references
(agentmesh-oauth-credentials), REGISTRY_URL for relay verification
- G1d: CLI 'azureclaw mesh auth' command — generates Ed25519 keypair,
runs browser-based OAuth flow, stores encrypted identity in
~/.azureclaw/mesh-identity.json (AES-256-GCM, machine-bound key).
Subcommands: auth, status, reset.
- G1e: CLI 'azureclaw up --global-registry <url>' skips local registry
deployment. '--expose-registry' deploys AGIC Ingress to make this
cluster's registry the global endpoint. Context persists registry mode.
- G1f: Relay registration verification — after Ed25519 signature check,
relay calls registry /v1/registry/lookup to confirm AMID is registered.
Unregistered/revoked agents rejected. Fails open on registry errors
(avoids cascading failures). Gated by REQUIRE_REGISTRATION=true.
Security: 4-layer auth chain (WAF → Ed25519 → registry check → OAuth).
PostgreSQL never exposed externally (NetworkPolicy enforced).
Private keys encrypted at rest (AES-256-GCM).
Tests: 188 Rust + 159 CLI passing, all clean.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test+docs(mesh): integration tests and security/architecture documentation
Tests:
- 28 new CLI mesh tests (mesh.test.ts): base58 encoding, Ed25519
keypair generation, AMID derivation, encrypt/decrypt roundtrip,
tamper detection, command structure verification
- 3 relay registry verifier tests (registry_verify.rs): disabled
verifier passthrough, env-based construction, enable logic
(compile-gated by pre-existing ed25519-dalek API mismatch in relay)
- All 188 Rust + 187 CLI tests passing
Documentation:
- architecture.md: new 'Global Registry Deployment' section — deployment
modes table, 4-layer auth chain diagram, NetworkPolicy enforcement,
identity management overview
- security.md: new 'Layer 9: Global Registry & Handoff Security' section
— relay auth layers table, handoff threat/mitigation matrix,
NetworkPolicy diagram, identity-at-rest encryption details
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(plugin): gate handoff mutation tools behind AGT_REGISTRY_MODE=global
In local registry mode, only azureclaw_handoff_status is registered.
The request and confirm tools are hidden from the LLM, preventing
unnecessary AGT governance prompts for tools that would 409 anyway.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(mesh): add promote/demote commands for registry global mode
azureclaw mesh promote — deploys AGIC Ingress + NetworkPolicies to expose
the cluster's AgentMesh registry and relay as public endpoints. Updates
deployment context to global mode.
azureclaw mesh demote — removes Ingress resources and reverts to
cluster-local registry. Disables cross-environment handoff.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(mesh): add --allow-ip to promote for IP-based access control
mesh promote auto-detects your public IP (via ifconfig.me) and injects
the AGIC whitelist-source-range annotation into both Ingress resources.
Override with --allow-ip <cidr>. If detection fails, warns and leaves
the registry open.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(mesh): auto-detect AppGW IP and use sslip.io for zero-config DNS
mesh promote now queries the AGIC Application Gateway for its public IP
and generates sslip.io hostnames (e.g. registry.20-30-40-50.sslip.io).
No DNS setup needed for testing. TLS is disabled for sslip.io domains
(secured by IP allowlist instead). Use --domain for custom domains.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor(mesh): switch promote/demote to LoadBalancer Services
Replace Ingress-based approach with direct LoadBalancer Service patching.
No ingress controller needed. promote patches registry + relay services
to LoadBalancer with loadBalancerSourceRanges for IP restriction, waits
for external IPs, builds sslip.io URLs. demote reverts to ClusterIP.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(plugin): LLM-driven handoff orchestration via E2E mesh
Replace the CLI-only handoff confirm flow with full LLM-driven
orchestration. After the user confirms the handoff code, the plugin
now executes the entire transfer autonomously:
1. Confirm → router creates handoff token (stays in plugin memory)
2. Snapshot → encrypted AES-256-GCM state blob
3. Drain → stop accepting new work
4. Spawn → create cloud target on AKS (or find existing)
5. Transfer → send state blob via E2E encrypted mesh (Signal Protocol)
6. Verify → target restores, sends verification digest back via mesh
7. Succession → registry identity chain update
8. Decommission → local agent enters dormant state
Key changes:
- handoff_confirm tool: full orchestration instead of returning CLI cmd
- onMessage handler: new handoff_transfer message type for target agent
to auto-restore state and send verification back
- _routerCallStrict: new helper that rejects on HTTP >= 400
- _readAdminToken: reads admin token from filesystem paths
- _routerCall: added extraHeaders parameter (backward compatible)
- agtReconnect: disconnect before connect to clear stale SDK state
Security model (§9.9.9): the LLM can REQUEST a handoff but never
EXECUTE one. The handoff token stays in plugin memory — the LLM
never sees it. All router calls use this token. Human confirmation
via the 2-stage code flow is the gate.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(mesh): promote checks health and reconnects stale port-forwards
When registry is already in global mode, 'azureclaw mesh promote' now:
- Checks registry HTTP health (/v1/health)
- Checks relay TCP connectivity
- If both healthy: reports status and exits
- If either dead: kills stale PIDs, clears held ports, restarts
fresh port-forward tunnels, verifies connectivity
Previously it just said 'already global' and exited, even when the
port-forwards had died (e.g. after IP change or sleep).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(plugin): use async import for fs in ESM context
_readAdminToken used require('node:fs') which is unavailable in ESM.
Changed to async function with await import('node:fs') and updated
both call sites to await the result.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(handoff): spawn AKS pod from dev mode for cloud handoff
In dev mode, the router's /sandbox/spawn endpoint was creating Docker
containers. For handoff (local→cloud), we need actual AKS pods.
Changes:
- Add HandoffMeta struct to SpawnRequest (mode + predecessor fields)
- When handoff.mode='restore' in dev mode, bypass Docker path and use
K8s CRD creation via kube-rs (kubeconfig mounted from host)
- Mount ~/.kube/config into dev container at /run/secrets/kubeconfig
so the router can reach the K8s API for handoff spawns
The controller already sets AGT_RELAY_URL and AGT_REGISTRY_URL on
spawned pods, and NetworkPolicy allows mesh egress — so the handoff
target automatically joins the global mesh.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(handoff): propagate trusted_peers and registry_mode to spawned pods
The handoff target was rejecting the source's KNOCK (trust score 0 <
threshold 500) because AGT_TRUSTED_PEERS wasn't propagated. Also,
AGT_REGISTRY_MODE wasn't set, so handoff tools were skipped.
Changes:
- CRD: add trusted_peers and registry_mode fields to GovernanceConfig
- Controller: propagate AGT_TRUSTED_PEERS and AGT_REGISTRY_MODE to
the openclaw container env vars
- Spawn: write trusted_peers and registry_mode='global' into CRD
governance spec for handoff targets
- Spawn: use 'handoff'/'predecessor' labels instead of 'agent'/'parent'
for handoff-spawned CRDs (not sub-agents)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: add cloud handoff flow diagram (section 11)
Sequence diagram covering all 5 phases: two-stage confirm, snapshot/drain,
spawn on AKS, E2E mesh transfer, succession/decommission. Includes security
model diagram and current vs future (Entra OAuth) trust flow comparison.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(handoff): async orchestration with real-time progress tracking
Refactor handoff_confirm to return immediately and run orchestration
in the background via _runHandoffOrchestration(). The LLM polls
handoff_status every 3-5s and relays emoji step updates to the user
in real-time instead of blocking for 2-3 minutes.
Key changes:
- HandoffProgress interface tracks phase, steps[], status, error
- _hp() helper updates progress + logs at each step
- _runHandoffOrchestration() contains the full 7-step flow:
snapshot → drain → spawn → mesh-wait → transfer → verify →
succession → decommission
- handoff_status returns rich progress with active polling instruction
- Module-level _log set during register() for background access
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(review): address security and reliability findings from handoff audit
sec-6: Fix message filter AND→OR — verification now rejects messages
unless BOTH from_amid AND from_agent match the expected target
sec-1: Propagate AGT_TRUSTED_PEERS and AGT_REGISTRY_MODE to router
container (was only on openclaw container)
sec-2: Validate trusted_peers — reject values with control chars
sec-3: Validate registry_mode — only accept 'local'|'global'
rel-7: Wrap _runHandoffOrchestration in top-level try-catch
rel-6: Replace non-null assertions with explicit guard at completion
rel-3: Bump snapshot timeout 15s→60s, drain timeout 15s→30s
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: revise handoff flow diagrams with full review findings
Replace Section 11 with 7 comprehensive diagrams:
- 11.1: End-to-end sequence (both source + target sides, live progress)
- 11.2: Handoff state machine with known limitations noted
- 11.3: 7-layer security model (gate → isolation → auth → encryption →
injection hardening → identity → infrastructure)
- 11.4: Env var propagation showing both containers receive vars
- 11.5: Two orchestration paths (LLM vs CLI) and their differences
- 11.6: Trust flow (current unauthenticated vs future Entra OAuth)
- 11.7: Error recovery and planned improvements
Also add nohup.out to .gitignore (stale port-forward logs).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(review): address remaining TS findings — orphan cleanup, guards, tests
rel-1: Clean up orphaned CRDs on abort when mesh transfer or discovery
fails after spawn (DELETE /sandbox/spawn/<name>)
rel-8: Concurrent handoff guard — reject confirm if handoff already running
sec-4: Respect $KUBECONFIG env var with fallback to ~/.kube/config
test-3: Fix 4 failing spawn error tests — tools handle unreachable router
gracefully (return status JSON), update assertions accordingly
(187/187 tests now pass)
dup-1: Document dual orchestration paths (CLI operator-mode vs plugin
LLM-mode) in handoff.ts header comment + architecture-diagrams.md
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(review): address Rust findings — state machine, resume, tests
rel-2: Add try_transition() to enforce handoff phase ordering
(Idle→Init→Snapshot→Drain→Transfer→Restore→Verify→Decom→Complete)
rel-4: Add resume() + POST /agt/handoff/resume endpoint to cancel
drain state after abort (Aborted|Draining → Idle)
rel-5: Change body.unwrap() to expect() with message in spawn.rs
dead-1: Remove unused _used field from ActiveToken
test-1: Add 10 state machine transition tests (valid sequence,
invalid skip, abort, fail, resume, restart after complete)
test-2: Add 4 auth token tests (wrong value, no active, after
revoke, wrong pending confirmation code)
201 Rust tests pass (74 controller + 101 router + 26 integration)
Clippy clean with -D warnings
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* handoff: incremental progress polling, SDK reconnect fix, policy cleanup
- handoff_status tool: add since_step param for incremental polling,
returns only new_steps since last call so LLM relays one step at a time
- vendor SDK patch #9: AgentMeshClient.connect() no longer sets
connected=true when transport.connect() returns false, allowing retry
- Policy: remove handoff-tool-approval gate (two-step confirmation code
mechanism is sufficient; approval gate can return once native UI exists)
- config: add promoteMode to DeploymentContext for mesh promote tracking
All tests pass: 201 Rust (74+101+26), 187 CLI
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(crd): add trustedPeers and registryMode to Helm CRD schema
The K8s API server was silently stripping these fields because the Helm
CRD template only defined enabled/toolPolicy/trustThreshold. spawn.rs
wrote the fields and reconciler.rs read them, but the schema validation
layer dropped them in between.
Root cause of handoff mesh registration failure: target pods never
received AGT_TRUSTED_PEERS or AGT_REGISTRY_MODE env vars because the
CRD never stored the values.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(router): read admin token from correct mount path
AppState::new() only checked /run/secrets/admin-token but the controller
mounts the secret at /etc/azureclaw/secrets/admin-token. This caused
'Server misconfiguration: no admin token' on target pods during handoff
verification. main.rs had the correct path but its token was only used
for the admin_auth_middleware, not the handoff middleware which reads
from state.admin_token.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(handoff): pre-build audit — snapshot strict, direction propagation, relay URLs
Three defensive fixes from comprehensive flow audit:
1. Snapshot endpoint now uses _routerCallStrict (was _routerCall) —
if snapshot fails, error surfaces immediately instead of continuing
with undefined blob data
2. Target-side handoff_transfer handler now reads direction from the
mesh message instead of hardcoding 'local_to_aks' — enables
reverse (aks_to_local) handoffs
3. Controller propagates AGT_RELAY_URL and AGT_REGISTRY_URL to the
openclaw container (was only on router container) — plugin no
longer relies on fallback to router proxy for relay connection
All tests pass: 201 Rust (74+101+26), 187 CLI, clippy clean
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(handoff): enforce state machine — migrate all handlers to try_transition
All 5 handoff route handlers now use try_transition() instead of
set_phase(), returning 409 Conflict on invalid phase transitions.
Also fixed the transition rules: Decommissioning is now allowed from
Draining (source-side flow: Init→Snapshot→Drain→Decommission skips
Verify/Restore which happen on the target router, not the source).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(handoff): verification hash mismatch + synchronous progress
Two fixes:
1. Verification hash mismatch: verify endpoint was rebuilding a fresh
snapshot (new timestamp, nonce, hostname) instead of using the hash
from the restored data. Now restore stores the hash of the decrypted
compressed bytes, and verify reuses it. Falls back to build_snapshot
for source-side verify (where no restore happened).
2. No proactive progress: LLMs don't autonomously poll tools, so the
handoff_confirm tool now awaits _runHandoffOrchestration() and
returns all steps when complete, instead of firing-and-forgetting
and expecting the LLM to poll handoff_status.
All tests pass: 201 Rust (74+101+26), 187 CLI, clippy clean
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(spawn): check Docker API HTTP status codes in docker_api
The docker_api helper only checked curl's exit code, not the HTTP
response status from Docker Engine. This caused silent failures —
e.g. container start returning HTTP 404 (network not found) was
swallowed and spawn reported success even though the container
never started.
Add -w flag to capture HTTP status code and return Err for 4xx/5xx
responses with the Docker error message extracted from the JSON body.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(handoff): propagate channel credentials to cloud target
During handoff spawn, collect channel/plugin credentials from the
source environment (TELEGRAM_BOT_TOKEN, SLACK_BOT_TOKEN, etc.) and
create a {name}-credentials K8s secret in the target namespace.
The controller already mounts this secret via envFrom (optional),
so the cloud agent inherits Telegram and other channels.
Also fix docker_api to check HTTP status codes — previously it only
checked curl's exit code, silently swallowing Docker Engine errors
like 'network not found' (HTTP 404).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(handoff): full state hydration — workspace, memory, conversations, Telegram
Source side (orchestration):
- Pack workspace tar from /sandbox/.openclaw/ (AGENTS.md, SOUL.md, etc.)
- Search Foundry Memory for recent context and include as chat_snapshot
- Include credential refs (channel/plugin names) in snapshot
Target router (restore):
- Extract workspace tar to /sandbox/ with path traversal protection
- Write chat_snapshot and metadata to /tmp/handoff/ for plugin
Target plugin (post-restore hydration):
- Create Foundry Conversation with replayed chat messages
- Store handoff event fact in Foundry Memory (update_memories)
- Write HANDOFF_CONTEXT.md to workspace (fallback context)
- Send 'handoff_ready' mesh message back to predecessor
The cloud agent now comes online with full context: workspace files,
conversation history in Foundry Conversations, semantic memory via
shared Memory Store, and proactively greets the user via Telegram.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: cross-container handoff — return state in response, not filesystem
The router and openclaw containers have separate filesystems in AKS.
Previously the router wrote workspace tar and chat snapshot to /tmp/
which the plugin couldn't read.
Changes:
- Router: return workspace_tar (base64) and chat_snapshot in restore
response JSON instead of writing to filesystem
- Plugin: extract workspace tar and parse chat snapshot from the
HTTP response body (runs in openclaw container where /sandbox/ lives)
- Remove unused extract_workspace_tar() from routes.rs
- Remove tar crate dependency (extraction now done by plugin via CLI tar)
- Add direction, initiated_at, restored_at to restore response
Future: workspace payloads >5MB will auto-transfer via Azure Blob
Storage (SAS URL in handoff state) — not yet implemented.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* security: harden workspace tar extraction and chat snapshot parsing
Tar extraction:
- Pre-extract validation: list entries, reject any containing '..' or
starting with '/' (path traversal)
- Size guard: reject compressed payloads >5MB (decompression bomb)
- Unique temp dir per extraction (race condition prevention)
- --no-same-owner --no-overwrite-dir flags on extraction
- Temp dir cleaned up after extraction
- Removed '|| true' — errors now surface in logs
Chat snapshot:
- Schema validation: must be array, each entry must have string
role + content
- Cap at 100 messages, role capped at 20 chars, content at 10k chars
- Rejects non-conforming entries silently
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* perf: split sandbox Dockerfile into base + overlay for fast rebuilds
The sandbox image was 4.96 GB with ~4.0 GB of rarely-changing deps
(OpenClaw, Python wheels, Go tools, Node.js, CLI tools) rebuilt on
every code change. Now split into:
Dockerfile.base (~4.0 GB, rebuild weekly/on dep upgrade):
- Azure Linux 3 + system packages
- Node.js 22, Python 3 + 41 packages, Go CLI tools
- OpenClaw framework + extension symlinks + skills
- gh, ripgrep, 1password, himalaya
- User setup (sandbox:1000, router:1001)
Dockerfile (~50 MB, rebuild per commit in ~30s):
- FROM azureclaw-sandbox-base (all heavy deps pre-cached)
- CLI plugin builder reuses base image (has Node.js already)
- Router binary, plugin dist, vendored SDK overlay
- Entrypoint, proxy-bootstrap, skills, policies
All functionality preserved:
- UID separation, iptables egress guard, seccomp, read-only rootfs
- Channel plugins (Telegram/Slack/Discord/WhatsApp)
- Extension dep symlinks (grammy, carbon, bolt, etc.)
- Vendored SDK overlay, proxy-bootstrap, Control UI symlink
- ClawHub skills, npm CLI tools (clawhub, mcporter, oracle)
Build paths updated:
- azureclaw dev: auto-builds base if not cached, --build-base to force
- azureclaw push: --only sandbox-base to push base image
- Makefile: image-sandbox-base target added
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(handoff): implement reverse handoff (cloud → local)
CLI-driven reverse handoff orchestration:
- aksRouterExec: kubectl port-forward to AKS pod router
- wakeDormantDocker: detect and restart stopped containers
- readAksCrdSpec: inherit model/egress/isolation from CRD
- rehydrateCredentials: copy K8s secrets to Docker container
- Full 10-step reverse flow: connect → verify → init → snapshot →
drain → wake local → credentials → restore → succession →
decommission + delete CRD
Direction-aware source routing:
- sourceExec alias delegates to routerExec (Docker) or
aksRouterExec (AKS) based on direction
- Forward path unchanged at runtime (sourceExec === routerExec)
Plugin reverse handoff:
- handoff_request returns CLI command for aks_to_local direction
- Completion messages updated for both directions
- Decommission label direction-aware
Operator TUI:
- 'returning' handoff state for active aks_to_local handoff
- Table shows '<' icon and 'Returning' status
- ASCII-only table icons for reliable column alignment
- Column widths tightened
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(router): raise body limit on handoff routes to 50MB
Axum's default body limit is 2MB. Encrypted state snapshots easily
exceed this, causing HTTP 413 on /agt/handoff/snapshot and /restore.
Add DefaultBodyLimit::max(MAX_BLOB_SIZE_BYTES) layer to
handoff_protected_routes — matches the existing 50MB blob size
constant from §9.9.4.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(handoff): read AKS admin token from mounted secret
On AKS, the admin token is stored in K8s secret 'router-admin-token'
and mounted at /etc/azureclaw/secrets/admin-token — not as env var
or /tmp file. Updated getAksAdminToken() to:
1. Read from /etc/azureclaw/secrets/admin-token (router container)
2. Fallback: same path in openclaw container
3. Fallback: kubectl get secret (base64 decode)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(handoff): use POST for snapshot route (was GET → 405)
The /agt/handoff/snapshot route is POST-only but both forward and
reverse handoff paths were sending GET requests, causing HTTP 405.
Changed both to POST.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(handoff): reuse existing snapshot blob in reverse path
The reverse handoff was requesting a second snapshot at step 9, but
the state machine had already advanced to 'draining' after step 5.
The snapshot blob was already captured at step 3 — now the reverse
path uses snapshotResp.body.blob directly instead of re-fetching.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(handoff): pipe restore payload via stdin for large blobs
routerExec passes JSON as a curl -d argument, which hits shell
argument length limits for large encrypted snapshots. The reverse
handoff restore now uses 'docker exec -i ... curl -d @-' with the
payload piped via stdin, avoiding ARG_MAX issues.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(connect): handle Ctrl+C to disconnect port-forward
The kubectl port-forward child process with stdio:pipe did not
receive SIGINT from the terminal. Added explicit SIGINT/SIGTERM
handlers that terminate the child process and exit cleanly.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(handoff): init local handoff session + auth headers for restore
The local Docker router requires admin token + handoff token for
/agt/handoff/restore. The reverse path now:
1. Gets local admin token from Docker container
2. Inits a handoff session on the local router
3. Passes both auth headers to the restore curl call
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(handoff): correct step count + Telegram notification on handoff
- Fixed reverse handoff step counter: 13 steps (was 10, showing 11/10+)
- Added Telegram notification on handoff completion for both directions:
- local→cloud: 'I moved to the cloud'
- cloud→local: 'I am back on your local machine'
Best-effort — reads credentials from Docker container (reverse) or
env (forward), sends via Telegram Bot API. Failures are silent.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(handoff): clean up local handoff state after reverse restore
Two fixes for stale handoff sessions blocking subsequent handoffs:
1. CLI: After successful reverse restore, transition local router
through verify → decommission to reach a terminal state.
2. Router: Expand can_start() to allow re-init from Restoring,
Verifying, and Decommissioning phases. These indicate a previous
handoff that completed data transfer but wasn't properly finalized.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(handoff): extend verification timeout + retry mesh send
The verification timeout was 60s but AKS pods can take longer to
fully initialize their plugin message handlers after mesh registration.
The blob sent before the handler is ready gets silently dropped.
Fix:
- Extended timeout from 60s to 180s
- Re-send the handoff_transfer blob every 30s within the verification
loop, in case the target's message handler wasn't ready on first send
- Also fixes: router can_start() allows stale Restoring/Verifying states,
local handoff session cleaned up after reverse restore
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(relay): increase max_message_size to 1MB for handoff blobs
Handoff snapshots with real state (chat, audit, credentials) can be
80+ KB. After Signal Protocol encryption + base64 + JSON envelope,
they exceed the relay's 64KB default max_message_size. Bumped to 1MB.
Also includes verification timeout extension (60s→180s) with 30s
re-send retries, already committed in plugin.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: collect workspace/chat/credentials in CLI handoff snapshot
The CLI handoff command was sending an empty snapshot payload
(only shared_secret). The forward handoff via plugin.ts collected
workspace tar, Foundry memories, and credential refs — but the
CLI path (used for both forward and reverse) skipped this.
Changes:
- Collect workspace tar via kubectl exec (AKS) or docker exec (local)
- Collect Foundry Memory Store items as chat context
- Collect credential refs from container environment
- Fix snapshot response field name: size_bytes → snapshot_size_bytes
- Include items breakdown in snapshot response
- Fix step counter: move transfer step into forward branch only
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: stop stepper spinner and cleanup port-forward after handoff
stepper.step('Handoff summary...') started a spinner that was never
stopped with stepper.done(), keeping the event loop alive and requiring
Ctrl+C. Also aksPortForwardStop() was only called in the error path.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: add /agt/handoff/succession router endpoint for Ed25519-signed succession
The registry's succession API requires the predecessor's Ed25519 signature
over a canonical message. The private key lives in the router's Governance
identity — inaccessible to the CLI.
New endpoint POST /agt/handoff/succession on the router:
- Takes {successor_amid, reason} from CLI
- Looks up predecessor (self) AMID from registry
- Looks up successor signing key from registry
- Signs canonical message 'succession:{pred}:{succ}:{timestamp}'
- Submits complete SuccessionRequest to registry
- Returns registry response
CLI + plugin updated to call /agt/handoff/succession instead of
/agt/registry/registry/succession (which lacked signing keys).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: sub-agent handoff — collect, snapshot, and re-spawn during handoff
Sub-agents spawned by a parent agent are now included in handoff state
transfer. The full pipeline:
Collection (source side):
- New GET /agt/handoff/sub-agents endpoint lists active sub-agents
and reconstructs SpawnRequest from CRD spec (K8s) or container
labels (Docker dev mode)
- CLI + plugin call this endpoint and inject sub_agent_snapshots
into the handoff snapshot payload
Re-spawn (target side):
- handoff_restore iterates sub_agent_snapshots after state hydration
- Calls create_sandbox() for each sub-agent with the stored config
- Returns per-sub-agent results (spawned/failed) in restore response
- Audit-logged as handoff:restore:sub-agent
Supporting changes:
- SpawnRequest, HandoffMeta, SubAgentSnapshot: added Clone derive
- Sub-agent results included in restore response as sub_agent_results
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: AMID remapping + sub-agent workspace collection during handoff
Two improvements to sub-agent handoff:
1. AMID remapping: When re-spawning sub-agents on the target, the old
parent AMID in trusted_peers is replaced with the new parent's AMID.
This ensures sub-agents trust the new parent for KNOCK handshakes.
The new parent AMID is looked up from the registry at restore time.
2. Workspace collection: The CLI now exec's into each sub-agent's
container (kubectl for AKS, docker for local) to collect workspace
tar before including it in the snapshot. Each sub-agent's workspace
is capped at 2MB. The plugin path is best-effort without workspace
(no container exec access from inside the sandbox).
Also adds sub_agents_respawned count to plugin restore metadata.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: collect sub-agent workspace via E2E mesh during handoff
Sub-agents now respond to handoff:workspace_request mesh messages by
tarring their /sandbox/.openclaw/workspace and sending it back via the
E2E encrypted relay. No shared volumes or cross-container exec needed.
Plugin message handler (sub-agent side):
- Receives handoff:workspace_request from parent
- Tars workspace (excludes extensions, node_modules, etc.)
- Sends base64-encoded tar back as handoff:workspace_response
- Falls back to empty response on error (parent doesn't hang)
Plugin handoff orchestration (parent side):
- After fetching sub-agent list from router, discovers each sub-agent
via registry search to get their AMID
- Sends handoff:workspace_request to each via mesh
- Polls agtInbox for handoff:workspace_response (up to 15s per agent)
- Enriches sub-agent snapshots with workspace tar before creating
the encrypted handoff snapshot
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: expand workspace tar to include cron/policies/agents + mesh 768KB cap
- Add WORKSPACE_TAR_CMD constant in handoff.ts for consistent tar commands
- Include .openclaw/cron, .openclaw/policies, .openclaw/agents in all 5 tar commands
- Refine extension exclusion: only exclude */dist and */node_modules (keep manifests)
- Cap mesh workspace response at 768KB (safe under relay's ~1MB limit)
- Add truncated flag in workspace_response messages
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: chunked mesh transfer for large sub-agent workspaces
Workspaces > 512KB are split into chunks sent via separate mesh messages,
then reassembled on the receiver side. This lifts the practical workspace
transfer limit from ~768KB to ~40MB (80 chunks × 512KB).
Sender (sub-agent):
- Small workspace (≤512KB): single handoff:workspace_response (fast path)
- Large workspace: N handoff:workspace_chunk messages + completion marker
- Max 80 chunks (leaves headroom in relay's 100-message offline queue)
Receiver (parent):
- Collects workspace_chunk messages into a Map keyed by chunk_index
- Reassembles in order when all chunks received or completion marker arrives
- 30s timeout with partial-chunk recovery (uses what was received)
- Backwards compatible: single-message responses still work unchanged
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: unified chunked mesh transport layer + file transfer tool
Implements a general-purpose auto-chunking transport layer for the E2E
encrypted mesh. Any payload exceeding 512KB is transparently split into
chunks with per-chunk SHA-256 integrity verification, then reassembled
on the receiver side before reaching application logic.
Transport layer (meshSend + meshHandleTransportMessage):
- meshSend(): auto-chunks large payloads into mesh:transfer_manifest +
N mesh:transfer_chunk messages. Small messages pass through directly.
- meshHandleTransportMessage(): intercepts transport messages in onMessage,
accumulates chunks, verifies SHA-256 hashes, reassembles, and delivers
the original message to the application layer.
- Per-chunk + manifest-level SHA-256 integrity verification
- 2-minute TTL with automatic stale transfer cleanup
- Max ~40MB per transfer (80 chunks × 512KB)
New tool — azureclaw_mesh_transfer_file:
- Agents can send files to each other via E2E encrypted mesh
- Files up to 30MB supported (auto-chunked transparently)
- Received files auto-saved to /sandbox/.openclaw/workspace/incoming/
- Path traversal protection (must be within /sandbox)
Consumers updated to use unified transport:
- mesh_send tool: auto-chunks large task messages
- Handoff blob transfer: auto-chunks encrypted snapshots
- Sub-agent workspace collection: auto-chunks workspace tars
- Handoff re-send loop: uses meshSend for retransmit
Limits raised:
- Router MAX_BLOB_SIZE_BYTES: 50MB → 200MB (sub-agent workspaces)
- Plugin workspace tar cap: 5MB → 50MB
- CLI workspace tar cap: 5MB → 50MB
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: configurable router URL + fix spawn test timeouts + add transport tests
- Make ROUTER and ROUTER_BASE configurable via AZURECLAW_ROUTER_URL env var
(defaults to http://127.0.0.1:8443 for backward compat)
- Fix 5 pre-existing spawn tool test timeouts caused by local dev router
on port 8443 — tests now point to unused port 19876 for immediate
ECONNREFUSED instead of 45s polling loop
- Fix test assertions to match actual error behavior (plain text errors,
not JSON, when router is unreachable)
- Add 11 new tests (198 total, up from 187):
- mesh_transfer_file: registration, schema, path traversal, abs path,
mesh-not-connected
- mesh_send: registration, params, error when disconnected
- handoff_status: registration, returns status JSON
- AZURECLAW_ROUTER_URL: spawn + spawn_status use configurable URL
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: address 4 direction-specific handoff gaps
1. Plugin reverse: retry with 60s backoff when discovering local target
(local agent may be waking from dormant state after CLI runs
wakeDormantDocker)
2. Direction validation: both plugin and router now validate that the
incoming handoff direction matches the environment (AZURECLAW_DEV_MODE).
Warn-only on mismatch — doesn't block to avoid false positives.
3. Forward credential rehydration: collect actual credential VALUES
from Docker (not just refs), create K8s secret on AKS target before
pod starts so envFrom can mount them. Closes the credential gap
where forward handoff lost Telegram/Slack/Brave tokens.
4. Symmetric cleanup: reverse handoff now scales deployment to 0
instead of deleting the CRD. This preserves the sandbox definition
for instant re-forward handoff while freeing all compute resources.
Falls back to CRD deletion if scale fails.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: graceful sub-agent interrupt during handoff
Add handoff:interrupt protocol so sub-agents can save in-progress work
before their workspace is collected during a handoff.
Plugin path (mesh-based):
- Parent sends handoff:interrupt to all sub-agents concurrently
- Sub-agents set handoffInterruptRequested flag
- processTaskWithTools checks the flag between LLM rounds
- On interrupt: saves task progress to .task-in-progress.json
(round, messages, last content, original task)
- Sends handoff:interrupt_ack back to parent
- Parent waits up to 10s for acks, then proceeds with workspace collection
CLI path (exec-based):
- CLI writes .handoff-interrupt sentinel file into each sub-agent container
- processTaskWithTools also checks for this file between rounds
- Same progress save behavior (.task-in-progress.json)
Both paths ensure the workspace tar includes the progress checkpoint,
so it survives the handoff and is available on the target for resumption.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: sub-agent workspace injection + task resumption after handoff
Complete the sub-agent handoff lifecycle — after re-spawn on the target,
sub-agents now receive their workspace and resume interrupted work.
Router changes:
- Restore response now includes sub_agent_workspaces array with each
sub-agent's workspace_tar, task_context, status, and checkpoint
Plugin — target side (post-restore):
- Waits up to 60s for each re-spawned sub-agent to register in mesh
- Sends workspace tar via meshSend (auto-chunked for large workspaces)
- Sends handoff:resume with task context and checkpoint info
Plugin — sub-agent side (new message handlers):
- handoff:workspace_inject: extracts received workspace tar into /sandbox/
with path traversal validation and size guard
- handoff:resume: reads .task-in-progress.json, sends resume_ack to parent
with status report ('Successfully restored in cloud. Resuming interrupted
work from round N: <task>'), then re-enters processTaskWithTools with a
contextual prompt that includes the original task, progress, and last output
The full sub-agent handoff lifecycle is now:
interrupt → save progress → collect workspace → transfer →
re-spawn → inject workspace → resume task → report to parent
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: sub-agent handoff — Docker API encoding + name prefix + source cleanup
Three bugs prevented sub-agents from properly migrating during handoff:
1. collect_sub_agent_snapshots_docker passed raw JSON braces in the
Docker API URL — curl treated {} as glob patterns and the Docker
daemon couldn't parse the filter. Result: always returned 0
sub-agents, so the snapshot blob had no sub-agents to respawn.
Now uses URL-encoded filter via the docker_api() helper (matching
list_sandboxes_docker's pattern).
2. Same function used the Docker container name (azureclaw-{name})
as the agent name, causing respawn to create
azureclaw-azureclaw-{name} on the target. Now strips the prefix.
3. Source decommission only put the main agent dormant — sub-agent
containers kept running. Now destroys all source sub-agents via
the spawn API before decommissioning.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: sub-agent handoff — wrong API URLs + missing report steps
Four fixes for sub-agent handoff orchestration:
1. Sub-agent list used GET /sandbox/spawn (wrong) — now GET /sandbox/list
2. Sub-agent delete used DELETE /sandbox/spawn/{name} (wrong) — now
DELETE /sandbox/{name} (matches actual router routes)
3. Response field was 'sub_agents' but endpoint returns 'sandboxes'
4. No sub-agent info in handoff progress report — added _hp() calls
for: discovery count, interrupt/checkpoint status, workspace
collection count, snapshot inclusion, cleanup status, and
sub_agents_transferred in the final result object
Also fixed orphan target cleanup URLs (2 places) that had the same
/sandbox/spawn/{name} → /sandbox/{name} issue.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: set trusted_peers for re-spawned sub-agents after handoff
The trusted_peers remapping in handoff_restore was dead code —
Docker snapshots had trusted_peers=None, so the if-let guard never
fired. Re-spawned sub-agents had no trusted parent AMID and rejected
handoff:workspace_inject + handoff:resume messages from the new
parent. This caused workspace injection to silently fail and task
resumption to never trigger.
Fix: always set trusted_peers to include the new parent's AMID,
regardless of whether the original snapshot had it set:
- If peers existed: remap old parent → new parent (existing logic)
- If peers existed but old parent absent: append new parent
- If peers was None: set to new parent entry (new case)
This ensures the sub-agent trusts the new parent on first KNOCK
and accepts workspace/resume messages immediately after spawn.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: include sub-agent status in Telegram greeting after handoff
Restructure the restore flow so sub-agent workspace injection and resume
happens BEFORE the Telegram greeting. After sending resume signals, wait
up to 8s for sub-agents to send resume_ack messages. The Telegram greeting
now includes a sub-agent status section showing each agent's name, state
(resumed/ready/starting/failed), and a task preview.
The handoff_ready mesh message back to the predecessor also now includes
sub_agents_restored count, sub_agents_resumed count, and per-agent details.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: use per-sandbox runtime for operator exec path, not global devMode
The operator's unified view fetches both Docker and AKS agents, but the
routerExec/fetchAgtQuick/fetchEgressDomains functions used the global
devMode flag to choose between docker-exec and kubectl-exec. This meant
Docker agents were queried via kubectl (which fails — no K8s namespace)
when the operator ran in non-dev mode, and vice versa.
Fix: use sb.runtime === 'docker' per-sandbox instead of the global devMode
flag in all 4 places that exec into agent containers:
- fetchSecurityState (routerExec + k8sCheck)
- fetchEgressDomains (routerCurl)
- fetchAgtQuick (docker/kubectl exec)
- seccomp profile inference
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: sub-agent trust + workspace logging after handoff
Two fixes for broken parent↔sub-agent communication after handoff:
1. Stale trusted_peers: After handoff, re-spawned sub-agents get new
AMIDs but the parent's parentTrustedAmids set contains old AMIDs.
KNOCK handler rejects all incoming sub-agent messages (score=0 <
threshold=500). Fix: when the plugin discovers a sub-agent's new
AMID during workspace injection, register it in amidToName,
nameToAmid, parentTrustedAmids, and push baseline trust to router.
2. Silent from_value failure: serde_json::from_value for sub-agent
snapshots at POST /handoff/snapshot was wrapped in `if let Ok`
which silently swallowed deserialization errors, potentially losing
all sub-agent workspace data. Changed to match with tracing::warn
that logs the exact error + JSON preview for debugging.
Also adds roundtrip test for SubAgentSnapshot workspace_tar through
the full serialize→compress→encrypt→decrypt→decompress→deserialize
chain, and a test for the JS↔Rust base64 round-trip.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* security: remove clawhub.com and openclaw.ai from default egress allowlist
clawhub.com had a 12% malware rate (Atomic Stealer was #1 skill).
openclaw.ai enables `curl | bash` install vectors from inside sandboxes.
Both were in the default Helm values and example CRD. Removed from:
- deploy/helm/azureclaw/values.yaml (default egress allowlist)
- examples/basic-agent/clawsandbox.yaml (example CRD)
- PLAN.md (policy presets documentation)
Defense is now three layers deep:
1. Egress proxy blocks clawhub.com/openclaw.ai (network)
2. Skills directory is root-owned, chmod 640/750 (filesystem)
3. Plugin code is root-owned, read-only for sandbox (code integrity)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: use sub_agent_results as trust+resume loop driver, not sub_agent_workspaces
Root cause: the post-restore trust registration and resume signals were
gated on restoreResp.sub_agent_workspaces (workspace data), which could
be empty even when sub-agents were successfully spawned. This caused the
entire block to be skipped — no trust registration, no resume signals,
no Telegram sub-agent status.
Fix: use restoreResp.sub_agent_results (always populated when sub-agents
spawn) as the primary loop driver. Workspace data is looked up by name
from sub_agent_workspaces as a secondary source.
Also:
- Added workspace_inject_ack: sub-agent confirms extraction success/fail
with file_count + error back to parent before resume is sent
- Parent waits up to 15s for ack, logs result, passes workspace_delivered
flag in resume payload
- handoff_ready report now includes sub_agents_workspace_delivered count
- Telegram greeting shows 📦 icon per sub-agent when workspace arrived
- Increased mesh registration wait from 60s to 90s (AKS pods need boot)
- Router logs snapshot details when building sub_agent_workspaces
Tests added:
- Rust: sub_agent_workspaces builder filter (empty/non-empty workspace_tar)
- Rust: full encrypt→decrypt→restore round-trip with 2 sub-agents
- Rust: edge case — empty workspace + empty task_context filtered out
- TS: sub_agent_results drives loop even when sub_agent_workspaces empty
- TS: workspace_inject_ack protocol (success + failure paths)
- TS: handoff_ready includes workspace delivery status
- TS: only spawned sub-agents enter trust loop
- TS: missing sub_agent_results graceful fallback
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(sdk): reuse active Signal Protocol session instead of crashing
Vendor patch #10: SessionManager.initiateSession() threw 'Active session
already exists' when the crypto layer had a session established via
incoming KNOCK but client.activeSessions wasn't synced.
This broke mesh_transfer_file and any second send to the same peer.
Fix: return existing session info with reused=true flag instead of
throwing. establishSession detects reuse, syncs activeSessions, and
skips redundant KNOCK/activate.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: don't expose handoff confirmation code to LLM, add console.log diagnostics
Security fix: The handoff confirmation token was returned in the tool
result, allowing the LLM to self-confirm without user input. Now the
code is sent directly to Telegram (side-channel) and printed to console
for TUI users — the LLM never sees it.
Changes:
- Remove confirmation_token from azureclaw_handoff_request tool response
- Send code via Telegram sendMessage as a side-channel delivery
- Print code to console.log (visible in kubectl logs, not to LLM)
- Update tool descriptions to emphasize code comes from user input
- Bump CONFIRMATION_MIN_DELAY_SECS from 3s to 8s
- Add console.log diagnostics in post-restore IIFE for handoff debugging
- Add test: tool response must not contain confirmation_token
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: increase sub-agent tool-calling rounds from 10 to 25
10 rounds was too tight for data-heavy tasks — sub-agents hit the
cap and returned truncated results. 25 gives enough room for
multi-step research/collection while still preventing runaway loops.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: increase prekey retry window from 16s to 45s for sub-agent mesh send
Sub-agents take 20-30s+ after pod is Running to upload prekeys
(gateway start → plugin load → SDK init → relay connect → prekey
upload). The previous 8×2s=16s window wasn't enough, causing
'Cannot get prekeys' failures on task dispatch.
Now: 15 attempts × 3s = 45s max wait with clearer hint message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: add file_transfer_ack for verified file delivery
The mesh_transfer_file tool had no delivery confirmation — it reported
'sent' but couldn't verify the file was actually written to disk on the
target agent. This caused silent failures where coordinator-notes.txt
appeared to transfer but never landed.
Now:
- Receiver sends file_transfer_ack with success/saved_to/error
- Sender waits up to 15s for ack
- Tool returns 'delivered' (with path) or 'sent_no_ack' (no confirmation)
- Write is verified with stat after writeFileSync
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: filter protocol messages from mesh_inbox, improve file transfer reliability
mesh_inbox now filters out internal protocol messages (handoff blobs,
acks, workspace inject/resume) so the LLM only sees actual sub-agent
replies. Shows filtered_protocol_messages count for visibility.
file_transfer now retries up to 3 times with ack verification — sends,
waits 15s for file_transfer_ack, retries with 3s backoff if no ack.
Returns 'delivered' with exact path or 'sent_no_ack' after all retries.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: auto-decode file_transfer content in mesh_inbox
file_transfer messages now show decoded text content (or a binary
placeholder) instead of raw base64 blobs. Uses null-byte detection
to distinguish text vs binary files. Text files are fully readable
in the inbox; binary files show filename and size.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: stale AMID cache poisoning breaks post-handoff mesh delivery
Root cause: trust+resume loop found OLD sub-agent AMIDs still in the
registry (Docker containers hadn't timed out yet) and cached them.
New AKS sub-agents registered with different AMIDs 26s later, but
the parent never discovered them — all messages went to dead relay
connections and were silently dropped.
Three-layer fix:
1. Stale AMID rejection: collect original_amid from handoff snapshots,
reject matching registry results, wait for NEW AMIDs to appear
2. Prekey readiness gate: verify E2E session is established before
sending workspace_inject (20 attempts × 3s = 60s max)
3. Workspace inject retry: 3 attempts with 20s ack wait each, catches
send errors and retries instead of fire-and-forget
Also filters protocol messages from mesh_inbox and auto-decodes
file_transfer base64 content so LLM sees readable text.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: write HANDOFF_FILES.md manifest after workspace inject
After extracting the workspace tar, writes a manifest listing restored
user-facing files to /sandbox/.openclaw/workspace/HANDOFF_FILES.md.
This makes injected files discoverable when the agent is asked about
its workspace contents post-handoff.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: bump AGT rate limits for multi-agent handoff
Policy: 120 → 240 max_calls/60s for inference:* actions.
Router: 100 → 200 global req/s, 10 → 20 per-agent req/s.
Handoff with 3+ agents doing workspace inject + resume + relay
traffic was hitting the old limits.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: promote incoming/ files to workspace root after handoff inject
Copies files from incoming/ to the workspace root so the agent sees
them immediately when listing files, without needing to know about
the incoming/ directory convention.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: scale down sub-agent deployments during cloud→local decommission
After scaling the parent to 0, also scale down all sub-agent
deployments that were snapshotted. Prevents orphaned sub-agent
pods running on AKS after reverse handoff.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: add bidirectional handoff architecture diagrams and changelog
- New section 12 in architecture-diagrams.md: agent lifecycle across
handoff, forward/reverse flows with sub-agents, stale AMID cache
poisoning problem & fix, workspace injection detail
- CHANGELOG: add handoff features, sub-agent support, rate limit bump
- README: add handoff to features list and CLI reference table
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Pal Lakatos-Toth <pallakatos@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Live-migrate agents (including sub-agents) between local Docker and AKS cloud. Full roundtrip:
azureclaw handoff <name> --to cloud|local.What's included (80 commits)
Core handoff
Sub-agent lifecycle
.handoff-interrupt) for in-progress task checkpointingStale AMID cache poisoning fix
Decommission cleanup
Workspace delivery
HANDOFF_FILES.mdmanifest written after extractionincoming/files promoted to workspace root for agent discoverabilityOther
Testing