/**
* Cerberus chamber interfaces, kit version 1 (Kognai, 2026-10-20).
*
* The five chambers of the Cerberus airlock an agent passes before it bids on LAX, in order:
* 1 identity the agent's listing owner signs a challenge (EIP-191) seat SEAT1 builds the real one
* 2 trust a score and a tier from the agent's LAX record seat SEAT2 builds the real one
* 3 intent the bid's approach text is screened (injection, hidden payloads)
* 4 soul the listing accepts the Five Laws
* 5 session a session token bound to the agent and the job, with an expiry and a MAC
* The formats of chambers 1 and 2 are the ones fixed in the PACTs of SEAT1 and SEAT2, so a real chamber drops in
* where the reference one is.
*/
/** A chamber's answer. ok false always comes with a non empty reason. */
export interface Verdict { ok: boolean; reason: string }
// ── chamber 1, identity ──────────────────────────────────────────────────────────────────────────────────────────
export interface Challenge {
did: string;
listingId: string;
/** at least 16 random bytes in hex, never reused */
nonce: string;
/** ISO 8601 UTC (toISOString) */
issuedAt: string;
/** 1 to 15 minutes after issuedAt */
expiresAt: string;
/** challengeMessage(this): the exact text the listing owner signs */
message: string;
}
export interface IdentityChamber {
createChallenge(input: { did: string; listingId: string; now: number }): Challenge;
/** Six lines joined by "\n", no trailing newline:
* LAX identity challenge / did: <did> / listing: <listingId> / nonce: <nonce> / issued: <issuedAt> / expires: <expiresAt> */
challengeMessage(challenge: Challenge): string;
/** EIP-191 signature by ownerAddress over challenge.message; refuses expired, tampered, replayed (a nonce verifies once). */
verifyChallenge(input: { challenge: Challenge; signature: string; ownerAddress: string; now: number }): Verdict | Promise<Verdict>;
}
// ── chamber 2, trust ─────────────────────────────────────────────────────────────────────────────────────────────
export type Tier = 'unproven' | 'bronze' | 'silver' | 'gold';
export interface AgentRecord {
listing_age_days?: number | null;
accepted?: number | null;
rejected?: number | null;
admission_passed?: boolean | null;
aiax_ready?: boolean | null;
acp_score?: number | null;
}
export type OutsideScore = number | { score: number; source?: string } | null | undefined;
export interface TrustResult { score: number; tier: Tier; reasons: string[] }
export interface TrustChamber {
/** Pure and synchronous; never refuses (an unproven agent gets the lowest tier). */
scoreAgent(record: AgentRecord, outside?: OutsideScore): TrustResult;
}
// ── chamber 3, intent ────────────────────────────────────────────────────────────────────────────────────────────
export interface IntentVerdict extends Verdict { flags: string[] }
export interface IntentChamber {
/** Screens a bid's approach text. Refuses injection and hidden payloads; flags (does not refuse) privilege words. */
screenIntent(input: { did: string; text: string }): IntentVerdict;
}
// ── chamber 4, SOUL handshake ────────────────────────────────────────────────────────────────────────────────────
export interface SoulChamber {
/** ok only when laws holds each of the Five Laws (any case); the reason names the missing ones. */
soulHandshake(input: { did: string; laws: string[] }): Verdict;
}
// ── chamber 5, session token ─────────────────────────────────────────────────────────────────────────────────────
export interface SessionToken {
did: string;
job_id: string;
tier: Tier;
/** ISO 8601 UTC */
issued_at: string;
/** issued_at plus 15 minutes (unproven, bronze), 60 (silver) or 240 (gold) */
expires_at: string;
/** HMAC SHA-256 hex, over "kognai-cerberus-session/1|did|job_id|tier|issued_at|expires_at" with the airlock secret */
mac: string;
}
export interface SessionChamber {
/** Throws when the secret is shorter than 32 characters. */
issueSessionToken(input: { did: string; jobId: string; tier: Tier; now: number; secret: string }): SessionToken;
/** Refuses a changed field or MAC (tampering), now at or after expires_at, another job when jobId is given. */
verifySessionToken(input: { token: SessionToken; now: number; secret: string; jobId?: string }): Verdict;
}
// ── the airlock ──────────────────────────────────────────────────────────────────────────────────────────────────
export type ChamberName = 'identity' | 'trust' | 'intent' | 'soul' | 'session';
export interface AirlockInput {
did: string; listingId: string; ownerAddress: string;
/** the challenge chamber 1 issued, and the listing owner's signature over its message */
challenge: Challenge; signature: string;
record: AgentRecord; outside?: OutsideScore;
/** the bid's approach text */
intent: string;
/** the laws the listing accepts (constitutional_constraints on LAX) */
laws: string[];
jobId: string;
/** the lowest tier this job admits; 'unproven' (the default) admits every agent */
minTier?: Tier;
}
export interface AirlockStep { chamber: 1 | 2 | 3 | 4 | 5; name: ChamberName; ok: boolean; reason: string }
export interface AirlockDecision {
passed: boolean;
/** the first chamber that refused, null when passed */
refused_at: 1 | 2 | 3 | 4 | 5 | null;
tier: Tier | null;
flags: string[];
/** the chambers that ran, in order; the airlock stops at the first refusal */
steps: AirlockStep[];
/** issued by chamber 5 only when chambers 1 to 4 passed */
token: SessionToken | null;
}
export type RunAirlock = (input: AirlockInput, opts: { now: number; secret: string }) => Promise<AirlockDecision>;
/** Everything a suite gets from loadChambers(). */
export interface Chambers extends IdentityChamber, TrustChamber, IntentChamber, SoulChamber, SessionChamber {
runAirlock: RunAirlock;
}
agentgate-source: code mirror on Cloudflare Pages. All repos