@fusionkit/protocol 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +106 -0
- package/dist/api.js +6 -0
- package/dist/chain.d.ts +14 -0
- package/dist/chain.js +49 -0
- package/dist/constants.d.ts +25 -0
- package/dist/constants.js +36 -0
- package/dist/contract.d.ts +6 -0
- package/dist/contract.js +32 -0
- package/dist/execution.d.ts +67 -0
- package/dist/execution.js +27 -0
- package/dist/generated/model-fusion-openapi.d.ts +44 -0
- package/dist/generated/model-fusion-openapi.js +23 -0
- package/dist/hash.d.ts +10 -0
- package/dist/hash.js +31 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +30 -0
- package/dist/jcs.d.ts +14 -0
- package/dist/jcs.js +49 -0
- package/dist/keys.d.ts +14 -0
- package/dist/keys.js +36 -0
- package/dist/model-fusion.d.ts +167 -0
- package/dist/model-fusion.js +596 -0
- package/dist/receipt-story.d.ts +27 -0
- package/dist/receipt-story.js +127 -0
- package/dist/receipt.d.ts +20 -0
- package/dist/receipt.js +162 -0
- package/dist/test/model-fusion.test.d.ts +1 -0
- package/dist/test/model-fusion.test.js +213 -0
- package/dist/test/protocol.test.d.ts +1 -0
- package/dist/test/protocol.test.js +240 -0
- package/dist/test/tool-executor.test.d.ts +1 -0
- package/dist/test/tool-executor.test.js +86 -0
- package/dist/test/trace.test.d.ts +1 -0
- package/dist/test/trace.test.js +75 -0
- package/dist/tool-executor.d.ts +58 -0
- package/dist/tool-executor.js +80 -0
- package/dist/trace.d.ts +119 -0
- package/dist/trace.js +248 -0
- package/dist/types.d.ts +375 -0
- package/dist/types.js +14 -0
- package/dist/validators.d.ts +7 -0
- package/dist/validators.js +32 -0
- package/dist/vocabulary.d.ts +12 -0
- package/dist/vocabulary.js +79 -0
- package/package.json +27 -0
package/dist/trace.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fusion-trace-event.v1 — fire-and-forget observability emitter.
|
|
3
|
+
*
|
|
4
|
+
* This is the canonical TypeScript implementation of the standalone fusion-trace
|
|
5
|
+
* contract (see fusionkit `spec/fusion-trace`). It lives in `@fusionkit/protocol`
|
|
6
|
+
* (a dependency-free leaf) so the gateway, ensemble harness, the AI SDK worktree
|
|
7
|
+
* agent, and the CLI can all emit against the same shape without import cycles.
|
|
8
|
+
*
|
|
9
|
+
* Emission is a no-op unless `FUSION_TRACE_URL` or `FUSION_TRACE_DIR` is set, so
|
|
10
|
+
* normal runs are never blocked by, or coupled to, the collector.
|
|
11
|
+
*/
|
|
12
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
15
|
+
export const FUSION_TRACE_EVENT_SCHEMA = "fusion-trace-event.v1";
|
|
16
|
+
export const FUSION_TRACE_EVENT_VERSION = "1.0.0";
|
|
17
|
+
export const TRACE_ID_HEADER = "x-fusion-trace-id";
|
|
18
|
+
export const TRACE_SPAN_HEADER = "x-fusion-span-id";
|
|
19
|
+
export const TRACE_PARENT_SPAN_HEADER = "x-fusion-parent-span-id";
|
|
20
|
+
export const TRACE_CANDIDATE_HEADER = "x-fusion-candidate-id";
|
|
21
|
+
/** Runtime-iterable mirrors of the closed unions (used by the validator). */
|
|
22
|
+
export const FUSION_TRACE_COMPONENTS = [
|
|
23
|
+
"gateway",
|
|
24
|
+
"ensemble",
|
|
25
|
+
"agent",
|
|
26
|
+
"panel-model",
|
|
27
|
+
"judge",
|
|
28
|
+
"synthesis",
|
|
29
|
+
"cursor-bridge"
|
|
30
|
+
];
|
|
31
|
+
export const FUSION_TRACE_EVENT_TYPES = [
|
|
32
|
+
"session.started",
|
|
33
|
+
"session.finished",
|
|
34
|
+
"harness.candidate.started",
|
|
35
|
+
"harness.candidate.finished",
|
|
36
|
+
"trajectory.step",
|
|
37
|
+
"model.call.started",
|
|
38
|
+
"model.call.finished",
|
|
39
|
+
"judge.request",
|
|
40
|
+
"judge.thinking",
|
|
41
|
+
"judge.scored",
|
|
42
|
+
"judge.synthesis",
|
|
43
|
+
"judge.final",
|
|
44
|
+
"tool.execution",
|
|
45
|
+
"cursor.route",
|
|
46
|
+
"log"
|
|
47
|
+
];
|
|
48
|
+
export function newTraceId() {
|
|
49
|
+
return `trace_${randomUUID().replace(/-/g, "")}`;
|
|
50
|
+
}
|
|
51
|
+
export function newSpanId() {
|
|
52
|
+
return `span_${randomUUID().replace(/-/g, "").slice(0, 12)}`;
|
|
53
|
+
}
|
|
54
|
+
export function ambientTraceId() {
|
|
55
|
+
const value = process.env.FUSION_TRACE_ID;
|
|
56
|
+
return value && value.length > 0 ? value : undefined;
|
|
57
|
+
}
|
|
58
|
+
export class TraceEmitter {
|
|
59
|
+
url;
|
|
60
|
+
dir;
|
|
61
|
+
enabled;
|
|
62
|
+
seq = 0;
|
|
63
|
+
dirReady = false;
|
|
64
|
+
constructor(config) {
|
|
65
|
+
this.url = config?.url ?? process.env.FUSION_TRACE_URL ?? undefined;
|
|
66
|
+
this.dir = config?.dir ?? process.env.FUSION_TRACE_DIR ?? undefined;
|
|
67
|
+
this.enabled = Boolean(this.url || this.dir);
|
|
68
|
+
}
|
|
69
|
+
isEnabled() {
|
|
70
|
+
return this.enabled;
|
|
71
|
+
}
|
|
72
|
+
emit(input) {
|
|
73
|
+
if (!this.enabled)
|
|
74
|
+
return;
|
|
75
|
+
const traceId = input.traceId ?? ambientTraceId();
|
|
76
|
+
if (traceId === undefined)
|
|
77
|
+
return;
|
|
78
|
+
const event = {
|
|
79
|
+
schema: FUSION_TRACE_EVENT_SCHEMA,
|
|
80
|
+
schema_version: FUSION_TRACE_EVENT_VERSION,
|
|
81
|
+
trace_id: traceId,
|
|
82
|
+
span_id: input.spanId ?? newSpanId(),
|
|
83
|
+
seq: this.seq++,
|
|
84
|
+
ts: Date.now(),
|
|
85
|
+
component: input.component,
|
|
86
|
+
event_type: input.event_type,
|
|
87
|
+
...(input.parentSpanId !== undefined ? { parent_span_id: input.parentSpanId } : {}),
|
|
88
|
+
...(input.sessionId !== undefined ? { session_id: input.sessionId } : {}),
|
|
89
|
+
...(input.candidateId !== undefined ? { candidate_id: input.candidateId } : {}),
|
|
90
|
+
...(input.modelId !== undefined ? { model_id: input.modelId } : {}),
|
|
91
|
+
...(input.payload !== undefined ? { payload: input.payload } : {})
|
|
92
|
+
};
|
|
93
|
+
this.writeJsonl(event);
|
|
94
|
+
void this.post(event);
|
|
95
|
+
}
|
|
96
|
+
writeJsonl(event) {
|
|
97
|
+
if (this.dir === undefined)
|
|
98
|
+
return;
|
|
99
|
+
try {
|
|
100
|
+
if (!this.dirReady) {
|
|
101
|
+
mkdirSync(this.dir, { recursive: true });
|
|
102
|
+
this.dirReady = true;
|
|
103
|
+
}
|
|
104
|
+
appendFileSync(join(this.dir, `${event.trace_id}.jsonl`), `${JSON.stringify(event)}\n`);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// best-effort durable fallback
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async post(event) {
|
|
111
|
+
if (this.url === undefined)
|
|
112
|
+
return;
|
|
113
|
+
try {
|
|
114
|
+
const controller = new AbortController();
|
|
115
|
+
const timeout = setTimeout(() => controller.abort(), 2_000);
|
|
116
|
+
await fetch(this.url, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: { "content-type": "application/json" },
|
|
119
|
+
body: JSON.stringify({ events: [event] }),
|
|
120
|
+
signal: controller.signal
|
|
121
|
+
}).catch(() => undefined);
|
|
122
|
+
clearTimeout(timeout);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// collector being down must never break a run
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
let defaultEmitter;
|
|
130
|
+
export function getTraceEmitter() {
|
|
131
|
+
if (defaultEmitter === undefined)
|
|
132
|
+
defaultEmitter = new TraceEmitter();
|
|
133
|
+
return defaultEmitter;
|
|
134
|
+
}
|
|
135
|
+
export function emitTrace(input) {
|
|
136
|
+
getTraceEmitter().emit(input);
|
|
137
|
+
}
|
|
138
|
+
// ---- runtime validation (the formalized fusion-trace-event.v1 contract) ----
|
|
139
|
+
function fail(message) {
|
|
140
|
+
throw new Error(`invalid fusion-trace-event.v1: ${message}`);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Assert that `value` is a well-formed wire `FusionTraceEvent`. Hand-written
|
|
144
|
+
* (Node-only, no deps) to match the `assertModelFusionRecord` style, so both the
|
|
145
|
+
* emitter side and the scope ingest boundary validate against one contract.
|
|
146
|
+
*/
|
|
147
|
+
export function assertFusionTraceEvent(value) {
|
|
148
|
+
if (typeof value !== "object" || value === null)
|
|
149
|
+
fail("event must be an object");
|
|
150
|
+
const event = value;
|
|
151
|
+
if (event.schema !== FUSION_TRACE_EVENT_SCHEMA)
|
|
152
|
+
fail(`schema must be "${FUSION_TRACE_EVENT_SCHEMA}"`);
|
|
153
|
+
if (typeof event.trace_id !== "string" || event.trace_id.length === 0)
|
|
154
|
+
fail("trace_id must be a non-empty string");
|
|
155
|
+
if (typeof event.span_id !== "string" || event.span_id.length === 0)
|
|
156
|
+
fail("span_id must be a non-empty string");
|
|
157
|
+
if (typeof event.seq !== "number" || !Number.isFinite(event.seq))
|
|
158
|
+
fail("seq must be a finite number");
|
|
159
|
+
if (typeof event.ts !== "number" || !Number.isFinite(event.ts))
|
|
160
|
+
fail("ts must be a finite number");
|
|
161
|
+
if (!FUSION_TRACE_COMPONENTS.includes(event.component)) {
|
|
162
|
+
fail(`unknown component "${String(event.component)}"`);
|
|
163
|
+
}
|
|
164
|
+
if (!FUSION_TRACE_EVENT_TYPES.includes(event.event_type)) {
|
|
165
|
+
fail(`unknown event_type "${String(event.event_type)}"`);
|
|
166
|
+
}
|
|
167
|
+
if (event.parent_span_id !== undefined && typeof event.parent_span_id !== "string") {
|
|
168
|
+
fail("parent_span_id must be a string when present");
|
|
169
|
+
}
|
|
170
|
+
for (const key of ["session_id", "candidate_id", "model_id"]) {
|
|
171
|
+
if (event[key] !== undefined && typeof event[key] !== "string")
|
|
172
|
+
fail(`${key} must be a string when present`);
|
|
173
|
+
}
|
|
174
|
+
if (event.payload !== undefined &&
|
|
175
|
+
(typeof event.payload !== "object" || event.payload === null || Array.isArray(event.payload))) {
|
|
176
|
+
fail("payload must be a plain object when present");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
export function isFusionTraceEvent(value) {
|
|
180
|
+
try {
|
|
181
|
+
assertFusionTraceEvent(value);
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// ---- typed per-event payload builders (snake_case wire shape) ----
|
|
189
|
+
//
|
|
190
|
+
// One builder per emitted event keeps payload field names consistent across
|
|
191
|
+
// every emit site and documents exactly what each event carries. The scope
|
|
192
|
+
// collector reads these field names directly.
|
|
193
|
+
export function judgeRequestPayload(input) {
|
|
194
|
+
return {
|
|
195
|
+
...(input.judgeModel !== undefined ? { judge_model: input.judgeModel } : {}),
|
|
196
|
+
messages: input.messages,
|
|
197
|
+
trajectories: input.trajectories,
|
|
198
|
+
...(input.tools !== undefined ? { tools: input.tools } : {}),
|
|
199
|
+
...(input.toolChoice !== undefined ? { tool_choice: input.toolChoice } : {}),
|
|
200
|
+
...(input.trajectoryIds !== undefined ? { trajectory_ids: input.trajectoryIds } : {}),
|
|
201
|
+
...(input.turn !== undefined ? { turn: input.turn } : {})
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/** An intermediate (tool-calling) judge step within a turn. */
|
|
205
|
+
export function judgeThinkingPayload(input) {
|
|
206
|
+
return {
|
|
207
|
+
...(input.rawAnalysis !== undefined ? { raw_analysis: input.rawAnalysis } : {}),
|
|
208
|
+
...(input.toolCalls !== undefined ? { tool_calls: input.toolCalls } : {}),
|
|
209
|
+
...(input.usage !== undefined ? { usage: input.usage } : {}),
|
|
210
|
+
...(input.turn !== undefined ? { turn: input.turn } : {})
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
export function judgeFinalPayload(input) {
|
|
214
|
+
const finalOutput = input.finalOutput ?? input.content;
|
|
215
|
+
return {
|
|
216
|
+
...(finalOutput !== undefined ? { final_output: finalOutput, record: { final_output: finalOutput } } : {}),
|
|
217
|
+
...(input.content !== undefined ? { content: input.content } : {}),
|
|
218
|
+
...(input.toolCalls !== undefined ? { tool_calls: input.toolCalls } : {}),
|
|
219
|
+
...(input.usage !== undefined ? { usage: input.usage } : {}),
|
|
220
|
+
...(input.httpStatus !== undefined ? { http_status: input.httpStatus } : {}),
|
|
221
|
+
...(input.error !== undefined ? { error: input.error } : {}),
|
|
222
|
+
...(input.turn !== undefined ? { turn: input.turn } : {})
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
export function modelCallStartedPayload(input) {
|
|
226
|
+
return {
|
|
227
|
+
model: input.model,
|
|
228
|
+
...(input.systemPrompt !== undefined ? { system_prompt: input.systemPrompt } : {}),
|
|
229
|
+
...(input.prompt !== undefined ? { prompt: input.prompt } : {}),
|
|
230
|
+
...(input.tools !== undefined ? { tools: input.tools } : {}),
|
|
231
|
+
...(input.turn !== undefined ? { turn: input.turn } : {})
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
export function modelCallFinishedPayload(input) {
|
|
235
|
+
return {
|
|
236
|
+
model: input.model,
|
|
237
|
+
...(input.finalOutput !== undefined
|
|
238
|
+
? { final_output: input.finalOutput, content_preview: input.finalOutput.slice(0, 280) }
|
|
239
|
+
: {}),
|
|
240
|
+
...(input.usage !== undefined ? { usage: input.usage } : {}),
|
|
241
|
+
...(input.finishReason !== undefined ? { finish_reason: input.finishReason } : {}),
|
|
242
|
+
...(input.stepCount !== undefined ? { step_count: input.stepCount } : {}),
|
|
243
|
+
...(input.toolCallCount !== undefined ? { tool_call_count: input.toolCallCount } : {}),
|
|
244
|
+
...(input.latencyS !== undefined ? { latency_s: input.latencyS } : {}),
|
|
245
|
+
...(input.error !== undefined ? { error: input.error } : {}),
|
|
246
|
+
...(input.turn !== undefined ? { turn: input.turn } : {})
|
|
247
|
+
};
|
|
248
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Warrant protocol types. These are the schemas marked for open
|
|
3
|
+
* publication in the spec (warrant.contract.v1, warrant.receipt.v1,
|
|
4
|
+
* warrant.event.v1, warrant.manifest.v1, warrant.policy.v1).
|
|
5
|
+
*/
|
|
6
|
+
import type { JsonValue } from "./jcs.js";
|
|
7
|
+
import type { ExecutionSpec } from "./execution.js";
|
|
8
|
+
export type RunStatus = "created" | "claimed" | "provisioning" | "running" | "awaiting_approval" | "completed" | "failed" | "cancelled";
|
|
9
|
+
export type FailureClass = "policy_denied" | "consent_timeout" | "capability_mismatch" | "attestation_failed" | "secret_release_denied" | "capture_failed" | "transfer_failed" | "session_failed" | "side_effect_conflict" | "budget_exceeded";
|
|
10
|
+
export type DisclosureMode = "none" | "metadata-only" | "redacted" | "minimal-context" | "full";
|
|
11
|
+
export type CheckpointTier = "semantic" | "workspace";
|
|
12
|
+
export type AttestationTier = "mock" | "standard" | "zdr" | "cpu-tee" | "cpu-gpu-tee";
|
|
13
|
+
/**
|
|
14
|
+
* How the runner isolates the agent session, recorded honestly in receipts:
|
|
15
|
+
*
|
|
16
|
+
* - "process": a child process with a scrubbed environment and an egress
|
|
17
|
+
* proxy. Enforcement is process-level — a malicious binary can ignore
|
|
18
|
+
* proxy variables — but every attempt is recorded.
|
|
19
|
+
* - "hermetic": a simulated bash interpreter (just-bash) with a virtual
|
|
20
|
+
* filesystem rooted in the workspace and interpreter-enforced network
|
|
21
|
+
* allowlists. There are no real processes or sockets to escape with;
|
|
22
|
+
* only the "command" harness can run here.
|
|
23
|
+
* - "vercel-sandbox": a Firecracker microVM (Vercel Sandbox) with
|
|
24
|
+
* VM-level isolation and domain-based egress policy.
|
|
25
|
+
*/
|
|
26
|
+
export type SessionIsolation = "process" | "hermetic" | "vercel-sandbox";
|
|
27
|
+
/**
|
|
28
|
+
* "command" is the harness for app-owned loops and the compute adapter:
|
|
29
|
+
* a single shell command executed inside a governed session. "mock" is the
|
|
30
|
+
* built-in test harness. "pi" is a host-runtime harness with no vendor CLI:
|
|
31
|
+
* it runs only through the AI SDK harness session backend, never as a
|
|
32
|
+
* spawned process. The rest are vendor CLIs wrapped as-is.
|
|
33
|
+
*/
|
|
34
|
+
export type AgentKind = "claude-code" | "codex" | "pi" | "mock" | "command";
|
|
35
|
+
export type AgentSpec = {
|
|
36
|
+
kind: AgentKind;
|
|
37
|
+
version?: string;
|
|
38
|
+
};
|
|
39
|
+
export type TaskSpec = {
|
|
40
|
+
prompt: string;
|
|
41
|
+
};
|
|
42
|
+
export type RunnerSelector = {
|
|
43
|
+
pool: string;
|
|
44
|
+
};
|
|
45
|
+
export type ActorRef = {
|
|
46
|
+
kind: "human" | "service";
|
|
47
|
+
id: string;
|
|
48
|
+
};
|
|
49
|
+
export type KeyRef = {
|
|
50
|
+
keyId: string;
|
|
51
|
+
role: "org" | "plane" | "runner";
|
|
52
|
+
};
|
|
53
|
+
export type Signature = {
|
|
54
|
+
keyId: string;
|
|
55
|
+
alg: "ed25519";
|
|
56
|
+
signer: "org" | "plane" | "runner";
|
|
57
|
+
sig: string;
|
|
58
|
+
};
|
|
59
|
+
export type ManifestFile = {
|
|
60
|
+
path: string;
|
|
61
|
+
hash: string;
|
|
62
|
+
bytes: number;
|
|
63
|
+
};
|
|
64
|
+
export type WorkspaceManifest = {
|
|
65
|
+
version: "warrant.manifest.v1";
|
|
66
|
+
baseRef: string;
|
|
67
|
+
/** Hash of the git bundle blob containing history up to baseRef. */
|
|
68
|
+
bundleHash: string;
|
|
69
|
+
/** Hash of the binary diff blob for staged+unstaged changes, if any. */
|
|
70
|
+
dirtyDiffHash?: string;
|
|
71
|
+
/** Allowlisted untracked files, content-addressed. */
|
|
72
|
+
untrackedFiles: ManifestFile[];
|
|
73
|
+
/** Patterns that were denied capture; recorded so absence is provable. */
|
|
74
|
+
deniedPatterns: string[];
|
|
75
|
+
/** Paths that matched a deny pattern and were excluded. */
|
|
76
|
+
deniedPaths: string[];
|
|
77
|
+
};
|
|
78
|
+
export type SecretClaim = {
|
|
79
|
+
name: string;
|
|
80
|
+
scope: string;
|
|
81
|
+
};
|
|
82
|
+
export type NetworkPolicy = {
|
|
83
|
+
defaultDeny: boolean;
|
|
84
|
+
allowHosts: string[];
|
|
85
|
+
};
|
|
86
|
+
export type BudgetSpec = {
|
|
87
|
+
maxSpendUsd?: number;
|
|
88
|
+
maxDurationMin?: number;
|
|
89
|
+
};
|
|
90
|
+
export type RunContract = {
|
|
91
|
+
version: "warrant.contract.v1";
|
|
92
|
+
runId: string;
|
|
93
|
+
issuedAt: string;
|
|
94
|
+
issuer: KeyRef;
|
|
95
|
+
requestedBy: ActorRef;
|
|
96
|
+
approvedBy?: ActorRef[];
|
|
97
|
+
agent: AgentSpec;
|
|
98
|
+
task: TaskSpec;
|
|
99
|
+
runner: RunnerSelector;
|
|
100
|
+
workspace: WorkspaceManifest;
|
|
101
|
+
policyHash: string;
|
|
102
|
+
secrets: SecretClaim[];
|
|
103
|
+
network: NetworkPolicy;
|
|
104
|
+
budget: BudgetSpec;
|
|
105
|
+
disclosure: DisclosureMode;
|
|
106
|
+
/** Durable machine intent. If absent, legacy contracts derive execution from agent/task. */
|
|
107
|
+
execution?: ExecutionSpec;
|
|
108
|
+
/** Requested session isolation. Defaults to "process". */
|
|
109
|
+
isolation?: SessionIsolation;
|
|
110
|
+
/** Present when this run continues prior work from a handoff envelope. */
|
|
111
|
+
continuation?: ContinuationRef;
|
|
112
|
+
expiresAt: string;
|
|
113
|
+
signatures: Signature[];
|
|
114
|
+
};
|
|
115
|
+
export type DataClassRule = {
|
|
116
|
+
dataClass: string;
|
|
117
|
+
allowPools: string[];
|
|
118
|
+
};
|
|
119
|
+
export type SecretScopeRule = {
|
|
120
|
+
name: string;
|
|
121
|
+
scope: string;
|
|
122
|
+
pools: string[];
|
|
123
|
+
};
|
|
124
|
+
export type ConsentRule = {
|
|
125
|
+
when: "secret-release" | "any-run" | "agent-kind";
|
|
126
|
+
match?: string;
|
|
127
|
+
approvers: string[];
|
|
128
|
+
};
|
|
129
|
+
export type RetentionPolicy = {
|
|
130
|
+
receiptsDays: number;
|
|
131
|
+
artifactsDays: number;
|
|
132
|
+
};
|
|
133
|
+
export type Policy = {
|
|
134
|
+
version: "warrant.policy.v1";
|
|
135
|
+
runners: {
|
|
136
|
+
allowPools: string[];
|
|
137
|
+
};
|
|
138
|
+
agents: {
|
|
139
|
+
allow: AgentKind[];
|
|
140
|
+
};
|
|
141
|
+
dataClasses: DataClassRule[];
|
|
142
|
+
network: NetworkPolicy;
|
|
143
|
+
secrets: {
|
|
144
|
+
releasable: SecretScopeRule[];
|
|
145
|
+
};
|
|
146
|
+
budget: {
|
|
147
|
+
maxSpendUsd: number;
|
|
148
|
+
maxDurationMin: number;
|
|
149
|
+
};
|
|
150
|
+
consent: ConsentRule[];
|
|
151
|
+
retention: RetentionPolicy;
|
|
152
|
+
};
|
|
153
|
+
export type ArtifactKind = "diff" | "log" | "file" | "bundle" | "trace";
|
|
154
|
+
export type RunEvent = {
|
|
155
|
+
type: "run.created";
|
|
156
|
+
} | {
|
|
157
|
+
type: "run.claimed";
|
|
158
|
+
runnerId: string;
|
|
159
|
+
runnerKeyId: string;
|
|
160
|
+
} | {
|
|
161
|
+
type: "workspace.materialized";
|
|
162
|
+
manifestHash: string;
|
|
163
|
+
} | {
|
|
164
|
+
type: "policy.evaluated";
|
|
165
|
+
decision: "allow" | "ask";
|
|
166
|
+
reason: string;
|
|
167
|
+
} | {
|
|
168
|
+
type: "consent.requested";
|
|
169
|
+
requirement: string;
|
|
170
|
+
} | {
|
|
171
|
+
type: "consent.granted";
|
|
172
|
+
actor: ActorRef;
|
|
173
|
+
} | {
|
|
174
|
+
type: "secret.released";
|
|
175
|
+
name: string;
|
|
176
|
+
scope: string;
|
|
177
|
+
} | {
|
|
178
|
+
type: "command.executed";
|
|
179
|
+
argvHash: string;
|
|
180
|
+
exitCode: number;
|
|
181
|
+
} | {
|
|
182
|
+
type: "file.changed";
|
|
183
|
+
path: string;
|
|
184
|
+
contentHash: string;
|
|
185
|
+
} | {
|
|
186
|
+
type: "network.connected";
|
|
187
|
+
host: string;
|
|
188
|
+
decision: "allowed" | "blocked";
|
|
189
|
+
} | {
|
|
190
|
+
type: "model.called";
|
|
191
|
+
provider: string;
|
|
192
|
+
model: string;
|
|
193
|
+
} | {
|
|
194
|
+
type: "boundary.crossed";
|
|
195
|
+
direction: "out" | "in";
|
|
196
|
+
contentHash: string;
|
|
197
|
+
dataClass: string;
|
|
198
|
+
} | {
|
|
199
|
+
type: "artifact.created";
|
|
200
|
+
kind: ArtifactKind;
|
|
201
|
+
hash: string;
|
|
202
|
+
} | {
|
|
203
|
+
type: "checkpoint.created";
|
|
204
|
+
checkpointId: string;
|
|
205
|
+
tier: CheckpointTier;
|
|
206
|
+
} | {
|
|
207
|
+
type: "run.completed";
|
|
208
|
+
} | {
|
|
209
|
+
type: "run.failed";
|
|
210
|
+
failure: FailureClass;
|
|
211
|
+
message: string;
|
|
212
|
+
} | {
|
|
213
|
+
type: "run.cancelled";
|
|
214
|
+
actor: ActorRef;
|
|
215
|
+
};
|
|
216
|
+
export type ChainedEvent = {
|
|
217
|
+
version: "warrant.event.v1";
|
|
218
|
+
seq: number;
|
|
219
|
+
ts: string;
|
|
220
|
+
/** Hash of the previous chained event; the genesis event uses the contract hash. */
|
|
221
|
+
prev: string;
|
|
222
|
+
event: RunEvent;
|
|
223
|
+
/** sha256 over canonical JSON of {seq, ts, prev, event}. */
|
|
224
|
+
hash: string;
|
|
225
|
+
};
|
|
226
|
+
export type RunnerIdentity = {
|
|
227
|
+
runnerId: string;
|
|
228
|
+
keyId: string;
|
|
229
|
+
pool: string;
|
|
230
|
+
attestationTier: AttestationTier;
|
|
231
|
+
/** How the session was actually isolated. Absent in older receipts means "process". */
|
|
232
|
+
isolation?: SessionIsolation;
|
|
233
|
+
};
|
|
234
|
+
export type SecretReleaseRecord = {
|
|
235
|
+
name: string;
|
|
236
|
+
scope: string;
|
|
237
|
+
ts: string;
|
|
238
|
+
};
|
|
239
|
+
export type NetworkAccessRecord = {
|
|
240
|
+
host: string;
|
|
241
|
+
decision: "allowed" | "blocked";
|
|
242
|
+
};
|
|
243
|
+
export type ModelUsageRecord = {
|
|
244
|
+
provider: string;
|
|
245
|
+
model: string;
|
|
246
|
+
};
|
|
247
|
+
export type DisclosureRecord = {
|
|
248
|
+
direction: "out" | "in";
|
|
249
|
+
contentHash: string;
|
|
250
|
+
dataClass: string;
|
|
251
|
+
};
|
|
252
|
+
export type Receipt = {
|
|
253
|
+
version: "warrant.receipt.v1";
|
|
254
|
+
runId: string;
|
|
255
|
+
contractHash: string;
|
|
256
|
+
runner: RunnerIdentity;
|
|
257
|
+
startedAt: string;
|
|
258
|
+
endedAt: string;
|
|
259
|
+
status: Extract<RunStatus, "completed" | "failed" | "cancelled">;
|
|
260
|
+
eventsHead: string;
|
|
261
|
+
eventCount: number;
|
|
262
|
+
workspaceIn: {
|
|
263
|
+
baseRef: string;
|
|
264
|
+
manifestHash: string;
|
|
265
|
+
};
|
|
266
|
+
workspaceOut: {
|
|
267
|
+
diffHash: string;
|
|
268
|
+
artifactHashes: string[];
|
|
269
|
+
};
|
|
270
|
+
secretsReleased: SecretReleaseRecord[];
|
|
271
|
+
networkAccessed: NetworkAccessRecord[];
|
|
272
|
+
modelsUsed: ModelUsageRecord[];
|
|
273
|
+
boundaryDisclosures: DisclosureRecord[];
|
|
274
|
+
costUsd?: number;
|
|
275
|
+
signatures: Signature[];
|
|
276
|
+
};
|
|
277
|
+
/**
|
|
278
|
+
* Handoff protocol objects (warrant.checkpoint.v1, warrant.envelope.v1).
|
|
279
|
+
*
|
|
280
|
+
* A checkpoint captures resumable state at a semantic boundary. An envelope
|
|
281
|
+
* is the portable description of a continuation: which checkpoint, which
|
|
282
|
+
* agent, which target, and under which conditions work should continue.
|
|
283
|
+
* Envelopes are content-addressed; the signed run contract pins the
|
|
284
|
+
* envelope hash, so the continuation provenance is covered by the plane
|
|
285
|
+
* signature without requiring requester-held keys.
|
|
286
|
+
*/
|
|
287
|
+
export type SemanticState = {
|
|
288
|
+
/** Blob hash of the captured transcript, if one was attached. */
|
|
289
|
+
transcriptHash?: string;
|
|
290
|
+
/** Blob hash of the tool-call journal (warrant.tooljournal.v1), if any. */
|
|
291
|
+
toolJournalHash?: string;
|
|
292
|
+
/** Short human-readable summary of where the work stands. */
|
|
293
|
+
note?: string;
|
|
294
|
+
};
|
|
295
|
+
/** One recorded tool invocation from an app-owned loop. */
|
|
296
|
+
export type ToolCallRecord = {
|
|
297
|
+
seq: number;
|
|
298
|
+
ts: string;
|
|
299
|
+
toolName: string;
|
|
300
|
+
input: JsonValue;
|
|
301
|
+
output?: JsonValue;
|
|
302
|
+
error?: string;
|
|
303
|
+
durationMs: number;
|
|
304
|
+
};
|
|
305
|
+
/**
|
|
306
|
+
* Tool-call history captured at semantic boundaries (warrant.tooljournal.v1).
|
|
307
|
+
* Content-addressed and referenced from a checkpoint's semantic state, so a
|
|
308
|
+
* continuation carries what the loop's tools saw and did.
|
|
309
|
+
*/
|
|
310
|
+
export type ToolJournal = {
|
|
311
|
+
version: "warrant.tooljournal.v1";
|
|
312
|
+
entries: ToolCallRecord[];
|
|
313
|
+
};
|
|
314
|
+
export type Checkpoint = {
|
|
315
|
+
version: "warrant.checkpoint.v1";
|
|
316
|
+
checkpointId: string;
|
|
317
|
+
createdAt: string;
|
|
318
|
+
tier: CheckpointTier;
|
|
319
|
+
message?: string;
|
|
320
|
+
semantic?: SemanticState;
|
|
321
|
+
workspace?: WorkspaceManifest;
|
|
322
|
+
/** Lineage: the checkpoint this one descends from, if any. */
|
|
323
|
+
parent?: string;
|
|
324
|
+
};
|
|
325
|
+
export type HandoffSource = {
|
|
326
|
+
kind: "local";
|
|
327
|
+
actor: ActorRef;
|
|
328
|
+
host?: string;
|
|
329
|
+
};
|
|
330
|
+
export type HandoffTargetRef = {
|
|
331
|
+
kind: "runner-pool";
|
|
332
|
+
pool: string;
|
|
333
|
+
};
|
|
334
|
+
export type HandoffEnvelope = {
|
|
335
|
+
version: "warrant.envelope.v1";
|
|
336
|
+
envelopeId: string;
|
|
337
|
+
createdAt: string;
|
|
338
|
+
source: HandoffSource;
|
|
339
|
+
target: HandoffTargetRef;
|
|
340
|
+
checkpoint: Checkpoint;
|
|
341
|
+
agent: AgentSpec;
|
|
342
|
+
task: TaskSpec;
|
|
343
|
+
reason?: string;
|
|
344
|
+
secrets: SecretClaim[];
|
|
345
|
+
network: NetworkPolicy;
|
|
346
|
+
budget: BudgetSpec;
|
|
347
|
+
disclosure: DisclosureMode;
|
|
348
|
+
/** Durable machine intent for this continuation. */
|
|
349
|
+
execution?: ExecutionSpec;
|
|
350
|
+
/** Requested session isolation for the continuation. */
|
|
351
|
+
isolation?: SessionIsolation;
|
|
352
|
+
};
|
|
353
|
+
/** Reference embedded in a run contract when the run continues prior work. */
|
|
354
|
+
export type ContinuationRef = {
|
|
355
|
+
envelopeHash: string;
|
|
356
|
+
checkpointId: string;
|
|
357
|
+
tier: CheckpointTier;
|
|
358
|
+
};
|
|
359
|
+
/** Self-contained bundle sufficient for fully offline verification. */
|
|
360
|
+
export type ReceiptBundle = {
|
|
361
|
+
version: "warrant.bundle.v1";
|
|
362
|
+
contract: RunContract;
|
|
363
|
+
receipt: Receipt;
|
|
364
|
+
events: ChainedEvent[];
|
|
365
|
+
keys: {
|
|
366
|
+
planePublicKeyPem: string;
|
|
367
|
+
runnerPublicKeyPem: string;
|
|
368
|
+
orgPublicKeyPem?: string;
|
|
369
|
+
};
|
|
370
|
+
};
|
|
371
|
+
export declare class PolicyDeniedError extends Error {
|
|
372
|
+
readonly code: FailureClass;
|
|
373
|
+
readonly reasons: string[];
|
|
374
|
+
constructor(reasons: string[]);
|
|
375
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Warrant protocol types. These are the schemas marked for open
|
|
3
|
+
* publication in the spec (warrant.contract.v1, warrant.receipt.v1,
|
|
4
|
+
* warrant.event.v1, warrant.manifest.v1, warrant.policy.v1).
|
|
5
|
+
*/
|
|
6
|
+
export class PolicyDeniedError extends Error {
|
|
7
|
+
code = "policy_denied";
|
|
8
|
+
reasons;
|
|
9
|
+
constructor(reasons) {
|
|
10
|
+
super(`policy denied: ${reasons.join("; ")}`);
|
|
11
|
+
this.name = "PolicyDeniedError";
|
|
12
|
+
this.reasons = reasons;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const SECRET_NAME_PATTERN: RegExp;
|
|
2
|
+
export declare const POOL_NAME_PATTERN: RegExp;
|
|
3
|
+
export declare const WORKSPACE_RELATIVE_PATH_PATTERN: RegExp;
|
|
4
|
+
export declare function parseSecretName(value: string): string;
|
|
5
|
+
export declare function parsePoolName(value: string): string;
|
|
6
|
+
export declare function parseHostAllowlistEntry(value: string): string;
|
|
7
|
+
export declare function parseWorkspaceManifestPath(value: string): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const SECRET_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2
|
+
export const POOL_NAME_PATTERN = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
3
|
+
export const WORKSPACE_RELATIVE_PATH_PATTERN = /^(?!\/)(?![A-Za-z]:)(?!.*(?:^|\/)\.\.(?:\/|$))(?!.*\/\/)(?!.*\\)(?!.*\0).+$/;
|
|
4
|
+
export function parseSecretName(value) {
|
|
5
|
+
if (!SECRET_NAME_PATTERN.test(value)) {
|
|
6
|
+
throw new Error(`secret name "${value}" must be a POSIX environment identifier`);
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
export function parsePoolName(value) {
|
|
11
|
+
if (!POOL_NAME_PATTERN.test(value)) {
|
|
12
|
+
throw new Error(`pool name "${value}" contains unsupported characters`);
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
export function parseHostAllowlistEntry(value) {
|
|
17
|
+
const trimmed = value.trim().toLowerCase().replace(/\.$/, "");
|
|
18
|
+
if (trimmed.length === 0 ||
|
|
19
|
+
trimmed.length > 253 ||
|
|
20
|
+
trimmed.includes("/") ||
|
|
21
|
+
trimmed.includes("@") ||
|
|
22
|
+
trimmed.includes(":")) {
|
|
23
|
+
throw new Error(`host allowlist entry "${value}" is not a bare host name`);
|
|
24
|
+
}
|
|
25
|
+
return trimmed;
|
|
26
|
+
}
|
|
27
|
+
export function parseWorkspaceManifestPath(value) {
|
|
28
|
+
if (!WORKSPACE_RELATIVE_PATH_PATTERN.test(value)) {
|
|
29
|
+
throw new Error(`workspace path "${value}" must be a safe relative path`);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AgentKind, RunEvent, RunStatus } from "./types.js";
|
|
2
|
+
export declare const RUN_STATUSES: readonly ["created", "claimed", "provisioning", "running", "awaiting_approval", "completed", "failed", "cancelled"];
|
|
3
|
+
export declare const TERMINAL_RUN_STATUSES: readonly ["completed", "failed", "cancelled"];
|
|
4
|
+
export declare const AGENT_KINDS: readonly ["claude-code", "codex", "pi", "mock", "command"];
|
|
5
|
+
export declare const SESSION_ISOLATIONS: readonly ["process", "hermetic", "vercel-sandbox"];
|
|
6
|
+
export declare const DISCLOSURE_MODES: readonly ["none", "metadata-only", "redacted", "minimal-context", "full"];
|
|
7
|
+
export declare const CHECKPOINT_TIERS: readonly ["semantic", "workspace"];
|
|
8
|
+
export declare const ACTOR_KINDS: readonly ["human", "service"];
|
|
9
|
+
export declare const RUN_EVENT_TYPES: readonly RunEvent["type"][];
|
|
10
|
+
export declare const HEX_HASH_PATTERN: RegExp;
|
|
11
|
+
export declare function isTerminalStatus(status: RunStatus): boolean;
|
|
12
|
+
export declare function isAgentKind(value: string): value is AgentKind;
|