# Rho Git Collaboration Tutorial
This is the intended end-to-end Rho workflow for a GitHub-backed project:
- a project owner creates a Rho repo and publishes policy
- a collaborator joins by submitting their identity in a PR
- the owner admits the collaborator
- the owner publishes mock data while keeping real data private
- the collaborator writes analysis code against the mock data
- the collaborator requests a protected real-data run
- the owner reviews the PR, validates policy, converts a bounded receiver-side proposal into an exact action, approves it, runs it in the sandbox, and releases the result
The current automated version of this flow is:
```bash
RHO_LOCAL_GIT_PI_LIVE=1 bash tests/e2e/local-git-pi-sandbox-encrypted.sh
```
That test uses a local bare Git repo instead of GitHub PRs, but the roles and files match the flow below.
## Project Owner
Install Rho:
```bash
git clone git@github.com:<you>/rho.git
cd rho
./install.sh
rho --version
```
Create a GitHub repository, then clone it:
```bash
git clone git@github.com:<owner>/<project>.git
cd <project>
```
Initialize the Rho project:
```bash
rho repo init \
--repo-id rho://repo/github/<owner>/<project> \
--owner rho://id/github/<owner>
```
Create or select your local identity:
```bash
rho id init \
--github <owner> \
--generate-ssh-key \
--display-name "<Your Name>"
```
Export your public identity into the repo:
```bash
mkdir -p rho/participants
rho id export \
--identity rho://id/github/<owner> \
--out rho/participants/<owner>.yaml
```
Add encrypted inbox policy for yourself:
```bash
rho repo protect-path "rho/messages/inbox/<owner>/**" \
--recipient rho://id/github/<owner>
```
Check the repo:
```bash
rho repo doctor --root .
```
Commit and push:
```bash
git add rho.yaml rho .gitattributes
git commit -m "Initialize Rho project"
git push origin main
```
## Project Collaborator
Install Rho:
```bash
git clone git@github.com:<you>/rho.git
cd rho
./install.sh
rho --version
```
Find and clone the project:
```bash
git clone git@github.com:<owner>/<project>.git
cd <project>
```
Install the repo-local crypto filters:
```bash
rho repo install-filters --root .
rho id import rho/participants/<owner>.yaml
```
Create or select your local identity:
```bash
rho id init \
--github <collaborator> \
--generate-ssh-key \
--display-name "<Your Name>"
```
Export your public identity into the repo on a branch:
```bash
git checkout -b <collaborator>/join-rho
mkdir -p rho/participants
rho id export \
--identity rho://id/github/<collaborator> \
--out rho/participants/<collaborator>.yaml
```
Commit, push, and open a PR:
```bash
git add rho/participants/<collaborator>.yaml
git commit -m "Request Rho project access"
git push origin <collaborator>/join-rho
```
Today, this is the join request: a PR containing the collaborator's public identity. Later this should become a higher-level `rho repo join` command that writes the same files.
## Owner: Review Join PR
Fetch the collaborator branch and inspect the identity:
```bash
git fetch origin <collaborator>/join-rho
git checkout -b review/<collaborator>/join-rho origin/<collaborator>/join-rho
rho id import rho/participants/<collaborator>.yaml
rho id show rho://id/github/<collaborator>
```
Verify the GitHub profile claim:
```bash
rho id verify-github --identity rho://id/github/<collaborator>
```
Run doctor:
```bash
rho repo doctor --root .
```
If the identity and PR are acceptable, admit the collaborator by adding the paths they are allowed to receive encrypted messages on:
```bash
rho repo protect-path "rho/messages/inbox/<collaborator>/**" \
--recipient rho://id/github/<collaborator>
```
Commit the owner-controlled policy update and merge the PR:
```bash
git add rho/participants/<collaborator>.yaml rho/policy/permissions.yaml .gitattributes
git commit -m "Admit <collaborator> to Rho project"
git push origin review/<collaborator>/join-rho
```
In the current implementation, the owner is responsible for making this policy update. Later this should be wrapped by an explicit `rho repo admit` command.
## Owner: Add Data
Prepare real data and mock data. The mock data is committed to Git. The real data stays in the owner's private local storage.
```bash
mkdir -p data/private data/mock
$EDITOR data/private/prices-real.csv
$EDITOR data/mock/prices-mock.csv
```
Create the twin dataset:
```bash
rho dataset \
--name prices \
--owner rho://id/github/<owner> \
--real data/private/prices-real.csv \
--mock data/mock/prices-mock.csv \
--share-dir users/<owner>/datasets/share \
--private-dir ~/.rho/projects/<project>/datasets/private
```
Publish the mock dataset into the project:
```bash
rho publish <owner> <dataset-uuid> \
--source-root users \
--target-root datasets
```
Commit and push:
```bash
git add datasets
git commit -m "Publish prices mock dataset"
git push origin main
```
The local test currently uses:
```text
sandbox/live-git-pi-sandbox-encrypted/user1/datasets/public/<dataset-uuid>/mock/prices-mock.csv
sandbox/live-git-pi-sandbox-encrypted/user1-private/users/rho-user1/datasets/private/<dataset-uuid>/real/prices-real.csv
```
## Collaborator: Create Code And Request A Run
Pull the latest project:
```bash
git checkout main
git pull origin main
rho repo install-filters --root .
```
Create code against the mock dataset. You can write this yourself or ask your agent to do it.
Example prompt:
```text
Write a Python script at workspace/sum_prices.py that reads a CSV path from argv[1],
sums the price column, prints only the numeric total, and write a Rho request for
dataset <dataset-uuid>. Do not create a controlled action or tool call.
```
Run against mock data locally:
```bash
python3 workspace/sum_prices.py datasets/<dataset-uuid>/mock/prices-mock.csv
```
Create a branch:
```bash
git checkout -b <collaborator>/prices-analysis
```
Submit the run request. The lower-level command today writes a request manifest; the encrypted inbox message is what the Pi-backed flow writes around it:
```bash
rho request create-run \
--shared-root . \
--id req-<id> \
--from rho://id/github/<collaborator> \
--to rho://id/github/<owner> \
--tool-id run_real \
--dataset-uuid <dataset-uuid> \
--code-path workspace/sum_prices.py \
--command "python3 workspace/sum_prices.py DATASET_CSV" \
--tier real
```
For the Pi-backed flow, the collaborator-side agent writes only code and a request message. It must not create a controlled action or tool call. The live e2e test proves this path: Pi creates `workspace/sum_prices.py` and the encrypted request message, while the trusted owner side later decides whether to construct an action.
Sign the encrypted inbox request if your flow created one:
```bash
rho crypto sign rho/messages/inbox/<owner>/req-<id>/request.yaml \
--identity rho://id/github/<collaborator> \
--out rho/messages/inbox/<owner>/req-<id>/request.rhosig.yaml
git add workspace rho/messages .rho/requests
git commit -m "Request prices real-data run"
git push origin <collaborator>/prices-analysis
```
Open a PR.
## Owner: Review Run PR
Fetch the PR branch:
```bash
git fetch origin <collaborator>/prices-analysis
git checkout -b review/<collaborator>/prices-analysis origin/<collaborator>/prices-analysis
rho repo install-filters --root .
```
Validate the repo state:
```bash
rho repo doctor --root .
```
Review the code:
```bash
sed -n '1,200p' workspace/sum_prices.py
```
Optionally run it against mock data:
```bash
python3 workspace/sum_prices.py datasets/<dataset-uuid>/mock/prices-mock.csv
```
Verify the collaborator signature:
```bash
rho crypto verify rho/messages/inbox/<owner>/req-<id>/request.yaml \
--signature rho/messages/inbox/<owner>/req-<id>/request.rhosig.yaml \
--identity rho://id/github/<collaborator>
```
Have your trusted owner-side agent read the encrypted request and reviewed code if you want help deciding what to do. That agent writes a proposal, not a controlled action. The important boundary is:
- collaborator-side agent writes request/code only
- owner-side agent writes proposal only
- host maps and validates the proposal into `control/outbox/<action-id>.json`
- owner grants the exact action and input hashes
- sandbox execution touches real data only after grant
```bash
mkdir -p host-control/proposals
$EDITOR host-control/proposals/act-<id>.yaml
```
The proposal format is intentionally smaller than a controlled action:
```yaml
version: 1
proposed_action:
action_id: act-<id>
request_id: req-<id>
tool_id: run_real
requested_by: rho://id/github/<collaborator>
requested_for: rho://id/github/<owner>
action_type: run_real_data
script_path: /exec/workspace/sum_prices.py
output_path: /exec/.rho/runs/run-<id>/stdout.txt
summary: Run the reviewed price summing script on real data.
reason: The request signature and script shape were reviewed.
```
Convert the proposal into a controlled action using explicit allowlists:
```bash
rho run proposal-action \
--proposal host-control/proposals/act-<id>.yaml \
--control-root host-control \
--action-id act-<id> \
--map /exec=<owner-exec-workspace> \
--script-root <owner-exec-workspace>/workspace \
--output-root <owner-exec-workspace>/.rho/runs \
--allow-tool run_real \
--allow-requested-by rho://id/github/<collaborator> \
--allow-requested-for rho://id/github/<owner> \
--allow-action-type run_real_data
```
Approve the exact action and input hashes:
```bash
rho run grant-action \
--shared-root . \
--control-root host-control \
--action-id act-<id> \
--granted-by rho://id/github/<owner>
```
Run it in the sandbox:
```bash
RHO_SANDBOX_RUNNER=gondolin rho run controlled-action \
--shared-root <owner-exec-workspace> \
--control-root host-control \
--action-id act-<id> \
--private-root ~/.rho/projects/<project>/datasets/private
```
Release the result back to the collaborator:
```bash
rho result publish \
--shared-root . \
--request-id req-<id> \
--result-id result-<id> \
--from rho://id/github/<owner> \
--to rho://id/github/<collaborator> \
--artifact .rho/runs/run-<id>/stdout.txt
```
Commit and push:
```bash
git add rho/messages .rho/results
git commit -m "Release prices run result"
git push origin main
```
## Collaborator: Read Result
Pull the owner release:
```bash
git checkout main
git pull origin main
rho repo install-filters --root .
rho repo doctor --root .
```
Read the decrypted result:
```bash
sed -n '1,120p' rho/messages/inbox/<collaborator>/req-<id>/result.yaml
```
## Local Automated Version
Run the deterministic cached version:
```bash
bash tests/e2e/local-git-pi-sandbox-encrypted.sh
```
Run the live Pi + Gondolin version:
```bash
RHO_LOCAL_GIT_PI_LIVE=1 bash tests/e2e/local-git-pi-sandbox-encrypted.sh
```
Expected final line:
```text
live git pi sandbox encrypted e2e passed
```
The live test creates:
```text
sandbox/live-git-pi-sandbox-encrypted/remote.git
sandbox/live-git-pi-sandbox-encrypted/user1
sandbox/live-git-pi-sandbox-encrypted/user2
sandbox/live-git-pi-sandbox-encrypted/user1-review
sandbox/live-git-pi-sandbox-encrypted/user1-exec
sandbox/live-git-pi-sandbox-encrypted/user1-release
sandbox/live-git-pi-sandbox-encrypted/user2-review
```
It also writes Gondolin policy artifacts for the two live Pi calls:
```text
sandbox/live-git-pi-sandbox-encrypted/user2-agent-policy.json
sandbox/live-git-pi-sandbox-encrypted/user1-review-agent-policy.json
```
Those policies record the explicit host filesystem mounts, fixed environment, default-deny network policy, synthetic DNS, disabled websockets, and allowed outbound hosts used by `rho agent-run --sandbox`.
Those folders map to the GitHub flow:
```text
remote.git GitHub repo
user1 owner clone
user2 collaborator clone
user1-review owner checkout of collaborator PR
user1-exec owner execution workspace
user1-release owner result branch
user2-review collaborator checkout after release
```
## Current Gaps
- Join and admit are still composed from `rho id export`, Git commits, and `rho repo protect-path`. They should become first-class commands.
- The GitHub PR version is not automated yet. The e2e test uses local branches and a local bare remote.
- Release/publish actions still need the same proposal-to-controlled-action boundary as run actions.
- The tutorial shows the target human flow. The automated test is the source of truth for the exact commands that currently pass end to end.