@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.
- package/package.json +1 -1
- package/src/__tests__/config-watcher.test.ts +24 -0
- package/src/__tests__/credential-execution-client.test.ts +5 -5
- package/src/__tests__/plugin-api-resolve-credential.test.ts +97 -0
- package/src/__tests__/skill-script-runner-sandbox.test.ts +32 -0
- package/src/config/schemas/__tests__/tools.test.ts +31 -0
- package/src/config/schemas/mcp.ts +2 -1
- package/src/config/schemas/tools.ts +10 -0
- package/src/config/skills.ts +2 -6
- package/src/credential-execution/executable-discovery.ts +1 -11
- package/src/daemon/config-watcher.ts +10 -0
- package/src/daemon/conversation-skill-tools.ts +1 -0
- package/src/mcp/__tests__/manager-tool-caps.test.ts +33 -0
- package/src/mcp/__tests__/tool-caps.test.ts +28 -1
- package/src/mcp/manager.ts +3 -0
- package/src/mcp/tool-caps.ts +12 -4
- package/src/monitoring/__tests__/control.test.ts +17 -5
- package/src/monitoring/control.ts +15 -5
- package/src/plugin-api/__tests__/plugin-name-env.test.ts +249 -0
- package/src/plugin-api/index.ts +5 -3
- package/src/plugin-api/plugin-name-env.ts +125 -0
- package/src/plugin-api/resolve-credential.ts +79 -20
- package/src/plugins/__tests__/active-plugin-env.test.ts +136 -0
- package/src/plugins/active-plugin-env.ts +51 -0
- package/src/plugins/defaults/memory/__tests__/worker-control.test.ts +19 -6
- package/src/plugins/defaults/memory/worker-control.ts +15 -5
- package/src/routes/control.ts +7 -5
- package/src/schedule/__tests__/worker-control.test.ts +36 -5
- package/src/schedule/worker-control.ts +15 -5
- package/src/skills/clawhub.ts +3 -8
- package/src/skills/managed-store.ts +1 -5
- package/src/tools/host-terminal/host-shell.ts +4 -1
- package/src/tools/skills/sandbox-runner.ts +11 -2
- package/src/tools/skills/skill-script-runner.ts +7 -0
- package/src/tools/skills/skill-tool-factory.ts +4 -1
- package/src/tools/terminal/__tests__/safe-env.test.ts +11 -0
- package/src/tools/terminal/safe-env.ts +4 -0
- package/src/tools/terminal/shell.ts +2 -0
- package/src/util/__tests__/worker-process-command.test.ts +5 -4
- 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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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.
|
|
85
|
-
// letting the error escape a status probe.
|
|
86
|
-
|
|
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.
|
|
398
|
-
*
|
|
399
|
-
*
|
|
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
|
|
414
|
-
* command line
|
|
415
|
-
*
|
|
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: "
|
|
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 =
|
|
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;
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
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
|
-
*
|
|
673
|
+
* this worker.
|
|
637
674
|
*
|
|
638
675
|
* Returns the status observed before signalling, so callers can report
|
|
639
|
-
* whether anything was stopped.
|
|
640
|
-
*
|
|
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(
|
|
643
|
-
|
|
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
|
}
|