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

ScopePermission
git:readClone, fetch, pull, browse files via API
git:writePush, 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

EndpointMethodDescriptionAuth
/api/files?ref=mainGET List visible files with size, SHA, last commitgit:read
/api/file?ref=main&path=bob/notes.mdGET Download raw file contentgit:read

Commit History

EndpointMethodDescriptionAuth
/api/commits?ref=main&limit=20GET List visible commits (ACL-filtered shadow tree)git:read
/api/commits?ref=main&expected_head_sha=XGET Same, but returns 409 if branch moved since Xgit:read

Notes

EndpointMethodDescriptionAuth
/api/notes?sha=abc123GET Get note attached to a commitgit:read
/api/notesPOST Add, append, or remove a note (body: action, sha, note)git:write

Webhooks

EndpointMethodDescriptionAuth
/api/webhooksGET List registered webhooksgit:read
/api/webhooksPOST Register webhook (body: url, secret)git:write
/api/webhooks?url=...DELETE Unregister webhookgit:write

Users (Admin)

EndpointMethodDescriptionAuth
/api/usersGET List all usersadmin
/api/usersPOST Create user (body: username, display_name, role)admin
/api/users/:usernameGET Get user detailsadmin or self
/api/users/:usernameDELETE Delete user (cascades ACL and tokens)admin
/api/users/:username/keysGET List SSH keysadmin or self
/api/users/:username/keysPOST Add SSH key (body: key_type, key_data, fingerprint, label)admin or self
/api/users/:username/keys/:fpDELETE Delete SSH keyadmin

Repos (Admin)

EndpointMethodDescriptionAuth
/api/reposGET List reposany
/api/reposPOST Create repo (body: name, path, description, default_branch)admin
/api/repos/:nameGET Get repo detailsany
/api/repos/:nameDELETE Delete repo recordadmin

ACL (Admin)

EndpointMethodDescriptionAuth
/api/acl?repo=workspaceGET List ACL rules (non-admin sees own only)any
/api/acl/:username?repo=workspaceGET Get user ACL for repoadmin or self
/api/acl/:usernamePUT Set ACL (body: repo, paths_read, paths_write, deny, scopes)admin
/api/acl/:username?repo=workspaceDELETE Delete ACL ruleadmin
/api/acl/audit?user=X&repo=Y&limit=100GET Audit log of admin actionsadmin

Tokens

EndpointMethodDescriptionAuth
/api/tokens?user=agent-alphaGET List active tokens (non-admin sees own only)any
/api/tokensPOST Mint token (body: username, repo, scopes, ttl, label)admin or self
/api/tokens/:jtiDELETE Revoke tokenadmin or owner

5. Webhook Events

EventTriggerPayload Fields
pushSuccessful push ref, before, after, pusher, files
branch.createNew branch created ref, sha, pusher
branch.deleteBranch deleted ref, sha, pusher
note.addNote added or appended sha, user
note.removeNote 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:

Rolepaths_readpaths_writedenyscopesTTL
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:

HeaderFormatExample
X-Auth-Userstringagent-alpha
X-Auth-Scopescomma-separatedgit:read,git:write
X-Auth-Paths-Readcomma-separated globsshared/**,agent-alpha/**
X-Auth-Paths-Writecomma-separated globsagent-alpha/**
X-Auth-Denycomma-separated globssecrets/**

Glob Syntax

PatternMatches
*Everything including /
**Everything (same as *)
?Single character
[abc]Character classes
agent-alpha/**All files under agent-alpha/
shared/*.mdMarkdown files in shared/ (not subdirectories)