feat: configure agentrun provider profiles online
This commit is contained in:
@@ -38,6 +38,7 @@ import { controlPlaneApply, controlPlanePlan, parseCleanupReleasedPvOptions, par
|
||||
import { gitMirrorStatus } from "./git-mirror";
|
||||
import { agentRunExplain, isRecord, parseGitMirrorStatusOptions, parseStatusOptions, parseTriggerOptions } from "./options";
|
||||
import { renderAgentRunControlPlaneActionSummary, renderAgentRunControlPlanePlanSummary, renderAgentRunControlPlaneStatusSummary } from "./public-exposure";
|
||||
import { providerProfileHelp, runProviderProfileCommand } from "./provider-profile";
|
||||
import { renderedCliResult } from "./render";
|
||||
import { agentRunGetKindHelp, runAgentRunResourceCommand } from "./resource-actions";
|
||||
import { runAgentRunRestCompatCommand, runGitMirrorJob, startAsyncAgentRunJob } from "./rest-bridge";
|
||||
@@ -94,6 +95,10 @@ export function agentRunHelp(): unknown {
|
||||
"bun scripts/cli.ts agentrun control-plane cleanup-released-pvs --limit 200 --confirm",
|
||||
"bun scripts/cli.ts agentrun control-plane cleanup-local-postgres --node NC01 --lane nc01-v02 --dry-run",
|
||||
"bun scripts/cli.ts agentrun control-plane cleanup-local-postgres --node NC01 --lane nc01-v02 --confirm",
|
||||
"bun scripts/cli.ts agentrun provider-profile plan --node NC01 --lane nc01-v02 --profile gpt.pika",
|
||||
"bun scripts/cli.ts agentrun provider-profile apply --node NC01 --lane nc01-v02 --profile gpt.pika --confirm",
|
||||
"bun scripts/cli.ts agentrun provider-profile status --node NC01 --lane nc01-v02 --profile gpt.pika",
|
||||
"bun scripts/cli.ts agentrun provider-profile validate --node NC01 --lane nc01-v02 --profile gpt.pika --wait",
|
||||
"bun scripts/cli.ts agentrun git-mirror status",
|
||||
"bun scripts/cli.ts agentrun git-mirror status --full",
|
||||
"bun scripts/cli.ts agentrun git-mirror sync --confirm",
|
||||
@@ -156,6 +161,7 @@ export async function runAgentRunCommand(config: UniDeskConfig | null, args: str
|
||||
if (action === "cleanup-local-postgres") return await cleanupLocalPostgres(config, parseLaneConfirmOptions(actionArgs));
|
||||
if (action === "cleanup-released-pvs") return await cleanupReleasedPvs(config, parseCleanupReleasedPvOptions(actionArgs));
|
||||
}
|
||||
if (group === "provider-profile") return await runProviderProfileCommand(config, action, actionArgs);
|
||||
if (group === "git-mirror") {
|
||||
if (action === "status") return await gitMirrorStatus(config, parseGitMirrorStatusOptions(actionArgs));
|
||||
if (action === "sync" || action === "flush") {
|
||||
@@ -299,6 +305,7 @@ export function agentRunHelpText(args: string[]): string {
|
||||
" bun scripts/cli.ts agentrun control-plane cleanup-runs --min-age-minutes 30 --limit 200 --dry-run",
|
||||
].join("\n");
|
||||
}
|
||||
if (verb === "provider-profile") return providerProfileHelp();
|
||||
if (verb === "git-mirror") {
|
||||
return [
|
||||
"Usage: bun scripts/cli.ts agentrun git-mirror <status|sync|flush> [--full|--raw|--confirm]",
|
||||
|
||||
@@ -0,0 +1,438 @@
|
||||
// SPEC: issue-1616 YAML-first AgentRun provider profile online configuration.
|
||||
// Provides a thin CLI over YAML-declared provider credential Secrets, runtime GitOps env and bounded validation.
|
||||
import type { UniDeskConfig } from "../config";
|
||||
import {
|
||||
agentRunImageArtifact,
|
||||
renderedFilesDigest,
|
||||
renderAgentRunGitopsFiles,
|
||||
} from "../agentrun-manifests";
|
||||
import {
|
||||
agentRunLaneSummary,
|
||||
agentRunProviderCredentialRefs,
|
||||
resolveAgentRunLaneTarget,
|
||||
type AgentRunLaneSecretSpec,
|
||||
type AgentRunLaneSpec,
|
||||
} from "../agentrun-lanes";
|
||||
import { refreshYamlLaneScript } from "./git-mirror";
|
||||
import type { ConfirmOptions, DisclosureOptions } from "./options";
|
||||
import { compactAgentRunLaneStatusTarget } from "./trigger";
|
||||
import {
|
||||
collectLaneSecretSources,
|
||||
createYamlLaneJobScript,
|
||||
readSecretSourceValue,
|
||||
restartYamlLaneScript,
|
||||
secretSyncScript,
|
||||
yamlLaneGitopsPublishJobManifest,
|
||||
yamlLaneGitopsPublishPayloadFromProbe,
|
||||
yamlLaneJobProbeScript,
|
||||
} from "./secrets";
|
||||
import { capture, captureJsonPayload, compactCapture, progressEvent, record, shQuote, sleep, stringOrNull } from "./utils";
|
||||
|
||||
export interface ProviderProfileOptions extends ConfirmOptions, DisclosureOptions {
|
||||
readonly node: string | null;
|
||||
readonly lane: string | null;
|
||||
readonly profile: string | null;
|
||||
readonly wait: boolean;
|
||||
readonly prompt: string | null;
|
||||
}
|
||||
|
||||
export function parseProviderProfileOptions(args: string[]): ProviderProfileOptions {
|
||||
let node: string | null = null;
|
||||
let lane: string | null = null;
|
||||
let profile: string | null = null;
|
||||
let full = false;
|
||||
let raw = false;
|
||||
let wait = false;
|
||||
let prompt: string | null = null;
|
||||
let confirm = false;
|
||||
let dryRun = false;
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === "--confirm") {
|
||||
confirm = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--dry-run") {
|
||||
dryRun = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--full") {
|
||||
full = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--raw") {
|
||||
raw = true;
|
||||
full = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--wait") {
|
||||
wait = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--node" || arg === "--lane" || arg === "--profile" || arg === "--prompt") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
|
||||
if (arg === "--node") node = value;
|
||||
if (arg === "--lane") lane = value;
|
||||
if (arg === "--profile") profile = value;
|
||||
if (arg === "--prompt") prompt = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
throw new Error(`unsupported provider-profile option: ${arg}`);
|
||||
}
|
||||
if (confirm && dryRun) throw new Error("accepts only one of --confirm or --dry-run");
|
||||
return { node, lane, profile, full, raw, wait, prompt, confirm, dryRun: dryRun || !confirm };
|
||||
}
|
||||
|
||||
export function providerProfileHelp(): string {
|
||||
return [
|
||||
"Usage: bun scripts/cli.ts agentrun provider-profile <plan|status|apply|validate> --node <NODE> --lane <lane> --profile <profile> [options]",
|
||||
"",
|
||||
"Examples:",
|
||||
" bun scripts/cli.ts agentrun provider-profile plan --node NC01 --lane nc01-v02 --profile gpt.pika",
|
||||
" bun scripts/cli.ts agentrun provider-profile status --node NC01 --lane nc01-v02 --profile gpt.pika",
|
||||
" bun scripts/cli.ts agentrun provider-profile apply --node NC01 --lane nc01-v02 --profile gpt.pika --confirm",
|
||||
" bun scripts/cli.ts agentrun provider-profile validate --node NC01 --lane nc01-v02 --profile gpt.pika --wait",
|
||||
"",
|
||||
"Boundary:",
|
||||
" apply syncs YAML-declared Secret/config, publishes runtime GitOps using the current manager image, refreshes Argo and restarts the manager; it does not rebuild images or create a PipelineRun.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export async function runProviderProfileCommand(config: UniDeskConfig, action: string | undefined, args: string[]): Promise<Record<string, unknown>> {
|
||||
const options = parseProviderProfileOptions(args);
|
||||
if (action === "plan") return providerProfilePlan(options);
|
||||
if (action === "status") return providerProfileStatus(config, options);
|
||||
if (action === "apply") return providerProfileApply(config, options);
|
||||
if (action === "validate") return providerProfileValidate(config, options);
|
||||
throw new Error(`unsupported provider-profile action: ${action ?? "(empty)"}`);
|
||||
}
|
||||
|
||||
function resolveProviderProfile(options: ProviderProfileOptions): { configPath: string; spec: AgentRunLaneSpec; profile: string; secrets: AgentRunLaneSecretSpec[] } {
|
||||
if (options.profile === null) throw new Error("provider-profile requires --profile");
|
||||
const target = resolveAgentRunLaneTarget(options);
|
||||
const secrets = target.spec.secrets.filter((secret) => secret.providerCredentialProfile === options.profile);
|
||||
if (secrets.length === 0) {
|
||||
const declared = agentRunProviderCredentialRefs(target.spec).map((item) => item.profile);
|
||||
throw new Error(`provider profile ${options.profile} is not declared in ${target.configPath}; declared profiles: ${declared.join(", ") || "(none)"}`);
|
||||
}
|
||||
return { ...target, profile: options.profile, secrets };
|
||||
}
|
||||
|
||||
function providerProfilePlan(options: ProviderProfileOptions): Record<string, unknown> {
|
||||
const { configPath, spec, profile, secrets } = resolveProviderProfile(options);
|
||||
const values = secrets.map((secret) => {
|
||||
const source = collectLaneSecretSources(spec).find((item) => item.id === secret.id);
|
||||
const sourceValue = source === undefined ? null : readSecretSourceValue(spec, source);
|
||||
return {
|
||||
id: secret.id,
|
||||
sourceRef: secret.sourceRef,
|
||||
sourceMode: secret.sourceMode,
|
||||
targetRef: secret.targetRef,
|
||||
key: secret.targetRef.key,
|
||||
codexConfig: secret.codexConfig === null ? null : codexConfigSummary(secret.codexConfig),
|
||||
sourcePath: sourceValue?.redactedPath ?? null,
|
||||
fingerprint: sourceValue?.fingerprint ?? null,
|
||||
valueBytes: sourceValue?.valueBytes ?? null,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
});
|
||||
return {
|
||||
ok: true,
|
||||
command: "agentrun provider-profile plan",
|
||||
mode: "yaml-first-online-profile",
|
||||
mutation: false,
|
||||
configPath,
|
||||
target: options.full || options.raw ? agentRunLaneSummary(spec) : compactAgentRunLaneStatusTarget(spec),
|
||||
profile: providerProfileSummary(spec, profile, secrets),
|
||||
plan: {
|
||||
secretCount: values.length,
|
||||
secrets: values,
|
||||
runtime: {
|
||||
managerDeployment: spec.runtime.managerDeployment,
|
||||
runnerEgressProxyUrl: spec.deployment.runner.egressProxyUrl,
|
||||
runnerNoProxyExtra: spec.deployment.runner.noProxyExtra,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
applySteps: ["secret-sync", "gitops-publish-current-image", "argocd-refresh-sync", "manager-rollout-restart"],
|
||||
rebuildImages: false,
|
||||
pipelineRun: false,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
next: {
|
||||
apply: `bun scripts/cli.ts agentrun provider-profile apply --node ${spec.nodeId} --lane ${spec.lane} --profile ${profile} --confirm`,
|
||||
status: `bun scripts/cli.ts agentrun provider-profile status --node ${spec.nodeId} --lane ${spec.lane} --profile ${profile}`,
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
async function providerProfileStatus(config: UniDeskConfig, options: ProviderProfileOptions): Promise<Record<string, unknown>> {
|
||||
const { configPath, spec, profile, secrets } = resolveProviderProfile(options);
|
||||
const live = await capture(config, spec.nodeKubeRoute, ["sh", "--", providerProfileLiveStatusScript(spec, secrets)]);
|
||||
const payload = captureJsonPayload(live);
|
||||
return {
|
||||
ok: live.exitCode === 0 && payload.ok !== false,
|
||||
command: "agentrun provider-profile status",
|
||||
mode: "yaml-first-online-profile",
|
||||
mutation: false,
|
||||
configPath,
|
||||
target: options.full || options.raw ? agentRunLaneSummary(spec) : compactAgentRunLaneStatusTarget(spec),
|
||||
profile: providerProfileSummary(spec, profile, secrets),
|
||||
yamlDesired: {
|
||||
secrets: secrets.map((secret) => ({
|
||||
id: secret.id,
|
||||
targetRef: secret.targetRef,
|
||||
key: secret.targetRef.key,
|
||||
codexConfig: secret.codexConfig === null ? null : codexConfigSummary(secret.codexConfig),
|
||||
valuesPrinted: false,
|
||||
})),
|
||||
runnerNoProxyExtra: spec.deployment.runner.noProxyExtra,
|
||||
runnerEgressProxyUrl: spec.deployment.runner.egressProxyUrl,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
live: payload,
|
||||
capture: compactCapture(live, { full: options.full || options.raw || live.exitCode !== 0, stdoutTailChars: 3000, stderrTailChars: 3000 }),
|
||||
next: {
|
||||
apply: `bun scripts/cli.ts agentrun provider-profile apply --node ${spec.nodeId} --lane ${spec.lane} --profile ${profile} --confirm`,
|
||||
validate: `bun scripts/cli.ts agentrun provider-profile validate --node ${spec.nodeId} --lane ${spec.lane} --profile ${profile} --wait`,
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
async function providerProfileApply(config: UniDeskConfig, options: ProviderProfileOptions): Promise<Record<string, unknown>> {
|
||||
const { configPath, spec, profile, secrets } = resolveProviderProfile(options);
|
||||
if (options.dryRun || !options.confirm) return providerProfilePlan({ ...options, dryRun: true, confirm: false });
|
||||
const secretIds = secrets.map((secret) => secret.id);
|
||||
const allSources = collectLaneSecretSources(spec);
|
||||
const selectedSources = allSources.filter((source) => secretIds.includes(source.id));
|
||||
const secretValues = selectedSources.map((source) => ({ spec: source, value: readSecretSourceValue(spec, source) }));
|
||||
const secretSync = await capture(config, spec.nodeKubeRoute, ["sh", "--", secretSyncScript(spec, secretValues.map(({ spec: item, value }) => ({ targetRef: item.targetRef, value: value.value })))]);
|
||||
const secretPayload = captureJsonPayload(secretSync);
|
||||
if (secretSync.exitCode !== 0 || secretPayload.ok === false) {
|
||||
return applyFailure(configPath, spec, profile, "secret-sync", secretPayload, secretSync);
|
||||
}
|
||||
const liveBefore = await capture(config, spec.nodeKubeRoute, ["sh", "--", currentManagerImageScript(spec)]);
|
||||
const liveBeforePayload = captureJsonPayload(liveBefore);
|
||||
const sourceCommit = stringOrNull(liveBeforePayload.sourceCommit);
|
||||
const envIdentity = stringOrNull(liveBeforePayload.envIdentity);
|
||||
const digest = stringOrNull(liveBeforePayload.digest);
|
||||
if (liveBefore.exitCode !== 0 || sourceCommit === null || envIdentity === null || digest === null) {
|
||||
return applyFailure(configPath, spec, profile, "current-image-resolve", liveBeforePayload, liveBefore);
|
||||
}
|
||||
const image = agentRunImageArtifact(spec, { sourceCommit, envIdentity, digest, status: "reused" });
|
||||
const renderedFiles = renderAgentRunGitopsFiles(spec, { sourceCommit, image });
|
||||
const publish = await publishProfileGitops(config, spec, sourceCommit, renderedFiles);
|
||||
if (publish.ok !== true) {
|
||||
return { ok: false, command: "agentrun provider-profile apply", mode: "confirmed-online-profile", mutation: true, configPath, target: compactAgentRunLaneStatusTarget(spec), profile, phase: "gitops-publish", result: publish, valuesPrinted: false };
|
||||
}
|
||||
const argo = await capture(config, spec.nodeKubeRoute, ["sh", "--", providerProfileArgoRefreshSyncScript(spec)]);
|
||||
const argoPayload = captureJsonPayload(argo);
|
||||
if (argo.exitCode !== 0 || argoPayload.ok === false) {
|
||||
return applyFailure(configPath, spec, profile, "argocd-refresh-sync", argoPayload, argo);
|
||||
}
|
||||
const restart = await capture(config, spec.nodeKubeRoute, ["sh", "--", restartYamlLaneScript(spec)]);
|
||||
const restartPayload = captureJsonPayload(restart);
|
||||
if (restart.exitCode !== 0 || restartPayload.ok === false) {
|
||||
return applyFailure(configPath, spec, profile, "manager-restart", restartPayload, restart);
|
||||
}
|
||||
const liveAfter = await capture(config, spec.nodeKubeRoute, ["sh", "--", providerProfileLiveStatusScript(spec, secrets)]);
|
||||
const liveAfterPayload = captureJsonPayload(liveAfter);
|
||||
return {
|
||||
ok: liveAfter.exitCode === 0 && liveAfterPayload.ok !== false,
|
||||
command: "agentrun provider-profile apply",
|
||||
mode: "confirmed-online-profile",
|
||||
mutation: true,
|
||||
configPath,
|
||||
target: compactAgentRunLaneStatusTarget(spec),
|
||||
profile: providerProfileSummary(spec, profile, secrets),
|
||||
steps: {
|
||||
secretSync: secretPayload,
|
||||
gitops: publish.payload,
|
||||
argo: argoPayload,
|
||||
restart: restartPayload,
|
||||
live: liveAfterPayload,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
renderedFiles: {
|
||||
count: renderedFiles.length,
|
||||
digest: renderedFilesDigest(renderedFiles),
|
||||
valuesPrinted: false,
|
||||
},
|
||||
image: { sourceCommit, envIdentity, digest, status: "reused", valuesPrinted: false },
|
||||
rebuildImages: false,
|
||||
pipelineRun: false,
|
||||
captures: options.full || options.raw || liveAfter.exitCode !== 0 ? {
|
||||
secretSync: compactCapture(secretSync, { full: options.full || options.raw || secretSync.exitCode !== 0 }),
|
||||
argo: compactCapture(argo, { full: options.full || options.raw || argo.exitCode !== 0 }),
|
||||
restart: compactCapture(restart, { full: options.full || options.raw || restart.exitCode !== 0 }),
|
||||
live: compactCapture(liveAfter, { full: options.full || options.raw || liveAfter.exitCode !== 0 }),
|
||||
} : undefined,
|
||||
next: {
|
||||
status: `bun scripts/cli.ts agentrun provider-profile status --node ${spec.nodeId} --lane ${spec.lane} --profile ${profile}`,
|
||||
validate: `bun scripts/cli.ts agentrun provider-profile validate --node ${spec.nodeId} --lane ${spec.lane} --profile ${profile} --wait`,
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
async function providerProfileValidate(config: UniDeskConfig, options: ProviderProfileOptions): Promise<Record<string, unknown>> {
|
||||
const { configPath, spec, profile, secrets } = resolveProviderProfile(options);
|
||||
const prompt = options.prompt ?? `请只回复:${profile} upstream OK`;
|
||||
const live = await capture(config, spec.nodeKubeRoute, ["sh", "--", providerProfileLiveStatusScript(spec, secrets)]);
|
||||
const livePayload = captureJsonPayload(live);
|
||||
const canRun = live.exitCode === 0 && livePayload.ok !== false;
|
||||
return {
|
||||
ok: canRun,
|
||||
command: "agentrun provider-profile validate",
|
||||
mode: "profile-runtime-preflight",
|
||||
mutation: false,
|
||||
configPath,
|
||||
target: compactAgentRunLaneStatusTarget(spec),
|
||||
profile: providerProfileSummary(spec, profile, secrets),
|
||||
promptBytes: Buffer.byteLength(prompt, "utf8"),
|
||||
live: livePayload,
|
||||
note: "This command validates YAML/live profile wiring. User-entry Web validation still runs through HWLAB web-probe so it uses the same Workbench path.",
|
||||
webProbeNext: `bun scripts/cli.ts web-probe observe command <observer> --type sendPrompt --provider ${profile} --text '${prompt.replaceAll("'", "'\\''")}'`,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function applyFailure(configPath: string, spec: AgentRunLaneSpec, profile: string, phase: string, payload: Record<string, unknown>, captureResult: Awaited<ReturnType<typeof capture>>): Record<string, unknown> {
|
||||
return {
|
||||
ok: false,
|
||||
command: "agentrun provider-profile apply",
|
||||
mode: "confirmed-online-profile",
|
||||
mutation: true,
|
||||
configPath,
|
||||
target: compactAgentRunLaneStatusTarget(spec),
|
||||
profile,
|
||||
phase,
|
||||
result: payload,
|
||||
capture: compactCapture(captureResult, { full: true, stdoutTailChars: 4000, stderrTailChars: 4000 }),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
async function publishProfileGitops(config: UniDeskConfig, spec: AgentRunLaneSpec, sourceCommit: string, files: readonly { path: string; content: string }[]): Promise<Record<string, unknown> & { ok: boolean; payload: Record<string, unknown> }> {
|
||||
const jobName = `profile-gitops-${spec.nodeId.toLowerCase()}-${spec.lane}-${Date.now().toString(36)}`.slice(0, 63);
|
||||
const manifest = yamlLaneGitopsPublishJobManifest(spec, files, jobName);
|
||||
const created = await capture(config, spec.nodeKubeRoute, ["sh", "--", createYamlLaneJobScript(spec.gitMirror.namespace, jobName, manifest)]);
|
||||
if (created.exitCode !== 0) return { ok: false, payload: { ok: false, status: "create-failed", jobName, valuesPrinted: false }, capture: compactCapture(created, { full: true }), valuesPrinted: false };
|
||||
const startedAt = Date.now();
|
||||
let polls = 0;
|
||||
let lastPayload: Record<string, unknown> = {};
|
||||
while (Date.now() - startedAt < 300_000) {
|
||||
polls += 1;
|
||||
const probe = await capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneJobProbeScript(spec.gitMirror.namespace, jobName)]);
|
||||
const probePayload = captureJsonPayload(probe);
|
||||
const publishPayload = yamlLaneGitopsPublishPayloadFromProbe(probePayload);
|
||||
if (Object.keys(publishPayload).length > 0) lastPayload = publishPayload;
|
||||
progressEvent("agentrun.provider-profile.gitops-publish.progress", { node: spec.nodeId, lane: spec.lane, sourceCommit, jobName, polls, status: stringOrNull(publishPayload.status) ?? "running", valuesPrinted: false });
|
||||
if (probePayload.succeeded === true) return { ok: publishPayload.ok === true, payload: publishPayload, jobName, polls, elapsedMs: Date.now() - startedAt, probe: compactCapture(probe), valuesPrinted: false };
|
||||
if (probePayload.failed === true) return { ok: false, payload: { ...lastPayload, ok: false, status: "failed", jobName, valuesPrinted: false }, jobName, polls, elapsedMs: Date.now() - startedAt, probe: compactCapture(probe, { full: true }), valuesPrinted: false };
|
||||
await sleep(3_000);
|
||||
}
|
||||
return { ok: false, payload: { ...lastPayload, ok: false, status: "timeout", jobName, valuesPrinted: false }, jobName, polls, elapsedMs: Date.now() - startedAt, valuesPrinted: false };
|
||||
}
|
||||
|
||||
function providerProfileSummary(spec: AgentRunLaneSpec, profile: string, secrets: readonly AgentRunLaneSecretSpec[]): Record<string, unknown> {
|
||||
const credential = agentRunProviderCredentialRefs(spec, profile)[0] ?? null;
|
||||
return {
|
||||
profile,
|
||||
declared: true,
|
||||
backendProfile: profile === "gpt.pika" ? "gpt-pika" : profile,
|
||||
secretIds: secrets.map((secret) => secret.id),
|
||||
secretRef: credential?.secretRef ?? null,
|
||||
configuredKeys: credential?.secretRef.keys ?? secrets.map((secret) => secret.targetRef.key),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function codexConfigSummary(config: NonNullable<AgentRunLaneSecretSpec["codexConfig"]>): Record<string, unknown> {
|
||||
return {
|
||||
modelProvider: config.modelProvider,
|
||||
model: config.model,
|
||||
reviewModel: config.reviewModel,
|
||||
modelProviderCount: config.modelProviders.length,
|
||||
providers: config.modelProviders.map((provider) => ({
|
||||
id: provider.id,
|
||||
baseUrl: provider.baseUrl,
|
||||
wireApi: provider.wireApi,
|
||||
requiresOpenaiAuth: provider.requiresOpenaiAuth,
|
||||
})),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function currentManagerImageScript(spec: AgentRunLaneSpec): string {
|
||||
return [
|
||||
"set -eu",
|
||||
`namespace=${shQuote(spec.runtime.namespace)}`,
|
||||
`deployment=${shQuote(spec.runtime.managerDeployment)}`,
|
||||
"kubectl -n \"$namespace\" get deployment \"$deployment\" -o json >/tmp/agentrun-provider-profile-current-manager.json",
|
||||
"node <<'NODE'",
|
||||
"const fs = require('node:fs');",
|
||||
" const j = JSON.parse(fs.readFileSync('/tmp/agentrun-provider-profile-current-manager.json', 'utf8'));",
|
||||
" const c = (j.spec?.template?.spec?.containers || [])[0] || {};",
|
||||
" const env = new Map((c.env || []).map((e) => [e.name, e.value || null]));",
|
||||
" const image = c.image || '';",
|
||||
" const digest = image.includes('@') ? image.split('@').pop() : null;",
|
||||
" console.log(JSON.stringify({ ok: Boolean(env.get('AGENTRUN_SOURCE_COMMIT') && env.get('AGENTRUN_ENV_IDENTITY') && digest), sourceCommit: env.get('AGENTRUN_SOURCE_COMMIT'), envIdentity: env.get('AGENTRUN_ENV_IDENTITY'), digest, imageStatus: 'reused', valuesPrinted: false }));",
|
||||
"NODE",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function providerProfileLiveStatusScript(spec: AgentRunLaneSpec, secrets: readonly AgentRunLaneSecretSpec[]): string {
|
||||
const refs = Buffer.from(JSON.stringify(secrets.map((secret) => secret.targetRef)), "utf8").toString("base64");
|
||||
return [
|
||||
"set -eu",
|
||||
`namespace=${shQuote(spec.runtime.namespace)}`,
|
||||
`deployment=${shQuote(spec.runtime.managerDeployment)}`,
|
||||
`refs_b64=${shQuote(refs)}`,
|
||||
"kubectl -n \"$namespace\" get deployment \"$deployment\" -o json >/tmp/agentrun-provider-profile-deploy.json",
|
||||
"REFS_B64=\"$refs_b64\" node <<'NODE'",
|
||||
"const fs = require('node:fs');",
|
||||
"const cp = require('node:child_process');",
|
||||
"const refs = JSON.parse(Buffer.from(process.env.REFS_B64 || '', 'base64').toString('utf8'));",
|
||||
"const deployment = JSON.parse(fs.readFileSync('/tmp/agentrun-provider-profile-deploy.json', 'utf8'));",
|
||||
"const container = deployment.spec?.template?.spec?.containers?.[0] || {};",
|
||||
"const env = new Map((container.env || []).map((e) => [e.name, e.value || null]));",
|
||||
"const noProxy = env.get('AGENTRUN_RUNNER_NO_PROXY_EXTRA') || '';",
|
||||
"const secretItems = refs.map((ref) => {",
|
||||
" const out = cp.spawnSync('kubectl', ['-n', ref.namespace, 'get', 'secret', ref.name, '-o', 'json'], { encoding: 'utf8' });",
|
||||
" if (out.status !== 0) return { namespace: ref.namespace, name: ref.name, key: ref.key, present: false, keyPresent: false, valuesPrinted: false };",
|
||||
" const secret = JSON.parse(out.stdout);",
|
||||
" const raw = secret.data?.[ref.key] || '';",
|
||||
" const bytes = raw ? Buffer.from(raw, 'base64').length : 0;",
|
||||
" const fingerprint = raw ? 'sha256:' + require('node:crypto').createHash('sha256').update(Buffer.from(raw, 'base64')).digest('hex') : null;",
|
||||
" return { namespace: ref.namespace, name: ref.name, key: ref.key, present: true, keyPresent: Boolean(raw), valueBytes: bytes, fingerprint, valuesPrinted: false };",
|
||||
"});",
|
||||
"console.log(JSON.stringify({ ok: secretItems.every((item) => item.keyPresent), manager: { deploymentExists: Boolean(deployment.metadata?.name), readyReplicas: deployment.status?.readyReplicas ?? null, replicas: deployment.status?.replicas ?? null, sourceCommit: env.get('AGENTRUN_SOURCE_COMMIT'), envIdentity: env.get('AGENTRUN_ENV_IDENTITY'), runnerEgressProxyUrl: env.get('AGENTRUN_RUNNER_EGRESS_PROXY_URL'), runnerNoProxyExtra: noProxy, noProxyContainsHyue: noProxy.includes('hyueapi.com'), noProxyContainsPikapython: noProxy.includes('api.pikapython.com') || noProxy.includes('.pikapython.com'), valuesPrinted: false }, secrets: { count: secretItems.length, ready: secretItems.every((item) => item.keyPresent), items: secretItems, valuesPrinted: false }, valuesPrinted: false }));",
|
||||
"NODE",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function providerProfileArgoRefreshSyncScript(spec: AgentRunLaneSpec): string {
|
||||
return [
|
||||
refreshYamlLaneScript(spec),
|
||||
`namespace=${shQuote(spec.gitops.argoNamespace)}`,
|
||||
`application=${shQuote(spec.gitops.argoApplication)}`,
|
||||
"kubectl -n \"$namespace\" patch application \"$application\" --type=merge -p '{\"operation\":{\"sync\":{\"syncStrategy\":{\"hook\":{}}}}}' >/tmp/agentrun-provider-profile-argo-sync.out 2>/tmp/agentrun-provider-profile-argo-sync.err || true",
|
||||
"for i in $(seq 1 60); do",
|
||||
" app_json=$(kubectl -n \"$namespace\" get application \"$application\" -o json 2>/dev/null || printf '{}')",
|
||||
" APP_JSON=\"$app_json\" node <<'NODE' >/tmp/agentrun-provider-profile-argo-state.json",
|
||||
"const app = JSON.parse(process.env.APP_JSON || '{}');",
|
||||
"const sync = app.status?.sync || {};",
|
||||
"const health = app.status?.health || {};",
|
||||
"const op = app.status?.operationState || null;",
|
||||
"console.log(JSON.stringify({sync: sync.status || null, health: health.status || null, revision: sync.revision || null, operationPhase: op?.phase || null, message: op?.message || null}));",
|
||||
"NODE",
|
||||
" if node -e \"const s=require('fs').readFileSync('/tmp/agentrun-provider-profile-argo-state.json','utf8'); const j=JSON.parse(s); process.exit(j.sync==='Synced' && j.health==='Healthy' && (!j.operationPhase || j.operationPhase==='Succeeded') ? 0 : 1)\"; then break; fi",
|
||||
" sleep 3",
|
||||
"done",
|
||||
"cat /tmp/agentrun-provider-profile-argo-state.json | node -e \"let s='';process.stdin.on('data',d=>s+=d);process.stdin.on('end',()=>{const j=JSON.parse(s||'{}'); console.log(JSON.stringify({ok:j.sync==='Synced' && j.health==='Healthy', ...j, valuesPrinted:false}));})\"",
|
||||
].join("\n");
|
||||
}
|
||||
@@ -93,6 +93,7 @@ async function processCommand(command) {
|
||||
case "goto": return withObserverSync(await gotoTarget(command.path || command.url || targetPath), "goto");
|
||||
case "newSession": return withObserverSync(await createSessionFromUi(), "newSession");
|
||||
case "sendPrompt": return withObserverSync(await sendPrompt(String(command.text || ""), {
|
||||
provider: command.provider,
|
||||
expectedAction: "turn",
|
||||
responsePath: "/v1/agent/chat",
|
||||
alternateResponsePaths: ["/v1/agent/chat/steer"],
|
||||
@@ -1593,9 +1594,11 @@ async function forceRecoverControlPageForCommand(reason) {
|
||||
async function sendPrompt(text, options = {}) {
|
||||
if (text.trim().length === 0) throw new Error("sendPrompt requires non-empty text");
|
||||
const responsePath = options.responsePath || "/v1/agent/chat";
|
||||
const requestedProvider = String(options.provider || "").trim();
|
||||
const controlRecovery = await ensureControlPageResponsiveForCommand("sendPrompt");
|
||||
const beforeUrl = currentPageUrl();
|
||||
const beforeEvidence = await promptSideEffectSnapshot();
|
||||
const providerSelection = requestedProvider ? await selectProvider(requestedProvider) : null;
|
||||
let editor = null;
|
||||
let composerRecovery = null;
|
||||
let editorWaitError = null;
|
||||
@@ -1618,7 +1621,7 @@ async function sendPrompt(text, options = {}) {
|
||||
if (!editor) {
|
||||
const snapshot = await controlPageLivenessSnapshot("sendPrompt-composer-editor-missing-final", 3000);
|
||||
const error = new Error("sendPrompt composer editor did not become visible");
|
||||
error.details = { beforeUrl, afterUrl: currentPageUrl(), controlRecovery, composerRecovery, snapshot, editorWaitError: errorSummary(editorWaitError), pageId, pageEpoch: controlPageEpoch, valuesRedacted: true };
|
||||
error.details = { beforeUrl, afterUrl: currentPageUrl(), requestedProvider: requestedProvider || null, providerSelection, controlRecovery, composerRecovery, snapshot, editorWaitError: errorSummary(editorWaitError), pageId, pageEpoch: controlPageEpoch, valuesRedacted: true };
|
||||
throw error;
|
||||
}
|
||||
editor = await fillComposerEditorWithRetry(editor, text, { beforeUrl, controlRecovery });
|
||||
@@ -1658,6 +1661,8 @@ async function sendPrompt(text, options = {}) {
|
||||
afterUrl: currentPageUrl(),
|
||||
textHash: sha256Text(text),
|
||||
textBytes: Buffer.byteLength(text),
|
||||
requestedProvider: requestedProvider || null,
|
||||
providerSelection,
|
||||
submitted: false,
|
||||
blocked: true,
|
||||
degradedReason: options.noActiveReason || "composer-action-mismatch",
|
||||
@@ -1708,6 +1713,8 @@ async function sendPrompt(text, options = {}) {
|
||||
afterUrl: currentPageUrl(),
|
||||
textHash: sha256Text(text),
|
||||
textBytes: Buffer.byteLength(text),
|
||||
requestedProvider: requestedProvider || null,
|
||||
providerSelection,
|
||||
chatSubmit: { status: null, statusText: null, urlPath: responsePath, waitError: chatResponse.waitError, sideEffectObserved: true, sideEffect },
|
||||
controlRecovery,
|
||||
pageId,
|
||||
@@ -1720,6 +1727,8 @@ async function sendPrompt(text, options = {}) {
|
||||
afterUrl: currentPageUrl(),
|
||||
textHash: sha256Text(text),
|
||||
textBytes: Buffer.byteLength(text),
|
||||
requestedProvider: requestedProvider || null,
|
||||
providerSelection,
|
||||
chatSubmit: { status: null, statusText: null, urlPath: responsePath, waitError: chatResponse.waitError, sideEffectObserved: false, sideEffect },
|
||||
controlRecovery,
|
||||
pageId,
|
||||
@@ -1755,6 +1764,8 @@ async function sendPrompt(text, options = {}) {
|
||||
afterUrl: currentPageUrl(),
|
||||
textHash: sha256Text(text),
|
||||
textBytes: Buffer.byteLength(text),
|
||||
requestedProvider: requestedProvider || null,
|
||||
providerSelection,
|
||||
chatSubmit: {
|
||||
status: chatStatus,
|
||||
statusText: chatResponse.statusText(),
|
||||
|
||||
Reference in New Issue
Block a user