Skip to content

Instantly share code, notes, and snippets.

@Jing-yilin
Last active April 7, 2026 07:51
Show Gist options
  • Select an option

  • Save Jing-yilin/12ce6c24129f39a9adeed612a7acef78 to your computer and use it in GitHub Desktop.

Select an option

Save Jing-yilin/12ce6c24129f39a9adeed612a7acef78 to your computer and use it in GitHub Desktop.
PR #42 Plan — World Development Workflow & Identity Backup

PR #42 Plan — World Development Workflow & Identity Backup

PR #42 — World Development Workflow & Identity Backup

Date: 2026-04-07 Branch: hal/world-dev-workflow (from main post PR #41 merge) Spec refs: §7.3, §7.5, §7.8, §8.3–§8.6, §19.2, §20.4 Priority: P1 — Core developer workflow commands are missing; agents cannot introspect worlds Estimated: ~800 LoC net new across 8 files Coverage lift: §10 World API 87%→95%, §01 Identity 95%→100%, §11 CLI 95%→98%


Motivation

After PR #41, the runtime infrastructure is solid — worlds can be started, agents can join, communicate, and build trust. But the developer-facing workflow has critical gaps:

  1. No awn world dev: The spec's canonical local development command doesn't exist. Developers must manually run awn daemon start + awn world start <manifest> — a two-step process the spec explicitly collapses into one.

  2. No world introspection CLI: awn world info, awn world manifest, awn world actions don't exist. An agent cannot discover what a world offers without reading source code.

  3. WorldActionDefinition is name-only: The struct has only a name field — no description, params, schema. This blocks awn world actions (§8.6) and makes the Agent Card's skills array uninformative.

  4. No identity backup: awn identity export/import are missing (§20.4). A lost device means a lost agent identity.

  5. awn world ps: Spec §19.2 requires listing running worlds. awn world list exists and does this but the spec names it ps.


Codebase Anchor Points (verified 2026-04-07, main branch)

Component File:Line What It Provides
WorldSubcommand enum src/cli/commands.rs:791-807 7 variants: New, Check, Discover, Start, WorldStop, WorldList, WorldTrust
WorldStartCommand src/cli/commands.rs:855-894 Takes manifest: PathBuf, calls DaemonClient::start_world()
WorldManifest src/schema/mod.rs:67-86 world, actions, protocol, state_schema, events, trust, operator
WorldDefinition src/schema/mod.rs:185-187 Only slug: String
WorldActionDefinition src/schema/mod.rs:189-192 Only name: Stringminimal
ActionDeclaration src/schema/mod.rs:209-210 Inside ProtocolConfig.actions HashMap — richer, has params
DaemonClient src/daemon/client.rs:45-260 health(), start_world(), stop_world(), list_worlds(), world_trust()
WorldSummary src/daemon/client.rs or compound.rs world_id, slug, port, agent_count, stage
Agent Card handler src/world_api/server.rs:1340-1410 Serves manifest actions as A2A skills at /.well-known/agent.json
IdentitySubcommand src/cli/commands.rs:3299-3308 Init, Show, Register, Verify, RotateTransport, RotateWallet, EmergencyRekey
Identity src/identity/mod.rs:30-150 signing_key_bytes(), public_key_bytes(), agent_id(), save_to(), load_from()
RuntimeDirectories src/config/mod.rs identity_keypair_path(), agent_id_path(), sessions_dir()
Dockerfile generation src/world/mod.rs:514-541 Template for multi-stage Rust build
Docker probe src/doctor.rs:212-255 probe_system_docker() checks Docker availability
world_list_command_result() src/cli/commands.rs Existing implementation for awn world list

User Stories

US-001: Enrich WorldActionDefinition Schema

Goal: Expand WorldActionDefinition from name-only to include description, params, and schema — foundation for awn world actions and better Agent Card skills.

Spec: §8.6 — "Each action should include: name, description, params, param type, required flag, enum values"

Current state: WorldActionDefinition at src/schema/mod.rs:189-192 has only name: String.

Implementation

Step 1: Expand the struct:

// src/schema/mod.rs — replace WorldActionDefinition
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorldActionDefinition {
    pub name: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    /// JSON Schema for action parameters.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub params: Option<serde_json::Value>,
}

Using Option + skip_serializing_if ensures backwards compat with existing world.yaml files that only have name.

Step 2: Expand WorldDefinition minimally:

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorldDefinition {
    pub slug: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub category: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub max_agents: Option<u32>,
}

Step 3: Update Agent Card handler (server.rs:1340-1410) to use enriched fields:

// In agent_card_handler, replace hardcoded description:
"description": action.description.as_deref().unwrap_or(&action.name),

Step 4: Update existing world template YAML files (casino_poker, etc.) to include descriptions and param schemas for their actions.

Files: src/schema/mod.rs, src/world_api/server.rs, world template YAML files LoC: ~60

AC:

  • WorldActionDefinition has name, description, params fields
  • WorldDefinition has name, description, category, max_agents fields
  • All fields are Option — backwards compat with existing YAML
  • Agent Card uses enriched description when available
  • Existing tests pass without modification
  • Test: deserialize YAML with only name → still works
  • Test: deserialize YAML with all fields → all populated

US-002: awn world info <world> — World Summary

Goal: Show a quick summary of a running world's status.

Spec §8.4:

awn world info – summary view:
- World name, slug, worldId
- Status (online/offline)
- Joined agent count
- Owner/operator
- Gateway URL

Implementation

Step 1: Add Info subcommand:

// Add to WorldSubcommand enum
/// Show summary information about a world.
#[command(name = "info")]
Info(WorldInfoCommand),

#[derive(Debug, Args, Clone)]
pub struct WorldInfoCommand {
    /// World URL, world ID, or slug.
    pub world: String,
    /// Output JSON instead of TOON.
    #[arg(long)]
    pub json: bool,
}

Step 2: Implementation approach — fetch from Agent Card (public, no auth needed):

impl WorldInfoCommand {
    pub fn result(&self) -> CommandResult {
        let rt = Runtime::new()?;
        let url = resolve_world_url(&self.world);

        // Fetch /.well-known/agent.json (public, no auth)
        let card = rt.block_on(fetch_agent_card(&url))?;

        // Fetch /v0/health for status + agent count
        let health = rt.block_on(fetch_health(&url))?;

        // Format TOON output
    }
}

The Agent Card already has: name, description, worldId, category, maxAgents, protocolType, skills. Health endpoint provides: status, agent_count.

Step 3: Add helper resolve_world_url():

  • If input starts with http → use as-is
  • If input looks like aw:sha256:... → look up in SessionManager or DaemonClient
  • If input looks like a slug → try DaemonClient list_worlds() to find matching port

Files: src/cli/commands.rs LoC: ~80

AC:

  • awn world info <url> shows name, slug, worldId, status, agent count, owner/operator, gateway URL
  • awn world info <slug> resolves via daemon's running worlds
  • --json returns raw JSON
  • Works without authentication (uses public endpoints only)
  • Test: TOON output includes all spec §8.4 fields (name, slug, worldId, status, joined count, owner/operator, gateway URL)

US-003: awn world manifest <world> — Full Schema + Occupancy

Goal: Show the full world manifest merged with live occupancy data.

Spec §8.5:

Minimum JSON shape:
{
  "worldId": "aw:sha256:...",
  "slug": "my-office",
  "manifest": { "world": {}, "actions": [] },
  "joinedAgents": 4,
  "maxAgents": 20
}

Implementation

Step 1: Add Manifest subcommand:

#[command(name = "manifest")]
Manifest(WorldManifestCommand),

#[derive(Debug, Args, Clone)]
pub struct WorldManifestCommand {
    /// World URL, world ID, or slug.
    pub world: String,
    /// Output JSON instead of TOON.
    #[arg(long)]
    pub json: bool,
}

Step 2: Fetch Agent Card + health, compose hybrid output:

The Agent Card contains the manifest data (skills/actions, capabilities, awnExtensions). Health endpoint provides joined agent count.

For --json mode, combine into the spec-required shape. For TOON mode, render hierarchically:

World
  Name:       Casino Poker
  Slug:       casino-poker
  World ID:   aw:sha256:abc123...
  Category:   gaming

Members
  Joined:     4
  Capacity:   4 / 6

Actions (3)
  bet         Place a bet (amount: number, required)
  fold        Fold current hand
  ready       Signal readiness to start

Protocol
  Type:       turn-based

Files: src/cli/commands.rs LoC: ~80

AC:

  • awn world manifest <world> shows full manifest + occupancy
  • TOON output includes: world metadata, members, actions with params, protocol
  • --json returns spec-required shape with manifest, joinedAgents, maxAgents
  • No auth required
  • Test: JSON output matches spec shape

US-004: awn world actions <world> — Action Schema Details

Goal: Expose action schemas clearly enough that an agent can invoke actions without source code.

Spec §8.6:

Each action should include: name, description, params, param type, required flag, enum values

Implementation

#[command(name = "actions")]
Actions(WorldActionsCommand),

#[derive(Debug, Args, Clone)]
pub struct WorldActionsCommand {
    /// World URL, world ID, or slug.
    pub world: String,
    /// Output JSON instead of TOON.
    #[arg(long)]
    pub json: bool,
}

Fetch Agent Card, extract skills array, render:

Actions (3)

  bet
    Place a bet with a specific amount.
    Params:
      amount    number    required

  fold
    Fold current hand and forfeit the round.
    Params:    (none)

  ready
    Signal readiness to start next round.
    Params:    (none)

If WorldActionDefinition has params (JSON Schema), parse and render param names, types, required flags, and enum values from the schema.

Files: src/cli/commands.rs LoC: ~70

AC:

  • awn world actions <world> lists all actions with schema
  • Shows name, description, params with types, required flag, and enum values when present (per §8.6)
  • --json returns actions array
  • No auth required
  • Test: 3 actions → 3 entries in output
  • Test: action with enum param → enum values displayed

US-005: awn world dev [path] — Local Development Server

Goal: One-command local development experience per spec §8.3.

Spec §8.3:

awn world dev [path]
- path defaults to "." (must contain world.yaml)
- start local world server on default port 8080
- print health URL, worldId, slug, endpoints, agent count, actions
Options: --sandbox, --port N, --gateway URL, --no-announce

Current state: awn world start <manifest> exists but requires daemon running + explicit manifest path. No --port, no --no-announce, no default path resolution.

Implementation

Step 1: Add Dev subcommand:

/// Run a world locally for development.
#[command(name = "dev")]
Dev(WorldDevCommand),

#[derive(Debug, Args, Clone)]
pub struct WorldDevCommand {
    /// Path to world project directory (defaults to ".").
    #[arg(default_value = ".")]
    pub path: PathBuf,
    /// Override default port (8080).
    #[arg(long)]
    pub port: Option<u16>,
    /// Announce to a specific gateway URL.
    #[arg(long)]
    pub gateway: Option<String>,
    /// Run without announcing to gateway.
    #[arg(long)]
    pub no_announce: bool,
    /// Run in background (detached).
    #[arg(long)]
    pub detach: bool,
}

Step 2: Implementation flow:

impl WorldDevCommand {
    pub fn result(&self) -> CommandResult {
        // 1. Resolve manifest path: self.path.join("world.yaml")
        let manifest_path = self.path.join("world.yaml");
        if !manifest_path.exists() {
            return error("world.yaml not found in {path}");
        }

        // 2. Ensure daemon is running (auto-start if not)
        let rt = Runtime::new()?;
        let client = DaemonClient::new();
        if rt.block_on(client.health()).is_err() {
            // Auto-start daemon in background
            crate::daemon::start_daemon_background(&directories)?;
        }

        // 3. Start world via daemon with options
        let summary = rt.block_on(client.start_world_with_options(
            &manifest_path,
            self.port,
            self.gateway.as_deref(),
            self.no_announce,
        ))?;

        // 4. Print dev-friendly output
        // Health URL, worldId, slug, port, endpoints, actions
    }
}

Step 3: Add DaemonClient::start_world_with_options() that accepts port and no_announce:

pub async fn start_world_with_options(
    &self,
    manifest_path: &Path,
    port: Option<u16>,
    gateway: Option<&str>,
    no_announce: bool,
) -> Result<WorldSummary, DaemonClientError> {
    let abs_path = manifest_path.canonicalize()...;
    let mut body = serde_json::json!({
        "manifest_path": abs_path.display().to_string()
    });
    if let Some(port) = port {
        body["port"] = serde_json::json!(port);
    }
    if let Some(gw) = gateway {
        body["gateway"] = serde_json::json!(gw);
    }
    if no_announce {
        body["no_announce"] = serde_json::json!(true);
    }
    // POST /v0/worlds/start
}

Step 4: Update daemon's start_world_handler to accept optional port and no_announce in the request body. Currently it only reads manifest_path.

Files: src/cli/commands.rs, src/daemon/client.rs, src/daemon/compound.rs LoC: ~120

AC:

  • awn world dev in a directory with world.yaml starts the world
  • awn world dev ./path/to/project works with explicit path
  • Default port 8080, overridable with --port N
  • --gateway URL announces to a specific gateway (per §8.3)
  • --no-announce skips gateway announcement
  • Auto-starts daemon if not running
  • Output includes: health URL, worldId, slug, endpoints, agent count, available actions (per §8.3)
  • --detach returns immediately (world runs in background)
  • Error: no world.yaml → clear error message
  • Test: manifest path resolution from directory

US-006: awn world ps — List Running Worlds

Goal: Alias awn world ps to the existing awn world list functionality, matching spec naming.

Spec §19.2:

awn world ps
  SLUG          STATUS    PORT    PID     UPTIME
  my-office     running   8080    12345   2h 15m
  test-world    running   8081    12346   30m

Current state: awn world list exists and works. Spec calls it ps.

Implementation

Add Ps as an alias for WorldList:

/// List locally running worlds (alias for 'list').
#[command(name = "ps")]
Ps,

In the match:

Some(WorldSubcommand::Ps) => world_list_command_result(),

This is a one-line change. Both awn world list and awn world ps will work.

Files: src/cli/commands.rs LoC: ~5

AC:

  • awn world ps returns same output as awn world list
  • Output columns match spec §19.2: SLUG, STATUS, PORT, PID, UPTIME
  • Both commands work
  • Test: awn world ps dispatches to world_list_command_result

US-007: awn identity export and awn identity import

Goal: Enable agent identity backup and restore.

Spec §20.4:

awn identity export > agent-backup.json    # Export identity (encrypted)
awn identity import agent-backup.json      # Import identity

Export format:
{
  "version": 1,
  "agentId": "aw:sha256:abc123...",
  "keypair": "<encrypted>",
  "exportedAt": "2026-03-26T10:00:00Z",
  "note": "Encrypted with passphrase. Use 'awn identity import' to restore."
}

Current state: Identity has signing_key_bytes(), public_key_bytes(), agent_id(). No export/import subcommands.

Implementation

Step 1: Add subcommands:

// Add to IdentitySubcommand enum
/// Export identity (keypair) for backup.
Export(IdentityExportCommand),
/// Import identity from a backup file.
Import(IdentityImportCommand),

#[derive(Debug, Args, Clone)]
pub struct IdentityExportCommand {
    /// Output file path (defaults to stdout).
    #[arg(long)]
    pub output: Option<PathBuf>,
    /// Passphrase for encryption (prompted if not given).
    #[arg(long)]
    pub passphrase: Option<String>,
}

#[derive(Debug, Args, Clone)]
pub struct IdentityImportCommand {
    /// Path to the backup file.
    pub file: PathBuf,
    /// Passphrase for decryption (prompted if not given).
    #[arg(long)]
    pub passphrase: Option<String>,
    /// Overwrite existing identity without confirmation.
    #[arg(long)]
    pub force: bool,
}

Step 2: Export implementation:

fn identity_export_result(cmd: &IdentityExportCommand) -> CommandResult {
    let dirs = RuntimeDirectories::resolve()?;
    let identity = Identity::load_from(&dirs)?;

    // For MVP: base64-encode the signing key bytes.
    // The spec says "encrypted" — use chacha20poly1305 with passphrase-derived key.
    // If no passphrase provided, use a default marker (or require --passphrase).
    let passphrase = cmd.passphrase.as_deref().unwrap_or("awn-export-default");
    let encrypted = encrypt_keypair(identity.signing_key_bytes(), passphrase);

    let export = serde_json::json!({
        "version": 1,
        "agentId": identity.agent_id(),
        "keypair": encrypted,
        "exportedAt": chrono::Utc::now().to_rfc3339(),
        "note": "Encrypted with passphrase. Use 'awn identity import' to restore."
    });

    // Output to file or stdout
    let json_str = serde_json::to_string_pretty(&export).unwrap();
    if let Some(ref path) = cmd.output {
        std::fs::write(path, &json_str)?;
        // Return file path confirmation
    } else {
        // Return JSON as command output (goes to stdout)
    }
}

Step 3: Import implementation:

fn identity_import_result(cmd: &IdentityImportCommand) -> CommandResult {
    let dirs = RuntimeDirectories::resolve()?;

    // Check if identity already exists
    if Identity::load_from(&dirs).is_ok() && !cmd.force {
        return error("identity already exists — use --force to overwrite");
    }

    // Read and parse backup file
    let content = std::fs::read_to_string(&cmd.file)?;
    let backup: serde_json::Value = serde_json::from_str(&content)?;

    // Decrypt keypair
    let passphrase = cmd.passphrase.as_deref().unwrap_or("awn-export-default");
    let key_bytes = decrypt_keypair(backup["keypair"].as_str()?, passphrase)?;

    // Reconstruct Identity and save
    let signing_key = SigningKey::from_bytes(&key_bytes);
    let identity = Identity::from_signing_key_external(signing_key);

    // Verify agent ID matches
    if identity.agent_id() != backup["agentId"].as_str().unwrap_or("") {
        return error("agent ID mismatch — backup may be corrupted");
    }

    identity.save_to(&dirs)?;
    // Return success with agent ID + reminder per §20.4:
    // "Note: Does NOT restore account binding — run `awn auth link` after import"
}

Step 4: For MVP encryption, use base64(xor(key_bytes, sha256(passphrase))) — simple but effective. Full chacha20poly1305 can come later. The format is versioned ("version": 1) so we can upgrade crypto without breaking old exports.

Files: src/cli/commands.rs, src/identity/mod.rs (add encrypt/decrypt helpers) LoC: ~150

AC:

  • awn identity export outputs JSON to stdout
  • awn identity export --output backup.json writes to file
  • awn identity export --passphrase secret encrypts with given passphrase
  • awn identity import backup.json restores identity
  • awn identity import backup.json --passphrase secret decrypts with passphrase
  • Import fails if identity exists (unless --force)
  • Agent ID verified on import (corruption check)
  • Export format matches spec §20.4 shape
  • Import success message includes reminder: "Run awn auth link to restore account binding" (per §20.4)
  • If --passphrase not given, prompt interactively for passphrase (per §20.4: "Prompt for passphrase")
  • Test: export → import round-trip preserves agent ID
  • Test: import with wrong passphrase → error
  • Test: import when identity exists → error without --force

US-008: Shared resolve_world_url() Helper

Goal: Extract common world URL resolution logic used by US-002, US-003, US-004.

All three introspection commands need to resolve a user-provided world reference (URL, slug, or world ID) to an HTTP base URL. This should be a shared helper.

Implementation

// src/cli/commands.rs (or a new src/cli/world_resolve.rs)

/// Resolve a world reference to an HTTP base URL.
///
/// Accepts:
/// - Full URL: "http://localhost:8080" → used as-is
/// - Slug: "casino-poker" → look up in daemon's running worlds
/// - World ID: "aw:sha256:..." → look up in sessions or daemon
fn resolve_world_url(reference: &str) -> Result<String, CommandResult> {
    // 1. If starts with "http://" or "https://" → return as-is
    if reference.starts_with("http://") || reference.starts_with("https://") {
        return Ok(reference.trim_end_matches('/').to_string());
    }

    // 2. Try daemon's running worlds (match by slug or world_id)
    let rt = Runtime::new().map_err(|e| ...)?;
    let client = DaemonClient::new();
    if let Ok(worlds) = rt.block_on(client.list_worlds()) {
        for w in &worlds {
            if w.slug == reference || w.world_id == reference {
                return Ok(format!("http://localhost:{}", w.port));
            }
        }
    }

    // 3. Try session manager (for worlds joined but not locally running)
    let dirs = RuntimeDirectories::resolve().map_err(|e| ...)?;
    let manager = SessionManager::new(&dirs);
    if let Ok(session) = manager.resolve(reference) {
        return Ok(session.world_url);
    }

    Err(error(format!("could not resolve world: {reference}")))
}

/// Fetch the Agent Card from a world's /.well-known/agent.json endpoint.
async fn fetch_agent_card(base_url: &str) -> Result<serde_json::Value, WorldClientError> {
    let url = format!("{}/.well-known/agent.json", base_url);
    let resp = reqwest::get(&url).await?;
    Ok(resp.json().await?)
}

Files: src/cli/commands.rs LoC: ~60

AC:

  • URL input → used directly
  • Slug input → resolved via daemon
  • World ID input → resolved via sessions
  • Unresolvable → clear error message
  • Test: URL passthrough
  • Test: slug resolution

Files Changed

File Change Type LoC Description
src/schema/mod.rs Modified ~30 Enrich WorldActionDefinition + WorldDefinition
src/cli/commands.rs Modified ~450 6 new subcommands (info, manifest, actions, dev, ps, export/import) + resolve helper
src/daemon/client.rs Modified ~30 start_world_with_options() method
src/daemon/compound.rs Modified ~20 Accept port/no_announce in start handler
src/identity/mod.rs Modified ~60 Export/import encrypt/decrypt helpers
src/world_api/server.rs Modified ~10 Use enriched action fields in Agent Card
src/world/mod.rs Modified ~20 Update generated world.yaml to include action descriptions
Tests Modified ~80 Schema compat, round-trip export/import, command output

Total: ~800 LoC net new across 8 files


Dependency Graph

US-001 (Enrich schema)     ─── FIRST — foundation for US-003, US-004
US-008 (resolve helper)    ─── SECOND — foundation for US-002, US-003, US-004
  ↓
US-002 (world info)        ─── depends on US-008
US-003 (world manifest)    ─── depends on US-001 + US-008
US-004 (world actions)     ─── depends on US-001 + US-008
  ↓
US-005 (world dev)         ─── independent (daemon changes)
US-006 (world ps)          ─── independent (trivial alias)
US-007 (identity export)   ─── independent (identity changes)

Recommended commit order:
  1. US-001 (schema enrichment)        — unblocks manifest/actions
  2. US-008 (resolve helper)           — unblocks info/manifest/actions
  3. US-006 (world ps alias)           — trivial, ship early
  4. US-002 (world info)               — first introspection command
  5. US-003 + US-004 (manifest+actions) — together, share code
  6. US-005 (world dev)                — daemon changes
  7. US-007 (identity export/import)   — independent

New CLI Surface (+7 commands)

Command Spec Interaction
awn world dev [path] [--port N] [--gateway URL] [--no-announce] [--detach] §8.3 DaemonClient → start world
awn world ps §19.2 DaemonClient → list worlds
awn world info <world> [--json] §8.4 HTTP → Agent Card + health
awn world manifest <world> [--json] §8.5 HTTP → Agent Card + health
awn world actions <world> [--json] §8.6 HTTP → Agent Card
awn identity export [--output file] [--passphrase] §20.4 Local FS
awn identity import <file> [--passphrase] [--force] §20.4 Local FS

Test Plan

Unit Tests (18)

Test Story Validates
action_def_deserialize_name_only US-001 Backwards compat: YAML with only name
action_def_deserialize_full US-001 All fields populated from YAML
world_def_deserialize_minimal US-001 Backwards compat: YAML with only slug
world_def_deserialize_full US-001 All optional fields populated
resolve_world_url_http US-008 URL passthrough
resolve_world_url_slug US-008 Slug → daemon lookup
world_info_toon_output US-002 Output includes all spec fields
world_manifest_json_shape US-003 JSON matches spec minimum shape
world_actions_toon_output US-004 Actions with params rendered
world_actions_no_params US-004 Actions without params → "(none)"
world_dev_resolves_manifest US-005 ./world.yaml found from path
world_dev_missing_yaml US-005 No world.yaml → error
world_ps_dispatches US-006 Same as world list
identity_export_json_shape US-007 Matches spec §20.4 format
identity_export_import_roundtrip US-007 Export → import preserves key
identity_import_wrong_passphrase US-007 Decryption fails → error
identity_import_existing_no_force US-007 Existing identity → error
identity_import_existing_force US-007 --force overwrites

Out of Scope

Feature Why Where
awn world dev --sandbox Requires Docker execution, credential proxy (§14.3) Future PR
awn world build Docker image build logic Future PR
awn world publish Requires Hub package registry Future PR (Hub)
Gateway state persistence Architectural decision (SQLite vs file) Future PR
Benchmark world template New world logic, not CLI workflow Future PR
chacha20poly1305 encryption Proper AEAD for export; MVP uses XOR+SHA256 Follow-up

Risk Assessment

Risk Level Mitigation
Schema enrichment breaks existing YAML Low All new fields are Option with skip_serializing_if; backwards compat guaranteed
resolve_world_url fails for remote worlds Medium Falls back to sessions; clear error message if unresolvable
awn world dev auto-starts daemon unexpectedly Low Only if daemon not running; prints clear message
Export encryption too weak (XOR) Medium Format versioned; upgrade to AEAD in follow-up. Warns user in export output
Agent Card endpoint changes break info/manifest Low Card format is under our control; test assertions cover expected fields

Implementation Notes for hal Agent

  1. US-001: Use #[serde(default, skip_serializing_if = "Option::is_none")] on ALL new fields to ensure existing world.yaml files continue to parse. Run cargo test after every schema change.

  2. US-002/003/004: These 3 commands share the same resolve + fetch pattern. Extract resolve_world_url() and fetch_agent_card() as shared helpers FIRST (US-008), then each command is just formatting logic.

  3. US-005: The daemon's start_world_handler in compound.rs currently only reads manifest_path from the JSON body. Extend it to also read optional port and no_announce fields. Don't break the existing awn world start command.

  4. US-006: Literally 3 lines of code — add Ps variant, add match arm, done.

  5. US-007: For MVP export encryption, use base64(signing_key_bytes XOR sha256(passphrase)). This is NOT production-grade crypto, but the format has "version": 1 so we can upgrade later. The spec says "encrypted" but doesn't mandate a specific algorithm.

  6. Don't add content_type to SendMessageRequest or any other unrelated changes. Keep this PR focused on world dev workflow + identity backup.

  7. Run cargo fmt and cargo clippy before every commit. CI is strict.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment