diff --git a/scripts/g14-gitops-render.mjs b/scripts/g14-gitops-render.mjs index 680565b0..065f1599 100644 --- a/scripts/g14-gitops-render.mjs +++ b/scripts/g14-gitops-render.mjs @@ -1,6 +1,7 @@ #!/usr/bin/env node import assert from "node:assert/strict"; import { execFile } from "node:child_process"; +import { createHash } from "node:crypto"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import { promisify } from "node:util"; @@ -1418,8 +1419,193 @@ function deepSeekProxyManifest({ profile = "dev" } = {}) { }; } -function runtimeKustomization({ profile = "dev" } = {}) { +function deviceAgent71FreqServerScript() { + return [ + "import http from \"node:http\";", + "", + "const deviceId = process.env.DEVICE_ID || \"71-freq\";", + "const port = Number(process.env.PORT || 7601);", + "const cloudApiUrl = (process.env.HWLAB_CLOUD_API_URL || \"http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667\").replace(/\\/+$/u, \"\");", + "const gatewaySessionId = process.env.HWLAB_GATEWAY_SESSION_ID || \"gws_d601_win_71_freq\";", + "const resourceId = process.env.HWLAB_GATEWAY_RESOURCE_ID || \"res_d601_windows_host\";", + "const capabilityId = process.env.HWLAB_GATEWAY_CAPABILITY_ID || \"cap_d601_windows_cmd_exec\";", + "const workspaceRoot = process.env.DEVICE_WORKSPACE_ROOT || \"F:\\\\Work\\\\ConStart\";", + "", + "function sendJson(res, statusCode, payload) {", + " const body = JSON.stringify(payload, null, 2);", + " res.writeHead(statusCode, { \"content-type\": \"application/json; charset=utf-8\" });", + " res.end(body);", + "}", + "", + "function readBody(req) {", + " return new Promise((resolve, reject) => {", + " let body = \"\";", + " req.setEncoding(\"utf8\");", + " req.on(\"data\", (chunk) => {", + " body += chunk;", + " if (body.length > 1024 * 1024) req.destroy(new Error(\"request body too large\"));", + " });", + " req.on(\"end\", () => resolve(body ? JSON.parse(body) : {}));", + " req.on(\"error\", reject);", + " });", + "}", + "", + "function postJson(url, payload, timeoutMs) {", + " return new Promise((resolve, reject) => {", + " const parsed = new URL(url);", + " const body = JSON.stringify(payload);", + " const request = http.request({", + " hostname: parsed.hostname,", + " port: parsed.port || 80,", + " path: parsed.pathname + parsed.search,", + " method: \"POST\",", + " headers: { \"content-type\": \"application/json\", \"content-length\": Buffer.byteLength(body) },", + " timeout: timeoutMs", + " }, (response) => {", + " let responseBody = \"\";", + " response.setEncoding(\"utf8\");", + " response.on(\"data\", (chunk) => { responseBody += chunk; });", + " response.on(\"end\", () => {", + " try {", + " const parsedBody = responseBody ? JSON.parse(responseBody) : null;", + " resolve({ statusCode: response.statusCode, body: parsedBody });", + " } catch (error) {", + " reject(error);", + " }", + " });", + " });", + " request.on(\"timeout\", () => request.destroy(new Error(\"request timeout after \" + timeoutMs + \"ms\")));", + " request.on(\"error\", reject);", + " request.end(body);", + " });", + "}", + "", + "function gatewayPayload(input) {", + " const traceId = input.traceId || \"trc_device_agent_71_freq_\" + Date.now();", + " return {", + " jsonrpc: \"2.0\",", + " id: input.id || \"req_device_agent_71_freq_\" + Date.now(),", + " method: \"hardware.invoke.shell\",", + " meta: { serviceId: \"hwlab-cloud-api\", actorId: \"device-agent-71-freq\", environment: \"dev\", traceId, deviceId, workspaceRoot },", + " params: {", + " gatewaySessionId,", + " resourceId,", + " capabilityId,", + " input: {", + " command: input.command,", + " cwd: input.cwd || workspaceRoot,", + " timeoutMs: input.timeoutMs || 30000", + " }", + " }", + " };", + "}", + "", + "const server = http.createServer(async (req, res) => {", + " try {", + " const url = new URL(req.url || \"/\", \"http://device-agent-71-freq\");", + " if (req.method === \"GET\" && url.pathname === \"/health\") {", + " return sendJson(res, 200, { ok: true, deviceId, workspaceRoot, gatewaySessionId, resourceId, capabilityId, cloudApiUrl });", + " }", + " if (req.method === \"GET\" && url.pathname === \"/skills\") {", + " return sendJson(res, 200, { ok: true, deviceId, skills: [{ name: \"cmd\", description: \"Run a bounded Windows cmd command through hwlab-gateway.\" }] });", + " }", + " if (req.method === \"POST\" && url.pathname === \"/run\") {", + " const input = await readBody(req);", + " if (!input.command || typeof input.command !== \"string\") return sendJson(res, 400, { ok: false, error: \"command is required\" });", + " const timeoutMs = Number(input.timeoutMs || 30000);", + " const upstream = await postJson(cloudApiUrl + \"/json-rpc\", gatewayPayload({ ...input, timeoutMs }), timeoutMs + 5000);", + " return sendJson(res, upstream.statusCode || 502, { ok: upstream.statusCode >= 200 && upstream.statusCode < 300, deviceId, gatewaySessionId, upstream: upstream.body });", + " }", + " return sendJson(res, 404, { ok: false, error: \"not found\" });", + " } catch (error) {", + " return sendJson(res, 500, { ok: false, error: error.message });", + " }", + "});", + "", + "server.listen(port, \"0.0.0.0\", () => {", + " console.log(JSON.stringify({ ok: true, service: \"device-agent-71-freq\", port, deviceId, workspaceRoot, cloudApiUrl }));", + "});" + ].join("\n") + "\n"; +} + +function deviceAgent71FreqManifest({ profile = "dev", source, registryPrefix }) { + assert.equal(profile, "dev", "71-freq device-agent is dev-only"); + const namespace = namespaceNameForProfile(profile); + const name = "device-agent-71-freq"; + const labels = { + "app.kubernetes.io/name": name, + "app.kubernetes.io/part-of": "hwlab", + "hwlab.pikastech.local/component": "device-agent", + "hwlab.pikastech.local/device-id": "71-freq", + "hwlab.pikastech.local/environment": runtimeLabelForProfile(profile), + "hwlab.pikastech.local/gitops-target": "g14", + "hwlab.pikastech.local/profile": runtimeLabelForProfile(profile), + "hwlab.pikastech.local/service-id": name, + "hwlab.pikastech.local/source-commit": source.full + }; + const annotations = { "hwlab.pikastech.local/rendered-by": "scripts/g14-gitops-render.mjs" }; + const selector = { "app.kubernetes.io/name": name }; + const script = deviceAgent71FreqServerScript(); + const scriptSha256 = createHash("sha256").update(script).digest("hex"); + const templateAnnotations = { ...annotations, "hwlab.pikastech.local/script-sha256": scriptSha256 }; + return { + apiVersion: "v1", + kind: "List", + items: [ + { + apiVersion: "v1", + kind: "ConfigMap", + metadata: { name: `${name}-script`, namespace, labels, annotations: templateAnnotations }, + data: { "server.mjs": script } + }, + { + apiVersion: "apps/v1", + kind: "Deployment", + metadata: { name, namespace, labels, annotations }, + spec: { + replicas: 1, + selector: { matchLabels: selector }, + template: { + metadata: { labels: { ...labels, ...selector }, annotations: templateAnnotations }, + spec: { + containers: [{ + name: "device-agent", + image: imageFor(registryPrefix, "hwlab-cloud-api", source.short), + imagePullPolicy: "IfNotPresent", + command: ["node", "/opt/device-agent/server.mjs"], + env: [ + { name: "PORT", value: "7601" }, + { name: "DEVICE_ID", value: "71-freq" }, + { name: "DEVICE_WORKSPACE_ROOT", value: "F:\\Work\\ConStart" }, + { name: "HWLAB_CLOUD_API_URL", value: `http://hwlab-cloud-api.${namespace}.svc.cluster.local:6667` }, + { name: "HWLAB_GATEWAY_SESSION_ID", value: "gws_d601_win_71_freq" }, + { name: "HWLAB_GATEWAY_RESOURCE_ID", value: "res_d601_windows_host" }, + { name: "HWLAB_GATEWAY_CAPABILITY_ID", value: "cap_d601_windows_cmd_exec" } + ], + ports: [{ name: "http", containerPort: 7601 }], + readinessProbe: { httpGet: { path: "/health", port: "http" }, initialDelaySeconds: 3, periodSeconds: 10 }, + livenessProbe: { httpGet: { path: "/health", port: "http" }, initialDelaySeconds: 10, periodSeconds: 20 }, + resources: { requests: { cpu: "10m", memory: "64Mi" }, limits: { cpu: "200m", memory: "256Mi" } }, + volumeMounts: [{ name: "script", mountPath: "/opt/device-agent", readOnly: true }] + }], + volumes: [{ name: "script", configMap: { name: `${name}-script` } }] + } + } + } + }, + { + apiVersion: "v1", + kind: "Service", + metadata: { name, namespace, labels, annotations }, + spec: { type: "ClusterIP", selector, ports: [{ name: "http", port: 7601, targetPort: "http" }] } + } + ] + }; +} + +function runtimeKustomization({ profile = "dev", includeDeviceAgent = profile === "dev" } = {}) { const resources = ["namespace.yaml", "code-agent-codex-config.yaml", "services.yaml", "health-contract.yaml", "workloads.yaml", "deepseek-proxy.yaml"]; + if (includeDeviceAgent) resources.push("device-agent-71-freq.yaml"); resources.push("g14-frpc.yaml"); return { apiVersion: "kustomize.config.k8s.io/v1beta1", @@ -1498,6 +1684,7 @@ async function plannedFiles(args) { for (const profile of ["dev", "prod"]) { const namespace = namespaceNameForProfile(profile); + const includeDeviceAgent = profile === "dev"; const profileLabels = { ...baseLabels, "hwlab.pikastech.local/environment": runtimeLabelForProfile(profile), "hwlab.pikastech.local/profile": runtimeLabelForProfile(profile) }; const runtimeNamespace = cloneJson(namespaceTemplate); runtimeNamespace.metadata.name = namespace; @@ -1505,13 +1692,14 @@ async function plannedFiles(args) { annotate(runtimeNamespace.metadata, annotations); const runtimePath = runtimePathForProfile(profile); const endpoints = profileEndpoint(args, profile); - putJson(`${runtimePath}/kustomization.yaml`, runtimeKustomization({ profile })); + putJson(`${runtimePath}/kustomization.yaml`, runtimeKustomization({ profile, includeDeviceAgent })); putJson(`${runtimePath}/namespace.yaml`, runtimeNamespace); putJson(`${runtimePath}/code-agent-codex-config.yaml`, transformListNamespace(config, namespace, profileLabels, annotations)); putJson(`${runtimePath}/services.yaml`, transformListNamespace(services, namespace, profileLabels, annotations)); putJson(`${runtimePath}/health-contract.yaml`, transformHealthContract(health, namespace, profileLabels, annotations, endpoints.runtimeEndpoint, endpoints.webEndpoint, profile)); putJson(`${runtimePath}/workloads.yaml`, transformWorkloads({ workloads, deploy, source, registryPrefix: args.registryPrefix, runtimeEndpoint: endpoints.runtimeEndpoint, webEndpoint: endpoints.webEndpoint, profile })); putJson(`${runtimePath}/deepseek-proxy.yaml`, deepSeekProxyManifest({ profile })); + if (includeDeviceAgent) putJson(`${runtimePath}/device-agent-71-freq.yaml`, deviceAgent71FreqManifest({ profile, source, registryPrefix: args.registryPrefix })); putJson(`${runtimePath}/g14-frpc.yaml`, g14FrpcManifest({ profile })); } return { files, source };