@entrorove/skill 0.1.23 → 0.1.24
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/AGREEMENT.md +6 -6
- package/CHANGELOG.md +5 -0
- package/SKILL.md +12 -4
- package/archguard-runtime-build.mjs +15 -0
- package/blueprint-criterion-runner.mjs +110 -0
- package/blueprint-criterion-wire.mjs +78 -0
- package/calctool-runtime-build.mjs +59 -0
- package/calctool-runtime-entry.mjs +92 -0
- package/cli.mjs +17 -82
- package/local-approved-plan.mjs +74 -15
- package/local-archguard-ast.mjs +371 -0
- package/local-archguard-checkpoint.mjs +123 -0
- package/local-archguard-components.mjs +177 -0
- package/local-archguard-contract.mjs +109 -0
- package/local-archguard-contracts.mjs +42 -0
- package/local-archguard-evaluate.mjs +120 -0
- package/local-archguard-language-ast.mjs +165 -0
- package/local-archguard-mutation-fence.mjs +28 -0
- package/local-archguard-operations.mjs +25 -0
- package/local-archguard-scope.mjs +64 -0
- package/local-archguard-signing.mjs +52 -0
- package/local-archguard-snapshot.mjs +126 -0
- package/local-archguard-state.mjs +200 -0
- package/local-archguard.mjs +74 -0
- package/local-budget-git-attributes.mjs +49 -0
- package/local-budget-git-loader.mjs +61 -0
- package/local-budget-git-native-attributes.mjs +81 -0
- package/local-budget-git-native-darwin.mjs +104 -0
- package/local-budget-git-native-linux.mjs +93 -0
- package/local-budget-git-native-reads.mjs +88 -0
- package/local-budget-git-native-request.mjs +53 -0
- package/local-budget-git-native-runtime.mjs +18 -0
- package/local-budget-git-native.c +313 -0
- package/local-budget-git-native.mjs +109 -0
- package/local-budget-git-outcome.mjs +33 -0
- package/local-budget-git-scratch-loader.mjs +158 -0
- package/local-budget-git-scratch.mjs +112 -0
- package/local-budget-git-system-attributes.mjs +44 -0
- package/local-budget-git.mjs +92 -0
- package/local-budget-grants.mjs +82 -0
- package/local-budget-handoff-completion.mjs +70 -0
- package/local-budget-handoff-contracts.mjs +53 -0
- package/local-budget-handoff.mjs +229 -0
- package/local-budget-lock.mjs +55 -0
- package/local-budget-reads.mjs +135 -0
- package/local-budget-renewal.mjs +86 -34
- package/local-budget-source-bindings.mjs +51 -0
- package/local-budget-store.mjs +131 -0
- package/local-budget-transition.mjs +46 -0
- package/local-budget.mjs +185 -0
- package/local-calctool-cli.mjs +22 -0
- package/local-calctool-contracts.mjs +42 -0
- package/local-calctool-runtime.mjs +10 -0
- package/local-calctool-storage.mjs +101 -0
- package/local-calctool.mjs +103 -0
- package/local-cli.mjs +49 -16
- package/local-contract-types.mjs +46 -0
- package/local-contracts.mjs +124 -0
- package/local-credential.mjs +101 -0
- package/local-decimal.mjs +85 -0
- package/local-evidence-verification.mjs +199 -0
- package/local-execution.mjs +115 -304
- package/local-final-gate16-bundle.mjs +21 -0
- package/local-final-gate16-checks.mjs +88 -0
- package/local-final-gate16-contract.mjs +15 -0
- package/local-final-gate16-plan.mjs +66 -0
- package/local-final-gate16-proof.mjs +75 -0
- package/local-final-gate16-release.mjs +17 -0
- package/local-final-gate16-state.mjs +70 -0
- package/local-final-gate16-worker.mjs +19 -0
- package/local-final-gate16.mjs +137 -0
- package/local-formula-ast.mjs +97 -0
- package/local-formula-engine.mjs +111 -0
- package/local-formula-tests.mjs +61 -0
- package/local-formula.mjs +25 -125
- package/local-integrity.mjs +22 -41
- package/local-invocations.mjs +107 -0
- package/local-ledger.mjs +2 -7
- package/local-merge-archguard-verification.d.mts +44 -0
- package/local-merge-archguard-verification.mjs +240 -0
- package/local-merge-archguard.mjs +138 -0
- package/local-merge-artifacts.mjs +86 -0
- package/local-merge-authority.mjs +89 -0
- package/local-merge-contracts.mjs +56 -0
- package/local-merge-controller.mjs +170 -0
- package/local-merge-head.mjs +56 -0
- package/local-merge-journal.mjs +77 -0
- package/local-merge-mutation.mjs +97 -0
- package/local-merge-rollback.mjs +126 -0
- package/local-merge-snapshot-rollback.mjs +26 -0
- package/local-merge-storage.mjs +103 -0
- package/local-merge-targets.mjs +47 -0
- package/local-merge-transaction.mjs +133 -0
- package/local-mutation-audit.mjs +34 -0
- package/local-mutation-authority.mjs +96 -0
- package/local-mutation-batch-receipt.mjs +84 -0
- package/local-mutation-batch.mjs +143 -0
- package/local-mutation-binding.mjs +86 -0
- package/local-mutation-files.mjs +245 -0
- package/local-mutation-pass-store.mjs +63 -0
- package/local-mutation-snapshot.mjs +200 -0
- package/local-mutation-wire.mjs +89 -0
- package/local-probe.mjs +133 -11
- package/local-scope-candidate.mjs +28 -0
- package/local-scope-context.mjs +28 -0
- package/local-scope-files.mjs +106 -0
- package/local-scope-git-attributes.mjs +200 -0
- package/local-scope-git-comparison.mjs +8 -0
- package/local-scope-git-history.mjs +72 -0
- package/local-scope-git-reads.mjs +66 -0
- package/local-scope-git.mjs +124 -0
- package/local-scope-repository.mjs +249 -0
- package/local-scope-signature.mjs +70 -0
- package/local-scope-snapshot.mjs +39 -0
- package/local-scope-state.mjs +204 -0
- package/local-scope-workspaces.mjs +83 -0
- package/local-swarm-authority.mjs +143 -0
- package/local-swarm-baseline.mjs +32 -0
- package/local-swarm-completion.mjs +102 -0
- package/local-swarm-contracts.mjs +47 -0
- package/local-swarm-mutation.mjs +142 -0
- package/local-swarm-priority.mjs +237 -0
- package/local-swarm-state.mjs +102 -0
- package/local-swarm-storage.mjs +135 -0
- package/local-swarm.mjs +179 -0
- package/local-task-handoff-completion-wire.mjs +71 -0
- package/local-task-handoff-contracts.mjs +66 -0
- package/local-task-handoff-fence.mjs +478 -0
- package/local-task-handoff-observation.mjs +38 -0
- package/local-task-handoff-overlay-wire.mjs +94 -0
- package/local-task-handoff-read-authority.mjs +236 -0
- package/local-task-handoff-storage.mjs +73 -0
- package/local-task-handoff-write-archive.mjs +80 -0
- package/local-task-handoff-write-constraints.mjs +66 -0
- package/local-task-handoff-write-receipts.mjs +99 -0
- package/local-task-handoff-write-wire.mjs +99 -0
- package/local-task-routing.mjs +202 -22
- package/manifest.json +1 -1
- package/package.json +153 -3
- package/release-state.json +153 -17
- package/release.mjs +238 -4
- package/releases.json +6 -0
- package/remote-client.mjs +127 -36
- package/remote-transport.mjs +80 -0
- package/review-client.mjs +38 -0
- package/runtime/archguard-runtime-v7.0.44.mjs +314 -0
- package/runtime/calctool-runtime-v7.0.43.mjs +1887 -0
- package/runtime/git-native-darwin-v1.mjs +5 -0
- package/runtime/git-native-linux-v1.mjs +5 -0
- package/task-message-cli.mjs +47 -0
- package/task-message-entry.mjs +159 -0
- package/workflow-local-client.mjs +35 -0
- package/workflow-local-contracts.mjs +58 -0
- package/workflow-local-request.mjs +53 -0
- package/workflow-local-runner.mjs +350 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { fixedNativeScratchComparison } from './local-budget-git-native.mjs';
|
|
2
|
+
import { constants, promises as fs } from 'node:fs';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { fail, requireLiveBudget } from './local-budget-grants.mjs';
|
|
5
|
+
import { withBudget, loadBudget, commitBudget } from './local-budget-store.mjs';
|
|
6
|
+
import { assertDependencyReadAllowed, assertBudgetReadContext, copyBudgetedProjectBuffer } from './local-budget-reads.mjs';
|
|
7
|
+
import { consumeBudgetRenewal } from './local-budget-renewal.mjs';
|
|
8
|
+
import { gitReadBinding, gitReadDeadline, validateGitRead, chargedGitRead } from './local-budget-git.mjs';
|
|
9
|
+
import { createGitScratch, assertGitScratch, fixedScratchComparison, scratchNumstat, verifyScratchBytes } from './local-budget-git-scratch-loader.mjs';
|
|
10
|
+
|
|
11
|
+
function renewedState(state) {
|
|
12
|
+
if (state.grant?.purpose === 'handoff-read') { requireLiveBudget(state); return state; }
|
|
13
|
+
return consumeBudgetRenewal({ state });
|
|
14
|
+
}
|
|
15
|
+
async function scratchSourceStream(descriptor) {
|
|
16
|
+
await assertGitScratch(descriptor);
|
|
17
|
+
if (descriptor.mode === '120000') return { chunks: [Buffer.from(await fs.readlink(descriptor.target))], close: async () => {} };
|
|
18
|
+
const handle = await fs.open(descriptor.target, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
19
|
+
const stat = await handle.stat({ bigint: true });
|
|
20
|
+
if (stat.dev !== descriptor.stat.dev || stat.ino !== descriptor.stat.ino || !stat.isFile()) { await handle.close(); fail('ENTROROVE_GIT_SOURCE_INVALID', 'Scratch descriptor lost its bound source inode'); }
|
|
21
|
+
return { chunks: handle.createReadStream({ autoClose: false }), close: () => handle.close() };
|
|
22
|
+
}
|
|
23
|
+
async function receiveScratch(input, state, descriptor, received) {
|
|
24
|
+
await validateGitRead(input, state, descriptor, descriptor.bytes);
|
|
25
|
+
const source = await scratchSourceStream(descriptor), chunks = [];
|
|
26
|
+
try {
|
|
27
|
+
for await (const chunk of source.chunks) {
|
|
28
|
+
received.bytes += chunk.length; chunks.push(chunk);
|
|
29
|
+
await validateGitRead(input, state, descriptor, received.bytes);
|
|
30
|
+
if (received.bytes > descriptor.bytes) fail('ENTROROVE_GIT_SOURCE_INVALID', 'actual scratch bytes exceed the original source binding');
|
|
31
|
+
}
|
|
32
|
+
received.complete = true; verifyScratchBytes(descriptor, Buffer.concat(chunks, received.bytes));
|
|
33
|
+
await assertGitScratch(descriptor); await validateGitRead(input, state, descriptor, received.bytes);
|
|
34
|
+
} finally { await source.close(); }
|
|
35
|
+
}
|
|
36
|
+
async function readScratch(input, descriptor) {
|
|
37
|
+
if (!input.flowId) return receiveScratch(input, null, descriptor, { bytes: 0 });
|
|
38
|
+
return withBudget(input, async (location, loaded) => {
|
|
39
|
+
const received = { bytes: 0, complete: false }, readId = randomUUID(); let state = loaded.state, committed = false;
|
|
40
|
+
try {
|
|
41
|
+
await assertDependencyReadAllowed(input); state = renewedState(state);
|
|
42
|
+
await receiveScratch(input, state, descriptor, received);
|
|
43
|
+
state = await commitBudget(location, loaded, chargedGitRead(state, descriptor, received.bytes), 'budget-read',
|
|
44
|
+
{ path: descriptor.path, bytes: received.bytes, tokens: Math.ceil(received.bytes / 4), source: evidence(descriptor), readId });
|
|
45
|
+
committed = true; await validateGitRead(input, state, descriptor); await assertBudgetReadContext(input);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
let current = await loadBudget(location);
|
|
48
|
+
if (current.latest.data.readId === readId) committed = true;
|
|
49
|
+
if (!committed && (received.bytes || received.complete)) {
|
|
50
|
+
await commitBudget(location, current, chargedGitRead(current.state, descriptor, received.bytes), 'budget-read',
|
|
51
|
+
{ path: descriptor.path, bytes: received.bytes, tokens: Math.ceil(received.bytes / 4), source: evidence(descriptor), readId, readIncomplete: true });
|
|
52
|
+
}
|
|
53
|
+
current = await loadBudget(location);
|
|
54
|
+
if (error.code?.startsWith('ENTROROVE_') && error.code !== 'ENTROROVE_BUDGET_CHAIN_INVALID') {
|
|
55
|
+
await commitBudget(location, current, current.state, 'budget-read-denied', { path: descriptor.path, sourceRole: descriptor.role, code: error.code });
|
|
56
|
+
}
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function evidence(descriptor) {
|
|
62
|
+
const { target, stat, ...source } = descriptor; return source;
|
|
63
|
+
}
|
|
64
|
+
async function validateComparison(input, state, session) {
|
|
65
|
+
await assertBudgetReadContext(input);
|
|
66
|
+
if (state) await assertDependencyReadAllowed(input);
|
|
67
|
+
for (const descriptor of session.descriptors) {
|
|
68
|
+
await assertGitScratch(descriptor); await validateGitRead(input, state, descriptor);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function fixedComparison(input, state, session) {
|
|
72
|
+
await validateComparison(input, state, session);
|
|
73
|
+
const runner = fixedScratchComparison(session, gitReadDeadline(state)); let done = false;
|
|
74
|
+
const result = runner.done.then(value => { done = true; return value; });
|
|
75
|
+
try {
|
|
76
|
+
while (!done) {
|
|
77
|
+
await Promise.race([result, new Promise(resolve => setTimeout(resolve, 25))]);
|
|
78
|
+
await validateComparison(input, state, session);
|
|
79
|
+
}
|
|
80
|
+
const output = await result;
|
|
81
|
+
if (!output.ok) fail('ENTROROVE_GIT_FAILED', 'fixed sealed source comparison failed or exceeded its execution deadline');
|
|
82
|
+
await validateComparison(input, state, session); return scratchNumstat(output.output);
|
|
83
|
+
} finally { runner.close(); await result; }
|
|
84
|
+
}
|
|
85
|
+
async function preflight(input, before, after) {
|
|
86
|
+
if (!input.flowId) return;
|
|
87
|
+
await withBudget(input, async (_location, loaded) => {
|
|
88
|
+
await assertDependencyReadAllowed(input); const state = renewedState(loaded.state);
|
|
89
|
+
for (const entry of [...before, ...after]) await validateGitRead(input, state, { path: entry.path, role: 'git-comparison' });
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/** Only fixed numstat over adapter-created sealed scratch inputs; no caller command or callback. */
|
|
93
|
+
export async function compareBudgetedGitSources(input, before, after, options) {
|
|
94
|
+
const binding = await gitReadBinding(input);
|
|
95
|
+
if (!Array.isArray(before) || !Array.isArray(after)) fail('ENTROROVE_GIT_SOURCE_INVALID', 'fixed Git comparison requires both source arrays');
|
|
96
|
+
const copy = entries => entries.map(entry => {
|
|
97
|
+
if (!entry || !Buffer.isBuffer(entry.content)) fail('ENTROROVE_GIT_SOURCE_INVALID', 'Git comparison requires actual immutable source buffers');
|
|
98
|
+
return { ...entry, content: copyBudgetedProjectBuffer(entry.content) };
|
|
99
|
+
});
|
|
100
|
+
before = copy(before); after = copy(after);
|
|
101
|
+
await preflight(binding.input, before, after);
|
|
102
|
+
const session = await createGitScratch(binding.input, before, after, options);
|
|
103
|
+
try {
|
|
104
|
+
// Captured scope metadata permits verification of each actual native chunk against its private original buffer.
|
|
105
|
+
// Standalone comparisons retain their original full-source preflight before starting the fixed process.
|
|
106
|
+
if (!binding.input.flowId || !session.capturedAttributes) {
|
|
107
|
+
for (const descriptor of session.descriptors) await readScratch(binding.input, descriptor);
|
|
108
|
+
}
|
|
109
|
+
if (!binding.input.flowId) return fixedComparison(binding.input, null, session);
|
|
110
|
+
return await fixedNativeScratchComparison(binding.input, session);
|
|
111
|
+
} finally { await fs.rm(session.directory, { recursive: true, force: true }); }
|
|
112
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { constants, promises as fs } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { fail } from './local-budget-grants.mjs';
|
|
4
|
+
const target = '/usr/bin/git', maximumBytes = 33_554_432;
|
|
5
|
+
const hash = data => createHash('sha256').update(data).digest('hex');
|
|
6
|
+
const unsupported = () => fail('ENTROROVE_GIT_ATTRIBUTES_UNSUPPORTED', 'old fixed Git must expose one actual immutable compiled system attribute path');
|
|
7
|
+
function same(a, b) {
|
|
8
|
+
return ['dev', 'ino', 'mode', 'uid', 'size', 'mtimeNs', 'ctimeNs'].every(key => a[key] === b[key]);
|
|
9
|
+
}
|
|
10
|
+
async function trustedStat() {
|
|
11
|
+
for (const directory of ['/usr', '/usr/bin']) {
|
|
12
|
+
const stat = await fs.lstat(directory);
|
|
13
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== 0 || stat.mode & 0o7022 || await fs.realpath(directory) !== directory) unsupported();
|
|
14
|
+
}
|
|
15
|
+
const stat = await fs.lstat(target, { bigint: true });
|
|
16
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== 0n || stat.mode & 0o7022n
|
|
17
|
+
|| stat.size < 1n || stat.size > BigInt(maximumBytes) || await fs.realpath(target) !== target) unsupported();
|
|
18
|
+
return stat;
|
|
19
|
+
}
|
|
20
|
+
/** Only immutable binary metadata from the original fixed system Git, never project source bytes. */
|
|
21
|
+
export async function compiledGitSystemAttributes() {
|
|
22
|
+
const before = await trustedStat(), handle = await fs.open(target, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
23
|
+
try {
|
|
24
|
+
if (!same(before, await handle.stat({ bigint: true }))) unsupported();
|
|
25
|
+
const first = await handle.readFile();
|
|
26
|
+
if (first.length !== Number(before.size) || !same(before, await handle.stat({ bigint: true }))) unsupported();
|
|
27
|
+
const second = Buffer.alloc(first.length); let count = 0;
|
|
28
|
+
while (count < second.length) {
|
|
29
|
+
const { bytesRead } = await handle.read(second, count, second.length - count, count);
|
|
30
|
+
if (!bytesRead) unsupported(); count += bytesRead;
|
|
31
|
+
}
|
|
32
|
+
if (hash(first) !== hash(second) || !same(before, await handle.stat({ bigint: true })) || !same(before, await trustedStat())) unsupported();
|
|
33
|
+
const paths = new Set();
|
|
34
|
+
for (const raw of first.toString('latin1').split('\0').slice(0, -1)) {
|
|
35
|
+
if (!raw.startsWith('/') || !raw.endsWith('/gitattributes')) continue;
|
|
36
|
+
let candidate;
|
|
37
|
+
try { candidate = new TextDecoder('utf-8', { fatal: true }).decode(Buffer.from(raw, 'latin1')); } catch { unsupported(); }
|
|
38
|
+
if (raw.length > 4096 || /[\x00-\x1f\x7f]/.test(candidate) || candidate.split('/').some(part => part === '.' || part === '..')) unsupported();
|
|
39
|
+
paths.add(candidate);
|
|
40
|
+
}
|
|
41
|
+
if (paths.size !== 1) unsupported();
|
|
42
|
+
return [...paths][0];
|
|
43
|
+
} finally { await handle.close(); }
|
|
44
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { fail, relativePath, requireLiveBudget } from './local-budget-grants.mjs';
|
|
4
|
+
import { assertBudgetSourceReadAllowed } from './local-budget.mjs';
|
|
5
|
+
import { budgetBindingForRead, assertBudgetReadContext, assertDependencyReadAllowed } from './local-budget-reads.mjs';
|
|
6
|
+
import { budgetLocation, loadBudget, withBudget, commitBudget } from './local-budget-store.mjs';
|
|
7
|
+
import { consumeBudgetRenewal } from './local-budget-renewal.mjs';
|
|
8
|
+
import { assertBudgetHandoffReadAllowed } from './local-budget-handoff.mjs';
|
|
9
|
+
import { fixedGitBlobStream, gitSourceDescriptor, verifyFixedGitBlob, verifyGitBlobBytes } from './local-budget-git-loader.mjs';
|
|
10
|
+
|
|
11
|
+
export const BUDGET_GIT_BLOB_READER = Object.freeze({ kind: 'fixed-git-blob' });
|
|
12
|
+
export function gitReadDeadline(state) {
|
|
13
|
+
if (!state?.enforced) return Date.now() + 10000;
|
|
14
|
+
return Math.min(Date.now() + 10000, Date.parse(state.startedAt) + state.maxDurationMs,
|
|
15
|
+
Date.parse(state.grant.expiresAt));
|
|
16
|
+
}
|
|
17
|
+
export async function gitReadBinding(input) {
|
|
18
|
+
if (!input || typeof input.repositoryRoot !== 'string') fail('ENTROROVE_ROOT_INVALID', 'fixed Git reading requires its original repository input');
|
|
19
|
+
await assertBudgetReadContext(input);
|
|
20
|
+
const root = await fs.realpath(input.repositoryRoot), stat = await fs.lstat(input.repositoryRoot);
|
|
21
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) fail('ENTROROVE_ROOT_INVALID', 'Git reading requires its original real repository directory');
|
|
22
|
+
const selected = await budgetBindingForRead(input);
|
|
23
|
+
if (!selected) return { root, input: Object.freeze({ repositoryRoot: root }) };
|
|
24
|
+
const location = await budgetLocation(selected);
|
|
25
|
+
return { root, input: Object.freeze({ repositoryRoot: location.root, home: location.home, flowId: location.flowId }) };
|
|
26
|
+
}
|
|
27
|
+
export async function validateGitRead(input, state, descriptor, bytes = 0) {
|
|
28
|
+
relativePath(descriptor.path);
|
|
29
|
+
if (!state) return;
|
|
30
|
+
requireLiveBudget(state);
|
|
31
|
+
assertBudgetSourceReadAllowed(state, descriptor.path, Math.ceil(bytes / 4));
|
|
32
|
+
await assertBudgetHandoffReadAllowed({ ...input, path: descriptor.path, sourceRole: descriptor.role }, state);
|
|
33
|
+
}
|
|
34
|
+
async function receiveBlob(input, state, descriptor, received) {
|
|
35
|
+
await validateGitRead(input, state, descriptor, descriptor.bytes);
|
|
36
|
+
const expires = gitReadDeadline(state);
|
|
37
|
+
await verifyFixedGitBlob(input.repositoryRoot, descriptor, expires);
|
|
38
|
+
await validateGitRead(input, state, descriptor, descriptor.bytes);
|
|
39
|
+
const stream = fixedGitBlobStream(input.repositoryRoot, descriptor, expires), chunks = [];
|
|
40
|
+
try {
|
|
41
|
+
for await (const chunk of stream.chunks) {
|
|
42
|
+
received.bytes += chunk.length; chunks.push(chunk);
|
|
43
|
+
await validateGitRead(input, state, descriptor, received.bytes);
|
|
44
|
+
if (received.bytes > descriptor.bytes) fail('ENTROROVE_GIT_SOURCE_INVALID', 'Git source exceeded its exact authorized immutable size');
|
|
45
|
+
}
|
|
46
|
+
await validateGitRead(input, state, descriptor, received.bytes);
|
|
47
|
+
if (!await stream.done) fail('ENTROROVE_GIT_FAILED', 'fixed Git blob source reading failed or exceeded its deadline');
|
|
48
|
+
received.complete = true;
|
|
49
|
+
const content = Buffer.concat(chunks, received.bytes);
|
|
50
|
+
verifyGitBlobBytes(descriptor, content);
|
|
51
|
+
await verifyFixedGitBlob(input.repositoryRoot, descriptor, gitReadDeadline(state));
|
|
52
|
+
await validateGitRead(input, state, descriptor, received.bytes);
|
|
53
|
+
return content;
|
|
54
|
+
} finally { stream.close(); await stream.done; }
|
|
55
|
+
}
|
|
56
|
+
export function chargedGitRead(state, descriptor, bytes) {
|
|
57
|
+
return { ...state, files: state.files.includes(descriptor.path) ? state.files : [...state.files, descriptor.path],
|
|
58
|
+
readCalls: state.readCalls + 1, tokenEstimate: state.tokenEstimate + Math.ceil(bytes / 4) };
|
|
59
|
+
}
|
|
60
|
+
export async function readBudgetedGitBlob(input, descriptor, reader = BUDGET_GIT_BLOB_READER) {
|
|
61
|
+
if (reader !== BUDGET_GIT_BLOB_READER) fail('ENTROROVE_GIT_READER_UNSUPPORTED', 'Git source reading accepts only the fixed opaque blob reader identity');
|
|
62
|
+
descriptor = gitSourceDescriptor(descriptor); relativePath(descriptor.path);
|
|
63
|
+
const binding = await gitReadBinding(input);
|
|
64
|
+
if (!binding.input.flowId) return receiveBlob(binding.input, null, descriptor, { bytes: 0 });
|
|
65
|
+
return withBudget(binding.input, async (location, loaded) => {
|
|
66
|
+
const received = { bytes: 0, complete: false }, readId = randomUUID(); let state = loaded.state, committed = false;
|
|
67
|
+
try {
|
|
68
|
+
await assertDependencyReadAllowed(binding.input);
|
|
69
|
+
if (state.grant?.purpose === 'handoff-read') requireLiveBudget(state);
|
|
70
|
+
else state = consumeBudgetRenewal({ state });
|
|
71
|
+
const content = await receiveBlob(binding.input, state, descriptor, received);
|
|
72
|
+
state = await commitBudget(location, loaded, chargedGitRead(state, descriptor, received.bytes), 'budget-read',
|
|
73
|
+
{ path: descriptor.path, bytes: received.bytes, tokens: Math.ceil(received.bytes / 4), source: descriptor, readId });
|
|
74
|
+
committed = true; await validateGitRead(binding.input, state, descriptor);
|
|
75
|
+
await assertBudgetReadContext(binding.input); return content;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
let current = await loadBudget(location);
|
|
78
|
+
if (current.latest.data.readId === readId) { state = current.state; committed = true; }
|
|
79
|
+
if (!committed && (received.bytes || received.complete)) {
|
|
80
|
+
state = await commitBudget(location, current, chargedGitRead(state, descriptor, received.bytes), 'budget-read',
|
|
81
|
+
{ path: descriptor.path, bytes: received.bytes, tokens: Math.ceil(received.bytes / 4), source: descriptor, readId, readIncomplete: true });
|
|
82
|
+
committed = true;
|
|
83
|
+
}
|
|
84
|
+
if (error.code?.startsWith('ENTROROVE_') && error.code !== 'ENTROROVE_BUDGET_CHAIN_INVALID') {
|
|
85
|
+
current = await loadBudget(location);
|
|
86
|
+
await commitBudget(location, current, current.state, 'budget-read-denied',
|
|
87
|
+
{ path: descriptor.path, sourceRole: descriptor.role, code: error.code });
|
|
88
|
+
}
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createHash, createPublicKey, verify } from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const BUDGET_SCHEMA = 'entrorove.read-budget/2.0';
|
|
5
|
+
export const GRANT_SCHEMA = 'entrorove.budget-grant/1.0';
|
|
6
|
+
export const READ_BUDGETS = Object.freeze({
|
|
7
|
+
lock: Object.freeze({ maxFiles: 3, maxTokenEstimate: 12000, maxDurationMs: 120000 }),
|
|
8
|
+
probe: Object.freeze({ maxFiles: 10, maxTokenEstimate: 30000, maxDurationMs: 480000 }),
|
|
9
|
+
swarm: Object.freeze({ maxFiles: 30, maxTokenEstimate: 100000, maxDurationMs: 3600000 }),
|
|
10
|
+
});
|
|
11
|
+
export function fail(code, message) { throw Object.assign(new Error(message), { code }); }
|
|
12
|
+
export function canonical(value) {
|
|
13
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
14
|
+
if (value && typeof value === 'object') return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
|
|
15
|
+
return JSON.stringify(value);
|
|
16
|
+
}
|
|
17
|
+
export const sha = value => createHash('sha256').update(value).digest('hex');
|
|
18
|
+
export function identifier(value, field) {
|
|
19
|
+
if (typeof value !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value)) fail('ENTROROVE_ID_INVALID', `${field} must be a safe identifier`);
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
export function relativePath(value) {
|
|
23
|
+
if (typeof value !== 'string' || !value || value !== value.normalize('NFC') || path.isAbsolute(value)
|
|
24
|
+
|| /^[A-Za-z]:/.test(value) || /[\\\u0000-\u001f\u007f]/.test(value)
|
|
25
|
+
|| value.split('/').some(part => !part || part === '.' || part === '..')) fail('ENTROROVE_PATH_UNSAFE', 'budget source path is unsafe');
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
export function normalizeScope(scope) {
|
|
29
|
+
if (!scope || typeof scope.goal !== 'string' || !scope.goal.trim() || !Array.isArray(scope.allowedPaths) || !scope.allowedPaths.length) fail('ENTROROVE_RENEWAL_SCOPE', 'scope must contain the original goal and allowed paths');
|
|
30
|
+
const allowedPaths = [...new Set(scope.allowedPaths.map(relativePath))].sort();
|
|
31
|
+
if (scope.forbiddenPaths !== undefined && !Array.isArray(scope.forbiddenPaths)) fail('ENTROROVE_BUDGET_SCOPE', 'forbidden source paths must be an array');
|
|
32
|
+
const forbiddenPaths = [...new Set((scope.forbiddenPaths ?? []).map(relativePath))].sort();
|
|
33
|
+
return { goal: scope.goal, allowedPaths, ...(forbiddenPaths.length ? { forbiddenPaths } : {}) };
|
|
34
|
+
}
|
|
35
|
+
export function validatePolicy(policy) {
|
|
36
|
+
if (!policy || !Number.isSafeInteger(policy.intervalMs) || policy.intervalMs < 1 || !Number.isSafeInteger(policy.maxRenewals) || policy.maxRenewals < 1 || policy.maxRenewals > 100
|
|
37
|
+
|| !Number.isSafeInteger(policy.intervalMs * policy.maxRenewals)) fail('ENTROROVE_RENEWAL_POLICY', 'renewal intervals and cap must be bounded safe integers');
|
|
38
|
+
return { intervalMs: policy.intervalMs, maxRenewals: policy.maxRenewals };
|
|
39
|
+
}
|
|
40
|
+
export function validateAdditions(additions) {
|
|
41
|
+
if (!additions || ['files', 'tokenEstimate', 'durationMs'].some(key => !Number.isSafeInteger(additions[key]) || additions[key] < 0)
|
|
42
|
+
|| !Object.values(additions).some(value => value > 0)) fail('ENTROROVE_EXTENSION_INVALID', 'budget additions must be nonnegative safe integers with a positive increase');
|
|
43
|
+
return { files: additions.files, tokenEstimate: additions.tokenEstimate, durationMs: additions.durationMs };
|
|
44
|
+
}
|
|
45
|
+
function configuredKey(grant) {
|
|
46
|
+
let keys;
|
|
47
|
+
try { keys = JSON.parse(process.env.ENTROROVE_BUDGET_PUBLIC_KEYS ?? '{}'); } catch { fail('ENTROROVE_BUDGET_TRUST_INVALID', 'budget trust configuration is invalid'); }
|
|
48
|
+
if (!keys || typeof keys[grant.keyId] !== 'string') fail('ENTROROVE_BUDGET_UNTRUSTED', 'budget grant signer is not configured');
|
|
49
|
+
try {
|
|
50
|
+
const encoded = keys[grant.keyId];
|
|
51
|
+
const key = encoded.includes('BEGIN PUBLIC KEY') ? createPublicKey(encoded) : createPublicKey({ key: Buffer.from(encoded, 'base64'), type: 'spki', format: 'der' });
|
|
52
|
+
if (key.asymmetricKeyType !== 'ed25519' || sha(key.export({ type: 'spki', format: 'der' })) !== grant.keyId) throw new Error();
|
|
53
|
+
return key;
|
|
54
|
+
} catch { fail('ENTROROVE_BUDGET_TRUST_INVALID', 'budget trusted key identity is invalid'); }
|
|
55
|
+
}
|
|
56
|
+
export function verifyBudgetGrant(grant, binding, kind, checkExpiry = true) {
|
|
57
|
+
if (!grant || grant.schemaVersion !== GRANT_SCHEMA || grant.kind !== kind || typeof grant.signature !== 'string') fail('ENTROROVE_BUDGET_GRANT_REQUIRED', 'a cloud-signed budget grant is required');
|
|
58
|
+
for (const field of ['grantId', 'flowId', 'stepId', 'ownerId', 'leaseId', 'requestId']) identifier(grant[field], field);
|
|
59
|
+
const unsigned = Object.fromEntries(Object.entries(grant).filter(([key]) => key !== 'signature'));
|
|
60
|
+
let valid = false;
|
|
61
|
+
try { valid = verify(null, Buffer.from(canonical(unsigned)), configuredKey(grant), Buffer.from(grant.signature, 'base64url')); } catch (error) { if (error.code) throw error; }
|
|
62
|
+
if (!valid) fail('ENTROROVE_BUDGET_SIGNATURE', 'budget cloud signature is invalid');
|
|
63
|
+
if (!READ_BUDGETS[grant.mode] || !Number.isSafeInteger(grant.revision) || grant.revision < 1 || !Number.isSafeInteger(grant.leaseGeneration) || grant.leaseGeneration < 1
|
|
64
|
+
|| !/^[0-9a-f]{32}$/.test(grant.repositoryDigest) || !Number.isFinite(Date.parse(grant.issuedAt)) || !Number.isFinite(Date.parse(grant.expiresAt))
|
|
65
|
+
|| Date.parse(grant.issuedAt) > Date.now() + 5000 || Date.parse(grant.expiresAt) <= Date.parse(grant.issuedAt)) fail('ENTROROVE_BUDGET_GRANT_INVALID', 'budget grant fields are invalid');
|
|
66
|
+
if (checkExpiry && Date.parse(grant.expiresAt) <= Date.now()) fail('ENTROROVE_BUDGET_LEASE_EXPIRED', 'the cloud step budget lease expired');
|
|
67
|
+
if (grant.handoff !== undefined && grant.purpose !== 'handoff-read'
|
|
68
|
+
|| grant.purpose === 'handoff-read' && !['init', 'handoff-read', 'handoff-claim'].includes(grant.kind)) {
|
|
69
|
+
fail('ENTROROVE_BUDGET_HANDOFF_FORBIDDEN', 'handoff provenance cannot be reused by an ordinary budget operation');
|
|
70
|
+
}
|
|
71
|
+
const scope = normalizeScope(grant.scope);
|
|
72
|
+
if (canonical(scope) !== canonical(grant.scope) || sha(canonical(scope)) !== grant.scopeDigest) fail('ENTROROVE_BUDGET_SCOPE', 'budget scope digest is invalid');
|
|
73
|
+
if (!grant.limits || ['maxFiles', 'maxTokenEstimate', 'maxDurationMs'].some(field => !Number.isSafeInteger(grant.limits[field]) || grant.limits[field] < 1)) fail('ENTROROVE_BUDGET_GRANT_INVALID', 'budget limits are invalid');
|
|
74
|
+
for (const field of ['flowId', 'repositoryDigest', 'stepId', 'ownerId', 'leaseId', 'leaseGeneration', 'mode', 'scopeDigest']) {
|
|
75
|
+
if (binding[field] !== undefined && grant[field] !== binding[field]) fail('ENTROROVE_BUDGET_SCOPE', `budget grant ${field} does not match its original task lease`);
|
|
76
|
+
}
|
|
77
|
+
return grant;
|
|
78
|
+
}
|
|
79
|
+
export function requireLiveBudget(state, now = Date.now()) {
|
|
80
|
+
if (state.closedReason === 'completed') fail('ENTROROVE_BUDGET_COMPLETED', 'completed tasks cannot read more source');
|
|
81
|
+
if (state.enforced && Date.parse(state.grant.expiresAt) <= now) fail('ENTROROVE_BUDGET_LEASE_EXPIRED', 'the cloud budget lease expired');
|
|
82
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { canonical, fail, sha } from './local-budget-grants.mjs';
|
|
3
|
+
import { withBudget } from './local-budget-store.mjs';
|
|
4
|
+
import { completionChallenge } from './local-task-handoff-completion-wire.mjs';
|
|
5
|
+
|
|
6
|
+
const observation = new AsyncLocalStorage();
|
|
7
|
+
const fields = ['home', 'repositoryRoot', 'flowId', 'challenge', 'signerConfigPath'];
|
|
8
|
+
function denied(message) { fail('ENTROROVE_BUDGET_HANDOFF_REVOKED', message); }
|
|
9
|
+
function fixedInput(input) {
|
|
10
|
+
if (!input || typeof input !== 'object' || Object.keys(input).some(field => !fields.includes(field))) {
|
|
11
|
+
denied('completion observation requires its fixed native input');
|
|
12
|
+
}
|
|
13
|
+
try { return structuredClone(input); } catch { denied('completion controls must be immutable data'); }
|
|
14
|
+
}
|
|
15
|
+
function challengeFor(input, state, location) {
|
|
16
|
+
const retained = state.handoffRead, h = state.grant.handoff;
|
|
17
|
+
if (!state.enforced || state.closedReason || state.grant.purpose !== 'handoff-read' || h?.stage !== 'claimed'
|
|
18
|
+
|| !retained || canonical(retained.handoff) !== canonical(h)
|
|
19
|
+
|| canonical(retained.local) !== canonical({ home: location.home, root: location.root, flowId: location.flowId })) {
|
|
20
|
+
denied('completion observation requires the original claimed receiver budget and home');
|
|
21
|
+
}
|
|
22
|
+
const challenge = completionChallenge(input.challenge, retained.proof, { flowId: location.flowId, repo: location.repositoryDigest });
|
|
23
|
+
if (challenge.ownerId !== state.grant.ownerId || challenge.taskId !== h.receiverTaskId
|
|
24
|
+
|| challenge.handoffId !== h.handoffId || challenge.messageRequestId !== h.messageRequestId
|
|
25
|
+
|| challenge.epoch !== h.receiverEpoch || challenge.passDigest !== h.passDigest || challenge.leaseDigest !== h.leaseDigest
|
|
26
|
+
|| canonical([...challenge.paths].sort()) !== canonical(state.grant.scope.allowedPaths)) {
|
|
27
|
+
denied('completion observation changed its signed takeover or exact source paths');
|
|
28
|
+
}
|
|
29
|
+
return challenge;
|
|
30
|
+
}
|
|
31
|
+
export function isBudgetHandoffCompletionObservation(input) {
|
|
32
|
+
const current = observation.getStore();
|
|
33
|
+
return Boolean(current && current.inputDigest === sha(canonical(input)));
|
|
34
|
+
}
|
|
35
|
+
// This entry can only invoke the fixed native completion producer. It accepts no reader or callback.
|
|
36
|
+
export async function observeBudgetedTaskHandoffReceiverCompletion(input) {
|
|
37
|
+
input = fixedInput(input);
|
|
38
|
+
if (observation.getStore()) denied('completion observation cannot nest another authorization');
|
|
39
|
+
const native = await import('./local-task-handoff-fence.mjs');
|
|
40
|
+
const current = await withBudget(input, async (location, loaded) => {
|
|
41
|
+
const state = loaded?.state;
|
|
42
|
+
const nativeState = state?.grant?.purpose === 'handoff-read' ? await native.readTaskHandoffFence(input) : null;
|
|
43
|
+
const replay = nativeState?.receiverCompletion && !nativeState.receiverRestoration
|
|
44
|
+
&& canonical(nativeState.receiverCompletion.challenge) === canonical(input.challenge);
|
|
45
|
+
const challenge = state?.grant?.purpose === 'handoff-read' && !replay ? challengeFor(input, state, location) : null;
|
|
46
|
+
return Object.freeze({ inputDigest: sha(canonical(input)), input, location,
|
|
47
|
+
grantId: challenge ? state.grant.grantId : null, challenge });
|
|
48
|
+
}, true);
|
|
49
|
+
return observation.run(current, () => native.observeTaskHandoffReceiverCompletion(input));
|
|
50
|
+
}
|
|
51
|
+
export function budgetHandoffCompletionValidationState(state) {
|
|
52
|
+
const current = observation.getStore();
|
|
53
|
+
if (!current?.challenge || current.grantId !== state.grant?.grantId) return state;
|
|
54
|
+
return { ...state, grant: { ...state.grant, expiresAt: current.challenge.expiresAt } };
|
|
55
|
+
}
|
|
56
|
+
export function assertBudgetHandoffCompletionRead(input, state, nativeState) {
|
|
57
|
+
const current = observation.getStore();
|
|
58
|
+
if (!current?.challenge) return false;
|
|
59
|
+
if (input.sourceRole?.startsWith('git-') || current.grantId !== state.grant?.grantId || input.flowId !== current.location.flowId
|
|
60
|
+
|| input.home !== current.location.home || input.repositoryRoot !== current.location.root
|
|
61
|
+
|| !current.challenge.paths.includes(input.path) || nativeState.receiverRestoration
|
|
62
|
+
|| !nativeState.receiverClaim || canonical(nativeState.receiverClaim) !== canonical(state.handoffRead?.proof)
|
|
63
|
+
|| nativeState.epoch !== state.grant.handoff.receiverEpoch
|
|
64
|
+
|| !['active', 'paused'].includes(nativeState.status)
|
|
65
|
+
|| nativeState.receiverCompletion && Date.parse(nativeState.receiverCompletion.receipt.expiresAt) > Date.now()) {
|
|
66
|
+
denied('completion source read is outside its fixed original live observation');
|
|
67
|
+
}
|
|
68
|
+
challengeFor(current.input, state, current.location);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { integer, ledgerIdentifier, object, record, sha256 } from './local-contract-types.mjs';
|
|
2
|
+
import { HANDOFF_PROVISIONAL_LEASE_SCHEMA, HANDOFF_SNAPSHOT_CANDIDATE_SCHEMA,
|
|
3
|
+
HANDOFF_WRITE_PASS_SCHEMA } from './local-task-handoff-contracts.mjs';
|
|
4
|
+
import { HANDOFF_RESTORATION_FIELDS } from './local-task-handoff-overlay-wire.mjs';
|
|
5
|
+
|
|
6
|
+
const leased = HANDOFF_PROVISIONAL_LEASE_SCHEMA.properties;
|
|
7
|
+
const inspection = Object.fromEntries(Object.entries(leased).filter(([field]) =>
|
|
8
|
+
!['generation', 'passId', 'passDigest', 'claimReceiptId'].includes(field)));
|
|
9
|
+
inspection.purpose = { const: 'scope-inspection' };
|
|
10
|
+
inspection.mutationAllowed = { const: false };
|
|
11
|
+
inspection.baselineReceiptDigest = sha256;
|
|
12
|
+
export const HANDOFF_BUDGET_CANDIDATE_LEASE_SCHEMA = object(inspection, Object.keys(inspection));
|
|
13
|
+
const timestamp = leased.issuedAt;
|
|
14
|
+
function restorationField(field) {
|
|
15
|
+
if (field === 'schemaVersion') return { const: 'entrorove.task-handoff-receiver-restoration/1.0' };
|
|
16
|
+
if (field === 'localRepositoryDigest') return leased.localRepositoryDigest;
|
|
17
|
+
if (field.endsWith('Digest')) return sha256;
|
|
18
|
+
if (['epoch', 'previousEpoch'].includes(field)) return integer(0, 2147483647);
|
|
19
|
+
if (field === 'paths') return leased.paths;
|
|
20
|
+
if (['issuedAt', 'expiresAt'].includes(field)) return timestamp;
|
|
21
|
+
if (['revokedPassIds', 'revokedLeaseIds'].includes(field)) return { type: 'array', items: ledgerIdentifier, minItems: 1, uniqueItems: true };
|
|
22
|
+
return ledgerIdentifier;
|
|
23
|
+
}
|
|
24
|
+
const restoration = Object.fromEntries(HANDOFF_RESTORATION_FIELDS.map(field => [field, restorationField(field)]));
|
|
25
|
+
restoration.authorityReceipt = leased.authorityReceipt;
|
|
26
|
+
export const HANDOFF_BUDGET_RESTORATION_SCHEMA = object(restoration, Object.keys(restoration));
|
|
27
|
+
export const HANDOFF_BUDGET_INIT_PROPERTIES = {
|
|
28
|
+
handoffCandidate: HANDOFF_SNAPSHOT_CANDIDATE_SCHEMA, provisionalLease: HANDOFF_BUDGET_CANDIDATE_LEASE_SCHEMA,
|
|
29
|
+
};
|
|
30
|
+
export const HANDOFF_BUDGET_INIT_CONDITION = {
|
|
31
|
+
if: { type: 'object', anyOf: [
|
|
32
|
+
{ properties: { grant: { type: 'object', properties: { purpose: { const: 'handoff-read' } }, required: ['purpose'] } }, required: ['grant'] },
|
|
33
|
+
{ required: ['handoffCandidate'] }, { required: ['provisionalLease'] },
|
|
34
|
+
] },
|
|
35
|
+
then: { type: 'object', required: ['grant', 'handoffCandidate', 'provisionalLease'],
|
|
36
|
+
properties: { grant: { type: 'object', properties: { purpose: { const: 'handoff-read' }, kind: { const: 'init' } }, required: ['purpose', 'kind'] } } },
|
|
37
|
+
};
|
|
38
|
+
function grant(kind, purpose) {
|
|
39
|
+
return { ...record, properties: { kind: { const: kind }, ...(purpose ? { purpose: { const: purpose } } : {}) },
|
|
40
|
+
required: ['kind', ...(purpose ? ['purpose'] : [])] };
|
|
41
|
+
}
|
|
42
|
+
export function handoffBudgetOperationSchema(flow, mode, executionContext) {
|
|
43
|
+
const required = ['repositoryRoot', 'home', 'flowId', 'grant'];
|
|
44
|
+
return { oneOf: [
|
|
45
|
+
object({ ...flow, grant: grant('handoff-read', 'handoff-read'), mode, executionContext, ...HANDOFF_BUDGET_INIT_PROPERTIES },
|
|
46
|
+
[...required, 'handoffCandidate', 'provisionalLease']),
|
|
47
|
+
object({ ...flow, grant: grant('handoff-claim', 'handoff-read'), pass: HANDOFF_WRITE_PASS_SCHEMA,
|
|
48
|
+
lease: HANDOFF_PROVISIONAL_LEASE_SCHEMA, challenge: HANDOFF_SNAPSHOT_CANDIDATE_SCHEMA },
|
|
49
|
+
[...required, 'pass', 'lease', 'challenge']),
|
|
50
|
+
object({ ...flow, grant: grant('handoff-restore'), authorization: HANDOFF_BUDGET_RESTORATION_SCHEMA },
|
|
51
|
+
[...required, 'authorization']),
|
|
52
|
+
] };
|
|
53
|
+
}
|