syntax = "proto3";

package nomos;

service NomosAgent {
  // Send a chat message and receive a stream of agent events
  rpc Chat (ChatRequest) returns (stream AgentEvent);

  // Send a command (e.g., /compact)
  rpc Command (CommandRequest) returns (CommandResponse);

  // Get daemon status
  rpc GetStatus (Empty) returns (StatusResponse);

  // Session management
  rpc ListSessions (Empty) returns (SessionList);
  rpc GetSession (SessionRequest) returns (SessionResponse);

  // Draft management (Slack User Mode approve-before-send)
  rpc ListDrafts (Empty) returns (DraftList);
  rpc ApproveDraft (DraftAction) returns (DraftResponse);
  rpc RejectDraft (DraftAction) returns (DraftResponse);

  // Autonomous loops (recurring background jobs — audit + disable)
  rpc ListLoops (Empty) returns (LoopList);
  rpc SetLoopEnabled (SetLoopEnabledRequest) returns (LoopActionResponse);
  rpc DeleteLoop (LoopDeleteRequest) returns (LoopActionResponse);

  // Scheduled tasks (the local owner's cron_jobs) — mirrors MobileApi for local mode.
  rpc ListTasks (Empty) returns (MTasksResponse);
  rpc UpdateTask (MTaskUpdateRequest) returns (MAck);
  rpc DeleteTask (MTaskDeleteRequest) returns (MAck);

  // Action items (the ranked commitments list) — check off / snooze / delegate.
  rpc CompleteCommitment (MCommitmentAction) returns (MAck);
  rpc SnoozeCommitment (MCommitmentSnooze) returns (MAck);
  rpc DelegateCommitment (MCommitmentDelegate) returns (MAck);

  // Answer a pending ask_user elicitation out-of-band (does not enqueue a turn).
  rpc AnswerQuestion (MAnswerRequest) returns (MAck);

  // Health check
  rpc Ping (Empty) returns (PongResponse);
}

message Empty {}

// ── Autonomous loops ──

message LoopInfo {
  string id = 1;
  string name = 2;
  string schedule = 3;
  bool enabled = 4;
  string source = 5;       // system | bundled | user | agent
  int32 error_count = 6;
  string last_run = 7;     // ISO-8601, empty if never run
  string prompt = 8;
}
message LoopList {
  repeated LoopInfo loops = 1;
}
message SetLoopEnabledRequest {
  string name = 1;
  bool enabled = 2;
}
message LoopDeleteRequest {
  string name = 1;
}
message LoopActionResponse {
  bool success = 1;
  string message = 2;
}

// ── Chat ──

message ChatRequest {
  string content = 1;
  string session_key = 2;
}

message AgentEvent {
  // Event type: "stream_event", "tool_use_summary", "result", "system", "error", "pong"
  string type = 1;
  // JSON-encoded event data (preserves flexibility of current protocol)
  string json_payload = 2;
}

// ── Commands ──

message CommandRequest {
  string command = 1;
  string session_key = 2;
}

message CommandResponse {
  bool success = 1;
  string message = 2;
}

// ── Status ──

message StatusResponse {
  bool running = 1;
  int32 connected_clients = 2;
  repeated string platforms = 3;
}

// ── Sessions ──

message SessionRequest {
  string session_key = 1;
}

message SessionResponse {
  string id = 1;
  string session_key = 2;
  string model = 3;
  string created_at = 4;
}

message SessionList {
  repeated SessionResponse sessions = 1;
}

// ── Drafts ──

message DraftAction {
  string draft_id = 1;
}

message DraftResponse {
  bool success = 1;
  string message = 2;
}

message DraftList {
  repeated DraftItem drafts = 1;
}

message DraftItem {
  string id = 1;
  string content = 2;
  string platform = 3;
  string channel_id = 4;
  string status = 5;
  string created_at = 6;
}

// ── Ping ──

message PongResponse {
  int64 timestamp = 1;
}

// ── Mobile API ──
// Surface for the Expo mobile app. Five tabs map to handler groups:
//   Chat / Inbox / Skills / Earnings / Settings.
//
// All calls require a Bearer JWT issued by the central Better Auth server.
// Validation + tenant context resolution happens in the gRPC interceptor;
// handlers receive the authenticated TenantContext via call metadata.

service MobileApi {
  // Chat tab
  rpc Chat (MChatRequest) returns (stream MChatEvent);
  rpc GetMessages (MGetMessagesRequest) returns (MGetMessagesResponse);
  rpc ApproveDraft (MDraftAction) returns (MDraftActionResponse);
  rpc RejectDraft (MDraftAction) returns (MDraftActionResponse);
  rpc ApproveDraftWithEdit (MDraftActionWithEdit) returns (MDraftActionResponse);

  // Inbox tab (CATE inbound queue — Phase 5b)
  rpc ListInbox (MInboxRequest) returns (MInboxResponse);
  rpc GetCateEnvelope (MEnvelopeRequest) returns (MCateEnvelope);
  rpc ActOnInboxItem (MInboxActionRequest) returns (MInboxActionResponse);

  // Skills tab
  rpc ListSkills (Empty) returns (MSkillsResponse);
  rpc ToggleSkill (MSkillToggleRequest) returns (MSkillToggleResponse);
  rpc ListPlugins (Empty) returns (MPluginsResponse);

  // Earnings tab
  rpc GetEarnings (MEarningsRequest) returns (MEarningsResponse);

  // Brain tab (knowledge graph)
  rpc GetGraph (MGraphRequest) returns (MGraphResponse);
  rpc GetGraphNeighbors (MGraphNeighborsRequest) returns (MGraphResponse);
  rpc SearchGraph (MGraphSearchRequest) returns (MGraphResponse);

  // Settings tab
  rpc GetSettings (Empty) returns (MSettingsResponse);
  rpc UpdateConsent (MConsentRequest) returns (MAck);
  rpc UpdateTrustTier (MTrustTierRequest) returns (MAck);
  rpc UpdatePermission (MPermissionRequest) returns (MAck);
  rpc UpdateAppSetting (MAppSettingRequest) returns (MAck);
  rpc UpdateAgentIdentity (MAgentIdentityRequest) returns (MAck);
  rpc ListIntegrations (Empty) returns (MIntegrationsResponse);
  rpc StartConnectIntegration (MStartConnectRequest) returns (MStartConnectResponse);
  rpc ConnectGoogleAccount (MConnectGoogleRequest) returns (MAck);
  rpc SetGoogleSend (MSetGoogleSendRequest) returns (MAck);
  rpc DisconnectIntegration (MDisconnectRequest) returns (MAck);

  // Push notifications
  rpc RegisterDevice (MDeviceRegister) returns (MAck);
  rpc UnregisterDevice (MDeviceUnregister) returns (MAck);

  // Vault tab (long-term memory / knowledge base)
  rpc ListVaultNotes (MVaultListRequest) returns (MVaultListResponse);
  rpc GetVaultNote (MVaultGetRequest) returns (MVaultNote);
  rpc WriteVaultNote (MVaultWriteRequest) returns (MAck);
  rpc DeleteVaultNote (MVaultDeleteRequest) returns (MAck);

  // Loops tab (autonomous recurring jobs — audit + disable agent-created loops)
  rpc ListLoops (Empty) returns (MLoopsResponse);
  rpc SetLoopEnabled (MSetLoopEnabledRequest) returns (MAck);
  rpc DeleteLoop (MLoopDeleteRequest) returns (MAck);

  // Tasks tab (the user's scheduled tasks: one-off reminders + recurring jobs,
  // editable: reschedule, rename, edit instruction, enable/disable, delete)
  rpc ListTasks (Empty) returns (MTasksResponse);
  rpc UpdateTask (MTaskUpdateRequest) returns (MAck);
  rpc DeleteTask (MTaskDeleteRequest) returns (MAck);

  // Action items (the ranked commitments list) — check off / snooze / delegate.
  rpc CompleteCommitment (MCommitmentAction) returns (MAck);
  rpc SnoozeCommitment (MCommitmentSnooze) returns (MAck);
  rpc DelegateCommitment (MCommitmentDelegate) returns (MAck);

  // Answer a pending ask_user elicitation out-of-band (does not enqueue a turn).
  rpc AnswerQuestion (MAnswerRequest) returns (MAck);

  // Brain tab (the user's knowledge graph + learned facts, for the feed + map)
  rpc GetBrain (Empty) returns (MBrainResponse);

  // Inbox tab (drafts to approve + CATE agent requests). Actions reuse
  // ApproveDraft/RejectDraft (drafts) + ActOnInboxItem (CATE).
  rpc GetInbox (Empty) returns (MGetInboxResponse);

  // Today tab (the daily brief: calendar + commitments + tasks, gated on the
  // daily briefing being enabled).
  rpc GetToday (Empty) returns (MTodayResponse);

  // Studio (hosted-only feature). Blobs move via presigned PUT/GET, never gRPC.
  rpc StudioCreateAsset (MStudioCreateAssetRequest) returns (MStudioCreateAssetResponse);
  rpc StudioGetAssetUrl (MStudioAssetRef) returns (MStudioAssetUrlResponse);
  rpc StudioEdit (MStudioEditRequest) returns (stream MStudioEvent);
  rpc StudioHistory (MStudioAssetRef) returns (MStudioHistoryResponse);
  // Recent editing sessions for the Home launchpad ("Pick up where you left off").
  rpc StudioListAssets (MStudioListAssetsRequest) returns (MStudioListAssetsResponse);
  // AI-native: a vision model looks at the photo and proposes tap-to-apply edits.
  rpc StudioSuggestEdits (MStudioAssetRef) returns (MStudioSuggestionsResponse);
  // The on-device identity check reports its score for an edit (0..1).
  rpc StudioReportIdentity (MStudioIdentityReport) returns (MAck);

  // Device bridge — the phone exposes its native EventKit (Calendar + Reminders)
  // as agent tools. The app opens DeviceBridge on launch and the daemon streams
  // tool invocations down it; the app runs each via EventKit and returns the
  // result through SubmitDeviceResult (correlated by id). The agent never touches
  // the device directly — it calls the native-device MCP tools, which route here.
  rpc DeviceBridge (MDeviceHello) returns (stream MDeviceInvocation);
  rpc SubmitDeviceResult (MDeviceResult) returns (MAck);

  // Compose tab — used by the iMessage app extension. A one-shot, non-streaming draft:
  // the user gives an instruction ("tell my mom I'll be late"), the agent returns a few
  // ready-to-send message drafts; the user picks one and Messages sends it.
  rpc ComposeReply (MComposeRequest) returns (MComposeResponse);
}

// Loops (autonomous recurring jobs)
message MLoop {
  string id = 1;
  string name = 2;
  string schedule = 3;
  bool enabled = 4;
  string source = 5;       // system | bundled | user | agent
  int32 error_count = 6;
  string last_run = 7;     // ISO-8601, empty if never run
  string prompt = 8;
}
message MLoopsResponse {
  repeated MLoop loops = 1;
}
message MSetLoopEnabledRequest {
  string name = 1;
  bool enabled = 2;
}
message MLoopDeleteRequest {
  string name = 1;
}

// Tasks (the user's scheduled tasks)
message MTask {
  string id = 1;
  string name = 2;
  string prompt = 3;             // the instruction the task runs
  string schedule = 4;           // raw: "15m" | cron expr | ISO timestamp
  string schedule_type = 5;      // every | cron | at
  string display_schedule = 6;   // friendly, display-only
  bool enabled = 7;
  string source = 8;
  string last_run = 9;           // ISO-8601, empty if never run
}
message MTasksResponse {
  repeated MTask tasks = 1;
}
// Full-state update (the client sends the edited task); empty name/schedule are
// ignored so a toggle-only call never blanks fields.
message MTaskUpdateRequest {
  string id = 1;
  string name = 2;
  string prompt = 3;
  string schedule = 4;
  string schedule_type = 5;
  bool enabled = 6;
}
message MTaskDeleteRequest {
  string id = 1;
}

// Brain (knowledge graph + learned facts)
message MBrainNode {
  string id = 1;
  string label = 2;
  string kind = 3;       // person | org | topic | decision | project | value | event | wiki | vault
  string summary = 4;
  int32 degree = 5;      // connection count within the returned subgraph
  double confidence = 6;
}
message MBrainEdge {
  string src = 1;
  string dst = 2;
  string relation = 3;
}
message MBrainFact {
  string text = 1;
  string source = 2;
  int32 confidence = 3;  // 0..3
  string learned_at = 4; // ISO-8601
}
message MBrainResponse {
  repeated MBrainNode nodes = 1;
  repeated MBrainEdge edges = 2;
  repeated MBrainFact facts = 3;
  int32 entity_count = 4;
  int32 fact_count = 5;
}

// Inbox (drafts + CATE agent requests)
message MInboxDraft {
  string id = 1;
  string recipient = 2;
  string preview = 3;
  string status = 4;        // pending | approved | sent
  string platform = 5;
  string created_at = 6;
}
message MInboxCate {
  string id = 1;
  string from_label = 2;
  string trust_tier = 3;
  string subject = 4;
  string bond_amount = 5;
  string created_at = 6;
}
message MGetInboxResponse {
  repeated MInboxDraft drafts = 1;
  repeated MInboxCate cate = 2;
  int32 blocked_count = 3;
}

// Today (the daily brief)
message MTodayEvent {
  string time = 1;
  string title = 2;
  string meta = 3;
}
message MTodayCommitment {
  string id = 1;
  string description = 2;
  string due = 3;
  string priority = 4;    // p0..p3, empty if unranked
  string rank_reason = 5; // why it's ranked here (one line)
  string direction = 6;   // mine | theirs
  string contact = 7;     // the other party, if known
}
message MTodayTask {
  string id = 1;
  string name = 2;
  string schedule = 3;
}
message MTodayResponse {
  bool briefing_enabled = 1;
  repeated MTodayEvent events = 2;
  repeated MTodayCommitment commitments = 3;  // "needs you" — items I owe, ranked
  repeated MTodayTask tasks = 4;
  repeated MTodayCommitment waiting = 5;       // "waiting on others" — items owed to me
}

// Action-item mutations (the ranked commitments list).
message MCommitmentAction {
  string id = 1;
}
message MCommitmentSnooze {
  string id = 1;
  string until = 2;  // ISO date/time to push the item's deadline to
}
message MCommitmentDelegate {
  string id = 1;
  string to = 2;     // who it's delegated to (a name, or 'nomos')
}

message MAck {
  bool success = 1;
  string message = 2;
}

// Answer to a pending ask_user elicitation, delivered out-of-band so it does not
// queue behind the suspended turn that is awaiting it.
message MAnswerRequest {
  string session_key = 1;
  string question_id = 2;
  string answer = 3;
}

// Vault (long-term memory / knowledge base)
message MVaultListRequest {
  string prefix = 1;
}
message MVaultNoteSummary {
  string path = 1;
  string title = 2;
  string updated_at = 3;
}
message MVaultListResponse {
  repeated MVaultNoteSummary notes = 1;
}
message MVaultGetRequest {
  string path = 1;
}
message MVaultNote {
  string path = 1;
  string title = 2;
  string content = 3;
  string updated_at = 4;
  bool exists = 5;
}
message MVaultWriteRequest {
  string path = 1;
  string content = 2;
  string title = 3;
}
message MVaultDeleteRequest {
  string path = 1;
}

// Chat
message MChatRequest {
  string content = 1;
  string session_key = 2;
}

message MChatEvent {
  string type = 1;
  string json_payload = 2;
}

message MGetMessagesRequest {
  string session_key = 1;
  int32 limit = 2;
  string before_id = 3;
}

message MMessage {
  string id = 1;
  string role = 2;          // user | agent
  string content = 3;
  string created_at = 4;
}

message MGetMessagesResponse {
  repeated MMessage messages = 1;
}

message MDraftAction {
  string draft_id = 1;
}

message MDraftActionWithEdit {
  string draft_id = 1;
  string edited_text = 2;
}

message MDraftActionResponse {
  bool success = 1;
  string message = 2;
}

// Inbox
message MInboxRequest {
  string status = 1;        // pending | approved | denied | all
  int32 limit = 2;
}

message MInboxItem {
  string id = 1;
  string from_label = 2;
  string trust_tier = 3;    // verified | bonded | friend | blocked | unknown
  string subject = 4;
  string time = 5;          // human-formatted by client
  string bond_amount = 6;   // empty if free
  bool unread = 7;
  string created_at = 8;
}

message MInboxResponse {
  repeated MInboxItem items = 1;
  int32 blocked_count = 2;
}

message MEnvelopeRequest {
  string inbox_id = 1;
}

message MCateEnvelope {
  string did = 1;
  string trust_tier = 2;
  string intent = 3;
  string consent_grant = 4;
  string stamp = 5;
  string bond_amount = 6;
  string raw_json = 7;
}

message MInboxActionRequest {
  string inbox_id = 1;
  string action = 2;        // approve | deny | block
}

message MInboxActionResponse {
  bool success = 1;
  string message = 2;
}

// Skills
message MSkill {
  string name = 1;
  string description = 2;
  string source = 3;        // bundled | personal | project
  bool enabled = 4;
  repeated string certs = 5;
  string price = 6;
}

message MSkillsResponse {
  repeated MSkill skills = 1;
}

message MPlugin {
  string name = 1;
  string description = 2;
  string marketplace = 3;
}

message MPluginsResponse {
  repeated MPlugin plugins = 1;
}

message MSkillToggleRequest {
  string name = 1;
  bool enabled = 2;
}

message MSkillToggleResponse {
  bool success = 1;
  string message = 2;
}

// Earnings
message MEarningsRequest {
  string period = 1;        // month | week | all
}

message MEarningsResponse {
  // All amounts in USD cents.
  int64 this_period_cents = 1;
  int64 bonds_count = 2;
  int64 avg_bond_cents = 3;
  int32 accept_rate_pct = 4;
  // Daily series for sparkline (cents per day, oldest → newest).
  repeated int64 series_cents = 5;
}

// Brain (knowledge graph)
message MGraphRequest {
  repeated string kinds = 1;     // optional node-kind filter (person|project|topic|...)
  int32 limit = 2;               // max nodes (default 500)
}

message MGraphNeighborsRequest {
  string node_id = 1;
  int32 depth = 2;               // hops, 1-3 (default 2)
  repeated string rel_types = 3; // optional relationship-type filter
  string direction = 4;          // in | out | both (default both)
}

message MGraphSearchRequest {
  string query = 1;
  int32 limit = 2;
}

message MGraphNode {
  string id = 1;
  string kind = 2;
  string name = 3;
  repeated string aliases = 4;
  string summary = 5;
  double confidence = 6;
  string external_kind = 7;      // contact | wiki | memory_chunk | ...
  string external_ref = 8;
}

message MGraphEdge {
  string id = 1;
  string src_id = 2;
  string dst_id = 3;
  string rel_type = 4;
  string fact = 5;
  double weight = 6;
}

message MGraphResponse {
  repeated MGraphNode nodes = 1;
  repeated MGraphEdge edges = 2;
}

// Settings
message MTrustTier {
  string id = 1;
  string name = 2;
  string description = 3;
  string mode = 4;          // free | blocked | bond
  string bond_amount = 5;
}

message MPermission {
  string id = 1;
  string label = 2;
  bool enabled = 3;
}

message MIntegration {
  string id = 1;            // for google: the account email (disconnect/toggle key)
  string label = 2;
  string icon = 3;
  bool connected = 4;
  string account_email = 5;
  bool send_enabled = 6;    // google: whether the agent may send email
  string provider = 7;      // google | slack | ...
  // google: the services this account actually granted, derived from its scopes —
  // gmail | calendar | drive | classroom. Lets the client label a classroom-only
  // school account correctly (not "Gmail · Calendar · Drive") and hide the send toggle.
  repeated string services = 8;
}

message MProfile {
  string name = 1;
  string plan = 2;
  int64 message_count = 3;
  int64 earned_cents = 4;
  int64 saved_cents = 5;
}

message MSettingsResponse {
  MProfile profile = 1;
  repeated MTrustTier trust_tiers = 2;
  repeated MPermission permissions = 3;
  repeated MIntegration integrations = 4;
  repeated MConsentEntry consent = 5;
  MAgentIdentity identity = 6;
  repeated MAppToggle app_toggles = 7;
  MProactive proactive = 8;
}

message MProactive {
  string mode = 1;          // off | passive | active (app.inboxAutonomy)
  string briefing = 2;      // cron for the daily briefing (app.briefingCron)
}

message MConsentEntry {
  string platform = 1;
  string mode = 2;          // always_ask | auto_approve | notify_only
}

message MAgentIdentity {
  string name = 1;
  string voice = 2;         // personality / voice & tone (the SOUL)
  string avatar = 3;        // chosen avatar (an emoji), empty = monogram fallback
}

message MAppToggle {
  string key = 1;           // app.<field> config key
  bool enabled = 2;
}

message MAppSettingRequest {
  string key = 1;           // app.<field> config key (consumer-safe subset only)
  string value = 2;         // "true"/"false" for bools, raw string otherwise
}

message MAgentIdentityRequest {
  string name = 1;
  string voice = 2;
  string avatar = 3;
}

message MConsentRequest {
  string platform = 1;
  string mode = 2;          // always_ask | auto_approve | notify_only
}

message MTrustTierRequest {
  string id = 1;
  string mode = 2;
  string bond_amount = 3;
}

message MPermissionRequest {
  string id = 1;
  bool enabled = 2;
}

message MIntegrationsResponse {
  repeated MIntegration integrations = 1;
  // Whether the Google Classroom capability is enabled on this deployment
  // (FEATURES.classroom / NOMOS_CLASSROOM, off by default). Clients hide the
  // Classroom connector entirely when false.
  bool classroom_enabled = 2;
}

message MStartConnectRequest {
  string provider = 1;      // gmail | calendar | slack | notion | linkedin | classroom | classroom_write
}

message MStartConnectResponse {
  // URL the client opens in a browser to begin OAuth. For google this is the
  // daemon-built Google consent URL (state-signed); the callback then relays the
  // code back via ConnectGoogleAccount.
  string oauth_url = 1;
}

message MDisconnectRequest {
  string integration_id = 1;
  string account_email = 2;   // google: disconnect by account email (preferred)
}

// Relay an OAuth authorization code from the (web/mobile) callback to the daemon,
// which exchanges it for tokens and stores the connected Google account. The
// connecting user is the JWT subject; `state` is the signed CSRF token.
message MConnectGoogleRequest {
  string code = 1;
  string state = 2;
}

message MSetGoogleSendRequest {
  string account_email = 1;
  bool enabled = 2;
}

message MDeviceRegister {
  string expo_push_token = 1;
  string platform = 2;      // ios | android
  string app_version = 3;
}

message MDeviceUnregister {
  string expo_push_token = 1;
}

// ── Device bridge (native EventKit tools) ──
// The phone opens DeviceBridge with the capabilities it is offering this session.
message MDeviceHello {
  repeated string capabilities = 1; // e.g. "calendar", "reminders"
}

// A tool invocation the daemon pushes down to the phone.
message MDeviceInvocation {
  string id = 1;        // correlates with MDeviceResult.id
  string tool = 2;      // e.g. "calendar_create_event", "reminders_list"
  string args_json = 3; // JSON-encoded tool arguments
}

// The phone's result for one invocation, returned via SubmitDeviceResult.
message MDeviceResult {
  string id = 1;
  bool ok = 2;
  string result_json = 3; // JSON-encoded result payload (when ok)
  string error = 4;       // human-readable error (when !ok)
}

// ── Compose (iMessage extension) ──
message MComposeRequest {
  string instruction = 1;  // what the user wants to say, in their words
  string context_hint = 2; // optional: the message being replied to (if the user pasted it)
}
message MComposeResponse {
  repeated string drafts = 1; // a few ready-to-send drafts; the user picks one
}

// ── OAuth Deposit ──
// Used by the central Better Auth server to push OAuth tokens it obtained on
// the customer's behalf into the customer's nomos instance. The call is
// authenticated via mTLS (client cert signed by the central CA). Tokens are
// stored encrypted in the customer's `integrations` / `google_accounts` /
// `slack_workspaces` tables.

service OAuthDeposit {
  rpc Deposit (DepositRequest) returns (DepositResponse);
}

message DepositRequest {
  // gmail | calendar | drive | slack | notion | ...
  string provider = 1;
  // Authenticated user inside the customer's instance (BA user.id).
  // The instance enforces that this user_id is a member of NOMOS_ORG_ID.
  string user_id = 2;
  string access_token = 3;
  string refresh_token = 4;
  // Unix seconds.
  int64 expires_at = 5;
  // Comma-separated OAuth scopes granted.
  string scopes = 6;
  // Provider-specific extras (e.g., {"workspace_id": "T0123", "account_email": "x@y"}).
  map<string, string> metadata = 7;
}

message DepositResponse {
  bool success = 1;
  string message = 2;
  // Opaque integration id assigned by the customer instance (for revocation).
  string integration_id = 3;
}

// ── Studio (hosted-only feature) ──────────────────────────────────────
// Register an uploaded original. The client uploads the (downscaled, transcoded)
// image to upload_url (presigned PUT), then confirms by calling StudioEdit or
// StudioHistory; unconfirmed rows are reaped by __studio_gc__.
message MStudioCreateAssetRequest {
  string mime = 1;          // e.g. image/jpeg (HEIC transcoded client-side)
  string content_hash = 2;  // sha256 of the bytes
  int32 width = 3;
  int32 height = 4;
  int32 bytes = 5;
}
message MStudioCreateAssetResponse {
  string asset_id = 1;
  string upload_url = 2;    // presigned PUT
  string object_key = 3;
  int64 expires_at = 4;     // ms epoch
}

message MStudioAssetRef {
  string asset_id = 1;
  bool original = 2;        // GetAssetUrl: presign the ORIGINAL (for before/after compare)
}
message MStudioAssetUrlResponse {
  string url = 1;           // presigned GET for the current head (or original)
  int64 expires_at = 2;
}

// Apply one op. params_json is the JSON-encoded op params (validated server-side
// against the op registry). parent_edit_id "" means "on the current head".
message MStudioEditRequest {
  string asset_id = 1;
  string op = 2;            // adjust | editSemantic | cutout | upscale | restore | ...
  string params_json = 3;
  string parent_edit_id = 4;
  string idempotency_key = 5;
  string mask_key = 6;      // optional device/tap mask object key
  bytes input_image = 7;    // deviceRender only: the on-device-rendered output bytes
}
message MStudioEvent {
  string kind = 1;          // progress | done | error
  string edit_id = 2;
  string status = 3;        // pending | running | done | failed
  string preview_key = 4;
  string output_key = 5;
  double cost_usd = 6;
  string message = 7;
}

message MStudioEdit {
  string id = 1;
  string op = 2;
  string status = 3;
  string preview_key = 4;
  string output_key = 5;
  double cost_usd = 6;
  string parent_edit_id = 7;
  string created_at = 8;
}
message MStudioHistoryResponse {
  repeated MStudioEdit edits = 1;
  string head_edit_id = 2;
}

message MStudioIdentityReport {
  string edit_id = 1;
  double score = 2; // face-embedding similarity in 0..1
}

// A recent editing session for the Home launchpad. An asset + the gist of its chain.
message MStudioListAssetsRequest {
  int32 limit = 1;          // max sessions (server caps; default ~30)
}
message MStudioAssetSummary {
  string asset_id = 1;
  string preview_url = 2;   // presigned GET (~thumbnail): head preview, else original
  int64 updated_at = 3;     // ms epoch
  bool finalized = 4;       // a stored final artifact (vs in-progress)
  int32 edit_count = 5;
  string head_op = 6;       // op of the head edit ("" if none) — client humanizes
  int64 expires_at = 7;     // ms epoch for preview_url
}
message MStudioListAssetsResponse {
  repeated MStudioAssetSummary assets = 1;
}

// A per-photo edit suggestion: a short chip label + the instruction it applies.
message MStudioSuggestion {
  string label = 1;
  string prompt = 2;
}
message MStudioSuggestionsResponse {
  repeated MStudioSuggestion suggestions = 1;
}
