Skip to content
Closed
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
12 changes: 12 additions & 0 deletions .changeset/neutral-shared-plan-versions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@sapiom/harness": minor
---

Add role-neutral immutable Agent Map and shared build-plan versions, durable
migration and concurrency-safe persistence, universal build-plan authoring
tools, and the reserved neutral focused-brief history seam.

**Breaking:** `ProposalActor` and proposal-history payloads now contain only
trusted `userId` and `sessionId` attribution. Consumers must stop reading or
constructing the removed `role` and `assignment` fields; those fields never
represented write or implementation authority.
16 changes: 15 additions & 1 deletion packages/harness/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,13 +160,27 @@ renews its inactivity lease, while session exit, resume rotation, signed-in
principal changes, and server shutdown revoke it. Consumers should not copy,
persist, log, or reuse the capability outside the launched session.

Every project session receives the same three project-wide tools:
Every project session receives the same project-wide tools:

- `agent_map_read` reads the current confirmed workspace and shared proposal.
- `agent_map_validate` validates one complete operation batch without mutating
shared state or allocating permanent IDs.
- `agent_map_propose` atomically and idempotently applies one validated batch
to the shared Proposed map.
- `build_plan_read` reads the current plan or one exact immutable historical
version.
- `build_plan_validate` previews the same strict request accepted by apply
without writing state or consuming IDs.
- `build_plan_apply` atomically appends an idempotent plan version using exact
expected map and plan references.
- `build_plan_rebase` moves the current plan between exact map versions using
explicit remap or removal resolutions.

The map and plan use append-only immutable histories with optimistic
concurrency. Roles, assignment completeness, proposal state, and focused brief
availability never determine whether a session may use these tools or write
code. See [`docs/shared-build-plan.md`](docs/shared-build-plan.md) for the
version, replay, rebase, and reserved brief-storage contracts.

HTTP contracts that need more than a type to use are written up under `docs/`:

Expand Down
58 changes: 58 additions & 0 deletions packages/harness/docs/shared-build-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Shared build-plan versions

Agent Studio stores one project Agent Map and one project build plan as
immutable version histories. Every ordinary project session receives the same
map and build-plan MCP tools. Trusted `{ projectId, userId, sessionId }` scope
comes only from the private session capability; tool input cannot select or
override it.

## Digests and exact references

`GraphContentDigest` identifies canonical graph semantics without project,
version, author, or timestamp metadata. An `AgentMapVersionRef` adds the exact
project and immutable version identity. Build-plan semantic digests cover only
normalized plan content, while version-record digests also cover exact map
binding, ancestry, authorship, origin, and creation time.

Current reads and historical reads are distinct. Historical reads require the
logical plan ID, immutable version ID, and semantic digest. No omitted version
is interpreted as “latest.” Restoring an old map or plan appends a new
`changeKind: "restored"` version; it never rewinds a current pointer or mutates
history.

## Authoring and concurrency

`build_plan_validate` executes the apply parser, deterministic ID mapping,
source checks, reducer, and contract validation without writing a receipt or
moving a pointer. `build_plan_apply` persists a semantic change, current
pointer, and complete replay receipt in one locked atomic replacement. An exact
semantic no-op stores only its receipt.

Request identity is scoped by the trusted project, user, session, and request
ID. Retrying identical content returns the original result; changing content
under the same request ID fails. Concurrent same-source edits merge only when
their stable touch sets are disjoint. Overlaps return stable conflict IDs and
paths. A map-version change always requires `build_plan_rebase`, including
explicit resolutions for every invalidated assignment, repository intent, or
dependency; intent is never silently dropped.

Immutable map and plan histories are each bounded at 1,024 versions and are
never silently trimmed. Exhaustion returns terminal `quota_exceeded` with
`manual_intervention` recovery so callers do not retry forever; an operator
must preserve/archive the project history before a future storage migration can
raise or replace the bound.

Validation warnings such as missing assignments, missing briefs, or unresolved
decisions are diagnostic. They do not restrict coding, tool discovery, or
session creation.

## Reserved focused-brief seam

SAP-3149 reserves append-only brief histories for later focused-context work
without running a compiler. A brief has a stable logical ID and a neutral focus
scope: either a canonical workstream or an ad-hoc delegation whose parent scope
may identify nested delegation. Each scope has an explicit active or retired
pointer. Retirement preserves history, and reactivation appends the next
version against that retained history. New and migrated aggregates start with
empty brief histories; plan apply and rebase never invoke a compiler or mutate
brief pointers.
212 changes: 212 additions & 0 deletions packages/harness/src/core/agent-map-aggregate-migration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import { describe, expect, it } from "vitest";

import type { PlanNode, PlanNodeId } from "../shared/agent-map.js";
import {
computeProjectPlanningAggregateDigest,
migrateProjectPlanningAggregate,
parseProjectPlanningAggregate,
} from "./agent-map-aggregate-migration.js";

const projectId = "project_018f0000-0000-4000-8000-000000000001";
const proposalId = "proposal_018f0000-0000-7000-8000-000000000002";
const nodeId = "node_018f0000-0000-7000-8000-000000000010";
const operationOne = "operation_018f0000-0000-7000-8000-000000000020";
const operationTwo = "operation_018f0000-0000-7000-8000-000000000021";
const createdAt = "2026-01-02T03:04:05.000Z";
const updatedAt = "2026-01-02T03:05:05.000Z";

const node: PlanNode = {
id: nodeId as PlanNodeId,
kind: "agent",
name: "Market Research",
purpose: "Find the top ten stocks trading today.",
ownerAgentId: null,
contractRefs: [],
};

function legacyE2() {
return {
storageSchemaVersion: 1,
workspace: {
projectId,
schemaVersion: 1,
recordVersion: 9,
confirmedRevisionId: null,
activeProposalId: proposalId,
projectBuildPlanId: null,
createdAt,
updatedAt,
},
proposal: {
schemaVersion: 1,
id: proposalId,
projectId,
baseRevisionId: null,
version: 2,
nodes: [node],
relationships: [],
history: [
{
id: operationOne,
requestId: "request-one",
acceptedVersion: 1,
operation: { kind: "add-node", node },
actor: { userId: "user-one", sessionId: "session-one", role: "map-planner", assignment: null },
acceptedAt: createdAt,
},
{
id: operationTwo,
requestId: "request-two",
acceptedVersion: 2,
operation: { kind: "update-node", nodeId, changes: { purpose: node.purpose } },
actor: {
userId: "user-two",
sessionId: "session-two",
role: "agent-builder",
assignment: { kind: "unplanned" },
},
acceptedAt: updatedAt,
},
],
createdAt,
updatedAt,
},
receipts: [
{
sessionId: "session-two",
requestId: "request-two",
requestDigest: "2".repeat(64),
version: 2,
allocatedNodeIds: {},
allocatedRelationshipIds: {},
},
],
};
}

describe("project planning aggregate migration", () => {
it("migrates exact empty E1 state without inventing versions or changing record metadata", () => {
const raw = {
projectId,
schemaVersion: 1,
recordVersion: 7,
confirmedRevisionId: null,
activeProposalId: null,
projectBuildPlanId: null,
createdAt,
updatedAt,
};
const { aggregate, migrated } = migrateProjectPlanningAggregate(raw, projectId);
expect(migrated).toBe(true);
expect(aggregate).toMatchObject({
storageSchemaVersion: 2,
projectId,
recordVersion: 7,
current: { map: null, buildPlan: null, briefsByScope: {} },
mapVersions: [],
buildPlanVersions: [],
briefVersionsById: {},
createdAt,
updatedAt,
});
});

it("rejects dangling E1 pointers instead of persisting unreconstructable state", () => {
expect(() => migrateProjectPlanningAggregate({
projectId,
schemaVersion: 1,
recordVersion: 1,
confirmedRevisionId: null,
activeProposalId: proposalId,
projectBuildPlanId: null,
createdAt,
updatedAt,
}, projectId)).toThrowError(expect.objectContaining({ code: "malformed_state" }));
});

it("deterministically migrates populated E2 history, neutralizes actors, and preserves no-op provenance", () => {
const first = migrateProjectPlanningAggregate(legacyE2(), projectId).aggregate;
const second = migrateProjectPlanningAggregate(legacyE2(), projectId).aggregate;

expect(first).toEqual(second);
expect(first.recordVersion).toBe(9);
expect(first.mapVersions).toHaveLength(1);
expect(first.mapVersions[0]).toMatchObject({
version: 1,
graph: { nodes: [node], relationships: [] },
authoredBy: { userId: "user-one", sessionId: "session-one" },
origin: {
kind: "migration",
legacyProposalId: proposalId,
legacyAcceptedVersion: 1,
operationIds: [operationOne],
},
});
expect(first.mapOperationHistory).toHaveLength(2);
expect(first.mapOperationHistory.map(({ actor }) => actor)).toEqual([
{ userId: "user-one", sessionId: "session-one" },
{ userId: "user-two", sessionId: "session-two" },
]);
expect(first.requestTombstones).toEqual([
expect.objectContaining({ userId: "user-one", sessionId: "session-one", requestId: "request-one" }),
]);
expect(first.requestReceipts[0]?.result).toMatchObject({
schemaVersion: 1,
proposalId,
version: 2,
operationIds: [operationTwo],
delta: {
projectId,
proposalId,
fromVersion: 1,
version: 2,
actor: { userId: "user-two", sessionId: "session-two" },
operations: [{ kind: "update-node", nodeId, changes: { purpose: node.purpose } }],
},
});
expect(first.buildPlanVersions).toEqual([]);
expect(first.briefVersionsById).toEqual({});
expect(first.current.briefsByScope).toEqual({});
});

it("rejects an E2 snapshot that does not equal strict operation replay", () => {
const raw = legacyE2();
raw.proposal.nodes[0] = { ...node, name: "Tampered" };
expect(() => migrateProjectPlanningAggregate(raw, projectId)).toThrowError(
expect.objectContaining({ code: "malformed_state" }),
);
});

it("rejects corrupted final records even when an attacker refreshes the aggregate digest", () => {
const aggregate = migrateProjectPlanningAggregate(legacyE2(), projectId).aggregate;
aggregate.mapVersions[0]!.graph.nodes[0]!.purpose = "Tampered";
aggregate.aggregateDigest = computeProjectPlanningAggregateDigest(aggregate);
expect(() => parseProjectPlanningAggregate(aggregate, projectId)).toThrowError(
expect.objectContaining({ code: "malformed_state" }),
);
});

it("rejects future outer schemas without attempting downgrade", () => {
expect(() => migrateProjectPlanningAggregate({ storageSchemaVersion: 3 }, projectId)).toThrowError(
expect.objectContaining({ code: "unsupported_schema", schemaVersion: 3 }),
);
});

it("reports future nested immutable record schemas without rewriting them as corruption", () => {
const aggregate = migrateProjectPlanningAggregate({
projectId,
schemaVersion: 1,
recordVersion: 1,
confirmedRevisionId: null,
activeProposalId: null,
projectBuildPlanId: null,
createdAt,
updatedAt,
}, projectId).aggregate as unknown as Record<string, unknown>;
aggregate.mapVersions = [{ schemaVersion: 2 }];
aggregate.aggregateDigest = computeProjectPlanningAggregateDigest(aggregate as never);
expect(() => parseProjectPlanningAggregate(aggregate, projectId)).toThrowError(
expect.objectContaining({ code: "unsupported_schema", schemaVersion: 2 }),
);
});
});
Loading
Loading