Kenn Forge can run agent workspaces in your account on a shared Linux machine. Your usual Forge remains the controller: you choose work, supervise agents, review diffs and push branches there. The devbox owns the worktrees, tools, agent processes and tmux sessions. Closing the browser does not stop them.
This is an initial operator-managed deployment for trusted teams on Tailscale. It is not a container sandbox. Each developer gets a separate non-root Linux account. Administrators can read account data and installed agent credentials; personal GitHub credentials stay on the controller.
- Open Settings → Workspaces on your usual Forge hub or standalone Forge. Workspace machines lists this Forge machine and every devbox assigned to you, each once, with its current state: Not connected, Online, Offline, or Maintenance. The registry that supplied the list is shown beneath it. If no registry is configured yet, enter the address from your operator and select Find devboxes; you never enter ports or tokens. Select Connect beside a machine; Forge checks its account identity and updates that row. Refresh checks for new assignments without removing saved connections.
- Select the radio button beside a connected machine to run new workspaces there. Existing workspaces remain where they were created. You can choose another connected machine in the new-workspace dialog. An unavailable default does not silently move work to your local machine; the settings row shows Offline with a Reconnect action instead.
- Create a workspace from a repository, pull request or issue. Remote pull request and issue actions open the workspace in Workspaces. The first clone of each repository in your account can take longer. No second local checkout is needed.
- Start an agent, give it the task and inspect its output and diff. Files and tests stay on the devbox. The agent uses the tools and subscription installed in your account. Manual editing is optional; the normal workflow is to ask the agent to make the change.
- Push the branch. Commits use your configured name and GitHub noreply email; the GitHub App authenticates the push. Forge checks GitHub's resolved author and committer IDs after pushing and displays a warning if attribution cannot be verified. A verification failure does not retry the push.
- Review and merge through your usual trusted Forge or GitHub session. Delete the remote workspace explicitly when finished. Automatic cleanup after a merge is not part of this initial deployment.
Reconnect after sleep to see progress. There are no new worker-generated notifications while the controller is disconnected. Existing terminals and Git work survive an expired source-context lease; starting another agent for a pull request or issue needs fresh context from the controller. If a worker credential changes, use Reconnect in settings. Removing a connection leaves its remote work intact.
Preview-port discovery and forwarding are not included. For a browser preview, read the port from agent output and use your terminal client's explicit port forwarding. Do not expose application listeners publicly to obtain a preview.
Each developer uses a separate Linux account on the devbox. The operator links that account's numeric Linux user ID to the developer's GitHub user ID and configures their commit name and email. Forge uses that link to check repository access; Git uses the name and email to identify new commits.
The credential broker is a small service on each devbox that supplies temporary GitHub credentials to Git. It runs under its own service account, which holds the GitHub App's private key. Development accounts receive short-lived tokens; the deployment keeps the App key readable only by the broker service account. Your personal GitHub credentials stay on the controller.
Repository access follows these steps:
- The operator enables the repository. They install the organization's GitHub App on selected repositories and add the allowed repositories and developer accounts to the broker configuration. Connecting a devbox in Forge does not itself grant GitHub access.
- Git asks for a credential. When Forge clones a repository, or Git needs to fetch or push from a managed worktree, it asks the broker for access to that specific repository. An agent or SSH shell uses the same Git helper.
- The broker identifies the caller and checks access. The request travels over a local Unix socket. Linux supplies the caller's user ID, which the broker looks up in its configured account list. The broker checks the linked GitHub user's current organization membership and repository permissions, as well as the configured App installation and repository identity.
- Git receives a temporary token. The broker requests a GitHub App installation token restricted to that one repository. A developer with read access can clone and fetch; write access also permits pushes, subject to GitHub's branch rules. The helper passes the token to Git without saving it as a stored credential.
- Later requests repeat the access checks. The broker can reuse a token cached in memory, but still checks the caller's access before returning it. It replaces tokens near expiry. Already issued tokens remain usable until they expire or are revoked.
This initial devbox integration supports repositories on github.com. It does
not require a personal access token or gh auth login in the developer's
devbox account. See GitHub App and branch rules
for the operator setup.
Forge sets user.name and user.email in each managed worktree from your
configured developer identity. Use the GitHub-provided noreply address shown
in your GitHub email settings. GitHub uses the email in a commit to
associate it with your account.
New commits made by an agent in your account therefore use your configured
identity, just as commits made from your SSH shell do.
The identities have different jobs:
| Identity | What it means |
|---|---|
| GitHub App | Authenticates the push and any pull request actions made through the devbox wrapper. |
| Commit author | The person credited with the original change, recorded in the commit. |
| Committer | The person who created this version of the commit, also recorded in the commit. |
For a new commit, author and committer normally both identify you. A cherry-pick or rebase can preserve someone else's author identity while recording you as the committer. The App can push either kind of commit without becoming its author.
Forge validates the worktree's configured identity before starting an agent or pushing through Forge. After a push, the controller asks GitHub which accounts it resolved for the branch's latest commit and compares their numeric user IDs with your enrolled GitHub ID. Forge reports a match, a preserved original author, a mismatch, or an unverified result. If the email is wrong or GitHub cannot be reached, a successful push is not repeated. Commit attribution does not provide a cryptographic signature or prove who typed the change.
An SSH terminal client connects to the same Unix account with the
operator-approved development key and agent forwarding disabled. With the
worker configured to use that account's default tmux server, clients can
list and attach to its existing sessions. Configure window-size latest in
system tmux settings so two attached clients do not constrain each other to
the smaller terminal. Forge still owns its worktrees and workspace lifecycle.
For repository-only work started from the remote terminal, the existing API CLI can create a workspace without a second Forge daemon:
kenn-forge api --config /etc/forge-worker/user-a.toml \
POST /api/v1/worker/workspaces --data '{"repository":{"provider":"github","platform_host":"github.com","owner":"example-org","name":"project-a"},"branch":"work/task-a"}'The response includes the workspace ID and setup state. Use the same CLI with
GET /api/v1/workspaces/ID to wait for ready, then start a configured agent:
kenn-forge api --config /etc/forge-worker/user-a.toml \
POST /api/v1/workspaces/ID/runtime/sessions --data '{"target_key":"codex"}'Create pull request and issue workspaces through the controller. The worker
cannot obtain or validate that source context itself. Git in managed worktrees
uses the same broker helper from Forge, an agent or an SSH shell. Do not run
gh auth login on the devbox. Supported pull request commands use:
kenn-forge devbox github --socket /run/forge-broker/broker.sock \
--repository example-org/project-a -- pr createThe wrapper supports pr create, edit, comment, view, list and checks.
Its token goes only to that child process. Git and the wrapper explain broker
failures, such as a repository not being admitted or missing write permission.
Forge supplies generic worker, registry and credential-broker commands. Your provisioning repository owns users, SSH admission, packages, service units, roster assignments, agent credentials and network policy. The examples here use ordinary Linux accounts, systemd and Caddy; no private deployment tooling is required. Keep your chosen versions and configuration in your own automation.
| Host | Processes and state |
|---|---|
| Your existing controller | Full Forge with its browser UI, provider access and saved devbox connections. |
| Each devbox | One credential broker and one worker per developer account. Worktrees, tools, agent credentials and tmux sessions live here. |
| Registry host | One registry process, a roster and copies of worker connection tokens, behind an HTTPS proxy. It needs no worktrees, agents or GitHub App key. |
The registry handles discovery and connection setup. Workspace requests and terminal traffic go directly from the controller to the worker. An existing connection does not depend on the registry staying online. A registry host can also run other trusted services; give the registry its own service user, configuration directory and Unix socket.
- Install the same reviewed Forge revision on the controller, devboxes and
registry. Use the Quick Start build instructions when building
from source, and record the full commit and toolchain version. Confirm that
kenn-forge devbox --helplistsworker,brokerandregistry. - Enroll all hosts in Tailscale. The controller daemon's host, which may differ from the browser's laptop, needs access to the registry and workers.
- On each Linux devbox, install Git, tmux, GitHub CLI and your agent tools.
Create each developer's account with a private home and no sudo or privileged
supplementary groups. Enroll dedicated SSH public keys for terminal clients;
retain a separate administrator login and recovery access.
For Codex, install the distribution's
bubblewrappackage and follow its Linux sandbox prerequisites. Runcodex sandbox -- /usr/bin/trueas the developer to check the sandbox before launching an agent; a successful login check does not exercise it. - On the registry host, install Caddy and prepare a stable hostname, such as
devboxes.example.org, pointing to its Tailscale address. Obtain a certificate trusted by the controller and arrange automatic renewal. For a private listener, DNS validation avoids exposing an HTTP challenge endpoint. Verify certificate issuance before publishing the registry site.
Use your existing tailnet policy with these connections:
| Source | Destination |
|---|---|
| Approved controller hosts | Registry HTTPS port 443 and devbox worker port range, for example TCP 9100–9199. |
| Approved terminal clients and administrators | Devbox SSH port 22. |
| Devboxes | GitHub and the agent providers over outbound HTTPS. |
| Registry | Local Tailscale identity service; outbound access needed by your certificate renewal setup. |
The registry does not need SSH access to devboxes during normal operation. Deployment automation needs administrator access when it copies worker identities into the registry. Keep certificate credentials, App keys and worker tokens outside source control and deployment logs.
Create this App once for your organization using GitHub's browser UI. It is
separate from any App used by your controller to sync GitHub data. The same
installation can serve multiple devboxes; you do not create an App per host
or developer. The examples below use example-org, project-a and user-a;
replace them with your organization, pilot repository and developer login.
Sign in to GitHub as an organization owner or authorized App manager. Start with a disposable repository in that organization for the push and branch-rule checks below.
-
Open your organization's Settings → Developer settings → GitHub Apps → New GitHub App. Do not create it under your personal account.
-
Choose a globally unique name, such as
example-org-devboxes. For Homepage URL, use your organization's website or GitHub page. A running Forge server or public callback endpoint is not needed to register the App. -
Leave Callback URL and Setup URL blank. Leave Request user authorization (OAuth) during installation and Enable Device Flow unchecked. Under Webhook, uncheck Active.
-
Set the permissions in this table. Leave all others at No access.
Scope Permission Access Repository Contents Read and write Repository Pull requests Read and write Repository Checks Read-only Repository Commit statuses Read-only Repository Metadata Read-only (automatic) Organization Members Read-only -
For Where can this GitHub App be installed?, choose Only on this account, then click Create GitHub App.
-
On its General page, record the numeric App ID, not the Client ID. Under Private keys, click Generate a private key. Keep the downloaded PEM on your trusted operator workstation, outside Git, with mode
0600. No client secret is needed. -
Open Install App, choose your organization, then Only select repositories. Select the pilot repository and click Install. Add production repositories only after the branch-rule checks pass.
These are GitHub's standard registration, key generation and installation steps. Forge does not automate App registration or require a user OAuth flow for devbox Git access.
Use an existing GitHub CLI login on your trusted workstation for these read-only lookups. Do not sign in with a personal GitHub credential on the devbox.
gh api --paginate /orgs/example-org/installations \
--jq '.installations[] | {app_slug, app_id, installation_id: .id, repository_selection}'
gh api /orgs/example-org --jq .id
gh api /repos/example-org/project-a --jq '{id, name, full_name}'
gh api /users/user-a --jq '{id, login}'Choose the installation whose app_slug matches the App you just created and
whose repository_selection is selected. Use its app_id and
installation_id, the organization's id, and the repository's id and name
in the broker TOML below. Use the user's id for github_user_id; uid is
that developer's Linux account ID. These IDs are not secrets.
Your deployment tooling installs the PEM at private_key_file, readable only
by the broker service account, never by development accounts. Host administrators
remain trusted with this organization-scoped App authority. Keep the PEM out of
source control and logs. The App authenticates pushes; each developer's commit
name and GitHub noreply email determine commit attribution.
The broker verifies each Unix UID's rostered GitHub ID, current organization membership, repository identity and repository permission before issuing a short-lived installation token for one repository. Read-only collaborators get read-only Git access. Token caching does not skip those admission checks.
App permissions cannot express “push branches, but never merge or push main.” Use an enforced Restrict updates ruleset on default/protected branches, with bypass assigned only to the intended human maintainers and never this App. Keep the required-review and checks rules in a separate ruleset so bypassing the update restriction does not waive those requirements. Test an actual rejected default-branch push and merge attempt before admitting production repositories. Contents write can authorize the merge endpoint; omitting Pull requests write does not solve this. Preserve protection against force pushes and deletion. Do the initial allow/deny exercises in a disposable repository governed by the same rules.
The broker configuration is TOML:
socket = "/run/forge-broker/broker.sock"
private_key_file = "/etc/forge-broker/app.pem"
app_id = 100
installation_id = 200
organization_id = 300
organization = "example-org"
[[repositories]]
id = 400
name = "project-a"
[[accounts]]
uid = 1001
github_user_id = 1234
login = "user-a"
commit_name = "Developer A"
commit_email = "1234+user-a@users.noreply.github.com"Start kenn-forge devbox broker --config /etc/forge-broker/broker.toml as a
separate service user. On Linux, the broker authenticates the connecting UID
with SO_PEERCRED. Its socket permits local connections; an unlisted UID is
rejected regardless of request fields. It does not listen on the network.
For systemd, create a system user named forge-broker with no login shell or
home. Install the configuration and PEM under /etc/forge-broker, readable
only by that user (directory 0700, files 0600). Install the binary at
/usr/local/bin/kenn-forge, owned by root. Save this unit as
/etc/systemd/system/forge-broker.service:
[Unit]
Description=Forge devbox GitHub credentials
Wants=network-online.target
After=network-online.target
[Service]
User=forge-broker
Group=forge-broker
RuntimeDirectory=forge-broker
RuntimeDirectoryMode=0755
UMask=0077
ExecStart=/usr/local/bin/kenn-forge devbox broker --config /etc/forge-broker/broker.toml
Restart=on-failure
RestartSec=3
[Install]
WantedBy=multi-user.targetRun sudo systemctl daemon-reload and
sudo systemctl enable --now forge-broker. The runtime directory allows
developers to reach the socket; admission is checked using their kernel-supplied
Unix UID on every request.
Create a private data directory owned by the developer, for example
/home/user-a/.local/share/forge-worker with mode 0700. Save this worker
configuration as /etc/forge-worker/user-a.toml, owned by root and readable by
the account. Obtain the actual Linux UID with id -u user-a and use it in
both worker and broker configuration.
host = "127.0.0.1"
port = 9100
base_path = "/"
data_dir = "/home/user-a/.local/share/forge-worker"
allowed_hosts = ["host-a.example.ts.net:9100"]
trust_reverse_proxy = true
[api]
require_auth = true
[execution_worker]
enabled = true
uid = 1001
github_user_id = 1234
broker_socket = "/run/forge-broker/broker.sock"
commit_name = "Developer A"
commit_email = "1234+user-a@users.noreply.github.com"
worktree_dir = "/home/user-a/workspaces"
[tmux]
command = ["tmux"]worktree_dir places new GitHub.com worktrees at <worktree_dir>/<owner>/<repo>/<workspace>.
Forge keeps its database, credentials and managed clones in data_dir. Changing
this setting does not move existing workspaces or interrupt their agents. When
omitted, worktrees remain under data_dir/worktrees.
Run kenn-forge devbox worker --config /etc/forge-worker/user-a.toml as that
account. Provide its tool PATH, agent installation and credentials. Use
KillMode=process so restarting the worker does not kill tmux sessions. Apply
CPU and memory limits to the user's slice if SSH and worker processes should
share one budget. Run the broker before the workers.
Save this unit in the developer's home at
.config/systemd/user/forge-worker.service. Adapt PATH to include the actual
agent and tool installation directories; a systemd service does not source the
developer's interactive shell configuration.
[Unit]
Description=Forge workspace worker
[Service]
Environment=HOME=/home/user-a
Environment=PATH=/home/user-a/.local/bin:/usr/local/bin:/usr/bin:/bin
Environment=KENN_FORGE_DEV_RESTART=1
UMask=0077
ExecStart=/usr/local/bin/kenn-forge devbox worker --config /etc/forge-worker/user-a.toml
Restart=on-failure
RestartSec=3
KillMode=process
TimeoutStopSec=30
[Install]
WantedBy=default.targetAn administrator runs sudo loginctl enable-linger user-a. Then, in an SSH
session as user-a, run:
systemctl --user daemon-reload
systemctl --user enable --now forge-worker
kenn-forge api --config /etc/forge-worker/user-a.toml GET /api/v1/workerCheck the returned account identity before publication. On first startup Forge
creates auth_token and node_id in the data directory. Keep those files and
the rest of the data directory across upgrades; deployment must not generate
replacement identities. Both identity files belong to the developer and use
mode 0600. Back up this directory as application data, including the workspace
repositories; reinstalling services does not recover lost work.
Publish just that listener with Tailscale Serve:
tailscale serve --bg --http=9100 http://127.0.0.1:9100Use a distinct fixed port for each account, then one tailnet policy grant from
approved developers/controllers to the worker port range. The account boundary
is the bearer token. Keep api.tailscale_serve disabled: another local account
can send forged identity headers to a loopback listener. Worker bearers are
unscoped; the restricted worker route set is the authorization boundary. It
excludes settings, provider mutations, fleet enrollment and the browser UI.
The registry is a read-only roster served by
kenn-forge devbox registry --config /etc/forge-registry/registry.toml.
Use a stable HTTPS hostname resolving to the registry host's tailnet address.
Terminate TLS with a reverse proxy and forward over a Unix socket whose parent
is accessible only to the registry user and proxy group. Do not replace this
with unrestricted loopback TCP and trusted identity headers.
Create a system user named forge-registry with no login shell or home.
Create /etc/forge-registry and its tokens subdirectory owned by that user
with mode 0700. Through your provisioning tool's private file-transfer
mechanism, copy each running worker's auth_token into the corresponding
registry token file, owned by forge-registry with mode 0600. Read the
worker's node_id into its assignment below. Preserve the worker's originals.
Do not paste tokens into a terminal or print them in automation output.
Use tailscale whois --json CONTROLLER_TAILSCALE_IP on the registry host to
obtain UserProfile.ID for tailscale_user_id and, when explicitly pinning a
controller, Node.StableID. Obtain SSH host fingerprints from an independently
trusted source, such as the host console. Save the roster below as
/etc/forge-registry/registry.toml, owned by forge-registry, mode 0600.
socket = "/run/forge-registry/registry.sock"
registry_id = "example-devboxes"
revision = "deployment-1"
[[developers]]
github_user_id = 1234
tailscale_user_id = 5678
controller_node_ids = []
enabled = true
[[assignments]]
host_id = "host-a"
name = "Compute A"
url = "http://host-a.example.ts.net:9100"
account = "user-a"
ssh_address = "user-a@host-a.example.ts.net"
ssh_host_fingerprint = "SHA256:REPLACE_WITH_VERIFIED_HOST_FINGERPRINT"
node_id = "REPLACE_WITH_WORKER_32_HEX_NODE_ID"
uid = 1001
github_user_id = 1234
role = "execution"
protocol = 1
enabled = true
maintenance = false
token_file = "/etc/forge-registry/tokens/host-a-user-a"Save /etc/systemd/system/forge-registry.service:
[Unit]
Description=Forge devbox discovery
Requires=tailscaled.service
After=tailscaled.service
[Service]
User=forge-registry
Group=caddy
RuntimeDirectory=forge-registry
RuntimeDirectoryMode=0750
UMask=0077
ExecStart=/usr/local/bin/kenn-forge devbox registry --config /etc/forge-registry/registry.toml
Restart=on-failure
RestartSec=3
[Install]
WantedBy=multi-user.targetRun sudo systemctl daemon-reload and
sudo systemctl enable --now forge-registry. Check the local service:
sudo curl --fail --unix-socket /run/forge-registry/registry.sock http://localhost/healthzThe response should contain "status":"ok". This checks process health,
not Tailscale admission. The socket is accessible to the service and Caddy
group, and the registry uses no shared database or job queue.
Add a site to your existing Caddy configuration, replacing the hostname, Tailscale IP and certificate paths. The PEM pair must be readable by Caddy. The proxy overwrites the peer headers with the actual TCP peer:
https://devboxes.example.org {
bind YOUR_REGISTRY_TAILSCALE_IP
tls /etc/caddy/certs/devboxes.crt /etc/caddy/certs/devboxes.key
reverse_proxy unix//run/forge-registry/registry.sock {
header_up X-Devbox-Peer-IP {remote_host}
header_up X-Devbox-Peer-Port {remote_port}
}
}Validate the complete configuration with
sudo caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile before
reloading Caddy. Retain the previous site and certificate until validation and
reload succeed. Your certificate renewal process must reload Caddy after
publishing a renewed pair. See Caddy's
Unix-socket proxy
and certificate configuration.
The service calls tailscale whois --json IP:PORT. Verify that it succeeds as
its service user before publication. Ordinary untagged nodes map by Tailscale
user ID. Tagged or shared controllers require an explicit stable node ID in
controller_node_ids. This trusts every process on that node as the assigned
developer; do not pin a shared multi-user devbox as one person's controller.
Discovery lists only the caller's assignments. The native controller obtains
the worker token and verifies node ID, Unix UID, GitHub ID, role and protocol.
The browser never receives worker tokens. Connections survive registry outages;
new connections and token refresh require the registry. Relocation preserves
registry_id, host IDs, assignments and tokens and changes DNS behind the
stable hostname. Restart the registry after changing its prepared roster.
From the actual controller host, check
curl --fail https://devboxes.example.org/api/v1/devboxes, then connect through
Settings → Workspaces. That discovery response contains assignments, not
tokens. A successful local health check alone does not prove this network path.
| Situation | Action |
|---|---|
| Add a devbox or developer | Provision the account and worker, copy its persistent identity into the registry, restart the registry, then discover and connect from Forge. |
| Update Forge | Preserve account data and identities, replace the reviewed binary, restart services, then check identity and reattach to an existing workspace. |
| Registry unavailable | Existing saved connections keep using their workers. Restore the registry to add connections or refresh tokens. Agents keep running on their devboxes. |
| Worker unavailable | Check its user service and journal, disk space and account resource pressure. Restart it with the same data directory; do not delete the directory to repair startup. |
| Worker token changed | Restart the worker with the intended token, copy that token into the registry, restart the registry and use Reconnect. An unexpected node/account identity change requires explicit removal and re-enrollment. |
| GitHub or the broker unavailable | Inspect the broker journal and helper's error. Local work can continue, but GitHub operations need restored service. |
| Agent subscription exhausted | Stop affected agent sessions, install the replacement credentials in that account and start new sessions. GitHub App credentials are independent. |
Use journalctl --user -u forge-worker as the developer and
sudo journalctl -u forge-broker -u forge-registry on the hosts that run those
services. Ordinary deployment should not reset worktrees, worker identities or
agent credentials. For offboarding, removing a registry assignment alone does
not revoke an already saved bearer: also disable account access and stop the
worker, and remove the broker admission for that account.
Managed worktrees reset inherited credential helpers and set user.useConfigOnly,
name and email. Forge validates identity before agent start and push. Git
preserves another person's author on cherry-pick and rebase while changing the
committer; Forge reports that preserved author separately. Commit metadata is
an attribution convention, not a cryptographic proof of who typed a change.
The App remains the push actor and PR-edit actor.
Before calling a deployment ready, exercise the actual controller, registry, worker, agent and GitHub repository: create, attach, commit, push, inspect both resolved GitHub identities, disconnect, restart, reattach and delete. Test a second account's denied access with the wrong bearer and forged Serve headers, plus allowed concurrent work using its own credentials. A local fixture test cannot prove the deployment's Tailscale grants, App installation or rulesets.