The Workspace
Interbox does not carry your pipelines — it loads them from a separate workspace repository at boot and keeps them up to date while it runs. This page covers how you point an engine at one, how updates reach it, and what the dashboard is allowed to change.
Two settings decide almost everything here:
INTERBOX_WORKSPACE_MODE | how the workspace is run — a copy the engine owns, or a directory it reads in place |
INTERBOX_WORKSPACE_ACCESS | what the dashboard may do to it — browse, propose changes, or change what is running |
They are independent. The first is about the engine, the second about the people looking at it.
Pointing the engine at a workspace
INTERBOX_WORKSPACE_GIT_URL=git@github.com:acme/interbox-workspace.git
INTERBOX_WORKSPACE_GIT_REF=main # branch to run (default: main)
INTERBOX_WORKSPACE_GIT_KEY= # the credential for a private workspace
INTERBOX_WORKSPACE_POLL_MS=30000 # how often to look for changes; 0 disables
The credential
A private workspace needs one credential, and INTERBOX_WORKSPACE_GIT_KEY is
it — for both transports. An https:// remote wants a token, which Interbox
splices into the clone URL. A git@… or ssh://… remote wants an SSH private
key, as PEM or base64-encoded PEM. A public repository over https://, or a
local path, needs nothing at all.
A public repository over SSH still needs a key, which surprises people. The
forges do not offer anonymous SSH — GitHub answers an unauthenticated
git@github.com:… with Permission denied (publickey) whether the repository is
public or not, and the others behave the same way. So point a public workspace at
its https:// URL and supply nothing, or supply a key and use the SSH one; what
does not work is an SSH URL with no credential.
INTERBOX_WORKSPACE_GIT_KEY | the credential itself |
INTERBOX_WORKSPACE_GIT_KEY_PATH | the same credential, from a file |
One setting offered twice. Inline suits a secret manager, which hands you a value and no filesystem — and base64 is accepted alongside PEM because several of them mangle a multi-line value on the way through. A path suits anything that already arrives as a file: a mounted secret volume, a projected file. Set both and the inline one wins.
There is deliberately no setting that says which kind of credential you
supplied. The URL already decides the transport, and a private key announces
itself — so Interbox reads the value, recognises a private key by its
-----BEGIN … PRIVATE KEY armour, and treats anything that is not a PEM block at
all as a token. It then checks that against the remote, and says so plainly when
the two disagree: a key against an https:// URL is refused with the
git@host:org/repo form to use instead, and a token against an SSH URL is
refused as a token where a key is needed. Both of those used to fail much further
along, as a mangled clone URL or an invalid format complaint from ssh.
A PEM block that is not a private key — a certificate, or a public key — is
refused by name rather than handed to ssh, which is the other way people arrive
at that same invalid format. If you reach for the file with the .pub suffix,
you will be told so.
A key must not be passphrase-protected — nothing here has a terminal to type one
into, so a key that asks for one is refused with a message saying so rather than
hanging the clone. A token must survive being put in a URL, so one containing
whitespace, @, /, ? or # is refused too. Interbox will not
percent-encode it for you: if you were already encoding it yourself, doing it
twice would send a credential that looks well-formed and is wrong, which a forge
answers with a bare 403. Encode it yourself if your token really does contain
one of those characters — a %2F is passed through untouched.
A token is only ever sent over https://. A plain http:// remote is refused
rather than cloned anonymously, because the alternative is a credential crossing
the network in cleartext on every poll.
Nothing is written into ~/.ssh. A key is materialised at mode 0600 under the
engine's own cache directory, never into the workspace
checkout, which the dashboard can browse, and ssh is pointed at it per git
command. Once the credential is resolved, both variables are deleted from the
process environment, so workspace code and the actions the dashboard runs cannot
read it back out of process.env. That is not the same as hiding it. A key file
is still there for anything running as the same user, which is what the mode and
the location are for — and a token, having been spliced into the clone URL, is
stored in the clone's own .git/config by git, exactly as it would be if you
had run the clone by hand. Interbox strips it back out before showing you a
remote, but it is on disk.
Set neither and git behaves as it always has: on a developer's machine it finds
~/.ssh and any running agent, which is usually what you want locally. For an
SSH remote the engine warns once at boot, because in a deployment a clone with
no key configured is one that is going to fail for a reason nothing else
explains.
Changing a credential needs a restart. It is read once, when the engine starts.
The credential reaches the workspace repository and nothing else. After cloning,
the engine installs the workspace's own dependencies, and that step does not get
it — so a dependency your package.json names as git+ssh://… or over private
https will not resolve, and the clone will look fine while the install fails.
Publish such a dependency to a registry the engine can reach, or vendor it into
the workspace. The credential stays scoped to the one repository it was issued
for, which is also what makes a read-only deploy key a complete answer.
Host keys are checked
The workspace repository is the code this engine imports and executes, so
whatever the SSH connection actually reaches chooses that code. Whenever Interbox
supplies the key it verifies the host key on every connection, and there is no
setting to relax that — no need to go looking for one. An SSH remote whose host
key cannot be found anywhere fails the clone rather than trusting it. With no key
configured Interbox is not in the connection at all: git uses the ambient
~/.ssh, and that machine's own ssh configuration decides how strictly it
checks.
GitHub, GitLab, Bitbucket and Azure DevOps (ssh.dev.azure.com and
vs-ssh.visualstudio.com) are already known to the image, so they need nothing
beyond a key — though Azure DevOps is the one that may refuse an ed25519 deploy
key, and if it does, generate an RSA one instead
(ssh-keygen -t rsa -b 4096 -N ""). Anything else — a self-hosted forge, a
mirror inside your own network — you read once and pass in:
ssh-keyscan git.example.com
INTERBOX_WORKSPACE_SSH_KNOWN_HOSTS="git.example.com ssh-ed25519 AAAAC3Nza…"
ssh-keyscan trusts whatever answers it, so compare the fingerprint it prints
against one your forge publishes before pasting it anywhere.
INTERBOX_WORKSPACE_SSH_KNOWN_HOSTS_PATH takes a known_hosts file instead, if
that is how yours arrives. These two are the only SSH-specific settings left —
host keys are what an https token has no equivalent of.
What you supply is added to what the image already knows rather than replacing
it — a host is accepted when the key it offers matches any entry — so adding a
private forge never costs you github.com. It is also the way out when a public
forge rotates its host key faster than an Interbox release does: set the new
line and restart the engine. Like the credential, host keys are read once, when
the engine starts, so a rotation does not reach a process that is already
running — and that holds for a _PATH file just the same, since its contents
are read at that same moment rather than watched.
A read-only key is enough
The engine clones, fetches and resets; it never pushes. A deploy key with read-only access therefore runs a deployment end to end, and that is what to give it.
Write access is a second, separate decision. The dashboard can commit and push a
branch, but only at INTERBOX_WORKSPACE_ACCESS=push or apply (see what the
dashboard may do) — and only then does the key need
to be writable. Both have to be true before anything leaves this deployment, and
neither is the default.
Prefer a deploy key to a personal access token where the forge offers one. A deploy key belongs to the repository: it reaches that repository and nothing else, and it does not expire. A token belongs to a person — it carries whatever that person can reach, stops working when their access does, and expires on a clock (GitHub caps a fine-grained token at 366 days, and an organisation can require less).
A key also stays out of the clone URL, and a token cannot. git clone https://<token>@host/… records that URL verbatim in the clone's .git/config,
so the token lives on disk in a second place that nothing rotates for you; a key
is never part of a URL at all.
clone or worktree
INTERBOX_WORKSPACE_MODE=clone # or: worktree
clone — the engine clones the URL into its cache, installs the workspace's
dependencies and builds it. That copy belongs to the engine: it fetches, checks
out and resets it freely, which is what makes branch switching and dashboard
edits safe. This is what a deployment runs.
worktree — the engine bundles a directory exactly as it stands, including
uncommitted edits, and never checks anything out or resets anything. The
directory belongs to someone else: your own checkout while developing, or the
volume mounted at /workspace in the image. Changes are picked up by watching
the newest modification time under src/.
If you don't set it, the URL scheme picks the default — git@, https:// and
ssh:// mean clone; a file:// URL or a plain path means worktree. That is
only a default. file:///path/to/repo is a git remote like any other, so
INTERBOX_WORKSPACE_GIT_URL=file:///home/me/interbox-workspace
INTERBOX_WORKSPACE_MODE=clone
runs a local repository exactly as a deployment runs a remote one — the way to exercise branch switching and dashboard commits without a stand. Note that the engine then runs your commits, not your working tree; uncommitted edits are invisible to it until you commit them.
The two modes differ in one more way worth knowing: only a clone can be
held (see below), because only a clone is ever reset.
How an update reaches a running engine
A pipeline change is a workspace change, not an engine upgrade — no image bump, no restart. Three things trigger a reload:
- the poll —
INTERBOX_WORKSPACE_POLL_MS(default 30s). Inclonemode it compares the ref's sha at the remote; inworktreemode, the newest mtime undersrc/. - "Sync now" in the dashboard's Repository tab, which asks the engine directly rather than waiting for the poll.
SIGHUP, for anyone driving the process from outside.
A reload loads the new workspace first and only swaps the running workers
once it builds. A broken commit therefore leaves the previous pipeline running
rather than taking the engine down, and the failure is reported in the
dashboard: "the workspace on <ref> failed to load, so the engine is still
running the code it had", with the underlying error.
Held updates
If the engine's copy contains work that a refresh would destroy — uncommitted edits, or commits that were never pushed — it stops updating rather than resetting over them, and says so in the dashboard:
Workspace updates are paused — the engine is holding
mainat the current commit because updating would destroy local changes.
The environment keeps running the code it has until someone resolves it: push
the work to keep it, or discard it (INTERBOX_WORKSPACE_ACCESS=apply, since
discarding destroys it for good — as does replacing the pod). Untracked files
never cause a hold; they survive a reset anyway.
This only applies to clone mode. A worktree is never refreshed or reset, so
there is nothing to hold.
What the dashboard may do
INTERBOX_WORKSPACE_ACCESS=read # or: push, apply
The Repository tab can browse the workspace, and — depending on this setting — edit it. Each level includes the ones before it:
read (default) | push | apply | |
|---|---|---|---|
| browse files, history, diffs | ✅ | ✅ | ✅ |
| edit and push to a new branch | ✅ | ✅ | |
| push to the deployed or default branch | |||
| switch the branch the engine runs | ✅ | ||
| save straight into a worktree | ✅ | ||
| discard a held update | ✅ |
The line that matters is between push and apply: can an unreviewed change
reach what this deployment is running? A pushed branch cannot — it waits for a
human to merge it. Switching the ref, or writing into a worktree the engine runs
in place, does it immediately.
The dashboard has no login. Whoever can open it gets whatever this allows, so treat it as a property of the environment rather than of a person:
- production —
read, orpushif you want people proposing pipeline changes from the browser. Neverapply. - a stand —
pushis usually right. Useapplywhen you deliberately want to point it at a branch from the browser to verify something. - a local machine —
apply. This is the only level at which a worktree workspace can be edited at all.
Anything unrecognised is treated as read: a typo in a deployment's config
should cost a feature, never grant one.
Editing from the dashboard
What "edit" means follows from the mode, because the two workspaces keep changes in different places:
- clone — the copy is reset on every refresh, so an edit only survives as a
commit. The dashboard writes your changed files, commits them as
interbox-dashboard(there is no login, so there is no author to attribute them to) and pushes to a branch you name. Never the deployed branch, never the remote's default. Open a pull request from there as usual. - worktree — the file on disk is what runs, so the dashboard saves it there and the engine picks it up on the next poll. Committing is yours to do, in your own git.
Edits live in your browser until then — not on the engine, not in the repository. They survive a reload and are visible only to you.
When two people edit the same file
The editor records which version of the file it loaded. If that file changes before you commit — a colleague pushes, the engine refreshes — Interbox performs the same three-way merge git would:
- edits in different parts of the file are merged, and the confirmation says which files were merged and why.
- edits on the same lines cannot be merged. The push is refused with the file named, and you choose: keep yours (the pull request shows exactly what that replaced) or cancel and rework on top of theirs.
Nothing is overwritten silently, and nothing is lost by refusing — your work stays in the browser either way.
Switching the branch an environment runs
At apply, the Repository tab's branch picker points the engine at another
branch: verify a pull request on a stand, then switch back. The override is
stored beside the clone, so a restart returns the environment to the ref it
declares — nobody can leave a stand silently pinned to a test branch, and the
dashboard says when it is running an override.
Switching takes as long as a full reload (fetch, checkout, install, build), and the dashboard waits for the engine to confirm rather than assuming.
Cache and disk
INTERBOX_WORKSPACE_CACHE=/var/lib/interbox # default: the system temp dir
In clone mode this holds the clone, its node_modules, the built bundle and
the ref override. The default lives in the temp directory, which on Kubernetes
means the pod's own filesystem: a replaced pod re-clones from scratch and
returns to the declared ref. Point it at a volume if you would rather keep the
clone (and any branch override) across restarts.