f42d1594ef
The extraction half. internal/email.Extractor asks the resident Qwen3-1.7B, under a GBNF grammar, what one message requires of him, and returns at most three short candidates with an optional date. Everything it can produce is a row in `tasks` with status "candidate", written through the intake seam #130 built for exactly this (Source "email:<mailbox>", Evidence = the subject line). No reminder, no fact, no note, no calendar event. That bound is the design: a reminder FIRES, so a 1.7B misreading "встреча была в четверг" as a future appointment would wake him up about it, whereas a wrong candidate is a line he dismisses in one click. A due date the model read out of the mail is stored on the candidate, where no scheduler reads it — the review page sorts by it. Relative wording ("до пятницы") is deliberately left in the text rather than resolved to a date the model would get wrong. The prompt is written against the two things a small model does here: it summarises when asked to extract, and it invents an obligation out of a polite closing line. Hence the demand for a verb phrase, and an explicit empty array — most mail contains no task, and a model with no way to say "nothing" says something. Wiring: core owns extraction because llama-server lives in core's process, so the reader hands messages over a new ipc.MethodIngestMail. It is a Server hook (like StepUp/UnlockFn), not a CoreAPI method — not a store operation, and no CoreAPI implementation should have to carry it. The hook stays nil without an `email` config block or without a llama-server phraser, so the method answers ErrUnknownMethod: off unless configured, twice over. There is no keyword fallback on purpose — "the subject became a task" is a mailbox rendered as a to-do list, not extraction. Privacy: junk is refused before the model is called, mail text is never search input, extraction errors carry byte counts rather than the reply, the stored evidence is a truncated subject, and the log line names the mailbox and the UID only.
558 lines
22 KiB
Go
558 lines
22 KiB
Go
package ipc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
)
|
|
|
|
// DTOs — wire-level data. Decoupled from internal/store so the protocol is
|
|
// self-describing and a module never needs to import store internals (the
|
|
// boundary is the point). The store adapter maps store.* ⇔ these 1:1.
|
|
|
|
// Fact — one observation. Ts is valid-time (true-as-of), as in store.
|
|
type Fact struct {
|
|
ID int64 `json:"id"`
|
|
Ts time.Time `json:"ts"`
|
|
Kind string `json:"kind"` // "self" | "env" | "config"
|
|
Key string `json:"key"`
|
|
Value string `json:"value"` // raw json if structured
|
|
Source string `json:"source"` // tap:*|infer:*|poll:*|ambient|promote|feedback
|
|
Confidence float64 `json:"confidence"`
|
|
VoidsID *int64 `json:"voids_id,omitempty"`
|
|
}
|
|
|
|
// Bucket — presence hysteresis state: "present" | "away".
|
|
type Bucket string
|
|
|
|
const (
|
|
Present Bucket = "present"
|
|
Away Bucket = "away"
|
|
)
|
|
|
|
// Nudge — one proactive send + its outcome, for the monitoring read path.
|
|
type Nudge struct {
|
|
ID int64 `json:"id"`
|
|
Ts time.Time `json:"ts"`
|
|
Rule string `json:"rule"`
|
|
Channel string `json:"channel"`
|
|
Message string `json:"message"`
|
|
Outcome string `json:"outcome"` // pending|acted|snoozed|ignored
|
|
OutcomeTs *int64 `json:"outcome_ts,omitempty"`
|
|
}
|
|
|
|
// Note — a recall/preference item; ranked by embedding cosine on query.
|
|
// Score is set by QueryNotes (0 on the write path).
|
|
type Note struct {
|
|
ID int64 `json:"id"`
|
|
Ts time.Time `json:"ts"`
|
|
Text string `json:"text"`
|
|
Source string `json:"source"`
|
|
Score float64 `json:"score"`
|
|
}
|
|
|
|
// Reminder — user-stated future intent; fires once or recurring (if cron set).
|
|
type Reminder struct {
|
|
ID int64 `json:"id"`
|
|
CreatedTs time.Time `json:"created_ts"`
|
|
FireTs time.Time `json:"fire_ts"`
|
|
NextFireTs time.Time `json:"next_fire_ts"`
|
|
Payload string `json:"payload"`
|
|
Status string `json:"status"` // pending|fired|cancelled
|
|
Cron string `json:"cron"`
|
|
}
|
|
|
|
// Presence — the read the phraser / delivery modules need to decide channel
|
|
// routing and tone. presence = reachability, NOT wakefulness (spec). Loop
|
|
// reads probes + computes score itself; modules get the resolved snapshot.
|
|
type Presence struct {
|
|
Bucket Bucket `json:"bucket"`
|
|
Score float64 `json:"score"`
|
|
Updated time.Time `json:"updated"`
|
|
}
|
|
|
|
// WriteFactReq — the only state mutation a capture/tool module performs.
|
|
// Confidence is 1.0 for taps, (0,1) for inferences; the store enforces range.
|
|
// Source is provenance — the server-side source-scope seam (auth layer) will
|
|
// refuse a module writing under a source it doesn't own ("compromised poller
|
|
// can't forge a trigger"). Today the floor permits any local caller.
|
|
type WriteFactReq struct {
|
|
Ts time.Time `json:"ts"`
|
|
Kind string `json:"kind"`
|
|
Key string `json:"key"`
|
|
Value string `json:"value"`
|
|
Source string `json:"source"`
|
|
Confidence float64 `json:"confidence"`
|
|
VoidsID *int64 `json:"voids_id,omitempty"`
|
|
|
|
// Subject — free-text "who/what this fact is about" (e.g. "the espresso
|
|
// machine", "Kate"). Empty (the default, so old callers are unaffected)
|
|
// means the fact isn't about a resolvable entity. When set, the fact
|
|
// enrichment worker (cmd/mavend/factenrichment.go) later resolves it
|
|
// against Nexus into an entity_id — see Vikunja #279.
|
|
Subject string `json:"subject,omitempty"`
|
|
}
|
|
|
|
// Task — one captured piece of work (Vikunja #130). Status is
|
|
// "candidate" (Maven derived it and it is unconfirmed), "open" (his work),
|
|
// "done" or "dropped". Source is provenance in the facts vocabulary:
|
|
// "tap:voice", "tap:web", "email:<account>". Evidence is the trail a derived
|
|
// task came from, empty for anything he stated himself.
|
|
type Task struct {
|
|
ID int64 `json:"id"`
|
|
CreatedTs time.Time `json:"created_ts"`
|
|
Text string `json:"text"`
|
|
Source string `json:"source"`
|
|
Evidence string `json:"evidence,omitempty"`
|
|
Status string `json:"status"`
|
|
Due *time.Time `json:"due,omitempty"`
|
|
Weight int `json:"weight,omitempty"`
|
|
Resolved *time.Time `json:"resolved,omitempty"`
|
|
}
|
|
|
|
// CaptureTaskReq — THE INTAKE SEAM. Everything that captures a task goes
|
|
// through this one shape: the voice path, the web form, and (Vikunja #246) the
|
|
// email reader, which has not been built yet.
|
|
//
|
|
// An extractor that reads mail sets Source "email:<account>", Status
|
|
// "candidate", and Evidence to whatever makes the task reviewable (the subject
|
|
// line). It must NOT set Status "open" — work Maven inferred from something she
|
|
// read is a suggestion until the owner confirms it on the /tasks page. Capture
|
|
// is idempotent on normalised text among live tasks, so re-reading the same
|
|
// mailbox is free.
|
|
type CaptureTaskReq struct {
|
|
Text string `json:"text"`
|
|
Source string `json:"source"`
|
|
Evidence string `json:"evidence,omitempty"`
|
|
Status string `json:"status,omitempty"` // "" ⇒ open
|
|
Due *time.Time `json:"due,omitempty"`
|
|
Weight int `json:"weight,omitempty"`
|
|
Ts time.Time `json:"ts"`
|
|
}
|
|
|
|
// CaptureTaskResp — Created is false when the same live task already existed,
|
|
// in which case ID is the existing row. A caller tells the owner "уже в
|
|
// списке" rather than claiming it saved something new.
|
|
type CaptureTaskResp struct {
|
|
ID int64 `json:"id"`
|
|
Created bool `json:"created"`
|
|
}
|
|
|
|
// IngestMailReq — one message a mail reader has fetched, handed to core for
|
|
// extraction (Vikunja #246).
|
|
//
|
|
// The mail reader (cmd/mavmaild) holds the IMAP credential and core never sees
|
|
// it, the same split mavpoll uses for the zenmoney token. What crosses this
|
|
// boundary is only the message text, because extraction runs on the resident
|
|
// model and llama-server lives inside core's process.
|
|
//
|
|
// Body is already plaintext and truncated by internal/email; core does not
|
|
// re-parse MIME and never stores the body. Junk means the reader's header
|
|
// filter already classified the message as bulk — core is told rather than
|
|
// asked, so a junk message can be counted without a model call.
|
|
//
|
|
// This method is available only when core has an email block configured AND a
|
|
// llama-server phraser; otherwise it answers ErrUnknownMethod, which is what
|
|
// "off unless configured" looks like at the wire.
|
|
type IngestMailReq struct {
|
|
Mailbox string `json:"mailbox"`
|
|
UID uint32 `json:"uid"`
|
|
From string `json:"from,omitempty"`
|
|
Subject string `json:"subject,omitempty"`
|
|
Date string `json:"date,omitempty"`
|
|
Body string `json:"body,omitempty"`
|
|
Junk bool `json:"junk,omitempty"`
|
|
}
|
|
|
|
// IngestMailResp — what core did with the message. TaskIDs are the rows
|
|
// CaptureTask returned; Created counts the ones that were new (a re-read
|
|
// mailbox dedupes to Created=0). Skipped is set when nothing was asked of the
|
|
// model at all — junk, or an empty message.
|
|
//
|
|
// Nothing here echoes the mail back. The reader logs counts.
|
|
type IngestMailResp struct {
|
|
TaskIDs []int64 `json:"task_ids,omitempty"`
|
|
Created int `json:"created"`
|
|
Skipped bool `json:"skipped,omitempty"`
|
|
}
|
|
|
|
type listTasksReq struct {
|
|
Status string `json:"status"` // "" all | "live" | candidate|open|done|dropped
|
|
}
|
|
type listTasksResp struct {
|
|
Tasks []Task `json:"tasks"`
|
|
}
|
|
type setTaskStatusReq struct {
|
|
ID int64 `json:"id"`
|
|
Status string `json:"status"`
|
|
Ts time.Time `json:"ts"`
|
|
}
|
|
|
|
// idReq — methods keyed by a single id.
|
|
type idReq struct {
|
|
ID int64 `json:"id"`
|
|
}
|
|
|
|
// markReminderReq — pending→fired|cancelled.
|
|
type markReminderReq struct {
|
|
ID int64 `json:"id"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
// resolveNudgeReq — pending→acted|snoozed|ignored, once.
|
|
type resolveNudgeReq struct {
|
|
ID int64 `json:"id"`
|
|
Outcome string `json:"outcome"`
|
|
Ts time.Time `json:"ts"`
|
|
}
|
|
|
|
// keyReq / keySourceReq / sinceReq / outcomesReq — read param shapes.
|
|
type keyReq struct {
|
|
Key string `json:"key"`
|
|
}
|
|
type keySourceReq struct {
|
|
Key string `json:"key"`
|
|
Source string `json:"source"`
|
|
}
|
|
type sinceReq struct {
|
|
Key string `json:"key"`
|
|
Now time.Time `json:"now"`
|
|
}
|
|
type outcomesReq struct {
|
|
Rule string `json:"rule"`
|
|
N int `json:"n"`
|
|
}
|
|
type nReq struct {
|
|
N int `json:"n"`
|
|
}
|
|
type calendarEventsReq struct {
|
|
From time.Time `json:"from"`
|
|
To time.Time `json:"to"`
|
|
}
|
|
type revertReq struct {
|
|
Key string `json:"key"`
|
|
}
|
|
type writeNoteReq struct {
|
|
Ts time.Time `json:"ts"`
|
|
Text string `json:"text"`
|
|
Embedding []float32 `json:"embedding"`
|
|
Source string `json:"source"`
|
|
}
|
|
type queryNotesReq struct {
|
|
Embedding []float32 `json:"embedding"`
|
|
K int `json:"k"`
|
|
}
|
|
type createReminderReq struct {
|
|
Fire time.Time `json:"fire"`
|
|
Payload string `json:"payload"`
|
|
Cron string `json:"cron"`
|
|
}
|
|
type recordNudgeReq struct {
|
|
Rule string `json:"rule"`
|
|
Channel string `json:"channel"`
|
|
Message string `json:"message"`
|
|
Ts time.Time `json:"ts"`
|
|
}
|
|
|
|
// idResp / sinceResp — small scalar return wrappers.
|
|
type idResp struct {
|
|
ID int64 `json:"id"`
|
|
}
|
|
type sinceResp struct {
|
|
Dur time.Duration `json:"dur"`
|
|
}
|
|
|
|
// Tool — an act allowlist entry as core exposes it. Status 'proposed' is an
|
|
// inert scaffold; 'enabled' is runnable. The executor only runs 'enabled'.
|
|
type Tool struct {
|
|
Name string `json:"name"`
|
|
Scope string `json:"scope"`
|
|
Cmd []string `json:"cmd"`
|
|
Destructive bool `json:"destructive"`
|
|
Status string `json:"status"`
|
|
Utterance string `json:"utterance"`
|
|
Created time.Time `json:"created"`
|
|
Updated time.Time `json:"updated"`
|
|
}
|
|
|
|
// chatReq / chatResp — text chat round-trip for the IPC Chat method.
|
|
type chatReq struct {
|
|
Text string `json:"text"`
|
|
}
|
|
type chatResp struct {
|
|
Reply string `json:"reply"`
|
|
}
|
|
|
|
type proposeToolReq struct {
|
|
Name string `json:"name"`
|
|
Scope string `json:"scope"`
|
|
Utterance string `json:"utterance"`
|
|
Ts time.Time `json:"ts"`
|
|
}
|
|
type proposeToolResp struct {
|
|
Proposed bool `json:"proposed"`
|
|
}
|
|
type enableToolReq struct {
|
|
Name string `json:"name"`
|
|
Scope string `json:"scope"`
|
|
Cmd []string `json:"cmd"`
|
|
Destructive bool `json:"destructive"`
|
|
Ts time.Time `json:"ts"`
|
|
}
|
|
type disableToolReq struct {
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type lookupToolReq struct {
|
|
Name string `json:"name"`
|
|
}
|
|
type listToolsReq struct {
|
|
Status string `json:"status"`
|
|
}
|
|
type listToolsResp struct {
|
|
Tools []Tool `json:"tools"`
|
|
}
|
|
|
|
// ProposedRoutine — a detected pattern awaiting human confirmation.
|
|
type ProposedRoutine struct {
|
|
ID int64 `json:"id"`
|
|
Action string `json:"action"`
|
|
Object string `json:"object"`
|
|
IntervalDays float64 `json:"interval_days"`
|
|
Status string `json:"status"` // proposed | accepted | dismissed
|
|
CreatedTs int64 `json:"created_ts"`
|
|
ReminderID *int64 `json:"reminder_id,omitempty"`
|
|
}
|
|
|
|
type listProposedRoutinesResp struct {
|
|
Routines []ProposedRoutine `json:"routines"`
|
|
}
|
|
|
|
type dismissProposedRoutineReq struct {
|
|
ID int64 `json:"id"`
|
|
}
|
|
|
|
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)
|
|
CalendarEvents(ctx context.Context, from, to time.Time) ([]Fact, error)
|
|
RecentNudges(ctx context.Context, n int) ([]Nudge, 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)
|
|
|
|
// 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) 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)
|
|
|
|
// 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).
|
|
Chat(ctx context.Context, text string) (string, error)
|
|
}
|
|
|
|
// --- Rule trace / explanation DTOs ---
|
|
|
|
// RuleTrace — per-rule evaluation result for one tick.
|
|
type RuleTrace struct {
|
|
RuleName string `json:"rule_name"`
|
|
Severity int `json:"severity"`
|
|
PredicateResult bool `json:"predicate_result"`
|
|
GateResult bool `json:"gate_result"`
|
|
GateBlockedBy string `json:"gate_blocked_by,omitempty"`
|
|
GateDetail GateDetail `json:"gate_detail,omitempty"`
|
|
WasSelected bool `json:"was_selected"`
|
|
LostTo string `json:"lost_to,omitempty"`
|
|
}
|
|
|
|
// GateDetail — snapshot of the values the gate checked.
|
|
type GateDetail struct {
|
|
SnoozeUntil *time.Time `json:"snooze_until,omitempty"`
|
|
CooldownUntil *time.Time `json:"cooldown_until,omitempty"`
|
|
QuietHours bool `json:"quiet_hours"`
|
|
CalendarBusy bool `json:"calendar_busy"`
|
|
Presence string `json:"presence"`
|
|
InertKeysMissing []string `json:"inert_keys_missing,omitempty"`
|
|
}
|
|
|
|
// TickTrace — snapshot of one tick's rule evaluations.
|
|
type TickTrace struct {
|
|
Now time.Time `json:"now"`
|
|
Winner string `json:"winner"`
|
|
Rules []RuleTrace `json:"rules"`
|
|
}
|
|
|
|
// MorningRoutineItem — one checklist entry's current state.
|
|
type MorningRoutineItem struct {
|
|
Key string `json:"key"`
|
|
Label string `json:"label"`
|
|
Done bool `json:"done"`
|
|
}
|
|
|
|
// MorningRoutineStatus — one routine's checklist state right now.
|
|
type MorningRoutineStatus struct {
|
|
Name string `json:"name"`
|
|
Active bool `json:"active"`
|
|
WindowStart string `json:"window_start"`
|
|
WindowEnd string `json:"window_end"`
|
|
Items []MorningRoutineItem `json:"items"`
|
|
}
|
|
|
|
// DayPlanItem — one line of the day plan. Kind is "event", "reminder" or
|
|
// "checklist"; Uncertain marks an item whose provenance is below a full
|
|
// calendar read (a meeting relayed off a phone notification), so a UI can hedge
|
|
// the same way the spoken form does.
|
|
type DayPlanItem struct {
|
|
At time.Time `json:"at"`
|
|
Text string `json:"text"`
|
|
Kind string `json:"kind"`
|
|
Uncertain bool `json:"uncertain,omitempty"`
|
|
}
|
|
|
|
// DayPlan — the plan for one calendar day. Spoken is the RU sentence maven
|
|
// says when asked, rendered core-side so the voice reply and the web view can
|
|
// never drift apart.
|
|
type DayPlan struct {
|
|
Date time.Time `json:"date"`
|
|
Items []DayPlanItem `json:"items"`
|
|
Spoken string `json:"spoken"`
|
|
}
|
|
|
|
// storeEncryptionKeyReq — passkey credential public key for wrapping the store
|
|
// encryption key at enrollment time. Called by mavweb after RegisterFinish.
|
|
type storeEncryptionKeyReq struct {
|
|
PublicKey []byte `json:"public_key"`
|
|
}
|
|
|
|
// unlockReq — passkey credential public key for unwrapping the store
|
|
// encryption key at cold-start. mavend reads the wrapped blob from its own
|
|
// configured path; the public key is the other half needed for unwrapping.
|
|
type unlockReq struct {
|
|
PublicKey []byte `json:"public_key"`
|
|
}
|
|
|
|
// ErrToolNotFound — no tool row with this name (re-exported store sentinel for
|
|
// wire round-tripping via errors.Is).
|
|
var ErrToolNotFound = errors.New("ipc: tool not found")
|
|
|
|
// callerKey — context key for the authenticated caller. Server sets it from
|
|
// SO_PEERCRED before dispatch; in-process callers omit it (the adapter treats
|
|
// a missing Caller as "trusted same-process", the equivalent of the socket's
|
|
// 0600 floor).
|
|
type callerKey struct{}
|
|
|
|
// Caller — the peer identity as core sees it. Uid/Pid come from SO_PEERCRED
|
|
// on Linux; the future auth layer maps Uid + module enrollment → authority.
|
|
// Today only Uid is populated and used for a same-user check.
|
|
type Caller struct {
|
|
Uid int32
|
|
Pid int32
|
|
}
|
|
|
|
// WithCaller returns ctx annotated with c. Server-side use only.
|
|
func WithCaller(ctx context.Context, c Caller) context.Context {
|
|
return context.WithValue(ctx, callerKey{}, c)
|
|
}
|
|
|
|
// CallerFrom retrieves the Caller, or ok=false if absent (in-process path).
|
|
func CallerFrom(ctx context.Context) (Caller, bool) {
|
|
c, ok := ctx.Value(callerKey{}).(Caller)
|
|
return c, ok
|
|
}
|
|
|
|
// Sentinel errors. Mirror store's 1:1 so module code reads the same whether
|
|
// in-process or over the wire. The store adapter translates store.* → these.
|
|
var (
|
|
ErrNoFact = errors.New("ipc: no fact for key")
|
|
ErrConfidence = errors.New("ipc: confidence must be in (0.0, 1.0]")
|
|
ErrVoidsMissing = errors.New("ipc: voids_id does not reference an existing fact")
|
|
ErrNudgeNotFound = errors.New("ipc: nudge not found")
|
|
ErrNudgeOutcome = errors.New("ipc: nudge already resolved")
|
|
ErrReminderNotFound = errors.New("ipc: reminder not found")
|
|
ErrReminderState = errors.New("ipc: reminder not in a mutable state")
|
|
ErrUnknownMethod = errors.New("ipc: unknown method")
|
|
ErrBadParams = errors.New("ipc: bad params")
|
|
// ErrForbidden — the caller's authority doesn't cover this call. The
|
|
// auth layer's only wire-exported verdict: surface caps the layer, or a
|
|
// write was out-of-scope, or step-up was required but not asserted. The
|
|
// text ErrForbidden carries is derived during dispatch (from auth.ErrForbidden
|
|
// via fmt.Errorf %w wrapping); the wire carries codeForbidden.
|
|
ErrForbidden = errors.New("ipc: forbidden")
|
|
)
|