@2kw/ai 7.1.0-dev.2 → 7.1.0-dev.3

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.
@@ -11,8 +11,8 @@ export declare function redactSecrets(tools: Record<string, unknown>[] | undefin
11
11
  envNames: string[];
12
12
  };
13
13
  /**
14
- * Tool entries the backend stores but never runs (planner/runtime-injected or executor-less,
15
- * e.g. a console-stored `web_search`). agent.yaml cannot author them, so export drops them.
14
+ * Tool entries the backend stores but never runs as authored (planner- or runtime-injected,
15
+ * e.g. `backbone.tool_search`). agent.yaml cannot author them, so export drops them.
16
16
  */
17
17
  export declare function dropInertTools(tools: Record<string, unknown>[] | undefined): {
18
18
  tools: Record<string, unknown>[] | undefined;
@@ -43,8 +43,8 @@ export function redactSecrets(tools) {
43
43
  return { tools: redacted, envNames: [...owners.keys()] };
44
44
  }
45
45
  /**
46
- * Tool entries the backend stores but never runs (planner/runtime-injected or executor-less,
47
- * e.g. a console-stored `web_search`). agent.yaml cannot author them, so export drops them.
46
+ * Tool entries the backend stores but never runs as authored (planner- or runtime-injected,
47
+ * e.g. `backbone.tool_search`). agent.yaml cannot author them, so export drops them.
48
48
  */
49
49
  export function dropInertTools(tools) {
50
50
  if (!tools)
@@ -138,6 +138,77 @@ export declare const agentSchema: {
138
138
  };
139
139
  };
140
140
  };
141
+ readonly webSearchTool: {
142
+ readonly type: "object";
143
+ readonly additionalProperties: false;
144
+ readonly required: readonly ["type"];
145
+ readonly properties: {
146
+ readonly type: {
147
+ readonly const: "web_search";
148
+ };
149
+ readonly config: {
150
+ readonly type: "object";
151
+ readonly properties: {
152
+ readonly source: {
153
+ readonly enum: readonly ["builtin", "external", "native"];
154
+ };
155
+ readonly searchProviderId: {
156
+ readonly type: readonly ["string", "null"];
157
+ };
158
+ readonly fallback: {
159
+ readonly enum: readonly ["builtin", "external"];
160
+ };
161
+ readonly allowedDomains: {
162
+ readonly type: "array";
163
+ readonly items: {
164
+ readonly type: "string";
165
+ readonly pattern: "^(?=.{1,253}$)([\\p{L}\\p{N}](?:[\\p{L}\\p{N}-]{0,61}[\\p{L}\\p{N}])?\\.)+\\p{L}(?:[\\p{L}\\p{N}-]{0,61}[\\p{L}\\p{N}])?$";
166
+ };
167
+ readonly maxItems: 10;
168
+ };
169
+ };
170
+ readonly if: {
171
+ readonly anyOf: readonly [{
172
+ readonly required: readonly ["source"];
173
+ readonly properties: {
174
+ readonly source: {
175
+ readonly const: "external";
176
+ };
177
+ };
178
+ }, {
179
+ readonly required: readonly ["source", "fallback"];
180
+ readonly properties: {
181
+ readonly source: {
182
+ readonly const: "native";
183
+ };
184
+ readonly fallback: {
185
+ readonly const: "external";
186
+ };
187
+ };
188
+ }];
189
+ };
190
+ readonly then: {
191
+ readonly required: readonly ["searchProviderId"];
192
+ readonly properties: {
193
+ readonly searchProviderId: {
194
+ readonly type: "string";
195
+ readonly pattern: "\\S";
196
+ };
197
+ };
198
+ };
199
+ readonly else: {
200
+ readonly properties: {
201
+ readonly searchProviderId: {
202
+ readonly type: "null";
203
+ };
204
+ };
205
+ };
206
+ };
207
+ readonly requiresConfirmation: {
208
+ readonly type: "boolean";
209
+ };
210
+ };
211
+ };
141
212
  readonly extractionTool: {
142
213
  readonly type: "object";
143
214
  readonly additionalProperties: false;
@@ -306,6 +377,18 @@ export declare const agentSchema: {
306
377
  readonly then: {
307
378
  readonly $ref: "#/$defs/fileSearchTool";
308
379
  };
380
+ }, {
381
+ readonly if: {
382
+ required: string[];
383
+ properties: {
384
+ type: {
385
+ const: string;
386
+ };
387
+ };
388
+ };
389
+ readonly then: {
390
+ readonly $ref: "#/$defs/webSearchTool";
391
+ };
309
392
  }, {
310
393
  readonly if: {
311
394
  required: string[];
@@ -16,6 +16,9 @@ const MODEL_REF = {
16
16
  maxLength: 255,
17
17
  pattern: "^(?:[^/#]+|[A-Za-z0-9-]+/[^#]+)$",
18
18
  };
19
+ // A plain hostname (no scheme, path, port or wildcard): dot-separated labels of letters, digits and
20
+ // inner hyphens, Unicode or ASCII. Mirrors WebToolEntryValidator.HOST; needs a Unicode-aware regex engine.
21
+ const HOSTNAME = "^(?=.{1,253}$)([\\p{L}\\p{N}](?:[\\p{L}\\p{N}-]{0,61}[\\p{L}\\p{N}])?\\.)+\\p{L}(?:[\\p{L}\\p{N}-]{0,61}[\\p{L}\\p{N}])?$";
19
22
  const oneOfLowercase = (...values) => ({ type: "string", enum: values });
20
23
  /** Types the backend injects or cannot execute; authoring them has no effect or breaks the run. */
21
24
  export const NON_AUTHORABLE_TOOL_TYPES = {
@@ -27,10 +30,10 @@ export const NON_AUTHORABLE_TOOL_TYPES = {
27
30
  "backbone.deliver": "it is added at run time while sandbox outputs are held",
28
31
  // #1244: injected wherever backbone.bash is advertised; an authored copy is dropped.
29
32
  "backbone.view_image": "it is added at run time wherever the sandboxed shell is",
30
- web_search: "it has no executor and is skipped",
31
33
  };
32
34
  const BUILTIN_TYPES = [
33
35
  "file_search",
36
+ "web_search",
34
37
  "backbone.extraction",
35
38
  "backbone.document_convert",
36
39
  "backbone.todo",
@@ -118,6 +121,35 @@ export const agentSchema = {
118
121
  },
119
122
  },
120
123
  },
124
+ // A missing config, `config: {}` or a missing source runs the built-in search. Mirrors
125
+ // WebToolEntryValidator; `config` stays open so later sources' fields do not break older CLIs.
126
+ webSearchTool: {
127
+ type: "object",
128
+ additionalProperties: false,
129
+ required: ["type"],
130
+ properties: {
131
+ type: { const: "web_search" },
132
+ config: {
133
+ type: "object",
134
+ properties: {
135
+ source: { enum: ["builtin", "external", "native"] },
136
+ searchProviderId: { type: ["string", "null"] },
137
+ fallback: { enum: ["builtin", "external"] },
138
+ allowedDomains: { type: "array", items: { type: "string", pattern: HOSTNAME }, maxItems: 10 },
139
+ },
140
+ // A provider id is required exactly when an external provider serves the search.
141
+ if: {
142
+ anyOf: [
143
+ { required: ["source"], properties: { source: { const: "external" } } },
144
+ { required: ["source", "fallback"], properties: { source: { const: "native" }, fallback: { const: "external" } } },
145
+ ],
146
+ },
147
+ then: { required: ["searchProviderId"], properties: { searchProviderId: { type: "string", pattern: "\\S" } } },
148
+ else: { properties: { searchProviderId: { type: "null" } } },
149
+ },
150
+ requiresConfirmation: { type: "boolean" },
151
+ },
152
+ },
121
153
  extractionTool: {
122
154
  type: "object",
123
155
  additionalProperties: false,
@@ -204,6 +236,7 @@ export const agentSchema = {
204
236
  type: "object",
205
237
  allOf: [
206
238
  { if: typeIs("file_search"), then: { $ref: "#/$defs/fileSearchTool" } },
239
+ { if: typeIs("web_search"), then: { $ref: "#/$defs/webSearchTool" } },
207
240
  { if: typeIs("backbone.extraction"), then: { $ref: "#/$defs/extractionTool" } },
208
241
  { if: typeIs("backbone.document_convert"), then: { $ref: "#/$defs/documentConvertTool" } },
209
242
  { if: typeIs("backbone.todo"), then: { $ref: "#/$defs/todoTool" } },
@@ -26,6 +26,9 @@ tools: []
26
26
  # - type: file_search
27
27
  # config:
28
28
  # knowledgeBaseIds: [kb_id]
29
+ # - type: web_search
30
+ # config:
31
+ # allowedDomains: [example.com]
29
32
  # - type: backbone.extraction
30
33
  # config:
31
34
  # schemaIds: [schema_id]
@@ -6,6 +6,8 @@ const validate = ajv.compile(agentSchema);
6
6
  const NULLABLE_KEYS = ["description", "models", "instructions", "tools", "hitlPolicy", "options", "skills"];
7
7
  /** Tool-rule values the backend matches case-insensitively; the actions (approve, auto, deny) are exact. */
8
8
  const TOOL_RULE_CLASSES = new Set(["read", "write", "destructive"]);
9
+ /** web_search config keys whose explicit `null` the backend reads as absent. */
10
+ const WEB_SEARCH_NULLABLE_KEYS = ["source", "fallback", "searchProviderId", "allowedDomains"];
9
11
  const isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
10
12
  /**
11
13
  * A deep copy of `doc` in the form the backend actually checks: explicit nulls it treats as absent
@@ -31,6 +33,8 @@ export function normalizeForValidation(doc) {
31
33
  for (const tool of copy.tools) {
32
34
  if (isPlainObject(tool) && tool.annotations === null)
33
35
  delete tool.annotations;
36
+ if (isPlainObject(tool) && tool.type === "web_search")
37
+ normalizeWebSearch(tool);
34
38
  }
35
39
  }
36
40
  const policy = copy.hitlPolicy;
@@ -50,6 +54,21 @@ export function normalizeForValidation(doc) {
50
54
  }
51
55
  return copy;
52
56
  }
57
+ /** WebToolEntryValidator reads a null config and null config fields as absent, and trims each domain. */
58
+ function normalizeWebSearch(tool) {
59
+ if (tool.config === null)
60
+ delete tool.config;
61
+ const config = tool.config;
62
+ if (!isPlainObject(config))
63
+ return;
64
+ for (const key of WEB_SEARCH_NULLABLE_KEYS) {
65
+ if (config[key] === null)
66
+ delete config[key];
67
+ }
68
+ if (Array.isArray(config.allowedDomains)) {
69
+ config.allowedDomains = config.allowedDomains.map((d) => (typeof d === "string" ? d.trim() : d));
70
+ }
71
+ }
53
72
  function describe(e) {
54
73
  // if/then wrappers restate the real error one level up; drop them. `not` only marks
55
74
  // non-authorable tool types, which validateAgentConfig reports with the reason.
@@ -75,6 +94,13 @@ function describe(e) {
75
94
  if (e.keyword === "const") {
76
95
  return { pointer, message: `must be ${JSON.stringify(params.allowedValue)}` };
77
96
  }
97
+ // The `else` branch of webSearchTool's provider rule: an id where no external provider serves the search.
98
+ if (e.keyword === "type" && params.type === "null" && pointer.endsWith("/config/searchProviderId")) {
99
+ return {
100
+ pointer,
101
+ message: "only allowed when an external provider serves the search (source external, or native with fallback external)",
102
+ };
103
+ }
78
104
  return { pointer, message: e.message ?? e.keyword };
79
105
  }
80
106
  export function validateAgentConfig(raw) {
@@ -98,11 +124,18 @@ export function validateAgentConfig(raw) {
98
124
  }
99
125
  }
100
126
  if (Array.isArray(record.tools)) {
127
+ let webSearchSeen = false;
101
128
  record.tools.forEach((tool, i) => {
102
129
  const type = tool?.type;
103
130
  if (typeof type === "string" && Object.hasOwn(NON_AUTHORABLE_TOOL_TYPES, type)) {
104
131
  issues.push({ pointer: `/tools/${i}/type`, message: `type "${type}" cannot be authored: ${NON_AUTHORABLE_TOOL_TYPES[type]}` });
105
132
  }
133
+ // The backend refuses a second envelope (WebToolEntryValidator).
134
+ if (type === "web_search") {
135
+ if (webSearchSeen)
136
+ issues.push({ pointer: `/tools/${i}/type`, message: "an agent may carry only one web_search tool" });
137
+ webSearchSeen = true;
138
+ }
106
139
  });
107
140
  }
108
141
  if (Array.isArray(record.skills)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2kw/ai",
3
- "version": "7.1.0-dev.2",
3
+ "version": "7.1.0-dev.3",
4
4
  "description": "CLI for 2kw.ai — schema-driven document extraction, an OpenAI-compatible EU LLM gateway, transcription, prompts, datasets, and experiments from your terminal or agentic workflows. Ships as 2kw, backbone, and bb.",
5
5
  "keywords": [
6
6
  "cli",