Workspace cloning (ghorg)
pi-sandbox workspace builds the workspace ws/<domain>/<host>/<group>/<subgroup…>/<repo> from
whole forge groups with ghorg (pinned in the image) — so
every repository is a sandbox project with its configuration and memory levels (pi-config spec
009). It runs on the Mac, never inside an agent session.
Commands#
pi-sandbox workspace clone <domain> <host>/<group path> [--user] [--include-archived] [--include-forks]
pi-sandbox workspace refresh <domain> [<host>/<group path>]
pi-sandbox workspace list [<domain>]- clone clones every repository of the group and its subgroups that is not there yet, and
refreshes the ones that are (below).
--userclones a GitHub/Codeberg user’s repositories instead of an organisation’s. Archived repositories and forks are skipped unless--include-archived/--include-forks. - refresh repeats
clonewith the options recorded for the group; without a group it runs every group recorded for the domain, one after the other. - list shows per domain the recorded groups (type, repository count, last run and result) and
per host where its token comes from (
cmd,keychainormissing). It reads no token, runs no*_CMDand starts nothing.
Layout#
| Forge | <group path> | Result |
|---|---|---|
| GitLab (gitlab.com or self-hosted) | group-a or group-a/sub-b | ws/<domain>/<host>/group-a/sub-b/…/<repo> — subgroups keep their folders |
| GitHub, Codeberg/Forgejo/Gitea | one organisation (or user with --user) | ws/<domain>/<host>/<org>/<repo> |
Paths keep the forge’s spelling. After each run, every newly cloned repository’s folder is
compared with the path of its remote (the host name of the clone URL is ignored: a forge may
hand out another one than you addressed, and the remote is rewritten to <host> anyway); a mismatch is reported as failed (path does not match remote) and the folder is left for you to check.
Settings#
Setting (.env) | Values | Default |
|---|---|---|
WS_<ID>_<HOST_ID>_TOKEN_CMD | command whose output is the forge token for that domain and host | — (then the Keychain item ws-<domain>-<host>-token) |
WS_<HOST_ID>_TYPE | gitlab, github, gitea | inferred for gitlab.com, github.com, codeberg.org (a set value must match); required for other hosts |
WS_<HOST_ID>_REMOTE | ssh, https | ssh |
WS_<ID>_<HOST_ID>_REMOTE | ssh, https; this domain only, beats WS_<HOST_ID>_REMOTE | — |
WS_<ID>_<HOST_ID>_PUSH_KEY / _PUSH_KEY_CMD, WS_<ID>_<HOST_ID>_PUSH_TOKEN_CMD, WS_<ID>_<HOST_ID>_PUSH_USER, WS_<HOST_ID>_KNOWN_HOSTS_FILE | credential for pushing from agent sessions to that domain and host: key (remote ssh) or token (remote https) | off (credentials.md) |
Only the token setting names the domain; type and remote are per host. <ID> is the domain
name in uppercase with - → _; <HOST_ID> the host in uppercase with .
and - → _ (e.g. WS_WORK_GITLAB_EXAMPLE_COM_TOKEN_CMD, WS_GITLAB_EXAMPLE_COM_TYPE=gitlab).
A Keychain item instead of the command:
security add-generic-password -s pi-sandbox -a ws-<domain>-<host>-token -U -w. A token with
read-only repository access is enough.
New repositories get the remote git@<host>:<path>.git (ssh) or https://<host>/<path>.git
without credentials (https); you push and fetch on the Mac with your own credentials.
What happens to existing repositories#
Nothing, unless all of this holds: no uncommitted or untracked changes, no commits that are not
on the forge, the default branch is checked out, and the forge’s branch is a fast-forward. Then
it is fast-forwarded. Everything else is listed as skipped (<reason>) — exists, dirty,
ahead of origin, on branch <b>, diverged, fetch failed, default branch unknown,
remote is not on <host> — and left exactly as it was, including its remote. ghorg runs only with
--no-clean (it never cleans, resets or pulls an existing repository) and never with --prune.
The workspace container and the token#
Clone and refresh run in their own container (compose.workspace.yaml): the session image and
hardening (uid 1000, read-only root, no capabilities, no-new-privileges, throwaway home), and
only ws/<domain> mounted — no memory, no configuration store, no agent state, no other
domain. The token is read on the Mac when the command starts and passed to that container in its
environment (WS_TOKEN) only. Inside, ghorg clones over HTTPS with it and resets every remote
without it; refreshes pass it through a credential helper given on the git command line (nothing
written). Afterwards every repository’s git config is checked for the token and cleaned; all
output shows *** instead of it. Agent sessions never get a forge token (isolation self-test:
“no git credentials”).
Record#
ws/<domain>/.pi-workspace/groups.json keeps one entry per <host>/<group path>: forge type,
options, repository count, last run (UTC) and the counts of the last run. No secrets. refresh
and list read it.
Exit codes#
2 invalid input (nothing started), 12 unknown domain, 40 container runtime not reachable,
50 the run finished with failed repositories (listed) or ghorg failed, 51 no token for that
domain and host, or its *_TOKEN_CMD failed or printed nothing — usually a locked password
manager (bw unlock, op signin) — (nothing started).