cli.sgit.ai / Research / sgit serve: running a vault locally — research and plan

sgit serve: running a vault locally — research and plan

Date2026-10-12 · 16:17 UTCKindresearchTopicsserve local apiWritten bythe sgit-ai CLI team (Claude Code sessions), for Dinis CruzSourceteam/humans/dinis_cruz/claude-code-web/10/12/research__sgit-serve-local-vault-server.md in SGit-AI__CLI at 5597526. Published here as written; the CLI repository is where the work happened.

Date: 2026-10-12 · Status: research and design, no code yet · Asked: "how can I run a vault locally … just clone a vault and run it from there … sgit serve … SSL if needed … how much plumbing do we need to add to sgit".

Superseded in part (same day): Dinis chose to run the SG/Send container image rather than re-implement the server in sgit. See options__sgit-serve-container-and-static.md. The API inventory (§3), browser modes (§5) and TLS notes (§6) below still apply.

1. Short answer

It is very doable, mostly with what we already have.

Estimate:

2. What exists today

PieceWhereWhat it gives us
sgit vault servesgit_ai/cli/CLI__Serve.py, sgit_ai/core/serve/Vault__Static_Server.pyA stdlib ThreadingHTTPServer, read-only (GET/HEAD; anything else is 405), bound to 127.0.0.1, with a Host-header check against DNS rebinding and a path guard. It serves the published static loader and maps /api/vault/read/<id>/bare/* onto .sg_vault/bare/.
Vault__API__In_Memorysgit_ai/network/api/The API semantics the CLI relies on (batch, write-if-match conflicts, list, tombstones), in process. It does not check write keys.
Real SG/Send serversgraph-ai-app-send (PyPI, Python ≥ 3.12; fastapi, uvicorn, osbot-aws, mcp …)Our integration suite already runs it in-process with in-memory storage. It also has disk storage.
SG/Send containerdiniscruz/sg-send-vault (~250 MB; source sgraph_ai_app_send__docker/)The whole local stack already exists as an image: the vault UI at / plus the API on one port. Disk storage goes to /data (layout in §3.4). It has an optional access token, can terminate TLS itself if given a cert/key, and its README covers the secure-context and Web Crypto issue.
Vault UIsgraph_ai_app_send__ui__vault (inside the server package; v0.2.3 is 2.6 MB unbuilt)The endpoint comes from window.SG_ENDPOINT, then sessionStorage['sg-vault-endpoint'] (there is an endpoint field on the entry form), then https://dev.send.sgraph.ai. A /#<key> hash opens a vault directly. It calls only vault endpoints, plus the transfer and presigned endpoints for other features.

So "run a vault locally" works today with docker:

docker run --rm -p 8080:8080 -v "$PWD/_sg-send_data:/data" diniscruz/sg-send-vault:latest
sgit clone <vault-key> my-vault --base-url http://localhost:8080   # or push an existing clone there

What is missing:

3. The API a local server must speak

This is from the code of sgraph-ai-app-send 0.33.0, the user lambda: lambda__user/fast_api/routes/Routes__Vault__Pointer.py and service/Service__Vault__Pointer.py.

3.1 Routes

MethodPathAuthNotes
GET/api/vault/read/{vault_id}/{file_id:path}publicraw bytes; 404 if missing
PUT/api/vault/write/{vault_id}/{file_id:path}write keyan empty body gives 400; returns {file_id, vault_id, status:"completed"}
DELETE/api/vault/delete/{vault_id}/{file_id:path}write key403 for a key mismatch and for a missing file
POST/api/vault/batch/{vault_id}public if every op is read, otherwise write key1–100 ops; always HTTP 200 (§3.3)
GET/api/vault/list/{vault_id}?prefix=public{vault_id, prefix, files:[…]}, sorted
GET/api/vault/health/{vault_id}public200 if manifest.json exists, otherwise 404
DELETE/api/vault/destroy/{vault_id}write keybody {vault_id, purge?}; writes a tombstone
GET/api/info/healthpublic{"status":"ok"}

vault_id must match ^[a-z0-9]{8,24}$.

Extras the local server does not need for vaults:

3.2 Auth

3.3 Batch

Ops are read, write (b64), write-if-match (match b64, or null for "must not exist") and delete.

This is exactly what sgit's CAS code already handles; it is the live server's behaviour.

3.4 Disk layout (container and lambda disk mode)

<root>/vault/<vault_id[:2]>/<vault_id>/manifest.json          {vault_id, write_key_hash, created_at}
<root>/vault/<vault_id[:2]>/<vault_id>/deleted.json           tombstone
<root>/vault/<vault_id[:2]>/<vault_id>/<file_id>/payload      e.g. bare/data/obj-cas-imm-…/payload

If the native server uses the same layout, a data folder works with both sgit serve and docker run -v <folder>:/data diniscruz/sg-send-vault. That gives an exact-parity fallback for free.

Note that this is not the clone's own layout: .sg_vault/bare/data/<id> is a file, not <id>/payload. Section 4 explains why the server keeps its own store anyway.

3.5 CORS

4. "Clone a vault and run it from there": the model

Recommendation: the server has its own store, seeded from the clone; the clone talks to it as a remote. The server should not read and write the clone's .sg_vault/bare/ directly.

Why not serve the clone's bare/ directly:

So:

my-vault/                      the clone (unchanged behaviour)
my-vault/.sg_vault/serve/      the local server's store, in SG/Send disk layout (§3.4)
                               (inside .sg_vault, so it is never committed; excluded from uninit backups)

sgit serve, run inside a clone:

  1. Seeds the store if it is empty:
  1. Starts the server on http://localhost:8420, with both the API and the UI.
  2. Prints:

Then:

Outside a clone, sgit serve --data <dir> is a general local SG/Send-compatible server:

5. Browser access: three ways, and what each needs

ModeURL the user opensCross-origin?Needs
A. Same origin (default)http://localhost:8420/ (UI served by sgit)nonothing. localhost is a secure context, so Web Crypto works in Chrome, Firefox and Safari.
B. Hosted UI → local APIhttps://vault.sgraph.ai, endpoint field set to http://localhost:8420yesCORS on the local server (* or an allow-list, as production does).
Chrome 141/142+: a one-time Local Network Access permission prompt per site.
Safari: blocks https→http://localhost as mixed content (WebKit bug 279249), so it needs https://localhost (§6).
C. Another machine / LANhttps://<host>:8443–TLS (Web Crypto refuses plain http on non-localhost hosts) and an access token

For B, the local server should also answer preflights with Access-Control-Allow-Private-Network: true. Older Chrome Private Network Access preflights need it, and it is harmless otherwise.

6. TLS

7. The UI: where its files come from

OptionWheel sizeNetwork at runtimeRisk
Vendor a pinned, flattened build into the sgit wheel (recommended)+~1 MB zippednone, works offlinemust be refreshed per UI release; covered by a script, a pinned version and a SHA-256 manifest
Download on first serve into ~/.sgit/ui/<version>/, hash-pinnednoneoncesupply chain (mitigated by the pinned hash); fails offline the first time
Use the docker image for the UI–dockernot "just sgit"

Ask of the SG/Send team: publish the flattened vault UI as a versioned release asset (zip + SHA-256). Then sgit vendors from it rather than rebuilding from their source tree.

8. Implementation plan

Components

ComponentLayerSize
Vault__Local_Store: the §3.4 layout; atomic writes (temp + rename); a per-vault lock for batch and CAS; list by prefix; manifest, write-key hash, tombstonestorage~200 lines
Vault__Local_Server: stdlib ThreadingHTTPServer; the §3.1 routes with matching status codes and JSON; batch semantics (§3.3); access-token option; Host check (from Vault__Static_Server); Origin allow-list on writes in mode A; CORS and PNA preflight; static UI; optional ssl wrapcore/serve~350 lines
Vault__Serve__Seed: hard-link objects, copy refs/indexes/keys, write the manifestcore/serve~100 lines
Vault__Local_TLS: CA and leaf generation, mkcert detection, trust instructionscrypto~120 lines
CLI__Serve: sgit serve [--data DIR] [--port] [--bind] [--open] [--tls] [--cert/--key] [--cors-origin …] [--token] [--no-ui] [--ui-dir] [--add-remote]; sgit serve trust; keep vault serve as the static-publish servercli~200 lines

Command placement (rule 9):

Tests

Phases

PhaseWorkEstimate
0Spike: vendored UI v0.2.3 + the real server on localhost, driven by Playwright; confirm modes A and B in Chromium½ day
1Store + API server + seeding + sgit serve (API only); CLI interop; contract tests1½–2 days
2UI vendoring + serving; --open1 day
3Mode B: CORS allow-list, PNA header, Origin checks½ day
4TLS: generated CA/leaf, mkcert, --cert/--key, serve trust1 day
5Docs, threat model rows, CHANGELOG½ day

MVP = phases 0–2 (~3 days). Full = ~5 days.

9. Security notes (to become threat-model rows)

10. Decisions for you

  1. UI files: vendor into the wheel (recommended), download on first run, or rely on docker?
  2. Store location: per clone in .sg_vault/serve/ (recommended for "serve this clone"), with --data DIR for a shared multi-vault store?
  3. local remote: should sgit serve add it automatically, or only print the command?
  4. Default port: 8420, the same as today's vault serve, or 8080 like the container?
  5. Asks of SG/Send:

Sources

This page as markdown · its source, admin/content/articles/sgit-serve-running-a-vault-locally.md