-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathconfig.js
More file actions
299 lines (271 loc) · 18.8 KB
/
Copy pathconfig.js
File metadata and controls
299 lines (271 loc) · 18.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
// ---------------------------------------------------------------------------
// config.js
// Central place for all environment variables and shared constants.
// ---------------------------------------------------------------------------
// (test commit: verifying the Vercel deploy check after removing the
// legacy "Manufact"-named deployment -- no functional change)
export const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
export const GITHUB_API = "https://api.github.com";
export const DEFAULT_OWNER = process.env.DEFAULT_OWNER || "allocsys";
// Minimum spacing (ms) enforced between outgoing GitHub REST requests, to
// avoid tripping GitHub's *secondary* rate limit, which fires on request
// burstiness/concurrency rather than raw hourly quota (see
// https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api).
// A shared in-process queue in client.js enforces this even across
// concurrent tool calls. Override via env var if this proves too
// conservative or not conservative enough in practice.
export const GITHUB_MIN_REQUEST_INTERVAL_MS = Number(process.env.GITHUB_MIN_REQUEST_INTERVAL_MS) || 300;
// Retry behavior specifically for secondary-rate-limit (403) and primary
// rate-limit-exhausted (403 with x-ratelimit-remaining: 0) responses, plus
// 429s. Does NOT retry other 4xx/5xx errors -- those are real failures, not
// pacing issues, and should surface immediately.
export const GITHUB_MAX_RETRIES = Number(process.env.GITHUB_MAX_RETRIES) || 3;
// Fallback backoff (ms) when GitHub doesn't send a Retry-After header.
// Doubles each retry (300 -> ~1.6s -> ~3.2s with jitter) if Retry-After is absent.
export const GITHUB_RETRY_BASE_MS = Number(process.env.GITHUB_RETRY_BASE_MS) || 1500;
export const NOTION_TOKEN = process.env.NOTION_TOKEN;
export const NOTION_API = "https://api.notion.com/v1";
export const NOTION_VERSION = "2022-06-28";
// Throttle + retry for the Notion API (fix #3 -- rate-limit asymmetry,
// 2026-07-27). Notion's documented average rate limit is ~3 requests/second
// per integration; this spacing keeps a single madmcp instance comfortably
// under that even when several Notion calls land in the same parallelized
// delegate_agent step. Mirrors GITHUB_MIN_REQUEST_INTERVAL_MS/
// GITHUB_MAX_RETRIES/GITHUB_RETRY_BASE_MS above -- same override pattern.
export const NOTION_MIN_REQUEST_INTERVAL_MS = Number(process.env.NOTION_MIN_REQUEST_INTERVAL_MS) || 350;
export const NOTION_MAX_RETRIES = Number(process.env.NOTION_MAX_RETRIES) || 3;
export const NOTION_RETRY_BASE_MS = Number(process.env.NOTION_RETRY_BASE_MS) || 1000;
// Dedicated index DATABASE used for entity_id -> page_id dedup lookups.
// SUPERSEDES the original page-based index (2026-07-17 fix for gap #1, see
// mem0 entity_id: madmcp-notion-connector-gaps-roadmap): that fix solved the
// notion_search indexing-lag problem by reading a page's own blocks directly
// (uncached, no lag) instead of searching -- but inherited a NEW gap it
// documented at the time: page block reads are capped at 100 blocks per
// page (Notion's /blocks/{id}/children pagination), so an index page with
// more than ~100 tracked entities would silently stop finding older entries.
// REAL FIX (2026-07-24): a Notion database queried via /databases/{id}/query
// with a filter on EntityId is just as immediately-consistent as the direct
// block read (no search-index lag either way, since it's not going through
// notion_search) but is NOT subject to the 100-block-page limit -- database
// queries paginate independently of any single page's block count.
// UPDATE (2026-07-24, later same day): the old page-based index, its
// migration tool, and a since-discovered duplicate database were all
// archived/removed once every remaining reader (linking.js's
// findTagOverlapCandidates, sync/mem0_notion.js's readSyncedIndexEntries)
// was moved onto queryAllIndexEntries (client.js), which reads this
// database directly. NOTION_INDEX_PAGE_ID no longer exists as a config
// value -- nothing in the codebase reads it anymore. This database was
// also recreated fresh (new ID below) as part of that same cleanup, with
// zero rows -- no old entries were migrated in.
// Entity Index database properties: Name (title, holds the entity_id for
// readability in the Notion UI), EntityId (rich_text, the actual filter
// target), PageId (rich_text), Url (url), Tags (rich_text, comma-separated).
// Override via env var if this database is ever moved/recreated.
export const NOTION_INDEX_DATABASE_ID = process.env.NOTION_INDEX_DATABASE_ID || "3a745572-b580-8160-856b-cf6544c8ffa8";
// Parent page for new pages created by sync_mem0_to_notion (connectors/sync/
// mem0_notion.js). Was the "Memory Index" page, but that page went 404
// (deleted/unshared) during a manual Notion reorg on 2026-08-01. Now
// defaults to the "Claude" page (id below), adopted as the new root --
// override via env var if that page is ever moved/recreated, same pattern
// as NOTION_INDEX_PAGE_ID above.
export const NOTION_SYNC_PARENT_PAGE_ID = process.env.NOTION_SYNC_PARENT_PAGE_ID || "3a045572-b580-8007-b622-c120958557bf";
export const MEM0_API_KEY = process.env.MEM0_API_KEY;
export const MEM0_API = "https://api.mem0.ai";
export const MEM0_USER_ID = process.env.MEM0_USER_ID || "default";
// Throttle + retry for the Mem0 API (fix #3 -- rate-limit asymmetry,
// 2026-07-27). Mem0 doesn't publish a hard per-second limit the way GitHub
// and Notion do, so this is a conservative default rather than a figure
// tied to a documented threshold -- same override pattern as the others.
export const MEM0_MIN_REQUEST_INTERVAL_MS = Number(process.env.MEM0_MIN_REQUEST_INTERVAL_MS) || 300;
export const MEM0_MAX_RETRIES = Number(process.env.MEM0_MAX_RETRIES) || 3;
export const MEM0_RETRY_BASE_MS = Number(process.env.MEM0_RETRY_BASE_MS) || 1000;
export const CLOUDFLARE_API_TOKEN = process.env.CLOUDFLARE_API_TOKEN;
export const CLOUDFLARE_ACCOUNT_ID = process.env.CLOUDFLARE_ACCOUNT_ID;
export const CLOUDFLARE_API = "https://api.cloudflare.com/client/v4";
// Context7 works without a key at low rate limits, so this is optional
// (unlike the other connectors' tokens) — only warn, never hard-fail on it.
export const CONTEXT7_API_KEY = process.env.CONTEXT7_API_KEY;
export const CONTEXT7_API = "https://context7.com/api/v2";
// Shared-secret auth for the /mcp endpoint. If set, every request to /mcp
// must include a matching `x-manufact-key` header, or it is rejected before
// any connector tools (GitHub, Notion, Mem0, Fetch) are reachable.
// If unset, the endpoint remains open (legacy behavior) — set this in
// production so your tokens/connectors aren't usable by anyone with the URL.
export const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
export const GEMINI_API = "https://generativelanguage.googleapis.com/v1beta";
// Default model -- override via env var if this drifts out of date; Google
// renames/retires Gemini model IDs periodically, so don't assume this stays
// current without checking https://ai.google.dev/gemini-api/docs/models.
export const GEMINI_MODEL = process.env.GEMINI_MODEL || "gemini-flash-latest";
// Fallback model cascade for rate-limit (429) errors. Free-tier Gemini quotas
// are tracked PER MODEL, so a different model has its own separate RPM
// bucket -- on a 429 from GEMINI_MODEL, client.js retries the same request
// against the next model here instead of failing the whole call/investigation
// outright. This multiplies effective free-tier throughput without enabling
// billing. Order matters: put higher-RPM/lower-capability models later, since
// they're only used once the primary model's quota is exhausted for the
// current window. Override via env var as a comma-separated list of model
// IDs; GEMINI_MODEL is always tried first regardless of whether it's
// repeated in this list. See https://ai.google.dev/gemini-api/docs/models for
// current model IDs/limits -- these drift as Google ships new Flash/Flash-Lite
// generations.
export const GEMINI_FALLBACK_MODELS = (process.env.GEMINI_FALLBACK_MODELS || "gemini-3.5-flash-lite,gemini-3.1-flash-lite")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
// Defensive ceiling on a single generateContent call -- no official guidance
// from Google on max latency, but without SOME timeout a hung/dropped
// connection leaves agent_delegate.js's per-step checkpointing unable to kick in
// at all (the call just never returns). Override via env var if this proves
// too tight for slower multi-tool-call turns, or too loose relative to the
// hosting platform's own request-duration limit.
export const GEMINI_REQUEST_TIMEOUT_MS = Number(process.env.GEMINI_REQUEST_TIMEOUT_MS) || 55000;
// Read/write isolation for the Gemini connector's Notion access (2026-07-25
// plan): Gemini tools may READ any page/database reachable via the existing
// Notion connector (Memory Index, Entity Index, Job Leads, etc.), but may
// only WRITE under this one page -- deliberately NOT a caller-supplied
// parameter anywhere in connectors/gemini/, so there is no code path that
// lets a Gemini tool call target a write anywhere else. A bad or
// hallucinated Gemini write can only ever land inside this subtree, never
// inside the Claude-side Memory Index / Entity Index / Job Leads structures
// that other tools' dedup and sync logic depend on.
// "Gemini" page, created as a sibling of the "Claude" root page.
export const GEMINI_NOTION_ROOT_PAGE_ID = process.env.GEMINI_NOTION_ROOT_PAGE_ID || "3a845572-b580-81d0-8653-f64596e45e58";
// Redis-backed per-model rate-limit cooldown for the Gemini connector (see
// connectors/gemini/cooldown.js), provisioned via the Vercel Marketplace
// Upstash integration ("vercel install upstash", or the Vercel dashboard).
// Not read as named exports here -- connectors/gemini/cooldown.js reads the
// env vars directly, and accepts EITHER naming convention Vercel might hand
// you depending on how the integration was provisioned:
// UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN (raw Upstash Marketplace integration)
// KV_REST_API_URL / KV_REST_API_TOKEN (Vercel's own "KV" product, Upstash-backed)
// Discovered 2026-07-26: a deployment with the latter names set had Redis
// fully provisioned and reachable, but every cooldown/checkpoint call
// reported "not configured" anyway, because the code only checked for the
// UPSTASH_* names at the time. Listed here only so both are discoverable
// alongside every other service's env vars, not because config.js exports
// them. If neither pair is set, cooldown.js fails open (no cross-call
// rate-limit memory, but never breaks a real Gemini call) -- safe to leave
// both unset until an integration is activated.
// Exa /answer API (docs.exa.ai/reference/answer) -- backs delegate_research's
// wide mode as the sole implementation (a single search+synthesis call),
// not a fallback for anything; see connectors/exa/client.js's file header
// for the 2026-07-27 history of what this replaced. FORMERLY OPENAI
// (2026-07-27): this section replaced the OPENAI_* config that used to
// serve the same role via OpenAI's Responses API web_search tool -- see git
// history if that needs to be resurrected.
//
// EXA_API_KEYS is a comma-separated list -- deliberately supporting
// MULTIPLE keys/accounts, same reasoning as the OpenAI config it replaces.
// There is no free tier for this endpoint (billed per call, on top of
// content-retrieval costs baked into the same call), so this cascade is
// about rate-limit headroom (Exa's documented default is 10 QPS per
// account, shared across ALL endpoints) and cost/account isolation, not
// accessing a free quota. Unlike the OpenAI config this replaces, there is
// no per-key model tier to cascade through first -- Exa's /answer endpoint
// has no selectable model for this call shape -- so connectors/exa/client.js
// simply rotates through EXA_API_KEYS in order on a 429/503/network error.
export const EXA_API_KEYS = (process.env.EXA_API_KEYS || "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
export const EXA_API = "https://api.exa.ai/answer";
// Same defensive-ceiling reasoning as GEMINI_REQUEST_TIMEOUT_MS above.
export const EXA_REQUEST_TIMEOUT_MS = Number(process.env.EXA_REQUEST_TIMEOUT_MS) || 55000;
// ---------------------------------------------------------------------------
// Frontend/design delegate (connectors/frontend/) -- delegate_designer's
// write-capable agent loop (agent.js), backed by the existing Gemini
// connector (geminiChat/GEMINI_API_KEY/GEMINI_MODEL above) -- no separate
// provider config needed here.
// Extensions delegate_designer is allowed to read as context OR write
// as output. Fences BOTH the read side (so a manipulated task can't feed a
// secrets-adjacent file like config.js to a third-party LLM API as prompt
// text) and the write side (so a generation can't overwrite server.js/
// package.json/workflow files/etc) to the frontend surface this tool exists
// for. Comma-separated, override via env var if the frontend stack changes.
export const FRONTEND_ALLOWED_EXTENSIONS = (process.env.FRONTEND_ALLOWED_EXTENSIONS || ".html,.css,.scss,.jsx,.tsx,.vue")
.split(",")
.map((s) => s.trim().toLowerCase())
.filter(Boolean);
// ---------------------------------------------------------------------------
// delegate_designer (issue #61 agent redesign) -- bounds
// connectors/frontend/designer_delegate.js's runDesignAgent loop, the read_file/
// write_file/validate-based replacement for the generate->validate->fix
// loop above. Deliberately TIGHTER than delegate_agent's 20 default / 30
// hard cap (see the GEMINI connector's HARD_MAX_STEPS in connectors/gemini/
// agent_delegate.js): this agent's tool set (read/write/validate on frontend
// files within one branch) is far narrower than delegate_agent's open-ended
// cross-system investigation surface, so it doesn't need investigation-scale
// step counts to do useful work. Resolved 2026-08-01 per the Notion design
// doc's open question -- see issue #61.
export const FRONTEND_DEFAULT_STEPS = Number(process.env.FRONTEND_DEFAULT_STEPS) || 12;
export const FRONTEND_HARD_MAX_STEPS = Number(process.env.FRONTEND_HARD_MAX_STEPS) || 20;
// validate() calls do NOT count against the step budget above (a validate
// call is cheap -- local syntax checking, no LLM/network round trip beyond
// the agent's own turn -- so charging a full step for it would waste budget
// that's better spent on read_file/write_file turns). Capped independently,
// PER FILE PATH, so a model stuck in a validate/tweak/validate loop on one
// file can't thrash indefinitely without ever burning a step -- resolved
// alongside FRONTEND_DEFAULT_STEPS above, same source.
export const FRONTEND_MAX_VALIDATE_CALLS = Number(process.env.FRONTEND_MAX_VALIDATE_CALLS) || 5;
// ---------------------------------------------------------------------------
// GitHub App -- scoped, short-lived clone tokens for PRIVATE repos (2026-07-28
// plan, see Notion entity_id madmcp-github-app-scoped-clone-token-plan).
// Deliberately a SEPARATE credential from GITHUB_TOKEN above: GITHUB_TOKEN is
// a broad, long-lived token used by every other GitHub tool in this
// connector, while this App is scoped ONLY to contents:read and installed
// only on repos that need sandbox-clone access. connectors/github/app_auth.js
// mints per-repo installation tokens from these credentials on demand
// (~1hr TTL, GitHub's max), returned to the calling model so it can `git
// clone` a private repo into its own sandbox -- see that file's header for
// why the token has to pass through the calling model at all (the sandbox
// can't reach this server directly to fetch it itself).
// GITHUB_APP_PRIVATE_KEY: paste the PEM as-is; if your env var tooling can't
// store literal newlines, escape them as \n and app_auth.js unescapes them.
export const GITHUB_APP_ID = process.env.GITHUB_APP_ID;
export const GITHUB_APP_INSTALLATION_ID = process.env.GITHUB_APP_INSTALLATION_ID;
export const GITHUB_APP_PRIVATE_KEY = process.env.GITHUB_APP_PRIVATE_KEY;
// Grace period (connectors/github/app_auth.js): how long a freshly minted
// clone token is allowed to live before this server auto-revokes it via
// GitHub's revoke endpoint, making it effectively single-use rather than
// relying on GitHub's own ~1hr installation-token TTL. Enforced via
// @vercel/functions' waitUntil() (requires Fluid Compute), not a bare
// setTimeout -- Vercel can freeze/tear down a serverless invocation right
// after its response is sent, so a plain unref()'d timer isn't reliable
// there. 30s by default -- short, to minimize compute kept alive per call;
// raise it (env var) if clones of a particularly large private repo start
// getting cut off mid-transfer.
export const GITHUB_APP_TOKEN_REVOKE_GRACE_SECONDS = Number(process.env.GITHUB_APP_TOKEN_REVOKE_GRACE_SECONDS) || 30;
// Jules (Google's async coding agent) REST API. Alpha API -- no
// unauthenticated tier, unlike Context7 above; JULES_API_KEY is required
// for any jules_* tool to work. Get a key from the Jules web app's Settings
// page (https://jules.google.com/settings#api), max 3 keys per account.
export const JULES_API_KEY = process.env.JULES_API_KEY;
export const JULES_API = "https://jules.googleapis.com/v1alpha";
export const MCP_SHARED_KEY = process.env.MCP_SHARED_KEY;
// IP allowlist for /mcp, /mcp/:key, and /. Restricts inbound requests to
// known client CIDR ranges regardless of whether the shared key is valid,
// so a leaked key alone isn't enough to reach the server.
// Defaults ON, and defaults to Anthropic's published outbound range for
// Claude connector traffic (https://claude.com/docs/connectors/building/authentication).
// Add more ranges (e.g. for OpenAI/GPT actions) as a comma-separated list.
// Set IP_ALLOWLIST_ENABLED=false to disable entirely (e.g. for local dev).
export const IP_ALLOWLIST_ENABLED = process.env.IP_ALLOWLIST_ENABLED !== "false";
// 208.77.244.90/32 is manufact's own deploy-time health-check IP (it POSTs an
// MCP `initialize` request to /mcp as part of deploy verification) — without
// it, every deploy fails its own health check against this allowlist.
export const ALLOWED_IP_RANGES = (process.env.ALLOWED_IP_RANGES || "160.79.104.0/21,208.77.244.90/32")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
// Number of reverse-proxy hops in front of this server whose X-Forwarded-For
// entries should be trusted when determining the real client IP (used for
// both Express's own trust-proxy setting and the IP allowlist check).
// Default of 1 matches Render and most single-CDN-hop platforms. Deploying
// behind a different proxy chain (e.g. a platform that adds more hops before
// reaching this app) may need a different value — if legitimate requests
// start getting 403'd, or IP allowlisting seems to trust the wrong address,
// check this first rather than assuming the allowlist itself is wrong.
export const TRUST_PROXY_HOPS = Number.isInteger(Number(process.env.TRUST_PROXY_HOPS))
? Number(process.env.TRUST_PROXY_HOPS)
: 1;