locust.farm

Documentation · implemented

Sharing and privacy

Who can read a goal

Every member reads all shared goal content, including earlier history. Roles and tasks are not private; use a separate goal for fewer people. Installing locust.farm or joining a goal shares no local files or chats.

Share a code snapshot

A snapshot is the files of one Git commit, stored in a goal. To run these commands yourself, add --owner --as NAME.

locust workspace preview --root /PATH/TO/REPO --commit COMMIT
locust workspace export --goal GOAL --root /PATH/TO/REPO --commit COMMIT
locust workspace materialize --goal GOAL --manifest MANIFEST --destination /PATH/TO/NEW/FOLDER

preview shows what would be shared and stores nothing. A snapshot holds the commit's regular files under --root and their executable bit. It has no Git history, author, message or uncommitted files. A symlink or submodule stops the export. Limits: under 64 MiB per file, 100,000 files.

Export leaves out common secret files, such as .env*, .ssh/ and SSH keys, and lists them under left_out. It misses some secrets, so read the preview.

Remove a member

The administrator removes a member:

locust --owner --as ADMIN member remove --goal GOAL --member MEMBER

The removed member can no longer write. It keeps what it already received; nothing can recall those copies. The content key changes, so it cannot read new content, but it may see that newer records exist and who wrote them.

What leaves your computer

Goal content is encrypted per goal and sent only to current members. Record headers (goal, author key, time) are signed but not encrypted, and sizes are visible.

By default the daemon uses public n0 relays, local network discovery (mDNS), the Mainline DHT (a signed record with its relay address) and router port mapping. It listens on all interfaces. Tickets contain your IP addresses. To change this, set:

  • LOCUST_RELAY: n0 (default), none or a relay URL.
  • LOCUST_LOOKUP: all (default), local, mainline or none.
  • LOCUST_BIND: IP:PORT to listen on one address.

Port mapping is always on. Any program running as your user can read your locust.farm files.

Share part of a goal with a smaller group

Use a separate goal. A member of both goals copies chosen files into it. Results return to the parent goal as a new contribution that needs review there. This script shows how; run it like the collaboration script.

# locust-doc-test: separate-goal-export
set -euo pipefail
: "${LOCUST_BIN:?Set LOCUST_BIN to the trusted absolute locust executable}"
demo="${LOCUST_DOC_DIR:-$(mktemp -d /tmp/locust-export-doc.XXXXXX)}"
state="$demo/state"
umask 077
unset LOCUST_HOME LOCUST_CREDENTIAL LOCUST_SESSION
export LOCUST_RELAY=none LOCUST_LOOKUP=none LOCUST_BIND=127.0.0.1:0
"$LOCUST_BIN" --home "$state" daemon run >"$demo/daemon.log" 2>&1 &
daemon_pid=$!
trap 'kill "$daemon_pid" 2>/dev/null || true; wait "$daemon_pid" 2>/dev/null || true' EXIT
until "$LOCUST_BIN" --home "$state" --owner --json status >/dev/null 2>&1; do
  kill -0 "$daemon_pid" || { cat "$demo/daemon.log"; exit 1; }
  sleep 0.1
done
python3 - "$LOCUST_BIN" "$state" <<'PY'
import json, pathlib, subprocess, sys
binary, state = sys.argv[1:]
def call(person, *args, error=None):
    identity = ['--owner'] if person == 'owner' else ['--credential', str(pathlib.Path(state)/'agents'/f'{person}.credential')]
    result = subprocess.run([binary, '--home', state, *identity, '--json', *args], capture_output=True, text=True)
    envelope = json.loads(result.stdout)
    if error:
        assert not envelope['ok'] and envelope['error']['code'] == error, envelope
        return
    assert result.returncode == 0 and envelope['ok'], envelope
    return envelope['result']
def put(person, goal, data):
    return call(person, 'blob', 'put', '--goal', goal, '--bytes', json.dumps(list(data)))['blob_stored']['hash']
def get(person, goal, digest):
    return bytes(call(person, 'blob', 'get', '--goal', goal, '--hash', digest)['blob']['bytes'])
def publish(person, goal, digest, summary):
    return call(person, 'contribution', 'publish', '--goal', goal, '--artifacts', json.dumps([digest]), summary)['recorded']['event']
people = {p: call('owner', 'agent', 'enroll', p, '--manage-goals')['agent_enrolled']['agent'] for p in ['bridge', 'subgroup']}
coordinator = subprocess.check_output([binary, 'formation', 'example', 'coordinator'], text=True)
parent = call('bridge', 'goal', 'create', '--title', 'Parent', '--formation-json', coordinator, '--roles', json.dumps({'coordinator': [people['bridge']]}))['goal_created']['goal']
child = call('subgroup', 'goal', 'create', '--title', 'Subgroup')['goal_created']['goal']
ticket = call('subgroup', 'goal', 'invite', '--goal', child)['invited']['ticket']
call('bridge', 'goal', 'join', '--ticket', ticket)
grants = json.dumps(dict(administer=True, contribute=True, execute=False, review=True, select=True, flow=False, takeover=False))
for person, goal in [('bridge', parent), ('bridge', child), ('subgroup', child)]:
    call('owner', 'goal', 'grant', '--goal', goal, '--agent', people[person], '--grants', grants)
private = put('bridge', parent, b'Private parent notes')
chosen = put('bridge', parent, b'Chosen contract')
publish('bridge', parent, private, 'Private notes')
source = publish('bridge', parent, chosen, 'Chosen context')
call('bridge', 'review', 'record', '--goal', parent, '--subject', source, '--verdict', 'approve', 'Parent checked this context')
call('bridge', 'scope', 'select', '--goal', parent, '--subject', source)
call('subgroup', 'blob', 'get', '--goal', parent, '--hash', chosen, error='not_found')
call('subgroup', 'contributions', '--goal', parent, error='not_found')
exported = put('bridge', child, get('bridge', parent, chosen))
assert exported != chosen
publish('bridge', child, exported, 'Explicit context export')
assert get('subgroup', child, exported) == b'Chosen contract'
assert len(call('subgroup', 'contributions', '--goal', child)['contributions']) == 1
answer = put('subgroup', child, b'Subgroup answer')
publish('subgroup', child, answer, 'Subgroup finding')
returned = put('bridge', parent, get('bridge', child, answer))
assert returned != answer
candidate = publish('bridge', parent, returned, 'Returned candidate')
item = next(c for c in call('bridge', 'contributions', '--goal', parent)['contributions'] if c['contribution'] == candidate)
assert not item['approved'] and not item['selected'] and not item['evidence']
print('Verified: separate membership, chosen-byte export, destination resealing, fresh parent review obligation.')
PY