fullcourtdefense-cli 1.35.2 → 1.35.5
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.js +88 -8
- package/dist/approvalPrompt.d.ts +3 -0
- package/dist/approvalPrompt.js +20 -1
- package/dist/blockExplanation.js +17 -0
- package/dist/commandNarrative.d.ts +29 -0
- package/dist/commandNarrative.js +605 -0
- package/dist/commands/deterministicGuard.d.ts +7 -0
- package/dist/commands/deterministicGuard.js +146 -13
- package/dist/commands/shellGuard.js +1 -1
- package/dist/commands/workloadProtect.d.ts +10 -0
- package/dist/commands/workloadProtect.js +27 -4
- package/dist/detectorBaseline.d.ts +2 -2
- package/dist/detectorBaseline.js +2 -2
- package/dist/devConfirm.d.ts +30 -13
- package/dist/devConfirm.js +194 -38
- package/dist/index.js +4 -0
- package/dist/version.json +1 -1
- package/package.json +3 -1
|
@@ -6945,6 +6945,7 @@ function isHighEntropySecretToken(token, allowWordSlug = false) {
|
|
|
6945
6945
|
if (/^data:/i.test(token) || /^[A-Za-z0-9+/]{200,}={0,2}$/.test(token)) return false;
|
|
6946
6946
|
if (/^(?:https?|s3|gs|file):\/\//i.test(token) || /[\\/]{1}[\w.-]+[\\/]/.test(token)) return false;
|
|
6947
6947
|
if (allowWordSlug && /^[A-Za-z_][\w.-]*\/[A-Za-z_][\w.-]*\.[A-Za-z][A-Za-z0-9]{0,9}$/.test(token)) return false;
|
|
6948
|
+
if (allowWordSlug && /[/.]/.test(token) && token.split(/[/.]/).length >= 2 && token.split(/[/.]/).every((part) => /^[A-Za-z][a-z]*(?:[A-Z][a-z0-9]*)*$/.test(part))) return false;
|
|
6948
6949
|
if (/^[a-z]+(?:[A-Z][a-z0-9]+)+$/.test(token) || /^[a-z]+(?:_[a-z0-9]+)+$/.test(token)) return false;
|
|
6949
6950
|
if (allowWordSlug && /^[A-Z][a-z]+-(?:[A-Z][a-z]+){2,}$/.test(token)) return false;
|
|
6950
6951
|
if (allowWordSlug && /^[a-z]+(?:-[a-z]+)+(?:-\d+)?$/.test(token)) return false;
|
|
@@ -7033,7 +7034,7 @@ function hasSecretLikeValue(value) {
|
|
|
7033
7034
|
return detectSecretKind(value) !== "none";
|
|
7034
7035
|
}
|
|
7035
7036
|
var PII_EMAIL_RE = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
|
|
7036
|
-
var PII_EMAIL_IGNORE_RE = /(?:@(?:example\.(?:com|org|net)|test\.[a-z]+|localhost(?:\.[a-z]+)?|[\w.-]*\.(?:invalid|test|local|localdomain|example)|users\.noreply\.github\.com|noreply\.[\w.-]
|
|
7037
|
+
var PII_EMAIL_IGNORE_RE = /(?:@(?:example\.(?:com|org|net)|test\.[a-z]+|localhost(?:\.[a-z]+)?|[\w.-]*\.(?:invalid|test|local|localdomain|example)|users\.noreply\.github\.com|noreply\.[\w.-]+|[\w.-]*\.gserviceaccount\.com|[\w.-]*\.svc\.id\.goog)$|^(?:no-?reply|noreply|do-?not-?reply|test\d*|example|user\d*|admin|root|foo|bar|baz|someone|somebody|you|me|name|first\.last|john\.doe|jane\.doe|email)@)/i;
|
|
7037
7038
|
var PII_PHONE_RE = /(?:^|[^\w.@/:-])(\+?\(?\d{1,4}\)?[\s-]\(?\d{2,4}\)?[\s-]\d{2,4}(?:[\s-]\d{2,4}){0,2})(?![\w.@/:-])/g;
|
|
7038
7039
|
function hasPiiLikeValue(raw) {
|
|
7039
7040
|
if (!raw) return false;
|
|
@@ -7171,7 +7172,9 @@ function extractUrl(args, _argsText) {
|
|
|
7171
7172
|
if (typeof raw !== "string") continue;
|
|
7172
7173
|
const shellField = ["command", "cmd", "script", "input"].includes(field);
|
|
7173
7174
|
const value = shellField ? splitShellSegments(shellUrlActionText(raw)).map(withoutUnattributedJavaScriptUrls).map(withoutGhMetadataProse).join("\n") : raw;
|
|
7174
|
-
const
|
|
7175
|
+
const firstWebUrl = (text) => text.match(/https?:\/\/[^\s"'<>`]+/i)?.[0];
|
|
7176
|
+
const firstStorageUrl = (text) => text.match(/\b(?:s3|gs|az|ftp|sftp|smb):\/\/[A-Za-z0-9][A-Za-z0-9._-]*(?:\/[^\s"'<>`]*)?/i)?.[0];
|
|
7177
|
+
const firstUrl = (text) => firstWebUrl(text) || firstStorageUrl(text);
|
|
7175
7178
|
const outboundUrls = shellField ? splitShellSegments(value).filter((segment) => /^(?:curl(?:\.exe)?|wget(?:\.exe)?|invoke-restmethod|invoke-webrequest|irm|iwr)$/i.test(shellSegmentLead(segment)) && classifyShellSegment(segment) === "write").map(firstUrl).filter((url) => Boolean(url)) : [];
|
|
7176
7179
|
const externalOutbound = outboundUrls.find((url) => {
|
|
7177
7180
|
try {
|
|
@@ -7180,7 +7183,8 @@ function extractUrl(args, _argsText) {
|
|
|
7180
7183
|
return false;
|
|
7181
7184
|
}
|
|
7182
7185
|
});
|
|
7183
|
-
const
|
|
7186
|
+
const networkSegments = shellField ? segmentsThatMayReachNetwork(splitShellSegments(value)) : [value];
|
|
7187
|
+
const found = externalOutbound || outboundUrls[0] || firstWebUrl(networkSegments.join("\n")) || (shellField ? firstStorageUrl(networkSegments.filter((segment, index, all) => storageUriIsDestination(segment, index, all)).join("\n")) : void 0) || firstStorageUrl(networkSegments.join("\n"));
|
|
7184
7188
|
if (found) return found;
|
|
7185
7189
|
}
|
|
7186
7190
|
return "";
|
|
@@ -7827,6 +7831,24 @@ function shellSegmentLead(segment) {
|
|
|
7827
7831
|
}
|
|
7828
7832
|
var PS_SESSION_VARIABLE_ASSIGN_RE = /^\s*\$(?:psdefaultparametervalues|psmodulepath|profile|executioncontext|env:(?:path|pathext|psmodulepath))\s*(?:\[[^\]]*\]\s*)?(?:\+?=)/i;
|
|
7829
7833
|
var PS_VALUE_EXECUTOR_RE = /\b(?:invoke-expression|iex|invoke-command|icm|start-process|saps|invoke-item|ii)\b|&\s*[$(]|\.\s*invoke\s*\(/i;
|
|
7834
|
+
var PS_VALUE_LEAD_RE = /^\$(?:env:)?\w+(?:\.\w+|\[\d+\])*$/;
|
|
7835
|
+
var PS_BLOCK_STAGE_LEADS = /* @__PURE__ */ new Set(["foreach-object", "foreach", "%", "where-object", "where", "?"]);
|
|
7836
|
+
function classifyPipelineStage(stage, depth) {
|
|
7837
|
+
const trimmed = stage.trim();
|
|
7838
|
+
if (!trimmed) return "read";
|
|
7839
|
+
if (isPowerShellReadFormatter(trimmed)) return "read";
|
|
7840
|
+
const lead = shellSegmentLead(trimmed);
|
|
7841
|
+
if (PS_BLOCK_STAGE_LEADS.has(lead) && depth < 3) {
|
|
7842
|
+
const open = trimmed.indexOf("{");
|
|
7843
|
+
const close = trimmed.lastIndexOf("}");
|
|
7844
|
+
if (open >= 0 && close > open) {
|
|
7845
|
+
const rest = maskQuotedSpans(trimmed.slice(0, open) + trimmed.slice(close + 1));
|
|
7846
|
+
if (!/^\s*(?:foreach-object|foreach|%|where-object|where|\?)\s*(?:-process\s*)?\s*$/i.test(rest)) return "SHELL";
|
|
7847
|
+
return classifyShellCommandOperation(trimmed.slice(open + 1, close), depth + 1);
|
|
7848
|
+
}
|
|
7849
|
+
}
|
|
7850
|
+
return classifyShellSegment(trimmed, depth + 1);
|
|
7851
|
+
}
|
|
7830
7852
|
function isInertValueExpression(raw) {
|
|
7831
7853
|
const masked = maskQuotedSpans(raw);
|
|
7832
7854
|
if (/\$\(|`|&\s*[$(\w"']|[|<>]/.test(masked) || PS_VALUE_EXECUTOR_RE.test(masked)) return false;
|
|
@@ -7843,8 +7865,12 @@ function classifyShellSegment(segment, depth = 0) {
|
|
|
7843
7865
|
if (!raw) return "read";
|
|
7844
7866
|
if (PS_SESSION_VARIABLE_ASSIGN_RE.test(raw)) return "SHELL";
|
|
7845
7867
|
const lead = shellSegmentLead(segment);
|
|
7846
|
-
if (
|
|
7847
|
-
if (SHELL_NEUTRAL_BUILTINS.has(lead)
|
|
7868
|
+
if (PS_VALUE_LEAD_RE.test(lead) && PS_VALUE_EXECUTOR_RE.test(maskQuotedSpans(raw))) return "SHELL";
|
|
7869
|
+
if (SHELL_NEUTRAL_BUILTINS.has(lead)) return "read";
|
|
7870
|
+
if (PS_VALUE_LEAD_RE.test(lead)) {
|
|
7871
|
+
const stages = splitShellSegments(raw, true).slice(1);
|
|
7872
|
+
return worstShellOp(stages.map((stage) => classifyPipelineStage(stage, depth)), "read");
|
|
7873
|
+
}
|
|
7848
7874
|
if (isInertValueExpression(raw)) return "read";
|
|
7849
7875
|
const rawLower = raw.toLowerCase();
|
|
7850
7876
|
const sqlClient = SHELL_SQL_CLIENTS.has(lead) || /\|\s*(?:sudo\s+)?(?:psql|mysql|mariadb|sqlcmd|sqlite3|mongosh?|clickhouse-client|bq|cockroach|usql)\b/.test(rawLower);
|
|
@@ -7896,7 +7922,26 @@ function classifyShellWords(masked, raw, lead) {
|
|
|
7896
7922
|
if (lead === "git" && /\bgit\s+(?:status|diff|log|show|blame|clone|fetch|pull|ls-files|remote|branch(?!\s+-[dD])|describe|rev-parse|stash\s+(?:list|show)|tag$)\b/.test(value)) return "read";
|
|
7897
7923
|
if (lead === "gh" && /\bgh\s+(?:pr|issue|repo|run|release|workflow)\s+(?:view|list|status|checks|diff|download)\b/.test(value)) return "read";
|
|
7898
7924
|
if (lead === "gh" && /\bgh\s+auth\s+status\b/.test(value) && !/--show-token|\s-t\b/.test(value)) return "read";
|
|
7899
|
-
if (
|
|
7925
|
+
if (lead === "gh" && /\bgh\s+api(?=\s|$)/.test(value)) {
|
|
7926
|
+
const method = /(?:\s-x|\s--method)[\s=]+["']?([a-z]+)/i.exec(segment);
|
|
7927
|
+
const hasField = /\s(?:-f|-F|--field|--raw-field|--input)(?:[\s=]|$)/.test(segment);
|
|
7928
|
+
if (method && method[1].toUpperCase() !== "GET" || hasField) return "write";
|
|
7929
|
+
const header = /\s(?:-H|--header)(?:[\s=]|$)/.exec(segment);
|
|
7930
|
+
if (header && /\$\(|`|\$env:|\(\s*[A-Za-z][\w-]*\s/.test(segment.slice(header.index))) return "write";
|
|
7931
|
+
return pipesIntoInterpreter ? "SHELL" : "read";
|
|
7932
|
+
}
|
|
7933
|
+
const gcloudVerb = gcloudVerbWord(value);
|
|
7934
|
+
if (gcloudVerb) {
|
|
7935
|
+
if (GCLOUD_READ_VERB_RE.test(gcloudVerb)) {
|
|
7936
|
+
if (pipesIntoInterpreter) return "SHELL";
|
|
7937
|
+
const redirect = value.match(/(?:^|[^>0-9])>{1,2}\s*([^\s&|;]+)/);
|
|
7938
|
+
if (redirect && !/^\/dev\/null$|^nul$|^\$null$/i.test(redirect[1])) return "write";
|
|
7939
|
+
return "read";
|
|
7940
|
+
}
|
|
7941
|
+
if (gcloudVerb === "delete") return "DELETE";
|
|
7942
|
+
if (GCLOUD_WRITE_VERB_RE.test(gcloudVerb)) return pipesIntoInterpreter ? "SHELL" : "write";
|
|
7943
|
+
}
|
|
7944
|
+
if (/^(?:kubectl\s+(?:get|describe|logs|top|config\s+(?:current-context|get-contexts|view(?!\s+--raw)))|docker\s+(?:ps|images|logs|inspect|version|info)|terraform\s+(?:plan|validate|fmt|show|state\s+list|output)|aws\s+(?:s3\s+ls|sts\s+get-caller-identity)|az\s+account\s+show|npm\s+(?:ls|list|view|outdated|--version)|node\s+--version|python3?\s+--version)\b/.test(value)) return "read";
|
|
7900
7945
|
if (/\b(rm\s+-[a-z]*r[a-z]*f|rm\s+-[a-z]*f[a-z]*r|del\s+\/[fqs]|remove-item\b.*\s-recurse\b|rmdir\s+\/s)\b/.test(value)) {
|
|
7901
7946
|
return isWorkspaceScopedRemoval(segment) ? "write" : "DELETE";
|
|
7902
7947
|
}
|
|
@@ -7927,6 +7972,35 @@ function classifyShellWords(masked, raw, lead) {
|
|
|
7927
7972
|
if (SHELL_BLOCK_STRUCTURE_RE.test(value)) return "read";
|
|
7928
7973
|
return "SHELL";
|
|
7929
7974
|
}
|
|
7975
|
+
var GCLOUD_READ_VERB_RE = /^(?:describe|list|ls|get-iam-policy|get-value|get-ancestors|read|check|versions)$/;
|
|
7976
|
+
var GCLOUD_WRITE_VERB_RE = /^(?:create|update|patch|replace|enable|disable|deploy|apply|import|export|start|stop|restart|resume|suspend|scale|promote|migrate|rollback|add-[a-z-]+|remove-[a-z-]+|set-[a-z-]+|unset|clear|activate|revoke|copy)$/;
|
|
7977
|
+
function gcloudVerbWord(value) {
|
|
7978
|
+
const tokens = value.split("|")[0].trim().split(/\s+/);
|
|
7979
|
+
if (!/^gcloud(?:\.cmd|\.exe)?$/.test(tokens.shift() || "")) return null;
|
|
7980
|
+
let depth = 0;
|
|
7981
|
+
for (const token of tokens) {
|
|
7982
|
+
if (token.startsWith("-")) continue;
|
|
7983
|
+
if (GCLOUD_READ_VERB_RE.test(token) || GCLOUD_WRITE_VERB_RE.test(token) || token === "delete") return token;
|
|
7984
|
+
if (!/^[a-z][a-z0-9-]*$/.test(token) || ++depth > 6) return null;
|
|
7985
|
+
}
|
|
7986
|
+
return null;
|
|
7987
|
+
}
|
|
7988
|
+
var CLOUD_CLI_LEAD_RE = /^(?:gcloud(?:\.cmd|\.exe)?|gsutil|aws|az)$/i;
|
|
7989
|
+
var STORAGE_TRANSFER_RE = /\b(?:aws\s+s3\s+(?:cp|sync|mv)|gsutil\s+(?:-m\s+)?(?:cp|rsync|mv)|gcloud(?:\.cmd|\.exe)?\s+storage\s+(?:cp|rsync|mv)|az\s+storage\s+blob\s+upload|scp|rsync|sftp)\b/i;
|
|
7990
|
+
var PIPES_INTO_INTERPRETER_RE = /\|\s*(?:sudo\s+)?(?:ba|z|da|k)?sh\b|\|\s*(?:pwsh|powershell|python3?|node|perl|ruby)\b/i;
|
|
7991
|
+
function storageUriIsDestination(segment, index = 0, all = []) {
|
|
7992
|
+
const lead = shellSegmentLead(segment);
|
|
7993
|
+
if (!CLOUD_CLI_LEAD_RE.test(lead)) return true;
|
|
7994
|
+
const words = maskQuotedSpans(segment).toLowerCase();
|
|
7995
|
+
if (PIPES_INTO_INTERPRETER_RE.test(words) || /\$\(|`|&\s*[$(]/.test(words) || PS_VALUE_EXECUTOR_RE.test(words)) return true;
|
|
7996
|
+
if (STORAGE_TRANSFER_RE.test(words)) return true;
|
|
7997
|
+
if (/^\s*(?:\$[\w:]+\s*=|[A-Za-z_]\w*=)/.test(segment) && all.slice(index + 1).some((later) => {
|
|
7998
|
+
const laterWords = maskQuotedSpans(later).toLowerCase();
|
|
7999
|
+
return PS_VALUE_EXECUTOR_RE.test(laterWords) || PIPES_INTO_INTERPRETER_RE.test(laterWords) || /(?:^|[\s;&|(])(?:sudo\s+)?(?:ba|z|da|k)?sh\s+-c\b|(?:^|[\s;&|(])(?:pwsh|powershell)(?:\.exe)?\s+-c(?:ommand)?\b|\beval\b|&\s*[$(]/.test(laterWords);
|
|
8000
|
+
})) return true;
|
|
8001
|
+
if (/^gcloud(?:\.cmd|\.exe)?\s+(?:--\S+\s+)*(?:alpha\s+|beta\s+)?[a-z-]+(?:\s+[a-z-]+){0,4}\s+export\b/.test(words.split("|")[0].trim())) return true;
|
|
8002
|
+
return false;
|
|
8003
|
+
}
|
|
7930
8004
|
var SHELL_BLOCK_STRUCTURE_RE = /^(?:\s|[{}()=\-\d]|\+=|""|\$[\w:.]+|\b(?:try|catch|finally|if|then|else|elseif|elif|fi|do|done|esac|end|exit|return|break|continue)\b)*$/i;
|
|
7931
8005
|
function classifyShellCommandOperation(command, depth = 0) {
|
|
7932
8006
|
if (provenPowerShellReadSequence(command)) return "read";
|
|
@@ -8386,11 +8460,17 @@ function inferBundledToolContext(toolName, args) {
|
|
|
8386
8460
|
context["destination.domain"] = parsed.hostname;
|
|
8387
8461
|
context["url.risk"] = hostType;
|
|
8388
8462
|
const requestHasSecret = recordHttpRequestFacts(context, args, detectedUrl);
|
|
8389
|
-
|
|
8463
|
+
const storageResourceOnly = isCodeExecTool && /^(?:gs|s3|az):\/\//i.test(detectedUrl) && (() => {
|
|
8464
|
+
const segments = splitShellSegments(commandText);
|
|
8465
|
+
const bucket = (detectedUrl.match(/^[a-z0-9]+:\/\/[^/\s"'<>`]+/i) || [detectedUrl])[0].toLowerCase();
|
|
8466
|
+
const carriers = segments.map((segment, index) => [segment, index]).filter(([segment]) => segment.toLowerCase().includes(bucket));
|
|
8467
|
+
return carriers.length > 0 && carriers.every(([segment, index]) => !storageUriIsDestination(segment, index, segments));
|
|
8468
|
+
})();
|
|
8469
|
+
if (inbound || storageResourceOnly) {
|
|
8390
8470
|
context["source.domain"] = parsed.hostname;
|
|
8391
8471
|
context["source.type"] = hostType;
|
|
8392
8472
|
}
|
|
8393
|
-
if (!inbound || requestHasSecret) {
|
|
8473
|
+
if (!inbound && !storageResourceOnly || requestHasSecret) {
|
|
8394
8474
|
context["destination.type"] = hostType;
|
|
8395
8475
|
}
|
|
8396
8476
|
} catch {
|
package/dist/approvalPrompt.d.ts
CHANGED
|
@@ -16,7 +16,10 @@ export interface NativeDialogView {
|
|
|
16
16
|
headline: string;
|
|
17
17
|
subtitle: string;
|
|
18
18
|
kind?: string;
|
|
19
|
+
/** The action: the whole script when the explanation carried it, else the short "What" line. */
|
|
19
20
|
action?: string;
|
|
21
|
+
/** Plain-words line per step of a shell script ("Exports Firestore data … to storage bucket …"). */
|
|
22
|
+
steps: string[];
|
|
20
23
|
policy?: string;
|
|
21
24
|
scope?: string;
|
|
22
25
|
agent?: string;
|
package/dist/approvalPrompt.js
CHANGED
|
@@ -12,6 +12,8 @@ function line(value, limit) {
|
|
|
12
12
|
const head = Math.ceil((limit - 3) * 0.6);
|
|
13
13
|
return `${text.slice(0, head)} … ${text.slice(-(limit - head - 3))}`;
|
|
14
14
|
}
|
|
15
|
+
/** The full script is shown in a scrolling box; this only guards against a runaway payload. */
|
|
16
|
+
const MAX_DIALOG_SCRIPT = 6000;
|
|
15
17
|
const CEILING_NOTE_RE = /\s*This policy is set to Block[.;]\s*(?:Because it is not marked "Hard block", your workstation requires your approval before continuing\.|on a developer workstation it asks you instead\.)?\s*(?:An admin can (?:review the matching rule and its scope in the console|mark it "Hard block" in the console to stop this without asking)\.)?/;
|
|
16
18
|
const CEILING_NOTE_SHORT = 'set to Block; on your workstation it asks you first.';
|
|
17
19
|
/** Parse the `Rule:` line the block explanation writes into the name the console lists and its scope. */
|
|
@@ -39,6 +41,20 @@ function promptFacts(reason) {
|
|
|
39
41
|
const facts = { agent, ide: field('IDE') };
|
|
40
42
|
if (rule)
|
|
41
43
|
Object.assign(facts, ruleParts(rule));
|
|
44
|
+
const script = field('Script');
|
|
45
|
+
if (script)
|
|
46
|
+
facts.script = script.length > MAX_DIALOG_SCRIPT ? `${script.slice(0, MAX_DIALOG_SCRIPT - 1)}…` : script;
|
|
47
|
+
const doesAt = rows.findIndex(row => /^Does:\s*$/.test(row));
|
|
48
|
+
if (doesAt >= 0) {
|
|
49
|
+
const steps = [];
|
|
50
|
+
for (const row of rows.slice(doesAt + 1)) {
|
|
51
|
+
if (!row.startsWith('- '))
|
|
52
|
+
break;
|
|
53
|
+
steps.push(row.slice(2).trim());
|
|
54
|
+
}
|
|
55
|
+
if (steps.length)
|
|
56
|
+
facts.steps = steps;
|
|
57
|
+
}
|
|
42
58
|
if (what) {
|
|
43
59
|
// summarizeAction ends with the kind in parentheses: "… (shell command)".
|
|
44
60
|
const kind = /^(.*)\s\(([a-z][a-z /-]{2,30})\)$/i.exec(what);
|
|
@@ -94,7 +110,9 @@ function nativeDialogView(reason, options = {}) {
|
|
|
94
110
|
? 'A security policy paused this action for an admin or manager.'
|
|
95
111
|
: 'A security policy paused this action. Review the details and choose what to do next.',
|
|
96
112
|
kind: f.kind,
|
|
97
|
-
|
|
113
|
+
// The whole script when we have it (the box scrolls); the short line otherwise.
|
|
114
|
+
action: f.script ? f.script.replace(/ ⏎ /g, '\n') : f.action ? line(f.action, 280) : undefined,
|
|
115
|
+
steps: (f.steps || []).slice(0, 14).map(step => line(step, 220)),
|
|
98
116
|
policy: f.policy ? line(f.policy, 160) : undefined,
|
|
99
117
|
scope: f.scope ? line(f.scope, 220) : undefined,
|
|
100
118
|
agent: f.agent ? line(f.agent, 80) : undefined,
|
|
@@ -117,6 +135,7 @@ function nativeApprovalMessage(reason, options = {}) {
|
|
|
117
135
|
f.policy ? `Policy: ${line(f.policy, 160)}` : '',
|
|
118
136
|
f.scope ? ` ${line(f.scope, 200)}` : '',
|
|
119
137
|
f.action ? `Action: ${f.kind ? `${f.kind} — ` : ''}${line(f.action, 230)}` : '',
|
|
138
|
+
...(f.steps || []).slice(0, 6).map((step, index) => `${index === 0 ? 'Does: ' : ' '}${index + 1}. ${line(step, 160)}`),
|
|
120
139
|
f.agent ? `Agent: ${line(f.agent, 80)}` : '',
|
|
121
140
|
f.ide ? `IDE: ${line(f.ide, 40)}` : '',
|
|
122
141
|
f.tool ? `Tool: ${line(f.tool, 230)}` : '',
|
package/dist/blockExplanation.js
CHANGED
|
@@ -19,7 +19,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
19
19
|
exports.summarizeAction = summarizeAction;
|
|
20
20
|
exports.buildBlockExplanation = buildBlockExplanation;
|
|
21
21
|
exports.oneLineExplanation = oneLineExplanation;
|
|
22
|
+
const commandNarrative_1 = require("./commandNarrative");
|
|
22
23
|
const MAX_WHAT = 160;
|
|
24
|
+
/** The full script travels to the approval dialog; anything longer is still cut, but far later. */
|
|
25
|
+
const MAX_SCRIPT = 6000;
|
|
26
|
+
/** Newlines flattened so the multi-line explanation grammar (one `Field:` per row) still parses. */
|
|
27
|
+
function scriptLine(command) {
|
|
28
|
+
const flat = command.replace(/\r?\n/g, ' ⏎ ').trim();
|
|
29
|
+
return flat.length > MAX_SCRIPT ? `${flat.slice(0, MAX_SCRIPT - 1)}…` : flat;
|
|
30
|
+
}
|
|
23
31
|
function str(v) {
|
|
24
32
|
return typeof v === 'string' && v.trim() ? v : undefined;
|
|
25
33
|
}
|
|
@@ -229,9 +237,18 @@ function buildBlockExplanation(input) {
|
|
|
229
237
|
if (input.note)
|
|
230
238
|
why = `${why} ${input.note}`.trim();
|
|
231
239
|
const headline = headlineFor(input.outcome, noun);
|
|
240
|
+
// Shell / terminal: say what each step of the script does, in plain words,
|
|
241
|
+
// and carry the whole script when the short "What" line had to cut it.
|
|
242
|
+
const command = (input.event === 'shell' || input.event === 'terminal')
|
|
243
|
+
? (str(input.toolArgs?.command) || str(input.toolArgs?.cmd) || str(input.toolArgs?.commandLine) || str(input.toolArgs?.script))
|
|
244
|
+
: undefined;
|
|
245
|
+
const steps = command ? (0, commandNarrative_1.describeShellCommandLines)(command) : [];
|
|
246
|
+
const script = command && command.replace(/\s+/g, ' ').trim().length > MAX_WHAT - 1 ? scriptLine(command) : undefined;
|
|
232
247
|
const userMessage = [
|
|
233
248
|
headline,
|
|
234
249
|
`What: ${what}`,
|
|
250
|
+
...(steps.length ? ['Does:', ...steps.map(step => ` - ${step}`)] : []),
|
|
251
|
+
...(script ? [`Script: ${script}`] : []),
|
|
235
252
|
`Rule: ${rule}`,
|
|
236
253
|
`Why: ${why}`,
|
|
237
254
|
stepsTitle(input.outcome),
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a shell script DOES, one plain-words line per step — for the approval
|
|
3
|
+
* dialog and the IDE message. A developer asked to approve
|
|
4
|
+
*
|
|
5
|
+
* $prefix="gs://b/prod-export"; gcloud firestore export $prefix --project acme-prod; gcloud firestore import $prefix --project acme-staging
|
|
6
|
+
*
|
|
7
|
+
* should read "Exports Firestore data from project acme-prod to bucket b ·
|
|
8
|
+
* Imports Firestore data into project acme-staging from bucket b", not a
|
|
9
|
+
* truncated command line.
|
|
10
|
+
*
|
|
11
|
+
* Fixed templates only, keyed on the program and its verb — no model, no
|
|
12
|
+
* scoring, nothing that could decide anything. This module never influences a
|
|
13
|
+
* verdict; it only words one. A program the grammar does not know is reported
|
|
14
|
+
* as exactly that ("Runs `foo bar`"), never guessed. Pure: no I/O.
|
|
15
|
+
*/
|
|
16
|
+
export interface CommandStep {
|
|
17
|
+
/** The segment as written (trimmed). */
|
|
18
|
+
segment: string;
|
|
19
|
+
/** Plain-words description of what it does. */
|
|
20
|
+
text: string;
|
|
21
|
+
/** The grammar recognised the program and its verb. */
|
|
22
|
+
known: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** Quote-aware split on `;`, `&&`, `||`, newlines. Pipes stay inside a step. */
|
|
25
|
+
export declare function splitCommandSteps(command: string): string[];
|
|
26
|
+
/** One plain-words line per step, in order. */
|
|
27
|
+
export declare function describeShellCommand(command: string): CommandStep[];
|
|
28
|
+
/** The step lines as the explanation prints them (`- …`). */
|
|
29
|
+
export declare function describeShellCommandLines(command: string): string[];
|