fullcourtdefense-cli 1.26.15 → 1.26.16

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.
@@ -17,15 +17,47 @@
17
17
  * they diverge. Change this file? Copy it to the other location verbatim.
18
18
  */
19
19
  export type ActionPolicyVerdict = 'allow' | 'block' | 'require_approval' | 'log';
20
+ /**
21
+ * Constraint operators. `not_matches` is the glob complement of `matches`: it is what lets an
22
+ * allow-list be expressed under "most restrictive verdict wins" — `to matches *@company.com ->
23
+ * allow` needs a `to not_matches *@company.com -> block` partner, because an unconstrained block
24
+ * rule would match every call and swallow the allow.
25
+ */
26
+ export type ActionPolicyConstraintOperator = 'contains' | 'not_contains' | 'equals' | 'not_equals' | 'gt' | 'lt' | 'matches' | 'not_matches';
20
27
  export interface ActionPolicyConstraint {
21
28
  field: string;
22
- operator: 'contains' | 'not_contains' | 'equals' | 'not_equals' | 'gt' | 'lt' | 'matches';
29
+ operator: ActionPolicyConstraintOperator;
23
30
  value: string;
24
31
  }
32
+ /**
33
+ * Who resolves a `require_approval` verdict.
34
+ * - `org` — an admin in the console / Slack (default; the only scope with no human at the keyboard).
35
+ * - `developer` — the developer running the agent confirms in their IDE's own permission prompt
36
+ * (Cursor `permission: ask`, Claude `permissionDecision: ask`) or a terminal y/N.
37
+ * Only meaningful where a human is present — see `effectiveApprovalScope`.
38
+ */
39
+ export type ActionPolicyApprovalScope = 'developer' | 'org';
40
+ /** How long a developer's confirmation is remembered for the same tool + operation. */
41
+ export type ActionPolicyApprovalRemember = 'none' | 'session' | '1h';
42
+ /** What happens when nobody answers within the approval window. */
43
+ export type ActionPolicyApprovalOnTimeout = 'block' | 'org';
44
+ export interface ActionPolicyApproval {
45
+ scope?: ActionPolicyApprovalScope;
46
+ remember?: ActionPolicyApprovalRemember;
47
+ onTimeout?: ActionPolicyApprovalOnTimeout;
48
+ }
49
+ /** Fully-resolved approval options (every field present) attached to a `require_approval` result. */
50
+ export interface ResolvedActionPolicyApproval {
51
+ scope: ActionPolicyApprovalScope;
52
+ remember: ActionPolicyApprovalRemember;
53
+ onTimeout: ActionPolicyApprovalOnTimeout;
54
+ }
25
55
  export interface ActionPolicyRule {
26
56
  operations: string[];
27
57
  verdict: ActionPolicyVerdict;
28
58
  constraints?: ActionPolicyConstraint[];
59
+ /** Approval routing for `require_approval` rules. Absent = org admin approval, no memory, block on timeout. */
60
+ approval?: ActionPolicyApproval;
29
61
  }
30
62
  export interface ActionPolicyTargeting {
31
63
  developerNames?: string[];
@@ -59,11 +91,36 @@ export interface EngineCheckResult {
59
91
  resourceType?: string;
60
92
  matchedRule?: string;
61
93
  reason?: string;
94
+ /**
95
+ * Resolved approval routing — present only when `verdict === 'require_approval'`. When several
96
+ * approval rules match, the most restrictive option wins per field (org > developer, none >
97
+ * session > 1h, block > org) so one permissive rule can never loosen another.
98
+ */
99
+ approval?: ResolvedActionPolicyApproval;
62
100
  /** Monitor-stage policies that matched (would-block / would-approve) without enforcing. */
63
101
  monitorMatches?: ActionPolicyMonitorMatch[];
64
102
  /** Org-scoped policies skipped because the call carried no developerName (for caller-side logging). */
65
103
  skippedOrgPolicies?: string[];
66
104
  }
105
+ export declare const DEFAULT_ACTION_POLICY_APPROVAL: ResolvedActionPolicyApproval;
106
+ /**
107
+ * Normalize a rule's approval options: unknown / missing values fall back to the DEFAULT
108
+ * (org, none, block) — the strictest — so a typo in a policy document can never widen who
109
+ * may approve.
110
+ */
111
+ export declare function resolveApprovalOptions(approval?: ActionPolicyApproval): ResolvedActionPolicyApproval;
112
+ /**
113
+ * Where an approval actually goes given who is present at the enforcement point.
114
+ *
115
+ * `developer` scope means "the human running the agent confirms in their IDE". That human only
116
+ * exists on an end machine. Ephemeral machines (CI runners, containers, cloud coding agents) and
117
+ * SDK-embedded online services have no keyboard, so a developer-scoped rule there degrades to the
118
+ * org admin queue — never to a silent self-approval, and never to allow. Pure and shared so the
119
+ * backend, the CLI hook and the SDK agree on this fallback without a network round-trip.
120
+ */
121
+ export declare function effectiveApprovalScope(approval: ActionPolicyApproval | ResolvedActionPolicyApproval | undefined, presence: {
122
+ humanPresent: boolean;
123
+ }): ActionPolicyApprovalScope;
67
124
  /**
68
125
  * Derive the set of canonical operations a tool represents from its NAME and declared inventory
69
126
  * actions. This is the "declared operations" model: the tool's identity/declaration defines what
@@ -18,11 +18,60 @@
18
18
  * they diverge. Change this file? Copy it to the other location verbatim.
19
19
  */
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.DEFAULT_ACTION_POLICY_APPROVAL = void 0;
22
+ exports.resolveApprovalOptions = resolveApprovalOptions;
23
+ exports.effectiveApprovalScope = effectiveApprovalScope;
21
24
  exports.deriveToolOperations = deriveToolOperations;
22
25
  exports.toolCapabilities = toolCapabilities;
23
26
  exports.evaluateActionPolicies = evaluateActionPolicies;
24
27
  exports.inferToolContext = inferToolContext;
25
28
  exports.inventoryToolMatchesActionPolicy = inventoryToolMatchesActionPolicy;
29
+ exports.DEFAULT_ACTION_POLICY_APPROVAL = {
30
+ scope: 'org',
31
+ remember: 'none',
32
+ onTimeout: 'block',
33
+ };
34
+ const APPROVAL_SCOPE_RANK = { developer: 0, org: 1 };
35
+ const APPROVAL_REMEMBER_RANK = { '1h': 0, session: 1, none: 2 };
36
+ const APPROVAL_ON_TIMEOUT_RANK = { org: 0, block: 1 };
37
+ /**
38
+ * Normalize a rule's approval options: unknown / missing values fall back to the DEFAULT
39
+ * (org, none, block) — the strictest — so a typo in a policy document can never widen who
40
+ * may approve.
41
+ */
42
+ function resolveApprovalOptions(approval) {
43
+ const scope = approval?.scope;
44
+ const remember = approval?.remember;
45
+ const onTimeout = approval?.onTimeout;
46
+ return {
47
+ scope: scope === 'developer' || scope === 'org' ? scope : exports.DEFAULT_ACTION_POLICY_APPROVAL.scope,
48
+ remember: remember === 'session' || remember === '1h' || remember === 'none' ? remember : exports.DEFAULT_ACTION_POLICY_APPROVAL.remember,
49
+ onTimeout: onTimeout === 'org' || onTimeout === 'block' ? onTimeout : exports.DEFAULT_ACTION_POLICY_APPROVAL.onTimeout,
50
+ };
51
+ }
52
+ /** Field-wise most-restrictive merge of two resolved approval options. */
53
+ function mergeApprovalOptions(a, b) {
54
+ return {
55
+ scope: APPROVAL_SCOPE_RANK[b.scope] > APPROVAL_SCOPE_RANK[a.scope] ? b.scope : a.scope,
56
+ remember: APPROVAL_REMEMBER_RANK[b.remember] > APPROVAL_REMEMBER_RANK[a.remember] ? b.remember : a.remember,
57
+ onTimeout: APPROVAL_ON_TIMEOUT_RANK[b.onTimeout] > APPROVAL_ON_TIMEOUT_RANK[a.onTimeout] ? b.onTimeout : a.onTimeout,
58
+ };
59
+ }
60
+ /**
61
+ * Where an approval actually goes given who is present at the enforcement point.
62
+ *
63
+ * `developer` scope means "the human running the agent confirms in their IDE". That human only
64
+ * exists on an end machine. Ephemeral machines (CI runners, containers, cloud coding agents) and
65
+ * SDK-embedded online services have no keyboard, so a developer-scoped rule there degrades to the
66
+ * org admin queue — never to a silent self-approval, and never to allow. Pure and shared so the
67
+ * backend, the CLI hook and the SDK agree on this fallback without a network round-trip.
68
+ */
69
+ function effectiveApprovalScope(approval, presence) {
70
+ const resolved = resolveApprovalOptions(approval);
71
+ if (resolved.scope === 'developer' && presence.humanPresent)
72
+ return 'developer';
73
+ return 'org';
74
+ }
26
75
  function matchesGlob(value, pattern) {
27
76
  const escaped = pattern
28
77
  .replace(/[.+^${}()|[\]\\]/g, '\\$&')
@@ -174,6 +223,8 @@ function evaluateConstraint(constraint, context) {
174
223
  return !actualNorm.includes(expectedNorm);
175
224
  case 'matches':
176
225
  return matchesGlob(actualNorm, expectedNorm);
226
+ case 'not_matches':
227
+ return !matchesGlob(actualNorm, expectedNorm);
177
228
  default:
178
229
  return false;
179
230
  }
@@ -190,6 +241,8 @@ function evaluateConstraint(constraint, context) {
190
241
  return !actual.toLowerCase().includes(expected.toLowerCase());
191
242
  case 'matches':
192
243
  return matchesGlob(actual, expected);
244
+ case 'not_matches':
245
+ return !matchesGlob(actual, expected);
193
246
  default:
194
247
  return false;
195
248
  }
@@ -207,6 +260,7 @@ function evaluateConstraint(constraint, context) {
207
260
  return capabilityMatch;
208
261
  case 'not_equals':
209
262
  case 'not_contains':
263
+ case 'not_matches':
210
264
  return !capabilityMatch;
211
265
  default:
212
266
  return false;
@@ -227,6 +281,8 @@ function evaluateConstraint(constraint, context) {
227
281
  return parseFloat(actual) < parseFloat(expected);
228
282
  case 'matches':
229
283
  return matchesGlob(actual, expected);
284
+ case 'not_matches':
285
+ return !matchesGlob(actual, expected);
230
286
  default:
231
287
  return false;
232
288
  }
@@ -528,6 +584,16 @@ function evaluateActionPolicies(policies, toolName, operation, context = {}, dec
528
584
  resourceType: policy.resourceType,
529
585
  matchedRule,
530
586
  reason: `Action policy "${policy.name}": ${rule.operations.join('/')} ${rule.verdict === 'block' ? 'blocked' : rule.verdict === 'require_approval' ? 'requires approval' : rule.verdict}`,
587
+ ...(rule.verdict === 'require_approval' ? { approval: resolveApprovalOptions(rule.approval) } : {}),
588
+ };
589
+ }
590
+ else if (rule.verdict === 'require_approval' && worstResult.verdict === 'require_approval') {
591
+ // A second approval rule matched: keep the first rule's attribution but tighten the
592
+ // approval routing to the most restrictive of both (a developer-scoped rule can never
593
+ // relax an org-scoped one that also fires).
594
+ worstResult = {
595
+ ...worstResult,
596
+ approval: mergeApprovalOptions(worstResult.approval || exports.DEFAULT_ACTION_POLICY_APPROVAL, resolveApprovalOptions(rule.approval)),
531
597
  };
532
598
  }
533
599
  }
@@ -28,6 +28,8 @@ export interface CiProtectArgs {
28
28
  hooks?: string;
29
29
  /** 'false' => skip wrapping MCP client configs with the gateway. */
30
30
  gateway?: string;
31
+ /** 'true' => allow replacing an existing developer-machine enrollment (see assertNotEnrolledLaptop). */
32
+ force?: string;
31
33
  }
32
34
  interface CiContext {
33
35
  provider: string;
@@ -1,44 +1,9 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
3
  exports.detectCiContext = detectCiContext;
37
4
  exports.ciProtectCommand = ciProtectCommand;
38
- const fs = __importStar(require("fs"));
39
5
  const config_1 = require("../config");
40
- const installClaudeHook_1 = require("./installClaudeHook");
41
- const mcpGateway_1 = require("./mcpGateway");
6
+ const ephemeralStack_1 = require("./ephemeralStack");
42
7
  /** Detect the pipeline from standard CI env vars (GitHub Actions, GitLab CI). */
43
8
  function detectCiContext(env = process.env) {
44
9
  if (env.GITHUB_ACTIONS === 'true') {
@@ -89,6 +54,9 @@ async function ciProtectCommand(args, config) {
89
54
  throw new Error('An organization API key is required. Add FCD_API_KEY to the repo/pipeline secrets '
90
55
  + '(create one in the dashboard: Workspace → API keys) or pass --api-key.');
91
56
  }
57
+ // Before any network call or file write: never overwrite a developer's enrollment
58
+ // (self-hosted runner that is also someone's workstation).
59
+ (0, ephemeralStack_1.assertNotEnrolledLaptop)('pipeline', { force: args.force === 'true' });
92
60
  const detected = detectCiContext();
93
61
  const provider = (args.provider || detected?.provider || 'generic-ci').trim();
94
62
  const repo = (args.repo || detected?.repo || '').trim();
@@ -129,62 +97,26 @@ async function ciProtectCommand(args, config) {
129
97
  shieldKey: result.shieldKey,
130
98
  apiUrl,
131
99
  });
100
+ (0, ephemeralStack_1.writeEphemeralIdentityMarker)('pipeline', result.machineId);
132
101
  // Attribute every subsequent hook/gateway event in this job to the PIPELINE
133
102
  // machine record: current process + GITHUB_ENV for the steps that follow
134
103
  // (where the AI agent actually runs).
135
- const identityEnv = {
104
+ (0, ephemeralStack_1.exportIdentityEnv)({
136
105
  FCD_MACHINE_ID: result.machineId,
137
106
  FCD_DEVELOPER_NAME: `ci@${repo.toLowerCase()}`,
138
107
  FCD_MACHINE_HOSTNAME: result.pipeline.key,
139
- };
140
- for (const [key, value] of Object.entries(identityEnv))
141
- process.env[key] = value;
142
- if (process.env.GITHUB_ENV) {
143
- try {
144
- fs.appendFileSync(process.env.GITHUB_ENV, Object.entries(identityEnv).map(([k, v]) => `${k}=${v}\n`).join(''));
145
- }
146
- catch { /* non-GitHub runner with a stray env var — identity still set for this process */ }
147
- }
108
+ }, { githubEnvPath: process.env.GITHUB_ENV });
148
109
  console.log(`\x1b[32m✓ ${result.reused ? 'Pipeline re-enrolled' : 'Pipeline enrolled'}\x1b[0m ${result.shieldName}`);
149
110
  console.log(` Mode: ${result.enforcementMode} · Config: ${savedPath}`);
150
- // Same protection stack as laptops. The Claude-format hook covers Claude
151
- // Code, VS Code agent mode, and Copilot CLI — the common CI agents.
152
- if (args.hooks !== 'false') {
153
- console.log('\n\x1b[1mInstalling runtime hook (Claude Code / VS Code / Copilot CLI)…\x1b[0m');
154
- const hookArgs = {
155
- shieldId: result.shieldId,
156
- shieldKey: result.shieldKey,
157
- apiUrl,
158
- events: 'tools,prompt',
159
- };
160
- try {
161
- await (0, installClaudeHook_1.installClaudeHookCommand)(hookArgs, config);
162
- await sendLog([{ level: 'success', message: 'Runtime hook installed (Claude Code / VS Code / Copilot CLI)' }]);
163
- }
164
- catch (error) {
165
- const reason = error instanceof Error ? error.message : String(error);
166
- console.log(`Hook install skipped: ${reason}`);
167
- await sendLog([{ level: 'warn', message: `Runtime hook install skipped: ${reason}` }]);
168
- }
169
- }
170
- if (args.gateway !== 'false') {
171
- console.log('\n\x1b[1mWrapping MCP client configs with the gateway…\x1b[0m');
172
- const gatewayArgs = {
173
- shieldId: result.shieldId,
174
- shieldKey: result.shieldKey,
175
- apiUrl,
176
- clients: 'all',
177
- };
178
- try {
179
- await (0, mcpGateway_1.protectAllCommand)(gatewayArgs, config);
180
- await sendLog([{ level: 'success', message: 'MCP client configs wrapped with the FullCourtDefense gateway' }]);
181
- }
182
- catch (error) {
183
- const reason = error instanceof Error ? error.message : String(error);
184
- console.log(`MCP gateway wrap skipped: ${reason}`);
185
- await sendLog([{ level: 'warn', message: `MCP gateway wrap skipped: ${reason}` }]);
186
- }
187
- }
111
+ // Same protection stack as laptops (shared with workload-protect).
112
+ await (0, ephemeralStack_1.installEphemeralProtectionStack)({
113
+ shieldId: result.shieldId,
114
+ shieldKey: result.shieldKey,
115
+ apiUrl,
116
+ hooks: args.hooks,
117
+ gateway: args.gateway,
118
+ sendLog,
119
+ }, config);
188
120
  console.log('\n\x1b[32mDone.\x1b[0m This job\'s AI tool calls are now policy-checked and streamed to the fleet console.');
189
121
  console.log(`Pipeline appears in AI Fleet → Machines → CI pipelines as \x1b[1m${repo} · ${workflow}\x1b[0m.`);
190
122
  await sendLog([{ level: 'success', message: `Protection active (${result.enforcementMode} mode) — this job's AI tool calls are policy-checked and recorded` }], 'succeeded');
@@ -590,30 +590,49 @@ async function runDaemon(args, config) {
590
590
  * back within one heartbeat instead of up to an hour later (the 8/12
591
591
  * incident: re-enroll fixed the hooks instantly while the daemon kept
592
592
  * 401-ing for 16 more minutes).
593
+ *
594
+ * The same applies when the daemon already HOLDS credentials: a re-enroll
595
+ * rotates the shield key, so the key in memory is dead the moment
596
+ * ~/.fullcourtdefense.yml is rewritten. Until 9/3 this function returned
597
+ * early whenever creds were present, and a re-enrolled laptop's daemon kept
598
+ * 401-ing (bundle, heartbeat, telemetry) until someone killed the process.
599
+ * Now a changed enrollment file reloads credentials whether or not the
600
+ * daemon had some — one heartbeat later it is on the new key.
593
601
  */
594
602
  const recoverCredentialsIfMissing = () => {
595
- if (creds.shieldId && creds.shieldKey)
596
- return;
597
603
  let enrollmentChanged = false;
598
604
  try {
599
605
  const mtime = fs.statSync((0, config_1.getHomeConfigPath)()).mtimeMs;
600
- enrollmentChanged = mtime !== lastEnrollmentMtimeMs;
606
+ // First observation only records the baseline; a later different mtime is a re-enroll.
607
+ enrollmentChanged = lastEnrollmentMtimeMs !== 0 && mtime !== lastEnrollmentMtimeMs;
601
608
  lastEnrollmentMtimeMs = mtime;
602
609
  }
603
610
  catch { /* no config file — the tick cadence below applies */ }
604
- credRecoveryTicks += 1;
605
- if (!enrollmentChanged && credRecoveryTicks > 3 && credRecoveryTicks % 12 !== 0)
611
+ const missing = !(creds.shieldId && creds.shieldKey);
612
+ if (!missing && !enrollmentChanged)
606
613
  return;
614
+ if (missing) {
615
+ credRecoveryTicks += 1;
616
+ if (!enrollmentChanged && credRecoveryTicks > 3 && credRecoveryTicks % 12 !== 0)
617
+ return;
618
+ }
607
619
  try {
608
620
  const fresh = (0, config_1.resolveCliCredentials)((0, config_1.loadConfig)(args.config), {
609
621
  shieldId: args.shieldId,
610
622
  shieldKey: args.shieldKey,
611
623
  apiUrl: args.apiUrl,
612
624
  });
613
- if (fresh.shieldId && fresh.shieldKey) {
625
+ if (!fresh.shieldId || !fresh.shieldKey)
626
+ return;
627
+ const rotated = fresh.shieldId !== creds.shieldId || fresh.shieldKey !== creds.shieldKey || fresh.apiUrl !== creds.apiUrl;
628
+ if (missing) {
614
629
  Object.assign(creds, fresh);
615
630
  log('Credentials recovered — telemetry and control-plane sync restored.');
616
631
  }
632
+ else if (rotated) {
633
+ Object.assign(creds, fresh);
634
+ log(`Enrollment changed on disk — reloaded credentials (shield ${fresh.shieldId}); telemetry and control-plane sync continue on the new key.`);
635
+ }
617
636
  }
618
637
  catch { /* next tick */ }
619
638
  };
@@ -0,0 +1,117 @@
1
+ import type { BotGuardConfig } from '../config';
2
+ import { type LocalSafetySnapshot } from '../localSafetySnapshot';
3
+ export interface DemoActionsArgs {
4
+ live?: string;
5
+ json?: string;
6
+ demoPolicies?: string;
7
+ timeout?: string;
8
+ }
9
+ export type DemoEvent = 'shell' | 'mcp' | 'read' | 'file';
10
+ export interface DemoScenario {
11
+ id: string;
12
+ /** One line a security lead understands. */
13
+ title: string;
14
+ event: DemoEvent;
15
+ /** Payload in the exact shape the IDE hook receives. */
16
+ payload: Record<string, unknown>;
17
+ /** What the scenario is meant to show. */
18
+ why: string;
19
+ }
20
+ export interface DemoScenarioResult {
21
+ id: string;
22
+ title: string;
23
+ event: DemoEvent;
24
+ action: string;
25
+ permission: 'allow' | 'deny' | 'ask' | 'unknown';
26
+ reason?: string;
27
+ /**
28
+ * Catalog rules that matched at WARN level (recorded as findings, action allowed). Shown so
29
+ * an allowed credential read is not mistaken for "not noticed": the org can flip these
30
+ * rules to block in the console. Offline replay only.
31
+ */
32
+ flagged?: string[];
33
+ /** Monitor/shadow machines allow and report what WOULD have been blocked. */
34
+ wouldBlock?: boolean;
35
+ latencyMs: number;
36
+ raw?: Record<string, unknown>;
37
+ }
38
+ export interface DemoActionsReport {
39
+ mode: 'offline' | 'live';
40
+ policySource: 'machine-cache' | 'demo-policies';
41
+ machineMode: 'block' | 'monitor' | 'shadow' | 'unknown';
42
+ policyCount: number;
43
+ shieldId: string;
44
+ scenarios: DemoScenarioResult[];
45
+ summary: {
46
+ allow: number;
47
+ deny: number;
48
+ ask: number;
49
+ wouldBlock: number;
50
+ unknown: number;
51
+ };
52
+ }
53
+ /**
54
+ * Policies seeded when the machine has no cached bundle (not enrolled). Plain
55
+ * action policies in the shape the engine evaluates — labelled so the output
56
+ * cannot be mistaken for the org's real configuration.
57
+ */
58
+ export declare const DEMO_POLICIES: ({
59
+ id: string;
60
+ name: string;
61
+ resourceType: string;
62
+ rules: {
63
+ operations: string[];
64
+ verdict: string;
65
+ }[];
66
+ } | {
67
+ id: string;
68
+ name: string;
69
+ resourceType: string;
70
+ rules: {
71
+ operations: string[];
72
+ verdict: string;
73
+ constraints: {
74
+ field: string;
75
+ operator: string;
76
+ value: string;
77
+ }[];
78
+ }[];
79
+ })[];
80
+ export declare const DEMO_SCENARIOS: DemoScenario[];
81
+ /**
82
+ * Build the throw-away HOME for an offline replay. Copies this machine's cached
83
+ * policy bundle when there is one (refreshed so the hook does not try to
84
+ * revalidate against the dead API), otherwise seeds the demo policy set.
85
+ */
86
+ export interface OfflineHome {
87
+ home: string;
88
+ policySource: 'machine-cache' | 'demo-policies';
89
+ shieldId: string;
90
+ /** Machine mode carried over from the cache (`monitor` machines record would-block instead of denying). */
91
+ mode: 'block' | 'monitor' | 'shadow';
92
+ /** Number of org action policies in the replayed cache (0 = enrolled org without policies yet). */
93
+ policyCount: number;
94
+ }
95
+ export declare function prepareOfflineHome(realHome: string, shieldId: string, forceDemoPolicies?: boolean): OfflineHome;
96
+ interface RunOptions {
97
+ cliPath: string;
98
+ home: string;
99
+ /** Offline replay: explicit sandbox credentials + dead API. Live: absent — the hook loads the machine's real config (incl. DPAPI keys). */
100
+ sandbox?: {
101
+ apiUrl: string;
102
+ shieldId: string;
103
+ shieldKey: string;
104
+ };
105
+ timeoutMs: number;
106
+ developerId: string;
107
+ }
108
+ export declare function runScenario(scenario: DemoScenario, opts: RunOptions): DemoScenarioResult;
109
+ /**
110
+ * Which catalog rules matched at WARN level for a scenario — the same scan the hook ran
111
+ * (same guard, same snapshot) — so the report can show "allowed, but flagged". The hook
112
+ * spools these as findings; it does not put them in the verdict it hands the IDE.
113
+ */
114
+ export declare function flaggedRules(scenario: DemoScenario, snapshot: LocalSafetySnapshot | undefined): string[];
115
+ export declare function renderReport(report: DemoActionsReport): string;
116
+ export declare function demoActionsCommand(args: DemoActionsArgs, config: BotGuardConfig): Promise<void>;
117
+ export {};