@mokuyoaxis/dsh-iris 0.3.0-alpha.1 → 0.3.0-beta.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.
Files changed (174) hide show
  1. package/CHANGELOG.md +63 -1
  2. package/README.en.md +20 -13
  3. package/README.md +20 -13
  4. package/bin/dsh-iris.js +143 -51
  5. package/docs/AGENT_CORE_QUICKSTART.md +2 -2
  6. package/docs/AGENT_QUICKSTART.md +1 -1
  7. package/docs/ARCHITECTURE.md +5 -5
  8. package/docs/ARTIFACT_MANIFEST.md +4 -0
  9. package/docs/AUDIO_FLOWS.md +20 -4
  10. package/docs/BETA_SCOPE.md +42 -0
  11. package/docs/CONTACT_SHEET.md +1 -1
  12. package/docs/CORE_CONNECTIONS.md +3 -1
  13. package/docs/CORE_QUICKSTART.md +7 -5
  14. package/docs/CORE_RUNTIME_CONTRACT.md +3 -3
  15. package/docs/CORE_SERVICE.md +9 -5
  16. package/docs/CORE_SKILLS.md +2 -2
  17. package/docs/DSH_COMPATIBILITY.md +11 -1
  18. package/docs/DSH_CORE_MIGRATION.md +6 -0
  19. package/docs/DSH_RUNTIME.md +2 -2
  20. package/docs/HEADLESS_CLI.md +29 -4
  21. package/docs/HOST_ADAPTER_CONTRACT.md +5 -3
  22. package/docs/IMAGE_FLOW.md +2 -2
  23. package/docs/IMAGE_GENERATION.md +1 -1
  24. package/docs/LOCAL_API.md +2 -2
  25. package/docs/LOCAL_API_CONTRACT.md +17 -15
  26. package/docs/LOCAL_CREATION.md +2 -2
  27. package/docs/LOCAL_OPERATIONS.md +2 -2
  28. package/docs/LOCAL_SELECTION.md +1 -1
  29. package/docs/LOCAL_WEB.md +11 -5
  30. package/docs/MCP.md +30 -9
  31. package/docs/MEDIA_SCHEDULING.md +72 -0
  32. package/docs/NETWORK_RESOURCES.md +106 -0
  33. package/docs/NETWORK_ROUTING.md +103 -0
  34. package/docs/NETWORK_WORKBENCH.md +65 -0
  35. package/docs/PANEL_EXTENSIONS.md +1 -1
  36. package/docs/PLUGINS.md +9 -5
  37. package/docs/PLUGIN_DEVELOPMENT.md +39 -0
  38. package/docs/PLUGIN_STARTUP.md +15 -2
  39. package/docs/PROVIDER_ADAPTER_CONTRACT.md +8 -2
  40. package/docs/PROVIDER_CREDENTIALS.md +67 -0
  41. package/docs/PROVIDER_EXTENSIONS.md +8 -4
  42. package/docs/PROVIDER_HEALTH.md +4 -2
  43. package/docs/PROVIDER_NETWORK.md +45 -0
  44. package/docs/PROVIDER_PLUGIN_CONTRACT.md +24 -0
  45. package/docs/PROVIDER_SETTINGS.md +13 -7
  46. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +3 -1
  47. package/docs/ROADMAP.md +6 -6
  48. package/docs/SECURITY.md +1 -1
  49. package/docs/SYNC_DELIVERY_RECOVERY.md +5 -1
  50. package/docs/VIDEO_FLOWS.md +89 -0
  51. package/docs/VISION_MODEL.md +2 -0
  52. package/docs/WEB_PROTOTYPE.md +3 -3
  53. package/docs/WEB_RECOVERY.md +2 -2
  54. package/docs/WORKBENCH_ARTIFACTS.md +6 -0
  55. package/docs/WORKBENCH_LOGIN.md +2 -2
  56. package/docs/releases/0.3.0-beta.1.md +34 -0
  57. package/docs/screenshots.md +2 -2
  58. package/lib/actions.js +5 -4
  59. package/lib/adapters.js +58 -41
  60. package/lib/api.js +23 -13
  61. package/lib/application-runtime.js +44 -15
  62. package/lib/application-writer.js +193 -0
  63. package/lib/client.d.ts +3 -3
  64. package/lib/client.js +62 -25
  65. package/lib/comfyui-adapter.js +3 -2
  66. package/lib/comfyui-client.js +10 -1
  67. package/lib/command-service.js +18 -5
  68. package/lib/config.js +13 -9
  69. package/lib/configured-image-runtime.js +28 -9
  70. package/lib/connection-skill.js +9 -0
  71. package/lib/core-artifact-store.js +68 -26
  72. package/lib/core-bridge-tools.js +5 -3
  73. package/lib/core-capability-catalog.js +21 -11
  74. package/lib/core-delivery-cache.js +8 -3
  75. package/lib/core-service-client.js +6 -2
  76. package/lib/core-tasks.js +86 -11
  77. package/lib/creation-request-store.js +8 -5
  78. package/lib/doctor.js +2 -2
  79. package/lib/dsh-core-adapter.js +61 -9
  80. package/lib/dsh-provider-config.js +1 -1
  81. package/lib/dsh-service-bridge.js +1 -0
  82. package/lib/flow-store.js +64 -17
  83. package/lib/image-flow.js +39 -17
  84. package/lib/index.js +10 -5
  85. package/lib/local-api-projection.js +6 -2
  86. package/lib/local-api.js +137 -49
  87. package/lib/local-creation.js +12 -5
  88. package/lib/local-operations.js +2 -2
  89. package/lib/local-request-root.js +2 -2
  90. package/lib/local-web-assets.js +2 -0
  91. package/lib/mcp-http.js +82 -9
  92. package/lib/mcp-server.js +23 -1
  93. package/lib/media-scheduler.js +194 -0
  94. package/lib/model-directory-service.js +25 -8
  95. package/lib/music-transport.js +4 -2
  96. package/lib/network-config-service.js +123 -0
  97. package/lib/network-config.js +127 -0
  98. package/lib/network-doctor.js +94 -0
  99. package/lib/network-management.js +51 -0
  100. package/lib/network-policy.js +19 -0
  101. package/lib/network-route-evidence.js +38 -0
  102. package/lib/network-routing.js +283 -0
  103. package/lib/operation-runner.js +83 -38
  104. package/lib/operation-store.js +6 -6
  105. package/lib/plugins.d.ts +25 -6
  106. package/lib/plugins.js +2 -1
  107. package/lib/provider-adapter.js +2 -2
  108. package/lib/provider-adapters.js +37 -5
  109. package/lib/provider-catalog.js +33 -16
  110. package/lib/provider-config-format.js +51 -0
  111. package/lib/provider-config-service.js +52 -22
  112. package/lib/provider-context.d.ts +90 -0
  113. package/lib/provider-context.js +42 -0
  114. package/lib/provider-credentials.js +97 -0
  115. package/lib/provider-diagnostics.js +5 -5
  116. package/lib/provider-health.js +10 -5
  117. package/lib/provider-monitor.js +5 -3
  118. package/lib/provider-network.js +52 -0
  119. package/lib/provider-protocol.js +3 -2
  120. package/lib/provider-registry.js +15 -5
  121. package/lib/provider-response.js +10 -0
  122. package/lib/provider-settings.js +14 -12
  123. package/lib/provider-task-runner.js +113 -37
  124. package/lib/provider-transport.js +16 -0
  125. package/lib/render.js +11 -1
  126. package/lib/scheduling-policy.js +66 -0
  127. package/lib/tasks.js +3 -2
  128. package/lib/video-flow-input.js +16 -0
  129. package/lib/video-input.js +12 -5
  130. package/lib/vision-model-routing.js +2 -0
  131. package/lib/web/app.js +107 -46
  132. package/lib/web/connections.js +151 -12
  133. package/lib/web/creation.js +29 -25
  134. package/lib/web/flow-view.js +4 -3
  135. package/lib/web/gallery.js +17 -16
  136. package/lib/web/index.html +58 -5
  137. package/lib/web/media-client.js +18 -4
  138. package/lib/web/network.js +232 -0
  139. package/lib/web/plugin-panel.js +4 -2
  140. package/lib/web/plugin-status.js +140 -0
  141. package/lib/web/providers.js +34 -13
  142. package/lib/web/recovery.js +2 -2
  143. package/lib/web/styles.css +54 -1
  144. package/lib/web/transport.js +6 -1
  145. package/lib/workbench-artifacts.js +3 -2
  146. package/package.json +5 -2
  147. package/skills/iris-core/references/comfyui.md +1 -1
  148. package/templates/plugin-starter/LICENSE +21 -0
  149. package/templates/plugin-starter/README.md +74 -0
  150. package/templates/plugin-starter/assets/peach.png +0 -0
  151. package/templates/plugin-starter/assets/violet.png +0 -0
  152. package/templates/plugin-starter/index.mjs +27 -0
  153. package/templates/plugin-starter/package.json +18 -0
  154. package/templates/plugin-starter/scripts/core-cli.mjs +20 -0
  155. package/templates/plugin-starter/scripts/environment.mjs +65 -0
  156. package/templates/plugin-starter/scripts/mcp-probe.mjs +22 -0
  157. package/templates/plugin-starter/scripts/setup.mjs +42 -0
  158. package/templates/plugin-starter/scripts/smoke.mjs +76 -0
  159. package/templates/plugin-starter/web/board.css +27 -0
  160. package/templates/plugin-starter/web/board.mjs +160 -0
  161. package/templates/plugin-starter/web/png.mjs +23 -0
  162. package/templates/provider-starter/LICENSE +21 -0
  163. package/templates/provider-starter/README.md +50 -0
  164. package/templates/provider-starter/index.d.mts +5 -0
  165. package/templates/provider-starter/index.mjs +165 -0
  166. package/templates/provider-starter/package.json +13 -0
  167. package/templates/provider-starter/scripts/core-cli.mjs +22 -0
  168. package/templates/provider-starter/scripts/environment.mjs +122 -0
  169. package/templates/provider-starter/scripts/fixture.mjs +54 -0
  170. package/templates/provider-starter/scripts/mcp-probe.mjs +21 -0
  171. package/templates/provider-starter/scripts/protocol-smoke.mjs +48 -0
  172. package/templates/provider-starter/scripts/setup.mjs +44 -0
  173. package/templates/provider-starter/scripts/smoke.mjs +145 -0
  174. package/user_guide.md +3 -3
@@ -1,6 +1,9 @@
1
1
  // Native HTTP transport shared by generation and explicit read-only workflow checks.
2
2
  import { ProviderContractError } from './provider-contract.js';
3
3
  import { normalizeMediaEndpoint, providerApiKey, providerMediaBaseUrl } from './provider-protocol.js';
4
+ import { resolveProviderCredential } from './provider-credentials.js';
5
+ import { inheritedProviderFetch } from './provider-network.js';
6
+ import { providerNetworkRequirement, sameNetworkRequirement, networkPolicyError } from './network-policy.js';
4
7
 
5
8
  export function comfyFail(message, stage, category = 'protocol', acceptance = stage === 'validate' ? 'not_accepted' : 'accepted', details = {}) {
6
9
  throw new ProviderContractError(message, { stage, category, acceptance, ...details });
@@ -15,7 +18,12 @@ async function bounded(response, limit, stage) {
15
18
  }
16
19
  return Buffer.concat(chunks, size);
17
20
  }
18
- export function createComfyUiClient(provider, { fetchPort = globalThis.fetch } = {}) {
21
+ export function createComfyUiClient(provider, { fetchPort = inheritedProviderFetch, network } = {}) {
22
+ const requirement = providerNetworkRequirement(provider);
23
+ if (requirement && (!network || !sameNetworkRequirement(requirement, network.networkRequirement) || network.providerId !== provider.id)) {
24
+ throw networkPolicyError('IRIS_NETWORK_CONTEXT_REQUIRED', 'ComfyUI 需要供应商匹配的网络上下文,不会静默直连');
25
+ }
26
+ provider = resolveProviderCredential(provider);
19
27
  const base = normalizeMediaEndpoint(providerMediaBaseUrl(provider)), key = providerApiKey(provider);
20
28
  const route = leaf => base + '/' + leaf;
21
29
  async function request(url, { method = 'GET', value, signal, stage, timeoutMs = 15000, binary = false } = {}) {
@@ -26,6 +34,7 @@ export function createComfyUiClient(provider, { fetchPort = globalThis.fetch } =
26
34
  headers: { ...(key ? { Authorization: 'Bearer ' + key } : {}), ...(value ? { 'Content-Type': 'application/json' } : {}) },
27
35
  ...(value ? { body: JSON.stringify(value) } : {}) });
28
36
  } catch (error) {
37
+ if (error instanceof ProviderContractError) throw error;
29
38
  comfyFail('ComfyUI 服务通信失败,请核对原任务与服务状态', stage,
30
39
  error.name === 'AbortError' ? 'aborted' : error.name === 'TimeoutError' ? 'timeout' : 'network',
31
40
  stage === 'submit' ? 'unknown' : stage === 'validate' ? 'not_accepted' : 'accepted');
@@ -429,23 +429,34 @@ export function createCommandService(runtime, ports = {}) {
429
429
  if (input.confirm_billing !== true) {
430
430
  throw commandError('IRIS_COMMAND_BILLING_CONFIRM_REQUIRED', '重试会创建新任务并可能产生重复生成费用;必须显式确认计费');
431
431
  }
432
- return runtime.run('execute', async ({ dataRoot }) => {
432
+ return runtime.run('execute', async ({ dataRoot, signal }) => {
433
433
  const before = inspectCoreTask(dataRoot, input.task_id);
434
434
  if (before.phase !== 'terminal' || (before.outcome === 'succeeded' && before.deliveryState === 'ready')) {
435
435
  throw commandError('IRIS_TASK_NOT_RETRYABLE', '只有终态且未成功交付的 Task 可以重试为新任务');
436
436
  }
437
- let normalized;
437
+ let normalized, requestedVideoMode;
438
+ let suppliedInput = input.provider_input;
439
+ if (before.capability === 'video') {
440
+ if (!plain(suppliedInput) || !['t2v', 'i2v', 's2v'].includes(suppliedInput.video_mode)) {
441
+ throw commandError('IRIS_COMMAND_INPUT_INVALID', '视频重试必须明确 video_mode(t2v/i2v/s2v),并重新提供该模式所需的首帧和音频;旧 Task 不保存这些输入');
442
+ }
443
+ const { video_mode, ...generation } = suppliedInput;
444
+ requestedVideoMode = video_mode; suppliedInput = generation;
445
+ }
438
446
  if (before.capability === 'music' && input.provider_input &&
439
447
  ['retry_502', 'retry_delay_ms'].some(field => Object.hasOwn(input.provider_input, field))) {
440
448
  throw commandError('IRIS_COMMAND_INPUT_INVALID', 'task.retry 只执行一次重新生成;502重试配置请使用Music生成入口');
441
449
  }
442
- try { normalized = normalizeGenerationInput(before.capability, input.provider_input, { allowVideoPaths: before.capability === 'video' }); }
450
+ try { normalized = normalizeGenerationInput(before.capability, suppliedInput, { allowVideoPaths: before.capability === 'video' }); }
443
451
  catch (error) {
444
452
  if (error instanceof GenerationInputError) throw commandError('IRIS_COMMAND_INPUT_INVALID', error.message);
445
453
  throw error;
446
454
  }
455
+ if (before.capability === 'video' && normalized.videoMode !== requestedVideoMode) {
456
+ throw commandError('IRIS_COMMAND_INPUT_INVALID', 'video_mode 与重新提供的首帧/音频不符;i2v 需首帧,s2v 需首帧和音频,可通过 CLI 重新提供本地文件');
457
+ }
447
458
  const providerInput = normalized.providerInput;
448
- if (before.sourceArtifactId && !normalized.sourceArtifactId) {
459
+ if (before.capability === 'image' && before.sourceArtifactId && !normalized.sourceArtifactId) {
449
460
  throw commandError('IRIS_COMMAND_INPUT_INVALID', '改图重试必须重新提供 source_artifact_id 和修改指令');
450
461
  }
451
462
  if (Object.hasOwn(input.provider_input, 'model_ref')) {
@@ -454,7 +465,9 @@ export function createCommandService(runtime, ports = {}) {
454
465
  const modelRef = String(input.model_ref || '').trim();
455
466
  if (input.model_ref !== undefined && !modelRef) throw commandError('IRIS_COMMAND_INPUT_INVALID', 'model_ref 必须是 providerId::modelId 复合引用');
456
467
  if (typeof ports.resolveTaskCandidates !== 'function') throw commandError('IRIS_COMMAND_PROVIDER_REQUIRED', 'task.retry 需要宿主显式提供候选链 resolver');
457
- let candidates = await ports.resolveTaskCandidates({ capability: before.capability, modelRef });
468
+ signal.throwIfAborted();
469
+ let candidates = await ports.resolveTaskCandidates({ capability: before.capability, modelRef, signal });
470
+ signal.throwIfAborted();
458
471
  if (before.capability === 'video') candidates = videoTaskCandidates(candidates, normalized);
459
472
  const result = await createProviderTaskRunner(runtime).submit({
460
473
  capability: before.capability,
package/lib/config.js CHANGED
@@ -7,7 +7,7 @@ import { refreshModelDirectory } from './model-directory-service.js';
7
7
  /**
8
8
  * Iris 配置存储:$DSH_HOME/iris/v1/providers.json
9
9
  * Iris 独立持有配置;旧工作台仅作为用户显式指定的一次性导入来源。
10
- * Key 明文落盘(0600);接口层永远不回明文。
10
+ * 内联 Key 明文落盘(0600);环境引用只存名称,解析值不持久化;接口层不回明文。
11
11
  */
12
12
  import fs from 'node:fs';
13
13
  import path from 'node:path';
@@ -17,6 +17,7 @@ import * as models from './models.js';
17
17
  import { atomicWritePrivate, chmodPrivateFile, privateSibling } from './private-storage.js';
18
18
  import { selectMediaProtocol, selectImageProtocol, normalizeImageProtocol, normalizeMusicProtocol, isConfiguredProvider } from './provider-protocol.js';
19
19
  import { redactProviderMessage } from './provider-contract.js';
20
+ import { validateProviderCredential, providerCredentialStatus, providerCredentialValue, applyCredentialPatch } from './provider-credentials.js';
20
21
  import {
21
22
  PROVIDER_HEALTH_FRESH_MS,
22
23
  aggregateHealthStates,
@@ -37,6 +38,7 @@ function validateStoredConfig(value) {
37
38
  for (const [capability, assignment] of Object.entries(value.assignments)) models.functionPolicy(assignment, capability);
38
39
  for (const provider of value.providers) {
39
40
  if (!isPlainObject(provider)) throw new Error('provider 条目必须是对象');
41
+ validateProviderCredential(provider);
40
42
  validateModelDirectory(provider.modelDirectory);
41
43
  if (provider.id !== undefined && typeof provider.id !== 'string') throw new Error('provider.id 必须是字符串');
42
44
  if (provider.baseUrl !== undefined && typeof provider.baseUrl !== 'string') throw new Error('provider.baseUrl 必须是字符串');
@@ -47,7 +49,7 @@ function validateStoredConfig(value) {
47
49
  if (!isPlainObject(ref) || typeof ref.provider !== 'string' || !ref.provider.trim()
48
50
  || typeof ref.settingsNs !== 'string' || !ref.settingsNs.trim()
49
51
  || Object.keys(ref).some(key => !['provider', 'settingsNs'].includes(key))) throw new Error('DSH 连接引用无效');
50
- if (provider.apiKey !== undefined || provider.baseUrl !== undefined) throw new Error('DSH 引用不能同时保存密钥或普通 Base URL');
52
+ if (provider.apiKey !== undefined || provider.apiKeyEnv !== undefined || provider.baseUrl !== undefined) throw new Error('DSH 引用不能同时保存密钥、环境引用或普通 Base URL');
51
53
  }
52
54
  if (provider.models !== undefined && !Array.isArray(provider.models)) throw new Error('provider.models 必须是数组');
53
55
  for (const model of provider.models || []) {
@@ -72,7 +74,7 @@ function authModeOf(provider) {
72
74
  function missingCredentials(provider) {
73
75
  if (!provider || authModeOf(provider) === 'none') return false;
74
76
  if (provider.dshProvider) return false;
75
- return typeof provider.apiKey !== 'string' || !provider.apiKey.trim();
77
+ return providerCredentialStatus(provider) !== 'ready';
76
78
  }
77
79
 
78
80
  function normalizeStoredProvider(provider) {
@@ -133,6 +135,9 @@ export function load() {
133
135
  for (const provider of cache.providers) normalized = normalizeStoredProvider(provider) || normalized;
134
136
  if (normalized) persist();
135
137
  } catch (err) {
138
+ // A rejected source/field combination is actionable configuration, not corrupt
139
+ // JSON. Keep it intact so a policy change cannot quarantine user providers.
140
+ if (err.code === 'IRIS_CONFIG_INPUT_INVALID') throw err;
136
141
  // 文件存在但损坏:隔离,绝不静默覆盖证据
137
142
  const backup = privateSibling(file, 'corrupted');
138
143
  try {
@@ -400,9 +405,8 @@ function healthObservation(provider, modelId, capability) {
400
405
  function safeHealthNote(provider, value) {
401
406
  const raw = String(value ?? '').trim();
402
407
  if (!raw) return '';
403
- let note = redactProviderMessage(raw, 200);
404
- const key = String(provider && scopedProvider(provider).apiKey || '');
405
- if (key) note = note.split(key).join('[REDACTED]');
408
+ const key = providerCredentialValue(provider && scopedProvider(provider));
409
+ let note = redactProviderMessage(key ? raw.split(key).join('[REDACTED]') : raw, 200);
406
410
  return note;
407
411
  }
408
412
 
@@ -587,7 +591,7 @@ export function upsert(provider) {
587
591
  if (!isPlainObject(reference) || typeof reference.provider !== 'string' || !reference.provider.trim()
588
592
  || typeof reference.settingsNs !== 'string' || !reference.settingsNs.trim()
589
593
  || Object.keys(reference).some(key => !['provider', 'settingsNs'].includes(key))) throw new Error('DSH 连接引用无效');
590
- if (['apiKey', 'baseUrl'].some(key => rest[key] !== undefined || existing?.[key] !== undefined)) {
594
+ if (['apiKey', 'apiKeyEnv', 'baseUrl'].some(key => rest[key] !== undefined || existing?.[key] !== undefined)) {
591
595
  throw new Error('DSH 引用不保存密钥或普通 Base URL;请新建引用供应商');
592
596
  }
593
597
  rest.dshProvider = { provider: reference.provider.trim(), settingsNs: reference.settingsNs.trim() };
@@ -602,7 +606,7 @@ export function upsert(provider) {
602
606
  const i = c.providers.findIndex((p) => p.id === id);
603
607
  if (i >= 0) {
604
608
  const previous = c.providers[i];
605
- const next = { ...previous, ...rest };
609
+ const next = applyCredentialPatch(previous, rest);
606
610
  if (Object.prototype.hasOwnProperty.call(rest, 'mediaProtocol')) next.protocolInferred = false;
607
611
  normalizeStoredProvider(next);
608
612
  resetHealthOnCriticalChange(previous, next, rest);
@@ -613,7 +617,7 @@ export function upsert(provider) {
613
617
  return next;
614
618
  }
615
619
  }
616
- const fresh = { id: 'iris_' + Math.random().toString(36).slice(2, 8), ...rest };
620
+ const fresh = applyCredentialPatch(null, { id: 'iris_' + Math.random().toString(36).slice(2, 8), ...rest });
617
621
  fresh.protocolInferred = false;
618
622
  normalizeStoredProvider(fresh);
619
623
  c.providers.push(fresh);
@@ -1,42 +1,61 @@
1
1
  // Shared CLI/service assembly. Configuration is read only during explicit commands.
2
2
  import path from 'node:path';
3
- import { loadProviderCatalog, imageCandidatesFromCatalog, ttsCandidatesFromCatalog, transcribeCandidatesFromCatalog, providerForTaskFromCatalog, providerTaskBinding } from './provider-catalog.js';
3
+ import { loadProviderCatalog, imageCandidatesFromCatalog, ttsCandidatesFromCatalog, transcribeCandidatesFromCatalog, videoCandidatesFromCatalog, providerForTaskFromCatalog, providerTaskBinding } from './provider-catalog.js';
4
4
  import { createConfiguredProviderAdapter } from './provider-adapters.js';
5
5
  import { recordCatalogAvailability, recordCatalogRateLimit } from './provider-config-service.js';
6
6
  import { parseModelRef } from './models.js';
7
7
  import { supportsImageEdit } from './provider-protocol.js';
8
8
  import { compileComfyWorkflow } from './comfyui-workflow.js';
9
+ import { providerNetworkRequirement, sameNetworkRequirement, networkPolicyError } from './network-policy.js';
9
10
 
10
- export function createTrackedProviderAdapter(provider, providerConfigPath, task, registry) {
11
+ export function createTrackedProviderAdapter(provider, providerConfigPath, task, registry, { network, modelRef, deferCooling = false } = {}) {
12
+ if (task && !sameNetworkRequirement(task.networkRequirement, providerNetworkRequirement(provider))) {
13
+ throw networkPolicyError('IRIS_NETWORK_REQUIREMENT_MISMATCH', '原 Task 的网络要求与当前供应商目录不一致;不会隐式转换或解除');
14
+ }
11
15
  return createConfiguredProviderAdapter(provider, {
12
16
  registry,
13
- modelId: parseModelRef(task?.modelRef)?.modelId,
17
+ ...(network !== undefined ? { network } : {}),
18
+ modelId: parseModelRef(task?.modelRef || modelRef)?.modelId,
19
+ deferCooling,
14
20
  onModelAvailability: event => recordCatalogAvailability(providerConfigPath, event),
15
21
  onRateLimit: event => recordCatalogRateLimit(providerConfigPath, event)
16
22
  });
17
23
  }
18
24
 
19
- export function configuredImagePorts(providerConfigPath, registry) {
25
+ export function configuredImagePorts(providerConfigPath, registry, { resolveProviderNetwork, deferCooling = false } = {}) {
20
26
  if (typeof providerConfigPath !== 'string' || !path.isAbsolute(providerConfigPath)) throw new TypeError('Provider 配置须为显式绝对路径');
27
+ if (resolveProviderNetwork !== undefined && typeof resolveProviderNetwork !== 'function') throw new TypeError('resolveProviderNetwork 必须是函数');
28
+ const adapterFor = (provider, task, modelRef) => createTrackedProviderAdapter(provider, providerConfigPath, task, registry, {
29
+ modelRef, deferCooling,
30
+ network: resolveProviderNetwork?.({ providerId: provider.id, modelRef: modelRef || task?.modelRef, taskId: task?.id,
31
+ ...(providerNetworkRequirement(provider) ? { networkRequirement: providerNetworkRequirement(provider) } : {}) })
32
+ });
21
33
  return Object.freeze({
34
+ resolveVideoCandidates({ modelRef, signal }) {
35
+ signal?.throwIfAborted();
36
+ return videoCandidatesFromCatalog(loadProviderCatalog(providerConfigPath), modelRef, registry, { deferCooling }).map(route => ({
37
+ adapter: adapterFor(route.provider, undefined, route.modelRef), model: route.modelRef,
38
+ providerBinding: providerTaskBinding(route.provider, registry), selectionReason: 'explicit'
39
+ }));
40
+ },
22
41
  resolveAudioCandidates({ capability, modelRef, signal }) {
23
42
  signal?.throwIfAborted();
24
43
  if (!['tts', 'transcribe'].includes(capability)) throw new TypeError('未知音频能力');
25
44
  const select = capability === 'tts' ? ttsCandidatesFromCatalog : transcribeCandidatesFromCatalog;
26
- return select(loadProviderCatalog(providerConfigPath), modelRef, registry).map(route => ({
27
- adapter: createTrackedProviderAdapter(route.provider, providerConfigPath, undefined, registry), model: route.modelRef,
45
+ return select(loadProviderCatalog(providerConfigPath), modelRef, registry, { deferCooling }).map(route => ({
46
+ adapter: adapterFor(route.provider, undefined, route.modelRef), model: route.modelRef,
28
47
  providerBinding: providerTaskBinding(route.provider, registry), selectionReason: 'explicit'
29
48
  }));
30
49
  },
31
50
  resolveImageCandidates({ modelRef, signal, input }) {
32
51
  signal?.throwIfAborted();
33
- return imageCandidatesFromCatalog(loadProviderCatalog(providerConfigPath), modelRef, registry).map(route => {
52
+ return imageCandidatesFromCatalog(loadProviderCatalog(providerConfigPath), modelRef, registry, { deferCooling }).map(route => {
34
53
  if (input?.workflow_parameters !== undefined) {
35
54
  if (route.provider.mediaProtocol !== 'comfyui') throw Object.assign(new TypeError('工作流参数仅用于 ComfyUI 模板'), { code: 'IRIS_COMFYUI_PARAMETERS_INVALID' });
36
55
  compileComfyWorkflow(route.provider.comfyWorkflow, input);
37
56
  }
38
57
  return {
39
- adapter: createTrackedProviderAdapter(route.provider, providerConfigPath, undefined, registry), model: route.modelRef,
58
+ adapter: adapterFor(route.provider, undefined, route.modelRef), model: route.modelRef,
40
59
  supportsImageEdit: supportsImageEdit(route.provider.mediaProtocol, route.model, registry),
41
60
  providerBinding: providerTaskBinding(route.provider, registry), selectionReason: 'explicit'
42
61
  };
@@ -44,7 +63,7 @@ export function configuredImagePorts(providerConfigPath, registry) {
44
63
  },
45
64
  resolveTaskAdapter(task, { signal } = {}) {
46
65
  signal?.throwIfAborted();
47
- return createTrackedProviderAdapter(providerForTaskFromCatalog(loadProviderCatalog(providerConfigPath), task, registry), providerConfigPath, task, registry);
66
+ return adapterFor(providerForTaskFromCatalog(loadProviderCatalog(providerConfigPath), task, registry), task);
48
67
  }
49
68
  });
50
69
  }
@@ -61,6 +61,14 @@ function imageGuidance(tools) {
61
61
  ];
62
62
  }
63
63
 
64
+ function videoGuidance(tools) {
65
+ if (!tools.includes('iris_core_create_video') && !tools.includes('iris_core_video')) return [];
66
+ return ['## Text to video · 文生视频', '',
67
+ 'Use the actual video directory when selected. Keep an explicit permitted modelRef; do not guess models or supported sizes/durations. This fixed flow takes a prompt and optional size/duration (1–60 seconds). It has no image/audio input or S2V path. Omit optional parameters unless required by the user or model.', '',
68
+ 'Creation saves an intent without generation. Only an authorized continue can submit it and incur charges. Keep distinct stable creation/execution UUIDs, the original Run, Task and Operation. An asynchronous submission needs another explicit continue to observe the SAME Task. Unknown acceptance must never cause another submission. Local stop stops waiting, not remote billing.', '',
69
+ 'Video has one output and needs no image selection. If the video reading tool is selected, it returns an iris://video/ resource_link for the original MP4/WebM (at most 32 MiB). Use standard resources/read to obtain its bytes; each read checks current Core authorization. Client playback support varies. Do not replace it with a provider URL or claim to have watched it merely from a reference.', ''];
70
+ }
71
+
64
72
  export function connectionSkillResources(input, metadata) {
65
73
  const bundle = checkedConnectionSkillBundle(input), skillName = 'iris-mcp-' + bundle.connectionId;
66
74
  const write = bundle.tools.some(name => toolDeclarations.get(name).scope !== 'read');
@@ -74,6 +82,7 @@ export function connectionSkillResources(input, metadata) {
74
82
  ...bundle.tools.map(name => '- `' + name + '`: ' + toolDeclarations.get(name).guidance), '',
75
83
  ...imageGuidance(bundle.tools),
76
84
  ...audioGuidance(bundle.tools),
85
+ ...videoGuidance(bundle.tools),
77
86
  '## Work within the selection · 遵循所选范围', '',
78
87
  'Choose only the steps needed for the user\'s request and available in the list above. A partial selection may not support a complete workflow. Ask for missing input or report a missing tool rather than inventing IDs, changing grants, creating another connection, or bypassing permissions through a shell/CLI.', '',
79
88
  'This is a saved instruction snapshot. Core checks actual current authorization on every request; loading this skill grants no permission. If a tool is unavailable or denied, report the mismatch and have the owner regenerate the bundle when appropriate. Do not automatically retry a write.', '',
@@ -46,7 +46,8 @@ function dirs(dataRoot) {
46
46
  objects: path.join(base, 'objects'),
47
47
  records: path.join(base, 'records'),
48
48
  manifests: path.join(base, 'manifests'),
49
- index: path.join(base, 'index.json')
49
+ index: path.join(base, 'index.json'),
50
+ indexDirty: path.join(base, 'index.dirty')
50
51
  };
51
52
  }
52
53
 
@@ -186,7 +187,7 @@ function verifyObject(dataRoot, manifest, verifyContent = true) {
186
187
  return object;
187
188
  }
188
189
 
189
- function legacyManifest(dataRoot, record, id) {
190
+ function legacyObject(dataRoot, record, id) {
190
191
  if (!record || record.schemaVersion !== 0 || record.id !== id
191
192
  || typeof record.kind !== 'string' || !MEDIA_EXTENSION[record.mediaType]
192
193
  || !Number.isSafeInteger(record.size) || record.size <= 0
@@ -197,6 +198,15 @@ function legacyManifest(dataRoot, record, id) {
197
198
  const object = path.join(dirs(dataRoot).objects, record.object);
198
199
  const stat = regularFile(object);
199
200
  if (stat.size !== record.size) fail('IRIS_ARTIFACT_DIGEST_MISMATCH', '早期 Core Artifact 大小不一致');
201
+ return object;
202
+ }
203
+
204
+ function legacyManifest(dataRoot, record, id, indexed) {
205
+ const object = legacyObject(dataRoot, record, id);
206
+ if (indexed && (indexed.id !== id || indexed.createdAt !== record.createdAt
207
+ || indexed.mediaType !== record.mediaType || indexed.size !== record.size || !HASH.test(indexed.digest))) {
208
+ fail('IRIS_ARTIFACT_INDEX_INVALID', '早期 Artifact 与缓存索引不一致,请显式重建索引');
209
+ }
200
210
  return {
201
211
  schemaVersion: 0,
202
212
  id,
@@ -205,13 +215,13 @@ function legacyManifest(dataRoot, record, id) {
205
215
  size: record.size,
206
216
  createdAt: record.createdAt,
207
217
  object: record.object,
208
- digest: { algorithm: 'sha256', value: hashFile(object) },
218
+ digest: { algorithm: 'sha256', value: indexed ? indexed.digest : hashFile(object) },
209
219
  relations: [],
210
220
  metadata: { ...record.metadata }
211
221
  };
212
222
  }
213
223
 
214
- function readArtifact(dataRoot, id, { allowLegacy = true, verifyContent = true } = {}) {
224
+ function readArtifact(dataRoot, id, { allowLegacy = true, verifyContent = true, legacyIndex } = {}) {
215
225
  checkStore(dataRoot);
216
226
  const stable = stableId(id);
217
227
  let record;
@@ -226,7 +236,7 @@ function readArtifact(dataRoot, id, { allowLegacy = true, verifyContent = true }
226
236
  manifest = validateManifest(
227
237
  jsonFile(manifestPath(dataRoot, stable), 'IRIS_ARTIFACT_MANIFEST_INVALID'), stable
228
238
  );
229
- } else if (allowLegacy) manifest = legacyManifest(dataRoot, record, stable);
239
+ } else if (allowLegacy) manifest = legacyManifest(dataRoot, record, stable, legacyIndex?.(stable));
230
240
  else fail('IRIS_ARTIFACT_INVALID', 'Artifact 提交记录无效');
231
241
  const object = verifyObject(dataRoot, manifest, verifyContent);
232
242
  return { manifest, object, legacy: record.schemaVersion === 0 };
@@ -252,11 +262,9 @@ function readIndexRevision(file) {
252
262
 
253
263
  function committedIds(dataRoot) {
254
264
  const directory = dirs(dataRoot).records;
255
- try {
256
- return fs.readdirSync(directory)
257
- .map((name) => name.endsWith('.json') ? name.slice(0, -5) : '')
258
- .filter((id) => ID.test(id));
259
- } catch (_) { return []; }
265
+ return fs.readdirSync(directory)
266
+ .map((name) => name.endsWith('.json') ? name.slice(0, -5) : '')
267
+ .filter((id) => ID.test(id));
260
268
  }
261
269
 
262
270
  function writeIndex(dataRoot, manifests) {
@@ -275,23 +283,35 @@ function writeIndex(dataRoot, manifests) {
275
283
  atomicWritePrivate(file, JSON.stringify({
276
284
  schemaVersion: 0, revision, count: artifacts.length, inventoryDigest, artifacts
277
285
  }, null, 2) + '\n');
286
+ try { fs.unlinkSync(dirs(dataRoot).indexDirty); }
287
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
278
288
  return { revision, artifacts };
279
289
  }
280
290
 
291
+ function markIndexDirty(dataRoot) {
292
+ atomicWritePrivate(dirs(dataRoot).indexDirty, 'Artifact index needs refresh\n');
293
+ }
294
+
281
295
  function refreshIndex(dataRoot) {
282
- const manifests = [];
283
- for (const id of committedIds(dataRoot)) {
284
- try { manifests.push(readArtifact(dataRoot, id).manifest); } catch (_) { /* rebuild 会报告坏记录 */ }
285
- }
296
+ // The new object was just hashed. Historical bytes are checked on use and rebuild,
297
+ // not on every unrelated commit. Do not silently publish a subset of bad records.
298
+ let previous;
299
+ const legacyIndex = id => {
300
+ previous ||= new Map(readArtifactIndex(dataRoot, { allowDirty: true }).artifacts.map(entry => [entry.id, entry]));
301
+ const entry = previous.get(id);
302
+ if (!entry) fail('IRIS_ARTIFACT_INDEX_INVALID', '早期 Artifact 缺少缓存摘要,请显式重建索引');
303
+ return entry;
304
+ };
305
+ const manifests = committedIds(dataRoot).map(id => readArtifact(dataRoot, id, { verifyContent: false, legacyIndex }).manifest);
286
306
  return writeIndex(dataRoot, manifests);
287
307
  }
288
308
 
289
309
  /** 只更新已提交条目的派生索引,不升级旧记录或接回孤儿。 */
290
310
  export function refreshCoreArtifactInventory(dataRoot) {
291
311
  checkStore(dataRoot, { manifests: true });
292
- const manifests = committedIds(dataRoot).map(id => readArtifact(dataRoot, id).manifest);
293
- const index = writeIndex(dataRoot, manifests);
294
- return { revision: index.revision, total: manifests.length };
312
+ markIndexDirty(dataRoot);
313
+ const index = refreshIndex(dataRoot);
314
+ return { revision: index.revision, total: index.artifacts.length };
295
315
  }
296
316
 
297
317
  function validateRelationTargets(dataRoot, relations) {
@@ -329,13 +349,15 @@ function finishObject(dataRoot, input) {
329
349
  || JSON.stringify(previous.relations) !== JSON.stringify(relations)) {
330
350
  fail('IRIS_ARTIFACT_INPUT_INVALID', '可恢复 Artifact 与已提交内容不一致');
331
351
  }
332
- try { refreshIndex(dataRoot); } catch (_) { /* 不撤销已有提交 */ }
352
+ try { markIndexDirty(dataRoot); refreshIndex(dataRoot); } catch (_) { /* 已提交事实保留;脏索引由读取/Doctor 报告 */ }
333
353
  return publicArtifact(previous);
334
354
  }
335
- let stat;
355
+ let stat, digest;
336
356
  try {
337
357
  if (resumable && exists(object)) {
338
- if (regularFile(object).size !== input.expectedSize || hashFile(object) !== input.expectedDigest) {
358
+ stat = regularFile(object);
359
+ digest = hashFile(object);
360
+ if (stat.size !== input.expectedSize || digest !== input.expectedDigest) {
339
361
  fail('IRIS_ARTIFACT_INPUT_INVALID', '未提交 Artifact 与恢复内容不一致');
340
362
  }
341
363
  } else input.writeObject(object);
@@ -357,7 +379,7 @@ function finishObject(dataRoot, input) {
357
379
  size: stat.size,
358
380
  createdAt: input.createdAt || new Date().toISOString(),
359
381
  object: objectName,
360
- digest: { algorithm: 'sha256', value: hashFile(object) },
382
+ digest: { algorithm: 'sha256', value: digest || hashFile(object) },
361
383
  relations,
362
384
  metadata
363
385
  };
@@ -372,6 +394,7 @@ function finishObject(dataRoot, input) {
372
394
  }
373
395
  try {
374
396
  writeManifest(dataRoot, manifest);
397
+ markIndexDirty(dataRoot);
375
398
  commitRecord(dataRoot, id);
376
399
  } catch (_) {
377
400
  if (!resumable) {
@@ -380,7 +403,7 @@ function finishObject(dataRoot, input) {
380
403
  }
381
404
  fail('IRIS_ARTIFACT_WRITE_FAILED', 'Artifact 无法安全提交');
382
405
  }
383
- try { refreshIndex(dataRoot); } catch (_) { /* index 是可重建缓存,不撤销已提交 Artifact */ }
406
+ try { refreshIndex(dataRoot); } catch (_) { /* 脏索引保留;不撤销已提交 Artifact 或触发重新生成 */ }
384
407
  return publicArtifact(manifest);
385
408
  }
386
409
 
@@ -441,10 +464,16 @@ export function readCoreArtifactBytes(dataRoot, id) {
441
464
  const hit = readArtifact(dataRoot, id);
442
465
  return Object.freeze({
443
466
  artifact: publicArtifact(hit.manifest),
444
- bytes: Buffer.from(fs.readFileSync(hit.object))
467
+ bytes: fs.readFileSync(hit.object)
445
468
  });
446
469
  }
447
470
 
471
+ /** Caller owns the stream until close; content is verified in bounded chunks before delivery. */
472
+ export function readCoreArtifactStream(dataRoot, id, { signal } = {}) {
473
+ const hit = readArtifact(dataRoot, id);
474
+ return { artifact: publicArtifact(hit.manifest), stream: fs.createReadStream(hit.object, { signal }) };
475
+ }
476
+
448
477
  export function exportCoreArtifact(dataRoot, id, outputPath) {
449
478
  const destination = String(outputPath || '').trim();
450
479
  if (!destination || !path.isAbsolute(destination)) {
@@ -462,13 +491,19 @@ export function exportCoreArtifact(dataRoot, id, outputPath) {
462
491
  return Object.freeze({ artifact: publicArtifact(hit.manifest), exported: true, bytes: hit.manifest.size });
463
492
  }
464
493
 
465
- function readArtifactIndex(dataRoot) {
494
+ function readArtifactIndex(dataRoot, { allowDirty = false } = {}) {
466
495
  try { fs.lstatSync(dirs(dataRoot).storeRoot); }
467
496
  catch (error) {
468
497
  if (error?.code !== 'ENOENT') fail('IRIS_ARTIFACT_STORE_INVALID', 'Artifact Store 无法读取');
469
498
  return { schemaVersion: 0, revision: 0, artifacts: [], absent: true };
470
499
  }
471
500
  checkStore(dataRoot, { manifests: true });
501
+ if (!allowDirty) {
502
+ try {
503
+ fs.lstatSync(dirs(dataRoot).indexDirty);
504
+ fail('IRIS_ARTIFACT_INDEX_INVALID', 'Artifact Index 尚未刷新;写入结束后重试,持续失败请显式重建');
505
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
506
+ }
472
507
  const index = jsonFile(dirs(dataRoot).index, 'IRIS_ARTIFACT_INDEX_INVALID');
473
508
  const inventoryDigest = Array.isArray(index?.artifacts)
474
509
  ? crypto.createHash('sha256').update(JSON.stringify(index.artifacts)).digest('hex') : '';
@@ -483,16 +518,22 @@ function readArtifactIndex(dataRoot) {
483
518
  /** 工作台目录只读已提交元数据和文件大小;媒体打开/识别仍须核验完整内容哈希。 */
484
519
  export function listCoreArtifactMetadata(dataRoot) {
485
520
  const index = readArtifactIndex(dataRoot), artifacts = [];
486
- let dropped = 0;
521
+ let dropped = 0, legacySkipped = 0;
487
522
  for (const entry of index.artifacts) {
488
523
  try {
524
+ const record = jsonFile(recordPath(dataRoot, entry.id), 'IRIS_ARTIFACT_NOT_FOUND');
525
+ if (record?.schemaVersion === 0) {
526
+ legacyObject(dataRoot, record, entry.id);
527
+ if (entry.createdAt !== record.createdAt || entry.mediaType !== record.mediaType || entry.size !== record.size) throw new Error('index mismatch');
528
+ legacySkipped++; continue;
529
+ }
489
530
  const { manifest } = readArtifact(dataRoot, entry.id, { allowLegacy: false, verifyContent: false });
490
531
  if (entry.createdAt !== manifest.createdAt || entry.mediaType !== manifest.mediaType
491
532
  || entry.size !== manifest.size || entry.digest !== manifest.digest.value) throw new Error('index mismatch');
492
533
  artifacts.push(publicArtifact(manifest));
493
534
  } catch (_) { dropped++; }
494
535
  }
495
- return { revision: index.revision, artifacts, dropped };
536
+ return { revision: index.revision, artifacts, dropped, legacySkipped };
496
537
  }
497
538
 
498
539
  export function listCoreArtifacts(dataRoot, { offset = 0, limit = 50, kind, media_type, task_id } = {}) {
@@ -623,6 +664,7 @@ function inferredOrphanManifest(dataRoot, id, objectName) {
623
664
  */
624
665
  export function rebuildCoreArtifactIndex(dataRoot) {
625
666
  const store = ensureStore(dataRoot);
667
+ markIndexDirty(dataRoot);
626
668
  const report = { recovered: 0, legacyUpgraded: 0, invalid: 0, unresolved: 0, danglingRelations: 0 };
627
669
  const records = new Set(committedIds(dataRoot));
628
670
  const manifests = new Set(names(store.manifests)
@@ -28,7 +28,7 @@ export function validateBridgeArguments(tool, value) {
28
28
  try { normalizeComfyParameters(item); } catch (_) { invalid(); }
29
29
  continue;
30
30
  }
31
- if (field.type === 'integer' ? !Number.isSafeInteger(item) || item < field.minimum || field.maximum !== undefined && item > field.maximum
31
+ if (['integer', 'number'].includes(field.type) ? !(field.type === 'integer' ? Number.isSafeInteger(item) : typeof item === 'number' && Number.isFinite(item)) || item < field.minimum || field.maximum !== undefined && item > field.maximum
32
32
  : typeof item !== 'string' || field.minLength !== undefined && item.length < field.minLength || field.maxLength !== undefined && item.length > field.maxLength
33
33
  || field.pattern && !new RegExp(field.pattern).test(item) || field.enum && !field.enum.includes(item)) invalid();
34
34
  }
@@ -49,6 +49,7 @@ export async function callCoreBridgeTool(client, name, args, { signal } = {}) {
49
49
  if (name === 'iris_core_plugin_call') return { value: await client.callPlugin(args, options) };
50
50
  if (name === 'iris_core_models') return { value: await client.models(options) };
51
51
  if (name === 'iris_core_audio_models') return { value: await client.audioModels(options) };
52
+ if (name === 'iris_core_video_models') return { value: await client.videoModels(options) };
52
53
  if (name === 'iris_core_runs') return { value: await client.runs(args, options) };
53
54
  if (name === 'iris_core_run') return { value: await client.run(args.runId, options) };
54
55
  if (name === 'iris_core_import_image') {
@@ -66,13 +67,14 @@ export async function callCoreBridgeTool(client, name, args, { signal } = {}) {
66
67
  return { value: await client[name === 'iris_core_create_speech' ? 'createSpeech' : 'createTranscription']({ key, value }, options) };
67
68
  }
68
69
  if (name === 'iris_core_audio') return { audio: { artifactId: args.artifactId, ...await client.audio(args.artifactId, options) } };
70
+ if (name === 'iris_core_video') return { video: { artifactId: args.artifactId, ...await client.video(args.artifactId, options) } };
69
71
  if (name === 'iris_core_transcript') {
70
72
  const result = await client.transcript(args.artifactId, options);
71
73
  return { value: { contractVersion: 0, artifactId: args.artifactId, text: new TextDecoder('utf-8', { fatal: true }).decode(result.bytes) } };
72
74
  }
73
- if (['iris_core_create_run', 'iris_core_create_generation'].includes(name)) {
75
+ if (['iris_core_create_run', 'iris_core_create_generation', 'iris_core_create_video'].includes(name)) {
74
76
  const { key, ...value } = args;
75
- return { value: await client[name === 'iris_core_create_run' ? 'createRun' : 'createGeneration']({ key, value }, options) };
77
+ return { value: await client[name === 'iris_core_create_run' ? 'createRun' : name === 'iris_core_create_video' ? 'createVideo' : 'createGeneration']({ key, value }, options) };
76
78
  }
77
79
  if (name === 'iris_core_continue') return { value: await client.continueRun(args, options) };
78
80
  if (name === 'iris_core_operation') return { value: await client.operation(args.operationId, options) };