fullcourtdefense-cli 1.35.8 → 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 +1 -0
- package/dist/actionPolicyEngine.js +203 -14
- 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.js +6 -1
- package/dist/commands/hook.d.ts +34 -1
- package/dist/commands/hook.js +171 -30
- package/dist/commands/taintLedger.js +3 -1
- 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/telemetry.d.ts +2 -0
- package/dist/telemetry.js +6 -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 +3 -3
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.`);
|
|
@@ -1219,6 +1219,11 @@ function interpreterCodeSpawnsProcess(before, code) {
|
|
|
1219
1219
|
return true;
|
|
1220
1220
|
return BACKTICK_SHELL_INTERPRETER.test(before) && /`[^`]*`|%x[\[{(]/.test(code);
|
|
1221
1221
|
}
|
|
1222
|
+
// An HTTP client in the same code makes a metadata endpoint literal a request target, not prose.
|
|
1223
|
+
const CODE_OPENS_REQUEST = /\b(?:urlopen|Request|requests|httpx|urllib3|aiohttp|http\.client|HTTPS?Connection|fetch|axios|got|XMLHttpRequest|https?\.(?:get|request)|Net::HTTP|LWP|HTTP::Tiny|URI\.open|curl_exec|file_get_contents|fsockopen|socket)\b/;
|
|
1224
|
+
function interpreterCodeRequestsMetadata(code) {
|
|
1225
|
+
return Boolean(containsMetadataEndpoint(code)) && CODE_OPENS_REQUEST.test(code);
|
|
1226
|
+
}
|
|
1222
1227
|
/** First path argument of direct file APIs in inline language code, not printed examples. */
|
|
1223
1228
|
function interpreterFileArguments(text) {
|
|
1224
1229
|
if (text.length > 32_000)
|
|
@@ -1306,7 +1311,7 @@ function stripInertDataSegments(text) {
|
|
|
1306
1311
|
const before = src.slice(Math.max(0, offset - 80), offset);
|
|
1307
1312
|
// Language-interpreter code: data unless it spawns a process (see above).
|
|
1308
1313
|
if (LANG_INTERPRETER_CODE_BEFORE.test(before))
|
|
1309
|
-
return interpreterCodeSpawnsProcess(before, body) ? whole : INERT_DATA;
|
|
1314
|
+
return interpreterCodeSpawnsProcess(before, body) || interpreterCodeRequestsMetadata(body) ? whole : INERT_DATA;
|
|
1310
1315
|
if (CODE_CONTEXT_BEFORE.test(before))
|
|
1311
1316
|
return whole; // code position: executes
|
|
1312
1317
|
if (dq !== undefined && INTERPOLATES.test(body))
|
package/dist/commands/hook.d.ts
CHANGED
|
@@ -193,7 +193,27 @@ export interface HookIo {
|
|
|
193
193
|
* mode, where `exit` only unwinds the evaluation and the process lives on.
|
|
194
194
|
*/
|
|
195
195
|
exitsProcess: boolean;
|
|
196
|
+
/** Evaluated inside the resident daemon for a thin client (see DaemonDeclined). */
|
|
197
|
+
viaDaemon?: boolean;
|
|
198
|
+
/** Set when this evaluation's verdict is a stop still being settled (approval wait). */
|
|
199
|
+
pendingStop?: string;
|
|
196
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* The daemon declines a request whose verdict is a human wait (org approval,
|
|
203
|
+
* taint approve-once). The thin client stops listening after ~2.5s and
|
|
204
|
+
* re-evaluates locally, so a daemon-side wait only opened a second approval
|
|
205
|
+
* request nobody would ever read. Declining sends the client to its local
|
|
206
|
+
* evaluation at once, where the one wait runs in the developer's own process.
|
|
207
|
+
*/
|
|
208
|
+
export declare class DaemonDeclined extends Error {
|
|
209
|
+
}
|
|
210
|
+
/** Absolute time a human wait must end by: its own cap, and never past the IDE's timeout. */
|
|
211
|
+
export declare function humanWaitDeadline(waitMs: number, now?: number, startedAt?: number, ideTimeoutMs?: number): number;
|
|
212
|
+
/** Verdict for an unexpected error that escaped the evaluation (process-level handlers). */
|
|
213
|
+
export declare function crashVerdictForProcess(event: string | undefined, message: string): {
|
|
214
|
+
stdout: string;
|
|
215
|
+
exitCode: number;
|
|
216
|
+
};
|
|
197
217
|
/**
|
|
198
218
|
* Daemon-side entry: evaluate one hook request in-process and return the
|
|
199
219
|
* verdict instead of writing/exiting. Everything the per-event hook process
|
|
@@ -202,7 +222,9 @@ export interface HookIo {
|
|
|
202
222
|
* Mirrors hookCommand's fail-open contract: any unexpected exception yields
|
|
203
223
|
* an allow verdict, never an error the thin client would surface.
|
|
204
224
|
*/
|
|
205
|
-
export declare function evaluateHookRequest(args: HookArgs, stdinRaw: string, config: BotGuardConfig
|
|
225
|
+
export declare function evaluateHookRequest(args: HookArgs, stdinRaw: string, config: BotGuardConfig, opts?: {
|
|
226
|
+
viaDaemon?: boolean;
|
|
227
|
+
}): Promise<{
|
|
206
228
|
stdout: string;
|
|
207
229
|
stderr: string;
|
|
208
230
|
exitCode: number;
|
|
@@ -217,4 +239,15 @@ export declare function evaluateHookRequest(args: HookArgs, stdinRaw: string, co
|
|
|
217
239
|
* through respond() inside and never reach this catch.
|
|
218
240
|
*/
|
|
219
241
|
export declare function hookCommand(args: HookArgs, config: BotGuardConfig): Promise<void>;
|
|
242
|
+
/**
|
|
243
|
+
* Errors that escape every await (an async 'error' event from a child process,
|
|
244
|
+
* a stray rejection) never reach the try/catch around the evaluation. Without
|
|
245
|
+
* these handlers Node prints a stack and exits 1 with NO verdict, which every
|
|
246
|
+
* client reads as a non-blocking hook error — the action runs. Route them
|
|
247
|
+
* through the same contract as a caught crash instead.
|
|
248
|
+
*/
|
|
249
|
+
export declare function installHookCrashHandlers(verdict: (message: string) => {
|
|
250
|
+
stdout: string;
|
|
251
|
+
exitCode: number;
|
|
252
|
+
}): void;
|
|
220
253
|
export {};
|