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:
@@ -78,6 +78,20 @@ func (s *Store) EnableTool(ctx context.Context, name string, cmd []string, destr
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisableTool sets a tool's status from 'enabled' back to 'proposed'. This is
|
||||
// the "disable" act on the authed surface — the tool stays in the store (its
|
||||
// provenance preserved) but won't run until re-enabled. Idempotent: disabling
|
||||
// a tool that is already proposed or doesn't exist is a no-op.
|
||||
func (s *Store) DisableTool(ctx context.Context, name string) error {
|
||||
_, err := s.db.ExecContext(ctx,
|
||||
`UPDATE tools SET status = 'proposed', updated_ts = ? WHERE name = ? AND status = 'enabled'`,
|
||||
time.Now().UnixMilli(), name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("disable tool: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LookupTool returns the tool by name. ErrToolNotFound when absent.
|
||||
func (s *Store) LookupTool(ctx context.Context, name string) (Tool, error) {
|
||||
row := s.db.QueryRowContext(ctx, `
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestToolLifecycle covers propose → enable → disable, the states the authed
|
||||
// /tools page drives. Disable must revert an enabled tool to 'proposed' (kept
|
||||
// in the store, won't run) and be idempotent.
|
||||
func TestToolLifecycle(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
|
||||
if _, err := s.ProposeTool(ctx, "restart_svc", "restart the service", now); err != nil {
|
||||
t.Fatalf("propose: %v", err)
|
||||
}
|
||||
if err := s.EnableTool(ctx, "restart_svc", []string{"systemctl", "restart", "x"}, true, now); err != nil {
|
||||
t.Fatalf("enable: %v", err)
|
||||
}
|
||||
if tl, _ := s.LookupTool(ctx, "restart_svc"); tl.Status != "enabled" {
|
||||
t.Fatalf("after enable: status=%q want enabled", tl.Status)
|
||||
}
|
||||
|
||||
if err := s.DisableTool(ctx, "restart_svc"); err != nil {
|
||||
t.Fatalf("disable: %v", err)
|
||||
}
|
||||
tl, err := s.LookupTool(ctx, "restart_svc")
|
||||
if err != nil {
|
||||
t.Fatalf("lookup after disable: %v", err)
|
||||
}
|
||||
if tl.Status != "proposed" {
|
||||
t.Fatalf("after disable: status=%q want proposed", tl.Status)
|
||||
}
|
||||
|
||||
// idempotent: disabling an already-proposed (or absent) tool is a no-op.
|
||||
if err := s.DisableTool(ctx, "restart_svc"); err != nil {
|
||||
t.Fatalf("disable idempotent: %v", err)
|
||||
}
|
||||
if err := s.DisableTool(ctx, "does_not_exist"); err != nil {
|
||||
t.Fatalf("disable absent must be no-op: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user