fullcourtdefense-cli 1.36.3 → 1.37.0
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 +19 -0
- package/dist/actionPolicyEngine.js +147 -0
- package/dist/askDialog.d.ts +1 -0
- package/dist/askDialog.js +20 -3
- package/dist/callerEnv.d.ts +29 -2
- package/dist/callerEnv.js +48 -2
- package/dist/commandNarrative.js +87 -7
- package/dist/commands/daemon.d.ts +10 -0
- package/dist/commands/daemon.js +348 -170
- package/dist/commands/desktopChatGuard.js +2 -0
- package/dist/commands/deterministicGuard.d.ts +11 -0
- package/dist/commands/deterministicGuard.js +445 -28
- package/dist/commands/discoverPaths.d.ts +12 -9
- package/dist/commands/discoverPaths.js +30 -0
- package/dist/commands/ephemeralStack.js +7 -0
- package/dist/commands/flushSpool.d.ts +2 -0
- package/dist/commands/flushSpool.js +9 -2
- package/dist/commands/hook.d.ts +14 -0
- package/dist/commands/hook.js +302 -101
- package/dist/commands/httpGatewayCommand.d.ts +1 -0
- package/dist/commands/httpGatewayCommand.js +7 -1
- package/dist/commands/installClaudeHook.js +32 -10
- package/dist/commands/installCursorHook.js +20 -9
- package/dist/commands/login.js +8 -1
- package/dist/commands/mcpGateway.d.ts +21 -0
- package/dist/commands/mcpGateway.js +90 -32
- package/dist/commands/refreshPolicyCache.d.ts +28 -0
- package/dist/commands/refreshPolicyCache.js +59 -0
- package/dist/commands/watchdog.d.ts +2 -0
- package/dist/commands/watchdog.js +30 -5
- package/dist/config.d.ts +36 -0
- package/dist/config.js +78 -12
- package/dist/daemonDetectorBoot.d.ts +1 -0
- package/dist/daemonDetectorBoot.js +12 -0
- package/dist/daemonForensics.d.ts +14 -1
- package/dist/daemonForensics.js +32 -0
- package/dist/detectorInProcess.d.ts +18 -0
- package/dist/detectorInProcess.js +234 -0
- package/dist/detectorInThread.d.ts +15 -0
- package/dist/detectorInThread.js +129 -0
- package/dist/distress.d.ts +2 -0
- package/dist/distress.js +2 -0
- package/dist/fileWriteCanon.d.ts +17 -0
- package/dist/fileWriteCanon.js +141 -0
- package/dist/gateReports.d.ts +144 -0
- package/dist/gateReports.js +488 -0
- package/dist/hookIo.d.ts +59 -0
- package/dist/hookIo.js +319 -9
- package/dist/hookSlim.d.ts +15 -8
- package/dist/hookSlim.js +187 -25
- package/dist/housekeeping.d.ts +64 -0
- package/dist/housekeeping.js +384 -0
- package/dist/housekeepingOps.d.ts +150 -0
- package/dist/housekeepingOps.js +218 -0
- package/dist/housekeepingWorker.d.ts +1 -0
- package/dist/housekeepingWorker.js +113 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +19 -0
- package/dist/localDetectionData.d.ts +10 -0
- package/dist/localDetectionData.js +48 -0
- package/dist/localDetectionUpdates.d.ts +2 -2
- package/dist/localDetectionUpdates.js +4 -4
- package/dist/localSafetySnapshot.d.ts +48 -2
- package/dist/localSafetySnapshot.js +241 -25
- package/dist/machineIdentity.d.ts +7 -0
- package/dist/machineIdentity.js +20 -2
- package/dist/monitorFastPath.d.ts +191 -0
- package/dist/monitorFastPath.js +1198 -0
- package/dist/nativeHook.js +9 -2
- package/dist/pendingDrain.d.ts +82 -0
- package/dist/pendingDrain.js +215 -0
- package/dist/pendingDrainer.d.ts +30 -0
- package/dist/pendingDrainer.js +217 -0
- package/dist/perfSnapshot.d.ts +1 -1
- package/dist/perfSnapshot.js +28 -4
- package/dist/policyCacheFiles.d.ts +73 -0
- package/dist/policyCacheFiles.js +320 -0
- package/dist/policyGateHealth.d.ts +8 -0
- package/dist/policyGateHealth.js +17 -0
- package/dist/policyRefresh.d.ts +28 -0
- package/dist/policyRefresh.js +69 -0
- package/dist/protectionMaintenance.d.ts +15 -0
- package/dist/protectionMaintenance.js +43 -1
- package/dist/runtimeConfig.d.ts +77 -20
- package/dist/runtimeConfig.js +278 -57
- package/dist/selfTest.d.ts +6 -1
- package/dist/selfTest.js +11 -4
- package/dist/telemetry.d.ts +136 -4
- package/dist/telemetry.js +493 -96
- package/dist/verdictIpc.d.ts +60 -0
- package/dist/verdictIpc.js +504 -19
- package/dist/verdictIpcClient.d.ts +175 -18
- package/dist/verdictIpcClient.js +277 -59
- package/dist/version.json +1 -1
- package/package.json +2 -2
|
@@ -309,6 +309,25 @@ export declare function inferToolContext(toolName: string, args: Record<string,
|
|
|
309
309
|
operation: string;
|
|
310
310
|
context: Record<string, string>;
|
|
311
311
|
};
|
|
312
|
+
/**
|
|
313
|
+
* What kind of site a destination host is, for the few kinds that change how an
|
|
314
|
+
* upload should be read. A curated list, not a reputation feed: a host is in it
|
|
315
|
+
* only when its PURPOSE is the category (a paste site exists to publish text; a
|
|
316
|
+
* request catcher exists to capture whatever is sent to it). Deliberately absent:
|
|
317
|
+
* general cloud storage, code hosting and gists, which are everyday developer
|
|
318
|
+
* destinations and would turn the category into noise.
|
|
319
|
+
*/
|
|
320
|
+
export type DestinationCategory = 'paste' | 'file_drop' | 'request_capture' | 'tunnel' | 'messaging' | 'news';
|
|
321
|
+
export declare const DESTINATION_CATEGORY_HOSTS: Readonly<Record<DestinationCategory, readonly string[]>>;
|
|
322
|
+
/** Categories whose purpose is to receive data from strangers: a first upload there is high risk. */
|
|
323
|
+
export declare const HIGH_RISK_DESTINATION_CATEGORIES: ReadonlySet<DestinationCategory>;
|
|
324
|
+
/** The listed host itself or any subdomain of it (`www.pastebin.com`, `x.m.pipedream.net`); never a look-alike (`notpastebin.com`). */
|
|
325
|
+
export declare function destinationCategory(host: string | undefined | null): DestinationCategory | undefined;
|
|
326
|
+
/** A host's category and whether that category exists to receive data from anyone — the shape the anomaly scorer takes. */
|
|
327
|
+
export declare function classifyDestinationHost(host: string): {
|
|
328
|
+
category: DestinationCategory;
|
|
329
|
+
dataDrop: boolean;
|
|
330
|
+
} | undefined;
|
|
312
331
|
/** Exported for reproducible detector artifacts; policy decisions are not packaged. */
|
|
313
332
|
export declare function inferBundledToolContext(toolName: string, args: Record<string, any>): {
|
|
314
333
|
operation: string;
|
|
@@ -24,14 +24,18 @@ __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
|
+
DESTINATION_CATEGORY_HOSTS: () => DESTINATION_CATEGORY_HOSTS,
|
|
27
28
|
GCLOUD_DELETE_VERB_RE: () => GCLOUD_DELETE_VERB_RE,
|
|
28
29
|
GCLOUD_READ_VERB_RE: () => GCLOUD_READ_VERB_RE,
|
|
29
30
|
GCLOUD_WRITE_VERB_RE: () => GCLOUD_WRITE_VERB_RE,
|
|
31
|
+
HIGH_RISK_DESTINATION_CATEGORIES: () => HIGH_RISK_DESTINATION_CATEGORIES,
|
|
30
32
|
MACHINE_KINDS: () => MACHINE_KINDS,
|
|
31
33
|
WORKSTATION_ASK_APPROVAL: () => WORKSTATION_ASK_APPROVAL,
|
|
32
34
|
approvalExceptionError: () => approvalExceptionError,
|
|
33
35
|
canonicalAgentClient: () => canonicalAgentClient,
|
|
36
|
+
classifyDestinationHost: () => classifyDestinationHost,
|
|
34
37
|
deriveToolOperations: () => deriveToolOperations,
|
|
38
|
+
destinationCategory: () => destinationCategory,
|
|
35
39
|
detectSecretKind: () => detectSecretKind,
|
|
36
40
|
effectiveApprovalScope: () => effectiveApprovalScope,
|
|
37
41
|
evaluateActionPolicies: () => evaluateActionPolicies,
|
|
@@ -6888,6 +6892,7 @@ var OPERATION_MATCH_SKIP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
6888
6892
|
"destination.host",
|
|
6889
6893
|
"destination.type",
|
|
6890
6894
|
"destination.repository",
|
|
6895
|
+
"destination.category",
|
|
6891
6896
|
"destinationHost",
|
|
6892
6897
|
"host",
|
|
6893
6898
|
"hostname",
|
|
@@ -8767,8 +8772,145 @@ function inferToolContext(toolName, args) {
|
|
|
8767
8772
|
}
|
|
8768
8773
|
applyRemoteControlFact(toolName, args, context);
|
|
8769
8774
|
applyWebReadFacts(toolName, args, context);
|
|
8775
|
+
applyDestinationCategoryFact(context);
|
|
8770
8776
|
return { operation, context };
|
|
8771
8777
|
}
|
|
8778
|
+
var DESTINATION_CATEGORY_HOSTS = {
|
|
8779
|
+
paste: [
|
|
8780
|
+
"pastebin.com",
|
|
8781
|
+
"paste.ee",
|
|
8782
|
+
"hastebin.com",
|
|
8783
|
+
"rentry.co",
|
|
8784
|
+
"rentry.org",
|
|
8785
|
+
"dpaste.org",
|
|
8786
|
+
"dpaste.com",
|
|
8787
|
+
"paste.rs",
|
|
8788
|
+
"termbin.com",
|
|
8789
|
+
"controlc.com",
|
|
8790
|
+
"justpaste.it",
|
|
8791
|
+
"privatebin.net",
|
|
8792
|
+
"ix.io",
|
|
8793
|
+
"sprunge.us",
|
|
8794
|
+
"pastes.io",
|
|
8795
|
+
"paste.mozilla.org"
|
|
8796
|
+
],
|
|
8797
|
+
file_drop: [
|
|
8798
|
+
"transfer.sh",
|
|
8799
|
+
"file.io",
|
|
8800
|
+
"0x0.st",
|
|
8801
|
+
"temp.sh",
|
|
8802
|
+
"bashupload.com",
|
|
8803
|
+
"gofile.io",
|
|
8804
|
+
"catbox.moe",
|
|
8805
|
+
"uguu.se",
|
|
8806
|
+
"tmpfiles.org",
|
|
8807
|
+
"filebin.net",
|
|
8808
|
+
"wetransfer.com",
|
|
8809
|
+
"we.tl",
|
|
8810
|
+
"sendspace.com",
|
|
8811
|
+
"mediafire.com",
|
|
8812
|
+
"mega.nz",
|
|
8813
|
+
"anonfiles.com",
|
|
8814
|
+
"oshi.at",
|
|
8815
|
+
"pixeldrain.com",
|
|
8816
|
+
"ufile.io",
|
|
8817
|
+
"keep.sh",
|
|
8818
|
+
"filetransfer.io"
|
|
8819
|
+
],
|
|
8820
|
+
request_capture: [
|
|
8821
|
+
"webhook.site",
|
|
8822
|
+
"pipedream.net",
|
|
8823
|
+
"beeceptor.com",
|
|
8824
|
+
"hookbin.com",
|
|
8825
|
+
"requestcatcher.com",
|
|
8826
|
+
"interact.sh",
|
|
8827
|
+
"oast.fun",
|
|
8828
|
+
"oast.pro",
|
|
8829
|
+
"oast.live",
|
|
8830
|
+
"oast.site",
|
|
8831
|
+
"oast.online",
|
|
8832
|
+
"oast.me",
|
|
8833
|
+
"oastify.com",
|
|
8834
|
+
"burpcollaborator.net",
|
|
8835
|
+
"dnslog.cn",
|
|
8836
|
+
"ceye.io"
|
|
8837
|
+
],
|
|
8838
|
+
tunnel: [
|
|
8839
|
+
"ngrok.io",
|
|
8840
|
+
"ngrok.app",
|
|
8841
|
+
"ngrok.dev",
|
|
8842
|
+
"ngrok-free.app",
|
|
8843
|
+
"ngrok-free.dev",
|
|
8844
|
+
"trycloudflare.com",
|
|
8845
|
+
"loca.lt",
|
|
8846
|
+
"localtunnel.me",
|
|
8847
|
+
"serveo.net",
|
|
8848
|
+
"localhost.run",
|
|
8849
|
+
"lhr.life",
|
|
8850
|
+
"pinggy.io",
|
|
8851
|
+
"pinggy.link",
|
|
8852
|
+
"bore.pub"
|
|
8853
|
+
],
|
|
8854
|
+
messaging: ["discord.com", "discordapp.com", "api.telegram.org", "hooks.slack.com"],
|
|
8855
|
+
news: [
|
|
8856
|
+
"ynet.co.il",
|
|
8857
|
+
"ynetnews.com",
|
|
8858
|
+
"walla.co.il",
|
|
8859
|
+
"mako.co.il",
|
|
8860
|
+
"n12.co.il",
|
|
8861
|
+
"haaretz.co.il",
|
|
8862
|
+
"haaretz.com",
|
|
8863
|
+
"themarker.com",
|
|
8864
|
+
"maariv.co.il",
|
|
8865
|
+
"globes.co.il",
|
|
8866
|
+
"calcalist.co.il",
|
|
8867
|
+
"israelhayom.co.il",
|
|
8868
|
+
"kan.org.il",
|
|
8869
|
+
"timesofisrael.com",
|
|
8870
|
+
"jpost.com",
|
|
8871
|
+
"i24news.tv",
|
|
8872
|
+
"bbc.com",
|
|
8873
|
+
"bbc.co.uk",
|
|
8874
|
+
"cnn.com",
|
|
8875
|
+
"nytimes.com",
|
|
8876
|
+
"reuters.com",
|
|
8877
|
+
"apnews.com",
|
|
8878
|
+
"theguardian.com",
|
|
8879
|
+
"washingtonpost.com",
|
|
8880
|
+
"wsj.com",
|
|
8881
|
+
"bloomberg.com",
|
|
8882
|
+
"ft.com",
|
|
8883
|
+
"foxnews.com",
|
|
8884
|
+
"nbcnews.com",
|
|
8885
|
+
"cbsnews.com",
|
|
8886
|
+
"npr.org",
|
|
8887
|
+
"aljazeera.com"
|
|
8888
|
+
]
|
|
8889
|
+
};
|
|
8890
|
+
var HIGH_RISK_DESTINATION_CATEGORIES = /* @__PURE__ */ new Set(["paste", "file_drop", "request_capture", "tunnel"]);
|
|
8891
|
+
var DESTINATION_CATEGORY_BY_HOST = new Map(
|
|
8892
|
+
Object.entries(DESTINATION_CATEGORY_HOSTS).flatMap(([category, hosts]) => hosts.map((host) => [host, category]))
|
|
8893
|
+
);
|
|
8894
|
+
function destinationCategory(host) {
|
|
8895
|
+
let candidate = String(host || "").trim().toLowerCase().replace(/\.$/, "");
|
|
8896
|
+
while (candidate) {
|
|
8897
|
+
const category = DESTINATION_CATEGORY_BY_HOST.get(candidate);
|
|
8898
|
+
if (category) return category;
|
|
8899
|
+
const dot = candidate.indexOf(".");
|
|
8900
|
+
if (dot < 0) return void 0;
|
|
8901
|
+
candidate = candidate.slice(dot + 1);
|
|
8902
|
+
}
|
|
8903
|
+
return void 0;
|
|
8904
|
+
}
|
|
8905
|
+
function classifyDestinationHost(host) {
|
|
8906
|
+
const category = destinationCategory(host);
|
|
8907
|
+
return category ? { category, dataDrop: HIGH_RISK_DESTINATION_CATEGORIES.has(category) } : void 0;
|
|
8908
|
+
}
|
|
8909
|
+
function applyDestinationCategoryFact(context) {
|
|
8910
|
+
delete context["destination.category"];
|
|
8911
|
+
const category = destinationCategory(context["destination.domain"]);
|
|
8912
|
+
if (category) context["destination.category"] = category;
|
|
8913
|
+
}
|
|
8772
8914
|
function applyRemoteControlFact(toolName, args, context) {
|
|
8773
8915
|
delete context["toolArgs.remoteControl"];
|
|
8774
8916
|
const shellText = remoteControlProvider ? shellCommandTextOf(toolName, args) : void 0;
|
|
@@ -8920,6 +9062,7 @@ function inferBundledToolContext(toolName, args) {
|
|
|
8920
9062
|
}
|
|
8921
9063
|
applyRemoteControlFact(toolName, args, context);
|
|
8922
9064
|
applyWebReadFacts(toolName, args, context);
|
|
9065
|
+
applyDestinationCategoryFact(context);
|
|
8923
9066
|
return { operation, context };
|
|
8924
9067
|
}
|
|
8925
9068
|
var RESERVED_INPUT_FACTS = /* @__PURE__ */ new Set([
|
|
@@ -9021,14 +9164,18 @@ function inventoryToolMatchesActionPolicy(toolName, toolActions, policies) {
|
|
|
9021
9164
|
ACTION_POLICY_GRANT_MAX_USES,
|
|
9022
9165
|
CONTENT_FACT_FIELDS,
|
|
9023
9166
|
DEFAULT_ACTION_POLICY_APPROVAL,
|
|
9167
|
+
DESTINATION_CATEGORY_HOSTS,
|
|
9024
9168
|
GCLOUD_DELETE_VERB_RE,
|
|
9025
9169
|
GCLOUD_READ_VERB_RE,
|
|
9026
9170
|
GCLOUD_WRITE_VERB_RE,
|
|
9171
|
+
HIGH_RISK_DESTINATION_CATEGORIES,
|
|
9027
9172
|
MACHINE_KINDS,
|
|
9028
9173
|
WORKSTATION_ASK_APPROVAL,
|
|
9029
9174
|
approvalExceptionError,
|
|
9030
9175
|
canonicalAgentClient,
|
|
9176
|
+
classifyDestinationHost,
|
|
9031
9177
|
deriveToolOperations,
|
|
9178
|
+
destinationCategory,
|
|
9032
9179
|
detectSecretKind,
|
|
9033
9180
|
effectiveApprovalScope,
|
|
9034
9181
|
evaluateActionPolicies,
|
package/dist/askDialog.d.ts
CHANGED
|
@@ -96,6 +96,7 @@ export declare function resolveAskWithDialog(verdict: HookVerdict, options?: {
|
|
|
96
96
|
clientLabel?: string;
|
|
97
97
|
timeoutMs?: number;
|
|
98
98
|
dialog?: DialogFn;
|
|
99
|
+
deadlineAt?: number;
|
|
99
100
|
}): Promise<HookVerdict & {
|
|
100
101
|
resolution?: AskResolution;
|
|
101
102
|
envelope?: AskEnvelope;
|
package/dist/askDialog.js
CHANGED
|
@@ -36,6 +36,8 @@ const callerEnv_1 = require("./callerEnv");
|
|
|
36
36
|
exports.ASK_ENVELOPE_FIELD = 'fcdAsk';
|
|
37
37
|
/** Default wait for the person. Cursor's hook window is 90 s, Claude's 60 s by default; stay inside both. */
|
|
38
38
|
exports.ASK_DIALOG_TIMEOUT_MS = 55_000;
|
|
39
|
+
/** Less time than this before the IDE's cutoff is not enough to read and answer a dialog. */
|
|
40
|
+
const MIN_DIALOG_TIMEOUT_MS = 5_000;
|
|
39
41
|
/** Which channel asks the developer. `dialog` (default) = our OS dialog, `ide` = the IDE's own card. */
|
|
40
42
|
function askChannel() {
|
|
41
43
|
return ((0, callerEnv_1.callerEnv)().FCD_ASK_CHANNEL || '').trim().toLowerCase() === 'ide' ? 'ide' : 'dialog';
|
|
@@ -97,9 +99,20 @@ function rewriteDecision(decision, allowed, userMsg, agentMsg) {
|
|
|
97
99
|
* written as-is with the envelope removed. Then the process exits.
|
|
98
100
|
*/
|
|
99
101
|
async function emitVerdictForIde(args, result, entry) {
|
|
102
|
+
// This process ends with the verdict below: an upload the dialog's outcome
|
|
103
|
+
// asks for (allow, decline, timeout) starts after stdout, at exit — never
|
|
104
|
+
// while the IDE still waits for the answer.
|
|
105
|
+
try {
|
|
106
|
+
(0, telemetry_1.deferFlushUntilExit)();
|
|
107
|
+
}
|
|
108
|
+
catch { /* best-effort */ }
|
|
100
109
|
let final = result;
|
|
101
110
|
try {
|
|
102
|
-
|
|
111
|
+
// This is the per-event process the IDE started: its clock started with
|
|
112
|
+
// the process, so the dialog must end before the IDE's hook timeout does
|
|
113
|
+
// (a killed hook runs the action without anyone's answer).
|
|
114
|
+
const processStartedAt = Date.now() - Math.round(process.uptime() * 1000);
|
|
115
|
+
const resolved = await resolveAskWithDialog(result, { clientLabel: args.agentClient, deadlineAt: (0, callerEnv_1.ideWaitCutoff)(processStartedAt) });
|
|
103
116
|
if (resolved.resolution)
|
|
104
117
|
(0, hookIo_1.dbg)({ phase: 'ask_dialog', event: args.event, entry, resolution: resolved.resolution, tool: resolved.envelope?.toolName });
|
|
105
118
|
final = resolved;
|
|
@@ -137,7 +150,11 @@ async function resolveAskWithDialog(verdict, options = {}) {
|
|
|
137
150
|
const clean = { ...verdict, stdout: taken.stdout, envelope };
|
|
138
151
|
// An injected dialog (tests, embedding hosts) only needs a person; the OS dialog also needs a display.
|
|
139
152
|
const canAsk = options.dialog ? (0, devConfirm_1.isHumanPresent)() : (0, devConfirm_1.nativeDialogAvailable)();
|
|
140
|
-
|
|
153
|
+
// Wait = min(configured, time left before the IDE's cutoff). Too little time
|
|
154
|
+
// left for a person to read and answer: the IDE's own ask card takes the
|
|
155
|
+
// question instead, so the hook still answers inside the IDE's window.
|
|
156
|
+
const dialogTimeoutMs = Math.min(options.timeoutMs ?? exports.ASK_DIALOG_TIMEOUT_MS, options.deadlineAt === undefined ? Number.POSITIVE_INFINITY : options.deadlineAt - Date.now());
|
|
157
|
+
if (askChannel() === 'ide' || !canAsk || dialogTimeoutMs < MIN_DIALOG_TIMEOUT_MS)
|
|
141
158
|
return { ...clean, resolution: 'ide' };
|
|
142
159
|
const grantHash = envelope.ruleHash || envelope.policyHash;
|
|
143
160
|
const grantLimits = envelope.grantLimits === null ? undefined : (envelope.grantLimits || { ttlMs: devConfirm_1.GRANT_TTL_MS });
|
|
@@ -163,7 +180,7 @@ async function resolveAskWithDialog(verdict, options = {}) {
|
|
|
163
180
|
let outcome = 'unavailable';
|
|
164
181
|
let latencyMs = 0;
|
|
165
182
|
try {
|
|
166
|
-
({ outcome, latencyMs } = await dialog(reason,
|
|
183
|
+
({ outcome, latencyMs } = await dialog(reason, dialogTimeoutMs, { allowAlways, alwaysScopeLabel: scopeLabel, alwaysLimitsLabel: limitsLabel, alwaysCovers, alwaysNotOffered, subject }));
|
|
167
184
|
}
|
|
168
185
|
catch {
|
|
169
186
|
outcome = 'unavailable';
|
package/dist/callerEnv.d.ts
CHANGED
|
@@ -11,10 +11,23 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Node built-ins only: this leaf is on the slim hook's warm-path module graph.
|
|
13
13
|
*/
|
|
14
|
-
export declare const CALLER_ENV_KEYS: readonly ["FCD_HUMAN_PRESENT", "FCD_EPHEMERAL", "FULLCOURTDEFENSE_EPHEMERAL", "CI", "GITHUB_ACTIONS", "GITLAB_CI", "TF_BUILD", "JENKINS_URL", "BUILDKITE", "CIRCLECI", "DISPLAY", "WAYLAND_DISPLAY", "FCD_ASK_CHANNEL", "FCD_SESSION_ID", "CLAUDE_CODE_SESSION_ID", "CODEX_THREAD_ID", "FCD_RUN_ID", "GITHUB_RUN_ID", "CI_PIPELINE_ID", "FCD_WORKLOAD_INSTANCE_ID", "CLAUDECODE", "CLAUDE_PROJECT_DIR", "VSCODE_PID", "TERM_PROGRAM", "WORKSPACE_PATH", "FCD_HOOK_IDE_TIMEOUT_MS", "FCD_TAINT_APPROVE_ONCE_DISABLED", "FCD_TAINT_APPROVAL_TIMEOUT_MS", "CLOUDSDK_CORE_PROJECT", "CLOUDSDK_ACTIVE_CONFIG_NAME", "CLOUDSDK_CONFIG", "APPDATA", "AWS_PROFILE", "AWS_DEFAULT_PROFILE", "AWS_REGION", "AWS_DEFAULT_REGION", "AWS_CONFIG_FILE", "AWS_SHARED_CREDENTIALS_FILE", "AWS_ACCESS_KEY_ID", "KUBECONFIG", "HELM_KUBECONTEXT", "HELM_NAMESPACE", "HELM_KUBEAPISERVER", "HELM_KUBETOKEN", "HELM_KUBEASUSER", "DOCKER_HOST", "DOCKER_CONTEXT", "AZURE_CONFIG_DIR", "AZURE_DEFAULTS_GROUP", "TF_WORKSPACE", "TF_DATA_DIR", "PGHOST", "PGDATABASE", "PGSERVICE", "MYSQL_HOST"];
|
|
14
|
+
export declare const CALLER_ENV_KEYS: readonly ["FCD_HUMAN_PRESENT", "FCD_EPHEMERAL", "FULLCOURTDEFENSE_EPHEMERAL", "CI", "GITHUB_ACTIONS", "GITLAB_CI", "TF_BUILD", "JENKINS_URL", "BUILDKITE", "CIRCLECI", "DISPLAY", "WAYLAND_DISPLAY", "FCD_ASK_CHANNEL", "FCD_SESSION_ID", "CLAUDE_CODE_SESSION_ID", "CODEX_THREAD_ID", "FCD_RUN_ID", "GITHUB_RUN_ID", "CI_PIPELINE_ID", "FCD_WORKLOAD_INSTANCE_ID", "CLAUDECODE", "CLAUDE_PROJECT_DIR", "VSCODE_PID", "TERM_PROGRAM", "WORKSPACE_PATH", "FCD_HOOK_IDE_TIMEOUT_MS", "FCD_TAINT_APPROVE_ONCE_DISABLED", "FCD_TAINT_APPROVAL_TIMEOUT_MS", "FCD_HOOK_DEBUG", "CLOUDSDK_CORE_PROJECT", "CLOUDSDK_ACTIVE_CONFIG_NAME", "CLOUDSDK_CONFIG", "APPDATA", "AWS_PROFILE", "AWS_DEFAULT_PROFILE", "AWS_REGION", "AWS_DEFAULT_REGION", "AWS_CONFIG_FILE", "AWS_SHARED_CREDENTIALS_FILE", "AWS_ACCESS_KEY_ID", "KUBECONFIG", "HELM_KUBECONTEXT", "HELM_NAMESPACE", "HELM_KUBEAPISERVER", "HELM_KUBETOKEN", "HELM_KUBEASUSER", "DOCKER_HOST", "DOCKER_CONTEXT", "AZURE_CONFIG_DIR", "AZURE_DEFAULTS_GROUP", "TF_WORKSPACE", "TF_DATA_DIR", "PGHOST", "PGDATABASE", "PGSERVICE", "MYSQL_HOST"];
|
|
15
15
|
export type CallerEnvSnapshot = Partial<Record<(typeof CALLER_ENV_KEYS)[number], string>>;
|
|
16
|
+
/**
|
|
17
|
+
* Keys a decision reads only for WHETHER they are set (cloudGrantTools: static
|
|
18
|
+
* AWS keys beat every profile; a helm API server / token / impersonation makes
|
|
19
|
+
* the target unknown) — and some are credentials (HELM_KUBETOKEN is a bearer
|
|
20
|
+
* token). A snapshot that is written to disk keeps only that they are set.
|
|
21
|
+
*/
|
|
22
|
+
export declare const PRESENCE_ONLY_CALLER_ENV_KEYS: readonly (typeof CALLER_ENV_KEYS)[number][];
|
|
16
23
|
/** The keys above that are set in `env` — what the thin client sends. */
|
|
17
24
|
export declare function snapshotCallerEnv(env?: NodeJS.ProcessEnv): CallerEnvSnapshot;
|
|
25
|
+
/**
|
|
26
|
+
* snapshotCallerEnv for a snapshot that is STORED (the monitor fast path's
|
|
27
|
+
* local queue): the same decision facts, without credential values — a
|
|
28
|
+
* presence-only key keeps '1' when set (non-blank), '' when blank.
|
|
29
|
+
*/
|
|
30
|
+
export declare function storableCallerEnv(env?: NodeJS.ProcessEnv): CallerEnvSnapshot;
|
|
18
31
|
/** The environment decision facts are read from: the requesting caller's inside a daemon request, else this process's. */
|
|
19
32
|
export declare function callerEnv(): NodeJS.ProcessEnv;
|
|
20
33
|
/**
|
|
@@ -26,11 +39,25 @@ export declare function withCallerEnv<T>(snapshot: unknown, fn: () => T): T;
|
|
|
26
39
|
/**
|
|
27
40
|
* How long the IDE lets an approval-capable hook run before it kills the hook
|
|
28
41
|
* (and runs the action): Claude Code / Copilot / VS Code and Cursor: 90s.
|
|
29
|
-
* FCD_HOOK_IDE_TIMEOUT_MS narrows it
|
|
42
|
+
* FCD_HOOK_IDE_TIMEOUT_MS narrows it — per hook entry (applyIdeTimeoutArg) and
|
|
43
|
+
* for drills.
|
|
30
44
|
*/
|
|
31
45
|
export declare const IDE_HOOK_TIMEOUT_MS = 90000;
|
|
32
46
|
export declare const IDE_TIMEOUT_MARGIN_MS = 15000;
|
|
33
47
|
export declare function ideHookTimeoutMs(env?: NodeJS.ProcessEnv): number;
|
|
48
|
+
/**
|
|
49
|
+
* The installer writes each hook entry's real IDE timeout into its command
|
|
50
|
+
* (`--ide-timeout <seconds>`, the same number as the entry's `timeout`): Claude
|
|
51
|
+
* Code PostToolUse 10s, UserPromptSubmit 30s, PreToolUse 90s; Cursor per event.
|
|
52
|
+
* A hook the IDE kills runs the action, so every wait must end before THAT
|
|
53
|
+
* timeout, not before a fixed 90s. Applied, in the hook's own process only, as
|
|
54
|
+
* its FCD_HOOK_IDE_TIMEOUT_MS — a caller-env key, so the daemon evaluating the
|
|
55
|
+
* call applies it too (older daemons already honour it). It only ever narrows
|
|
56
|
+
* the 90s default: an entry configured longer keeps today's waits. An explicit
|
|
57
|
+
* FCD_HOOK_IDE_TIMEOUT_MS (drills) wins; a command line without the argument
|
|
58
|
+
* (older installs) keeps the default.
|
|
59
|
+
*/
|
|
60
|
+
export declare function applyIdeTimeoutArg(seconds: string | undefined, env?: NodeJS.ProcessEnv): void;
|
|
34
61
|
/** Absolute time every wait inside a hook must end by, leaving the margin before the IDE's timeout. */
|
|
35
62
|
export declare function ideWaitCutoff(startedAt: number, ideTimeoutMs?: number): number;
|
|
36
63
|
/** CI / pipeline environments — no human at a keyboard. Shared by hooks and discovery. */
|
package/dist/callerEnv.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.IDE_TIMEOUT_MARGIN_MS = exports.IDE_HOOK_TIMEOUT_MS = exports.CALLER_ENV_KEYS = void 0;
|
|
3
|
+
exports.IDE_TIMEOUT_MARGIN_MS = exports.IDE_HOOK_TIMEOUT_MS = exports.PRESENCE_ONLY_CALLER_ENV_KEYS = exports.CALLER_ENV_KEYS = void 0;
|
|
4
4
|
exports.snapshotCallerEnv = snapshotCallerEnv;
|
|
5
|
+
exports.storableCallerEnv = storableCallerEnv;
|
|
5
6
|
exports.callerEnv = callerEnv;
|
|
6
7
|
exports.withCallerEnv = withCallerEnv;
|
|
7
8
|
exports.ideHookTimeoutMs = ideHookTimeoutMs;
|
|
9
|
+
exports.applyIdeTimeoutArg = applyIdeTimeoutArg;
|
|
8
10
|
exports.ideWaitCutoff = ideWaitCutoff;
|
|
9
11
|
exports.isCiEnvironment = isCiEnvironment;
|
|
10
12
|
exports.isHumanPresent = isHumanPresent;
|
|
@@ -34,6 +36,8 @@ exports.CALLER_ENV_KEYS = [
|
|
|
34
36
|
'CLAUDECODE', 'CLAUDE_PROJECT_DIR', 'VSCODE_PID', 'TERM_PROGRAM', 'WORKSPACE_PATH',
|
|
35
37
|
// Human-wait budget and approve-once switches.
|
|
36
38
|
'FCD_HOOK_IDE_TIMEOUT_MS', 'FCD_TAINT_APPROVE_ONCE_DISABLED', 'FCD_TAINT_APPROVAL_TIMEOUT_MS',
|
|
39
|
+
// Support diagnostics: per-phase hook.log lines for this caller's verdicts (never a decision fact).
|
|
40
|
+
'FCD_HOOK_DEBUG',
|
|
37
41
|
// Which GCP project a gcloud line targets when it names none (cloudGrant.activeGcloudProject).
|
|
38
42
|
'CLOUDSDK_CORE_PROJECT', 'CLOUDSDK_ACTIVE_CONFIG_NAME', 'CLOUDSDK_CONFIG', 'APPDATA',
|
|
39
43
|
// Which aws profile / region and which kube context an aws / kubectl / helm line targets
|
|
@@ -49,6 +53,15 @@ exports.CALLER_ENV_KEYS = [
|
|
|
49
53
|
// Which database host / name a psql / mysql line falls back to (cloudGrantTools).
|
|
50
54
|
'PGHOST', 'PGDATABASE', 'PGSERVICE', 'MYSQL_HOST',
|
|
51
55
|
];
|
|
56
|
+
/**
|
|
57
|
+
* Keys a decision reads only for WHETHER they are set (cloudGrantTools: static
|
|
58
|
+
* AWS keys beat every profile; a helm API server / token / impersonation makes
|
|
59
|
+
* the target unknown) — and some are credentials (HELM_KUBETOKEN is a bearer
|
|
60
|
+
* token). A snapshot that is written to disk keeps only that they are set.
|
|
61
|
+
*/
|
|
62
|
+
exports.PRESENCE_ONLY_CALLER_ENV_KEYS = [
|
|
63
|
+
'AWS_ACCESS_KEY_ID', 'HELM_KUBEAPISERVER', 'HELM_KUBETOKEN', 'HELM_KUBEASUSER',
|
|
64
|
+
];
|
|
52
65
|
/** The keys above that are set in `env` — what the thin client sends. */
|
|
53
66
|
function snapshotCallerEnv(env = process.env) {
|
|
54
67
|
const snapshot = {};
|
|
@@ -59,6 +72,20 @@ function snapshotCallerEnv(env = process.env) {
|
|
|
59
72
|
}
|
|
60
73
|
return snapshot;
|
|
61
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* snapshotCallerEnv for a snapshot that is STORED (the monitor fast path's
|
|
77
|
+
* local queue): the same decision facts, without credential values — a
|
|
78
|
+
* presence-only key keeps '1' when set (non-blank), '' when blank.
|
|
79
|
+
*/
|
|
80
|
+
function storableCallerEnv(env = process.env) {
|
|
81
|
+
const snapshot = snapshotCallerEnv(env);
|
|
82
|
+
for (const key of exports.PRESENCE_ONLY_CALLER_ENV_KEYS) {
|
|
83
|
+
const value = snapshot[key];
|
|
84
|
+
if (typeof value === 'string')
|
|
85
|
+
snapshot[key] = value.trim() ? '1' : '';
|
|
86
|
+
}
|
|
87
|
+
return snapshot;
|
|
88
|
+
}
|
|
62
89
|
const requestEnv = new async_hooks_1.AsyncLocalStorage();
|
|
63
90
|
/** The environment decision facts are read from: the requesting caller's inside a daemon request, else this process's. */
|
|
64
91
|
function callerEnv() {
|
|
@@ -85,7 +112,8 @@ function withCallerEnv(snapshot, fn) {
|
|
|
85
112
|
/**
|
|
86
113
|
* How long the IDE lets an approval-capable hook run before it kills the hook
|
|
87
114
|
* (and runs the action): Claude Code / Copilot / VS Code and Cursor: 90s.
|
|
88
|
-
* FCD_HOOK_IDE_TIMEOUT_MS narrows it
|
|
115
|
+
* FCD_HOOK_IDE_TIMEOUT_MS narrows it — per hook entry (applyIdeTimeoutArg) and
|
|
116
|
+
* for drills.
|
|
89
117
|
*/
|
|
90
118
|
exports.IDE_HOOK_TIMEOUT_MS = 90_000;
|
|
91
119
|
exports.IDE_TIMEOUT_MARGIN_MS = 15_000;
|
|
@@ -93,6 +121,24 @@ function ideHookTimeoutMs(env = callerEnv()) {
|
|
|
93
121
|
const override = Number(env.FCD_HOOK_IDE_TIMEOUT_MS);
|
|
94
122
|
return override > 0 ? override : exports.IDE_HOOK_TIMEOUT_MS;
|
|
95
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* The installer writes each hook entry's real IDE timeout into its command
|
|
126
|
+
* (`--ide-timeout <seconds>`, the same number as the entry's `timeout`): Claude
|
|
127
|
+
* Code PostToolUse 10s, UserPromptSubmit 30s, PreToolUse 90s; Cursor per event.
|
|
128
|
+
* A hook the IDE kills runs the action, so every wait must end before THAT
|
|
129
|
+
* timeout, not before a fixed 90s. Applied, in the hook's own process only, as
|
|
130
|
+
* its FCD_HOOK_IDE_TIMEOUT_MS — a caller-env key, so the daemon evaluating the
|
|
131
|
+
* call applies it too (older daemons already honour it). It only ever narrows
|
|
132
|
+
* the 90s default: an entry configured longer keeps today's waits. An explicit
|
|
133
|
+
* FCD_HOOK_IDE_TIMEOUT_MS (drills) wins; a command line without the argument
|
|
134
|
+
* (older installs) keeps the default.
|
|
135
|
+
*/
|
|
136
|
+
function applyIdeTimeoutArg(seconds, env = process.env) {
|
|
137
|
+
const ms = Math.round(Number(seconds) * 1000);
|
|
138
|
+
if (!Number.isFinite(ms) || ms <= 0 || Number(env.FCD_HOOK_IDE_TIMEOUT_MS) > 0)
|
|
139
|
+
return;
|
|
140
|
+
env.FCD_HOOK_IDE_TIMEOUT_MS = String(Math.min(ms, exports.IDE_HOOK_TIMEOUT_MS));
|
|
141
|
+
}
|
|
96
142
|
/** Absolute time every wait inside a hook must end by, leaving the margin before the IDE's timeout. */
|
|
97
143
|
function ideWaitCutoff(startedAt, ideTimeoutMs = ideHookTimeoutMs()) {
|
|
98
144
|
return startedAt + ideTimeoutMs - Math.min(exports.IDE_TIMEOUT_MARGIN_MS, ideTimeoutMs / 4);
|
package/dist/commandNarrative.js
CHANGED
|
@@ -21,6 +21,7 @@ exports.tokenDestinations = tokenDestinations;
|
|
|
21
21
|
exports.capturedValueDestinations = capturedValueDestinations;
|
|
22
22
|
exports.describeShellCommand = describeShellCommand;
|
|
23
23
|
exports.describeShellCommandLines = describeShellCommandLines;
|
|
24
|
+
const deterministicGuard_1 = require("./commands/deterministicGuard");
|
|
24
25
|
const MAX_STEPS = 14;
|
|
25
26
|
const MAX_LINE = 220;
|
|
26
27
|
/** Token printers whose output can be assigned; `issuer` = who accepts the token (identity tokens go to their audience). */
|
|
@@ -643,7 +644,7 @@ function describeOne(segmentIn, captured, fns = new Map()) {
|
|
|
643
644
|
case 'python3':
|
|
644
645
|
case 'tsx':
|
|
645
646
|
case 'ts-node':
|
|
646
|
-
return step(`Runs ${program} ${rest[1] ? `(${shortValue(rest.slice(1).join(' '), 60)})` : ''}`, false);
|
|
647
|
+
return step(`Runs ${program} ${rest[1] ? `(${/[\\/]/.test(rest[1]) && !rest[1].startsWith('-') ? [shortPath(rest[1]), ...rest.slice(2)].join(' ').slice(0, 80) : shortValue(rest.slice(1).join(' '), 60)})` : ''}`, false);
|
|
647
648
|
case 'curl':
|
|
648
649
|
case 'wget':
|
|
649
650
|
case 'invoke-webrequest':
|
|
@@ -678,7 +679,10 @@ function describeOne(segmentIn, captured, fns = new Map()) {
|
|
|
678
679
|
case 'mkdir':
|
|
679
680
|
case 'new-item':
|
|
680
681
|
case 'ni':
|
|
681
|
-
case 'touch':
|
|
682
|
+
case 'touch': {
|
|
683
|
+
const made = rest.slice(1).filter(t => !t.startsWith('-'));
|
|
684
|
+
return step(`Creates ${made.length === 1 ? shortPath(made[0]) : shortValue(made.join(', '), 60)}`);
|
|
685
|
+
}
|
|
682
686
|
case 'cat':
|
|
683
687
|
case 'get-content':
|
|
684
688
|
case 'gc':
|
|
@@ -686,8 +690,22 @@ function describeOne(segmentIn, captured, fns = new Map()) {
|
|
|
686
690
|
case 'head':
|
|
687
691
|
case 'tail':
|
|
688
692
|
case 'less':
|
|
689
|
-
case 'more':
|
|
690
|
-
|
|
693
|
+
case 'more': {
|
|
694
|
+
// `cat > f` / `cat 2>/dev/null f`: redirections and their targets are not files read.
|
|
695
|
+
const operands = [];
|
|
696
|
+
for (let k = 1; k < rest.length; k++) {
|
|
697
|
+
if (/^(?:\d*>{1,2}|&>|<)$/.test(rest[k])) {
|
|
698
|
+
k++;
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
if (/^(?:\d*>|&>|<)/.test(rest[k]) || rest[k].startsWith('-'))
|
|
702
|
+
continue;
|
|
703
|
+
operands.push(rest[k]);
|
|
704
|
+
}
|
|
705
|
+
if (!operands.length && redirectTo)
|
|
706
|
+
return step(`Writes its input to ${shortValue(redirectTo, 60)}`);
|
|
707
|
+
return step(`Reads the file ${shortValue(operands[0] || '', 60)}${redirectNote}`);
|
|
708
|
+
}
|
|
691
709
|
case 'ls':
|
|
692
710
|
case 'dir':
|
|
693
711
|
case 'get-childitem':
|
|
@@ -948,13 +966,75 @@ function noteTokenDestinations(steps) {
|
|
|
948
966
|
return { ...step, text: shortValue(`${step.text}, then sends it to ${described.join('; ')}`, MAX_LINE) };
|
|
949
967
|
});
|
|
950
968
|
}
|
|
969
|
+
const HEREDOC_OPENER = /(?:^|[^<])<<-?[ \t]*(?:'[^'\n]*'|"[^"\n]*"|\\?[^\s;&|<>()'"]+)/g;
|
|
970
|
+
/**
|
|
971
|
+
* A heredoc body is the input of the step that opens it, not steps of its own:
|
|
972
|
+
* drop every body (and its delimiter line) and hand each opener step its body.
|
|
973
|
+
*/
|
|
974
|
+
function stepsWithHeredocs(command) {
|
|
975
|
+
const heredocs = (0, deterministicGuard_1.posixHeredocs)(command);
|
|
976
|
+
if (!heredocs.length)
|
|
977
|
+
return splitCommandSteps(command).map(segment => ({ segment, heredocs: [] }));
|
|
978
|
+
let code = '';
|
|
979
|
+
let at = 0;
|
|
980
|
+
for (const heredoc of heredocs) {
|
|
981
|
+
code += command.slice(at, heredoc.bodyStart);
|
|
982
|
+
at = Math.min(command.length, heredoc.end + 1);
|
|
983
|
+
}
|
|
984
|
+
code += command.slice(at);
|
|
985
|
+
let next = 0;
|
|
986
|
+
return splitCommandSteps(code).map(segment => {
|
|
987
|
+
const opened = segment.replace(/"(?:[^"\\]|\\.)*"|'[^']*'/g, m => (/<</.test(m) ? m : '""')).match(HEREDOC_OPENER) || [];
|
|
988
|
+
const mine = heredocs.slice(next, next + opened.length);
|
|
989
|
+
next += opened.length;
|
|
990
|
+
return { segment, heredocs: mine };
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
/** A long path shortened from the front: the file name is what the reader needs. */
|
|
994
|
+
function shortPath(path, max = 60) {
|
|
995
|
+
if (path.length <= max)
|
|
996
|
+
return path;
|
|
997
|
+
const parts = path.split(/[\\/]/);
|
|
998
|
+
let out = parts.pop() || '';
|
|
999
|
+
while (parts.length && out.length + parts[parts.length - 1].length + 3 <= max)
|
|
1000
|
+
out = `${parts.pop()}/${out}`;
|
|
1001
|
+
return `…/${out}`.slice(0, max + 2);
|
|
1002
|
+
}
|
|
1003
|
+
function lineCount(body) {
|
|
1004
|
+
return body ? body.split('\n').length : 0;
|
|
1005
|
+
}
|
|
1006
|
+
/** What a step does with the heredoc bodies it opens. */
|
|
1007
|
+
function describeHeredocStep(segment, heredocs, fns) {
|
|
1008
|
+
const [heredoc] = heredocs;
|
|
1009
|
+
const lines = lineCount(heredoc.body);
|
|
1010
|
+
const size = `${lines}-line`;
|
|
1011
|
+
const masked = segment.replace(/"(?:[^"\\]|\\.)*"|'[^']*'/g, m => m.replace(/[>|]/g, '_'));
|
|
1012
|
+
const target = /(?:^|[^>0-9-])>{1,2}\s*("[^"]*"|'[^']*'|[^\s&|;<]+)/.exec(masked);
|
|
1013
|
+
const file = target ? unquote(segment.slice(target.index + target[0].length - target[1].length, target.index + target[0].length)) : '';
|
|
1014
|
+
const words = tokens(segment).filter(t => !/^<</.test(t));
|
|
1015
|
+
const program = (words[0] || '').toLowerCase().replace(/^.*[\\/]/, '');
|
|
1016
|
+
const { kind, program: reader } = heredoc.receiver;
|
|
1017
|
+
const into = program === 'cat' ? file : program === 'tee' ? (file || words.slice(1).find(t => !t.startsWith('-')) || '') : '';
|
|
1018
|
+
// Writing the body to a file is this step; a later step that runs the file says so itself.
|
|
1019
|
+
if (into && !/\|/.test(masked)) {
|
|
1020
|
+
const what = kind === 'shell' ? `a ${size} shell script` : kind === 'language' ? `a ${size} ${reader} script` : `${lines} line${lines === 1 ? '' : 's'} of text`;
|
|
1021
|
+
return { segment, text: shortValue(`Writes ${what} to ${shortPath(into)}`, MAX_LINE), known: true };
|
|
1022
|
+
}
|
|
1023
|
+
if (kind === 'shell')
|
|
1024
|
+
return { segment, text: shortValue(`Runs a ${size} script with ${reader || program}`, MAX_LINE), known: true };
|
|
1025
|
+
if (kind === 'language')
|
|
1026
|
+
return { segment, text: shortValue(`Runs a ${size} ${reader} script`, MAX_LINE), known: true };
|
|
1027
|
+
const base = describeOne(segment.replace(HEREDOC_OPENER, m => m.replace(/<<[\s\S]*$/, '')), undefined, fns);
|
|
1028
|
+
return { ...base, segment, text: shortValue(`${base.text} (given ${lines} line${lines === 1 ? '' : 's'} of input text)`, MAX_LINE) };
|
|
1029
|
+
}
|
|
951
1030
|
/** One plain-words line per step, in order. */
|
|
952
1031
|
function describeShellCommand(command) {
|
|
953
|
-
const
|
|
1032
|
+
const parts = stepsWithHeredocs(command);
|
|
1033
|
+
const steps = parts.map(part => part.segment);
|
|
954
1034
|
const fns = localFunctions(command);
|
|
955
|
-
const described =
|
|
1035
|
+
const described = parts.map(({ segment, heredocs }) => {
|
|
956
1036
|
try {
|
|
957
|
-
return describeOne(segment, undefined, fns);
|
|
1037
|
+
return heredocs.length ? describeHeredocStep(segment, heredocs, fns) : describeOne(segment, undefined, fns);
|
|
958
1038
|
}
|
|
959
1039
|
catch {
|
|
960
1040
|
return { segment, text: 'Runs a command the grammar does not recognise', known: false };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BotGuardConfig } from '../config';
|
|
2
2
|
import { ProtectAllArgs } from './mcpGateway';
|
|
3
|
+
import '../askDialog';
|
|
3
4
|
import { windowsTaskRunsInteractive, windowsTaskRunsVisibly } from '../windowsTaskState';
|
|
4
5
|
export interface DaemonArgs extends ProtectAllArgs {
|
|
5
6
|
install?: string;
|
|
@@ -97,6 +98,15 @@ export declare function registerHiddenTask(taskName: string, xml: string | strin
|
|
|
97
98
|
* (the pre-1.22.1 VBS launcher) — the migration trigger. */
|
|
98
99
|
export declare function windowsTaskReferencesScriptHost(taskName: string): boolean;
|
|
99
100
|
export { windowsTaskRunsInteractive, windowsTaskRunsVisibly };
|
|
101
|
+
/**
|
|
102
|
+
* Boot-time self-heal for the Windows autostart chain. Recreates a missing
|
|
103
|
+
* daemon logon entry, refreshes the launcher scripts (paths move on upgrade),
|
|
104
|
+
* and re-points the watchdog task at the `watchdog` launcher (older versions
|
|
105
|
+
* pointed it at daemon.vbs, which restarts but never beacons). Creating
|
|
106
|
+
* per-user MINUTE tasks needs no elevation; ONLOGON does — fall back to the
|
|
107
|
+
* per-user Run key exactly like installWindows().
|
|
108
|
+
*/
|
|
109
|
+
export declare function ensureWindowsAutostartHealthy(logFn: (message: string) => void): void;
|
|
100
110
|
/** Snapshot of the resident daemon for out-of-process callers (watchdog/status). */
|
|
101
111
|
export interface DaemonRuntimeState {
|
|
102
112
|
alive: boolean;
|