@gestaltrun/dsh-workspace-bundle 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.i18n.yaml +37 -0
- package/README.md +92 -2
- package/README.zh.md +93 -0
- package/lib/index.js +2868 -0
- package/lib/types/agent-config-contract/agent-configs.d.ts +621 -0
- package/lib/types/agent-config-contract/agent-configs.js +68 -0
- package/lib/types/agent-config-contract/cs-hosting.d.ts +1333 -0
- package/lib/types/agent-config-contract/cs-hosting.js +163 -0
- package/lib/types/agent-config-contract/errors.d.ts +224 -0
- package/lib/types/agent-config-contract/errors.js +202 -0
- package/lib/types/agent-config-contract/ids.d.ts +65 -0
- package/lib/types/agent-config-contract/ids.js +35 -0
- package/lib/types/agent-config-contract/route.d.ts +56 -0
- package/lib/types/agent-config-contract/route.js +1 -0
- package/lib/types/agent-config-contract/test-account-claim.d.ts +24 -0
- package/lib/types/agent-config-contract/test-account-claim.js +5 -0
- package/lib/types/agent-config-format.d.ts +86 -0
- package/lib/types/agent-config-format.js +119 -0
- package/lib/types/agent-config-observation.d.ts +64 -0
- package/lib/types/agent-config-observation.js +448 -0
- package/lib/types/agent-config-source.d.ts +78 -0
- package/lib/types/agent-config-source.js +411 -0
- package/lib/types/agent-config-types.d.ts +67 -0
- package/lib/types/agent-config-types.js +1 -0
- package/lib/types/bundled-agent-config.d.ts +29 -0
- package/lib/types/bundled-agent-config.js +47 -0
- package/lib/types/config.d.ts +34 -0
- package/lib/types/config.js +53 -0
- package/lib/types/definition.d.ts +82 -0
- package/lib/types/definition.js +20 -0
- package/lib/types/digest.d.ts +38 -0
- package/lib/types/digest.js +243 -0
- package/lib/types/errors.d.ts +22 -0
- package/lib/types/errors.js +26 -0
- package/lib/types/filesystem.d.ts +16 -0
- package/lib/types/filesystem.js +64 -0
- package/lib/types/index.d.ts +28 -0
- package/lib/types/index.js +19 -0
- package/lib/types/journal.d.ts +51 -0
- package/lib/types/journal.js +109 -0
- package/lib/types/local-host-identity.d.ts +65 -0
- package/lib/types/local-host-identity.js +87 -0
- package/lib/types/local-host-replacement.d.ts +49 -0
- package/lib/types/local-host-replacement.js +112 -0
- package/lib/types/paths.d.ts +26 -0
- package/lib/types/paths.js +48 -0
- package/lib/types/provider.d.ts +83 -0
- package/lib/types/provider.js +560 -0
- package/lib/types/session-events.d.ts +23 -0
- package/lib/types/session-events.js +14 -0
- package/lib/types/types.d.ts +126 -0
- package/lib/types/types.js +1 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +69 -3
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { brandString } from '@deepseek-ai/dsh-brand';
|
|
2
|
+
import { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { mkdir, readFile, readdir } from 'node:fs/promises';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { HOST_AGENT_CONFIG_API_VERSION } from "./agent-config-format.js";
|
|
8
|
+
import { LocalHostReplacementWriteError } from "./local-host-replacement.js";
|
|
9
|
+
import { ModuleAgentConfigSnapshot, RegisterAgentConfigHostRequest, ReportAgentConfigHostRequest, registerAgentConfigHost, reportAgentConfigHost, getAgentConfigSandboxHost } from "./agent-config-contract/agent-configs.js";
|
|
10
|
+
const sequence = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
|
|
11
|
+
const ScopeState = z.strictObject({ generation: sequence, replaced: z.boolean().optional(),
|
|
12
|
+
running: z.strictObject({ bootId: z.uuid(), bootSequence: sequence, sequence }).nullable(),
|
|
13
|
+
pendingRegistration: RegisterAgentConfigHostRequest.nullable(), pendingReport: ReportAgentConfigHostRequest.nullable(),
|
|
14
|
+
});
|
|
15
|
+
const State = z.strictObject({ version: z.literal(1), hostId: z.uuid(), bootSequence: sequence, scopes: z.record(z.string().regex(/^[a-f0-9]{64}$/u), ScopeState) });
|
|
16
|
+
function sameSandbox(left, right) {
|
|
17
|
+
return left?.sandboxId === right.sandboxId && left.instanceId === right.instanceId && left.sandboxGeneration === right.sandboxGeneration;
|
|
18
|
+
}
|
|
19
|
+
class ObservationError extends Error {
|
|
20
|
+
code;
|
|
21
|
+
constructor(code) {
|
|
22
|
+
super(code);
|
|
23
|
+
this.code = code;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build the observation owner when its source is registered; the server owns holder attestation.
|
|
28
|
+
* @param ctx - authenticated merchant transport and scope-change events.
|
|
29
|
+
* @param options - module identity, private persistent directory and resolved scheduling limits.
|
|
30
|
+
* @returns factory bound to the source's actual committed state.
|
|
31
|
+
*/
|
|
32
|
+
export function agentConfigHostObservation(ctx, options) {
|
|
33
|
+
return read => new ModuleHostObservation(ctx, options, read);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Cloud-only factory; importing this export rejects older packages that cannot enforce sandbox identity.
|
|
37
|
+
* @param ctx - Authenticated merchant transport and scope-change events.
|
|
38
|
+
* @param options - Required sandbox deployment identity and private reporter settings.
|
|
39
|
+
* @returns factory whose registrations require authenticated sandbox readback.
|
|
40
|
+
*/
|
|
41
|
+
export function sandboxAgentConfigHostObservation(ctx, options) {
|
|
42
|
+
return read => new ModuleHostObservation(ctx, options, read);
|
|
43
|
+
}
|
|
44
|
+
class ModuleHostObservation {
|
|
45
|
+
ctx;
|
|
46
|
+
options;
|
|
47
|
+
read;
|
|
48
|
+
lifetime = new AbortController();
|
|
49
|
+
active;
|
|
50
|
+
run;
|
|
51
|
+
stopped;
|
|
52
|
+
chain = Promise.resolve();
|
|
53
|
+
timer;
|
|
54
|
+
stopScope;
|
|
55
|
+
stopActivation;
|
|
56
|
+
stopActivated;
|
|
57
|
+
preparedRevision;
|
|
58
|
+
preparedHostId;
|
|
59
|
+
activationRequest = false;
|
|
60
|
+
stopReplacement;
|
|
61
|
+
migrated = new Set();
|
|
62
|
+
member() { return this.ctx.merchantAccount.captureIdentity()?.identity; }
|
|
63
|
+
get replaced() { if (this.options.sandbox !== undefined)
|
|
64
|
+
return false; const member = this.member(); return member !== undefined && this.options.replacementState.isReplaced(member); }
|
|
65
|
+
stopIdentity;
|
|
66
|
+
hostId() { return typeof this.options.hostId === 'function' ? this.options.hostId() : this.options.hostId; }
|
|
67
|
+
constructor(ctx, options, read) {
|
|
68
|
+
this.ctx = ctx;
|
|
69
|
+
this.options = options;
|
|
70
|
+
this.read = read;
|
|
71
|
+
this.stopReplacement = options.sandbox === undefined ? options.replacementState.subscribe(() => {
|
|
72
|
+
if (!this.replaced)
|
|
73
|
+
return;
|
|
74
|
+
if (!this.activationRequest)
|
|
75
|
+
this.active?.abort();
|
|
76
|
+
this.options.onReplaced?.(this.hostId());
|
|
77
|
+
void this.refresh();
|
|
78
|
+
}) : () => { };
|
|
79
|
+
this.stopScope = ctx.on('merchant-account/scope-changed', () => { this.active?.abort(); void this.refresh(); });
|
|
80
|
+
this.stopIdentity = options.sandbox === undefined ? ctx.on('workspace-bundle/local-host-identity-changed', () => { this.active?.abort(); this.stopped = undefined; return this.refresh(); }) : () => { };
|
|
81
|
+
this.stopActivation = options.sandbox === undefined ? ctx.on('workspace-bundle/activate-local-host', async (revision, registered) => { await this.activate(revision); registered.add(this.options.workspaceKind); }) : () => { };
|
|
82
|
+
this.stopActivated = options.sandbox === undefined ? ctx.on('workspace-bundle/local-host-activated', revision => {
|
|
83
|
+
if (this.preparedRevision !== revision || this.preparedHostId !== this.hostId())
|
|
84
|
+
return Promise.reject(new ObservationError('AGENT_CONFIG_HOST_UNAVAILABLE'));
|
|
85
|
+
const work = this.chain.then(() => this.report(revision, false, true));
|
|
86
|
+
this.chain = work.catch((error) => { void error; });
|
|
87
|
+
return work;
|
|
88
|
+
}) : () => { };
|
|
89
|
+
void this.cycle();
|
|
90
|
+
}
|
|
91
|
+
async cycle() {
|
|
92
|
+
await this.refresh();
|
|
93
|
+
if (this.lifetime.signal.aborted)
|
|
94
|
+
return;
|
|
95
|
+
this.timer = setTimeout(() => { void this.cycle(); }, this.options.intervalMs);
|
|
96
|
+
this.timer.unref();
|
|
97
|
+
}
|
|
98
|
+
refresh() {
|
|
99
|
+
if (this.lifetime.signal.aborted)
|
|
100
|
+
return this.chain;
|
|
101
|
+
const revision = this.ctx.merchantAccount.getScopeRevision();
|
|
102
|
+
this.chain = this.chain.then(() => this.report(revision)).catch((error) => {
|
|
103
|
+
if (this.lifetime.signal.aborted || revision !== this.ctx.merchantAccount.getScopeRevision())
|
|
104
|
+
return;
|
|
105
|
+
// The identity-changed listener cancelled this run; its failure must not stop the run for the new identity.
|
|
106
|
+
if (this.run !== undefined && this.run.hostId !== this.hostId())
|
|
107
|
+
return;
|
|
108
|
+
const code = error instanceof LocalHostReplacementWriteError ? 'LOCAL_HOST_REPLACEMENT_WRITE_FAILED' : error instanceof ObservationError ? error.code : 'AGENT_CONFIG_HOST_STATE_INVALID';
|
|
109
|
+
if (code === 'LOCAL_HOST_REPLACEMENT_WRITE_FAILED') {
|
|
110
|
+
this.ctx.logger.warn('workspace-bundle: %s replacement state could not be saved; reporting remains stopped; check disk space and directory permissions before retrying', this.options.workspaceKind);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (code !== 'LOCAL_HOST_REPLACED' && code !== 'AGENT_CONFIG_HOST_UNAVAILABLE' && this.run !== undefined)
|
|
114
|
+
this.stopped = { key: this.run.key, revision };
|
|
115
|
+
this.ctx.logger.warn('workspace-bundle: %s module observation failed (%s); %s', this.options.workspaceKind, code, code === 'AGENT_CONFIG_HOST_UNAVAILABLE' ? 'the exact pending request is retained for retry' : 'reporting is stopped; restore matching Host state before restarting');
|
|
116
|
+
});
|
|
117
|
+
return this.chain;
|
|
118
|
+
}
|
|
119
|
+
activate(revision) {
|
|
120
|
+
if (this.lifetime.signal.aborted || revision !== this.ctx.merchantAccount.getScopeRevision())
|
|
121
|
+
return Promise.reject(new ObservationError('AGENT_CONFIG_HOST_UNAVAILABLE'));
|
|
122
|
+
const work = this.chain.then(() => this.report(revision, true));
|
|
123
|
+
this.chain = work.catch((error) => { void error; });
|
|
124
|
+
return work;
|
|
125
|
+
}
|
|
126
|
+
scope(revision) {
|
|
127
|
+
const state = this.ctx.merchantAccount.getState();
|
|
128
|
+
const identity = state.status === 'signed-in' ? state.identity : null;
|
|
129
|
+
const tenantId = identity?.currentTenantId;
|
|
130
|
+
if (identity === null || tenantId == null || !identity.memberships.some(member => member.tenantId === tenantId && member.kind === 'shop'))
|
|
131
|
+
return undefined;
|
|
132
|
+
if (revision !== this.ctx.merchantAccount.getScopeRevision())
|
|
133
|
+
return undefined;
|
|
134
|
+
const userId = identity.user.id;
|
|
135
|
+
const key = createHash('sha256').update(JSON.stringify(this.options.sandbox === undefined ? [tenantId, userId, this.options.workspaceKind]
|
|
136
|
+
: [this.member()?.serverOrigin, tenantId, userId, this.options.workspaceKind, this.options.sandbox.sandboxId, this.options.sandbox.instanceId])).digest('hex');
|
|
137
|
+
if (this.stopped?.key === key && this.stopped.revision === revision)
|
|
138
|
+
return undefined;
|
|
139
|
+
if (this.run?.key !== key || this.run.revision !== revision || this.run.hostId !== this.hostId()) {
|
|
140
|
+
this.run = { key, tenantId, userId, revision, hostId: this.hostId(), bootId: brandString(randomUUID()), started: false };
|
|
141
|
+
}
|
|
142
|
+
return this.run;
|
|
143
|
+
}
|
|
144
|
+
current(run, signal) {
|
|
145
|
+
signal.throwIfAborted();
|
|
146
|
+
const state = this.ctx.merchantAccount.getState();
|
|
147
|
+
if (run.hostId !== this.hostId() || run.revision !== this.ctx.merchantAccount.getScopeRevision() || state.status !== 'signed-in'
|
|
148
|
+
|| state.identity?.currentTenantId !== run.tenantId || state.identity.user.id !== run.userId)
|
|
149
|
+
throw new ObservationError('AGENT_CONFIG_HOST_UNAVAILABLE');
|
|
150
|
+
}
|
|
151
|
+
async load(hostId) {
|
|
152
|
+
let text;
|
|
153
|
+
try {
|
|
154
|
+
text = await readFile(join(this.options.stateRoot, `agent-config-${this.options.workspaceKind}-${hostId}.json`), 'utf8');
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
if (error.code !== 'ENOENT')
|
|
158
|
+
throw new ObservationError('AGENT_CONFIG_HOST_STATE_INVALID');
|
|
159
|
+
try {
|
|
160
|
+
text = await readFile(join(this.options.stateRoot, `agent-config-${this.options.workspaceKind}-host.json`), 'utf8');
|
|
161
|
+
}
|
|
162
|
+
catch (legacyError) {
|
|
163
|
+
if (legacyError.code !== 'ENOENT')
|
|
164
|
+
throw new ObservationError('AGENT_CONFIG_HOST_STATE_INVALID');
|
|
165
|
+
return { version: 1, hostId: hostId, bootSequence: 0, scopes: {} };
|
|
166
|
+
}
|
|
167
|
+
const legacy = State.parse(JSON.parse(text));
|
|
168
|
+
if (legacy.hostId !== hostId)
|
|
169
|
+
return { version: 1, hostId: hostId, bootSequence: 0, scopes: {} };
|
|
170
|
+
}
|
|
171
|
+
const parsed = State.safeParse(JSON.parse(text));
|
|
172
|
+
if (!parsed.success || parsed.data.hostId !== hostId)
|
|
173
|
+
throw new ObservationError('AGENT_CONFIG_HOST_STATE_INVALID');
|
|
174
|
+
return parsed.data;
|
|
175
|
+
}
|
|
176
|
+
async migrate(run, member, explicitActivation) {
|
|
177
|
+
if (!explicitActivation && this.migrated.has(run.key))
|
|
178
|
+
return;
|
|
179
|
+
await mkdir(this.options.stateRoot, { recursive: true, mode: 0o700 });
|
|
180
|
+
const paths = (await readdir(this.options.stateRoot)).filter(path => path.startsWith(`agent-config-${this.options.workspaceKind}-`) && path.endsWith('.json'));
|
|
181
|
+
const legacy = await Promise.all(paths.map(async (path) => ({ path, state: State.parse(JSON.parse(await readFile(join(this.options.stateRoot, path), 'utf8'))) })));
|
|
182
|
+
await this.options.replacementState.importLegacy(member, this.options.workspaceKind, legacy.some(({ state }) => state.scopes[run.key]?.replaced === true));
|
|
183
|
+
for (const { path, state } of legacy) {
|
|
184
|
+
const record = state.scopes[run.key];
|
|
185
|
+
if (record?.replaced === undefined)
|
|
186
|
+
continue;
|
|
187
|
+
delete record.replaced;
|
|
188
|
+
await writeFileAtomic(join(this.options.stateRoot, path), JSON.stringify(state) + '\n', { mode: 0o600, dirMode: 0o700 });
|
|
189
|
+
}
|
|
190
|
+
this.migrated.add(run.key);
|
|
191
|
+
}
|
|
192
|
+
async save(state) {
|
|
193
|
+
await writeFileAtomic(join(this.options.stateRoot, `agent-config-${this.options.workspaceKind}-${state.hostId}.json`), JSON.stringify(state) + '\n', { mode: 0o600, dirMode: 0o700 });
|
|
194
|
+
}
|
|
195
|
+
async rememberReplacement(state, record, member) {
|
|
196
|
+
const saveModule = record.replaced !== true;
|
|
197
|
+
record.replaced = true;
|
|
198
|
+
const writes = await Promise.allSettled([
|
|
199
|
+
saveModule ? this.save(state) : Promise.resolve(),
|
|
200
|
+
this.options.replacementState.markReplaced(member),
|
|
201
|
+
]);
|
|
202
|
+
for (const write of writes)
|
|
203
|
+
if (write.status === 'rejected')
|
|
204
|
+
throw new LocalHostReplacementWriteError(write.reason);
|
|
205
|
+
}
|
|
206
|
+
failure(failure) {
|
|
207
|
+
if (failure.kind === 'server' && failure.status === 409 && failure.code === 'LOCAL_HOST_REPLACED')
|
|
208
|
+
return new ObservationError(this.options.sandbox === undefined ? 'LOCAL_HOST_REPLACED' : 'AGENT_CONFIG_HOST_PROTOCOL');
|
|
209
|
+
if (failure.kind === 'server' && failure.status === 409 && failure.code === 'VERSION_CONFLICT')
|
|
210
|
+
return new ObservationError('AGENT_CONFIG_HOST_GENERATION_CONFLICT');
|
|
211
|
+
if (failure.kind === 'contract')
|
|
212
|
+
return new ObservationError('AGENT_CONFIG_HOST_PROTOCOL');
|
|
213
|
+
// An unbound sandbox session is retried: the cloud Host binds its placement instance after the reporter starts.
|
|
214
|
+
return new ObservationError(failure.kind === 'server' && (failure.status === 409 || failure.status === 404) ? 'AGENT_CONFIG_HOST_CONFLICT' : 'AGENT_CONFIG_HOST_UNAVAILABLE');
|
|
215
|
+
}
|
|
216
|
+
async attestSandbox(state, record, run, signal) {
|
|
217
|
+
const expected = this.options.sandbox;
|
|
218
|
+
if (expected === undefined)
|
|
219
|
+
return;
|
|
220
|
+
this.current(run, signal);
|
|
221
|
+
const result = await this.ctx.merchantAccount.request(getAgentConfigSandboxHost, {
|
|
222
|
+
params: { tenantId: run.tenantId, workspaceKind: this.options.workspaceKind },
|
|
223
|
+
query: { sandboxId: expected.sandboxId, instanceId: expected.instanceId }, tenantContextReload: 'caller',
|
|
224
|
+
}, signal).catch(() => { throw new ObservationError('AGENT_CONFIG_HOST_UNAVAILABLE'); });
|
|
225
|
+
this.current(run, signal);
|
|
226
|
+
if (!result.ok)
|
|
227
|
+
throw this.failure(result.failure);
|
|
228
|
+
const value = result.value;
|
|
229
|
+
if (String(value.tenantId) !== run.tenantId || String(value.userId) !== run.userId || value.workspaceKind !== this.options.workspaceKind
|
|
230
|
+
|| value.sandboxBinding.sandboxId !== expected.sandboxId || value.sandboxBinding.instanceId !== expected.instanceId)
|
|
231
|
+
throw new ObservationError('AGENT_CONFIG_HOST_PROTOCOL');
|
|
232
|
+
if (run.started && !sameSandbox(run.sandboxBinding, value.sandboxBinding))
|
|
233
|
+
throw new ObservationError('AGENT_CONFIG_HOST_CONFLICT');
|
|
234
|
+
run.sandboxBinding = value.sandboxBinding;
|
|
235
|
+
if (record.pendingRegistration !== null && !sameSandbox(record.pendingRegistration.sandboxBinding, value.sandboxBinding)) {
|
|
236
|
+
record.pendingRegistration = null;
|
|
237
|
+
record.pendingReport = null;
|
|
238
|
+
record.running = null;
|
|
239
|
+
}
|
|
240
|
+
if (run.started)
|
|
241
|
+
return;
|
|
242
|
+
const registration = value.registration;
|
|
243
|
+
if (record.pendingReport !== null && (registration?.hostId !== run.hostId || registration.generation !== record.pendingReport.generation
|
|
244
|
+
|| !sameSandbox(registration.sandboxBinding, value.sandboxBinding)))
|
|
245
|
+
record.pendingReport = null;
|
|
246
|
+
if (record.pendingRegistration === null)
|
|
247
|
+
record.generation = registration?.generation ?? 0;
|
|
248
|
+
state.bootSequence = Math.max(state.bootSequence, registration?.bootSequence ?? 0);
|
|
249
|
+
await this.save(state);
|
|
250
|
+
}
|
|
251
|
+
async registration(state, record, run, signal) {
|
|
252
|
+
const body = record.pendingRegistration;
|
|
253
|
+
if (body === null)
|
|
254
|
+
return;
|
|
255
|
+
if (this.options.sandbox !== undefined && (run.sandboxBinding === undefined || !sameSandbox(body.sandboxBinding, run.sandboxBinding)))
|
|
256
|
+
throw new ObservationError('AGENT_CONFIG_HOST_PROTOCOL');
|
|
257
|
+
this.current(run, signal);
|
|
258
|
+
const response = await this.ctx.merchantAccount.request(registerAgentConfigHost, { params: { tenantId: run.tenantId, workspaceKind: this.options.workspaceKind }, body, tenantContextReload: 'caller' }, signal)
|
|
259
|
+
.catch(() => { throw new ObservationError('AGENT_CONFIG_HOST_UNAVAILABLE'); });
|
|
260
|
+
this.current(run, signal);
|
|
261
|
+
if (!response.ok)
|
|
262
|
+
throw this.failure(response.failure);
|
|
263
|
+
const receipt = response.value;
|
|
264
|
+
if (receipt.workspaceKind !== this.options.workspaceKind || receipt.hostId !== run.hostId
|
|
265
|
+
|| receipt.generation !== body.expectedGeneration + 1 || receipt.holder.kind === 'installation' && receipt.holder.id !== run.hostId
|
|
266
|
+
|| this.options.sandbox !== undefined && (receipt.holder.kind !== 'sandbox' || receipt.holder.id !== this.options.sandbox.sandboxId))
|
|
267
|
+
throw new ObservationError('AGENT_CONFIG_HOST_PROTOCOL');
|
|
268
|
+
run.holder = receipt.holder.kind;
|
|
269
|
+
record.generation = receipt.generation;
|
|
270
|
+
record.running = { bootId: body.bootId, bootSequence: body.bootSequence, sequence: 0 };
|
|
271
|
+
record.pendingRegistration = null;
|
|
272
|
+
await this.save(state);
|
|
273
|
+
}
|
|
274
|
+
async observation(state, record, run, signal) {
|
|
275
|
+
const body = record.pendingReport;
|
|
276
|
+
if (body === null)
|
|
277
|
+
return;
|
|
278
|
+
this.current(run, signal);
|
|
279
|
+
const response = await this.ctx.merchantAccount.request(reportAgentConfigHost, {
|
|
280
|
+
params: { tenantId: run.tenantId, workspaceKind: this.options.workspaceKind, hostId: run.hostId }, body, tenantContextReload: 'caller',
|
|
281
|
+
}, signal).catch(() => { throw new ObservationError('AGENT_CONFIG_HOST_UNAVAILABLE'); });
|
|
282
|
+
this.current(run, signal);
|
|
283
|
+
if (!response.ok)
|
|
284
|
+
throw this.failure(response.failure);
|
|
285
|
+
const receipt = response.value;
|
|
286
|
+
if (receipt.generation !== body.generation || receipt.sequence !== body.sequence || record.running === null)
|
|
287
|
+
throw new ObservationError('AGENT_CONFIG_HOST_PROTOCOL');
|
|
288
|
+
record.running.sequence = receipt.sequence;
|
|
289
|
+
record.pendingReport = null;
|
|
290
|
+
await this.save(state);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Pending reports from an earlier run can be discarded on a generation conflict.
|
|
294
|
+
* Reporters without an explicit sandbox identity can also rebind a server-attested sandbox run.
|
|
295
|
+
* An explicitly bound live sandbox run stops on conflict.
|
|
296
|
+
* @returns whether the report was discarded and the run needs a new registration.
|
|
297
|
+
*/
|
|
298
|
+
async replay(state, record, run, signal) {
|
|
299
|
+
try {
|
|
300
|
+
await this.observation(state, record, run, signal);
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
if (!(error instanceof ObservationError) || error.code !== 'AGENT_CONFIG_HOST_GENERATION_CONFLICT'
|
|
304
|
+
|| run.started && (this.options.sandbox !== undefined || run.holder !== 'sandbox'))
|
|
305
|
+
throw error;
|
|
306
|
+
record.pendingReport = null;
|
|
307
|
+
if (run.started) {
|
|
308
|
+
run.bootId = brandString(randomUUID());
|
|
309
|
+
run.started = false;
|
|
310
|
+
}
|
|
311
|
+
await this.save(state);
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
snapshot() {
|
|
317
|
+
const state = this.read();
|
|
318
|
+
const rejected = state.lastRejection;
|
|
319
|
+
const current = state.current.sha256 === null ? { source: 'builtin', version: state.current.version, sha256: null }
|
|
320
|
+
: { source: 'registry', version: state.current.version, sha256: state.current.sha256 };
|
|
321
|
+
return ModuleAgentConfigSnapshot.parse({ hostAgentConfigApiVersion: HOST_AGENT_CONFIG_API_VERSION, runtimeApiVersion: this.options.runtimeApiVersion,
|
|
322
|
+
current, ...(this.options.sandbox === undefined ? {} : { blockingFailure: state.blockingFailure === null ? null : { code: state.blockingFailure.code, committed: current } }),
|
|
323
|
+
pending: state.pending === null ? null : { version: state.pending.version, sha256: state.pending.sha256 }, blocked: state.blocked,
|
|
324
|
+
lastRejection: rejected === null ? null : { code: rejected.code,
|
|
325
|
+
target: rejected.version === undefined || rejected.sha256 === undefined ? null : { version: rejected.version, sha256: rejected.sha256 } },
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
async report(revision, explicitActivation = false, activated = false) {
|
|
329
|
+
if (this.lifetime.signal.aborted)
|
|
330
|
+
return;
|
|
331
|
+
if (explicitActivation)
|
|
332
|
+
this.stopped = undefined;
|
|
333
|
+
const run = this.scope(revision);
|
|
334
|
+
if (run === undefined) {
|
|
335
|
+
if (explicitActivation)
|
|
336
|
+
throw new ObservationError('AGENT_CONFIG_HOST_UNAVAILABLE');
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const member = this.member();
|
|
340
|
+
if (member === undefined)
|
|
341
|
+
return;
|
|
342
|
+
if (this.options.sandbox === undefined)
|
|
343
|
+
await this.migrate(run, member, explicitActivation);
|
|
344
|
+
if (explicitActivation)
|
|
345
|
+
await this.options.replacementState.markReplaced(member);
|
|
346
|
+
else if (activated && this.preparedHostId !== run.hostId)
|
|
347
|
+
throw new ObservationError('AGENT_CONFIG_HOST_UNAVAILABLE');
|
|
348
|
+
this.activationRequest = explicitActivation || activated;
|
|
349
|
+
const active = this.active = new AbortController();
|
|
350
|
+
const signal = AbortSignal.any([active.signal, this.lifetime.signal, AbortSignal.timeout(this.options.requestTimeoutMs)]);
|
|
351
|
+
try {
|
|
352
|
+
await mkdir(this.options.stateRoot, { recursive: true, mode: 0o700 });
|
|
353
|
+
await withFileLock(join(this.options.stateRoot, `agent-config-${this.options.workspaceKind}-${run.hostId}.json`), async () => {
|
|
354
|
+
this.current(run, signal);
|
|
355
|
+
const state = await this.load(run.hostId);
|
|
356
|
+
this.current(run, signal);
|
|
357
|
+
const record = state.scopes[run.key] ??= { generation: 0, running: null, pendingRegistration: null, pendingReport: null };
|
|
358
|
+
await this.attestSandbox(state, record, run, signal);
|
|
359
|
+
const uncertainGeneration = record.pendingRegistration?.expectedGeneration;
|
|
360
|
+
if (explicitActivation) {
|
|
361
|
+
this.preparedRevision = undefined;
|
|
362
|
+
this.preparedHostId = undefined;
|
|
363
|
+
record.pendingRegistration = null;
|
|
364
|
+
record.pendingReport = null;
|
|
365
|
+
run.bootId = brandString(randomUUID());
|
|
366
|
+
run.started = false;
|
|
367
|
+
}
|
|
368
|
+
else if (!activated && (this.replaced || record.pendingRegistration?.explicitActivation === true)) {
|
|
369
|
+
await this.rememberReplacement(state, record, member);
|
|
370
|
+
this.options.onReplaced?.(run.hostId);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
if (run.started && record.running?.bootId !== run.bootId)
|
|
375
|
+
throw new ObservationError('AGENT_CONFIG_HOST_CONFLICT');
|
|
376
|
+
await this.registration(state, record, run, signal);
|
|
377
|
+
await this.replay(state, record, run, signal);
|
|
378
|
+
if (record.running?.bootId === run.bootId)
|
|
379
|
+
run.started = true;
|
|
380
|
+
// A discarded sandbox report registers again under this lock, so no other process sharing the file takes the Host in between.
|
|
381
|
+
for (let rebinds = 0;; rebinds++) {
|
|
382
|
+
if (!run.started) {
|
|
383
|
+
if (state.bootSequence >= Number.MAX_SAFE_INTEGER)
|
|
384
|
+
throw new ObservationError('AGENT_CONFIG_HOST_STATE_INVALID');
|
|
385
|
+
state.bootSequence++;
|
|
386
|
+
record.pendingRegistration = { hostId: run.hostId, bootId: run.bootId, requestId: randomUUID(), bootSequence: state.bootSequence, expectedGeneration: explicitActivation ? uncertainGeneration ?? record.generation : record.generation,
|
|
387
|
+
...(run.sandboxBinding === undefined ? {} : { sandboxBinding: run.sandboxBinding }), ...(explicitActivation ? { explicitActivation: true } : {}) };
|
|
388
|
+
await this.save(state);
|
|
389
|
+
try {
|
|
390
|
+
await this.registration(state, record, run, signal);
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
if (!explicitActivation || uncertainGeneration === undefined || !(error instanceof ObservationError) || error.code !== 'AGENT_CONFIG_HOST_GENERATION_CONFLICT')
|
|
394
|
+
throw error;
|
|
395
|
+
// An unacknowledged registration can have advanced the server by exactly one generation.
|
|
396
|
+
const pending = record.pendingRegistration;
|
|
397
|
+
record.pendingRegistration = { ...pending, requestId: randomUUID(), expectedGeneration: pending.expectedGeneration + 1 };
|
|
398
|
+
await this.save(state);
|
|
399
|
+
await this.registration(state, record, run, signal);
|
|
400
|
+
}
|
|
401
|
+
run.started = true;
|
|
402
|
+
}
|
|
403
|
+
if (explicitActivation) {
|
|
404
|
+
this.preparedRevision = revision;
|
|
405
|
+
this.preparedHostId = run.hostId;
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (record.running === null || record.running.sequence >= Number.MAX_SAFE_INTEGER)
|
|
409
|
+
throw new ObservationError('AGENT_CONFIG_HOST_STATE_INVALID');
|
|
410
|
+
record.pendingReport = { ...this.snapshot(), generation: record.generation, sequence: record.running.sequence + 1 };
|
|
411
|
+
await this.save(state);
|
|
412
|
+
if (!await this.replay(state, record, run, signal))
|
|
413
|
+
break;
|
|
414
|
+
if (rebinds > 0)
|
|
415
|
+
throw new ObservationError('AGENT_CONFIG_HOST_UNAVAILABLE');
|
|
416
|
+
}
|
|
417
|
+
delete record.replaced;
|
|
418
|
+
if (activated)
|
|
419
|
+
await this.save(state);
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
if (this.options.sandbox === undefined && (explicitActivation || error instanceof ObservationError && error.code === 'LOCAL_HOST_REPLACED')) {
|
|
423
|
+
await this.rememberReplacement(state, record, member);
|
|
424
|
+
this.options.onReplaced?.(run.hostId);
|
|
425
|
+
}
|
|
426
|
+
throw error;
|
|
427
|
+
}
|
|
428
|
+
}, { signal, waitMs: this.options.requestTimeoutMs });
|
|
429
|
+
}
|
|
430
|
+
finally {
|
|
431
|
+
this.activationRequest = false;
|
|
432
|
+
if (this.active === active)
|
|
433
|
+
this.active = undefined;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async dispose() {
|
|
437
|
+
this.lifetime.abort();
|
|
438
|
+
this.active?.abort();
|
|
439
|
+
if (this.timer !== undefined)
|
|
440
|
+
clearTimeout(this.timer);
|
|
441
|
+
this.stopScope();
|
|
442
|
+
this.stopActivation();
|
|
443
|
+
this.stopActivated();
|
|
444
|
+
this.stopIdentity();
|
|
445
|
+
this.stopReplacement();
|
|
446
|
+
await this.chain;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { AgentConfigError } from './agent-config-format.ts';
|
|
2
|
+
import type { WorkspaceAgentConfigRegistration, WorkspaceAgentConfigState } from './agent-config-types.ts';
|
|
3
|
+
import type { WorkspaceBundlesSpec, WorkspaceTemplate, WorkspaceTemplateDigest } from './types.ts';
|
|
4
|
+
/** 一个注册来源的缓存、轮询和候选版本;卸载必须等待所有网络与文件工作结束。 */
|
|
5
|
+
export declare class WorkspaceAgentConfigSourceOwner {
|
|
6
|
+
readonly registration: WorkspaceAgentConfigRegistration;
|
|
7
|
+
private readonly spec;
|
|
8
|
+
private readonly report;
|
|
9
|
+
private readonly download;
|
|
10
|
+
private readonly recoverIdle;
|
|
11
|
+
private readonly lifetime;
|
|
12
|
+
private inFlight;
|
|
13
|
+
private requestAbort;
|
|
14
|
+
private requestRevision;
|
|
15
|
+
private readonly preparedRevisions;
|
|
16
|
+
private timer;
|
|
17
|
+
private selected;
|
|
18
|
+
private candidate;
|
|
19
|
+
private failure;
|
|
20
|
+
private rejection;
|
|
21
|
+
private admissionFailure;
|
|
22
|
+
private readonly root;
|
|
23
|
+
private observation;
|
|
24
|
+
private startup;
|
|
25
|
+
private blockedVersion;
|
|
26
|
+
/** @param registration - 内置模板及鉴权查询回调。 @param spec - 部署预算。 @param report - 固定失败诊断的报告者。 @param download - 文件 HTTP 传输。 @param recoverIdle - Provider 串行检查活动租约后恢复已阻断的配置;省略时保留候选等待获取工作区。 */
|
|
27
|
+
constructor(registration: WorkspaceAgentConfigRegistration, spec: WorkspaceBundlesSpec, report: (error: AgentConfigError) => void, download?: typeof fetch, recoverIdle?: () => Promise<void>);
|
|
28
|
+
/** @returns 已应用/候选版本、执行阻断及当前作用域的最近失败和拒绝。 */
|
|
29
|
+
state(): WorkspaceAgentConfigState;
|
|
30
|
+
/**
|
|
31
|
+
* 从 journal 已提交的摘要定位只读缓存,不从 server 的无分配回答推导降级。
|
|
32
|
+
* @param digest - 已提交 Workspace 模板摘要;undefined 表示首次使用。
|
|
33
|
+
* @param version - 已提交完整版本,包括构建元数据。
|
|
34
|
+
* @param sha256 - 已提交包摘要;undefined 支持旧内置记录。
|
|
35
|
+
* @returns 对应完整模板;缺失已安装缓存时拒绝继续。
|
|
36
|
+
*/
|
|
37
|
+
recover(digest: WorkspaceTemplateDigest | undefined, version?: string, sha256?: string | null): Promise<WorkspaceTemplate>;
|
|
38
|
+
private recoverExecution;
|
|
39
|
+
/**
|
|
40
|
+
* Load a complete old/new tree only for physical swap recovery; this grants no execution permission.
|
|
41
|
+
* @param digest - Journal template digest.
|
|
42
|
+
* @param version - Journal release version, when recorded.
|
|
43
|
+
* @param sha256 - Journal package digest, absent for builtin.
|
|
44
|
+
* @returns Physically verified template; consumers must subsequently call recover for execution validation.
|
|
45
|
+
*/
|
|
46
|
+
recoverPhysical(digest: WorkspaceTemplateDigest | undefined, version?: string, sha256?: string | null): Promise<WorkspaceTemplate>;
|
|
47
|
+
private restoredTemplate;
|
|
48
|
+
private assertCompatible;
|
|
49
|
+
private configError;
|
|
50
|
+
private reportFailure;
|
|
51
|
+
/** @param restore - Restore this identity's committed journal before starting reports or queries; omission is for an owner without a journal. */
|
|
52
|
+
start(restore?: () => Promise<void>): void;
|
|
53
|
+
/** @returns 本次查询和完整候选下载结束;失败通过 state 与 report 保留可观察诊断。 */
|
|
54
|
+
refresh(): Promise<void>;
|
|
55
|
+
private refreshReady;
|
|
56
|
+
/** @returns 下一个 turn 可应用的候选;没有候选时使用已安装版本。 */
|
|
57
|
+
template(): WorkspaceTemplate;
|
|
58
|
+
/** @throws AgentConfigError while committed content is blocked, including when another lease remains active. */
|
|
59
|
+
assertExecutionAllowed(): void;
|
|
60
|
+
/** @param template - Pending template. @returns After compatibility and consumer validation, without selecting it. */
|
|
61
|
+
prepare(template: WorkspaceTemplate): Promise<void>;
|
|
62
|
+
private prepareValidated;
|
|
63
|
+
/** @param template - 即将整体替换的候选。 @throws 来源代次已经改变或已撤销时拒绝应用。 */
|
|
64
|
+
assertApplicable(template: WorkspaceTemplate): void;
|
|
65
|
+
/** @param template - 已通过 Workspace 整体替换提交的模板。 */
|
|
66
|
+
committed(template: WorkspaceTemplate): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* 清除未提交候选,报告整体替换失败;已提交状态由 journal 恢复结果决定。
|
|
69
|
+
* @param template - 未能完成提交的模板。
|
|
70
|
+
* @param cause - 保留来源失效分类;其他原因报告固定应用失败诊断。
|
|
71
|
+
*/
|
|
72
|
+
failedApply(template: WorkspaceTemplate, cause: unknown): void;
|
|
73
|
+
/** 撤销查询、清除后续定时器,并等待下载和目录写入完成。 */
|
|
74
|
+
dispose(): Promise<void>;
|
|
75
|
+
private versionDirectory;
|
|
76
|
+
private rememberRejection;
|
|
77
|
+
private fetchCandidate;
|
|
78
|
+
}
|