@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.
- package/dist/private-file.js +79 -6
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/private-file.js
CHANGED
|
@@ -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
|
|
77
|
-
|
|
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
|
|
296
|
-
if (!
|
|
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
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.
|
|
6
|
+
"version": "0.2.47",
|
|
7
7
|
"packages": [
|
|
8
8
|
{
|
|
9
9
|
"registryType": "npm",
|
|
10
10
|
"identifier": "@aipermission/mcp",
|
|
11
|
-
"version": "0.2.
|
|
11
|
+
"version": "0.2.47",
|
|
12
12
|
"transport": {
|
|
13
13
|
"type": "stdio"
|
|
14
14
|
}
|