fullcourtdefense-cli 1.35.6 → 1.35.7

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.
@@ -820,7 +820,9 @@ function prepareDeveloperConfirmation(event, call, payload, source) {
820
820
  // this exact rule (same rule hash). Rules that forbid memory never reach here.
821
821
  // The caps in force come from the rule and the org ceiling in the cached bundle;
822
822
  // an org that switched "Always allow" off (`null`) honours no grant at all.
823
- const grantScope = (0, devConfirm_1.grantScopeFor)(event, call.toolName, call.toolArgs, operation, context);
823
+ // The grant binds the facts THIS rule fired on: a credential-command rule is
824
+ // about the program, an egress / SSRF / exfil rule is about the host too.
825
+ const grantScope = (0, devConfirm_1.grantScopeFor)(event, call.toolName, call.toolArgs, operation, context, (0, devConfirm_1.grantRuleFactsFor)(source));
824
826
  const grantLimits = (0, devConfirm_1.resolveGrantLimits)(source.grantCaps, (0, runtimeConfig_1.getCachedDeveloperGrantPolicy)());
825
827
  const grant = source.remember === 'none' ? undefined : (0, devConfirm_1.findDeveloperGrant)(grantScope, { policyId: source.policyId, matchedRule: source.matchedRule, policyHash: memoryHash }, grantLimits);
826
828
  if (grant) {
@@ -334,7 +334,38 @@ export declare function listDeveloperGrants(now?: Date): DeveloperGrant[];
334
334
  export declare function revokeDeveloperGrants(key?: string): number;
335
335
  /** One line a person can read on the button and in the grant list: what "Always allow" covers. */
336
336
  export declare function describeGrantScope(scope: GrantScope): string;
337
- export declare function grantScopeFor(event: string, toolName: string, toolArgs: Record<string, unknown> | undefined, operation: string, context: Record<string, unknown> | undefined): GrantScope;
337
+ /**
338
+ * Which line-level facts the RULE that asked actually depends on. A grant binds
339
+ * the facts the rule fired on and nothing else: `gcloud_access_token_cmd` fired
340
+ * on `gcloud auth print-access-token`, so where a later `Invoke-RestMethod` in
341
+ * the same line posts (`logging.` vs `monitoring.googleapis.com`) and whether
342
+ * the engine classed the line `write` or `SHELL` must not partition the grant —
343
+ * that is what made every Always allow ask again (this machine, 2026-09-14..20).
344
+ * A rule that IS about the host (egress policy, SSRF, exfiltration, reverse
345
+ * shell) keeps the host in the grant so `evil.example` never rides on it.
346
+ */
347
+ export interface GrantRuleFacts {
348
+ bindsDestination: boolean;
349
+ }
350
+ /**
351
+ * Derive `GrantRuleFacts` from the source of the ask. Unknown = bound (the
352
+ * narrower grant), so a policy the cache no longer holds never widens anything.
353
+ */
354
+ export declare function grantRuleFactsFor(source: {
355
+ localFinding?: {
356
+ category?: string;
357
+ };
358
+ policyId?: string;
359
+ policies?: Array<{
360
+ id: string;
361
+ rules?: Array<{
362
+ constraints?: Array<{
363
+ field: string;
364
+ }>;
365
+ }>;
366
+ }>;
367
+ }): GrantRuleFacts;
368
+ export declare function grantScopeFor(event: string, toolName: string, toolArgs: Record<string, unknown> | undefined, operation: string, context: Record<string, unknown> | undefined, facts?: GrantRuleFacts): GrantScope;
338
369
  /**
339
370
  * Is this exact action covered in the same session and policy context?
340
371
  * The 1h choice is a shorter expiry, never authorization for another session.
@@ -61,6 +61,7 @@ exports.recordDeveloperGrant = recordDeveloperGrant;
61
61
  exports.listDeveloperGrants = listDeveloperGrants;
62
62
  exports.revokeDeveloperGrants = revokeDeveloperGrants;
63
63
  exports.describeGrantScope = describeGrantScope;
64
+ exports.grantRuleFactsFor = grantRuleFactsFor;
64
65
  exports.grantScopeFor = grantScopeFor;
65
66
  exports.isConfirmationRemembered = isConfirmationRemembered;
66
67
  /**
@@ -1200,7 +1201,25 @@ function scriptGrantKeys(command, cwd) {
1200
1201
  });
1201
1202
  return [...new Set(keys)].sort();
1202
1203
  }
1203
- function grantScopeFor(event, toolName, toolArgs, operation, context) {
1204
+ /** Local Safety categories whose finding is the destination host itself. */
1205
+ const DESTINATION_BOUND_CATEGORIES = new Set(['metadata_ssrf', 'secret_exfiltration', 'reverse_shell']);
1206
+ /**
1207
+ * Derive `GrantRuleFacts` from the source of the ask. Unknown = bound (the
1208
+ * narrower grant), so a policy the cache no longer holds never widens anything.
1209
+ */
1210
+ function grantRuleFactsFor(source) {
1211
+ if (source.localFinding) {
1212
+ return { bindsDestination: DESTINATION_BOUND_CATEGORIES.has(String(source.localFinding.category || '')) };
1213
+ }
1214
+ const policy = source.policyId ? source.policies?.find(p => p.id === source.policyId) : undefined;
1215
+ if (!policy)
1216
+ return { bindsDestination: true };
1217
+ const bindsDestination = (policy.rules || []).some(rule => (rule.constraints || []).some(c => typeof c.field === 'string' && c.field.startsWith('destination.')));
1218
+ return { bindsDestination };
1219
+ }
1220
+ /** Grants made without rule facts (MCP gateway, older callers) keep the narrow, host-bound shape. */
1221
+ const DEFAULT_GRANT_RULE_FACTS = { bindsDestination: true };
1222
+ function grantScopeFor(event, toolName, toolArgs, operation, context, facts = DEFAULT_GRANT_RULE_FACTS) {
1204
1223
  const scope = { event, toolName, operation: String(operation || '').trim() || 'unknown' };
1205
1224
  if (event === 'shell') {
1206
1225
  const command = typeof toolArgs?.command === 'string' ? toolArgs.command : typeof toolArgs?.cmd === 'string' ? toolArgs.cmd : '';
@@ -1209,9 +1228,14 @@ function grantScopeFor(event, toolName, toolArgs, operation, context) {
1209
1228
  const scripts = scriptGrantKeys(command, cwd);
1210
1229
  if (scripts.length)
1211
1230
  scope.scripts = scripts;
1231
+ // A shell line's identity is its programs. The engine verb (`write` for a
1232
+ // POST, `SHELL` otherwise) describes the line, not the app the rule asked
1233
+ // about, and it changed the key between two `gcloud … ; Invoke-RestMethod`
1234
+ // lines that fired the very same rule. One verb for every shell grant.
1235
+ scope.operation = 'SHELL';
1212
1236
  }
1213
1237
  const domain = context?.['destination.domain'];
1214
- if (typeof domain === 'string' && domain.trim())
1238
+ if (facts.bindsDestination && typeof domain === 'string' && domain.trim())
1215
1239
  scope.destinationDomain = domain.trim().toLowerCase();
1216
1240
  return scope;
1217
1241
  }
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.35.6"
2
+ "version": "1.35.7"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.35.6",
3
+ "version": "1.35.7",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {