@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.
Files changed (96) hide show
  1. package/README.md +82 -141
  2. package/dist/discovery-metadata.d.ts +1 -1
  3. package/dist/discovery-metadata.js +1 -1
  4. package/dist/resources.d.ts +1 -2
  5. package/dist/resources.js +47 -95
  6. package/dist/server-validation.d.ts +1 -1
  7. package/dist/server-validation.js +16 -11
  8. package/dist/server.d.ts +23 -0
  9. package/dist/server.js +152 -7
  10. package/dist/tools/preflight.d.ts +2 -0
  11. package/dist/tools/preflight.js +42 -2
  12. package/dist/tools/run-dossier.d.ts +3 -1
  13. package/dist/tools/run-dossier.js +40 -2
  14. package/dist/tools/run-loop.d.ts +1 -4
  15. package/dist/tools/run-loop.js +6 -15
  16. package/dist/tools/tool-support.d.ts +2 -1
  17. package/dist/tools/tool-support.js +1 -12
  18. package/dist/vendor/adapters/claude-cli.js +38 -35
  19. package/dist/vendor/adapters/cli-bridge.d.ts +9 -1
  20. package/dist/vendor/adapters/cli-bridge.js +54 -13
  21. package/dist/vendor/adapters/codex-launcher.d.ts +61 -1
  22. package/dist/vendor/adapters/codex-launcher.js +118 -19
  23. package/dist/vendor/adapters/index.d.ts +1 -3
  24. package/dist/vendor/adapters/index.js +1 -3
  25. package/dist/vendor/adapters/openai-compatible.js +51 -12
  26. package/dist/vendor/contracts/adapter-capabilities.d.ts +1 -1
  27. package/dist/vendor/contracts/context-handoff.d.ts +100 -0
  28. package/dist/vendor/contracts/context-handoff.js +11 -0
  29. package/dist/vendor/contracts/context-manifest.d.ts +195 -0
  30. package/dist/vendor/contracts/context-manifest.js +17 -0
  31. package/dist/vendor/contracts/context-shadow.d.ts +75 -0
  32. package/dist/vendor/contracts/context-shadow.js +8 -0
  33. package/dist/vendor/contracts/delivery.d.ts +47 -0
  34. package/dist/vendor/contracts/delivery.js +18 -0
  35. package/dist/vendor/contracts/exits.d.ts +126 -0
  36. package/dist/vendor/contracts/exits.js +32 -0
  37. package/dist/vendor/contracts/index.d.ts +41 -2
  38. package/dist/vendor/contracts/index.js +24 -0
  39. package/dist/vendor/contracts/mission.d.ts +102 -0
  40. package/dist/vendor/contracts/mission.js +64 -0
  41. package/dist/vendor/contracts/operator.d.ts +3 -1
  42. package/dist/vendor/contracts/operator.js +2 -1
  43. package/dist/vendor/contracts/verified-handoff.d.ts +100 -0
  44. package/dist/vendor/contracts/verified-handoff.js +35 -0
  45. package/dist/vendor/core/context-chain-gate.d.ts +89 -0
  46. package/dist/vendor/core/context-chain-gate.js +173 -0
  47. package/dist/vendor/core/context-compiler.d.ts +53 -0
  48. package/dist/vendor/core/context-compiler.js +261 -0
  49. package/dist/vendor/core/context-handoff.d.ts +42 -0
  50. package/dist/vendor/core/context-handoff.js +167 -0
  51. package/dist/vendor/core/context-shadow.d.ts +39 -0
  52. package/dist/vendor/core/context-shadow.js +200 -0
  53. package/dist/vendor/core/delivery/index.d.ts +6 -0
  54. package/dist/vendor/core/delivery/index.js +4 -0
  55. package/dist/vendor/core/delivery/message-client.d.ts +18 -0
  56. package/dist/vendor/core/delivery/message-client.js +38 -0
  57. package/dist/vendor/core/delivery/message-ledger.d.ts +35 -0
  58. package/dist/vendor/core/delivery/message-ledger.js +97 -0
  59. package/dist/vendor/core/delivery/message-schema.d.ts +16 -0
  60. package/dist/vendor/core/delivery/message-schema.js +111 -0
  61. package/dist/vendor/core/delivery/update-check.d.ts +19 -0
  62. package/dist/vendor/core/delivery/update-check.js +107 -0
  63. package/dist/vendor/core/exit-signal.d.ts +88 -0
  64. package/dist/vendor/core/exit-signal.js +339 -0
  65. package/dist/vendor/core/exits.d.ts +41 -0
  66. package/dist/vendor/core/exits.js +258 -0
  67. package/dist/vendor/core/index.d.ts +44 -1
  68. package/dist/vendor/core/index.js +1088 -662
  69. package/dist/vendor/core/mission/aggregate.d.ts +27 -0
  70. package/dist/vendor/core/mission/aggregate.js +42 -0
  71. package/dist/vendor/core/mission/index.d.ts +2 -0
  72. package/dist/vendor/core/mission/index.js +1 -0
  73. package/dist/vendor/core/persistence/compiler.d.ts +8 -0
  74. package/dist/vendor/core/persistence/compiler.js +156 -0
  75. package/dist/vendor/core/persistence/context-handoff-store.d.ts +68 -0
  76. package/dist/vendor/core/persistence/context-handoff-store.js +112 -0
  77. package/dist/vendor/core/persistence/index.d.ts +4 -0
  78. package/dist/vendor/core/persistence/index.js +2 -0
  79. package/dist/vendor/core/persistence/ledger.d.ts +1 -1
  80. package/dist/vendor/core/persistence/mission-store.d.ts +56 -0
  81. package/dist/vendor/core/persistence/mission-store.js +281 -0
  82. package/dist/vendor/core/policy.d.ts +2 -0
  83. package/dist/vendor/core/policy.js +5 -2
  84. package/dist/vendor/core/rollback.js +15 -1
  85. package/dist/vendor/core/savings.d.ts +13 -0
  86. package/dist/vendor/core/savings.js +22 -0
  87. package/dist/vendor/core/termination-store.d.ts +15 -0
  88. package/dist/vendor/core/termination-store.js +44 -0
  89. package/dist/vendor/core/verified-handoff.d.ts +67 -0
  90. package/dist/vendor/core/verified-handoff.js +210 -0
  91. package/package.json +3 -13
  92. package/server.json +3 -3
  93. package/dist/vendor/adapters/stub-agent-cli.d.ts +0 -8
  94. package/dist/vendor/adapters/stub-agent-cli.js +0 -40
  95. package/dist/vendor/adapters/verifier-only.d.ts +0 -9
  96. package/dist/vendor/adapters/verifier-only.js +0 -63
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Context Chain Gate — C1
3
+ *
4
+ * Converts chain verification evidence into a merge-gate decision and
5
+ * a PR comment (Infracost-style). Two things hard-fail. Everything else
6
+ * is configurable or comment-only.
7
+ *
8
+ * Hard failures (always block merge):
9
+ * 1. no_governance_receipt — no receipt at all
10
+ * 2. tamper_detected — receipt hash does not match producer run
11
+ *
12
+ * Configurable gate (team decides; default: warn only):
13
+ * 3. verifier_failed — unverified claims, incomplete integrity,
14
+ * missing artifacts, unresolved assumptions
15
+ *
16
+ * Comment-only (never block, always visible):
17
+ * - Cost: actual vs budget — money is already spent, blocking adds friction
18
+ * - Chain lineage: chainId, producerRunId, consumerRunId
19
+ * - Integrity state
20
+ *
21
+ * Standing rules:
22
+ * - Pure functions, no side effects.
23
+ * - evaluateChainGate never reads environment variables or config files.
24
+ * - renderGatePrComment produces valid GitHub-flavored markdown.
25
+ * - No fake data, no placeholder behaviour.
26
+ */
27
+ import type { ContextHandoffReceipt } from "../contracts/index.js";
28
+ import type { ChainIntegrityState, ContextHandoffVerification } from "../contracts/index.js";
29
+ export interface ChainGateConfig {
30
+ /**
31
+ * When true, a failed verifier (unverified claims, incomplete integrity,
32
+ * missing artifacts, unresolved assumptions) blocks the merge.
33
+ * Default: false — warn in PR comment, do not block.
34
+ */
35
+ blockOnVerifierFailure: boolean;
36
+ }
37
+ export interface ChainGateCost {
38
+ actualUsd: number;
39
+ budgetUsd: number;
40
+ }
41
+ export interface ChainGateInput {
42
+ /** null means no receipt was provided — triggers the hard no_governance_receipt failure. */
43
+ receipt: ContextHandoffReceipt | null;
44
+ /** null when receipt is null. Must be provided when receipt is non-null. */
45
+ verification: ContextHandoffVerification | null;
46
+ cost?: ChainGateCost;
47
+ config?: Partial<ChainGateConfig>;
48
+ }
49
+ export type ChainGateConclusion = "failure" | "neutral" | "success";
50
+ export interface ChainGateResult {
51
+ noGovernance: boolean;
52
+ tamperDetected: boolean;
53
+ verifierFailed: boolean;
54
+ shouldBlock: boolean;
55
+ conclusion: ChainGateConclusion;
56
+ failureReasonCode?: string;
57
+ failureMessage?: string;
58
+ cost?: ChainGateCost & {
59
+ exceeded: boolean;
60
+ };
61
+ integrity: ChainIntegrityState | "absent";
62
+ }
63
+ /**
64
+ * Evaluates whether the chain gate should block merge, and assembles
65
+ * the data needed for the PR comment.
66
+ *
67
+ * Call order:
68
+ * 1. Check for missing receipt (hard fail).
69
+ * 2. Check for tamper_detected integrity (hard fail).
70
+ * 3. Check verifier result against config (configurable gate).
71
+ * 4. Return success / neutral with full comment data.
72
+ */
73
+ export declare function evaluateChainGate(input: ChainGateInput): ChainGateResult;
74
+ export interface GatePrCommentOptions {
75
+ runId?: string;
76
+ chainId?: string;
77
+ handoffId?: string;
78
+ producerRunId?: string;
79
+ prNumber?: number;
80
+ headSha?: string;
81
+ }
82
+ /**
83
+ * Renders an Infracost-style GitHub PR comment summarising the gate result.
84
+ *
85
+ * Hard failures and cost overruns are shown prominently.
86
+ * Cost is always present when provided — never omitted, never a blocking signal.
87
+ * Lineage fields (chainId, producerRunId, runId) provide audit trail links.
88
+ */
89
+ export declare function renderGatePrComment(result: ChainGateResult, options?: GatePrCommentOptions): string;
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Context Chain Gate — C1
3
+ *
4
+ * Converts chain verification evidence into a merge-gate decision and
5
+ * a PR comment (Infracost-style). Two things hard-fail. Everything else
6
+ * is configurable or comment-only.
7
+ *
8
+ * Hard failures (always block merge):
9
+ * 1. no_governance_receipt — no receipt at all
10
+ * 2. tamper_detected — receipt hash does not match producer run
11
+ *
12
+ * Configurable gate (team decides; default: warn only):
13
+ * 3. verifier_failed — unverified claims, incomplete integrity,
14
+ * missing artifacts, unresolved assumptions
15
+ *
16
+ * Comment-only (never block, always visible):
17
+ * - Cost: actual vs budget — money is already spent, blocking adds friction
18
+ * - Chain lineage: chainId, producerRunId, consumerRunId
19
+ * - Integrity state
20
+ *
21
+ * Standing rules:
22
+ * - Pure functions, no side effects.
23
+ * - evaluateChainGate never reads environment variables or config files.
24
+ * - renderGatePrComment produces valid GitHub-flavored markdown.
25
+ * - No fake data, no placeholder behaviour.
26
+ */
27
+ const DEFAULT_CONFIG = {
28
+ blockOnVerifierFailure: false
29
+ };
30
+ // ─── Gate evaluation ─────────────────────────────────────────────────────────
31
+ /**
32
+ * Evaluates whether the chain gate should block merge, and assembles
33
+ * the data needed for the PR comment.
34
+ *
35
+ * Call order:
36
+ * 1. Check for missing receipt (hard fail).
37
+ * 2. Check for tamper_detected integrity (hard fail).
38
+ * 3. Check verifier result against config (configurable gate).
39
+ * 4. Return success / neutral with full comment data.
40
+ */
41
+ export function evaluateChainGate(input) {
42
+ const config = { ...DEFAULT_CONFIG, ...input.config };
43
+ const costResult = input.cost !== undefined
44
+ ? { ...input.cost, exceeded: input.cost.actualUsd > input.cost.budgetUsd }
45
+ : undefined;
46
+ // ── Hard fail 1: no governance receipt ───────────────────────────────────
47
+ if (input.receipt === null || input.verification === null) {
48
+ return {
49
+ noGovernance: true,
50
+ tamperDetected: false,
51
+ verifierFailed: false,
52
+ shouldBlock: true,
53
+ conclusion: "failure",
54
+ failureReasonCode: "no_governance_receipt",
55
+ failureMessage: "No MartinLoop governance receipt was found for this run. " +
56
+ "Every AI-authored PR must carry a verified governance receipt.",
57
+ integrity: "absent",
58
+ ...(costResult !== undefined ? { cost: costResult } : {})
59
+ };
60
+ }
61
+ const integrity = input.verification.integrity;
62
+ // ── Hard fail 2: tampered receipt ─────────────────────────────────────────
63
+ if (integrity === "tamper_detected") {
64
+ return {
65
+ noGovernance: false,
66
+ tamperDetected: true,
67
+ verifierFailed: false,
68
+ shouldBlock: true,
69
+ conclusion: "failure",
70
+ failureReasonCode: "tamper_detected",
71
+ failureMessage: "Governance receipt integrity check failed: tamper detected. " +
72
+ "The receipt hash does not match the producer run record.",
73
+ integrity,
74
+ ...(costResult !== undefined ? { cost: costResult } : {})
75
+ };
76
+ }
77
+ // ── Configurable gate: verifier failed ───────────────────────────────────
78
+ const verifierFailed = !input.verification.ok;
79
+ const blockOnVerifier = verifierFailed && config.blockOnVerifierFailure;
80
+ if (blockOnVerifier) {
81
+ const first = input.verification.reasons[0];
82
+ return {
83
+ noGovernance: false,
84
+ tamperDetected: false,
85
+ verifierFailed: true,
86
+ shouldBlock: true,
87
+ conclusion: "failure",
88
+ failureReasonCode: first?.code ?? "verifier_failed",
89
+ failureMessage: first?.message ?? "Context chain verification failed.",
90
+ integrity,
91
+ ...(costResult !== undefined ? { cost: costResult } : {})
92
+ };
93
+ }
94
+ // ── Pass (or warn-only verifier failure) ──────────────────────────────────
95
+ return {
96
+ noGovernance: false,
97
+ tamperDetected: false,
98
+ verifierFailed,
99
+ shouldBlock: false,
100
+ conclusion: verifierFailed ? "neutral" : "success",
101
+ integrity,
102
+ ...(costResult !== undefined ? { cost: costResult } : {})
103
+ };
104
+ }
105
+ /**
106
+ * Renders an Infracost-style GitHub PR comment summarising the gate result.
107
+ *
108
+ * Hard failures and cost overruns are shown prominently.
109
+ * Cost is always present when provided — never omitted, never a blocking signal.
110
+ * Lineage fields (chainId, producerRunId, runId) provide audit trail links.
111
+ */
112
+ export function renderGatePrComment(result, options = {}) {
113
+ const lines = [];
114
+ lines.push("## MartinLoop Governance");
115
+ lines.push("");
116
+ // Status headline
117
+ if (result.noGovernance) {
118
+ lines.push("🚫 **No governance receipt** — merge is blocked.");
119
+ }
120
+ else if (result.tamperDetected) {
121
+ lines.push("🚫 **Tampered receipt** — merge is blocked.");
122
+ }
123
+ else if (result.verifierFailed && result.shouldBlock) {
124
+ lines.push("⚠️ **Verifier failed** — merge is blocked (configured as required).");
125
+ }
126
+ else if (result.verifierFailed) {
127
+ lines.push("⚠️ **Verifier warnings** — merge is permitted, review recommended.");
128
+ }
129
+ else {
130
+ lines.push("✅ **Governance verified** — chain integrity confirmed.");
131
+ }
132
+ lines.push("");
133
+ lines.push("| Signal | Value | Status |");
134
+ lines.push("|--------|-------|--------|");
135
+ // Chain integrity
136
+ const integrityIcon = result.integrity === "verified"
137
+ ? "✅"
138
+ : result.integrity === "absent" || result.integrity === "tamper_detected"
139
+ ? "🚫"
140
+ : "⚠️";
141
+ lines.push(`| Chain integrity | \`${result.integrity}\` | ${integrityIcon} |`);
142
+ // Cost — always comment-only, never a block signal
143
+ if (result.cost !== undefined) {
144
+ const { actualUsd, budgetUsd, exceeded } = result.cost;
145
+ const costIcon = exceeded ? "⚠️" : "✅";
146
+ const delta = actualUsd - budgetUsd;
147
+ const costNote = exceeded
148
+ ? `over budget (+$${delta.toFixed(4)})`
149
+ : "within budget";
150
+ lines.push(`| Cost | \`$${actualUsd.toFixed(4)}\` / \`$${budgetUsd.toFixed(4)}\` | ${costIcon} ${costNote} |`);
151
+ }
152
+ // Lineage (audit trail, never a block signal)
153
+ if (options.chainId) {
154
+ lines.push(`| Chain ID | \`${options.chainId}\` | ℹ️ |`);
155
+ }
156
+ if (options.handoffId) {
157
+ lines.push(`| Handoff ID | \`${options.handoffId}\` | ℹ️ |`);
158
+ }
159
+ if (options.producerRunId) {
160
+ lines.push(`| Producer run | \`${options.producerRunId}\` | ℹ️ |`);
161
+ }
162
+ if (options.runId) {
163
+ lines.push(`| Consumer run | \`${options.runId}\` | ℹ️ |`);
164
+ }
165
+ // Failure detail
166
+ if (result.failureMessage) {
167
+ lines.push("");
168
+ lines.push(`> **Failure:** ${result.failureMessage}`);
169
+ }
170
+ lines.push("");
171
+ lines.push("_Powered by [MartinLoop](https://github.com/Keesan12/martin-loop) governance gate_");
172
+ return lines.join("\n");
173
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Context Compiler — A-CTX-1
3
+ *
4
+ * Deterministic working-set selector. Given a set of ContextObject candidates,
5
+ * a budget, a policy, and an explicit nowMs, produces a ContextManifest whose
6
+ * hash is identical for identical inputs on any platform.
7
+ *
8
+ * Shadow mode: the manifest is computed and emitted but the original provider
9
+ * packet is returned byte-for-byte unchanged.
10
+ *
11
+ * Standing invariants:
12
+ * - nowMs is ALWAYS a captured input — never Date.now() internally
13
+ * - No raw source text in manifest or ledger receipt — hashes and counts only
14
+ * - No confidence float, no outcome_delta — both permanently banned
15
+ * - Fault budget accumulates by taskId, not runId
16
+ * - required-over-budget is an explicit failure, never a silent drop
17
+ * - MAX_RENDER_PASSES = 5 — hard constant
18
+ */
19
+ import type { ContextBudget, ContextLedgerEntry, ContextManifest, ContextObject, ContextPolicy } from "../contracts/index.js";
20
+ export declare const MAX_RENDER_PASSES = 5;
21
+ /**
22
+ * Minimal adapter interface for token recounting.
23
+ * Implementations may call the provider SDK's token counter.
24
+ * Must not make network calls to the provider inference endpoint.
25
+ */
26
+ export interface ContextAdapter {
27
+ readonly version: string;
28
+ /** Recount the actual rendered tokens for a serialized text segment. */
29
+ recountTokens(text: string): number;
30
+ }
31
+ /** Fallback adapter: char÷4 heuristic, no external calls. */
32
+ export declare const HEURISTIC_ADAPTER: ContextAdapter;
33
+ export interface CompileContextInput {
34
+ taskId: string;
35
+ runId: string;
36
+ /** Must be a captured value — never call Date.now() inside the compiler. */
37
+ nowMs: number;
38
+ candidates: readonly ContextObject[];
39
+ budget: ContextBudget;
40
+ policy: ContextPolicy;
41
+ adapter: ContextAdapter;
42
+ }
43
+ export type CompileContextOutput = {
44
+ ok: true;
45
+ manifest: ContextManifest;
46
+ ledgerEntry: ContextLedgerEntry;
47
+ } | {
48
+ ok: false;
49
+ reason: "required_over_budget" | "recount_exceeded_passes";
50
+ manifest: null;
51
+ ledgerEntry: null;
52
+ };
53
+ export declare function compileContext(input: CompileContextInput): CompileContextOutput;
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Context Compiler — A-CTX-1
3
+ *
4
+ * Deterministic working-set selector. Given a set of ContextObject candidates,
5
+ * a budget, a policy, and an explicit nowMs, produces a ContextManifest whose
6
+ * hash is identical for identical inputs on any platform.
7
+ *
8
+ * Shadow mode: the manifest is computed and emitted but the original provider
9
+ * packet is returned byte-for-byte unchanged.
10
+ *
11
+ * Standing invariants:
12
+ * - nowMs is ALWAYS a captured input — never Date.now() internally
13
+ * - No raw source text in manifest or ledger receipt — hashes and counts only
14
+ * - No confidence float, no outcome_delta — both permanently banned
15
+ * - Fault budget accumulates by taskId, not runId
16
+ * - required-over-budget is an explicit failure, never a silent drop
17
+ * - MAX_RENDER_PASSES = 5 — hard constant
18
+ */
19
+ import { createHash } from "node:crypto";
20
+ import { CONTEXT_LEDGER_VERSION, CONTEXT_MANIFEST_VERSION } from "../contracts/index.js";
21
+ // ─── Constants ────────────────────────────────────────────────────────────────
22
+ export const MAX_RENDER_PASSES = 5;
23
+ /** Fallback adapter: char÷4 heuristic, no external calls. */
24
+ export const HEURISTIC_ADAPTER = {
25
+ version: "heuristic@1",
26
+ recountTokens: (text) => text.length === 0 ? 0 : Math.max(1, Math.ceil(Buffer.byteLength(text, "utf8") / 4))
27
+ };
28
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
29
+ function sha256(value) {
30
+ return createHash("sha256").update(value, "utf8").digest("hex");
31
+ }
32
+ function canonicalize(value) {
33
+ if (Array.isArray(value))
34
+ return value.map(canonicalize);
35
+ if (value !== null && typeof value === "object") {
36
+ return Object.fromEntries(Object.entries(value)
37
+ .sort(([a], [b]) => a.localeCompare(b))
38
+ .map(([k, v]) => [k, canonicalize(v)]));
39
+ }
40
+ return value;
41
+ }
42
+ function canonicalJson(value) {
43
+ return JSON.stringify(canonicalize(value));
44
+ }
45
+ /** Priority order for selection: required → pinned → high → normal → low */
46
+ const PRIORITY_RANK = {
47
+ required: 0,
48
+ high: 2,
49
+ normal: 3,
50
+ low: 4
51
+ };
52
+ /** Pinned objects slot between required and high. */
53
+ function selectionRank(obj) {
54
+ if (obj.priority === "required")
55
+ return 0;
56
+ if (obj.pinnedBy !== undefined)
57
+ return 1;
58
+ return PRIORITY_RANK[obj.priority];
59
+ }
60
+ // ─── Compiler ────────────────────────────────────────────────────────────────
61
+ export function compileContext(input) {
62
+ const { taskId, runId, nowMs, candidates, budget, policy, adapter } = input;
63
+ // ── Eligibility filter — policy-denied objects become governed exclusion records ──
64
+ //
65
+ // A-CTX-2 fix: denied objects must NOT silently disappear. They enter decisions[]
66
+ // as "excluded" with a reason code so the exclusion is visible as governed evidence.
67
+ // executableContext = admittedObjects only; ledgerDecisions = admitted + excluded.
68
+ const policyExcluded = [];
69
+ const eligibleList = [];
70
+ for (const obj of candidates) {
71
+ if (policy.deniedSensitivities.includes(obj.sensitivity)) {
72
+ policyExcluded.push({
73
+ objectId: obj.id,
74
+ decision: "excluded",
75
+ reason: `sensitivity_denied:${obj.sensitivity}`
76
+ });
77
+ }
78
+ else if (policy.deniedTrustLevels.includes(obj.trust)) {
79
+ policyExcluded.push({
80
+ objectId: obj.id,
81
+ decision: "excluded",
82
+ reason: `trust_denied:${obj.trust}`
83
+ });
84
+ }
85
+ else {
86
+ eligibleList.push(obj);
87
+ }
88
+ }
89
+ const eligible = eligibleList;
90
+ // ── Compute compilerInputHash ────────────────────────────────────────────
91
+ const inputFingerprint = {
92
+ taskId,
93
+ runId,
94
+ nowMs,
95
+ adapterVersion: adapter.version,
96
+ policyHash: policy.policyHash,
97
+ budget: {
98
+ modelWindowTokens: budget.modelWindowTokens,
99
+ maxWorkingSetTokens: budget.maxWorkingSetTokens,
100
+ pinnedTokensMax: budget.pinnedTokensMax
101
+ },
102
+ candidateIds: eligible.map((o) => o.id).sort()
103
+ };
104
+ // ── Pinned token cap ─────────────────────────────────────────────────────
105
+ const pinnedTokensTotal = eligible
106
+ .filter((o) => o.pinnedBy !== undefined)
107
+ .reduce((s, o) => s + o.estimatedTokens, 0);
108
+ if (pinnedTokensTotal > budget.pinnedTokensMax) {
109
+ // Fail-closed: reject the entire batch, not individual pins
110
+ return { ok: false, reason: "required_over_budget", manifest: null, ledgerEntry: null };
111
+ }
112
+ // ── Sort by selection rank, then objectId as stable tie-breaker ──────────
113
+ const sorted = [...eligible].sort((a, b) => {
114
+ const rankDiff = selectionRank(a) - selectionRank(b);
115
+ if (rankDiff !== 0)
116
+ return rankDiff;
117
+ // Stable tie-breaker: objectId lexicographic
118
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
119
+ });
120
+ // ── Selection pass ───────────────────────────────────────────────────────
121
+ let remaining = budget.maxWorkingSetTokens;
122
+ const decisions = [];
123
+ let requiredOverBudget = false;
124
+ // Required objects total token check first
125
+ const requiredTokens = sorted
126
+ .filter((o) => o.priority === "required")
127
+ .reduce((s, o) => s + o.estimatedTokens, 0);
128
+ if (requiredTokens > remaining) {
129
+ requiredOverBudget = true;
130
+ if (policy.requiredOverBudgetAction === "fail_closed") {
131
+ return { ok: false, reason: "required_over_budget", manifest: null, ledgerEntry: null };
132
+ }
133
+ // explicit_escalation: record and continue — all required marked excluded
134
+ }
135
+ for (const obj of sorted) {
136
+ const isRequired = obj.priority === "required";
137
+ if (requiredOverBudget && isRequired) {
138
+ decisions.push({
139
+ objectId: obj.id,
140
+ decision: "excluded",
141
+ reason: "required_over_budget"
142
+ });
143
+ continue;
144
+ }
145
+ if (obj.estimatedTokens <= remaining) {
146
+ decisions.push({
147
+ objectId: obj.id,
148
+ decision: "included",
149
+ reason: isRequired ? "required" : obj.pinnedBy !== undefined ? "pinned" : "within_budget"
150
+ });
151
+ remaining -= obj.estimatedTokens;
152
+ }
153
+ else {
154
+ // Attempt truncation for required/pinned, exclude otherwise
155
+ if (isRequired || obj.pinnedBy !== undefined) {
156
+ decisions.push({
157
+ objectId: obj.id,
158
+ decision: "included_truncated",
159
+ truncatedFromTokens: obj.estimatedTokens,
160
+ reason: isRequired ? "required_truncated" : "pinned_truncated"
161
+ });
162
+ // Truncated objects consume whatever is left
163
+ remaining = 0;
164
+ }
165
+ else {
166
+ decisions.push({
167
+ objectId: obj.id,
168
+ decision: "excluded",
169
+ reason: "budget_exhausted"
170
+ });
171
+ }
172
+ }
173
+ }
174
+ // ── Adapter recount (up to MAX_RENDER_PASSES) ────────────────────────────
175
+ const includedDecisions = decisions.filter((d) => d.decision === "included" || d.decision === "included_truncated");
176
+ let totalRenderedTokens = 0;
177
+ let renderPasses = 0;
178
+ let recountConverged = false;
179
+ const includedObjects = new Map(eligible.map((o) => [o.id, o]));
180
+ for (let pass = 0; pass < MAX_RENDER_PASSES; pass++) {
181
+ renderPasses = pass + 1;
182
+ let passTotal = 0;
183
+ for (const d of includedDecisions) {
184
+ const obj = includedObjects.get(d.objectId);
185
+ if (!obj)
186
+ continue;
187
+ // In shadow mode we use the contentHash as a proxy text for recounting
188
+ const recounted = adapter.recountTokens(obj.contentHash);
189
+ d.renderedTokens = recounted;
190
+ passTotal += recounted;
191
+ }
192
+ if (passTotal === totalRenderedTokens && pass > 0) {
193
+ recountConverged = true;
194
+ break;
195
+ }
196
+ totalRenderedTokens = passTotal;
197
+ if (pass === 0)
198
+ recountConverged = false;
199
+ }
200
+ if (!recountConverged && renderPasses >= MAX_RENDER_PASSES) {
201
+ return { ok: false, reason: "recount_exceeded_passes", manifest: null, ledgerEntry: null };
202
+ }
203
+ // ── Merge policy exclusions — excluded objects precede selection decisions ──
204
+ //
205
+ // A-CTX-2: executableContext = admittedObjects (decisions[]);
206
+ // ledgerDecisions = [...policyExcluded, ...decisions]
207
+ // Policy-excluded objects never enter the admitted working set, but their
208
+ // exclusion is visible as governed evidence in the manifest and ledger.
209
+ const allDecisions = [...policyExcluded, ...decisions];
210
+ // ── Manifest hash ────────────────────────────────────────────────────────
211
+ const manifestHashInput = canonicalJson({
212
+ adapterVersion: adapter.version,
213
+ budget: inputFingerprint.budget,
214
+ decisions: allDecisions.map((d) => ({
215
+ decision: d.decision,
216
+ objectId: d.objectId,
217
+ reason: d.reason,
218
+ ...(d.truncatedFromTokens !== undefined
219
+ ? { truncatedFromTokens: d.truncatedFromTokens }
220
+ : {})
221
+ })),
222
+ nowMs,
223
+ policyHash: policy.policyHash,
224
+ runId,
225
+ taskId,
226
+ totalRenderedTokens
227
+ });
228
+ const manifestHash = sha256(manifestHashInput);
229
+ const compilationDurationMs = 0; // In shadow mode: nowMs is frozen; duration is metadata-only
230
+ const manifest = {
231
+ schemaVersion: CONTEXT_MANIFEST_VERSION,
232
+ manifestHash,
233
+ taskId,
234
+ runId,
235
+ nowMs,
236
+ adapterVersion: adapter.version,
237
+ policyHash: policy.policyHash,
238
+ budget,
239
+ decisions: allDecisions,
240
+ totalRenderedTokens,
241
+ renderPasses,
242
+ compilationDurationMs
243
+ };
244
+ const totalIncluded = allDecisions.filter((d) => d.decision === "included" || d.decision === "included_truncated").length;
245
+ const totalExcluded = allDecisions.filter((d) => d.decision === "excluded").length;
246
+ const totalFaultLoaded = allDecisions.filter((d) => d.decision === "fault_loaded").length;
247
+ const ledgerEntry = {
248
+ schemaVersion: CONTEXT_LEDGER_VERSION,
249
+ entryType: "context",
250
+ taskId,
251
+ runId,
252
+ manifestHash,
253
+ totalIncluded,
254
+ totalExcluded,
255
+ totalFaultLoaded,
256
+ overflowCount: requiredOverBudget ? 1 : 0,
257
+ faultBudgetUsed: 0,
258
+ adapterVersion: adapter.version
259
+ };
260
+ return { ok: true, manifest, ledgerEntry };
261
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Context Handoff Verifier — A-CTX-2
3
+ *
4
+ * Pure functions for verifying upstream-to-downstream context handoffs and
5
+ * deciding whether to circuit-break before the downstream agent executes.
6
+ *
7
+ * Standing rules:
8
+ * - These functions are pure and side-effect-free.
9
+ * - verifyContextHandoff fails closed — any ambiguity → not ok.
10
+ * - decideContextCircuitBreak is the sole gate before adapter execution.
11
+ * - The downstream adapter MUST NOT be called when shouldStop is true.
12
+ */
13
+ import type { ContextCircuitBreakResult, ContextHandoffReceipt, ContextHandoffVerification } from "../contracts/index.js";
14
+ export interface VerifyContextHandoffInput {
15
+ handoff: ContextHandoffReceipt;
16
+ /** true when the producer receipt file hash has been independently confirmed */
17
+ producerReceiptVerified: boolean;
18
+ /** Map of sha256 → true for every artifact hash available to the verifier */
19
+ availableArtifacts: ReadonlyMap<string, true>;
20
+ }
21
+ /**
22
+ * Deterministically verifies a context handoff.
23
+ *
24
+ * Returns ok=false when ANY of the following is true:
25
+ * - schemaVersion is not in SUPPORTED_SCHEMAS
26
+ * - producerReceiptVerified is false
27
+ * - upstreamIntegrity is not "verified"
28
+ * - a required artifact is absent from availableArtifacts
29
+ * - an artifact sha256 does not appear in availableArtifacts (hash changed)
30
+ * - any required claim has state "unverified" or "rejected"
31
+ * - any claim has state "unknown"
32
+ * - unresolvedAssumptions is non-empty
33
+ * - handoffId, chainId, producerRunId, or producerReceiptHash is blank
34
+ */
35
+ export declare function verifyContextHandoff(input: VerifyContextHandoffInput): ContextHandoffVerification;
36
+ /**
37
+ * Converts a ContextHandoffVerification into a gate decision.
38
+ *
39
+ * Healthy: { shouldStop: false, silent: true }
40
+ * Broken: { shouldStop: true, silent: false, reasonCode, message, nextAction }
41
+ */
42
+ export declare function decideContextCircuitBreak(verification: ContextHandoffVerification): ContextCircuitBreakResult;