Documentation
Practical guide to graph.storage for agentic workflows, multi-tenant platforms, and compartmentalized access.
1. Overview
graph.storage is a git proxy that enforces per-user access control at the file level. It sits in front of a bare git repository and rewrites tree objects so each user sees only the files they are allowed to read. Push operations are filtered in reverse: modified files must fall within the user's write patterns. The REST API provides file browsing, commit history, notes, and webhooks - all ACL-filtered - so agents can interact with the repo without running git at all.
Designed for: AI agents that need isolated workspaces, multi-tenant platforms with per-customer data, and teams where each member operates in a compartmentalized directory.
2. Core Concepts
Workspaces
Directories are the access boundary. Each agent or user gets its own directory
(e.g. agent-alpha/) plus optional shared directories. The directory
structure is the access control signal.
repo/
shared/ # readable by all, writable by all
agent-alpha/ # only agent-alpha can read and write
agent-beta/ # only agent-beta can read and write
secrets/ # denied to everyone via deny patterns
Tokens
JWT tokens carry identity (subject), scopes, and path patterns. The proxy validates the token on every request. Tokens are minted via the admin API or CLI, with configurable TTL.
Scopes
| Scope | Permission |
|---|---|
git:read | Clone, fetch, pull, browse files via API |
git:write | Push, add notes, register webhooks (requires git:read) |
Deny Patterns
Deny patterns override both read and write allows. A file matching any deny pattern is invisible regardless of other rules. Use deny patterns for secrets, credentials, and infrastructure files that no agent should access.
Ephemeral Branches
Agents can push to private branches (e.g. refs/heads/agent-alpha/draft)
for scratch work. Combine with webhooks to notify a supervisor when a branch is
created or updated.
3. Agent Workflows
Setting Up a Multi-Agent Workspace
An admin provisions users, sets ACL rules, and mints tokens. Each agent then operates independently in its own directory.
Step 1: Create users for each agent
# Create agent users
curl -X POST http://localhost:8443/api/users \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"username": "agent-alpha", "display_name": "Alpha Agent", "role": "user"}'
curl -X POST http://localhost:8443/api/users \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"username": "agent-beta", "display_name": "Beta Agent", "role": "user"}'
curl -X POST http://localhost:8443/api/users \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"username": "supervisor", "display_name": "Supervisor", "role": "admin"}'
Step 2: Set ACL per agent
# agent-alpha: reads shared + own dir, writes own dir
curl -X PUT http://localhost:8443/api/acl/agent-alpha \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"repo": "workspace",
"paths_read": "shared/**,agent-alpha/**",
"paths_write": "agent-alpha/**",
"deny": "secrets/**",
"scopes": "git:read,git:write"
}'
# agent-beta: same pattern, own directory
curl -X PUT http://localhost:8443/api/acl/agent-beta \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"repo": "workspace",
"paths_read": "shared/**,agent-beta/**",
"paths_write": "agent-beta/**",
"deny": "secrets/**",
"scopes": "git:read,git:write"
}'
# supervisor: reads everything, writes shared
curl -X PUT http://localhost:8443/api/acl/supervisor \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"repo": "workspace",
"paths_read": "**",
"paths_write": "shared/**",
"deny": "",
"scopes": "git:read,git:write"
}'
Step 3: Mint tokens
# Mint a 7-day token for agent-alpha
curl -X POST http://localhost:8443/api/tokens \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "agent-alpha",
"repo": "workspace",
"scopes": "git:read,git:write",
"ttl": 604800,
"label": "agent-alpha-weekly"
}'
# Response: {"token": {"jti": "...", "username": "agent-alpha", ...}}
Step 4: Each agent clones with its token
# agent-alpha clones - only sees shared/ and agent-alpha/
git clone http://t:${ALPHA_TOKEN}@localhost:8443/repo workspace
cd workspace
ls
# shared/ agent-alpha/
# agent-beta clones - only sees shared/ and agent-beta/
git clone http://t:${BETA_TOKEN}@localhost:8443/repo workspace
cd workspace
ls
# shared/ agent-beta/
Step 5: Each agent pushes to its own directory
# agent-alpha writes results
echo "analysis complete" > agent-alpha/results.md
git add agent-alpha/results.md
git commit -m "alpha: analysis results"
git push # succeeds - writing to own directory
# agent-alpha tries to write to agent-beta's directory
echo "oops" > agent-beta/hack.txt # file doesn't even exist in clone
# push would be rejected even if attempted
Step 6: Supervisor reviews via REST API
# Supervisor lists all files (sees everything)
curl http://localhost:8443/api/files?ref=main \
-H "Authorization: Bearer $SUPERVISOR_TOKEN"
# Supervisor reads agent-alpha's output
curl http://localhost:8443/api/file?ref=main&path=agent-alpha/results.md \
-H "Authorization: Bearer $SUPERVISOR_TOKEN"
# Supervisor checks recent commits
curl "http://localhost:8443/api/commits?ref=main&limit=10" \
-H "Authorization: Bearer $SUPERVISOR_TOKEN"
Agent Reading Files Without Git
Agents that cannot run git use the REST API to browse and read files directly.
# List all files the agent can see
curl http://localhost:8443/api/files?ref=main \
-H "Authorization: Bearer $AGENT_TOKEN"
# Response: {"files": [
# {"path": "shared/config.yaml", "size": 1024, "sha": "abc123..."},
# {"path": "agent-alpha/results.md", "size": 512, "sha": "def456..."}
# ], "ref": "main"}
# Read a specific file
curl http://localhost:8443/api/file?ref=main&path=shared/config.yaml \
-H "Authorization: Bearer $AGENT_TOKEN"
# Response: raw file content (application/octet-stream)
# Check commit history
curl "http://localhost:8443/api/commits?ref=main&limit=5" \
-H "Authorization: Bearer $AGENT_TOKEN"
# Response: {"commits": [
# {"sha": "abc...", "message": "update config", "author": "...", "time": 1710000000}
# ], "ref": "main", "head_sha": "abc..."}
# Poll for new commits using expected_head_sha
curl "http://localhost:8443/api/commits?ref=main&expected_head_sha=abc123" \
-H "Authorization: Bearer $AGENT_TOKEN"
# Returns 409 if the branch has moved since the expected SHA
Agent Writing Notes Without Git
Agents annotate commits with structured metadata via the notes API. Notes
are stored in refs/notes/commits and visible to all users with
read access.
# Add a note to a commit
curl -X POST http://localhost:8443/api/notes \
-H "Authorization: Bearer $AGENT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"action": "add", "sha": "abc123def456", "note": "reviewed: LGTM"}'
# Append to an existing note
curl -X POST http://localhost:8443/api/notes \
-H "Authorization: Bearer $AGENT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"action": "append", "sha": "abc123def456", "note": "tests passed"}'
# Read a note
curl "http://localhost:8443/api/notes?sha=abc123def456" \
-H "Authorization: Bearer $AGENT_TOKEN"
# Response: {"sha": "abc123def456", "note": "reviewed: LGTM\ntests passed"}
Webhook-Driven Pipelines
Register a webhook to trigger downstream actions when agents push.
# Register a webhook
curl -X POST http://localhost:8443/api/webhooks \
-H "Authorization: Bearer $AGENT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url": "https://hooks.example.com/on-push", "secret": "whsec_abc123"}'
# When agent-alpha pushes, the webhook fires with:
# POST https://hooks.example.com/on-push
# X-Hub-Signature-256: sha256=...
# {"event": "push", "ref": "refs/heads/main", "before": "...", "after": "...",
# "pusher": "agent-alpha", "files": ["agent-alpha/results.md"]}
Read-Only Audit Agent
A token with git:read only can clone, browse, and query history
but cannot push, add notes, or register webhooks.
# Mint read-only token with 24h TTL
curl -X POST http://localhost:8443/api/tokens \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "auditor",
"repo": "workspace",
"scopes": "git:read",
"ttl": 86400,
"label": "daily-audit"
}'
# Auditor can clone and read everything
git clone http://t:${AUDITOR_TOKEN}@localhost:8443/repo audit-copy
# Auditor can browse via API
curl http://localhost:8443/api/files?ref=main \
-H "Authorization: Bearer $AUDITOR_TOKEN"
# Auditor cannot push - rejected with 403
git push # error: authentication required (no write scope)
4. API Reference
File Operations
| Endpoint | Method | Description | Auth |
|---|---|---|---|
/api/files?ref=main | GET |
List visible files with size, SHA, last commit | git:read |
/api/file?ref=main&path=bob/notes.md | GET |
Download raw file content | git:read |
Commit History
| Endpoint | Method | Description | Auth |
|---|---|---|---|
/api/commits?ref=main&limit=20 | GET |
List visible commits (ACL-filtered shadow tree) | git:read |
/api/commits?ref=main&expected_head_sha=X | GET |
Same, but returns 409 if branch moved since X | git:read |
Notes
| Endpoint | Method | Description | Auth |
|---|---|---|---|
/api/notes?sha=abc123 | GET |
Get note attached to a commit | git:read |
/api/notes | POST |
Add, append, or remove a note (body: action, sha, note) | git:write |
Webhooks
| Endpoint | Method | Description | Auth |
|---|---|---|---|
/api/webhooks | GET |
List registered webhooks | git:read |
/api/webhooks | POST |
Register webhook (body: url, secret) | git:write |
/api/webhooks?url=... | DELETE |
Unregister webhook | git:write |
Users (Admin)
| Endpoint | Method | Description | Auth |
|---|---|---|---|
/api/users | GET |
List all users | admin |
/api/users | POST |
Create user (body: username, display_name, role) | admin |
/api/users/:username | GET |
Get user details | admin or self |
/api/users/:username | DELETE |
Delete user (cascades ACL and tokens) | admin |
/api/users/:username/keys | GET |
List SSH keys | admin or self |
/api/users/:username/keys | POST |
Add SSH key (body: key_type, key_data, fingerprint, label) | admin or self |
/api/users/:username/keys/:fp | DELETE |
Delete SSH key | admin |
Repos (Admin)
| Endpoint | Method | Description | Auth |
|---|---|---|---|
/api/repos | GET |
List repos | any |
/api/repos | POST |
Create repo (body: name, path, description, default_branch) | admin |
/api/repos/:name | GET |
Get repo details | any |
/api/repos/:name | DELETE |
Delete repo record | admin |
ACL (Admin)
| Endpoint | Method | Description | Auth |
|---|---|---|---|
/api/acl?repo=workspace | GET |
List ACL rules (non-admin sees own only) | any |
/api/acl/:username?repo=workspace | GET |
Get user ACL for repo | admin or self |
/api/acl/:username | PUT |
Set ACL (body: repo, paths_read, paths_write, deny, scopes) | admin |
/api/acl/:username?repo=workspace | DELETE |
Delete ACL rule | admin |
/api/acl/audit?user=X&repo=Y&limit=100 | GET |
Audit log of admin actions | admin |
Tokens
| Endpoint | Method | Description | Auth |
|---|---|---|---|
/api/tokens?user=agent-alpha | GET |
List active tokens (non-admin sees own only) | any |
/api/tokens | POST |
Mint token (body: username, repo, scopes, ttl, label) | admin or self |
/api/tokens/:jti | DELETE |
Revoke token | admin or owner |
5. Webhook Events
| Event | Trigger | Payload Fields |
|---|---|---|
push | Successful push | ref, before, after, pusher, files |
branch.create | New branch created | ref, sha, pusher |
branch.delete | Branch deleted | ref, sha, pusher |
note.add | Note added or appended | sha, user |
note.remove | Note removed | sha, user |
Push Payload Example
{
"event": "push",
"ref": "refs/heads/main",
"before": "aaa111aaa111aaa111aaa111aaa111aaa111aaa1",
"after": "bbb222bbb222bbb222bbb222bbb222bbb222bbb2",
"pusher": "agent-alpha",
"files": [
"agent-alpha/results.md",
"agent-alpha/data/output.json"
]
}
HMAC Verification (Python)
Webhook payloads include X-Hub-Signature-256. Verify before processing:
import hashlib, hmac
def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
# Usage in a Flask/FastAPI handler:
# sig = request.headers["X-Hub-Signature-256"]
# if not verify_webhook(request.data, sig, WEBHOOK_SECRET):
# return "invalid signature", 403
6. Access Patterns
Common configurations for different roles:
| Role | paths_read | paths_write | deny | scopes | TTL |
|---|---|---|---|---|---|
| AI agent | shared/**,agent-X/** |
agent-X/** |
secrets/** |
git:read,git:write |
7 days |
| Supervisor | ** |
shared/** |
- | git:read,git:write |
30 days |
| Auditor | ** |
- | - | git:read |
24 hours |
| CI bot | ** |
- | secrets/** |
git:read |
1 hour |
| Service account | shared/**,svc/** |
svc/** |
secrets/** |
git:read,git:write |
7 days |
7. Auth Headers
When running behind a reverse proxy, these trusted headers carry identity:
| Header | Format | Example |
|---|---|---|
X-Auth-User | string | agent-alpha |
X-Auth-Scopes | comma-separated | git:read,git:write |
X-Auth-Paths-Read | comma-separated globs | shared/**,agent-alpha/** |
X-Auth-Paths-Write | comma-separated globs | agent-alpha/** |
X-Auth-Deny | comma-separated globs | secrets/** |
Glob Syntax
| Pattern | Matches |
|---|---|
* | Everything including / |
** | Everything (same as *) |
? | Single character |
[abc] | Character classes |
agent-alpha/** | All files under agent-alpha/ |
shared/*.md | Markdown files in shared/ (not subdirectories) |