@mokuyoaxis/dsh-iris 0.2.7 → 0.3.0-alpha.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 (160) hide show
  1. package/CHANGELOG.md +81 -1
  2. package/README.en.md +103 -312
  3. package/README.md +100 -310
  4. package/bin/dsh-iris.js +333 -65
  5. package/docs/AGENT_CORE_QUICKSTART.md +64 -0
  6. package/docs/AGENT_QUICKSTART.md +2 -0
  7. package/docs/ARCHITECTURE.md +13 -3
  8. package/docs/AUDIO_FLOWS.md +71 -0
  9. package/docs/CONTACT_SHEET.md +62 -0
  10. package/docs/CORE_CONNECTIONS.md +99 -0
  11. package/docs/CORE_QUICKSTART.md +108 -0
  12. package/docs/CORE_RUNTIME_CONTRACT.md +7 -2
  13. package/docs/CORE_SERVICE.md +97 -0
  14. package/docs/CORE_SKILLS.md +35 -3
  15. package/docs/DSH_COMPATIBILITY.md +10 -0
  16. package/docs/DSH_RC2_ADAPTATION.md +1 -1
  17. package/docs/DSH_RUNTIME.md +6 -2
  18. package/docs/HEADLESS_CLI.md +2 -2
  19. package/docs/IMAGE_FLOW.md +4 -0
  20. package/docs/IMAGE_GENERATION.md +127 -0
  21. package/docs/LOCAL_API.md +4 -2
  22. package/docs/LOCAL_API_CONTRACT.md +39 -14
  23. package/docs/LOCAL_CREATION.md +7 -1
  24. package/docs/LOCAL_OPERATIONS.md +46 -0
  25. package/docs/LOCAL_SELECTION.md +57 -0
  26. package/docs/LOCAL_WEB.md +201 -0
  27. package/docs/MCP.md +191 -0
  28. package/docs/MUSIC.md +2 -0
  29. package/docs/PANEL_EXTENSIONS.md +64 -0
  30. package/docs/PLUGINS.md +189 -0
  31. package/docs/PLUGIN_STARTUP.md +100 -0
  32. package/docs/PROVIDER_ADAPTER_CONTRACT.md +1 -1
  33. package/docs/PROVIDER_EXTENSIONS.md +78 -0
  34. package/docs/PROVIDER_HEALTH.md +3 -1
  35. package/docs/PROVIDER_SETTINGS.md +85 -0
  36. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +6 -0
  37. package/docs/ROADMAP.md +17 -7
  38. package/docs/VISION_CLI.md +1 -1
  39. package/docs/WEB_PROTOTYPE.md +21 -6
  40. package/docs/WEB_RECOVERY.md +46 -0
  41. package/docs/WORKBENCH_LOGIN.md +63 -0
  42. package/docs/assets/screenshots/core-connections-preview.webp +0 -0
  43. package/docs/assets/screenshots/core-model-pools-preview.webp +0 -0
  44. package/docs/assets/screenshots/core-overview-preview.webp +0 -0
  45. package/docs/assets/screenshots/core-spectrum-preview.webp +0 -0
  46. package/docs/assets/screenshots/core-studio-dark-preview.webp +0 -0
  47. package/docs/assets/screenshots/core-studio-mobile-preview.webp +0 -0
  48. package/docs/assets/screenshots/core-studio-preview.webp +0 -0
  49. package/docs/releases/0.3.0-alpha.1.md +51 -0
  50. package/docs/screenshots.md +41 -1
  51. package/lib/actions.js +53 -2
  52. package/lib/adapters.js +20 -11
  53. package/lib/application-runtime.js +34 -7
  54. package/lib/audio-flow-input.js +37 -0
  55. package/lib/client.js +346 -53
  56. package/lib/comfyui-adapter.js +94 -0
  57. package/lib/comfyui-client.js +51 -0
  58. package/lib/comfyui-inspection.js +73 -0
  59. package/lib/comfyui-workflow.js +94 -0
  60. package/lib/config.js +36 -1
  61. package/lib/configured-image-runtime.js +50 -0
  62. package/lib/connection-skill.js +89 -0
  63. package/lib/connection-template.js +23 -0
  64. package/lib/core-artifact-store.js +16 -2
  65. package/lib/core-bridge-selection.js +31 -0
  66. package/lib/core-bridge-tools.js +83 -0
  67. package/lib/core-capability-catalog.js +182 -0
  68. package/lib/core-connection-diagnostics.js +49 -0
  69. package/lib/core-connection-store.js +277 -0
  70. package/lib/core-connections.js +225 -0
  71. package/lib/core-maintenance.js +9 -3
  72. package/lib/core-runtime.js +2 -1
  73. package/lib/core-service-client.js +170 -0
  74. package/lib/core-tasks.js +29 -12
  75. package/lib/core-writer-lease.js +16 -0
  76. package/lib/creation-request-store.js +38 -64
  77. package/lib/dsh-host-adapter.js +4 -1
  78. package/lib/dsh-service-bridge.js +66 -0
  79. package/lib/execution-ownership.js +43 -0
  80. package/lib/flow-store.js +105 -10
  81. package/lib/generation-input.js +10 -1
  82. package/lib/headless-vision.js +7 -7
  83. package/lib/image-edit-input.js +1 -1
  84. package/lib/image-flow.js +73 -29
  85. package/lib/index.js +37 -17
  86. package/lib/local-api-projection.js +10 -5
  87. package/lib/local-api-token-directory.js +31 -0
  88. package/lib/local-api.js +769 -67
  89. package/lib/local-creation.js +89 -24
  90. package/lib/local-operations.js +36 -0
  91. package/lib/local-request-root.js +86 -0
  92. package/lib/local-web-assets.js +37 -0
  93. package/lib/mcp-bridge.js +58 -0
  94. package/lib/mcp-check.js +26 -0
  95. package/lib/mcp-config.js +42 -0
  96. package/lib/mcp-delivery.js +46 -0
  97. package/lib/mcp-http.js +101 -0
  98. package/lib/mcp-server.js +47 -0
  99. package/lib/model-directory-service.js +16 -6
  100. package/lib/model-directory.js +3 -3
  101. package/lib/models.js +2 -2
  102. package/lib/music-transport.js +2 -1
  103. package/lib/operation-runner.js +236 -0
  104. package/lib/operation-store.js +216 -0
  105. package/lib/panel-extension.js +36 -0
  106. package/lib/plugin-contract.js +194 -0
  107. package/lib/plugin-execution.js +51 -0
  108. package/lib/plugin-launch.js +86 -0
  109. package/lib/plugin-operation-store.js +101 -0
  110. package/lib/plugin-operations.js +28 -0
  111. package/lib/plugin-registry.js +109 -0
  112. package/lib/plugin-selection.js +15 -0
  113. package/lib/plugin-source-image.js +5 -0
  114. package/lib/plugin-web-assets.js +54 -0
  115. package/lib/plugins.d.ts +288 -0
  116. package/lib/plugins.js +58 -0
  117. package/lib/provider-adapter.js +5 -1
  118. package/lib/provider-adapters.js +56 -18
  119. package/lib/provider-catalog.js +40 -29
  120. package/lib/provider-config-service.js +105 -23
  121. package/lib/provider-diagnostics.js +56 -0
  122. package/lib/provider-extensions.js +29 -0
  123. package/lib/provider-health.js +8 -1
  124. package/lib/provider-monitor.js +79 -0
  125. package/lib/provider-protocol.js +70 -20
  126. package/lib/provider-registry.js +118 -0
  127. package/lib/provider-response.js +28 -0
  128. package/lib/provider-settings.js +134 -0
  129. package/lib/provider-task-runner.js +22 -9
  130. package/lib/skill-installation.js +23 -11
  131. package/lib/vision-model-routing.js +3 -0
  132. package/lib/web/app.js +642 -0
  133. package/lib/web/appearance.js +111 -0
  134. package/lib/web/comfyui.js +146 -0
  135. package/lib/web/connections.js +377 -0
  136. package/lib/web/creation.js +355 -0
  137. package/lib/web/flow-view.js +33 -0
  138. package/lib/web/gallery.js +132 -0
  139. package/lib/web/index.html +291 -0
  140. package/lib/web/media-client.js +204 -0
  141. package/lib/web/plugin-panel.js +221 -0
  142. package/lib/web/plugin-surface.css +17 -0
  143. package/lib/web/providers.js +492 -0
  144. package/lib/web/recovery.js +105 -0
  145. package/lib/web/startup.js +18 -0
  146. package/lib/web/styles.css +518 -0
  147. package/lib/web/transport.js +96 -0
  148. package/lib/web/workbench-login.js +165 -0
  149. package/lib/workbench-auth-cli.js +47 -0
  150. package/lib/workbench-auth.js +200 -0
  151. package/package.json +20 -5
  152. package/plugins/contact-sheet/index.d.mts +4 -0
  153. package/plugins/contact-sheet/index.mjs +33 -0
  154. package/plugins/contact-sheet/web/board.css +14 -0
  155. package/plugins/contact-sheet/web/board.mjs +93 -0
  156. package/skills/iris-core/SKILL.md +8 -4
  157. package/skills/iris-core/references/cli.md +2 -2
  158. package/skills/iris-core/references/comfyui.md +69 -0
  159. package/skills/iris-core/references/tasks.md +2 -0
  160. package/user_guide.md +5 -1
@@ -0,0 +1,15 @@
1
+ import { checkedPluginDependency, pluginFail } from './plugin-contract.js';
2
+
3
+ /** Saved authority is an exact dependency list, never a live registry expansion. */
4
+ export function checkedPluginSelection(value = []) {
5
+ if (!Array.isArray(value) || value.length > 16) pluginFail('IRIS_CONNECTION_INPUT_INVALID', '插件业务须为最多 16 项明确依赖');
6
+ let result;
7
+ try { result = value.map(checkedPluginDependency); }
8
+ catch (error) { if (error.code !== 'IRIS_PLUGIN_INVALID') throw error; pluginFail('IRIS_CONNECTION_INPUT_INVALID', '插件业务依赖格式无效'); }
9
+ if (result.some(item => item.kind !== 'operations') || new Set(result.map(item => item.contributionId)).size !== result.length) pluginFail('IRIS_CONNECTION_INPUT_INVALID', '插件业务依赖重复或类型无效');
10
+ return result;
11
+ }
12
+ export function hasPluginSelection(values, dependency) {
13
+ const pin = checkedPluginDependency(dependency);
14
+ return values.some(value => JSON.stringify(checkedPluginDependency(value)) === JSON.stringify(pin));
15
+ }
@@ -0,0 +1,5 @@
1
+ // Shared source-image limits for the catalog and the plugin command boundary.
2
+ const MEDIA_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp']);
3
+ export function isPluginSourceImage(artifact) {
4
+ return MEDIA_TYPES.has(artifact.mediaType) && artifact.size <= 8 * 1024 * 1024;
5
+ }
@@ -0,0 +1,54 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { freezePluginValue, pluginFail, pluginFields } from './plugin-contract.js';
4
+
5
+ const WEB_KINDS = ['pages', 'settings', 'renderers'];
6
+ const resources = new WeakMap();
7
+ const invalid = message => pluginFail('IRIS_PLUGIN_WEB_INVALID', message);
8
+
9
+ /** Only explicitly declared entry/styles. Never serve a plugin directory or evaluate browser JS. */
10
+ export function pinPluginWebAssets(definition, file) {
11
+ const root = path.dirname(file), entries = new Map();
12
+ let total = 0;
13
+ for (const kind of WEB_KINDS) for (const item of definition.manifest.contributions[kind]) {
14
+ const assets = new Map();
15
+ for (const relative of [item.entry, ...(item.styles || [])]) {
16
+ const target = fs.realpathSync(path.join(root, relative)), rel = path.relative(root, target), stat = fs.statSync(target);
17
+ if (!rel || rel.startsWith('..' + path.sep) || path.isAbsolute(rel) || !stat.isFile()) {
18
+ pluginFail('IRIS_PLUGIN_INVALID', 'Web 入口必须位于插件目录内');
19
+ }
20
+ if (stat.size > 8 * 1024 * 1024 || (total += stat.size) > 16 * 1024 * 1024) invalid('插件 Web 资源超过 8 MiB 单文件 / 16 MiB 总量限制');
21
+ assets.set(relative, { bytes: fs.readFileSync(target), mediaType: relative.endsWith('.css') ? 'text/css; charset=utf-8' : 'text/javascript; charset=utf-8' });
22
+ }
23
+ entries.set(kind + ':' + definition.manifest.id + '.' + item.id, assets);
24
+ }
25
+ return entries;
26
+ }
27
+
28
+ export function registerPluginWebAssets(registry, entries) { resources.set(registry, entries); }
29
+
30
+ /** A Core service opts in to exact contribution IDs. Installing a plugin never opts in. */
31
+ export function createPluginPanelAssembly(registry, selection = {}) {
32
+ if (!pluginFields(selection, WEB_KINDS, [])) invalid('Panel 组合选择须为 pages/settings/renderers 的明确 ID 列表');
33
+ const source = registry?.snapshot(), pinned = resources.get(registry), routes = new Map(), manifest = { contractVersion: 0 }, selected = {};
34
+ if (source && source.state !== 'active') pluginFail('IRIS_PLUGIN_CLOSED', '插件注册实例正在关闭或已关闭');
35
+ for (const kind of WEB_KINDS) {
36
+ const ids = selection[kind] ?? [];
37
+ if (!Array.isArray(ids) || ids.some(id => typeof id !== 'string') || new Set(ids).size !== ids.length) invalid('Panel 选择须为不重复的完整贡献 ID');
38
+ selected[kind] = [...ids];
39
+ manifest[kind] = ids.map(id => {
40
+ const item = source?.contributions[kind].find(value => value.id === id);
41
+ if (!item) pluginFail('IRIS_PLUGIN_CONTRIBUTION_MISSING', 'Panel 贡献未加载:' + kind + ':' + id);
42
+ const assets = pinned?.get(kind + ':' + id);
43
+ if (!assets) pluginFail('IRIS_PLUGIN_WEB_ASSET_MISSING', 'Panel 贡献须经显式文件入口加载并固定资源:' + id);
44
+ const base = '/plugin-assets/' + item.dependency.pluginId + '/' + item.dependency.pluginVersion + '/' + kind + '/' + id + '/';
45
+ for (const [name, asset] of assets) routes.set(base + name, asset);
46
+ return { id, title: item.title, dependency: item.dependency, entry: base + item.entry,
47
+ styles: (item.styles || []).map(name => base + name), ...(kind === 'renderers' ? { mediaTypes: item.mediaTypes } : {}) };
48
+ });
49
+ }
50
+ const active = () => !registry || registry.snapshot().state === 'active';
51
+ freezePluginValue(manifest); freezePluginValue(selected);
52
+ return Object.freeze({ selection: selected, manifest,
53
+ has: route => active() && routes.has(route), read: async route => active() ? routes.get(route) || null : null });
54
+ }
@@ -0,0 +1,288 @@
1
+ /** Experimental v0. Trusted local code; no sandbox, discovery, installation or implicit grant. */
2
+ export type JsonValue = null | boolean | number | string | readonly JsonValue[] | { readonly [key: string]: JsonValue };
3
+ export type PluginKind = 'providers' | 'operations' | 'pages' | 'renderers' | 'settings';
4
+ export const PLUGIN_CONTRACT_VERSION: 0;
5
+ export const PLUGIN_KINDS: readonly PluginKind[];
6
+ export type PluginCoreCommand = 'artifact.inspect' | 'artifact.read' | 'artifact.list' | 'task.inspect' | 'task.list' | 'artifact.import';
7
+ export const PLUGIN_CORE_COMMANDS: Readonly<Record<PluginCoreCommand, 'reader' | 'writer'>>;
8
+ export class PluginContractError extends Error { constructor(code: string, message: string); readonly code: string; }
9
+ export interface PluginDependency {
10
+ readonly contractVersion: 0;
11
+ readonly pluginId: string;
12
+ readonly pluginVersion: string;
13
+ readonly kind: PluginKind;
14
+ /** Fully qualified pluginId.localId. */
15
+ readonly contributionId: string;
16
+ readonly contributionVersion: string;
17
+ }
18
+ export interface Contribution { readonly id: string; readonly version: string; readonly title: string; }
19
+ export interface ProviderContribution extends Contribution {
20
+ readonly capabilities: readonly ('image-gen' | 'video-gen' | 'tts' | 'music-gen' | 'transcribe')[];
21
+ readonly configuration?: readonly { key: 'baseUrl' | 'mediaBaseUrl' | 'apiKey'; label: string; type: 'url' | 'secret'; required?: boolean; advanced?: boolean }[];
22
+ }
23
+ export interface OperationContribution extends Contribution {
24
+ readonly mode: 'reader' | 'writer';
25
+ readonly commands: readonly PluginCoreCommand[];
26
+ /** Top-level string or string-array input fields containing source Artifact IDs. Bound at acceptance. */
27
+ readonly artifactInputs?: readonly string[];
28
+ /** Closed root object; supported JSON Schema subset documented in PLUGINS.md. */
29
+ readonly inputSchema: { readonly type: 'object'; readonly additionalProperties: false; readonly [key: string]: JsonValue };
30
+ }
31
+ export interface PageContribution extends Contribution {
32
+ /** Plugin-relative bundled JS/MJS module. Explicit entry loader pins it before Panel selection. */
33
+ readonly entry: string;
34
+ readonly styles?: readonly string[];
35
+ }
36
+ export interface RendererContribution extends PageContribution { readonly mediaTypes: readonly string[]; }
37
+ export interface SettingsContribution extends PageContribution {}
38
+ export interface PluginPanelSelection {
39
+ readonly pages?: readonly string[];
40
+ readonly settings?: readonly string[];
41
+ readonly renderers?: readonly string[];
42
+ }
43
+ export interface PluginManifest {
44
+ readonly contractVersion: 0;
45
+ readonly id: string;
46
+ readonly version: string;
47
+ /** SemVer precedence interval, min inclusive/maxExclusive exclusive. Not npm range syntax. */
48
+ readonly compatibility: { readonly min: string; readonly maxExclusive: string };
49
+ readonly contributions: {
50
+ readonly providers?: readonly ProviderContribution[];
51
+ readonly operations?: readonly OperationContribution[];
52
+ readonly pages?: readonly PageContribution[];
53
+ readonly renderers?: readonly RendererContribution[];
54
+ readonly settings?: readonly SettingsContribution[];
55
+ };
56
+ }
57
+ export interface PluginOperationContext {
58
+ readonly signal: AbortSignal;
59
+ readonly dependency: PluginDependency;
60
+ readonly core: { execute(command: PluginCoreCommand, input: Record<string, unknown>): Promise<unknown> };
61
+ }
62
+ export type PluginOperation = (input: Readonly<Record<string, JsonValue>>, context: PluginOperationContext) => JsonValue | Promise<JsonValue>;
63
+ export type ProviderCapability = 'image' | 'video' | 'tts' | 'music' | 'transcribe';
64
+ export type ProviderMethod = 'discover' | 'submit' | 'poll' | 'cancel' | 'download' | 'mapError';
65
+ export interface ProviderAdapterInput {
66
+ id: string; protocol: string; capabilities: ProviderCapability[]; contractVersion?: 0;
67
+ operations: Partial<Record<ProviderMethod, (...args: any[]) => unknown>>;
68
+ unsupported?: Partial<Record<ProviderMethod, string>>;
69
+ prepareInput?: (...args: any[]) => unknown;
70
+ unsupportedPreparation?: string;
71
+ pluginDependency?: PluginDependency;
72
+ }
73
+ export type ProviderAdapter = Readonly<Omit<ProviderAdapterInput, 'contractVersion' | 'capabilities' | 'operations' | 'unsupported'> & {
74
+ contractVersion: 0;
75
+ capabilities: readonly ProviderCapability[];
76
+ operations: Readonly<Partial<Record<ProviderMethod, (...args: any[]) => unknown>>>;
77
+ unsupported: Readonly<Partial<Record<ProviderMethod, { readonly reason: string }>>>;
78
+ }>;
79
+ export function defineProviderAdapter(input: ProviderAdapterInput): ProviderAdapter;
80
+ export function normalizeMediaEndpoint(value: string): string;
81
+ export interface ProviderImplementation {
82
+ normalizeEndpoint(value: string): string;
83
+ createAdapter(provider: Record<string, any>, options?: Record<string, any>): ProviderAdapter;
84
+ supportsModel?(input: { provider: Record<string, unknown>; modelId: string; capability: string; checkEndpoint?: boolean }): boolean;
85
+ supportsImageEdit?(modelId: string): boolean;
86
+ }
87
+ export interface PluginActivationContext {
88
+ readonly coreVersion: string;
89
+ readonly plugin: { readonly id: string; readonly version: string };
90
+ /** Register during activate, including before awaited initialization; reverse order cleanup. */
91
+ onDispose(cleanup: () => void | Promise<void>): void;
92
+ }
93
+ export interface PluginImplementation {
94
+ readonly providers?: Readonly<Record<string, ProviderImplementation>>;
95
+ readonly operations?: Readonly<Record<string, PluginOperation>>;
96
+ }
97
+ export interface CorePlugin {
98
+ readonly manifest: PluginManifest;
99
+ activate(context: PluginActivationContext): PluginImplementation | Promise<PluginImplementation>;
100
+ }
101
+ export function definePlugin(definition: CorePlugin): CorePlugin;
102
+ export interface ProviderRegistry {
103
+ descriptor(id: string): Readonly<Record<string, any>> | undefined;
104
+ snapshot(): { state: string; protocols: Record<string, any>[]; extensions: { id: string; version: string }[]; activeCalls: number; cleanupFailed: boolean; contractVersion: 0 };
105
+ createAdapter(id: string, provider: Record<string, any>, options?: Record<string, any>): ProviderAdapter;
106
+ dispose(): Promise<void>;
107
+ }
108
+ export interface CorePluginRegistry {
109
+ readonly providers: ProviderRegistry;
110
+ snapshot(): {
111
+ contractVersion: 0; coreVersion: string; state: string; activeCalls: number; cleanupFailed: boolean;
112
+ plugins: PluginManifest[];
113
+ contributions: {
114
+ providers: (ProviderContribution & { dependency: PluginDependency })[];
115
+ operations: (OperationContribution & { dependency: PluginDependency })[];
116
+ pages: (PageContribution & { dependency: PluginDependency })[];
117
+ renderers: (RendererContribution & { dependency: PluginDependency })[];
118
+ settings: (SettingsContribution & { dependency: PluginDependency })[];
119
+ };
120
+ };
121
+ dependency(kind: PluginKind, qualifiedId: string): PluginDependency;
122
+ assertDependency(dependency: PluginDependency): PluginDependency;
123
+ dispose(): Promise<void>;
124
+ }
125
+ export function createCorePluginRegistry(options?: { plugins?: CorePlugin[]; coreVersion?: string }): Promise<CorePluginRegistry>;
126
+ export function loadCorePluginRegistry(options?: { entries?: string[]; coreVersion?: string }): Promise<CorePluginRegistry>;
127
+ export type CoreCapabilityScope = 'read' | 'create' | 'execute' | 'stop';
128
+ export type CoreBridgeCapability = 'image-edit' | 'image-generate' | 'tts' | 'transcribe';
129
+ export interface ContributionAccess {
130
+ readonly local: 'requires-configuration' | 'requires-grant' | 'descriptor-only' | 'unavailable';
131
+ readonly web: 'via-media-flow' | 'via-operation' | 'not-exposed' | 'durable-operation-required' | 'not-mounted' | 'available-in-panel' | 'unavailable';
132
+ readonly mcp: 'via-media-flow' | 'requires-selection' | 'not-exposed' | 'durable-operation-required' | 'not-applicable' | 'unavailable';
133
+ }
134
+ export interface CatalogContribution extends Contribution {
135
+ readonly dependency: PluginDependency;
136
+ /** Declaration/assembly facts only; never the caller's authorization or service health. */
137
+ readonly access: ContributionAccess;
138
+ }
139
+ export interface CoreCapabilitySnapshot {
140
+ readonly contractVersion: 0;
141
+ readonly state: string;
142
+ readonly coreVersion?: string;
143
+ readonly scopeOptions: readonly { readonly id: CoreCapabilityScope; readonly title: string }[];
144
+ readonly controlTools: readonly string[];
145
+ readonly groups: readonly {
146
+ readonly id: CoreBridgeCapability; readonly recipe: string; readonly title: string; readonly label: string; readonly shortTitle: string;
147
+ readonly media: string; readonly requiresAudio?: boolean; readonly tools: readonly string[];
148
+ }[];
149
+ readonly tools: readonly { readonly name: string; readonly scope: CoreCapabilityScope; readonly description: string }[];
150
+ readonly pluginTools: CoreCapabilitySnapshot['tools'];
151
+ readonly pluginControlTools: readonly string[];
152
+ readonly media: readonly { readonly id: string; readonly title: string; readonly note?: string; readonly capabilities: readonly CoreBridgeCapability[] }[];
153
+ readonly plugins: readonly { readonly id: string; readonly version: string; readonly compatibility: PluginManifest['compatibility'] }[];
154
+ readonly contributions: {
155
+ readonly providers: readonly (ProviderContribution & CatalogContribution)[];
156
+ readonly operations: readonly (OperationContribution & CatalogContribution)[];
157
+ /** No browser module path is exposed or mounted by discovery. */
158
+ readonly pages: readonly CatalogContribution[];
159
+ readonly renderers: readonly (CatalogContribution & { readonly mediaTypes: readonly string[] })[];
160
+ readonly settings: readonly CatalogContribution[];
161
+ };
162
+ }
163
+ export interface CoreCapabilityCatalog {
164
+ snapshot(options?: { audioEnabled?: boolean; panelContributions?: PluginPanelSelection; allowedOperations?: readonly string[] }): CoreCapabilitySnapshot;
165
+ /** Validate an explicit local selection. Does not invoke, persist a grant or enable MCP/Web. */
166
+ selectOperations(ids?: readonly string[]): readonly (OperationContribution & CatalogContribution)[];
167
+ }
168
+ export function createCoreCapabilityCatalog(options?: { registry?: CorePluginRegistry }): CoreCapabilityCatalog;
169
+ export interface CorePluginHost {
170
+ call(id: string, input: Record<string, JsonValue>, options?: { dependency?: PluginDependency; signal?: AbortSignal }): Promise<{ dependency: PluginDependency; value: JsonValue }>;
171
+ snapshot(): { state: string; queuedWrites: number; activeWrites: number; activeReaders: number; cleanupFailed: boolean };
172
+ /** Caller disposes the shared registry after every host has closed. */
173
+ dispose(): Promise<ReturnType<CorePluginHost['snapshot']>>;
174
+ }
175
+ export function createCorePluginHost(options: { dataRoot: string; registry: CorePluginRegistry; allowedOperations?: string[] }): CorePluginHost;
176
+ export interface CorePluginWorkbenchSnapshot {
177
+ readonly contractVersion: 0;
178
+ readonly state: string;
179
+ readonly origin: string | null;
180
+ /** Media writes remain disabled; optional Provider editing has a separate owner gate. */
181
+ readonly readOnly: true;
182
+ readonly activeReads: number;
183
+ readonly cleanupFailed?: boolean;
184
+ }
185
+ export interface CorePluginWorkbench {
186
+ start(options?: { port?: number }): Promise<{ readonly origin: string; readonly accessToken: string; readonly loginUrl: string; readonly loginExpiresAt: string }>;
187
+ snapshot(): CorePluginWorkbenchSnapshot;
188
+ readonly closed: Promise<CorePluginWorkbenchSnapshot>;
189
+ dispose(): Promise<CorePluginWorkbenchSnapshot>;
190
+ }
191
+ export interface CorePasswordPluginWorkbench extends Omit<CorePluginWorkbench, 'start'> {
192
+ start(options?: { port?: number }): Promise<{ readonly origin: string; readonly accessToken: string; readonly loginUrl: string;
193
+ readonly authentication: { readonly mode: 'password'; readonly configured: boolean; readonly authId: string;
194
+ readonly rememberDays: 30; readonly minPasswordLength: 8; readonly maxPasswordLength: 128 } }>;
195
+ }
196
+ export interface CorePluginWorkbenchOptions {
197
+ dataRoot: string; registry?: CorePluginRegistry; panelContributions?: PluginPanelSelection;
198
+ panelManifestPath?: string; providerConfigPath?: string; allowProviderManagement?: boolean;
199
+ /** Omitted: no business runner. []: reconcile/read history only. IDs: explicitly enabled durable businesses. */
200
+ allowedOperations?: string[];
201
+ /** Explicit private directory outside dataRoot. Enables owner connection management. */
202
+ connectionStore?: string;
203
+ /** Enable the standard HTTP /mcp edge. Does not start a second Core. */
204
+ allowMcp?: boolean;
205
+ /** Optional persistent browser password/session store, in a private directory outside dataRoot. */
206
+ workbenchAuth?: { directory: string };
207
+ }
208
+ export function createCorePluginWorkbench(options: CorePluginWorkbenchOptions & { workbenchAuth: { directory: string } }): CorePasswordPluginWorkbench;
209
+ export function createCorePluginWorkbench(options: Omit<CorePluginWorkbenchOptions, 'workbenchAuth'> & { workbenchAuth?: undefined }): CorePluginWorkbench;
210
+ export function createCorePluginWorkbench(options: CorePluginWorkbenchOptions): CorePluginWorkbench | CorePasswordPluginWorkbench;
211
+ export function checkedPluginDependency(value: unknown): PluginDependency;
212
+ export function comparePluginVersions(a: string, b: string): -1 | 0 | 1;
213
+
214
+ export interface PluginTheme { readonly mode: 'light' | 'dark'; readonly density: string; readonly accent: 'iris'; }
215
+ export interface PluginWebSession {
216
+ readonly contractVersion: 0;
217
+ readonly instanceId: string;
218
+ readonly rootId?: string | null;
219
+ readonly operations: readonly string[];
220
+ readonly readOnly: boolean;
221
+ readonly audio: boolean;
222
+ readonly providerManagement: boolean;
223
+ readonly pluginExecution: boolean;
224
+ readonly pluginOperations: readonly PluginDependency[];
225
+ }
226
+ export interface PluginArtifact { readonly id: string; readonly mediaType: string; readonly label?: string; }
227
+ export interface PluginWebRequestOptions { readonly signal?: AbortSignal; }
228
+ export interface PluginBusinessRequest {
229
+ readonly key: string;
230
+ readonly operationId: string;
231
+ readonly dependency: PluginDependency;
232
+ readonly input: Readonly<Record<string, JsonValue>>;
233
+ }
234
+ export interface PluginDurableOperation {
235
+ readonly contractVersion: 0;
236
+ readonly id: string;
237
+ readonly rootId: string;
238
+ readonly action: 'plugin.execute';
239
+ readonly operationId: string;
240
+ readonly dependency: PluginDependency;
241
+ readonly input: Readonly<Record<string, JsonValue>>;
242
+ readonly inputDigest: string;
243
+ readonly sourceArtifactIds: readonly string[];
244
+ readonly revision: number;
245
+ readonly state: 'queued' | 'running' | 'succeeded' | 'failed' | 'canceled' | 'interrupted';
246
+ readonly cancelRequested: boolean;
247
+ readonly createdAt: string;
248
+ readonly updatedAt: string;
249
+ readonly startedAt?: string;
250
+ readonly finishedAt?: string;
251
+ readonly failureCode?: 'dependency_unavailable' | 'permission_denied' | 'execution_failed';
252
+ readonly result?: { readonly value: JsonValue; readonly artifactIds: readonly string[] };
253
+ }
254
+ export interface PluginWebApi {
255
+ runs(query?: Readonly<Record<string, string>>, options?: PluginWebRequestOptions): Promise<JsonValue>;
256
+ run(id: string, options?: PluginWebRequestOptions): Promise<JsonValue>;
257
+ models(options?: PluginWebRequestOptions): Promise<JsonValue>;
258
+ audioModels(options?: PluginWebRequestOptions): Promise<JsonValue>;
259
+ artifact(artifact: PluginArtifact, options?: PluginWebRequestOptions): Promise<Blob>;
260
+ /** Only settings mounts in an explicitly enabled owner Provider-management session. */
261
+ readonly providers?: {
262
+ read(options?: PluginWebRequestOptions): Promise<JsonValue>;
263
+ save(input: Record<string, JsonValue>, options?: PluginWebRequestOptions): Promise<JsonValue>;
264
+ };
265
+ /** Present only when the service explicitly owns a durable plugin runner. Core checks each request. */
266
+ readonly pluginOperations?: {
267
+ catalog(query?: Readonly<Record<string, string>>, options?: PluginWebRequestOptions): Promise<JsonValue>;
268
+ history(options?: PluginWebRequestOptions): Promise<JsonValue>;
269
+ execute(input: PluginBusinessRequest, options?: PluginWebRequestOptions): Promise<{ contractVersion: 0; operation: PluginDurableOperation; idempotentReplay: boolean }>;
270
+ inspect(id: string, options?: PluginWebRequestOptions): Promise<JsonValue>;
271
+ lookup(key: string, options?: PluginWebRequestOptions): Promise<JsonValue>;
272
+ stop(id: string, options?: PluginWebRequestOptions): Promise<JsonValue>;
273
+ };
274
+ }
275
+ /** Same-origin trusted modules; Shadow DOM is a style boundary, not a security sandbox. */
276
+ export interface PluginWebContext<Container = unknown> {
277
+ readonly container: Container;
278
+ readonly signal: AbortSignal;
279
+ readonly dependency: PluginDependency;
280
+ readonly session: PluginWebSession;
281
+ readonly api: PluginWebApi;
282
+ readonly theme: { current(): PluginTheme; subscribe(listener: (theme: PluginTheme) => void): () => void };
283
+ }
284
+ export interface PluginPageModule<Container = unknown> { mount(context: PluginWebContext<Container>): void | (() => void) | Promise<void | (() => void)>; }
285
+ export interface PluginSettingsModule<Container = unknown> extends PluginPageModule<Container> {}
286
+ export interface PluginRendererModule<Container = unknown> {
287
+ mount(context: PluginWebContext<Container> & { readonly artifact: PluginArtifact }): void | (() => void) | Promise<void | (() => void)>;
288
+ }
package/lib/plugins.js ADDED
@@ -0,0 +1,58 @@
1
+ // Experimental v0 public plugin author/embedding boundary. No automatic discovery or installation.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { createPluginRegistry } from './plugin-registry.js';
6
+ import { createApplicationRuntime } from './application-runtime.js';
7
+ import { definePlugin, pluginFail, pluginFields } from './plugin-contract.js';
8
+ import { createCoreCapabilityCatalog } from './core-capability-catalog.js';
9
+ import { pinPluginWebAssets, registerPluginWebAssets } from './plugin-web-assets.js';
10
+ import { createLocalReadApi } from './local-api.js';
11
+
12
+ export { createCoreCapabilityCatalog } from './core-capability-catalog.js';
13
+
14
+ export { definePlugin, PluginContractError, PLUGIN_CONTRACT_VERSION, PLUGIN_KINDS, PLUGIN_CORE_COMMANDS,
15
+ checkedPluginDependency, comparePluginVersions } from './plugin-contract.js';
16
+ export { defineProviderAdapter, normalizeMediaEndpoint } from './provider-extensions.js';
17
+
18
+ export function createCorePluginRegistry({ plugins = [], coreVersion } = {}) {
19
+ coreVersion ??= JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
20
+ return createPluginRegistry({ plugins, coreVersion });
21
+ }
22
+
23
+ /** Importing a module executes trusted code. Compatibility gates activate, not module evaluation. */
24
+ export async function loadCorePluginRegistry({ entries = [], coreVersion } = {}) {
25
+ if (!Array.isArray(entries)) pluginFail('IRIS_PLUGIN_INVALID', '入口须为明确文件列表');
26
+ const plugins = [], assets = new Map();
27
+ for (const entry of entries) {
28
+ if (typeof entry !== 'string' || !path.isAbsolute(entry) || !/\.m?js$/.test(entry) || !fs.statSync(entry).isFile()) pluginFail('IRIS_PLUGIN_INVALID', '插件入口须为 JS/MJS 文件绝对路径');
29
+ const file = fs.realpathSync(entry), definition = definePlugin((await import(pathToFileURL(file).href)).default);
30
+ for (const [id, files] of pinPluginWebAssets(definition, file)) assets.set(id, files);
31
+ plugins.push(definition);
32
+ }
33
+ const registry = await createCorePluginRegistry({ plugins, coreVersion });
34
+ registerPluginWebAssets(registry, assets);
35
+ return registry;
36
+ }
37
+
38
+ /** Trusted Core assembly. Grants are captured once; individual calls cannot enlarge them. */
39
+ export function createCorePluginHost({ dataRoot, registry, allowedOperations = [] } = {}) {
40
+ const selection = createCoreCapabilityCatalog({ registry }).selectOperations(allowedOperations);
41
+ const grants = Object.freeze(selection.map(item => item.id)), application = createApplicationRuntime({ dataRoot });
42
+ return Object.freeze({
43
+ call: (id, input, { dependency, signal } = {}) => application.executePluginOperation(registry, id, input,
44
+ { allowedOperations: grants, dependency, signal }),
45
+ snapshot: () => application.snapshot(),
46
+ dispose: () => application.dispose()
47
+ });
48
+ }
49
+
50
+ /** Public, bounded Panel assembly. Media remains read-only; Provider editing is an explicit owner gate. */
51
+ export function createCorePluginWorkbench(options) {
52
+ if (!pluginFields(options, ['dataRoot', 'registry', 'panelContributions', 'panelManifestPath', 'providerConfigPath', 'allowProviderManagement', 'allowedOperations', 'connectionStore', 'allowMcp', 'workbenchAuth'], ['dataRoot'])) {
53
+ pluginFail('IRIS_PLUGIN_WEB_INVALID', '工作台只接受明确的数据根、注册器、Panel 选择及供应商管理设置');
54
+ }
55
+ const { dataRoot, registry: pluginRegistry, panelContributions, panelManifestPath, providerConfigPath, allowProviderManagement = false, allowedOperations, connectionStore, allowMcp = false, workbenchAuth } = options;
56
+ return createLocalReadApi({ dataRoot, serveWorkbench: true, pluginRegistry, panelContributions, panelManifestPath, providerConfigPath, allowProviderManagement,
57
+ allowedOperations, connectionStore, allowConnectionManagement: connectionStore !== undefined, allowMcp, workbenchAuth });
58
+ }
@@ -13,6 +13,7 @@ import {
13
13
  providerErrorRecord,
14
14
  resultFromThrown
15
15
  } from './provider-contract.js';
16
+ import { checkedPluginDependency } from './plugin-contract.js';
16
17
 
17
18
  export const PROVIDER_ADAPTER_CONTRACT_VERSION = 0;
18
19
 
@@ -72,7 +73,7 @@ export function defineProviderAdapter(input) {
72
73
  if (!isPlainObject(input)) throw new TypeError('Provider Adapter 必须是对象');
73
74
  const allowed = new Set([
74
75
  'contractVersion', 'id', 'protocol', 'capabilities', 'operations', 'unsupported',
75
- 'prepareInput', 'unsupportedPreparation'
76
+ 'prepareInput', 'unsupportedPreparation', 'pluginDependency'
76
77
  ]);
77
78
  for (const key of Object.keys(input)) {
78
79
  if (!allowed.has(key)) throw new Error('Provider Adapter 不允许顶层字段:' + key);
@@ -129,6 +130,8 @@ export function defineProviderAdapter(input) {
129
130
  }
130
131
  const unsupportedPreparation = input.prepareInput ? undefined
131
132
  : normalizedReason(input.unsupportedPreparation, '协议未实现临时输入上传');
133
+ const pluginDependency = input.pluginDependency === undefined ? undefined : checkedPluginDependency(input.pluginDependency);
134
+ if (pluginDependency && (pluginDependency.kind !== 'providers' || pluginDependency.contributionId !== protocol)) throw new TypeError('Provider Adapter 的插件依赖与协议不一致');
132
135
 
133
136
  return Object.freeze({
134
137
  contractVersion,
@@ -137,6 +140,7 @@ export function defineProviderAdapter(input) {
137
140
  capabilities: Object.freeze(capabilities),
138
141
  operations: Object.freeze(operations),
139
142
  unsupported: Object.freeze(unsupported),
143
+ ...(pluginDependency ? { pluginDependency } : {}),
140
144
  prepareInput: input.prepareInput,
141
145
  unsupportedPreparation
142
146
  });
@@ -10,10 +10,12 @@ import * as defaultTransport from './adapters.js';
10
10
  import { ProviderContractError, providerErrorRecord } from './provider-contract.js';
11
11
  import { writePrivateFile } from './private-storage.js';
12
12
  import { defineProviderAdapter } from './provider-adapter.js';
13
- import { selectMediaProtocol, providerApiKey, providerMediaBaseUrl, unsupportedProtocolError, supportsImageEdit, dashscopeTtsInputIssue } from './provider-protocol.js';
13
+ import { selectMediaProtocol, providerApiKey, providerMediaBaseUrl, unsupportedProtocolError, supportsImageEdit, dashscopeTtsInputIssue, BUILTIN_PROVIDER_PROTOCOLS } from './provider-protocol.js';
14
14
  import { modelRateLimit, reportModelRateLimit, isFreeQuotaExhausted, recordModelAvailability } from './provider-health.js';
15
15
  import { modelUnavailable, modelUnavailableReason, modelAvailabilityBinding } from './model-directory.js';
16
16
  import { prepareVisionImage, visionInputLimits } from './vision-image-input.js';
17
+ import { createComfyUiProviderAdapter } from './comfyui-adapter.js';
18
+ import { PROVIDER_RESPONSE_LIMITS } from './provider-response.js';
17
19
 
18
20
  const AMBIGUOUS_4XX = new Set([408, 409, 425, 499]);
19
21
 
@@ -26,7 +28,7 @@ function providerIdentity(provider) {
26
28
  return { id, key, baseUrl };
27
29
  }
28
30
 
29
- function mappedCategory(error, status) {
31
+ function mappedCategory(error, status, stage) {
30
32
  const raw = String(error?.category || '').toLowerCase();
31
33
  if (error?.name === 'AbortError' || error?.code === 'ABORT_ERR') return 'aborted';
32
34
  if (error?.name === 'TimeoutError' || raw === 'timeout'
@@ -37,7 +39,10 @@ function mappedCategory(error, status) {
37
39
  if (status === 429 || raw === 'rate_limit') return 'rate_limit';
38
40
  if (raw === 'invalid_parameter' || raw === 'invalid_request') return 'invalid_request';
39
41
  if (raw === 'protocol') return 'protocol';
42
+ if (raw === 'local_io') return 'local_io';
40
43
  if (raw === 'network' || error instanceof TypeError) return 'network';
44
+ if (!status && ['download', 'persist'].includes(stage)
45
+ && ['ENOSPC', 'EDQUOT', 'EACCES', 'EPERM', 'ENOENT', 'EEXIST', 'EROFS', 'EIO'].includes(error?.code)) return 'local_io';
41
46
  if (raw === 'server' || status >= 500) return 'provider';
42
47
  if (!status && /未返回|响应|协议|结构|json/i.test(String(error?.message || ''))) return 'protocol';
43
48
  return 'provider';
@@ -50,7 +55,7 @@ export function mapProviderAdapterError(error, context = {}) {
50
55
  ? statusValue
51
56
  : undefined;
52
57
  const stage = String(context.stage || error?.stage || 'submit');
53
- const category = mappedCategory(error, httpStatus);
58
+ const category = mappedCategory(error, httpStatus, stage);
54
59
  let acceptance = context.acceptance || error?.acceptance;
55
60
  if (stage === 'validate' || stage === 'prepare' || stage === 'upload') {
56
61
  acceptance = 'not_accepted';
@@ -117,8 +122,16 @@ function openAiImageArtifacts(items) {
117
122
  }
118
123
 
119
124
  function writeInlineArtifact(artifact, targetPath) {
125
+ const maxBytes = String(artifact.mediaType || '').startsWith('image/') ? PROVIDER_RESPONSE_LIMITS.image
126
+ : artifact.mediaType === 'text/plain' ? PROVIDER_RESPONSE_LIMITS.transcript : PROVIDER_RESPONSE_LIMITS.audio;
127
+ if (typeof artifact.data === 'string' && artifact.data.length > Math.ceil(maxBytes / 3) * 4) {
128
+ throw new ProviderContractError('内嵌媒体超过允许的大小', { stage: 'download', category: 'protocol', acceptance: 'accepted' });
129
+ }
120
130
  const data = String(artifact?.data || '').trim();
121
131
  const body = Buffer.from(data, 'base64');
132
+ if (body.length > maxBytes) {
133
+ throw new ProviderContractError('内嵌媒体超过允许的大小', { stage: 'download', category: 'protocol', acceptance: 'accepted' });
134
+ }
122
135
  if (!data || body.length === 0 || body.toString('base64') !== data) {
123
136
  throw new ProviderContractError('inline-base64 媒体数据无效', {
124
137
  stage: 'download', category: 'protocol', acceptance: 'accepted'
@@ -262,6 +275,8 @@ function operationsForDashScope(provider, transport) {
262
275
  return {
263
276
  bytes: await transport.downloadTo(artifact.url, targetPath, {
264
277
  signal: request.signal,
278
+ maxBytes: { image: PROVIDER_RESPONSE_LIMITS.image, video: PROVIDER_RESPONSE_LIMITS.video,
279
+ tts: PROVIDER_RESPONSE_LIMITS.audio, transcribe: PROVIDER_RESPONSE_LIMITS.transcript }[request.capability] || PROVIDER_RESPONSE_LIMITS.video,
265
280
  ...(request.timeoutMs ? { timeoutMs: request.timeoutMs } : {})
266
281
  })
267
282
  };
@@ -278,8 +293,8 @@ export function createDashScopeProviderAdapter(provider, { transport = defaultTr
278
293
  protocol: 'dashscope',
279
294
  capabilities: ['image', 'video', 'tts', 'transcribe'],
280
295
  operations: operationsForDashScope(provider, transport),
281
- async prepareInput({ model, filePath, signal, timeoutMs } = {}) {
282
- return { url: await transport.uploadTempFile({ key, baseUrl, model, filePath, signal, timeoutMs }) };
296
+ async prepareInput({ model, filePath, bytes, filename, signal, timeoutMs } = {}) {
297
+ return { url: await transport.uploadTempFile({ key, baseUrl, model, filePath, ...(bytes !== undefined ? { bytes, filename } : {}), signal, timeoutMs }) };
283
298
  },
284
299
  unsupported: {
285
300
  cancel: '当前 DashScope 媒体协议未提供经过验证的远端取消实现'
@@ -345,6 +360,7 @@ function operationsForOpenAiImages(provider, transport, protocol = 'openai-image
345
360
  return {
346
361
  bytes: await transport.downloadTo(artifact.url, targetPath, {
347
362
  signal: request.signal,
363
+ maxBytes: PROVIDER_RESPONSE_LIMITS.image,
348
364
  ...(request.timeoutMs ? { timeoutMs: request.timeoutMs } : {})
349
365
  })
350
366
  };
@@ -418,22 +434,44 @@ export function createGeminiWebMusicProviderAdapter(provider, { transport = defa
418
434
  });
419
435
  }
420
436
 
421
- const PROTOCOL_FACTORIES = new Map([
422
- ['gemini-web-music', createGeminiWebMusicProviderAdapter],
423
- ['dashscope', createDashScopeProviderAdapter],
424
- ['openai-images', createOpenAiImagesProviderAdapter],
425
- ['openai-chat-images', createOpenAiChatImagesProviderAdapter],
426
- ['openai-responses-images', createOpenAiResponsesImagesProviderAdapter]
427
- ]);
437
+ const PROTOCOL_FACTORIES = Object.freeze({
438
+ 'gemini-web-music': createGeminiWebMusicProviderAdapter,
439
+ dashscope: createDashScopeProviderAdapter,
440
+ 'openai-images': createOpenAiImagesProviderAdapter,
441
+ 'openai-chat-images': createOpenAiChatImagesProviderAdapter,
442
+ 'openai-responses-images': createOpenAiResponsesImagesProviderAdapter,
443
+ comfyui: createComfyUiProviderAdapter
444
+ });
445
+
446
+ export function builtinProviderExtension() {
447
+ return { contractVersion: 0, id: 'iris.builtin-media', version: '0.1.0',
448
+ protocols: BUILTIN_PROVIDER_PROTOCOLS.map(descriptor => ({ ...descriptor, createAdapter: PROTOCOL_FACTORIES[descriptor.id] })) };
449
+ }
428
450
 
429
451
  export function createConfiguredProviderAdapter(provider, options = {}) {
430
452
  const selection = options.protocol
431
453
  ? selectMediaProtocol({ ...provider, mediaProtocol: options.protocol, protocolInferred: false })
432
454
  : selectMediaProtocol(provider);
433
- const factory = PROTOCOL_FACTORIES.get(selection.mediaProtocol);
434
- if (!factory) throw unsupportedProtocolError(selection.mediaProtocol);
435
- const adapter = factory(provider, options);
455
+ const factory = Object.hasOwn(PROTOCOL_FACTORIES, selection.mediaProtocol) ? PROTOCOL_FACTORIES[selection.mediaProtocol] : null;
456
+ if (!options.registry && !factory) throw unsupportedProtocolError(selection.mediaProtocol);
457
+ const adapter = options.registry ? options.registry.createAdapter(selection.mediaProtocol, provider, options) : factory(provider, options);
436
458
  const operations = { ...adapter.operations };
459
+ const key = providerApiKey(provider);
460
+ const scrub = value => typeof value === 'string' && key ? value.split(key).join('[REDACTED]') : value;
461
+ operations.mapError = (error, context) => {
462
+ let mapped;
463
+ try { mapped = adapter.operations.mapError(error, context); } catch (failure) { mapped = failure; }
464
+ mapped ||= error;
465
+ const defaults = {};
466
+ for (const field of ['stage', 'category', 'acceptance', 'retryable']) {
467
+ if (mapped?.[field] === undefined) defaults[field] = context?.[field];
468
+ }
469
+ return providerErrorRecord(mapped, {
470
+ ...defaults,
471
+ safeMessage: scrub(String(mapped?.safeMessage || mapped?.message || mapped)),
472
+ providerCode: scrub(mapped?.providerCode ?? mapped?.code)
473
+ });
474
+ };
437
475
  for (const name of ['submit', 'poll']) {
438
476
  if (!operations[name]) continue;
439
477
  const run = operations[name];
@@ -441,15 +479,15 @@ export function createConfiguredProviderAdapter(provider, options = {}) {
441
479
  const modelId = request.model || options.modelId;
442
480
  const report = error => reportModelRateLimit(provider, modelId, {
443
481
  httpStatus: error.httpStatus ?? error.status, category: error.category,
444
- code: error.code, providerCode: error.providerCode, message: error.safeMessage || error.message,
482
+ code: scrub(error.code), providerCode: scrub(error.providerCode), message: scrub(error.safeMessage || error.message),
445
483
  retryAfterMs: error.retryAfterMs,
446
484
  source: options.allowRateLimited ? 'probe' : 'task'
447
485
  }, options.onRateLimit, request.capability);
448
486
  const availability = result => {
449
487
  if (name !== 'submit') return;
450
488
  const event = { ok: result.ok === true, category: result.category,
451
- httpStatus: result.httpStatus ?? result.status, providerCode: result.providerCode || result.code,
452
- note: result.safeMessage || result.message,
489
+ httpStatus: result.httpStatus ?? result.status, providerCode: scrub(result.providerCode || result.code),
490
+ note: scrub(result.safeMessage || result.message),
453
491
  stage: 'submit', source: options.allowRateLimited ? 'probe' : 'task',
454
492
  binding: modelAvailabilityBinding(provider, modelId, capability) };
455
493
  if (!recordModelAvailability(provider, modelId, request.capability, event)) return;