@yanlinglabs/winter-agent-runtime 0.0.35 → 0.0.38

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.
@@ -9,14 +9,14 @@ import {
9
9
  resolveEngineSession2,
10
10
  resolveProductionWinterHome2,
11
11
  resolveProductionStoreHome
12
- } from "./index-q5jpx2mz.js";
12
+ } from "./index-5xe59vkt.js";
13
13
  import {
14
14
  registerDefaultChildEngineFactory,
15
15
  loadResumedChain,
16
16
  buildProductionWiring,
17
17
  withAutoSkillPermissions,
18
18
  restoreChildRoster
19
- } from "./index-838n35xx.js";
19
+ } from "./index-z3970wsj.js";
20
20
 
21
21
  // src/embedded.ts
22
22
  import { splitFrames, encodeFrame } from "@yanlinglabs/winter-agent-sdk";
@@ -15,6 +15,10 @@ import {
15
15
  mcpServerHasRegistrations,
16
16
  STRUCTURED_OUTPUT_TOOL_NAME,
17
17
  looksLikeCommand,
18
+ matchFileRule,
19
+ resolveFileRuleAbsolutePath,
20
+ resolveFileRuleAbsoluteGlobText,
21
+ resolvesWithinPluginRoot,
18
22
  capBytes,
19
23
  neutralizeReminderTags,
20
24
  readWhole,
@@ -22,12 +26,8 @@ import {
22
26
  registerAttachmentRenderer,
23
27
  attachmentsIn,
24
28
  registerChildEngineFactory,
25
- matchFileRule,
26
29
  rawToRuleValue,
27
30
  sourceRule,
28
- resolveFileRuleAbsolutePath,
29
- resolveFileRuleAbsoluteGlobText,
30
- resolvesWithinPluginRoot,
31
31
  parseFrontmatter,
32
32
  parseAgentDefinitionFile,
33
33
  allowedAgentTypesFromTools,
@@ -85,7 +85,7 @@ import {
85
85
  providerCredentialRef2,
86
86
  createModelClassifier2,
87
87
  selectClassifierRoute2
88
- } from "./index-q5jpx2mz.js";
88
+ } from "./index-5xe59vkt.js";
89
89
  import {
90
90
  createKeychainCredentialStore,
91
91
  createKeychainSecretReader,
@@ -311,7 +311,7 @@ function applySubagentToolPool(tools, opts) {
311
311
  return true;
312
312
  if (policyName === BACKGROUND_AGENT_ALWAYS_ALLOWED_TOOL)
313
313
  return true;
314
- if (name.startsWith("mcp__") && !twins.has(name))
314
+ if (!twins.has(name) && (name.startsWith("mcp__") || getRegisteredTool(name)?.descriptor.source === "mcp"))
315
315
  return true;
316
316
  return allow.has(policyName);
317
317
  });
@@ -492,12 +492,14 @@ function renameMcpReference(entry, renames) {
492
492
  }
493
493
  return;
494
494
  }
495
- function allocateChildScopedServers(declared, parentVisible, prefer, parentDeclared) {
495
+ function allocateChildScopedServers(declared, parentVisible, prefer, parentDeclared, reserved) {
496
496
  const claimed = [];
497
- const inUse = (name) => liveChildScopedServerNames.has(name) || parentVisible.has(name) || mcpServerHasRegistrations(name);
497
+ const reservedSet = new Set(reserved ?? []);
498
+ const inUse = (name) => liveChildScopedServerNames.has(name) || parentVisible.has(name) || mcpServerHasRegistrations(name) || reservedSet.has(name);
498
499
  const servers = {};
499
500
  const actual = new Map;
500
501
  const notes = [];
502
+ const reservedRenames = new Set;
501
503
  const claim = (name) => {
502
504
  liveChildScopedServerNames.add(name);
503
505
  claimed.push(name);
@@ -505,7 +507,7 @@ function allocateChildScopedServers(declared, parentVisible, prefer, parentDecla
505
507
  for (const [name, cfg] of Object.entries(declared)) {
506
508
  if (cfg.type === "sdk") {
507
509
  if (inUse(name)) {
508
- notes.push(parentVisible.has(name) && parentDeclared?.[name]?.type === "sdk" ? `mcpServers declares the in-process server "${name}", which this session already has -- the session's "${name}" is used` : parentVisible.has(name) ? `mcpServers declares the in-process server "${name}", but this session's "${name}" is a different server -- this agent's own in-process "${name}" is not connected, and the session's is what this agent sees` : `mcpServers declares the in-process server "${name}", which another agent in this session is using -- this agent's own in-process "${name}" is not connected`);
510
+ notes.push(parentVisible.has(name) && parentDeclared?.[name]?.type === "sdk" ? `mcpServers declares the in-process server "${name}", which this session already has -- the session's "${name}" is used` : parentVisible.has(name) ? `mcpServers declares the in-process server "${name}", but this session's "${name}" is a different server -- this agent's own in-process "${name}" is not connected, and the session's is what this agent sees` : reservedSet.has(name) && !liveChildScopedServerNames.has(name) && !mcpServerHasRegistrations(name) ? `mcpServers declares the in-process server "${name}", a name this session's host reserves -- this agent's own in-process "${name}" is not connected` : `mcpServers declares the in-process server "${name}", which another agent in this session is using -- this agent's own in-process "${name}" is not connected`);
509
511
  continue;
510
512
  }
511
513
  claim(name);
@@ -523,6 +525,8 @@ function allocateChildScopedServers(declared, parentVisible, prefer, parentDecla
523
525
  claim(pick);
524
526
  servers[pick] = cfg;
525
527
  actual.set(name, pick);
528
+ if (pick !== name && reservedSet.has(name))
529
+ reservedRenames.add(name);
526
530
  if (pick !== name)
527
531
  notes.push(`mcpServers declares "${name}", which is already in use in this session -- this agent's own "${name}" is connected as "${pick}" (its tools are named mcp__${pick}__<tool>)`);
528
532
  }
@@ -531,6 +535,7 @@ function allocateChildScopedServers(declared, parentVisible, prefer, parentDecla
531
535
  servers,
532
536
  actual,
533
537
  notes,
538
+ reservedRenames,
534
539
  release: () => {
535
540
  if (released)
536
541
  return;
@@ -775,7 +780,7 @@ async function spawnChildEngine(req, inherit, runCtx, deps) {
775
780
  ...!ownServers && parentMcp?.stateSource !== undefined ? { mcpServerStateSource: parentMcp.stateSource } : {},
776
781
  ...!ownServers && parentMcp?.controlSeam !== undefined ? { mcpControlSeam: parentMcp.controlSeam } : {},
777
782
  ...(() => {
778
- const merged = { ...parentMcp?.serverRenames ?? {}, ...Object.fromEntries(renames.map(([declared, actual]) => [actual, declared])) };
783
+ const merged = { ...parentMcp?.serverRenames ?? {}, ...Object.fromEntries(renames.filter(([declared]) => !servers.reservedRenames.has(declared)).map(([declared, actual]) => [actual, declared])) };
779
784
  return Object.keys(merged).length > 0 ? { mcpServerRenames: merged } : {};
780
785
  })(),
781
786
  ...parentMcp?.visibleServerNames !== undefined ? { inheritedMcpServerNames: ownServerNames.size === 0 ? parentMcp.visibleServerNames : () => parentMcp.visibleServerNames().filter((name) => !ownServerNames.has(name)) } : {},
@@ -886,7 +891,12 @@ async function spawnChildEngine(req, inherit, runCtx, deps) {
886
891
  const complementDeny = allToolNames.filter((name) => !allowSet.has(name));
887
892
  const disallowedTools = [...new Set([...complementDeny, ...req.definition?.disallowedTools ?? []])];
888
893
  const allowedAgentTypes = allowedAgentTypesFromTools(req.definition?.tools);
889
- const capabilities = [...new Set(effectiveTools.flatMap((name) => getRegisteredTool(name)?.descriptor.capabilityRequirements ?? []))];
894
+ const capabilities = [
895
+ ...new Set(effectiveTools.flatMap((name) => {
896
+ const descriptor = getRegisteredTool(name)?.descriptor;
897
+ return [...descriptor?.capabilityRequirements ?? [], ...descriptor?.availability.requiresAnyCapability ?? []];
898
+ }))
899
+ ];
890
900
  const definitionWarnings = req.definition !== undefined ? validateAgentDefinition(req.definition) : [];
891
901
  const parentMcp = runCtx.getParentMcpState?.();
892
902
  const parentAgents = runCtx.getParentAgents?.();
@@ -955,6 +965,8 @@ async function spawnChildEngine(req, inherit, runCtx, deps) {
955
965
  ...deps.parentBrand !== undefined ? { brand: deps.parentBrand } : {},
956
966
  ...deps.parentAutoMemory !== undefined ? { autoMemory: deps.parentAutoMemory } : {},
957
967
  ...deps.parentWeb !== undefined ? { web: deps.parentWeb } : {},
968
+ ...deps.parentReservedMcpServerNames !== undefined ? { reservedMcpServerNames: deps.parentReservedMcpServerNames } : {},
969
+ ...deps.parentLegacyToolNames !== undefined ? { legacyToolNames: deps.parentLegacyToolNames } : {},
958
970
  model: resolvedModel.effectiveModel,
959
971
  permissionMode: inherit.policy.effectiveMode,
960
972
  allowDangerouslySkipPermissions: inherit.policy.effectiveMode === "bypassPermissions",
@@ -965,6 +977,7 @@ async function spawnChildEngine(req, inherit, runCtx, deps) {
965
977
  ...req.fork === true && inherit.effectiveEffort !== undefined && typeof inherit.effectiveEffort !== "number" ? { effort: inherit.effectiveEffort } : {},
966
978
  ...req.fork === true && inherit.effectiveThinking !== undefined ? { thinking: inherit.effectiveThinking } : {},
967
979
  disallowedTools,
980
+ tools: [...effectiveTools],
968
981
  capabilities,
969
982
  ...allowedAgentTypes !== undefined ? { allowedAgentTypes } : {},
970
983
  forwardSubagentText: deps.forwardSubagentText === true,
@@ -980,7 +993,7 @@ async function spawnChildEngine(req, inherit, runCtx, deps) {
980
993
  const resolvedSystemPrompt = [inherit.systemPrompt, req.fork === true ? undefined : req.definition?.prompt].filter((s) => s !== undefined && s.length > 0).join(`
981
994
 
982
995
  `);
983
- let serverAllocation = allocateChildScopedServers(childScopedMcpServers, parentVisibleServers(), undefined, parentMcp?.declaredServers);
996
+ let serverAllocation = allocateChildScopedServers(childScopedMcpServers, parentVisibleServers(), undefined, parentMcp?.declaredServers, deps.parentReservedMcpServerNames);
984
997
  pendingServerRelease = serverAllocation.release;
985
998
  definitionWarnings.push(...serverAllocation.notes);
986
999
  const firstTurnText = req.fork === true ? buildForkDirectiveText({ prompt: req.prompt, ...req.isolation === "worktree" ? { worktree: { parentRoot: inherit.sessionRoot, worktreeRoot: workspace.root } } : {} }) : [req.definition?.initialPrompt, req.prompt, definitionWarnings.length > 0 ? `
@@ -1093,7 +1106,7 @@ async function spawnChildEngine(req, inherit, runCtx, deps) {
1093
1106
  pendingResume = undefined;
1094
1107
  if (ticket.stopped)
1095
1108
  return { status: "unavailable", messageId: msg.messageId, retryable: false, reason: `child ${agentId} was stopped while resuming` };
1096
- serverAllocation = allocateChildScopedServers(childScopedMcpServers, parentVisibleServers(), previous, parentMcp?.declaredServers);
1109
+ serverAllocation = allocateChildScopedServers(childScopedMcpServers, parentVisibleServers(), previous, parentMcp?.declaredServers, deps.parentReservedMcpServerNames);
1097
1110
  const moved = [...serverAllocation.actual].filter(([declared, name]) => previous.get(declared) !== name).map(([declared, name]) => `this agent's own "${declared}" is now connected as "${name}" (its tools are named mcp__${name}__<tool>)`);
1098
1111
  startGeneration(generationConfig(resumeMode), rebuilt, moved.length > 0 ? `${msg.body}
1099
1112
 
@@ -1159,6 +1172,8 @@ function registerDefaultChildEngineFactory(opts) {
1159
1172
  ...config.brand !== undefined ? { parentBrand: config.brand } : {},
1160
1173
  ...config.autoMemory !== undefined ? { parentAutoMemory: config.autoMemory } : {},
1161
1174
  ...config.web !== undefined ? { parentWeb: config.web } : {},
1175
+ ...config.reservedMcpServerNames !== undefined ? { parentReservedMcpServerNames: config.reservedMcpServerNames } : {},
1176
+ ...config.legacyToolNames !== undefined ? { parentLegacyToolNames: config.legacyToolNames } : {},
1162
1177
  ...opts.systemPromptAssembler !== undefined ? { systemPromptAssembler: opts.systemPromptAssembler } : {},
1163
1178
  ...opts.skillRuntime !== undefined ? { skillRuntime: opts.skillRuntime } : {},
1164
1179
  ...opts.structuredOutput !== undefined ? { structuredOutput: opts.structuredOutput } : {},
@@ -1264,7 +1279,7 @@ function createToolSecretResolver(deps) {
1264
1279
  }
1265
1280
 
1266
1281
  // src/provider/bridge.ts
1267
- import { normalizeThrown, shouldRequestSummary, stripOpaque } from "@yanlinglabs/winter-provider-runtime";
1282
+ import { imageBudgetFor, normalizeThrown, shouldRequestSummary, stripOpaque, withinImageBudget } from "@yanlinglabs/winter-provider-runtime";
1268
1283
  function createIdentityHistoryRenderer() {
1269
1284
  return {
1270
1285
  render(messages, _chain, target) {
@@ -1299,9 +1314,14 @@ function adapterAsProvider(resolved, ctx, opts = {}) {
1299
1314
  modelKey: resolved.modelKey,
1300
1315
  ...Array.isArray(resolved.descriptor?.inputModalities?.value) ? { readsImages: resolved.descriptor.inputModalities.value.includes("image") } : {}
1301
1316
  };
1317
+ const imageBudget = imageBudgetFor({
1318
+ family: adapter.family,
1319
+ providerId: resolved.providerId,
1320
+ ...typeof resolved.descriptor?.contextWindow?.value === "number" ? { contextWindow: resolved.descriptor.contextWindow.value } : {}
1321
+ });
1302
1322
  return {
1303
1323
  async generate(input) {
1304
- const rendered = renderer.render(input.messages, opts.chain?.() ?? new Map, target);
1324
+ const rendered = withinImageBudget(renderer.render(input.messages, opts.chain?.() ?? new Map, target), imageBudget);
1305
1325
  const request = {
1306
1326
  model: input.model === undefined || input.model === resolved.modelKey ? resolved.providerModelId : input.model,
1307
1327
  messages: rendered,
@@ -1452,7 +1472,7 @@ async function foldProviderStream(stream, sink) {
1452
1472
  emitter.messageStop(event.stopReason);
1453
1473
  break;
1454
1474
  case "error":
1455
- throw providerErrorToTurnError(event.error, committed && !(event.error.contextOverflow === true && ordered.length === 0));
1475
+ throw providerErrorToTurnError(event.error, committed && !((event.error.contextOverflow === true || event.error.imageOverflow === true) && ordered.length === 0));
1456
1476
  }
1457
1477
  }
1458
1478
  } catch (err) {
@@ -1604,7 +1624,8 @@ function providerErrorToTurnError(error, committed = false) {
1604
1624
  code: error.code,
1605
1625
  retryable: error.retryable,
1606
1626
  committed,
1607
- ...error.contextOverflow === true ? { contextOverflow: true } : {}
1627
+ ...error.contextOverflow === true ? { contextOverflow: true } : {},
1628
+ ...error.imageOverflow === true ? { imageOverflow: true } : {}
1608
1629
  });
1609
1630
  }
1610
1631
  function toProviderTurnError(err, committed = false) {
@@ -1618,7 +1639,14 @@ function toProviderTurnError(err, committed = false) {
1618
1639
  const message = raw.length > MAX_ERROR_MESSAGE_CHARS ? `${raw.slice(0, MAX_ERROR_MESSAGE_CHARS)}...` : raw;
1619
1640
  const normalized = normalizeThrown(err);
1620
1641
  const status = typeof err === "object" && err !== null && typeof err.status === "number" ? err.status : normalized.status;
1621
- return new ProviderTurnError(`provider request failed: ${message}`, { ...status !== undefined ? { status } : {}, code: normalized.code, retryable: normalized.retryable, committed });
1642
+ return new ProviderTurnError(`provider request failed: ${message}`, {
1643
+ ...status !== undefined ? { status } : {},
1644
+ code: normalized.code,
1645
+ retryable: normalized.retryable,
1646
+ committed,
1647
+ ...normalized.contextOverflow === true ? { contextOverflow: true } : {},
1648
+ ...normalized.imageOverflow === true ? { imageOverflow: true } : {}
1649
+ });
1622
1650
  }
1623
1651
  function stampNativeState(turn, origin) {
1624
1652
  if (turn.nativeState === undefined)
package/dist/index.js CHANGED
@@ -2,11 +2,11 @@ import {
2
2
  createRpcBridge2,
3
3
  registerTool2,
4
4
  onCompaction2,
5
+ buildWorkflowWorkerSeatbeltProfile2,
5
6
  WinterPathsError2,
6
7
  sessionTempDir2,
7
8
  ensureTasksDir2,
8
9
  resolveProjectDirName2,
9
- buildWorkflowWorkerSeatbeltProfile2,
10
10
  WORKFLOW_SANDBOX_REFUSED_EXIT_CODE2,
11
11
  DEFAULT_CONTEXT_WINDOW_TOKENS2,
12
12
  createContextAccountant2,
@@ -55,7 +55,7 @@ import {
55
55
  DEFAULT_CLASSIFIER_TIMEOUT_MS2,
56
56
  createModelClassifier2,
57
57
  selectClassifierRoute2
58
- } from "./index-q5jpx2mz.js";
58
+ } from "./index-5xe59vkt.js";
59
59
  import"./index-1hef2gff.js";
60
60
  import"./index-mwew595z.js";
61
61
  import"./index-2wgfv0pa.js";
@@ -2,4 +2,5 @@ import type { McpControlSeam } from "./control-seam.js";
2
2
  import type { McpLifecycleInternals } from "./lifecycle.js";
3
3
  export declare function createMcpControlSeam(internals: McpLifecycleInternals, opts?: {
4
4
  reservedServerName?: string;
5
+ hostReservedServerNames?: readonly string[];
5
6
  }): McpControlSeam;
@@ -85,7 +85,10 @@ export declare function resolveMcpServerSources(sources: readonly McpServerSourc
85
85
  strictMcpConfig?: boolean;
86
86
  trustedWorkspace: boolean;
87
87
  reservedServerName?: string;
88
+ hostReservedServerNames?: readonly string[];
88
89
  }): ResolveMcpServerSourcesResult;
90
+ /** The text budget of an MCP result that carries images (≈ the default `MAX_MCP_OUTPUT_TOKENS` of 25000 at 4 characters a token). */
91
+ export declare const MCP_IMAGE_RESULT_TEXT_CHARS = 100000;
89
92
  export interface McpDiscoveryCache {
90
93
  get(key: string): McpToolInfo[] | undefined;
91
94
  set(key: string, tools: McpToolInfo[]): void;
@@ -109,6 +112,8 @@ export interface McpLifecycleDeps {
109
112
  * `addAndConnect` guard is the second door onto the same rule. Defaults to `WINTER_SERVER_NAME`.
110
113
  */
111
114
  reservedServerName?: string;
115
+ /** `RuntimeConfig.reservedMcpServerNames`, for the control seam's live `mcp_set_servers` guard: a non-sdk server is refused under any of them. */
116
+ hostReservedServerNames?: readonly string[];
112
117
  /** WS-23: the session cwd every stdio server of this lifecycle is spawned in (see `ConnectMcpServerOptions.cwd`). */
113
118
  cwd?: string;
114
119
  /**
@@ -27,7 +27,7 @@ export declare const P5_FIXTURE_SKILL_NAME = "p5probe";
27
27
  * `{"answer":"42","doubled":"4242"}` on every leg.
28
28
  */
29
29
  export declare const P5_WORKFLOW_SCRIPT: string;
30
- export type TestProviderName = "boom" | "tooluse" | "hang" | "reflect" | "modeswitch" | "bgtask" | "lanea" | "laneb" | "lanec" | "laned" | "lanee" | "mcpsdk" | "subagent" | "childmsg" | "subagentperm" | "toolsearch" | "p5compact" | "p5structured" | "p5structuredfail" | "p5skill" | "p5checkpoint" | "p5workflow";
30
+ export type TestProviderName = "boom" | "tooluse" | "hang" | "reflect" | "modeswitch" | "bgtask" | "lanea" | "laneb" | "lanec" | "laned" | "lanee" | "mcpsdk" | "subagent" | "childmsg" | "subagentperm" | "toolsearch" | "p5compact" | "p5structured" | "p5structuredfail" | "p5skill" | "p5checkpoint" | "p5workflow" | "readimage" | "calls";
31
31
  export declare function isTestProviderName(v: string): v is TestProviderName;
32
32
  /**
33
33
  * The name the reserved `winter-test/<name>` namespace uses for the plain echo double.
@@ -43,6 +43,43 @@ export declare class SandboxConfigError extends Error {
43
43
  }
44
44
  export declare function resolveNetworkPosture(network: SandboxNetworkSettings | undefined): boolean;
45
45
  export declare function canonicalizePath(p: string): string;
46
+ /**
47
+ * Fix round 14 (Winter-specific hardening, NOT itself a claude port -- empirically discovered and
48
+ * verified while implementing `buildReadDenyWritePermitBlock` above, disclosed prominently rather
49
+ * than smoothed over): `pR`'s own trailing re-permit is a BLANKET, UNCONDITIONAL
50
+ * `(allow file-write-unlink file-write-create (subpath <every write root>))`, ported faithfully per
51
+ * the controller's own explicit instruction. Real `sandbox-exec` runs proved this is not merely
52
+ * "narrower than a later wildcard deny wins" (round 13's own finding, about an EARLIER explicit deny
53
+ * surviving a LATER broad `file-write*` allow) -- it runs the OTHER direction too: an EARLIER
54
+ * EXPLICIT `file-write-unlink`/`file-write-create` ALLOW is not overridden by a LATER, broader
55
+ * `(deny file-write* ...)` for the SAME target either. Seatbelt appears to give a clause naming
56
+ * `file-write-unlink`/`file-write-create` explicitly priority over one that only reaches those
57
+ * operations via the `file-write*` wildcard, independent of which clause is textually first or last.
58
+ *
59
+ * Every OTHER Winter-owned write-protection floor in this module that used only the `file-write*`
60
+ * wildcard was therefore silently punched through for CREATE and UNLINK/RENAME specifically (never
61
+ * for `file-write-data`, `file-write-mode`, etc., which this re-permit never names) by this ONE new
62
+ * block: the control-plane carve-outs (WS-12 §5.2's own "the seatbelt is the only enforcement point
63
+ * left" floor -- verified empirically: `mkdir -p .winter && echo '{}' > .winter/permissions.local.json`
64
+ * and `rm .winter/settings.json` both SUCCEEDED against the unpatched fix), the checkpoint/backup
65
+ * store write-deny (T8 rider 25's own identical floor), and a GLOB-shaped `denyWrite` entry (a plain
66
+ * `denyWritePaths` entry was already safe -- `Ch`'s own write-side ancestor-rename block, round 12,
67
+ * already emits an explicit `(subpath <path>)` deny for it; `Ch`'s own GLOB branch, by contrast, only
68
+ * ever emits a `(literal <fixedPrefix>)` -- protecting the prefix DIRECTORY's own identity against a
69
+ * rename-shuffle, never the glob-matched files themselves).
70
+ *
71
+ * The fix, verified against real `sandbox-exec` (a `(deny file-write* file-write-unlink
72
+ * file-write-create (regex ...))` clause DOES win back the CREATE it needs to, confirmed by a direct
73
+ * before/after run rather than assumed): every one of those floors now names
74
+ * `file-write-unlink`/`file-write-create` EXPLICITLY, alongside the `file-write*` wildcard it already
75
+ * carried (for the OTHER write operations the wildcard alone still covers correctly) -- this constant
76
+ * is that shared operation-name list, applied wherever `file-write*` ALONE previously appeared on a
77
+ * deny this round's own re-permit could otherwise reach. `fR` (`buildReadDenyKeepInPlaceBlock`) is
78
+ * deliberately NOT touched here: it already names `file-write-unlink` explicitly (never `create`, by
79
+ * claude's own design -- see that function's own header), so it was never in the affected set.
80
+ */
81
+ /** The runtime's image working directory under the winter/store home (tools/image-prep.ts) -- write-denied to the shell. */
82
+ export declare const IMAGE_PREP_DIRNAME = "image-prep";
46
83
  /**
47
84
  * P7a fix r1 (Important-1): render ONE brand token as a case-insensitive, regex-escaped SBPL literal.
48
85
  *
@@ -478,7 +478,7 @@ export declare class TranscriptWriter implements SessionPersistence {
478
478
  private dialectRecord;
479
479
  static readBack(store: SessionStore, key: SessionKey): Promise<SessionStoreEntry[]>;
480
480
  }
481
- export declare const RUNTIME_ENGINE_VERSION = "0.0.35";
481
+ export declare const RUNTIME_ENGINE_VERSION = "0.0.38";
482
482
  export interface ResolvedEngineSession {
483
483
  config: RuntimeConfig;
484
484
  store: SessionPersistence | undefined;
@@ -130,6 +130,10 @@ export interface ChildEngineFactoryDeps {
130
130
  */
131
131
  parentAutoMemory?: RuntimeConfig["autoMemory"];
132
132
  parentWeb?: RuntimeConfig["web"];
133
+ /** `RuntimeConfig.reservedMcpServerNames`: an agent's own inline server never takes one (it is connected under a free name instead). */
134
+ parentReservedMcpServerNames?: RuntimeConfig["reservedMcpServerNames"];
135
+ /** `RuntimeConfig.legacyToolNames`, so a child resolves the host's old tool names as its parent does. */
136
+ parentLegacyToolNames?: RuntimeConfig["legacyToolNames"];
133
137
  /**
134
138
  * The session's pricing, stated-model and tool-secret seams, so a CHILD's engine has what its
135
139
  * parent's has. `priceUsage` is what makes a child's generations priced at all; the cost then
@@ -189,6 +193,27 @@ export interface ChildEngineFactoryDeps {
189
193
  */
190
194
  compactionControllerFactory?: () => CompactionController;
191
195
  }
196
+ type ChildMcpServers = NonNullable<RuntimeConfig["mcpServers"]>;
197
+ interface ChildServerAllocation {
198
+ /** The servers this generation connects itself, under the names it holds. */
199
+ servers: ChildMcpServers;
200
+ /** Declared name -> the name it is connected under (only for servers this generation connects). */
201
+ actual: Map<string, string>;
202
+ /** One legible line per server not connected under its declared name. */
203
+ notes: string[];
204
+ /**
205
+ * Declared names renamed because the HOST reserves them (`RuntimeConfig.reservedMcpServerNames`). Such a
206
+ * server is NOT "the server the definition named" for permission purposes: its declared spelling is the
207
+ * host's own server's, so it never becomes an extra identity (`mcpServerRenames`) — a saved rule on the
208
+ * host's `mcp__<reserved>__<tool>` must not reach it. Only its connected name governs it.
209
+ */
210
+ reservedRenames: Set<string>;
211
+ /** Releases every name this allocation claimed. Idempotent. */
212
+ release: () => void;
213
+ }
214
+ export declare function allocateChildScopedServers(declared: ChildMcpServers, parentVisible: ReadonlySet<string>, prefer?: ReadonlyMap<string, string>, parentDeclared?: Readonly<Record<string, {
215
+ type?: string;
216
+ }>>, reserved?: readonly string[]): ChildServerAllocation;
192
217
  export declare function createChildEngineFactory(deps: ChildEngineFactoryDeps): ChildEngineFactory;
193
218
  /**
194
219
  * What a child reports when the SPENDING CEILING ended it (whole-branch review MINOR 9). One constant,
@@ -196,3 +221,4 @@ export declare function createChildEngineFactory(deps: ChildEngineFactoryDeps):
196
221
  * not be told a retry is worth trying: the ceiling is the session's, so a retry hits it again.
197
222
  */
198
223
  export declare const CHILD_BUDGET_STOP_TEXT = "The agent stopped because the session reached its spending limit (maxBudgetUsd), so no further model calls could be made. Retrying will not help -- continue with what it produced, or ask the user to raise the session's budget.";
224
+ export {};
package/dist/testing.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  runEngine2,
4
4
  echoProvider2,
5
5
  resolveEngineSession2
6
- } from "./index-q5jpx2mz.js";
6
+ } from "./index-5xe59vkt.js";
7
7
  import"./index-1hef2gff.js";
8
8
  import"./index-mwew595z.js";
9
9
  import"./index-2wgfv0pa.js";
@@ -14,7 +14,7 @@ import {
14
14
  buildProductionWiring,
15
15
  withAutoSkillPermissions,
16
16
  restoreChildRoster
17
- } from "./index-838n35xx.js";
17
+ } from "./index-z3970wsj.js";
18
18
  import {
19
19
  Queue
20
20
  } from "./index-97t2rmtf.js";
@@ -48,6 +48,7 @@ import "./task-output.ts";
48
48
  import "./task-stop.ts";
49
49
  import "./task-update.ts";
50
50
  import "./todo-write.ts";
51
+ import "./search.ts";
51
52
  import "./tool-search.ts";
52
53
  import "./wait-for-mcp-servers.ts";
53
54
  import "./web-fetch.ts";
@@ -0,0 +1,4 @@
1
+ export declare const SEARCH_CANONICAL_NAME = "Search";
2
+ export declare const SEARCH_ANSWER_CAPABILITY = "winter.search-answer";
3
+ /** The Winter daemon's own description, verbatim (it is the interface the models were shown). */
4
+ export declare const SEARCH_DESCRIPTION = "Search the web and get back a written answer with its sources, in a single call. Ask a real question, not keywords \u2014 a search engine answers it and the answer comes back already synthesized, followed by the pages it came from. Use it freely whenever a fact might be newer than you are, or when the user asks about something current, and cite the URLs you used.";
@@ -0,0 +1,208 @@
1
+ import type { ToolResultBlock } from "./registry.js";
2
+ /**
3
+ * The exact refusal a model with no image input gets (the host's composer shows the same words). Exported
4
+ * so a host or a test matches it rather than retyping it.
5
+ */
6
+ export declare const MODEL_DOES_NOT_SUPPORT_IMAGES = "The selected model doesn't support images";
7
+ /** The image types every provider adapter can carry (Anthropic, Bedrock, Gemini and OpenAI all accept exactly these four). */
8
+ export declare const DELIVERABLE_IMAGE_TYPES: ReadonlySet<string>;
9
+ /**
10
+ * The largest image handed to a model, in raw bytes: 3.75 MiB, whose base64 is exactly 5 MiB.
11
+ *
12
+ * The strictest per-image limit among the providers Winter drives: "5 MB (base64-encoded) on Amazon
13
+ * Bedrock and Google Cloud" for Claude (https://platform.claude.com/docs/en/build-with-claude/vision,
14
+ * "Request limits"; the Claude API itself takes 10 MB). An image over a provider's limit stays in the
15
+ * conversation and fails EVERY later request, so the limit is applied here, before the image enters
16
+ * history, not in an adapter. A SOURCE file may be larger (up to `IMAGE_MAX_INPUT_BYTES`): it is shrunk
17
+ * and, if it must be, re-encoded as JPEG to fit (`prepareImageForModel`).
18
+ */
19
+ export declare const READ_IMAGE_MAX_BYTES = 3932160;
20
+ /** The largest width or height any provider accepts: "The maximum dimensions per image are 8000x8000 px" (same page). */
21
+ export declare const READ_IMAGE_MAX_DIMENSION = 8000;
22
+ /** What the bytes actually are, from their magic numbers -- never trusting the extension. `undefined` when none matches. */
23
+ export declare function sniffImageType(buf: Uint8Array): string | undefined;
24
+ export declare function parsePngDimensions(buf: Uint8Array): {
25
+ width: number;
26
+ height: number;
27
+ } | undefined;
28
+ export declare function parseGifDimensions(buf: Uint8Array): {
29
+ width: number;
30
+ height: number;
31
+ } | undefined;
32
+ export declare function parseBmpDimensions(buf: Uint8Array): {
33
+ width: number;
34
+ height: number;
35
+ } | undefined;
36
+ export declare function parseJpegDimensions(buf: Uint8Array): {
37
+ width: number;
38
+ height: number;
39
+ } | undefined;
40
+ /** WebP's canvas size from its VP8 (lossy), VP8L (lossless) or VP8X (extended) header. */
41
+ export declare function parseWebpDimensions(buf: Uint8Array): {
42
+ width: number;
43
+ height: number;
44
+ } | undefined;
45
+ /** A TIFF's first image size, from its first IFD's ImageWidth (256) / ImageLength (257) tags (SHORT or LONG). */
46
+ export declare function parseTiffDimensions(buf: Uint8Array): {
47
+ width: number;
48
+ height: number;
49
+ } | undefined;
50
+ /**
51
+ * A HEIC's size from its `ispe` (image spatial extents) boxes: the LARGEST one, since a grid image carries
52
+ * a small `ispe` per tile and one for the whole picture. A scan for well-formed `ispe` boxes (size 20,
53
+ * version/flags 0), not a full box walk; `sips -g`'s answer is taken too and the larger wins.
54
+ */
55
+ export declare function parseHeicDimensions(buf: Uint8Array): {
56
+ width: number;
57
+ height: number;
58
+ } | undefined;
59
+ /**
60
+ * A GIF's size for the PIXEL CHECK: the larger of its logical screen and every frame's own descriptor --
61
+ * a tiny logical screen can front a frame that declares 60000x60000 px, and the decoder allocates for the
62
+ * frame.
63
+ */
64
+ export declare function parseGifExtent(buf: Uint8Array): {
65
+ width: number;
66
+ height: number;
67
+ } | undefined;
68
+ export declare function parseImageDimensions(mediaType: string, bytes: Uint8Array): {
69
+ width: number;
70
+ height: number;
71
+ } | undefined;
72
+ /**
73
+ * The long edge an image is shrunk to before a model sees it: 1568 px, the size above which Claude's
74
+ * standard tier downsizes an image anyway ("Resolution and token cost",
75
+ * https://platform.claude.com/docs/en/build-with-claude/vision). Sending more only costs request bytes:
76
+ * the per-request limits (32 MB on the Claude API, 20 MB of inline data on Gemini) fill up after a few
77
+ * full-size screenshots, and past 20 images Anthropic refuses any image over 2000 px. At 1568 none of
78
+ * that is reached by an ordinary session.
79
+ */
80
+ export declare const IMAGE_MAX_LONG_EDGE = 1568;
81
+ /** The largest file Read or an MCP result may hand the resizer at all (a guard against absurd inputs, not a provider limit). */
82
+ export declare const IMAGE_MAX_INPUT_BYTES: number;
83
+ /**
84
+ * The most pixels an image may DECLARE before anything decodes it: 100 megapixels. A few-kilobyte PNG can
85
+ * declare 40000x40000 px, and decoding that (sips does, to resize it) takes gigabytes; a declared size is
86
+ * all a header costs. 100 MP is above any camera or screenshot in ordinary use (a 12K frame is ~80 MP).
87
+ */
88
+ export declare const IMAGE_MAX_PIXELS = 100000000;
89
+ /** An image ready for a model: the bytes to send, and what happened to them on the way. */
90
+ export interface PreparedImage {
91
+ ok: true;
92
+ mediaType: string;
93
+ bytes: Buffer;
94
+ width?: number;
95
+ height?: number;
96
+ /** The source as found. */
97
+ original: {
98
+ mediaType: string;
99
+ bytes: number;
100
+ width?: number;
101
+ height?: number;
102
+ };
103
+ /** True when the pixels were scaled down. */
104
+ resized: boolean;
105
+ /** Set when the format changed (a conversion, or a JPEG re-encode to fit the byte limit): the quality used for a JPEG. */
106
+ jpegQuality?: number;
107
+ }
108
+ export type ImagePreparation = PreparedImage | {
109
+ ok: false;
110
+ reason: string;
111
+ };
112
+ export interface PrepareImageOptions {
113
+ /**
114
+ * The runtime-owned directory the per-call working directory is made in: `<winter or store home>/
115
+ * image-prep` (`imagePrepWorkRoot`), which the Bash sandbox's seatbelt profile write-denies
116
+ * (sandbox/profile.ts, `IMAGE_PREP_DIRNAME`) -- NEVER the session temp dir, which a sandboxed shell can
117
+ * write (a link planted there would make `sips` write through it). A getter, read only when `sips`
118
+ * actually runs. `undefined`: no private place to work, so nothing is converted or resized.
119
+ */
120
+ workRoot: () => string | undefined;
121
+ /** Test seam: the `sips` binary (a missing path simulates a platform without it). Default `/usr/bin/sips`. */
122
+ sipsPath?: string;
123
+ /** Test seam: the byte limit. Default `READ_IMAGE_MAX_BYTES`. */
124
+ maxBytes?: number;
125
+ /** The turn's abort: an interrupt kills a running `sips`. */
126
+ signal?: AbortSignal;
127
+ }
128
+ /** A short, model-facing description of what happened, e.g. `image/png, 25856 bytes, 1568x1018, resized from 3024x1964`. */
129
+ export declare function describePreparedImage(image: PreparedImage): string;
130
+ /**
131
+ * Reads `path` only if it is a regular file, and never through a link: `O_NOFOLLOW` refuses a symlink at
132
+ * open time, and the type is checked on the OPENED descriptor, so nothing can be swapped in between the
133
+ * check and the read. `undefined` for anything else (a link, a directory, a missing file).
134
+ */
135
+ export declare function readRegularFileNoFollow(path: string): Buffer | undefined;
136
+ /**
137
+ * Where image work happens for a session: `<store home or winter home>/image-prep` -- the two anchors the
138
+ * seatbelt profile write-denies `image-prep` under, both handed to a session EXPLICITLY by its host. With
139
+ * neither, `undefined`: nothing is resized or converted (an image already within every limit is still
140
+ * sent as it is). There is deliberately NO fallback, and here is why each candidate is unsafe:
141
+ * - `~/<homeDirName>`: a guess at the user's real winter home, which a process given no home must not
142
+ * write into (and which a test would reach);
143
+ * - the session temp dir, the cwd, the outputs dir and any `allowWrite` entry: sandbox-WRITABLE roots,
144
+ * where a sandboxed command could plant a link;
145
+ * - `$TMPDIR` (the per-user temp dir): its DIRECT children are sandbox-writable (the profile's mktemp
146
+ * allowance), so a folder made there could be renamed away and replaced by a link mid-run.
147
+ */
148
+ export declare function imagePrepWorkRoot(ctx: {
149
+ storeHome?: string;
150
+ winterHome?: string;
151
+ }): string | undefined;
152
+ /**
153
+ * How many frames a GIF holds (image descriptors), walking its block structure; `undefined` when the
154
+ * structure cannot be walked. OpenAI takes only a "non-animated GIF"
155
+ * (https://developers.openai.com/api/docs/guides/images-vision) and Claude reads only the first frame.
156
+ */
157
+ export declare function gifFrameCount(buf: Uint8Array): number | undefined;
158
+ /** Every frame's own size (its image descriptor), in order; `undefined` when the structure cannot be walked to its end. */
159
+ export declare function gifFrameSizes(buf: Uint8Array): Array<{
160
+ width: number;
161
+ height: number;
162
+ }> | undefined;
163
+ /**
164
+ * Makes `source` ready for a model, or says why it cannot be.
165
+ *
166
+ * - The type is SNIFFED from the bytes; something that is none of PNG/JPEG/GIF/WebP/BMP/TIFF/HEIC is refused.
167
+ * - An image whose long edge is over `IMAGE_MAX_LONG_EDGE` is scaled down to it (aspect kept).
168
+ * - The format is kept where a provider takes it: PNG stays PNG and JPEG stays JPEG (quality 85 when
169
+ * re-encoded). `sips` cannot WRITE WebP, so a WebP that has to be rewritten becomes PNG. EVERY GIF
170
+ * becomes PNG (its first frame): Gemini reads no GIF, OpenAI no animated one, Claude only the first
171
+ * frame. BMP and TIFF, which no provider takes, become PNG; HEIC (a camera photo) becomes JPEG. A WebP
172
+ * that needs no rewrite is sent as it is.
173
+ * - An image DECLARING more than `IMAGE_MAX_PIXELS` is refused before anything decodes it.
174
+ * - Still over `READ_IMAGE_MAX_BYTES`? Re-encoded as JPEG at quality 85, 70, 55, then 40; still over,
175
+ * refused. Never truncated.
176
+ * - Without `sips`, or when it fails: the image as it is if it is already a deliverable type within the
177
+ * byte limit and `READ_IMAGE_MAX_DIMENSION` (and, for a GIF, a single frame), else a refusal naming the
178
+ * reason.
179
+ * - `sips` works in a FRESH private directory per call (`mkdtemp`) under `imagePrepWorkRoot` -- a tree
180
+ * the Bash sandbox is denied writing to, so no link can be planted where `sips` writes -- its input is
181
+ * written with `wx`, and its output is read through `readRegularFileNoFollow` (no link, no swap).
182
+ */
183
+ export declare function prepareImageForModel(source: Buffer, opts: PrepareImageOptions): Promise<ImagePreparation>;
184
+ /** One part of a result before it is made model-ready: text, or an image's raw bytes. */
185
+ export type RawResultPart = {
186
+ type: "text";
187
+ text: string;
188
+ } | {
189
+ type: "image";
190
+ bytes: Buffer;
191
+ };
192
+ export interface ModelResult {
193
+ /** The model-facing blocks, in order (adjacent text merged). */
194
+ blocks: ToolResultBlock[];
195
+ /** A text rendering of the same result (each image as `[image: …]`) -- for hooks, logs and a text-only reader. */
196
+ text: string;
197
+ /** True when at least one image block survived. */
198
+ hasImage: boolean;
199
+ }
200
+ /**
201
+ * Makes a mixed text/image result model-ready: every image goes through `prepareImageForModel`, and an
202
+ * image that cannot be shown -- a text-only model (`readsImages: false`), a refusal, or images past the
203
+ * result's total of `maxBytes` -- becomes a one-line `[image omitted: …]` note IN ITS PLACE, never
204
+ * dropped and never base64 text.
205
+ */
206
+ export declare function resultBlocksForModel(parts: readonly RawResultPart[], opts: PrepareImageOptions & {
207
+ readsImages: boolean;
208
+ }): Promise<ModelResult>;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * A real, decodable 8-bit RGB PNG of `width` x `height`. `noise: true` fills it with random pixels (it
3
+ * does not compress, so it is large); otherwise a repeating gradient (tiny once compressed).
4
+ */
5
+ export declare function realPng(width: number, height: number, opts?: {
6
+ noise?: boolean;
7
+ }): Buffer;
@@ -31,6 +31,8 @@ export interface ExaSearchHit {
31
31
  /** The result's highlight text, capped at `EXA_HIGHLIGHTS_MAX_CHARACTERS`. Empty when the backend sent none. */
32
32
  highlight: string;
33
33
  publishedDate?: string;
34
+ /** The result's `favicon` when the backend sent one (Exa's /search reference: "The URL of the favicon for the search result's domain"), https only. Never model-visible: `WebSearch` hands it to the HOST as `ToolResultPayload.siteIcons`. Absent from the basic text form and, as measured 2026-09-18, from the advanced JSON form too -- read when present. */
35
+ favicon?: string;
34
36
  }
35
37
  export interface ExaSearchParams {
36
38
  query: string;