# Live GitHub Two-Identity Rehearsal
This is a manual end-to-end rehearsal for running Rho through a real GitHub
repository from one local machine while simulating two users.
Use a throwaway private GitHub repository first.
Assumptions:
- `install.sh` has installed the Rho binaries.
- `rho install-shell` has installed shell helpers.
- Your current shell has been reopened or sourced after `rho install-shell`.
- `rho-use` works in your shell.
- You have two GitHub identities or handles: `<owner>` and `<collab>`.
Rho accepts shorthand IDs in commands. In this tutorial `github/$OWNER` means
`rho://id/github/$OWNER`, and `$OWNER/$REPO` means
`rho://repo/github/$OWNER/$REPO`.
## 0. Setup Vars
In any shell:
```bash
export OWNER=<owner>
export COLLAB=<collab>
export REPO=rho-live-tutorial-test
export OWNER_GH_HOST=github-$OWNER
export COLLAB_GH_HOST=github-$COLLAB
export OWNER_GH_URL=git@$OWNER_GH_HOST:$OWNER/$REPO.git
export COLLAB_GH_URL=git@$COLLAB_GH_HOST:$OWNER/$REPO.git
export COLLAB_FORK_URL=git@$COLLAB_GH_HOST:$COLLAB/$REPO.git
export RHO_REPO_ID=$OWNER/$REPO
```
Configure SSH so Git uses the intended GitHub account for each clone and push.
`rho-use` selects the active Rho identity; it does not change GitHub auth.
`rho gh switch` selects the active `gh` account, and `rho commit` commits with
the commit name/email from the active Rho identity if one is bound, otherwise
from the active `gh` account.
```sshconfig
Host github-<owner>
HostName github.com
User git
IdentityFile ~/.ssh/<owner-github-key>
IdentitiesOnly yes
Host github-<collab>
HostName github.com
User git
IdentityFile ~/.ssh/<collab-github-key>
IdentitiesOnly yes
```
Create the GitHub repo as the owner through the GitHub UI, or with `gh`:
```bash
rho gh switch "$OWNER"
gh repo create "$OWNER/$REPO" --private --confirm
```
For a private repository, the collaborator GitHub account must be able to read
the repo before it can clone or fork it. Invite the collaborator as a GitHub
outside collaborator, or use a public throwaway repository. Rho membership is
separate from GitHub repository access.
## 1. Owner: Init Repo
Terminal 1:
```bash
rho-use github/$OWNER
rho gh switch "$OWNER"
mkdir -p ~/rho-tutorial
cd ~/rho-tutorial
git clone "$OWNER_GH_URL" owner-main
cd owner-main
rho id init github/$OWNER
```
`rho id init` prints a `github profile proof URL` like:
```text
https://github.com/<owner>#rho.claim/id/github/<owner>/key/ssh-ed25519/sha256-...
```
It also asks whether to bind the active `gh` account for future `rho commit`
commands. Accept the default if the active `gh` account is the same person as
the Rho identity you just created.
Copy that full URL into your public GitHub profile where it is visible on
`https://github.com/<owner>`. The fragment starting with `rho.claim/...` is the
claim Rho verifies.
Verify the identity, initialize the repo using detected defaults, add yourself
as a participant, and protect your inbox:
```bash
rho id verify-github --identity github/$OWNER
rho id list
rho repo init --repo-id "$RHO_REPO_ID" --yes
rho add user github/$OWNER
rho repo protect-path "rho/messages/inbox/$OWNER/**" \
--recipient github/$OWNER
```
Before the first commit, prove the protected inbox is encrypted in Git while
still readable in your working tree:
```bash
mkdir -p "rho/messages/inbox/$OWNER/self-test"
cat > "rho/messages/inbox/$OWNER/self-test/message.yaml" <<EOF
version: 1
message:
from: rho://id/github/$OWNER
to: rho://id/github/$OWNER
type: self-test
body: "hello encrypted rho inbox"
EOF
git add "rho/messages/inbox/$OWNER/self-test/message.yaml"
# Plaintext working-tree view for the active local identity.
cat "rho/messages/inbox/$OWNER/self-test/message.yaml"
# Rho storage view: what Git stages after the clean filter runs.
rho status
rho crypto view "rho/messages/inbox/$OWNER/self-test/message.yaml"
```
`cat path` shows the local working-tree file, so it remains plaintext for the
active identity. `rho crypto view path` is the Rho equivalent of `git show
:path` for encrypted storage. It should show a `rho_recipient_envelope` with
ciphertext, not the plaintext message body. By default `rho-use` uses the
shared `~/.rho` home unless you pass `--home`.
If a GUI Git client such as SourceTree still reports a `.rho-env` path, reinstall
the repo filters from this repository:
```bash
rho-use github/$OWNER
rho repo protect-path "rho/messages/inbox/$OWNER/**" \
--recipient github/$OWNER
git config --local --get-regexp '^filter\.rho-crypt\.'
```
The filter commands should point at `~/.rho`, or the explicit home you chose,
not an old `.rho-env` directory.
Commit and push:
```bash
git add .
rho commit -m "Initialize Rho project"
# Committed storage view: the Git object is encrypted.
git show HEAD:rho/messages/inbox/$OWNER/self-test/message.yaml
# Working-tree view: the active identity still sees plaintext.
cat "rho/messages/inbox/$OWNER/self-test/message.yaml"
git push origin main
```
## 2. Collaborator: Join PR
Terminal 2:
```bash
rho-use github/$COLLAB
cd ~/rho-tutorial
rho gh switch "$COLLAB"
gh repo fork "$OWNER/$REPO" --clone=false
git clone "$COLLAB_GH_URL" collab-join
cd collab-join
git remote rename origin upstream
git remote add origin "$COLLAB_FORK_URL"
rho repo init --yes
rho id init github/$COLLAB
```
Copy the full `github profile proof URL` from `rho id init` into the
collaborator GitHub profile. If prompted, bind the active collaborator `gh`
account so `rho commit` uses the collaborator commit author. Then verify it
before committing the participant file:
```bash
rho id verify-github --identity github/$COLLAB
rho id list
git checkout -b "$COLLAB/join-rho"
rho add user github/$COLLAB
git add rho/participants/$COLLAB.yaml rho/membership.yaml
rho commit -m "Request Rho access for $COLLAB"
git push origin "$COLLAB/join-rho"
```
Open a PR on GitHub from `$COLLAB:$COLLAB/join-rho` into `$OWNER:main`.
## 3. Owner: Admit Collaborator
Terminal 1:
```bash
rho-use github/$OWNER
rho gh switch "$OWNER"
cd ~/rho-tutorial
git clone "$OWNER_GH_URL" owner-review-join
cd owner-review-join
rho repo init --yes
git fetch "$COLLAB_FORK_URL" "$COLLAB/join-rho"
git checkout -b "review/$COLLAB/join-rho" FETCH_HEAD
rho id show github/$COLLAB
rho id verify-github --identity github/$COLLAB
rho add user github/$COLLAB
rho repo protect-path "rho/messages/inbox/$COLLAB/**" \
--recipient github/$COLLAB
rho repo doctor
git add .gitattributes rho
rho commit -m "Admit $COLLAB to Rho project"
git push origin "review/$COLLAB/join-rho"
```
Merge that PR into `main`.
## 4. Owner: Add Mock Dataset
Terminal 1:
```bash
rho-use github/$OWNER
rho gh switch "$OWNER"
cd ~/rho-tutorial
git clone "$OWNER_GH_URL" owner-data
cd owner-data
git pull origin main
rho repo init --yes
mkdir -p data/private data/mock
cat > data/private/prices-real.csv <<'EOF'
date,symbol,price
2026-01-01,A,100.01
2026-01-02,B,200.02
2026-01-03,C,813.00
EOF
cat > data/mock/prices-mock.csv <<'EOF'
date,symbol,price
2026-01-01,A,10.00
2026-01-02,B,20.00
EOF
rho dataset \
--name prices \
--owner github/$OWNER \
--real data/private/prices-real.csv \
--mock data/mock/prices-mock.csv \
--share-dir users/$OWNER/datasets/share \
--private-dir users/$OWNER/datasets/private \
--uuid "$UUID" \
--description "Tutorial prices dataset"
rho publish "$OWNER" "$UUID" --source-root users --target-root datasets
git add datasets
rho commit -m "Publish prices mock dataset"
git push origin main
echo "$UUID"
```
Save the printed `UUID`.
## 5. Collaborator: Submit Request
Terminal 2:
```bash
rho-use github/$COLLAB
cd ~/rho-tutorial
rho gh switch "$COLLAB"
git clone "$COLLAB_GH_URL" collab-request
cd collab-request
git remote rename origin upstream
git remote add origin "$COLLAB_FORK_URL"
git pull origin main
rho repo init --yes
git checkout -b "$COLLAB/prices-request"
mkdir -p workspace "rho/messages/inbox/$OWNER/req-$UUID"
cat > workspace/sum_prices.py <<'EOF'
import csv
import sys
from decimal import Decimal
total = Decimal("0")
with open(sys.argv[1], newline="") as f:
for row in csv.DictReader(f):
total += Decimal(row["price"])
print(f"{total:.2f}")
EOF
cat > "rho/messages/inbox/$OWNER/req-$UUID/request.yaml" <<EOF
version: 1
request:
id: req-$UUID
from: rho://id/github/$COLLAB
to: rho://id/github/$OWNER
type: private-analysis
dataset_uuid: $UUID
code_path: rho://repo/workspace/sum_prices.py
note: Please run this price sum on real data.
EOF
rho crypto sign "rho/messages/inbox/$OWNER/req-$UUID/request.yaml" \
--out "rho/messages/inbox/$OWNER/req-$UUID/request.rhosig.yaml"
git add workspace rho/messages
rho commit -m "Request private prices analysis"
git push origin "$COLLAB/prices-request"
```
Open a PR from `$COLLAB:$COLLAB/prices-request` into `$OWNER:main`.
## 6. Owner: Review And Run
Terminal 1:
```bash
rho-use github/$OWNER
rho gh switch "$OWNER"
cd ~/rho-tutorial
git clone "$OWNER_GH_URL" owner-review-run
cd owner-review-run
rho repo init --yes
git fetch "$COLLAB_FORK_URL" "$COLLAB/prices-request"
git checkout -b "review/$COLLAB/prices-request" FETCH_HEAD
rho crypto verify "rho/messages/inbox/$OWNER/req-$UUID/request.yaml" \
--signature "rho/messages/inbox/$OWNER/req-$UUID/request.rhosig.yaml" \
--identity github/$COLLAB
python3 workspace/sum_prices.py "datasets/$UUID/mock/prices-mock.csv"
```
Create the execution workspace:
```bash
cd ~/rho-tutorial
export EXEC_ROOT="$PWD/owner-exec"
export OWNER_DATA_ROOT="$PWD/owner-data/users"
export OWNER_PRIVATE_ROOT="$OWNER_DATA_ROOT/$OWNER/datasets/private"
mkdir -p "$EXEC_ROOT/workspace" "$EXEC_ROOT/control/proposals" "$EXEC_ROOT/control/outbox" "$EXEC_ROOT/control/inbox"
cp owner-review-run/workspace/sum_prices.py "$EXEC_ROOT/workspace/"
rho publish "$OWNER" "$UUID" \
--source-root "$OWNER_DATA_ROOT" \
--target-root "$EXEC_ROOT/datasets"
rho tools install-minimal --shared-root "$EXEC_ROOT" --owner github/$OWNER
rho request create-run \
--shared-root "$EXEC_ROOT" \
--id "req-$UUID" \
--from github/$COLLAB \
--to github/$OWNER \
--tool-id run_real \
--dataset-uuid "$UUID" \
--code-path "$EXEC_ROOT/workspace/sum_prices.py" \
--command "python3 sum_prices.py DATASET_CSV" \
--tier real
```
The collaborator sends only the request and code. The owner constructs this
proposal locally on the trusted side after review:
```bash
cat > "$EXEC_ROOT/control/proposals/act-run-real-$UUID.yaml" <<EOF
version: 1
proposed_action:
action_id: act-run-real-$UUID
request_id: req-$UUID
tool_id: run_real
requested_by: github/$COLLAB
requested_for: github/$OWNER
action_type: run_real_data
script_path: /exec/workspace/sum_prices.py
output_path: /exec/.rho/runs/run-run-real-$UUID/stdout.txt
summary: Run reviewed sum_prices.py on real data.
reason: Request signature and script were reviewed.
EOF
rho run proposal-action \
--proposal "$EXEC_ROOT/control/proposals/act-run-real-$UUID.yaml" \
--control-root "$EXEC_ROOT/control" \
--action-id "act-run-real-$UUID" \
--map "/exec=$EXEC_ROOT" \
--script-root "$EXEC_ROOT/workspace" \
--output-root "$EXEC_ROOT/.rho/runs" \
--allow-tool run_real \
--allow-requested-by github/$COLLAB \
--allow-requested-for github/$OWNER \
--allow-action-type run_real_data
rho run grant-action \
--shared-root "$EXEC_ROOT" \
--control-root "$EXEC_ROOT/control" \
--action-id "act-run-real-$UUID" \
--granted-by github/$OWNER \
--input "code:$EXEC_ROOT/workspace/sum_prices.py"
RHO_SANDBOX_RUNNER=gondolin rho run controlled-action \
--shared-root "$EXEC_ROOT" \
--control-root "$EXEC_ROOT/control" \
--action-id "act-run-real-$UUID" \
--private-root "$OWNER_PRIVATE_ROOT"
```
Check the run output:
```bash
cat "$EXEC_ROOT/.rho/runs/run-run-real-$UUID/stdout.txt"
```
Expected:
```text
1113.03
```
After the owner has reviewed the request, verified the signature, and completed
the protected run, merge the request PR into `main`. Create the result release
branch from that updated `main` so the committed result follows the committed
request in Git history.
## 7. Owner: Release Result
Terminal 1:
```bash
rho-use github/$OWNER
rho gh switch "$OWNER"
cd ~/rho-tutorial
git clone "$OWNER_GH_URL" owner-release
cd owner-release
rho repo init --yes
git checkout -b "$OWNER/result-$UUID"
RESULT_DIR="rho/messages/inbox/$COLLAB/req-$UUID"
RUN_ID="run-run-real-$UUID"
mkdir -p "$RESULT_DIR"
RESULT_VALUE="$(cat "$EXEC_ROOT/.rho/runs/$RUN_ID/stdout.txt")"
cat > "$RESULT_DIR/result.yaml" <<EOF
version: 1
result:
request_id: req-$UUID
from: rho://id/github/$OWNER
to: rho://id/github/$COLLAB
output: "$RESULT_VALUE"
run_id: $RUN_ID
runner: gondolin
note: encrypted result released from approved sandbox run
EOF
rho crypto sign "$RESULT_DIR/result.yaml" \
--identity github/$OWNER \
--out "$RESULT_DIR/result.rhosig.yaml"
git add "$RESULT_DIR"
# Confirm the staged Git storage is encrypted and does not expose the result.
rho status
git show ":$RESULT_DIR/result.yaml"
rho commit -m "Release encrypted prices result"
git push origin "$OWNER/result-$UUID"
```
Open a PR from `$OWNER:$OWNER/result-$UUID` into `$OWNER:main`. After the
collaborator has verified it, merge the result PR.
## 8. Collaborator: Verify Result
Terminal 2:
```bash
rho-use github/$COLLAB
rho gh switch "$COLLAB"
cd ~/rho-tutorial
git clone "$COLLAB_GH_URL" collab-result-review
cd collab-result-review
rho repo init --yes
git pull origin main
RESULT_DIR="rho/messages/inbox/$COLLAB/req-$UUID"
# A fresh clone checks files out before Rho installs the Git filters. Refresh
# the result files after `rho repo init --yes` so Git applies the smudge filter.
rm "$RESULT_DIR/result.yaml" "$RESULT_DIR/result.rhosig.yaml"
git checkout HEAD -- "$RESULT_DIR/result.yaml" "$RESULT_DIR/result.rhosig.yaml"
cat "$RESULT_DIR/result.yaml"
rho crypto verify "$RESULT_DIR/result.yaml" \
--signature "$RESULT_DIR/result.rhosig.yaml" \
--identity github/$OWNER
# Committed storage view remains encrypted.
git show "HEAD:$RESULT_DIR/result.yaml"
```
Expected decrypted result:
```text
output: "1113.03"
```