@dsh-enhanced/plugin-control-plane 0.1.48 → 0.1.49
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 +183 -15
- package/bin/dsh-live-qualification-authority.js +15 -0
- package/bin/dsh-local-release-adapter.js +328 -8
- package/bin/dsh-systemd-host-attestor.js +359 -9
- package/bin/dsh-systemd-host-authority.js +12 -0
- package/lib/adoption-coordinator.d.ts +3 -1
- package/lib/adoption-coordinator.d.ts.map +1 -1
- package/lib/adoption-coordinator.js +24 -9
- package/lib/adoption-coordinator.js.map +1 -1
- package/lib/cli.d.ts.map +1 -1
- package/lib/cli.js +33 -14
- package/lib/cli.js.map +1 -1
- package/lib/creation-capability-feedback.d.ts +23 -0
- package/lib/creation-capability-feedback.d.ts.map +1 -0
- package/lib/creation-capability-feedback.js +132 -0
- package/lib/creation-capability-feedback.js.map +1 -0
- package/lib/creation-capability-journal.d.ts +66 -0
- package/lib/creation-capability-journal.d.ts.map +1 -0
- package/lib/creation-capability-journal.js +751 -0
- package/lib/creation-capability-journal.js.map +1 -0
- package/lib/creation-capability-runtime.d.ts +63 -0
- package/lib/creation-capability-runtime.d.ts.map +1 -0
- package/lib/creation-capability-runtime.js +564 -0
- package/lib/creation-capability-runtime.js.map +1 -0
- package/lib/creation-capability-source.d.ts +37 -0
- package/lib/creation-capability-source.d.ts.map +1 -0
- package/lib/creation-capability-source.js +328 -0
- package/lib/creation-capability-source.js.map +1 -0
- package/lib/creation-capability-types.d.ts +252 -0
- package/lib/creation-capability-types.d.ts.map +1 -0
- package/lib/creation-capability-types.js +2 -0
- package/lib/creation-capability-types.js.map +1 -0
- package/lib/deployment-readiness.d.ts +16 -0
- package/lib/deployment-readiness.d.ts.map +1 -1
- package/lib/deployment-readiness.js +48 -1
- package/lib/deployment-readiness.js.map +1 -1
- package/lib/effect-blocked-replay.js +2 -2
- package/lib/effect-blocked-replay.js.map +1 -1
- package/lib/foreground-deployment-runtime.d.ts +3 -0
- package/lib/foreground-deployment-runtime.d.ts.map +1 -1
- package/lib/foreground-deployment-runtime.js +55 -13
- package/lib/foreground-deployment-runtime.js.map +1 -1
- package/lib/foreground-deployment.d.ts.map +1 -1
- package/lib/foreground-deployment.js +5 -0
- package/lib/foreground-deployment.js.map +1 -1
- package/lib/host-attestor.d.ts +2 -0
- package/lib/host-attestor.d.ts.map +1 -1
- package/lib/host-attestor.js +8 -1
- package/lib/host-attestor.js.map +1 -1
- package/lib/host-inputs.d.ts +13 -0
- package/lib/host-inputs.d.ts.map +1 -0
- package/lib/host-inputs.js +162 -0
- package/lib/host-inputs.js.map +1 -0
- package/lib/host-maintenance.d.ts +61 -0
- package/lib/host-maintenance.d.ts.map +1 -0
- package/lib/host-maintenance.js +158 -0
- package/lib/host-maintenance.js.map +1 -0
- package/lib/host-readiness.d.ts +5 -0
- package/lib/host-readiness.d.ts.map +1 -0
- package/lib/host-readiness.js +22 -0
- package/lib/host-readiness.js.map +1 -0
- package/lib/index.d.ts +16 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +7 -0
- package/lib/index.js.map +1 -1
- package/lib/live-qualification-authority.d.ts +32 -0
- package/lib/live-qualification-authority.d.ts.map +1 -0
- package/lib/live-qualification-authority.js +280 -0
- package/lib/live-qualification-authority.js.map +1 -0
- package/lib/live-qualification-client.d.ts +4 -0
- package/lib/live-qualification-client.d.ts.map +1 -0
- package/lib/live-qualification-client.js +22 -0
- package/lib/live-qualification-client.js.map +1 -0
- package/lib/live-qualification-runtime.d.ts +78 -0
- package/lib/live-qualification-runtime.d.ts.map +1 -0
- package/lib/live-qualification-runtime.js +367 -0
- package/lib/live-qualification-runtime.js.map +1 -0
- package/lib/live-qualification.d.ts +73 -0
- package/lib/live-qualification.d.ts.map +1 -0
- package/lib/live-qualification.js +164 -0
- package/lib/live-qualification.js.map +1 -0
- package/lib/owner-task-gaps.d.ts +2 -0
- package/lib/owner-task-gaps.d.ts.map +1 -1
- package/lib/owner-task-gaps.js +9 -1
- package/lib/owner-task-gaps.js.map +1 -1
- package/lib/runtime-epoch.d.ts +69 -0
- package/lib/runtime-epoch.d.ts.map +1 -0
- package/lib/runtime-epoch.js +139 -0
- package/lib/runtime-epoch.js.map +1 -0
- package/lib/runtime-observer.d.ts.map +1 -1
- package/lib/runtime-observer.js +42 -6
- package/lib/runtime-observer.js.map +1 -1
- package/lib/service.d.ts +247 -4
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +1332 -27
- package/lib/service.js.map +1 -1
- package/lib/source-adoption-authority.d.ts +3 -0
- package/lib/source-adoption-authority.d.ts.map +1 -1
- package/lib/source-adoption-authority.js +13 -3
- package/lib/source-adoption-authority.js.map +1 -1
- package/lib/source-adoption-runner.d.ts +3 -0
- package/lib/source-adoption-runner.d.ts.map +1 -1
- package/lib/source-adoption-runner.js +16 -4
- package/lib/source-adoption-runner.js.map +1 -1
- package/lib/source-baseline.d.ts +37 -0
- package/lib/source-baseline.d.ts.map +1 -0
- package/lib/source-baseline.js +250 -0
- package/lib/source-baseline.js.map +1 -0
- package/lib/source-build.d.ts +17 -1
- package/lib/source-build.d.ts.map +1 -1
- package/lib/source-build.js +501 -7
- package/lib/source-build.js.map +1 -1
- package/lib/source-context.d.ts +18 -0
- package/lib/source-context.d.ts.map +1 -1
- package/lib/source-context.js +85 -11
- package/lib/source-context.js.map +1 -1
- package/lib/source-creation.d.ts +106 -0
- package/lib/source-creation.d.ts.map +1 -0
- package/lib/source-creation.js +664 -0
- package/lib/source-creation.js.map +1 -0
- package/lib/source-job-types.d.ts +19 -0
- package/lib/source-job-types.d.ts.map +1 -1
- package/lib/source-jobs.d.ts +30 -3
- package/lib/source-jobs.d.ts.map +1 -1
- package/lib/source-jobs.js +382 -40
- package/lib/source-jobs.js.map +1 -1
- package/lib/source-maintenance.d.ts +66 -0
- package/lib/source-maintenance.d.ts.map +1 -0
- package/lib/source-maintenance.js +177 -0
- package/lib/source-maintenance.js.map +1 -0
- package/lib/source-revision.d.ts +57 -0
- package/lib/source-revision.d.ts.map +1 -0
- package/lib/source-revision.js +175 -0
- package/lib/source-revision.js.map +1 -0
- package/lib/source-workspace.d.ts +19 -14
- package/lib/source-workspace.d.ts.map +1 -1
- package/lib/source-workspace.js +90 -17
- package/lib/source-workspace.js.map +1 -1
- package/lib/sqlite.d.ts +13 -3
- package/lib/sqlite.d.ts.map +1 -1
- package/lib/sqlite.js +362 -1
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +258 -3
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +2211 -95
- package/lib/store.js.map +1 -1
- package/lib/systemd-host-authority.d.ts +68 -0
- package/lib/systemd-host-authority.d.ts.map +1 -0
- package/lib/systemd-host-authority.js +424 -0
- package/lib/systemd-host-authority.js.map +1 -0
- package/lib/task-observation-evidence.d.ts +32 -0
- package/lib/task-observation-evidence.d.ts.map +1 -0
- package/lib/task-observation-evidence.js +62 -0
- package/lib/task-observation-evidence.js.map +1 -0
- package/lib/task-observation-runtime.d.ts +2 -3
- package/lib/task-observation-runtime.d.ts.map +1 -1
- package/lib/task-observation-runtime.js +7 -39
- package/lib/task-observation-runtime.js.map +1 -1
- package/lib/task-observation-store.js +1 -1
- package/lib/trust.d.ts.map +1 -1
- package/lib/trust.js +0 -0
- package/lib/trust.js.map +1 -1
- package/lib/types.d.ts +43 -6
- package/lib/types.d.ts.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +21 -14
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
import { createHash, createPrivateKey, createPublicKey, sign, verify } from 'node:crypto';
|
|
2
|
+
import { closeSync, constants, fstatSync, fsyncSync, lstatSync, openSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
4
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
5
|
+
import { canonicalGrowthJson, validatePluginCreationVerificationCertificate } from '@dsh-enhanced/assistant-growth-contract';
|
|
6
|
+
import { validateCreationCapabilitySource } from './creation-capability-source.js';
|
|
7
|
+
const ID = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$/u;
|
|
8
|
+
const SHA = /^[a-f0-9]{64}$/u;
|
|
9
|
+
const APP_ID = 0x44534341;
|
|
10
|
+
const SEMANTICS = 'dsh-created-capability-journal/v1:reserve-before-dispatch:unknown-no-retry:finite-owner-grant';
|
|
11
|
+
const SOURCE_ARCHIVE_LIMIT = 2 * 1024 * 1024;
|
|
12
|
+
const SOURCE_ARCHIVE_DOMAIN = 'dsh-created-capability-source-archive-v1\0';
|
|
13
|
+
function fail() { throw new Error('creation capability journal rejected input or state'); }
|
|
14
|
+
function obj(value, expected) {
|
|
15
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype
|
|
16
|
+
|| Reflect.ownKeys(value).length !== expected.length)
|
|
17
|
+
fail();
|
|
18
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
19
|
+
if (Object.keys(descriptors).sort().join('\0') !== [...expected].sort().join('\0')
|
|
20
|
+
|| !Object.values(descriptors).every(d => d.enumerable && 'value' in d))
|
|
21
|
+
fail();
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function str(value, pattern = ID, max = 512) {
|
|
25
|
+
if (typeof value !== 'string' || !pattern.test(value) || value.normalize('NFC') !== value
|
|
26
|
+
|| Buffer.byteLength(value) > max || /[\p{Cc}]/u.test(value))
|
|
27
|
+
fail();
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
function int(value, min, max) {
|
|
31
|
+
if (!Number.isSafeInteger(value) || Number(value) < min || Number(value) > max)
|
|
32
|
+
fail();
|
|
33
|
+
return Number(value);
|
|
34
|
+
}
|
|
35
|
+
function path(value) {
|
|
36
|
+
const result = str(value, /^[\s\S]+$/u, 4096);
|
|
37
|
+
if (!isAbsolute(result) || result === '/' || resolve(result) !== result)
|
|
38
|
+
fail();
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
function privateStat(file, kind) {
|
|
42
|
+
const s = lstatSync(file);
|
|
43
|
+
if (realpathSync(file) !== file || (kind === 'file' ? !s.isFile() || s.nlink !== 1 : !s.isDirectory())
|
|
44
|
+
|| s.uid !== process.getuid?.() || (s.mode & 0o077) !== 0)
|
|
45
|
+
fail();
|
|
46
|
+
}
|
|
47
|
+
function keyBytes(file) {
|
|
48
|
+
privateStat(dirname(file), 'directory');
|
|
49
|
+
privateStat(file, 'file');
|
|
50
|
+
const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
51
|
+
try {
|
|
52
|
+
const before = fstatSync(fd, { bigint: true }), named = lstatSync(file, { bigint: true });
|
|
53
|
+
if (before.size > 32768n || before.ino !== named.ino || before.dev !== named.dev)
|
|
54
|
+
fail();
|
|
55
|
+
const bytes = readFileSync(fd), after = fstatSync(fd, { bigint: true });
|
|
56
|
+
if (bytes.length > 32768 || before.ino !== after.ino || before.dev !== after.dev
|
|
57
|
+
|| before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs)
|
|
58
|
+
fail();
|
|
59
|
+
return bytes;
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
closeSync(fd);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function json(value, max = 65536) {
|
|
66
|
+
const seen = new Set();
|
|
67
|
+
let nodes = 0;
|
|
68
|
+
const walk = (v, depth) => {
|
|
69
|
+
if (++nodes > 8192 || depth > 32)
|
|
70
|
+
fail();
|
|
71
|
+
if (v === null || typeof v === 'string' || typeof v === 'boolean')
|
|
72
|
+
return;
|
|
73
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
74
|
+
return;
|
|
75
|
+
if (!v || typeof v !== 'object' || seen.has(v))
|
|
76
|
+
fail();
|
|
77
|
+
seen.add(v);
|
|
78
|
+
if (Array.isArray(v)) {
|
|
79
|
+
if (Object.getPrototypeOf(v) !== Array.prototype || Reflect.ownKeys(v).length !== v.length + 1)
|
|
80
|
+
fail();
|
|
81
|
+
for (let i = 0; i < v.length; i++) {
|
|
82
|
+
if (!Object.hasOwn(v, i))
|
|
83
|
+
fail();
|
|
84
|
+
walk(v[i], depth + 1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
const d = Object.getOwnPropertyDescriptors(v);
|
|
89
|
+
if ((Object.getPrototypeOf(v) !== Object.prototype && Object.getPrototypeOf(v) !== null)
|
|
90
|
+
|| Reflect.ownKeys(v).length !== Object.keys(d).length
|
|
91
|
+
|| !Object.values(d).every(x => x.enumerable && 'value' in x))
|
|
92
|
+
fail();
|
|
93
|
+
for (const [k, x] of Object.entries(d)) {
|
|
94
|
+
if (k === '__proto__' || k === 'constructor')
|
|
95
|
+
fail();
|
|
96
|
+
walk(x.value, depth + 1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
seen.delete(v);
|
|
100
|
+
};
|
|
101
|
+
walk(value, 0);
|
|
102
|
+
const encoded = canonicalGrowthJson(value);
|
|
103
|
+
if (Buffer.byteLength(encoded) > max)
|
|
104
|
+
fail();
|
|
105
|
+
return encoded;
|
|
106
|
+
}
|
|
107
|
+
const sha = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
108
|
+
function digest(value) { return sha(json(value)); }
|
|
109
|
+
function witnessJson(value, config, planId, alias, argumentsDigest, key) {
|
|
110
|
+
const root = obj(value, ['protocol', 'task', 'turn', 'call']);
|
|
111
|
+
if (root.protocol !== 'assistant-delivery/foreground-tool-call/v1')
|
|
112
|
+
fail();
|
|
113
|
+
const task = obj(root.task, ['protocol', 'inboxId', 'sessionId', 'scope', 'owner', 'binding', 'dispatchedAt']);
|
|
114
|
+
if (task.protocol !== 'assistant-delivery/foreground-task/v1')
|
|
115
|
+
fail();
|
|
116
|
+
str(task.inboxId);
|
|
117
|
+
str(task.sessionId);
|
|
118
|
+
const scope = obj(task.scope, ['workspace', 'preset']);
|
|
119
|
+
if (path(scope.workspace) !== config.owner.workspace || str(scope.preset) !== config.owner.agentPreset)
|
|
120
|
+
fail();
|
|
121
|
+
const owner = obj(task.owner, ['principalRecordId', 'principalVersion']);
|
|
122
|
+
if (str(owner.principalRecordId, /^[\s\S]+$/u, 512) !== config.owner.principalRecordId
|
|
123
|
+
|| int(owner.principalVersion, 1, Number.MAX_SAFE_INTEGER) !== config.owner.principalVersion)
|
|
124
|
+
fail();
|
|
125
|
+
const binding = obj(task.binding, ['id', 'version', 'generation']);
|
|
126
|
+
str(binding.id, /^[\s\S]+$/u, 512);
|
|
127
|
+
int(binding.version, 1, Number.MAX_SAFE_INTEGER);
|
|
128
|
+
int(binding.generation, 1, Number.MAX_SAFE_INTEGER);
|
|
129
|
+
int(task.dispatchedAt, 1, 8_640_000_000_000_000);
|
|
130
|
+
int(root.turn, 0, Number.MAX_SAFE_INTEGER);
|
|
131
|
+
const call = obj(root.call, ['id', 'toolName', 'eventSeq', 'eventDigest', 'argumentsDigest']);
|
|
132
|
+
str(call.id);
|
|
133
|
+
str(call.toolName);
|
|
134
|
+
int(call.eventSeq, 0, Number.MAX_SAFE_INTEGER);
|
|
135
|
+
str(call.eventDigest, SHA, 64);
|
|
136
|
+
str(call.argumentsDigest, SHA, 64);
|
|
137
|
+
if (call.toolName !== alias || call.argumentsDigest !== argumentsDigest
|
|
138
|
+
|| sha(JSON.stringify({ planId, sessionId: task.sessionId, callId: call.id, toolName: alias })) !== key)
|
|
139
|
+
fail();
|
|
140
|
+
return json(value, 4096);
|
|
141
|
+
}
|
|
142
|
+
function receiptBody(receipt) {
|
|
143
|
+
const { signature: _signature, ...body } = receipt;
|
|
144
|
+
return body;
|
|
145
|
+
}
|
|
146
|
+
function receiptShape(value) {
|
|
147
|
+
const v = obj(value, ['protocol', 'authorityId', 'authorityDigest', 'keyId', 'planId', 'planDigest', 'verificationDigest',
|
|
148
|
+
'artifactSha256', 'artifactBytes', 'source', 'schemaDigest', 'toolsDigest', 'expiresAt', 'adoptedAt', 'signature']);
|
|
149
|
+
if (v.protocol !== 'dsh-created-capability-adoption/v1' && v.protocol !== 'dsh-created-capability-adoption/v2')
|
|
150
|
+
fail();
|
|
151
|
+
for (const name of ['authorityId', 'keyId', 'planId'])
|
|
152
|
+
str(v[name]);
|
|
153
|
+
for (const name of ['authorityDigest', 'planDigest', 'verificationDigest', 'artifactSha256', 'schemaDigest', 'toolsDigest'])
|
|
154
|
+
str(v[name], SHA, 64);
|
|
155
|
+
int(v.artifactBytes, 1, 512 * 1024);
|
|
156
|
+
int(v.expiresAt, 1, 8_640_000_000_000_000);
|
|
157
|
+
int(v.adoptedAt, 1, 8_640_000_000_000_000);
|
|
158
|
+
obj(v.source, ['referenceDigest', 'ownerDigest', 'growthRunDigest']);
|
|
159
|
+
for (const x of Object.values(v.source))
|
|
160
|
+
str(x, SHA, 64);
|
|
161
|
+
const sig = str(v.signature, /^[A-Za-z0-9_-]{86}$/u, 86);
|
|
162
|
+
if (Buffer.from(sig, 'base64url').length !== 64 || Buffer.from(sig, 'base64url').toString('base64url') !== sig)
|
|
163
|
+
fail();
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
export function verifyCreationCapabilityReceipt(value, authorityDigest, publicKey, now = Date.now()) {
|
|
167
|
+
try {
|
|
168
|
+
const receipt = receiptShape(value);
|
|
169
|
+
if (!SHA.test(authorityDigest) || receipt.authorityDigest !== authorityDigest || !Number.isSafeInteger(now)
|
|
170
|
+
|| receipt.adoptedAt > now || receipt.expiresAt <= now || receipt.expiresAt <= receipt.adoptedAt)
|
|
171
|
+
return false;
|
|
172
|
+
const key = createPublicKey(publicKey);
|
|
173
|
+
return key.asymmetricKeyType === 'ed25519'
|
|
174
|
+
&& verify(null, Buffer.from(json(receiptBody(receipt))), key, Buffer.from(receipt.signature, 'base64url'));
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** Read-only preflight; it never creates a signing key or ledger. */
|
|
181
|
+
export function validateCreationCapabilityConfig(value) {
|
|
182
|
+
const v = obj(value, ['authorityId', 'keyId', 'keyPath', 'owner', 'namePrefix', 'expiresAt', 'maxAdoptions', 'maxTools',
|
|
183
|
+
'maxCallsPerAdoption', 'maxCallRecords', 'maxInputBytes', 'runner', ...(Object.hasOwn(value, 'retention') ? ['retention'] : [])]);
|
|
184
|
+
str(v.authorityId);
|
|
185
|
+
str(v.keyId);
|
|
186
|
+
const keyPath = path(v.keyPath);
|
|
187
|
+
const owner = obj(v.owner, ['authorityId', 'authorityHash', 'principalId', 'principalRecordId', 'principalVersion', 'workspace', 'agentPreset']);
|
|
188
|
+
str(owner.authorityId);
|
|
189
|
+
str(owner.authorityHash, SHA, 64);
|
|
190
|
+
str(owner.principalId, /^[\s\S]+$/u, 512);
|
|
191
|
+
str(owner.principalRecordId, /^[\s\S]+$/u, 512);
|
|
192
|
+
int(owner.principalVersion, 1, Number.MAX_SAFE_INTEGER);
|
|
193
|
+
path(owner.workspace);
|
|
194
|
+
str(owner.agentPreset);
|
|
195
|
+
str(v.namePrefix, /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*-$/u, 48);
|
|
196
|
+
int(v.expiresAt, 1, 8_640_000_000_000_000);
|
|
197
|
+
int(v.maxAdoptions, 1, 32);
|
|
198
|
+
int(v.maxTools, 1, 16);
|
|
199
|
+
int(v.maxCallsPerAdoption, 1, 1024);
|
|
200
|
+
int(v.maxCallRecords, 1, 1024);
|
|
201
|
+
int(v.maxInputBytes, 1, 65536);
|
|
202
|
+
if (Object.hasOwn(v, 'retention')) {
|
|
203
|
+
const retention = obj(v.retention, ['maximumLifetimeMs']);
|
|
204
|
+
int(retention.maximumLifetimeMs, 1, 30 * 86_400_000);
|
|
205
|
+
}
|
|
206
|
+
const runner = obj(v.runner, ['stateRoot', 'image', 'dockerPath', 'expiresAt', 'maxRuns', 'maxTotalDurationMs', 'maxDurationMs', 'maxOutputBytes']);
|
|
207
|
+
const root = path(runner.stateRoot);
|
|
208
|
+
privateStat(root, 'directory');
|
|
209
|
+
str(runner.image, /^(?:[a-z0-9][a-z0-9._:/-]*@)?sha256:[a-f0-9]{64}$/u, 512);
|
|
210
|
+
const dockerPath = path(runner.dockerPath);
|
|
211
|
+
if (dockerPath === keyPath)
|
|
212
|
+
fail();
|
|
213
|
+
int(runner.expiresAt, 1, 8_640_000_000_000_000);
|
|
214
|
+
int(runner.maxRuns, 1, 1024);
|
|
215
|
+
int(runner.maxTotalDurationMs, 1, 86_400_000);
|
|
216
|
+
int(runner.maxDurationMs, 1, 300_000);
|
|
217
|
+
if (runner.maxDurationMs > runner.maxTotalDurationMs)
|
|
218
|
+
fail();
|
|
219
|
+
int(runner.maxOutputBytes, 65536, 65536);
|
|
220
|
+
const privateKey = createPrivateKey(keyBytes(keyPath));
|
|
221
|
+
if (privateKey.asymmetricKeyType !== 'ed25519')
|
|
222
|
+
fail();
|
|
223
|
+
json(value);
|
|
224
|
+
}
|
|
225
|
+
/** Stable public identity for preflight comparison with the independent verifier key. */
|
|
226
|
+
export function creationCapabilityPublicKey(config) {
|
|
227
|
+
validateCreationCapabilityConfig(config);
|
|
228
|
+
return createPublicKey(createPrivateKey(keyBytes(config.keyPath))).export({ type: 'spki', format: 'pem' }).toString();
|
|
229
|
+
}
|
|
230
|
+
/** Reservations consume quota before any external invocation; recovery only marks uncertainty. */
|
|
231
|
+
export class CreationCapabilityJournal {
|
|
232
|
+
authorityDigest;
|
|
233
|
+
publicKey;
|
|
234
|
+
db;
|
|
235
|
+
key;
|
|
236
|
+
identity;
|
|
237
|
+
path;
|
|
238
|
+
config;
|
|
239
|
+
constructor(input) {
|
|
240
|
+
validateCreationCapabilityConfig(input.config);
|
|
241
|
+
this.config = structuredClone(input.config);
|
|
242
|
+
this.path = path(input.path);
|
|
243
|
+
if (!this.path.endsWith('/creation-adoptions.sqlite') || this.path === this.config.keyPath)
|
|
244
|
+
fail();
|
|
245
|
+
this.key = createPrivateKey(keyBytes(this.config.keyPath));
|
|
246
|
+
this.publicKey = createPublicKey(this.key).export({ type: 'spki', format: 'pem' }).toString();
|
|
247
|
+
this.authorityDigest = digest({ semantics: SEMANTICS, path: this.path, config: this.config,
|
|
248
|
+
keyFingerprint: sha(keyBytes(this.config.keyPath)) });
|
|
249
|
+
privateStat(dirname(this.path), 'directory');
|
|
250
|
+
try {
|
|
251
|
+
const fd = openSync(this.path, constants.O_CREAT | constants.O_EXCL | constants.O_RDWR | constants.O_NOFOLLOW, 0o600);
|
|
252
|
+
try {
|
|
253
|
+
fsyncSync(fd);
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
closeSync(fd);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
if (error.code !== 'EEXIST')
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
privateStat(this.path, 'file');
|
|
264
|
+
const stat = lstatSync(this.path);
|
|
265
|
+
this.identity = { dev: stat.dev, ino: stat.ino };
|
|
266
|
+
this.db = new DatabaseSync(this.path);
|
|
267
|
+
try {
|
|
268
|
+
this.checkFiles();
|
|
269
|
+
this.db.exec('PRAGMA busy_timeout=1000; PRAGMA synchronous=FULL; PRAGMA foreign_keys=ON; BEGIN IMMEDIATE');
|
|
270
|
+
try {
|
|
271
|
+
const app = this.db.prepare('PRAGMA application_id').get().application_id;
|
|
272
|
+
const version = this.db.prepare('PRAGMA user_version').get().user_version;
|
|
273
|
+
if (app === 0 && version === 0 && this.db.prepare('SELECT name FROM sqlite_master').all().length === 0) {
|
|
274
|
+
this.db.exec(`CREATE TABLE authority (authority_id TEXT PRIMARY KEY, authority_digest TEXT NOT NULL CHECK(length(authority_digest)=64),
|
|
275
|
+
key_id TEXT NOT NULL, public_key TEXT NOT NULL, config_json TEXT NOT NULL CHECK(json_valid(config_json) AND length(config_json)<=65536)) STRICT, WITHOUT ROWID;
|
|
276
|
+
CREATE TABLE adoptions (plan_id TEXT PRIMARY KEY, status TEXT NOT NULL CHECK(status IN ('claimed','authorized','active','closed','unknown','rejected')),
|
|
277
|
+
certificate_json TEXT NOT NULL CHECK(json_valid(certificate_json) AND length(certificate_json)<=65536), certificate_digest TEXT NOT NULL CHECK(length(certificate_digest)=64),
|
|
278
|
+
artifact BLOB NOT NULL CHECK(length(artifact)>0 AND length(artifact)<=524288), artifact_sha TEXT NOT NULL CHECK(length(artifact_sha)=64),
|
|
279
|
+
tools_json TEXT CHECK(tools_json IS NULL OR (json_valid(tools_json) AND length(tools_json)<=65536)), tools_digest TEXT,
|
|
280
|
+
receipt_json TEXT CHECK(receipt_json IS NULL OR (json_valid(receipt_json) AND length(receipt_json)<=65536)), reason TEXT CHECK(reason IS NULL OR (length(reason)>0 AND length(reason)<=64)),
|
|
281
|
+
CHECK((status='claimed' AND tools_json IS NULL AND tools_digest IS NULL AND receipt_json IS NULL AND reason IS NULL)
|
|
282
|
+
OR (status IN ('authorized','active') AND tools_json IS NOT NULL AND tools_digest IS NOT NULL AND receipt_json IS NOT NULL AND reason IS NULL)
|
|
283
|
+
OR (status IN ('closed','unknown','rejected') AND reason IS NOT NULL))) STRICT, WITHOUT ROWID;
|
|
284
|
+
CREATE TABLE calls (plan_id TEXT NOT NULL REFERENCES adoptions(plan_id), call_key TEXT NOT NULL, arguments_digest TEXT NOT NULL CHECK(length(arguments_digest)=64),
|
|
285
|
+
status TEXT NOT NULL CHECK(status IN ('claimed','completed','unknown')), result_json TEXT CHECK(result_json IS NULL OR (json_valid(result_json) AND length(result_json)<=65536)),
|
|
286
|
+
job_id TEXT, tool_alias TEXT, foreground_json TEXT CHECK(foreground_json IS NULL OR (json_valid(foreground_json) AND length(foreground_json)<=4096)),
|
|
287
|
+
claimed_at INTEGER, settled_at INTEGER, PRIMARY KEY(plan_id,call_key),
|
|
288
|
+
CHECK((status='claimed' AND result_json IS NULL AND job_id IS NULL) OR status IN ('completed','unknown'))) STRICT, WITHOUT ROWID;
|
|
289
|
+
PRAGMA application_id=${APP_ID}; PRAGMA user_version=2;`);
|
|
290
|
+
}
|
|
291
|
+
else if (app === APP_ID && version === 1) {
|
|
292
|
+
this.db.exec(`ALTER TABLE calls ADD COLUMN tool_alias TEXT;
|
|
293
|
+
ALTER TABLE calls ADD COLUMN foreground_json TEXT CHECK(foreground_json IS NULL OR (json_valid(foreground_json) AND length(foreground_json)<=4096));
|
|
294
|
+
ALTER TABLE calls ADD COLUMN claimed_at INTEGER;
|
|
295
|
+
ALTER TABLE calls ADD COLUMN settled_at INTEGER;
|
|
296
|
+
PRAGMA user_version=2;`);
|
|
297
|
+
}
|
|
298
|
+
else if (app !== APP_ID || ![2, 3].includes(version))
|
|
299
|
+
fail();
|
|
300
|
+
if (version !== 3) {
|
|
301
|
+
this.db.exec(`CREATE TABLE source_archives (
|
|
302
|
+
plan_id TEXT PRIMARY KEY REFERENCES adoptions(plan_id),
|
|
303
|
+
archive_json TEXT NOT NULL CHECK(json_valid(archive_json) AND length(archive_json)<=${SOURCE_ARCHIVE_LIMIT})
|
|
304
|
+
) STRICT, WITHOUT ROWID; PRAGMA user_version=3;`);
|
|
305
|
+
}
|
|
306
|
+
const row = this.db.prepare('SELECT * FROM authority WHERE authority_id=?').get(this.config.authorityId);
|
|
307
|
+
const count = this.db.prepare('SELECT COUNT(*) AS n FROM authority').get().n;
|
|
308
|
+
if (row) {
|
|
309
|
+
if (count !== 1 || row.authority_digest !== this.authorityDigest || row.key_id !== this.config.keyId
|
|
310
|
+
|| row.public_key !== this.publicKey || row.config_json !== json(this.config))
|
|
311
|
+
fail();
|
|
312
|
+
}
|
|
313
|
+
else if (count !== 0)
|
|
314
|
+
fail();
|
|
315
|
+
else
|
|
316
|
+
this.db.prepare('INSERT INTO authority VALUES (?,?,?,?,?)').run(this.config.authorityId, this.authorityDigest, this.config.keyId, this.publicKey, json(this.config));
|
|
317
|
+
this.db.exec('COMMIT; PRAGMA journal_mode=WAL');
|
|
318
|
+
}
|
|
319
|
+
catch (e) {
|
|
320
|
+
try {
|
|
321
|
+
this.db.exec('ROLLBACK');
|
|
322
|
+
}
|
|
323
|
+
catch { }
|
|
324
|
+
throw e;
|
|
325
|
+
}
|
|
326
|
+
if (this.db.prepare('PRAGMA foreign_key_check').all().length)
|
|
327
|
+
fail();
|
|
328
|
+
this.checkFiles();
|
|
329
|
+
this.syncDirectory();
|
|
330
|
+
this.list();
|
|
331
|
+
}
|
|
332
|
+
catch (e) {
|
|
333
|
+
this.db.close();
|
|
334
|
+
throw e;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
checkFiles() {
|
|
338
|
+
privateStat(dirname(this.path), 'directory');
|
|
339
|
+
privateStat(this.path, 'file');
|
|
340
|
+
const stat = lstatSync(this.path);
|
|
341
|
+
if (stat.dev !== this.identity.dev || stat.ino !== this.identity.ino)
|
|
342
|
+
fail();
|
|
343
|
+
for (const suffix of ['-wal', '-shm', '-journal']) {
|
|
344
|
+
try {
|
|
345
|
+
privateStat(this.path + suffix, 'file');
|
|
346
|
+
}
|
|
347
|
+
catch (e) {
|
|
348
|
+
if (e.code !== 'ENOENT')
|
|
349
|
+
throw e;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
syncDirectory() {
|
|
354
|
+
this.checkFiles();
|
|
355
|
+
const fd = openSync(dirname(this.path), constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
356
|
+
try {
|
|
357
|
+
fsyncSync(fd);
|
|
358
|
+
}
|
|
359
|
+
finally {
|
|
360
|
+
closeSync(fd);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
transaction(fn) {
|
|
364
|
+
this.checkFiles();
|
|
365
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
366
|
+
try {
|
|
367
|
+
const result = fn();
|
|
368
|
+
this.db.exec('COMMIT');
|
|
369
|
+
this.syncDirectory();
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
catch (e) {
|
|
373
|
+
try {
|
|
374
|
+
this.db.exec('ROLLBACK');
|
|
375
|
+
}
|
|
376
|
+
catch { }
|
|
377
|
+
throw e;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
row(planId) {
|
|
381
|
+
str(planId);
|
|
382
|
+
return this.db.prepare('SELECT * FROM adoptions WHERE plan_id=?').get(planId);
|
|
383
|
+
}
|
|
384
|
+
decode(row) {
|
|
385
|
+
if (!(row.artifact instanceof Uint8Array) || row.artifact.length < 1 || row.artifact.length > 524288 || sha(Buffer.from(row.artifact)) !== row.artifact_sha
|
|
386
|
+
|| Buffer.byteLength(row.certificate_json) > 65536 || sha(row.certificate_json) !== row.certificate_digest)
|
|
387
|
+
fail();
|
|
388
|
+
const certificate = JSON.parse(row.certificate_json);
|
|
389
|
+
validatePluginCreationVerificationCertificate(certificate);
|
|
390
|
+
if (json(certificate) !== row.certificate_json || certificate.plan.id !== row.plan_id
|
|
391
|
+
|| certificate.plan.artifactSha256 !== row.artifact_sha || certificate.plan.artifactBytes !== row.artifact.length)
|
|
392
|
+
fail();
|
|
393
|
+
const record = { planId: row.plan_id, status: row.status, certificate, artifact: Buffer.from(row.artifact) };
|
|
394
|
+
if (row.tools_json !== null) {
|
|
395
|
+
if (Buffer.byteLength(row.tools_json) > 65536 || digest(JSON.parse(row.tools_json)) !== row.tools_digest)
|
|
396
|
+
fail();
|
|
397
|
+
const tools = JSON.parse(row.tools_json);
|
|
398
|
+
this.validateTools(tools);
|
|
399
|
+
if (json(tools) !== row.tools_json)
|
|
400
|
+
fail();
|
|
401
|
+
record.tools = tools;
|
|
402
|
+
}
|
|
403
|
+
else if (row.tools_digest !== null)
|
|
404
|
+
fail();
|
|
405
|
+
if (row.receipt_json !== null) {
|
|
406
|
+
if (Buffer.byteLength(row.receipt_json) > 65536)
|
|
407
|
+
fail();
|
|
408
|
+
const receipt = receiptShape(JSON.parse(row.receipt_json));
|
|
409
|
+
if (json(receipt) !== row.receipt_json || !verifyCreationCapabilityReceipt(receipt, this.authorityDigest, this.publicKey, receipt.adoptedAt)
|
|
410
|
+
|| receipt.authorityId !== this.config.authorityId || receipt.keyId !== this.config.keyId
|
|
411
|
+
|| receipt.planId !== row.plan_id || receipt.planDigest !== certificate.plan.digest
|
|
412
|
+
|| receipt.verificationDigest !== digest(certificate) || receipt.artifactSha256 !== row.artifact_sha
|
|
413
|
+
|| receipt.artifactBytes !== row.artifact.length || receipt.source.referenceDigest !== certificate.source.referenceDigest
|
|
414
|
+
|| receipt.source.ownerDigest !== certificate.source.ownerDigest || receipt.source.growthRunDigest !== certificate.source.growthRunDigest
|
|
415
|
+
|| receipt.toolsDigest !== row.tools_digest || receipt.schemaDigest !== certificate.schemaDigest)
|
|
416
|
+
fail();
|
|
417
|
+
const expectedExpiry = this.config.retention === undefined
|
|
418
|
+
? Math.min(this.config.expiresAt, this.config.runner.expiresAt, certificate.expiresAt)
|
|
419
|
+
: Math.min(this.config.expiresAt, this.config.runner.expiresAt, receipt.adoptedAt + this.config.retention.maximumLifetimeMs);
|
|
420
|
+
if (receipt.protocol !== (this.config.retention === undefined
|
|
421
|
+
? 'dsh-created-capability-adoption/v1' : 'dsh-created-capability-adoption/v2')
|
|
422
|
+
|| receipt.expiresAt !== expectedExpiry || receipt.adoptedAt < certificate.verifiedAt
|
|
423
|
+
|| receipt.adoptedAt >= certificate.expiresAt || receipt.adoptedAt >= this.config.expiresAt
|
|
424
|
+
|| receipt.adoptedAt >= this.config.runner.expiresAt)
|
|
425
|
+
fail();
|
|
426
|
+
record.receipt = receipt;
|
|
427
|
+
}
|
|
428
|
+
if (row.reason !== null)
|
|
429
|
+
record.reason = row.reason;
|
|
430
|
+
if (row.status === 'claimed' && (row.tools_json !== null || row.receipt_json !== null || row.reason !== null))
|
|
431
|
+
fail();
|
|
432
|
+
if ((row.status === 'authorized' || row.status === 'active') && (!record.receipt || !record.tools || row.reason !== null))
|
|
433
|
+
fail();
|
|
434
|
+
return record;
|
|
435
|
+
}
|
|
436
|
+
inspect(planId) {
|
|
437
|
+
this.checkFiles();
|
|
438
|
+
const row = this.row(planId);
|
|
439
|
+
if (!row)
|
|
440
|
+
return undefined;
|
|
441
|
+
this.checkCalls(planId);
|
|
442
|
+
return this.decode(row);
|
|
443
|
+
}
|
|
444
|
+
list() {
|
|
445
|
+
this.checkFiles();
|
|
446
|
+
const rows = this.db.prepare('SELECT * FROM adoptions ORDER BY plan_id').all();
|
|
447
|
+
if (rows.length > this.config.maxAdoptions)
|
|
448
|
+
fail();
|
|
449
|
+
if (this.db.prepare('SELECT COUNT(*) AS n FROM calls').get().n > this.config.maxCallRecords)
|
|
450
|
+
fail();
|
|
451
|
+
for (const row of rows)
|
|
452
|
+
this.checkCalls(row.plan_id);
|
|
453
|
+
return rows.map(row => this.decode(row));
|
|
454
|
+
}
|
|
455
|
+
checkCalls(planId) {
|
|
456
|
+
const rows = this.db.prepare('SELECT * FROM calls WHERE plan_id=?').all(planId);
|
|
457
|
+
if (rows.length > this.config.maxCallsPerAdoption)
|
|
458
|
+
fail();
|
|
459
|
+
const adoption = this.row(planId);
|
|
460
|
+
if (!adoption)
|
|
461
|
+
fail();
|
|
462
|
+
const record = this.decode(adoption);
|
|
463
|
+
for (const row of rows)
|
|
464
|
+
this.decodeCall(row, record);
|
|
465
|
+
}
|
|
466
|
+
decodeCall(row, record) {
|
|
467
|
+
str(row.plan_id);
|
|
468
|
+
str(row.call_key);
|
|
469
|
+
str(row.arguments_digest, SHA, 64);
|
|
470
|
+
if (!['claimed', 'completed', 'unknown'].includes(row.status) || (row.status === 'claimed' && (row.result_json !== null || row.job_id !== null)))
|
|
471
|
+
fail();
|
|
472
|
+
const adoption = record ?? this.decode(this.row(row.plan_id));
|
|
473
|
+
if (row.tool_alias !== null) {
|
|
474
|
+
str(row.tool_alias);
|
|
475
|
+
if (!adoption.receipt || !adoption.tools?.some(tool => tool.name === row.tool_alias))
|
|
476
|
+
fail();
|
|
477
|
+
}
|
|
478
|
+
if (row.claimed_at === null) {
|
|
479
|
+
if (row.tool_alias !== null || row.foreground_json !== null || row.settled_at !== null)
|
|
480
|
+
fail();
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
int(row.claimed_at, 1, 8_640_000_000_000_000);
|
|
484
|
+
if (row.settled_at !== null)
|
|
485
|
+
int(row.settled_at, row.claimed_at, 8_640_000_000_000_000);
|
|
486
|
+
}
|
|
487
|
+
if (row.status === 'claimed' && row.settled_at !== null)
|
|
488
|
+
fail();
|
|
489
|
+
if (row.foreground_json !== null) {
|
|
490
|
+
if (row.tool_alias === null || row.claimed_at === null || Buffer.byteLength(row.foreground_json) > 4096
|
|
491
|
+
|| witnessJson(JSON.parse(row.foreground_json), this.config, row.plan_id, row.tool_alias, row.arguments_digest, row.call_key) !== row.foreground_json)
|
|
492
|
+
fail();
|
|
493
|
+
}
|
|
494
|
+
const call = { key: row.call_key, status: row.status };
|
|
495
|
+
if (row.result_json !== null) {
|
|
496
|
+
if (Buffer.byteLength(row.result_json) > 65536)
|
|
497
|
+
fail();
|
|
498
|
+
call.result = JSON.parse(row.result_json);
|
|
499
|
+
if (json(call.result) !== row.result_json)
|
|
500
|
+
fail();
|
|
501
|
+
}
|
|
502
|
+
if (row.job_id !== null)
|
|
503
|
+
call.jobId = str(row.job_id);
|
|
504
|
+
return call;
|
|
505
|
+
}
|
|
506
|
+
claim(input) {
|
|
507
|
+
validatePluginCreationVerificationCertificate(input.certificate);
|
|
508
|
+
if (!Buffer.isBuffer(input.artifact) || input.artifact.length < 1 || input.artifact.length > 524288)
|
|
509
|
+
fail();
|
|
510
|
+
const certificate = input.certificate, now = Date.now(), bytes = Buffer.from(input.artifact);
|
|
511
|
+
if (certificate.plan.artifactSha256 !== sha(bytes) || certificate.plan.artifactBytes !== bytes.length
|
|
512
|
+
|| !certificate.plan.name.startsWith(this.config.namePrefix) || certificate.authority.keyId === this.config.keyId
|
|
513
|
+
|| certificate.authority.authorityId === this.config.authorityId
|
|
514
|
+
|| now >= this.config.expiresAt || now >= this.config.runner.expiresAt || now >= certificate.expiresAt)
|
|
515
|
+
fail();
|
|
516
|
+
const certificateJson = json(certificate);
|
|
517
|
+
let archiveJson;
|
|
518
|
+
if (input.source !== undefined) {
|
|
519
|
+
validateCreationCapabilitySource(input.source, certificate);
|
|
520
|
+
const body = {
|
|
521
|
+
protocol: 'dsh-created-capability-source-archive/v1', authorityId: this.config.authorityId,
|
|
522
|
+
authorityDigest: this.authorityDigest, keyId: this.config.keyId, planId: certificate.plan.id,
|
|
523
|
+
certificateDigest: sha(certificateJson), artifactSha256: sha(bytes), source: input.source,
|
|
524
|
+
};
|
|
525
|
+
archiveJson = json({ ...body, signature: sign(null, Buffer.from(SOURCE_ARCHIVE_DOMAIN + json(body, SOURCE_ARCHIVE_LIMIT)), this.key).toString('base64url') }, SOURCE_ARCHIVE_LIMIT);
|
|
526
|
+
}
|
|
527
|
+
return this.transaction(() => {
|
|
528
|
+
const prior = this.row(certificate.plan.id);
|
|
529
|
+
if (prior) {
|
|
530
|
+
const record = this.decode(prior);
|
|
531
|
+
if (prior.certificate_digest !== sha(certificateJson) || prior.artifact_sha !== sha(bytes)
|
|
532
|
+
|| !record.artifact.equals(bytes))
|
|
533
|
+
fail();
|
|
534
|
+
if (archiveJson !== undefined) {
|
|
535
|
+
const existing = this.db.prepare('SELECT archive_json FROM source_archives WHERE plan_id=?')
|
|
536
|
+
.get(certificate.plan.id);
|
|
537
|
+
if (existing?.archive_json !== archiveJson)
|
|
538
|
+
fail();
|
|
539
|
+
}
|
|
540
|
+
return { created: false, record };
|
|
541
|
+
}
|
|
542
|
+
if (this.db.prepare('SELECT COUNT(*) AS n FROM adoptions').get().n >= this.config.maxAdoptions)
|
|
543
|
+
fail();
|
|
544
|
+
this.db.prepare("INSERT INTO adoptions(plan_id,status,certificate_json,certificate_digest,artifact,artifact_sha) VALUES (?,'claimed',?,?,?,?)")
|
|
545
|
+
.run(certificate.plan.id, certificateJson, sha(certificateJson), bytes, sha(bytes));
|
|
546
|
+
if (archiveJson !== undefined)
|
|
547
|
+
this.db.prepare('INSERT INTO source_archives VALUES (?,?)').run(certificate.plan.id, archiveJson);
|
|
548
|
+
return { created: true, record: this.decode(this.row(certificate.plan.id)) };
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
/** Archive validation is lazy; adoption reconciliation never loads source text. */
|
|
552
|
+
inspectSourceArchive(planId) {
|
|
553
|
+
const record = this.inspect(planId);
|
|
554
|
+
if (!record?.receipt)
|
|
555
|
+
return undefined;
|
|
556
|
+
const row = this.db.prepare('SELECT archive_json FROM source_archives WHERE plan_id=?')
|
|
557
|
+
.get(planId);
|
|
558
|
+
if (!row)
|
|
559
|
+
return undefined;
|
|
560
|
+
if (Buffer.byteLength(row.archive_json) > SOURCE_ARCHIVE_LIMIT)
|
|
561
|
+
fail();
|
|
562
|
+
const value = JSON.parse(row.archive_json);
|
|
563
|
+
const item = obj(value, ['protocol', 'authorityId', 'authorityDigest', 'keyId', 'planId', 'certificateDigest', 'artifactSha256', 'source', 'signature']);
|
|
564
|
+
if (item.protocol !== 'dsh-created-capability-source-archive/v1'
|
|
565
|
+
|| item.authorityId !== this.config.authorityId || item.authorityDigest !== this.authorityDigest
|
|
566
|
+
|| item.keyId !== this.config.keyId || item.planId !== record.planId
|
|
567
|
+
|| item.certificateDigest !== digest(record.certificate)
|
|
568
|
+
|| item.artifactSha256 !== record.certificate.plan.artifactSha256)
|
|
569
|
+
fail();
|
|
570
|
+
validateCreationCapabilitySource(item.source, record.certificate);
|
|
571
|
+
const signature = str(item.signature, /^[A-Za-z0-9_-]{86}$/u, 86);
|
|
572
|
+
const { signature: _signature, ...body } = item;
|
|
573
|
+
if (json(value, SOURCE_ARCHIVE_LIMIT) !== row.archive_json
|
|
574
|
+
|| Buffer.from(signature, 'base64url').toString('base64url') !== signature
|
|
575
|
+
|| !verify(null, Buffer.from(SOURCE_ARCHIVE_DOMAIN + json(body, SOURCE_ARCHIVE_LIMIT)), createPublicKey(this.key), Buffer.from(signature, 'base64url')))
|
|
576
|
+
fail();
|
|
577
|
+
return value;
|
|
578
|
+
}
|
|
579
|
+
validateTools(value) {
|
|
580
|
+
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype || value.length < 1
|
|
581
|
+
|| value.length > this.config.maxTools || Reflect.ownKeys(value).length !== value.length + 1)
|
|
582
|
+
fail();
|
|
583
|
+
const names = new Set();
|
|
584
|
+
for (const tool of value) {
|
|
585
|
+
const item = obj(tool, ['originalName', 'name', 'description', 'parameters']);
|
|
586
|
+
str(item.originalName);
|
|
587
|
+
str(item.name);
|
|
588
|
+
str(item.description, /^[\s\S]+$/u, 2048);
|
|
589
|
+
if (names.has(item.name))
|
|
590
|
+
fail();
|
|
591
|
+
names.add(item.name);
|
|
592
|
+
if (!item.parameters || typeof item.parameters !== 'object' || Array.isArray(item.parameters))
|
|
593
|
+
fail();
|
|
594
|
+
json(item.parameters, 65536);
|
|
595
|
+
}
|
|
596
|
+
json(value);
|
|
597
|
+
}
|
|
598
|
+
authorize(planId, tools) {
|
|
599
|
+
this.validateTools(tools);
|
|
600
|
+
const toolsJson = json(tools), toolsDigest = sha(toolsJson);
|
|
601
|
+
return this.transaction(() => {
|
|
602
|
+
const prior = this.row(planId);
|
|
603
|
+
if (!prior)
|
|
604
|
+
fail();
|
|
605
|
+
const record = this.decode(prior);
|
|
606
|
+
if (record.status === 'authorized' || record.status === 'active') {
|
|
607
|
+
if (prior.tools_digest !== toolsDigest)
|
|
608
|
+
fail();
|
|
609
|
+
return record;
|
|
610
|
+
}
|
|
611
|
+
if (record.status !== 'claimed')
|
|
612
|
+
fail();
|
|
613
|
+
const now = Date.now(), freshExpiry = Math.min(this.config.expiresAt, this.config.runner.expiresAt, record.certificate.expiresAt);
|
|
614
|
+
if (now >= freshExpiry || record.certificate.verifiedAt > now)
|
|
615
|
+
fail();
|
|
616
|
+
const expiresAt = this.config.retention === undefined ? freshExpiry
|
|
617
|
+
: Math.min(this.config.expiresAt, this.config.runner.expiresAt, now + this.config.retention.maximumLifetimeMs);
|
|
618
|
+
const unsigned = { protocol: this.config.retention === undefined
|
|
619
|
+
? 'dsh-created-capability-adoption/v1' : 'dsh-created-capability-adoption/v2',
|
|
620
|
+
authorityId: this.config.authorityId, authorityDigest: this.authorityDigest, keyId: this.config.keyId,
|
|
621
|
+
planId, planDigest: record.certificate.plan.digest, verificationDigest: digest(record.certificate),
|
|
622
|
+
artifactSha256: prior.artifact_sha, artifactBytes: prior.artifact.length, source: record.certificate.source,
|
|
623
|
+
schemaDigest: record.certificate.schemaDigest, toolsDigest, expiresAt, adoptedAt: now };
|
|
624
|
+
const receipt = receiptShape({ ...unsigned, signature: sign(null, Buffer.from(json(unsigned)), this.key).toString('base64url') });
|
|
625
|
+
this.db.prepare("UPDATE adoptions SET status='authorized',tools_json=?,tools_digest=?,receipt_json=? WHERE plan_id=? AND status='claimed'")
|
|
626
|
+
.run(toolsJson, toolsDigest, json(receipt), planId);
|
|
627
|
+
return this.decode(this.row(planId));
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
activate(planId) {
|
|
631
|
+
return this.transaction(() => {
|
|
632
|
+
const prior = this.row(planId);
|
|
633
|
+
if (!prior)
|
|
634
|
+
fail();
|
|
635
|
+
const record = this.decode(prior);
|
|
636
|
+
if (record.status === 'active')
|
|
637
|
+
return record;
|
|
638
|
+
if (record.status !== 'authorized' || !record.receipt || Date.now() >= record.receipt.expiresAt)
|
|
639
|
+
fail();
|
|
640
|
+
this.db.prepare("UPDATE adoptions SET status='active' WHERE plan_id=? AND status='authorized'").run(planId);
|
|
641
|
+
return this.decode(this.row(planId));
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
settle(planId, status, reason) {
|
|
645
|
+
if (!['closed', 'unknown', 'rejected'].includes(status))
|
|
646
|
+
fail();
|
|
647
|
+
str(reason, /^[a-z][a-z0-9-]{0,63}$/u, 64);
|
|
648
|
+
this.transaction(() => {
|
|
649
|
+
const prior = this.row(planId);
|
|
650
|
+
if (!prior)
|
|
651
|
+
fail();
|
|
652
|
+
const current = this.decode(prior);
|
|
653
|
+
if (current.status === status && prior.reason === reason)
|
|
654
|
+
return;
|
|
655
|
+
if (['closed', 'unknown', 'rejected'].includes(current.status))
|
|
656
|
+
fail();
|
|
657
|
+
this.db.prepare('UPDATE adoptions SET status=?,reason=? WHERE plan_id=?').run(status, reason, planId);
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
claimCall(input) {
|
|
661
|
+
str(input.planId);
|
|
662
|
+
str(input.key);
|
|
663
|
+
str(input.argumentsDigest, SHA, 64);
|
|
664
|
+
return this.transaction(() => {
|
|
665
|
+
const row = this.row(input.planId);
|
|
666
|
+
if (!row)
|
|
667
|
+
fail();
|
|
668
|
+
const record = this.decode(row);
|
|
669
|
+
if (input.toolAlias !== undefined && (!record.tools?.some(tool => tool.name === input.toolAlias)
|
|
670
|
+
|| str(input.toolAlias) !== input.toolAlias))
|
|
671
|
+
fail();
|
|
672
|
+
if (input.foreground !== undefined && input.toolAlias === undefined)
|
|
673
|
+
fail();
|
|
674
|
+
const foreground = input.foreground === undefined ? null : witnessJson(input.foreground, this.config, input.planId, input.toolAlias, input.argumentsDigest, input.key);
|
|
675
|
+
const prior = this.db.prepare('SELECT * FROM calls WHERE plan_id=? AND call_key=?').get(input.planId, input.key);
|
|
676
|
+
if (prior) {
|
|
677
|
+
if (prior.arguments_digest !== input.argumentsDigest
|
|
678
|
+
|| (input.toolAlias !== undefined && prior.tool_alias !== null && prior.tool_alias !== input.toolAlias)
|
|
679
|
+
|| (prior.foreground_json !== null && foreground === null)
|
|
680
|
+
|| (foreground !== null && prior.foreground_json !== null && prior.foreground_json !== foreground)
|
|
681
|
+
|| (foreground !== null && prior.foreground_json === null && prior.claimed_at !== null))
|
|
682
|
+
fail();
|
|
683
|
+
return { created: false, call: this.decodeCall(prior, record) };
|
|
684
|
+
}
|
|
685
|
+
const now = Date.now();
|
|
686
|
+
if (record.status !== 'active' || !record.receipt || now >= record.receipt.expiresAt)
|
|
687
|
+
fail();
|
|
688
|
+
const global = this.db.prepare('SELECT COUNT(*) AS n FROM calls').get().n;
|
|
689
|
+
const local = this.db.prepare('SELECT COUNT(*) AS n FROM calls WHERE plan_id=?').get(input.planId).n;
|
|
690
|
+
if (global >= this.config.maxCallRecords || local >= this.config.maxCallsPerAdoption)
|
|
691
|
+
fail();
|
|
692
|
+
this.db.prepare("INSERT INTO calls(plan_id,call_key,arguments_digest,status,tool_alias,foreground_json,claimed_at) VALUES (?,?,?,'claimed',?,?,?)")
|
|
693
|
+
.run(input.planId, input.key, input.argumentsDigest, input.toolAlias ?? null, foreground, now);
|
|
694
|
+
return { created: true, call: { key: input.key, status: 'claimed' } };
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
settleCall(input) {
|
|
698
|
+
str(input.planId);
|
|
699
|
+
str(input.key);
|
|
700
|
+
if (input.status !== 'completed' && input.status !== 'unknown')
|
|
701
|
+
fail();
|
|
702
|
+
const resultJson = Object.hasOwn(input, 'result') ? json(input.result) : null;
|
|
703
|
+
const jobId = input.jobId === undefined ? null : str(input.jobId);
|
|
704
|
+
this.transaction(() => {
|
|
705
|
+
const row = this.db.prepare('SELECT * FROM calls WHERE plan_id=? AND call_key=?').get(input.planId, input.key);
|
|
706
|
+
if (!row || row.status !== 'claimed')
|
|
707
|
+
fail();
|
|
708
|
+
this.decodeCall(row);
|
|
709
|
+
this.db.prepare('UPDATE calls SET status=?,result_json=?,job_id=?,settled_at=? WHERE plan_id=? AND call_key=? AND status=\'claimed\'')
|
|
710
|
+
.run(input.status, resultJson, jobId, row.claimed_at === null ? null : Math.max(Date.now(), row.claimed_at), input.planId, input.key);
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
/** Host-only bounded metadata; never expose candidate arguments, results or private artifact bytes. */
|
|
714
|
+
listCallEvidence(planId) {
|
|
715
|
+
this.checkFiles();
|
|
716
|
+
const adoption = this.row(planId);
|
|
717
|
+
if (!adoption)
|
|
718
|
+
return [];
|
|
719
|
+
const record = this.decode(adoption);
|
|
720
|
+
const rows = this.db.prepare('SELECT * FROM calls WHERE plan_id=? ORDER BY call_key').all(planId);
|
|
721
|
+
if (rows.length > this.config.maxCallsPerAdoption || (rows.length > 0 && !record.receipt))
|
|
722
|
+
fail();
|
|
723
|
+
if (!record.receipt)
|
|
724
|
+
return [];
|
|
725
|
+
const receiptDigest = digest(record.receipt);
|
|
726
|
+
return rows.map(row => {
|
|
727
|
+
this.decodeCall(row, record);
|
|
728
|
+
const tool = record.tools?.find(item => item.name === row.tool_alias);
|
|
729
|
+
return { protocol: 'dsh-created-capability-call-evidence/v1',
|
|
730
|
+
planId, key: row.call_key, status: row.status,
|
|
731
|
+
attribution: row.claimed_at === null ? 'legacy-unattributed'
|
|
732
|
+
: row.foreground_json === null ? 'unattributed' : 'foreground',
|
|
733
|
+
...(row.tool_alias === null ? {} : { toolAlias: row.tool_alias }),
|
|
734
|
+
...(tool === undefined ? {} : { originalName: tool.originalName }),
|
|
735
|
+
receiptDigest, artifactSha256: record.receipt.artifactSha256, schemaDigest: record.receipt.schemaDigest,
|
|
736
|
+
...(row.claimed_at === null ? {} : { claimedAt: row.claimed_at }),
|
|
737
|
+
...(row.settled_at === null ? {} : { settledAt: row.settled_at }),
|
|
738
|
+
...(row.foreground_json === null ? {} : { foreground: JSON.parse(row.foreground_json) }),
|
|
739
|
+
};
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
recoverClaims() {
|
|
743
|
+
this.transaction(() => {
|
|
744
|
+
this.db.prepare("UPDATE adoptions SET status='unknown',reason='recovered-claim' WHERE status='claimed'").run();
|
|
745
|
+
this.db.prepare("UPDATE calls SET status='unknown',settled_at=CASE WHEN claimed_at IS NULL THEN NULL ELSE max(claimed_at,?) END WHERE status='claimed'")
|
|
746
|
+
.run(Date.now());
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
close() { this.db.close(); }
|
|
750
|
+
}
|
|
751
|
+
//# sourceMappingURL=creation-capability-journal.js.map
|