@martinloop/mcp 0.7.1 → 0.8.1
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/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/vendor/adapters/claude-cli.d.ts +16 -1
- package/dist/vendor/adapters/claude-cli.js +286 -71
- package/dist/vendor/adapters/cli-bridge.d.ts +12 -3
- package/dist/vendor/adapters/cli-bridge.js +129 -149
- package/dist/vendor/adapters/codex-capabilities.d.ts +4 -1
- package/dist/vendor/adapters/codex-capabilities.js +36 -4
- package/dist/vendor/adapters/codex-cli.d.ts +10 -3
- package/dist/vendor/adapters/codex-cli.js +21 -24
- package/dist/vendor/adapters/codex-host.js +150 -132
- package/dist/vendor/adapters/index.d.ts +3 -2
- package/dist/vendor/adapters/index.js +3 -2
- package/dist/vendor/adapters/openai-compatible.js +88 -6
- package/dist/vendor/adapters/process-supervisor.d.ts +60 -0
- package/dist/vendor/adapters/process-supervisor.js +541 -0
- package/dist/vendor/adapters/verifier-only.d.ts +10 -1
- package/dist/vendor/adapters/verifier-only.js +43 -1
- package/dist/vendor/contracts/external-outcome.d.ts +102 -0
- package/dist/vendor/contracts/external-outcome.js +226 -0
- package/dist/vendor/contracts/index.d.ts +12 -1
- package/dist/vendor/contracts/index.js +2 -0
- package/dist/vendor/contracts/swarm-hosted.d.ts +204 -0
- package/dist/vendor/contracts/swarm-hosted.js +629 -0
- package/dist/vendor/contracts/swarm.d.ts +449 -0
- package/dist/vendor/contracts/swarm.js +550 -0
- package/dist/vendor/core/external-outcome.d.ts +34 -0
- package/dist/vendor/core/external-outcome.js +515 -0
- package/dist/vendor/core/grounding.js +1 -1
- package/dist/vendor/core/index.d.ts +52 -1
- package/dist/vendor/core/index.js +55 -3
- package/dist/vendor/core/leash.d.ts +1 -0
- package/dist/vendor/core/leash.js +13 -0
- package/dist/vendor/core/persistence/integrity.d.ts +4 -0
- package/dist/vendor/core/persistence/integrity.js +19 -6
- package/dist/vendor/core/persistence/swarm-integrity.d.ts +63 -0
- package/dist/vendor/core/persistence/swarm-integrity.js +309 -0
- package/dist/vendor/core/policy.d.ts +3 -0
- package/dist/vendor/core/policy.js +13 -0
- package/dist/vendor/core/swarm/candidates.d.ts +78 -0
- package/dist/vendor/core/swarm/candidates.js +810 -0
- package/dist/vendor/core/swarm/evidence-projection.d.ts +87 -0
- package/dist/vendor/core/swarm/evidence-projection.js +128 -0
- package/dist/vendor/core/swarm/evidence.d.ts +40 -0
- package/dist/vendor/core/swarm/evidence.js +575 -0
- package/dist/vendor/core/swarm/hosted-export.d.ts +48 -0
- package/dist/vendor/core/swarm/hosted-export.js +362 -0
- package/dist/vendor/core/swarm/hosted-projection.d.ts +5 -0
- package/dist/vendor/core/swarm/hosted-projection.js +258 -0
- package/dist/vendor/core/swarm/index.d.ts +110 -0
- package/dist/vendor/core/swarm/index.js +1060 -0
- package/dist/vendor/core/swarm/integration.d.ts +76 -0
- package/dist/vendor/core/swarm/integration.js +615 -0
- package/dist/vendor/core/swarm/lifecycle.d.ts +72 -0
- package/dist/vendor/core/swarm/lifecycle.js +252 -0
- package/dist/vendor/core/swarm/live-runtime.d.ts +78 -0
- package/dist/vendor/core/swarm/live-runtime.js +1482 -0
- package/dist/vendor/core/swarm/live-store.d.ts +99 -0
- package/dist/vendor/core/swarm/live-store.js +860 -0
- package/dist/vendor/core/swarm/operational-reader.d.ts +17 -0
- package/dist/vendor/core/swarm/operational-reader.js +96 -0
- package/dist/vendor/core/swarm/operations.d.ts +29 -0
- package/dist/vendor/core/swarm/operations.js +128 -0
- package/dist/vendor/core/swarm/parent-receipt.d.ts +13 -0
- package/dist/vendor/core/swarm/parent-receipt.js +489 -0
- package/dist/vendor/core/swarm/receipt-projection.d.ts +26 -0
- package/dist/vendor/core/swarm/receipt-projection.js +120 -0
- package/dist/vendor/core/swarm/scheduler.d.ts +92 -0
- package/dist/vendor/core/swarm/scheduler.js +555 -0
- package/dist/vendor/core/swarm/verification.d.ts +70 -0
- package/dist/vendor/core/swarm/verification.js +295 -0
- package/dist/vendor/core/swarm/workspaces.d.ts +77 -0
- package/dist/vendor/core/swarm/workspaces.js +488 -0
- package/dist/vendor/core/verified-handoff.d.ts +4 -0
- package/dist/vendor/core/verified-handoff.js +4 -0
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -16,6 +16,7 @@ import { verifierActuallyPassed, } from "./verified-handoff.js";
|
|
|
16
16
|
export { DEFAULT_AGENT_EXECUTION_INTENT, DEFAULT_PROVIDER_EXECUTION_TIMEOUT_MS, GOVERNED_AUTONOMOUS_BOUNDARY, normalizeProviderExecutionTimeoutMs } from "../contracts/index.js";
|
|
17
17
|
export { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe, evaluateVerificationLeash, evaluateFilesystemLeash, evaluateChangeApprovalLeash, evaluateSecretLeash, resolveExecutionProfile, redactSecretsFromText, buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations, captureRollbackBoundary, listAttemptChangedFilesSinceBoundary, restoreRollbackBoundary };
|
|
18
18
|
export { buildContextGraphSnapshot, queryContextGraph } from "./context-graph.js";
|
|
19
|
+
export { ExternalOutcomeContractError, ExternalOutcomePolicyError, hashExternalOutcomeContract, verifyExternalOutcomes, writeExternalOutcomeEvidence, } from "./external-outcome.js";
|
|
19
20
|
export { createLocalIdentityAuthority, hasIdentityScope, issueIdentityToken, verifyIdentityToken } from "./identity.js";
|
|
20
21
|
export { compileExecutionPolicy } from "./policy-compiler.js";
|
|
21
22
|
export { assessTrajectory, decideCircuitBreak } from "./trajectory.js";
|
|
@@ -401,6 +402,34 @@ export async function runMartin(input) {
|
|
|
401
402
|
}
|
|
402
403
|
}
|
|
403
404
|
}
|
|
405
|
+
let parentAbortObserved = false;
|
|
406
|
+
function observeParentAbort() {
|
|
407
|
+
if (parentAbortObserved || input.signal === undefined)
|
|
408
|
+
return;
|
|
409
|
+
parentAbortObserved = true;
|
|
410
|
+
const reason = input.signal.reason instanceof Error
|
|
411
|
+
? input.signal.reason.message
|
|
412
|
+
: typeof input.signal.reason === "string"
|
|
413
|
+
? input.signal.reason
|
|
414
|
+
: "parent cancelled governed child run";
|
|
415
|
+
const parentSignal = {
|
|
416
|
+
kind: "human_interrupt",
|
|
417
|
+
schemaVersion: "exit-signal/1",
|
|
418
|
+
runId: loop.loopId,
|
|
419
|
+
reason: `Parent cancellation: ${reason}`,
|
|
420
|
+
requestedAt: now(),
|
|
421
|
+
requestedBy: "parent_swarm"
|
|
422
|
+
};
|
|
423
|
+
observeSignals([parentSignal]);
|
|
424
|
+
activeAttemptController?.abort(input.signal.reason ?? parentSignal);
|
|
425
|
+
}
|
|
426
|
+
if (input.signal?.aborted)
|
|
427
|
+
observeParentAbort();
|
|
428
|
+
else if (input.signal) {
|
|
429
|
+
input.signal.addEventListener("abort", observeParentAbort, { once: true });
|
|
430
|
+
if (input.signal.aborted)
|
|
431
|
+
observeParentAbort();
|
|
432
|
+
}
|
|
404
433
|
function queueControlDiagnostic(code, details) {
|
|
405
434
|
if (!input.store)
|
|
406
435
|
return;
|
|
@@ -443,6 +472,7 @@ export async function runMartin(input) {
|
|
|
443
472
|
// snapshot. Draining alone leaves a window for new writes during signing.
|
|
444
473
|
disposeSignalMonitor();
|
|
445
474
|
runController.abort();
|
|
475
|
+
input.signal?.removeEventListener("abort", observeParentAbort);
|
|
446
476
|
await Promise.all(pendingControlWrites.splice(0));
|
|
447
477
|
}
|
|
448
478
|
// Snapshot of the current run state for eight-exit evaluation
|
|
@@ -570,6 +600,8 @@ export async function runMartin(input) {
|
|
|
570
600
|
promptCharCount: distilled.focus.length + loop.task.objective.length * 3,
|
|
571
601
|
attemptCount: loop.attempts.length,
|
|
572
602
|
remainingBudgetUsd: distilled.constraints.remainingBudgetUsd,
|
|
603
|
+
remainingTokens: distilled.constraints.remainingTokens,
|
|
604
|
+
minimumViableTokens: executingAdapter.metadata.budgetPreflight?.minimumViableTokens,
|
|
573
605
|
perAttemptCapUsd: loop.budget.maxUsd * 0.25
|
|
574
606
|
});
|
|
575
607
|
if (!budgetPreflight.allowed) {
|
|
@@ -753,6 +785,7 @@ export async function runMartin(input) {
|
|
|
753
785
|
? { definitionOfDonePreSatisfied: loop.task.definitionOfDonePreSatisfied }
|
|
754
786
|
: {}),
|
|
755
787
|
...(loop.task.repoRoot ? { repoRoot: loop.task.repoRoot } : {}),
|
|
788
|
+
...(input.store?.runsRoot ? { runsRoot: input.store.runsRoot } : {}),
|
|
756
789
|
...(loop.task.allowedPaths ? { allowedPaths: loop.task.allowedPaths } : {}),
|
|
757
790
|
...(loop.task.deniedPaths ? { deniedPaths: loop.task.deniedPaths } : {}),
|
|
758
791
|
...(loop.task.acceptanceCriteria ? { acceptanceCriteria: loop.task.acceptanceCriteria } : {}),
|
|
@@ -766,9 +799,13 @@ export async function runMartin(input) {
|
|
|
766
799
|
focus: distilled.focus,
|
|
767
800
|
remainingBudgetUsd: distilled.constraints.remainingBudgetUsd,
|
|
768
801
|
remainingIterations: distilled.constraints.remainingIterations,
|
|
769
|
-
remainingTokens: distilled.constraints.remainingTokens
|
|
802
|
+
remainingTokens: distilled.constraints.remainingTokens,
|
|
803
|
+
...(input.verificationExecutionOwner
|
|
804
|
+
? { verificationExecutionOwner: input.verificationExecutionOwner }
|
|
805
|
+
: {})
|
|
770
806
|
},
|
|
771
|
-
previousAttempts: loop.attempts
|
|
807
|
+
previousAttempts: loop.attempts,
|
|
808
|
+
...(input.observedUsageGovernor ? { observedUsageGovernor: input.observedUsageGovernor } : {})
|
|
772
809
|
};
|
|
773
810
|
const tracksWorkspaceMutations = request.context.repoRoot !== undefined &&
|
|
774
811
|
executingAdapter.metadata.capabilities?.workspaceMutations !== false;
|
|
@@ -996,7 +1033,8 @@ export async function runMartin(input) {
|
|
|
996
1033
|
repoRoot: request.context.repoRoot,
|
|
997
1034
|
changedFiles,
|
|
998
1035
|
allowedPaths: request.context.allowedPaths,
|
|
999
|
-
deniedPaths: request.context.deniedPaths
|
|
1036
|
+
deniedPaths: request.context.deniedPaths,
|
|
1037
|
+
mutationMode: request.context.mutationMode
|
|
1000
1038
|
});
|
|
1001
1039
|
if (!filesystemDecision.allowed) {
|
|
1002
1040
|
const patchDecision = evaluatePatchDecision({
|
|
@@ -1412,6 +1450,7 @@ export async function runMartin(input) {
|
|
|
1412
1450
|
clearTimeout(wallTimer);
|
|
1413
1451
|
disposeSignalMonitor();
|
|
1414
1452
|
runController.abort();
|
|
1453
|
+
input.signal?.removeEventListener("abort", observeParentAbort);
|
|
1415
1454
|
activeAttemptController?.abort();
|
|
1416
1455
|
// Flush any diagnostic ledger writes queued from signal callbacks
|
|
1417
1456
|
await Promise.all(pendingControlWrites);
|
|
@@ -1581,6 +1620,9 @@ function resolveChangedFiles(result, repoRoot, rollbackBoundary) {
|
|
|
1581
1620
|
return listAttemptChangedFilesSinceBoundary({ repoRoot, boundary: rollbackBoundary });
|
|
1582
1621
|
}
|
|
1583
1622
|
function buildPatchDiff(result, changedFiles) {
|
|
1623
|
+
if (result.execution?.patch) {
|
|
1624
|
+
return result.execution.patch;
|
|
1625
|
+
}
|
|
1584
1626
|
// Use structured diff stats to build a minimal diff header if no raw diff is available
|
|
1585
1627
|
if (result.execution?.changedFiles?.length) {
|
|
1586
1628
|
// Build a synthetic diff header from changed file list
|
|
@@ -1663,7 +1705,11 @@ function resolveExpectedVerifierBinding(request) {
|
|
|
1663
1705
|
return {
|
|
1664
1706
|
runId: request.loopId,
|
|
1665
1707
|
workspaceId: request.workspaceId,
|
|
1708
|
+
attemptId: request.attemptId,
|
|
1666
1709
|
cwd: request.context.repoRoot ?? process.cwd(),
|
|
1710
|
+
...(request.context.runsRoot ? { runsRoot: request.context.runsRoot } : {}),
|
|
1711
|
+
...(request.context.executionProfile ? { executionProfile: request.context.executionProfile } : {}),
|
|
1712
|
+
...(request.context.allowedNetworkDomains?.length ? { allowedNetworkDomains: [...request.context.allowedNetworkDomains] } : {}),
|
|
1667
1713
|
commands,
|
|
1668
1714
|
};
|
|
1669
1715
|
}
|
|
@@ -1806,3 +1852,9 @@ export { createDefaultExitPolicy, evaluateExitPolicy, hashProgressState, toLegac
|
|
|
1806
1852
|
export { SignalDiagnosticError, createFileExitSignalSource, exitSignalPath, readAllExitSignals, readExitSignal, startExitSignalMonitor, writeExitSignal } from "./exit-signal.js";
|
|
1807
1853
|
// ─── R4 Delivery ─────────────────────────────────────────────────────────────
|
|
1808
1854
|
export { cacheMessage, fetchSelectedMessage, getCliInstalledVersion, getMcpInstalledVersion, isCooldownExpired, isDismissed, isNewerVersion, loadDeliveryRecord, parseMessageSelectionResponse, recordDismissed, recordShown, resolveDefaultLedgerPath, saveDeliveryRecord, } from "./delivery/index.js";
|
|
1855
|
+
export { completeDeterministicDemoSwarmRun, createSwarmBudgetLedger, extendSwarmBudgetLease, releaseSwarmBudgetLease, reserveSwarmBudgetLease, selectNextSwarmBatch, settleSwarmBudgetLease, tasksHaveScopeCollision, validateSwarmTaskGraph, runParentSwarmPipeline } from "./swarm/index.js";
|
|
1856
|
+
export { readSwarmOperationalState } from "./swarm/operational-reader.js";
|
|
1857
|
+
export { readSwarmReceiptProjection, verifySwarmReceiptProjection, } from "./swarm/receipt-projection.js";
|
|
1858
|
+
export { buildSwarmShareProjection, readSwarmDossier, verifySwarmEvidence, } from "./swarm/evidence-projection.js";
|
|
1859
|
+
export { buildSwarmHostedEnvelope } from "./swarm/hosted-export.js";
|
|
1860
|
+
export { assertSwarmPathIdentifier } from "./swarm/workspaces.js";
|
|
@@ -32,6 +32,7 @@ export declare function evaluateFilesystemLeash(input: {
|
|
|
32
32
|
changedFiles: string[];
|
|
33
33
|
allowedPaths?: string[];
|
|
34
34
|
deniedPaths?: string[];
|
|
35
|
+
mutationMode?: "edit" | "read_only" | "verify_only";
|
|
35
36
|
}): SafetyLeashDecision;
|
|
36
37
|
export declare function evaluateSecretLeash(input: {
|
|
37
38
|
values: string[];
|
|
@@ -52,6 +52,11 @@ function commandContainsDestructiveRemoval(command) {
|
|
|
52
52
|
return false;
|
|
53
53
|
}
|
|
54
54
|
const SECRET_PATTERNS = [
|
|
55
|
+
{
|
|
56
|
+
kind: "secret_value",
|
|
57
|
+
pattern: /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}\b/giu,
|
|
58
|
+
replacement: "Bearer [REDACTED_SECRET]"
|
|
59
|
+
},
|
|
55
60
|
{
|
|
56
61
|
kind: "secret_value",
|
|
57
62
|
pattern: /\bOPENAI_API_KEY\s*=\s*[^\s"'`]+/giu,
|
|
@@ -196,6 +201,14 @@ export function evaluateFilesystemLeash(input) {
|
|
|
196
201
|
if (!file) {
|
|
197
202
|
continue;
|
|
198
203
|
}
|
|
204
|
+
if (input.mutationMode === "read_only") {
|
|
205
|
+
violations.push({
|
|
206
|
+
kind: "path_not_allowed",
|
|
207
|
+
file,
|
|
208
|
+
message: `Read-only execution changed a file: ${file}`
|
|
209
|
+
});
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
199
212
|
if ((input.deniedPaths ?? []).some((pattern) => matchesPathPattern(file, pattern))) {
|
|
200
213
|
violations.push({
|
|
201
214
|
kind: "path_denied",
|
|
@@ -37,3 +37,7 @@ export declare function verifyReceiptIntegrityFromFiles(input: {
|
|
|
37
37
|
ledgerPath: string;
|
|
38
38
|
}): Promise<ReceiptIntegritySummary>;
|
|
39
39
|
export declare function resolveReceiptIntegrityPath(runsRoot: string, runId: string): string;
|
|
40
|
+
export declare function ensurePrivateIntegrityKey(runsRoot: string, keyFileName: string): Promise<[string, string] | undefined>;
|
|
41
|
+
export declare function readPrivateIntegrityKey(runsRoot: string, keyFileName: string): Promise<string>;
|
|
42
|
+
export declare function resolvePrivateIntegrityKeyPath(runsRoot: string, keyFileName: string): string;
|
|
43
|
+
export declare function createPrivateIntegrityHmac(key: string, material: unknown): string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, createHmac, randomBytes } from "node:crypto";
|
|
2
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { lstat, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
const RECEIPT_INTEGRITY_SCHEMA_VERSION = "martin.receipt-integrity.v1";
|
|
@@ -208,10 +208,17 @@ function buildReceiptIntegrityChain(entries) {
|
|
|
208
208
|
});
|
|
209
209
|
}
|
|
210
210
|
function createReceiptIntegritySignature(key, material) {
|
|
211
|
-
return
|
|
211
|
+
return createPrivateIntegrityHmac(key, material);
|
|
212
212
|
}
|
|
213
213
|
async function ensureReceiptIntegrityKey(runsRoot, runId) {
|
|
214
|
-
|
|
214
|
+
return ensurePrivateIntegrityKey(runsRoot, `${runId}.key`);
|
|
215
|
+
}
|
|
216
|
+
export async function ensurePrivateIntegrityKey(runsRoot, keyFileName) {
|
|
217
|
+
const keyPath = resolvePrivateIntegrityKeyPath(runsRoot, keyFileName);
|
|
218
|
+
const existingStat = await lstat(keyPath).catch(() => undefined);
|
|
219
|
+
if (existingStat?.isSymbolicLink() || (existingStat && !existingStat.isFile())) {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
215
222
|
const existing = await readFile(keyPath, "utf8").catch(() => null);
|
|
216
223
|
if (existing) {
|
|
217
224
|
const trimmed = existing.trim();
|
|
@@ -231,12 +238,18 @@ async function ensureReceiptIntegrityKey(runsRoot, runId) {
|
|
|
231
238
|
return [generated, sha256(generated).slice(0, 16)];
|
|
232
239
|
}
|
|
233
240
|
async function readReceiptIntegrityKey(runsRoot, runId) {
|
|
234
|
-
|
|
241
|
+
return readPrivateIntegrityKey(runsRoot, `${runId}.key`);
|
|
242
|
+
}
|
|
243
|
+
export async function readPrivateIntegrityKey(runsRoot, keyFileName) {
|
|
244
|
+
const raw = await readFile(resolvePrivateIntegrityKeyPath(runsRoot, keyFileName), "utf8");
|
|
235
245
|
return raw.trim();
|
|
236
246
|
}
|
|
237
|
-
function
|
|
247
|
+
export function resolvePrivateIntegrityKeyPath(runsRoot, keyFileName) {
|
|
238
248
|
const rootHash = sha256(runsRoot).slice(0, 16);
|
|
239
|
-
return join(resolveReceiptIntegrityRoot(), rootHash,
|
|
249
|
+
return join(resolveReceiptIntegrityRoot(), rootHash, keyFileName);
|
|
250
|
+
}
|
|
251
|
+
export function createPrivateIntegrityHmac(key, material) {
|
|
252
|
+
return createHmac("sha256", key).update(JSON.stringify(material)).digest("hex");
|
|
240
253
|
}
|
|
241
254
|
function serializeStoredJson(value) {
|
|
242
255
|
return `${JSON.stringify(value, null, 2)}\n`;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
declare const SWARM_RECEIPT_INTEGRITY_SCHEMA_VERSION: "martin.swarm-receipt-integrity.v1";
|
|
2
|
+
export interface SwarmReceiptIntegrityArtifact {
|
|
3
|
+
path: string;
|
|
4
|
+
sha256: string;
|
|
5
|
+
bytes: number;
|
|
6
|
+
}
|
|
7
|
+
export interface StoredSwarmReceiptIntegrityMaterial {
|
|
8
|
+
schemaVersion: typeof SWARM_RECEIPT_INTEGRITY_SCHEMA_VERSION;
|
|
9
|
+
swarmId: string;
|
|
10
|
+
planHash: string;
|
|
11
|
+
receiptSchemaVersion: string;
|
|
12
|
+
receiptSha256: string;
|
|
13
|
+
evidenceIndexSha256: string;
|
|
14
|
+
eventChainHead: string;
|
|
15
|
+
rootBindingSha256: string;
|
|
16
|
+
keyId: string;
|
|
17
|
+
signedAt: string;
|
|
18
|
+
taskVerificationState: "passed" | "failed" | "unknown";
|
|
19
|
+
artifacts: SwarmReceiptIntegrityArtifact[];
|
|
20
|
+
signatureHmacSha256: string;
|
|
21
|
+
}
|
|
22
|
+
export interface SwarmReceiptIntegritySummary {
|
|
23
|
+
state: "verified" | "material_missing" | "tamper_detected";
|
|
24
|
+
taskVerificationState?: "passed" | "failed" | "unknown";
|
|
25
|
+
keyId?: string;
|
|
26
|
+
signedAt?: string;
|
|
27
|
+
receiptSha256?: string;
|
|
28
|
+
evidenceIndexSha256?: string;
|
|
29
|
+
eventChainHead?: string;
|
|
30
|
+
reason?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface SwarmReceiptSealCommitBase {
|
|
33
|
+
schemaVersion: "martin.swarm-receipt-seal.v1";
|
|
34
|
+
swarmId: string;
|
|
35
|
+
planHash: string;
|
|
36
|
+
receiptSha256: string;
|
|
37
|
+
evidenceIndexSha256: string;
|
|
38
|
+
integrityMaterialSha256: string;
|
|
39
|
+
committedAt: string;
|
|
40
|
+
}
|
|
41
|
+
export declare function authenticateSwarmReceiptSealCommit(input: {
|
|
42
|
+
runsRoot: string;
|
|
43
|
+
swarmId: string;
|
|
44
|
+
commit: SwarmReceiptSealCommitBase;
|
|
45
|
+
}): Promise<string>;
|
|
46
|
+
export declare function verifySwarmReceiptSealCommitAuthentication(input: {
|
|
47
|
+
runsRoot: string;
|
|
48
|
+
swarmId: string;
|
|
49
|
+
commit: SwarmReceiptSealCommitBase & {
|
|
50
|
+
commitHmacSha256?: string;
|
|
51
|
+
};
|
|
52
|
+
}): Promise<boolean>;
|
|
53
|
+
export declare function writeSwarmReceiptIntegrityMaterial(input: {
|
|
54
|
+
runsRoot: string;
|
|
55
|
+
swarmId: string;
|
|
56
|
+
signedAt?: string;
|
|
57
|
+
}): Promise<StoredSwarmReceiptIntegrityMaterial | undefined>;
|
|
58
|
+
export declare function verifySwarmReceiptIntegrityFromFiles(input: {
|
|
59
|
+
runsRoot: string;
|
|
60
|
+
swarmId: string;
|
|
61
|
+
}): Promise<SwarmReceiptIntegritySummary>;
|
|
62
|
+
export declare function resolveSwarmReceiptIntegrityPath(runsRoot: string, swarmId: string): string;
|
|
63
|
+
export {};
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { lstat, open, readFile, realpath } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { verifyParentSwarmReceipt } from "../swarm/parent-receipt.js";
|
|
5
|
+
import { assertSwarmPathIdentifier } from "../swarm/workspaces.js";
|
|
6
|
+
import { createPrivateIntegrityHmac, ensurePrivateIntegrityKey, readPrivateIntegrityKey } from "./integrity.js";
|
|
7
|
+
const SWARM_RECEIPT_INTEGRITY_SCHEMA_VERSION = "martin.swarm-receipt-integrity.v1";
|
|
8
|
+
export async function authenticateSwarmReceiptSealCommit(input) {
|
|
9
|
+
const runsRoot = await realpath(resolve(input.runsRoot));
|
|
10
|
+
const key = await readPrivateIntegrityKey(runsRoot, keyFileName(input.swarmId));
|
|
11
|
+
if (!key)
|
|
12
|
+
throw new Error("SWARM_SEAL_KEY_UNAVAILABLE");
|
|
13
|
+
return createPrivateIntegrityHmac(key, sealAuthenticationBase(runsRoot, input.commit));
|
|
14
|
+
}
|
|
15
|
+
export async function verifySwarmReceiptSealCommitAuthentication(input) {
|
|
16
|
+
const runsRoot = await realpath(resolve(input.runsRoot));
|
|
17
|
+
const key = await readPrivateIntegrityKey(runsRoot, keyFileName(input.swarmId));
|
|
18
|
+
if (!key || typeof input.commit.commitHmacSha256 !== "string")
|
|
19
|
+
return false;
|
|
20
|
+
const { commitHmacSha256, ...commit } = input.commit;
|
|
21
|
+
const expected = createPrivateIntegrityHmac(key, sealAuthenticationBase(runsRoot, commit));
|
|
22
|
+
return constantTimeHexEqual(commitHmacSha256, expected);
|
|
23
|
+
}
|
|
24
|
+
export async function writeSwarmReceiptIntegrityMaterial(input) {
|
|
25
|
+
const bundle = await resolveCanonicalSwarmIntegrityBundle(input.runsRoot, input.swarmId);
|
|
26
|
+
const receipt = parseParentReceipt(bundle.receiptRaw);
|
|
27
|
+
const evidenceIndex = parseEvidenceIndex(bundle.indexRaw);
|
|
28
|
+
validateArtifactIdentities(input.swarmId, receipt, evidenceIndex, bundle.indexRaw, bundle.eventsRaw);
|
|
29
|
+
const keyMaterial = await ensurePrivateIntegrityKey(bundle.runsRoot, keyFileName(input.swarmId));
|
|
30
|
+
if (!keyMaterial)
|
|
31
|
+
return undefined;
|
|
32
|
+
const [key, keyId] = keyMaterial;
|
|
33
|
+
const artifacts = canonicalArtifactManifest(bundle);
|
|
34
|
+
const base = {
|
|
35
|
+
schemaVersion: SWARM_RECEIPT_INTEGRITY_SCHEMA_VERSION,
|
|
36
|
+
swarmId: input.swarmId,
|
|
37
|
+
planHash: receipt.planHash,
|
|
38
|
+
receiptSchemaVersion: receipt.schemaVersion,
|
|
39
|
+
receiptSha256: receipt.receiptSha256,
|
|
40
|
+
evidenceIndexSha256: sha256(bundle.indexRaw),
|
|
41
|
+
eventChainHead: sha256(bundle.eventsRaw),
|
|
42
|
+
rootBindingSha256: sha256(bundle.runsRoot),
|
|
43
|
+
keyId,
|
|
44
|
+
signedAt: input.signedAt ?? new Date().toISOString(),
|
|
45
|
+
taskVerificationState: receipt.taskVerificationState,
|
|
46
|
+
artifacts
|
|
47
|
+
};
|
|
48
|
+
const material = {
|
|
49
|
+
...base,
|
|
50
|
+
signatureHmacSha256: createPrivateIntegrityHmac(key, base)
|
|
51
|
+
};
|
|
52
|
+
await writeExactOnce(bundle.integrityPath, serializeJson(material));
|
|
53
|
+
return material;
|
|
54
|
+
}
|
|
55
|
+
export async function verifySwarmReceiptIntegrityFromFiles(input) {
|
|
56
|
+
let bundle;
|
|
57
|
+
try {
|
|
58
|
+
bundle = await resolveCanonicalSwarmIntegrityBundle(input.runsRoot, input.swarmId);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return { state: "material_missing", reason: "canonical_swarm_bundle_missing" };
|
|
62
|
+
}
|
|
63
|
+
const [materialRaw, key] = await Promise.all([
|
|
64
|
+
readFile(bundle.integrityPath, "utf8").catch(() => null),
|
|
65
|
+
readPrivateIntegrityKey(bundle.runsRoot, keyFileName(input.swarmId)).catch(() => null)
|
|
66
|
+
]);
|
|
67
|
+
if (!materialRaw || !key) {
|
|
68
|
+
return { state: "material_missing", reason: "swarm_receipt_integrity_material_incomplete" };
|
|
69
|
+
}
|
|
70
|
+
let material;
|
|
71
|
+
let receipt;
|
|
72
|
+
let evidenceIndex;
|
|
73
|
+
try {
|
|
74
|
+
material = JSON.parse(materialRaw);
|
|
75
|
+
receipt = parseParentReceipt(bundle.receiptRaw);
|
|
76
|
+
evidenceIndex = parseEvidenceIndex(bundle.indexRaw);
|
|
77
|
+
validateArtifactIdentities(input.swarmId, receipt, evidenceIndex, bundle.indexRaw, bundle.eventsRaw);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return { state: "tamper_detected", reason: "malformed_integrity_or_artifact_material" };
|
|
81
|
+
}
|
|
82
|
+
const actualArtifacts = canonicalArtifactManifest(bundle);
|
|
83
|
+
if (!validMaterialShape(material) || !sameManifest(material.artifacts, actualArtifacts)) {
|
|
84
|
+
return tamper(material, "artifact_manifest_mismatch");
|
|
85
|
+
}
|
|
86
|
+
if (material.rootBindingSha256 !== sha256(bundle.runsRoot)) {
|
|
87
|
+
return tamper(material, "root_binding_mismatch");
|
|
88
|
+
}
|
|
89
|
+
if (material.swarmId !== input.swarmId
|
|
90
|
+
|| material.planHash !== receipt.planHash
|
|
91
|
+
|| material.receiptSchemaVersion !== receipt.schemaVersion
|
|
92
|
+
|| material.receiptSha256 !== receipt.receiptSha256
|
|
93
|
+
|| material.evidenceIndexSha256 !== sha256(bundle.indexRaw)
|
|
94
|
+
|| material.eventChainHead !== sha256(bundle.eventsRaw)
|
|
95
|
+
|| material.taskVerificationState !== receipt.taskVerificationState) {
|
|
96
|
+
return tamper(material, "canonical_binding_mismatch");
|
|
97
|
+
}
|
|
98
|
+
const { signatureHmacSha256, ...base } = material;
|
|
99
|
+
const expected = createPrivateIntegrityHmac(key, base);
|
|
100
|
+
if (!constantTimeHexEqual(signatureHmacSha256, expected)) {
|
|
101
|
+
return tamper(material, "signature_mismatch");
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
state: "verified",
|
|
105
|
+
taskVerificationState: receipt.taskVerificationState,
|
|
106
|
+
keyId: material.keyId,
|
|
107
|
+
signedAt: material.signedAt,
|
|
108
|
+
receiptSha256: material.receiptSha256,
|
|
109
|
+
evidenceIndexSha256: material.evidenceIndexSha256,
|
|
110
|
+
eventChainHead: material.eventChainHead
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export function resolveSwarmReceiptIntegrityPath(runsRoot, swarmId) {
|
|
114
|
+
assertSwarmPathIdentifier(swarmId, "swarm ID");
|
|
115
|
+
return join(runsRoot, "_swarms", swarmId, "evidence", "swarm-receipt-integrity.json");
|
|
116
|
+
}
|
|
117
|
+
async function resolveCanonicalSwarmIntegrityBundle(runsRootInput, swarmId) {
|
|
118
|
+
assertSwarmPathIdentifier(swarmId, "swarm ID");
|
|
119
|
+
const runsRoot = await realpath(resolve(runsRootInput));
|
|
120
|
+
const swarmsRoot = await resolveExactDirectory(runsRoot, join(runsRoot, "_swarms"));
|
|
121
|
+
assertContained(runsRoot, swarmsRoot);
|
|
122
|
+
const swarmRoot = await resolveExactDirectory(swarmsRoot, join(swarmsRoot, swarmId));
|
|
123
|
+
assertContained(swarmsRoot, swarmRoot);
|
|
124
|
+
const evidenceRoot = await resolveExactDirectory(swarmRoot, join(swarmRoot, "evidence"));
|
|
125
|
+
assertContained(swarmRoot, evidenceRoot);
|
|
126
|
+
const receiptPath = await resolveExactRegularFile(evidenceRoot, join(evidenceRoot, "parent-receipt.json"));
|
|
127
|
+
const indexPath = await resolveExactRegularFile(evidenceRoot, join(evidenceRoot, "evidence-index.json"));
|
|
128
|
+
const eventsPath = await resolveExactRegularFile(swarmRoot, join(swarmRoot, "events.jsonl"));
|
|
129
|
+
assertContained(evidenceRoot, receiptPath);
|
|
130
|
+
assertContained(evidenceRoot, indexPath);
|
|
131
|
+
assertContained(swarmRoot, eventsPath);
|
|
132
|
+
const [receiptRaw, indexRaw, eventsRaw] = await Promise.all([
|
|
133
|
+
readFile(receiptPath, "utf8"),
|
|
134
|
+
readFile(indexPath, "utf8"),
|
|
135
|
+
readFile(eventsPath, "utf8")
|
|
136
|
+
]);
|
|
137
|
+
return {
|
|
138
|
+
runsRoot,
|
|
139
|
+
swarmRoot,
|
|
140
|
+
evidenceRoot,
|
|
141
|
+
integrityPath: join(evidenceRoot, "swarm-receipt-integrity.json"),
|
|
142
|
+
receiptRaw,
|
|
143
|
+
indexRaw,
|
|
144
|
+
eventsRaw
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function canonicalArtifactManifest(bundle) {
|
|
148
|
+
return [
|
|
149
|
+
{ path: "evidence/evidence-index.json", sha256: sha256(bundle.indexRaw), bytes: Buffer.byteLength(bundle.indexRaw) },
|
|
150
|
+
{ path: "evidence/parent-receipt.json", sha256: sha256(bundle.receiptRaw), bytes: Buffer.byteLength(bundle.receiptRaw) }
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
function parseParentReceipt(raw) {
|
|
154
|
+
const receipt = JSON.parse(raw);
|
|
155
|
+
if (verifyParentSwarmReceipt(receipt).ok !== true) {
|
|
156
|
+
throw new Error("INVALID_PARENT_SWARM_RECEIPT");
|
|
157
|
+
}
|
|
158
|
+
return receipt;
|
|
159
|
+
}
|
|
160
|
+
function parseEvidenceIndex(raw) {
|
|
161
|
+
const value = JSON.parse(raw);
|
|
162
|
+
if (!isRecord(value) || value.schemaVersion !== "martin.swarm-evidence-index.v1") {
|
|
163
|
+
throw new Error("INVALID_SWARM_EVIDENCE_INDEX");
|
|
164
|
+
}
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
function validateArtifactIdentities(swarmId, receipt, evidenceIndex, indexRaw, eventsRaw) {
|
|
168
|
+
if (receipt.swarmId !== swarmId
|
|
169
|
+
|| evidenceIndex.swarmId !== swarmId
|
|
170
|
+
|| evidenceIndex.planHash !== receipt.planHash) {
|
|
171
|
+
throw new Error("SWARM_ARTIFACT_IDENTITY_MISMATCH");
|
|
172
|
+
}
|
|
173
|
+
if (receipt.evidenceIndexSha256 !== sha256(indexRaw)
|
|
174
|
+
|| stableJson(receipt.evidenceFiles) !== stableJson(evidenceIndex.files)
|
|
175
|
+
|| `${receipt.events.map((event) => JSON.stringify(event)).join("\n")}\n` !== eventsRaw) {
|
|
176
|
+
throw new Error("SWARM_ARTIFACT_RELATION_MISMATCH");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function resolveExactDirectory(parent, expected) {
|
|
180
|
+
const metadata = await lstat(expected);
|
|
181
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory())
|
|
182
|
+
throw new Error("SWARM_INTEGRITY_PATH_ALIAS");
|
|
183
|
+
const canonical = await realpath(expected);
|
|
184
|
+
assertContained(parent, canonical);
|
|
185
|
+
if (!sameFilesystemPath(canonical, resolve(expected)))
|
|
186
|
+
throw new Error("SWARM_INTEGRITY_PATH_ALIAS");
|
|
187
|
+
return canonical;
|
|
188
|
+
}
|
|
189
|
+
async function resolveExactRegularFile(parent, expected) {
|
|
190
|
+
const metadata = await lstat(expected);
|
|
191
|
+
if (metadata.isSymbolicLink() || !metadata.isFile())
|
|
192
|
+
throw new Error("SWARM_INTEGRITY_PATH_ALIAS");
|
|
193
|
+
const canonical = await realpath(expected);
|
|
194
|
+
assertContained(parent, canonical);
|
|
195
|
+
if (!sameFilesystemPath(canonical, resolve(expected)))
|
|
196
|
+
throw new Error("SWARM_INTEGRITY_PATH_ALIAS");
|
|
197
|
+
return canonical;
|
|
198
|
+
}
|
|
199
|
+
function sealAuthenticationBase(runsRoot, commit) {
|
|
200
|
+
return {
|
|
201
|
+
purpose: "martin.swarm-receipt-seal-commit.v1",
|
|
202
|
+
rootBindingSha256: sha256(runsRoot),
|
|
203
|
+
...commit
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function sameFilesystemPath(left, right) {
|
|
207
|
+
return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
208
|
+
}
|
|
209
|
+
function stableJson(value) {
|
|
210
|
+
return JSON.stringify(canonicalize(value));
|
|
211
|
+
}
|
|
212
|
+
function canonicalize(value) {
|
|
213
|
+
if (Array.isArray(value))
|
|
214
|
+
return value.map(canonicalize);
|
|
215
|
+
if (!isRecord(value))
|
|
216
|
+
return value;
|
|
217
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalize(value[key])]));
|
|
218
|
+
}
|
|
219
|
+
function validMaterialShape(value) {
|
|
220
|
+
return value.schemaVersion === SWARM_RECEIPT_INTEGRITY_SCHEMA_VERSION
|
|
221
|
+
&& typeof value.signatureHmacSha256 === "string"
|
|
222
|
+
&& typeof value.keyId === "string"
|
|
223
|
+
&& typeof value.signedAt === "string"
|
|
224
|
+
&& Array.isArray(value.artifacts);
|
|
225
|
+
}
|
|
226
|
+
function sameManifest(stored, actual) {
|
|
227
|
+
if (stored.length !== actual.length)
|
|
228
|
+
return false;
|
|
229
|
+
const seen = new Set();
|
|
230
|
+
return stored.every((entry, index) => {
|
|
231
|
+
if (entry.path.includes("..") || entry.path.includes("\\") || seen.has(entry.path))
|
|
232
|
+
return false;
|
|
233
|
+
seen.add(entry.path);
|
|
234
|
+
const expected = actual[index];
|
|
235
|
+
return expected !== undefined
|
|
236
|
+
&& entry.path === expected.path
|
|
237
|
+
&& entry.sha256 === expected.sha256
|
|
238
|
+
&& entry.bytes === expected.bytes;
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
function tamper(material, reason) {
|
|
242
|
+
return {
|
|
243
|
+
state: "tamper_detected",
|
|
244
|
+
...(typeof material.taskVerificationState === "string"
|
|
245
|
+
? { taskVerificationState: material.taskVerificationState }
|
|
246
|
+
: {}),
|
|
247
|
+
...(typeof material.keyId === "string" ? { keyId: material.keyId } : {}),
|
|
248
|
+
...(typeof material.signedAt === "string" ? { signedAt: material.signedAt } : {}),
|
|
249
|
+
reason
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function constantTimeHexEqual(left, right) {
|
|
253
|
+
if (!/^[a-f0-9]{64}$/u.test(left) || !/^[a-f0-9]{64}$/u.test(right))
|
|
254
|
+
return false;
|
|
255
|
+
const leftBytes = Buffer.from(left, "hex");
|
|
256
|
+
const rightBytes = Buffer.from(right, "hex");
|
|
257
|
+
return leftBytes.byteLength === rightBytes.byteLength && timingSafeEqual(leftBytes, rightBytes);
|
|
258
|
+
}
|
|
259
|
+
function keyFileName(swarmId) {
|
|
260
|
+
return `swarm-${swarmId}.key`;
|
|
261
|
+
}
|
|
262
|
+
function assertContained(parent, child) {
|
|
263
|
+
const value = relative(parent, child);
|
|
264
|
+
if (value === "" || (!value.startsWith(`..${sep}`) && value !== ".." && !isAbsolute(value)))
|
|
265
|
+
return;
|
|
266
|
+
throw new Error("SWARM_INTEGRITY_PATH_ESCAPE");
|
|
267
|
+
}
|
|
268
|
+
function serializeJson(value) {
|
|
269
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
270
|
+
}
|
|
271
|
+
async function writeExactOnce(path, contents) {
|
|
272
|
+
const existing = await lstat(path).catch((error) => {
|
|
273
|
+
if (isNodeError(error, "ENOENT"))
|
|
274
|
+
return undefined;
|
|
275
|
+
throw error;
|
|
276
|
+
});
|
|
277
|
+
if (existing?.isSymbolicLink() || (existing && !existing.isFile()))
|
|
278
|
+
throw new Error("UNSAFE_SWARM_INTEGRITY_PATH");
|
|
279
|
+
if (existing) {
|
|
280
|
+
if (await readFile(path, "utf8") === contents)
|
|
281
|
+
return;
|
|
282
|
+
throw new Error("SWARM_INTEGRITY_IDENTITY_CONFLICT");
|
|
283
|
+
}
|
|
284
|
+
let handle;
|
|
285
|
+
try {
|
|
286
|
+
handle = await open(path, "wx", 0o600);
|
|
287
|
+
await handle.writeFile(contents, "utf8");
|
|
288
|
+
await handle.sync();
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
if (!isNodeError(error, "EEXIST"))
|
|
292
|
+
throw error;
|
|
293
|
+
if (await readFile(path, "utf8") === contents)
|
|
294
|
+
return;
|
|
295
|
+
throw new Error("SWARM_INTEGRITY_IDENTITY_CONFLICT");
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
await handle?.close();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function isNodeError(error, code) {
|
|
302
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
303
|
+
}
|
|
304
|
+
function sha256(value) {
|
|
305
|
+
return createHash("sha256").update(value).digest("hex");
|
|
306
|
+
}
|
|
307
|
+
function isRecord(value) {
|
|
308
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
309
|
+
}
|
|
@@ -103,6 +103,9 @@ export interface BudgetPreflightInput {
|
|
|
103
103
|
promptCharCount: number;
|
|
104
104
|
attemptCount: number;
|
|
105
105
|
remainingBudgetUsd: number;
|
|
106
|
+
remainingTokens?: number;
|
|
107
|
+
/** Adapter-declared estimated floor required to admit a credible first turn. */
|
|
108
|
+
minimumViableTokens?: number;
|
|
106
109
|
perAttemptCapUsd?: number;
|
|
107
110
|
pricePerMTokenUsd?: number;
|
|
108
111
|
}
|
|
@@ -365,6 +365,8 @@ export function evaluateBudgetPreflight(input) {
|
|
|
365
365
|
const estimatedPromptTokens = Math.ceil(rawPromptTokens * 1.2);
|
|
366
366
|
const estimatedToolOverheadTokens = 800 + input.attemptCount * 200;
|
|
367
367
|
const estimatedOutputTokensMax = 4_000;
|
|
368
|
+
const estimatedMinimumViableTokens = Math.max(0, input.minimumViableTokens ?? 0);
|
|
369
|
+
const estimatedTotalTokens = Math.max(estimatedPromptTokens + estimatedToolOverheadTokens + estimatedOutputTokensMax, estimatedMinimumViableTokens);
|
|
368
370
|
const estimatedVerifierCostUsd = 0.01;
|
|
369
371
|
const estimatedAttemptCostUsd = roundUsd(((estimatedPromptTokens + estimatedToolOverheadTokens + estimatedOutputTokensMax) /
|
|
370
372
|
1_000_000) *
|
|
@@ -374,10 +376,21 @@ export function evaluateBudgetPreflight(input) {
|
|
|
374
376
|
estimatedPromptTokens,
|
|
375
377
|
estimatedToolOverheadTokens,
|
|
376
378
|
estimatedOutputTokensMax,
|
|
379
|
+
estimatedMinimumViableTokens,
|
|
380
|
+
estimatedTotalTokens,
|
|
377
381
|
estimatedVerifierCostUsd,
|
|
378
382
|
estimatedAttemptCostUsd,
|
|
379
383
|
provenance
|
|
380
384
|
};
|
|
385
|
+
if (input.remainingTokens !== undefined
|
|
386
|
+
&& estimatedMinimumViableTokens > 0
|
|
387
|
+
&& estimatedTotalTokens > input.remainingTokens) {
|
|
388
|
+
return {
|
|
389
|
+
allowed: false,
|
|
390
|
+
reason: `Preflight: ${String(estimatedTotalTokens)} estimated tokens exceed ${String(input.remainingTokens)} remaining tokens.`,
|
|
391
|
+
estimate
|
|
392
|
+
};
|
|
393
|
+
}
|
|
381
394
|
if (estimatedAttemptCostUsd > input.remainingBudgetUsd) {
|
|
382
395
|
return {
|
|
383
396
|
allowed: false,
|