PR #42 Plan — 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%
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:
-
No
awn world dev: The spec's canonical local development command doesn't exist. Developers must manually runawn daemon start+awn world start <manifest>— a two-step process the spec explicitly collapses into one. -
No world introspection CLI:
awn world info,awn world manifest,awn world actionsdon't exist. An agent cannot discover what a world offers without reading source code. -
WorldActionDefinitionis name-only: The struct has only anamefield — no description, params, schema. This blocksawn world actions(§8.6) and makes the Agent Card's skills array uninformative. -
No identity backup:
awn identity export/importare missing (§20.4). A lost device means a lost agent identity. -
awn world ps: Spec §19.2 requires listing running worlds.awn world listexists and does this but the spec names itps.
| 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: String — minimal |
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 |
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.
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:
-
WorldActionDefinitionhasname,description,paramsfields -
WorldDefinitionhasname,description,category,max_agentsfields - 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
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
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 -
--jsonreturns 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)
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
}
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
-
--jsonreturns spec-required shape withmanifest,joinedAgents,maxAgents - No auth required
- Test: JSON output matches spec shape
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
#[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)
-
--jsonreturns actions array - No auth required
- Test: 3 actions → 3 entries in output
- Test: action with enum param → enum values displayed
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.
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 devin a directory withworld.yamlstarts the world -
awn world dev ./path/to/projectworks with explicit path - Default port 8080, overridable with
--port N -
--gateway URLannounces to a specific gateway (per §8.3) -
--no-announceskips gateway announcement - Auto-starts daemon if not running
- Output includes: health URL, worldId, slug, endpoints, agent count, available actions (per §8.3)
-
--detachreturns immediately (world runs in background) - Error: no
world.yaml→ clear error message - Test: manifest path resolution from directory
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.
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 psreturns same output asawn world list - Output columns match spec §19.2: SLUG, STATUS, PORT, PID, UPTIME
- Both commands work
- Test:
awn world psdispatches toworld_list_command_result
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.
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 exportoutputs JSON to stdout -
awn identity export --output backup.jsonwrites to file -
awn identity export --passphrase secretencrypts with given passphrase -
awn identity import backup.jsonrestores identity -
awn identity import backup.json --passphrase secretdecrypts 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 linkto restore account binding" (per §20.4) - If
--passphrasenot 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
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.
// 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
| 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
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
| 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 | 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 |
| 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 | 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 |
-
US-001: Use
#[serde(default, skip_serializing_if = "Option::is_none")]on ALL new fields to ensure existingworld.yamlfiles continue to parse. Runcargo testafter every schema change. -
US-002/003/004: These 3 commands share the same resolve + fetch pattern. Extract
resolve_world_url()andfetch_agent_card()as shared helpers FIRST (US-008), then each command is just formatting logic. -
US-005: The daemon's
start_world_handlerincompound.rscurrently only readsmanifest_pathfrom the JSON body. Extend it to also read optionalportandno_announcefields. Don't break the existingawn world startcommand. -
US-006: Literally 3 lines of code — add
Psvariant, add match arm, done. -
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": 1so we can upgrade later. The spec says "encrypted" but doesn't mandate a specific algorithm. -
Don't add
content_typetoSendMessageRequestor any other unrelated changes. Keep this PR focused on world dev workflow + identity backup. -
Run
cargo fmtandcargo clippybefore every commit. CI is strict.