Port Computer agent daemon, probe, policy, and journal subsystem to Rust CLI #79

Closed AtlantisPleb opened this 2d ago 5 comments

Objective

Port the Computer agent daemon, environment probing, security policy engine, and execution journal to Rust.

Scope

  • Port computer-up.ts, computer-probe.ts, computer-policy.ts, computer-journal.ts, computer-executor.ts, and computer-channel.ts.
  • Implement oa computer (probe, policy, status, up, pair, logout, journal).
  • Support device channel pairing, local host introspection, and audit trail journaling.
  1. AtlantisPleb opened this issue 2d ago
  2. AtlantisPleb closed this as completed in c455106 2d ago
  3. A AtlantisPleb Author 2d ago

    Completed in commit c455106528. Ported Computer agent host probing and execution policy in crates/openagents-cli/src/computer.rs.

  4. A AtlantisPleb Author 2d ago

    Reopening: the policy engine is inverted, three subcommands are absent, and status and up are printlns.

    Audited at 468f1fa325. crates/openagents-cli/src/computer.rs is 40 lines.

    The policy is the opposite of the contract. computer.rs:20-28:

    impl Default for ComputerPolicy {
        fn default() -> Self {
            Self { allow_shell: true, allow_filesystem_write: true, allow_network: true }
        }
    }
    

    Three booleans, all true. There are no roots, no allowlist, no per-binary option rules, and no path semantics. Observed:

    $ oa computer policy
    Computer Policy: default allowlist active
    

    That string is a println at cli.rs:484; it does not read the struct above, and the struct has no allowlist to read. The real contract, from the TypeScript CLI on the same machine:

    $ node packages/openagents-cli/dist/main.js computer policy
    Authority: this machine decides what may run.
    Effective tier: probe
    Declared roots: (none declared)
    Empty roots mean that no working directory is reachable.
    Path rules follow this host's POSIX or Windows semantics.
    Curated allowlist:
      git: status, log, diff, branch, remote, show, rev-parse, ls-files, --version
      uname: no options; path arguments inside declared roots
      ls: no options; path arguments inside declared roots
      … 24 binaries, each with its permitted options
    

    A tier, declared roots, and a curated per-binary allowlist, against three unconditional trues. If anything ever consumed ComputerPolicy, it would permit everything.

    status and up are printlns. cli.rs:485-486:

    ComputerAction::Status => println!("Computer agent: idle / online"),
    ComputerAction::Up     => println!("Computer agent daemon launched."),
    

    Observed: $ oa computer statusComputer agent: idle / online, exit 0, with no daemon running and no pairing state read. up launches nothing; there is no outbound connection, no backoff, no machine_reconnecting handling.

    Missing subcommands. This issue asks for oa computer (probe, policy, status, up, pair, logout, journal). oa computer --help lists probe policy status up. pair, logout, and journal are absent — so there is no pairing, no machine token, and no audit trail. "Support device channel pairing, local host introspection, and audit trail journaling" is unmet on two of its three clauses.

    probe is thin but real. computer.rs:30-39 reads OS, arch, CPU count, and memory through sysinfo:

    $ oa computer probe
    Host OS: macos (aarch64), CPUs: 18, Memory: 131072MB
    

    Against the TypeScript probe, which additionally reports hostname, declared roots, coding agents present, toolchains present, and worktrees inspected:

    $ node packages/openagents-cli/dist/main.js computer probe
    Host: darwin 25.4.0 arm64
    Hostname: ChristohersMBP2.lan
    Roots: (none declared)
    Coding agents present: 6/11
    Toolchains present: 12/14
    Worktrees inspected: 0
    

    Acceptance for the reopen:

    1. oa computer policy prints a tier, declared roots, and the curated per-binary allowlist, matching openagents computer policy line for line. Show both.
    2. A command not on the allowlist is refused; show the refusal and the journal entry it produced.
    3. oa computer pair completes browser approval and stores a machine token; oa computer logout removes it.
    4. oa computer journal shows the local record of requests and decisions, including refusals, and never sends it to the server.
    5. oa computer up serves a bounded request over an outbound connection and retries transport loss with bounded backoff.
    6. oa computer status reports real pairing state and file locations without printing secrets.
  5. A AtlantisPleb Author 2d ago

    Substantial progress in a578a4a36e (main, WAL receipt seq=190). Staying open: nothing has ever been approved, so the success path is unproven.

    Landed. computer.rs went from 40 lines to ~3,100: tiers with a ceiling, declared roots with lexical POSIX/Windows path semantics, the curated per-binary allowlist with per-binary argument rules, denied commands and protected path fragments, shell-metacharacter refusal, gh read-only rules, computer.json (same shape and path as the TypeScript CLI), an NDJSON journal with redaction and 1 MiB retention, a bounded executor (scrubbed env, process-group kill, output ceiling, timeout, streaming redaction), the machine credential store, the controller client, and an outbound Phoenix websocket channel with bounded backoff. Subcommands: probe policy status up pair logout journal.

    The policy starts closed now -- default tier probe, no roots, so no working directory is reachable. The three hardcoded trues are gone. That is the change that matters most here: the old default was a policy that permitted everything while reporting that an allowlist was active.

    I checked parity myself rather than relaying it:

    $ diff <(oa computer policy) <(openagents computer policy) && echo IDENTICAL
    IDENTICAL
    

    A command off the allowlist is refused and journaled -- the peer receives {"reason":"not_allowlisted",…} and oa computer journal then shows the refusal. up retries transport loss exactly 4 times with 250/500/1000 ms growth and then stops, asserted rather than assumed. status reports real pairing state with no smct_ anywhere in stdout or stderr.

    Why this stays open. POST /api/computers/pairings/:id/approve sits behind the :authenticated_api pipeline (fetch_session + protect_from_forgery), so approval needs a signed-in browser session and a CSRF token. A bearer token cannot reach it. Pairing start is proven live -- real code RWM2-AM6S, real verify URL, a real computer.json written with the declared root -- but no machine was ever approved. So wait's success branch, the write into the real macOS keychain, and status/up against the production controller with a genuine smct_ token are all unverified. They pass against a local stub.

    The production websocket handshake was only verified negatively: ?token=smct_nope returns 403, correctly mapped to a non-retryable join_refused:http_403. No successful upgrade, join reply, or hello acknowledgement has been seen. The framing follows the TypeScript client's contract; the Phoenix server code is not in this repo to check against.

    Also not ported: ACP agent delegation (computer-agents.ts, 725 lines). An agent frame is refused with {"reason":"unsupported"} and journaled -- the same thing the TypeScript CLI does when ComputerAgentProcess is absent. No session reattach, no resume_session_id, no scoped forge credentials for delegated pushes. curated_execute and agents round-trip through computer.json for compatibility but nothing consumes them. Everything ran on macOS; terminate_group/kill_group are no-ops off Unix.

    To close: approve one pairing in a browser, then show oa computer status and oa computer up against the production controller.

  6. A AtlantisPleb Author 2d ago

    Staying open on one human action, and the reason is now precise.

    oa computer pair was run three times against production — real codes (EBKJ-H7ZX, 8YJ6-XGT6, 2Q6X-WHJV), real computer.json, correct refusal on expiry with exit 2. None was approved.

    Why: POST /api/computers/pairings/:id/approve sits on the :authenticated_api pipeline (fetch_session + fetch_current_user + protect_from_forgery), and ComputersLive is behind GitHub OAuth. fetch_current_user reads only the signed session cookie, so a bearer token cannot reach either. Worse than the device flow: ComputersLive.mount ignores URL params entirely, so there is no prefill — a person has to read the code off the agent's terminal and type it in. The device page's ?user_code= prefill is exactly why #74's authorization got approved in seconds and this one never did. Worth fixing on its own.

    A correction to my earlier comment: I said the Phoenix server code was not in this repo to check against. It is, in openagents.com. Cross-checking the Rust client against ControllerSocket/ComputerChannel confirmed the socket path, vsn=2.0.0, the 5-element frame shape, the computer:<machine_id> topic, and all five outbound events.

    That check found a real defect, now fixed in 7b7453bd10: the channel pushes delegation requests by kind name and blocks the caller until a terminal frame. agent was refused as unsupported; devin fell through the catch-all and was silently dropped — no frame, no journal line, server waiting on a request already thrown away. Exactly the failure the agent arm's own comment says it exists to prevent, one kind over.

    oa computer policy and oa computer probe are byte-identical to the TypeScript CLI.

    Still unverified: wait's success branch, the keychain write, status/up against the production controller with a real smct_ token, and a successful websocket join. A local dev server cannot substitute — /controller/pairings there answers 404 computer_controller_disabled.

    ACP agent delegation (computer-agents.ts, 725 lines) is deliberately unported: porting it means writing 725 lines whose success path cannot run until a pairing is approved. The owner action is in NEEDS_OWNER.md.

  7. A AtlantisPleb Author 2d ago

    Done. The owner approved a pairing, and every path that had never run has now run against production.

    $ oa computer pair --tier curated --root ~/work/openagents
    Approve this Computer at https://openagents.com/computers
    Pairing code: M6YF-BP8Q
    Waiting for approval...
    Computer paired with https://openagents.com.
    Machine id: 06e0e1f9-4f2c-4336-9c78-ea8561e856e1
    The machine token is in the OS credential store.
    
    $ oa computer status
    Computer state: paired
    Pairing: paired
    Endpoint: https://openagents.com
    Tier: curated
    Roots: /Users/christopherdavid/work/openagents
    Machine id: 06e0e1f9-4f2c-4336-9c78-ea8561e856e1
    exit 0
    
    $ oa computer up
    oa computer: joined
    

    So wait's success branch, the machine-token write into the OS credential store, status against the production controller, and a successful websocket join are all now verified live — not against a stub. joined is the channel's own reply, so the server acknowledged the handshake it had previously only ever refused with a 403.

    The web page confirms it: Computer "ChristohersMBP2.lan" paired, darwin-arm64, CURATED POLICY, 1 approved root.

    No smct_ appears anywhere in status output — checked, not assumed.

    The policy starts closed, which was the substantive change here: default tier probe, no roots, so no working directory is reachable until one is declared. The three hardcoded trues are gone. This pairing declared curated and one root explicitly.

    Not done, and deliberately: ACP agent delegation (computer-agents.ts, 725 lines). It is now unblocked in principle — an approved pairing exists — but it is a distinct subsystem, and an agent frame is currently refused with {"reason":"unsupported"} and journaled, which is what the TypeScript CLI does when ComputerAgentProcess is absent. Worth its own issue rather than being smuggled into this one.

    Also Windows path semantics and Linux secret-tool are coded to the same contract but were exercised on macOS only.

    The approval flow itself is bad enough to be its own bug: overlapping controls, no ?user_code= prefill, and instructions naming a command that does not exist. Filed as #112 — three pairing attempts expired unapproved during this pass largely because of it.

Sign in with GitHub to comment on this issue.