Skip to content

feat(join): datacore join -- a new machine as a mirror of the existing install - #16

Open
plur9 wants to merge 3 commits into
fix/second-machine-from-snapshotfrom
feat/datacore-join
Open

plur9 wants to merge 3 commits into
fix/second-machine-from-snapshotfrom
feat/datacore-join

Conversation

@plur9

@plur9 plur9 commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Stacked on #15 (base fix/second-machine-from-snapshot).

One command makes a new machine a mirror of an existing Datacore install: the same person, the same access, so sessions there behave the same. It automates the manual second-machine steps from the 2026-10-23 install test.

On the existing machine: datacore join --prepare <new-machine>

  • Adds the machine to the private roster (infrastructure.yaml: kind workstation, ssh alias, its own ledger actor) and to the owner principal's hosts / writes_as in principals.yaml. Comments are kept, and backups go to ~/.datacore/state/.
  • Refuses before changing anything when:
    • the actor is already written by another machine or principal (two machines writing as one actor fork that writer's log);
    • the actor is this machine's own;
    • the name is already on the roster under another actor;
    • a registry file does not parse.
  • Writes ONE bundle (mode 600, default ~/.datacore/join/<name>.join.yaml). It holds the snapshot with the private registry, plus the gitignored local files: .datacore/config/*.local.yaml, manifest.local.yaml, install.yaml roles and every CLAUDE.local.md. Every carried file and the bundle itself are scanned for secrets; a hit names the file and line, never the value, and stops before anything changes.
  • Prints the scp line and the one command to run on the new machine. --dry-run checks everything and writes nothing.

On the new machine: datacore join <bundle>

  1. init --from-snapshot (or restore onto an existing install), then moves any detached clone back onto its branch.
  2. Local files, using the WHOLE / MERGE / ALLOWED lists of .datacore/lib/local_files_sync.py, read from the install when present (a built-in copy otherwise). It never overwrites a file the machine already has. Roster files gain only the allowed keys they lack, and install.yaml roles only for spaces the machine has. A file the repo would commit is refused.
  3. The same registration on this machine, plus ~/.datacore/identity.env (DATACORE_ACTOR=<actor>). It refuses before any change when this machine already declares another actor, or when the bundle's roster or this machine's roster does not parse.
  4. Credential store: the machine cannot grant itself access. It prints its ssh public key and the owner's store-side steps (copy the source instance manifest to instances/<new>.yaml, push, authorise the key), then waits; exit 2 means it cannot wait, and a rerun continues. Only after that does it clone the store and run scripts/install.sh, creds sync and creds doctor, reporting counts and failing ids only. It never commits or pushes to the store.
  5. Checks: identity resolves to the new actor, every repo (root, spaces, own-repo modules) is on a branch, every space is clean, datacore doctor is green. Then a summary of what is done and what the owner still has to do.

Tests

  • bun test: 227 pass, 0 fail (30 new in tests/join.test.ts, using real git repos). They include the refusal cases: actor collision, the source's own actor, a taken name, unparsable registry (prepare, dry run, and join on both the bundle's and the machine's copy), an existing identity, existing files not overwritten, the secret scan of the bundle, and the store never being written.
  • tsc --noEmit: clean.
  • Read-only run against a real install: the rules were read from local_files_sync.py, 23 files were collected, there were 0 secret hits, and --prepare --dry-run planned the registration and left the registry md5 unchanged with no bundle written.

Not covered

The real initDatacore inside join (the tests swap in a stand-in), ssh to a real store host, the default creds binary, the Enter-to-continue wait (needs a TTY), and Windows.

🤖 Generated with Claude Code

plur9 and others added 3 commits October 23, 2026 20:02
…, secret scan, store grant

Fake credential-shaped strings are assembled at runtime, so the source holds
no token-shaped text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the existing machine --prepare registers the new machine (roster entry,
own ledger actor, owner's hosts/writes_as), refusing an actor another
machine writes as or a registry file that does not parse, and writes one
mode-600 bundle: snapshot with the private registry plus the gitignored
local files, scanned for secrets first.

On the new machine join runs init --from-snapshot, lands the local files with
local_files_sync.py's WHOLE/MERGE rules (read from the install when present,
never overwriting a file the machine has), registers it there too, writes
~/.datacore/identity.env, and joins the credential store only after the
owner grants access store-side (prints the key and steps, waits). Ends with
identity, branch, clean-space and doctor checks and a to-do summary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant