feat: add HWLAB web probe credential injection

This commit is contained in:
Codex
2026-06-15 12:16:29 +00:00
parent c8ce421a3b
commit b7fee7e899
2 changed files with 240 additions and 4 deletions
+4 -3
View File
@@ -58,7 +58,7 @@ export function rootHelp(): unknown {
{ command: "auth-broker contract|health --dry-run|credential-request --dry-run|pr-preflight --dry-run", description: "Inspect the P0 Rust auth broker and CLI adapter contract without reading token values, writing GitHub, or starting services." },
{ command: "gh preflight|auth|issue|pr", description: "Run safe GitHub issue and PR CRUD/lifecycle operations through REST with body-file update replace/append, issue/comment apply_patch body patching, comment delete, token diagnostics, PR closeout preflight, hard delete unsupported, and guarded PR merge." },
{ command: "commander contract|plan --dry-run|smoke --dry-run|approval request --dry-run", description: "Host Codex commander skeleton contract, no-daemon smoke plan, and dry-run approval preview without live bridges or message sends." },
{ command: "hwlab nodes control-plane|git-mirror|secret|test-accounts --node <node> --lane <lane>", description: "Manage HWLAB node/lane runtime prerequisites, including D601 YAML-declared infra/tools-image/Argo bootstrap, redacted test-account preparation, and G14 v0.3+ runtime lanes, with the node identity passed as data." },
{ command: "hwlab nodes control-plane|git-mirror|secret|test-accounts|web-probe --node <node> --lane <lane>", description: "Manage HWLAB node/lane runtime prerequisites, including D601 YAML-declared infra/tools-image/Argo bootstrap, redacted test-account preparation, Web DOM probe credential injection, and G14 v0.3+ runtime lanes, with the node identity passed as data." },
{ command: "hwlab g14 monitor-prs | hwlab g14 control-plane status|apply|trigger-current|runtime-migration|cleanup-runs|cleanup-released-pvs | hwlab g14 git-mirror status|apply|sync|flush | hwlab g14 tools-image status|build", description: "Start the legacy G14 PR monitor, run bounded v0.2 Tekton/Argo control-plane, manual PipelineRun trigger, runtime migration, CI workspace retention, manual devops-infra git mirror/relay maintenance, or fixed HWLAB CI tools image actions; long confirmed trigger/sync/flush actions return async jobs by default." },
{ command: "agentrun get|describe|events|logs|result|ack|cancel|dispatch|create|apply|send|control-plane|git-mirror", description: "Use AgentRun v0.1 resource primitives with low-noise human output by default; session follow-up uses send only and the server decides internal steer vs turn." },
{ command: "platform-infra sub2api|langbot|n8n|wechat-archive ...", description: "Deploy platform-infra services such as Sub2API, LangBot and n8n, manage YAML-controlled public FRP/Caddy exposure and WeChat archive workflows, and inspect status/logs without printing secrets." },
@@ -643,7 +643,7 @@ function platformInfraHelpSummary(): unknown {
function hwlabNodeHelpSummary(): unknown {
return {
command: "hwlab nodes control-plane|git-mirror|secret|test-accounts --node <node> --lane <lane>",
command: "hwlab nodes control-plane|git-mirror|secret|test-accounts|web-probe --node <node> --lane <lane>",
output: "json",
usage: [
"bun scripts/cli.ts hwlab nodes control-plane infra plan --node D601 --lane v03",
@@ -655,8 +655,9 @@ function hwlabNodeHelpSummary(): unknown {
"bun scripts/cli.ts hwlab nodes secret status --node G14 --lane v03 --name <secret>",
"bun scripts/cli.ts hwlab nodes test-accounts status --node D601 --lane v03",
"bun scripts/cli.ts hwlab nodes test-accounts sync --node D601 --lane v03 --confirm",
"bun scripts/cli.ts hwlab nodes web-probe run --node D601 --lane v03 --wait-messages-ms 1000",
],
description: "Operate HWLAB node/lane runtime prerequisites with node and lane passed as data. The infra subcommand manages YAML-controlled node-local CI/CD, git-mirror, public Dockerfile tools image, and declarative Argo CD prerequisites for D601 v03 while keeping cross-node work semi-automatic; test-accounts prepares UniDesk YAML-declared admin/test account API keys with redacted sourceRef/fingerprint output.",
description: "Operate HWLAB node/lane runtime prerequisites with node and lane passed as data. The infra subcommand manages YAML-controlled node-local CI/CD, git-mirror, public Dockerfile tools image, and declarative Argo CD prerequisites for D601 v03 while keeping cross-node work semi-automatic; test-accounts prepares UniDesk YAML-declared admin/test account API keys with redacted sourceRef/fingerprint output; web-probe runs the target workspace DOM probe with bootstrap Web credentials injected only as one-shot stdin/env.",
};
}
+236 -1
View File
@@ -14,6 +14,22 @@ type SecretPreset = "openfga" | "master-server-admin-api-key" | "bootstrap-admin
type DelegatedNodeDomain = "control-plane" | "git-mirror";
type NodeRuntimeRenderLocation = "node-host" | "local";
interface NodeWebProbeOptions {
action: "run";
node: string;
lane: string;
url: string;
timeoutMs: number;
waitAfterSubmitMs: number;
waitMessagesMs: number;
waitAgentTerminalMs: number;
message: string | null;
conversationId: string | null;
freshSession: boolean;
cancelRunning: boolean;
commandTimeoutSeconds: number;
}
interface NodeRuntimeRenderResult {
readonly result: CommandResult;
readonly renderDir: string;
@@ -30,6 +46,7 @@ interface NodeSecretOptions {
preset: SecretPreset;
dryRun: boolean;
confirm: boolean;
force?: boolean;
timeoutSeconds: number;
}
@@ -44,6 +61,17 @@ interface BootstrapAdminSecretMaterial {
error: string | null;
}
interface BootstrapAdminPasswordMaterial {
ok: boolean;
sourceRef: string | null;
sourceKey: string | null;
sourcePath: string | null;
sourcePresent: boolean;
sourceFingerprint: string | null;
password: string | null;
error: string | null;
}
interface NodePublicExposureOptions {
action: "public-exposure";
node: string;
@@ -138,12 +166,16 @@ export async function runHwlabNodeCommand(_config: Config, args: string[]): Prom
const { runHwlabTestAccountsCommand } = await import("./hwlab-test-accounts");
return runHwlabTestAccountsCommand(args.slice(1));
}
if (domain === "web-probe") {
if (args.length === 1 || args.includes("--help") || args.includes("-h") || args[1] === "help") return hwlabNodeWebProbeHelp();
return runNodeWebProbe(parseNodeWebProbeOptions(args.slice(1)));
}
if (args.includes("--help") || args.includes("-h")) return hwlabNodeHelp();
if (domain === "control-plane" || domain === "git-mirror") {
return runNodeDelegatedDomain(_config, domain, args.slice(1));
}
if (domain !== "secret") {
return { ok: false, command: `hwlab nodes ${domain ?? ""}`.trim(), message: "supported commands: hwlab nodes control-plane, hwlab nodes git-mirror, hwlab nodes secret, hwlab nodes test-accounts" };
return { ok: false, command: `hwlab nodes ${domain ?? ""}`.trim(), message: "supported commands: hwlab nodes control-plane, hwlab nodes git-mirror, hwlab nodes secret, hwlab nodes test-accounts, hwlab nodes web-probe" };
}
const options = parseSecretOptions(args.slice(1));
return runNodeSecret(options);
@@ -182,6 +214,7 @@ export function hwlabNodeHelp(): Record<string, unknown> {
"bun scripts/cli.ts hwlab nodes secret ensure --node G14 --lane v03 --name hwlab-v03-master-server-admin-api-key --confirm",
"bun scripts/cli.ts hwlab nodes secret status --node G14 --lane v03 --name hwlab-v03-bootstrap-admin",
"bun scripts/cli.ts hwlab nodes secret ensure --node G14 --lane v03 --name hwlab-v03-bootstrap-admin --confirm",
"bun scripts/cli.ts hwlab nodes secret ensure --node D601 --lane v03 --name hwlab-v03-bootstrap-admin --confirm --force",
"bun scripts/cli.ts hwlab nodes secret status --node G14 --lane v03 --name hwlab-cloud-api-v03-db",
"bun scripts/cli.ts hwlab nodes secret cleanup-owned-postgres --node G14 --lane v03 --dry-run",
"bun scripts/cli.ts hwlab nodes secret cleanup-owned-postgres --node G14 --lane v03 --confirm",
@@ -191,6 +224,19 @@ export function hwlabNodeHelp(): Record<string, unknown> {
"bun scripts/cli.ts hwlab nodes secret ensure --node G14 --lane v03 --name hwlab-v03-code-agent-provider --confirm",
"bun scripts/cli.ts hwlab nodes test-accounts status --node D601 --lane v03",
"bun scripts/cli.ts hwlab nodes test-accounts sync --node D601 --lane v03 --confirm",
"bun scripts/cli.ts hwlab nodes web-probe run --node D601 --lane v03 --wait-messages-ms 1000",
],
};
}
function hwlabNodeWebProbeHelp(): Record<string, unknown> {
return {
ok: true,
command: "hwlab nodes web-probe",
description: "Run the target node/lane HWLAB Cloud Web DOM probe with Web login credentials resolved from YAML-declared bootstrap admin sourceRef and injected only as one-shot stdin/env.",
examples: [
"bun scripts/cli.ts hwlab nodes web-probe run --node D601 --lane v03 --wait-messages-ms 1000",
"bun scripts/cli.ts hwlab nodes web-probe run --node D601 --lane v03 --url https://hwlab.pikapython.com --fresh-session --message 'ping'",
],
};
}
@@ -3246,6 +3292,34 @@ function readBootstrapAdminSecretMaterial(spec: RuntimeSecretSpec): BootstrapAdm
};
}
function readBootstrapAdminPasswordMaterial(spec: RuntimeSecretSpec): BootstrapAdminPasswordMaterial {
const sourceRef = spec.bootstrapAdminPasswordSourceRef;
const sourceKey = spec.bootstrapAdminPasswordSourceKey;
if (sourceRef === undefined || sourceKey === undefined || spec.bootstrapAdminPasswordHashTransform === undefined) {
return { ok: false, sourceRef: sourceRef ?? null, sourceKey: sourceKey ?? null, sourcePath: null, sourcePresent: false, sourceFingerprint: null, password: null, error: "bootstrap-admin-yaml-source-missing" };
}
const paths = secretSourcePaths(sourceRef);
const sourcePath = paths.find((candidate) => existsSync(candidate)) ?? paths[0] ?? join(repoRoot, ".state", "secrets", sourceRef);
if (!existsSync(sourcePath)) {
return { ok: false, sourceRef, sourceKey, sourcePath, sourcePresent: false, sourceFingerprint: null, password: null, error: "secret-source-missing" };
}
const values = parseEnvFile(readFileSync(sourcePath, "utf8"));
const password = values[sourceKey];
if (password === undefined || password.length === 0) {
return { ok: false, sourceRef, sourceKey, sourcePath, sourcePresent: true, sourceFingerprint: null, password: null, error: "secret-key-missing" };
}
return {
ok: true,
sourceRef,
sourceKey,
sourcePath,
sourcePresent: true,
sourceFingerprint: `sha256:${createHash("sha256").update(password).digest("hex").slice(0, 16)}`,
password,
error: null,
};
}
function parseEnvFile(text: string): Record<string, string> {
const values: Record<string, string> = {};
for (const rawLine of text.split(/\r?\n/u)) {
@@ -3414,6 +3488,127 @@ function rewriteDelegatedNodeString(value: string, scoped: ReturnType<typeof par
return replaceCommand(replaceCommand(value, "control-plane"), "git-mirror");
}
function parseNodeWebProbeOptions(args: string[]): NodeWebProbeOptions {
const [actionRaw] = args;
if (actionRaw !== "run") throw new Error("web-probe usage: run --node NODE --lane vNN [--url URL] [--wait-messages-ms N]");
const node = requiredOption(args, "--node");
assertNodeId(node);
const lane = requiredOption(args, "--lane");
assertLane(lane);
if (!isHwlabRuntimeLane(lane)) throw new Error(`web-probe only supports HWLAB runtime lanes, got ${lane}`);
const spec = hwlabRuntimeLaneSpecForNode(lane, node);
assertKnownOptions(args.slice(1), new Set([
"--node",
"--lane",
"--url",
"--timeout-ms",
"--wait-after-submit-ms",
"--wait-messages-ms",
"--wait-agent-terminal-ms",
"--message",
"--conversation-id",
"--command-timeout-seconds",
]), new Set([
"--fresh-session",
"--no-cancel-running",
]));
return {
action: "run",
node,
lane,
url: optionValue(args, "--url") ?? spec.publicWebUrl,
timeoutMs: positiveIntegerOption(args, "--timeout-ms", 30000, 60000),
waitAfterSubmitMs: positiveIntegerOption(args, "--wait-after-submit-ms", 1500, 60000),
waitMessagesMs: positiveIntegerOption(args, "--wait-messages-ms", 2500, 60000),
waitAgentTerminalMs: positiveIntegerOption(args, "--wait-agent-terminal-ms", 0, 600000),
message: optionValue(args, "--message") ?? null,
conversationId: optionValue(args, "--conversation-id") ?? null,
freshSession: args.includes("--fresh-session"),
cancelRunning: !args.includes("--no-cancel-running"),
commandTimeoutSeconds: positiveIntegerOption(args, "--command-timeout-seconds", 60, 60),
};
}
function assertKnownOptions(args: string[], valueOptions: Set<string>, flagOptions: Set<string>): void {
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (!arg.startsWith("--")) continue;
if (flagOptions.has(arg)) continue;
if (valueOptions.has(arg)) {
index += 1;
continue;
}
throw new Error(`unknown option: ${arg}`);
}
}
function runNodeWebProbe(options: NodeWebProbeOptions): Record<string, unknown> {
const lane = options.lane;
if (!isHwlabRuntimeLane(lane)) throw new Error(`web-probe only supports HWLAB runtime lanes, got ${lane}`);
const spec = hwlabRuntimeLaneSpecForNode(lane, options.node);
const secretSpec = runtimeSecretSpec({ node: options.node, lane });
const material = readBootstrapAdminPasswordMaterial(secretSpec);
const credential = {
username: secretSpec.bootstrapAdminUsername,
sourceRef: material.sourceRef,
sourceKey: material.sourceKey,
sourcePath: material.sourcePath === null ? null : displayRepoPath(material.sourcePath),
sourcePresent: material.sourcePresent,
sourceFingerprint: material.sourceFingerprint,
injectedVia: material.ok ? "stdin-env" : null,
valuesRedacted: true,
error: material.error,
};
if (!material.ok || material.password === null) {
return {
ok: false,
status: "blocked",
command: `hwlab nodes web-probe run --node ${options.node} --lane ${options.lane}`,
node: options.node,
lane: options.lane,
workspace: spec.workspace,
url: options.url,
degradedReason: "web_login_secret_missing",
credential,
next: { secretStatus: `bun scripts/cli.ts hwlab nodes secret status --node ${options.node} --lane ${options.lane} --name ${secretSpec.bootstrapAdminSecret}` },
};
}
const probeArgs = [
"node",
"scripts/web-live-dom-probe.mjs",
"run",
"--url", options.url,
"--timeout-ms", String(options.timeoutMs),
"--wait-after-submit-ms", String(options.waitAfterSubmitMs),
"--wait-messages-ms", String(options.waitMessagesMs),
];
if (options.waitAgentTerminalMs > 0) probeArgs.push("--wait-agent-terminal-ms", String(options.waitAgentTerminalMs));
if (options.freshSession) probeArgs.push("--fresh-session");
if (!options.cancelRunning) probeArgs.push("--no-cancel-running");
if (options.conversationId !== null) probeArgs.push("--conversation-id", options.conversationId);
if (options.message !== null) probeArgs.push("--message", options.message);
const script = [
"set -eu",
`HWLAB_WEB_USER=${shellQuote(secretSpec.bootstrapAdminUsername)} HWLAB_WEB_PASS=${shellQuote(material.password)} ${probeArgs.map(shellQuote).join(" ")}`,
].join("\n");
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
const probe = compactWebProbeResult(parseJsonObject(result.stdout));
const passed = result.exitCode === 0 && probe?.status === "pass";
return {
ok: passed,
status: passed ? "pass" : "blocked",
command: `hwlab nodes web-probe run --node ${options.node} --lane ${options.lane}`,
node: options.node,
lane: options.lane,
workspace: spec.workspace,
url: options.url,
credential,
probe,
result: compactCommandResult(result),
valuesRedacted: true,
};
}
function parseSecretOptions(args: string[]): NodeSecretOptions {
const [actionRaw] = args;
if (actionRaw !== "status" && actionRaw !== "ensure" && actionRaw !== "cleanup-owned-postgres" && actionRaw !== "cleanup-obsolete") {
@@ -3475,6 +3670,8 @@ function parseSecretOptions(args: string[]): NodeSecretOptions {
}
if (name === spec.bootstrapAdminSecret) {
if (key !== undefined && key !== BOOTSTRAP_ADMIN_PASSWORD_HASH_KEY) throw new Error(`secret ${name} supports only key ${BOOTSTRAP_ADMIN_PASSWORD_HASH_KEY}`);
const force = args.includes("--force");
if (force && actionRaw !== "ensure") throw new Error("secret --force is only supported with ensure");
return {
action: actionRaw,
node,
@@ -3483,6 +3680,7 @@ function parseSecretOptions(args: string[]): NodeSecretOptions {
key: key ?? BOOTSTRAP_ADMIN_PASSWORD_HASH_KEY,
preset: "bootstrap-admin",
confirm,
force,
dryRun: actionRaw === "status" ? true : explicitDryRun || !confirm,
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 180, 900),
};
@@ -4234,6 +4432,10 @@ function runTransHostScript(node: string, script: string, input: string, timeout
return runCommand([transPath(), node, "sh", "--", script], repoRoot, { input, timeoutMs: timeoutSeconds * 1000 });
}
function runTransWorkspaceStdinScript(node: string, workspace: string, script: string, timeoutSeconds: number): CommandResult {
return runCommand([transPath(), `${node}:${workspace}`, "sh"], repoRoot, { input: script, timeoutMs: timeoutSeconds * 1000 });
}
function runObsoleteSecretCleanup(options: NodeSecretOptions, spec: RuntimeSecretSpec): Record<string, unknown> {
const kubernetesResult = runTransScript(options.node, obsoleteSecretCleanupScript(options, spec), "", options.timeoutSeconds);
const kubernetesStatus = secretStatusFromText(statusText(kubernetesResult), kubernetesResult.exitCode === 0, kubernetesResult.exitCode, kubernetesResult.stderr, spec);
@@ -5135,6 +5337,7 @@ function bootstrapAdminSecretScript(options: NodeSecretOptions, spec: RuntimeSec
`source_error=${shellQuote(material?.error ?? "")}`,
`transform=${shellQuote(spec.bootstrapAdminPasswordHashTransform ?? "")}`,
`material_ok=${shellQuote(materialOk ? "true" : "false")}`,
`force_sync=${shellQuote(options.force === true ? "true" : "false")}`,
`cloud_api_deployment=${shellQuote(spec.cloudApiDeployment)}`,
`action_request=${shellQuote(options.action)}`,
`dry_run=${shellQuote(options.dryRun ? "true" : "false")}`,
@@ -5161,6 +5364,7 @@ function bootstrapAdminSecretScript(options: NodeSecretOptions, spec: RuntimeSec
" needs_sync=false",
" [ \"$before_exists\" = yes ] && [ \"$before_hash_bytes\" -gt 0 ] || needs_sync=true",
" [ \"$before_source_ref\" = \"$source_ref\" ] && [ \"$before_source_key\" = \"$source_key\" ] && [ \"$before_source_fingerprint\" = \"$source_fingerprint\" ] && [ \"$before_username\" = \"$username\" ] || needs_sync=true",
" [ \"$force_sync\" = true ] && needs_sync=true",
" if [ \"$material_ok\" != true ]; then",
" action=${source_error:-secret-source-invalid}",
" apply_exit=44",
@@ -5228,6 +5432,7 @@ function bootstrapAdminSecretScript(options: NodeSecretOptions, spec: RuntimeSec
"printf 'sourceExists\\t%s\\n' \"$source_present\"",
"printf 'sourceFingerprint\\t%s\\n' \"$source_fingerprint\"",
"printf 'passwordHashTransform\\t%s\\n' \"$transform\"",
"printf 'forceSync\\t%s\\n' \"$force_sync\"",
"printf 'action\\t%s\\n' \"$action\"",
"printf 'dryRun\\t%s\\n' \"$dry_run\"",
"printf 'mutation\\t%s\\n' \"$mutation\"",
@@ -5756,6 +5961,7 @@ function secretStatusFromText(text: string, commandOk: boolean, exitCode: number
},
action: fields.action || null,
dryRun: fields.dryRun === "true",
forceSync: fields.forceSync === "true",
mutation: fields.mutation === "true",
before: {
exists: fields.beforeExists === "yes",
@@ -6214,6 +6420,35 @@ function compactCommandResult(result: CommandResult): Record<string, unknown> {
};
}
function parseJsonObject(text: string): Record<string, unknown> | null {
try {
const parsed = JSON.parse(text) as unknown;
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed as Record<string, unknown> : null;
} catch {
return null;
}
}
function compactWebProbeResult(report: Record<string, unknown> | null): Record<string, unknown> | null {
if (report === null) return null;
const dom = record(report.dom);
return {
ok: report.ok === true,
status: typeof report.status === "string" ? report.status : null,
finalUrl: typeof report.finalUrl === "string" ? report.finalUrl : null,
error: typeof report.error === "string" ? report.error : null,
actions: Array.isArray(report.actions) ? report.actions : [],
dom: {
authState: typeof dom.authState === "string" ? dom.authState : null,
requiredSelectors: record(dom.requiredSelectors),
messageCount: typeof dom.messageCount === "number" ? dom.messageCount : null,
},
failureDom: record(report.failureDom),
artifacts: record(report.artifacts),
safety: record(report.safety),
};
}
function compactCommand(command: string[]): string[] {
const scriptIndex = command.indexOf("--");
if (scriptIndex >= 0 && scriptIndex + 1 < command.length) return [...command.slice(0, scriptIndex + 1), "<script omitted>"];