From c1df458c850a5c7394d60c3ec1cfe6fd2bd74399 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Fri, 5 Jun 2026 13:39:20 +0800 Subject: [PATCH] feat: wire hwpod target node-ops flow --- internal/cloud/server.ts | 18 +----- tools/hwpod-harness.test.ts | 20 +++++- tools/hwpod-node.test.ts | 36 +++++++++++ tools/src/hwpod-harness-lib.ts | 92 +++++++++++++++++++--------- tools/src/hwpod-node-lib.ts | 34 +++++++++- tools/src/hwpod-node-ops-contract.ts | 19 ++++++ 6 files changed, 169 insertions(+), 50 deletions(-) create mode 100644 tools/src/hwpod-node-ops-contract.ts diff --git a/internal/cloud/server.ts b/internal/cloud/server.ts index ec96efab..c708ba8c 100644 --- a/internal/cloud/server.ts +++ b/internal/cloud/server.ts @@ -67,25 +67,9 @@ import { configureSkillRuntime, ensureCodexSkillsAggregationSync } from "./skills-store.ts"; +import { HWPOD_NODE_OPS, HWPOD_NODE_OPS_CONTRACT_VERSION } from "../../tools/src/hwpod-node-ops-contract.ts"; const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024; -const HWPOD_NODE_OPS_CONTRACT_VERSION = "hwpod-node-ops-v1"; -const HWPOD_NODE_OPS = new Set([ - "node.health", - "node.version", - "node.inventory", - "workspace.ls", - "workspace.cat", - "workspace.rg", - "workspace.apply-patch", - "debug.build", - "debug.download", - "debug.reset", - "io.uart.read", - "io.uart.write", - "io.uart.jsonrpc", - "cmd.run" -]); function runtimeEnvironment(env = process.env) { const value = env.HWLAB_GITOPS_PROFILE || env.HWLAB_ENVIRONMENT || ENVIRONMENT_DEV; diff --git a/tools/hwpod-harness.test.ts b/tools/hwpod-harness.test.ts index 2a2c7309..9ea8f60c 100644 --- a/tools/hwpod-harness.test.ts +++ b/tools/hwpod-harness.test.ts @@ -65,11 +65,17 @@ test("hwpod-compiler-cli compiles workspace-local spec into node ops", async () assert.equal(result.payload.plan.ops[0].op, "workspace.ls"); assert.equal(result.payload.plan.ops[0].args.path, "src"); assert.equal(result.payload.plan.ops[0].args.workspacePath, "/workspace/fw"); + + await runHwpodCtl(["spec", "set", "spec.workspace.buildCommand", "printf spec-build", "--spec", specPath], { now: () => NOW }); + const build = await runHwpodCompilerCli(["compile", "--spec", specPath, "--intent", "debug.build"], { now: () => NOW }); + assert.equal(build.exitCode, 0); + assert.equal(build.payload.plan.ops[0].op, "debug.build"); + assert.equal(build.payload.plan.ops[0].args.command, "printf spec-build"); } finally { await rm(root, { recursive: true, force: true }); } }); -test("hwpod-cli dry-run keeps high-level intent out of hwlab-api and exposes hwpod-node-ops plan", async () => { +test("hwpod-cli dry-run invokes hwpod-compiler-cli and exposes hwpod-node-ops plan", async () => { const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-cli-")); const specPath = path.join(root, ".hwlab", "hwpod-spec.yaml"); try { @@ -77,12 +83,19 @@ test("hwpod-cli dry-run keeps high-level intent out of hwlab-api and exposes hwp const inspect = await runHwpodCli(["inspect", "--spec", specPath, "--dry-run"], { now: () => NOW }); assert.equal(inspect.exitCode, 0); assert.equal(inspect.payload.dryRun, true); + assert.equal(inspect.payload.compilerInvocation.compiler, "hwpod-compiler-cli"); + assert.equal(inspect.payload.compilerInvocation.mode, "subprocess"); + assert.equal(inspect.payload.compilerInvocation.action, "hwpod-compiler.compile"); + assert.equal(inspect.payload.plan.source.compiler, "hwpod-compiler-cli"); assert.deepEqual(inspect.payload.plan.ops.map((op: any) => op.op), ["node.health", "node.inventory"]); - const build = await runHwpodCli(["build", "--spec", specPath, "--dry-run", "--target", "Debug"], { now: () => NOW }); + const build = await runHwpodCli(["build", "--spec", specPath, "--dry-run", "--target", "Debug", "--command", "printf cli-build", "--reason", "target flow smoke"], { now: () => NOW }); assert.equal(build.exitCode, 0); + assert.equal(build.payload.compilerInvocation.mode, "subprocess"); assert.equal(build.payload.plan.ops[0].op, "debug.build"); assert.equal(build.payload.plan.ops[0].args.target, "Debug"); + assert.equal(build.payload.plan.ops[0].args.command, "printf cli-build"); + assert.equal(build.payload.plan.ops[0].args.reason, "target flow smoke"); } finally { await rm(root, { recursive: true, force: true }); } @@ -108,9 +121,12 @@ test("hwpod-cli submits compiled node ops to hwlab-api when not dry-run", async now: () => NOW }); assert.equal(result.exitCode, 0); + assert.equal(result.payload.compilerInvocation.compiler, "hwpod-compiler-cli"); + assert.equal(result.payload.compilerInvocation.mode, "subprocess"); assert.equal(seen[0].url, "http://cloud.test/v1/hwpod-node-ops"); assert.equal(seen[0].init.headers["x-hwlab-device-pod-api-key"], "hwl_live_test"); assert.equal(seen[0].body.contractVersion, "hwpod-node-ops-v1"); + assert.equal(seen[0].body.source.compiler, "hwpod-compiler-cli"); assert.equal(seen[0].body.ops[0].op, "workspace.ls"); } finally { await rm(root, { recursive: true, force: true }); diff --git a/tools/hwpod-node.test.ts b/tools/hwpod-node.test.ts index a6c5229f..cb6e6d3c 100644 --- a/tools/hwpod-node.test.ts +++ b/tools/hwpod-node.test.ts @@ -47,12 +47,48 @@ test("hwpod-node HTTP endpoint accepts hwpod-node-ops plans", async () => { const payload = await response.json(); assert.equal(response.status, 200); assert.equal(payload.ok, true); + assert.equal(payload.specAuthority, "none"); assert.equal(payload.results[0].op, "node.version"); } finally { await new Promise((resolve, reject) => server.close((error: Error | undefined) => (error ? reject(error) : resolve(undefined)))); } }); +test("hwpod-node does not fall back to legacy device-pod for debug ops", async () => { + const result = await executeHwpodNodeOpsPlan({ + contractVersion: "hwpod-node-ops-v1", + planId: "hwpod_plan_debug_unbound", + hwpodId: "hwpod-local", + nodeId: "pc-host-1", + ops: [{ opId: "op_build", op: "debug.build", args: { workspacePath: ".", target: "Debug" } }] + }, { now: () => "2026-06-05T00:00:00.000Z" }); + + assert.equal(result.ok, false); + assert.equal(result.specAuthority, "none"); + assert.equal(result.results[0].ok, false); + assert.equal(result.results[0].blocker.code, "hwpod_node_op_not_configured"); +}); + +test("hwpod-node executes debug ops only through explicit node-side command bindings", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-node-debug-")); + try { + const result = await executeHwpodNodeOpsPlan({ + contractVersion: "hwpod-node-ops-v1", + planId: "hwpod_plan_debug_bound", + hwpodId: "hwpod-local", + nodeId: "pc-host-1", + ops: [{ opId: "op_build", op: "debug.build", args: { workspacePath: root, command: "printf build-ok > build.ok" } }] + }, { now: () => "2026-06-05T00:00:00.000Z" }); + + assert.equal(result.ok, true); + assert.equal(result.results[0].ok, true); + assert.equal(result.results[0].output.bindingSource, "hwpod-node-ops.command"); + assert.match(await readFile(path.join(root, "build.ok"), "utf8"), /build-ok/u); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + test("hwpod-node applies workspace patches through the stable node op", async () => { const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-node-patch-")); try { diff --git a/tools/src/hwpod-harness-lib.ts b/tools/src/hwpod-harness-lib.ts index 6a17a493..43f5b7ab 100644 --- a/tools/src/hwpod-harness-lib.ts +++ b/tools/src/hwpod-harness-lib.ts @@ -1,10 +1,11 @@ import { randomUUID } from "node:crypto"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { HWPOD_NODE_OPS, HWPOD_NODE_OPS_CONTRACT_VERSION } from "./hwpod-node-ops-contract.ts"; import { resolveRuntimeEndpoint, runtimeEndpointVisibility } from "./runtime-endpoint-resolver.ts"; -export const HWPOD_NODE_OPS_CONTRACT_VERSION = "hwpod-node-ops-v1"; export const DEFAULT_HWPOD_SPEC_PATH = ".hwlab/hwpod-spec.yaml"; const COMPILER_NAME = "hwpod-compiler-cli"; @@ -12,23 +13,6 @@ const CTL_NAME = "hwpod-ctl"; const CLI_NAME = "hwpod-cli"; const DEFAULT_TIMEOUT_MS = 30000; const BOOLEAN_OPTIONS = new Set(["dryRun", "force", "full", "help", "h", "json", "noAuth"]); -const HWPOD_NODE_OPS = new Set([ - "node.health", - "node.version", - "node.inventory", - "workspace.ls", - "workspace.cat", - "workspace.rg", - "workspace.apply-patch", - "debug.build", - "debug.download", - "debug.reset", - "io.uart.read", - "io.uart.write", - "io.uart.jsonrpc", - "cmd.run" -]); - type EnvLike = Record; type ParsedArgs = Record & { _: string[] }; type FetchLike = typeof fetch; @@ -99,15 +83,15 @@ export async function runHwpodCli(argv: string[], options: { env?: EnvLike; fetc if (command === "closeout") return result(0, closeout(parsed), now); const specPath = specPathFrom(parsed); - const document = await readHwpodSpec(specPath); const { intent, args } = commandToIntent(parsed, options.stdinText); - const plan = compileHwpodNodeOpsPlan({ document, specPath, intent, args, now }); + const compiled = await compilePlanWithCompilerCli({ specPath, intent, args }); + const plan = compiled.plan; if (parsed.dryRun === true) { - return result(0, ok("hwpod-cli.plan", { specPath, intent: plan.intent, contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, plan, dryRun: true }), now); + return result(0, ok("hwpod-cli.plan", { specPath, intent: plan.intent, contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, compilerInvocation: compiled.compilerInvocation, plan, dryRun: true }), now); } const response = await submitHwpodNodeOpsPlan({ parsed, env, fetchImpl: options.fetchImpl, plan }); const exitCode = response.body?.ok === false || response.status >= 400 ? 1 : 0; - return result(exitCode, ok("hwpod-cli.invoke", { specPath, intent: plan.intent, contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, route: response.route, runtimeEndpoint: response.runtimeEndpoint, body: response.body, httpStatus: response.status }, response.body?.status ?? (exitCode === 0 ? "succeeded" : "failed")), now); + return result(exitCode, ok("hwpod-cli.invoke", { specPath, intent: plan.intent, contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, compilerInvocation: compiled.compilerInvocation, route: response.route, runtimeEndpoint: response.runtimeEndpoint, body: response.body, httpStatus: response.status }, response.body?.status ?? (exitCode === 0 ? "succeeded" : "failed")), now); } catch (error) { return result(1, failure(CLI_NAME, error), now); } @@ -247,6 +231,51 @@ function closeout(parsed: ParsedArgs) { }); } +async function compilePlanWithCompilerCli({ specPath, intent, args }: { specPath: string; intent: string; args: Record }) { + const compilerPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../hwpod-compiler-cli.ts"); + const argsJson = JSON.stringify(args ?? {}); + const command = [process.execPath, compilerPath, "compile", "--spec", specPath, "--intent", intent, "--args", argsJson]; + const proc = Bun.spawn(command, { cwd: process.cwd(), env: process.env, stdout: "pipe", stderr: "pipe" }); + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited + ]); + const compilerInvocation = clean({ + compiler: COMPILER_NAME, + mode: "subprocess", + entrypoint: "tools/hwpod-compiler-cli.ts", + argv: ["compile", "--spec", specPath, "--intent", intent, "--args", ""], + exitCode, + stderr: clipText(stderr) + }); + if (exitCode !== 0) { + throw cliError("hwpod_compiler_failed", "hwpod-compiler-cli failed to compile hwpod-node-ops plan", { + compilerInvocation, + stdout: clipText(stdout), + stderr: clipText(stderr) + }); + } + const payload = parseJsonObject(stdout, "compiler_stdout"); + const plan = isPlainObject(payload.plan) ? payload.plan : null; + if (payload.ok === false || !plan) { + throw cliError("hwpod_compiler_invalid_output", "hwpod-compiler-cli did not return a valid hwpod-node-ops plan", { + compilerInvocation, + payload: clean({ ok: payload.ok, action: payload.action, status: payload.status, error: payload.error }) + }); + } + return { + plan, + compilerInvocation: clean({ + ...compilerInvocation, + action: payload.action, + contractVersion: payload.contractVersion, + planId: plan.planId, + source: plan.source + }) + }; +} + function commandToIntent(parsed: ParsedArgs, stdinText?: string) { const command = parsed._[0]; if (command === "inspect") return { intent: "inspect", args: {} }; @@ -262,9 +291,9 @@ function commandToIntent(parsed: ParsedArgs, stdinText?: string) { if (subcommand === "apply-patch") return { intent: "workspace.apply-patch", args: { patch: text(parsed.patch ?? parsed.patchText ?? stdinText), patchBase64: text(parsed.patchBase64), reason: text(parsed.reason) } }; throw cliError("unsupported_workspace_command", `unsupported workspace command: ${subcommand}`); } - if (command === "build") return { intent: "debug.build", args: clean({ target: text(parsed.target), command: text(parsed.command) }) }; - if (command === "download") return { intent: "debug.download", args: clean({ artifact: text(parsed.artifact ?? parsed._[1]), target: text(parsed.target) }) }; - if (command === "reset") return { intent: "debug.reset", args: clean({ mode: text(parsed.mode ?? parsed._[1]) }) }; + if (command === "build") return { intent: "debug.build", args: clean({ target: text(parsed.target), command: text(parsed.command ?? parsed.commandLine), reason: text(parsed.reason) }) }; + if (command === "download") return { intent: "debug.download", args: clean({ artifact: text(parsed.artifact ?? parsed._[1]), target: text(parsed.target), command: text(parsed.command ?? parsed.commandLine), reason: text(parsed.reason) }) }; + if (command === "reset") return { intent: "debug.reset", args: clean({ mode: text(parsed.mode ?? parsed._[1]), command: text(parsed.command ?? parsed.commandLine), reason: text(parsed.reason) }) }; if (command === "uart") { const subcommand = parsed._[1] || "read"; if (subcommand === "read") return { intent: "io.uart.read", args: clean({ port: text(parsed.port ?? parsed._[2] ?? "uart1"), maxBytes: numberValue(parsed.maxBytes), since: text(parsed.since) }) }; @@ -290,9 +319,9 @@ function opsForIntent(intent: string, args: any, document: any) { if (intent === "workspace.cat") return [{ op: "workspace.cat", args: { ...common, path: requiredText(args.path, "path") } }]; if (intent === "workspace.rg") return [{ op: "workspace.rg", args: { ...common, pattern: requiredText(args.pattern, "pattern"), path: text(args.path) || ".", ignoreCase: args.ignoreCase === true } }]; if (intent === "workspace.apply-patch") return [{ op: "workspace.apply-patch", args: clean({ ...common, patch: text(args.patch), patchBase64: text(args.patchBase64), reason: text(args.reason) }) }]; - if (intent === "debug.build") return [{ op: "debug.build", args: clean({ ...common, target: text(args.target), command: text(args.command) }) }]; - if (intent === "debug.download") return [{ op: "debug.download", args: clean({ ...common, artifact: text(args.artifact), target: text(args.target) }) }]; - if (intent === "debug.reset") return [{ op: "debug.reset", args: clean({ ...common, mode: text(args.mode) }) }]; + if (intent === "debug.build") return [{ op: "debug.build", args: clean({ ...common, target: text(args.target), command: text(args.command) || text(document.spec.workspace.buildCommand), reason: text(args.reason) }) }]; + if (intent === "debug.download") return [{ op: "debug.download", args: clean({ ...common, artifact: text(args.artifact), target: text(args.target), command: text(args.command) || text(document.spec.debugProbe.downloadCommand), reason: text(args.reason) }) }]; + if (intent === "debug.reset") return [{ op: "debug.reset", args: clean({ ...common, mode: text(args.mode), command: text(args.command) || text(document.spec.debugProbe.resetCommand), reason: text(args.reason) }) }]; if (intent === "io.uart.read") return [{ op: "io.uart.read", args: clean({ ...common, port: text(args.port) || "uart1", maxBytes: numberValue(args.maxBytes), since: text(args.since) }) }]; if (intent === "io.uart.write") return [{ op: "io.uart.write", args: clean({ ...common, port: text(args.port) || "uart1", data: requiredText(args.data, "data") }) }]; if (intent === "io.uart.jsonrpc") return [{ op: "io.uart.jsonrpc", args: clean({ ...common, port: text(args.port) || "uart1", method: requiredText(args.method, "method"), params: objectValue(args.params) }) }]; @@ -579,6 +608,13 @@ function numberValue(value: unknown) { return Number.isInteger(parsed) ? parsed : undefined; } +function clipText(value: unknown, maxBytes = 4000) { + const textValue = typeof value === "string" ? value : String(value ?? ""); + const buffer = Buffer.from(textValue, "utf8"); + if (buffer.length <= maxBytes) return textValue; + return `${buffer.subarray(0, maxBytes).toString("utf8").replace(/\uFFFD$/u, "")}\n... clipped ...`; +} + function clean>(value: T): T { return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== "" && item !== null)) as T; } diff --git a/tools/src/hwpod-node-lib.ts b/tools/src/hwpod-node-lib.ts index cec842c2..959f6465 100644 --- a/tools/src/hwpod-node-lib.ts +++ b/tools/src/hwpod-node-lib.ts @@ -3,7 +3,7 @@ import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises" import os from "node:os"; import path from "node:path"; -import { HWPOD_NODE_OPS_CONTRACT_VERSION } from "./hwpod-harness-lib.ts"; +import { HWPOD_NODE_OPS_CONTRACT_VERSION } from "./hwpod-node-ops-contract.ts"; const NODE_VERSION = "0.1.0-thin-node-ops"; const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024; @@ -54,6 +54,9 @@ export function createHwpodNodeServer(options: any = {}) { serviceId: "hwpod-node", contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, version: NODE_VERSION, + nodeRole: "single-host-hwpod-node", + acceptedInput: "hwpod-node-ops", + specAuthority: "none", nodeId: process.env.HWPOD_NODE_ID || os.hostname(), observedAt: now() }); @@ -86,6 +89,9 @@ export async function executeHwpodNodeOpsPlan(plan: any, options: any = {}) { contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, serviceId: "hwpod-node", nodeVersion: NODE_VERSION, + nodeRole: "single-host-hwpod-node", + acceptedInput: "hwpod-node-ops", + specAuthority: "none", planId: plan.planId ?? null, hwpodId: plan.hwpodId ?? null, nodeId: plan.nodeId ?? process.env.HWPOD_NODE_ID ?? os.hostname(), @@ -107,7 +113,11 @@ async function executeOp(op: any, context: any) { if (name === "workspace.rg") return opOk(opId, name, await workspaceRg(args)); if (name === "workspace.apply-patch") return opOk(opId, name, await workspaceApplyPatch(args)); if (name === "cmd.run") return opOk(opId, name, await cmdRun(args)); - if (["debug.build", "debug.download", "debug.reset", "io.uart.read", "io.uart.write", "io.uart.jsonrpc"].includes(name)) { + if (["debug.build", "debug.download", "debug.reset"].includes(name)) { + const output = await debugCommand(name, args); + return output.ok ? opOk(opId, name, output) : opFailed(opId, name, output, "hwpod_node_command_failed", `${name} node-side command exited with ${output.exitCode}`); + } + if (["io.uart.read", "io.uart.write", "io.uart.jsonrpc"].includes(name)) { return opBlocked(opId, name, "hwpod_node_op_not_configured", `${name} requires node-side tool binding; the thin node contract is present but this local executor has no binding yet`); } return opBlocked(opId, name, "unsupported_hwpod_node_op", `unsupported hwpod-node op: ${name}`); @@ -155,6 +165,14 @@ async function workspaceApplyPatch(args: any) { return { cwd, changes: await applyPatchEnvelope(cwd, patch) }; } +async function debugCommand(op: string, args: any) { + const command = text(args.commandLine) || text(args.command); + if (!command) throw cliError("hwpod_node_op_not_configured", `${op} requires an explicit node-side command binding in the compiled hwpod-node-ops plan`); + const cwd = workspaceRoot(args); + const output = await spawnShellOutput(command, { cwd, timeoutMs: numberValue(args.timeoutMs) ?? 120000 }); + return { cwd, command, bindingSource: "hwpod-node-ops.command", ...output }; +} + async function applyPatchEnvelope(root: string, patch: string) { const lines = patch.replace(/\r\n?/gu, "\n").split("\n"); while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop(); @@ -251,6 +269,12 @@ async function cmdRun(args: any) { return { cwd, command: [command, ...argv], ...output }; } +async function spawnShellOutput(command: string, { cwd, timeoutMs }: any) { + const shellCommand = process.platform === "win32" ? ["cmd.exe", "/d", "/s", "/c", command] : ["sh", "-lc", command]; + const output = await spawnOutput(shellCommand, { cwd, timeoutMs }); + return { shell: shellCommand.slice(0, -1), exitCode: output.exitCode, stdout: output.stdout, stderr: output.stderr, ok: output.ok }; +} + async function spawnOutput(command: string[], { cwd, stdinText = "", timeoutMs }: any) { const proc = Bun.spawn(command, { cwd, stdin: stdinText ? "pipe" : "ignore", stdout: "pipe", stderr: "pipe" }); if (stdinText && proc.stdin) { @@ -293,7 +317,7 @@ function help() { status: "succeeded", contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, version: NODE_VERSION, - role: "Thin node-side executor for stable hwpod-node-ops.", + role: "Single host-side executor for stable hwpod-node-ops; hwpod-spec, hwpod-cli, hwpod-ctl, and hwpod-compiler stay in the Code Agent workspace.", usage: [ "bun tools/hwpod-node.ts run --plan-json '{...}'", "bun tools/hwpod-node.ts serve --host 127.0.0.1 --port 19678" @@ -313,6 +337,10 @@ function opBlocked(opId: string, op: string, code: string, summary: string) { return { opId, op, ok: false, status: "blocked", blocker: { code, layer: "hwpod-node", retryable: true, summary } }; } +function opFailed(opId: string, op: string, output: any, code: string, summary: string) { + return { opId, op, ok: false, status: "failed", output, blocker: { code, layer: "hwpod-node", retryable: false, summary } }; +} + function failure(action: string, error: any) { return { ok: false, action, status: "failed", error: { code: error?.code || "hwpod_node_error", message: error?.message || String(error), details: error?.details || undefined } }; } diff --git a/tools/src/hwpod-node-ops-contract.ts b/tools/src/hwpod-node-ops-contract.ts new file mode 100644 index 00000000..797b9ffe --- /dev/null +++ b/tools/src/hwpod-node-ops-contract.ts @@ -0,0 +1,19 @@ +export const HWPOD_NODE_OPS_CONTRACT_VERSION = "hwpod-node-ops-v1"; + +export const HWPOD_NODE_OPS = new Set([ + "node.health", + "node.version", + "node.inventory", + "workspace.ls", + "workspace.cat", + "workspace.rg", + "workspace.apply-patch", + "debug.build", + "debug.download", + "debug.reset", + "io.uart.read", + "io.uart.write", + "io.uart.jsonrpc", + "cmd.run" +]); +