Files
pikasTech-HWLAB/cmd/hwlab-device-pod/main.ts
T
Codex eb5bada0d1 fix(v0.2): device-pod output.text evidence read-only selectors for #773
Tracks pikasTech/HWLAB#773. PR #765 fixed selector confusion but did
not touch cloud-api evidence propagation. This change closes the
real root cause and adds the read-only evidence selectors that #760
follow-up called for.

cloud-api (internal/cloud/access-control.ts):
- DEVICE_JOB_INTENTS adds workspace.evidence and debug.evidence.
- DEVICE_JOB_READ_ONLY_SUB_ACTIONS = { status, output, wait, cancel,
  evidence } and DEVICE_JOB_ACTIONABLE_INTENTS = { workspace.build,
  debug.download } make the mutating-intent / sub-action matrix
  explicit.
- _deviceJobRequiresReason(intent, args, reason) returns false when
  the caller already provided reason OR when the actionable mutating
  intent is paired with a read-only sub-action. debug.reset and other
  non-actionable mutating intents still always require reason.
- executorOutputPayload now also surfaces output.summary,
  nestedOutput.summary, evidence.text, evidence.logTail and
  evidence.summary as body.output.text, so a dispatcher that
  includes the host logTail / buildSummary in its result becomes
  visible to the Code Agent without a separate bootsharp dance.

device-pod executor (cmd/hwlab-device-pod/main.ts):
- gatewayDispatchText also walks result.evidence.{text,logTail,
  summary}, dispatch.message (only when dispatchStatus=completed),
  dispatch.summary, result.summary and dispatch.buildSummary before
  falling back to JSON.stringify.
- deviceHostArgs maps workspace.evidence / debug.evidence to the
  host device-host-cli evidence subcommands.

device-host-cli (skills/device-pod-cli/assets/device-host-cli.mjs):
- new readJobEvidence(kindPrefix, requestedId, { tail, full,
  target }) reads the most recent (or specified) keil-build /
  keil-download job log file and returns it as keil-build.evidence /
  keil-download.evidence. tail defaults to 200, --full for entire
  log. Missing job returns ok=false but does not throw.
- workspace build evidence and debug-probe download evidence now
  wired in main() and help text.

device-pod-cli (tools/src/device-pod-cli-lib.ts):
- selectorCheatSheet adds the evidence row and clarifies that
  build/download status/output/wait/cancel/evidence are read-only
  and do NOT need --reason. build/download start still require
  --reason. usage examples now include the two evidence selectors.
- failed-fast selectors are unchanged; existing 26 tests still pass.

SKILL.md (skills/device-pod-cli/SKILL.md):
- Selector Cheat Sheet adds workspace.evidence and
  debug.evidence rows.
- #773 follow-up note: --reason is now required only for
  mutating sub-actions, not for the read-only ones.

cloud-api tests (internal/cloud/access-control.test.ts):
- workspace.evidence and debug.evidence must be in DEVICE_JOB_INTENTS.
- _deviceJobRequiresReason signature and the read-only / actionable
  branch table.
- executorOutputPayload must look at evidence and summary fields
  in addition to text. 3 new tests; pre-existing 4 workbench
  failures are unrelated to this change and reproduce on the
  unchanged v0.2 base.

Verification (host-side, G14 /root/hwlab-v02, source=0cf9a8c6):
- bun test tools/device-pod-cli.test.ts → 26/26 pass.
- bun test internal/cloud/access-control.test.ts → 23 pass,
  4 pre-existing workbench failures (unchanged on v0.2 base).
- bun test cmd/hwlab-device-pod/main.test.ts → 7/7 pass.
- node --check skills/device-pod-cli/assets/device-host-cli.mjs
  → syntax OK.

Hot probe: live v0.2 cloud-api at 74.48.78.17:19667 confirmed
job_devicepod_804c5db4... (workspace.build) returned
text="", bytes=0, output={} before this change. After the
executor text-extraction update and the new evidence selector,
a follow-up workspace.build start + build evidence will surface
the host logTail / buildSummary in body.output.text without
requiring bootsharp + host file fallback.

Tracked-by: pikasTech/HWLAB#773
2026-06-03 21:57:39 +08:00

794 lines
28 KiB
JavaScript

#!/usr/bin/env node
import { createServer } from "node:http";
import { randomUUID } from "node:crypto";
import { runtimeIdentityFromEnv } from "../../internal/build-metadata.mjs";
import { jsonResponse, listen, parsePort, readJson } from "../../internal/sim/http.mjs";
const SERVICE_ID = "hwlab-device-pod";
const CONTRACT_VERSION = "device-pod-executor-v1";
const DEFAULT_DEVICE_POD_ID = "device-pod-71-freq";
const port = parsePort(process.env.HWLAB_DEVICE_POD_PORT, parsePort(process.env.PORT, 7601));
const devicePodId = process.env.HWLAB_DEVICE_POD_ID || DEFAULT_DEVICE_POD_ID;
const devicePodIds = configuredDevicePodIds(process.env.HWLAB_DEVICE_POD_IDS, devicePodId);
const environment = process.env.HWLAB_ENVIRONMENT || process.env.HWLAB_GITOPS_PROFILE || "dev";
const cloudApiInternalUrl = normalizeBaseUrl(process.env.HWLAB_CLOUD_API_INTERNAL_URL || process.env.HWLAB_CLOUD_API_URL);
const internalToken = textOr(process.env.HWLAB_DEVICE_POD_INTERNAL_TOKEN, "");
const dispatchTimeoutMs = numberOr(process.env.HWLAB_DEVICE_POD_GATEWAY_DISPATCH_TIMEOUT_MS, 120000);
const DEVICE_JOB_OUTPUT_MAX_BYTES = 12000;
const jobs = new Map();
listen(createServer(async (request, response) => {
try {
const url = new URL(request.url ?? "/", "http://localhost");
if (request.method === "GET" && (url.pathname === "/health" || url.pathname === "/health/live")) {
jsonResponse(response, 200, healthPayload());
return;
}
if (url.pathname === "/health" || url.pathname === "/health/live") {
jsonResponse(response, 405, { error: "method_not_allowed", method: request.method ?? "UNKNOWN", path: url.pathname });
return;
}
if (request.method === "GET" && url.pathname === "/v1/device-pods") {
jsonResponse(response, 200, listPayload());
return;
}
const route = parseDevicePodPath(url.pathname);
if (!route) {
jsonResponse(response, 404, { error: "not_found", path: url.pathname });
return;
}
if (request.method === "GET" && route.subpath === "status") {
jsonResponse(response, 200, statusPayload(route.devicePodId));
return;
}
if (request.method === "GET" && route.subpath === "events") {
jsonResponse(response, 200, eventsPayload(route.devicePodId, url.searchParams));
return;
}
if (request.method === "POST" && route.subpath === "jobs") {
await handleInternalJob(request, response, route.devicePodId);
return;
}
if (request.method === "GET" && route.subpath.startsWith("jobs/")) {
handleGetInternalJob(request, response, route.devicePodId, route.subpath);
return;
}
if (request.method === "POST" && route.subpath.startsWith("jobs/") && route.subpath.endsWith("/cancel")) {
handleCancelInternalJob(request, response, route.devicePodId, route.subpath);
return;
}
jsonResponse(response, 405, { error: "method_not_allowed", method: request.method ?? "UNKNOWN", path: url.pathname });
} catch (error) {
jsonResponse(response, 500, { error: "internal_error", message: error?.message ?? String(error), valuesRedacted: true });
}
}), port);
function healthPayload() {
return {
serviceId: SERVICE_ID,
environment,
status: "live",
ready: true,
contractVersion: CONTRACT_VERSION,
devicePodId,
devicePodIds,
observedAt: new Date().toISOString(),
role: "device-pod-internal-executor",
authority: "hwlab-cloud-api",
acceptsUserAuthority: false,
fake: false,
gatewayAdapter: gatewayAdapterState(),
runtime: runtimeIdentityFromEnv(process.env),
source: sourcePayload(),
note: "Profile, grant, lease, and user-facing job authority live in hwlab-cloud-api; this service only exposes the internal executor boundary."
};
}
function listPayload() {
return {
serviceId: SERVICE_ID,
contractVersion: CONTRACT_VERSION,
status: "ok",
source: sourcePayload(),
selectedDevicePodId: devicePodId,
devicePods: devicePodIds.map((id) => executorDevicePodSummary(id))
};
}
function executorDevicePodSummary(id) {
return {
devicePodId: id,
status: "executor-ready",
authority: "hwlab-cloud-api",
fake: false,
gatewayAdapter: gatewayAdapterState(),
routes: {
cloudAuthority: "/v1/device-pods",
internalJob: `/v1/device-pods/${encodeURIComponent(id)}/jobs`
}
};
}
function statusPayload(id) {
const blocker = gatewayAdapterBlocker("device-host-cli dispatch is owned by hwlab-cloud-api gateway routing");
const observedAt = new Date().toISOString();
const output = boundedOutput({ text: blocker.summary, summary: blocker.summary });
const freshnessPayload = freshness(observedAt, blocker);
return {
serviceId: SERVICE_ID,
contractVersion: CONTRACT_VERSION,
status: "blocked",
devicePodId: id,
targetId: null,
profileHash: "",
traceId: `trc_devicepod_${randomUUID()}`,
operationId: `op_devicepod_${randomUUID()}`,
observedAt,
source: sourcePayload(),
blocker,
freshness: freshnessPayload,
output,
text: output.text,
bytes: output.bytes,
truncation: output.truncation,
summary: {
devicePodId: id,
targetId: null,
profileHash: "",
status: "blocked",
freshness: freshnessPayload,
blocker
}
};
}
function eventsPayload(id, searchParams) {
const limit = Math.min(Math.max(Number.parseInt(searchParams.get("limit") ?? "80", 10) || 80, 1), 1000);
const event = {
eventId: `evt_${id}_executor_boundary`,
devicePodId: id,
ts: new Date().toISOString(),
level: "warn",
scope: "executor",
status: "blocked",
summary: "No standalone fake device events are emitted by hwlab-device-pod; use hwlab-cloud-api job authority.",
blocker: gatewayAdapterBlocker("standalone device-pod executor has no gateway session")
};
return {
serviceId: SERVICE_ID,
contractVersion: CONTRACT_VERSION,
status: "ok",
devicePodId: id,
source: sourcePayload(),
events: limit > 0 ? [event] : [],
truncation: { limit, returned: limit > 0 ? 1 : 0, truncated: false }
};
}
async function handleInternalJob(request, response, id) {
if (!isInternalCaller(request)) {
jsonResponse(response, 403, {
accepted: false,
status: "blocked",
error: {
code: "cloud_api_authority_required",
message: "Device Pod jobs must be authorized by hwlab-cloud-api before reaching the internal executor."
},
source: sourcePayload()
});
return;
}
const body = await readJson(request);
const traceId = typeof body.traceId === "string" && body.traceId.startsWith("trc_") ? body.traceId : `trc_devicepod_${randomUUID()}`;
const operationId = typeof body.operationId === "string" ? body.operationId : `op_devicepod_${randomUUID()}`;
const now = new Date().toISOString();
const profile = normalizeObject(body.profile);
const route = normalizeObject(profile.route);
const jobId = typeof body.jobId === "string" && body.jobId.startsWith("job_") ? body.jobId : `job_devicepod_${randomUUID()}`;
const command = deviceHostCommand(profile, {
id: jobId,
devicePodId: id,
intent: typeof body.intent === "string" ? body.intent : "unknown",
args: normalizeObject(body.args)
});
const dispatchBlocker = gatewayDispatchPreflightBlocker(route, command);
const job = {
id: jobId,
devicePodId: id,
status: dispatchBlocker ? "blocked" : "running",
intent: typeof body.intent === "string" ? body.intent : "unknown",
reason: typeof body.reason === "string" ? body.reason : "",
traceId,
operationId,
createdAt: now,
updatedAt: now,
completedAt: dispatchBlocker ? now : null,
targetId: typeof body.targetId === "string" ? body.targetId : targetIdFromProfile(profile),
profileHash: typeof body.profileHash === "string" ? body.profileHash : "",
ownerUserId: typeof body.ownerUserId === "string" ? body.ownerUserId : "",
command,
output: dispatchBlocker ? boundedOutput({ text: "", summary: dispatchBlocker.summary }) : boundedOutput({ text: "", summary: "gateway/device-host-cli dispatch queued" }),
blocker: dispatchBlocker,
source: sourcePayload()
};
jobs.set(jobKey(id, job.id), job);
if (!dispatchBlocker) dispatchGatewayJob({ job, route });
jsonResponse(response, dispatchBlocker ? 409 : 202, jobPayload(job, { accepted: !dispatchBlocker }));
}
function handleGetInternalJob(request, response, id, subpath) {
if (!isInternalCaller(request)) return rejectExternalJobRoute(response);
const parts = subpath.split("/");
const job = jobs.get(jobKey(id, decodeURIComponent(parts[1] ?? "")));
if (!job) {
jsonResponse(response, 404, { error: { code: "device_job_not_found", message: "Device Pod executor job was not found" }, source: sourcePayload() });
return;
}
if (parts[2] === "output") {
jsonResponse(response, 200, jobOutputPayload(job));
return;
}
jsonResponse(response, 200, jobPayload(job));
}
function handleCancelInternalJob(request, response, id, subpath) {
if (!isInternalCaller(request)) return rejectExternalJobRoute(response);
const jobId = decodeURIComponent(subpath.split("/")[1] ?? "");
const job = jobs.get(jobKey(id, jobId));
if (!job) {
jsonResponse(response, 404, { error: { code: "device_job_not_found", message: "Device Pod executor job was not found" }, source: sourcePayload() });
return;
}
if (terminalJobStatus(job.status)) {
jsonResponse(response, 200, jobPayload(job));
return;
}
const now = new Date().toISOString();
const canceled = { ...job, status: "canceled", updatedAt: now, completedAt: now, blocker: { code: "device_job_canceled", layer: "device-pod", retryable: false, summary: "Device Pod executor job was canceled by cloud-api" } };
jobs.set(jobKey(id, jobId), canceled);
jsonResponse(response, 200, jobPayload(canceled));
}
async function dispatchGatewayJob({ job, route }) {
const target = `${cloudApiInternalUrl}/v1/internal/device-pod/gateway-dispatch`;
const requestId = `req_${job.id}`;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), dispatchTimeoutMs + 10000);
try {
const response = await fetch(target, {
method: "POST",
signal: controller.signal,
headers: {
accept: "application/json",
"content-type": "application/json",
"x-hwlab-internal-service": SERVICE_ID,
"x-hwlab-internal-token": internalToken,
"x-trace-id": job.traceId,
"x-request-id": requestId
},
body: JSON.stringify({
id: requestId,
actorId: job.ownerUserId || "svc_hwlab-device-pod",
params: {
projectId: "prj_v02_device_pod",
gatewaySessionId: route.gatewaySessionId,
resourceId: route.resourceId ?? "res_device_pod",
capabilityId: route.capabilityId ?? "cap_device_host_cli",
operationId: job.operationId,
traceId: job.traceId,
input: {
command: job.command,
cwd: route.hostWorkspaceRoot,
timeoutMs: dispatchTimeoutMs
}
}
})
});
const payload = await response.json().catch(() => ({}));
const current = jobs.get(jobKey(job.devicePodId, job.id));
if (current && terminalJobStatus(current.status)) return;
const next = jobFromGatewayDispatch(job, payload, response.status);
jobs.set(jobKey(job.devicePodId, job.id), next);
} catch (error) {
const current = jobs.get(jobKey(job.devicePodId, job.id));
if (current && terminalJobStatus(current.status)) return;
const now = new Date().toISOString();
jobs.set(jobKey(job.devicePodId, job.id), {
...job,
status: "failed",
updatedAt: now,
completedAt: now,
output: boundedOutput({ text: "", error: error?.message ?? "gateway dispatch failed" }),
blocker: gatewayDispatchFailedBlocker(error?.message ?? "gateway dispatch failed")
});
} finally {
clearTimeout(timer);
}
}
function jobPayload(job, { accepted = !["blocked", "failed"].includes(job.status) } = {}) {
return {
accepted,
status: job.status,
contractVersion: CONTRACT_VERSION,
devicePodId: job.devicePodId,
targetId: job.targetId ?? null,
profileHash: job.profileHash ?? "",
traceId: job.traceId,
operationId: job.operationId,
job: publicJob(job),
blocker: job.blocker,
freshness: freshness(job.updatedAt, job.blocker),
outputUrl: `/v1/device-pods/${encodeURIComponent(job.devicePodId)}/jobs/${encodeURIComponent(job.id)}/output`,
cancelUrl: `/v1/device-pods/${encodeURIComponent(job.devicePodId)}/jobs/${encodeURIComponent(job.id)}/cancel`,
source: job.source ?? sourcePayload()
};
}
function rejectExternalJobRoute(response) {
jsonResponse(response, 403, {
accepted: false,
status: "blocked",
error: {
code: "cloud_api_authority_required",
message: "Device Pod jobs must be authorized by hwlab-cloud-api before reaching the internal executor."
},
source: sourcePayload()
});
}
function parseDevicePodPath(pathname) {
const prefix = "/v1/device-pods/";
if (!pathname.startsWith(prefix)) return null;
const [id, ...rest] = pathname.slice(prefix.length).split("/").filter(Boolean).map((part) => decodeURIComponent(part));
return id ? { devicePodId: id, subpath: rest.join("/") } : null;
}
function isInternalCaller(request) {
if (!internalToken) return false;
const serviceHeader = request.headers["x-hwlab-internal-service"];
const tokenHeader = request.headers["x-hwlab-internal-token"];
return String(Array.isArray(serviceHeader) ? serviceHeader[0] : serviceHeader ?? "").trim() === "hwlab-cloud-api"
&& String(Array.isArray(tokenHeader) ? tokenHeader[0] : tokenHeader ?? "").trim() === internalToken;
}
function sourcePayload() {
return {
kind: "INTERNAL_EXECUTOR",
serviceId: SERVICE_ID,
authority: "hwlab-cloud-api",
fake: false,
devLiveEvidence: false,
runtime: runtimeIdentityFromEnv(process.env)
};
}
function gatewayAdapterBlocker(summary) {
return { code: "gateway_dispatch_unavailable", layer: "device-pod", retryable: true, summary, userMessage: "Device Pod executor boundary is present, but gateway/device-host-cli dispatch is not connected here." };
}
function gatewayDispatchFailedBlocker(summary) {
return { code: "gateway_dispatch_failed", layer: "device-pod", retryable: true, summary: String(summary || "gateway/device-host-cli dispatch failed"), userMessage: "Device Pod gateway/device-host-cli dispatch failed." };
}
function gatewayDispatchPreflightBlocker(route, command) {
if (!cloudApiInternalUrl) return gatewayAdapterBlocker("HWLAB_CLOUD_API_INTERNAL_URL is not configured for gateway dispatch");
if (!route.gatewaySessionId) return gatewayAdapterBlocker("profile route.gatewaySessionId is missing");
if (!command) return gatewayAdapterBlocker("device-host-cli command mapping is not available for this intent");
return null;
}
function gatewayAdapterState() {
return { mode: cloudApiInternalUrl ? "cloud-api-internal-dispatch" : "blocked", cloudApiInternalUrlConfigured: Boolean(cloudApiInternalUrl) };
}
function jobFromGatewayDispatch(job, payload, httpStatus) {
const expectedId = `req_${job.id}`;
if (payload && Object.hasOwn(payload, "id") && String(payload.id) !== expectedId) {
const blocker = gatewayDispatchFailedBlocker(`gateway JSON-RPC id mismatch: expected ${expectedId}, got ${String(payload.id)}`);
const now = new Date().toISOString();
return { ...job, status: "failed", updatedAt: now, completedAt: now, output: boundedOutput({ text: "", httpStatus, summary: blocker.summary }), blocker };
}
const result = normalizeObject(payload.result);
const dispatch = normalizeObject(result.dispatch);
const dispatchText = gatewayDispatchText(result, dispatch);
const blocker = payload.blocker ?? result.blocker ?? (payload.error ? gatewayDispatchFailedBlocker(payload.error.message ?? "gateway dispatch failed") : null);
const completed = !blocker && httpStatus < 400 && (result.status === "completed" || result.status === "succeeded" || dispatch.dispatchStatus === "succeeded" || dispatch.dispatchStatus === "completed");
const status = completed ? "completed" : blocker?.code === "gateway_dispatch_unavailable" ? "blocked" : "failed";
const failureSummary = blocker?.summary || dispatch.message || dispatch.stderr || dispatchText || result.status || "gateway/device-host-cli dispatch failed";
const now = new Date().toISOString();
return {
...job,
status,
updatedAt: now,
completedAt: now,
output: boundedOutput({
text: dispatchText,
httpStatus,
dispatch: gatewayDispatchSummary(result, dispatch),
gateway: result.gateway ?? null,
auditId: result.auditId ?? null,
evidenceId: result.evidenceId ?? null,
summary: completed ? "gateway/device-host-cli dispatch completed" : failureSummary
}),
blocker: completed ? null : (blocker?.summary ? blocker : gatewayDispatchFailedBlocker(failureSummary))
};
}
function gatewayDispatchText(result, dispatch) {
if (typeof result.stdout === "string" && result.stdout) return result.stdout;
if (typeof result.stderr === "string" && result.stderr) return result.stderr;
if (typeof dispatch.stdout === "string" && dispatch.stdout) return dispatch.stdout;
if (typeof dispatch.stderr === "string" && dispatch.stderr) return dispatch.stderr;
const evidence = normalizeObject(result.evidence ?? dispatch.evidence);
if (typeof evidence.text === "string" && evidence.text) return evidence.text;
if (typeof evidence.logTail === "string" && evidence.logTail) return evidence.logTail;
if (typeof evidence.summary === "string" && evidence.summary) return evidence.summary;
if (typeof dispatch.message === "string" && dispatch.message && dispatch.dispatchStatus === "completed") return dispatch.message;
if (typeof dispatch.summary === "string" && dispatch.summary) return dispatch.summary;
if (typeof result.summary === "string" && result.summary) return result.summary;
if (typeof dispatch.buildSummary === "string" && dispatch.buildSummary) return dispatch.buildSummary;
if (typeof result.text === "string") return result.text;
if (result && Object.keys(result).length > 0) return JSON.stringify(result);
return "";
}
function gatewayDispatchSummary(result, dispatch) {
const value = (key) => dispatch[key] ?? result[key];
return pruneUndefined({
shellExecuted: value("shellExecuted"),
dispatchStatus: value("dispatchStatus"),
status: value("status"),
exitCode: value("exitCode"),
signal: value("signal"),
timedOut: value("timedOut"),
cwd: value("cwd"),
command: value("command"),
stdoutBytes: textBytes(value("stdout")),
stderrBytes: textBytes(value("stderr")),
stdoutTruncated: value("stdoutTruncated"),
stderrTruncated: value("stderrTruncated"),
durationMs: value("durationMs")
});
}
function deviceHostCommand(profile, job) {
const args = deviceHostArgs(job.intent, job.args);
if (!args) return null;
const payload = {
devicePodId: profile.devicePodId || job.devicePodId,
profile,
hostCli: profile.route?.hostCli || "node tools/device-host-cli.mjs",
args
};
const profileJsonB64 = Buffer.from(JSON.stringify(payload.profile), "utf8").toString("base64");
return shellJoin([...splitCommand(payload.hostCli), "--profile-json-b64", profileJsonB64, "--pod-id", payload.devicePodId, ...payload.args]);
}
function splitCommand(value) {
const out = [];
let quote = null;
let buffer = "";
for (const char of String(value || "")) {
if (quote) {
if (char === quote) quote = null;
else buffer += char;
} else if (char === "'" || char === '"') {
quote = char;
} else if (/\s/u.test(char)) {
if (buffer) {
out.push(buffer);
buffer = "";
}
} else {
buffer += char;
}
}
if (buffer) out.push(buffer);
return out.length ? out : ["node", "tools/device-host-cli.mjs"];
}
function shellJoin(parts) {
return parts.map(shellQuote).join(" ");
}
function shellQuote(value) {
const text = String(value ?? "");
if (/^[A-Za-z0-9_./:=@\\-]+$/u.test(text)) return text;
return `"${text.replace(/(["\\])/gu, "\\$1")}"`;
}
function deviceHostArgs(intent, args = {}) {
if (intent === "workspace.bootsharp") return ["workspace", "bootsharp", textOr(args.path, "."), ...hostOptionArgs(args, ["depth", "limit", "agentsLimit"])];
if (intent === "workspace.ls") return ["workspace", "ls", textOr(args.path, ".")];
if (intent === "workspace.cat") {
const path = textOr(args.path, "");
return path ? ["workspace", "cat", path, "--limit", String(numberOr(args.limit ?? args.maxBytes, 40000))] : null;
}
if (intent === "workspace.rg") {
const pattern = textOr(args.pattern ?? args.query, "");
return pattern ? ["workspace", "rg", pattern, textOr(args.path, "."), ...hostOptionArgs(args, ["glob", "g", "filesWithMatches", "l", "ignoreCase", "i", "maxCount"])] : null;
}
if (intent === "workspace.put") {
const path = textOr(args.path, "");
return path
? ["workspace", "put", path, ...hostOptionArgs(args, [
"contentB64",
"textB64",
"text",
"encoding",
"charset",
"createOnly",
"createDirs",
"updateOnly"
])]
: null;
}
if (intent === "workspace.rm") {
const path = textOr(args.path, "");
return path ? ["workspace", "rm", path, ...hostOptionArgs(args, ["missingOk"])] : null;
}
if (intent === "workspace.rmdir") {
const path = textOr(args.path, "");
return path ? ["workspace", "rmdir", path] : null;
}
if (intent === "workspace.keil") {
const action = textOr(args.action, "");
return action
? [
"workspace",
"keil",
action,
...optionalValue(args.path),
...hostOptionArgs(args, ["base", "group", "groupName", "target", "timeoutMs"])
]
: null;
}
if (intent === "workspace.apply-patch") return ["workspace", "apply-patch", textOr(args.base, "."), "--patch-b64", patchBase64(args)];
if (intent === "workspace.build") {
return [
"workspace",
"build",
textOr(args.action, "start"),
...jobIdArgs(args),
...hostOptionArgs(args, ["target", "timeoutMs", "clean", "dryRun"]),
];
}
if (intent === "debug.status") return ["debug-probe", "status"];
if (intent === "debug.chip-id") return ["debug-probe", "chip-id"];
if (intent === "debug.download") {
return [
"debug-probe",
"download",
textOr(args.action, "start"),
...jobIdArgs(args),
...hostOptionArgs(args, [
"target",
"timeoutMs",
"captureUart",
"captureDurationMs",
"durationMs",
"port",
"baudRate"
])
];
}
if (intent === "debug.reset") return ["debug-probe", "reset"];
if (intent === "workspace.evidence") {
return [
"workspace",
"evidence",
textOr(args.kind, "build"),
...jobIdArgs(args),
...hostOptionArgs(args, ["tail", "full", "path", "target"])
];
}
if (intent === "debug.evidence") {
return [
"debug-probe",
"evidence",
textOr(args.kind, "download"),
...jobIdArgs(args),
...hostOptionArgs(args, ["tail", "full", "path", "target"])
];
}
if (intent === "io.ports") return ["io-probe", textOr(args.uartId, "uart/1"), "ports"];
if (intent === "io.uart.read") {
return [
"io-probe",
textOr(args.uartId, "uart/1"),
"read",
"--duration-ms",
String(numberOr(args.durationMs ?? args["duration-ms"], 1000)),
...hostOptionArgs(args, ["port", "baudRate"])
];
}
if (intent === "io.uart.read-after-launch-flash") {
return [
"io-probe",
textOr(args.uartId, "uart/1"),
"read-after-launch-flash",
...hostOptionArgs(args, uartLaunchFlashOptionKeys)
];
}
if (intent === "io.uart.write") {
return [
"io-probe",
textOr(args.uartId, "uart/1"),
"write",
...(args.hex ? ["--hex"] : []),
...hostOptionArgs(args, ["port", "baudRate"]),
textOr(args.message ?? args.text ?? args.data, "")
];
}
if (intent === "io.uart.jsonrpc") {
return [
"io-probe",
textOr(args.uartId, "uart/1"),
"jsonrpc",
...optionalValue(args.method),
...hostOptionArgs(args, uartJsonRpcOptionKeys)
];
}
return null;
}
const uartLaunchFlashOptionKeys = [
"durationMs",
"port",
"baudRate",
"flashBase",
"skipHardwareReset",
"connectMode",
"timeoutMs"
];
const uartJsonRpcOptionKeys = [
"id",
"request",
"requestB64",
"params",
"paramsB64",
"responseTimeoutMs",
"durationMs",
"retry",
"retries",
"retryDelayMs",
"lineEnding",
"noNewline",
"lineDelimited",
"discardBefore",
"requireResponse",
"requireJson",
"requireJsonrpc",
"requireJsonrpcResult",
"allowIdMismatch",
"expectResultField",
"port",
"baudRate"
];
function optionalValue(value) {
const result = textOr(value, "");
return result ? [result] : [];
}
function jobIdArgs(args = {}) {
const jobId = textOr(args.jobId, "");
return jobId ? [jobId] : [];
}
function hostOptionArgs(args = {}, keys = []) {
const out = [];
for (const key of keys) {
const value = args[key];
if (value === undefined || value === null || value === "" || value === false) continue;
const name = `--${key.replace(/[A-Z]/gu, (match) => `-${match.toLowerCase()}`)}`;
const values = Array.isArray(value) ? value : [value];
for (const item of values) {
if (item === undefined || item === null || item === "" || item === false) continue;
if (item === true) out.push(name);
else out.push(name, String(item));
}
}
return out;
}
function patchBase64(args = {}) {
if (typeof args.patchB64 === "string") return args.patchB64;
if (typeof args["patch-b64"] === "string") return args["patch-b64"];
return Buffer.from(String(args.patch ?? ""), "utf8").toString("base64");
}
function targetIdFromProfile(profile = {}) {
return profile.target?.id ?? profile.targetId ?? null;
}
function normalizeObject(value) {
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
}
function textOr(value, fallback) {
return typeof value === "string" && value.trim() ? value : fallback;
}
function numberOr(value, fallback) {
const parsed = Number.parseInt(String(value ?? ""), 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}
function normalizeBaseUrl(value) {
const text = String(value ?? "").trim();
return text ? text.replace(/\/+$/u, "") : "";
}
function configuredDevicePodIds(value, fallbackId) {
const ids = String(value ?? "")
.split(",")
.map((item) => item.trim())
.filter(Boolean);
const unique = [];
for (const id of [fallbackId, ...ids]) {
if (!unique.includes(id)) unique.push(id);
}
return unique;
}
function jobOutputPayload(job) {
return {
...jobPayload(job),
output: job.output,
text: job.output.text,
bytes: job.output.bytes,
truncation: job.output.truncation
};
}
function publicJob(job) {
return {
id: job.id,
devicePodId: job.devicePodId,
status: job.status,
intent: job.intent,
reason: job.reason,
traceId: job.traceId,
operationId: job.operationId,
createdAt: job.createdAt,
updatedAt: job.updatedAt,
completedAt: job.completedAt
};
}
function boundedOutput(output, maxBytes = DEVICE_JOB_OUTPUT_MAX_BYTES) {
const text = String(output?.text ?? "");
const buffer = Buffer.from(text, "utf8");
const clipped = buffer.length > maxBytes;
const boundedText = clipped ? buffer.subarray(0, maxBytes).toString("utf8") : text;
const bounded = { ...output, text: boundedText };
if (clipped) {
delete bounded.dispatch;
bounded.omitted = { reason: "device_job_output_truncated", originalBytes: buffer.length };
}
return { ...bounded, bytes: Math.min(buffer.length, maxBytes), truncation: { maxBytes, truncated: clipped, originalBytes: buffer.length } };
}
function pruneUndefined(value) {
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
}
function textBytes(value) {
return typeof value === "string" ? Buffer.byteLength(value, "utf8") : undefined;
}
function freshness(observedAt, blocker) {
return { observedAt, ageMs: 0, stale: Boolean(blocker), source: blocker ? "blocked" : "device-pod-executor" };
}
function terminalJobStatus(status) {
return ["completed", "failed", "blocked", "canceled"].includes(status);
}
function jobKey(id, jobId) {
return `${id}\u0000${jobId}`;
}