@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.
- package/CHANGELOG.md +81 -1
- package/README.en.md +103 -312
- package/README.md +100 -310
- package/bin/dsh-iris.js +333 -65
- package/docs/AGENT_CORE_QUICKSTART.md +64 -0
- package/docs/AGENT_QUICKSTART.md +2 -0
- package/docs/ARCHITECTURE.md +13 -3
- package/docs/AUDIO_FLOWS.md +71 -0
- package/docs/CONTACT_SHEET.md +62 -0
- package/docs/CORE_CONNECTIONS.md +99 -0
- package/docs/CORE_QUICKSTART.md +108 -0
- package/docs/CORE_RUNTIME_CONTRACT.md +7 -2
- package/docs/CORE_SERVICE.md +97 -0
- package/docs/CORE_SKILLS.md +35 -3
- package/docs/DSH_COMPATIBILITY.md +10 -0
- package/docs/DSH_RC2_ADAPTATION.md +1 -1
- package/docs/DSH_RUNTIME.md +6 -2
- package/docs/HEADLESS_CLI.md +2 -2
- package/docs/IMAGE_FLOW.md +4 -0
- package/docs/IMAGE_GENERATION.md +127 -0
- package/docs/LOCAL_API.md +4 -2
- package/docs/LOCAL_API_CONTRACT.md +39 -14
- package/docs/LOCAL_CREATION.md +7 -1
- package/docs/LOCAL_OPERATIONS.md +46 -0
- package/docs/LOCAL_SELECTION.md +57 -0
- package/docs/LOCAL_WEB.md +201 -0
- package/docs/MCP.md +191 -0
- package/docs/MUSIC.md +2 -0
- package/docs/PANEL_EXTENSIONS.md +64 -0
- package/docs/PLUGINS.md +189 -0
- package/docs/PLUGIN_STARTUP.md +100 -0
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +1 -1
- package/docs/PROVIDER_EXTENSIONS.md +78 -0
- package/docs/PROVIDER_HEALTH.md +3 -1
- package/docs/PROVIDER_SETTINGS.md +85 -0
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +6 -0
- package/docs/ROADMAP.md +17 -7
- package/docs/VISION_CLI.md +1 -1
- package/docs/WEB_PROTOTYPE.md +21 -6
- package/docs/WEB_RECOVERY.md +46 -0
- package/docs/WORKBENCH_LOGIN.md +63 -0
- package/docs/assets/screenshots/core-connections-preview.webp +0 -0
- package/docs/assets/screenshots/core-model-pools-preview.webp +0 -0
- package/docs/assets/screenshots/core-overview-preview.webp +0 -0
- package/docs/assets/screenshots/core-spectrum-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-dark-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-mobile-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-preview.webp +0 -0
- package/docs/releases/0.3.0-alpha.1.md +51 -0
- package/docs/screenshots.md +41 -1
- package/lib/actions.js +53 -2
- package/lib/adapters.js +20 -11
- package/lib/application-runtime.js +34 -7
- package/lib/audio-flow-input.js +37 -0
- package/lib/client.js +346 -53
- package/lib/comfyui-adapter.js +94 -0
- package/lib/comfyui-client.js +51 -0
- package/lib/comfyui-inspection.js +73 -0
- package/lib/comfyui-workflow.js +94 -0
- package/lib/config.js +36 -1
- package/lib/configured-image-runtime.js +50 -0
- package/lib/connection-skill.js +89 -0
- package/lib/connection-template.js +23 -0
- package/lib/core-artifact-store.js +16 -2
- package/lib/core-bridge-selection.js +31 -0
- package/lib/core-bridge-tools.js +83 -0
- package/lib/core-capability-catalog.js +182 -0
- package/lib/core-connection-diagnostics.js +49 -0
- package/lib/core-connection-store.js +277 -0
- package/lib/core-connections.js +225 -0
- package/lib/core-maintenance.js +9 -3
- package/lib/core-runtime.js +2 -1
- package/lib/core-service-client.js +170 -0
- package/lib/core-tasks.js +29 -12
- package/lib/core-writer-lease.js +16 -0
- package/lib/creation-request-store.js +38 -64
- package/lib/dsh-host-adapter.js +4 -1
- package/lib/dsh-service-bridge.js +66 -0
- package/lib/execution-ownership.js +43 -0
- package/lib/flow-store.js +105 -10
- package/lib/generation-input.js +10 -1
- package/lib/headless-vision.js +7 -7
- package/lib/image-edit-input.js +1 -1
- package/lib/image-flow.js +73 -29
- package/lib/index.js +37 -17
- package/lib/local-api-projection.js +10 -5
- package/lib/local-api-token-directory.js +31 -0
- package/lib/local-api.js +769 -67
- package/lib/local-creation.js +89 -24
- package/lib/local-operations.js +36 -0
- package/lib/local-request-root.js +86 -0
- package/lib/local-web-assets.js +37 -0
- package/lib/mcp-bridge.js +58 -0
- package/lib/mcp-check.js +26 -0
- package/lib/mcp-config.js +42 -0
- package/lib/mcp-delivery.js +46 -0
- package/lib/mcp-http.js +101 -0
- package/lib/mcp-server.js +47 -0
- package/lib/model-directory-service.js +16 -6
- package/lib/model-directory.js +3 -3
- package/lib/models.js +2 -2
- package/lib/music-transport.js +2 -1
- package/lib/operation-runner.js +236 -0
- package/lib/operation-store.js +216 -0
- package/lib/panel-extension.js +36 -0
- package/lib/plugin-contract.js +194 -0
- package/lib/plugin-execution.js +51 -0
- package/lib/plugin-launch.js +86 -0
- package/lib/plugin-operation-store.js +101 -0
- package/lib/plugin-operations.js +28 -0
- package/lib/plugin-registry.js +109 -0
- package/lib/plugin-selection.js +15 -0
- package/lib/plugin-source-image.js +5 -0
- package/lib/plugin-web-assets.js +54 -0
- package/lib/plugins.d.ts +288 -0
- package/lib/plugins.js +58 -0
- package/lib/provider-adapter.js +5 -1
- package/lib/provider-adapters.js +56 -18
- package/lib/provider-catalog.js +40 -29
- package/lib/provider-config-service.js +105 -23
- package/lib/provider-diagnostics.js +56 -0
- package/lib/provider-extensions.js +29 -0
- package/lib/provider-health.js +8 -1
- package/lib/provider-monitor.js +79 -0
- package/lib/provider-protocol.js +70 -20
- package/lib/provider-registry.js +118 -0
- package/lib/provider-response.js +28 -0
- package/lib/provider-settings.js +134 -0
- package/lib/provider-task-runner.js +22 -9
- package/lib/skill-installation.js +23 -11
- package/lib/vision-model-routing.js +3 -0
- package/lib/web/app.js +642 -0
- package/lib/web/appearance.js +111 -0
- package/lib/web/comfyui.js +146 -0
- package/lib/web/connections.js +377 -0
- package/lib/web/creation.js +355 -0
- package/lib/web/flow-view.js +33 -0
- package/lib/web/gallery.js +132 -0
- package/lib/web/index.html +291 -0
- package/lib/web/media-client.js +204 -0
- package/lib/web/plugin-panel.js +221 -0
- package/lib/web/plugin-surface.css +17 -0
- package/lib/web/providers.js +492 -0
- package/lib/web/recovery.js +105 -0
- package/lib/web/startup.js +18 -0
- package/lib/web/styles.css +518 -0
- package/lib/web/transport.js +96 -0
- package/lib/web/workbench-login.js +165 -0
- package/lib/workbench-auth-cli.js +47 -0
- package/lib/workbench-auth.js +200 -0
- package/package.json +20 -5
- package/plugins/contact-sheet/index.d.mts +4 -0
- package/plugins/contact-sheet/index.mjs +33 -0
- package/plugins/contact-sheet/web/board.css +14 -0
- package/plugins/contact-sheet/web/board.mjs +93 -0
- package/skills/iris-core/SKILL.md +8 -4
- package/skills/iris-core/references/cli.md +2 -2
- package/skills/iris-core/references/comfyui.md +69 -0
- package/skills/iris-core/references/tasks.md +2 -0
- package/user_guide.md +5 -1
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// Explicit owner-managed connection profiles. The business Core root and static grants are untouched.
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { createCoreRuntime } from './core-runtime.js';
|
|
6
|
+
import { atomicCreatePrivate, atomicWritePrivate } from './private-storage.js';
|
|
7
|
+
import { checkedClientConnection, checkedConnectionScopes, checkedDeclaredClient, readPrivateConnectionJson, connectionFail, CONNECTION_SCOPES } from './core-connections.js';
|
|
8
|
+
import { checkedBridgeCapabilities, checkedBridgeTools, selectedBridgeTools } from './core-bridge-selection.js';
|
|
9
|
+
import { exportMcpConfiguration } from './mcp-config.js';
|
|
10
|
+
import { checkedConnectionTemplate } from './connection-template.js';
|
|
11
|
+
import { connectionSkillResources } from './connection-skill.js';
|
|
12
|
+
import { bridgeToolsForScopes } from './core-bridge-tools.js';
|
|
13
|
+
import { resolveMcpConfigurationChoice, formatMcpConfiguration } from './mcp-delivery.js';
|
|
14
|
+
import { checkedPluginSelection } from './plugin-selection.js';
|
|
15
|
+
|
|
16
|
+
const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/;
|
|
17
|
+
const sha = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
18
|
+
const json = value => JSON.stringify(value, null, 2) + '\n';
|
|
19
|
+
const inside = (parent, file) => { const relative = path.relative(parent, file); return !relative || !path.isAbsolute(relative) && relative !== '..' && !relative.startsWith('..' + path.sep); };
|
|
20
|
+
const invalid = message => connectionFail('IRIS_CONNECTION_INPUT_INVALID', message);
|
|
21
|
+
function privateDirectory(directory, create = false) {
|
|
22
|
+
if (create && !fs.existsSync(directory)) fs.mkdirSync(directory, { mode: 0o700 });
|
|
23
|
+
const stat = fs.lstatSync(directory);
|
|
24
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || process.platform !== 'win32' && stat.mode & 0o077) {
|
|
25
|
+
connectionFail('IRIS_CONNECTION_CONFIG_INVALID', '连接管理目录必须是私有普通目录');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function profile(input) {
|
|
29
|
+
const { label, scopes, capabilities } = input;
|
|
30
|
+
if (typeof label !== 'string' || !label.trim() || label.length > 80 || /[\x00-\x1f\x7f]/.test(label)) invalid('请输入 1–80 字符的连接名称');
|
|
31
|
+
checkedConnectionScopes(scopes); checkedBridgeCapabilities(capabilities);
|
|
32
|
+
const pluginOperations = checkedPluginSelection(input.pluginOperations);
|
|
33
|
+
const tools = input.tools === undefined ? selectedBridgeTools(capabilities, undefined, pluginOperations) : checkedBridgeTools(input.tools);
|
|
34
|
+
if (tools.some(name => !selectedBridgeTools(capabilities, undefined, pluginOperations).includes(name))) invalid('工具不属于所选功能');
|
|
35
|
+
return { label: label.trim(), scopes: [...scopes], capabilities: [...capabilities], tools, ...(pluginOperations.length ? { pluginOperations } : {}) };
|
|
36
|
+
}
|
|
37
|
+
function only(value, names) {
|
|
38
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !names.includes(key))) invalid('连接请求包含不支持的字段');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createManagedConnectionStore({ directory, dataRoot, rootId, getOrigin,
|
|
42
|
+
availableScopes = CONNECTION_SCOPES, availableTools, onChange = () => {}, beforeCreate = () => {} }) {
|
|
43
|
+
if (typeof directory !== 'string' || !path.isAbsolute(directory)) invalid('连接管理目录须为显式根外绝对路径');
|
|
44
|
+
checkedConnectionScopes(availableScopes);
|
|
45
|
+
const records = new Map(), hashes = new Map(), templates = new Map(), archived = new Map();
|
|
46
|
+
let root, owner, state = 'new', disposal;
|
|
47
|
+
const fileFor = id => path.join(root, 'profiles', id + '.json');
|
|
48
|
+
const archiveFor = id => path.join(root, 'archived', id + '.json');
|
|
49
|
+
function grant(record) {
|
|
50
|
+
return { rootId, connectionId: record.connectionId, adapterId: 'iris.mcp', harnessKind: 'generic', label: record.label,
|
|
51
|
+
accessToken: record.accessToken, scopes: [...record.scopes], capabilities: [...record.capabilities], tools: [...record.tools],
|
|
52
|
+
...(record.pluginOperations?.length ? { pluginOperations: structuredClone(record.pluginOperations) } : {}) };
|
|
53
|
+
}
|
|
54
|
+
function checked(record) {
|
|
55
|
+
only(record, ['schemaVersion', 'rootId', 'connectionId', 'accessToken', 'label', 'scopes', 'capabilities', 'tools',
|
|
56
|
+
'revision', 'createdAt', 'updatedAt', 'revokedAt', 'creationDigest', 'lastContact', 'pluginOperations']);
|
|
57
|
+
if (record.schemaVersion !== 0 || record.rootId !== rootId || !UUID.test(record.connectionId)
|
|
58
|
+
|| !Number.isSafeInteger(record.revision) || record.revision < 1 || !/^[a-f0-9]{64}$/.test(record.creationDigest)
|
|
59
|
+
|| record.revokedAt !== null && typeof record.revokedAt !== 'string'
|
|
60
|
+
|| ![record.createdAt, record.updatedAt, ...(record.revokedAt !== null ? [record.revokedAt] : [])].every(value => typeof value === 'string' && Number.isFinite(Date.parse(value)))) {
|
|
61
|
+
connectionFail('IRIS_CONNECTION_CONFIG_INVALID', '连接档案版本、身份或修订无效');
|
|
62
|
+
}
|
|
63
|
+
profile(record);
|
|
64
|
+
const { scopes: _scopes, ...identity } = grant(record);
|
|
65
|
+
checkedClientConnection({ schemaVersion: 0, origin: 'http://127.0.0.1:1', ...identity });
|
|
66
|
+
if (record.lastContact !== null) {
|
|
67
|
+
const contact = record.lastContact;
|
|
68
|
+
if (!contact || Object.keys(contact).sort().join(',') !== 'at,clientInfo,source,transport' || contact.source !== 'client-declared'
|
|
69
|
+
|| !['stdio', 'http'].includes(contact.transport) || typeof contact.at !== 'string' || !Number.isFinite(Date.parse(contact.at))) {
|
|
70
|
+
connectionFail('IRIS_CONNECTION_CONFIG_INVALID', '客户端声明记录无效');
|
|
71
|
+
}
|
|
72
|
+
checkedDeclaredClient(contact.clientInfo);
|
|
73
|
+
}
|
|
74
|
+
return record;
|
|
75
|
+
}
|
|
76
|
+
function open() { if (state !== 'open') connectionFail('IRIS_CONNECTION_CONFIG_INVALID', '连接管理尚未开启或已关闭'); }
|
|
77
|
+
function read(id) {
|
|
78
|
+
open();
|
|
79
|
+
if (!UUID.test(id)) invalid('连接身份无效');
|
|
80
|
+
const record = records.get(id);
|
|
81
|
+
if (!record) connectionFail('IRIS_CONNECTION_NOT_FOUND', '没有此可管理连接');
|
|
82
|
+
readPrivateConnectionJson(fileFor(id));
|
|
83
|
+
if (sha(fs.readFileSync(fileFor(id))) !== hashes.get(id)) connectionFail('IRIS_CONNECTION_CONFIG_CHANGED', '档案在服务外被修改,请停用服务后核对,未覆盖文件');
|
|
84
|
+
return record;
|
|
85
|
+
}
|
|
86
|
+
function persist(record, create = false) {
|
|
87
|
+
const bytes = json(record);
|
|
88
|
+
(create ? atomicCreatePrivate : atomicWritePrivate)(fileFor(record.connectionId), bytes);
|
|
89
|
+
records.set(record.connectionId, structuredClone(record)); hashes.set(record.connectionId, sha(bytes));
|
|
90
|
+
onChange(record.revokedAt ? null : grant(record), record.connectionId);
|
|
91
|
+
}
|
|
92
|
+
function readArchive(id) {
|
|
93
|
+
read(id); readPrivateConnectionJson(archiveFor(id));
|
|
94
|
+
if (sha(fs.readFileSync(archiveFor(id))) !== archived.get(id)) {
|
|
95
|
+
connectionFail('IRIS_CONNECTION_CONFIG_CHANGED', '删除档案在服务外被修改,已保留现场');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function start() {
|
|
99
|
+
if (state !== 'new') invalid('连接管理只能显式启动一次');
|
|
100
|
+
const parent = fs.realpathSync(path.dirname(directory));
|
|
101
|
+
root = path.join(parent, path.basename(directory));
|
|
102
|
+
const physicalRoot = path.join(fs.realpathSync.native(parent), path.basename(directory)), physicalDataRoot = fs.realpathSync.native(dataRoot);
|
|
103
|
+
if (inside(physicalDataRoot, physicalRoot) || inside(physicalRoot, physicalDataRoot)) invalid('连接管理目录与 Core 数据根须相互独立');
|
|
104
|
+
privateDirectory(root, true);
|
|
105
|
+
const markerFile = path.join(root, 'store.json');
|
|
106
|
+
if (!fs.existsSync(markerFile)) {
|
|
107
|
+
if (fs.readdirSync(root).length) connectionFail('IRIS_CONNECTION_CONFIG_INVALID', '现有非空目录不是 Iris 连接管理目录');
|
|
108
|
+
atomicCreatePrivate(markerFile, json({ schemaVersion: 0, rootId }));
|
|
109
|
+
}
|
|
110
|
+
const marker = readPrivateConnectionJson(markerFile);
|
|
111
|
+
if (Object.keys(marker).length !== 2 || marker.schemaVersion !== 0 || marker.rootId !== rootId) connectionFail('IRIS_CONNECTION_CONFIG_INVALID', '连接管理目录属于其他数据空间或版本');
|
|
112
|
+
for (const name of ['profiles', 'clients', 'ownership', 'templates', 'archived']) privateDirectory(path.join(root, name), true);
|
|
113
|
+
owner = createCoreRuntime({ dataRoot: path.join(root, 'ownership'), mode: 'writer' });
|
|
114
|
+
try {
|
|
115
|
+
owner.start();
|
|
116
|
+
for (const name of fs.readdirSync(path.join(root, 'profiles'))) {
|
|
117
|
+
if (!/^[a-f0-9-]{36}\.json$/.test(name)) connectionFail('IRIS_CONNECTION_CONFIG_INVALID', '连接档案目录存在未知文件,已保留现场');
|
|
118
|
+
const file = path.join(root, 'profiles', name), record = checked(readPrivateConnectionJson(file));
|
|
119
|
+
if (name !== record.connectionId + '.json') connectionFail('IRIS_CONNECTION_CONFIG_INVALID', '连接档案身份与文件名不一致');
|
|
120
|
+
records.set(record.connectionId, record); hashes.set(record.connectionId, sha(fs.readFileSync(file)));
|
|
121
|
+
}
|
|
122
|
+
for (const name of fs.readdirSync(path.join(root, 'archived'))) {
|
|
123
|
+
if (!/^[a-f0-9-]{36}\.json$/.test(name)) connectionFail('IRIS_CONNECTION_CONFIG_INVALID', '删除档案目录存在未知文件,已保留现场');
|
|
124
|
+
const file = path.join(root, 'archived', name), record = checked(readPrivateConnectionJson(file)), digest = sha(fs.readFileSync(file));
|
|
125
|
+
if (name !== record.connectionId + '.json' || !record.revokedAt || digest !== hashes.get(record.connectionId)) {
|
|
126
|
+
connectionFail('IRIS_CONNECTION_CONFIG_INVALID', '删除档案与已撤销连接不一致,已保留现场');
|
|
127
|
+
}
|
|
128
|
+
archived.set(record.connectionId, digest);
|
|
129
|
+
}
|
|
130
|
+
for (const name of fs.readdirSync(path.join(root, 'templates'))) {
|
|
131
|
+
if (!/^[a-f0-9]{64}\.json$/.test(name) || templates.size >= 64) connectionFail('IRIS_CONNECTION_CONFIG_INVALID', '模板目录存在未知文件或超过 64 项,已保留现场');
|
|
132
|
+
const file = path.join(root, 'templates', name), template = checkedConnectionTemplate(readPrivateConnectionJson(file));
|
|
133
|
+
if (sha(json(template)) + '.json' !== name || fs.readFileSync(file, 'utf8') !== json(template)) connectionFail('IRIS_CONNECTION_CONFIG_CHANGED', '模板内容与身份不一致,已保留现场');
|
|
134
|
+
templates.set(name.slice(0, -5), template);
|
|
135
|
+
}
|
|
136
|
+
state = 'open'; return activeGrants();
|
|
137
|
+
} catch (error) { await owner.dispose(); state = 'failed'; throw error; }
|
|
138
|
+
}
|
|
139
|
+
function activeGrants() { return [...records.values()].filter(value => !value.revokedAt).map(grant); }
|
|
140
|
+
function list() {
|
|
141
|
+
open();
|
|
142
|
+
return [...records.values()].filter(record => !archived.has(record.connectionId)).sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.connectionId.localeCompare(b.connectionId))
|
|
143
|
+
.map(({ accessToken: _secret, creationDigest: _input, ...record }) => ({ ...structuredClone(record),
|
|
144
|
+
managed: true, state: record.revokedAt ? 'revoked' : record.lastContact ? 'used' : 'pending', harnessInstalled: 'unknown' }));
|
|
145
|
+
}
|
|
146
|
+
function create(input) {
|
|
147
|
+
only(input, ['key', 'label', 'scopes', 'capabilities', 'tools', 'pluginOperations']);
|
|
148
|
+
if (!UUID.test(input.key)) invalid('新建连接需要稳定 UUID');
|
|
149
|
+
const value = profile(input), creationDigest = sha(JSON.stringify(value));
|
|
150
|
+
open();
|
|
151
|
+
if (archived.has(input.key)) { readArchive(input.key); connectionFail('IRIS_CONNECTION_REVOKED', '此创建标识的连接已删除,请使用新标识新建连接'); }
|
|
152
|
+
if (records.has(input.key)) {
|
|
153
|
+
const existing = read(input.key);
|
|
154
|
+
if (existing.creationDigest !== creationDigest) connectionFail('IRIS_CONNECTION_CONFIG_CHANGED', '此创建标识已用于另一套连接配置');
|
|
155
|
+
return { connectionId: existing.connectionId, idempotentReplay: true };
|
|
156
|
+
}
|
|
157
|
+
beforeCreate(input.key);
|
|
158
|
+
const now = new Date().toISOString();
|
|
159
|
+
persist({ schemaVersion: 0, rootId, connectionId: input.key, accessToken: crypto.randomBytes(32).toString('base64url'),
|
|
160
|
+
...value, revision: 1, createdAt: now, updatedAt: now, revokedAt: null, creationDigest, lastContact: null }, true);
|
|
161
|
+
return { connectionId: input.key, idempotentReplay: false };
|
|
162
|
+
}
|
|
163
|
+
function update(id, input) {
|
|
164
|
+
only(input, ['expectedRevision', 'label', 'scopes', 'capabilities', 'tools', 'pluginOperations']);
|
|
165
|
+
const value = profile(input), record = read(id);
|
|
166
|
+
if (record.revokedAt) connectionFail('IRIS_CONNECTION_REVOKED', '此连接已撤销,请新建连接');
|
|
167
|
+
if (input.expectedRevision !== record.revision) connectionFail('IRIS_CONNECTION_CONFIG_CHANGED', '连接已改变,请刷新后重新编辑');
|
|
168
|
+
const { pluginOperations: _oldPlugins, ...original } = record;
|
|
169
|
+
persist({ ...original, ...value, revision: record.revision + 1, updatedAt: new Date().toISOString() });
|
|
170
|
+
return { connectionId: id, revision: record.revision + 1 };
|
|
171
|
+
}
|
|
172
|
+
function revoke(id, input) {
|
|
173
|
+
only(input, ['expectedRevision']);
|
|
174
|
+
const record = read(id);
|
|
175
|
+
if (record.revokedAt) return { connectionId: id, revoked: true, idempotentReplay: true };
|
|
176
|
+
if (input.expectedRevision !== record.revision) connectionFail('IRIS_CONNECTION_CONFIG_CHANGED', '连接已改变,请刷新后撤销');
|
|
177
|
+
const now = new Date().toISOString();
|
|
178
|
+
persist({ ...record, revision: record.revision + 1, updatedAt: now, revokedAt: now });
|
|
179
|
+
return { connectionId: id, revoked: true };
|
|
180
|
+
}
|
|
181
|
+
function remove(id, input) {
|
|
182
|
+
only(input, ['expectedRevision']);
|
|
183
|
+
const record = read(id);
|
|
184
|
+
if (archived.has(id)) { readArchive(id); return { connectionId: id, deleted: true, idempotentReplay: true }; }
|
|
185
|
+
if (input.expectedRevision !== record.revision) connectionFail('IRIS_CONNECTION_CONFIG_CHANGED', '连接已改变,请刷新后删除');
|
|
186
|
+
// Revoke durably before marking the record deleted. A failed archive leaves a visible, revoked connection.
|
|
187
|
+
if (!record.revokedAt) revoke(id, input);
|
|
188
|
+
privateDirectory(path.join(root, 'archived'));
|
|
189
|
+
const bytes = fs.readFileSync(fileFor(id));
|
|
190
|
+
atomicCreatePrivate(archiveFor(id), bytes);
|
|
191
|
+
archived.set(id, sha(bytes));
|
|
192
|
+
return { connectionId: id, deleted: true, idempotentReplay: false };
|
|
193
|
+
}
|
|
194
|
+
function observe(id, contact) {
|
|
195
|
+
if (!records.has(id)) return;
|
|
196
|
+
const record = read(id);
|
|
197
|
+
if (record.revokedAt) return;
|
|
198
|
+
// Display facts are checkpointed at first use, identity changes, and at most once a minute thereafter.
|
|
199
|
+
if (record.lastContact && JSON.stringify(record.lastContact.clientInfo) === JSON.stringify(contact.clientInfo)
|
|
200
|
+
&& record.lastContact.transport === contact.transport && Date.parse(contact.at) - Date.parse(record.lastContact.at) < 60000) return;
|
|
201
|
+
const next = { ...record, lastContact: contact };
|
|
202
|
+
const bytes = json(next); atomicWritePrivate(fileFor(id), bytes);
|
|
203
|
+
records.set(id, next); hashes.set(id, sha(bytes));
|
|
204
|
+
}
|
|
205
|
+
function listTemplates() {
|
|
206
|
+
open();
|
|
207
|
+
return [...templates.entries()].map(([templateId, template]) => {
|
|
208
|
+
const file = path.join(root, 'templates', templateId + '.json');
|
|
209
|
+
readPrivateConnectionJson(file);
|
|
210
|
+
if (fs.readFileSync(file, 'utf8') !== json(template)) connectionFail('IRIS_CONNECTION_CONFIG_CHANGED', '模板在服务外被修改,已保留现场');
|
|
211
|
+
return { templateId, ...structuredClone(template) };
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
function importTemplate(input) {
|
|
215
|
+
const template = checkedConnectionTemplate(input); open();
|
|
216
|
+
const templateId = sha(json(template));
|
|
217
|
+
if (templates.has(templateId)) { listTemplates(); return { templateId, idempotentReplay: true }; }
|
|
218
|
+
if (templates.size >= 64) connectionFail('IRIS_CONNECTION_BUSY', '模板已达 64 项,请使用现有模板或手动选择格式');
|
|
219
|
+
atomicCreatePrivate(path.join(root, 'templates', templateId + '.json'), json(template));
|
|
220
|
+
templates.set(templateId, template); return { templateId, idempotentReplay: false };
|
|
221
|
+
}
|
|
222
|
+
function resolveExport(input) {
|
|
223
|
+
only(input, ['transport', 'format', 'templateId', 'presetId']);
|
|
224
|
+
if (input.templateId !== undefined) {
|
|
225
|
+
if (Object.keys(input).length !== 1 || typeof input.templateId !== 'string') invalid('模板不能与手动格式混用');
|
|
226
|
+
const template = listTemplates().find(item => item.templateId === input.templateId);
|
|
227
|
+
if (!template) invalid('没有此接入模板');
|
|
228
|
+
return { transport: template.transport, format: template.format };
|
|
229
|
+
}
|
|
230
|
+
if (input.presetId !== undefined) {
|
|
231
|
+
if (Object.keys(input).length !== 1) invalid('接入预设不能与模板或手动格式混用');
|
|
232
|
+
return resolveMcpConfigurationChoice(input);
|
|
233
|
+
}
|
|
234
|
+
if (Object.keys(input).length !== 2 || input.transport === undefined || input.format === undefined) invalid('请选择明确的传输与格式');
|
|
235
|
+
return resolveMcpConfigurationChoice(input);
|
|
236
|
+
}
|
|
237
|
+
function activeRecord(id) {
|
|
238
|
+
const record = read(id);
|
|
239
|
+
if (record.revokedAt) connectionFail('IRIS_CONNECTION_REVOKED', '已撤销连接不能导出');
|
|
240
|
+
return record;
|
|
241
|
+
}
|
|
242
|
+
function exportSkill(id, input) {
|
|
243
|
+
only(input, []);
|
|
244
|
+
const record = activeRecord(id), scopes = record.scopes.filter(scope => availableScopes.includes(scope));
|
|
245
|
+
const tools = bridgeToolsForScopes(scopes, record.capabilities, record.tools, record.pluginOperations).map(tool => tool.name)
|
|
246
|
+
.filter(name => availableTools === undefined || availableTools.includes(name));
|
|
247
|
+
if (!tools.length) invalid('此连接没有当前可用的工具,无法生成 Skill');
|
|
248
|
+
const { bundle, bytes } = connectionSkillResources({ schemaVersion: 0, kind: 'iris-mcp-skill', rootId,
|
|
249
|
+
connectionId: id, revision: record.revision, label: record.label, scopes, capabilities: record.capabilities, tools,
|
|
250
|
+
...(record.pluginOperations?.length ? { pluginOperations: record.pluginOperations } : {}) });
|
|
251
|
+
return { bundle, preview: bytes['SKILL.md'].toString('utf8') };
|
|
252
|
+
}
|
|
253
|
+
function exportConfig(id, input) {
|
|
254
|
+
const choice = resolveExport(input), record = activeRecord(id);
|
|
255
|
+
const { scopes: savedScopes, ...identity } = grant(record);
|
|
256
|
+
const scopes = savedScopes.filter(scope => availableScopes.includes(scope));
|
|
257
|
+
const client = checkedClientConnection({ schemaVersion: 0, origin: getOrigin(), ...identity,
|
|
258
|
+
tools: identity.tools.filter(name => availableTools === undefined || availableTools.includes(name)) });
|
|
259
|
+
if (choice.transport === 'http') {
|
|
260
|
+
const entry = { url: getOrigin() + '/mcp', headers: { Authorization: 'Bearer ' + client.accessToken } };
|
|
261
|
+
return { ...formatMcpConfiguration(entry, choice), containsCredentials: true };
|
|
262
|
+
}
|
|
263
|
+
const content = json(client), clientFile = path.join(root, 'clients', sha(content) + '.json');
|
|
264
|
+
if (fs.existsSync(clientFile)) {
|
|
265
|
+
readPrivateConnectionJson(clientFile);
|
|
266
|
+
if (fs.readFileSync(clientFile, 'utf8') !== content) connectionFail('IRIS_CONNECTION_CONFIG_CHANGED', '导出副本被修改,已保留,未覆盖');
|
|
267
|
+
} else atomicCreatePrivate(clientFile, content);
|
|
268
|
+
const entry = exportMcpConfiguration({ connectionFile: clientFile, scopes, format: 'entry' });
|
|
269
|
+
return { ...formatMcpConfiguration(entry, choice), containsCredentials: false };
|
|
270
|
+
}
|
|
271
|
+
function dispose() {
|
|
272
|
+
if (!disposal) { state = 'closing'; disposal = Promise.resolve(owner?.dispose()).then(() => { records.clear(); hashes.clear(); templates.clear(); archived.clear(); state = 'closed'; }); }
|
|
273
|
+
return disposal;
|
|
274
|
+
}
|
|
275
|
+
return Object.freeze({ start, activeGrants, list, create, update, revoke, remove, observe,
|
|
276
|
+
listTemplates, importTemplate, resolveExport, exportSkill, exportConfig, dispose });
|
|
277
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { inspectExecutionRoot } from './operation-store.js';
|
|
5
|
+
import { checkedBridgeCapabilities, checkedBridgeTools, selectedBridgeTools, DEFAULT_BRIDGE_CAPABILITIES, LEGACY_BRIDGE_TOOLS_V0 } from './core-bridge-selection.js';
|
|
6
|
+
import { CORE_SCOPE_DEFINITIONS } from './core-capability-catalog.js';
|
|
7
|
+
import { checkedPluginSelection, hasPluginSelection } from './plugin-selection.js';
|
|
8
|
+
|
|
9
|
+
export const CONNECTION_CONTRACT_VERSION = 0;
|
|
10
|
+
export const CONNECTION_SCOPES = Object.freeze(CORE_SCOPE_DEFINITIONS.map(scope => scope.id));
|
|
11
|
+
export const HARNESS_ADAPTERS = Object.freeze([
|
|
12
|
+
Object.freeze({ adapterId: 'iris.mcp', harnessKinds: ['kimi', 'generic'], transport: 'stdio-to-local-http', contractVersions: [0] }),
|
|
13
|
+
Object.freeze({ adapterId: 'iris.dsh-service', harnessKinds: ['dsh'], transport: 'local-http', contractVersions: [0] })
|
|
14
|
+
]);
|
|
15
|
+
const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/;
|
|
16
|
+
const TOKEN = /^[A-Za-z0-9_-]{43}$/;
|
|
17
|
+
const hash = value => crypto.createHash('sha256').update(value).digest();
|
|
18
|
+
export function connectionFail(code, message) { throw Object.assign(new Error(message), { code }); }
|
|
19
|
+
function exact(value, fields, optional = []) {
|
|
20
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !fields.includes(key) && !optional.includes(key))
|
|
21
|
+
|| !fields.every(field => Object.hasOwn(value, field))) connectionFail('IRIS_CONNECTION_INPUT_INVALID', '连接配置字段无效');
|
|
22
|
+
}
|
|
23
|
+
export function checkedCoreOrigin(value) {
|
|
24
|
+
const match = typeof value === 'string' && /^http:\/\/127\.0\.0\.1:([1-9][0-9]{0,4})$/.exec(value);
|
|
25
|
+
if (!match || Number(match[1]) > 65535) {
|
|
26
|
+
connectionFail('IRIS_CONNECTION_INPUT_INVALID', 'Core 地址必须是 http://127.0.0.1:端口,不含路径或凭据');
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
function checkedIdentity(value) {
|
|
31
|
+
const adapter = HARNESS_ADAPTERS.find(item => item.adapterId === value.adapterId);
|
|
32
|
+
if (!adapter?.harnessKinds.includes(value.harnessKind) || !UUID.test(value.connectionId) || !UUID.test(value.rootId)
|
|
33
|
+
|| typeof value.label !== 'string' || !value.label.trim() || value.label.length > 80
|
|
34
|
+
|| /[\x00-\x1f\x7f]/.test(value.label)) connectionFail('IRIS_CONNECTION_INPUT_INVALID', '接入身份或标签无效');
|
|
35
|
+
}
|
|
36
|
+
function checkedScopes(scopes) {
|
|
37
|
+
if (!Array.isArray(scopes) || !scopes.includes('read') || scopes.length > CONNECTION_SCOPES.length || new Set(scopes).size !== scopes.length
|
|
38
|
+
|| scopes.some(scope => !CONNECTION_SCOPES.includes(scope))) connectionFail('IRIS_CONNECTION_INPUT_INVALID', '连接权限必须包含 read,且仅允许已知的有限范围');
|
|
39
|
+
}
|
|
40
|
+
export { checkedScopes as checkedConnectionScopes };
|
|
41
|
+
export function checkedDeclaredClient(value) {
|
|
42
|
+
exact(value, ['name', 'version'], ['title']);
|
|
43
|
+
for (const item of Object.values(value)) {
|
|
44
|
+
if (typeof item !== 'string' || !item.trim() || item.length > 160 || /[\x00-\x1f\x7f]/.test(item)) {
|
|
45
|
+
connectionFail('IRIS_CONNECTION_INPUT_INVALID', '客户端声明需要有界名称与版本');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return structuredClone(value);
|
|
49
|
+
}
|
|
50
|
+
export function projectedMcpClient(value) {
|
|
51
|
+
if (!value || typeof value.name !== 'string' || typeof value.version !== 'string') return null;
|
|
52
|
+
const text = item => item.replace(/[\x00-\x1f\x7f]/g, ' ').trim().slice(0, 160) || 'unknown';
|
|
53
|
+
return { name: text(value.name), version: text(value.version), ...(typeof value.title === 'string' && value.title.trim() ? { title: text(value.title) } : {}) };
|
|
54
|
+
}
|
|
55
|
+
export function readPrivateConnectionJson(file) {
|
|
56
|
+
if (typeof file !== 'string' || !path.isAbsolute(file)) connectionFail('IRIS_CONNECTION_CONFIG_INVALID', '连接文件必须为显式绝对路径');
|
|
57
|
+
let stat, value;
|
|
58
|
+
try {
|
|
59
|
+
stat = fs.lstatSync(file);
|
|
60
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 64 * 1024 || process.platform !== 'win32' && stat.mode & 0o077) throw new Error('private file required');
|
|
61
|
+
value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
62
|
+
} catch (_) { connectionFail('IRIS_CONNECTION_CONFIG_INVALID', '连接文件不可读、格式损坏或权限过宽'); }
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
export function checkedClientConnection(value) {
|
|
66
|
+
exact(value, ['schemaVersion', 'origin', 'rootId', 'connectionId', 'adapterId', 'harnessKind', 'label', 'accessToken'], ['capabilities', 'tools', 'pluginOperations']);
|
|
67
|
+
if (value.schemaVersion !== 0 || !TOKEN.test(value.accessToken)) connectionFail('IRIS_CONNECTION_CONFIG_INVALID', '连接凭据版本或格式不受支持');
|
|
68
|
+
checkedIdentity(value); checkedCoreOrigin(value.origin);
|
|
69
|
+
checkedPluginSelection(value.pluginOperations);
|
|
70
|
+
return { ...structuredClone(value), capabilities: checkedBridgeCapabilities(value.capabilities === undefined ? DEFAULT_BRIDGE_CAPABILITIES : value.capabilities),
|
|
71
|
+
tools: checkedBridgeTools(value.tools === undefined ? LEGACY_BRIDGE_TOOLS_V0 : value.tools) };
|
|
72
|
+
}
|
|
73
|
+
export function readConnectionGrants(file) {
|
|
74
|
+
const value = readPrivateConnectionJson(file);
|
|
75
|
+
exact(value, ['schemaVersion', 'grants']);
|
|
76
|
+
if (value.schemaVersion !== 0 || !Array.isArray(value.grants) || !value.grants.length || value.grants.length > 32) {
|
|
77
|
+
connectionFail('IRIS_CONNECTION_CONFIG_INVALID', '需要 1–32 个显式连接授权');
|
|
78
|
+
}
|
|
79
|
+
const ids = new Set(), tokens = new Set();
|
|
80
|
+
for (const grant of value.grants) {
|
|
81
|
+
exact(grant, ['rootId', 'connectionId', 'adapterId', 'harnessKind', 'label', 'accessToken', 'scopes'], ['capabilities', 'tools', 'pluginOperations']);
|
|
82
|
+
checkedIdentity(grant); checkedScopes(grant.scopes);
|
|
83
|
+
checkedPluginSelection(grant.pluginOperations);
|
|
84
|
+
checkedBridgeCapabilities(grant.capabilities === undefined ? DEFAULT_BRIDGE_CAPABILITIES : grant.capabilities);
|
|
85
|
+
checkedBridgeTools(grant.tools === undefined ? LEGACY_BRIDGE_TOOLS_V0 : grant.tools);
|
|
86
|
+
if (!TOKEN.test(grant.accessToken) || ids.has(grant.connectionId) || tokens.has(grant.accessToken)) connectionFail('IRIS_CONNECTION_CONFIG_INVALID', '连接身份或凭据重复/无效');
|
|
87
|
+
ids.add(grant.connectionId); tokens.add(grant.accessToken);
|
|
88
|
+
}
|
|
89
|
+
return value.grants.map(grant => ({ ...structuredClone(grant), capabilities: [...(grant.capabilities ?? DEFAULT_BRIDGE_CAPABILITIES)],
|
|
90
|
+
tools: [...(grant.tools ?? LEGACY_BRIDGE_TOOLS_V0)] }));
|
|
91
|
+
}
|
|
92
|
+
/** Explicit new pairing directory. No harness configuration discovery or modification. */
|
|
93
|
+
export function createConnectionPair({ dataRoot, directory, origin, harnessKind = 'generic', label, scopes = ['read'], capabilities = DEFAULT_BRIDGE_CAPABILITIES, tools, pluginOperations } = {}) {
|
|
94
|
+
checkedCoreOrigin(origin); checkedScopes(scopes);
|
|
95
|
+
checkedBridgeCapabilities(capabilities);
|
|
96
|
+
pluginOperations = checkedPluginSelection(pluginOperations);
|
|
97
|
+
tools = tools === undefined ? selectedBridgeTools(capabilities, undefined, pluginOperations) : checkedBridgeTools(tools);
|
|
98
|
+
if (tools.some(name => !selectedBridgeTools(capabilities, undefined, pluginOperations).includes(name))) connectionFail('IRIS_CONNECTION_INPUT_INVALID', '工具不属于所选功能分组');
|
|
99
|
+
if (typeof dataRoot !== 'string' || !path.isAbsolute(dataRoot)) connectionFail('IRIS_CONNECTION_INPUT_INVALID', '需要明确的 v1 数据根绝对路径');
|
|
100
|
+
const adapterId = harnessKind === 'dsh' ? 'iris.dsh-service' : 'iris.mcp';
|
|
101
|
+
const root = fs.realpathSync.native(dataRoot), marker = inspectExecutionRoot(root);
|
|
102
|
+
const client = { schemaVersion: 0, origin, rootId: marker.rootId, connectionId: crypto.randomUUID(), adapterId, harnessKind, label,
|
|
103
|
+
accessToken: crypto.randomBytes(32).toString('base64url'), capabilities: [...capabilities], tools: [...tools], ...(pluginOperations.length ? { pluginOperations } : {}) };
|
|
104
|
+
checkedClientConnection(client);
|
|
105
|
+
if (typeof directory !== 'string' || !path.isAbsolute(directory)) connectionFail('IRIS_CONNECTION_INPUT_INVALID', '输出目录须为新的绝对路径');
|
|
106
|
+
const relative = path.relative(root, path.join(fs.realpathSync.native(path.dirname(directory)), path.basename(directory)));
|
|
107
|
+
if (!relative || !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative)) connectionFail('IRIS_CONNECTION_INPUT_INVALID', '连接凭据须存放在数据根之外');
|
|
108
|
+
fs.mkdirSync(directory, { mode: 0o700 });
|
|
109
|
+
const serverFile = path.join(directory, 'server-grants.json'), clientFile = path.join(directory, 'client.json');
|
|
110
|
+
const { schemaVersion: _version, origin: _origin, ...grant } = client;
|
|
111
|
+
fs.writeFileSync(serverFile, JSON.stringify({ schemaVersion: 0, grants: [{ ...grant, scopes }] }, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
|
|
112
|
+
fs.writeFileSync(clientFile, JSON.stringify(client, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
|
|
113
|
+
return { contractVersion: 0, connectionId: client.connectionId, adapterId, harnessKind, scopes, capabilities, tools, serverFile, clientFile };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Authenticated, per-connection and per-runtime facts. No filesystem or Provider access. */
|
|
117
|
+
export function createConnectionRegistry(grants, availableScopes, { onClientInfo = () => {}, availableCapabilities, availableTools } = {}) {
|
|
118
|
+
const configured = new Map(grants.map(grant => {
|
|
119
|
+
const { accessToken, ...publicGrant } = grant;
|
|
120
|
+
return [grant.connectionId, { ...structuredClone(publicGrant), capabilities: [...(grant.capabilities ?? DEFAULT_BRIDGE_CAPABILITIES)],
|
|
121
|
+
tools: [...(grant.tools ?? LEGACY_BRIDGE_TOOLS_V0)], tokenDigest: hash(accessToken), instances: new Map() }];
|
|
122
|
+
}));
|
|
123
|
+
function identify(connectionId, token) {
|
|
124
|
+
const grant = configured.get(connectionId);
|
|
125
|
+
if (!grant || !TOKEN.test(token) || !crypto.timingSafeEqual(hash(token), grant.tokenDigest)) connectionFail('IRIS_CONNECTION_AUTH_REQUIRED', '连接身份或凭据无效');
|
|
126
|
+
return grant.connectionId;
|
|
127
|
+
}
|
|
128
|
+
function handshake(connectionId, value) {
|
|
129
|
+
exact(value, ['contractVersion', 'adapterId', 'harnessKind', 'runtimeInstanceId', 'harnessVersion', 'requestedScopes'], ['requestedCapabilities', 'requestedTools', 'requestedPluginOperations']);
|
|
130
|
+
const grant = configured.get(connectionId);
|
|
131
|
+
if (value.contractVersion !== 0) connectionFail('IRIS_CONNECTION_VERSION_UNSUPPORTED', '连接契约版本不受支持');
|
|
132
|
+
if (value.adapterId !== grant.adapterId || value.harnessKind !== grant.harnessKind || !UUID.test(value.runtimeInstanceId)
|
|
133
|
+
|| typeof value.harnessVersion !== 'string' || value.harnessVersion.length > 80 || /[\x00-\x1f\x7f]/.test(value.harnessVersion)) {
|
|
134
|
+
connectionFail('IRIS_CONNECTION_INPUT_INVALID', '握手身份不符合本地授权');
|
|
135
|
+
}
|
|
136
|
+
checkedScopes(value.requestedScopes);
|
|
137
|
+
const capabilities = checkedBridgeCapabilities(value.requestedCapabilities === undefined ? grant.capabilities : value.requestedCapabilities);
|
|
138
|
+
const tools = checkedBridgeTools(value.requestedTools === undefined ? grant.tools : value.requestedTools);
|
|
139
|
+
const pluginOperations = checkedPluginSelection(value.requestedPluginOperations === undefined ? grant.pluginOperations : value.requestedPluginOperations);
|
|
140
|
+
if (pluginOperations.some(pin => !hasPluginSelection(grant.pluginOperations || [], pin))) connectionFail('IRIS_CONNECTION_CAPABILITY_DENIED', '插件业务或版本不在明确授权中');
|
|
141
|
+
if (value.requestedScopes.some(scope => !grant.scopes.includes(scope) || !availableScopes.includes(scope))) {
|
|
142
|
+
connectionFail('IRIS_CONNECTION_CAPABILITY_DENIED', '请求的能力未获授权或服务未开启');
|
|
143
|
+
}
|
|
144
|
+
if (capabilities.some(id => !grant.capabilities.includes(id) || availableCapabilities && !availableCapabilities.includes(id)) || tools.some(name => !grant.tools.includes(name))) {
|
|
145
|
+
connectionFail('IRIS_CONNECTION_CAPABILITY_DENIED', '请求的功能或工具未获本地授权');
|
|
146
|
+
}
|
|
147
|
+
if (!grant.instances.has(value.runtimeInstanceId) && grant.instances.size >= 8) {
|
|
148
|
+
const retired = [...grant.instances.values()].find(instance => instance.state === 'disconnected');
|
|
149
|
+
if (retired) grant.instances.delete(retired.runtimeInstanceId);
|
|
150
|
+
else connectionFail('IRIS_CONNECTION_BUSY', '该连接实例数量已达上限,请由 owner 明确断开旧实例');
|
|
151
|
+
}
|
|
152
|
+
const now = new Date().toISOString(), instance = { runtimeInstanceId: value.runtimeInstanceId, harnessVersion: value.harnessVersion,
|
|
153
|
+
connectedAt: now, lastSeenAt: now, state: 'connected', scopes: [...value.requestedScopes], capabilities,
|
|
154
|
+
tools: selectedBridgeTools(capabilities, tools, pluginOperations).filter(name => availableTools === undefined || availableTools.includes(name)),
|
|
155
|
+
...(pluginOperations.length ? { pluginOperations } : {}),
|
|
156
|
+
verification: 'handshake', liveness: 'not_probed' };
|
|
157
|
+
grant.instances.set(value.runtimeInstanceId, instance);
|
|
158
|
+
return { contractVersion: 0, rootId: grant.rootId, connectionId, adapterId: grant.adapterId, harnessKind: grant.harnessKind,
|
|
159
|
+
instance: structuredClone(instance), remoteHostPorts: [], connectionFeatures: ['client-info'] };
|
|
160
|
+
}
|
|
161
|
+
function authorize(connectionId, runtimeInstanceId, scope, tool) {
|
|
162
|
+
const grant = configured.get(connectionId), instance = grant?.instances.get(runtimeInstanceId);
|
|
163
|
+
if (!instance || instance.state !== 'connected') connectionFail('IRIS_CONNECTION_HANDSHAKE_REQUIRED', '请先完成本次服务的连接握手');
|
|
164
|
+
if (!instance.scopes.includes(scope)) connectionFail('IRIS_CONNECTION_CAPABILITY_DENIED', '此连接未获该操作权限');
|
|
165
|
+
if (tool && !instance.tools.includes(tool)) connectionFail('IRIS_CONNECTION_CAPABILITY_DENIED', '此功能或工具未被用户选择');
|
|
166
|
+
instance.lastSeenAt = new Date().toISOString();
|
|
167
|
+
return [...instance.scopes];
|
|
168
|
+
}
|
|
169
|
+
function disconnect(connectionId, runtimeInstanceId) {
|
|
170
|
+
const instance = configured.get(connectionId)?.instances.get(runtimeInstanceId);
|
|
171
|
+
if (!instance) connectionFail('IRIS_CONNECTION_HANDSHAKE_REQUIRED', '没有此连接实例');
|
|
172
|
+
instance.state = 'disconnected'; instance.lastSeenAt = new Date().toISOString();
|
|
173
|
+
return { contractVersion: 0, disconnected: true };
|
|
174
|
+
}
|
|
175
|
+
function list(connectionId) {
|
|
176
|
+
return { contractVersion: 0, adapters: structuredClone(HARNESS_ADAPTERS), connections: [...configured.values()]
|
|
177
|
+
.filter(grant => connectionId === undefined || grant.connectionId === connectionId)
|
|
178
|
+
.map(({ tokenDigest: _secret, instances, ...grant }) => ({ ...structuredClone(grant), configured: true,
|
|
179
|
+
harnessInstalled: 'unknown', instances: [...instances.values()].map(instance => structuredClone(instance)) })) };
|
|
180
|
+
}
|
|
181
|
+
function dispose() { for (const grant of configured.values()) grant.tokenDigest.fill(0); configured.clear(); }
|
|
182
|
+
function selection(connectionId, runtimeInstanceId) {
|
|
183
|
+
const instance = configured.get(connectionId).instances.get(runtimeInstanceId);
|
|
184
|
+
return { capabilities: [...instance.capabilities], tools: [...instance.tools], ...(instance.pluginOperations?.length ? { pluginOperations: structuredClone(instance.pluginOperations) } : {}) };
|
|
185
|
+
}
|
|
186
|
+
function clientInfo(connectionId, runtimeInstanceId, value) {
|
|
187
|
+
exact(value, ['clientInfo', 'transport']);
|
|
188
|
+
const info = checkedDeclaredClient(value.clientInfo);
|
|
189
|
+
if (!['stdio', 'http'].includes(value.transport)) connectionFail('IRIS_CONNECTION_INPUT_INVALID', 'MCP 传输声明无效');
|
|
190
|
+
const instance = configured.get(connectionId)?.instances.get(runtimeInstanceId);
|
|
191
|
+
if (!instance || instance.state !== 'connected') connectionFail('IRIS_CONNECTION_HANDSHAKE_REQUIRED', '请先连接当前实例');
|
|
192
|
+
const contact = { clientInfo: info, transport: value.transport, source: 'client-declared', at: new Date().toISOString() };
|
|
193
|
+
onClientInfo(connectionId, contact);
|
|
194
|
+
instance.declaredClient = contact; instance.lastSeenAt = contact.at;
|
|
195
|
+
return { contractVersion: 0, recorded: true };
|
|
196
|
+
}
|
|
197
|
+
function setGrant(grant) {
|
|
198
|
+
const old = configured.get(grant.connectionId), { accessToken, ...publicGrant } = grant;
|
|
199
|
+
const same = old && crypto.timingSafeEqual(old.tokenDigest, hash(accessToken))
|
|
200
|
+
&& ['adapterId', 'harnessKind', 'scopes', 'capabilities', 'tools', 'pluginOperations'].every(key => JSON.stringify(old[key]) === JSON.stringify(publicGrant[key]));
|
|
201
|
+
if (old) old.tokenDigest.fill(0);
|
|
202
|
+
configured.set(grant.connectionId, { ...structuredClone(publicGrant), tokenDigest: hash(accessToken), instances: same ? old.instances : new Map() });
|
|
203
|
+
return { authorizationChanged: !same };
|
|
204
|
+
}
|
|
205
|
+
function revoke(connectionId) {
|
|
206
|
+
const grant = configured.get(connectionId);
|
|
207
|
+
if (grant) { grant.tokenDigest.fill(0); configured.delete(connectionId); }
|
|
208
|
+
}
|
|
209
|
+
function identifyMcp(token) {
|
|
210
|
+
if (!TOKEN.test(token)) connectionFail('IRIS_CONNECTION_AUTH_REQUIRED', '需要明确的 MCP 连接凭据');
|
|
211
|
+
const tokenDigest = hash(token);
|
|
212
|
+
const grant = [...configured.values()].find(item => crypto.timingSafeEqual(tokenDigest, item.tokenDigest));
|
|
213
|
+
if (!grant) connectionFail('IRIS_CONNECTION_AUTH_REQUIRED', 'MCP 连接凭据无效');
|
|
214
|
+
if (grant.adapterId !== 'iris.mcp') connectionFail('IRIS_CONNECTION_CAPABILITY_DENIED', '此凭据没有授权 MCP 接入');
|
|
215
|
+
const { tokenDigest: _secret, instances: _instances, ...publicGrant } = grant;
|
|
216
|
+
return structuredClone(publicGrant);
|
|
217
|
+
}
|
|
218
|
+
function authorizePlugin(connectionId, dependency) {
|
|
219
|
+
const grant = configured.get(connectionId);
|
|
220
|
+
if (!grant?.scopes.includes('execute') || !grant.tools.includes('iris_core_plugin_call') || !hasPluginSelection(grant.pluginOperations || [], dependency)) {
|
|
221
|
+
connectionFail('IRIS_CONNECTION_CAPABILITY_DENIED', '原连接的插件业务执行授权已缩减或撤销');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return Object.freeze({ identify, identifyMcp, handshake, authorize, authorizePlugin, selection, disconnect, list, clientInfo, setGrant, revoke, dispose });
|
|
225
|
+
}
|
package/lib/core-maintenance.js
CHANGED
|
@@ -5,8 +5,9 @@ import { inspectCoreArtifact, refreshCoreArtifactInventory } from './core-artifa
|
|
|
5
5
|
import { assertFlowTaskMatches, inspectCoreTask, listCoreTasks } from './core-tasks.js';
|
|
6
6
|
import { atomicWritePrivate } from './private-storage.js';
|
|
7
7
|
import { readDeliveryCache } from './core-delivery-cache.js';
|
|
8
|
-
import { flowReferences } from './flow-store.js';
|
|
8
|
+
import { flowReferences, flowStep } from './flow-store.js';
|
|
9
9
|
import { creationReferences } from './creation-request-store.js';
|
|
10
|
+
import { operationReferences } from './operation-store.js';
|
|
10
11
|
|
|
11
12
|
const TASK = /^task_[a-f0-9]{24}$/;
|
|
12
13
|
const ARTIFACT = /^artifact_[a-f0-9]{24}$/;
|
|
@@ -55,7 +56,7 @@ function inventory(root) {
|
|
|
55
56
|
.map(name => inspectCoreArtifact(root, name.slice(0, -5)));
|
|
56
57
|
const flows = flowReferences(root);
|
|
57
58
|
for (const reference of flows) {
|
|
58
|
-
if (reference.run.
|
|
59
|
+
if (flowStep(reference.run).claimed && !tasks.some(task => task.id === flowStep(reference.run).taskId)) {
|
|
59
60
|
fail('IRIS_FLOW_TASK_CONFLICT', '流程已建立的 Task 丢失,停止管理');
|
|
60
61
|
}
|
|
61
62
|
}
|
|
@@ -64,7 +65,7 @@ function inventory(root) {
|
|
|
64
65
|
if (!reference) fail('IRIS_FLOW_TASK_CONFLICT', '流程 Task 的 Run 记录丢失,停止管理');
|
|
65
66
|
assertFlowTaskMatches(task, reference.run);
|
|
66
67
|
}
|
|
67
|
-
return { tasks, artifacts, flows, creations: creationReferences(root) };
|
|
68
|
+
return { tasks, artifacts, flows, creations: creationReferences(root), operations: operationReferences(root) };
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
function ids(value, pattern, label) {
|
|
@@ -78,6 +79,10 @@ export function planCoreDeletion(root, input) {
|
|
|
78
79
|
if (!taskIds.length && !artifactIds.length) fail('IRIS_COMMAND_INPUT_INVALID', '请明确指定 Task 或 Artifact ID');
|
|
79
80
|
const existing = inventory(root), selectedTasks = new Set(taskIds), selectedArtifacts = new Set(artifactIds);
|
|
80
81
|
const blockers = [], files = [];
|
|
82
|
+
for (const reference of existing.operations) {
|
|
83
|
+
for (const id of reference.taskIds) if (selectedTasks.has(id)) blockers.push({ id, referencedBy: reference.operationId, reason: 'operation_task_reference' });
|
|
84
|
+
for (const id of reference.artifactIds) if (selectedArtifacts.has(id)) blockers.push({ id, referencedBy: reference.operationId, reason: 'operation_artifact_reference' });
|
|
85
|
+
}
|
|
81
86
|
for (const reference of existing.creations) {
|
|
82
87
|
for (const id of reference.taskIds) if (selectedTasks.has(id)) blockers.push({ id, referencedBy: reference.requestId, reason: 'creation_task_reference' });
|
|
83
88
|
for (const id of reference.artifactIds) if (selectedArtifacts.has(id)) blockers.push({ id, referencedBy: reference.requestId, reason: 'creation_artifact_reference' });
|
|
@@ -136,6 +141,7 @@ export function planCoreCleanup(root, { older_than_ms = 3600000 } = {}) {
|
|
|
136
141
|
if (!Number.isSafeInteger(older_than_ms) || older_than_ms < 60000) fail('IRIS_COMMAND_INPUT_INVALID', 'older_than_ms 至少为 60000');
|
|
137
142
|
const existing = inventory(root);
|
|
138
143
|
const referenced = new Set(existing.artifacts.map(artifact => artifact.id));
|
|
144
|
+
for (const reference of existing.operations) for (const id of reference.artifactIds) referenced.add(id);
|
|
139
145
|
for (const reference of existing.creations) for (const id of reference.artifactIds) referenced.add(id);
|
|
140
146
|
for (const reference of existing.flows) for (const id of reference.artifactIds) referenced.add(id);
|
|
141
147
|
for (const artifact of existing.artifacts) for (const edge of artifact.relations) referenced.add(edge.artifactId);
|
package/lib/core-runtime.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from './core-contract.js';
|
|
15
15
|
import { ensurePrivateDir } from './private-storage.js';
|
|
16
16
|
import { listCreationRequests } from './creation-request-store.js';
|
|
17
|
+
import { readAllOperations } from './operation-store.js';
|
|
17
18
|
import { WRITER_LEASE_DIR, WRITER_INTENT_FILE, publishLeaseRecord, readWriterLease,
|
|
18
19
|
removeWriterIntent, retireLeaseDirectory, writerOwnerStatus } from './core-writer-lease.js';
|
|
19
20
|
|
|
@@ -164,7 +165,7 @@ export function createCoreRuntime(input) {
|
|
|
164
165
|
const candidateRoot = physicalDataRoot(options);
|
|
165
166
|
const candidateLease = options.mode === 'writer' ? acquireWriterLease(candidateRoot, instanceId) : null;
|
|
166
167
|
if (candidateLease) {
|
|
167
|
-
try { listCreationRequests(candidateRoot); }
|
|
168
|
+
try { listCreationRequests(candidateRoot); readAllOperations(candidateRoot); }
|
|
168
169
|
catch (error) {
|
|
169
170
|
try { releaseWriterLease(candidateLease); }
|
|
170
171
|
catch (cleanupError) {
|