@aipermission/mcp 0.2.44 → 0.2.47

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.
@@ -8,6 +8,7 @@ const execFileAsync = promisify(execFile);
8
8
  const staleTemporaryAgeMs = 24 * 60 * 60 * 1000;
9
9
  const lockRetryDelayMs = 50;
10
10
  const lockRetryLimit = 100;
11
+ const windowsFullControlRights = 2032127;
11
12
 
12
13
  export async function atomicWritePrivateFile(filePath, contents, options = {}) {
13
14
  const destination = path.resolve(filePath);
@@ -73,9 +74,8 @@ export async function assertPrivateFilePermissions(filePath, options = {}) {
73
74
  if ((stat.mode & 0o077) !== 0) throw new Error(`permissions are not private; run chmod 600 ${filePath}`);
74
75
  return;
75
76
  }
76
- const sid = await currentWindowsSID(options);
77
- const { stdout } = await runWindowsSystemExecutable("icacls", [filePath], options, { encoding: "utf8" });
78
- if (!stdout.includes(sid) || !stdout.includes("(F)") || stdout.includes("(I)")) {
77
+ const [identity, acl] = await Promise.all([currentWindowsIdentity(options), readWindowsACL(filePath, options)]);
78
+ if (!windowsACLIsPrivate(acl, identity)) {
79
79
  throw new Error(`Windows ACL is not restricted to the current user: ${filePath}`);
80
80
  }
81
81
  }
@@ -272,6 +272,7 @@ async function enforcePrivateFilePermissions(filePath, options) {
272
272
  return;
273
273
  }
274
274
  const sid = await currentWindowsSID(options);
275
+ await setWindowsOwner(filePath, sid, options);
275
276
  await runWindowsSystemExecutable("icacls", [filePath, "/inheritance:r", "/grant:r", `*${sid}:(F)`], options);
276
277
  }
277
278
 
@@ -285,16 +286,80 @@ async function enforcePrivateDirectoryPermissions(directory, options) {
285
286
  return;
286
287
  }
287
288
  const sid = await currentWindowsSID(options);
289
+ await setWindowsOwner(directory, sid, options);
288
290
  await runWindowsSystemExecutable("icacls", [directory, "/inheritance:r", "/grant:r", `*${sid}:(OI)(CI)(F)`], options);
289
291
  }
290
292
 
293
+ async function setWindowsOwner(targetPath, sid, options) {
294
+ await runWindowsSystemExecutable("icacls", [targetPath, "/setowner", `*${sid}`], options);
295
+ }
296
+
291
297
  async function currentWindowsSID(options) {
298
+ return (await currentWindowsIdentity(options)).sid;
299
+ }
300
+
301
+ async function currentWindowsIdentity(options) {
292
302
  const { stdout } = await runWindowsSystemExecutable("whoami", ["/user", "/fo", "csv", "/nh"], options, {
293
303
  encoding: "utf8",
294
304
  });
295
- const sid = stdout.match(/S-\d-(?:\d+-)+\d+/)?.[0];
296
- if (!sid) throw new Error("Could not determine current Windows SID for private config ACL");
297
- return sid;
305
+ const match = stdout.match(/^\s*"((?:[^"]|"")*)"\s*,\s*"(S-\d-(?:\d+-)+\d+)"\s*$/im);
306
+ if (!match) throw new Error("Could not determine current Windows identity for private config ACL");
307
+ return { name: match[1].replaceAll('""', '"'), sid: match[2] };
308
+ }
309
+
310
+ async function readWindowsACL(filePath, options) {
311
+ const script = [
312
+ "$ErrorActionPreference = 'Stop'",
313
+ "$TargetPath = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($env:AIPERMISSION_PRIVATE_FILE_PATH_B64))",
314
+ "$Acl = [System.IO.File]::GetAccessControl($TargetPath)",
315
+ "$Rules = @($Acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier]) | ForEach-Object {",
316
+ "[PSCustomObject]@{ identity_sid = $_.IdentityReference.Value; access_type = $_.AccessControlType.ToString(); inherited = [bool]$_.IsInherited; rights = [int64]$_.FileSystemRights }",
317
+ "})",
318
+ "[PSCustomObject]@{ owner_sid = $Acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value; protected = [bool]$Acl.AreAccessRulesProtected; rules = $Rules } | ConvertTo-Json -Depth 4 -Compress",
319
+ ].join("\n");
320
+ const execute = options.execFile || execFileAsync;
321
+ const { stdout } = await execute(windowsPowerShellExecutable(options), ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script], {
322
+ windowsHide: true,
323
+ encoding: "utf8",
324
+ env: {
325
+ ...process.env,
326
+ AIPERMISSION_PRIVATE_FILE_PATH_B64: Buffer.from(filePath, "utf8").toString("base64"),
327
+ },
328
+ });
329
+ let acl;
330
+ try {
331
+ acl = JSON.parse(String(stdout).trim());
332
+ } catch {
333
+ throw new Error(`Could not determine Windows owner for private config: ${filePath}`);
334
+ }
335
+ if (
336
+ !acl ||
337
+ !/^S-\d-(?:\d+-)+\d+$/i.test(acl.owner_sid) ||
338
+ typeof acl.protected !== "boolean" ||
339
+ !Array.isArray(acl.rules) ||
340
+ !acl.rules.every(
341
+ (rule) =>
342
+ rule &&
343
+ /^S-\d-(?:\d+-)+\d+$/i.test(rule.identity_sid) &&
344
+ ["Allow", "Deny"].includes(rule.access_type) &&
345
+ typeof rule.inherited === "boolean" &&
346
+ Number.isSafeInteger(rule.rights),
347
+ )
348
+ ) {
349
+ throw new Error(`Could not determine Windows ACL for private config: ${filePath}`);
350
+ }
351
+ return acl;
352
+ }
353
+
354
+ function windowsACLIsPrivate(acl, identity) {
355
+ if (!acl.protected || acl.owner_sid.toLowerCase() !== identity.sid.toLowerCase() || acl.rules.length !== 1) return false;
356
+ const rule = acl.rules[0];
357
+ return (
358
+ rule.identity_sid.toLowerCase() === identity.sid.toLowerCase() &&
359
+ rule.access_type === "Allow" &&
360
+ !rule.inherited &&
361
+ rule.rights === windowsFullControlRights
362
+ );
298
363
  }
299
364
 
300
365
  function runWindowsSystemExecutable(name, args, options, executionOptions = {}) {
@@ -313,6 +378,14 @@ function windowsSystemExecutable(name, options) {
313
378
  return path.win32.join(systemRoot, "System32", `${name}.exe`);
314
379
  }
315
380
 
381
+ function windowsPowerShellExecutable(options) {
382
+ const systemRoot = options.windowsSystemRoot || process.env.SystemRoot || process.env.windir || "C:\\Windows";
383
+ if (!path.win32.isAbsolute(systemRoot)) {
384
+ throw new Error("Windows SystemRoot must be an absolute path for private config ACL setup");
385
+ }
386
+ return path.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
387
+ }
388
+
316
389
  function operatingSystem(options) {
317
390
  return options.platform || process.platform;
318
391
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aipermission/mcp",
3
- "version": "0.2.44",
3
+ "version": "0.2.47",
4
4
  "mcpName": "io.github.aipermission/aipermission-mcp",
5
5
  "description": "Local-first MCP bridge for the aipermission gateway.",
6
6
  "license": "AGPL-3.0-only",
package/server.json CHANGED
@@ -3,12 +3,12 @@
3
3
  "name": "io.github.aipermission/aipermission-mcp",
4
4
  "title": "AIPermission",
5
5
  "description": "Local-first MCP bridge for the AIPermission gateway.",
6
- "version": "0.2.44",
6
+ "version": "0.2.47",
7
7
  "packages": [
8
8
  {
9
9
  "registryType": "npm",
10
10
  "identifier": "@aipermission/mcp",
11
- "version": "0.2.44",
11
+ "version": "0.2.47",
12
12
  "transport": {
13
13
  "type": "stdio"
14
14
  }