@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.
Files changed (43) hide show
  1. package/README.md +42 -2
  2. package/lib/creation-capability-execution.d.ts +5 -1
  3. package/lib/creation-capability-execution.d.ts.map +1 -1
  4. package/lib/creation-capability-execution.js +42 -0
  5. package/lib/creation-capability-execution.js.map +1 -1
  6. package/lib/creation-capability-runtime.d.ts +47 -1
  7. package/lib/creation-capability-runtime.d.ts.map +1 -1
  8. package/lib/creation-capability-runtime.js +103 -1
  9. package/lib/creation-capability-runtime.js.map +1 -1
  10. package/lib/index.d.ts +3 -0
  11. package/lib/index.d.ts.map +1 -1
  12. package/lib/index.js +2 -0
  13. package/lib/index.js.map +1 -1
  14. package/lib/revision-capability-journal.d.ts +103 -0
  15. package/lib/revision-capability-journal.d.ts.map +1 -0
  16. package/lib/revision-capability-journal.js +872 -0
  17. package/lib/revision-capability-journal.js.map +1 -0
  18. package/lib/revision-capability-runtime.d.ts +74 -0
  19. package/lib/revision-capability-runtime.d.ts.map +1 -0
  20. package/lib/revision-capability-runtime.js +562 -0
  21. package/lib/revision-capability-runtime.js.map +1 -0
  22. package/lib/revision-capability-types.d.ts +289 -0
  23. package/lib/revision-capability-types.d.ts.map +1 -0
  24. package/lib/revision-capability-types.js +2 -0
  25. package/lib/revision-capability-types.js.map +1 -0
  26. package/lib/service.d.ts +25 -1
  27. package/lib/service.d.ts.map +1 -1
  28. package/lib/service.js +194 -3
  29. package/lib/service.js.map +1 -1
  30. package/lib/source-jobs.d.ts +5 -0
  31. package/lib/source-jobs.d.ts.map +1 -1
  32. package/lib/source-jobs.js +25 -4
  33. package/lib/source-jobs.js.map +1 -1
  34. package/lib/source-workspace.d.ts.map +1 -1
  35. package/lib/source-workspace.js +0 -1
  36. package/lib/source-workspace.js.map +1 -1
  37. package/lib/store.d.ts +1 -1
  38. package/lib/store.d.ts.map +1 -1
  39. package/lib/store.js +7 -3
  40. package/lib/store.js.map +1 -1
  41. package/lib/version.d.ts +1 -1
  42. package/lib/version.js +1 -1
  43. 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]), ...failures.map(result => result.reason)], 'source runtime cleanup failed');
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();