feat(api): CL0UD capability probe + a11y/HAR evidence + worker scale toggle
CI (SIAX Cloud) / contracts (pull_request) Successful in 17s
CI (SIAX Cloud) / contracts (push) Successful in 17s
CI (SIAX Cloud) / quality (pull_request) Successful in 54s
CI (SIAX Cloud) / quality (push) Successful in 56s
CI (SIAX Cloud) / security (pull_request) Successful in 1m7s
CI (SIAX Cloud) / security (push) Successful in 1m5s
CI (SIAX Cloud) / contracts (pull_request) Successful in 17s
CI (SIAX Cloud) / contracts (push) Successful in 17s
CI (SIAX Cloud) / quality (pull_request) Successful in 54s
CI (SIAX Cloud) / quality (push) Successful in 56s
CI (SIAX Cloud) / security (pull_request) Successful in 1m7s
CI (SIAX Cloud) / security (push) Successful in 1m5s
- cl0ud client: contract-first probe of documented GET /capabilities/:id, status connected/denied/unavailable/disabled surfaced in /health; CL0UD_REQUIRED=true = fail-closed 503; full policy-decisions gated on CL0UD B-4/ADR-0031 (deliberately not simulated) - capture depth: accessibility tree + condensed HAR as evidence kinds (null = explicit evidence gap) - CAPTURE_WORKER=on/off for separate-scale deployments - 34/34 tests
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import type { FastifyBaseLogger } from "fastify";
|
||||
|
||||
export interface CapabilityDescriptor {
|
||||
id: string;
|
||||
status?: string;
|
||||
version?: string;
|
||||
owner?: string;
|
||||
surfaces?: string[];
|
||||
}
|
||||
|
||||
export type Cl0udStatus = "disabled" | "connected" | "unavailable" | "denied";
|
||||
|
||||
export interface Cl0udProbe {
|
||||
status: Cl0udStatus;
|
||||
capabilities: Record<string, string | undefined>;
|
||||
checkedAt: string | null;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
export interface Cl0udClientDeps {
|
||||
baseUrl?: string;
|
||||
apiToken?: string;
|
||||
capabilityIds?: string[];
|
||||
logger?: FastifyBaseLogger;
|
||||
fetchImpl?: typeof fetch;
|
||||
probeIntervalMs?: number;
|
||||
}
|
||||
|
||||
const EXPECTED_IDS = ["c0py.capture.run.v1", "c0py.registry.manage.v1"];
|
||||
|
||||
// CL0UD wiring, contract-first: probes the documented capability discovery API
|
||||
// (GET /capabilities/:id) and surfaces the result in /health as evidence.
|
||||
// Full policy-decision calls remain gated on CL0UD B-4 / ADR-0031 (estate-level)
|
||||
// and are deliberately NOT simulated here. With CL0UD_REQUIRED=true the API
|
||||
// fails closed (503) while the probe cannot confirm ACTIVE capabilities.
|
||||
export function createCl0udClient(deps: Cl0udClientDeps) {
|
||||
const fetchImpl = deps.fetchImpl ?? fetch;
|
||||
const ids = deps.capabilityIds ?? EXPECTED_IDS;
|
||||
let last: Cl0udProbe = {
|
||||
status: deps.baseUrl ? "unavailable" : "disabled",
|
||||
capabilities: {},
|
||||
checkedAt: null,
|
||||
detail: deps.baseUrl ? undefined : "CL0UD_BASE_URL not configured",
|
||||
};
|
||||
|
||||
async function probe(): Promise<Cl0udProbe> {
|
||||
if (!deps.baseUrl) {
|
||||
last = { status: "disabled", capabilities: {}, checkedAt: new Date().toISOString(), detail: "CL0UD_BASE_URL not configured" };
|
||||
return last;
|
||||
}
|
||||
const capabilities: Record<string, string | undefined> = {};
|
||||
let unreachable = 0;
|
||||
let notActive = 0;
|
||||
for (const id of ids) {
|
||||
try {
|
||||
const res = await fetchImpl(`${deps.baseUrl.replace(/\/$/, "")}/capabilities/${encodeURIComponent(id)}`, {
|
||||
headers: deps.apiToken ? { Authorization: `Bearer ${deps.apiToken}` } : {},
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
if (res.ok) {
|
||||
const body = (await res.json()) as CapabilityDescriptor & { capability?: CapabilityDescriptor };
|
||||
const cap = body.capability ?? body;
|
||||
capabilities[id] = cap.status ?? "unknown";
|
||||
if (capabilities[id] !== "ACTIVE") notActive += 1;
|
||||
} else {
|
||||
capabilities[id] = `http_${res.status}`;
|
||||
unreachable += 1;
|
||||
}
|
||||
} catch {
|
||||
capabilities[id] = "unreachable";
|
||||
unreachable += 1;
|
||||
}
|
||||
}
|
||||
const status: Cl0udStatus =
|
||||
unreachable === ids.length
|
||||
? "unavailable"
|
||||
: unreachable === 0 && notActive === 0
|
||||
? "connected"
|
||||
: "denied";
|
||||
last = { status, capabilities, checkedAt: new Date().toISOString() };
|
||||
deps.logger?.info({ cl0ud: last.status }, "CL0UD probe done");
|
||||
return last;
|
||||
}
|
||||
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
function start() {
|
||||
void probe().catch(() => {});
|
||||
if (deps.probeIntervalMs && deps.probeIntervalMs > 0) {
|
||||
timer = setInterval(() => void probe().catch(() => {}), deps.probeIntervalMs);
|
||||
timer.unref?.();
|
||||
}
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (timer) clearInterval(timer);
|
||||
}
|
||||
|
||||
return {
|
||||
probe,
|
||||
start,
|
||||
stop,
|
||||
get status(): Cl0udProbe {
|
||||
return last;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user