@yawlabs/tailscale-mcp 0.19.0 → 0.19.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.
Files changed (2) hide show
  1. package/dist/index.js +24 -11
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -31481,12 +31481,17 @@ function filterTools(groups, options) {
31481
31481
  // src/tools/acl.ts
31482
31482
  var DEFAULT_PRINCIPAL_CAP = 25;
31483
31483
  var DIFF_TIME_BUDGET_MS = 6e4;
31484
- function accessSet(matches) {
31484
+ function accessSet(matches, postureDefs, definitionsAvailable) {
31485
31485
  const out = /* @__PURE__ */ new Set();
31486
31486
  for (const match of matches) {
31487
31487
  const via = [...match.via ?? []].sort();
31488
31488
  const postures = [...match.postures ?? []].sort();
31489
- const qualifier = `${via.length > 0 ? ` via ${via.join(",")}` : ""}${postures.length > 0 ? ` posture ${postures.join(",")}` : ""}`;
31489
+ const posturePart = postures.length > 0 ? ` posture ${postures.map((name) => {
31490
+ if (!definitionsAvailable) return name;
31491
+ const rules = [...postureDefs?.[name] ?? []].sort();
31492
+ return rules.length > 0 ? `${name}(${rules.join(";")})` : name;
31493
+ }).join(",")}` : "";
31494
+ const qualifier = `${via.length > 0 ? ` via ${via.join(",")}` : ""}${posturePart}`;
31490
31495
  for (const port of match.ports ?? []) out.add(`${port}${qualifier}`);
31491
31496
  }
31492
31497
  return out;
@@ -31517,7 +31522,13 @@ async function previewAccess(policy, principal) {
31517
31522
  status: res.status
31518
31523
  };
31519
31524
  }
31520
- return { ok: true, access: accessSet(parsed.matches) };
31525
+ return {
31526
+ ok: true,
31527
+ access: accessSet(parsed.matches, parsed.postures, false),
31528
+ hasPostureDefs: !!parsed.postures,
31529
+ matches: parsed.matches,
31530
+ postureDefs: parsed.postures
31531
+ };
31521
31532
  }
31522
31533
  var ETAG_FOOTER_MARKER = "// ETag: ";
31523
31534
  function stripEtagFooter(body) {
@@ -31654,7 +31665,7 @@ var aclTools = [
31654
31665
  },
31655
31666
  {
31656
31667
  name: "tailscale_diff_acl_access",
31657
- description: "Answer 'who loses access?' before applying an ACL change. Compares the CURRENT policy against a proposed one and reports, per user, which destinations they gain and lose. Run this before tailscale_update_acl -- validate_acl only checks syntax and the policy's own tests block, so a policy with no tests validates clean while revoking everyone. LIMITS, all reported in the response rather than left to be discovered. It compares USER principals only, so a revocation that runs through a tag or group can show a clean diff; it does not detect a change to a posture DEFINITION, because the comparison keys on posture names; and a narrowed port list shows as a paired loss and gain of the whole entry rather than a clean loss. An empty result is never proof a change is safe. It costs two preview requests per user, so it checks the first 25 by default and stops after 60 seconds regardless; either way it sets `truncated`, reports how many were skipped, and says which limit stopped it. Users whose preview fails are listed in `failed` and excluded from the compared count -- a failure is never reported as lost access, and if nothing could be compared the call fails rather than returning an empty diff.",
31668
+ description: "Answer 'who loses access?' before applying an ACL change. Compares the CURRENT policy against a proposed one and reports, per user, which destinations they gain and lose. Run this before tailscale_update_acl -- validate_acl only checks syntax and the policy's own tests block, so a policy with no tests validates clean while revoking everyone. LIMITS, all reported in the response rather than left to be discovered. It compares USER principals only, so a revocation that runs through a tag or group can show a clean diff, and an empty result is never proof a change is safe. Posture DEFINITION changes ARE detected: posture names are resolved to their rules, so tightening `posture:corp` shows as a change -- except when a preview omits the definitions map, where it falls back to comparing names. It costs two preview requests per user, so it checks the first 25 by default and stops after 60 seconds regardless; either way it sets `truncated`, reports how many were skipped, and says which limit stopped it. Users whose preview fails are listed in `failed` and excluded from the compared count -- a failure is never reported as lost access, and if nothing could be compared the call fails rather than returning an empty diff.",
31658
31669
  annotations: {
31659
31670
  title: "Diff ACL access",
31660
31671
  readOnlyHint: true,
@@ -31665,7 +31676,7 @@ var aclTools = [
31665
31676
  inputSchema: external_exports.object({
31666
31677
  policy: external_exports.string().describe("The proposed ACL policy text to compare against the current live policy"),
31667
31678
  principals: external_exports.array(external_exports.string().trim().min(1)).optional().describe(
31668
- "User emails to check. Omit to enumerate the tailnet's users automatically. Pass an explicit list to bound the request count, or to check specific users beyond the cap."
31679
+ "Principals to check, as they appear in `loginName` from tailscale_list_users. That is often an email, but on a GitHub or SSO tailnet it is not (e.g. 'alice@github') -- pass the loginName verbatim rather than an address you assume. Omit to enumerate the tailnet's users automatically. Pass an explicit list to bound the request count, or to check specific users beyond the cap."
31669
31680
  ),
31670
31681
  maxPrincipals: external_exports.number().int().positive().optional().describe(
31671
31682
  `Maximum users to check (default ${DEFAULT_PRINCIPAL_CAP}). Each costs two preview requests. Raising this on a large tailnet can be slow and may hit rate limits.`
@@ -31742,8 +31753,11 @@ var aclTools = [
31742
31753
  }
31743
31754
  continue;
31744
31755
  }
31745
- const lost = [...before.access].filter((a) => !after.access.has(a)).sort();
31746
- const gained = [...after.access].filter((a) => !before.access.has(a)).sort();
31756
+ const definitionsAvailable = before.hasPostureDefs && after.hasPostureDefs;
31757
+ const beforeAccess = accessSet(before.matches, before.postureDefs, definitionsAvailable);
31758
+ const afterAccess = accessSet(after.matches, after.postureDefs, definitionsAvailable);
31759
+ const lost = [...beforeAccess].filter((a) => !afterAccess.has(a)).sort();
31760
+ const gained = [...afterAccess].filter((a) => !beforeAccess.has(a)).sort();
31747
31761
  if (lost.length === 0 && gained.length === 0) unchanged.push(principal);
31748
31762
  else changed.push({ principal, lost, gained });
31749
31763
  }
@@ -31792,9 +31806,8 @@ var aclTools = [
31792
31806
  // can come back empty while real access changed.
31793
31807
  scope: [
31794
31808
  "User principals only.",
31795
- "Not compared: access granted via tags or groups;",
31796
- "changes to a posture DEFINITION (the diff keys on posture names, so redefining `posture:corp` more strictly leaves every key identical);",
31797
- "and a destination whose port list is narrowed appears as a paired loss and gain of the whole entry (`tag:prod:22,80` -> `tag:prod:22`) rather than a clean loss, so `gained` is not a literal list of newly-reachable destinations.",
31809
+ "Not compared: access granted via tags or groups.",
31810
+ "Posture definition changes ARE compared -- names are resolved to their rules -- unless a preview omitted the definitions map, in which case names alone are compared and a redefinition would not show.",
31798
31811
  "An empty diff is not proof the change is safe."
31799
31812
  ].join(" "),
31800
31813
  changed,
@@ -34538,7 +34551,7 @@ function buildMetaTools(state) {
34538
34551
  }
34539
34552
 
34540
34553
  // src/index.ts
34541
- var version2 = true ? "0.19.0" : resolveVersionFallback();
34554
+ var version2 = true ? "0.19.1" : resolveVersionFallback();
34542
34555
  var subcommand = process.argv[2];
34543
34556
  var cliSubcommandHandled = false;
34544
34557
  if (subcommand === "deploy-acl" || subcommand === "validate-acl") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/tailscale-mcp",
3
- "version": "0.19.0",
3
+ "version": "0.19.1",
4
4
  "mcpName": "io.github.YawLabs/tailscale-mcp",
5
5
  "description": "Tailscale MCP server for managing your tailnet from AI assistants",
6
6
  "license": "MIT",