From be7774162aa577b27f9c5749c1763e124bdb8d47 Mon Sep 17 00:00:00 2001 From: Andrei Hasna Date: Mon, 6 Jul 2026 16:42:58 +0300 Subject: [PATCH] feat: add agent run provenance schema --- README.md | 4 + examples/agent-run-provenance.invalid.json | 45 +++++++ examples/agent-run-provenance.valid.json | 130 ++++++++++++++++++++ src/schemas.ts | 133 +++++++++++++++++++++ tests/examples.test.ts | 1 + tests/schemas.test.ts | 26 ++++ 6 files changed, 339 insertions(+) create mode 100644 examples/agent-run-provenance.invalid.json create mode 100644 examples/agent-run-provenance.valid.json diff --git a/README.md b/README.md index bc686d1..9e48a97 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,10 @@ defaults are applied; output aliases such as `EvidenceRef` describe parsed data. traces, or other evidence. - `hasna.work_run.v1`: normalized run receipt for agent, command, workflow, loop, eval, test, deploy, or review work. +- `hasna.agent_run_provenance.v1`: durable control-plane provenance for agent + runs, linking task, prompt/model version, auth profile reference, sandbox, + approval policy, tool calls, costs, verifier result, commit/PR, artifacts, + provider health, and credential references without raw secret material. - `hasna.decision_envelope.v1`: decision record with selected/skipped resources, rationale, actor, costs, obligations, redactions, and evidence. - `hasna.cost_estimate.v1`: money and token estimates with provider, model, diff --git a/examples/agent-run-provenance.invalid.json b/examples/agent-run-provenance.invalid.json new file mode 100644 index 0000000..bc93908 --- /dev/null +++ b/examples/agent-run-provenance.invalid.json @@ -0,0 +1,45 @@ +{ + "schema": "hasna.agent_run_provenance.v1", + "id": "run_missing_verifier", + "createdAt": "2026-07-06T13:40:00.000Z", + "taskId": "task_missing_verifier", + "repoPath": "/home/hasna/workspace/hasna/opensource/open-coders", + "agentId": "worker-x", + "runId": "run_missing_verifier", + "promptRef": { + "kind": "document", + "id": "prompt_missing_verifier", + "uri": "task://task_missing_verifier" + }, + "modelRef": { + "kind": "model", + "id": "model_missing_verifier", + "sourcePackage": "@hasna/models", + "externalId": "model_missing_verifier" + }, + "sandboxMode": "worktree_write", + "approvalPolicy": { + "mode": "manual", + "requiresVerifier": true, + "mutationAllowed": true + }, + "startedAt": "2026-07-06T13:40:00.000Z", + "completedAt": "2026-07-06T13:41:00.000Z", + "status": "succeeded", + "commitOrPr": { + "commit": { + "kind": "commit", + "id": "commit_missing_verifier", + "externalId": "abcdef0", + "sourcePackage": "git" + } + }, + "artifactRefs": [ + { + "id": "ev_missing_verifier_tests", + "kind": "test_result", + "uri": "artifact://runs/run_missing_verifier/tests.txt" + } + ], + "redactionStatus": "partial" +} diff --git a/examples/agent-run-provenance.valid.json b/examples/agent-run-provenance.valid.json new file mode 100644 index 0000000..a2ccea5 --- /dev/null +++ b/examples/agent-run-provenance.valid.json @@ -0,0 +1,130 @@ +{ + "schema": "hasna.agent_run_provenance.v1", + "id": "run_worker_2_gateway_20260706", + "createdAt": "2026-07-06T13:40:00.000Z", + "taskId": "84481fea-7d49-4b2e-88d2-f4a8a81ecafc", + "projectId": "wks_dclch11fniur", + "repoPath": "/home/hasna/workspace/hasna/opensource/open-gateway", + "agentId": "worker-2", + "runId": "run_worker_2_gateway_20260706", + "parentRunId": "run_missing_core_worker_2", + "promptRef": { + "kind": "document", + "id": "prompt_missing_core_worker_2", + "uri": "task://84481fea-7d49-4b2e-88d2-f4a8a81ecafc" + }, + "promptVersion": "2026-07-06.worker-2", + "modelRef": { + "kind": "model", + "id": "codewith-default", + "sourcePackage": "@hasna/codewith", + "externalId": "selected-session-model" + }, + "modelCapabilityRef": { + "kind": "model", + "id": "model_capability_code", + "uri": "artifact://model-capabilities/codewith-default.json" + }, + "authProfileRef": { + "kind": "integration", + "id": "auth_profile_live_codewith", + "sourcePackage": "@hasna/accounts", + "externalId": "live-codewith" + }, + "sandboxMode": "worktree_write", + "approvalPolicy": { + "mode": "on_request", + "requiresVerifier": true, + "mutationAllowed": true, + "approvalRefs": [ + { + "kind": "task", + "id": "task_approval_84481fea", + "uri": "task://84481fea-7d49-4b2e-88d2-f4a8a81ecafc" + } + ] + }, + "toolCalls": [ + { + "id": "tool_git_diff", + "toolName": "exec_command", + "action": "git diff --check", + "status": "succeeded", + "startedAt": "2026-07-06T13:43:00.000Z", + "completedAt": "2026-07-06T13:43:01.000Z", + "evidenceRefs": [ + { + "id": "ev_git_diff_check", + "kind": "command_output", + "uri": "artifact://runs/run_worker_2_gateway_20260706/git-diff-check.txt" + } + ], + "redaction": "none" + } + ], + "cost": { + "schema": "hasna.cost_estimate.v1", + "id": "cost_worker_2_gateway", + "createdAt": "2026-07-06T13:44:00.000Z", + "currency": "USD", + "amountMicros": 0, + "basis": "actual" + }, + "startedAt": "2026-07-06T13:40:00.000Z", + "completedAt": "2026-07-06T13:45:00.000Z", + "status": "succeeded", + "verifierResult": { + "status": "passed", + "verifier": { + "kind": "agent", + "id": "adversarial-self-review", + "name": "Worker 2 adversarial self-review" + }, + "summary": "Validated bounded scope, no raw secrets, and evidence-backed claims.", + "evidenceRefs": [ + { + "id": "ev_adversarial_review", + "kind": "report", + "uri": "artifact://runs/run_worker_2_gateway_20260706/adversarial-review.md" + } + ] + }, + "commitOrPr": { + "commit": { + "kind": "commit", + "id": "commit_1065ea5", + "externalId": "1065ea5", + "sourcePackage": "git" + }, + "branch": { + "kind": "branch", + "id": "missing-core-infra-control-84481fea", + "externalId": "missing-core/infra-control-84481fea", + "sourcePackage": "git" + } + }, + "artifactRefs": [ + { + "id": "ev_gateway_tests", + "kind": "test_result", + "uri": "artifact://runs/run_worker_2_gateway_20260706/tests.txt", + "summary": "bun run typecheck && bun test && bun run build" + } + ], + "providerHealthRefs": [ + { + "kind": "verification", + "id": "gateway_mock_provider_health", + "uri": "artifact://runs/run_worker_2_gateway_20260706/mock-provider-health.json" + } + ], + "credentialRefs": [ + { + "kind": "integration", + "id": "gateway_api_key_env", + "sourcePackage": "@hasna/gateway", + "externalId": "GATEWAY_API_KEY" + } + ], + "redactionStatus": "full" +} diff --git a/src/schemas.ts b/src/schemas.ts index 2a5e1ed..b2b4156 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -8,6 +8,7 @@ export const SCHEMA_IDS = { resourceRef: "hasna.resource_ref.v1", evidenceRef: "hasna.evidence_ref.v1", workRun: "hasna.work_run.v1", + agentRunProvenance: "hasna.agent_run_provenance.v1", decisionEnvelope: "hasna.decision_envelope.v1", costEstimate: "hasna.cost_estimate.v1", capabilityCard: "hasna.capability_card.v1", @@ -1683,6 +1684,134 @@ export const WorkRunSchema = contractBaseSchema(SCHEMA_IDS.workRun) }); export type WorkRun = z.infer; +export const AgentRunSandboxModeSchema = z.enum(["none", "read_only", "workspace_write", "worktree_write", "networked", "danger_full_access"]); +export type AgentRunSandboxMode = z.infer; + +export const AgentRunApprovalPolicySchema = z + .object({ + mode: z.enum(["never", "on_request", "on_failure", "manual", "unknown"]), + requiresVerifier: z.boolean().default(false), + mutationAllowed: z.boolean().default(false), + approvalRefs: z.array(ResourcePointerSchema).default([]) + }) + .strict(); +export type AgentRunApprovalPolicy = z.infer; + +export const AgentRunToolCallSchema = z + .object({ + id: z.string().min(1), + toolName: z.string().min(1), + action: z.string().min(1).optional(), + status: ContractStatusSchema.default("unknown"), + startedAt: OptionalTimestampSchema, + completedAt: OptionalTimestampSchema, + inputHash: HashStringSchema.optional(), + outputHash: HashStringSchema.optional(), + resourceRefs: z.array(ResourcePointerSchema).default([]), + evidenceRefs: z.array(EvidencePointerSchema).default([]), + redaction: RedactionStateSchema.default("unknown") + }) + .strict() + .superRefine((value, ctx) => { + if (value.startedAt && value.completedAt && Date.parse(value.completedAt) < Date.parse(value.startedAt)) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "completedAt must be after or equal to startedAt", path: ["completedAt"] }); + } + }); +export type AgentRunToolCall = z.infer; + +export const AgentRunVerifierResultSchema = z + .object({ + status: z.enum(["passed", "failed", "not_required", "not_run", "unknown"]), + verifier: ActorPointerSchema.optional(), + summary: z.string().min(1).optional(), + proofBundleRef: ResourcePointerSchema.optional(), + evidenceRefs: z.array(EvidencePointerSchema).default([]) + }) + .strict() + .superRefine((value, ctx) => { + if ((value.status === "passed" || value.status === "failed") && !value.verifier && !value.proofBundleRef) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Verifier pass/fail results require verifier or proofBundleRef", + path: ["verifier"] + }); + } + }); +export type AgentRunVerifierResult = z.infer; + +export const CommitOrPrRefSchema = z + .object({ + commit: ResourcePointerSchema.optional(), + branch: ResourcePointerSchema.optional(), + pullRequest: ResourcePointerSchema.optional() + }) + .strict() + .superRefine((value, ctx) => { + if (!value.commit && !value.branch && !value.pullRequest) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "commitOrPr requires at least one commit, branch, or pullRequest ref", + path: ["commit"] + }); + } + }); +export type CommitOrPrRef = z.infer; + +export const AgentRunProvenanceSchema = contractBaseSchema(SCHEMA_IDS.agentRunProvenance) + .extend({ + taskId: z.string().min(1), + projectId: z.string().min(1).optional(), + repoPath: z.string().min(1), + agentId: z.string().min(1), + runId: z.string().min(1), + parentRunId: z.string().min(1).optional(), + promptRef: ResourcePointerSchema, + promptVersion: z.string().min(1).optional(), + modelRef: ResourcePointerSchema, + modelCapabilityRef: ResourcePointerSchema.optional(), + authProfileRef: ResourcePointerSchema.optional(), + sandboxMode: AgentRunSandboxModeSchema, + approvalPolicy: AgentRunApprovalPolicySchema, + toolCalls: z.array(AgentRunToolCallSchema).default([]), + cost: CostEstimateSchema.optional(), + startedAt: OptionalTimestampSchema, + completedAt: OptionalTimestampSchema, + status: ContractStatusSchema, + verifierResult: AgentRunVerifierResultSchema.optional(), + commitOrPr: CommitOrPrRefSchema.optional(), + artifactRefs: z.array(EvidencePointerSchema).default([]), + providerHealthRefs: z.array(ResourcePointerSchema).default([]), + credentialRefs: z.array(ResourcePointerSchema).default([]), + redactionStatus: RedactionStateSchema.default("unknown") + }) + .strict() + .superRefine((value, ctx) => { + if (value.runId !== value.id) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "runId must match id", path: ["runId"] }); + } + if (value.startedAt && value.completedAt && Date.parse(value.completedAt) < Date.parse(value.startedAt)) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "completedAt must be after or equal to startedAt", path: ["completedAt"] }); + } + if (TerminalStatuses.has(value.status) && !value.completedAt) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Terminal provenance records require completedAt", path: ["completedAt"] }); + } + if (value.approvalPolicy.requiresVerifier && value.verifierResult?.status !== "passed") { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Review-required runs require a passed verifierResult", + path: ["verifierResult"] + }); + } + if (value.status === "succeeded" && value.artifactRefs.length === 0 && !value.commitOrPr && !value.verifierResult?.proofBundleRef) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Succeeded provenance records require artifactRefs, commitOrPr, or verifier proof", + path: ["artifactRefs"] + }); + } + }); +export type AgentRunProvenance = z.infer; + export const TrajectoryEventSchema = z .object({ id: z.string().min(1), @@ -1927,6 +2056,7 @@ export const ContractSchemaRegistry = { [SCHEMA_IDS.resourceRef]: ResourceRefSchema, [SCHEMA_IDS.evidenceRef]: EvidenceRefSchema, [SCHEMA_IDS.workRun]: WorkRunSchema, + [SCHEMA_IDS.agentRunProvenance]: AgentRunProvenanceSchema, [SCHEMA_IDS.decisionEnvelope]: DecisionEnvelopeSchema, [SCHEMA_IDS.costEstimate]: CostEstimateSchema, [SCHEMA_IDS.capabilityCard]: CapabilityCardSchema, @@ -1954,6 +2084,7 @@ export type ContractBySchemaId = { [SCHEMA_IDS.resourceRef]: ResourceRef; [SCHEMA_IDS.evidenceRef]: EvidenceRef; [SCHEMA_IDS.workRun]: WorkRun; + [SCHEMA_IDS.agentRunProvenance]: AgentRunProvenance; [SCHEMA_IDS.decisionEnvelope]: DecisionEnvelope; [SCHEMA_IDS.costEstimate]: CostEstimate; [SCHEMA_IDS.capabilityCard]: CapabilityCard; @@ -1978,6 +2109,7 @@ export type ActorRefInput = z.input; export type ResourceRefInput = z.input; export type EvidenceRefInput = z.input; export type WorkRunInput = z.input; +export type AgentRunProvenanceInput = z.input; export type DecisionEnvelopeInput = z.input; export type CostEstimateInput = z.input; export type CapabilityCardInput = z.input; @@ -2005,6 +2137,7 @@ export type ContractInputBySchemaId = { [SCHEMA_IDS.resourceRef]: ResourceRefInput; [SCHEMA_IDS.evidenceRef]: EvidenceRefInput; [SCHEMA_IDS.workRun]: WorkRunInput; + [SCHEMA_IDS.agentRunProvenance]: AgentRunProvenanceInput; [SCHEMA_IDS.decisionEnvelope]: DecisionEnvelopeInput; [SCHEMA_IDS.costEstimate]: CostEstimateInput; [SCHEMA_IDS.capabilityCard]: CapabilityCardInput; diff --git a/tests/examples.test.ts b/tests/examples.test.ts index 283d926..531de0a 100644 --- a/tests/examples.test.ts +++ b/tests/examples.test.ts @@ -5,6 +5,7 @@ import { ContractSchemaRegistry, SCHEMA_IDS, type KnownSchemaId, validateContrac const examplesDir = join(import.meta.dir, "..", "examples"); const expectedInvalidIssuePaths: Record = { + "agent-run-provenance.invalid.json": ["verifierResult"], "app-cloud-manifest.invalid.json": ["cloudResources.0.ownerPackage", "dependencies", "forbiddenSharedRuntimes", "packageName"], "integration-ref.invalid.json": ["uri"], "no-cloud-evidence-pack.invalid.json": ["checks", "checks", "findings"], diff --git a/tests/schemas.test.ts b/tests/schemas.test.ts index d3d3e18..5381031 100644 --- a/tests/schemas.test.ts +++ b/tests/schemas.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test"; import { ActorRefSchema, + AgentRunProvenanceSchema, AppCloudManifestSchema, ContractSchemaRegistry, type EvidenceRef, @@ -23,6 +24,7 @@ import { WorkRunSchema } from "../src"; import providerLiveModeStandard from "../examples/provider-live-mode-standard.valid.json"; +import agentRunProvenance from "../examples/agent-run-provenance.valid.json"; const createdAt = "2026-06-27T10:00:00.000Z"; @@ -273,6 +275,30 @@ describe("core schemas", () => { } }); + test("validates agent run provenance with reference-only control-plane fields", () => { + const parsed = AgentRunProvenanceSchema.parse(agentRunProvenance); + expect(parsed.taskId).toBe("84481fea-7d49-4b2e-88d2-f4a8a81ecafc"); + expect(parsed.runId).toBe(parsed.id); + expect(parsed.authProfileRef?.externalId).toBe("live-codewith"); + expect(parsed.credentialRefs[0]?.externalId).toBe("GATEWAY_API_KEY"); + expect(parsed.verifierResult?.status).toBe("passed"); + expect(parsed.redactionStatus).toBe("full"); + }); + + test("rejects review-required successful provenance without passed verifier result", () => { + const result = validateContract(SCHEMA_IDS.agentRunProvenance, { + ...agentRunProvenance, + id: "run_missing_passed_verifier", + runId: "run_missing_passed_verifier", + verifierResult: { status: "not_run" } + }); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues.map((issue) => issue.path.join("."))).toContain("verifierResult"); + } + }); + test("validates a work run with nested evidence", () => { const workRun = WorkRunSchema.parse({ schema: SCHEMA_IDS.workRun,