synartesis 0.8.7 → 0.9.0

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,13 +1,18 @@
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,
12
17
  loadManifest,
13
18
  mark,
@@ -20,12 +25,15 @@ import {
20
25
  runRead,
21
26
  toPayload,
22
27
  toolShapes,
28
+ trustsMarks,
23
29
  ungoverned,
24
30
  untested,
31
+ upstreamEnv,
25
32
  verifyAgainstServers,
26
33
  warnUntested,
27
- withIdempotencyKey
28
- } from "./chunk-JE7MOCZO.js";
34
+ withIdempotencyKey,
35
+ withoutMissingTools
36
+ } from "./chunk-AEEKBR5D.js";
29
37
  import {
30
38
  SnapshotError,
31
39
  UpstreamError,
@@ -147,6 +155,10 @@ async function serveHttp(options) {
147
155
  await writeResponse(res, await existing.transport.handleRequest(toRequest(req, raw, origin)));
148
156
  return;
149
157
  }
158
+ if (typeof sessionId === "string") {
159
+ refuse(res, 404, "that session has ended; start a new one with an initialize request");
160
+ return;
161
+ }
150
162
  const body = req.method === "POST" && raw.length > 0 ? JSON.parse(raw.toString("utf8")) : void 0;
151
163
  if (req.method !== "POST" || !isInitialize(body)) {
152
164
  refuse(res, 400, "no such session; start one with an initialize request");
@@ -209,14 +221,28 @@ async function serveHttp(options) {
209
221
  // src/gate/gate.ts
210
222
  var DEFAULT_GATE_TIMEOUT_MS = 3e5;
211
223
  var DEFAULT_HINT = (actionId) => `synartesis approve ${actionId.slice(0, 8)}`;
212
- function createRetryGate(journal, approveHint = DEFAULT_HINT) {
224
+ function createRetryGate(journal, approveHint = DEFAULT_HINT, notify = SILENT) {
213
225
  return {
214
226
  decide(request) {
227
+ const first = journal.getAction(request.actionId)?.status !== "gated";
215
228
  journal.markGated(request.actionId, request.why);
229
+ if (first) {
230
+ notify({
231
+ server: request.server,
232
+ tool: request.tool,
233
+ actionId: request.actionId,
234
+ approve: approveHint(request.actionId)
235
+ });
236
+ }
216
237
  return Promise.resolve({
217
238
  approved: false,
218
239
  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."
240
+ // Nothing here tells the agent how to approve it. This used to hand
241
+ // over the exact `synartesis approve` command to relay, and an agent
242
+ // with a shell -- Claude Code, Codex -- could simply run it, recorded
243
+ // as the person. The command goes to the person instead: in the
244
+ // notification, in `watch`, in the console and in `gates`.
245
+ 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
246
  });
221
247
  }
222
248
  };
@@ -285,9 +311,29 @@ function shouldGateOnWrite(args) {
285
311
 
286
312
  // src/proxy/proxy.ts
287
313
  var APPROVAL_WINDOW_MS = 60 * 60 * 1e3;
314
+ function saidNo(name, denial) {
315
+ return new McpError(
316
+ ErrorCode.InvalidRequest,
317
+ `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.`
318
+ );
319
+ }
320
+ function ago(at) {
321
+ const minutes = Math.max(0, Math.round((Date.now() - Date.parse(at)) / 6e4));
322
+ if (minutes === 0) {
323
+ return "just now";
324
+ }
325
+ return `${String(minutes)} minute${minutes === 1 ? "" : "s"} ago`;
326
+ }
288
327
  var PassthroughResult = z.looseObject({});
289
328
  var ToolList = z.looseObject({
290
- tools: z.array(z.looseObject({ name: z.string() })),
329
+ tools: z.array(
330
+ z.looseObject({
331
+ name: z.string(),
332
+ // Only the one mark this reads. Anything else a server says passes
333
+ // through untouched, and a malformed value reads as no mark at all.
334
+ annotations: z.looseObject({ readOnlyHint: z.unknown().optional() }).optional().catch(void 0)
335
+ })
336
+ ),
291
337
  nextCursor: z.string().optional()
292
338
  });
293
339
  var PromptList = z.looseObject({
@@ -360,10 +406,11 @@ var SYNARTESIS_INSTRUCTIONS = [
360
406
  "Some actions cannot be undone. Those are held until a person approves them, and the call",
361
407
  'will fail with a message beginning "Synartesis is holding this call for approval".',
362
408
  "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."
409
+ " 1. Tell the user plainly that the call is waiting for their approval, and what it is for.",
410
+ " They have been notified, and can see and answer it in `synartesis watch`.",
411
+ " 2. Once they say they have approved it, make the same call again. It will go through.",
412
+ "Do not approve it yourself, and do not try to work around a held call by using a",
413
+ "different tool to achieve the same thing."
367
414
  ].join("\n");
368
415
  function instructionsFor(router) {
369
416
  const sections = router.upstreams.map((upstream2) => ({
@@ -422,8 +469,18 @@ function createProxyServer(options) {
422
469
  const { upstreams, manifest, journal } = options;
423
470
  const router = createRouter(upstreams, manifest);
424
471
  const policies = createPolicyResolver(manifest);
472
+ const markedReadOnly = /* @__PURE__ */ new Set();
425
473
  const log = options.logger;
426
- const gate = options.gate ?? createRetryGate(journal, options.approveHint);
474
+ const trusting = (found, server2, tool) => {
475
+ if (found.matched || !trustsMarks(manifest, server2) || !markedReadOnly.has(qualify(server2, tool))) {
476
+ return found;
477
+ }
478
+ return {
479
+ policy: { match: qualify(server2, tool), class: "readonly", gate: "never", refusal: "uncertain" },
480
+ matched: false
481
+ };
482
+ };
483
+ const gate = options.gate ?? createRetryGate(journal, options.approveHint, options.notify);
427
484
  const capabilities = mergeCapabilities(
428
485
  upstreams.map((upstream) => upstream.client.getServerCapabilities() ?? {})
429
486
  );
@@ -576,6 +633,12 @@ function createProxyServer(options) {
576
633
  return { items: page.tools, nextCursor: page.nextCursor };
577
634
  });
578
635
  for (const tool of items) {
636
+ const qualified = qualify(upstream.name, tool.name);
637
+ if (tool.annotations?.readOnlyHint === true) {
638
+ markedReadOnly.add(qualified);
639
+ } else {
640
+ markedReadOnly.delete(qualified);
641
+ }
579
642
  tools.push({
580
643
  ...compatible(tool, (dialect) => {
581
644
  log?.warn(
@@ -602,11 +665,17 @@ function createProxyServer(options) {
602
665
  `no configured server provides tool ${request.params.name}`
603
666
  );
604
667
  }
605
- const { policy } = policies.resolve(qualify(route.upstream.name, route.tool));
668
+ const { policy, matched } = trusting(
669
+ policies.resolve(qualify(route.upstream.name, route.tool)),
670
+ route.upstream.name,
671
+ route.tool
672
+ );
606
673
  const args = request.params.arguments ?? {};
607
674
  enter();
608
675
  try {
609
- const wantsGate = policy.gate === "always" || policy.gate === "on_write" && shouldGateOnWrite(args);
676
+ const policyGates = policy.gate === "always" || policy.gate === "on_write" && shouldGateOnWrite(args);
677
+ const allowance = policyGates ? journal.findAllowance(route.upstream.name, route.tool, (/* @__PURE__ */ new Date()).toISOString()) : void 0;
678
+ const wantsGate = policyGates && allowance === void 0;
610
679
  const unresolved = policy.class === "readonly" ? void 0 : journal.findPending({
611
680
  runId: activeRun,
612
681
  server: route.upstream.name,
@@ -627,6 +696,19 @@ function createProxyServer(options) {
627
696
  tool: route.tool,
628
697
  args
629
698
  }) : void 0;
699
+ const refused = mustAsk && granted === void 0 && waiting === void 0 ? journal.findDenial({
700
+ server: route.upstream.name,
701
+ tool: route.tool,
702
+ args,
703
+ notBefore: new Date(Date.now() - APPROVAL_WINDOW_MS).toISOString()
704
+ }) : void 0;
705
+ if (refused !== void 0) {
706
+ log?.info(
707
+ { action: refused.action.id, by: refused.by },
708
+ "refused again: a person denied this exact call"
709
+ );
710
+ throw saidNo(request.params.name, refused);
711
+ }
630
712
  const reusable = waiting ?? (inherited === void 0 ? granted : void 0);
631
713
  const pending = reusable === void 0 ? journal.recordPending({
632
714
  runId: activeRun,
@@ -639,6 +721,9 @@ function createProxyServer(options) {
639
721
  seq: reusable.seq,
640
722
  idempotencyKey: reusable.idempotencyKey
641
723
  };
724
+ if (allowance !== void 0 && !mustAsk) {
725
+ journal.markAllowed(pending.actionId, `${allowance.by} (allowed without asking)`);
726
+ }
642
727
  let spent = true;
643
728
  if (inherited !== void 0) {
644
729
  spent = journal.adoptApproval(pending.actionId, inherited);
@@ -709,7 +794,12 @@ function createProxyServer(options) {
709
794
  if (mustAsk && (granted === void 0 || !spent)) {
710
795
  const because = [
711
796
  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
797
+ // Two different problems used to share this one sentence. A tool
798
+ // no rule mentions is held because nobody has said what it does,
799
+ // not because it is known to be permanent -- and the answer to
800
+ // that is a rule, which the person is told how to write, not a
801
+ // yes to every call it makes.
802
+ 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
803
  ].filter((one) => one !== void 0);
714
804
  await decide(because.join(", and "));
715
805
  }
@@ -764,13 +854,33 @@ function createProxyServer(options) {
764
854
  }
765
855
  const priorState = probe === void 0 || foundNothing ? { present: false } : { present: true, value: snapshot };
766
856
  if (noWayBack !== void 0 && !askedAlready) {
767
- const standing = journal.findApproval({
857
+ const allowedNow = journal.findAllowance(
858
+ route.upstream.name,
859
+ route.tool,
860
+ (/* @__PURE__ */ new Date()).toISOString()
861
+ );
862
+ const standing = allowedNow === void 0 ? journal.findApproval({
768
863
  server: route.upstream.name,
769
864
  tool: route.tool,
770
865
  args,
771
866
  notBefore: new Date(Date.now() - APPROVAL_WINDOW_MS).toISOString()
772
- });
773
- if (standing === void 0) {
867
+ }) : void 0;
868
+ const refusedHere = allowedNow === void 0 && standing === void 0 ? journal.findDenial({
869
+ server: route.upstream.name,
870
+ tool: route.tool,
871
+ args,
872
+ notBefore: new Date(Date.now() - APPROVAL_WINDOW_MS).toISOString()
873
+ }) : void 0;
874
+ if (allowedNow !== void 0) {
875
+ journal.markAllowed(pending.actionId, `${allowedNow.by} (allowed without asking)`);
876
+ } else if (refusedHere !== void 0) {
877
+ journal.settleAsDenied(
878
+ pending.actionId,
879
+ refusedHere.by,
880
+ `not sent: ${refusedHere.by} had denied this exact call`
881
+ );
882
+ throw saidNo(request.params.name, refusedHere);
883
+ } else if (standing === void 0) {
774
884
  await decide(noWayBack.asked);
775
885
  } else if (journal.adoptApproval(pending.actionId, standing)) {
776
886
  log?.info(
@@ -794,11 +904,11 @@ function createProxyServer(options) {
794
904
  signal: extra.signal
795
905
  });
796
906
  let inferred;
797
- const refused = refusal(result);
798
- if (refused !== void 0) {
907
+ const refused2 = refusal(result);
908
+ if (refused2 !== void 0) {
799
909
  const settled = policy.refusal === "clean" ? "none" : await whatHappened(router, probe, priorState, extra.signal);
800
910
  if (settled !== "applied") {
801
- const why = `the upstream refused the call: ${refused}`;
911
+ const why = `the upstream refused the call: ${refused2}`;
802
912
  if (settled === "none") {
803
913
  journal.markFailed(pending.actionId, why);
804
914
  } else {
@@ -808,14 +918,14 @@ function createProxyServer(options) {
808
918
  );
809
919
  }
810
920
  log?.debug(
811
- { seq: pending.seq, tool: route.tool, reason: refused, settled },
921
+ { seq: pending.seq, tool: route.tool, reason: refused2, settled },
812
922
  "refused by the upstream"
813
923
  );
814
924
  return result;
815
925
  }
816
- inferred = `the upstream answered with an error and the change was established by reading the resource back, so nothing confirmed it: ${refused}`;
926
+ inferred = `the upstream answered with an error and the change was established by reading the resource back, so nothing confirmed it: ${refused2}`;
817
927
  log?.warn(
818
- { seq: pending.seq, tool: route.tool, reason: refused },
928
+ { seq: pending.seq, tool: route.tool, reason: refused2 },
819
929
  "the upstream reported an error after changing the resource"
820
930
  );
821
931
  }
@@ -866,8 +976,9 @@ function createProxyServer(options) {
866
976
  });
867
977
  return result;
868
978
  } catch (error) {
869
- const disconnected = isDisconnected(error);
870
- if (neverDispatched(error)) {
979
+ const kind = route.upstream.classify?.(error);
980
+ const disconnected = isDisconnected(error) || kind === "lost";
981
+ if (neverDispatched(error) || kind !== void 0) {
871
982
  journal.markFailed(pending.actionId, describe(error));
872
983
  } else {
873
984
  const settled = await whatHappened(router, probe, priorState, extra.signal);
@@ -1188,29 +1299,39 @@ async function main() {
1188
1299
  `--server ${String(argv.server)} is not declared in ${argv.manifest}; it has: ${declared.map(([name]) => name).join(", ")}`
1189
1300
  );
1190
1301
  }
1302
+ const source = argv.server === void 0 ? { kind: "manifest" } : { kind: "inherit" };
1191
1303
  const upstreams = [];
1304
+ const key = journal.fingerprintKey();
1305
+ const startedWith = /* @__PURE__ */ new Map();
1192
1306
  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
- );
1307
+ upstreams.push(await connectUpstream(name, spec, { env: source }));
1308
+ let client;
1309
+ try {
1310
+ client = source.kind === "inherit" ? clientEnvFor(argv.manifest, name)?.env : void 0;
1311
+ } catch {
1312
+ client = void 0;
1313
+ }
1314
+ startedWith.set(name, {
1315
+ cwd: process.cwd(),
1316
+ fingerprints: fingerprint(key, upstreamEnv(name, spec, source), declaredNames(spec, client))
1317
+ });
1318
+ }
1319
+ const { manifest: served, disabled } = await withoutMissingTools(upstreams, manifest);
1320
+ for (const line of disabled) {
1321
+ log.warn(line);
1201
1322
  }
1202
1323
  await verifyAgainstServers(
1203
1324
  upstreams,
1204
- argv.server === void 0 ? manifest : {
1205
- ...manifest,
1206
- tools: manifest.tools.filter((rule) => rule.match.startsWith(`${String(argv.server)}.`))
1325
+ argv.server === void 0 ? served : {
1326
+ ...served,
1327
+ tools: served.tools.filter((rule) => rule.match.startsWith(`${String(argv.server)}.`))
1207
1328
  }
1208
1329
  );
1209
1330
  const uncovered = ungoverned(
1210
- manifest,
1331
+ served,
1211
1332
  new Map(await Promise.all(upstreams.map(async (upstream) => [
1212
1333
  upstream.name,
1213
- (await toolShapes(upstream)).map((tool) => tool.name)
1334
+ (await toolShapes(upstream)).filter((tool) => !(tool.readOnly === true && trustsMarks(served, upstream.name))).map((tool) => tool.name)
1214
1335
  ])))
1215
1336
  );
1216
1337
  for (const entry of uncovered) {
@@ -1224,21 +1345,33 @@ async function main() {
1224
1345
  manifest: argv.manifest,
1225
1346
  journal: argv.journal,
1226
1347
  servers: upstreams.map((upstream) => upstream.name),
1227
- policies: manifest.tools.length,
1348
+ policies: served.tools.length,
1349
+ held: disabled.length,
1228
1350
  ungoverned: uncovered.reduce((sum, entry) => sum + entry.tools.length, 0)
1229
1351
  },
1230
1352
  "proxy ready"
1231
1353
  );
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
- });
1354
+ const build = () => {
1355
+ const proxy2 = createProxyServer({
1356
+ upstreams,
1357
+ manifest: served,
1358
+ journal,
1359
+ logger: log,
1360
+ // Absolute, because whoever approves may be in any directory at all.
1361
+ approveHint: (actionId) => `${cliCommandFrom(import.meta.url)} approve ${actionId.slice(0, 8)} --journal ${resolve(argv.journal)}`,
1362
+ notify: desktopNotifier()
1363
+ });
1364
+ proxy2.ready.then((runId) => {
1365
+ for (const [server, started] of startedWith) {
1366
+ journal.recordRunServer(runId, server, started.cwd, started.fingerprints);
1367
+ }
1368
+ }).catch((error) => {
1369
+ log.warn({ error: describe(error) }, "could not record what this session's servers were started with");
1370
+ });
1371
+ return proxy2;
1372
+ };
1240
1373
  if (argv.http !== void 0) {
1241
- const served = await serveHttp({
1374
+ const served2 = await serveHttp({
1242
1375
  ...argv.http,
1243
1376
  create: build,
1244
1377
  log: {
@@ -1252,7 +1385,7 @@ async function main() {
1252
1385
  });
1253
1386
  const stop = () => {
1254
1387
  void (async () => {
1255
- await served.close();
1388
+ await served2.close();
1256
1389
  for (const upstream of upstreams) {
1257
1390
  await upstream.close();
1258
1391
  }
@@ -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