Commit Graph
4 Commits
Author SHA1 Message Date
Daniele 67fc1455c5 fix(mcp): a failed handshake must not strand the child process
Nightly Build / build (push) Successful in 4m5s
An MCP server whose `initialize` answer is an error — a broken or version-
mismatched connector — starts fine and then never exits. `McpServer::start`
returned `Err` correctly, but the `Child` lives in the read-loop task rather
than in the returned value, so `kill_on_drop` followed a task nothing ever
drops. Every retry therefore left a live process holding three pipes and a
pidfd, and the supervisor's retry ceiling is deliberately not permanent.

The end state was not a dead connector but a dead instance: the process hit
its 1024-descriptor limit, `accept()` began failing with EMFILE, and incoming
connections queued on a socket nobody could accept from — while the process,
the port and every other connector still looked healthy. Observed in
production at ~5h from the first bad handshake to unreachable, with 229
orphaned interpreters.

`stop_server`/`stop_all` had the same hole from the other side: they document
the dropped handle as killing the process, but the task holds its end of
stdin, so the child stayed blocked on a read that would never return.

Both close with one seam. `McpServer` now owns a oneshot sender whose receiver
the read-loop selects on; nothing ever sends, so the drop is the message. That
covers a deliberate stop, the last `Arc` going away, and a `?` in `start()`
unwinding past the local binding before it was ever returned — including the
caller's `timeout`, which drops the same future. The loop then kills and, as
importantly, reaps: an unreaped child trades the orphan for a zombie holding
the same pipes.

Both leaks are covered by tests that fail without the fix.

Also raise LimitNOFILE to 65536: the installers write it, and update.sh heals
an existing unit additively, leaving an admin's own value alone. The leak is
the bug, but 1024 for a process sharing descriptors between the listener,
every user's SQLite handles and three pipes per connector is thin regardless.
2026-08-24 17:34:44 +01:00
dguiducci 046f060fcd rename the TIC system agent to event triage
Nightly Build / build (push) Successful in 7m16s
TIC said nothing about what the agent does, and named the wrong thing: the
tick belongs to the scheduler, which is generic and lives outside it. The
agent's only decision is whether an incoming event deserves an interruption
— it sorts, it never acts — so it is now event-triage, matching the
functional naming of the two memory lints.

- agents/tic/ -> agents/event-triage/, module tic/ -> event_triage/,
  TicManager -> EventTriageManager, TicConfig -> EventTriageConfig
- agent id and chat source: "tic" -> "event-triage"
- config keys: tic.* -> event_triage.*, and the config.yml section tic: ->
  event_triage: (greenfield: previously set values fall back to defaults)
- i18n en/it/fr: Event triage / Triage eventi / Tri des evenements; dropped
  the stale "TIC sessions" mention from the debug-pages description
- docs/system-agents.md, docs/index.md, docs/settings.md, CLAUDE.md, SKALD.md
2026-07-28 21:59:37 +01:00
dguiducci 6d299472e3 feat(mcp): Connectors — catalog + global vs per-user runtimes (§7/§14/§15)
Re-architects MCP from one owner table + agent-written registration into an
admin-curated catalog with two runtimes unioned per session, surfaced in the UI
as "Connectors" (mcp/schema stays neutral, §0.1).

Two runtimes behind one seam (§7):
- Global runtime: shared, stateless connectors (web-search, Tavily…) on the
  HOST, connected at boot from mcp_global_servers, access-filtered per user via
  mcp_global_access.
- Per-user runtime: a user's activated connectors run INSIDE their container,
  started at first login from mcp_user_servers and living until restart (§9);
  docker exec -i children die via kill_on_drop when the UserContext drops.
- McpProvider trait (mcp/provider.rs): the session round-loop never learns which
  runtime owns a server. McpManager implements it directly (inert ownerless
  bundle); UserMcpView implements global ∪ user with an accessible_global
  snapshot. Both share McpManager::connect_all; McpServerSpec +
  global_row_spec/user_row_spec turn a DB row into a connectable spec.
- mcp-client: McpServerConfig.launch_in runs a stdio command inside a container
  via docker exec -i (set at runtime, never parsed from config).

Authorization is a capability on the role, not `if role==admin` (§0.1/§14):
role_capabilities table + db/role_capabilities.rs — register_remote and
register_local_from_catalog are self-service (seeded on every new role), while
register_local_script and manage_catalog are admin-only. admin holds every
capability by construction. This removes the agent-facing register_mcp/delete_mcp
tools and the mcp kinds of list_items/toggle_item, closing the §14 RCE vector.

Schema:
- Registry: mcp_catalog (vetted templates — schema only, no live creds),
  mcp_global_servers + mcp_global_access, role_capabilities.
- Owner: mcp_user_servers (per-user activations; api_key encrypted at rest,
  catalog_name a bare TEXT snapshot, never an owner→registry FK).
- Drops the old owner table mcp_servers.

API + UI: src/frontend/api/mcp.rs (admin catalog/global/access + user
available/activate/activated, all capability-gated via require_cap);
web/components/connectors.js (<connectors-page>) renders the user view always
and the admin view for role_id === 'admin'.

Deferred: interactive per-user auth (OAuth callback / QR / SSH elicitation, §15)
— only none/api_key wired; no boot seed of catalog presets; per-(user, session)
MCP grant model still open.
2026-07-16 16:44:12 +01:00
dguiducci 38494a85a9 First Version 2026-07-10 15:02:09 +01:00