@dsh-enhanced/plugin-control-plane 0.1.66 → 0.1.68
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 +96 -22
- package/lib/cli.d.ts.map +1 -1
- package/lib/cli.js +66 -11
- package/lib/cli.js.map +1 -1
- package/lib/creation-capability-execution.d.ts +5 -1
- package/lib/creation-capability-execution.d.ts.map +1 -1
- package/lib/creation-capability-execution.js +42 -0
- package/lib/creation-capability-execution.js.map +1 -1
- package/lib/creation-capability-feedback.d.ts +16 -1
- package/lib/creation-capability-feedback.d.ts.map +1 -1
- package/lib/creation-capability-feedback.js +82 -5
- package/lib/creation-capability-feedback.js.map +1 -1
- package/lib/creation-capability-journal.d.ts.map +1 -1
- package/lib/creation-capability-journal.js +2 -0
- package/lib/creation-capability-journal.js.map +1 -1
- package/lib/creation-capability-runtime.d.ts +47 -1
- package/lib/creation-capability-runtime.d.ts.map +1 -1
- package/lib/creation-capability-runtime.js +105 -1
- package/lib/creation-capability-runtime.js.map +1 -1
- package/lib/creation-capability-types.d.ts +1 -0
- package/lib/creation-capability-types.d.ts.map +1 -1
- package/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/linux-source-build.d.ts +13 -2
- package/lib/linux-source-build.d.ts.map +1 -1
- package/lib/linux-source-build.js.map +1 -1
- package/lib/live-qualification-authority.js +2 -2
- package/lib/live-qualification-authority.js.map +1 -1
- package/lib/owner-instruction-drain.d.ts +60 -0
- package/lib/owner-instruction-drain.d.ts.map +1 -0
- package/lib/owner-instruction-drain.js +101 -0
- package/lib/owner-instruction-drain.js.map +1 -0
- package/lib/revision-capability-feedback.d.ts +165 -0
- package/lib/revision-capability-feedback.d.ts.map +1 -0
- package/lib/revision-capability-feedback.js +316 -0
- package/lib/revision-capability-feedback.js.map +1 -0
- package/lib/revision-capability-journal.d.ts +126 -0
- package/lib/revision-capability-journal.d.ts.map +1 -0
- package/lib/revision-capability-journal.js +981 -0
- package/lib/revision-capability-journal.js.map +1 -0
- package/lib/revision-capability-runtime.d.ts +92 -0
- package/lib/revision-capability-runtime.d.ts.map +1 -0
- package/lib/revision-capability-runtime.js +635 -0
- package/lib/revision-capability-runtime.js.map +1 -0
- package/lib/revision-capability-types.d.ts +335 -0
- package/lib/revision-capability-types.d.ts.map +1 -0
- package/lib/revision-capability-types.js +2 -0
- package/lib/revision-capability-types.js.map +1 -0
- package/lib/service.d.ts +199 -4
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +925 -70
- package/lib/service.js.map +1 -1
- package/lib/source-adoption-authority.js +2 -2
- package/lib/source-adoption-authority.js.map +1 -1
- package/lib/source-approval-authority.js +3 -3
- package/lib/source-approval-authority.js.map +1 -1
- package/lib/source-build.d.ts +33 -53
- package/lib/source-build.d.ts.map +1 -1
- package/lib/source-build.js +28 -408
- package/lib/source-build.js.map +1 -1
- package/lib/source-job-types.d.ts +111 -4
- package/lib/source-job-types.d.ts.map +1 -1
- package/lib/source-jobs.d.ts +72 -3
- package/lib/source-jobs.d.ts.map +1 -1
- package/lib/source-jobs.js +314 -43
- package/lib/source-jobs.js.map +1 -1
- package/lib/source-release-authority.js +2 -2
- package/lib/source-release-authority.js.map +1 -1
- package/lib/source-revision.d.ts +29 -3
- package/lib/source-revision.d.ts.map +1 -1
- package/lib/source-revision.js +40 -9
- package/lib/source-revision.js.map +1 -1
- package/lib/source-workspace.d.ts +11 -4
- package/lib/source-workspace.d.ts.map +1 -1
- package/lib/source-workspace.js +51 -18
- package/lib/source-workspace.js.map +1 -1
- package/lib/sqlite.d.ts +6 -7
- package/lib/sqlite.d.ts.map +1 -1
- package/lib/sqlite.js +255 -17
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +143 -4
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +739 -66
- package/lib/store.js.map +1 -1
- package/lib/systemd-host-authority.js +4 -4
- package/lib/systemd-host-authority.js.map +1 -1
- package/lib/task-observation-authority.js +2 -2
- package/lib/task-observation-authority.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/work-item-creation-contract.d.ts +3 -1
- package/lib/work-item-creation-contract.d.ts.map +1 -1
- package/lib/work-item-creation-contract.js +10 -1
- package/lib/work-item-creation-contract.js.map +1 -1
- package/package.json +11 -8
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalGrowthJson, isPluginRevisionTaskSource } from '@dsh-enhanced/assistant-growth-contract';
|
|
3
|
+
import { revisionCallArgumentsDigest } from './revision-capability-journal.js';
|
|
4
|
+
const sha = (value) => createHash('sha256').update(value).digest('hex');
|
|
5
|
+
const digest = (value) => sha(canonicalGrowthJson(value));
|
|
6
|
+
const safeName = /^[a-z][a-z0-9_-]{0,95}$/u;
|
|
7
|
+
const maxOutputBytes = 65_536;
|
|
8
|
+
const outputSchema = { type: 'object', additionalProperties: false, required: ['value', 'content'], properties: {
|
|
9
|
+
value: {}, content: { type: 'array', items: { type: 'object', additionalProperties: false,
|
|
10
|
+
required: ['type', 'text'], properties: { type: { type: 'string', const: 'text' }, text: { type: 'string' } } } },
|
|
11
|
+
} };
|
|
12
|
+
const ENTRY_PROTOCOL = 'dsh-created-capability-revision-entry/v1';
|
|
13
|
+
function fail(reason) { throw new Error('revision capability: ' + reason); }
|
|
14
|
+
function json(value, limit = 65_536) {
|
|
15
|
+
const encoded = canonicalGrowthJson(value);
|
|
16
|
+
if (Buffer.byteLength(encoded) > limit)
|
|
17
|
+
fail('JSON byte limit exceeded');
|
|
18
|
+
return encoded;
|
|
19
|
+
}
|
|
20
|
+
function sameEnvironment(a, b) {
|
|
21
|
+
return canonicalGrowthJson(a) === canonicalGrowthJson(b);
|
|
22
|
+
}
|
|
23
|
+
export class RevisionCapabilityRejected extends Error {
|
|
24
|
+
reason;
|
|
25
|
+
constructor(reason) { super('revision capability rejected: ' + reason); this.reason = reason; }
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Owns the stable tool entry of one already adopted creation lineage. Adoption reserves capacity in
|
|
29
|
+
* the append-only revision journal before any external action; the parent CAS decides whether this
|
|
30
|
+
* entry may switch at all; entry calls read the current version at claim time so a switch can never
|
|
31
|
+
* change an already running call.
|
|
32
|
+
*/
|
|
33
|
+
export class RevisionCapabilityRuntime {
|
|
34
|
+
ctx;
|
|
35
|
+
config;
|
|
36
|
+
journal;
|
|
37
|
+
ports;
|
|
38
|
+
regressionRequired;
|
|
39
|
+
abort = new AbortController();
|
|
40
|
+
mounted = new Map();
|
|
41
|
+
timers = new Map();
|
|
42
|
+
operations = new Set();
|
|
43
|
+
flights = new Set();
|
|
44
|
+
validateSchema;
|
|
45
|
+
admission = false;
|
|
46
|
+
started = false;
|
|
47
|
+
closed = false;
|
|
48
|
+
serial = Promise.resolve();
|
|
49
|
+
closeTask;
|
|
50
|
+
constructor(input) {
|
|
51
|
+
this.ctx = input.ctx;
|
|
52
|
+
this.config = input.config;
|
|
53
|
+
this.journal = input.journal;
|
|
54
|
+
this.ports = input.ports;
|
|
55
|
+
this.regressionRequired = input.regressionRequired;
|
|
56
|
+
}
|
|
57
|
+
enqueue(work) {
|
|
58
|
+
const result = this.serial.then(work);
|
|
59
|
+
this.serial = result.catch(() => { });
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
/** Read-only Host readiness; undefined once the controller is not admitting. */
|
|
63
|
+
inspectReadiness() {
|
|
64
|
+
if (!this.started || this.closed || !this.admission)
|
|
65
|
+
return undefined;
|
|
66
|
+
const usage = this.journal.usage();
|
|
67
|
+
const remainingAdoptions = Math.max(0, this.config.maxAdoptions - usage.adoptions);
|
|
68
|
+
const remainingRollbacks = Math.max(0, this.config.rollback.maxRollbacks - usage.rollbacks);
|
|
69
|
+
return { protocol: 'dsh-created-capability-revision-adoption-readiness/v1',
|
|
70
|
+
available: Date.now() < this.config.expiresAt && remainingAdoptions > 0,
|
|
71
|
+
authority: { authorityId: this.config.authorityId, expiresAt: this.config.expiresAt },
|
|
72
|
+
remainingAdoptions, remainingRollbacks, rollbackExpiresAt: this.config.rollback.expiresAt };
|
|
73
|
+
}
|
|
74
|
+
inspectEntry(entryId) {
|
|
75
|
+
try {
|
|
76
|
+
const record = this.journal.inspectEntry(entryId);
|
|
77
|
+
if (!record)
|
|
78
|
+
return undefined;
|
|
79
|
+
return { currentPlanId: record.entry.currentPlanId, parentPlanId: record.entry.parentPlanId,
|
|
80
|
+
aliases: record.entry.aliases, revision: record.entry.revision, expiresAt: record.current.expiresAt };
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Host-only content-free projection of every stable entry identity. */
|
|
87
|
+
listEntries() {
|
|
88
|
+
try {
|
|
89
|
+
return this.journal.listEntries().map(entry => Object.freeze({
|
|
90
|
+
entryId: entry.entryId, name: entry.name, currentPlanId: entry.currentPlanId,
|
|
91
|
+
parentPlanId: entry.parentPlanId, revision: entry.revision,
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
inspectCallEvidence(entryId) {
|
|
99
|
+
try {
|
|
100
|
+
return this.journal.listCallEvidence(entryId);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Host-only latest exact-parent rollback of one stable entry, for terminal reads. */
|
|
107
|
+
inspectLatestRollback(entryId) {
|
|
108
|
+
try {
|
|
109
|
+
return this.journal.listRollbacks(entryId).at(-1);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Host-only benefit evidence for one retained version: the exact adopted
|
|
117
|
+
* version plus its foreground-attributed calls. Never reads the creation
|
|
118
|
+
* ledger and never exposes artifacts or results.
|
|
119
|
+
*/
|
|
120
|
+
inspectBenefitEvidence(planId) {
|
|
121
|
+
try {
|
|
122
|
+
const version = this.journal.inspectVersion(planId);
|
|
123
|
+
if (version === undefined)
|
|
124
|
+
return undefined;
|
|
125
|
+
return Object.freeze({
|
|
126
|
+
version: Object.freeze({ planId: version.planId, entryId: version.entryId, role: version.role, name: version.name,
|
|
127
|
+
adoptedAt: version.adoptedAt, expiresAt: version.expiresAt, receiptDigest: version.receiptDigest,
|
|
128
|
+
artifactSha256: version.artifactSha256, schemaDigest: version.schemaDigest,
|
|
129
|
+
tools: Object.freeze(version.tools.map(tool => Object.freeze({ ...tool }))) }),
|
|
130
|
+
calls: this.journal.listBenefitCallEvidence(planId),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
assertCandidate(planId, inspection) {
|
|
138
|
+
const certificate = inspection.certificate;
|
|
139
|
+
if (certificate.plan.id !== planId || inspection.plan.id !== planId
|
|
140
|
+
|| certificate.plan.name !== inspection.plan.name || certificate.plan.digest !== inspection.plan.digest
|
|
141
|
+
|| !certificate.plan.name.startsWith(this.config.namePrefix)
|
|
142
|
+
|| certificate.plan.artifactSha256 !== sha(inspection.artifact)
|
|
143
|
+
|| certificate.plan.artifactBytes !== inspection.artifact.length
|
|
144
|
+
|| Date.now() >= certificate.expiresAt || Date.now() >= this.config.expiresAt
|
|
145
|
+
|| canonicalGrowthJson(inspection.owner) !== canonicalGrowthJson(this.config.owner)
|
|
146
|
+
|| !this.ports.matchesExecution(certificate)) {
|
|
147
|
+
throw new RevisionCapabilityRejected('candidate-ineligible');
|
|
148
|
+
}
|
|
149
|
+
if (this.regressionRequired) {
|
|
150
|
+
const regression = inspection.regression;
|
|
151
|
+
if (!regression || regression.candidateVerificationDigest !== digest(certificate)
|
|
152
|
+
|| regression.plan.digest !== certificate.plan.digest
|
|
153
|
+
|| canonicalGrowthJson(regression.parent) !== canonicalGrowthJson(certificate.parent)
|
|
154
|
+
|| Date.now() >= regression.expiresAt) {
|
|
155
|
+
throw new RevisionCapabilityRejected('regression-required');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else if (inspection.regression !== undefined
|
|
159
|
+
&& inspection.regression.candidateVerificationDigest !== digest(certificate)) {
|
|
160
|
+
throw new RevisionCapabilityRejected('regression-mismatch');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
assertParent(certificate, parent) {
|
|
164
|
+
if (parent.planId !== certificate.parent.planId
|
|
165
|
+
|| digest(parent.certificate) !== certificate.parent.certificateDigest
|
|
166
|
+
|| sha(parent.artifact) !== certificate.parent.artifactSha256
|
|
167
|
+
|| parent.certificate.plan.artifactBytes !== parent.artifact.length
|
|
168
|
+
|| parent.certificate.plan.name !== certificate.plan.name
|
|
169
|
+
|| !parent.certificate.plan.name.startsWith(this.config.namePrefix)
|
|
170
|
+
|| canonicalGrowthJson(parent.owner) !== canonicalGrowthJson(this.config.owner)
|
|
171
|
+
|| parent.certificate.schemaDigest !== certificate.schemaDigest
|
|
172
|
+
// The parent certificate is an independently verified creation
|
|
173
|
+
// certificate. Its source uses the creation digest keys while a revision
|
|
174
|
+
// certificate uses the revision source keys, so they are not comparable;
|
|
175
|
+
// the exact parent binding is already proven by
|
|
176
|
+
// certificate.parent.certificateDigest above.
|
|
177
|
+
|| parent.tools.length < 1 || parent.tools.length > 16) {
|
|
178
|
+
throw new RevisionCapabilityRejected('parent-adoption-mismatch');
|
|
179
|
+
}
|
|
180
|
+
const aliases = new Set();
|
|
181
|
+
for (const tool of parent.tools) {
|
|
182
|
+
if (!safeName.test(tool.name) || aliases.has(tool.name))
|
|
183
|
+
throw new RevisionCapabilityRejected('parent-adoption-mismatch');
|
|
184
|
+
aliases.add(tool.name);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
entryId(parentPlanId, aliases) {
|
|
188
|
+
return 'entry-' + digest({ protocol: ENTRY_PROTOCOL, parentPlanId, aliases }).slice(0, 32);
|
|
189
|
+
}
|
|
190
|
+
eligible(planId) {
|
|
191
|
+
try {
|
|
192
|
+
if (!this.started || this.closed || !this.admission || Date.now() >= this.config.expiresAt)
|
|
193
|
+
return false;
|
|
194
|
+
if (this.journal.inspectAdoption(planId))
|
|
195
|
+
return false;
|
|
196
|
+
if (this.journal.usage().adoptions >= this.config.maxAdoptions)
|
|
197
|
+
return false;
|
|
198
|
+
const inspection = this.ports.inspect(planId);
|
|
199
|
+
this.assertCandidate(planId, inspection);
|
|
200
|
+
const parent = this.ports.parent(inspection.certificate.parent.planId);
|
|
201
|
+
this.assertParent(inspection.certificate, parent);
|
|
202
|
+
const existing = this.journal.inspectEntry(this.entryId(inspection.certificate.parent.planId, parent.tools.map(tool => tool.name)));
|
|
203
|
+
if (existing && existing.entry.currentPlanId !== inspection.certificate.parent.planId)
|
|
204
|
+
return false;
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** Surfaces the exact ineligible reason (for example a parent binding mismatch). */
|
|
212
|
+
rejectAdoption(planId) {
|
|
213
|
+
try {
|
|
214
|
+
const inspection = this.ports.inspect(planId);
|
|
215
|
+
this.assertCandidate(planId, inspection);
|
|
216
|
+
this.assertParent(inspection.certificate, this.ports.parent(inspection.certificate.parent.planId));
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
if (error instanceof RevisionCapabilityRejected)
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
throw new RevisionCapabilityRejected('candidate-ineligible');
|
|
223
|
+
}
|
|
224
|
+
async adopt(planId, signal) {
|
|
225
|
+
return this.enqueue(async () => {
|
|
226
|
+
const controller = new AbortController();
|
|
227
|
+
this.operations.add(controller);
|
|
228
|
+
const cancel = () => controller.abort();
|
|
229
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
230
|
+
if (signal.aborted)
|
|
231
|
+
cancel();
|
|
232
|
+
const activeSignal = controller.signal;
|
|
233
|
+
try {
|
|
234
|
+
if (!this.eligible(planId))
|
|
235
|
+
this.rejectAdoption(planId);
|
|
236
|
+
activeSignal.throwIfAborted();
|
|
237
|
+
const inspection = this.ports.inspect(planId);
|
|
238
|
+
const certificate = inspection.certificate;
|
|
239
|
+
const parent = this.ports.parent(certificate.parent.planId);
|
|
240
|
+
this.assertCandidate(planId, inspection);
|
|
241
|
+
this.assertParent(certificate, parent);
|
|
242
|
+
const parentReceiptJson = json(JSON.parse(parent.receiptJson));
|
|
243
|
+
if (sha(parentReceiptJson) !== parent.receiptDigest)
|
|
244
|
+
throw new RevisionCapabilityRejected('parent-receipt-changed');
|
|
245
|
+
const parentReceipt = JSON.parse(parentReceiptJson);
|
|
246
|
+
await this.ports.recheck(planId, activeSignal);
|
|
247
|
+
await this.ports.recheckParent(certificate.parent.planId, activeSignal);
|
|
248
|
+
activeSignal.throwIfAborted();
|
|
249
|
+
const claim = this.journal.claim({ planId, parentPlanId: certificate.parent.planId,
|
|
250
|
+
certificateDigest: digest(certificate) });
|
|
251
|
+
if (!claim.created)
|
|
252
|
+
throw new RevisionCapabilityRejected('candidate-already-claimed');
|
|
253
|
+
const aliases = Object.freeze(parent.tools.map(tool => tool.name));
|
|
254
|
+
const entryId = this.entryId(certificate.parent.planId, aliases);
|
|
255
|
+
let entryMounted = false, parentSuspended = false, committed = false;
|
|
256
|
+
try {
|
|
257
|
+
activeSignal.throwIfAborted();
|
|
258
|
+
const pinned = this.ports.inspect(planId);
|
|
259
|
+
if (digest(pinned.certificate) !== digest(certificate) || sha(pinned.artifact) !== sha(inspection.artifact)) {
|
|
260
|
+
throw new RevisionCapabilityRejected('source-changed-before-claim');
|
|
261
|
+
}
|
|
262
|
+
await this.ports.recheckExecution(certificate, activeSignal);
|
|
263
|
+
activeSignal.throwIfAborted();
|
|
264
|
+
const discovery = await this.ports.runVersion({ certificate, artifact: inspection.artifact,
|
|
265
|
+
schemaDigest: certificate.schemaDigest, operation: { kind: 'discover' },
|
|
266
|
+
key: sha(json({ operation: 'discover', planId })), signal: activeSignal });
|
|
267
|
+
if (!this.observed(certificate, inspection.artifact, discovery)
|
|
268
|
+
|| discovery.schemaDigest !== certificate.schemaDigest || !discovery.schemas) {
|
|
269
|
+
throw new RevisionCapabilityRejected('discovery-mismatch');
|
|
270
|
+
}
|
|
271
|
+
const adoptedAt = Date.now();
|
|
272
|
+
const expiresAt = this.config.retention === undefined
|
|
273
|
+
? Math.min(this.config.expiresAt, certificate.expiresAt)
|
|
274
|
+
: Math.min(this.config.expiresAt, adoptedAt + this.config.retention.maximumLifetimeMs);
|
|
275
|
+
if (expiresAt <= adoptedAt || adoptedAt >= certificate.expiresAt) {
|
|
276
|
+
throw new RevisionCapabilityRejected('adoption-expired');
|
|
277
|
+
}
|
|
278
|
+
const inFlightParentCalls = this.ports.inFlightParentCalls(certificate.parent.planId);
|
|
279
|
+
const body = { protocol: 'dsh-created-capability-revision-adoption/v1',
|
|
280
|
+
authorityId: this.config.authorityId, authorityDigest: this.journal.authorityDigest,
|
|
281
|
+
keyId: this.config.keyId, planId, planDigest: certificate.plan.digest,
|
|
282
|
+
verificationDigest: digest(certificate), parentPlanId: certificate.parent.planId,
|
|
283
|
+
parentCertificateDigest: certificate.parent.certificateDigest,
|
|
284
|
+
parentReceiptDigest: parent.receiptDigest, parentArtifactSha256: certificate.parent.artifactSha256,
|
|
285
|
+
entryId, aliasesDigest: digest([...aliases]), artifactSha256: sha(inspection.artifact),
|
|
286
|
+
artifactBytes: inspection.artifact.length, schemaDigest: certificate.schemaDigest,
|
|
287
|
+
source: isPluginRevisionTaskSource(certificate.source)
|
|
288
|
+
? { referenceDigest: certificate.source.referenceDigest, ownerDigest: certificate.source.ownerDigest,
|
|
289
|
+
growthRunDigest: certificate.source.growthRunDigest }
|
|
290
|
+
: { instructionDigest: certificate.source.instructionDigest, ownerDigest: certificate.source.ownerDigest },
|
|
291
|
+
model: certificate.model,
|
|
292
|
+
...(inspection.regression === undefined ? {} : { regressionDigest: digest(inspection.regression) }),
|
|
293
|
+
inFlightParentCalls, expiresAt, adoptedAt };
|
|
294
|
+
const receipt = this.journal.signReceipt(body);
|
|
295
|
+
const toolVersions = parent.tools.map(tool => ({ originalName: tool.originalName, name: tool.name,
|
|
296
|
+
description: tool.description, parameters: structuredClone(tool.parameters) }));
|
|
297
|
+
const parentVersion = { planId: parent.planId, entryId, role: 'parent',
|
|
298
|
+
name: parent.certificate.plan.name, certificate: parent.certificate,
|
|
299
|
+
certificateDigest: digest(parent.certificate), receiptJson: parentReceiptJson,
|
|
300
|
+
receiptDigest: parent.receiptDigest, tools: toolVersions, artifact: parent.artifact,
|
|
301
|
+
artifactSha256: sha(parent.artifact), schemaDigest: parent.certificate.schemaDigest,
|
|
302
|
+
source: parent.certificate.source, model: parent.certificate.model,
|
|
303
|
+
adoptedAt: parentReceipt.adoptedAt, expiresAt: parentReceipt.expiresAt };
|
|
304
|
+
const receiptJson = canonicalGrowthJson(receipt);
|
|
305
|
+
const revisionVersion = { planId, entryId, role: 'revision',
|
|
306
|
+
name: certificate.plan.name, certificate, certificateDigest: digest(certificate),
|
|
307
|
+
receiptJson, receiptDigest: sha(receiptJson), tools: toolVersions,
|
|
308
|
+
artifact: Buffer.from(inspection.artifact), artifactSha256: sha(inspection.artifact),
|
|
309
|
+
schemaDigest: certificate.schemaDigest, source: body.source, model: body.model,
|
|
310
|
+
...(inspection.regression === undefined ? {} : { regressionDigest: digest(inspection.regression) }),
|
|
311
|
+
adoptedAt, expiresAt };
|
|
312
|
+
// Alias handover happens before the durable CAS so a failed swap keeps the parent entry.
|
|
313
|
+
await this.ports.suspendParent(parent.planId);
|
|
314
|
+
parentSuspended = true;
|
|
315
|
+
await this.mountEntry({ entryId, name: parent.certificate.plan.name, tools: toolVersions });
|
|
316
|
+
entryMounted = true;
|
|
317
|
+
activeSignal.throwIfAborted();
|
|
318
|
+
let record;
|
|
319
|
+
try {
|
|
320
|
+
record = this.journal.commit({ entry: { entryId, name: parent.certificate.plan.name, aliases },
|
|
321
|
+
parent: parentVersion, revision: revisionVersion, receipt,
|
|
322
|
+
expectedCurrentPlanId: parent.planId, inFlightParentCalls });
|
|
323
|
+
}
|
|
324
|
+
catch (commitError) {
|
|
325
|
+
const current = (() => { try {
|
|
326
|
+
return this.journal.inspectEntry(entryId);
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return undefined;
|
|
330
|
+
} })();
|
|
331
|
+
if (current && current.entry.currentPlanId !== parent.planId) {
|
|
332
|
+
throw new RevisionCapabilityRejected('parent-version-cas-conflict');
|
|
333
|
+
}
|
|
334
|
+
throw commitError;
|
|
335
|
+
}
|
|
336
|
+
committed = true;
|
|
337
|
+
this.armExpiry(entryId);
|
|
338
|
+
return record;
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
if (committed)
|
|
342
|
+
throw error;
|
|
343
|
+
let cleanupFailure;
|
|
344
|
+
try {
|
|
345
|
+
if (entryMounted)
|
|
346
|
+
await this.unmountEntry(entryId);
|
|
347
|
+
if (parentSuspended)
|
|
348
|
+
await this.ports.resumeParent(certificate.parent.planId);
|
|
349
|
+
}
|
|
350
|
+
catch (cleanupError) {
|
|
351
|
+
cleanupFailure = cleanupError;
|
|
352
|
+
}
|
|
353
|
+
const rejection = error instanceof RevisionCapabilityRejected ? error.reason : undefined;
|
|
354
|
+
try {
|
|
355
|
+
this.journal.settle(planId, rejection === undefined ? 'unknown' : 'rejected', rejection === undefined ? 'adoption-unsettled' : rejection);
|
|
356
|
+
}
|
|
357
|
+
catch { /* the durable claim remains a no-replay fence */ }
|
|
358
|
+
if (cleanupFailure !== undefined) {
|
|
359
|
+
throw new AggregateError([error, cleanupFailure], 'revision adoption swap cleanup unconfirmed');
|
|
360
|
+
}
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
finally {
|
|
365
|
+
signal.removeEventListener('abort', cancel);
|
|
366
|
+
this.operations.delete(controller);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
observed(certificate, artifact, result) {
|
|
371
|
+
if (result.status !== 'observed' || result.quiescent !== true
|
|
372
|
+
|| result.artifactSha256 !== certificate.plan.artifactSha256
|
|
373
|
+
|| certificate.plan.artifactSha256 !== sha(artifact)
|
|
374
|
+
|| result.schemaDigest !== certificate.schemaDigest
|
|
375
|
+
|| !sameEnvironment(result.environment, certificate.environment))
|
|
376
|
+
return false;
|
|
377
|
+
const protocol = certificate.protocol;
|
|
378
|
+
if (protocol === 'assistant-growth/creation-verification/v1' || protocol === 'assistant-growth/revision-verification/v1') {
|
|
379
|
+
return result.boundary === undefined || result.boundary === 'process-seccomp-v1';
|
|
380
|
+
}
|
|
381
|
+
const boundary = protocol === 'assistant-growth/creation-verification/v2'
|
|
382
|
+
? 'macos-seatbelt-single-process-v1' : 'linux-native-process-seccomp-v1';
|
|
383
|
+
return result.boundary === boundary && result.runtimeDigest === certificate.execution?.runtimeDigest;
|
|
384
|
+
}
|
|
385
|
+
async mountEntry(input) {
|
|
386
|
+
if (this.mounted.has(input.entryId))
|
|
387
|
+
return;
|
|
388
|
+
for (const tool of input.tools)
|
|
389
|
+
if (this.ctx.tools.get(tool.name))
|
|
390
|
+
fail('stable Host alias conflicts with an existing tool');
|
|
391
|
+
const definitions = input.tools.map(tool => ({ name: tool.name, description: tool.description,
|
|
392
|
+
parameters: tool.parameters, output: { schema: outputSchema,
|
|
393
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }] },
|
|
394
|
+
execute: (args, exec) => this.invoke(input.entryId, tool.name, args, exec) }));
|
|
395
|
+
for (const definition of definitions)
|
|
396
|
+
this.validateSchema(definition.parameters);
|
|
397
|
+
const plugin = { name: 'revision-capability-' + input.entryId.slice(0, 16), inject: ['tools'],
|
|
398
|
+
apply: (ctx) => { for (const definition of definitions)
|
|
399
|
+
ctx.tools.register(definition); } };
|
|
400
|
+
const fiber = this.ctx.plugin(plugin);
|
|
401
|
+
this.mounted.set(input.entryId, { fiber, aliases: definitions.map(definition => definition.name) });
|
|
402
|
+
const timeout = new AbortController();
|
|
403
|
+
const timer = setTimeout(() => timeout.abort(), 5_000);
|
|
404
|
+
try {
|
|
405
|
+
await Promise.race([fiber, new Promise((_resolve, reject) => {
|
|
406
|
+
timeout.signal.addEventListener('abort', () => reject(new Error('revision capability: Host activation interrupted')), { once: true });
|
|
407
|
+
})]);
|
|
408
|
+
if (fiber.state !== 2 || definitions.some(definition => this.ctx.tools.get(definition.name) !== definition)) {
|
|
409
|
+
fail('stable Host tool registration did not activate');
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
try {
|
|
414
|
+
await this.unmountEntry(input.entryId);
|
|
415
|
+
}
|
|
416
|
+
catch (disposeError) {
|
|
417
|
+
throw new AggregateError([error, disposeError], 'revision capability activation cleanup unconfirmed');
|
|
418
|
+
}
|
|
419
|
+
throw error;
|
|
420
|
+
}
|
|
421
|
+
finally {
|
|
422
|
+
clearTimeout(timer);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
armExpiry(entryId) {
|
|
426
|
+
const current = this.journal.inspectEntry(entryId);
|
|
427
|
+
if (!current)
|
|
428
|
+
return;
|
|
429
|
+
const deadline = Math.min(this.config.expiresAt, current.current.expiresAt);
|
|
430
|
+
const remaining = deadline - Date.now();
|
|
431
|
+
const previous = this.timers.get(entryId);
|
|
432
|
+
if (previous)
|
|
433
|
+
clearTimeout(previous);
|
|
434
|
+
const timer = setTimeout(() => {
|
|
435
|
+
this.timers.delete(entryId);
|
|
436
|
+
if (this.closed || !this.mounted.has(entryId))
|
|
437
|
+
return;
|
|
438
|
+
if (Date.now() < deadline)
|
|
439
|
+
this.armExpiry(entryId);
|
|
440
|
+
else
|
|
441
|
+
void this.reconcile().catch(() => { });
|
|
442
|
+
}, Math.max(1, Math.min(remaining, 2_147_483_647)));
|
|
443
|
+
this.timers.set(entryId, timer);
|
|
444
|
+
}
|
|
445
|
+
async unmountEntry(entryId) {
|
|
446
|
+
const timer = this.timers.get(entryId);
|
|
447
|
+
if (timer)
|
|
448
|
+
clearTimeout(timer);
|
|
449
|
+
this.timers.delete(entryId);
|
|
450
|
+
const mounted = this.mounted.get(entryId);
|
|
451
|
+
if (!mounted)
|
|
452
|
+
return;
|
|
453
|
+
await mounted.fiber.dispose();
|
|
454
|
+
if (mounted.aliases.some(name => this.ctx.tools.get(name)))
|
|
455
|
+
fail('stable Host tool remains after disposal');
|
|
456
|
+
this.mounted.delete(entryId);
|
|
457
|
+
}
|
|
458
|
+
async invoke(entryId, toolAlias, args, exec) {
|
|
459
|
+
const flight = this.invokeFlight(entryId, toolAlias, args, exec);
|
|
460
|
+
this.flights.add(flight);
|
|
461
|
+
void flight.then(() => this.flights.delete(flight), () => this.flights.delete(flight));
|
|
462
|
+
return flight;
|
|
463
|
+
}
|
|
464
|
+
async invokeFlight(entryId, toolAlias, args, exec) {
|
|
465
|
+
if (!this.admission || this.closed)
|
|
466
|
+
fail('capability unavailable');
|
|
467
|
+
const record = this.journal.inspectEntry(entryId);
|
|
468
|
+
if (!record || !record.entry.aliases.includes(toolAlias))
|
|
469
|
+
fail('capability unavailable');
|
|
470
|
+
if (!exec.agent)
|
|
471
|
+
fail('revision capability requires an authenticated owner Agent');
|
|
472
|
+
this.ports.assertCaller(exec);
|
|
473
|
+
const version = record.current;
|
|
474
|
+
if (Date.now() >= version.expiresAt || Date.now() >= this.config.expiresAt)
|
|
475
|
+
fail('authority or receipt expired');
|
|
476
|
+
const tool = version.tools.find(item => item.name === toolAlias);
|
|
477
|
+
if (!tool)
|
|
478
|
+
fail('capability unavailable');
|
|
479
|
+
const encoded = json(args, this.config.maxInputBytes);
|
|
480
|
+
// Delivery attests the native JSON.parse/JSON.stringify normalization; use
|
|
481
|
+
// the same text and digest so a witness exists for every key order.
|
|
482
|
+
const argumentsJson = JSON.stringify(args);
|
|
483
|
+
if (Buffer.byteLength(argumentsJson) > this.config.maxInputBytes)
|
|
484
|
+
fail('input exceeds bound');
|
|
485
|
+
const key = sha(json({ entryId, sessionId: String(exec.agent.session.id), callId: String(exec.callId),
|
|
486
|
+
toolName: toolAlias }));
|
|
487
|
+
this.ports.assertCaller(exec);
|
|
488
|
+
const foreground = this.ports.inspectCall?.({ entry: record.entry, current: version }, exec, toolAlias, argumentsJson);
|
|
489
|
+
if (foreground !== undefined && (foreground.task.sessionId !== String(exec.agent.session.id)
|
|
490
|
+
|| foreground.call.id !== String(exec.callId)))
|
|
491
|
+
fail('foreground call differs from current execution');
|
|
492
|
+
const call = this.journal.claimCall({ entryId, key, argumentsDigest: revisionCallArgumentsDigest(args), toolAlias,
|
|
493
|
+
versionPlanId: version.planId, ...(foreground === undefined ? {} : { foreground }) });
|
|
494
|
+
if (!call.created) {
|
|
495
|
+
if (call.call.status === 'completed')
|
|
496
|
+
return call.call.result;
|
|
497
|
+
fail('revision call already claimed or unknown');
|
|
498
|
+
}
|
|
499
|
+
try {
|
|
500
|
+
this.ports.assertCaller(exec);
|
|
501
|
+
const result = await this.ports.runVersion({ certificate: version.certificate, artifact: version.artifact,
|
|
502
|
+
schemaDigest: version.schemaDigest, operation: { kind: 'invoke', calls: [{ id: key,
|
|
503
|
+
toolName: tool.originalName, arguments: JSON.parse(encoded) }] },
|
|
504
|
+
key: sha(json({ operation: 'invoke', key })), signal: exec.signal });
|
|
505
|
+
if (!this.observed(version.certificate, version.artifact, result) || result.calls?.length !== 1
|
|
506
|
+
|| result.calls[0]?.id !== key || result.calls[0]?.toolName !== tool.originalName) {
|
|
507
|
+
fail('revision invocation unknown or changed');
|
|
508
|
+
}
|
|
509
|
+
const raw = result.calls[0].result;
|
|
510
|
+
if (!raw || typeof raw !== 'object' || raw.isError !== false || !Array.isArray(raw.content))
|
|
511
|
+
fail('candidate tool failed');
|
|
512
|
+
const content = raw.content.map(item => {
|
|
513
|
+
if (!item || typeof item !== 'object' || item.type !== 'text'
|
|
514
|
+
|| typeof item.text !== 'string')
|
|
515
|
+
fail('non-text candidate content');
|
|
516
|
+
return { type: 'text', text: item.text };
|
|
517
|
+
});
|
|
518
|
+
const value = { value: JSON.parse(json(raw.value, maxOutputBytes)), content };
|
|
519
|
+
json(value, maxOutputBytes);
|
|
520
|
+
this.journal.settleCall({ entryId, key, status: 'completed', result: value });
|
|
521
|
+
return value;
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
this.journal.settleCall({ entryId, key, status: 'unknown' });
|
|
525
|
+
throw error;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
async reconcileEntries() {
|
|
529
|
+
if (this.closed || !this.started)
|
|
530
|
+
return;
|
|
531
|
+
for (const entry of this.journal.listEntries()) {
|
|
532
|
+
if (this.closed)
|
|
533
|
+
return;
|
|
534
|
+
const record = this.journal.inspectEntry(entry.entryId);
|
|
535
|
+
if (!record)
|
|
536
|
+
continue;
|
|
537
|
+
if (Date.now() >= record.current.expiresAt || Date.now() >= this.config.expiresAt) {
|
|
538
|
+
await this.unmountEntry(entry.entryId).catch(() => { });
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (this.mounted.has(entry.entryId))
|
|
542
|
+
continue;
|
|
543
|
+
try {
|
|
544
|
+
await this.ports.suspendParent(record.parent.planId);
|
|
545
|
+
await this.mountEntry({ entryId: entry.entryId, name: record.entry.name, tools: record.parent.tools });
|
|
546
|
+
this.armExpiry(entry.entryId);
|
|
547
|
+
}
|
|
548
|
+
catch {
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
async reconcile() {
|
|
554
|
+
return this.enqueue(() => this.reconcileEntries());
|
|
555
|
+
}
|
|
556
|
+
async start() {
|
|
557
|
+
return this.enqueue(async () => {
|
|
558
|
+
if (this.started || this.closed)
|
|
559
|
+
fail('controller already started or closed');
|
|
560
|
+
const { assertObjectJsonSchema } = await import('@deepseek-ai/dsh-tools');
|
|
561
|
+
this.validateSchema = assertObjectJsonSchema;
|
|
562
|
+
this.started = true;
|
|
563
|
+
this.journal.recoverClaims();
|
|
564
|
+
this.admission = true;
|
|
565
|
+
await this.reconcileEntries();
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Host-only exact parent rollback under its own finite grant. The durable CAS requires the entry
|
|
570
|
+
* to be on the exact revision version and the retained parent receipt to be the one bound by that
|
|
571
|
+
* revision adoption; a missing/recycled parent or a changed version chain fails closed.
|
|
572
|
+
*/
|
|
573
|
+
async rollback(input) {
|
|
574
|
+
return this.enqueue(async () => {
|
|
575
|
+
if (!this.started || this.closed || !this.admission)
|
|
576
|
+
fail('controller unavailable');
|
|
577
|
+
const record = this.journal.inspectEntry(input.entryId);
|
|
578
|
+
if (!record)
|
|
579
|
+
fail('revision entry unavailable');
|
|
580
|
+
if (record.entry.currentPlanId !== input.expectedCurrentPlanId)
|
|
581
|
+
fail('revision entry version changed');
|
|
582
|
+
if (Date.now() >= this.config.rollback.expiresAt)
|
|
583
|
+
fail('rollback authority expired');
|
|
584
|
+
const usage = this.journal.usage();
|
|
585
|
+
if (usage.rollbacks >= this.config.rollback.maxRollbacks)
|
|
586
|
+
fail('rollback quota exhausted');
|
|
587
|
+
const signal = input.signal === undefined ? this.abort.signal : AbortSignal.any([this.abort.signal, input.signal]);
|
|
588
|
+
await this.ports.recheckParent(record.parent.planId, signal);
|
|
589
|
+
signal.throwIfAborted();
|
|
590
|
+
if (Date.now() >= record.parent.expiresAt)
|
|
591
|
+
fail('parent adoption receipt expired');
|
|
592
|
+
const result = this.journal.rollback({ entryId: input.entryId, expectedCurrentPlanId: input.expectedCurrentPlanId,
|
|
593
|
+
authorityId: this.config.rollback.authorityId });
|
|
594
|
+
this.armExpiry(input.entryId);
|
|
595
|
+
return result.record;
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
close() {
|
|
599
|
+
if (this.closeTask)
|
|
600
|
+
return this.closeTask;
|
|
601
|
+
this.admission = false;
|
|
602
|
+
this.closed = true;
|
|
603
|
+
this.abort.abort(new Error('revision capability provider disposed'));
|
|
604
|
+
for (const operation of this.operations)
|
|
605
|
+
operation.abort();
|
|
606
|
+
const attempt = this.enqueue(async () => {
|
|
607
|
+
await Promise.allSettled(this.flights);
|
|
608
|
+
const errors = [];
|
|
609
|
+
for (const timer of this.timers.values())
|
|
610
|
+
clearTimeout(timer);
|
|
611
|
+
this.timers.clear();
|
|
612
|
+
for (const entryId of this.mounted.keys()) {
|
|
613
|
+
try {
|
|
614
|
+
await this.unmountEntry(entryId);
|
|
615
|
+
}
|
|
616
|
+
catch (error) {
|
|
617
|
+
errors.push(error);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (!errors.length) {
|
|
621
|
+
try {
|
|
622
|
+
this.journal.close();
|
|
623
|
+
}
|
|
624
|
+
catch (error) {
|
|
625
|
+
errors.push(error);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (errors.length)
|
|
629
|
+
throw new AggregateError(errors, 'revision capability close could not prove release');
|
|
630
|
+
});
|
|
631
|
+
this.closeTask = attempt.catch(error => { this.closeTask = undefined; throw error; });
|
|
632
|
+
return this.closeTask;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
//# sourceMappingURL=revision-capability-runtime.js.map
|