@node9/proxy 1.63.0 → 1.65.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 +1787 -943
- package/dist/cli.mjs +1770 -926
- package/dist/dashboard.mjs +84 -26
- package/dist/index.js +161 -41
- package/dist/index.mjs +161 -41
- package/dist/scan-ink.mjs +6 -5
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -295,6 +295,7 @@ var ConfigFileSchema = z.object({
|
|
|
295
295
|
// nudge-only (default), and the scan cadence in minutes.
|
|
296
296
|
mcpAutoWrap: z.boolean().optional(),
|
|
297
297
|
mcpReconcileIntervalMinutes: z.number().positive().optional(),
|
|
298
|
+
mcpStaleAfterDays: z.number().min(0).optional(),
|
|
298
299
|
cloudSyncIntervalHours: z.number().positive().optional(),
|
|
299
300
|
// Seconds-granular override for the cloud policy sync cadence. Wins over
|
|
300
301
|
// cloudSyncIntervalHours when set. Lets you opt into fast apply (e.g. 20)
|
|
@@ -3305,6 +3306,7 @@ var redis_default = {
|
|
|
3305
3306
|
name: "redis",
|
|
3306
3307
|
description: "Protects Redis instances from destructive AI operations",
|
|
3307
3308
|
aliases: [],
|
|
3309
|
+
_scopeNote: "Every rule requires Redis to actually be involved: a client in the command (redis-cli / valkey-cli, in either order \u2014 `redis-cli FLUSHALL` and `echo FLUSHALL | redis-cli` are both real), OR the command field holding the bare keyword, which is what the MCP redis_command tool sends ({ command: 'FLUSHALL', args: [] }). Without the client requirement these rules matched the WORD anywhere \u2014 including prose in a heredoc writing a file \u2014 which produced two observed false positives: a test fixture describing the shield, and a commit message naming the rule. Every other builtin shield already scopes this way (aws s3, docker system prune, kubectl delete, gh repo delete). See redis-scope.spec.ts.",
|
|
3308
3310
|
smartRules: [
|
|
3309
3311
|
{
|
|
3310
3312
|
name: "shield:redis:block-flushall",
|
|
@@ -3313,7 +3315,7 @@ var redis_default = {
|
|
|
3313
3315
|
{
|
|
3314
3316
|
field: "command",
|
|
3315
3317
|
op: "matches",
|
|
3316
|
-
value: "\\bFLUSHALL\\b",
|
|
3318
|
+
value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
|
|
3317
3319
|
flags: "i"
|
|
3318
3320
|
}
|
|
3319
3321
|
],
|
|
@@ -3327,7 +3329,7 @@ var redis_default = {
|
|
|
3327
3329
|
{
|
|
3328
3330
|
field: "command",
|
|
3329
3331
|
op: "matches",
|
|
3330
|
-
value: "\\bFLUSHDB\\b",
|
|
3332
|
+
value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
|
|
3331
3333
|
flags: "i"
|
|
3332
3334
|
}
|
|
3333
3335
|
],
|
|
@@ -3341,7 +3343,7 @@ var redis_default = {
|
|
|
3341
3343
|
{
|
|
3342
3344
|
field: "command",
|
|
3343
3345
|
op: "matches",
|
|
3344
|
-
value: "\\
|
|
3346
|
+
value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
|
|
3345
3347
|
flags: "i"
|
|
3346
3348
|
}
|
|
3347
3349
|
],
|
|
@@ -3355,7 +3357,7 @@ var redis_default = {
|
|
|
3355
3357
|
{
|
|
3356
3358
|
field: "command",
|
|
3357
3359
|
op: "matches",
|
|
3358
|
-
value: "\\bCONFIG\\s+SET\\b",
|
|
3360
|
+
value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
|
|
3359
3361
|
flags: "i"
|
|
3360
3362
|
}
|
|
3361
3363
|
],
|
|
@@ -3369,7 +3371,7 @@ var redis_default = {
|
|
|
3369
3371
|
{
|
|
3370
3372
|
field: "command",
|
|
3371
3373
|
op: "matches",
|
|
3372
|
-
value: "
|
|
3374
|
+
value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
|
|
3373
3375
|
flags: "i"
|
|
3374
3376
|
}
|
|
3375
3377
|
],
|
|
@@ -4038,6 +4040,61 @@ function getActiveEnvironment(config) {
|
|
|
4038
4040
|
const env = config.settings.environment || process.env.NODE_ENV || "development";
|
|
4039
4041
|
return config.environments[env] ?? null;
|
|
4040
4042
|
}
|
|
4043
|
+
function readRulesCacheResilient(cacheFile) {
|
|
4044
|
+
let existed = false;
|
|
4045
|
+
let sawReadError = false;
|
|
4046
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
4047
|
+
let content;
|
|
4048
|
+
try {
|
|
4049
|
+
content = fs4.readFileSync(cacheFile, "utf-8");
|
|
4050
|
+
existed = true;
|
|
4051
|
+
} catch (err) {
|
|
4052
|
+
if (err.code === "ENOENT") return {};
|
|
4053
|
+
sawReadError = true;
|
|
4054
|
+
continue;
|
|
4055
|
+
}
|
|
4056
|
+
try {
|
|
4057
|
+
const parsed = JSON.parse(content);
|
|
4058
|
+
lastParsedRulesCache = parsed;
|
|
4059
|
+
return parsed;
|
|
4060
|
+
} catch {
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
4063
|
+
if (existed || sawReadError) {
|
|
4064
|
+
const backup = path4.join(path4.dirname(cacheFile), "rules-cache.last-good.json");
|
|
4065
|
+
if (backup !== cacheFile) {
|
|
4066
|
+
try {
|
|
4067
|
+
const raw = JSON.parse(fs4.readFileSync(backup, "utf-8"));
|
|
4068
|
+
logCacheReadIssue(cacheFile, "RULES_CACHE_CORRUPT_USED_BACKUP");
|
|
4069
|
+
lastParsedRulesCache = raw;
|
|
4070
|
+
return raw;
|
|
4071
|
+
} catch {
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
4074
|
+
if (lastParsedRulesCache) {
|
|
4075
|
+
logCacheReadIssue(cacheFile, "RULES_CACHE_USED_MEMORY");
|
|
4076
|
+
return lastParsedRulesCache;
|
|
4077
|
+
}
|
|
4078
|
+
logCacheReadIssue(cacheFile, "RULES_CACHE_UNREADABLE");
|
|
4079
|
+
}
|
|
4080
|
+
return {};
|
|
4081
|
+
}
|
|
4082
|
+
var lastParsedRulesCache = null;
|
|
4083
|
+
var CACHE_LOG_REARM_MS = 5 * 60 * 1e3;
|
|
4084
|
+
var cacheReadLastLoggedAt = 0;
|
|
4085
|
+
function logCacheReadIssue(cacheFile, kind) {
|
|
4086
|
+
const now = Date.now();
|
|
4087
|
+
if (now - cacheReadLastLoggedAt < CACHE_LOG_REARM_MS) return;
|
|
4088
|
+
cacheReadLastLoggedAt = now;
|
|
4089
|
+
try {
|
|
4090
|
+
fs4.appendFileSync(
|
|
4091
|
+
path4.join(os4.homedir(), ".node9", "hook-debug.log"),
|
|
4092
|
+
`[${(/* @__PURE__ */ new Date()).toISOString()}] ${kind} ${cacheFile}
|
|
4093
|
+
`
|
|
4094
|
+
);
|
|
4095
|
+
} catch {
|
|
4096
|
+
}
|
|
4097
|
+
}
|
|
4041
4098
|
function getConfig(cwd) {
|
|
4042
4099
|
if (!cwd && cachedConfig) return cachedConfig;
|
|
4043
4100
|
const globalPath = path4.join(os4.homedir(), ".node9", "config.json");
|
|
@@ -4105,6 +4162,7 @@ function getConfig(cwd) {
|
|
|
4105
4162
|
if (s.mcpAutoWrap !== void 0) mergedSettings.mcpAutoWrap = s.mcpAutoWrap === true;
|
|
4106
4163
|
if (s.mcpReconcileIntervalMinutes !== void 0)
|
|
4107
4164
|
mergedSettings.mcpReconcileIntervalMinutes = s.mcpReconcileIntervalMinutes;
|
|
4165
|
+
if (s.mcpStaleAfterDays !== void 0) mergedSettings.mcpStaleAfterDays = s.mcpStaleAfterDays;
|
|
4108
4166
|
if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
|
|
4109
4167
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
4110
4168
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
@@ -4114,12 +4172,13 @@ function getConfig(cwd) {
|
|
|
4114
4172
|
if (p.smartRules) {
|
|
4115
4173
|
const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
|
|
4116
4174
|
const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
|
|
4117
|
-
const
|
|
4175
|
+
const localRules = p.smartRules.map(({ pinned: _pinned, ...r }) => r);
|
|
4176
|
+
const userRuleNames = new Set(localRules.filter((r) => r.name).map((r) => r.name));
|
|
4118
4177
|
const filteredBlocks = defaultBlocks.filter((r) => !r.name || !userRuleNames.has(r.name));
|
|
4119
4178
|
const filteredNonBlocks = defaultNonBlocks.filter(
|
|
4120
4179
|
(r) => !r.name || !userRuleNames.has(r.name)
|
|
4121
4180
|
);
|
|
4122
|
-
mergedPolicy.smartRules = [...filteredBlocks, ...
|
|
4181
|
+
mergedPolicy.smartRules = [...filteredBlocks, ...localRules, ...filteredNonBlocks];
|
|
4123
4182
|
}
|
|
4124
4183
|
if (p.snapshot) {
|
|
4125
4184
|
const s2 = p.snapshot;
|
|
@@ -4184,10 +4243,12 @@ function getConfig(cwd) {
|
|
|
4184
4243
|
applyLayer(globalConfig);
|
|
4185
4244
|
applyLayer(projectConfig);
|
|
4186
4245
|
let cloudManagedShields = [];
|
|
4246
|
+
let modeCloudControlled = false;
|
|
4247
|
+
let modeCloudStaged = false;
|
|
4187
4248
|
{
|
|
4188
4249
|
const cacheFile = path4.join(os4.homedir(), ".node9", "rules-cache.json");
|
|
4189
4250
|
try {
|
|
4190
|
-
const raw =
|
|
4251
|
+
const raw = readRulesCacheResilient(cacheFile);
|
|
4191
4252
|
if (Array.isArray(raw.rules) && raw.rules.length > 0) {
|
|
4192
4253
|
applyLayer({ policy: { smartRules: raw.rules } });
|
|
4193
4254
|
}
|
|
@@ -4204,6 +4265,9 @@ function getConfig(cwd) {
|
|
|
4204
4265
|
locked.includes("mode")
|
|
4205
4266
|
);
|
|
4206
4267
|
}
|
|
4268
|
+
if (typeof mc.mode === "string" || locked.includes("mode")) {
|
|
4269
|
+
modeCloudControlled = true;
|
|
4270
|
+
}
|
|
4207
4271
|
if (mc.egress && typeof mc.egress === "object") {
|
|
4208
4272
|
const hosts = (v) => Array.isArray(v) ? v.filter((h) => typeof h === "string") : void 0;
|
|
4209
4273
|
mergedPolicy.egress = applyManagedEgress(
|
|
@@ -4302,19 +4366,28 @@ function getConfig(cwd) {
|
|
|
4302
4366
|
}
|
|
4303
4367
|
if (raw.shadowMode === true) {
|
|
4304
4368
|
mergedSettings.mode = "observe";
|
|
4369
|
+
modeCloudStaged = true;
|
|
4305
4370
|
}
|
|
4306
4371
|
} catch {
|
|
4307
4372
|
}
|
|
4308
4373
|
}
|
|
4309
4374
|
const shieldOverrides = readShieldOverrides();
|
|
4310
4375
|
const activeShieldNames = [.../* @__PURE__ */ new Set([...readActiveShields(), ...cloudManagedShields])];
|
|
4376
|
+
const cloudManagedSet = new Set(cloudManagedShields);
|
|
4311
4377
|
for (const shieldName of activeShieldNames) {
|
|
4312
|
-
const
|
|
4378
|
+
const isCloudMandated = cloudManagedSet.has(shieldName);
|
|
4379
|
+
const shield = isCloudMandated ? BUILTIN_SHIELDS[shieldName] : getShield(shieldName);
|
|
4313
4380
|
if (!shield) continue;
|
|
4314
4381
|
const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
4315
|
-
const ruleOverrides = shieldOverrides[shieldName] ?? {};
|
|
4382
|
+
const ruleOverrides = isCloudMandated ? {} : shieldOverrides[shieldName] ?? {};
|
|
4316
4383
|
for (const rule of shield.smartRules) {
|
|
4317
|
-
|
|
4384
|
+
const collides = rule.name ? existingRuleNames.has(rule.name) : false;
|
|
4385
|
+
if (isCloudMandated) {
|
|
4386
|
+
if (collides) {
|
|
4387
|
+
mergedPolicy.smartRules = mergedPolicy.smartRules.filter((r) => r.name !== rule.name);
|
|
4388
|
+
}
|
|
4389
|
+
mergedPolicy.smartRules.push({ ...rule, pinned: true });
|
|
4390
|
+
} else if (!collides) {
|
|
4318
4391
|
const overrideVerdict = rule.name ? ruleOverrides[rule.name] : void 0;
|
|
4319
4392
|
mergedPolicy.smartRules.push(
|
|
4320
4393
|
overrideVerdict !== void 0 ? { ...rule, verdict: overrideVerdict } : rule
|
|
@@ -4330,7 +4403,27 @@ function getConfig(cwd) {
|
|
|
4330
4403
|
for (const rule of ADVISORY_SMART_RULES) {
|
|
4331
4404
|
if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
|
|
4332
4405
|
}
|
|
4333
|
-
|
|
4406
|
+
const envMode = process.env.NODE9_MODE;
|
|
4407
|
+
if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
|
|
4408
|
+
mergedSettings.mode = envMode;
|
|
4409
|
+
}
|
|
4410
|
+
if (cloudManagedShields.length > 0 && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
|
|
4411
|
+
mergedSettings.mode = "standard";
|
|
4412
|
+
}
|
|
4413
|
+
const managedFloorActive = cloudManagedShields.length > 0 || modeCloudControlled && mergedSettings.mode === "strict";
|
|
4414
|
+
if (modeCloudControlled && mergedSettings.mode === "strict") {
|
|
4415
|
+
for (const name of Object.keys(mergedEnvironments)) {
|
|
4416
|
+
if (mergedEnvironments[name]?.requireApproval === false) {
|
|
4417
|
+
const cleaned = { ...mergedEnvironments[name] };
|
|
4418
|
+
delete cleaned.requireApproval;
|
|
4419
|
+
mergedEnvironments[name] = cleaned;
|
|
4420
|
+
}
|
|
4421
|
+
}
|
|
4422
|
+
}
|
|
4423
|
+
if (managedFloorActive) {
|
|
4424
|
+
mergedPolicy.ignoredTools = [...DEFAULT_CONFIG.policy.ignoredTools];
|
|
4425
|
+
mergedPolicy.sandboxPaths = [...DEFAULT_CONFIG.policy.sandboxPaths];
|
|
4426
|
+
}
|
|
4334
4427
|
mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
|
|
4335
4428
|
mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
|
|
4336
4429
|
mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
|
|
@@ -4718,6 +4811,18 @@ function isDaemonRunning() {
|
|
|
4718
4811
|
}
|
|
4719
4812
|
return false;
|
|
4720
4813
|
}
|
|
4814
|
+
async function daemonHasInteractiveApprover(timeoutMs = 400) {
|
|
4815
|
+
try {
|
|
4816
|
+
const res = await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/approver`, {
|
|
4817
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
4818
|
+
});
|
|
4819
|
+
if (!res.ok) return false;
|
|
4820
|
+
const body = await res.json();
|
|
4821
|
+
return body.interactive === true;
|
|
4822
|
+
} catch {
|
|
4823
|
+
return false;
|
|
4824
|
+
}
|
|
4825
|
+
}
|
|
4721
4826
|
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent) {
|
|
4722
4827
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
4723
4828
|
const ctrl = new AbortController();
|
|
@@ -5414,6 +5519,12 @@ function isNetworkTool(toolName, args) {
|
|
|
5414
5519
|
function notifyActivity(data) {
|
|
5415
5520
|
return notifyActivitySocket(data);
|
|
5416
5521
|
}
|
|
5522
|
+
async function hasReachableHumanApprover(opts) {
|
|
5523
|
+
const hasDisplay = !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
|
|
5524
|
+
const nativeReachable = !opts.calledFromDaemon && opts.approvers.native !== false && hasDisplay;
|
|
5525
|
+
if (nativeReachable) return true;
|
|
5526
|
+
return opts.approvers.terminal !== false && await daemonHasInteractiveApprover();
|
|
5527
|
+
}
|
|
5417
5528
|
async function authorizeHeadless(toolName, args, meta, options) {
|
|
5418
5529
|
if (!options?.calledFromDaemon) {
|
|
5419
5530
|
const actId = randomUUID();
|
|
@@ -5487,6 +5598,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5487
5598
|
let riskMetadata;
|
|
5488
5599
|
let statefulRecoveryCommand;
|
|
5489
5600
|
let localSmartRuleMatched = false;
|
|
5601
|
+
let hardBlockDowngraded = false;
|
|
5490
5602
|
let taintWarning = null;
|
|
5491
5603
|
if (isNetworkTool(toolName, args)) {
|
|
5492
5604
|
const filePaths = extractFilePaths(toolName, args);
|
|
@@ -5728,6 +5840,36 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5728
5840
|
return { approved: true, checkedBy: "local-policy" };
|
|
5729
5841
|
}
|
|
5730
5842
|
if (policyResult.decision === "block") {
|
|
5843
|
+
hardBlockDowngraded = true;
|
|
5844
|
+
const daemonUp = isDaemonRunning();
|
|
5845
|
+
let humanApproverReachable = false;
|
|
5846
|
+
if (!policyResult.dependsOnStatePredicates?.length && daemonUp && !isTestEnv2) {
|
|
5847
|
+
humanApproverReachable = await hasReachableHumanApprover({
|
|
5848
|
+
approvers,
|
|
5849
|
+
calledFromDaemon: options?.calledFromDaemon
|
|
5850
|
+
});
|
|
5851
|
+
}
|
|
5852
|
+
const mayDowngrade = daemonUp && !isTestEnv2 && humanApproverReachable;
|
|
5853
|
+
const hardBlock = () => {
|
|
5854
|
+
if (!isManual)
|
|
5855
|
+
appendLocalAudit(
|
|
5856
|
+
toolName,
|
|
5857
|
+
args,
|
|
5858
|
+
"deny",
|
|
5859
|
+
"smart-rule-block",
|
|
5860
|
+
{ ...meta, ruleName: policyResult.ruleName },
|
|
5861
|
+
hashAuditArgs
|
|
5862
|
+
);
|
|
5863
|
+
return {
|
|
5864
|
+
approved: false,
|
|
5865
|
+
reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
|
|
5866
|
+
blockedBy: "local-config",
|
|
5867
|
+
blockedByLabel: policyResult.blockedByLabel,
|
|
5868
|
+
ruleHit: policyResult.ruleName,
|
|
5869
|
+
...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
|
|
5870
|
+
...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription }
|
|
5871
|
+
};
|
|
5872
|
+
};
|
|
5731
5873
|
if (policyResult.dependsOnStatePredicates?.length) {
|
|
5732
5874
|
const stateResults = await checkStatePredicates(policyResult.dependsOnStatePredicates);
|
|
5733
5875
|
const predicatesMet = stateResults !== null && policyResult.dependsOnStatePredicates.every((p) => stateResults[p] === true);
|
|
@@ -5744,7 +5886,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5744
5886
|
if (predicatesMet && policyResult.recoveryCommand) {
|
|
5745
5887
|
statefulRecoveryCommand = policyResult.recoveryCommand;
|
|
5746
5888
|
}
|
|
5747
|
-
} else if (
|
|
5889
|
+
} else if (mayDowngrade) {
|
|
5748
5890
|
if (!isManual)
|
|
5749
5891
|
appendLocalAudit(
|
|
5750
5892
|
toolName,
|
|
@@ -5766,36 +5908,14 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5766
5908
|
};
|
|
5767
5909
|
}
|
|
5768
5910
|
} else {
|
|
5769
|
-
|
|
5770
|
-
appendLocalAudit(
|
|
5771
|
-
toolName,
|
|
5772
|
-
args,
|
|
5773
|
-
"deny",
|
|
5774
|
-
"smart-rule-block",
|
|
5775
|
-
// Include policyResult.ruleName so the [2] Report SHIELDS
|
|
5776
|
-
// panel can attribute this block to its specific shield
|
|
5777
|
-
// (e.g. `shield:project-jail:block-read-ssh`) via the
|
|
5778
|
-
// rule→shield map. checkedBy stays as the generic
|
|
5779
|
-
// `smart-rule-block` for backward compat with existing
|
|
5780
|
-
// log readers.
|
|
5781
|
-
{ ...meta, ruleName: policyResult.ruleName },
|
|
5782
|
-
hashAuditArgs
|
|
5783
|
-
);
|
|
5784
|
-
return {
|
|
5785
|
-
approved: false,
|
|
5786
|
-
reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
|
|
5787
|
-
blockedBy: "local-config",
|
|
5788
|
-
blockedByLabel: policyResult.blockedByLabel,
|
|
5789
|
-
ruleHit: policyResult.ruleName,
|
|
5790
|
-
...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
|
|
5791
|
-
...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription }
|
|
5792
|
-
};
|
|
5911
|
+
return hardBlock();
|
|
5793
5912
|
}
|
|
5794
5913
|
}
|
|
5795
5914
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
5796
5915
|
policyMatchedField = policyResult.matchedField;
|
|
5797
5916
|
policyMatchedWord = policyResult.matchedWord;
|
|
5798
|
-
if (policyResult.ruleName
|
|
5917
|
+
if (policyResult.ruleName || policyResult.tier === 7 || hardBlockDowngraded)
|
|
5918
|
+
localSmartRuleMatched = true;
|
|
5799
5919
|
if (policyResult.ruleDescription) policyRuleDescription = policyResult.ruleDescription;
|
|
5800
5920
|
else if (policyResult.reason) policyRuleDescription = policyResult.reason;
|
|
5801
5921
|
riskMetadata = computeRiskMetadata(
|
|
@@ -5807,7 +5927,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5807
5927
|
policyResult.ruleName
|
|
5808
5928
|
);
|
|
5809
5929
|
if (policyRuleDescription) riskMetadata.ruleDescription = policyRuleDescription.slice(0, 200);
|
|
5810
|
-
const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
|
|
5930
|
+
const persistent = policyResult.ruleName || policyResult.tier === 7 || hardBlockDowngraded ? null : getPersistentDecision(toolName);
|
|
5811
5931
|
if (persistent === "allow" && !appPermReview) {
|
|
5812
5932
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta, hashAuditArgs);
|
|
5813
5933
|
return { approved: true, checkedBy: "persistent" };
|
|
@@ -5840,7 +5960,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5840
5960
|
return { approved: true };
|
|
5841
5961
|
}
|
|
5842
5962
|
}
|
|
5843
|
-
if (!taintWarning && !appPermReview && getActiveTrustSession(toolName, args)) {
|
|
5963
|
+
if (!taintWarning && !appPermReview && !hardBlockDowngraded && getActiveTrustSession(toolName, args)) {
|
|
5844
5964
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
|
|
5845
5965
|
return { approved: true, checkedBy: "trust" };
|
|
5846
5966
|
}
|
|
@@ -5904,7 +6024,7 @@ ${appPermReview}`
|
|
|
5904
6024
|
forceReview
|
|
5905
6025
|
);
|
|
5906
6026
|
if (!initResult.pending) {
|
|
5907
|
-
if (initResult.shadowMode && !appPermReview) {
|
|
6027
|
+
if (initResult.shadowMode && !localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview) {
|
|
5908
6028
|
return { approved: true, checkedBy: "cloud" };
|
|
5909
6029
|
}
|
|
5910
6030
|
if (!localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview) {
|
package/dist/scan-ink.mjs
CHANGED
|
@@ -1500,6 +1500,7 @@ var redis_default = {
|
|
|
1500
1500
|
name: "redis",
|
|
1501
1501
|
description: "Protects Redis instances from destructive AI operations",
|
|
1502
1502
|
aliases: [],
|
|
1503
|
+
_scopeNote: "Every rule requires Redis to actually be involved: a client in the command (redis-cli / valkey-cli, in either order \u2014 `redis-cli FLUSHALL` and `echo FLUSHALL | redis-cli` are both real), OR the command field holding the bare keyword, which is what the MCP redis_command tool sends ({ command: 'FLUSHALL', args: [] }). Without the client requirement these rules matched the WORD anywhere \u2014 including prose in a heredoc writing a file \u2014 which produced two observed false positives: a test fixture describing the shield, and a commit message naming the rule. Every other builtin shield already scopes this way (aws s3, docker system prune, kubectl delete, gh repo delete). See redis-scope.spec.ts.",
|
|
1503
1504
|
smartRules: [
|
|
1504
1505
|
{
|
|
1505
1506
|
name: "shield:redis:block-flushall",
|
|
@@ -1508,7 +1509,7 @@ var redis_default = {
|
|
|
1508
1509
|
{
|
|
1509
1510
|
field: "command",
|
|
1510
1511
|
op: "matches",
|
|
1511
|
-
value: "\\bFLUSHALL\\b",
|
|
1512
|
+
value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
|
|
1512
1513
|
flags: "i"
|
|
1513
1514
|
}
|
|
1514
1515
|
],
|
|
@@ -1522,7 +1523,7 @@ var redis_default = {
|
|
|
1522
1523
|
{
|
|
1523
1524
|
field: "command",
|
|
1524
1525
|
op: "matches",
|
|
1525
|
-
value: "\\bFLUSHDB\\b",
|
|
1526
|
+
value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
|
|
1526
1527
|
flags: "i"
|
|
1527
1528
|
}
|
|
1528
1529
|
],
|
|
@@ -1536,7 +1537,7 @@ var redis_default = {
|
|
|
1536
1537
|
{
|
|
1537
1538
|
field: "command",
|
|
1538
1539
|
op: "matches",
|
|
1539
|
-
value: "\\
|
|
1540
|
+
value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
|
|
1540
1541
|
flags: "i"
|
|
1541
1542
|
}
|
|
1542
1543
|
],
|
|
@@ -1550,7 +1551,7 @@ var redis_default = {
|
|
|
1550
1551
|
{
|
|
1551
1552
|
field: "command",
|
|
1552
1553
|
op: "matches",
|
|
1553
|
-
value: "\\bCONFIG\\s+SET\\b",
|
|
1554
|
+
value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
|
|
1554
1555
|
flags: "i"
|
|
1555
1556
|
}
|
|
1556
1557
|
],
|
|
@@ -1564,7 +1565,7 @@ var redis_default = {
|
|
|
1564
1565
|
{
|
|
1565
1566
|
field: "command",
|
|
1566
1567
|
op: "matches",
|
|
1567
|
-
value: "
|
|
1568
|
+
value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
|
|
1568
1569
|
flags: "i"
|
|
1569
1570
|
}
|
|
1570
1571
|
],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/proxy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.65.0",
|
|
4
4
|
"description": "The Sudo Command for AI Agents. Execution Security for Claude Code, Codex, Gemini, Cursor, Opencode, Pi, and any MCP server.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|