@agent-link/agent 0.1.298 → 0.1.300

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.
@@ -4,7 +4,43 @@
4
4
  const fs = require('fs');
5
5
  const os = require('os');
6
6
  const path = require('path');
7
- const { spawn } = require('child_process');
7
+ const crypto = require('crypto');
8
+ const childProcess = require('child_process');
9
+ const { spawn } = childProcess;
10
+ let forceLastKnownGood = false;
11
+ let repairResumeJobId = null;
12
+ let workerOwnedJobId = null;
13
+ const JOB_ID = /^[a-zA-Z0-9-]{1,128}$/;
14
+ const LAUNCH_HOST_TYPES = new Set([
15
+ 'standalone',
16
+ 'systemd',
17
+ 'launchd',
18
+ 'windows-startup',
19
+ 'winsw',
20
+ ]);
21
+ const UPGRADE_PHASES = new Set([
22
+ 'created',
23
+ 'downloading',
24
+ 'verifying',
25
+ 'staging',
26
+ 'self-checking',
27
+ 'worker-starting',
28
+ 'worker-ready',
29
+ 'shutdown-requested',
30
+ 'old-agent-stopped',
31
+ 'switching',
32
+ 'target-starting',
33
+ 'target-local-health',
34
+ 'relay-pending',
35
+ 'rolling-back',
36
+ 'rollback-starting',
37
+ 'rollback-local-health',
38
+ 'repair-starting',
39
+ 'completed',
40
+ 'rolled-back',
41
+ 'repair-required',
42
+ 'failed',
43
+ ]);
8
44
 
9
45
  function fail(message) {
10
46
  process.stderr.write(`[AgentLink Launcher] ${message}\n`);
@@ -63,11 +99,472 @@ function loadPointer(file, versionsRoot) {
63
99
  return { ...value, entry: canonicalEntry };
64
100
  }
65
101
 
102
+ function loadJson(file) {
103
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
104
+ }
105
+
106
+ function isRecord(value) {
107
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
108
+ }
109
+
110
+ function validateLatestReference(value) {
111
+ if (
112
+ !isRecord(value)
113
+ || value.schemaVersion !== 2
114
+ || typeof value.jobId !== 'string'
115
+ || !JOB_ID.test(value.jobId)
116
+ || typeof value.phase !== 'string'
117
+ || !UPGRADE_PHASES.has(value.phase)
118
+ ) {
119
+ throw new Error('invalid latest upgrade state');
120
+ }
121
+ return value;
122
+ }
123
+
124
+ function loadRecoveryLaunchSpec(launchSpecPath, agentHome) {
125
+ if (typeof launchSpecPath !== 'string' || launchSpecPath.length === 0) {
126
+ throw new Error('recovery LaunchSpec path is invalid');
127
+ }
128
+ const canonicalHome = fs.realpathSync(agentHome);
129
+ const resolvedLaunchSpec = path.resolve(launchSpecPath);
130
+ const canonicalLaunchSpec = fs.realpathSync(resolvedLaunchSpec);
131
+ if (
132
+ path.resolve(agentHome) !== canonicalHome
133
+ || resolvedLaunchSpec !== canonicalLaunchSpec
134
+ || !isInside(canonicalHome, canonicalLaunchSpec)
135
+ || !fs.statSync(canonicalLaunchSpec).isFile()
136
+ ) {
137
+ throw new Error('recovery LaunchSpec is relocated or escapes the trusted data root');
138
+ }
139
+ const launchSpec = loadJson(canonicalLaunchSpec);
140
+ if (
141
+ !isRecord(launchSpec)
142
+ || !isRecord(launchSpec.host)
143
+ || !LAUNCH_HOST_TYPES.has(launchSpec.host.type)
144
+ ) {
145
+ throw new Error('recovery LaunchSpec has an invalid shape');
146
+ }
147
+ return launchSpec;
148
+ }
149
+
150
+ function validateRecoveryJob(value, expectedJobId, agentHome) {
151
+ if (
152
+ !isRecord(value)
153
+ || value.schemaVersion !== 2
154
+ || value.jobId !== expectedJobId
155
+ || value.restartOwner !== 'worker'
156
+ || typeof value.phase !== 'string'
157
+ || !UPGRADE_PHASES.has(value.phase)
158
+ || typeof value.lockNonce !== 'string'
159
+ || value.lockNonce.length === 0
160
+ || path.resolve(value.dataRoot || '') !== agentHome
161
+ ) {
162
+ throw new Error('upgrade job identity, schema, or data root is invalid');
163
+ }
164
+ loadRecoveryLaunchSpec(value.launchSpecPath, agentHome);
165
+ return value;
166
+ }
167
+
168
+ function atomicJson(file, value) {
169
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
170
+ const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
171
+ const fd = fs.openSync(temporary, 'wx', 0o600);
172
+ try {
173
+ fs.writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
174
+ fs.fsyncSync(fd);
175
+ } finally {
176
+ fs.closeSync(fd);
177
+ }
178
+ fs.renameSync(temporary, file);
179
+ if (process.platform !== 'win32') fs.chmodSync(file, 0o600);
180
+ }
181
+
182
+ function processIdentity(pid) {
183
+ if (!Number.isSafeInteger(pid) || pid <= 0) return { status: 'error' };
184
+ if (process.platform === 'linux') {
185
+ try {
186
+ const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8').trim();
187
+ const end = stat.lastIndexOf(')');
188
+ const fields = stat.slice(end + 2).split(/\s+/);
189
+ const boot = fs.readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim();
190
+ return end >= 0 && fields[19] && boot
191
+ ? { status: 'found', identity: `linux:${boot}:${fields[19]}` }
192
+ : { status: 'error' };
193
+ } catch (error) {
194
+ return error.code === 'ENOENT' || error.code === 'ESRCH'
195
+ ? { status: 'absent' }
196
+ : { status: 'error' };
197
+ }
198
+ }
199
+ if (process.platform === 'darwin') {
200
+ try {
201
+ const started = childProcess.execFileSync('/bin/ps', ['-o', 'lstart=', '-p', String(pid)], {
202
+ encoding: 'utf8',
203
+ timeout: 3000,
204
+ }).trim();
205
+ return started ? { status: 'found', identity: `darwin:${started}` } : { status: 'absent' };
206
+ } catch (error) {
207
+ return error.status === 1 ? { status: 'absent' } : { status: 'error' };
208
+ }
209
+ }
210
+ if (process.platform === 'win32') {
211
+ try {
212
+ const script = '& { param([int]$ProcessId) $p=Get-CimInstance Win32_Process -Filter ("ProcessId="+$ProcessId);if($p){$p.CreationDate.ToUniversalTime().ToString("O",[Globalization.CultureInfo]::InvariantCulture)} }';
213
+ const started = childProcess.execFileSync('powershell.exe', [
214
+ '-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script, '-ProcessId', String(pid),
215
+ ], { encoding: 'utf8', windowsHide: true, timeout: 3000 }).trim();
216
+ return started ? { status: 'found', identity: `win32:${started}` } : { status: 'absent' };
217
+ } catch {
218
+ return { status: 'error' };
219
+ }
220
+ }
221
+ return { status: 'error' };
222
+ }
223
+
224
+ function identityState(identity) {
225
+ if (!identity) return { status: 'absent' };
226
+ const actual = processIdentity(identity.pid);
227
+ if (actual.status !== 'found') return actual;
228
+ return actual.identity === identity.processStartIdentity
229
+ ? { status: 'match' }
230
+ : { status: 'mismatch' };
231
+ }
232
+
233
+ function sameIdentity(left, right) {
234
+ return isRecord(left)
235
+ && isRecord(right)
236
+ && Number.isSafeInteger(left.pid)
237
+ && left.pid > 0
238
+ && left.pid === right.pid
239
+ && typeof left.processStartIdentity === 'string'
240
+ && left.processStartIdentity === right.processStartIdentity;
241
+ }
242
+
243
+ function hasValidWorkerRecoveryOwner(agentHome, job) {
244
+ if (
245
+ process.env.AGENTLINK_RECOVERY_OWNER !== 'worker'
246
+ || process.env.AGENTLINK_UPGRADE_JOB !== job.jobId
247
+ || !['target-starting', 'rollback-starting'].includes(job.phase)
248
+ || !isRecord(job.worker)
249
+ || job.worker.protocolVersion !== 1
250
+ || identityState(job.worker).status !== 'match'
251
+ ) {
252
+ return false;
253
+ }
254
+ try {
255
+ const lockDirectory = path.join(agentHome, 'upgrades', 'active.lock');
256
+ const owner = loadJson(path.join(lockDirectory, 'owner.json'));
257
+ const claim = loadJson(path.join(lockDirectory, `handoff-${job.lockNonce}.claim`));
258
+ return isRecord(owner)
259
+ && owner.jobId === job.jobId
260
+ && owner.nonce === job.lockNonce
261
+ && path.resolve(owner.dataRoot || '') === agentHome
262
+ && sameIdentity(owner.worker, job.worker)
263
+ && isRecord(claim)
264
+ && claim.role === 'worker'
265
+ && claim.jobId === job.jobId
266
+ && claim.nonce === job.lockNonce
267
+ && sameIdentity(claim.worker, job.worker);
268
+ } catch {
269
+ return false;
270
+ }
271
+ }
272
+
273
+ function verifyLockReclaimable(agentHome, job) {
274
+ const lock = path.join(agentHome, 'upgrades', 'active.lock');
275
+ if (!fs.existsSync(lock)) return null;
276
+ const owner = loadJson(path.join(lock, 'owner.json'));
277
+ if (owner.jobId !== job.jobId || owner.nonce !== job.lockNonce) {
278
+ throw new Error('active upgrade lock does not match interrupted job');
279
+ }
280
+ for (const identity of [owner.controller, owner.worker]) {
281
+ const state = identityState(identity);
282
+ if (state.status === 'match') throw new Error('active upgrade lock still has a live owner');
283
+ if (state.status === 'error') {
284
+ throw new Error('cannot safely reclaim active upgrade lock after an identity lookup failure');
285
+ }
286
+ }
287
+ return { lock, owner };
288
+ }
289
+
290
+ function releaseStaleLock(agentHome, job) {
291
+ const reclaimable = verifyLockReclaimable(agentHome, job);
292
+ if (!reclaimable) return;
293
+ const { lock } = reclaimable;
294
+ const generation = crypto.createHash('sha256')
295
+ .update(`${reclaimable.owner.jobId}\0${reclaimable.owner.nonce}\0${reclaimable.owner.controller.pid}\0${reclaimable.owner.controller.processStartIdentity}`)
296
+ .digest('hex')
297
+ .slice(0, 16);
298
+ const stale = `${lock}.stale-${job.lockNonce}-${generation}`;
299
+ try {
300
+ fs.renameSync(lock, stale);
301
+ } catch (error) {
302
+ if (
303
+ ['EEXIST', 'ENOTEMPTY', 'ENOENT', 'EPERM'].includes(error.code)
304
+ && fs.existsSync(stale)
305
+ ) {
306
+ return;
307
+ }
308
+ throw error;
309
+ }
310
+ }
311
+
312
+ function markInterruptedPreShutdown(agentHome, jobPath, job, reason) {
313
+ verifyLockReclaimable(agentHome, job);
314
+ const updated = {
315
+ ...job,
316
+ phase: 'failed',
317
+ updatedAt: new Date().toISOString(),
318
+ error: reason,
319
+ };
320
+ atomicJson(jobPath, updated);
321
+ atomicJson(path.join(agentHome, 'upgrades', 'latest.json'), updated);
322
+ releaseStaleLock(agentHome, updated);
323
+ }
324
+
325
+ function reconcileTerminalLock(agentHome, job) {
326
+ const lock = path.join(agentHome, 'upgrades', 'active.lock');
327
+ if (!fs.existsSync(lock)) return false;
328
+ const owner = loadJson(path.join(lock, 'owner.json'));
329
+ if (owner.jobId !== job.jobId || owner.nonce !== job.lockNonce) {
330
+ throw new Error('terminal upgrade lock does not match the durable job');
331
+ }
332
+ let ownerAlive = false;
333
+ for (const identity of [owner.controller, owner.worker]) {
334
+ const state = identityState(identity);
335
+ if (state.status === 'match') ownerAlive = true;
336
+ if (state.status === 'error') {
337
+ throw new Error('cannot reconcile terminal upgrade lock after an identity lookup failure');
338
+ }
339
+ }
340
+ let replacementAlive = false;
341
+ for (const identity of [job.repairRuntime, job.rollbackRuntime, job.targetRuntime]) {
342
+ const state = identityState(identity);
343
+ if (state.status === 'match') replacementAlive = true;
344
+ if (state.status === 'error') {
345
+ throw new Error('cannot reconcile terminal runtime after an identity lookup failure');
346
+ }
347
+ }
348
+ if (ownerAlive) return true;
349
+ releaseStaleLock(agentHome, job);
350
+ return replacementAlive;
351
+ }
352
+
353
+ function escapeXml(value) {
354
+ return value
355
+ .replace(/&/g, '&amp;')
356
+ .replace(/</g, '&lt;')
357
+ .replace(/>/g, '&gt;')
358
+ .replace(/"/g, '&quot;')
359
+ .replace(/'/g, '&apos;');
360
+ }
361
+
362
+ function startRecoveryWorker(agentHome, jobPath, job) {
363
+ const workerPath = path.join(path.dirname(jobPath), 'worker.cjs');
364
+ if (!fs.statSync(workerPath).isFile()) throw new Error('recovery worker is missing');
365
+ const logPath = path.join(path.dirname(jobPath), 'worker.log');
366
+ const hostType = loadRecoveryLaunchSpec(job.launchSpecPath, agentHome).host.type;
367
+ const environment = { ...process.env, AGENTLINK_HOME: agentHome };
368
+ if (hostType === 'systemd' && process.platform !== 'win32') {
369
+ childProcess.execFileSync('systemd-run', [
370
+ '--user',
371
+ `--unit=agentlink-upgrade-${job.jobId}`,
372
+ '--collect',
373
+ '--property=Type=exec',
374
+ '--property=KillMode=process',
375
+ `--property=StandardOutput=append:${logPath}`,
376
+ `--property=StandardError=append:${logPath}`,
377
+ `--setenv=AGENTLINK_HOME=${agentHome}`,
378
+ process.execPath,
379
+ workerPath,
380
+ '--job',
381
+ jobPath,
382
+ ], {
383
+ cwd: path.dirname(jobPath),
384
+ env: environment,
385
+ stdio: 'ignore',
386
+ windowsHide: true,
387
+ timeout: 10_000,
388
+ });
389
+ return;
390
+ }
391
+ if (hostType === 'launchd' && process.platform === 'darwin') {
392
+ const label = `com.agentlink.upgrade.${job.jobId}`;
393
+ const plistPath = path.join(path.dirname(jobPath), 'worker.plist');
394
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
395
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
396
+ <plist version="1.0"><dict>
397
+ <key>Label</key><string>${escapeXml(label)}</string>
398
+ <key>ProgramArguments</key><array>
399
+ <string>${escapeXml(process.execPath)}</string><string>${escapeXml(workerPath)}</string>
400
+ <string>--job</string><string>${escapeXml(jobPath)}</string>
401
+ </array>
402
+ <key>WorkingDirectory</key><string>${escapeXml(path.dirname(jobPath))}</string>
403
+ <key>EnvironmentVariables</key><dict>
404
+ <key>AGENTLINK_HOME</key><string>${escapeXml(agentHome)}</string>
405
+ </dict>
406
+ <key>RunAtLoad</key><true/>
407
+ <key>StandardOutPath</key><string>${escapeXml(logPath)}</string>
408
+ <key>StandardErrorPath</key><string>${escapeXml(logPath)}</string>
409
+ </dict></plist>
410
+ `;
411
+ fs.writeFileSync(plistPath, plist, { encoding: 'utf8', mode: 0o600 });
412
+ fs.chmodSync(plistPath, 0o600);
413
+ const domain = `gui/${process.getuid()}`;
414
+ try {
415
+ childProcess.execFileSync('launchctl', ['bootout', `${domain}/${label}`], {
416
+ cwd: path.dirname(jobPath),
417
+ env: environment,
418
+ stdio: 'ignore',
419
+ windowsHide: true,
420
+ timeout: 5000,
421
+ });
422
+ } catch {
423
+ // A crashed worker may already have been unloaded.
424
+ }
425
+ childProcess.execFileSync('launchctl', ['bootstrap', domain, plistPath], {
426
+ cwd: path.dirname(jobPath),
427
+ env: environment,
428
+ stdio: 'ignore',
429
+ windowsHide: true,
430
+ timeout: 10_000,
431
+ });
432
+ return;
433
+ }
434
+ const output = fs.openSync(logPath, 'a', 0o600);
435
+ try {
436
+ const child = spawn(process.execPath, [workerPath, '--job', jobPath], {
437
+ cwd: path.dirname(jobPath),
438
+ env: environment,
439
+ detached: true,
440
+ stdio: ['ignore', output, output],
441
+ windowsHide: true,
442
+ });
443
+ child.unref();
444
+ } finally {
445
+ fs.closeSync(output);
446
+ }
447
+ }
448
+
449
+ function recoverUpgrade(agentHome) {
450
+ const latestPath = path.join(agentHome, 'upgrades', 'latest.json');
451
+ if (!fs.existsSync(latestPath)) return false;
452
+ let latest;
453
+ try {
454
+ latest = validateLatestReference(loadJson(latestPath));
455
+ } catch (error) {
456
+ process.stderr.write(`[AgentLink Launcher] Upgrade state is corrupt; using last-known-good and repair is required: ${error.message}\n`);
457
+ atomicJson(path.join(agentHome, 'upgrades', 'recovery-error.json'), {
458
+ phase: 'repair-required',
459
+ updatedAt: new Date().toISOString(),
460
+ error: `corrupt latest upgrade state: ${error.message}`,
461
+ });
462
+ forceLastKnownGood = true;
463
+ return false;
464
+ }
465
+ const referencedJobId = latest.jobId;
466
+ const jobPath = path.join(agentHome, 'upgrades', referencedJobId, 'job.json');
467
+ let job;
468
+ try {
469
+ job = validateRecoveryJob(loadJson(jobPath), referencedJobId, agentHome);
470
+ } catch (error) {
471
+ process.stderr.write(`[AgentLink Launcher] Upgrade job is corrupt; using last-known-good and repair is required: ${error.message}\n`);
472
+ atomicJson(path.join(agentHome, 'upgrades', 'recovery-error.json'), {
473
+ phase: 'repair-required',
474
+ jobId: referencedJobId,
475
+ updatedAt: new Date().toISOString(),
476
+ error: `corrupt upgrade job: ${error.message}`,
477
+ });
478
+ forceLastKnownGood = true;
479
+ return false;
480
+ }
481
+ if (hasValidWorkerRecoveryOwner(agentHome, job)) {
482
+ workerOwnedJobId = job.jobId;
483
+ return false;
484
+ }
485
+ if (
486
+ process.env.AGENTLINK_RECOVERY_OWNER === 'repair'
487
+ && process.env.AGENTLINK_UPGRADE_JOB === job.jobId
488
+ && job.phase === 'repair-starting'
489
+ ) {
490
+ repairResumeJobId = job.jobId;
491
+ forceLastKnownGood = true;
492
+ return false;
493
+ }
494
+ if (job.phase === 'repair-required') {
495
+ atomicJson(latestPath, job);
496
+ reconcileTerminalLock(agentHome, job);
497
+ process.stderr.write('[AgentLink Launcher] Upgrade repair is required; run agentlink-client upgrade-repair.\n');
498
+ return true;
499
+ }
500
+ if (['completed', 'rolled-back', 'failed'].includes(job.phase)) {
501
+ atomicJson(latestPath, job);
502
+ return reconcileTerminalLock(agentHome, job);
503
+ }
504
+ if (job.phase === 'repair-starting') {
505
+ for (const identity of [job.repairRuntime, job.repairLauncher]) {
506
+ const state = identityState(identity);
507
+ if (state.status === 'match') return true;
508
+ if (state.status === 'error') {
509
+ throw new Error('cannot determine interrupted repair process identity');
510
+ }
511
+ }
512
+ const launcherIdentity = processIdentity(process.pid);
513
+ if (launcherIdentity.status !== 'found') {
514
+ throw new Error('cannot determine exact repair resume launcher identity');
515
+ }
516
+ job = {
517
+ ...job,
518
+ repairLauncher: {
519
+ pid: process.pid,
520
+ processStartIdentity: launcherIdentity.identity,
521
+ },
522
+ updatedAt: new Date().toISOString(),
523
+ };
524
+ atomicJson(jobPath, job);
525
+ atomicJson(latestPath, job);
526
+ repairResumeJobId = job.jobId;
527
+ forceLastKnownGood = true;
528
+ return false;
529
+ }
530
+ if (['created', 'downloading', 'verifying', 'staging', 'self-checking'].includes(job.phase)) {
531
+ try {
532
+ markInterruptedPreShutdown(agentHome, jobPath, job, `launcher recovered interrupted ${job.phase}`);
533
+ } catch (error) {
534
+ if (error.message === 'active upgrade lock still has a live owner') return true;
535
+ throw error;
536
+ }
537
+ return false;
538
+ }
539
+ if (job.phase === 'worker-starting') {
540
+ const state = identityState(job.worker);
541
+ if (state.status === 'match') return true;
542
+ if (state.status === 'error') throw new Error('cannot determine interrupted worker identity');
543
+ markInterruptedPreShutdown(agentHome, jobPath, job, 'launcher recovered a worker that never became ready');
544
+ return false;
545
+ }
546
+ const workerState = identityState(job.worker);
547
+ if (workerState.status === 'match') return true;
548
+ if (workerState.status === 'error') throw new Error('cannot determine recovery worker identity');
549
+ startRecoveryWorker(agentHome, jobPath, job);
550
+ return true;
551
+ }
552
+
66
553
  const agentHome = path.resolve(process.env.AGENTLINK_HOME || path.join(os.homedir(), '.agentlink'));
67
554
  const versionsRoot = path.join(agentHome, 'versions');
555
+ try {
556
+ if (recoverUpgrade(agentHome)) return;
557
+ } catch (error) {
558
+ process.stderr.write(`[AgentLink Launcher] Upgrade recovery could not start: ${error.message}\n`);
559
+ process.exitCode = 1;
560
+ return;
561
+ }
68
562
  let pointer;
69
563
  try {
70
- pointer = loadPointer(path.join(agentHome, 'current-version.json'), versionsRoot);
564
+ pointer = loadPointer(
565
+ path.join(agentHome, forceLastKnownGood ? 'last-known-good.json' : 'current-version.json'),
566
+ versionsRoot,
567
+ );
71
568
  } catch (currentError) {
72
569
  try {
73
570
  pointer = loadPointer(path.join(agentHome, 'last-known-good.json'), versionsRoot);
@@ -78,14 +575,31 @@ try {
78
575
  }
79
576
  }
80
577
 
81
- const child = spawn(process.execPath, [pointer.entry, ...process.argv.slice(2)], {
578
+ const {
579
+ AGENTLINK_RECOVERY_OWNER: _recoveryOwner,
580
+ AGENTLINK_UPGRADE_JOB: _upgradeJob,
581
+ ...agentEnvironment
582
+ } = process.env;
583
+ const forwardedArgs = process.argv.slice(2);
584
+ const activeUpgradeJobId = repairResumeJobId || workerOwnedJobId;
585
+ if (activeUpgradeJobId && !forwardedArgs.includes('--upgrade-job')) {
586
+ forwardedArgs.push('--upgrade-job', activeUpgradeJobId);
587
+ }
588
+ const child = spawn(process.execPath, [pointer.entry, ...forwardedArgs], {
82
589
  stdio: 'inherit',
83
590
  windowsHide: true,
84
591
  env: {
85
- ...process.env,
592
+ ...agentEnvironment,
86
593
  AGENTLINK_HOME: agentHome,
87
594
  AGENTLINK_VERSION_STORE_ACTIVE: '1',
88
595
  AGENTLINK_SELECTED_VERSION: pointer.version,
596
+ AGENTLINK_SELECTED_INTEGRITY: pointer.integrity,
597
+ ...(activeUpgradeJobId ? {
598
+ AGENTLINK_UPGRADE_JOB: activeUpgradeJobId,
599
+ } : {}),
600
+ ...(repairResumeJobId ? {
601
+ AGENTLINK_RECOVERY_OWNER: 'repair',
602
+ } : {}),
89
603
  },
90
604
  });
91
605
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-link/agent",
3
- "version": "0.1.298",
3
+ "version": "0.1.300",
4
4
  "description": "AgentLink local agent CLI",
5
5
  "license": "MIT",
6
6
  "repository": {