hosystem Engagements

Sharibako — System Design (Kamae 2)

created 2026-06-30
status draft
type system-design
project sharibako
stage kamae-2
kamae-chain seed → **system-design** → injection-decision → ownership-decision → readme → ho-overview
builds-on kamae-1-sharibako-seed
next kamae-3-sharibako-readme
superseded-in-part-by
  • kamae-2.1-sharibako-injection-decision (§7 "No runtime injection" line; all other commitments stand)
  • kamae-2.2-sharibako-ownership-decision (§2 Materializer's whole-file assumption; per-key ownership with `update` verb)

Reader's note (2026-07-01): Two subsequent decision documents supersede specific parts of this document; all other commitments stand.

  • kamae-2.1-sharibako-injection-decision.md §7's "No runtime injection" line. sharibako run is now a peer output verb alongside sharibako materialize, with a fourth threat-model class (workspace file-readers) documented in ../SECURITY.md.
  • kamae-2.2-sharibako-ownership-decision.md supersedes §2's implicit whole-file-ownership assumption. Sharibako owns a per-scope set of keys made explicit by the vault's filesystem; the Materializer merges owned keys into .env and preserves everything else. A new operation, update, closes the bidirectional loop (.env → vault).

Everything else in this document — four-component slice, age-per-secret, file-per-secret in the vault, filesystem-as-schema, git-backed, macOS Keychain gating, the deferred-decisions table — stands unchanged. Preserved as-authored to record what was decided when.

Sharibako is disciplined so you don't have to be.

Sharibako is a reliquary for digital secrets. A native Mac app and CLI that hold your API keys and env vars in a calm, age-encrypted vault — local, git-backed, and shaped to how you actually work: by project and by machine. Edit secrets in the workshop or grab them from the terminal. Materialize them as .env files at the right paths. Linked secrets share a value across projects — rotate once, every place updates. Good hygiene is the easy default; bad hygiene is possible but visible. It is disciplined so you don't have to be.


1. Architecture Overview

Four components, sliced by purpose, not by technical layer.

┌────────────────────────────────────────────────────────────────┐
│                        The Surfaces                             │
│  ┌─────────────────────┐         ┌─────────────────────────┐   │
│  │  The Workshop       │         │   The Tool              │   │
│  │  SwiftUI Mac app    │         │   Swift CLI             │   │
│  │  Apple Silicon only │         │   Mac + Linux           │   │
│  └──────────┬──────────┘         └────────────┬────────────┘   │
└─────────────┼──────────────────────────────────┼───────────────┘
              │                                  │
              └────────────────┬─────────────────┘
                               │
                               ▼
                ┌──────────────────────────────┐
                │     The Vault Core           │
                │  Owns vault/ on disk.        │
                │  age encryption, schema,     │
                │  link graph resolution.      │
                └──────┬───────────────┬───────┘
                       │               │
                       ▼               ▼
         ┌──────────────────────┐  ┌──────────────────┐
         │  The Materializer    │  │   The Conduit    │
         │  Markers, ingest,    │  │  git pull/push   │
         │  materialize, heal.  │  │  of the vault.   │
         │  Bridges vault and   │  │  Knows nothing   │
         │  user filesystem.    │  │  of secrets.     │
         └──────────────────────┘  └──────────────────┘

One-line per component:


2. Component Breakdown

The Surfaces

Two products from one Swift package, sharing a common library that uses Vault Core, Materializer, and Conduit.

The Workshop (GUI).

SwiftUI app for macOS Apple Silicon. The calm local place. A three-pane window:

Top-level actions: Add Scope, Add Shared Secret, Rescan, Sync.

The Tool (CLI).

Swift ArgumentParser, cross-platform (macOS Apple Silicon + Linux x86_64 + Linux arm64). Frictionless retrieval. Commands:

Both products authenticate to the age key via macOS Keychain (Mac) or a passphrase-protected age key on disk (Linux). All operations route through the same shared core code.

The Vault Core

Owns: the vault directory on disk. The filesystem IS the schema.

vault/
├── .git/
├── shared/
│   ├── openai-personal.age
│   ├── cloudflare-dns-token.age
│   └── tailscale-auth-key.age
└── scopes/
    ├── kanyo-dev/
    │   ├── scope.yaml
    │   ├── OPENAI_API_KEY.link
    │   ├── DATABASE_URL.age
    │   └── DEBUG.age
    ├── kanyo-prod-on-chumon/
    │   ├── scope.yaml
    │   ├── OPENAI_API_KEY.link
    │   └── ...
    └── chumon-host/
        ├── scope.yaml
        ├── TAILSCALE_AUTH_KEY.link
        └── ADMIN_PASSWORD.age

Three file types:

Operations exposed to the surfaces:

Link graph: implicit. Computed at runtime by walking scopes/*/*.link files and collecting references. No manifest file. Orphan detection (a shared entry nothing references) is a cleanup feature.

The Materializer

Owns: the user's filesystem — markers and materialize_to targets.

Marker schema — a plaintext file named .sharibako at a project's root:

scope: kanyo-dev
materialize_to: ./.env       # optional, relative path, defaults to ./.env

Markers are portable (no machine-specific paths) and committable to the project's repo — cloning the project on another machine reveals its sharibako-managed scope automatically.

Operations:

The three-state model is computed here: a scope is live_here if its marker exists in a configured root; live_elsewhere if not (another machine owns it); orphaned if a marker references a scope that no longer exists in the vault.

Nothing auto-deletes. Deletion is always an explicit user action and only ever touches the vault — markers on other machines become orphans on next scan, surfaced for cleanup but not destroyed.

The Conduit

Owns: git operations on the vault directory.

Operations:

The Conduit is intentionally thin. It wraps git-the-binary (via Process); it doesn't reimplement git. It knows nothing about secrets — every change is just a file change in the vault directory.

Merge behavior: the file-per-secret structure makes most conflicts impossible by construction (different secrets = different files). Conflicts only occur when the same secret is rotated from two machines between syncs — which is rare and the failure mode (one of the rotations needs to be re-applied) is clear.


3. Core Interaction

sharibako init invoked from ~/Vaults/sageframe-no-kaji-dev/kanyo/. End-to-end:

  1. The Surfaces. CLI invoked from current working directory (kanyo/). GUI does the equivalent via a directory picker. Either way, hands the path to the Materializer.

  2. The Materializer checks for an existing .sharibako marker. None found → proceed (if found → refuse with "already managed").

  3. The Materializer proposes a scope identity:

    • Basename of directory → kanyo
    • Detects type from parent directory pattern (~/Vaults/sageframe-no-kaji-dev/<this>/type: project-dev)
    • Asks Vault Core for name collisions; if kanyo exists, proposes kanyo-dev
    • Surfaces the proposed identity for user confirmation
    • Requests Touch ID via Keychain (vault is about to be touched)
  4. The Materializer scans for existing secrets:

    • Reads .env, .env.local; falls back to .env.example for key schema if no values present
    • Parses KEY=value pairs
    • Asks Vault Core for shared/ entries with name matches (e.g., scan finds OPENAI_API_KEY → check shared/openai-personal)
  5. The Surfaces present the decision matrix — for each detected secret:

    • Import as new project-local secret
    • Link to an existing shared (name-matched suggestions ranked first)
    • Move to shared (create new shared entry, link this scope to it)
    • Skip
  6. The Vault Core writes the scope to disk:

    • Creates vault/scopes/kanyo-dev/
    • Writes scope.yaml with { identity: kanyo-dev, type: project-dev }
    • For each "import as local": writes <KEY>.age (age-encrypted { value, rotated_at })
    • For each "link to existing": writes <KEY>.link (plaintext ID)
    • For each "move to shared": writes shared/<new_id>.age AND vault/scopes/kanyo-dev/<KEY>.link
  7. The Materializer writes the marker:

    • .sharibako (plaintext YAML) at ~/Vaults/sageframe-no-kaji-dev/kanyo/:
      scope: kanyo-dev
      
    • materialize_to omitted (defaults to ./.env)
  8. The Materializer rewrites .env at the materialize_to path:

    • Walks the scope's secrets, decrypting .age files and resolving .link targets
    • Composes the full env content
    • Shows a diff against any existing .env if changes are non-trivial; confirms before write
  9. The Conduit stages the vault changes:

    • git add vault/
    • git commit -m "init scope: kanyo-dev"
    • git push if a remote is configured (otherwise no-op)
  10. Done. CLI prints what was created. GUI navigates to the new scope's view.

The same architecture handles every other action — rotate, link, materialize, sync — by composing the same components. The init flow exercises all four.


4. Data Model

The filesystem layout (vault/ and markers) is the complete data model. No databases, no metadata stores.

Marker (.sharibako):

scope: kanyo-dev
materialize_to: ./.env    # optional

Scope metadata (vault/scopes/<id>/scope.yaml):

identity: kanyo-dev
type: project-dev | project-prod | service | machine | other
display_name: "Kanyo (dev)"    # optional, defaults to identity

Encrypted secret (vault/scopes/<id>/<KEY>.age or vault/shared/<id>.age):

Decrypted content:

value: <the actual secret string>
notes: |
  Optional free-form context. Where it came from, what it's for.
rotated_at: 2026-04-15    # ISO 8601 date; updated on rotation

Link (vault/scopes/<id>/<KEY>.link):

Plaintext, single line:

openai-personal

The contents is the shared entry's ID — corresponds to vault/shared/<id>.age. Resolution is filesystem-relative.

App config (per machine, outside the vault):

vault_path: /Users/atmarcus/Vaults/sageframe-no-kaji-dev/sharibako-vault
scan_roots:
  - /Users/atmarcus/Vaults/sageframe-no-kaji-dev
keychain_age_key_label: sharibako-primary-age-key

Stored at ~/Library/Application Support/Sharibako/config.yaml on Mac; ~/.config/sharibako/config.yaml on Linux.


5. Technology Stack

Element Choice Rationale
GUI Swift + SwiftUI Native Mac feel; calm-workshop aesthetic; first Swift project as learning vehicle. Electron rejected categorically. Tauri rejected as morally Electron-shaped. Python + Qt rejected because Swift is the language Andrew has been waiting for.
CLI Swift + ArgumentParser Same codebase as the GUI. Swift on Linux is mature for CLI work (Foundation, ArgumentParser, Process all stable).
Encryption age (BSD-2-Clause) Modern, focused, well-audited. Chosen over sops to drop a 10MB layer and simplify the crypto path. File-per-secret model needs no sops; the linking is filesystem-native.
Storage substrate git + plain filesystem Filesystem IS the schema (the .eml precedent). git provides sync, history, conflict surfacing — without secrets ever entering git's textual diff (each .age file is opaque ciphertext).
Authentication macOS Keychain on Mac; passphrase on Linux Touch ID per vault open mirrors Apple Passwords. Same threat model SSH keys live under, well-understood, accepted.
Distribution (Mac GUI) Signed/notarized DMG via Cloudflare Pages M4Bookmaker pattern. Direct download, paid binary. No App Store.
Distribution (CLI) Homebrew tap brew install sageframe-no-kaji/tap/sharibako — works on Mac and Linux. Formula declares depends_on "age" so Homebrew handles age installation. Direct .tar.gz for non-Homebrew users (bundles age inside).
Bundled binaries (Mac DMG) age binary in Sharibako.app/Contents/Resources/ Same pattern as M4Bookmaker bundling ffmpeg. Xcode signs as part of the bundle during notarization.
Signing Existing Apple Developer Program Reused from M4Bookmaker; same cert can sign multiple products.

Non-obvious evaluations:


6. Deployment Model

Mac (GUI + CLI):

CLI only (Mac or Linux):

Distribution :

Signing infrastructure:

No App Store distribution. No sandboxing constraints. Bundled executables are allowed and signed as part of the notarized bundle.


7. Scope Boundaries

The 's scope list, restated as architectural commitments — what the code structurally prevents.

MVP Architectural Commitments

Architecturally Prepared For (Not Built)


Provisional Ho Sequence

Subject to refinement in 4 (Ho Overview). Phases:

Ho Stage Title Purpose
setup Project scaffolding Swift package, signing setup, GitHub repo, CI, baseline tests
ho-01 Vault Core — schema + age Filesystem layout, age invocation, encrypt/decrypt round-trips, scope/secret operations (no UI)
ho-02 shu The Conduit — git wrapping git pull/push/commit/status over the vault directory
ho-03 The Materializer — markers, ingest, materialize Marker read/write, .env parsing, ingest flow, .env writing, drift detection
ho-04 ha The Tool (CLI) — first usable shape init, add, get, materialize, sync, scan, status commands wired to the core
ho-05 ha The Workshop (GUI) — MVP shell SwiftUI window, scope sidebar, secret editing, materialize button, sync button
ho-06 ha GUI polish — three-state UI, ingest flow, first-run Live/elsewhere/orphan glyphs, ingest decision matrix, first-run setup wizard, age key generation + backup nudge, heal surface
ho-07 Linking semantics — UI + CLI Link/unlink commands, GUI link target picker, shared-secret browser, rotation propagation surface
ho-08 ri Bundling, signing, installer Xcode notarization workflow, DMG with bundled age, Homebrew formula, Linux .tar.gz
ho-09 ri Website + first release sharibako.sageframe.net (Cloudflare Pages), release manifest, in-app update check, v1.0 release

Estimated total scope: ~1,500–3,000 LOC of Swift across all hos. Calibrated to a first-Swift learning curve.


Deferred Decisions

Each open question is assigned to a specific ho with evaluation criteria.

# Question Deferred to Evaluation criteria
1 Pricing model for the signed DMG ho-09 (Website + release) Survey of comparable indie Mac tools; choose one of one-time / donation / patron / per-major-version
2 Backup nudge UX for age key generation ho-06 (GUI polish) Mockup in ; test against vibe-coder usability (15-minute install success criterion)
3 sharibako-agent daemon for CLI Touch ID friction Post-MVP Need CLI to be in regular use first; defer until friction is felt
4 Multi-root scanning UI Post-MVP Single root is sufficient until proven otherwise by personal use
5 Additional materialization formats beyond .env Post-MVP Wait for concrete user request with a non-.env consumer
6 Remote-host materialization (SSH/SCP push) Post-MVP sageframe-config-sync and equivalents cover this externally; revisit if vibe-coder audience needs it
7 Conflict resolution UI for git pulls ho-02 (Conduit) Basic surfacing in ho-02; UI polish if and when conflicts happen in practice
8 Shared-vault use documentation (silent vs. explicit) ho-09 (Website + release) Content decision, not architecture. Default plan: document it briefly as "two humans sharing one age key works fine; no team features."
9 First-run experience full ho-06 (GUI polish) "Where do you keep code?" + "Where should the vault live?" + "Optional remote" — design in mockup
10 Import flows from Vaultwarden / iCloud Keychain Post-MVP .env ingest is the v1 import story; other sources are second-wave
11 Auto-update mechanism specifics ho-09 Simple manifest-on-Cloudflare check on launch; defer Sparkle vs. custom decision until ho-09

System Design complete. Next document: Kamae 3 (README), to be drafted in a fresh session against this document.

Rendered from the corpus, verbatim · source on GitHub →

ingested: sharibako @ a97b22af9b61 · ho-system @ 79e96b801a13 · the glossary · the colophon