crates/nornir-warehouse-embed/proto/workspaces.proto
// A TRIMMED copy of nornir-server's `Workspaces` control surface — ONLY the
// verbs dwarves' "Add warehouse (SSH) → Populate" affordance drives: Register
// (add + eager-populate), Fetch (re-sync), List. Package `nornir.v1` so the
// generated method paths (`/nornir.v1.Workspaces/Register` …) match the live
// `nornir-server` — dwarves speaks it as a THIN client WITHOUT linking the
// `nornir` crate (a `dwarves → nornir` edge would be a dependency cycle; the
// `nornir-funnel-embed` / telemetry pattern). Field numbers are byte-identical
// to `nornir/proto/nornir.proto` so the wire is compatible.
//
// ARCHITECTURE (amended doctrine): the warehouse is nornir's; dwarves reaches it
// by gRPC, never directly to skade. The UI ("Add warehouse…", "Populate") lives
// in dwarves; the warehouse-WRITE (clone SSH members + build the warehouse) is
// nornir's — `Register{lazy=false}` clones + builds inline, `Fetch{force}` re-syncs.
syntax = "proto3";
package nornir.v1;
service Workspaces {
rpc List (Empty) returns (WorkspaceList);
rpc Get (WorkspaceName) returns (WorkspaceRecord);
rpc Register (RegisterWorkspaceRequest) returns (WorkspaceRecord);
// Remove — the OTHER half of the whole-action pair. `purge = true` also
// `remove_dir_all`s the workspace's on-disk `builds/` (warehouse +
// catalog.redb + jobs.redb), not just the registry row, so "remove" clears
// the warehouse and everything with it rather than leaving a registered
// entry pointing at data nobody deletes. It touches the SERVER's workspace
// data only — it is not, and must never become, a credential operation.
rpc Remove (WorkspaceName) returns (Empty);
rpc Fetch (WorkspaceFetchRequest) returns (WorkspaceFetchReport);
// PopulateStatus — the per-workspace populate ROLL-UP for the whole registry:
// one compact `RosterRow`-shaped JSON object per workspace (name / mode /
// members / last_synced / populate verdict / failing_member / last_error),
// rolled up server-side from `clone_events`. Compact by design, so it can be
// POLLED — unlike the unpaginated `Viz.CloneEvents`, which overflows the gRPC
// decode cap on a big workspace (and is Unimplemented besides).
rpc PopulateStatus (Empty) returns (JsonResponse);
// Repopulate — THE ONE REFRESH, and the reason the warehouse pane needs no
// per-pane Scan buttons. Clear-and-repopulate the DERIVED half of a served
// workspace's warehouse through the server's OPEN handle: `purge = true` drops
// the derived lens tables (the set is read off nornir's own lens registry, never
// a hand list, and never a raw scan / snapshot / base table); with or without it
// the feed chain and the lens chain are re-enqueued.
//
// Both refusals are NAMED. The populate gate held mid-flight ⇒
// FAILED_PRECONDITION carrying the in-flight job id. A chain already queued at
// this generation ⇒ an OK report whose `refusal` is set and whose `in_flight`
// carries those job ids — the ChainQueue dedup path.
//
// `nornir warehouse reset` is NOT this verb: it opens `catalog.redb` itself, so
// it refuses whenever a server is up — which is exactly when a Refresh is asked
// for.
rpc Repopulate (WorkspaceRepopulateRequest) returns (WorkspaceRepopulateReport);
}
// Field numbers byte-identical to nornir-wire's `nornir.proto`.
message WorkspaceRepopulateRequest {
string name = 1;
bool purge = 2;
// `BuildWorkspace { only: Affected }` (edda b4623dc): only the members that
// are behind and their dependents. false = every member.
bool affected = 3;
}
message WorkspaceRepopulateReport {
string workspace = 1;
bool purged = 2;
repeated string dropped = 3;
string generation = 4;
repeated string queued = 5;
repeated string in_flight = 6;
string refusal = 7;
}
// Carrier for the JSON-bodied read RPCs (field number matches nornir's).
message JsonResponse { string json = 1; }
message Empty {}
// Field numbers byte-identical to nornir/proto/nornir.proto's `WorkspaceName`.
message WorkspaceName {
string name = 1;
// Only honored by `Remove`: also delete the on-disk warehouse, not just the
// registry row. Default false = registry-only.
bool purge = 2;
}
message RegisterWorkspaceRequest {
string name = 1;
string descriptor = 2;
string mode = 3; // monitored | pushed | external; default pushed
string poll = 4;
// EAGER by default (lazy=false): record AND immediately clone members + build
// the warehouse inline (the populate). lazy=true = record-only.
bool lazy = 5;
// THIN-MODE descriptor SHIP: the client reads a local `nornir-workspace.toml`
// and ships its CONTENT here; the server writes it to its own descriptors dir
// and registers pointing at THAT server-local path. Empty = `descriptor`
// carries a git URL or an already-server-readable path.
string descriptor_content = 6;
}
message WorkspaceFetchRequest {
string name = 1;
bool force = 2;
bool background = 3;
}
message WorkspaceMember {
string name = 1;
string remote = 2;
string git_ref = 3;
string last_seen_sha = 4;
string last_synced = 5;
string sync_state = 6;
string worktree_digest = 7;
bool worktree_dirty = 8;
}
message WorkspaceRecord {
string name = 1;
string mode = 2;
string descriptor = 3;
string poll = 4;
string current_snapshot = 5;
repeated WorkspaceMember members = 6;
string created_at = 7;
string updated_at = 8;
}
message WorkspaceList { repeated WorkspaceRecord workspaces = 1; }
message WorkspaceFetchReport {
string workspace = 1;
uint32 fetched = 2;
repeated string changed = 3;
repeated string errors = 4; // "member: message"
string snapshot = 5; // new warehouse snapshot id if republished, else ""
}