@bike4mind/cli 3.2.0 → 3.3.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-CLidbOHG.mjs} +231 -95
- package/dist/{ConfigStore-v0wgf0_j.mjs → ConfigStore-DUATzLxB.mjs} +312 -48
- package/dist/{WorkItemsClient-BZL44N12.mjs → WorkItemsClient-BSoOblGp.mjs} +1 -1
- package/dist/{buildAgent-BZnmDQIr.mjs → buildAgent-DyZWK0LW.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-DPP7UmK9.mjs} +2 -2
- package/dist/index.mjs +26 -10
- package/dist/{package-COr17yKT.mjs → package-DYPVrada.mjs} +1 -1
- package/dist/{serve-D6Eu3oXv.mjs → serve-w6v3rZiR.mjs} +2 -2
- package/package.json +10 -10
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { At as splitEquals, C as htmlErrorTitle, J as McpServerName, K as LOCATION_MAP_LANGUAGE, Kt as APP_NAME, Ot as secureParameters, P as BedrockEmbeddingModel, Q as OpenAIEmbeddingModel, R as ChatModels, Rt as withRetry, S as isValidSessionId, St as isRetryableError, T as escapeArtifactBodyJson, Tt as obfuscateApiKey, X as NotFoundError, Z as OllamaEmbeddingModel, a as commandTextOf, at as SupportedFabFileMimeTypes, b as logger, ct as VoyageAIEmbeddingModel, et as PermissionDeniedError, gt as getMcpProviderMetadata, j as ApiKeyType, kt as signImageUrl, l as writePathOf, lt as WORK_ITEM_STATUSES, nt as SEARCH_RESULT_CARDS_LANGUAGE, o as isReadOnlyTool, ot as UnprocessableEntityError, qt as WEBSITE_URL, r as loadContextFiles, s as sandboxedCommandArg, ut as assertAuthOriginMatchesApiKey, vt as googleMapsSearchUrl, w as htmlFirstH1, x as SESSION_ID_PATTERN, xt as isPlaceholderImageSigningSecret } from "./ConfigStore-DUATzLxB.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-DPP7UmK9.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-BVxSjEv_.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();
|
|
@@ -4844,7 +4895,7 @@ const askUserQuestionTool = {
|
|
|
4844
4895
|
* re-export them without pulling the full tool graph. `index.ts` re-exports them
|
|
4845
4896
|
* so the server barrel's public API is unchanged.
|
|
4846
4897
|
*/
|
|
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) => {
|
|
4898
|
+
const generateTools = (userId, user, logger, { db, retrievalFilter, kbScope, inlinedAttachmentIds, attachedFileIds, fullyInlinedAttachmentIds, suppressLakeArms, sessionRetrievalTags, sessionReaderConsentDatalakeTags, sessionLakeScopeExplicit, sessionIncludeLibraryFiles, sessionPreauthorizedLakeIds, organizationId, authOrigin, apiKeyId, questId, numberCitables, getAbortSignal }, storage, imageGenerateStorage, statusUpdate, onStart, onFinish, llm, config, model, imageProcessorLambdaName, tools, allowedDirectories, entitlementKeys = [], sessionId, codeMinifier, availableModels, onToolLlmUsage) => {
|
|
4848
4899
|
assertAuthOriginMatchesApiKey(authOrigin, apiKeyId);
|
|
4849
4900
|
const context = {
|
|
4850
4901
|
userId,
|
|
@@ -4880,7 +4931,8 @@ const generateTools = (userId, user, logger, { db, retrievalFilter, kbScope, inl
|
|
|
4880
4931
|
codeMinifier,
|
|
4881
4932
|
availableModels,
|
|
4882
4933
|
onToolLlmUsage,
|
|
4883
|
-
getAbortSignal
|
|
4934
|
+
getAbortSignal,
|
|
4935
|
+
numberCitables
|
|
4884
4936
|
};
|
|
4885
4937
|
return Object.entries(tools).reduce((acc, [key, tool]) => {
|
|
4886
4938
|
const built = tool.implementation(context, config[key]);
|
|
@@ -7868,9 +7920,16 @@ const DEFAULT_PAGE_LIMIT = 100;
|
|
|
7868
7920
|
var RemoteSkillSource = class {
|
|
7869
7921
|
constructor(apiClient, options = {}) {
|
|
7870
7922
|
this.apiClient = apiClient;
|
|
7923
|
+
this.forceNext = false;
|
|
7871
7924
|
this.cacheFilePath = options.cacheFilePath ?? path.join(os.homedir(), ".bike4mind", "cache", "remote-skills.json");
|
|
7872
7925
|
this.freshTtlMs = options.freshTtlMs ?? DEFAULT_FRESH_TTL_MS;
|
|
7873
7926
|
this.pageLimit = options.pageLimit ?? DEFAULT_PAGE_LIMIT;
|
|
7927
|
+
this.includeImports = options.includeImports ?? false;
|
|
7928
|
+
}
|
|
7929
|
+
/** Make the next `fetchSkills` go to the network even inside the TTL. The cache stays as the
|
|
7930
|
+
* offline fallback. For a host that just changed the account's skills itself. */
|
|
7931
|
+
invalidate() {
|
|
7932
|
+
this.forceNext = true;
|
|
7874
7933
|
}
|
|
7875
7934
|
/**
|
|
7876
7935
|
* Returns the user's remote skills mapped to `CustomCommand`. Uses an
|
|
@@ -7879,7 +7938,9 @@ var RemoteSkillSource = class {
|
|
|
7879
7938
|
*/
|
|
7880
7939
|
async fetchSkills() {
|
|
7881
7940
|
const cached = await this.readCache();
|
|
7882
|
-
|
|
7941
|
+
const forced = this.forceNext;
|
|
7942
|
+
this.forceNext = false;
|
|
7943
|
+
if (cached && !forced && this.isFresh(cached)) return this.mapAndFilter(cached.skills);
|
|
7883
7944
|
try {
|
|
7884
7945
|
const skills = (await this.apiClient.get(`/api/skills?limit=${this.pageLimit}`))?.data ?? [];
|
|
7885
7946
|
if (skills.length > 0 || !cached || cached.skills.length === 0) await this.writeCache({
|
|
@@ -7901,6 +7962,7 @@ var RemoteSkillSource = class {
|
|
|
7901
7962
|
* only visible cross-account via `isGlobalRead`, and
|
|
7902
7963
|
* - a Skill Market import: the user owns the copy but someone else wrote it, and local commands run
|
|
7903
7964
|
* as trusted instructions next to shell/file tools (the web app wraps it as untrusted instead).
|
|
7965
|
+
* With `includeImports` it is kept as `untrusted: true`, for a host that wraps it the same way.
|
|
7904
7966
|
* Own, system (no `userId`), and org-scoped (server-gated, no `userId`) skills pass.
|
|
7905
7967
|
*
|
|
7906
7968
|
* Unauthenticated contract (fail-closed, intentional): when getCurrentUser()
|
|
@@ -7914,7 +7976,7 @@ var RemoteSkillSource = class {
|
|
|
7914
7976
|
return skills.filter((skill) => {
|
|
7915
7977
|
if (isReservedCommandName(skill.name)) return false;
|
|
7916
7978
|
if (skill.userId && skill.userId !== currentUserId) return false;
|
|
7917
|
-
if (skill.importedFrom) return false;
|
|
7979
|
+
if (skill.importedFrom && !this.includeImports) return false;
|
|
7918
7980
|
return true;
|
|
7919
7981
|
}).map((skill) => this.toCustomCommand(skill));
|
|
7920
7982
|
}
|
|
@@ -7965,6 +8027,7 @@ var RemoteSkillSource = class {
|
|
|
7965
8027
|
argumentHint: skill.argumentHint,
|
|
7966
8028
|
allowedTools: skill.allowedTools,
|
|
7967
8029
|
disableModelInvocation: skill.disableModelInvocation,
|
|
8030
|
+
...skill.importedFrom ? { untrusted: true } : {},
|
|
7968
8031
|
source: "remote",
|
|
7969
8032
|
filePath: `b4m:/api/skills/${skill.id}`
|
|
7970
8033
|
};
|
|
@@ -11427,12 +11490,15 @@ z$1.object({
|
|
|
11427
11490
|
//#endregion
|
|
11428
11491
|
//#region src/tools/gateway/stages/sandbox.ts
|
|
11429
11492
|
/**
|
|
11430
|
-
* Decide whether a
|
|
11431
|
-
*
|
|
11493
|
+
* Decide whether a call runs inside the OS sandbox, and rewrite its args to do so. Only a tool
|
|
11494
|
+
* whose descriptor declares `sandbox` is wrapped; any other tool, or a call with no command or
|
|
11495
|
+
* no orchestrator, is left unchanged.
|
|
11432
11496
|
*/
|
|
11433
|
-
function prepareSandbox(
|
|
11434
|
-
|
|
11435
|
-
|
|
11497
|
+
function prepareSandbox(descriptor, args, sandboxOrchestrator, allowedDirectories) {
|
|
11498
|
+
const commandArg = sandboxedCommandArg(descriptor);
|
|
11499
|
+
if (!commandArg || !args || !sandboxOrchestrator) return { kind: "unchanged" };
|
|
11500
|
+
const command = args[commandArg];
|
|
11501
|
+
if (typeof command !== "string" || !command) return { kind: "unchanged" };
|
|
11436
11502
|
let cwd = args.cwd ? path.resolve(process.cwd(), args.cwd) : process.cwd();
|
|
11437
11503
|
try {
|
|
11438
11504
|
cwd = realpathSync(cwd);
|
|
@@ -11451,15 +11517,13 @@ function prepareSandbox(toolName, args, sandboxOrchestrator, allowedDirectories)
|
|
|
11451
11517
|
};
|
|
11452
11518
|
}
|
|
11453
11519
|
sandboxOrchestrator.recordSandboxed();
|
|
11454
|
-
const cleanupPaths = decision.wrappedCommand.cleanupPaths;
|
|
11455
11520
|
return {
|
|
11456
11521
|
kind: "sandboxed",
|
|
11457
|
-
cleanupPaths,
|
|
11522
|
+
cleanupPaths: decision.wrappedCommand.cleanupPaths,
|
|
11458
11523
|
args: {
|
|
11459
11524
|
...args,
|
|
11460
11525
|
cwd,
|
|
11461
|
-
|
|
11462
|
-
_sandboxCleanup: cleanupPaths
|
|
11526
|
+
[commandArg]: decision.wrappedCommand.commandString
|
|
11463
11527
|
}
|
|
11464
11528
|
};
|
|
11465
11529
|
}
|
|
@@ -11493,23 +11557,6 @@ function cleanupSandboxFiles(paths) {
|
|
|
11493
11557
|
} catch {}
|
|
11494
11558
|
}
|
|
11495
11559
|
/**
|
|
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
11560
|
* Parse sandbox runtime stderr for violations and record them.
|
|
11514
11561
|
* Fire-and-forget - errors are silently swallowed.
|
|
11515
11562
|
*/
|
|
@@ -13045,25 +13092,119 @@ function getProcessHooks() {
|
|
|
13045
13092
|
return cached;
|
|
13046
13093
|
}
|
|
13047
13094
|
//#endregion
|
|
13095
|
+
//#region src/tools/gateway/stages/sandboxEscalation.ts
|
|
13096
|
+
const SNIPPET_LENGTH = 200;
|
|
13097
|
+
const EXIT_CODE_LINE = /^Exit code:\s*(-?\d+)\s*$/gm;
|
|
13098
|
+
const LAUNCHER_MARKER = /^(?:sandbox-exec|bwrap):/m;
|
|
13099
|
+
const EPERM_TEXT = "Operation not permitted";
|
|
13100
|
+
const NETWORK_TIP = "If this was a network call, prefer '/sandbox:network on' to allow filtered egress.";
|
|
13101
|
+
/**
|
|
13102
|
+
* Detect a sandbox denial in a tool result. Both evidence kinds - a launcher marker
|
|
13103
|
+
* (`sandbox-exec:`, `bwrap:` at the start of a line) and the EPERM text a Seatbelt denial
|
|
13104
|
+
* surfaces on macOS - require a non-zero exit: a command that printed either and still exited
|
|
13105
|
+
* 0 (e.g. a tolerated `kill`, or output that merely quotes the launcher) was not stopped.
|
|
13106
|
+
*/
|
|
13107
|
+
function detectSandboxDenial(sandboxed, result) {
|
|
13108
|
+
if (!sandboxed || !exitedNonZero(result)) return null;
|
|
13109
|
+
const marker = LAUNCHER_MARKER.exec(result);
|
|
13110
|
+
if (marker) return {
|
|
13111
|
+
reason: "launcher",
|
|
13112
|
+
snippet: snippetAt(result, marker.index)
|
|
13113
|
+
};
|
|
13114
|
+
const epermIndex = result.indexOf(EPERM_TEXT);
|
|
13115
|
+
if (epermIndex === -1) return null;
|
|
13116
|
+
return {
|
|
13117
|
+
reason: "eperm",
|
|
13118
|
+
snippet: snippetAt(result, epermIndex)
|
|
13119
|
+
};
|
|
13120
|
+
}
|
|
13121
|
+
/**
|
|
13122
|
+
* The tool's exit code is non-zero. The last "Exit code:" line is the tool's own (the command
|
|
13123
|
+
* may have printed earlier ones); a command that ends its own output with such a line can still
|
|
13124
|
+
* be mistaken for a failure, which costs an extra question, never a silent unsandboxed run.
|
|
13125
|
+
*/
|
|
13126
|
+
function exitedNonZero(result) {
|
|
13127
|
+
const exitCodes = [...result.matchAll(EXIT_CODE_LINE)];
|
|
13128
|
+
const exitCode = exitCodes[exitCodes.length - 1]?.[1];
|
|
13129
|
+
return exitCode !== void 0 && Number(exitCode) !== 0;
|
|
13130
|
+
}
|
|
13131
|
+
/** The error text from the start of the line holding `index`, so long stdout never hides it. */
|
|
13132
|
+
function snippetAt(result, index) {
|
|
13133
|
+
const lineStart = result.lastIndexOf("\n", index) + 1;
|
|
13134
|
+
return result.slice(lineStart, lineStart + SNIPPET_LENGTH);
|
|
13135
|
+
}
|
|
13136
|
+
/** A bash session (background, or promoted after yield_time_ms) may still be running. */
|
|
13137
|
+
function isSessionRun(args) {
|
|
13138
|
+
return args.run_in_background === true || typeof args.yield_time_ms === "number";
|
|
13139
|
+
}
|
|
13140
|
+
/**
|
|
13141
|
+
* The sandbox stage of execution: run the attempt, record violations, and if the OS sandbox
|
|
13142
|
+
* denied it, ask before retrying outside the sandbox. The question is its own prompt: auto-accept,
|
|
13143
|
+
* session trust, trusted tools and the host allowlist never approve it, and when nobody can
|
|
13144
|
+
* answer, the model gets the sandbox error back. Session runs (background or yield_time_ms) are
|
|
13145
|
+
* never escalated: the sandboxed process may still be running.
|
|
13146
|
+
*/
|
|
13147
|
+
async function runSandboxed(input) {
|
|
13148
|
+
const { descriptor, rawArgs, execArgs, isSandboxed, attempt, permissionManager, sandboxOrchestrator } = input;
|
|
13149
|
+
const result = await attempt(execArgs);
|
|
13150
|
+
await captureViolations(isSandboxed, result, commandTextOf(descriptor, rawArgs), sandboxOrchestrator);
|
|
13151
|
+
const commandArg = sandboxedCommandArg(descriptor);
|
|
13152
|
+
const command = commandTextOf(descriptor, rawArgs);
|
|
13153
|
+
if (!commandArg || command === void 0 || isSessionRun(execArgs)) return result;
|
|
13154
|
+
const denial = detectSandboxDenial(isSandboxed, result);
|
|
13155
|
+
if (!denial) return result;
|
|
13156
|
+
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}`;
|
|
13157
|
+
const cwd = typeof execArgs.cwd === "string" ? execArgs.cwd : process.cwd();
|
|
13158
|
+
const isHighRisk = assessCommandRisk(descriptor, rawArgs)?.level === "high";
|
|
13159
|
+
const approvalKey = JSON.stringify([cwd, command]);
|
|
13160
|
+
if (isHighRisk || !permissionManager.isSandboxEscalationApproved(approvalKey)) {
|
|
13161
|
+
const response = await input.showPermissionPrompt(input.toolName, rawArgs, escalationPreview(denial, isHighRisk), "sandbox-escalation");
|
|
13162
|
+
if (response.action === "deny") return `${result}\n\n${NETWORK_TIP}`;
|
|
13163
|
+
if (response.action === "allow-session" && !isHighRisk) permissionManager.approveSandboxEscalation(approvalKey);
|
|
13164
|
+
}
|
|
13165
|
+
sandboxOrchestrator?.recordUnsandboxed();
|
|
13166
|
+
return attempt({
|
|
13167
|
+
...execArgs,
|
|
13168
|
+
[commandArg]: command,
|
|
13169
|
+
cwd
|
|
13170
|
+
});
|
|
13171
|
+
}
|
|
13172
|
+
function escalationPreview(denial, isHighRisk) {
|
|
13173
|
+
return `🛑 SANDBOX BLOCKED - This command was denied by the OS sandbox.
|
|
13174
|
+
|
|
13175
|
+
- The sandbox prevented this operation because it violates filesystem or network restrictions.
|
|
13176
|
+
- ${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}`;
|
|
13177
|
+
}
|
|
13178
|
+
//#endregion
|
|
13048
13179
|
//#region src/tools/gateway/stages/execution.ts
|
|
13049
13180
|
/**
|
|
13050
|
-
* Run an approved call: checkpoint, execute (ToolRouter redacts and
|
|
13051
|
-
*
|
|
13052
|
-
* post-tool signals.
|
|
13181
|
+
* Run an approved call: checkpoint, execute through the sandbox stage (ToolRouter redacts and
|
|
13182
|
+
* caps the output; a sandbox denial asks before retrying unsandboxed), offer retry on a path
|
|
13183
|
+
* denial, then fire the post-tool signals.
|
|
13053
13184
|
*/
|
|
13054
13185
|
async function executeAndRecord(input) {
|
|
13055
13186
|
const { toolName, descriptor, rawArgs, execArgs, isSandboxed, originalFn, apiClient, showPermissionPrompt } = input;
|
|
13056
13187
|
await checkpointBeforeWrite(input.checkpointStore, toolName, writePathOf(descriptor, rawArgs));
|
|
13057
|
-
|
|
13058
|
-
|
|
13059
|
-
|
|
13060
|
-
|
|
13061
|
-
|
|
13062
|
-
|
|
13063
|
-
|
|
13064
|
-
|
|
13065
|
-
|
|
13066
|
-
result = await
|
|
13188
|
+
const attempt = async (args) => {
|
|
13189
|
+
try {
|
|
13190
|
+
return await executeWithFuzzyConfirmation(toolName, args, apiClient, originalFn, showPermissionPrompt);
|
|
13191
|
+
} catch (err) {
|
|
13192
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13193
|
+
if (!isPathAccessDenial(msg)) throw err;
|
|
13194
|
+
return msg;
|
|
13195
|
+
}
|
|
13196
|
+
};
|
|
13197
|
+
let result = await runSandboxed({
|
|
13198
|
+
toolName,
|
|
13199
|
+
descriptor,
|
|
13200
|
+
rawArgs,
|
|
13201
|
+
execArgs,
|
|
13202
|
+
isSandboxed,
|
|
13203
|
+
attempt,
|
|
13204
|
+
permissionManager: input.permissionManager,
|
|
13205
|
+
showPermissionPrompt,
|
|
13206
|
+
sandboxOrchestrator: input.sandboxOrchestrator
|
|
13207
|
+
});
|
|
13067
13208
|
result = await retryPathAccessDenial(result, toolName, execArgs, input.allowedDirectories, input.configStore, apiClient, originalFn, showPermissionPrompt);
|
|
13068
13209
|
notifyFileChanged(input.agentContext, writePathOf(descriptor, execArgs), result);
|
|
13069
13210
|
getProcessHooks()?.firePostToolUse(toolName);
|
|
@@ -13087,21 +13228,6 @@ function notifyFileChanged(agentContext, writePath, result) {
|
|
|
13087
13228
|
agentContext.onFileChanged(writePath);
|
|
13088
13229
|
}
|
|
13089
13230
|
/**
|
|
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
13231
|
* Matches the three shapes of filesystem allow-list denial emitted by core:
|
|
13106
13232
|
* - file tools: `Access denied: Cannot <op> files outside allowed directories.`
|
|
13107
13233
|
* - glob_files: `Access denied: Cannot search outside allowed directories.` (no "files")
|
|
@@ -13194,7 +13320,7 @@ function wrapToolWithPermission(tool, deps) {
|
|
|
13194
13320
|
toolFn: async (input) => {
|
|
13195
13321
|
const args = input;
|
|
13196
13322
|
const descriptor = permissionManager.catalog.resolve(toolName);
|
|
13197
|
-
const sandbox = prepareSandbox(
|
|
13323
|
+
const sandbox = prepareSandbox(descriptor, args, deps.sandboxOrchestrator, allowedDirectories);
|
|
13198
13324
|
if (sandbox.kind === "blocked") return sandbox.message;
|
|
13199
13325
|
const isSandboxed = sandbox.kind === "sandboxed";
|
|
13200
13326
|
const effectiveArgs = isSandboxed ? sandbox.args : args;
|
|
@@ -13209,6 +13335,7 @@ function wrapToolWithPermission(tool, deps) {
|
|
|
13209
13335
|
originalFn,
|
|
13210
13336
|
apiClient: deps.apiClient,
|
|
13211
13337
|
showPermissionPrompt,
|
|
13338
|
+
permissionManager,
|
|
13212
13339
|
agentContext: deps.agentContext,
|
|
13213
13340
|
configStore: deps.configStore,
|
|
13214
13341
|
sandboxOrchestrator: deps.sandboxOrchestrator,
|
|
@@ -14195,6 +14322,7 @@ var PermissionManager = class {
|
|
|
14195
14322
|
this.deniedTools = /* @__PURE__ */ new Set();
|
|
14196
14323
|
this._sandboxMode = "disabled";
|
|
14197
14324
|
this._sandboxActive = false;
|
|
14325
|
+
this.sandboxEscalationApprovals = /* @__PURE__ */ new Set();
|
|
14198
14326
|
this.trustedTools = new Set(trustedTools);
|
|
14199
14327
|
this.deniedTools = new Set(deniedTools);
|
|
14200
14328
|
}
|
|
@@ -14220,14 +14348,14 @@ var PermissionManager = class {
|
|
|
14220
14348
|
*
|
|
14221
14349
|
* @param toolName - The tool being checked
|
|
14222
14350
|
* @param options.isSandboxed - If true, the command will run inside the OS-level sandbox.
|
|
14223
|
-
* In auto-allow mode, sandboxed
|
|
14351
|
+
* In auto-allow mode, sandboxed commands (a core tool whose descriptor declares `sandbox`) skip the permission prompt
|
|
14224
14352
|
* because the sandbox provides the security boundary.
|
|
14225
14353
|
*/
|
|
14226
14354
|
needsPermission(toolName, options) {
|
|
14227
14355
|
const category = this.catalog.category(toolName);
|
|
14228
14356
|
if (this.deniedTools.has(toolName)) return true;
|
|
14229
14357
|
if (category === "auto_approve") return false;
|
|
14230
|
-
if (options?.isSandboxed && toolName
|
|
14358
|
+
if (options?.isSandboxed && sandboxedCommandArg(this.catalog.resolve(toolName)) !== void 0 && this.isSandboxAutoAllow()) return false;
|
|
14231
14359
|
if (this.sessionTrustedTools.has(toolName)) return false;
|
|
14232
14360
|
if (category === "prompt_always") return true;
|
|
14233
14361
|
return !this.trustedTools.has(toolName);
|
|
@@ -14300,11 +14428,19 @@ var PermissionManager = class {
|
|
|
14300
14428
|
isSessionTrusted(toolName) {
|
|
14301
14429
|
return this.sessionTrustedTools.has(toolName);
|
|
14302
14430
|
}
|
|
14431
|
+
/** Remember a session-scoped approval to run one command outside the sandbox. Never persisted. */
|
|
14432
|
+
approveSandboxEscalation(key) {
|
|
14433
|
+
this.sandboxEscalationApprovals.add(key);
|
|
14434
|
+
}
|
|
14435
|
+
isSandboxEscalationApproved(key) {
|
|
14436
|
+
return this.sandboxEscalationApprovals.has(key);
|
|
14437
|
+
}
|
|
14303
14438
|
/**
|
|
14304
14439
|
* Clear all session-scoped trust (called on exit or session reset)
|
|
14305
14440
|
*/
|
|
14306
14441
|
clearSessionTrust() {
|
|
14307
14442
|
this.sessionTrustedTools.clear();
|
|
14443
|
+
this.sandboxEscalationApprovals.clear();
|
|
14308
14444
|
}
|
|
14309
14445
|
/**
|
|
14310
14446
|
* Clear all trusted tools
|