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/devConfirm.js
CHANGED
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.GRANT_MAX_USES_LIMIT = exports.GRANT_TTL_MS = exports.CONFIRMED_ACTION_TTL_MS = exports.CONFIRM_PENDING_TTL_MS = exports.DIALOG_SCRIPT_MARKER = exports.DIALOG_BUTTON_DENY = exports.DIALOG_BUTTON_ONCE = exports.DIALOG_BUTTON_ALWAYS = exports.MIN_HUMAN_ANSWER_MS = void 0;
|
|
36
|
+
exports.GRANT_MAX_USES_LIMIT = exports.GRANT_TTL_MS = exports.CONFIRMED_ACTION_TTL_MS = exports.CONFIRM_PENDING_TTL_MS = exports.DIALOG_SCRIPT_MARKER = exports.DIALOG_BUTTON_DENY = exports.DIALOG_BUTTON_ONCE = exports.DIALOG_BUTTON_ALWAYS = exports.MIN_HUMAN_ANSWER_MS = exports.isHumanPresent = exports.isCiEnvironment = void 0;
|
|
37
37
|
exports.looksLikeClientAutoAnswer = looksLikeClientAutoAnswer;
|
|
38
38
|
exports.confirmNativeAction = confirmNativeAction;
|
|
39
39
|
exports.nativeDialogAvailable = nativeDialogAvailable;
|
|
@@ -41,8 +41,6 @@ exports.confirmNativeActionDetailed = confirmNativeActionDetailed;
|
|
|
41
41
|
exports.resolveGrantLimits = resolveGrantLimits;
|
|
42
42
|
exports.describeGrantLimits = describeGrantLimits;
|
|
43
43
|
exports.grantRemainingUses = grantRemainingUses;
|
|
44
|
-
exports.isCiEnvironment = isCiEnvironment;
|
|
45
|
-
exports.isHumanPresent = isHumanPresent;
|
|
46
44
|
exports.actionDigest = actionDigest;
|
|
47
45
|
exports.actionPolicyRuleHash = actionPolicyRuleHash;
|
|
48
46
|
exports.localSafetyRuleHash = localSafetyRuleHash;
|
|
@@ -55,6 +53,7 @@ exports.settleDeveloperConfirmation = settleDeveloperConfirmation;
|
|
|
55
53
|
exports.rejectDeveloperConfirmation = rejectDeveloperConfirmation;
|
|
56
54
|
exports.sweepExpiredConfirmations = sweepExpiredConfirmations;
|
|
57
55
|
exports.shellPrograms = shellPrograms;
|
|
56
|
+
exports.shellProgramsForRule = shellProgramsForRule;
|
|
58
57
|
exports.splitLocalSafetyRuleIds = splitLocalSafetyRuleIds;
|
|
59
58
|
exports.findDeveloperGrant = findDeveloperGrant;
|
|
60
59
|
exports.recordDeveloperGrant = recordDeveloperGrant;
|
|
@@ -94,6 +93,10 @@ const os = __importStar(require("os"));
|
|
|
94
93
|
const path = __importStar(require("path"));
|
|
95
94
|
const approvalPrompt_1 = require("./approvalPrompt");
|
|
96
95
|
const deterministicGuard_1 = require("./commands/deterministicGuard");
|
|
96
|
+
const callerEnv_1 = require("./callerEnv");
|
|
97
|
+
var callerEnv_2 = require("./callerEnv");
|
|
98
|
+
Object.defineProperty(exports, "isCiEnvironment", { enumerable: true, get: function () { return callerEnv_2.isCiEnvironment; } });
|
|
99
|
+
Object.defineProperty(exports, "isHumanPresent", { enumerable: true, get: function () { return callerEnv_2.isHumanPresent; } });
|
|
97
100
|
/**
|
|
98
101
|
* Fastest plausible HUMAN answer to a confirmation prompt. A person has to notice
|
|
99
102
|
* the dialog, read at least the tool name and press a button; MCP clients that
|
|
@@ -126,11 +129,12 @@ exports.DIALOG_BUTTON_DENY = 'Deny';
|
|
|
126
129
|
exports.DIALOG_SCRIPT_MARKER = 'FullCourtDefense.Dialog';
|
|
127
130
|
/** True when this process can put a dialog in front of a person on this machine. */
|
|
128
131
|
function nativeDialogAvailable() {
|
|
129
|
-
if (!isHumanPresent())
|
|
132
|
+
if (!(0, callerEnv_1.isHumanPresent)())
|
|
130
133
|
return false;
|
|
131
134
|
if (process.platform === 'win32' || process.platform === 'darwin')
|
|
132
135
|
return true;
|
|
133
|
-
|
|
136
|
+
const env = (0, callerEnv_1.callerEnv)();
|
|
137
|
+
return Boolean(env.DISPLAY || env.WAYLAND_DISPLAY);
|
|
134
138
|
}
|
|
135
139
|
function b64(value) {
|
|
136
140
|
return Buffer.from(value, 'utf8').toString('base64');
|
|
@@ -430,28 +434,6 @@ function grantExhausted(grant, limits) {
|
|
|
430
434
|
const cap = [grant.maxUses, limits?.maxUses].filter((c) => typeof c === 'number' && c >= 1);
|
|
431
435
|
return cap.length > 0 && (Number(grant.uses) || 0) >= Math.min(...cap);
|
|
432
436
|
}
|
|
433
|
-
/** CI / pipeline environments — no human at a keyboard. Shared by hooks and discovery. */
|
|
434
|
-
function isCiEnvironment(env = process.env) {
|
|
435
|
-
return env.GITHUB_ACTIONS === 'true'
|
|
436
|
-
|| env.GITLAB_CI === 'true'
|
|
437
|
-
|| env.TF_BUILD === 'True' // Azure Pipelines
|
|
438
|
-
|| !!env.JENKINS_URL
|
|
439
|
-
|| !!env.BUILDKITE
|
|
440
|
-
|| !!env.CIRCLECI
|
|
441
|
-
|| env.CI === 'true';
|
|
442
|
-
}
|
|
443
|
-
/**
|
|
444
|
-
* Whether a developer can answer an IDE permission prompt on this machine. False on CI runners
|
|
445
|
-
* and on ephemeral workloads (`FCD_EPHEMERAL=1`, exported by `fcd workload-protect`) — there a
|
|
446
|
-
* developer-scoped approval must go to the org queue or block, never self-resolve.
|
|
447
|
-
*/
|
|
448
|
-
function isHumanPresent(env = process.env) {
|
|
449
|
-
if (env.FCD_EPHEMERAL === '1' || env.FCD_EPHEMERAL === 'true' || env.FULLCOURTDEFENSE_EPHEMERAL === '1')
|
|
450
|
-
return false;
|
|
451
|
-
if (env.FCD_HUMAN_PRESENT === '0' || env.FCD_HUMAN_PRESENT === 'false')
|
|
452
|
-
return false;
|
|
453
|
-
return !isCiEnvironment(env);
|
|
454
|
-
}
|
|
455
437
|
function ledgerFile() {
|
|
456
438
|
return path.join(os.homedir(), '.fullcourtdefense', 'dev-confirm.json');
|
|
457
439
|
}
|
|
@@ -832,6 +814,12 @@ const SHELL_TEXT_FILTERS = new Set([
|
|
|
832
814
|
'grep', 'egrep', 'fgrep', 'rg', 'findstr', 'select-string', 'sls', 'awk', 'gawk', 'sed',
|
|
833
815
|
'head', 'tail', 'wc', 'sort', 'uniq', 'cut', 'tr', 'jq', 'yq', 'column', 'nl', 'more', 'less', 'cat', 'tee',
|
|
834
816
|
]);
|
|
817
|
+
/**
|
|
818
|
+
* PowerShell aliases of the pipeline / formatting cmdlets above (`| Select -Last 25`,
|
|
819
|
+
* `| ft`, `| measure`). Output shaping in pipeline position, like their full names.
|
|
820
|
+
* Piped-only: leading a statement `sort` / `where` are real programs.
|
|
821
|
+
*/
|
|
822
|
+
const SHELL_PIPELINE_ALIASES = new Set(['select', 'where', 'sort', 'group', 'measure', 'compare', 'ft', 'fl', 'fw', 'tee']);
|
|
835
823
|
/**
|
|
836
824
|
* Cheap quote-aware split on `; && || | \n` — the grant needs program NAMES, not
|
|
837
825
|
* semantics. Separators inside `{ … }` do not split: a control statement's
|
|
@@ -944,6 +932,37 @@ function braceBodies(statement) {
|
|
|
944
932
|
bodies.push(statement.slice(start));
|
|
945
933
|
return bodies.map(body => body.trim()).filter(Boolean);
|
|
946
934
|
}
|
|
935
|
+
/**
|
|
936
|
+
* Programs that change only local file-system structure — they cannot send
|
|
937
|
+
* data off the machine or run other code. Dropped from a grant only when the
|
|
938
|
+
* rule declares its own programs (`gcloud_secret_access_cmd` is about `gcloud`,
|
|
939
|
+
* not the `Remove-Item $tmp` that cleans up after it). Writers that take
|
|
940
|
+
* content (`New-Item -Value`, `Set-Content`, `Copy-Item` to a share) stay.
|
|
941
|
+
*/
|
|
942
|
+
const SHELL_LOCAL_FILE_OPS = new Set(['remove-item', 'ri', 'rm', 'del', 'erase', 'rmdir', 'rd', 'mkdir', 'md', 'new-temporaryfile', 'mktemp']);
|
|
943
|
+
/** `git` subcommands that read or change the local repository only (no remote, no pager/diff driver override). */
|
|
944
|
+
const GIT_LOCAL_SUBCOMMANDS = new Set([
|
|
945
|
+
'status', 'diff', 'log', 'show', 'add', 'commit', 'rev-parse', 'ls-files', 'branch', 'stash', 'restore', 'switch', 'checkout', 'reset',
|
|
946
|
+
]);
|
|
947
|
+
/** git flags that run a configured command or write output elsewhere: never local-only. */
|
|
948
|
+
const GIT_NONLOCAL_FLAGS = /^(?:-c|--config-env\b.*|--exec-path\b.*|--output\b.*|--ext-diff|--upload-pack\b.*|--receive-pack\b.*)$/i;
|
|
949
|
+
function localOnlyUse(name, args) {
|
|
950
|
+
// A UNC path or URL operand reaches another host (`mkdir \\host\share\$v` carries $v in the name).
|
|
951
|
+
if (args.some(arg => /^["'(]*(?:\\\\|\/\/)|:\/\//.test(arg)))
|
|
952
|
+
return false;
|
|
953
|
+
if (SHELL_LOCAL_FILE_OPS.has(name))
|
|
954
|
+
return true;
|
|
955
|
+
if (name !== 'git' || args.some(arg => GIT_NONLOCAL_FLAGS.test(arg)))
|
|
956
|
+
return false;
|
|
957
|
+
let k = 0;
|
|
958
|
+
while (k < args.length && args[k].startsWith('-'))
|
|
959
|
+
k += args[k] === '-C' ? 2 : 1;
|
|
960
|
+
return GIT_LOCAL_SUBCOMMANDS.has((args[k] || '').toLowerCase());
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* `out`: program name -> true when EVERY use of it on the line is local-only
|
|
964
|
+
* (`localOnlyUse`). `git status; git push` is `git -> false`.
|
|
965
|
+
*/
|
|
947
966
|
function collectPrograms(command, out, depth) {
|
|
948
967
|
if (depth > 6 || !command.trim())
|
|
949
968
|
return;
|
|
@@ -973,7 +992,18 @@ function collectPrograms(command, out, depth) {
|
|
|
973
992
|
i += 2;
|
|
974
993
|
continue;
|
|
975
994
|
}
|
|
976
|
-
|
|
995
|
+
// PowerShell `$v=((gcloud …` glued in one token: the right-hand side can be a
|
|
996
|
+
// command, so keep reading the same token after the `=`.
|
|
997
|
+
const glued = w.match(/^\$[\w:.]+[+-]?=/);
|
|
998
|
+
if (glued) {
|
|
999
|
+
const rhs = w.slice(glued[0].length);
|
|
1000
|
+
if (rhs)
|
|
1001
|
+
words[i] = rhs;
|
|
1002
|
+
else
|
|
1003
|
+
i++;
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
if (/^[A-Za-z_][\w]*=/.test(w)) {
|
|
977
1007
|
i++;
|
|
978
1008
|
continue;
|
|
979
1009
|
}
|
|
@@ -998,7 +1028,7 @@ function collectPrograms(command, out, depth) {
|
|
|
998
1028
|
const name = programToken(lead);
|
|
999
1029
|
if (!name || SHELL_PIPELINE_NOISE.has(name) || SHELL_INCIDENTAL.has(name))
|
|
1000
1030
|
continue;
|
|
1001
|
-
if (piped && SHELL_TEXT_FILTERS.has(name))
|
|
1031
|
+
if (piped && (SHELL_TEXT_FILTERS.has(name) || SHELL_PIPELINE_ALIASES.has(name)))
|
|
1002
1032
|
continue;
|
|
1003
1033
|
if (GENERIC_SHELLS.has(name)) {
|
|
1004
1034
|
const nested = nestedShellBody(raw);
|
|
@@ -1006,7 +1036,8 @@ function collectPrograms(command, out, depth) {
|
|
|
1006
1036
|
collectPrograms(nested, out, depth + 1);
|
|
1007
1037
|
continue;
|
|
1008
1038
|
}
|
|
1009
|
-
|
|
1039
|
+
const localOnly = localOnlyUse(name, words.slice(i + 1));
|
|
1040
|
+
out.set(name, (out.get(name) ?? true) && localOnly);
|
|
1010
1041
|
}
|
|
1011
1042
|
}
|
|
1012
1043
|
/**
|
|
@@ -1021,9 +1052,24 @@ function collectPrograms(command, out, depth) {
|
|
|
1021
1052
|
* (`out-string).trim()`) is stripped. Sorted and unique.
|
|
1022
1053
|
*/
|
|
1023
1054
|
function shellPrograms(command) {
|
|
1024
|
-
const out = new
|
|
1055
|
+
const out = new Map();
|
|
1025
1056
|
collectPrograms(command, out, 0);
|
|
1026
|
-
return [...out].sort();
|
|
1057
|
+
return [...out.keys()].sort();
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* `shellPrograms` narrowed to what a rule that declares its programs is about:
|
|
1061
|
+
* the rule's programs plus every other program that could move data off the
|
|
1062
|
+
* machine or run code (`curl`, `node`, `Invoke-RestMethod`, anything unknown).
|
|
1063
|
+
* Only local file-structure changes and local `git` reads/commits drop out.
|
|
1064
|
+
* Without declared programs, or when none of them is on the line, the full set.
|
|
1065
|
+
*/
|
|
1066
|
+
function shellProgramsForRule(command, ruleCommands) {
|
|
1067
|
+
const out = new Map();
|
|
1068
|
+
collectPrograms(command, out, 0);
|
|
1069
|
+
const all = [...out.keys()].sort();
|
|
1070
|
+
if (!ruleCommands || !ruleCommands.length || !all.some(p => ruleCommands.includes(p)))
|
|
1071
|
+
return all;
|
|
1072
|
+
return all.filter(p => ruleCommands.includes(p) || !out.get(p));
|
|
1027
1073
|
}
|
|
1028
1074
|
/** Local Safety item ids joined as `a+b` when two ask-rules fire in one dialog. */
|
|
1029
1075
|
function splitLocalSafetyRuleIds(matchedRule) {
|
|
@@ -1057,11 +1103,9 @@ function grantRuleApplies(grant, rule) {
|
|
|
1057
1103
|
return true;
|
|
1058
1104
|
if (!namesOverlap)
|
|
1059
1105
|
return false;
|
|
1060
|
-
// Per-item grant (new) vs a combined live finding (joined hash).
|
|
1061
|
-
if (grantItems.length === 1 && liveItems.length > 1) {
|
|
1062
|
-
return grant.policyHash === localSafetyRuleHash({ itemId: grantItems[0], source: 'builtin', action: 'ask' });
|
|
1063
|
-
}
|
|
1064
1106
|
// Combined leftover grant (joined id + joined hash) vs a later single-rule ask.
|
|
1107
|
+
// A per-item grant never covers a combined live finding on its own: the
|
|
1108
|
+
// combined ask is split per item in `findDeveloperGrant`, each item needs its own.
|
|
1065
1109
|
if (grantItems.length > 1 && liveItems.length === 1)
|
|
1066
1110
|
return true;
|
|
1067
1111
|
return false;
|
|
@@ -1082,8 +1126,9 @@ function grantKey(scope, policyId, matchedRule, policyHash) {
|
|
|
1082
1126
|
return sha256(JSON.stringify([scope.event, scope.toolName, scope.programs || null, scope.scripts || null, scope.operation, scope.destinationDomain || null, policyId || null, matchedRule || null, policyHash]));
|
|
1083
1127
|
}
|
|
1084
1128
|
/**
|
|
1085
|
-
* Does a live grant cover this action under this rule?
|
|
1086
|
-
*
|
|
1129
|
+
* Does a live grant cover this action under this rule? A combined Local Safety
|
|
1130
|
+
* ask (`a+b`) needs a grant for `a` AND one for `b` (or one for `a+b`); a grant
|
|
1131
|
+
* for `a` alone never silences `b`. Shell programs match by INCLUSION:
|
|
1087
1132
|
* a grant for `{gh, git}` covers a line that only runs `gh`, never a line that
|
|
1088
1133
|
* also runs `curl`. Generic shells and pipeline cmdlets are not in the set.
|
|
1089
1134
|
* Touch counts a use.
|
|
@@ -1101,7 +1146,7 @@ function findDeveloperGrant(scope, rule, limits = { ttlMs: exports.GRANT_TTL_MS
|
|
|
1101
1146
|
const ttlCap = Math.min(exports.GRANT_TTL_MS, limits.ttlMs);
|
|
1102
1147
|
return mutateLedger(undefined, () => {
|
|
1103
1148
|
const ledger = readLedger();
|
|
1104
|
-
const
|
|
1149
|
+
const covers = (g, r) => grantRuleApplies(g, r)
|
|
1105
1150
|
&& g.scope.event === scope.event
|
|
1106
1151
|
&& g.scope.toolName === scope.toolName
|
|
1107
1152
|
&& g.scope.operation === scope.operation
|
|
@@ -1115,13 +1160,23 @@ function findDeveloperGrant(scope, rule, limits = { ttlMs: exports.GRANT_TTL_MS
|
|
|
1115
1160
|
&& (scope.event !== 'shell'
|
|
1116
1161
|
|| (Array.isArray(g.scope.programs) && (scope.programs || []).length > 0 && (scope.programs || []).every(p => g.scope.programs.includes(p))))
|
|
1117
1162
|
&& (!(g.scope.scripts && g.scope.scripts.length)
|
|
1118
|
-
|| ((scope.scripts || []).length > 0 && (scope.scripts || []).every(s => g.scope.scripts.includes(s))))
|
|
1119
|
-
|
|
1163
|
+
|| ((scope.scripts || []).length > 0 && (scope.scripts || []).every(s => g.scope.scripts.includes(s))));
|
|
1164
|
+
// A combined ask (`a+b`) is covered by a grant for exactly that combination,
|
|
1165
|
+
// or by one grant per item — never by a grant for `a` alone.
|
|
1166
|
+
const whole = ledger.grants.find(g => covers(g, rule));
|
|
1167
|
+
const hits = whole
|
|
1168
|
+
? [whole]
|
|
1169
|
+
: localSafetyItemIds(rule.matchedRule).length > 1
|
|
1170
|
+
? expandGrantRuleIdentities({ ...rule, policyHash: rule.policyHash }).map(item => ledger.grants.find(g => covers(g, item)))
|
|
1171
|
+
: [];
|
|
1172
|
+
if (!hits.length || hits.some(hit => !hit))
|
|
1120
1173
|
return undefined;
|
|
1121
|
-
hit
|
|
1122
|
-
|
|
1174
|
+
for (const hit of hits) {
|
|
1175
|
+
hit.lastUsedAt = now.toISOString();
|
|
1176
|
+
hit.uses = (Number(hit.uses) || 0) + 1;
|
|
1177
|
+
}
|
|
1123
1178
|
writeLedger(ledger);
|
|
1124
|
-
return
|
|
1179
|
+
return hits[0];
|
|
1125
1180
|
});
|
|
1126
1181
|
}
|
|
1127
1182
|
/**
|
|
@@ -1175,8 +1230,17 @@ function revokeDeveloperGrants(key) {
|
|
|
1175
1230
|
return before - ledger.grants.length;
|
|
1176
1231
|
});
|
|
1177
1232
|
}
|
|
1178
|
-
/**
|
|
1179
|
-
|
|
1233
|
+
/**
|
|
1234
|
+
* One line a person can read on the button and in the grant list: what "Always allow" covers.
|
|
1235
|
+
* With `rule`, names the rule the grant is under (`… under rule gcloud_secret_access_cmd`).
|
|
1236
|
+
*/
|
|
1237
|
+
function describeGrantScope(scope, rule) {
|
|
1238
|
+
const base = describeGrantTarget(scope);
|
|
1239
|
+
const localItem = rule?.policyId?.startsWith('local-safety:') ? rule.policyId.slice('local-safety:'.length) : undefined;
|
|
1240
|
+
const ruleName = localItem || (rule?.policyName ? `"${rule.policyName}"` : undefined);
|
|
1241
|
+
return ruleName ? `${base} under rule ${ruleName}` : base;
|
|
1242
|
+
}
|
|
1243
|
+
function describeGrantTarget(scope) {
|
|
1180
1244
|
const what = scope.event === 'shell'
|
|
1181
1245
|
? (scope.scripts && scope.scripts.length
|
|
1182
1246
|
? `running ${scope.scripts.map(s => `\`${s}\``).join(', ')}`
|
|
@@ -1209,7 +1273,11 @@ const DESTINATION_BOUND_CATEGORIES = new Set(['metadata_ssrf', 'secret_exfiltrat
|
|
|
1209
1273
|
*/
|
|
1210
1274
|
function grantRuleFactsFor(source) {
|
|
1211
1275
|
if (source.localFinding) {
|
|
1212
|
-
|
|
1276
|
+
const facts = { bindsDestination: DESTINATION_BOUND_CATEGORIES.has(String(source.localFinding.category || '')) };
|
|
1277
|
+
const ruleCommands = localSafetyRuleCommands(source.localFinding);
|
|
1278
|
+
if (ruleCommands)
|
|
1279
|
+
facts.ruleCommands = ruleCommands;
|
|
1280
|
+
return facts;
|
|
1213
1281
|
}
|
|
1214
1282
|
const policy = source.policyId ? source.policies?.find(p => p.id === source.policyId) : undefined;
|
|
1215
1283
|
if (!policy)
|
|
@@ -1217,13 +1285,31 @@ function grantRuleFactsFor(source) {
|
|
|
1217
1285
|
const bindsDestination = (policy.rules || []).some(rule => (rule.constraints || []).some(c => typeof c.field === 'string' && c.field.startsWith('destination.')));
|
|
1218
1286
|
return { bindsDestination };
|
|
1219
1287
|
}
|
|
1288
|
+
/**
|
|
1289
|
+
* The programs a built-in Local Safety item declares (`commands`). A combined
|
|
1290
|
+
* finding (`a+b`) is the union, and only when every item declares programs;
|
|
1291
|
+
* custom rules and items without `commands` return undefined.
|
|
1292
|
+
*/
|
|
1293
|
+
function localSafetyRuleCommands(finding) {
|
|
1294
|
+
if (!finding.itemId || finding.source === 'custom')
|
|
1295
|
+
return undefined;
|
|
1296
|
+
const commands = new Set();
|
|
1297
|
+
for (const itemId of splitLocalSafetyRuleIds(finding.itemId) || [finding.itemId]) {
|
|
1298
|
+
const rule = deterministicGuard_1.CREDENTIAL_COMMAND_RULES.find(r => r.itemId === itemId);
|
|
1299
|
+
if (!rule || !rule.commands.length)
|
|
1300
|
+
return undefined;
|
|
1301
|
+
for (const command of rule.commands)
|
|
1302
|
+
commands.add(command.toLowerCase());
|
|
1303
|
+
}
|
|
1304
|
+
return [...commands].sort();
|
|
1305
|
+
}
|
|
1220
1306
|
/** Grants made without rule facts (MCP gateway, older callers) keep the narrow, host-bound shape. */
|
|
1221
1307
|
const DEFAULT_GRANT_RULE_FACTS = { bindsDestination: true };
|
|
1222
1308
|
function grantScopeFor(event, toolName, toolArgs, operation, context, facts = DEFAULT_GRANT_RULE_FACTS) {
|
|
1223
1309
|
const scope = { event, toolName, operation: String(operation || '').trim() || 'unknown' };
|
|
1224
1310
|
if (event === 'shell') {
|
|
1225
1311
|
const command = typeof toolArgs?.command === 'string' ? toolArgs.command : typeof toolArgs?.cmd === 'string' ? toolArgs.cmd : '';
|
|
1226
|
-
scope.programs =
|
|
1312
|
+
scope.programs = shellProgramsForRule(command, facts.ruleCommands);
|
|
1227
1313
|
const cwd = typeof toolArgs?.cwd === 'string' ? toolArgs.cwd : undefined;
|
|
1228
1314
|
const scripts = scriptGrantKeys(command, cwd);
|
|
1229
1315
|
if (scripts.length)
|
package/dist/hookSlim.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* hour. This entry exists to make the common path (daemon up, IPC verdict)
|
|
11
11
|
* load almost nothing:
|
|
12
12
|
*
|
|
13
|
-
* node boot -> hookSlim.js -> hookIo.js + verdictIpcClient.js (
|
|
13
|
+
* node boot -> hookSlim.js -> hookIo.js + verdictIpcClient.js + callerEnv.js (4 small files,
|
|
14
14
|
* Node built-ins only) -> named-pipe verdict -> print -> exit.
|
|
15
15
|
*
|
|
16
16
|
* FALLBACK PARITY: when the daemon is unreachable, this entry lazily requires
|
package/dist/hookSlim.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* hour. This entry exists to make the common path (daemon up, IPC verdict)
|
|
12
12
|
* load almost nothing:
|
|
13
13
|
*
|
|
14
|
-
* node boot -> hookSlim.js -> hookIo.js + verdictIpcClient.js (
|
|
14
|
+
* node boot -> hookSlim.js -> hookIo.js + verdictIpcClient.js + callerEnv.js (4 small files,
|
|
15
15
|
* Node built-ins only) -> named-pipe verdict -> print -> exit.
|
|
16
16
|
*
|
|
17
17
|
* FALLBACK PARITY: when the daemon is unreachable, this entry lazily requires
|
|
@@ -82,6 +82,25 @@ async function main() {
|
|
|
82
82
|
const startedAt = Date.now();
|
|
83
83
|
const flags = (0, hookIo_1.parseHookFlags)(process.argv.slice(2));
|
|
84
84
|
const args = (0, hookIo_1.mapHookFlags)(flags);
|
|
85
|
+
// An error that escapes every await (async child-process 'error', stray
|
|
86
|
+
// rejection) would otherwise exit 1 with no verdict — an allow to the IDE,
|
|
87
|
+
// even mid-approval. Once the full hook is loaded it owns the verdict.
|
|
88
|
+
let hook;
|
|
89
|
+
const onCrash = (err) => {
|
|
90
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
91
|
+
if (!hook) {
|
|
92
|
+
emitFailOpen(args.event, message);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const outcome = hook.crashVerdictForProcess(args.event, message);
|
|
96
|
+
try {
|
|
97
|
+
process.stdout.write(outcome.stdout);
|
|
98
|
+
}
|
|
99
|
+
catch { /* stdout gone */ }
|
|
100
|
+
process.exit(outcome.exitCode);
|
|
101
|
+
};
|
|
102
|
+
process.on('uncaughtException', onCrash);
|
|
103
|
+
process.on('unhandledRejection', onCrash);
|
|
85
104
|
try {
|
|
86
105
|
const raw = await (0, hookIo_1.readStdin)();
|
|
87
106
|
// FAST PATH: the resident daemon computes the verdict with everything warm.
|
|
@@ -121,7 +140,8 @@ async function main() {
|
|
|
121
140
|
// kill-switch handling), so daemon-down behavior is identical to the
|
|
122
141
|
// pre-slim hook. evaluateHookRequest never rejects by contract.
|
|
123
142
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
124
|
-
|
|
143
|
+
hook = require('./commands/hook');
|
|
144
|
+
const { evaluateHookRequest } = hook;
|
|
125
145
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
126
146
|
const { loadConfig } = require('./config');
|
|
127
147
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
package/dist/nativeHook.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** Native hook adapters. Never emit an unsupported `ask` to an IDE. */
|
|
2
1
|
import { HookArgs } from './commands/hook';
|
|
3
2
|
import { BotGuardConfig } from './config';
|
|
4
3
|
import { confirmNativeAction } from './devConfirm';
|
|
@@ -6,6 +5,8 @@ export type NativeClient = 'codex' | 'gemini_cli' | 'windsurf' | 'kiro';
|
|
|
6
5
|
type Payload = Record<string, any>;
|
|
7
6
|
export declare const NATIVE_CLIENTS: NativeClient[];
|
|
8
7
|
export declare function patchToolCalls(raw: Payload): Payload[];
|
|
8
|
+
/** Gemini `read_many_files` reads every `include` path or glob: one read per target, like a Codex patch. */
|
|
9
|
+
export declare function geminiReadManyCalls(raw: Payload): Payload[] | null;
|
|
9
10
|
export declare function normalizeNativeInput(client: NativeClient, raw: Payload, codexServers?: string[]): Payload;
|
|
10
11
|
export declare function nativeVerdict(client: NativeClient, denied: boolean, reason?: string): {
|
|
11
12
|
stdout: string;
|
package/dist/nativeHook.js
CHANGED
|
@@ -1,12 +1,48 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.confirmNativeAction = exports.NATIVE_CLIENTS = void 0;
|
|
4
37
|
exports.patchToolCalls = patchToolCalls;
|
|
38
|
+
exports.geminiReadManyCalls = geminiReadManyCalls;
|
|
5
39
|
exports.normalizeNativeInput = normalizeNativeInput;
|
|
6
40
|
exports.nativeVerdict = nativeVerdict;
|
|
7
41
|
exports.nativeErrorVerdict = nativeErrorVerdict;
|
|
8
42
|
exports.evaluateNativeHook = evaluateNativeHook;
|
|
9
43
|
/** Native hook adapters. Never emit an unsupported `ask` to an IDE. */
|
|
44
|
+
const os = __importStar(require("os"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
10
46
|
const hook_1 = require("./commands/hook");
|
|
11
47
|
const config_1 = require("./config");
|
|
12
48
|
const runtimeConfig_1 = require("./runtimeConfig");
|
|
@@ -16,6 +52,7 @@ Object.defineProperty(exports, "confirmNativeAction", { enumerable: true, get: f
|
|
|
16
52
|
const askDialog_1 = require("./askDialog");
|
|
17
53
|
const telemetry_1 = require("./telemetry");
|
|
18
54
|
const codexMcpIdentity_1 = require("./codexMcpIdentity");
|
|
55
|
+
const actionPolicyEngine_1 = require("./actionPolicyEngine");
|
|
19
56
|
/** Inside the 110 s process deadline below, with room for the verdict itself. */
|
|
20
57
|
const NATIVE_DIALOG_TIMEOUT_MS = 90_000;
|
|
21
58
|
exports.NATIVE_CLIENTS = ['codex', 'gemini_cli', 'windsurf', 'kiro'];
|
|
@@ -42,6 +79,41 @@ function patchToolCalls(raw) {
|
|
|
42
79
|
throw new Error('Patch contains no recognized file operations');
|
|
43
80
|
return calls;
|
|
44
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Gemini CLI built-ins that never run a command, touch a workspace file, reach a
|
|
84
|
+
* server or the network (packages/core/src/tools/tool-names.ts, v0.61). Without
|
|
85
|
+
* this list the unknown-tool fallback below judged them as MCP calls.
|
|
86
|
+
*/
|
|
87
|
+
const GEMINI_PASSIVE_TOOLS = new Set([
|
|
88
|
+
'list_directory', 'glob', 'grep_search', 'search_file_content', 'update_topic', 'write_todos', 'complete_task',
|
|
89
|
+
'ask_user', 'enter_plan_mode', 'exit_plan_mode', 'activate_skill', 'get_internal_docs',
|
|
90
|
+
'tracker_create_task', 'tracker_update_task', 'tracker_get_task', 'tracker_list_tasks', 'tracker_add_dependency', 'tracker_visualize',
|
|
91
|
+
]);
|
|
92
|
+
function isAbsolutePathLike(value) {
|
|
93
|
+
return /^(?:[\\/]|[A-Za-z]:[\\/]|~)/.test(value);
|
|
94
|
+
}
|
|
95
|
+
/** Gemini `read_many_files` reads every `include` path or glob: one read per target, like a Codex patch. */
|
|
96
|
+
function geminiReadManyCalls(raw) {
|
|
97
|
+
if (raw.tool_name !== 'read_many_files' || raw.mcp_context)
|
|
98
|
+
return null;
|
|
99
|
+
const include = Array.isArray(raw.tool_input?.include) ? raw.tool_input.include.map((p) => String(p || '').trim()).filter(Boolean) : [];
|
|
100
|
+
if (!include.length)
|
|
101
|
+
return null;
|
|
102
|
+
return include.map((file_path) => ({ ...raw, tool_name: 'read_file', tool_input: { file_path } }));
|
|
103
|
+
}
|
|
104
|
+
function geminiBuiltinInput(tool, raw, input) {
|
|
105
|
+
if (tool === 'run_shell_command' && typeof input.dir_path === 'string' && input.dir_path && !input.cwd) {
|
|
106
|
+
const base = String(raw.cwd || '');
|
|
107
|
+
const dir = input.dir_path;
|
|
108
|
+
const cwd = isAbsolutePathLike(dir) || !base ? dir : `${base.replace(/[\\/]+$/, '')}${base.includes('/') ? '/' : '\\'}${dir}`;
|
|
109
|
+
return { tool, input: { ...input, cwd } };
|
|
110
|
+
}
|
|
111
|
+
if (tool === 'save_memory') {
|
|
112
|
+
// Pre-0.61 memory tool: appends `fact` to the global GEMINI.md context file.
|
|
113
|
+
return { tool: 'write_file', input: { file_path: path.join(os.homedir(), '.gemini', 'GEMINI.md'), content: String(input.fact || '') } };
|
|
114
|
+
}
|
|
115
|
+
return { tool, input };
|
|
116
|
+
}
|
|
45
117
|
function normalizeNativeInput(client, raw, codexServers = []) {
|
|
46
118
|
let name = String(raw.hook_event_name || '');
|
|
47
119
|
let tool = String(raw.tool_name || '');
|
|
@@ -84,6 +156,8 @@ function normalizeNativeInput(client, raw, codexServers = []) {
|
|
|
84
156
|
tool = `mcp__${raw.mcp_context.server_name}__${raw.mcp_context.tool_name}`;
|
|
85
157
|
else if (tool.startsWith('mcp_'))
|
|
86
158
|
tool = `mcp__gemini__${tool}`; // retain full name when the server split is ambiguous
|
|
159
|
+
else
|
|
160
|
+
({ tool, input } = geminiBuiltinInput(tool, raw, input));
|
|
87
161
|
}
|
|
88
162
|
else if (client === 'kiro') {
|
|
89
163
|
name = { preToolUse: 'PreToolUse', postToolUse: 'PostToolUse', userPromptSubmit: 'UserPromptSubmit' }[name] || name;
|
|
@@ -96,7 +170,8 @@ function normalizeNativeInput(client, raw, codexServers = []) {
|
|
|
96
170
|
throw new Error('Missing tool name');
|
|
97
171
|
// Unknown function tools still need Action Policy evaluation, not silent ignore.
|
|
98
172
|
let normalized = { ...raw, hook_event_name: name, tool_name: tool, tool_input: input };
|
|
99
|
-
|
|
173
|
+
const passiveBuiltin = client === 'gemini_cli' && GEMINI_PASSIVE_TOOLS.has(tool);
|
|
174
|
+
if (name !== 'UserPromptSubmit' && !passiveBuiltin && (0, hook_1.normalizeClaudePayload)(normalized)?.event === 'ignore') {
|
|
100
175
|
normalized = { ...normalized, tool_name: `mcp__${client}-builtin__${tool}` };
|
|
101
176
|
}
|
|
102
177
|
return normalized;
|
|
@@ -135,6 +210,15 @@ async function evaluateNativeHook(client, raw, args, config, confirm) {
|
|
|
135
210
|
return nativeErrorVerdict(client, `Patch validation failed: ${String(error)}`);
|
|
136
211
|
}
|
|
137
212
|
}
|
|
213
|
+
const readManyCalls = client === 'gemini_cli' ? geminiReadManyCalls(raw) : null;
|
|
214
|
+
if (readManyCalls) {
|
|
215
|
+
for (const call of readManyCalls) {
|
|
216
|
+
const result = await evaluateNativeHook(client, call, args, config, confirm);
|
|
217
|
+
if (result.exitCode !== 0 || JSON.parse(result.stdout || '{}').decision === 'deny')
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
return nativeVerdict(client, false);
|
|
221
|
+
}
|
|
138
222
|
let payload;
|
|
139
223
|
const resolveCodexIdentity = client === 'codex' && String(raw.tool_name || '').startsWith('mcp__');
|
|
140
224
|
if (resolveCodexIdentity && !(0, codexMcpIdentity_1.loadTomlParser)()) {
|
|
@@ -147,8 +231,10 @@ async function evaluateNativeHook(client, raw, args, config, confirm) {
|
|
|
147
231
|
catch (error) {
|
|
148
232
|
return nativeErrorVerdict(client, `FullCourtDefense could not parse the hook: ${String(error)}`);
|
|
149
233
|
}
|
|
150
|
-
|
|
151
|
-
|
|
234
|
+
// `client` names the vendor output shape; policies, telemetry and the console use the product id.
|
|
235
|
+
const agentClient = (0, actionPolicyEngine_1.canonicalAgentClient)(client);
|
|
236
|
+
return (0, telemetry_1.markVerdictTiming)({ startedAt: Date.now(), path: 'local', agentClient }, async () => {
|
|
237
|
+
const evaluated = await (0, hook_1.evaluateHookRequest)({ ...args, agentClient }, JSON.stringify(payload), config);
|
|
152
238
|
// The three-button FullCourtDefense dialog (askDialog.ts) — the same one Cursor
|
|
153
239
|
// and Claude Code get. A test-injected `confirm` keeps the legacy yes/no shape.
|
|
154
240
|
const result = await (0, askDialog_1.resolveAskWithDialog)(evaluated, {
|
|
@@ -201,9 +287,28 @@ async function main() {
|
|
|
201
287
|
throw new Error('Unsupported native hook client');
|
|
202
288
|
const config = (0, config_1.loadConfig)();
|
|
203
289
|
const shieldId = (0, config_1.resolveCliCredentials)(config, { shieldId: flags['shield-id'] }, { skipDpapi: true }).shieldId;
|
|
290
|
+
// Errors that escape every await (an async child-process 'error') would exit
|
|
291
|
+
// 1 with no verdict, which these clients read as "hook failed, continue".
|
|
292
|
+
// Same stance as the deadline below. After the verdict is out, nothing is left to decide.
|
|
293
|
+
let verdictWritten = false;
|
|
294
|
+
const onCrash = (err) => {
|
|
295
|
+
if (verdictWritten)
|
|
296
|
+
return;
|
|
297
|
+
verdictWritten = true;
|
|
298
|
+
const deny = (0, runtimeConfig_1.isSuspendedByCache)() || (0, runtimeConfig_1.cachedEnforcementState)(shieldId).mode === 'block';
|
|
299
|
+
const result = deny ? nativeErrorVerdict(client, `FullCourtDefense hook failed: ${String(err)}; action not approved.`) : nativeVerdict(client, false);
|
|
300
|
+
if (result.stdout)
|
|
301
|
+
process.stdout.write(result.stdout + '\n');
|
|
302
|
+
if (result.stderr)
|
|
303
|
+
process.stderr.write(result.stderr + '\n');
|
|
304
|
+
process.exit(result.exitCode);
|
|
305
|
+
};
|
|
306
|
+
process.on('uncaughtException', onCrash);
|
|
307
|
+
process.on('unhandledRejection', onCrash);
|
|
204
308
|
// Return before the vendor's 130s timeout: some clients fail open on timeout.
|
|
205
309
|
// Bound the entire hook, including multiple patch files and human dialogs.
|
|
206
310
|
const deadline = setTimeout(() => {
|
|
311
|
+
verdictWritten = true;
|
|
207
312
|
const deny = (0, runtimeConfig_1.isSuspendedByCache)() || (0, runtimeConfig_1.cachedEnforcementState)(shieldId).mode === 'block';
|
|
208
313
|
const result = deny ? nativeErrorVerdict(client, 'FullCourtDefense evaluation deadline exceeded; action not approved.') : nativeVerdict(client, false);
|
|
209
314
|
if (result.stdout)
|
|
@@ -216,6 +321,7 @@ async function main() {
|
|
|
216
321
|
const raw = JSON.parse((await (0, hookIo_1.readStdin)()).replace(/^\uFEFF/, ''));
|
|
217
322
|
const result = await evaluateNativeHook(client, raw, (0, hookIo_1.mapHookFlags)(flags), config);
|
|
218
323
|
clearTimeout(deadline);
|
|
324
|
+
verdictWritten = true;
|
|
219
325
|
if (result.stdout)
|
|
220
326
|
process.stdout.write(result.stdout + '\n');
|
|
221
327
|
if (result.stderr)
|
package/dist/notify.js
CHANGED
|
@@ -22,6 +22,10 @@ function spawnDetached(command, args, opts) {
|
|
|
22
22
|
windowsHide: opts?.showWindow ? false : true,
|
|
23
23
|
env: opts?.env ? { ...process.env, ...opts.env } : process.env,
|
|
24
24
|
});
|
|
25
|
+
// A missing binary (no notify-send on a headless box) surfaces as an
|
|
26
|
+
// ASYNC 'error' event, not a throw. Unhandled, it kills the calling hook
|
|
27
|
+
// mid-verdict — and a hook that dies without output is an allow to the IDE.
|
|
28
|
+
child.on('error', () => { });
|
|
25
29
|
child.unref();
|
|
26
30
|
}
|
|
27
31
|
catch { /* best-effort */ }
|
package/dist/telemetry.d.ts
CHANGED
|
@@ -109,6 +109,8 @@ interface VerdictTiming {
|
|
|
109
109
|
}
|
|
110
110
|
/** Run one hook evaluation with timing attached to everything it spools. */
|
|
111
111
|
export declare function markVerdictTiming<T>(timing: VerdictTiming, fn: () => T): T;
|
|
112
|
+
/** When the developer's wait for the current evaluation began (undefined outside one). */
|
|
113
|
+
export declare function verdictStartedAt(): number | undefined;
|
|
112
114
|
/**
|
|
113
115
|
* Re-label the current evaluation (e.g. it ended in the fail-open handler).
|
|
114
116
|
* Keeps the original start time so latency still covers the full wait.
|
package/dist/telemetry.js
CHANGED
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.EVIDENCE_MAX_CHARS = void 0;
|
|
37
37
|
exports.evidenceFromToolArgs = evidenceFromToolArgs;
|
|
38
38
|
exports.markVerdictTiming = markVerdictTiming;
|
|
39
|
+
exports.verdictStartedAt = verdictStartedAt;
|
|
39
40
|
exports.setVerdictPath = setVerdictPath;
|
|
40
41
|
exports.setVerdictClient = setVerdictClient;
|
|
41
42
|
exports.setVerdictIdentity = setVerdictIdentity;
|
|
@@ -178,6 +179,10 @@ const verdictTiming = new async_hooks_1.AsyncLocalStorage();
|
|
|
178
179
|
function markVerdictTiming(timing, fn) {
|
|
179
180
|
return verdictTiming.run(timing, fn);
|
|
180
181
|
}
|
|
182
|
+
/** When the developer's wait for the current evaluation began (undefined outside one). */
|
|
183
|
+
function verdictStartedAt() {
|
|
184
|
+
return verdictTiming.getStore()?.startedAt;
|
|
185
|
+
}
|
|
181
186
|
/**
|
|
182
187
|
* Re-label the current evaluation (e.g. it ended in the fail-open handler).
|
|
183
188
|
* Keeps the original start time so latency still covers the full wait.
|
|
@@ -486,6 +491,7 @@ function triggerFlush(immediate = false) {
|
|
|
486
491
|
stdio: 'ignore',
|
|
487
492
|
windowsHide: true,
|
|
488
493
|
});
|
|
494
|
+
child.on('error', () => { });
|
|
489
495
|
child.unref();
|
|
490
496
|
}
|
|
491
497
|
catch { /* best-effort */ }
|
package/dist/verdictIpc.js
CHANGED
|
@@ -39,6 +39,7 @@ const crypto = __importStar(require("crypto"));
|
|
|
39
39
|
const fs = __importStar(require("fs"));
|
|
40
40
|
const net = __importStar(require("net"));
|
|
41
41
|
const telemetry_1 = require("./telemetry");
|
|
42
|
+
const callerEnv_1 = require("./callerEnv");
|
|
42
43
|
const verdictIpcClient_1 = require("./verdictIpcClient");
|
|
43
44
|
/**
|
|
44
45
|
* Daemon verdict IPC — SERVER side (runs inside the resident daemon).
|
|
@@ -139,8 +140,10 @@ function startVerdictServer(evaluate, log) {
|
|
|
139
140
|
// evaluation spools, so fleet telemetry reports the wall time the
|
|
140
141
|
// developer waited. Per-request (AsyncLocalStorage), so concurrent
|
|
141
142
|
// hooks in this one daemon never borrow each other's timings.
|
|
142
|
-
|
|
143
|
-
|
|
143
|
+
// Decision facts read from the environment are the CALLER's (#1466):
|
|
144
|
+
// this process is a scheduled task, not the IDE session that asked.
|
|
145
|
+
const result = await (0, callerEnv_1.withCallerEnv)(request.env, () => (0, telemetry_1.markVerdictTiming)({ startedAt: typeof request.t0 === 'number' ? request.t0 : Date.now(), path: 'ipc' }, () => evaluate(request.args || {}, String(request.stdin ?? ''))));
|
|
146
|
+
reply({ v: verdictIpcClient_1.VERDICT_IPC_PROTOCOL_VERSION, ...result, ...(request.env ? { envApplied: true } : {}) });
|
|
144
147
|
}
|
|
145
148
|
catch (err) {
|
|
146
149
|
// Evaluator bug — tell the client to fall back locally (which will
|