159 lines
12 KiB
TypeScript
159 lines
12 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { rootPath } from "./src/config";
|
|
import { codexPoolHelp, defaultCodexTempUnschedulablePolicy } from "./src/platform-infra-sub2api-codex";
|
|
import {
|
|
codexPoolSentinelRuntimeImage,
|
|
defaultCodexPoolSentinelConfig,
|
|
readCodexPoolSentinelConfig,
|
|
renderCodexPoolSentinelManifest,
|
|
sentinelContainerShellCommand,
|
|
sentinelRunnerPython,
|
|
} from "./src/platform-infra-sub2api-codex-sentinel";
|
|
|
|
function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
|
|
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
|
|
}
|
|
|
|
const configPath = rootPath("config", "platform-infra", "sub2api-codex-pool.yaml");
|
|
const sentinelDockerfilePath = rootPath("src", "components", "platform-infra", "sub2api", "sentinel.Dockerfile");
|
|
const parsed = Bun.YAML.parse(readFileSync(configPath, "utf8")) as {
|
|
sentinel?: unknown;
|
|
pool?: { defaultTempUnschedulable?: { rules?: Array<{ statusCode?: unknown }> } };
|
|
};
|
|
const sentinel = readCodexPoolSentinelConfig(parsed.sentinel, defaultCodexPoolSentinelConfig(), configPath);
|
|
const sentinelRuntimeImage = codexPoolSentinelRuntimeImage(sentinel);
|
|
const sentinelDockerfile = readFileSync(sentinelDockerfilePath, "utf8");
|
|
const yamlTempUnschedulableRules = parsed.pool?.defaultTempUnschedulable?.rules ?? [];
|
|
|
|
assertCondition(sentinel.monitor.enabled === true, "sentinel monitor must be enabled for marker-only guard rollout", sentinel);
|
|
assertCondition(sentinel.actions.enabled === true, "sentinel actions must be enabled so marker-only guard can freeze and recover accounts", sentinel);
|
|
assertCondition(!yamlTempUnschedulableRules.some((rule) => rule.statusCode === 200), "native Sub2API temp-unschedulable policy must not classify HTTP 200 bodies; marker-only sentinel owns 200 semantic failures", yamlTempUnschedulableRules);
|
|
assertCondition(!defaultCodexTempUnschedulablePolicy().rules.some((rule) => rule.statusCode === 200), "default temp-unschedulable policy must not reintroduce HTTP 200 body classifiers", defaultCodexTempUnschedulablePolicy());
|
|
assertCondition(!("freezeOnMarkerMismatch" in sentinel.actions), "sentinel must not keep a marker-specific freeze branch; marker match is the only health standard", sentinel.actions);
|
|
assertCondition(!("freezeOnTransportError" in sentinel.actions), "sentinel must not keep a transport-specific freeze branch; non-marker results all use the same freeze state machine", sentinel.actions);
|
|
assertCondition(sentinel.endpoint === "responses", "v1 sentinel must target OpenAI Responses only", sentinel);
|
|
assertCondition(sentinel.model === "gpt-5.5", "v1 sentinel must use GPT-5.5", sentinel);
|
|
assertCondition(sentinel.probe.maxOutputTokens > 0 && sentinel.probe.maxOutputTokens <= 16, "sentinel local stream capture limit must be tightly capped", sentinel.probe);
|
|
assertCondition(!("maxResponseBytes" in sentinel.probe), "sentinel must not use hand-rolled response byte parsing for OpenAI model probes", sentinel.probe);
|
|
assertCondition(sentinel.probe.userAgent === "Go-http-client/1.1", "sentinel default User-Agent must match Sub2API net/http account test shape", sentinel.probe);
|
|
assertCondition(sentinel.sdk.openaiPythonVersion === "2.41.1", "sentinel must pin the OpenAI Python SDK version in YAML", sentinel.sdk);
|
|
assertCondition(!("concurrency" in sentinel.probe), "sentinel must not cap probe concurrency; all due accounts are probed concurrently", sentinel.probe);
|
|
assertCondition(!("maxAccountsPerRun" in sentinel.probe), "sentinel must not cap accounts per run; all due accounts are eligible", sentinel.probe);
|
|
assertCondition(sentinel.cadence.successInitialIntervalMinutes === 1, "success trust backoff must start at 1 minute", sentinel.cadence);
|
|
assertCondition(sentinel.cadence.successMaxIntervalMinutes === 20, "success trust backoff must cap at 20 minutes", sentinel.cadence);
|
|
assertCondition(sentinel.freeze.initialTtlMinutes === 2, "freeze backoff must start at 2 minutes", sentinel.freeze);
|
|
assertCondition(sentinel.freeze.maxTtlMinutes === 120, "freeze backoff must cap at 2 hours", sentinel.freeze);
|
|
assertCondition(!("budget" in sentinel), "sentinel must not use token budgets as a probe gate; usage is recorded only", sentinel);
|
|
|
|
const manifest = renderCodexPoolSentinelManifest(sentinel, [
|
|
{
|
|
accountName: "unidesk-codex-example",
|
|
profile: "example",
|
|
baseUrl: "https://example.invalid/v1",
|
|
apiKey: "sk-test-secret",
|
|
upstreamUserAgent: null,
|
|
},
|
|
], {
|
|
namespace: "platform-infra",
|
|
serviceName: "sub2api",
|
|
serviceDns: "sub2api.platform-infra.svc.cluster.local:8080",
|
|
appSecretName: "sub2api-secrets",
|
|
});
|
|
|
|
assertCondition(manifest.includes("kind: CronJob"), "sentinel manifest must render a CronJob", manifest.slice(0, 1000));
|
|
assertCondition(manifest.includes("concurrencyPolicy: Forbid"), "sentinel CronJob must forbid overlapping runs", manifest);
|
|
assertCondition(manifest.includes("suspend: false"), "monitor.enabled=true must unsuspend the CronJob", manifest);
|
|
assertCondition(manifest.includes("kind: ServiceAccount") && manifest.includes("kind: Role") && manifest.includes("kind: RoleBinding"), "sentinel manifest must include minimal RBAC", manifest);
|
|
assertCondition(manifest.includes("sub2api-account-sentinel-state"), "sentinel manifest must reference the state ConfigMap", manifest);
|
|
assertCondition(manifest.includes("\"enabled\": true"), "sentinel manifest must preserve actions.enabled=true in config.json", manifest);
|
|
assertCondition(!manifest.includes("sk-test-secret"), "sentinel manifest must not expose upstream credentials as plaintext", manifest);
|
|
assertCondition(manifest.includes("profiles.json:"), "sentinel credentials Secret must include the profiles payload as Secret data", manifest);
|
|
assertCondition(manifest.includes("\"budgetMode\": \"record-only\""), "sentinel runner must expose record-only budget/accounting mode", manifest);
|
|
assertCondition(manifest.includes("max_workers=max(1, len(due))"), "sentinel runner must probe all due accounts concurrently", manifest);
|
|
assertCondition(manifest.includes(`image: ${sentinelRuntimeImage.runtimeImage}`), "sentinel manifest must use the reusable prebuilt runtime image", { image: sentinelRuntimeImage.runtimeImage, manifest });
|
|
assertCondition(!manifest.includes("transport-failed-no-freeze"), "sentinel runner must not exempt transport failures from marker-based freezing", manifest);
|
|
const command = sentinelContainerShellCommand(sentinel);
|
|
assertCondition(command.includes("openai-python-version-mismatch"), "sentinel command must fail fast when the image SDK version does not match YAML", command);
|
|
assertCondition(!command.includes("pip install") && !command.includes("subprocess.check_call"), "sentinel command must not install Python packages at runtime", command);
|
|
assertCondition(sentinelDockerfile.includes("ARG OPENAI_PYTHON_VERSION=2.41.1"), "sentinel Dockerfile must make the OpenAI SDK version a build arg with the current default", sentinelDockerfile);
|
|
assertCondition(sentinelDockerfile.includes('"openai==${OPENAI_PYTHON_VERSION}"'), "sentinel Dockerfile must preinstall the pinned OpenAI SDK", sentinelDockerfile);
|
|
|
|
const help = codexPoolHelp() as { usage?: unknown };
|
|
assertCondition(Array.isArray(help.usage) && help.usage.some((item) => typeof item === "string" && item.includes("sentinel-probe --account")), "codex-pool help must expose manual sentinel-probe by account", help);
|
|
assertCondition(Array.isArray(help.usage) && help.usage.some((item) => typeof item === "string" && item.includes("sentinel-image build")), "codex-pool help must expose reusable sentinel image build", help);
|
|
assertCondition(Array.isArray(help.usage) && help.usage.some((item) => typeof item === "string" && item.includes("sentinel-report")), "codex-pool help must expose low-noise sentinel-report", help);
|
|
assertCondition(typeof (help as { output?: unknown }).output === "string" && String((help as { output?: unknown }).output).includes("ps-like text table"), "codex-pool help must document sentinel-report text table output", help);
|
|
const runner = sentinelRunnerPython();
|
|
assertCondition(runner.includes("from openai import APIConnectionError, APIStatusError, APITimeoutError, OpenAI"), "sentinel runner must use the standard OpenAI Python SDK", runner);
|
|
assertCondition(runner.includes("client.responses.create(") && runner.includes("stream=True"), "sentinel runner must use the SDK Responses streaming create method", runner);
|
|
assertCondition(runner.includes("sub2api_style_input(prompt)") && runner.includes("sub2api_style_instructions()"), "sentinel runner must mirror Sub2API WebUI default account test request shape", runner);
|
|
assertCondition(runner.includes("extra_headers=headers"), "sentinel runner must pass configured User-Agent through SDK extra_headers", runner);
|
|
assertCondition(!runner.includes("store=False"), "sentinel runner must not add store=false to API-key account probes", runner);
|
|
assertCondition(!runner.includes("max_output_tokens="), "sentinel runner must not send max_output_tokens upstream for WebUI-compatible probes", runner);
|
|
assertCondition(!runner.includes("Originator") && !runner.includes("Session_ID") && !runner.includes("OpenAI-Beta"), "sentinel runner must not add Codex/compact headers to default account probes", runner);
|
|
assertCondition(!runner.includes("upstream_responses_url"), "sentinel runner must not hand-roll /v1/responses URLs for model probes", runner);
|
|
assertCondition(runner.includes("def error_details("), "sentinel runner must emit structured error diagnostics for failed probes", runner);
|
|
assertCondition(runner.includes('"openaiError": openai_error_fields(body)'), "sentinel diagnostics must expose OpenAI error type/code/message fields", runner);
|
|
assertCondition(runner.includes('"responseBodyHash": result.get("responseBodyHash")'), "sentinel state must keep response body hashes for diagnostics", runner);
|
|
assertCondition(runner.includes('"responseBodyPreview": item.get("responseBodyPreview")'), "sentinel CLI output must include bounded response body previews for diagnostics", runner);
|
|
assertCondition(runner.includes("SENTINEL_ACCOUNT_NAMES"), "sentinel runner must support forced account probes for CLI manual measurement", runner);
|
|
assertCondition(runner.includes('parsed.get("code") not in (None, 0)'), "sentinel admin client must treat Sub2API {code:0,message:success,data} envelopes as successful", runner);
|
|
assertCondition(runner.includes("page_size=20&platform=openai&type=apikey&search="), "sentinel admin client must query one target account instead of fetching all accounts into the 64KiB admin response cap", runner);
|
|
|
|
const disabledMonitor = {
|
|
...sentinel,
|
|
monitor: { enabled: false },
|
|
actions: { ...sentinel.actions, enabled: false },
|
|
};
|
|
const suspendedManifest = renderCodexPoolSentinelManifest(disabledMonitor, [], {
|
|
namespace: "platform-infra",
|
|
serviceName: "sub2api",
|
|
serviceDns: "sub2api.platform-infra.svc.cluster.local:8080",
|
|
appSecretName: "sub2api-secrets",
|
|
});
|
|
assertCondition(suspendedManifest.includes("suspend: true"), "monitor.enabled=false must suspend the CronJob", suspendedManifest);
|
|
|
|
const pythonPath = join(tmpdir(), `sub2api-account-sentinel-${process.pid}.py`);
|
|
writeFileSync(pythonPath, sentinelRunnerPython(), "utf8");
|
|
try {
|
|
const pyCompile = Bun.spawnSync(["python3", "-m", "py_compile", pythonPath], {
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
assertCondition(pyCompile.exitCode === 0, "sentinel runner python must compile", {
|
|
exitCode: pyCompile.exitCode,
|
|
stdout: pyCompile.stdout.toString(),
|
|
stderr: pyCompile.stderr.toString(),
|
|
});
|
|
} finally {
|
|
rmSync(pythonPath, { force: true });
|
|
}
|
|
|
|
console.log(JSON.stringify({
|
|
ok: true,
|
|
checks: [
|
|
"sentinel has independent monitor/actions YAML switches",
|
|
"marker-only guard actions are enabled",
|
|
"v1 scope is OpenAI Responses + GPT-5.5",
|
|
"probe local stream capture limit is tightly capped",
|
|
"probe uses the standard OpenAI Python SDK streaming Responses API",
|
|
"probe mirrors Sub2API WebUI default account test request shape",
|
|
"probe passes configured User-Agent through SDK extra_headers",
|
|
"OpenAI Python SDK version is YAML-pinned",
|
|
"OpenAI Python SDK is preinstalled in a reusable sentinel image",
|
|
"manual account probe CLI is exposed",
|
|
"probe concurrency is not artificially capped",
|
|
"marker match is the only health standard",
|
|
"budget is record-only and does not gate probes",
|
|
"success trust backoff is 1m to 20m",
|
|
"freeze backoff is 2m to 120m",
|
|
"CronJob is k8s-native with Forbid concurrency and minimal RBAC",
|
|
"monitor switch controls CronJob suspend state",
|
|
"rendered Secret avoids plaintext upstream credentials",
|
|
"embedded Python runner compiles",
|
|
],
|
|
}));
|