@dsh-enhanced/plugin-control-plane 0.1.32 → 0.1.34

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 (199) hide show
  1. package/README.md +352 -4
  2. package/bin/dsh-local-release-adapter.js +80 -16
  3. package/bin/dsh-npm-registry-adapter.js +613 -0
  4. package/bin/dsh-source-adoption-authority.js +12 -0
  5. package/bin/dsh-source-approval-authority.js +16 -0
  6. package/bin/dsh-source-release-authority.js +12 -0
  7. package/bin/dsh-systemd-host-attestor.js +707 -0
  8. package/bin/dsh-task-observation-authority.js +15 -0
  9. package/lib/adapter-process.d.ts +23 -0
  10. package/lib/adapter-process.d.ts.map +1 -0
  11. package/lib/adapter-process.js +305 -0
  12. package/lib/adapter-process.js.map +1 -0
  13. package/lib/adoption-coordinator.d.ts +63 -0
  14. package/lib/adoption-coordinator.d.ts.map +1 -0
  15. package/lib/adoption-coordinator.js +241 -0
  16. package/lib/adoption-coordinator.js.map +1 -0
  17. package/lib/adoption-handoff.d.ts +16 -0
  18. package/lib/adoption-handoff.d.ts.map +1 -0
  19. package/lib/adoption-handoff.js +11 -0
  20. package/lib/adoption-handoff.js.map +1 -0
  21. package/lib/attestation.d.ts.map +1 -1
  22. package/lib/attestation.js +49 -1
  23. package/lib/attestation.js.map +1 -1
  24. package/lib/catalog.d.ts.map +1 -1
  25. package/lib/catalog.js +29 -2
  26. package/lib/catalog.js.map +1 -1
  27. package/lib/cli.d.ts +33 -9
  28. package/lib/cli.d.ts.map +1 -1
  29. package/lib/cli.js +624 -178
  30. package/lib/cli.js.map +1 -1
  31. package/lib/deployment-readiness.d.ts +38 -0
  32. package/lib/deployment-readiness.d.ts.map +1 -0
  33. package/lib/deployment-readiness.js +154 -0
  34. package/lib/deployment-readiness.js.map +1 -0
  35. package/lib/effect-blocked-replay.d.ts +81 -0
  36. package/lib/effect-blocked-replay.d.ts.map +1 -0
  37. package/lib/effect-blocked-replay.js +259 -0
  38. package/lib/effect-blocked-replay.js.map +1 -0
  39. package/lib/errors.d.ts +6 -0
  40. package/lib/errors.d.ts.map +1 -0
  41. package/lib/errors.js +9 -0
  42. package/lib/errors.js.map +1 -0
  43. package/lib/foreground-deployment-runtime.d.ts +20 -0
  44. package/lib/foreground-deployment-runtime.d.ts.map +1 -0
  45. package/lib/foreground-deployment-runtime.js +79 -0
  46. package/lib/foreground-deployment-runtime.js.map +1 -0
  47. package/lib/foreground-deployment.d.ts +17 -0
  48. package/lib/foreground-deployment.d.ts.map +1 -0
  49. package/lib/foreground-deployment.js +43 -0
  50. package/lib/foreground-deployment.js.map +1 -0
  51. package/lib/host-attestor.d.ts +1 -1
  52. package/lib/host-attestor.d.ts.map +1 -1
  53. package/lib/host-attestor.js +51 -57
  54. package/lib/host-attestor.js.map +1 -1
  55. package/lib/index.d.ts +25 -2
  56. package/lib/index.d.ts.map +1 -1
  57. package/lib/index.js +11 -2
  58. package/lib/index.js.map +1 -1
  59. package/lib/npm-publish.d.ts +25 -0
  60. package/lib/npm-publish.d.ts.map +1 -0
  61. package/lib/npm-publish.js +280 -0
  62. package/lib/npm-publish.js.map +1 -0
  63. package/lib/owner-task-gap-types.d.ts +21 -0
  64. package/lib/owner-task-gap-types.d.ts.map +1 -0
  65. package/lib/owner-task-gap-types.js +2 -0
  66. package/lib/owner-task-gap-types.js.map +1 -0
  67. package/lib/owner-task-gaps.d.ts +24 -0
  68. package/lib/owner-task-gaps.d.ts.map +1 -0
  69. package/lib/owner-task-gaps.js +74 -0
  70. package/lib/owner-task-gaps.js.map +1 -0
  71. package/lib/post-activation.d.ts +33 -0
  72. package/lib/post-activation.d.ts.map +1 -0
  73. package/lib/post-activation.js +219 -0
  74. package/lib/post-activation.js.map +1 -0
  75. package/lib/registry-fetch.d.ts +50 -0
  76. package/lib/registry-fetch.d.ts.map +1 -0
  77. package/lib/registry-fetch.js +267 -0
  78. package/lib/registry-fetch.js.map +1 -0
  79. package/lib/release.d.ts +4 -4
  80. package/lib/release.d.ts.map +1 -1
  81. package/lib/release.js +142 -56
  82. package/lib/release.js.map +1 -1
  83. package/lib/replay-endpoint-protocol.d.ts +65 -0
  84. package/lib/replay-endpoint-protocol.d.ts.map +1 -0
  85. package/lib/replay-endpoint-protocol.js +183 -0
  86. package/lib/replay-endpoint-protocol.js.map +1 -0
  87. package/lib/replay-endpoint.d.ts +7 -0
  88. package/lib/replay-endpoint.d.ts.map +1 -0
  89. package/lib/replay-endpoint.js +193 -0
  90. package/lib/replay-endpoint.js.map +1 -0
  91. package/lib/replay-grant.d.ts +62 -0
  92. package/lib/replay-grant.d.ts.map +1 -0
  93. package/lib/replay-grant.js +208 -0
  94. package/lib/replay-grant.js.map +1 -0
  95. package/lib/replay-journal.d.ts +25 -0
  96. package/lib/replay-journal.d.ts.map +1 -0
  97. package/lib/replay-journal.js +275 -0
  98. package/lib/replay-journal.js.map +1 -0
  99. package/lib/runtime-observer-protocol.d.ts +63 -0
  100. package/lib/runtime-observer-protocol.d.ts.map +1 -0
  101. package/lib/runtime-observer-protocol.js +280 -0
  102. package/lib/runtime-observer-protocol.js.map +1 -0
  103. package/lib/runtime-observer.d.ts +8 -0
  104. package/lib/runtime-observer.d.ts.map +1 -0
  105. package/lib/runtime-observer.js +182 -0
  106. package/lib/runtime-observer.js.map +1 -0
  107. package/lib/service.d.ts +145 -1
  108. package/lib/service.d.ts.map +1 -1
  109. package/lib/service.js +734 -6
  110. package/lib/service.js.map +1 -1
  111. package/lib/source-adoption-authority.d.ts +65 -0
  112. package/lib/source-adoption-authority.d.ts.map +1 -0
  113. package/lib/source-adoption-authority.js +271 -0
  114. package/lib/source-adoption-authority.js.map +1 -0
  115. package/lib/source-adoption-runner.d.ts +28 -0
  116. package/lib/source-adoption-runner.d.ts.map +1 -0
  117. package/lib/source-adoption-runner.js +197 -0
  118. package/lib/source-adoption-runner.js.map +1 -0
  119. package/lib/source-approval-authority.d.ts +50 -0
  120. package/lib/source-approval-authority.d.ts.map +1 -0
  121. package/lib/source-approval-authority.js +412 -0
  122. package/lib/source-approval-authority.js.map +1 -0
  123. package/lib/source-approval-client.d.ts +32 -0
  124. package/lib/source-approval-client.d.ts.map +1 -0
  125. package/lib/source-approval-client.js +175 -0
  126. package/lib/source-approval-client.js.map +1 -0
  127. package/lib/source-build.d.ts +60 -0
  128. package/lib/source-build.d.ts.map +1 -0
  129. package/lib/source-build.js +311 -0
  130. package/lib/source-build.js.map +1 -0
  131. package/lib/source-context.d.ts +24 -0
  132. package/lib/source-context.d.ts.map +1 -0
  133. package/lib/source-context.js +99 -0
  134. package/lib/source-context.js.map +1 -0
  135. package/lib/source-job-types.d.ts +87 -0
  136. package/lib/source-job-types.d.ts.map +1 -0
  137. package/lib/source-job-types.js +2 -0
  138. package/lib/source-job-types.js.map +1 -0
  139. package/lib/source-jobs.d.ts +113 -0
  140. package/lib/source-jobs.d.ts.map +1 -0
  141. package/lib/source-jobs.js +535 -0
  142. package/lib/source-jobs.js.map +1 -0
  143. package/lib/source-release-authority.d.ts +35 -0
  144. package/lib/source-release-authority.d.ts.map +1 -0
  145. package/lib/source-release-authority.js +271 -0
  146. package/lib/source-release-authority.js.map +1 -0
  147. package/lib/source-release-client.d.ts +10 -0
  148. package/lib/source-release-client.d.ts.map +1 -0
  149. package/lib/source-release-client.js +24 -0
  150. package/lib/source-release-client.js.map +1 -0
  151. package/lib/source-release-runner.d.ts +31 -0
  152. package/lib/source-release-runner.d.ts.map +1 -0
  153. package/lib/source-release-runner.js +125 -0
  154. package/lib/source-release-runner.js.map +1 -0
  155. package/lib/source-versioning.d.ts +24 -0
  156. package/lib/source-versioning.d.ts.map +1 -0
  157. package/lib/source-versioning.js +213 -0
  158. package/lib/source-versioning.js.map +1 -0
  159. package/lib/source-workspace.d.ts +117 -0
  160. package/lib/source-workspace.d.ts.map +1 -0
  161. package/lib/source-workspace.js +458 -0
  162. package/lib/source-workspace.js.map +1 -0
  163. package/lib/sqlite.d.ts +1 -1
  164. package/lib/sqlite.d.ts.map +1 -1
  165. package/lib/sqlite.js +509 -7
  166. package/lib/sqlite.js.map +1 -1
  167. package/lib/store.d.ts +229 -6
  168. package/lib/store.d.ts.map +1 -1
  169. package/lib/store.js +2431 -383
  170. package/lib/store.js.map +1 -1
  171. package/lib/task-observation-authority.d.ts +29 -0
  172. package/lib/task-observation-authority.d.ts.map +1 -0
  173. package/lib/task-observation-authority.js +370 -0
  174. package/lib/task-observation-authority.js.map +1 -0
  175. package/lib/task-observation-client.d.ts +4 -0
  176. package/lib/task-observation-client.d.ts.map +1 -0
  177. package/lib/task-observation-client.js +20 -0
  178. package/lib/task-observation-client.js.map +1 -0
  179. package/lib/task-observation-runtime.d.ts +59 -0
  180. package/lib/task-observation-runtime.d.ts.map +1 -0
  181. package/lib/task-observation-runtime.js +331 -0
  182. package/lib/task-observation-runtime.js.map +1 -0
  183. package/lib/task-observation-store.d.ts +17 -0
  184. package/lib/task-observation-store.d.ts.map +1 -0
  185. package/lib/task-observation-store.js +115 -0
  186. package/lib/task-observation-store.js.map +1 -0
  187. package/lib/task-observation-types.d.ts +67 -0
  188. package/lib/task-observation-types.d.ts.map +1 -0
  189. package/lib/task-observation-types.js +2 -0
  190. package/lib/task-observation-types.js.map +1 -0
  191. package/lib/trust.d.ts +3 -0
  192. package/lib/trust.d.ts.map +1 -1
  193. package/lib/trust.js +0 -0
  194. package/lib/trust.js.map +1 -1
  195. package/lib/types.d.ts +275 -8
  196. package/lib/types.d.ts.map +1 -1
  197. package/lib/version.d.ts +1 -1
  198. package/lib/version.js +1 -1
  199. package/package.json +40 -5
package/lib/service.js CHANGED
@@ -1,14 +1,46 @@
1
+ import { AdoptionCoordinatorRuntime, validateAdoptionCoordinatorConfig } from './adoption-coordinator.js';
1
2
  import { lstat, realpath } from 'node:fs/promises';
3
+ import { realpathSync } from 'node:fs';
2
4
  import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
5
+ import { TaskObservationRuntime, validateTaskObservationConfig } from './task-observation-runtime.js';
6
+ import { rollbackPluginWatch } from './cli.js';
7
+ import { OwnerTaskFailureGaps } from './owner-task-gaps.js';
3
8
  import { Context, Service } from '@deepseek-ai/cordis';
4
9
  import Schema from '@deepseek-ai/schemastery';
5
10
  import { discover, loadCatalogWithMetadata } from './catalog.js';
6
- import { ControlPlaneStore } from './store.js';
7
- import { loadTrustConfig } from './trust.js';
11
+ import { ControlPlaneCliError } from './errors.js';
12
+ import { assertPluginModificationAllowed, createIsolatedWorktree, gcPreparedModifyWorktrees, runLocalCommand, validateScopedPluginFiles, verifyPreparedSourceWorktree, writeScopedPluginFiles, } from './source-workspace.js';
13
+ import { assertManagedVersionPaths, managedPatchVersionFiles, verifyManagedPatchVersion } from './source-versioning.js';
14
+ import { requestSourceApproval, validateSourceApprovalClientConfig } from './source-approval-client.js';
15
+ import { requestSourceReleaseAuthorization, validateSourceReleaseClientConfig } from './source-release-client.js';
16
+ import { Ed25519SourceReleaseAuthorizationAuthority } from './release.js';
17
+ import { advanceSourceRelease, sourceReleaseAuthorities, validateSourceReleaseExecutionConfig } from './source-release-runner.js';
18
+ import { adoptSourceRelease, validateSourceAdoptionConfig } from './source-adoption-runner.js';
19
+ import { Ed25519ApprovalAuthority } from './approval.js';
20
+ import { ControlPlaneStore, MODIFY_GENERATOR_DIGEST, controlPlaneDigest } from './store.js';
21
+ import { runDockerPreparedChecks, validateSourceBuildConfig } from './source-build.js';
22
+ import { awaitSourceSignal, inspectSourceContext } from './source-context.js';
23
+ import { inheritedEnvironment, loadTrustConfig, resolveTrustKey } from './trust.js';
8
24
  import { registerPluginControlTools } from './tools.js';
25
+ import { SourceJobRuntime, validateSourceJobsConfig } from './source-jobs.js';
26
+ import { installRuntimeObserver, validateRuntimeObserverConfig } from './runtime-observer.js';
27
+ import { installReplayEndpoint, validateReplayEndpointConfig } from './replay-endpoint.js';
28
+ import { readPrivateRuntimeObserverKey } from './runtime-observer-protocol.js';
29
+ import { createForegroundDeploymentObserver, foregroundTrustSnapshot, validateForegroundDeploymentConfig } from './foreground-deployment-runtime.js';
9
30
  const schema = Schema.object({
10
31
  catalogPath: Schema.string().required(), statePath: Schema.string().required(), trustPath: Schema.string().required(),
11
32
  proposalTtlMs: Schema.number().step(1).min(60_000).max(86_400_000).default(900_000),
33
+ sourceBuild: Schema.any(),
34
+ sourceJobs: Schema.any(),
35
+ sourceApprovals: Schema.any(),
36
+ sourceReleases: Schema.any(),
37
+ sourceReleaseExecution: Schema.any(),
38
+ sourceAdoptions: Schema.any(),
39
+ adoptionCoordinator: Schema.any(),
40
+ runtimeObserver: Schema.any(),
41
+ foregroundDeployments: Schema.any(),
42
+ taskObservations: Schema.any(),
43
+ replayEndpoint: Schema.any(),
12
44
  });
13
45
  async function canonicalTarget(dshHome, profile) {
14
46
  const profiles = join(dshHome, 'profiles');
@@ -28,19 +60,311 @@ async function canonicalTarget(dshHome, profile) {
28
60
  }
29
61
  return Object.freeze({ dshHome, profile, profilePath });
30
62
  }
63
+ /**
64
+ * Read-only deployment preflight for profile installers. It validates and clones
65
+ * control-plane configuration without opening a ledger, mounting a service,
66
+ * starting effects, or creating runtime resources. Existing path/key validators
67
+ * may read deployment files.
68
+ */
69
+ export function normalizeControlPlaneConfig(input) {
70
+ const config = structuredClone(schema(input));
71
+ if (config.runtimeObserver !== undefined)
72
+ validateRuntimeObserverConfig(config.runtimeObserver);
73
+ if (config.foregroundDeployments !== undefined) {
74
+ validateForegroundDeploymentConfig(config.foregroundDeployments);
75
+ if (!config.runtimeObserver)
76
+ throw new Error('plugin-control-plane: foregroundDeployments requires runtimeObserver');
77
+ }
78
+ if (config.taskObservations !== undefined) {
79
+ validateTaskObservationConfig(config.taskObservations);
80
+ if (!config.foregroundDeployments || config.taskObservations.profilePath !== config.runtimeObserver?.profilePath) {
81
+ throw new Error('plugin-control-plane: taskObservations requires foregroundDeployments on the same profile');
82
+ }
83
+ }
84
+ if (config.replayEndpoint !== undefined) {
85
+ validateReplayEndpointConfig(config.replayEndpoint);
86
+ if (config.runtimeObserver !== undefined) {
87
+ const observer = config.runtimeObserver, replay = config.replayEndpoint.runtime;
88
+ if (observer.socketPath === replay.socketPath || observer.keyPath === replay.keyPath)
89
+ throw new Error('plugin-control-plane: replay requires a separate socket and key');
90
+ const observerKey = readPrivateRuntimeObserverKey(observer.keyPath), replayKey = readPrivateRuntimeObserverKey(replay.keyPath);
91
+ try {
92
+ if (observerKey.equals(replayKey))
93
+ throw new Error('plugin-control-plane: replay requires distinct key material');
94
+ }
95
+ finally {
96
+ observerKey.fill(0);
97
+ replayKey.fill(0);
98
+ }
99
+ }
100
+ }
101
+ if (config.sourceApprovals !== undefined)
102
+ validateSourceApprovalClientConfig(config.sourceApprovals);
103
+ if (config.sourceBuild !== undefined)
104
+ validateSourceBuildConfig(config.sourceBuild);
105
+ if (config.sourceReleases !== undefined) {
106
+ validateSourceReleaseClientConfig(config.sourceReleases);
107
+ if (!config.sourceApprovals || config.sourceBuild?.versioning !== 'patch')
108
+ throw new Error('plugin-control-plane: sourceReleases requires sourceApprovals and Host patch versioning');
109
+ }
110
+ if (config.sourceReleaseExecution !== undefined) {
111
+ validateSourceReleaseExecutionConfig(config.sourceReleaseExecution);
112
+ if (!config.sourceReleases)
113
+ throw new Error('plugin-control-plane: sourceReleaseExecution requires sourceReleases');
114
+ }
115
+ if (config.adoptionCoordinator !== undefined) {
116
+ validateAdoptionCoordinatorConfig(config.adoptionCoordinator);
117
+ if (config.sourceJobs || config.sourceAdoptions || config.runtimeObserver || config.replayEndpoint) {
118
+ throw new Error('plugin-control-plane: adoptionCoordinator requires a separate Host from target source jobs and observation');
119
+ }
120
+ }
121
+ if (config.sourceAdoptions !== undefined) {
122
+ validateSourceAdoptionConfig(config.sourceAdoptions);
123
+ if (!config.sourceReleaseExecution)
124
+ throw new Error('plugin-control-plane: sourceAdoptions requires sourceReleaseExecution');
125
+ }
126
+ if (config.sourceJobs !== undefined) {
127
+ validateSourceJobsConfig(config.sourceJobs, config.sourceBuild);
128
+ if (realpathSync(config.sourceJobs.repository) !== config.sourceJobs.repository)
129
+ throw new Error('plugin-control-plane: sourceJobs.repository must be canonical');
130
+ }
131
+ if (![config.catalogPath, config.statePath, config.trustPath].every(isAbsolute))
132
+ throw new Error('plugin-control-plane: catalogPath, statePath and trustPath must be absolute');
133
+ return config;
134
+ }
31
135
  export class PluginControlPlaneService extends Service {
32
136
  static Config = schema;
33
137
  config;
34
138
  store;
139
+ taskGaps;
140
+ abort = new AbortController();
141
+ sourceBuilds = new Set();
142
+ sourceInspections = new Set();
143
+ sourceApprovalFlights = new Set();
144
+ sourceReleaseFlights = new Set();
145
+ sourceReleaseAdvances = new Map();
146
+ sourceAdoptionFlights = new Map();
147
+ foregroundObservers = new Set();
148
+ sourceRuntime;
149
+ sourceRuntimes = new Set();
35
150
  constructor(ctx, input) {
36
151
  super(ctx, 'pluginControlPlane');
37
- this.config = schema(input);
38
- if (![this.config.catalogPath, this.config.statePath, this.config.trustPath].every(isAbsolute))
39
- throw new Error('plugin-control-plane: catalogPath, statePath and trustPath must be absolute');
152
+ this.config = normalizeControlPlaneConfig(input);
40
153
  this.store = new ControlPlaneStore({ path: join(this.config.statePath, 'control.sqlite') });
41
- ctx.effect(() => () => this.store.close(), 'plugin-control-plane.store');
154
+ this.taskGaps = new OwnerTaskFailureGaps(this.store, () => {
155
+ this.abort.signal.throwIfAborted();
156
+ const delivery = ctx.get('assistantDelivery', false);
157
+ const evaluation = ctx.get('assistantEvaluation', false);
158
+ if (typeof delivery?.inspectOwnerForegroundLearningTask !== 'function'
159
+ || typeof evaluation?.withTrustedCanonicalTaskWriterFence !== 'function')
160
+ throw new Error('plugin-control-plane: owner task source services unavailable');
161
+ return { delivery, evaluation };
162
+ });
163
+ ctx.effect(() => async () => {
164
+ this.abort.abort();
165
+ await Promise.allSettled([...this.sourceRuntimes].map(runtime => runtime.close()));
166
+ await Promise.allSettled([...this.sourceBuilds, ...this.sourceInspections, ...this.sourceApprovalFlights, ...this.sourceReleaseFlights, ...this.sourceAdoptionFlights.values()]);
167
+ this.store.close();
168
+ }, 'plugin-control-plane.store');
42
169
  ctx.inject(['tools'], toolsCtx => registerPluginControlTools(toolsCtx, this));
170
+ if (this.config.runtimeObserver !== undefined)
171
+ installRuntimeObserver(ctx, this.config.runtimeObserver, this.config.foregroundDeployments ? (observerCtx, sample) => {
172
+ const fiber = observerCtx.inject(['assistantDelivery'], deliveryCtx => {
173
+ deliveryCtx.effect(async () => {
174
+ const snapshot = foregroundTrustSnapshot(this.config.trustPath);
175
+ const trust = await this.boundTrust();
176
+ let live = true;
177
+ const assertCurrent = () => {
178
+ this.abort.signal.throwIfAborted();
179
+ if (!live || foregroundTrustSnapshot(this.config.trustPath) !== snapshot)
180
+ throw new Error('foreground deployment trust changed');
181
+ };
182
+ assertCurrent();
183
+ const registration = createForegroundDeploymentObserver({ config: this.config.foregroundDeployments,
184
+ profilePath: this.config.runtimeObserver.profilePath, store: this.store, trust, sample, assertCurrent, owner: this });
185
+ this.foregroundObservers.add(registration);
186
+ try {
187
+ const delivery = deliveryCtx.get('assistantDelivery');
188
+ const remove = delivery.registerForegroundTaskObserver(registration);
189
+ return () => { live = false; this.foregroundObservers.delete(registration); remove(); };
190
+ }
191
+ catch (error) {
192
+ live = false;
193
+ this.foregroundObservers.delete(registration);
194
+ throw error;
195
+ }
196
+ }, 'plugin-control-plane.foreground-deployments');
197
+ });
198
+ return () => fiber.dispose();
199
+ } : undefined);
200
+ if (this.config.replayEndpoint !== undefined)
201
+ installReplayEndpoint(ctx, this.config.replayEndpoint);
202
+ if (this.config.adoptionCoordinator !== undefined)
203
+ ctx.inject(['assistantAutomations'], coordinatorCtx => {
204
+ coordinatorCtx.effect(async () => {
205
+ const snapshot = foregroundTrustSnapshot(this.config.trustPath), trust = await this.boundTrust();
206
+ this.abort.signal.throwIfAborted();
207
+ const store = new ControlPlaneStore({ path: trust.ledger.path, adoptionCoordinatorId: this.config.adoptionCoordinator.coordinatorId });
208
+ const automations = () => coordinatorCtx.get('assistantAutomations');
209
+ let runtime;
210
+ try {
211
+ runtime = new AdoptionCoordinatorRuntime({ config: this.config.adoptionCoordinator, store, trust,
212
+ assertCurrent: () => {
213
+ this.abort.signal.throwIfAborted();
214
+ if (foregroundTrustSnapshot(this.config.trustPath) !== snapshot)
215
+ throw new Error('adoption coordinator trust changed');
216
+ },
217
+ automations: {
218
+ registerHostExecutor: input => automations().registerHostExecutor(input),
219
+ reconcileSystem: input => automations().reconcileSystem(input),
220
+ inspectSystemOwnedActivation: input => automations().inspectSystemOwnedActivation(input),
221
+ }, });
222
+ runtime.start();
223
+ return () => runtime.close();
224
+ }
225
+ catch (error) {
226
+ if (runtime)
227
+ await runtime.close();
228
+ else
229
+ store.close();
230
+ throw error;
231
+ }
232
+ }, 'plugin-control-plane.adoption-coordinator');
233
+ });
234
+ if (this.config.taskObservations !== undefined)
235
+ ctx.inject(['assistantAutomations', 'assistantDelivery', 'assistantEvaluation'], observerCtx => {
236
+ observerCtx.effect(async () => {
237
+ const snapshot = foregroundTrustSnapshot(this.config.trustPath), trust = await this.boundTrust();
238
+ this.abort.signal.throwIfAborted();
239
+ const store = new ControlPlaneStore({ path: trust.ledger.path });
240
+ const evaluation = () => observerCtx.get('assistantEvaluation');
241
+ const delivery = () => observerCtx.get('assistantDelivery');
242
+ const automations = () => observerCtx.get('assistantAutomations');
243
+ let runtime;
244
+ try {
245
+ runtime = new TaskObservationRuntime({ config: this.config.taskObservations, store, trust,
246
+ assertCurrent: () => {
247
+ this.abort.signal.throwIfAborted();
248
+ if (foregroundTrustSnapshot(this.config.trustPath) !== snapshot)
249
+ throw new Error('task observation trust changed');
250
+ },
251
+ evaluation: {
252
+ canonicalHostScope: input => evaluation().canonicalHostScope(input),
253
+ getTrustedForegroundLearningProjection: input => evaluation().getTrustedForegroundLearningProjection(input),
254
+ withTrustedCanonicalTaskWriterFence: (input, callback) => evaluation().withTrustedCanonicalTaskWriterFence(input, callback),
255
+ onTrustedTaskChange: callback => evaluation().onTrustedTaskChange(callback),
256
+ },
257
+ delivery: {
258
+ validateOwnerRoute: input => delivery().validateOwnerRoute(input),
259
+ inspectOwnerForegroundLearningTask: input => delivery().inspectOwnerForegroundLearningTask(input),
260
+ },
261
+ automations: {
262
+ registerHostExecutor: input => automations().registerHostExecutor(input),
263
+ reconcileSystem: input => automations().reconcileSystem(input),
264
+ inspectSystemOwnedActivation: input => automations().inspectSystemOwnedActivation(input),
265
+ },
266
+ rollback: (planId, signal) => rollbackPluginWatch({ store, trust, planId, signal }), });
267
+ runtime.start();
268
+ return () => runtime.close();
269
+ }
270
+ catch (error) {
271
+ if (runtime)
272
+ await runtime.close();
273
+ else
274
+ store.close();
275
+ throw error;
276
+ }
277
+ }, 'plugin-control-plane.task-observations');
278
+ });
279
+ if (this.config.sourceJobs !== undefined)
280
+ ctx.inject(['assistantAutomations', 'assistantDelivery',
281
+ ...(this.config.sourceApprovals ? ['assistantEvaluation'] : []),
282
+ ...(this.config.sourceReleaseExecution?.independentReview ? ['assistantVerifier', 'agents', 'sessions', 'tools', 'llm', 'systemPrompt', 'assistantPolicy'] : [])], jobsCtx => {
283
+ jobsCtx.effect(async () => {
284
+ this.abort.signal.throwIfAborted();
285
+ const current = (key) => jobsCtx.get((key === 'automations' ? 'assistantAutomations' : 'assistantDelivery'));
286
+ for (const method of ['registerHostExecutor', 'reconcileSystem', 'inspectSystemOwnedActivation', 'inspectSystemOwned']) {
287
+ if (typeof current('automations')[method] !== 'function')
288
+ throw new Error(`plugin-control-plane: durable source jobs require assistantAutomations.${method}`);
289
+ }
290
+ if (typeof current('delivery').validateOwnerRoute !== 'function')
291
+ throw new Error('plugin-control-plane: durable source jobs require Delivery v2 owner validation');
292
+ const runtime = new SourceJobRuntime({ config: this.config.sourceJobs, build: this.config.sourceBuild, statePath: this.config.statePath, store: this.store,
293
+ ports: {
294
+ automations: {
295
+ registerHostExecutor: executor => current('automations').registerHostExecutor(executor),
296
+ reconcileSystem: request => current('automations').reconcileSystem(request),
297
+ inspectSystemOwnedActivation: request => current('automations').inspectSystemOwnedActivation(request),
298
+ inspectSystemOwned: request => current('automations').inspectSystemOwned(request),
299
+ },
300
+ delivery: { validateOwnerRoute: request => current('delivery').validateOwnerRoute(request) },
301
+ }, withGapSourceFence: (gapId, owner, callback) => this.taskGaps.withCurrent(gapId, owner, callback),
302
+ ...(this.config.sourceApprovals ? { approvePrepared: async (job, signal) => {
303
+ if (!job.planId)
304
+ throw new Error('source job has no prepared plan');
305
+ await this.requestOwnerSourceApproval({ planId: job.planId, signal, expectedTrustDigest: job.intent.trustDigest });
306
+ } } : {}),
307
+ ...(this.config.sourceReleases ? { releasePrepared: async (job, signal) => {
308
+ if (!job.planId)
309
+ throw new Error('source job has no prepared plan');
310
+ await this.requestOwnerSourceRelease({ planId: job.planId, signal, expectedTrustDigest: job.intent.trustDigest });
311
+ } } : {}),
312
+ ...(this.config.sourceReleaseExecution ? { releaseTimeoutMs: this.config.sourceReleaseExecution.timeoutMs,
313
+ advanceReleased: async (job, signal) => {
314
+ if (!job.planId)
315
+ throw new Error('source job has no release plan');
316
+ await this.advanceOwnerSourceRelease({ planId: job.planId, signal, expectedTrustDigest: job.intent.trustDigest });
317
+ } } : {}),
318
+ ...(this.config.sourceAdoptions ? { adoptionTimeoutMs: this.config.sourceAdoptions.timeoutMs,
319
+ adoptReleased: async (job, signal, assertCurrent) => {
320
+ if (!job.planId)
321
+ throw new Error('source job has no released plan');
322
+ await this.adoptOwnerSourceRelease({ sourcePlanId: job.planId, signal, expectedTrustDigest: job.intent.trustDigest, assertCurrent });
323
+ } } : {}),
324
+ trust: () => this.boundTrust(), prepare: (job, signal, assertCurrent) => this.prepareSourceJob(job, signal, assertCurrent), });
325
+ this.sourceRuntimes.add(runtime);
326
+ const close = async () => {
327
+ if (this.sourceRuntime === runtime)
328
+ this.sourceRuntime = undefined;
329
+ try {
330
+ await runtime.close();
331
+ }
332
+ finally {
333
+ this.sourceRuntimes.delete(runtime);
334
+ }
335
+ };
336
+ try {
337
+ runtime.start();
338
+ this.abort.signal.throwIfAborted();
339
+ this.sourceRuntime = runtime;
340
+ return close;
341
+ }
342
+ catch (error) {
343
+ await close();
344
+ throw error;
345
+ }
346
+ }, 'plugin-control-plane.source-jobs');
347
+ });
43
348
  }
349
+ ownsForegroundTaskObservationRegistration = (registration) => !this.abort.signal.aborted && this.foregroundObservers.has(registration);
350
+ /** Host-only readback. The current trusted task is reread; no caller rating is accepted. */
351
+ inspectOwnerForegroundDeployment = (input) => {
352
+ this.abort.signal.throwIfAborted();
353
+ const delivery = this.ctx.get('assistantDelivery', false);
354
+ if (!delivery)
355
+ throw new Error('foreground deployment Delivery unavailable');
356
+ const source = delivery.inspectOwnerForegroundLearningTask(input);
357
+ if (!source)
358
+ return undefined;
359
+ const record = this.store.getForegroundDeployment(source.source.inboxId);
360
+ if (!record || record.state !== 'observed' || !source.source.quiescent
361
+ || record.task.sessionId !== source.source.sessionId
362
+ || record.task.owner.principalRecordId !== source.owner.principalRecordId
363
+ || record.task.owner.principalVersion !== source.owner.principalVersion
364
+ || record.task.scope.workspace !== source.owner.workspace || record.task.scope.preset !== source.owner.agentPreset)
365
+ return undefined;
366
+ return record;
367
+ };
44
368
  async boundTrust() {
45
369
  const trust = await loadTrustConfig(this.config.trustPath);
46
370
  if (resolve(join(this.config.statePath, 'control.sqlite')) !== trust.ledger.path
@@ -54,9 +378,297 @@ export class PluginControlPlaneService extends Service {
54
378
  const loaded = await loadCatalogWithMetadata(this.config.catalogPath);
55
379
  return discover(loaded.catalog, capability);
56
380
  }
381
+ /** Host only: reread the exact source; no caller text or ratings are admitted. */
382
+ recordOwnerTaskFailureGap = (source) => {
383
+ this.abort.signal.throwIfAborted();
384
+ return this.taskGaps.record(source);
385
+ };
386
+ /** Host-only idempotent approval under a preconfigured finite authority. */
387
+ requestOwnerSourceApproval = async (input) => {
388
+ this.abort.signal.throwIfAborted();
389
+ const config = this.config.sourceApprovals;
390
+ if (!config)
391
+ throw new Error('plugin-control-plane: source approval authority unavailable');
392
+ const signal = AbortSignal.any([this.abort.signal, ...(input.signal ? [input.signal] : [])]);
393
+ const operation = (async () => {
394
+ const plan = this.store.getSourcePlan(input.planId);
395
+ const source = this.store.getOwnerTaskFailureReference(plan.gapId);
396
+ if (!source || plan.mode !== 'modify' || !plan.sourceCheck || !plan.preparedEvidence)
397
+ throw new Error('source approval requires a prepared owner task repair');
398
+ signal.throwIfAborted();
399
+ this.taskGaps.withCurrent(plan.gapId, source.owner, () => { });
400
+ if (plan.status === 'approved')
401
+ return plan;
402
+ if (plan.status !== 'pending-approval')
403
+ throw new Error('source plan is not pending approval');
404
+ const trust = await this.boundTrust();
405
+ if (input.expectedTrustDigest !== undefined && controlPlaneDigest(trust) !== input.expectedTrustDigest)
406
+ throw new Error('source job approval trust changed');
407
+ const receipt = await requestSourceApproval(config, { protocol: 'dsh-source-approval/v1', planId: plan.id,
408
+ planDigest: plan.digest, sourceReferenceDigest: controlPlaneDigest(source) }, signal);
409
+ signal.throwIfAborted();
410
+ if (controlPlaneDigest(await this.boundTrust()) !== controlPlaneDigest(trust))
411
+ throw new Error('source approval trust changed');
412
+ const key = resolveTrustKey(trust, 'approval', receipt.authority, receipt.keyId);
413
+ const result = await this.store.approveSource({ planId: plan.id, expectedRevision: plan.revision, receipt,
414
+ resolveAuthority: () => new Ed25519ApprovalAuthority(key.publicKeyPem, key.authority, key.keyId),
415
+ idempotencyKey: `source-authority:${plan.id}`, withSourceFence: callback => {
416
+ signal.throwIfAborted();
417
+ return this.taskGaps.withCurrent(plan.gapId, source.owner, callback);
418
+ } });
419
+ return result.result;
420
+ })();
421
+ // The child has a bounded timeout and is drained before the store closes.
422
+ this.sourceApprovalFlights.add(operation);
423
+ try {
424
+ return await operation;
425
+ }
426
+ finally {
427
+ this.sourceApprovalFlights.delete(operation);
428
+ }
429
+ };
430
+ /** Host-only: enter the existing release state machine under a separate finite grant. */
431
+ requestOwnerSourceRelease = async (input) => {
432
+ this.abort.signal.throwIfAborted();
433
+ const config = this.config.sourceReleases;
434
+ if (!config)
435
+ throw new Error('plugin-control-plane: source release authority unavailable');
436
+ const signal = AbortSignal.any([this.abort.signal, AbortSignal.timeout(30_000), ...(input.signal ? [input.signal] : [])]);
437
+ const operation = (async () => {
438
+ let plan = this.store.getSourcePlan(input.planId);
439
+ const source = this.store.getOwnerTaskFailureReference(plan.gapId);
440
+ if (!source || plan.mode !== 'modify' || !plan.sourceCheck || !plan.preparedEvidence)
441
+ throw new Error('source release requires a prepared owner task repair');
442
+ const withSourceFence = (callback) => {
443
+ signal.throwIfAborted();
444
+ return this.taskGaps.withCurrent(plan.gapId, source.owner, callback);
445
+ };
446
+ withSourceFence(() => { });
447
+ const trust = await this.boundTrust();
448
+ if (input.expectedTrustDigest !== undefined && controlPlaneDigest(trust) !== input.expectedTrustDigest)
449
+ throw new Error('source job release trust changed');
450
+ signal.throwIfAborted();
451
+ if (trust.schemaVersion !== 4 || !trust.releaseRegistry || !trust.releaseRegistry.locator.startsWith('file:'))
452
+ throw new Error('finite source release requires local schema-v4 release trust');
453
+ // A committed release never requests a new signature, even after restart.
454
+ if (plan.release !== undefined)
455
+ return withSourceFence(() => plan);
456
+ if (plan.status !== 'approved' && plan.status !== 'ready-for-human-review')
457
+ throw new Error('source plan is not approved for release preparation');
458
+ const checked = await verifyPreparedSourceWorktree(plan, inheritedEnvironment(trust), signal);
459
+ if (plan.status === 'approved')
460
+ plan = this.store.verifyPreparedSourcePlan({ planId: plan.id, expectedRevision: plan.revision,
461
+ recheckedTreeDigest: checked.checkedTreeDigest, recheckedPatchDigest: checked.checkedPatchDigest, withSourceFence }).result;
462
+ withSourceFence(() => { });
463
+ const authorization = await requestSourceReleaseAuthorization(config, { protocol: 'dsh-source-release-authorization/v1',
464
+ planId: plan.id, planDigest: plan.digest, sourceReferenceDigest: controlPlaneDigest(source) }, signal);
465
+ signal.throwIfAborted();
466
+ if (controlPlaneDigest(await this.boundTrust()) !== controlPlaneDigest(trust))
467
+ throw new Error('source release trust changed');
468
+ const policy = authorization.releasePolicy;
469
+ if (policy.registryId !== trust.releaseRegistry.id || policy.registryLocator !== trust.releaseRegistry.locator
470
+ || policy.catalogId !== trust.catalog.id || policy.catalogPath !== trust.catalog.path
471
+ || policy.packageName !== `@dsh-enhanced/${plan.name}` || policy.packagePath !== `plugins/${plan.name}`
472
+ || policy.packageVersion !== plan.preparedEvidence.pack.version)
473
+ throw new Error('source release policy differs from configured trust or prepared package');
474
+ const key = resolveTrustKey(trust, 'release-authorization', authorization.authority, authorization.keyId);
475
+ return (await this.store.startSourceRelease({ planId: plan.id, expectedRevision: plan.revision, authorization,
476
+ resolveAuthority: () => new Ed25519SourceReleaseAuthorizationAuthority(key.publicKeyPem, key.authority, key.keyId),
477
+ idempotencyKey: `source-release-authorization:${authorization.authorizationId}`, withSourceFence })).result;
478
+ })();
479
+ this.sourceReleaseFlights.add(operation);
480
+ try {
481
+ return await operation;
482
+ }
483
+ finally {
484
+ this.sourceReleaseFlights.delete(operation);
485
+ }
486
+ };
487
+ /** Host-only continuation, also used by an independent reviewer after its decision is ready. */
488
+ advanceOwnerSourceRelease = async (input) => {
489
+ this.abort.signal.throwIfAborted();
490
+ const config = this.config.sourceReleaseExecution;
491
+ if (!config)
492
+ throw new Error('plugin-control-plane: source release execution unavailable');
493
+ if (this.sourceReleaseAdvances.has(input.planId))
494
+ throw new Error('source release continuation already running');
495
+ const signal = AbortSignal.any([this.abort.signal, AbortSignal.timeout(config.timeoutMs), ...(input.signal ? [input.signal] : [])]);
496
+ const operation = (async () => {
497
+ const plan = this.store.getSourcePlan(input.planId), source = this.store.getOwnerTaskFailureReference(plan.gapId);
498
+ if (!source || plan.mode !== 'modify' || !plan.release)
499
+ throw new Error('source release continuation requires an owner repair release');
500
+ const reviewerAvailable = (operationId) => {
501
+ const reviewer = this.ctx.get('assistantVerifier');
502
+ const snapshot = this.taskGaps.snapshot(plan.gapId, source.owner);
503
+ return reviewer?.canReviewSourceRepair?.({ decisionRoot: config.reviewDecisionRoot, owner: snapshot.owner, name: plan.name,
504
+ ...(operationId === undefined ? {} : { operationId }),
505
+ ...(snapshot.source.modelSelectionState === 'frozen' && snapshot.source.modelSelection
506
+ ? { modelSelection: snapshot.source.modelSelection } : {}) }) === true;
507
+ };
508
+ if (config.independentReview && plan.status === 'awaiting-pr' && !reviewerAvailable() && !input.receipt)
509
+ return plan;
510
+ const withSourceFence = (callback) => { signal.throwIfAborted(); return this.taskGaps.withCurrent(plan.gapId, source.owner, callback); };
511
+ withSourceFence(() => { });
512
+ const trust = await this.boundTrust(), trustDigest = controlPlaneDigest(trust);
513
+ if (input.expectedTrustDigest !== undefined && trustDigest !== input.expectedTrustDigest)
514
+ throw new Error('source release continuation trust changed');
515
+ const assertCurrent = async () => {
516
+ withSourceFence(() => { });
517
+ if (controlPlaneDigest(await this.boundTrust()) !== trustDigest)
518
+ throw new Error('source release continuation trust changed');
519
+ withSourceFence(() => { });
520
+ };
521
+ if (input.receipt) {
522
+ if (input.receipt.planId !== plan.id)
523
+ throw new Error('release reconciliation receipt targets another plan');
524
+ await assertCurrent();
525
+ const { authorize, authority } = sourceReleaseAuthorities(trust);
526
+ await this.store.acceptSourceReleaseReceipt({ operationId: input.receipt.operationId, expectedRevision: plan.revision,
527
+ expectedFence: plan.release.fence, receipt: input.receipt, resolveAuthority: authority,
528
+ resolveAuthorizationAuthority: authorize, withSourceFence });
529
+ }
530
+ return advanceSourceRelease({ store: this.store, planId: plan.id, trust, config, signal, assertCurrent, withSourceFence,
531
+ review: async (request, currentPlan) => {
532
+ const reviewer = this.ctx.get('assistantVerifier');
533
+ if (!reviewer || typeof reviewer.reviewSourceRepair !== 'function' || !reviewerAvailable(request.operationId))
534
+ return;
535
+ const snapshot = this.taskGaps.snapshot(plan.gapId, source.owner);
536
+ await reviewer.reviewSourceRepair({ request: {
537
+ protocol: 'dsh-source-review/v1', operationId: request.operationId,
538
+ planId: currentPlan.id, planDigest: currentPlan.digest, releaseId: request.release.id,
539
+ fence: request.release.fence, revision: request.plan.revision, name: currentPlan.name,
540
+ ...request.input, checkedTreeDigest: request.authorization.checkedTreeDigest,
541
+ checkedPatchDigest: request.authorization.checkedPatchDigest, scope: request.authorization.scope,
542
+ source: { owner: snapshot.owner, outcomeId: source.outcomeId, sourceDigest: source.sourceDigest,
543
+ objective: snapshot.source.objective,
544
+ ...(snapshot.source.modelSelectionState === 'frozen' && snapshot.source.modelSelection
545
+ ? { modelSelection: snapshot.source.modelSelection } : {}) },
546
+ }, signal, withSourceFence });
547
+ } });
548
+ })();
549
+ this.sourceReleaseAdvances.set(input.planId, operation);
550
+ this.sourceReleaseFlights.add(operation);
551
+ try {
552
+ return await operation;
553
+ }
554
+ finally {
555
+ this.sourceReleaseAdvances.delete(input.planId);
556
+ this.sourceReleaseFlights.delete(operation);
557
+ }
558
+ };
559
+ /** Host only. Dedicated connection keeps activation writer locks out of the source job's connection. */
560
+ adoptOwnerSourceRelease = async (input) => {
561
+ const config = this.config.sourceAdoptions;
562
+ if (!config)
563
+ throw new Error('plugin-control-plane: source adoption authority unavailable');
564
+ if (this.sourceAdoptionFlights.size !== 0)
565
+ throw new Error('source adoption worker is already running');
566
+ const signal = AbortSignal.any([this.abort.signal, AbortSignal.timeout(config.timeoutMs), ...(input.signal ? [input.signal] : [])]);
567
+ const operation = (async () => {
568
+ const plan = this.store.getSourcePlan(input.sourcePlanId);
569
+ const source = this.store.getOwnerTaskFailureReference(plan.gapId);
570
+ if (!source || plan.mode !== 'modify' || plan.status !== 'release-complete')
571
+ throw new Error('source adoption requires a completed owner repair release');
572
+ const current = (callback) => {
573
+ signal.throwIfAborted();
574
+ return this.taskGaps.withCurrent(plan.gapId, source.owner, callback);
575
+ };
576
+ const trust = await this.boundTrust(), trustDigest = controlPlaneDigest(trust);
577
+ if (input.expectedTrustDigest !== undefined && trustDigest !== input.expectedTrustDigest)
578
+ throw new Error('source adoption trust changed');
579
+ const store = new ControlPlaneStore({ path: trust.ledger.path,
580
+ withOwnerActivationFence: (gapId, callback) => {
581
+ if (gapId !== plan.gapId)
582
+ throw new Error('source adoption escaped its owner task');
583
+ return current(callback);
584
+ } });
585
+ const withSourceFence = (callback) => current(() => store.withOwnerTaskFailureGapAdmission(plan.gapId, callback));
586
+ try {
587
+ return await adoptSourceRelease({ store, sourcePlanId: plan.id, trust, config, signal, withSourceFence,
588
+ assertCurrent: async () => {
589
+ await input.assertCurrent?.();
590
+ current(() => { });
591
+ if (controlPlaneDigest(await this.boundTrust()) !== trustDigest)
592
+ throw new Error('source adoption trust changed');
593
+ current(() => { });
594
+ } });
595
+ }
596
+ finally {
597
+ store.close();
598
+ }
599
+ })();
600
+ this.sourceAdoptionFlights.set(input.sourcePlanId, operation);
601
+ try {
602
+ return await operation;
603
+ }
604
+ finally {
605
+ this.sourceAdoptionFlights.delete(input.sourcePlanId);
606
+ }
607
+ };
57
608
  recordGap(input) { return this.store.recordGap(input); }
58
609
  gaps(limit) { return this.store.listGaps(limit); }
59
610
  health() { return this.store.health(); }
611
+ canPrepareSource() { return this.config.sourceBuild !== undefined; }
612
+ canEnqueueSource() { return this.sourceRuntime?.available() === true && !this.abort.signal.aborted; }
613
+ // Bind Host entry points: Cordis service proxies must not become resource owners.
614
+ enqueueSourceJob = async (input) => {
615
+ this.abort.signal.throwIfAborted();
616
+ const runtime = this.sourceRuntime;
617
+ if (runtime === undefined || !runtime.available())
618
+ throw new Error('plugin-control-plane: durable source jobs unavailable');
619
+ if (this.sourceBuilds.size !== 0 || this.sourceInspections.size !== 0)
620
+ throw new Error('plugin-control-plane: another source operation is draining');
621
+ const operation = runtime.enqueue(input);
622
+ this.sourceInspections.add(operation);
623
+ try {
624
+ return await operation;
625
+ }
626
+ finally {
627
+ this.sourceInspections.delete(operation);
628
+ }
629
+ };
630
+ inspectSourceJob = (input) => {
631
+ if (this.sourceRuntime === undefined)
632
+ throw new Error('plugin-control-plane: durable source jobs unavailable');
633
+ return this.sourceRuntime.inspect(input);
634
+ };
635
+ reconcileSourceJob = (input) => {
636
+ if (this.sourceRuntime === undefined || this.sourceBuilds.size !== 0)
637
+ throw new Error('plugin-control-plane: durable source jobs unavailable or still draining');
638
+ return this.sourceRuntime.reconcileUnknown(input);
639
+ };
640
+ async prepareSourceJob(job, signal, assertCurrent) {
641
+ this.abort.signal.throwIfAborted();
642
+ if (this.sourceBuilds.size !== 0 || this.sourceInspections.size !== 0)
643
+ throw new Error('plugin-control-plane: another source operation is draining');
644
+ const intent = job.intent;
645
+ const operation = this.prepareModifySourcePlanOwned({ gapId: intent.gapId, name: intent.name, repository: intent.repository, files: intent.files,
646
+ idempotencyKey: `source-job-plan:${job.id}`, expectedBaseCommit: intent.baseCommit, ttlMs: intent.ttlMs, timeoutMs: intent.build.timeoutMs, offline: true, signal, assertCurrent }, job);
647
+ this.sourceBuilds.add(operation);
648
+ try {
649
+ return await operation;
650
+ }
651
+ finally {
652
+ this.sourceBuilds.delete(operation);
653
+ }
654
+ }
655
+ async inspectSource(input) {
656
+ this.abort.signal.throwIfAborted();
657
+ if (this.sourceBuilds.size !== 0 || this.sourceInspections.size !== 0)
658
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'another source operation is still draining');
659
+ const signal = AbortSignal.any([this.abort.signal, ...(input.signal === undefined ? [] : [input.signal]), AbortSignal.timeout(15_000)]);
660
+ const operation = this.inspectSourceOwned({ ...input, signal });
661
+ this.sourceInspections.add(operation);
662
+ void operation.then(() => this.sourceInspections.delete(operation), () => this.sourceInspections.delete(operation));
663
+ return operation;
664
+ }
665
+ async inspectSourceOwned(input) {
666
+ const signal = input.signal === undefined ? this.abort.signal : AbortSignal.any([this.abort.signal, input.signal]);
667
+ const assertCurrent = async () => { signal.throwIfAborted(); await awaitSourceSignal(signal, () => input.assertCurrent?.()); signal.throwIfAborted(); };
668
+ await assertCurrent();
669
+ const trust = await awaitSourceSignal(signal, () => this.boundTrust());
670
+ return inspectSourceContext({ ...input, environment: inheritedEnvironment(trust), signal, assertCurrent });
671
+ }
60
672
  async plan(candidateId, profile, idempotencyKey, gapId) {
61
673
  const gap = this.store.getGap(gapId);
62
674
  const loaded = await loadCatalogWithMetadata(this.config.catalogPath);
@@ -73,6 +685,122 @@ export class PluginControlPlaneService extends Service {
73
685
  executor: { id: trust.executor.id, version: trust.executor.version, path: trust.executor.path, sha256: trust.executor.sha256 }, ttlMs: this.config.proposalTtlMs,
74
686
  gapId, idempotencyKey }).result;
75
687
  }
688
+ /**
689
+ * Prepare a 'modify' source plan for an pre-existing control-plane open capability gap:
690
+ * write the caller-supplied bounded patch into a fresh isolated worktree and
691
+ * persist a pending plan. The build gate is deliberately delegated to the
692
+ * configured owner isolation runner; this Host service must never execute
693
+ * proposal-controlled package scripts in its own process environment.
694
+ *
695
+ * This is a proposal-only entry point. It never approves, signs, releases,
696
+ * activates, reloads or touches a production profile; the repository, base
697
+ * commit, environment, timeouts and TTL are host-controlled and cannot be
698
+ * supplied by the proposal caller. On any worktree/build failure the isolated
699
+ * worktree is removed and no plan row is written and no gap is reserved.
700
+ */
701
+ async prepareModifySourcePlan(input) {
702
+ this.abort.signal.throwIfAborted();
703
+ if (this.sourceBuilds.size !== 0 || this.sourceInspections.size !== 0)
704
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'another source operation is still draining');
705
+ const operation = this.prepareModifySourcePlanOwned(input);
706
+ this.sourceBuilds.add(operation);
707
+ void operation.then(() => this.sourceBuilds.delete(operation), () => this.sourceBuilds.delete(operation));
708
+ return operation;
709
+ }
710
+ async prepareModifySourcePlanOwned(input, sourceJob) {
711
+ const signal = input.signal === undefined ? this.abort.signal : AbortSignal.any([this.abort.signal, input.signal]);
712
+ const gapOwner = sourceJob?.intent.owner ?? input.owner;
713
+ const assertCurrent = async () => {
714
+ signal.throwIfAborted();
715
+ await input.assertCurrent?.();
716
+ signal.throwIfAborted();
717
+ this.taskGaps.withCurrent(input.gapId, gapOwner, () => { });
718
+ };
719
+ await assertCurrent();
720
+ const trust = await this.boundTrust();
721
+ const name = input.name.normalize('NFC').trim();
722
+ if (!/^(?=.{1,64}$)[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(name))
723
+ throw new ControlPlaneCliError('INVALID_ARGUMENT', 'plugin name is invalid');
724
+ assertPluginModificationAllowed(name);
725
+ validateScopedPluginFiles(input.files);
726
+ if (this.config.sourceBuild?.versioning === 'patch')
727
+ assertManagedVersionPaths(input.files);
728
+ // Re-validate the gap reservation immediately before doing the work: the
729
+ // store re-checks under BEGIN IMMEDIATE, but failing early avoids building
730
+ // a patch against a gap that is already matched or closed.
731
+ const gap = this.store.getGap(input.gapId);
732
+ if (gap.status !== 'open' || gap.candidateId !== undefined) {
733
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'only an unreserved pre-existing control-plane open gap can receive a modify proposal');
734
+ }
735
+ const repository = await realpath(resolve(input.repository));
736
+ if (resolve(repository) !== repository)
737
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'repository path must be canonical');
738
+ const ttlMs = input.ttlMs ?? 86_400_000;
739
+ if (!Number.isSafeInteger(ttlMs) || ttlMs < 900_000 || ttlMs > 86_400_000) {
740
+ throw new ControlPlaneCliError('INVALID_ARGUMENT', 'ttlMs must be an integer within 900000..86400000');
741
+ }
742
+ const maximumTimeoutMs = this.config.sourceBuild?.profile === 'repository' ? 1_800_000 : 240_000;
743
+ const timeoutMs = input.timeoutMs ?? 180_000;
744
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 60_000 || timeoutMs > maximumTimeoutMs) {
745
+ throw new ControlPlaneCliError('INVALID_ARGUMENT', `timeoutMs must be an integer within 60000..${maximumTimeoutMs}`);
746
+ }
747
+ const offline = input.offline ?? true;
748
+ const environment = inheritedEnvironment(trust);
749
+ const baseCommit = (await runLocalCommand('git', ['rev-parse', 'HEAD'], repository, environment, { capture: true })).trim();
750
+ await assertCurrent();
751
+ if (!/^[a-f0-9]{40}$/u.test(baseCommit))
752
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'repository HEAD is not a 40-hex commit id');
753
+ if (input.expectedBaseCommit !== undefined && input.expectedBaseCommit !== baseCommit)
754
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'prepared source base commit is stale');
755
+ const stateRoot = join(this.config.statePath, 'source-worktrees');
756
+ const isolated = await createIsolatedWorktree({ stateRoot, repository, baseCommit, environment,
757
+ ...(sourceJob === undefined ? {} : { worktreeName: basename(sourceJob.intent.worktree) }) });
758
+ try {
759
+ await writeScopedPluginFiles({ worktree: isolated.worktree, name, files: input.files });
760
+ await assertCurrent();
761
+ if (this.config.sourceBuild === undefined)
762
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'isolated source build runner is not configured');
763
+ if (!offline)
764
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'isolated source builds must remain offline');
765
+ const configured = this.config.sourceBuild;
766
+ const versionInput = { worktree: isolated.worktree, baseCommit, name, environment, signal, assertCurrent };
767
+ const managed = configured.versioning === 'patch' ? await managedPatchVersionFiles(versionInput) : undefined;
768
+ if (managed !== undefined) {
769
+ await assertCurrent();
770
+ await writeScopedPluginFiles({ worktree: isolated.worktree, name, files: managed.files });
771
+ await verifyManagedPatchVersion(versionInput);
772
+ }
773
+ const checked = await runDockerPreparedChecks({ config: { ...configured, timeoutMs: Math.min(timeoutMs, configured.timeoutMs) },
774
+ worktree: isolated.worktree, baseCommit, name, scope: [`plugins/${name}`], environment, signal,
775
+ assertCurrent, preparedAt: Date.now(), ...(sourceJob === undefined ? {} : { sourceJob: { id: sourceJob.id, containerName: sourceJob.intent.containerName } }) });
776
+ await assertCurrent();
777
+ if (managed !== undefined) {
778
+ await verifyManagedPatchVersion(versionInput);
779
+ if (checked.evidence.pack.version !== managed.version)
780
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'prepared artifact does not carry the Host-managed version');
781
+ }
782
+ // The worktree survives success: the owner recomputes its digests on this
783
+ // exact directory during `source verify-prepared`.
784
+ return this.taskGaps.withCurrent(input.gapId, gapOwner, () => this.store.createSourcePlan({ gapId: input.gapId, repository, worktree: isolated.worktree, baseCommit,
785
+ name, generatorDigest: MODIFY_GENERATOR_DIGEST, scope: [`plugins/${name}`], mode: 'modify', ttlMs,
786
+ idempotencyKey: input.idempotencyKey,
787
+ ...(sourceJob === undefined ? {} : { sourceJob: { jobId: sourceJob.id, jobRevision: sourceJob.revision, occurrenceId: sourceJob.occurrenceId } }),
788
+ prepared: { treeDigest: checked.treeDigest, patchDigest: checked.patchDigest, checkedAt: checked.checkedAt, evidence: checked.evidence } }).result);
789
+ }
790
+ catch (error) {
791
+ await isolated.remove();
792
+ throw error;
793
+ }
794
+ }
795
+ /**
796
+ * Garbage-collect prepared modify worktrees whose plans expired while still
797
+ * pending owner action. The trust binding is re-checked first.
798
+ */
799
+ async gcPreparedSourceWorktrees(now = Date.now()) {
800
+ const trust = await this.boundTrust();
801
+ return gcPreparedModifyWorktrees({ store: this.store,
802
+ statePath: this.config.statePath, environment: inheritedEnvironment(trust), now });
803
+ }
76
804
  }
77
805
  export const Config = schema;
78
806
  //# sourceMappingURL=service.js.map