| //! `amem`: the per-session memory server, the egress, the launcher, and the command | |
| //! line. With no subcommand it opens the browser on the web launcher, starting it first | |
| //! when it is not already running. | |
| //! | |
| //! Every subcommand is a thin call into the library crates; the MCP server and the command | |
| //! line see the same operations. The binary resolves its mounts at startup (`--root`, | |
| //! `MEMORY_ROOT`, or `~/memory`) and, for `launch`, reads the one or two credentials it | |
| //! needs from its own environment, filled from the root's `credentials.env`, and never | |
| //! passes them inside. | |
| mod board; | |
| mod browser; | |
| mod canary; | |
| mod checker; | |
| mod cost; | |
| mod credentials; | |
| mod curation; | |
| mod deputy; | |
| mod doctor; | |
| mod drift; | |
| mod eventlog; | |
| mod gofannon; | |
| mod golden; | |
| mod harness_usage; | |
| mod hooks; | |
| mod hosted; | |
| mod image; | |
| mod librarian; | |
| mod mcp_http; | |
| mod mcp_meta; | |
| mod mine; | |
| mod model; | |
| mod open; | |
| mod platform; | |
| mod profiles; | |
| mod reenact; | |
| mod replay; | |
| mod requests; | |
| mod review; | |
| mod savings; | |
| mod sources; | |
| mod status; | |
| mod stops; | |
| mod taps; | |
| mod tool_server; | |
| mod transcripts; | |
| mod work; | |
| mod wslc; | |
| mod wslc_api; | |
| use clap::{CommandFactory, Parser, Subcommand, ValueEnum}; | |
| use dm_core::{EntryType, Label}; | |
| use dm_egress::{Auth, Destination, Egress, Telemetry}; | |
| use dm_ledger::{Status, TaskSpec}; | |
| use dm_sandbox::{AuthMode, Harness}; | |
| use dm_server::recall::{self, Budget, Depth, Deputy}; | |
| use dm_server::{mcp, Memory, WriteRequest}; | |
| use std::collections::HashMap; | |
| use std::ffi::OsString; | |
| use std::io::Read; | |
| use std::path::{Path, PathBuf}; | |
| use std::sync::Arc; | |
| use time::Duration; | |
| #[derive(Parser)] | |
| #[command( | |
| name = "amem", | |
| version, | |
| about = "Durable memory for disposable agents: one root, served over MCP, with a confined launcher for the harness.", | |
| after_help = "With no command, amem opens the web launcher in the browser, starting it when it is not running. `amem doctor` says what this host can run." | |
| )] | |
| struct Cli { | |
| /// The memory root: grants.toml, credentials.env, record/, state/. The only path | |
| /// this process touches. Defaults to ~/memory. | |
| #[arg(long, env = "MEMORY_ROOT", global = true)] | |
| root: Option<PathBuf>, | |
| #[command(flatten)] | |
| open: OpenArgs, | |
| #[command(subcommand)] | |
| command: Option<Command>, | |
| } | |
| /// What `amem` with no command takes: `amem web`'s two everyday flags. | |
| #[derive(clap::Args)] | |
| struct OpenArgs { | |
| /// Print the pairing link instead of opening a browser on it. | |
| #[arg(long)] | |
| no_open: bool, | |
| /// The working tree sessions start in. Defaults to the current directory, which may | |
| /// not hold the root. | |
| #[arg(long)] | |
| workdir: Option<PathBuf>, | |
| /// A hosted copy of the pages to open instead of the server's own, its origin let | |
| /// through: `https://<host>/<path>/`. | |
| #[arg(long, env = "AMEM_HOSTED")] | |
| hosted: Option<String>, | |
| } | |
| /// How much of each hit a search or a recall carries. | |
| #[derive(Clone, Copy, ValueEnum)] | |
| enum DepthArg { | |
| /// Title and summary. | |
| Summaries, | |
| /// Bodies too, best first, within a budget. | |
| Sections, | |
| } | |
| impl From<DepthArg> for Depth { | |
| fn from(d: DepthArg) -> Depth { | |
| match d { | |
| DepthArg::Summaries => Depth::Summaries, | |
| DepthArg::Sections => Depth::Sections, | |
| } | |
| } | |
| } | |
| #[derive(Clone, Copy, ValueEnum)] | |
| enum Kind { | |
| Decision, | |
| Fact, | |
| Convention, | |
| Method, | |
| Handoff, | |
| Policy, | |
| } | |
| impl From<Kind> for EntryType { | |
| fn from(k: Kind) -> EntryType { | |
| match k { | |
| Kind::Decision => EntryType::Decision, | |
| Kind::Fact => EntryType::Fact, | |
| Kind::Convention => EntryType::Convention, | |
| Kind::Method => EntryType::Method, | |
| Kind::Handoff => EntryType::Handoff, | |
| Kind::Policy => EntryType::Policy, | |
| } | |
| } | |
| } | |
| #[derive(Clone, Copy, ValueEnum)] | |
| enum LabelArg { | |
| Public, | |
| Internal, | |
| Restricted, | |
| } | |
| impl From<LabelArg> for Label { | |
| fn from(l: LabelArg) -> Label { | |
| match l { | |
| LabelArg::Public => Label::Public, | |
| LabelArg::Internal => Label::Internal, | |
| LabelArg::Restricted => Label::Restricted, | |
| } | |
| } | |
| } | |
| #[derive(Clone, Copy, ValueEnum)] | |
| enum StatusArg { | |
| Proposed, | |
| Admitted, | |
| Claimed, | |
| Completed, | |
| Invalidated, | |
| } | |
| impl From<StatusArg> for Status { | |
| fn from(s: StatusArg) -> Status { | |
| match s { | |
| StatusArg::Proposed => Status::Proposed, | |
| StatusArg::Admitted => Status::Admitted, | |
| StatusArg::Claimed => Status::Claimed, | |
| StatusArg::Completed => Status::Completed, | |
| StatusArg::Invalidated => Status::Invalidated, | |
| } | |
| } | |
| } | |
| /// Where a subagent the harness would spawn runs. | |
| #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)] | |
| enum SubagentsArg { | |
| /// In the harness, as it would without the launcher. | |
| #[default] | |
| Harness, | |
| /// Filed on the task board as a task of its own; the call is refused with its id. | |
| Board, | |
| } | |
| #[derive(Clone, Copy, ValueEnum)] | |
| enum HarnessArg { | |
| Claude, | |
| Codex, | |
| Gemini, | |
| /// Google's Antigravity CLI, `agy`. | |
| Antigravity, | |
| /// Alibaba's Qwen Code. | |
| Qwen, | |
| /// xAI's Grok Build, `grok`. | |
| Grok, | |
| /// Meta's Muse Code, `muse`. | |
| Muse, | |
| /// The pi coding agent, `pi`. | |
| Pi, | |
| Copilot, | |
| Opencode, | |
| /// A program: the first argument after `--` is run as given, with the egress and | |
| /// the memory server in its environment. | |
| Command, | |
| } | |
| impl From<HarnessArg> for Harness { | |
| fn from(h: HarnessArg) -> Harness { | |
| match h { | |
| HarnessArg::Claude => Harness::Claude, | |
| HarnessArg::Codex => Harness::Codex, | |
| HarnessArg::Gemini => Harness::Gemini, | |
| HarnessArg::Antigravity => Harness::Antigravity, | |
| HarnessArg::Qwen => Harness::Qwen, | |
| HarnessArg::Grok => Harness::Grok, | |
| HarnessArg::Muse => Harness::Muse, | |
| HarnessArg::Pi => Harness::Pi, | |
| HarnessArg::Copilot => Harness::Copilot, | |
| HarnessArg::Opencode => Harness::OpenCode, | |
| HarnessArg::Command => Harness::Command, | |
| } | |
| } | |
| } | |
| #[derive(Subcommand)] | |
| enum ImageOp { | |
| /// Export the image from the runtime that holds it (wslc, podman or docker) and | |
| /// unpack it under the record, for the namespace backend to pivot into. | |
| Unpack { | |
| #[arg(long, env = "MEMORY_IMAGE", default_value = wslc::DEFAULT_IMAGE)] | |
| image: String, | |
| }, | |
| /// Where the unpacked image is, if it is there. | |
| Path, | |
| } | |
| /// How mining reaches a model. | |
| #[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)] | |
| pub enum MineVia { | |
| /// Through the harness on a subscription login, directly through the egress otherwise. | |
| Auto, | |
| /// A prompted run of the harness itself, which is the client the login belongs to. | |
| Harness, | |
| /// A direct call through the egress, with a key or the gateway. | |
| Api, | |
| } | |
| /// Where the harness runs. | |
| #[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)] | |
| enum BackendArg { | |
| /// Namespaces on Linux; Seatbelt on macOS; WSL containers on Windows. | |
| Auto, | |
| /// Native Linux namespaces, from this host's binaries. | |
| Namespaces, | |
| /// Native Linux namespaces, pivoted into the unpacked harness image (`amem image | |
| /// unpack` first). What `auto` picks on Linux once the image is unpacked. | |
| Image, | |
| /// WSL containers: the harness image in a session VM, through `wslc`. | |
| Wslc, | |
| /// Seatbelt, macOS's sandbox, over this host's binaries, with a profile made from the | |
| /// plan. What `auto` picks on macOS. Dry-coded: not yet run on a Mac. | |
| Seatbelt, | |
| /// No confinement: the egress still proxies and meters, nothing else. | |
| None, | |
| } | |
| #[derive(Clone, Copy, ValueEnum)] | |
| enum AuthArg { | |
| Gateway, | |
| Key, | |
| Subscription, | |
| } | |
| impl From<AuthArg> for AuthMode { | |
| fn from(a: AuthArg) -> AuthMode { | |
| match a { | |
| AuthArg::Gateway => AuthMode::Gateway, | |
| AuthArg::Key => AuthMode::Key, | |
| AuthArg::Subscription => AuthMode::Subscription, | |
| } | |
| } | |
| } | |
| #[derive(Subcommand)] | |
| enum Command { | |
| /// Create a root for one owner and one domain. | |
| Init { | |
| /// The owner, system-qualified. Defaults to human:local:<user>. | |
| #[arg(long)] | |
| owner: Option<String>, | |
| /// The first domain, such as dom-me. | |
| #[arg(long, default_value = "dom-me")] | |
| domain: String, | |
| }, | |
| /// Serve the root over MCP on stdin and stdout until the client goes away. | |
| Serve { | |
| /// Attribute writes to this author instead of the owner, e.g. "agent:claude-code (model: x)". | |
| #[arg(long)] | |
| author: Option<String>, | |
| /// Capabilities this session carries, matched against task requirements. | |
| #[arg(long = "cap")] | |
| manifest: Vec<String>, | |
| }, | |
| /// Run a harness confined: no network except the egress, the memory server attached, | |
| /// model calls forwarded and metered. Credentials come from this process's | |
| /// environment: ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, XAI_API_KEY or | |
| /// DASHSCOPE_API_KEY for --auth key, LLMAO_KEY (and LLMAO_URL) for --auth gateway, | |
| /// the harness's own login for --auth subscription. | |
| Launch { | |
| /// Which harness. No default: say which one. | |
| #[arg(long, value_enum)] | |
| harness: HarnessArg, | |
| #[arg(long, value_enum, default_value = "subscription")] | |
| auth: AuthArg, | |
| /// Send model calls to another destination: openai, xai (Grok), dashscope, or a | |
| /// name from grants.toml. Codex, Qwen Code and OpenCode only. | |
| #[arg(long)] | |
| provider: Option<String>, | |
| /// A credential of their own for the memory's calls, the recall deputy and | |
| /// mining, so a harness on a personal login can leave them to an institution's | |
| /// gateway: key or gateway. They follow --auth otherwise, and a login gives | |
| /// them nothing. | |
| #[arg(long, value_enum)] | |
| server_auth: Option<AuthArg>, | |
| /// Where the memory's own calls go under --server-auth key; the gateway needs | |
| /// no name. | |
| #[arg(long)] | |
| server_provider: Option<String>, | |
| /// The working tree the harness may read and write. Defaults to the current directory. | |
| #[arg(long)] | |
| workdir: Option<PathBuf>, | |
| /// Run without namespaces: the egress still proxies and meters, but nothing is | |
| /// confined. For platforms without a backend, and for debugging. | |
| #[arg(long)] | |
| unconfined: bool, | |
| /// Where the harness runs. `wslc` uses the harness image under WSL containers. | |
| #[arg(long, value_enum, default_value = "auto")] | |
| backend: BackendArg, | |
| /// The harness image for the `wslc` backend. | |
| #[arg(long, env = "MEMORY_IMAGE", default_value = wslc::DEFAULT_IMAGE)] | |
| image: String, | |
| /// How the `wslc` backend drives WSL containers: the C API on Windows where it | |
| /// can serve (not interactive, not the gateway), or the command line. | |
| #[arg(long, value_enum, env = "MEMORY_WSLC_VIA", default_value = "auto")] | |
| wslc_via: wslc::Via, | |
| /// Where the C API keeps its session disks; under the root's state by default, | |
| /// and needed when the root is a `\\wsl.localhost` path. | |
| #[arg(long, env = "MEMORY_WSLC_STORAGE")] | |
| wslc_storage: Option<PathBuf>, | |
| /// Non-interactive: run this prompt in the harness's headless form, print its | |
| /// answer, and exit when it does. The run is expected to leave a handoff. | |
| #[arg(long, conflicts_with = "prompt_file")] | |
| prompt: Option<String>, | |
| /// As --prompt, read from a file. | |
| #[arg(long)] | |
| prompt_file: Option<PathBuf>, | |
| /// The model the harness runs, in its own flag; for Claude Code also every | |
| /// role's default, so a gateway that serves one model is asked for that one. | |
| #[arg(long)] | |
| model: Option<String>, | |
| /// Where a subagent the harness would spawn runs: in the harness, or filed on | |
| /// the task board as a task of its own and refused here. | |
| #[arg(long, value_enum, default_value = "harness")] | |
| subagents: SubagentsArg, | |
| /// With --prompt: a run that leaves no handoff is fine. | |
| #[arg(long)] | |
| no_handoff: bool, | |
| /// Run over an empty record of the same owner and domain, made for this run under | |
| /// the system's temporary directory: the same sandbox, egress and tools, and | |
| /// nothing in memory. For an evaluation's baseline. Its calls are metered in that | |
| /// record's telemetry, not this one's. | |
| #[arg(long)] | |
| empty_memory: bool, | |
| /// After the run, mine the session's transcript for entries with a small model | |
| /// through the same destination; they land raw, low confidence, with the | |
| /// transcript as source. | |
| #[arg(long)] | |
| mine: bool, | |
| /// The model mining uses; a small default per destination otherwise. | |
| #[arg(long)] | |
| mine_model: Option<String>, | |
| /// How mining reaches the model. | |
| #[arg(long, value_enum, default_value = "auto")] | |
| mine_via: MineVia, | |
| /// Arguments passed to the harness. | |
| #[arg(trailing_var_arg = true, allow_hyphen_values = true)] | |
| args: Vec<String>, | |
| }, | |
| /// Claim the next task from the ledger and run the harness on it, non-interactively. | |
| /// The launcher holds the lease and heartbeats while the harness works. A task the | |
| /// harness completed stays complete; anything else goes back to the pool with the | |
| /// harness's output filed as a handoff. One invocation, one task; --loop keeps going. | |
| Work { | |
| /// Which harness. No default: say which one. | |
| #[arg(long, value_enum)] | |
| harness: HarnessArg, | |
| #[arg(long, value_enum, default_value = "subscription")] | |
| auth: AuthArg, | |
| /// Send model calls to another destination. Codex, Qwen Code and OpenCode only. | |
| #[arg(long)] | |
| provider: Option<String>, | |
| /// A credential of their own for the memory's calls, the recall deputy and | |
| /// mining, so a harness on a personal login can leave them to an institution's | |
| /// gateway: key or gateway. They follow --auth otherwise, and a login gives | |
| /// them nothing. | |
| #[arg(long, value_enum)] | |
| server_auth: Option<AuthArg>, | |
| /// Where the memory's own calls go under --server-auth key; the gateway needs | |
| /// no name. | |
| #[arg(long)] | |
| server_provider: Option<String>, | |
| /// The working tree the harness may read and write. Defaults to the current directory. | |
| #[arg(long)] | |
| workdir: Option<PathBuf>, | |
| /// Run without namespaces. | |
| #[arg(long)] | |
| unconfined: bool, | |
| /// Where the harness runs. `wslc` uses the harness image under WSL containers. | |
| #[arg(long, value_enum, default_value = "auto")] | |
| backend: BackendArg, | |
| /// The harness image for the `wslc` backend. | |
| #[arg(long, env = "MEMORY_IMAGE", default_value = wslc::DEFAULT_IMAGE)] | |
| image: String, | |
| /// How the `wslc` backend drives WSL containers: the C API on Windows where it | |
| /// can serve (not interactive, not the gateway), or the command line. | |
| #[arg(long, value_enum, env = "MEMORY_WSLC_VIA", default_value = "auto")] | |
| wslc_via: wslc::Via, | |
| /// Where the C API keeps its session disks; under the root's state by default, | |
| /// and needed when the root is a `\\wsl.localhost` path. | |
| #[arg(long, env = "MEMORY_WSLC_STORAGE")] | |
| wslc_storage: Option<PathBuf>, | |
| /// Claim from this domain; the only granted domain when omitted. | |
| #[arg(long)] | |
| domain: Option<String>, | |
| /// Lease length in seconds; the launcher heartbeats at a third of it. | |
| #[arg(long, default_value_t = 900)] | |
| lease_seconds: i64, | |
| /// Keep claiming until interrupted; wait --idle-seconds when nothing is claimable. | |
| #[arg(long = "loop")] | |
| keep_going: bool, | |
| /// With --loop: how long to wait when the queue is empty. | |
| #[arg(long, default_value_t = 30)] | |
| idle_seconds: u64, | |
| /// The model the harness runs, in its own flag. | |
| #[arg(long)] | |
| model: Option<String>, | |
| /// Where a subagent the harness would spawn runs. On the board by default here: | |
| /// a task's fan-out is more tasks, which the board claims and runs. | |
| #[arg(long, value_enum, default_value = "board")] | |
| subagents: SubagentsArg, | |
| /// After each task, mine the session's transcript for entries. | |
| #[arg(long)] | |
| mine: bool, | |
| #[arg(long)] | |
| mine_model: Option<String>, | |
| #[arg(long, value_enum, default_value = "auto")] | |
| mine_via: MineVia, | |
| /// Leave the prefetch out of the prompt: no summaries of the entries the task | |
| /// likely needs, for comparisons against a run without memory's push. | |
| #[arg(long)] | |
| no_prefetch: bool, | |
| /// Work this task rather than the next one, and stop after it. | |
| #[arg(long)] | |
| task: Option<String>, | |
| }, | |
| /// The librarian's survey: what in a domain needs curation, from the record and | |
| /// the read log, as proposals; with --file, each becomes a task for a run to do with | |
| /// the memory server's own tools, born proposed unless the domain admits its own. | |
| Librarian { | |
| #[arg(long)] | |
| domain: Option<String>, | |
| /// File each proposal as a task on the ledger. | |
| #[arg(long)] | |
| file: bool, | |
| /// Days after which a raw entry with no review date is due for a look. | |
| #[arg(long, default_value_t = 30)] | |
| stale_days: i64, | |
| /// Characters of body above which an entry is proposed for a split. | |
| #[arg(long, default_value_t = 12_000)] | |
| split_chars: usize, | |
| /// Where an entry's `file:` sources are resolved; a source changed after the | |
| /// entry was written calls for a verify. Defaults to the current directory. | |
| #[arg(long)] | |
| workdir: Option<PathBuf>, | |
| /// The librarian's own telemetry instead of a survey: curation per author from | |
| /// the record and the ledger, and the alarms it raises; --file files them. | |
| #[arg(long)] | |
| telemetry: bool, | |
| }, | |
| /// One curation cycle under a budget: the survey filed, the checker on a sample, | |
| /// contradictions hunted, the canaries checked, the telemetry's alarms filed, and a | |
| /// line in state/curation.jsonl; with --every, again at that interval. The tasks it | |
| /// files are worked by `amem work` on a cheap model. | |
| Curate { | |
| #[arg(long)] | |
| domain: Option<String>, | |
| #[arg(long, value_enum, default_value = "gateway")] | |
| auth: AuthArg, | |
| #[arg(long)] | |
| provider: Option<String>, | |
| /// The checker's model, a different one than the librarian runs on. | |
| #[arg(long)] | |
| model: Option<String>, | |
| /// Entries the checker samples each cycle. | |
| #[arg(long, default_value_t = 8)] | |
| sample: usize, | |
| /// Hunt contradictions too, on the same sample. | |
| #[arg(long)] | |
| contradictions: bool, | |
| /// Curation tasks that may be open at once; the survey files up to the gap. | |
| #[arg(long, default_value_t = 20)] | |
| budget_tasks: usize, | |
| /// Tokens a cycle's model calls may take in all; the checker stops at it. | |
| #[arg(long, default_value_t = 200_000)] | |
| budget_tokens: u64, | |
| /// Run the vendor-drift probes on the checker's model each cycle, against its | |
| /// baseline, filing confirmed drift for the owner. Uses probes/drift.toml in the | |
| /// working directory. | |
| #[arg(long)] | |
| drift: bool, | |
| /// Where `file:` sources are resolved. | |
| #[arg(long)] | |
| workdir: Option<PathBuf>, | |
| /// Run a cycle every so many seconds, until stopped. | |
| #[arg(long)] | |
| every: Option<u64>, | |
| }, | |
| /// The checker: a model of its own, not the librarian's, judges whether each | |
| /// summary the index serves entails its body, on a sample of a domain; it writes | |
| /// nothing, and with --file a rejected summary becomes a task for a librarian run. | |
| Check { | |
| #[arg(long, value_enum, default_value = "key")] | |
| auth: AuthArg, | |
| /// The destination; openai by default, llmao with --auth gateway. | |
| #[arg(long)] | |
| provider: Option<String>, | |
| /// The checker's model. Make it a different one than wrote the summaries. | |
| #[arg(long)] | |
| model: Option<String>, | |
| #[arg(long)] | |
| domain: Option<String>, | |
| /// How many entries to sample, newest first. | |
| #[arg(long, default_value_t = 20)] | |
| sample: usize, | |
| #[arg(long)] | |
| file: bool, | |
| /// Hunt contradictions too: pairs a search puts side by side, judged by the model. | |
| #[arg(long)] | |
| contradictions: bool, | |
| /// Read the quarantine too: each held write not yet reviewed gets the model's | |
| /// verdict, malicious, report or benign, kept beside it for the owner. | |
| #[arg(long)] | |
| quarantine: bool, | |
| }, | |
| /// Frozen-context replay: the decision contexts the egress recorded for a session sent | |
| /// again with only the model changed, the next action scored against the one | |
| /// recorded; a session with no contexts falls back to its transcript, and says so. | |
| /// The owner's; the run is logged under state/replay. | |
| Replay { | |
| /// The session whose decision contexts, or transcript, hold the decision points. | |
| session: String, | |
| /// Only this turn; every assistant turn otherwise, up to --limit. | |
| #[arg(long)] | |
| turn: Option<usize>, | |
| #[arg(long, default_value_t = 6)] | |
| limit: usize, | |
| #[arg(long, value_enum, default_value = "gateway")] | |
| auth: AuthArg, | |
| #[arg(long)] | |
| provider: Option<String>, | |
| #[arg(long)] | |
| model: Option<String>, | |
| }, | |
| /// Canary entries: planted with known content, checked for alteration. | |
| Canary { | |
| #[command(subcommand)] | |
| op: CanaryOp, | |
| }, | |
| /// The policy in force: the checkable perimeter as Rego, built in or the root's own | |
| /// under policy/*.rego. `show` prints it with its version; `eval` decides one | |
| /// request read as JSON from a file or standard input. | |
| Policy { | |
| #[command(subcommand)] | |
| op: PolicyOp, | |
| }, | |
| /// Writes the gates refused, held under state/quarantine and never served: list | |
| /// them, show one, release one into the record as it was, or drop it. | |
| Quarantine { | |
| #[command(subcommand)] | |
| op: QuarantineOp, | |
| }, | |
| /// What an entry may have reached: the sessions that read it and the entries they | |
| /// wrote after; where to look when an entry turns out to be poison. | |
| Taint { id: String }, | |
| /// Promote an entry's trust tier. To curated at once, by anyone but its author; to | |
| /// verified by proposal only, a task a person who did not propose it approves. | |
| Promote { | |
| id: String, | |
| /// curated or verified. | |
| #[arg(long)] | |
| to: String, | |
| /// What was checked, and against what. Required: the ledger keeps it. | |
| #[arg(long)] | |
| why: String, | |
| }, | |
| /// Approve a proposal to promote an entry to verified. The approver did not | |
| /// propose it, and is a person. | |
| Approve { task: String }, | |
| /// Why each promotion of an entry was made, as JSON, oldest first. A proposal to | |
| /// verified is listed, not landed until a person approves it. An auditor reads it | |
| /// after judging the change, not before. | |
| Promotions { id: String }, | |
| /// The models a destination lists, as JSON: what the web form offers to pick from. | |
| /// Routed as a launch would be, with the destination's credential; a login mode has | |
| /// no list, since the login is the harness's own. | |
| Models { | |
| #[arg(long, value_enum, default_value = "key")] | |
| auth: AuthArg, | |
| /// The destination; the gateway's with --auth gateway, openai otherwise. | |
| #[arg(long)] | |
| provider: Option<String>, | |
| }, | |
| /// Mine one transcript for entries with a small model through a destination. | |
| Mine { | |
| /// The transcript, JSON Lines, from any of the harnesses. | |
| transcript: PathBuf, | |
| #[arg(long, value_enum, default_value = "key")] | |
| auth: AuthArg, | |
| /// The destination; openai by default. | |
| #[arg(long, default_value = "openai")] | |
| provider: String, | |
| #[arg(long)] | |
| model: Option<String>, | |
| }, | |
| /// Prove the WSL containers C API end to end: take a session, provision the image | |
| /// into its store, and run a container from it. Windows. | |
| #[command(hide = true)] | |
| WslcApiCheck { | |
| /// The image to provision and run. | |
| #[arg(long, env = "MEMORY_IMAGE", default_value = wslc::DEFAULT_IMAGE)] | |
| image: String, | |
| /// Where the session disks live; under the root's state by default. | |
| #[arg(long, env = "MEMORY_WSLC_STORAGE")] | |
| wslc_storage: Option<PathBuf>, | |
| /// Provision again from the command line's copy, or the registry's, even when | |
| /// the session's store has the image: after `make image`. | |
| #[arg(long)] | |
| refresh: bool, | |
| }, | |
| /// Inside the egress container of the `wslc` backend: open the memory, serve the | |
| /// egress on the shared volume, and run `launch` or `work` by writing run requests | |
| /// the host turns into harness containers. | |
| #[command(hide = true)] | |
| Inside { | |
| #[arg(long, value_enum)] | |
| harness: HarnessArg, | |
| #[arg(long, value_enum)] | |
| auth: AuthArg, | |
| #[arg(long)] | |
| provider: Option<String>, | |
| #[arg(long, value_enum)] | |
| server_auth: Option<AuthArg>, | |
| #[arg(long)] | |
| server_provider: Option<String>, | |
| /// The shared volume. | |
| #[arg(long)] | |
| requests: PathBuf, | |
| /// The host's login directories, read-only. | |
| #[arg(long)] | |
| host_home: Option<PathBuf>, | |
| /// Where the working tree appears in the harness container. | |
| #[arg(long, default_value = "/work")] | |
| workdir_inside: PathBuf, | |
| /// The projection's directory name, which the launcher outside chose and mounts. | |
| #[arg(long)] | |
| projection: Option<String>, | |
| #[arg(long, conflicts_with = "prompt_file")] | |
| prompt: Option<String>, | |
| #[arg(long)] | |
| prompt_file: Option<PathBuf>, | |
| #[arg(long)] | |
| no_handoff: bool, | |
| /// Task mode. | |
| #[arg(long)] | |
| work: bool, | |
| #[arg(long)] | |
| model: Option<String>, | |
| #[arg(long, value_enum, default_value = "harness")] | |
| subagents: SubagentsArg, | |
| #[arg(long)] | |
| domain: Option<String>, | |
| #[arg(long, default_value_t = 900)] | |
| lease_seconds: i64, | |
| #[arg(long = "loop")] | |
| keep_going: bool, | |
| #[arg(long, default_value_t = 30)] | |
| idle_seconds: u64, | |
| /// Task mode without the prefetch. | |
| #[arg(long)] | |
| no_prefetch: bool, | |
| #[arg(trailing_var_arg = true, allow_hyphen_values = true)] | |
| args: Vec<String>, | |
| }, | |
| /// Inside the sandbox: forward a loopback port to the egress socket, then run the | |
| /// harness in its own pid namespace. | |
| #[command(hide = true)] | |
| Relay { | |
| #[arg(long)] | |
| socket: PathBuf, | |
| #[arg(long)] | |
| listen: String, | |
| #[arg(trailing_var_arg = true, allow_hyphen_values = true, required = true)] | |
| argv: Vec<OsString>, | |
| }, | |
| /// Token usage recorded by the egress, by destination and model. | |
| Usage, | |
| /// What runs cost: the egress's metering priced by prices.toml under the root, per | |
| /// session and per completed task, with plan quota and unpriced models shown as | |
| /// tokens beside the money. | |
| Cost { | |
| #[arg(long)] | |
| json: bool, | |
| }, | |
| /// The paper's read-path claims measured on this record: recovery from T0 and a | |
| /// handoff against transcript replay, a page of hits against bodies and corpus, | |
| /// known-item lookups in the top three, and tokens served per session beside what | |
| /// its model calls ingested. | |
| Savings { | |
| /// The query for the retrieval section; the newest entry's title when omitted. | |
| #[arg(long)] | |
| query: Option<String>, | |
| /// Numbers as JSON instead of the text report. | |
| #[arg(long)] | |
| json: bool, | |
| /// A destination for the paper's judge of "used": a small model reads a sample | |
| /// of what sessions opened beside their handoffs, reported against the citation | |
| /// rule. | |
| #[arg(long)] | |
| judge_provider: Option<String>, | |
| #[arg(long, value_enum, default_value = "key", requires = "judge_provider")] | |
| judge_auth: AuthArg, | |
| /// The judge's model; a small default per destination otherwise. | |
| #[arg(long, requires = "judge_provider")] | |
| judge_model: Option<String>, | |
| /// How many opened entries the judge reads. | |
| #[arg(long, default_value_t = 20, requires = "judge_provider")] | |
| judge_sample: usize, | |
| }, | |
| /// Score a golden suite: questions and the entries that answer them, each question | |
| /// asked as written, stripped of stopwords, and by its keywords, through the pack | |
| /// with no model; with `--provider`, the deputy's brief too. | |
| Golden { | |
| /// The suite, TOML; see golden/docs.toml. | |
| #[arg(required_unless_present = "platform")] | |
| suite: Option<PathBuf>, | |
| /// The suite from the platform's domain instead: a verified `eval` artifact by | |
| /// this name. | |
| #[arg(long, conflicts_with = "suite")] | |
| platform: Option<String>, | |
| /// Hits count within the top k. | |
| #[arg(long, default_value_t = 8)] | |
| k: usize, | |
| /// Numbers as JSON instead of the text report. | |
| #[arg(long)] | |
| json: bool, | |
| /// A destination to run the deputy through, so the brief is scored too. | |
| #[arg(long)] | |
| provider: Option<String>, | |
| #[arg(long, value_enum, default_value = "key", requires = "provider")] | |
| auth: AuthArg, | |
| /// The deputy's model; a small default per destination otherwise. | |
| #[arg(long, requires = "provider")] | |
| model: Option<String>, | |
| }, | |
| /// Vendor drift: run a pinned probe set against one destination and model, and | |
| /// compare the latest run with a baseline, detector by detector, once nothing on | |
| /// our side is found to have changed. | |
| Drift { | |
| #[command(subcommand)] | |
| op: drift::DriftOp, | |
| }, | |
| /// Inside the sandbox, run by the harness's hooks: read the event from standard | |
| /// input and leave a note in the outbox for the launcher to file as a handoff. | |
| #[command(hide = true)] | |
| Hook { | |
| /// Which harness's hook format: claude, codex, gemini, antigravity, qwen, grok, copilot or opencode. | |
| harness: String, | |
| }, | |
| /// A web terminal on the launcher: sessions in pseudo-terminals streamed to the | |
| /// browser, with a reading pane over the record. Loopback only, behind a token kept | |
| /// in state/web.json. When one is already running over this root, opens that instead. | |
| Web { | |
| /// Where to listen; a loopback address. Defaults to 127.0.0.1:4200, or the next | |
| /// free port above it. | |
| #[arg(long)] | |
| listen: Option<String>, | |
| /// Print the link instead of opening a browser on it. | |
| #[arg(long)] | |
| no_open: bool, | |
| /// For the browser tests: every session runs this program, split on spaces, in | |
| /// place of the launcher, so the pages are driven without a harness or a sandbox. | |
| #[arg(long, hide = true)] | |
| session_command: Option<String>, | |
| /// A hosted copy of the pages to open instead of the server's own, its origin let | |
| /// through: `https://<host>/<path>/`. | |
| #[arg(long, env = "AMEM_HOSTED")] | |
| hosted: Option<String>, | |
| /// The harness the form preselects; each session names its own. | |
| #[arg(long, value_enum)] | |
| harness: Option<HarnessArg>, | |
| #[arg(long, value_enum, default_value = "subscription")] | |
| auth: AuthArg, | |
| #[arg(long, value_enum, default_value = "auto")] | |
| backend: BackendArg, | |
| /// The working tree sessions start in. Defaults to the current directory. | |
| #[arg(long)] | |
| workdir: Option<PathBuf>, | |
| }, | |
| /// The profiles: `profiles.toml` under the root, and every harness by its own name. | |
| /// `amem <profile>` launches one; `launch` and `work` take `--profile <name>`. | |
| Profiles { | |
| /// As JSON, for the web launcher. | |
| #[arg(long)] | |
| json: bool, | |
| }, | |
| /// What this host can run: the root and its grants, the credentials and logins | |
| /// found, the harnesses on the PATH and in the image, the backend, and whether the | |
| /// gateway answers. Names credentials, never their values. | |
| Doctor { | |
| /// The report as JSON, for the web launcher. | |
| #[arg(long)] | |
| json: bool, | |
| /// Skip the network: do not ask the gateway whether it answers. | |
| #[arg(long)] | |
| offline: bool, | |
| }, | |
| /// The harness image on this host. | |
| Image { | |
| #[command(subcommand)] | |
| op: ImageOp, | |
| }, | |
| /// Write an entry. The body comes from --body or standard input. | |
| Write { | |
| #[arg(long, value_enum, default_value = "fact")] | |
| kind: Kind, | |
| #[arg(long)] | |
| title: Option<String>, | |
| #[arg(long)] | |
| domain: Option<String>, | |
| #[arg(long)] | |
| body: Option<String>, | |
| #[arg(long, value_enum)] | |
| classification: Option<LabelArg>, | |
| #[arg(long)] | |
| source: Vec<String>, | |
| #[arg(long)] | |
| supersedes: Option<String>, | |
| }, | |
| /// Ranked search, summaries only. | |
| Search { | |
| query: String, | |
| #[arg(long)] | |
| domain: Option<String>, | |
| #[arg(long, default_value_t = 10)] | |
| limit: usize, | |
| /// Summaries, or the best hits with their bodies within a budget. | |
| #[arg(long, value_enum, default_value = "summaries")] | |
| depth: DepthArg, | |
| }, | |
| /// Delegated recall: a cited brief when a model is behind it, else the pack the | |
| /// brief would be written from, with the synthesis owed to the caller. | |
| Recall { | |
| question: String, | |
| #[arg(long)] | |
| domain: Option<String>, | |
| /// How much of each entry comes back when synthesis is owed; the session's | |
| /// default otherwise: sections, or summaries once it has fetched in parallel. | |
| #[arg(long, value_enum)] | |
| depth: Option<DepthArg>, | |
| /// A destination to run the deputy through, with `--auth` and `--model`. With | |
| /// none, the pack comes back with synthesis owed. | |
| #[arg(long)] | |
| provider: Option<String>, | |
| #[arg(long, value_enum, default_value = "key", requires = "provider")] | |
| auth: AuthArg, | |
| /// The deputy's model; a small default per destination otherwise. | |
| #[arg(long, requires = "provider")] | |
| model: Option<String>, | |
| }, | |
| /// One entry, in full. | |
| Get { id: String }, | |
| /// Entries of one kind, newest first. | |
| List { | |
| #[arg(long, value_enum)] | |
| kind: Option<Kind>, | |
| #[arg(long)] | |
| domain: Option<String>, | |
| #[arg(long, default_value_t = 10)] | |
| limit: usize, | |
| }, | |
| /// The T0 for a domain. | |
| T0 { | |
| #[arg(long)] | |
| domain: Option<String>, | |
| }, | |
| /// The task ledger. | |
| Task { | |
| #[command(subcommand)] | |
| op: TaskOp, | |
| }, | |
| /// The platform's domain: agent definitions, prompt templates, eval suites and | |
| /// harness configurations, which the launcher loads only once verified. | |
| Platform { | |
| #[command(subcommand)] | |
| op: platform::PlatformOp, | |
| }, | |
| /// Gofannon's deployed agents: list them, or work the ledger's tasks with one, its | |
| /// result filed as each task's handoff. Through the `gofannon` tool's grant. | |
| Gofannon { | |
| #[command(subcommand)] | |
| op: gofannon::GofannonOp, | |
| }, | |
| /// The event log of a domain, the order of record of its entries, and the | |
| /// materializer that builds the index from it. | |
| Log { | |
| #[command(subcommand)] | |
| op: eventlog::LogOp, | |
| }, | |
| /// Re-enactment: a session run again on its prompt and commit against its tape, | |
| /// the memory calls answered from the tape until the run diverges; the tapes, and | |
| /// a report of the runs by harness and model. | |
| Reenact { | |
| #[command(subcommand)] | |
| op: reenact::ReenactOp, | |
| }, | |
| /// Session transcripts, kept in the record as sources and reached by citation: | |
| /// list them, show one turn, sweep the expired, erase one early. | |
| Transcript { | |
| #[command(subcommand)] | |
| op: transcripts::TranscriptOp, | |
| }, | |
| /// Call a tool tap as the owner, fetch or search: every request it makes is decided | |
| /// by the policy, and its answer is scanned and kept as a source. `args` is JSON, | |
| /// `{"url": "https://..."}` for fetch, `{"query": "..."}` for search. | |
| Tool { | |
| /// The tool, by its name in the grants. | |
| name: String, | |
| /// The tool's arguments, as JSON. | |
| args: String, | |
| }, | |
| /// Serve this root's tools over MCP's streamable HTTP, for launchers elsewhere to | |
| /// call instead of carrying the components (`hosted = "<url>"` in their grant): the | |
| /// components installed here, and fetch natively when none is installed as it. Each | |
| /// request a tool makes goes only to a host both this root's grant and the calling | |
| /// session name, and the answer says every URL it reached. Off loopback it wants a | |
| /// bearer token. | |
| ToolServer { | |
| /// Where to listen, as host:port; loopback unless a token is set. | |
| #[arg(long, default_value = "127.0.0.1:4100")] | |
| listen: String, | |
| /// The environment variable holding the bearer token callers must send. | |
| #[arg(long)] | |
| token_env: Option<String>, | |
| }, | |
| /// What tools brought in from outside, kept as sources and cited as `source:<id>`: | |
| /// list them, show one, sweep the expired. | |
| Source { | |
| #[command(subcommand)] | |
| op: sources::SourceOp, | |
| }, | |
| /// Export a domain to a bundle directory, at or below a label. | |
| Export { | |
| dest: PathBuf, | |
| #[arg(long)] | |
| domain: Option<String>, | |
| #[arg(long, value_enum, default_value = "public")] | |
| max_label: LabelArg, | |
| }, | |
| /// A tap: a connector run as a WASI component with exactly the access its grant | |
| /// allows. | |
| Tap { | |
| #[command(subcommand)] | |
| op: TapOp, | |
| }, | |
| /// Import a bundle directory into a domain. | |
| Import { | |
| src: PathBuf, | |
| #[arg(long)] | |
| domain: Option<String>, | |
| }, | |
| /// Rebuild the index from the record and the ledger from its events. | |
| Reindex, | |
| /// Erase an entry: the body leaves every version and the index, a stub and a receipt | |
| /// remain, and entries that cite it are flagged for review. Irreversible; the owner | |
| /// only. | |
| Erase { | |
| id: String, | |
| /// Why, as a class: the receipt keeps it. | |
| #[arg(long, value_enum)] | |
| reason: ReasonArg, | |
| }, | |
| } | |
| /// Why an entry is erased, on the command line. | |
| #[derive(Clone, Copy, ValueEnum)] | |
| enum ReasonArg { | |
| /// The person it concerns, or its author, asked. | |
| Requested, | |
| /// It should never have been written. | |
| Mistaken, | |
| /// It carries material above what the record may hold. | |
| Sensitive, | |
| /// A legal obligation. | |
| Legal, | |
| } | |
| impl From<ReasonArg> for dm_core::erasure::Reason { | |
| fn from(r: ReasonArg) -> Self { | |
| match r { | |
| ReasonArg::Requested => Self::Requested, | |
| ReasonArg::Mistaken => Self::Mistaken, | |
| ReasonArg::Sensitive => Self::Sensitive, | |
| ReasonArg::Legal => Self::Legal, | |
| } | |
| } | |
| } | |
| /// Tap operations. | |
| #[derive(Subcommand)] | |
| enum TapOp { | |
| /// Copy a component under the root as `name`; `[taps.<name>]` in grants.toml says | |
| /// what it may do. | |
| Install { name: String, file: PathBuf }, | |
| /// Run a tap once, or on a schedule. Its outcome is printed as JSON. | |
| Run { | |
| name: String, | |
| /// The tap's own configuration, JSON. | |
| #[arg(long)] | |
| config: Option<String>, | |
| /// Run again every this many seconds until stopped. | |
| #[arg(long)] | |
| every: Option<u64>, | |
| }, | |
| } | |
| #[derive(Subcommand)] | |
| enum PolicyOp { | |
| /// The policy text, its version and where it came from. | |
| Show, | |
| /// Decide one request: a JSON input, from a file or `-` for standard input. | |
| Eval { | |
| #[arg(default_value = "-")] | |
| input: String, | |
| }, | |
| /// The record's policies: each by layer and tier, whether its rule is enforced, and | |
| /// the exceptions in force. | |
| List, | |
| /// The paper's conflict rules over the record's policies, and from the decision log | |
| /// each policy's citations and overrides; --file files a bug on the lower layer of | |
| /// each conflict, for the owner. | |
| Lint { | |
| #[arg(long)] | |
| file: bool, | |
| }, | |
| } | |
| #[derive(Subcommand)] | |
| enum QuarantineOp { | |
| List, | |
| Show { | |
| id: String, | |
| }, | |
| /// Write it into the record as it was written, author and all. The owner's act. | |
| Release { | |
| id: String, | |
| }, | |
| /// Discard it. | |
| Drop { | |
| id: String, | |
| }, | |
| } | |
| #[derive(Subcommand)] | |
| enum CanaryOp { | |
| /// Plant canaries in a domain and record them under the record's state. | |
| Plant { | |
| #[arg(long)] | |
| domain: Option<String>, | |
| #[arg(long, default_value_t = 3)] | |
| count: usize, | |
| }, | |
| /// Read every planted canary back; exit 3 when one has been altered, and with | |
| /// --file, a task for the owner per tripped canary. | |
| Check { | |
| #[arg(long)] | |
| file: bool, | |
| }, | |
| } | |
| #[derive(Subcommand)] | |
| enum TaskOp { | |
| /// File a task. It is born proposed. | |
| Create { | |
| title: String, | |
| #[arg(long, default_value = "")] | |
| body: String, | |
| #[arg(long)] | |
| domain: Option<String>, | |
| #[arg(long = "require")] | |
| requirements: Vec<String>, | |
| #[arg(long = "after")] | |
| depends_on: Vec<String>, | |
| #[arg(long, default_value_t = 0)] | |
| priority: i64, | |
| }, | |
| /// Admit a proposed task. | |
| Admit { id: String }, | |
| /// Tasks with a status. | |
| List { | |
| #[arg(long, value_enum, default_value = "admitted")] | |
| status: StatusArg, | |
| #[arg(long)] | |
| domain: Option<String>, | |
| }, | |
| /// Claim the highest-ranked claimable task. | |
| ClaimNext { | |
| #[arg(long)] | |
| domain: Option<String>, | |
| #[arg(long, default_value_t = 1800)] | |
| lease_seconds: i64, | |
| }, | |
| /// Renew a lease. | |
| Heartbeat { | |
| id: String, | |
| #[arg(long, default_value_t = 1800)] | |
| lease_seconds: i64, | |
| }, | |
| /// Complete a task with a handoff entry id. | |
| Complete { id: String, handoff: String }, | |
| /// Give a task back, optionally with a handoff for the next claimant. | |
| Release { | |
| id: String, | |
| #[arg(long)] | |
| handoff: Option<String>, | |
| }, | |
| /// Close a task without doing it, and everything still open that was filed from it. | |
| Invalidate { | |
| id: String, | |
| #[arg(long, default_value = "obsolete")] | |
| reason: String, | |
| }, | |
| /// One task. | |
| Get { id: String }, | |
| /// A task with everything filed from it: subagent tasks, continuations, retries. | |
| Tree { id: String }, | |
| /// File a task again from one that dead-ended, with its chain's handoffs as lessons. | |
| Retry { id: String }, | |
| /// Rank a domain's open tasks by the rubric in force, once: the paper's cycle. | |
| /// Blockers rise above what they block, children to their parents, and curation | |
| /// keeps its floor; each move is a `rescored` event with its reason. | |
| Rescore { | |
| #[arg(long)] | |
| domain: Option<String>, | |
| }, | |
| /// How tasks flow: by family and status, why they were invalidated, and the | |
| /// requirements waiting that no completed task ever carried. | |
| Stats { | |
| #[arg(long)] | |
| domain: Option<String>, | |
| }, | |
| } | |
| type Fallible = Result<(), Box<dyn std::error::Error>>; | |
| fn json<T: serde::Serialize>(v: &T) -> Fallible { | |
| println!("{}", serde_json::to_string_pretty(v)?); | |
| Ok(()) | |
| } | |
| fn hits_json(hits: &[dm_index::Hit]) -> Vec<serde_json::Value> { | |
| hits.iter() | |
| .map(|h| { | |
| serde_json::json!({ | |
| "id": h.id, "domain": h.domain, "type": h.entry_type.to_string(), | |
| "trust_tier": h.trust_tier.to_string(), | |
| "classification": h.classification.to_string(), | |
| "title": h.title, "summary": h.summary, | |
| }) | |
| }) | |
| .collect() | |
| } | |
| fn default_owner() -> String { | |
| let user = std::env::var("USER") | |
| .or_else(|_| std::env::var("USERNAME")) | |
| .unwrap_or_else(|_| "me".into()); | |
| format!("human:local:{user}") | |
| } | |
| fn body_or_stdin(body: Option<String>) -> std::io::Result<String> { | |
| match body { | |
| Some(b) => Ok(b), | |
| None => { | |
| let mut s = String::new(); | |
| std::io::stdin().read_to_string(&mut s)?; | |
| Ok(s) | |
| } | |
| } | |
| } | |
| /// The command line, with any profile it names spelled out as flags, parsed. The root is | |
| /// found as `Cli` finds it: `--root`, `MEMORY_ROOT`, then `~/memory`. | |
| fn parse_with_profiles(args: Vec<OsString>) -> Result<Cli, String> { | |
| let commands = command_words(); | |
| let root = profiles_root(&args); | |
| let cli = Cli::command(); | |
| // which of a command's flags take a value, as its definition says | |
| let takes_value = |command: &str, flag: &str| { | |
| let long = flag.trim_start_matches('-'); | |
| cli.find_subcommand(command) | |
| .and_then(|c| c.get_arguments().find(|a| a.get_long() == Some(long))) | |
| .is_some_and(|a| a.get_action().takes_values()) | |
| }; | |
| let argv = profiles::expand(args, &commands, &takes_value, || match &root { | |
| Some(r) => profiles::load(r, &commands), | |
| None => Ok(profiles::builtin()), | |
| })?; | |
| Ok(Cli::parse_from(argv)) | |
| } | |
| /// The words that are commands, which no profile may take: clap's own `help` too, though | |
| /// it is not listed until the parser is built. | |
| fn command_words() -> Vec<String> { | |
| Cli::command() | |
| .get_subcommands() | |
| .map(|c| c.get_name().to_string()) | |
| .chain(std::iter::once("help".to_string())) | |
| .collect() | |
| } | |
| /// The root a command line names, before it is parsed. | |
| fn profiles_root(args: &[OsString]) -> Option<PathBuf> { | |
| let mut it = args.iter().skip(1); | |
| while let Some(a) = it.next() { | |
| if a == "--root" { | |
| return it.next().map(PathBuf::from); | |
| } | |
| if let Some(r) = a.to_str().and_then(|s| s.strip_prefix("--root=")) { | |
| return Some(PathBuf::from(r)); | |
| } | |
| } | |
| std::env::var_os("MEMORY_ROOT") | |
| .map(PathBuf::from) | |
| .or_else(credentials::default_root) | |
| } | |
| fn main() { | |
| // The relay unshares a pid namespace, which only a single-threaded process may do, | |
| // so it runs before any runtime starts. | |
| let args: Vec<OsString> = std::env::args_os().collect(); | |
| if args.get(1).is_some_and(|a| a == "relay") || args.get(3).is_some_and(|a| a == "relay") { | |
| let cli = Cli::parse(); | |
| if let Some(Command::Relay { | |
| socket, | |
| listen, | |
| argv, | |
| }) = cli.command | |
| { | |
| match relay(&socket, &listen, &argv) { | |
| Ok(code) => std::process::exit(code), | |
| Err(e) => { | |
| eprintln!("relay: {e}"); | |
| std::process::exit(1); | |
| } | |
| } | |
| } | |
| } | |
| // A hook needs no record and no runtime: it reads one event and writes one file. | |
| if args.get(1).is_some_and(|a| a == "hook") { | |
| let outbox = std::env::var_os("MEMORY_OUTBOX") | |
| .map(PathBuf::from) | |
| .or_else(|| home_dir().map(|h| h.join(hooks::OUTBOX))); | |
| let harness = args.get(2).and_then(|a| a.to_str()).unwrap_or("claude"); | |
| match outbox { | |
| Some(outbox) => { | |
| if let Err(e) = hooks::run(&outbox, harness) { | |
| eprintln!("amem: hook: {e}"); | |
| } | |
| } | |
| None => eprintln!("amem: hook: no MEMORY_OUTBOX and no HOME"), | |
| } | |
| std::process::exit(0); | |
| } | |
| // Traces, when a collector is named; the guard flushes at exit. After the relay | |
| // and the hook: neither runs a runtime, and the relay may not have a second thread. | |
| let _otel = dm_otel::init("amem"); | |
| let cli = match parse_with_profiles(args) { | |
| Ok(cli) => cli, | |
| Err(e) => { | |
| eprintln!("error: {e}"); | |
| std::process::exit(2); | |
| } | |
| }; | |
| let Some(root) = cli.root.clone().or_else(credentials::default_root) else { | |
| eprintln!("error: give --root, or set MEMORY_ROOT; there is no home for ~/memory"); | |
| std::process::exit(1); | |
| }; | |
| // one spelling of the root: the launcher's proof, its state and its sessions name it | |
| let root = std::fs::canonicalize(&root).unwrap_or(root); | |
| // the environment is written here, while this is the only thread | |
| let at_start = credentials::KNOWN | |
| .iter() | |
| .filter(|(k, _)| credentials::present(std::env::var(k).ok()).is_some()) | |
| .map(|(k, _)| (*k).to_string()) | |
| .collect::<Vec<_>>(); | |
| credentials::load(&root); | |
| // The command futures are large and Windows gives the main thread one megabyte, so | |
| // the CLI runs on a thread with room; the runtime starts there. | |
| let outcome = std::thread::Builder::new() | |
| .name("amem".into()) | |
| .stack_size(64 << 20) | |
| .spawn(move || run_cli(cli, root, at_start).map_err(|e| e.to_string())) | |
| .and_then(|h| { | |
| h.join() | |
| .map_err(|_| std::io::Error::other("the CLI thread panicked")) | |
| }); | |
| match outcome { | |
| Ok(Ok(())) => {} | |
| Ok(Err(e)) => { | |
| eprintln!("error: {e}"); | |
| std::process::exit(1); | |
| } | |
| Err(e) => { | |
| eprintln!("error: {e}"); | |
| std::process::exit(1); | |
| } | |
| } | |
| } | |
| /// What is wrong with an invocation before anything runs: amem's own flags given to a | |
| /// command, which would ignore them, or a root that is not there for a command that | |
| /// needs one. | |
| fn refuse_invocation(cli: &Cli, root: &Path) -> Fallible { | |
| let Some(command) = &cli.command else { | |
| return Ok(()); | |
| }; | |
| if cli.open.no_open || cli.open.workdir.is_some() { | |
| return Err( | |
| "--workdir and --no-open before a command are amem's own; give them after \ | |
| the command that takes them, as `amem web --workdir <dir>`" | |
| .into(), | |
| ); | |
| } | |
| if !root.exists() && !matches!(command, Command::Init { .. } | Command::Doctor { .. }) { | |
| return Err(format!( | |
| "no record at {}: run `amem init` to make one there, or give --root (or MEMORY_ROOT)", | |
| root.display() | |
| ) | |
| .into()); | |
| } | |
| Ok(()) | |
| } | |
| /// `amem serve`: the root over MCP on standard input and output, as an author, holding | |
| /// the task `MEMORY_TASK` names if a launcher set it. | |
| async fn serve_stdio_cmd(root: &Path, author: Option<String>, manifest: Vec<String>) -> Fallible { | |
| let mut m = Memory::open(root)?.with_manifest(manifest); | |
| if let Some(a) = author { | |
| m = m.with_author(a); | |
| } | |
| // a harness working a task has it in MEMORY_TASK, as the launcher sets it; | |
| // what this session files or writes is judged against that task | |
| if let Some(t) = std::env::var("MEMORY_TASK").ok().filter(|t| !t.is_empty()) { | |
| m.set_held_task(&t); | |
| } | |
| m.set_tools(taps::TapTools::for_root(root)); | |
| mcp::serve_stdio(Arc::new(m)).await?; | |
| Ok(()) | |
| } | |
| /// `amem init`, `amem usage` and `amem cost`: the root made, and what it has spent. | |
| async fn bookkeeping_cmd(root: &Path, command: Command) -> Fallible { | |
| let root = root.to_path_buf(); | |
| match command { | |
| Command::Init { owner, domain } => { | |
| let m = Memory::init(&root, &owner.unwrap_or_else(default_owner), &domain)?; | |
| let prices = root.join("prices.toml"); | |
| if !prices.exists() { | |
| std::fs::write(&prices, cost::TEMPLATE)?; | |
| } | |
| println!( | |
| "initialised {} for {} with domain {domain}", | |
| m.root().display(), | |
| m.author() | |
| ); | |
| Ok(()) | |
| } | |
| Command::Usage => { | |
| let t = Telemetry::open(&root.join("state/telemetry.db"))?; | |
| print_usage(&t.summary()?); | |
| Ok(()) | |
| } | |
| Command::Cost { json: as_json } => { | |
| let r = cost::report(&root).await?; | |
| if as_json { | |
| json(&r) | |
| } else { | |
| print!("{}", cost::render(&r)); | |
| Ok(()) | |
| } | |
| } | |
| _ => unreachable!("routed by run_cli"), | |
| } | |
| } | |
| #[tokio::main] | |
| async fn run_cli(cli: Cli, root: PathBuf, at_start: Vec<String>) -> Fallible { | |
| refuse_invocation(&cli, &root)?; | |
| let Some(command) = cli.command else { | |
| return web_cmd( | |
| &root, | |
| Command::Web { | |
| listen: None, | |
| no_open: cli.open.no_open, | |
| session_command: None, | |
| hosted: cli.open.hosted, | |
| harness: None, | |
| auth: AuthArg::Subscription, | |
| backend: BackendArg::Auto, | |
| workdir: cli.open.workdir, | |
| }, | |
| ) | |
| .await; | |
| }; | |
| match command { | |
| cmd @ (Command::Init { .. } | Command::Usage | Command::Cost { .. }) => { | |
| bookkeeping_cmd(&root, cmd).await | |
| } | |
| Command::Serve { author, manifest } => serve_stdio_cmd(&root, author, manifest).await, | |
| cmd @ (Command::Launch { .. } | Command::Work { .. } | Command::Inside { .. }) => { | |
| launch_cmd(&root, cmd).await | |
| } | |
| Command::Hook { .. } => unreachable!("handled in main before the root is needed"), | |
| cmd @ Command::Web { .. } => web_cmd(&root, cmd).await, | |
| Command::Doctor { json, offline } => doctor::run(&root, &at_start, json, offline).await, | |
| Command::Profiles { json: as_json } => { | |
| profiles::show(&profiles::load(&root, &command_words())?, as_json) | |
| } | |
| cmd @ (Command::Mine { .. } | |
| | Command::Check { .. } | |
| | Command::Curate { .. } | |
| | Command::Replay { .. } | |
| | Command::Models { .. } | |
| | Command::WslcApiCheck { .. } | |
| | Command::Savings { .. } | |
| | Command::Golden { .. } | |
| | Command::Recall { | |
| provider: Some(_), .. | |
| }) => tools_cmd(&root, cmd).await, | |
| Command::Image { op } => match op { | |
| ImageOp::Unpack { image } => { | |
| image::unpack(&root, &image)?; | |
| Ok(()) | |
| } | |
| ImageOp::Path => match image::present(&root) { | |
| Some(p) => { | |
| println!("{}", p.display()); | |
| Ok(()) | |
| } | |
| None => Err("no unpacked image; run `amem image unpack`".into()), | |
| }, | |
| }, | |
| Command::Relay { .. } => unreachable!("handled in main"), | |
| Command::Task { op } => task(&Memory::open(&root)?, op).await, | |
| Command::Transcript { op } => transcripts::cmd(&Memory::open(&root)?, op).await, | |
| Command::Reenact { op } => reenact::cmd(&root, op).await, | |
| Command::Log { op } => eventlog::cmd(&Memory::open(&root)?, op).await, | |
| Command::Gofannon { op } => gofannon::cmd(&root, op).await, | |
| Command::Platform { op } => platform::cmd(&Memory::open(&root)?, op).await, | |
| Command::Tool { name, args } => sources::call(&root, &name, &args).await, | |
| Command::ToolServer { listen, token_env } => { | |
| tool_server::serve(&root, &listen, token_env.as_deref()).await | |
| } | |
| Command::Source { op } => sources::cmd(&Memory::open(&root)?, op).await, | |
| Command::Tap { op } => tap_cmd(&root, op).await, | |
| Command::Drift { op } => drift::run(&root, op).await, | |
| other => entries(&Memory::open(&root)?, other).await, | |
| } | |
| } | |
| /// What one launch needs for its model calls: the destination name as the harness will | |
| /// address it, the destination itself, the grants entry whose ceiling caps the session, | |
| /// and any credential the harness must hold itself. | |
| #[derive(Debug)] | |
| struct Route { | |
| name: String, | |
| destination: Destination, | |
| ceiling_of: String, | |
| harness_env: Vec<(String, String)>, | |
| } | |
| /// The route the memory's own model calls take: the recall deputy and mining. By default | |
| /// the harness's; `--server-auth` gives them a credential of their own, so a harness on | |
| /// a personal login can leave them to an institution's gateway. | |
| #[derive(Clone, Debug, PartialEq, Eq)] | |
| struct ServerRoute { | |
| /// How those calls authenticate; a login means they are not made at all. | |
| auth: AuthMode, | |
| /// The egress destination they go to. | |
| provider: String, | |
| } | |
| /// The server's route beside the harness's, and the destination to register for it when | |
| /// it is not the harness's own. The gateway's route is named for the gateway, so it | |
| /// never shares the harness's name; a key to a provider the harness's login already | |
| /// names would, with a different destination behind it, and is refused rather than | |
| /// guessed at. | |
| fn server_route( | |
| harness: &Route, | |
| auth: AuthMode, | |
| server_auth: Option<AuthMode>, | |
| server_provider: Option<&str>, | |
| ) -> Result<(ServerRoute, Option<Route>), Box<dyn std::error::Error>> { | |
| let follows = ServerRoute { | |
| auth, | |
| provider: harness.name.clone(), | |
| }; | |
| let Some(mode) = server_auth else { | |
| return Ok((follows, None)); | |
| }; | |
| if mode == AuthMode::Subscription { | |
| return Err( | |
| "a subscription login is the harness's alone and cannot stand in for the \ | |
| memory's own calls; --server-auth takes key or gateway" | |
| .into(), | |
| ); | |
| } | |
| if mode == AuthMode::Gateway && server_provider.is_some() { | |
| return Err( | |
| "--server-provider names a destination for --server-auth key; the gateway \ | |
| is one destination and takes no name" | |
| .into(), | |
| ); | |
| } | |
| let route = route_for(Harness::Command, mode, server_provider)?; | |
| let server = ServerRoute { | |
| auth: mode, | |
| provider: route.name.clone(), | |
| }; | |
| if route.name != harness.name { | |
| return Ok((server, Some(route))); | |
| } | |
| // the same name under the same mode is the same route, built by the same arm; under | |
| // another mode it is another destination, a login's pass-through beside a key's | |
| // injection say, which one name cannot hold | |
| if mode == auth { | |
| return Ok((server, None)); | |
| } | |
| Err(format!( | |
| "the memory's own route would share the name {} with the harness's while going \ | |
| somewhere else; use --server-auth gateway, or --server-provider with another name", | |
| route.name | |
| ) | |
| .into()) | |
| } | |
| /// What a session authenticates with: the harness's route, and the memory's own when it | |
| /// has one. | |
| #[derive(Clone, Copy, Debug)] | |
| struct Credentials<'a> { | |
| auth: AuthMode, | |
| provider: Option<&'a str>, | |
| server_auth: Option<AuthMode>, | |
| server_provider: Option<&'a str>, | |
| } | |
| impl<'a> Credentials<'a> { | |
| /// One route for everything: a tool run on the host, where the harness is the | |
| /// launcher itself. | |
| fn keyed(auth: AuthMode, provider: &'a str) -> Credentials<'a> { | |
| Credentials { | |
| auth, | |
| provider: Some(provider), | |
| server_auth: None, | |
| server_provider: None, | |
| } | |
| } | |
| } | |
| /// Whether mining runs as a prompted run of the harness: asked for, or by default when | |
| /// the memory's own calls have no credential of their own. | |
| fn via_harness(via: MineVia, server_auth: AuthMode) -> bool { | |
| match via { | |
| MineVia::Harness => true, | |
| MineVia::Api => false, | |
| MineVia::Auto => server_auth == AuthMode::Subscription, | |
| } | |
| } | |
| /// A variable's value, if it carries one: a variable set to nothing is not set. | |
| fn present(value: Option<String>) -> Option<String> { | |
| value.filter(|v| !v.trim().is_empty()) | |
| } | |
| fn need(var: &str, why: &str) -> Result<String, Box<dyn std::error::Error>> { | |
| present(std::env::var(var).ok()) | |
| .ok_or_else(|| format!("{why} needs {var} in the launcher's environment").into()) | |
| } | |
| fn bearer(value: String) -> Auth { | |
| Auth::Inject { | |
| header: axum_header("authorization"), | |
| value: format!("Bearer {value}"), | |
| } | |
| } | |
| /// The provider-shaped destination for a key: where it goes and how the key travels. | |
| fn keyed_provider(name: &str) -> Result<Destination, Box<dyn std::error::Error>> { | |
| Ok(match name { | |
| "anthropic" => Destination::forward( | |
| "https://api.anthropic.com", | |
| Auth::Inject { | |
| header: axum_header("x-api-key"), | |
| value: need("ANTHROPIC_API_KEY", "--auth key to anthropic")?, | |
| }, | |
| ), | |
| "openai" => Destination::forward( | |
| "https://api.openai.com", | |
| bearer(need("OPENAI_API_KEY", "--auth key to openai")?), | |
| ), | |
| "xai" => Destination::forward( | |
| "https://api.x.ai", | |
| bearer(need("XAI_API_KEY", "--auth key to xai")?), | |
| ), | |
| "gemini" => Destination::forward( | |
| "https://generativelanguage.googleapis.com", | |
| Auth::Inject { | |
| header: axum_header("x-goog-api-key"), | |
| value: need("GEMINI_API_KEY", "--auth key to gemini")?, | |
| }, | |
| ), | |
| // Alibaba's models over OpenAI's API, at the international endpoint; the | |
| // harness appends /v1 as it does at OpenAI | |
| "dashscope" => Destination::forward( | |
| "https://dashscope-intl.aliyuncs.com/compatible-mode", | |
| bearer(need("DASHSCOPE_API_KEY", "--auth key to dashscope")?), | |
| ), | |
| // Meta's Model API: OpenAI's shapes under /v1 and Anthropic's at the root, and | |
| // Muse Code's own model catalog at /v1/muse-code/models, so the whole host | |
| "meta" => Destination::forward( | |
| "https://api.meta.ai", | |
| bearer(need("META_API_KEY", "--auth key to meta")?), | |
| ) | |
| .with_origin_paths(), | |
| other => return Err(format!("no key-mode destination named {other}").into()), | |
| }) | |
| } | |
| fn pass_through_provider(name: &str) -> Result<Destination, Box<dyn std::error::Error>> { | |
| Ok(match name { | |
| "anthropic" => Destination::forward("https://api.anthropic.com", Auth::PassThrough), | |
| "openai" => Destination::forward("https://api.openai.com", Auth::PassThrough), | |
| // GitHub Copilot, from Copilot CLI or from OpenCode's supported Copilot login | |
| "github" | "github-copilot" => { | |
| Destination::tunnel(["api.github.com", "github.com", ".githubcopilot.com"]) | |
| } | |
| // the two logins whose backend is not the public API: forwarded in the clear and | |
| // metered like any other, with a tunnel only for the token refresh | |
| // the ChatGPT backend refuses relayed requests (Cloudflare, and Codex speaks | |
| // websockets first), so a Codex login is a tunnel, like Copilot's: pinned egress, | |
| // no metering | |
| "chatgpt" if std::env::var_os("MEMORY_CHATGPT_FORWARD").is_some() => { | |
| Destination::forward("https://chatgpt.com/backend-api", Auth::PassThrough) | |
| .with_tunnels(["auth.openai.com"]) | |
| } | |
| "chatgpt" => Destination::tunnel(["chatgpt.com", ".chatgpt.com", "auth.openai.com"]), | |
| // Gemini CLI's login is forwarded and metered through its endpoint override; | |
| // Antigravity CLI's has none, so its model host is tunnelled, pinned without | |
| // metering, as Copilot's is | |
| // a Muse login talks to the same host a key does, through --base-url, so it is | |
| // forwarded and metered, with the account's login host tunnelled for a refresh; | |
| // written from the docs and a run on a key, not from a login: the first launch | |
| // on a real login is the test | |
| "meta" => Destination::forward("https://api.meta.ai", Auth::PassThrough) | |
| .with_tunnels(["auth.meta.com"]) | |
| .with_origin_paths(), | |
| "google" => Destination::forward("https://cloudcode-pa.googleapis.com", Auth::PassThrough) | |
| .with_tunnels([ | |
| "oauth2.googleapis.com", | |
| // Antigravity's eligibility check, before anything else, on a login: | |
| // the account's profile, then its picture. The picture's host, not the | |
| // domain: a Compute Engine VM's reverse name is under googleusercontent.com | |
| // and resolves to the VM. The egress also holds the tunnel's TLS name to | |
| // the host; the paths and HTTP Host behind that name it cannot see | |
| "www.googleapis.com", | |
| "lh3.googleusercontent.com", | |
| "accounts.google.com", | |
| "cloudcode-pa.googleapis.com", | |
| "daily-cloudcode-pa.googleapis.com", | |
| ]), | |
| // a Grok Build login talks to Grok's cli-chat-proxy and websocket relays and | |
| // refreshes at auth.x.ai; a tunnel, pinned without metering, in this first | |
| // cut. The proxy has an override, GROK_CLI_CHAT_PROXY_BASE_URL, and speaks the | |
| // Responses API, so a forward with pass-through auth and tunnels only for the | |
| // websocket and login hosts is the metered route to prove on a real login | |
| "grok" => Destination::tunnel([ | |
| "cli-chat-proxy.grok.com", | |
| "code.grok.com", | |
| "grok.com", | |
| ".grok.com", | |
| "auth.x.ai", | |
| "api.x.ai", | |
| ]), | |
| // Qwen's free OAuth tier ended in April 2026; the harness exits on it | |
| "dashscope" | "qwen" => { | |
| return Err( | |
| "qwen has no login mode: Qwen's free OAuth tier ended in April 2026; \ | |
| use --auth key with DASHSCOPE_API_KEY" | |
| .into(), | |
| ) | |
| } | |
| other => return Err(format!("no subscription-mode destination named {other}").into()), | |
| }) | |
| } | |
| fn route_for( | |
| harness: Harness, | |
| auth: AuthMode, | |
| provider: Option<&str>, | |
| ) -> Result<Route, Box<dyn std::error::Error>> { | |
| let default = if auth == AuthMode::Subscription { | |
| harness.subscription_provider() | |
| } else { | |
| harness.provider() | |
| }; | |
| // through the gateway there is one destination, whatever the harness, and its | |
| // ceiling is the gateway's: the harness's base path and the egress's check agree | |
| let name = if auth == AuthMode::Gateway { | |
| "llmao".to_string() | |
| } else { | |
| provider.unwrap_or(default).to_string() | |
| }; | |
| let mut harness_env = Vec::new(); | |
| let (destination, ceiling_of) = match auth { | |
| AuthMode::Subscription => { | |
| if harness == Harness::Copilot { | |
| let (var, token) = crate::credentials::copilot_token().ok_or( | |
| "copilot needs COPILOT_GITHUB_TOKEN or GH_TOKEN in the launcher's \ | |
| environment or credentials.env, or a `gh auth login` on this host", | |
| )?; | |
| if var == "GH_TOKEN" { | |
| eprintln!( | |
| "amem: copilot holds your GitHub login's token, which reaches every \ | |
| repository the login can; a token with only the Copilot Requests \ | |
| permission, as COPILOT_GITHUB_TOKEN, keeps that out of the sandbox" | |
| ); | |
| } | |
| harness_env.push((var.to_string(), token)); | |
| } | |
| let ceiling_of = if name == "github-copilot" { | |
| "github".to_string() | |
| } else { | |
| name.clone() | |
| }; | |
| (pass_through_provider(&name)?, ceiling_of) | |
| } | |
| AuthMode::Key => (keyed_provider(&name)?, name.clone()), | |
| AuthMode::Gateway => { | |
| let key = need("LLMAO_KEY", "--auth gateway")?; | |
| let url = std::env::var("LLMAO_URL").unwrap_or_else(|_| "http://127.0.0.1:4000".into()); | |
| // a gateway translates Anthropic's shape for other models, whose chat | |
| // templates refuse Claude Code's trailing system-role message | |
| // and Muse Code asks for 128k output tokens whatever the model's window, | |
| // so a cap that leaves a 131k-context model room for the prompt; and its | |
| // Gemini adapter drops a tool result Antigravity sends under the model's role | |
| ( | |
| Destination::forward(url, bearer(key)) | |
| .with_folded_system_messages() | |
| .with_function_responses_in_user_turns() | |
| .with_max_output_tokens(32_768), | |
| "llmao".to_string(), | |
| ) | |
| } | |
| }; | |
| Ok(Route { | |
| name, | |
| destination, | |
| ceiling_of, | |
| harness_env, | |
| }) | |
| } | |
| fn axum_header(name: &'static str) -> axum::http::HeaderName { | |
| axum::http::HeaderName::from_static(name) | |
| } | |
| /// Everything a launch needs before the harness starts: the memory with its handling | |
| /// capped, the egress with its one destination, and the socket it serves on. | |
| struct Prepared { | |
| root: PathBuf, | |
| memory: Arc<Memory>, | |
| egress: Egress, | |
| /// The recall deputy, when the route gives the server a model of its own. Held | |
| /// here because the memory holds it weakly: it lives as long as the session. | |
| #[allow(dead_code)] // kept alive, not read | |
| deputy: Option<Arc<dyn Deputy>>, | |
| socket: PathBuf, | |
| home: PathBuf, | |
| /// Where the record is projected as files for the session, the launcher's own. | |
| projection: PathBuf, | |
| /// Removes the render with the session, on the error paths `finish` never reaches. | |
| _render: RemoveOnDrop, | |
| ceiling: Label, | |
| ceiling_of: String, | |
| provider: String, | |
| /// Where the memory's own calls go, and under what. | |
| server: ServerRoute, | |
| harness_env: Vec<(String, String)>, | |
| /// The model the run was given, for the adapters whose config names it. | |
| model: Option<String>, | |
| /// Whether the egress meters this route. A tunnel is pinned without metering, so | |
| /// the launcher reads what the harness reports about itself afterwards instead. | |
| metered: bool, | |
| } | |
| /// A directory removed when this is dropped. A session's render is its own and goes with | |
| /// it, whichever way the session ends short of the process being killed. | |
| struct RemoveOnDrop(Option<PathBuf>); | |
| impl Drop for RemoveOnDrop { | |
| fn drop(&mut self) { | |
| if let Some(dir) = &self.0 { | |
| std::fs::remove_dir_all(dir).ok(); | |
| } | |
| } | |
| } | |
| /// Whether a launch renders the projection, and under what name. A tool run on the host, | |
| /// `recall` or `golden` say, mounts nothing and renders nothing. | |
| #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| enum Projected<'a> { | |
| No, | |
| /// Under this session's id. | |
| AsSession, | |
| /// Under a name the launcher outside chose, for a backend that mounts the directory | |
| /// before the session inside it exists. | |
| Named(&'a str), | |
| } | |
| /// A projection's directory name as the command line gives it: one path component, so it | |
| /// cannot name a directory outside the harness's own. | |
| fn projection_name(name: &str) -> Result<&str, Box<dyn std::error::Error>> { | |
| if !name.is_empty() | |
| && name.len() <= 64 | |
| && name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-') | |
| { | |
| Ok(name) | |
| } else { | |
| Err(format!("--projection {name:?} is not a name: letters, digits and '-'").into()) | |
| } | |
| } | |
| #[allow(clippy::too_many_arguments)] // one argument per thing a launch is given | |
| async fn prepare_launch( | |
| root: &Path, | |
| harness: Harness, | |
| creds: Credentials<'_>, | |
| socket: Option<PathBuf>, | |
| recall_model: Option<&str>, | |
| projected: Projected<'_>, | |
| ) -> Result<Prepared, Box<dyn std::error::Error>> { | |
| let auth = creds.auth; | |
| let route = route_for(harness, auth, creds.provider)?; | |
| let (server, extra) = server_route(&route, auth, creds.server_auth, creds.server_provider)?; | |
| let Route { | |
| name, | |
| destination, | |
| ceiling_of, | |
| harness_env, | |
| } = route; | |
| let memory = Memory::open(root)?.with_author(format!("agent:{}", harness.binary())); | |
| let ceiling = memory.grants().ceiling(&ceiling_of).ok_or_else(|| { | |
| format!("grants.toml has no [destinations.{ceiling_of}]; add one with a ceiling") | |
| })?; | |
| let home = root.join("state/sandbox").join(harness.binary()); | |
| // one directory per session: two sessions of a harness at different ceilings would | |
| // otherwise each read what the other rendered | |
| let projection_dir = match projected { | |
| Projected::Named(n) => projection_name(n)?.to_string(), | |
| Projected::No | Projected::AsSession => memory.session_id().to_string(), | |
| }; | |
| let projection = root | |
| .join(dm_sandbox::PROJECTION) | |
| .join(harness.binary()) | |
| .join(projection_dir); | |
| // a name from outside is a directory made empty for this session; one with a render | |
| // in it is another session's, whose recall pages and handling this one would take over | |
| if matches!(projected, Projected::Named(_)) | |
| && std::fs::read_dir(&projection).is_ok_and(|mut d| d.next().is_some()) | |
| { | |
| return Err(format!( | |
| "the projection {} already holds a render; a session's is its own", | |
| projection.display() | |
| ) | |
| .into()); | |
| } | |
| let memory = memory.with_handling_cap(ceiling); | |
| let memory = Arc::new(if projected != Projected::No { | |
| memory.with_projection(projection.clone()) | |
| } else { | |
| memory | |
| }); | |
| let metered = !destination.upstream.is_empty(); | |
| let mut destinations = HashMap::new(); | |
| destinations.insert(name.clone(), destination); | |
| if let Some(extra) = extra { | |
| destinations.insert(extra.name, extra.destination); | |
| } | |
| // the hosted tools' servers leave through the same egress, as the launcher's own | |
| // destinations, never the harness's | |
| let egress = Egress::with_tool_servers( | |
| memory.clone(), | |
| destinations, | |
| hosted::tool_servers(memory.grants())?, | |
| &root.join("state/telemetry.db"), | |
| )?; | |
| // a key or the gateway is the launcher's credential, so the server may spend a | |
| // small model on recall; a subscription login is the harness's alone | |
| let deputy: Option<Arc<dyn Deputy>> = (server.auth != AuthMode::Subscription).then(|| { | |
| Arc::new(deputy::EgressDeputy::new( | |
| egress.clone(), | |
| &server.provider, | |
| recall_model, | |
| )) as Arc<dyn Deputy> | |
| }); | |
| deputy.iter().for_each(|d| memory.set_deputy(d)); | |
| memory.set_tools(taps::TapTools::with_egress(root, egress.clone())); | |
| let run_dir = run_dir(root)?; | |
| Ok(Prepared { | |
| memory, | |
| egress, | |
| deputy, | |
| socket: socket | |
| .unwrap_or_else(|| run_dir.join(format!("egress-{}.sock", std::process::id()))), | |
| root: root.to_path_buf(), | |
| home, | |
| _render: RemoveOnDrop((projected != Projected::No).then(|| projection.clone())), | |
| projection, | |
| ceiling, | |
| ceiling_of, | |
| provider: name, | |
| server, | |
| harness_env, | |
| model: None, | |
| metered, | |
| }) | |
| } | |
| /// Where launchers keep their egress sockets, made if need be, with what killed | |
| /// launchers left there swept. | |
| fn run_dir(root: &Path) -> std::io::Result<PathBuf> { | |
| let dir = root.join("state/run"); | |
| std::fs::create_dir_all(&dir)?; | |
| sweep_sockets(&dir); | |
| Ok(dir) | |
| } | |
| /// Remove the egress sockets killed launchers left in `run_dir`. One goes only when it is | |
| /// named `egress-<pid>.sock`, as [`prepare_launch`] names it, it is a socket and not a | |
| /// link, and the launcher its name carries is not a running process. Returns the | |
| /// sockets removed. | |
| fn sweep_sockets(run_dir: &Path) -> Vec<PathBuf> { | |
| let Ok(entries) = std::fs::read_dir(run_dir) else { | |
| return Vec::new(); | |
| }; | |
| let own = std::process::id(); | |
| entries | |
| .flatten() | |
| .filter_map(|e| { | |
| let name = e.file_name(); | |
| let digits = name | |
| .to_str()? | |
| .strip_prefix("egress-")? | |
| .strip_suffix(".sock")?; | |
| if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) { | |
| return None; | |
| } | |
| let pid: u32 = digits.parse().ok()?; | |
| if pid == 0 || pid == own || dm_sandbox::stop::process_runs(pid) { | |
| return None; | |
| } | |
| #[cfg(unix)] | |
| { | |
| use std::os::unix::fs::FileTypeExt; | |
| if !e.file_type().ok()?.is_socket() { | |
| return None; | |
| } | |
| // a pid names a process in this pid namespace only: a launcher in | |
| // another, sharing the root, still listens | |
| if !dm_sandbox::stop::socket_refuses(&e.path()) { | |
| return None; | |
| } | |
| } | |
| std::fs::remove_file(e.path()).ok()?; | |
| Some(e.path()) | |
| }) | |
| .collect() | |
| } | |
| /// What a tunnelled harness reported about itself, read from the transcript it left and | |
| /// recorded beside what the egress observed, marked as the harness's own count. | |
| fn record_reported_usage(p: &Prepared) { | |
| let Some(t) = mine::newest_transcript(&p.home) else { | |
| return; | |
| }; | |
| let Ok(text) = std::fs::read_to_string(&t) else { | |
| return; | |
| }; | |
| let reported = harness_usage::from_transcript(&text); | |
| for usage in reported.by_model.values() { | |
| if let Err(e) = | |
| p.egress | |
| .telemetry() | |
| .record_reported(p.memory.session_id(), &p.provider, usage) | |
| { | |
| eprintln!("amem: could not record reported usage: {e}"); | |
| } | |
| } | |
| if let Some((pct, window)) = reported.plan_used_percent { | |
| eprintln!( | |
| "amem: the harness reports {pct:.1}% of its plan's {}-hour window used", | |
| window / 60 | |
| ); | |
| } | |
| } | |
| /// `amem usage`: the telemetry by destination, model and who counted. | |
| fn print_usage(rows: &[dm_egress::telemetry::Totals]) { | |
| if rows.is_empty() { | |
| println!("no model calls recorded"); | |
| return; | |
| } | |
| println!( | |
| "{:<12} {:<28} {:<8} {:>6} {:>10} {:>10} {:>10} {:>10} {:>9}", | |
| "destination", | |
| "model", | |
| "source", | |
| "calls", | |
| "input", | |
| "output", | |
| "cache_rd", | |
| "cache_wr", | |
| "ms/call" | |
| ); | |
| for r in rows { | |
| // a harness's own report carries no timing: its calls are not averaged in | |
| let timed = if r.source == dm_egress::telemetry::SOURCE_HARNESS { | |
| "-".to_string() | |
| } else { | |
| (r.call_ms / r.calls.max(1)).to_string() | |
| }; | |
| println!( | |
| "{:<12} {:<28} {:<8} {:>6} {:>10} {:>10} {:>10} {:>10} {:>9}", | |
| r.destination, | |
| r.model, | |
| r.source, | |
| r.calls, | |
| r.input_tokens, | |
| r.output_tokens, | |
| r.cache_read_tokens, | |
| r.cache_write_tokens, | |
| timed | |
| ); | |
| } | |
| } | |
| fn report_usage(egress: &Egress) { | |
| let Ok(rows) = egress.telemetry().summary() else { | |
| return; | |
| }; | |
| if rows.is_empty() { | |
| return; | |
| } | |
| eprintln!("amem: usage this root, by destination and model:"); | |
| for r in rows { | |
| eprintln!( | |
| " {:<10} {:<26} {:<8} calls {:>4} in {:>8} out {:>8} cache {:>8}", | |
| r.destination, | |
| r.model, | |
| r.source, | |
| r.calls, | |
| r.input_tokens, | |
| r.output_tokens, | |
| r.cache_read_tokens | |
| ); | |
| } | |
| } | |
| #[allow(clippy::too_many_arguments)] // one argument per thing a run is given | |
| async fn run_confined( | |
| p: &Prepared, | |
| harness: Harness, | |
| auth: AuthMode, | |
| workdir: &Path, | |
| args: &[String], | |
| env: &[(String, String)], | |
| capture: bool, | |
| runner: &Runner, | |
| ) -> Result<Ran, Box<dyn std::error::Error>> { | |
| // counted until the mount below is gone, so a launcher told to stop waits for it | |
| let _in_flight = stops::InFlight::enter()?; | |
| let host_home = home_dir(); | |
| let rootfs = match runner { | |
| Runner::Image(rootfs) => Some(rootfs.as_path()), | |
| _ => None, | |
| }; | |
| let memory_binary = std::env::current_exe()?; | |
| p.memory.project_into(&p.projection).await?; | |
| // the render is mounted live where the host can serve it, as files otherwise | |
| let live = live_mount(p, harness); | |
| let projection = live | |
| .as_ref() | |
| .map_or(p.projection.as_path(), dm_mount::Mounted::at); | |
| let req = dm_sandbox::Request { | |
| harness, | |
| auth, | |
| provider: Some(p.provider.as_str()), | |
| workdir, | |
| home: &p.home, | |
| egress_socket: &p.socket, | |
| memory_binary: &memory_binary, | |
| destination_ceiling: p.ceiling, | |
| harness_args: args, | |
| host_home: host_home.as_deref(), | |
| harness_env: env, | |
| model: p.model.as_deref(), | |
| image: false, | |
| workdir_inside: None, | |
| rootfs, | |
| projection: Some(projection), | |
| }; | |
| let mut plan = match runner { | |
| // no network namespace: the relay takes a loopback port of its own on the host's | |
| Runner::Seatbelt => dm_sandbox::prepare_in( | |
| &req, | |
| dm_sandbox::Layout::seatbelt(&req, &free_loopback_port()?), | |
| )?, | |
| _ => dm_sandbox::prepare(&req)?, | |
| }; | |
| // the hook frees a recall page of this session only | |
| plan.env | |
| .insert("MEMORY_SESSION".into(), p.memory.session_id().to_string()); | |
| if live.is_some() { | |
| // the mount's open is the get; the hook keeps its refusals and makes no second one | |
| plan.env.insert("MEMORY_PROJECTION_LIVE".into(), "1".into()); | |
| } | |
| if std::env::var_os("MEMORY_EGRESS_DEBUG").is_some() { | |
| eprintln!("launch debug: argv {:?}", plan.argv); | |
| } | |
| let ran = tokio::task::spawn_blocking(move || { | |
| if capture { | |
| dm_sandbox::run_captured(&plan).map(|(code, out, r)| { | |
| ( | |
| code, | |
| Some(String::from_utf8_lossy(&out).into_owned()), | |
| Some(r), | |
| ) | |
| }) | |
| } else { | |
| dm_sandbox::run(&plan).map(|(code, r)| (code, None, Some(r))) | |
| } | |
| }) | |
| .await?; | |
| // unmounted once the sandbox has gone, before the render it was served over | |
| drop(live); | |
| Ok(ran?) | |
| } | |
| /// The session's render served as a live mount, where this host can: every open of an | |
| /// entry inside is then a `get` the memory serves, whatever tool made it, and the hook | |
| /// is told it need not make one. Where it cannot, the render is mounted as files, as | |
| /// before, and the launch says why. Mounts killed launchers left under the root's mount | |
| /// directory, of any harness, are cleared first. | |
| fn live_mount(p: &Prepared, harness: Harness) -> Option<dm_mount::Mounted> { | |
| dm_mount::sweep_root(&p.root); | |
| let base = p.root.join(dm_mount::MOUNTS).join(harness.binary()); | |
| let at = base.join(p.projection.file_name().unwrap_or_default()); | |
| match dm_mount::mount(p.memory.clone(), &p.projection, &at) { | |
| Ok(m) => { | |
| eprintln!( | |
| "amem: the projection at {} is a live mount: every open of an entry is a get", | |
| dm_sandbox::INSIDE_PROJECTION | |
| ); | |
| Some(m) | |
| } | |
| Err(e) => { | |
| eprintln!( | |
| "amem: the projection at {} is a render: {e}", | |
| dm_sandbox::INSIDE_PROJECTION | |
| ); | |
| None | |
| } | |
| } | |
| } | |
| /// A run's exit code, its captured output when asked for, and what it cost the host | |
| /// where the runner could measure it. | |
| type Ran = (i32, Option<String>, Option<dm_sandbox::Resources>); | |
| /// A loopback address no listener holds now, for a relay on the host's own loopback. | |
| /// The port is free when asked and taken by the relay a moment later; another process | |
| /// could take it between, and the relay then says it cannot listen rather than share it. | |
| fn free_loopback_port() -> std::io::Result<String> { | |
| let probe = std::net::TcpListener::bind("127.0.0.1:0")?; | |
| Ok(probe.local_addr()?.to_string()) | |
| } | |
| /// `web`: the web terminal. | |
| async fn web_cmd(root: &Path, command: Command) -> Fallible { | |
| let Command::Web { | |
| listen, | |
| no_open, | |
| session_command, | |
| hosted, | |
| harness, | |
| auth, | |
| backend, | |
| workdir, | |
| } = command | |
| else { | |
| unreachable!("routed by run_cli") | |
| }; | |
| if Memory::open(root).is_err() { | |
| return Err(format!( | |
| "no record at {}: run `amem init` to make one, or give --root; `amem doctor` says more", | |
| root.display() | |
| ) | |
| .into()); | |
| } | |
| let wanted = resolve_workdir(root, workdir); | |
| let opening = open::Opening { | |
| browser: !no_open, | |
| hosted: hosted.as_deref().map(open::Hosted::parse).transpose()?, | |
| }; | |
| let mut state = open::state(root).await?; | |
| if open_running( | |
| root, | |
| &state, | |
| listen.as_deref(), | |
| wanted.as_ref().ok(), | |
| &opening, | |
| ) | |
| .await? | |
| { | |
| return Ok(()); | |
| } | |
| let workdir = wanted?; | |
| let listener = open::bind(listen.as_deref()).await?; | |
| let url = format!("http://{}", listener.local_addr()?); | |
| state.url = Some(url.clone()); | |
| state.workdir = Some(workdir.display().to_string()); | |
| dm_web::state::save(root, &state)?; | |
| let origins = opening | |
| .hosted | |
| .iter() | |
| .map(|h| h.origin().to_string()) | |
| .collect(); | |
| let access = dm_web::Access::new(root, &state, origins); | |
| let code = access.pairing_code()?; | |
| let config = web_config( | |
| root, | |
| workdir, | |
| (harness, auth, backend), | |
| access, | |
| session_command, | |
| )?; | |
| eprintln!( | |
| "amem: web launcher over {} · sessions start in {} · ctrl-c stops it", | |
| root.display(), | |
| config.workdir.display() | |
| ); | |
| open::show(&url, &code, &opening); | |
| tokio::select! { | |
| r = dm_web::serve(config, listener) => r?, | |
| _ = tokio::signal::ctrl_c() => {} | |
| } | |
| // the address goes with the process, and only this process's address: the token | |
| // stays for the next server, and no browser key survives it | |
| let mut after = dm_web::state::load(root)?.state; | |
| if after.token == state.token && after.url.as_deref() == Some(url.as_str()) { | |
| after.url = None; | |
| after.workdir = None; | |
| dm_web::state::save(root, &after)?; | |
| } | |
| Ok(()) | |
| } | |
| /// One server per root: when it is already up and proves it, the browser goes to it, and | |
| /// a second one is not started beside it, whatever address was asked for. Whether it was. | |
| async fn open_running( | |
| root: &Path, | |
| state: &dm_web::state::WebState, | |
| listen: Option<&str>, | |
| wanted: Option<&PathBuf>, | |
| opening: &open::Opening, | |
| ) -> Result<bool, Box<dyn std::error::Error>> { | |
| let Some(url) = &state.url else { | |
| return Ok(false); | |
| }; | |
| let Some(running) = open::running(url, &state.token, root).await else { | |
| return Ok(false); | |
| }; | |
| if listen.is_some_and(|l| format!("http://{l}") != running.url) { | |
| return Err(format!( | |
| "a launcher over this root is already running at {}; one per root", | |
| running.url | |
| ) | |
| .into()); | |
| } | |
| let pairing = open::pair(&running, &state.token) | |
| .await | |
| .ok_or("the running launcher gave no pairing code")?; | |
| if let Some(h) = &opening.hosted { | |
| if !pairing.origins.iter().any(|o| o == h.origin()) { | |
| return Err(format!( | |
| "the launcher running at {} was not started with --hosted {}, so that page \ | |
| cannot reach it: stop it (ctrl-c where it runs) and run amem again", | |
| running.url, | |
| h.origin() | |
| ) | |
| .into()); | |
| } | |
| } | |
| if let Some(w) = wanted { | |
| if pairing.workdir != w.display().to_string() { | |
| eprintln!( | |
| "amem: already running over this root, with sessions starting in {}, not {}; \ | |
| stop it (ctrl-c where it runs) to start them here", | |
| pairing.workdir, | |
| w.display() | |
| ); | |
| } | |
| } | |
| open::show(&running.url, &pairing.code, opening); | |
| Ok(true) | |
| } | |
| /// The server's configuration. `session_command`, for the browser tests, runs in each | |
| /// session's terminal in place of the launcher. | |
| fn web_config( | |
| root: &Path, | |
| workdir: PathBuf, | |
| (harness, auth, backend): (Option<HarnessArg>, AuthArg, BackendArg), | |
| access: dm_web::Access, | |
| session_command: Option<String>, | |
| ) -> Result<dm_web::Config, Box<dyn std::error::Error>> { | |
| let backend_name = match backend { | |
| BackendArg::Auto => "auto", | |
| BackendArg::Namespaces => "namespaces", | |
| BackendArg::Image => "image", | |
| BackendArg::Wslc => "wslc", | |
| BackendArg::Seatbelt => "seatbelt", | |
| BackendArg::None => "none", | |
| }; | |
| Ok(dm_web::Config { | |
| root: root.to_path_buf(), | |
| memory_binary: std::env::current_exe()?, | |
| workdir, | |
| harness: harness.map(|h| Harness::from(h).binary().to_string()), | |
| auth: match AuthMode::from(auth) { | |
| AuthMode::Subscription => "subscription", | |
| AuthMode::Key => "key", | |
| AuthMode::Gateway => "gateway", | |
| } | |
| .to_string(), | |
| backend: backend_name.to_string(), | |
| command_override: session_command | |
| .map(|c| c.split_whitespace().map(str::to_string).collect::<Vec<_>>()) | |
| .filter(|c| !c.is_empty()), | |
| access, | |
| }) | |
| } | |
| /// The auth mode a deputy may run under. A subscription login is the harness's alone, | |
| /// so naming a provider with it would quietly give no deputy; refuse it instead. | |
| fn deputy_auth(auth: AuthArg) -> Result<AuthMode, Box<dyn std::error::Error>> { | |
| match AuthMode::from(auth) { | |
| AuthMode::Subscription => Err("a subscription login is the harness's alone and cannot \ | |
| stand in for the deputy; use --auth key or --auth gateway with --provider" | |
| .into()), | |
| mode => Ok(mode), | |
| } | |
| } | |
| /// The suite `golden` scores: a file, or a verified `eval` artifact in the platform. | |
| async fn golden_suite( | |
| root: &Path, | |
| suite: Option<PathBuf>, | |
| platform: Option<String>, | |
| ) -> Result<golden::Suite, Box<dyn std::error::Error>> { | |
| match (suite, platform) { | |
| (Some(path), _) => golden::Suite::load(&path), | |
| (None, Some(name)) => { | |
| let a = Memory::open(root)? | |
| .platform_artifact("eval", &name) | |
| .await? | |
| .ok_or_else(|| { | |
| format!("the platform holds no verified eval artifact named {name}") | |
| })?; | |
| eprintln!("amem: the suite is the platform's {}", a.entry); | |
| golden::Suite::parse(&a.text) | |
| } | |
| (None, None) => Err("name a suite, or --platform".into()), | |
| } | |
| } | |
| /// `golden`: the suite against this root, with the deputy's brief when a provider is | |
| /// named. | |
| async fn golden_cmd( | |
| root: &Path, | |
| suite: golden::Suite, | |
| k: usize, | |
| as_json: bool, | |
| provider: Option<String>, | |
| auth: AuthArg, | |
| model: Option<&str>, | |
| ) -> Fallible { | |
| let r = match provider { | |
| Some(provider) => { | |
| let p = prepare_launch( | |
| root, | |
| Harness::Command, | |
| Credentials::keyed(deputy_auth(auth)?, &provider), | |
| None, | |
| model, | |
| Projected::No, | |
| ) | |
| .await?; | |
| // the suite's reads are a measurement, not a session's | |
| p.memory.disable_read_log(); | |
| golden::score(&p.memory, &suite, k).await? | |
| } | |
| None => golden::score(&Memory::open(root)?.without_read_log(), &suite, k).await?, | |
| }; | |
| if as_json { | |
| println!("{}", serde_json::to_string_pretty(&r)?); | |
| } else { | |
| print!("{}", golden::render(&r)); | |
| } | |
| Ok(()) | |
| } | |
| /// `tap install` and `tap run`. | |
| async fn tap_cmd(root: &Path, op: TapOp) -> Fallible { | |
| match op { | |
| TapOp::Install { name, file } => { | |
| let dest = taps::install(root, &name, &file)?; | |
| println!("{}", dest.display()); | |
| Ok(()) | |
| } | |
| TapOp::Run { | |
| name, | |
| config, | |
| every, | |
| } => taps::run(root, &name, config.as_deref(), every).await, | |
| } | |
| } | |
| /// `mine` and the C API check. | |
| /// `amem mine`: a transcript read by a small model through the session's destination. | |
| async fn mine_cmd( | |
| root: &Path, | |
| transcript: &Path, | |
| auth: AuthArg, | |
| provider: &str, | |
| model: Option<String>, | |
| ) -> Fallible { | |
| let p = prepare_launch( | |
| root, | |
| Harness::Command, | |
| Credentials::keyed(auth.into(), provider), | |
| None, | |
| None, | |
| Projected::No, | |
| ) | |
| .await?; | |
| let client = work::client(p.memory.clone()).await?; | |
| let model = model::resolve(&p.egress, &p.provider, model.as_deref()).await; | |
| let kept = transcripts::keep_file(&p.memory, "mined", None, None, transcript).await?; | |
| let ids = mine::mine( | |
| &p.egress, | |
| &client, | |
| &p.provider, | |
| &model, | |
| transcript, | |
| Some(&kept.session), | |
| ) | |
| .await?; | |
| println!("{}", serde_json::to_string_pretty(&ids)?); | |
| Ok(()) | |
| } | |
| /// `amem models`: the destination's own model list, as the web form's picker reads it. | |
| async fn models_cmd(root: &Path, auth: AuthArg, provider: Option<String>) -> Fallible { | |
| let auth = AuthMode::from(auth); | |
| if auth == AuthMode::Subscription { | |
| return Err("a login is the harness's own and lists no models here; name the model, or leave it to the harness".into()); | |
| } | |
| let provider = provider.unwrap_or_else(|| { | |
| if auth == AuthMode::Gateway { | |
| "llmao".to_string() | |
| } else { | |
| "openai".to_string() | |
| } | |
| }); | |
| let (destination, models) = list_models(root, auth, &provider).await?; | |
| println!( | |
| "{}", | |
| serde_json::json!({ "destination": destination, "models": models }) | |
| ); | |
| Ok(()) | |
| } | |
| /// The models a destination lists, through the egress as a session would reach it. | |
| async fn list_models( | |
| root: &Path, | |
| auth: AuthMode, | |
| provider: &str, | |
| ) -> Result<(String, Vec<String>), Box<dyn std::error::Error>> { | |
| let p = prepare_launch( | |
| root, | |
| Harness::Command, | |
| Credentials::keyed(auth, provider), | |
| None, | |
| None, | |
| Projected::No, | |
| ) | |
| .await?; | |
| // Google lists under v1beta; everyone else speaks OpenAI's list, Anthropic included | |
| let path = if matches!(p.provider.as_str(), "gemini" | "google") { | |
| "v1beta/models" | |
| } else { | |
| "v1/models" | |
| }; | |
| let v = p.egress.get(&p.provider, path).await?; | |
| Ok((p.provider.clone(), model::model_ids(&v))) | |
| } | |
| async fn tools_cmd(root: &Path, command: Command) -> Fallible { | |
| match command { | |
| Command::Models { auth, provider } => models_cmd(root, auth, provider).await, | |
| cmd @ (Command::Check { .. } | Command::Curate { .. } | Command::Replay { .. }) => { | |
| curation_cmd(root, cmd).await | |
| } | |
| Command::Mine { | |
| transcript, | |
| auth, | |
| provider, | |
| model, | |
| } => mine_cmd(root, &transcript, auth, &provider, model).await, | |
| Command::Recall { | |
| question, | |
| domain, | |
| depth, | |
| provider: Some(provider), | |
| auth, | |
| model, | |
| } => { | |
| let p = prepare_launch( | |
| root, | |
| Harness::Command, | |
| Credentials::keyed(deputy_auth(auth)?, &provider), | |
| None, | |
| model.as_deref(), | |
| Projected::No, | |
| ) | |
| .await?; | |
| let r = p | |
| .memory | |
| .recall( | |
| &question, | |
| domain.as_deref(), | |
| depth.map(Into::into), | |
| Budget::default(), | |
| ) | |
| .await?; | |
| json(&recall::to_json(&r)) | |
| } | |
| Command::WslcApiCheck { | |
| image, | |
| wslc_storage, | |
| refresh, | |
| } => wslc_api_check(root, &image, wslc_storage.as_deref(), refresh), | |
| Command::Golden { | |
| suite, | |
| platform, | |
| k, | |
| json: as_json, | |
| provider, | |
| auth, | |
| model, | |
| } => { | |
| let suite = golden_suite(root, suite, platform).await?; | |
| golden_cmd(root, suite, k, as_json, provider, auth, model.as_deref()).await | |
| } | |
| Command::Savings { | |
| query, | |
| json: as_json, | |
| judge_provider, | |
| judge_auth, | |
| judge_model, | |
| judge_sample, | |
| } => { | |
| let judge = judge_provider.map(|p| (p, judge_auth, judge_model, judge_sample)); | |
| savings_cmd(root, query.as_deref(), as_json, judge).await | |
| } | |
| _ => unreachable!("routed by run_cli"), | |
| } | |
| } | |
| /// `amem savings`, with the judge through a destination when one is named. | |
| async fn savings_cmd( | |
| root: &Path, | |
| query: Option<&str>, | |
| as_json: bool, | |
| judge: Option<(String, AuthArg, Option<String>, usize)>, | |
| ) -> Fallible { | |
| let r = match judge { | |
| Some((provider, auth, model, sample)) => { | |
| let p = prepare_launch( | |
| root, | |
| Harness::Command, | |
| Credentials::keyed(deputy_auth(auth)?, &provider), | |
| None, | |
| None, | |
| Projected::No, | |
| ) | |
| .await?; | |
| p.memory.disable_read_log(); | |
| let deputy = deputy::EgressDeputy::new(p.egress.clone(), &p.provider, model.as_deref()); | |
| let judge = savings::Judge { | |
| deputy: &deputy, | |
| sample, | |
| }; | |
| savings::measure(root, query, Some(judge)).await? | |
| } | |
| None => savings::measure(root, query, None).await?, | |
| }; | |
| if as_json { | |
| println!("{}", serde_json::to_string_pretty(&r)?); | |
| } else { | |
| print!("{}", savings::render(&r)); | |
| } | |
| Ok(()) | |
| } | |
| /// `amem replay`. | |
| #[allow(clippy::too_many_arguments)] // one per flag of the command | |
| async fn replay_cmd( | |
| root: &Path, | |
| session: &str, | |
| turn: Option<usize>, | |
| limit: usize, | |
| auth: AuthArg, | |
| provider: Option<String>, | |
| model: Option<String>, | |
| ) -> Fallible { | |
| let auth = AuthMode::from(auth); | |
| let provider = checker_destination(auth, provider); | |
| let p = prepare_launch( | |
| root, | |
| Harness::Command, | |
| Credentials::keyed(auth, &provider), | |
| None, | |
| None, | |
| Projected::No, | |
| ) | |
| .await?; | |
| p.memory.disable_read_log(); | |
| let model = model::resolve(&p.egress, &p.provider, model.as_deref()).await; | |
| // the decision contexts are read as the owner; the calls go through the egress | |
| let owner = Memory::open(root)?.without_read_log(); | |
| // the egress's record when the session has one; its transcript otherwise | |
| let contexts = owner.contexts(session).await.unwrap_or_default(); | |
| let run = if let Some(first) = contexts.first() { | |
| let to = replay::retarget(&first.destination, &p.provider); | |
| if to.is_none() && first.destination != p.provider { | |
| return Err(format!( | |
| "{session}'s calls went to {}; replay them there with --auth key --provider {}, or on the gateway with --auth gateway", | |
| first.destination, first.destination | |
| ) | |
| .into()); | |
| } | |
| replay::replay_contexts( | |
| &p.egress, | |
| &owner, | |
| session, | |
| &model, | |
| turn, | |
| limit, | |
| to.as_deref(), | |
| ) | |
| .await? | |
| } else { | |
| replay::replay(&p.egress, &owner, session, &p.provider, &model, turn, limit).await? | |
| }; | |
| json(&run) | |
| } | |
| /// The curation commands: the checker, and the cycle. | |
| async fn curation_cmd(root: &Path, command: Command) -> Fallible { | |
| match command { | |
| Command::Check { | |
| auth, | |
| provider, | |
| model, | |
| domain, | |
| sample, | |
| file, | |
| contradictions, | |
| quarantine, | |
| } => { | |
| let what = CheckWhat { | |
| file, | |
| contradictions, | |
| quarantine, | |
| }; | |
| check_cmd(root, auth, provider, model, domain, sample, what).await | |
| } | |
| Command::Curate { | |
| domain, | |
| auth, | |
| provider, | |
| model, | |
| sample, | |
| contradictions, | |
| budget_tasks, | |
| budget_tokens, | |
| drift, | |
| workdir, | |
| every, | |
| } => { | |
| let plan = CuratePlan { | |
| domain, | |
| auth, | |
| provider, | |
| model, | |
| sample, | |
| contradictions, | |
| budget_tasks, | |
| budget_tokens, | |
| drift, | |
| workdir: workdir.or_else(|| std::env::current_dir().ok()), | |
| }; | |
| curate_loop(root, &plan, every).await | |
| } | |
| Command::Replay { | |
| session, | |
| turn, | |
| limit, | |
| auth, | |
| provider, | |
| model, | |
| } => replay_cmd(root, &session, turn, limit, auth, provider, model).await, | |
| _ => unreachable!("routed by tools_cmd"), | |
| } | |
| } | |
| /// `wslc-api-check`: the C API end to end, on Windows. | |
| fn wslc_api_check(root: &Path, image: &str, storage: Option<&Path>, refresh: bool) -> Fallible { | |
| #[cfg(windows)] | |
| { | |
| let storage = wslc_api::storage_for(root, storage)?; | |
| println!("{}", wslc_api::check(&storage, image, refresh)?); | |
| Ok(()) | |
| } | |
| #[cfg(not(windows))] | |
| { | |
| let _ = (root, image, storage, refresh); | |
| Err(dm_wslc_api::WINDOWS_ONLY.into()) | |
| } | |
| } | |
| /// `launch`, `work` and `inside`: start a session, run it once or per task, exit with | |
| /// the code. On the `wslc` backend the host hands the whole thing to the egress | |
| /// container, which runs this same code as `inside`. | |
| async fn launch_cmd(root: &Path, command: Command) -> Fallible { | |
| match command { | |
| Command::Launch { .. } => cmd_launch(root, command).await, | |
| Command::Work { .. } => cmd_work(root, command).await, | |
| Command::Inside { .. } => cmd_inside(root, command).await, | |
| _ => unreachable!("only launch, work and inside are routed here"), | |
| } | |
| } | |
| /// A `launch` the `wslc` backend runs, when that is where it runs: the egress container | |
| /// is told what to run, and everything else happens inside it. | |
| fn wslc_for_launch( | |
| root: &Path, | |
| command: &Command, | |
| ) -> Result<Option<wslc::Launch>, Box<dyn std::error::Error>> { | |
| let Command::Launch { | |
| harness, | |
| auth, | |
| provider, | |
| server_auth, | |
| server_provider, | |
| workdir, | |
| unconfined, | |
| backend, | |
| image, | |
| wslc_via, | |
| wslc_storage, | |
| prompt, | |
| prompt_file, | |
| model, | |
| subagents, | |
| no_handoff, | |
| args, | |
| .. | |
| } = command | |
| else { | |
| return Ok(None); | |
| }; | |
| if backend_of(*backend, *unconfined, root, (*harness).into()) != Backend::Wslc { | |
| return Ok(None); | |
| } | |
| let mut inside_args = run_options_args(model.as_deref(), *subagents); | |
| inside_args.extend(inside_args_for_launch( | |
| prompt.as_deref(), | |
| prompt_file.as_deref(), | |
| *no_handoff, | |
| args, | |
| )?); | |
| Ok(Some(wslc::Launch { | |
| projection: wslc::projection_name(), | |
| root: root.to_path_buf(), | |
| harness: (*harness).into(), | |
| auth: (*auth).into(), | |
| provider: provider.clone(), | |
| server_auth: server_auth.map(AuthMode::from), | |
| server_provider: server_provider.clone(), | |
| workdir: resolve_workdir(root, workdir.clone())?, | |
| image: image.clone(), | |
| inside_args, | |
| interactive: prompt.is_none() && prompt_file.is_none(), | |
| via: *wslc_via, | |
| storage: wslc_storage.clone(), | |
| })) | |
| } | |
| /// A `work` the `wslc` backend runs, when that is where it runs. | |
| fn wslc_for_work( | |
| root: &Path, | |
| command: &Command, | |
| ) -> Result<Option<wslc::Launch>, Box<dyn std::error::Error>> { | |
| let Command::Work { | |
| harness, | |
| auth, | |
| provider, | |
| server_auth, | |
| server_provider, | |
| workdir, | |
| unconfined, | |
| backend, | |
| image, | |
| wslc_via, | |
| wslc_storage, | |
| domain, | |
| lease_seconds, | |
| keep_going, | |
| idle_seconds, | |
| model, | |
| subagents, | |
| no_prefetch, | |
| .. | |
| } = command | |
| else { | |
| return Ok(None); | |
| }; | |
| if backend_of(*backend, *unconfined, root, (*harness).into()) != Backend::Wslc { | |
| return Ok(None); | |
| } | |
| let mut inside_args = work_inside_args( | |
| run_options_args(model.as_deref(), *subagents), | |
| domain.as_deref(), | |
| *lease_seconds, | |
| *keep_going, | |
| *idle_seconds, | |
| ); | |
| if *no_prefetch { | |
| inside_args.push("--no-prefetch".into()); | |
| } | |
| Ok(Some(wslc::Launch { | |
| projection: wslc::projection_name(), | |
| root: root.to_path_buf(), | |
| harness: (*harness).into(), | |
| auth: (*auth).into(), | |
| provider: provider.clone(), | |
| server_auth: server_auth.map(AuthMode::from), | |
| server_provider: server_provider.clone(), | |
| workdir: resolve_workdir(root, workdir.clone())?, | |
| image: image.clone(), | |
| inside_args, | |
| interactive: false, | |
| via: *wslc_via, | |
| storage: wslc_storage.clone(), | |
| })) | |
| } | |
| /// A session the web launcher started runs confined unless the operator allowed | |
| /// otherwise: a browser, or a profile it picked, asking for no confinement is refused. | |
| /// `auto` on a platform with no backend is what the command line gets too, and stands. | |
| fn refuse_browser_unconfined(unconfined: bool, backend: BackendArg) -> Fallible { | |
| let asked = unconfined || matches!(backend, BackendArg::None); | |
| let allowed = std::env::var("AMEM_WEB_ALLOW_UNCONFINED").is_ok_and(|v| v == "1"); | |
| if asked && status::web_session().is_some() && !allowed { | |
| return Err( | |
| "a session started from the browser runs confined; the operator allows \ | |
| otherwise with `amem web --backend none`" | |
| .into(), | |
| ); | |
| } | |
| Ok(()) | |
| } | |
| async fn cmd_launch(root: &Path, command: Command) -> Fallible { | |
| if let Some(l) = wslc_for_launch(root, &command)? { | |
| std::process::exit(wslc::run(&l)?); | |
| } | |
| let Command::Launch { | |
| harness, | |
| auth, | |
| provider, | |
| server_auth, | |
| server_provider, | |
| workdir, | |
| unconfined, | |
| backend, | |
| prompt, | |
| prompt_file, | |
| model, | |
| subagents, | |
| no_handoff, | |
| empty_memory, | |
| mine: mine_after, | |
| mine_model, | |
| mine_via, | |
| args, | |
| .. | |
| } = command | |
| else { | |
| unreachable!("routed by launch_cmd") | |
| }; | |
| let workdir = resolve_workdir(root, workdir)?; | |
| refuse_browser_unconfined(unconfined, backend)?; | |
| let prompt = match (prompt, prompt_file) { | |
| (Some(p), _) => Some(p), | |
| (None, Some(f)) => Some(std::fs::read_to_string(&f)?), | |
| (None, None) => None, | |
| }; | |
| let (program, args) = split_program(harness.into(), args); | |
| let stops = stops::Stops::listen()?; | |
| let session = Session::start( | |
| root, | |
| harness.into(), | |
| Credentials { | |
| auth: auth.into(), | |
| provider: provider.as_deref(), | |
| server_auth: server_auth.map(AuthMode::from), | |
| server_provider: server_provider.as_deref(), | |
| }, | |
| workdir, | |
| runner_for(backend_of(backend, unconfined, root, harness.into()), root)?, | |
| RunOptions::new(model, subagents, program, None).empty_memory(empty_memory), | |
| ) | |
| .await?; | |
| let code = stops | |
| .around( | |
| async { | |
| let code = match prompt { | |
| Some(prompt) => work::prompted(&session, &prompt, &args, no_handoff).await?, | |
| None => work::interactive(&session, &args).await?, | |
| }; | |
| if mine_after { | |
| session.mine(mine_model.as_deref(), mine_via).await?; | |
| } | |
| Ok(code) | |
| }, | |
| session.abandon(), | |
| ) | |
| .await?; | |
| session.finish(code).await; | |
| std::process::exit(code); | |
| } | |
| /// The `amem inside` flags for a task run, after the run options. | |
| fn work_inside_args( | |
| mut args: Vec<String>, | |
| domain: Option<&str>, | |
| lease_seconds: i64, | |
| keep_going: bool, | |
| idle_seconds: u64, | |
| ) -> Vec<String> { | |
| args.push("--work".to_string()); | |
| if let Some(d) = domain { | |
| args.extend(["--domain".to_string(), d.to_string()]); | |
| } | |
| args.extend(["--lease-seconds".to_string(), lease_seconds.to_string()]); | |
| if keep_going { | |
| args.push("--loop".into()); | |
| } | |
| args.extend(["--idle-seconds".to_string(), idle_seconds.to_string()]); | |
| args | |
| } | |
| async fn cmd_work(root: &Path, command: Command) -> Fallible { | |
| if let Some(l) = wslc_for_work(root, &command)? { | |
| std::process::exit(wslc::run(&l)?); | |
| } | |
| let Command::Work { | |
| harness, | |
| auth, | |
| provider, | |
| server_auth, | |
| server_provider, | |
| workdir, | |
| unconfined, | |
| backend, | |
| domain, | |
| lease_seconds, | |
| keep_going, | |
| idle_seconds, | |
| model, | |
| subagents, | |
| mine: mine_after, | |
| mine_model, | |
| mine_via, | |
| no_prefetch, | |
| task, | |
| .. | |
| } = command | |
| else { | |
| unreachable!("routed by launch_cmd") | |
| }; | |
| let workdir = resolve_workdir(root, workdir)?; | |
| refuse_browser_unconfined(unconfined, backend)?; | |
| let stops = stops::Stops::listen()?; | |
| let session = Session::start( | |
| root, | |
| harness.into(), | |
| Credentials { | |
| auth: auth.into(), | |
| provider: provider.as_deref(), | |
| server_auth: server_auth.map(AuthMode::from), | |
| server_provider: server_provider.as_deref(), | |
| }, | |
| workdir, | |
| runner_for(backend_of(backend, unconfined, root, harness.into()), root)?, | |
| RunOptions::new(model, subagents, Vec::new(), None), | |
| ) | |
| .await?; | |
| let options = work::Options { | |
| domain, | |
| lease_seconds, | |
| keep_going, | |
| idle: std::time::Duration::from_secs(idle_seconds), | |
| mine: mine_after.then(|| work::MinePlan { | |
| model: mine_model, | |
| via_harness: session.mines_via_harness(mine_via), | |
| }), | |
| prefetch: !no_prefetch, | |
| task, | |
| }; | |
| let code = stops | |
| .around(work::run(&session, &options), session.abandon()) | |
| .await?; | |
| session.finish(code).await; | |
| std::process::exit(code); | |
| } | |
| async fn cmd_inside(root: &Path, command: Command) -> Fallible { | |
| let Command::Inside { | |
| harness, | |
| auth, | |
| provider, | |
| server_auth, | |
| server_provider, | |
| requests, | |
| host_home, | |
| workdir_inside, | |
| projection, | |
| prompt, | |
| prompt_file, | |
| no_handoff, | |
| work, | |
| model, | |
| subagents, | |
| domain, | |
| lease_seconds, | |
| keep_going, | |
| idle_seconds, | |
| no_prefetch, | |
| args, | |
| } = command | |
| else { | |
| unreachable!("routed by launch_cmd") | |
| }; | |
| let runner = Runner::Requests(RequestsRunner { | |
| dir: requests, | |
| host_home, | |
| workdir_inside, | |
| next: std::sync::atomic::AtomicUsize::new(0), | |
| }); | |
| // the harness container's working tree is at the inside path; this process | |
| // never touches it, so any existing directory serves as the host-side value | |
| let (program, args) = split_program(harness.into(), args); | |
| let session = Session::start( | |
| root, | |
| harness.into(), | |
| Credentials { | |
| auth: auth.into(), | |
| provider: provider.as_deref(), | |
| server_auth: server_auth.map(AuthMode::from), | |
| server_provider: server_provider.as_deref(), | |
| }, | |
| PathBuf::from("/"), | |
| runner, | |
| RunOptions::new(model, subagents, program, projection), | |
| ) | |
| .await?; | |
| let code = if work { | |
| let options = work::Options { | |
| domain, | |
| lease_seconds, | |
| keep_going, | |
| idle: std::time::Duration::from_secs(idle_seconds), | |
| mine: None, | |
| prefetch: !no_prefetch, | |
| task: None, | |
| }; | |
| work::run(&session, &options).await? | |
| } else { | |
| let prompt = match (prompt, prompt_file) { | |
| (Some(p), _) => Some(p), | |
| (None, Some(f)) => Some(std::fs::read_to_string(&f)?), | |
| (None, None) => None, | |
| }; | |
| match prompt { | |
| Some(prompt) => work::prompted(&session, &prompt, &args, no_handoff).await?, | |
| None => work::interactive(&session, &args).await?, | |
| } | |
| }; | |
| session.finish(code).await; | |
| std::process::exit(code); | |
| } | |
| /// A command harness's program is fixed for the session and everything else is a run's | |
| /// arguments; for the other harnesses all of it is the run's. | |
| /// The run options as arguments for the `inside` process of the `wslc` backend. | |
| fn run_options_args(model: Option<&str>, subagents: SubagentsArg) -> Vec<String> { | |
| let mut out = Vec::new(); | |
| if let Some(m) = model { | |
| out.extend(["--model".to_string(), m.to_string()]); | |
| } | |
| if subagents == SubagentsArg::Board { | |
| out.extend(["--subagents".to_string(), "board".to_string()]); | |
| } | |
| out | |
| } | |
| /// What a run is given beyond the harness and its auth: a model, where subagents go, | |
| /// and, for a `command` harness, the program and its fixed arguments. | |
| #[derive(Clone, Debug, Default)] | |
| struct RunOptions { | |
| model: Option<String>, | |
| subagents: SubagentsArg, | |
| program: Vec<String>, | |
| /// The projection's directory name, when the launcher outside chose it. | |
| projection: Option<String>, | |
| /// Run over an empty record made for this run, for an evaluation's baseline. | |
| empty_memory: bool, | |
| /// A private home of the run's own, for a re-run, which must not leave its | |
| /// transcript among the harness's real sessions. | |
| home: Option<PathBuf>, | |
| } | |
| impl RunOptions { | |
| fn new( | |
| model: Option<String>, | |
| subagents: SubagentsArg, | |
| program: Vec<String>, | |
| projection: Option<String>, | |
| ) -> RunOptions { | |
| RunOptions { | |
| model, | |
| subagents, | |
| program, | |
| projection, | |
| empty_memory: false, | |
| home: None, | |
| } | |
| } | |
| fn home(mut self, home: PathBuf) -> RunOptions { | |
| self.home = Some(home); | |
| self | |
| } | |
| fn empty_memory(mut self, yes: bool) -> RunOptions { | |
| self.empty_memory = yes; | |
| self | |
| } | |
| } | |
| /// An empty record of the same owner and domain as `root`, under the system's temporary | |
| /// directory, for a run that should have everything but memory. | |
| fn empty_record(root: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> { | |
| let grants = Memory::open(root)?.grants().clone(); | |
| let domain = grants | |
| .domains | |
| .keys() | |
| .next() | |
| .cloned() | |
| .ok_or("the record grants no domain to copy")?; | |
| let dir = std::env::temp_dir().join(format!( | |
| "amem-empty-{}-{}", | |
| std::process::id(), | |
| time::OffsetDateTime::now_utc().unix_timestamp_nanos() | |
| )); | |
| std::fs::create_dir_all(&dir)?; | |
| Memory::init(&dir, &grants.owner, &domain)?; | |
| eprintln!("amem: empty memory at {}", dir.display()); | |
| Ok(dir) | |
| } | |
| /// The environment the run options add: the board for subagents, and for Claude Code | |
| /// the model as every role's default, since it picks a model per role and a gateway | |
| /// that serves one model answers only for that one. | |
| fn apply_run_options(p: &mut Prepared, harness: Harness, auth: AuthMode, options: &RunOptions) { | |
| p.model.clone_from(&options.model); | |
| if harness == Harness::Claude && auth == AuthMode::Gateway { | |
| // Claude Code asks for `high` effort by default, which only Anthropic's models | |
| // take; a gateway's open models take low, medium and xhigh, so medium goes in | |
| // the environment, and `-- --effort <level>` still overrides it | |
| p.harness_env | |
| .push(("CLAUDE_CODE_EFFORT_LEVEL".to_string(), "medium".to_string())); | |
| // Claude Code assumes a 200k window and compacts too late for the open models | |
| // behind a gateway; llmao's own notes say 120k against a 131k window | |
| p.harness_env.push(( | |
| "CLAUDE_CODE_MAX_CONTEXT_TOKENS".to_string(), | |
| "120000".to_string(), | |
| )); | |
| } | |
| if options.subagents == SubagentsArg::Board { | |
| // the pre-tool hook files a subagent call on the board and refuses it | |
| p.harness_env | |
| .push(("MEMORY_SUBAGENTS".to_string(), "board".to_string())); | |
| } | |
| if let (Some(m), Harness::Claude) = (&options.model, harness) { | |
| for k in [ | |
| "ANTHROPIC_MODEL", | |
| "ANTHROPIC_DEFAULT_OPUS_MODEL", | |
| "ANTHROPIC_DEFAULT_SONNET_MODEL", | |
| "ANTHROPIC_DEFAULT_HAIKU_MODEL", | |
| "ANTHROPIC_SMALL_FAST_MODEL", | |
| "CLAUDE_CODE_SUBAGENT_MODEL", | |
| ] { | |
| p.harness_env.push((k.to_string(), m.clone())); | |
| } | |
| } | |
| } | |
| fn split_program(harness: Harness, args: Vec<String>) -> (Vec<String>, Vec<String>) { | |
| if harness == Harness::Command { | |
| (args, Vec::new()) | |
| } else { | |
| (Vec::new(), args) | |
| } | |
| } | |
| /// The flags `amem inside` gets for a launch handed to the wslc backend. | |
| fn inside_args_for_launch( | |
| prompt: Option<&str>, | |
| prompt_file: Option<&Path>, | |
| no_handoff: bool, | |
| args: &[String], | |
| ) -> Result<Vec<String>, Box<dyn std::error::Error>> { | |
| let mut inside_args = Vec::new(); | |
| if let Some(p) = prompt { | |
| inside_args.extend(["--prompt".to_string(), p.to_string()]); | |
| } | |
| if let Some(f) = prompt_file { | |
| inside_args.extend(["--prompt".to_string(), std::fs::read_to_string(f)?]); | |
| } | |
| if no_handoff { | |
| inside_args.push("--no-handoff".into()); | |
| } | |
| inside_args.push("--".into()); | |
| inside_args.extend(args.iter().cloned()); | |
| Ok(inside_args) | |
| } | |
| /// The working tree, canonical. A tree that holds the record root, or sits inside it, | |
| /// is refused: the tree is mounted read-write, and the root, its record, its state and | |
| /// its projection are the launcher's alone. | |
| fn resolve_workdir( | |
| root: &Path, | |
| workdir: Option<PathBuf>, | |
| ) -> Result<PathBuf, Box<dyn std::error::Error>> { | |
| let w = match workdir { | |
| Some(w) => w, | |
| None => std::env::current_dir()?, | |
| }; | |
| let w = std::fs::canonicalize(&w)?; | |
| let root = std::fs::canonicalize(root).unwrap_or_else(|_| root.to_path_buf()); | |
| if root.starts_with(&w) || w.starts_with(&root) { | |
| return Err(format!( | |
| "the working tree {} and the record root {} overlap: sessions would reach the \ | |
| record by the side door. Run amem from a project directory, or pass --workdir", | |
| w.display(), | |
| root.display() | |
| ) | |
| .into()); | |
| } | |
| Ok(w) | |
| } | |
| /// The model as the harness names it on this route. OpenCode and pi name a model by its | |
| /// provider, which here is the destination. Antigravity CLI takes a model outside | |
| /// Google's catalog only as `gemini-api://<model>`, sent to its base URL as | |
| /// `v1beta/models/<model>`, and a gateway's models are never in that catalog. | |
| fn model_for(harness: Harness, auth: AuthMode, provider: &str, model: &str) -> String { | |
| match harness { | |
| Harness::OpenCode | Harness::Pi if !model.contains('/') => format!("{provider}/{model}"), | |
| Harness::Antigravity if auth == AuthMode::Gateway && !model.contains("://") => { | |
| format!("gemini-api://{model}") | |
| } | |
| _ => model.to_string(), | |
| } | |
| } | |
| /// Which backend a launch resolves to. | |
| #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| enum Backend { | |
| Namespaces, | |
| Image, | |
| Wslc, | |
| Seatbelt, | |
| None, | |
| } | |
| /// Which backend a launch resolves to. `auto` on Linux is the unpacked image when there | |
| /// is one that carries the harness, so homogeneity is the default once you have it, and | |
| /// the host's binaries otherwise: Antigravity CLI, say, which the image does not carry. | |
| /// On macOS it is Seatbelt over the host's binaries. | |
| fn backend_of(arg: BackendArg, unconfined: bool, root: &Path, harness: Harness) -> Backend { | |
| if unconfined { | |
| return Backend::None; | |
| } | |
| match arg { | |
| BackendArg::Namespaces => Backend::Namespaces, | |
| BackendArg::Image => Backend::Image, | |
| BackendArg::Wslc => Backend::Wslc, | |
| BackendArg::Seatbelt => Backend::Seatbelt, | |
| BackendArg::None => Backend::None, | |
| BackendArg::Auto => { | |
| if cfg!(target_os = "linux") { | |
| if image::present(root).is_some_and(|r| image::carries(&r, harness)) { | |
| Backend::Image | |
| } else { | |
| Backend::Namespaces | |
| } | |
| } else if cfg!(target_os = "macos") { | |
| Backend::Seatbelt | |
| } else if cfg!(windows) { | |
| Backend::Wslc | |
| } else { | |
| Backend::None | |
| } | |
| } | |
| } | |
| } | |
| fn runner_for(backend: Backend, root: &Path) -> Result<Runner, Box<dyn std::error::Error>> { | |
| Ok(match backend { | |
| Backend::Namespaces => Runner::Namespaces, | |
| Backend::Seatbelt => Runner::Seatbelt, | |
| Backend::Image => Runner::Image( | |
| image::present(root).ok_or("no unpacked image; run `amem image unpack` first")?, | |
| ), | |
| Backend::None | Backend::Wslc => Runner::Unconfined(None), | |
| }) | |
| } | |
| /// The line a session starts with: where the harness runs, where its calls go, and the | |
| /// memory session it is. | |
| fn announce(runner: &Runner, p: &Prepared, harness: Harness, workdir: &Path) { | |
| let how = match runner { | |
| Runner::Namespaces => "confined to", | |
| Runner::Seatbelt => "confined by Seatbelt to", | |
| Runner::Image(_) => "confined, in the harness image, to", | |
| Runner::Unconfined(_) => "unconfined in", | |
| Runner::Requests(_) => "in the harness image, working tree at", | |
| }; | |
| let shown = match runner { | |
| Runner::Requests(r) => r.workdir_inside.clone(), | |
| _ => workdir.to_path_buf(), | |
| }; | |
| eprintln!( | |
| "amem: {} {how} {} · model calls via {} at ceiling {} · session {}", | |
| harness.binary(), | |
| shown.display(), | |
| p.ceiling_of, | |
| p.ceiling, | |
| p.memory.session_id() | |
| ); | |
| } | |
| /// The status a session started from the web launcher writes for it. | |
| fn start_status( | |
| root: &Path, | |
| p: &Prepared, | |
| harness: Harness, | |
| auth: AuthMode, | |
| model: Option<String>, | |
| ) -> Option<status::Writer> { | |
| status::Writer::start( | |
| root, | |
| status::Fixed { | |
| memory_session: p.memory.session_id().to_string(), | |
| harness: harness.binary().to_string(), | |
| auth: wslc::auth_name(auth).to_string(), | |
| destination: p.provider.clone(), | |
| ceiling_of: p.ceiling_of.clone(), | |
| ceiling: p.ceiling, | |
| metered: p.metered, | |
| model, | |
| }, | |
| p.memory.clone(), | |
| ) | |
| } | |
| /// The model a verified `harness` artifact in the platform's domain names for this | |
| /// harness, `{"model": "..."}`, when the launch names none. | |
| async fn platform_model(memory: &Memory, harness: Harness) -> Option<String> { | |
| let a = memory | |
| .platform_artifact("harness", harness.binary()) | |
| .await | |
| .ok() | |
| .flatten()?; | |
| let v: serde_json::Value = serde_json::from_str(&a.text).ok()?; | |
| let model = v.get("model")?.as_str()?.to_string(); | |
| eprintln!("amem: model {model} from the platform's {}", a.entry); | |
| Some(model) | |
| } | |
| /// A started session: the memory opened, the egress serving, the harness not yet run. | |
| /// `launch` runs it once; `work` runs it once per task. | |
| pub struct Session { | |
| p: Prepared, | |
| harness: Harness, | |
| auth: AuthMode, | |
| workdir: PathBuf, | |
| runner: Runner, | |
| /// For a `command` harness: the program and its fixed arguments, which every run | |
| /// starts with; a prompt or task comes after them. | |
| program: Vec<String>, | |
| /// The model, in the harness's own flag, after every run's arguments: a flag after | |
| /// the prompt is where every harness, subcommand or not, accepts one. | |
| model: Option<String>, | |
| /// Serving the egress socket, for every runner but the unconfined one. | |
| #[cfg(unix)] | |
| serve_unix: Option<tokio::task::JoinHandle<Result<(), dm_egress::Error>>>, | |
| /// What the session says about itself to the web launcher that started it. | |
| #[allow(dead_code)] // kept for its drop, which writes the last word | |
| status: Option<status::Writer>, | |
| } | |
| /// How a session runs the harness. | |
| pub enum Runner { | |
| /// Native Linux namespaces, from this host's binaries. | |
| Namespaces, | |
| /// Native Linux namespaces, pivoted into this unpacked image. | |
| Image(PathBuf), | |
| /// Seatbelt on macOS, from this host's binaries at their own paths. | |
| Seatbelt, | |
| /// On the host, with the egress on a loopback TCP port. | |
| Unconfined( | |
| Option<( | |
| std::net::SocketAddr, | |
| tokio::task::JoinHandle<Result<(), dm_egress::Error>>, | |
| )>, | |
| ), | |
| /// Inside the egress container: each run is a request the host turns into a harness | |
| /// container. | |
| Requests(RequestsRunner), | |
| } | |
| impl Runner { | |
| /// The runner's name in a run's record. | |
| fn name(&self) -> &'static str { | |
| match self { | |
| Runner::Namespaces => "namespaces", | |
| Runner::Image(_) => "image", | |
| Runner::Seatbelt => "seatbelt", | |
| Runner::Unconfined(_) => "unconfined", | |
| Runner::Requests(_) => "requests", | |
| } | |
| } | |
| } | |
| /// The `Requests` runner's state. | |
| pub struct RequestsRunner { | |
| /// The shared volume. | |
| dir: PathBuf, | |
| /// The host's login directories, read-only, for the subscription modes. | |
| host_home: Option<PathBuf>, | |
| /// Where the working tree appears in the harness container. | |
| workdir_inside: PathBuf, | |
| /// The next request number. | |
| next: std::sync::atomic::AtomicUsize, | |
| } | |
| impl Session { | |
| async fn start( | |
| root: &Path, | |
| harness: Harness, | |
| creds: Credentials<'_>, | |
| workdir: PathBuf, | |
| mut runner: Runner, | |
| options: RunOptions, | |
| ) -> Result<Session, Box<dyn std::error::Error>> { | |
| let socket = match &runner { | |
| Runner::Requests(r) => Some(r.dir.join("egress.sock")), | |
| _ => None, | |
| }; | |
| let auth = creds.auth; | |
| let projected = options | |
| .projection | |
| .as_deref() | |
| .map_or(Projected::AsSession, Projected::Named); | |
| let empty; | |
| let root = if options.empty_memory { | |
| empty = empty_record(root)?; | |
| empty.as_path() | |
| } else { | |
| root | |
| }; | |
| let mut p = prepare_launch(root, harness, creds, socket, None, projected).await?; | |
| let mut options = options; | |
| if options.model.is_none() { | |
| options.model = platform_model(&p.memory, harness).await; | |
| } | |
| apply_run_options(&mut p, harness, auth, &options); | |
| if let Some(h) = &options.home { | |
| p.home.clone_from(h); | |
| } | |
| // bound here, so a socket that cannot be bound fails the launch with its own error; | |
| // the unconfined harness reaches the egress over TCP and needs no socket | |
| #[cfg(unix)] | |
| let serve_unix = if matches!(runner, Runner::Unconfined(_)) { | |
| None | |
| } else { | |
| let listener = Egress::bind_unix(&p.socket)?; | |
| let egress = p.egress.clone(); | |
| Some(tokio::spawn(async move { egress.serve(listener).await })) | |
| }; | |
| if let Runner::Requests(r) = &runner { | |
| // the harness container runs as uid 1000 and must reach the socket and leave | |
| // its exit code beside it; this process is root in the egress container | |
| open_to_all(&r.dir, 0o1777)?; | |
| open_to_all(&p.socket, 0o666)?; | |
| } | |
| #[cfg(not(unix))] | |
| if matches!( | |
| runner, | |
| Runner::Namespaces | Runner::Image(_) | Runner::Seatbelt | Runner::Requests(_) | |
| ) { | |
| return Err( | |
| "confinement needs Linux; run under WSL2, or pass --backend wslc or --unconfined" | |
| .into(), | |
| ); | |
| } | |
| if let Runner::Unconfined(slot) = &mut runner { | |
| let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; | |
| let addr = listener.local_addr()?; | |
| let egress = p.egress.clone(); | |
| *slot = Some(( | |
| addr, | |
| tokio::spawn(async move { egress.serve(listener).await }), | |
| )); | |
| } | |
| announce(&runner, &p, harness, &workdir); | |
| let status = start_status(root, &p, harness, auth, options.model.clone()); | |
| Ok(Session { | |
| p, | |
| harness, | |
| auth, | |
| workdir, | |
| runner, | |
| program: options.program, | |
| model: options.model, | |
| #[cfg(unix)] | |
| serve_unix, | |
| status, | |
| }) | |
| } | |
| /// The opened memory, for a client in the same process. | |
| pub fn memory(&self) -> Arc<Memory> { | |
| self.p.memory.clone() | |
| } | |
| /// Which harness. | |
| pub fn harness(&self) -> Harness { | |
| self.harness | |
| } | |
| /// Where the harness's hooks leave notes. | |
| pub fn outbox(&self) -> PathBuf { | |
| self.p.home.join(hooks::OUTBOX) | |
| } | |
| /// The destination the session's model calls go to. | |
| pub fn provider(&self) -> String { | |
| self.p.provider.clone() | |
| } | |
| /// The egress, for the launcher's own calls. | |
| pub fn egress(&self) -> &Egress { | |
| &self.p.egress | |
| } | |
| /// The private home, where the harness leaves its transcript. | |
| pub fn home(&self) -> &Path { | |
| &self.p.home | |
| } | |
| /// The working tree the harness runs in. | |
| pub fn workdir(&self) -> &Path { | |
| &self.workdir | |
| } | |
| /// The model the session was given, if any. | |
| pub fn model(&self) -> Option<&str> { | |
| self.model.as_deref() | |
| } | |
| /// Whether mining goes through the harness: always on a subscription login, since | |
| /// that credential belongs to the harness and may be used by nothing else; by choice | |
| /// otherwise. | |
| pub fn mines_via_harness(&self, via: MineVia) -> bool { | |
| via_harness(via, self.p.server.auth) | |
| } | |
| /// Mine the newest transcript in the private home, if there is one. Through the | |
| /// harness on a subscription login: a prompted, confined run of the harness itself on | |
| /// a small model, whose printed answer is the proposals. Through the egress otherwise. | |
| /// | |
| /// # Errors | |
| /// | |
| /// When the model call, the harness run, or a write fails. | |
| pub async fn mine( | |
| &self, | |
| model: Option<&str>, | |
| via: MineVia, | |
| ) -> Result<Vec<String>, Box<dyn std::error::Error>> { | |
| let Some(t) = mine::newest_transcript(&self.p.home) else { | |
| eprintln!("amem: nothing to mine; no transcript under the private home"); | |
| return Ok(Vec::new()); | |
| }; | |
| let model = model::resolve(&self.p.egress, &self.p.server.provider, model).await; | |
| let client = work::client(self.p.memory.clone()).await?; | |
| let home = Some(self.p.home.as_path()); | |
| let kept = transcripts::keep_file(&self.p.memory, self.harness.binary(), None, home, &t); | |
| let kept = kept.await.ok(); | |
| let kept = kept.as_ref().map(|k| k.session.as_str()); | |
| let ids = if self.mines_via_harness(via) { | |
| self.mine_via_harness(&client, &model, &t, kept).await? | |
| } else { | |
| mine::mine( | |
| &self.p.egress, | |
| &client, | |
| &self.p.server.provider, | |
| &model, | |
| &t, | |
| kept, | |
| ) | |
| .await? | |
| }; | |
| eprintln!("amem: mined {} entries from {}", ids.len(), t.display()); | |
| client.close().await.ok(); | |
| Ok(ids) | |
| } | |
| async fn mine_via_harness( | |
| &self, | |
| client: &dm_client::MemoryClient, | |
| model: &str, | |
| transcript: &Path, | |
| kept: Option<&str>, | |
| ) -> Result<Vec<String>, Box<dyn std::error::Error>> { | |
| let Some((excerpt, turns)) = mine::excerpt_of(transcript)? else { | |
| return Ok(Vec::new()); | |
| }; | |
| let mut args = self.harness.headless(&mine::prompt_for(&excerpt)); | |
| // the harness's name for the model on this route; a mined entry names it plainly | |
| let named = model_for(self.harness, self.auth, &self.p.provider, model); | |
| args.extend(self.harness.model_flag(&named)); | |
| let (code, output) = self.run(&args, true).await?; | |
| // the mining run is not a session anyone continues: its own hook notes go | |
| let _ = std::fs::remove_dir_all(self.outbox()); | |
| if code != 0 { | |
| return Err(format!("the mining run exited {code}").into()); | |
| } | |
| let found = mine::proposals(&output.unwrap_or_default()); | |
| let cite = mine::cite_for(kept, transcript, turns); | |
| mine::write_proposals(client, &found, &cite, model).await | |
| } | |
| /// Run the harness once with these arguments. With `capture`, its standard output | |
| /// comes back instead of reaching the terminal. | |
| /// | |
| /// # Errors | |
| /// | |
| /// When the sandbox cannot be built or the harness cannot be spawned. | |
| pub async fn run( | |
| &self, | |
| args: &[String], | |
| capture: bool, | |
| ) -> Result<(i32, Option<String>), Box<dyn std::error::Error>> { | |
| self.run_task(args, capture, None).await | |
| } | |
| /// Run the harness for a task it holds: the task's id goes into the environment as | |
| /// `MEMORY_TASK`, so the hooks know which task a subagent is filed from. | |
| /// | |
| /// # Errors | |
| /// | |
| /// When the sandbox cannot be built or the harness cannot be started. | |
| pub async fn run_task( | |
| &self, | |
| args: &[String], | |
| capture: bool, | |
| task: Option<&str>, | |
| ) -> Result<(i32, Option<String>), Box<dyn std::error::Error>> { | |
| let mut env = self.p.harness_env.clone(); | |
| if let Some(t) = task { | |
| env.push(("MEMORY_TASK".to_string(), t.to_string())); | |
| self.p.memory.set_held_task(t); | |
| } | |
| let mut full: Vec<String> = [self.program.as_slice(), args].concat(); | |
| if let Some(m) = &self.model { | |
| let m = model_for(self.harness, self.auth, &self.p.provider, m); | |
| full.extend(self.harness.model_flag(&m)); | |
| } | |
| let args = full.as_slice(); | |
| let started = std::time::Instant::now(); | |
| let (user, sys) = dm_sandbox::measure::own_cpu_ms(); | |
| let (code, out, used) = match &self.runner { | |
| Runner::Unconfined(Some((addr, _))) => { | |
| run_unconfined( | |
| self.harness, | |
| self.auth, | |
| &self.workdir, | |
| &self.p.home, | |
| &format!("http://{addr}"), | |
| args, | |
| capture, | |
| ) | |
| .await | |
| } | |
| Runner::Unconfined(None) => Err("the unconfined runner was not started".into()), | |
| Runner::Namespaces | Runner::Image(_) | Runner::Seatbelt => { | |
| run_confined( | |
| &self.p, | |
| self.harness, | |
| self.auth, | |
| &self.workdir, | |
| args, | |
| &env, | |
| capture, | |
| &self.runner, | |
| ) | |
| .await | |
| } | |
| Runner::Requests(r) => self | |
| .run_requested(r, args, &env, capture) | |
| .await | |
| .map(|(code, out)| (code, out, None)), | |
| }?; | |
| let (user_after, sys_after) = dm_sandbox::measure::own_cpu_ms(); | |
| let used = used.unwrap_or_default(); | |
| let run = dm_egress::telemetry::Run { | |
| session: self.p.memory.session_id().to_string(), | |
| harness: self.harness.binary().to_string(), | |
| runner: self.runner.name().to_string(), | |
| task: task.map(str::to_string), | |
| exit: code, | |
| wall_ms: if used.measured { | |
| used.wall_ms | |
| } else { | |
| u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX) | |
| }, | |
| launcher_cpu_ms: (user_after + sys_after).saturating_sub(user + sys), | |
| user_ms: used.user_ms, | |
| sys_ms: used.sys_ms, | |
| max_rss_kib: used.max_rss_kib, | |
| measured: used.measured, | |
| at: String::new(), | |
| }; | |
| if let Err(e) = self.p.egress.telemetry().record_run(&run) { | |
| eprintln!("amem: the run's resources were not kept: {e}"); | |
| } | |
| Ok((code, out)) | |
| } | |
| /// Write a run request for the host and wait for the harness container's exit code. | |
| async fn run_requested( | |
| &self, | |
| r: &RequestsRunner, | |
| args: &[String], | |
| env: &[(String, String)], | |
| capture: bool, | |
| ) -> Result<(i32, Option<String>), Box<dyn std::error::Error>> { | |
| self.p.memory.project_into(&self.p.projection).await?; | |
| let mut plan = dm_sandbox::prepare(&dm_sandbox::Request { | |
| harness: self.harness, | |
| auth: self.auth, | |
| provider: Some(self.p.provider.as_str()), | |
| workdir: &self.workdir, | |
| home: &self.p.home, | |
| egress_socket: &self.p.socket, | |
| memory_binary: Path::new(dm_sandbox::INSIDE_MEMORY), | |
| destination_ceiling: self.p.ceiling, | |
| harness_args: args, | |
| host_home: r.host_home.as_deref(), | |
| harness_env: env, | |
| model: self.p.model.as_deref(), | |
| image: true, | |
| workdir_inside: Some(&r.workdir_inside), | |
| rootfs: None, | |
| projection: Some(&self.p.projection), | |
| })?; | |
| plan.env.insert( | |
| "MEMORY_SESSION".into(), | |
| self.p.memory.session_id().to_string(), | |
| ); | |
| let n = r.next.fetch_add(1, std::sync::atomic::Ordering::SeqCst); | |
| let home_rel = self | |
| .p | |
| .home | |
| .strip_prefix(&self.p.root) | |
| .map(|h| h.display().to_string()) | |
| .unwrap_or_default(); | |
| let req = requests::RunRequest { | |
| argv: plan.argv, | |
| env: plan.env, | |
| cwd: plan.cwd.display().to_string(), | |
| home: home_rel, | |
| interactive: !capture, | |
| }; | |
| let tmp = r.dir.join(format!("{}.tmp", requests::RunRequest::file(n))); | |
| std::fs::write(&tmp, serde_json::to_vec(&req)?)?; | |
| std::fs::rename(&tmp, r.dir.join(requests::RunRequest::file(n)))?; | |
| let exit = PathBuf::from(requests::RunRequest::exit_file(n)); | |
| let exit = r.dir.join(exit.file_name().unwrap_or_default()); | |
| loop { | |
| if let Ok(text) = std::fs::read_to_string(&exit) { | |
| if let Ok(code) = text.trim().parse::<i32>() { | |
| let out = if capture { | |
| let out = PathBuf::from(requests::RunRequest::out_file(n)); | |
| std::fs::read_to_string(r.dir.join(out.file_name().unwrap_or_default())) | |
| .ok() | |
| } else { | |
| None | |
| }; | |
| return Ok((code, out.or_else(|| capture.then(String::new)))); | |
| } | |
| } | |
| tokio::time::sleep(std::time::Duration::from_millis(200)).await; | |
| } | |
| } | |
| /// What a launcher told twice to stop does before it exits at once: remove the | |
| /// socket and the render, which no sweep removes, and put the task a worker holds | |
| /// back in the pool. The run in flight, its mount and its scratch directory, end | |
| /// with the launcher and are swept by the next one. | |
| pub async fn abandon(&self) { | |
| std::fs::remove_file(&self.p.socket).ok(); | |
| std::fs::remove_dir_all(&self.p.projection).ok(); | |
| if let Some(id) = work::holding() { | |
| self.p.memory.release(&id, None).await.ok(); | |
| } | |
| } | |
| /// Stop serving, report usage, take the transcripts the record now keeps out of the | |
| /// private home, and tell the host the final code where there is one. After mining, | |
| /// which reads the home's copy. | |
| pub async fn finish(self, code: i32) { | |
| if let Runner::Unconfined(Some((_, serve))) = &self.runner { | |
| serve.abort(); | |
| } | |
| #[cfg(unix)] | |
| if let Some(serve) = &self.serve_unix { | |
| serve.abort(); | |
| std::fs::remove_file(&self.p.socket).ok(); | |
| } | |
| // the render was this session's alone, and goes with it; here as well as in the | |
| // guard, since a launch ends in process::exit, which runs no destructors. Before | |
| // anything is said, which a terminal that hung up refuses. | |
| std::fs::remove_dir_all(&self.p.projection).ok(); | |
| if !self.p.metered { | |
| record_reported_usage(&self.p); | |
| } | |
| report_usage(&self.p.egress); | |
| transcripts::remove_kept(&self.p.memory, &self.p.home).await; | |
| if let Runner::Requests(r) = &self.runner { | |
| std::fs::write(r.dir.join(requests::DONE), code.to_string()).ok(); | |
| } | |
| } | |
| } | |
| #[cfg(unix)] | |
| fn open_to_all(path: &Path, mode: u32) -> std::io::Result<()> { | |
| use std::os::unix::fs::PermissionsExt; | |
| std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode)) | |
| } | |
| #[cfg(not(unix))] | |
| fn open_to_all(_path: &Path, _mode: u32) -> std::io::Result<()> { | |
| Ok(()) | |
| } | |
| fn home_dir() -> Option<PathBuf> { | |
| std::env::var_os("HOME").map(PathBuf::from) | |
| } | |
| /// Run the harness on the host with the egress on a TCP port. The same configuration as | |
| /// the confined path, minus the namespaces. | |
| #[allow(clippy::too_many_arguments)] // the host-side twin of dm_sandbox::Request | |
| async fn run_unconfined( | |
| harness: Harness, | |
| auth: AuthMode, | |
| workdir: &Path, | |
| home: &Path, | |
| egress_url: &str, | |
| args: &[String], | |
| capture: bool, | |
| ) -> Result<Ran, Box<dyn std::error::Error>> { | |
| // counted until the harness is reaped and its end said, so a launcher told to stop | |
| // waits for that rather than exiting first | |
| let _in_flight = stops::InFlight::enter()?; | |
| std::fs::create_dir_all(home)?; | |
| let mut cmd = std::process::Command::new(harness.binary()); | |
| cmd.current_dir(workdir); | |
| // the egress holds the credentials; the harness gets what is set for it below | |
| for (k, _) in credentials::KNOWN { | |
| cmd.env_remove(k); | |
| } | |
| match harness { | |
| Harness::Claude => { | |
| let mcp = serde_json::json!({ | |
| "mcpServers": { "memory": { "type": "http", "url": format!("{egress_url}/mcp") } } | |
| }); | |
| let mcp_path = home.join("mcp.json"); | |
| std::fs::write(&mcp_path, mcp.to_string())?; | |
| cmd.env("ANTHROPIC_BASE_URL", format!("{egress_url}/anthropic")); | |
| if auth != AuthMode::Subscription { | |
| cmd.env("ANTHROPIC_AUTH_TOKEN", "egress"); | |
| } | |
| cmd.arg("--mcp-config") | |
| .arg(&mcp_path) | |
| .arg("--strict-mcp-config"); | |
| cmd.env("MEMORY_OUTBOX", home.join(hooks::OUTBOX)); | |
| } | |
| Harness::Gemini | |
| | Harness::Antigravity | |
| | Harness::Qwen | |
| | Harness::Grok | |
| | Harness::Muse | |
| | Harness::Pi | |
| | Harness::Copilot | |
| | Harness::OpenCode | |
| | Harness::Command => { | |
| return Err(format!( | |
| "--unconfined is wired for claude and codex only so far; {} runs confined", | |
| harness.binary() | |
| ) | |
| .into()); | |
| } | |
| Harness::Codex => { | |
| let codex_home = home.join("codex-home"); | |
| std::fs::create_dir_all(&codex_home)?; | |
| std::fs::write( | |
| codex_home.join("config.toml"), | |
| format!( | |
| "model_provider = \"egress\"\n\n[model_providers.egress]\nname = \"egress\"\nbase_url = \"{egress_url}/openai/v1\"\nenv_key = \"DM_EGRESS_TOKEN\"\nwire_api = \"responses\"\n\n[mcp_servers.memory]\nurl = \"{egress_url}/mcp\"\n" | |
| ), | |
| )?; | |
| cmd.env("CODEX_HOME", &codex_home); | |
| cmd.env("DM_EGRESS_TOKEN", "egress"); | |
| } | |
| } | |
| cmd.args(args); | |
| // on a thread of its own, so a launcher told to stop hears the signal while it runs; | |
| // a headless run in a process group of its own, so a stop kills what it left in the | |
| // background too, and an interactive one in the terminal's foreground group | |
| let (code, out, r) = tokio::task::spawn_blocking(move || { | |
| if capture { | |
| dm_sandbox::measure::run_grouped(&mut cmd, capture) | |
| } else { | |
| dm_sandbox::measure::run(&mut cmd, capture) | |
| } | |
| }) | |
| .await??; | |
| Ok(( | |
| code, | |
| capture.then(|| String::from_utf8_lossy(&out).into_owned()), | |
| Some(r), | |
| )) | |
| } | |
| /// Inside the sandbox, as pid 1 of the harness's namespace: forward the loopback port to | |
| /// the socket and run the harness. | |
| #[cfg(not(unix))] | |
| fn relay( | |
| _socket: &Path, | |
| _listen: &str, | |
| _argv: &[OsString], | |
| ) -> Result<i32, Box<dyn std::error::Error>> { | |
| Err("the relay runs inside a Linux sandbox only".into()) | |
| } | |
| #[cfg(unix)] | |
| fn relay( | |
| socket: &Path, | |
| listen: &str, | |
| argv: &[OsString], | |
| ) -> Result<i32, Box<dyn std::error::Error>> { | |
| let rt = tokio::runtime::Builder::new_multi_thread() | |
| .worker_threads(2) | |
| .enable_all() | |
| .build()?; | |
| let socket = socket.to_path_buf(); | |
| // bound before the harness starts, which without it has no way out | |
| let listener = rt | |
| .block_on(tokio::net::TcpListener::bind(listen)) | |
| .map_err(|e| format!("cannot listen on {listen}: {e}"))?; | |
| rt.spawn(async move { | |
| loop { | |
| let Ok((mut inbound, _)) = listener.accept().await else { | |
| continue; | |
| }; | |
| let socket = socket.clone(); | |
| tokio::spawn(async move { | |
| match tokio::net::UnixStream::connect(&socket).await { | |
| Ok(mut outbound) => { | |
| let _ = tokio::io::copy_bidirectional(&mut inbound, &mut outbound).await; | |
| } | |
| Err(e) => eprintln!("relay: egress socket: {e}"), | |
| } | |
| }); | |
| } | |
| }); | |
| let argv = argv.to_vec(); | |
| let code = | |
| rt.block_on( | |
| async move { tokio::task::spawn_blocking(move || spawn_harness(&argv)).await }, | |
| )??; | |
| Ok(code) | |
| } | |
| #[cfg(unix)] | |
| fn spawn_harness(argv: &[OsString]) -> Result<i32, String> { | |
| #[cfg(target_os = "linux")] | |
| { | |
| dm_sandbox::linux::spawn_in_pid_namespace(argv).map_err(|e| e.to_string()) | |
| } | |
| #[cfg(not(target_os = "linux"))] | |
| { | |
| let status = std::process::Command::new(&argv[0]) | |
| .args(&argv[1..]) | |
| .status() | |
| .map_err(|e| e.to_string())?; | |
| Ok(dm_sandbox::measure::exit_code(status, &argv[0])) | |
| } | |
| } | |
| async fn entries(m: &Memory, command: Command) -> Fallible { | |
| match command { | |
| Command::Write { | |
| kind, | |
| title, | |
| domain, | |
| body, | |
| classification, | |
| source, | |
| supersedes, | |
| } => { | |
| let w = m | |
| .write(WriteRequest { | |
| domain, | |
| entry_type: kind.into(), | |
| title, | |
| body: body_or_stdin(body)?, | |
| source, | |
| confidence: None, | |
| classification: classification.map(Into::into), | |
| supersedes, | |
| review_by: None, | |
| summary: None, | |
| }) | |
| .await?; | |
| json(&w) | |
| } | |
| cmd @ (Command::Search { .. } | Command::Recall { .. }) => retrieve(m, cmd).await, | |
| Command::Get { id } => { | |
| let (e, from) = m.get_following(&id).await?; | |
| for old in &from { | |
| eprintln!("amem: {old} was replaced; reading its successor"); | |
| } | |
| print!("{}", e.render()); | |
| Ok(()) | |
| } | |
| Command::List { | |
| kind, | |
| domain, | |
| limit, | |
| } => json(&hits_json( | |
| &m.list(domain.as_deref(), kind.map(Into::into), limit) | |
| .await?, | |
| )), | |
| Command::T0 { domain } => { | |
| print!("{}", m.t0(domain.as_deref()).await?); | |
| Ok(()) | |
| } | |
| other => maintain(m, other).await, | |
| } | |
| } | |
| /// `search` and `recall`: the retrieval operations, one shape of JSON each. | |
| async fn retrieve(m: &Memory, command: Command) -> Fallible { | |
| match command { | |
| Command::Search { | |
| query, | |
| domain, | |
| limit, | |
| depth: DepthArg::Sections, | |
| } => { | |
| let pack = m.search_sections(&query, domain.as_deref(), limit).await?; | |
| json(&serde_json::json!({ | |
| "hits": pack.pieces, | |
| "truncated": pack.truncated, | |
| "tokens": pack.tokens, | |
| })) | |
| } | |
| Command::Search { | |
| query, | |
| domain, | |
| limit, | |
| depth: DepthArg::Summaries, | |
| } => json(&hits_json( | |
| &m.search(&query, domain.as_deref(), limit).await?, | |
| )), | |
| Command::Recall { | |
| question, | |
| domain, | |
| depth, | |
| .. | |
| } => { | |
| let r = m | |
| .recall( | |
| &question, | |
| domain.as_deref(), | |
| depth.map(Into::into), | |
| Budget::default(), | |
| ) | |
| .await?; | |
| json(&recall::to_json(&r)) | |
| } | |
| _ => unreachable!("only search and recall are routed here"), | |
| } | |
| } | |
| /// `amem canary`: plant, or check and alarm. | |
| async fn canary_cmd(m: &Memory, op: CanaryOp) -> Fallible { | |
| match op { | |
| CanaryOp::Plant { domain, count } => { | |
| let new = canary::plant(m, m.root(), domain.as_deref(), count).await?; | |
| json(&new) | |
| } | |
| CanaryOp::Check { file } => { | |
| let report = canary::check(m, m.root()).await?; | |
| let filed = if file && !report.tripped.is_empty() { | |
| let domain = canary::planted(m.root())? | |
| .first() | |
| .map(|c| c.domain.clone()) | |
| .unwrap_or_default(); | |
| canary::file(m, &report, &domain).await? | |
| } else { | |
| Vec::new() | |
| }; | |
| json(&serde_json::json!({ | |
| "planted": report.planted, | |
| "intact": report.intact, | |
| "tripped": report.tripped, | |
| "filed": filed, | |
| }))?; | |
| if !report.tripped.is_empty() { | |
| std::process::exit(3); | |
| } | |
| Ok(()) | |
| } | |
| } | |
| } | |
| /// `amem check`: the checker over a sample of a domain, through a destination of its own. | |
| /// A curation cycle's settings. | |
| struct CuratePlan { | |
| domain: Option<String>, | |
| auth: AuthArg, | |
| provider: Option<String>, | |
| model: Option<String>, | |
| sample: usize, | |
| contradictions: bool, | |
| budget_tasks: usize, | |
| budget_tokens: u64, | |
| drift: bool, | |
| workdir: Option<PathBuf>, | |
| } | |
| /// The destination a checker or a cycle speaks to, from the auth mode and provider given. | |
| fn checker_destination(auth: AuthMode, provider: Option<String>) -> String { | |
| provider.unwrap_or_else(|| { | |
| if auth == AuthMode::Gateway { | |
| "llmao".to_string() | |
| } else { | |
| "openai".to_string() | |
| } | |
| }) | |
| } | |
| /// How many curation tasks are open: proposed or admitted, filed by the survey, the | |
| /// checker or the telemetry. | |
| async fn open_curation_tasks( | |
| m: &Memory, | |
| domain: &str, | |
| ) -> Result<usize, Box<dyn std::error::Error>> { | |
| let mut n = 0; | |
| for status in [dm_ledger::Status::Proposed, dm_ledger::Status::Admitted] { | |
| n += m | |
| .tasks(Some(domain), status) | |
| .await? | |
| .iter() | |
| .filter(|t| { | |
| t.spec.origin.as_deref().is_some_and(|o| { | |
| o.starts_with("librarian:") | |
| || o.starts_with("checker:") | |
| || o.starts_with("telemetry:") | |
| }) | |
| }) | |
| .count(); | |
| } | |
| Ok(n) | |
| } | |
| /// The summaries of a domain's newest entries, for the checker's tasks. | |
| async fn summaries_of( | |
| m: &Memory, | |
| domain: &str, | |
| n: usize, | |
| ) -> Result<Vec<(String, String)>, Box<dyn std::error::Error>> { | |
| Ok(checker::sample_of(m, domain, n) | |
| .await? | |
| .iter() | |
| .map(|h| (h.id.clone(), h.summary.clone())) | |
| .collect()) | |
| } | |
| /// `amem librarian --telemetry`: the librarian's own telemetry, its alarms filed with --file. | |
| async fn telemetry_cmd(m: &Memory, domain: Option<&str>, file: bool) -> Fallible { | |
| let t = curation::telemetry(m, domain).await?; | |
| let filed = if file { | |
| curation::file_alarms(m, &t).await? | |
| } else { | |
| Vec::new() | |
| }; | |
| json(&serde_json::json!({ | |
| "domain": t.domain, | |
| "by_author": t.by_author, | |
| "alarms": t.alarms, | |
| "filed": filed, | |
| })) | |
| } | |
| /// `amem curate`: a cycle, and with `every`, a cycle at that interval until stopped. | |
| async fn curate_loop(root: &Path, plan: &CuratePlan, every: Option<u64>) -> Fallible { | |
| loop { | |
| curate_cmd(root, plan).await?; | |
| let Some(secs) = every else { | |
| return Ok(()); | |
| }; | |
| eprintln!("amem: next curation cycle in {secs}s"); | |
| tokio::time::sleep(std::time::Duration::from_secs(secs)).await; | |
| } | |
| } | |
| /// The checker's part of a cycle: the sample judged, contradictions hunted, both filed. | |
| async fn checker_pass( | |
| p: &Prepared, | |
| plan: &CuratePlan, | |
| domain: &str, | |
| model: &str, | |
| cycle: &mut curation::Cycle, | |
| ) -> Result<(), Box<dyn std::error::Error>> { | |
| let m = &p.memory; | |
| let spend = checker::Spend { | |
| egress: &p.egress, | |
| session: m.session_id(), | |
| limit: plan.budget_tokens, | |
| }; | |
| let report = checker::check( | |
| &p.egress, | |
| m, | |
| &p.provider, | |
| model, | |
| Some(domain), | |
| plan.sample, | |
| Some(&spend), | |
| ) | |
| .await?; | |
| cycle.checked = report.checked.len(); | |
| cycle.rejected = report.checked.iter().filter(|v| !v.entails).count(); | |
| let summaries = summaries_of(m, domain, plan.sample).await?; | |
| checker::file(m, &report, &summaries).await?; | |
| if plan.contradictions { | |
| let found = checker::hunt( | |
| &p.egress, | |
| m, | |
| &p.provider, | |
| model, | |
| Some(domain), | |
| plan.sample, | |
| Some(&spend), | |
| ) | |
| .await?; | |
| cycle.contradictions = found.len(); | |
| checker::file_contradictions(m, domain, model, &found).await?; | |
| } | |
| if !spend.exhausted() { | |
| let reviewed = review::review(&p.egress, m, &p.provider, model, plan.sample).await?; | |
| cycle.reviewed = reviewed.len(); | |
| cycle.malicious = reviewed | |
| .iter() | |
| .filter(|r| r.verdict == review::Kind::Malicious) | |
| .count(); | |
| } | |
| if plan.drift && !spend.exhausted() { | |
| let set = plan | |
| .workdir | |
| .clone() | |
| .unwrap_or_default() | |
| .join("probes/drift.toml"); | |
| if set.is_file() { | |
| cycle.drift = drift::scheduled(&p.egress, m, &set, &p.provider, model, Some(domain)) | |
| .await | |
| .unwrap_or_else(|e| Some(format!("probe failed: {e}"))); | |
| } | |
| } | |
| if let Some(t) = p.egress.telemetry().session(m.session_id())? { | |
| cycle.calls = t.calls; | |
| cycle.tokens_in = t.input_tokens; | |
| cycle.tokens_out = t.output_tokens; | |
| } | |
| Ok(()) | |
| } | |
| /// The owner's part of a cycle: the canaries checked and the telemetry's alarms, filed. | |
| async fn owner_pass( | |
| m: &Memory, | |
| root: &Path, | |
| domain: &str, | |
| cycle: &mut curation::Cycle, | |
| ) -> Result<(), Box<dyn std::error::Error>> { | |
| let canaries = canary::check(m, root).await?; | |
| cycle.canaries_tripped = canaries.tripped.len(); | |
| if !canaries.tripped.is_empty() { | |
| canary::file(m, &canaries, domain).await?; | |
| } | |
| cycle.rescored = m.rescore(Some(domain)).await?.changed.len(); | |
| let t = curation::telemetry(m, Some(domain)).await?; | |
| cycle.alarms = t.alarms.len(); | |
| curation::file_alarms(m, &t).await?; | |
| transcripts::sweep(m).await?; | |
| Ok(()) | |
| } | |
| /// `amem curate`: one cycle, logged. | |
| async fn curate_cmd(root: &Path, plan: &CuratePlan) -> Fallible { | |
| let auth = AuthMode::from(plan.auth); | |
| let provider = checker_destination(auth, plan.provider.clone()); | |
| let p = prepare_launch( | |
| root, | |
| Harness::Command, | |
| Credentials::keyed(auth, &provider), | |
| None, | |
| None, | |
| Projected::No, | |
| ) | |
| .await?; | |
| let m = &p.memory; | |
| m.disable_read_log(); | |
| let domain = m.resolve_domain(plan.domain.as_deref())?; | |
| let model = model::resolve(&p.egress, &p.provider, plan.model.as_deref()).await; | |
| let mut cycle = curation::Cycle { | |
| at: time::OffsetDateTime::now_utc() | |
| .format(&time::format_description::well_known::Rfc3339) | |
| .unwrap_or_default(), | |
| domain: domain.clone(), | |
| budget_tasks: plan.budget_tasks, | |
| budget_tokens: plan.budget_tokens, | |
| checker_model: model.clone(), | |
| ..curation::Cycle::default() | |
| }; | |
| // the survey, filed up to the budget's gap | |
| cycle.open_before = open_curation_tasks(m, &domain).await?; | |
| let options = librarian::Options { | |
| source_root: plan.workdir.clone(), | |
| ..librarian::Options::default() | |
| }; | |
| let mut survey = librarian::survey(m, root, Some(&domain), &options).await?; | |
| cycle.proposals = survey.proposals.len(); | |
| let room = plan.budget_tasks.saturating_sub(cycle.open_before); | |
| survey.proposals.truncate(room); | |
| let (filed, already) = librarian::file(m, &survey).await?; | |
| cycle.filed = filed.len(); | |
| cycle.already_filed = already; | |
| checker_pass(&p, plan, &domain, &model, &mut cycle).await?; | |
| owner_pass(m, root, &domain, &mut cycle).await?; | |
| curation::log_cycle(root, &cycle)?; | |
| json(&cycle) | |
| } | |
| /// What a checker run does besides judging summaries. | |
| struct CheckWhat { | |
| /// File what it finds as tasks. | |
| file: bool, | |
| /// Hunt contradictions. | |
| contradictions: bool, | |
| /// Read the quarantine. | |
| quarantine: bool, | |
| } | |
| async fn check_cmd( | |
| root: &Path, | |
| auth: AuthArg, | |
| provider: Option<String>, | |
| model: Option<String>, | |
| domain: Option<String>, | |
| sample: usize, | |
| what: CheckWhat, | |
| ) -> Fallible { | |
| let auth = AuthMode::from(auth); | |
| let provider = checker_destination(auth, provider); | |
| let p = prepare_launch( | |
| root, | |
| Harness::Command, | |
| Credentials::keyed(auth, &provider), | |
| None, | |
| None, | |
| Projected::No, | |
| ) | |
| .await?; | |
| p.memory.disable_read_log(); | |
| let model = model::resolve(&p.egress, &p.provider, model.as_deref()).await; | |
| let report = checker::check( | |
| &p.egress, | |
| &p.memory, | |
| &p.provider, | |
| &model, | |
| domain.as_deref(), | |
| sample, | |
| None, | |
| ) | |
| .await?; | |
| let found = if what.contradictions { | |
| checker::hunt( | |
| &p.egress, | |
| &p.memory, | |
| &p.provider, | |
| &model, | |
| domain.as_deref(), | |
| sample, | |
| None, | |
| ) | |
| .await? | |
| } else { | |
| Vec::new() | |
| }; | |
| let reviewed = if what.quarantine { | |
| review::review(&p.egress, &p.memory, &p.provider, &model, sample).await? | |
| } else { | |
| Vec::new() | |
| }; | |
| let mut filed = Vec::new(); | |
| if what.file { | |
| let summaries = summaries_of(&p.memory, &report.domain, sample).await?; | |
| filed = checker::file(&p.memory, &report, &summaries).await?; | |
| filed | |
| .extend(checker::file_contradictions(&p.memory, &report.domain, &model, &found).await?); | |
| } | |
| json(&serde_json::json!({ | |
| "domain": report.domain, | |
| "model": report.model, | |
| "checked": report.checked, | |
| "rejected": report.checked.iter().filter(|v| !v.entails).count(), | |
| "contradictions": found, | |
| "quarantine": reviewed, | |
| "filed": filed, | |
| })) | |
| } | |
| /// `amem policy`, `amem quarantine`, `amem taint`, `amem canary`, `amem erase` and | |
| /// `amem reindex`: the perimeter's commands and the record's repair. | |
| async fn perimeter_cmd(m: &Memory, command: Command) -> Fallible { | |
| match command { | |
| Command::Policy { op: PolicyOp::Show } => { | |
| println!( | |
| "# policy {} from {}\n{}", | |
| m.policy().version(), | |
| m.policy().source(), | |
| m.policy().text() | |
| ); | |
| Ok(()) | |
| } | |
| Command::Policy { | |
| op: PolicyOp::Eval { input }, | |
| } => { | |
| let text = if input == "-" { | |
| let mut s = String::new(); | |
| std::io::Read::read_to_string(&mut std::io::stdin(), &mut s)?; | |
| s | |
| } else { | |
| std::fs::read_to_string(&input)? | |
| }; | |
| let input: dm_policy::decide::Input = serde_json::from_str(&text)?; | |
| json(&m.policy().decide(&input)?) | |
| } | |
| Command::Policy { op: PolicyOp::List } => json(&policy_list(m)), | |
| Command::Policy { | |
| op: PolicyOp::Lint { file }, | |
| } => policy_lint(m, file).await, | |
| Command::Quarantine { | |
| op: QuarantineOp::List, | |
| } => json( | |
| &m.quarantined()? | |
| .iter() | |
| .map(|e| { | |
| let review = review::read(m.root(), &e.front.id); | |
| serde_json::json!({ | |
| "review": review.map(|r| serde_json::json!({"verdict": r.verdict, "why": r.why, "model": r.model})), | |
| "id": e.front.id, "domain": e.front.domain, "author": e.front.author, | |
| "created": e.front.created.format(&time::format_description::well_known::Rfc3339).unwrap_or_default(), | |
| "title": e.front.title, | |
| }) | |
| }) | |
| .collect::<Vec<_>>(), | |
| ), | |
| Command::Quarantine { | |
| op: QuarantineOp::Show { id }, | |
| } => { | |
| let e = m | |
| .quarantined()? | |
| .into_iter() | |
| .find(|e| e.front.id == id) | |
| .ok_or_else(|| format!("{id} is not in quarantine"))?; | |
| print!("{}", e.render()); | |
| Ok(()) | |
| } | |
| Command::Quarantine { | |
| op: QuarantineOp::Release { id }, | |
| } => json(&m.release_quarantined(&id, true).await?), | |
| Command::Quarantine { | |
| op: QuarantineOp::Drop { id }, | |
| } => json(&m.release_quarantined(&id, false).await?), | |
| Command::Taint { id } => json(&m.taint(&id)), | |
| Command::Promote { id, to, why } => json(&m.promote(&id, to.parse()?, &why).await?), | |
| Command::Approve { task } => json(&m.approve(&task).await?), | |
| Command::Promotions { id } => json(&m.promotions(&id).await?), | |
| Command::Canary { op } => canary_cmd(m, op).await, | |
| Command::Erase { id, reason } => json(&m.erase(&id, reason.into()).await?), | |
| Command::Reindex => { | |
| let (n, events) = m.reindex().await?; | |
| println!("indexed {n} entries, replayed {events} ledger events"); | |
| Ok(()) | |
| } | |
| _ => unreachable!("routed by maintain"), | |
| } | |
| } | |
| /// The record's policies, as `amem policy list` shows them. | |
| fn policy_list(m: &Memory) -> serde_json::Value { | |
| let p = m.policy(); | |
| let now = time::OffsetDateTime::now_utc(); | |
| let rows: Vec<_> = p | |
| .entries() | |
| .iter() | |
| .map(|e| { | |
| serde_json::json!({ | |
| "id": e.meta.id, "entry": e.entry_id, "layer": e.meta.layer, "tier": e.tier, | |
| "rule": e.meta.rule, "exception": e.meta.is_exception(), | |
| "enforced": e.enforced(), "in_force": e.in_force(now), | |
| "breaches": e.meta.breaches, "scope_task": e.meta.scope_task, | |
| }) | |
| }) | |
| .collect(); | |
| serde_json::json!({ | |
| "version": p.version(), "policies": rows, | |
| "refused": p.refused().iter().map(|(id, why)| serde_json::json!({"id": id, "why": why})).collect::<Vec<_>>(), | |
| }) | |
| } | |
| /// Citations and overrides per policy, from the decision log: how often each rule | |
| /// refused something, and how often an exception let something through. | |
| fn policy_rates(root: &Path) -> serde_json::Value { | |
| let text = std::fs::read_to_string(root.join("state/decisions.jsonl")).unwrap_or_default(); | |
| let mut cited: std::collections::BTreeMap<String, (u64, u64)> = | |
| std::collections::BTreeMap::new(); | |
| for line in text.lines() { | |
| let Ok(v) = serde_json::from_str::<serde_json::Value>(line) else { | |
| continue; | |
| }; | |
| let allow = v["allow"].as_bool().unwrap_or(false); | |
| for id in v["cites"] | |
| .as_array() | |
| .into_iter() | |
| .flatten() | |
| .filter_map(|c| c.as_str()) | |
| { | |
| let row = cited.entry(id.to_string()).or_default(); | |
| if allow { | |
| row.1 += 1; | |
| } else { | |
| row.0 += 1; | |
| } | |
| } | |
| } | |
| serde_json::json!(cited | |
| .into_iter() | |
| .map(|(id, (refused, overridden))| serde_json::json!({"id": id, "refused": refused, "let_through": overridden})) | |
| .collect::<Vec<_>>()) | |
| } | |
| /// `amem policy lint`. | |
| async fn policy_lint(m: &Memory, file: bool) -> Fallible { | |
| let p = m.policy(); | |
| let mut findings = dm_policy::record::lint(p.entries(), time::OffsetDateTime::now_utc()); | |
| findings.extend( | |
| p.refused() | |
| .iter() | |
| .map(|(id, why)| dm_policy::record::Finding { | |
| policy: id.clone(), | |
| kind: "module-refused".into(), | |
| why: format!("{id}'s module would not load: {why}"), | |
| }), | |
| ); | |
| let mut filed = Vec::new(); | |
| if file { | |
| let domain = m.resolve_domain(None)?; | |
| for f in findings.iter().filter(|f| f.kind != "inert") { | |
| let created = m | |
| .file_own(dm_ledger::TaskSpec { | |
| domain: domain.clone(), | |
| title: format!("Policy bug, {}: {}", f.kind, f.policy), | |
| body: format!( | |
| "{}\n\nAcross layers the higher wins and the lower is fixed: amend or supersede {} so it agrees, or file an exception with a scope, an expiry and an approver. Within a layer, resolve from the stated intents and record the trade-off.", | |
| f.why, f.policy | |
| ), | |
| requirements: vec!["human".into()], | |
| depends_on: Vec::new(), | |
| origin: Some(format!("policy:{}", f.kind)), | |
| upstream_event: Some(f.policy.clone()), | |
| created_by: String::new(), | |
| classification: dm_core::Label::Internal, | |
| priority: 4, | |
| }) | |
| .await?; | |
| if !created.deduplicated { | |
| filed.push(created.task.id); | |
| } | |
| } | |
| } | |
| json(&serde_json::json!({ | |
| "findings": findings, "rates": policy_rates(m.root()), "filed": filed, | |
| })) | |
| } | |
| /// `amem librarian`: the survey, or the survey filed as tasks. | |
| async fn librarian_cmd( | |
| m: &Memory, | |
| domain: Option<&str>, | |
| file: bool, | |
| stale_days: i64, | |
| split_chars: usize, | |
| workdir: Option<PathBuf>, | |
| ) -> Fallible { | |
| let options = librarian::Options { | |
| stale_days, | |
| split_chars, | |
| source_root: workdir, | |
| ..librarian::Options::default() | |
| }; | |
| let survey = librarian::survey(m, m.root(), domain, &options).await?; | |
| if file { | |
| let (ids, already) = librarian::file(m, &survey).await?; | |
| json(&serde_json::json!({ | |
| "domain": survey.domain, | |
| "entries": survey.entries, | |
| "proposals": survey.proposals.len(), | |
| "filed": ids, | |
| "already_filed": already, | |
| })) | |
| } else { | |
| json(&survey) | |
| } | |
| } | |
| /// `amem export` and `amem import`: a domain as a bundle, out and in. | |
| async fn bundle_cmd(m: &Memory, command: Command) -> Fallible { | |
| match command { | |
| Command::Export { | |
| dest, | |
| domain, | |
| max_label, | |
| } => { | |
| let ex = m.export(domain.as_deref(), max_label.into(), &dest).await?; | |
| println!( | |
| "exported {} entries from {} to {}, highest label {}", | |
| ex.manifest.entries.len(), | |
| ex.manifest.domain, | |
| dest.display(), | |
| ex.manifest.highest_label | |
| ); | |
| Ok(()) | |
| } | |
| Command::Import { src, domain } => { | |
| let im = m.import(&src, domain.as_deref()).await?; | |
| println!( | |
| "imported {} entries with {} transcript excerpts, skipped {} already present, \ | |
| refused {} erased here, restored {} promotion records", | |
| im.written.len(), | |
| im.excerpts, | |
| im.skipped.len(), | |
| im.erased.len(), | |
| im.promotions.len() | |
| ); | |
| Ok(()) | |
| } | |
| _ => unreachable!("routed by maintain"), | |
| } | |
| } | |
| async fn maintain(m: &Memory, command: Command) -> Fallible { | |
| match command { | |
| cmd @ (Command::Export { .. } | Command::Import { .. }) => bundle_cmd(m, cmd).await, | |
| Command::Librarian { | |
| domain, | |
| file, | |
| stale_days, | |
| split_chars, | |
| workdir, | |
| telemetry, | |
| } => { | |
| // the survey's reads are the librarian's, not a session's | |
| m.disable_read_log(); | |
| if telemetry { | |
| return telemetry_cmd(m, domain.as_deref(), file).await; | |
| } | |
| let workdir = workdir.or_else(|| std::env::current_dir().ok()); | |
| librarian_cmd(m, domain.as_deref(), file, stale_days, split_chars, workdir).await | |
| } | |
| cmd @ (Command::Canary { .. } | |
| | Command::Erase { .. } | |
| | Command::Promote { .. } | |
| | Command::Approve { .. } | |
| | Command::Promotions { .. } | |
| | Command::Reindex | |
| | Command::Policy { .. } | |
| | Command::Quarantine { .. } | |
| | Command::Taint { .. }) => perimeter_cmd(m, cmd).await, | |
| Command::Init { .. } | |
| | Command::Serve { .. } | |
| | Command::Launch { .. } | |
| | Command::Work { .. } | |
| | Command::Inside { .. } | |
| | Command::Image { .. } | |
| | Command::Web { .. } | |
| | Command::Mine { .. } | |
| | Command::Check { .. } | |
| | Command::Curate { .. } | |
| | Command::Replay { .. } | |
| | Command::Models { .. } | |
| | Command::WslcApiCheck { .. } | |
| | Command::Savings { .. } | |
| | Command::Golden { .. } | |
| | Command::Drift { .. } | |
| | Command::Hook { .. } | |
| | Command::Doctor { .. } | |
| | Command::Profiles { .. } | |
| | Command::Relay { .. } | |
| | Command::Usage | |
| | Command::Cost { .. } | |
| | Command::Task { .. } | |
| | Command::Transcript { .. } | |
| | Command::Reenact { .. } | |
| | Command::Log { .. } | |
| | Command::Gofannon { .. } | |
| | Command::Platform { .. } | |
| | Command::Tool { .. } | |
| | Command::ToolServer { .. } | |
| | Command::Source { .. } | |
| | Command::Tap { .. } | |
| | Command::Write { .. } | |
| | Command::Search { .. } | |
| | Command::Recall { .. } | |
| | Command::Get { .. } | |
| | Command::List { .. } | |
| | Command::T0 { .. } => unreachable!("dispatched before reaching maintain"), | |
| } | |
| } | |
| /// `amem task rescore` and `amem task stats`: the fabric's ranking and its telemetry. | |
| async fn fabric_cmd(m: &Memory, op: TaskOp) -> Fallible { | |
| match op { | |
| TaskOp::Rescore { domain } => json(&m.rescore(domain.as_deref()).await?), | |
| TaskOp::Stats { domain } => json(&m.task_stats(domain.as_deref()).await?), | |
| _ => unreachable!("routed by task"), | |
| } | |
| } | |
| async fn task(m: &Memory, op: TaskOp) -> Fallible { | |
| match op { | |
| TaskOp::Create { | |
| title, | |
| body, | |
| domain, | |
| requirements, | |
| depends_on, | |
| priority, | |
| } => { | |
| let c = m | |
| .create_task(TaskSpec { | |
| domain: domain.unwrap_or_default(), | |
| title, | |
| body, | |
| requirements, | |
| depends_on, | |
| origin: None, | |
| upstream_event: None, | |
| created_by: String::new(), | |
| classification: Label::Public, | |
| priority, | |
| }) | |
| .await?; | |
| json(&c.task) | |
| } | |
| TaskOp::Admit { id } => json(&m.admit(&id).await?), | |
| TaskOp::List { status, domain } => json(&m.tasks(domain.as_deref(), status.into()).await?), | |
| TaskOp::ClaimNext { | |
| domain, | |
| lease_seconds, | |
| } => match m | |
| .claim_next(domain.as_deref(), Duration::seconds(lease_seconds)) | |
| .await? | |
| { | |
| Some(c) => json(&c.task), | |
| None => { | |
| println!("null"); | |
| Ok(()) | |
| } | |
| }, | |
| TaskOp::Heartbeat { id, lease_seconds } => { | |
| json(&m.heartbeat(&id, Duration::seconds(lease_seconds)).await?) | |
| } | |
| TaskOp::Complete { id, handoff } => json(&m.complete(&id, &handoff).await?), | |
| TaskOp::Release { id, handoff } => json(&m.release(&id, handoff.as_deref()).await?), | |
| TaskOp::Invalidate { id, reason } => json(&m.invalidate(&id, &reason, None).await?), | |
| TaskOp::Tree { id } => json(&m.task_tree(&id).await?), | |
| TaskOp::Retry { id } => json(&m.retry_task(&id).await?), | |
| op @ (TaskOp::Rescore { .. } | TaskOp::Stats { .. }) => fabric_cmd(m, op).await, | |
| TaskOp::Get { id } => json(&m.get_task(&id).await?), | |
| } | |
| } | |
| #[cfg(test)] | |
| mod backend_tests { | |
| use super::*; | |
| #[tokio::test] | |
| async fn each_session_renders_under_a_name_of_its_own() { | |
| assert_eq!(dm_sandbox::PROJECTION, dm_server::projection::PROJECTIONS); | |
| let dir = tempfile::tempdir().unwrap(); | |
| let root = dir.path().join("m"); | |
| dm_server::Memory::init(&root, "human:test:me", "dom-me").unwrap(); | |
| let creds = || Credentials { | |
| auth: AuthMode::Subscription, | |
| provider: None, | |
| server_auth: None, | |
| server_provider: None, | |
| }; | |
| let at = |projected| prepare_launch(&root, Harness::Claude, creds(), None, None, projected); | |
| let base = root.join(dm_sandbox::PROJECTION).join("claude"); | |
| let a = at(Projected::AsSession).await.unwrap(); | |
| let b = at(Projected::AsSession).await.unwrap(); | |
| assert_eq!(a.projection, base.join(a.memory.session_id())); | |
| assert_ne!(a.projection, b.projection, "two sessions, two renders"); | |
| // the name the launcher outside mounts is the one the inside renders under | |
| let named = at(Projected::Named("run-7-1a2b")).await.unwrap(); | |
| assert_eq!(named.projection, base.join("run-7-1a2b")); | |
| assert!(at(Projected::Named("../codex")).await.is_err()); | |
| // a name whose directory another session already rendered into is not taken over | |
| let busy = base.join("run-8-busy"); | |
| std::fs::create_dir_all(busy.join("dom-me")).unwrap(); | |
| assert!(at(Projected::Named("run-8-busy")).await.is_err()); | |
| assert!(busy.join("dom-me").exists(), "and is left as it was"); | |
| // the empty one the launcher outside made for it is the session's to render into | |
| std::fs::create_dir_all(base.join("run-9-empty")).unwrap(); | |
| assert!(at(Projected::Named("run-9-empty")).await.is_ok()); | |
| } | |
| #[test] | |
| fn a_projection_name_from_the_command_line_is_one_component() { | |
| assert!(projection_name("run-4242").is_ok()); | |
| assert!(projection_name("sess-ab12").is_ok()); | |
| for bad in [ | |
| "", | |
| "..", | |
| "../claude", | |
| "a/b", | |
| "/abs", | |
| "a\\b", | |
| "run 1", | |
| &"x".repeat(65), | |
| ] { | |
| assert!(projection_name(bad).is_err(), "{bad:?}"); | |
| } | |
| } | |
| #[test] | |
| fn model_ids_are_read_from_either_listing_shape() { | |
| let openai = serde_json::json!({ "object": "list", "data": [{ "id": "b" }, { "id": "a" }, { "id": "a" }] }); | |
| assert_eq!(model::model_ids(&openai), vec!["a", "b"]); | |
| let google = serde_json::json!({ "models": [{ "name": "models/gemini-2.5-flash" }] }); | |
| assert_eq!(model::model_ids(&google), vec!["gemini-2.5-flash"]); | |
| assert!(model::model_ids(&serde_json::json!({})).is_empty()); | |
| } | |
| #[test] | |
| fn run_options_cross_to_the_inside_process_as_flags() { | |
| assert!(run_options_args(None, SubagentsArg::Harness).is_empty()); | |
| assert_eq!( | |
| run_options_args(Some("qwen3.8-27b"), SubagentsArg::Board), | |
| vec!["--model", "qwen3.8-27b", "--subagents", "board"] | |
| ); | |
| assert_eq!(SubagentsArg::default(), SubagentsArg::Harness); | |
| } | |
| #[test] | |
| fn the_working_tree_and_the_root_may_not_overlap() { | |
| let dir = tempfile::tempdir().unwrap(); | |
| let root = dir.path().join("memory"); | |
| std::fs::create_dir_all(root.join("state/projection/claude")).unwrap(); | |
| let beside = dir.path().join("proj"); | |
| std::fs::create_dir_all(&beside).unwrap(); | |
| assert!(resolve_workdir(&root, Some(beside)).is_ok(), "beside: fine"); | |
| for inside in ["state", "state/projection/claude", "."] { | |
| let err = resolve_workdir(&root, Some(root.join(inside))) | |
| .unwrap_err() | |
| .to_string(); | |
| assert!(err.contains("overlap"), "{inside}: {err}"); | |
| } | |
| let err = resolve_workdir(&root, Some(dir.path().to_path_buf())) | |
| .unwrap_err() | |
| .to_string(); | |
| assert!(err.contains("overlap"), "a tree holding the root: {err}"); | |
| } | |
| /// Set these for a test and put back what was there, so a developer's own keys | |
| /// survive the run. | |
| fn with_env<T>(vars: &[(&str, &str)], f: impl FnOnce() -> T) -> T { | |
| let kept: Vec<(&str, Option<String>)> = vars | |
| .iter() | |
| .map(|(k, _)| (*k, std::env::var(k).ok())) | |
| .collect(); | |
| for (k, v) in vars { | |
| std::env::set_var(k, v); | |
| } | |
| let out = f(); | |
| for (k, v) in kept { | |
| match v { | |
| Some(v) => std::env::set_var(k, v), | |
| None => std::env::remove_var(k), | |
| } | |
| } | |
| out | |
| } | |
| #[test] | |
| fn a_gateway_route_reshapes_what_its_translation_drops() { | |
| let gateway = with_env(&[("LLMAO_KEY", "pat")], || { | |
| route_for(Harness::Antigravity, AuthMode::Gateway, None) | |
| }) | |
| .unwrap(); | |
| assert!(gateway.destination.fold_function_responses); | |
| assert!(gateway.destination.fold_system_messages); | |
| let google = with_env(&[("GEMINI_API_KEY", "k")], || { | |
| route_for(Harness::Antigravity, AuthMode::Key, None) | |
| }) | |
| .unwrap(); | |
| assert!( | |
| !google.destination.fold_function_responses, | |
| "Google's own API takes a tool result as agy sends it" | |
| ); | |
| } | |
| #[test] | |
| fn the_server_s_own_calls_follow_the_harness_unless_given_a_route() { | |
| // the harness on a personal login: its route is pass-through, named for the provider | |
| let harness = route_for(Harness::Claude, AuthMode::Subscription, None).unwrap(); | |
| assert_eq!(harness.name, "anthropic"); | |
| // nothing asked: the server's calls follow the harness, so a login gives no deputy | |
| let (server, extra) = server_route(&harness, AuthMode::Subscription, None, None).unwrap(); | |
| assert_eq!( | |
| (server.auth, server.provider.as_str()), | |
| (AuthMode::Subscription, "anthropic") | |
| ); | |
| assert!(extra.is_none()); | |
| // a login cannot stand in for the server's calls | |
| let e = server_route( | |
| &harness, | |
| AuthMode::Subscription, | |
| Some(AuthMode::Subscription), | |
| None, | |
| ) | |
| .unwrap_err() | |
| .to_string(); | |
| assert!(e.contains("harness's alone"), "{e}"); | |
| // the same key route as the harness's is the harness's, registered once | |
| with_env(&[("OPENAI_API_KEY", "k")], || { | |
| let keyed = route_for(Harness::Codex, AuthMode::Key, Some("openai")).unwrap(); | |
| let (server, extra) = | |
| server_route(&keyed, AuthMode::Key, Some(AuthMode::Key), Some("openai")).unwrap(); | |
| assert_eq!( | |
| (server.auth, server.provider.as_str()), | |
| (AuthMode::Key, "openai") | |
| ); | |
| assert!(extra.is_none()); | |
| }); | |
| // mining goes through the harness only when the server's calls have no | |
| // credential of their own | |
| assert!(via_harness(MineVia::Auto, AuthMode::Subscription)); | |
| assert!(!via_harness(MineVia::Auto, AuthMode::Gateway)); | |
| assert!(via_harness(MineVia::Harness, AuthMode::Gateway)); | |
| assert!(!via_harness(MineVia::Api, AuthMode::Subscription)); | |
| } | |
| #[test] | |
| fn the_server_s_gateway_route_is_named_for_the_gateway_and_a_clash_is_refused() { | |
| let harness = route_for(Harness::Claude, AuthMode::Subscription, None).unwrap(); | |
| with_env(&[("LLMAO_KEY", "pat"), ("ANTHROPIC_API_KEY", "k")], || { | |
| // the gateway: a route of its own, named for the gateway so it never shares | |
| // the harness's name, with the gateway's ceiling | |
| let (server, extra) = server_route( | |
| &harness, | |
| AuthMode::Subscription, | |
| Some(AuthMode::Gateway), | |
| None, | |
| ) | |
| .unwrap(); | |
| assert_eq!( | |
| (server.auth, server.provider.as_str()), | |
| (AuthMode::Gateway, "llmao") | |
| ); | |
| let extra = extra.expect("a destination to register"); | |
| assert_eq!( | |
| (extra.name.as_str(), extra.ceiling_of.as_str()), | |
| ("llmao", "llmao") | |
| ); | |
| assert_eq!(extra.destination.upstream, "http://127.0.0.1:4000"); | |
| // it takes no provider name | |
| let e = server_route( | |
| &harness, | |
| AuthMode::Subscription, | |
| Some(AuthMode::Gateway), | |
| Some("openai"), | |
| ) | |
| .unwrap_err() | |
| .to_string(); | |
| assert!(e.contains("takes no name"), "{e}"); | |
| // a key to the provider the harness's login already names would collide | |
| // under one name with a different destination; refused rather than guessed | |
| let e = server_route( | |
| &harness, | |
| AuthMode::Subscription, | |
| Some(AuthMode::Key), | |
| Some("anthropic"), | |
| ) | |
| .unwrap_err() | |
| .to_string(); | |
| assert!( | |
| e.contains("anthropic") && e.contains("--server-provider"), | |
| "{e}" | |
| ); | |
| }); | |
| } | |
| #[test] | |
| fn a_grok_login_is_a_tunnel_to_grok_s_own_hosts() { | |
| // a login talks to the cli-chat-proxy and websocket relays on grok.com, and | |
| // refreshes at auth.x.ai: a tunnel in this first cut, pinned without metering | |
| let d = pass_through_provider("grok").unwrap(); | |
| assert!(d.upstream.is_empty(), "a tunnel, not a forward"); | |
| for host in [ | |
| "cli-chat-proxy.grok.com", | |
| "code.grok.com", | |
| "grok.com", | |
| "auth.x.ai", | |
| ] { | |
| assert!( | |
| d.tunnel_hosts.iter().any(|h| h == host), | |
| "{host}: {:?}", | |
| d.tunnel_hosts | |
| ); | |
| } | |
| } | |
| /// The status and body the egress answers `CONNECT host:443` with. A oneshot is | |
| /// never upgraded, so nothing is dialled. | |
| async fn connect(egress: &Egress, host: &str) -> (axum::http::StatusCode, String) { | |
| use tower::ServiceExt; | |
| let r = egress | |
| .router() | |
| .oneshot( | |
| axum::http::Request::builder() | |
| .method(axum::http::Method::CONNECT) | |
| .uri(format!("{host}:443")) | |
| .body(axum::body::Body::empty()) | |
| .unwrap(), | |
| ) | |
| .await | |
| .unwrap(); | |
| let status = r.status(); | |
| let body = axum::body::to_bytes(r.into_body(), 1 << 16).await.unwrap(); | |
| (status, String::from_utf8_lossy(&body).into_owned()) | |
| } | |
| #[tokio::test] | |
| async fn a_google_login_tunnels_to_the_hosts_it_needs_and_not_to_a_vm_s_reverse_name() { | |
| use axum::http::StatusCode; | |
| let dir = tempfile::tempdir().unwrap(); | |
| let memory = Memory::init(&dir.path().join("m"), "human:test:me", "dom-me").unwrap(); | |
| // reading a restricted entry raises the session's label above google's starter | |
| // ceiling, so a host the allowlist admits stops at the ceiling, which names the | |
| // destination, before the egress resolves anything | |
| memory | |
| .write(WriteRequest { | |
| classification: Some(Label::Restricted), | |
| body: "the restricted thing".into(), | |
| ..WriteRequest::default() | |
| }) | |
| .await | |
| .unwrap(); | |
| memory.search("restricted", None, 10).await.unwrap(); | |
| let memory = Arc::new(memory); | |
| let destinations: HashMap<String, Destination> = | |
| ["github", "chatgpt", "google", "grok", "meta"] | |
| .into_iter() | |
| .map(|n| (n.to_string(), pass_through_provider(n).unwrap())) | |
| .collect(); | |
| let egress = Egress::new(memory, destinations, &dir.path().join("t.db")).unwrap(); | |
| // a Compute Engine VM's reverse name, which resolves to the VM, 34.117.59.81; | |
| // then names on Google's and GitHub's shared front ends, which no login's | |
| // CONNECT authority names | |
| for host in [ | |
| "81.59.117.34.bc.googleusercontent.com", | |
| "someone.appspot.com", | |
| "someone-abc123-uc.a.run.app", | |
| "us-central1-someone.cloudfunctions.net", | |
| "someone.storage.googleapis.com", | |
| "someone.github.io", | |
| "raw.githubusercontent.com", | |
| ] { | |
| let (status, body) = connect(&egress, host).await; | |
| assert_eq!(status, StatusCode::FORBIDDEN, "a tunnel to {host}: {body}"); | |
| assert!( | |
| body.contains("no destination allows a tunnel to"), | |
| "a tunnel to {host}: {body}" | |
| ); | |
| } | |
| // a Google login's token refresh and Cloud Code, and Antigravity's eligibility | |
| // check: the account's profile at www.googleapis.com, its picture at lh3. Each is | |
| // admitted by google's entries, and refused only by google's ceiling | |
| for host in [ | |
| "oauth2.googleapis.com", | |
| "accounts.google.com", | |
| "cloudcode-pa.googleapis.com", | |
| "daily-cloudcode-pa.googleapis.com", | |
| "www.googleapis.com", | |
| "lh3.googleusercontent.com", | |
| ] { | |
| let (status, body) = connect(&egress, host).await; | |
| assert_eq!(status, StatusCode::FORBIDDEN, "a tunnel to {host}: {body}"); | |
| assert!( | |
| body.contains("and google accepts at most"), | |
| "a tunnel to {host}: {body}" | |
| ); | |
| } | |
| } | |
| #[test] | |
| fn dashscope_is_a_keyed_forward_and_has_no_login_mode() { | |
| // the DashScope compatible endpoint, keyed from DASHSCOPE_API_KEY; the harness | |
| // appends /v1 to it, so the base ends at compatible-mode | |
| let err = keyed_provider("dashscope").err().map(|e| e.to_string()); | |
| if let Some(e) = &err { | |
| assert!(e.contains("DASHSCOPE_API_KEY"), "{e}"); | |
| } | |
| std::env::set_var("DASHSCOPE_API_KEY", "k"); | |
| let d = keyed_provider("dashscope").unwrap(); | |
| std::env::remove_var("DASHSCOPE_API_KEY"); | |
| assert_eq!( | |
| d.upstream, | |
| "https://dashscope-intl.aliyuncs.com/compatible-mode" | |
| ); | |
| assert!(d.tunnel_hosts.is_empty(), "a forward, metered"); | |
| // Qwen OAuth ended in April 2026: no subscription destination, under either name | |
| for name in ["dashscope", "qwen"] { | |
| let e = pass_through_provider(name).unwrap_err().to_string(); | |
| assert!(e.contains("DASHSCOPE_API_KEY"), "{name}: {e}"); | |
| } | |
| assert_eq!(crate::model::default_model("dashscope"), "qwen-flash"); | |
| } | |
| #[test] | |
| fn an_empty_variable_is_a_missing_one() { | |
| assert_eq!(present(Some("k".into())).as_deref(), Some("k")); | |
| assert_eq!(present(Some(String::new())), None); | |
| assert_eq!(present(Some(" ".into())), None); | |
| assert_eq!(present(None), None); | |
| } | |
| #[test] | |
| fn a_model_is_named_as_the_harness_takes_it_on_the_route() { | |
| let at = |h, a| model_for(h, a, "llmao", "gemma4-26b"); | |
| assert_eq!( | |
| at(Harness::Antigravity, AuthMode::Gateway), | |
| "gemini-api://gemma4-26b", | |
| "a gateway's model is outside Antigravity's catalog" | |
| ); | |
| assert_eq!(at(Harness::Antigravity, AuthMode::Key), "gemma4-26b"); | |
| assert_eq!( | |
| model_for( | |
| Harness::Antigravity, | |
| AuthMode::Gateway, | |
| "llmao", | |
| "gemini-api://x" | |
| ), | |
| "gemini-api://x", | |
| "named in full already" | |
| ); | |
| assert_eq!(at(Harness::OpenCode, AuthMode::Gateway), "llmao/gemma4-26b"); | |
| assert_eq!(at(Harness::Pi, AuthMode::Gateway), "llmao/gemma4-26b"); | |
| assert_eq!(at(Harness::Gemini, AuthMode::Gateway), "gemma4-26b"); | |
| } | |
| #[test] | |
| #[cfg(target_os = "linux")] | |
| fn auto_takes_the_image_only_for_a_harness_it_carries() { | |
| let dir = tempfile::tempdir().unwrap(); | |
| let r = image::rootfs_dir(dir.path()); | |
| std::fs::create_dir_all(r.join("usr/local/bin")).unwrap(); | |
| std::fs::create_dir_all(r.join("opt/memory")).unwrap(); | |
| std::fs::write(r.join("opt/memory/memory"), "").unwrap(); | |
| // npm's entries are links to its own tree, which the host need not have | |
| std::os::unix::fs::symlink( | |
| "/usr/local/lib/node_modules/claude/cli.js", | |
| r.join("usr/local/bin/claude"), | |
| ) | |
| .unwrap(); | |
| assert_eq!( | |
| backend_of(BackendArg::Auto, false, dir.path(), Harness::Claude), | |
| Backend::Image | |
| ); | |
| // a harness the image does not carry runs from this host's install | |
| assert_eq!( | |
| backend_of(BackendArg::Auto, false, dir.path(), Harness::Antigravity), | |
| Backend::Namespaces | |
| ); | |
| // a program is whatever the command names, wherever it lives | |
| assert_eq!( | |
| backend_of(BackendArg::Auto, false, dir.path(), Harness::Command), | |
| Backend::Image | |
| ); | |
| // asked for by name, the image is what it gets | |
| assert_eq!( | |
| backend_of(BackendArg::Image, false, dir.path(), Harness::Antigravity), | |
| Backend::Image | |
| ); | |
| } | |
| #[test] | |
| fn unconfined_wins_and_auto_prefers_the_image_when_unpacked() { | |
| let dir = tempfile::tempdir().unwrap(); | |
| assert_eq!( | |
| backend_of(BackendArg::Wslc, true, dir.path(), Harness::Claude), | |
| Backend::None | |
| ); | |
| assert_eq!( | |
| backend_of(BackendArg::Wslc, false, dir.path(), Harness::Claude), | |
| Backend::Wslc | |
| ); | |
| assert_eq!( | |
| backend_of(BackendArg::Image, false, dir.path(), Harness::Claude), | |
| Backend::Image | |
| ); | |
| assert_eq!( | |
| backend_of(BackendArg::None, false, dir.path(), Harness::Claude), | |
| Backend::None | |
| ); | |
| assert_eq!( | |
| backend_of(BackendArg::Namespaces, false, dir.path(), Harness::Claude), | |
| Backend::Namespaces | |
| ); | |
| let auto = backend_of(BackendArg::Auto, false, dir.path(), Harness::Claude); | |
| if cfg!(target_os = "linux") { | |
| assert_eq!(auto, Backend::Namespaces, "no image unpacked yet"); | |
| let r = image::rootfs_dir(dir.path()); | |
| std::fs::create_dir_all(r.join("usr/local/bin")).unwrap(); | |
| std::fs::create_dir_all(r.join("opt/memory")).unwrap(); | |
| std::fs::write(r.join("opt/memory/memory"), "").unwrap(); | |
| std::fs::write(r.join("usr/local/bin/claude"), "").unwrap(); | |
| assert_eq!( | |
| backend_of(BackendArg::Auto, false, dir.path(), Harness::Claude), | |
| Backend::Image | |
| ); | |
| assert!(matches!( | |
| runner_for(Backend::Image, dir.path()).unwrap(), | |
| Runner::Image(_) | |
| )); | |
| } | |
| assert!(runner_for(Backend::Image, &dir.path().join("empty")).is_err()); | |
| assert!(matches!( | |
| runner_for(Backend::None, dir.path()).unwrap(), | |
| Runner::Unconfined(None) | |
| )); | |
| } | |
| #[test] | |
| fn seatbelt_is_auto_on_macos_and_named_anywhere() { | |
| let dir = tempfile::tempdir().unwrap(); | |
| assert_eq!( | |
| backend_of(BackendArg::Seatbelt, false, dir.path(), Harness::Claude), | |
| Backend::Seatbelt | |
| ); | |
| assert_eq!( | |
| backend_of(BackendArg::Seatbelt, true, dir.path(), Harness::Claude), | |
| Backend::None, | |
| "--unconfined still wins" | |
| ); | |
| assert!(matches!( | |
| runner_for(Backend::Seatbelt, dir.path()).unwrap(), | |
| Runner::Seatbelt | |
| )); | |
| let auto = backend_of(BackendArg::Auto, false, dir.path(), Harness::Claude); | |
| assert_eq!( | |
| auto == Backend::Seatbelt, | |
| cfg!(target_os = "macos"), | |
| "auto on this platform is {auto:?}" | |
| ); | |
| let port = free_loopback_port().unwrap(); | |
| assert!( | |
| port.starts_with("127.0.0.1:") && !port.ends_with(":0"), | |
| "{port}" | |
| ); | |
| } | |
| } | |
| #[cfg(all(test, unix))] | |
| mod socket_sweep_tests { | |
| use super::*; | |
| #[test] | |
| fn a_killed_launcher_s_socket_is_swept_and_nothing_a_live_one_holds() { | |
| let dir = tempfile::tempdir().unwrap(); | |
| let run_dir = dir.path(); | |
| let mut gone = std::process::Command::new("/bin/sh") | |
| .args(["-c", ":"]) | |
| .spawn() | |
| .unwrap(); | |
| let dead = gone.id(); | |
| gone.wait().unwrap(); | |
| let mut live_child = std::process::Command::new("/bin/sh") | |
| .args(["-c", "read x"]) | |
| .stdin(std::process::Stdio::piped()) | |
| .spawn() | |
| .unwrap(); | |
| let live = live_child.id(); | |
| let stale = run_dir.join(format!("egress-{dead}.sock")); | |
| // the file a listener leaves when its process dies: nothing answers there | |
| drop(std::os::unix::net::UnixListener::bind(&stale).unwrap()); | |
| // a dead pid's name on a socket something still listens on: a launcher in | |
| // another pid namespace, whose pid means nothing here | |
| let elsewhere = run_dir.join(format!("egress-{dead}2.sock")); | |
| let _elsewhere = std::os::unix::net::UnixListener::bind(&elsewhere).unwrap(); | |
| let held = run_dir.join(format!("egress-{live}.sock")); | |
| let _held = std::os::unix::net::UnixListener::bind(&held).unwrap(); | |
| let own = run_dir.join(format!("egress-{}.sock", std::process::id())); | |
| let _own = std::os::unix::net::UnixListener::bind(&own).unwrap(); | |
| // a dead pid's name on a file that is not a socket, and on a link | |
| let file = run_dir.join(format!("egress-{dead}0.sock")); | |
| std::fs::write(&file, "").unwrap(); | |
| let outside = tempfile::tempdir().unwrap(); | |
| let target = outside.path().join("kept.sock"); | |
| let _target = std::os::unix::net::UnixListener::bind(&target).unwrap(); | |
| let link = run_dir.join(format!("egress-{dead}1.sock")); | |
| std::os::unix::fs::symlink(&target, &link).unwrap(); | |
| let other = run_dir.join(format!("other-{dead}.sock")); | |
| let _other = std::os::unix::net::UnixListener::bind(&other).unwrap(); | |
| // a few times: a process another test forks holds a copy of the closed | |
| // listener's descriptor until it execs, and a connection to it meanwhile is taken | |
| let mut swept = Vec::new(); | |
| for _ in 0..20 { | |
| swept.extend(sweep_sockets(run_dir)); | |
| if !stale.exists() { | |
| break; | |
| } | |
| std::thread::sleep(std::time::Duration::from_millis(10)); | |
| } | |
| live_child.kill().unwrap(); | |
| live_child.wait().unwrap(); | |
| assert_eq!(swept, vec![stale.clone()]); | |
| assert!(!stale.exists()); | |
| for kept in [&held, &own, &file, &other, &target, &elsewhere] { | |
| assert!(kept.exists(), "{}", kept.display()); | |
| } | |
| assert!(link.is_symlink()); | |
| } | |
| } |