@bike4mind/cli 3.1.0 → 3.2.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.
@@ -250,6 +250,19 @@ const StorageLimitErrorInfoSchema = z$1.object({
250
250
  requested_bytes: z$1.number().nonnegative().describe("Size of the refused upload, in bytes."),
251
251
  owner: z$1.enum(["user", "org"]).describe("Whose limit was hit: `user` or `org`. Always `user` for `file_too_large`, a platform-wide cap.")
252
252
  });
253
+ /**
254
+ * The refusal a caller gets when the limit belongs to someone else (an update-share editor growing
255
+ * another user's file): the code and whose limit, without that owner's byte counts.
256
+ */
257
+ const RedactedStorageLimitErrorInfoSchema = StorageLimitErrorInfoSchema.pick({
258
+ errorCode: true,
259
+ owner: true
260
+ }).extend({
261
+ used_bytes: z$1.never().optional(),
262
+ limit_bytes: z$1.never().optional(),
263
+ requested_bytes: z$1.never().optional()
264
+ });
265
+ z$1.union([StorageLimitErrorInfoSchema, RedactedStorageLimitErrorInfoSchema]);
253
266
  function isZodError(err) {
254
267
  return Boolean(err && (err instanceof ZodError || err.name === "ZodError"));
255
268
  }
@@ -3643,6 +3656,199 @@ z$1.enum([
3643
3656
  "investigate_soon",
3644
3657
  "immediate_action"
3645
3658
  ]);
3659
+ const CompletionReasoningEffortSchema = z$1.enum([
3660
+ "none",
3661
+ "minimal",
3662
+ "low",
3663
+ "medium",
3664
+ "high",
3665
+ "xhigh"
3666
+ ]);
3667
+ /**
3668
+ * Tool schema matching ICompletionOptionTools.toolSchema. The Zod surface only
3669
+ * covers wire-format fields (toolFn is server-side). Replaces the historical
3670
+ * z.array(z.any()) so client-side bugs (typos in name, missing type) surface
3671
+ * at parse time instead of failing silently inside the backend.
3672
+ */
3673
+ const CompletionCustomToolSchema = z$1.object({
3674
+ toolSchema: z$1.object({
3675
+ name: z$1.string(),
3676
+ description: z$1.string(),
3677
+ parameters: z$1.object({
3678
+ type: z$1.literal("object"),
3679
+ properties: z$1.record(z$1.string(), z$1.any()).optional(),
3680
+ additionalProperties: z$1.boolean().optional(),
3681
+ required: z$1.array(z$1.string()).optional()
3682
+ }).passthrough(),
3683
+ strict: z$1.boolean().optional(),
3684
+ defer_loading: z$1.boolean().optional().describe("Keep this definition out of the model context until the tool search server tool (or a `tool_reference`) loads it. Honoured only on models served by the direct Anthropic API whose `supports_tool_search` is true; on every other model (including Bedrock-served Claude, for now) it is ignored and the tool is sent as usual. At least one tool, or a `serverTool` entry, must stay non-deferred (`errorCode: \"all_tools_deferred\"`)."),
3685
+ cache_control: z$1.unknown().optional().describe("Accepted for validation only and not forwarded. Rejected on a deferred tool (`errorCode: \"deferred_tool_cache_control\"`).")
3686
+ }),
3687
+ _isMcpTool: z$1.boolean().optional()
3688
+ });
3689
+ /** Anthropic's tool search server tools; `name` is fixed by `type`. */
3690
+ const ToolSearchServerToolSchema = z$1.discriminatedUnion("type", [z$1.object({
3691
+ type: z$1.literal("tool_search_tool_regex_20251119"),
3692
+ name: z$1.literal("tool_search_tool_regex")
3693
+ }), z$1.object({
3694
+ type: z$1.literal("tool_search_tool_bm25_20251119"),
3695
+ name: z$1.literal("tool_search_tool_bm25")
3696
+ })]);
3697
+ const CompletionServerToolSchema = z$1.object({ serverTool: ToolSearchServerToolSchema.describe("A provider-run tool, sent to the provider as `{ type, name }`. Only the Anthropic tool search tools exist today, and only on models served by the direct Anthropic API whose `supports_tool_search` is true; any other model, Bedrock-served Claude included, is refused with `errorCode: \"tool_search_unsupported\"` rather than run without it.") });
3698
+ /** One `tools[]` entry: a caller-defined tool, or a provider-run server tool. */
3699
+ const CompletionToolSchema = z$1.union([CompletionCustomToolSchema, CompletionServerToolSchema]);
3700
+ /**
3701
+ * The `errorCode`s of the completions 400s for tool search requests (CONVENTIONS.md section 1).
3702
+ * Assigned in services/cliCompletionsToolSearch.ts.
3703
+ */
3704
+ const COMPLETION_TOOL_ERROR_CODES = [
3705
+ "tool_search_unsupported",
3706
+ "all_tools_deferred",
3707
+ "deferred_tool_cache_control",
3708
+ "unknown_tool_reference"
3709
+ ];
3710
+ /**
3711
+ * JSON Schema definition for structured output. The `schema` field is a full
3712
+ * JSON Schema document (we don't try to validate JSON Schema with Zod - that's
3713
+ * the caller's contract with the model). `strict: true` is the default, matching
3714
+ * OpenAI's strict-mode subset semantics.
3715
+ */
3716
+ const ResponseFormatSchema = z$1.discriminatedUnion("type", [z$1.object({ type: z$1.literal("text") }), z$1.object({
3717
+ type: z$1.literal("json_schema"),
3718
+ json_schema: z$1.object({
3719
+ name: z$1.string(),
3720
+ description: z$1.string().optional(),
3721
+ schema: z$1.record(z$1.string(), z$1.any()),
3722
+ strict: z$1.boolean().optional().default(true)
3723
+ })
3724
+ })]);
3725
+ /**
3726
+ * Wire-format message for /api/ai/v1/completions. The optional `cache: true`
3727
+ * flag is honored by Anthropic (translated to `cache_control: { type: 'ephemeral' }`
3728
+ * with the appropriate beta header) and silently ignored by other providers.
3729
+ */
3730
+ const CompletionMessageSchema = z$1.object({
3731
+ role: z$1.enum([
3732
+ "user",
3733
+ "assistant",
3734
+ "system"
3735
+ ]),
3736
+ content: z$1.union([z$1.string(), z$1.array(z$1.any())]),
3737
+ cache: z$1.boolean().optional()
3738
+ });
3739
+ /**
3740
+ * Schema for CLI LLM completion requests
3741
+ * Shared between Next.js API route (dev) and Lambda function (production)
3742
+ *
3743
+ * `response_format`, `stream`, `tools`, `temperature`, and `max_tokens` are all
3744
+ * accepted at the top level (OpenAI-compatible, matching how every major LLM
3745
+ * SDK shapes a completion request) AND nested under `options` (legacy shape).
3746
+ * Use `normalizeCompletionRequest()` to collapse both surfaces into the
3747
+ * canonical `options.<field>` location before downstream consumption.
3748
+ */
3749
+ const CompletionRequestSchema = z$1.object({
3750
+ model: z$1.string().max(256),
3751
+ messages: z$1.array(CompletionMessageSchema),
3752
+ response_format: ResponseFormatSchema.optional(),
3753
+ stream: z$1.boolean().optional(),
3754
+ tools: z$1.array(CompletionToolSchema).optional(),
3755
+ temperature: z$1.number().optional(),
3756
+ max_tokens: z$1.number().optional(),
3757
+ options: z$1.object({
3758
+ temperature: z$1.number().optional(),
3759
+ maxTokens: z$1.number().optional(),
3760
+ stream: z$1.boolean().optional(),
3761
+ tools: z$1.array(CompletionToolSchema).optional(),
3762
+ response_format: ResponseFormatSchema.optional(),
3763
+ reasoningEffort: CompletionReasoningEffortSchema.optional().describe("Reasoning effort hint. Honoured by the OpenAI (reasoning models only, and not alongside tools on some of them), Kimi (K3 only), DeepSeek and Anthropic Claude (direct and Bedrock) adapters; each maps it onto its own scale. On Claude, `none` turns thinking off where the model allows it (otherwise it runs at the lowest effort), so short `max_tokens` replies stream visible text from the start; `minimal` maps to `low`. When `thinking.enabled` is also set on a Claude model, thinking stays on and this sets its effort instead, with `none` and `minimal` mapping to `low`. Claude models that reason inside `max_tokens` never have that ceiling raised, and on older Claude models any level above `none` turns thinking on (it is off by default there), forces temperature to 1 and drops top_p, and a thinking budget that cannot fit under `max_tokens` is skipped. On the direct Anthropic API, a json_schema `response_format` with no tools is served by a forced tool on models that support forced tool use, and that tool cannot run alongside thinking, so there only `none` is honoured; models served by native structured outputs honour every level. Silently ignored by every other adapter (Gemini, xAI, Ollama) and by non-reasoning models, so sending it there is a no-op."),
3764
+ thinking: z$1.object({
3765
+ enabled: z$1.boolean(),
3766
+ budget_tokens: z$1.number().int().min(1024).optional()
3767
+ }).optional().describe("Ask a model that can think to think on this turn. On Claude, `reasoningEffort` then sets how much it thinks, and `budget_tokens` (default 16000, minimum 1024) sizes thinking on older Claude models. Ignored by models that cannot think, and on the direct Anthropic API when a json_schema `response_format` is served by a forced tool, which cannot run alongside thinking.")
3768
+ }).optional()
3769
+ });
3770
+ const CompletionUsageSchema = z$1.object({
3771
+ inputTokens: z$1.number().optional(),
3772
+ outputTokens: z$1.number().optional(),
3773
+ reasoningTokens: z$1.number().optional().describe("Reasoning/thinking tokens the provider reported. A subset of outputTokens, not additive and not billed on top. Absent when the provider or adapter reports no count."),
3774
+ cacheReadInputTokens: z$1.number().optional(),
3775
+ cacheCreationInputTokens: z$1.number().optional()
3776
+ });
3777
+ const CompletionMetaEventSchema = z$1.object({
3778
+ type: z$1.literal("meta"),
3779
+ requestId: z$1.string()
3780
+ });
3781
+ const CompletionContentEventSchema = z$1.object({
3782
+ type: z$1.enum(["content", "tool_use"]),
3783
+ text: z$1.string(),
3784
+ tools: z$1.array(z$1.object({
3785
+ name: z$1.string(),
3786
+ arguments: z$1.string().optional(),
3787
+ id: z$1.string().optional()
3788
+ })).optional(),
3789
+ usage: CompletionUsageSchema.optional(),
3790
+ credits: z$1.object({
3791
+ used: z$1.number().optional(),
3792
+ usdCost: z$1.number().optional()
3793
+ }).optional(),
3794
+ responseFormatMode: z$1.enum([
3795
+ "native",
3796
+ "tool_use",
3797
+ "best-effort"
3798
+ ]).optional(),
3799
+ stopReason: z$1.string().optional(),
3800
+ thinking: z$1.array(z$1.any()).optional(),
3801
+ toolStarted: z$1.object({
3802
+ name: z$1.string(),
3803
+ id: z$1.string().optional()
3804
+ }).optional()
3805
+ });
3806
+ /**
3807
+ * Classifiers the completions `error` frame may carry: the persisted quest codes plus the two
3808
+ * provider codes. A separate tuple because QUEST_ERROR_CODES is stored on quests and must not widen.
3809
+ */
3810
+ const COMPLETION_SSE_ERROR_CODES = [
3811
+ ...QUEST_ERROR_CODES,
3812
+ "provider_error",
3813
+ "provider_rejected",
3814
+ "content_blocked"
3815
+ ];
3816
+ /**
3817
+ * The in-band terminal failure frame, sent only once the stream has opened. Failures found before
3818
+ * the provider call (auth, scope, validation, rate limit, model, credits) are JSON errors with a
3819
+ * real status instead; see completions.contract.ts.
3820
+ *
3821
+ * `code` is the machine-readable classifier: a billing code when settlement runs out mid-generation,
3822
+ * `provider_rejected` / `provider_error` when the model provider fails, `content_blocked` when the
3823
+ * model's safety classifier declined the request. Optional; callers must never match on the prose
3824
+ * `message`.
3825
+ *
3826
+ * The field is `code` rather than the conventions' `errorCode` because this frame shipped that way
3827
+ * and is a published wire shape.
3828
+ */
3829
+ const CompletionSseErrorEventSchema = z$1.object({
3830
+ type: z$1.literal("error"),
3831
+ message: z$1.string(),
3832
+ requestId: z$1.string().optional(),
3833
+ code: z$1.enum(COMPLETION_SSE_ERROR_CODES).optional()
3834
+ });
3835
+ /**
3836
+ * A provider content block the caller must echo back verbatim, in stream order, in the assistant
3837
+ * `messages` content of the next request: today the Anthropic tool search `server_tool_use` and
3838
+ * `tool_search_tool_result` blocks. Never executed by the caller and never answered with a tool_result.
3839
+ */
3840
+ const CompletionProviderBlockEventSchema = z$1.object({
3841
+ type: z$1.literal("provider_block"),
3842
+ provider: z$1.literal("anthropic"),
3843
+ block: z$1.record(z$1.string(), z$1.unknown())
3844
+ });
3845
+ /** One `data:` event in the `text/event-stream` completions response. */
3846
+ const CompletionStreamEventSchema = z$1.union([
3847
+ CompletionMetaEventSchema,
3848
+ CompletionContentEventSchema,
3849
+ CompletionProviderBlockEventSchema,
3850
+ CompletionSseErrorEventSchema
3851
+ ]);
3646
3852
  /** Splits on the marker tokens, keeping them in the result so a scan can track nesting depth. */
3647
3853
  const THINK_TAG_TOKENS = /(<think>|<\/think>)/g;
3648
3854
  /**
@@ -3780,8 +3986,8 @@ const invalid = (reason) => ({
3780
3986
  status: "invalid",
3781
3987
  reason
3782
3988
  });
3783
- const FENCE_OPEN_LINE = /^[ \t]*(`{3,}|~{3,})(.*)$/;
3784
- const FENCE_CLOSE_LINE = /^[ \t]*(`{3,}|~{3,})[ \t]*$/;
3989
+ const FENCE_OPEN_LINE$1 = /^[ \t]*(`{3,}|~{3,})(.*)$/;
3990
+ const FENCE_CLOSE_LINE$1 = /^[ \t]*(`{3,}|~{3,})[ \t]*$/;
3785
3991
  /**
3786
3992
  * Offset where a slot's visible answer starts: just past its last top-level `</think>`, 0 when it
3787
3993
  * has none, or null while a `<think>` is still open. Depth-tracked to match visibleReplyText in
@@ -3811,7 +4017,7 @@ function findLastChoicesFence(text) {
3811
4017
  const lineEnd = newline === -1 ? text.length : newline;
3812
4018
  const line = text.slice(lineStart, lineEnd).replace(/\r$/, "");
3813
4019
  if (!open) {
3814
- const match = line.match(FENCE_OPEN_LINE);
4020
+ const match = line.match(FENCE_OPEN_LINE$1);
3815
4021
  if (match && !(match[1][0] === "`" && match[2].includes("`"))) open = {
3816
4022
  start: lineStart,
3817
4023
  bodyStart: newline === -1 ? text.length : newline + 1,
@@ -3819,7 +4025,7 @@ function findLastChoicesFence(text) {
3819
4025
  isChoices: match[1] === "```" && match[2].trim().toLowerCase() === "choices"
3820
4026
  };
3821
4027
  } else {
3822
- const match = line.match(FENCE_CLOSE_LINE);
4028
+ const match = line.match(FENCE_CLOSE_LINE$1);
3823
4029
  if (match && match[1][0] === open.marker[0] && match[1].length >= open.marker.length) {
3824
4030
  if (open.isChoices) last = {
3825
4031
  start: open.start,
@@ -4826,6 +5032,13 @@ z$1.object({
4826
5032
  reason: z$1.enum(REPLY_CHOICES_INVALID_REASONS).optional()
4827
5033
  }).optional(),
4828
5034
  /**
5035
+ * Set when finalize stripped a tool call the model wrote into its reply as text instead of
5036
+ * calling it (see ../utils/narratedToolCalls.ts). The call never ran; `toolNames` are the
5037
+ * offered tools it named. Absent when nothing was stripped. Consumers read this rather than
5038
+ * matching the reply text, which no longer carries the call.
5039
+ */
5040
+ narratedToolCall: z$1.object({ toolNames: z$1.array(z$1.string()) }).optional(),
5041
+ /**
4829
5042
  * Set when an emitted artifact looks voluntarily abbreviated - placeholder comments in
4830
5043
  * place of real code, or calls into functions that were never defined. The complement to
4831
5044
  * `finishReason === 'max_tokens'`: truncation is a hard stop the provider reports, elision
@@ -5064,8 +5277,11 @@ const ChatWaitTimeoutErrorSchema = ApiErrorSchema.extend({
5064
5277
  const StorageLimitErrorSchema = ApiErrorSchema.extend(StorageLimitErrorInfoSchema.partial().shape);
5065
5278
  /** 503 when this deployment has no usable key for a provider the request needs (data-lake search, voice). */
5066
5279
  const ProviderNotConfiguredErrorSchema = ApiErrorSchema.extend({ errorCode: z$1.literal("provider_not_configured").optional() });
5067
- /** 400 for a `model` the caller cannot use; `errorCode` is absent on the other 400s (e.g. malformed JSON). */
5068
- const UnknownModelErrorSchema = ApiErrorSchema.extend({ errorCode: z$1.literal(UNKNOWN_MODEL_ERROR_CODE).optional() });
5280
+ /**
5281
+ * 400 for a `model` the caller cannot use, or a refused tool search request; `errorCode` is absent on the
5282
+ * other 400s (e.g. malformed JSON).
5283
+ */
5284
+ const CompletionsBadRequestErrorSchema = ApiErrorSchema.extend({ errorCode: z$1.enum([UNKNOWN_MODEL_ERROR_CODE, ...COMPLETION_TOOL_ERROR_CODES]).optional() });
5069
5285
  /**
5070
5286
  * Error envelope for the 403 apiKeyAuth (apps/client/server/middlewares/apiKeyAuth.ts)
5071
5287
  * throws when an API key lacks a route's scopes. It names what the ROUTE requires,
@@ -5989,34 +6205,95 @@ function parseCidr(raw) {
5989
6205
  prefix: Number(value.slice(slash + 1))
5990
6206
  };
5991
6207
  }
5992
- function cidrContains(cidr, address) {
5993
- const target = addressBytes(address);
5994
- if (!target || target.version !== cidr.version) return false;
5995
- for (let bit = 0; bit < cidr.prefix; bit++) {
6208
+ /**
6209
+ * Networks a private-network opt-in never reaches, whatever the operator lists, self-host included.
6210
+ * One list for both sides: the settings validator refuses any range that overlaps an entry, and the
6211
+ * fetch path (`buildNeverAdmitted` in fab-pipeline's pinnedFetch.ts) refuses these at connect time.
6212
+ * IPv4 entries also cover their IPv4-mapped IPv6 forms (`::ffff:0:0/96`) on both sides.
6213
+ */
6214
+ const NEVER_ADMITTED_NETWORKS = [
6215
+ {
6216
+ cidr: "0.0.0.0/8",
6217
+ label: "this network"
6218
+ },
6219
+ {
6220
+ cidr: "127.0.0.0/8",
6221
+ label: "loopback"
6222
+ },
6223
+ {
6224
+ cidr: "169.254.0.0/16",
6225
+ label: "link-local, including cloud metadata"
6226
+ },
6227
+ {
6228
+ cidr: "100.100.100.200/32",
6229
+ label: "cloud metadata"
6230
+ },
6231
+ {
6232
+ cidr: "fd00:ec2::254/128",
6233
+ label: "cloud metadata"
6234
+ },
6235
+ {
6236
+ cidr: "64:ff9b::a9fe:a9fe/128",
6237
+ label: "cloud metadata"
6238
+ },
6239
+ {
6240
+ cidr: "::/128",
6241
+ label: "unspecified"
6242
+ },
6243
+ {
6244
+ cidr: "::1/128",
6245
+ label: "loopback"
6246
+ },
6247
+ {
6248
+ cidr: "fe80::/10",
6249
+ label: "link-local"
6250
+ }
6251
+ ];
6252
+ const IPV4_MAPPED_PREFIX = [
6253
+ 0,
6254
+ 0,
6255
+ 0,
6256
+ 0,
6257
+ 0,
6258
+ 0,
6259
+ 0,
6260
+ 0,
6261
+ 0,
6262
+ 0,
6263
+ 255,
6264
+ 255
6265
+ ];
6266
+ /** An IPv4 CIDR in its IPv4-mapped IPv6 form; other CIDRs unchanged. */
6267
+ function asIpv6(cidr) {
6268
+ if (cidr.version === 6) return cidr;
6269
+ return {
6270
+ version: 6,
6271
+ bytes: [...IPV4_MAPPED_PREFIX, ...cidr.bytes],
6272
+ prefix: cidr.prefix + 96
6273
+ };
6274
+ }
6275
+ function cidrsOverlap(a, b) {
6276
+ const [left, right] = a.version === b.version ? [a, b] : [asIpv6(a), asIpv6(b)];
6277
+ const prefix = Math.min(left.prefix, right.prefix);
6278
+ for (let bit = 0; bit < prefix; bit++) {
5996
6279
  const byte = bit >> 3;
5997
6280
  const mask = 128 >> (bit & 7);
5998
- if (((cidr.bytes[byte] ?? 0) & mask) !== ((target.bytes[byte] ?? 0) & mask)) return false;
6281
+ if (((left.bytes[byte] ?? 0) & mask) !== ((right.bytes[byte] ?? 0) & mask)) return false;
5999
6282
  }
6000
6283
  return true;
6001
6284
  }
6002
- /**
6003
- * Cloud instance-metadata endpoints, plus the IPv4 one in its IPv4-mapped and NAT64 IPv6 forms.
6004
- * Never reachable through `customMcpAllowPrivateNetworks`, self-host included.
6005
- */
6006
- const CLOUD_METADATA_ADDRESSES = [
6007
- "169.254.169.254",
6008
- "fd00:ec2::254",
6009
- "::ffff:169.254.169.254",
6010
- "64:ff9b::169.254.169.254"
6011
- ];
6285
+ const NEVER_ADMITTED_PARSED = NEVER_ADMITTED_NETWORKS.map((entry) => ({
6286
+ ...entry,
6287
+ parsed: parseCidr(entry.cidr)
6288
+ }));
6012
6289
  /** Why `raw` may not be allowed as a private network, or null when it may. */
6013
6290
  function customMcpPrivateCidrProblem(raw) {
6014
6291
  const cidr = parseCidr(raw);
6015
6292
  if (!cidr) return "Must be a CIDR such as 10.0.0.0/8 or fd12:3456::/32";
6016
6293
  const minPrefix = CUSTOM_MCP_MIN_CIDR_PREFIX[cidr.version];
6017
6294
  if (cidr.prefix < minPrefix) return `IPv${cidr.version} ranges must be /${minPrefix} or narrower`;
6018
- const metadata = CLOUD_METADATA_ADDRESSES.find((address) => cidrContains(cidr, address));
6019
- if (metadata) return `Range covers the cloud metadata address ${metadata}`;
6295
+ const blocked = NEVER_ADMITTED_PARSED.find((entry) => cidrsOverlap(cidr, entry.parsed));
6296
+ if (blocked) return `Range overlaps ${blocked.cidr} (${blocked.label}), which is never reachable`;
6020
6297
  return null;
6021
6298
  }
6022
6299
  const CustomMcpPrivateCidrSchema = z$1.string().trim().toLowerCase().superRefine((value, ctx) => {
@@ -7020,8 +7297,8 @@ const CustomMcpPolicySchema = z$1.object({
7020
7297
  const CustomMcpBlockedHostsSchema = z$1.object({ hosts: McpHostPatternListSchema.default([]) });
7021
7298
  /**
7022
7299
  * Self-host only; hosted deployments never reach private networks regardless of this value. Each
7023
- * CIDR is strictly parsed, at least /8 (IPv4) or /16 (IPv6), and never covers a cloud metadata
7024
- * address (see customMcpHosts.ts).
7300
+ * CIDR is strictly parsed, at least /8 (IPv4) or /16 (IPv6), and never overlaps a network the fetch
7301
+ * path refuses anyway (NEVER_ADMITTED_NETWORKS in customMcpHosts.ts).
7025
7302
  */
7026
7303
  const CustomMcpAllowPrivateNetworksSchema = z$1.object({
7027
7304
  cidrs: z$1.array(CustomMcpPrivateCidrSchema).max(100).default([]),
@@ -11733,16 +12010,8 @@ const CliCompletionRequestAction = z$1.object({
11733
12010
  temperature: z$1.number().optional(),
11734
12011
  maxTokens: z$1.number().optional(),
11735
12012
  stream: z$1.boolean().optional(),
11736
- tools: z$1.array(z$1.unknown()).optional(),
11737
- response_format: z$1.discriminatedUnion("type", [z$1.object({ type: z$1.literal("text") }), z$1.object({
11738
- type: z$1.literal("json_schema"),
11739
- json_schema: z$1.object({
11740
- name: z$1.string(),
11741
- description: z$1.string().optional(),
11742
- schema: z$1.record(z$1.string(), z$1.any()),
11743
- strict: z$1.boolean().optional().default(true)
11744
- })
11745
- })]).optional()
12013
+ tools: z$1.array(CompletionToolSchema).optional(),
12014
+ response_format: ResponseFormatSchema.optional()
11746
12015
  }).optional()
11747
12016
  });
11748
12017
  /**
@@ -12231,7 +12500,7 @@ const VoiceCreditsExhaustedAction = z$1.object({
12231
12500
  const CliCompletionChunkAction = z$1.object({
12232
12501
  action: z$1.literal("cli_completion_chunk"),
12233
12502
  requestId: z$1.string(),
12234
- chunk: z$1.object({
12503
+ chunk: z$1.union([z$1.object({
12235
12504
  type: z$1.enum(["content", "tool_use"]),
12236
12505
  text: z$1.string(),
12237
12506
  toolStarted: z$1.object({
@@ -12244,7 +12513,7 @@ const CliCompletionChunkAction = z$1.object({
12244
12513
  outputTokens: z$1.number().optional()
12245
12514
  }).optional(),
12246
12515
  thinking: z$1.array(z$1.unknown()).optional()
12247
- })
12516
+ }), CompletionProviderBlockEventSchema])
12248
12517
  });
12249
12518
  /**
12250
12519
  * CLI Completion stream finished successfully
@@ -12261,7 +12530,9 @@ const CliCompletionErrorAction = z$1.object({
12261
12530
  requestId: z$1.string(),
12262
12531
  error: z$1.string(),
12263
12532
  /** Seconds until the caller's rate-limit window reopens; present only on a rate-limit refusal. */
12264
- retryAfter: z$1.number().optional()
12533
+ retryAfter: z$1.number().optional(),
12534
+ /** Machine-readable reason on a request the caller must fix (a 4xx), e.g. `tool_search_unsupported`. */
12535
+ errorCode: z$1.string().optional()
12265
12536
  });
12266
12537
  /**
12267
12538
  * CLI Tool execution response over WebSocket
@@ -12912,7 +13183,12 @@ const AgentProgressAction = z$1.object({
12912
13183
  generationJobs: z$1.array(z$1.object({
12913
13184
  id: z$1.string(),
12914
13185
  kind: z$1.enum(GENERATION_JOB_KINDS)
12915
- })).optional()
13186
+ })).optional(),
13187
+ pendingAction: z$1.object({
13188
+ tool: z$1.string(),
13189
+ params: z$1.record(z$1.string(), z$1.unknown()),
13190
+ ts: z$1.number()
13191
+ }).optional()
12916
13192
  });
12917
13193
  const AgentCompletedAction = z$1.object({
12918
13194
  action: z$1.literal("completed"),
@@ -13376,165 +13652,6 @@ const AgentExecutionStatusResponseSchema = z$1.object({
13376
13652
  });
13377
13653
  /** Path parameter for `GET /api/v1/agent-executions/{id}`. */
13378
13654
  const AgentExecutionIdParamSchema = z$1.object({ id: z$1.string().min(1) });
13379
- const CompletionReasoningEffortSchema = z$1.enum([
13380
- "none",
13381
- "minimal",
13382
- "low",
13383
- "medium",
13384
- "high",
13385
- "xhigh"
13386
- ]);
13387
- /**
13388
- * Tool schema matching ICompletionOptionTools.toolSchema. The Zod surface only
13389
- * covers wire-format fields (toolFn is server-side). Replaces the historical
13390
- * z.array(z.any()) so client-side bugs (typos in name, missing type) surface
13391
- * at parse time instead of failing silently inside the backend.
13392
- */
13393
- const CompletionToolSchema = z$1.object({
13394
- toolSchema: z$1.object({
13395
- name: z$1.string(),
13396
- description: z$1.string(),
13397
- parameters: z$1.object({
13398
- type: z$1.literal("object"),
13399
- properties: z$1.record(z$1.string(), z$1.any()).optional(),
13400
- additionalProperties: z$1.boolean().optional(),
13401
- required: z$1.array(z$1.string()).optional()
13402
- }).passthrough(),
13403
- strict: z$1.boolean().optional()
13404
- }),
13405
- _isMcpTool: z$1.boolean().optional()
13406
- });
13407
- /**
13408
- * JSON Schema definition for structured output. The `schema` field is a full
13409
- * JSON Schema document (we don't try to validate JSON Schema with Zod - that's
13410
- * the caller's contract with the model). `strict: true` is the default, matching
13411
- * OpenAI's strict-mode subset semantics.
13412
- */
13413
- const ResponseFormatSchema = z$1.discriminatedUnion("type", [z$1.object({ type: z$1.literal("text") }), z$1.object({
13414
- type: z$1.literal("json_schema"),
13415
- json_schema: z$1.object({
13416
- name: z$1.string(),
13417
- description: z$1.string().optional(),
13418
- schema: z$1.record(z$1.string(), z$1.any()),
13419
- strict: z$1.boolean().optional().default(true)
13420
- })
13421
- })]);
13422
- /**
13423
- * Wire-format message for /api/ai/v1/completions. The optional `cache: true`
13424
- * flag is honored by Anthropic (translated to `cache_control: { type: 'ephemeral' }`
13425
- * with the appropriate beta header) and silently ignored by other providers.
13426
- */
13427
- const CompletionMessageSchema = z$1.object({
13428
- role: z$1.enum([
13429
- "user",
13430
- "assistant",
13431
- "system"
13432
- ]),
13433
- content: z$1.union([z$1.string(), z$1.array(z$1.any())]),
13434
- cache: z$1.boolean().optional()
13435
- });
13436
- /**
13437
- * Schema for CLI LLM completion requests
13438
- * Shared between Next.js API route (dev) and Lambda function (production)
13439
- *
13440
- * `response_format`, `stream`, `tools`, `temperature`, and `max_tokens` are all
13441
- * accepted at the top level (OpenAI-compatible, matching how every major LLM
13442
- * SDK shapes a completion request) AND nested under `options` (legacy shape).
13443
- * Use `normalizeCompletionRequest()` to collapse both surfaces into the
13444
- * canonical `options.<field>` location before downstream consumption.
13445
- */
13446
- const CompletionRequestSchema = z$1.object({
13447
- model: z$1.string().max(256),
13448
- messages: z$1.array(CompletionMessageSchema),
13449
- response_format: ResponseFormatSchema.optional(),
13450
- stream: z$1.boolean().optional(),
13451
- tools: z$1.array(CompletionToolSchema).optional(),
13452
- temperature: z$1.number().optional(),
13453
- max_tokens: z$1.number().optional(),
13454
- options: z$1.object({
13455
- temperature: z$1.number().optional(),
13456
- maxTokens: z$1.number().optional(),
13457
- stream: z$1.boolean().optional(),
13458
- tools: z$1.array(CompletionToolSchema).optional(),
13459
- response_format: ResponseFormatSchema.optional(),
13460
- reasoningEffort: CompletionReasoningEffortSchema.optional().describe("Reasoning effort hint. Honoured by the OpenAI (reasoning models only, and not alongside tools on some of them), Kimi (K3 only), DeepSeek and Anthropic Claude (direct and Bedrock) adapters; each maps it onto its own scale. On Claude, `none` turns thinking off where the model allows it (otherwise it runs at the lowest effort), so short `max_tokens` replies stream visible text from the start; `minimal` maps to `low`. When `thinking.enabled` is also set on a Claude model, thinking stays on and this sets its effort instead, with `none` and `minimal` mapping to `low`. Claude models that reason inside `max_tokens` never have that ceiling raised, and on older Claude models any level above `none` turns thinking on (it is off by default there), forces temperature to 1 and drops top_p, and a thinking budget that cannot fit under `max_tokens` is skipped. On the direct Anthropic API, a json_schema `response_format` with no tools is served by a forced tool on models that support forced tool use, and that tool cannot run alongside thinking, so there only `none` is honoured; models served by native structured outputs honour every level. Silently ignored by every other adapter (Gemini, xAI, Ollama) and by non-reasoning models, so sending it there is a no-op."),
13461
- thinking: z$1.object({
13462
- enabled: z$1.boolean(),
13463
- budget_tokens: z$1.number().int().min(1024).optional()
13464
- }).optional().describe("Ask a model that can think to think on this turn. On Claude, `reasoningEffort` then sets how much it thinks, and `budget_tokens` (default 16000, minimum 1024) sizes thinking on older Claude models. Ignored by models that cannot think, and on the direct Anthropic API when a json_schema `response_format` is served by a forced tool, which cannot run alongside thinking.")
13465
- }).optional()
13466
- });
13467
- const CompletionUsageSchema = z$1.object({
13468
- inputTokens: z$1.number().optional(),
13469
- outputTokens: z$1.number().optional(),
13470
- reasoningTokens: z$1.number().optional().describe("Reasoning/thinking tokens the provider reported. A subset of outputTokens, not additive and not billed on top. Absent when the provider or adapter reports no count."),
13471
- cacheReadInputTokens: z$1.number().optional(),
13472
- cacheCreationInputTokens: z$1.number().optional()
13473
- });
13474
- const CompletionMetaEventSchema = z$1.object({
13475
- type: z$1.literal("meta"),
13476
- requestId: z$1.string()
13477
- });
13478
- const CompletionContentEventSchema = z$1.object({
13479
- type: z$1.enum(["content", "tool_use"]),
13480
- text: z$1.string(),
13481
- tools: z$1.array(z$1.object({
13482
- name: z$1.string(),
13483
- arguments: z$1.string().optional(),
13484
- id: z$1.string().optional()
13485
- })).optional(),
13486
- usage: CompletionUsageSchema.optional(),
13487
- credits: z$1.object({
13488
- used: z$1.number().optional(),
13489
- usdCost: z$1.number().optional()
13490
- }).optional(),
13491
- responseFormatMode: z$1.enum([
13492
- "native",
13493
- "tool_use",
13494
- "best-effort"
13495
- ]).optional(),
13496
- stopReason: z$1.string().optional(),
13497
- thinking: z$1.array(z$1.any()).optional(),
13498
- toolStarted: z$1.object({
13499
- name: z$1.string(),
13500
- id: z$1.string().optional()
13501
- }).optional()
13502
- });
13503
- /**
13504
- * Classifiers the completions `error` frame may carry: the persisted quest codes plus the two
13505
- * provider codes. A separate tuple because QUEST_ERROR_CODES is stored on quests and must not widen.
13506
- */
13507
- const COMPLETION_SSE_ERROR_CODES = [
13508
- ...QUEST_ERROR_CODES,
13509
- "provider_error",
13510
- "provider_rejected",
13511
- "content_blocked"
13512
- ];
13513
- /**
13514
- * The in-band terminal failure frame, sent only once the stream has opened. Failures found before
13515
- * the provider call (auth, scope, validation, rate limit, model, credits) are JSON errors with a
13516
- * real status instead; see completions.contract.ts.
13517
- *
13518
- * `code` is the machine-readable classifier: a billing code when settlement runs out mid-generation,
13519
- * `provider_rejected` / `provider_error` when the model provider fails, `content_blocked` when the
13520
- * model's safety classifier declined the request. Optional; callers must never match on the prose
13521
- * `message`.
13522
- *
13523
- * The field is `code` rather than the conventions' `errorCode` because this frame shipped that way
13524
- * and is a published wire shape.
13525
- */
13526
- const CompletionSseErrorEventSchema = z$1.object({
13527
- type: z$1.literal("error"),
13528
- message: z$1.string(),
13529
- requestId: z$1.string().optional(),
13530
- code: z$1.enum(COMPLETION_SSE_ERROR_CODES).optional()
13531
- });
13532
- /** One `data:` event in the `text/event-stream` completions response. */
13533
- const CompletionStreamEventSchema = z$1.union([
13534
- CompletionMetaEventSchema,
13535
- CompletionContentEventSchema,
13536
- CompletionSseErrorEventSchema
13537
- ]);
13538
13655
  /**
13539
13656
  * Server-side tool execution schemas for POST /api/ai/v1/tools.
13540
13657
  *
@@ -14943,6 +15060,10 @@ const questMasterPlanResponseSchema = z$2.object(passthroughShape({
14943
15060
  decisions: true
14944
15061
  }));
14945
15062
  z$2.array(questMasterPlanResponseSchema);
15063
+ z$2.object({
15064
+ success: z$2.literal(true),
15065
+ plan: questMasterPlanResponseSchema
15066
+ }).extend({ metrics: passthroughField });
14946
15067
  z$1.preprocess((val) => {
14947
15068
  if (typeof val === "string") {
14948
15069
  if (val.toLowerCase() === "true") return true;
@@ -16249,8 +16370,8 @@ const ListModelsResponseSchema = paginatedResponseSchema(z$1.object({
16249
16370
  reasons_within_output_budget: z$1.boolean().describe("Whether reasoning tokens are spent inside `max_output_tokens` on every turn. When true, a small `max_tokens` can be used up by reasoning alone and return an empty reply, so leave room above the answer you expect."),
16250
16371
  reasoning_efforts: z$1.array(CompletionReasoningEffortSchema).describe("The `reasoningEffort` levels this model acts on, lowest first; empty when it ignores the option. `none` is listed only where it turns reasoning off. Each level is mapped to the nearest one the provider offers. A listed level can still be dropped on one request: some OpenAI models drop it on tool turns, and Claude models with a fixed thinking budget drop it when `max_tokens` leaves no room for the budget plus the answer."),
16251
16372
  supports_tools: z$1.boolean(),
16252
- supports_tool_search: z$1.boolean().describe("Whether the model accepts the tool search server tool and `defer_loading: true` on tool definitions, so a large tool catalog is searched on demand instead of loaded into context up front. Describes the model only: `/api/ai/v1/completions` does not accept these yet."),
16253
- supports_tool_changes: z$1.boolean().describe("Whether a system-role message can carry `tool_addition` and `tool_removal` blocks, which add or remove a declared tool mid-conversation. Describes the model only: `/api/ai/v1/completions` does not accept these blocks yet."),
16373
+ supports_tool_search: z$1.boolean().describe("Whether the model accepts the tool search server tool and `defer_loading: true` on tool definitions, so a large tool catalog is searched on demand instead of loaded into context up front. Describes the model: `/api/ai/v1/completions` honours it only for models whose `backend` is `anthropic`. A `bedrock` model with this flag set is not served with tool search there yet: a `serverTool` entry is refused with `errorCode: \"tool_search_unsupported\"` and `defer_loading` is ignored."),
16374
+ supports_tool_changes: z$1.boolean().describe("Whether a system-role message can carry `tool_addition` and `tool_removal` blocks, which add or remove a declared tool mid-conversation. Describes the model only: `/api/ai/v1/completions` does not accept these blocks yet, and will accept them first for models whose `backend` is `anthropic`."),
16254
16375
  supports_vision: z$1.boolean(),
16255
16376
  deprecation_date: z$1.string().nullable().describe("`YYYY-MM-DD`; null when no retirement is scheduled."),
16256
16377
  replaced_by: z$1.string().nullable().describe("The id of the recommended successor, when there is one."),
@@ -16644,7 +16765,12 @@ z$1.object({
16644
16765
  * Session-creation input, not a completion parameter, so it is applied only when `sessionId`
16645
16766
  * is absent; ignored when an existing session is resolved.
16646
16767
  */
16647
- agentIds: z$1.array(z$1.string()).optional()
16768
+ agentIds: z$1.array(z$1.string()).optional(),
16769
+ /**
16770
+ * The subset of `messageFileIds` the user scoped to this message only. Every other notebook-scope
16771
+ * message attachment is pinned to the session so it stays in context on later turns.
16772
+ */
16773
+ messageOnlyFileIds: z$1.array(z$1.string()).optional()
16648
16774
  });
16649
16775
  /**
16650
16776
  * Response shapes for the public image endpoints (`generateImage` / `editImage`,
@@ -17311,7 +17437,7 @@ defineEndpoint({
17311
17437
  path: "/api/ai/v1/completions",
17312
17438
  operationId: "createCompletion",
17313
17439
  summary: "Create a chat completion",
17314
- description: "OpenAI-compatible completion. A successful request is ALWAYS answered with an SSE stream (`text/event-stream`), regardless of the `stream` flag: a `meta` event, then `content`/`tool_use` events carrying `usage`/`credits`, terminated by `data: [DONE]`. The terminal event carries `stopReason`; treat `max_tokens` as a TRUNCATED reply rather than a complete one, and note that omitting `max_tokens` on the request lets the server size the output ceiling for the model (recommended for reasoning models, which spend thinking tokens inside that ceiling). A message `content` may be a string or an array of parts; image parts are accepted in OpenAI Chat (`image_url`), OpenAI Responses (`input_image`) or Anthropic (`image` with a `source`) form and are translated to whatever the target model speaks. Authenticate with an API key (`b4m_live_`) or a JWT.\n\nERRORS. Everything checked before the model provider is called fails with a JSON error body and a real HTTP status, before any stream opens: a malformed or otherwise invalid request or an unknown `model` `400` (`errorCode: \"unknown_model\"` for the latter), a bad credential `401`, a key without a required scope `403` (with `required_scopes`), a body over 25 MB `413`, a body that fails schema validation `422`, insufficient credits `422` with `errorCode: \"insufficient_credits\"` (personal or org balance, or an org per-member credit cap), a rate limit `429` with `Retry-After`, and a catalogued model whose provider is not configured on this deployment `503` with `errorCode: \"provider_not_configured\"` (retryable). Once the stream has opened the status stays `200` and a failure arrives as one terminal in-band `error` event. Branch on that event's `code`, never on `message`, which is prose and may change: `insufficient_credits` (buy credits) when credits run out mid-generation, `provider_rejected` when the model provider refused our credentials, `content_blocked` when the model declined the request (do not retry it unchanged), and `provider_error` for any other provider failure. Treat `code` as optional.",
17440
+ description: "OpenAI-compatible completion. A successful request is ALWAYS answered with an SSE stream (`text/event-stream`), regardless of the `stream` flag: a `meta` event, then `content`/`tool_use` events carrying `usage`/`credits`, terminated by `data: [DONE]`. The terminal event carries `stopReason`; treat `max_tokens` as a TRUNCATED reply rather than a complete one, and note that omitting `max_tokens` on the request lets the server size the output ceiling for the model (recommended for reasoning models, which spend thinking tokens inside that ceiling). A message `content` may be a string or an array of parts; image parts are accepted in OpenAI Chat (`image_url`), OpenAI Responses (`input_image`) or Anthropic (`image` with a `source`) form and are translated to whatever the target model speaks. Authenticate with an API key (`b4m_live_`) or a JWT.\n\nTOOL SEARCH. On a model whose `GET /api/v1/models` row has `supports_tool_search: true` and `backend: \"anthropic\"`, a `tools` entry may set `toolSchema.defer_loading: true`, and a `{ \"serverTool\": { \"type\": \"tool_search_tool_regex_20251119\", \"name\": \"tool_search_tool_regex\" } }` (or the `bm25` pair) entry adds Anthropic's tool search, so deferred definitions load only when the model searches for them. The stream then carries `provider_block` events (`{ type, provider: \"anthropic\", block }`) for the server-run search; echo each block back verbatim, in order, in the assistant `messages` content of the next request, and never answer one with a `tool_result`. When the server-run search loop hits its iteration limit the stream ends with `stopReason: \"pause_turn\"`; continue by re-sending the conversation with that assistant turn's provider blocks echoed. A client-side search may return `{ \"type\": \"tool_reference\", \"tool_name\": \"...\" }` blocks inside a `tool_result`'s content. On every other model, Bedrock-served Claude included, `defer_loading` is ignored (every tool is sent as usual) and a server tool, a `tool_reference` or an echoed tool search block is refused with a `400` rather than run without it.\n\nERRORS. Everything checked before the model provider is called fails with a JSON error body and a real HTTP status, before any stream opens: a malformed or otherwise invalid request or an unknown `model` `400` (`errorCode: \"unknown_model\"` for the latter), a bad credential `401`, a key without a required scope `403` (with `required_scopes`), a body over 25 MB `413`, a body that fails schema validation `422`, insufficient credits `422` with `errorCode: \"insufficient_credits\"` (personal or org balance, or an org per-member credit cap), a rate limit `429` with `Retry-After`, and a catalogued model whose provider is not configured on this deployment `503` with `errorCode: \"provider_not_configured\"` (retryable). Once the stream has opened the status stays `200` and a failure arrives as one terminal in-band `error` event. Branch on that event's `code`, never on `message`, which is prose and may change: `insufficient_credits` (buy credits) when credits run out mid-generation, `provider_rejected` when the model provider refused our credentials, `content_blocked` when the model declined the request (do not retry it unchanged), and `provider_error` for any other provider failure. Treat `code` as optional.",
17315
17441
  tags: ["AI"],
17316
17442
  auth: "apiKeyOrJwt",
17317
17443
  scopes: ["ai:chat", "ai:generate"],
@@ -17345,8 +17471,8 @@ defineEndpoint({
17345
17471
  }
17346
17472
  },
17347
17473
  400: {
17348
- description: "Malformed or otherwise invalid request (no `errorCode`), or a `model` the caller cannot use (`errorCode: \"unknown_model\"`): not in the catalog or not available to the caller. List valid ids with `GET /api/v1/models`.",
17349
- schema: UnknownModelErrorSchema
17474
+ description: "Malformed or otherwise invalid request (no `errorCode`), or a `model` the caller cannot use (`errorCode: \"unknown_model\"`): not in the catalog or not available to the caller. List valid ids with `GET /api/v1/models`. Tool search requests are refused with: `tool_search_unsupported` (a server tool, `tool_reference` or echoed tool search block on a model that cannot serve tool search here), `all_tools_deferred` (every tool deferred and no server tool), `deferred_tool_cache_control` (`cache_control` on a deferred tool), `unknown_tool_reference` (a `tool_reference` naming a tool not in `tools`, or carrying no string `tool_name`).",
17475
+ schema: CompletionsBadRequestErrorSchema
17350
17476
  },
17351
17477
  413: {
17352
17478
  description: "Request body over 25 MB.",
@@ -21178,7 +21304,7 @@ function scanArtifactOpenTag(text, at, leading, memo) {
21178
21304
  end: close + 1
21179
21305
  };
21180
21306
  }
21181
- const OPENER = /<artifact(?=\s)/gi;
21307
+ const OPENER$1 = /<artifact(?=\s)/gi;
21182
21308
  const LEADING_WS = /\s+/y;
21183
21309
  const CLOSER = /<\/artifact>/gi;
21184
21310
  const GT = />/g;
@@ -21193,9 +21319,9 @@ function scanArtifactTags(text, attrsMayCrossLines) {
21193
21319
  const stop = attrsMayCrossLines ? GT : GT_OR_LINE_TERMINATOR;
21194
21320
  const out = [];
21195
21321
  let termAt = -1;
21196
- OPENER.lastIndex = 0;
21322
+ OPENER$1.lastIndex = 0;
21197
21323
  let opener;
21198
- while ((opener = OPENER.exec(text)) !== null) {
21324
+ while ((opener = OPENER$1.exec(text)) !== null) {
21199
21325
  LEADING_WS.lastIndex = opener.index + opener[0].length;
21200
21326
  LEADING_WS.exec(text);
21201
21327
  const attrsStart = LEADING_WS.lastIndex;
@@ -21215,7 +21341,7 @@ function scanArtifactTags(text, attrsMayCrossLines) {
21215
21341
  attrs: text.slice(attrsStart, termAt),
21216
21342
  body: text.slice(termAt + 1, closer.index)
21217
21343
  });
21218
- OPENER.lastIndex = end;
21344
+ OPENER$1.lastIndex = end;
21219
21345
  }
21220
21346
  return out;
21221
21347
  }
@@ -23793,111 +23919,273 @@ function getEnvironmentName(configApiConfig) {
23793
23919
  return "Self-Hosted";
23794
23920
  }
23795
23921
  //#endregion
23796
- //#region src/config/toolSafety.ts
23922
+ //#region src/tools/gateway/coreToolDescriptors.ts
23923
+ const CORE = { kind: "core" };
23924
+ const CLI = { kind: "cli" };
23925
+ const pureCompute = () => ({
23926
+ source: CORE,
23927
+ effect: "none",
23928
+ approval: "auto_approve"
23929
+ });
23930
+ const sessionTool = () => ({
23931
+ source: CLI,
23932
+ effect: "session",
23933
+ approval: "auto_approve"
23934
+ });
23935
+ const reader = (source = CORE, extra = {}) => ({
23936
+ source,
23937
+ effect: "read",
23938
+ approval: "prompt_default",
23939
+ ...extra
23940
+ });
23941
+ const fileWriter = () => ({
23942
+ source: CORE,
23943
+ effect: "write",
23944
+ approval: "prompt_always",
23945
+ writePathArg: "path"
23946
+ });
23947
+ const lattice = () => ({
23948
+ source: CORE,
23949
+ effect: "none",
23950
+ approval: "prompt_default"
23951
+ });
23952
+ /**
23953
+ * Descriptors for every tool the CLI builds itself or gets from @bike4mind/services:
23954
+ * generateCliTools, buildCliToolList and the subagent-injected tools. A tool missing here
23955
+ * resolves to the unknown fallback in toolCatalog.ts; coreToolCompleteness.test.ts
23956
+ * fails when a generated tool has no entry.
23957
+ */
23958
+ const CORE_TOOL_DESCRIPTORS = new Map(Object.entries({
23959
+ math_evaluate: pureCompute(),
23960
+ current_datetime: pureCompute(),
23961
+ dice_roll: pureCompute(),
23962
+ prompt_enhancement: pureCompute(),
23963
+ find_definition: {
23964
+ source: CLI,
23965
+ effect: "read",
23966
+ approval: "auto_approve"
23967
+ },
23968
+ ask_user_question: {
23969
+ source: CORE,
23970
+ effect: "session",
23971
+ approval: "auto_approve"
23972
+ },
23973
+ agent_delegate: sessionTool(),
23974
+ check_agent_status: sessionTool(),
23975
+ list_background_agents: sessionTool(),
23976
+ cancel_background_agent: sessionTool(),
23977
+ resume_agent: sessionTool(),
23978
+ create_dynamic_agent: sessionTool(),
23979
+ coordinate_task: sessionTool(),
23980
+ decompose_task: sessionTool(),
23981
+ shared_context_read: sessionTool(),
23982
+ shared_context_write: sessionTool(),
23983
+ write_todos: sessionTool(),
23984
+ read_prior_todos: sessionTool(),
23985
+ log_decision: sessionTool(),
23986
+ track_blocker: sessionTool(),
23987
+ resolve_blocker: sessionTool(),
23988
+ request_review_gate: sessionTool(),
23989
+ work_item_create: sessionTool(),
23990
+ work_item_update: sessionTool(),
23991
+ work_item_close: sessionTool(),
23992
+ work_item_ready: sessionTool(),
23993
+ work_item_graph: sessionTool(),
23994
+ tool_search: sessionTool(),
23995
+ create_file: fileWriter(),
23996
+ edit_local_file: fileWriter(),
23997
+ delete_file: fileWriter(),
23998
+ edit_file: {
23999
+ source: CORE,
24000
+ effect: "write",
24001
+ approval: "prompt_always"
24002
+ },
24003
+ bash_execute: {
24004
+ source: CORE,
24005
+ effect: "execute",
24006
+ approval: "prompt_always",
24007
+ openWorld: true,
24008
+ commandArg: "command"
24009
+ },
24010
+ shell_execute: {
24011
+ source: CORE,
24012
+ effect: "execute",
24013
+ approval: "prompt_always",
24014
+ openWorld: true
24015
+ },
24016
+ write_shell_stdin: {
24017
+ source: CORE,
24018
+ effect: "execute",
24019
+ approval: "prompt_always",
24020
+ commandArg: "chars"
24021
+ },
24022
+ kill_background_shell: {
24023
+ source: CORE,
24024
+ effect: "execute",
24025
+ approval: "prompt_always"
24026
+ },
24027
+ git_commit: {
24028
+ source: CORE,
24029
+ effect: "write",
24030
+ approval: "prompt_always"
24031
+ },
24032
+ git_push: {
24033
+ source: CORE,
24034
+ effect: "write",
24035
+ approval: "prompt_always",
24036
+ openWorld: true
24037
+ },
24038
+ skill: {
24039
+ source: CLI,
24040
+ effect: "execute",
24041
+ approval: "prompt_always"
24042
+ },
24043
+ weather_info: reader(CORE, { openWorld: true }),
24044
+ web_search: reader(CORE, { openWorld: true }),
24045
+ web_fetch: reader(CORE, { openWorld: true }),
24046
+ deep_research: reader(CORE, { openWorld: true }),
24047
+ check_shell_output: reader(),
24048
+ list_background_shells: reader(),
24049
+ file_read: reader(),
24050
+ grep_search: reader(),
24051
+ glob_files: reader(),
24052
+ get_file_tree: reader(),
24053
+ get_file_structure: reader(CLI),
24054
+ recent_changes: reader(),
24055
+ git_status: reader(),
24056
+ git_diff: reader(),
24057
+ git_log: reader(),
24058
+ git_branch: reader(),
24059
+ lattice_create_model: lattice(),
24060
+ lattice_add_entity: lattice(),
24061
+ lattice_set_value: lattice(),
24062
+ lattice_create_rule: lattice(),
24063
+ lattice_query: lattice(),
24064
+ lattice_explain: lattice()
24065
+ }));
24066
+ //#endregion
24067
+ //#region src/tools/gateway/toolPolicy.ts
23797
24068
  /**
23798
- * Tool safety categories determine when permission is required
24069
+ * Every policy decision about a tool, derived from its descriptor. `undefined` is a tool the
24070
+ * catalog could not resolve; it keeps the pre-descriptor fallback (prompt_default, read-only)
24071
+ * because feature-module and plugin tools do not register descriptors yet.
23799
24072
  */
23800
- const ToolCategorySchema = z$1.enum([
23801
- "auto_approve",
23802
- "prompt_always",
23803
- "prompt_default"
24073
+ const UNKNOWN_TOOL_CATEGORY = "prompt_default";
24074
+ const READ_ONLY_EFFECTS = /* @__PURE__ */ new Set([
24075
+ "none",
24076
+ "session",
24077
+ "read"
23804
24078
  ]);
23805
- z$1.object({
23806
- categories: z$1.record(z$1.string(), ToolCategorySchema),
23807
- trustedTools: z$1.array(z$1.string())
23808
- });
23809
- /**
23810
- * Default tool categories
23811
- *
23812
- * Categories:
23813
- * - auto_approve: Safe tools that don't need permission (math, search, datetime)
23814
- * - prompt_always: Dangerous tools that ALWAYS need permission, cannot be trusted (file edits, shell commands)
23815
- * - prompt_default: Tools that prompt by default but users can trust them (file reads, searches)
23816
- */
23817
- const DEFAULT_TOOL_CATEGORIES = {
23818
- math_evaluate: "auto_approve",
23819
- current_datetime: "auto_approve",
23820
- dice_roll: "auto_approve",
23821
- prompt_enhancement: "auto_approve",
23822
- find_definition: "auto_approve",
23823
- ask_user_question: "auto_approve",
23824
- weather_info: "prompt_default",
23825
- agent_delegate: "auto_approve",
23826
- check_agent_status: "auto_approve",
23827
- list_background_agents: "auto_approve",
23828
- cancel_background_agent: "auto_approve",
23829
- resume_agent: "auto_approve",
23830
- create_dynamic_agent: "auto_approve",
23831
- coordinate_task: "auto_approve",
23832
- decompose_task: "auto_approve",
23833
- shared_context_read: "auto_approve",
23834
- shared_context_write: "auto_approve",
23835
- write_todos: "auto_approve",
23836
- read_prior_todos: "auto_approve",
23837
- log_decision: "auto_approve",
23838
- track_blocker: "auto_approve",
23839
- resolve_blocker: "auto_approve",
23840
- request_review_gate: "auto_approve",
23841
- work_item_create: "auto_approve",
23842
- work_item_update: "auto_approve",
23843
- work_item_close: "auto_approve",
23844
- work_item_ready: "auto_approve",
23845
- work_item_graph: "auto_approve",
23846
- tool_search: "auto_approve",
23847
- edit_file: "prompt_always",
23848
- edit_local_file: "prompt_always",
23849
- create_file: "prompt_always",
23850
- delete_file: "prompt_always",
23851
- shell_execute: "prompt_always",
23852
- bash_execute: "prompt_always",
23853
- write_shell_stdin: "prompt_always",
23854
- kill_background_shell: "prompt_always",
23855
- git_commit: "prompt_always",
23856
- git_push: "prompt_always",
23857
- skill: "prompt_always",
23858
- web_search: "prompt_default",
23859
- check_shell_output: "prompt_default",
23860
- list_background_shells: "prompt_default",
23861
- web_fetch: "prompt_default",
23862
- deep_research: "prompt_default",
23863
- file_read: "prompt_default",
23864
- grep_search: "prompt_default",
23865
- glob_files: "prompt_default",
23866
- get_file_tree: "prompt_default",
23867
- get_file_structure: "prompt_default",
23868
- git_status: "prompt_default",
23869
- git_diff: "prompt_default",
23870
- git_log: "prompt_default",
23871
- git_branch: "prompt_default"
23872
- };
23873
24079
  /**
23874
- * Get the category for a tool
23875
- * Returns 'prompt_default' if tool is not in the default categories
23876
- */
23877
- function getToolCategory(toolName, customCategories) {
23878
- if (toolName.startsWith("agent_hook:") || toolName.startsWith("skill_hook:")) return "prompt_always";
23879
- if (customCategories && toolName in customCategories) return customCategories[toolName];
23880
- if (toolName in DEFAULT_TOOL_CATEGORIES) return DEFAULT_TOOL_CATEGORIES[toolName];
23881
- if (toolName.startsWith("mcp__")) return "prompt_always";
23882
- return "prompt_default";
24080
+ * How far a source's own declarations are believed. Core, cli and builtin-module tools ship in
24081
+ * this repo and are reviewed, so their descriptor is taken as written. A plugin is third-party
24082
+ * code the user installed and enabled: it can be trusted per tool after a prompt, but cannot
24083
+ * skip the first prompt or claim to be read-only. MCP tools and lifecycle hooks always prompt.
24084
+ */
24085
+ function categoryCeiling(source) {
24086
+ switch (source.kind) {
24087
+ case "mcp":
24088
+ case "hook": return "prompt_always";
24089
+ case "plugin": return "prompt_default";
24090
+ default: return;
24091
+ }
24092
+ }
24093
+ function believesDeclaredEffect(source) {
24094
+ return categoryCeiling(source) === void 0;
24095
+ }
24096
+ function toolCategory(descriptor) {
24097
+ if (!descriptor) return UNKNOWN_TOOL_CATEGORY;
24098
+ const ceiling = categoryCeiling(descriptor.source);
24099
+ if (ceiling === "prompt_always") return ceiling;
24100
+ if (ceiling === "prompt_default" && descriptor.approval === "auto_approve") return ceiling;
24101
+ return descriptor.approval;
24102
+ }
24103
+ /** Whether a prompt_default trust decision can be persisted for the tool. */
24104
+ function canTrustTool(descriptor) {
24105
+ return toolCategory(descriptor) !== "prompt_always";
24106
+ }
24107
+ /** Read-only tools run in plan mode and in parallel batches; the rest are blocked / sequential. */
24108
+ function isReadOnlyTool(descriptor) {
24109
+ if (!descriptor) return true;
24110
+ if (!believesDeclaredEffect(descriptor.source)) return false;
24111
+ return READ_ONLY_EFFECTS.has(descriptor.effect);
24112
+ }
24113
+ /** The shell command text the risk gate classifies, when the tool has one in `args`. */
24114
+ function commandTextOf(descriptor, args) {
24115
+ return stringArg(args, descriptor?.commandArg);
24116
+ }
24117
+ /** The path a file-writing tool will write, when the tool declares one and `args` carries it. */
24118
+ function writePathOf(descriptor, args) {
24119
+ const writePath = stringArg(args, descriptor?.writePathArg);
24120
+ return writePath ? writePath : void 0;
24121
+ }
24122
+ function stringArg(args, field) {
24123
+ if (!field || args === null || typeof args !== "object") return void 0;
24124
+ const value = args[field];
24125
+ return typeof value === "string" ? value : void 0;
24126
+ }
24127
+ //#endregion
24128
+ //#region src/tools/gateway/toolCatalog.ts
24129
+ const MCP_PREFIX = "mcp__";
24130
+ const HOOK_PREFIXES = ["agent_hook:", "skill_hook:"];
24131
+ function isHookName(toolName) {
24132
+ return HOOK_PREFIXES.some((prefix) => toolName.startsWith(prefix));
23883
24133
  }
23884
24134
  /**
23885
- * Check if a tool can be trusted (not prompt_always)
23886
- */
23887
- function canTrustTool(toolName, customCategories) {
23888
- return getToolCategory(toolName, customCategories) !== "prompt_always";
24135
+ * Resolve a tool name without a catalog instance: the core table, then the MCP namespace
24136
+ * (`mcp__<server>__<tool>`), then the lifecycle-hook namespaces. Undefined means unknown.
24137
+ * ToolCatalog.resolve falls back to this after its registered descriptors.
24138
+ */
24139
+ function resolveStaticToolDescriptor(toolName) {
24140
+ const core = CORE_TOOL_DESCRIPTORS.get(toolName);
24141
+ if (core) return core;
24142
+ if (toolName.startsWith(MCP_PREFIX)) return {
24143
+ source: {
24144
+ kind: "mcp",
24145
+ server: toolName.slice(5).split("__")[0]
24146
+ },
24147
+ effect: "execute",
24148
+ approval: "prompt_always"
24149
+ };
24150
+ if (isHookName(toolName)) return {
24151
+ source: { kind: "hook" },
24152
+ effect: "execute",
24153
+ approval: "prompt_always"
24154
+ };
23889
24155
  }
23890
24156
  /**
23891
- * Check if a tool is read-only (safe for parallel execution).
23892
- * Write tools (prompt_always category) must always be sequential.
23893
- *
23894
- * @param toolName - Name of the tool to check
23895
- * @param customCategories - Optional custom category overrides
23896
- * @returns true if the tool is read-only, false if it's a write tool
24157
+ * Name -> descriptor lookup for one CLI process or ACP stack. Entry points create one and hand
24158
+ * it to their PermissionManager, which the permission gateway and the agent's read-only
24159
+ * predicate read it through.
23897
24160
  */
23898
- function isReadOnlyTool(toolName, customCategories) {
23899
- return getToolCategory(toolName, customCategories) !== "prompt_always";
23900
- }
24161
+ var ToolCatalog = class {
24162
+ constructor() {
24163
+ this.registered = /* @__PURE__ */ new Map();
24164
+ }
24165
+ /**
24166
+ * Add a descriptor for a tool the core table does not know. Core names and the MCP and hook
24167
+ * namespaces are reserved: nothing can re-declare a core tool, or declare itself into a
24168
+ * namespace whose descriptor comes from the name alone, with a lower tier.
24169
+ */
24170
+ register(toolName, descriptor) {
24171
+ if (toolName.startsWith(MCP_PREFIX) || isHookName(toolName)) throw new Error(`Tool name "${toolName}" is in a reserved namespace and cannot be registered`);
24172
+ if (CORE_TOOL_DESCRIPTORS.has(toolName)) throw new Error(`Tool "${toolName}" is a core tool and already has a descriptor`);
24173
+ if (this.registered.has(toolName)) throw new Error(`Tool "${toolName}" already has a registered descriptor`);
24174
+ this.registered.set(toolName, descriptor);
24175
+ }
24176
+ resolve(toolName) {
24177
+ return this.registered.get(toolName) ?? resolveStaticToolDescriptor(toolName);
24178
+ }
24179
+ category(toolName) {
24180
+ return toolCategory(this.resolve(toolName));
24181
+ }
24182
+ canTrust(toolName) {
24183
+ return canTrustTool(this.resolve(toolName));
24184
+ }
24185
+ isReadOnly(toolName) {
24186
+ return isReadOnlyTool(this.resolve(toolName));
24187
+ }
24188
+ };
23901
24189
  const PROJECT_CONTEXT_FILES = [
23902
24190
  "CLAUDE.local.md",
23903
24191
  "CLAUDE.md",
@@ -24638,7 +24926,7 @@ function mergeConfigs(global, project, local) {
24638
24926
  enabled: merged.tools.enabled.filter((t) => !disabledSet.has(t))
24639
24927
  };
24640
24928
  const globalTrusted = new Set(global.trustedTools || []);
24641
- merged.trustedTools = (merged.trustedTools || []).filter((t) => globalTrusted.has(t) || canTrustTool(t) && !disabledSet.has(t));
24929
+ merged.trustedTools = (merged.trustedTools || []).filter((t) => globalTrusted.has(t) || canTrustTool(resolveStaticToolDescriptor(t)) && !disabledSet.has(t));
24642
24930
  return merged;
24643
24931
  }
24644
24932
  /**
@@ -25315,4 +25603,4 @@ var ConfigStore = class {
25315
25603
  }
25316
25604
  };
25317
25605
  //#endregion
25318
- export { SEARCH_RESULT_CARDS_LANGUAGE as $, BRIEFCASE_CATALOG_QUERIES as A, tokenEstimateMultiplier as At, GENERATED_IMAGE_EXTENSION_RE as B, delegationPayloadSchema as Bt, AGENT_QUEST_ID as C, replacePromptVariables as Ct, ARTIFACT_REMOVED_PLACEHOLDER as D, stripDeliveredArtifactBlocks as Dt, ARTIFACT_DELIVERED_PLACEHOLDER as E, splitEquals as Et, ChatModels as F, ACTOR_COLOR_SLOTS as Ft, McpServerName as G, ImagePromptResolutionSchema as H, APP_NAME as Ht, CompletionReasoningEffortSchema as I, DELEGATION_PAYLOAD_SCHEMA_NAME as It, OllamaEmbeddingModel as J, ModelBackend as K, CreateArtifactRequestSchema as L, actorColorIndex as Lt, BriefcasePromptIdSchema as M, ttsRequestSchema as Mt, CONTEXT_WINDOW_SAFETY_BUFFER_TOKENS as N, ttsResponseTooLargeSchema as Nt, ApiKeyType as O, stripToolArtifactMarkup as Ot, CREDIT_DEDUCT_TRANSACTION_TYPES as P, withRetry as Pt, REVIEW_GATE_STATUS_VALUES as Q, DEFAULT_TTS_PROVIDER as R, actorKindMarker as Rt, escapeArtifactBodyJson as S, questReplyText as St, AGENT_QUEST_MCP_URI as T, signImageUrl as Tt, LOCATION_MAP_LANGUAGE as U, WEBSITE_URL as Ut, ImageModels as V, selfClaimedActorKindSchema as Vt, MODEL_INFO_FIELD_GROUP_OF as W, PROMPT_TEXT_MAX as X, OpenAIEmbeddingModel as Y, PermissionDeniedError as Z, logger as _, isPlaceholderImageSigningSecret as _t, getToolCategory as a, WORK_ITEM_STATUSES as at, htmlErrorTitle as b, joinReplySlots as bt, getCreditsUrl as c, createThinkMarkerEscaper as ct, resolveApiEndpoint as d, generatedAudioResponseSchema as dt, SLUG_OWNED_BY_OTHER as et, AuthenticatedApiClient as f, getMcpProviderMetadata as ft, parseApiUrl as g, isEarlyStop as gt, NotAuthenticatedError as h, hasDeliverablePinnedArtifact as ht, canTrustTool as i, VoyageAIEmbeddingModel as it, BedrockEmbeddingModel as j, ttsBase64ResponseSchema as jt, ArtifactTypeSchema as k, supportedVoiceGenerationVendor as kt, getEnvironmentName as l, escapeThinkMarkers as lt, LOCAL_DEV_URL as m, googleMapsSearchUrl as mt, extractCompactInstructions as n, SupportedFabFileMimeTypes as nt, isReadOnlyTool as o, assertAuthOriginMatchesApiKey as ot, DeviceFlowClient as p, getQuestErrorCode as pt, NotFoundError as q, loadContextFiles as r, VisibilitySchema as rt, ApiEndpointUnconfiguredError as s, buildPromptContext as st, ConfigStore as t, SUBQUEST_STATUS_VALUES as tt, requireApiUrl as u, filterToolArtifactMarkup as ut, SESSION_ID_PATTERN as v, isRetryableError as vt, AGENT_QUEST_MANIFEST as w, secureParameters as wt, htmlFirstH1 as x, obfuscateApiKey as xt, isValidSessionId as y, isUserInitiatedAbort as yt, DEGENERATE_FINISH_REASON as z, actorKindSchema as zt };
25606
+ export { PermissionDeniedError as $, ApiKeyType as A, stripToolArtifactMarkup as At, DEFAULT_TTS_PROVIDER as B, actorKindMarker as Bt, htmlFirstH1 as C, obfuscateApiKey as Ct, AGENT_QUEST_MCP_URI as D, signImageUrl as Dt, AGENT_QUEST_MANIFEST as E, secureParameters as Et, CONTEXT_WINDOW_SAFETY_BUFFER_TOKENS as F, ttsResponseTooLargeSchema as Ft, LOCATION_MAP_LANGUAGE as G, WEBSITE_URL as Gt, GENERATED_IMAGE_EXTENSION_RE as H, delegationPayloadSchema as Ht, CREDIT_DEDUCT_TRANSACTION_TYPES as I, withRetry as It, ModelBackend as J, MODEL_INFO_FIELD_GROUP_OF as K, ChatModels as L, ACTOR_COLOR_SLOTS as Lt, BRIEFCASE_CATALOG_QUERIES as M, tokenEstimateMultiplier as Mt, BedrockEmbeddingModel as N, ttsBase64ResponseSchema as Nt, ARTIFACT_DELIVERED_PLACEHOLDER as O, splitEquals as Ot, BriefcasePromptIdSchema as P, ttsRequestSchema as Pt, PROMPT_TEXT_MAX as Q, CompletionReasoningEffortSchema as R, DELEGATION_PAYLOAD_SCHEMA_NAME as Rt, htmlErrorTitle as S, joinReplySlots as St, AGENT_QUEST_ID as T, replacePromptVariables as Tt, ImageModels as U, selfClaimedActorKindSchema as Ut, DEGENERATE_FINISH_REASON as V, actorKindSchema as Vt, ImagePromptResolutionSchema as W, APP_NAME as Wt, OllamaEmbeddingModel as X, NotFoundError as Y, OpenAIEmbeddingModel as Z, NotAuthenticatedError as _, hasDeliverablePinnedArtifact as _t, commandTextOf as a, VisibilitySchema as at, SESSION_ID_PATTERN as b, isRetryableError as bt, writePathOf as c, assertAuthOriginMatchesApiKey as ct, getEnvironmentName as d, escapeThinkMarkers as dt, REVIEW_GATE_STATUS_VALUES as et, requireApiUrl as f, filterToolArtifactMarkup as ft, LOCAL_DEV_URL as g, googleMapsSearchUrl as gt, DeviceFlowClient as h, getQuestErrorCode as ht, ToolCatalog as i, SupportedFabFileMimeTypes as it, ArtifactTypeSchema as j, supportedVoiceGenerationVendor as jt, ARTIFACT_REMOVED_PLACEHOLDER as k, stripDeliveredArtifactBlocks as kt, ApiEndpointUnconfiguredError as l, buildPromptContext as lt, AuthenticatedApiClient as m, getMcpProviderMetadata as mt, extractCompactInstructions as n, SLUG_OWNED_BY_OTHER as nt, isReadOnlyTool as o, VoyageAIEmbeddingModel as ot, resolveApiEndpoint as p, generatedAudioResponseSchema as pt, McpServerName as q, loadContextFiles as r, SUBQUEST_STATUS_VALUES as rt, toolCategory as s, WORK_ITEM_STATUSES as st, ConfigStore as t, SEARCH_RESULT_CARDS_LANGUAGE as tt, getCreditsUrl as u, createThinkMarkerEscaper as ut, parseApiUrl as v, isEarlyStop as vt, escapeArtifactBodyJson as w, questReplyText as wt, isValidSessionId as x, isUserInitiatedAbort as xt, logger as y, isPlaceholderImageSigningSecret as yt, CreateArtifactRequestSchema as z, actorColorIndex as zt };