diff --git a/internal/ipc/api.go b/internal/ipc/api.go index 1eb8178..6e8d572 100644 --- a/internal/ipc/api.go +++ b/internal/ipc/api.go @@ -678,135 +678,6 @@ type acceptProposedRoutineReq struct { ID int64 `json:"id"` } -// CoreAPI — what core exposes to modules. One Go interface, satisfied by: -// - the in-process store adapter (server.go storeAPI) — used by the daemon -// for modules that live in-process for now (router, delivery) and by tests, -// - the socket-backed server's dispatcher (which delegates to a CoreAPI), -// - the client proxy (client.go) — same interface, over the wire. -// -// So a module imports ipc, holds a CoreAPI, and is agnostic to whether it's -// been wired in-process (tests / daemon-embedded) or socketed (full topology). -// That swappability is the seam the auth layer will insert into without -// touching the module code. -type CoreAPI interface { - WriteFact(ctx context.Context, req WriteFactReq) (int64, error) - LatestFact(ctx context.Context, key string) (Fact, error) - LatestFactBySource(ctx context.Context, key, source string) (Fact, error) - Since(ctx context.Context, key string, now time.Time) (time.Duration, error) - Presence(ctx context.Context) (Presence, error) - CreateReminder(ctx context.Context, fire time.Time, payload, cron string) (int64, error) - MarkReminder(ctx context.Context, id int64, status string) error - ListReminders(ctx context.Context, n int) ([]Reminder, error) - RecordNudge(ctx context.Context, rule, channel, message string, ts time.Time) (int64, error) - ResolveNudge(ctx context.Context, id int64, outcome string, ts time.Time) error - RecentOutcomes(ctx context.Context, rule string, n int) ([]string, error) - RecentFacts(ctx context.Context, n int) ([]Fact, error) - RecentActiveFactsByKind(ctx context.Context, kind string, n int) ([]Fact, error) - CalendarEvents(ctx context.Context, from, to time.Time) ([]Fact, error) - RecentNudges(ctx context.Context, n int) ([]Nudge, error) - // DeliveryAttempts reads the outbox, newest first. An empty status means - // every status (Vikunja #390). - DeliveryAttempts(ctx context.Context, status string, n int) ([]DeliveryAttempt, error) - - // RecentEcosystemTraces reads the ecosystem call log, which lives in its - // own table so machine-rate traces never crowd out human-rate facts. - RecentEcosystemTraces(ctx context.Context, n int) ([]EcosystemTrace, error) - WriteNote(ctx context.Context, ts time.Time, text string, embedding []float32, source string) (int64, error) - QueryNotes(ctx context.Context, embedding []float32, k int) ([]Note, error) - RecentNotes(ctx context.Context, n int) ([]Note, error) - // RecentNotesFromSource — the newest n notes whose source starts with - // prefix. Notes Maven read rather than heard (rss:, crawl:) are excluded - // from recall, so this is the only way to reach them, and it keeps the feed - // answer from being crowded out of a fixed window by his own notes. - RecentNotesFromSource(ctx context.Context, prefix string, n int) ([]Note, error) - - // ProposeTool drafts an inert 'proposed' tool scaffold (maven-callable); - // returns whether a new proposal was written. EnableTool fills cmd + - // destructive and flips status to 'enabled'. DisableTool reverts an - // enabled tool back to proposed (it stays in the store, won't run). - // Enable/DisableTool gate at AuthStepUp (allowlist mutation, human-only); - // ProposeTool is maven-callable (no step-up — she has no passkey). - // LookupTool/ListTools read them. - // scope defaults to "homelab" when empty. - ProposeTool(ctx context.Context, name, utterance, scope string, ts time.Time) (bool, error) - EnableTool(ctx context.Context, name string, cmd []string, destructive bool, scope string, ts time.Time) error - DisableTool(ctx context.Context, name string) error - DeleteTool(ctx context.Context, name string) error - LookupTool(ctx context.Context, name string) (Tool, error) - ListTools(ctx context.Context, status string) ([]Tool, error) - RevertFact(ctx context.Context, key string) (int64, error) - - // ListProposedRoutines returns proposed routines, newest first. - ListProposedRoutines(ctx context.Context) ([]ProposedRoutine, error) - // DismissProposedRoutine flips a proposed routine to 'dismissed'. - DismissProposedRoutine(ctx context.Context, id int64) error - // AcceptProposedRoutine flips a proposed routine to 'accepted'. The tick - // loop takes the schedule from there — no reminder is created (Vikunja #366). - AcceptProposedRoutine(ctx context.Context, id int64) error - - // CaptureTask records a task. See CaptureTaskReq — this is the single - // intake seam for the voice path, the web form and the future email - // extractor. Idempotent per live normalised text; the response says - // whether a row was actually created. - CaptureTask(ctx context.Context, req CaptureTaskReq) (CaptureTaskResp, error) - // ListTasks returns tasks in one status, newest first. "" is every row, - // "live" is candidate + open (outstanding work). - ListTasks(ctx context.Context, status string) ([]Task, error) - // SetTaskStatus moves a task forward once: candidate→open|dropped, - // open→done|dropped. Any other move is refused. - SetTaskStatus(ctx context.Context, id int64, status string, ts time.Time, by string) error - - // TickTrace returns the most recent tick's rule trace. The daemon caches - // this after every tick; the store adapter returns an error (trace is not - // persisted — it's a daemon-level cache). - TickTrace(ctx context.Context) (TickTrace, error) - - // MorningStatus returns each configured morning routine's current - // checklist state (see internal/morning): active today/now, which items - // are done, which are still missing. The store adapter returns an error - // (morning routines are daemon-config, not persisted) — same shape as - // TickTrace. - MorningStatus(ctx context.Context) ([]MorningRoutineStatus, error) - - // MCPServers reports the configured MCP servers and their health - // (Vikunja #251). Read-only introspection for /tools — there is no - // "call this tool" method on purpose: an MCP tool runs through the same - // allowlist, confirm turn and act path as any other tool, and a second - // mutation path would be a second thing to get wrong. Empty when the - // mcp config block is absent, which is the default. - MCPServers(ctx context.Context) ([]MCPServerStatus, error) - - // DayPlan returns today's ordered plan — calendar events, pending - // reminders and any morning checklist still outstanding (see - // internal/morning.BuildPlan) — plus the spoken RU rendering of it. - // Read-only: asking for the plan never dispatches or schedules anything. - // The store adapter returns an error (the plan needs the daemon's routine - // config) — same shape as TickTrace and MorningStatus. - DayPlan(ctx context.Context) (DayPlan, error) - - // Chat routes a text utterance through the reactive handler's core path - // (router → dialogue → action → replier) and returns the reply text. - // No audio or stt/tts — for text channels (mavweb, telegram). - // - // conversation names the thread. A parked clarifying question is held per - // conversation, so an unanswered question on one reach cannot eat the next - // utterance from another (Vikunja #466). Empty means the unattributed text - // tap and is still one conversation of its own, separate from the mic. - Chat(ctx context.Context, conversation, text string) (string, error) - - // RecentEvents returns the daemon's unified intake journal, newest first - // (Vikunja #283) — one envelope per thing that arrived, whatever direction - // it came from: a relayed notification, a mail candidate, a feed item, a - // changed page, a spend, a presence probe. - // - // Read-only and daemon-cached, the same shape as TickTrace and DayPlan: - // the store adapter returns an error, because the journal is a bounded - // in-memory ring and not a table. Its contents are a window over intake, - // never the durable record — that is still the fact, note or task the - // intake path wrote. - RecentEvents(ctx context.Context, n int) ([]IntakeEvent, error) -} - // IntakeEvent — one entry of the unified intake journal on the wire. Mirrors // event.Event field for field; the ipc package does not import internal/event // so the wire shape stays independent of the in-process type. diff --git a/internal/ipc/coreapi.go b/internal/ipc/coreapi.go new file mode 100644 index 0000000..481c517 --- /dev/null +++ b/internal/ipc/coreapi.go @@ -0,0 +1,194 @@ +package ipc + +import ( + "context" + "time" +) + +// CoreAPI and the eight domain interfaces it composes (Vikunja #408). +// +// It used to be one flat block of forty methods, and the cost of that shape +// was paid three times over: every method added an arm to the dispatcher, a +// stub to the daemon's locked API, and a stub to every test double. Two of +// those three are already gone — the dispatcher is a table (methodTable in +// server.go), and UnimplementedCoreAPI took the padding out of the doubles and +// out of lockedAPI, which no longer exists. +// +// What was left is the interface itself, and this file is that half. CoreAPI +// is unchanged as a type: the same forty methods, in the same order, so the +// wire contract, the client proxy and the store adapter are all untouched. +// What it gains is a named seam per domain, so a caller that only reads facts +// can say FactAPI and a reader can see which cluster a method belongs to +// without counting lines. +// +// Add a method to the domain it belongs to, not to CoreAPI. + +// FactAPI — the fact store: write, read the current value, read history, and +// undo. Presence sits here because it is a hysteresis view over presence +// facts, not a store of its own. +type FactAPI interface { + WriteFact(ctx context.Context, req WriteFactReq) (int64, error) + LatestFact(ctx context.Context, key string) (Fact, error) + LatestFactBySource(ctx context.Context, key, source string) (Fact, error) + Since(ctx context.Context, key string, now time.Time) (time.Duration, error) + Presence(ctx context.Context) (Presence, error) + RecentFacts(ctx context.Context, n int) ([]Fact, error) + RecentActiveFactsByKind(ctx context.Context, kind string, n int) ([]Fact, error) + CalendarEvents(ctx context.Context, from, to time.Time) ([]Fact, error) + RevertFact(ctx context.Context, key string) (int64, error) +} + +// ReminderAPI — scheduled sends the owner asked for. +type ReminderAPI interface { + CreateReminder(ctx context.Context, fire time.Time, payload, cron string) (int64, error) + MarkReminder(ctx context.Context, id int64, status string) error + ListReminders(ctx context.Context, n int) ([]Reminder, error) +} + +// NudgeAPI — proactive sends Maven proposed, their outcomes, and the outbox +// they went out through. +type NudgeAPI interface { + RecordNudge(ctx context.Context, rule, channel, message string, ts time.Time) (int64, error) + ResolveNudge(ctx context.Context, id int64, outcome string, ts time.Time) error + RecentOutcomes(ctx context.Context, rule string, n int) ([]string, error) + RecentNudges(ctx context.Context, n int) ([]Nudge, error) + // DeliveryAttempts reads the outbox, newest first. An empty status means + // every status (Vikunja #390). + DeliveryAttempts(ctx context.Context, status string, n int) ([]DeliveryAttempt, error) +} + +// NoteAPI — free text he captured, plus the embedded recall over it. +type NoteAPI interface { + WriteNote(ctx context.Context, ts time.Time, text string, embedding []float32, source string) (int64, error) + QueryNotes(ctx context.Context, embedding []float32, k int) ([]Note, error) + RecentNotes(ctx context.Context, n int) ([]Note, error) + // RecentNotesFromSource — the newest n notes whose source starts with + // prefix. Notes Maven read rather than heard (rss:, crawl:) are excluded + // from recall, so this is the only way to reach them, and it keeps the feed + // answer from being crowded out of a fixed window by his own notes. + RecentNotesFromSource(ctx context.Context, prefix string, n int) ([]Note, error) +} + +// ToolAPI — the capability allowlist and its read side. +// +// ProposeTool drafts an inert 'proposed' tool scaffold (maven-callable); +// returns whether a new proposal was written. EnableTool fills cmd + +// destructive and flips status to 'enabled'. DisableTool reverts an +// enabled tool back to proposed (it stays in the store, won't run). +// Enable/DisableTool gate at AuthStepUp (allowlist mutation, human-only); +// ProposeTool is maven-callable (no step-up — she has no passkey). +// LookupTool/ListTools read them. +// scope defaults to "homelab" when empty. +type ToolAPI interface { + ProposeTool(ctx context.Context, name, utterance, scope string, ts time.Time) (bool, error) + EnableTool(ctx context.Context, name string, cmd []string, destructive bool, scope string, ts time.Time) error + DisableTool(ctx context.Context, name string) error + DeleteTool(ctx context.Context, name string) error + LookupTool(ctx context.Context, name string) (Tool, error) + ListTools(ctx context.Context, status string) ([]Tool, error) + + // MCPServers reports the configured MCP servers and their health + // (Vikunja #251). Read-only introspection for /tools — there is no + // "call this tool" method on purpose: an MCP tool runs through the same + // allowlist, confirm turn and act path as any other tool, and a second + // mutation path would be a second thing to get wrong. Empty when the + // mcp config block is absent, which is the default. + MCPServers(ctx context.Context) ([]MCPServerStatus, error) +} + +// RoutineAPI — the shapes of his day: routines Maven noticed and proposed, the +// morning checklist, and today's plan. +// +// MorningStatus and DayPlan are daemon-computed rather than stored, so the +// store adapter returns an error for both — the same shape as TickTrace. +type RoutineAPI interface { + // ListProposedRoutines returns proposed routines, newest first. + ListProposedRoutines(ctx context.Context) ([]ProposedRoutine, error) + // DismissProposedRoutine flips a proposed routine to 'dismissed'. + DismissProposedRoutine(ctx context.Context, id int64) error + // AcceptProposedRoutine flips a proposed routine to 'accepted'. The tick + // loop takes the schedule from there — no reminder is created (Vikunja #366). + AcceptProposedRoutine(ctx context.Context, id int64) error + + // MorningStatus returns each configured morning routine's current + // checklist state (see internal/morning): active today/now, which items + // are done, which are still missing. + MorningStatus(ctx context.Context) ([]MorningRoutineStatus, error) + + // DayPlan returns today's ordered plan — calendar events, pending + // reminders and any morning checklist still outstanding (see + // internal/morning.BuildPlan) — plus the spoken RU rendering of it. + // Read-only: asking for the plan never dispatches or schedules anything. + DayPlan(ctx context.Context) (DayPlan, error) +} + +// TaskAPI — outstanding work, whatever surface it arrived from. +type TaskAPI interface { + // CaptureTask records a task. See CaptureTaskReq — this is the single + // intake seam for the voice path, the web form and the future email + // extractor. Idempotent per live normalised text; the response says + // whether a row was actually created. + CaptureTask(ctx context.Context, req CaptureTaskReq) (CaptureTaskResp, error) + // ListTasks returns tasks in one status, newest first. "" is every row, + // "live" is candidate + open (outstanding work). + ListTasks(ctx context.Context, status string) ([]Task, error) + // SetTaskStatus moves a task forward once: candidate→open|dropped, + // open→done|dropped. Any other move is refused. + SetTaskStatus(ctx context.Context, id int64, status string, ts time.Time, by string) error +} + +// SystemAPI — what the daemon knows about itself, plus the one method that +// runs a whole turn. +type SystemAPI interface { + // TickTrace returns the most recent tick's rule trace. The daemon caches + // this after every tick; the store adapter returns an error (trace is not + // persisted — it's a daemon-level cache). + TickTrace(ctx context.Context) (TickTrace, error) + + // RecentEcosystemTraces reads the ecosystem call log, which lives in its + // own table so machine-rate traces never crowd out human-rate facts. + RecentEcosystemTraces(ctx context.Context, n int) ([]EcosystemTrace, error) + + // RecentEvents returns the daemon's unified intake journal, newest first + // (Vikunja #283) — one envelope per thing that arrived, whatever direction + // it came from: a relayed notification, a mail candidate, a feed item, a + // changed page, a spend, a presence probe. + // + // Read-only and daemon-cached, the same shape as TickTrace and DayPlan: + // the store adapter returns an error, because the journal is a bounded + // in-memory ring and not a table. Its contents are a window over intake, + // never the durable record — that is still the fact, note or task the + // intake path wrote. + RecentEvents(ctx context.Context, n int) ([]IntakeEvent, error) + + // Chat routes a text utterance through the reactive handler's core path + // (router → dialogue → action → replier) and returns the reply text. + // No audio or stt/tts — for text channels (mavweb, telegram). + // + // conversation names the thread. A parked clarifying question is held per + // conversation, so an unanswered question on one reach cannot eat the next + // utterance from another (Vikunja #466). Empty means the unattributed text + // tap and is still one conversation of its own, separate from the mic. + Chat(ctx context.Context, conversation, text string) (string, error) +} + +// CoreAPI — what core exposes to modules. One Go interface, satisfied by: +// - the in-process store adapter (storeapi.go) — used by the daemon +// for modules that live in-process for now (router, delivery) and by tests, +// - the socket-backed server's dispatcher (which delegates to a CoreAPI), +// - the client proxy (client.go) — same interface, over the wire. +// +// So a module imports ipc, holds a CoreAPI, and is agnostic to whether it's +// been wired in-process (tests / daemon-embedded) or socketed (full topology). +// That swappability is the seam the auth layer inserts into without touching +// the module code. +type CoreAPI interface { + FactAPI + ReminderAPI + NudgeAPI + NoteAPI + ToolAPI + RoutineAPI + TaskAPI + SystemAPI +}