diff --git a/docs/api.md b/docs/api.md index 9e9c595..f1821ee 100644 --- a/docs/api.md +++ b/docs/api.md @@ -279,6 +279,8 @@ | `limit` | `50` | Page size | | `offset` | `0` | Items to skip | | `profile_id` | — | Filter by profile | +| `search` | — | Full-text search over names/previews | +| `special` | — | Session type filter. Omitted — no filtering (all sessions). `false` — service (`special=True`) sessions excluded, **the webclient default**. `true` — service sessions only | **Response `200`** (when pagination params provided) ```json @@ -1502,6 +1504,52 @@ - `400` — invalid payload - `503` — OAuth not configured +#### `POST /webhooks/synapse` (also `/webhooks/synapse/`) + +Receive one GNEXUS Synapse s2s delivery (see [`synapse.md`](synapse.md)). +Signature verified against every active per-user target secret; replays are +re-acked but never acted on twice. A non-duplicate delivery schedules a +fire-and-forget agent reaction (per-user `reactions_enabled` gate). + +**Response `200`** +```json +{ "received": true, "event_id": "evt-..." } +``` + +**Errors** +- `401` — invalid webhook signature (or unparseable body) +- `503` — no navi-side Synapse targets configured + +--- + +### Synapse + +Per-user reaction settings (auth required). See details in +[`synapse.md`](synapse.md). + +#### `GET /synapse-settings` + +Returns the caller's settings plus `source_ready` (not persisted): +`reactions_enabled`, `push_target` (`app` | `app_synapse` | `synapse`), +`completion_notify` (`always` | `important` | `never`), `instructions`, +`source_ready`. + +#### `PUT /synapse-settings` + +Full save (`reactions_enabled`, `push_target`, `completion_notify`, +`instructions`). When `instructions` change, records an edit version +(`edited_by="user"`). Returns the saved settings + `source_ready`. + +#### `GET /synapse-settings/versions?limit=20` + +Instruction edit history, newest first: `content`, `edited_by` +(`user` | `navi`), `reason`, `created_at`. `limit` 1..100. + +#### `POST /synapse-targets` · `GET /synapse-targets` · `DELETE /synapse-targets/{id}` + +Per-user Synapse s2s target secrets: create (`token_ref` + `secret`, +secret is write-only), list (`id`, `token_ref`, `created_at`), revoke. + --- ## Files diff --git a/docs/config.md b/docs/config.md index 1a12006..2b7c99a 100644 --- a/docs/config.md +++ b/docs/config.md @@ -204,6 +204,18 @@ Push notifications fire when an agent turn (or a scheduled recall) completes and no browser tab is watching that session. See [`docs/push.md`](push.md). +## Synapse source + +| Variable | Type | Default | Description | +|---|---|---|---| +| `SYNAPSE_SOURCE_URL` | str | `""` | GNEXUS Synapse base URL for the outgoing source API. Empty = nothing is ever emitted. | +| `SYNAPSE_SOURCE_API_KEY` | str | `""` | `syn_*` source key registered in the Synapse admin panel (write it into `.env` yourself). | +| `SYNAPSE_SOURCE_NAME` | str | `navi` | Source name on Synapse's side. | + +Without both URL and key, Synapse-linked outgoing events are silently skipped and +the UI disables Synapse-linked options (`source_ready: false`). +See [`docs/synapse.md`](synapse.md). + ## Persona | Variable | Type | Default | Description | diff --git a/docs/index.md b/docs/index.md index cfb8ed0..b5e79c4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -35,6 +35,7 @@ | [`tasks.md`](tasks.md) | Background tasks — detach tool calls, TaskManager, completion notes, message queue | | [`websocket.md`](websocket.md) | WebSocket protocol — all events, stop mechanism | | [`push.md`](push.md) | PWA — install, offline shell, web push (VAPID, trigger rule) | +| [`synapse.md`](synapse.md) | GNEXUS Synapse integration — inbound gateway, agent reactions, notify tool, outbound events | | [`profiles.md`](profiles.md) | Profiles, system prompts, persona, profile switching | | [`context_providers.md`](context_providers.md) | Context providers — inject dynamic system context per turn | | [`memory.md`](memory.md) | Long-term memory — facts, extraction, search | diff --git a/docs/synapse.md b/docs/synapse.md new file mode 100644 index 0000000..dc4d962 --- /dev/null +++ b/docs/synapse.md @@ -0,0 +1,104 @@ +# Synapse integration + +navi speaks to the [GNEXUS Synapse](https://handbook.gnexus) notification hub in +both directions: + +- **Inbound** — Synapse delivers platform events to navi's webhook; each event + can trigger an *agent reaction* (a background agent run). +- **Outbound** — navi emits low-level lifecycle events (reaction finished / + failed, notify-tool pushes) into Synapse's ingest, where they are simply + logged. + +The `gnexus-synapse` package (vendored dependency) owns the wire format: +`verify_webhook` / `ack` / `make_signature` on the receiving side, +`AsyncSynapseClient` on the emitting side. + +## Configuration + +| Env var | Purpose | +|---|---| +| `SYNAPSE_SOURCE_URL` | Synapse base URL for the *outgoing* source API | +| `SYNAPSE_SOURCE_API_KEY` | `syn_*` key registered in the Synapse admin panel | +| `SYNAPSE_SOURCE_NAME` | source name shown in Synapse (default `navi`) | + +Without a source key nothing outgoing is sent, and Synapse-linked UI options +are disabled (`source_ready: false` in settings responses). + +Inbound deliveries need a per-user **target**: a `token_ref` + shared secret +created in the Synapse admin panel and registered by the user in navi +(`POST /synapse-targets`). + +## Inbound: delivery gateway + +`POST /webhooks/synapse` (`navi/api/routes/synapse.py`) — verifies the +signature against every active target secret (a mismatch never tells which +target failed → 401), dedupes by `event_id` (Synapse retries until 2xx; replays +are re-acked but never acted on twice), logs `synapse.delivery_accepted`, and +hands the envelope to `schedule_reaction()` fire-and-forget, so the ack never +waits for agent work. + +## Inbound: reaction runner + +`navi/synapse/reactions.py` — the point of the feature. Pipeline: + +1. **Gate** — the matched target's user must have `reactions_enabled: true`; + otherwise the event is accepted and discarded (`synapse.reaction_disabled`). +2. **Dispatcher meta-pass** — a single LLM call to the hidden `dispatcher` + profile (`navi/profiles/dispatcher/`, temperature 0.2, one shot, no tools). + Input: the user's reaction instructions + the event envelope. Output: strict + JSON `{profile_id, task, understood}` or `{skip: true}`. Unparseable output + or a backend failure = skip (logged, never a crash). +3. **Profile resolution** — only *visible* profiles may host a reaction; a + hidden or unknown id falls back to `secretary`. +4. **Special session** — a fresh session with `special=True`, name + `Synapse: {event_type}` and `session_metadata["synapse"]` (event id/type/ + dispatcher's understanding). Session + opening message are persisted + *before* the run, so a crash mid-run still leaves a visible trail. +5. **Headless run** — orchestrator `create_run` + `run_agent` with the target + user's tool context. No live subscriber, but the run registry means the user + watching the session in the UI sees the stream live. +6. **Finalise** — app push per `completion_notify` (`always` / `important` = + only when the run had errors / `never`) and a low-level + `reaction.finished` / `reaction.failed` event to Synapse (best effort). + +## Outbound: source side + +`navi/synapse/outbound.py` — `emit_low_level(subject, action, payload, +priority, dedup_key)`: silently no-ops when the source is not configured, +otherwise emits through `AsyncSynapseClient` and closes it per call. + +Users of it: the reaction runner (`reaction`, `finished`/`failed`) and the +`notify` tool (`navi-notification`, action = push level). + +## notify tool + +`navi/tools/notify.py` — the agent's signal channel. Parameters: `message` +(required) + `level` (`info` | `warning` | `intervention`). Reads the user's +`push_target` and delivers through both legs, then answers *honestly*: +`Delivered: app. Skipped: synapse (source key not configured)` — the agent can +see what actually happened and never retries into a wall. Priorities map +`info→normal`, `warning→high`, `intervention→critical`. + +## Per-user settings + +`navi/synapse/settings_store.py` (postgres `synapse_settings`, +`synapse_instruction_versions`): + +- `reactions_enabled` (default `false`), +- `push_target`: `app` | `app_synapse` | `synapse` (default `app`), +- `completion_notify`: `always` | `important` | `never` (default `important`), +- `instructions`: reaction instructions, editable by **both** the user and + navi (`synthesize_instructions` tool edits with `edited_by="navi"`), every + change recorded in the version history (last 100 kept, with author + reason). + +REST: `GET/PUT /synapse-settings` (PUT records a version when instructions +change), `GET /synapse-settings/versions?limit=`. GET/PUT replies include the +non-persisted `source_ready` flag. + +## Service sessions in the UI + +`special=True` sessions are excluded from the regular session list at the REST +layer (`GET /sessions?special=false|true`; omitting the param shows +everything — admin/API callers see all). The webclient sidebar has a +service-sessions toggle that fetches `special=true`; special items render +muted with a robot icon. \ No newline at end of file diff --git a/docs/tools.md b/docs/tools.md index eccb3cc..b096a49 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -56,6 +56,8 @@ | `PlanTool` | `plan` | Agent-invoked planning: fresh plan or re-plan with a `reason` (and optional `updated_goal`). The tool result instructs the agent to wait for user confirmation when the task is complex, and to proceed immediately otherwise | | `ScheduleRecallTool` | `schedule_recall` | Schedule a headless callback for the current session (once/recurring/immediate) | | `ManageRecallTool` | `manage_recall` | Cancel, skip, or list scheduled recalls for the current session | +| `SynapseInstructionsTool` | `synapse_instructions` | Read / update the user's Synapse reaction instructions (`edited_by="navi"`, `reason` recorded); updates are stored in the edit history | +| `NotifyTool` | `notify` | Push a notification to the user (`message` + `level`: info/warning/intervention); legs app push and Synapse event per the user's push_target, result reports delivered/skipped honestly | ### User tools (`tools/*.py`)