fullcourtdefense-cli 1.35.7 → 1.35.9
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/actionIdentity.js +2 -1
- package/dist/actionPolicyEngine.d.ts +38 -0
- package/dist/actionPolicyEngine.js +383 -24
- package/dist/approvalPrompt.js +2 -2
- package/dist/askDialog.js +5 -4
- package/dist/blockExplanation.js +2 -0
- package/dist/callerEnv.d.ts +33 -0
- package/dist/callerEnv.js +90 -0
- package/dist/commandNarrative.js +111 -4
- package/dist/commands/daemon.js +1 -1
- package/dist/commands/deterministicGuard.d.ts +32 -0
- package/dist/commands/deterministicGuard.js +214 -19
- package/dist/commands/hook.d.ts +34 -1
- package/dist/commands/hook.js +188 -31
- package/dist/commands/mcpGateway.js +2 -0
- package/dist/commands/taintLedger.js +3 -1
- package/dist/commands/workloadProtect.js +6 -2
- package/dist/devConfirm.d.ts +28 -12
- package/dist/devConfirm.js +134 -48
- package/dist/hookSlim.d.ts +1 -1
- package/dist/hookSlim.js +22 -2
- package/dist/nativeHook.d.ts +2 -1
- package/dist/nativeHook.js +109 -3
- package/dist/notify.js +4 -0
- 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 +20 -0
- package/dist/telemetry.js +7 -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/verdictIpc.js +5 -2
- package/dist/verdictIpcClient.d.ts +11 -1
- package/dist/verdictIpcClient.js +12 -2
- package/dist/version.json +1 -1
- package/package.json +9 -6
package/dist/askDialog.js
CHANGED
|
@@ -31,13 +31,14 @@ exports.resolveAskWithDialog = resolveAskWithDialog;
|
|
|
31
31
|
const devConfirm_1 = require("./devConfirm");
|
|
32
32
|
const telemetry_1 = require("./telemetry");
|
|
33
33
|
const hookIo_1 = require("./hookIo");
|
|
34
|
+
const callerEnv_1 = require("./callerEnv");
|
|
34
35
|
/** Field on the verdict JSON that carries the envelope. Stripped before the IDE sees the verdict. */
|
|
35
36
|
exports.ASK_ENVELOPE_FIELD = 'fcdAsk';
|
|
36
37
|
/** Default wait for the person. Cursor's hook window is 90 s, Claude's 60 s by default; stay inside both. */
|
|
37
38
|
exports.ASK_DIALOG_TIMEOUT_MS = 55_000;
|
|
38
39
|
/** Which channel asks the developer. `dialog` (default) = our OS dialog, `ide` = the IDE's own card. */
|
|
39
40
|
function askChannel() {
|
|
40
|
-
return (
|
|
41
|
+
return ((0, callerEnv_1.callerEnv)().FCD_ASK_CHANNEL || '').trim().toLowerCase() === 'ide' ? 'ide' : 'dialog';
|
|
41
42
|
}
|
|
42
43
|
/** Attach the envelope to a decision object (core side). */
|
|
43
44
|
function attachAskEnvelope(decision, envelope) {
|
|
@@ -142,7 +143,7 @@ async function resolveAskWithDialog(verdict, options = {}) {
|
|
|
142
143
|
const grantLimits = envelope.grantLimits === null ? undefined : (envelope.grantLimits || { ttlMs: devConfirm_1.GRANT_TTL_MS });
|
|
143
144
|
const allowAlways = envelope.remember !== 'none' && Boolean(grantHash) && Boolean(grantLimits)
|
|
144
145
|
&& (envelope.scope.event !== 'shell' || (envelope.scope.programs || []).length > 0);
|
|
145
|
-
const scopeLabel = allowAlways ? (0, devConfirm_1.describeGrantScope)(envelope.scope) : undefined;
|
|
146
|
+
const scopeLabel = allowAlways ? (0, devConfirm_1.describeGrantScope)(envelope.scope, { policyId: envelope.policyId, policyName: envelope.policyName }) : undefined;
|
|
146
147
|
const limitsLabel = allowAlways && grantLimits ? (0, devConfirm_1.describeGrantLimits)(grantLimits) : undefined;
|
|
147
148
|
const dialog = options.dialog || devConfirm_1.confirmNativeActionDetailed;
|
|
148
149
|
const reason = options.clientLabel ? `IDE: ${options.clientLabel}\n${envelope.reason}` : envelope.reason;
|
|
@@ -172,7 +173,7 @@ async function resolveAskWithDialog(verdict, options = {}) {
|
|
|
172
173
|
toolName: envelope.toolName,
|
|
173
174
|
operation: envelope.operation,
|
|
174
175
|
reason: granted
|
|
175
|
-
? `developer chose Always allow in the FullCourtDefense dialog — ${scopeLabel} (${limitsLabel}
|
|
176
|
+
? `developer chose Always allow in the FullCourtDefense dialog — ${scopeLabel} (${limitsLabel})${policyLabel}`
|
|
176
177
|
: `developer confirmed in the FullCourtDefense dialog${policyLabel}`,
|
|
177
178
|
ruleId: envelope.policyId,
|
|
178
179
|
policyHash: envelope.policyHash,
|
|
@@ -182,7 +183,7 @@ async function resolveAskWithDialog(verdict, options = {}) {
|
|
|
182
183
|
});
|
|
183
184
|
(0, telemetry_1.triggerFlush)(false);
|
|
184
185
|
const agentMsg = granted
|
|
185
|
-
? `FullCourtDefense: the developer allowed this ${envelope.event} and chose "Always allow" for ${scopeLabel} (${limitsLabel})
|
|
186
|
+
? `FullCourtDefense: the developer allowed this ${envelope.event} and chose "Always allow" for ${scopeLabel} (${limitsLabel}).`
|
|
186
187
|
: `FullCourtDefense: the developer allowed this ${envelope.event} once in the FullCourtDefense dialog.`;
|
|
187
188
|
return { ...clean, stdout: JSON.stringify(rewriteDecision(decision, true, '', agentMsg)), exitCode: 0, resolution: outcome };
|
|
188
189
|
}
|
package/dist/blockExplanation.js
CHANGED
|
@@ -84,6 +84,8 @@ function scopeWords(scope, developerName) {
|
|
|
84
84
|
return '🌐 company default — applies to every machine';
|
|
85
85
|
if (scope === 'machine' || scope === 'developer' || scope === 'local')
|
|
86
86
|
return `📍 local rule — only ${developerName || 'this machine'}`;
|
|
87
|
+
if (scope === 'shield')
|
|
88
|
+
return '📍 shield rule — only the machines on the shield it was saved on, or the ones it names';
|
|
87
89
|
return `${scope} policy`;
|
|
88
90
|
}
|
|
89
91
|
const FACT_WORDS = [
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The environment of the process that ASKED for a verdict (#1466).
|
|
3
|
+
*
|
|
4
|
+
* A hook answered by the resident daemon is evaluated inside the daemon's
|
|
5
|
+
* process, whose own environment belongs to a scheduled task — not to the IDE
|
|
6
|
+
* session that spawned the hook. Every fact the decision reads from the
|
|
7
|
+
* environment (is a human present, which CI / ephemeral markers are set, the
|
|
8
|
+
* session identity, the IDE's hook budget) must be the CALLER's. The thin
|
|
9
|
+
* client sends a snapshot of exactly these keys; the daemon evaluates the
|
|
10
|
+
* request with them in place of its own values, per request.
|
|
11
|
+
*
|
|
12
|
+
* Node built-ins only: this leaf is on the slim hook's warm-path module graph.
|
|
13
|
+
*/
|
|
14
|
+
export declare const CALLER_ENV_KEYS: readonly ["FCD_HUMAN_PRESENT", "FCD_EPHEMERAL", "FULLCOURTDEFENSE_EPHEMERAL", "CI", "GITHUB_ACTIONS", "GITLAB_CI", "TF_BUILD", "JENKINS_URL", "BUILDKITE", "CIRCLECI", "DISPLAY", "WAYLAND_DISPLAY", "FCD_ASK_CHANNEL", "FCD_SESSION_ID", "CLAUDE_CODE_SESSION_ID", "CODEX_THREAD_ID", "FCD_RUN_ID", "GITHUB_RUN_ID", "CI_PIPELINE_ID", "FCD_WORKLOAD_INSTANCE_ID", "CLAUDECODE", "CLAUDE_PROJECT_DIR", "VSCODE_PID", "TERM_PROGRAM", "WORKSPACE_PATH", "FCD_HOOK_IDE_TIMEOUT_MS", "FCD_TAINT_APPROVE_ONCE_DISABLED", "FCD_TAINT_APPROVAL_TIMEOUT_MS"];
|
|
15
|
+
export type CallerEnvSnapshot = Partial<Record<(typeof CALLER_ENV_KEYS)[number], string>>;
|
|
16
|
+
/** The keys above that are set in `env` — what the thin client sends. */
|
|
17
|
+
export declare function snapshotCallerEnv(env?: NodeJS.ProcessEnv): CallerEnvSnapshot;
|
|
18
|
+
/** The environment decision facts are read from: the requesting caller's inside a daemon request, else this process's. */
|
|
19
|
+
export declare function callerEnv(): NodeJS.ProcessEnv;
|
|
20
|
+
/**
|
|
21
|
+
* Run `fn` with the caller's snapshot in place of this process's values for
|
|
22
|
+
* CALLER_ENV_KEYS — a key the caller did not send is UNSET, never inherited
|
|
23
|
+
* from the daemon. No snapshot (older client) = this process's env, as before.
|
|
24
|
+
*/
|
|
25
|
+
export declare function withCallerEnv<T>(snapshot: unknown, fn: () => T): T;
|
|
26
|
+
/** CI / pipeline environments — no human at a keyboard. Shared by hooks and discovery. */
|
|
27
|
+
export declare function isCiEnvironment(env?: NodeJS.ProcessEnv): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Whether a developer can answer an IDE permission prompt on this machine. False on CI runners
|
|
30
|
+
* and on ephemeral workloads (`FCD_EPHEMERAL=1`, exported by `fcd workload-protect`) — there a
|
|
31
|
+
* developer-scoped approval must go to the org queue or block, never self-resolve.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isHumanPresent(env?: NodeJS.ProcessEnv): boolean;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CALLER_ENV_KEYS = void 0;
|
|
4
|
+
exports.snapshotCallerEnv = snapshotCallerEnv;
|
|
5
|
+
exports.callerEnv = callerEnv;
|
|
6
|
+
exports.withCallerEnv = withCallerEnv;
|
|
7
|
+
exports.isCiEnvironment = isCiEnvironment;
|
|
8
|
+
exports.isHumanPresent = isHumanPresent;
|
|
9
|
+
const async_hooks_1 = require("async_hooks");
|
|
10
|
+
/**
|
|
11
|
+
* The environment of the process that ASKED for a verdict (#1466).
|
|
12
|
+
*
|
|
13
|
+
* A hook answered by the resident daemon is evaluated inside the daemon's
|
|
14
|
+
* process, whose own environment belongs to a scheduled task — not to the IDE
|
|
15
|
+
* session that spawned the hook. Every fact the decision reads from the
|
|
16
|
+
* environment (is a human present, which CI / ephemeral markers are set, the
|
|
17
|
+
* session identity, the IDE's hook budget) must be the CALLER's. The thin
|
|
18
|
+
* client sends a snapshot of exactly these keys; the daemon evaluates the
|
|
19
|
+
* request with them in place of its own values, per request.
|
|
20
|
+
*
|
|
21
|
+
* Node built-ins only: this leaf is on the slim hook's warm-path module graph.
|
|
22
|
+
*/
|
|
23
|
+
exports.CALLER_ENV_KEYS = [
|
|
24
|
+
// Human presence (isHumanPresent / isCiEnvironment).
|
|
25
|
+
'FCD_HUMAN_PRESENT', 'FCD_EPHEMERAL', 'FULLCOURTDEFENSE_EPHEMERAL',
|
|
26
|
+
'CI', 'GITHUB_ACTIONS', 'GITLAB_CI', 'TF_BUILD', 'JENKINS_URL', 'BUILDKITE', 'CIRCLECI',
|
|
27
|
+
// Whether a dialog can be shown in the caller's session.
|
|
28
|
+
'DISPLAY', 'WAYLAND_DISPLAY', 'FCD_ASK_CHANNEL',
|
|
29
|
+
// Action identity (captureActionIdentity).
|
|
30
|
+
'FCD_SESSION_ID', 'CLAUDE_CODE_SESSION_ID', 'CODEX_THREAD_ID', 'FCD_RUN_ID', 'GITHUB_RUN_ID', 'CI_PIPELINE_ID', 'FCD_WORKLOAD_INSTANCE_ID',
|
|
31
|
+
// Which client / workspace the verdict is written for.
|
|
32
|
+
'CLAUDECODE', 'CLAUDE_PROJECT_DIR', 'VSCODE_PID', 'TERM_PROGRAM', 'WORKSPACE_PATH',
|
|
33
|
+
// Human-wait budget and approve-once switches.
|
|
34
|
+
'FCD_HOOK_IDE_TIMEOUT_MS', 'FCD_TAINT_APPROVE_ONCE_DISABLED', 'FCD_TAINT_APPROVAL_TIMEOUT_MS',
|
|
35
|
+
];
|
|
36
|
+
/** The keys above that are set in `env` — what the thin client sends. */
|
|
37
|
+
function snapshotCallerEnv(env = process.env) {
|
|
38
|
+
const snapshot = {};
|
|
39
|
+
for (const key of exports.CALLER_ENV_KEYS) {
|
|
40
|
+
const value = env[key];
|
|
41
|
+
if (typeof value === 'string')
|
|
42
|
+
snapshot[key] = value;
|
|
43
|
+
}
|
|
44
|
+
return snapshot;
|
|
45
|
+
}
|
|
46
|
+
const requestEnv = new async_hooks_1.AsyncLocalStorage();
|
|
47
|
+
/** The environment decision facts are read from: the requesting caller's inside a daemon request, else this process's. */
|
|
48
|
+
function callerEnv() {
|
|
49
|
+
return requestEnv.getStore() ?? process.env;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Run `fn` with the caller's snapshot in place of this process's values for
|
|
53
|
+
* CALLER_ENV_KEYS — a key the caller did not send is UNSET, never inherited
|
|
54
|
+
* from the daemon. No snapshot (older client) = this process's env, as before.
|
|
55
|
+
*/
|
|
56
|
+
function withCallerEnv(snapshot, fn) {
|
|
57
|
+
if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot))
|
|
58
|
+
return fn();
|
|
59
|
+
const env = { ...process.env };
|
|
60
|
+
for (const key of exports.CALLER_ENV_KEYS) {
|
|
61
|
+
const value = snapshot[key];
|
|
62
|
+
if (typeof value === 'string')
|
|
63
|
+
env[key] = value;
|
|
64
|
+
else
|
|
65
|
+
delete env[key];
|
|
66
|
+
}
|
|
67
|
+
return requestEnv.run(env, fn);
|
|
68
|
+
}
|
|
69
|
+
/** CI / pipeline environments — no human at a keyboard. Shared by hooks and discovery. */
|
|
70
|
+
function isCiEnvironment(env = callerEnv()) {
|
|
71
|
+
return env.GITHUB_ACTIONS === 'true'
|
|
72
|
+
|| env.GITLAB_CI === 'true'
|
|
73
|
+
|| env.TF_BUILD === 'True' // Azure Pipelines
|
|
74
|
+
|| !!env.JENKINS_URL
|
|
75
|
+
|| !!env.BUILDKITE
|
|
76
|
+
|| !!env.CIRCLECI
|
|
77
|
+
|| env.CI === 'true';
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Whether a developer can answer an IDE permission prompt on this machine. False on CI runners
|
|
81
|
+
* and on ephemeral workloads (`FCD_EPHEMERAL=1`, exported by `fcd workload-protect`) — there a
|
|
82
|
+
* developer-scoped approval must go to the org queue or block, never self-resolve.
|
|
83
|
+
*/
|
|
84
|
+
function isHumanPresent(env = callerEnv()) {
|
|
85
|
+
if (env.FCD_EPHEMERAL === '1' || env.FCD_EPHEMERAL === 'true' || env.FULLCOURTDEFENSE_EPHEMERAL === '1')
|
|
86
|
+
return false;
|
|
87
|
+
if (env.FCD_HUMAN_PRESENT === '0' || env.FCD_HUMAN_PRESENT === 'false')
|
|
88
|
+
return false;
|
|
89
|
+
return !isCiEnvironment(env);
|
|
90
|
+
}
|
package/dist/commandNarrative.js
CHANGED
|
@@ -110,12 +110,12 @@ const GCLOUD_SERVICE_NAMES = {
|
|
|
110
110
|
secrets: 'Secret Manager', iam: 'IAM', projects: 'project', functions: 'Cloud Functions', container: 'GKE', logging: 'Cloud Logging',
|
|
111
111
|
artifacts: 'Artifact Registry', scheduler: 'Cloud Scheduler', tasks: 'Cloud Tasks', kms: 'Cloud KMS', builds: 'Cloud Build', auth: 'auth', config: 'config',
|
|
112
112
|
};
|
|
113
|
-
const GCLOUD_READ = /^(?:describe|list|ls|get-iam-policy|get-value|get-ancestors|read|check
|
|
113
|
+
const GCLOUD_READ = /^(?:describe|list|ls|get-iam-policy|get-value|get-ancestors|read|check)$/;
|
|
114
114
|
const GCLOUD_WRITE = /^(?:create|update|patch|replace|enable|disable|deploy|apply|start|stop|restart|resume|suspend|scale|promote|migrate|rollback|add-[a-z-]+|remove-[a-z-]+|set-[a-z-]+|unset|clear|activate|revoke|copy)$/;
|
|
115
115
|
function humanVerb(verb) {
|
|
116
116
|
const map = {
|
|
117
117
|
describe: 'Reads details of', list: 'Lists', ls: 'Lists', 'get-iam-policy': 'Reads the access policy of', 'get-value': 'Reads the setting',
|
|
118
|
-
read: 'Reads', check: 'Checks',
|
|
118
|
+
read: 'Reads', check: 'Checks',
|
|
119
119
|
create: 'Creates', update: 'Updates', patch: 'Updates', replace: 'Replaces', enable: 'Enables', disable: 'Disables', deploy: 'Deploys',
|
|
120
120
|
apply: 'Applies', start: 'Starts', stop: 'Stops', restart: 'Restarts', resume: 'Resumes', suspend: 'Suspends', scale: 'Scales',
|
|
121
121
|
promote: 'Promotes', migrate: 'Migrates', rollback: 'Rolls back', unset: 'Unsets', clear: 'Clears', activate: 'Activates', revoke: 'Revokes', copy: 'Copies',
|
|
@@ -135,12 +135,39 @@ function humanVerb(verb) {
|
|
|
135
135
|
return `Sets ${verb.slice(4).replace(/-/g, ' ')} on`;
|
|
136
136
|
return `Runs \`${verb}\` on`;
|
|
137
137
|
}
|
|
138
|
+
/** `gcloud secrets versions <verb>`: `access` reads the secret VALUE, `list` lists versions, `add` stores a new value. */
|
|
139
|
+
function describeSecretVersions(toks, verbAt, where) {
|
|
140
|
+
const verb = toks[verbAt] || '';
|
|
141
|
+
const ops = operandsAfter(toks, verbAt + 1);
|
|
142
|
+
const flagSecret = flagValue(toks, '--secret');
|
|
143
|
+
const secret = (name) => (name ? `secret ${shortValue(name, 60)}` : 'a secret');
|
|
144
|
+
if (verb === 'access') {
|
|
145
|
+
return `Reads the value of ${secret(flagSecret)} (version ${shortValue(ops[0] || 'latest', 20)}) from Secret Manager${where}`;
|
|
146
|
+
}
|
|
147
|
+
if (verb === 'list')
|
|
148
|
+
return `Lists the versions of ${secret(ops[0])} in Secret Manager${where}`;
|
|
149
|
+
if (verb === 'add') {
|
|
150
|
+
const dataFile = flagValue(toks, '--data-file');
|
|
151
|
+
return `Adds a new version to ${secret(ops[0])} in Secret Manager${dataFile ? `, value from ${dataFile === '-' ? 'standard input' : shortValue(dataFile, 60)}` : ''}${where}`;
|
|
152
|
+
}
|
|
153
|
+
const version = ops[0] ? `version ${shortValue(ops[0], 20)} of ` : '';
|
|
154
|
+
if (verb === 'describe')
|
|
155
|
+
return `Reads details of ${version}${secret(flagSecret)} in Secret Manager${where}`;
|
|
156
|
+
const state = { destroy: 'Destroys', disable: 'Disables', enable: 'Enables' };
|
|
157
|
+
if (state[verb])
|
|
158
|
+
return `${state[verb]} ${version}${secret(flagSecret)} in Secret Manager${where}`;
|
|
159
|
+
return `Runs \`gcloud secrets versions ${verb}\`${where}`;
|
|
160
|
+
}
|
|
138
161
|
function describeGcloud(toks) {
|
|
139
162
|
let i = 1;
|
|
140
163
|
while (i < toks.length && toks[i].startsWith('-'))
|
|
141
164
|
i++;
|
|
142
165
|
if (/^(?:alpha|beta)$/.test(toks[i] || ''))
|
|
143
166
|
i++;
|
|
167
|
+
if (toks[i] === 'secrets' && toks[i + 1] === 'versions') {
|
|
168
|
+
const project = flagValue(toks, '--project');
|
|
169
|
+
return describeSecretVersions(toks, i + 2, project ? ` in project ${project}` : '');
|
|
170
|
+
}
|
|
144
171
|
const groups = [];
|
|
145
172
|
let verb;
|
|
146
173
|
for (; i < toks.length; i++) {
|
|
@@ -234,6 +261,79 @@ function describePipeTail(stages) {
|
|
|
234
261
|
return ', and writes the output to a file';
|
|
235
262
|
return '';
|
|
236
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* The whole statement is one quoted string (`"len=$($t.Length)"`, `'done'`): its text
|
|
266
|
+
* with each `$( … )` sub-expression shown as `…`. Undefined when it is anything else.
|
|
267
|
+
*/
|
|
268
|
+
function bareStringText(head) {
|
|
269
|
+
const value = head.trim();
|
|
270
|
+
const q = value[0];
|
|
271
|
+
if ((q !== '"' && q !== "'") || value.length < 2 || value[value.length - 1] !== q)
|
|
272
|
+
return undefined;
|
|
273
|
+
let depth = 0;
|
|
274
|
+
let text = '';
|
|
275
|
+
for (let k = 1; k < value.length - 1; k++) {
|
|
276
|
+
const ch = value[k];
|
|
277
|
+
if (q === '"' && ch === '`') {
|
|
278
|
+
text += value[++k] ?? '';
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (q === '"' && ch === '$' && value[k + 1] === '(') {
|
|
282
|
+
if (depth++ === 0)
|
|
283
|
+
text += '…';
|
|
284
|
+
k++;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (depth > 0) {
|
|
288
|
+
if (ch === '(')
|
|
289
|
+
depth++;
|
|
290
|
+
else if (ch === ')')
|
|
291
|
+
depth--;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (ch === q) {
|
|
295
|
+
if (q === "'" && value[k + 1] === "'") {
|
|
296
|
+
text += "'";
|
|
297
|
+
k++;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
text += ch;
|
|
303
|
+
}
|
|
304
|
+
return depth === 0 ? text.trim() : undefined;
|
|
305
|
+
}
|
|
306
|
+
/** `((gcloud … $p) -join '').Trim()` -> `gcloud … $p`: the command inside nested groups (quote-aware). */
|
|
307
|
+
function peelGroups(text) {
|
|
308
|
+
let value = text.trim();
|
|
309
|
+
for (let guard = 0; guard < 8 && value.startsWith('('); guard++) {
|
|
310
|
+
let depth = 0;
|
|
311
|
+
let end = -1;
|
|
312
|
+
let quote = '';
|
|
313
|
+
for (let k = 0; k < value.length; k++) {
|
|
314
|
+
const ch = value[k];
|
|
315
|
+
if (quote) {
|
|
316
|
+
if (ch === quote)
|
|
317
|
+
quote = '';
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (ch === '"' || ch === "'") {
|
|
321
|
+
quote = ch;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (ch === '(')
|
|
325
|
+
depth++;
|
|
326
|
+
else if (ch === ')' && --depth === 0) {
|
|
327
|
+
end = k;
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (end < 0)
|
|
332
|
+
break;
|
|
333
|
+
value = value.slice(1, end).trim();
|
|
334
|
+
}
|
|
335
|
+
return value;
|
|
336
|
+
}
|
|
237
337
|
function describeOne(segmentIn) {
|
|
238
338
|
const segment = segmentIn.trim();
|
|
239
339
|
const [head, ...pipe] = splitPipe(segment);
|
|
@@ -270,10 +370,14 @@ function describeOne(segmentIn) {
|
|
|
270
370
|
}
|
|
271
371
|
return step(`Sets variable ${name}${interpolates ? ' from other values' : ` to ${shortValue(bare, 60)}`}`);
|
|
272
372
|
}
|
|
273
|
-
const inner = rhs.
|
|
373
|
+
const inner = rhs.startsWith('(') ? peelGroups(rhs) : rhs.replace(/\)\s*(?:\.\w+\(\))*$/, '');
|
|
274
374
|
const innerStep = describeOne(inner);
|
|
275
375
|
return { segment, text: shortValue(`Stores in ${name}: ${innerStep.text.charAt(0).toLowerCase()}${innerStep.text.slice(1)}${tail}`, MAX_LINE), known: innerStep.known };
|
|
276
376
|
}
|
|
377
|
+
// A bare string statement: PowerShell writes it to the output (`"len=$($t.Length)"`).
|
|
378
|
+
const bare = bareStringText(head);
|
|
379
|
+
if (bare !== undefined)
|
|
380
|
+
return step(bare ? `Prints text: ${shortValue(bare, 80)}` : 'Prints an empty line');
|
|
277
381
|
const toks = tokens(head);
|
|
278
382
|
if (toks.length === 0)
|
|
279
383
|
return step('(empty)', false);
|
|
@@ -531,7 +635,10 @@ function describeOne(segmentIn) {
|
|
|
531
635
|
default:
|
|
532
636
|
if (/^\$/.test(programToken))
|
|
533
637
|
return step(pipe.length ? `Uses ${shortValue(programToken, 40)}${tail}` : `Reads variable ${shortValue(programToken, 40)}`, false);
|
|
534
|
-
|
|
638
|
+
{
|
|
639
|
+
const shown = shortValue([program, rest[1]].filter(t => t && !t.startsWith('-')).join(' '), 50);
|
|
640
|
+
return step(shown ? `Runs \`${shown}\`` : 'Runs a command this dialog cannot describe', false);
|
|
641
|
+
}
|
|
535
642
|
}
|
|
536
643
|
}
|
|
537
644
|
/** Split one step on top-level `|` (quote-aware). */
|
package/dist/commands/daemon.js
CHANGED
|
@@ -1593,7 +1593,7 @@ async function runDaemon(args, config) {
|
|
|
1593
1593
|
// to full local evaluation whenever this server is down — never a blocker.
|
|
1594
1594
|
let verdictServer;
|
|
1595
1595
|
try {
|
|
1596
|
-
verdictServer = (0, verdictIpc_1.startVerdictServer)((hookArgs, stdin) => (0, hook_1.evaluateHookRequest)(hookArgs, stdin, (0, config_1.loadConfig)()), log);
|
|
1596
|
+
verdictServer = (0, verdictIpc_1.startVerdictServer)((hookArgs, stdin) => (0, hook_1.evaluateHookRequest)(hookArgs, stdin, (0, config_1.loadConfig)(), { viaDaemon: true }), log);
|
|
1597
1597
|
}
|
|
1598
1598
|
catch (error) {
|
|
1599
1599
|
log(`Verdict IPC: failed to start (${error.message}) — hooks will evaluate locally.`);
|
|
@@ -106,6 +106,38 @@ export interface SharedCommandRule {
|
|
|
106
106
|
* see DEFAULT_DISABLED_BUILT_INS). Ids match local-safety-catalog.ts.
|
|
107
107
|
*/
|
|
108
108
|
export declare const CREDENTIAL_COMMAND_RULES: readonly SharedCommandRule[];
|
|
109
|
+
export type ShellAstMode = 'off' | 'shadow' | 'primary';
|
|
110
|
+
/** Structural subset of shellAst.ts ShellFacts that the guard reads. */
|
|
111
|
+
export interface ShellAstFactsLike {
|
|
112
|
+
complete: boolean;
|
|
113
|
+
incompleteReason?: string;
|
|
114
|
+
dynamicExecution?: boolean;
|
|
115
|
+
commands: Array<{
|
|
116
|
+
name: string;
|
|
117
|
+
argv: string[];
|
|
118
|
+
literal: boolean[];
|
|
119
|
+
cwdScope: 'scoped' | 'unscoped' | 'unknown';
|
|
120
|
+
wrapper?: {
|
|
121
|
+
kind: string;
|
|
122
|
+
program: string;
|
|
123
|
+
inner?: ShellAstFactsLike;
|
|
124
|
+
};
|
|
125
|
+
}>;
|
|
126
|
+
}
|
|
127
|
+
export interface ShellAstComparison {
|
|
128
|
+
mode: ShellAstMode;
|
|
129
|
+
complete: boolean;
|
|
130
|
+
/** Destructive-rm item id the regex floor produced (undefined = clean). */
|
|
131
|
+
regex?: string;
|
|
132
|
+
/** Destructive-rm item id the AST produced (undefined = clean). */
|
|
133
|
+
ast?: string;
|
|
134
|
+
agree: boolean;
|
|
135
|
+
}
|
|
136
|
+
export declare function setShellAstProvider(provider: ((command: string) => ShellAstFactsLike | undefined) | undefined): void;
|
|
137
|
+
export declare function setShellAstMode(mode: ShellAstMode): void;
|
|
138
|
+
export declare function getShellAstMode(): ShellAstMode;
|
|
139
|
+
/** Shadow telemetry: the comparison recorded by the most recent shell scan (consumed once). */
|
|
140
|
+
export declare function takeShellAstComparison(): ShellAstComparison | undefined;
|
|
109
141
|
/**
|
|
110
142
|
* Code-only view of a line: each `'…'` / `"…"` / `` `…` `` span becomes a
|
|
111
143
|
* space. Detectors that ask "does this EXECUTE" must look here; the raw line
|