Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 138 additions & 4 deletions eval/feature-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,34 @@ export const FEATURES: FeatureSpec[] = [
"agent-created loops carry source='agent' + the owner's user_id (auditable + per-owner scoped)",
],
},
{
id: "managed-loop-override",
summary:
"Consumer Loops audit + control surface (MobileApi.ListLoops/SetLoopEnabled). The hosted user owns none of the instance's `system`-owned background loops, so ListLoops also surfaces a curated managed set (auto-dream -> 'Brain consolidation', style-analyze -> 'Writing style learning') under friendly labels. Toggling one writes a per-user app.userLoop.<name>.enabled config override (setLoopUserEnabled) instead of mutating the shared system row; cron-engine honors it at fire time as an AND-gate (isLoopUserDisabled).",
trigger: { kind: "turn" },
entry: [
"curateConsumerLoops",
"isLoopUserDisabled",
"setLoopUserEnabled",
"userLoopOverrideKey",
],
effects: [
{
claim: "toggling a managed loop persists app.userLoop.<name>.enabled in the config table",
sql: {
query: "SELECT count(*) FROM config WHERE key LIKE 'app.userLoop.%'",
expect: "nonzero",
},
notExercised: true,
},
],
invariants: [
"managed loops display friendly labels but toggle/delete key off the real job name",
"AND-gate: a managed loop fires only if its system row is enabled AND the user has not opted out",
"the shared system cron_jobs row is never mutated per-user (per-customer DB scoping)",
"infra plumbing (wiki/graph/magic-docs/delta-sync) + the proactive family are hidden from the consumer surface",
],
},

// ── Wired runtime helpers (dormant-prone) ──
{
Expand Down Expand Up @@ -794,20 +822,27 @@ export const FEATURES: FeatureSpec[] = [
},
{
id: "draft-edit-learning",
summary: "Capture user edits to drafts as corrections that update the user model.",
summary:
"Capture user edits to drafts as corrections that update the user model (approveWithEdit -> captureDraftEdit -> updateUserModel). Only an actual edit (edited != original) is captured; a plain approve is not.",
trigger: { kind: "turn" },
entry: ["approveWithEdit"],
effects: [
{
claim: "edits land as user_model corrections",
// A correction is stored as a fact whose key is `correction_<slug>` and
// whose value is { text: corrected, original } (see updateUserModel).
claim: "edits land as user_model corrections (category='fact', key LIKE 'correction_%')",
sql: {
query: "SELECT count(*) FROM user_model WHERE category='correction'",
query:
"SELECT count(*) FROM user_model WHERE category = 'fact' AND key LIKE 'correction_%'",
expect: "nonzero",
},
notExercised: true,
},
],
invariants: ["per-owner scoped"],
invariants: [
"per-owner scoped",
"only an actual edit is learned (plain approve writes no correction)",
],
},
{
id: "shadow-observer",
Expand Down Expand Up @@ -877,6 +912,105 @@ export const FEATURES: FeatureSpec[] = [
},
],
},
{
id: "consumer-advanced-surface",
summary:
"Hosted Advanced curation (MobileApi.ListSkills/ToggleSkill/ListPlugins). ListSkills filters the full skill catalog to consumer-facing skills (operator-curated external Google skills + an allowlist of bundled consumer skills like pdf/xlsx/weather), under friendly labels, with each skill's persisted on/off folded in (skill.<name>.enabled). ToggleSkill resolves the friendly label back to the raw name (resolveSkillName) before persisting. ListPlugins returns a curated read-only built-in tool set instead of the developer marketplace plugins.",
trigger: { kind: "turn" },
entry: ["curateConsumerSkills", "resolveSkillName", "isConsumerSkill"],
effects: [
{
claim: "toggling a skill persists skill.<name>.enabled in the config table",
sql: {
query: "SELECT count(*) FROM config WHERE key LIKE 'skill.%.enabled'",
expect: "nonzero",
},
notExercised: true,
},
],
invariants: [
"consumer skills = external (operator-curated) + an allowlist of bundled consumer skills; dev/internal/channel skills are hidden",
"skills display friendly labels but the toggle round-trips back to the raw skill name",
"ListPlugins surfaces the curated built-in tool set (read-only), not the developer marketplace plugins",
],
},
{
id: "scheduled-tasks",
summary:
"Consumer Tasks surface (MobileApi.ListTasks/UpdateTask/DeleteTask). ListTasks returns the user's own scheduled cron_jobs (one-off 'at' reminders + recurring jobs created via schedule_task/loop_create), owner-scoped by user_id so the instance's system-owned background loops never appear. UpdateTask reschedules/renames/edits the instruction/enables; DeleteTask removes one. Both assert ownership before mutating.",
trigger: { kind: "turn" },
entry: ["curateConsumerTasks", "toConsumerTask"],
effects: [
{
claim: "the user's scheduled tasks are stored as owner-scoped cron_jobs",
sql: {
query: "SELECT count(*) FROM cron_jobs WHERE source IN ('agent','user')",
expect: "nonzero",
},
notExercised: true,
},
],
invariants: [
"Tasks are owner-scoped (user_id); managed/system loops never appear on this surface",
"UpdateTask/DeleteTask assert job.userId === the resolved owner before mutating",
"schedule_task stamps source='agent' (a user-owned task, not infra)",
],
},
{
id: "brain-overview",
summary:
"Consumer Brain page (MobileApi.GetBrain). Composes the read model from real per-user memory: the knowledge graph (kg_nodes/kg_edges via getProjection) drives the map + entities, and the accumulated user_model drives the recently-learned facts feed. Owner-scoped via TenantContext.",
trigger: { kind: "turn" },
entry: ["getBrainOverview", "getProjection"],
effects: [
{
claim: "the brain map reads nodes from the per-user knowledge graph",
sql: { query: "SELECT count(*) FROM kg_nodes", expect: "nonzero" },
notExercised: true,
},
],
invariants: [
"owner-scoped: nodes/edges/facts filtered by user_id",
"facts come from user_model (confidence binned to 0..3); entities/edges from kg_nodes/kg_edges",
],
},
{
id: "inbox-overview",
summary:
"Consumer Inbox (MobileApi.GetInbox). Two owner-scoped sections: the agent's drafted replies awaiting approval (draft_messages: pending=needs-you, approved/sent=handled) and the CATE agent-to-agent inbound queue (cate_inbound, best-effort). Draft actions reuse ApproveDraft/RejectDraft; CATE via ActOnInboxItem.",
trigger: { kind: "turn" },
entry: ["getInboxOverview"],
effects: [
{
claim: "pending drafts awaiting approval are owner-scoped in draft_messages",
sql: { query: "SELECT count(*) FROM draft_messages", expect: "nonzero" },
notExercised: true,
},
],
invariants: [
"owner-scoped by user_id",
"drafts + CATE merged read-only; mutations via existing draft/inbox RPCs",
],
},
{
id: "today-overview",
summary:
"Consumer Today brief (MobileApi.GetToday). Composes today's Google Calendar events (live via gapiFetch, best-effort -- empty when Google isn't connected), pending commitments, and the user's scheduled tasks. briefingEnabled=false (proactive autonomy off) tells the client to show the enable-briefing deep link.",
trigger: { kind: "turn" },
entry: ["getTodayOverview"],
effects: [
{
claim: "commitments feeding the brief are owner-scoped",
sql: { query: "SELECT count(*) FROM commitments", expect: "nonzero" },
notExercised: true,
},
],
invariants: [
"owner-scoped",
"calendar is best-effort (empty when Google isn't connected)",
"gated on app.inboxAutonomy != 'off'",
],
},

// ── Consent-aware drafting ──
{
Expand Down
169 changes: 169 additions & 0 deletions proto/nomos.proto
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ service MobileApi {
// Skills tab
rpc ListSkills (Empty) returns (MSkillsResponse);
rpc ToggleSkill (MSkillToggleRequest) returns (MSkillToggleResponse);
rpc ListPlugins (Empty) returns (MPluginsResponse);

// Earnings tab
rpc GetEarnings (MEarningsRequest) returns (MEarningsResponse);
Expand All @@ -178,6 +179,8 @@ service MobileApi {
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);
Expand All @@ -199,6 +202,23 @@ service MobileApi {
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);

// 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);
Expand Down Expand Up @@ -234,6 +254,109 @@ 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;
}
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;
repeated MTodayTask tasks = 4;
}

message MAck {
bool success = 1;
string message = 2;
Expand Down Expand Up @@ -372,6 +495,16 @@ 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;
Expand Down Expand Up @@ -478,6 +611,42 @@ message MSettingsResponse {
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 {
Expand Down
Loading
Loading