Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,14 @@ DAY_MAX_COST_USD=20
# ── Social login (optional) ──────────────────────────────────────────────────
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=

# ── Google (optional) ────────────────────────────────────────────────────────
# One OAuth app powers "Sign in with Google" and the built-in Gmail + Calendar
# integration (and email triggers). Setup: docs/google.md
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# How often email triggers check for new mail, in seconds (min 30).
# TRIGGER_POLL_SECONDS=120

# Allow MCP servers / web_fetch on private networks (self-hosted MCP on your LAN).
SYNAPSE_ALLOW_PRIVATE_NETWORK=false
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**An open-source agent that does real work in your tools — and can't be talked into doing the wrong thing.**

SynapseAI connects to the services you already use (GitHub, Linear, Notion, Jira, Sentry, or any [MCP](https://modelcontextprotocol.io) server), takes multi-step actions on your behalf, and asks before anything that writes, sends or deletes. Approvals are enforced in code, not in the prompt; once the agent has read untrusted content such as an email or a web page, every outward action needs your sign-off. Self-host it with one command, bring your own model keys, and see exactly what it did and what it cost.
SynapseAI works in the services you already use (Gmail and Google Calendar built in; GitHub, Linear, Notion, Jira, Sentry, or any [MCP](https://modelcontextprotocol.io) server), takes multi-step actions on your behalf, and asks before anything that writes, sends or deletes. It doesn't wait to be asked: automations start work on a schedule, when an email arrives, or when another app calls a webhook. Approvals are enforced in code, not in the prompt; once the agent has read untrusted content such as an email or a web page, every outward action needs your sign-off. Self-host it with one command, bring your own model keys, and see exactly what it did and what it cost.

![A task waiting for approval: the agent drafted an email and shows it as an editable card, with the activity timeline on the right](docs/screenshots/task.png)

Expand All @@ -18,13 +18,16 @@ Agent products that act in your tools share three problems: approvals that live
| **Taint tracking (Rule of Two)** | When a run reads untrusted content — an email body, a web page, an MCP result — it is marked tainted, and from then on no write or send is auto-approved, whatever the rules say. Prompt injection can still mislead the model, but it can't act without you. |
| **Approvals you can edit** | Approval cards show the full payload. Edit the email before it goes, deny with a reason the agent sees, or approve from the inbox later — paused runs live in Postgres, not in a process. |
| **Durable runs** | Runs are leased to workers and checkpointed at every step. Restart a server mid-run and another worker picks it up; side-effecting calls interrupted mid-flight are reported as *unknown*, never silently retried. |
| **Works when you're not looking** | Automations fire on a schedule, on new email, or on a webhook from GitHub, Linear, Stripe or anything else. Each event becomes its own task, starts tainted, and lands in your inbox only when it needs a signature. |
| **Steer while it works** | Send a message while the agent is busy and it reads it at the next step. Send one while it's waiting for approval and the pending action is dropped in favour of your new instruction. |
| **Predictable cost** | Per-run and per-day budgets enforced before every model call, a fallback chain across providers, prompt-cache-friendly history, and cost on every run, priced from a daily-synced catalog of 300+ models. |
| **Yours** | MIT, self-hosted, one Postgres. Any model: Anthropic, OpenAI, Google, Vercel AI Gateway, or anything OpenAI-compatible (Ollama, vLLM). Users can bring their own keys. |

## Features

- Chat with live streaming (reasoning, tool calls, results) that resumes after reconnects
- Built-in Gmail and Google Calendar: search, read threads, draft, reply in-thread, label and archive, book and move meetings, answer invitations ([setup](docs/google.md))
- Automations triggered by a cron schedule, new Gmail messages matching a search, or a secret webhook URL
- MCP connections over Streamable HTTP with OAuth 2.1 + PKCE, or bearer tokens; presets for GitHub, Linear, Notion, Atlassian, Sentry
- Approval inbox, editable approvals, deny-with-reason, cancel now / after this step
- Knowledge base: upload docs, hybrid full-text + vector search (pgvector, reciprocal rank fusion)
Expand Down Expand Up @@ -72,15 +75,15 @@ The server is stateless apart from Postgres. Run the API and workers separately
Workers ─────────┘
└─ runtime (packages/core): one model step at a time
├─ policy engine (effects, rules, taint, budgets)
├─ tools (packages/tools): MCP, web, knowledge, memory
├─ tools (packages/tools): Google, MCP, web, knowledge, memory
└─ models: AI SDK providers + fallback chain
```

| Package | What it is |
|---|---|
| `packages/core` | Host-agnostic agent runtime: run loop, approval state machine, policy, compaction, model fallback, client event reducer. No database or HTTP. |
| `packages/db` | Drizzle schema and migrations, Postgres store for the runtime, run leasing, ordered event log, secret encryption. |
| `packages/tools` | MCP client with OAuth, SSRF guard, web search/fetch, knowledge base, memory. |
| `packages/tools` | Gmail and Calendar, MCP client with OAuth, SSRF guard, web search/fetch, knowledge base, memory. |
| `packages/evals` | Scenario harness (task + safety) with simulated users; `bun run evals`. |
| `apps/server` | HTTP API, auth, SSE, worker. |
| `apps/web` | Next.js UI. |
Expand Down
2 changes: 2 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ The main threats and how they are handled:
| Runaway spend | Per-run step, token and cost limits and a per-day cost limit, checked before every model call. Users can only tighten the operator's caps. |
| Mis-annotated MCP tools | MCP `readOnlyHint`/`destructiveHint` are server-supplied hints. Users can override any tool's effect; MCP reads are never re-run after a crash; all MCP output taints the run. |
| Taint laundering across turns | Taint is per chat: once any run in a chat has read untrusted content, every later run in it starts tainted. |
| Event triggers (new email, webhooks) | Each event starts its own task, tainted from its first message, so nothing but reads is auto-allowed. Event content is quoted in a delimited block whose closing tag third parties cannot forge. Webhook URLs carry a 192-bit secret (compared in constant time, rotatable) and are rate-limited per automation. |
| Google tokens | Sealed with `SYNAPSE_SECRET` (AES-256-GCM). The OAuth `state` is a sealed blob holding the user id and PKCE verifier, so a callback cannot complete for another account. Disconnecting revokes the grant at Google. Outgoing mail headers are stripped of line breaks (no header injection). |
| Regex denial of service in policy rules | Patterns with nested quantifiers or backreferences are refused, and matching input is capped. |
| Two workers driving one run | Status writes are fenced by lease owner; a worker that cannot renew its lease stops before it expires. |

Expand Down
8 changes: 7 additions & 1 deletion apps/server/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { Hono } from "hono";
import { secureHeaders } from "hono/secure-headers";
import { type AppEnv, onError, requireUser } from "./http";
import { documents, memories, policy, settings, usage } from "./routes/account";
import { automations } from "./routes/automations";
import { automations, hooks } from "./routes/automations";
import { chats } from "./routes/chats";
import { clientMetadataDocument, connections, oauthCallback } from "./routes/connections";
import { googleCallback, integrations } from "./routes/integrations";
import { models } from "./routes/models";
import { approvals, runs } from "./routes/runs";
import type { Services } from "./services";
Expand Down Expand Up @@ -45,12 +46,15 @@ export function createApp(services: Services) {
github: Boolean(config.GITHUB_CLIENT_ID),
google: Boolean(config.GOOGLE_CLIENT_ID),
},
integrations: { google: Boolean(config.google) },
features: { webSearch: Boolean(config.webSearch), embeddings: Boolean(config.embedding) },
models: config.SYNAPSE_MODELS,
}),
);
app.get("/oauth/client-metadata.json", (c) => c.json(clientMetadataDocument(c)));
app.on(["GET", "POST"], "/api/auth/*", (c) => services.auth.handler(c.req.raw));
// Public: authenticated by the secret token in the URL, not a session.
app.route("/api/hooks", hooks);

const api = new Hono<AppEnv>();
api.use(requireUser);
Expand All @@ -59,6 +63,8 @@ export function createApp(services: Services) {
api.route("/approvals", approvals);
api.route("/connections/oauth/callback", oauthCallback);
api.route("/connections", connections);
api.route("/integrations/google/callback", googleCallback);
api.route("/integrations", integrations);
api.route("/policy", policy);
api.route("/memories", memories);
api.route("/documents", documents);
Expand Down
15 changes: 14 additions & 1 deletion apps/server/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname } from "node:path";
import { parseModelRef } from "@synapse/core";
import type { WebSearchConfig } from "@synapse/tools";
import type { GoogleAppConfig, WebSearchConfig } from "@synapse/tools";
import { z } from "zod";

const csv = z.string().transform((s) =>
Expand Down Expand Up @@ -54,8 +54,11 @@ const Env = z.object({

GITHUB_CLIENT_ID: z.string().optional(),
GITHUB_CLIENT_SECRET: z.string().optional(),
/** One Google OAuth app serves both "Sign in with Google" and the Gmail/Calendar integration. */
GOOGLE_CLIENT_ID: z.string().optional(),
GOOGLE_CLIENT_SECRET: z.string().optional(),
/** How often email triggers check for new mail. */
TRIGGER_POLL_SECONDS: z.coerce.number().int().min(30).default(120),
});

export type Config = ReturnType<typeof loadConfig>;
Expand All @@ -82,8 +85,18 @@ export function loadConfig(env: Record<string, string | undefined> = process.env
? parsed.GOOGLE_GENERATIVE_AI_API_KEY
: undefined;

const google: GoogleAppConfig | null =
parsed.GOOGLE_CLIENT_ID && parsed.GOOGLE_CLIENT_SECRET
? {
clientId: parsed.GOOGLE_CLIENT_ID,
clientSecret: parsed.GOOGLE_CLIENT_SECRET,
publicUrl: parsed.PUBLIC_URL,
}
: null;

return {
...parsed,
google,
SYNAPSE_MODELS: models,
SYNAPSE_SUMMARY_MODEL: summaryModel,
secret,
Expand Down
Loading
Loading