chore: docker, config, delivery sinks, dialogue, and agent docs
- Dockerfile: multi-stage build with CGO_ENABLED=0, embedder model copy, non-root user, healthcheck, and /data volume. - docker-compose.yml: mavend + mavweb services with shared volume, health checks, and restart policy. - .gitignore: ignore models/llm/*.gguf, deploy/telegram.env, tmp artifacts. - deploy/mavend.json: add LLM, phraser, voice sections (embedder, model paths, wake sensitivity). Add telegram token env-var expansion. - deploy/telegram.env.example: template for telegram bot token. - internal/config/config.go: add LLM config struct, voice config struct (embedder, llama, wake sensitivity), telegram token loading. - telegramsink: add chat intent delivery support alongside existing types. - voicesink: skip empty payloads in delivery. - dialogue/session: add chat intent to anaphora resolution, test coverage. - AGENTS.md: update with LLM embedder, LFM model download/configure steps, new UI conventions. - REARCH.md: architecture research document. - cmd/mavend/main.go: wire LLM config, phraser, embedder, telegram config, WebAuthn, IPC event/routine handlers, and reactive notes.
This commit is contained in:
+100
-38
@@ -21,19 +21,20 @@
|
||||
// verifier + ask-password transport (open spec item).
|
||||
//
|
||||
// Cold-start unlock (2026-07-06):
|
||||
// When a passkey credential is enrolled AND no env key is set, the daemon
|
||||
// starts in LOCKED mode: the IPC server runs but rejects all store methods
|
||||
// except MethodAssertStepUp and MethodUnlock. A passkey assertion followed
|
||||
// by MethodUnlock (with the same credential's public key) unwraps the at-rest
|
||||
// AES-256 key from a wrapped blob on disk (HKDF-SHA256 + AES-GCM) and opens
|
||||
// the encrypted store. After unlock, the daemon wires voice, loop, and
|
||||
// delivery and runs normally.
|
||||
//
|
||||
// Fallback: when db_key_env is set (or no wrapped file exists), the daemon
|
||||
// starts unlocked from the env key (pre-unlock behavior). Enrolling a passkey
|
||||
// while unlocked calls MethodStoreEncryptionKey to wrap the env key and
|
||||
// persist the wrapped blob — enabling cold-start unlock on the next boot
|
||||
// after the env key is removed.
|
||||
// When a passkey credential is enrolled AND no env key is set, the daemon
|
||||
// starts in LOCKED mode: the IPC server runs but rejects all store methods
|
||||
// except MethodAssertStepUp and MethodUnlock. A passkey assertion followed
|
||||
// by MethodUnlock (with the same credential's public key) unwraps the at-rest
|
||||
// AES-256 key from a wrapped blob on disk (HKDF-SHA256 + AES-GCM) and opens
|
||||
// the encrypted store. After unlock, the daemon wires voice, loop, and
|
||||
// delivery and runs normally.
|
||||
//
|
||||
// Fallback: when db_key_env is set (or no wrapped file exists), the daemon
|
||||
// starts unlocked from the env key (pre-unlock behavior). Enrolling a passkey
|
||||
// while unlocked calls MethodStoreEncryptionKey to wrap the env key and
|
||||
// persist the wrapped blob — enabling cold-start unlock on the next boot
|
||||
// after the env key is removed.
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -102,30 +103,82 @@ type lockedAPI struct{}
|
||||
|
||||
var _ ipc.CoreAPI = (*lockedAPI)(nil)
|
||||
|
||||
func (l *lockedAPI) WriteFact(ctx context.Context, req ipc.WriteFactReq) (int64, error) { return 0, errLocked }
|
||||
func (l *lockedAPI) LatestFact(ctx context.Context, key string) (ipc.Fact, error) { return ipc.Fact{}, errLocked }
|
||||
func (l *lockedAPI) LatestFactBySource(ctx context.Context, key, source string) (ipc.Fact, error) { return ipc.Fact{}, errLocked }
|
||||
func (l *lockedAPI) Since(ctx context.Context, key string, now time.Time) (time.Duration, error) { return 0, errLocked }
|
||||
func (l *lockedAPI) Presence(ctx context.Context) (ipc.Presence, error) { return ipc.Presence{}, errLocked }
|
||||
func (l *lockedAPI) CreateReminder(ctx context.Context, fire time.Time, payload, cron string) (int64, error) { return 0, errLocked }
|
||||
func (l *lockedAPI) MarkReminder(ctx context.Context, id int64, status string) error { return errLocked }
|
||||
func (l *lockedAPI) ListReminders(ctx context.Context, n int) ([]ipc.Reminder, error) { return nil, errLocked }
|
||||
func (l *lockedAPI) RecordNudge(ctx context.Context, rule, channel, message string, ts time.Time) (int64, error) { return 0, errLocked }
|
||||
func (l *lockedAPI) ResolveNudge(ctx context.Context, id int64, outcome string, ts time.Time) error { return errLocked }
|
||||
func (l *lockedAPI) RecentOutcomes(ctx context.Context, rule string, n int) ([]string, error) { return nil, errLocked }
|
||||
func (l *lockedAPI) RecentFacts(ctx context.Context, n int) ([]ipc.Fact, error) { return nil, errLocked }
|
||||
func (l *lockedAPI) CalendarEvents(ctx context.Context, from, to time.Time) ([]ipc.Fact, error) { return nil, errLocked }
|
||||
func (l *lockedAPI) RecentNudges(ctx context.Context, n int) ([]ipc.Nudge, error) { return nil, errLocked }
|
||||
func (l *lockedAPI) WriteNote(ctx context.Context, ts time.Time, text string, embedding []float32, source string) (int64, error) { return 0, errLocked }
|
||||
func (l *lockedAPI) QueryNotes(ctx context.Context, embedding []float32, k int) ([]ipc.Note, error) { return nil, errLocked }
|
||||
func (l *lockedAPI) RecentNotes(ctx context.Context, n int) ([]ipc.Note, error) { return nil, errLocked }
|
||||
func (l *lockedAPI) ProposeTool(ctx context.Context, name, utterance, scope string, ts time.Time) (bool, error) { return false, errLocked }
|
||||
func (l *lockedAPI) EnableTool(ctx context.Context, name string, cmd []string, destructive bool, scope string, ts time.Time) error { return errLocked }
|
||||
func (l *lockedAPI) DisableTool(ctx context.Context, name string) error { return errLocked }
|
||||
func (l *lockedAPI) LookupTool(ctx context.Context, name string) (ipc.Tool, error) { return ipc.Tool{}, errLocked }
|
||||
func (l *lockedAPI) ListTools(ctx context.Context, status string) ([]ipc.Tool, error) { return nil, errLocked }
|
||||
func (l *lockedAPI) RevertFact(ctx context.Context, key string) (int64, error) { return 0, errLocked }
|
||||
func (l *lockedAPI) TickTrace(ctx context.Context) (ipc.TickTrace, error) { return ipc.TickTrace{}, errLocked }
|
||||
func (l *lockedAPI) WriteFact(ctx context.Context, req ipc.WriteFactReq) (int64, error) {
|
||||
return 0, errLocked
|
||||
}
|
||||
func (l *lockedAPI) LatestFact(ctx context.Context, key string) (ipc.Fact, error) {
|
||||
return ipc.Fact{}, errLocked
|
||||
}
|
||||
func (l *lockedAPI) LatestFactBySource(ctx context.Context, key, source string) (ipc.Fact, error) {
|
||||
return ipc.Fact{}, errLocked
|
||||
}
|
||||
func (l *lockedAPI) Since(ctx context.Context, key string, now time.Time) (time.Duration, error) {
|
||||
return 0, errLocked
|
||||
}
|
||||
func (l *lockedAPI) Presence(ctx context.Context) (ipc.Presence, error) {
|
||||
return ipc.Presence{}, errLocked
|
||||
}
|
||||
func (l *lockedAPI) CreateReminder(ctx context.Context, fire time.Time, payload, cron string) (int64, error) {
|
||||
return 0, errLocked
|
||||
}
|
||||
func (l *lockedAPI) MarkReminder(ctx context.Context, id int64, status string) error {
|
||||
return errLocked
|
||||
}
|
||||
func (l *lockedAPI) ListReminders(ctx context.Context, n int) ([]ipc.Reminder, error) {
|
||||
return nil, errLocked
|
||||
}
|
||||
func (l *lockedAPI) RecordNudge(ctx context.Context, rule, channel, message string, ts time.Time) (int64, error) {
|
||||
return 0, errLocked
|
||||
}
|
||||
func (l *lockedAPI) ResolveNudge(ctx context.Context, id int64, outcome string, ts time.Time) error {
|
||||
return errLocked
|
||||
}
|
||||
func (l *lockedAPI) RecentOutcomes(ctx context.Context, rule string, n int) ([]string, error) {
|
||||
return nil, errLocked
|
||||
}
|
||||
func (l *lockedAPI) RecentFacts(ctx context.Context, n int) ([]ipc.Fact, error) {
|
||||
return nil, errLocked
|
||||
}
|
||||
func (l *lockedAPI) CalendarEvents(ctx context.Context, from, to time.Time) ([]ipc.Fact, error) {
|
||||
return nil, errLocked
|
||||
}
|
||||
func (l *lockedAPI) RecentNudges(ctx context.Context, n int) ([]ipc.Nudge, error) {
|
||||
return nil, errLocked
|
||||
}
|
||||
func (l *lockedAPI) WriteNote(ctx context.Context, ts time.Time, text string, embedding []float32, source string) (int64, error) {
|
||||
return 0, errLocked
|
||||
}
|
||||
func (l *lockedAPI) QueryNotes(ctx context.Context, embedding []float32, k int) ([]ipc.Note, error) {
|
||||
return nil, errLocked
|
||||
}
|
||||
func (l *lockedAPI) RecentNotes(ctx context.Context, n int) ([]ipc.Note, error) {
|
||||
return nil, errLocked
|
||||
}
|
||||
func (l *lockedAPI) ProposeTool(ctx context.Context, name, utterance, scope string, ts time.Time) (bool, error) {
|
||||
return false, errLocked
|
||||
}
|
||||
func (l *lockedAPI) EnableTool(ctx context.Context, name string, cmd []string, destructive bool, scope string, ts time.Time) error {
|
||||
return errLocked
|
||||
}
|
||||
func (l *lockedAPI) DisableTool(ctx context.Context, name string) error { return errLocked }
|
||||
func (l *lockedAPI) DeleteTool(ctx context.Context, name string) error { return errLocked }
|
||||
func (l *lockedAPI) ListProposedRoutines(ctx context.Context) ([]ipc.ProposedRoutine, error) {
|
||||
return nil, errLocked
|
||||
}
|
||||
func (l *lockedAPI) DismissProposedRoutine(ctx context.Context, id int64) error { return errLocked }
|
||||
func (l *lockedAPI) LookupTool(ctx context.Context, name string) (ipc.Tool, error) {
|
||||
return ipc.Tool{}, errLocked
|
||||
}
|
||||
func (l *lockedAPI) ListTools(ctx context.Context, status string) ([]ipc.Tool, error) {
|
||||
return nil, errLocked
|
||||
}
|
||||
func (l *lockedAPI) RevertFact(ctx context.Context, key string) (int64, error) { return 0, errLocked }
|
||||
func (l *lockedAPI) Chat(ctx context.Context, text string) (string, error) {
|
||||
return "", errLocked
|
||||
}
|
||||
func (l *lockedAPI) TickTrace(ctx context.Context) (ipc.TickTrace, error) {
|
||||
return ipc.TickTrace{}, errLocked
|
||||
}
|
||||
|
||||
func run(args []string) error {
|
||||
cfgPath := flag.String("config", defaultConfigPath(), "path to mavend JSON config")
|
||||
@@ -236,7 +289,7 @@ func run(args []string) error {
|
||||
}
|
||||
|
||||
// voice
|
||||
voiceW, err = wireVoice(cfg, ipc.NewStoreAPI(st), phr, st.VectorMemory())
|
||||
voiceW, err = wireVoice(cfg, ipc.NewStoreAPI(st), phr, st.VectorMemory(), st)
|
||||
if err != nil {
|
||||
return fmt.Errorf("wire voice: %w", err)
|
||||
}
|
||||
@@ -266,6 +319,7 @@ func run(args []string) error {
|
||||
Ntfy: ntfy,
|
||||
Telegram: telegram,
|
||||
Voice: voiceSink,
|
||||
Ack: st,
|
||||
Nudges: st,
|
||||
Reminders: st,
|
||||
})
|
||||
@@ -280,6 +334,10 @@ func run(args []string) error {
|
||||
CoreAPI: ipc.NewStoreAPI(st),
|
||||
getTrace: tl.trace,
|
||||
}
|
||||
if voiceW != nil && voiceW.handler != nil {
|
||||
api := coreAPI.(*daemonAPI)
|
||||
api.chatFn = voiceW.handler.handleText
|
||||
}
|
||||
} else {
|
||||
// locked mode: dummy CoreAPI that returns errLocked for everything
|
||||
coreAPI = &lockedAPI{}
|
||||
@@ -386,7 +444,7 @@ func run(args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
voiceW, err = wireVoice(cfg, ipc.NewStoreAPI(st), phr, st.VectorMemory())
|
||||
voiceW, err = wireVoice(cfg, ipc.NewStoreAPI(st), phr, st.VectorMemory(), st)
|
||||
if err != nil {
|
||||
return fmt.Errorf("wire voice: %w", err)
|
||||
}
|
||||
@@ -415,6 +473,7 @@ func run(args []string) error {
|
||||
Ntfy: ntfy,
|
||||
Telegram: telegram,
|
||||
Voice: voiceSink,
|
||||
Ack: st,
|
||||
Nudges: st,
|
||||
Reminders: st,
|
||||
})
|
||||
@@ -429,6 +488,9 @@ func run(args []string) error {
|
||||
CoreAPI: ipc.NewStoreAPI(st),
|
||||
getTrace: tl.trace,
|
||||
}
|
||||
if voiceW != nil && voiceW.handler != nil {
|
||||
newAPI.chatFn = voiceW.handler.handleText
|
||||
}
|
||||
srv.SetAPI(newAPI)
|
||||
srv.Check = (&auth.Gate{Enrollment: auth.NewFloorEnrollment(), Session: passkeySess}).Check
|
||||
|
||||
|
||||
Reference in New Issue
Block a user