195 lines · 7.3 KB · Go
RawDownload
package cli import (	"context"	"flag"	"fmt"	"os" 	"github.com/davis7dotsh/tx9/internal/archive"	"github.com/davis7dotsh/tx9/internal/box"	"github.com/davis7dotsh/tx9/internal/docker"	"github.com/davis7dotsh/tx9/internal/names"	"github.com/davis7dotsh/tx9/internal/state"	"github.com/davis7dotsh/tx9/internal/token"	"github.com/davis7dotsh/tx9/internal/version") // restoreStageScript is boxd's stage-then-promote restore script, lifted// verbatim (dossier §7.2), run inside a throwaway container that mounts// the freshly-created (but not-yet-started) box's agent-data volume// directly at /data. It wipes /data, extracts into a staging subdirectory,// sanity-checks the tree, fixes ownership, re-arms the quiesce/gateway// markers (dossier §7.3 — a restored box must always arrive quiesced and// gateway-disabled regardless of the source box's state when it was// saved), runs a last-chance hermes-state integrity check against the// staged (not yet live) Hermes home, and only then promotes the staged// tree up into /data itself.const restoreStageScript = `set -euo pipefailfind /data -mindepth 1 -deletestage=/data/.hb-restore-stagemkdir -m 0700 "$stage"tar xzf - --no-same-owner -C "$stage"test -d "$stage/home/agent"chown root:root "$stage" "$stage/home"chmod 0711 "$stage" "$stage/home"chown -R agent:agent "$stage/home/agent" "$stage/logs" 2>/dev/null || truemkdir -p "$stage/home/agent/.config/hermes-box"touch "$stage/home/agent/.config/hermes-box/quiesced" \      "$stage/home/agent/.config/hermes-box/gateway-disabled" \      "$stage/home/agent/.config/hermes-box/gateway-policy-initialized"chown -R agent:agent "$stage/home/agent/.config/hermes-box"runuser -u agent -- env HOME=/data/home/agent \  /opt/hermes-box/bin/hermes-state verify --home "$stage/home/agent/.hermes" >/dev/nullshopt -s dotglobmv "$stage"/* /data/rmdir "$stage"chown root:root /data && chmod 0711 /data` // cmdImport implements `tx9 import <file.tx9>`, the Go port of boxd's// cmd_load (dossier §7): read metadata (plaintext, before touching the// possibly-encrypted payload) -> resolve target name -> hard fail on// collision -> decrypt+validate BEFORE creating anything -> mint a fresh// token -> create network/volumes/containers without starting them ->// stage-then-promote restore via a throwaway container -> start the box ->// verify-state (not the full runtime-prep gate, since that would try to// wire MCP against a box the restore deliberately leaves quiesced).func cmdImport(args []string) error {	fs := flag.NewFlagSet("import", flag.ContinueOnError)	nameOverride := fs.String("name", "", "box name to restore under (default: from archive metadata)")	password := fs.String("password", "", "archive passphrase (else TX9_PASSWORD env, else prompt)")	if err := parseFlagsAnywhere(fs, args); err != nil {		return err	}	if fs.NArg() < 1 {		return fmt.Errorf("import: archive file required (usage: tx9 import <file.tx9>)")	}	archivePath := fs.Arg(0) 	meta, err := archive.ReadMetadata(archivePath)	if err != nil {		return fmt.Errorf("import %s: %w", archivePath, err)	} 	name := *nameOverride	if name == "" {		name = meta.BoxName	}	if err := names.Validate(name); err != nil {		return fmt.Errorf("import %s: %w", archivePath, err)	} 	var passphrase string	if meta.Encrypted {		passphrase, err = resolvePassword(*password, false)		if err != nil {			return fmt.Errorf("import %s: %w", archivePath, err)		}	} 	return withBoxLock(name, func(ctx context.Context, cli *docker.Client) error {		if exists, err := box.Exists(ctx, cli, name); err != nil {			return fmt.Errorf("import %s: %w", name, err)		} else if exists {			return fmt.Errorf("import %s: box %q already exists, choose a different --name or delete it first", name, name)		} 		tmpDir, err := os.MkdirTemp("", "tx9-import-*")		if err != nil {			return fmt.Errorf("import %s: %w", name, err)		}		defer os.RemoveAll(tmpDir) 		fmt.Println("tx9: extracting archive payload")		rawPath := tmpDir + "/data.payload"		if _, err := archive.ExtractData(archivePath, rawPath); err != nil {			return fmt.Errorf("import %s: %w", name, err)		} 		dataTarGzPath := rawPath		if meta.Encrypted {			fmt.Println("tx9: decrypting archive")			decPath := tmpDir + "/data.tar.gz"			if err := archive.Decrypt(rawPath, decPath, passphrase); err != nil {				return fmt.Errorf("import %s: %w", name, err)			}			dataTarGzPath = decPath		} 		fmt.Println("tx9: validating archive contents")		if err := validateFileAsDataTar(dataTarGzPath); err != nil {			return fmt.Errorf("import %s: archive failed validation, refusing to restore: %w", name, err)		} 		tok, err := token.Mint()		if err != nil {			return fmt.Errorf("import %s: %w", name, err)		}		if err := state.WriteBoxEnv(name, map[string]string{"EXECUTOR_MCP_TOKEN": tok}); err != nil {			return fmt.Errorf("import %s: %w", name, err)		} 		imageTag := fmt.Sprintf("tx9-box:%s", version.Version)		if err := ensureBoxImage(ctx, cli, imageTag); err != nil {			return fmt.Errorf("import %s: %w", name, err)		} 		fmt.Printf("tx9: creating box %q (network, volumes, containers — not started yet)\n", name)		b, err := box.Create(ctx, cli, box.CreateOpts{Name: name, Image: imageTag, Token: tok, NoStart: true})		if err != nil {			box.Destroy(ctx, cli, name)			return fmt.Errorf("import %s: %w", name, err)		} 		fmt.Println("tx9: restoring data into the agent volume (stage-then-promote)")		agentVol, _ := box.VolumeNames(name)		sf, err := os.Open(dataTarGzPath)		if err != nil {			box.Destroy(ctx, cli, name)			return fmt.Errorf("import %s: %w", name, err)		}		exitCode, err := cli.RunEphemeral(ctx, docker.EphemeralOpts{			Image:      imageTag,			Entrypoint: []string{"bash"},			Cmd:        []string{"-c", restoreStageScript},			Binds:      []string{agentVol + ":/data"},			Stdin:      sf,			Stdout:     os.Stdout,			Stderr:     os.Stderr,		})		sf.Close()		if err != nil {			box.Destroy(ctx, cli, name)			return fmt.Errorf("import %s: stage-then-promote restore: %w", name, err)		}		if exitCode != 0 {			box.Destroy(ctx, cli, name)			return fmt.Errorf("import %s: stage-then-promote restore exited %d", name, exitCode)		} 		fmt.Println("tx9: starting box")		if err := box.Start(ctx, cli, b); err != nil {			box.Destroy(ctx, cli, name)			return fmt.Errorf("import %s: %w", name, err)		} 		// Deliberately NOT box.PrepareRuntime: it unconditionally runs		// `hb wire-once`, which would try to wire MCP against a box the		// restore just arranged to arrive quiesced + gateway-disabled		// (dossier §7.3). verify-state confirms the container/daemon		// stack is healthy without touching that contract; a failure here		// is a warning, not a reason to tear the box back down, since the		// restore itself already succeeded.		if err := box.HB(ctx, cli, b, tok, os.Stdout, os.Stderr, "verify-state"); err != nil {			fmt.Fprintf(os.Stderr, "tx9: warning: post-import verify-state reported a problem, inspect the box before trusting it: %v\n", err)		} 		fmt.Printf("\ntx9: box %q restored from %s.\n", name, archivePath)		fmt.Println("It has arrived quiesced and with the Hermes gateway disabled (single-writer safety gate). Next steps:")		fmt.Printf("  1. tx9 enter %s\n", name)		fmt.Println("  2. hb resume")		fmt.Println("  3. hb gateway-enable --confirm-single-writer I_CONFIRM_NO_OTHER_GATEWAY_USES_THIS_IDENTITY")		return nil	})}