Files
Maven/internal/ipc/api.go
T
kami a02e10fd11 ipc+mavweb: add revert/undo endpoint to void latest fact for a key
- New store.VoidLatestFact() method finds latest non-voided fact for
  a key and writes a void-marker row pointing at it (transactional).
- New IPC method MethodRevertFact with CoreAPI.RevertFact interface,
  storeAPI adapter, server dispatch, and client proxy.
- New HTTP endpoint POST /api/revert?key=<key> in mavweb.
- History page adds a 'revert' button per non-voided fact row with
  JS confirmation and optimistic UI (marks row voided on success).
- All existing store, IPC, and mavweb tests pass.
2026-07-05 02:18:39 +04:00

285 lines
10 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.
type Reminder struct {
ID int64 `json:"id"`
CreatedTs time.Time `json:"created_ts"`
FireTs time.Time `json:"fire_ts"`
Payload string `json:"payload"`
Status string `json:"status"` // pending|fired|cancelled
}
// 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"`
}
// 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 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"`
}
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"`
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"`
}
type proposeToolReq struct {
Name string `json:"name"`
Utterance string `json:"utterance"`
Ts time.Time `json:"ts"`
}
type proposeToolResp struct {
Proposed bool `json:"proposed"`
}
type enableToolReq struct {
Name string `json:"name"`
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"`
}
// 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 string) (int64, error)
MarkReminder(ctx context.Context, id int64, status string) 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)
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.
ProposeTool(ctx context.Context, name, utterance string, ts time.Time) (bool, error)
EnableTool(ctx context.Context, name string, cmd []string, destructive bool, ts time.Time) error
DisableTool(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)
}
// 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")
)