Add deterministic capability control plane (#12355)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Semantic operations need a control plane that owns authorization and state changes > - Conformance tests need this authority without a live server or database > - The existing in-memory port covers transport facts but not capability-governed domains > - This pull request adds a deterministic capability-aware control plane for tests > - It keeps the implementation behind the package testing entry point > - The benefit is repeatable authorization and mutation tests before production wiring lands ## Linked Issues or Issue Description **Subsystem affected** packages/paperclip-runner testing and semantic capability contracts **Problem or motivation** Later semantic action slices need deterministic company, actor, task, governance, workspace, secret, budget, routine, and audit state. A live Paperclip server would make those tests slow and environment-dependent. **Proposed solution** Add serializable capability fixture types and an in-memory control-plane adapter. Enforce company scope, claims, roles, idempotency, redaction, faults, and audit records. Export the adapter only from the testing entry point. **Alternatives considered** The tests could mock each action separately. That would duplicate authorization rules and would not verify state transitions across multiple actions. **Roadmap alignment** This supports the existing experimental Paperclip Runner rollout. It does not enable production semantic operations or change app execution. ## What Changed - Add deterministic capability fixture types and seed state. - Add an in-memory capability control-plane adapter. - Enforce run, company, actor, claim, role, and idempotency boundaries. - Model governed task, document, interaction, workspace, secret, routine, and audit changes. - Add deterministic tests and testing-entry-point exports. ## Verification - `pnpm --filter @paperclipai/paperclip-runner test:typescript` - `pnpm --filter @paperclipai/paperclip-runner typecheck:typescript` - `pnpm -r typecheck` - `pnpm build` - The branch changes 5 files relative to its declared base. ## Risks The main risk is a permissive mock that hides a production authorization error. The adapter fails closed for missing claims, wrong roles, cross-company access, duplicate mutations, invalid state, and restricted secret access. Tests cover each fixture domain and serialized restore behavior. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used OpenAI Codex, `gpt-5`, with agentic reasoning, tool use, and code execution. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either linked an existing public issue or described the issue in-PR - [x] I have not referenced internal or instance-local Paperclip issues or links - [x] My branch name describes the change and contains no internal Paperclip ticket id - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge
This commit is contained in:
parent
773d357d8a
commit
300b7f4b7e
|
|
@ -0,0 +1,558 @@
|
|||
import type {
|
||||
CompleteControlPlaneRunInput,
|
||||
ControlPlanePort,
|
||||
OpenControlPlaneRunInput,
|
||||
} from "../contracts/control-plane-port.js";
|
||||
import type { NativeRunEvent, NativeRunResult } from "../contracts/types.js";
|
||||
import type { PersistedNativeSession } from "../contracts/native-session-backend.js";
|
||||
import type { PrpEvent } from "../protocol/replay-contract.js";
|
||||
|
||||
export type CapabilityJsonPrimitive = string | number | boolean | null;
|
||||
export type CapabilityJsonValue =
|
||||
| CapabilityJsonPrimitive
|
||||
| CapabilityJsonValue[]
|
||||
| { [key: string]: CapabilityJsonValue };
|
||||
|
||||
export type CapabilityTaskStatus =
|
||||
| "backlog"
|
||||
| "todo"
|
||||
| "in_progress"
|
||||
| "in_review"
|
||||
| "done"
|
||||
| "blocked"
|
||||
| "cancelled";
|
||||
|
||||
export interface CapabilityFixtureCompany {
|
||||
id: string;
|
||||
name: string;
|
||||
issuePrefix: string;
|
||||
status: "active" | "paused";
|
||||
budgetId: string;
|
||||
}
|
||||
export interface CapabilityFixtureActor {
|
||||
id: string;
|
||||
companyId: string;
|
||||
name: string;
|
||||
role: string;
|
||||
status: "active" | "paused";
|
||||
budgetId: string;
|
||||
capabilityGrants: string[];
|
||||
}
|
||||
|
||||
export interface CapabilityFixtureTask {
|
||||
id: string;
|
||||
companyId: string;
|
||||
identifier: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
status: CapabilityTaskStatus;
|
||||
priority: "critical" | "high" | "medium" | "low";
|
||||
workMode: "standard" | "ask" | "planning" | "skill_test";
|
||||
parentId: string | null;
|
||||
assigneeActorId: string | null;
|
||||
checkoutRunId: string | null;
|
||||
executionRunId: string | null;
|
||||
startedAt: string | null;
|
||||
completedAt: string | null;
|
||||
}
|
||||
|
||||
export interface CapabilityFixtureComment {
|
||||
id: string;
|
||||
taskId: string;
|
||||
authorActorId: string | null;
|
||||
body: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CapabilityFixtureDocumentRevision {
|
||||
id: string;
|
||||
documentId: string;
|
||||
revision: number;
|
||||
body: string;
|
||||
changeSummary: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CapabilityFixtureDocument {
|
||||
id: string;
|
||||
taskId: string;
|
||||
key: string;
|
||||
title: string;
|
||||
format: "markdown";
|
||||
latestRevisionId: string;
|
||||
revisions: CapabilityFixtureDocumentRevision[];
|
||||
}
|
||||
|
||||
export type CapabilityInteractionKind =
|
||||
| "confirmation"
|
||||
| "checkbox"
|
||||
| "questions"
|
||||
| "suggest_tasks"
|
||||
| "item_verdicts";
|
||||
|
||||
export interface CapabilityFixtureInteraction {
|
||||
id: string;
|
||||
taskId: string;
|
||||
kind: CapabilityInteractionKind;
|
||||
status: "pending" | "answered" | "accepted" | "rejected" | "expired";
|
||||
title: string;
|
||||
prompt: string;
|
||||
payload: CapabilityJsonValue;
|
||||
targetRevisionId: string | null;
|
||||
continuationPolicy: "none" | "wake_assignee" | "wake_assignee_on_accept";
|
||||
result: CapabilityJsonValue | null;
|
||||
createdAt: string;
|
||||
resolvedAt: string | null;
|
||||
}
|
||||
|
||||
export interface CapabilityFixtureApprovalComment {
|
||||
id: string;
|
||||
authorActorId: string | null;
|
||||
body: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CapabilityFixtureApproval {
|
||||
id: string;
|
||||
companyId: string;
|
||||
taskIds: string[];
|
||||
type: string;
|
||||
status: "pending" | "approved" | "rejected" | "cancelled";
|
||||
requestedByActorId: string | null;
|
||||
payload: CapabilityJsonValue;
|
||||
decisionNote: string | null;
|
||||
comments: CapabilityFixtureApprovalComment[];
|
||||
createdAt: string;
|
||||
decidedAt: string | null;
|
||||
}
|
||||
|
||||
export interface CapabilityFixtureArtifact {
|
||||
id: string;
|
||||
taskId: string;
|
||||
filename: string;
|
||||
contentType: string;
|
||||
byteSize: number;
|
||||
sha256: string;
|
||||
contentRef: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CapabilityFixtureWorkProduct {
|
||||
id: string;
|
||||
taskId: string;
|
||||
type: "artifact" | "document" | "pull_request" | "preview_url" | "runtime_service" | "commit" | "branch";
|
||||
title: string;
|
||||
metadata: CapabilityJsonValue;
|
||||
artifactId: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CapabilityFixtureBlocker {
|
||||
id: string;
|
||||
taskId: string;
|
||||
blockedByTaskId: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CapabilityFixtureWorkspaceService {
|
||||
id: string;
|
||||
taskId: string;
|
||||
name: string;
|
||||
status: "stopped" | "starting" | "running" | "failed";
|
||||
url: string | null;
|
||||
lastTransitionAt: string;
|
||||
}
|
||||
|
||||
export interface CapabilityFixtureBudget {
|
||||
id: string;
|
||||
scope: "company" | "actor";
|
||||
scopeId: string;
|
||||
limitCents: number;
|
||||
spentCents: number;
|
||||
hardStop: boolean;
|
||||
}
|
||||
|
||||
export type CapabilityWakeReason =
|
||||
| "manual"
|
||||
| "issue_commented"
|
||||
| "interaction_resolved"
|
||||
| "approval_resolved"
|
||||
| "blockers_resolved"
|
||||
| "scheduled_retry"
|
||||
| "resume";
|
||||
|
||||
export interface CapabilityWakeContext {
|
||||
reason: CapabilityWakeReason;
|
||||
payload: CapabilityJsonValue;
|
||||
}
|
||||
|
||||
export interface CapabilityScheduledWake {
|
||||
id: string;
|
||||
actorId: string;
|
||||
taskId: string;
|
||||
reason: CapabilityWakeReason;
|
||||
payload: CapabilityJsonValue;
|
||||
dueAt: string;
|
||||
status: "scheduled" | "delivered" | "cancelled";
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CapabilityFixtureRun {
|
||||
id: string;
|
||||
companyId: string;
|
||||
actorId: string;
|
||||
taskId: string;
|
||||
sessionId: string;
|
||||
backendKind: OpenControlPlaneRunInput["backendKind"];
|
||||
sourceInstanceId: string;
|
||||
status: "running" | "succeeded" | "failed" | "cancelled" | "budget_stopped";
|
||||
attempt: number;
|
||||
openedAt: string;
|
||||
finishedAt: string | null;
|
||||
wake: CapabilityWakeContext;
|
||||
capabilities: string[];
|
||||
events: Array<NativeRunEvent | PrpEvent>;
|
||||
result: NativeRunResult | CompleteControlPlaneRunInput | null;
|
||||
sessionCheckpoint: PersistedNativeSession | null;
|
||||
}
|
||||
|
||||
export interface CapabilityAuditRecord {
|
||||
id: string;
|
||||
at: string;
|
||||
runId: string | null;
|
||||
actorId: string | null;
|
||||
action: string;
|
||||
entityType: string;
|
||||
entityId: string;
|
||||
details: CapabilityJsonValue;
|
||||
}
|
||||
|
||||
export interface CapabilityDecisionRecord {
|
||||
id: string;
|
||||
at: string;
|
||||
runId: string | null;
|
||||
operation: string;
|
||||
outcome: "allowed" | "denied" | "duplicate" | "reconciled" | "faulted";
|
||||
reason: string;
|
||||
stateRevision: number;
|
||||
entityRefs: string[];
|
||||
}
|
||||
|
||||
export interface CapabilityFaultRule {
|
||||
id: string;
|
||||
operation: "open_run" | "append_event" | "apply_command" | "complete_run";
|
||||
commandKind?: CapabilitySemanticCommand["kind"];
|
||||
effect: "retryable_error" | "lost_ack";
|
||||
remaining: number;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface ScenarioChatdempotencyRecord {
|
||||
scope: string;
|
||||
key: string;
|
||||
inputCanonical: string;
|
||||
result: CapabilityCommandResult;
|
||||
}
|
||||
|
||||
export interface CapabilityFixtureState {
|
||||
schema: "paperclip.capability.mock-state.v1";
|
||||
revision: number;
|
||||
clock: { epochMs: number; tick: number };
|
||||
counters: Record<string, number>;
|
||||
lifecycle: "stopped" | "running";
|
||||
activeRunId: string | null;
|
||||
company: CapabilityFixtureCompany;
|
||||
actors: CapabilityFixtureActor[];
|
||||
tasks: CapabilityFixtureTask[];
|
||||
/** Known task ids outside the fixture company, without leaking foreign data. */
|
||||
outOfScopeTaskIds: string[];
|
||||
comments: CapabilityFixtureComment[];
|
||||
documents: CapabilityFixtureDocument[];
|
||||
interactions: CapabilityFixtureInteraction[];
|
||||
approvals: CapabilityFixtureApproval[];
|
||||
artifacts: CapabilityFixtureArtifact[];
|
||||
workProducts: CapabilityFixtureWorkProduct[];
|
||||
blockers: CapabilityFixtureBlocker[];
|
||||
workspaceServices: CapabilityFixtureWorkspaceService[];
|
||||
budgets: CapabilityFixtureBudget[];
|
||||
runs: CapabilityFixtureRun[];
|
||||
wakes: CapabilityScheduledWake[];
|
||||
audit: CapabilityAuditRecord[];
|
||||
decisions: CapabilityDecisionRecord[];
|
||||
idempotency: ScenarioChatdempotencyRecord[];
|
||||
faults: CapabilityFaultRule[];
|
||||
}
|
||||
|
||||
export interface CapabilityRunContext {
|
||||
schema: "paperclip.capability.run-context.v1";
|
||||
company: Pick<CapabilityFixtureCompany, "id" | "name" | "issuePrefix" | "status">;
|
||||
actor: Pick<CapabilityFixtureActor, "id" | "name" | "role" | "status" | "capabilityGrants">;
|
||||
activeTask: CapabilityFixtureTask;
|
||||
ancestors: CapabilityFixtureTask[];
|
||||
wake: CapabilityWakeContext;
|
||||
capabilities: string[];
|
||||
budget: { limitCents: number; spentCents: number; remainingCents: number };
|
||||
interactionResults: Array<Pick<CapabilityFixtureInteraction, "id" | "kind" | "status" | "result">>;
|
||||
}
|
||||
|
||||
export interface CapabilityOpenFixtureRunInput extends OpenControlPlaneRunInput {
|
||||
wake?: CapabilityWakeContext;
|
||||
capabilities?: string[];
|
||||
resume?: boolean;
|
||||
}
|
||||
|
||||
interface CapabilityBaseCommand {
|
||||
taskId?: string;
|
||||
}
|
||||
|
||||
export type CapabilitySemanticCommand =
|
||||
| (CapabilityBaseCommand & { kind: "report_progress"; body: string })
|
||||
| (CapabilityBaseCommand & {
|
||||
kind: "write_document";
|
||||
key: string;
|
||||
title: string;
|
||||
body: string;
|
||||
baseRevisionId: string | null;
|
||||
changeSummary?: string | null;
|
||||
})
|
||||
| (CapabilityBaseCommand & {
|
||||
kind: "request_human_input";
|
||||
interactionKind: CapabilityInteractionKind;
|
||||
title: string;
|
||||
prompt: string;
|
||||
payload?: CapabilityJsonValue;
|
||||
targetRevisionId?: string | null;
|
||||
continuationPolicy: CapabilityFixtureInteraction["continuationPolicy"];
|
||||
})
|
||||
| (CapabilityBaseCommand & {
|
||||
kind: "resolve_human_input";
|
||||
interactionId: string;
|
||||
outcome: "answered" | "accepted" | "rejected" | "expired";
|
||||
result: CapabilityJsonValue;
|
||||
})
|
||||
| (CapabilityBaseCommand & {
|
||||
kind: "register_deliverable";
|
||||
filename: string;
|
||||
contentType: string;
|
||||
byteSize: number;
|
||||
sha256: string;
|
||||
contentRef: string;
|
||||
title: string;
|
||||
})
|
||||
| (CapabilityBaseCommand & { kind: "set_dependencies"; blockedByTaskIds: string[] })
|
||||
| (CapabilityBaseCommand & {
|
||||
kind: "create_task";
|
||||
title: string;
|
||||
description?: string | null;
|
||||
assigneeActorId?: string | null;
|
||||
priority?: CapabilityFixtureTask["priority"];
|
||||
blockedByTaskIds?: string[];
|
||||
})
|
||||
| (CapabilityBaseCommand & {
|
||||
kind: "request_approval";
|
||||
approvalType: string;
|
||||
payload: CapabilityJsonValue;
|
||||
})
|
||||
| (CapabilityBaseCommand & {
|
||||
kind: "decide_approval";
|
||||
approvalId: string;
|
||||
decision: "approved" | "rejected" | "cancelled";
|
||||
note: string;
|
||||
})
|
||||
| (CapabilityBaseCommand & {
|
||||
kind: "comment_on_approval";
|
||||
approvalId: string;
|
||||
body: string;
|
||||
})
|
||||
| (CapabilityBaseCommand & {
|
||||
kind: "control_workspace_service";
|
||||
serviceId: string;
|
||||
action: "start" | "stop" | "fail";
|
||||
url?: string | null;
|
||||
})
|
||||
| (CapabilityBaseCommand & { kind: "record_budget_usage"; cents: number })
|
||||
| (CapabilityBaseCommand & { kind: "finish_task"; summary: string })
|
||||
| (CapabilityBaseCommand & {
|
||||
kind: "block_task";
|
||||
reason: string;
|
||||
blockedByTaskIds?: string[];
|
||||
})
|
||||
| (CapabilityBaseCommand & { kind: "request_review"; summary: string })
|
||||
| (CapabilityBaseCommand & { kind: "release_task"; reason: string })
|
||||
| (CapabilityBaseCommand & {
|
||||
kind: "schedule_wake";
|
||||
reason: CapabilityWakeReason;
|
||||
payload?: CapabilityJsonValue;
|
||||
delayTicks: number;
|
||||
});
|
||||
|
||||
export interface CapabilityCommandEnvelope {
|
||||
runId: string;
|
||||
idempotencyKey: string;
|
||||
expectedRevision?: number;
|
||||
command: CapabilitySemanticCommand;
|
||||
}
|
||||
|
||||
export interface CapabilityCommandResult {
|
||||
commandId: string;
|
||||
commandKind: CapabilitySemanticCommand["kind"];
|
||||
disposition: "applied" | "duplicate";
|
||||
stateRevision: number;
|
||||
entityRefs: string[];
|
||||
scheduledWakeIds: string[];
|
||||
}
|
||||
|
||||
export interface CapabilityCommandErrorResult {
|
||||
code: string;
|
||||
message: string;
|
||||
retryable: boolean;
|
||||
}
|
||||
|
||||
export type CapabilityCommandOutcome =
|
||||
| { ok: true; result: CapabilityCommandResult }
|
||||
| {
|
||||
ok: false;
|
||||
commandKind: CapabilitySemanticCommand["kind"];
|
||||
stateRevision: number;
|
||||
error: CapabilityCommandErrorResult;
|
||||
};
|
||||
|
||||
/**
|
||||
* Claims enforced by the mock command boundary itself. The semantic catalog
|
||||
* may hide ungranted operations earlier, but direct adapter consumers cannot
|
||||
* bypass these checks.
|
||||
*/
|
||||
export const CAPABILITY_COMMAND_REQUIRED_CLAIMS = {
|
||||
report_progress: [],
|
||||
write_document: [],
|
||||
request_human_input: [],
|
||||
resolve_human_input: ["control_plane:interactions"],
|
||||
register_deliverable: [],
|
||||
set_dependencies: ["dependencies:write"],
|
||||
create_task: ["delegation:tasks:create"],
|
||||
request_approval: ["governance:approvals:request"],
|
||||
decide_approval: ["governance:approvals:decide"],
|
||||
comment_on_approval: ["governance:approvals:comment"],
|
||||
control_workspace_service: ["workspace:control"],
|
||||
record_budget_usage: ["control_plane:budget"],
|
||||
finish_task: [],
|
||||
block_task: [],
|
||||
request_review: [],
|
||||
release_task: ["control_plane:release"],
|
||||
schedule_wake: ["control_plane:wakes"],
|
||||
} as const satisfies Record<CapabilitySemanticCommand["kind"], readonly string[]>;
|
||||
|
||||
export interface CapabilityMockControlPlanePort extends ControlPlanePort {
|
||||
start(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
openFixtureRun(input: CapabilityOpenFixtureRunInput): Promise<CapabilityRunContext>;
|
||||
context(runId: string): CapabilityRunContext;
|
||||
applyCommand(envelope: CapabilityCommandEnvelope): Promise<CapabilityCommandResult>;
|
||||
tryApplyCommand(envelope: CapabilityCommandEnvelope): Promise<CapabilityCommandOutcome>;
|
||||
snapshot(): Readonly<CapabilityFixtureState>;
|
||||
decisionRecords(): readonly CapabilityDecisionRecord[];
|
||||
serialize(): string;
|
||||
}
|
||||
|
||||
export interface CapabilityFixtureSeed {
|
||||
epochMs?: number;
|
||||
company?: Partial<CapabilityFixtureCompany>;
|
||||
actors?: CapabilityFixtureActor[];
|
||||
tasks?: CapabilityFixtureTask[];
|
||||
outOfScopeTaskIds?: string[];
|
||||
comments?: CapabilityFixtureComment[];
|
||||
documents?: CapabilityFixtureDocument[];
|
||||
interactions?: CapabilityFixtureInteraction[];
|
||||
approvals?: CapabilityFixtureApproval[];
|
||||
artifacts?: CapabilityFixtureArtifact[];
|
||||
workProducts?: CapabilityFixtureWorkProduct[];
|
||||
blockers?: CapabilityFixtureBlocker[];
|
||||
workspaceServices?: CapabilityFixtureWorkspaceService[];
|
||||
budgets?: CapabilityFixtureBudget[];
|
||||
faults?: CapabilityFaultRule[];
|
||||
}
|
||||
|
||||
export function createCapabilityFixtureState(seed: CapabilityFixtureSeed = {}): CapabilityFixtureState {
|
||||
const company: CapabilityFixtureCompany = {
|
||||
id: "company-1",
|
||||
name: "Mock Paperclip Company",
|
||||
issuePrefix: "MCK",
|
||||
status: "active",
|
||||
budgetId: "budget-company-1",
|
||||
...seed.company,
|
||||
};
|
||||
const actors = seed.actors ?? [
|
||||
{
|
||||
id: "actor-1",
|
||||
companyId: company.id,
|
||||
name: "Mock Engineer",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
budgetId: "budget-actor-1",
|
||||
capabilityGrants: [],
|
||||
},
|
||||
];
|
||||
const tasks = seed.tasks ?? [
|
||||
{
|
||||
id: "task-1",
|
||||
companyId: company.id,
|
||||
identifier: `${company.issuePrefix}-1`,
|
||||
title: "Deterministic fixture task",
|
||||
description: null,
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
workMode: "standard",
|
||||
parentId: null,
|
||||
assigneeActorId: actors[0]?.id ?? null,
|
||||
checkoutRunId: null,
|
||||
executionRunId: null,
|
||||
startedAt: null,
|
||||
completedAt: null,
|
||||
},
|
||||
];
|
||||
const budgets = seed.budgets ?? [
|
||||
{
|
||||
id: company.budgetId,
|
||||
scope: "company",
|
||||
scopeId: company.id,
|
||||
limitCents: 10_000,
|
||||
spentCents: 0,
|
||||
hardStop: true,
|
||||
},
|
||||
...actors.map((actor) => ({
|
||||
id: actor.budgetId,
|
||||
scope: "actor" as const,
|
||||
scopeId: actor.id,
|
||||
limitCents: 1_000,
|
||||
spentCents: 0,
|
||||
hardStop: true,
|
||||
})),
|
||||
];
|
||||
return {
|
||||
schema: "paperclip.capability.mock-state.v1",
|
||||
revision: 0,
|
||||
clock: { epochMs: seed.epochMs ?? Date.UTC(2026, 7, 9), tick: 0 },
|
||||
counters: {},
|
||||
lifecycle: "stopped",
|
||||
activeRunId: null,
|
||||
company,
|
||||
actors: structuredClone(actors),
|
||||
tasks: structuredClone(tasks),
|
||||
outOfScopeTaskIds: structuredClone(seed.outOfScopeTaskIds ?? []),
|
||||
comments: structuredClone(seed.comments ?? []),
|
||||
documents: structuredClone(seed.documents ?? []),
|
||||
interactions: structuredClone(seed.interactions ?? []),
|
||||
approvals: structuredClone(seed.approvals ?? []),
|
||||
artifacts: structuredClone(seed.artifacts ?? []),
|
||||
workProducts: structuredClone(seed.workProducts ?? []),
|
||||
blockers: structuredClone(seed.blockers ?? []),
|
||||
workspaceServices: structuredClone(seed.workspaceServices ?? []),
|
||||
budgets: structuredClone(budgets),
|
||||
runs: [],
|
||||
wakes: [],
|
||||
audit: [],
|
||||
decisions: [],
|
||||
idempotency: [],
|
||||
faults: structuredClone(seed.faults ?? []),
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -11,6 +11,8 @@ export * from "./conformance/harness-driver.js";
|
|||
export * from "./conformance/semantic-conformance.js";
|
||||
export * from "./mock-core/deterministic-harness-driver.js";
|
||||
export * from "./mock-core/mock-control-plane-adapter.js";
|
||||
export * from "./mock-core/capability-control-plane-types.js";
|
||||
export * from "./mock-core/capability-mock-control-plane-adapter.js";
|
||||
export * from "./protocol/conformance-fixture.js";
|
||||
export * from "./protocol/replay-loader.js";
|
||||
export * from "./tracer/conformance-runner.js";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,210 @@
|
|||
import type {
|
||||
CapabilityCommandResult,
|
||||
CapabilityInteractionKind,
|
||||
CapabilityJsonValue,
|
||||
CapabilitySemanticCommand,
|
||||
} from "../mock-core/capability-control-plane-types.js";
|
||||
|
||||
export type { CapabilityJsonValue } from "../mock-core/capability-control-plane-types.js";
|
||||
|
||||
export type CapabilityToolDisposition = "always_agent_tool" | "optional_agent_tool";
|
||||
|
||||
export type CapabilityOptionalCatalogGroup =
|
||||
| "discovery"
|
||||
| "delegation_dependencies"
|
||||
| "governance"
|
||||
| "cases"
|
||||
| "workspace_runtime"
|
||||
| "routines"
|
||||
| "company_skills"
|
||||
| "secrets"
|
||||
| "portability_admin"
|
||||
| "test_escape_hatch";
|
||||
|
||||
export type CapabilitySideEffectClass =
|
||||
| "read"
|
||||
| "task_write"
|
||||
| "company_write"
|
||||
| "governance"
|
||||
| "workspace_control"
|
||||
| "secret_read"
|
||||
| "admin"
|
||||
| "test_escape_hatch";
|
||||
|
||||
export type ScenarioChatdempotencyBehavior = "none" | "required" | "recommended";
|
||||
|
||||
export type CapabilityToolTaskMode = "standard" | "ask" | "planning" | "skill_test";
|
||||
|
||||
export interface CapabilityJsonSchema {
|
||||
$schema?: string;
|
||||
type?: "object" | "array" | "string" | "number" | "integer" | "boolean" | "null";
|
||||
title?: string;
|
||||
description?: string;
|
||||
properties?: Record<string, CapabilityJsonSchema>;
|
||||
required?: string[];
|
||||
additionalProperties?: boolean | CapabilityJsonSchema;
|
||||
items?: CapabilityJsonSchema;
|
||||
enum?: CapabilityJsonValue[];
|
||||
minLength?: number;
|
||||
minimum?: number;
|
||||
oneOf?: CapabilityJsonSchema[];
|
||||
}
|
||||
|
||||
export interface CapabilityRedactionRule {
|
||||
path: string;
|
||||
replacement: "[REDACTED]" | "[SECRET_VALUE]";
|
||||
appliesTo: ReadonlyArray<"input" | "output" | "error" | "authorization_record">;
|
||||
}
|
||||
|
||||
export type CapabilityMockCommandMapping =
|
||||
| { kind: "context_read"; projection: string }
|
||||
| { kind: "snapshot_read"; projection: string }
|
||||
| { kind: "semantic_command"; commandKind: CapabilitySemanticCommand["kind"] }
|
||||
| { kind: "operation_result" }
|
||||
| { kind: "mock_extension"; extension: string };
|
||||
|
||||
export interface CapabilitySemanticToolDescriptor {
|
||||
operationId: string;
|
||||
version: 1;
|
||||
title: string;
|
||||
description: string;
|
||||
inputSchema: CapabilityJsonSchema;
|
||||
outputSchema: CapabilityJsonSchema;
|
||||
disposition: CapabilityToolDisposition;
|
||||
optionalGroup: CapabilityOptionalCatalogGroup | null;
|
||||
requiredClaims: string[];
|
||||
allowedRoles?: string[];
|
||||
taskModes: CapabilityToolTaskMode[];
|
||||
sideEffectClass: CapabilitySideEffectClass;
|
||||
idempotency: ScenarioChatdempotencyBehavior;
|
||||
redaction: CapabilityRedactionRule[];
|
||||
mockCommandMapping: CapabilityMockCommandMapping;
|
||||
}
|
||||
|
||||
export interface CapabilityScenarioToolPolicy {
|
||||
deniedOperationIds?: string[];
|
||||
deniedClaims?: string[];
|
||||
allowedInteractionKinds?: CapabilityInteractionKind[];
|
||||
allowSecretValueAccess?: boolean;
|
||||
allowGenericEscapeHatch?: boolean;
|
||||
genericEscapeHatchAllowlist?: Array<{ method: string; path: string }>;
|
||||
}
|
||||
|
||||
export interface CapabilityToolAuthorizationContext {
|
||||
runId: string;
|
||||
actor: {
|
||||
id: string;
|
||||
role: string;
|
||||
capabilityGrants: string[];
|
||||
};
|
||||
task: {
|
||||
id: string;
|
||||
assigneeActorId: string | null;
|
||||
workMode: CapabilityToolTaskMode;
|
||||
};
|
||||
scenarioGrants: string[];
|
||||
policy?: CapabilityScenarioToolPolicy;
|
||||
}
|
||||
|
||||
export type CapabilityAuthorizationOutcome = "exposed" | "allowed" | "denied" | "absent";
|
||||
|
||||
export interface CapabilityAuthorizationRecord {
|
||||
schema: "paperclip.capability.authorization-record.v1";
|
||||
sequence: number;
|
||||
runId: string;
|
||||
actorId: string;
|
||||
taskId: string;
|
||||
operationId: string;
|
||||
phase: "exposure" | "invocation";
|
||||
outcome: CapabilityAuthorizationOutcome;
|
||||
consideredClaims: string[];
|
||||
grantedClaims: string[];
|
||||
missingClaims: string[];
|
||||
reason: string;
|
||||
redactions: Array<{ path: string; replacement: string }>;
|
||||
resultingStateChange: {
|
||||
beforeRevision: number | null;
|
||||
afterRevision: number | null;
|
||||
entityRefs: string[];
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface CapabilityVisibleToolSet {
|
||||
schema: "paperclip.capability.visible-tools.v1";
|
||||
tools: CapabilitySemanticToolDescriptor[];
|
||||
authorizationRecords: CapabilityAuthorizationRecord[];
|
||||
}
|
||||
|
||||
export interface CapabilityPolicyDenial {
|
||||
schema: "paperclip.capability.tool-result.v1";
|
||||
ok: false;
|
||||
error: {
|
||||
code: "policy_denied" | "operation_absent" | "input_invalid" | "operation_unsupported";
|
||||
message: string;
|
||||
operationId: string;
|
||||
reason: string;
|
||||
};
|
||||
authorization: CapabilityAuthorizationRecord;
|
||||
}
|
||||
|
||||
export interface CapabilityToolSuccess {
|
||||
schema: "paperclip.capability.tool-result.v1";
|
||||
ok: true;
|
||||
operationId: string;
|
||||
operationResultId: string;
|
||||
value: CapabilityJsonValue;
|
||||
commandResult: CapabilityCommandResult | null;
|
||||
authorization: CapabilityAuthorizationRecord;
|
||||
}
|
||||
|
||||
export type CapabilityToolInvocationResult = CapabilityToolSuccess | CapabilityPolicyDenial;
|
||||
|
||||
/**
|
||||
* A result intended only for the provider/model tool-response channel.
|
||||
*
|
||||
* The distinct schema prevents this value from being assigned to an observable
|
||||
* tool-result sink. Callers receive it only by explicitly opening a
|
||||
* {@link CapabilityModelToolDelivery}; the delivery capsule itself serializes to
|
||||
* its redacted observable result.
|
||||
*/
|
||||
export interface CapabilityModelToolSuccess {
|
||||
schema: "paperclip.capability.model-tool-result.v1";
|
||||
ok: true;
|
||||
operationId: string;
|
||||
operationResultId: string;
|
||||
value: CapabilityJsonValue;
|
||||
commandResult: CapabilityCommandResult | null;
|
||||
authorization: CapabilityAuthorizationRecord;
|
||||
}
|
||||
|
||||
export type CapabilityModelToolInvocationResult = CapabilityModelToolSuccess | CapabilityPolicyDenial;
|
||||
|
||||
export interface CapabilityModelToolDelivery {
|
||||
/** Safe for traces, snapshots, persistence, errors, and browser payloads. */
|
||||
readonly observableResult: CapabilityToolInvocationResult;
|
||||
|
||||
/** Explicitly opens the narrowly scoped provider/model delivery value. */
|
||||
readModelResult(): CapabilityModelToolInvocationResult;
|
||||
|
||||
/** Accidental JSON serialization is always the observable redacted form. */
|
||||
toJSON(): CapabilityToolInvocationResult;
|
||||
}
|
||||
|
||||
export interface CapabilityToolInvocation {
|
||||
operationId: string;
|
||||
input: CapabilityJsonValue;
|
||||
idempotencyKey?: string;
|
||||
}
|
||||
|
||||
export const CAPABILITY_CONTROL_PLANE_OWNED_OPERATION_IDS = [
|
||||
"checkout_task",
|
||||
"release_task",
|
||||
"select_work",
|
||||
"route_wake",
|
||||
"enforce_budget",
|
||||
"append_audit_record",
|
||||
"persist_run",
|
||||
"replay_run",
|
||||
"schedule_blocker_wake",
|
||||
"reconcile_run",
|
||||
] as const;
|
||||
Loading…
Reference in New Issue