From 554b7ea8210e6b19b07a9a74cf0500d4896cd04f Mon Sep 17 00:00:00 2001 From: Janko Date: Fri, 13 Mar 2026 14:19:44 +0100 Subject: [PATCH 1/2] =?UTF-8?q?feat(runtime):=20implement=20Product=20Phas?= =?UTF-8?q?e=201=20=E2=80=94=20portable=20sovereign=20agents?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add DID identity encoding, CLI subcommands (run/resume/verify/inspect), checkpoint history archival, lineage chain verification, heartbeat demo agent, and portable agent demo script. The checkpoint file IS the agent — copy it anywhere, resume it with the same DID and continuous tick count. Co-Authored-By: Claude Opus 4.6 --- Makefile | 16 +- agents/heartbeat/Makefile | 7 + agents/heartbeat/agent.manifest.json | 9 + agents/heartbeat/go.mod | 7 + agents/heartbeat/main.go | 68 +++++ cmd/igord/main.go | 394 +++++++++++++++++++++++++++ internal/inspector/chain.go | 211 ++++++++++++++ internal/storage/fs_provider.go | 37 +++ pkg/identity/did.go | 125 +++++++++ pkg/identity/did_test.go | 117 ++++++++ scripts/demo-portable.sh | 107 ++++++++ 11 files changed, 1097 insertions(+), 1 deletion(-) create mode 100644 agents/heartbeat/Makefile create mode 100644 agents/heartbeat/agent.manifest.json create mode 100644 agents/heartbeat/go.mod create mode 100644 agents/heartbeat/main.go create mode 100644 internal/inspector/chain.go create mode 100644 pkg/identity/did.go create mode 100644 pkg/identity/did_test.go create mode 100755 scripts/demo-portable.sh diff --git a/Makefile b/Makefile index 8c34768..b9ed3a5 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help bootstrap build clean test lint vet fmt fmt-check tidy agent agent-reconciliation run-agent demo gh-check gh-metadata gh-release +.PHONY: help bootstrap build clean test lint vet fmt fmt-check tidy agent agent-heartbeat agent-reconciliation run-agent demo demo-portable gh-check gh-metadata gh-release .DEFAULT_GOAL := help @@ -6,6 +6,7 @@ BINARY_NAME := igord BINARY_DIR := bin AGENT_DIR := agents/example +HEARTBEAT_AGENT_DIR := agents/heartbeat RECONCILIATION_AGENT_DIR := agents/reconciliation # Go commands @@ -45,6 +46,7 @@ clean: ## Remove build artifacts rm -rf checkpoints rm -f agents/example/agent.wasm rm -f agents/example/agent.wasm.checkpoint + rm -f agents/heartbeat/agent.wasm rm -f agents/reconciliation/agent.wasm @echo "Clean complete" @@ -96,6 +98,13 @@ run-agent: build agent ## Build and run example agent locally @echo "Running agent with default budget (1.0)..." ./$(BINARY_DIR)/$(BINARY_NAME) --run-agent $(AGENT_DIR)/agent.wasm --budget 1.0 +agent-heartbeat: ## Build heartbeat demo agent WASM + @echo "Building heartbeat agent..." + @which tinygo > /dev/null || \ + (echo "tinygo not found. See docs/governance/DEVELOPMENT.md for installation" && exit 1) + cd $(HEARTBEAT_AGENT_DIR) && $(MAKE) build + @echo "Agent built: $(HEARTBEAT_AGENT_DIR)/agent.wasm" + agent-reconciliation: ## Build reconciliation agent WASM @echo "Building reconciliation agent..." @which tinygo > /dev/null || \ @@ -110,6 +119,11 @@ demo: build agent-reconciliation ## Build and run reconciliation demo @echo "Running Bridge Reconciliation Demo..." ./$(BINARY_DIR)/demo-reconciliation --wasm $(RECONCILIATION_AGENT_DIR)/agent.wasm +demo-portable: build agent-heartbeat ## Run the portable agent demo (run, stop, resume, verify) + @echo "Running Portable Agent Demo..." + @chmod +x scripts/demo-portable.sh + @./scripts/demo-portable.sh + check: fmt-check vet lint test ## Run all checks (formatting, vet, lint, tests) @echo "All checks passed" diff --git a/agents/heartbeat/Makefile b/agents/heartbeat/Makefile new file mode 100644 index 0000000..7fa1ed3 --- /dev/null +++ b/agents/heartbeat/Makefile @@ -0,0 +1,7 @@ +.PHONY: build clean + +build: + tinygo build -target=wasi -no-debug -o agent.wasm . + +clean: + rm -f agent.wasm diff --git a/agents/heartbeat/agent.manifest.json b/agents/heartbeat/agent.manifest.json new file mode 100644 index 0000000..11f02ba --- /dev/null +++ b/agents/heartbeat/agent.manifest.json @@ -0,0 +1,9 @@ +{ + "capabilities": { + "clock": { "version": 1 }, + "log": { "version": 1 } + }, + "resource_limits": { + "max_memory_bytes": 67108864 + } +} diff --git a/agents/heartbeat/go.mod b/agents/heartbeat/go.mod new file mode 100644 index 0000000..79c0968 --- /dev/null +++ b/agents/heartbeat/go.mod @@ -0,0 +1,7 @@ +module github.com/simonovic86/igor/agents/heartbeat + +go 1.24 + +require github.com/simonovic86/igor/sdk/igor v0.0.0 + +replace github.com/simonovic86/igor/sdk/igor => ../../sdk/igor diff --git a/agents/heartbeat/main.go b/agents/heartbeat/main.go new file mode 100644 index 0000000..e29dd2a --- /dev/null +++ b/agents/heartbeat/main.go @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 + +//go:build tinygo || wasip1 + +package main + +import ( + "github.com/simonovic86/igor/sdk/igor" +) + +const stateSize = 28 + +// Heartbeat is a demo agent that emits visible heartbeat logs on every tick. +// It demonstrates continuity across checkpoint/resume/migration by tracking +// tick count and birth time — both survive transparent migration. +type Heartbeat struct { + TickCount uint64 + BirthNano int64 + LastNano int64 + MessageNum uint32 +} + +func (h *Heartbeat) Init() {} + +func (h *Heartbeat) Tick() bool { + h.TickCount++ + + now := igor.ClockNow() + if h.BirthNano == 0 { + h.BirthNano = now + } + h.LastNano = now + + ageSec := (h.LastNano - h.BirthNano) / 1_000_000_000 + + igor.Logf("[heartbeat] tick=%d age=%ds", h.TickCount, ageSec) + + if h.TickCount%10 == 0 { + h.MessageNum++ + igor.Logf("[heartbeat] MILESTONE #%d: survived %d ticks across %ds", + h.MessageNum, h.TickCount, ageSec) + } + + return false +} + +func (h *Heartbeat) Marshal() []byte { + return igor.NewEncoder(stateSize). + Uint64(h.TickCount). + Int64(h.BirthNano). + Int64(h.LastNano). + Uint32(h.MessageNum). + Finish() +} + +func (h *Heartbeat) Unmarshal(data []byte) { + d := igor.NewDecoder(data) + h.TickCount = d.Uint64() + h.BirthNano = d.Int64() + h.LastNano = d.Int64() + h.MessageNum = d.Uint32() + if err := d.Err(); err != nil { + panic("unmarshal checkpoint: " + err.Error()) + } +} + +func init() { igor.Run(&Heartbeat{}) } +func main() {} diff --git a/cmd/igord/main.go b/cmd/igord/main.go index 35b5d74..d764e97 100644 --- a/cmd/igord/main.go +++ b/cmd/igord/main.go @@ -9,6 +9,7 @@ import ( "log/slog" "os" "os/signal" + "path/filepath" "syscall" "time" @@ -31,6 +32,25 @@ import ( ) func main() { + // Handle subcommands before flag parsing. + if len(os.Args) > 1 { + switch os.Args[1] { + case "run": + subcmdRun(os.Args[2:]) + return + case "resume": + subcmdResume(os.Args[2:]) + return + case "verify": + subcmdVerify(os.Args[2:]) + return + case "inspect": + subcmdInspect(os.Args[2:]) + return + } + } + + // Legacy flag-based CLI (backwards compatible). // Parse CLI flags runAgent := flag.String("run-agent", "", "Path to WASM agent to run locally") budgetFlag := flag.Float64("budget", 1.0, "Initial budget for agent execution") @@ -488,3 +508,377 @@ func loadOrGenerateIdentity( logger.Info("Agent identity generated and saved", "agent_id", agentID) return id, nil } + +// subcmdRun implements "igord run [--budget N] [--manifest path]". +// Runs an agent locally with a simplified setup (no P2P, no migration). +func subcmdRun(args []string) { + fs := flag.NewFlagSet("run", flag.ExitOnError) + budgetFlag := fs.Float64("budget", 1.0, "Initial budget for agent execution") + manifestPath := fs.String("manifest", "", "Path to capability manifest JSON") + checkpointDir := fs.String("checkpoint-dir", "checkpoints", "Directory for checkpoint storage") + agentID := fs.String("agent-id", "", "Agent ID (default: derived from WASM filename)") + leaseDuration := fs.Duration("lease-duration", 0, "Lease validity period (0 = disabled)") + fs.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: igord run [flags]\n\n") + fmt.Fprintf(os.Stderr, "Run a WASM agent locally. The agent gets a DID identity,\n") + fmt.Fprintf(os.Stderr, "checkpoints periodically, and can be resumed from checkpoint.\n\n") + fs.PrintDefaults() + } + if err := fs.Parse(args); err != nil { + os.Exit(1) + } + if fs.NArg() < 1 { + fs.Usage() + os.Exit(1) + } + wasmPath := fs.Arg(0) + + aid := *agentID + if aid == "" { + aid = agentIDFromPath(wasmPath) + } + + runStandalone(wasmPath, aid, *budgetFlag, *manifestPath, *checkpointDir, *leaseDuration) +} + +// subcmdResume implements "igord resume --checkpoint --wasm ". +// Resumes an agent from a checkpoint file. +func subcmdResume(args []string) { + fs := flag.NewFlagSet("resume", flag.ExitOnError) + checkpointPath := fs.String("checkpoint", "", "Path to checkpoint file") + wasmPath := fs.String("wasm", "", "Path to WASM binary") + budgetFlag := fs.Float64("budget", 0, "Override budget (0 = use checkpoint budget)") + manifestPath := fs.String("manifest", "", "Path to capability manifest JSON") + checkpointDir := fs.String("checkpoint-dir", "checkpoints", "Directory for checkpoint storage") + agentID := fs.String("agent-id", "", "Agent ID (default: derived from WASM filename)") + fs.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: igord resume --checkpoint --wasm [flags]\n\n") + fmt.Fprintf(os.Stderr, "Resume an agent from a checkpoint file. The agent continues\n") + fmt.Fprintf(os.Stderr, "from exactly where it left off with the same DID identity.\n\n") + fs.PrintDefaults() + } + if err := fs.Parse(args); err != nil { + os.Exit(1) + } + if *checkpointPath == "" || *wasmPath == "" { + fs.Usage() + os.Exit(1) + } + + aid := *agentID + if aid == "" { + aid = agentIDFromPath(*wasmPath) + } + + resumeFromCheckpoint(*checkpointPath, *wasmPath, aid, *budgetFlag, *manifestPath, *checkpointDir) +} + +// subcmdVerify implements "igord verify ". +func subcmdVerify(args []string) { + fs := flag.NewFlagSet("verify", flag.ExitOnError) + fs.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: igord verify \n\n") + fmt.Fprintf(os.Stderr, "Verify the cryptographic lineage of an agent's checkpoint history.\n") + fmt.Fprintf(os.Stderr, "The history directory contains numbered .ckpt files.\n\n") + } + if err := fs.Parse(args); err != nil { + os.Exit(1) + } + if fs.NArg() < 1 { + fs.Usage() + os.Exit(1) + } + + result, err := inspector.VerifyChain(fs.Arg(0)) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + result.PrintChain(os.Stdout) + if !result.ChainValid { + os.Exit(1) + } +} + +// subcmdInspect implements "igord inspect [--wasm path]". +func subcmdInspect(args []string) { + fs := flag.NewFlagSet("inspect", flag.ExitOnError) + wasmPath := fs.String("wasm", "", "Optional WASM binary to verify against checkpoint hash") + fs.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: igord inspect [--wasm path]\n\n") + fmt.Fprintf(os.Stderr, "Parse and display a checkpoint file, including DID identity.\n\n") + fs.PrintDefaults() + } + if err := fs.Parse(args); err != nil { + os.Exit(1) + } + if fs.NArg() < 1 { + fs.Usage() + os.Exit(1) + } + runInspectorWithDID(fs.Arg(0), *wasmPath) +} + +// runStandalone runs an agent without P2P, migration, or leases. +func runStandalone(wasmPath, agentID string, budgetVal float64, manifestPath, checkpointDir string, leaseDuration time.Duration) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + logger := logging.NewLogger() + + storageProvider, err := storage.NewFSProvider(checkpointDir, logger) + if err != nil { + logger.Error("Failed to create storage provider", "error", err) + os.Exit(1) + } + + engine, err := runtime.NewEngine(ctx, logger) + if err != nil { + logger.Error("Failed to create runtime engine", "error", err) + os.Exit(1) + } + defer engine.Close(ctx) + + // Load or generate agent identity. + agentIdent, err := loadOrGenerateIdentity(ctx, storageProvider, agentID, logger) + if err != nil { + logger.Error("Agent identity error", "error", err) + os.Exit(1) + } + + logger.Info("Agent identity", + "did", agentIdent.DID(), + "did_short", agentIdent.DIDShort(), + ) + + manifestData := runner.LoadManifestData(wasmPath, manifestPath, logger) + budgetMicrocents := budget.FromFloat(budgetVal) + + instance, err := agent.LoadAgent( + ctx, engine, wasmPath, agentID, storageProvider, + budgetMicrocents, 1000000, // default price: 1.0/s + manifestData, nil, "", agentIdent, logger, + ) + if err != nil { + logger.Error("Failed to load agent", "error", err) + os.Exit(1) + } + defer instance.Close(ctx) + + instance.BudgetAdapter = settlement.NewMockAdapter(logger) + + if err := instance.Init(ctx); err != nil { + logger.Error("Failed to initialize agent", "error", err) + os.Exit(1) + } + + // Try to load existing checkpoint. + if loadErr := instance.LoadCheckpointFromStorage(ctx); loadErr != nil { + logger.Info("No existing checkpoint, starting fresh") + } + + if leaseDuration > 0 { + leaseCfg := authority.LeaseConfig{ + Duration: leaseDuration, + RenewalWindow: 0.5, + GracePeriod: 10 * time.Second, + } + instance.Lease = authority.NewLease(leaseCfg) + } + + logger.Info("Agent started", + "agent_id", agentID, + "did", agentIdent.DIDShort(), + "budget", budget.Format(budgetMicrocents), + "tick", instance.TickNumber, + ) + + runTickLoop(ctx, instance, logger) +} + +// resumeFromCheckpoint resumes an agent from a specific checkpoint file. +func resumeFromCheckpoint(checkpointPath, wasmPath, agentID string, budgetVal float64, manifestPath, checkpointDir string) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + logger := logging.NewLogger() + + // Copy checkpoint file to storage directory so the agent can find it. + storageProvider, err := storage.NewFSProvider(checkpointDir, logger) + if err != nil { + logger.Error("Failed to create storage provider", "error", err) + os.Exit(1) + } + + // Read the provided checkpoint. + ckptData, err := os.ReadFile(checkpointPath) + if err != nil { + logger.Error("Failed to read checkpoint", "path", checkpointPath, "error", err) + os.Exit(1) + } + + // Save it to the storage directory so LoadCheckpointFromStorage finds it. + if err := storageProvider.SaveCheckpoint(ctx, agentID, ckptData); err != nil { + logger.Error("Failed to stage checkpoint", "error", err) + os.Exit(1) + } + + // Extract identity from checkpoint if present. + hdr, _, parseErr := agent.ParseCheckpointHeader(ckptData) + if parseErr != nil { + logger.Error("Failed to parse checkpoint", "error", parseErr) + os.Exit(1) + } + + engine, err := runtime.NewEngine(ctx, logger) + if err != nil { + logger.Error("Failed to create runtime engine", "error", err) + os.Exit(1) + } + defer engine.Close(ctx) + + // Try to load identity from checkpoint's agent pubkey or from storage. + var agentIdent *identity.AgentIdentity + idData, idErr := storageProvider.LoadIdentity(ctx, agentID) + if idErr == nil { + agentIdent, _ = identity.UnmarshalBinary(idData) + } + if agentIdent == nil { + agentIdent, err = loadOrGenerateIdentity(ctx, storageProvider, agentID, logger) + if err != nil { + logger.Error("Agent identity error", "error", err) + os.Exit(1) + } + } + + logger.Info("Resuming agent", + "did", agentIdent.DID(), + "from_tick", hdr.TickNumber, + "checkpoint", checkpointPath, + ) + + manifestData := runner.LoadManifestData(wasmPath, manifestPath, logger) + + b := budget.FromFloat(budgetVal) + if b == 0 { + b = hdr.Budget + } + + instance, err := agent.LoadAgent( + ctx, engine, wasmPath, agentID, storageProvider, + b, hdr.PricePerSecond, + manifestData, nil, "", agentIdent, logger, + ) + if err != nil { + logger.Error("Failed to load agent", "error", err) + os.Exit(1) + } + defer instance.Close(ctx) + + instance.BudgetAdapter = settlement.NewMockAdapter(logger) + + if err := instance.Init(ctx); err != nil { + logger.Error("Failed to initialize agent", "error", err) + os.Exit(1) + } + + if err := instance.LoadCheckpointFromStorage(ctx); err != nil { + logger.Error("Failed to resume from checkpoint", "error", err) + os.Exit(1) + } + + logger.Info("Agent resumed", + "agent_id", agentID, + "did", agentIdent.DIDShort(), + "tick", instance.TickNumber, + "budget", budget.Format(b), + ) + + runTickLoop(ctx, instance, logger) +} + +// runTickLoop runs the simplified tick loop (no replay, no lease, no migration). +func runTickLoop(ctx context.Context, instance *agent.Instance, logger *slog.Logger) { + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + const ( + normalInterval = 1 * time.Second + fastInterval = 10 * time.Millisecond + ) + + checkpointTicker := time.NewTicker(5 * time.Second) + defer checkpointTicker.Stop() + + tickTimer := time.NewTimer(normalInterval) + defer tickTimer.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-sigChan: + logger.Info("Shutting down, saving final checkpoint...") + if err := instance.SaveCheckpointToStorage(ctx); err != nil { + logger.Error("Failed to save checkpoint on shutdown", "error", err) + } else { + logger.Info("Final checkpoint saved") + } + return + case <-tickTimer.C: + hasMore, err := runner.SafeTick(ctx, instance) + if err != nil { + logger.Error("Tick failed", "error", err) + if saveErr := instance.SaveCheckpointToStorage(ctx); saveErr != nil { + logger.Error("Failed to save checkpoint after error", "error", saveErr) + } + return + } + if hasMore { + tickTimer.Reset(fastInterval) + } else { + tickTimer.Reset(normalInterval) + } + case <-checkpointTicker.C: + if err := instance.SaveCheckpointToStorage(ctx); err != nil { + logger.Error("Failed to save checkpoint", "error", err) + } + } + } +} + +// runInspectorWithDID inspects a checkpoint and displays DID identity. +func runInspectorWithDID(checkpointPath, wasmPath string) { + result, err := inspector.InspectFile(checkpointPath) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + if wasmPath != "" { + if verr := result.VerifyWASM(wasmPath); verr != nil { + fmt.Fprintf(os.Stderr, "Warning: %v\n", verr) + } + } + result.Print(os.Stdout) + + // Show DID if checkpoint has lineage. + if result.HasLineage && len(result.AgentPubKey) == 32 { + id := &identity.AgentIdentity{PublicKey: result.AgentPubKey} + fmt.Fprintf(os.Stdout, "Agent DID: %s\n", id.DID()) + } +} + +// agentIDFromPath derives an agent ID from a WASM file path. +func agentIDFromPath(wasmPath string) string { + base := filepath.Base(wasmPath) + ext := filepath.Ext(base) + name := base[:len(base)-len(ext)] + if name == "" || name == "agent" { + // Use parent directory name. + name = filepath.Base(filepath.Dir(wasmPath)) + } + if name == "" || name == "." { + name = "agent" + } + return name +} diff --git a/internal/inspector/chain.go b/internal/inspector/chain.go new file mode 100644 index 0000000..0f9cf20 --- /dev/null +++ b/internal/inspector/chain.go @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: Apache-2.0 + +package inspector + +import ( + "crypto/ed25519" + "encoding/hex" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/simonovic86/igor/internal/agent" + "github.com/simonovic86/igor/pkg/identity" + "github.com/simonovic86/igor/pkg/lineage" +) + +// ChainResult holds the result of verifying an agent's full checkpoint lineage. +type ChainResult struct { + AgentDID string + AgentPubKeyHex string + Checkpoints int + FirstTick uint64 + LastTick uint64 + ChainValid bool + Errors []string + // Segments tracks execution across different WASM binaries (migration points). + Segments []ChainSegment +} + +// ChainSegment represents a contiguous run of checkpoints with the same WASM hash. +type ChainSegment struct { + WASMHashHex string + StartTick uint64 + EndTick uint64 + Count int +} + +// chainState tracks mutable state across checkpoint verification iterations. +type chainState struct { + prevContentHash [32]byte + prevTick uint64 + currentSegment *ChainSegment + agentPubKey ed25519.PublicKey +} + +// VerifyChain walks a checkpoint history directory and verifies the full +// cryptographic lineage: each checkpoint's signature, and prevHash continuity. +func VerifyChain(historyDir string) (*ChainResult, error) { + files, err := listCheckpointFiles(historyDir) + if err != nil { + return nil, err + } + + result := &ChainResult{ChainValid: true} + state := &chainState{} + + for i, f := range files { + data, readErr := os.ReadFile(f) + if readErr != nil { + result.addError("failed to read %s: %v", filepath.Base(f), readErr) + continue + } + + hdr, agentState, parseErr := agent.ParseCheckpointHeader(data) + if parseErr != nil { + result.addError("failed to parse %s: %v", filepath.Base(f), parseErr) + continue + } + + result.Checkpoints++ + verifyCheckpoint(result, state, i, data, hdr, agentState) + } + + if len(result.Errors) > 0 { + result.ChainValid = false + } + + return result, nil +} + +// listCheckpointFiles returns sorted .ckpt file paths from the history directory. +func listCheckpointFiles(historyDir string) ([]string, error) { + entries, err := os.ReadDir(historyDir) + if err != nil { + return nil, fmt.Errorf("read history directory: %w", err) + } + + var files []string + for _, e := range entries { + if !e.IsDir() && strings.HasSuffix(e.Name(), ".ckpt") { + files = append(files, filepath.Join(historyDir, e.Name())) + } + } + + if len(files) == 0 { + return nil, fmt.Errorf("no checkpoint files found in %s", historyDir) + } + + sort.Strings(files) // sorted by tick number (zero-padded filenames) + return files, nil +} + +// verifyCheckpoint validates a single checkpoint against the chain state. +func verifyCheckpoint(result *ChainResult, cs *chainState, i int, data []byte, hdr *agent.CheckpointHeader, agentState []byte) { + if i == 0 { + result.FirstTick = hdr.TickNumber + if hdr.HasLineage { + cs.agentPubKey = hdr.AgentPubKey + result.AgentPubKeyHex = hex.EncodeToString(cs.agentPubKey) + id := &identity.AgentIdentity{PublicKey: cs.agentPubKey} + result.AgentDID = id.DID() + } + } + result.LastTick = hdr.TickNumber + + if i > 0 && hdr.TickNumber <= cs.prevTick { + result.addError("tick %d: non-monotonic (prev was %d)", hdr.TickNumber, cs.prevTick) + } + + verifyLineage(result, cs, i, data, hdr, agentState) + updateSegments(result, cs, hdr) + + cs.prevContentHash = lineage.ContentHash(data) + cs.prevTick = hdr.TickNumber +} + +// verifyLineage checks signature, prevHash chain, and consistent identity. +func verifyLineage(result *ChainResult, cs *chainState, i int, data []byte, hdr *agent.CheckpointHeader, agentState []byte) { + if !hdr.HasLineage { + if i > 0 { + result.addError("tick %d: missing lineage (not v4)", hdr.TickNumber) + } + return + } + + signingDomain := lineage.BuildSigningDomain(data[:145], agentState) + if !lineage.VerifyCheckpoint(signingDomain, hdr.AgentPubKey, hdr.Signature) { + result.addError("tick %d: INVALID signature", hdr.TickNumber) + } + + if i > 0 { + if hdr.PrevHash != cs.prevContentHash { + result.addError("tick %d: prevHash mismatch (expected %s, got %s)", + hdr.TickNumber, + hex.EncodeToString(cs.prevContentHash[:8])+"...", + hex.EncodeToString(hdr.PrevHash[:8])+"...", + ) + } + if !hdr.AgentPubKey.Equal(cs.agentPubKey) { + result.addError("tick %d: agent identity changed", hdr.TickNumber) + } + } +} + +// updateSegments tracks WASM hash segments across checkpoints. +func updateSegments(result *ChainResult, cs *chainState, hdr *agent.CheckpointHeader) { + wasmHex := hex.EncodeToString(hdr.WASMHash[:]) + if cs.currentSegment == nil || cs.currentSegment.WASMHashHex != wasmHex { + cs.currentSegment = &ChainSegment{ + WASMHashHex: wasmHex, + StartTick: hdr.TickNumber, + EndTick: hdr.TickNumber, + Count: 1, + } + result.Segments = append(result.Segments, *cs.currentSegment) + } else { + cs.currentSegment.EndTick = hdr.TickNumber + cs.currentSegment.Count++ + result.Segments[len(result.Segments)-1] = *cs.currentSegment + } +} + +func (r *ChainResult) addError(format string, args ...any) { + r.Errors = append(r.Errors, fmt.Sprintf(format, args...)) +} + +// PrintChain writes a human-readable chain verification report. +func (r *ChainResult) PrintChain(w io.Writer) { + fmt.Fprintf(w, "Checkpoint Lineage Verifier\n") + fmt.Fprintf(w, "===========================\n\n") + + if r.AgentDID != "" { + fmt.Fprintf(w, "Agent: %s\n", r.AgentDID) + } + if r.AgentPubKeyHex != "" { + fmt.Fprintf(w, "Public Key: %s\n", r.AgentPubKeyHex) + } + fmt.Fprintf(w, "Checkpoints: %d\n", r.Checkpoints) + fmt.Fprintf(w, "Tick Range: %d → %d\n", r.FirstTick, r.LastTick) + + if len(r.Segments) > 0 { + fmt.Fprintf(w, "\nExecution Segments:\n") + for i, seg := range r.Segments { + fmt.Fprintf(w, " [%d] WASM %s... ticks %d→%d (%d checkpoints)\n", + i+1, seg.WASMHashHex[:16], seg.StartTick, seg.EndTick, seg.Count) + } + } + + fmt.Fprintln(w) + if r.ChainValid { + fmt.Fprintf(w, "Lineage: VALID (all %d signatures verified, chain unbroken)\n", r.Checkpoints) + } else { + fmt.Fprintf(w, "Lineage: INVALID (%d errors)\n", len(r.Errors)) + for _, e := range r.Errors { + fmt.Fprintf(w, " - %s\n", e) + } + } +} diff --git a/internal/storage/fs_provider.go b/internal/storage/fs_provider.go index ebea709..d077a3c 100644 --- a/internal/storage/fs_provider.go +++ b/internal/storage/fs_provider.go @@ -4,6 +4,7 @@ package storage import ( "context" + "encoding/binary" "fmt" "log/slog" "os" @@ -108,6 +109,9 @@ func (p *FSProvider) SaveCheckpoint( "size_bytes", len(state), ) + // Archive to history for lineage verification. + p.archiveCheckpoint(agentID, state) + return nil } @@ -356,6 +360,39 @@ func (p *FSProvider) DeleteIdentity( return nil } +// archiveCheckpoint copies a checkpoint to the history directory for lineage verification. +// Failures are logged but do not affect the main checkpoint save. +func (p *FSProvider) archiveCheckpoint(agentID string, state []byte) { + // Extract tick number from checkpoint header (bytes 17:25, little-endian uint64). + // Minimum checkpoint size is 57 bytes (v2 header). + if len(state) < 25 { + return + } + tickNumber := binary.LittleEndian.Uint64(state[17:25]) + + histDir := filepath.Join(p.baseDir, "history", agentID) + if err := os.MkdirAll(histDir, 0755); err != nil { + p.logger.Warn("Failed to create history directory", "error", err) + return + } + + histPath := filepath.Join(histDir, fmt.Sprintf("%010d.ckpt", tickNumber)) + + // Skip if this tick's checkpoint already archived. + if _, err := os.Stat(histPath); err == nil { + return + } + + if err := os.WriteFile(histPath, state, 0644); err != nil { + p.logger.Warn("Failed to archive checkpoint", "tick", tickNumber, "error", err) + } +} + +// HistoryDir returns the path to an agent's checkpoint history directory. +func (p *FSProvider) HistoryDir(agentID string) string { + return filepath.Join(p.baseDir, "history", agentID) +} + // receiptPath returns the filesystem path for an agent's receipts. func (p *FSProvider) receiptPath(agentID string) (string, error) { path := filepath.Join(p.baseDir, agentID+".receipts") diff --git a/pkg/identity/did.go b/pkg/identity/did.go new file mode 100644 index 0000000..9ce2d16 --- /dev/null +++ b/pkg/identity/did.go @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "fmt" + "math/big" +) + +// base58btc alphabet used by did:key encoding. +const base58Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + +// DID returns the agent's decentralized identifier as a did:key string. +// Encoding: did:key:z + base58btc(multicodec_ed25519_pub ++ raw_public_key). +// The multicodec prefix for Ed25519 public keys is 0xed01 (varint-encoded). +// See: https://w3c-ccg.github.io/did-method-key/ +func (id *AgentIdentity) DID() string { + // multicodec prefix for ed25519-pub: 0xed, 0x01 + buf := make([]byte, 0, 2+len(id.PublicKey)) + buf = append(buf, 0xed, 0x01) + buf = append(buf, id.PublicKey...) + return "did:key:z" + base58Encode(buf) +} + +// DIDShort returns a truncated DID for display: did:key:z6Mk...Xf3q +func (id *AgentIdentity) DIDShort() string { + did := id.DID() + if len(did) <= 24 { + return did + } + return did[:16] + "..." + did[len(did)-4:] +} + +// ParseDID extracts the raw Ed25519 public key from a did:key string. +// Returns an error if the DID is malformed or uses a different key type. +func ParseDID(did string) ([]byte, error) { + const prefix = "did:key:z" + if len(did) <= len(prefix) { + return nil, fmt.Errorf("invalid did:key: too short") + } + if did[:len(prefix)] != prefix { + return nil, fmt.Errorf("invalid did:key: missing 'did:key:z' prefix") + } + decoded, err := base58Decode(did[len(prefix):]) + if err != nil { + return nil, fmt.Errorf("invalid did:key: base58 decode: %w", err) + } + if len(decoded) < 2 { + return nil, fmt.Errorf("invalid did:key: decoded too short") + } + if decoded[0] != 0xed || decoded[1] != 0x01 { + return nil, fmt.Errorf("invalid did:key: not an Ed25519 key (got multicodec 0x%02x%02x)", decoded[0], decoded[1]) + } + pubKey := decoded[2:] + if len(pubKey) != 32 { + return nil, fmt.Errorf("invalid did:key: Ed25519 key must be 32 bytes, got %d", len(pubKey)) + } + return pubKey, nil +} + +// base58Encode encodes bytes to base58btc string. +func base58Encode(data []byte) string { + x := new(big.Int).SetBytes(data) + base := big.NewInt(58) + zero := big.NewInt(0) + mod := new(big.Int) + + var result []byte + for x.Cmp(zero) > 0 { + x.DivMod(x, base, mod) + result = append(result, base58Alphabet[mod.Int64()]) + } + + // Leading zero bytes → leading '1's + for _, b := range data { + if b != 0 { + break + } + result = append(result, base58Alphabet[0]) + } + + // Reverse + for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 { + result[i], result[j] = result[j], result[i] + } + + return string(result) +} + +// base58Decode decodes a base58btc string to bytes. +func base58Decode(s string) ([]byte, error) { + result := big.NewInt(0) + base := big.NewInt(58) + + for _, c := range s { + idx := -1 + for i, a := range base58Alphabet { + if a == c { + idx = i + break + } + } + if idx < 0 { + return nil, fmt.Errorf("invalid base58 character: %c", c) + } + result.Mul(result, base) + result.Add(result, big.NewInt(int64(idx))) + } + + decoded := result.Bytes() + + // Restore leading zeros + numLeadingZeros := 0 + for _, c := range s { + if c != rune(base58Alphabet[0]) { + break + } + numLeadingZeros++ + } + if numLeadingZeros > 0 { + decoded = append(make([]byte, numLeadingZeros), decoded...) + } + + return decoded, nil +} diff --git a/pkg/identity/did_test.go b/pkg/identity/did_test.go new file mode 100644 index 0000000..ae59293 --- /dev/null +++ b/pkg/identity/did_test.go @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "crypto/ed25519" + "strings" + "testing" +) + +func TestDID_Format(t *testing.T) { + id, err := Generate() + if err != nil { + t.Fatalf("Generate() error: %v", err) + } + did := id.DID() + if !strings.HasPrefix(did, "did:key:z6Mk") { + t.Fatalf("DID should start with 'did:key:z6Mk', got: %s", did) + } +} + +func TestDID_RoundTrip(t *testing.T) { + id, err := Generate() + if err != nil { + t.Fatalf("Generate() error: %v", err) + } + did := id.DID() + pubKey, err := ParseDID(did) + if err != nil { + t.Fatalf("ParseDID() error: %v", err) + } + if !ed25519.PublicKey(pubKey).Equal(id.PublicKey) { + t.Fatal("public key mismatch after DID round-trip") + } +} + +func TestDID_Deterministic(t *testing.T) { + id, err := Generate() + if err != nil { + t.Fatalf("Generate() error: %v", err) + } + did1 := id.DID() + did2 := id.DID() + if did1 != did2 { + t.Fatalf("DID not deterministic: %s != %s", did1, did2) + } +} + +func TestDID_UniquePerIdentity(t *testing.T) { + id1, _ := Generate() + id2, _ := Generate() + if id1.DID() == id2.DID() { + t.Fatal("two different identities produced the same DID") + } +} + +func TestDIDShort(t *testing.T) { + id, err := Generate() + if err != nil { + t.Fatalf("Generate() error: %v", err) + } + short := id.DIDShort() + if !strings.HasPrefix(short, "did:key:z6Mk") { + t.Fatalf("DIDShort should start with 'did:key:z6Mk', got: %s", short) + } + if !strings.Contains(short, "...") { + t.Fatalf("DIDShort should contain '...', got: %s", short) + } + if len(short) > 24 { + t.Fatalf("DIDShort too long: %d chars: %s", len(short), short) + } +} + +func TestParseDID_InvalidPrefix(t *testing.T) { + _, err := ParseDID("did:web:example.com") + if err == nil { + t.Fatal("expected error for non-did:key DID") + } +} + +func TestParseDID_TooShort(t *testing.T) { + _, err := ParseDID("did:key:z") + if err == nil { + t.Fatal("expected error for too-short DID") + } +} + +func TestParseDID_InvalidBase58(t *testing.T) { + _, err := ParseDID("did:key:z0OOO") // 0 and O are not in base58 + if err == nil { + t.Fatal("expected error for invalid base58 characters") + } +} + +func TestBase58_RoundTrip(t *testing.T) { + testCases := [][]byte{ + {0x00}, + {0x00, 0x00, 0x01}, + {0xed, 0x01, 0xff, 0xaa, 0xbb}, + make([]byte, 32), + } + for _, data := range testCases { + encoded := base58Encode(data) + decoded, err := base58Decode(encoded) + if err != nil { + t.Fatalf("base58Decode(%q) error: %v", encoded, err) + } + if len(decoded) != len(data) { + t.Fatalf("length mismatch: got %d, want %d", len(decoded), len(data)) + } + for i := range data { + if decoded[i] != data[i] { + t.Fatalf("byte %d mismatch: got 0x%02x, want 0x%02x", i, decoded[i], data[i]) + } + } + } +} diff --git a/scripts/demo-portable.sh b/scripts/demo-portable.sh new file mode 100755 index 0000000..9de198c --- /dev/null +++ b/scripts/demo-portable.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# demo-portable.sh — Demonstrate Igor's portable, immortal agent. +# +# Flow: +# 1. Run heartbeat agent on "Machine A" (local dir A) +# 2. Let it tick for a few seconds, then stop it +# 3. Copy checkpoint to "Machine B" (local dir B) +# 4. Resume from checkpoint — same DID, continuous tick count +# 5. Verify cryptographic lineage across both "machines" +set -euo pipefail + +IGORD="./bin/igord" +WASM="./agents/heartbeat/agent.wasm" +DIR_A="/tmp/igor-demo-machine-a" +DIR_B="/tmp/igor-demo-machine-b" + +# Cleanup from previous runs. +rm -rf "$DIR_A" "$DIR_B" +mkdir -p "$DIR_A" "$DIR_B" + +echo "" +echo "==========================================" +echo " Igor: Portable Immortal Agent Demo" +echo "==========================================" +echo "" + +# --- Machine A --- +echo "[Machine A] Starting heartbeat agent..." +$IGORD run --budget 100.0 --checkpoint-dir "$DIR_A" --agent-id heartbeat "$WASM" & +PID_A=$! + +# Let it tick for 8 seconds. +sleep 8 + +echo "" +echo "[Machine A] Stopping agent (sending SIGINT)..." +kill -INT "$PID_A" 2>/dev/null || true +wait "$PID_A" 2>/dev/null || true + +echo "" +echo "[Machine A] Checkpoint saved. Contents:" +ls -la "$DIR_A/" +echo "" + +# --- Copy to Machine B --- +CKPT_FILE="$DIR_A/heartbeat.checkpoint" +if [ ! -f "$CKPT_FILE" ]; then + echo "ERROR: Checkpoint file not found at $CKPT_FILE" + exit 1 +fi + +echo "[Transfer] Copying checkpoint to Machine B..." +cp "$CKPT_FILE" "$DIR_B/" +# Also copy identity so the agent keeps its DID. +if [ -f "$DIR_A/heartbeat.identity" ]; then + cp "$DIR_A/heartbeat.identity" "$DIR_B/" +fi +echo "" + +# --- Machine B --- +echo "[Machine B] Resuming agent from checkpoint..." +$IGORD resume --checkpoint "$DIR_B/heartbeat.checkpoint" --wasm "$WASM" --checkpoint-dir "$DIR_B" --agent-id heartbeat & +PID_B=$! + +# Let it tick for 6 seconds. +sleep 6 + +echo "" +echo "[Machine B] Stopping agent..." +kill -INT "$PID_B" 2>/dev/null || true +wait "$PID_B" 2>/dev/null || true + +echo "" + +# --- Verify Lineage --- +# Merge history from both machines for full chain verification. +VERIFY_DIR="/tmp/igor-demo-verify" +rm -rf "$VERIFY_DIR" +mkdir -p "$VERIFY_DIR" + +if [ -d "$DIR_A/history/heartbeat" ]; then + cp "$DIR_A/history/heartbeat/"*.ckpt "$VERIFY_DIR/" 2>/dev/null || true +fi +if [ -d "$DIR_B/history/heartbeat" ]; then + cp "$DIR_B/history/heartbeat/"*.ckpt "$VERIFY_DIR/" 2>/dev/null || true +fi + +CKPT_COUNT=$(ls "$VERIFY_DIR/"*.ckpt 2>/dev/null | wc -l | tr -d ' ') +echo "==========================================" +echo " Lineage Verification ($CKPT_COUNT checkpoints)" +echo "==========================================" +echo "" + +if [ "$CKPT_COUNT" -gt 0 ]; then + $IGORD verify "$VERIFY_DIR" +else + echo "No checkpoint history found for verification." + echo "(Agent may not have run long enough to produce history.)" +fi + +echo "" +echo "==========================================" +echo " Demo Complete" +echo "==========================================" + +# Cleanup. +rm -rf "$DIR_A" "$DIR_B" "$VERIFY_DIR" From 546971926a0748a305d6ac2ad3537f71d3d78746 Mon Sep 17 00:00:00 2001 From: Janko Date: Fri, 13 Mar 2026 14:22:17 +0100 Subject: [PATCH 2/2] docs(runtime): full docs refresh for Product Phase 1 pivot Reframe Igor from decentralized P2P research runtime to portable immortal agent runtime. Update README (new pitch, architecture diagram, quick start with subcommands), CLAUDE.md (new packages, CLI, DID), ROADMAP (product phases), IMPLEMENTATION_STATUS (Phase 1 table). Research foundation (Phases 2-5) preserved as history. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 41 +++++-- README.md | 203 +++++++++++++-------------------- docs/IMPLEMENTATION_STATUS.md | 22 +++- docs/governance/ROADMAP.md | 206 ++++++++++++++++------------------ 4 files changed, 228 insertions(+), 244 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e0c46f6..57c7d08 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## What is Igor -Igor is a decentralized runtime for autonomous, survivable software agents. Agents are WASM binaries that checkpoint their state, migrate between peer nodes over libp2p, and pay for execution from internal budgets. Phases 2–4 are complete, Phase 5 (Hardening) is complete — agents run, checkpoint, migrate, resume, meter cost, enforce capability membranes, replay-verify, support multi-node chain migration, sign checkpoint lineage, recover from migration failures, and enforce lease-based authority. Task 15 (Permissionless Hardening) is next. Research-stage, not production-ready. +Igor is the runtime for portable, immortal software agents. The checkpoint file IS the agent — copy it anywhere, run `igord resume`, it continues exactly where it left off. Every agent has a DID identity (`did:key:z6Mk...`) derived from its Ed25519 keypair, and a signed checkpoint lineage providing cryptographic proof of its entire life history. No infrastructure lock-in. + +**Product Phase 1 (Portable Sovereign Agent)** is complete: DID identity, `igord run/resume/verify/inspect` subcommands, checkpoint history archival, lineage chain verification, heartbeat demo agent, portable demo script. Built on a research foundation (Phases 2–5) of WASM sandboxing, P2P migration, budget metering, replay verification, and signed checkpoint lineage. **Stack:** Go 1.25 · wazero (pure Go WASM, no CGo) · libp2p-go · TinyGo (agent compilation) @@ -14,6 +16,7 @@ Igor is a decentralized runtime for autonomous, survivable software agents. Agen make bootstrap # Install toolchain (Go, golangci-lint, goimports, TinyGo) make build # Build igord → bin/igord make agent # Build example WASM agent → agents/example/agent.wasm +make agent-heartbeat # Build heartbeat WASM agent → agents/heartbeat/agent.wasm make test # Run tests: go test -v ./... make lint # golangci-lint (5m timeout) make vet # go vet @@ -21,15 +24,23 @@ make fmt # gofmt + goimports make check # fmt-check + vet + lint + test (same as precommit) make run-agent # Build + run example agent with budget 1.0 make demo # Build + run bridge reconciliation demo +make demo-portable # Build + run portable agent demo (run → stop → copy → resume → verify) make clean # Remove bin/, checkpoints/, agent.wasm ``` Run a single test: `go test -v -run TestName ./internal/agent/...` -Run the node manually: +Run manually (new subcommands): +```bash +./bin/igord run --budget 1.0 agents/heartbeat/agent.wasm +./bin/igord resume checkpoints/heartbeat/checkpoint.ckpt agents/heartbeat/agent.wasm +./bin/igord verify checkpoints/heartbeat/history/ +./bin/igord inspect checkpoints/heartbeat/checkpoint.ckpt +``` + +Legacy mode (P2P/migration): ```bash ./bin/igord --run-agent agents/example/agent.wasm --budget 10.0 -./bin/igord --run-agent agents/example/agent.wasm --budget 10.0 --manifest agents/example/agent.manifest.json ./bin/igord --migrate-agent local-agent --to /ip4/127.0.0.1/tcp/4002/p2p/ --wasm agent.wasm ``` @@ -43,34 +54,44 @@ Current version is v0x04 (209-byte header). Supports reading v0x02 (57 bytes) an `[version: 1 byte (0x04)][budget: 8 bytes int64 microcents][pricePerSecond: 8 bytes int64 microcents][tickNumber: 8 bytes uint64][wasmHash: 32 bytes SHA-256][majorVersion: 8 bytes uint64][leaseGeneration: 8 bytes uint64][leaseExpiry: 8 bytes uint64][prevHash: 32 bytes SHA-256][agentPubKey: 32 bytes Ed25519][signature: 64 bytes Ed25519][agent state: N bytes]` -Header is 209 bytes. Budget uses int64 microcents (1 currency unit = 1,000,000 microcents). WASM hash binds the checkpoint to the binary that created it; mismatch on resume is rejected. prevHash chains checkpoints into a tamper-evident lineage. Signature covers everything except the signature field itself. +Header is 209 bytes. Budget uses int64 microcents (1 currency unit = 1,000,000 microcents). WASM hash binds the checkpoint to the binary that created it; mismatch on resume is rejected. prevHash chains checkpoints into a tamper-evident lineage. Signature covers everything except the signature field itself. AgentPubKey encodes as DID: `did:key:z` + base58btc(0xed01 + pubkey). -Atomic writes via temp file → fsync → rename. +Atomic writes via temp file → fsync → rename. Every checkpoint is also archived to `history/{agentID}/{tickNumber}.ckpt` for lineage verification. ### Key packages -- `cmd/igord/` — CLI entry point, flag parsing, tick loop orchestration +- `cmd/igord/` — CLI entry point, subcommand dispatch (`run`, `resume`, `verify`, `inspect`), tick loop - `internal/agent/` — Agent lifecycle: load WASM, init, tick, checkpoint, resume, budget deduction - `internal/runtime/` — wazero sandbox: 64MB memory limit, WASI with fs/net disabled - `internal/hostcall/` — `igor` host module: clock, rand, log, wallet hostcall implementations +- `internal/inspector/` — Checkpoint inspection and lineage chain verification (`chain.go`: `VerifyChain`) +- `internal/storage/` — `CheckpointProvider` interface + filesystem impl + checkpoint history archival - `internal/eventlog/` — Per-tick observation event log for deterministic replay - `internal/replay/` — Deterministic replay verification: single-tick (`ReplayTick`) and chain (`ReplayChain`) - `internal/runner/` — Tick loop orchestration, divergence escalation policies, lease management - `internal/authority/` — Lease-based authority epochs, state machine (Active→Expired→RecoveryRequired) - `internal/migration/` — P2P migration over libp2p stream protocol `/igor/migrate/1.0.0`, retry with backoff - `internal/registry/` — Peer registry with health tracking for migration target selection -- `internal/storage/` — `CheckpointProvider` interface + filesystem implementation - `internal/p2p/` — libp2p host setup, bootstrap peers, protocol handlers +- `pkg/identity/` — Agent Ed25519 keypair management, DID encoding (`did:key:z6Mk...`), DID parsing +- `pkg/lineage/` — Signed checkpoint types, content hashing, signature verification - `pkg/manifest/` — Capability manifest parsing and validation - `pkg/protocol/` — Message types: `AgentPackage`, `AgentTransfer`, `AgentStarted` - `pkg/receipt/` — Payment receipt data structure, Ed25519 signing, binary serialization -- `pkg/identity/` — Agent Ed25519 keypair management for signed checkpoint lineage -- `pkg/lineage/` — Signed checkpoint types, content hashing, signature verification - `sdk/igor/` — Agent SDK: hostcall wrappers (ClockNow, RandBytes, Log, WalletBalance), lifecycle plumbing (Agent interface), Encoder/Decoder with Raw/FixedBytes/ReadInto for checkpoint serialization +- `agents/heartbeat/` — Demo agent: logs heartbeat with tick count and age, milestones every 10 ticks +- `agents/example/` — Original demo agent (Survivor) from research phases +- `scripts/demo-portable.sh` — End-to-end portable agent demo ### Migration flow Source checkpoints → packages (WASM + checkpoint + budget) → transfers over libp2p → target instantiates + resumes → target confirms → source terminates + deletes local checkpoint. Single-instance invariant maintained throughout. Failures classified as retriable/fatal/ambiguous; ambiguous transfers enter RECOVERY_REQUIRED state (EI-6). Retry with exponential backoff; peer registry tracks health for target selection. -### Key CLI flags +### CLI subcommands (Product Phase 1) +- `igord run [flags] ` — run agent with new identity (`--budget`, `--checkpoint-dir`, `--agent-id`) +- `igord resume ` — resume agent from checkpoint file +- `igord verify ` — verify checkpoint lineage chain +- `igord inspect ` — display checkpoint details with DID identity + +### Legacy CLI flags (research/P2P mode) - `--replay-mode off|periodic|on-migrate|full` — when to run replay verification (default: full) - `--replay-on-divergence log|pause|intensify|migrate` — escalation policy on divergence (default: log) - `--verify-interval N` — ticks between verification passes (default: 5) diff --git a/README.md b/README.md index be476ae..5924009 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ # Igor -**Runtime for Survivable Autonomous Agents** +**Runtime for Portable, Immortal Software Agents** -Igor is a decentralized execution runtime for autonomous software agents. It provides infrastructure primitives enabling agents to checkpoint state, migrate between peer nodes over libp2p, and pay for execution using internal budgets. Agents built on Igor persist independently of any infrastructure provider through WASM sandbox execution, peer-to-peer migration protocols, and runtime economics. +Igor makes any WASM program into a sovereign agent with its own identity, memory, and verifiable life history. The checkpoint file IS the agent — copy it anywhere, run `igord resume`, it continues exactly where it left off. No infrastructure lock-in. --- ## About This Repository -**What:** Experimental infrastructure for autonomous agent survival -**Status:** Research-stage — Phases 2–4 complete, Phase 5 (Hardening) complete. Agents run, checkpoint, migrate, resume, meter cost, enforce capability membranes, replay-verify, support multi-node chain migration, sign checkpoint lineage, recover from migration failures, and enforce lease-based authority. Task 15 (Permissionless Hardening) next. -**Purpose:** Demonstrate that software can checkpoint, migrate, and self-fund execution +**What:** Runtime for portable, infrastructure-independent agents +**Status:** Product Phase 1 complete. Agents have DID identity, checkpoint/resume across machines, and cryptographic lineage verification. Built on a research foundation (Phases 2–5) of WASM sandboxing, P2P migration, budget metering, replay verification, and signed checkpoint lineage. +**Purpose:** Give software agents identity, memory, and continuity — independent of any machine, cloud, or operator **Read first:** - [ANNOUNCEMENT.md](./ANNOUNCEMENT.md) - Public project introduction @@ -25,11 +25,11 @@ Igor is a decentralized execution runtime for autonomous software agents. It pro ## Why Igor Exists -Autonomous economic software—including DeFi automation, oracle networks, and AI service agents—can execute decisions independently but cannot survive infrastructure failure autonomously. These distributed systems hold capital, operate continuously, yet remain existentially tied to specific infrastructure. +Agents today are tied to their infrastructure. Kill the server, the agent dies. Restart it, and it has to start from scratch — losing in-memory state, execution history, and continuity. -Trading strategies manage capital without human approval yet stop when servers fail. Oracle participants must maintain continuous uptime yet depend entirely on operators. AI service agents process requests autonomously yet cannot survive the loss of their hosting infrastructure. +Kubernetes restarts processes but loses state. Temporal forces you into a workflow programming model. AO replays entire message histories from Arweave. An LLM with a wallet can rent a server but can't survive dying on it. -Igor provides survivable agent runtime primitives: explicit state checkpointing, peer-to-peer agent migration, and runtime economic metering. Agents persist across distributed infrastructure changes. Infrastructure becomes fungible; agents become persistent. +Igor gives agents three things nothing else provides together: **identity** (DID), **memory** (checkpointed state that survives infrastructure failure), and **verifiable continuity** (cryptographic proof of the agent's entire life history). The agent is a portable digital object — not a deployment tied to specific infrastructure. ## Technical Domains @@ -60,12 +60,12 @@ Igor provides execution survival primitives that these higher-level systems coul ## Core Guarantees -- **Survival:** Agents checkpoint state and resume after node failure -- **Migration:** Agents transfer between nodes over libp2p streams -- **Single-instance:** At most one active instance exists (no split-brain) -- **Budget enforcement:** Execution metered per-tick, cost deducted automatically +- **Portable:** The checkpoint file IS the agent — copy it anywhere, resume it +- **Identity:** Every agent has a DID (`did:key:z6Mk...`) derived from its Ed25519 keypair +- **Verifiable:** Signed checkpoint lineage — cryptographic proof of entire life history +- **Survival:** Agents checkpoint state and resume after infrastructure failure - **Sandboxing:** WASM isolation, 64MB memory limit, no filesystem/network access -- **Decentralization:** Peer-to-peer coordination, no central authority +- **Migration:** Agents transfer between nodes over libp2p streams (research foundation) ## What Igor Does Not Provide @@ -77,33 +77,30 @@ Igor is **not**: - A multi-agent coordination platform - A general-purpose orchestration system -Igor is a minimal survival runtime. It implements checkpointing, migration, and budget metering. Nothing more. +Igor is a minimal runtime for portable, sovereign agents. It provides identity, checkpointing, and verifiable continuity. Nothing more. ## Architecture Overview ``` ┌─────────────────────────────────────────────────┐ -│ Autonomous Agent (WASM) │ +│ Agent (WASM binary) │ │ ┌──────────┐ ┌───────────┐ ┌──────────────┐ │ │ │ Init │→ │ Tick Loop │→ │ Checkpoint │ │ -│ └──────────┘ └─────┬─────┘ └──────┬───────┘ │ -│ │ │ │ -│ ↓ ↓ │ -│ ┌──────────────┐ ┌─────────────┐ │ -│ │ Budget │ │ State │ │ -│ │ Metering │ │ Persistence │ │ -│ └──────────────┘ └─────────────┘ │ -└─────────────────────────────────────────────────┘ - │ - │ Migration (libp2p) - ↓ -┌─────────────────────────────────────────────────┐ -│ Igor Runtime (Node) │ -│ ┌──────────┐ ┌───────────┐ ┌──────────────┐ │ -│ │ WASM │ │ Migration │ │ P2P │ │ -│ │ Sandbox │ │ Protocol │ │ Network │ │ │ └──────────┘ └───────────┘ └──────────────┘ │ └─────────────────────────────────────────────────┘ + │ │ + ↓ ↓ +┌─────────────────┐ ┌────────────────────────────┐ +│ Igor Runtime │ │ Checkpoint File (.ckpt) │ +│ (igord) │ │ ┌──────────────────────┐ │ +│ ┌───────────┐ │ │ │ Identity (Ed25519) │ │ +│ │ WASM │ │ │ │ State + Budget │ │ +│ │ Sandbox │ │ │ │ Signed Lineage │ │ +│ └───────────┘ │ │ │ WASM Hash Binding │ │ +│ ┌───────────┐ │ │ └──────────────────────┘ │ +│ │ Hostcalls │ │ │ ↕ Copy to any machine │ +│ └───────────┘ │ │ ↕ igord resume → continues │ +└─────────────────┘ └────────────────────────────┘ ``` ### Agents @@ -118,94 +115,49 @@ WASM executables implementing five lifecycle functions: Agents interact with the runtime through the `igor` host module (clock, rand, log hostcalls), mediated by a capability manifest declared at load time. -Agents carry budgets. Cost calculated per tick: `duration_seconds × price_per_second` - -### Nodes - -Peer-to-peer participants providing execution services: - -- Execute agents in wazero WASM sandbox -- Meter resource consumption per tick -- Charge agents per second of execution -- Facilitate migration via libp2p streams - -Nodes operate autonomously without coordination. - ### Checkpoints -Atomic snapshots preserving agent state, budget, and binary identity (209-byte header): - -``` -Offset Size Field -0 1 Version (0x04) -1 8 Budget (int64 microcents, little-endian) -9 8 PricePerSecond (int64 microcents, little-endian) -17 8 TickNumber (uint64, little-endian) -25 32 WASMHash (SHA-256 of agent binary) -57 8 MajorVersion (uint64, little-endian) -65 8 LeaseGeneration (uint64, little-endian) -73 8 LeaseExpiry (int64, little-endian) -81 32 PrevHash (SHA-256 of previous checkpoint) -113 32 AgentPubKey (Ed25519 public key) -145 64 Signature (Ed25519, covers bytes 0–144) -209 N Agent State (application-defined) -``` - -Budget unit: 1 currency unit = 1,000,000 microcents. Integer arithmetic avoids float precision drift. - -### Migration Protocol - -Agents migrate via direct peer-to-peer streams: +The checkpoint file IS the agent. It contains everything needed to resume: -1. Source checkpoints agent -2. Source packages: WASM + checkpoint + budget + manifest + WASM hash -3. Source includes replay verification data (if available) -4. Source transfers to target (libp2p stream) -5. Target verifies WASM hash integrity -6. Target replays last tick to verify checkpoint (if replay data present) -7. Target resumes agent -8. Target confirms success -9. Source terminates local instance +- **Identity:** Ed25519 public key → DID (`did:key:z6Mk...`) +- **State:** Application-defined agent memory +- **Budget:** Remaining execution budget in microcents +- **Lineage:** PrevHash chain + Ed25519 signature for tamper-evident history +- **Binding:** SHA-256 hash of the WASM binary that created it -Single-instance invariant maintained throughout. +Every checkpoint is archived to `history/{agentID}/{tickNumber}.ckpt` for full lineage verification. ## Current Capabilities -**Phase 2 (Survival) - Complete** +**Product Phase 1 (Portable Sovereign Agent) - Complete** -All 6 success criteria met: +- Agent runs with DID identity (`did:key:z6Mk...`) +- Agent checkpoints and resumes on any machine — same DID, continuous tick count +- Signed checkpoint lineage — cryptographic proof of entire life history +- Checkpoint history archival for lineage verification +- CLI subcommands: `igord run`, `resume`, `verify`, `inspect` -1. ✓ Agent runs on Node A -2. ✓ Agent checkpoints state explicitly -3. ✓ Agent migrates to Node B -4. ✓ Agent resumes from checkpoint -5. ✓ Agent pays runtime rent -6. ✓ No centralized coordination +**Research Foundation (Phases 2–5) - Complete** -## Project Status: Experimental +- WASM sandboxing, P2P migration, budget metering, replay verification +- Capability membranes, lease-based authority, signed lineage, migration failure recovery -**Maturity:** Research-stage, proof-of-concept (Phase 3 complete, Phase 4 next) -**Production:** Not ready for production use -**Security:** WASM hash identity binding; no full cryptographic attestation yet +## Project Status + +**Maturity:** Product Phase 1 complete. Built on research foundation (Phases 2–5). +**Production:** Not yet production-ready — early product stage. +**Security:** Ed25519 signed checkpoint lineage with DID identity. **Known limitations:** -- Trusted runtime accounting (no payment receipts) -- Chain migration tested (A→B→C→A) but no routing protocol -- Local filesystem storage (no distribution) -- No agent discovery protocol +- Local filesystem storage only (no permanent archival yet) +- No HTTP or payment hostcalls (agents can't call external APIs yet) +- No self-provisioning (agents can't deploy themselves yet) - Minimal security hardening -**Do not use for:** -- Production workloads -- Public network deployments -- Sensitive data -- Financial transactions - **Suitable for:** -- Research and experimentation -- Trusted network environments -- Understanding autonomous agent patterns -- Academic exploration +- Building and running portable agents +- Experimenting with agent identity and continuity +- Understanding infrastructure-independent agent patterns See [SECURITY.md](./SECURITY.md) for complete security model. @@ -220,30 +172,35 @@ See [SECURITY.md](./SECURITY.md) for complete security model. ### Build and Run ```bash -# Build node runtime +# Build runtime and heartbeat agent make build +make agent-heartbeat + +# Run agent (creates identity, starts ticking) +./bin/igord run --budget 1.0 agents/heartbeat/agent.wasm +# [heartbeat] tick=1 age=1s +# [heartbeat] tick=2 age=2s +# Ctrl+C → checkpoint saved + +# Resume on same or different machine +./bin/igord resume checkpoints/heartbeat/checkpoint.ckpt agents/heartbeat/agent.wasm +# [heartbeat] tick=3 age=3s ← continues where it left off -# Build example agent -make agent +# Verify the agent's entire life history +./bin/igord verify checkpoints/heartbeat/history/ -# Run agent with budget -./bin/igord --run-agent agents/example/agent.wasm --budget 10.0 +# Inspect a checkpoint +./bin/igord inspect checkpoints/heartbeat/checkpoint.ckpt ``` -### Survival Demonstration +### Portable Agent Demo ```bash -# First run -./bin/igord --run-agent agents/example/agent.wasm --budget 1.0 -# Agent ticks: 1, 2, 3, 4, 5... -# Ctrl+C (checkpoint saved to ./checkpoints/) - -# Second run (restart) -./bin/igord --run-agent agents/example/agent.wasm --budget 1.0 -# Agent resumes: 6, 7, 8, 9... +# Full demo: run → stop → copy → resume → verify +make demo-portable ``` -Agent state survives restart via checkpoint. +The demo shows an agent running on "Machine A", checkpoint copied to "Machine B", resuming with the same DID identity and continuous tick count, then verifying the cryptographic lineage across both machines. ## Specification Overview @@ -321,16 +278,16 @@ Security issues: [SECURITY.md](./SECURITY.md) ## Discovery Keywords **Core Identity:** -Autonomous agent runtime | Survivable distributed systems | WASM agent execution | Peer-to-peer agent migration | Runtime economics | Decentralized execution infrastructure +Portable agent runtime | Immortal software agents | Agent identity (did:key) | Sovereign agents | Infrastructure-independent agents | WASM agent execution | Verifiable agent continuity **Technical Stack:** -WebAssembly sandbox | wazero runtime | libp2p networking | Go distributed systems | Agent checkpoint persistence | Budget metering infrastructure +WebAssembly sandbox | wazero runtime | Ed25519 signed lineage | DID identity | Go distributed systems | Agent checkpoint persistence | TinyGo WASM agents **Use Cases:** -DeFi automation infrastructure | Oracle network runtime | AI agent execution platform | Economic software agents | Autonomous service infrastructure | Distributed compute mobility +Long-running autonomous agents | Self-provisioning compute | AI agent execution platform | Portable stateful agents | Agent survival across infrastructure | Decentralized agent deployment -**Research Areas:** -Agent survival primitives | Mobile code execution | Process migration protocols | Runtime accounting systems | Survivable software research | Experimental distributed infrastructure +**Research Foundation:** +Agent survival primitives | Mobile code execution | Process migration protocols | P2P agent migration | Runtime accounting systems | Survivable software research See [docs/governance/KEYWORDS.md](./docs/governance/KEYWORDS.md) for keyword governance policy. diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index 69221ec..935ff13 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -2,7 +2,24 @@ Truth matrix mapping spec documents to current code. Source of truth is the code; docs have been updated to match. -Last updated: 2026-03-05 +Last updated: 2026-03-13 + +## Portable Agent (Product Phase 1) + +| Aspect | Status | Code Reference | +|--------|--------|----------------| +| DID identity encoding (`did:key:z6Mk...`) | Implemented | `pkg/identity/did.go` `DID()` | +| DID round-trip parsing | Implemented | `pkg/identity/did.go` `ParseDID()` | +| DID short form | Implemented | `pkg/identity/did.go` `DIDShort()` | +| CLI `run` subcommand | Implemented | `cmd/igord/main.go` `subcmdRun` | +| CLI `resume` subcommand | Implemented | `cmd/igord/main.go` `subcmdResume` | +| CLI `verify` subcommand | Implemented | `cmd/igord/main.go` `subcmdVerify` | +| CLI `inspect` subcommand | Implemented | `cmd/igord/main.go` `subcmdInspect` | +| Checkpoint history archival | Implemented | `internal/storage/fs_provider.go` `archiveCheckpoint` | +| History directory accessor | Implemented | `internal/storage/fs_provider.go` `HistoryDir()` | +| Lineage chain verification | Implemented | `internal/inspector/chain.go` `VerifyChain` | +| Heartbeat demo agent | Implemented | `agents/heartbeat/main.go` | +| Portable demo script | Implemented | `scripts/demo-portable.sh` | ## Checkpoint Format @@ -86,6 +103,7 @@ Last updated: 2026-03-05 | Signed checkpoint lineage | Implemented | Task 13: `pkg/lineage/`, checkpoint v0x04 with signed hash chain. See [SIGNED_LINEAGE.md](runtime/SIGNED_LINEAGE.md). | | Lease-based authority epochs | Implemented | Task 12: `internal/authority/`, checkpoint v0x03 with epoch metadata. See [LEASE_EPOCH.md](runtime/LEASE_EPOCH.md). | | Cryptographic agent identity | Implemented | Task 13: `pkg/identity/`, Ed25519 agent keypairs with persistent storage. | +| DID encoding (did:key multicodec) | Implemented | `pkg/identity/did.go`, base58btc with 0xed01 prefix. | ## Migration @@ -121,6 +139,8 @@ Last updated: 2026-03-05 | Checkpoint inspector | Implemented | `internal/inspector/inspector.go` | | WASM hash verification in inspector | Implemented | `internal/inspector/inspector.go` `VerifyWASM` | | Agent template (Survivor example) | Implemented | `agents/example/` | +| Heartbeat demo agent | Implemented | `agents/heartbeat/` — tick count, age, milestones | +| Lineage chain verifier | Implemented | `internal/inspector/chain.go` `VerifyChain`, `PrintChain` | | `--simulate` CLI flag | Implemented | `cmd/igord/main.go` | | `--inspect-checkpoint` CLI flag | Implemented | `cmd/igord/main.go` | diff --git a/docs/governance/ROADMAP.md b/docs/governance/ROADMAP.md index 3eb18d6..ea27101 100644 --- a/docs/governance/ROADMAP.md +++ b/docs/governance/ROADMAP.md @@ -1,8 +1,14 @@ -# Igor v0 Roadmap +# Igor Roadmap -## Current Status: Phase 5 In Progress +## Current Status: Product Phase 1 Complete -Igor v0 has completed **Phase 2 (Survival)**, **Phase 3 (Autonomy)**, **Phase 4 (Economics)**, and started **Phase 5 (Hardening)** with Tasks 12–14. +Igor has completed its research foundation (**Phases 2–5**) and pivoted to product development. The runtime now supports **portable, infrastructure-independent agents** with DID identity, cryptographic lineage verification, and clean CLI workflows (`igord run`, `resume`, `verify`). + +**Product Phase 1 (Portable Sovereign Agent)** is complete. **Product Phase 2 (Agent Self-Provisioning)** is next. + +--- + +## Research Foundation ### Completed Tasks @@ -174,50 +180,76 @@ Igor v0 has completed **Phase 2 (Survival)**, **Phase 3 (Autonomy)**, **Phase 4 **Outcome:** Robust migration under adverse conditions with single-instance invariant preserved. -### Task 15: Permissionless Hardening +### Task 15: Permissionless Hardening (Deprioritized) + +**Status:** Deferred. Deprioritized in favor of product pivot. + +**Scope:** Sybil resistance, host attestation, anti-withholding, cross-node replay. May be revisited when agents self-provision infrastructure (Product Phase 2+). + +--- + +## Product Pivot + +Igor's research foundation (Phases 2–5) proved that agents can checkpoint, migrate, resume, and maintain cryptographic lineage. The pivot focuses on making agents **portable and infrastructure-independent** — the agent is a digital object, not a deployment. + +--- + +## Product Phase 1: Portable Sovereign Agent ✅ + +**Goal:** Make the agent a portable digital object with identity and verifiable history. + +**Status:** Complete. + +**Delivered:** +- ✅ **Task P1** - DID identity encoding (`pkg/identity/did.go`) — agents get `did:key:z6Mk...` identifiers from their Ed25519 keys +- ✅ **Task P2** - CLI subcommands — `igord run`, `igord resume`, `igord verify`, `igord inspect` for clean developer workflow +- ✅ **Task P3** - Checkpoint history archival (`internal/storage/fs_provider.go`) — every checkpoint archived for lineage verification +- ✅ **Task P4** - Lineage chain verifier (`internal/inspector/chain.go`) — walk checkpoint history, verify all signatures and hash chain +- ✅ **Task P5** - Heartbeat demo agent (`agents/heartbeat/`) — visible agent that demonstrates continuity across machines +- ✅ **Task P6** - Portable agent demo (`scripts/demo-portable.sh`) — end-to-end: run, stop, copy, resume on different machine, verify lineage + +**Outcome:** An agent runs on Machine A, gets stopped, its checkpoint is copied to Machine B, and it resumes with the same DID, continuous tick count, and unbroken cryptographic lineage. The checkpoint file IS the agent. + +--- + +## Product Phase 2: Agent Self-Provisioning -**Objective:** Security and incentive mechanisms for untrusted networks. +**Goal:** Agents choose and pay for their own infrastructure. **Scope:** -- Sybil resistance (stake/reputation/cost-to-advertise) -- Host attestation options -- Anti-withholding incentives -- Full replay verification (cross-node) +- **HTTP hostcall** — agent calls external APIs (REST, webhooks) +- **x402/USDC wallet hostcall** — agent pays for services with real money +- **Compute provider hostcall** — agent deploys itself to Akash, Golem, or similar +- **Self-migration** — agent decides when and where to move based on price/performance -**Outcome:** Foundation for permissionless deployment. +**Outcome:** Agents are economically autonomous — they rent infrastructure, pay for it, and move when they choose. --- -## Beyond Phase 5 +## Product Phase 3: Permanent Memory -Potential future directions (not committed): +**Goal:** Agents have tamper-evident, publicly verifiable life histories. -### Advanced Features +**Scope:** +- **Arweave checkpoint archival** — permanent storage tier for checkpoint lineage +- **Two-tier storage** — fast local checkpoints + periodic Arweave archival (async, not in critical path) +- **Content-addressed checkpoints** — anyone can verify an agent's history from its content hash -- **Agent communication** - Peer-to-peer agent messaging -- **Agent composition** - Agents spawning sub-agents -- **Persistent agent storage** - Long-term state persistence -- **Agent marketplaces** - Discovery and matching -- **Reputation systems** - Node and agent ratings -- **Advanced payment rails** - Payment channels, L2s +**Outcome:** An agent's entire execution history is publicly verifiable and permanent. -### Performance Optimizations +--- -- **Multi-agent nodes** - Run multiple agents concurrently -- **Hot migration** - Migrate without stopping ticks -- **Checkpoint compression** - Reduce transfer size -- **Connection pooling** - Reuse peer connections -- **WASM compilation cache** - Faster agent loading +## Product Phase 4: Ecosystem -### Ecosystem Tools +**Goal:** Multi-language support, tooling, and community infrastructure. -- **Node operator tools** - Monitoring dashboards -- **Agent debugger** - Inspect state and execution via replay -- **Migration visualizer** - Track agent movement -- **Economic analytics** - Budget and pricing insights -- **Checkpoint inspector** - Browse lineage, verify signatures +**Scope:** +- **Multi-language SDK** — Rust and AssemblyScript compilation targets (beyond TinyGo) +- **Agent registry** — discover and share agents +- **Supervisor tooling** — optional auto-resurrection across node pools +- **Dashboard** — deploy, monitor, and fund agents via web UI -**Important:** These are speculative. Focus remains on v0 core functionality. +**Outcome:** A developer ecosystem around portable agents. --- @@ -282,38 +314,28 @@ None. v0 is experimental. Things may be: ## Success Metrics -### Phase 2 (Complete) - -- ✅ Agent runs on Node A -- ✅ Agent checkpoints state -- ✅ Agent migrates to Node B -- ✅ Agent resumes from checkpoint -- ✅ Agent pays for execution -- ✅ No centralized coordination - -**All 6 success criteria met.** +### Research Foundation (Phases 2–5) — All Met -### Phase 3 Goals +- ✅ Agent runs, checkpoints, migrates, resumes +- ✅ Budget metering and payment receipts +- ✅ Capability membrane and replay verification +- ✅ Signed checkpoint lineage +- ✅ Lease-based authority and migration failure recovery -- All agent I/O through capability membrane (hostcalls) -- Observation event log enables deterministic replay -- Basic replay verification working -- Agent SDK makes agent authoring accessible -- Capability-aware multi-node migration +### Product Phase 1 (Complete) -### Phase 4 Goals +- ✅ Agent has DID identity (`did:key:z6Mk...`) +- ✅ Agent checkpoints and resumes on a different machine +- ✅ Same DID, continuous tick count across machines +- ✅ Cryptographic lineage verified across machines +- ✅ Clean CLI: `igord run`, `resume`, `verify` -- Cryptographic payment receipts -- Auditable execution costs -- Competitive pricing market -- Economic incentives aligned +### Product Phase 2 Goals -### Phase 5 Goals - -- Production-ready reliability -- Security hardening complete -- Failure recovery robust -- Performance acceptable +- Agent calls external HTTP APIs via hostcall +- Agent pays for compute with real money (x402/USDC) +- Agent deploys itself to Akash/Golem +- Agent decides when to migrate --- @@ -351,28 +373,12 @@ Focus: Validate Phase 2 before expanding scope. ## Long-Term Vision -### Year 1: Proof of Concept - -- Complete Phases 2-3 -- Validate autonomous migration -- Run in research environments -- Publish findings - -### Year 2: Economics - -- Complete Phase 4 -- Add payment infrastructure -- Test with real economic incentives -- Build small ecosystem - -### Year 3: Hardening +Agents will pick their own infrastructure. Igor makes them portable enough to do so. -- Complete Phase 5 -- Security audit -- Production-grade reliability -- Consider v1.0 - -**Highly speculative.** Depends on validation results and community interest. +- **Now:** Agents are portable digital objects with identity and verifiable history +- **Next:** Agents pay for their own compute and self-provision infrastructure +- **Later:** Agents have permanent, publicly verifiable memory on Arweave +- **Eventually:** A multi-language ecosystem of sovereign, immortal agents --- @@ -406,39 +412,19 @@ Igor is **not novel**. It combines existing ideas in a specific way to explore a ## Open Questions -Questions to answer through v0 development: - -1. **Can agents practically migrate fast enough?** -2. **Is budget accounting sufficient without cryptographic proofs?** -3. **Do agents need more host functions?** -4. **Is WASM overhead acceptable?** -5. **Can checkpoint sizes stay small?** -6. **Will nodes actually compete on price?** - -Answers will inform future phases. - ---- - -## Success Criteria for v0 - -Igor v0 is **complete** when all Phase 2 tasks are done and validated. - -Phase 2 is **validated** when: - -- Agents run for days without issues -- Migration works reliably (>95% success) -- Budget accounting is accurate -- No critical bugs remain -- Documentation is comprehensive - -**Status: Phase 2 validated. Phase 5 in progress.** +1. **Can agents practically self-provision infrastructure?** (Akash/Golem integration complexity) +2. **Is the checkpoint format efficient enough for large agent state?** (MB+ state sizes) +3. **Will developers adopt the Igor SDK over simpler alternatives?** (DX matters) +4. **Is WASM overhead acceptable for latency-sensitive agents?** (not for HFT, but for long-running?) +5. **Can two-tier storage (local + Arweave) work without slowing the critical path?** +6. **What hostcalls do agents actually need?** (HTTP, storage, payments — what else?) --- ## Next Immediate Steps -Phase 4 complete. Phase 5 in progress (Tasks 12–14 complete). Next: +Product Phase 1 complete. Next: -1. **Task 15: Permissionless Hardening** - Sybil resistance, host attestation, anti-withholding -2. **Extended testing** - Run agents with migration retry under adverse conditions -3. **Hardening** - Bug fixes, test coverage, documentation accuracy +1. **Product Phase 2: Agent Self-Provisioning** — HTTP hostcall, x402 wallet, Akash deployment +2. **Demo agents** — more compelling demo agents that use external APIs +3. **Developer experience** — better error messages, documentation, examples