7. rysh-server — The Cloud Backend
rysh-server (~27K LOC Go) is the multi-tenant control plane. A single Go binary (cmd/rysh-server/main.go) bundles a Gin HTTP/WebSocket API, an embedded NATS server (the cross-machine messaging backbone), PostgreSQL via GORM, server-side agentic panes, Stripe billing, Firebase auth, and external channel integrations. Its defining job is multi-tenant isolation.
7.1 Startup & wiring (cmd/rysh-server/main.go)
graph TD
Cfg["1. Load TOML config
(RYSH_SERVER_CONFIG)"] --> DB["2. Connect Postgres (GORM)
(schema migrated externally by golang-migrate)"]
DB --> NATS["3. Start embedded NATS server
(+ JetStream, WS listener)"]
NATS --> Svc["4. Construct services"]
Svc --> Share["5. Wire share NATS handlers
+ stale-share cleanup goroutine"]
Share --> Cond["6. Conditionally enable:
Firebase, WhatsApp, connections,
browser panes, chatbot panes"]
Cond --> Router["7. api.NewRouter → router.Run(addr)"]
Router --> Block["block on SIGTERM/SIGINT"]
Many features are optional and conditional: Firebase (if enabled), WhatsApp (if access token set), external connections and the Integration Catalog / Rysh Forge (if EncryptionKey set — both share that gate), browser panes (if Agent.APIKey set), chatbot panes (if chatbot enabled + agent key + browser-pane service). A second binary, cmd/rysh-e2e/main.go, is an end-to-end harness validating share registration, output forwarding, command routing/ack, and namespace isolation. (A small third main.go also exists at docs/cmd/geomtest/ — a standalone lipgloss/termenv TrueColor terminal-rendering test tool, not part of the server runtime.)
Configuration (internal/config/config.go)
TOML with env overrides (env always wins; Load always applies applyEnvOverrides even when the file is missing/unparseable). Sections: Server (default 0.0.0.0:8080), Database (DSN builder; default localhost:5432/rysh_server, user/password both rysh, sslmode disable), Auth (JWT secret change-me-in-production, access TTL 15m, refresh TTL 168h, bcrypt cost 12), Firebase, NATS (port 4222, WS 9222, MaxPayload 8388608 = 8MB for base64 screenshots, monitoring HTTP hardcoded to 8222), Stripe, Shares (workspace name default default, DisableSubjectACL escape hatch), Agent (browser-pane LLM; default model claude-opus-4-5, maxTokens 8192, maxIterations 20, PaneTTL 24h), WhatsApp, Chatbot (enabled true, default model claude-sonnet-4-20250514, maxTokens 4096, maxIterations 10, session timeout 30min, MaxMessageLength 10000), and a top-level EncryptionKey for connection secrets. Env keys follow RYSH_<SECTION>_<FIELD> (e.g. RYSH_AGENT_API_KEY, RYSH_NATS_WS_PORT, RYSH_ENCRYPTION_KEY).
7.2 Web framework & routing (internal/api/)
Gin (release mode). Global middleware: Recovery, RequestLogger, RateLimit (a no-op placeholder), permissive CORS (AllowOrigins: *). The static React frontend is served via NoRoute → ./web/dist/index.html.
Authentication middleware
| Middleware | Validates | Sets |
|---|---|---|
JWTAuth |
HS256 Bearer token (or ?token= for WS upgrades) |
user_id, user_email |
AdminOnly |
User.Role == "admin" (after JWTAuth) |
— |
ChatbotKeyAuth |
rcb_… widget key (X-Chatbot-Key or ?key=) |
chatbot_config_id, workspace_id |
ChatbotOriginCheck |
per-chatbot allowed origins (fail-closed) | — |
BrowserPaneAuth |
JWT or rysh_… server API key |
JWT branch: user_id, user_email; API-key branch: user_id, workspace_id (so workspace_id is set only on the API-key path) |
graph TD
Req["incoming request"] --> Group{route group}
Group -->|public| Pub["/health, /api/server-info,
/api/auth/*, /api/billing/{plans,config,webhook}"]
Group -->|JWTAuth| Prot["workspaces, members, invitations,
messages, keys, connections, chatbots,
panes, billing, shares"]
Group -->|JWTAuth+AdminOnly| Admin["/api/admin/*"]
Group -->|BrowserPaneAuth| Mixed["connection credentials,
shares list, browser-panes"]
Group -->|ChatbotKey+Origin| Widget["/api/chatbot/* (widget)"]
Group -->|API key| Res["/api/resource/* (daemon limits)"]
Group -->|proxy| WS["/nats, /workspaces/:id/nats,
/ws/proxy, /ws/shares/:wsID/:shareID"]
See §7.8 for the full endpoint catalog.
7.3 Auth model
Four credential types coexist:
graph TD
EP["email/password"] -->|bcrypt verify| JWT["issue HS256 JWT
+ rotating refresh token
(SHA-256 hash stored)"]
FB["Firebase ID token"] -->|verify via Admin SDK| Find["find/create User
(by firebase_uid then email)
+ auto-provision default workspace"]
Find --> JWT
JWT --> Access["access protected API"]
APIK["rysh_ API key
(SHA-256 hash stored)"] --> NATSauth["NATS proxy auth,
resource checks,
extension/CLI auth"]
CBK["rcb_ chatbot key"] --> WidgetAuth["widget endpoints"]
- Email/password → JWT:
issueTokensparsesAccessTokenTTL(fallback15m) andRefreshTokenTTL(fallback7×24h), signs an HS256 JWT (Issuer: "rysh-server", claimsuser_id+email), and stores the SHA-256 of a 32-byte (64-hex) refresh token.Refreshrequiresrevoked=false AND expires_at>now, revokes the old token (rotation), and re-issues.Logoutrevokes all. - Firebase social (
LoginWithIDToken): verifies the ID token, finds-or-creates the user byfirebase_uidthenemail(linking provider google/github/email), auto-provisions a default workspace for new users, then issues the same rysh JWT+refresh pair. - Server API keys — format
rysh_+ 32 alphanumerics (GenerateAPIKey); onlyKeyHash = SHA-256andKeyPrefix = key[:9]are stored.Validatechecks hash +revoked=false+ expiry and updateslast_used_at.PermissionsForRole: observer→"read", empty→"read,write", else the requested scope. - Chatbot keys —
rcb_{slug}_{32-hex}, SHA-256 hashed (KeyPrefix = rcb_{slug}_{first8hex}).
All secrets are stored hashed/encrypted: passwords (bcrypt), tokens (SHA-256), connection secrets (AES-256-GCM). Never plaintext. util.Encrypt derives the AES-256 key as sha256.Sum256(passphrase), prepends the random GCM nonce, and base64-encodes; Decrypt errors "ciphertext too short" if shorter than the nonce.
7.4 Multi-tenant NATS isolation (the security boundary)
This is the most important server concept. The embedded NATS server is flat — no native accounts or permissions; all workspaces share one subject space. Isolation is enforced entirely at the WebSocket proxy by a streaming subject-ACL parser.
sequenceDiagram
participant C as Client (CLI / extension)
participant P as TransparentWSProxy
participant ACL as subjectACL (per workspace)
participant N as Internal NATS (ws://:9222)
C->>P: WS upgrade + API key (+ optional :wsID)
P->>P: validate key → workspaceID
alt route is /workspaces/:wsID/nats and wsID ≠ key's workspace
P-->>C: 403
end
P->>P: CheckSessionLimit (share conns exempt)
P->>N: dial internal WS, pipe frames
loop client → NATS frames
C->>ACL: PUB/SUB/HPUB subject
ACL->>ACL: parse NATS protocol; skip PUB payload bytes by length
alt subject ∉ {ws.{workspaceID}., _INBOX.}
ACL-->>P: violation → close connection
else allowed
ACL->>N: forward
end
end
subjectACL (internal/nats/subject_acl.go) is a streaming NATS-protocol parser scoped to one workspace. The allowed prefixes are exactly ws.{workspaceID}. and _INBOX.:
func newSubjectACL(workspaceID string) *subjectACL {
return &subjectACL{prefixes: []string{"ws." + workspaceID + ".", "_INBOX."}}
}
The clever part is inspect — it skips PUB/HPUB payload bytes by their declared count (a.remaining), so a payload that happens to contain "SUB ..." or CRLFs can never be mis-parsed as a protocol command:
func (a *subjectACL) inspect(data []byte) (reason string) {
a.buf = append(a.buf, data...)
for {
if a.remaining > 0 { // mid-payload: skip opaque bytes
if a.remaining >= len(a.buf) { a.remaining -= len(a.buf); a.buf = a.buf[:0]; return "" }
a.buf = a.buf[a.remaining:]; a.remaining = 0
}
idx := bytes.Index(a.buf, []byte("\r\n"))
if idx < 0 { return "" } // incomplete command; wait for more bytes
line := string(a.buf[:idx]); a.buf = a.buf[idx+2:]
if r := a.checkLine(line); r != "" { return r }
}
}
checkLine parses PUB <subject> [reply] <#bytes> / HPUB <subject> [reply] <#hdr> <#total> — arming a.remaining = byteCount + 2 (payload + CRLF) and checking fields[1] — and SUB <subject> [queue] <sid>. A disallowed subject returns e.g. publish to %q denied (outside workspace namespace) and the proxy closes the connection. Tests prove: own-workspace + _INBOX allowed; foreign ws.{other}.… blocked; global wildcards (>, ws.>, *) blocked; payloads with protocol-looking bytes are not mis-parsed. The only escape hatch is [shares] disable_subject_acl.
The ACL only inspects the client→NATS direction; NATS→client is unrestricted. Share connections (
?connection_type=share) are exempt from the session-limit check.
The proxies
| Proxy | File | Role |
|---|---|---|
TransparentWSProxy |
ws_transparent_proxy.go |
primary client path; raw NATS frames + subject-ACL |
| JSON-envelope proxy | service/nats_proxy_service.go, ws_proxy.go |
{subject,data} JSON for the web dashboard; validates share-command access |
| Browser-pane proxy | browser_pane_proxy.go |
trusted; explicit allow-list of pane subjects; the Chrome extension speaks this |
| Chatbot-pane proxy | chatbot_pane_proxy.go |
translating; the untrusted widget speaks a closed {type,payload} protocol and never sees raw subjects |
TransparentWSProxy.Handle keepalive constants: proxyPingInterval = 30s, proxyPongTimeout = 90s, proxyWriteWait = 10s; it disables Nagle on both sockets for low-latency keystrokes and runs three goroutines (client→NATS with ACL, NATS→client unrestricted, a ping ticker). The browser-pane proxy allow-lists exactly these subjects under rysh.pane.{paneID}:
subscribe: .llm_prompt_execution.output, .llm_prompt_execution.status, .approval.request,
.output.shell, .output.rysh, .output.chat, .share.command.inbound, .browser.request
publish: .llm_prompt_execution.inbox, .approval.response, .browser.response
The chatbot-pane proxy maps the widget's closed protocol: inbound chatbot.visitor_message{content} → HandleVisitorMessage; outbound rysh.pane.{id}.output.chat → chatbot.message{id,role,content} (skipping role=="visitor"); …llm_prompt_execution.status phase done|error|complete|idle|cancelled → chatbot.typing_stop, any other phase → chatbot.typing.
AccountManagerandAuthCalloutexist as scaffolding for per-workspace NATS accounts but are not wired into the embedded server in this build — the subject-ACL is the real isolation.
7.5 Server-side agentic panes (internal/agentic/)
BrowserPaneService is the shared pane factory. It owns a protoactor system and a rysh-shared agentic provider, and per-pane spawns NewLLMPromptExecutionActor with a curated tool registry:
| Mode | Tools | Used by |
|---|---|---|
PaneToolsBrowser |
web_search (if Brave key) + web_fetch + page_context + browser_action + list_tools | Chrome extension panes |
PaneToolsNone |
empty (text-only) | chatbot panes (untrusted third-party sites) |
SharePane bridges a pane's agentic output to an upstream workspace share subject so terminal users can ##upstream subscribe <shareID>; inbound commands are forwarded to the extension. Browser-pane shares are in-memory only.
ChatbotPaneService (chatbot_pane_service.go) layers chatbot concerns on the factory: spawns a PaneToolsNone pane with ChatOutputToPane, auto-registers a SharedEntity (origin chatbot, mode view) so operators can observe, persists a session row, and subscribes rysh.pane.{id}.output.chat as the single persistence path. A messageBatcher flushes to AppendMessageBatch when the buffer hits 10 messages or every 500ms. Auto-greet persists the welcome message as seq 0 without publishing (the widget renders it from the HTTP response). It supports human takeover/release (suppresses the LLM while a human replies, posting a system notice), enforces daily quotas, and runs an idle-session reaper every 5 min (closes sessions idle beyond SessionTimeoutMin, default 30, reason idle_timeout).
graph TD
Ext["Chrome extension"] -->|POST /api/browser-panes| BPS["BrowserPaneService.CreatePaneWithConfig
(PaneToolsBrowser)"]
Widget["website widget"] -->|POST /api/chatbot/sessions| CPS["ChatbotPaneService.CreateSession
(PaneToolsNone)"]
BPS --> LLM1["LLMPromptExecutionActor"]
CPS --> LLM2["LLMPromptExecutionActor"]
LLM1 --> Claude["Claude"]
LLM2 --> Claude
CPS --> Persist["chatbot_sessions / chatbot_messages"]
7.6 Chatbot (embeddable widget)
ChatbotConfig holds the widget config: name, unique slug, hashed rcb_ key, system prompt, welcome message, model/tokens/iterations, theme JSON, AllowedOrigins (fail-closed origin check), rate limit, flags. Related: ChatbotSession (one pane + optional share per visitor), ChatbotMessage (append-only, seq-numbered transcript), ChatbotDailyUsage (per-workspace daily quota).
- Management (owner, JWT): CRUD chatbots, rotate key, embed snippet (
<script>pointing at/chatbot/widget.js), list sessions, operator takeover/release/reply/close, messages, usage. - Widget (chatbot-key + origin): create session, session WebSocket, history, close.
widget.jsis served statically — built by Vite from the React/TS sources inweb/widget/src/(ChatWidget.tsx,ws.ts,types.ts, …).
Caveat — the widget path is hardcoded, and two config fields are dead. The router constructs the widget handler with a hardcoded
/opt/rysh/chatbot/widget.js; the config fieldsChatbot.WidgetJSPath(default/opt/rysh/chatbot/widget.js, envRYSH_CHATBOT_WIDGET_PATH) andChatbot.CDNURLexist but are never read by the server.
7.7 Subscriptions / billing (Stripe)
SubscriptionPlan defines limits (MaxWorkspaces/Sessions/Panes/Connections/Chatbots/ChatbotSessions/ChatbotMessagesPerDay/ChatbotHistoryDays; 0 = unlimited) and Stripe price IDs. Subscription ties a user to a plan with Stripe IDs and status. SubscriptionService lazily creates a free-tier subscription, checks workspace/session/resource limits, and integrates Stripe checkout/portal/cancel/resume. The Stripe webhook (signature-verified) handles checkout.session.completed, customer.subscription.updated/deleted, invoice.paid, invoice.payment_failed. The rysh daemon enforces limits pre-creation via /api/resource/*.
7.8 Sharing, channels, admin
- Sharing (
share_service.go):SharedEntity= a shared tab/lane/pane_group/pane (uniqueshare_id, mode view/control, origin terminal/chatbot,LastHeartbeatAt).ShareSubscriberjoins users to shares. A NATS-driven control plane (SetupNATSHandlerssubscribes wildcardws.*.share.{register,unregister,heartbeat}— identity comes from the payload/subject token, not the wildcard) lets CLI clients register over NATS;Registerupserts on reconnect, andSubscribepublishes aws.{id}.share.{shareID}.subscriberevent so the source replays state.StartStaleShareCleanup(60s, 2min)marks shares disconnected after a 2-minute heartbeat gap (clients heartbeat every 30s).ValidateCommandAccessrequires control mode + active + subscriber. Share output streams over WebSocket (incl. a public?token=variant).- Remote file browse (
fs/*) (fs_handler.go): a read-only file-browser relay for shared panes / the mobile app.fsHandlerexposesfs.list/fs.read/fs.statas both JWT-protected routes (GET …/shares/:shareID/fs/{list,read,stat}) and public?token=routes (GET /ws/shares/:wsID/:shareID/fs/{list,read,stat}). The server holds no local filesystem access: it only authorizes the request and relays it over NATS request/reply to the share source, which answers. ANATSMaxPayloadcap guards oversized reads.
- Remote file browse (
- External connections (
internal/channel/,connection_service.go):ExternalConnectionstores per-workspace integrations withEncryptedData(AES-256-GCM, key = SHA-256 ofEncryptionKey). ARegistrymaps connection type → webhook handler + servicer (Slack signature-validated; WhatsAppX-Hub-Signature-256-validated). A generic dispatcher publishes inbound messages torysh.channel.{type}.inbound.{wsID}. TheCredentialsendpoint returns decrypted secrets to trusted CLIs so humanoid adapters can connect. - Integration Catalog / Rysh Forge (
internal/service/integration.go,internal/model/integration.go,internal/api/integration_handler.go): the server-side registry of external APIs catalogued per workspace for therysh forgeCLI feature (which turns API specs into agent tools — see doc 06). Four entities:Integration(one row per API per workspace, unique(workspace_id, slug), statusdraft → generated → published,source_type ∈ openapi|graphql|grpc),IntegrationVersion(immutable spec snapshots — raw bytes inline in aBYTEAspec_blob, SHA-256checksum, auto-incrementedversion),GeneratedArtifact(client-producedgo-sdk|ts-sdk|py-sdk|mcp-stdio|mcp-http|rysh-toolpack|docs, bytes inline inblob, unique(version, target); registering the first one flips the integrationdraft → generated), andIntegrationCredential(secret_ref= AES-256-GCM+base64 viautil.Encrypt, the same scheme andDeriveKey(EncryptionKey)asExternalConnection;json:"-", never returned). Generation runs client-side in the CLI — the server only persists and serves. The…/mcpendpoint returns hosted-MCP endpoint metadata (preferring anmcp-httpartifact overmcp-stdio) so a CLI can auto-configure. Gated onEncryptionKey(disabled when empty). Blob storage is inlineBYTEAfor the MVP;*_blob_refcolumns are forward-compat pointers for a future object-store migration. - Admin (
admin_service.go): admin role is set only by direct DB update. Paginated platform-wide queries (users, workspaces, shares, subscriptions, API keys, audit logs), stats, destructive deletes, Stripe plan-price management.
7.9 Data model (ER)
All entities use UUID PKs (except SubscriptionPlan = string id, ChatbotDailyUsage = composite key).
erDiagram
User ||--o{ Workspace : owns
User ||--o{ RefreshToken : has
User ||--o{ Subscription : has
Subscription }o--|| SubscriptionPlan : on
User ||--o{ UserMessage : "inbox"
User }o--o{ Workspace : "member (WorkspaceMember/role)"
Workspace ||--o{ APIKey : has
Workspace ||--o{ WorkspaceInvitation : has
Workspace ||--o{ ExternalConnection : has
Workspace ||--o{ SharedEntity : has
SharedEntity ||--o{ ShareSubscriber : "subscribed by"
ShareSubscriber }o--|| User : "is"
Workspace ||--o{ ChatbotConfig : has
ChatbotConfig ||--o{ ChatbotSession : has
ChatbotSession ||--o{ ChatbotMessage : transcript
Workspace ||--o{ ChatbotDailyUsage : "daily quota"
Workspace ||--o{ Integration : "forge catalog"
Integration ||--o{ IntegrationVersion : "spec snapshots"
Integration ||--o{ IntegrationCredential : "encrypted auth"
IntegrationVersion ||--o{ GeneratedArtifact : "forge output"
Integration }o--|| User : "created_by"
AuditLog }o--|| User : references
AuditLog }o--|| Workspace : references
The "unique per owner" rule is a GORM composite index — note the wire/REST identifier is always the UUID:
type Workspace struct {
ID string `gorm:"type:uuid;primaryKey" json:"id"`
OwnerID string `gorm:"type:uuid;not null;index;uniqueIndex:idx_workspaces_owner_name,priority:1" json:"owner_id"`
Name string `gorm:"not null;uniqueIndex:idx_workspaces_owner_name,priority:2" json:"name"`
State string `gorm:"default:'active'" json:"state"` // active|suspended|deleted
NATSAccount string `json:"nats_account,omitempty"` // "ws-"+ID (scaffolding)
// Owner User; Members []WorkspaceMember; APIKeys []APIKey; ExternalConnections []ExternalConnection
}
Key facts: workspace name is unique per owner (not globally), so the UUID is the canonical identifier used in all NATS subjects (ws.{id}.…) and REST paths. Creating a workspace also creates an owner WorkspaceMember and an initial API key, and sets NATSAccount = "ws-"+ID. WorkspaceMember roles are owner/admin/member/observer with capability helpers (CanManageMembers = owner||admin, CanAttach = +member, CanView = any). Most models implement a BeforeCreate assigning a UUID; ChatbotMessage is append-only (no UpdatedAt, DB-side gen_random_uuid() default) and ChatbotConfig.IsOriginAllowed is fail-closed (empty/malformed allowlist ⇒ no origin permitted). SubscriptionPlan uses string IDs (free/enterprise) and 0-means-unlimited limits. Invitations store only the token hash and are delivered through the in-dashboard UserMessage box.
The GORM
AutoMigrateinmigrate.gois incomplete and uncalled — schema is migrated externally by golang-migrate.
7.10 REST endpoint catalog
Public: GET /health · GET /api/server-info · POST /api/auth/{register,login,refresh} · GET /api/auth/social/status · POST /api/auth/social/firebase · GET /api/billing/{plans,config} · POST /api/billing/webhook · GET|POST /api/webhook/whatsapp · GET|POST /api/webhook/{type}/:connectionID.
Protected (JWT):
GET /api/auth/me·POST /api/auth/logout- Workspaces:
POST|GET /api/workspaces·GET|PATCH|DELETE /api/workspaces/:wsID·GET …/status - Members:
GET|POST …/members·PATCH|DELETE …/members/:userID - Invitations:
POST|GET …/invitations·DELETE …/invitations/:id·GET /api/invitations/:token·POST /api/invitations/:token/accept - Messages:
GET /api/messages·POST /api/messages/:id/read - API keys:
POST|GET …/keys·DELETE …/keys/:id·POST …/keys/:id/rotate - Connections:
GET …/connections/types·GET|POST …/connections·GET|PATCH|DELETE …/connections/:connID·POST …/connections/:connID/test - Integrations (Rysh Forge; mutations = owner/admin, reads = member; present only when
EncryptionKeyset — JWT only, no API-key path):POST|GET …/integrations·GET …/integrations/:id·POST …/integrations/:id/versions·POST …/integrations/:id/artifacts·GET …/integrations/:id/artifacts/:target(streams blob) ·POST …/integrations/:id/credentials(secret encrypted at rest) ·GET …/integrations/:id/mcp(MCP endpoint metadata) - Chatbots:
GET …/chatbots/usage·GET|POST …/chatbots·GET|PATCH|DELETE …/chatbots/:id·POST …/chatbots/:id/rotate-key·GET …/chatbots/:id/embed·GET …/chatbots/:id/sessions·POST …/sessions/:sid/{takeover,release,reply,close}·GET …/sessions/:sid/messages - Panes:
GET …/panes·GET …/panes/:paneID/ws·GET …/panes/:paneID/buffer - Billing:
GET /api/billing/{subscription,limits}·POST /api/billing/{checkout,portal,cancel,resume} - Shares:
GET …/shares·GET …/shares/:shareID·GET …/shares/:shareID/ws·GET …/shares/:shareID/buffer·POST|DELETE …/shares/:shareID/subscribe·POST …/shares/:shareID/command·GET …/shares/:shareID/fs/{list,read,stat}(remote file browse, relayed to source over NATS)
Admin (JWT+admin): GET /api/admin/stats · GET /api/admin/users · DELETE …/users/:userID · GET /api/admin/workspaces · DELETE …/workspaces/:workspaceID · GET /api/admin/{shares,subscriptions,api-keys,audit-logs,plans} · PATCH /api/admin/plans/:planID/prices · GET /api/admin/stripe/status.
Mixed auth (JWT or API key): GET /api/workspaces/:wsID/connections/by-type/:type/credentials · GET /api/workspaces/:wsID/shares/list · POST /api/browser-panes · DELETE …/:id · GET …/:id/history · GET …/:id/ws · POST …/:id/context · POST …/:id/share.
Chatbot widget (key + origin): POST /api/chatbot/sessions · GET …/sessions/:id/ws · GET …/sessions/:id/history · POST …/sessions/:id/close · GET /chatbot/widget.js.
Resource (API key): GET /api/resource/limits · POST /api/resource/check-limits · GET /api/resource/check-session-limit.
NATS/WS: GET /nats · GET /workspaces/:wsID/nats (transparent proxy) · GET /ws/proxy (JSON) · GET /ws/shares/:wsID/:shareID (public, ?token=) · GET /ws/shares/:wsID/:shareID/fs/{list,read,stat} (public file browse, ?token=).
7.11 Caveats
- Isolation depends entirely on the subject-ACL — the embedded NATS server is flat;
AccountManager/AuthCalloutare unwired scaffolding. RateLimit()middleware is a no-op placeholder.- DB schema is migrated by golang-migrate, not the incomplete
AutoMigrate. - Many subsystems are conditional on config (Firebase, WhatsApp, connections, browser/chatbot panes).
- The chatbot
widget.jspath is hardcoded to/opt/rysh/chatbot/widget.js; theChatbot.WidgetJSPathandChatbot.CDNURLconfig fields are present but unwired. - The remote file-browse (
fs/*) endpoints hold no local FS access on the server — every request is relayed over NATS to the share source, which performs the read. - The Integration Catalog is storage-only (the CLI generates artifacts; the server never runs the generator), has no
max_integrationslimit enforced yet (aTODO(limits)inCreateIntegration), and never sets thepublishedstatus (only thedraft→generatedauto-transition exists). The hosted-MCP…/mcproute returns metadata only — the server does not host or proxy the MCP endpoint. Schema comes from migration000023; the version/artifact unique constraints live in that SQL (not in GORM tags), andAutoMigrate(still uncalled) gained the four models.