ipc: a seed_event method, step-up gated, refused by the store (V-518)
The pattern detector needs four events for one action+object spread by at least two hours before it proposes a routine. The only writer in the tree is a fact write at time.Now(), so V-43, V-46, V-247 and V-254 all stopped at the same missing step. This is the wire half of the seam that unblocks them. The request takes a fact — key, value, timestamp — not an event, so pattern.Extract runs for real on the daemon side and a key the extractor ignores seeds nothing. The response says which of those happened, because a caller that assumed a seed always yields an event would read four silent successes as a broken detector. AuthStepUp, the same rung as mutating the tool allowlist, and not because backdating is privileged in the usual sense: every other write records when something happened and this one asserts it. StoreAPI refuses outright — the method needs the daemon's detect-and-propose step, and a direct store caller would write a fact and quietly skip it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011x5DgnExQ5XZy8TZPs5bot
This commit is contained in:
@@ -91,6 +91,19 @@ func Requirement(m ipc.Method) Authority {
|
||||
// can do is make Maven stop recognising someone, which is the state the
|
||||
// box ships in anyway.
|
||||
return AuthWrite
|
||||
case ipc.MethodSeedEvent:
|
||||
// The one backdating write path in the tree (Vikunja #518). AuthStepUp,
|
||||
// the same rung as mutating the tool allowlist, and for a reason that is
|
||||
// not about privilege: every other write records when something actually
|
||||
// happened, and this one asserts it. A caller who can place a fact in the
|
||||
// past can manufacture a routine Maven will then act on forever, which is
|
||||
// the tick loop obeying evidence nobody produced.
|
||||
//
|
||||
// Step-up is not the real gate and is not meant to be. mavend refuses the
|
||||
// method entirely unless started with -allow-seed, so the ordinary state
|
||||
// of the box is that no gesture reaches it. This rung is what stops a
|
||||
// module from calling it on a box where QA left the flag on.
|
||||
return AuthStepUp
|
||||
case ipc.MethodWriteFact:
|
||||
return AuthWrite
|
||||
case ipc.MethodIngestMail:
|
||||
|
||||
@@ -184,6 +184,48 @@ type CaptureTaskResp struct {
|
||||
Promoted bool `json:"promoted,omitempty"`
|
||||
}
|
||||
|
||||
// SeedEventReq — write one fact at a caller-supplied timestamp and run the
|
||||
// pattern path over it, so a recurring routine can be produced on demand
|
||||
// instead of over real days (Vikunja #518).
|
||||
//
|
||||
// This is the ONLY backdating write path in the tree, and it exists for one
|
||||
// reason: the detector needs four events spread over hours before it proposes
|
||||
// anything, so V-43, V-46, V-247 and V-254 could not be verified against a
|
||||
// running daemon at all. A store fixture would have exercised the detector
|
||||
// without the wiring those tasks doubt.
|
||||
//
|
||||
// Two things hold it shut. It is AuthStepUp in the authority table, the same
|
||||
// rung as mutating the tool allowlist. And mavend refuses it outright unless
|
||||
// started with -allow-seed, so a box nobody is testing carries no live
|
||||
// backdating path even for a caller who cleared the gate.
|
||||
//
|
||||
// Key and Value are a fact, not an event: extraction runs for real, so a key
|
||||
// the extractor ignores seeds nothing and says so. That is deliberate — a
|
||||
// seam that accepted action and object directly would let QA prove a detector
|
||||
// against events no utterance could ever produce.
|
||||
type SeedEventReq struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
Ts time.Time `json:"ts"`
|
||||
}
|
||||
|
||||
// SeedEventResp — what the seed produced. Extracted is false when the fact was
|
||||
// written but yielded no event, which is the extractor declining rather than a
|
||||
// failure. Proposed is true only when this seed completed a pattern; the first
|
||||
// three seeds of a run return false with no routine.
|
||||
type SeedEventResp struct {
|
||||
FactID int64 `json:"fact_id"`
|
||||
EventID int64 `json:"event_id,omitempty"`
|
||||
Extracted bool `json:"extracted"`
|
||||
Action string `json:"action,omitempty"`
|
||||
Object string `json:"object,omitempty"`
|
||||
Proposed bool `json:"proposed"`
|
||||
RoutineID int64 `json:"routine_id,omitempty"`
|
||||
// IntervalDays — the median the detector settled on, echoed so QA can
|
||||
// check it against the spacing it asked for.
|
||||
IntervalDays float64 `json:"interval_days,omitempty"`
|
||||
}
|
||||
|
||||
// IngestMailReq — one message a mail reader has fetched, handed to core for
|
||||
// extraction (Vikunja #246).
|
||||
//
|
||||
|
||||
@@ -487,6 +487,14 @@ func (c *Client) ListProposedRoutines(ctx context.Context) ([]ProposedRoutine, e
|
||||
return r.Routines, nil
|
||||
}
|
||||
|
||||
func (c *Client) SeedEvent(ctx context.Context, req SeedEventReq) (SeedEventResp, error) {
|
||||
var r SeedEventResp
|
||||
if err := c.call(ctx, MethodSeedEvent, req, &r); err != nil {
|
||||
return SeedEventResp{}, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (c *Client) CaptureTask(ctx context.Context, req CaptureTaskReq) (CaptureTaskResp, error) {
|
||||
var r CaptureTaskResp
|
||||
if err := c.call(ctx, MethodCaptureTask, req, &r); err != nil {
|
||||
|
||||
@@ -110,6 +110,13 @@ type RoutineAPI interface {
|
||||
// loop takes the schedule from there — no reminder is created (Vikunja #366).
|
||||
AcceptProposedRoutine(ctx context.Context, id int64) error
|
||||
|
||||
// SeedEvent writes a backdated fact and runs extraction and detection over
|
||||
// it, so a proposal can be produced in one sitting rather than over real
|
||||
// days (Vikunja #518). See SeedEventReq for why this exists and what keeps
|
||||
// it shut. Daemon-computed, like MorningStatus — the store adapter refuses
|
||||
// it, because the detect-and-propose step lives in mavend.
|
||||
SeedEvent(ctx context.Context, req SeedEventReq) (SeedEventResp, 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.
|
||||
|
||||
@@ -524,6 +524,9 @@ var methodTable = map[Method]handlerFunc{
|
||||
MethodCaptureTask: withParams(func(ctx context.Context, api CoreAPI, p CaptureTaskReq) (CaptureTaskResp, error) {
|
||||
return api.CaptureTask(ctx, p)
|
||||
}),
|
||||
MethodSeedEvent: withParams(func(ctx context.Context, api CoreAPI, p SeedEventReq) (SeedEventResp, error) {
|
||||
return api.SeedEvent(ctx, p)
|
||||
}),
|
||||
MethodListTasks: withParams(func(ctx context.Context, api CoreAPI, p listTasksReq) (listTasksResp, error) {
|
||||
out, err := api.ListTasks(ctx, p.Status)
|
||||
if err != nil {
|
||||
|
||||
@@ -265,6 +265,14 @@ func (a *storeAPI) TickTrace(ctx context.Context) (TickTrace, error) {
|
||||
return TickTrace{}, errors.New("store: tick trace not available via direct store API")
|
||||
}
|
||||
|
||||
// SeedEvent — same shape as MorningStatus: writing the fact is a store call,
|
||||
// but extraction and detect-and-propose live in mavend, and a seed that wrote
|
||||
// the fact without running them would be the one thing this seam must not be,
|
||||
// a way to prove a detector that never ran (Vikunja #518).
|
||||
func (a *storeAPI) SeedEvent(ctx context.Context, req SeedEventReq) (SeedEventResp, error) {
|
||||
return SeedEventResp{}, errors.New("store: seed event not available via direct store API")
|
||||
}
|
||||
|
||||
func (a *storeAPI) MorningStatus(ctx context.Context) ([]MorningRoutineStatus, error) {
|
||||
return nil, errors.New("store: morning status not available via direct store API")
|
||||
}
|
||||
|
||||
@@ -105,6 +105,9 @@ func (UnimplementedCoreAPI) DeleteTool(ctx context.Context, name string) error {
|
||||
func (UnimplementedCoreAPI) CaptureTask(ctx context.Context, req CaptureTaskReq) (CaptureTaskResp, error) {
|
||||
return CaptureTaskResp{}, ErrNotImplemented
|
||||
}
|
||||
func (UnimplementedCoreAPI) SeedEvent(ctx context.Context, req SeedEventReq) (SeedEventResp, error) {
|
||||
return SeedEventResp{}, ErrNotImplemented
|
||||
}
|
||||
func (UnimplementedCoreAPI) ListTasks(ctx context.Context, status string) ([]Task, error) {
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ const (
|
||||
MethodListSpeakers Method = "list_speakers"
|
||||
MethodForgetSpeaker Method = "forget_speaker"
|
||||
MethodRecentEvents Method = "recent_events"
|
||||
MethodSeedEvent Method = "seed_event"
|
||||
|
||||
// MethodPing — liveness, and the only method that answers in locked mode
|
||||
// without a passkey assertion. It reaches no store, takes no arguments and
|
||||
|
||||
Reference in New Issue
Block a user