items 5-7: passkey step-up, tools enable/disable, note RAG — end to end

Completes the three in-flight open items and fixes the away-fallthrough bug.

Item 7 — passkey step-up (WebAuthn):
- internal/webauthn: ES256/P-256 register + assert with real ecdsa signature
  verification, minimal CBOR/COSE decode, PasskeySession (L2→L3 on assert,
  decays after TTL). Drop the RS256 offer we can't verify (register-ok/
  assert-fail trap). Verify rpIdHash + UP/UV flags in FinishAssertion — UV is
  the step-up gesture. Round-trip test with negative cases (tampered sig,
  missing UV, wrong origin).
- cmd/mavweb: /auth/passkey enroll+assert page (the only surface that can do
  a WebAuthn gesture) + the four begin/finish endpoints. Without this the
  daemon's PasskeySession swap leaves /tools enable permanently blocked.
- daemon wires PasskeySession as the auth Session + srv.StepUp; policy gates
  MethodAssertStepUp at AuthRead.

Item 5 — tools page: DisableTool through store/ipc/client/wire; /tools grows a
disable action and a link to the passkey page. Lifecycle test.

Item 6 — note RAG: PhraseQuery on the phraser (LLM-composed answer over top-k
notes, raw-notes fallback); IntentQuery routes through it. Stub returns a
deterministic summary.

Item 2 — away-fallthrough: on ErrVoiceNoSession the dispatcher now reroutes
through the AWAY table (sev3→ntfy, sev4→telegram-repeat-til-ack, sev≤2→drop)
instead of silently dropping / mis-routing to the present-list remainder.
Covers DispatchNudge + DispatchReminder. 4 tests.

Also: re-add ProposeTool to CoreAPI (dropped in a comment rewrite), fix
missing imports + a duplicate block left mid-edit, drop dead AssertStepUpFunc,
gitignore /mavcaldav.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
kami
2026-07-03 18:41:13 +04:00
parent 36233058dd
commit 6239eca243
21 changed files with 1492 additions and 50 deletions
+32 -3
View File
@@ -166,6 +166,31 @@ func (p *LLMPhraser) PhraseNudge(ctx context.Context, c loop.Candidate) (deliver
return delivery.PhrasedNudge{Candidate: c, Body: body, Summary: summary}, nil
}
// PhraseQuery prompts the LLM with the user's utterance and matching notes to
// compose a natural answer. Falls back to "вот что я нашла: <notes>" on any
// LLM error — better to give the raw data than silence.
func (p *LLMPhraser) PhraseQuery(ctx context.Context, utterance string, notes []string) (string, error) {
if len(notes) == 0 {
return "у меня нет заметок по этому вопросу.", nil
}
if len(notes) == 1 {
notes[0] = strings.TrimSpace(notes[0])
}
sys := "You are maven, a self-hosted personal assistant answering from your notes. Answer briefly and naturally in Russian starting with \"вот что я нашла: \". Respond with just the answer text, no JSON wrapper."
prompt := fmt.Sprintf(
`The user asks: "%s". Your notes matching the query contain: "%s". Answer them naturally and briefly. If the notes don't answer the question, say so.`,
utterance, strings.Join(notes, `"; "`),
)
resp, err := p.chatWithSystem(ctx, sys, prompt, 256)
if err != nil {
if len(notes) == 1 {
return "вот что я нашла: " + notes[0], nil
}
return "вот что я нашла: " + strings.Join(notes, "; "), nil
}
return resp, nil
}
func (p *LLMPhraser) PhraseReminder(ctx context.Context, d loop.ReminderDecision) (delivery.PhrasedReminder, error) {
text := extractReminderText(d.Reminder.Payload)
if text == "" {
@@ -214,13 +239,17 @@ type chatResp struct {
}
func (p *LLMPhraser) chat(ctx context.Context, userPrompt string) (string, error) {
return p.chatWithSystem(ctx, systemPrompt(), userPrompt, 256)
}
func (p *LLMPhraser) chatWithSystem(ctx context.Context, system, user string, maxTokens int) (string, error) {
req := chatReq{
Messages: []chatMsg{
{Role: "system", Content: systemPrompt()},
{Role: "user", Content: userPrompt},
{Role: "system", Content: system},
{Role: "user", Content: user},
},
Temperature: 0.7,
MaxTokens: 256,
MaxTokens: maxTokens,
}
body, err := json.Marshal(req)
if err != nil {
+12
View File
@@ -43,6 +43,7 @@ import (
type Phraser interface {
PhraseNudge(ctx context.Context, c loop.Candidate) (delivery.PhrasedNudge, error)
PhraseReminder(ctx context.Context, d loop.ReminderDecision) (delivery.PhrasedReminder, error)
PhraseQuery(ctx context.Context, utterance string, notes []string) (string, error)
Close() error
}
@@ -59,6 +60,17 @@ type Stub struct{}
// NewStub builds the floor phraser. no config — the Stub is stateless.
func NewStub() *Stub { return &Stub{} }
// PhraseQuery returns a deterministic summary of the best matching notes.
func (s *Stub) PhraseQuery(_ context.Context, _ string, notes []string) (string, error) {
if len(notes) == 0 {
return "у меня нет заметок по этому вопросу.", nil
}
if len(notes) == 1 {
return "вот что я нашла: " + notes[0], nil
}
return "вот что я нашла: " + strings.Join(notes, "; "), nil
}
// Close implements Phraser.Close (no-op for the stub).
func (s *Stub) Close() error { return nil }