Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-planners-author.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sapiom/harness": minor
---

Add capability-scoped build-plan reads and strict authoring contracts for trusted Agent Map planners. Validation, application, and rebasing fail closed until production compilation and impact evaluation are installed by the follow-on integration.
20 changes: 20 additions & 0 deletions packages/harness/src/core/architecture-source-resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,26 @@ describe("ArchitectureSourceResolver", () => {
).rejects.toMatchObject({ code: "source_not_found" });
});

it("fails closed when confirmed revision storage is not installed", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "source-resolver-"));
roots.push(root);
await expect(
new ArchitectureSourceResolver(new AgentMapWorkspaceStore(root)).resolve(
PROJECT_ID,
{
kind: "revision",
revisionId:
"revision_00000000-0000-7000-8000-000000000006" as AgentMapRevisionId,
revisionNumber: 1,
graphDigest: computeArchitectureGraphDigest({
nodes: [],
relationships: [],
}),
},
),
).rejects.toMatchObject({ code: "revision_source_unavailable" });
});

it("verifies the proposal base revision identity before materializing", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "source-resolver-"));
roots.push(root);
Expand Down
19 changes: 15 additions & 4 deletions packages/harness/src/core/architecture-source-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export interface ResolvedArchitectureSource {
export type ArchitectureSourceResolutionErrorCode =
| "source_not_found"
| "source_digest_mismatch"
| "cross_project";
| "cross_project"
| "revision_source_unavailable";

export class ArchitectureSourceResolutionError extends Error {
constructor(readonly code: ArchitectureSourceResolutionErrorCode) {
Expand All @@ -34,7 +35,9 @@ export class ArchitectureSourceResolutionError extends Error {
? "Architecture source was not found"
: code === "source_digest_mismatch"
? "Architecture source digest does not match"
: "Architecture source belongs to another project",
: code === "revision_source_unavailable"
? "Confirmed revision storage is unavailable"
: "Architecture source belongs to another project",
);
this.name = "ArchitectureSourceResolutionError";
}
Expand All @@ -44,9 +47,9 @@ export class ArchitectureSourceResolutionError extends Error {
export class ArchitectureSourceResolver {
constructor(
private readonly store: AgentMapWorkspaceStore,
private readonly readRevision: (
private readonly readRevision?: (
revisionId: AgentMapRevisionId,
) => Promise<AgentMapRevisionSnapshot | null> = async () => null,
) => Promise<AgentMapRevisionSnapshot | null>,
) {}

async resolve(
Expand All @@ -56,6 +59,10 @@ export class ArchitectureSourceResolver {
const source = parseArchitectureSourceRef(input);
let graph: AgentMapGraph;
if (source.kind === "revision") {
if (!this.readRevision)
throw new ArchitectureSourceResolutionError(
"revision_source_unavailable",
);
const revision = await this.readRevision(source.revisionId);
if (
!revision ||
Expand All @@ -77,6 +84,10 @@ export class ArchitectureSourceResolver {
throw new ArchitectureSourceResolutionError("source_not_found");
let base: AgentMapGraph = { nodes: [], relationships: [] };
if (proposal.baseRevisionId !== null) {
if (!this.readRevision)
throw new ArchitectureSourceResolutionError(
"revision_source_unavailable",
);
const revision = await this.readRevision(
proposal.baseRevisionId as AgentMapRevisionId,
);
Expand Down
223 changes: 223 additions & 0 deletions packages/harness/src/core/build-plan-schema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import { describe, expect, it } from "vitest";

import {
AGENT_ID,
PLAN_ID,
proposalSource,
} from "./build-plan.test-support.js";
import {
BUILD_PLAN_MAX_OPERATIONS,
buildPlanApplyRequestSchema,
buildPlanReadInputSchema,
buildPlanRebaseRequestSchema,
buildPlanValidateRequestSchema,
} from "./build-plan-schema.js";

const assignment = {
plannedAgentId: AGENT_ID,
mission: "Build the bounded feature",
scope: { inScope: ["Authoring"], nonGoals: ["Deployment"] },
deliverables: [],
constraints: [],
acceptanceCriteria: [],
milestoneIds: [],
unresolvedDecisions: [],
};

describe("build plan tool schemas", () => {
it("accepts the strict versioned creation contract", () => {
expect(
buildPlanApplyRequestSchema.parse({
schemaVersion: 1,
planId: null,
expectedPlanVersion: null,
expectedSource: proposalSource(),
requestId: "request-1",
operations: [
{ op: "set-project-outcome", outcome: { summary: "Ship it" } },
{ op: "upsert-agent-assignment", assignment },
],
}),
).toMatchObject({ schemaVersion: 1, planId: null });
});

it.each([
{ schemaVersion: 1, surprise: true },
{ schemaVersion: 1, plan: { planId: PLAN_ID, version: 1, extra: true } },
])("rejects unknown read keys", (input) => {
expect(buildPlanReadInputSchema.safeParse(input).success).toBe(false);
});

it("rejects unknown operations, duplicate IDs, malformed sources, and oversized batches", () => {
const base = {
schemaVersion: 1,
planId: null,
expectedPlanVersion: null,
expectedSource: proposalSource(),
};
expect(
buildPlanValidateRequestSchema.safeParse({
...base,
operations: [{ op: "write-files" }],
}).success,
).toBe(false);
expect(
buildPlanValidateRequestSchema.safeParse({
...base,
projectId: "model-controlled-project",
role: "map-planner",
operations: [{ op: "upsert-agent-assignment", assignment }],
}).success,
).toBe(false);
expect(
buildPlanValidateRequestSchema.safeParse({
...base,
operations: [
{
op: "set-shared-constraints",
constraints: [
{ constraintId: "same", description: "One", required: true },
{ constraintId: "same", description: "Two", required: false },
],
},
],
}).success,
).toBe(false);
expect(
buildPlanValidateRequestSchema.safeParse({
...base,
expectedSource: { ...proposalSource(), graphDigest: "latest" },
operations: [{ op: "upsert-agent-assignment", assignment }],
}).success,
).toBe(false);
expect(
buildPlanValidateRequestSchema.safeParse({
...base,
operations: null,
}).success,
).toBe(false);
expect(
buildPlanValidateRequestSchema.safeParse({
...base,
operations: { op: "set-project-outcome" },
}).success,
).toBe(false);
expect(
buildPlanValidateRequestSchema.safeParse({
...base,
operations: Array.from(
{ length: BUILD_PLAN_MAX_OPERATIONS + 1 },
() => ({ op: "set-project-outcome", outcome: { summary: "x" } }),
),
}).success,
).toBe(false);
expect(
buildPlanRebaseRequestSchema.safeParse({
schemaVersion: 1,
planId: PLAN_ID,
expectedPlanVersion: 1,
fromSource: proposalSource(),
toSource: proposalSource(),
requestId: "request-malformed-resolutions",
resolutions: null,
}).success,
).toBe(false);
});

it("accepts strict repository-intent remove and remap rebase resolutions", () => {
const base = {
schemaVersion: 1 as const,
planId: PLAN_ID,
expectedPlanVersion: 1,
fromSource: proposalSource(),
toSource: proposalSource(),
requestId: "request-rebase",
};
expect(
buildPlanRebaseRequestSchema.parse({
...base,
resolutions: [
{
kind: "remap-repository-intent",
repositoryIntentId: "repository-primary",
toPlannedAgentId: AGENT_ID,
},
{
kind: "remove-repository-intent",
repositoryIntentId: "repository-retired",
},
{
kind: "remap-artifact-reference",
plannedAgentId: AGENT_ID,
deliverableId: "deliverable_00000000-0000-7000-8000-000000000011",
fromNodeId: AGENT_ID,
toNodeId: AGENT_ID,
},
{
kind: "remove-artifact-reference",
plannedAgentId: AGENT_ID,
deliverableId: "deliverable_00000000-0000-7000-8000-000000000012",
nodeId: AGENT_ID,
},
],
}).resolutions,
).toHaveLength(4);
});

it("accepts client-correlated creates without canonical authored IDs", () => {
const parsed = buildPlanValidateRequestSchema.parse({
schemaVersion: 1,
planId: null,
expectedPlanVersion: null,
expectedSource: proposalSource(),
operations: [
{
op: "create-milestone",
clientRef: "milestone-alpha",
milestone: {
ordinal: 1,
title: "Alpha",
outcome: "Ready",
dependsOn: [],
},
},
{
op: "create-agent-assignment",
assignment: {
plannedAgentId: AGENT_ID,
mission: "Ship the plan",
scope: { inScope: ["Core"], nonGoals: ["Deploy"] },
deliverables: [
{
clientRef: "deliverable-alpha",
description: "Complete the artifact",
artifactNodeIds: [AGENT_ID],
acceptanceCriterionRefs: [{ clientRef: "criterion-alpha" }],
},
],
constraints: [],
acceptanceCriteria: [
{
clientRef: "criterion-alpha",
ordinal: 1,
description: "It works",
verification: "Run tests",
},
],
milestoneRefs: [{ clientRef: "milestone-alpha" }],
unresolvedDecisions: [
{
clientRef: "decision-alpha",
question: "Ready?",
required: false,
status: "resolved",
resolution: "Yes",
},
],
},
},
],
});
expect(JSON.stringify(parsed)).not.toContain("milestone_0000");
});
});
Loading
Loading