@vellumai/assistant 0.12.2-dev.202609171225.565e464 → 0.12.2-dev.202609171516.2acdfd4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/src/__tests__/config-watcher.test.ts +24 -0
  3. package/src/__tests__/credential-execution-client.test.ts +5 -5
  4. package/src/__tests__/plugin-api-resolve-credential.test.ts +97 -0
  5. package/src/__tests__/skill-script-runner-sandbox.test.ts +32 -0
  6. package/src/config/schemas/__tests__/tools.test.ts +31 -0
  7. package/src/config/schemas/mcp.ts +2 -1
  8. package/src/config/schemas/tools.ts +10 -0
  9. package/src/config/skills.ts +2 -6
  10. package/src/credential-execution/executable-discovery.ts +1 -11
  11. package/src/daemon/config-watcher.ts +10 -0
  12. package/src/daemon/conversation-skill-tools.ts +1 -0
  13. package/src/mcp/__tests__/manager-tool-caps.test.ts +33 -0
  14. package/src/mcp/__tests__/tool-caps.test.ts +28 -1
  15. package/src/mcp/manager.ts +3 -0
  16. package/src/mcp/tool-caps.ts +12 -4
  17. package/src/monitoring/__tests__/control.test.ts +17 -5
  18. package/src/monitoring/control.ts +15 -5
  19. package/src/plugin-api/__tests__/plugin-name-env.test.ts +249 -0
  20. package/src/plugin-api/index.ts +5 -3
  21. package/src/plugin-api/plugin-name-env.ts +125 -0
  22. package/src/plugin-api/resolve-credential.ts +79 -20
  23. package/src/plugins/__tests__/active-plugin-env.test.ts +136 -0
  24. package/src/plugins/active-plugin-env.ts +51 -0
  25. package/src/plugins/defaults/memory/__tests__/worker-control.test.ts +19 -6
  26. package/src/plugins/defaults/memory/worker-control.ts +15 -5
  27. package/src/routes/control.ts +7 -5
  28. package/src/schedule/__tests__/worker-control.test.ts +36 -5
  29. package/src/schedule/worker-control.ts +15 -5
  30. package/src/skills/clawhub.ts +3 -8
  31. package/src/skills/managed-store.ts +1 -5
  32. package/src/tools/host-terminal/host-shell.ts +4 -1
  33. package/src/tools/skills/sandbox-runner.ts +11 -2
  34. package/src/tools/skills/skill-script-runner.ts +7 -0
  35. package/src/tools/skills/skill-tool-factory.ts +4 -1
  36. package/src/tools/terminal/__tests__/safe-env.test.ts +11 -0
  37. package/src/tools/terminal/safe-env.ts +4 -0
  38. package/src/tools/terminal/shell.ts +2 -0
  39. package/src/util/__tests__/worker-process-command.test.ts +5 -4
  40. package/src/util/worker-process.ts +77 -35
@@ -50,13 +50,11 @@ function isEsrchError(err: unknown): boolean {
50
50
  );
51
51
  }
52
52
 
53
- /**
54
- * Read a PID file and report liveness. A missing or malformed file reports
55
- * not_running; a file pointing at a dead process is cleaned up and reported as
56
- * not_running. Intended for worker-process PID files whose PID is a normal
57
- * spawned child (never PID 1), so `process.kill(pid, 0)` liveness is reliable.
58
- */
59
- export function probeWorkerPidFile(path: string): WorkerProcessStatus {
53
+ /** Kill-0 plus command-line identity for a PID file. */
54
+ function probePidFile(
55
+ path: string,
56
+ signature: readonly string[],
57
+ ): WorkerProcessStatus {
60
58
  if (!existsSync(path)) {
61
59
  return { status: "not_running" };
62
60
  }
@@ -69,22 +67,49 @@ export function probeWorkerPidFile(path: string): WorkerProcessStatus {
69
67
 
70
68
  try {
71
69
  process.kill(pid, 0);
72
- return { status: "running", pid };
73
70
  } catch (err: unknown) {
74
71
  if (isEsrchError(err)) {
75
- // Stale file — clean it up.
76
- try {
77
- unlinkSync(path);
78
- } catch {
79
- // best-effort
80
- }
72
+ unlinkPidFileIfNames(path, pid);
81
73
  return { status: "not_running" };
82
74
  }
83
75
  // Any other error (e.g. EPERM: the process exists but this caller may not
84
- // signal it) means the process is alive. Report it running rather than
85
- // letting the error escape a status probe.
86
- return { status: "running", pid };
76
+ // signal it) means the process is alive. Fall through to the identity
77
+ // check rather than letting the error escape a status probe.
78
+ }
79
+
80
+ const fate = classifyOrphanAfterWait(
81
+ true,
82
+ readRawProcessCommand(pid),
83
+ signature,
84
+ );
85
+ if (fate === "gone") {
86
+ log.info(
87
+ { pid, pidPath: path },
88
+ "Worker PID file names a live process this runtime does not recognise as its worker; releasing the slot rather than treating it as running",
89
+ );
90
+ unlinkPidFileIfNames(path, pid);
91
+ return { status: "not_running" };
87
92
  }
93
+ return { status: "running", pid };
94
+ }
95
+
96
+ /**
97
+ * Read a PID file and report whether this worker is actually running there.
98
+ *
99
+ * Identity comes from the same `entry` / `packagedEntry` spawn uses. A missing
100
+ * or malformed file reports not_running. A file pointing at a dead process, or
101
+ * at a live process whose command line is not this worker (a recycled PID
102
+ * after a container restart), is cleaned up and reported as not_running. A
103
+ * live PID whose command line cannot be read is reported running: uncertainty
104
+ * must not start a second worker next to a maybe-live one, and must not be
105
+ * treated as a license to signal a stranger.
106
+ */
107
+ export function probeWorkerPidFile(
108
+ pidPath: string,
109
+ entry: URL,
110
+ packagedEntry?: PackagedWorkerEntry,
111
+ ): WorkerProcessStatus {
112
+ return probePidFile(pidPath, workerKindSignature(entry, packagedEntry));
88
113
  }
89
114
 
90
115
  /** Thrown when a worker process fails to come up within the wait window. */
@@ -394,11 +419,13 @@ async function stopOrphanedWorker(
394
419
  /**
395
420
  * What to do about the process a worker's PID file currently names.
396
421
  *
397
- * - `adopt`: reuse it. Either it is this process's own worker, or it belongs
398
- * to another live owner, or it is not recognisably one of our workers at
399
- * all and must never be signalled.
422
+ * - `adopt`: reuse it. It is this process's own worker, it belongs to
423
+ * another live owner, or its identity could not be read so a second
424
+ * worker must not be started next to a maybe-live one.
400
425
  * - `reclaim`: an orphan left by an owner that is gone. Stop it, then spawn.
401
- * - `spawn`: nothing is holding the slot.
426
+ * - `spawn`: nothing is holding the slot, including a live PID whose
427
+ * command line is not this worker (a recycled PID). Never signal that
428
+ * process.
402
429
  */
403
430
  export type WorkerSlotDecision =
404
431
  | { action: "adopt"; pid: number }
@@ -410,9 +437,12 @@ export type WorkerSlotDecision =
410
437
  *
411
438
  * `reclaim` is the only outcome that signals a process, and it requires two
412
439
  * independent things to agree: the command line marks the process as this
413
- * worker, and parentage says no daemon owns it. A missing row, an unreadable
414
- * command line, and a command line that does not match all fall back to
415
- * `adopt`, so an uncertain answer never costs a process its life.
440
+ * worker, and parentage says no daemon owns it. A missing row and an
441
+ * unreadable command line fall back to `adopt`, so an uncertain answer never
442
+ * costs a process its life and never starts a duplicate. A command line that
443
+ * does not match is `spawn` without a signal: the PID file is stale, and
444
+ * treating that PID as the worker would leave the slot occupied by a
445
+ * stranger forever.
416
446
  *
417
447
  * `isOwnerAlive` is the caller's definition of a legitimate owner. These
418
448
  * workers have exactly one, the daemon, so passing a plain liveness probe
@@ -435,7 +465,7 @@ export function decideWorkerSlot(
435
465
  return { action: "adopt", pid: status.pid };
436
466
  }
437
467
  if (!matchesSignature(row.command, signature)) {
438
- return { action: "adopt", pid: status.pid };
468
+ return { action: "spawn" };
439
469
  }
440
470
  const ownership = classifyWorkerOwnership(
441
471
  row,
@@ -458,7 +488,7 @@ function inspectWorkerSlot(
458
488
  pidPath: string,
459
489
  signature: readonly string[],
460
490
  ): WorkerSlotDecision {
461
- const status = probeWorkerPidFile(pidPath);
491
+ const status = probePidFile(pidPath, signature);
462
492
  if (status.status !== "running" || status.pid == null) {
463
493
  return { action: "spawn" };
464
494
  }
@@ -479,11 +509,11 @@ function inspectWorkerSlot(
479
509
  if (row && !matchesSignature(row.command, signature)) {
480
510
  log.info(
481
511
  { pid: row.pid, pidPath },
482
- "Worker PID file names a live process this runtime does not recognise as its worker; reusing it rather than signalling it",
512
+ "Worker PID file names a live process this runtime does not recognise as its worker; releasing the slot rather than treating it as running",
483
513
  );
484
514
  }
485
515
 
486
- return decideWorkerSlot(
516
+ const decision = decideWorkerSlot(
487
517
  status,
488
518
  row,
489
519
  signature,
@@ -491,6 +521,12 @@ function inspectWorkerSlot(
491
521
  (pid) => isDaemonCommand(commandOf(pid)),
492
522
  pid1OwnsWorkers(commandOf(1)),
493
523
  );
524
+ // A spawn decision while the PID file still names a live stranger would
525
+ // make waitForWorkerPidFile treat that stale file as readiness.
526
+ if (decision.action === "spawn") {
527
+ unlinkPidFileIfNames(pidPath, status.pid);
528
+ }
529
+ return decision;
494
530
  }
495
531
 
496
532
  /**
@@ -508,7 +544,7 @@ async function reclaimWorkerSlot(
508
544
  // Still running and beyond our reach. One stale worker beats two live ones.
509
545
  return pid;
510
546
  }
511
- const replacement = probeWorkerPidFile(pidPath);
547
+ const replacement = probePidFile(pidPath, signature);
512
548
  return replacement.status === "running" && replacement.pid != null
513
549
  ? replacement.pid
514
550
  : null;
@@ -519,7 +555,8 @@ async function reclaimWorkerSlot(
519
555
  * process, and wait for it to report readiness by writing its PID file. The
520
556
  * child is `unref`'d, so the spawning process never blocks on it.
521
557
  *
522
- * If a worker is already running (per the PID file), returns its PID with
558
+ * If a worker is already running (the PID file names a live process whose
559
+ * command line matches this worker), returns its PID with
523
560
  * `alreadyRunning: true` rather than spawning a second one. Throws
524
561
  * {@link WorkerProcessSpawnError} if the child crashes during startup or
525
562
  * never writes its PID file within the wait window.
@@ -633,14 +670,19 @@ export async function spawnWorkerProcess(args: {
633
670
 
634
671
  /**
635
672
  * Send SIGTERM to the worker process behind `pidPath` if it is actually
636
- * running.
673
+ * this worker.
637
674
  *
638
675
  * Returns the status observed before signalling, so callers can report
639
- * whether anything was stopped. Only throws if `process.kill` itself fails
640
- * (e.g. EPERM) — a not-running worker is a no-op.
676
+ * whether anything was stopped. A PID file that names a dead process or a
677
+ * live process that is not this worker is a no-op. Only throws if
678
+ * `process.kill` itself fails (e.g. EPERM).
641
679
  */
642
- export function stopWorkerProcess(pidPath: string): WorkerProcessStatus {
643
- const current = probeWorkerPidFile(pidPath);
680
+ export function stopWorkerProcess(
681
+ pidPath: string,
682
+ entry: URL,
683
+ packagedEntry?: PackagedWorkerEntry,
684
+ ): WorkerProcessStatus {
685
+ const current = probeWorkerPidFile(pidPath, entry, packagedEntry);
644
686
  if (current.status === "running" && current.pid != null) {
645
687
  process.kill(current.pid, "SIGTERM");
646
688
  }