@bike4mind/cli 3.2.0 → 3.4.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/{AgentHistoryStore-BWko6txx.mjs → AgentHistoryStore-ClO2zOn6.mjs} +394 -115
- package/dist/{ConfigStore-v0wgf0_j.mjs → ConfigStore-CbVFLXMH.mjs} +470 -58
- package/dist/{WorkItemsClient-BZL44N12.mjs → WorkItemsClient-SeNbzvEw.mjs} +1 -1
- package/dist/{buildAgent-BZnmDQIr.mjs → buildAgent-BeaVbpzH.mjs} +27 -12
- package/dist/commands/acpCommand.mjs +16 -12
- package/dist/commands/apiCommand.mjs +1 -1
- package/dist/commands/doctorCommand.mjs +1 -1
- package/dist/commands/envCommand.mjs +1 -1
- package/dist/commands/headlessCommand.mjs +62 -31
- package/dist/commands/mcpCommand.mjs +3 -3
- package/dist/commands/pluginCommand.mjs +1 -1
- package/dist/commands/updateCommand.mjs +1 -1
- package/dist/{dist-5DDaX8tB.mjs → dist-DlBq3wHu.mjs} +2 -2
- package/dist/index.mjs +26 -10
- package/dist/{package-COr17yKT.mjs → package-MoDZ9cGj.mjs} +1 -1
- package/dist/{serve-D6Eu3oXv.mjs → serve-zi-B2iUT.mjs} +2 -2
- package/package.json +10 -10
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as
|
|
2
|
+
import { $ as OllamaEmbeddingModel, At as secureParameters, Bt as withRetry, C as htmlErrorTitle, Ct as isPlaceholderImageSigningSecret, Dt as obfuscateApiKey, J as LOCATION_MAP_LANGUAGE, Jt as frontmatterHasSourceStamp, Mt as splitEquals, P as BedrockEmbeddingModel, Q as NotFoundError, R as ChatModels, S as isValidSessionId, T as escapeArtifactBodyJson, U as DecisionTextPartSchema, W as DecisionsRequestSchema, X as McpServerName, Xt as WEBSITE_URL, Yt as APP_NAME, a as commandTextOf, b as logger, bt as googleMapsSearchUrl, ct as UnprocessableEntityError, dt as WORK_ITEM_STATUSES, et as OpenAIEmbeddingModel, ft as assertAuthOriginMatchesApiKey, it as SEARCH_RESULT_CARDS_LANGUAGE, j as ApiKeyType, jt as signImageUrl, l as writePathOf, nt as PermissionDeniedError, o as isReadOnlyTool, r as loadContextFiles, s as sandboxedCommandArg, st as SupportedFabFileMimeTypes, ut as VoyageAIEmbeddingModel, vt as getMcpProviderMetadata, w as htmlFirstH1, wt as isRetryableError, x as SESSION_ID_PATTERN } from "./ConfigStore-CbVFLXMH.mjs";
|
|
3
3
|
import { n as isPathAllowed, t as assertPathAllowed } from "./pathValidation-BRqf4HFX-CHwtwp3O.mjs";
|
|
4
4
|
import { n as isTerminalShellStatus, t as getShellSessionManager } from "./ShellSessionManager-6o8KZzl1-vrbPAUTq.mjs";
|
|
5
|
-
import { n as B4mStreamEndedError, r as createClient, t as B4mApiError } from "./dist-
|
|
5
|
+
import { n as B4mStreamEndedError, r as createClient, t as B4mApiError } from "./dist-DlBq3wHu.mjs";
|
|
6
6
|
import { createHash, randomBytes } from "crypto";
|
|
7
7
|
import { existsSync, lstatSync, promises, readdirSync, realpathSync, rmSync, statSync } from "fs";
|
|
8
8
|
import os, { homedir } from "os";
|
|
@@ -2363,7 +2363,7 @@ new Map([
|
|
|
2363
2363
|
...Object.values(OLLAMA_EMBEDDING_MODEL_MAP)
|
|
2364
2364
|
].map((info) => [info.model, info.dimensions[0]]));
|
|
2365
2365
|
//#endregion
|
|
2366
|
-
//#region ../../b4m-core/services/dist/webfetch-
|
|
2366
|
+
//#region ../../b4m-core/services/dist/webfetch-BDmPyiMm.mjs
|
|
2367
2367
|
const htmlToMarkdown = (html, _isArxiv = false) => {
|
|
2368
2368
|
const turndownService = new turndown({
|
|
2369
2369
|
headingStyle: "atx",
|
|
@@ -2655,6 +2655,13 @@ function webFetchBody(result) {
|
|
|
2655
2655
|
if (result.extractedChars === 0 && result.offset > 0) return `[web_fetch: offset ${result.offset} is at or beyond the end of the content (~${result.originalChars} chars); nothing further to read.]`;
|
|
2656
2656
|
return result.truncated ? result.markdown + truncationMarker(result) : result.markdown;
|
|
2657
2657
|
}
|
|
2658
|
+
/**
|
|
2659
|
+
* Leads an indexed turn's fetched page with the number the turn gave it (ToolContext.numberCitables).
|
|
2660
|
+
* A directive for the same reason as INDEXED_TOOL_CITATION_RULE.
|
|
2661
|
+
*/
|
|
2662
|
+
function webFetchCitationLine(citationIndex) {
|
|
2663
|
+
return `This page is source [${citationIndex}]. Cite content from it ONLY as [${citationIndex}], placed immediately after the claim it supports.\n\n`;
|
|
2664
|
+
}
|
|
2658
2665
|
function isPdfUrl(url) {
|
|
2659
2666
|
try {
|
|
2660
2667
|
const { pathname } = new URL(url);
|
|
@@ -2704,6 +2711,35 @@ async function probeLlmsTxt(pageUrl) {
|
|
|
2704
2711
|
return full ?? index;
|
|
2705
2712
|
}
|
|
2706
2713
|
/**
|
|
2714
|
+
* Firecrawl refused the target site itself (its 403 "we do not support this site"). A property of
|
|
2715
|
+
* the URL, not a fault of ours, so it is a 422: /api/tools/web-fetch logs it at warn instead of
|
|
2716
|
+
* paging, and the web_fetch tool turns it into a hint to use web_search. Only 403 - Firecrawl's
|
|
2717
|
+
* 401/402/429 are our key, credits or rate limit and must keep surfacing as server errors.
|
|
2718
|
+
*/
|
|
2719
|
+
var UnsupportedScrapeTargetError = class extends UnprocessableEntityError {
|
|
2720
|
+
constructor(url, cause) {
|
|
2721
|
+
super(`This site can't be fetched: the web scraper does not support ${new URL(url).hostname}.`);
|
|
2722
|
+
this.name = "UnsupportedScrapeTargetError";
|
|
2723
|
+
this.cause = cause;
|
|
2724
|
+
}
|
|
2725
|
+
};
|
|
2726
|
+
/** Scrape after a 1s wait for client-side rendering, retrying without the action where Firecrawl rejects actions. */
|
|
2727
|
+
async function scrapeAfterRenderWait(app, url, params) {
|
|
2728
|
+
try {
|
|
2729
|
+
return await app.scrapeUrl(url, {
|
|
2730
|
+
...params,
|
|
2731
|
+
actions: [{
|
|
2732
|
+
type: "wait",
|
|
2733
|
+
milliseconds: 1e3
|
|
2734
|
+
}]
|
|
2735
|
+
});
|
|
2736
|
+
} catch (scrapeError) {
|
|
2737
|
+
if (!(scrapeError instanceof Error ? scrapeError.message : "").includes("Actions are not supported")) throw scrapeError;
|
|
2738
|
+
Logger.globalInstance.log("📥 WebFetch Tool: Actions not supported, retrying without actions...");
|
|
2739
|
+
return app.scrapeUrl(url, params);
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
/**
|
|
2707
2743
|
* Fetch URL content using Firecrawl (shared function, no ToolContext)
|
|
2708
2744
|
* Pattern follows serpApiSearch from websearch tool
|
|
2709
2745
|
*
|
|
@@ -2742,20 +2778,11 @@ async function firecrawlFetch(adapters, url, options) {
|
|
|
2742
2778
|
};
|
|
2743
2779
|
const startedAt = Date.now();
|
|
2744
2780
|
let result;
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
result = await app.scrapeUrl(url, {
|
|
2748
|
-
...baseParams,
|
|
2749
|
-
actions: [{
|
|
2750
|
-
type: "wait",
|
|
2751
|
-
milliseconds: 1e3
|
|
2752
|
-
}]
|
|
2753
|
-
});
|
|
2781
|
+
try {
|
|
2782
|
+
result = isPdf ? await app.scrapeUrl(url, baseParams) : await scrapeAfterRenderWait(app, url, baseParams);
|
|
2754
2783
|
} catch (scrapeError) {
|
|
2755
|
-
if (
|
|
2756
|
-
|
|
2757
|
-
result = await app.scrapeUrl(url, baseParams);
|
|
2758
|
-
} else throw scrapeError;
|
|
2784
|
+
if (scrapeError instanceof FirecrawlError && scrapeError.statusCode === 403) throw new UnsupportedScrapeTargetError(url, scrapeError);
|
|
2785
|
+
throw scrapeError;
|
|
2759
2786
|
}
|
|
2760
2787
|
const durationMs = Date.now() - startedAt;
|
|
2761
2788
|
if (!result || result.error) {
|
|
@@ -2830,19 +2857,27 @@ const webFetchTool = {
|
|
|
2830
2857
|
cap: result.cap
|
|
2831
2858
|
}
|
|
2832
2859
|
};
|
|
2833
|
-
|
|
2860
|
+
const citationIndex = context.numberCitables?.([citable])?.[0].metadata?.citationIndex;
|
|
2861
|
+
await context.statusUpdate({ promptMeta: { citables: [citationIndex === void 0 ? citable : {
|
|
2862
|
+
...citable,
|
|
2863
|
+
metadata: {
|
|
2864
|
+
...citable.metadata,
|
|
2865
|
+
citationIndex
|
|
2866
|
+
}
|
|
2867
|
+
}] } }, "WebFetch completed successfully");
|
|
2834
2868
|
Logger.globalInstance.log(`📚 WebFetch Tool: Stored citable source for ${params.url}`);
|
|
2835
|
-
return webFetchBody(result);
|
|
2869
|
+
return (citationIndex === void 0 ? "" : webFetchCitationLine(citationIndex)) + webFetchBody(result);
|
|
2836
2870
|
} catch (error) {
|
|
2837
2871
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
2838
|
-
|
|
2872
|
+
const firecrawlStatus = error instanceof UnsupportedScrapeTargetError ? 403 : error instanceof FirecrawlError ? error.statusCode : void 0;
|
|
2873
|
+
if (firecrawlStatus !== void 0 && [
|
|
2839
2874
|
400,
|
|
2840
2875
|
403,
|
|
2841
2876
|
408,
|
|
2842
2877
|
502,
|
|
2843
2878
|
503,
|
|
2844
2879
|
504
|
|
2845
|
-
].includes(
|
|
2880
|
+
].includes(firecrawlStatus)) {
|
|
2846
2881
|
const { userMessage, statusLabel } = {
|
|
2847
2882
|
400: {
|
|
2848
2883
|
userMessage: `This site requires advanced browser features that are not available. Try using web_search to find the information instead.`,
|
|
@@ -2868,10 +2903,10 @@ const webFetchTool = {
|
|
|
2868
2903
|
userMessage: `The website took too long to respond (Gateway Timeout). Try again later or use web_search to find the information instead.`,
|
|
2869
2904
|
statusLabel: "gateway timeout"
|
|
2870
2905
|
}
|
|
2871
|
-
}[
|
|
2906
|
+
}[firecrawlStatus];
|
|
2872
2907
|
context.logger.warn("WebFetch expected Firecrawl failure", {
|
|
2873
2908
|
url: params.url,
|
|
2874
|
-
statusCode:
|
|
2909
|
+
statusCode: firecrawlStatus,
|
|
2875
2910
|
error: errorMessage
|
|
2876
2911
|
});
|
|
2877
2912
|
await context.statusUpdate({}, `WebFetch: ${statusLabel}`);
|
|
@@ -2987,7 +3022,16 @@ const WEB_SEARCH_MAP_PROMPT = [
|
|
|
2987
3022
|
"explanation or citations. Omit it only if none of the places actually answer the question."
|
|
2988
3023
|
].join("\n");
|
|
2989
3024
|
//#endregion
|
|
2990
|
-
//#region ../../b4m-core/services/dist/websearch-
|
|
3025
|
+
//#region ../../b4m-core/services/dist/websearch-Dm7iz09B.mjs
|
|
3026
|
+
/**
|
|
3027
|
+
* Shown to the model under tool results that carry the turn's shared numbers, so it cites them the
|
|
3028
|
+
* way forced retrieval's header asks it to cite the documents numbered before them. Keep it a directive:
|
|
3029
|
+
* on a tool-only turn there is no forced-retrieval header, and a purely descriptive label here was
|
|
3030
|
+
* tried and the model mostly stopped writing [N] markers at all.
|
|
3031
|
+
*/
|
|
3032
|
+
const INDEXED_TOOL_CITATION_RULE = "Cite each of these sources ONLY by the bracketed number it is labelled with, placed immediately after the claim it supports. These numbers continue the sources already numbered this turn.";
|
|
3033
|
+
/** Each stamped citable's number, keyed by citable id, for labelling results that point at it. */
|
|
3034
|
+
const citationNumbersById = (citables) => new Map(citables.flatMap((citable) => citable.metadata?.citationIndex === void 0 ? [] : [[citable.id, citable.metadata.citationIndex]]));
|
|
2991
3035
|
/**
|
|
2992
3036
|
* Per-process cache for web-search results. Keyed on normalized query + result count, with a
|
|
2993
3037
|
* short TTL so repeated/similar searches within a turn or across back-to-back turns reuse the
|
|
@@ -3667,15 +3711,19 @@ function describePlace(place) {
|
|
|
3667
3711
|
place.address
|
|
3668
3712
|
].filter((part) => !!part).map(stripNewlines).join(" - ");
|
|
3669
3713
|
}
|
|
3670
|
-
function formatPlaceResults(places, anchor) {
|
|
3714
|
+
function formatPlaceResults(places, anchor, citationNumbers) {
|
|
3715
|
+
const numberOf = (place) => citationNumbers?.get(`place:${place.id}`);
|
|
3671
3716
|
return [
|
|
3672
3717
|
"Places found for this search (each has a map location; refer to it by its id):",
|
|
3673
3718
|
"",
|
|
3674
|
-
...places.map((place, index) =>
|
|
3719
|
+
...places.map((place, index) => {
|
|
3720
|
+
const n = numberOf(place);
|
|
3721
|
+
return `${n !== void 0 ? `[${n}]` : `${index + 1}.`} ${describePlace(place)}\n id: ${stripNewlines(place.id)}`;
|
|
3722
|
+
}),
|
|
3675
3723
|
...anchor ? [
|
|
3676
3724
|
"",
|
|
3677
3725
|
"Anchor location (the place the user named):",
|
|
3678
|
-
`${describePlace(anchor)}\n id: ${stripNewlines(anchor.id)}`
|
|
3726
|
+
`${numberOf(anchor) !== void 0 ? `[${numberOf(anchor)}] ` : ""}${describePlace(anchor)}\n id: ${stripNewlines(anchor.id)}`
|
|
3679
3727
|
] : []
|
|
3680
3728
|
].join("\n");
|
|
3681
3729
|
}
|
|
@@ -3781,7 +3829,7 @@ async function searchWithHedge(lead, backup, query, numResults, searchOptions) {
|
|
|
3781
3829
|
throw new Error(`Web search failed on both providers. ${lead.name}: ${errorMessage$1(leadError)}; ${backup.name}: ${errorMessage$1(backupError)}`);
|
|
3782
3830
|
}
|
|
3783
3831
|
}
|
|
3784
|
-
async function performWebSearch(adapters, params, imageUrlSigningSecret = "", resolvedProviders) {
|
|
3832
|
+
async function performWebSearch(adapters, params, imageUrlSigningSecret = "", resolvedProviders, numberCitables) {
|
|
3785
3833
|
Logger.globalInstance.log("🔍 WebSearch Tool: Starting search for query:", params.query);
|
|
3786
3834
|
const [provider, backup] = await (resolvedProviders ?? resolveWebSearchProviders(adapters));
|
|
3787
3835
|
if (!provider) {
|
|
@@ -3820,7 +3868,7 @@ async function performWebSearch(adapters, params, imageUrlSigningSecret = "", re
|
|
|
3820
3868
|
if (placeResults.length) Logger.globalInstance.log(`WebSearch Tool: ${servedBy.name} found ${placeResults.length} places`);
|
|
3821
3869
|
if (imageResults.length) Logger.globalInstance.log(`🖼️ WebSearch Tool: ${servedBy.name} found ${imageResults.length} images`);
|
|
3822
3870
|
const withImages = shouldIncludeImages(results, wantsImages, imageResults);
|
|
3823
|
-
const
|
|
3871
|
+
const unnumberedCitables = results.map((result, index) => ({
|
|
3824
3872
|
id: result.url,
|
|
3825
3873
|
type: "web_url",
|
|
3826
3874
|
title: result.title,
|
|
@@ -3838,15 +3886,18 @@ async function performWebSearch(adapters, params, imageUrlSigningSecret = "", re
|
|
|
3838
3886
|
} : {}
|
|
3839
3887
|
}
|
|
3840
3888
|
}));
|
|
3841
|
-
if (withPlaces) for (const place of anchor ? [anchor, ...places] : places)
|
|
3889
|
+
if (withPlaces) for (const place of anchor ? [anchor, ...places] : places) unnumberedCitables.push(placeCitable(place, imageUrlSigningSecret, canSignImages));
|
|
3890
|
+
const numberedCitables = numberCitables?.(unnumberedCitables);
|
|
3891
|
+
const citables = numberedCitables ?? unnumberedCitables;
|
|
3892
|
+
const resultLabel = (index) => numberedCitables ? `[${numberedCitables[index].metadata?.citationIndex}]` : `${index + 1}.`;
|
|
3842
3893
|
const formattedResults = results.map((result, index) => {
|
|
3843
3894
|
const imageLine = withImages && result.images?.length ? `Images: ${result.images.map((url) => signImageUrl(url, imageUrlSigningSecret)).join(" | ")}\n` : "";
|
|
3844
|
-
return `${index
|
|
3895
|
+
return `${resultLabel(index)} **${stripNewlines(result.title)}**\n${stripNewlines(result.snippet)}\n` + imageLine + `Source: [${safeHostname(result.url)}](${result.url})\n`;
|
|
3845
3896
|
}).join("\n");
|
|
3846
3897
|
const imageSection = withImages && imageResults.length ? `\n${formatImageResults(imageResults, imageUrlSigningSecret)}\n` : "";
|
|
3847
|
-
const placeSection = withPlaces ? `\n${formatPlaceResults(places, anchor)}\n` : "";
|
|
3898
|
+
const placeSection = withPlaces ? `\n${formatPlaceResults(places, anchor, numberedCitables ? citationNumbersById(numberedCitables) : void 0)}\n` : "";
|
|
3848
3899
|
return {
|
|
3849
|
-
formattedResults: (formattedResults ? `Here's what I found from searching the web:\n\n${formattedResults}` : imageSection || placeSection ? "Here's what I found from searching the web:\n" : "No results found from web search.") + imageSection + placeSection + (withImages ? `\n${WEB_SEARCH_CARDS_PROMPT}` : "") + (withPlaces ? `\n${WEB_SEARCH_MAP_PROMPT}` : ""),
|
|
3900
|
+
formattedResults: (formattedResults ? `Here's what I found from searching the web:\n\n${formattedResults}` + (numberedCitables ? `\n${INDEXED_TOOL_CITATION_RULE}\n` : "") : imageSection || placeSection ? "Here's what I found from searching the web:\n" : "No results found from web search.") + imageSection + placeSection + (withImages ? `\n${WEB_SEARCH_CARDS_PROMPT}` : "") + (withPlaces ? `\n${WEB_SEARCH_MAP_PROMPT}` : ""),
|
|
3850
3901
|
citables
|
|
3851
3902
|
};
|
|
3852
3903
|
} catch (error) {
|
|
@@ -3869,7 +3920,7 @@ const webSearchTool = {
|
|
|
3869
3920
|
throw error;
|
|
3870
3921
|
});
|
|
3871
3922
|
const startedAt = Date.now();
|
|
3872
|
-
const { formattedResults, citables } = await performWebSearch({ db: context.db }, params, config?.imageUrlSigningSecret, providersPromise);
|
|
3923
|
+
const { formattedResults, citables } = await performWebSearch({ db: context.db }, params, config?.imageUrlSigningSecret, providersPromise, context.numberCitables);
|
|
3873
3924
|
context.logger.log("🔍 WebSearch Tool: search timing", {
|
|
3874
3925
|
callNumber,
|
|
3875
3926
|
query: params.query,
|
|
@@ -4130,7 +4181,7 @@ async function getModelForWrite(user, modelId, deps) {
|
|
|
4130
4181
|
return isModelOwner(model, user) ? model : null;
|
|
4131
4182
|
}
|
|
4132
4183
|
//#endregion
|
|
4133
|
-
//#region ../../b4m-core/services/dist/toolGenerators-
|
|
4184
|
+
//#region ../../b4m-core/services/dist/toolGenerators-Ce9EJurs.mjs
|
|
4134
4185
|
const diceRoll = async (parameters) => {
|
|
4135
4186
|
if (!parameters?.sides || !parameters?.times) throw new Error("Tool dice roll: Missing required parameters");
|
|
4136
4187
|
return sum(times(parameters.times, () => random(1, parameters.sides))).toString();
|
|
@@ -4725,6 +4776,119 @@ const promptEnhancementTool = {
|
|
|
4725
4776
|
}
|
|
4726
4777
|
})
|
|
4727
4778
|
};
|
|
4779
|
+
const DecisionToolRequestSchema = DecisionsRequestSchema.pick({
|
|
4780
|
+
input: true,
|
|
4781
|
+
questions: true,
|
|
4782
|
+
model: true
|
|
4783
|
+
}).extend({
|
|
4784
|
+
input: DecisionTextPartSchema.shape.text,
|
|
4785
|
+
model: DecisionsRequestSchema.shape.model.optional()
|
|
4786
|
+
});
|
|
4787
|
+
/**
|
|
4788
|
+
* Typed decision checks (predicate / choice / score) answered with calibrated probabilities,
|
|
4789
|
+
* without spending a generation turn. Execution is host-injected (see `ToolContext.decide`): the
|
|
4790
|
+
* runner, its credit hold and provider retry live in the app tier, so this tool only forwards.
|
|
4791
|
+
*/
|
|
4792
|
+
const decideTool = {
|
|
4793
|
+
name: "decide",
|
|
4794
|
+
implementation: (context) => ({
|
|
4795
|
+
toolFn: async (value) => {
|
|
4796
|
+
const run = context.decide;
|
|
4797
|
+
if (!run) throw new Error("decide is not available on this surface");
|
|
4798
|
+
const { input, questions, model } = DecisionToolRequestSchema.parse(value);
|
|
4799
|
+
const result = await run({
|
|
4800
|
+
userId: context.userId,
|
|
4801
|
+
user: context.user,
|
|
4802
|
+
logger: context.logger,
|
|
4803
|
+
authOrigin: context.authOrigin,
|
|
4804
|
+
apiKeyId: context.apiKeyId,
|
|
4805
|
+
organizationId: context.organizationId,
|
|
4806
|
+
request: {
|
|
4807
|
+
model,
|
|
4808
|
+
input,
|
|
4809
|
+
questions
|
|
4810
|
+
}
|
|
4811
|
+
});
|
|
4812
|
+
return JSON.stringify({
|
|
4813
|
+
id: result.id,
|
|
4814
|
+
model: result.model,
|
|
4815
|
+
answers: result.answers
|
|
4816
|
+
});
|
|
4817
|
+
},
|
|
4818
|
+
toolSchema: {
|
|
4819
|
+
name: "decide",
|
|
4820
|
+
description: "Answer typed questions about text with calibrated probabilities: classify, route or score without spending a full answer. Types are predicate (probability), choice (probabilities over values) and score (probabilities over an ordered scale). Answers return in question order; a declined question is a `refusal`, not an error.",
|
|
4821
|
+
parameters: {
|
|
4822
|
+
type: "object",
|
|
4823
|
+
properties: {
|
|
4824
|
+
input: {
|
|
4825
|
+
type: "string",
|
|
4826
|
+
description: "Text to judge."
|
|
4827
|
+
},
|
|
4828
|
+
questions: {
|
|
4829
|
+
type: "array",
|
|
4830
|
+
description: "One to twenty, each judged independently.",
|
|
4831
|
+
items: {
|
|
4832
|
+
type: "object",
|
|
4833
|
+
properties: {
|
|
4834
|
+
type: {
|
|
4835
|
+
type: "string",
|
|
4836
|
+
enum: [
|
|
4837
|
+
"predicate",
|
|
4838
|
+
"choice",
|
|
4839
|
+
"score"
|
|
4840
|
+
]
|
|
4841
|
+
},
|
|
4842
|
+
name: {
|
|
4843
|
+
type: "string",
|
|
4844
|
+
description: "Unique within the call."
|
|
4845
|
+
},
|
|
4846
|
+
instructions: {
|
|
4847
|
+
type: "string",
|
|
4848
|
+
description: "What to judge."
|
|
4849
|
+
},
|
|
4850
|
+
choices: {
|
|
4851
|
+
type: "array",
|
|
4852
|
+
description: "Required for `choice`.",
|
|
4853
|
+
items: {
|
|
4854
|
+
type: "object",
|
|
4855
|
+
properties: {
|
|
4856
|
+
value: { type: "string" },
|
|
4857
|
+
description: { type: "string" }
|
|
4858
|
+
},
|
|
4859
|
+
required: ["value"]
|
|
4860
|
+
}
|
|
4861
|
+
},
|
|
4862
|
+
levels: {
|
|
4863
|
+
type: "array",
|
|
4864
|
+
description: "Required for `score`: lowest first.",
|
|
4865
|
+
items: {
|
|
4866
|
+
type: "object",
|
|
4867
|
+
properties: {
|
|
4868
|
+
label: { type: "string" },
|
|
4869
|
+
description: { type: "string" }
|
|
4870
|
+
},
|
|
4871
|
+
required: ["label"]
|
|
4872
|
+
}
|
|
4873
|
+
}
|
|
4874
|
+
},
|
|
4875
|
+
required: [
|
|
4876
|
+
"type",
|
|
4877
|
+
"name",
|
|
4878
|
+
"instructions"
|
|
4879
|
+
]
|
|
4880
|
+
}
|
|
4881
|
+
},
|
|
4882
|
+
model: {
|
|
4883
|
+
type: "string",
|
|
4884
|
+
description: "Optional model id; omit for the deployment default."
|
|
4885
|
+
}
|
|
4886
|
+
},
|
|
4887
|
+
required: ["input", "questions"]
|
|
4888
|
+
}
|
|
4889
|
+
}
|
|
4890
|
+
})
|
|
4891
|
+
};
|
|
4728
4892
|
let _showUserQuestion = null;
|
|
4729
4893
|
/**
|
|
4730
4894
|
* Inject the CLI callback that displays the question UI.
|
|
@@ -4844,7 +5008,7 @@ const askUserQuestionTool = {
|
|
|
4844
5008
|
* re-export them without pulling the full tool graph. `index.ts` re-exports them
|
|
4845
5009
|
* so the server barrel's public API is unchanged.
|
|
4846
5010
|
*/
|
|
4847
|
-
const generateTools = (userId, user, logger, { db, retrievalFilter, kbScope, inlinedAttachmentIds, attachedFileIds, fullyInlinedAttachmentIds, suppressLakeArms, sessionRetrievalTags, sessionReaderConsentDatalakeTags, sessionLakeScopeExplicit, sessionIncludeLibraryFiles, sessionPreauthorizedLakeIds, organizationId, authOrigin, apiKeyId, questId, getAbortSignal }, storage, imageGenerateStorage, statusUpdate, onStart, onFinish, llm, config, model, imageProcessorLambdaName, tools, allowedDirectories, entitlementKeys = [], sessionId, codeMinifier, availableModels, onToolLlmUsage) => {
|
|
5011
|
+
const generateTools = (userId, user, logger, { db, retrievalFilter, kbScope, inlinedAttachmentIds, attachedFileIds, fullyInlinedAttachmentIds, suppressLakeArms, sessionRetrievalTags, sessionReaderConsentDatalakeTags, sessionLakeScopeExplicit, sessionIncludeLibraryFiles, sessionPreauthorizedLakeIds, organizationId, authOrigin, apiKeyId, questId, numberCitables, decide, getAbortSignal }, storage, imageGenerateStorage, statusUpdate, onStart, onFinish, llm, config, model, imageProcessorLambdaName, tools, allowedDirectories, entitlementKeys = [], sessionId, codeMinifier, availableModels, onToolLlmUsage) => {
|
|
4848
5012
|
assertAuthOriginMatchesApiKey(authOrigin, apiKeyId);
|
|
4849
5013
|
const context = {
|
|
4850
5014
|
userId,
|
|
@@ -4880,7 +5044,9 @@ const generateTools = (userId, user, logger, { db, retrievalFilter, kbScope, inl
|
|
|
4880
5044
|
codeMinifier,
|
|
4881
5045
|
availableModels,
|
|
4882
5046
|
onToolLlmUsage,
|
|
4883
|
-
|
|
5047
|
+
decide,
|
|
5048
|
+
getAbortSignal,
|
|
5049
|
+
numberCitables
|
|
4884
5050
|
};
|
|
4885
5051
|
return Object.entries(tools).reduce((acc, [key, tool]) => {
|
|
4886
5052
|
const built = tool.implementation(context, config[key]);
|
|
@@ -7075,7 +7241,8 @@ const cliSharedTools = {
|
|
|
7075
7241
|
prompt_enhancement: promptEnhancementTool,
|
|
7076
7242
|
weather_info: weatherTool,
|
|
7077
7243
|
web_search: webSearchTool,
|
|
7078
|
-
web_fetch: webFetchTool
|
|
7244
|
+
web_fetch: webFetchTool,
|
|
7245
|
+
decide: decideTool
|
|
7079
7246
|
};
|
|
7080
7247
|
/**
|
|
7081
7248
|
* The 6 Lattice tool implementations as a resolvable map keyed by tool name.
|
|
@@ -7734,7 +7901,8 @@ var CustomCommandStore = class {
|
|
|
7734
7901
|
*/
|
|
7735
7902
|
async loadCommandFile(filePath, source) {
|
|
7736
7903
|
const filename = path.basename(filePath);
|
|
7737
|
-
const
|
|
7904
|
+
const isSkillFile = filename.toLowerCase() === "skill.md";
|
|
7905
|
+
const commandName = isSkillFile ? this.extractSkillName(filePath) : extractCommandName(filename);
|
|
7738
7906
|
if (!commandName) {
|
|
7739
7907
|
console.warn(`Invalid command filename: ${filename} (must end with .md and have valid name)`);
|
|
7740
7908
|
return;
|
|
@@ -7743,7 +7911,12 @@ var CustomCommandStore = class {
|
|
|
7743
7911
|
console.warn(`Ignoring project command "${commandName}": name is reserved (built-in or feature command)`);
|
|
7744
7912
|
return;
|
|
7745
7913
|
}
|
|
7746
|
-
const
|
|
7914
|
+
const fileContent = await fs.readFile(filePath, "utf-8");
|
|
7915
|
+
if (isSkillFile && frontmatterHasSourceStamp(fileContent)) {
|
|
7916
|
+
logger.debug(`Skipping ${filePath}: exported from Bike4Mind; the account skill is used instead`);
|
|
7917
|
+
return;
|
|
7918
|
+
}
|
|
7919
|
+
const command = parseCommandFile(fileContent, filePath, commandName, source);
|
|
7747
7920
|
this.commands.set(commandName, command);
|
|
7748
7921
|
}
|
|
7749
7922
|
/**
|
|
@@ -7868,9 +8041,16 @@ const DEFAULT_PAGE_LIMIT = 100;
|
|
|
7868
8041
|
var RemoteSkillSource = class {
|
|
7869
8042
|
constructor(apiClient, options = {}) {
|
|
7870
8043
|
this.apiClient = apiClient;
|
|
8044
|
+
this.forceNext = false;
|
|
7871
8045
|
this.cacheFilePath = options.cacheFilePath ?? path.join(os.homedir(), ".bike4mind", "cache", "remote-skills.json");
|
|
7872
8046
|
this.freshTtlMs = options.freshTtlMs ?? DEFAULT_FRESH_TTL_MS;
|
|
7873
8047
|
this.pageLimit = options.pageLimit ?? DEFAULT_PAGE_LIMIT;
|
|
8048
|
+
this.includeImports = options.includeImports ?? false;
|
|
8049
|
+
}
|
|
8050
|
+
/** Make the next `fetchSkills` go to the network even inside the TTL. The cache stays as the
|
|
8051
|
+
* offline fallback. For a host that just changed the account's skills itself. */
|
|
8052
|
+
invalidate() {
|
|
8053
|
+
this.forceNext = true;
|
|
7874
8054
|
}
|
|
7875
8055
|
/**
|
|
7876
8056
|
* Returns the user's remote skills mapped to `CustomCommand`. Uses an
|
|
@@ -7879,7 +8059,9 @@ var RemoteSkillSource = class {
|
|
|
7879
8059
|
*/
|
|
7880
8060
|
async fetchSkills() {
|
|
7881
8061
|
const cached = await this.readCache();
|
|
7882
|
-
|
|
8062
|
+
const forced = this.forceNext;
|
|
8063
|
+
this.forceNext = false;
|
|
8064
|
+
if (cached && !forced && this.isFresh(cached)) return this.mapAndFilter(cached.skills);
|
|
7883
8065
|
try {
|
|
7884
8066
|
const skills = (await this.apiClient.get(`/api/skills?limit=${this.pageLimit}`))?.data ?? [];
|
|
7885
8067
|
if (skills.length > 0 || !cached || cached.skills.length === 0) await this.writeCache({
|
|
@@ -7901,6 +8083,7 @@ var RemoteSkillSource = class {
|
|
|
7901
8083
|
* only visible cross-account via `isGlobalRead`, and
|
|
7902
8084
|
* - a Skill Market import: the user owns the copy but someone else wrote it, and local commands run
|
|
7903
8085
|
* as trusted instructions next to shell/file tools (the web app wraps it as untrusted instead).
|
|
8086
|
+
* With `includeImports` it is kept as `untrusted: true`, for a host that wraps it the same way.
|
|
7904
8087
|
* Own, system (no `userId`), and org-scoped (server-gated, no `userId`) skills pass.
|
|
7905
8088
|
*
|
|
7906
8089
|
* Unauthenticated contract (fail-closed, intentional): when getCurrentUser()
|
|
@@ -7914,7 +8097,7 @@ var RemoteSkillSource = class {
|
|
|
7914
8097
|
return skills.filter((skill) => {
|
|
7915
8098
|
if (isReservedCommandName(skill.name)) return false;
|
|
7916
8099
|
if (skill.userId && skill.userId !== currentUserId) return false;
|
|
7917
|
-
if (skill.importedFrom) return false;
|
|
8100
|
+
if (skill.importedFrom && !this.includeImports) return false;
|
|
7918
8101
|
return true;
|
|
7919
8102
|
}).map((skill) => this.toCustomCommand(skill));
|
|
7920
8103
|
}
|
|
@@ -7965,6 +8148,7 @@ var RemoteSkillSource = class {
|
|
|
7965
8148
|
argumentHint: skill.argumentHint,
|
|
7966
8149
|
allowedTools: skill.allowedTools,
|
|
7967
8150
|
disableModelInvocation: skill.disableModelInvocation,
|
|
8151
|
+
...skill.importedFrom ? { untrusted: true } : {},
|
|
7968
8152
|
source: "remote",
|
|
7969
8153
|
filePath: `b4m:/api/skills/${skill.id}`
|
|
7970
8154
|
};
|
|
@@ -11427,12 +11611,15 @@ z$1.object({
|
|
|
11427
11611
|
//#endregion
|
|
11428
11612
|
//#region src/tools/gateway/stages/sandbox.ts
|
|
11429
11613
|
/**
|
|
11430
|
-
* Decide whether a
|
|
11431
|
-
*
|
|
11614
|
+
* Decide whether a call runs inside the OS sandbox, and rewrite its args to do so. Only a tool
|
|
11615
|
+
* whose descriptor declares `sandbox` is wrapped; any other tool, or a call with no command or
|
|
11616
|
+
* no orchestrator, is left unchanged.
|
|
11432
11617
|
*/
|
|
11433
|
-
function prepareSandbox(
|
|
11434
|
-
|
|
11435
|
-
|
|
11618
|
+
function prepareSandbox(descriptor, args, sandboxOrchestrator, allowedDirectories) {
|
|
11619
|
+
const commandArg = sandboxedCommandArg(descriptor);
|
|
11620
|
+
if (!commandArg || !args || !sandboxOrchestrator) return { kind: "unchanged" };
|
|
11621
|
+
const command = args[commandArg];
|
|
11622
|
+
if (typeof command !== "string" || !command) return { kind: "unchanged" };
|
|
11436
11623
|
let cwd = args.cwd ? path.resolve(process.cwd(), args.cwd) : process.cwd();
|
|
11437
11624
|
try {
|
|
11438
11625
|
cwd = realpathSync(cwd);
|
|
@@ -11451,15 +11638,13 @@ function prepareSandbox(toolName, args, sandboxOrchestrator, allowedDirectories)
|
|
|
11451
11638
|
};
|
|
11452
11639
|
}
|
|
11453
11640
|
sandboxOrchestrator.recordSandboxed();
|
|
11454
|
-
const cleanupPaths = decision.wrappedCommand.cleanupPaths;
|
|
11455
11641
|
return {
|
|
11456
11642
|
kind: "sandboxed",
|
|
11457
|
-
cleanupPaths,
|
|
11643
|
+
cleanupPaths: decision.wrappedCommand.cleanupPaths,
|
|
11458
11644
|
args: {
|
|
11459
11645
|
...args,
|
|
11460
11646
|
cwd,
|
|
11461
|
-
|
|
11462
|
-
_sandboxCleanup: cleanupPaths
|
|
11647
|
+
[commandArg]: decision.wrappedCommand.commandString
|
|
11463
11648
|
}
|
|
11464
11649
|
};
|
|
11465
11650
|
}
|
|
@@ -11493,23 +11678,6 @@ function cleanupSandboxFiles(paths) {
|
|
|
11493
11678
|
} catch {}
|
|
11494
11679
|
}
|
|
11495
11680
|
/**
|
|
11496
|
-
* Detect whether a tool result indicates a sandbox-specific runtime failure
|
|
11497
|
-
* worth offering an unsandboxed retry for. Matches the sandbox markers
|
|
11498
|
-
* (`sandbox-exec:`, `bwrap:`) AND a bare "Operation not permitted": on macOS a
|
|
11499
|
-
* real Seatbelt denial is the denied syscall's own EPERM, printed by the tool
|
|
11500
|
-
* that hit it with NO `sandbox-exec:` prefix (that prefix only appears when the
|
|
11501
|
-
* profile fails to load) - a write denial reads `touch: <path>: Operation not
|
|
11502
|
-
* permitted`, and a blocked connect surfaces the same EPERM. Matching only the
|
|
11503
|
-
* markers would never fire for those, silently dropping the recovery offer and
|
|
11504
|
-
* the `/sandbox:network on` tip. The known false positive (e.g. `kill` on a
|
|
11505
|
-
* foreign pid) is accepted: it only costs a deniable retry prompt, whereas a
|
|
11506
|
-
* false negative removes the recovery path entirely.
|
|
11507
|
-
*/
|
|
11508
|
-
function isSandboxFailure(isSandboxed, result) {
|
|
11509
|
-
if (!isSandboxed) return false;
|
|
11510
|
-
return result.includes("sandbox-exec:") || result.includes("bwrap:") || result.includes("Operation not permitted");
|
|
11511
|
-
}
|
|
11512
|
-
/**
|
|
11513
11681
|
* Parse sandbox runtime stderr for violations and record them.
|
|
11514
11682
|
* Fire-and-forget - errors are silently swallowed.
|
|
11515
11683
|
*/
|
|
@@ -12529,11 +12697,12 @@ function setWebSocketToolExecutor(executor) {
|
|
|
12529
12697
|
const SERVER_TOOLS = [
|
|
12530
12698
|
"weather_info",
|
|
12531
12699
|
"web_search",
|
|
12532
|
-
"web_fetch"
|
|
12700
|
+
"web_fetch",
|
|
12701
|
+
"decide"
|
|
12533
12702
|
];
|
|
12534
12703
|
/**
|
|
12535
|
-
* Server-side tools (weather, web_search, web_fetch) run on B4M's API with company keys;
|
|
12536
|
-
* other tool runs locally through the implementation its caller passes in.
|
|
12704
|
+
* Server-side tools (weather, web_search, web_fetch, decide) run on B4M's API with company keys;
|
|
12705
|
+
* every other tool runs locally through the implementation its caller passes in.
|
|
12537
12706
|
*/
|
|
12538
12707
|
function isServerTool(toolName) {
|
|
12539
12708
|
return SERVER_TOOLS.includes(toolName);
|
|
@@ -13045,25 +13214,119 @@ function getProcessHooks() {
|
|
|
13045
13214
|
return cached;
|
|
13046
13215
|
}
|
|
13047
13216
|
//#endregion
|
|
13217
|
+
//#region src/tools/gateway/stages/sandboxEscalation.ts
|
|
13218
|
+
const SNIPPET_LENGTH = 200;
|
|
13219
|
+
const EXIT_CODE_LINE = /^Exit code:\s*(-?\d+)\s*$/gm;
|
|
13220
|
+
const LAUNCHER_MARKER = /^(?:sandbox-exec|bwrap):/m;
|
|
13221
|
+
const EPERM_TEXT = "Operation not permitted";
|
|
13222
|
+
const NETWORK_TIP = "If this was a network call, prefer '/sandbox:network on' to allow filtered egress.";
|
|
13223
|
+
/**
|
|
13224
|
+
* Detect a sandbox denial in a tool result. Both evidence kinds - a launcher marker
|
|
13225
|
+
* (`sandbox-exec:`, `bwrap:` at the start of a line) and the EPERM text a Seatbelt denial
|
|
13226
|
+
* surfaces on macOS - require a non-zero exit: a command that printed either and still exited
|
|
13227
|
+
* 0 (e.g. a tolerated `kill`, or output that merely quotes the launcher) was not stopped.
|
|
13228
|
+
*/
|
|
13229
|
+
function detectSandboxDenial(sandboxed, result) {
|
|
13230
|
+
if (!sandboxed || !exitedNonZero(result)) return null;
|
|
13231
|
+
const marker = LAUNCHER_MARKER.exec(result);
|
|
13232
|
+
if (marker) return {
|
|
13233
|
+
reason: "launcher",
|
|
13234
|
+
snippet: snippetAt(result, marker.index)
|
|
13235
|
+
};
|
|
13236
|
+
const epermIndex = result.indexOf(EPERM_TEXT);
|
|
13237
|
+
if (epermIndex === -1) return null;
|
|
13238
|
+
return {
|
|
13239
|
+
reason: "eperm",
|
|
13240
|
+
snippet: snippetAt(result, epermIndex)
|
|
13241
|
+
};
|
|
13242
|
+
}
|
|
13243
|
+
/**
|
|
13244
|
+
* The tool's exit code is non-zero. The last "Exit code:" line is the tool's own (the command
|
|
13245
|
+
* may have printed earlier ones); a command that ends its own output with such a line can still
|
|
13246
|
+
* be mistaken for a failure, which costs an extra question, never a silent unsandboxed run.
|
|
13247
|
+
*/
|
|
13248
|
+
function exitedNonZero(result) {
|
|
13249
|
+
const exitCodes = [...result.matchAll(EXIT_CODE_LINE)];
|
|
13250
|
+
const exitCode = exitCodes[exitCodes.length - 1]?.[1];
|
|
13251
|
+
return exitCode !== void 0 && Number(exitCode) !== 0;
|
|
13252
|
+
}
|
|
13253
|
+
/** The error text from the start of the line holding `index`, so long stdout never hides it. */
|
|
13254
|
+
function snippetAt(result, index) {
|
|
13255
|
+
const lineStart = result.lastIndexOf("\n", index) + 1;
|
|
13256
|
+
return result.slice(lineStart, lineStart + SNIPPET_LENGTH);
|
|
13257
|
+
}
|
|
13258
|
+
/** A bash session (background, or promoted after yield_time_ms) may still be running. */
|
|
13259
|
+
function isSessionRun(args) {
|
|
13260
|
+
return args.run_in_background === true || typeof args.yield_time_ms === "number";
|
|
13261
|
+
}
|
|
13262
|
+
/**
|
|
13263
|
+
* The sandbox stage of execution: run the attempt, record violations, and if the OS sandbox
|
|
13264
|
+
* denied it, ask before retrying outside the sandbox. The question is its own prompt: auto-accept,
|
|
13265
|
+
* session trust, trusted tools and the host allowlist never approve it, and when nobody can
|
|
13266
|
+
* answer, the model gets the sandbox error back. Session runs (background or yield_time_ms) are
|
|
13267
|
+
* never escalated: the sandboxed process may still be running.
|
|
13268
|
+
*/
|
|
13269
|
+
async function runSandboxed(input) {
|
|
13270
|
+
const { descriptor, rawArgs, execArgs, isSandboxed, attempt, permissionManager, sandboxOrchestrator } = input;
|
|
13271
|
+
const result = await attempt(execArgs);
|
|
13272
|
+
await captureViolations(isSandboxed, result, commandTextOf(descriptor, rawArgs), sandboxOrchestrator);
|
|
13273
|
+
const commandArg = sandboxedCommandArg(descriptor);
|
|
13274
|
+
const command = commandTextOf(descriptor, rawArgs);
|
|
13275
|
+
if (!commandArg || command === void 0 || isSessionRun(execArgs)) return result;
|
|
13276
|
+
const denial = detectSandboxDenial(isSandboxed, result);
|
|
13277
|
+
if (!denial) return result;
|
|
13278
|
+
if (sandboxOrchestrator?.getConfig().allowUnsandboxedCommands === false) return `${result}\n\nThe command was denied by the OS sandbox and running it outside the sandbox is disabled. ${NETWORK_TIP}`;
|
|
13279
|
+
const cwd = typeof execArgs.cwd === "string" ? execArgs.cwd : process.cwd();
|
|
13280
|
+
const isHighRisk = assessCommandRisk(descriptor, rawArgs)?.level === "high";
|
|
13281
|
+
const approvalKey = JSON.stringify([cwd, command]);
|
|
13282
|
+
if (isHighRisk || !permissionManager.isSandboxEscalationApproved(approvalKey)) {
|
|
13283
|
+
const response = await input.showPermissionPrompt(input.toolName, rawArgs, escalationPreview(denial, isHighRisk), "sandbox-escalation");
|
|
13284
|
+
if (response.action === "deny") return `${result}\n\n${NETWORK_TIP}`;
|
|
13285
|
+
if (response.action === "allow-session" && !isHighRisk) permissionManager.approveSandboxEscalation(approvalKey);
|
|
13286
|
+
}
|
|
13287
|
+
sandboxOrchestrator?.recordUnsandboxed();
|
|
13288
|
+
return attempt({
|
|
13289
|
+
...execArgs,
|
|
13290
|
+
[commandArg]: command,
|
|
13291
|
+
cwd
|
|
13292
|
+
});
|
|
13293
|
+
}
|
|
13294
|
+
function escalationPreview(denial, isHighRisk) {
|
|
13295
|
+
return `🛑 SANDBOX BLOCKED - This command was denied by the OS sandbox.
|
|
13296
|
+
|
|
13297
|
+
- The sandbox prevented this operation because it violates filesystem or network restrictions.
|
|
13298
|
+
- ${NETWORK_TIP}\n- You can retry without the sandbox, but the command will run with full system access.\n` + (isHighRisk ? `- This command is high risk, so a session approval is not remembered: you will be asked again.\n` : "") + `\n@@Error Details@@\n${denial.snippet}`;
|
|
13299
|
+
}
|
|
13300
|
+
//#endregion
|
|
13048
13301
|
//#region src/tools/gateway/stages/execution.ts
|
|
13049
13302
|
/**
|
|
13050
|
-
* Run an approved call: checkpoint, execute (ToolRouter redacts and
|
|
13051
|
-
*
|
|
13052
|
-
* post-tool signals.
|
|
13303
|
+
* Run an approved call: checkpoint, execute through the sandbox stage (ToolRouter redacts and
|
|
13304
|
+
* caps the output; a sandbox denial asks before retrying unsandboxed), offer retry on a path
|
|
13305
|
+
* denial, then fire the post-tool signals.
|
|
13053
13306
|
*/
|
|
13054
13307
|
async function executeAndRecord(input) {
|
|
13055
13308
|
const { toolName, descriptor, rawArgs, execArgs, isSandboxed, originalFn, apiClient, showPermissionPrompt } = input;
|
|
13056
13309
|
await checkpointBeforeWrite(input.checkpointStore, toolName, writePathOf(descriptor, rawArgs));
|
|
13057
|
-
|
|
13058
|
-
|
|
13059
|
-
|
|
13060
|
-
|
|
13061
|
-
|
|
13062
|
-
|
|
13063
|
-
|
|
13064
|
-
|
|
13065
|
-
|
|
13066
|
-
result = await
|
|
13310
|
+
const attempt = async (args) => {
|
|
13311
|
+
try {
|
|
13312
|
+
return await executeWithFuzzyConfirmation(toolName, args, apiClient, originalFn, showPermissionPrompt);
|
|
13313
|
+
} catch (err) {
|
|
13314
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13315
|
+
if (!isPathAccessDenial(msg)) throw err;
|
|
13316
|
+
return msg;
|
|
13317
|
+
}
|
|
13318
|
+
};
|
|
13319
|
+
let result = await runSandboxed({
|
|
13320
|
+
toolName,
|
|
13321
|
+
descriptor,
|
|
13322
|
+
rawArgs,
|
|
13323
|
+
execArgs,
|
|
13324
|
+
isSandboxed,
|
|
13325
|
+
attempt,
|
|
13326
|
+
permissionManager: input.permissionManager,
|
|
13327
|
+
showPermissionPrompt,
|
|
13328
|
+
sandboxOrchestrator: input.sandboxOrchestrator
|
|
13329
|
+
});
|
|
13067
13330
|
result = await retryPathAccessDenial(result, toolName, execArgs, input.allowedDirectories, input.configStore, apiClient, originalFn, showPermissionPrompt);
|
|
13068
13331
|
notifyFileChanged(input.agentContext, writePathOf(descriptor, execArgs), result);
|
|
13069
13332
|
getProcessHooks()?.firePostToolUse(toolName);
|
|
@@ -13087,21 +13350,6 @@ function notifyFileChanged(agentContext, writePath, result) {
|
|
|
13087
13350
|
agentContext.onFileChanged(writePath);
|
|
13088
13351
|
}
|
|
13089
13352
|
/**
|
|
13090
|
-
* If the result looks like a sandbox runtime failure, prompt the user to retry
|
|
13091
|
-
* the command unsandboxed. Returns the retry result or the original result.
|
|
13092
|
-
*/
|
|
13093
|
-
async function retrySandboxFailure(isSandboxed, result, toolName, originalArgs, apiClient, originalFn, showPermissionPrompt) {
|
|
13094
|
-
if (!isSandboxFailure(isSandboxed, result)) return result;
|
|
13095
|
-
if ((await showPermissionPrompt(toolName, originalArgs, `🛑 SANDBOX BLOCKED - This command was denied by the OS sandbox.
|
|
13096
|
-
|
|
13097
|
-
- The sandbox prevented this operation because it violates filesystem or network restrictions.
|
|
13098
|
-
- If this was a network call, prefer '/sandbox:network on' to allow filtered egress instead of full access.
|
|
13099
|
-
- You can retry without the sandbox, but the command will run with full system access.
|
|
13100
|
-
|
|
13101
|
-
@@Error Details@@\n${result.slice(0, 200)}`)).action !== "deny") return executeTool(toolName, originalArgs, apiClient, originalFn);
|
|
13102
|
-
return result;
|
|
13103
|
-
}
|
|
13104
|
-
/**
|
|
13105
13353
|
* Matches the three shapes of filesystem allow-list denial emitted by core:
|
|
13106
13354
|
* - file tools: `Access denied: Cannot <op> files outside allowed directories.`
|
|
13107
13355
|
* - glob_files: `Access denied: Cannot search outside allowed directories.` (no "files")
|
|
@@ -13194,7 +13442,7 @@ function wrapToolWithPermission(tool, deps) {
|
|
|
13194
13442
|
toolFn: async (input) => {
|
|
13195
13443
|
const args = input;
|
|
13196
13444
|
const descriptor = permissionManager.catalog.resolve(toolName);
|
|
13197
|
-
const sandbox = prepareSandbox(
|
|
13445
|
+
const sandbox = prepareSandbox(descriptor, args, deps.sandboxOrchestrator, allowedDirectories);
|
|
13198
13446
|
if (sandbox.kind === "blocked") return sandbox.message;
|
|
13199
13447
|
const isSandboxed = sandbox.kind === "sandboxed";
|
|
13200
13448
|
const effectiveArgs = isSandboxed ? sandbox.args : args;
|
|
@@ -13209,6 +13457,7 @@ function wrapToolWithPermission(tool, deps) {
|
|
|
13209
13457
|
originalFn,
|
|
13210
13458
|
apiClient: deps.apiClient,
|
|
13211
13459
|
showPermissionPrompt,
|
|
13460
|
+
permissionManager,
|
|
13212
13461
|
agentContext: deps.agentContext,
|
|
13213
13462
|
configStore: deps.configStore,
|
|
13214
13463
|
sandboxOrchestrator: deps.sandboxOrchestrator,
|
|
@@ -13364,7 +13613,7 @@ The user has cycled into plan mode (Shift+Tab). Plan mode restricts WRITING, not
|
|
|
13364
13613
|
- \`get_file_structure\` — AST overview of a file
|
|
13365
13614
|
- \`agent_delegate\` — delegate to read-only subagents (e.g. 'explore', 'plan')
|
|
13366
13615
|
- \`ask_user_question\` — ask the user clarifying questions
|
|
13367
|
-
- \`current_datetime\`, \`math_evaluate\`, \`web_search\`, \`web_fetch\`
|
|
13616
|
+
- \`current_datetime\`, \`math_evaluate\`, \`web_search\`, \`web_fetch\`, \`decide\` - also fine
|
|
13368
13617
|
|
|
13369
13618
|
**Tools blocked in plan mode:**
|
|
13370
13619
|
- \`bash_execute\`, \`edit_local_file\`, \`create_file\`, \`delete_file\` — and any other tool that mutates state.
|
|
@@ -13937,6 +14186,28 @@ const DEFAULT_AGENT_MODEL = ChatModels.CLAUDE_4_5_HAIKU;
|
|
|
13937
14186
|
*/
|
|
13938
14187
|
const DEFAULT_THOROUGHNESS = "medium";
|
|
13939
14188
|
//#endregion
|
|
14189
|
+
//#region src/utils/decisionModelProbe.ts
|
|
14190
|
+
const probeDecisionModels = async (apiClient) => {
|
|
14191
|
+
try {
|
|
14192
|
+
const body = await apiClient.get("/api/v1/decision-models");
|
|
14193
|
+
return Array.isArray(body?.models) && body.models.length > 0;
|
|
14194
|
+
} catch {
|
|
14195
|
+
logger.debug("[decide] Decision-model availability lookup failed; hiding the tool until the next build.");
|
|
14196
|
+
return false;
|
|
14197
|
+
}
|
|
14198
|
+
};
|
|
14199
|
+
let decisionModelsProbe;
|
|
14200
|
+
/**
|
|
14201
|
+
* Whether this deployment serves a decision model this caller can call. The CLI holds no key
|
|
14202
|
+
* table of its own - the server resolves the key - so one request is the only honest signal;
|
|
14203
|
+
* a failed probe hides the tool rather than offering one that would only 503. A yes is cached
|
|
14204
|
+
* for the process (keys rarely appear mid-run); a no is not, so a later build re-checks.
|
|
14205
|
+
*/
|
|
14206
|
+
const hasDecisionModels = (apiClient) => decisionModelsProbe ??= probeDecisionModels(apiClient).then((available) => {
|
|
14207
|
+
if (!available) decisionModelsProbe = void 0;
|
|
14208
|
+
return available;
|
|
14209
|
+
});
|
|
14210
|
+
//#endregion
|
|
13940
14211
|
//#region src/utils/toolsAdapter.ts
|
|
13941
14212
|
/**
|
|
13942
14213
|
* Simple CLI-friendly storage adapter
|
|
@@ -14124,7 +14395,7 @@ async function generateCliTools(userId, llm, model, permissionManager, showPermi
|
|
|
14124
14395
|
},
|
|
14125
14396
|
authOrigin: "session"
|
|
14126
14397
|
};
|
|
14127
|
-
const
|
|
14398
|
+
const enabledB4mToolNames = [
|
|
14128
14399
|
"dice_roll",
|
|
14129
14400
|
"math_evaluate",
|
|
14130
14401
|
"current_datetime",
|
|
@@ -14132,7 +14403,11 @@ async function generateCliTools(userId, llm, model, permissionManager, showPermi
|
|
|
14132
14403
|
"weather_info",
|
|
14133
14404
|
"web_search",
|
|
14134
14405
|
"web_fetch"
|
|
14135
|
-
]
|
|
14406
|
+
];
|
|
14407
|
+
const normalizedAllowed = toolFilter?.allowedTools?.map(normalizeToolName);
|
|
14408
|
+
const normalizedDenied = toolFilter?.deniedTools?.map(normalizeToolName);
|
|
14409
|
+
if (!normalizedDenied?.includes("decide") && (!normalizedAllowed?.length || normalizedAllowed.includes("decide")) && await hasDecisionModels(apiClient)) enabledB4mToolNames.push("decide");
|
|
14410
|
+
const filteredB4mTools = Object.fromEntries(enabledB4mToolNames.filter((name) => name in cliSharedTools).map((name) => [name, cliSharedTools[name]]));
|
|
14136
14411
|
const cliOnlyTools = await getCliOnlyTools();
|
|
14137
14412
|
const tools_to_generate = {
|
|
14138
14413
|
...filteredB4mTools,
|
|
@@ -14156,17 +14431,12 @@ async function generateCliTools(userId, llm, model, permissionManager, showPermi
|
|
|
14156
14431
|
checkpointStore
|
|
14157
14432
|
};
|
|
14158
14433
|
let tools = wrapTools(Object.values(toolsMap), wrapDeps);
|
|
14159
|
-
if (toolFilter) {
|
|
14160
|
-
const
|
|
14161
|
-
|
|
14162
|
-
|
|
14163
|
-
|
|
14164
|
-
|
|
14165
|
-
if (normalizedDenied && normalizedDenied.includes(toolName)) return false;
|
|
14166
|
-
if (normalizedAllowed && normalizedAllowed.length > 0) return normalizedAllowed.includes(toolName);
|
|
14167
|
-
return true;
|
|
14168
|
-
});
|
|
14169
|
-
}
|
|
14434
|
+
if (toolFilter) tools = tools.filter((tool) => {
|
|
14435
|
+
const toolName = tool.toolSchema.name;
|
|
14436
|
+
if (normalizedDenied && normalizedDenied.includes(toolName)) return false;
|
|
14437
|
+
if (normalizedAllowed && normalizedAllowed.length > 0) return normalizedAllowed.includes(toolName);
|
|
14438
|
+
return true;
|
|
14439
|
+
});
|
|
14170
14440
|
return {
|
|
14171
14441
|
tools,
|
|
14172
14442
|
agentContext
|
|
@@ -14195,6 +14465,7 @@ var PermissionManager = class {
|
|
|
14195
14465
|
this.deniedTools = /* @__PURE__ */ new Set();
|
|
14196
14466
|
this._sandboxMode = "disabled";
|
|
14197
14467
|
this._sandboxActive = false;
|
|
14468
|
+
this.sandboxEscalationApprovals = /* @__PURE__ */ new Set();
|
|
14198
14469
|
this.trustedTools = new Set(trustedTools);
|
|
14199
14470
|
this.deniedTools = new Set(deniedTools);
|
|
14200
14471
|
}
|
|
@@ -14220,14 +14491,14 @@ var PermissionManager = class {
|
|
|
14220
14491
|
*
|
|
14221
14492
|
* @param toolName - The tool being checked
|
|
14222
14493
|
* @param options.isSandboxed - If true, the command will run inside the OS-level sandbox.
|
|
14223
|
-
* In auto-allow mode, sandboxed
|
|
14494
|
+
* In auto-allow mode, sandboxed commands (a core tool whose descriptor declares `sandbox`) skip the permission prompt
|
|
14224
14495
|
* because the sandbox provides the security boundary.
|
|
14225
14496
|
*/
|
|
14226
14497
|
needsPermission(toolName, options) {
|
|
14227
14498
|
const category = this.catalog.category(toolName);
|
|
14228
14499
|
if (this.deniedTools.has(toolName)) return true;
|
|
14229
14500
|
if (category === "auto_approve") return false;
|
|
14230
|
-
if (options?.isSandboxed && toolName
|
|
14501
|
+
if (options?.isSandboxed && sandboxedCommandArg(this.catalog.resolve(toolName)) !== void 0 && this.isSandboxAutoAllow()) return false;
|
|
14231
14502
|
if (this.sessionTrustedTools.has(toolName)) return false;
|
|
14232
14503
|
if (category === "prompt_always") return true;
|
|
14233
14504
|
return !this.trustedTools.has(toolName);
|
|
@@ -14300,11 +14571,19 @@ var PermissionManager = class {
|
|
|
14300
14571
|
isSessionTrusted(toolName) {
|
|
14301
14572
|
return this.sessionTrustedTools.has(toolName);
|
|
14302
14573
|
}
|
|
14574
|
+
/** Remember a session-scoped approval to run one command outside the sandbox. Never persisted. */
|
|
14575
|
+
approveSandboxEscalation(key) {
|
|
14576
|
+
this.sandboxEscalationApprovals.add(key);
|
|
14577
|
+
}
|
|
14578
|
+
isSandboxEscalationApproved(key) {
|
|
14579
|
+
return this.sandboxEscalationApprovals.has(key);
|
|
14580
|
+
}
|
|
14303
14581
|
/**
|
|
14304
14582
|
* Clear all session-scoped trust (called on exit or session reset)
|
|
14305
14583
|
*/
|
|
14306
14584
|
clearSessionTrust() {
|
|
14307
14585
|
this.sessionTrustedTools.clear();
|
|
14586
|
+
this.sandboxEscalationApprovals.clear();
|
|
14308
14587
|
}
|
|
14309
14588
|
/**
|
|
14310
14589
|
* Clear all trusted tools
|