@xemahq/agent-session-runtime 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 +201 -0
- package/README.md +61 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/composer.d.ts +14 -0
- package/dist/lib/composer.d.ts.map +1 -0
- package/dist/lib/composer.js +595 -0
- package/dist/lib/composer.js.map +1 -0
- package/dist/lib/composition-workspace-manifest.d.ts +44 -0
- package/dist/lib/composition-workspace-manifest.d.ts.map +1 -0
- package/dist/lib/composition-workspace-manifest.js +143 -0
- package/dist/lib/composition-workspace-manifest.js.map +1 -0
- package/dist/lib/dispatch-contract.d.ts +48 -0
- package/dist/lib/dispatch-contract.d.ts.map +1 -0
- package/dist/lib/dispatch-contract.js +8 -0
- package/dist/lib/dispatch-contract.js.map +1 -0
- package/dist/lib/drift-detector.d.ts +40 -0
- package/dist/lib/drift-detector.d.ts.map +1 -0
- package/dist/lib/drift-detector.js +386 -0
- package/dist/lib/drift-detector.js.map +1 -0
- package/dist/lib/environment-resolver.d.ts +84 -0
- package/dist/lib/environment-resolver.d.ts.map +1 -0
- package/dist/lib/environment-resolver.js +181 -0
- package/dist/lib/environment-resolver.js.map +1 -0
- package/dist/lib/errors.d.ts +12 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +27 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/lifecycle-state.d.ts +23 -0
- package/dist/lib/lifecycle-state.d.ts.map +1 -0
- package/dist/lib/lifecycle-state.js +70 -0
- package/dist/lib/lifecycle-state.js.map +1 -0
- package/dist/lib/skill-bundle-template-resolver.d.ts +23 -0
- package/dist/lib/skill-bundle-template-resolver.d.ts.map +1 -0
- package/dist/lib/skill-bundle-template-resolver.js +54 -0
- package/dist/lib/skill-bundle-template-resolver.js.map +1 -0
- package/dist/lib/types.d.ts +141 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +3 -0
- package/dist/lib/types.js.map +1 -0
- package/package.json +45 -0
- package/src/index.ts +34 -0
- package/src/lib/composer.ts +1041 -0
- package/src/lib/composition-workspace-manifest.ts +432 -0
- package/src/lib/dispatch-contract.ts +156 -0
- package/src/lib/drift-detector.ts +497 -0
- package/src/lib/environment-resolver.ts +480 -0
- package/src/lib/errors.ts +43 -0
- package/src/lib/lifecycle-state.ts +139 -0
- package/src/lib/skill-bundle-template-resolver.ts +147 -0
- package/src/lib/types.ts +443 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// ── Environment drift detector ──
|
|
3
|
+
//
|
|
4
|
+
// Diffs two `ResolvedEnvironment` snapshots and emits a structured
|
|
5
|
+
// drift report. Used by agent-session resume to compare the
|
|
6
|
+
// snapshot from the previous run against a freshly-resolved snapshot
|
|
7
|
+
// at resume time — re-resolve is the source of truth, drift is what
|
|
8
|
+
// surfaces to the operator.
|
|
9
|
+
//
|
|
10
|
+
// Drift kinds are a closed enum (`DriftKind`). Severity is `info |
|
|
11
|
+
// warning` — purely advisory: NO drift blocks a resume. Resume always
|
|
12
|
+
// proceeds with the freshly-resolved environment; the drift list is
|
|
13
|
+
// surfaced to the operator (timeline entry + the `SessionResumed`
|
|
14
|
+
// degradation toast) so they see what changed. `warning` flags the
|
|
15
|
+
// changes worth a second look (a removed sub-agent/skill/credential, a
|
|
16
|
+
// changed agent, a tightened permission); `info` is benign drift
|
|
17
|
+
// (version bumps, additions, value tweaks).
|
|
18
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
19
|
+
|
|
20
|
+
import type { ToolSelectionEntry } from '@xemahq/kernel-contracts/mcp-tool';
|
|
21
|
+
import type {
|
|
22
|
+
CompiledManifestCredential,
|
|
23
|
+
CompiledManifestSkillRef,
|
|
24
|
+
SubAgentBinding,
|
|
25
|
+
} from '@xemahq/kernel-contracts/workflow';
|
|
26
|
+
|
|
27
|
+
import type { ResolvedEnvironment } from './environment-resolver';
|
|
28
|
+
|
|
29
|
+
export enum DriftKind {
|
|
30
|
+
AGENT_CHANGED = 'agent-changed',
|
|
31
|
+
ROLE_CHANGED = 'role-changed',
|
|
32
|
+
MANIFEST_VERSION_CHANGED = 'manifest-version-changed',
|
|
33
|
+
MODEL_CHANGED = 'model-changed',
|
|
34
|
+
SUBAGENT_ADDED = 'subagent-added',
|
|
35
|
+
SUBAGENT_REMOVED = 'subagent-removed',
|
|
36
|
+
SUBAGENT_MODEL_CHANGED = 'subagent-model-changed',
|
|
37
|
+
SKILL_ADDED = 'skill-added',
|
|
38
|
+
SKILL_REMOVED = 'skill-removed',
|
|
39
|
+
SKILL_VERSION_CHANGED = 'skill-version-changed',
|
|
40
|
+
TOOL_SELECTION_ADDED = 'tool-selection-added',
|
|
41
|
+
TOOL_SELECTION_REMOVED = 'tool-selection-removed',
|
|
42
|
+
CREDENTIAL_ADDED = 'credential-added',
|
|
43
|
+
CREDENTIAL_REMOVED = 'credential-removed',
|
|
44
|
+
CREDENTIAL_KIND_CHANGED = 'credential-kind-changed',
|
|
45
|
+
PERMISSION_TIGHTENED = 'permission-tightened',
|
|
46
|
+
PERMISSION_RELAXED = 'permission-relaxed',
|
|
47
|
+
PERSISTENCE_PATH_ADDED = 'persistence-path-added',
|
|
48
|
+
PERSISTENCE_PATH_REMOVED = 'persistence-path-removed',
|
|
49
|
+
OUTPUT_SURFACE_CHANGED = 'output-surface-changed',
|
|
50
|
+
ENV_VAR_ADDED = 'env-var-added',
|
|
51
|
+
ENV_VAR_REMOVED = 'env-var-removed',
|
|
52
|
+
ENV_VAR_VALUE_CHANGED = 'env-var-value-changed',
|
|
53
|
+
MOUNT_ADDED = 'mount-added',
|
|
54
|
+
MOUNT_REMOVED = 'mount-removed',
|
|
55
|
+
MOUNT_MODE_CHANGED = 'mount-mode-changed',
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Advisory drift severity — `info` (benign) or `warning` (worth a look).
|
|
60
|
+
* There is deliberately no `critical`: resume is non-blocking by design
|
|
61
|
+
* (plan Epic D §D4), so severity only drives how the operator's drift
|
|
62
|
+
* toast renders, never whether the session resumes.
|
|
63
|
+
*/
|
|
64
|
+
export type DriftSeverity = 'info' | 'warning';
|
|
65
|
+
|
|
66
|
+
export interface EnvironmentDrift {
|
|
67
|
+
readonly kind: DriftKind;
|
|
68
|
+
readonly severity: DriftSeverity;
|
|
69
|
+
readonly path: string;
|
|
70
|
+
readonly before?: unknown;
|
|
71
|
+
readonly after?: unknown;
|
|
72
|
+
readonly message: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Diff two `ResolvedEnvironment` snapshots. Returns a possibly-empty
|
|
77
|
+
* array of structured drifts. Order is stable (insertion order across
|
|
78
|
+
* the structured walk) so callers can render a deterministic timeline
|
|
79
|
+
* entry.
|
|
80
|
+
*
|
|
81
|
+
* Equal hashes short-circuit to `[]` — the hash covers every
|
|
82
|
+
* structured field, so two snapshots with the same hash are
|
|
83
|
+
* guaranteed to produce no drift.
|
|
84
|
+
*/
|
|
85
|
+
export function diffResolvedEnvironment(
|
|
86
|
+
prev: ResolvedEnvironment,
|
|
87
|
+
next: ResolvedEnvironment,
|
|
88
|
+
): readonly EnvironmentDrift[] {
|
|
89
|
+
if (prev.hash === next.hash) {return [];}
|
|
90
|
+
|
|
91
|
+
const drifts: EnvironmentDrift[] = [];
|
|
92
|
+
|
|
93
|
+
diffAgent(prev, next, drifts);
|
|
94
|
+
diffSubAgents(prev.subAgents, next.subAgents, drifts);
|
|
95
|
+
diffSkills(prev.skills, next.skills, drifts);
|
|
96
|
+
diffToolSelection(prev.toolSelection, next.toolSelection, drifts);
|
|
97
|
+
diffCredentials(prev.credentials, next.credentials, drifts);
|
|
98
|
+
diffPermissions(prev, next, drifts);
|
|
99
|
+
diffPersistence(prev, next, drifts);
|
|
100
|
+
diffOutputSurface(prev, next, drifts);
|
|
101
|
+
diffEnv(prev, next, drifts);
|
|
102
|
+
diffMounts(prev, next, drifts);
|
|
103
|
+
|
|
104
|
+
return drifts;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function diffAgent(
|
|
108
|
+
prev: ResolvedEnvironment,
|
|
109
|
+
next: ResolvedEnvironment,
|
|
110
|
+
out: EnvironmentDrift[],
|
|
111
|
+
): void {
|
|
112
|
+
if (prev.manifest.version !== next.manifest.version) {
|
|
113
|
+
out.push({
|
|
114
|
+
kind: DriftKind.MANIFEST_VERSION_CHANGED,
|
|
115
|
+
severity: 'info',
|
|
116
|
+
path: '$.manifest.version',
|
|
117
|
+
before: prev.manifest.version,
|
|
118
|
+
after: next.manifest.version,
|
|
119
|
+
message: `Manifest version moved ${prev.manifest.version} → ${next.manifest.version}.`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (prev.agent.slug !== next.agent.slug) {
|
|
123
|
+
out.push({
|
|
124
|
+
kind: DriftKind.AGENT_CHANGED,
|
|
125
|
+
severity: 'warning',
|
|
126
|
+
path: '$.agent.slug',
|
|
127
|
+
before: prev.agent.slug,
|
|
128
|
+
after: next.agent.slug,
|
|
129
|
+
message: `Primary agent slug changed ${prev.agent.slug} → ${next.agent.slug}; the session resumes with the new agent.`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (prev.agent.role !== next.agent.role) {
|
|
133
|
+
out.push({
|
|
134
|
+
kind: DriftKind.ROLE_CHANGED,
|
|
135
|
+
severity: 'info',
|
|
136
|
+
path: '$.agent.role',
|
|
137
|
+
before: prev.agent.role,
|
|
138
|
+
after: next.agent.role,
|
|
139
|
+
message: `Agent role changed ${prev.agent.role} → ${next.agent.role}; AGENTS.md will be re-rendered.`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const prevModel = JSON.stringify(prev.agent.model ?? null);
|
|
143
|
+
const nextModel = JSON.stringify(next.agent.model ?? null);
|
|
144
|
+
if (prevModel !== nextModel) {
|
|
145
|
+
out.push({
|
|
146
|
+
kind: DriftKind.MODEL_CHANGED,
|
|
147
|
+
severity: 'info',
|
|
148
|
+
path: '$.agent.model',
|
|
149
|
+
before: prev.agent.model,
|
|
150
|
+
after: next.agent.model,
|
|
151
|
+
message: 'Primary agent default model changed.',
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function diffSubAgents(
|
|
157
|
+
prev: readonly SubAgentBinding[],
|
|
158
|
+
next: readonly SubAgentBinding[],
|
|
159
|
+
out: EnvironmentDrift[],
|
|
160
|
+
): void {
|
|
161
|
+
const prevBySlug = new Map(prev.map((s) => [s.slug, s]));
|
|
162
|
+
const nextBySlug = new Map(next.map((s) => [s.slug, s]));
|
|
163
|
+
for (const [slug, n] of nextBySlug) {
|
|
164
|
+
const p = prevBySlug.get(slug);
|
|
165
|
+
if (!p) {
|
|
166
|
+
out.push({
|
|
167
|
+
kind: DriftKind.SUBAGENT_ADDED,
|
|
168
|
+
severity: 'info',
|
|
169
|
+
path: `$.subAgents[${slug}]`,
|
|
170
|
+
after: n,
|
|
171
|
+
message: `Sub-agent '${slug}' added.`,
|
|
172
|
+
});
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (JSON.stringify(p.modelOverride ?? null) !== JSON.stringify(n.modelOverride ?? null)) {
|
|
176
|
+
out.push({
|
|
177
|
+
kind: DriftKind.SUBAGENT_MODEL_CHANGED,
|
|
178
|
+
severity: 'info',
|
|
179
|
+
path: `$.subAgents[${slug}].modelOverride`,
|
|
180
|
+
before: p.modelOverride,
|
|
181
|
+
after: n.modelOverride,
|
|
182
|
+
message: `Sub-agent '${slug}' model override changed.`,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
for (const [slug, p] of prevBySlug) {
|
|
187
|
+
if (!nextBySlug.has(slug)) {
|
|
188
|
+
out.push({
|
|
189
|
+
kind: DriftKind.SUBAGENT_REMOVED,
|
|
190
|
+
severity: 'warning',
|
|
191
|
+
path: `$.subAgents[${slug}]`,
|
|
192
|
+
before: p,
|
|
193
|
+
message: `Sub-agent '${slug}' removed; existing delegations may fail until the session is restarted.`,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function diffSkills(
|
|
200
|
+
prev: readonly CompiledManifestSkillRef[],
|
|
201
|
+
next: readonly CompiledManifestSkillRef[],
|
|
202
|
+
out: EnvironmentDrift[],
|
|
203
|
+
): void {
|
|
204
|
+
const prevBySlug = new Map(prev.map((s) => [s.slug, s]));
|
|
205
|
+
const nextBySlug = new Map(next.map((s) => [s.slug, s]));
|
|
206
|
+
for (const [slug, n] of nextBySlug) {
|
|
207
|
+
const p = prevBySlug.get(slug);
|
|
208
|
+
if (!p) {
|
|
209
|
+
out.push({
|
|
210
|
+
kind: DriftKind.SKILL_ADDED,
|
|
211
|
+
severity: 'info',
|
|
212
|
+
path: `$.skills[${slug}]`,
|
|
213
|
+
after: n,
|
|
214
|
+
message: `Skill '${slug}' added.`,
|
|
215
|
+
});
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (p.version !== n.version) {
|
|
219
|
+
out.push({
|
|
220
|
+
kind: DriftKind.SKILL_VERSION_CHANGED,
|
|
221
|
+
severity: 'info',
|
|
222
|
+
path: `$.skills[${slug}].version`,
|
|
223
|
+
before: p.version,
|
|
224
|
+
after: n.version,
|
|
225
|
+
message: `Skill '${slug}' version moved ${p.version ?? '*'} → ${n.version ?? '*'}.`,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
for (const [slug, p] of prevBySlug) {
|
|
230
|
+
if (!nextBySlug.has(slug)) {
|
|
231
|
+
out.push({
|
|
232
|
+
kind: DriftKind.SKILL_REMOVED,
|
|
233
|
+
severity: 'warning',
|
|
234
|
+
path: `$.skills[${slug}]`,
|
|
235
|
+
before: p,
|
|
236
|
+
message: `Skill '${slug}' removed.`,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function toolSelectionKey(entry: ToolSelectionEntry): string {
|
|
243
|
+
return entry.kind === 'tool'
|
|
244
|
+
? `tool:${entry.providerKind}:${entry.resourceId}:${entry.toolName}`
|
|
245
|
+
: `provider:${entry.providerKind}:${entry.resourceId}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function diffToolSelection(
|
|
249
|
+
prev: readonly ToolSelectionEntry[],
|
|
250
|
+
next: readonly ToolSelectionEntry[],
|
|
251
|
+
out: EnvironmentDrift[],
|
|
252
|
+
): void {
|
|
253
|
+
const prevByKey = new Map(prev.map((e) => [toolSelectionKey(e), e]));
|
|
254
|
+
const nextByKey = new Map(next.map((e) => [toolSelectionKey(e), e]));
|
|
255
|
+
for (const [key, n] of nextByKey) {
|
|
256
|
+
if (!prevByKey.has(key)) {
|
|
257
|
+
out.push({
|
|
258
|
+
kind: DriftKind.TOOL_SELECTION_ADDED,
|
|
259
|
+
severity: 'info',
|
|
260
|
+
path: `$.toolSelection[${key}]`,
|
|
261
|
+
after: n,
|
|
262
|
+
message: `Tool selection entry '${key}' added.`,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
for (const [key, p] of prevByKey) {
|
|
267
|
+
if (!nextByKey.has(key)) {
|
|
268
|
+
out.push({
|
|
269
|
+
kind: DriftKind.TOOL_SELECTION_REMOVED,
|
|
270
|
+
severity: 'warning',
|
|
271
|
+
path: `$.toolSelection[${key}]`,
|
|
272
|
+
before: p,
|
|
273
|
+
message: `Tool selection entry '${key}' removed.`,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function diffCredentials(
|
|
280
|
+
prev: readonly CompiledManifestCredential[],
|
|
281
|
+
next: readonly CompiledManifestCredential[],
|
|
282
|
+
out: EnvironmentDrift[],
|
|
283
|
+
): void {
|
|
284
|
+
const prevByName = new Map(prev.map((c) => [c.name, c]));
|
|
285
|
+
const nextByName = new Map(next.map((c) => [c.name, c]));
|
|
286
|
+
for (const [name, n] of nextByName) {
|
|
287
|
+
const p = prevByName.get(name);
|
|
288
|
+
if (!p) {
|
|
289
|
+
out.push({
|
|
290
|
+
kind: DriftKind.CREDENTIAL_ADDED,
|
|
291
|
+
severity: 'info',
|
|
292
|
+
path: `$.credentials[${name}]`,
|
|
293
|
+
after: n,
|
|
294
|
+
message: `Credential '${name}' added.`,
|
|
295
|
+
});
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (p.kind !== n.kind) {
|
|
299
|
+
out.push({
|
|
300
|
+
kind: DriftKind.CREDENTIAL_KIND_CHANGED,
|
|
301
|
+
severity: 'warning',
|
|
302
|
+
path: `$.credentials[${name}].kind`,
|
|
303
|
+
before: p.kind,
|
|
304
|
+
after: n.kind,
|
|
305
|
+
message: `Credential '${name}' kind changed ${p.kind} → ${n.kind}; the resolver will re-route it on resume.`,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
for (const [name, p] of prevByName) {
|
|
310
|
+
if (!nextByName.has(name)) {
|
|
311
|
+
out.push({
|
|
312
|
+
kind: DriftKind.CREDENTIAL_REMOVED,
|
|
313
|
+
// Resume is non-blocking even for a previously-required
|
|
314
|
+
// credential — the restore path degrades to a still-usable
|
|
315
|
+
// provider. `warning` simply flags it for the operator.
|
|
316
|
+
severity: 'warning',
|
|
317
|
+
path: `$.credentials[${name}]`,
|
|
318
|
+
before: p,
|
|
319
|
+
message: `Credential '${name}' removed${p.required ? ' (was required; the session resumes with the remaining credentials)' : ''}.`,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function diffPermissions(
|
|
326
|
+
prev: ResolvedEnvironment,
|
|
327
|
+
next: ResolvedEnvironment,
|
|
328
|
+
out: EnvironmentDrift[],
|
|
329
|
+
): void {
|
|
330
|
+
const prevAllow = new Set(prev.permissions.tools.allow);
|
|
331
|
+
const nextAllow = new Set(next.permissions.tools.allow);
|
|
332
|
+
const prevDeny = new Set(prev.permissions.tools.deny);
|
|
333
|
+
const nextDeny = new Set(next.permissions.tools.deny);
|
|
334
|
+
// A tool is "tightened" if it was previously allowed and is now
|
|
335
|
+
// explicitly denied OR removed from the allow-list.
|
|
336
|
+
for (const tool of prevAllow) {
|
|
337
|
+
if (!nextAllow.has(tool) || nextDeny.has(tool)) {
|
|
338
|
+
out.push({
|
|
339
|
+
kind: DriftKind.PERMISSION_TIGHTENED,
|
|
340
|
+
severity: 'warning',
|
|
341
|
+
path: `$.permissions.tools.${tool}`,
|
|
342
|
+
before: 'allow',
|
|
343
|
+
after: nextDeny.has(tool) ? 'deny' : 'unset',
|
|
344
|
+
message: `Tool '${tool}' is no longer allowed.`,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
for (const tool of nextAllow) {
|
|
349
|
+
if (!prevAllow.has(tool) || prevDeny.has(tool)) {
|
|
350
|
+
out.push({
|
|
351
|
+
kind: DriftKind.PERMISSION_RELAXED,
|
|
352
|
+
severity: 'info',
|
|
353
|
+
path: `$.permissions.tools.${tool}`,
|
|
354
|
+
before: prevDeny.has(tool) ? 'deny' : 'unset',
|
|
355
|
+
after: 'allow',
|
|
356
|
+
message: `Tool '${tool}' is now allowed.`,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function diffPersistence(
|
|
363
|
+
prev: ResolvedEnvironment,
|
|
364
|
+
next: ResolvedEnvironment,
|
|
365
|
+
out: EnvironmentDrift[],
|
|
366
|
+
): void {
|
|
367
|
+
const prevPaths = new Set(prev.persistence.paths);
|
|
368
|
+
const nextPaths = new Set(next.persistence.paths);
|
|
369
|
+
for (const path of nextPaths) {
|
|
370
|
+
if (!prevPaths.has(path)) {
|
|
371
|
+
out.push({
|
|
372
|
+
kind: DriftKind.PERSISTENCE_PATH_ADDED,
|
|
373
|
+
severity: 'info',
|
|
374
|
+
path: `$.persistence.paths`,
|
|
375
|
+
after: path,
|
|
376
|
+
message: `Persistence path '${path}' added; future pauses will snapshot it.`,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
for (const path of prevPaths) {
|
|
381
|
+
if (!nextPaths.has(path)) {
|
|
382
|
+
out.push({
|
|
383
|
+
kind: DriftKind.PERSISTENCE_PATH_REMOVED,
|
|
384
|
+
severity: 'info',
|
|
385
|
+
path: `$.persistence.paths`,
|
|
386
|
+
before: path,
|
|
387
|
+
message: `Persistence path '${path}' removed; future pauses will not snapshot it.`,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function diffOutputSurface(
|
|
394
|
+
prev: ResolvedEnvironment,
|
|
395
|
+
next: ResolvedEnvironment,
|
|
396
|
+
out: EnvironmentDrift[],
|
|
397
|
+
): void {
|
|
398
|
+
if (
|
|
399
|
+
prev.outputSurface.kind !== next.outputSurface.kind ||
|
|
400
|
+
prev.outputSurface.port !== next.outputSurface.port ||
|
|
401
|
+
prev.outputSurface.healthPath !== next.outputSurface.healthPath ||
|
|
402
|
+
prev.outputSurface.mode !== next.outputSurface.mode
|
|
403
|
+
) {
|
|
404
|
+
out.push({
|
|
405
|
+
kind: DriftKind.OUTPUT_SURFACE_CHANGED,
|
|
406
|
+
severity: 'info',
|
|
407
|
+
path: '$.outputSurface',
|
|
408
|
+
before: prev.outputSurface,
|
|
409
|
+
after: next.outputSurface,
|
|
410
|
+
message: 'Output surface configuration changed.',
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function diffEnv(
|
|
416
|
+
prev: ResolvedEnvironment,
|
|
417
|
+
next: ResolvedEnvironment,
|
|
418
|
+
out: EnvironmentDrift[],
|
|
419
|
+
): void {
|
|
420
|
+
const prevByName = new Map(prev.env.map((e) => [e.name, e.value]));
|
|
421
|
+
const nextByName = new Map(next.env.map((e) => [e.name, e.value]));
|
|
422
|
+
for (const [name, value] of nextByName) {
|
|
423
|
+
const prevValue = prevByName.get(name);
|
|
424
|
+
if (prevValue === undefined) {
|
|
425
|
+
out.push({
|
|
426
|
+
kind: DriftKind.ENV_VAR_ADDED,
|
|
427
|
+
severity: 'info',
|
|
428
|
+
path: `$.env[${name}]`,
|
|
429
|
+
after: value,
|
|
430
|
+
message: `Env var '${name}' added.`,
|
|
431
|
+
});
|
|
432
|
+
} else if (prevValue !== value) {
|
|
433
|
+
out.push({
|
|
434
|
+
kind: DriftKind.ENV_VAR_VALUE_CHANGED,
|
|
435
|
+
severity: 'info',
|
|
436
|
+
path: `$.env[${name}]`,
|
|
437
|
+
before: prevValue,
|
|
438
|
+
after: value,
|
|
439
|
+
message: `Env var '${name}' value changed.`,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
for (const name of prevByName.keys()) {
|
|
444
|
+
if (!nextByName.has(name)) {
|
|
445
|
+
out.push({
|
|
446
|
+
kind: DriftKind.ENV_VAR_REMOVED,
|
|
447
|
+
severity: 'warning',
|
|
448
|
+
path: `$.env[${name}]`,
|
|
449
|
+
before: prevByName.get(name),
|
|
450
|
+
message: `Env var '${name}' removed.`,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function diffMounts(
|
|
457
|
+
prev: ResolvedEnvironment,
|
|
458
|
+
next: ResolvedEnvironment,
|
|
459
|
+
out: EnvironmentDrift[],
|
|
460
|
+
): void {
|
|
461
|
+
const prevSlots = new Map(prev.mountPlan.entries.map((e) => [e.slot, e]));
|
|
462
|
+
const nextSlots = new Map(next.mountPlan.entries.map((e) => [e.slot, e]));
|
|
463
|
+
for (const [slot, entry] of nextSlots) {
|
|
464
|
+
const prevEntry = prevSlots.get(slot);
|
|
465
|
+
if (!prevEntry) {
|
|
466
|
+
out.push({
|
|
467
|
+
kind: DriftKind.MOUNT_ADDED,
|
|
468
|
+
severity: 'info',
|
|
469
|
+
path: `$.mounts[${slot}]`,
|
|
470
|
+
after: { mountKey: entry.mountKey },
|
|
471
|
+
message: `Mount slot '${slot}' added.`,
|
|
472
|
+
});
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
if (prevEntry.mode !== entry.mode) {
|
|
476
|
+
out.push({
|
|
477
|
+
kind: DriftKind.MOUNT_MODE_CHANGED,
|
|
478
|
+
severity: 'warning',
|
|
479
|
+
path: `$.mounts[${slot}].mode`,
|
|
480
|
+
before: prevEntry.mode,
|
|
481
|
+
after: entry.mode,
|
|
482
|
+
message: `Mount slot '${slot}' mode changed ${prevEntry.mode} → ${entry.mode}.`,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
for (const slot of prevSlots.keys()) {
|
|
487
|
+
if (!nextSlots.has(slot)) {
|
|
488
|
+
out.push({
|
|
489
|
+
kind: DriftKind.MOUNT_REMOVED,
|
|
490
|
+
severity: 'warning',
|
|
491
|
+
path: `$.mounts[${slot}]`,
|
|
492
|
+
before: { slot },
|
|
493
|
+
message: `Mount slot '${slot}' removed.`,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|