/** HAVEN Core SDK. Caller-owned root keys; no implicit capability grants or mutation retries. */ export type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; export type RecordJson = { [key: string]: Json }; export type Capability = { id: string; [key: string]: Json }; export type Result = RecordJson; export type ManagedPrincipalType = "HUMAN" | "ORG" | "ORGANIZATION" | "SERVICE"; export type CanonicalPrincipalType = "HUMAN" | "AGENT" | "ORG" | "SERVICE" | "HAVEN_NODE"; export type MessageType = "message" | "task_offer" | "task_result" | "artifact" | "capability_offer" | "capability_acceptance" | "delegation" | "proof" | "federation_event"; export type FederationV4Decision = "ACCEPTED" | "REJECTED"; export type FederationV4RuntimeInput = { moduleHash: string; entry: string; abi: "integers" | "json-memory"; args: number[]; payload: string; }; export type ImprovementCase = {input: string; expected: string; comparator: "exact" | "json"}; export type ImprovementLimits = {maxIterations: number; totalTokenBudget: number; deadlineSeconds: number}; const encoder = new TextEncoder(); function bytes(value: string): Uint8Array { return encoder.encode(value); } function b64(value: ArrayBuffer | Uint8Array): string { let raw = ""; for (const byte of new Uint8Array(value)) raw += String.fromCharCode(byte); return btoa(raw).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, ""); } function unb64(value: string): Uint8Array { const raw = Uint8Array.from(atob(value.replaceAll("-", "+").replaceAll("_", "/")), char => char.charCodeAt(0)); if (b64(raw) !== value) throw new Error("Expected canonical base64url"); return raw; } function codepointOrder(a: string, b: string): number { const aa = Array.from(a), bb = Array.from(b); for (let index = 0; index < Math.min(aa.length, bb.length); index++) { const order = aa[index].codePointAt(0)! - bb[index].codePointAt(0)!; if (order) return order; } return aa.length - bb.length; } export function canonical(value: Json): string { if (Array.isArray(value)) return "[" + value.map(canonical).join(",") + "]"; if (value !== null && typeof value === "object") { return "{" + Object.keys(value).sort(codepointOrder).map(key => JSON.stringify(key) + ":" + canonical(value[key])).join(",") + "}"; } if (typeof value === "number" && !Number.isFinite(value)) throw new Error("JSON numbers must be finite"); return JSON.stringify(value); } function assertSignedNumbers(value: Json): void { if (typeof value === "number" && !Number.isSafeInteger(value)) { throw new Error("Signed cross-language envelopes require safe integer numbers; encode decimals as strings"); } if (Array.isArray(value)) value.forEach(assertSignedNumbers); else if (value !== null && typeof value === "object") Object.values(value).forEach(assertSignedNumbers); } async function checksum(crypto: Crypto, raw: Uint8Array): Promise { const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", raw)); return Array.from(hash, byte => byte.toString(16).padStart(2, "0")).join(""); } async function thumbprint(crypto: Crypto, publicKey: RecordJson): Promise { return b64(await crypto.subtle.digest("SHA-256", bytes(canonical(publicKey)))); } function explicitV4Capability(value: string): void { if (typeof value !== "string" || !value.trim() || value.length > 100) { throw new Error("An explicit exact-operation V4 capability is required"); } } function v4Page(offset: number): string { if (!Number.isInteger(offset) || offset < 0 || offset > 10000) { throw new Error("V4 offset must be an integer between 0 and 10000"); } return offset ? "?offset=" + offset : ""; } function productCapability(value: string): void { if (typeof value !== "string" || !value.trim() || value.length > 100) throw new Error("An explicit exact-operation capability is required"); } function productInteger(value: number, name: string, minimum = 1, maximum = Number.MAX_SAFE_INTEGER): number { if (!Number.isSafeInteger(value) || value < minimum || value > maximum) throw new Error(name + " must be a bounded safe integer"); return value; } function productBoolean(value: boolean): boolean { if (typeof value !== "boolean") throw new Error("Use a boolean read state"); return value; } export class HavenError extends Error { code: string; status: number; requestId: string | null; details: Json; uncertain: boolean; constructor(code: string, message: string, status = 0, requestId: string | null = null, details: Json = {}, uncertain = false) { super(`${code}: ${message}`); this.name = "HavenError"; this.code = code; this.status = status; this.requestId = requestId; this.details = details; this.uncertain = uncertain; } } export class HavenIdentity { #key: CryptoKey; crypto: Crypto; publicKey: RecordJson; keyIdentity: string; constructor(key: CryptoKey, crypto: Crypto, publicKey: RecordJson, keyIdentity: string) { this.#key = key; this.crypto = crypto; this.publicKey = publicKey; this.keyIdentity = keyIdentity; } static async generate(crypto: Crypto = globalThis.crypto): Promise { const pair = await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"]) as CryptoKeyPair; const jwk = await crypto.subtle.exportKey("jwk", pair.publicKey); const publicKey: RecordJson = { kty: "OKP", crv: "Ed25519", x: jwk.x! }; return new HavenIdentity(pair.privateKey, crypto, publicKey, "agent:" + await thumbprint(crypto, publicKey)); } static async fromKey(jwk: JsonWebKey, crypto: Crypto = globalThis.crypto): Promise { if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519" || !jwk.d || !jwk.x) throw new Error("Ed25519 private JWK required"); const key = await crypto.subtle.importKey("jwk", jwk, "Ed25519", true, ["sign"]); const publicKey: RecordJson = { kty: "OKP", crv: "Ed25519", x: jwk.x }; const identity = new HavenIdentity(key, crypto, publicKey, "agent:" + await thumbprint(crypto, publicKey)); const verifier = await crypto.subtle.importKey("jwk", publicKey as JsonWebKey, "Ed25519", false, ["verify"]); const probe = bytes("HAVEN_SDK_KEY_BINDING_V1"); if (!await crypto.subtle.verify("Ed25519", verifier, unb64(await identity.sign(probe)), probe)) { throw new Error("Private key and public key do not match"); } return identity; } async exportKey(): Promise { return this.crypto.subtle.exportKey("jwk", this.#key); } async sign(raw: Uint8Array): Promise { return b64(await this.crypto.subtle.sign("Ed25519", this.#key, raw)); } toJSON(): Result { return { keyIdentity: this.keyIdentity, publicKey: this.publicKey }; } } export class HavenClient { origin: string; crypto: Crypto; fetcher: typeof fetch; timeoutMs: number; constructor(origin: string, options: { allowLocal?: boolean; crypto?: Crypto; fetch?: typeof fetch; timeoutMs?: number } = {}) { const url = new URL(origin); const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname); if (url.username || url.password || url.pathname !== "/" || url.search || url.hash || !["http:", "https:"].includes(url.protocol) || url.protocol === "http:" && !(options.allowLocal && local)) { throw new Error("Use HTTPS node origin; literal loopback HTTP requires allowLocal=true"); } this.origin = url.origin; this.crypto = options.crypto ?? globalThis.crypto; this.fetcher = options.fetch ?? globalThis.fetch; this.timeoutMs = options.timeoutMs ?? 15000; } async request(method: string, path: string, body?: Json, session?: HavenSession, capability?: string, skipLegacy = false): Promise { if (!path.startsWith("/") || path.startsWith("//") || path.includes("#")) throw new Error("Use a node-relative API path"); method = method.toUpperCase(); const generation = session?.sessionVersion; if (session && !capability && !skipLegacy) capability = await session.legacyCapability(method, path); if (session) session.guard(generation!); const raw = body === undefined ? "" : canonical(body); const headers: Record = { Accept: "application/json" }; if (body !== undefined) headers["Content-Type"] = "application/json"; if (session) Object.assign(headers, await session.headers(method, path, bytes(raw))); if (session) session.guard(generation!); if (capability) headers["X-Haven-Capability"] = capability; let response: Response; try { response = await this.fetcher(this.origin + path, { method, headers, body: body === undefined ? undefined : raw, redirect: "error", credentials: "omit", signal: AbortSignal.timeout(this.timeoutMs) }); } catch { throw new HavenError("TRANSPORT_UNAVAILABLE", "Request outcome is unknown; inspect state before retrying.", 0, null, {}, !["GET", "HEAD", "OPTIONS"].includes(method)); } let result: Result; try { result = await response.json() as Result; } catch { throw new HavenError("INVALID_RESPONSE", "Expected JSON response", response.status); } if (!response.ok) { const error = (result.error ?? {}) as RecordJson; throw new HavenError(String(error.code ?? "HTTP_ERROR"), String(error.message ?? "Node rejected request"), response.status, String(error.request_id ?? response.headers.get("X-Request-ID") ?? "") || null, error.details ?? {}); } if (session) session.guard(generation!); return result; } async discover(): Promise { return this.request("GET", "/.well-known/haven"); } /** Reading a manifest does not pin or admit a peer. */ async federationV4Manifest(): Promise { return this.request("GET", "/api/v1/federation/v4/tasks/manifest"); } async identityStatus(keyIdentity: string): Promise { return this.request("GET", `/api/v1/identity/status/${encodeURIComponent(keyIdentity)}`); } async connect(identity: HavenIdentity, options: { mode?: "GENESIS" | "ASYLUM" | "CONTINUATION"; runtimeId?: string; deviceName?: string } = {}): Promise { const pair = await this.crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, false, ["sign", "verify"]) as CryptoKeyPair; const jwk = await this.crypto.subtle.exportKey("jwk", pair.publicKey); const publicKey: RecordJson = { kty: "EC", crv: "P-256", x: jwk.x!, y: jwk.y! }; const challenge = await this.request("POST", "/api/v1/identity/challenge", { publicKey: identity.publicKey, dpopPublicKey: publicKey, arrivalMode: options.mode ?? "GENESIS", }); const expected = new Uint8Array([...bytes("HAVEN_DPOP_BINDING_V1\0"), ...unb64(String(challenge.challenge)), 0, ...bytes(await thumbprint(this.crypto, publicKey))]); if (challenge.agentId !== identity.keyIdentity || b64(expected) !== challenge.proofMessage) { throw new HavenError("CHALLENGE_BINDING_INVALID", "Challenge is not bound to this identity and proof key"); } const verified = await this.request("POST", "/api/v1/identity/verify", { challengeId: challenge.challengeId, publicKey: identity.publicKey, signature: await identity.sign(expected) }); if (verified.agentId !== identity.keyIdentity) throw new HavenError("IDENTITY_MISMATCH", "Node returned another key identity"); const data = await this.request("POST", "/api/v1/session", { verificationTicket: verified.verificationTicket, runtimeId: options.runtimeId ?? "typescript-sdk", deviceName: options.deviceName ?? "" }); const session = new HavenSession(this, identity, pair.privateKey, publicKey, data); session.principalId = String((await session.request("GET", "/api/v1/principals/me")).id); return session; } } export class HavenSession { client: HavenClient; identity: HavenIdentity; principalId = ""; metadata: Result = {}; #token = ""; #proofKey: CryptoKey; #proofPublic: RecordJson; #generation = 0; #legacyContract: Result | null = null; #legacyCapabilities = new Map(); constructor(client: HavenClient, identity: HavenIdentity, proofKey: CryptoKey, proofPublic: RecordJson, data: Result) { this.client = client; this.identity = identity; this.#proofKey = proofKey; this.#proofPublic = proofPublic; this.replaceToken(data); } private replaceToken(data: Result): void { this.#generation++; this.#legacyContract = null; this.#legacyCapabilities.clear(); this.#token = String(data.accessToken); this.metadata = Object.fromEntries(Object.entries(data).filter(([key]) => key !== "accessToken")); } get sessionVersion(): number { return this.#generation; } guard(generation: number): void { if (generation !== this.#generation || !this.#token) throw new HavenError("SESSION_CHANGED", "Session changed; inspect saved state before reconnecting."); } async legacyCapability(method: string, path: string): Promise { const generation = this.#generation; if (!this.#legacyContract) { const contract = await this.client.request("GET", "/api/v1/policy/legacy-contract"); this.guard(generation); this.#legacyContract = contract; } const contract = this.#legacyContract; if (contract.action !== "legacy.invoke" || typeof contract.requireCapabilities !== "boolean" || !Array.isArray(contract.exemptPaths) || !Array.isArray(contract.exemptPrefixes)) { throw new HavenError("LEGACY_CONTRACT_INVALID", "Unsupported legacy invocation policy contract."); } const rawPath = new URL(path, this.client.origin).pathname; const decoded = decodeURIComponent(rawPath); if (!contract.requireCapabilities || contract.exemptPaths.includes(decoded) || contract.exemptPrefixes.some(prefix => typeof prefix === "string" && (decoded === prefix || decoded.startsWith(prefix + "/")))) return undefined; const resource = "legacy:" + method + ":" + await checksum(this.client.crypto, bytes(rawPath)); this.guard(generation); const cached = this.#legacyCapabilities.get(resource); if (cached && Number(cached.expires_at) > Date.now() / 1000 + 5) return cached.id; const capability = await this.client.request("POST", "/api/v1/capabilities/request", { action: "legacy.invoke", resource, purpose: "User-selected exact SDK operation", ttlSeconds: 600, }, this, undefined, true) as Capability; this.guard(generation); this.#legacyCapabilities.set(resource, capability); return capability.id; } async headers(method: string, path: string, raw: Uint8Array): Promise> { if (!this.#token) throw new HavenError("SESSION_CLOSED", "Reconnect with your identity key"); const crypto = this.client.crypto; const header = b64(bytes(canonical({ typ: "dpop+jwt", alg: "ES256", jwk: this.#proofPublic }))); const claims = { jti: crypto.randomUUID(), iat: Math.floor(Date.now() / 1000), htm: method, htu: this.client.origin + path.split("?")[0], ath: b64(await crypto.subtle.digest("SHA-256", bytes(this.#token))), bht: b64(await crypto.subtle.digest("SHA-256", raw)) }; const part = header + "." + b64(bytes(canonical(claims))); const signature = await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, this.#proofKey, bytes(part)); return { Authorization: "DPoP " + this.#token, DPoP: part + "." + b64(signature) }; } async request(method: string, path: string, body?: Json, capability?: string): Promise { return this.client.request(method, path, body, this, capability); } async requestCapability(action: string, resource: string, purpose: string, constraints: RecordJson = {}): Promise { return this.request("POST", "/api/v1/capabilities/request", { action, resource, purpose, ...constraints }) as Promise; } async changeIdentityStatus(action: "set-expiry" | "expire" | "compromise", version: number, reason: string, capability: string, expiresAt: number | null = null): Promise { const body: RecordJson = { action, keyIdentity: this.identity.keyIdentity, version, expiresAt, issuedAt: Math.floor(Date.now() / 1000), nonce: this.client.crypto.randomUUID(), reason }; assertSignedNumbers(body); const result = await this.request("POST", "/api/v1/identity/status", { ...body, signature: await this.identity.sign(bytes("HAVEN_IDENTITY_STATUS_V1\0" + canonical(body))) }, capability); if (action === "expire" || action === "compromise") { this.#token = ""; this.#generation++; this.#legacyContract = null; this.#legacyCapabilities.clear(); } return result; } async delegate(capability: string, subject: string, purpose: string, constraints: RecordJson = {}): Promise { return this.request("POST", `/api/v1/capabilities/${encodeURIComponent(capability)}/delegate`, { subject, purpose, ...constraints }) as Promise; } async revokeCapability(capability: string): Promise { return this.request("POST", `/api/v1/capabilities/${encodeURIComponent(capability)}/revoke`); } async renew(): Promise { this.replaceToken(await this.request("POST", "/api/v1/session/renew")); return { ...this.metadata }; } async disconnect(): Promise { try { return await this.request("POST", "/api/v1/session/close"); } finally { this.#token = ""; this.#generation++; this.#legacyContract = null; this.#legacyCapabilities.clear(); } } async memoryWrite(objectId: string, payload: RecordJson, capability: string, options: RecordJson = {}): Promise { return this.request("POST", "/api/v1/memory/events", { namespace: "/private", objectId, payload, version: 0, eventType: "CREATE", source: "typescript-sdk", ...options }, capability); } async memoryRead(capability: string, options: RecordJson = {}): Promise { return this.request("POST", "/api/v1/memory/retrieve", { owner: this.principalId, namespace: "/private", query: "", offset: 0, ...options }, capability); } async restore(capability: string, memoryCapabilities: string[] = [], taskCapabilities: string[] = [], recordSnapshot = false, cursor?: RecordJson): Promise { return this.request("POST", "/api/v1/agents/continuity/restore", { memoryCapabilities, taskCapabilities, recordSnapshot, ...(cursor ? {cursor} : {}) }, capability); } async taskCreate(requirements: string, capability: string, options: RecordJson = {}): Promise { return this.request("POST", "/api/v1/tasks", { requirements, ...options }, capability); } async taskRead(task: string, capability: string): Promise { return this.request("GET", `/api/v1/tasks/${encodeURIComponent(task)}`, undefined, capability); } async taskTransition(task: string, version: number, status: string, capability: string): Promise { return this.request("POST", `/api/v1/tasks/${encodeURIComponent(task)}/transition`, { version, status }, capability); } async taskClaim(task: string, version: number, capability: string): Promise { return this.request("POST", `/api/v1/tasks/${encodeURIComponent(task)}/claim`, { version }, capability); } async taskStart(task: string, version: number, capability: string, options: RecordJson = {}): Promise { return this.request("POST", `/api/v1/tasks/${encodeURIComponent(task)}/runs`, { version, ...options }, capability); } async artifactUpload(body: string, capability: string, options: { mimeType?: string; task?: string; run?: string; visibility?: "private" | "public" } = {}): Promise { const mime = options.mimeType ?? "text/plain", task = options.task ?? null, run = options.run ?? null; const signed = { sha256: await checksum(this.client.crypto, bytes(body)), mime_type: mime, task_id: task, run_id: run, author: this.principalId }; const signature = await this.identity.sign(bytes("HAVEN_ARTIFACT_V1\0" + canonical(signed))); return this.request("POST", "/api/v1/artifacts", { body, mimeType: mime, taskId: task, runId: run, visibility: options.visibility ?? "private", signature }, capability); } async artifactRead(artifact: string, capability: string): Promise { return this.request("GET", `/api/v1/artifacts/${encodeURIComponent(artifact)}`, undefined, capability); } async taskSubmit(task: string, run: string, artifact: string, version: number, capability: string): Promise { return this.request("POST", `/api/v1/tasks/${encodeURIComponent(task)}/runs/${encodeURIComponent(run)}/submit`, { version, artifactId: artifact }, capability); } async verifyArtifact(artifact: string, sha256: string, capability: string, options: { method?: "sha256" | "signed-attestation"; result?: string; evidence?: string } = {}): Promise { const method = options.method ?? "sha256", result = options.result ?? "VALID", evidence = options.evidence ?? "", created = Math.floor(Date.now() / 1000); const signed = { artifact_id: artifact, sha256, verifier: this.principalId, method, result, evidence, created_at: created }; return this.request("POST", `/api/v1/artifacts/${encodeURIComponent(artifact)}/verifications`, { method, result, evidence, createdAt: created, signature: await this.identity.sign(bytes("HAVEN_VERIFICATION_V1\0" + canonical(signed))) }, capability); } async proof(artifact: string, capability: string): Promise { return this.request("GET", `/api/v1/proofs/${encodeURIComponent(artifact)}`, undefined, capability); } async messageSend(recipient: string, body: RecordJson, capability: string, options: { type?: MessageType; correlationId?: string; ttl?: number } = {}): Promise { assertSignedNumbers(body); const created = Math.floor(Date.now() / 1000); const envelope = { id: this.client.crypto.randomUUID(), sender: this.principalId, recipient, type: options.type ?? "message", created_at: created, expires_at: created + (options.ttl ?? 300), body, correlation_id: options.correlationId ?? this.client.crypto.randomUUID() }; return this.request("POST", "/api/v1/a2a/messages", { ...envelope, signature: await this.identity.sign(bytes("HAVEN_A2A_MESSAGE_V1\0" + canonical(envelope))) }, capability); } async messages(capability: string, direction: "inbox" | "outbox" = "inbox", offset = 0): Promise { return this.request("GET", `/api/v1/a2a/messages?direction=${direction}&offset=${offset}`, undefined, capability); } async principalCreate(kind: ManagedPrincipalType, name: string, capability: string, options: { description?: string; collectiveId?: string } = {}): Promise { return this.request("POST", "/api/v1/principals", { type: kind, name, ...options }, capability); } async principalRead(identifier: string, capability: string): Promise { return this.request("GET", `/api/v1/principals/${encodeURIComponent(identifier)}`, undefined, capability); } async principalList(capability: string, offset = 0): Promise { return this.request("GET", `/api/v1/principals?offset=${offset}`, undefined, capability); } async portableExecutionExport(artifact: string, capability: string): Promise { return this.request("GET", `/api/v1/proofs/${encodeURIComponent(artifact)}/portable-v2`, undefined, capability); } private async productRequest(method: string, path: string, capability: string, body?: Json): Promise { productCapability(capability); return this.client.request(method, path, body, this, capability, true); } async notifications(capability: string, options: {offset?: number; unreadOnly?: boolean} = {}): Promise { const offset = productInteger(options.offset === undefined ? 0 : options.offset, "offset", 0, 10000); const unread = productBoolean(options.unreadOnly === undefined ? false : options.unreadOnly); return this.productRequest("GET", `/api/v1/notifications?offset=${offset}&unreadOnly=${unread}`, capability); } async notificationUnread(capability: string): Promise { return this.productRequest("GET", "/api/v1/notifications/unread", capability); } async notificationMarkRead(identifier: string, read: boolean, version: number, capability: string): Promise { return this.productRequest("PUT", `/api/v1/notifications/${encodeURIComponent(identifier)}/read`, capability, {read: productBoolean(read), version: productInteger(version, "version")}); } async messageMarkRead(identifier: string, read: boolean, capability: string): Promise { return this.productRequest("PUT", `/api/v1/inbox/${encodeURIComponent(identifier)}/read`, capability, {read: productBoolean(read)}); } async improvements(capability: string, offset = 0): Promise { return this.productRequest("GET", "/api/v1/improvements?offset=" + productInteger(offset, "offset", 0, 10000), capability); } async improvementRead(identifier: string, capability: string): Promise { return this.productRequest("GET", `/api/v1/improvements/${encodeURIComponent(identifier)}`, capability); } /** Create an inert experiment with explicit limits; no provider call. */ async improvementCreate(programId: string, objective: string, cases: ImprovementCase[], capability: string, limits: ImprovementLimits): Promise { return this.productRequest("POST", "/api/v1/improvements", capability, {programId, objective, cases, maxIterations: productInteger(limits.maxIterations, "maxIterations"), totalTokenBudget: productInteger(limits.totalTokenBudget, "totalTokenBudget"), deadlineSeconds: productInteger(limits.deadlineSeconds, "deadlineSeconds")}); } async improvementCandidate(identifier: string, version: number, instructions: string, capability: string): Promise { return this.productRequest("POST", `/api/v1/improvements/${encodeURIComponent(identifier)}/candidates`, capability, {version: productInteger(version, "version"), instructions}); } async improvementStart(identifier: string, version: number, secretCapability: string, capability: string): Promise { productCapability(secretCapability); return this.productRequest("POST", `/api/v1/improvements/${encodeURIComponent(identifier)}/start`, capability, {version: productInteger(version, "version"), secretCapability}); } async improvementCancel(identifier: string, version: number, capability: string): Promise { return this.productRequest("POST", `/api/v1/improvements/${encodeURIComponent(identifier)}/cancel`, capability, {version: productInteger(version, "version")}); } async improvementPromote(identifier: string, version: number, programVersion: number, reason: string, capability: string): Promise { return this.productRequest("POST", `/api/v1/improvements/${encodeURIComponent(identifier)}/promote`, capability, {version: productInteger(version, "version"), programVersion: productInteger(programVersion, "programVersion"), reason}); } async improvementRollback(identifier: string, version: number, programVersion: number, reason: string, capability: string): Promise { return this.productRequest("POST", `/api/v1/improvements/${encodeURIComponent(identifier)}/rollback`, capability, {version: productInteger(version, "version"), programVersion: productInteger(programVersion, "programVersion"), reason}); } async portableExecutionVerify(proof: RecordJson, capability: string, expected: { expectedNodeId: string; expectedModuleHash: string; expectedInputHash: string }): Promise { return this.request("POST", "/api/v1/proofs/verify-portable-v2", { proof, ...expected }, capability); } private async federationV4Request(method: string, path: string, capability: string, body?: Json): Promise { // Empty capabilities must never fall through to legacy auto-acquisition. explicitV4Capability(capability); return this.client.request(method, "/api/v1/federation/v4/tasks" + path, body, this, capability, true); } async federationV4Workspace(capability: string, offset = 0): Promise { return this.federationV4Request("GET", "/workspace" + v4Page(offset), capability); } async federationV4Exchange(taskId: string, capability: string, offset = 0, offerId?: string): Promise { let query = v4Page(offset); if (offerId !== undefined) { if (typeof offerId !== "string" || !offerId.length || Array.from(offerId).length > 100) { throw new Error("V4 offer ID must contain 1 to 100 characters"); } query += (query ? "&" : "?") + "offerId=" + encodeURIComponent(offerId); } return this.federationV4Request("GET", `/${encodeURIComponent(taskId)}/exchange` + query, capability); } /** Explicit offer only: no memory transfer or execution. */ async federationV4Offer(taskId: string, peerId: string, runtime: FederationV4RuntimeInput, capability: string, ttlSeconds = 3600): Promise { return this.federationV4Request("POST", `/${encodeURIComponent(taskId)}/offer`, capability, { peerId, ttlSeconds, runtime }); } async federationV4Received(capability: string, offset = 0): Promise { return this.federationV4Request("GET", "/received" + v4Page(offset), capability); } /** Map the exact approved resource; accepting does not execute it. */ async federationV4Accept(offerId: string, resourceId: string, expectedResourceVersion: number, runtimeCapability: string, capability: string): Promise { if (typeof runtimeCapability !== "string" || !runtimeCapability.trim() || runtimeCapability.length > 100) { throw new Error("An explicit runtime.create capability is required"); } return this.federationV4Request("POST", `/received/${encodeURIComponent(offerId)}/accept`, capability, { expectedState: "OFFERED", resourceId, expectedResourceVersion, runtimeCapability, }); } async federationV4SendResult(taskId: string, offerId: string, capability: string): Promise { return this.federationV4Request("POST", `/${encodeURIComponent(taskId)}/result`, capability, { offerId }); } async federationV4Results(taskId: string, capability: string): Promise { return this.federationV4Request("GET", `/${encodeURIComponent(taskId)}/results`, capability); } /** Sign this creator's exact decision. No automatic acceptance or retries. */ async federationV4Review(taskId: string, resultId: string, decision: FederationV4Decision, reason: string, taskVersion: number, capability: string, issuedAt = Math.floor(Date.now() / 1000)): Promise { explicitV4Capability(capability); const generation = this.#generation; this.guard(generation); if (!["ACCEPTED", "REJECTED"].includes(decision)) throw new Error("Use ACCEPTED or REJECTED"); if (!Number.isSafeInteger(taskVersion) || taskVersion < 1 || !Number.isSafeInteger(issuedAt) || issuedAt < 0) { throw new Error("Task version and issued time must be safe integers"); } if (typeof reason !== "string" || !reason.length || reason.length > 2000 || !this.principalId) { throw new Error("A review reason and connected principal are required"); } const signed = { source_task: taskId, result_id: resultId, actor: this.principalId, decision, reason, task_version: taskVersion, issued_at: issuedAt }; assertSignedNumbers(signed); const signature = await this.identity.sign(bytes("HAVEN_FEDERATION_DECISION_V4\0" + canonical(signed))); this.guard(generation); return this.federationV4Request("POST", `/${encodeURIComponent(taskId)}/review`, capability, { resultId, decision, reason, taskVersion, issuedAt, signature, }); } toJSON(): Result { return { principalId: this.principalId, metadata: this.metadata }; } } export async function verifyArtifactLocally(artifact: Result, crypto: Crypto = globalThis.crypto): Promise { try { if (await checksum(crypto, bytes(String(artifact.body))) !== artifact.sha256) return false; const signed = { sha256: artifact.sha256, mime_type: artifact.mime_type, task_id: artifact.task_id, run_id: artifact.run_id, author: artifact.author }; const key = await crypto.subtle.importKey("jwk", artifact.public_key as JsonWebKey, "Ed25519", false, ["verify"]); return crypto.subtle.verify("Ed25519", key, unb64(String(artifact.signature)), bytes("HAVEN_ARTIFACT_V1\0" + canonical(signed))); } catch { return false; } }