initial commit
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
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 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 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 to 'enabled' — the human-only "enable" act, gated
|
||||
// at AuthStepUp (see auth/policy.go). 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
|
||||
LookupTool(ctx context.Context, name string) (Tool, error)
|
||||
ListTools(ctx context.Context, status string) ([]Tool, 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")
|
||||
)
|
||||
Reference in New Issue
Block a user