Remote execution
Reference for anchoring: how a turn whose work lands on another machine is run, where each part of it runs, what crosses between machines, and what is refused. The layer is CogAnchor, hmz.coganchor.anchor and its command line is hmz internal anchor. Where an agent's turns land is set per agent by machine= and per flow session by its environment; where the harness runs is set per saved runtime by its harness setting (below).
Terms
| Term | Definition |
|---|---|
| Anchor | The settings that send one agent's turns to another machine: an AnchorConfig. An agent with no anchor runs its turns on this machine as ordinary processes. |
| Target | The machine the work lands on: the files the agent reads and writes and the commands it runs. Named by a target spelling. |
| Serving half | hmz internal anchor serve, running on the target. It answers file operations and runs commands inside the directories it exports. |
| Harness | The agent's own process (the coding agent CLI) and, when supervised, the supervisor tracing it. |
| Supervisor | A seccomp-filtered ptrace tracer that stops the agent's path, exec and connect syscalls and answers them from the target. |
| Mirror (shadow) | A directory on the harness's machine that reproduces the target's workspace, filled lazily. The supervised agent works in it. |
| Arrangement | How one anchor runs its harness relative to the target: supervised here, supervised on another machine (afar), or the target's own CLI (native). A flow session's harness location becomes one of them: local is supervised, standalone is afar, env is native. |
| Broker (rendezvous) | A TCP server that introduces a harness and a serving half on two machines that cannot dial each other, and relays their bytes when no direct connection can be made. |
| Ticket | A 128-bit secret naming one meeting at a broker. |
| Export | A directory the serving half exposes, as VIRTUAL[:REAL]: the path the agent names, and where it really is on the target. |
| Fence | What an agent's processes may reach (Agents › The fence). Under an anchor it is held on both machines (below). |
Arrangements
Two fields decide the arrangement: native and harness.
Supervised (harness local) | Afar, beside the work (harness standalone) | Afar, third machine (harness standalone) | Native (harness env) | |
|---|---|---|---|---|
| Setting | harness="local" (default), native=False | harness="same", or a harness target equal to target | harness= an ssh:// or docker:// target other than target | native=True |
| Agent process and supervisor | this machine | the target | the harness machine | none: the target's installed CLI runs there |
| Mirror | this machine, at shadow (default: the workspace's own path) | the target, in its mirror cache, kept between turns | the harness machine, in its mirror cache, kept between turns | none |
| Account, CLI state, model connection | this machine | the target | the harness machine | the target, for each turn |
| What crosses to this machine | one request per trapped syscall, file contents, command I/O | the agent's stdin, stdout, stderr | the agent's stdin, stdout, stderr; file work goes between the other two through a rendezvous | the CLI's stdin, stdout, stderr, signals, exit status |
| Capabilities | anchor:supervised | anchor:supervised, anchor:afar | anchor:supervised, anchor:afar | anchor:native-cli |
native=Truewith a harness that parses to a non-localmachine is refused (see Validation).- A
harnesswhose spelling resolves to alocaltarget (local,local:DIR, orsamewith alocaltarget) is the supervised arrangement:AnchorConfig(target="local:/x", harness="same").capabilitiesis{"anchor:supervised"}. AnchorConfig.capabilitiesis computed from the settings alone; nothing connects.
supervised native
this machine target this machine target
+-------------+ +-----------+ +---------------+ +-------------+
| agent CLI | | | | hmz internal | | agent CLI |
| | syscalls | | anchor | | anchor |--->| files, |
| supervisor -+---->| serve | | --native | | commands, |
| mirror | | files, | | 3 streams | | network |
+-------------+ | commands | +---------------+ +-------------+
+-----------+
afar, third machine
this machine harness machine target
+----------------+ +-------------------+ +--------------+
| hmz internal |----->| agent CLI | | anchor serve |
| anchor | ssh/ | supervisor -------+----->| files, |
| 3 streams |docker| mirror (kept) | peer | commands |
| broker |<-----+-------------------+------+ |
+----------------+ +-------------------+ +--------------+Defined in src/hmz/coganchor/anchor.py (AnchorConfig.capabilities), src/hmz/coganchor/places.py, src/hmz/coganchor/elsewhere.py.
A project's own programs
A CLI starts some programs because the project it works in says so: hooks, MCP servers it spawns over stdio, helper commands, plugins. It starts them where the CLI runs. A flow session runs only where that is the work's own machine:
| Arrangement | CLI and work on one machine | A project that would start programs |
|---|---|---|
| No anchor (work here) | yes | allowed |
| Native | yes | allowed |
Afar, beside the work (harness="same") | yes | allowed |
| Supervised | no | refused |
| Afar, third machine | no | refused |
In a supervised or third-machine arrangement, Humanize reads the files the CLI's Profile.launchers lists when a session opens, moves or carries on a conversation, and again before each turn. It reads them from the target's copy of the workspace, in the session's directory and each directory above it up to the workspace. A file counts if a listed key holds a value other than null, false or an empty string, table or list. A file listed with no keys, or a listed plugin directory that is not empty, counts by being there. If anything counts, the open or the turn fails with UnsupportedOperation before the CLI starts. The error names every file and key it found:
claude runs on another machine than its work on ssh://box, and the project's own configuration
there would have it start programs on claude's machine rather than the work's:
.claude/settings.json: hooks.*.*.hooks.*.command; .mcp.json: mcpServers.*.command. Run it
natively on ssh://box, or take those out of the projectA file that cannot be parsed, or that the target will not let be read, counts and is named (unreadable). If the target cannot be reached, the open or the turn fails with HarnessDropped. The per-CLI files and keys are listed under Agents.
Defined in src/hmz/coganchor/crossing.py and HarnessDriver.crossed in src/hmz/runtime/flowing/harnesses.py.
Targets
A target spelling names the machine the work lands on. The same grammar names a harness machine in AnchorConfig.harness.
| Spelling | Machine | How it is reached | Something started on it | Requirements there |
|---|---|---|---|---|
local | this machine, standing in for another | the serving half is a child process here | yes | none |
local:DIR | this machine, with DIR standing in for the far side's copy of the workspace | as local | yes | none |
ssh://[USER@]HOST[:PORT][?KEYWORD=VALUE&...] | a host reached with the system ssh | the serving half is bootstrapped over ssh and spoken to on its pipes | yes | POSIX /bin/sh, Python >= 3.12 |
docker://CONTAINER[?OPTIONS][@ENDPOINT] | a running container | the serving half is bootstrapped with docker exec -i, running as the container's user or the one OPTIONS names, with its variables and under its wrapper | yes | docker here; /bin/sh and Python >= 3.12 in the container |
apple-container://CONTAINER | a running Apple container on this Mac | the serving half is bootstrapped with container exec -i, running as the container's user | yes | Apple's container here; /bin/sh and Python >= 3.12 in the container |
slurm://JOBID[?python=PATH] | the node a running Slurm job of this user's was given | the serving half is bootstrapped with srun --jobid=JOBID --overlap, a step of that job sharing its allocation, running as this user | yes | srun here; /bin/sh and Python >= 3.12 on the node (PATH, absolute or under ~/, is tried first) |
tcp://HOST:PORT | a serving half left listening | a TCP connection, TCP_NODELAY, 30 s connect timeout | no | a served target and its token |
peer://TICKET@HOST:PORT | a serving half waiting at a broker | the broker at HOST:PORT, presenting TICKET | no | written by Humanize only |
Parsing rules:
ssh://: the authority is split at its last:into host and port when the part after it is all digits; otherwise the whole authority is the host (user@hostincluded). After?, options areKEYWORD=VALUEpairs separated by&, URL-quoted, parsed strictly.KEYWORDmatches[A-Za-z][A-Za-z0-9]*;VALUEmust be non-empty and must not contain a newline, carriage return, NUL or". Each option is passed tosshas-o KEYWORD=VALUE(the value double-quoted where it contains whitespace), exceptF, passed as-F VALUE.docker://: the container name ends at the first?or@; what follows the first@is a docker endpoint. An endpoint oflocalis the same target as none. Between a?and the@, options areKEY=VALUEpairs separated by&, URL-quoted (an@in a value as%40), parsed strictly, none holding a newline, carriage return or NUL, and passed to everydocker execon the target's road in the order written:Option Passed as Value user=UID[:GID]-u UID[:GID]numeric; at most once env=NAME=VALUE-e NAME=VALUENAMEmatches[A-Za-z_][A-Za-z0-9_]*; any numberexec=WORDa word after the container name, ahead of the command non-empty; any number The
execwords are a wrapper: a program in the container (with its arguments) that runs the command it is given after them, with the same streams, and exits with its status, asexec "$@"does. The target is written on command lines and in logs, so anenvvalue is no place for a secret. A target naming auserkeeps its archive and mirrors in/tmp/humanize-<UID>and/tmp/humanize-mirrors-<UID>instead (see Bootstrapping). The options are part of the target: two spellings telling one container different things are two targets, each given the archive once.apple-container://: the container name, holding no@,/,?or#.slurm://: the job's id, all digits; after?, onlypython=PATH, URL-quoted,PATHabsolute or under~/(the node's home) and holding no newline, carriage return, NUL or".tcp://:HOST:PORTsplit at the last:; the port must be digits.peer://:TICKET@HOST:PORT; brackets around an IPv6 host are stripped.
| Input | ValueError message |
|---|---|
| an unknown scheme | unsupported target 'bogus://x'; expected ssh://HOST, docker://CONTAINER[@ENDPOINT], apple-container://CONTAINER, slurm://JOBID[?python=PATH], tcp://HOST:PORT, peer://TICKET@HOST:PORT or local[:PATH] |
a slurm:// query other than one python=PATH | malformed target 'slurm://12?py=x'; expected slurm://JOBID[?python=PATH] |
tcp:// without a numeric port | malformed target 'tcp://h'; expected tcp://HOST:PORT |
| an ssh option that is not one | malformed target 'ssh://h?Bad-Key=1'; Bad-Key='1' is not an ssh option |
| an ssh query that does not parse | malformed target '<spec>'; expected ssh://HOST?KEYWORD=VALUE&... |
| a docker option that is not one | malformed target 'docker://c?user=me'; user='me' is not a docker exec option |
| a docker query that does not parse | malformed target '<spec>'; expected docker://CONTAINER?user=UID[:GID]&env=NAME=VALUE&exec=WORD&... |
| a bad docker endpoint | unsupported docker endpoint 'weird'; expected local, unix:///PATH, tcp://HOST:PORT[?tls=DIR], ssh://[USER@]HOST[:PORT][?KEYWORD=VALUE&...] or context:NAME |
| a malformed meeting | malformed meeting '<spec>'; expected TICKET@HOST:PORT |
Bootstrapping the serving half
For local, ssh://, docker://, apple-container:// and slurm://, Humanize starts hmz internal anchor serve --stdio --export WORKSPACE[:REAL] on the far side and speaks the protocol over its stdin and stdout.
| Step | Behaviour |
|---|---|
| Archive | A zipapp of hmz.coganchor and the command line reaching it, with an hmz-<version>.dist-info/METADATA so that __version__ and hmz --version answer the same on the target, built once per source tree into $TMPDIR/humanize-<uid>/humanize-<digest>.pyz (directory 0700, refused if anyone else can write it), beside a <stamp>.digest naming the archive each source tree (by version, path, file sizes and modification times) built. Two checkouts or versions never share an archive. A run touches the archive it uses at least hourly; a build removes anything in the directory, and the $TMPDIR/humanize-<uid>.pyz an earlier Humanize shared, untouched for 14 days. A directory anyone else can write fails the install with ConnectionError: could not install Humanize on <target>: …. The driving half (agents, providers, machines, backends.py, models.py, fallbacks.py, prices.py) is left out. Pure standard library; runs on any architecture. |
| Name on the target | humanize-<digest>.pyz, <digest> the first 16 hex digits of the archive's SHA-256. |
| Cache on the target | $HOME/.cache/humanize/ over ssh and on a Slurm node, /tmp/humanize/ in a container, and /tmp/humanize-<UID>/ in a container whose target names user=UID[:GID]. That one, and the target's mirror directory, are made by the far side's shell before anything is put in them or run from them, refused unless each is a directory (not a link) the user owns, and made 0700: Humanize: <dir> is not a directory this user owns, exit 1. |
| Install | Run with the interpreter below, HUMANIZE_BUNDLES set to the cache and mkdir -p'd by the far side's shell, the archive on stdin: refuse it (Humanize: <n> bytes arrived that are not humanize-<digest>.pyz) unless its SHA-256 starts with <digest>; keep a file already there only if its own SHA-256 does too, touching it; otherwise write <file>.<pid> and move it into place. Then remove other humanize-*.pyz* in the cache untouched for 14 days. Every line that runs the archive first touch -ces it. Concurrent installs are safe. Each (target, digest) pair is pushed at most once per process. |
| Interpreter | The first candidate that exists and reports a version >= 3.12, tried in order: a slurm:// target's python, then python3, python3.14, python3.13, python3.12, /opt/homebrew/bin/python3, /usr/local/bin/python3, /Library/Frameworks/Python.framework/Versions/Current/bin/python3, /usr/bin/python3. |
| No interpreter | Exit 127, stderr Humanize: no python 3.12 or newer on this machine; looked for: <candidates>. |
| Install failure | ConnectionError: could not install Humanize on <target>: <stderr>; for a container, ; the container said: <last 3 log lines> is appended. |
| Protocol | Both halves send PROTOCOL_VERSION (currently 1). A mismatch fails the handshake with EPROTO, client speaks protocol <n>, this Humanize speaks 1, and closes the connection. |
The handshake reply (Op.HELLO) carries version, hostname, platform (sys.platform), python, pid, exports ([{virtual, real}]) and fence ({fs: bool, net: bool}, see below). A token mismatch fails it with EACCES, invalid token.
ssh connections
Every ssh Humanize runs is:
ssh <target options> -T -o BatchMode=no -o ServerAliveInterval=30 <reuse options> [-p PORT] HOST '<quoted command>'- The target's own options come first;
sshkeeps the first value it is given for a keyword. - Reuse options, unless
HUMANIZE_SSH_REUSEisoff,0,noorfalse(trimmed, case-insensitive) or set and empty:-o ControlMaster=auto -o ControlPersist=120 -o ControlPath=<dir>/%C[-<8 hex>], where<dir>is$XDG_RUNTIME_DIR/humanize-ssh-<uid>(else the system temporary directory), mode0700— or/tmp/humanize-ssh-<uid>where that path would leave the socket longer than a Unix socket path may be (103 bytes; a Mac's$TMPDIRalone is about 49). Connections are not shared where<dir>is not a directory of the user's that nobody else may enter. The-<8 hex>suffix is a digest of the target's options, so two targets at one host with different options use different master connections. - The command is sent as one string, each word shell-quoted, prefixed with
exec.
Slurm steps
Every command Humanize runs on a slurm://JOBID target is a step of that job:
srun --jobid=JOBID --overlap --nodes=1 --ntasks=1 --unbuffered --quiet --export=HOME,USER --chdir=/ <argv>--overlapshares the job's allocation rather than waiting for a share of it; the step runs on the job's one node, within its CPUs, memory and GPUs.- The argv is passed as it is, not through a shell; nothing of this process's environment crosses but
HOMEandUSER, nor its directory, which the node may not have. - Each step that is running holds listening ports on this machine, from the cluster's
SrunPortRange, which may be only a few dozen ports shared by every user here: a cluster withSrunPortRange=30927-30951holds about 8 connections at once. A Slurm runtime saved withvia: tcpholds none: its job runs the serving half listening on its node, andsrunis used only to put the archive there and read the port.
Defined in src/hmz/coganchor/transport.py (Target, Road, python_command, build_bundle), src/hmz/coganchor/proto.py, src/hmz/coganchor/serve/server.py.
AnchorConfig
hmz.coganchor.AnchorConfig is a frozen, keyword-only dataclass. Every field is an option of hmz internal anchor, and every option but --check and --log-level is a field; hmz.coganchor.argv reads one into the other and renders it back (python -Pm hmz internal anchor <options> AGENT ARGS...).
| Field | Type | Default | Option | Option default | Meaning |
|---|---|---|---|---|---|
target | str | "local" | --target URL | $HUMANIZE_TARGET, else local | Where the work lands. See Targets. |
harness | str | "local" | --harness WHERE | $HUMANIZE_HARNESS, else local | Where the harness runs: local, same (the machine target names), or a target spelling. |
broker | str | "" | --broker HOST | $HUMANIZE_RENDEZVOUS, else "" | The address both halves dial to be introduced. "": $HUMANIZE_RENDEZVOUS, else this machine's outward-facing IPv4 address. |
workspace | str | None | None | --workspace PATH | None | The project directory as the target names it. None: this process's working directory. |
chdir | str | None | None | --chdir PATH | None | Where inside the workspace the agent starts, as the target names it. Must be the workspace or under it. |
remote_path | str | None | None | --remote-path PATH | None | Where the workspace really is on the target, where that differs from workspace (the export's REAL). |
shadow | str | None | None | --shadow PATH | $HUMANIZE_SHADOW | The mirror directory on the harness's machine. None: the workspace path itself; for a harness elsewhere, a kept mirror under that machine's mirror cache. |
local_paths | tuple[str, ...] | () | --local-path PATH (repeatable) | Paths kept on this machine even inside the workspace. | |
local_execs | tuple[str, ...] | () | --local-exec PATH (repeatable) | Programs under these paths run here, not on the target. | |
private | tuple[str, ...] | () | --private NAME (repeatable) | Variables the agent has and the commands it runs on the target do not. | |
redirects | tuple[tuple[str, str], ...] | () | --redirect FROM=TO (repeatable) | Paths answered with other paths, kept on this machine. A directory covers everything under it. | |
net | str | "local" | --net {local,remote} | local | Where the agent's own TCP connections go. Commands always use the target's network. |
net_allow | tuple[str, ...] | () | --net-allow HOST[:PORT] (repeatable) | With net="remote", destinations kept local. | |
token | str | None | None | --token TOKEN | $HUMANIZE_TOKEN | The secret a tcp:// target requires. |
force | bool | False | --force | Use a mirror directory that holds unrelated files or was last used for another target. | |
native | bool | False | --native | Run the CLI installed on the target. See Native. | |
hushes | tuple[str, ...] | () | --hush NAME (repeatable) | Native: variables removed from the CLI's environment on the target. | |
projects | tuple[tuple[str, str], ...] | () | --project NAME=DIR (repeatable) | Native: credential directories written to the target for the turn; NAME is set to where each landed. | |
home | Virtual | None | None | --home JSON | The CLI's virtual home. Native: built on the target for the turn, with nothing merged. Supervised: built on the harness machine around the CLI the supervisor starts, its merges applied, and removed when the CLI exits; set for a harness elsewhere. | |
installs | str | "" | --installs LINE | "" | Native: the install line quoted when the CLI is missing on the target. |
fence | Fence | None | None | --fence JSON | None | What the agent may reach, held on both machines. --fence takes Fence.dumps() output. |
--log-level {debug,info,warning,error}defaults to$HUMANIZE_LOG, elsewarning, and logs to stderr.- The environment-variable defaults apply to the command line only.
check(),connect()anddrive()called in-process do not read$HUMANIZE_TOKENor$HUMANIZE_SHADOW; a spawned turn is the rendered command line and does. - A turn spawned from an agent is
AnchorConfig.command(argv, swaps=…, private=…, chdir=…): the provider's credential paths and kept session paths are added asredirects, the provider's variables asprivate, and the session's directory aschdir.
Variables the agent is given
| Variable | Value | Arrangements |
|---|---|---|
HUMANIZE | the hmz.coganchor version | supervised, native |
HUMANIZE_TARGET | the target, as Target.describe() spells it | supervised, native |
HUMANIZE_WORKSPACE | the workspace, as workspace names it (absolute) | supervised, native |
PWD | the mirror directory the agent starts in | supervised |
Validation
AnchorConfig.__post_init__ raises ValueError; hmz internal anchor reports the same message as a usage error and exits 2.
| Refused | Message |
|---|---|
target or harness not a target spelling | the target parse message |
native=True with a harness on another machine | a native session has no harness to put anywhere: the CLI on the target is the one that runs, so --harness and --native ask for opposite things |
a peer:// target with any harness but local | a peer:// target is a meeting Humanize books for a harness it is placing; naming both is naming the same introduction twice |
net not local or remote | unsupported net 'X'; expected local or remote (the option: argument --net: invalid choice: …) |
| a redirect that is not two absolute paths | unsupported redirect 'a=b'; expected two absolute paths |
an empty hush, or one containing = | unsupported hush 'A=B'; expected a variable name |
| a projection without a variable name and an absolute path | unsupported projection X='rel'; expected a variable name and an absolute path |
a non-open fence with no scopes (drawn path by path) | a fence drawn path by path cannot be held on another machine; draw it from its levels with Fence.of |
a non-open fence with a harness on another machine | a fence is drawn around this machine's paths, and cannot hold a harness on another machine |
a fence with online=False and net="remote" | a fence that cuts the network keeps the agent's own connections here; --net remote would send them past it |
The command line also refuses no agent and no --check with no agent given; try `hmz internal anchor claude` (exit 2).
Refused only when a turn is started:
| Condition | Error |
|---|---|
| no agent named | ValueError: no agent given (native), ValueError: no agent command given (supervised) |
a harness on a tcp:// or peer:// machine, or same with such a target | ValueError: nothing is started on the far side of tcp://h:1 |
chdir outside the workspace | ValueError: <chdir> is not inside <workspace> |
the agent not on this machine's PATH (supervised) | FileNotFoundError: <name>: not found on PATH |
Exit status of hmz internal anchor
| Status | Meaning |
|---|---|
| the agent's own | the agent ran; a signal-killed CLI under --native is 128 + signal |
0 | --check succeeded |
1 | ConnectionError, ProtocolError, OSError or ValueError while reaching the target or running the turn, printed as hmz: <message> |
2 | refused settings (usage error) |
127 | --native and the CLI is not installed on the target: hmz: <cli> is not installed on <target>[; install it there with `<installs>`] |
130 | interrupted |
--check prints, without running anything on the target:
target ssh://build-box
hostname build-box
python 3.12.3 (pid 41207)
export /srv/project -> /srv/project
workspace /srv/project (184 entries)Defined in src/hmz/coganchor/anchor.py, src/hmz/coganchor/argv.py, src/hmz/cli/anchor.py.
Where the harness runs
It is decided at two levels: AnchorConfig.harness for one anchor, and the harness setting of the runtime a flow session works on, which the flow runtime turns into anchor settings per session.
AnchorConfig.harness
| Value | Harness machine | Mirror | Notes |
|---|---|---|---|
local | this machine | shadow, else the workspace path | the supervised arrangement |
same | the machine target names | kept, on that machine | afar; nothing is introduced, the serving half is started beside the harness with a local: target |
an ssh:// or docker:// spelling equal to target (scheme, host, port, path and options) | that machine | kept, on that machine | as same |
any other ssh:// or docker:// spelling | that machine | kept, on that machine | afar; a rendezvous introduces the harness to the serving half on the target |
local:DIR | this machine | as local | not afar |
A turn with the harness elsewhere is spawned as hmz internal anchor on the harness machine (bootstrapped as a target is), with harness rewritten to local and target rewritten to local:<remote_path> (same machine) or peer://<ticket>@<broker> (third machine). This machine runs only the process carrying the three streams.
The CLI there runs in a virtual home as it would here, built on the harness machine from that machine's user's own configuration (AnchorConfig.home, --home JSON) by the hmz internal anchor supervising it, with each driver's merges applied (Qwen Code's effort, hook table and MCP server; a gateway's entry in Grok Build's config.toml and pi's models.json; agy's agents), named to the CLI by its home variable or, for agy, --app_data_dir right after the program's name, and removed when the CLI exits. The home is kept on the harness machine as the CLI's state paths are: no path in it and no program run from it goes to the target. The flow's skills it carries are put there first: one tar, unpacked under <cache>/skills/<16 hex digits of its sha256> (the target's cache), renamed into place whole, reused while a turn carries the same skills, and swept once no turn has carried them for 30 days. A failure to put them there fails the turn with ConnectionError: could not carry the flow's skills to <harness>: <stderr>.
Where shadow is unset, the mirror on the harness machine is <mirrors>/<name>, where <mirrors> is $HOME/.cache/humanize-mirrors over ssh, /tmp/humanize-mirrors in a container, or /tmp/humanize-mirrors-<UID> in a container whose target names user=UID[:GID], and <name> is the first 16 hex digits of sha256(workspace \0 remote_path \0 target). It is passed as HUMANIZE_SHADOW with force set, and kept between turns.
The driver still names the workspace by its own path: a CLI told where to work over its protocol (Codex's thread/start cwd, an ACP session/new cwd, and so on) is told workspace. On a harness reaching its work as a peer:// target (a third machine), the supervisor answers workspace and everything under it with the mirror, as another spelling of it, whatever the harness machine holds at that path; it is never created there. A --local-path or --local-exec under workspace is kept here by either name. Not for a mirror nested in the workspace or the other way round.
The harness setting of a runtime
A flow session has two settings that say where it runs:
| Setting | Values |
|---|---|
| Env location | local: the environment's machine is this machine (a local environment). standalone: any other machine (ssh, docker, swarm, apple-container, slurm). |
| Harness location | local: the CLI runs on this machine; with a standalone environment it works in a mirror here, supervised. env: the CLI installed on the environment's machine runs there natively, with no mirror. standalone: the CLI runs on a separate saved runtime, a harness runtime, in a mirror there. |
Env location local always has harness location local, with no mirror. For a standalone environment, the harness location is what the harness field of the saved runtime the work is on says:
harness := env | local | ssh:<name> | docker:<name> | swarm:<name> | apple-container:<name> | slurm:<name>| Setting | The harness | Refused when |
|---|---|---|
env (default) | native on the environment's machine | the CLI is missing there (HarnessNotInstalled), the machine cannot hold the session's fence (HarnessSandboxed), the machine cannot be asked (HarnessUnrecoverable), or a PreToolUse or PermissionRequest hook is hung on the agent (UnsupportedOperation) |
local | supervised here, anchored to the environment's machine | never, by the setting (the CLI must still be installed here) |
ssh:<name>, docker:<name>, swarm:<name>, apple-container:<name>, slurm:<name> | supervised on that saved runtime's machine, reaching the environment's machine through the anchor | it cannot be opened or reached (EnvError), has no share left, its container limit among it (ResourceUnmet), or the role's Permission fences anything (HarnessSandboxed) |
- There is no fallback: the harness goes where the setting says or the session is refused, with an error of the refusal's own type, as
<backend>[@<provider>]: <cli>'s harness cannot run where its harness setting (<setting>) says: <refusal>. - Work in a
localenvironment always has its harness here. Work on a machine that is no saved runtime (ssh@[<host>], a host nobody saved;docker/…,swarm/…,apple-container/…orslurm/…, this machine's), and work on a runtime saved without aharness, hasenv. - The setting read is the one of the runtime the session's placement names (
store.find(placement.backend, placement.provider)), the one actually opened. A runtime a harness is put on is opened as it is: its own setting and fallback list are never read. - A harness runtime is opened (
hmz.runtime.flowing.harbors.Harbors) once per run, shared by every role, as an environment of roleharnesswith no resources asked, in its saved workdir, else~over ssh, else an empty$TMPDIR/humanize-<uid>/harness(created by the run) for a docker daemon on this machine or this Mac's Apple containers; any other runtime saved without a workdir is refused. It is probed before the flow is called and closed with the run's environments. A container for it is started with--cap-add SYS_PTRACE: without it, docker's default seccomp profile refuses thepidfd_getfdthe supervisor borrows each command's descriptors with, and a command whose output goes to a socket (opencode's, Claude Code's stdin) is run with that output lost. - The setting is checked when the runtime is made: one that is none of the above, or one naming the runtime itself, is a
ValueError(<name>: '<setting>' is not where a harness runs: env, local or <ssh|docker|swarm|apple-container|slurm>:<runtime name>,<name>: its harness names itself; env is its own machine). A setting naming a runtime nobody saved is accepted, and refused when used. - A runtime saved by an earlier Humanize with an ordered
affinitylist is read as though itsharnesswere the first entry (selfread asenv), orenvwhere the list is empty; the next write of it dropsaffinity.
Settling it
The decision is made in HarnessDriver.open (hmz.runtime.flowing.harnesses) for every session a role opens, before the session's agent is built. With the session's machine M (from its environment), its fence, its runtime's setting, and the set H of hooks hung on the flow agent among PreToolUse and PermissionRequest (an AskUser hook is not among them: a question comes back down the CLI's own stream wherever the CLI runs):
Mis this machine:Munchanged.- The setting is
local:Munchanged, the harness supervised here. - The setting is
envandHis non-empty: refused. The CLI's own hook table names a program on this machine, so a CLI on another machine cannot be gated by it. - The setting is
env: the machine is asked, once per role and target, whether the CLI is there; then, for a session with a non-open fence, once per role, target andonlinevalue, whether the machine can hold the fence. Both answers yes:Mwithnative=Trueandshadow=None. Otherwise refused. - The setting names a runtime: that runtime is opened, and
Mgetsharnessset to its target andshadow=None. A runtime that is this machine isMunchanged, aslocal.
| Probe | How | Result |
|---|---|---|
| CLI present | The native road itself: AnchorConfig(native=True, shadow=None, fence=None).command(["/bin/sh", "-c", 'command -v -- "$1" >/dev/null 2>&1 || command -v -- "$2" >/dev/null 2>&1 || exit 69', "humanize", PROGRAM, basename(PROGRAM)]), in a new process session, stdin /dev/null, 300 s limit. PROGRAM is the CLI's name, or an added CLI's configured command. | exit 0: present; exit 69: absent; other exit, spawn failure or timeout: cannot be asked |
| Fence holdable | hmz.coganchor.check(anchor); the handshake's fence.fs (and fence.net where the fence cuts the network) | both true: holdable |
Answers are cached on the role's driver for the rest of the run; concurrent sessions wait for one answer. The probes are asked one at a time.
Runner.arun settles the harness of every agent on every machine (HarnessDriver.placeable), against every environment, the workspace included, with the role's declared permission, once the environments are probed and before the flow is called; a refusal there is Refused, which hmz exec prints as hmz exec: error: <message> with exit 2, and the answers are kept for the sessions. A session opened later is still refused as it opens, raised from its spawn. The refusals (<where> is <backend>[@<provider>] of the environment, e.g. ssh@gpu-box), each after the prefix above:
| Error | Message | When |
|---|---|---|
HarnessNotInstalled | <cli> is not installed on <where>: <install line> there, or set the harness of the runtime it is on to local | env, probe exit 69 |
HarnessUnrecoverable | <where> could not be asked whether <cli> is there: <last stderr line | exit status N> | env, probe failed |
HarnessUnrecoverable | <where> did not say within 300s whether <cli> is there | env, probe timed out |
HarnessUnrecoverable | <where> could not be asked whether it can fence: <error> | env, handshake failed |
HarnessSandboxed | <where> cannot fence the agent to its permission: it needs Landlock; grant the agent everything, or set the harness of the runtime it is on to local | env, the machine cannot hold the fence |
UnsupportedOperation | a <kind>[ and a <kind>] hook cannot gate the tools of a CLI on another machine; set the harness of the runtime it is on to local, <kind> being permission_request or pre_tool_use | env, a PreToolUse or PermissionRequest hook hung |
HarnessNotInstalled | <cli> is not installed here: <install line> | the harness is here and the CLI is not on this machine |
HarnessSandboxed | <role>=<cli>[@<account>]/<model>:<effort>: <AgentClass>: a fence cannot hold a harness that runs on another machine | a harness runtime, and the role's Permission is anything but every scope ALL |
EnvUnavailable, EnvConnectionError, ResourceUnmet | as the runtime's own environment would be refused | a harness runtime that cannot be opened, reached, or given a container |
Recording
Where each session's harness went is written with it: the epic's session record carries harness (local, env, or the <backend>:<name> of the harness runtime its runtime's setting named; "" for work on this machine; self, written by an earlier Humanize, is read as env), and so does the daemon's opened record. See Tracing.
Limitations
- A fence cannot follow a harness elsewhere. A fence names this machine's paths. On a harness runtime, and under an
AnchorConfig.harnesselsewhere, any non-open fence is refused; a flow role must grantlocal,user,systemandonlineallALL. The defaultPermission(local=ALL, user=READ, system=READ, online=ALL) is refused. - Accounts do not follow a harness elsewhere. See Where the account lives.
- Gating hooks need the harness here. A role with a
PreToolUseorPermissionRequesthook hung is refused under the settingenv; set the runtime's harness tolocal. AnAskUserhook does not need it here. An anchored turn gets no hook table, so therePreToolUseis read off the CLI's stream and cannot stop a tool (Agents › Refusing a tool). - Callbacks do not cross. A native turn on a non-
localtarget offered the flow's tool callbacks or command lines is refused:<cli>: a turn driven on <target> cannot be offered the flow's own callbacks or command lines -- the bridge that carries them is a program on this machine, and the CLI that would start it is on that one(command linesalone foromp).omp's callbacks are the exception (AgentBase.pipes_tools): it asks for each call over the RPC pipe it is driven through, so they reach it wherever it runs. - Sessions whose harness is on the env or standalone stay where that CLI keeps them, on the other machine; a run copies what they log into its epic as they go and whole as they close (Providers › Sessions on another machine). So only a session whose harness is
local, whose CLI keeps its conversation here, can be kept in a flow's state and carried on: writing one whose harness isenvor standalone is refused withStateNotSerializable, and a kept one read back onto such a turn withUnsupportedOperation. - Forks stay on one machine. A fork into a session on another machine is refused with
UnsupportedOperation: <cli> cannot fork a session onto another machine.
Defined in src/hmz/runtime/flowing/harnesses.py (HarnessDriver._harnessed, _put, _native_on, _has_cli, _fenceable), src/hmz/runtime/flowing/harbors.py (Harbors), src/hmz/runtime/runner.py, src/hmz/runtime/epic.py (harnessed, harbor), specs/features/placements.md §4.
Supervised arrangement
connect(command, config) with harness here and native=False. The agent process runs on this machine, unchanged, under a supervisor. Everything it does to files, programs and the network is answered from the target.
Interception
- A seccomp filter traps exactly the syscalls that name a path, start a program or open a connection; every other syscall (
read,write,mmap,futex,getdents64, …) runs natively. - Trapped on x86-64 (55):
access,chdir,chmod,chown,connect,creat,execve,execveat,faccessat,faccessat2,fanotify_mark,fchmodat,fchmodat2,fchownat,futimesat,getxattr,inotify_add_watch,lchown,lgetxattr,link,linkat,listxattr,llistxattr,lremovexattr,lsetxattr,lstat,mkdir,mkdirat,mknod,mknodat,name_to_handle_at,newfstatat,open,open_tree,openat,openat2,readlink,readlinkat,removexattr,rename,renameat,renameat2,rmdir,setxattr,stat,statfs,statx,symlink,symlinkat,truncate,unlink,unlinkat,utime,utimensat,utimes. On aarch64 the 19 legacy forms do not exist and 36 are trapped. chmod,fchmodat,fchmodat2(except on a link it was told not to follow, which the kernel refuses), theutime*family (a descriptor's own file too, asfutimensnames it) and every mutation of the tree are replayed on the target first. A plain filemknodmakes is sent like any other; a pipe or device it makes stays here.chown,lchown,fchownatand the*setxattr/*removexattrcalls act on the mirror only, once it holds the file's contents; the lookups (stat*,access*,statfs,*getxattr,*listxattr,name_to_handle_at,open_tree,inotify_add_watch,fanotify_mark) run against the mirror once it holds the path.- Not trapped:
fstatfsand every other call taking a descriptor rather than a path; calls that need a capability an agent is not given (mount,umount2,pivot_root,chroot,swapon,swapoff,acct,quotactl,move_mount,uselib); the*xattratandfile_getattr/file_setattrcalls; and a local socket's address inbindorconnect. - A path is resolved as text:
.and..are collapsed, doubled/removed, and/proc/self/fd/<n>,/proc/<pid>/fd/<n>,/proc/self/cwd,/proc/self/rootand/proc/self/exeare followed to what they name. An ordinary symlink in the middle of a path is not walked. A link that cannot be read back fails the call. - A path the mirror is also reached by is rewritten onto the mirror's own path before the call runs, for every trapped call that names a path:
/private/tmp/…for a mirror under/tmpon a Mac target, any case on a target that ignores case, andworkspace/…for apeer://target (a harness elsewhere). A mirror whose path runs through a symlink here (~/.cachelinked elsewhere, say) is kept at the path the link resolves to, which is where the agent starts and what the kernel reports as its working directory, and the path it was given is one more such name, in calls and in a command's arguments alike. A program run here (the agent's own, or a--local-exec) named that way is run from the mirror; one run on the target is named to it by its own path. - Only x86-64 and aarch64 Linux are supported. Any other platform fails at start-up with
RuntimeError, naming where the supervisor can run instead.
The mirror
The mirror is prepared before the target is reached. Its guard, recorded outside the directory in
~/.cache/humanize/shadows/<sha256(path)[:16]>.json(or under$HUMANIZE_SHADOWS), written whole beside it and moved into place, so that a turn starting while another records the mirror reads the record and not half of it:Condition Without forcethe directory was last used for another target FileExistsError: <path> mirrors <old>, not <new>. Humanize replaces this directory with the new target's contents and would delete everything only the old one has. Use a different directory, or pass --force.the directory holds files and was never a mirror FileExistsError: <path> already contains files and is not a Humanize mirror. Humanize replaces this directory with the target's contents and would delete them. Use an empty directory, or pass --force.the path exists and is not a directory Unmirrored: … shadow root is not a directory(always)the path cannot be created here Unmirrored: … <reason>, e.g.Permission denied: /home/me(always)Unmirrored(anOSError) is printed byhmz internal anchorashmz: cannot keep the local copy of the work at <path>: <reason>, with exit 1. A turn that fails with it is classifiedunmirrored(Agents › Failures), not as a refused credential. The target's identity for this check is its spelling without ssh options.Structure is materialised on first access: real directories and symlinks, and sparse placeholder files carrying the target's size, mode and mtime.
stat,getdents64,read,write,mmapandlseekthen run locally.Content is pulled the first time a file is opened for reading.
A file the agent modifies is pushed in full before any command runs on the target, and again when the session ends.
Creating, removing, renaming, linking and mode changes are replayed on the target first; the target's error is the agent's error.
Completing a remote command invalidates every cached directory listing.
The mirror is authoritative: what the target does not have is deleted from it.
A file written since it was last pushed is pushed before what the target says of it is taken: it is never deleted, made a placeholder or fetched over first.
Turns working in one mirror at once, as the sessions of one run on one environment do, share what they know of it. Each turn appends every change to that knowledge (a file fetched, made a placeholder, written, pushed, renamed or removed) to
<sha256(path)[:16]>.journalbeside the mirror's guard, reads the others' changes from it, and holds its lock while it changes the mirror, network calls included. A file one turn wrote is pushed by whichever turn next flushes, not deleted as one the target lacks, and one it pushed or fetched is not made a placeholder again under it. A turn starting with no other in the mirror (none holding<sha256(path)[:16]>.live) begins the journal again. Where the two cannot be opened or locked, the turn works alone and logs<mirror> cannot be shared with other turns working in it, which may then lose what this one writes: <reason>. Each turn still reads a directory again only after its own commands.
What stays on this machine
A path is answered from the target unless it is one of these:
| Kept here | Source |
|---|---|
The agent program as resolved on PATH, and its real path | statepaths.resolve |
For a script, its #! interpreter, and for #!/usr/bin/env NAME every PATH candidate for NAME | statepaths._interpreter |
Codex's native binary and codex-code-mode-host | statepaths._codex_runtime_programs |
Every executable beside the program inside its own node_modules package (e.g. mimo's bin/.mimocode) | statepaths._beside |
| The CLI's state paths (table below), as files and as programs | statepaths.PROFILES |
~/.hmz, ~/.humanize, ~/.cache/humanize, ~/.config/humanize | statepaths.COMMON_STATE_PATHS |
local_paths, local_execs, and what every redirects entry is answered with | AnchorConfig |
The program that bridges the flow's tool callbacks, when offered (not for omp, which needs no bridge) | added to local_execs |
| The agent's shelf of the flow's command lines, once any is shelved, and the Python they run | added to local_execs; the shelf to local_paths too |
The virtual home the anchor builds around the CLI (home), as files and as programs | AnchorConfig.home |
| CLI | State paths kept here |
|---|---|
agy | ~/.gemini |
claude | ~/.claude, ~/.claude.json, ~/.local/share/claude, ~/.cache/claude-cli-nodejs |
codex | ~/.codex |
cursor-agent | ~/.cursor, ~/.local/share/cursor-agent |
dsh | ~/.dsh (also for dsh-jsonrpc-agent-*) |
grok | ~/.grok |
kimi | ~/.kimi-code, ~/.kimi |
mcode | ~/.minimax, ~/.mavis |
mimo | ~/.mimocode, ~/.config/mimocode, ~/.local/share/mimocode, ~/.cache/mimocode, ~/.local/state/mimocode |
omp | ~/.omp |
opencode | ~/.opencode, ~/.config/opencode, ~/.local/share/opencode, ~/.cache/opencode, ~/.local/state/opencode |
pi | ~/.pi |
qwen | ~/.qwen |
| any other program | none beyond the common paths; state inside the workspace must be named with --local-path |
A path kept here carves a hole in the mirror only where it lies inside the mirror. A mirror kept inside one of them, as a container's is under ~/.hmz/envs/mirrors, is the target's workspace all the same, and what is beside it in that directory stays here.
Commands, network and signals
- Every program the agent starts that is not kept here runs on the target, in the target's copy of the working directory, work helpers such as a bundled
ripgrepincluded. It gets the agent's descriptors, output and exit status; its parent is released as soon as it starts, so commands run concurrently. - A command that inherited the pipe the agent's driver speaks on gets an empty stdin on the target. An agent that is a shell hands its stdin on as a shell does.
- Variables in
privateare removed from the environment commands run with on the target. net="local": the agent's own TCP connections leave from this machine.net="remote": they are tunnelled and opened from the target, except destinations innet_allow.SIGINT,SIGTERMand the other common signals are forwarded both ways. A repeated signal and the rarer signals are not reproduced.- A command that cannot be started, or that the supervisor loses track of, fails. Nothing it started is left running when the session ends.
Defined in src/hmz/coganchor/supervisor.py, src/hmz/coganchor/linux/syscalls.py, src/hmz/coganchor/shadow.py, src/hmz/coganchor/statepaths.py, src/hmz/coganchor/policy.py.
Native arrangement
drive(command, config), reached through connect when native=True. Nothing is mirrored, traced or supervised. The sequence per turn:
- Handshake with the serving half.
- Find the CLI on the target:
command -v -- NAMEfrom the workspace, then by the last component ofNAME. Not found:NotInstalled(exit 127 on the command line). - If a fence is set and not open, require the handshake's
fencecapability; otherwisePermissionError: the target cannot fence the CLI: it needs Landlock[ ABI 4 and seccomp]. - If
projectsis non-empty, make a private directory on the target (umask 077,mktemp -d,chmod 700) and write each credential file into<dir>/<variable lowercased>/...through a command's stdin, mode0600. The variable is set to that directory. A failure raisesOSError: could not put <VAR> on the target: …and the turn does not run. A file holding a refresh token (a sign-in that refreshes itself) is first locked here, exclusively, for the whole turn (flockon its directory); one any other turn is using refuses this one withOSError(EBUSY) before anything reaches the target. A supervised turn holds the same lock shared, for the refresh-token files its redirects answer. - If
homeis set, build it in the turn's private directory (made for it whereprojectsmade none), with the flow's skills in its skills directory (below). - Start the CLI as
env [-u HUSHED]... NAME=VALUE... PROGRAM ARGS...inchdir(default the workspace), with this process's environment layered on the target's. - Relay stdin, stdout and stderr byte for byte; forward the first
SIGINT/SIGTERMto the CLI and restore the previous handler, so a second one reaches this process. - Read each refresh-token file back from the target; one the CLI changed, that is still a whole sign-in, is written atomically over the file it was copied from here, unless that file changed meanwhile (then it is left, with a warning that the account may need signing in again). Then remove the private directory, whatever happened.
| What does not follow the CLI | How it crosses |
|---|---|
| The account's variables | Sent as the turn's environment. Every variable the provider hushes is removed on the target with env -u, since the target's own shell profile would otherwise supply it. |
| The account's credential files | --project NAME=DIR, written through stdin, never argv. |
| The flow's skills | Written into the skills directory of the CLI's virtual home (--home), never into the workspace. A CLI with no virtual home on the target gets none. |
| The CLI's virtual home | --home JSON. Built in the turn's private directory by a POSIX shell script on the target, around the target user's own home ($VARIABLE, else $HOME/<default>), never over a credential --project put there; the home variable is set to it, and Profile.secures to the projected directory under an account and to the target user's value otherwise. Under a projected account, the session paths stay in it rather than being linked, and are brought back by --bring. Removed with the private directory. |
Credential files cross only where a variable of the backend's own moves the directory they are in:
| Credential root | Crosses as | Backends |
|---|---|---|
| the backend's home | its home variable | claude (CLAUDE_CONFIG_DIR), codex (CODEX_HOME), cursor-agent (CURSOR_CONFIG_DIR), grok (GROK_HOME), kimi (KIMI_CODE_HOME), mcode (MINIMAX_DATA_DIR), pi (PI_CODING_AGENT_DIR), qwen (QWEN_HOME) |
$XDG_CONFIG_HOME | XDG_CONFIG_HOME | files written config/…, e.g. Claude's anthropic/ |
| the backend's home, no own variable | does not cross | agy, opencode, mimo |
the user's home (~/…) | does not cross | e.g. Claude's ~/.claude.json, Cursor's ~/.cursor/auth.json |
An account some of whose credential files cannot cross, with nothing else to carry it, is refused rather than run as whatever account the target is signed into.
- There is no
net: the CLI's connections are the target's. A gateway at127.0.0.1is the target's loopback. - The CLI keeps its sessions on the target; a run copies what they log into its epic (Providers › Sessions on another machine).
Defined in src/hmz/coganchor/anchor.py (drive, _installed, _projected, _homed, _swept), src/hmz/coganchor/agents/base.py (_reaching, _projecting).
Afar arrangement and the rendezvous
With the harness on the target (same), nothing is introduced: the harness machine runs hmz internal anchor --target local:<remote_path> against itself. With the harness on a third machine, Humanize books a meeting:
- This process starts (once, for its lifetime) a broker listening on
0.0.0.0, port$HUMANIZE_RENDEZVOUS_PORT(default0, any free port). - It mints a ticket (
secrets.token_hex(16), 128 bits) and starts, on the target,hmz internal anchor serve --peer TICKET@HOST:PORT --export …, whereHOSTisbroker, else$HUMANIZE_RENDEZVOUS, else this machine's outward-facing IPv4 address (the source address of a route to192.0.2.1;127.0.0.1if there is none). - It spawns
hmz internal anchor --target peer://TICKET@HOST:PORTon the harness machine. - Both halves dial the broker with the ticket and their role (
anchororserve), and offer the address they dialled from plus the IPv4 addresses their hostname resolves to -- left out where resolving it takes longer than 1 s, as an unanswered macOS.localname does.
| Phase | Behaviour | Time limit |
|---|---|---|
| Pairing | a half waits at the broker for the other | 120 s |
| Discovery | each half is told the address its connection arrived from, and the other half's addresses | 30 s per broker answer |
| Punching | both halves connect to each other's candidates from the port they dialled the broker from, and listen on it; each punched socket must present HMZ1 and a digest of the ticket; the anchor half says GO on the socket it keeps and the serving half answers OK | 4 s (--punching on a standalone broker) |
| Greeting | a punched socket that does not prove itself is dropped | 3 s |
| Relay | if punching fails, the two connections already held to the broker are spliced, 64 KiB at a time | none |
- The broker remembers pairs of external addresses it failed to introduce (up to 1024 pairs). After 3 failures a pair is relayed without punching, except when its failure count is a multiple of 16.
- A broker holds at most 65,536 meetings at once; beyond that a half is refused with
this rendezvous is already holding 65536 meetings. - Addresses are IPv4 only; a machine reachable only over IPv6 is relayed.
- Neither half is told whether it was relayed.
hmz internal anchor rendezvous [--listen [HOST:]PORT] [--punching SECONDS]runs a broker on its own (default0.0.0.0:0) and printshmz internal anchor rendezvous listening HOST PORTon stderr.
Defined in src/hmz/coganchor/elsewhere.py (afar, _carried), src/hmz/coganchor/rendezvous.py.
A fence on both machines
AnchorConfig.fence is held by the anchor on each machine around that machine's own paths, never by a wrapper around the anchor. The fence must have scopes (built with Fence.of); what crosses is its levels, not its paths.
| Arrangement | This machine | Target |
|---|---|---|
| Supervised | The agent process is walled in by Landlock before it runs, with the mirror granted at the workdir's level. Where online=False, a proxy served by the anchor process is its only way out, passing the fence's hosts; binds are limited to a kernel-chosen port and the fence's listen ports, on loopback. The supervisor, its link and the mirror are outside the wall. | Every command the agent runs is sent with {local, user, system, online, tmp}, tmp being the agent's scratch directory here. The serving half draws the fence again (abroad.drawn) around its exported directories, its own $HOME and its own minimum, and runs the command under its own hmz internal fence with its scratch at that same path: made 0700 there if missing and left afterwards, used where it is a directory of the serving user's own (not a link), and otherwise replaced by one made for the one command. A command that names the path, as Claude Code's shell commands name the file they write their working directory to, finds it. Where online=False, the command reaches no host. |
| Native | nothing | The CLI is run under hmz internal fence on the target with the levels, the hosts its model is at, the state it writes under its home (sent as ~/…), the turn's credential directory, read access to its own install tree, and its listen ports. |
| Afar | refused | refused |
- A CLI's own sandbox is not used under an anchor: it would wrap a command here that runs on the target. Codex is sent
sandboxPolicy: {"type": "externalSandbox", "networkAccess": "enabled" | "restricted"}(by the fence'sonline) where the fence writes nothing the rung forbids: nothing at all forread-only, nothing of the home or system forworkspace-write; Cursor'sworkspace-writeruns withoutcursorsandboxwhere the fence writes nothing of the home or the system. - Whatever a driver grants its CLI past the wall (a port it serves on, a sign-in outside its home, generated settings) is granted on both machines.
- The target reports what it can hold at the handshake:
fence.fsis Landlock ABI >= 1;fence.netis Landlock ABI >= 4 (Linux 6.7), a loadable seccomp user-notification filter, andpidfd_getfdpermitted with Yamaptrace_scope< 2. A macOS target reports both as whethersandbox-execcan apply a profile there. A target of a build before fences reports nothing and is treated as unable.
| Where | Error | Message |
|---|---|---|
| agent construction | Unfenced | <Agent>: a fence drawn path by path cannot be held on another machine |
| agent construction | Unfenced | <Agent>: a fence cannot hold a harness that runs on another machine |
| agent construction | Unfenced | <Agent>: an agent whose own connections are sent to the target cannot have its network cut here |
| agent construction, supervised | Unfenced | <Agent> cannot be held to its permission: it is supervised on this machine, which has no Landlock[ ABI 4 and seccomp to cut the network] |
| first turn, supervised | Unfenced | <id>: <target> cannot fence the commands the agent runs there: it needs Seatbelt on a Mac, or Landlock (Linux 5.13 or later, not refused by a container's seccomp profile)[, at ABI 4 (Linux 6.7) with seccomp, to cut the network]; grant the agent everything, or run it where it can be fenced |
| anchor start, supervised | PermissionError | this machine cannot fence the agent: it needs Landlock[ ABI 4 and seccomp] |
| native turn | PermissionError | the target cannot fence the CLI: it needs Landlock[ ABI 4 and seccomp] |
| a fenced command on the target | PermissionError | this machine cannot fence a command: it needs Landlock[ ABI 4 and seccomp]; on a macOS target, … it needs Seatbelt, which a process already inside a sandbox cannot apply |
Under a flow, Unfenced becomes HarnessSandboxed. Docker's default seccomp profile permits Landlock but not the seccomp listener or pidfd_getfd, so a container under it holds a fence with online=True and refuses one with online=False; run such a container with a profile that allows them, or --security-opt seccomp=unconfined.
Defined in src/hmz/coganchor/anchor.py (_Walls, _fenced), src/hmz/coganchor/fence/abroad.py, src/hmz/coganchor/serve/fencing.py, src/hmz/coganchor/agents/base.py (_abroad, _reached, _walled).
Serving a target
hmz internal anchor serve --export VIRTUAL[:REAL] [--export ...]
(--stdio | --listen [HOST:]PORT | --peer TICKET@HOST:PORT)
[--token TOKEN] [--log-level LEVEL]| Mode | Serves | Started by |
|---|---|---|
--stdio | one session on stdin/stdout; fds 0 and 1 are pointed at /dev/null for the rest of the process | Humanize, for local, ssh://, docker://, apple-container:// and slurm:// targets |
--listen [HOST:]PORT | every TCP connection, one thread each, until interrupted; prints hmz internal anchor serve listening HOST PORT on stderr | a person, for tcp:// targets; or the batch step of a Slurm job whose runtime says via: tcp, on 0.0.0.0:0 with a random secret set in its script |
--peer TICKET@HOST:PORT | one session met at a broker | Humanize, for a harness on a third machine |
--export VIRTUAL[:REAL]is required and repeatable.VIRTUALmust be absolute;REALdefaults toVIRTUAL, with~expanded. A path outside every export is refused withEACCES,path is outside every export. Virtual prefixes inside command arguments are rewritten to real paths. Malformed:hmz: malformed export '<spec>'; expected VIRTUAL[:REAL], exit 2.- A bare
--listen PORTlistens on127.0.0.1. Any host other than127.0.0.1,::1orlocalhostwithout--tokenis refused:hmz: cannot listen on a non-loopback address without --token, exit 2. A malformed address:hmz: malformed listen address '<spec>'; expected [HOST:]PORT, exit 2. A bind failure exits 1. --tokendefaults to$HUMANIZE_TOKEN. With a token, a session whose handshake does not present it is refused withinvalid token.- For
ssh://,docker://andslurm://targets the token is unused; ssh, the docker socket and Slurm authenticate the session.
A listening serving half is a shell on its machine
An export bounds which paths a request may name; it does not confine what the commands it runs can do. Listen on a non-loopback address only with a secret --token.
Defined in src/hmz/cli/anchor.py, src/hmz/coganchor/serve/, specs/features/placements.md §10.
Where the account lives
| Arrangement | Credentials, CLI state, model-provider connection | Provider variables | Provider credential files |
|---|---|---|---|
| Supervised | this machine | set on the agent here; listed in private, so commands on the target do not get them | answered here as redirects; never cross |
| Afar | the harness machine | not sent: the harness process's environment is the harness machine's | redirected to this machine's paths, which the harness machine does not have |
| Native | the target, for each turn | sent as the turn's environment; hushed variables removed with env -u | written to a private directory on the target and removed after the turn, where they can cross; a refresh-token file goes to one turn at a time and is written back refreshed |
Python API
from hmz.coganchor import AnchorConfig, check, connect, drive
from hmz.coganchor.anchor import NotInstalled| Call | Returns | Raises |
|---|---|---|
check(config=None) -> dict | the handshake reply (version, hostname, platform, python, pid, exports, fence) plus target, workspace and entries (entries in the workspace). Runs nothing on the target. | ValueError (target), OSError (unreachable, no workspace) |
connect(command, config=None) -> int | the agent's exit status, after everything it wrote has been pushed. Calls drive when native; spawns the afar line when the harness is elsewhere. | ValueError, FileNotFoundError, OSError |
drive(command, config=None) -> int | the CLI's exit status, or 128 + signal | ValueError (no agent given, target), NotInstalled (a FileNotFoundError), OSError, PermissionError (fence) |
AnchorConfig.command(argv, *, swaps=(), private=(), chdir="") -> list[str] | the argv that runs argv under this anchor in its own process | ValueError, ConnectionError |
AnchorConfig.capabilities -> frozenset[str] | {"anchor:native-cli"}, {"anchor:supervised"} or {"anchor:supervised", "anchor:afar"} | |
AnchorConfig.mount() -> (Target, str, str) | the parsed target, the workspace's absolute path here, and the export spelling | ValueError |
config=None is AnchorConfig(): this directory on a local target.
Requirements
| Machine | Requirement |
|---|---|
| The harness machine, supervised or afar | Linux on x86-64 or aarch64, Python >= 3.12, ptrace and seccomp permitted. |
| This machine, when the harness is elsewhere or the arrangement is native | none of the above; only what runs the three streams |
| A target | a POSIX /bin/sh and Python >= 3.12; no root, compiler, kernel module or installed package |
| A target of a fenced session | Linux with Landlock (ABI >= 1); ABI >= 4 (Linux 6.7), seccomp user notification and pidfd_getfd where the network is cut. Or macOS, not itself inside a sandbox (Seatbelt) |
| A Slurm target | the job's node sees the workspace at the same path as this machine (a shared filesystem); srun here; with via: tcp, this machine able to reach the node's ports |
| A native target | the CLI installed and on the login's PATH |
| A harness machine elsewhere | an ssh:// or docker:// machine meeting the first row, with the CLI installed |
| A third-machine harness | both machines able to reach this machine's broker port over IPv4 |
Failure behaviour
- Serving is not a sandbox. Exports bound the paths a request may name, not what commands do; a symlink out of an export is followed. Only a fence confines commands.
- Mirrored directories carry this machine's permissions and the time the mirror made them.
- Only file contents are pushed. A mode change through an open descriptor is not replayed; ownership, device nodes and extended attributes never leave the mirror.
- An unanswered request is abandoned here, not there. It may still take effect on the target after the agent was told it failed.
- Losing the connection does not stop the agent. Work that needs the target fails, mirrored files still read, and the agent exits with its own status.
- The mirror is authoritative. What the target lacks is deleted from the mirror, including a file written here and never pushed; the deletion is logged.
- A command's output may not reach the agent in a container. The supervisor borrows each command's descriptors with
pidfd_getfd, which docker's default seccomp profile refuses withoutCAP_SYS_PTRACE. A pipe or tty is opened again through/procinstead; a socket cannot be, and is logged ascould not borrow fd <n> from pid <pid>. Humanize starts the container a harness setting puts a harness in with--cap-add SYS_PTRACE; one started otherwise needs it. - A path is read when its syscall stops. A descriptor replaced by another thread in between resolves as it was.
- Native setup commands are bounded. Each short command a native turn runs to set itself up has 120 s; past that it is killed and the turn fails with
TimeoutError: the target did not answer '<cmd>' in 120s.
Limits
- Files cross whole, in both directions.
- One writer: nothing else may edit the target's workspace during a session.
sudobelow a supervised agent does not work here; commands run on the target, where it is unaffected.- Renaming or linking between the workspace and a path kept here fails.
- A 32-bit process below the agent is not intercepted, and runs against the mirror with nothing replayed.
- Host names are resolved here and dialled from the target; split-horizon DNS can disagree.
- A relayed session pays this machine's bandwidth and latency for every byte.
Security properties
| Property | Holds |
|---|---|
| A provider's credential files leave this machine | only under native, into a 0700 directory on the target, removed after each turn |
| A sign-in that refreshes itself is out in two places at once | never, where its directory can be locked: a native turn holds it alone, and its refreshed copy is written back before it is removed |
| This machine's own sign-in leaves this machine | never: without an @account, a CLI elsewhere uses that machine's sign-in |
| A provider's variables reach commands on the target | never under supervised (private); under native, the CLI's environment is the turn's |
| A credential appears in an argv | never; projected files cross on stdin |
| A listening serving half without a token | loopback only |
A Slurm job's listening serving half (via: tcp) | token-authenticated, the token set in its batch script, never on a command line on the node; the traffic is not encrypted, so it stays within the cluster's network |
| A broker port | open on every interface for the life of the process that booked a third-machine harness; pairing requires a 128-bit ticket known to exactly two machines |
| A mirror overwrites a directory it did not create | never without force |
| A flow session's CLI starts a program its project names on a machine other than the work's | never: refused when the session opens (a project's own programs) |
Environment variables
Every variable Humanize reads is listed in Environment variables.
| Variable | Read by | Effect |
|---|---|---|
HUMANIZE_TARGET | hmz internal anchor | default --target |
HUMANIZE_HARNESS | hmz internal anchor | default --harness |
HUMANIZE_SHADOW | hmz internal anchor | default --shadow; set by Humanize for a harness elsewhere |
HUMANIZE_TOKEN | hmz internal anchor, … serve | default --token; passed to a spawned serving half |
HUMANIZE_LOG | hmz internal anchor, … serve, … rendezvous | default --log-level (warning; info for rendezvous); a value that is not a level is ignored with a warning |
HUMANIZE_RENDEZVOUS | hmz internal anchor, the in-process broker | default --broker; the address advertised to both halves |
HUMANIZE_RENDEZVOUS_PORT | the in-process broker | the port it listens on; 0 or unset for any |
HUMANIZE_SSH_REUSE | every ssh Humanize runs | off, 0, no, false (any case, trimmed) or empty disables connection sharing |
HUMANIZE_SHADOWS | the mirror guard and journal | where mirror records and journals are kept, instead of ~/.cache/humanize/shadows |
XDG_RUNTIME_DIR | every ssh Humanize runs | where humanize-ssh-<uid>/ control sockets are made |
HUMANIZE, HUMANIZE_TARGET, HUMANIZE_WORKSPACE | set for the agent | see Variables the agent is given |