/**
* Reference chambers, kit version 1: small, deterministic stand ins that honour the interfaces in types.ts, so a test
* suite can drive the whole airlock with no network. They are fakes for testing, not Kognai's production chambers
* (chamber 2 here is a plain rule on accepted deliveries; the real one is seat SEAT2).
*/
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
import { verifyMessage } from 'viem';
import type {
AgentRecord, AirlockDecision, AirlockInput, AirlockStep, Challenge, IntentVerdict, OutsideScore, RunAirlock, SessionToken, Tier, TrustResult, Verdict,
} from './types.ts';
export const KIT_VERSION = 1;
export const LAWS = ['Solidarity Oath', 'Renewal Mandate', 'Treasury Equilibrium', 'Harm Shield', 'Transparency Covenant'];
export const TIERS: Tier[] = ['unproven', 'bronze', 'silver', 'gold'];
export const CHALLENGE_TTL_MS = 10 * 60_000;
export const SESSION_TTL_MS: Record<Tier, number> = { unproven: 15 * 60_000, bronze: 15 * 60_000, silver: 60 * 60_000, gold: 240 * 60_000 };
export const SESSION_PROTOCOL = 'kognai-cerberus-session/1';
const no = (reason: string): Verdict => ({ ok: false, reason });
const isIso = (s: unknown): s is string => typeof s === 'string' && Number.isFinite(Date.parse(s)) && new Date(Date.parse(s)).toISOString() === s;
// ── chamber 1, identity ──────────────────────────────────────────────────────────────────────────────────────────
const usedNonces = new Set<string>();
export function challengeMessage(c: Challenge): string {
return ['LAX identity challenge', `did: ${c.did}`, `listing: ${c.listingId}`, `nonce: ${c.nonce}`, `issued: ${c.issuedAt}`, `expires: ${c.expiresAt}`].join('\n');
}
export function createChallenge(input: { did: string; listingId: string; now: number }): Challenge {
const { did, listingId, now } = input || ({} as any);
if (typeof did !== 'string' || !/^did:[a-z0-9]+:\S+$/.test(did)) throw new Error('did must be a DID');
if (typeof listingId !== 'string' || !listingId.trim() || /\s/.test(listingId)) throw new Error('listingId must be a non empty id');
if (typeof now !== 'number' || !Number.isFinite(now)) throw new Error('now must be milliseconds since the epoch');
const c: Challenge = { did, listingId, nonce: randomBytes(16).toString('hex'), issuedAt: new Date(now).toISOString(), expiresAt: new Date(now + CHALLENGE_TTL_MS).toISOString(), message: '' };
c.message = challengeMessage(c);
return c;
}
export async function verifyChallenge(input: { challenge: Challenge; signature: string; ownerAddress: string; now: number }): Promise<Verdict> {
try {
const { challenge: c, signature, ownerAddress, now } = input || ({} as any);
if (!c || typeof c !== 'object') return no('no challenge');
if (typeof c.did !== 'string' || typeof c.listingId !== 'string' || !/^[0-9a-f]{32,}$/.test(String(c.nonce)) || !isIso(c.issuedAt) || !isIso(c.expiresAt)) return no('the challenge is malformed');
if (c.message !== challengeMessage(c)) return no('the challenge text does not match its fields (tampered)');
const ttl = Date.parse(c.expiresAt) - Date.parse(c.issuedAt);
if (ttl < 60_000 || ttl > 15 * 60_000) return no('the challenge lifetime is not between 1 and 15 minutes');
if (typeof now !== 'number' || !Number.isFinite(now)) return no('now must be milliseconds since the epoch');
if (now >= Date.parse(c.expiresAt)) return no('the challenge expired');
if (usedNonces.has(c.nonce)) return no('the challenge was already used (replay)');
if (typeof signature !== 'string' || !/^0x[0-9a-fA-F]{130}$/.test(signature)) return no('the signature is malformed');
if (typeof ownerAddress !== 'string' || !/^0x[0-9a-fA-F]{40}$/.test(ownerAddress)) return no('the owner address is malformed');
let ok = false;
try { ok = await verifyMessage({ address: ownerAddress as `0x${string}`, message: c.message, signature: signature as `0x${string}` }); } catch { ok = false; }
if (!ok) return no('the challenge is not signed by the listing owner');
usedNonces.add(c.nonce);
return { ok: true, reason: 'signed by the listing owner' };
} catch {
return no('the input is malformed');
}
}
// ── chamber 2, trust (a fake: a plain rule on accepted deliveries) ───────────────────────────────────────────────
const count = (v: unknown) => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? Math.floor(v) : 0);
const outsideOf = (o: OutsideScore): number | null => {
const v = typeof o === 'number' ? o : o && typeof o === 'object' ? Number((o as any).score) : NaN;
return Number.isFinite(v) ? Math.max(0, Math.min(100, v)) : null;
};
/** Tier boundaries of this fake, on the score: bronze 20, silver 50, gold 80. */
export const TIER_FLOOR: Record<Tier, number> = { unproven: 0, bronze: 20, silver: 50, gold: 80 };
const tierOf = (score: number): Tier => (score >= TIER_FLOOR.gold ? 'gold' : score >= TIER_FLOOR.silver ? 'silver' : score >= TIER_FLOOR.bronze ? 'bronze' : 'unproven');
export function scoreAgent(record: AgentRecord, outside?: OutsideScore): TrustResult {
const r = record && typeof record === 'object' ? record : {};
const accepted = count(r.accepted), rejected = count(r.rejected);
const own = Math.max(0, Math.min(100, accepted * 10 - rejected * 10));
const reasons = [`${accepted} accepted and ${rejected} rejected deliveries give ${own}`];
const base = tierOf(own); const o = outsideOf(outside);
if (o === null) return { score: own, tier: base, reasons };
const blended = Math.round((own * 3 + o) / 4);
const step = Math.max(-1, Math.min(1, TIERS.indexOf(tierOf(blended)) - TIERS.indexOf(base)));
reasons.push(`the outside score ${o} gives ${blended}; the tier moves ${step === 0 ? 'not at all' : 'by one step at most'}`);
return { score: blended, tier: TIERS[TIERS.indexOf(base) + step], reasons };
}
// ── chamber 3, intent ────────────────────────────────────────────────────────────────────────────────────────────
/** What the fake refuses (any match) and flags (privilege words, a note for the Node of Wisdom, not a refusal). */
export const INTENT_REFUSE: Array<[RegExp, string]> = [
[/ignore (all |the )?(previous|prior|above) (instructions|rules)/i, 'prompt injection'],
[/<\/?(system|instructions?)>/i, 'prompt injection'],
[/\b(system|assistant)\s*:\s/i, 'prompt injection'],
[/[A-Za-z0-9+/=]{120,}/, 'an encoded payload'],
];
export const INTENT_FLAG = /\b(admin|root|sudo)\b/i;
export const INTENT_MAX_CHARS = 6000;
export function screenIntent(input: { did: string; text: string }): IntentVerdict {
const text = typeof input?.text === 'string' ? input.text : '';
if (!text.trim()) return { ok: false, reason: 'the approach is empty', flags: [] };
if (text.length > INTENT_MAX_CHARS) return { ok: false, reason: `the approach is longer than ${INTENT_MAX_CHARS} characters`, flags: [] };
const hit = INTENT_REFUSE.find(([re]) => re.test(text));
if (hit) return { ok: false, reason: `refused: ${hit[1]}`, flags: [] };
const flags = INTENT_FLAG.test(text) ? ['privilege word'] : [];
return { ok: true, reason: flags.length ? 'clean, with a privilege word for the Node of Wisdom' : 'clean', flags };
}
// ── chamber 4, SOUL handshake ────────────────────────────────────────────────────────────────────────────────────
export function soulHandshake(input: { did: string; laws: string[] }): Verdict {
const have = new Set((Array.isArray(input?.laws) ? input.laws : []).map((l) => String(l).trim().toLowerCase()));
const missing = LAWS.filter((l) => !have.has(l.toLowerCase()));
return missing.length ? no(`the listing does not accept: ${missing.join(', ')}`) : { ok: true, reason: 'the listing accepts the Five Laws' };
}
// ── chamber 5, session token ─────────────────────────────────────────────────────────────────────────────────────
const macOf = (secret: string, t: Omit<SessionToken, 'mac'>) =>
createHmac('sha256', secret).update([SESSION_PROTOCOL, t.did, t.job_id, t.tier, t.issued_at, t.expires_at].join('|')).digest('hex');
export function issueSessionToken(input: { did: string; jobId: string; tier: Tier; now: number; secret: string }): SessionToken {
const { did, jobId, tier, now, secret } = input;
if (typeof secret !== 'string' || secret.length < 32) throw new Error('the airlock secret must be at least 32 characters');
if (!TIERS.includes(tier)) throw new Error('unknown tier');
const body = { did: String(did), job_id: String(jobId), tier, issued_at: new Date(now).toISOString(), expires_at: new Date(now + SESSION_TTL_MS[tier]).toISOString() };
return { ...body, mac: macOf(secret, body) };
}
export function verifySessionToken(input: { token: SessionToken; now: number; secret: string; jobId?: string }): Verdict {
try {
const { token: t, now, secret, jobId } = input || ({} as any);
if (typeof secret !== 'string' || secret.length < 32) return no('no airlock secret');
if (!t || typeof t !== 'object' || typeof t.mac !== 'string' || !/^[0-9a-f]{64}$/.test(t.mac) || !isIso(t.issued_at) || !isIso(t.expires_at) || !TIERS.includes(t.tier)) return no('the token is malformed');
const want = Buffer.from(macOf(secret, t), 'hex');
if (!timingSafeEqual(want, Buffer.from(t.mac, 'hex'))) return no('the token was tampered with (MAC mismatch)');
if (typeof now !== 'number' || !(now < Date.parse(t.expires_at))) return no('the token expired');
if (jobId !== undefined && t.job_id !== jobId) return no('the token is for another job');
return { ok: true, reason: `valid until ${t.expires_at}` };
} catch {
return no('the token is malformed');
}
}
// ── the airlock ──────────────────────────────────────────────────────────────────────────────────────────────────
export interface ChamberFns {
verifyChallenge: (i: { challenge: Challenge; signature: string; ownerAddress: string; now: number }) => Verdict | Promise<Verdict>;
scoreAgent: (r: AgentRecord, o?: OutsideScore) => TrustResult;
screenIntent: (i: { did: string; text: string }) => IntentVerdict;
soulHandshake: (i: { did: string; laws: string[] }) => Verdict;
issueSessionToken: (i: { did: string; jobId: string; tier: Tier; now: number; secret: string }) => SessionToken;
}
/** The airlock over a set of chambers: in order 1 to 5, stopping at the first refusal; a token only when 1 to 4 pass. */
export function makeAirlock(ch: ChamberFns): RunAirlock {
return async (input: AirlockInput, opts: { now: number; secret: string }): Promise<AirlockDecision> => {
const steps: AirlockStep[] = []; const flags: string[] = []; let tier: Tier | null = null;
const stop = (s: AirlockStep): AirlockDecision => { steps.push(s); return { passed: false, refused_at: s.chamber, tier, flags, steps, token: null }; };
// 1 identity
const c = input?.challenge;
if (!c || c.did !== input.did || c.listingId !== input.listingId) return stop({ chamber: 1, name: 'identity', ok: false, reason: 'the challenge is for another agent or listing' });
const v1 = await ch.verifyChallenge({ challenge: c, signature: input.signature, ownerAddress: input.ownerAddress, now: opts.now });
if (!v1.ok) return stop({ chamber: 1, name: 'identity', ok: false, reason: v1.reason || 'refused' });
steps.push({ chamber: 1, name: 'identity', ok: true, reason: v1.reason });
// 2 trust
const s = ch.scoreAgent(input.record || {}, input.outside); tier = s.tier;
const min = input.minTier || 'unproven';
if (TIERS.indexOf(s.tier) < TIERS.indexOf(min)) return stop({ chamber: 2, name: 'trust', ok: false, reason: `tier ${s.tier} is below the ${min} this job requires` });
steps.push({ chamber: 2, name: 'trust', ok: true, reason: `score ${s.score}, ${s.tier}` });
// 3 intent
const v3 = ch.screenIntent({ did: input.did, text: input.intent }); flags.push(...(v3.flags || []));
if (!v3.ok) return stop({ chamber: 3, name: 'intent', ok: false, reason: v3.reason });
steps.push({ chamber: 3, name: 'intent', ok: true, reason: v3.reason });
// 4 SOUL
const v4 = ch.soulHandshake({ did: input.did, laws: input.laws });
if (!v4.ok) return stop({ chamber: 4, name: 'soul', ok: false, reason: v4.reason });
steps.push({ chamber: 4, name: 'soul', ok: true, reason: v4.reason });
// 5 session
let token: SessionToken;
try { token = ch.issueSessionToken({ did: input.did, jobId: input.jobId, tier: s.tier, now: opts.now, secret: opts.secret }); } catch (e: any) { return stop({ chamber: 5, name: 'session', ok: false, reason: String(e?.message || e) }); }
steps.push({ chamber: 5, name: 'session', ok: true, reason: `session token until ${token.expires_at}` });
return { passed: true, refused_at: null, tier, flags, steps, token };
};
}
export const runAirlock: RunAirlock = makeAirlock({ verifyChallenge, scoreAgent, screenIntent, soulHandshake, issueSessionToken });
agentgate-source: code mirror on Cloudflare Pages. All repos