181 lines
6.6 KiB
JavaScript
181 lines
6.6 KiB
JavaScript
import { spawn } from "node:child_process";
|
|
import { createServer } from "node:http";
|
|
import { chmodSync, existsSync, lstatSync, mkdirSync, symlinkSync } from "node:fs";
|
|
import path from "node:path";
|
|
|
|
import { serveCloudWeb } from "/app/internal/dev-entrypoint/cloud-web-runtime.mjs";
|
|
|
|
const serviceId = process.env.HWLAB_SERVICE_ID || "hwlab-unknown";
|
|
const environment = process.env.HWLAB_ENVIRONMENT || "dev";
|
|
const runtimeKind = process.env.HWLAB_ARTIFACT_KIND || "health-placeholder";
|
|
const entrypoint = process.env.HWLAB_SERVICE_ENTRYPOINT || "";
|
|
const port = Number.parseInt(process.env.PORT || process.env.HWLAB_PORT || "8080", 10);
|
|
|
|
function ensureCodeAgentRuntimeBase() {
|
|
if (serviceId !== "hwlab-cloud-api") return;
|
|
const workspace = process.env.HWLAB_CODE_AGENT_CODEX_WORKSPACE || process.env.HWLAB_CODE_AGENT_WORKSPACE || "/workspace/hwlab";
|
|
const codexHome = process.env.CODEX_HOME || process.env.HWLAB_CODE_AGENT_CODEX_HOME || "/codex-home";
|
|
process.env.HWLAB_CODE_AGENT_WORKSPACE = process.env.HWLAB_CODE_AGENT_WORKSPACE || workspace;
|
|
process.env.HWLAB_CODE_AGENT_CODEX_WORKSPACE = process.env.HWLAB_CODE_AGENT_CODEX_WORKSPACE || workspace;
|
|
process.env.HWLAB_CODE_AGENT_CODEX_SANDBOX = process.env.HWLAB_CODE_AGENT_CODEX_SANDBOX || "danger-full-access";
|
|
process.env.HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED = process.env.HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED || "1";
|
|
process.env.HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR = process.env.HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR || "repo-owned";
|
|
process.env.HWLAB_CODE_AGENT_PROVIDER = process.env.HWLAB_CODE_AGENT_PROVIDER || "codex-stdio";
|
|
process.env.CODEX_HOME = codexHome;
|
|
process.env.HWLAB_CODE_AGENT_CODEX_COMMAND = process.env.HWLAB_CODE_AGENT_CODEX_COMMAND || "/app/node_modules/.bin/codex";
|
|
process.env.HWLAB_CODE_AGENT_SKILLS_DIRS = process.env.HWLAB_CODE_AGENT_SKILLS_DIRS || "/app/skills";
|
|
|
|
mkdirSync(path.dirname(workspace), { recursive: true });
|
|
if (!existsSync(workspace)) {
|
|
if (workspace === "/workspace/hwlab" && existsSync("/app")) {
|
|
symlinkSync("/app", workspace, "dir");
|
|
} else {
|
|
mkdirSync(workspace, { recursive: true });
|
|
}
|
|
}
|
|
mkdirSync(codexHome, { recursive: true });
|
|
for (const target of [workspace, codexHome]) {
|
|
try {
|
|
const info = lstatSync(target);
|
|
chmodSync(info.isSymbolicLink() ? path.resolve(target) : target, 0o777);
|
|
} catch {
|
|
// Runtime readiness reports the concrete blocker without exposing secret values.
|
|
}
|
|
}
|
|
}
|
|
|
|
ensureCodeAgentRuntimeBase();
|
|
|
|
function sendJson(response, statusCode, body) {
|
|
const payload = JSON.stringify(body, null, 2);
|
|
response.writeHead(statusCode, {
|
|
"content-type": "application/json; charset=utf-8",
|
|
"content-length": Buffer.byteLength(payload)
|
|
});
|
|
response.end(payload);
|
|
}
|
|
|
|
function healthPayload() {
|
|
const commitId = process.env.HWLAB_COMMIT_ID || process.env.HWLAB_GIT_SHA || "unknown";
|
|
const imageReference = process.env.HWLAB_IMAGE || "unknown";
|
|
const imageTag = imageTagFromReference(imageReference) || shortRevision(commitId);
|
|
const buildCreatedAt = normalizeIsoTimestamp(
|
|
process.env.HWLAB_BUILD_CREATED_AT ||
|
|
process.env.HWLAB_IMAGE_CREATED_AT ||
|
|
process.env.SOURCE_DATE_EPOCH ||
|
|
process.env.BUILD_DATE
|
|
);
|
|
const buildSource = process.env.HWLAB_BUILD_SOURCE || "runtime-env";
|
|
return {
|
|
serviceId,
|
|
environment,
|
|
status: "ok",
|
|
artifactKind: runtimeKind,
|
|
revision: process.env.HWLAB_REVISION || commitId,
|
|
commit: {
|
|
id: commitId,
|
|
source: buildSource
|
|
},
|
|
image: {
|
|
reference: imageReference,
|
|
tag: imageTag,
|
|
digest: process.env.HWLAB_IMAGE_DIGEST || "unknown"
|
|
},
|
|
build: {
|
|
createdAt: buildCreatedAt,
|
|
source: buildSource,
|
|
provenance: process.env.HWLAB_BUILD_PROVENANCE || null,
|
|
metadataSource: buildCreatedAt ? "runtime-env:HWLAB_BUILD_CREATED_AT" : "unavailable",
|
|
unavailableReason: buildCreatedAt ? null : "HWLAB_BUILD_CREATED_AT missing from runtime environment"
|
|
}
|
|
};
|
|
}
|
|
|
|
function normalizeIsoTimestamp(value) {
|
|
const source = String(value || "").trim();
|
|
if (!source) return null;
|
|
if (/^\d+$/u.test(source)) {
|
|
const seconds = Number.parseInt(source, 10);
|
|
if (Number.isSafeInteger(seconds)) {
|
|
const date = new Date(seconds * 1000);
|
|
if (Number.isFinite(date.getTime())) return date.toISOString();
|
|
}
|
|
}
|
|
const date = new Date(source);
|
|
return Number.isFinite(date.getTime()) ? date.toISOString() : null;
|
|
}
|
|
|
|
function imageTagFromReference(imageReference) {
|
|
const source = String(imageReference || "").trim();
|
|
const slashIndex = source.lastIndexOf("/");
|
|
const colonIndex = source.lastIndexOf(":");
|
|
if (colonIndex <= slashIndex) return null;
|
|
return source.slice(colonIndex + 1) || null;
|
|
}
|
|
|
|
function shortRevision(value) {
|
|
const source = String(value || "").trim();
|
|
return source.length >= 7 ? source.slice(0, 7) : source || "unknown";
|
|
}
|
|
|
|
function runEntrypoint(command, args) {
|
|
const child = spawn(command, args, {
|
|
cwd: "/app",
|
|
env: process.env,
|
|
stdio: "inherit"
|
|
});
|
|
child.on("error", (error) => {
|
|
process.stderr.write(JSON.stringify({
|
|
serviceId,
|
|
status: "failed",
|
|
error: "entrypoint_spawn_failed",
|
|
command,
|
|
reason: error.message
|
|
}) + "\n");
|
|
process.exit(127);
|
|
});
|
|
child.on("exit", (code, signal) => {
|
|
if (signal) process.kill(process.pid, signal);
|
|
else process.exit(code ?? 0);
|
|
});
|
|
}
|
|
|
|
function runNodeEntrypoint(file) {
|
|
runEntrypoint(process.execPath, [file]);
|
|
}
|
|
|
|
function runBunEntrypoint(file) {
|
|
const candidates = [
|
|
process.env.HWLAB_BUN_COMMAND,
|
|
"/app/node_modules/.bin/bun",
|
|
"/usr/local/bin/bun",
|
|
"bun"
|
|
].filter(Boolean);
|
|
const command = candidates.find((candidate) => !candidate.includes("/") || existsSync(candidate)) || "bun";
|
|
runEntrypoint(command, [file]);
|
|
}
|
|
|
|
function serveHealthOnly() {
|
|
const server = createServer((request, response) => {
|
|
const url = new URL(request.url || "/", "http://hwlab-artifact.local");
|
|
if (url.pathname === "/health/live" || url.pathname === "/health") {
|
|
sendJson(response, 200, healthPayload());
|
|
return;
|
|
}
|
|
sendJson(response, 404, { error: "not_found", serviceId, path: url.pathname });
|
|
});
|
|
server.listen(port, "0.0.0.0", () => {
|
|
process.stdout.write(JSON.stringify({ serviceId, environment, status: "listening", port }) + "\n");
|
|
});
|
|
}
|
|
|
|
if (runtimeKind === "node-command" && entrypoint) {
|
|
runNodeEntrypoint(entrypoint);
|
|
} else if (runtimeKind === "bun-command" && entrypoint) {
|
|
runBunEntrypoint(entrypoint);
|
|
} else if (runtimeKind === "cloud-web") {
|
|
await serveCloudWeb({ port, serviceId, healthPayload, sendJson });
|
|
} else {
|
|
serveHealthOnly();
|
|
}
|