fullcourtdefense-cli 1.35.6 → 1.35.8
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/actionPolicyEngine.d.ts +37 -0
- package/dist/actionPolicyEngine.js +180 -10
- package/dist/commands/deterministicGuard.d.ts +32 -0
- package/dist/commands/deterministicGuard.js +208 -18
- package/dist/commands/hook.js +20 -2
- package/dist/commands/mcpGateway.js +2 -0
- package/dist/commands/workloadProtect.js +6 -2
- package/dist/devConfirm.d.ts +32 -1
- package/dist/devConfirm.js +26 -2
- package/dist/shellAst.d.ts +117 -0
- package/dist/shellAst.js +590 -0
- package/dist/shellAstLoader.d.ts +32 -0
- package/dist/shellAstLoader.js +271 -0
- package/dist/telemetry.d.ts +18 -0
- package/dist/telemetry.js +1 -0
- package/dist/vendor/tree-sitter/LICENSE.tree-sitter-bash +21 -0
- package/dist/vendor/tree-sitter/LICENSE.web-tree-sitter +21 -0
- package/dist/vendor/tree-sitter/VERSIONS.json +4 -0
- package/dist/vendor/tree-sitter/tree-sitter-bash.wasm +0 -0
- package/dist/vendor/tree-sitter/tree-sitter.cjs +4031 -0
- package/dist/vendor/tree-sitter/tree-sitter.wasm +0 -0
- package/dist/version.json +1 -1
- package/package.json +7 -4
package/dist/commands/hook.js
CHANGED
|
@@ -69,6 +69,7 @@ const askDialog_1 = require("../askDialog");
|
|
|
69
69
|
const hookIo_1 = require("../hookIo");
|
|
70
70
|
const fileWriteCanon_1 = require("../fileWriteCanon");
|
|
71
71
|
const discoverProxy_1 = require("./discoverProxy");
|
|
72
|
+
const shellAstLoader_1 = require("../shellAstLoader");
|
|
72
73
|
function inferEvent(explicit, payload) {
|
|
73
74
|
const e = (explicit || '').toLowerCase();
|
|
74
75
|
if (e) {
|
|
@@ -552,6 +553,7 @@ function spoolCallEvent(call, rest) {
|
|
|
552
553
|
toolName: call.toolName,
|
|
553
554
|
operation: rest.operation || operation,
|
|
554
555
|
evidence: rest.evidence || (0, telemetry_1.evidenceFromToolArgs)(call.toolArgs),
|
|
556
|
+
shellAst: rest.shellAst || (0, shellAstLoader_1.collectShellAstFacts)(),
|
|
555
557
|
});
|
|
556
558
|
}
|
|
557
559
|
/**
|
|
@@ -820,7 +822,9 @@ function prepareDeveloperConfirmation(event, call, payload, source) {
|
|
|
820
822
|
// this exact rule (same rule hash). Rules that forbid memory never reach here.
|
|
821
823
|
// The caps in force come from the rule and the org ceiling in the cached bundle;
|
|
822
824
|
// an org that switched "Always allow" off (`null`) honours no grant at all.
|
|
823
|
-
|
|
825
|
+
// The grant binds the facts THIS rule fired on: a credential-command rule is
|
|
826
|
+
// about the program, an egress / SSRF / exfil rule is about the host too.
|
|
827
|
+
const grantScope = (0, devConfirm_1.grantScopeFor)(event, call.toolName, call.toolArgs, operation, context, (0, devConfirm_1.grantRuleFactsFor)(source));
|
|
824
828
|
const grantLimits = (0, devConfirm_1.resolveGrantLimits)(source.grantCaps, (0, runtimeConfig_1.getCachedDeveloperGrantPolicy)());
|
|
825
829
|
const grant = source.remember === 'none' ? undefined : (0, devConfirm_1.findDeveloperGrant)(grantScope, { policyId: source.policyId, matchedRule: source.matchedRule, policyHash: memoryHash }, grantLimits);
|
|
826
830
|
if (grant) {
|
|
@@ -962,7 +966,7 @@ function tryLocalPolicyEnforcement(ctx, call, detail, opts = {}) {
|
|
|
962
966
|
});
|
|
963
967
|
const declaredOperations = (0, actionPolicyEngine_1.deriveToolOperations)(call.toolName);
|
|
964
968
|
const local = (0, actionPolicyEngine_1.evaluateActionPolicies)(policies, call.toolName, operation, context, declaredOperations);
|
|
965
|
-
(0, hookIo_1.dbg)({ phase: 'policy_local_verdict', event: ctx.event, tool: call.toolName, operation, verdict: local.verdict, policyName: local.policyName, detail });
|
|
969
|
+
(0, hookIo_1.dbg)({ phase: 'policy_local_verdict', event: ctx.event, tool: call.toolName, operation, verdict: local.verdict, policyName: local.policyName, detail, astMode: (0, actionPolicyEngine_1.getShellAstMode)() });
|
|
966
970
|
// Developer-scoped approvals need no backend at all — the IDE prompt IS the approver.
|
|
967
971
|
if (tryDeveloperConfirmation(ctx, call, local, ctx.payload, 'offline'))
|
|
968
972
|
return true;
|
|
@@ -1237,6 +1241,9 @@ function processIo(stdin, stdinErr) {
|
|
|
1237
1241
|
* an allow verdict, never an error the thin client would surface.
|
|
1238
1242
|
*/
|
|
1239
1243
|
async function evaluateHookRequest(args, stdinRaw, config) {
|
|
1244
|
+
// Structural shell facts (shadow by default). A missing/slow parser means
|
|
1245
|
+
// "no facts" and the regex floor decides alone — never a wait, never fail-open.
|
|
1246
|
+
await (0, shellAstLoader_1.ensureShellAst)();
|
|
1240
1247
|
return evaluateBuffered(args, stdinRaw, '', config, { isTTY: false, exitsProcess: false });
|
|
1241
1248
|
}
|
|
1242
1249
|
/**
|
|
@@ -1317,6 +1324,11 @@ async function hookCommandOuter(args, config, startedAt) {
|
|
|
1317
1324
|
// machine that silently stopped using the fast path is diagnosable.
|
|
1318
1325
|
(0, hookIo_1.dbg)({ phase: 'ipc_unavailable', event: args.event, detail: ipc.detail });
|
|
1319
1326
|
}
|
|
1327
|
+
// Structural shell facts for the local evaluation (shadow by default; the
|
|
1328
|
+
// daemon path above initialises its own copy once). Bounded: a slow or
|
|
1329
|
+
// missing parser resolves to "no facts" and the regex floor decides alone.
|
|
1330
|
+
const astReady = await (0, shellAstLoader_1.ensureShellAst)();
|
|
1331
|
+
(0, hookIo_1.dbg)({ phase: 'shell_ast', ready: astReady, status: (0, shellAstLoader_1.shellAstStatus)() });
|
|
1320
1332
|
// Buffered so an `ask` can be put to the developer in OUR dialog before the
|
|
1321
1333
|
// IDE reads the verdict; exitsProcess stays true — this process ends right after.
|
|
1322
1334
|
const result = await evaluateBuffered(args, raw, stdinErr, config, { isTTY: !!process.stdin.isTTY, exitsProcess: true });
|
|
@@ -1454,9 +1466,11 @@ async function hookCommandInner(args, config, io) {
|
|
|
1454
1466
|
// this is instant when fresh and bounded (~1.5s) only when stale.
|
|
1455
1467
|
let machineSuspended = false;
|
|
1456
1468
|
let modeResolved = false;
|
|
1469
|
+
let runtimeBundleFlags = {};
|
|
1457
1470
|
if (shieldId) {
|
|
1458
1471
|
try {
|
|
1459
1472
|
const bundle = await (0, runtimeConfig_1.getRuntimeBundle)({ apiUrl, shieldId, shieldKey, developerName: developerId(), machineName: os.hostname(), hotPath: true });
|
|
1473
|
+
runtimeBundleFlags = bundle;
|
|
1460
1474
|
// Kill-switch holds even on a 'default' bundle: with the cache missing
|
|
1461
1475
|
// but a signed last-known-suspended marker standing, getRuntimeBundle
|
|
1462
1476
|
// stamps suspended on the default — deleting the cache file must not
|
|
@@ -1484,6 +1498,10 @@ async function hookCommandInner(args, config, io) {
|
|
|
1484
1498
|
}
|
|
1485
1499
|
catch { /* keep the local default */ }
|
|
1486
1500
|
}
|
|
1501
|
+
// Shell AST mode: FCD_SHELL_AST_MODE env, else the bundle's `shellAstMode`
|
|
1502
|
+
// (server-controlled rollout), else shadow — facts recorded, verdicts unchanged.
|
|
1503
|
+
(0, shellAstLoader_1.applyShellAstMode)((0, shellAstLoader_1.resolveShellAstMode)(process.env.FCD_SHELL_AST_MODE, runtimeBundleFlags.shellAstMode));
|
|
1504
|
+
(0, hookIo_1.dbg)({ phase: 'shell_ast_mode', mode: (0, actionPolicyEngine_1.getShellAstMode)(), fromEnv: process.env.FCD_SHELL_AST_MODE, fromBundle: runtimeBundleFlags.shellAstMode });
|
|
1487
1505
|
// Monitor-first: no authoritative bundle (fresh/uncached/fetch failed) => report-only.
|
|
1488
1506
|
if (!modeResolved && !forceEnforce) {
|
|
1489
1507
|
shadow = true;
|
|
@@ -74,6 +74,7 @@ const policyGateHealth_1 = require("../policyGateHealth");
|
|
|
74
74
|
const actionPolicyEngine_1 = require("../actionPolicyEngine");
|
|
75
75
|
const machineIdentity_1 = require("../machineIdentity");
|
|
76
76
|
const backupManifest_1 = require("../backupManifest");
|
|
77
|
+
const shellAstLoader_1 = require("../shellAstLoader");
|
|
77
78
|
const devConfirm_1 = require("../devConfirm");
|
|
78
79
|
const DEFAULT_API_URL = 'https://api.fullcourtdefense.ai';
|
|
79
80
|
const MANAGED_SERVER_NAME = 'agentguard-gateway';
|
|
@@ -2148,6 +2149,7 @@ class McpGatewayServer {
|
|
|
2148
2149
|
}
|
|
2149
2150
|
}
|
|
2150
2151
|
async function mcpGatewayCommand(args, config) {
|
|
2152
|
+
await (0, shellAstLoader_1.ensureShellAstReady)(); // resident process: load the parser once, before the first tool call (shadow by default)
|
|
2151
2153
|
// The gateway is a resident proxy in front of the IDE's MCP servers — if it
|
|
2152
2154
|
// crashes, EVERY protected tool goes down at once. Any escaped exception
|
|
2153
2155
|
// (detached flush, timer, transport edge case) logs one line and the proxy
|
|
@@ -66,7 +66,7 @@ function assertWorkloadSubstrate(kind, rt, options = {}) {
|
|
|
66
66
|
+ 'or pass --force true if this really is a disposable workload host.');
|
|
67
67
|
}
|
|
68
68
|
/** Connections the console has a section for; anything else is the generic snippet. */
|
|
69
|
-
const ENROLLED_VIA_VALUES = new Set(['heroku', 'kubernetes', 'aws', 'gcp', 'snippet']);
|
|
69
|
+
const ENROLLED_VIA_VALUES = new Set(['heroku', 'kubernetes', 'aws', 'gcp', 'azure', 'snippet']);
|
|
70
70
|
function resolveEnrolledVia(explicit, env, detected) {
|
|
71
71
|
for (const candidate of [explicit, env.FCD_ENROLLED_VIA, detected]) {
|
|
72
72
|
const clean = (candidate || '').trim().toLowerCase();
|
|
@@ -105,7 +105,11 @@ function detectWorkloadContext(env = process.env, fsProbe = fs) {
|
|
|
105
105
|
}
|
|
106
106
|
if (env.FUNCTIONS_WORKER_RUNTIME && env.WEBSITE_SITE_NAME) {
|
|
107
107
|
// Azure Functions
|
|
108
|
-
return { kind: 'serverless', name: explicitName || env.WEBSITE_SITE_NAME, environment, instanceId: env.WEBSITE_INSTANCE_ID, image };
|
|
108
|
+
return { kind: 'serverless', name: explicitName || env.WEBSITE_SITE_NAME, environment, instanceId: env.WEBSITE_INSTANCE_ID, image, enrolledVia: 'azure' };
|
|
109
|
+
}
|
|
110
|
+
if (env.CONTAINER_APP_NAME && env.CONTAINER_APP_REVISION) {
|
|
111
|
+
// Azure Container Apps: the app name is the stable identity, the replica the instance.
|
|
112
|
+
return { kind: 'container', name: explicitName || env.CONTAINER_APP_NAME, environment, instanceId: env.CONTAINER_APP_REPLICA_NAME || env.HOSTNAME, image, enrolledVia: 'azure' };
|
|
109
113
|
}
|
|
110
114
|
if (env.ECS_CONTAINER_METADATA_URI_V4 || env.ECS_CONTAINER_METADATA_URI) {
|
|
111
115
|
// ECS / Fargate task: the service name comes from the task definition env.
|
package/dist/devConfirm.d.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
package/dist/devConfirm.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell AST facts — the structural view of a shell command line that the
|
|
3
|
+
* deterministic guard and the action-policy engine consume as DATA.
|
|
4
|
+
*
|
|
5
|
+
* A tree-sitter-bash parse tree is walked ONCE into a flat, JSON-serializable
|
|
6
|
+
* list of simple commands in execution order. Each command carries its argv
|
|
7
|
+
* with quoting resolved, which words are fully literal, the cwd scope in force
|
|
8
|
+
* when it runs (tracked through `cd`/`pushd`, subshells, loops, functions), the
|
|
9
|
+
* nesting it sits in, its redirections, and — for wrappers such as `sudo`,
|
|
10
|
+
* `bash -c '…'`, `docker run … sh -c '…'` — the inner script parsed recursively.
|
|
11
|
+
*
|
|
12
|
+
* Contract (the part that keeps this safe to put on the decision path):
|
|
13
|
+
*
|
|
14
|
+
* 1. FAIL CLOSED. `complete` is true only when the tree has no ERROR / MISSING
|
|
15
|
+
* node and every node type met is in the explicit allow-list below. Any
|
|
16
|
+
* structure this module does not understand makes the whole result
|
|
17
|
+
* incomplete, and an incomplete result may never RELAX a verdict — the
|
|
18
|
+
* regex floor in the guard and the engine then decides alone. (Same
|
|
19
|
+
* property Codex's `codex-command` and the tree-sitter rewrite of Claude
|
|
20
|
+
* Code's bash safety take: never interpret structure you cannot prove.)
|
|
21
|
+
* 2. Deterministic, no I/O. This file has no imports; the parser is injected
|
|
22
|
+
* as a minimal node interface so the same bytes are vendored into the
|
|
23
|
+
* backend (console simulator) and stay byte-identical with the CLI.
|
|
24
|
+
* 3. Data, not code. The facts cross into the hot-updatable detector worker
|
|
25
|
+
* as JSON. Nothing here executes, expands or resolves anything.
|
|
26
|
+
*
|
|
27
|
+
* Why an AST at all: every false positive of the last months was structural —
|
|
28
|
+
* quoted text mistaken for a command, a heredoc body scanned as shell, a bare
|
|
29
|
+
* `rm -rf *` after `cd scratch/` read as a root wipe, a `$( )` boundary missed.
|
|
30
|
+
* A parser answers those once; a regex answers them one incident at a time.
|
|
31
|
+
*/
|
|
32
|
+
export interface ShellSyntaxNode {
|
|
33
|
+
type: string;
|
|
34
|
+
text: string;
|
|
35
|
+
startIndex: number;
|
|
36
|
+
endIndex: number;
|
|
37
|
+
childCount: number;
|
|
38
|
+
isMissing: boolean;
|
|
39
|
+
isNamed: boolean;
|
|
40
|
+
child(index: number): ShellSyntaxNode | null;
|
|
41
|
+
childForFieldName(name: string): ShellSyntaxNode | null;
|
|
42
|
+
}
|
|
43
|
+
export type CwdScope = 'scoped' | 'unscoped' | 'unknown';
|
|
44
|
+
export interface ShellRedirectFact {
|
|
45
|
+
op: string;
|
|
46
|
+
target: string;
|
|
47
|
+
literal: boolean;
|
|
48
|
+
}
|
|
49
|
+
export interface ShellWrapperFact {
|
|
50
|
+
kind: 'privilege' | 'shell' | 'eval' | 'exec' | 'env' | 'container' | 'timing' | 'xargs';
|
|
51
|
+
program: string;
|
|
52
|
+
/** The wrapped script when it is a literal string, parsed with the same walker. */
|
|
53
|
+
inner?: ShellFacts;
|
|
54
|
+
}
|
|
55
|
+
export interface ShellCommandFact {
|
|
56
|
+
/** argv[0] with quotes resolved; '' when it is dynamic (variable, substitution). */
|
|
57
|
+
name: string;
|
|
58
|
+
/** Words with quotes resolved. Dynamic parts keep their source text (`$X`, `$(…)`). */
|
|
59
|
+
argv: string[];
|
|
60
|
+
/** argv[i] is entirely literal: no expansion, substitution, glob-carrying variable. */
|
|
61
|
+
literal: boolean[];
|
|
62
|
+
/** Source text of the command including its redirects, EXCLUDING heredoc bodies. */
|
|
63
|
+
text: string;
|
|
64
|
+
start: number;
|
|
65
|
+
end: number;
|
|
66
|
+
/** cwd scope in force when this command runs. */
|
|
67
|
+
cwdScope: CwdScope;
|
|
68
|
+
depth: {
|
|
69
|
+
subshell: number;
|
|
70
|
+
function: number;
|
|
71
|
+
loop: number;
|
|
72
|
+
conditional: number;
|
|
73
|
+
substitution: number;
|
|
74
|
+
};
|
|
75
|
+
/** Members of one pipeline share an id (0 = not in a pipeline). */
|
|
76
|
+
pipeline: number;
|
|
77
|
+
redirects: ShellRedirectFact[];
|
|
78
|
+
hasHeredoc: boolean;
|
|
79
|
+
wrapper?: ShellWrapperFact;
|
|
80
|
+
}
|
|
81
|
+
export interface ShellFacts {
|
|
82
|
+
parser: 'tree-sitter-bash';
|
|
83
|
+
/** No parse errors and every node type understood. Only a complete result may relax a verdict. */
|
|
84
|
+
complete: boolean;
|
|
85
|
+
/** Why the result is incomplete (first reason). */
|
|
86
|
+
incompleteReason?: string;
|
|
87
|
+
commands: ShellCommandFact[];
|
|
88
|
+
/** Node types met that are not in the allow-list (for diagnostics / corpus growth). */
|
|
89
|
+
unknownNodeTypes: string[];
|
|
90
|
+
/** Any `eval`, `source`/`.`, or dynamic command name — structure the parse cannot see through. */
|
|
91
|
+
dynamicExecution: boolean;
|
|
92
|
+
}
|
|
93
|
+
/** What a `cd`/`pushd` target says about the cwd afterwards. */
|
|
94
|
+
export declare function cdTargetScope(target: string, literal: boolean): CwdScope;
|
|
95
|
+
/** A parsed inner script: its root plus the release of the parser's native tree. */
|
|
96
|
+
export interface ParsedShellTree {
|
|
97
|
+
root: ShellSyntaxNode;
|
|
98
|
+
release(): void;
|
|
99
|
+
}
|
|
100
|
+
export declare const MAX_SHELL_AST_SOURCE = 64000;
|
|
101
|
+
/**
|
|
102
|
+
* Walk a parsed tree into facts. `nesting` guards the recursion through
|
|
103
|
+
* literal `bash -c` scripts (depth 3 is plenty; deeper is treated as unknown).
|
|
104
|
+
*/
|
|
105
|
+
export declare function analyzeShellTree(root: ShellSyntaxNode, nesting?: number): ShellFacts;
|
|
106
|
+
/** Register the grammar-backed parser used for inner `bash -c` scripts. */
|
|
107
|
+
export declare function setInnerShellParser(parse: ((source: string) => ParsedShellTree | undefined) | undefined): void;
|
|
108
|
+
/** All commands, including those inside literal wrapper scripts, depth-first in execution order. */
|
|
109
|
+
export declare function flattenShellCommands(facts: ShellFacts): ShellCommandFact[];
|
|
110
|
+
/** True when every part of the analysis (including inner scripts) is complete. */
|
|
111
|
+
export declare function shellFactsFullyComplete(facts: ShellFacts | undefined): facts is ShellFacts;
|
|
112
|
+
/**
|
|
113
|
+
* Text that looks like PowerShell — tree-sitter-bash will often parse it
|
|
114
|
+
* WITHOUT errors into nonsense, so the loader refuses it up front and the
|
|
115
|
+
* PowerShell model in the engine keeps deciding alone.
|
|
116
|
+
*/
|
|
117
|
+
export declare function looksLikePowerShell(command: string): boolean;
|