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.
@@ -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|versions)$/;
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', versions: 'Lists versions of',
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.replace(/^\(\s*/, '').replace(/\)\s*(?:\.\w+\(\))*$/, '');
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
- return step(`Runs \`${shortValue([program, rest[1]].filter(t => t && !t.startsWith('-')).join(' '), 50)}\``, false);
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). */
@@ -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))
@@ -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): Promise<{
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 {};