@martinloop/mcp 0.3.9 → 0.5.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/README.md +82 -141
- package/dist/discovery-metadata.d.ts +1 -1
- package/dist/discovery-metadata.js +1 -1
- package/dist/resources.d.ts +1 -2
- package/dist/resources.js +47 -95
- package/dist/server-validation.d.ts +1 -1
- package/dist/server-validation.js +16 -11
- package/dist/server.d.ts +23 -0
- package/dist/server.js +152 -7
- package/dist/tools/preflight.d.ts +2 -0
- package/dist/tools/preflight.js +42 -2
- package/dist/tools/run-dossier.d.ts +3 -1
- package/dist/tools/run-dossier.js +40 -2
- package/dist/tools/run-loop.d.ts +1 -4
- package/dist/tools/run-loop.js +6 -15
- package/dist/tools/tool-support.d.ts +2 -1
- package/dist/tools/tool-support.js +1 -12
- package/dist/vendor/adapters/claude-cli.js +38 -35
- package/dist/vendor/adapters/cli-bridge.d.ts +9 -1
- package/dist/vendor/adapters/cli-bridge.js +54 -13
- package/dist/vendor/adapters/codex-launcher.d.ts +61 -1
- package/dist/vendor/adapters/codex-launcher.js +118 -19
- package/dist/vendor/adapters/index.d.ts +1 -3
- package/dist/vendor/adapters/index.js +1 -3
- package/dist/vendor/adapters/openai-compatible.js +51 -12
- package/dist/vendor/contracts/adapter-capabilities.d.ts +1 -1
- package/dist/vendor/contracts/context-handoff.d.ts +100 -0
- package/dist/vendor/contracts/context-handoff.js +11 -0
- package/dist/vendor/contracts/context-manifest.d.ts +195 -0
- package/dist/vendor/contracts/context-manifest.js +17 -0
- package/dist/vendor/contracts/context-shadow.d.ts +75 -0
- package/dist/vendor/contracts/context-shadow.js +8 -0
- package/dist/vendor/contracts/delivery.d.ts +47 -0
- package/dist/vendor/contracts/delivery.js +18 -0
- package/dist/vendor/contracts/exits.d.ts +126 -0
- package/dist/vendor/contracts/exits.js +32 -0
- package/dist/vendor/contracts/index.d.ts +41 -2
- package/dist/vendor/contracts/index.js +24 -0
- package/dist/vendor/contracts/mission.d.ts +102 -0
- package/dist/vendor/contracts/mission.js +64 -0
- package/dist/vendor/contracts/operator.d.ts +3 -1
- package/dist/vendor/contracts/operator.js +2 -1
- package/dist/vendor/contracts/verified-handoff.d.ts +100 -0
- package/dist/vendor/contracts/verified-handoff.js +35 -0
- package/dist/vendor/core/context-chain-gate.d.ts +89 -0
- package/dist/vendor/core/context-chain-gate.js +173 -0
- package/dist/vendor/core/context-compiler.d.ts +53 -0
- package/dist/vendor/core/context-compiler.js +261 -0
- package/dist/vendor/core/context-handoff.d.ts +42 -0
- package/dist/vendor/core/context-handoff.js +167 -0
- package/dist/vendor/core/context-shadow.d.ts +39 -0
- package/dist/vendor/core/context-shadow.js +200 -0
- package/dist/vendor/core/delivery/index.d.ts +6 -0
- package/dist/vendor/core/delivery/index.js +4 -0
- package/dist/vendor/core/delivery/message-client.d.ts +18 -0
- package/dist/vendor/core/delivery/message-client.js +38 -0
- package/dist/vendor/core/delivery/message-ledger.d.ts +35 -0
- package/dist/vendor/core/delivery/message-ledger.js +97 -0
- package/dist/vendor/core/delivery/message-schema.d.ts +16 -0
- package/dist/vendor/core/delivery/message-schema.js +111 -0
- package/dist/vendor/core/delivery/update-check.d.ts +19 -0
- package/dist/vendor/core/delivery/update-check.js +107 -0
- package/dist/vendor/core/exit-signal.d.ts +88 -0
- package/dist/vendor/core/exit-signal.js +339 -0
- package/dist/vendor/core/exits.d.ts +41 -0
- package/dist/vendor/core/exits.js +258 -0
- package/dist/vendor/core/index.d.ts +44 -1
- package/dist/vendor/core/index.js +1088 -662
- package/dist/vendor/core/mission/aggregate.d.ts +27 -0
- package/dist/vendor/core/mission/aggregate.js +42 -0
- package/dist/vendor/core/mission/index.d.ts +2 -0
- package/dist/vendor/core/mission/index.js +1 -0
- package/dist/vendor/core/persistence/compiler.d.ts +8 -0
- package/dist/vendor/core/persistence/compiler.js +156 -0
- package/dist/vendor/core/persistence/context-handoff-store.d.ts +68 -0
- package/dist/vendor/core/persistence/context-handoff-store.js +112 -0
- package/dist/vendor/core/persistence/index.d.ts +4 -0
- package/dist/vendor/core/persistence/index.js +2 -0
- package/dist/vendor/core/persistence/ledger.d.ts +1 -1
- package/dist/vendor/core/persistence/mission-store.d.ts +56 -0
- package/dist/vendor/core/persistence/mission-store.js +281 -0
- package/dist/vendor/core/policy.d.ts +2 -0
- package/dist/vendor/core/policy.js +5 -2
- package/dist/vendor/core/rollback.js +15 -1
- package/dist/vendor/core/savings.d.ts +13 -0
- package/dist/vendor/core/savings.js +22 -0
- package/dist/vendor/core/termination-store.d.ts +15 -0
- package/dist/vendor/core/termination-store.js +44 -0
- package/dist/vendor/core/verified-handoff.d.ts +67 -0
- package/dist/vendor/core/verified-handoff.js +210 -0
- package/package.json +3 -13
- package/server.json +3 -3
- package/dist/vendor/adapters/stub-agent-cli.d.ts +0 -8
- package/dist/vendor/adapters/stub-agent-cli.js +0 -40
- package/dist/vendor/adapters/verifier-only.d.ts +0 -9
- package/dist/vendor/adapters/verifier-only.js +0 -63
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mission aggregation — C2
|
|
3
|
+
*
|
|
4
|
+
* Computes verified-outcome and cost metrics from a mission's linked runs.
|
|
5
|
+
* Does not hit the filesystem — operates on already-loaded MissionRecord.
|
|
6
|
+
*/
|
|
7
|
+
import type { MissionCost, MissionRecord, MissionRunLink } from "../../contracts/index.js";
|
|
8
|
+
export interface MissionMetrics {
|
|
9
|
+
totalActualUsd: number;
|
|
10
|
+
verifiedOutcomeCount: number;
|
|
11
|
+
totalRunCount: number;
|
|
12
|
+
/** USD per verified outcome. Infinity when verifiedOutcomeCount === 0. */
|
|
13
|
+
costPerVerifiedOutcome: number;
|
|
14
|
+
/** Fraction of runs with verified outcomes (0–1). */
|
|
15
|
+
verifiedRate: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Re-derive mission cost metrics from the run links.
|
|
19
|
+
* Use this to rebuild the cost object from ledger-authoritative data
|
|
20
|
+
* rather than trusting the cached mission.json cost field.
|
|
21
|
+
*/
|
|
22
|
+
export declare function aggregateMissionMetrics(runLinks: MissionRunLink[]): MissionMetrics;
|
|
23
|
+
/**
|
|
24
|
+
* Rebuild the MissionCost record from the authoritative run links.
|
|
25
|
+
* Call after loading from ledger to ensure the cached cost field is consistent.
|
|
26
|
+
*/
|
|
27
|
+
export declare function rebuildMissionCost(mission: MissionRecord): MissionCost;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mission aggregation — C2
|
|
3
|
+
*
|
|
4
|
+
* Computes verified-outcome and cost metrics from a mission's linked runs.
|
|
5
|
+
* Does not hit the filesystem — operates on already-loaded MissionRecord.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Re-derive mission cost metrics from the run links.
|
|
9
|
+
* Use this to rebuild the cost object from ledger-authoritative data
|
|
10
|
+
* rather than trusting the cached mission.json cost field.
|
|
11
|
+
*/
|
|
12
|
+
export function aggregateMissionMetrics(runLinks) {
|
|
13
|
+
let totalActualUsd = 0;
|
|
14
|
+
let verifiedOutcomeCount = 0;
|
|
15
|
+
for (const link of runLinks) {
|
|
16
|
+
totalActualUsd += link.actualUsd ?? 0;
|
|
17
|
+
if (link.verifiedOutcome === true)
|
|
18
|
+
verifiedOutcomeCount += 1;
|
|
19
|
+
}
|
|
20
|
+
const totalRunCount = runLinks.length;
|
|
21
|
+
const costPerVerifiedOutcome = verifiedOutcomeCount === 0 ? Infinity : totalActualUsd / verifiedOutcomeCount;
|
|
22
|
+
const verifiedRate = totalRunCount === 0 ? 0 : verifiedOutcomeCount / totalRunCount;
|
|
23
|
+
return {
|
|
24
|
+
totalActualUsd,
|
|
25
|
+
verifiedOutcomeCount,
|
|
26
|
+
totalRunCount,
|
|
27
|
+
costPerVerifiedOutcome,
|
|
28
|
+
verifiedRate
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Rebuild the MissionCost record from the authoritative run links.
|
|
33
|
+
* Call after loading from ledger to ensure the cached cost field is consistent.
|
|
34
|
+
*/
|
|
35
|
+
export function rebuildMissionCost(mission) {
|
|
36
|
+
const metrics = aggregateMissionMetrics(mission.runLinks);
|
|
37
|
+
return {
|
|
38
|
+
totalActualUsd: metrics.totalActualUsd,
|
|
39
|
+
verifiedOutcomeCount: metrics.verifiedOutcomeCount,
|
|
40
|
+
totalRunCount: metrics.totalRunCount
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { aggregateMissionMetrics, rebuildMissionCost } from "./aggregate.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type CompilerAdapterRequest, type PromptPacket } from "../compiler.js";
|
|
2
|
+
import { type ContextAdapter } from "../context-compiler.js";
|
|
2
3
|
import type { RunStore } from "./store.js";
|
|
3
4
|
export interface CompileResult {
|
|
4
5
|
packet: PromptPacket;
|
|
@@ -10,9 +11,16 @@ export interface CompileResult {
|
|
|
10
11
|
*
|
|
11
12
|
* R3.7: Context compiler produces deterministic compiled-context.json.
|
|
12
13
|
* R3.8: Any attempt prompt can be reconstructed from disk artifacts alone.
|
|
14
|
+
*
|
|
15
|
+
* A-CTX-0: After compiling, emit a shadow manifest via compileContextShadow
|
|
16
|
+
* and append a context.shadow_compiled ledger event. Shadow failure must not
|
|
17
|
+
* break the governed run — original packet is returned unchanged in all cases.
|
|
13
18
|
*/
|
|
14
19
|
export declare function compileAndPersistContext(request: CompilerAdapterRequest, options: {
|
|
15
20
|
attemptIndex: number;
|
|
16
21
|
store?: RunStore;
|
|
17
22
|
now?: string;
|
|
23
|
+
nowMs?: number;
|
|
24
|
+
contextShadowBudgetTokens?: number;
|
|
25
|
+
contextAdapter?: ContextAdapter;
|
|
18
26
|
}): Promise<CompileResult>;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { compilePromptPacket } from "../compiler.js";
|
|
3
|
+
import { compileContext, HEURISTIC_ADAPTER } from "../context-compiler.js";
|
|
4
|
+
import { compileContextShadow } from "../context-shadow.js";
|
|
2
5
|
import { makeLedgerEvent } from "./ledger.js";
|
|
3
6
|
/**
|
|
4
7
|
* Compile a deterministic PromptPacket from the request state and, when a
|
|
@@ -7,6 +10,10 @@ import { makeLedgerEvent } from "./ledger.js";
|
|
|
7
10
|
*
|
|
8
11
|
* R3.7: Context compiler produces deterministic compiled-context.json.
|
|
9
12
|
* R3.8: Any attempt prompt can be reconstructed from disk artifacts alone.
|
|
13
|
+
*
|
|
14
|
+
* A-CTX-0: After compiling, emit a shadow manifest via compileContextShadow
|
|
15
|
+
* and append a context.shadow_compiled ledger event. Shadow failure must not
|
|
16
|
+
* break the governed run — original packet is returned unchanged in all cases.
|
|
10
17
|
*/
|
|
11
18
|
export async function compileAndPersistContext(request, options) {
|
|
12
19
|
const packet = compilePromptPacket(request);
|
|
@@ -29,6 +36,155 @@ export async function compileAndPersistContext(request, options) {
|
|
|
29
36
|
},
|
|
30
37
|
timestamp: ts
|
|
31
38
|
}));
|
|
39
|
+
// ── A-CTX-0: Shadow manifest emission ────────────────────────────────
|
|
40
|
+
// Use the serialized PromptPacket as the single shadow segment.
|
|
41
|
+
// This is a A-CTX-0 fallback — A-CTX-1 will map structured segments.
|
|
42
|
+
// Shadow failure MUST NOT break the governed run.
|
|
43
|
+
try {
|
|
44
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
45
|
+
const shadow = compileContextShadow({
|
|
46
|
+
runId: request.loopId,
|
|
47
|
+
adapter: "martin-core",
|
|
48
|
+
nowMs,
|
|
49
|
+
shadowBudgetTokens: options.contextShadowBudgetTokens ?? 8_000,
|
|
50
|
+
modelWindowEvidence: "unknown",
|
|
51
|
+
segments: [
|
|
52
|
+
{
|
|
53
|
+
segmentId: "compiled-prompt",
|
|
54
|
+
kind: "compiled_prompt",
|
|
55
|
+
required: true,
|
|
56
|
+
text: JSON.stringify(packet)
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
});
|
|
60
|
+
await options.store.appendLedger(request.loopId, makeLedgerEvent({
|
|
61
|
+
kind: "context.shadow_compiled",
|
|
62
|
+
runId: request.loopId,
|
|
63
|
+
attemptIndex: options.attemptIndex,
|
|
64
|
+
payload: shadow.receipt,
|
|
65
|
+
timestamp: ts
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
catch (shadowErr) {
|
|
69
|
+
// Shadow telemetry failure is non-fatal. Emit a diagnostic ledger entry
|
|
70
|
+
// so the failure is visible without breaking the governed run.
|
|
71
|
+
const errorKind = shadowErr instanceof Error ? shadowErr.name : "UnknownError";
|
|
72
|
+
try {
|
|
73
|
+
await options.store.appendLedger(request.loopId, makeLedgerEvent({
|
|
74
|
+
kind: "context.shadow_compiled",
|
|
75
|
+
runId: request.loopId,
|
|
76
|
+
attemptIndex: options.attemptIndex,
|
|
77
|
+
payload: {
|
|
78
|
+
mode: "shadow",
|
|
79
|
+
error: "shadow_emit_failed",
|
|
80
|
+
errorKind
|
|
81
|
+
},
|
|
82
|
+
timestamp: ts
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Last-resort: if the diagnostic ledger write also fails, swallow it
|
|
87
|
+
// so the governed run completes. No prompt content is stored here.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// ── End A-CTX-0 shadow emission ───────────────────────────────────────
|
|
91
|
+
// ── A-CTX-1: Deterministic governed context compilation ───────────────
|
|
92
|
+
// Shadow mode: compute manifest but return original packet unchanged.
|
|
93
|
+
// nowMs captured once here — never passed as Date.now() inside compileContext.
|
|
94
|
+
// Failure must not break the governed run.
|
|
95
|
+
try {
|
|
96
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
97
|
+
const adapter = options.contextAdapter ?? HEURISTIC_ADAPTER;
|
|
98
|
+
// Build one candidate from the serialized packet.
|
|
99
|
+
// Only the hash goes into the manifest — never the raw text.
|
|
100
|
+
const packetText = JSON.stringify(packet);
|
|
101
|
+
const packetHash = createHash("sha256").update(packetText, "utf8").digest("hex");
|
|
102
|
+
const estimatedTokens = Math.max(1, Math.ceil(Buffer.byteLength(packetText, "utf8") / 4));
|
|
103
|
+
const result = compileContext({
|
|
104
|
+
taskId: request.attemptId,
|
|
105
|
+
runId: request.loopId,
|
|
106
|
+
nowMs,
|
|
107
|
+
candidates: [
|
|
108
|
+
{
|
|
109
|
+
id: "compiled-prompt",
|
|
110
|
+
kind: "task",
|
|
111
|
+
priority: "required",
|
|
112
|
+
trust: "authoritative",
|
|
113
|
+
sensitivity: "workspace",
|
|
114
|
+
sourceRef: `run://${request.loopId}/compiled-context`,
|
|
115
|
+
contentHash: packetHash,
|
|
116
|
+
estimatedTokens
|
|
117
|
+
}
|
|
118
|
+
],
|
|
119
|
+
budget: {
|
|
120
|
+
modelWindowTokens: 200_000,
|
|
121
|
+
systemReserveTokens: 2_000,
|
|
122
|
+
outputReserveTokens: 4_000,
|
|
123
|
+
toolReserveTokens: 3_000,
|
|
124
|
+
overflowReserveTokens: 2_000,
|
|
125
|
+
maxWorkingSetTokens: options.contextShadowBudgetTokens ?? 8_000,
|
|
126
|
+
pinnedTokensMax: 1_500
|
|
127
|
+
},
|
|
128
|
+
policy: {
|
|
129
|
+
policyHash: "shadow-passthrough-v1",
|
|
130
|
+
deniedSensitivities: ["secret"],
|
|
131
|
+
deniedTrustLevels: [],
|
|
132
|
+
requiredOverBudgetAction: "explicit_escalation",
|
|
133
|
+
maxCompilerDurationMs: 250,
|
|
134
|
+
maxOverheadRatio: 0.15
|
|
135
|
+
},
|
|
136
|
+
adapter
|
|
137
|
+
});
|
|
138
|
+
if (result.ok) {
|
|
139
|
+
await options.store.appendLedger(request.loopId, makeLedgerEvent({
|
|
140
|
+
kind: "context.manifest_compiled",
|
|
141
|
+
runId: request.loopId,
|
|
142
|
+
attemptIndex: options.attemptIndex,
|
|
143
|
+
payload: result.ledgerEntry,
|
|
144
|
+
timestamp: ts
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
// Explicit compiler failure (required_over_budget or recount_exceeded_passes).
|
|
149
|
+
// Record as a diagnostic — not a crash, not silent.
|
|
150
|
+
await options.store.appendLedger(request.loopId, makeLedgerEvent({
|
|
151
|
+
kind: "context.manifest_compiled",
|
|
152
|
+
runId: request.loopId,
|
|
153
|
+
attemptIndex: options.attemptIndex,
|
|
154
|
+
payload: {
|
|
155
|
+
mode: "shadow",
|
|
156
|
+
error: "compiler_failed",
|
|
157
|
+
reason: result.reason
|
|
158
|
+
},
|
|
159
|
+
timestamp: ts
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (compilerErr) {
|
|
164
|
+
// Unexpected compiler failure is non-fatal. Emit a named diagnostic so
|
|
165
|
+
// the failure is visible in the ledger without breaking the governed run.
|
|
166
|
+
// Only error.name (e.g. "TypeError") is recorded — never error.message,
|
|
167
|
+
// which could contain prompt or segment text.
|
|
168
|
+
const errorKind = compilerErr instanceof Error ? compilerErr.name : "UnknownError";
|
|
169
|
+
try {
|
|
170
|
+
await options.store.appendLedger(request.loopId, makeLedgerEvent({
|
|
171
|
+
kind: "context.manifest_compiled",
|
|
172
|
+
runId: request.loopId,
|
|
173
|
+
attemptIndex: options.attemptIndex,
|
|
174
|
+
payload: {
|
|
175
|
+
mode: "shadow",
|
|
176
|
+
error: "compiler_threw",
|
|
177
|
+
errorKind
|
|
178
|
+
},
|
|
179
|
+
timestamp: ts
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// Last-resort: diagnostic write failed too. The run must not be affected.
|
|
184
|
+
// No prompt content is stored anywhere in this path.
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// ── End A-CTX-1 compiler ──────────────────────────────────────────────
|
|
32
188
|
}
|
|
33
189
|
return { packet };
|
|
34
190
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Handoff Store — A-CTX-2 persistence layer.
|
|
3
|
+
*
|
|
4
|
+
* Produces, writes, and reads ContextHandoffReceipt files so that
|
|
5
|
+
* downstream processes can verify them without rerunning the upstream agent.
|
|
6
|
+
*
|
|
7
|
+
* Storage layout (under runsRoot/<runId>/):
|
|
8
|
+
* context-handoff.json — the receipt produced by this run for the next hop
|
|
9
|
+
*
|
|
10
|
+
* The producerReceiptHash is computed as SHA-256 of the receipt-integrity.json
|
|
11
|
+
* file written by writeLoopRecord. If that file is absent, upstreamIntegrity is
|
|
12
|
+
* set to "evidence_boundary" — the receipt is still written but will not pass
|
|
13
|
+
* verifyContextHandoff.
|
|
14
|
+
*/
|
|
15
|
+
import type { ContextHandoffArtifact, ContextHandoffClaim, ContextHandoffReceipt } from "../../contracts/index.js";
|
|
16
|
+
/** Compute the SHA-256 hex digest of a file's raw bytes. */
|
|
17
|
+
export declare function computeFileHash(filePath: string): Promise<string>;
|
|
18
|
+
export declare function contextHandoffPath(runsRoot: string, runId: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Persist a ContextHandoffReceipt for this run so the next hop can load it.
|
|
21
|
+
* Overwrites any existing file — callers must not call this more than once per run.
|
|
22
|
+
*/
|
|
23
|
+
export declare function writeContextHandoff(runsRoot: string, runId: string, receipt: ContextHandoffReceipt): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Load a ContextHandoffReceipt from a previous run.
|
|
26
|
+
* Returns null when the file does not exist (not an error — the consumer
|
|
27
|
+
* must treat absence as a hard gate failure).
|
|
28
|
+
*/
|
|
29
|
+
export declare function readContextHandoff(runsRoot: string, runId: string): Promise<ContextHandoffReceipt | null>;
|
|
30
|
+
export interface BuildHandoffReceiptInput {
|
|
31
|
+
runsRoot: string;
|
|
32
|
+
/** Run that is producing this handoff (the upstream/producer run). */
|
|
33
|
+
producerRunId: string;
|
|
34
|
+
/** Stable identifier for the chain. Shared across all hops. */
|
|
35
|
+
chainId: string;
|
|
36
|
+
/** Optional mission identifier shared across hops. */
|
|
37
|
+
missionId?: string;
|
|
38
|
+
/** Unique ID for this specific handoff crossing. Generated if absent. */
|
|
39
|
+
handoffId?: string;
|
|
40
|
+
/** Verified claims the producer is asserting. All must be state "verified". */
|
|
41
|
+
claims?: ContextHandoffClaim[];
|
|
42
|
+
/**
|
|
43
|
+
* File paths whose SHA-256 hashes should be captured as artifacts.
|
|
44
|
+
* Each file is hashed at call time — callers must call after writing artifacts.
|
|
45
|
+
*/
|
|
46
|
+
artifactFiles?: Array<{
|
|
47
|
+
filePath: string;
|
|
48
|
+
label?: string;
|
|
49
|
+
required: boolean;
|
|
50
|
+
}>;
|
|
51
|
+
/** Optional pre-computed artifacts (bypasses file hashing). */
|
|
52
|
+
artifacts?: ContextHandoffArtifact[];
|
|
53
|
+
/** Assumptions that are not yet resolved at handoff time. */
|
|
54
|
+
unresolvedAssumptions?: string[];
|
|
55
|
+
/** Parent handoff IDs for multi-hop lineage. */
|
|
56
|
+
parentHandoffIds?: string[];
|
|
57
|
+
/** Clock override (defaults to new Date().toISOString()). */
|
|
58
|
+
now?: () => string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Build a ContextHandoffReceipt from a completed run.
|
|
62
|
+
*
|
|
63
|
+
* - producerReceiptHash is SHA-256 of the receipt-integrity.json file
|
|
64
|
+
* written by writeLoopRecord. If that file is absent, upstreamIntegrity
|
|
65
|
+
* is "evidence_boundary".
|
|
66
|
+
* - Artifact hashes are computed from real files at call time.
|
|
67
|
+
*/
|
|
68
|
+
export declare function buildContextHandoffReceipt(input: BuildHandoffReceiptInput): Promise<ContextHandoffReceipt>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Handoff Store — A-CTX-2 persistence layer.
|
|
3
|
+
*
|
|
4
|
+
* Produces, writes, and reads ContextHandoffReceipt files so that
|
|
5
|
+
* downstream processes can verify them without rerunning the upstream agent.
|
|
6
|
+
*
|
|
7
|
+
* Storage layout (under runsRoot/<runId>/):
|
|
8
|
+
* context-handoff.json — the receipt produced by this run for the next hop
|
|
9
|
+
*
|
|
10
|
+
* The producerReceiptHash is computed as SHA-256 of the receipt-integrity.json
|
|
11
|
+
* file written by writeLoopRecord. If that file is absent, upstreamIntegrity is
|
|
12
|
+
* set to "evidence_boundary" — the receipt is still written but will not pass
|
|
13
|
+
* verifyContextHandoff.
|
|
14
|
+
*/
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
16
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { HANDOFF_SCHEMA_VERSION } from "../../contracts/index.js";
|
|
19
|
+
import { resolveReceiptIntegrityPath } from "./integrity.js";
|
|
20
|
+
import { runDir } from "./store.js";
|
|
21
|
+
// ─── Hash helpers ──────────────────────────────────────────────────────────────
|
|
22
|
+
/** Compute the SHA-256 hex digest of a file's raw bytes. */
|
|
23
|
+
export async function computeFileHash(filePath) {
|
|
24
|
+
const buf = await readFile(filePath);
|
|
25
|
+
return createHash("sha256").update(buf).digest("hex");
|
|
26
|
+
}
|
|
27
|
+
/** Compute the SHA-256 hex digest of a UTF-8 string. */
|
|
28
|
+
function sha256String(value) {
|
|
29
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
30
|
+
}
|
|
31
|
+
// ─── Paths ─────────────────────────────────────────────────────────────────────
|
|
32
|
+
export function contextHandoffPath(runsRoot, runId) {
|
|
33
|
+
return join(runDir(runsRoot, runId), "context-handoff.json");
|
|
34
|
+
}
|
|
35
|
+
// ─── Write ─────────────────────────────────────────────────────────────────────
|
|
36
|
+
/**
|
|
37
|
+
* Persist a ContextHandoffReceipt for this run so the next hop can load it.
|
|
38
|
+
* Overwrites any existing file — callers must not call this more than once per run.
|
|
39
|
+
*/
|
|
40
|
+
export async function writeContextHandoff(runsRoot, runId, receipt) {
|
|
41
|
+
const dir = runDir(runsRoot, runId);
|
|
42
|
+
await mkdir(dir, { recursive: true });
|
|
43
|
+
await writeFile(contextHandoffPath(runsRoot, runId), JSON.stringify(receipt, null, 2), "utf8");
|
|
44
|
+
}
|
|
45
|
+
// ─── Read ──────────────────────────────────────────────────────────────────────
|
|
46
|
+
/**
|
|
47
|
+
* Load a ContextHandoffReceipt from a previous run.
|
|
48
|
+
* Returns null when the file does not exist (not an error — the consumer
|
|
49
|
+
* must treat absence as a hard gate failure).
|
|
50
|
+
*/
|
|
51
|
+
export async function readContextHandoff(runsRoot, runId) {
|
|
52
|
+
const path = contextHandoffPath(runsRoot, runId);
|
|
53
|
+
const raw = await readFile(path, "utf8").catch(() => null);
|
|
54
|
+
if (raw === null)
|
|
55
|
+
return null;
|
|
56
|
+
return JSON.parse(raw);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Build a ContextHandoffReceipt from a completed run.
|
|
60
|
+
*
|
|
61
|
+
* - producerReceiptHash is SHA-256 of the receipt-integrity.json file
|
|
62
|
+
* written by writeLoopRecord. If that file is absent, upstreamIntegrity
|
|
63
|
+
* is "evidence_boundary".
|
|
64
|
+
* - Artifact hashes are computed from real files at call time.
|
|
65
|
+
*/
|
|
66
|
+
export async function buildContextHandoffReceipt(input) {
|
|
67
|
+
const { runsRoot, producerRunId, chainId, missionId, claims = [], unresolvedAssumptions = [], parentHandoffIds, now = () => new Date().toISOString() } = input;
|
|
68
|
+
// Determine handoffId
|
|
69
|
+
const handoffId = input.handoffId ??
|
|
70
|
+
`hoff_${sha256String(`${chainId}:${producerRunId}:${now()}`).slice(0, 16)}`;
|
|
71
|
+
// Hash the receipt-integrity.json file to obtain producerReceiptHash
|
|
72
|
+
const integrityPath = resolveReceiptIntegrityPath(runsRoot, producerRunId);
|
|
73
|
+
let producerReceiptHash;
|
|
74
|
+
let upstreamIntegrity;
|
|
75
|
+
const integrityRaw = await readFile(integrityPath, "utf8").catch(() => null);
|
|
76
|
+
if (integrityRaw === null) {
|
|
77
|
+
// No integrity file — cannot establish upstream integrity
|
|
78
|
+
producerReceiptHash = "";
|
|
79
|
+
upstreamIntegrity = "evidence_boundary";
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
producerReceiptHash = sha256String(integrityRaw);
|
|
83
|
+
upstreamIntegrity = "verified";
|
|
84
|
+
}
|
|
85
|
+
// Hash artifact files
|
|
86
|
+
const artifactFiles = input.artifactFiles ?? [];
|
|
87
|
+
const fileArtifacts = await Promise.all(artifactFiles.map(async (af) => ({
|
|
88
|
+
path: af.filePath,
|
|
89
|
+
sha256: await computeFileHash(af.filePath),
|
|
90
|
+
required: af.required,
|
|
91
|
+
label: af.label
|
|
92
|
+
})));
|
|
93
|
+
const artifacts = [
|
|
94
|
+
...(input.artifacts ?? []),
|
|
95
|
+
...fileArtifacts
|
|
96
|
+
];
|
|
97
|
+
const receipt = {
|
|
98
|
+
schemaVersion: HANDOFF_SCHEMA_VERSION,
|
|
99
|
+
handoffId,
|
|
100
|
+
chainId,
|
|
101
|
+
...(missionId !== undefined ? { missionId } : {}),
|
|
102
|
+
producerRunId,
|
|
103
|
+
producerReceiptHash,
|
|
104
|
+
...(parentHandoffIds !== undefined ? { parentHandoffIds } : {}),
|
|
105
|
+
claims,
|
|
106
|
+
artifacts,
|
|
107
|
+
unresolvedAssumptions,
|
|
108
|
+
upstreamIntegrity,
|
|
109
|
+
createdAt: now()
|
|
110
|
+
};
|
|
111
|
+
return receipt;
|
|
112
|
+
}
|
|
@@ -8,3 +8,7 @@ export { readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile,
|
|
|
8
8
|
export type { LoopAttemptRecord, LoopRunRecord } from "./runs-reader.js";
|
|
9
9
|
export { compileAndPersistContext } from "./compiler.js";
|
|
10
10
|
export type { CompileResult } from "./compiler.js";
|
|
11
|
+
export { buildContextHandoffReceipt, computeFileHash, contextHandoffPath, readContextHandoff, writeContextHandoff } from "./context-handoff-store.js";
|
|
12
|
+
export type { BuildHandoffReceiptInput } from "./context-handoff-store.js";
|
|
13
|
+
export { attachRun, changeMissionStatus, createMission, missionDir, readMission, readMissionLedger, verifyMissionLedger } from "./mission-store.js";
|
|
14
|
+
export type { AttachRunOptions, ChangeMissionStatusOptions, LedgerIntegrityResult } from "./mission-store.js";
|
|
@@ -3,3 +3,5 @@ export { resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeRece
|
|
|
3
3
|
export { artifactDir, createFileRunStore, resolveRunsRoot, runDir } from "./store.js";
|
|
4
4
|
export { readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile } from "./runs-reader.js";
|
|
5
5
|
export { compileAndPersistContext } from "./compiler.js";
|
|
6
|
+
export { buildContextHandoffReceipt, computeFileHash, contextHandoffPath, readContextHandoff, writeContextHandoff } from "./context-handoff-store.js";
|
|
7
|
+
export { attachRun, changeMissionStatus, createMission, missionDir, readMission, readMissionLedger, verifyMissionLedger } from "./mission-store.js";
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* These are WRITE-side events that go into ledger.jsonl — separate from the
|
|
4
4
|
* read-model LoopEventType used by the control plane.
|
|
5
5
|
*/
|
|
6
|
-
export type LedgerEventKind = "contract.created" | "attempt.admitted" | "attempt.rejected" | "prompt.compiled" | "patch.generated" | "verification.completed" | "grounding.violations_found" | "safety.violations_found" | "budget.settled" | "attempt.kept" | "attempt.discarded" | "run.exited";
|
|
6
|
+
export type LedgerEventKind = "contract.created" | "attempt.admitted" | "attempt.rejected" | "prompt.compiled" | "patch.generated" | "verification.completed" | "grounding.violations_found" | "safety.violations_found" | "budget.settled" | "attempt.kept" | "attempt.discarded" | "run.exited" | "run.terminated" | "run.diagnostic" | "context.shadow_compiled" | "context.manifest_compiled" | "context.handoff.received" | "context.handoff.verified" | "context.handoff.blocked" | "context.object.excluded";
|
|
7
7
|
export interface LedgerEvent {
|
|
8
8
|
kind: LedgerEventKind;
|
|
9
9
|
runId: string;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mission Store — C2 durable persistence.
|
|
3
|
+
*
|
|
4
|
+
* Layout under <runsRoot>/missions/<missionId>/:
|
|
5
|
+
* mission.json — rebuildable snapshot/cache (NOT the authority)
|
|
6
|
+
* ledger.jsonl — append-only, SHA-256 hash-chained event log (authority)
|
|
7
|
+
* ledger-chain.json — chain head hash for tamper detection
|
|
8
|
+
* .lock — cross-process exclusive lock (created with O_EXCL)
|
|
9
|
+
*
|
|
10
|
+
* Rules:
|
|
11
|
+
* - Ledger is always written before mission.json is updated.
|
|
12
|
+
* - mission.json is written atomically (temp file → rename).
|
|
13
|
+
* - Cross-process lock is held during every write; stale locks (>8s) are removed.
|
|
14
|
+
* - CAS revision must match before any write is accepted.
|
|
15
|
+
* - Corrupt or unverifiable ledger data fails closed.
|
|
16
|
+
* - Workspace isolation: all paths are under caller-supplied runsRoot.
|
|
17
|
+
*/
|
|
18
|
+
import type { MissionDecision, MissionEvent, MissionRecord, MissionRunRole, MissionStatus } from "../../contracts/index.js";
|
|
19
|
+
export declare function missionDir(runsRoot: string, missionId: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Read the current mission snapshot.
|
|
22
|
+
* Returns null when no mission exists at this path.
|
|
23
|
+
* Fails closed when the snapshot schema version is not recognised.
|
|
24
|
+
*/
|
|
25
|
+
export declare function readMission(runsRoot: string, missionId: string): Promise<MissionRecord | null>;
|
|
26
|
+
export declare function readMissionLedger(runsRoot: string, missionId: string): Promise<MissionEvent[]>;
|
|
27
|
+
export interface LedgerIntegrityResult {
|
|
28
|
+
ok: boolean;
|
|
29
|
+
reason?: string;
|
|
30
|
+
entryCount: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Verify the stored chain head matches a full replay of the ledger.
|
|
34
|
+
* Returns ok=false when the ledger has been tampered with or entries are missing.
|
|
35
|
+
*/
|
|
36
|
+
export declare function verifyMissionLedger(runsRoot: string, missionId: string): Promise<LedgerIntegrityResult>;
|
|
37
|
+
export declare function createMission(runsRoot: string, mission: MissionRecord): Promise<void>;
|
|
38
|
+
export interface AttachRunOptions {
|
|
39
|
+
loopId: string;
|
|
40
|
+
role: MissionRunRole;
|
|
41
|
+
verifiedOutcome?: boolean;
|
|
42
|
+
actualUsd?: number;
|
|
43
|
+
now?: () => string;
|
|
44
|
+
/** Expected revision for CAS enforcement. */
|
|
45
|
+
expectedRevision: number;
|
|
46
|
+
}
|
|
47
|
+
export declare function attachRun(runsRoot: string, missionId: string, options: AttachRunOptions): Promise<MissionRecord>;
|
|
48
|
+
export interface ChangeMissionStatusOptions {
|
|
49
|
+
toStatus: MissionStatus;
|
|
50
|
+
expectedRevision: number;
|
|
51
|
+
decidedBy?: string;
|
|
52
|
+
decision?: MissionDecision;
|
|
53
|
+
note?: string;
|
|
54
|
+
now?: () => string;
|
|
55
|
+
}
|
|
56
|
+
export declare function changeMissionStatus(runsRoot: string, missionId: string, options: ChangeMissionStatusOptions): Promise<MissionRecord>;
|