From fab9603547a8a34f4316c0e7cd5f72dae924d84c Mon Sep 17 00:00:00 2001 From: RussellLuo Date: Fri, 20 Mar 2026 23:24:46 +0800 Subject: [PATCH 1/3] feat(matrix): support encrypted messages with E2EE - Add `crypto_database_path` and `crypto_passphrase` configuration - Integrate cryptohelper for decrypting `m.room.encrypted` events - Handle both plaintext and encrypted messages in `handleMessageEvent` - Enable `goolm` build tag for libsignal crypto support Fixes #1840. --- Makefile | 2 +- go.mod | 3 +- pkg/channels/matrix/init.go | 9 +- pkg/channels/matrix/matrix.go | 150 +++++++++++++++++++++++++++++++++- pkg/config/config.go | 2 + 5 files changed, 159 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 411cd9dc5..41aa9f51c 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ LDFLAGS=-X $(CONFIG_PKG).Version=$(VERSION) -X $(CONFIG_PKG).GitCommit=$(GIT_COM # Go variables GO?=CGO_ENABLED=0 go WEB_GO?=$(GO) -GOFLAGS?=-v -tags stdjson +GOFLAGS?=-v -tags goolm,stdjson # Patch MIPS LE ELF e_flags (offset 36) for NaN2008-only kernels (e.g. Ingenic X2600). # diff --git a/go.mod b/go.mod index bce41d0d3..e9ef37e98 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/gorilla/websocket v1.5.3 github.com/h2non/filetype v1.1.3 github.com/larksuite/oapi-sdk-go/v3 v3.5.3 + github.com/mattn/go-sqlite3 v1.14.34 github.com/mdp/qrterminal/v3 v3.2.1 github.com/modelcontextprotocol/go-sdk v1.4.1 github.com/mymmrac/telego v1.7.0 @@ -31,6 +32,7 @@ require ( github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 github.com/tencent-connect/botgo v0.2.1 + go.mau.fi/util v0.9.7 go.mau.fi/whatsmeow v0.0.0-20260219150138-7ae702b1eed4 golang.org/x/oauth2 v0.36.0 golang.org/x/term v0.41.0 @@ -77,7 +79,6 @@ require ( github.com/spf13/pflag v1.0.10 // indirect github.com/vektah/gqlparser/v2 v2.5.27 // indirect go.mau.fi/libsignal v0.2.1 // indirect - go.mau.fi/util v0.9.7 // indirect golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect golang.org/x/text v0.35.0 // indirect modernc.org/libc v1.67.6 // indirect diff --git a/pkg/channels/matrix/init.go b/pkg/channels/matrix/init.go index 6677f855e..4d6ad45a7 100644 --- a/pkg/channels/matrix/init.go +++ b/pkg/channels/matrix/init.go @@ -1,6 +1,8 @@ package matrix import ( + "path/filepath" + "github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/channels" "github.com/sipeed/picoclaw/pkg/config" @@ -8,6 +10,11 @@ import ( func init() { channels.RegisterFactory("matrix", func(cfg *config.Config, b *bus.MessageBus) (channels.Channel, error) { - return NewMatrixChannel(cfg.Channels.Matrix, b) + matrixCfg := cfg.Channels.Matrix + cryptoDatabasePath := matrixCfg.CryptoDatabasePath + if cryptoDatabasePath == "" { + cryptoDatabasePath = filepath.Join(cfg.WorkspacePath(), "matrix") + } + return NewMatrixChannel(matrixCfg, b, cryptoDatabasePath) }) } diff --git a/pkg/channels/matrix/matrix.go b/pkg/channels/matrix/matrix.go index 98c607d0b..07f9c80fa 100644 --- a/pkg/channels/matrix/matrix.go +++ b/pkg/channels/matrix/matrix.go @@ -2,6 +2,7 @@ package matrix import ( "context" + "database/sql" "fmt" "html" "io" @@ -17,9 +18,12 @@ import ( "github.com/gomarkdown/markdown" mdhtml "github.com/gomarkdown/markdown/html" "github.com/gomarkdown/markdown/parser" + "go.mau.fi/util/dbutil" "maunium.net/go/mautrix" + "maunium.net/go/mautrix/crypto/cryptohelper" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" + _ "modernc.org/sqlite" "github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/channels" @@ -30,6 +34,9 @@ import ( ) const ( + sqliteDriver = "sqlite" + dbName = "store.db" + typingRefreshInterval = 20 * time.Second typingServerTTL = 30 * time.Second roomKindCacheTTL = 5 * time.Minute @@ -181,9 +188,12 @@ type MatrixChannel struct { roomKindCache *roomKindCache localpartMentionR *regexp.Regexp + + cryptoHelper *cryptohelper.CryptoHelper + cryptoDbPath string } -func NewMatrixChannel(cfg config.MatrixConfig, messageBus *bus.MessageBus) (*MatrixChannel, error) { +func NewMatrixChannel(cfg config.MatrixConfig, messageBus *bus.MessageBus, cryptoDatabasePath string) (*MatrixChannel, error) { homeserver := strings.TrimSpace(cfg.Homeserver) userID := strings.TrimSpace(cfg.UserID) accessToken := strings.TrimSpace(cfg.AccessToken()) @@ -230,6 +240,7 @@ func NewMatrixChannel(cfg config.MatrixConfig, messageBus *bus.MessageBus) (*Mat roomKindCache: newRoomKindCache(roomKindCacheMaxEntries, roomKindCacheTTL), localpartMentionR: localpartMentionRegexp(matrixLocalpart(client.UserID)), typingMu: sync.Mutex{}, + cryptoDbPath: cryptoDatabasePath, }, nil } @@ -239,7 +250,17 @@ func (c *MatrixChannel) Start(ctx context.Context) error { c.ctx, c.cancel = context.WithCancel(ctx) c.startTime = time.Now() + // Initialize crypto helper if database and passphrase are configured + if c.cryptoDbPath != "" && c.config.CryptoPassphrase != "" { + if err := c.initCrypto(ctx); err != nil { + logger.WarnCF("matrix", "Failed to initialize crypto, continuing without encryption support", map[string]any{ + "error": err.Error(), + }) + } + } + c.syncer.OnEventType(event.EventMessage, c.handleMessageEvent) + c.syncer.OnEventType(event.EventEncrypted, c.handleMessageEvent) c.syncer.OnEventType(event.StateMember, c.handleMemberEvent) c.SetRunning(true) @@ -266,10 +287,84 @@ func (c *MatrixChannel) Stop(ctx context.Context) error { } c.stopTypingSessions(ctx) + // Close crypto helper if initialized + if c.cryptoHelper != nil { + c.cryptoHelper.Close() + c.cryptoHelper = nil + c.client.Crypto = nil + } + logger.InfoC("matrix", "Matrix channel stopped") return nil } +func (c *MatrixChannel) initCrypto(ctx context.Context) error { + logger.InfoC("matrix", "Initializing crypto helper") + + // Ensure the crypto database directory exists + if err := os.MkdirAll(c.cryptoDbPath, 0o700); err != nil { + return fmt.Errorf("create crypto database directory: %w", err) + } + + // Create database with sqlite driver (modernc.org/sqlite) + dbPath := filepath.Join(c.cryptoDbPath, dbName) + connStr := "file:" + dbPath + "?_foreign_keys=on" + + db, err := sql.Open(sqliteDriver, connStr) + if err != nil { + return fmt.Errorf("open crypto database: %w", err) + } + db.SetMaxOpenConns(1) + db.SetMaxIdleConns(1) + + // Execute PRAGMA statements + // This is equivalent to the "sqlite3-fk-wal" dialect used by cryptohelper + pragmaStmts := []string{ + "PRAGMA foreign_keys = ON", + "PRAGMA journal_mode = WAL", + "PRAGMA synchronous = NORMAL", + "PRAGMA busy_timeout = 5000", + } + for _, pragma := range pragmaStmts { + if _, err = db.ExecContext(ctx, pragma); err != nil { + _ = db.Close() + return fmt.Errorf("execute %s: %w", pragma, err) + } + } + + // Wrap with dbutil for dialect support + wrappedDB, err := dbutil.NewWithDB(db, sqliteDriver) + if err != nil { + _ = db.Close() + return fmt.Errorf("wrap database: %w", err) + } + + cryptoHelper, err := cryptohelper.NewCryptoHelper(c.client, []byte(c.config.CryptoPassphrase), wrappedDB) + if err != nil { + return fmt.Errorf("create crypto helper: %w", err) + } + + if c.client.DeviceID == "" { + resp, err := c.client.Whoami(ctx) + if err != nil { + _ = db.Close() + return fmt.Errorf("get device ID via whoami: %w", err) + } + c.client.DeviceID = resp.DeviceID + } + + if err = cryptoHelper.Init(ctx); err != nil { + cryptoHelper.Close() + return fmt.Errorf("init crypto helper: %w", err) + } + + c.client.Crypto = cryptoHelper + c.cryptoHelper = cryptoHelper + + logger.InfoC("matrix", "Crypto helper initialized successfully") + return nil +} + func markdownToHTML(md string) string { p := parser.NewWithExtensions(parser.CommonExtensions | parser.AutoHeadingIDs) renderer := mdhtml.NewRenderer(mdhtml.RendererOptions{Flags: mdhtml.CommonFlags}) @@ -548,9 +643,26 @@ func (c *MatrixChannel) handleMessageEvent(ctx context.Context, evt *event.Event return } - msgEvt := evt.Content.AsMessage() - if msgEvt == nil { - return + var msgEvt *event.MessageEventContent + switch evt.Type { + case event.EventMessage: + // When crypto is enabled, events marked WasEncrypted=true are + // re-dispatched by c.cryptoHelper after decryption and will be + // processed again in the EventEncrypted branch. Skip to avoid duplication. + if c.client.Crypto != nil && evt.Mautrix.WasEncrypted { + return + } + + msgEvt = evt.Content.AsMessage() + if msgEvt == nil || msgEvt.MsgType == "" { + return + } + case event.EventEncrypted: + var ok bool + msgEvt, ok = c.decryptEvent(ctx, evt) + if !ok { + return + } } // Ignore edits. @@ -642,6 +754,36 @@ func (c *MatrixChannel) handleMessageEvent(ctx context.Context, evt *event.Event ) } +// decryptEvent decrypts an encrypted event and returns the decrypted message event content. +// It returns the decrypted content and a boolean indicating whether decryption was successful. +func (c *MatrixChannel) decryptEvent(ctx context.Context, evt *event.Event) (*event.MessageEventContent, bool) { + if c.client.Crypto == nil { + logger.DebugCF("matrix", "Received encrypted message but crypto is not enabled", map[string]any{ + "room_id": evt.RoomID.String(), + }) + return nil, false + } + + decrypted, err := c.client.Crypto.Decrypt(ctx, evt) + if err != nil { + logger.WarnCF("matrix", "Failed to decrypt message", map[string]any{ + "room_id": evt.RoomID.String(), + "error": err.Error(), + }) + return nil, false + } + + if decrypted.Type != event.EventMessage { + logger.DebugCF("matrix", "Decrypted event is not a message event", map[string]any{ + "room_id": evt.RoomID.String(), + "type": decrypted.Type.String(), + }) + return nil, false + } + + return decrypted.Content.AsMessage(), true +} + func (c *MatrixChannel) extractInboundContent( ctx context.Context, msgEvt *event.MessageEventContent, diff --git a/pkg/config/config.go b/pkg/config/config.go index 84e1ab61a..52fe2400a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -603,6 +603,8 @@ type MatrixConfig struct { Placeholder PlaceholderConfig `json:"placeholder,omitempty"` ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_MATRIX_REASONING_CHANNEL_ID"` secDirty bool + CryptoDatabasePath string `json:"crypto_database_path,omitempty" env:"PICOCLAW_CHANNELS_MATRIX_CRYPTO_DATABASE_PATH"` + CryptoPassphrase string `json:"crypto_passphrase,omitempty" env:"PICOCLAW_CHANNELS_MATRIX_CRYPTO_PASSPHRASE"` } // AccessToken returns the Matrix access token From 7f163658c9496c4c8f343c6f0be81709935dedc5 Mon Sep 17 00:00:00 2001 From: RussellLuo Date: Sat, 21 Mar 2026 00:00:37 +0800 Subject: [PATCH 2/3] docs(matrix): Update docs --- config/config.example.json | 4 +++- docs/channels/matrix/README.md | 7 ++++++- docs/channels/matrix/README.zh.md | 8 +++++++- pkg/config/defaults.go | 2 ++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/config/config.example.json b/config/config.example.json index 88578701a..a5dd0bed9 100644 --- a/config/config.example.json +++ b/config/config.example.json @@ -162,7 +162,9 @@ "enabled": true, "text": "Thinking... 💭" }, - "reasoning_channel_id": "" + "reasoning_channel_id": "", + "crypto_database_path": "", + "crypto_passphrase": "YOUR_MATRIX_CRYPTO_PICKLE_KEY" }, "line": { "enabled": false, diff --git a/docs/channels/matrix/README.md b/docs/channels/matrix/README.md index 2ed19245a..dd4b45eba 100644 --- a/docs/channels/matrix/README.md +++ b/docs/channels/matrix/README.md @@ -25,7 +25,9 @@ Add this to `config.json`: "text": "Thinking..." }, "reasoning_channel_id": "", - "message_format": "richtext" + "message_format": "richtext", + "crypto_database_path": "", + "crypto_passphrase": "YOUR_MATRIX_CRYPTO_PICKLE_KEY" } } } @@ -46,6 +48,8 @@ Add this to `config.json`: | placeholder | object | No | Placeholder message config | | reasoning_channel_id | string | No | Target channel for reasoning output | | message_format | string | No | Output format: `"richtext"` (default) renders markdown as HTML; `"plain"` sends plain text only | +| crypto_database_path | string | No | Path to store the crypto database (uses workspace path `~/.picoclaw/workspace` if empty) | +| crypto_passphrase | string | No | Serialization key for encrypting session keys in the database; must remain unchanged once set | ## 3. Currently Supported @@ -58,6 +62,7 @@ Add this to `config.json`: - Typing state (`m.typing`) - Placeholder message + final reply replacement - Auto-join invited rooms (can be disabled) +- End-to-end encryption (E2EE) support for encrypted messages ## 4. TODO diff --git a/docs/channels/matrix/README.zh.md b/docs/channels/matrix/README.zh.md index 8db3e4383..cd68a057e 100644 --- a/docs/channels/matrix/README.zh.md +++ b/docs/channels/matrix/README.zh.md @@ -24,7 +24,10 @@ "enabled": true, "text": "Thinking... 💭" }, - "reasoning_channel_id": "" + "reasoning_channel_id": "", + "message_format": "richtext", + "crypto_database_path": "", + "crypto_passphrase": "YOUR_MATRIX_CRYPTO_PICKLE_KEY" } } } @@ -45,6 +48,8 @@ | placeholder | object | 否 | 占位消息配置 | | reasoning_channel_id | string | 否 | 思维链输出目标通道 | | message_format | string | 否 | 消息格式:`richtext`(富文本)或 `plain`(纯文本) | +| crypto_database_path | string | 否 | 加密数据库存储路径(为空时使用工作空间路径 `~/.picoclaw/workspace`) | +| crypto_passphrase | string | 否 | 加密数据库中 session key 的序列化密钥;设置后不能更改 | ## 3. 当前支持 @@ -56,6 +61,7 @@ - Typing 状态(`m.typing`) - 占位消息(`Thinking... 💭`)+ 最终回复替换 - 自动加入邀请房间(可关闭) +- 端对端加密(E2EE)消息支持 ## 4. TODO diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index c1d0ea0f6..0619f9e06 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -113,6 +113,8 @@ func DefaultConfig() *Config { Enabled: true, Text: "Thinking... 💭", }, + CryptoDatabasePath: "", + CryptoPassphrase: "", }, LINE: LINEConfig{ Enabled: false, From 74a9dcaa5c8a5854e37c8c0e664380145db410e3 Mon Sep 17 00:00:00 2001 From: RussellLuo Date: Mon, 23 Mar 2026 09:29:17 +0800 Subject: [PATCH 3/3] fix(ci): Make CI happy --- .github/workflows/pr.yml | 5 ++++- .goreleaser.yaml | 3 +++ Makefile | 13 +++++++------ pkg/channels/matrix/matrix.go | 22 +++++++++++++++------- pkg/config/config.go | 18 +++++++++--------- 5 files changed, 38 insertions(+), 23 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 902d4d4eb..2d544d4f0 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -23,10 +23,13 @@ jobs: uses: golangci/golangci-lint-action@v9 with: version: v2.10.1 + args: --build-tags=goolm,stdjson vuln_check: name: Security Check runs-on: ubuntu-latest + env: + GOFLAGS: -tags=goolm,stdjson steps: - name: Checkout uses: actions/checkout@v6 @@ -59,4 +62,4 @@ jobs: run: go generate ./... - name: Run go test - run: go test ./... + run: go test -tags goolm,stdjson ./... diff --git a/.goreleaser.yaml b/.goreleaser.yaml index a73f87f30..ea93d0377 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -15,6 +15,7 @@ builds: env: - CGO_ENABLED=0 tags: + - goolm - stdjson ldflags: - -s -w @@ -57,6 +58,7 @@ builds: env: - CGO_ENABLED=0 tags: + - goolm - stdjson ldflags: - -s -w @@ -95,6 +97,7 @@ builds: env: - CGO_ENABLED=0 tags: + - goolm - stdjson ldflags: - -s -w diff --git a/Makefile b/Makefile index 41aa9f51c..2a82e587b 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,8 @@ LDFLAGS=-X $(CONFIG_PKG).Version=$(VERSION) -X $(CONFIG_PKG).GitCommit=$(GIT_COM # Go variables GO?=CGO_ENABLED=0 go WEB_GO?=$(GO) -GOFLAGS?=-v -tags goolm,stdjson +GO_BUILD_TAGS?=goolm,stdjson +GOFLAGS?=-v -tags $(GO_BUILD_TAGS) # Patch MIPS LE ELF e_flags (offset 36) for NaN2008-only kernels (e.g. Ingenic X2600). # @@ -221,13 +222,13 @@ clean: ## vet: Run go vet for static analysis vet: generate - @packages="$$(go list ./...)" && \ - $(GO) vet $$(printf '%s\n' "$$packages" | grep -v '^github.com/sipeed/picoclaw/web/') + @packages="$$($(GO) list $(GOFLAGS) ./...)" && \ + $(GO) vet $(GOFLAGS) $$(printf '%s\n' "$$packages" | grep -v '^github.com/sipeed/picoclaw/web/') @cd web/backend && $(WEB_GO) vet ./... ## test: Test Go code test: generate - @$(GO) test $$(go list ./... | grep -v github.com/sipeed/picoclaw/web/) + @$(GO) test $(GOFLAGS) $$($(GO) list $(GOFLAGS) ./... | grep -v github.com/sipeed/picoclaw/web/) @cd web && make test ## fmt: Format Go code @@ -236,11 +237,11 @@ fmt: ## lint: Run linters lint: - @$(GOLANGCI_LINT) run + @CGO_ENABLED=0 $(GOLANGCI_LINT) run --build-tags $(GO_BUILD_TAGS) ## fix: Fix linting issues fix: - @$(GOLANGCI_LINT) run --fix + @CGO_ENABLED=0 $(GOLANGCI_LINT) run --fix --build-tags $(GO_BUILD_TAGS) ## deps: Download dependencies deps: diff --git a/pkg/channels/matrix/matrix.go b/pkg/channels/matrix/matrix.go index 07f9c80fa..50b86158d 100644 --- a/pkg/channels/matrix/matrix.go +++ b/pkg/channels/matrix/matrix.go @@ -193,7 +193,11 @@ type MatrixChannel struct { cryptoDbPath string } -func NewMatrixChannel(cfg config.MatrixConfig, messageBus *bus.MessageBus, cryptoDatabasePath string) (*MatrixChannel, error) { +func NewMatrixChannel( + cfg config.MatrixConfig, + messageBus *bus.MessageBus, + cryptoDatabasePath string, +) (*MatrixChannel, error) { homeserver := strings.TrimSpace(cfg.Homeserver) userID := strings.TrimSpace(cfg.UserID) accessToken := strings.TrimSpace(cfg.AccessToken()) @@ -253,9 +257,13 @@ func (c *MatrixChannel) Start(ctx context.Context) error { // Initialize crypto helper if database and passphrase are configured if c.cryptoDbPath != "" && c.config.CryptoPassphrase != "" { if err := c.initCrypto(ctx); err != nil { - logger.WarnCF("matrix", "Failed to initialize crypto, continuing without encryption support", map[string]any{ - "error": err.Error(), - }) + logger.WarnCF( + "matrix", + "Failed to initialize crypto, continuing without encryption support", + map[string]any{ + "error": err.Error(), + }, + ) } } @@ -345,10 +353,10 @@ func (c *MatrixChannel) initCrypto(ctx context.Context) error { } if c.client.DeviceID == "" { - resp, err := c.client.Whoami(ctx) - if err != nil { + resp, whoamiErr := c.client.Whoami(ctx) + if whoamiErr != nil { _ = db.Close() - return fmt.Errorf("get device ID via whoami: %w", err) + return fmt.Errorf("get device ID via whoami: %w", whoamiErr) } c.client.DeviceID = resp.DeviceID } diff --git a/pkg/config/config.go b/pkg/config/config.go index 52fe2400a..b46432c2c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -591,20 +591,20 @@ func (c *SlackConfig) SetAppToken(token string) { } type MatrixConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_MATRIX_ENABLED"` - Homeserver string `json:"homeserver" env:"PICOCLAW_CHANNELS_MATRIX_HOMESERVER"` - UserID string `json:"user_id" env:"PICOCLAW_CHANNELS_MATRIX_USER_ID"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_MATRIX_ENABLED"` + Homeserver string `json:"homeserver" env:"PICOCLAW_CHANNELS_MATRIX_HOMESERVER"` + UserID string `json:"user_id" env:"PICOCLAW_CHANNELS_MATRIX_USER_ID"` accessToken string - DeviceID string `json:"device_id,omitempty" env:"PICOCLAW_CHANNELS_MATRIX_DEVICE_ID"` - JoinOnInvite bool `json:"join_on_invite" env:"PICOCLAW_CHANNELS_MATRIX_JOIN_ON_INVITE"` - MessageFormat string `json:"message_format,omitempty" env:"PICOCLAW_CHANNELS_MATRIX_MESSAGE_FORMAT"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_MATRIX_ALLOW_FROM"` + DeviceID string `json:"device_id,omitempty" env:"PICOCLAW_CHANNELS_MATRIX_DEVICE_ID"` + JoinOnInvite bool `json:"join_on_invite" env:"PICOCLAW_CHANNELS_MATRIX_JOIN_ON_INVITE"` + MessageFormat string `json:"message_format,omitempty" env:"PICOCLAW_CHANNELS_MATRIX_MESSAGE_FORMAT"` + AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_MATRIX_ALLOW_FROM"` GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` Placeholder PlaceholderConfig `json:"placeholder,omitempty"` - ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_MATRIX_REASONING_CHANNEL_ID"` + ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_MATRIX_REASONING_CHANNEL_ID"` secDirty bool CryptoDatabasePath string `json:"crypto_database_path,omitempty" env:"PICOCLAW_CHANNELS_MATRIX_CRYPTO_DATABASE_PATH"` - CryptoPassphrase string `json:"crypto_passphrase,omitempty" env:"PICOCLAW_CHANNELS_MATRIX_CRYPTO_PASSPHRASE"` + CryptoPassphrase string `json:"crypto_passphrase,omitempty" env:"PICOCLAW_CHANNELS_MATRIX_CRYPTO_PASSPHRASE"` } // AccessToken returns the Matrix access token