Repository navigation
Conversation
orsissimo
force-pushed
the
feat/private-runtime
branch
from
October 31, 2026 12:04
ebd572b to
1f77a83
Compare
orsissimo
force-pushed
the
feat/private-runtime
branch
from
October 31, 2026 13:16
1f77a83 to
09e2fc1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Step 1 of the split proposed in #115, "Private runtime state, unique identity, and real locking". Based directly on master, with no dependency on driver work.
$XDG_STATE_HOME/swarm-core/<project>(SWARM_RUNTIME_DIRoverrides), inside a mode 700 directory; the bare repo stays writable for any container agent UID.swarm.envwith mode 600state.json(swarm-core.state/v1);stopkeeps the state file.swarm-core.projectandswarm-core.checkoutcontainer labels; refuse collisions between checkouts and accept a separate project id throughSWARM_PROJECT.SWARM_ENGAGEMENT_ID, and refuse mismatches in later phases.start,post-process, interactive startup and harvest withflock -nonengagement.lock; fail fast on contention and verify a caller'sSWARM_LOCK_FD./tmp/<project>-upstream.gitstate in start/post-process/interactive/harvest and block commands on unlabeled containers until removed, with recovery instructions.These are breaking changes for embedders;
flockis now required (brew install flockon macOS). Preservation refs, safe replacement, retirement/cleanup and engagement container labels are the next step.Lifecycle invariants
These invariants cover the whole series: this PR implements 1-5 (cleanup follows in step 2), and later steps implement 6-10; each item names its step.
(step 1) One project id names the runtime directory, image and containers; the state file records the owning checkout, containers carry
swarm-core.projectandswarm-core.checkoutlabels, and stop/removal re-checks those labels before acting on container IDs. A second checkout with that id is refused until it sets a distinctSWARM_PROJECT, because an automatic hash suffix would change its id once the first runtime is gone and strand unharvested work.(step 1)
startassigns and saves an engagement id (<UTC timestamp>-<8 hex>unlessSWARM_ENGAGEMENT_IDis set); standalonepost-processandinteractivedo the same when they create the bare repo. Later lifecycle phases refuse a suppliedSWARM_ENGAGEMENT_IDthat differs from the id instate.json.(step 1; cleanup in step 2)
start,post-process,interactive/chat/shellandharvest.shtakeengagement.lockwithflock -nand fail immediately on contention; a caller holding it passes the descriptor itself throughSWARM_LOCK_FD, which the engine verifies is open on the lock file before locking through it, because an inherited "already locked" flag cannot be verified. Interactive releases the engine's copy once the container starts, andstart --dashboardbefore opening the dashboard;stopand read-only commands do not take it, while cleanup will in step 2.(step 1) The runtime directory must be mode 700, owned by the user and not a symlink, or the engine refuses it.
state.jsonis parsed JSON (swarm-core.state/v1), written with mode 600 and never sourced; the bare repo and mirrors have write permissions for any containeragentUID.(step 1) Legacy
/tmpstate is never adopted. If the old bare repo is a directory owned by the user and is not a symlink,start,post-process,interactive/chat/shellandharvest.shrefuse to proceed and print commands to stop old containers, fetch their branches and remove the old state.(step 2) Every destructive path will check the same preservation refs:
agent-work,swarm/*,agent-parked/*and any per-run namespace from Multi-Agent Post-Process Proposal #112. A tip the checkout does not contain will block the operation.(step 2) An existing engine container will make
startfail; the engine will never force-replace it or delete state on its own. Removal will require explicit, fail-closed operations for stopped containers and for the whole runtime.(step 3) The host will resolve a target once to a full commit SHA before containers start, and every phase will use that SHA. A later request for a different commit will fail without re-resolving the target; the target credential will stay on the host.
(step 4) Every initialized submodule, at any depth and including in worktree layouts, will have a host mirror listed in a manifest. Containers will need no network access for submodules.
(step 5) Embedders will depend only on a versioned control interface and container labels. Queries will have no side effects; paths, file formats and container names outside that interface may change.
Changes
lib/project.sh: add runtime permission and ownership checks, JSON state helpers, ASCII project ids, engagement checks and descriptor-based locking.launch.sh: use the runtime directory, record engagements, label containers and check ownership before acting on their IDs; build mirror arguments as an array and create transient files withmktemp.dashboard.sh: read state with jq and check checkout and engagement identity before actions; copy container files into a private temporary directory.harvest.sh: check ownership and engagement identity, take the lock and print recovery commands that remove agent-owned files through Docker.costs.sh,progress.sh: use private temporary directories and restrict container reads to the owning checkout.README.md,USAGE.md,CHANGELOG.md: document the runtime contract and upgrade steps, includingdocker stop -t 60 <names>before fetching work and removing old containers, Alpine removal of agent-owned files, and cleanup by project label..github/workflows/ci.yml: installflockin the macOS job.tests/test_runtime.sh,tests/test_launch.sh,tests/test_dashboard.sh,tests/test_harvest.sh,tests/test_costs.sh: cover permissions, JSON state, identity collisions, engagement mismatches, locking and temporary-file handling.tests/test.sh: keep the caller'sSWARM_PROJECTfor smoke runs when another checkout with the same directory name owns the project's containers on the Docker host; document it in the help text and USAGE.md "Testing".tests/_test_env.sh,tests/test.sh,tests/test_ci_workflows.sh,tests/runtime_signal_trap.sh: isolate unit test state from caller settings and update cleanup, dependency and stop-path checks.The mirror argument,
mktempand ASCII fixes are separate prerequisite commits and can move to another PR if the code owner prefers.Self-review checklist
Test plan
./tests/test.sh --unit: 1695 tests in 14 files passed on macOS/bin/bash3.2.57; 1696 passed on Ubuntu 24.04 Bash 5.2. Both also passed withSWARM_PROJECTandSWARM_ENGAGEMENT_IDexported../tests/test.sh --runtime: passed 16/16 on OrbStack and on native Linux (Ubuntu 24.04, host UID 501).shellcheck -s bash --severity=warningis clean on every changed script of every commit.h,p,sandstart --dashboardthrough a pseudo-terminal; run and remove an isolated triage runtime./tmpstate.SWARM_PROJECTvalues, run both and stop one without stopping the other.start --dashboardin a pseudo-terminal; dashboard harvest merged 3 commits mid-run; both agents exited 143 through emergency pushes with the 60-second grace. Post-process held the lock while a harvest attempt failed fast, then its harvest merged the remaining 11 commits and the interactive branch.launch.sh shell --agent-index 1ran while the agents were active, with the lock free and container UID 1000, and pushed its branch on exit;startreplaced a half-removed bare repo with agent-owned files left behind. Printed recovery commands and documented cleanup remove agent-owned files through Docker../tests/test.sh --oauth: passed with a real agent on that host as UID 501 and UID 1000, the second withSWARM_PROJECTwhile the first checkout's exited container was still there.flock: start, nested agent push, harvest and export../tests/test.sh --all.