fullcourtdefense-cli 1.36.0 → 1.36.3
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 +23 -0
- package/dist/actionPolicyEngine.js +68 -20
- package/dist/approvalPrompt.d.ts +15 -0
- package/dist/approvalPrompt.js +157 -20
- package/dist/askDialog.d.ts +2 -0
- package/dist/askDialog.js +15 -1
- package/dist/askHeader.d.ts +87 -0
- package/dist/askHeader.js +612 -0
- package/dist/blockExplanation.d.ts +9 -0
- package/dist/blockExplanation.js +14 -0
- package/dist/callerEnv.d.ts +1 -1
- package/dist/callerEnv.js +14 -0
- package/dist/cloudGrant.d.ts +46 -0
- package/dist/cloudGrant.js +420 -0
- package/dist/cloudGrantTools.d.ts +97 -0
- package/dist/cloudGrantTools.js +1112 -0
- package/dist/commandNarrative.d.ts +13 -0
- package/dist/commandNarrative.js +249 -49
- package/dist/commands/cmdGuard.js +1 -1
- package/dist/commands/daemon.js +37 -3
- package/dist/commands/demoActions.js +2 -2
- package/dist/commands/deterministicGuard.d.ts +13 -2
- package/dist/commands/deterministicGuard.js +259 -57
- package/dist/commands/discover.js +37 -14
- package/dist/commands/discoverAgentFiles.d.ts +2 -0
- package/dist/commands/discoverAgentFiles.js +1 -0
- package/dist/commands/discoverPaths.d.ts +4 -0
- package/dist/commands/discoverPaths.js +19 -1
- package/dist/commands/discoverProjectRoots.d.ts +72 -0
- package/dist/commands/discoverProjectRoots.js +327 -0
- package/dist/commands/discoverProxy.d.ts +1 -1
- package/dist/commands/discoverProxy.js +1 -1
- package/dist/commands/discoverSecrets.d.ts +44 -0
- package/dist/commands/discoverSecrets.js +158 -14
- package/dist/commands/doctor.js +1 -0
- package/dist/commands/hook.js +10 -7
- package/dist/commands/login.js +4 -0
- package/dist/commands/mcpGateway.js +9 -7
- package/dist/commands/posixShellGuard.js +1 -1
- package/dist/commands/shellGuard.js +70 -25
- package/dist/commands/uninstallAll.d.ts +1 -7
- package/dist/commands/uninstallAll.js +6 -45
- package/dist/config.d.ts +1 -1
- package/dist/config.js +3 -3
- package/dist/credentialGrant.d.ts +48 -0
- package/dist/credentialGrant.js +29 -18
- package/dist/credentialStore.js +1 -1
- package/dist/devConfirm.d.ts +44 -3
- package/dist/devConfirm.js +166 -30
- package/dist/distress.d.ts +2 -0
- package/dist/distress.js +2 -0
- package/dist/grantTargets.d.ts +21 -0
- package/dist/grantTargets.js +235 -0
- package/dist/identityRecovery.d.ts +50 -0
- package/dist/identityRecovery.js +99 -0
- package/dist/localSafetySnapshot.js +2 -0
- package/dist/machineIdentity.d.ts +90 -1
- package/dist/machineIdentity.js +457 -94
- package/dist/msiProductCode.d.ts +10 -0
- package/dist/msiProductCode.js +124 -0
- package/dist/notify.js +1 -0
- package/dist/selfTest.d.ts +1 -1
- package/dist/selfTest.js +1 -1
- package/dist/selfUpdate.d.ts +51 -2
- package/dist/selfUpdate.js +217 -19
- package/dist/shellAst.d.ts +2 -0
- package/dist/shellAst.js +7 -1
- package/dist/shellAstLoader.js +12 -1
- package/dist/telemetry.d.ts +9 -0
- package/dist/telemetry.js +29 -7
- package/dist/version.json +1 -1
- package/package.json +7 -1
|
@@ -235,6 +235,29 @@ export declare function canonicalAgentClient(value: string): string;
|
|
|
235
235
|
*/
|
|
236
236
|
export type SecretKind = 'github_token' | 'aws_access_key' | 'google_api_key' | 'openai_key' | 'slack_token' | 'jwt' | 'private_key' | 'assignment' | 'high_entropy' | 'none';
|
|
237
237
|
export declare function detectSecretKind(raw: string): SecretKind;
|
|
238
|
+
/**
|
|
239
|
+
* gcloud read verbs. Token printers (`print-access-token`, `print-identity-token`)
|
|
240
|
+
* are credential dumps and are deliberately absent; `cat` streams object bytes
|
|
241
|
+
* and keeps its own stricter proof (isGcloudStorageCatRead). `versions` is a
|
|
242
|
+
* GROUP (`app versions`, `secrets versions`), never a verb: as a verb it made
|
|
243
|
+
* `gcloud app versions delete` a read. `access` is `secrets versions access`.
|
|
244
|
+
*/
|
|
245
|
+
export declare const GCLOUD_READ_VERB_RE: RegExp;
|
|
246
|
+
/** gcloud verbs that change a cloud resource. Deletes are handled apart (DELETE). */
|
|
247
|
+
export declare const GCLOUD_WRITE_VERB_RE: RegExp;
|
|
248
|
+
/** gcloud verbs that remove a resource or its data (`kms keys versions destroy`, `secrets versions destroy`). */
|
|
249
|
+
export declare const GCLOUD_DELETE_VERB_RE: RegExp;
|
|
250
|
+
/**
|
|
251
|
+
* `gcloud [--global-flag …] [alpha|beta] <group…> <verb> …` on the masked,
|
|
252
|
+
* lowercased command words: the first token after the program that is not a
|
|
253
|
+
* flag and not a lowercase group word that names a further group. gcloud
|
|
254
|
+
* groups and verbs are both lowercase words, so the walk returns the FIRST
|
|
255
|
+
* word that is a known read/write/delete verb; a value, a redirect or an
|
|
256
|
+
* unknown word before any verb ends the walk with `null` (segment stays
|
|
257
|
+
* unknown). Nothing here reads the resource operand — `gs://…`, a service
|
|
258
|
+
* name, a secret name are what the verb acts ON, not where data goes.
|
|
259
|
+
*/
|
|
260
|
+
export declare function gcloudVerbWord(value: string): string | null;
|
|
238
261
|
export type ShellAstMode = 'off' | 'shadow' | 'primary';
|
|
239
262
|
/** Structural subset of shellAst.ts ShellFacts that the engine reads. */
|
|
240
263
|
export interface ShellAstFactsLike {
|
|
@@ -24,6 +24,9 @@ __export(actionPolicyEngine_exports, {
|
|
|
24
24
|
ACTION_POLICY_GRANT_MAX_USES: () => ACTION_POLICY_GRANT_MAX_USES,
|
|
25
25
|
CONTENT_FACT_FIELDS: () => CONTENT_FACT_FIELDS,
|
|
26
26
|
DEFAULT_ACTION_POLICY_APPROVAL: () => DEFAULT_ACTION_POLICY_APPROVAL,
|
|
27
|
+
GCLOUD_DELETE_VERB_RE: () => GCLOUD_DELETE_VERB_RE,
|
|
28
|
+
GCLOUD_READ_VERB_RE: () => GCLOUD_READ_VERB_RE,
|
|
29
|
+
GCLOUD_WRITE_VERB_RE: () => GCLOUD_WRITE_VERB_RE,
|
|
27
30
|
MACHINE_KINDS: () => MACHINE_KINDS,
|
|
28
31
|
WORKSTATION_ASK_APPROVAL: () => WORKSTATION_ASK_APPROVAL,
|
|
29
32
|
approvalExceptionError: () => approvalExceptionError,
|
|
@@ -32,6 +35,7 @@ __export(actionPolicyEngine_exports, {
|
|
|
32
35
|
detectSecretKind: () => detectSecretKind,
|
|
33
36
|
effectiveApprovalScope: () => effectiveApprovalScope,
|
|
34
37
|
evaluateActionPolicies: () => evaluateActionPolicies,
|
|
38
|
+
gcloudVerbWord: () => gcloudVerbWord,
|
|
35
39
|
getShellAstMode: () => getShellAstMode,
|
|
36
40
|
inferBundledToolContext: () => inferBundledToolContext,
|
|
37
41
|
inferToolContext: () => inferToolContext,
|
|
@@ -6822,6 +6826,7 @@ var OPERATION_MATCH_SKIP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
6822
6826
|
// its operation/capability; the words a person typed into a title are not a verb.
|
|
6823
6827
|
"title",
|
|
6824
6828
|
"subject",
|
|
6829
|
+
"topic",
|
|
6825
6830
|
"description",
|
|
6826
6831
|
"message",
|
|
6827
6832
|
"msg",
|
|
@@ -6903,25 +6908,30 @@ function buildOperationMatchText(_toolName, context) {
|
|
|
6903
6908
|
const identifierField = /^(?:id|ids|uuid|uuids|guid|guids)$/.test(fieldWords[fieldWords.length - 1]);
|
|
6904
6909
|
if (OPERATION_MATCH_SKIP_FIELDS.has(key) || identifierField || key.startsWith("args.") || key.startsWith("request.")) continue;
|
|
6905
6910
|
if (typeof value !== "string" || value.length === 0 || value.length > 2e3) continue;
|
|
6906
|
-
const text = COMMAND_LINE_FIELDS.has(key) ? shellVerbWords(value) : value;
|
|
6911
|
+
const text = COMMAND_LINE_FIELDS.has(key) ? shellVerbWords(value) : SQL_STATEMENT_FIELDS.has(key) && !SQL_EXECUTES_STRING_RE.test(value) ? maskQuotedSpans(value) : value;
|
|
6907
6912
|
parts.push(`${key} ${text}`);
|
|
6908
6913
|
}
|
|
6909
6914
|
return parts.join(" ");
|
|
6910
6915
|
}
|
|
6911
6916
|
var COMMAND_LINE_FIELDS = /* @__PURE__ */ new Set(["command", "cmd", "commandLine", "script", "args"]);
|
|
6917
|
+
var SQL_STATEMENT_FIELDS = /* @__PURE__ */ new Set(["query", "sql", "statement"]);
|
|
6918
|
+
var SQL_EXECUTES_STRING_RE = /\b(?:exec(?:ute)?|sp_executesql|prepare)\b/i;
|
|
6912
6919
|
var CODE_EXEC_TOOL_NAME_RE = /\b(node ?repl|repl|shell|exec|execute|terminal|command|cmd|run ?code|bash|zsh|sh|powershell|pwsh)\b/i;
|
|
6913
6920
|
function shellVerbWords(command) {
|
|
6914
6921
|
let lead = "";
|
|
6915
6922
|
let first = true;
|
|
6916
6923
|
let scriptArgs = false;
|
|
6917
6924
|
let previousBare = "";
|
|
6918
|
-
|
|
6919
|
-
|
|
6925
|
+
let previousToken = "";
|
|
6926
|
+
return shellCommandWords(command).replace(/\|/g, " | ").split(/\s+/).filter(Boolean).map((token) => {
|
|
6927
|
+
const afterToken = previousToken;
|
|
6928
|
+
previousToken = token;
|
|
6929
|
+
if (token === ";" || token === "|") {
|
|
6920
6930
|
lead = "";
|
|
6921
6931
|
first = true;
|
|
6922
6932
|
scriptArgs = false;
|
|
6923
6933
|
previousBare = "";
|
|
6924
|
-
return token;
|
|
6934
|
+
return token === "|" ? "" : token;
|
|
6925
6935
|
}
|
|
6926
6936
|
if (scriptArgs) return "";
|
|
6927
6937
|
const flag = /^(-{1,2})([A-Za-z][A-Za-z0-9_-]*)(?:=.*)?$/.exec(token);
|
|
@@ -6933,6 +6943,14 @@ function shellVerbWords(command) {
|
|
|
6933
6943
|
return "";
|
|
6934
6944
|
}
|
|
6935
6945
|
if (!flag && !lead) lead = word.toLowerCase();
|
|
6946
|
+
else if (!flag && PACKAGE_EXECUTORS.has(lead) && previousBare === lead) lead = word.toLowerCase();
|
|
6947
|
+
if (!flag && !leading && SEARCH_PROGRAMS.has(lead)) return "";
|
|
6948
|
+
if (!flag && !leading && TEST_RUNNERS.has(lead)) return "";
|
|
6949
|
+
if (!flag && PACKAGE_RUNNERS.has(lead) && (previousBare === "run" || previousBare === "run-script")) {
|
|
6950
|
+
scriptArgs = true;
|
|
6951
|
+
return "";
|
|
6952
|
+
}
|
|
6953
|
+
if (!flag && VCS_PROGRAMS.has(lead) && /^(?:-[a-z]*m|--message)$/.test(afterToken)) return "";
|
|
6936
6954
|
if (flag) {
|
|
6937
6955
|
if (VCS_PROGRAMS.has(lead)) return "";
|
|
6938
6956
|
const segs = word.split(/[-_]+/).filter(Boolean);
|
|
@@ -6952,6 +6970,10 @@ function shellVerbWords(command) {
|
|
|
6952
6970
|
}).filter(Boolean).join(" ");
|
|
6953
6971
|
}
|
|
6954
6972
|
var VCS_PROGRAMS = /* @__PURE__ */ new Set(["git", "gh", "hg", "svn", "glab"]);
|
|
6973
|
+
var SEARCH_PROGRAMS = /* @__PURE__ */ new Set(["grep", "egrep", "fgrep", "zgrep", "rg", "ag", "ack", "findstr", "select-string", "sls"]);
|
|
6974
|
+
var TEST_RUNNERS = /* @__PURE__ */ new Set(["pytest", "jest", "vitest", "mocha"]);
|
|
6975
|
+
var PACKAGE_RUNNERS = /* @__PURE__ */ new Set(["npm", "pnpm", "yarn", "bun"]);
|
|
6976
|
+
var PACKAGE_EXECUTORS = /* @__PURE__ */ new Set(["npx", "bunx", "pnpx"]);
|
|
6955
6977
|
var GH_CONFIG_RESOURCES = /* @__PURE__ */ new Set(["secret", "variable", "cache", "alias", "label", "ssh-key", "gpg-key"]);
|
|
6956
6978
|
var SCRIPT_RUNNER_LEADS = /* @__PURE__ */ new Set([
|
|
6957
6979
|
"bash",
|
|
@@ -7058,7 +7080,7 @@ function isHighEntropySecretToken(token, allowWordSlug = false) {
|
|
|
7058
7080
|
return shannonEntropy(token) >= 4;
|
|
7059
7081
|
}
|
|
7060
7082
|
var SECRET_NAME = String.raw`(?:api[_-]?key|apikey|access[_-]?token|auth[_-]?token|refresh[_-]?token|bearer[_-]?token|client[_-]?secret|secret[_-]?key|secret|password|passwd|pwd|credentials?|private[_-]?key|token)`;
|
|
7061
|
-
var SECRET_ASSIGNMENT_RE = new RegExp(String.raw
|
|
7083
|
+
var SECRET_ASSIGNMENT_RE = new RegExp(String.raw`${SECRET_NAME}[\\"']*\s*(?:=>|[:=])\s*[\\"']*([^\s\\"',;&|)}\]<>]{8,})`, "gi");
|
|
7062
7084
|
var SECRET_FLAG_RE = /(?:^|[\s"'`;(\[{,])--?(?:password|passwd|pwd|token|api-?key|secret|client-secret|access-token)(?:=|\s+)[\\"']*([^\s\\"',;&|)}\]<>-][^\s\\"',;&|)}\]<>]{7,})/gi;
|
|
7063
7085
|
var SECRET_FLAG_QUOTED_RE = /(?:^|[\s"'`;(\[{,])--?(?:password|passwd|pwd|token|api-?key|secret|client-secret|access-token)(?:=|\s+)\\?(["'])([^\s"'\\-][^\s"'\\]{7,})\\?\1/gi;
|
|
7064
7086
|
function isWordPhraseWithCounters(value) {
|
|
@@ -7144,7 +7166,7 @@ var AUTH_HEADER_RE = /\b(?:bearer|basic|token|apikey|x-api-key)\s+([A-Za-z0-9._~
|
|
|
7144
7166
|
function hasSecretLikeValue(value) {
|
|
7145
7167
|
return detectSecretKind(value) !== "none";
|
|
7146
7168
|
}
|
|
7147
|
-
var PII_EMAIL_RE = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
|
|
7169
|
+
var PII_EMAIL_RE = /(?<![A-Z0-9._%+-])[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
|
|
7148
7170
|
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;
|
|
7149
7171
|
var PII_PHONE_RE = /(?:^|[^\w.@/:-])(\+?\(?\d{1,4}\)?[\s-]\(?\d{2,4}\)?[\s-]\d{2,4}(?:[\s-]\d{2,4}){0,2})(?![\w.@/:-])/g;
|
|
7150
7172
|
function hasPiiLikeValue(raw) {
|
|
@@ -7439,8 +7461,17 @@ var GIT_NETWORK_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
|
7439
7461
|
"receive-pack",
|
|
7440
7462
|
"lfs"
|
|
7441
7463
|
]);
|
|
7464
|
+
var POWERSHELL_LINE_RE = /\$env:|-ErrorAction\b|\bWrite-(?:Output|Host)\b|\b(?:Get|Set|New|Remove|Start|Stop|Invoke|Select|Where|ForEach|Test|Add|Format|Out|ConvertTo|ConvertFrom|Import|Export|Measure|Sort|Group|Restart|Register|Unregister|Enable|Disable|Copy|Move|Rename|Resolve|Split|Join|Read|Wait|Clear)-[A-Z][A-Za-z]+\b|@\{|@'|@"|\[(?:System\.|Security\.|IO\.|math\])|\$_\.|\$\w+\s*=\s*(?:Get|New|Read)-|\|\s*%\s|\|\s*\?\s/;
|
|
7465
|
+
var POWERSHELL_ONLY_SYNTAX_RE = /(?:^|[\s;{(|])\$[A-Za-z_][\w:]*\s*[+-]?=(?!=)|`"|(?:^|[\s;{])(?:if|elseif|foreach|while|switch)\s*\((?!\()|\}\s*(?:else|elseif|catch|finally)\b|\|\s*(?:where|foreach|%|\?)\s*\{/i;
|
|
7466
|
+
var POWERSHELL_ESCAPES_ONLY_RE = /^(?:[^`]|`[$"'`0abefnrtuv])*$/;
|
|
7467
|
+
function backtickMayExecute(segment) {
|
|
7468
|
+
if (!segment.includes("`")) return false;
|
|
7469
|
+
const code = maskQuotedSpans(segment);
|
|
7470
|
+
const powershell = POWERSHELL_LINE_RE.test(code) || POWERSHELL_ONLY_SYNTAX_RE.test(code);
|
|
7471
|
+
return !(powershell && POWERSHELL_ESCAPES_ONLY_RE.test(segment));
|
|
7472
|
+
}
|
|
7442
7473
|
function segmentMayReachNetwork(segment) {
|
|
7443
|
-
if (
|
|
7474
|
+
if (segment.includes("$(") || backtickMayExecute(segment) || PS_VALUE_EXECUTOR_RE.test(segment)) return true;
|
|
7444
7475
|
return splitShellSegments(segment, true).some((stage) => {
|
|
7445
7476
|
if (/^\s*(?:\$[\w:]+|[A-Za-z_]\w*)\s*=/.test(stage)) return true;
|
|
7446
7477
|
const masked = maskQuotedSpans(stage);
|
|
@@ -8090,7 +8121,7 @@ function classifyShellWords(masked, raw, lead, cwdScoped = false) {
|
|
|
8090
8121
|
if (redirect && !/^\/dev\/null$|^nul$|^\$null$/i.test(redirect[1])) return "write";
|
|
8091
8122
|
return "read";
|
|
8092
8123
|
}
|
|
8093
|
-
if (gcloudVerb
|
|
8124
|
+
if (GCLOUD_DELETE_VERB_RE.test(gcloudVerb)) return "DELETE";
|
|
8094
8125
|
if (GCLOUD_WRITE_VERB_RE.test(gcloudVerb)) return pipesIntoInterpreter ? "SHELL" : "write";
|
|
8095
8126
|
}
|
|
8096
8127
|
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";
|
|
@@ -8126,15 +8157,16 @@ function classifyShellWords(masked, raw, lead, cwdScoped = false) {
|
|
|
8126
8157
|
if (SHELL_BLOCK_STRUCTURE_RE.test(value)) return "read";
|
|
8127
8158
|
return "SHELL";
|
|
8128
8159
|
}
|
|
8129
|
-
var GCLOUD_READ_VERB_RE = /^(?:describe|list|ls|get-iam-policy|get-value|get-ancestors|read|check|
|
|
8130
|
-
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)$/;
|
|
8160
|
+
var GCLOUD_READ_VERB_RE = /^(?:describe|list|ls|get-iam-policy|get-value|get-ancestors|read|check|access)$/;
|
|
8161
|
+
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|add-[a-z-]+|remove-[a-z-]+|set-[a-z-]+|unset|clear|activate|revoke|copy)$/;
|
|
8162
|
+
var GCLOUD_DELETE_VERB_RE = /^(?:delete|destroy)$/;
|
|
8131
8163
|
function gcloudVerbWord(value) {
|
|
8132
8164
|
const tokens = value.split("|")[0].trim().split(/\s+/);
|
|
8133
8165
|
if (!/^gcloud(?:\.cmd|\.exe)?$/.test(tokens.shift() || "")) return null;
|
|
8134
8166
|
let depth = 0;
|
|
8135
8167
|
for (const token of tokens) {
|
|
8136
8168
|
if (token.startsWith("-")) continue;
|
|
8137
|
-
if (GCLOUD_READ_VERB_RE.test(token) || GCLOUD_WRITE_VERB_RE.test(token) || token
|
|
8169
|
+
if (GCLOUD_READ_VERB_RE.test(token) || GCLOUD_WRITE_VERB_RE.test(token) || GCLOUD_DELETE_VERB_RE.test(token)) return token;
|
|
8138
8170
|
if (!/^[a-z][a-z0-9-]*$/.test(token) || ++depth > 6) return null;
|
|
8139
8171
|
}
|
|
8140
8172
|
return null;
|
|
@@ -8315,7 +8347,7 @@ function shellCommandTextOf(toolName, args) {
|
|
|
8315
8347
|
return text || void 0;
|
|
8316
8348
|
}
|
|
8317
8349
|
function isPowerShellReadFormatter(segment) {
|
|
8318
|
-
if (/^(?:Select-Object\s+[\w., *-]+|Format-Table(?:\s+[\w., *-]+)?|Format-List(?:\s+[\w., *-]+)?|Out-Null|ConvertFrom-Json)$/i.test(segment)) return true;
|
|
8350
|
+
if (/^(?:Select-Object\s+[\w., *-]+|Format-Table(?:\s+[\w., *-]+)?|Format-List(?:\s+[\w., *-]+)?|Out-Null|Out-String(?:\s+-Stream)?|ConvertFrom-Json)$/i.test(segment)) return true;
|
|
8319
8351
|
const tokens = segment.trim().split(/\s+/);
|
|
8320
8352
|
if (tokens.shift()?.toLowerCase() !== "convertto-json") return false;
|
|
8321
8353
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -8384,10 +8416,11 @@ function provenCompoundDownload(command, detectedUrl) {
|
|
|
8384
8416
|
return segments.every((segment) => {
|
|
8385
8417
|
const statusRead = /^\(((?:Invoke-WebRequest|iwr)\s+[^\r\n]+)\)\.(?:StatusCode|StatusDescription|RawContentLength)$/i.exec(segment);
|
|
8386
8418
|
if (statusRead) segment = statusRead[1];
|
|
8419
|
+
segment = segment.replace(/\s+(?:[12&]?>\s*(?:\/dev\/null|\$null|nul)|2>&1)(?=\s|$)/gi, "");
|
|
8387
8420
|
const withoutWriteOut = segment.replace(/(\s(?:-w|--write-out)\s+)(?:"[^"\r\n$`@]*"|'[^'\r\n$`@]*')/g, '$1"fcd-write-out"');
|
|
8388
8421
|
if (/[$`{}()<>;&]/.test(withoutWriteOut)) return false;
|
|
8389
8422
|
const pipeline = splitShellSegments(segment, true);
|
|
8390
|
-
if (pipeline.slice(1).some((part) => !isPowerShellReadFormatter(part))) return false;
|
|
8423
|
+
if (pipeline.slice(1).some((part) => !isPowerShellReadFormatter(part) && !(classifyShellSegment(part) === "read" && !/[a-z][a-z0-9+.-]*:\/\//i.test(part)))) return false;
|
|
8391
8424
|
const raw = pipeline[0];
|
|
8392
8425
|
if (isGcloudStorageCatRead(raw)) return true;
|
|
8393
8426
|
const pieces = raw.match(/"[^"\r\n]*"|'[^'\r\n]*'|[^\s"']+/g) || [];
|
|
@@ -8431,6 +8464,13 @@ function provenCompoundDownload(command, detectedUrl) {
|
|
|
8431
8464
|
continue;
|
|
8432
8465
|
}
|
|
8433
8466
|
if (/^-[sfSLI]+$|^--(?:silent|fail|location|head)$|^-UseBasicParsing$/.test(token)) continue;
|
|
8467
|
+
if (!isCurl && /^wget/i.test(program)) {
|
|
8468
|
+
if (/^-q$|^--quiet$|^-q?O-$|^--output-document=-$/.test(token)) continue;
|
|
8469
|
+
if (/^-q?O$|^--output-document$/.test(token) && tokens[i2 + 1] === "-") {
|
|
8470
|
+
i2++;
|
|
8471
|
+
continue;
|
|
8472
|
+
}
|
|
8473
|
+
}
|
|
8434
8474
|
if (/^-TimeoutSec$/i.test(token) && /^\d+$/.test(tokens[i2 + 1] || "")) {
|
|
8435
8475
|
i2++;
|
|
8436
8476
|
continue;
|
|
@@ -8765,6 +8805,7 @@ function applyWebReadFacts(toolName, args, context) {
|
|
|
8765
8805
|
if (isLoopbackHost(parsed.hostname)) context["destination.loopback"] = "true";
|
|
8766
8806
|
else delete context["destination.loopback"];
|
|
8767
8807
|
}
|
|
8808
|
+
var SEARCH_PATTERN_FIELDS = ["query", "q", "pattern", "search", "searchQuery", "search_query", "term", "keyword", "keywords", "regex"];
|
|
8768
8809
|
function inferBundledToolContext(toolName, args) {
|
|
8769
8810
|
const context = {};
|
|
8770
8811
|
let operation = toolName;
|
|
@@ -8786,16 +8827,19 @@ function inferBundledToolContext(toolName, args) {
|
|
|
8786
8827
|
if (!reservedInputFact(key)) context[key] = String(val);
|
|
8787
8828
|
}
|
|
8788
8829
|
const nameWords = toolNameLower.replace(/[_\-.]/g, " ");
|
|
8789
|
-
const
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
|
|
8794
|
-
|
|
8830
|
+
const notMutating = !/\b(delete|remove|drop|write|update|insert|replace|create)\b/.test(nameWords);
|
|
8831
|
+
const isSearchTool = /\b(search|grep|find|lookup|locate)\b/.test(nameWords) && notMutating;
|
|
8832
|
+
const isFilterTool = isSearchTool || /\blist\b/.test(nameWords) && notMutating;
|
|
8833
|
+
if (isSearchTool) operation = "read";
|
|
8834
|
+
if (isFilterTool) {
|
|
8835
|
+
const patternKeys = SEARCH_PATTERN_FIELDS.filter((key) => typeof context[key] === "string");
|
|
8836
|
+
if (patternKeys.length) {
|
|
8837
|
+
context["search.pattern"] = patternKeys.map((key) => context[key]).join(" ");
|
|
8838
|
+
for (const key of patternKeys) delete context[key];
|
|
8795
8839
|
}
|
|
8796
8840
|
}
|
|
8797
8841
|
const isCodeExecTool = operation === toolName && CODE_EXEC_TOOL_NAME_RE.test(nameWords);
|
|
8798
|
-
const sqlArg = args.query || args.sql || args.statement || (isCodeExecTool ? "" : args.command || args.code || args.script || args.input) || "";
|
|
8842
|
+
const sqlArg = (isFilterTool ? "" : args.query) || args.sql || args.statement || (isCodeExecTool ? "" : args.command || args.code || args.script || args.input) || "";
|
|
8799
8843
|
if (!isSearchTool && typeof sqlArg === "string" && sqlArg.length > 0) {
|
|
8800
8844
|
const sqlMatch = sqlArg.match(/(?<!-)\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|TRUNCATE|GRANT|REVOKE|SHOW|DESCRIBE|EXPLAIN)\b(?!-)/i);
|
|
8801
8845
|
if (sqlMatch) {
|
|
@@ -8977,6 +9021,9 @@ function inventoryToolMatchesActionPolicy(toolName, toolActions, policies) {
|
|
|
8977
9021
|
ACTION_POLICY_GRANT_MAX_USES,
|
|
8978
9022
|
CONTENT_FACT_FIELDS,
|
|
8979
9023
|
DEFAULT_ACTION_POLICY_APPROVAL,
|
|
9024
|
+
GCLOUD_DELETE_VERB_RE,
|
|
9025
|
+
GCLOUD_READ_VERB_RE,
|
|
9026
|
+
GCLOUD_WRITE_VERB_RE,
|
|
8980
9027
|
MACHINE_KINDS,
|
|
8981
9028
|
WORKSTATION_ASK_APPROVAL,
|
|
8982
9029
|
approvalExceptionError,
|
|
@@ -8985,6 +9032,7 @@ function inventoryToolMatchesActionPolicy(toolName, toolActions, policies) {
|
|
|
8985
9032
|
detectSecretKind,
|
|
8986
9033
|
effectiveApprovalScope,
|
|
8987
9034
|
evaluateActionPolicies,
|
|
9035
|
+
gcloudVerbWord,
|
|
8988
9036
|
getShellAstMode,
|
|
8989
9037
|
inferBundledToolContext,
|
|
8990
9038
|
inferToolContext,
|
package/dist/approvalPrompt.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type AskHeader, type AskSubject } from './askHeader';
|
|
2
|
+
export type { AskHeader, AskSubject } from './askHeader';
|
|
1
3
|
export interface ApprovalMessageOptions {
|
|
2
4
|
/** The dialog offers "Always allow"; the footer must say what that covers. */
|
|
3
5
|
allowAlways?: boolean;
|
|
@@ -5,9 +7,20 @@ export interface ApprovalMessageOptions {
|
|
|
5
7
|
alwaysScopeLabel?: string;
|
|
6
8
|
/** How long it holds: "30 days" (default) or "7 days or 20 uses". */
|
|
7
9
|
alwaysLimitsLabel?: string;
|
|
10
|
+
/** What / How / Where the grant covers (compact rows). Absent: the scope label is the What. */
|
|
11
|
+
alwaysCovers?: AlwaysCovers;
|
|
12
|
+
/** Two-button ask: one short line saying why "Always allow" is not offered. */
|
|
13
|
+
alwaysNotOffered?: string;
|
|
14
|
+
/** Facts from the ask envelope (grant scope, rule id) for the "Why you're asked" header. */
|
|
15
|
+
subject?: AskSubject;
|
|
8
16
|
/** Visual variant. Ask is the interactive dialog; approval/block share the card, not extra buttons. */
|
|
9
17
|
tone?: DialogTone;
|
|
10
18
|
}
|
|
19
|
+
export interface AlwaysCovers {
|
|
20
|
+
what: string;
|
|
21
|
+
how?: string;
|
|
22
|
+
where?: string;
|
|
23
|
+
}
|
|
11
24
|
/** Which policy outcome this window is showing. Icons and copy follow this; buttons do not. */
|
|
12
25
|
export type DialogTone = 'ask' | 'approval' | 'block';
|
|
13
26
|
export interface NativeDialogView {
|
|
@@ -15,6 +28,8 @@ export interface NativeDialogView {
|
|
|
15
28
|
badge: string;
|
|
16
29
|
headline: string;
|
|
17
30
|
subtitle: string;
|
|
31
|
+
/** Ask dialogs: why it popped up and the facts behind it, above everything else. */
|
|
32
|
+
header?: AskHeader;
|
|
18
33
|
kind?: string;
|
|
19
34
|
/** The action: the whole script when the explanation carried it, else the short "What" line. */
|
|
20
35
|
action?: string;
|
package/dist/approvalPrompt.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.nativeDialogView = nativeDialogView;
|
|
4
4
|
exports.nativeApprovalTitle = nativeApprovalTitle;
|
|
5
5
|
exports.nativeApprovalMessage = nativeApprovalMessage;
|
|
6
|
+
const askHeader_1 = require("./askHeader");
|
|
6
7
|
/** Compact OS prompt. The complete decision remains in the IDE response and audit. */
|
|
7
8
|
function line(value, limit) {
|
|
8
9
|
const text = value.replace(/\s+/g, ' ').trim();
|
|
@@ -12,6 +13,23 @@ function line(value, limit) {
|
|
|
12
13
|
const head = Math.ceil((limit - 3) * 0.6);
|
|
13
14
|
return `${text.slice(0, head)} … ${text.slice(-(limit - head - 3))}`;
|
|
14
15
|
}
|
|
16
|
+
/** `What: gcloud · How: change · Where: project acme-staging · 30 days, this machine`. */
|
|
17
|
+
function alwaysCoversParts(options) {
|
|
18
|
+
const covers = options.alwaysCovers || (options.alwaysScopeLabel ? { what: options.alwaysScopeLabel } : undefined);
|
|
19
|
+
if (!options.allowAlways || !covers)
|
|
20
|
+
return undefined;
|
|
21
|
+
return [
|
|
22
|
+
`What: ${line(covers.what, 90)}`,
|
|
23
|
+
covers.how ? `How: ${line(covers.how, 60)}` : '',
|
|
24
|
+
covers.where ? `Where: ${line(covers.where, 180)}` : '',
|
|
25
|
+
`${options.alwaysLimitsLabel || '30 days'}, this machine`,
|
|
26
|
+
].filter(Boolean).join(' · ');
|
|
27
|
+
}
|
|
28
|
+
/** `Always allow covers — What: gcloud · How: change · Where: project acme-staging · 30 days, this machine`. */
|
|
29
|
+
function alwaysCoversLine(options) {
|
|
30
|
+
const parts = alwaysCoversParts(options);
|
|
31
|
+
return parts ? `Always allow covers — ${parts}` : undefined;
|
|
32
|
+
}
|
|
15
33
|
/** The full script is shown in a scrolling box; this only guards against a runaway payload. */
|
|
16
34
|
const MAX_DIALOG_SCRIPT = 6000;
|
|
17
35
|
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)\.)?/;
|
|
@@ -19,13 +37,72 @@ const CEILING_NOTE_SHORT = 'set to Block; on your workstation it asks you first.
|
|
|
19
37
|
/** Parse the `Rule:` line the block explanation writes into the name the console lists and its scope. */
|
|
20
38
|
function ruleParts(rule) {
|
|
21
39
|
const policy = /^Action Policy "(.+?)" \((.+?)\)(?: — rule: (.+))?$/.exec(rule);
|
|
22
|
-
if (policy)
|
|
23
|
-
|
|
40
|
+
if (policy) {
|
|
41
|
+
const scopeWords = policy[2].replace(/^[^\w]+/, '');
|
|
42
|
+
return { policy: policy[1], scopeWords, ...(policy[3] ? { matched: policy[3] } : {}), scope: [scopeWords, policy[3]].filter(Boolean).join(' · ') };
|
|
43
|
+
}
|
|
24
44
|
const local = /^(built-in Local Safety rule|org custom rule) "(.+?)"(?: in Local Safety)?(?: \((.+)\))?$/.exec(rule);
|
|
25
45
|
if (local)
|
|
26
|
-
return { policy: local[2], localSafety: true, scope: [local[1] === 'org custom rule' ? 'Local Safety — org custom rule' : 'Local Safety — built-in rule', local[3]].filter(Boolean).join(' · ') };
|
|
46
|
+
return { policy: local[2], localSafety: true, customRule: local[1] === 'org custom rule', ...(local[3] ? { ruleDetail: local[3] } : {}), scope: [local[1] === 'org custom rule' ? 'Local Safety — org custom rule' : 'Local Safety — built-in rule', local[3]].filter(Boolean).join(' · ') };
|
|
27
47
|
return { policy: rule };
|
|
28
48
|
}
|
|
49
|
+
/** `toolArgs.command matches gcloud *` → "gcloud commands"; unknown shapes → undefined (the IDE message keeps the raw rule). */
|
|
50
|
+
function constraintWords(constraint) {
|
|
51
|
+
const m = /^(?:toolArgs\.)?([\w.]+) (equals|matches|contains|startsWith|in|notEquals|not_contains) (.+)$/.exec(constraint.trim());
|
|
52
|
+
if (!m)
|
|
53
|
+
return undefined;
|
|
54
|
+
const [, field, op, raw] = m;
|
|
55
|
+
const value = raw.trim();
|
|
56
|
+
if (field === 'command' || field === 'cmd') {
|
|
57
|
+
const lead = value.replace(/\s*\*+\s*$/, '').trim();
|
|
58
|
+
if ((op === 'matches' || op === 'startsWith') && /^[\w.-]+(?: [\w.-]+)?$/.test(lead))
|
|
59
|
+
return `${lead} commands`;
|
|
60
|
+
if (op === 'contains')
|
|
61
|
+
return `commands that contain "${value}"`;
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
if (field === 'destination.type' && op === 'equals')
|
|
65
|
+
return `actions that reach ${value === 'external' ? 'an external' : `a ${value}`} destination`;
|
|
66
|
+
if ((field === 'destination.domain' || field === 'destination.host') && op === 'equals')
|
|
67
|
+
return `actions that reach ${value}`;
|
|
68
|
+
if (field === 'agentClient' && op === 'equals')
|
|
69
|
+
return `actions by ${value}`;
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
const VERDICT_WORDS = { require_approval: 'asks for approval', block: 'blocks', allow: 'allows', log: 'records', warn: 'warns' };
|
|
73
|
+
const OPERATION_WORDS = { SHELL: 'shell commands', EXECUTE: 'commands', DELETE: 'deletes', write: 'writes', read: 'reads', delete: 'deletes' };
|
|
74
|
+
/**
|
|
75
|
+
* The rule that asked, in customer words — never engine syntax:
|
|
76
|
+
* `Company policy "Cloud changes need OK" asks for approval on gcloud commands, for every machine.`
|
|
77
|
+
* The raw matcher (`SHELL → require_approval (toolArgs.command matches gcloud *)`) stays in
|
|
78
|
+
* the IDE message's `Rule:` line.
|
|
79
|
+
*/
|
|
80
|
+
function ruleSentence(f, tone) {
|
|
81
|
+
if (!f.policy || f.fallback)
|
|
82
|
+
return undefined;
|
|
83
|
+
const name = `"${line(f.policy, 80)}"`;
|
|
84
|
+
if (f.localSafety) {
|
|
85
|
+
const category = (f.ruleDetail || '').split(',').map(s => s.trim()).find(s => s && !/^id /.test(s));
|
|
86
|
+
const kind = `${f.customRule ? "Your organization's" : 'Built-in'} Local Safety rule ${name}${category ? ` (${category.replace(/_/g, ' ')})` : ''}`;
|
|
87
|
+
return `${kind} ${tone === 'block' ? 'blocks this' : tone === 'approval' ? 'needs approval for this' : 'asks before this runs'}.`;
|
|
88
|
+
}
|
|
89
|
+
const owner = /^company default/i.test(f.scopeWords || '') ? 'Company policy' : /^local rule/i.test(f.scopeWords || '') ? 'Local rule' : 'Policy';
|
|
90
|
+
const whom = /applies to (.+)$/i.exec(f.scopeWords || '')?.[1] || /only (.+)$/i.exec(f.scopeWords || '')?.[1];
|
|
91
|
+
const m = /^(\S+) → (\w+)(?: \((.*)\))?$/.exec(f.matched || '');
|
|
92
|
+
const verb = m ? VERDICT_WORDS[m[2]] || 'asks' : 'asks';
|
|
93
|
+
const on = m && m[3] ? m[3].split(/,\s*(?=[\w.]+ (?:equals|matches|contains|startsWith|in|notEquals|not_contains) )/).map(constraintWords) : [];
|
|
94
|
+
const what = on.length && on.every(Boolean) ? on.join(' and ')
|
|
95
|
+
: m && !m[3] ? unique(m[1].split('/').map(op => OPERATION_WORDS[op]).filter(Boolean)).join(' and ') || undefined
|
|
96
|
+
: undefined;
|
|
97
|
+
return `${owner} ${name} ${verb}${what ? ` on ${what}` : ''}${whom ? `, for ${whom}` : ''}.`;
|
|
98
|
+
}
|
|
99
|
+
/** The header sentence already states the fact the rule fired on (cloud, credential, a local removal). */
|
|
100
|
+
function saysTheFact(header) {
|
|
101
|
+
return header.source === 'cloud' || header.source === 'credential' || (header.source === 'operation' && Boolean(header.affects));
|
|
102
|
+
}
|
|
103
|
+
function unique(values) {
|
|
104
|
+
return [...new Set(values)];
|
|
105
|
+
}
|
|
29
106
|
function promptFacts(reason) {
|
|
30
107
|
const rows = reason.split(/\r?\n/).map(row => row.trim()).filter(Boolean);
|
|
31
108
|
const field = (name) => rows.find(row => row.startsWith(`${name}:`))?.slice(name.length + 1).trim();
|
|
@@ -61,8 +138,10 @@ function promptFacts(reason) {
|
|
|
61
138
|
facts.kind = kind?.[2];
|
|
62
139
|
facts.action = kind ? kind[1] : what;
|
|
63
140
|
}
|
|
64
|
-
if (tool)
|
|
141
|
+
if (tool) {
|
|
65
142
|
facts.tool = [tool, evidence].filter(Boolean).join(' · ');
|
|
143
|
+
facts.toolName = tool;
|
|
144
|
+
}
|
|
66
145
|
if (why) {
|
|
67
146
|
const stripped = why.replace(CEILING_NOTE_RE, ' ').replace(/\s+/g, ' ').trim();
|
|
68
147
|
if (stripped !== why.trim())
|
|
@@ -74,6 +153,24 @@ function promptFacts(reason) {
|
|
|
74
153
|
function sentences(text) {
|
|
75
154
|
return text.split(/(?<=\.)\s+/).map(part => part.trim()).filter(Boolean);
|
|
76
155
|
}
|
|
156
|
+
/** The full action text: the script when the explanation carried it, else the "What" line. */
|
|
157
|
+
function actionText(f) {
|
|
158
|
+
return f.script ? f.script.replace(/ ⏎ /g, '\n') : f.action;
|
|
159
|
+
}
|
|
160
|
+
function askHeaderFor(f, options) {
|
|
161
|
+
return (0, askHeader_1.buildAskHeader)({
|
|
162
|
+
command: actionText(f),
|
|
163
|
+
kind: f.kind,
|
|
164
|
+
tool: f.toolName,
|
|
165
|
+
policy: f.policy,
|
|
166
|
+
ruleDetail: f.ruleDetail,
|
|
167
|
+
ide: f.ide,
|
|
168
|
+
subject: options.subject,
|
|
169
|
+
alwaysOffered: Boolean(options.allowAlways),
|
|
170
|
+
alwaysCovers: alwaysCoversParts(options),
|
|
171
|
+
alwaysNotOffered: options.alwaysNotOffered,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
77
174
|
/**
|
|
78
175
|
* Layout model for the branded OS dialog. Same facts as the compact prompt;
|
|
79
176
|
* skips mockup-only chrome (other-outcome chips, Request approval, project tags).
|
|
@@ -81,8 +178,19 @@ function sentences(text) {
|
|
|
81
178
|
function nativeDialogView(reason, options = {}) {
|
|
82
179
|
const f = promptFacts(reason);
|
|
83
180
|
const tone = options.tone || 'ask';
|
|
181
|
+
// Free-form text carries no facts to put in the header.
|
|
182
|
+
const header = tone === 'ask' && !f.fallback ? askHeaderFor(f, options) : undefined;
|
|
183
|
+
const because = f.because ? sentences(line(f.because, 400)) : [];
|
|
184
|
+
// The header already says the fact a cloud / credential rule fired on; the rule line only names the rule.
|
|
185
|
+
if (header && saysTheFact(header))
|
|
186
|
+
because.shift();
|
|
187
|
+
// The rule by name, its source and who it applies to — in customer words; the engine
|
|
188
|
+
// matcher stays in the IDE message. Then the fact it fired on.
|
|
189
|
+
if (because.length && /^[a-z]/.test(because[0]))
|
|
190
|
+
because[0] = `Because ${because[0]}`;
|
|
84
191
|
const why = [
|
|
85
|
-
|
|
192
|
+
ruleSentence(f, tone) || '',
|
|
193
|
+
...because,
|
|
86
194
|
f.note ? line(f.note, 180) : '',
|
|
87
195
|
f.fallback ? line(f.fallback, 400) : '',
|
|
88
196
|
].filter(Boolean);
|
|
@@ -90,13 +198,14 @@ function nativeDialogView(reason, options = {}) {
|
|
|
90
198
|
? ['This action is stopped.', 'An admin can change the rule in the console.']
|
|
91
199
|
: tone === 'approval'
|
|
92
200
|
? ['An admin or manager can approve it in the console.', 'Closing this window does not approve it.']
|
|
201
|
+
// With a header, what Always allow covers (or why it is not offered) is its last row.
|
|
93
202
|
: [
|
|
94
|
-
'Allow once lets only this action
|
|
95
|
-
'
|
|
96
|
-
options.allowAlways && options.alwaysScopeLabel
|
|
97
|
-
? `Always allow: ${line(options.alwaysScopeLabel, 180)} for ${options.alwaysLimitsLabel || '30 days'} on this machine, while the rule stays as it is written today.`
|
|
98
|
-
: '',
|
|
203
|
+
'Allow once lets only this action run; Deny or close keeps it stopped.',
|
|
204
|
+
header ? '' : alwaysCoversLine(options) || (!options.allowAlways && options.alwaysNotOffered ? line(options.alwaysNotOffered, 120) : ''),
|
|
99
205
|
].filter(Boolean);
|
|
206
|
+
// One statement shown whole in the header's Command row: the step list and script box would repeat it.
|
|
207
|
+
const full = actionText(f)?.replace(/\s+/g, ' ').trim();
|
|
208
|
+
const shownWhole = Boolean(header && full && header.command === full);
|
|
100
209
|
const footer = f.policy && !f.fallback
|
|
101
210
|
? `Full details: the IDE message and this machine’s Activity. Console → Action Policies → ${f.localSafety ? 'Source: Local Safety → ' : ''}"${line(f.policy, 80)}".`
|
|
102
211
|
: 'Full details are in the IDE message and this machine’s Activity in FullCourtDefense.';
|
|
@@ -109,10 +218,11 @@ function nativeDialogView(reason, options = {}) {
|
|
|
109
218
|
: tone === 'approval'
|
|
110
219
|
? 'A security policy paused this action for an admin or manager.'
|
|
111
220
|
: 'A security policy paused this action. Review the details and choose what to do next.',
|
|
221
|
+
...(header ? { header } : {}),
|
|
112
222
|
kind: f.kind,
|
|
113
223
|
// The whole script when we have it (the box scrolls); the short line otherwise.
|
|
114
|
-
action:
|
|
115
|
-
steps: (f.steps || []).slice(0, 14).map(step => line(step, 220)),
|
|
224
|
+
action: shownWhole ? undefined : f.script ? actionText(f) : f.action ? line(f.action, 280) : undefined,
|
|
225
|
+
steps: shownWhole ? [] : (f.steps || []).slice(0, 14).map(step => line(step, 220)),
|
|
116
226
|
policy: f.policy ? line(f.policy, 160) : undefined,
|
|
117
227
|
scope: f.scope ? line(f.scope, 220) : undefined,
|
|
118
228
|
agent: f.agent ? line(f.agent, 80) : undefined,
|
|
@@ -129,17 +239,41 @@ function nativeApprovalTitle(reason) {
|
|
|
129
239
|
const parts = ['FullCourtDefense', f.kind ? `Approval needed: ${f.kind}` : 'Approval needed', f.policy ? line(f.policy, 60) : ''].filter(Boolean);
|
|
130
240
|
return parts.join(' · ');
|
|
131
241
|
}
|
|
242
|
+
/** The header as text rows: the sentence, then Command / Action / Where / Always allow. */
|
|
243
|
+
function headerLines(header) {
|
|
244
|
+
return [
|
|
245
|
+
header.who,
|
|
246
|
+
`Why you're asked: ${header.why}`,
|
|
247
|
+
`${`${header.commandLabel}:`.padEnd(9)}${header.command}`,
|
|
248
|
+
// One line in the compact text (the card draws Affects as its own row).
|
|
249
|
+
`Action: ${header.action}${header.affects ? ` — affects ${header.affects}` : ''}`,
|
|
250
|
+
`Where: ${header.whereText}`,
|
|
251
|
+
...(header.secretSentTo ? [`${header.secretSentTo.label}: ${header.secretSentTo.value}`] : []),
|
|
252
|
+
`Always allow: ${header.always}`,
|
|
253
|
+
];
|
|
254
|
+
}
|
|
132
255
|
function nativeApprovalMessage(reason, options = {}) {
|
|
133
256
|
const f = promptFacts(reason);
|
|
257
|
+
const header = !f.fallback && (options.tone || 'ask') === 'ask' ? askHeaderFor(f, options) : undefined;
|
|
258
|
+
// As in the card: the fact a cloud / credential rule fired on is the header's sentence, not repeated.
|
|
259
|
+
const because = header && saysTheFact(header) && f.because
|
|
260
|
+
? sentences(f.because).slice(1).join(' ')
|
|
261
|
+
: f.because;
|
|
262
|
+
const full = actionText(f)?.replace(/\s+/g, ' ').trim();
|
|
263
|
+
const steps = header && full && header.command === full ? [] : f.steps || [];
|
|
134
264
|
const details = f.fallback ? [line(f.fallback, 650)] : [
|
|
265
|
+
...(header ? headerLines(header) : []),
|
|
135
266
|
f.policy ? `Policy: ${line(f.policy, 160)}` : '',
|
|
136
|
-
|
|
137
|
-
f.
|
|
138
|
-
|
|
267
|
+
// Where the rule comes from, in customer words (the raw matcher is on the IDE message's Rule: line).
|
|
268
|
+
f.policy ? ` ${line(ruleSentence(f, options.tone || 'ask') || f.scope || '', 220)}` : '',
|
|
269
|
+
// The header's Command row is the action; without a header the action line stays.
|
|
270
|
+
f.action && !header ? `Action: ${f.kind ? `${f.kind} — ` : ''}${line(f.action, 230)}` : '',
|
|
271
|
+
...steps.slice(0, 6).map((step, index) => `${index === 0 ? 'Does: ' : ' '}${index + 1}. ${line(step, 160)}`),
|
|
139
272
|
f.agent ? `Agent: ${line(f.agent, 80)}` : '',
|
|
140
|
-
|
|
273
|
+
// The header's first line already names the client.
|
|
274
|
+
f.ide && !header ? `IDE: ${line(f.ide, 40)}` : '',
|
|
141
275
|
f.tool ? `Tool: ${line(f.tool, 230)}` : '',
|
|
142
|
-
|
|
276
|
+
because ? `Because: ${line(because, 300)}` : '',
|
|
143
277
|
f.note ? `Note: ${f.note}` : '',
|
|
144
278
|
].filter(Boolean);
|
|
145
279
|
const where = f.policy && !f.fallback
|
|
@@ -148,15 +282,18 @@ function nativeApprovalMessage(reason, options = {}) {
|
|
|
148
282
|
if (!options.allowAlways) {
|
|
149
283
|
return [
|
|
150
284
|
'Allow this action once?',
|
|
151
|
-
''
|
|
285
|
+
// The header's "Why you're asked" answers the question directly below it.
|
|
286
|
+
...(header ? [] : ['']), ...details, '',
|
|
287
|
+
...(options.alwaysNotOffered && !header ? [line(options.alwaysNotOffered, 120)] : []),
|
|
152
288
|
'Allow once permits only this action. Deny or closing keeps it stopped.',
|
|
153
289
|
where,
|
|
154
290
|
].join('\n');
|
|
155
291
|
}
|
|
292
|
+
const covers = header ? undefined : alwaysCoversLine(options);
|
|
156
293
|
return [
|
|
157
294
|
'Allow this action?',
|
|
158
|
-
'', ...details, '',
|
|
159
|
-
...(
|
|
295
|
+
...(header ? [] : ['']), ...details, '',
|
|
296
|
+
...(covers ? [covers, ''] : []),
|
|
160
297
|
'Allow once permits only this action. Deny or closing keeps it stopped.',
|
|
161
298
|
where,
|
|
162
299
|
].join('\n');
|
package/dist/askDialog.d.ts
CHANGED
|
@@ -54,6 +54,8 @@ export interface AskEnvelope {
|
|
|
54
54
|
grantLimits?: GrantLimits | null;
|
|
55
55
|
/** Text for the dialog (the same explanation the IDE card shows). */
|
|
56
56
|
reason: string;
|
|
57
|
+
/** The client the runtime event records as `agentClient` (cursor, claude-code, …): the dialog names the same agent as the console. */
|
|
58
|
+
agentClient?: string;
|
|
57
59
|
}
|
|
58
60
|
export type AskResolution = 'allow' | 'allow_always' | 'deny' | 'timeout' | 'ide';
|
|
59
61
|
export interface HookVerdict {
|
package/dist/askDialog.js
CHANGED
|
@@ -144,12 +144,26 @@ async function resolveAskWithDialog(verdict, options = {}) {
|
|
|
144
144
|
const allowAlways = envelope.remember !== 'none' && Boolean(grantHash) && Boolean(grantLimits) && (0, devConfirm_1.grantOffered)(envelope.scope);
|
|
145
145
|
const scopeLabel = allowAlways ? (0, devConfirm_1.describeGrantScope)(envelope.scope, { policyId: envelope.policyId, policyName: envelope.policyName }) : undefined;
|
|
146
146
|
const limitsLabel = allowAlways && grantLimits ? (0, devConfirm_1.describeGrantLimits)(grantLimits) : undefined;
|
|
147
|
+
const alwaysCovers = allowAlways ? (0, devConfirm_1.grantCoverage)(envelope.scope) : undefined;
|
|
148
|
+
// The scope's own refusal; `remember: none` / org switch-off / no rule identity travel as `subject.alwaysOff`.
|
|
149
|
+
const alwaysNotOffered = !allowAlways && envelope.remember !== 'none' && grantHash && grantLimits ? (0, devConfirm_1.grantNotOfferedReason)(envelope.scope) : undefined;
|
|
150
|
+
const subject = {
|
|
151
|
+
event: envelope.event,
|
|
152
|
+
operation: envelope.operation,
|
|
153
|
+
...(envelope.scope.cloud ? { cloud: envelope.scope.cloud } : {}),
|
|
154
|
+
...(envelope.scope.destinationDomain ? { destinationDomain: envelope.scope.destinationDomain } : {}),
|
|
155
|
+
...(envelope.scope.credential?.hosts?.length ? { credentialHosts: envelope.scope.credential.hosts } : {}),
|
|
156
|
+
...(envelope.policyId ? { ruleId: envelope.policyId } : {}),
|
|
157
|
+
// The client the runtime record carries; older hook builds leave it off (the `--agent-client` flag then).
|
|
158
|
+
...(envelope.agentClient || options.clientLabel ? { agentClient: envelope.agentClient || options.clientLabel } : {}),
|
|
159
|
+
...(allowAlways || alwaysNotOffered ? {} : { alwaysOff: envelope.remember === 'none' ? 'rule' : envelope.grantLimits === null ? 'org' : 'no_rule' }),
|
|
160
|
+
};
|
|
147
161
|
const dialog = options.dialog || devConfirm_1.confirmNativeActionDetailed;
|
|
148
162
|
const reason = options.clientLabel ? `IDE: ${options.clientLabel}\n${envelope.reason}` : envelope.reason;
|
|
149
163
|
let outcome = 'unavailable';
|
|
150
164
|
let latencyMs = 0;
|
|
151
165
|
try {
|
|
152
|
-
({ outcome, latencyMs } = await dialog(reason, options.timeoutMs ?? exports.ASK_DIALOG_TIMEOUT_MS, { allowAlways, alwaysScopeLabel: scopeLabel, alwaysLimitsLabel: limitsLabel }));
|
|
166
|
+
({ outcome, latencyMs } = await dialog(reason, options.timeoutMs ?? exports.ASK_DIALOG_TIMEOUT_MS, { allowAlways, alwaysScopeLabel: scopeLabel, alwaysLimitsLabel: limitsLabel, alwaysCovers, alwaysNotOffered, subject }));
|
|
153
167
|
}
|
|
154
168
|
catch {
|
|
155
169
|
outcome = 'unavailable';
|