@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.
- package/README.md +352 -4
- package/bin/dsh-local-release-adapter.js +80 -16
- package/bin/dsh-npm-registry-adapter.js +613 -0
- package/bin/dsh-source-adoption-authority.js +12 -0
- package/bin/dsh-source-approval-authority.js +16 -0
- package/bin/dsh-source-release-authority.js +12 -0
- package/bin/dsh-systemd-host-attestor.js +707 -0
- package/bin/dsh-task-observation-authority.js +15 -0
- package/lib/adapter-process.d.ts +23 -0
- package/lib/adapter-process.d.ts.map +1 -0
- package/lib/adapter-process.js +305 -0
- package/lib/adapter-process.js.map +1 -0
- package/lib/adoption-coordinator.d.ts +63 -0
- package/lib/adoption-coordinator.d.ts.map +1 -0
- package/lib/adoption-coordinator.js +241 -0
- package/lib/adoption-coordinator.js.map +1 -0
- package/lib/adoption-handoff.d.ts +16 -0
- package/lib/adoption-handoff.d.ts.map +1 -0
- package/lib/adoption-handoff.js +11 -0
- package/lib/adoption-handoff.js.map +1 -0
- package/lib/attestation.d.ts.map +1 -1
- package/lib/attestation.js +49 -1
- package/lib/attestation.js.map +1 -1
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +29 -2
- package/lib/catalog.js.map +1 -1
- package/lib/cli.d.ts +33 -9
- package/lib/cli.d.ts.map +1 -1
- package/lib/cli.js +624 -178
- package/lib/cli.js.map +1 -1
- package/lib/deployment-readiness.d.ts +38 -0
- package/lib/deployment-readiness.d.ts.map +1 -0
- package/lib/deployment-readiness.js +154 -0
- package/lib/deployment-readiness.js.map +1 -0
- package/lib/effect-blocked-replay.d.ts +81 -0
- package/lib/effect-blocked-replay.d.ts.map +1 -0
- package/lib/effect-blocked-replay.js +259 -0
- package/lib/effect-blocked-replay.js.map +1 -0
- package/lib/errors.d.ts +6 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +9 -0
- package/lib/errors.js.map +1 -0
- package/lib/foreground-deployment-runtime.d.ts +20 -0
- package/lib/foreground-deployment-runtime.d.ts.map +1 -0
- package/lib/foreground-deployment-runtime.js +79 -0
- package/lib/foreground-deployment-runtime.js.map +1 -0
- package/lib/foreground-deployment.d.ts +17 -0
- package/lib/foreground-deployment.d.ts.map +1 -0
- package/lib/foreground-deployment.js +43 -0
- package/lib/foreground-deployment.js.map +1 -0
- package/lib/host-attestor.d.ts +1 -1
- package/lib/host-attestor.d.ts.map +1 -1
- package/lib/host-attestor.js +51 -57
- package/lib/host-attestor.js.map +1 -1
- package/lib/index.d.ts +25 -2
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +11 -2
- package/lib/index.js.map +1 -1
- package/lib/npm-publish.d.ts +25 -0
- package/lib/npm-publish.d.ts.map +1 -0
- package/lib/npm-publish.js +280 -0
- package/lib/npm-publish.js.map +1 -0
- package/lib/owner-task-gap-types.d.ts +21 -0
- package/lib/owner-task-gap-types.d.ts.map +1 -0
- package/lib/owner-task-gap-types.js +2 -0
- package/lib/owner-task-gap-types.js.map +1 -0
- package/lib/owner-task-gaps.d.ts +24 -0
- package/lib/owner-task-gaps.d.ts.map +1 -0
- package/lib/owner-task-gaps.js +74 -0
- package/lib/owner-task-gaps.js.map +1 -0
- package/lib/post-activation.d.ts +33 -0
- package/lib/post-activation.d.ts.map +1 -0
- package/lib/post-activation.js +219 -0
- package/lib/post-activation.js.map +1 -0
- package/lib/registry-fetch.d.ts +50 -0
- package/lib/registry-fetch.d.ts.map +1 -0
- package/lib/registry-fetch.js +267 -0
- package/lib/registry-fetch.js.map +1 -0
- package/lib/release.d.ts +4 -4
- package/lib/release.d.ts.map +1 -1
- package/lib/release.js +142 -56
- package/lib/release.js.map +1 -1
- package/lib/replay-endpoint-protocol.d.ts +65 -0
- package/lib/replay-endpoint-protocol.d.ts.map +1 -0
- package/lib/replay-endpoint-protocol.js +183 -0
- package/lib/replay-endpoint-protocol.js.map +1 -0
- package/lib/replay-endpoint.d.ts +7 -0
- package/lib/replay-endpoint.d.ts.map +1 -0
- package/lib/replay-endpoint.js +193 -0
- package/lib/replay-endpoint.js.map +1 -0
- package/lib/replay-grant.d.ts +62 -0
- package/lib/replay-grant.d.ts.map +1 -0
- package/lib/replay-grant.js +208 -0
- package/lib/replay-grant.js.map +1 -0
- package/lib/replay-journal.d.ts +25 -0
- package/lib/replay-journal.d.ts.map +1 -0
- package/lib/replay-journal.js +275 -0
- package/lib/replay-journal.js.map +1 -0
- package/lib/runtime-observer-protocol.d.ts +63 -0
- package/lib/runtime-observer-protocol.d.ts.map +1 -0
- package/lib/runtime-observer-protocol.js +280 -0
- package/lib/runtime-observer-protocol.js.map +1 -0
- package/lib/runtime-observer.d.ts +8 -0
- package/lib/runtime-observer.d.ts.map +1 -0
- package/lib/runtime-observer.js +182 -0
- package/lib/runtime-observer.js.map +1 -0
- package/lib/service.d.ts +145 -1
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +734 -6
- package/lib/service.js.map +1 -1
- package/lib/source-adoption-authority.d.ts +65 -0
- package/lib/source-adoption-authority.d.ts.map +1 -0
- package/lib/source-adoption-authority.js +271 -0
- package/lib/source-adoption-authority.js.map +1 -0
- package/lib/source-adoption-runner.d.ts +28 -0
- package/lib/source-adoption-runner.d.ts.map +1 -0
- package/lib/source-adoption-runner.js +197 -0
- package/lib/source-adoption-runner.js.map +1 -0
- package/lib/source-approval-authority.d.ts +50 -0
- package/lib/source-approval-authority.d.ts.map +1 -0
- package/lib/source-approval-authority.js +412 -0
- package/lib/source-approval-authority.js.map +1 -0
- package/lib/source-approval-client.d.ts +32 -0
- package/lib/source-approval-client.d.ts.map +1 -0
- package/lib/source-approval-client.js +175 -0
- package/lib/source-approval-client.js.map +1 -0
- package/lib/source-build.d.ts +60 -0
- package/lib/source-build.d.ts.map +1 -0
- package/lib/source-build.js +311 -0
- package/lib/source-build.js.map +1 -0
- package/lib/source-context.d.ts +24 -0
- package/lib/source-context.d.ts.map +1 -0
- package/lib/source-context.js +99 -0
- package/lib/source-context.js.map +1 -0
- package/lib/source-job-types.d.ts +87 -0
- package/lib/source-job-types.d.ts.map +1 -0
- package/lib/source-job-types.js +2 -0
- package/lib/source-job-types.js.map +1 -0
- package/lib/source-jobs.d.ts +113 -0
- package/lib/source-jobs.d.ts.map +1 -0
- package/lib/source-jobs.js +535 -0
- package/lib/source-jobs.js.map +1 -0
- package/lib/source-release-authority.d.ts +35 -0
- package/lib/source-release-authority.d.ts.map +1 -0
- package/lib/source-release-authority.js +271 -0
- package/lib/source-release-authority.js.map +1 -0
- package/lib/source-release-client.d.ts +10 -0
- package/lib/source-release-client.d.ts.map +1 -0
- package/lib/source-release-client.js +24 -0
- package/lib/source-release-client.js.map +1 -0
- package/lib/source-release-runner.d.ts +31 -0
- package/lib/source-release-runner.d.ts.map +1 -0
- package/lib/source-release-runner.js +125 -0
- package/lib/source-release-runner.js.map +1 -0
- package/lib/source-versioning.d.ts +24 -0
- package/lib/source-versioning.d.ts.map +1 -0
- package/lib/source-versioning.js +213 -0
- package/lib/source-versioning.js.map +1 -0
- package/lib/source-workspace.d.ts +117 -0
- package/lib/source-workspace.d.ts.map +1 -0
- package/lib/source-workspace.js +458 -0
- package/lib/source-workspace.js.map +1 -0
- package/lib/sqlite.d.ts +1 -1
- package/lib/sqlite.d.ts.map +1 -1
- package/lib/sqlite.js +509 -7
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +229 -6
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +2431 -383
- package/lib/store.js.map +1 -1
- package/lib/task-observation-authority.d.ts +29 -0
- package/lib/task-observation-authority.d.ts.map +1 -0
- package/lib/task-observation-authority.js +370 -0
- package/lib/task-observation-authority.js.map +1 -0
- package/lib/task-observation-client.d.ts +4 -0
- package/lib/task-observation-client.d.ts.map +1 -0
- package/lib/task-observation-client.js +20 -0
- package/lib/task-observation-client.js.map +1 -0
- package/lib/task-observation-runtime.d.ts +59 -0
- package/lib/task-observation-runtime.d.ts.map +1 -0
- package/lib/task-observation-runtime.js +331 -0
- package/lib/task-observation-runtime.js.map +1 -0
- package/lib/task-observation-store.d.ts +17 -0
- package/lib/task-observation-store.d.ts.map +1 -0
- package/lib/task-observation-store.js +115 -0
- package/lib/task-observation-store.js.map +1 -0
- package/lib/task-observation-types.d.ts +67 -0
- package/lib/task-observation-types.d.ts.map +1 -0
- package/lib/task-observation-types.js +2 -0
- package/lib/task-observation-types.js.map +1 -0
- package/lib/trust.d.ts +3 -0
- package/lib/trust.d.ts.map +1 -1
- package/lib/trust.js +0 -0
- package/lib/trust.js.map +1 -1
- package/lib/types.d.ts +275 -8
- package/lib/types.d.ts.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- 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 {
|
|
7
|
-
import {
|
|
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 =
|
|
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
|
-
|
|
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
|