@dsh-enhanced/plugin-control-plane 0.1.66 → 0.1.67
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 +42 -2
- 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-runtime.d.ts +47 -1
- package/lib/creation-capability-runtime.d.ts.map +1 -1
- package/lib/creation-capability-runtime.js +103 -1
- package/lib/creation-capability-runtime.js.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/revision-capability-journal.d.ts +103 -0
- package/lib/revision-capability-journal.d.ts.map +1 -0
- package/lib/revision-capability-journal.js +872 -0
- package/lib/revision-capability-journal.js.map +1 -0
- package/lib/revision-capability-runtime.d.ts +74 -0
- package/lib/revision-capability-runtime.d.ts.map +1 -0
- package/lib/revision-capability-runtime.js +562 -0
- package/lib/revision-capability-runtime.js.map +1 -0
- package/lib/revision-capability-types.d.ts +289 -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 +25 -1
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +194 -3
- package/lib/service.js.map +1 -1
- package/lib/source-jobs.d.ts +5 -0
- package/lib/source-jobs.d.ts.map +1 -1
- package/lib/source-jobs.js +25 -4
- package/lib/source-jobs.js.map +1 -1
- package/lib/source-workspace.d.ts.map +1 -1
- package/lib/source-workspace.js +0 -1
- package/lib/source-workspace.js.map +1 -1
- package/lib/store.d.ts +1 -1
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +7 -3
- package/lib/store.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +8 -8
package/lib/service.js
CHANGED
|
@@ -2,6 +2,8 @@ import { cleanedLinuxSourceBuildFailure, cleanedLinuxSourcePreflight, consumeCle
|
|
|
2
2
|
import { AdoptionCoordinatorRuntime, validateAdoptionCoordinatorConfig } from './adoption-coordinator.js';
|
|
3
3
|
import { CreationCapabilityJournal, creationCapabilityAuthorityDigest, creationCapabilityPublicKey, validateCreationCapabilityConfig } from './creation-capability-journal.js';
|
|
4
4
|
import { CreationCapabilityRuntime } from './creation-capability-runtime.js';
|
|
5
|
+
import { RevisionCapabilityJournal, revisionCapabilityPublicKey, validateRevisionCapabilityConfig } from './revision-capability-journal.js';
|
|
6
|
+
import { RevisionCapabilityRuntime } from './revision-capability-runtime.js';
|
|
5
7
|
import { inspectCreationCapabilityAvailabilityAttribution, inspectCreationCapabilityOrdinaryUseReport, inspectCreationCapabilityOrdinaryUseTrend, inspectCreationCapabilityTaskAssociations } from './creation-capability-feedback.js';
|
|
6
8
|
import { captureCreationCapabilitySource } from './creation-capability-source.js';
|
|
7
9
|
import { lstat, readFile, realpath } from 'node:fs/promises';
|
|
@@ -57,6 +59,7 @@ const schema = Schema.object({
|
|
|
57
59
|
revisionVerifications: Schema.any(),
|
|
58
60
|
revisionRegressions: Schema.any(),
|
|
59
61
|
creationCapabilities: Schema.any(),
|
|
62
|
+
revisionCapabilities: Schema.any(),
|
|
60
63
|
sourceApprovals: Schema.any(),
|
|
61
64
|
sourceReleases: Schema.any(),
|
|
62
65
|
sourceReleaseExecution: Schema.any(),
|
|
@@ -149,7 +152,8 @@ export function normalizeControlPlaneConfig(input) {
|
|
|
149
152
|
if (config.sourceBuild !== undefined && 'backend' in config.sourceBuild && config.sourceBuild.backend === 'linux-native') {
|
|
150
153
|
if (!config.sourceJobs)
|
|
151
154
|
throw new Error('plugin-control-plane: Linux repository preparation requires durable sourceJobs');
|
|
152
|
-
if ((config.creationVerifications || config.creationCapabilities || config.revisionVerifications || config.revisionRegressions
|
|
155
|
+
if ((config.creationVerifications || config.creationCapabilities || config.revisionVerifications || config.revisionRegressions
|
|
156
|
+
|| config.revisionCapabilities)
|
|
153
157
|
&& (!config.sourceBuild.userNamespace || !config.sourceBuild.systemSnapshot)) {
|
|
154
158
|
throw new Error('plugin-control-plane: Linux independent verification requires the dual-UID repository boundary');
|
|
155
159
|
}
|
|
@@ -262,6 +266,39 @@ export function normalizeControlPlaneConfig(input) {
|
|
|
262
266
|
if (adoptionKey.equals(verificationKey))
|
|
263
267
|
throw new Error('plugin-control-plane: adoption and verification signing keys must be separate');
|
|
264
268
|
}
|
|
269
|
+
if (config.revisionCapabilities !== undefined) {
|
|
270
|
+
validateRevisionCapabilityConfig(config.revisionCapabilities);
|
|
271
|
+
const capability = config.revisionCapabilities;
|
|
272
|
+
const creation = config.creationCapabilities;
|
|
273
|
+
const verification = config.revisionVerifications;
|
|
274
|
+
const jobs = config.sourceJobs;
|
|
275
|
+
const build = config.sourceBuild;
|
|
276
|
+
if (!creation || !verification || !jobs?.revision || !build) {
|
|
277
|
+
throw new Error('plugin-control-plane: revision adoption requires creation capabilities, revision verification and finite revision source');
|
|
278
|
+
}
|
|
279
|
+
const buildBackend = 'backend' in build ? build.backend : 'docker';
|
|
280
|
+
const runnerBackend = 'backend' in creation.runner ? creation.runner.backend : 'docker';
|
|
281
|
+
if (buildBackend !== runnerBackend
|
|
282
|
+
|| capability.namePrefix !== creation.namePrefix || capability.namePrefix !== jobs.revision.namePrefix
|
|
283
|
+
|| controlPlaneDigest(capability.owner) !== controlPlaneDigest(creation.owner)
|
|
284
|
+
|| capability.expiresAt > creation.expiresAt) {
|
|
285
|
+
throw new Error('plugin-control-plane: revision adoption authority does not match its authenticated creation lane');
|
|
286
|
+
}
|
|
287
|
+
if (capability.authorityId === creation.authorityId || capability.authorityId === verification.authority.authorityId
|
|
288
|
+
|| capability.rollback.authorityId === capability.authorityId || capability.rollback.authorityId === creation.authorityId
|
|
289
|
+
|| capability.rollback.authorityId === verification.authority.authorityId) {
|
|
290
|
+
throw new Error('plugin-control-plane: revision adoption, rollback, verification and creation authorities must be separate');
|
|
291
|
+
}
|
|
292
|
+
if (capability.keyId === creation.keyId || capability.keyId === verification.authority.keyId) {
|
|
293
|
+
throw new Error('plugin-control-plane: revision adoption signing key must differ from acceptance and creation keys');
|
|
294
|
+
}
|
|
295
|
+
const revisionKey = createPublicKey(revisionCapabilityPublicKey(capability)).export({ format: 'der', type: 'spki' });
|
|
296
|
+
const creationKey = createPublicKey(creationCapabilityPublicKey(creation)).export({ format: 'der', type: 'spki' });
|
|
297
|
+
const verificationKey = createPublicKey(verification.publicKey).export({ format: 'der', type: 'spki' });
|
|
298
|
+
if (revisionKey.equals(creationKey) || revisionKey.equals(verificationKey)) {
|
|
299
|
+
throw new Error('plugin-control-plane: revision adoption signing key material must be separate');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
265
302
|
if (![config.catalogPath, config.statePath, config.trustPath].every(isAbsolute))
|
|
266
303
|
throw new Error('plugin-control-plane: catalogPath, statePath and trustPath must be absolute');
|
|
267
304
|
return config;
|
|
@@ -285,6 +322,7 @@ export class PluginControlPlaneService extends Service {
|
|
|
285
322
|
nativeSourceRunner;
|
|
286
323
|
sourceRuntimes = new Set();
|
|
287
324
|
creationCapabilityRuntime;
|
|
325
|
+
revisionCapabilityRuntime;
|
|
288
326
|
creationCapabilityRuntimes = new Set();
|
|
289
327
|
creationCapabilityFlights = new Set();
|
|
290
328
|
creationCapabilityReconcileRequested = false;
|
|
@@ -662,6 +700,97 @@ export class PluginControlPlaneService extends Service {
|
|
|
662
700
|
throw error;
|
|
663
701
|
}
|
|
664
702
|
}
|
|
703
|
+
let revisionCapabilities;
|
|
704
|
+
if (this.config.revisionCapabilities) {
|
|
705
|
+
const config = this.config.revisionCapabilities;
|
|
706
|
+
const delivery = current('delivery');
|
|
707
|
+
if (typeof delivery.validateOwnerAgentForRoute !== 'function') {
|
|
708
|
+
throw new Error('plugin-control-plane: revision adoption requires authenticated live owner Agent admission');
|
|
709
|
+
}
|
|
710
|
+
const journal = new RevisionCapabilityJournal({ path: join(this.config.statePath, 'revision-adoptions.sqlite'), config });
|
|
711
|
+
try {
|
|
712
|
+
revisionCapabilities = new RevisionCapabilityRuntime({ ctx: jobsCtx, config, journal,
|
|
713
|
+
regressionRequired: this.config.revisionRegressions !== undefined,
|
|
714
|
+
ports: {
|
|
715
|
+
inspect: planId => {
|
|
716
|
+
const certificate = this.inspectVerifiedRevision(planId);
|
|
717
|
+
if (!certificate)
|
|
718
|
+
throw new Error('revision adoption requires a fresh independent certificate');
|
|
719
|
+
const prepared = this.inspectPreparedRevision(planId);
|
|
720
|
+
const source = this.store.readRevisionSource(planId);
|
|
721
|
+
if (!source)
|
|
722
|
+
throw new Error('revision adoption source archive unavailable');
|
|
723
|
+
const regression = this.config.revisionRegressions === undefined
|
|
724
|
+
? undefined : this.inspectVerifiedRevisionRegression(planId);
|
|
725
|
+
return { certificate, artifact: Buffer.from(prepared.artifact), source,
|
|
726
|
+
plan: { id: prepared.plan.id, digest: prepared.plan.digest, name: prepared.plan.name },
|
|
727
|
+
...(regression === undefined ? {} : { regression }), owner: config.owner };
|
|
728
|
+
},
|
|
729
|
+
recheck: async (planId, signal) => {
|
|
730
|
+
await this.inspectPreparedRevisionReviewContext(planId, signal);
|
|
731
|
+
if (this.config.sourceBuild && 'backend' in this.config.sourceBuild
|
|
732
|
+
&& this.config.sourceBuild.backend === 'linux-native') {
|
|
733
|
+
await this.inspectPreparedRevisionLinuxBuild(planId, signal);
|
|
734
|
+
}
|
|
735
|
+
if (!this.inspectVerifiedRevision(planId))
|
|
736
|
+
throw new Error('revision adoption independent certificate changed');
|
|
737
|
+
},
|
|
738
|
+
parent: planId => {
|
|
739
|
+
const adoption = capabilities?.inspectAdoptionVersion(planId);
|
|
740
|
+
if (!adoption)
|
|
741
|
+
throw new Error('revision adoption parent adoption unavailable');
|
|
742
|
+
return adoption;
|
|
743
|
+
},
|
|
744
|
+
recheckParent: async (planId, signal) => {
|
|
745
|
+
if (!capabilities)
|
|
746
|
+
throw new Error('revision adoption parent adoption unavailable');
|
|
747
|
+
await capabilities.recheckAdoptionVersion(planId, signal);
|
|
748
|
+
},
|
|
749
|
+
inFlightParentCalls: planId => capabilities?.inspectInFlightCalls(planId) ?? 0,
|
|
750
|
+
suspendParent: async (planId) => {
|
|
751
|
+
if (!capabilities)
|
|
752
|
+
throw new Error('revision adoption parent adoption unavailable');
|
|
753
|
+
await capabilities.suspendEntry(planId);
|
|
754
|
+
},
|
|
755
|
+
resumeParent: async (planId) => {
|
|
756
|
+
if (!capabilities)
|
|
757
|
+
throw new Error('revision adoption parent adoption unavailable');
|
|
758
|
+
await capabilities.resumeEntry(planId);
|
|
759
|
+
},
|
|
760
|
+
runVersion: input => {
|
|
761
|
+
if (!capabilities)
|
|
762
|
+
throw new Error('revision adoption runner unavailable');
|
|
763
|
+
return capabilities.runRevisionArtifact(input);
|
|
764
|
+
},
|
|
765
|
+
recheckExecution: async (certificate, signal) => {
|
|
766
|
+
if (!capabilities)
|
|
767
|
+
throw new Error('revision adoption runner unavailable');
|
|
768
|
+
await capabilities.verifyArtifactExecution(certificate, signal);
|
|
769
|
+
},
|
|
770
|
+
matchesExecution: certificate => capabilities?.matchesArtifactExecution(certificate) === true,
|
|
771
|
+
assertCaller: execution => {
|
|
772
|
+
if (!execution.agent)
|
|
773
|
+
throw new Error('revision capability requires an authenticated owner Agent');
|
|
774
|
+
const receipt = delivery.validateOwnerAgentForRoute(execution.agent, config.owner.authorityId);
|
|
775
|
+
if (!receipt || Object.keys(config.owner).some(key => receipt[key] !== config.owner[key])) {
|
|
776
|
+
throw new Error('revision capability caller is not the current authorized owner');
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
} });
|
|
780
|
+
}
|
|
781
|
+
catch (error) {
|
|
782
|
+
journal.close();
|
|
783
|
+
throw error;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
const closeRevision = async () => {
|
|
787
|
+
const currentRuntime = revisionCapabilities;
|
|
788
|
+
revisionCapabilities = undefined;
|
|
789
|
+
if (this.revisionCapabilityRuntime === currentRuntime)
|
|
790
|
+
this.revisionCapabilityRuntime = undefined;
|
|
791
|
+
if (currentRuntime)
|
|
792
|
+
await currentRuntime.close();
|
|
793
|
+
};
|
|
665
794
|
const sourceBuild = this.config.sourceBuild;
|
|
666
795
|
let nativeRunner;
|
|
667
796
|
try {
|
|
@@ -686,6 +815,10 @@ export class PluginControlPlaneService extends Service {
|
|
|
686
815
|
}
|
|
687
816
|
catch (error) {
|
|
688
817
|
unsubscribeTaskChanges?.();
|
|
818
|
+
try {
|
|
819
|
+
await closeRevision();
|
|
820
|
+
}
|
|
821
|
+
catch { /* original error wins */ }
|
|
689
822
|
if (capabilities) {
|
|
690
823
|
await capabilities.close();
|
|
691
824
|
this.creationCapabilityRuntimes.delete(capabilities);
|
|
@@ -699,6 +832,7 @@ export class PluginControlPlaneService extends Service {
|
|
|
699
832
|
// Restored fresh adoptions read their original Linux build receipt.
|
|
700
833
|
// Their source runner must exist before capability reconciliation.
|
|
701
834
|
await capabilities?.start();
|
|
835
|
+
await revisionCapabilities?.start();
|
|
702
836
|
this.abort.signal.throwIfAborted();
|
|
703
837
|
runtime = new SourceJobRuntime({ config: this.config.sourceJobs, build: sourceBuild, statePath: this.config.statePath, store: this.store,
|
|
704
838
|
ports: {
|
|
@@ -724,6 +858,12 @@ export class PluginControlPlaneService extends Service {
|
|
|
724
858
|
throw new Error('created capability has no prepared source plan');
|
|
725
859
|
await capabilities.adopt(job.planId, signal);
|
|
726
860
|
} } : {}),
|
|
861
|
+
...(revisionCapabilities ? { revisionAdoptionEligible: (planId) => revisionCapabilities.eligible(planId),
|
|
862
|
+
adoptVerifiedRevision: async (job, signal) => {
|
|
863
|
+
if (!job.planId)
|
|
864
|
+
throw new Error('revision adoption has no prepared source plan');
|
|
865
|
+
await revisionCapabilities.adopt(job.planId, signal);
|
|
866
|
+
} } : {}),
|
|
727
867
|
...(this.config.sourceApprovals ? { approvePrepared: async (job, signal) => {
|
|
728
868
|
if (!job.planId)
|
|
729
869
|
throw new Error('source job has no prepared plan');
|
|
@@ -757,6 +897,10 @@ export class PluginControlPlaneService extends Service {
|
|
|
757
897
|
this.nativeSourceRunner = undefined;
|
|
758
898
|
await nativeRunner?.runner.close();
|
|
759
899
|
unsubscribeTaskChanges?.();
|
|
900
|
+
try {
|
|
901
|
+
await closeRevision();
|
|
902
|
+
}
|
|
903
|
+
catch { /* original error wins */ }
|
|
760
904
|
if (capabilities) {
|
|
761
905
|
await capabilities.close();
|
|
762
906
|
this.creationCapabilityRuntimes.delete(capabilities);
|
|
@@ -765,6 +909,7 @@ export class PluginControlPlaneService extends Service {
|
|
|
765
909
|
}
|
|
766
910
|
this.sourceRuntimes.add(runtime);
|
|
767
911
|
this.creationCapabilityRuntime = capabilities;
|
|
912
|
+
this.revisionCapabilityRuntime = revisionCapabilities;
|
|
768
913
|
const close = async () => {
|
|
769
914
|
unsubscribeTaskChanges?.();
|
|
770
915
|
if (this.sourceRuntime === runtime)
|
|
@@ -781,12 +926,20 @@ export class PluginControlPlaneService extends Service {
|
|
|
781
926
|
this.sourceRuntimes.delete(runtime);
|
|
782
927
|
if (this.creationCapabilityRuntime === capabilities)
|
|
783
928
|
this.creationCapabilityRuntime = undefined;
|
|
929
|
+
let revisionCloseError;
|
|
930
|
+
try {
|
|
931
|
+
await closeRevision();
|
|
932
|
+
}
|
|
933
|
+
catch (error) {
|
|
934
|
+
revisionCloseError = error;
|
|
935
|
+
}
|
|
784
936
|
const results = await Promise.allSettled([nativeRunner?.runner.close(), capabilities?.close()]);
|
|
785
937
|
if (capabilities)
|
|
786
938
|
this.creationCapabilityRuntimes.delete(capabilities);
|
|
787
939
|
const failures = results.filter((result) => result.status === 'rejected');
|
|
788
|
-
if (closeError !== undefined || failures.length) {
|
|
789
|
-
throw new AggregateError([...(closeError === undefined ? [] : [closeError]),
|
|
940
|
+
if (closeError !== undefined || revisionCloseError !== undefined || failures.length) {
|
|
941
|
+
throw new AggregateError([...(closeError === undefined ? [] : [closeError]),
|
|
942
|
+
...(revisionCloseError === undefined ? [] : [revisionCloseError]), ...failures.map(result => result.reason)], 'source runtime cleanup failed');
|
|
790
943
|
}
|
|
791
944
|
};
|
|
792
945
|
try {
|
|
@@ -1145,6 +1298,44 @@ export class PluginControlPlaneService extends Service {
|
|
|
1145
1298
|
this.abort.signal.throwIfAborted();
|
|
1146
1299
|
return this.creationCapabilityRuntime?.inspectCallEvidence(planId) ?? [];
|
|
1147
1300
|
};
|
|
1301
|
+
/** Host-only read-only revision adoption readiness; undefined when not configured or not admitting. */
|
|
1302
|
+
inspectRevisionAdoptionReadiness = () => {
|
|
1303
|
+
this.abort.signal.throwIfAborted();
|
|
1304
|
+
return this.revisionCapabilityRuntime?.inspectReadiness();
|
|
1305
|
+
};
|
|
1306
|
+
/** Host-only current stable entry version; no certificate, artifact bytes or key material escape. */
|
|
1307
|
+
inspectRevisionCapabilityEntry = (entryId) => {
|
|
1308
|
+
this.abort.signal.throwIfAborted();
|
|
1309
|
+
return this.revisionCapabilityRuntime?.inspectEntry(entryId);
|
|
1310
|
+
};
|
|
1311
|
+
/** Host-only bounded call attribution of one stable entry, including the exact version per call. */
|
|
1312
|
+
inspectRevisionCapabilityCalls = (entryId) => {
|
|
1313
|
+
this.abort.signal.throwIfAborted();
|
|
1314
|
+
return this.revisionCapabilityRuntime?.inspectCallEvidence(entryId) ?? [];
|
|
1315
|
+
};
|
|
1316
|
+
/**
|
|
1317
|
+
* Host-only authenticated-owner exact parent rollback under its own finite grant. The parent
|
|
1318
|
+
* adoption receipt must still be current and the entry must be on the exact expected revision.
|
|
1319
|
+
*/
|
|
1320
|
+
rollbackRevisionCapabilityAdoption = async (input) => {
|
|
1321
|
+
this.abort.signal.throwIfAborted();
|
|
1322
|
+
const runtime = this.revisionCapabilityRuntime;
|
|
1323
|
+
const config = this.config.revisionCapabilities;
|
|
1324
|
+
if (!runtime || !config)
|
|
1325
|
+
throw new Error('revision adoption is not configured');
|
|
1326
|
+
const delivery = this.ctx.get('assistantDelivery', false);
|
|
1327
|
+
if (!delivery || typeof delivery.validateOwnerRoute !== 'function')
|
|
1328
|
+
throw new Error('revision rollback requires Delivery owner validation');
|
|
1329
|
+
const receipt = delivery.validateOwnerRoute({ authorityId: config.owner.authorityId, principalId: config.owner.principalId,
|
|
1330
|
+
workspace: config.owner.workspace, agentPreset: config.owner.agentPreset });
|
|
1331
|
+
if (!receipt || receipt.authorityId !== config.owner.authorityId || receipt.authorityHash !== config.owner.authorityHash
|
|
1332
|
+
|| receipt.principalId !== config.owner.principalId || receipt.principalRecordId !== config.owner.principalRecordId
|
|
1333
|
+
|| receipt.principalVersion !== config.owner.principalVersion || receipt.workspace !== config.owner.workspace
|
|
1334
|
+
|| receipt.agentPreset !== config.owner.agentPreset) {
|
|
1335
|
+
throw new Error('revision rollback caller is not the current authorized owner');
|
|
1336
|
+
}
|
|
1337
|
+
return runtime.rollback({ entryId: input.entryId, expectedCurrentPlanId: input.expectedVersionPlanId });
|
|
1338
|
+
};
|
|
1148
1339
|
/** Host-only historical source data. It grants no continuation, activation or replacement. */
|
|
1149
1340
|
inspectCreatedCapabilitySource = (planId) => {
|
|
1150
1341
|
this.abort.signal.throwIfAborted();
|