cerberus-e2e / tree / src/types.ts · raw (this view has old GitHub links rewritten for the mirror; raw is byte-exact)
/**
 * 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