package gateway import ( "context" "net/http" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/auth" capreg "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/session" "git.yeij.top/AskaEth/Cyrene_v2/internal/config" "git.yeij.top/AskaEth/Cyrene_v2/internal/iot" "git.yeij.top/AskaEth/Cyrene_v2/internal/persona/loader" personareg "git.yeij.top/AskaEth/Cyrene_v2/internal/persona/registry" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/knowledge" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/orchestration" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/task" "git.yeij.top/AskaEth/Cyrene_v2/internal/services/automations" "git.yeij.top/AskaEth/Cyrene_v2/internal/services/backthink" ) // This file declares the dependency contracts the gateway consumes. Every one is // a narrow interface satisfied by a concrete backend the composition root injects // (see Deps in deps.go), so the gateway never reaches for a concrete // implementation and tests can substitute a stub. // Authenticator abstracts the credential + RBAC service. *auth.JWTService is the // production implementation; it is an interface so tests substitute a mock. type Authenticator interface { // Sign issues a token for sub with the given role and lifetime. Sign(sub string, role auth.Role, ttl time.Duration) (string, error) // Verify validates a token and returns its claims. Verify(token string) (*auth.Claims, error) // Authorize returns an HTTP middleware enforcing at least one required role. Authorize(required ...auth.Role) func(http.Handler) http.Handler } // CapabilityStore abstracts the global capability registry used to enumerate // plugins and channels in the admin API. *capability.Registry is the production // implementation. type CapabilityStore interface { // List returns every capability whose Kind matches, or all when kind is // empty. List(kind capreg.Kind) []capreg.Capability // SetPolicy replaces the reachability markers for the named capability. SetPolicy(name string, p capreg.ToolPolicy) error } // Chatter runs a single agent turn. *loop.Loop is the production implementation. type Chatter interface { // Run drives one turn for sessionID and returns the final answer. Run(ctx context.Context, sessionID string, messages []model.Message) (*loop.Result, error) } // SessionStore lists known channel sessions. *session.SessionManager is the // production implementation. type SessionStore interface { List(ctx context.Context) ([]session.Session, error) } // PersonaStore lists registered characters (each bound one-to-one to a persona). // *persona/registry.Registry is the production implementation. The gateway maps // each Character to its wire summary; the interface deliberately returns the // domain type so the wire shape stays out of the store contract. type PersonaStore interface { List() []personareg.Character // Persona returns the persona with the given id (to read its visual assets). // The production registry implements it; the wire summary reads Avatar / // ChatBackground from it so each role's visuals stay role-scoped. Persona(id string) (*loader.Persona, bool) } // MemoryStore lists memory entries. *runtime/memory.Store is the production // implementation. The gateway maps each MemoryItem to its wire summary. type MemoryStore interface { List(ctx context.Context) ([]memory.MemoryItem, error) } // HistoryStore is the persistent chat-history store (design/16). *runtime/history.Store // is the production implementation. When nil the session/message endpoints degrade: list // sessions falls back to the channel-session view, and the create/messages/delete // endpoints answer with a clear 501. type HistoryStore interface { Append(ctx context.Context, msg history.Message) error // AppendFull persists msg like Append but returns the complete stored message // (with the assigned id/timestamp) so the orchestrator can broadcast it for // cross-end realtime sync (design/16 §4.2). AppendFull(ctx context.Context, msg history.Message) (history.Message, error) List(ctx context.Context, sessionID string, before int64, limit int, platform string) ([]history.Message, error) // ListAfter returns up to limit messages strictly newer than after in // oldest-first order, for forward incremental catch-up after a reconnect // (the "before" cursor can only walk backwards). platform filters as in List. ListAfter(ctx context.Context, sessionID string, after int64, limit int, platform string) ([]history.Message, error) Recent(ctx context.Context, sessionID string, n int) ([]history.Message, error) ListPlatformLog(ctx context.Context, platform string, before int64, limit int) ([]history.Message, error) ListAdapterLog(ctx context.Context, adapterID string, before int64, limit int) ([]history.Message, error) ListAllLog(ctx context.Context, before int64, limit int) ([]history.Message, error) DeleteSession(ctx context.Context, sessionID string) error // Search returns session-scoped content matches (newest first) for the // ?q= message-search path. An empty query yields an empty slice. Search(ctx context.Context, sessionID, query string, before int64, limit int) ([]history.Message, error) // UpdateMessage edits a single message's content and returns the updated // message. An unknown id returns history.ErrMessageNotFound. UpdateMessage(ctx context.Context, id, content string) (history.Message, error) // DeleteMessage removes a single message. An unknown id returns // history.ErrMessageNotFound. DeleteMessage(ctx context.Context, id string) error // RenameSession sets a session's display title (main or sub). An unknown id // returns history.ErrSessionNotFound. RenameSession(ctx context.Context, sessionID, title string) error // SetPinned pins/unpins a session (main or sub). An unknown id returns // history.ErrSessionNotFound. SetPinned(ctx context.Context, sessionID string, pinned bool) error NewSubSession(ctx context.Context, ownerID string) (string, error) // MarkSessionRead advances a session's read cursor (D4) to before and returns // the resulting cursor as a Unix millisecond timestamp; a zero before means // "now". The cursor only moves forward. An unknown id returns // history.ErrSessionNotFound. MarkSessionRead(ctx context.Context, sessionID string, before time.Time) (int64, error) ListSessions(ctx context.Context) ([]history.SessionMeta, error) } // IoTStore lists IoT devices. *iot.Manager is the production implementation. type IoTStore interface { List(ctx context.Context) ([]iot.Device, error) } // SubagentAuditor audits a sub-agent's conversation context for the subagent_ask // WS route. *orchestration.Scheduler is the production implementation. type SubagentAuditor interface { AuditContext(ctx context.Context, id string, mode orchestration.AuditMode, n int, query string) (*orchestration.AuditResult, error) } // TaskOutputStore queries a task's output for the task_output_query WS route. // *task.Manager is the production implementation. type TaskOutputStore interface { TaskOutput(ctx context.Context, id string) (task.OutputEvent, error) } // AutomationStore enumerates the automations and reminders for the admin list. // *automations.Service is the production implementation. The gateway maps each // record to its wire shape. type AutomationStore interface { List() []automations.AutomationRecord } // UserStore enumerates the accounts and API keys visible to the admin panel. // The production implementation reads the configured admin account plus the // auth.APIKey table. The gateway maps each summary to its wire shape. type UserStore interface { List() []auth.UserSummary } // RefreshGrantStore manages opaque refresh tokens (auth.RefreshStore is the // production implementation). It is the credential behind the refreshToken // returned by login and the POST /auth/refresh, /auth/logout endpoints. Only // hashes are persisted; the plaintext token crosses this interface exactly once // per issue/rotate. Optional: when nil login omits refreshToken and the refresh // and logout endpoints report a clear not-configured error. type RefreshGrantStore interface { // Issue mints a refresh token for userID/role and returns its plaintext // (visible once) plus its expiry. Issue(ctx context.Context, userID string, role auth.Role, ttl time.Duration) (string, time.Time, error) // Rotate validates plaintext and, when opts.Rotate is set, revokes it and // returns a replacement. It returns auth.ErrInvalidGrant for an unknown, // revoked, or expired token. Rotate(ctx context.Context, plaintext string, opts auth.RotateOptions) (auth.RefreshToken, string, error) // Revoke marks the token matching plaintext revoked, returning whether a // live token was found. It is idempotent. Revoke(ctx context.Context, plaintext string) (bool, error) // RevokeAll deletes every refresh token (global invalidation on secret // rotation) and returns how many were removed. RevokeAll(ctx context.Context) (int64, error) // RevokeByUser deletes every refresh token for userID (logout by access // token, where the individual grant is unknown) and returns how many were // removed. RevokeByUser(ctx context.Context, userID string) (int64, error) } // BackthinkStore enumerates the persisted background-think records for the // M9/M10 monitor panel. *backthink.Backthink is the production implementation. type BackthinkStore interface { List(ctx context.Context, limit int) ([]backthink.Record, error) } // ChannelConfigStore is the runtime-editable channel-adapter configuration // (channels.json, design/04 §3.1). It is implemented by the composition root's // channel store: Load returns the effective per-platform accounts (file → env // fallback), Path reports the channels.json path, and Reload hot-replaces a // platform's running adapters after a panel save (Bridge.Reload). Load returns // account configs with real tokens so the owner write path can persist them; the // admin read path masks them before returning to the client. type ChannelConfigStore interface { Path() string // Load returns the effective per-platform accounts (file → env fallback), // including the secret tokens needed to persist them. Load() map[string][]config.ChannelAccount // Reload rebuilds and hot-replaces the running adapters for platform. Reload(platform string) error } // OwnerRegistrar registers a channel account id into the owner whitelist // (configs/users/owner.yaml). It is the structural identity red line: a scan-login // confirmation stamps the scanner's bound ilink_user_id into the whitelist so the // channel bridge recognizes it as the owner via actor metadata, never a prompt. // The composition root implements it (cmd/cyrene's ownerRegistrar) to persist the // whitelist and hot-update the live owner profile. Registration is best-effort: // a failure is logged by the caller but does not fail the already-applied // channels.json persist. type OwnerRegistrar interface { RegisterChannelOwner(ctx context.Context, platform, accountID string) error } // RagStore backs the admin knowledge-base RAG module (design/10 §3.12 module 8). // *runtime/knowledge.Store is the production implementation: it lists/searches // the project index, imports a document (chunk + index) and deletes one // (project entry + cascaded chapters). An implementation that cannot write // reports a clear error (knowledge.ErrWriteUnsupported) rather than a fabricated // success; the handlers map a missing document to 404. type RagStore interface { // ListDocuments enumerates the knowledge-base documents (project-index // entries). ListDocuments(ctx context.Context) ([]knowledge.Doc, error) // Search answers a knowledge-base query (line-level hierarchy + optional // semantic/vector ranking). Search(ctx context.Context, query string) ([]knowledge.Hit, error) // IndexDocument uploads/chunks/embeds a document into the project index. IndexDocument(ctx context.Context, doc knowledge.Doc) error // DeleteDocument removes a document (and its chapters) by id, returning // knowledge.ErrDocumentNotFound when no such document exists. DeleteDocument(ctx context.Context, id string) error }