@node9/proxy 1.55.1 → 1.57.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/cli.js +3096 -1963
- package/dist/cli.mjs +3079 -1947
- package/dist/dashboard.mjs +70 -41
- package/dist/index.js +326 -101
- package/dist/index.mjs +326 -101
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -233,9 +233,9 @@ module.exports = __toCommonJS(src_exports);
|
|
|
233
233
|
init_audit();
|
|
234
234
|
|
|
235
235
|
// src/config/index.ts
|
|
236
|
-
var
|
|
237
|
-
var
|
|
238
|
-
var
|
|
236
|
+
var import_fs4 = __toESM(require("fs"));
|
|
237
|
+
var import_path4 = __toESM(require("path"));
|
|
238
|
+
var import_os4 = __toESM(require("os"));
|
|
239
239
|
|
|
240
240
|
// src/config-schema.ts
|
|
241
241
|
var import_zod = require("zod");
|
|
@@ -321,6 +321,10 @@ var ConfigFileSchema = import_zod.z.object({
|
|
|
321
321
|
// approver_set). Default (unset/false): those tools refuse over MCP — a human
|
|
322
322
|
// must run them from the CLI. node9's threat model is the agent itself.
|
|
323
323
|
mcpAllowWeakening: import_zod.z.boolean().optional(),
|
|
324
|
+
// Auto-wire reconciler (P3 2.6): auto-wrap new ungoverned MCP servers vs
|
|
325
|
+
// nudge-only (default), and the scan cadence in minutes.
|
|
326
|
+
mcpAutoWrap: import_zod.z.boolean().optional(),
|
|
327
|
+
mcpReconcileIntervalMinutes: import_zod.z.number().positive().optional(),
|
|
324
328
|
cloudSyncIntervalHours: import_zod.z.number().positive().optional(),
|
|
325
329
|
// Seconds-granular override for the cloud policy sync cadence. Wins over
|
|
326
330
|
// cloudSyncIntervalHours when set. Lets you opt into fast apply (e.g. 20)
|
|
@@ -3555,6 +3559,109 @@ function applyManagedDlp(local, managed, locked) {
|
|
|
3555
3559
|
}
|
|
3556
3560
|
return next;
|
|
3557
3561
|
}
|
|
3562
|
+
function applyManagedApprovers(local, managed) {
|
|
3563
|
+
return {
|
|
3564
|
+
...local,
|
|
3565
|
+
native: typeof managed.native === "boolean" ? managed.native : local.native,
|
|
3566
|
+
browser: typeof managed.browser === "boolean" ? managed.browser : local.browser,
|
|
3567
|
+
cloud: typeof managed.cloud === "boolean" ? managed.cloud : local.cloud,
|
|
3568
|
+
terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
|
|
3569
|
+
};
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
// src/shields/build.ts
|
|
3573
|
+
function escapeRegex(s) {
|
|
3574
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3575
|
+
}
|
|
3576
|
+
function slug(s) {
|
|
3577
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "rule";
|
|
3578
|
+
}
|
|
3579
|
+
var B = "[\\s/\\\\]";
|
|
3580
|
+
var SEP = "[/\\\\]";
|
|
3581
|
+
function pathToRegexFragment(rawPath) {
|
|
3582
|
+
const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
|
|
3583
|
+
const segments = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
|
|
3584
|
+
if (segments.length === 0) return "";
|
|
3585
|
+
return `(^|${B})${segments.join(SEP)}(${B}|$)`;
|
|
3586
|
+
}
|
|
3587
|
+
function pathRules(rawPath, verdict, reason) {
|
|
3588
|
+
const value = pathToRegexFragment(rawPath);
|
|
3589
|
+
if (!value) return [];
|
|
3590
|
+
const why = reason ?? `Accessing ${rawPath} is restricted by this shield`;
|
|
3591
|
+
const s = slug(rawPath);
|
|
3592
|
+
return [
|
|
3593
|
+
{
|
|
3594
|
+
name: `${verdict}-path-${s}-bash`,
|
|
3595
|
+
tool: "bash",
|
|
3596
|
+
conditions: [{ field: "command", op: "matches", value }],
|
|
3597
|
+
verdict,
|
|
3598
|
+
reason: why
|
|
3599
|
+
},
|
|
3600
|
+
{
|
|
3601
|
+
name: `${verdict}-path-${s}-anytool`,
|
|
3602
|
+
tool: "*",
|
|
3603
|
+
conditions: [{ field: "file_path", op: "matches", value }],
|
|
3604
|
+
verdict,
|
|
3605
|
+
reason: why
|
|
3606
|
+
}
|
|
3607
|
+
];
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3610
|
+
// src/auth/trusted-hosts.ts
|
|
3611
|
+
var import_fs3 = __toESM(require("fs"));
|
|
3612
|
+
var import_path3 = __toESM(require("path"));
|
|
3613
|
+
var import_os3 = __toESM(require("os"));
|
|
3614
|
+
function getTrustedHostsPath() {
|
|
3615
|
+
return import_path3.default.join(import_os3.default.homedir(), ".node9", "trusted-hosts.json");
|
|
3616
|
+
}
|
|
3617
|
+
function readTrustedHosts() {
|
|
3618
|
+
try {
|
|
3619
|
+
const raw = import_fs3.default.readFileSync(getTrustedHostsPath(), "utf8");
|
|
3620
|
+
const parsed = JSON.parse(raw);
|
|
3621
|
+
return Array.isArray(parsed.hosts) ? parsed.hosts : [];
|
|
3622
|
+
} catch {
|
|
3623
|
+
return [];
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
var _cache = null;
|
|
3627
|
+
var CACHE_TTL_MS = 5e3;
|
|
3628
|
+
function getFileMtime() {
|
|
3629
|
+
try {
|
|
3630
|
+
return import_fs3.default.statSync(getTrustedHostsPath()).mtimeMs;
|
|
3631
|
+
} catch {
|
|
3632
|
+
return 0;
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
function getCachedHosts() {
|
|
3636
|
+
const now = Date.now();
|
|
3637
|
+
if (_cache && now < _cache.expiry) {
|
|
3638
|
+
const mtime = getFileMtime();
|
|
3639
|
+
if (mtime === _cache.mtime) return _cache.hosts;
|
|
3640
|
+
}
|
|
3641
|
+
const hosts = readTrustedHosts();
|
|
3642
|
+
_cache = { hosts, expiry: now + CACHE_TTL_MS, mtime: getFileMtime() };
|
|
3643
|
+
return hosts;
|
|
3644
|
+
}
|
|
3645
|
+
function normalizeHost(raw) {
|
|
3646
|
+
return raw.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^[^@]+@/, "").replace(/:\d+$/, "");
|
|
3647
|
+
}
|
|
3648
|
+
function matchesTrustedHost(host, list) {
|
|
3649
|
+
const normalized = normalizeHost(host);
|
|
3650
|
+
return list.some((raw) => {
|
|
3651
|
+
const entryHost = raw.toLowerCase();
|
|
3652
|
+
if (entryHost.startsWith("*.")) {
|
|
3653
|
+
const domain = entryHost.slice(2);
|
|
3654
|
+
return normalized.endsWith("." + domain);
|
|
3655
|
+
}
|
|
3656
|
+
return normalized === entryHost;
|
|
3657
|
+
});
|
|
3658
|
+
}
|
|
3659
|
+
function isTrustedHost(host) {
|
|
3660
|
+
return matchesTrustedHost(
|
|
3661
|
+
host,
|
|
3662
|
+
getCachedHosts().map((entry) => entry.host)
|
|
3663
|
+
);
|
|
3664
|
+
}
|
|
3558
3665
|
|
|
3559
3666
|
// src/config/index.ts
|
|
3560
3667
|
var DANGEROUS_WORDS = [
|
|
@@ -3765,7 +3872,10 @@ var DEFAULT_CONFIG = {
|
|
|
3765
3872
|
egress: { enabled: false, mode: "review", allow: [], deny: [], allowPrivate: true },
|
|
3766
3873
|
loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 },
|
|
3767
3874
|
injectionScan: { enabled: false, minConfidence: "medium", allow: [] },
|
|
3768
|
-
skillPinning: { enabled: false, mode: "warn", roots: [] }
|
|
3875
|
+
skillPinning: { enabled: false, mode: "warn", roots: [] },
|
|
3876
|
+
trustedHosts: [],
|
|
3877
|
+
trustedHostsManaged: false,
|
|
3878
|
+
appPermissions: {}
|
|
3769
3879
|
},
|
|
3770
3880
|
environments: {}
|
|
3771
3881
|
};
|
|
@@ -3840,9 +3950,9 @@ function getCredentials() {
|
|
|
3840
3950
|
};
|
|
3841
3951
|
}
|
|
3842
3952
|
try {
|
|
3843
|
-
const credPath =
|
|
3844
|
-
if (
|
|
3845
|
-
const creds = JSON.parse(
|
|
3953
|
+
const credPath = import_path4.default.join(import_os4.default.homedir(), ".node9", "credentials.json");
|
|
3954
|
+
if (import_fs4.default.existsSync(credPath)) {
|
|
3955
|
+
const creds = JSON.parse(import_fs4.default.readFileSync(credPath, "utf-8"));
|
|
3846
3956
|
const profileName = process.env.NODE9_PROFILE || "default";
|
|
3847
3957
|
const profile = creds[profileName];
|
|
3848
3958
|
if (profile?.apiKey) {
|
|
@@ -3868,8 +3978,8 @@ function getActiveEnvironment(config) {
|
|
|
3868
3978
|
}
|
|
3869
3979
|
function getConfig(cwd) {
|
|
3870
3980
|
if (!cwd && cachedConfig) return cachedConfig;
|
|
3871
|
-
const globalPath =
|
|
3872
|
-
const projectPath =
|
|
3981
|
+
const globalPath = import_path4.default.join(import_os4.default.homedir(), ".node9", "config.json");
|
|
3982
|
+
const projectPath = import_path4.default.join(cwd ?? process.cwd(), "node9.config.json");
|
|
3873
3983
|
const globalConfig = tryLoadConfig(globalPath);
|
|
3874
3984
|
const projectConfig = tryLoadConfig(projectPath);
|
|
3875
3985
|
const mergedSettings = {
|
|
@@ -3902,7 +4012,13 @@ function getConfig(cwd) {
|
|
|
3902
4012
|
skillPinning: {
|
|
3903
4013
|
...DEFAULT_CONFIG.policy.skillPinning,
|
|
3904
4014
|
roots: [...DEFAULT_CONFIG.policy.skillPinning.roots]
|
|
3905
|
-
}
|
|
4015
|
+
},
|
|
4016
|
+
// Left empty on purpose: the local file is read fresh at policy-eval time
|
|
4017
|
+
// (getCachedHosts via isTrustedHost), NOT snapshotted into the frozen config
|
|
4018
|
+
// here. A managed list fills this below and flips trustedHostsManaged.
|
|
4019
|
+
trustedHosts: [],
|
|
4020
|
+
trustedHostsManaged: false,
|
|
4021
|
+
appPermissions: {}
|
|
3906
4022
|
};
|
|
3907
4023
|
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
3908
4024
|
const applyLayer = (source) => {
|
|
@@ -3924,6 +4040,9 @@ function getConfig(cwd) {
|
|
|
3924
4040
|
if (s.mcpAllowWeakening !== void 0) mergedSettings.mcpAllowWeakening = s.mcpAllowWeakening;
|
|
3925
4041
|
if (s.cloudSyncIntervalHours !== void 0)
|
|
3926
4042
|
mergedSettings.cloudSyncIntervalHours = s.cloudSyncIntervalHours;
|
|
4043
|
+
if (s.mcpAutoWrap !== void 0) mergedSettings.mcpAutoWrap = s.mcpAutoWrap === true;
|
|
4044
|
+
if (s.mcpReconcileIntervalMinutes !== void 0)
|
|
4045
|
+
mergedSettings.mcpReconcileIntervalMinutes = s.mcpReconcileIntervalMinutes;
|
|
3927
4046
|
if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
|
|
3928
4047
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
3929
4048
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
@@ -4004,9 +4123,9 @@ function getConfig(cwd) {
|
|
|
4004
4123
|
applyLayer(projectConfig);
|
|
4005
4124
|
let cloudManagedShields = [];
|
|
4006
4125
|
{
|
|
4007
|
-
const cacheFile =
|
|
4126
|
+
const cacheFile = import_path4.default.join(import_os4.default.homedir(), ".node9", "rules-cache.json");
|
|
4008
4127
|
try {
|
|
4009
|
-
const raw = JSON.parse(
|
|
4128
|
+
const raw = JSON.parse(import_fs4.default.readFileSync(cacheFile, "utf-8"));
|
|
4010
4129
|
if (Array.isArray(raw.rules) && raw.rules.length > 0) {
|
|
4011
4130
|
applyLayer({ policy: { smartRules: raw.rules } });
|
|
4012
4131
|
}
|
|
@@ -4047,6 +4166,74 @@ function getConfig(cwd) {
|
|
|
4047
4166
|
locked
|
|
4048
4167
|
);
|
|
4049
4168
|
}
|
|
4169
|
+
if (mc.approvers && typeof mc.approvers === "object") {
|
|
4170
|
+
const bool = (v) => typeof v === "boolean" ? v : void 0;
|
|
4171
|
+
mergedSettings.approvers = applyManagedApprovers(mergedSettings.approvers, {
|
|
4172
|
+
native: bool(mc.approvers.native),
|
|
4173
|
+
browser: bool(mc.approvers.browser),
|
|
4174
|
+
cloud: bool(mc.approvers.cloud),
|
|
4175
|
+
terminal: bool(mc.approvers.terminal)
|
|
4176
|
+
});
|
|
4177
|
+
}
|
|
4178
|
+
if (mc.reviewChannel === "ask" || mc.reviewChannel === "approver") {
|
|
4179
|
+
mergedSettings.reviewChannel = mc.reviewChannel;
|
|
4180
|
+
}
|
|
4181
|
+
if (typeof mc.approvalTimeoutMs === "number" && mc.approvalTimeoutMs > 0) {
|
|
4182
|
+
mergedSettings.approvalTimeoutMs = mc.approvalTimeoutMs;
|
|
4183
|
+
}
|
|
4184
|
+
if (mc.injectionScan && typeof mc.injectionScan === "object") {
|
|
4185
|
+
const i = mc.injectionScan;
|
|
4186
|
+
const cur = mergedPolicy.injectionScan;
|
|
4187
|
+
mergedPolicy.injectionScan = {
|
|
4188
|
+
enabled: typeof i.enabled === "boolean" ? i.enabled : cur.enabled,
|
|
4189
|
+
minConfidence: i.minConfidence === "high" || i.minConfidence === "medium" ? i.minConfidence : cur.minConfidence,
|
|
4190
|
+
allow: Array.isArray(i.allow) ? i.allow.filter((x) => typeof x === "string") : cur.allow
|
|
4191
|
+
};
|
|
4192
|
+
}
|
|
4193
|
+
if (mc.loopDetection && typeof mc.loopDetection === "object") {
|
|
4194
|
+
const l = mc.loopDetection;
|
|
4195
|
+
const cur = mergedPolicy.loopDetection;
|
|
4196
|
+
mergedPolicy.loopDetection = {
|
|
4197
|
+
enabled: typeof l.enabled === "boolean" ? l.enabled : cur.enabled,
|
|
4198
|
+
threshold: typeof l.threshold === "number" && Number.isFinite(l.threshold) ? l.threshold : cur.threshold,
|
|
4199
|
+
windowSeconds: typeof l.windowSeconds === "number" && Number.isFinite(l.windowSeconds) ? l.windowSeconds : cur.windowSeconds
|
|
4200
|
+
};
|
|
4201
|
+
}
|
|
4202
|
+
if (mc.skillPinning && typeof mc.skillPinning === "object") {
|
|
4203
|
+
const sk = mc.skillPinning;
|
|
4204
|
+
const cur = mergedPolicy.skillPinning;
|
|
4205
|
+
mergedPolicy.skillPinning = {
|
|
4206
|
+
enabled: typeof sk.enabled === "boolean" ? sk.enabled : cur.enabled,
|
|
4207
|
+
mode: sk.mode === "block" || sk.mode === "warn" ? sk.mode : cur.mode,
|
|
4208
|
+
roots: Array.isArray(sk.roots) ? sk.roots.filter((x) => typeof x === "string") : cur.roots
|
|
4209
|
+
};
|
|
4210
|
+
}
|
|
4211
|
+
if (Array.isArray(mc.jailPaths)) {
|
|
4212
|
+
for (const jp of mc.jailPaths) {
|
|
4213
|
+
const path13 = typeof jp?.path === "string" ? jp.path.trim() : "";
|
|
4214
|
+
if (!path13) continue;
|
|
4215
|
+
const verdict = jp?.verdict === "review" ? "review" : "block";
|
|
4216
|
+
for (const r of pathRules(path13, verdict, "org-managed jail")) {
|
|
4217
|
+
mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
if (Array.isArray(mc.trustedHosts)) {
|
|
4222
|
+
mergedPolicy.trustedHostsManaged = true;
|
|
4223
|
+
mergedPolicy.trustedHosts = mc.trustedHosts.filter((h) => typeof h === "string").map((h) => normalizeHost(h));
|
|
4224
|
+
}
|
|
4225
|
+
if (mc.appPermissions && typeof mc.appPermissions === "object" && !Array.isArray(mc.appPermissions)) {
|
|
4226
|
+
const coerced = {};
|
|
4227
|
+
for (const [srv, tools] of Object.entries(mc.appPermissions)) {
|
|
4228
|
+
if (!tools || typeof tools !== "object" || Array.isArray(tools)) continue;
|
|
4229
|
+
const m = {};
|
|
4230
|
+
for (const [tool, d] of Object.entries(tools)) {
|
|
4231
|
+
if (d === "allow" || d === "review" || d === "block") m[tool] = d;
|
|
4232
|
+
}
|
|
4233
|
+
if (Object.keys(m).length) coerced[srv] = m;
|
|
4234
|
+
}
|
|
4235
|
+
mergedPolicy.appPermissions = coerced;
|
|
4236
|
+
}
|
|
4050
4237
|
}
|
|
4051
4238
|
if (raw.panicMode === true) {
|
|
4052
4239
|
mergedSettings.panicMode = true;
|
|
@@ -4098,10 +4285,10 @@ function getConfig(cwd) {
|
|
|
4098
4285
|
return result;
|
|
4099
4286
|
}
|
|
4100
4287
|
function tryLoadConfig(filePath) {
|
|
4101
|
-
if (!
|
|
4288
|
+
if (!import_fs4.default.existsSync(filePath)) return null;
|
|
4102
4289
|
let raw;
|
|
4103
4290
|
try {
|
|
4104
|
-
raw = JSON.parse(
|
|
4291
|
+
raw = JSON.parse(import_fs4.default.readFileSync(filePath, "utf-8"));
|
|
4105
4292
|
} catch (err) {
|
|
4106
4293
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4107
4294
|
process.stderr.write(
|
|
@@ -4155,18 +4342,18 @@ ${error.replace("Invalid config:\n", "")}
|
|
|
4155
4342
|
var import_picomatch2 = __toESM(require("picomatch"));
|
|
4156
4343
|
|
|
4157
4344
|
// src/dlp.ts
|
|
4158
|
-
var
|
|
4159
|
-
var
|
|
4345
|
+
var import_fs5 = __toESM(require("fs"));
|
|
4346
|
+
var import_path5 = __toESM(require("path"));
|
|
4160
4347
|
function scanFilePath(filePath, cwd = process.cwd()) {
|
|
4161
4348
|
if (!filePath) return null;
|
|
4162
4349
|
let resolved;
|
|
4163
4350
|
try {
|
|
4164
|
-
const absolute =
|
|
4165
|
-
resolved =
|
|
4351
|
+
const absolute = import_path5.default.resolve(cwd, filePath);
|
|
4352
|
+
resolved = import_fs5.default.realpathSync.native(absolute);
|
|
4166
4353
|
} catch (err) {
|
|
4167
4354
|
const code = err.code;
|
|
4168
4355
|
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
4169
|
-
resolved =
|
|
4356
|
+
resolved = import_path5.default.resolve(cwd, filePath);
|
|
4170
4357
|
} else {
|
|
4171
4358
|
return sensitivePathMatch(filePath);
|
|
4172
4359
|
}
|
|
@@ -4175,27 +4362,27 @@ function scanFilePath(filePath, cwd = process.cwd()) {
|
|
|
4175
4362
|
}
|
|
4176
4363
|
|
|
4177
4364
|
// src/utils/provenance.ts
|
|
4178
|
-
var
|
|
4179
|
-
var
|
|
4180
|
-
var
|
|
4365
|
+
var import_fs6 = __toESM(require("fs"));
|
|
4366
|
+
var import_path6 = __toESM(require("path"));
|
|
4367
|
+
var import_os5 = __toESM(require("os"));
|
|
4181
4368
|
var SYSTEM_PREFIXES = ["/usr/bin", "/usr/sbin", "/bin", "/sbin"];
|
|
4182
4369
|
var MANAGED_PREFIXES = ["/usr/local/bin", "/opt/homebrew", "/home/linuxbrew", "/nix/store"];
|
|
4183
4370
|
var USER_PREFIXES = [
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4371
|
+
import_path6.default.join(import_os5.default.homedir(), "bin"),
|
|
4372
|
+
import_path6.default.join(import_os5.default.homedir(), ".local", "bin"),
|
|
4373
|
+
import_path6.default.join(import_os5.default.homedir(), ".cargo", "bin"),
|
|
4374
|
+
import_path6.default.join(import_os5.default.homedir(), ".npm-global", "bin"),
|
|
4375
|
+
import_path6.default.join(import_os5.default.homedir(), ".volta", "bin")
|
|
4189
4376
|
];
|
|
4190
4377
|
var SUSPECT_PREFIXES = ["/tmp", "/var/tmp", "/dev/shm"];
|
|
4191
4378
|
function findInPath(cmd) {
|
|
4192
|
-
if (
|
|
4379
|
+
if (import_path6.default.posix.isAbsolute(cmd)) return cmd;
|
|
4193
4380
|
const pathEnv = process.env.PATH ?? "";
|
|
4194
|
-
for (const dir of pathEnv.split(
|
|
4381
|
+
for (const dir of pathEnv.split(import_path6.default.delimiter)) {
|
|
4195
4382
|
if (!dir) continue;
|
|
4196
|
-
const full =
|
|
4383
|
+
const full = import_path6.default.join(dir, cmd);
|
|
4197
4384
|
try {
|
|
4198
|
-
|
|
4385
|
+
import_fs6.default.accessSync(full, import_fs6.default.constants.X_OK);
|
|
4199
4386
|
return full;
|
|
4200
4387
|
} catch {
|
|
4201
4388
|
}
|
|
@@ -4206,7 +4393,7 @@ function _classifyPath(resolved, cwd) {
|
|
|
4206
4393
|
if (cwd && resolved.startsWith(cwd + "/")) {
|
|
4207
4394
|
return { trustLevel: "user", reason: "binary in project directory" };
|
|
4208
4395
|
}
|
|
4209
|
-
const osTmp =
|
|
4396
|
+
const osTmp = import_os5.default.tmpdir();
|
|
4210
4397
|
const allSuspect = osTmp ? [...SUSPECT_PREFIXES, osTmp] : SUSPECT_PREFIXES;
|
|
4211
4398
|
if (allSuspect.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
|
|
4212
4399
|
return { trustLevel: "suspect", reason: `binary in temp directory: ${resolved}` };
|
|
@@ -4224,7 +4411,7 @@ function _classifyPath(resolved, cwd) {
|
|
|
4224
4411
|
}
|
|
4225
4412
|
function checkProvenance(cmd, cwd) {
|
|
4226
4413
|
const bare = cmd.startsWith("./") ? cmd.slice(2) : cmd;
|
|
4227
|
-
if (
|
|
4414
|
+
if (import_path6.default.posix.isAbsolute(bare)) {
|
|
4228
4415
|
const early = _classifyPath(bare, cwd);
|
|
4229
4416
|
if (early.trustLevel === "suspect") {
|
|
4230
4417
|
return { resolvedPath: bare, ...early };
|
|
@@ -4240,7 +4427,7 @@ function checkProvenance(cmd, cwd) {
|
|
|
4240
4427
|
reason: "binary not found in PATH"
|
|
4241
4428
|
};
|
|
4242
4429
|
}
|
|
4243
|
-
resolved =
|
|
4430
|
+
resolved = import_fs6.default.realpathSync(found);
|
|
4244
4431
|
} catch {
|
|
4245
4432
|
return {
|
|
4246
4433
|
resolvedPath: cmd,
|
|
@@ -4249,7 +4436,7 @@ function checkProvenance(cmd, cwd) {
|
|
|
4249
4436
|
};
|
|
4250
4437
|
}
|
|
4251
4438
|
try {
|
|
4252
|
-
const stat =
|
|
4439
|
+
const stat = import_fs6.default.statSync(resolved);
|
|
4253
4440
|
if (stat.mode & 2) {
|
|
4254
4441
|
return {
|
|
4255
4442
|
resolvedPath: resolved,
|
|
@@ -4268,56 +4455,6 @@ function checkProvenance(cmd, cwd) {
|
|
|
4268
4455
|
return { resolvedPath: resolved, ...classify };
|
|
4269
4456
|
}
|
|
4270
4457
|
|
|
4271
|
-
// src/auth/trusted-hosts.ts
|
|
4272
|
-
var import_fs6 = __toESM(require("fs"));
|
|
4273
|
-
var import_path6 = __toESM(require("path"));
|
|
4274
|
-
var import_os5 = __toESM(require("os"));
|
|
4275
|
-
function getTrustedHostsPath() {
|
|
4276
|
-
return import_path6.default.join(import_os5.default.homedir(), ".node9", "trusted-hosts.json");
|
|
4277
|
-
}
|
|
4278
|
-
function readTrustedHosts() {
|
|
4279
|
-
try {
|
|
4280
|
-
const raw = import_fs6.default.readFileSync(getTrustedHostsPath(), "utf8");
|
|
4281
|
-
const parsed = JSON.parse(raw);
|
|
4282
|
-
return Array.isArray(parsed.hosts) ? parsed.hosts : [];
|
|
4283
|
-
} catch {
|
|
4284
|
-
return [];
|
|
4285
|
-
}
|
|
4286
|
-
}
|
|
4287
|
-
var _cache = null;
|
|
4288
|
-
var CACHE_TTL_MS = 5e3;
|
|
4289
|
-
function getFileMtime() {
|
|
4290
|
-
try {
|
|
4291
|
-
return import_fs6.default.statSync(getTrustedHostsPath()).mtimeMs;
|
|
4292
|
-
} catch {
|
|
4293
|
-
return 0;
|
|
4294
|
-
}
|
|
4295
|
-
}
|
|
4296
|
-
function getCachedHosts() {
|
|
4297
|
-
const now = Date.now();
|
|
4298
|
-
if (_cache && now < _cache.expiry) {
|
|
4299
|
-
const mtime = getFileMtime();
|
|
4300
|
-
if (mtime === _cache.mtime) return _cache.hosts;
|
|
4301
|
-
}
|
|
4302
|
-
const hosts = readTrustedHosts();
|
|
4303
|
-
_cache = { hosts, expiry: now + CACHE_TTL_MS, mtime: getFileMtime() };
|
|
4304
|
-
return hosts;
|
|
4305
|
-
}
|
|
4306
|
-
function normalizeHost(raw) {
|
|
4307
|
-
return raw.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^[^@]+@/, "").replace(/:\d+$/, "");
|
|
4308
|
-
}
|
|
4309
|
-
function isTrustedHost(host) {
|
|
4310
|
-
const normalized = normalizeHost(host);
|
|
4311
|
-
return getCachedHosts().some((entry) => {
|
|
4312
|
-
const entryHost = entry.host.toLowerCase();
|
|
4313
|
-
if (entryHost.startsWith("*.")) {
|
|
4314
|
-
const domain = entryHost.slice(2);
|
|
4315
|
-
return normalized.endsWith("." + domain);
|
|
4316
|
-
}
|
|
4317
|
-
return normalized === entryHost;
|
|
4318
|
-
});
|
|
4319
|
-
}
|
|
4320
|
-
|
|
4321
4458
|
// src/policy/index.ts
|
|
4322
4459
|
async function evaluatePolicy2(toolName, args, agent, cwd) {
|
|
4323
4460
|
const config = getConfig();
|
|
@@ -4327,7 +4464,14 @@ async function evaluatePolicy2(toolName, args, agent, cwd) {
|
|
|
4327
4464
|
toolName,
|
|
4328
4465
|
args,
|
|
4329
4466
|
{ agent, cwd, activeEnvironment },
|
|
4330
|
-
{
|
|
4467
|
+
{
|
|
4468
|
+
checkProvenance,
|
|
4469
|
+
// Managed → match against the org list (frozen with the rest of managed
|
|
4470
|
+
// config; changes arrive via cloud sync). Unmanaged → the local file via
|
|
4471
|
+
// getCachedHosts (5s TTL + mtime), so a `node9 trust add/remove` still
|
|
4472
|
+
// reaches a long-lived in-process authorizer (the gateway) within seconds.
|
|
4473
|
+
isTrustedHost: config.policy.trustedHostsManaged ? (host) => matchesTrustedHost(host, config.policy.trustedHosts) : isTrustedHost
|
|
4474
|
+
}
|
|
4331
4475
|
);
|
|
4332
4476
|
}
|
|
4333
4477
|
function isIgnoredTool2(toolName) {
|
|
@@ -5274,6 +5418,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5274
5418
|
const isManual = meta?.agent === "Terminal";
|
|
5275
5419
|
const isObserveMode = config.settings.mode === "observe";
|
|
5276
5420
|
let explainableLabel = "Local Config";
|
|
5421
|
+
let dlpReviewFlagged = false;
|
|
5277
5422
|
let policyMatchedField;
|
|
5278
5423
|
let policyMatchedWord;
|
|
5279
5424
|
let policyRuleDescription;
|
|
@@ -5378,6 +5523,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5378
5523
|
if (!isManual)
|
|
5379
5524
|
appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta, hashAuditArgs);
|
|
5380
5525
|
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
|
|
5526
|
+
dlpReviewFlagged = true;
|
|
5381
5527
|
}
|
|
5382
5528
|
}
|
|
5383
5529
|
if (config.policy.dlp.pii === "block" && (!isIgnoredTool2(toolName) || config.policy.dlp.scanIgnoredTools)) {
|
|
@@ -5443,9 +5589,40 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5443
5589
|
}
|
|
5444
5590
|
return { approved: true, checkedBy: "audit" };
|
|
5445
5591
|
}
|
|
5592
|
+
let appPermReview = null;
|
|
5593
|
+
let appPermReviewTool = null;
|
|
5594
|
+
if (meta?.serverKey) {
|
|
5595
|
+
const prefix = meta.mcpServer ? `mcp__${meta.mcpServer}__` : "";
|
|
5596
|
+
const bareTool = prefix && toolName.startsWith(prefix) ? toolName.slice(prefix.length) : toolName;
|
|
5597
|
+
const decision = config.policy.appPermissions?.[meta.serverKey]?.[bareTool];
|
|
5598
|
+
const hardBlock = decision === "block" || decision === "review" && config.settings.panicMode === true;
|
|
5599
|
+
if (hardBlock) {
|
|
5600
|
+
if (!isManual)
|
|
5601
|
+
appendLocalAudit(
|
|
5602
|
+
toolName,
|
|
5603
|
+
args,
|
|
5604
|
+
"deny",
|
|
5605
|
+
"app-permission-block",
|
|
5606
|
+
// ruleName gives the dashboard row its "why" (rule attribution), the
|
|
5607
|
+
// same channel shield fires use; mcpServer (in meta) gives the app chip.
|
|
5608
|
+
{ ...meta, ruleName: `app-permission:${bareTool}` },
|
|
5609
|
+
hashAuditArgs
|
|
5610
|
+
);
|
|
5611
|
+
return {
|
|
5612
|
+
approved: false,
|
|
5613
|
+
blockedBy: "local-config",
|
|
5614
|
+
reason: decision === "block" ? `App permission: "${bareTool}" is set to block by your workspace.` : `App permission: "${bareTool}" requires review, and the workspace is in panic mode \u2014 all review actions are blocked.`,
|
|
5615
|
+
blockedByLabel: decision === "block" ? "\u{1F512} Node9 App Permission (Blocked)" : "\u{1F6A8} Panic mode (org policy)"
|
|
5616
|
+
};
|
|
5617
|
+
}
|
|
5618
|
+
if (decision === "review") {
|
|
5619
|
+
appPermReview = `App permission: "${bareTool}" requires human approval (workspace policy).`;
|
|
5620
|
+
appPermReviewTool = bareTool;
|
|
5621
|
+
}
|
|
5622
|
+
}
|
|
5446
5623
|
if (!taintWarning && !isIgnoredTool2(toolName)) {
|
|
5447
5624
|
const ld = config.policy.loopDetection;
|
|
5448
|
-
if (ld.enabled) {
|
|
5625
|
+
if (ld.enabled && !appPermReview) {
|
|
5449
5626
|
const loopResult = recordAndCheck(toolName, args, ld.threshold, ld.windowSeconds * 1e3);
|
|
5450
5627
|
if (loopResult.looping) {
|
|
5451
5628
|
const reason = `It looks like you've called "${toolName}" ${loopResult.count} times with identical arguments in the last ${ld.windowSeconds}s. Are you stuck? Step back and reconsider your approach \u2014 what are you actually trying to accomplish, and is there a different way to get there?`;
|
|
@@ -5476,7 +5653,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5476
5653
|
reason: "Workspace is in panic mode \u2014 all review-verdict actions are blocked. Contact your admin to disable panic mode in the Node9 dashboard."
|
|
5477
5654
|
};
|
|
5478
5655
|
}
|
|
5479
|
-
if (policyResult.decision === "allow") {
|
|
5656
|
+
if (policyResult.decision === "allow" && !appPermReview) {
|
|
5480
5657
|
if (!isManual)
|
|
5481
5658
|
appendLocalAudit(
|
|
5482
5659
|
toolName,
|
|
@@ -5569,7 +5746,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5569
5746
|
);
|
|
5570
5747
|
if (policyRuleDescription) riskMetadata.ruleDescription = policyRuleDescription.slice(0, 200);
|
|
5571
5748
|
const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
|
|
5572
|
-
if (persistent === "allow") {
|
|
5749
|
+
if (persistent === "allow" && !appPermReview) {
|
|
5573
5750
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta, hashAuditArgs);
|
|
5574
5751
|
return { approved: true, checkedBy: "persistent" };
|
|
5575
5752
|
}
|
|
@@ -5583,7 +5760,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5583
5760
|
blockedByLabel: "Persistent User Rule"
|
|
5584
5761
|
};
|
|
5585
5762
|
}
|
|
5586
|
-
} else if (!taintWarning) {
|
|
5763
|
+
} else if (!taintWarning && !appPermReview) {
|
|
5587
5764
|
const toolLower = toolName.toLowerCase();
|
|
5588
5765
|
const isFileTool = toolLower === "read" || toolLower === "grep" || toolLower === "glob" || toolLower === "read_file" || toolLower === "grep_search" || toolLower === "list_files";
|
|
5589
5766
|
if (isFileTool && readActiveShields().includes("project-jail")) {
|
|
@@ -5601,7 +5778,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5601
5778
|
return { approved: true };
|
|
5602
5779
|
}
|
|
5603
5780
|
}
|
|
5604
|
-
if (!taintWarning && getActiveTrustSession(toolName, args)) {
|
|
5781
|
+
if (!taintWarning && !appPermReview && getActiveTrustSession(toolName, args)) {
|
|
5605
5782
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
|
|
5606
5783
|
return { approved: true, checkedBy: "trust" };
|
|
5607
5784
|
}
|
|
@@ -5613,11 +5790,35 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5613
5790
|
explainableLabel,
|
|
5614
5791
|
void 0,
|
|
5615
5792
|
void 0,
|
|
5616
|
-
taintWarning
|
|
5793
|
+
appPermReview ? `${taintWarning}
|
|
5794
|
+
${appPermReview}` : taintWarning
|
|
5617
5795
|
);
|
|
5796
|
+
} else if (appPermReview) {
|
|
5797
|
+
if (dlpReviewFlagged) {
|
|
5798
|
+
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review) + \u{1F512} App Permission (Review)";
|
|
5799
|
+
riskMetadata = computeRiskMetadata(
|
|
5800
|
+
args,
|
|
5801
|
+
6,
|
|
5802
|
+
explainableLabel,
|
|
5803
|
+
void 0,
|
|
5804
|
+
void 0,
|
|
5805
|
+
`A credential was detected in this call (DLP review).
|
|
5806
|
+
${appPermReview}`
|
|
5807
|
+
);
|
|
5808
|
+
} else {
|
|
5809
|
+
explainableLabel = "\u{1F512} Node9 App Permission (Review)";
|
|
5810
|
+
riskMetadata = computeRiskMetadata(
|
|
5811
|
+
args,
|
|
5812
|
+
5,
|
|
5813
|
+
explainableLabel,
|
|
5814
|
+
void 0,
|
|
5815
|
+
void 0,
|
|
5816
|
+
appPermReview
|
|
5817
|
+
);
|
|
5818
|
+
}
|
|
5618
5819
|
}
|
|
5619
5820
|
const cloudEnforcedForDefer = approvers.cloud && !!creds?.apiKey;
|
|
5620
|
-
if (options?.deferReview && !taintWarning && !cloudEnforcedForDefer) {
|
|
5821
|
+
if (options?.deferReview && !taintWarning && !appPermReview && !cloudEnforcedForDefer) {
|
|
5621
5822
|
return {
|
|
5622
5823
|
approved: false,
|
|
5623
5824
|
review: true,
|
|
@@ -5628,7 +5829,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5628
5829
|
}
|
|
5629
5830
|
let cloudRequestId = null;
|
|
5630
5831
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
5631
|
-
const forceReview = localSmartRuleMatched === true || options?.localSmartRuleMatched === true || void 0;
|
|
5832
|
+
const forceReview = localSmartRuleMatched === true || options?.localSmartRuleMatched === true || !!appPermReview || void 0;
|
|
5632
5833
|
if (cloudEnforced) {
|
|
5633
5834
|
try {
|
|
5634
5835
|
const initResult = await initNode9SaaS(
|
|
@@ -5641,10 +5842,10 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5641
5842
|
forceReview
|
|
5642
5843
|
);
|
|
5643
5844
|
if (!initResult.pending) {
|
|
5644
|
-
if (initResult.shadowMode) {
|
|
5845
|
+
if (initResult.shadowMode && !appPermReview) {
|
|
5645
5846
|
return { approved: true, checkedBy: "cloud" };
|
|
5646
5847
|
}
|
|
5647
|
-
if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
5848
|
+
if (!localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview) {
|
|
5648
5849
|
return {
|
|
5649
5850
|
approved: !!initResult.approved,
|
|
5650
5851
|
reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
|
|
@@ -5655,7 +5856,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5655
5856
|
}
|
|
5656
5857
|
}
|
|
5657
5858
|
if (initResult.pending) cloudRequestId = initResult.requestId || null;
|
|
5658
|
-
if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
|
|
5859
|
+
if (!taintWarning && !appPermReview) explainableLabel = "Organization Policy (SaaS)";
|
|
5659
5860
|
} catch {
|
|
5660
5861
|
}
|
|
5661
5862
|
}
|
|
@@ -5708,7 +5909,13 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5708
5909
|
options?.activityId,
|
|
5709
5910
|
options?.cwd,
|
|
5710
5911
|
statefulRecoveryCommand,
|
|
5711
|
-
|
|
5912
|
+
// fix #1: for an app-perm review, tell the daemon NOT to run its own
|
|
5913
|
+
// background authorizeHeadless — it re-auths WITHOUT serverKey (meta
|
|
5914
|
+
// carries no serverKey), skips the app-perm gate, and silently
|
|
5915
|
+
// auto-allows. This process (the long-running gateway — appPermReview
|
|
5916
|
+
// only ever happens here) stays alive to run its own racers, so the
|
|
5917
|
+
// daemon just holds the card and waits for the human decision.
|
|
5918
|
+
appPermReview ? true : void 0,
|
|
5712
5919
|
void 0,
|
|
5713
5920
|
localSmartRuleMatched || options?.localSmartRuleMatched,
|
|
5714
5921
|
options?.socketActivitySent
|
|
@@ -5755,8 +5962,11 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5755
5962
|
riskMetadata?.ruleDescription
|
|
5756
5963
|
);
|
|
5757
5964
|
if (decision === "always_allow") {
|
|
5758
|
-
|
|
5759
|
-
|
|
5965
|
+
if (!appPermReview) {
|
|
5966
|
+
writeTrustSession(toolName, 36e5, args);
|
|
5967
|
+
return { approved: true, checkedBy: "trust" };
|
|
5968
|
+
}
|
|
5969
|
+
return { approved: true, checkedBy: "daemon", decisionSource: "native" };
|
|
5760
5970
|
}
|
|
5761
5971
|
const isApproved = decision === "allow";
|
|
5762
5972
|
return {
|
|
@@ -5794,6 +6004,15 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5794
6004
|
);
|
|
5795
6005
|
}
|
|
5796
6006
|
if (racePromises.length === 0) {
|
|
6007
|
+
if (!isManual && appPermReview)
|
|
6008
|
+
appendLocalAudit(
|
|
6009
|
+
toolName,
|
|
6010
|
+
args,
|
|
6011
|
+
"deny",
|
|
6012
|
+
"app-permission-review",
|
|
6013
|
+
{ ...meta, ruleName: `app-permission:${appPermReviewTool}` },
|
|
6014
|
+
hashAuditArgs
|
|
6015
|
+
);
|
|
5797
6016
|
return {
|
|
5798
6017
|
approved: false,
|
|
5799
6018
|
noApprovalMechanism: true,
|
|
@@ -5861,7 +6080,13 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
5861
6080
|
// the BE enriches that row instead of inserting a duplicate. Matters
|
|
5862
6081
|
// for EVERY racer outcome, not just cloud wins: a native-popup
|
|
5863
6082
|
// decision on a cloud-pending request would otherwise count twice.
|
|
5864
|
-
|
|
6083
|
+
// fix #6: carry app-perm attribution so a race-resolved approve/deny isn't
|
|
6084
|
+
// anonymous on the dashboard (matches the block row's ruleName).
|
|
6085
|
+
{
|
|
6086
|
+
...meta,
|
|
6087
|
+
...cloudRequestId && { cloudRequestId },
|
|
6088
|
+
...appPermReview && { ruleName: `app-permission:${appPermReviewTool}` }
|
|
6089
|
+
},
|
|
5865
6090
|
hashAuditArgs
|
|
5866
6091
|
);
|
|
5867
6092
|
}
|