@dharma-ai-labs/agent-fabric-local-vault 0.1.26 → 0.1.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/fullVaultScope.test.d.ts +2 -0
- package/dist/fullVaultScope.test.d.ts.map +1 -0
- package/dist/fullVaultScope.test.js +602 -0
- package/dist/fullVaultScope.test.js.map +1 -0
- package/dist/hostMasterKey.test.d.ts +2 -0
- package/dist/hostMasterKey.test.d.ts.map +1 -0
- package/dist/hostMasterKey.test.js +122 -0
- package/dist/hostMasterKey.test.js.map +1 -0
- package/dist/index.d.ts +83 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +988 -102
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +52 -0
- package/dist/index.test.js.map +1 -1
- package/dist/setupJournalScope.test.d.ts +2 -0
- package/dist/setupJournalScope.test.d.ts.map +1 -0
- package/dist/setupJournalScope.test.js +181 -0
- package/dist/setupJournalScope.test.js.map +1 -0
- package/dist/setupOperations.test.d.ts +2 -0
- package/dist/setupOperations.test.d.ts.map +1 -0
- package/dist/setupOperations.test.js +290 -0
- package/dist/setupOperations.test.js.map +1 -0
- package/dist/setupReadiness.d.ts +43 -0
- package/dist/setupReadiness.d.ts.map +1 -0
- package/dist/setupReadiness.js +46 -0
- package/dist/setupReadiness.js.map +1 -0
- package/dist/setupReadiness.test.d.ts +2 -0
- package/dist/setupReadiness.test.d.ts.map +1 -0
- package/dist/setupReadiness.test.js +185 -0
- package/dist/setupReadiness.test.js.map +1 -0
- package/dist/setupSessionHandoff.d.ts +60 -0
- package/dist/setupSessionHandoff.d.ts.map +1 -0
- package/dist/setupSessionHandoff.js +67 -0
- package/dist/setupSessionHandoff.js.map +1 -0
- package/dist/setupSessionHandoff.test.d.ts +2 -0
- package/dist/setupSessionHandoff.test.d.ts.map +1 -0
- package/dist/setupSessionHandoff.test.js +357 -0
- package/dist/setupSessionHandoff.test.js.map +1 -0
- package/dist/vaultLifecycle.test.d.ts +2 -0
- package/dist/vaultLifecycle.test.d.ts.map +1 -0
- package/dist/vaultLifecycle.test.js +275 -0
- package/dist/vaultLifecycle.test.js.map +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,6 +4,13 @@ Encrypted, content-addressed local storage for complete Agent Fabric trajectory
|
|
|
4
4
|
evidence. The vault seals its master key through the operating-system secure
|
|
5
5
|
store and fails closed when secure key storage is unavailable.
|
|
6
6
|
|
|
7
|
+
Trusted setup hosts can pass a `VaultKeyOperationScope` as the second argument
|
|
8
|
+
to `loadOrCreateVaultMasterKey`. The helper requalifies before and after each
|
|
9
|
+
store effect, withholds a key after cancellation, and disables the developer
|
|
10
|
+
environment-key fallback for that operation. It neither unlocks an OS store nor
|
|
11
|
+
proves that an OS adapter child has terminated. A completed write remains a
|
|
12
|
+
partial effect if subsequent admission is withdrawn.
|
|
13
|
+
|
|
7
14
|
- [Evidence boundary](https://github.com/dharma-ai-labs/dharma-agent-fabric#cognitive-integrity-evidence-ladder)
|
|
8
15
|
- [Security boundary](https://github.com/dharma-ai-labs/dharma-agent-fabric/blob/main/docs/10-security-privacy-and-threat-model.md)
|
|
9
16
|
- [Source](https://github.com/dharma-ai-labs/dharma-agent-fabric/tree/main/packages/local-vault)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fullVaultScope.test.d.ts","sourceRoot":"","sources":["../src/fullVaultScope.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import * as fs from 'node:fs/promises';
|
|
4
|
+
import * as syncFs from 'node:fs';
|
|
5
|
+
import * as crypto from 'node:crypto';
|
|
6
|
+
import * as os from 'node:os';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
9
|
+
import * as util from 'node:util';
|
|
10
|
+
import { runInNewContext } from 'node:vm';
|
|
11
|
+
import test from 'node:test';
|
|
12
|
+
import ts from 'typescript';
|
|
13
|
+
import * as contracts from '@dharma-ai-labs/agent-fabric-contracts';
|
|
14
|
+
import * as evidence from '@dharma-ai-labs/agent-fabric-evidence-reduction';
|
|
15
|
+
import * as setupReadiness from './setupReadiness.js';
|
|
16
|
+
import * as setupSessionHandoff from './setupSessionHandoff.js';
|
|
17
|
+
async function fixture(t) {
|
|
18
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'dharma-full-vault-scope-'));
|
|
19
|
+
const key = crypto.randomBytes(32), controller = new AbortController();
|
|
20
|
+
let active = true;
|
|
21
|
+
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
|
22
|
+
return { root, key, controller, scope: { signal: controller.signal, current: async () => active },
|
|
23
|
+
withdraw: () => { active = false; controller.abort(); } };
|
|
24
|
+
}
|
|
25
|
+
// Actual production module with real C-only files/SQLite; no credential-store access.
|
|
26
|
+
async function module(overrides = {}) {
|
|
27
|
+
const oldHead = process.env.DHARMA_FULL_VAULT_TEST_SOURCE_HEAD;
|
|
28
|
+
if (oldHead && oldHead !== 'f9d37d1b94721a4a01e3c4716c2c316d822a0d24')
|
|
29
|
+
throw new Error('fixture source not qualified');
|
|
30
|
+
const source = oldHead ? execFileSync('git', ['show', `${oldHead}:packages/local-vault/src/index.ts`], { encoding: 'utf8' })
|
|
31
|
+
: await fs.readFile(new URL('../src/index.ts', import.meta.url), 'utf8');
|
|
32
|
+
const result = ts.transpileModule(source, { compilerOptions: {
|
|
33
|
+
target: ts.ScriptTarget.ES2023, module: ts.ModuleKind.CommonJS
|
|
34
|
+
}, reportDiagnostics: true });
|
|
35
|
+
assert.equal(result.diagnostics?.filter(item => item.category === ts.DiagnosticCategory.Error).length, 0);
|
|
36
|
+
const exports = {};
|
|
37
|
+
runInNewContext(result.outputText, { exports, Buffer, process, Date, AbortSignal, structuredClone,
|
|
38
|
+
require: (name) => {
|
|
39
|
+
if (name === 'node:crypto')
|
|
40
|
+
return { ...crypto, ...overrides.crypto };
|
|
41
|
+
if (name === 'node:fs')
|
|
42
|
+
return syncFs;
|
|
43
|
+
if (name === 'node:fs/promises')
|
|
44
|
+
return { ...fs, ...overrides.fs };
|
|
45
|
+
if (name === 'node:os')
|
|
46
|
+
return os;
|
|
47
|
+
if (name === 'node:path')
|
|
48
|
+
return path;
|
|
49
|
+
if (name === 'node:sqlite')
|
|
50
|
+
return { DatabaseSync: overrides.database ?? DatabaseSync };
|
|
51
|
+
if (name === 'node:util')
|
|
52
|
+
return util;
|
|
53
|
+
if (name === '@dharma-ai-labs/agent-fabric-contracts')
|
|
54
|
+
return contracts;
|
|
55
|
+
if (name === '@dharma-ai-labs/agent-fabric-evidence-reduction')
|
|
56
|
+
return evidence;
|
|
57
|
+
if (name === './setupReadiness.js')
|
|
58
|
+
return setupReadiness;
|
|
59
|
+
if (name === './setupSessionHandoff.js')
|
|
60
|
+
return setupSessionHandoff;
|
|
61
|
+
if (name === '@dharma-ai-labs/agent-fabric-secure-store')
|
|
62
|
+
return {
|
|
63
|
+
createSystemSecureStore: async () => { throw new Error('real credential access forbidden in fixture'); },
|
|
64
|
+
};
|
|
65
|
+
throw new Error('unexpected fixture dependency');
|
|
66
|
+
} }, { timeout: 1000, contextCodeGeneration: { strings: false, wasm: false } });
|
|
67
|
+
return exports.LocalVault;
|
|
68
|
+
}
|
|
69
|
+
function capture() {
|
|
70
|
+
const raw = Buffer.from('synthetic executed turn'), capsule = Buffer.from(JSON.stringify({ contentIndex: [] }));
|
|
71
|
+
return { raw: { plaintext: raw, kind: 'raw-provider-turn', expectedContentId: contracts.sha256(raw) },
|
|
72
|
+
capsule: { plaintext: capsule, trajectoryId: 'synthetic-trajectory', revision: 1, capsuleHash: contracts.sha256(capsule) },
|
|
73
|
+
session: { sessionId: 'synthetic-session', provider: 'codex', workspaceId: 'synthetic-workspace',
|
|
74
|
+
sourceLocator: 'synthetic-local-reference', status: 'observed', observedAt: new Date().toISOString() } };
|
|
75
|
+
}
|
|
76
|
+
function binding(root, number = 6) {
|
|
77
|
+
const id = (n) => `40000000-0000-4000-8000-${String(n).padStart(12, '0')}`;
|
|
78
|
+
return { schema: 'dharma.local-provider-session-binding/v1', owner: 'dharma_bridge',
|
|
79
|
+
organizationId: 'org_synthetic', repositoryBindingId: id(1), workspaceId: id(2), endpointId: id(3),
|
|
80
|
+
membershipId: id(4), deviceId: id(5), bindingId: id(number), provider: 'codex', sessionId: `synthetic-thread-${number}`,
|
|
81
|
+
workspaceRoot: path.join(root, 'repo'), createdAt: new Date().toISOString(),
|
|
82
|
+
expiresAt: new Date(Date.now() + 86_400_000).toISOString(), maximumProviderCostCents: 25 };
|
|
83
|
+
}
|
|
84
|
+
for (const boundary of ['before-read', 'after-read']) {
|
|
85
|
+
test(`full scoped completion readback withholds ${boundary} withdrawal without acquiring another lease`, async (t) => {
|
|
86
|
+
const f = await fixture(t);
|
|
87
|
+
let armed = false, reads = 0;
|
|
88
|
+
class ReadbackDatabase extends DatabaseSync {
|
|
89
|
+
prepare(sql) {
|
|
90
|
+
const statement = super.prepare(sql), get = statement.get.bind(statement);
|
|
91
|
+
if (sql === 'select * from codex_setup_operations where operation_id = ?') {
|
|
92
|
+
statement.get = ((...args) => {
|
|
93
|
+
const result = Reflect.apply(get, statement, args);
|
|
94
|
+
if (armed) {
|
|
95
|
+
reads++;
|
|
96
|
+
if (boundary === 'after-read')
|
|
97
|
+
f.withdraw();
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return statement;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const api = await module({ database: ReadbackDatabase });
|
|
106
|
+
const vault = await api.open({ root: f.root, masterKey: f.key }, f.scope);
|
|
107
|
+
const operation = '11111111-1111-4111-8111-111111111111', digest = `sha256:${'a'.repeat(64)}`;
|
|
108
|
+
const result = { state: 'completed', readinessReceiptId: '22222222-2222-4222-8222-222222222222' };
|
|
109
|
+
try {
|
|
110
|
+
const lease = await vault.claimCodexSetupOperation(operation, digest);
|
|
111
|
+
assert.equal(lease.state, 'acquired');
|
|
112
|
+
await vault.finishCodexSetupOperation(lease.leaseId, digest, result);
|
|
113
|
+
armed = true;
|
|
114
|
+
if (boundary === 'before-read')
|
|
115
|
+
f.withdraw();
|
|
116
|
+
await assert.rejects(vault.readCodexSetupOperation(operation, digest), { message: 'vault_scope_unavailable' });
|
|
117
|
+
assert.equal(reads, boundary === 'before-read' ? 0 : 1);
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
await vault.close();
|
|
121
|
+
}
|
|
122
|
+
const reopened = await api.open({ root: f.root, masterKey: f.key });
|
|
123
|
+
try {
|
|
124
|
+
assert.deepEqual(JSON.parse(JSON.stringify(await reopened.readCodexSetupOperation(operation, digest))), { state: 'terminal', intentDigest: digest, result });
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
await reopened.close();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
for (const rollback of [true, false]) {
|
|
132
|
+
test(`full scoped lease release waits for ${rollback ? 'rolled-back' : 'committed'} capture before durable exact-holder cleanup`, async (t) => {
|
|
133
|
+
const f = await fixture(t);
|
|
134
|
+
let armed = false, ready, finish;
|
|
135
|
+
const entered = new Promise(resolve => { ready = resolve; }), pending = new Promise(resolve => { finish = resolve; });
|
|
136
|
+
const api = await module({ fs: { open: async (...args) => {
|
|
137
|
+
const handle = await fs.open(...args);
|
|
138
|
+
if (!armed || !String(args[0]).endsWith('.tmp'))
|
|
139
|
+
return handle;
|
|
140
|
+
return { stat: handle.stat.bind(handle), sync: handle.sync.bind(handle), close: handle.close.bind(handle),
|
|
141
|
+
writeFile: async (bytes) => { await handle.writeFile(bytes); ready(); await pending; } };
|
|
142
|
+
} } });
|
|
143
|
+
const vault = await api.open({ root: f.root, masterKey: f.key }, f.scope), record = binding(f.root);
|
|
144
|
+
await vault.saveProviderSessionBinding(record);
|
|
145
|
+
const lease = await vault.tryAcquireProviderSessionLease(record.bindingId, record);
|
|
146
|
+
assert.ok(lease);
|
|
147
|
+
armed = true;
|
|
148
|
+
const work = vault.commitCapture(capture());
|
|
149
|
+
work.catch(() => undefined);
|
|
150
|
+
await entered;
|
|
151
|
+
const released = lease.release(), duplicate = lease.release();
|
|
152
|
+
if (rollback)
|
|
153
|
+
f.withdraw();
|
|
154
|
+
try {
|
|
155
|
+
finish();
|
|
156
|
+
if (rollback)
|
|
157
|
+
await assert.rejects(work, { message: 'vault_scope_unavailable' });
|
|
158
|
+
else
|
|
159
|
+
await work;
|
|
160
|
+
await released;
|
|
161
|
+
await duplicate;
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
finish();
|
|
165
|
+
await work.catch(() => undefined);
|
|
166
|
+
await vault.close();
|
|
167
|
+
}
|
|
168
|
+
const db = new DatabaseSync(path.join(f.root, 'vault.sqlite'), { readOnly: true });
|
|
169
|
+
try {
|
|
170
|
+
assert.equal(db.prepare('select count(*) as n from provider_session_leases').get().n, 0);
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
db.close();
|
|
174
|
+
}
|
|
175
|
+
const reopened = await api.open({ root: f.root, masterKey: f.key }, { signal: new AbortController().signal, current: async () => true });
|
|
176
|
+
try {
|
|
177
|
+
const next = await reopened.tryAcquireProviderSessionLease(record.bindingId, record);
|
|
178
|
+
assert.ok(next);
|
|
179
|
+
await next.release();
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
await reopened.close();
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
test('full scoped failed lease release retains exact-holder ownership for close retry without touching a sibling', async (t) => {
|
|
187
|
+
const f = await fixture(t);
|
|
188
|
+
let fail = false, failures = 0;
|
|
189
|
+
class OwnedDatabase extends DatabaseSync {
|
|
190
|
+
prepare(sql) {
|
|
191
|
+
const statement = super.prepare(sql), run = statement.run.bind(statement);
|
|
192
|
+
if (sql.startsWith('delete from provider_session_leases')) {
|
|
193
|
+
statement.run = ((...args) => {
|
|
194
|
+
if (fail) {
|
|
195
|
+
failures++;
|
|
196
|
+
throw new Error('private lease delete canary');
|
|
197
|
+
}
|
|
198
|
+
return Reflect.apply(run, statement, args);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return statement;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const api = await module({ database: OwnedDatabase }), vault = await api.open({ root: f.root, masterKey: f.key }, f.scope);
|
|
205
|
+
const own = binding(f.root), sibling = binding(f.root, 7);
|
|
206
|
+
await vault.saveProviderSessionBinding(own);
|
|
207
|
+
const other = await api.open({ root: f.root, masterKey: f.key });
|
|
208
|
+
other.saveProviderSessionBinding(sibling);
|
|
209
|
+
const lease = await vault.tryAcquireProviderSessionLease(own.bindingId, own), peer = other.tryAcquireProviderSessionLease(sibling.bindingId, sibling);
|
|
210
|
+
assert.ok(lease);
|
|
211
|
+
assert.ok(peer);
|
|
212
|
+
try {
|
|
213
|
+
fail = true;
|
|
214
|
+
await assert.rejects(async () => lease.release(), { message: 'vault_cleanup_unconfirmed' });
|
|
215
|
+
fail = false;
|
|
216
|
+
f.withdraw();
|
|
217
|
+
await vault.close();
|
|
218
|
+
assert.equal(await peer.assertHeld(), true);
|
|
219
|
+
assert.equal(failures, 1);
|
|
220
|
+
const db = new DatabaseSync(path.join(f.root, 'vault.sqlite'), { readOnly: true });
|
|
221
|
+
try {
|
|
222
|
+
assert.equal(db.prepare('select count(*) as n from provider_session_leases where binding_id = ?').get(own.bindingId).n, 0);
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
db.close();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
fail = false;
|
|
230
|
+
await vault.close();
|
|
231
|
+
peer.release();
|
|
232
|
+
other.close();
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
test('full legacy lease refuses transaction-local release and retains ownership through rollback', async (t) => {
|
|
236
|
+
const f = await fixture(t);
|
|
237
|
+
let armed = false, ready, finish;
|
|
238
|
+
const entered = new Promise(resolve => { ready = resolve; }), pending = new Promise(resolve => { finish = resolve; });
|
|
239
|
+
const api = await module({ fs: { writeFile: async (...args) => {
|
|
240
|
+
const result = await fs.writeFile(...args);
|
|
241
|
+
if (armed && String(args[0]).endsWith('.tmp')) {
|
|
242
|
+
ready();
|
|
243
|
+
await pending;
|
|
244
|
+
}
|
|
245
|
+
return result;
|
|
246
|
+
} } });
|
|
247
|
+
const vault = await api.open({ root: f.root, masterKey: f.key }), record = binding(f.root);
|
|
248
|
+
vault.saveProviderSessionBinding(record);
|
|
249
|
+
const lease = vault.tryAcquireProviderSessionLease(record.bindingId, record);
|
|
250
|
+
assert.ok(lease);
|
|
251
|
+
const payload = capture();
|
|
252
|
+
payload.raw.expectedContentId = contracts.sha256('synthetic different bytes');
|
|
253
|
+
armed = true;
|
|
254
|
+
const work = vault.commitCapture(payload);
|
|
255
|
+
work.catch(() => undefined);
|
|
256
|
+
await entered;
|
|
257
|
+
try {
|
|
258
|
+
assert.throws(() => lease.release(), { message: 'vault_cleanup_unconfirmed' });
|
|
259
|
+
finish();
|
|
260
|
+
await assert.rejects(work, /Raw evidence content hash changed/);
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
finish();
|
|
264
|
+
await work.catch(() => undefined);
|
|
265
|
+
vault.close();
|
|
266
|
+
}
|
|
267
|
+
const db = new DatabaseSync(path.join(f.root, 'vault.sqlite'), { readOnly: true });
|
|
268
|
+
try {
|
|
269
|
+
assert.equal(db.prepare('select count(*) as n from provider_session_leases').get().n, 0);
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
db.close();
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
test('full scoped vault rejects proxy input without invoking traps and ignores byte-array species', async (t) => {
|
|
276
|
+
const f = await fixture(t), api = await module(), vault = await api.open({ root: f.root, masterKey: f.key }, f.scope);
|
|
277
|
+
let traps = 0, species = 0;
|
|
278
|
+
const payload = new Proxy({}, { ownKeys: () => { traps++; throw new Error('private proxy canary'); } });
|
|
279
|
+
const bytes = Buffer.from('synthetic byte input');
|
|
280
|
+
Object.defineProperty(bytes, 'constructor', { get: () => { species++; throw new Error('private species canary'); } });
|
|
281
|
+
try {
|
|
282
|
+
await assert.rejects(vault.recordSession(payload), { message: 'vault_input_invalid' });
|
|
283
|
+
const id = await vault.putBlob(bytes, 'synthetic');
|
|
284
|
+
assert.deepEqual(await vault.getBlob(id), Buffer.from('synthetic byte input'));
|
|
285
|
+
assert.equal(traps, 0);
|
|
286
|
+
assert.equal(species, 0);
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
await vault.close();
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
test('full scoped vault refuses initialization before even mkdir under withdrawn authority', async (t) => {
|
|
293
|
+
const f = await fixture(t);
|
|
294
|
+
let effects = 0, vault;
|
|
295
|
+
const api = await module({ fs: { mkdir: async (...args) => { effects++; return fs.mkdir(...args); } } });
|
|
296
|
+
f.withdraw();
|
|
297
|
+
try {
|
|
298
|
+
await assert.rejects(async () => { vault = await api.open({ root: f.root, masterKey: f.key }, f.scope); }, { message: 'vault_scope_unavailable' });
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
await vault?.close();
|
|
302
|
+
}
|
|
303
|
+
assert.equal(effects, 0);
|
|
304
|
+
});
|
|
305
|
+
test('full scoped vault refuses malformed scope without invoking its accessor', async (t) => {
|
|
306
|
+
const f = await fixture(t);
|
|
307
|
+
let getters = 0, effects = 0, vault;
|
|
308
|
+
const api = await module({ fs: { mkdir: async (...args) => { effects++; return fs.mkdir(...args); } } });
|
|
309
|
+
const scope = { get signal() { getters++; throw new Error('private-scope-canary'); }, current: async () => true };
|
|
310
|
+
try {
|
|
311
|
+
await assert.rejects(async () => { vault = await api.open({ root: f.root, masterKey: f.key }, scope); }, { message: 'vault_scope_unavailable' });
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
await vault?.close();
|
|
315
|
+
}
|
|
316
|
+
assert.equal(getters, 0);
|
|
317
|
+
assert.equal(effects, 0);
|
|
318
|
+
});
|
|
319
|
+
test('full scoped vault retains original authority and refuses later sync database access', async (t) => {
|
|
320
|
+
const f = await fixture(t), api = await module(), scope = f.scope;
|
|
321
|
+
const vault = await api.open({ root: f.root, masterKey: f.key }, scope);
|
|
322
|
+
f.withdraw();
|
|
323
|
+
scope.current = async () => true;
|
|
324
|
+
scope.signal = new AbortController().signal;
|
|
325
|
+
try {
|
|
326
|
+
await assert.rejects(async () => vault.stats(), { message: 'vault_scope_unavailable' });
|
|
327
|
+
}
|
|
328
|
+
finally {
|
|
329
|
+
await vault.close();
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
test('full scoped vault stops initialization after withdrawal at recovery read and closes owned database', async (t) => {
|
|
333
|
+
const f = await fixture(t);
|
|
334
|
+
let closes = 0, reads = 0, vault;
|
|
335
|
+
class OwnedDatabase extends DatabaseSync {
|
|
336
|
+
close() { closes++; return super.close(); }
|
|
337
|
+
}
|
|
338
|
+
const api = await module({ database: OwnedDatabase, fs: { readdir: async (...args) => {
|
|
339
|
+
const result = await fs.readdir(...args);
|
|
340
|
+
reads++;
|
|
341
|
+
f.withdraw();
|
|
342
|
+
return result;
|
|
343
|
+
} } });
|
|
344
|
+
try {
|
|
345
|
+
await assert.rejects(async () => { vault = await api.open({ root: f.root, masterKey: f.key }, f.scope); }, { message: 'vault_scope_unavailable' });
|
|
346
|
+
}
|
|
347
|
+
finally {
|
|
348
|
+
await vault?.close();
|
|
349
|
+
}
|
|
350
|
+
assert.equal(reads, 1);
|
|
351
|
+
assert.equal(closes, 1);
|
|
352
|
+
});
|
|
353
|
+
test('full scoped vault stops encrypted blob publication after its directory yield', async (t) => {
|
|
354
|
+
const f = await fixture(t);
|
|
355
|
+
let armed = false, writes = 0;
|
|
356
|
+
const api = await module({ fs: { mkdir: async (...args) => {
|
|
357
|
+
const result = await fs.mkdir(...args);
|
|
358
|
+
if (armed)
|
|
359
|
+
f.withdraw();
|
|
360
|
+
return result;
|
|
361
|
+
}, open: async (...args) => { if (armed)
|
|
362
|
+
writes++; return fs.open(...args); },
|
|
363
|
+
writeFile: async (...args) => { if (armed)
|
|
364
|
+
writes++; return fs.writeFile(...args); } } });
|
|
365
|
+
const vault = await api.open({ root: f.root, masterKey: f.key }, f.scope);
|
|
366
|
+
armed = true;
|
|
367
|
+
try {
|
|
368
|
+
await assert.rejects(vault.putBlob(Buffer.from('synthetic forbidden publication'), 'synthetic'), { message: 'vault_scope_unavailable' });
|
|
369
|
+
}
|
|
370
|
+
finally {
|
|
371
|
+
await vault.close();
|
|
372
|
+
}
|
|
373
|
+
assert.equal(writes, 0);
|
|
374
|
+
});
|
|
375
|
+
test('full scoped vault does not decrypt a read returned after withdrawal', async (t) => {
|
|
376
|
+
const f = await fixture(t);
|
|
377
|
+
let armed = false, decrypted = 0;
|
|
378
|
+
const api = await module({ fs: { readFile: async (...args) => {
|
|
379
|
+
const result = await fs.readFile(...args);
|
|
380
|
+
if (armed)
|
|
381
|
+
f.withdraw();
|
|
382
|
+
return result;
|
|
383
|
+
} }, crypto: { createDecipheriv: (...args) => { decrypted++; return crypto.createDecipheriv(...args); } } });
|
|
384
|
+
const vault = await api.open({ root: f.root, masterKey: f.key }, f.scope);
|
|
385
|
+
const hash = await vault.putBlob(Buffer.from('synthetic protected read'), 'synthetic');
|
|
386
|
+
armed = true;
|
|
387
|
+
try {
|
|
388
|
+
await assert.rejects(vault.getBlob(hash), { message: 'vault_scope_unavailable' });
|
|
389
|
+
}
|
|
390
|
+
finally {
|
|
391
|
+
await vault.close();
|
|
392
|
+
}
|
|
393
|
+
assert.equal(decrypted, 0);
|
|
394
|
+
});
|
|
395
|
+
test('full scoped vault snapshots capture inputs and keeps encrypted full evidence and sync queue', async (t) => {
|
|
396
|
+
const f = await fixture(t), input = capture();
|
|
397
|
+
let armed = false;
|
|
398
|
+
const expected = input.raw.expectedContentId;
|
|
399
|
+
const api = await module({ fs: { mkdir: async (...args) => {
|
|
400
|
+
const result = await fs.mkdir(...args);
|
|
401
|
+
if (armed) {
|
|
402
|
+
input.raw.plaintext.fill(0);
|
|
403
|
+
input.raw.expectedContentId = `sha256:${'0'.repeat(64)}`;
|
|
404
|
+
}
|
|
405
|
+
return result;
|
|
406
|
+
} } });
|
|
407
|
+
const vault = await api.open({ root: f.root, masterKey: f.key }, f.scope);
|
|
408
|
+
armed = true;
|
|
409
|
+
try {
|
|
410
|
+
const result = await vault.commitCapture(input);
|
|
411
|
+
assert.equal(result.rawContentId, expected);
|
|
412
|
+
await vault.queueCapsuleSync(input.capsule.trajectoryId, 1);
|
|
413
|
+
assert.equal((await vault.listPendingCapsuleSyncs()).length, 1);
|
|
414
|
+
assert.equal((await vault.stats()).sessions, 1);
|
|
415
|
+
assert.equal((await vault.getBlob(expected)).toString(), 'synthetic executed turn');
|
|
416
|
+
}
|
|
417
|
+
finally {
|
|
418
|
+
await vault.close();
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
test('full scoped vault retains encrypted quarantine and rolled-back metadata after retention withdrawal', async (t) => {
|
|
422
|
+
const f = await fixture(t);
|
|
423
|
+
let armed = false, renamed;
|
|
424
|
+
const api = await module({ fs: { rename: async (from, to) => {
|
|
425
|
+
await fs.rename(from, to);
|
|
426
|
+
if (armed) {
|
|
427
|
+
renamed = { from, to };
|
|
428
|
+
f.withdraw();
|
|
429
|
+
}
|
|
430
|
+
} } });
|
|
431
|
+
const vault = await api.open({ root: f.root, masterKey: f.key }, f.scope), bytes = Buffer.from('synthetic retained raw');
|
|
432
|
+
const hash = await vault.putBlob(bytes, 'raw-provider-turn');
|
|
433
|
+
armed = true;
|
|
434
|
+
try {
|
|
435
|
+
await assert.rejects(vault.enforceRawEvidenceRetention({ now: new Date(Date.now() + 40 * 86_400_000) }), { message: 'vault_scope_unavailable' });
|
|
436
|
+
}
|
|
437
|
+
finally {
|
|
438
|
+
await vault.close();
|
|
439
|
+
}
|
|
440
|
+
assert.ok(renamed);
|
|
441
|
+
assert.equal((await fs.readFile(renamed.to)).includes(bytes), false);
|
|
442
|
+
const reopened = await api.open({ root: f.root, masterKey: f.key });
|
|
443
|
+
try {
|
|
444
|
+
assert.deepEqual(await reopened.getBlob(hash), bytes);
|
|
445
|
+
}
|
|
446
|
+
finally {
|
|
447
|
+
await reopened.close();
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
test('full scoped vault close drains an owned pending read before closing its database', async (t) => {
|
|
451
|
+
const f = await fixture(t);
|
|
452
|
+
let armed = false, closes = 0, ready, finish;
|
|
453
|
+
const pending = new Promise(resolve => { finish = resolve; }), entered = new Promise(resolve => { ready = resolve; });
|
|
454
|
+
class OwnedDatabase extends DatabaseSync {
|
|
455
|
+
close() { closes++; return super.close(); }
|
|
456
|
+
}
|
|
457
|
+
const api = await module({ database: OwnedDatabase, fs: { readFile: async (...args) => {
|
|
458
|
+
const bytes = await fs.readFile(...args);
|
|
459
|
+
if (armed) {
|
|
460
|
+
ready();
|
|
461
|
+
await pending;
|
|
462
|
+
}
|
|
463
|
+
return bytes;
|
|
464
|
+
} } });
|
|
465
|
+
const vault = await api.open({ root: f.root, masterKey: f.key }, f.scope), hash = await vault.putBlob(Buffer.from('synthetic pending read'), 'synthetic');
|
|
466
|
+
armed = true;
|
|
467
|
+
const read = vault.getBlob(hash);
|
|
468
|
+
await entered;
|
|
469
|
+
const closing = vault.close();
|
|
470
|
+
try {
|
|
471
|
+
assert.equal(closes, 0);
|
|
472
|
+
}
|
|
473
|
+
finally {
|
|
474
|
+
finish();
|
|
475
|
+
await read.catch(() => undefined);
|
|
476
|
+
await closing;
|
|
477
|
+
}
|
|
478
|
+
await assert.rejects(read, { message: 'vault_scope_unavailable' });
|
|
479
|
+
assert.equal(closes, 1);
|
|
480
|
+
});
|
|
481
|
+
test('full scoped vault rolls back capture and retains only encrypted interruption evidence', async (t) => {
|
|
482
|
+
const f = await fixture(t);
|
|
483
|
+
let armed = false, writes = 0, retainedPath = '';
|
|
484
|
+
const api = await module({ fs: { open: async (...args) => {
|
|
485
|
+
const handle = await fs.open(...args);
|
|
486
|
+
if (!armed || !String(args[0]).endsWith('.tmp'))
|
|
487
|
+
return handle;
|
|
488
|
+
retainedPath = String(args[0]);
|
|
489
|
+
return { stat: handle.stat.bind(handle), sync: handle.sync.bind(handle), close: handle.close.bind(handle),
|
|
490
|
+
writeFile: async (bytes) => { await handle.writeFile(bytes); writes++; f.withdraw(); } };
|
|
491
|
+
} } });
|
|
492
|
+
const vault = await api.open({ root: f.root, masterKey: f.key }, f.scope), input = capture();
|
|
493
|
+
armed = true;
|
|
494
|
+
try {
|
|
495
|
+
await assert.rejects(vault.commitCapture(input), { message: 'vault_scope_unavailable' });
|
|
496
|
+
}
|
|
497
|
+
finally {
|
|
498
|
+
await vault.close();
|
|
499
|
+
}
|
|
500
|
+
assert.equal(writes, 1);
|
|
501
|
+
assert.equal((await fs.readFile(retainedPath)).includes(input.raw.plaintext), false);
|
|
502
|
+
const db = new DatabaseSync(path.join(f.root, 'vault.sqlite'));
|
|
503
|
+
try {
|
|
504
|
+
assert.equal(db.prepare('select count(*) as n from capsules').get().n, 0);
|
|
505
|
+
assert.equal(db.prepare('select count(*) as n from sessions').get().n, 0);
|
|
506
|
+
}
|
|
507
|
+
finally {
|
|
508
|
+
db.close();
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
test('full scoped vault never deletes a foreign replacement at an owned temporary name', async (t) => {
|
|
512
|
+
const f = await fixture(t);
|
|
513
|
+
let replacement = '', armed = false;
|
|
514
|
+
const bytes = Buffer.from('synthetic duplicate file'), foreign = Buffer.from('synthetic unrelated replacement');
|
|
515
|
+
const api = await module({ fs: { open: async (...args) => {
|
|
516
|
+
const handle = await fs.open(...args);
|
|
517
|
+
if (!armed || !String(args[0]).includes('.incoming-'))
|
|
518
|
+
return handle;
|
|
519
|
+
const file = String(args[0]);
|
|
520
|
+
return { stat: handle.stat.bind(handle), sync: handle.sync.bind(handle), write: handle.write.bind(handle),
|
|
521
|
+
close: async () => {
|
|
522
|
+
await handle.close();
|
|
523
|
+
await fs.rename(file, `${file}.retained`);
|
|
524
|
+
await fs.writeFile(file, foreign, { mode: 0o600, flag: 'wx' });
|
|
525
|
+
replacement = file;
|
|
526
|
+
} };
|
|
527
|
+
} } });
|
|
528
|
+
const vault = await api.open({ root: f.root, masterKey: f.key }, f.scope);
|
|
529
|
+
await vault.putBlob(bytes, 'synthetic');
|
|
530
|
+
const source = path.join(f.root, 'synthetic-source');
|
|
531
|
+
await fs.writeFile(source, bytes);
|
|
532
|
+
armed = true;
|
|
533
|
+
try {
|
|
534
|
+
await assert.rejects(vault.putFile(source, 'synthetic'), { message: 'vault_cleanup_unconfirmed' });
|
|
535
|
+
}
|
|
536
|
+
finally {
|
|
537
|
+
await vault.close();
|
|
538
|
+
}
|
|
539
|
+
assert.deepEqual(await fs.readFile(replacement), foreign);
|
|
540
|
+
});
|
|
541
|
+
test('full scoped vault refuses accessor payloads before executing user code or SQL', async (t) => {
|
|
542
|
+
const f = await fixture(t), api = await module(), vault = await api.open({ root: f.root, masterKey: f.key }, f.scope);
|
|
543
|
+
let getters = 0;
|
|
544
|
+
const session = { ...capture().session, get sourceLocator() { getters++; return 'private-input-canary'; } };
|
|
545
|
+
try {
|
|
546
|
+
await assert.rejects(vault.recordSession(session), { message: 'vault_input_invalid' });
|
|
547
|
+
assert.equal(getters, 0);
|
|
548
|
+
assert.equal((await vault.stats()).sessions, 0);
|
|
549
|
+
}
|
|
550
|
+
finally {
|
|
551
|
+
await vault.close();
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
test('full scoped vault lease withdrawal and cooperative release preserve a sibling holder', async (t) => {
|
|
555
|
+
const f = await fixture(t), api = await module(), vault = await api.open({ root: f.root, masterKey: f.key }, f.scope);
|
|
556
|
+
const id = (n) => `40000000-0000-4000-8000-${String(n).padStart(12, '0')}`;
|
|
557
|
+
const binding = { schema: 'dharma.local-provider-session-binding/v1', owner: 'dharma_bridge',
|
|
558
|
+
organizationId: 'org_synthetic', repositoryBindingId: id(1), workspaceId: id(2), endpointId: id(3),
|
|
559
|
+
membershipId: id(4), deviceId: id(5), bindingId: id(6), provider: 'codex', sessionId: 'synthetic-thread',
|
|
560
|
+
workspaceRoot: path.join(f.root, 'repo'), createdAt: new Date().toISOString(),
|
|
561
|
+
expiresAt: new Date(Date.now() + 86_400_000).toISOString(), maximumProviderCostCents: 25 };
|
|
562
|
+
const sibling = { ...binding, bindingId: id(7), sessionId: 'synthetic-sibling' };
|
|
563
|
+
await vault.saveProviderSessionBinding(binding);
|
|
564
|
+
const other = await api.open({ root: f.root, masterKey: f.key });
|
|
565
|
+
other.saveProviderSessionBinding(sibling);
|
|
566
|
+
const lease = await vault.tryAcquireProviderSessionLease(binding.bindingId, binding), siblingLease = other.tryAcquireProviderSessionLease(sibling.bindingId, sibling);
|
|
567
|
+
assert.ok(lease);
|
|
568
|
+
assert.ok(siblingLease);
|
|
569
|
+
f.withdraw();
|
|
570
|
+
try {
|
|
571
|
+
await assert.rejects(lease.assertHeld(), { message: 'vault_scope_unavailable' });
|
|
572
|
+
lease.release();
|
|
573
|
+
lease.release();
|
|
574
|
+
await vault.close();
|
|
575
|
+
assert.equal(await siblingLease.assertHeld(), true);
|
|
576
|
+
}
|
|
577
|
+
finally {
|
|
578
|
+
await vault.close();
|
|
579
|
+
siblingLease.release();
|
|
580
|
+
await other.close();
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
test('full scoped vault genuinely expires raw evidence while retaining revised capsules and queue', async (t) => {
|
|
584
|
+
const f = await fixture(t), api = await module(), vault = await api.open({ root: f.root, masterKey: f.key }, f.scope);
|
|
585
|
+
const input = capture();
|
|
586
|
+
input.capsule.plaintext = Buffer.from(JSON.stringify({
|
|
587
|
+
trajectoryId: input.capsule.trajectoryId, revision: 1, capsuleHash: input.capsule.capsuleHash,
|
|
588
|
+
contentIndex: [{ contentId: input.raw.expectedContentId, availableLocally: true }], localEvidenceAvailable: []
|
|
589
|
+
}));
|
|
590
|
+
try {
|
|
591
|
+
await vault.commitCapture(input);
|
|
592
|
+
const result = await vault.enforceRawEvidenceRetention({ now: new Date(Date.now() + 40 * 86_400_000) });
|
|
593
|
+
assert.equal(result.deleted, 1);
|
|
594
|
+
assert.equal((await vault.getLatestCapsule(input.capsule.trajectoryId)).revision, 2);
|
|
595
|
+
assert.equal((await vault.listPendingCapsuleSyncs()).length, 1);
|
|
596
|
+
await assert.rejects(vault.getBlob(input.raw.expectedContentId), { message: 'vault_storage_unavailable' });
|
|
597
|
+
}
|
|
598
|
+
finally {
|
|
599
|
+
await vault.close();
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
//# sourceMappingURL=fullVaultScope.test.js.map
|