@bike4mind/cli 3.0.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
  }
@@ -269,6 +282,7 @@ var PermissionDeniedError = class extends Error {
269
282
  };
270
283
  const UNKNOWN_MODEL_ERROR_CODE = "unknown_model";
271
284
  const BILLING_ORGANIZATION_MISMATCH_ERROR_CODE = "billing_organization_mismatch";
285
+ const REQUEST_TIMEOUT_ERROR_CODE = "request_timeout";
272
286
  const GROUP_TYPE_CATALOG = Object.freeze([
273
287
  {
274
288
  key: "sales",
@@ -777,6 +791,20 @@ const shareableDocumentSchema = z$1.object({
777
791
  users: z$1.array(userShareSchema),
778
792
  groups: z$1.array(groupShareSchema)
779
793
  });
794
+ /** Bedrock's `[<scope>.]anthropic.` id prefix (any region or `global`), as regex source. Shared with isClaudeModelId. */
795
+ const BEDROCK_ANTHROPIC_PREFIX = "(?:(?:[a-z-]+\\.)?anthropic\\.)?";
796
+ const CLAUDE_MODEL_ID = new RegExp(`^${BEDROCK_ANTHROPIC_PREFIX}claude-(opus|sonnet|haiku|fable|mythos)-(\\d+)(?:-(\\d{1,2}))?(?=$|[:@[]|-(?:\\d{8}(?!\\d)|[a-z]))`);
797
+ /** Family and version of a Claude id (native, Bedrock-scoped, dated or aliased); null for any other shape. */
798
+ function parseClaudeModelId(modelId) {
799
+ const match = CLAUDE_MODEL_ID.exec(modelId);
800
+ if (!match) return null;
801
+ const [, family, major, minor] = match;
802
+ return {
803
+ family,
804
+ major: Number(major),
805
+ minor: minor === void 0 ? null : Number(minor)
806
+ };
807
+ }
780
808
  /**
781
809
  * Model backends
782
810
  */
@@ -1032,6 +1060,7 @@ let ChatModels = /* @__PURE__ */ function(ChatModels) {
1032
1060
  }({});
1033
1061
  const CHAT_MODELS = Object.values(ChatModels);
1034
1062
  const supportedChatModels = z$1.enum(ChatModels);
1063
+ new RegExp(`^${BEDROCK_ANTHROPIC_PREFIX}claude-`);
1035
1064
  /**
1036
1065
  * Models that support OpenAI's `reasoning_effort` request parameter.
1037
1066
  * o1-preview and o1-mini do NOT support reasoning_effort.
@@ -3126,8 +3155,22 @@ z.object({
3126
3155
  /** Set for job-backed generations; the save is idempotent per job. */
3127
3156
  jobId: z.string().optional(),
3128
3157
  prompt: z.string(),
3158
+ /**
3159
+ * Storing caps `prompt` at a prefix of what was generated (sourceMetadata rides every metadata-only
3160
+ * listing). Written on every save, true when it was cut. Absent only on files saved before it existed:
3161
+ * read it through isStoredPromptPossiblyTruncated, which covers those.
3162
+ */
3163
+ promptTruncated: z.boolean().optional(),
3129
3164
  lyrics: z.string().optional(),
3130
- durationSeconds: z.number().positive().optional()
3165
+ durationSeconds: z.number().positive().optional(),
3166
+ /** Speech: the requested voice; absent when the provider default was used or another provider stood in. */
3167
+ voice: z.string().optional(),
3168
+ /** Speech: the output format. */
3169
+ format: z.string().optional(),
3170
+ /** Speech: the requested language; absent when the provider auto-detected it. */
3171
+ languageCode: z.string().optional(),
3172
+ /** Sound effects: how closely the provider followed the prompt, 0-1. */
3173
+ promptInfluence: z.number().min(0).max(1).optional()
3131
3174
  });
3132
3175
  /**
3133
3176
  * Why a generated file was not kept in the user's files. Never fatal: the output was already produced and
@@ -3613,6 +3656,199 @@ z$1.enum([
3613
3656
  "investigate_soon",
3614
3657
  "immediate_action"
3615
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
+ ]);
3616
3852
  /** Splits on the marker tokens, keeping them in the result so a scan can track nesting depth. */
3617
3853
  const THINK_TAG_TOKENS = /(<think>|<\/think>)/g;
3618
3854
  /**
@@ -3750,8 +3986,8 @@ const invalid = (reason) => ({
3750
3986
  status: "invalid",
3751
3987
  reason
3752
3988
  });
3753
- const FENCE_OPEN_LINE = /^[ \t]*(`{3,}|~{3,})(.*)$/;
3754
- 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]*$/;
3755
3991
  /**
3756
3992
  * Offset where a slot's visible answer starts: just past its last top-level `</think>`, 0 when it
3757
3993
  * has none, or null while a `<think>` is still open. Depth-tracked to match visibleReplyText in
@@ -3781,7 +4017,7 @@ function findLastChoicesFence(text) {
3781
4017
  const lineEnd = newline === -1 ? text.length : newline;
3782
4018
  const line = text.slice(lineStart, lineEnd).replace(/\r$/, "");
3783
4019
  if (!open) {
3784
- const match = line.match(FENCE_OPEN_LINE);
4020
+ const match = line.match(FENCE_OPEN_LINE$1);
3785
4021
  if (match && !(match[1][0] === "`" && match[2].includes("`"))) open = {
3786
4022
  start: lineStart,
3787
4023
  bodyStart: newline === -1 ? text.length : newline + 1,
@@ -3789,7 +4025,7 @@ function findLastChoicesFence(text) {
3789
4025
  isChoices: match[1] === "```" && match[2].trim().toLowerCase() === "choices"
3790
4026
  };
3791
4027
  } else {
3792
- const match = line.match(FENCE_CLOSE_LINE);
4028
+ const match = line.match(FENCE_CLOSE_LINE$1);
3793
4029
  if (match && match[1][0] === open.marker[0] && match[1].length >= open.marker.length) {
3794
4030
  if (open.isChoices) last = {
3795
4031
  start: open.start,
@@ -4796,6 +5032,13 @@ z$1.object({
4796
5032
  reason: z$1.enum(REPLY_CHOICES_INVALID_REASONS).optional()
4797
5033
  }).optional(),
4798
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
+ /**
4799
5042
  * Set when an emitted artifact looks voluntarily abbreviated - placeholder comments in
4800
5043
  * place of real code, or calls into functions that were never defined. The complement to
4801
5044
  * `finishReason === 'max_tokens'`: truncation is a hard stop the provider reports, elision
@@ -5016,6 +5259,17 @@ const ApiErrorSchema = z$1.object({
5016
5259
  */
5017
5260
  const InsufficientCreditsErrorSchema = ApiErrorSchema.extend({ errorCode: z$1.enum(QUEST_ERROR_CODES).optional() });
5018
5261
  /**
5262
+ * 504 for a `wait: true` turn that ran past the request deadline. The turn is already settled
5263
+ * (timed out, unused credits refunded) when this is sent; `quest_id` is the quest to poll and
5264
+ * `sessionId` is the notebook the turn was recorded in, so a caller that sent no `sessionId` can
5265
+ * continue the conversation it just started (same field name as the ACK and the `wait: true` body).
5266
+ */
5267
+ const ChatWaitTimeoutErrorSchema = ApiErrorSchema.extend({
5268
+ errorCode: z$1.literal(REQUEST_TIMEOUT_ERROR_CODE),
5269
+ quest_id: z$1.string(),
5270
+ sessionId: z$1.string().optional()
5271
+ });
5272
+ /**
5019
5273
  * 400 for an upload refused on size or quota. The typed members are optional because the same 400 also
5020
5274
  * covers errors that carry none (an unsupported file type); when `errorCode` is present all five arrive
5021
5275
  * together, which the endpoint's 400 description states for integrators.
@@ -5023,8 +5277,11 @@ const InsufficientCreditsErrorSchema = ApiErrorSchema.extend({ errorCode: z$1.en
5023
5277
  const StorageLimitErrorSchema = ApiErrorSchema.extend(StorageLimitErrorInfoSchema.partial().shape);
5024
5278
  /** 503 when this deployment has no usable key for a provider the request needs (data-lake search, voice). */
5025
5279
  const ProviderNotConfiguredErrorSchema = ApiErrorSchema.extend({ errorCode: z$1.literal("provider_not_configured").optional() });
5026
- /** 400 for a `model` the caller cannot use; `errorCode` is absent on the other 400s (e.g. malformed JSON). */
5027
- 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() });
5028
5285
  /**
5029
5286
  * Error envelope for the 403 apiKeyAuth (apps/client/server/middlewares/apiKeyAuth.ts)
5030
5287
  * throws when an API key lacks a route's scopes. It names what the ROUTE requires,
@@ -5205,6 +5462,16 @@ const MUSIC_VALIDATION_ERROR_CODES = [
5205
5462
  "lyrics_with_instrumental",
5206
5463
  "lyrics_too_long"
5207
5464
  ];
5465
+ /**
5466
+ * Building blocks for entity response schemas held to an entity's key list (see
5467
+ * selfUserResponseSchema in ./user.ts for the full rationale). A passthrough value is
5468
+ * `z.unknown()`: the boundary enforces WHICH keys leave the server, not their types, so a
5469
+ * legacy value on a long-lived Mongo document can never 500 a read.
5470
+ */
5471
+ const passthroughField = z$2.unknown().optional();
5472
+ function passthroughShape(fields) {
5473
+ return Object.fromEntries(Object.keys(fields).map((field) => [field, passthroughField]));
5474
+ }
5208
5475
  z$2.enum([
5209
5476
  "",
5210
5477
  "TFG",
@@ -5352,10 +5619,6 @@ const SELF_VIEW_PASSTHROUGH_FIELDS = {
5352
5619
  currentCredits: true,
5353
5620
  lastCreditsPurchasedAt: true
5354
5621
  };
5355
- const passthroughField = z$2.unknown().optional();
5356
- function passthroughShape(fields) {
5357
- return Object.fromEntries(Object.keys(fields).map((field) => [field, passthroughField]));
5358
- }
5359
5622
  /** A redacted integration object: only the named non-secret subfields survive. */
5360
5623
  function redactedSubdocument(fields) {
5361
5624
  return z$2.object(passthroughShape(Object.fromEntries(fields.map((field) => [field, true])))).nullable().optional();
@@ -5942,34 +6205,95 @@ function parseCidr(raw) {
5942
6205
  prefix: Number(value.slice(slash + 1))
5943
6206
  };
5944
6207
  }
5945
- function cidrContains(cidr, address) {
5946
- const target = addressBytes(address);
5947
- if (!target || target.version !== cidr.version) return false;
5948
- 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++) {
5949
6279
  const byte = bit >> 3;
5950
6280
  const mask = 128 >> (bit & 7);
5951
- 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;
5952
6282
  }
5953
6283
  return true;
5954
6284
  }
5955
- /**
5956
- * Cloud instance-metadata endpoints, plus the IPv4 one in its IPv4-mapped and NAT64 IPv6 forms.
5957
- * Never reachable through `customMcpAllowPrivateNetworks`, self-host included.
5958
- */
5959
- const CLOUD_METADATA_ADDRESSES = [
5960
- "169.254.169.254",
5961
- "fd00:ec2::254",
5962
- "::ffff:169.254.169.254",
5963
- "64:ff9b::169.254.169.254"
5964
- ];
6285
+ const NEVER_ADMITTED_PARSED = NEVER_ADMITTED_NETWORKS.map((entry) => ({
6286
+ ...entry,
6287
+ parsed: parseCidr(entry.cidr)
6288
+ }));
5965
6289
  /** Why `raw` may not be allowed as a private network, or null when it may. */
5966
6290
  function customMcpPrivateCidrProblem(raw) {
5967
6291
  const cidr = parseCidr(raw);
5968
6292
  if (!cidr) return "Must be a CIDR such as 10.0.0.0/8 or fd12:3456::/32";
5969
6293
  const minPrefix = CUSTOM_MCP_MIN_CIDR_PREFIX[cidr.version];
5970
6294
  if (cidr.prefix < minPrefix) return `IPv${cidr.version} ranges must be /${minPrefix} or narrower`;
5971
- const metadata = CLOUD_METADATA_ADDRESSES.find((address) => cidrContains(cidr, address));
5972
- 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`;
5973
6297
  return null;
5974
6298
  }
5975
6299
  const CustomMcpPrivateCidrSchema = z$1.string().trim().toLowerCase().superRefine((value, ctx) => {
@@ -6973,8 +7297,8 @@ const CustomMcpPolicySchema = z$1.object({
6973
7297
  const CustomMcpBlockedHostsSchema = z$1.object({ hosts: McpHostPatternListSchema.default([]) });
6974
7298
  /**
6975
7299
  * Self-host only; hosted deployments never reach private networks regardless of this value. Each
6976
- * CIDR is strictly parsed, at least /8 (IPv4) or /16 (IPv6), and never covers a cloud metadata
6977
- * 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).
6978
7302
  */
6979
7303
  const CustomMcpAllowPrivateNetworksSchema = z$1.object({
6980
7304
  cidrs: z$1.array(CustomMcpPrivateCidrSchema).max(100).default([]),
@@ -11686,16 +12010,8 @@ const CliCompletionRequestAction = z$1.object({
11686
12010
  temperature: z$1.number().optional(),
11687
12011
  maxTokens: z$1.number().optional(),
11688
12012
  stream: z$1.boolean().optional(),
11689
- tools: z$1.array(z$1.unknown()).optional(),
11690
- response_format: z$1.discriminatedUnion("type", [z$1.object({ type: z$1.literal("text") }), z$1.object({
11691
- type: z$1.literal("json_schema"),
11692
- json_schema: z$1.object({
11693
- name: z$1.string(),
11694
- description: z$1.string().optional(),
11695
- schema: z$1.record(z$1.string(), z$1.any()),
11696
- strict: z$1.boolean().optional().default(true)
11697
- })
11698
- })]).optional()
12013
+ tools: z$1.array(CompletionToolSchema).optional(),
12014
+ response_format: ResponseFormatSchema.optional()
11699
12015
  }).optional()
11700
12016
  });
11701
12017
  /**
@@ -12184,7 +12500,7 @@ const VoiceCreditsExhaustedAction = z$1.object({
12184
12500
  const CliCompletionChunkAction = z$1.object({
12185
12501
  action: z$1.literal("cli_completion_chunk"),
12186
12502
  requestId: z$1.string(),
12187
- chunk: z$1.object({
12503
+ chunk: z$1.union([z$1.object({
12188
12504
  type: z$1.enum(["content", "tool_use"]),
12189
12505
  text: z$1.string(),
12190
12506
  toolStarted: z$1.object({
@@ -12197,7 +12513,7 @@ const CliCompletionChunkAction = z$1.object({
12197
12513
  outputTokens: z$1.number().optional()
12198
12514
  }).optional(),
12199
12515
  thinking: z$1.array(z$1.unknown()).optional()
12200
- })
12516
+ }), CompletionProviderBlockEventSchema])
12201
12517
  });
12202
12518
  /**
12203
12519
  * CLI Completion stream finished successfully
@@ -12214,7 +12530,9 @@ const CliCompletionErrorAction = z$1.object({
12214
12530
  requestId: z$1.string(),
12215
12531
  error: z$1.string(),
12216
12532
  /** Seconds until the caller's rate-limit window reopens; present only on a rate-limit refusal. */
12217
- 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()
12218
12536
  });
12219
12537
  /**
12220
12538
  * CLI Tool execution response over WebSocket
@@ -12860,7 +13178,17 @@ const AgentProgressAction = z$1.object({
12860
13178
  executionId: z$1.string(),
12861
13179
  status: z$1.string().optional(),
12862
13180
  creditsUsed: z$1.number().optional(),
12863
- iteration: z$1.number().optional()
13181
+ iteration: z$1.number().optional(),
13182
+ questId: z$1.string().optional(),
13183
+ generationJobs: z$1.array(z$1.object({
13184
+ id: z$1.string(),
13185
+ kind: z$1.enum(GENERATION_JOB_KINDS)
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()
12864
13192
  });
12865
13193
  const AgentCompletedAction = z$1.object({
12866
13194
  action: z$1.literal("completed"),
@@ -13324,165 +13652,6 @@ const AgentExecutionStatusResponseSchema = z$1.object({
13324
13652
  });
13325
13653
  /** Path parameter for `GET /api/v1/agent-executions/{id}`. */
13326
13654
  const AgentExecutionIdParamSchema = z$1.object({ id: z$1.string().min(1) });
13327
- const CompletionReasoningEffortSchema = z$1.enum([
13328
- "none",
13329
- "minimal",
13330
- "low",
13331
- "medium",
13332
- "high",
13333
- "xhigh"
13334
- ]);
13335
- /**
13336
- * Tool schema matching ICompletionOptionTools.toolSchema. The Zod surface only
13337
- * covers wire-format fields (toolFn is server-side). Replaces the historical
13338
- * z.array(z.any()) so client-side bugs (typos in name, missing type) surface
13339
- * at parse time instead of failing silently inside the backend.
13340
- */
13341
- const CompletionToolSchema = z$1.object({
13342
- toolSchema: z$1.object({
13343
- name: z$1.string(),
13344
- description: z$1.string(),
13345
- parameters: z$1.object({
13346
- type: z$1.literal("object"),
13347
- properties: z$1.record(z$1.string(), z$1.any()).optional(),
13348
- additionalProperties: z$1.boolean().optional(),
13349
- required: z$1.array(z$1.string()).optional()
13350
- }).passthrough(),
13351
- strict: z$1.boolean().optional()
13352
- }),
13353
- _isMcpTool: z$1.boolean().optional()
13354
- });
13355
- /**
13356
- * JSON Schema definition for structured output. The `schema` field is a full
13357
- * JSON Schema document (we don't try to validate JSON Schema with Zod - that's
13358
- * the caller's contract with the model). `strict: true` is the default, matching
13359
- * OpenAI's strict-mode subset semantics.
13360
- */
13361
- const ResponseFormatSchema = z$1.discriminatedUnion("type", [z$1.object({ type: z$1.literal("text") }), z$1.object({
13362
- type: z$1.literal("json_schema"),
13363
- json_schema: z$1.object({
13364
- name: z$1.string(),
13365
- description: z$1.string().optional(),
13366
- schema: z$1.record(z$1.string(), z$1.any()),
13367
- strict: z$1.boolean().optional().default(true)
13368
- })
13369
- })]);
13370
- /**
13371
- * Wire-format message for /api/ai/v1/completions. The optional `cache: true`
13372
- * flag is honored by Anthropic (translated to `cache_control: { type: 'ephemeral' }`
13373
- * with the appropriate beta header) and silently ignored by other providers.
13374
- */
13375
- const CompletionMessageSchema = z$1.object({
13376
- role: z$1.enum([
13377
- "user",
13378
- "assistant",
13379
- "system"
13380
- ]),
13381
- content: z$1.union([z$1.string(), z$1.array(z$1.any())]),
13382
- cache: z$1.boolean().optional()
13383
- });
13384
- /**
13385
- * Schema for CLI LLM completion requests
13386
- * Shared between Next.js API route (dev) and Lambda function (production)
13387
- *
13388
- * `response_format`, `stream`, `tools`, `temperature`, and `max_tokens` are all
13389
- * accepted at the top level (OpenAI-compatible, matching how every major LLM
13390
- * SDK shapes a completion request) AND nested under `options` (legacy shape).
13391
- * Use `normalizeCompletionRequest()` to collapse both surfaces into the
13392
- * canonical `options.<field>` location before downstream consumption.
13393
- */
13394
- const CompletionRequestSchema = z$1.object({
13395
- model: z$1.string().max(256),
13396
- messages: z$1.array(CompletionMessageSchema),
13397
- response_format: ResponseFormatSchema.optional(),
13398
- stream: z$1.boolean().optional(),
13399
- tools: z$1.array(CompletionToolSchema).optional(),
13400
- temperature: z$1.number().optional(),
13401
- max_tokens: z$1.number().optional(),
13402
- options: z$1.object({
13403
- temperature: z$1.number().optional(),
13404
- maxTokens: z$1.number().optional(),
13405
- stream: z$1.boolean().optional(),
13406
- tools: z$1.array(CompletionToolSchema).optional(),
13407
- response_format: ResponseFormatSchema.optional(),
13408
- 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."),
13409
- thinking: z$1.object({
13410
- enabled: z$1.boolean(),
13411
- budget_tokens: z$1.number().int().min(1024).optional()
13412
- }).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.")
13413
- }).optional()
13414
- });
13415
- const CompletionUsageSchema = z$1.object({
13416
- inputTokens: z$1.number().optional(),
13417
- outputTokens: z$1.number().optional(),
13418
- 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."),
13419
- cacheReadInputTokens: z$1.number().optional(),
13420
- cacheCreationInputTokens: z$1.number().optional()
13421
- });
13422
- const CompletionMetaEventSchema = z$1.object({
13423
- type: z$1.literal("meta"),
13424
- requestId: z$1.string()
13425
- });
13426
- const CompletionContentEventSchema = z$1.object({
13427
- type: z$1.enum(["content", "tool_use"]),
13428
- text: z$1.string(),
13429
- tools: z$1.array(z$1.object({
13430
- name: z$1.string(),
13431
- arguments: z$1.string().optional(),
13432
- id: z$1.string().optional()
13433
- })).optional(),
13434
- usage: CompletionUsageSchema.optional(),
13435
- credits: z$1.object({
13436
- used: z$1.number().optional(),
13437
- usdCost: z$1.number().optional()
13438
- }).optional(),
13439
- responseFormatMode: z$1.enum([
13440
- "native",
13441
- "tool_use",
13442
- "best-effort"
13443
- ]).optional(),
13444
- stopReason: z$1.string().optional(),
13445
- thinking: z$1.array(z$1.any()).optional(),
13446
- toolStarted: z$1.object({
13447
- name: z$1.string(),
13448
- id: z$1.string().optional()
13449
- }).optional()
13450
- });
13451
- /**
13452
- * Classifiers the completions `error` frame may carry: the persisted quest codes plus the two
13453
- * provider codes. A separate tuple because QUEST_ERROR_CODES is stored on quests and must not widen.
13454
- */
13455
- const COMPLETION_SSE_ERROR_CODES = [
13456
- ...QUEST_ERROR_CODES,
13457
- "provider_error",
13458
- "provider_rejected",
13459
- "content_blocked"
13460
- ];
13461
- /**
13462
- * The in-band terminal failure frame, sent only once the stream has opened. Failures found before
13463
- * the provider call (auth, scope, validation, rate limit, model, credits) are JSON errors with a
13464
- * real status instead; see completions.contract.ts.
13465
- *
13466
- * `code` is the machine-readable classifier: a billing code when settlement runs out mid-generation,
13467
- * `provider_rejected` / `provider_error` when the model provider fails, `content_blocked` when the
13468
- * model's safety classifier declined the request. Optional; callers must never match on the prose
13469
- * `message`.
13470
- *
13471
- * The field is `code` rather than the conventions' `errorCode` because this frame shipped that way
13472
- * and is a published wire shape.
13473
- */
13474
- const CompletionSseErrorEventSchema = z$1.object({
13475
- type: z$1.literal("error"),
13476
- message: z$1.string(),
13477
- requestId: z$1.string().optional(),
13478
- code: z$1.enum(COMPLETION_SSE_ERROR_CODES).optional()
13479
- });
13480
- /** One `data:` event in the `text/event-stream` completions response. */
13481
- const CompletionStreamEventSchema = z$1.union([
13482
- CompletionMetaEventSchema,
13483
- CompletionContentEventSchema,
13484
- CompletionSseErrorEventSchema
13485
- ]);
13486
13655
  /**
13487
13656
  * Server-side tool execution schemas for POST /api/ai/v1/tools.
13488
13657
  *
@@ -14808,6 +14977,93 @@ const CreateSessionRequestSchema = z$1.object({
14808
14977
  tags: z$1.array(SessionTagSchema).optional(),
14809
14978
  lastUsedModel: z$1.string().nullish()
14810
14979
  });
14980
+ z$2.object(passthroughShape({
14981
+ id: true,
14982
+ createdAt: true,
14983
+ updatedAt: true,
14984
+ name: true,
14985
+ description: true,
14986
+ type: true,
14987
+ organizationId: true
14988
+ })).extend({
14989
+ memberIds: z$2.array(z$2.string()),
14990
+ memberCount: z$2.number()
14991
+ });
14992
+ z$2.object(passthroughShape({
14993
+ id: true,
14994
+ createdAt: true,
14995
+ updatedAt: true,
14996
+ deletedAt: true,
14997
+ sessionId: true,
14998
+ conversationItemId: true,
14999
+ openaiMessageId: true,
15000
+ claudeMessageId: true,
15001
+ type: true,
15002
+ timestamp: true,
15003
+ prompt: true,
15004
+ fabFileIds: true,
15005
+ agentIds: true,
15006
+ reply: true,
15007
+ replies: true,
15008
+ structuredReplies: true,
15009
+ toolResults: true,
15010
+ questMasterReply: true,
15011
+ images: true,
15012
+ videos: true,
15013
+ videoJobIds: true,
15014
+ generationJobs: true,
15015
+ agentVideoClipsClaimed: true,
15016
+ agentMusicTracksClaimed: true,
15017
+ oob: true,
15018
+ promptMeta: true,
15019
+ status: true,
15020
+ creditsUsed: true,
15021
+ errorCode: true,
15022
+ questMasterPlanId: true,
15023
+ agentExecutionId: true,
15024
+ correctsQuestId: true,
15025
+ clientFirstTokenTime: true,
15026
+ routingSource: true,
15027
+ pinned: true,
15028
+ researchModeResults: true,
15029
+ deepResearchState: true,
15030
+ fallbackInfo: true,
15031
+ promptEnhancement: true,
15032
+ pendingAction: true,
15033
+ attachmentNotices: true,
15034
+ attachmentDelivery: true,
15035
+ navigationIntents: true,
15036
+ suggestedChoices: true,
15037
+ attachmentList: true,
15038
+ uiSideEffects: true,
15039
+ jupyterNotebook: true
15040
+ }));
15041
+ const questMasterPlanResponseSchema = z$2.object(passthroughShape({
15042
+ id: true,
15043
+ createdAt: true,
15044
+ updatedAt: true,
15045
+ notebookId: true,
15046
+ goal: true,
15047
+ quests: true,
15048
+ userId: true,
15049
+ visibility: true,
15050
+ state: true,
15051
+ lastAccessedAt: true,
15052
+ sessionHistory: true,
15053
+ metrics: true,
15054
+ tags: true,
15055
+ priority: true,
15056
+ parentPlanId: true,
15057
+ sharedWith: true,
15058
+ handoff: true,
15059
+ blockers: true,
15060
+ decisions: true
15061
+ }));
15062
+ z$2.array(questMasterPlanResponseSchema);
15063
+ z$2.object({
15064
+ success: z$2.literal(true),
15065
+ plan: questMasterPlanResponseSchema
15066
+ }).extend({ metrics: passthroughField });
14811
15067
  z$1.preprocess((val) => {
14812
15068
  if (typeof val === "string") {
14813
15069
  if (val.toLowerCase() === "true") return true;
@@ -16114,8 +16370,8 @@ const ListModelsResponseSchema = paginatedResponseSchema(z$1.object({
16114
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."),
16115
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."),
16116
16372
  supports_tools: z$1.boolean(),
16117
- 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."),
16118
- 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`."),
16119
16375
  supports_vision: z$1.boolean(),
16120
16376
  deprecation_date: z$1.string().nullable().describe("`YYYY-MM-DD`; null when no retirement is scheduled."),
16121
16377
  replaced_by: z$1.string().nullable().describe("The id of the recommended successor, when there is one."),
@@ -16509,7 +16765,12 @@ z$1.object({
16509
16765
  * Session-creation input, not a completion parameter, so it is applied only when `sessionId`
16510
16766
  * is absent; ignored when an existing session is resolved.
16511
16767
  */
16512
- 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()
16513
16774
  });
16514
16775
  /**
16515
16776
  * Response shapes for the public image endpoints (`generateImage` / `editImage`,
@@ -16832,7 +17093,7 @@ defineEndpoint({
16832
17093
  path: "/api/chat",
16833
17094
  operationId: "sendChatMessage",
16834
17095
  summary: "Send a chat message",
16835
- description: "Sends a message to the AI and creates a quest to process it. By default (async) the call returns immediately with a quest id; poll `GET /api/v1/quests/{id}` for the reply. Send `wait: true` to block until the reply is ready and receive it inline. A tool that produced machine-readable state reports it under `toolPayloads` - an array of `{ type, payload }` entries in emission order, alongside (never instead of) the prose reply - on the `wait: true` body and on the polled quest. A turn can FAIL after the ACK - reported on the polled quest as `type: \"error\"`, since the ACK was already sent. A terminal `status: \"stopped\"` (a missing session, a user-cancelled turn) is ALSO a failure even without `type: \"error\"` - it carries an explanatory string in `reply`/`replies` rather than an answer. `type` is the failure signal for the classified failure classes (an abort, a provider timeout, credit exhaustion); `errorCode` is an optional refinement present only when the failure is a classified billing reason. A recovered stuck quest is NOT in that list: one that still has renderable content resolves as a success by design. `QUEST_ERROR_CODES` has two members, but only `insufficient_credits` is raised as a quest errorCode by any current throw site on this endpoint - `spend_cap_exceeded` is thrown only by the embed chat route's pre-flight, which fires outside the process try/catch that would classify it onto a quest. A caller must treat `type: \"error\"` OR a terminal `status: \"stopped\"` as failure even when `errorCode` is absent, and must not read `reply` as an answer without checking those first. Authenticate with an API key (`b4m_live_`) or a JWT. Session resolution: pass `sessionId` to continue an existing notebook. An API-key caller that omits it gets a BRAND-NEW notebook rather than the notebook the human last had open, and `newConversation: true` forces a new notebook for any auth mode; both return the new id as `sessionId`, so the caller can continue the conversation by passing it back. A first-party JWT caller that omits `sessionId` keeps the last-opened-notebook fallback. Sending `sessionId` and `newConversation` together is a 422.",
17096
+ description: "Sends a message to the AI and creates a quest to process it. By default (async) the call returns immediately with a quest id; poll `GET /api/v1/quests/{id}` for the reply. Send `wait: true` to block until the reply is ready and receive it inline. A `wait: true` turn is bounded at about 45 seconds: one that runs longer is ended (unused credits refunded) and answered with a 504 naming the quest in `quest_id`. Send long or tool-heavy turns (large attachments, tool calls) in the default async mode and poll instead. A tool that produced machine-readable state reports it under `toolPayloads` - an array of `{ type, payload }` entries in emission order, alongside (never instead of) the prose reply - on the `wait: true` body and on the polled quest. A turn can FAIL after the ACK - reported on the polled quest as `type: \"error\"`, since the ACK was already sent. A terminal `status: \"stopped\"` (a missing session, a user-cancelled turn) is ALSO a failure even without `type: \"error\"` - it carries an explanatory string in `reply`/`replies` rather than an answer. `type` is the failure signal for the classified failure classes (an abort, a provider timeout, credit exhaustion); `errorCode` is an optional refinement present only when the failure is a classified billing reason. A recovered stuck quest is NOT in that list: one that still has renderable content resolves as a success by design. `QUEST_ERROR_CODES` has two members, but only `insufficient_credits` is raised as a quest errorCode by any current throw site on this endpoint - `spend_cap_exceeded` is thrown only by the embed chat route's pre-flight, which fires outside the process try/catch that would classify it onto a quest. A caller must treat `type: \"error\"` OR a terminal `status: \"stopped\"` as failure even when `errorCode` is absent, and must not read `reply` as an answer without checking those first. Authenticate with an API key (`b4m_live_`) or a JWT. Session resolution: pass `sessionId` to continue an existing notebook. An API-key caller that omits it gets a BRAND-NEW notebook rather than the notebook the human last had open, and `newConversation: true` forces a new notebook for any auth mode; both return the new id as `sessionId`, so the caller can continue the conversation by passing it back. A first-party JWT caller that omits `sessionId` keeps the last-opened-notebook fallback. Sending `sessionId` and `newConversation` together is a 422.",
16836
17097
  tags: ["AI"],
16837
17098
  auth: "apiKeyOrJwt",
16838
17099
  scopes: ["ai:chat", "ai:generate"],
@@ -16873,6 +17134,10 @@ defineEndpoint({
16873
17134
  429: {
16874
17135
  description: "Per-user rate limit exceeded.",
16875
17136
  schema: ApiErrorSchema
17137
+ },
17138
+ 504: {
17139
+ description: "A `wait: true` turn ran past the request deadline. The turn is already ended: its quest (`quest_id`) is settled as timed out with any partial reply kept, and unused reserved credits are refunded. Retry in async mode for a long turn.",
17140
+ schema: ChatWaitTimeoutErrorSchema
16876
17141
  }
16877
17142
  },
16878
17143
  codeSample: {
@@ -17172,7 +17437,7 @@ defineEndpoint({
17172
17437
  path: "/api/ai/v1/completions",
17173
17438
  operationId: "createCompletion",
17174
17439
  summary: "Create a chat completion",
17175
- 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.",
17176
17441
  tags: ["AI"],
17177
17442
  auth: "apiKeyOrJwt",
17178
17443
  scopes: ["ai:chat", "ai:generate"],
@@ -17206,8 +17471,8 @@ defineEndpoint({
17206
17471
  }
17207
17472
  },
17208
17473
  400: {
17209
- 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`.",
17210
- 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
17211
17476
  },
17212
17477
  413: {
17213
17478
  description: "Request body over 25 MB.",
@@ -21039,7 +21304,7 @@ function scanArtifactOpenTag(text, at, leading, memo) {
21039
21304
  end: close + 1
21040
21305
  };
21041
21306
  }
21042
- const OPENER = /<artifact(?=\s)/gi;
21307
+ const OPENER$1 = /<artifact(?=\s)/gi;
21043
21308
  const LEADING_WS = /\s+/y;
21044
21309
  const CLOSER = /<\/artifact>/gi;
21045
21310
  const GT = />/g;
@@ -21054,9 +21319,9 @@ function scanArtifactTags(text, attrsMayCrossLines) {
21054
21319
  const stop = attrsMayCrossLines ? GT : GT_OR_LINE_TERMINATOR;
21055
21320
  const out = [];
21056
21321
  let termAt = -1;
21057
- OPENER.lastIndex = 0;
21322
+ OPENER$1.lastIndex = 0;
21058
21323
  let opener;
21059
- while ((opener = OPENER.exec(text)) !== null) {
21324
+ while ((opener = OPENER$1.exec(text)) !== null) {
21060
21325
  LEADING_WS.lastIndex = opener.index + opener[0].length;
21061
21326
  LEADING_WS.exec(text);
21062
21327
  const attrsStart = LEADING_WS.lastIndex;
@@ -21076,7 +21341,7 @@ function scanArtifactTags(text, attrsMayCrossLines) {
21076
21341
  attrs: text.slice(attrsStart, termAt),
21077
21342
  body: text.slice(termAt + 1, closer.index)
21078
21343
  });
21079
- OPENER.lastIndex = end;
21344
+ OPENER$1.lastIndex = end;
21080
21345
  }
21081
21346
  return out;
21082
21347
  }
@@ -21244,7 +21509,11 @@ function stripDeliveredArtifactBlocks(text, deliveredMarkup) {
21244
21509
  */
21245
21510
  const CLAUDE_4_7_PLUS_MULTIPLIER = 1.5;
21246
21511
  const CLAUDE_4_5_MULTIPLIER = 1.09;
21247
- const CLAUDE_FAMILY_VERSION = /(?:^|\.)claude-(?:opus|sonnet|haiku)-(\d+)(?:-(\d{1,2}))?(?=$|[-:])/;
21512
+ const MEASURED_FAMILIES = /* @__PURE__ */ new Set([
21513
+ "opus",
21514
+ "sonnet",
21515
+ "haiku"
21516
+ ]);
21248
21517
  /**
21249
21518
  * Factor to scale a cl100k_base token count by for `modelId`. 1 for anything unmeasured: non-Claude
21250
21519
  * models, Claude before 4.5 (retired on Bedrock, could not be measured), and Fable (Bedrock rejects it
@@ -21252,10 +21521,10 @@ const CLAUDE_FAMILY_VERSION = /(?:^|\.)claude-(?:opus|sonnet|haiku)-(\d+)(?:-(\d
21252
21521
  * assumption they keep its tokenizer; [BILLING_DRIFT] flags it if one does not.
21253
21522
  */
21254
21523
  function tokenEstimateMultiplier(modelId) {
21255
- const match = modelId?.match(CLAUDE_FAMILY_VERSION);
21256
- if (!match) return 1;
21257
- const major = Number(match[1]);
21258
- const minor = Number(match[2] ?? 0);
21524
+ const parsed = modelId ? parseClaudeModelId(modelId) : null;
21525
+ if (!parsed || !MEASURED_FAMILIES.has(parsed.family)) return 1;
21526
+ const { major } = parsed;
21527
+ const minor = parsed.minor ?? 0;
21259
21528
  if (major > 4 || major === 4 && minor >= 7) return CLAUDE_4_7_PLUS_MULTIPLIER;
21260
21529
  if (major === 4 && minor >= 5) return CLAUDE_4_5_MULTIPLIER;
21261
21530
  return 1;
@@ -23650,111 +23919,273 @@ function getEnvironmentName(configApiConfig) {
23650
23919
  return "Self-Hosted";
23651
23920
  }
23652
23921
  //#endregion
23653
- //#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
23654
24068
  /**
23655
- * 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.
23656
24072
  */
23657
- const ToolCategorySchema = z$1.enum([
23658
- "auto_approve",
23659
- "prompt_always",
23660
- "prompt_default"
24073
+ const UNKNOWN_TOOL_CATEGORY = "prompt_default";
24074
+ const READ_ONLY_EFFECTS = /* @__PURE__ */ new Set([
24075
+ "none",
24076
+ "session",
24077
+ "read"
23661
24078
  ]);
23662
- z$1.object({
23663
- categories: z$1.record(z$1.string(), ToolCategorySchema),
23664
- trustedTools: z$1.array(z$1.string())
23665
- });
23666
- /**
23667
- * Default tool categories
23668
- *
23669
- * Categories:
23670
- * - auto_approve: Safe tools that don't need permission (math, search, datetime)
23671
- * - prompt_always: Dangerous tools that ALWAYS need permission, cannot be trusted (file edits, shell commands)
23672
- * - prompt_default: Tools that prompt by default but users can trust them (file reads, searches)
23673
- */
23674
- const DEFAULT_TOOL_CATEGORIES = {
23675
- math_evaluate: "auto_approve",
23676
- current_datetime: "auto_approve",
23677
- dice_roll: "auto_approve",
23678
- prompt_enhancement: "auto_approve",
23679
- find_definition: "auto_approve",
23680
- ask_user_question: "auto_approve",
23681
- weather_info: "prompt_default",
23682
- agent_delegate: "auto_approve",
23683
- check_agent_status: "auto_approve",
23684
- list_background_agents: "auto_approve",
23685
- cancel_background_agent: "auto_approve",
23686
- resume_agent: "auto_approve",
23687
- create_dynamic_agent: "auto_approve",
23688
- coordinate_task: "auto_approve",
23689
- decompose_task: "auto_approve",
23690
- shared_context_read: "auto_approve",
23691
- shared_context_write: "auto_approve",
23692
- write_todos: "auto_approve",
23693
- read_prior_todos: "auto_approve",
23694
- log_decision: "auto_approve",
23695
- track_blocker: "auto_approve",
23696
- resolve_blocker: "auto_approve",
23697
- request_review_gate: "auto_approve",
23698
- work_item_create: "auto_approve",
23699
- work_item_update: "auto_approve",
23700
- work_item_close: "auto_approve",
23701
- work_item_ready: "auto_approve",
23702
- work_item_graph: "auto_approve",
23703
- tool_search: "auto_approve",
23704
- edit_file: "prompt_always",
23705
- edit_local_file: "prompt_always",
23706
- create_file: "prompt_always",
23707
- delete_file: "prompt_always",
23708
- shell_execute: "prompt_always",
23709
- bash_execute: "prompt_always",
23710
- write_shell_stdin: "prompt_always",
23711
- kill_background_shell: "prompt_always",
23712
- git_commit: "prompt_always",
23713
- git_push: "prompt_always",
23714
- skill: "prompt_always",
23715
- web_search: "prompt_default",
23716
- check_shell_output: "prompt_default",
23717
- list_background_shells: "prompt_default",
23718
- web_fetch: "prompt_default",
23719
- deep_research: "prompt_default",
23720
- file_read: "prompt_default",
23721
- grep_search: "prompt_default",
23722
- glob_files: "prompt_default",
23723
- get_file_tree: "prompt_default",
23724
- get_file_structure: "prompt_default",
23725
- git_status: "prompt_default",
23726
- git_diff: "prompt_default",
23727
- git_log: "prompt_default",
23728
- git_branch: "prompt_default"
23729
- };
23730
24079
  /**
23731
- * Get the category for a tool
23732
- * Returns 'prompt_default' if tool is not in the default categories
23733
- */
23734
- function getToolCategory(toolName, customCategories) {
23735
- if (toolName.startsWith("agent_hook:") || toolName.startsWith("skill_hook:")) return "prompt_always";
23736
- if (customCategories && toolName in customCategories) return customCategories[toolName];
23737
- if (toolName in DEFAULT_TOOL_CATEGORIES) return DEFAULT_TOOL_CATEGORIES[toolName];
23738
- if (toolName.startsWith("mcp__")) return "prompt_always";
23739
- 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));
23740
24133
  }
23741
24134
  /**
23742
- * Check if a tool can be trusted (not prompt_always)
23743
- */
23744
- function canTrustTool(toolName, customCategories) {
23745
- 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
+ };
23746
24155
  }
23747
24156
  /**
23748
- * Check if a tool is read-only (safe for parallel execution).
23749
- * Write tools (prompt_always category) must always be sequential.
23750
- *
23751
- * @param toolName - Name of the tool to check
23752
- * @param customCategories - Optional custom category overrides
23753
- * @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.
23754
24160
  */
23755
- function isReadOnlyTool(toolName, customCategories) {
23756
- return getToolCategory(toolName, customCategories) !== "prompt_always";
23757
- }
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
+ };
23758
24189
  const PROJECT_CONTEXT_FILES = [
23759
24190
  "CLAUDE.local.md",
23760
24191
  "CLAUDE.md",
@@ -24495,7 +24926,7 @@ function mergeConfigs(global, project, local) {
24495
24926
  enabled: merged.tools.enabled.filter((t) => !disabledSet.has(t))
24496
24927
  };
24497
24928
  const globalTrusted = new Set(global.trustedTools || []);
24498
- 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));
24499
24930
  return merged;
24500
24931
  }
24501
24932
  /**
@@ -25172,4 +25603,4 @@ var ConfigStore = class {
25172
25603
  }
25173
25604
  };
25174
25605
  //#endregion
25175
- export { SLUG_OWNED_BY_OTHER as $, BRIEFCASE_CATALOG_QUERIES as A, ttsBase64ResponseSchema as At, ImageModels as B, selfClaimedActorKindSchema as Bt, AGENT_QUEST_ID as C, secureParameters as Ct, ARTIFACT_REMOVED_PLACEHOLDER as D, stripToolArtifactMarkup as Dt, ARTIFACT_DELIVERED_PLACEHOLDER as E, stripDeliveredArtifactBlocks as Et, ChatModels as F, DELEGATION_PAYLOAD_SCHEMA_NAME as Ft, ModelBackend as G, LOCATION_MAP_LANGUAGE as H, WEBSITE_URL as Ht, CompletionReasoningEffortSchema as I, actorColorIndex as It, OpenAIEmbeddingModel as J, NotFoundError as K, DEFAULT_TTS_PROVIDER as L, actorKindMarker as Lt, BriefcasePromptIdSchema as M, ttsResponseTooLargeSchema as Mt, CONTEXT_WINDOW_SAFETY_BUFFER_TOKENS as N, withRetry as Nt, ApiKeyType as O, supportedVoiceGenerationVendor as Ot, CREDIT_DEDUCT_TRANSACTION_TYPES as P, ACTOR_COLOR_SLOTS as Pt, SEARCH_RESULT_CARDS_LANGUAGE as Q, DEGENERATE_FINISH_REASON as R, actorKindSchema as Rt, escapeArtifactBodyJson as S, replacePromptVariables as St, AGENT_QUEST_MCP_URI as T, splitEquals as Tt, MODEL_INFO_FIELD_GROUP_OF as U, ImagePromptResolutionSchema as V, APP_NAME as Vt, McpServerName as W, PermissionDeniedError as X, PROMPT_TEXT_MAX as Y, REVIEW_GATE_STATUS_VALUES as Z, logger as _, isRetryableError as _t, getToolCategory as a, assertAuthOriginMatchesApiKey as at, htmlErrorTitle as b, obfuscateApiKey as bt, getCreditsUrl as c, escapeThinkMarkers as ct, resolveApiEndpoint as d, getMcpProviderMetadata as dt, SUBQUEST_STATUS_VALUES as et, AuthenticatedApiClient as f, getQuestErrorCode as ft, parseApiUrl as g, isPlaceholderImageSigningSecret as gt, NotAuthenticatedError as h, isEarlyStop as ht, canTrustTool as i, WORK_ITEM_STATUSES as it, BedrockEmbeddingModel as j, ttsRequestSchema as jt, ArtifactTypeSchema as k, tokenEstimateMultiplier as kt, getEnvironmentName as l, filterToolArtifactMarkup as lt, LOCAL_DEV_URL as m, hasDeliverablePinnedArtifact as mt, extractCompactInstructions as n, VisibilitySchema as nt, isReadOnlyTool as o, buildPromptContext as ot, DeviceFlowClient as p, googleMapsSearchUrl as pt, OllamaEmbeddingModel as q, loadContextFiles as r, VoyageAIEmbeddingModel as rt, ApiEndpointUnconfiguredError as s, createThinkMarkerEscaper as st, ConfigStore as t, SupportedFabFileMimeTypes as tt, requireApiUrl as u, generatedAudioResponseSchema as ut, SESSION_ID_PATTERN as v, isUserInitiatedAbort as vt, AGENT_QUEST_MANIFEST as w, signImageUrl as wt, htmlFirstH1 as x, questReplyText as xt, isValidSessionId as y, joinReplySlots as yt, GENERATED_IMAGE_EXTENSION_RE as z, delegationPayloadSchema 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 };