108 lines
6.1 KiB
TypeScript
108 lines
6.1 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 {
|
|
defaultCodexPoolSentinelConfig,
|
|
readCodexPoolSentinelConfig,
|
|
renderCodexPoolSentinelManifest,
|
|
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 parsed = Bun.YAML.parse(readFileSync(configPath, "utf8")) as { sentinel?: unknown };
|
|
const sentinel = readCodexPoolSentinelConfig(parsed.sentinel, defaultCodexPoolSentinelConfig(), configPath);
|
|
|
|
assertCondition(sentinel.monitor.enabled === true, "sentinel monitor must be enabled for observation-first rollout", sentinel);
|
|
assertCondition(sentinel.actions.enabled === false, "sentinel actions must default off until monitoring quality is reviewed", sentinel);
|
|
assertCondition(sentinel.actions.freezeOnMarkerMismatch === true, "marker mismatch must be configured as freeze-worthy when actions are enabled", sentinel.actions);
|
|
assertCondition(sentinel.actions.freezeOnTransportError === false, "transport errors must not freeze by default", 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 maxOutputTokens must be tightly capped", sentinel.probe);
|
|
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 === 10, "freeze backoff must start at 10 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\": false"), "sentinel manifest must preserve actions.enabled=false 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);
|
|
|
|
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",
|
|
"observation-first rollout keeps actions disabled",
|
|
"v1 scope is OpenAI Responses + GPT-5.5",
|
|
"probe max_output_tokens is tightly capped",
|
|
"probe concurrency is not artificially capped",
|
|
"budget is record-only and does not gate probes",
|
|
"success trust backoff is 1m to 20m",
|
|
"freeze backoff is 10m 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",
|
|
],
|
|
}));
|