@vellumai/assistant 0.12.6-dev.202609292019.a6f6d85 → 0.12.6-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example CHANGED
@@ -2,8 +2,6 @@
2
2
  ANTHROPIC_API_KEY=sk-ant-...
3
3
  OPENAI_API_KEY=sk-...
4
4
  GEMINI_API_KEY=...
5
- # Optional. IO Intelligence (io.net) open-models endpoint.
6
- IONET_API_KEY=...
7
5
  # Optional. TypeSafe / Jev System One decision model.
8
6
  TYPESAFE_API_KEY=...
9
7
  # Optional for authenticated/proxied Ollama deployments
package/openapi.yaml CHANGED
@@ -6025,7 +6025,6 @@ paths:
6025
6025
  - minimax
6026
6026
  - atlascloud
6027
6027
  - baseten
6028
- - ionet
6029
6028
  - poolside
6030
6029
  connectionName:
6031
6030
  type: string
@@ -16974,8 +16973,8 @@ paths:
16974
16973
  type: string
16975
16974
  description:
16976
16975
  "Filter by provider id. One of: anthropic, openai, gemini, ollama, fireworks, together, openrouter,
16977
- vercel-ai-gateway, litellm, opencode, openai-compatible, minimax, atlascloud, baseten, ionet, poolside,
16978
- typesafe, vellum"
16976
+ vercel-ai-gateway, litellm, opencode, openai-compatible, minimax, atlascloud, baseten, poolside, typesafe,
16977
+ vellum"
16979
16978
  responses:
16980
16979
  "200":
16981
16980
  description: Successful response
@@ -17247,8 +17246,8 @@ paths:
17247
17246
  type: string
17248
17247
  description:
17249
17248
  "Filter by provider. One of: anthropic, openai, gemini, ollama, fireworks, together, openrouter,
17250
- vercel-ai-gateway, litellm, opencode, openai-compatible, minimax, atlascloud, baseten, ionet, poolside,
17251
- typesafe, vellum, chatgpt"
17249
+ vercel-ai-gateway, litellm, opencode, openai-compatible, minimax, atlascloud, baseten, poolside, typesafe,
17250
+ vellum, chatgpt"
17252
17251
  responses:
17253
17252
  "200":
17254
17253
  description: Successful response
@@ -39426,7 +39425,6 @@ components:
39426
39425
  - minimax
39427
39426
  - atlascloud
39428
39427
  - baseten
39429
- - ionet
39430
39428
  - poolside
39431
39429
  - typesafe
39432
39430
  - vellum
@@ -39691,7 +39689,6 @@ components:
39691
39689
  - minimax
39692
39690
  - atlascloud
39693
39691
  - baseten
39694
- - ionet
39695
39692
  - poolside
39696
39693
  connectionName:
39697
39694
  type: string
@@ -40080,7 +40077,6 @@ components:
40080
40077
  - minimax
40081
40078
  - atlascloud
40082
40079
  - baseten
40083
- - ionet
40084
40080
  - poolside
40085
40081
  - type: "null"
40086
40082
  connectionName:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.12.6-dev.202609292019.a6f6d85",
3
+ "version": "0.12.6-staging.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -148,19 +148,6 @@
148
148
  "ref": "2d293cdcb68af03d446b61a9915a0ee3dc842d4e"
149
149
  }
150
150
  },
151
- {
152
- "name": "contractor-photo-to-quote",
153
- "description": "Turns job-site and equipment photos into standardized trade estimates and quotes in Jobber or QuickBooks with multi-tier Good/Better/Best options in 5 minutes.",
154
- "category": "commerce",
155
- "icon": "📸",
156
- "license": "MIT",
157
- "homepage": "https://github.com/ZeebBoyBlue/contractor-photo-to-quote",
158
- "source": {
159
- "source": "github",
160
- "repo": "ZeebBoyBlue/contractor-photo-to-quote",
161
- "ref": "6c136186faaf03bcf125d202f0b1f51f97d94544"
162
- }
163
- },
164
151
  {
165
152
  "name": "contractor-voice-invoicer",
166
153
  "description": "Converts 20-second truck voice memos and job photos into itemized client invoices with custom labor rates, material markups, and one-click accounting staging.",
@@ -487,19 +474,6 @@
487
474
  "license": "MIT",
488
475
  "icon": "\ud83d\udcac"
489
476
  },
490
- {
491
- "name": "quickbooks-expense-categorizer",
492
- "description": "Scans uncategorized QuickBooks Online expenses and bank feeds, matches receipts, and stages categorized entries with confidence scoring for 1-click batch review.",
493
- "category": "commerce",
494
- "icon": "🧾",
495
- "license": "MIT",
496
- "homepage": "https://github.com/ZeebBoyBlue/quickbooks-expense-categorizer",
497
- "source": {
498
- "source": "github",
499
- "repo": "ZeebBoyBlue/quickbooks-expense-categorizer",
500
- "ref": "120776619698cc3e9490caf5c756465f5caaa6c2"
501
- }
502
- },
503
477
  {
504
478
  "name": "reading-pal",
505
479
  "source": {
@@ -560,6 +560,14 @@
560
560
  "description": "Streams an interactive virtual desktop from a platform-hosted, containerized assistant over /v1/desktop/stream. Requires platform hosting; self-hosted assistants are excluded. Off: the virtual desktop is hidden. On: the chat header offers its viewer and controls.",
561
561
  "defaultEnabled": false
562
562
  },
563
+ {
564
+ "id": "assistant-inbox",
565
+ "scope": "client",
566
+ "key": "assistant-inbox",
567
+ "label": "Assistant Inbox",
568
+ "description": "Shows an Assistant Inbox entry above Preferences in the web sidebar and the /assistant/inbox page it opens: a mailbox for the assistant's managed vellum.me address (received and sent, with a reading pane), the inline email setup card for entitled orgs with no address yet, and an upgrade card for orgs without the managed_email entitlement. With it on, email setup happens only there: the billing wizard hands off to the inbox instead of its own domain step, the billing page's finish-setup nudge gives way to the inbox's rail entry, and the Channels page's Email section points there instead of registering an address itself. Off hides the entry, redirects the route to chat, and keeps the older setup surfaces.",
569
+ "defaultEnabled": false
570
+ },
563
571
  {
564
572
  "id": "sidebar-done",
565
573
  "scope": "both",
@@ -74,7 +74,6 @@ export const KNOWN_LLM_PROVIDERS = [
74
74
  "litellm",
75
75
  "opencode",
76
76
  "baseten",
77
- "ionet",
78
77
  "poolside",
79
78
  "typesafe",
80
79
  // Routing identities: "vellum" = the platform-managed route (upstream
@@ -35,7 +35,6 @@ import { AtlasCloudProvider } from "../atlascloud/client.js";
35
35
  import { BasetenProvider } from "../baseten/client.js";
36
36
  import { FireworksProvider } from "../fireworks/client.js";
37
37
  import { GeminiProvider } from "../gemini/client.js";
38
- import { IonetProvider } from "../ionet/client.js";
39
38
  import { JevProvider } from "../jev/client.js";
40
39
  import { MinimaxProvider } from "../minimax/client.js";
41
40
  import { PROVIDER_CATALOG } from "../model-catalog.js";
@@ -207,11 +206,6 @@ const ADAPTER_FACTORIES: Record<string, AdapterFactory> = {
207
206
  streamTimeoutMs,
208
207
  ...(baseURL ? { baseURL } : {}),
209
208
  }),
210
- ionet: ({ apiKey, model, streamTimeoutMs, baseURL }) =>
211
- new IonetProvider(apiKey, model, {
212
- streamTimeoutMs,
213
- ...(baseURL ? { baseURL } : {}),
214
- }),
215
209
  typesafe: ({ apiKey, model, streamTimeoutMs, baseURL }) =>
216
210
  new JevProvider(apiKey, model, {
217
211
  streamTimeoutMs,
@@ -2746,46 +2746,6 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
2746
2746
  apiKeyUrl: "https://app.baseten.co/settings/api_keys",
2747
2747
  apiKeyPlaceholder: "Your Baseten API key",
2748
2748
  },
2749
- {
2750
- id: "ionet",
2751
- displayName: "IO Intelligence",
2752
- subtitle:
2753
- "Open models served by IO Intelligence, io.net's inference platform (OpenAI-compatible). Requires an IO Intelligence API key.",
2754
- setupMode: "api-key",
2755
- setupHint:
2756
- "Enter your IO Intelligence API key to enable IO Intelligence models.",
2757
- envVar: "IONET_API_KEY",
2758
- credentialsGuide: {
2759
- description: "Sign in to io.net and create an IO Intelligence API key.",
2760
- url: "https://io.net/docs/guides/intelligence/api-keys-and-secrets",
2761
- linkLabel: "Open io.net",
2762
- },
2763
- // Model ids are Hugging Face-style `org/name`. Context windows and tool
2764
- // support come from the endpoint's live `GET /models` catalog, and every
2765
- // entry is accessible at the basic access tier.
2766
- models: [
2767
- {
2768
- id: "meta-llama/Llama-3.3-70B-Instruct",
2769
- displayName: "Llama 3.3 70B Instruct",
2770
- contextWindowTokens: 128000,
2771
- defaultContextWindowTokens: 128000,
2772
- maxOutputTokens: 4096,
2773
- supportsToolUse: true,
2774
- },
2775
- {
2776
- id: "openai/gpt-oss-20b",
2777
- displayName: "GPT-OSS 20B",
2778
- contextWindowTokens: 64000,
2779
- defaultContextWindowTokens: 64000,
2780
- maxOutputTokens: 32768,
2781
- supportsThinking: true,
2782
- supportsToolUse: true,
2783
- },
2784
- ],
2785
- defaultModel: "meta-llama/Llama-3.3-70B-Instruct",
2786
- apiKeyUrl: "https://io.net/docs/guides/intelligence/api-keys-and-secrets",
2787
- apiKeyPlaceholder: "Your IO Intelligence API key",
2788
- },
2789
2749
  {
2790
2750
  id: "poolside",
2791
2751
  displayName: "Poolside",
@@ -212,12 +212,6 @@ export interface OpenAIChatCompletionsProviderOptions {
212
212
  * (Fireworks, Together) keep sending `none` / forced choices. Enabled for
213
213
  * the generic `openai-compatible` adapter, whose upstream is unknown. */
214
214
  omitToolChoiceWhenReasoning?: boolean;
215
- /** Send `tool_choice: "auto"` whenever tools are offered and the caller
216
- * left the choice unspecified. Strict OpenAI-compatible upstreams default
217
- * an unspecified `tool_choice` to "none" (io.net documents exactly that),
218
- * which would leave the model unable to call any tool; OpenAI's own
219
- * default is "auto", so this option only exists for upstreams that differ. */
220
- defaultToolChoiceAuto?: boolean;
221
215
  /** Wire field for the output-token limit. OpenAI and OpenAI-compatible
222
216
  * backends use `max_completion_tokens`. OpenRouter defaults to
223
217
  * `max_tokens` because its parameter router matches that key on
@@ -870,7 +864,6 @@ export class OpenAIChatCompletionsProvider implements Provider {
870
864
  private coerceObjectArgsToJsonString: boolean;
871
865
  private salvageXmlToolCalls: boolean;
872
866
  private omitToolChoiceWhenReasoning: boolean;
873
- private defaultToolChoiceAuto: boolean;
874
867
  private outputTokenLimitField: "max_completion_tokens" | "max_tokens";
875
868
 
876
869
  constructor(
@@ -903,7 +896,6 @@ export class OpenAIChatCompletionsProvider implements Provider {
903
896
  options.salvageXmlToolCalls ?? shouldSalvageXmlToolCalls(model);
904
897
  this.omitToolChoiceWhenReasoning =
905
898
  options.omitToolChoiceWhenReasoning ?? false;
906
- this.defaultToolChoiceAuto = options.defaultToolChoiceAuto ?? false;
907
899
  this.outputTokenLimitField =
908
900
  options.outputTokenLimitField ?? "max_completion_tokens";
909
901
  }
@@ -966,15 +958,6 @@ export class OpenAIChatCompletionsProvider implements Provider {
966
958
  maxTokens;
967
959
  }
968
960
 
969
- // Per-conversation prompt-cache key (OpenAI Chat Completions
970
- // `prompt_cache_key`). `RetryProvider` stamps it from the durable
971
- // conversation id for providers in PROMPT_CACHE_KEY_PROVIDERS; omitted
972
- // from the wire when absent.
973
- const promptCacheKey = configObj?.promptCacheKey;
974
- if (typeof promptCacheKey === "string" && promptCacheKey.length > 0) {
975
- params.prompt_cache_key = promptCacheKey;
976
- }
977
-
978
961
  // Profile-scoped token biasing (e.g. the `suppress-cjk` preset). Resolved
979
962
  // and gated to this provider family upstream in `RetryProvider`.
980
963
  if (logitBias) {
@@ -1045,12 +1028,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
1045
1028
  // wire. Catalog providers that honor `none` / forced choices still
1046
1029
  // receive them; the generic openai-compatible adapter drops every
1047
1030
  // explicit value in thinking mode via `omitToolChoiceWhenReasoning`.
1048
- const callerToolChoice = mapNeutralToolChoice(configObj?.tool_choice);
1049
- const toolChoice =
1050
- callerToolChoice ??
1051
- (this.defaultToolChoiceAuto
1052
- ? ("auto" as OpenAI.Chat.Completions.ChatCompletionToolChoiceOption)
1053
- : undefined);
1031
+ const toolChoice = mapNeutralToolChoice(configObj?.tool_choice);
1054
1032
  xmlToolCallSalvageEnabled =
1055
1033
  this.salvageXmlToolCalls && toolChoice !== "none";
1056
1034
  if (toolChoice !== undefined) {
@@ -78,16 +78,10 @@ const USAGE_ATTRIBUTION_HEADER_NAMES = {
78
78
  subagentSpawnMode: "X-Vellum-Subagent-Spawn-Mode",
79
79
  } as const;
80
80
 
81
- /** Providers whose transports consume `promptCacheKey` (OpenAI Responses /
82
- * Chat Completions `prompt_cache_key`); `RetryProvider` derives it from
83
- * `selectionSeed` for these only. `openai-compatible` is included so
84
- * self-hosted bridges (e.g. a Claude Code shim) receive a stable
85
- * per-conversation key and can pin server-side sessions / prompt caches to it. */
86
- const PROMPT_CACHE_KEY_PROVIDERS = new Set([
87
- "openai",
88
- "openrouter",
89
- "openai-compatible",
90
- ]);
81
+ /** Providers whose transports consume `promptCacheKey` (OpenAI Responses
82
+ * `prompt_cache_key`); `RetryProvider` derives it from `selectionSeed` for
83
+ * these only. */
84
+ const PROMPT_CACHE_KEY_PROVIDERS = new Set(["openai", "openrouter"]);
91
85
 
92
86
  /** Providers that support the `effort` config (extended thinking / reasoning). */
93
87
  const EFFORT_SUPPORTED_PROVIDERS = new Set([
@@ -30,7 +30,6 @@ import {
30
30
  import {
31
31
  computeLegacyManifestSha256,
32
32
  computeManifestChecksum,
33
- MANIFEST_MAX_BYTES,
34
33
  } from "../vbundle-validator.js";
35
34
  import { defaultV1Options } from "./v1-test-helpers.js";
36
35
 
@@ -203,16 +202,15 @@ describe("readAndValidateManifest — negative paths", () => {
203
202
  });
204
203
 
205
204
  test("throws manifest_too_large and fails fast before draining the whole body", async () => {
206
- // Fake a tar entry whose body would emit 10x the cap if fully drained.
207
- // The validator must destroy() the stream and throw the moment the
208
- // running byte count crosses MANIFEST_MAX_BYTES; it must NOT keep
209
- // pulling chunks.
205
+ // Fake a tar entry whose body would emit 5 MiB if fully drained. The
206
+ // validator must destroy() the stream and throw the moment the running
207
+ // byte count crosses the 1 MiB cap — it must NOT keep pulling chunks.
210
208
  //
211
209
  // We count bytes emitted via a _read implementation, and after the
212
- // throw assert both that destroy() fired and that far less than the
213
- // full body was ever pulled out of the stream.
214
- const CHUNK = MANIFEST_MAX_BYTES / 2;
215
- const TOTAL_CHUNKS = 20;
210
+ // throw assert both that destroy() fired and that far fewer than 5 MiB
211
+ // were ever pulled out of the stream.
212
+ const CHUNK = 512 * 1024; // 512 KiB
213
+ const TOTAL_CHUNKS = 10; // 5 MiB worth — way past the 1 MiB cap
216
214
  let chunksEmitted = 0;
217
215
  let bytesEmitted = 0;
218
216
  const body = new Readable({
@@ -247,34 +245,11 @@ describe("readAndValidateManifest — negative paths", () => {
247
245
  expect(err).toBeInstanceOf(StreamingValidationError);
248
246
  expect(err?.code).toBe("manifest_too_large");
249
247
  // Fail-fast assertions: destroy() was called, and we didn't drain past
250
- // the cap + one chunk. If the validator had drained to EOF we'd see all
251
- // 20 chunks here.
248
+ // ~1 MiB + one chunk. If the validator had drained to EOF we'd see the
249
+ // full 5 MiB / 10 chunks here.
252
250
  expect(body.destroyed).toBe(true);
253
251
  expect(chunksEmitted).toBeLessThanOrEqual(3);
254
- expect(bytesEmitted).toBeLessThan(2 * MANIFEST_MAX_BYTES);
255
- });
256
-
257
- test("throws bundle_too_many_entries when manifest.contents exceeds maxEntries", async () => {
258
- const { archive } = buildVBundle({
259
- files: [
260
- { path: "data/db/assistant.db", data: new Uint8Array() },
261
- { path: "workspace/a.txt", data: new TextEncoder().encode("a") },
262
- { path: "workspace/b.txt", data: new TextEncoder().encode("b") },
263
- ],
264
- ...defaultV1Options(),
265
- });
266
- const { entry, drainRest } = await firstEntryOf(archive);
267
-
268
- let err: StreamingValidationError | null = null;
269
- try {
270
- await readAndValidateManifest(entry, { maxEntries: 2 });
271
- } catch (e) {
272
- err = e as StreamingValidationError;
273
- }
274
- await drainRest();
275
-
276
- expect(err).toBeInstanceOf(StreamingValidationError);
277
- expect(err?.code).toBe("bundle_too_many_entries");
252
+ expect(bytesEmitted).toBeLessThan(2 * 1024 * 1024);
278
253
  });
279
254
 
280
255
  test("throws manifest_malformed when manifest body is not valid JSON", async () => {
@@ -19,8 +19,6 @@ import {
19
19
  canonicalizeJson,
20
20
  computeLegacyManifestSha256,
21
21
  computeManifestChecksum,
22
- MANIFEST_MAX_BYTES,
23
- MANIFEST_MAX_ENTRIES,
24
22
  ManifestSchema,
25
23
  validateVBundle,
26
24
  } from "../vbundle-validator.js";
@@ -176,52 +174,6 @@ describe("ManifestSchema — v1 acceptance", () => {
176
174
  });
177
175
  });
178
176
 
179
- describe("validateVBundle: streaming manifest limits", () => {
180
- test("rejects a manifest over MANIFEST_MAX_BYTES with MANIFEST_TOO_LARGE", () => {
181
- const oversized = new Uint8Array(MANIFEST_MAX_BYTES + 1).fill(0x20);
182
- const archive = gzipSync(
183
- tarArchive([{ name: "manifest.json", data: oversized }]),
184
- );
185
- const result = validateVBundle(archive, { enforceStreamingLimits: true });
186
- expect(result.is_valid).toBe(false);
187
- expect(result.errors.map((e) => e.code)).toEqual(["MANIFEST_TOO_LARGE"]);
188
- });
189
-
190
- test("rejects a manifest declaring more than MANIFEST_MAX_ENTRIES with MANIFEST_TOO_MANY_ENTRIES", () => {
191
- const skeleton = v1Skeleton();
192
- const dbEntry = (skeleton.contents as Array<Record<string, unknown>>)[0]!;
193
- const extra = Array.from({ length: MANIFEST_MAX_ENTRIES }, (_, i) => ({
194
- ...dbEntry,
195
- path: `workspace/${i}`,
196
- }));
197
- const archive = gzipTarOf(
198
- withChecksum({ ...skeleton, contents: [dbEntry, ...extra] }),
199
- );
200
- const result = validateVBundle(archive, { enforceStreamingLimits: true });
201
- expect(result.is_valid).toBe(false);
202
- expect(result.errors.map((e) => e.code)).toEqual([
203
- "MANIFEST_TOO_MANY_ENTRIES",
204
- ]);
205
- });
206
-
207
- test("does not apply the limits unless enforceStreamingLimits is set", () => {
208
- const skeleton = v1Skeleton();
209
- const dbEntry = (skeleton.contents as Array<Record<string, unknown>>)[0]!;
210
- const extra = Array.from({ length: MANIFEST_MAX_ENTRIES }, (_, i) => ({
211
- ...dbEntry,
212
- path: `workspace/${i}`,
213
- }));
214
- const archive = gzipTarOf(
215
- withChecksum({ ...skeleton, contents: [dbEntry, ...extra] }),
216
- [{ name: "data/db/assistant.db", data: DB_BYTES }],
217
- );
218
- const result = validateVBundle(archive);
219
- expect(result.errors.map((e) => e.code)).not.toContain(
220
- "MANIFEST_TOO_MANY_ENTRIES",
221
- );
222
- });
223
- });
224
-
225
177
  describe("ManifestSchema — v1 rejection", () => {
226
178
  test("rejects schema_version !== 1", () => {
227
179
  const manifest = withChecksum({ ...v1Skeleton(), schema_version: 2 });
@@ -31,11 +31,9 @@ import { sanitizeConfigForTransfer } from "../../config/sanitize-for-transfer.js
31
31
  import { trackDaemonActivity } from "../../daemon/activity-trail.js";
32
32
  import { getLogger } from "../../util/logger.js";
33
33
  import type { VBundleOriginMode } from "./origin-mode.js";
34
- import {
35
- MANIFEST_MAX_BYTES,
36
- MANIFEST_MAX_ENTRIES,
37
- type ManifestFileEntryType,
38
- type ManifestType,
34
+ import type {
35
+ ManifestFileEntryType,
36
+ ManifestType,
39
37
  } from "./vbundle-validator.js";
40
38
 
41
39
  // ---------------------------------------------------------------------------
@@ -402,20 +400,6 @@ function buildManifestObject(input: {
402
400
  const checksum = sha256Hex(canonicalizeJson(manifestWithEmptyChecksum));
403
401
  const manifest: ManifestType = { ...manifestWithEmptyChecksum, checksum };
404
402
  const manifestData = new TextEncoder().encode(JSON.stringify(manifest));
405
- if (
406
- manifest.contents.length > MANIFEST_MAX_ENTRIES ||
407
- manifestData.length > MANIFEST_MAX_BYTES
408
- ) {
409
- getLogger("vbundle-builder").warn(
410
- {
411
- entries: manifest.contents.length,
412
- manifestBytes: manifestData.length,
413
- maxEntries: MANIFEST_MAX_ENTRIES,
414
- maxBytes: MANIFEST_MAX_BYTES,
415
- },
416
- "Bundle manifest exceeds the streaming import limits; teleport and staged-path imports will reject this bundle",
417
- );
418
- }
419
403
  return { manifest, manifestData };
420
404
  }
421
405
 
@@ -69,10 +69,7 @@ import {
69
69
  verifySymlinkEntry,
70
70
  } from "./vbundle-streaming-validator.js";
71
71
  import { parseVBundleStream } from "./vbundle-tar-stream.js";
72
- import {
73
- MANIFEST_MAX_ENTRIES,
74
- type ManifestType,
75
- } from "./vbundle-validator.js";
72
+ import type { ManifestType } from "./vbundle-validator.js";
76
73
 
77
74
  const log = getLogger("vbundle-streaming-importer");
78
75
 
@@ -94,11 +91,11 @@ const log = getLogger("vbundle-streaming-importer");
94
91
  const DEFAULT_MAX_BUNDLE_BYTES = 16 * 1024 * 1024 * 1024;
95
92
 
96
93
  /**
97
- * Entry-count ceiling for the bundle, shared with manifest validation so
98
- * preflight and import agree. Also applied to tar-level entries so padding
99
- * extras cannot exceed what the manifest declared.
94
+ * Entry-count ceiling for the bundle. 100k is well above the largest
95
+ * workspace we ship; anything past that is almost certainly an attack or
96
+ * a corrupted archive.
100
97
  */
101
- const DEFAULT_MAX_BUNDLE_ENTRIES = MANIFEST_MAX_ENTRIES;
98
+ const DEFAULT_MAX_BUNDLE_ENTRIES = 100_000;
102
99
 
103
100
  /**
104
101
  * Prefixes used for scratch dirs the streaming importer creates INSIDE the
@@ -156,7 +153,7 @@ export interface StreamCommitArgs {
156
153
  maxBundleBytes?: number;
157
154
  /**
158
155
  * Test-only override for the entry-count ceiling. Production callers
159
- * should omit this and rely on the `MANIFEST_MAX_ENTRIES` default.
156
+ * should omit this and rely on the 100_000 default.
160
157
  */
161
158
  maxBundleEntries?: number;
162
159
  }
@@ -316,9 +313,7 @@ export async function streamCommitImport(
316
313
  if (entryIndex === 0) {
317
314
  // First entry MUST be manifest.json — readAndValidateManifest
318
315
  // enforces that and throws StreamingValidationError otherwise.
319
- const manifestResult = await readAndValidateManifest(entry, {
320
- maxEntries: bundleEntryCap,
321
- });
316
+ const manifestResult = await readAndValidateManifest(entry);
322
317
  manifest = manifestResult.manifest;
323
318
  expected = manifestResult.expected;
324
319
 
@@ -345,8 +340,18 @@ export async function streamCommitImport(
345
340
  );
346
341
  }
347
342
 
348
- // Only NOW, after the manifest is parsed (including its entry-count
349
- // ceiling) and the version gate passes, do we materialize the
343
+ // Entry-count ceiling check. The manifest declares every file the
344
+ // bundle claims to contain, so one check here bounds the work the
345
+ // importer is willing to do for this bundle.
346
+ if (manifest.contents.length > bundleEntryCap) {
347
+ throw new StreamingValidationError(
348
+ "bundle_too_many_entries",
349
+ `bundle contains more than ${bundleEntryCap} entries (declared: ${manifest.contents.length})`,
350
+ );
351
+ }
352
+
353
+ // Only NOW — after the manifest is parsed, the version gate passes,
354
+ // and the entry-count ceiling is enforced — do we materialize the
350
355
  // temp staging dir on disk. Doing this lazily preserves the plan
351
356
  // invariant that importers gate on runtime-version compat BEFORE
352
357
  // any state mutation. If this throws, the outer catch runs
@@ -28,8 +28,6 @@ import {
28
28
  computeLegacyManifestSha256,
29
29
  computeManifestChecksum,
30
30
  LegacyManifestSchema,
31
- MANIFEST_MAX_BYTES,
32
- MANIFEST_MAX_ENTRIES,
33
31
  ManifestSchema,
34
32
  type ManifestType,
35
33
  translateLegacyManifest,
@@ -76,17 +74,21 @@ export class StreamingValidationError extends Error {
76
74
  // Manifest validation
77
75
  // ---------------------------------------------------------------------------
78
76
 
77
+ // Manifests are metadata only — typically tens to hundreds of KB even for
78
+ // huge bundles. A 1 MiB cap is comfortably above realistic sizes and
79
+ // protects against a malicious archive whose "manifest" is actually a
80
+ // multi-GB stream intended to OOM the validator.
81
+ const MANIFEST_MAX_BYTES = 1 * 1024 * 1024;
82
+
79
83
  /**
80
84
  * Drain the first tar entry — which MUST be `manifest.json` — and run the
81
85
  * full manifest-level validation pipeline:
82
86
  * 1. Entry name check.
83
- * 2. Size cap (`MANIFEST_MAX_BYTES`).
87
+ * 2. Size cap (1 MiB).
84
88
  * 3. JSON parse.
85
89
  * 4. Zod schema validation.
86
90
  * 5. Self-referencing `checksum` verification against the
87
91
  * canonicalized JSON (minus that field).
88
- * 6. Entry-count ceiling on `contents` (`maxEntries`, default
89
- * `MANIFEST_MAX_ENTRIES`).
90
92
  *
91
93
  * On success, returns the parsed manifest plus a `Map` keyed by archive
92
94
  * path that callers consult as each subsequent entry streams past.
@@ -96,9 +98,7 @@ export class StreamingValidationError extends Error {
96
98
  */
97
99
  export async function readAndValidateManifest(
98
100
  first: StreamedTarEntry,
99
- options: { maxEntries?: number } = {},
100
101
  ): Promise<ManifestReadResult> {
101
- const maxEntries = options.maxEntries ?? MANIFEST_MAX_ENTRIES;
102
102
  if (first.header.name !== "manifest.json") {
103
103
  // Drain the body so the underlying tar extractor isn't left dangling
104
104
  // on backpressure before the caller reports the error.
@@ -186,13 +186,6 @@ export async function readAndValidateManifest(
186
186
  manifest = translateLegacyManifest(legacy);
187
187
  }
188
188
 
189
- if (manifest.contents.length > maxEntries) {
190
- throw new StreamingValidationError(
191
- "bundle_too_many_entries",
192
- `bundle contains more than ${maxEntries} entries (declared: ${manifest.contents.length})`,
193
- );
194
- }
195
-
196
189
  const expected = new Map<
197
190
  string,
198
191
  { sha256: string; size: number; linkTarget: string | null }
@@ -381,33 +381,6 @@ export function computeManifestChecksum(manifest: unknown): string {
381
381
  // Core validation
382
382
  // ---------------------------------------------------------------------------
383
383
 
384
- /**
385
- * Hard caps on `manifest.json`, shared by the buffered and streaming
386
- * validators so preflight and import agree on what is acceptable. The two
387
- * are sized together: the manifest carries one `contents` entry (path,
388
- * sha256, size) per bundled file at roughly 150 bytes each, so 200,000
389
- * entries serialize to about 32 MiB.
390
- *
391
- * That absorbs a workspace with a stray dependency install or checked-out
392
- * repo while still bounding the transient memory a hostile "manifest" can
393
- * force the importer to hold (buffer, decoded string, parsed object,
394
- * canonical re-serialization for the self-checksum) and the per-entry work
395
- * it commits to, on a memory-constrained pod.
396
- */
397
- export const MANIFEST_MAX_BYTES = 32 * 1024 * 1024;
398
- export const MANIFEST_MAX_ENTRIES = 200_000;
399
-
400
- export interface ValidateVBundleOptions {
401
- /**
402
- * Reject manifests over `MANIFEST_MAX_BYTES` or `MANIFEST_MAX_ENTRIES`.
403
- * Preflights that precede a streaming import set this so they never
404
- * approve a bundle the import will refuse. The buffered import path
405
- * (`commitImport`) has no such ceilings, so it stays off there to keep
406
- * existing large backups restorable.
407
- */
408
- enforceStreamingLimits?: boolean;
409
- }
410
-
411
384
  // Only manifest.json is structurally required. The DB and config live under
412
385
  // workspace/ (new format) or data/db/ + config/ (old format) — both are valid.
413
386
  const REQUIRED_ENTRIES = ["manifest.json"];
@@ -424,10 +397,7 @@ const MAX_DECOMPRESSED_SIZE = 2 * 1024 * 1024 * 1024;
424
397
  * 3. Manifest checksum (SHA-256 of canonicalized JSON with the `checksum` field set to empty string)
425
398
  * 4. Per-file content integrity (SHA-256 of each file vs manifest declaration)
426
399
  */
427
- export function validateVBundle(
428
- data: Uint8Array,
429
- { enforceStreamingLimits = false }: ValidateVBundleOptions = {},
430
- ): VBundleValidationResult {
400
+ export function validateVBundle(data: Uint8Array): VBundleValidationResult {
431
401
  const errors: ValidationError[] = [];
432
402
 
433
403
  // Step 1: Decompress gzip with size cap to prevent zip-bomb DoS
@@ -485,17 +455,6 @@ export function validateVBundle(
485
455
  }
486
456
 
487
457
  // Step 4: Parse and validate manifest schema
488
- if (
489
- enforceStreamingLimits &&
490
- manifestEntry.data.length > MANIFEST_MAX_BYTES
491
- ) {
492
- errors.push({
493
- code: "MANIFEST_TOO_LARGE",
494
- message: `manifest.json exceeds ${MANIFEST_MAX_BYTES} byte limit (${manifestEntry.data.length} bytes)`,
495
- path: "manifest.json",
496
- });
497
- return { is_valid: false, errors };
498
- }
499
458
  let manifestRaw: unknown;
500
459
  try {
501
460
  manifestRaw = JSON.parse(new TextDecoder().decode(manifestEntry.data));
@@ -564,18 +523,6 @@ export function validateVBundle(
564
523
  manifest = translateLegacyManifest(legacy);
565
524
  }
566
525
 
567
- if (
568
- enforceStreamingLimits &&
569
- manifest.contents.length > MANIFEST_MAX_ENTRIES
570
- ) {
571
- errors.push({
572
- code: "MANIFEST_TOO_MANY_ENTRIES",
573
- message: `manifest.json declares more than ${MANIFEST_MAX_ENTRIES} entries (${manifest.contents.length})`,
574
- path: "manifest.json",
575
- });
576
- return { is_valid: false, errors };
577
- }
578
-
579
526
  // Step 6: Verify per-file content integrity
580
527
  const manifestFilePaths = new Set(manifest.contents.map((f) => f.path));
581
528
 
@@ -2056,9 +2056,7 @@ export async function handleMigrationPreflightFromGcs({
2056
2056
  }
2057
2057
 
2058
2058
  const bytes = new Uint8Array(await upstream.arrayBuffer());
2059
- const validationResult = validateVBundle(bytes, {
2060
- enforceStreamingLimits: true,
2061
- });
2059
+ const validationResult = validateVBundle(bytes);
2062
2060
 
2063
2061
  if (!validationResult.is_valid || !validationResult.manifest) {
2064
2062
  return {
@@ -486,7 +486,7 @@ export const hostShellTool = {
486
486
  // Only the first handler to fire should wake the agent.
487
487
  let completed = false;
488
488
 
489
- child.on("close", (code, signal) => {
489
+ child.on("close", (code) => {
490
490
  if (completed) {
491
491
  return;
492
492
  }
@@ -494,7 +494,6 @@ export const hostShellTool = {
494
494
  clearTimeout(timer);
495
495
  const result = collector.format(code, timedOut, timeoutSec, {
496
496
  started: launch.didStart(),
497
- signal: aborted ? null : signal,
498
497
  });
499
498
  // Cancel takes precedence over the SIGKILL-induced error result.
500
499
  const status = aborted
@@ -668,13 +667,12 @@ export const hostShellTool = {
668
667
  }
669
668
  }
670
669
 
671
- child.on("close", (code, signal) => {
670
+ child.on("close", (code) => {
672
671
  clearTimeout(timer);
673
672
  context.signal?.removeEventListener("abort", onAbort);
674
673
 
675
674
  const result = collector.format(code, timedOut, timeoutSec, {
676
675
  started: launch.didStart(),
677
- signal: context.signal?.aborted ? null : signal,
678
676
  });
679
677
 
680
678
  resolve({
@@ -112,45 +112,3 @@ describe("formatShellOutput truncation", () => {
112
112
  expect(result.content).toContain(OUTPUT_TRUNCATED_TAG);
113
113
  });
114
114
  });
115
-
116
- describe("formatShellOutput kill reporting", () => {
117
- test("names the signal and hints at memory for a SIGKILL", () => {
118
- const result = formatShellOutput("", "", null, false, 120, {
119
- signal: "SIGKILL",
120
- });
121
- expect(result.isError).toBe(true);
122
- expect(result.content).toContain('<command_killed signal="SIGKILL" />');
123
- expect(result.content).toContain("out of memory");
124
- expect(result.content).not.toContain('code="null"');
125
- expect(result.status).toBe('<command_killed signal="SIGKILL" />');
126
- });
127
-
128
- test("keeps the output produced before the kill", () => {
129
- const result = formatShellOutput("partial", "", null, false, 120, {
130
- signal: "SIGTERM",
131
- });
132
- expect(result.content.startsWith("partial\n")).toBe(true);
133
- expect(result.content).toContain("killed by SIGTERM");
134
- expect(result.content).not.toContain("out of memory");
135
- });
136
-
137
- test("does not infer a kill from a 128 + n exit code", () => {
138
- const result = formatShellOutput("", "Killed", 137, false, 120);
139
- expect(result.content).toBe("Killed");
140
- expect(result.status).toBe('<command_exit code="137" />');
141
- });
142
-
143
- test("leaves a timeout framed as a timeout", () => {
144
- const result = formatShellOutput("", "", null, true, 120, {
145
- signal: "SIGKILL",
146
- });
147
- expect(result.content).toContain("<command_timeout");
148
- expect(result.content).not.toContain("command_killed");
149
- });
150
-
151
- test("leaves an ordinary failure unchanged", () => {
152
- const result = formatShellOutput("", "boom", 1, false, 120);
153
- expect(result.content).toBe("boom");
154
- expect(result.status).toBe('<command_exit code="1" />');
155
- });
156
- });
@@ -13,15 +13,6 @@ export interface ShellOutputResult {
13
13
  isError: boolean;
14
14
  }
15
15
 
16
- function describeKill(signal: NodeJS.Signals): string {
17
- const tag = `<command_killed signal="${signal}" />`;
18
- const hint =
19
- signal === "SIGKILL"
20
- ? " A SIGKILL the command did not send itself usually means the system ran out of memory."
21
- : "";
22
- return `${tag}\nCommand was killed by ${signal} before it finished.${hint}`;
23
- }
24
-
25
16
  type StdioStream = "stdout" | "stderr";
26
17
 
27
18
  type DataListener = (data: Buffer | string) => void;
@@ -43,11 +34,7 @@ export function formatShellOutput(
43
34
  code: number | null,
44
35
  timedOut: boolean,
45
36
  timeoutSec: number,
46
- options?: {
47
- truncated?: boolean;
48
- started?: boolean;
49
- signal?: NodeJS.Signals | null;
50
- },
37
+ options?: { truncated?: boolean; started?: boolean },
51
38
  ): ShellOutputResult {
52
39
  if (options?.started === false) {
53
40
  return {
@@ -79,12 +66,6 @@ export function formatShellOutput(
79
66
  statusParts.push(OUTPUT_TRUNCATED_TAG);
80
67
  }
81
68
 
82
- const killSignal = timedOut ? null : (options?.signal ?? null);
83
- if (killSignal) {
84
- output += (output ? "\n" : "") + describeKill(killSignal);
85
- statusParts.push(`<command_killed signal="${killSignal}" />`);
86
- }
87
-
88
69
  if (!output.trim()) {
89
70
  if (code === 0) {
90
71
  output = "<command_completed />";
@@ -93,7 +74,7 @@ export function formatShellOutput(
93
74
  output = `${exitTag}\nCommand failed with exit code ${code}. No stdout or stderr output was produced.`;
94
75
  statusParts.push(exitTag);
95
76
  }
96
- } else if (code !== 0 && !timedOut && !killSignal) {
77
+ } else if (code !== 0 && !timedOut) {
97
78
  statusParts.push(`<command_exit code="${code}" />`);
98
79
  }
99
80
 
@@ -167,7 +148,7 @@ export class BoundedStdioCollector {
167
148
  code: number | null,
168
149
  timedOut: boolean,
169
150
  timeoutSec: number,
170
- options?: { started?: boolean; signal?: NodeJS.Signals | null },
151
+ options?: { started?: boolean },
171
152
  ): ShellOutputResult {
172
153
  return formatShellOutput(
173
154
  Buffer.concat(this.stdoutParts).toString(),
@@ -175,11 +156,7 @@ export class BoundedStdioCollector {
175
156
  code,
176
157
  timedOut,
177
158
  timeoutSec,
178
- {
179
- truncated: this.truncated,
180
- started: options?.started,
181
- signal: options?.signal,
182
- },
159
+ { truncated: this.truncated, started: options?.started },
183
160
  );
184
161
  }
185
162
  }
@@ -374,7 +374,6 @@ export const shellTool = {
374
374
 
375
375
  const fmtResult = collector.format(code, timedOut, timeoutSec, {
376
376
  started: launch.didStart(),
377
- signal: aborted ? null : signal,
378
377
  });
379
378
 
380
379
  const status: BackgroundToolCompletedEvent["status"] = aborted
@@ -586,7 +585,6 @@ export const shellTool = {
586
585
 
587
586
  const fmtResult = collector.format(code, timedOut, timeoutSec, {
588
587
  started: launch.didStart(),
589
- signal: context.signal?.aborted ? null : signal,
590
588
  });
591
589
 
592
590
  resolve({
@@ -1,143 +0,0 @@
1
- /**
2
- * Verifies the IO Intelligence (io.net) provider wiring: the SDK client must
3
- * resolve to the documented IO Intelligence chat-completions endpoint unless
4
- * an explicit override is supplied (custom-connection / test usage).
5
- */
6
-
7
- import { describe, expect, test } from "bun:test";
8
-
9
- import { IonetProvider } from "./client.js";
10
-
11
- type MockChunk = {
12
- choices: Array<{
13
- delta: { content?: string | null };
14
- finish_reason?: string | null;
15
- }>;
16
- usage?: { prompt_tokens: number; completion_tokens: number };
17
- };
18
-
19
- const OK_CHUNKS: MockChunk[] = [
20
- {
21
- choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
22
- usage: { prompt_tokens: 1, completion_tokens: 1 },
23
- },
24
- ];
25
-
26
- function makeStream(chunks: MockChunk[]): AsyncIterable<MockChunk> {
27
- return {
28
- async *[Symbol.asyncIterator]() {
29
- for (const c of chunks) {
30
- yield c;
31
- }
32
- },
33
- };
34
- }
35
-
36
- function clientOf(provider: IonetProvider) {
37
- return (
38
- provider as unknown as {
39
- client: {
40
- baseURL: string;
41
- chat: {
42
- completions: {
43
- create: (params: unknown) => Promise<AsyncIterable<MockChunk>>;
44
- };
45
- };
46
- };
47
- }
48
- ).client;
49
- }
50
-
51
- describe("IonetProvider", () => {
52
- test("defaults the SDK client to the IO Intelligence endpoint", async () => {
53
- const provider = new IonetProvider(
54
- "test-key",
55
- "meta-llama/Llama-3.3-70B-Instruct",
56
- );
57
-
58
- const client = clientOf(provider);
59
- expect(client.baseURL).toBe("https://api.intelligence.io.solutions/api/v1");
60
-
61
- // The same base URL must be reported on the response, read off the live
62
- // client instance rather than a constant.
63
- client.chat.completions.create = async () => makeStream(OK_CHUNKS);
64
- const response = await provider.sendMessage([
65
- { role: "user", content: [{ type: "text", text: "hi" }] },
66
- ]);
67
- expect(response.resolvedEndpoint).toBe(
68
- "https://api.intelligence.io.solutions/api/v1",
69
- );
70
- });
71
-
72
- test("sends tool_choice auto when tools are offered without an explicit choice", async () => {
73
- // io.net defaults an unspecified tool_choice to "none" (not OpenAI's
74
- // "auto"), so the provider must send the explicit default or the model
75
- // could never invoke an offered tool.
76
- const provider = new IonetProvider(
77
- "test-key",
78
- "meta-llama/Llama-3.3-70B-Instruct",
79
- );
80
- const client = clientOf(provider);
81
-
82
- let seenParams:
83
- | {
84
- tools?: unknown;
85
- tool_choice?: unknown;
86
- }
87
- | undefined;
88
- client.chat.completions.create = async (params) => {
89
- seenParams = params as { tools?: unknown; tool_choice?: unknown };
90
- return makeStream(OK_CHUNKS);
91
- };
92
- await provider.sendMessage(
93
- [{ role: "user", content: [{ type: "text", text: "hi" }] }],
94
- {
95
- tools: [
96
- {
97
- name: "get_weather",
98
- description: "Get the weather",
99
- input_schema: { type: "object", properties: {} },
100
- },
101
- ],
102
- },
103
- );
104
- expect(Array.isArray(seenParams?.tools)).toBe(true);
105
- expect(seenParams?.tool_choice).toBe("auto");
106
-
107
- // An explicit caller choice still wins.
108
- let forcedParams: { tool_choice?: unknown } | undefined;
109
- client.chat.completions.create = async (params) => {
110
- forcedParams = params as { tool_choice?: unknown };
111
- return makeStream(OK_CHUNKS);
112
- };
113
- await provider.sendMessage(
114
- [{ role: "user", content: [{ type: "text", text: "hi" }] }],
115
- {
116
- tools: [
117
- {
118
- name: "get_weather",
119
- description: "Get the weather",
120
- input_schema: { type: "object", properties: {} },
121
- },
122
- ],
123
- config: { tool_choice: { type: "none" } },
124
- },
125
- );
126
- expect(forcedParams?.tool_choice).toBe("none");
127
- });
128
-
129
- test("honours an explicit baseURL override", async () => {
130
- const provider = new IonetProvider("test-key", "test-model", {
131
- baseURL: "https://inference.example.test/v1",
132
- });
133
-
134
- const client = clientOf(provider);
135
- expect(client.baseURL).toBe("https://inference.example.test/v1");
136
-
137
- client.chat.completions.create = async () => makeStream(OK_CHUNKS);
138
- const response = await provider.sendMessage([
139
- { role: "user", content: [{ type: "text", text: "hi" }] },
140
- ]);
141
- expect(response.resolvedEndpoint).toBe("https://inference.example.test/v1");
142
- });
143
- });
@@ -1,37 +0,0 @@
1
- import { OpenAIChatCompletionsProvider } from "../openai/chat-completions-provider.js";
2
-
3
- export interface IonetProviderOptions {
4
- apiKey?: string;
5
- baseURL?: string;
6
- streamTimeoutMs?: number;
7
- }
8
-
9
- const DEFAULT_IONET_BASE_URL = "https://api.intelligence.io.solutions/api/v1";
10
-
11
- /**
12
- * IO Intelligence (io.net) serves an OpenAI chat-completions API with
13
- * Hugging Face-style `org/name` model ids, so it rides the shared
14
- * chat-completions client with its own default base URL.
15
- */
16
- export class IonetProvider extends OpenAIChatCompletionsProvider {
17
- constructor(
18
- apiKey: string,
19
- model: string,
20
- options: IonetProviderOptions = {},
21
- ) {
22
- super(apiKey, model, {
23
- baseURL: options.baseURL?.trim() || DEFAULT_IONET_BASE_URL,
24
- providerName: "ionet",
25
- providerLabel: "IO Intelligence",
26
- streamTimeoutMs: options.streamTimeoutMs,
27
- // IO Intelligence serves DeepSeek- and GLM-style thinking models whose
28
- // reasoning arrives as `reasoning_content`; replay it there so
29
- // follow-up requests that include tools stay acceptable upstream.
30
- assistantReasoningField: "reasoning_content",
31
- // io.net defaults an unspecified `tool_choice` to "none" (unlike
32
- // OpenAI's "auto"), so tools offered without an explicit choice must
33
- // still send one or the model could never invoke them.
34
- defaultToolChoiceAuto: true,
35
- });
36
- }
37
- }