diff --git a/package-lock.json b/package-lock.json index 01478d2..b5c4183 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "license": "(MIT OR Apache-2.0)", "dependencies": { - "@predicatesystems/authority": "^0.3.3" + "@predicatesystems/authority": "^0.4.1" }, "devDependencies": { "@types/node": "^25.3.0", @@ -3743,9 +3743,9 @@ } }, "node_modules/@predicatesystems/authority": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@predicatesystems/authority/-/authority-0.3.3.tgz", - "integrity": "sha512-AGGfrzgnox7IG/9o3tAVLDd4eRkxvz+JTkNoQ+ypiQwxqRMchOX3gXyBP78pqg0TtkkBsCwtGMN8ml7XdE0otw==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@predicatesystems/authority/-/authority-0.4.1.tgz", + "integrity": "sha512-CI8iS5m+GJ/b5pTvlOhweTluuuIFt+GlhKrkWUKxBNsN9UEJYmKgN+Oy/R+oBysVf6x5dh0OwX6kbNls6rWObA==", "license": "(MIT OR Apache-2.0)", "engines": { "node": ">=20.0.0" diff --git a/package.json b/package.json index 0b328dd..6e6a6bc 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "license": "(MIT OR Apache-2.0)", "type": "module", "dependencies": { - "@predicatesystems/authority": "^0.3.3" + "@predicatesystems/authority": "^0.4.1" }, "devDependencies": { "@types/node": "^25.3.0", diff --git a/src/authority-client.ts b/src/authority-client.ts index b468a08..6b61895 100644 --- a/src/authority-client.ts +++ b/src/authority-client.ts @@ -1,17 +1,31 @@ import { AuthorityClient, + Verifier, type AuthorizationRequest, + type VerifyRequest, + type ActualOperation, } from "@predicatesystems/authority"; import type { ProviderConfig } from "./config.js"; +export type { VerifyRequest, ActualOperation }; + export interface AuthorityDecision { allow: boolean; reason?: string; mandateId?: string; } +export interface VerificationResult { + verified: boolean; + reason?: string; + auditId?: string; + authorized?: { action: string; resource: string }; + actual?: { action: string; resource: string }; +} + export interface AuthorityAdapter { authorize(request: AuthorizationRequest): Promise; + verify?(request: VerifyRequest): Promise; } interface SdkDecision { @@ -46,5 +60,36 @@ export function createDefaultAuthorityAdapter( maxRetries: config.maxRetries, backoffInitialMs: config.backoffInitialMs, }); - return createAuthorityAdapter(sdkClient); + + // Create verifier for post-execution verification + const verifier = new Verifier({ + baseUrl: config.baseUrl, + timeoutMs: config.timeoutMs, + }); + + return { + async authorize(request: AuthorizationRequest): Promise { + const decision = await sdkClient.authorize(request); + return { + allow: decision.allowed, + reason: decision.reason, + mandateId: decision.mandate_id ?? undefined, + }; + }, + + async verify(request: VerifyRequest): Promise { + const result = await verifier.verify(request); + return { + verified: result.verified, + reason: result.reason, + auditId: result.auditId, + authorized: result.details?.authorized + ? { action: result.details.authorized.action, resource: result.details.authorized.resource } + : undefined, + actual: result.details?.actual + ? { action: result.details.actual.action, resource: result.details.actual.resource } + : undefined, + }; + }, + }; } diff --git a/src/provider.ts b/src/provider.ts index 3b305f7..219e071 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -2,11 +2,16 @@ import crypto from "node:crypto"; import type { AuthorizationRequest } from "@predicatesystems/authority"; import { type AuthorityAdapter, + type VerificationResult, + type VerifyRequest, + type ActualOperation, createDefaultAuthorityAdapter, } from "./authority-client.js"; import { defaultProviderConfig, type ProviderConfig } from "./config.js"; import { ActionDeniedError, SidecarUnavailableError } from "./errors.js"; +export type { VerificationResult, VerifyRequest, ActualOperation }; + export interface GuardRequest { action: string; resource: string; @@ -144,6 +149,57 @@ export class GuardedProvider { } } + /** + * Verify that an actual operation matches what was authorized. + * + * Call this after executing an operation to confirm the agent + * did what it said it would do. + * + * @param mandateId - The mandate ID from authorization + * @param actual - The actual operation that was performed + * @returns Verification result + * + * @example + * ```typescript + * const mandateId = await provider.authorize({ action: 'fs.read', resource: '/src/index.ts', args: {} }); + * const content = await fs.readFile('/src/index.ts'); + * const result = await provider.verify(mandateId, { + * action: 'fs.read', + * resource: '/src/index.ts', + * contentHash: sha256(content), + * }); + * if (!result.verified) { + * console.error('Verification failed:', result.reason); + * } + * ``` + */ + async verify( + mandateId: string, + actual: ActualOperation, + ): Promise { + if (!this.authorityClient.verify) { + // Verification not supported by this adapter + return { + verified: true, + reason: "verification_not_supported", + }; + } + + try { + return await this.authorityClient.verify({ + mandateId, + actual, + }); + } catch (error) { + // Best-effort verification; don't fail the operation + const message = error instanceof Error ? error.message : String(error); + return { + verified: false, + reason: `verification_error: ${message}`, + }; + } + } + private async emitDecisionEvent(event: DecisionTelemetryEvent): Promise { this.telemetry?.onDecision?.(event); if (!this.auditExporter) {