@ouro.bot/cli 0.1.0-alpha.264 → 0.1.0-alpha.266
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/changelog.json
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.266",
|
|
6
|
+
"changes": [
|
|
7
|
+
"fix(daemon): vitest guard for production pidfile (~/.ouro-cli/daemon.pids). The pidfile path is hardcoded with no DI seam, so when a test creates a real OuroDaemon instance and calls start(), the daemon's killOrphanProcesses() reads the REAL pidfile, ps-verifies the PIDs, and SIGTERMs the production daemon's PIDs. Verified live: alpha.265 daemon (PID 64988) was killed 93s after startup by `npx vitest run` invoking daemon.start() in 6 different test files. SIGTERM forensics added in alpha.265 captured the death (parentPid=1, parentCommand=/sbin/launchd) but the killer hint was misleading — the real culprit was the production-pidfile leak. Fix: killOrphanProcesses() and writePidfile() now short-circuit under vitest with warn-level nerves events. Tests that need to verify these functions' behavior continue to use the extracted pure helpers (parseOrphanPidsFromPs, filterPidfilePidsToActualOrphans). New unit tests verify the pidfile is unchanged after no-op calls. Same defense-in-depth pattern as the alpha.265 socket-client hardening — production-side state being touched from tests is now physically impossible from a vitest worker."
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"version": "0.1.0-alpha.265",
|
|
12
|
+
"changes": [
|
|
13
|
+
"fix(daemon): bulletproof vitest socket leak + SIGTERM forensics. Hardens the socket-client vitest guard so production daemon socket calls (DEFAULT_DAEMON_SOCKET_PATH = /tmp/ouroboros-daemon.sock) are unconditionally blocked under vitest, regardless of __bypassVitestGuardForTests state. Cross-file leaks via the globalThis bypass flag (process-wide, leaks across concurrent test files in the same vitest worker) can no longer reach the production daemon. Test files that legitimately exercise the real socket-client transport against synthetic test socket paths (/tmp/daemon.sock) continue to work. Background: a daemon outage on 2026-04-08 was traced to a 14-call burst of leaked `inner.wake testagent` errors, signature of vitest test runs hammering the production socket — same pattern as 1,460 historical daemon log entries.",
|
|
14
|
+
"feat(daemon): SIGTERM/SIGINT tombstone forensics. New writeDaemonTombstone path captures process.ppid, parent command via `ps -p <ppid> -o command=`, and a filtered process snapshot (only node/vitest/ouro/kill lines) at signal-driven death time. Adds a killerHint heuristic: launchd parent reparenting suggests `launchctl bootout`/KeepAlive thrash; vitest worker presence suggests test cleanup; pkill/killall presence is an explicit kill. Forensics field is parsed by readDaemonTombstone and included in the daemon.tombstone_written nerves event meta. Also caps recentCrashes at 100 entries (was 12,265 from a March 31 thrash loop).",
|
|
15
|
+
"test(daemon): contract test BYPASS_USE_ALLOWLIST flags any new file calling __bypassVitestGuardForTests outside the two known-good test files (socket-client.test.ts and daemon-cli-defaults.test.ts). Prevents future regressions of the cross-file leak vector."
|
|
16
|
+
]
|
|
17
|
+
},
|
|
4
18
|
{
|
|
5
19
|
"version": "0.1.0-alpha.264",
|
|
6
20
|
"changes": [
|
|
@@ -33,14 +33,19 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.RECENT_CRASHES_MAX = void 0;
|
|
36
37
|
exports.getTombstonePath = getTombstonePath;
|
|
37
38
|
exports.setTombstonePath = setTombstonePath;
|
|
39
|
+
exports.captureDeathForensics = captureDeathForensics;
|
|
38
40
|
exports.writeDaemonTombstone = writeDaemonTombstone;
|
|
39
41
|
exports.readDaemonTombstone = readDaemonTombstone;
|
|
42
|
+
const child_process_1 = require("child_process");
|
|
40
43
|
const fs = __importStar(require("fs"));
|
|
41
44
|
const os = __importStar(require("os"));
|
|
42
45
|
const path = __importStar(require("path"));
|
|
43
46
|
const runtime_1 = require("../../nerves/runtime");
|
|
47
|
+
/** Maximum number of historical crash timestamps to retain in the tombstone. */
|
|
48
|
+
exports.RECENT_CRASHES_MAX = 100;
|
|
44
49
|
let _tombstonePath = null;
|
|
45
50
|
function getTombstonePath() {
|
|
46
51
|
if (!_tombstonePath) {
|
|
@@ -52,7 +57,84 @@ function getTombstonePath() {
|
|
|
52
57
|
function setTombstonePath(p) {
|
|
53
58
|
_tombstonePath = p;
|
|
54
59
|
}
|
|
55
|
-
|
|
60
|
+
/* v8 ignore start -- shells out to ps; covered via injected runPs in unit tests @preserve */
|
|
61
|
+
function defaultRunPs(args) {
|
|
62
|
+
try {
|
|
63
|
+
return (0, child_process_1.execSync)(`ps ${args.join(" ")}`, { encoding: "utf-8", timeout: 2000 });
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/* v8 ignore stop */
|
|
70
|
+
const DEFAULT_FORENSICS_DEPS = {
|
|
71
|
+
/* v8 ignore next -- defensive: process.ppid always exists in node @preserve */
|
|
72
|
+
ppid: () => (typeof process !== "undefined" && typeof process.ppid === "number" ? process.ppid : 0),
|
|
73
|
+
runPs: defaultRunPs,
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Capture forensic data about who is likely to have killed the daemon.
|
|
77
|
+
* Synchronous and best-effort: never throws, always returns a structured
|
|
78
|
+
* record (with nulls if anything failed). Called from inside SIGTERM/SIGINT
|
|
79
|
+
* handlers, so it must be fast and have no async dependencies.
|
|
80
|
+
*/
|
|
81
|
+
function captureDeathForensics(deps = DEFAULT_FORENSICS_DEPS) {
|
|
82
|
+
const parentPid = (() => {
|
|
83
|
+
try {
|
|
84
|
+
const p = deps.ppid();
|
|
85
|
+
return typeof p === "number" && p > 0 ? p : null;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
})();
|
|
91
|
+
let parentCommand = null;
|
|
92
|
+
if (parentPid !== null) {
|
|
93
|
+
const psParent = deps.runPs(["-p", String(parentPid), "-o", "command="]);
|
|
94
|
+
if (psParent !== null) {
|
|
95
|
+
const trimmed = psParent.trim();
|
|
96
|
+
parentCommand = trimmed.length > 0 ? trimmed : null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const processSnapshot = (() => {
|
|
100
|
+
const psAll = deps.runPs(["-eo", "pid,ppid,command"]);
|
|
101
|
+
if (psAll === null)
|
|
102
|
+
return null;
|
|
103
|
+
const lines = psAll.split("\n");
|
|
104
|
+
// Filter for relevant processes only — node, vitest, ouro, kill commands.
|
|
105
|
+
// Keeps the snapshot small and human-scannable.
|
|
106
|
+
const relevant = lines.filter((line) => {
|
|
107
|
+
const lower = line.toLowerCase();
|
|
108
|
+
return (lower.includes("node")
|
|
109
|
+
|| lower.includes("vitest")
|
|
110
|
+
|| lower.includes("ouro")
|
|
111
|
+
|| lower.includes("/kill ")
|
|
112
|
+
|| lower.includes("pkill")
|
|
113
|
+
|| lower.includes("killall"));
|
|
114
|
+
});
|
|
115
|
+
if (relevant.length === 0)
|
|
116
|
+
return null;
|
|
117
|
+
return relevant.join("\n");
|
|
118
|
+
})();
|
|
119
|
+
const killerHint = deriveKillerHint(parentCommand, processSnapshot);
|
|
120
|
+
return { parentPid, parentCommand, processSnapshot, killerHint };
|
|
121
|
+
}
|
|
122
|
+
function deriveKillerHint(parentCommand, snapshot) {
|
|
123
|
+
if (parentCommand !== null && parentCommand.toLowerCase().includes("launchd")) {
|
|
124
|
+
return "process was reparented to launchd — likely killed by launchctl bootout, KeepAlive thrash, or memory pressure";
|
|
125
|
+
}
|
|
126
|
+
if (snapshot !== null) {
|
|
127
|
+
const lower = snapshot.toLowerCase();
|
|
128
|
+
if (lower.includes("vitest")) {
|
|
129
|
+
return "vitest worker is running — possible test cleanup killing detached processes";
|
|
130
|
+
}
|
|
131
|
+
if (lower.includes("pkill") || lower.includes("killall")) {
|
|
132
|
+
return "saw pkill/killall in process list — explicit kill command";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
function writeDaemonTombstone(reason, error, forensicsDeps) {
|
|
56
138
|
const now = new Date().toISOString();
|
|
57
139
|
const filePath = getTombstonePath();
|
|
58
140
|
// Read existing recentCrashes from previous tombstone (best-effort)
|
|
@@ -67,6 +149,17 @@ function writeDaemonTombstone(reason, error) {
|
|
|
67
149
|
catch {
|
|
68
150
|
// No existing tombstone or unreadable — start fresh
|
|
69
151
|
}
|
|
152
|
+
// Append the new crash and cap at the most recent RECENT_CRASHES_MAX entries
|
|
153
|
+
// so the tombstone doesn't grow without bound (we saw it hit 12,265 entries
|
|
154
|
+
// after a March 31 crash-restart thrash loop).
|
|
155
|
+
const recentCrashes = [...existingCrashes, now].slice(-exports.RECENT_CRASHES_MAX);
|
|
156
|
+
// Forensics: only meaningful for signal-driven deaths (sigterm/sigint).
|
|
157
|
+
// For uncaughtException etc. we skip the snapshot to keep tombstone writes
|
|
158
|
+
// fast on the unhappy path.
|
|
159
|
+
const shouldCaptureForensics = reason === "sigterm" || reason === "sigint";
|
|
160
|
+
const forensics = shouldCaptureForensics
|
|
161
|
+
? captureDeathForensics(forensicsDeps)
|
|
162
|
+
: undefined;
|
|
70
163
|
const tombstone = {
|
|
71
164
|
reason,
|
|
72
165
|
message: error?.message ?? reason,
|
|
@@ -74,7 +167,8 @@ function writeDaemonTombstone(reason, error) {
|
|
|
74
167
|
timestamp: now,
|
|
75
168
|
pid: process.pid,
|
|
76
169
|
uptimeSeconds: Math.floor(process.uptime()),
|
|
77
|
-
recentCrashes
|
|
170
|
+
recentCrashes,
|
|
171
|
+
...(forensics ? { forensics } : {}),
|
|
78
172
|
};
|
|
79
173
|
try {
|
|
80
174
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
@@ -88,7 +182,13 @@ function writeDaemonTombstone(reason, error) {
|
|
|
88
182
|
component: "daemon",
|
|
89
183
|
event: "daemon.tombstone_written",
|
|
90
184
|
message: `daemon tombstone written: ${reason}`,
|
|
91
|
-
meta: {
|
|
185
|
+
meta: {
|
|
186
|
+
reason,
|
|
187
|
+
errorMessage: error?.message ?? null,
|
|
188
|
+
filePath,
|
|
189
|
+
parentPid: forensics?.parentPid ?? null,
|
|
190
|
+
killerHint: forensics?.killerHint ?? null,
|
|
191
|
+
},
|
|
92
192
|
});
|
|
93
193
|
}
|
|
94
194
|
function readDaemonTombstone() {
|
|
@@ -105,6 +205,7 @@ function readDaemonTombstone() {
|
|
|
105
205
|
message: "read daemon tombstone",
|
|
106
206
|
meta: { filePath },
|
|
107
207
|
});
|
|
208
|
+
const forensics = parseForensics(parsed.forensics);
|
|
108
209
|
return {
|
|
109
210
|
reason: parsed.reason,
|
|
110
211
|
message: typeof parsed.message === "string" ? parsed.message : String(parsed.reason),
|
|
@@ -115,9 +216,21 @@ function readDaemonTombstone() {
|
|
|
115
216
|
recentCrashes: Array.isArray(parsed.recentCrashes)
|
|
116
217
|
? parsed.recentCrashes.filter((e) => typeof e === "string")
|
|
117
218
|
: [],
|
|
219
|
+
...(forensics ? { forensics } : {}),
|
|
118
220
|
};
|
|
119
221
|
}
|
|
120
222
|
catch {
|
|
121
223
|
return null;
|
|
122
224
|
}
|
|
123
225
|
}
|
|
226
|
+
function parseForensics(value) {
|
|
227
|
+
if (value === null || typeof value !== "object")
|
|
228
|
+
return null;
|
|
229
|
+
const v = value;
|
|
230
|
+
return {
|
|
231
|
+
parentPid: typeof v.parentPid === "number" ? v.parentPid : null,
|
|
232
|
+
parentCommand: typeof v.parentCommand === "string" ? v.parentCommand : null,
|
|
233
|
+
processSnapshot: typeof v.processSnapshot === "string" ? v.processSnapshot : null,
|
|
234
|
+
killerHint: typeof v.killerHint === "string" ? v.killerHint : null,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
@@ -64,6 +64,26 @@ const outlook_types_1 = require("../outlook/outlook-types");
|
|
|
64
64
|
const outlook_read_1 = require("../outlook/outlook-read");
|
|
65
65
|
const outlook_view_1 = require("../outlook/outlook-view");
|
|
66
66
|
const PIDFILE_PATH = path.join(os.homedir(), ".ouro-cli", "daemon.pids");
|
|
67
|
+
/**
|
|
68
|
+
* Defense-in-depth: detect if we're running under vitest. The pidfile lives
|
|
69
|
+
* at a hardcoded path under the user's real ~/.ouro-cli/ — there's no DI
|
|
70
|
+
* seam to redirect it. So when a test creates a real OuroDaemon and calls
|
|
71
|
+
* start(), the daemon's killOrphanProcesses() reads the REAL pidfile,
|
|
72
|
+
* ps-verifies the PIDs, and SIGTERMs the production daemon. We saw this
|
|
73
|
+
* cause an outage on 2026-04-08 (alpha.265 daemon killed 93s after startup
|
|
74
|
+
* by a vitest test that called daemon.start()).
|
|
75
|
+
*
|
|
76
|
+
* Both killOrphanProcesses() and writePidfile() short-circuit under vitest
|
|
77
|
+
* to make the production pidfile sacred. Tests that need to verify these
|
|
78
|
+
* functions' behavior should use the extracted pure helpers
|
|
79
|
+
* (parseOrphanPidsFromPs, filterPidfilePidsToActualOrphans).
|
|
80
|
+
*/
|
|
81
|
+
function isVitestProcess() {
|
|
82
|
+
/* v8 ignore next -- defensive: process and process.argv always exist in node @preserve */
|
|
83
|
+
if (typeof process === "undefined" || !Array.isArray(process.argv))
|
|
84
|
+
return false;
|
|
85
|
+
return process.argv.some((arg) => typeof arg === "string" && arg.includes("vitest"));
|
|
86
|
+
}
|
|
67
87
|
/**
|
|
68
88
|
* Scan `ps -eo pid,ppid,command` output for daemon-owned entry points whose
|
|
69
89
|
* parent has died (PPID reparented to init/PID 1). Returns the list of PIDs
|
|
@@ -170,6 +190,16 @@ function runPsCheck(pids) {
|
|
|
170
190
|
*/
|
|
171
191
|
/* v8 ignore start -- process lifecycle: uses kill/ps, tested via deployment @preserve */
|
|
172
192
|
function killOrphanProcesses() {
|
|
193
|
+
if (isVitestProcess()) {
|
|
194
|
+
(0, runtime_1.emitNervesEvent)({
|
|
195
|
+
level: "warn",
|
|
196
|
+
component: "daemon",
|
|
197
|
+
event: "daemon.orphan_cleanup_test_blocked",
|
|
198
|
+
message: "blocked killOrphanProcesses from touching real pidfile under vitest",
|
|
199
|
+
meta: { pidfilePath: PIDFILE_PATH },
|
|
200
|
+
});
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
173
203
|
try {
|
|
174
204
|
let pidsToKill = [];
|
|
175
205
|
// Primary: read pidfile from previous daemon
|
|
@@ -223,6 +253,16 @@ function killOrphanProcesses() {
|
|
|
223
253
|
* Called after all agents and senses are spawned.
|
|
224
254
|
*/
|
|
225
255
|
function writePidfile(extraPids = []) {
|
|
256
|
+
if (isVitestProcess()) {
|
|
257
|
+
(0, runtime_1.emitNervesEvent)({
|
|
258
|
+
level: "warn",
|
|
259
|
+
component: "daemon",
|
|
260
|
+
event: "daemon.write_pidfile_test_blocked",
|
|
261
|
+
message: "blocked writePidfile from clobbering real pidfile under vitest",
|
|
262
|
+
meta: { pidfilePath: PIDFILE_PATH, attemptedPids: extraPids.length },
|
|
263
|
+
});
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
226
266
|
try {
|
|
227
267
|
const pids = [process.pid, ...extraPids].filter(Boolean);
|
|
228
268
|
fs.mkdirSync(path.dirname(PIDFILE_PATH), { recursive: true });
|
|
@@ -60,6 +60,23 @@ exports.DEFAULT_DAEMON_SOCKET_PATH = "/tmp/ouroboros-daemon.sock";
|
|
|
60
60
|
* test socket) call `__bypassVitestGuardForTests(true)` to opt out of the
|
|
61
61
|
* guard. We persist that flag on globalThis so it survives `vi.resetModules`
|
|
62
62
|
* (which clears the module cache between tests).
|
|
63
|
+
*
|
|
64
|
+
* HARDENING (cross-file leak protection): the bypass flag lives on globalThis
|
|
65
|
+
* and is process-wide. With vitest's default `fileParallelism: true`,
|
|
66
|
+
* concurrently-running test files in the same worker can have the bypass on
|
|
67
|
+
* even though they didn't ask for it — and any test that uses
|
|
68
|
+
* `name: "testagent"` and exercises a code path through this module will
|
|
69
|
+
* leak `inner.wake testagent` to the production daemon socket. We saw this
|
|
70
|
+
* cause a real outage on 2026-04-08 (daemon SIGTERMed mid-validation).
|
|
71
|
+
*
|
|
72
|
+
* Defense: even when the bypass flag is set, we ALWAYS hard-block calls that
|
|
73
|
+
* target the production daemon socket path. Test files that legitimately
|
|
74
|
+
* exercise the real socket-client code paths use a synthetic socket path like
|
|
75
|
+
* `/tmp/daemon.sock` or `/tmp/some-real-daemon.sock` — those keep working.
|
|
76
|
+
* Calls to `/tmp/ouroboros-daemon.sock` (DEFAULT_DAEMON_SOCKET_PATH) under
|
|
77
|
+
* vitest are unconditionally blocked, regardless of bypass state. This makes
|
|
78
|
+
* the cross-file leak vector impossible without restricting legitimate
|
|
79
|
+
* socket-client unit tests.
|
|
63
80
|
*/
|
|
64
81
|
const BYPASS_KEY = "__ouro_socket_client_bypass_vitest_guard__";
|
|
65
82
|
/** ONLY for socket-client.test.ts. Do not call from any other test or production code. */
|
|
@@ -67,22 +84,36 @@ function __bypassVitestGuardForTests(bypass) {
|
|
|
67
84
|
;
|
|
68
85
|
globalThis[BYPASS_KEY] = bypass;
|
|
69
86
|
}
|
|
70
|
-
function
|
|
71
|
-
if (globalThis[BYPASS_KEY] === true)
|
|
72
|
-
return false;
|
|
87
|
+
function isVitestProcess() {
|
|
73
88
|
/* v8 ignore next -- defensive: process and process.argv always exist in node @preserve */
|
|
74
89
|
if (typeof process === "undefined" || !Array.isArray(process.argv))
|
|
75
90
|
return false;
|
|
76
91
|
return process.argv.some((arg) => typeof arg === "string" && arg.includes("vitest"));
|
|
77
92
|
}
|
|
93
|
+
function isProductionDaemonSocket(socketPath) {
|
|
94
|
+
return socketPath === exports.DEFAULT_DAEMON_SOCKET_PATH;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Returns true when this socket call should be converted into a safe no-op
|
|
98
|
+
* because we're under vitest and the call would otherwise leak into a real
|
|
99
|
+
* daemon. The bypass flag short-circuits the guard for non-production socket
|
|
100
|
+
* paths only — production socket paths are ALWAYS blocked under vitest.
|
|
101
|
+
*/
|
|
102
|
+
function shouldSuppressSocketCall(socketPath) {
|
|
103
|
+
if (!isVitestProcess())
|
|
104
|
+
return false;
|
|
105
|
+
if (isProductionDaemonSocket(socketPath))
|
|
106
|
+
return true;
|
|
107
|
+
return globalThis[BYPASS_KEY] !== true;
|
|
108
|
+
}
|
|
78
109
|
function sendDaemonCommand(socketPath, command) {
|
|
79
|
-
if (
|
|
110
|
+
if (shouldSuppressSocketCall(socketPath)) {
|
|
80
111
|
(0, runtime_1.emitNervesEvent)({
|
|
81
112
|
level: "warn",
|
|
82
113
|
component: "daemon",
|
|
83
114
|
event: "daemon.socket_command_test_blocked",
|
|
84
115
|
message: "blocked socket command from leaking into real daemon under vitest",
|
|
85
|
-
meta: { socketPath, kind: command.kind },
|
|
116
|
+
meta: { socketPath, kind: command.kind, isProductionSocket: isProductionDaemonSocket(socketPath) },
|
|
86
117
|
});
|
|
87
118
|
return Promise.resolve({ ok: true, message: "test mode: socket call suppressed" });
|
|
88
119
|
}
|
|
@@ -191,7 +222,7 @@ function sendDaemonCommand(socketPath, command) {
|
|
|
191
222
|
});
|
|
192
223
|
}
|
|
193
224
|
function checkDaemonSocketAlive(socketPath) {
|
|
194
|
-
if (
|
|
225
|
+
if (shouldSuppressSocketCall(socketPath)) {
|
|
195
226
|
return Promise.resolve(false);
|
|
196
227
|
}
|
|
197
228
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -248,13 +279,13 @@ function checkDaemonSocketAlive(socketPath) {
|
|
|
248
279
|
});
|
|
249
280
|
}
|
|
250
281
|
async function requestInnerWake(agent, socketPath = exports.DEFAULT_DAEMON_SOCKET_PATH) {
|
|
251
|
-
if (
|
|
282
|
+
if (shouldSuppressSocketCall(socketPath)) {
|
|
252
283
|
(0, runtime_1.emitNervesEvent)({
|
|
253
284
|
level: "warn",
|
|
254
285
|
component: "daemon",
|
|
255
286
|
event: "daemon.inner_wake_test_blocked",
|
|
256
287
|
message: "blocked inner wake from leaking into real daemon under vitest",
|
|
257
|
-
meta: { agent, socketPath },
|
|
288
|
+
meta: { agent, socketPath, isProductionSocket: isProductionDaemonSocket(socketPath) },
|
|
258
289
|
});
|
|
259
290
|
return null;
|
|
260
291
|
}
|