Files
granthi-sync/README.md
T
2026-08-19 09:14:30 -04:00

7.3 KiB

granthi-sync v1

The signup → download → link-folders → cloud product spine for the Granthi forge, tested against the BETA forge (granthi-beta.shre.ai). Python 3 stdlib + git CLI only — same portability heritage as the estate's gitea_sync.py mesh.

   ┌──────────────┐   device flow    ┌─────────────────┐
   │ granthi-sync │ ───────────────▶ │ shre-id Zitadel │
   │  (client,    │ ◀─────────────── │  id.shre.ai     │
   │  Mac/laptop) │   access token   └─────────────────┘
   │              │
   │              │  POST /v1/link {zitadel_access_token, device_name}
   │              │ ───────────────▶ ┌───────────────────────────────┐
   │              │ ◀─────────────── │ granthi-link  :3042           │
   │              │  {login, token}  │ (granthi VPS, tailnet-only)   │
   │              │                  │  · userinfo validation        │
   │              │  POST /v1/repos  │  · ensure Gitea user (admin)  │
   │              │ ───────────────▶ │  · mint scoped user token     │
   │              │                  └──────────────┬────────────────┘
   │              │   git push/fetch (user token    │ admin API
   │              │   via credential helper)        ▼
   │              │ ───────────────▶ ┌───────────────────────────────┐
   └──────────────┘                  │ BETA forge  :3041             │
                                     │ granthi-beta.shre.ai          │
                                     └───────────────────────────────┘

Components

server/granthi_link.py — provisioning service (granthi VPS)

  • GET /health
  • POST /v1/link {zitadel_access_token, device_name} → validates the token against https://id.shre.ai/oidc/v1/userinfo, derives a login from preferred_username (email local-part fallback), ensures the Gitea user exists, mints a token scoped write:repository,write:user, returns {gitea_base, login, token, token_name}.
  • POST /v1/repos {token, name, private} → creates the user repo with the USER token; clone/html URLs are rebased onto public_gitea_base because the container ROOT_URL (https://granthi-beta.shre.ai) does not resolve for tailnet-only clients.

Deployment: /opt/granthi-link/{granthi_link.py,config.json} + systemd unit granthi-link.service; binds 127.0.0.1:3042 and 100.111.127.127:3042 (tailnet). Not publicly exposed — see promotion window.

Empirically verified mechanics on Gitea 1.27.1 (beta forge):

  • Token minting: POST /api/v1/users/{login}/tokens returns 401 for token-authenticated sudo (both Sudo: header and ?sudo=); it only works with admin basic auth + Sudo: <login> header (201). The config therefore carries admin_login/admin_password (root-only, 0600) in addition to admin_token (minted via gitea admin user generate-access-token, used for all other admin calls).
  • User creation: source_id is omitted → local user with a random 30-char password, must_change_password=false, visibility=private. Rationale: /api/v1/admin/identity-auth-sources 404s on 1.27.1; the shre-id OAuth2 source is ID 1 (via gitea admin auth list), but users attached to an OAuth2 source cannot basic-auth and admin-created users get no external_login_user row anyway — first OIDC web login links by email regardless of this choice.
  • test_mode (config flag, never in production): allows /v1/link to accept test_userinfo in the body instead of a Zitadel round-trip, so E2E can exercise the ensure-user + mint path headlessly.

client/granthi_sync_client.py (+ bin/granthi-sync) — client daemon

  • link [--server URL] [--token TOK] — Zitadel device flow (native app granthi-sync-device, client_id 386909715541590022, project granthi-forge 386906525790109702; grants: device_code + refresh_token): prints the verification URL + user code, polls the token endpoint (authorization_pending/slow_down handled), then calls /v1/link. --token skips the device flow with a ready Zitadel token (headless/dev). Result stored in ~/.granthi-sync/config.json (0600).
  • add <folder> [--name N] [--private|--public]git init -b main if needed, creates the cloud repo via /v1/repos, adds remote granthi, initial commit + push. The token is delivered by a git credential helper (the client's hidden git-credential subcommand reading the 0600 config) — never embedded in the remote URL (estate rule).
  • watch [--interval 30] [--once] — per folder: autocommit (sync: <ISO ts>) → fetch → ff-pull if remote strictly ahead → push if local strictly ahead. DIVERGED → log + record + SKIP. Never force, never merge — the same policy as the mesh. SIGTERM-clean.
  • status — table of linked folders, last sync, divergence flags.
  • Run as a daemon on macOS with client/launchd/ai.granthi.sync.plist (edit the script path, then launchctl bootstrap gui/$UID <plist>).

Tests

  • python3 -m unittest discover -s tests — 24 tests: autocommit/ff/diverged logic against real temp git repos (including "diverged never touches the remote"), config 0600 handling, mocked device-flow polling, credential helper protocol, and the full /v1/link + /v1/repos service flows against an in-process stub playing Zitadel + Gitea.
  • Live E2E against the beta forge is recorded in the delivery notes (link → add → watch ff/push → forced divergence → DIVERGED skip verified via API, remote sha untouched).

Invite-only story (v1)

There is no open signup. An operator invites a user by creating them in shre-id (Zitadel org). The user downloads the client, runs granthi-sync link, signs in at id.shre.ai with the printed device code, and the provisioning service creates their forge account + scoped token on the fly — the forge never sees a password and the user never sees the forge admin. Every linked folder becomes a private repo under their account.

Promotion window (beta → prod)

  1. Expose :3042 behind cloudflared (granthi.shre.ai vhost or link.granthi.shre.ai) — today it is tailnet-only by design.
  2. Swap forge base URLs in /opt/granthi-link/config.json: gitea_base → prod forge, public_gitea_basehttps://granthi.shre.ai; the client default server URL moves to the public endpoint.
  3. The granthi-web OIDC app already lists the prod callback; the device app is host-independent. Rotate the beta admin token/password out of the config when pointing at prod (prod forge is READ-ONLY to this estate — promotion is an operator action, not an agent action).
  4. Add rate limiting / abuse controls before public exposure (one token mint per link call today).