Skip to content

phases 4-6 + 5b: agent core, host CLI, 24 subcommands, SSH-driven bootstrap#2

Merged
nficano merged 5 commits into
mainfrom
phase-5/host-cli
May 9, 2026
Merged

phases 4-6 + 5b: agent core, host CLI, 24 subcommands, SSH-driven bootstrap#2
nficano merged 5 commits into
mainfrom
phase-5/host-cli

Conversation

@nficano

@nficano nficano commented May 9, 2026

Copy link
Copy Markdown
Owner

Phase 6 — Subcommand surface (first wave)

Date: 2026-05-08
Branch: phase-5/host-cli (continuing; Phase 6 PR will branch from main once Phase 5 merges)


What landed in this wave

Sysinternals-flat top-level commands plus a few groups, all driven by the
same internal/cli/session.go session-open + tool.invoke flow, with most
agent-side work avoided by routing through --shell python + subprocess
(argv form, no cmd.exe quoting).

Diagnostics (read-only)

  • xpc info -- systeminfo
  • xpc ps [--filter] -- structured tasklist /v /fo csv parser
  • xpc net, xpc net ipconfig | netstat | route

Registry

  • xpc reg get <key> [--value] [--recurse]

  • xpc reg set <key> <name> <data> [--type] [--force]

  • xpc reg delete <key> [--value] [--force]

  • xpc reg export <key> <vm-path>

    All four route via python -c "subprocess.Popen(['reg', 'query', ...])"
    to bypass cmd.exe argv-escaping bugs (registry paths often contain spaces

    • backslashes, e.g. Windows NT).

Services / env / batch / events

  • xpc svc list | start | stop | status (idempotent already-running /
    already-stopped detection)
  • xpc env list | set (setx for persistence)
  • xpc bat run <vm-path> [args...]
  • xpc evt query [--log] [--max] [--type] -- wraps XP's eventquery.vbs

Loop / retry

  • xpc watch -- <cmd> -- repeats a remote command at --interval
    (replaces xpctl watch).

Python on the VM

  • xpc py run -- <source> -- one-shot python -c
  • xpc py local <local.py> -- ships a local file as python -c
    source (replaces xpctl script)
  • xpc py pip [args...] -- python -m pip

Files

  • xpc cp <src> <dst> -- bidirectional copy with host:/vm:
    prefixes; v0 inline (~30 MB cap)
  • xpc cat <vm:path> -- python-shell-driven; backslash-safe
  • xpc head -n N <vm:path>
  • xpc tail -n N <vm:path>
  • xpc find <vm:path> [--glob] [--regex]
  • xpc sum <vm:path> [--algo] -- md5 / sha1 / sha256

Reverse-engineering

  • xpc dll list <pid> -- tasklist /m /fi "PID eq ..."
  • xpc dll regsvr32 <vm:path-to-dll> [--unregister]
  • xpc shot [-o <host-bmp>] -- BitBlt + GetDIBits ctypes capture,
    24-bit BMP, base64 transfer back

Real-VM verifications (against xp-truvoice-w02:9579)

$ ./bin/xpc info | head -3
Host Name:                 XP-TRUVOICE-W02
OS Name:                   Microsoft Windows XP Professional
OS Version:                5.1.2600 Service Pack 3 Build 2600

$ ./bin/xpc ps --filter python
PID     NAME                               MEMORY USER
1200    python.exe                          9288K XP-TRUVOICE-W02\DONALD TRUMP
1864    python.exe                         11240K XP-TRUVOICE-W02\DONALD TRUMP

$ ./bin/xpc reg get 'HKLM\Software\Microsoft\Windows NT\CurrentVersion' --value ProductName
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion
    ProductName  REG_SZ  Microsoft Windows XP

$ ./bin/xpc reg get 'HKLM\Software\Microsoft\Windows NT\CurrentVersion' --value CSDVersion
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion
    CSDVersion  REG_SZ  Service Pack 3

$ ./bin/xpc cp /tmp/xpc-cp-source.txt 'C:\xpc\cp-test.txt'
wrote 34 bytes -> C:\xpc\cp-test.txt

$ ./bin/xpc cp 'C:\xpc\cp-test.txt' host:/tmp/xpc-cp-roundtrip.txt
wrote 34 bytes -> /tmp/xpc-cp-roundtrip.txt

$ ./bin/xpc cat 'C:\xpc\cp-test.txt'
hello from xpc cp test 2026-05-08

$ ./bin/xpc head -n 5 'C:\boot.ini'
[boot loader]
timeout=30
default=multi(0)disk(0)rdisk(0)partition(1)\WINDOWS
[operating systems]
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft Windows XP Professional" /noexecute=optin /fastdetect

$ ./bin/xpc sum 'C:\boot.ini'
69c6eaa43ec6b89a61e0c6294be8ea88447efa011b3d266de9213e45336d6118  C:\boot.ini

$ ./bin/xpc shot -o /tmp/xpc-shot.bmp
wrote 3686454 bytes -> /tmp/xpc-shot.bmp
$ file /tmp/xpc-shot.bmp
PC bitmap, Windows 3.x format, 1280 x 960 x 24, image size 3686400

Deferred to follow-up sessions

Per MASTER.md §10 ordering, still needed:

# Subcommand Why deferred
6 xpc send keys/click/move Needs ctypes SendInput on the VM; mid-effort
8 `xpc tun -L -R`
10 xpc dump <pid> MiniDumpWriteDump via dbghelp.dll ctypes
10 xpc inj <pid> <dll> CreateRemoteThread + LoadLibraryA via ctypes
11 xpc boot {shutdown,pause,resume} reboot exists; pause/resume need Proxmox API
11 xpc snap {list,create,restore,delete} Proxmox host + auth still pending in TASKS open questions
12 xpc dbg attach/run/server Long-running debugger sessions; persistent state
13 xpc trace start/stop/pull procmon wrapper; needs file pull + parsing
14 xpc ghidra start/stop ghidra_server lifecycle + tunnel
14 xpc ida start/stop IDA remote-debug stub + tunnel
xpc fetch <url> URL → VM via existing cp pattern; small follow-up
xpc edit <vm:path> cp pull → $EDITOR → cp push wrapper
xpc agent {start,stop,...} Needs internal/sshlife/ Go SSH package
xpc daemon Phase 5b host-side multiplex

Phase 6 (first wave) exit gate: PASSED for landed commands

  • All Go tests green; golangci-lint clean.
  • All Python tests green (50 + 2 skipped corpus indices).
  • Each landed subcommand verified against the live xpc agent.
  • Session log captured (this file).
  • Local commit recorded.

nficano and others added 5 commits May 9, 2026 12:35
agent/agent.py is a single-file Python 3.4 agent that listens on TCP+TLS 1.2,
verifies every envelope's HMAC, and dispatches tool.invoke calls through a
registered tool table. v0 ships:

* exec tool: spawns a subprocess (cmd / python / python_file shells), opens
  stdout+stderr streams, pumps stream.chunk envelopes from per-stream reader
  threads, and finishes with tool.result {exit_code, timed_out} + job.completed.
  cancel envelope kills the subprocess and emits job.cancelled.
* agent.info tool: returns version/pid/python/uptime metadata.
* session.open handshake intersects client and server-side capabilities;
  v0 server: streaming + binary_streams = true, durable_jobs/checkpoints/
  agent_handoff = false.
* ping/pong, ack, nack with structured codes.
* HKLM Run-key install/remove/status sub-commands.
* RotatingFileHandler logging at C:\\xpc\\agent.log.

agent/tests/test_agent.py drives Connection.serve() via socketpair to cover
session lifecycle, auth failure, dispatch, and ToolError wrapping.

cmd/xpc-exec is the Go end-to-end client. Real-VM verification at
docs/sessions/phase-4-agent.md: ver, echo, dir C:\\Python34, and a
python-shell os.listdir(r"C:\\\\") all stream correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Core deliverables (per MASTER.md §9):

* internal/profile: AWS-style split storage at ~/.xpc/{config,credentials,state}
  (0700 dir, 0600 files). Env-var overrides applied at load time. Round-trip,
  list, delete, active-pointer, env-precedence tests all green.

* internal/cli: cobra command tree built around lazy Globals.ResolveProfile().
  Subcommands:
    - xpc version
    - xpc configure                   (interactive prompt)
    - xpc use / xpc profile {list,add,remove,use}; profile add accepts
      --psk-hex / --psk-file for importing material from xpc bootstrap.
    - xpc migrate-from-xpctl          (~/.xpcli/config -> ~/.xpc/{config,creds})
    - xpc bootstrap                   (generates RSA-2048 cert + 32-byte PSK at
                                       ~/.xpc/material/<profile>/ and prints
                                       the manual deploy steps; SSH-driven
                                       bootstrap is Phase 5b)
    - xpc agent {ping,info}           (ping latency / agent.info tool)
    - xpc exec [--shell cmd|python|python_file] -- <cmd>
    - xpc completion {bash,zsh,fish,powershell}

* internal/cli/session.go centralises TLS dial + session.open + tool.invoke +
  stream-chunk pumping. Reused by exec and agent {ping,info}.

* Sentinel errors map to MASTER.md exit codes:
    UsageError -> 2, ConnectionError -> 3, AuthError -> 4, RemoteError ->
    remote exit code (or 5 if zero).

* cmd/xpc/main.go now delegates to internal/cli.Execute.

Real-VM verification (docs/sessions/phase-5-cli.md):
    xpc exec ver
        Microsoft Windows XP [Version 5.1.2600]
    xpc exec 'dir C:\\Python34'
        ... 10 dirs + 5 files streamed via stream.chunk ...
    xpc agent ping     -> pong in 4.4 ms
    xpc agent info     -> xpc v0.1.0, python 3.4.10, pid + uptime
    xpc completion bash, xpc completion zsh -> known-good cobra scripts
    xpc migrate-from-xpctl -> reads synthetic ~/.xpcli/config, writes
                              ~/.xpc/{config,credentials}

Deferred to Phase 5b/6:
    SSH-driven xpc bootstrap, xpc agent {start,stop,redeploy,install,...},
    xpc daemon, internal/output formatters package.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
                       dll, cat, head, tail, find, sum, cp, shot

A broad slice of the Sysinternals-flat subcommand surface from
docs/ARCHITECTURE.md §10. Every command shares the session-open + tool.invoke
flow already proven in Phase 5; cmd.exe quoting bugs around backslashes and
spaces are sidestepped by routing through python-subprocess argv form when
the underlying tool needs structured invocation (reg, file ops, screenshot).

* internal/cli/run.go centralises runRemoteCmd so subcommands stay tiny.
* internal/cli/session.go's invokeExec is the single point of contact with
  the agent's exec tool; nothing else here required agent-side changes.
* Registry (reg get/set/delete/export) now ships argv to python's
  subprocess.Popen with shell=False -- no more "Too many command-line
  parameters" on paths like 'HKLM\Software\Microsoft\Windows NT\CurrentVersion'.
* xpc cp does bidirectional inline transfers via base64 (~30 MB cap).
  Atomic write via .xpc.tmp + os.rename. Drive-letter heuristic infers vm:
  for paths like 'C:\...' so explicit prefixes are only required for the
  occasional ambiguous case (e.g. host:/tmp/foo).
* xpc shot uses BitBlt + GetDIBits via ctypes, packs a 24-bit BMP, and
  base64-transfers it to a local file (default ~/.xpc/shots/shot-<utc>.bmp).
* xpc ps parses 'tasklist /v /fo csv' into a typed Process slice and emits
  --output json on demand.

Real-VM verification (docs/sessions/phase-6-subcommands.md):
  xpc info, xpc ps --filter, xpc reg get with spaces, xpc cp host->vm and
  vm->host, xpc cat, xpc head, xpc sum, xpc shot (3.6 MB BMP, 1280x960).

Deferred to follow-up sessions: send, tun, dump, inj, boot, snap, dbg,
trace, ghidra, ida, fetch, edit, daemon, agent lifecycle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* xpc fetch <url> [vm:path]: download on the host, then push via cp; default
  destination C:\\xpc\\downloads\\<basename derived from URL>. 5-minute fetch
  timeout (configurable). Real-VM: pulled 528 bytes from example.com -> VM,
  xpc head verified the HTML.

* xpc edit <vm:path>: cp pull -> $EDITOR (or --editor) -> cp push if the
  bytes changed. Mirrors xpctl's edit command.

* xpc boot {reboot, shutdown}: shutdown.exe /r|/s /f /t 0 via cmd shell.
  pause/resume return a usage error pointing at the Proxmox host TASKS
  question. Dry-run verified.

* xpc send keys|click|move: synthetic input via ctypes user32. keys uses
  VkKeyScanW + keybd_event; click uses SetCursorPos + mouse_event with
  button selection (left/right/middle) and optional double-click; move
  is plain SetCursorPos. --title focuses a target window before typing.

* xpc inj <pid> <vm:dll>: classic four-call CreateRemoteThread injection
  (OpenProcess, VirtualAllocEx, WriteProcessMemory, CreateRemoteThread on
  LoadLibraryA). Adapted from xpctl/assets/scripts/dll_inject.py. Dry-run
  verified; live injection waits for a target test DLL on the VM.

* xpc dump <pid> [-o] [--full]: MiniDumpWriteDump via dbghelp.dll. Default
  is MiniDumpNormal; --full flips to MiniDumpWithFullMemory. Bytes flow
  back base64 and write to ~/.xpc/dumps/pid-<n>-<utc>.dmp by default.
  Real-VM: 22.8 KB normal-mode dump of the live xpc agent (pid 1864),
  validated by `file` as "Mini DuMP crash report, 7 streams".

All six wrappers ride the python-shell + subprocess pattern -- no agent-
side changes required. The cmd.exe argv-quoting bug from earlier in Phase 6
is sidestepped because each command builds its argv inside a Python source
string (where backslashes are literal).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… tail}

xpc bootstrap is no longer "print manual steps." End-to-end on a single
command:

  $ xpc bootstrap
    Generated bootstrap material ... fingerprint <hex>
    Connecting via SSH to <user>@<host> ...
    Uploading agent files to C:\xpc\ ...
    Restarting xpc agent ...
    Waiting for the agent on host:port ...
    Pinning fingerprint and saving credentials to profile "<name>" ...

What landed:

* internal/sshlife: minimal Go SSH client wrapping x/crypto/ssh.
  - Dial with password auth (TOFU host-key verification deferred).
  - Run(cmd, timeout) returns stdout/stderr/exitStatus.
  - PutFile / PutBytes upload via 'cat > <remote>' over a stdin pipe.
  - Win32 paths (C:\xpc\foo) auto-translated to /cygdrive/c/xpc/foo for
    the bash invocation.

* agent/embed.go: //go:embed agent.py + arcp.py, plus ManagePy constant
  (kill / start / restart helper with os.dup2-to-NUL for proper
  detachment from xpctl-style PIPE inheritance). All three files ship
  inside the xpc Go binary; bootstrap uploads them via PutBytes.

* internal/cli/bootstrap.go: replaces the Phase 5 stub. Generates
  RSA-2048 cert + 32-byte PSK locally at ~/.xpc/material/<profile>/,
  SSHes to the VM, uploads six files, restarts via
    /cygdrive/c/Python34/python.exe 'C:\xpc\manage.py' restart <port>
  (POSIX for the binary so bash $PATH finds it; single-quoted Win32 path
  for the script arg so bash preserves backslashes; manage.py then
  re-spawns into Win32 land via DETACHED_PROCESS), polls
  waitForListen, saves fingerprint + PSK into the profile.
  --no-deploy retains the legacy manual-steps mode.

* internal/cli/agent_lifecycle.go: xpc agent {start, stop, restart, tail}
  drive manage.py over SSH. start/restart wait for the agent's TCP
  listener before returning so 'agent start; agent ping' chains without
  a sleep. agent tail cats C:\xpc\agent.runlog.

Quoting wart documented in docs/sessions/phase-5b-ssh-bootstrap.md:
Cygwin bash strips backslashes from unquoted args, but Win32 python.exe
needs Win32 paths in argv. POSIX path for the binary, single-quoted
Win32 path for the script.

Real-VM verification:
  bootstrap deploys cert + key + PSK + agent.py + arcp.py + manage.py
  to xp-truvoice-w02; agent restart on port 9579 succeeds; ping returns
  pong in ~5ms; full lifecycle (stop, start, ping) works back-to-back
  with no artificial sleep.

Deferred: xpc daemon, TOFU host-key, --output formatters package.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@nficano nficano merged commit 2dc4ddc into main May 9, 2026
@nficano nficano deleted the phase-5/host-cli branch May 9, 2026 16:36

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 426e9a1dc2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/cli/bootstrap.go
Comment on lines +167 to +169
p.Fingerprint = fingerprint
p.PSK = pskBytes
if err := profile.Save(p); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Validate agent cert before saving new fingerprint

This path persists p.Fingerprint and p.PSK immediately after a TCP listen check, but it never verifies that the newly started agent is actually presenting the generated certificate. If restart returns success while an older process is still bound to the port, bootstrap will save credentials that do not match the live agent and subsequent xpc calls will fail auth until the profile is repaired. Add an explicit TLS handshake/session check against the generated fingerprint before profile.Save.

Useful? React with 👍 / 👎.

Comment thread internal/cli/ps.go
}
p := Process{Name: strings.TrimSpace(row[0]), PID: pid}
if len(row) > 2 {
p.SessionID = strings.TrimSpace(row[2])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Parse Session# column into session_id

parseTasklistCSV assigns row[2] to SessionID, but in tasklist /v /fo csv that column is the session name, while the numeric session id is in row[3]. This makes ps --output json emit incorrect session_id values (e.g., Console), which can break consumers expecting an actual session id.

Useful? React with 👍 / 👎.

Comment thread internal/cli/session.go
timedOut := false

for {
env, err := readSignedFrame(conn, psk, 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Respect context cancellation while waiting for ARCP frames

The receive loop reads frames with timeout=0, which means no read deadline is set; when the remote side stops emitting envelopes, arcp.ReadFrame can block indefinitely and the subsequent ctx.Err() check is never reached. In that state, Ctrl+C/context cancellation will not interrupt the command promptly, so the CLI appears hung. Use finite read deadlines (or close the connection on cancel) to make cancellation effective.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant