@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.
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { $ as PermissionDeniedError, A as ApiKeyType, C as htmlFirstH1, Ct as obfuscateApiKey, Dt as signImageUrl, Et as secureParameters, G as LOCATION_MAP_LANGUAGE, Gt as WEBSITE_URL, It as withRetry, L as ChatModels, N as BedrockEmbeddingModel, Ot as splitEquals, S as htmlErrorTitle, Wt as APP_NAME, X as OllamaEmbeddingModel, Y as NotFoundError, Z as OpenAIEmbeddingModel, a as commandTextOf, b as SESSION_ID_PATTERN, bt as isRetryableError, c as writePathOf, ct as assertAuthOriginMatchesApiKey, gt as googleMapsSearchUrl, it as SupportedFabFileMimeTypes, mt as getMcpProviderMetadata, o as isReadOnlyTool, ot as VoyageAIEmbeddingModel, q as McpServerName, r as loadContextFiles, st as WORK_ITEM_STATUSES, tt as SEARCH_RESULT_CARDS_LANGUAGE, w as escapeArtifactBodyJson, x as isValidSessionId, y as logger, yt as isPlaceholderImageSigningSecret } from "./ConfigStore-v0wgf0_j.mjs";
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-5DDaX8tB.mjs";
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-D2pxGurU.mjs
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
- if (isPdf) result = await app.scrapeUrl(url, baseParams);
2746
- else try {
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 ((scrapeError instanceof Error ? scrapeError.message : "").includes("Actions are not supported")) {
2756
- Logger.globalInstance.log("📥 WebFetch Tool: Actions not supported, retrying without actions...");
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
- await context.statusUpdate({ promptMeta: { citables: [citable] } }, "WebFetch completed successfully");
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
- if (error instanceof FirecrawlError && [
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(error.statusCode)) {
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
- }[error.statusCode];
2906
+ }[firecrawlStatus];
2872
2907
  context.logger.warn("WebFetch expected Firecrawl failure", {
2873
2908
  url: params.url,
2874
- statusCode: error.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-Cak_rViw.mjs
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) => `${index + 1}. ${describePlace(place)}\n id: ${stripNewlines(place.id)}`),
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 citables = results.map((result, index) => ({
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) citables.push(placeCitable(place, imageUrlSigningSecret, canSignImages));
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 + 1}. **${stripNewlines(result.title)}**\n${stripNewlines(result.snippet)}\n` + imageLine + `Source: [${safeHostname(result.url)}](${result.url})\n`;
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-BE8RB3Ga.mjs
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
- getAbortSignal
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 commandName = filename.toLowerCase() === "skill.md" ? this.extractSkillName(filePath) : extractCommandName(filename);
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 command = parseCommandFile(await fs.readFile(filePath, "utf-8"), filePath, commandName, source);
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
- if (cached && this.isFresh(cached)) return this.mapAndFilter(cached.skills);
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 bash_execute call runs inside the OS sandbox, and rewrite its args to do so.
11431
- * Any other tool, or a call with no command or no orchestrator, is left unchanged.
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(toolName, args, sandboxOrchestrator, allowedDirectories) {
11434
- if (toolName !== "bash_execute" || !args?.command || !sandboxOrchestrator) return { kind: "unchanged" };
11435
- const command = args.command;
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
- command: decision.wrappedCommand.commandString,
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; every
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 caps the output), capture
13051
- * sandbox violations, offer retry on a sandbox failure or a path denial, then fire the
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
- let result;
13058
- try {
13059
- result = await executeWithFuzzyConfirmation(toolName, execArgs, apiClient, originalFn, showPermissionPrompt);
13060
- } catch (err) {
13061
- const msg = err instanceof Error ? err.message : String(err);
13062
- if (!isPathAccessDenial(msg)) throw err;
13063
- result = msg;
13064
- }
13065
- await captureViolations(isSandboxed, result, rawArgs?.command, input.sandboxOrchestrator);
13066
- result = await retrySandboxFailure(isSandboxed, result, toolName, rawArgs, apiClient, originalFn, showPermissionPrompt);
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(toolName, args, deps.sandboxOrchestrator, allowedDirectories);
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\` — also fine
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 filteredB4mTools = Object.fromEntries([
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
- ].filter((name) => name in cliSharedTools).map((name) => [name, cliSharedTools[name]]));
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 { allowedTools, deniedTools } = toolFilter;
14161
- const normalizedAllowed = allowedTools?.map(normalizeToolName);
14162
- const normalizedDenied = deniedTools?.map(normalizeToolName);
14163
- tools = tools.filter((tool) => {
14164
- const toolName = tool.toolSchema.name;
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 bash_execute commands skip the permission prompt
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 === "bash_execute" && this.isSandboxAutoAllow()) return false;
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