Group Exchange
Group exchange publishes explicitly selected committed history. It uses the existing
ACC authentication, HTTPS/device boundary and account broker peer checks. It does
not use the receive-only backup endpoint or put Git credentials in agent sessions.
The implementation decision is docs/decisions/0036-group-pack-exchange.md.
Boundaries
| Store under the configured Git root | Contents | Authority |
|---|---|---|
exchange/PROJECT.git |
Published commit histories and refs | Current project group membership; viewers read |
group-backups/PROJECT/SNAPSHOT |
Immutable published recovery and safe group metadata | Maintainers create/restore; members download; viewers see status |
private-groups/PROJECT/PERSON/SNAPSHOT |
Explicit private committed recovery | Placement's person and trusted instance owners, independently of group membership |
Existing PROJECT.git |
Existing personal project backups | Existing personal backup authority |
A group role never opens another person's broker path, transcript, dirty file, private recovery, terminal or legacy Files/Wiki view. Instance administration does not automatically become a group code identity: exchange requires membership. Placement provisioning remains an instance-owner action through the approved broker.
The broker creates 0700 account-owned group directories without extended ACLs or
symlinks. Every session checkout has independent .git metadata; config, hooks,
filters and alternates are not copied. Export reads only the selected commit's
objects through fresh metadata. Import validates in quarantine, then repacks only
that commit's closure. Rejected/unselected objects never enter shared storage.
Published code is untrusted input; no build, submodule initialization or agent
launch occurs during sync. People must review source before later execution.
API contract
Use /api/groups/GROUP/projects/PROJECT as BASE below. Cookie requests use ACC's
normal CSRF header; bootstrap instance tokens do not substitute for membership.
All commit IDs are full SHA-1 hex. Checkout IDs are 12–32 lowercase hex characters;
checkout IDs are durable receipts. The group launch endpoint binds the same ID to one live session after source trust and quota checks.
| Method and path | Request / result |
|---|---|
POST BASE/placements/PLACEMENT/provision |
Owner provisions an existing reservation; no supplied path/account |
POST BASE/exchange/checkout |
placement_id, session_id, commit; own provisioned placement, published tip, fresh standalone checkout |
POST BASE/exchange/publish |
placement_id, session_id, old_sha, new_sha; broker exports selected committed history, service checks fast-forward and expected old ref |
GET BASE/exchange/refs |
Published ref → commit map; no private refs |
POST BASE/exchange/grants |
operation (publish/fetch), optional read placement_id, session_id, old_sha, new_sha; one-use token, derived ref and 60-second expiry |
GET BASE/exchange/git/info/refs?service=git-upload-pack (or git-receive-pack) |
Standard Git HTTP advertisement, scoped bearer grant; current membership rechecked |
POST BASE/exchange/git/git-upload-pack |
Read negotiation/pack from trusted staging containing only the selected published history |
POST BASE/exchange/git/git-receive-pack |
Exact granted ref transition in disposable staging, then selected-history promotion |
POST BASE/exchange/transfer |
Exchange token as bearer; base64 pack in data for publish, empty body for fetch; fetch returns selected published commit's full pack |
POST BASE/approvals |
Phase 1 revision, branch, old_sha, new_sha; currently named project owner approves exact integration result |
POST BASE/exchange/integrate |
approval_id; maintainer applies current named-owner receipt, consumes it, never overwrites changed integration |
POST BASE/exchange/tags |
Maintainer supplies name, published commit; new immutable tag only |
GET BASE/published/tree?commit=SHA |
Regular published file paths; symlinks/submodules omitted |
GET BASE/published/file?commit=SHA&path=PATH |
Bounded UTF-8 text from published history |
GET BASE/published/wiki?commit=SHA&path=Home.md |
Markdown text under docs/wiki; no tree traversal |
POST BASE/exchange/backup |
Maintainer creates published recovery snapshot and safe role/owner metadata |
GET BASE/exchange/backups |
Snapshot IDs, creation times and ref counts; local evidence only |
GET BASE/exchange/backup/SNAPSHOT |
Member/maintainer downloads safe metadata and base64 packs keyed by commit |
POST BASE/exchange/restore |
Maintainer supplies snapshot, backed-up commit; creates a new restore/ID branch |
POST BASE/placements/PLACEMENT/private-backups |
Placement person or trusted instance owner supplies session_id, private commit; returns snapshot ID |
GET BASE/placements/PLACEMENT/private-backups/SNAPSHOT |
That person or trusted instance owner downloads private pack; group roles confer no authority |
For a first publication, old_sha is 40 zeros. Later updates must supply the current
published branch tip. The service derives refs/heads/people/STABLE_PERSON_ID/SESSION_ID;
callers cannot supply another person's namespace or an integration destination.
A stable person component hashes the never-reused username, never a recycled user ID.
There is no force, ref deletion, unscoped Git remote or arbitrary-object fetch. Standard Git clients use
BASE/exchange/git with the scoped grant in an Authorization bearer header supplied
only in process environment. The service initially uses protocol v0. Never run
credentialed transfer inside contributor-controlled config: broker helpers export
through fresh metadata and keep the credential out of the working repository.
The low-level fetch grant accepts no placement for viewers; if supplied, a placement
must belong to the caller. Grants are hashed in memory, capped at 64 outstanding per
person and 4,096 in total, and forgotten on restart. A grant serves one Git HTTP request
at a time (429 otherwise), and at most 8 Git HTTP requests are buffered at once (503).
Only the transfer and Git paths accept bodies over 4 MiB, and transfer checks its grant
before reading the body.
Git pushes must be uncompressed; push options and certificates are not advertised. Pack-helper requests and Git write POSTs spend the token even on refusal. Git
advertisements and intermediate stateless read negotiation can repeat; the read
capability is consumed before its first pack is returned. Every request, including
intermediate negotiation, rechecks current membership. A
completed transfer cannot be replayed. This permits ordinary Git negotiation while
keeping final pack delivery and all mutations one-use.
Fencing, limits and recovery
Service-side Git operations take the existing Git capture lease first, then the database: waiting behind a capture never holds SQLite's writer (503 after 60 s). Writes (publish, receive-pack, integrate, tag, backup, restore) hold a current-membership writer fence through their Git work. Reads recheck group, revision, role, placement and grant without the writer; publications are permanent, so a read racing a removal shows nothing the person could not see a moment earlier. Revision changes invalidate old grants/approvals. Integration also verifies the approver is still a named maintainer. Spent approval records are retained so their IDs cannot be recycled into later receipts. Git and SQLite cannot commit together: after an uncertain response, inspect refs first. If Git advanced before a crash, expected-old CAS refuses approval replay even if the receipt survived. Do not roll back or force a ref to make an old receipt apply.
Packs are complete (not thin), limited to 8 MiB. Before Git inflates anything, a scan of declared sizes refuses objects or delta results over 128 MiB and packs expanding beyond 512 MiB. Only objects the store does not already reach from its refs are promoted, so fast-forwards, tags and integrations add their new objects, not another history copy. Object stores are limited to 128 MiB and published refs to 128. Each person may publish 32 branches; people's publications stop at 112 refs and 112 MiB, leaving the rest for integration, tags and restores. The object-store admission budget includes pack indexes. Text views are limited to 1 MiB, trees to 5,000 files. Git children have 60-second wall/CPU limits, 512 MiB address space, 64 MiB file size and 128 file descriptors. HTTP bodies remain bounded before parsing. Snapshots have a 64 MiB transfer budget, 128 local snapshots and a 512 MiB local store budget with headroom reserved before capture. These fail closed rather than silently pruning recovery. Ask an instance owner to review storage/retention.
The backup server captures these directories under its existing Git lease and validates group/project/person identities in its inventory. Its encrypted off-site retention remains 48 hourly, 30 daily and 12 monthly copies, with owner-only pruning. A local snapshot is not proof of off-site backup or independent protection. Use Backups to inspect host backup results and follow its recovery-kit and held-restore procedure. Dirty files, credentials and native session state are not included. Restore of published code always creates a new branch; integration still needs fresh named-owner approval.
No new migration, machine configuration, account installation or service is needed. The phase 3 candidate adds executable-session bindings, source trust and group coordination. Operator qualification still covers transactional quotas, key delivery, and removal/downgrade stop/cleanup/reconnect before production release. Cross-machine operation remains phase 4.
Broker checkout also reserves expanded content before creating a working tree: 128 MiB of blob payload across paths (duplicate blobs count for every path), 5,000 total tree entries, 1,024-byte paths and 64 components. This bounds disk amplification from small compressed packs; no oversized worktree is created.
Coordinated same-host sessions
Migration 0044 adds checkout receipts, explicit work rows, advisory claim leases, operational notices and launch/resume reservations. Group policy includes concurrent and UTC-day launch quotas, owner ceilings, a 15-minute audited override and a reported-USD warning. Readings come from per-session provider observations; missing data is unknown. The ledger projection is bounded to 100 recent sessions and reports partial coverage when truncated.
POST BASE/sessions accepts checkout_id, explicit task, private prompt, name,
provider (Claude or Codex) and trust_source. The approved clone must belong to the current person
and local account. The supervisor rechecks membership before reservation and dispatch, freezes a
session placement, and uses the checkout ID as session ID. A duplicate returns that ID without another charge.
Manual resumes reserve another launch; uncertain starts retain their concurrency slot. Group helper,
schedule, container and multi-run entry points remain refused.
Under /api/groups/GROUP, board, usage and operations are safe read projections;
policy is maintainer/owner-only. work/SESSION/task, claim, release and context
are own-developer-only (trusted instance owners retain administrative access).
Path validation is shared by server and broker. Viewer projections omit claims and observed paths,
including paths in conflict notices, following decision 0008. Claims serialize overlap checks with insertion,
require explicit shared-work confirmation, expire after 15 minutes, and renew only from broker liveness.
Tasks are bounded explicit text, never opening prompts.
A separate group-context.json attachment refreshes through a fixed broker operation using held
no-follow directory descriptors and atomic replacement. It never rewrites session.env or the frozen
launch spec. Claude hooks return the bounded local snapshot as untrusted additional context; other
agents have a visible explicit-refresh requirement. Broker observations copy bounded index data into fresh
Git metadata, disable filters/external diff/fsmonitor, omit protected paths, and never import source config.
The work spool derives identity from its originating session and records deduplicated bounded receipts.
GET BASE/keys lists eligible consent IDs; POST BASE/keys/KEY/attach requires group administration
and current key-owner consent. Vault delivery rechecks current membership, attachment, consent and local host.
Group role never enumerates private values or broadens grant scope. Removal/downgrade records stop intent
and releases claims in the membership transaction; broker stop/secret cleanup retries survive unavailability.
Terminal checks revalidate authority, and the membership route closes affected live streams.
Security review must cover the runtime changes separately from the phase 1 and 2 reviews. Separate-UID deployment, production provider hooks and credentials remain operator qualification checks.
Own-machine first slice
ACC_GROUPS_REMOTE=1 (or RYNHULT_GROUPS_REMOTE=1) on the main enables the
optional path; absent/false leaves it closed. Migration 0063 (after main’s OIDC follow-ups 0054) adds owner-reviewed
group_machine_account identities and nullable placement references. No existing
placement or personal project acquires group access. Enrollment stays owner-only.
A person requests POST /api/group-machines with node_id and account; only
an instance owner approves POST /api/group-machines/ID/approve against an active
node's currently advertised broker accounts. GET lists one's own records; owners
may review all. Self/owner revoke through .../ID/revoke. Unapproved cancellations
release their reservation. Approved/revoked account identity remains exclusive
while its private files may still exist. No reactivation route is provided.
For a group project BASE, its developer requests POST BASE/remote-placements
with binding_id and current group revision. Maintainer/owner review reads
GET BASE/remote-placements; POST BASE/remote-placements/ID/approve provisions
a derived private path below a broker-reported deployment root. Approval rechecks
membership and revision after IO. .../ID/revoke persists stop/cleanup in the
authorization transaction. Membership departure/viewer downgrade invalidates these
placements. Published exchange routes reuse sanitized, bounded commit packs and
carry the recorded host/account on every broker call. Session routing uses the
frozen placement rather than mutable project host fields.
The launcher's own remote-enabled keys are the only remote group delivery in this slice; private teammate keys, including legacy group grants and MCP references, are excluded. Provider OAuth material remains on its broker account. Partition blocks new remote grants/checkout/launch, not already running offline editing. On reconnect, authority is rechecked before adoption/start; revoked sessions are killed and queued cleanup retries. Broker cleanup errors remain pending. A revoked node key cannot reconnect; retain pending state and arrange owner-run local cleanup. No automatic sync, key-grant expansion, container dependency or live migration. Private recovery endpoints retain their same-host restriction; this slice does not silently authorize remote private-checkout backups.
Qualify real separate OS users, machine administrators, production TLS and native Linux/WSL/macOS before rollout; simulated nodes do not certify those boundaries.
Local account-control proof
Migration 0064, chained after 0063, adds nullable group_machine_account.proof_verified_at. Earlier
approvals are preserved but unverified: no remote placement or session authority
is usable until that binding's person supplies proof. The historical 0063 table
DDL is frozen so model additions cannot leak into an earlier migration.
rynhult-node group-proof --request ID --node-id NODE_ID --run-dir BROKER_RUN_DIR
runs locally as the ordinary requested account. It generates a ten-character code
without reading enrollment keys or talking to the main. The account's broker run
directory holds .group-proof.json (0600, account-owned, no symlinks/hard links),
containing the digest, exact identity, expiry, counters and the last 32 local
receipts. Directory and file ownership/mode checks and a file lock gate access.
Only the local command displays the code; group_machine_verify is the only new
broker method and returns a boolean. Services brokers do not expose it.
The browser submits to POST /api/group-machines/{binding_id}/verify. Only the
requesting human can verify pending or older approved unverified bindings. The
main selects the active immutable node/account and rechecks authority after I/O.
Short database transactions run in workers; none span the broker call. Five-minute
expiry, one issuance per minute, five failed checks per challenge and ten API
attempts per person per minute survive restarts. Unrelated request IDs cannot
exhaust another request's challenge. Verification attempts/results are audited
without the code or digest. Owners see explicit verification state and cannot
approve unverified bindings. See decision 0068 and the Guide's own-machine steps.
Upgrade the main and each participating node before testing this flow. Older
brokers refuse the unknown method, leaving the request unverified. Keep
ACC_GROUPS_REMOTE off until native qualification; this feature adds no sudo or
socket permissions. Treat loss/corruption of the local proof file as unavailable
proof, never as successful verification.
The phase 4 replay chains directly after 0054; it does not wait for SCIM, which is parked until after v1. Migration upgrades leave own-machine groups off by default.
This page is generated from docs/wiki/Internal/Group-Exchange.md; the same page is under Guide inside Rynhult.