merge: 同步主工作区并保留 Nginx 并行改动
This commit is contained in:
@@ -42,7 +42,7 @@ const agentRunClientYaml = [
|
||||
" executionPolicy:",
|
||||
" sandbox: workspace-write",
|
||||
" approval: never",
|
||||
" timeoutMs: 900000",
|
||||
" timeoutMs: 7200000",
|
||||
" network: enabled",
|
||||
" secretScope:",
|
||||
" allowCredentialEcho: false",
|
||||
|
||||
@@ -76,7 +76,7 @@ const agentRunClientYaml = [
|
||||
" executionPolicy:",
|
||||
" sandbox: workspace-write",
|
||||
" approval: never",
|
||||
" timeoutMs: 900000",
|
||||
" timeoutMs: 7200000",
|
||||
" network: enabled",
|
||||
" secretScope:",
|
||||
" allowCredentialEcho: false",
|
||||
@@ -108,7 +108,7 @@ const agentRunMinimalClientYaml = [
|
||||
" executionPolicy:",
|
||||
" sandbox: workspace-write",
|
||||
" approval: never",
|
||||
" timeoutMs: 900000",
|
||||
" timeoutMs: 7200000",
|
||||
" network: enabled",
|
||||
" secretScope:",
|
||||
" allowCredentialEcho: false",
|
||||
@@ -166,6 +166,18 @@ describe("AgentRun render-only REST client config", () => {
|
||||
expect(config.client.transport).toBe("direct-http");
|
||||
});
|
||||
|
||||
test("uses the owning YAML two-hour session hard-timeout", () => {
|
||||
const config = parseAgentRunClientConfigYaml(readFileSync("config/agentrun.yaml", "utf8"), "config/agentrun.yaml");
|
||||
expect(config.client.sessionPolicy.executionPolicy.timeoutMs).toBe(7_200_000);
|
||||
});
|
||||
|
||||
test("rejects a session hard-timeout below one hour", () => {
|
||||
const invalid = agentRunClientYaml.replace(" timeoutMs: 7200000", " timeoutMs: 3599999");
|
||||
expect(() => parseAgentRunClientConfigYaml(invalid, "config/agentrun.yaml")).toThrow(
|
||||
"client.sessionPolicy.executionPolicy.timeoutMs hard-timeout must be at least 3600000 ms",
|
||||
);
|
||||
});
|
||||
|
||||
test("uses env auth before configured file auth without exposing the key", () => {
|
||||
const config = parseAgentRunClientConfigYaml(agentRunClientYaml, "config/agentrun.yaml");
|
||||
const auth = resolveAgentRunAuth(config, { HWLAB_API_KEY: "secret-value" });
|
||||
|
||||
@@ -124,7 +124,10 @@ export function readAgentRunSessionPolicyConfig(client: Record<string, unknown>,
|
||||
stringFieldFromRecord(workspaceRef, "kind", `${pathValue}.workspaceRef`);
|
||||
stringFieldFromRecord(executionPolicy, "sandbox", `${pathValue}.executionPolicy`);
|
||||
stringFieldFromRecord(executionPolicy, "approval", `${pathValue}.executionPolicy`);
|
||||
positiveIntegerFieldFromRecord(executionPolicy, "timeoutMs", `${pathValue}.executionPolicy`);
|
||||
const timeoutMs = positiveIntegerFieldFromRecord(executionPolicy, "timeoutMs", `${pathValue}.executionPolicy`);
|
||||
if (timeoutMs < 3_600_000) {
|
||||
throw new Error(`${sourcePath}: ${pathValue}.executionPolicy.timeoutMs hard-timeout must be at least 3600000 ms`);
|
||||
}
|
||||
stringFieldFromRecord(executionPolicy, "network", `${pathValue}.executionPolicy`);
|
||||
booleanFieldFromRecord(secretScope, "allowCredentialEcho", `${pathValue}.executionPolicy.secretScope`);
|
||||
return {
|
||||
|
||||
@@ -737,6 +737,7 @@ export function sentinelPublishImageBuildShell(state: SentinelCicdState, jobName
|
||||
"failure_error_type=ChildProcessError",
|
||||
"failure_error_code=EXIT_NONZERO",
|
||||
"buildkitd_pid=''",
|
||||
"isolate_buildkit_otel() { unset TRACEPARENT TRACESTATE OTEL_EXPORTER_OTLP_ENDPOINT OTEL_EXPORTER_OTLP_TRACES_ENDPOINT OTEL_EXPORTER_OTLP_PROTOCOL OTEL_EXPORTER_OTLP_TRACES_PROTOCOL OTEL_EXPORTER_OTLP_HEADERS OTEL_EXPORTER_OTLP_TRACES_HEADERS; export OTEL_SDK_DISABLED=true OTEL_TRACES_EXPORTER=none; }",
|
||||
"cleanup_buildkitd() { if [ -n \"$buildkitd_pid\" ] && kill -0 \"$buildkitd_pid\" 2>/dev/null; then kill -TERM \"$buildkitd_pid\" 2>/dev/null || true; cleanup_attempt=0; while [ \"$cleanup_attempt\" -lt 5 ] && kill -0 \"$buildkitd_pid\" 2>/dev/null; do cleanup_attempt=$((cleanup_attempt + 1)); sleep 1; done; if kill -0 \"$buildkitd_pid\" 2>/dev/null; then kill -KILL \"$buildkitd_pid\" 2>/dev/null || true; fi; wait \"$buildkitd_pid\" 2>/dev/null || true; fi; }",
|
||||
"emit_failed() { code=$?; cleanup_buildkitd; if [ \"$code\" -ne 0 ]; then trace_id=$(printf '%s' \"${TRACEPARENT:-}\" | cut -d- -f2); printf '{\"ok\":false,\"status\":\"failed\",\"traceId\":\"%s\",\"firstBreak\":{\"code\":\"%s\",\"phase\":\"%s\",\"reason\":\"%s\",\"valuesRedacted\":true},\"error\":{\"type\":\"%s\",\"code\":\"%s\",\"phase\":\"%s\",\"exitCode\":%s,\"stderrSummary\":\"bounded step logs contain the command failure\",\"valuesRedacted\":true},\"exitCode\":%s,\"jobName\":\"%s\",\"valuesRedacted\":true}\\n' \"$trace_id\" \"$failure_code\" \"${current_phase:-image-build}\" \"$failure_reason\" \"$failure_error_type\" \"$failure_error_code\" \"${current_phase:-image-build}\" \"$code\" \"$code\" \"$job_name\" | tee -a \"$event_log\"; fi; exit \"$code\"; }",
|
||||
"trap emit_failed EXIT",
|
||||
@@ -761,14 +762,14 @@ export function sentinelPublishImageBuildShell(state: SentinelCicdState, jobName
|
||||
"buildkit_log=/workspace/buildkitd.log",
|
||||
"mkdir -p \"$(dirname \"$buildkit_socket\")\"",
|
||||
"rm -f \"$buildkit_socket\"",
|
||||
"rootlesskit buildkitd --addr \"$buildkit_address\" ${BUILDKITD_FLAGS:-} > \"$buildkit_log\" 2>&1 &",
|
||||
"( isolate_buildkit_otel; exec rootlesskit buildkitd --addr \"$buildkit_address\" ${BUILDKITD_FLAGS:-} ) > \"$buildkit_log\" 2>&1 &",
|
||||
"buildkitd_pid=$!",
|
||||
"buildkit_ready=false",
|
||||
"buildkit_probe_attempt=0",
|
||||
"while [ \"$buildkit_probe_attempt\" -lt 30 ]; do buildkit_probe_attempt=$((buildkit_probe_attempt + 1)); if [ -S \"$buildkit_socket\" ] && buildctl --addr \"$buildkit_address\" debug workers >/dev/null 2>&1; then buildkit_ready=true; break; fi; if ! kill -0 \"$buildkitd_pid\" 2>/dev/null; then break; fi; sleep 1; done",
|
||||
"while [ \"$buildkit_probe_attempt\" -lt 30 ]; do buildkit_probe_attempt=$((buildkit_probe_attempt + 1)); if [ -S \"$buildkit_socket\" ] && ( isolate_buildkit_otel; exec buildctl --addr \"$buildkit_address\" debug workers ) >/dev/null 2>&1; then buildkit_ready=true; break; fi; if ! kill -0 \"$buildkitd_pid\" 2>/dev/null; then break; fi; sleep 1; done",
|
||||
"if [ \"$buildkit_ready\" != true ]; then cat \"$buildkit_log\"; failure_code=buildkit-not-ready; failure_reason=readiness-timeout; failure_error_type=BuildKitReadinessError; failure_error_code=BUILDKIT_NOT_READY; emit_stage image-build failed \"$image_build_started_ms\"; exit 1; fi",
|
||||
"build_status=0",
|
||||
"env HTTP_PROXY=\"$image_build_http_proxy\" HTTPS_PROXY=\"$image_build_https_proxy\" ALL_PROXY=\"$image_build_all_proxy\" NO_PROXY=\"$image_build_no_proxy\" http_proxy=\"$image_build_http_proxy\" https_proxy=\"$image_build_https_proxy\" all_proxy=\"$image_build_all_proxy\" no_proxy=\"$image_build_no_proxy\" buildctl --addr \"$buildkit_address\" build --allow network.host --frontend dockerfile.v0 --local context=/workspace/source --local dockerfile=/workspace/source --opt filename=Containerfile.web-probe-sentinel --opt \"network=$image_build_network_mode\" --opt \"build-arg:HTTP_PROXY=$image_build_http_proxy\" --opt \"build-arg:HTTPS_PROXY=$image_build_https_proxy\" --opt \"build-arg:ALL_PROXY=$image_build_all_proxy\" --opt \"build-arg:NO_PROXY=$image_build_no_proxy\" --opt \"build-arg:http_proxy=$image_build_http_proxy\" --opt \"build-arg:https_proxy=$image_build_https_proxy\" --opt \"build-arg:all_proxy=$image_build_all_proxy\" --opt \"build-arg:no_proxy=$image_build_no_proxy\" --metadata-file /workspace/build-metadata.json --output \"type=image,name=$image_ref,push=true,registry.insecure=true\" > \"$build_log\" 2>&1 || build_status=$?",
|
||||
"( isolate_buildkit_otel; exec env HTTP_PROXY=\"$image_build_http_proxy\" HTTPS_PROXY=\"$image_build_https_proxy\" ALL_PROXY=\"$image_build_all_proxy\" NO_PROXY=\"$image_build_no_proxy\" http_proxy=\"$image_build_http_proxy\" https_proxy=\"$image_build_https_proxy\" all_proxy=\"$image_build_all_proxy\" no_proxy=\"$image_build_no_proxy\" buildctl --addr \"$buildkit_address\" build --allow network.host --frontend dockerfile.v0 --local context=/workspace/source --local dockerfile=/workspace/source --opt filename=Containerfile.web-probe-sentinel --opt \"network=$image_build_network_mode\" --opt \"build-arg:HTTP_PROXY=$image_build_http_proxy\" --opt \"build-arg:HTTPS_PROXY=$image_build_https_proxy\" --opt \"build-arg:ALL_PROXY=$image_build_all_proxy\" --opt \"build-arg:NO_PROXY=$image_build_no_proxy\" --opt \"build-arg:http_proxy=$image_build_http_proxy\" --opt \"build-arg:https_proxy=$image_build_https_proxy\" --opt \"build-arg:all_proxy=$image_build_all_proxy\" --opt \"build-arg:no_proxy=$image_build_no_proxy\" --metadata-file /workspace/build-metadata.json --output \"type=image,name=$image_ref,push=true,registry.insecure=true\" ) > \"$build_log\" 2>&1 || build_status=$?",
|
||||
"cat \"$build_log\"",
|
||||
"image_build_finished_ms=$(now_ms)",
|
||||
"write_meta image_build_finished_ms \"$image_build_finished_ms\"",
|
||||
|
||||
@@ -78,9 +78,14 @@ describe("PaC 失败证据合同", () => {
|
||||
},
|
||||
} as unknown as SentinelCicdState, "sentinel-test");
|
||||
expect(shell).toContain("mkdir -p \"$(dirname \"$buildkit_socket\")\"");
|
||||
expect(shell).toContain("rootlesskit buildkitd --addr");
|
||||
expect(shell).toContain("emit_stage() {");
|
||||
expect(shell).toContain('${TRACEPARENT:-}');
|
||||
expect(shell).toContain("unset TRACEPARENT TRACESTATE OTEL_EXPORTER_OTLP_ENDPOINT OTEL_EXPORTER_OTLP_TRACES_ENDPOINT OTEL_EXPORTER_OTLP_PROTOCOL OTEL_EXPORTER_OTLP_TRACES_PROTOCOL OTEL_EXPORTER_OTLP_HEADERS OTEL_EXPORTER_OTLP_TRACES_HEADERS");
|
||||
expect(shell).toContain("export OTEL_SDK_DISABLED=true OTEL_TRACES_EXPORTER=none");
|
||||
expect(shell).toContain("exec rootlesskit buildkitd --addr");
|
||||
expect(shell).not.toMatch(/(^|\n)buildkitd --addr/);
|
||||
expect(shell).toContain("debug workers");
|
||||
expect(shell).toContain("exec buildctl --addr \"$buildkit_address\" debug workers");
|
||||
expect(shell).toContain("exec env HTTP_PROXY=\"$image_build_http_proxy\"");
|
||||
expect(shell).toContain("failure_error_type=BuildKitReadinessError");
|
||||
expect(shell).toContain("failure_error_code=BUILDKIT_NOT_READY");
|
||||
expect(shell).toContain("cleanup_buildkitd");
|
||||
|
||||
@@ -79,3 +79,7 @@ export function desiredAccountTempUnschedulableMap(pool: CodexPoolConfig): Recor
|
||||
}
|
||||
return policies;
|
||||
}
|
||||
|
||||
export function desiredDefaultAccountTempUnschedulable(pool: CodexPoolConfig): Record<string, unknown> {
|
||||
return renderSub2ApiTempUnschedulableCredentials(pool.defaultTempUnschedulable);
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@ import { collectCodexProfiles, readCodexPoolConfig } from "./config";
|
||||
import { codexPoolSentinelProbeConfigFingerprint, fingerprint } from "./config-utils";
|
||||
import { apiKeyPreview, codexConsumerBaseUrl, fetchPoolApiKey, probePublicModels, validatePublicGatewayWithKey, writeLocalCodexConfig } from "./local-codex";
|
||||
import { manualBindingSourcePlan, poolTarget, prepareTargetPublicExposureSecret, protectedManualAccountNamesForTarget, resolvedManualAccountProtections, secretMaterialSummary, sentinelProfileSecrets, targetFrpPublicExposure, targetPublicExposureApplyScript, targetPublicExposureSummary } from "./public-exposure";
|
||||
import { codexPoolConfigSummary, compactProfile, compactSentinelProbeResult, redactProfile, renderSub2ApiTempUnschedulableCredentials } from "./redaction";
|
||||
import { codexPoolConfigSummary, compactProfile, compactRuntimeAccountState, compactSentinelProbeResult, redactProfile, renderSub2ApiTempUnschedulableCredentials } from "./redaction";
|
||||
import { boolField, capture, compactCapture, parseJsonOutput, runRemoteCodexPoolScript } from "./remote";
|
||||
import { cleanupProbesScript, sentinelImageBuildScript, sentinelImageStatusScript, sentinelProbeScript, sentinelReportScript, syncScript, traceScript, validateScript } from "./remote-scripts";
|
||||
import { cleanupProbesScript, runtimeStateScript, sentinelImageBuildScript, sentinelImageStatusScript, sentinelProbeScript, sentinelReportScript, syncScript, traceScript, validateScript } from "./remote-scripts";
|
||||
import { codexPoolSyncSummary, codexPoolValidationSummary, renderCodexPoolPlan, renderCodexPoolSentinelProbeResult, renderCodexPoolSyncResult, renderCodexPoolValidateResult, renderSentinelReport, renderTraceReport, renderedCliResult } from "./render";
|
||||
import { codexPoolRuntimeTarget, defaultCodexPoolRuntimeTargetId, targetFlag } from "./runtime-target";
|
||||
import { codexPoolConfigPath, serviceName, sub2apiConfigPath } from "./types";
|
||||
@@ -317,6 +317,35 @@ export async function codexPoolValidate(config: UniDeskConfig, options: Disclosu
|
||||
return options.full ? response : renderCodexPoolValidateResult(response);
|
||||
}
|
||||
|
||||
export async function codexPoolRuntimeState(config: UniDeskConfig, options: DisclosureOptions): Promise<Record<string, unknown>> {
|
||||
const pool = readCodexPoolConfig();
|
||||
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
|
||||
const result = await runRemoteCodexPoolScript(config, "runtime-state", runtimeStateScript(pool, runtimeTarget), runtimeTarget);
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
if (options.raw) {
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
action: "platform-infra-sub2api-codex-pool-runtime-state",
|
||||
remote: compactCapture(result, { full: true }),
|
||||
parsed,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
action: "platform-infra-sub2api-codex-pool-runtime-state",
|
||||
target: {
|
||||
id: runtimeTarget.id,
|
||||
route: runtimeTarget.route,
|
||||
namespace: runtimeTarget.namespace,
|
||||
runtimeMode: runtimeTarget.runtimeMode,
|
||||
},
|
||||
state: options.full ? parsed : compactRuntimeAccountState(parsed),
|
||||
remote: compactCapture(result, { full: result.exitCode !== 0 || parsed === null }),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
export async function codexPoolTrace(config: UniDeskConfig, options: TraceOptions): Promise<Record<string, unknown> | RenderedCliResult> {
|
||||
const pool = readCodexPoolConfig();
|
||||
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
|
||||
|
||||
@@ -22,7 +22,7 @@ import { parseEnvFile, readTextFile, redactRepoPath, requiredEnvValue } from "..
|
||||
import { runSshCommandCapture, type SshCaptureResult } from "../ssh";
|
||||
|
||||
import type { ConfirmOptions, DisclosureOptions, SentinelImageOptions, SentinelProbeOptions, SentinelReportOptions, SyncOptions, TraceOptions } from "./types";
|
||||
import { codexPoolCleanupProbes, codexPoolConfigureLocal, codexPoolExpose, codexPoolPlan, codexPoolSentinelImage, codexPoolSentinelProbe, codexPoolSentinelReport, codexPoolSync, codexPoolTrace, codexPoolValidate } from "./actions";
|
||||
import { codexPoolCleanupProbes, codexPoolConfigureLocal, codexPoolExpose, codexPoolPlan, codexPoolRuntimeState, codexPoolSentinelImage, codexPoolSentinelProbe, codexPoolSentinelReport, codexPoolSync, codexPoolTrace, codexPoolValidate } from "./actions";
|
||||
import { renderCodexPoolPlan } from "./render";
|
||||
import { defaultCodexPoolRuntimeTargetId } from "./runtime-target";
|
||||
import { codexPoolHelp } from "./types";
|
||||
@@ -36,6 +36,7 @@ export async function runCodexPoolCommand(config: UniDeskConfig, args: string[])
|
||||
}
|
||||
if (action === "sync") return await codexPoolSync(config, parseSyncOptions(args.slice(1)));
|
||||
if (action === "validate") return await codexPoolValidate(config, parseDisclosureOptions(args.slice(1)));
|
||||
if (action === "runtime-state") return await codexPoolRuntimeState(config, parseDisclosureOptions(args.slice(1)));
|
||||
if (action === "trace") return await codexPoolTrace(config, parseTraceOptions(args.slice(1)));
|
||||
if (action === "sentinel-image") return await codexPoolSentinelImage(config, parseSentinelImageOptions(args.slice(1)));
|
||||
if (action === "sentinel-probe") return await codexPoolSentinelProbe(config, parseSentinelProbeOptions(args.slice(1)));
|
||||
|
||||
@@ -188,6 +188,43 @@ export function pickSummaryFields(item: Record<string, unknown>, keys: string[])
|
||||
return result;
|
||||
}
|
||||
|
||||
export function compactRuntimeAccountState(parsed: Record<string, unknown> | null): Record<string, unknown> | null {
|
||||
if (parsed === null) return null;
|
||||
const state = isRecord(parsed.state) ? parsed.state : {};
|
||||
const accounts = recordArray(state.accounts).map((item) => {
|
||||
const temp = isRecord(item.tempUnschedulable) ? item.tempUnschedulable : {};
|
||||
const poolMode = isRecord(item.poolMode) ? item.poolMode : {};
|
||||
const optimization = isRecord(item.optimization) ? item.optimization : {};
|
||||
return {
|
||||
...pickSummaryFields(item, ["accountName", "accountId", "type", "status", "schedulable", "proxyId"]),
|
||||
tempEnabled: temp.enabled,
|
||||
tempStatusCodes: temp.statusCodes,
|
||||
tempActive: temp.active,
|
||||
poolModeEnabled: poolMode.enabled,
|
||||
tempUnschedulableNeedsAlignment: optimization.tempUnschedulableNeedsAlignment,
|
||||
credentialsPrinted: false,
|
||||
};
|
||||
});
|
||||
const runtimeImage = isRecord(parsed.runtimeImage) ? parsed.runtimeImage : {};
|
||||
return {
|
||||
ok: parsed.ok,
|
||||
mode: parsed.mode,
|
||||
namespace: parsed.namespace,
|
||||
serviceDns: parsed.serviceDns,
|
||||
appPod: parsed.appPod,
|
||||
runtimeImage: pickSummaryFields(runtimeImage, ["container", "image", "ready", "restartCount", "startedAt", "health"]),
|
||||
group: state.group,
|
||||
yamlBaseline: state.yamlBaseline,
|
||||
summary: state.summary,
|
||||
accounts,
|
||||
disclosure: {
|
||||
full: "bun scripts/cli.ts platform-infra sub2api codex-pool runtime-state --full",
|
||||
raw: "bun scripts/cli.ts platform-infra sub2api codex-pool runtime-state --raw",
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function compactStatusBlock(block: unknown, keys: string[]): Record<string, unknown> | null {
|
||||
if (!isRecord(block)) return null;
|
||||
const items = recordArray(block.items);
|
||||
|
||||
@@ -22,7 +22,7 @@ import { parseEnvFile, readTextFile, redactRepoPath, requiredEnvValue } from "..
|
||||
import { runSshCommandCapture, type SshCaptureResult } from "../ssh";
|
||||
|
||||
import type { CodexPoolConfig, CodexPoolRuntimeTarget } from "./types";
|
||||
import { desiredAccountCapacityMap, desiredAccountLoadFactorMap, desiredAccountTempUnschedulableMap, desiredAccountWebSocketsV2ModeMap } from "./accounts";
|
||||
import { desiredAccountCapacityMap, desiredAccountLoadFactorMap, desiredAccountTempUnschedulableMap, desiredAccountWebSocketsV2ModeMap, desiredDefaultAccountTempUnschedulable } from "./accounts";
|
||||
import { resolvedManualAccountProtections } from "./public-exposure";
|
||||
import { fieldManager } from "./types";
|
||||
|
||||
@@ -30,7 +30,7 @@ function pyJson(value: unknown): string {
|
||||
return `json.loads(${JSON.stringify(JSON.stringify(value))})`;
|
||||
}
|
||||
|
||||
export function remotePythonScript(mode: "sync" | "validate" | "trace" | "cleanup-probes" | "sentinel-probe", encodedPayload: string, pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): string {
|
||||
export function remotePythonScript(mode: "sync" | "validate" | "runtime-state" | "trace" | "cleanup-probes" | "sentinel-probe", encodedPayload: string, pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): string {
|
||||
const hostDockerEnvPath = target.runtimeMode === "host-docker" ? target.hostDockerEnvPath : null;
|
||||
return `
|
||||
set -u
|
||||
@@ -75,6 +75,7 @@ EXPECTED_ACCOUNT_CAPACITIES = ${pyJson(desiredAccountCapacityMap(pool))}
|
||||
EXPECTED_ACCOUNT_LOAD_FACTORS = ${pyJson(desiredAccountLoadFactorMap(pool))}
|
||||
EXPECTED_ACCOUNT_WS_MODES = json.loads(${JSON.stringify(JSON.stringify(desiredAccountWebSocketsV2ModeMap(pool)))})
|
||||
EXPECTED_ACCOUNT_TEMP_UNSCHEDULABLE = json.loads(${JSON.stringify(JSON.stringify(desiredAccountTempUnschedulableMap(pool)))})
|
||||
EXPECTED_DEFAULT_TEMP_UNSCHEDULABLE = json.loads(${JSON.stringify(JSON.stringify(desiredDefaultAccountTempUnschedulable(pool)))})
|
||||
MANUAL_ACCOUNT_PROTECTIONS = json.loads(${JSON.stringify(JSON.stringify(resolvedManualAccountProtections(pool, target)))})
|
||||
SENTINEL_CONFIG = json.loads(${JSON.stringify(JSON.stringify(pool.sentinel))})
|
||||
TARGET_EGRESS_PROXY = json.loads(${JSON.stringify(JSON.stringify(target.egressProxy))})
|
||||
@@ -2436,6 +2437,134 @@ def account_temp_unschedulable_status(token):
|
||||
"valuesPrinted": False,
|
||||
}
|
||||
|
||||
def normalized_int(value):
|
||||
if isinstance(value, bool):
|
||||
return None
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
if isinstance(value, float):
|
||||
return int(value)
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return int(value.strip())
|
||||
except ValueError:
|
||||
return None
|
||||
return None
|
||||
|
||||
def normalized_status_codes(value):
|
||||
if not isinstance(value, list):
|
||||
return None
|
||||
codes = []
|
||||
for item in value:
|
||||
code = normalized_int(item)
|
||||
if code is not None and 100 <= code <= 599 and code not in codes:
|
||||
codes.append(code)
|
||||
return sorted(codes)
|
||||
|
||||
def temp_unschedulable_active(until):
|
||||
if not isinstance(until, str) or not until.strip():
|
||||
return False
|
||||
try:
|
||||
parsed = datetime.fromisoformat(until.replace("Z", "+00:00"))
|
||||
if parsed.tzinfo is None:
|
||||
parsed = parsed.replace(tzinfo=timezone.utc)
|
||||
return parsed > datetime.now(timezone.utc)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def account_runtime_state(token):
|
||||
group = next((item for item in list_groups(token) if item.get("name") == POOL_GROUP_NAME), None)
|
||||
if not isinstance(group, dict) or group.get("id") is None:
|
||||
return {
|
||||
"ok": False,
|
||||
"group": {"name": POOL_GROUP_NAME, "id": None, "found": False},
|
||||
"accounts": [],
|
||||
"error": "pool-group-not-found",
|
||||
"valuesPrinted": False,
|
||||
}
|
||||
baseline = normalize_temp_unschedulable_credentials(EXPECTED_DEFAULT_TEMP_UNSCHEDULABLE)
|
||||
baseline_codes = sorted(set(rule.get("error_code") for rule in baseline["rules"] if isinstance(rule.get("error_code"), int)))
|
||||
protected_names = set(item.get("accountName") for item in MANUAL_ACCOUNT_PROTECTIONS if isinstance(item, dict) and isinstance(item.get("accountName"), str))
|
||||
accounts = list_accounts_for_group(token, group["id"])
|
||||
items = []
|
||||
for account in sorted(accounts, key=lambda item: str(item.get("name") or "")):
|
||||
detail = get_account_detail(token, account)
|
||||
credentials = runtime_account_credentials(detail)
|
||||
runtime_policy = normalize_temp_unschedulable_credentials(credentials)
|
||||
runtime_codes = sorted(set(rule.get("error_code") for rule in runtime_policy["rules"] if isinstance(rule.get("error_code"), int)))
|
||||
name = detail.get("name") or account.get("name")
|
||||
pool_mode = bool_value(credentials.get("pool_mode"))
|
||||
temp_until = detail.get("temp_unschedulable_until") or detail.get("tempUnschedulableUntil")
|
||||
temp_reason = detail.get("temp_unschedulable_reason") or detail.get("tempUnschedulableReason") or ""
|
||||
origin = "yaml-managed" if name in EXPECTED_ACCOUNT_TEMP_UNSCHEDULABLE else ("yaml-protected-manual" if name in protected_names else "runtime-manual")
|
||||
supports_policy = (detail.get("type") or account.get("type")) == "apikey"
|
||||
policy_matches = runtime_policy == baseline if supports_policy else None
|
||||
missing_codes = [code for code in baseline_codes if code not in runtime_codes] if supports_policy else []
|
||||
items.append({
|
||||
"accountName": name,
|
||||
"accountId": detail.get("id") or account.get("id"),
|
||||
"origin": origin,
|
||||
"platform": detail.get("platform") or account.get("platform"),
|
||||
"type": detail.get("type") or account.get("type"),
|
||||
"status": detail.get("status") or account.get("status"),
|
||||
"schedulable": detail.get("schedulable") if detail.get("schedulable") is not None else account.get("schedulable"),
|
||||
"proxyId": detail.get("proxy_id") if detail.get("proxy_id") is not None else account.get("proxy_id"),
|
||||
"priority": detail.get("priority") if detail.get("priority") is not None else account.get("priority"),
|
||||
"concurrency": detail.get("concurrency") if detail.get("concurrency") is not None else account.get("concurrency"),
|
||||
"currentConcurrency": account.get("current_concurrency"),
|
||||
"baseUrl": normalize_runtime_base_url(credentials.get("base_url")),
|
||||
"tempUnschedulable": {
|
||||
"enabled": runtime_policy["enabled"],
|
||||
"ruleCount": len(runtime_policy["rules"]),
|
||||
"statusCodes": runtime_codes,
|
||||
"rules": summarize_temp_unschedulable_rules(runtime_policy["rules"]),
|
||||
"until": temp_until,
|
||||
"reasonPreview": text(str(temp_reason), 500) if temp_reason else "",
|
||||
"stateRecorded": temp_until is not None or bool(temp_reason),
|
||||
"active": temp_unschedulable_active(temp_until),
|
||||
"matchesYamlBaseline": policy_matches,
|
||||
"missingYamlBaselineStatusCodes": missing_codes,
|
||||
},
|
||||
"poolMode": {
|
||||
"enabled": pool_mode,
|
||||
"retryCountConfigured": normalized_int(credentials.get("pool_mode_retry_count")) if "pool_mode_retry_count" in credentials else None,
|
||||
"retryStatusCodesConfigured": normalized_status_codes(credentials.get("pool_mode_retry_status_codes")) if "pool_mode_retry_status_codes" in credentials else None,
|
||||
},
|
||||
"optimization": {
|
||||
"tempUnschedulableNeedsAlignment": supports_policy and runtime_policy != baseline,
|
||||
"poolModeShouldBeDisabled": pool_mode,
|
||||
},
|
||||
"credentialsPrinted": False,
|
||||
})
|
||||
temp_alignment = [item["accountName"] for item in items if item["optimization"]["tempUnschedulableNeedsAlignment"]]
|
||||
pool_mode_enabled = [item["accountName"] for item in items if item["poolMode"]["enabled"]]
|
||||
return {
|
||||
"ok": True,
|
||||
"group": {
|
||||
"name": POOL_GROUP_NAME,
|
||||
"id": group.get("id"),
|
||||
"found": True,
|
||||
"platform": group.get("platform"),
|
||||
"status": group.get("status"),
|
||||
},
|
||||
"yamlBaseline": {
|
||||
"enabled": baseline["enabled"],
|
||||
"ruleCount": len(baseline["rules"]),
|
||||
"statusCodes": baseline_codes,
|
||||
},
|
||||
"summary": {
|
||||
"accountCount": len(items),
|
||||
"tempUnschedulableEnabledCount": sum(1 for item in items if item["tempUnschedulable"]["enabled"]),
|
||||
"tempUnschedulableNeedsAlignmentCount": len(temp_alignment),
|
||||
"tempUnschedulableNeedsAlignment": temp_alignment,
|
||||
"poolModeEnabledCount": len(pool_mode_enabled),
|
||||
"poolModeEnabled": pool_mode_enabled,
|
||||
"optimizationAvailable": bool(temp_alignment or pool_mode_enabled),
|
||||
},
|
||||
"accounts": items,
|
||||
"valuesPrinted": False,
|
||||
}
|
||||
|
||||
def account_capacity_status(token):
|
||||
accounts = list_accounts(token)
|
||||
by_name = {item.get("name"): item for item in accounts if isinstance(item.get("name"), str)}
|
||||
@@ -2776,6 +2905,21 @@ def run_validate():
|
||||
"validation": {"gatewayModels": gateway, "gatewayResponses": responses_smoke, "gatewayResponsesRecent": responses_evidence, "gatewayCompactRecent": compact_evidence},
|
||||
}
|
||||
|
||||
def run_runtime_state():
|
||||
admin_email, token, admin_compliance = login()
|
||||
state = account_runtime_state(token)
|
||||
return {
|
||||
"ok": state.get("ok") is True,
|
||||
"mode": "runtime-state",
|
||||
"namespace": NAMESPACE,
|
||||
"serviceDns": SERVICE_DNS,
|
||||
"appPod": APP_POD,
|
||||
"admin": {"email": admin_email, "tokenPrinted": False, "compliance": admin_compliance},
|
||||
"runtimeImage": app_pod_runtime_image(),
|
||||
"state": state,
|
||||
"valuesPrinted": False,
|
||||
}
|
||||
|
||||
def parse_log_line(line):
|
||||
json_start = line.find("{")
|
||||
if json_start < 0:
|
||||
@@ -3286,6 +3430,8 @@ def run_cleanup_probes():
|
||||
try:
|
||||
if MODE == "sync":
|
||||
result = run_sync()
|
||||
elif MODE == "runtime-state":
|
||||
result = run_runtime_state()
|
||||
elif MODE == "trace":
|
||||
result = run_trace()
|
||||
elif MODE == "cleanup-probes":
|
||||
|
||||
@@ -35,6 +35,10 @@ export function validateScript(pool: CodexPoolConfig, target: CodexPoolRuntimeTa
|
||||
return remotePythonScript("validate", "", pool, target);
|
||||
}
|
||||
|
||||
export function runtimeStateScript(pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): string {
|
||||
return remotePythonScript("runtime-state", "", pool, target);
|
||||
}
|
||||
|
||||
export function traceScript(pool: CodexPoolConfig, options: TraceOptions, target: CodexPoolRuntimeTarget): string {
|
||||
const encoded = Buffer.from(JSON.stringify({
|
||||
requestId: options.requestId,
|
||||
|
||||
@@ -28,7 +28,7 @@ export async function capture(config: UniDeskConfig, target: string, args: strin
|
||||
return await runSshCommandCapture(config, target, args, input);
|
||||
}
|
||||
|
||||
export type RemoteCodexPoolMode = "sync" | "validate" | "sentinel-probe" | "sentinel-image-status" | "sentinel-image-build";
|
||||
export type RemoteCodexPoolMode = "sync" | "validate" | "runtime-state" | "sentinel-probe" | "sentinel-image-status" | "sentinel-image-build";
|
||||
|
||||
export async function runRemoteCodexPoolScript(config: UniDeskConfig, mode: RemoteCodexPoolMode, script: string, target = codexPoolRuntimeTarget()): Promise<SshCaptureResult> {
|
||||
const jobName = `codex-pool-${mode}-${Date.now().toString(36)}`.slice(0, 63);
|
||||
@@ -83,6 +83,7 @@ export async function runRemoteCodexPoolScript(config: UniDeskConfig, mode: Remo
|
||||
|
||||
export function codexPoolModeCommand(mode: RemoteCodexPoolMode): string {
|
||||
if (mode === "sync") return "bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm";
|
||||
if (mode === "runtime-state") return "bun scripts/cli.ts platform-infra sub2api codex-pool runtime-state";
|
||||
if (mode === "sentinel-probe") return "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-probe --account <accountName> --confirm";
|
||||
if (mode === "sentinel-image-status") return "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status";
|
||||
if (mode === "sentinel-image-build") return "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build --confirm";
|
||||
|
||||
@@ -325,13 +325,14 @@ export function codexPoolHelp(): unknown {
|
||||
const pool = readCodexPoolConfig();
|
||||
const runtimeTarget = codexPoolRuntimeTarget();
|
||||
return {
|
||||
command: "platform-infra sub2api codex-pool plan|sync|validate|trace|sentinel-image|sentinel-probe|sentinel-report|cleanup-probes|expose|configure-local",
|
||||
command: "platform-infra sub2api codex-pool plan|sync|validate|runtime-state|trace|sentinel-image|sentinel-probe|sentinel-report|cleanup-probes|expose|configure-local",
|
||||
output: "json, except trace and sentinel-report default to low-noise text tables",
|
||||
usage: [
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool plan",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool plan --target D601",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool sync [--target D601] --confirm [--prune-removed]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool validate [--target D601] [--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime-state [--target PK01] [--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool trace [--target D601] --request-id <requestId> [--since 24h|--tail 20000|--context-seconds 300|--show-lines|--raw]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status [--target D601]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build [--target D601] --confirm",
|
||||
|
||||
Reference in New Issue
Block a user