synartesis 0.8.8 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/proxy.js CHANGED
@@ -1,14 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  PROXY_FLAGS,
4
+ SILENT,
4
5
  canonical,
5
6
  cliCommandFrom,
6
- connectStdioUpstream,
7
+ clientEnvFor,
8
+ connectUpstream,
7
9
  createPolicyResolver,
8
10
  createRouter,
11
+ declaredNames,
12
+ desktopNotifier,
9
13
  findJournal,
10
14
  findManifest,
15
+ fingerprint2 as fingerprint,
11
16
  isDisconnected,
17
+ listAll,
12
18
  loadManifest,
13
19
  mark,
14
20
  observeState,
@@ -18,14 +24,17 @@ import {
18
24
  qualify,
19
25
  refusal,
20
26
  runRead,
27
+ startAll,
21
28
  toPayload,
22
- toolShapes,
29
+ trustsMarks,
23
30
  ungoverned,
24
31
  untested,
32
+ upstreamEnv,
25
33
  verifyAgainstServers,
26
34
  warnUntested,
27
- withIdempotencyKey
28
- } from "./chunk-JE7MOCZO.js";
35
+ withIdempotencyKey,
36
+ withoutMissingTools
37
+ } from "./chunk-H4SPMXQW.js";
29
38
  import {
30
39
  SnapshotError,
31
40
  UpstreamError,
@@ -147,6 +156,10 @@ async function serveHttp(options) {
147
156
  await writeResponse(res, await existing.transport.handleRequest(toRequest(req, raw, origin)));
148
157
  return;
149
158
  }
159
+ if (typeof sessionId === "string") {
160
+ refuse(res, 404, "that session has ended; start a new one with an initialize request");
161
+ return;
162
+ }
150
163
  const body = req.method === "POST" && raw.length > 0 ? JSON.parse(raw.toString("utf8")) : void 0;
151
164
  if (req.method !== "POST" || !isInitialize(body)) {
152
165
  refuse(res, 400, "no such session; start one with an initialize request");
@@ -209,14 +222,28 @@ async function serveHttp(options) {
209
222
  // src/gate/gate.ts
210
223
  var DEFAULT_GATE_TIMEOUT_MS = 3e5;
211
224
  var DEFAULT_HINT = (actionId) => `synartesis approve ${actionId.slice(0, 8)}`;
212
- function createRetryGate(journal, approveHint = DEFAULT_HINT) {
225
+ function createRetryGate(journal, approveHint = DEFAULT_HINT, notify = SILENT) {
213
226
  return {
214
227
  decide(request) {
228
+ const first = journal.getAction(request.actionId)?.status !== "gated";
215
229
  journal.markGated(request.actionId, request.why);
230
+ if (first) {
231
+ notify({
232
+ server: request.server,
233
+ tool: request.tool,
234
+ actionId: request.actionId,
235
+ approve: approveHint(request.actionId)
236
+ });
237
+ }
216
238
  return Promise.resolve({
217
239
  approved: false,
218
240
  awaiting: true,
219
- reason: "it is waiting for a person to approve it. Ask them to run: " + approveHint(request.actionId) + " --- then make this exact call again."
241
+ // Nothing here tells the agent how to approve it. This used to hand
242
+ // over the exact `synartesis approve` command to relay, and an agent
243
+ // with a shell -- Claude Code, Codex -- could simply run it, recorded
244
+ // as the person. The command goes to the person instead: in the
245
+ // notification, in `watch`, in the console and in `gates`.
246
+ reason: "It is waiting for a person to decide, and they have been told. Let the user know it is waiting, then make this exact call again once they say it is approved."
220
247
  });
221
248
  }
222
249
  };
@@ -285,9 +312,29 @@ function shouldGateOnWrite(args) {
285
312
 
286
313
  // src/proxy/proxy.ts
287
314
  var APPROVAL_WINDOW_MS = 60 * 60 * 1e3;
315
+ function saidNo(name, denial) {
316
+ return new McpError(
317
+ ErrorCode.InvalidRequest,
318
+ `synartesis blocked ${name}: ${denial.by} denied this exact call ${ago(denial.at)} (${denial.reason}). Ask the user what they want instead; do not retry it or reach the same result another way.`
319
+ );
320
+ }
321
+ function ago(at) {
322
+ const minutes = Math.max(0, Math.round((Date.now() - Date.parse(at)) / 6e4));
323
+ if (minutes === 0) {
324
+ return "just now";
325
+ }
326
+ return `${String(minutes)} minute${minutes === 1 ? "" : "s"} ago`;
327
+ }
288
328
  var PassthroughResult = z.looseObject({});
289
329
  var ToolList = z.looseObject({
290
- tools: z.array(z.looseObject({ name: z.string() })),
330
+ tools: z.array(
331
+ z.looseObject({
332
+ name: z.string(),
333
+ // Only the one mark this reads. Anything else a server says passes
334
+ // through untouched, and a malformed value reads as no mark at all.
335
+ annotations: z.looseObject({ readOnlyHint: z.unknown().optional() }).optional().catch(void 0)
336
+ })
337
+ ),
291
338
  nextCursor: z.string().optional()
292
339
  });
293
340
  var PromptList = z.looseObject({
@@ -360,10 +407,11 @@ var SYNARTESIS_INSTRUCTIONS = [
360
407
  "Some actions cannot be undone. Those are held until a person approves them, and the call",
361
408
  'will fail with a message beginning "Synartesis is holding this call for approval".',
362
409
  "When that happens:",
363
- " 1. Tell the user plainly that you are asking Synartesis for approval, and what for.",
364
- " 2. Give them the exact `synartesis approve ...` command from the error.",
365
- " 3. Once they say they have approved it, make the same call again. It will go through.",
366
- "Do not try to work around a held call by using a different tool to achieve the same thing."
410
+ " 1. Tell the user plainly that the call is waiting for their approval, and what it is for.",
411
+ " They have been notified, and can see and answer it in `synartesis watch`.",
412
+ " 2. Once they say they have approved it, make the same call again. It will go through.",
413
+ "Do not approve it yourself, and do not try to work around a held call by using a",
414
+ "different tool to achieve the same thing."
367
415
  ].join("\n");
368
416
  function instructionsFor(router) {
369
417
  const sections = router.upstreams.map((upstream2) => ({
@@ -422,8 +470,18 @@ function createProxyServer(options) {
422
470
  const { upstreams, manifest, journal } = options;
423
471
  const router = createRouter(upstreams, manifest);
424
472
  const policies = createPolicyResolver(manifest);
473
+ const markedReadOnly = /* @__PURE__ */ new Set();
425
474
  const log = options.logger;
426
- const gate = options.gate ?? createRetryGate(journal, options.approveHint);
475
+ const trusting = (found, server2, tool) => {
476
+ if (found.matched || !trustsMarks(manifest, server2) || !markedReadOnly.has(qualify(server2, tool))) {
477
+ return found;
478
+ }
479
+ return {
480
+ policy: { match: qualify(server2, tool), class: "readonly", gate: "never", refusal: "uncertain" },
481
+ matched: false
482
+ };
483
+ };
484
+ const gate = options.gate ?? createRetryGate(journal, options.approveHint, options.notify);
427
485
  const capabilities = mergeCapabilities(
428
486
  upstreams.map((upstream) => upstream.client.getServerCapabilities() ?? {})
429
487
  );
@@ -576,6 +634,12 @@ function createProxyServer(options) {
576
634
  return { items: page.tools, nextCursor: page.nextCursor };
577
635
  });
578
636
  for (const tool of items) {
637
+ const qualified = qualify(upstream.name, tool.name);
638
+ if (tool.annotations?.readOnlyHint === true) {
639
+ markedReadOnly.add(qualified);
640
+ } else {
641
+ markedReadOnly.delete(qualified);
642
+ }
579
643
  tools.push({
580
644
  ...compatible(tool, (dialect) => {
581
645
  log?.warn(
@@ -602,11 +666,17 @@ function createProxyServer(options) {
602
666
  `no configured server provides tool ${request.params.name}`
603
667
  );
604
668
  }
605
- const { policy } = policies.resolve(qualify(route.upstream.name, route.tool));
669
+ const { policy, matched } = trusting(
670
+ policies.resolve(qualify(route.upstream.name, route.tool)),
671
+ route.upstream.name,
672
+ route.tool
673
+ );
606
674
  const args = request.params.arguments ?? {};
607
675
  enter();
608
676
  try {
609
- const wantsGate = policy.gate === "always" || policy.gate === "on_write" && shouldGateOnWrite(args);
677
+ const policyGates = policy.gate === "always" || policy.gate === "on_write" && shouldGateOnWrite(args);
678
+ const allowance = policyGates ? journal.findAllowance(route.upstream.name, route.tool, (/* @__PURE__ */ new Date()).toISOString()) : void 0;
679
+ const wantsGate = policyGates && allowance === void 0;
610
680
  const unresolved = policy.class === "readonly" ? void 0 : journal.findPending({
611
681
  runId: activeRun,
612
682
  server: route.upstream.name,
@@ -627,6 +697,19 @@ function createProxyServer(options) {
627
697
  tool: route.tool,
628
698
  args
629
699
  }) : void 0;
700
+ const refused = mustAsk && granted === void 0 && waiting === void 0 ? journal.findDenial({
701
+ server: route.upstream.name,
702
+ tool: route.tool,
703
+ args,
704
+ notBefore: new Date(Date.now() - APPROVAL_WINDOW_MS).toISOString()
705
+ }) : void 0;
706
+ if (refused !== void 0) {
707
+ log?.info(
708
+ { action: refused.action.id, by: refused.by },
709
+ "refused again: a person denied this exact call"
710
+ );
711
+ throw saidNo(request.params.name, refused);
712
+ }
630
713
  const reusable = waiting ?? (inherited === void 0 ? granted : void 0);
631
714
  const pending = reusable === void 0 ? journal.recordPending({
632
715
  runId: activeRun,
@@ -639,6 +722,9 @@ function createProxyServer(options) {
639
722
  seq: reusable.seq,
640
723
  idempotencyKey: reusable.idempotencyKey
641
724
  };
725
+ if (allowance !== void 0 && !mustAsk) {
726
+ journal.markAllowed(pending.actionId, `${allowance.by} (allowed without asking)`);
727
+ }
642
728
  let spent = true;
643
729
  if (inherited !== void 0) {
644
730
  spent = journal.adoptApproval(pending.actionId, inherited);
@@ -709,7 +795,12 @@ function createProxyServer(options) {
709
795
  if (mustAsk && (granted === void 0 || !spent)) {
710
796
  const because = [
711
797
  unresolved === void 0 ? void 0 : `an earlier attempt at this exact call, action ${String(unresolved.seq)}, went out and its outcome could never be established, so sending it again may apply the change a second time`,
712
- wantsGate ? "this action cannot be undone" : void 0
798
+ // Two different problems used to share this one sentence. A tool
799
+ // no rule mentions is held because nobody has said what it does,
800
+ // not because it is known to be permanent -- and the answer to
801
+ // that is a rule, which the person is told how to write, not a
802
+ // yes to every call it makes.
803
+ wantsGate ? matched ? "this action cannot be undone" : `there is no rule for ${route.upstream.name}.${route.tool} in the policy, so nothing says whether it can be undone` : void 0
713
804
  ].filter((one) => one !== void 0);
714
805
  await decide(because.join(", and "));
715
806
  }
@@ -764,13 +855,33 @@ function createProxyServer(options) {
764
855
  }
765
856
  const priorState = probe === void 0 || foundNothing ? { present: false } : { present: true, value: snapshot };
766
857
  if (noWayBack !== void 0 && !askedAlready) {
767
- const standing = journal.findApproval({
858
+ const allowedNow = journal.findAllowance(
859
+ route.upstream.name,
860
+ route.tool,
861
+ (/* @__PURE__ */ new Date()).toISOString()
862
+ );
863
+ const standing = allowedNow === void 0 ? journal.findApproval({
768
864
  server: route.upstream.name,
769
865
  tool: route.tool,
770
866
  args,
771
867
  notBefore: new Date(Date.now() - APPROVAL_WINDOW_MS).toISOString()
772
- });
773
- if (standing === void 0) {
868
+ }) : void 0;
869
+ const refusedHere = allowedNow === void 0 && standing === void 0 ? journal.findDenial({
870
+ server: route.upstream.name,
871
+ tool: route.tool,
872
+ args,
873
+ notBefore: new Date(Date.now() - APPROVAL_WINDOW_MS).toISOString()
874
+ }) : void 0;
875
+ if (allowedNow !== void 0) {
876
+ journal.markAllowed(pending.actionId, `${allowedNow.by} (allowed without asking)`);
877
+ } else if (refusedHere !== void 0) {
878
+ journal.settleAsDenied(
879
+ pending.actionId,
880
+ refusedHere.by,
881
+ `not sent: ${refusedHere.by} had denied this exact call`
882
+ );
883
+ throw saidNo(request.params.name, refusedHere);
884
+ } else if (standing === void 0) {
774
885
  await decide(noWayBack.asked);
775
886
  } else if (journal.adoptApproval(pending.actionId, standing)) {
776
887
  log?.info(
@@ -794,11 +905,11 @@ function createProxyServer(options) {
794
905
  signal: extra.signal
795
906
  });
796
907
  let inferred;
797
- const refused = refusal(result);
798
- if (refused !== void 0) {
908
+ const refused2 = refusal(result);
909
+ if (refused2 !== void 0) {
799
910
  const settled = policy.refusal === "clean" ? "none" : await whatHappened(router, probe, priorState, extra.signal);
800
911
  if (settled !== "applied") {
801
- const why = `the upstream refused the call: ${refused}`;
912
+ const why = `the upstream refused the call: ${refused2}`;
802
913
  if (settled === "none") {
803
914
  journal.markFailed(pending.actionId, why);
804
915
  } else {
@@ -808,14 +919,14 @@ function createProxyServer(options) {
808
919
  );
809
920
  }
810
921
  log?.debug(
811
- { seq: pending.seq, tool: route.tool, reason: refused, settled },
922
+ { seq: pending.seq, tool: route.tool, reason: refused2, settled },
812
923
  "refused by the upstream"
813
924
  );
814
925
  return result;
815
926
  }
816
- inferred = `the upstream answered with an error and the change was established by reading the resource back, so nothing confirmed it: ${refused}`;
927
+ inferred = `the upstream answered with an error and the change was established by reading the resource back, so nothing confirmed it: ${refused2}`;
817
928
  log?.warn(
818
- { seq: pending.seq, tool: route.tool, reason: refused },
929
+ { seq: pending.seq, tool: route.tool, reason: refused2 },
819
930
  "the upstream reported an error after changing the resource"
820
931
  );
821
932
  }
@@ -866,8 +977,9 @@ function createProxyServer(options) {
866
977
  });
867
978
  return result;
868
979
  } catch (error) {
869
- const disconnected = isDisconnected(error);
870
- if (neverDispatched(error)) {
980
+ const kind = route.upstream.classify?.(error);
981
+ const disconnected = isDisconnected(error) || kind === "lost";
982
+ if (neverDispatched(error) || kind !== void 0) {
871
983
  journal.markFailed(pending.actionId, describe(error));
872
984
  } else {
873
985
  const settled = await whatHappened(router, probe, priorState, extra.signal);
@@ -1188,30 +1300,42 @@ async function main() {
1188
1300
  `--server ${String(argv.server)} is not declared in ${argv.manifest}; it has: ${declared.map(([name]) => name).join(", ")}`
1189
1301
  );
1190
1302
  }
1303
+ const source = argv.server === void 0 ? { kind: "manifest" } : { kind: "inherit" };
1191
1304
  const upstreams = [];
1305
+ const key = journal.fingerprintKey();
1306
+ const startedWith = /* @__PURE__ */ new Map();
1307
+ upstreams.push(...await startAll([...wanted], ([name, spec]) => connectUpstream(name, spec, { env: source })));
1192
1308
  for (const [name, spec] of wanted) {
1193
- upstreams.push(
1194
- await connectStdioUpstream({
1195
- name,
1196
- command: spec.command,
1197
- args: spec.args,
1198
- ...spec.env === void 0 ? {} : { env: spec.env }
1199
- })
1200
- );
1309
+ let client;
1310
+ try {
1311
+ client = source.kind === "inherit" ? clientEnvFor(argv.manifest, name)?.env : void 0;
1312
+ } catch {
1313
+ client = void 0;
1314
+ }
1315
+ startedWith.set(name, {
1316
+ cwd: process.cwd(),
1317
+ fingerprints: fingerprint(key, upstreamEnv(name, spec, source), declaredNames(spec, client))
1318
+ });
1319
+ }
1320
+ const listed = await listAll(upstreams);
1321
+ const { manifest: served, disabled } = await withoutMissingTools(upstreams, manifest, listed);
1322
+ for (const line of disabled) {
1323
+ log.warn(line);
1201
1324
  }
1202
1325
  await verifyAgainstServers(
1203
1326
  upstreams,
1204
- argv.server === void 0 ? manifest : {
1205
- ...manifest,
1206
- tools: manifest.tools.filter((rule) => rule.match.startsWith(`${String(argv.server)}.`))
1207
- }
1327
+ argv.server === void 0 ? served : {
1328
+ ...served,
1329
+ tools: served.tools.filter((rule) => rule.match.startsWith(`${String(argv.server)}.`))
1330
+ },
1331
+ listed
1208
1332
  );
1209
1333
  const uncovered = ungoverned(
1210
- manifest,
1211
- new Map(await Promise.all(upstreams.map(async (upstream) => [
1334
+ served,
1335
+ new Map(upstreams.map((upstream) => [
1212
1336
  upstream.name,
1213
- (await toolShapes(upstream)).map((tool) => tool.name)
1214
- ])))
1337
+ (listed.get(upstream.name) ?? []).filter((tool) => !(tool.readOnly === true && trustsMarks(served, upstream.name))).map((tool) => tool.name)
1338
+ ]))
1215
1339
  );
1216
1340
  for (const entry of uncovered) {
1217
1341
  log.warn(
@@ -1224,21 +1348,33 @@ async function main() {
1224
1348
  manifest: argv.manifest,
1225
1349
  journal: argv.journal,
1226
1350
  servers: upstreams.map((upstream) => upstream.name),
1227
- policies: manifest.tools.length,
1351
+ policies: served.tools.length,
1352
+ held: disabled.length,
1228
1353
  ungoverned: uncovered.reduce((sum, entry) => sum + entry.tools.length, 0)
1229
1354
  },
1230
1355
  "proxy ready"
1231
1356
  );
1232
- const build = () => createProxyServer({
1233
- upstreams,
1234
- manifest,
1235
- journal,
1236
- logger: log,
1237
- // Absolute, because whoever approves may be in any directory at all.
1238
- approveHint: (actionId) => `${cliCommandFrom(import.meta.url)} approve ${actionId.slice(0, 8)} --journal ${resolve(argv.journal)}`
1239
- });
1357
+ const build = () => {
1358
+ const proxy2 = createProxyServer({
1359
+ upstreams,
1360
+ manifest: served,
1361
+ journal,
1362
+ logger: log,
1363
+ // Absolute, because whoever approves may be in any directory at all.
1364
+ approveHint: (actionId) => `${cliCommandFrom(import.meta.url)} approve ${actionId.slice(0, 8)} --journal ${resolve(argv.journal)}`,
1365
+ notify: desktopNotifier()
1366
+ });
1367
+ proxy2.ready.then((runId) => {
1368
+ for (const [server, started] of startedWith) {
1369
+ journal.recordRunServer(runId, server, started.cwd, started.fingerprints);
1370
+ }
1371
+ }).catch((error) => {
1372
+ log.warn({ error: describe(error) }, "could not record what this session's servers were started with");
1373
+ });
1374
+ return proxy2;
1375
+ };
1240
1376
  if (argv.http !== void 0) {
1241
- const served = await serveHttp({
1377
+ const served2 = await serveHttp({
1242
1378
  ...argv.http,
1243
1379
  create: build,
1244
1380
  log: {
@@ -1252,7 +1388,7 @@ async function main() {
1252
1388
  });
1253
1389
  const stop = () => {
1254
1390
  void (async () => {
1255
- await served.close();
1391
+ await served2.close();
1256
1392
  for (const upstream of upstreams) {
1257
1393
  await upstream.close();
1258
1394
  }
@@ -0,0 +1,26 @@
1
+ # Policy for awslabs.aws-documentation-mcp-server.
2
+ #
3
+ # Written against the build current in September 2026 from the server's own tools/list.
4
+ #
5
+ # Reads AWS's public documentation. Nothing here touches an AWS account.
6
+ #
7
+ # A tool a later version adds is held until a rule says what it does.
8
+ version: 1
9
+
10
+ servers:
11
+ aws-docs:
12
+ command: uvx
13
+ args: ["awslabs.aws-documentation-mcp-server@latest"]
14
+ provenance: live
15
+
16
+ tools:
17
+ - match: "aws-docs.read_documentation"
18
+ class: readonly
19
+ - match: "aws-docs.read_sections"
20
+ class: readonly
21
+ - match: "aws-docs.search_table"
22
+ class: readonly
23
+ - match: "aws-docs.search_documentation"
24
+ class: readonly
25
+ - match: "aws-docs.recommend"
26
+ class: readonly
@@ -0,0 +1,33 @@
1
+ # Policy for @brave/brave-search-mcp-server.
2
+ #
3
+ # Written against 2.1.4 from the server's own tools/list.
4
+ #
5
+ # Every tool searches. The server does not mark them read-only, which is why
6
+ # each search was held before this file existed.
7
+ #
8
+ # A tool a later version adds is held until a rule says what it does.
9
+ version: 1
10
+
11
+ servers:
12
+ brave:
13
+ command: npx
14
+ args: ["-y", "@brave/brave-search-mcp-server"]
15
+ provenance: live
16
+
17
+ tools:
18
+ - match: "brave.brave_web_search"
19
+ class: readonly
20
+ - match: "brave.brave_local_search"
21
+ class: readonly
22
+ - match: "brave.brave_video_search"
23
+ class: readonly
24
+ - match: "brave.brave_image_search"
25
+ class: readonly
26
+ - match: "brave.brave_news_search"
27
+ class: readonly
28
+ - match: "brave.brave_summarizer"
29
+ class: readonly
30
+ - match: "brave.brave_llm_context"
31
+ class: readonly
32
+ - match: "brave.brave_place_search"
33
+ class: readonly
@@ -0,0 +1,115 @@
1
+ # Policy for chrome-devtools-mcp.
2
+ #
3
+ # Written against 1.10.1 from the server's own tools/list, read-only hints and
4
+ # all.
5
+ #
6
+ # What playwright.yaml says holds here too. Nothing a browser does can be
7
+ # undone, and holding every click made the server unusable, so interactions
8
+ # are recorded rather than held: each is classed as a call that cannot be
9
+ # undone, and written to the journal with its arguments.
10
+ #
11
+ # Two tools are still held. Running a script the agent wrote has the reach of
12
+ # the page and everything it is signed in to, and an upload sends a file from
13
+ # your disk to whoever runs the page. A tool this file does not mention is held
14
+ # until a rule says otherwise.
15
+ #
16
+ # Only the tools the server marks read-only are classed as reads. The ones it
17
+ # does not include screenshots, snapshots, traces and audits, and several of
18
+ # those can write a file to disk when given a path. They are recorded, not
19
+ # held.
20
+ version: 1
21
+
22
+ servers:
23
+ chrome-devtools:
24
+ command: npx
25
+ args: ["-y", "chrome-devtools-mcp@latest"]
26
+ provenance: live
27
+
28
+ tools:
29
+ # --- reads: the tools the server itself marks read-only ------------------
30
+ - match: "chrome-devtools.list_pages"
31
+ class: readonly
32
+ - match: "chrome-devtools.select_page"
33
+ class: readonly
34
+ - match: "chrome-devtools.list_console_messages"
35
+ class: readonly
36
+ - match: "chrome-devtools.get_console_message"
37
+ class: readonly
38
+ - match: "chrome-devtools.list_network_requests"
39
+ class: readonly
40
+ - match: "chrome-devtools.get_css_styles"
41
+ class: readonly
42
+ - match: "chrome-devtools.performance_analyze_insight"
43
+ class: readonly
44
+ - match: "chrome-devtools.wait_for"
45
+ class: readonly
46
+
47
+ # --- held: each reaches past the page -------------------------------------
48
+ - match: "chrome-devtools.evaluate_script"
49
+ class: irreversible
50
+ gate: always
51
+ - match: "chrome-devtools.upload_file"
52
+ class: irreversible
53
+ gate: always
54
+
55
+ # --- recorded, not held ----------------------------------------------------
56
+ - match: "chrome-devtools.navigate_page"
57
+ class: irreversible
58
+ gate: never
59
+ - match: "chrome-devtools.new_page"
60
+ class: irreversible
61
+ gate: never
62
+ - match: "chrome-devtools.close_page"
63
+ class: irreversible
64
+ gate: never
65
+ - match: "chrome-devtools.click"
66
+ class: irreversible
67
+ gate: never
68
+ - match: "chrome-devtools.fill"
69
+ class: irreversible
70
+ gate: never
71
+ - match: "chrome-devtools.fill_form"
72
+ class: irreversible
73
+ gate: never
74
+ - match: "chrome-devtools.type_text"
75
+ class: irreversible
76
+ gate: never
77
+ - match: "chrome-devtools.press_key"
78
+ class: irreversible
79
+ gate: never
80
+ - match: "chrome-devtools.hover"
81
+ class: irreversible
82
+ gate: never
83
+ - match: "chrome-devtools.drag"
84
+ class: irreversible
85
+ gate: never
86
+ - match: "chrome-devtools.handle_dialog"
87
+ class: irreversible
88
+ gate: never
89
+ - match: "chrome-devtools.emulate"
90
+ class: irreversible
91
+ gate: never
92
+ - match: "chrome-devtools.resize_page"
93
+ class: irreversible
94
+ gate: never
95
+ - match: "chrome-devtools.take_screenshot"
96
+ class: irreversible
97
+ gate: never
98
+ - match: "chrome-devtools.take_snapshot"
99
+ class: irreversible
100
+ gate: never
101
+ - match: "chrome-devtools.take_heapsnapshot"
102
+ class: irreversible
103
+ gate: never
104
+ - match: "chrome-devtools.get_network_request"
105
+ class: irreversible
106
+ gate: never
107
+ - match: "chrome-devtools.performance_start_trace"
108
+ class: irreversible
109
+ gate: never
110
+ - match: "chrome-devtools.performance_stop_trace"
111
+ class: irreversible
112
+ gate: never
113
+ - match: "chrome-devtools.lighthouse_audit"
114
+ class: irreversible
115
+ gate: never
@@ -0,0 +1,20 @@
1
+ # Policy for exa-mcp-server.
2
+ #
3
+ # Written against 3.4.1 from the server's own tools/list.
4
+ #
5
+ # Both tools are marked read-only by the server itself.
6
+ #
7
+ # A tool a later version adds is held until a rule says what it does.
8
+ version: 1
9
+
10
+ servers:
11
+ exa:
12
+ command: npx
13
+ args: ["-y", "exa-mcp-server"]
14
+ provenance: live
15
+
16
+ tools:
17
+ - match: "exa.web_search_exa"
18
+ class: readonly
19
+ - match: "exa.web_fetch_exa"
20
+ class: readonly
@@ -0,0 +1,20 @@
1
+ # Policy for mcp-server-fetch.
2
+ #
3
+ # Written against 1.30.0 from the server's own tools/list.
4
+ #
5
+ # It fetches a URL and hands back what is there. A GET can still reach
6
+ # something that changes state on being read -- that is the page's doing, and
7
+ # no policy here can see it.
8
+ #
9
+ # A tool a later version adds is held until a rule says what it does.
10
+ version: 1
11
+
12
+ servers:
13
+ fetch:
14
+ command: uvx
15
+ args: ["mcp-server-fetch"]
16
+ provenance: live
17
+
18
+ tools:
19
+ - match: "fetch.fetch"
20
+ class: readonly