diff --git a/CHANGELOG.md b/CHANGELOG.md index d562793..78f1437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ ## Unreleased +- Windows Node 的每个专业软件 adapter 现在自带唯一运行时合同,不再受安装目录遗留共享合同影响;诊断信息会显示实际合同路径、摘要和 Workspace 协议状态,服务端也会阻止协议不匹配的任务反复派发。 + - 修复专业软件持久工程首次建单失败后反复重试的问题;新任务会先建立工程空间再提交执行,并在数据库完整性异常时返回可诊断的错误。 - 专业软件任务失败或取消后会在原对话留下持久通知,页面离线、刷新或对话暂时忙碌时也不会错过结果。 diff --git a/DESIGN.md b/DESIGN.md index df723be..fcc94b9 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -462,7 +462,7 @@ scheduled_jobs(§8.5) channel_bindings(§8.7,判别列+JSONB) 云端控制面使用独立的 `software_node_enrollments` 与 `software_nodes`,不复用用户外部系统连接。管理员创建的一次性注册码具有 128 bit 随机熵,数据库只保存 SHA-256 摘要;节点注册在行锁事务中校验有效期、预期名称和服务端支持的能力,成功后原子消费。注册码只建立节点身份,不要求管理员逐项授权专业软件能力。每个节点获得独立高熵 Token,数据库只保存 bcrypt 强哈希,明文仅在注册响应出现一次。 -专业软件采用“共享能力契约 + 本机 adapter”边界。仓库根目录 `software-contracts/*.json` 是语言无关的声明式事实源,描述 capability、请求 JSON Schema、输入额度、输出 manifest、feature 与最低 adapter 版本;Core 按文件签名热加载契约,只负责身份、账本、调度、传输、摘要与最终发布,不包含 Origin 或其他软件的操作分支。热加载先完整校验新快照再原子切换,写入中或非法版本继续使用上一份有效快照。Windows Node 是稳定的可信宿主,只负责持久化 job 目录、下载/上传、恢复、取消,以及从程序目录 `adapters/*/adapter.json` 发现 adapter;Host 对请求只做通用 JSON Schema 校验,具体软件探测和二次语义校验属于 Worker。manifest 声明 capability、实际 adapter 版本、运行类型、入口和契约文件,并可声明 1–1440 分钟的单任务超时;旧 manifest 缺省时保持 30 分钟,版本与时限不编译进 EXE。完整 Node 包按目录自动收集 adapter,并把根目录契约复制到只读 `software-contracts/`;单 adapter 更新包仍可把同一契约副本放在 adapter 目录,加载时本地副本优先,两者都不形成源码第二事实源。Worker 可以是受管 Python 脚本或独立 EXE,进程只接收 job 目录,并以 `--probe` 返回运行状态,通过 `state.json`、`terminal.json` 与固定输出目录交接;Python、COM 或某个 SDK 都不属于通用协议。当前 Origin 和 ANSYS adapter 使用 Python 只是实现选择。 +专业软件采用“共享能力契约 + 本机 adapter”边界。仓库根目录 `software-contracts/*.json` 是语言无关的声明式源码事实源,描述 capability、请求 JSON Schema、输入额度、输出 manifest、feature 与最低 adapter 版本;Core 按文件签名热加载契约,只负责身份、账本、调度、传输、摘要与最终发布,不包含 Origin 或其他软件的操作分支。热加载先完整校验新快照再原子切换,写入中或非法版本继续使用上一份有效快照。Windows Node 是稳定的可信宿主,只负责持久化 job 目录、下载/上传、恢复、取消,以及从程序目录 `adapters/*/adapter.json` 发现 adapter;发布过程把对应源码合同复制进各 adapter 目录,Host 只读取 manifest 同目录指向的这一份合同并做通用 JSON Schema 校验,不再读取共享目录或采用本地优先 fallback。这样完整 Node 包与独立 adapter 包具有相同部署单元,manifest、合同、Worker 和依赖必须整体更新,避免旧合同静默覆盖新合同。manifest 声明 capability、实际 adapter 版本、运行类型、入口和契约文件,并可声明 1–1440 分钟的单任务超时;旧 manifest 缺省时保持 30 分钟,版本与时限不编译进 EXE。Node 在心跳中上报实际加载合同的 SHA-256 和 Workspace 协议版本,服务端只向明确兼容的节点调度 Workspace Job;具体软件探测和二次语义校验属于 Worker。Worker 可以是受管 Python 脚本或独立 EXE,进程只接收 job 目录,并以 `--probe` 返回运行状态,通过 `state.json`、`terminal.json` 与固定输出目录交接;Python、COM 或某个 SDK 都不属于通用协议。当前 Origin 和 ANSYS adapter 使用 Python 只是实现选择。 扩展现有 capability 的 feature 时修改共享契约、对应 Worker 和测试,不修改 Core 调度、Job 生命周期或 Node EXE;增加新专业软件时新增契约与一个 adapter 目录。Cloud 会在下一次契约查询时获得新校验和工具 schema;Node 在启动时从本地目录获得能力、版本和入口,并在首次连接及每次心跳中上报全部已发现 adapter。服务端用本次上报中自己具有共享契约的 capability 原子替换节点能力列表,未知能力不进入调度;管理端只读展示该结果,不保存人工能力选择。现有节点因此不需要清身份或重新注册,升级 Node 后重启一次即可在下一次心跳同步。当前选择明确的运维边界:zcbot 契约可热更新,不要求重启;本机 adapter 更新时先退出托盘 Node,整体替换 adapter 目录后重新启动,不重装、不替换 Node EXE,也不建设自动更新平台。当前 Node 仍按整机单执行槽保守串行,未来只有真实并行软件需求出现时,才把 slot 账本升级为 per-capability 租约,而不改变 Job 协议。 diff --git a/PROGRESS.md b/PROGRESS.md index 7984f1a..0e3dae7 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -20,6 +20,8 @@ --- ## 已完成关键能力 +- **08-19 / Unreleased / Windows Node adapter 合同单入口**:仓库根合同继续作为源码事实源,完整 Node 包与独立 adapter 包均在发布时把合同放入对应 adapter 目录;Node 移除共享 `software-contracts` fallback,心跳与诊断新增实际合同摘要和 Workspace 协议状态,服务端只向明确支持协议的节点派发 Workspace Job,避免遗留合同覆盖与确定性拒绝循环。 + - **08-19 / Unreleased / Windows Node 持久 Workspace 与按需导出**:新增独立 `software_workspaces` 注册表,首个 Job 建立 Workspace、后续 Job 只从当前 head 在同一 home node 串行续作;Node 以 `workspaces//current + rollback` 保存两代状态,Origin adapter 1.0.0 可打开上一版 OPJU 再加工,失败自动回滚且不会按 Job 永久复制工程。Workspace 完成时只上传契约声明的预览缓存,预览不注册 Artifact;用户明确要求下载或交付时才由 `software_job_export` 触发原节点上传并登记正式产物。专项 101 项 unittest 与 .NET build 通过;全量 672 项仍仅 3 个既有数据库集成模块因显式测试库缺少 `users` 表未通过(另跳过 4 项),未连接或迁移生产数据库。 - **08-18 / Unreleased / Windows Node 能力自动同步**:注册码只建立节点身份,不再要求管理员选择 Origin/ANSYS 能力;Node 从 adapter registry 自动发现全部本机能力,并在首次连接及每次心跳上报,服务端以双方已有共享契约的能力覆盖节点调度列表,管理后台改为只读展示。现有节点无需清身份或重新注册,替换新版 Node 并重启一次即可同步。 diff --git a/RUN.md b/RUN.md index ac06381..7ccf97e 100644 --- a/RUN.md +++ b/RUN.md @@ -1137,7 +1137,7 @@ Web 用户登录后,文件栏 Job 中心会聚合本人最近任务。活动 常用二维扩展单图 type 包括 `area`、`polar`、`pie`,均使用 `input/x/y`;其中 polar 的 X 是角度(度)、Y 是半径,pie 的 X 是扇区标签、Y 是数值,两者使用 Origin 原生坐标/标签系统且不接受笛卡尔 `x_axis/y_axis/z_axis` 参数。`stacked_area`、`stacked_bar` 与 `stacked_column` 一样至少提供两条 `input/x/y`。堆叠系列可以来自不同输入,但行数和逐行 X 值必须完全一致,否则 Worker 在启动绘图前以 `STACKED_PLOT_X_VALUES_MISMATCH` 拒绝,避免错位累计。统计图与这些扩展图暂不混入 `multi_panel.series[].kind`。 -专业软件契约位于 `software-contracts/*.json`。zcbot 会在文件变化后校验并热加载完整契约快照,正常修改契约不需要重启服务;非法或尚未写完的文件不会替换上一份有效快照。Node Host 从 EXE 同级的 `adapters/*/adapter.json` 发现本机能力,通用校验只读取 manifest 指向的 JSON Schema;完整包把所有共享契约放入 EXE 同级 `software-contracts/`,adapter 独立更新包也可携带同名契约副本。manifest 可用 `worker_timeout_minutes` 为每种软件声明 1–1440 分钟的执行上限;旧 manifest 未声明时继续使用 30 分钟。Worker 可以是受管 Python 脚本或独立 EXE。 +专业软件契约的源码事实源位于 `software-contracts/*.json`。zcbot 服务端会在文件变化后校验并热加载完整契约快照,正常修改契约不需要重启服务;非法或尚未写完的文件不会替换上一份有效快照。Node 发布时把对应事实源复制进 `adapters//`,运行时只读取 manifest 同目录指向的合同,不存在共享目录 fallback;因此完整 Node 包与独立 adapter 包具有相同结构,更新时必须整体替换 adapter 目录并重启 Node。Node 心跳上报实际合同 SHA-256 和 Workspace 协议版本,服务端不会把 Workspace Job 派给未明确声明支持的节点。manifest 可用 `worker_timeout_minutes` 为每种软件声明 1–1440 分钟的执行上限;旧 manifest 未声明时继续使用 30 分钟。Worker 可以是受管 Python 脚本或独立 EXE。 两个 JSON 的职责不同:`adapter.json` 只描述本机如何启动(实际版本、runtime、入口和契约文件名),`origin.plot.v2.json` 描述云端与本机共同遵守的业务请求/输出契约。前者不能替代后者;独立打包脚本只是把根目录的同一份业务契约复制进 adapter 交付目录,不维护第二份源码。 diff --git a/core/software_contracts.py b/core/software_contracts.py index ec8eddb..50e475d 100644 --- a/core/software_contracts.py +++ b/core/software_contracts.py @@ -13,7 +13,6 @@ from typing import Any from jsonschema import Draft202012Validator, FormatChecker - CONTRACT_ROOT = Path(__file__).resolve().parents[1] / "software-contracts" logger = logging.getLogger(__name__) @@ -355,10 +354,13 @@ def node_supports_request( if isinstance(advertised_features, list) and feature not in advertised_features: return False required_version = contract.required_adapter_version(request) - if contract.workspace is not None and version_at_least( - contract.workspace.required_adapter_version, required_version - ): - required_version = contract.workspace.required_adapter_version + if contract.workspace is not None: + if int(capability_runtime.get("workspace_protocol_version") or 0) < 1: + return False + if version_at_least( + contract.workspace.required_adapter_version, required_version + ): + required_version = contract.workspace.required_adapter_version return version_at_least(actual_version, required_version) if contract.workspace is not None: return False @@ -389,6 +391,11 @@ def node_available_slots(capability: str, runtime: dict[str, Any]) -> int: contract = get_contract(capability) item = (runtime.get("capability_runtime") or {}).get(capability) if isinstance(item, dict) and item.get("health") == "ready": + if ( + contract.workspace is not None + and int(item.get("workspace_protocol_version") or 0) < 1 + ): + return 0 return max(0, int(item.get("available_slots") or 0)) legacy_config = contract.legacy_runtime or {} if not legacy_config: diff --git a/tests/test_software_contracts.py b/tests/test_software_contracts.py index 8d8a032..4e04675 100644 --- a/tests/test_software_contracts.py +++ b/tests/test_software_contracts.py @@ -11,6 +11,7 @@ from core.software_contracts import ( DEFAULT_CAPABILITIES, _ContractRegistry, get_contract, + node_available_slots, node_supports_request, version_at_least, ) @@ -62,12 +63,22 @@ class SoftwareContractTests(unittest.TestCase): "health": "ready", "available_slots": 1, "adapter_version": "1.0.0", + "workspace_protocol_version": 1, "features": ["line", "heatmap"], } } } self.assertTrue(node_supports_request(contract, _request("heatmap"), current_runtime)) + self.assertEqual(node_available_slots("origin.plot@v2", current_runtime), 1) self.assertFalse(node_supports_request(contract, _request("bar"), current_runtime)) + current_runtime["capability_runtime"]["origin.plot@v2"].pop( + "workspace_protocol_version" + ) + self.assertFalse(node_supports_request(contract, _request("line"), current_runtime)) + self.assertEqual(node_available_slots("origin.plot@v2", current_runtime), 0) + current_runtime["capability_runtime"]["origin.plot@v2"][ + "workspace_protocol_version" + ] = 1 multi_panel = _request("line") multi_panel["operation"]["plot"] = { diff --git a/tests/test_software_nodes.py b/tests/test_software_nodes.py index 886e263..11ae2d4 100644 --- a/tests/test_software_nodes.py +++ b/tests/test_software_nodes.py @@ -716,6 +716,7 @@ class SoftwareJobProtocolTests(unittest.TestCase): node.capabilities = ["origin.plot@v2"] node.runtime = {"capability_runtime": {"origin.plot@v2": { "health": "ready", "available_slots": 1, "adapter_version": "1.0.0", + "workspace_protocol_version": 1, "features": ["line"], }}} jobs = [] diff --git a/tests/test_windows_node_source.py b/tests/test_windows_node_source.py index 9dedb8b..7cade72 100644 --- a/tests/test_windows_node_source.py +++ b/tests/test_windows_node_source.py @@ -203,8 +203,18 @@ class WindowsNodeSourceTests(unittest.TestCase): self.assertIn("..\\install-windows-node.bat", project) self.assertIn("..\\adapters\\**\\*", project) self.assertIn("adapters\\%(RecursiveDir)%(Filename)%(Extension)", project) - self.assertIn("..\\..\\software-contracts\\*.json", project) - self.assertIn("software-contracts\\%(Filename)%(Extension)", project) + self.assertIn("..\\..\\software-contracts\\origin.plot.v2.json", project) + self.assertIn("adapters\\origin.plot@v2\\origin.plot.v2.json", project) + self.assertIn( + "..\\..\\software-contracts\\ansys.mechanical.static_structural.v1.json", + project, + ) + self.assertIn( + "adapters\\ansys.mechanical.static_structural@v1\\" + "ansys.mechanical.static_structural.v1.json", + project, + ) + self.assertNotIn("software-contracts\\%(Filename)%(Extension)", project) self.assertFalse((ROOT / "install-windows-node.ps1").exists()) self.assertFalse((ROOT / "install-origin-runtime.ps1").exists()) self.assertFalse((ROOT / "install-startup.ps1").exists()) @@ -216,8 +226,11 @@ class WindowsNodeSourceTests(unittest.TestCase): self.assertIn('if /I "%~1"=="--self-contained"', script) self.assertIn("dotnet publish", script) self.assertIn('"install-windows-node.bat"', script) - self.assertIn('for /r "!PUBLISH_DIR!\\adapters" %%F in (adapter.json)', script) - self.assertIn('"!PUBLISH_DIR!\\software-contracts\\*.json"', script) + self.assertIn('for /r "%PUBLISH_DIR%\\adapters" %%F in (adapter.json)', script) + self.assertIn('do if exist "%%~fF"', script) + self.assertIn("ADAPTER_CONTRACT_COUNT", script) + self.assertIn("Published adapter has no colocated contract", script) + self.assertNotIn('"!PUBLISH_DIR!\\software-contracts\\*.json"', script) self.assertIn("tar.exe -a -c -f", script) self.assertIn("certutil.exe -hashfile", script) self.assertFalse((ROOT / "package-windows-node.ps1").exists()) @@ -275,6 +288,9 @@ class WindowsNodeSourceTests(unittest.TestCase): 'JsonSchema.Build(contractRoot.GetProperty("request_schema").Clone())', adapters, ) + self.assertIn("var contractPath = ResolveFile(root, manifest.Contract)", adapters) + self.assertNotIn("ResolveContractFile", adapters) + self.assertNotIn('AppContext.BaseDirectory, "software-contracts"', adapters) self.assertNotIn("IsValidOriginRequest", inbox) self.assertNotIn("IsValidPlot", inbox) self.assertIn('root.TryGetProperty("input_transfers"', inbox) @@ -359,6 +375,13 @@ class WindowsNodeSourceTests(unittest.TestCase): self.assertIn('"..\\software-contracts\\origin.plot.v2.json"', script) self.assertNotIn("dotnet", script.lower()) + form = (PROJECT / "ConfigurationForm.cs").read_text(encoding="utf-8") + self.assertIn("Contract SHA-256: {adapter.ContractSha256}", form) + self.assertIn("Workspace protocol:", form) + self.assertIn("Workspace state:", form) + self.assertIn("workspace_protocol_version", connection) + self.assertIn("contract_sha256", connection) + uploader = (PROJECT / "JobOutputUploader.cs").read_text(encoding="utf-8") self.assertIn('new AuthenticationHeaderValue("Bearer", config.NodeToken)', uploader) self.assertIn('DefaultRequestHeaders.Add("X-Node-Id"', uploader) diff --git a/windows-node/README.md b/windows-node/README.md index d4039c7..1c34596 100644 --- a/windows-node/README.md +++ b/windows-node/README.md @@ -2,7 +2,7 @@ 内网 MVP 的 Windows 执行节点,目标运行环境为 Windows 11 Enterprise + .NET 10 SDK 10.0.303。仓库根目录 `global.json` 固定 SDK patch;客户端只使用 .NET Windows Desktop Framework,不依赖第三方 NuGet 包。 -当前实现托盘状态角标、配置与本机任务窗口、注册、DPAPI/ACL 配置保存、WebSocket `hello`/心跳和退避重连。Node Host 以语言无关的 job 目录协议负责持久化、下载、恢复、取消和上传,并从 EXE 同级 `adapters/*/adapter.json` 发现能力;Host 只按 manifest 指向的 JSON Schema 通用校验请求,软件探测、语义校验和执行都由 Worker 完成。Worker 可为受管 Python 脚本或独立 EXE,manifest 独立声明每种软件的任务超时及可选 Workspace 状态。当前安装包带 `origin.plot@v2` 和受真机验收门保护的 `ansys.mechanical.static_structural@v1` Python Worker,但 Python 不是 Node 通用协议的一部分。本机任务列表只读取已派发到该 Node 的持久化目录;Origin 工程保存在 `workspaces//current`,只保留一份 `rollback`,完成时自动上传轻量预览,用户明确请求导出时才上传并发布所选正式产物。 +当前实现托盘状态角标、配置与本机任务窗口、注册、DPAPI/ACL 配置保存、WebSocket `hello`/心跳和退避重连。Node Host 以语言无关的 job 目录协议负责持久化、下载、恢复、取消和上传,并从 EXE 同级 `adapters/*/adapter.json` 发现能力;每个 adapter 目录必须自带 manifest 指向的合同,Node 不读取共享合同目录。Host 只按该合同的 JSON Schema 通用校验请求,软件探测、语义校验和执行都由 Worker 完成。Worker 可为受管 Python 脚本或独立 EXE,manifest 独立声明每种软件的任务超时及可选 Workspace 状态。当前安装包带 `origin.plot@v2` 和受真机验收门保护的 `ansys.mechanical.static_structural@v1` Python Worker,但 Python 不是 Node 通用协议的一部分。本机任务列表只读取已派发到该 Node 的持久化目录;Origin 工程保存在 `workspaces//current`,只保留一份 `rollback`,完成时自动上传轻量预览,用户明确请求导出时才上传并发布所选正式产物。 在仓库根目录执行一条命令生成可分发 ZIP: diff --git a/windows-node/Zcbot.WindowsNode/ConfigurationForm.cs b/windows-node/Zcbot.WindowsNode/ConfigurationForm.cs index 37fc1a7..143107a 100644 --- a/windows-node/Zcbot.WindowsNode/ConfigurationForm.cs +++ b/windows-node/Zcbot.WindowsNode/ConfigurationForm.cs @@ -309,7 +309,13 @@ internal sealed class ConfigurationForm : Form .AppendLine($"Software: {runtime.Software} {runtime.SoftwareVersion ?? "unknown"}") .AppendLine($"Health: {runtime.Health}") .AppendLine($"Detail: {runtime.Detail}") - .AppendLine($"Runtime: {adapter.RuntimePath}"); + .AppendLine($"Runtime: {adapter.RuntimePath}") + .AppendLine($"Contract: {adapter.ContractPath}") + .AppendLine($"Contract SHA-256: {adapter.ContractSha256}") + .AppendLine( + $"Workspace protocol: {(adapter.WorkspaceStateFilename is null ? "disabled" : "v1")}") + .AppendLine( + $"Workspace state: {adapter.WorkspaceStateFilename ?? "none"}"); } catch (Exception exception) when ( exception is IOException diff --git a/windows-node/Zcbot.WindowsNode/NodeAdapters.cs b/windows-node/Zcbot.WindowsNode/NodeAdapters.cs index 022eb55..8691836 100644 --- a/windows-node/Zcbot.WindowsNode/NodeAdapters.cs +++ b/windows-node/Zcbot.WindowsNode/NodeAdapters.cs @@ -1,4 +1,5 @@ using Json.Schema; +using System.Security.Cryptography; using System.Text.Json; using System.Text.RegularExpressions; @@ -14,6 +15,8 @@ internal interface INodeAdapter string RunningDetail { get; } bool SupportsLocalAcceptance { get; } string RuntimePath { get; } + string ContractPath { get; } + string ContractSha256 { get; } string? WorkspaceStateFilename { get; } IReadOnlyList PreviewOutputIds { get; } AdapterRuntimeStatus DetectRuntime(); @@ -52,6 +55,8 @@ internal sealed record NodeAdapterContract( internal sealed record AdapterDescriptor( string DirectoryPath, string EntrypointPath, + string ContractPath, + string ContractSha256, AdapterManifest Manifest, NodeAdapterContract Contract) { @@ -92,8 +97,10 @@ internal sealed record AdapterDescriptor( throw new InvalidDataException("Executable adapter must not declare runtime_id."); } - var contractPath = ResolveContractFile(root, manifest.Contract); - using var contractDocument = JsonDocument.Parse(File.ReadAllBytes(contractPath)); + var contractPath = ResolveFile(root, manifest.Contract); + var contractBytes = File.ReadAllBytes(contractPath); + var contractSha256 = Convert.ToHexString(SHA256.HashData(contractBytes)).ToLowerInvariant(); + using var contractDocument = JsonDocument.Parse(contractBytes); var contractRoot = contractDocument.RootElement; var capability = RequiredString(contractRoot, "capability"); if (!capability.Equals(manifest.Capability, StringComparison.Ordinal)) @@ -138,6 +145,8 @@ internal sealed record AdapterDescriptor( return new AdapterDescriptor( root, entrypointPath, + contractPath, + contractSha256, manifest, new NodeAdapterContract( capability, @@ -163,30 +172,6 @@ internal sealed record AdapterDescriptor( return resolved; } - private static string ResolveContractFile(string adapterRoot, string relativePath) - { - if (string.IsNullOrWhiteSpace(relativePath) - || Path.IsPathFullyQualified(relativePath) - || Path.GetFileName(relativePath) != relativePath) - { - throw new InvalidDataException("Adapter contract path must be a relative filename."); - } - var local = Path.GetFullPath(Path.Combine(adapterRoot, relativePath)); - if (local.StartsWith(adapterRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) - && File.Exists(local)) - { - return local; - } - var sharedRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "software-contracts")); - var shared = Path.GetFullPath(Path.Combine(sharedRoot, relativePath)); - if (!shared.StartsWith(sharedRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) - || !File.Exists(shared)) - { - throw new InvalidDataException("Adapter contract is missing."); - } - return shared; - } - private static void RequireOnlyProperties(JsonElement value, params string[] names) { if (value.ValueKind != JsonValueKind.Object) @@ -269,6 +254,8 @@ internal sealed class ProcessNodeAdapter : INodeAdapter "ansys.mechanical.static_structural@v1", StringComparison.Ordinal) && File.Exists(Path.Combine(Descriptor.DirectoryPath, "acceptance.py")); public string RuntimePath => runner.RuntimePath; + public string ContractPath => Descriptor.ContractPath; + public string ContractSha256 => Descriptor.ContractSha256; public string? WorkspaceStateFilename => Descriptor.Contract.WorkspaceStateFilename; public IReadOnlyList PreviewOutputIds => Descriptor.Contract.PreviewOutputIds; diff --git a/windows-node/Zcbot.WindowsNode/NodeConnectionLoop.cs b/windows-node/Zcbot.WindowsNode/NodeConnectionLoop.cs index e22d8d0..67859fc 100644 --- a/windows-node/Zcbot.WindowsNode/NodeConnectionLoop.cs +++ b/windows-node/Zcbot.WindowsNode/NodeConnectionLoop.cs @@ -578,6 +578,8 @@ internal sealed class NodeConnectionLoop(NodeConfig config, Action? software = runtime.Software, software_version = runtime.SoftwareVersion, adapter_version = item.AdapterVersion, + contract_sha256 = item.ContractSha256, + workspace_protocol_version = item.WorkspaceStateFilename is null ? 0 : 1, features = item.Features, available_slots = slots, health = runtime.Health, diff --git a/windows-node/Zcbot.WindowsNode/Zcbot.WindowsNode.csproj b/windows-node/Zcbot.WindowsNode/Zcbot.WindowsNode.csproj index 3a1b1ee..fb04fdb 100644 --- a/windows-node/Zcbot.WindowsNode/Zcbot.WindowsNode.csproj +++ b/windows-node/Zcbot.WindowsNode/Zcbot.WindowsNode.csproj @@ -20,8 +20,13 @@ PreserveNewest PreserveNewest - - software-contracts\%(Filename)%(Extension) + + adapters\origin.plot@v2\origin.plot.v2.json + PreserveNewest + PreserveNewest + + + adapters\ansys.mechanical.static_structural@v1\ansys.mechanical.static_structural.v1.json PreserveNewest PreserveNewest diff --git a/windows-node/package-windows-node.bat b/windows-node/package-windows-node.bat index 13cc2f6..4c2c078 100644 --- a/windows-node/package-windows-node.bat +++ b/windows-node/package-windows-node.bat @@ -70,15 +70,24 @@ for %%F in ( ) ) set "ADAPTER_COUNT=0" -for /r "!PUBLISH_DIR!\adapters" %%F in (adapter.json) do set /a ADAPTER_COUNT+=1 +for /r "%PUBLISH_DIR%\adapters" %%F in (adapter.json) do if exist "%%~fF" set /a ADAPTER_COUNT+=1 if !ADAPTER_COUNT! LSS 1 ( echo [ERR] Published package contains no adapters. goto :failed ) -if not exist "!PUBLISH_DIR!\software-contracts\*.json" ( - echo [ERR] Published package contains no software contracts. - goto :failed +set "ADAPTER_CONTRACT_ERROR=0" +for /r "%PUBLISH_DIR%\adapters" %%F in (adapter.json) do if exist "%%~fF" ( + set "ADAPTER_DIR=%%~dpF" + set "ADAPTER_CONTRACT_COUNT=0" + for %%C in ("!ADAPTER_DIR!*.json") do ( + if exist "%%~fC" if /I not "%%~nxC"=="adapter.json" set /a ADAPTER_CONTRACT_COUNT+=1 + ) + if !ADAPTER_CONTRACT_COUNT! LSS 1 ( + echo [ERR] Published adapter has no colocated contract: %%F + set "ADAPTER_CONTRACT_ERROR=1" + ) ) +if !ADAPTER_CONTRACT_ERROR! NEQ 0 goto :failed tar.exe -a -c -f "!ARCHIVE_PATH!" -C "!OUTPUT_ROOT!" "!PACKAGE_NAME!" if errorlevel 1 (