Sharing code — tcm-ts

Sharing code

A hash identifies code completely, so sharing a codebase sends only the hashes the other side lacks, and running code remotely sends only its missing closure.

$ tcm remote add origin https://tcm.example.com --token 8bS…
$ tcm pull                # origin/main into this branch
$ tcm push                # this branch to origin

Remotes

tcm remote add <name> <url|dir> [--token t] [--fingerprint sha256] adds a tcm serve URL or another codebase directory.

  • The token is the server's (its maintainer's) or a contributor's. A public server needs none to pull.
  • Adding a remote of the same name again replaces it (with a new token, say).
  • tcm remote lists remotes; tcm remote rm <name> removes one.
  • A URL used in place of a remote's name (tcm pull https://…, a library's source) is asked with no token. A token goes only to the remote it was added with.

Push and pull

  • tcm pull [remote] [branch] fetches into <remote>/<branch> (e.g. origin/main). If your branch's history is part of theirs, it fast-forwards; if theirs is part of yours, there's nothing to do; otherwise it merges from the common ancestor. A merge that leaves something to settle waits apart, as tcm merge's does; on main, it isn't made.
  • tcm push [remote] [--as branch] [--force] sends this branch's history and moves the remote branch to it. It only moves a remote forward: if the remote has history you don't, it refuses (pull first). --force replaces the remote branch's head.
  • Sync is by hash, history included. What's sent is what the other side lacks: the branch's history entries, the definitions they name, and the package lock entries they use (versions and tarball hashes: packages). Both sides end with the same history, hash for hash.
  • A push sends every version its history names. A branch whose sessions landed squashed (as tcm_merge lands them, agents) sends what each landing ended with. One merged without --squash sends every step too.
  • Everything named travels: code and the codebase's files (pages, images, configuration). A file is checked by its content: its text and links must match its hash, and bytes their digest.
  • Test results and lint findings travel too. A push sends the branch's test results the remote lacks (with the Node and Vitest versions and platform they ran on), and a pull fetches the ones this machine can use, so tests that passed there don't run again here (testing). Lint findings travel the same way, under the linter's version and configuration (linting). A remote on an older tcm that doesn't keep them syncs as before.
  • Production builds, watch values and kept constants don't travel. Each machine makes its own: a kept constant's value is code, which isn't taken on trust.
  • Pushing to a server running tcm start deploys (production).
  • To propose changes for review, tcm contribute pushes a branch and opens a contribution on the remote (contributions).

Serving a codebase

tcm serve [--port 7737] [--host 127.0.0.1] [--token t] [--no-run] [--public] [--tls [--tls-cert f --tls-key f]]
  [--insecure] [--site dir [--moved path]] [--proxy path=url,…] [--forge path=dir,…]

serves the codebase over HTTP(S).

Who can do what

  • The server's token is its maintainer's. With it, a client can push any branch, pull, merge contributions, manage contributors and, unless --no-run, run code. Without --token or $TCM_TOKEN, the server makes one up and prints it. A token it was given isn't printed, so it stays out of a host's logs.
  • Contributors have tokens of their own, as on Unison Share. tcm user add <handle> (the maintainer's, from any machine with the server's token, or on a codebase directory) makes one and prints it once. A contributor's token:
    • pulls, and pushes only branches under @<handle>/ (tcm push and tcm contribute push cubes as @ada/cubes);
    • opens contributions of them, comments, and closes or reopens its own, all signed @<handle> by the server, whatever the client says;
    • can't push other branches, merge, release, run code, or send test results and lint findings (its pushes leave them out).
  • tcm user says who you are on a remote, and lists its contributors to its maintainer. tcm user rm <handle> stops a token working. The server stores each token's SHA-256 digest, not the token.
  • --public lets anyone pull, and read contributions, without a token. Writing still needs one, and a wrong token is refused rather than treated as none. tcm's own server is public (developing).

TLS

  • --tls uses --tls-cert/--tls-key, or creates a self-signed certificate and prints its fingerprint. Clients pin it with tcm remote add … --fingerprint; the token is only sent after the pin checks out.
  • Serving beyond localhost without TLS is refused unless --insecure.

The web UI, a site, apps and other codebases

  • /forge/ is the web UI: the codebase's pages, search, history and contribution review. / goes to it on a server with no --site.
  • --site <dir> serves a directory of static files to anyone, for GET requests: a docs site beside the codebase. tcm's own server serves its docs this way (developing).
    • / and directories serve their index.html; /cli serves cli.html (before a directory of that name). Nothing outside the directory is served, and /forge/ stays the web UI's.
    • A path with no file gets the nearest 404.html above it, so a static site's own 404.html renders its app's other paths.
    • A directory asked for without its slash (/docs) redirects (301) to it with one, so its relative links work.
    • Each file's ETag is its content (asking again is a 304). A file named by its content (style.<digest>.css, 12 or more hex digits of its SHA-256) is cached for good (immutable). Text is sent brotli- or gzip-compressed.
    • Everything else still needs a token (or --public, to read).
  • --moved /docs says the site's pages moved under a path. A GET for a path the site has no file for, but has under /docs (or that an app it proxies serves there), redirects there (301, query kept), so old links keep working.
  • --proxy /examples/notes=http://127.0.0.1:7801,… passes every request under a path, from anyone, to a server beside it (an app tcm start --base /examples/notes serves there).
    • Several servers, |-separated (=http://127.0.0.1:7801|http://127.0.0.1:7901), are an app's version and its next while one replaces the other. The one that answered last is asked first; a request one refuses goes to the next.
    • Method, headers (with the browser's Host, and x-forwarded-for/-proto/-host) and body are streamed both ways. Uncompressed text replies (pages, server component trees, JSON, scripts, styles, SVG) are gzipped as they stream.
    • The longest path that holds a request wins.
    • While the server behind isn't answering (starting, or rebuilding after a deploy), a request gets a 503 with Retry-After: 5, and a page says so and reloads by itself.
  • --forge /code/notes=../notes,… shows other codebases, each in a web UI of its own under a path: read by anyone, signed in to by no one, nothing to pull. tcm's own server shows its examples so (developing).

Transfer limits

  • Big bodies travel gzipped, both ways, between tcms that support it. A compressed request is held to the server's 64 MB limit unpacked.
  • Definitions travel without what the receiver derives itself, about halving a push, between tcms that support it. Older tcms on either side get everything whole.
  • A push sends definitions in batches of up to 40 MB and waits up to 10 minutes for each to be checked. Other requests wait a minute.
  • Idle connections stay open for 65 seconds, and a client resends a request once if its reused connection was closed meanwhile.

Listing a codebase: tcm announce

tcm announce [url] [--name n] [--description d] [--as <handle>] makes a served codebase public. It writes a codes.tcm.codebase record to your AT Protocol repository naming the server it's at. Registries then list it by your handle and its name (@alice.dev/notes) and show its main branch, mirrored from that server and followed as it moves. Its history and contributions are on the server's own web UI. It's like announcing a release, without versions.

$ tcm announce https://code.alice.dev --name notes --description "Notes, and the app that keeps them."
listed notes as @alice.dev: at://did:plc:…/codes.tcm.codebase/notes
  served at https://code.alice.dev
  • Where it's served: a tcm serve URL (http or https). It should be --public: it's read without a token as it's listed, and you're told if it can't be. The default is the URL given last, or origin's.
  • Its name is the record's key, so one name is one codebase of yours: letters, digits, ., _ and -, 64 at most. The default is the project directory's name. The description is a line or two, 300 characters at most.
  • They're kept: the URL, name and description, so tcm announce alone lists it again. A new URL or description replaces the record's; listing it again unchanged changes nothing.
  • You sign in once, in the browser, as for tcm release --announce; the sign-in may write tcm's records and nothing else. Or use an app password, TCM_ATPROTO_APP_PASSWORD (releases). --as takes a handle or DID; the default is the account used last.
  • --withdraw deletes the record, and registries stop listing it.

Running remotely

tcm run <name> [args] --on <remote|url|dir> sends what the remote lacks of the definition's closure, runs it there, and streams stdout/stderr back.

Sync never trusts the sender

Received definitions are checked before anything is stored, from every sender, the server's maintainer included:

  • each component must arrive complete;
  • every reference must resolve, to a definition already there or one that came with it;
  • analyzing them locally (resolving every name they use, deriving their IR) and hashing must reproduce the claimed hashes.

So what's stored under a hash is the code that hash is of.

  • Everything derived from source is derived here: the IR, dependencies, signatures, schemas and how a definition renders. Tampered source sent under an existing hash is rejected, and IR sent beside honest source is ignored.
  • Nothing received runs while it's checked, not even a pure constant whose value tcm keeps (that's computed when the code runs). npm installs use --ignore-scripts.
  • Checking runs in parallel. TCM_CHECKERS sets how many checker processes (default: half the machine's cores, up to 4; 1 checks in tcm's own process). Each takes a slot of the machine's shared budget (testing). A long history needn't fit in memory: definitions are checked a batch at a time.

Types are judged where code is merged, not where it's received:

  • a branch its owner pushes was typechecked where it was saved (tcm saves only code that typechecks), and the server doesn't check it again;
  • a contribution is typechecked when it's merged, under each definition's recorded check profile. A merge that doesn't typecheck is refused, with the errors;
  • code no tcm would save can still be posted by hand over HTTP. The server keeps it on the branch it was pushed to, and merging is where it's caught (limitations). tcm verify on that branch reports its type errors.

Built from c482a14e