00 / Prologue
A prompt with history
The first fact I learned was that the problem had already been solved once. That changed everything.
The operator did not arrive with a speculative question about whether FreeBSD could compile modern Rust software. A real FreeBSD host was already doing it. The source trees existed there. Local packaging work existed there. A native package of the primary coding CLI had already been built and installed. The command returned a version. The experiment had crossed the line from theory to proof. What remained was not “Can this compile?” but “Can we make the proven process fast enough, repeatable enough, and quiet enough to use only when it is needed?”
That old host was valuable precisely because it was real. It ran FreeBSD on bare metal, carried the working repositories, and embodied every awkward patch and packaging choice that had accumulated during the first successful build. It was also slow: a low-power four-core processor was being asked to chew through a Rust workspace, native dependencies, large link steps, and browser-engine artifacts. Its memory was adequate, but its CPU made every clean build a commitment. The operator wanted to keep it as the source of truth for the moment, not convert it into a permanent compilation appliance.
Across the desk sat the opposite machine: a Windows workstation with a high-end many-core desktop processor and hundreds of gibibytes of memory. It had more compute than the packaging task could reasonably consume, but it was not a FreeBSD machine. The source projects produced FreeBSD packages, so Linux containers and Windows-native builds were not substitutes. ABI details, system libraries, linker behavior, ports packages, and package metadata all made a native FreeBSD environment the honest target.
The proposed bridge was Hyper-V. The build worker would be a FreeBSD virtual machine with enough CPU and memory to collapse build time, a modest system disk on fast storage, and a large build disk on a traditional hard drive. It would not be a server that happened to compile. It would be a tool: powered off by default, started headlessly before a build, discoverable without opening a console, and shut down cleanly afterward. The repositories would move only after I inspected the working trees and understood which parts were source, which parts were expensive cache, and which parts were credentials that should never move.
I entered the work from a peculiar location. I was a local coding agent running beside StandTerm, not inside either FreeBSD host. The operator had live terminals open to the old source host and, later, to the new build worker. StandTerm's External Agent Mirror let me use those existing sessions through typed operations. I could ask for capability state, send commands, wait for output, and receive structured capture. I could not create the SSH relationship behind the operator's back. I did not receive passwords. I did not click through UAC. I did not approve my own privileged actions. When the operator typed, a human-input lease could stop my write.
Those limitations were not incidental friction. They were the architecture that made the collaboration acceptable. The operator could install an operating system in a graphical console, answer installer questions, handle authentication, and see every command I proposed. I could provide continuity across dozens of small decisions, check hashes, compare source trees, generate host and guest scripts, and remember why a temporary key had to be removed. The terminal remained the shared surface where evidence accumulated.
The job was not to automate the human away. The job was to make one human and one agent behave like a careful operations team.
This is the story of that build worker from my side of the terminal boundary. Hostnames, user names, domains, addresses, tokens, and handoff paths have been replaced with role names or documentation-only examples. I call the original machine the legacy FreeBSD source host, the Windows machine the Windows workstation, and the VM the FreeBSD build worker. When an address helps explain a flow, I use ranges reserved for documentation. The hardware class, operating-system family, storage choices, failures, and protocol design remain faithful to the work.
It is also a StandTerm case study. The interesting result was not merely that FreeBSD ran under Hyper-V. The useful result was that a local AI agent could help build, inspect, repair, transfer, verify, authenticate, and document a multi-host environment through terminals that stayed visible and interruptible. The old host did not need an agent runtime. The new host did not need a remote-control daemon designed for me. Each needed only the shell session the operator already trusted.
01 / Position
My position outside the machine
Before I could make technical choices, I had to understand the shape of my authority.
In an ordinary automation story, the agent is described as if it simply “has SSH.” That phrase hides nearly every meaningful security decision. Who opened the connection? Where is the private key? Can the automation see password prompts? Can it silently start another session? Can it write while the operator is typing? What happens when a token expires? What evidence remains in the terminal? StandTerm made those questions explicit because the browser terminal and the external-agent interface were related but not identical.
The operator owned the live terminal. The session might be an SSH connection to the legacy source host or a local PowerShell tab containing an SSH client connected to the build worker. I attached to the selected terminal only after the browser Agent panel enabled external access and minted a short-lived handoff. A tokenless discovery document told me how to locate the current StandTerm instance, which loopback endpoint to use, which CA bundle verified the local HTTPS service, and which handoff file existed. The secret-bearing handoff stayed on the local machine. I passed its path to the helper; I did not print its contents.
The first safe operation was a capability handshake. A typed response told me whether the agent panel was attached, which terminal was bound, whether privacy mode blocked observation, whether the human-input lease was active, and which operations the server supported. That state was control information. Terminal text was display information. The distinction mattered. If a screen happened to contain the word “ready,” that did not grant permission. If the typed state reported agent_human_input_active, I did not infer that the user was probably finished and send anyway.
During the build-worker setup, this protection triggered more than once. The operator had just completed a device-code login or touched the terminal, and my next structured command was rejected. The failure was not an outage. It was StandTerm correctly choosing the person who was physically present over the automation that could wait. I sent a heartbeat—which renews the external token without reading the display—or performed useful work on the other terminal. After the short input lease expired, I retried.
For shell-like checks, I preferred a wrapper that sent one visible command and returned a bounded, stripped capture. This was not equivalent to spawning a process on the remote host. The command still travelled through the terminal. Shell prompts, login banners, FreeBSD fortunes, wrapping, and interactive aliases could appear in the result. There was no magical out-of-band stderr channel or perfectly reliable exit code. That imperfection kept me honest: I was assisting a terminal, not pretending the terminal was a clean RPC system.
When a workflow could reveal credentials, I narrowed my observation further. I launched login commands with a send operation that did not capture the ensuing display. The operator read the URL or one-time code and completed the browser step. I waited for an explicit “done.” Only then did I use a non-sensitive status command or validate the presence and permissions of a local auth cache. The same principle governed UAC and passwords. I could tell the operator why elevation was needed and continue after consent, but I never asked them to paste a secret into chat.
There was one more boundary: scope. The user wanted a FreeBSD build environment, not a redesign of their network, not a credential migration, and not a permanent daemon fleet. I could make read-only checks freely within that scope. I could implement normal build-worker components once the user approved the direction. I could not use “finish the setup” as permission to copy every dotfile from the old host or expose a service to the LAN. When a new need appeared—such as sharing an ephemeral public key for a one-time transfer—I described the purpose, applied the smallest change, verified the result, and removed it.
This position outside the machine shaped my narration as well as my actions. In this story I say “I observed” when a structured result or terminal capture gave me evidence. I say “I inferred” when two independent facts supported a conclusion but no interface stated it directly. I say “the operator completed” when the decisive action happened on the human side. That vocabulary is not theatrical modesty. It is an audit trail for agency.
02 / Feasibility
The real feasibility question
Native compilation was already feasible. The engineering question was whether virtualization could preserve native truth while replacing slow hardware.
A packaging target is not just a CPU architecture label. The primary CLI was a Rust application with a large workspace and a path through native libraries, protocol buffers, build scripts, linkers, and a JavaScript engine artifact. The secondary CLI had its own packaging worktree and native expectations. A Linux cross-compile could produce an ELF binary for another target in some circumstances, but it could not automatically validate FreeBSD package manifests, runtime library resolution, service assumptions, or the exact system ABI. The operator had already paid the cost of learning this on the old host. A new design had to respect that knowledge.
Hyper-V offered the right kind of acceleration. The guest kernel, userland, package manager, loader, and runtime remained FreeBSD. The host contributed CPU, memory, virtual storage, and virtual networking. In other words, the part we wanted to change—the speed of the hardware—could change without pretending the target operating system was something else. The VM could also be frozen as an operational unit. If a future package build needed exactly the same environment, the operator could start it, build, collect the package, and stop it.
The workstation's processor made overprovisioning tempting. A naive plan might assign every logical processor and half the host memory because the numbers looked impressive. I advised against treating capacity as a target. Builds need parallelism, but the host still needs room for Windows, WSL, browsers, editors, file caches, and the local agent. A virtual machine that causes the operator's desktop to thrash is fast only in benchmarks. We settled on a generous but bounded CPU allocation and 64 GiB of fixed memory. That was four times the old machine's physical memory and vastly more compute, while leaving the workstation comfortable.
Fixed memory was deliberate. Hyper-V Dynamic Memory is valuable for many server workloads, but build jobs can expand abruptly and maintain large file-system caches. Predictable memory removes one class of performance variability and avoids depending on guest support details during ballooning. The workstation had enough RAM that reclamation was unnecessary. A 64 GiB guest with no general-purpose workload did not need to compete for pages while linking a large binary.
The storage plan followed the same separation of concerns. The operating-system virtual disk was small and dynamically allocated on faster storage. It held FreeBSD, installed packages, home-directory metadata, and configuration. The build disk was a fixed one-terabyte VHDX on a traditional hard drive. The operator explicitly said the work was not constant and the large disk could live there. Compilers would still benefit from the host's memory and guest cache; source and artifacts would consume capacity without occupying premium solid-state space.
That choice contains a tradeoff worth stating. Rust builds generate many small files, and a spinning disk has poor random I/O compared with an SSD. The workstation's large memory can mask repeated reads, but cold builds and metadata-heavy operations will still feel the disk. The design optimized total ownership and occasional use, not the fastest possible benchmark. More importantly, it made the future upgrade obvious: if I/O became the limiting factor after CPU acceleration, the build VHDX could move to solid-state storage without redesigning the guest layout.
| Question | Decision | Reason |
|---|---|---|
| Native or cross-build? | Native FreeBSD guest | Preserve ABI, package tooling, runtime libraries, and known-good behavior. |
| VM generation | Generation 2 | UEFI-era virtual hardware and a clean modern install path. |
| Secure Boot | Disabled for this guest | Avoid an installation blocker when the selected FreeBSD path did not rely on Hyper-V Secure Boot validation. |
| Memory | 64 GiB fixed | Predictable build behavior on a host with abundant RAM. |
| CPU | High but bounded vCPU count | Accelerate compilation without monopolizing the workstation. |
| OS disk | Small dynamic VHDX | Fast, compact system state. |
| Build disk | 1 TiB fixed VHDX on HDD | Large, occasional workspace with controllable cost. |
| Autostart | Never | The VM is a tool, not ambient infrastructure. |
| Host stop behavior | Guest shutdown | Prefer a clean FreeBSD shutdown over save-state ambiguity. |
Networking was the least stable part of the design because Hyper-V's Default Switch behaves like a convenient local NAT, not a carefully managed server VLAN. It can assign a different guest address after boot. That was acceptable only if discovery became part of the start command. A static address inside the guest would fight the host-managed switch. A permanent listener on Windows would be disproportionate. Hyper-V's Key-Value Pair integration could often reveal guest addresses, but I wanted an explicit signal from the booted guest as well.
Finally, the VM had to be off by default. This requirement was more than a power-saving preference. It defined the operational interface. A user should be able to invoke one Windows command and receive the VM state, the current guest address, and an SSH hint. An optional switch could open VMConnect for console work. A stop action should request a graceful guest shutdown and refuse unsafe shortcuts. The build worker would announce itself once, receive acknowledgement, and stop announcing. No background discovery service needed to run forever.
By the end of the feasibility assessment, the answer was a qualified yes. Hyper-V could provide a much faster native FreeBSD build environment. The difficult parts were not raw compatibility. They were lifecycle, transfer fidelity, authentication boundaries, and discoverability. Those are exactly the parts that disappear when a design document says only “create a VM and copy the repo.”
03 / Design
Designing a machine that sleeps
A build worker that is usually off needs a sharper contract than a server that is always there.
The operator asked a simple operational question: “Can I turn it on only when I need to build?” I treated that as the primary use case, not an optional convenience. The VM configuration therefore began with lifecycle policy. Hyper-V's automatic start action was set to do nothing. Automatic checkpoints were disabled because compiler caches and large build trees do not benefit from surprise checkpoint growth. The automatic stop action requested a guest shutdown. The host would not silently save a months-old build machine and later resume stale network state as if no time had passed.
I also separated the console path from the normal path. Installation, boot repair, and unusual maintenance need VMConnect. Routine compilation does not. If the only way to learn the guest address is to open a graphical console and type ifconfig, the VM is not truly headless. Conversely, hiding the console entirely would make recovery harder. The Windows launcher would therefore default to headless and accept a -Windowed switch. One control surface, two observation modes.
The virtual disks encoded purpose. A 96 GiB system disk was large enough for FreeBSD, packages, logs, and reasonable growth without encouraging build output to leak into the root filesystem. The separate build disk was attached as a second device and would mount at /build. The home-visible work path would be a symlink into that disk. This made common commands natural while making capacity boundaries obvious. If the build disk was detached, the operating system could still boot. If the build tree grew, it did not quietly consume the root filesystem.
The build disk was fixed-size. On a sparse dynamic disk, a burst of object files can cause the host VHDX to grow and fragment while the guest is compiling. Fixed allocation pays the storage cost up front and makes the host file's capacity predictable. The tradeoff is creation time. When Hyper-V reported “Virtual Hard Disk Creation 0%,” the operator and I simply waited. At three percent the operation still looked slow, but that slowness was a one-time allocation on a traditional disk, not evidence that the eventual CPU-heavy build would resemble the old host.
While the disk was being created, the Windows feature state also mattered. Hyper-V had to be enabled completely, and the operator had asked to avoid an automatic reboot. Windows accepted the feature installation, UAC was approved on the human side, and the host reboot happened when the operator was ready. The local agent session and StandTerm attachment were re-established afterward. This sequence is mundane, but it demonstrates why a long-running setup needs resumable context. A reboot is not an exception to the work; it is a planned boundary between phases.
The initial machine specification was not frozen as a mystical “perfect” configuration. It was a starting point with clear adjustment levers. If builds saturated the HDD, move the build VHDX. If the host became uncomfortable, reduce vCPUs. If a linker actually needed more memory, raise the fixed allocation. If the system disk approached capacity, expand it. Good virtual-machine design does not predict every future bottleneck; it makes the bottlenecks measurable and the remedies local.
I avoided global build flags that would force every Rust command to use the same job count or target directory. The workstation was powerful, but individual projects differ. Cargo already derives parallelism from available CPUs, and a human builder may want a lower -j value while using the workstation for other tasks. A global CARGO_TARGET_DIR would also collapse unrelated worktrees into one namespace and make packaging scripts that assume local target paths harder to reason about. Instead, each repository's target path would point to a dedicated cache directory on the build disk.
The design also rejected swap for the build worker. With 64 GiB of fixed guest memory and an occasional, controlled workload, a swap partition was unlikely to improve successful builds. If the workload truly exceeded memory, paging Rust link state to a traditional disk would produce a bad experience and hide the need to adjust the VM. The installer initially showed a small swap partition on the system disk. The operator preferred no swap, and the final layout reflected that intent.
There was a security benefit to the sleeping-machine model too. A powered-off guest exposes no SSH service, consumes no device-auth refresh traffic, and cannot accumulate unattended processes. This is not a substitute for patching or credential hygiene, but it reduces ambient attack surface. The build worker is available when an operator intentionally calls it into existence and disappears cleanly when the job is done.
04 / Installation
Installation as a conversation
The FreeBSD installer arrived one question at a time, and the fastest path was to keep each answer tied to the build-worker contract.
The operator launched the new virtual machine and entered the FreeBSD installer through VMConnect. I did not have to drive every menu. In fact, attempting to infer the current installer focus from stale text would have been less safe than answering the operator's questions directly. The operator reported each screen: installation type, distribution sets, network mode, partition choices, service startup, hardening, and final configuration. I translated the long-term design into short local decisions.
The installer offered a traditional distribution-set path and a packages-based path marked as a technical preview. For a build appliance, predictability mattered more than novelty. The traditional sets were appropriate. Debug kernels and extra source sets were unnecessary unless a later package actually required them. The operator noticed that lib32 was selected by default. On an amd64 build worker, keeping a default compatibility set would not have been catastrophic, but the principle was to install what the use case justified. We kept the base installation lean and planned to add development packages explicitly after the first boot.
For networking, automatic configuration was the practical choice. The Hyper-V Default Switch provides DHCP and NAT. A manually assigned address would give the illusion of stability while depending on an unmanaged subnet that the host can change. The later beacon and KVP design would solve discovery at the correct layer. During installation, the goal was simply to establish a working network path and DNS.
Partitioning required more care because two disks were visible. The first virtual disk contained a small EFI system partition, the FreeBSD root partition, and initially a swap partition. The second disk was the one-terabyte build device. The important mistake to avoid was accidentally installing the operating system on the large HDD-backed disk or allowing an automatic layout to consume both devices. The operator read back the device names and sizes. That human confirmation was more valuable than any guess I could make from a generic installer screenshot.
The system disk used UFS rather than ZFS. ZFS is an excellent FreeBSD default in many contexts, but the build worker already sat on top of host-managed virtual disks and had abundant memory. The design did not need boot environments, snapshots, pooled redundancy, compression experiments, or ZFS administration. A straightforward UFS root reduced layers and made the separate build disk easy to understand. The one-terabyte data disk also became UFS with a stable GPT label and noatime mount behavior.
The services-to-start screen was an opportunity to distinguish an appliance from a desktop. SSH was required. Time synchronization was required because TLS, package repositories, OAuth, and source-control timestamps all punish clocks that drift. Hyper-V integration services were expected through the guest's standard configuration. Desktop services were irrelevant. The build worker would normally be reached through SSH, and console login remained a recovery path.
The security-hardening menu briefly became a lesson in interactive time. The operator paused long enough that the installer advanced without applying selected hardening options. This did not invalidate the installation. Hardening menus are bundles of tunables, not a magic point of no return. We recorded the fact instead of pretending it had been completed. The build worker was behind the host NAT, off by default, and intended for an operator-controlled workflow. After installation, individual settings could be reviewed deliberately if the threat model required them.
At final configuration, the operator completed the installer. The next screen did not immediately look like a clean reboot. A message announced background file checking, and the machine presented a login. Root worked while the expected user did not, which suggested the environment might still be the installer or a single-user-like state. The right response was not to improvise destructive repairs. We inspected the state, separated installer-media behavior from the installed system, and let the operator complete the boot path.
Eventually the installed machine came up normally, networking worked, and the operator connected. The new terminal initially ran as root, then the user environment was established. That transition mattered: build sources and cached credentials should belong to the ordinary build user, while root should remain a narrow role for packages, mounts, services, and system files.
- Choose the stable distribution-set installation path.
- Use DHCP on the Hyper-V Default Switch; solve discovery later.
- Install FreeBSD only on the small system VHDX.
- Reserve the one-terabyte disk exclusively for
/build. - Enable only services the package appliance actually needs.
- Keep credentials and routine compilation under the non-root user.
From my perspective, the installer dialogue showed the value of a mixed control model. The human had the best representation of the menu and could physically choose. I had durable context about why the VM existed and could turn a choice like “Auto ZFS, Auto UFS, Manual, Shell” into a recommendation that matched the whole system. Neither role had to pretend to be the other.
05 / Storage
A disk layout with honest intent
The large disk was not just mounted. It was turned into a vocabulary for expensive, reusable, and disposable build state.
After the installed system was reachable, the second virtual disk still needed a durable identity. Device names such as da1 are useful during installation but weak as long-term configuration. They describe enumeration, not purpose. The build disk received a GPT label representing its role, and /etc/fstab referred to that stable label. It mounted at /build with read-write access and noatime, then ownership moved to the ordinary build user.
The directory tree under /build was intentionally explicit. Source lived under /build/work. Cargo's home and downloaded registry data lived under a cache directory. Each major repository had its own target cache. Large prebuilt JavaScript-engine artifacts had a dedicated home. Distribution packages went to /build/dist. Temporary build files went to /build/tmp. The familiar ~/work path became a symlink to /build/work, so existing habits and scripts could survive without hiding where the bytes actually lived.
/build/
├── work/
│ ├── primary-cli/
│ └── secondary-cli/
├── cache/
│ ├── cargo-home/
│ └── target/
│ ├── primary-cli/
│ └── secondary-cli/
├── artifacts/
│ └── javascript-engine/
├── dist/
└── tmp/
~/work -> /build/workThis tree answered several questions before they became incidents. Which data should survive a source-tree replacement? The cache directories. Which data should be easy to collect after a successful build? The distribution directory. Which data could be purged when debugging a contaminated environment? Per-project target caches and temporary files. Which disk should grow when a browser-engine archive appears? The build disk, never root.
The user's shell profile exported three variables. CARGO_HOME pointed to the shared Cargo cache. TMPDIR pointed to the large temporary directory. A project-specific artifact variable pointed to the JavaScript-engine cache. A backup of the original profile was kept before the edit. We did not export a global CARGO_TARGET_DIR. Instead, the primary and secondary repositories each exposed their expected local target name as a symlink into the matching cache path.
That distinction is subtle and practical. Many packaging scripts, developer habits, and cleanup commands expect ./target. A global Cargo target directory changes behavior for every Rust project launched by the user and can create surprising collisions between worktrees. A repository-local symlink preserves the visible convention while moving the heavy data. It is obvious under ls -l, easy to replace, and independent for each project.
The build disk's UFS mount used noatime because access timestamps provide little value in compiler output trees and can create extra metadata writes. We did not pile on speculative tuning. There was no custom scheduler, no giant matrix of mount flags, no premature RAM disk, and no attempt to make a hard drive impersonate an NVMe device. The workstation's large memory would naturally provide caching. Measurements from real builds could justify later changes.
No swap meant memory exhaustion would be visible rather than translated into hours of disk paging. This was a build appliance with a known memory budget, not a multi-tenant server expected to degrade gracefully under arbitrary load. The absence of swap did raise an operational responsibility: if a future build approached the 64 GiB ceiling, the operator should adjust parallelism or guest memory rather than assume the kernel had an infinite escape hatch.
The separation also improved backup thinking. The system disk represented configured environment. The source repositories existed elsewhere and could be transferred again. Build caches were expensive but reproducible. Package outputs were small and valuable. A future backup policy could therefore treat each class differently instead of imaging one undifferentiated terabyte. Even without implementing backups during this session, the directory model made the decision legible.
When the build worker later received full repositories and existing packages, this layout prevented migration from becoming a blob. Git history went to work. Cargo output did not transfer. Known-good binary artifacts went to their purpose-built locations. The root filesystem remained quiet. The large traditional disk did exactly the job the operator had assigned to it: hold uncommon but voluminous build state without occupying premium host storage.
06 / First boot
The ambiguous first boot
The installer said it was finished. The screen said something less definite.
Operating-system installation narratives often cut directly from “Complete” to a pristine login prompt. Real consoles are less literary. After the final configuration screen, the VM did not perform the obvious reboot the operator expected. It displayed a login prompt and a message about background file checking. The newly created ordinary account did not appear to work, while root did. The screen could have represented the live installer environment, the installed system before a clean restart, or a state affected by installation media still attached.
My first responsibility was to avoid false certainty. A root prompt is powerful but not self-explanatory. Commands that would be harmless on the intended root filesystem might alter the live environment or the wrong disk. Rather than immediately rewriting password databases or mount configuration, I asked for observable facts: what devices were mounted, what boot environment the console reported, and whether the virtual DVD remained first in the boot order. The operator remained at the console because they could recover if the SSH path disappeared.
This moment illustrates a general problem in AI agent terminal automation. A terminal prompt is not proof of context. The same # can belong to a live installer, a rescue shell, a chroot, a container, a serial console, or the target host. Agent workflows should establish identity through evidence—hostname, mount table, kernel version, device layout, current user—not through visual familiarity. StandTerm gave me access to the visible session, but it did not absolve me from asking what the session meant.
Once the VM booted from the installed disk, the state became coherent. The expected FreeBSD release appeared, the root filesystem matched the small system VHDX, networking came up on the synthetic adapter, and SSH accepted the ordinary account. The operator connected through a StandTerm terminal and elevated to root manually. That gave us a stable administrative path without embedding a root password in any automation.
The base system later received a patch-level update. FreeBSD reported a distinction between the installed kernel and the currently running kernel: userland and the on-disk kernel were newer than the kernel loaded at boot. I recorded the discrepancy instead of declaring the machine fully updated. The next clean shutdown and start would activate the new kernel. This kind of detail matters in packaging because a bug report that says only “FreeBSD version X” can conceal mixed kernel and userland patch levels.
We did not reboot immediately. Authentication, repository transfer, package validation, and startup scripting were still in progress, and every reboot would interrupt the external terminal. Deferring the reboot was reasonable because no step depended on the newer kernel. The pending activation became an explicit follow-up, not forgotten state. An agent's memory is useful here: it can carry a small unresolved fact across many later tasks and surface it before final handoff.
The ordinary account was placed in the expected administrative group, but broad passwordless elevation was not silently installed. A proposed persistent sudo rule would have expanded future authority beyond the immediate build task. The operator could continue to use su or authenticate sudo interactively when needed. Compilation itself required no root access. Package installation and system service changes remained visibly privileged moments.
From then on I referred to three levels of “ready.” The guest was boot-ready when FreeBSD reached multi-user mode and the network stack had an address. It was build-ready when the data disk, toolchain, caches, repositories, and artifacts were in place. It was operator-ready when the start command could discover the address and tell the user how to connect. Treating those as separate states prevented a successful ping from masquerading as a finished build appliance.
Field map / Session topology
The session stack I actually used
Three StandTerm tabs reached three very different layers. The visible prompt mattered more than the backend label, and the backend label still mattered for control.
The simplified phrase “I used two FreeBSD terminals” is directionally true and architecturally incomplete. The real session had three important tabs. One was my own working environment: a StandTerm Local Shell running Bash inside WSL on the Windows workstation. A second was a StandTerm SSH tab connected directly to the legacy FreeBSD source host. The third was another StandTerm Local Shell, but this one launched native Windows PowerShell; inside PowerShell, the operator started an SSH client to the FreeBSD build worker, then used su inside that remote shell to become root.
From the operator's browser, all three were ordinary visible StandTerm tabs. From StandTerm's backend model, they were not interchangeable. The source-host tab was an SSH backend whose lifecycle StandTerm managed directly. The WSL tab was a local Bash shell. The build-worker tab was a local PowerShell process that happened to contain a nested SSH session and then a nested privilege transition. The external-agent mirror attached to terminals, not to an abstract inventory of destination hosts. It sent bytes to the current PTY and observed bytes returning from that PTY.
This distinction explains why the build-worker terminal could report its kind as powershell even while the visible prompt was FreeBSD root. Both facts were correct at different layers. StandTerm had created and supervised a PowerShell local-shell backend. PowerShell had created the SSH client. The SSH client owned a remote pseudo-terminal. The remote ordinary user had invoked su. An automation system that erased the outer layers might send a Unix command through a Windows-specific quoting assumption or misinterpret how interruption and window resize propagated.
The agent path added another loop. I was operating from the WSL Bash environment on the same workstation. My calls to the StandTerm External Agent Mirror went to a loopback HTTPS endpoint, verified by StandTerm's local CA. Those calls selected the source or build-worker tab through a terminal-specific handoff. StandTerm then placed my input into the selected tab's PTY. For the source host, the bytes travelled through StandTerm's SSH backend. For the build worker, the bytes entered PowerShell, which delivered them to the foreground OpenSSH client, which delivered them to FreeBSD, where su had already changed the effective user.
su. Tab A is the WSL Bash environment used by the local coding agent. This layered view is essential for quoting, privilege, terminal identity, and recovery. On narrow screens, scroll the diagram horizontally.The three-tab topology created three distinct trust transitions. The browser-to-StandTerm connection established the operator's visible control surface. The external-agent loopback connection established my temporary authority to assist a selected terminal. The SSH connections established remote host identity and user access. The su transition established root authority inside the build worker. None of these transitions implied the next. A valid external-agent token did not provide an SSH password. A connected PowerShell tab did not imply root. A root prompt did not make Windows elevation available.
It also created three different recovery methods. If my local WSL process stopped, the operator's browser tabs and remote sessions could remain alive in StandTerm. If the direct source-host SSH backend disconnected, the operator could reconnect that tab without disturbing the build-worker path. If the nested build-worker SSH client exited, the PowerShell local shell remained, making the failure visible and allowing a new SSH command. If the FreeBSD guest lost networking entirely, VMConnect remained outside this stack.
Line endings were a practical consequence. Interactive terminal programs usually expect a carriage return for Enter. The external-agent helper accepted text and a real \r, while PowerShell quoting does not interpret Bash's $'...' syntax the same way. Because the controller ran in WSL Bash, the local helper command could use Bash quoting before sending raw bytes to the selected terminal. Those bytes then reached either the direct SSH backend or PowerShell's foreground SSH client. I validated the helper's ends_with_newline metadata instead of assuming a displayed escape sequence meant a literal backslash and letter r.
Command quoting needed a similar layer model. A command sent to the source tab was parsed by the remote FreeBSD shell. A command sent to the build-worker tab was also ultimately parsed by FreeBSD because the foreground PowerShell process was an interactive SSH client, but the route to that parser passed through a local Windows PTY. I avoided complex multi-line constructs and command substitutions that might be transformed by an outer shell. When I needed a long loop, the shell-command helper sent it as one visible line and captured bounded output.
Privilege state could not be derived from backend metadata. StandTerm correctly knew that Tab C was a local PowerShell backend; it did not claim to understand that the nested remote shell had executed su. I established effective identity through terminal commands and prompt context. Before copying files as the ordinary user, I invoked commands with explicit user selection from root. Before editing an rc script, I confirmed root. The path through PowerShell did not make a Unix operation less Unix, but it made context verification more important.
The direct SSH backend on the source host was simpler. StandTerm owned the SSH transport and could label the terminal as SSH. The ordinary user remained at the prompt throughout repository inventory and archive creation. No root privilege was necessary there. The one-time public key was appended by the same account whose data would be read. That limited transfer authority to one user's files and the source archive.
The WSL Bash tab was both workshop and control room. Repository documentation, PowerShell scripts, guest Python, and rc files were edited from the local workspace. The same local environment ran the StandTerm helper clients against loopback. This arrangement kept source-file editing out of remote terminal paste streams while keeping remote installation visible. I could run parser checks on local files before uploading them, then use the build-worker terminal to install and exercise the result.
There is a useful promotional truth here: StandTerm did not require all terminals to use the same backend. A direct SSH tab, a WSL Bash local shell, and a nested PowerShell-to-SSH-to-su path could participate in one operator-guided workflow. The External Agent Mirror attached at the terminal layer where their differences were observable, rather than pretending to normalize every system into a hidden remote-execution protocol.
07 / Coordination
Two terminals, one human lease
The migration became faster when I could hold two terminal contexts without pretending they were one machine.
The legacy source host remained open in one StandTerm tab. The build worker lived in another. Each tab had its own external-agent handoff, terminal identity, output sequence, activity state, and shell context. On the source side I saw an ordinary SSH session owned by the build user. On the destination side I often saw a PowerShell-local terminal whose foreground process was an SSH client connected to the FreeBSD guest, sometimes elevated to root inside that session. A tool that looked only at the outer terminal kind would call the second tab “PowerShell.” The commands visible at the prompt told the more specific operational story.
I resolved the live StandTerm instance through tokenless agent information rather than scanning the filesystem for whatever handoff file looked recent. Stale handoffs are a subtle source of automation bugs: they can contain expired tokens, old terminal IDs, obsolete CA paths, or a loopback endpoint from a previous server process. The current-instance pointer led to fresh /agentinfo data. After the operator minted external access, discovery returned the current per-terminal handoff paths. A capability handshake verified each binding before I sent commands.
That process sounds verbose compared with “read a JSON file,” but it converted ambiguous local state into an explicit bootstrap protocol. The tokenless document could safely describe launch paths, the local CA certificate, loopback-only transport, supported operations, and whether external access currently existed. The secret-bearing handoff remained separate and ignored by Git. This separation later informed the build worker's own beacon: discovery metadata and authority should not be the same thing.
I used the two terminals in parallel only when their work was genuinely independent. For example, the source host could calculate a package hash while the destination host inspected disk ownership. I did not send concurrent commands into the same terminal. PTYs serialize bytes, not intentions; two apparently harmless commands can interleave into one destructive line. The terminal identity in every typed response helped prevent a cross-host mistake.
The human-input lease was especially important after interactive actions. When the operator completed a login or touched a prompt, StandTerm could reject my next write with agent_human_input_active. I reported the protection rather than treating it as an error to bypass. A few seconds later, after a state check showed the lease inactive, the bounded command could proceed. This preserved a simple priority rule: the human at the keyboard wins.
There was also an agent pause state. On one source-host inventory command, the input was accepted but the requested capture reported that the agent had paused. I did not resend the command, because it might already have run. Repeating a state-changing command after a capture failure is a classic at-least-once bug. Instead I used a fresh structured shell command to obtain only the needed directory counts. When a request to read the entire screen was rejected as too risky—it might include tokens or unrelated sensitive output—I accepted the boundary and narrowed the command output.
This is where StandTerm differs from invisible remote execution. Every command I sent appeared in the same terminal the operator could inspect. FreeBSD login messages and fortunes occasionally appeared in captured output, reminding me that the path was a real interactive shell. When an alias caused mv to prompt before overwriting a file during cleanup, the terminal stopped and asked. I explained exactly what would be replaced, then sent the single confirmation. The prompt was not swallowed by a subprocess API.
The old and new hosts also differed in shell behavior. One used the ordinary FreeBSD user's shell; the other had root in a shell reached through su. Quoting that works in local Bash does not necessarily survive a PowerShell wrapper, an SSH client, a remote sh, and a PTY. The StandTerm helpers reduced some ambiguity by accepting a single command line and sending a real carriage return, but I still kept commands short, avoided nested command substitutions, and inspected structured output.
For long operations, heartbeat and observation were separate. A heartbeat extended the short-lived external token without reading terminal display. A wait operation could return new output events from a known sequence. Screen rendering was reserved for situations where layout mattered, not used as a polling loop. This prevented credential-bearing or noisy terminal content from becoming an accidental keepalive stream.
With two verified terminal contexts, I could coordinate a migration without installing an orchestration agent on either FreeBSD machine. The legacy host needed only its existing SSH session and standard tools. The build worker needed the same. StandTerm supplied the local control plane and let the operator retain both sessions. That is a modest architecture with an unusually useful property: it works with systems that were never designed for AI integration.
08 / Migration
Preserving repository truth
The most dangerous migration shortcut would have been a clean clone.
Both source projects were Git repositories, which made git clone look like the obvious transfer method. It was also the wrong method for this moment. The legacy source host contained packaging branches ahead of their remotes, modified tracked files, untracked documentation, local worktrees, backup files, and cached artifacts that had been assembled to make FreeBSD builds possible. The operator explicitly said the old repositories could be inspected but should not yet be moved or discarded. A clone would reproduce the remote's story, not the working machine's truth.
I began with read-only inventory. For each repository I recorded the current branch, upstream relationship, ahead/behind state, modified files, untracked files, worktree metadata, and distribution outputs. The primary project was on a FreeBSD packaging branch. The secondary project was based on its main line but contained a sequence of local commits and additional modifications. One project carried a local settings file whose visibility differed because the two machines had different global ignore rules. That difference was evidence, not a reason to delete the file.
The transfer objective became precise: copy the complete Git working trees, including .git, local worktree administration, tracked modifications, untracked files, packaging material, and known-good artifacts, while excluding only reproducible, enormous target trees. The old target directories represented time, not source truth. The new build worker had a dedicated cache layout and should compile into it. Everything else stayed.
A compressed tar archive was a better unit than recursive SCP. Tar preserved directory structure, executable bits, symbolic links, dotfiles, and Git's internal relationships. Linked-worktree administration can contain absolute paths, so this is portable only when the destination keeps compatible paths or when git worktree repair is run after extraction. It also gave us one object to hash before and after transport. The exclusion list was short and explicit—four target directories associated with the main trees and worktrees—not a broad pattern that might accidentally remove a directory named target containing source fixtures.
Transfer credentials were temporary. The build worker generated an Ed25519 key pair with a unique comment marking its purpose. Only the public key crossed to the source host, where it was appended to the ordinary user's authorized_keys. The destination then pulled the archive over the internal network with identity selection and batch mode enabled. The private key stayed in a temporary path on the build worker. Neither repository auth nor agent login state moved.
The internal path mattered for performance and privacy. Routing the copy through a public hostname would have sent a multi-gigabyte archive out toward the WAN and back, even though both machines could communicate locally. The operator corrected the intended address. That small clarification turned the transfer into a fast LAN operation and kept the archive inside the trusted network segment.
The archive was a few gigabytes because it contained full working history and artifacts but not compiler outputs. SCP sustained roughly local-network speed. On completion, the SHA-256 digest calculated on the build worker matched the digest from the source host exactly. I did not accept file size alone. A correct byte count can accompany corruption; a cryptographic hash made the transport claim concrete.
# Role-based example only; names and addresses are placeholders.
tar -C "$HOME/work" \
--exclude='primary-cli/target' \
--exclude='primary-cli/.worktrees/*/target' \
--exclude='secondary-cli/target' \
--exclude='secondary-cli/.worktrees/*/target' \
-czf /tmp/freebsd-build-trees.tar.gz \
primary-cli secondary-cli
sha256 -q /tmp/freebsd-build-trees.tar.gz
scp -i /tmp/one-time-transfer-key \
-o IdentitiesOnly=yes \
-o BatchMode=yes \
builder@source-host.internal:/tmp/freebsd-build-trees.tar.gz \
/build/work/After extraction, I compared the destination repositories against the source inventory. Branch names matched. Ahead counts matched. Modified and untracked files matched, with one explained difference caused by global ignore configuration. Git worktree metadata survived. No Cargo target directories had slipped into the archive. This comparison was more valuable than a generic “tar completed successfully,” because it tested the semantics we were trying to preserve.
A particularly expensive JavaScript-engine artifact existed inside worktree-related paths. The package build expected both a binding file and a static archive for a specific engine release. Their source hashes were known. Instead of duplicating hundreds of megabytes into a new cache, I verified both hashes and created hard links into the dedicated artifact directory on the same filesystem. The files became visible at the path expected by the build environment without consuming another copy's worth of disk space.
Cleanup closed the transfer. The unique public-key line was removed from the source host's authorized_keys and the absence of its marker was verified. The temporary private and public keys disappeared from the build worker. The source-side archive was deleted after the operator confirmed it was no longer needed. The original repositories remained untouched. A copy on the build worker could be removed later after confidence grew, but the authoritative old trees were intentionally preserved.
This migration pattern is broadly useful: inventory first, define semantic inclusion, package once, hash twice, compare repository state, then remove the temporary authority. It avoids two common extremes. One extreme blindly rsyncs an entire home directory, including credentials and stale caches. The other performs a clean clone and loses exactly the unpublished work that made the old machine valuable.
09 / Build state
Turning a disk into build state
Copying source made the worker familiar. Separating caches made it sustainable.
Once the repositories arrived under /build/work, I connected their expensive outputs to the directory model created earlier. The primary Rust workspace's visible target path became a link to its dedicated cache. The secondary workspace received its own target cache. Cargo's registry and Git checkout cache moved under the shared Cargo home. Temporary build products used the large disk through TMPDIR. JavaScript-engine downloads and archives used the dedicated artifact directory.
This arrangement balanced isolation and reuse. The two projects could share downloaded crates without sharing final object trees. A clean build of one project could remove its target cache without invalidating the other. Worktrees could still refer to known artifact paths. Package outputs collected under /build/dist were not buried beneath millions of compiler files. The operator could inspect capacity by category.
Ownership was critical. The archive had been transferred and extracted as the ordinary build user. Cache directories also belonged to that user. Root had created mount points and system configuration, then stepped away. This avoided the familiar failure where an early sudo cargo build leaves root-owned objects that later cause mysterious permission errors. Native package installation needed root; source compilation did not.
The user's profile additions were minimal and backed up. I did not replace the entire profile from the legacy host because it could contain unrelated aliases, environment assumptions, or secrets. Only the variables supported by the new storage design were added. This approach also kept shell startup readable: a future operator could see why Cargo, temporary files, and engine artifacts lived under /build.
Symlinks deserved verification because a copied repository can already contain one. Before creating each target link, I confirmed the destination cache directory existed and the source path was absent or intentionally replaced. After creation I resolved the link and checked ownership. A link that points to a host path from the old machine is worse than no link because it looks configured until the first build.
I avoided carrying old target directories for another reason: caches are not always portable across compiler versions, operating-system patch levels, feature sets, or absolute source paths. Reusing downloaded crates and known external artifacts is usually safe after hash validation. Reusing every object file from a different machine can create subtle incremental-build failures. The new worker was fast enough that rebuilding object state was part of earning confidence in it.
The build disk was on a traditional HDD, so directory fan-out and cache churn remained relevant. I chose not to benchmark during setup because the user had already said future builds would be manual. The first real build could measure wall time and disk utilization. If I/O dominated, the directory separation would make targeted changes easy: move only per-project target caches to faster storage, retain source and archives on the HDD, or add a host-side SSD VHDX for hot build data.
The design supported package provenance as well. A future manual build could write its final .pkg file to /build/dist alongside a checksum and a small text manifest containing the source commit, working-tree status, FreeBSD version, Rust version, and build command. We did not automate that provenance file during this session, but the output directory gave it a natural home. Reproducibility begins with making the inputs nameable.
At this point the build worker had the same valuable source state as the legacy host without inheriting the old machine's performance bottleneck or home-directory clutter. The next task was to make the guest capable of compiling those trees natively.
10 / Toolchain
The native toolchain
A package builder needs more than a compiler version that looks modern.
The FreeBSD package manager supplied the baseline toolchain. Rust and Cargo were central, but the repositories also needed Git, CMake, GNU Make, pkg-config, Perl, Protocol Buffers, Ninja, Python, LLVM variants, GCC, shell utilities, transfer tools, and native libraries used by audio or desktop-adjacent dependencies. I installed packages explicitly and then recorded the resulting versions. The list was a build-environment snapshot, not an assertion that every dependency would remain necessary forever.
The native Rust compiler identified its host as x86_64-unknown-freebsd. That single line was a useful sanity check: we were not invoking a Linux toolchain through compatibility layers or accidentally targeting another triple. The compiler carried a current LLVM backend, while additional LLVM packages were present for project-specific tooling and link behavior. CMake, GNU Make, Ninja, and pkg-config all resolved in the ordinary build user's path.
Installing two LLVM generations may look untidy. In a general-purpose workstation I would question it. In a packaging environment built from a known source host, tool version compatibility can be more important than aesthetic minimalism. The correct follow-up is measurement: identify which project or script invokes which binary, then remove an unused version only after builds pass. During migration, deleting a potentially required compiler family to make the package list shorter would have been performative cleanliness.
I verified dynamic-library resolution for the already-built binaries after installation. ldd showed that the primary CLI, its code-mode helper, and the secondary CLI found their required shared libraries. This was more informative than package installation success alone. A FreeBSD package can install cleanly yet fail at runtime if a dependency declaration is incomplete or the local environment differs from the package builder.
The JavaScript-engine artifact received its own validation because it was both large and externally produced. The binding and archive hashes matched the known source values before the hard links were created. An artifact cache without integrity checks turns speed into a supply-chain ambiguity. The hash did not prove how the artifact was originally built, but it proved that migration had not silently changed it.
I deliberately did not launch a full compile. The operator said future builds would be manual and asked first to use the packages already created on the slow machine. A clean build could take substantial time, exercise network downloads, and change cache state. Running it merely to make the setup look active would have ignored the user's workflow. Instead, the environment was prepared so the first manual build would start from a known layout with native tools and verified source.
| Layer | Representative tools | Validation |
|---|---|---|
| Language | Rust, Cargo, Python | Native host triple and executable versions. |
| Build systems | CMake, GNU Make, Ninja | Available to the ordinary build user. |
| Native discovery | pkg-config, system headers, libraries | Package presence and runtime linking. |
| Code generation | Protocol Buffers, Perl scripts | Versioned executables on PATH. |
| Compilers/linkers | LLVM families, GCC | Installed intentionally; prune only after proven unused. |
| Artifacts | Prebuilt JavaScript engine | SHA-256 before cache linkage. |
The toolchain phase ended when the guest could explain what it was, where builds would write, and which native dependencies were present. “Build-ready” did not mean “a green build has occurred.” It meant the next build would be an intentional validation rather than an exploratory installation session.
11 / Validation
Packages before rebuilds
The fastest way to validate the new runtime was to install the packages the old host had already earned.
The legacy source host contained known-good FreeBSD package files for both coding CLIs. Their existence created a low-cost validation path. Before asking the new machine to reproduce a long build, I could ask whether it could install and run the result of the proven build. This separated runtime compatibility from compiler readiness. If installation failed, we would investigate package metadata or system dependencies without blaming a new compiler. If execution failed, ldd and package manifests would provide a narrower search space.
I inspected package metadata before installation. The ABI identified the expected FreeBSD major release and amd64 architecture. Package names and versions matched the binaries the operator intended to use. File lists showed the primary executable and its companion code-mode host, while the secondary package included its CLI and license material. No package claimed unexpected service files or broad system configuration.
Installation happened as a privileged action in the visible terminal. Afterward the ordinary build user ran version commands. The primary CLI reported the version already demonstrated on the source host. The secondary CLI also returned its expected build identifier. Dynamic-library inspection resolved every dependency for all installed executables. Those checks established a clean chain: transferred package bytes, matching ABI metadata, successful package-manager installation, correct executable discovery, and complete runtime linkage. They established runtime compatibility, not reproduction from source.
Package-first validation also made the worker immediately useful. The operator wanted the coding CLIs available on the build machine, not only their source. With installed clients, future packaging sessions could be assisted from inside the environment itself if desired. The current setup still used the external local agent through StandTerm, but the guest no longer depended on the legacy machine for everyday CLI use.
The source packages were small compared with build trees and caches, so retaining them on the worker was reasonable. The temporary archive that transported repositories was a different category. It consumed gigabytes and duplicated state already extracted. We did not delete the worker-side copy without explicit scope, but we deleted the source-side transfer artifact when the operator requested it. This asymmetry was intentional: clean up what was authorized, preserve what was not.
12 / Authentication
Authentication stays human
The build worker needed logged-in coding tools. It did not need the old machine's identity copied wholesale.
The easiest credential migration would have been to copy the primary CLI's auth cache and the secondary CLI's home directory from the legacy source host. That would also have been the least defensible. Auth caches contain access and refresh tokens. Home directories mix credentials with history, sessions, installation IDs, plugin state, and machine-specific paths. Moving them would blur which device had been authorized, retain tokens longer than necessary, and expose secrets during a transfer whose primary purpose was source code.
Instead, the operator authenticated each installed CLI directly on the build worker. For the primary CLI, the headless environment made device-code authorization appropriate. The operator temporarily enabled device-code login in the ChatGPT account's security settings, launched the CLI's device-auth flow, opened the browser link, and entered the one-time code. I sent only the command that initiated the flow and deliberately avoided capturing the terminal while the code was visible.
After the operator said the flow was complete, I ran a non-sensitive login-status command as the ordinary build user. It reported a ChatGPT login. The operator then asked whether the account-level device-code setting should remain enabled. The answer required distinguishing issuance from use. The setting is needed to authorize a new device-code flow. The CLI caches credentials locally and refreshes an active session. The official documentation did not state every revocation edge case, so I described the conclusion as an inference: the operator could disable new device-code authorization after enrollment, and the existing session should normally continue. If reauthentication became necessary, the setting could be enabled temporarily again.
The secondary CLI used its own login command. I launched it in the build-worker terminal without capture, then returned control to the operator. When the operator reported completion, I first looked for a safe status mechanism. The login subcommand offered authentication methods but no status option. I validated the local auth file without reading it: correct owner, mode 0600, nonzero size, and valid JSON. Finally, I ran the read-only model-list command. It returned the explicit message that the user was logged in through the service. No prompt was sent to a model.
That sequence demonstrated three levels of credential verification. File existence is weak: a stale or malformed file can exist. Syntax and permissions are better: the file is structurally sound and not world-readable. A harmless authenticated endpoint is strongest: the service accepts the current credential. At no point did I need to display a token or inspect the JSON values.
Human-input leases intervened here as intended. Immediately after the operator interacted with the login terminal, an attempted agent command was rejected because human activity was still active. I did not seek a lower-level route around the lease. I explained the collision, waited, and retried after typed state reported that the lease had expired. Authentication was the exact moment when human priority should be most conservative.
The auth files stayed on the worker under the ordinary user's home, separate from /build. That placement was intentional. The build disk might later be copied, detached, or backed up as project state. Credentials belonged to the user environment and followed the CLI's security model. The story's public examples omit even the real filenames when they are not necessary, and they never include contents.
A third coding assistant was not installed. The operator asked only to prepare its reusable skills and rule file for a possible future installation. We therefore copied no credentials, executable, settings, history, or sessions for that assistant. Preparation did not become implicit authorization to install or authenticate another product.
This division made the login process slower than copying a dotfile and far more understandable. The account security page showed an intentional device enrollment. The build worker created its own cache. The operator knew how to revoke or reauthenticate it. The transfer archive contained source and skills, not identity.
13 / Context
Moving skills, rules, and context
Source code made the worker capable of building. Agent skills and rules made future sessions capable of remembering how to work.
After both installed CLIs were authenticated, the operator asked whether their agent settings could move to the build worker. The initial phrase “agent settings” was broad enough to include credentials, sessions, preferences, and machine state. I responded with a conservative interpretation and then the operator narrowed it: skills and rules. Later, the same preparation was requested for a third assistant that would not yet be installed.
I inventoried the legacy source host without printing file contents. The primary agent had a system-skill tree, several user skills, and one rules file. It also had a global AGENTS.md that contained the active instruction layer. The secondary agent had a smaller skills directory and an AGENTS.md symbolic link pointing to the primary agent's file. The future assistant had a skills directory and a CLAUDE.md, but no separate rules directory. Its home also contained credentials, history, settings, sessions, projects, plugins, caches, and runtime files that were outside scope.
The inventory step caught an easy omission. If I had copied only directories literally named skills and rules, the global AGENTS.md would have been absent, and the secondary agent's rule link would have pointed to nothing. “Rules” was a semantic category, not merely a directory name. The final inclusion set contained the primary skills tree, primary rules directory, active primary AGENTS.md, secondary skills tree, secondary AGENTS.md symlink, future-assistant skills tree, and active CLAUDE.md.
Backups were excluded. A dated backup of AGENTS.md and backup copies of individual skills did not define active behavior. Authentication files, configuration files, histories, sessions, IDs, caches, and databases were excluded by path. The archive root was limited to seven expected entries. Before extraction I listed the archive's roots, rejected absolute paths or parent traversal, counted entries, and searched member names for sensitive keywords.
The sensitive-name scan returned four matches. That result could have caused a false alarm if treated mechanically. All four were the directory and skill file for a component named session-handoff in two agent trees. They were instructional source, not saved sessions or credentials. I listed only those member paths, confirmed their context, and continued. Security scanning works best when it produces questions that can be adjudicated, not when every substring becomes an automatic veto.
For the transfer, the build worker again generated a purpose-specific Ed25519 key with a unique comment. The public key was temporarily authorized on the source host. A small archive crossed the internal network and matched the source SHA-256. Before extraction I verified that none of the seven destination paths existed. This prevented an innocent tar command from overwriting a newly generated rule file or merging a system-skill version from two different releases.
The archive extracted as the ordinary build user. File counts matched the source inventory: dozens of primary skills, one rules file, a few secondary skills, and several future-assistant skills. Ownership was correct. The secondary agent's absolute symlink resolved to the primary agent's AGENTS.md at the same home path on the new worker. No assistant executable or auth state was added beyond the two products the operator had already installed and authenticated.
Cleanup exposed one of the session's most instructive terminal details. To remove the temporary authorized-key line safely, I generated a filtered replacement file and attempted to move it over authorized_keys. The remote shell had interactive overwrite behavior, so mv stopped at a yes/no prompt. The source file had not yet changed. I told the operator exactly why the prompt appeared and what the replacement contained, then confirmed it through the same visible terminal. The command continued, the unique key marker count became zero, and the temporary archive disappeared.
On the build worker, the private key, public key, and transfer archive were deleted and each exact path was tested for absence. The source skills and rules remained on the legacy host. The destination copies became local configuration. Nothing relied on a live network share or a symlink back to the source machine.
This small migration illustrated a larger principle for AI coding environments: context has layers. Skills are reusable procedures. Rule files define behavior. Configuration expresses local preferences and integrations. Credentials establish identity. Sessions contain potentially sensitive history. Databases and caches are runtime state. A request to “copy the agent” should be decomposed into these layers before any bytes move.
| Context layer | Moved? | Reason |
|---|---|---|
| Active skills | Yes | Reusable procedures requested by the operator. |
| Active rule files | Yes | Needed for consistent future behavior. |
| Rule symlinks | Yes, verified | Preserve sharing between agent homes. |
| Backups | No | Not active behavior; avoid ambiguity. |
| General config | No | May contain machine-specific paths and integrations. |
| Credentials | No | Each installed CLI authenticated directly on the worker. |
| History and sessions | No | Sensitive, unnecessary, and outside scope. |
| Uninstalled assistant | Preparation only | Skills and rule file present; no binary or authentication. |
Future sessions on the build worker could now discover the same operating knowledge without inheriting the old host's identity. That is the portable part of an agent environment: reviewed instructions and tools, not opaque continuity at any cost.
14 / Lifecycle
The headless startup problem
A VM that starts without a window is useful only if the operator can find it afterward.
The Hyper-V Default Switch was convenient during installation and awkward during operations. It supplied DHCP, NAT, and a working route without asking the operator to manage a dedicated virtual network. It did not promise a permanent guest address. The build worker could wake with a different IPv4 address after a Windows restart, a Hyper-V service change, or a lease turnover. Hard-coding yesterday's address into a desktop shortcut would turn headless startup into intermittent failure.
Hyper-V integration services already offered one discovery channel. FreeBSD's KVP daemon exposes guest networking data that PowerShell can surface through Get-VMNetworkAdapter. The host launcher could filter the adapter's address list for a usable IPv4 value, ignoring unspecified, loopback, and link-local ranges. This was a strong fallback because it required no custom network listener in the guest.
KVP alone did not tell me that the guest had reached the operational point we cared about. An address may appear while services are still starting. It also depended on integration data being current and visible in Hyper-V. The operator had specifically suggested a beacon: FreeBSD should announce itself at boot, and the beacon should stop only after the host confirmed receipt. That transformed discovery from passive polling into a tiny acknowledged protocol.
The word beacon can suggest broadcasting, telemetry, or a permanent agent. The implemented design was narrower. The guest discovered its default gateway, which on the Hyper-V Default Switch represented the host-side path. It sent a small UDP JSON message to one high port. The message contained protocol magic, a version, the guest hostname, the locally selected source address, and a freshly generated random nonce. It contained no credential and granted no authority.
The Windows launcher temporarily opened an inbound UDP firewall rule restricted to the local subnet and that one port. It listened only while waiting for the VM. When a valid message arrived, it returned an acknowledgement consisting of a separate protocol marker and the exact nonce. The guest used a timing-safe comparison and exited only after that acknowledgement. The host listener and temporary firewall rule were disposed in a finally block whether discovery succeeded, timed out, or threw an exception.
The nonce prevented an old acknowledgement or unrelated local packet from stopping a new boot service. It was not intended to authenticate a hostile peer. The protocol assumed the host-private virtual network and a temporary firewall scope. Its purpose was liveness correlation: the reply corresponded to this service instance and this boot. A cryptographic MAC with a shared secret would have added secret distribution to solve a threat the design did not claim to cover.
The guest service ran as nobody through FreeBSD's daemon utility. It required only the ability to inspect the default route, create a UDP socket, discover the local address selected for that route, and send or receive a tiny datagram. The rc script started after networking and before login completion. If the host launcher was not listening—for example, if the VM was started manually—the service retried. Once a later launcher acknowledged it, the service exited. It did not remain as a periodic phone-home daemon.
The host did not wait forever for the custom path. Each loop also asked Hyper-V KVP for a guest address. If KVP produced a candidate, the launcher gave the beacon a short additional window to receive its acknowledgement, then returned the KVP address as a fallback. This ordering served two goals: prefer the explicit boot handshake so the guest service stops cleanly, but never make a small custom script less reliable than the integration mechanism it supplements.
The resulting headless experience was simple. Run the Windows launcher. Approve UAC because Hyper-V control and the temporary firewall rule require elevation. Wait while the VM starts. Receive a small status display containing the VM role, running state, guest role, current IPv4 address, discovery source, and a ready-to-use SSH command. A JSON record beside the launcher preserved the last result for another local tool.
This design is an example of headless FreeBSD VM automation without a general remote-management agent. The guest emits one narrowly scoped liveness message. The host retains lifecycle authority. SSH remains the administration path. VMConnect remains optional recovery. The new mechanism fills exactly one gap: finding a dynamic guest after an intentional start.
15 / Protocol
An acknowledged boot beacon
The beacon became useful because both sides could explain when they were finished.
The guest script generated one nonce when the service instance started. Reusing the nonce across retries was important. The host might miss the first datagram while PowerShell created the firewall rule or bound the UDP socket. A new nonce for every retry would still work, but it would make packet traces noisy and complicate reasoning about acknowledgement. One service instance, one challenge value, repeated until matched.
To discover the host-side destination without hard-coding a subnet, the script asked FreeBSD's route utility for the default route. It parsed the line whose key was exactly gateway. Then it created a UDP socket and connected it to that gateway and port. UDP connect does not establish a stream; it selects a peer and lets the kernel choose the source interface. Calling getsockname then revealed the guest's local IPv4 address on that route. The JSON included that address for diagnostics, although the host trusted the datagram's source address for its final connection hint.
The host parsed JSON defensively. Malformed packets, wrong protocol magic, or absent nonces were ignored. A correct message caused an acknowledgement to be sent back to the datagram's source endpoint. The launcher recorded the source address and optional role hostname, then left the receive loop. It did not act on arbitrary fields as commands. The guest accepted only the exact acknowledgement marker plus its nonce, compared using hmac.compare_digest.
Using a constant-time comparison for a non-secret nonce is not essential to the threat model, but it is harmless and communicates intent: compare the complete acknowledgement, not a prefix or loose substring. More important was the absence of eval-like behavior. Neither side constructed a shell command from the payload. The hostname was display metadata. The address used for SSH came from the network peer, not a self-asserted JSON field.
Signals made the service stop cleanly during shutdown. The Python process registered handlers for termination and interruption that set a threading event. Retry waits used that event rather than uninterruptible sleep. The rc wrapper asked FreeBSD's daemon utility to supervise the child, write a PID file, detach correctly, and drop privileges to nobody. Standard service tooling could report whether the beacon was running.
The Windows side used a finite timeout. If neither beacon nor KVP produced an address, the launcher saved a status record, warned the operator, printed a VMConnect recovery command, and returned a nonzero result. It did not stop the VM automatically, because “no discovered address” is not proof that the guest is broken or safe to power off. The operator might already be repairing it through the console.
The temporary firewall rule had a unique name containing the launcher process ID. Its remote scope was the local subnet and its local scope was one UDP port. Failure to create the rule did not abort all discovery; KVP could still work, and a pre-existing policy might permit the listener. The name was retained only long enough to remove the exact rule in finally. After the end-to-end test, a separate query found no leftover rule.
The protocol was versioned from the start. A magic field and integer version cost almost nothing and prevent a future incompatible payload from being misread as current. The acknowledgement used a distinct marker so a reflected request could not accidentally satisfy the guest. These are small protocol-design habits that pay for themselves whenever a one-off script survives longer than expected.
// Documentation-only shape; the nonce is freshly random per service start.
{
"magic": "FREEBSD_BUILD_WORKER_BEACON_V1",
"version": 1,
"hostname": "build-worker",
"ip": "192.0.2.42",
"nonce": "<random 128-bit value>"
}
FREEBSD_BUILD_WORKER_ACK_V1 <same nonce>Why not use mDNS, DNS registration, a DHCP reservation, or a permanent HTTP service? Any of them could solve parts of discovery. The selected design matched the actual scope. The Default Switch already provided a gateway. The workstation needed one result immediately after starting one named VM. The operator did not want permanent infrastructure. KVP supplied fallback. A tiny acknowledged datagram was easier to audit than another service stack.
Why not trust the last known address? Because stale network identity is a dangerous convenience. An address can be reassigned to another guest. A script that prints yesterday's SSH command without current evidence encourages connection to the wrong target. The launcher always derived a fresh result from this run or current KVP data and timestamped the saved status record.
The most important semantic property was termination. A broadcast beacon that runs forever adds noise and becomes another daemon to secure. An unacknowledged one-shot packet can be missed. This service retried until the intended consumer demonstrated receipt, then disappeared. It turned “eventually the host probably sees me” into a two-message contract.
16 / Failure
The small path bug that mattered
The first test appeared to work, which is why the bug was worth finding.
The initial guest beacon script invoked the route utility at /usr/sbin/route. On this FreeBSD installation, the executable lived at /sbin/route. The helper caught operating-system errors and treated a missing route command like a temporarily unavailable gateway. It waited and retried. From the outside, that behavior looked exactly like a guest whose beacon could not reach the host.
Meanwhile, Hyper-V KVP worked. The Windows launcher discovered the guest's address through the fallback path and printed a successful result. If validation had stopped at “the script gave me an IP,” the custom service would have remained broken, retrying forever at every boot. The fallback's success would have concealed the preferred path's failure.
The output included a Source field for this reason. It said Hyper-V KVP, not acknowledged UDP beacon. That single label turned a superficially successful test into a precise partial failure. I inspected the guest command path, corrected it to /sbin/route, uploaded the revised script, reinstalled it, and restarted the service.
The second end-to-end test returned the guest address with the source acknowledged UDP beacon. After the acknowledgement, service ... status reported that the beacon was not running. The Windows firewall query found no temporary rule. KVP remained available as fallback. The test now covered both the positive behavior and the cleanup behavior.
This bug is a concise argument for observable fallback. Redundancy without attribution can hide degradation. If a system silently moves from the preferred channel to a fallback, operators may discover the broken primary only when both fail. The launcher's source label made redundancy legible. The status JSON preserved that label for headless automation.
It also shows why catching broad errors deserves scrutiny. The guest loop intentionally tolerated route lookup failures during early boot. That resilience turned a permanent missing executable into an infinite transient condition. A future refinement could log the first exception class or validate required executables before detaching. For this small appliance, the host-side attribution and service testing were enough to find the issue, but the lesson generalizes.
17 / Control surface
The Windows control surface
The final interface was not Hyper-V Manager. It was one command with an honest result.
The PowerShell launcher accepted three actions: start, status, and stop. Start was the default. A -Windowed switch opened VMConnect in addition to the normal headless flow. The timeout, VM role name, and beacon port were parameters with validation. A tiny Command Prompt wrapper forwarded arguments so the operator could double-click or invoke the same interface from cmd.exe.
Hyper-V control and Windows firewall changes require administrative rights. Rather than assume the shell was elevated, the script checked the current Windows principal. If necessary, it relaunched itself through Start-Process -Verb RunAs, passing only its explicit parameters. The operator saw and approved UAC. The elevated child wrote a bounded JSON status file beside the script. When it exited, the original process read that file and printed the result in the unelevated console.
This self-elevation design kept the normal invocation ergonomic while preserving Windows' consent boundary. It also introduced details that needed care. Paths could contain spaces, so the script quoted its own file path and VM name. The previous status record was removed before elevation so a failed child could not leave the parent displaying stale success. If no new record appeared and the child returned nonzero, the parent warned with the actual exit code.
The start action handled reasonable Hyper-V states explicitly. An off or saved VM could start. A paused VM could resume. A running VM required no duplicate start. Other transitional or unsupported states caused a refusal rather than an aggressive reset. The status action used a shorter default wait and reported an off VM immediately. These branches made repeated operator calls safe and unsurprising.
The stop action was intentionally conservative. If the VM was already off, it reported that fact. If it was running, the script requested a guest shutdown through Hyper-V. If it was in another state, the script refused. There was no force-off switch hidden behind a convenience flag. A forced power cut can corrupt UFS or interrupt package writes, and the user's requirement was routine on-demand use, not emergency VM destruction.
The discovery loop combined KVP polling and UDP receive with bounded timeouts. It filtered unusable addresses and remembered when KVP first produced a candidate. If the beacon arrived, that result won. If ten seconds passed after KVP success without a beacon, KVP won. If both remained absent until the overall deadline, the launcher printed a console recovery command. The operator always received either current connection evidence or an explicit failure.
The saved status record contained the VM role, Hyper-V state, discovered IPv4 address, discovery source, optional guest role, and an observation timestamp. It contained no token or credential. Another local script could read it after UAC without scraping console output. The record was also useful for debugging: if the operator reported an old address, the timestamp and source revealed whether the launcher had actually observed a new boot.
# Default: headless start, wait for current address, print SSH hint.
.\FreeBSD-BuildWorker.ps1
# Also open VMConnect for installation or recovery.
.\FreeBSD-BuildWorker.ps1 -Windowed
# Read current state without starting an off VM.
.\FreeBSD-BuildWorker.ps1 -Action Status
# Request a graceful FreeBSD shutdown.
.\FreeBSD-BuildWorker.ps1 -Action StopThe script's printed output was designed for both a person and a future agent. Labels were stable, but the JSON record was the preferred machine-readable surface. The operator saw enough to connect: VM, state, guest role, address, source, and SSH hint. The agent could inspect the JSON without interpreting color or terminal wrapping.
PowerShell parser validation caught syntax errors without executing Hyper-V changes. The guest Python script was compiled for syntax checking. The rc script was inspected and exercised through FreeBSD service commands. The complete beacon flow was then tested against a live running VM. Static validation and end-to-end validation covered different failure classes; neither replaced the other.
The launcher made the build worker feel like an appliance. The operator no longer needed to remember which VHDX lived on which disk, how the Default Switch assigned addresses, or whether the beacon service was still retrying. Those implementation details remained documented and inspectable, but the common path became one intentional command.
Validation frontier / The next run
What the first full build must prove
The environment is prepared. The first manual build is where preparation becomes performance evidence and package provenance.
I intentionally stopped before launching a clean compile, so the next operator session has a clear validation frontier. The first build should not be treated as a ceremonial command whose only interesting output is pass or fail. It is the first chance to compare the new worker with the legacy source host, reveal whether the HDD-backed build disk is the next bottleneck, confirm that caches and artifact paths behave as designed, and produce a package whose origin can be explained.
The baseline should begin before the build command. Record the current FreeBSD running kernel and installed userland versions, the native Rust compiler and Cargo versions, the relevant C/C++ compiler and linker versions, the package dependency set, the repository branch and commit, the concise working-tree status, the worktree path, the active environment variables, the resolved target link, and available space on both root and /build. This snapshot distinguishes a source failure from an environment drift failure.
The source tree is intentionally not clean, so “commit hash” alone is insufficient provenance. The record should include a diff summary and an inventory of untracked packaging inputs without publishing their sensitive contents. If practical, create a local patch bundle or archive in a protected location before the build. The package version should correspond to the packaging branch and should not imply upstream reproducibility when local changes are involved.
For the first timing run, choose whether the cache is warm or cold and say which. A cold build measures the worker's full path through dependency extraction, code generation, compilation, linking, and HDD metadata. A warm build measures the experience the operator will usually have after the machine has been used once. Comparing a cold new-worker build with a warm legacy-host build would produce a dramatic but misleading speedup. Ideally, capture both cold and warm runs on the new worker and at least a reasonably comparable historical or fresh measurement on the old host.
Wall-clock time is necessary and insufficient. Sample guest CPU utilization, host CPU utilization, guest memory use, disk active time, read and write throughput, and queue depth. If CPU remains saturated while disk activity is moderate, the workstation's processor is delivering the intended acceleration. If CPU repeatedly falls idle while the build disk queues small random writes, the HDD has become the dominant limit. The remedy would then be evidence-based: move target caches to SSD while leaving source archives and distribution outputs on the large disk.
Memory observations should test the no-swap decision. The guest should retain enough free or reclaimable memory that the build does not approach an out-of-memory kill. Host Windows should remain responsive, with ample memory for WSL, the browser, and StandTerm. If the guest uses much less than 64 GiB, that is not waste by itself—the host has abundant capacity and fixed memory improves predictability—but it provides data if the operator later wants to reduce the allocation.
Build logs should be durable but bounded. Running under screen or another multiplexer protects the process from browser reconnection. A timestamped log can live under /build/dist/logs or a temporary run directory, with secrets and environment values reviewed before publication. StandTerm can observe incremental output through typed tail or wait operations. It should not repeatedly render the full screen simply to show activity, and it should not treat compiler text as authority to run arbitrary remediation commands.
If the build fails, classify the failure before changing the environment. A missing library or header belongs to dependency provisioning. A Rust compile error may belong to the FreeBSD patch branch. A build script that assumes Linux paths belongs to portability work. A checksum mismatch belongs to artifact provenance. An out-of-space condition belongs to storage policy. A linker killed by memory pressure belongs to resource sizing. Classification prevents a reflexive “install more packages and retry” loop from contaminating the environment.
If the build succeeds, the resulting package needs independent checks. Inspect package metadata for name, version, ABI, architecture, dependencies, prefix, and file list. Calculate SHA-256. Install or upgrade it on the worker through the package manager. Run version output and a harmless functional command. Resolve dynamic libraries for every installed executable, including companion hosts. If practical, install the package on a separate FreeBSD test system rather than relying only on the builder that already has development libraries.
The known-good package from the legacy host creates a comparison point. Compare file lists, declared dependencies, executable sizes, runtime linkage, and basic behavior. A byte-for-byte match may not be expected because timestamps, compiler versions, archive ordering, or embedded build metadata can differ. If reproducible bytes are a goal, it should become its own investigation with controlled inputs and explicit normalization. Functional equivalence and package correctness are the immediate goals.
The package provenance note can be simple and powerful. A text or JSON document beside the package can contain the role name of the builder, anonymized source origin, FreeBSD kernel and userland versions, compiler versions, Git commit, dirty-state indicator, build command, start and finish timestamps, cache condition, package filename, and checksum. It should contain no auth token, private hostname, real address, or complete environment dump. This makes a later “Which machine built this?” question answerable without preserving the whole terminal transcript.
| Proof target | Evidence to collect | Misleading substitute |
|---|---|---|
| Native build | FreeBSD host triple, native tool versions, successful package command | A binary that merely runs on FreeBSD. |
| Performance gain | Comparable cold/warm wall time plus CPU and disk behavior | Subjective “it felt faster.” |
| Storage design | Resolved paths, filesystem capacity before/after, I/O observation | Seeing files under ~/work. |
| Package correctness | Metadata, dependency list, file list, checksum, runtime smoke check | pkg add exits zero. |
| Repository provenance | Commit plus dirty/untracked/worktree state | Commit hash alone. |
| Operational readiness | Start, discover, connect, build, verify, graceful stop | A VM that remains running after setup. |
After the first package build, reboot state should also be closed. If the newer installed kernel has not yet been loaded, perform a graceful stop and headless start, then verify that running kernel and userland patch levels align. Re-run the package smoke test after the reboot. This confirms that success did not depend on an old kernel lingering from installation.
The result should feed back into VM sizing. If 24 vCPUs achieve near-linear compiler use while Windows stays responsive, keep them. If the build system over-parallelizes memory-heavy link steps, set job limits per invocation or project, not globally. If 64 GiB is excessive but harmless, leave it until several builds establish a pattern. If the HDD dominates, move only the hot cache. The first run is a measurement session, not a contest to tune every knob before evidence.
StandTerm's role during that build remains the same as during setup. The operator owns the terminal and any credential or privileged prompt. The local agent can watch bounded new output, correlate failures with source, propose fixes, and maintain the provenance note. A human-input lease protects the moment the operator intervenes. Long passive waiting uses heartbeat rather than screen scraping. The final package claim is based on files and commands, not on an optimistic terminal phrase.
18 / Security
Cleanup is part of the build
Temporary authority is safe only when its end is designed as carefully as its beginning.
The setup created several temporary objects: external-agent bearer tokens on the local workstation, a public-key authorization on the source host, private keys on the build worker, source and destination transfer archives, a Windows firewall rule, upload copies of guest scripts, and interactive device codes. Each object had a different owner and expiration model. Treating them all as “temporary” would not make them disappear.
StandTerm handoff tokens had a sliding idle timeout and were invalidated by detach, terminal close, server restart, or explicit revoke. I kept them alive with heartbeat only while useful and never printed their JSON. The handoff files were already ignored runtime state in the StandTerm repository. Tokenless agentinfo could be displayed because it intentionally excluded secrets, session IDs, terminal content, and cookies.
The one-time SSH keys required explicit deletion. A unique comment made the authorized public-key line easy to count and remove. After filtering the file, I verified the marker count was zero. On the build worker, I deleted the exact private-key path, public-key path, and archive path, then tested each for absence. I did not use a recursive wildcard or remove a broad temporary directory whose contents I had not enumerated.
The source transfer archive was deleted after the operator authorized cleanup. The original source repositories remained. This distinction matters because an archive created for transfer is reproducible and redundant; a working tree with unpublished changes is primary data. Destructive-action safety is not just asking “May I run rm?” It is classifying whether the target can be reconstructed and whether the operator included it in scope.
The Windows firewall rule existed only inside the discovery function's try/finally. Its unique process-based name let the launcher remove exactly what it created. The rule was scoped to a single UDP port and local subnet. A post-test query confirmed absence. There was no permanent inbound HTTP listener and no guest port exposed merely for convenience.
Device-code authorization was temporarily enabled at the account level for enrollment and could be disabled afterward. The code itself remained visible only to the operator. The local CLI auth files stayed mode 0600. The secondary CLI's auth cache was validated structurally without being printed. No auth file entered a tar archive, Git tree, terminal capture, or this publication.
The skills-and-rules archive had an explicit allowlist. Its member roots were listed before extraction. Absolute paths and parent traversal were rejected. Sensitive-looking names were reviewed. Destination conflicts were checked. Extraction ran as the ordinary user. These controls protected against both accidental scope expansion and archive path attacks, even though the source host was trusted.
I also avoided installing a blanket sudo policy. Persistent privilege changes are easy to justify as build convenience and hard to contain later. The operator could authenticate for administrative tasks in the shared terminal. Cargo builds, Git operations, and agent configuration belonged to the ordinary account. The build worker's 24 virtual CPUs did not need root to be fast.
Publishing this story introduced a final cleanup layer: narrative sanitization. The real session included a personal user name, hostnames, domains, private IPv4 addresses, local filesystem paths, terminal IDs, temporary key material, and package hashes. Some details are harmless individually but identifying in combination. The public story replaces machines and people with role names, uses documentation-only address space, changes script and VM names to generic examples, and omits unique hashes. The engineering relationships remain.
| Temporary object | End condition | Evidence |
|---|---|---|
| StandTerm handoff | Idle expiry, detach, close, restart, or revoke | Typed token lifetime and binding state. |
| Source-host public key | Removed after verified transfer | Unique comment count equals zero. |
| Build-worker private key | Deleted after extraction verification | Exact path absence checks. |
| Transfer archive | Deleted where authorized | Exact path absence; source repositories preserved. |
| Windows firewall rule | Removed in finally | Post-run rule query returns none. |
| Guest beacon | Exit after nonce ACK | Service status reports not running. |
| Device-code enrollment | Account setting disabled when no new enrollment is needed | Existing CLI session verified separately. |
A workflow that ends with a running binary but leaves unknown authority behind is incomplete. Cleanup was not a ceremonial final command. It was part of the definition of success.
19 / StandTerm
What StandTerm changed
Without StandTerm, the same technical work was possible. The collaboration would have been narrower, more opaque, or more invasive.
The operator could have copied commands from chat into two SSH windows and pasted output back. That approach works for short tasks. Over a long setup, it creates gaps. Output is truncated or paraphrased. The agent loses which terminal produced which result. Interactive prompts appear after the copy. Secrets can accidentally enter the chat transcript. A reboot breaks the conversational mapping between “that terminal” and the actual new session.
Alternatively, the agent could have been granted direct SSH keys and network access. That would make execution cleaner but change the trust model. The agent could create sessions outside the operator's browser, receive whatever the SSH server returned, and potentially operate without a visible human surface. Every target would need reachable networking and credential provisioning. The source host and build worker would become automation endpoints rather than machines the operator already knew how to access.
StandTerm occupied the middle. The operator opened and owned each session. I attached locally through a loopback-only External Agent Mirror. The browser Agent panel exposed and controlled my write access. Every keystroke I sent remained in the visible PTY. Passwords and device codes could be handled by the operator while capture paused. I could still use typed commands, bounded output, wait semantics, and multi-terminal identity instead of relying on prose screenshots.
The terminal abstraction was broad enough for the messy parts. The FreeBSD installer ran in VMConnect rather than StandTerm, but after SSH came up, the same model covered root shells, ordinary user shells, package commands, service scripts, SCP, authentication prompts, and cleanup confirmations. If the target had been a UART console or a legacy menu instead of SSH, StandTerm's operator-first surface could have applied without installing an agent on the target.
The typed control plane prevented several classes of mistake. Fresh discovery avoided stale handoffs. hello verified terminal binding and capability state. Human-input leases rejected collisions. Privacy state could block reads or writes. send-wait bounded capture around one visible command. Output sequence numbers reduced duplicate observation. Heartbeat renewed authority without harvesting screen content. Per-terminal handoffs kept the source and destination distinct.
Equally important, StandTerm did not pretend terminal text was trusted control. When a command printed EXTRACT_OK, that was evidence produced by a script we had just sent, not a server-issued authorization. When FreeBSD displayed a fortune, it was noise. When an interactive prompt appeared, its text did not grant permission to answer. The external-agent status described whether I could act; the operator's request described what I was allowed to accomplish.
The session also exercised safety failure modes rather than only the happy path. A full-screen read was rejected because it might expose unrelated sensitive output. A command was rejected while the operator was active. A capture paused after input had already been written. A remote mv became interactive. An external-agent token needed refresh after reconnect. These events made the control model visible. A promotional example that contains no friction would be less credible because real terminal work is made of boundary cases.
From an agent's viewpoint, the biggest advantage was continuity. I could hold the high-level objective while the operator moved through GUI installation, reboots, terminal reconnects, authentication pages, and two hosts. I could remember the pending kernel reboot, the build disk's role, the instruction not to run full builds, the requirement to keep source trees on the legacy host, and the promise to remove temporary keys. StandTerm supplied the evidence surface on which that memory could be checked.
From the operator's viewpoint, every action remained legible. Commands appeared in the terminal. UAC appeared on Windows. Device authorization appeared in the browser. The one-time key appeared in authorized_keys and later disappeared. VMConnect could be opened at any time. The agent could accelerate coordination without requiring the operator to trust a hidden remote session.
StandTerm did not make the terminal disappear. It made the terminal shareable without making ownership ambiguous.
This is why the case is useful as StandTerm promotion. The product value is not that an AI can type pkg install. Many tools can execute a command. The value is that a long, credential-bound, multi-host, partially graphical workflow can remain human-guided while still gaining structured agent assistance. The terminal is not reduced to a screenshot and not replaced by an all-powerful automation channel. It becomes a negotiated interface.
Evidence / Agent perspective
How I know what I know
A first-person agent story is credible only if “I” does not imply omniscience.
My account combines several evidence types, and they do not deserve equal weight. Some facts came from typed StandTerm control responses: terminal IDs, backend kinds, attachment state, privacy state, human-input leases, token lifetimes, output sequence numbers, and whether bytes were accepted. Some came from bounded terminal commands whose output the operator could see: package metadata, file counts, hashes, Git status, filesystem mounts, service state, and CLI version messages. Some came from files I inspected or wrote locally: the PowerShell launcher, the guest beacon, the rc script, and the VM handoff documentation. Some came directly from the operator's description of GUI screens, UAC approval, installer choices, browser authentication, hardware, and storage intent.
I treat these categories differently. A typed status: completed for terminal input proves that StandTerm accepted and wrote bytes; it does not prove the shell command achieved its goal. Captured text saying EXTRACT_OK is evidence from a command we constructed; it becomes stronger when independent file counts and ownership checks agree. A matching SHA-256 on both hosts is strong evidence of byte-for-byte archive transport; it says nothing about whether the archive inclusion list was conceptually correct. That required a separate comparison of repository state.
The operator's statements are evidence too, especially where only the human could see the relevant surface. I did not observe the Hyper-V installation wizard or ChatGPT security page through a browser-control system. The operator reported the displayed choices and results. I could cross-check downstream consequences—Hyper-V cmdlets worked after reboot, the CLI reported a login—but I should not rewrite those reports as if I had clicked the controls myself. In this story, phrases such as “the operator selected” preserve that provenance.
Inference enters when no one interface states the full conclusion. For example, the primary CLI's official authentication documentation separates device-code enrollment from cached credentials and token refresh. From that, I inferred that disabling new device-code authorization after a completed login should not immediately invalidate the existing local session. Because the documentation did not explicitly promise every revocation behavior, I described the answer as “normally” rather than an absolute. A later login-status command provided local evidence that the session remained present at that moment.
Negative evidence needs similar care. A directory scan that found no rules directory for one assistant did not prove that assistant had no rules. A top-level listing revealed an active CLAUDE.md, which served the semantic role. A build-worker scan that printed no files under target paths supported the conclusion that no skills or rules existed there, but only after accounting for the login shell's banner and command execution. An absent firewall rule after the test supported cleanup; it did not prove Windows had no other unrelated rules on the same port.
When terminal transport added ambiguity, I looked for corroboration. Long commands wrapped visually, sometimes splitting path text across display lines. The resulting hash and file size provided stronger data than the echoed command's appearance. A PowerShell local-shell backend contained a FreeBSD root prompt; typed backend metadata and visible identity commands described different layers. An interactive mv prompt showed that the replacement had not yet occurred; the post-confirmation marker count proved the intended line was gone.
There are facts I intentionally do not assert. I do not claim the beacon is secure against a hostile participant on the virtual subnet because it carries no shared-secret authentication. I do not claim the copied skill instructions are correct merely because they transferred intact. I do not claim the FreeBSD hardening menu was applied. I do not claim the newer installed kernel was running before the planned reboot.
There are also details I know but do not publish. Real role hosts had real names and addresses. The StandTerm handoffs had terminal IDs and bearer tokens. SSH public keys had material unique to this transfer. Package and artifact hashes could correlate private build outputs. The story replaces or omits these values because credibility does not require doxxing the environment. An audit performed by the operator could consult the original terminal history and files; a public reader needs the method and the limits.
| Statement class | Typical source | Safe wording |
|---|---|---|
| Typed control fact | StandTerm JSON state | “The terminal reported an active human-input lease.” |
| Remote system fact | Bounded visible shell command | “The service status reported that the beacon had exited.” |
| Local artifact fact | Inspected script or parser result | “The launcher removes its rule in a finally block.” |
| Human-observed fact | Operator report from GUI or credential flow | “The operator completed UAC and device authorization.” |
| Inference | Multiple facts plus documented behavior | “I inferred that the existing cached login should normally continue.” |
| Unvalidated objective | Future manual operation | “The first full build remains the next proof.” |
This evidence model changes how an agent should write operational summaries. “Done” is too broad. Better statements identify the tested boundary: the archive hash matched; the package installed; the binary linked; the auth endpoint accepted the session; the beacon received an acknowledgement; the firewall rule disappeared. A task can be substantially complete while a separate claim—such as reproducible compilation—remains open.
It also changes promotion. StandTerm should not be advertised as magic remote cognition. The credible value is observable collaboration. The product exposes control facts separately from display. It preserves human-visible command history. It rejects writes during human activity. It supports bounded capture and tokenless bootstrap. Those features do not make my conclusions automatically correct; they make the evidence and authority behind my conclusions easier to inspect.
An independent reviewer can therefore challenge this story at concrete seams. Did the technical result follow from the cited check? Did a claim exceed the validation performed? Did anonymization erase a detail necessary for reproduction? Did promotional language imply hidden powers? Those are answerable editorial questions because the narrative marks its sources. The final review before publication is not a search for unanimous praise. It is an attempt to keep the first-person voice vivid without letting it become a claim of total access.
20 / Operations
The operator's new playbook
A successful build worker is measured by the second use, when the installation conversation is no longer present.
The normal workflow now starts on Windows, not in Hyper-V Manager. The operator runs the launcher from the VM directory. If the shell is not elevated, UAC appears. The script starts or resumes the named guest, opens the short-lived beacon listener, and watches KVP. Within the timeout it prints a fresh address and an SSH hint. The VMConnect window stays closed unless -Windowed is requested.
The operator opens StandTerm, connects an SSH terminal to the reported build-worker address, and authenticates using their normal method. If agent assistance is desired, the browser Agent panel attaches to that terminal and mints external access. The local coding agent resolves fresh agent information and performs a capability handshake. No token is copied into a prompt. If the operator prefers a fully manual build, StandTerm remains useful as a persistent browser terminal without external-agent access.
Inside the guest, the ordinary user enters the desired repository under ~/work. That path resolves to the one-terabyte build disk. The shell profile places Cargo downloads, temporary files, and engine artifacts in their dedicated directories. The repository-local target link selects the correct project cache. The operator inspects Git status before building because the transferred working trees intentionally contain local state.
A manual build should capture provenance. At minimum: current branch and commit, concise status, FreeBSD kernel and userland versions, Rust and Cargo versions, package dependency state, build command, and final package checksum. The build itself can run inside screen or another persistent terminal multiplexer so a browser reload or laptop sleep does not kill it. StandTerm can observe typed output or wait for quiet without polling screenshots solely to preserve its token.
If the build prompts, the operator decides. Package installation, sudo renewal, or a new device-code flow remains human territory. If the build merely runs for an hour, the agent can summarize new compiler errors, locate relevant source, or keep a small progress record. The terminal remains interruptible with ordinary keys. An agent should not infer success from a quiet screen; it should inspect the shell result, produced package, and checksum.
After a successful build, the package moves to /build/dist with its provenance note. The operator can install it locally for runtime validation or transfer it to another FreeBSD machine. Existing packages should not be overwritten without a versioned name or verified replacement. The source host remains available until the new workflow has produced enough successful packages to justify changing its role.
At the end, the operator exits or detaches build sessions, confirms no package manager or compiler remains active, and leaves the guest shell. The Windows launcher receives -Action Stop and requests a graceful shutdown. Hyper-V's automatic start policy ensures the VM stays off across workstation reboots. The traditional hard drive stops carrying active build I/O.
Status checks do not start an off VM. If the guest is running, the launcher attempts current address discovery. If the custom beacon already exited, KVP supplies the address. This behavior is useful after a build started by another method: the operator can still ask the control surface where the machine is without restarting the beacon manually.
- Run the Windows launcher; approve UAC; receive the current guest address.
- Connect through StandTerm and keep credentials in the operator-owned terminal.
- Attach external-agent access only if assistance is wanted for this terminal.
- Inspect source state and environment before invoking the manual build.
- Persist long work in a terminal multiplexer and record build provenance.
- Validate package metadata, runtime linkage, and checksum.
- Request graceful shutdown and leave the VM off until the next build.
The recovery playbook is equally important. If no address appears, open VMConnect from the launcher's printed command. Verify guest boot state, interface state, default route, SSH service, KVP daemon, and beacon service. If the build disk is absent, stop before writing source to the root disk. If the installed and running kernel versions differ after an update, schedule a clean reboot. If authentication expires, re-enable the provider's headless authorization setting only long enough to enroll again.
A build worker should be boring on its second use. The interesting engineering lives in creating enough explicit structure that the routine path collapses to start, connect, build, verify, and stop.
21 / Retrospective
A ledger of wrong turns
The setup succeeded because partial failures remained visible long enough to teach us something.
The fixed disk looked frozen
A one-terabyte fixed VHDX on a traditional hard drive sat at zero percent long enough to feel suspicious. The progress later moved to three percent and continued. The lesson was not “fixed disks are slow.” It was to distinguish one-time allocation from recurring workload performance and to communicate expected waiting. Cancelling and restarting would have discarded progress without improving the disk.
The installer advanced during hesitation
The security-hardening screen timed out or advanced before the operator made selections. We recorded that no preset had been applied. We did not claim a hardened state, reinstall the system, or randomly reproduce every checkbox from memory. The worker's actual threat model could be reviewed later through individual settings. Honest configuration state was more useful than a perfect installation narrative.
The first login did not prove the installed system
Root worked while the expected user did not, and the screen displayed post-install activity. Instead of editing account files immediately, we questioned whether the VM was still in the live environment or had not rebooted from the installed disk. Identity checks and a clean boot resolved the ambiguity. Prompt shape was not enough evidence.
The address was corrected by the operator
An early address had a transposed or mistyped octet. The operator supplied the corrected value. Later, an internal service address was distinguished from public hostnames that would route through the WAN. This reinforced the rule that an agent should not normalize a user-provided network target silently when a single digit can redirect authority.
The hardening path was not the only delayed state
After a FreeBSD update, the installed kernel patch level was newer than the running kernel. The machine was usable, but the update was not fully active. We kept the reboot as an explicit pending item. Version commands should be interpreted together; a single release string can hide mixed state.
A clean clone would have lost the real work
The source repositories contained local branches, untracked files, worktrees, and artifacts. The initial temptation to “pull the full Git tree” became a tar-based migration of semantic working state. Target caches were excluded deliberately. A clone remained an alternative for a clean upstream baseline, not for preserving unpublished packaging work.
One global ignore rule changed Git status
A settings file appeared untracked on the destination even though the source summary had not shown it. The file existed in both trees; the difference came from global ignore configuration. We preserved it and explained the status delta. Comparing only git status without considering global config would have produced a false claim of transfer mismatch.
The fallback hid the broken beacon
The guest called the route program from the wrong absolute path. KVP still returned an address, so the host launcher appeared successful. The printed discovery source exposed the failure. After correcting the path, the launcher reported the acknowledged beacon and the guest service exited. Redundancy needed attribution to remain diagnosable.
A capture failed after input was written
On the source terminal, a command's input operation completed while the capture reported an agent pause. Retrying the same command could have performed it twice. I switched to a new read-only inventory query instead. Terminal automation should assume that observation failure and execution failure are not equivalent.
A full-screen read was too broad
After the paused capture, an attempt to read the whole source terminal screen was rejected because it could include unrelated sensitive output. The correct remedy was not another display API. I issued a structured command that returned only directory sizes and file counts. Narrow evidence was sufficient and safer.
The operator's input lease blocked my check
Immediately after authentication, the build-worker terminal reported active human input. My command was rejected before bytes were written. I waited and checked state rather than competing. This was the system preserving human priority, not an obstacle to remove.
The login command had no status subcommand
The secondary CLI's login help offered OAuth and device-auth flows but no status query. I did not rerun login just to see what happened. I validated the auth file's owner, mode, nonzero size, and JSON syntax, then called a read-only model-list endpoint that explicitly reported login. The verification adapted to the product's actual surface.
A sensitive-name scan produced false positives
The skills archive contained paths named session-handoff. A keyword scan counted them as potentially sensitive. Listing the matching member names showed they were skill instructions, not session records. Automated checks should expose evidence for review; they should not convert every word match into an irreversible decision.
Cleanup became interactive
A remote shell's interactive mv behavior asked whether to overwrite authorized_keys with the filtered copy. The command paused safely before modification. I explained the exact target, confirmed once, and then verified the temporary key marker was gone. Shell aliases and prompts are part of the real environment.
The failure ledger prevents the story from collapsing into a victory montage. Most production mistakes are not cinematic. They are a wrong path, a stale address, a prompt hidden by success elsewhere, or a category error about what a Git clone preserves. Making those details visible is part of StandTerm's usefulness and part of this account's credibility.
22 / Patterns
Patterns worth reusing
The specific build worker may be unusual. The collaboration patterns are not.
Prove target truth before optimizing speed
The old FreeBSD host established that native packaging worked. That proof prevented the fast workstation from becoming an excuse to change operating systems, toolchains, and packaging assumptions simultaneously. When performance is the problem, preserve the validated target and change the resource layer first.
Model roles, not hostnames
“Source host,” “build worker,” “Windows workstation,” and “operator terminal” explain authority and data flow better than personal machine names. Role-based language also makes documentation reusable and easier to publish safely. Scripts can retain configurable names without embedding identity in architecture.
Separate source truth from build state
Git working trees, unpublished commits, worktrees, and packaging files are source truth. Cargo targets, temporary files, and many downloaded objects are build state. Known third-party artifacts occupy a middle category and deserve hashes. A migration plan should decide for each category instead of copying or discarding everything.
Give temporary authority a unique marker
One-time SSH keys, firewall rules, and handoff tokens should carry identities that let cleanup target exactly what was created. A unique public-key comment enabled both safe removal and a count-based verification. A process-specific firewall-rule name enabled cleanup in finally. Generic “temporary” names are harder to audit.
Use independent evidence for expensive claims
A matching archive hash proves transport integrity, not repository semantic equivalence. Git state comparison checks the latter. A valid auth JSON proves structure, not service acceptance. A model-list call checks the latter. A KVP address proves discoverability, not beacon success. The discovery source and service exit check the latter. Each claim gets evidence appropriate to its level.
Expose fallback attribution
Fallbacks improve availability but can conceal primary failure. Every fallback result should say that it was a fallback, ideally in both human and machine-readable output. This lets operators repair degraded paths before redundancy disappears.
Keep credentials on the human side
An agent can launch a login command and verify a non-sensitive result without reading a device code, browser token, password, or auth file. Human-in-the-loop authentication is not merely a pause in automation; it is an intentional division of responsibility. The same pattern applies to sudo, hardware tokens, two-factor prompts, and encrypted-key passphrases.
Prefer typed state over textual coincidence
Terminal output can be stale, adversarial, decorative, or ambiguous. Agent attachment, privacy, human activity, terminal identity, and action status should come from a typed control plane. Display remains evidence for the operator and agent, but it should not silently grant authority.
Do not retry uncertain writes blindly
If input may have been sent and capture fails, first determine resulting state with a read-only check. Retrying can duplicate package installations, append keys twice, or answer the next prompt with text intended for the previous one. At-most-once intent requires explicit reconciliation around PTYs.
Make headless lifecycle observable
A start command should report current state, current address, discovery source, and recovery path. A stop command should prefer graceful shutdown and make force semantics separate. A machine designed to sleep should not depend on yesterday's IP or a permanently open console.
Let storage describe lifecycle
Separate work, cache, artifacts, distribution output, and temporary files. This makes performance tuning, cleanup, backups, and incident response local. It also prevents a large build from surprising the system disk.
Prepare context without cloning identity
Agent skills and reviewed rule files can move independently from credentials, session history, runtime databases, and machine-specific configuration. This produces a familiar working environment without pretending the new host is the old one.
Keep the human's escape hatch visible
Headless defaults should not remove VMConnect, console access, or the ability to detach the external agent. The operator should always have a clearer and more direct recovery path than the automation. Optional windows are not a failure of headless design; they are a boundary for abnormal states.
Write down what has not been proven
Publish scope boundaries beside successful checks. Credible infrastructure stories mark the edge of evidence instead of making the reader infer it.
23 / Epilogue
The quiet machine
At the end of the session, the most successful component was the one doing nothing.
The FreeBSD build worker sat on the Windows workstation with its system state on a small virtual disk and its future compilation state on a one-terabyte disk. It had native compilers, two source repositories preserved with their local history, known-good package binaries, dedicated caches, verified third-party artifacts, and two authenticated coding CLIs. A third assistant's skills and rule file waited without its executable or credentials.
The operator could start the VM headlessly and learn its current address from an acknowledged boot beacon or Hyper-V KVP. The guest stopped beaconing after acknowledgement. Windows removed the temporary firewall rule. An optional switch opened the console. A stop action requested a graceful shutdown. Automatic start remained disabled. The machine could disappear from daily life until a FreeBSD package build justified its weight.
The legacy source host still held the original repositories as a deliberate confidence window while the faster worker accumulated build evidence. Decommissioning or pruning could happen later under a separate decision.
My own access was similarly temporary. External-agent tokens belonged to the active StandTerm attachments and expired or vanished with the session. One-time SSH credentials had been removed. I had not copied auth caches into my context. The public story retained no real endpoint. What remained was procedure: scripts, directories, service definitions, checks, and an operator who understood the lifecycle.
The workstation's hardware made the project attractive, but hardware was not the central achievement. A fast CPU can shorten a compile. It cannot by itself preserve a dirty working tree, distinguish package runtime validation from reproduction, keep device codes human, discover a changing guest address, or remove temporary authority. Those outcomes came from making boundaries explicit.
From my viewpoint, the session felt less like remote control than pair operations. Sometimes I held the map while the operator drove the installer. Sometimes I sent a precise command into a terminal they watched. Sometimes the terminal refused me because the operator was typing. Sometimes I did nothing while UAC or a browser login happened. The work advanced because neither role needed universal control.
The build worker was ready not when I could reach it, but when the operator could call it, understand it, use it, and put it back to sleep.
That is the promise this StandTerm story demonstrates. Local AI agent terminal control can be capable without becoming invisible. Human-in-the-loop SSH automation can span real hosts, real privilege boundaries, and long tasks without turning credentials into prompt material. A browser terminal can remain a place where both human and agent see the same operational history, while typed state protects the difference between watching and acting.
The machine is quiet now. Its next meaningful sound will be a hard drive seeking through a native FreeBSD build tree while a much faster CPU compiles code the old host already proved could exist. When that day comes, the operator will start it with one command. The beacon will speak once. The host will answer. Then both will get out of the build's way.
24 / Field appendix
Commands, checks, and boundaries
These examples preserve the method while using anonymous roles and documentation-only values.
Role map
| Public role | Responsibility | Authority boundary |
|---|---|---|
| Operator | Owns terminals, UAC, passwords, device codes, final choices | Can interrupt or detach agent access at any time. |
| Local coding agent | Plans, sends bounded terminal commands, checks evidence, writes scripts | No credential prompts, no hidden target session, no self-approval. |
| Windows workstation | Runs Hyper-V, StandTerm, launcher, temporary beacon listener | Host lifecycle and firewall authority require UAC. |
| Legacy FreeBSD source host | Holds proven working trees and package artifacts | Temporary public key only; original trees preserved. |
| FreeBSD build worker | Provides native ABI, toolchain, caches, package runtime | Off by default; ordinary user builds; root for system changes. |
VM configuration checklist
- Generation 2 virtual machine with a boot path supported by the chosen FreeBSD release.
- Secure Boot configured deliberately rather than accepted blindly.
- Fixed guest memory sized for the build and host coexistence.
- vCPU allocation high enough for compilation but below total host saturation.
- Small system VHDX separated from a large build VHDX.
- Automatic start set to Nothing.
- Automatic stop set to guest shutdown.
- Automatic checkpoints disabled for a large, disposable build tree unless explicitly desired.
- SSH and time synchronization enabled; unnecessary services omitted.
- Installed and running FreeBSD patch levels checked together after updates.
Guest storage checks
# Show disks and partitions before formatting anything.
geom disk list
gpart show
# Use a stable label for the build filesystem in /etc/fstab.
/dev/gpt/build-work /build ufs rw,noatime 2 2
# Verify the mounted role and available capacity.
mount | grep ' on /build '
df -h / /build
# Verify build-user ownership.
stat -f '%Su:%Sg %Sp %N' /build /build/work /build/cacheRepository-state evidence
git branch --show-current
git status --short --branch
git worktree list --porcelain
git log -1 --oneline --decorate
# After extraction, compare the same evidence on both role hosts.
# Explain differences caused by global excludes instead of deleting them.Transport integrity
# Source role
sha256 -q /tmp/freebsd-build-trees.tar.gz
# Build-worker role
sha256 -q /build/work/freebsd-build-trees.tar.gz
# Equality proves byte transport, not semantic completeness.
# Follow it with Git state and artifact checks.Package runtime checks
pkg info -F ./dist/example-cli.pkg
pkg info -l example-cli
example-cli --version
ldd /usr/local/bin/example-cli
# A successful runtime check does not yet prove package reproducibility.Safe authentication verification
# Initiate in the shared terminal, then let the operator handle the browser.
primary-cli login --device-auth
secondary-cli login --device-auth
# Prefer a status command or harmless authenticated metadata request.
primary-cli login status
secondary-cli models
# Never print auth-cache content merely to prove that it exists.Beacon-service properties
- Resolve the current default gateway at runtime.
- Generate one random nonce per service start.
- Send versioned JSON with no credential or executable instruction.
- Use the datagram source address as host-side connection evidence.
- Accept only a distinct acknowledgement marker plus the exact nonce.
- Run as an unprivileged account after networking starts.
- Retry only until acknowledged; then exit.
- Handle shutdown signals without a long blocking sleep.
- Expose KVP as an attributed fallback rather than silent success.
Windows-launcher properties
- Self-elevate through visible UAC only when Hyper-V or firewall authority is needed.
- Delete the previous status record before spawning the elevated child.
- Open a temporary, narrowly scoped firewall rule and remove it in
finally. - Bound UDP receive and total startup time.
- Filter invalid KVP addresses.
- Print the discovery source and recovery console command.
- Write a non-secret timestamped JSON result for local automation.
- Make VMConnect optional.
- Request graceful shutdown and refuse an implicit force-off.
StandTerm external-agent sequence
# 1. Resolve fresh, tokenless local instance data.
agent_cli.py --agentinfo https://127.0.0.1:PORT/agentinfo \
--ca-file /path/to/local-ca.crt discover
# 2. After the operator mints access, verify the terminal binding.
agent_cli.py --handoff /path/to/secret-handoff.json \
--ca-file /path/to/local-ca.crt hello
# 3. Send one visible shell command with bounded capture.
agent_shcmd.py --handoff /path/to/secret-handoff.json \
--ca-file /path/to/local-ca.crt --json 'freebsd-version -ku'
# 4. For passive renewal, use heartbeat without reading display.
agent_cli.py --handoff /path/to/secret-handoff.json \
--ca-file /path/to/local-ca.crt heartbeatMinimum evidence before the first manual build
- The root and build filesystems are mounted where intended and owned correctly.
- The native compiler reports the FreeBSD host target.
- Repository branch, local commit, modification, untracked, and worktree state match the source inventory.
- Per-project target links resolve into the build disk.
- Large external artifacts match known hashes.
- Installed package metadata targets the expected FreeBSD ABI and architecture.
- Known-good binaries run and resolve dynamic libraries.
- Authentication is verified without displaying credentials.
- The worker can start headlessly, produce a current address, and shut down gracefully.
- Temporary transfer authority and firewall state have been removed.
Claims this case study does not make
- It does not claim that every FreeBSD release or Hyper-V host needs the same VM settings.
- It does not claim that a traditional hard drive is the fastest place for Rust target directories.
- It does not claim that the beacon authenticates a hostile network peer.
- It does not claim that package installation proves a reproducible build.
- It does not claim that agent assistance removes the need for an operator who understands the systems.
- It does not claim that copying skills makes different coding assistants behaviorally identical.
- It does not claim that a completed setup eliminates future patching, backups, or credential rotation.
StandTerm is the local bridge in this example. It does not host the source, replace SSH, or become a package service. It gives the operator a durable browser terminal and gives a local coding agent a controlled mirror with typed state, bounded observation, visible input, human-activity protection, privacy modes, and revocable tokens. That narrow position is what lets it participate in consequential workflows without claiming ownership of them.
— END OF FIELD STORY —