@dsh-enhanced/assistant-delivery 0.1.24 → 0.1.30
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 +81 -10
- package/lib/acceptance.d.ts +46 -0
- package/lib/acceptance.d.ts.map +1 -0
- package/lib/acceptance.js +2 -0
- package/lib/acceptance.js.map +1 -0
- package/lib/agent-runtime.d.ts +33 -3
- package/lib/agent-runtime.d.ts.map +1 -1
- package/lib/agent-runtime.js +835 -62
- package/lib/agent-runtime.js.map +1 -1
- package/lib/coordinator.d.ts +4 -1
- package/lib/coordinator.d.ts.map +1 -1
- package/lib/coordinator.js +9 -0
- package/lib/coordinator.js.map +1 -1
- package/lib/feedback-command.d.ts +21 -1
- package/lib/feedback-command.d.ts.map +1 -1
- package/lib/feedback-command.js +18 -0
- package/lib/feedback-command.js.map +1 -1
- package/lib/goal-wake-types.d.ts +93 -0
- package/lib/goal-wake-types.d.ts.map +1 -0
- package/lib/goal-wake-types.js +70 -0
- package/lib/goal-wake-types.js.map +1 -0
- package/lib/inbound-images.d.ts.map +1 -1
- package/lib/inbound-images.js +11 -9
- package/lib/inbound-images.js.map +1 -1
- package/lib/index.d.ts +4 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/native-web-owner.d.ts +78 -0
- package/lib/native-web-owner.d.ts.map +1 -0
- package/lib/native-web-owner.js +291 -0
- package/lib/native-web-owner.js.map +1 -0
- package/lib/operator-snapshot.d.ts +80 -0
- package/lib/operator-snapshot.d.ts.map +1 -0
- package/lib/operator-snapshot.js +935 -0
- package/lib/operator-snapshot.js.map +1 -0
- package/lib/operator.d.ts +2 -0
- package/lib/operator.d.ts.map +1 -1
- package/lib/operator.js +10 -0
- package/lib/operator.js.map +1 -1
- package/lib/service.d.ts +63 -13
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +917 -349
- package/lib/service.js.map +1 -1
- package/lib/session-lease-runtime.d.ts +69 -0
- package/lib/session-lease-runtime.d.ts.map +1 -0
- package/lib/session-lease-runtime.js +395 -0
- package/lib/session-lease-runtime.js.map +1 -0
- package/lib/session-lease-types.d.ts +27 -0
- package/lib/session-lease-types.d.ts.map +1 -0
- package/lib/session-lease-types.js +2 -0
- package/lib/session-lease-types.js.map +1 -0
- package/lib/sqlite.d.ts +1 -1
- package/lib/sqlite.d.ts.map +1 -1
- package/lib/sqlite.js +201 -3
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +133 -4
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +771 -43
- package/lib/store.js.map +1 -1
- package/lib/types.d.ts +9 -2
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +61 -44
|
@@ -0,0 +1,935 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { closeSync, constants, fstatSync, lstatSync, mkdtempSync, openSync, readdirSync, readSync, realpathSync, rmSync, rmdirSync, statSync, unlinkSync, writeSync, } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { dirname, isAbsolute, join, basename, resolve } from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
7
|
+
import { canonicalConversation, canonicalPrincipal } from './canonical.js';
|
|
8
|
+
import { deliverySchemaVersion } from './sqlite.js';
|
|
9
|
+
function unavailable() { return Object.freeze({ status: 'unavailable' }); }
|
|
10
|
+
function mismatch() { return Object.freeze({ status: 'mismatch' }); }
|
|
11
|
+
function policyRef(value) {
|
|
12
|
+
return typeof value === 'string' && value === value.normalize('NFC').trim() && value.length > 0
|
|
13
|
+
&& Buffer.byteLength(value, 'utf8') <= 256 && ![...value].some(character => (character.codePointAt(0) ?? 0) <= 0x1f || character.codePointAt(0) === 0x7f);
|
|
14
|
+
}
|
|
15
|
+
function privateDatabase(path) {
|
|
16
|
+
try {
|
|
17
|
+
if (!isAbsolute(path))
|
|
18
|
+
return undefined;
|
|
19
|
+
const entry = lstatSync(path);
|
|
20
|
+
if (!entry.isFile() || entry.isSymbolicLink() || entry.uid !== process.getuid?.() || (entry.mode & 0o077) !== 0)
|
|
21
|
+
return undefined;
|
|
22
|
+
const parent = realpathSync(dirname(path));
|
|
23
|
+
const resolved = realpathSync(path);
|
|
24
|
+
if (resolved !== join(parent, basename(path)))
|
|
25
|
+
return undefined;
|
|
26
|
+
const directory = statSync(parent);
|
|
27
|
+
if (!directory.isDirectory() || directory.uid !== process.getuid?.() || (directory.mode & 0o077) !== 0)
|
|
28
|
+
return undefined;
|
|
29
|
+
return resolved;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function number(value, min = 0) { return typeof value === 'number' && Number.isSafeInteger(value) && value >= min; }
|
|
36
|
+
function equal(left, right) { return JSON.stringify(left) === JSON.stringify(right); }
|
|
37
|
+
function identifier(value, prefix) { return typeof value === 'string' && new RegExp(`^${prefix}[0-9a-f-]{8,}$`, 'u').test(value); }
|
|
38
|
+
function inspectRow(input, expected, row) {
|
|
39
|
+
if (row.lease_state !== null && row.lease_state !== 'released')
|
|
40
|
+
return Object.freeze({ status: 'busy' });
|
|
41
|
+
const conversation = canonicalConversation(JSON.parse(row.conversation_json));
|
|
42
|
+
const bindingPrincipal = canonicalPrincipal(JSON.parse(row.binding_principal_json));
|
|
43
|
+
const ownerPrincipal = canonicalPrincipal(JSON.parse(row.owner_principal_json));
|
|
44
|
+
const expectedConversation = { channel: 'web', account: expected.account, tenant: expected.tenant, kind: 'dm', chat: input.sessionId };
|
|
45
|
+
if (row.conversation_json !== JSON.stringify(conversation) || row.binding_principal_json !== JSON.stringify(bindingPrincipal) || row.owner_principal_json !== JSON.stringify(ownerPrincipal)
|
|
46
|
+
|| !equal(conversation, expectedConversation) || row.session_id !== input.sessionId || !identifier(row.binding_id, 'binding_') || !identifier(row.owner_id, 'principal_')
|
|
47
|
+
|| bindingPrincipal.channel !== 'web' || !equal(bindingPrincipal, expected) || !equal(ownerPrincipal, expected)
|
|
48
|
+
|| row.binding_status !== 'active' || row.owner_role !== 'owner' || row.owner_status !== 'active' || row.linked_to_id !== null
|
|
49
|
+
|| row.workspace !== input.workspace || row.agent_preset !== input.agentPreset || !policyRef(row.policy_ref)
|
|
50
|
+
|| ![row.generation, row.binding_version, row.owner_version].every(value => number(value, 1))
|
|
51
|
+
|| ![row.binding_created_at, row.binding_updated_at, row.owner_created_at, row.owner_updated_at].every(value => number(value)))
|
|
52
|
+
return mismatch();
|
|
53
|
+
const binding = Object.freeze({ id: row.binding_id, conversation: Object.freeze(conversation), principal: Object.freeze(bindingPrincipal), workspace: row.workspace,
|
|
54
|
+
agentPreset: row.agent_preset, sessionId: row.session_id, generation: row.generation, policyRef: row.policy_ref, status: 'active', createdAt: row.binding_created_at, updatedAt: row.binding_updated_at, version: row.binding_version });
|
|
55
|
+
const owner = Object.freeze({ id: row.owner_id, principal: Object.freeze(ownerPrincipal), role: 'owner', status: 'active', createdAt: row.owner_created_at, updatedAt: row.owner_updated_at, version: row.owner_version });
|
|
56
|
+
return Object.freeze({ status: 'matched', snapshot: Object.freeze({ binding, owner }) });
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Opens an already-existing Delivery database read-only. It never invokes the Delivery migrator,
|
|
60
|
+
* creates a database, pairs an owner, reads message content, or acquires a Session lease.
|
|
61
|
+
*/
|
|
62
|
+
export function inspectActiveWebOwnerBindingLocally(input) {
|
|
63
|
+
let expected;
|
|
64
|
+
try {
|
|
65
|
+
expected = canonicalPrincipal(input.expectedPrincipal);
|
|
66
|
+
if (expected.channel !== 'web' || !isAbsolute(input.workspace) || typeof input.agentPreset !== 'string' || input.agentPreset.length === 0 || typeof input.sessionId !== 'string' || input.sessionId.length === 0)
|
|
67
|
+
return mismatch();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return mismatch();
|
|
71
|
+
}
|
|
72
|
+
const path = privateDatabase(input.databasePath);
|
|
73
|
+
if (path === undefined)
|
|
74
|
+
return unavailable();
|
|
75
|
+
let database;
|
|
76
|
+
try {
|
|
77
|
+
database = new DatabaseSync(path, { readOnly: true });
|
|
78
|
+
database.exec('PRAGMA query_only = ON; PRAGMA busy_timeout = 100');
|
|
79
|
+
database.exec('BEGIN');
|
|
80
|
+
const version = database.prepare('PRAGMA user_version').get();
|
|
81
|
+
if (version.user_version !== deliverySchemaVersion)
|
|
82
|
+
return unavailable();
|
|
83
|
+
const rows = database.prepare(`
|
|
84
|
+
SELECT binding.id AS binding_id, binding.conversation_json, binding.principal_json AS binding_principal_json,
|
|
85
|
+
binding.workspace, binding.agent_preset, binding.session_id, binding.generation, binding.policy_ref,
|
|
86
|
+
binding.status AS binding_status, binding.created_at AS binding_created_at, binding.updated_at AS binding_updated_at, binding.version AS binding_version,
|
|
87
|
+
owner.id AS owner_id, owner.principal_json AS owner_principal_json, owner.role AS owner_role, owner.status AS owner_status,
|
|
88
|
+
owner.linked_to_id, owner.created_at AS owner_created_at, owner.updated_at AS owner_updated_at, owner.version AS owner_version,
|
|
89
|
+
lease.state AS lease_state
|
|
90
|
+
FROM conversation_bindings AS binding
|
|
91
|
+
JOIN delivery_principals AS owner ON owner.id = binding.principal_id
|
|
92
|
+
LEFT JOIN delivery_session_leases AS lease ON lease.session_id = binding.session_id
|
|
93
|
+
WHERE binding.session_id = ?
|
|
94
|
+
LIMIT 2
|
|
95
|
+
`).all(input.sessionId);
|
|
96
|
+
if (rows.length !== 1)
|
|
97
|
+
return mismatch();
|
|
98
|
+
const row = rows[0];
|
|
99
|
+
return inspectRow(input, expected, row);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (error instanceof Error && /(?:busy|locked)/iu.test(error.message))
|
|
103
|
+
return Object.freeze({ status: 'busy' });
|
|
104
|
+
return unavailable();
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
try {
|
|
108
|
+
database?.exec('ROLLBACK');
|
|
109
|
+
}
|
|
110
|
+
catch { }
|
|
111
|
+
try {
|
|
112
|
+
database?.close();
|
|
113
|
+
}
|
|
114
|
+
catch { }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** Bounded operator discovery in one read-only snapshot; it creates no sessions or bindings. */
|
|
118
|
+
export function listActiveIdleWebOwnerBindingsLocally(input) {
|
|
119
|
+
let expected;
|
|
120
|
+
try {
|
|
121
|
+
expected = canonicalPrincipal(input.expectedPrincipal);
|
|
122
|
+
if (expected.channel !== 'web' || !isAbsolute(input.workspace) || typeof input.agentPreset !== 'string' || !input.agentPreset)
|
|
123
|
+
return { status: 'unavailable', reason: 'invalid-scope' };
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return { status: 'unavailable', reason: 'invalid-scope' };
|
|
127
|
+
}
|
|
128
|
+
const path = privateDatabase(input.databasePath);
|
|
129
|
+
if (!path)
|
|
130
|
+
return { status: 'unavailable', reason: 'snapshot-unavailable' };
|
|
131
|
+
let database;
|
|
132
|
+
try {
|
|
133
|
+
database = new DatabaseSync(path, { readOnly: true });
|
|
134
|
+
database.exec('PRAGMA query_only = ON; PRAGMA busy_timeout = 100; BEGIN');
|
|
135
|
+
if (database.prepare('PRAGMA user_version').get().user_version !== deliverySchemaVersion)
|
|
136
|
+
return { status: 'unavailable', reason: 'snapshot-unavailable' };
|
|
137
|
+
const rows = database.prepare(`
|
|
138
|
+
SELECT binding.id AS binding_id, binding.conversation_json, binding.principal_json AS binding_principal_json,
|
|
139
|
+
binding.workspace, binding.agent_preset, binding.session_id, binding.generation, binding.policy_ref,
|
|
140
|
+
binding.status AS binding_status, binding.created_at AS binding_created_at, binding.updated_at AS binding_updated_at, binding.version AS binding_version,
|
|
141
|
+
owner.id AS owner_id, owner.principal_json AS owner_principal_json, owner.role AS owner_role, owner.status AS owner_status,
|
|
142
|
+
owner.linked_to_id, owner.created_at AS owner_created_at, owner.updated_at AS owner_updated_at, owner.version AS owner_version,
|
|
143
|
+
lease.state AS lease_state
|
|
144
|
+
FROM conversation_bindings AS binding
|
|
145
|
+
JOIN delivery_principals AS owner ON owner.id = binding.principal_id
|
|
146
|
+
LEFT JOIN delivery_session_leases AS lease ON lease.session_id = binding.session_id
|
|
147
|
+
WHERE owner.principal_json = ? AND binding.workspace = ? AND binding.agent_preset = ?
|
|
148
|
+
AND owner.role = 'owner' AND owner.status = 'active' AND binding.status = 'active'
|
|
149
|
+
AND (lease.state IS NULL OR lease.state = 'released')
|
|
150
|
+
ORDER BY binding.session_id LIMIT 101
|
|
151
|
+
`).all(JSON.stringify(expected), input.workspace, input.agentPreset);
|
|
152
|
+
if (rows.length > 100)
|
|
153
|
+
return { status: 'unavailable', reason: 'too-many-sessions' };
|
|
154
|
+
const snapshots = [];
|
|
155
|
+
const seen = new Set();
|
|
156
|
+
for (const row of rows) {
|
|
157
|
+
if (seen.has(row.session_id))
|
|
158
|
+
return { status: 'unavailable', reason: 'snapshot-unavailable' };
|
|
159
|
+
const checked = inspectRow({ ...input, sessionId: row.session_id }, expected, row);
|
|
160
|
+
if (checked.status !== 'matched')
|
|
161
|
+
return { status: 'unavailable', reason: 'snapshot-unavailable' };
|
|
162
|
+
seen.add(row.session_id);
|
|
163
|
+
snapshots.push(checked.snapshot);
|
|
164
|
+
}
|
|
165
|
+
return Object.freeze({ status: 'matched', snapshots: Object.freeze(snapshots) });
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return { status: 'unavailable', reason: 'snapshot-unavailable' };
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
try {
|
|
172
|
+
database?.exec('ROLLBACK');
|
|
173
|
+
}
|
|
174
|
+
catch { }
|
|
175
|
+
try {
|
|
176
|
+
database?.close();
|
|
177
|
+
}
|
|
178
|
+
catch { }
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
export const activeLarkOwnerBindingsSnapshotProtocol = 'assistant-delivery/active-lark-owner-bindings-snapshot/v1';
|
|
182
|
+
export class ActiveLarkOwnerBindingsSnapshotError extends Error {
|
|
183
|
+
code;
|
|
184
|
+
constructor(code, message) {
|
|
185
|
+
super(message);
|
|
186
|
+
this.code = code;
|
|
187
|
+
this.name = 'ActiveLarkOwnerBindingsSnapshotError';
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const operatorDigestPattern = /^[a-f0-9]{64}$/u;
|
|
191
|
+
const operatorKeyPattern = /^[A-Za-z0-9][A-Za-z0-9._@/-]{0,255}$/u;
|
|
192
|
+
const operatorMaximumBindings = 100;
|
|
193
|
+
const operatorBufferBytes = 64 * 1024;
|
|
194
|
+
function operatorFail(code, message) {
|
|
195
|
+
throw new ActiveLarkOwnerBindingsSnapshotError(code, `delivery Lark owner snapshot: ${message}`);
|
|
196
|
+
}
|
|
197
|
+
function operatorFreeze(value) {
|
|
198
|
+
if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
199
|
+
for (const child of Object.values(value))
|
|
200
|
+
operatorFreeze(child);
|
|
201
|
+
Object.freeze(value);
|
|
202
|
+
}
|
|
203
|
+
return value;
|
|
204
|
+
}
|
|
205
|
+
function operatorCanonicalJson(value) {
|
|
206
|
+
if (value === null || typeof value !== 'object')
|
|
207
|
+
return JSON.stringify(value);
|
|
208
|
+
if (Array.isArray(value))
|
|
209
|
+
return `[${value.map(operatorCanonicalJson).join(',')}]`;
|
|
210
|
+
const record = value;
|
|
211
|
+
return `{${Object.keys(record).sort().map(key => `${JSON.stringify(key)}:${operatorCanonicalJson(record[key])}`).join(',')}}`;
|
|
212
|
+
}
|
|
213
|
+
function operatorSha256(value) {
|
|
214
|
+
return createHash('sha256').update(value).digest('hex');
|
|
215
|
+
}
|
|
216
|
+
function operatorSameFile(left, right) {
|
|
217
|
+
return left.dev === right.dev && left.ino === right.ino && left.size === right.size
|
|
218
|
+
&& left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs
|
|
219
|
+
&& left.mode === right.mode && left.uid === right.uid && left.nlink === right.nlink;
|
|
220
|
+
}
|
|
221
|
+
function operatorDigestDescriptor(descriptor, size) {
|
|
222
|
+
const hash = createHash('sha256');
|
|
223
|
+
const buffer = Buffer.allocUnsafe(operatorBufferBytes);
|
|
224
|
+
let position = 0n;
|
|
225
|
+
while (position < size) {
|
|
226
|
+
const remaining = size - position;
|
|
227
|
+
const length = Number(remaining > BigInt(buffer.length) ? BigInt(buffer.length) : remaining);
|
|
228
|
+
const count = readSync(descriptor, buffer, 0, length, Number(position));
|
|
229
|
+
if (count <= 0)
|
|
230
|
+
operatorFail('database-drift', 'a storage file changed while it was hashed');
|
|
231
|
+
hash.update(buffer.subarray(0, count));
|
|
232
|
+
position += BigInt(count);
|
|
233
|
+
}
|
|
234
|
+
return hash.digest('hex');
|
|
235
|
+
}
|
|
236
|
+
function operatorFileIdentity(file) {
|
|
237
|
+
return operatorFreeze({
|
|
238
|
+
device: file.stat.dev.toString(), inode: file.stat.ino.toString(), size: file.stat.size.toString(),
|
|
239
|
+
mtimeNs: file.stat.mtimeNs.toString(), digest: file.digest,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
function operatorPinPrivateParent(path) {
|
|
243
|
+
let descriptor;
|
|
244
|
+
try {
|
|
245
|
+
descriptor = openSync(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
if (error.code === 'ENOENT') {
|
|
249
|
+
operatorFail('database-missing', 'database parent disappeared before the snapshot');
|
|
250
|
+
}
|
|
251
|
+
operatorFail('unsafe-parent', 'database parent is unavailable');
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
const pathname = lstatSync(path, { bigint: true });
|
|
255
|
+
const metadata = fstatSync(descriptor, { bigint: true });
|
|
256
|
+
const uid = process.getuid?.();
|
|
257
|
+
const physical = realpathSync(path);
|
|
258
|
+
if (!pathname.isDirectory() || pathname.isSymbolicLink() || !metadata.isDirectory()
|
|
259
|
+
|| pathname.dev !== metadata.dev || pathname.ino !== metadata.ino || physical !== path
|
|
260
|
+
|| uid === undefined || metadata.uid !== BigInt(uid)
|
|
261
|
+
|| (metadata.mode & 3647n) !== 0n || (metadata.mode & 320n) !== 320n) {
|
|
262
|
+
operatorFail('unsafe-parent', 'database parent must be a private owner directory without symlinks');
|
|
263
|
+
}
|
|
264
|
+
const pinned = { path, descriptor, device: metadata.dev, inode: metadata.ino, mode: metadata.mode };
|
|
265
|
+
descriptor = undefined;
|
|
266
|
+
return pinned;
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
if (error instanceof ActiveLarkOwnerBindingsSnapshotError)
|
|
270
|
+
throw error;
|
|
271
|
+
operatorFail('unsafe-parent', 'database parent cannot be resolved');
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
if (descriptor !== undefined)
|
|
275
|
+
closeSync(descriptor);
|
|
276
|
+
}
|
|
277
|
+
throw new Error('unreachable');
|
|
278
|
+
}
|
|
279
|
+
function operatorPinnedParentMatches(parent) {
|
|
280
|
+
try {
|
|
281
|
+
const pathname = lstatSync(parent.path, { bigint: true });
|
|
282
|
+
const descriptor = fstatSync(parent.descriptor, { bigint: true });
|
|
283
|
+
return pathname.isDirectory() && !pathname.isSymbolicLink() && descriptor.isDirectory()
|
|
284
|
+
&& pathname.dev === parent.device && pathname.ino === parent.inode
|
|
285
|
+
&& descriptor.dev === parent.device && descriptor.ino === parent.inode
|
|
286
|
+
&& descriptor.mode === parent.mode;
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// Inode binding is enforced through Linux /proc/self/fd. On other platforms the
|
|
293
|
+
// snapshot keeps its historical path-based pinning; CI still exercises that code.
|
|
294
|
+
const operatorInodeBindSupported = process.platform === 'linux';
|
|
295
|
+
const operatorProcSelfFd = '/proc/self/fd';
|
|
296
|
+
/**
|
|
297
|
+
* Enumerate the regular-file inodes held by THIS process, excluding the
|
|
298
|
+
* descriptors the snapshot pinned itself. After the exclusion the only
|
|
299
|
+
* remaining database fds are SQLite's, so a (dev,ino) match proves the
|
|
300
|
+
* connection genuinely opened the pinned inode rather than an attacker's
|
|
301
|
+
* same-name replacement. An attacker in another process cannot plant an fd
|
|
302
|
+
* here because /proc/self/fd never lists another process's descriptors.
|
|
303
|
+
*/
|
|
304
|
+
function operatorConnectionInodes(exclude) {
|
|
305
|
+
let entries;
|
|
306
|
+
try {
|
|
307
|
+
entries = readdirSync(operatorProcSelfFd);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
operatorFail('database-corrupt', 'cannot inspect the database connection file descriptors');
|
|
311
|
+
}
|
|
312
|
+
const inodes = [];
|
|
313
|
+
for (const entry of entries) {
|
|
314
|
+
const descriptor = Number(entry);
|
|
315
|
+
if (!Number.isInteger(descriptor) || descriptor <= 2 || exclude.has(descriptor))
|
|
316
|
+
continue;
|
|
317
|
+
let stat;
|
|
318
|
+
try {
|
|
319
|
+
stat = fstatSync(descriptor, { bigint: true });
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
continue;
|
|
323
|
+
} // transient/closed fd
|
|
324
|
+
if (stat.isFile())
|
|
325
|
+
inodes.push({ device: stat.dev, inode: stat.ino });
|
|
326
|
+
}
|
|
327
|
+
return inodes;
|
|
328
|
+
}
|
|
329
|
+
function operatorAssertConnectionBound(files, exclude) {
|
|
330
|
+
const held = operatorConnectionInodes(exclude);
|
|
331
|
+
for (const file of files) {
|
|
332
|
+
if (!held.some(inode => inode.device === file.stat.dev && inode.inode === file.stat.ino)) {
|
|
333
|
+
operatorFail('database-drift', 'database connection is not bound to the pinned inode');
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/** Pin the private mkdtemp copy directory itself so a same-name swap cannot retarget the copies. */
|
|
338
|
+
function operatorPinTempDirectory(path) {
|
|
339
|
+
let descriptor;
|
|
340
|
+
try {
|
|
341
|
+
descriptor = openSync(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
342
|
+
const linked = lstatSync(path, { bigint: true });
|
|
343
|
+
const opened = fstatSync(descriptor, { bigint: true });
|
|
344
|
+
const uid = process.getuid?.();
|
|
345
|
+
if (!linked.isDirectory() || linked.isSymbolicLink() || !opened.isDirectory()
|
|
346
|
+
|| linked.dev !== opened.dev || linked.ino !== opened.ino
|
|
347
|
+
|| (opened.mode & 4095n) !== 448n
|
|
348
|
+
|| uid === undefined || opened.uid !== BigInt(uid)) {
|
|
349
|
+
operatorFail('unsafe-path', 'temporary snapshot directory cannot be pinned');
|
|
350
|
+
}
|
|
351
|
+
const pinned = { path, descriptor, device: opened.dev, inode: opened.ino };
|
|
352
|
+
descriptor = undefined;
|
|
353
|
+
return pinned;
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
if (error instanceof ActiveLarkOwnerBindingsSnapshotError)
|
|
357
|
+
throw error;
|
|
358
|
+
operatorFail('unsafe-path', 'temporary snapshot directory cannot be pinned');
|
|
359
|
+
}
|
|
360
|
+
finally {
|
|
361
|
+
if (descriptor !== undefined)
|
|
362
|
+
try {
|
|
363
|
+
closeSync(descriptor);
|
|
364
|
+
}
|
|
365
|
+
catch { }
|
|
366
|
+
}
|
|
367
|
+
throw new Error('unreachable');
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Materialise one private byte-for-byte copy THROUGH the pinned directory fd
|
|
371
|
+
* (`/proc/self/fd/<dirfd>/<name>`), then reopen it O_NOFOLLOW through the same
|
|
372
|
+
* anchor and keep that descriptor open. Neither the create, the verify, the
|
|
373
|
+
* SQLite open, or the cleanup ever resolve a swappable path component, so a
|
|
374
|
+
* same-name directory swap cannot retarget the replica. The returned file is
|
|
375
|
+
* left open and must be closed by the caller.
|
|
376
|
+
*/
|
|
377
|
+
function operatorCopyAndPin(source, targetRef) {
|
|
378
|
+
const buffer = Buffer.allocUnsafe(operatorBufferBytes);
|
|
379
|
+
let destination;
|
|
380
|
+
try {
|
|
381
|
+
destination = openSync(targetRef, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600);
|
|
382
|
+
let position = 0n;
|
|
383
|
+
while (position < source.stat.size) {
|
|
384
|
+
const remaining = source.stat.size - position;
|
|
385
|
+
const length = Number(remaining > BigInt(buffer.length) ? BigInt(buffer.length) : remaining);
|
|
386
|
+
const count = readSync(source.descriptor, buffer, 0, length, Number(position));
|
|
387
|
+
if (count <= 0)
|
|
388
|
+
operatorFail('database-drift', `${basename(source.path)} changed while it was copied`);
|
|
389
|
+
let written = 0;
|
|
390
|
+
while (written < count)
|
|
391
|
+
written += writeSync(destination, buffer, written, count - written);
|
|
392
|
+
position += BigInt(count);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
if (destination !== undefined)
|
|
397
|
+
try {
|
|
398
|
+
closeSync(destination);
|
|
399
|
+
}
|
|
400
|
+
catch { }
|
|
401
|
+
if (error instanceof ActiveLarkOwnerBindingsSnapshotError)
|
|
402
|
+
throw error;
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
if (destination !== undefined)
|
|
406
|
+
closeSync(destination);
|
|
407
|
+
const copied = operatorOpenPrivateFile(targetRef, false);
|
|
408
|
+
if (copied.stat.size !== source.stat.size || copied.digest !== source.digest) {
|
|
409
|
+
try {
|
|
410
|
+
closeSync(copied.descriptor);
|
|
411
|
+
}
|
|
412
|
+
catch { }
|
|
413
|
+
operatorFail('database-drift', `${basename(source.path)} copy does not match its pinned source`);
|
|
414
|
+
}
|
|
415
|
+
return copied;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Remove the replica THROUGH the pinned directory fd, unlinking only the fixed
|
|
419
|
+
* names SQLite could create. The directory itself is rmdir'd by path only after
|
|
420
|
+
* the pinned fd proves the path still names the same empty inode; a swapped or
|
|
421
|
+
* non-empty directory is left untouched and the snapshot fails closed rather
|
|
422
|
+
* than recursively deleting an attacker-controlled replacement.
|
|
423
|
+
*/
|
|
424
|
+
function operatorRemovePinnedTemp(pinned, names) {
|
|
425
|
+
const anchor = `${operatorProcSelfFd}/${pinned.descriptor}`;
|
|
426
|
+
for (const name of names) {
|
|
427
|
+
try {
|
|
428
|
+
unlinkSync(`${anchor}/${name}`);
|
|
429
|
+
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
if (error.code !== 'ENOENT') {
|
|
432
|
+
operatorFail('database-drift', 'temporary snapshot copy cannot be removed through its pinned directory');
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
let entries;
|
|
437
|
+
try {
|
|
438
|
+
entries = readdirSync(anchor);
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
operatorFail('database-drift', 'temporary snapshot directory became unavailable');
|
|
442
|
+
}
|
|
443
|
+
if (entries.length !== 0)
|
|
444
|
+
operatorFail('database-drift', 'temporary snapshot directory contains unexpected entries');
|
|
445
|
+
const linked = lstatSync(pinned.path, { bigint: true });
|
|
446
|
+
const opened = fstatSync(pinned.descriptor, { bigint: true });
|
|
447
|
+
if (linked.dev !== pinned.device || linked.ino !== pinned.inode
|
|
448
|
+
|| opened.dev !== pinned.device || opened.ino !== pinned.inode) {
|
|
449
|
+
operatorFail('database-drift', 'temporary snapshot directory was retargeted before cleanup');
|
|
450
|
+
}
|
|
451
|
+
closeSync(pinned.descriptor);
|
|
452
|
+
try {
|
|
453
|
+
rmdirSync(pinned.path);
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
if (error.code !== 'ENOENT') {
|
|
457
|
+
operatorFail('database-drift', 'temporary snapshot directory could not be removed');
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
function operatorOpenPrivateFile(path, missing) {
|
|
462
|
+
let linked;
|
|
463
|
+
try {
|
|
464
|
+
linked = lstatSync(path, { bigint: true });
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
if (missing && error.code === 'ENOENT')
|
|
468
|
+
operatorFail('database-missing', 'database does not exist');
|
|
469
|
+
operatorFail('unsafe-path', `${basename(path)} cannot be inspected`);
|
|
470
|
+
}
|
|
471
|
+
const uid = process.getuid?.();
|
|
472
|
+
if (!linked.isFile() || linked.isSymbolicLink() || linked.nlink !== 1n
|
|
473
|
+
|| (linked.mode & 4095n) !== 384n
|
|
474
|
+
|| (uid !== undefined && linked.uid !== BigInt(uid))
|
|
475
|
+
|| linked.size > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
476
|
+
operatorFail('unsafe-path', `${basename(path)} must be one private owned regular file`);
|
|
477
|
+
}
|
|
478
|
+
let descriptor;
|
|
479
|
+
try {
|
|
480
|
+
descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
operatorFail('unsafe-path', `${basename(path)} cannot be opened safely`);
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
const opened = fstatSync(descriptor, { bigint: true });
|
|
487
|
+
const after = lstatSync(path, { bigint: true });
|
|
488
|
+
if (!operatorSameFile(linked, opened) || !operatorSameFile(opened, after)) {
|
|
489
|
+
operatorFail('database-drift', `${basename(path)} changed while it was opened`);
|
|
490
|
+
}
|
|
491
|
+
const digest = operatorDigestDescriptor(descriptor, opened.size);
|
|
492
|
+
if (!operatorSameFile(opened, fstatSync(descriptor, { bigint: true }))) {
|
|
493
|
+
operatorFail('database-drift', `${basename(path)} changed while it was hashed`);
|
|
494
|
+
}
|
|
495
|
+
return { path, descriptor, stat: opened, digest };
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
closeSync(descriptor);
|
|
499
|
+
throw error;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
function operatorOptionalFile(path) {
|
|
503
|
+
try {
|
|
504
|
+
lstatSync(path);
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
if (error.code === 'ENOENT')
|
|
508
|
+
return null;
|
|
509
|
+
operatorFail('unsafe-path', `${basename(path)} cannot be inspected`);
|
|
510
|
+
}
|
|
511
|
+
return operatorOpenPrivateFile(path, false);
|
|
512
|
+
}
|
|
513
|
+
function operatorPinnedFileIdentityMatches(file) {
|
|
514
|
+
try {
|
|
515
|
+
const pathname = lstatSync(file.path, { bigint: true });
|
|
516
|
+
const opened = fstatSync(file.descriptor, { bigint: true });
|
|
517
|
+
return pathname.isFile() && !pathname.isSymbolicLink()
|
|
518
|
+
&& pathname.dev === file.stat.dev && pathname.ino === file.stat.ino
|
|
519
|
+
&& opened.dev === file.stat.dev && opened.ino === file.stat.ino;
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function operatorAssertUnchanged(file) {
|
|
526
|
+
try {
|
|
527
|
+
const linked = lstatSync(file.path, { bigint: true });
|
|
528
|
+
const opened = fstatSync(file.descriptor, { bigint: true });
|
|
529
|
+
if (!operatorSameFile(file.stat, linked) || !operatorSameFile(file.stat, opened)
|
|
530
|
+
|| operatorDigestDescriptor(file.descriptor, opened.size) !== file.digest) {
|
|
531
|
+
operatorFail('database-drift', `${basename(file.path)} changed during the snapshot`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
if (error instanceof ActiveLarkOwnerBindingsSnapshotError)
|
|
536
|
+
throw error;
|
|
537
|
+
operatorFail('database-drift', `${basename(file.path)} changed during the snapshot`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
function operatorAssertSidecar(path, file) {
|
|
541
|
+
if (file !== null)
|
|
542
|
+
return operatorAssertUnchanged(file);
|
|
543
|
+
try {
|
|
544
|
+
lstatSync(path);
|
|
545
|
+
operatorFail('database-drift', `${basename(path)} appeared during the snapshot`);
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
if (error instanceof ActiveLarkOwnerBindingsSnapshotError)
|
|
549
|
+
throw error;
|
|
550
|
+
if (error.code !== 'ENOENT') {
|
|
551
|
+
operatorFail('database-drift', `${basename(path)} changed during the snapshot`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function operatorDatabasePath(path) {
|
|
556
|
+
if (typeof path !== 'string' || !isAbsolute(path) || resolve(path) !== path || path === '/'
|
|
557
|
+
|| path !== path.normalize('NFC') || /[\p{Cc}]/u.test(path)) {
|
|
558
|
+
operatorFail('invalid-path', 'database path must be canonical and absolute');
|
|
559
|
+
}
|
|
560
|
+
let physical;
|
|
561
|
+
try {
|
|
562
|
+
physical = realpathSync(path);
|
|
563
|
+
}
|
|
564
|
+
catch (error) {
|
|
565
|
+
if (error.code === 'ENOENT')
|
|
566
|
+
operatorFail('database-missing', 'database does not exist');
|
|
567
|
+
operatorFail('unsafe-path', 'database path cannot be resolved');
|
|
568
|
+
}
|
|
569
|
+
let parentPhysical;
|
|
570
|
+
try {
|
|
571
|
+
parentPhysical = realpathSync(dirname(path));
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
if (error.code === 'ENOENT')
|
|
575
|
+
operatorFail('database-missing', 'database parent disappeared');
|
|
576
|
+
operatorFail('unsafe-path', 'database parent cannot be resolved');
|
|
577
|
+
}
|
|
578
|
+
if (physical !== path || parentPhysical !== dirname(path)) {
|
|
579
|
+
operatorFail('unsafe-path', 'database path must not traverse a symbolic link');
|
|
580
|
+
}
|
|
581
|
+
return physical;
|
|
582
|
+
}
|
|
583
|
+
function operatorScope(input) {
|
|
584
|
+
const validKey = (value) => typeof value === 'string' && value === value.normalize('NFC').trim()
|
|
585
|
+
&& operatorKeyPattern.test(value);
|
|
586
|
+
const validText = (value, maximum) => typeof value === 'string' && value.length > 0
|
|
587
|
+
&& value === value.normalize('NFC').trim() && Buffer.byteLength(value, 'utf8') <= maximum
|
|
588
|
+
&& !/[\p{Cc}]/u.test(value);
|
|
589
|
+
if (!validKey(input.account) || !validKey(input.tenant)
|
|
590
|
+
|| !validText(input.workspace, 4_096) || !isAbsolute(input.workspace) || resolve(input.workspace) !== input.workspace
|
|
591
|
+
|| !validText(input.agentPreset, 128))
|
|
592
|
+
operatorFail('invalid-scope', 'query scope is invalid');
|
|
593
|
+
return operatorFreeze({ account: input.account, tenant: input.tenant, workspace: input.workspace, agentPreset: input.agentPreset });
|
|
594
|
+
}
|
|
595
|
+
function operatorCopyFile(source, target) {
|
|
596
|
+
const buffer = Buffer.allocUnsafe(operatorBufferBytes);
|
|
597
|
+
let destination;
|
|
598
|
+
try {
|
|
599
|
+
destination = openSync(target, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600);
|
|
600
|
+
let position = 0n;
|
|
601
|
+
while (position < source.stat.size) {
|
|
602
|
+
const remaining = source.stat.size - position;
|
|
603
|
+
const length = Number(remaining > BigInt(buffer.length) ? BigInt(buffer.length) : remaining);
|
|
604
|
+
const count = readSync(source.descriptor, buffer, 0, length, Number(position));
|
|
605
|
+
if (count <= 0)
|
|
606
|
+
operatorFail('database-drift', `${basename(source.path)} changed while it was copied`);
|
|
607
|
+
let written = 0;
|
|
608
|
+
while (written < count)
|
|
609
|
+
written += writeSync(destination, buffer, written, count - written);
|
|
610
|
+
position += BigInt(count);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
finally {
|
|
614
|
+
if (destination !== undefined)
|
|
615
|
+
closeSync(destination);
|
|
616
|
+
}
|
|
617
|
+
// Close the write window, then re-open the private copy as an independent
|
|
618
|
+
// file and prove its bytes equal the pinned source before it is ever read.
|
|
619
|
+
const copied = operatorOpenPrivateFile(target, false);
|
|
620
|
+
try {
|
|
621
|
+
if (copied.stat.size !== source.stat.size || copied.digest !== source.digest) {
|
|
622
|
+
operatorFail('database-drift', `${basename(source.path)} copy does not match its pinned source`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
finally {
|
|
626
|
+
closeSync(copied.descriptor);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
function operatorText(value, maximum) {
|
|
630
|
+
return typeof value === 'string' && value.length > 0 && value === value.normalize('NFC').trim()
|
|
631
|
+
&& Buffer.byteLength(value, 'utf8') <= maximum && !/[\p{Cc}]/u.test(value);
|
|
632
|
+
}
|
|
633
|
+
function operatorInteger(value, minimum = 0) {
|
|
634
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= minimum;
|
|
635
|
+
}
|
|
636
|
+
function operatorBinding(row, scope) {
|
|
637
|
+
let conversation;
|
|
638
|
+
let principal;
|
|
639
|
+
try {
|
|
640
|
+
if (typeof row.conversation_json !== 'string' || typeof row.binding_principal_json !== 'string'
|
|
641
|
+
|| typeof row.owner_principal_json !== 'string')
|
|
642
|
+
throw new Error('invalid JSON metadata');
|
|
643
|
+
conversation = canonicalConversation(JSON.parse(row.conversation_json));
|
|
644
|
+
principal = canonicalPrincipal(JSON.parse(row.binding_principal_json));
|
|
645
|
+
const ownerPrincipal = canonicalPrincipal(JSON.parse(row.owner_principal_json));
|
|
646
|
+
if (row.conversation_json !== JSON.stringify(conversation)
|
|
647
|
+
|| row.binding_principal_json !== JSON.stringify(principal)
|
|
648
|
+
|| row.owner_principal_json !== JSON.stringify(ownerPrincipal)
|
|
649
|
+
|| !equal(principal, ownerPrincipal))
|
|
650
|
+
throw new Error('non-canonical identity metadata');
|
|
651
|
+
}
|
|
652
|
+
catch {
|
|
653
|
+
return operatorFail('database-corrupt', 'a matching binding has invalid canonical identity metadata');
|
|
654
|
+
}
|
|
655
|
+
if (conversation.channel !== 'lark' || conversation.account !== scope.account
|
|
656
|
+
|| conversation.tenant !== scope.tenant || conversation.kind !== 'dm' || conversation.thread !== undefined
|
|
657
|
+
|| principal.channel !== 'lark' || principal.account !== scope.account || principal.tenant !== scope.tenant
|
|
658
|
+
|| row.workspace !== scope.workspace || row.agent_preset !== scope.agentPreset
|
|
659
|
+
|| row.binding_status !== 'active' || row.owner_role !== 'owner' || row.owner_status !== 'active'
|
|
660
|
+
|| row.linked_to_id !== null || !identifier(row.binding_id, 'binding_') || !identifier(row.owner_id, 'principal_')
|
|
661
|
+
|| row.principal_id !== row.owner_id || !operatorDigestPattern.test(String(row.conversation_hash))
|
|
662
|
+
|| !operatorDigestPattern.test(String(row.owner_key_hash))
|
|
663
|
+
|| row.conversation_hash !== operatorSha256(row.conversation_json)
|
|
664
|
+
|| row.owner_key_hash !== operatorSha256(row.owner_principal_json)
|
|
665
|
+
|| !operatorText(row.session_id, 512) || !operatorText(row.policy_ref, 256)
|
|
666
|
+
|| !operatorInteger(row.generation, 1) || !operatorInteger(row.binding_version, 1)
|
|
667
|
+
|| !operatorInteger(row.owner_version, 1) || !operatorInteger(row.binding_created_at)
|
|
668
|
+
|| !operatorInteger(row.binding_updated_at) || row.binding_updated_at < row.binding_created_at
|
|
669
|
+
|| !operatorInteger(row.owner_created_at) || !operatorInteger(row.owner_updated_at)
|
|
670
|
+
|| row.owner_updated_at < row.owner_created_at) {
|
|
671
|
+
operatorFail('database-corrupt', 'a matching binding row is inconsistent');
|
|
672
|
+
}
|
|
673
|
+
const owner = { id: row.owner_id, principal, role: 'owner', status: 'active',
|
|
674
|
+
createdAt: row.owner_created_at, updatedAt: row.owner_updated_at, version: row.owner_version };
|
|
675
|
+
return operatorFreeze({
|
|
676
|
+
id: row.binding_id, conversation, principal, workspace: row.workspace, agentPreset: row.agent_preset,
|
|
677
|
+
sessionId: row.session_id, generation: row.generation, policyRef: row.policy_ref, status: 'active',
|
|
678
|
+
createdAt: row.binding_created_at, updatedAt: row.binding_updated_at, version: row.binding_version, owner,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
function operatorClassifySqlite(error) {
|
|
682
|
+
const message = error instanceof Error ? error.message.toLowerCase() : '';
|
|
683
|
+
const code = error.errcode;
|
|
684
|
+
if (code === 5 || code === 6 || /(?:busy|locked)/u.test(message))
|
|
685
|
+
operatorFail('database-busy', 'database is busy');
|
|
686
|
+
operatorFail('database-corrupt', 'database cannot produce a valid snapshot');
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Produces one exact, content-free Lark owner-DM inventory without opening a
|
|
690
|
+
* writable connection to the source. A temporary private byte-for-byte view
|
|
691
|
+
* is used when WAL state exists so SQLite cannot update the source WAL index.
|
|
692
|
+
*/
|
|
693
|
+
export function inspectActiveLarkOwnerBindingsLocally(input) {
|
|
694
|
+
const scope = operatorScope(input);
|
|
695
|
+
const path = operatorDatabasePath(input.databasePath);
|
|
696
|
+
// Pin the private parent directory for the whole snapshot so an ancestor
|
|
697
|
+
// rename or a same-name directory swap cannot retarget the database path
|
|
698
|
+
// between the path checks and the read.
|
|
699
|
+
const pinnedParent = operatorPinPrivateParent(dirname(path));
|
|
700
|
+
const walPath = `${path}-wal`;
|
|
701
|
+
const shmPath = `${path}-shm`;
|
|
702
|
+
let source;
|
|
703
|
+
let wal = null;
|
|
704
|
+
let shm = null;
|
|
705
|
+
let temporaryPath;
|
|
706
|
+
let temporaryPinned;
|
|
707
|
+
let copyFiles = [];
|
|
708
|
+
let database;
|
|
709
|
+
let transaction = false;
|
|
710
|
+
let deferred;
|
|
711
|
+
let result;
|
|
712
|
+
// Descriptors the snapshot itself holds; these are excluded when proving that
|
|
713
|
+
// SQLite opened the pinned inode, so the connection can never vouch for itself.
|
|
714
|
+
const selfDescriptors = () => new Set([
|
|
715
|
+
pinnedParent.descriptor, source?.descriptor, wal?.descriptor, shm?.descriptor,
|
|
716
|
+
temporaryPinned?.descriptor, ...copyFiles.map(file => file.descriptor),
|
|
717
|
+
].filter((descriptor) => descriptor !== undefined));
|
|
718
|
+
try {
|
|
719
|
+
try {
|
|
720
|
+
lstatSync(`${path}-journal`);
|
|
721
|
+
operatorFail('database-busy', 'database has a rollback journal');
|
|
722
|
+
}
|
|
723
|
+
catch (error) {
|
|
724
|
+
if (error instanceof ActiveLarkOwnerBindingsSnapshotError)
|
|
725
|
+
throw error;
|
|
726
|
+
if (error.code !== 'ENOENT')
|
|
727
|
+
operatorFail('unsafe-path', 'rollback journal cannot be inspected');
|
|
728
|
+
}
|
|
729
|
+
source = operatorOpenPrivateFile(path, true);
|
|
730
|
+
wal = operatorOptionalFile(walPath);
|
|
731
|
+
shm = operatorOptionalFile(shmPath);
|
|
732
|
+
if ((wal === null) !== (shm === null))
|
|
733
|
+
operatorFail('database-drift', 'WAL sidecar set is incomplete');
|
|
734
|
+
operatorAssertUnchanged(source);
|
|
735
|
+
operatorAssertSidecar(walPath, wal);
|
|
736
|
+
operatorAssertSidecar(shmPath, shm);
|
|
737
|
+
// Files SQLite must prove it is reading. Without WAL that is the pinned
|
|
738
|
+
// source inode; with WAL it is the pinned private replica inodes.
|
|
739
|
+
let bound = [];
|
|
740
|
+
let target;
|
|
741
|
+
if (wal === null) {
|
|
742
|
+
// Open the pinned descriptor itself by inode (Linux) so a same-name swap
|
|
743
|
+
// cannot retarget the connection; immutable=1 forbids WAL/journal access.
|
|
744
|
+
target = operatorInodeBindSupported
|
|
745
|
+
? `file://${operatorProcSelfFd}/${source.descriptor}?mode=ro&immutable=1`
|
|
746
|
+
: `${pathToFileURL(path).href}?immutable=1`;
|
|
747
|
+
bound = [source];
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
temporaryPath = mkdtempSync(join(tmpdir(), 'delivery-operator-snapshot-'));
|
|
751
|
+
const temporaryMetadata = lstatSync(temporaryPath, { bigint: true });
|
|
752
|
+
const temporaryUid = process.getuid?.();
|
|
753
|
+
if (!temporaryMetadata.isDirectory() || temporaryMetadata.isSymbolicLink()
|
|
754
|
+
|| (temporaryMetadata.mode & 4095n) !== 448n
|
|
755
|
+
|| (temporaryUid !== undefined && temporaryMetadata.uid !== BigInt(temporaryUid))) {
|
|
756
|
+
operatorFail('unsafe-path', 'temporary snapshot directory is not private');
|
|
757
|
+
}
|
|
758
|
+
if (operatorInodeBindSupported) {
|
|
759
|
+
temporaryPinned = operatorPinTempDirectory(temporaryPath);
|
|
760
|
+
const anchor = `${operatorProcSelfFd}/${temporaryPinned.descriptor}`;
|
|
761
|
+
const mainRef = `${anchor}/delivery.sqlite`;
|
|
762
|
+
const copiedMain = operatorCopyAndPin(source, mainRef);
|
|
763
|
+
let copiedWal;
|
|
764
|
+
try {
|
|
765
|
+
copiedWal = operatorCopyAndPin(wal, `${mainRef}-wal`);
|
|
766
|
+
}
|
|
767
|
+
catch (error) {
|
|
768
|
+
try {
|
|
769
|
+
closeSync(copiedMain.descriptor);
|
|
770
|
+
}
|
|
771
|
+
catch { }
|
|
772
|
+
;
|
|
773
|
+
throw error;
|
|
774
|
+
}
|
|
775
|
+
copyFiles = Object.freeze([copiedMain, copiedWal]);
|
|
776
|
+
target = mainRef;
|
|
777
|
+
bound = copyFiles;
|
|
778
|
+
}
|
|
779
|
+
else {
|
|
780
|
+
const copy = join(temporaryPath, 'delivery.sqlite');
|
|
781
|
+
operatorCopyFile(source, copy);
|
|
782
|
+
operatorCopyFile(wal, `${copy}-wal`);
|
|
783
|
+
target = copy;
|
|
784
|
+
}
|
|
785
|
+
operatorAssertUnchanged(source);
|
|
786
|
+
operatorAssertSidecar(walPath, wal);
|
|
787
|
+
operatorAssertSidecar(shmPath, shm);
|
|
788
|
+
}
|
|
789
|
+
if (!operatorPinnedFileIdentityMatches(source)) {
|
|
790
|
+
operatorFail('database-drift', 'database path changed before open');
|
|
791
|
+
}
|
|
792
|
+
database = new DatabaseSync(target, { readOnly: true });
|
|
793
|
+
// The WAL/SHM fds are opened lazily on first query, so right after the
|
|
794
|
+
// constructor only the main database inode can be claimed.
|
|
795
|
+
if (operatorInodeBindSupported)
|
|
796
|
+
operatorAssertConnectionBound([bound[0]], selfDescriptors());
|
|
797
|
+
if (!operatorPinnedFileIdentityMatches(source)) {
|
|
798
|
+
operatorFail('database-drift', 'database path changed during open');
|
|
799
|
+
}
|
|
800
|
+
database.exec('PRAGMA query_only = ON; PRAGMA busy_timeout = 0; BEGIN');
|
|
801
|
+
transaction = true;
|
|
802
|
+
const queryOnly = database.prepare('PRAGMA query_only').get();
|
|
803
|
+
if (queryOnly.query_only !== 1)
|
|
804
|
+
operatorFail('database-corrupt', 'read-only guard was not enabled');
|
|
805
|
+
const version = database.prepare('PRAGMA user_version').get().user_version;
|
|
806
|
+
if (version !== deliverySchemaVersion)
|
|
807
|
+
operatorFail('schema-unsupported', `expected schema ${deliverySchemaVersion}`);
|
|
808
|
+
const quick = database.prepare('PRAGMA quick_check').all();
|
|
809
|
+
if (quick.length !== 1 || quick[0]?.quick_check !== 'ok')
|
|
810
|
+
operatorFail('database-corrupt', 'SQLite quick_check failed');
|
|
811
|
+
if (database.prepare('PRAGMA foreign_key_check').all().length !== 0)
|
|
812
|
+
operatorFail('database-corrupt', 'foreign-key integrity check failed');
|
|
813
|
+
const rows = database.prepare(`
|
|
814
|
+
SELECT binding.id AS binding_id, binding.conversation_hash, binding.conversation_json,
|
|
815
|
+
binding.principal_id, binding.principal_json AS binding_principal_json, binding.workspace,
|
|
816
|
+
binding.agent_preset, binding.session_id, binding.generation, binding.policy_ref,
|
|
817
|
+
binding.status AS binding_status, binding.created_at AS binding_created_at,
|
|
818
|
+
binding.updated_at AS binding_updated_at, binding.version AS binding_version,
|
|
819
|
+
owner.id AS owner_id, owner.key_hash AS owner_key_hash, owner.principal_json AS owner_principal_json,
|
|
820
|
+
owner.role AS owner_role, owner.status AS owner_status, owner.linked_to_id,
|
|
821
|
+
owner.created_at AS owner_created_at, owner.updated_at AS owner_updated_at, owner.version AS owner_version,
|
|
822
|
+
NULL AS lease_state
|
|
823
|
+
FROM conversation_bindings AS binding
|
|
824
|
+
JOIN delivery_principals AS owner ON owner.id = binding.principal_id
|
|
825
|
+
WHERE binding.status = 'active' AND owner.role = 'owner' AND owner.status = 'active'
|
|
826
|
+
AND binding.workspace = ? AND binding.agent_preset = ?
|
|
827
|
+
AND json_extract(owner.principal_json, '$.channel') = 'lark'
|
|
828
|
+
AND json_extract(owner.principal_json, '$.account') = ?
|
|
829
|
+
AND json_extract(owner.principal_json, '$.tenant') = ?
|
|
830
|
+
AND json_extract(binding.conversation_json, '$.channel') = 'lark'
|
|
831
|
+
AND json_extract(binding.conversation_json, '$.account') = ?
|
|
832
|
+
AND json_extract(binding.conversation_json, '$.tenant') = ?
|
|
833
|
+
AND json_extract(binding.conversation_json, '$.kind') = 'dm'
|
|
834
|
+
ORDER BY binding.id COLLATE BINARY
|
|
835
|
+
LIMIT ?
|
|
836
|
+
`).all(scope.workspace, scope.agentPreset, scope.account, scope.tenant, scope.account, scope.tenant, operatorMaximumBindings + 1);
|
|
837
|
+
if (rows.length > operatorMaximumBindings)
|
|
838
|
+
operatorFail('too-many-bindings', `more than ${operatorMaximumBindings} bindings match`);
|
|
839
|
+
// The main query has now opened the WAL/SHM fds; re-prove every inode the
|
|
840
|
+
// result depends on is still the pinned one (non-WAL keeps just the source).
|
|
841
|
+
if (operatorInodeBindSupported)
|
|
842
|
+
operatorAssertConnectionBound(bound, selfDescriptors());
|
|
843
|
+
const bindings = operatorFreeze(rows.map(row => operatorBinding(row, scope)));
|
|
844
|
+
database.exec('ROLLBACK');
|
|
845
|
+
transaction = false;
|
|
846
|
+
database.close();
|
|
847
|
+
database = undefined;
|
|
848
|
+
operatorAssertUnchanged(source);
|
|
849
|
+
operatorAssertSidecar(walPath, wal);
|
|
850
|
+
operatorAssertSidecar(shmPath, shm);
|
|
851
|
+
const databaseIdentity = operatorFileIdentity(source);
|
|
852
|
+
const sidecars = operatorFreeze({ wal: wal === null ? null : operatorFileIdentity(wal), shm: shm === null ? null : operatorFileIdentity(shm) });
|
|
853
|
+
const storageDigest = operatorSha256(operatorCanonicalJson({
|
|
854
|
+
protocol: 'assistant-delivery/operator-storage/v1', database: source.digest, wal: wal?.digest ?? null,
|
|
855
|
+
}));
|
|
856
|
+
const unsigned = operatorFreeze({
|
|
857
|
+
protocol: activeLarkOwnerBindingsSnapshotProtocol, schemaVersion: deliverySchemaVersion, scope,
|
|
858
|
+
database: databaseIdentity, sidecars, storageDigest, bindings,
|
|
859
|
+
});
|
|
860
|
+
result = operatorFreeze({ ...unsigned, snapshotDigest: operatorSha256(operatorCanonicalJson(unsigned)) });
|
|
861
|
+
}
|
|
862
|
+
catch (error) {
|
|
863
|
+
deferred = error;
|
|
864
|
+
}
|
|
865
|
+
finally {
|
|
866
|
+
if (transaction)
|
|
867
|
+
try {
|
|
868
|
+
database?.exec('ROLLBACK');
|
|
869
|
+
}
|
|
870
|
+
catch (error) {
|
|
871
|
+
deferred ??= error;
|
|
872
|
+
}
|
|
873
|
+
try {
|
|
874
|
+
database?.close();
|
|
875
|
+
}
|
|
876
|
+
catch (error) {
|
|
877
|
+
deferred ??= error;
|
|
878
|
+
}
|
|
879
|
+
if (source !== undefined) {
|
|
880
|
+
try {
|
|
881
|
+
operatorAssertUnchanged(source);
|
|
882
|
+
operatorAssertSidecar(walPath, wal);
|
|
883
|
+
operatorAssertSidecar(shmPath, shm);
|
|
884
|
+
}
|
|
885
|
+
catch (error) {
|
|
886
|
+
deferred ??= error;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
if (!operatorPinnedParentMatches(pinnedParent)) {
|
|
890
|
+
deferred = new ActiveLarkOwnerBindingsSnapshotError('unsafe-parent', 'delivery Lark owner snapshot: database parent changed while it was inspected');
|
|
891
|
+
}
|
|
892
|
+
for (const file of [shm, wal, source])
|
|
893
|
+
if (file !== null && file !== undefined) {
|
|
894
|
+
try {
|
|
895
|
+
closeSync(file.descriptor);
|
|
896
|
+
}
|
|
897
|
+
catch { }
|
|
898
|
+
}
|
|
899
|
+
for (const file of copyFiles) {
|
|
900
|
+
try {
|
|
901
|
+
closeSync(file.descriptor);
|
|
902
|
+
}
|
|
903
|
+
catch { }
|
|
904
|
+
}
|
|
905
|
+
if (temporaryPinned !== undefined) {
|
|
906
|
+
try {
|
|
907
|
+
operatorRemovePinnedTemp(temporaryPinned, ['delivery.sqlite', 'delivery.sqlite-wal', 'delivery.sqlite-shm']);
|
|
908
|
+
}
|
|
909
|
+
catch (error) {
|
|
910
|
+
deferred ??= error;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
else if (temporaryPath !== undefined) {
|
|
914
|
+
try {
|
|
915
|
+
rmSync(temporaryPath, { recursive: true, force: true });
|
|
916
|
+
}
|
|
917
|
+
catch (error) {
|
|
918
|
+
deferred ??= error;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
try {
|
|
922
|
+
closeSync(pinnedParent.descriptor);
|
|
923
|
+
}
|
|
924
|
+
catch (error) {
|
|
925
|
+
deferred ??= error;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
if (deferred !== undefined) {
|
|
929
|
+
if (deferred instanceof ActiveLarkOwnerBindingsSnapshotError)
|
|
930
|
+
throw deferred;
|
|
931
|
+
operatorClassifySqlite(deferred);
|
|
932
|
+
}
|
|
933
|
+
return result;
|
|
934
|
+
}
|
|
935
|
+
//# sourceMappingURL=operator-snapshot.js.map
|