@zq-silk/yui 1.0.0 → 1.1.0
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/README.md +27 -9
- package/dist/cli/commandCatalog.js +7 -5
- package/dist/cli/invocationAuthority.js +4 -0
- package/dist/cli.js +130 -5
- package/dist/commands/releaseCommands.js +33 -24
- package/dist/commands/taskCommands.js +18 -28
- package/dist/commands/taskFactCommands.js +6 -2
- package/dist/controller/clientRuntime.js +7 -2
- package/dist/controller/domainIdentity.js +7 -7
- package/dist/controller/handoverCandidate.js +2 -5
- package/dist/controller/jobSupervisor.js +4 -4
- package/dist/controller/resourceCleanupLinux.js +35 -19
- package/dist/controller/resourceInventoryLinux.js +104 -7
- package/dist/controller/runtime.js +4 -13
- package/dist/controller/runtimeLaunchCoordinator.js +2 -4
- package/dist/controller/sessionOwnerReconciliation.js +40 -16
- package/dist/core/controllerClient.js +44 -16
- package/dist/core/controllerEndpoint.js +4 -3
- package/dist/core/controllerProcessIdentity.js +54 -0
- package/dist/core/controllerServer.js +16 -11
- package/dist/core/fileLockOwner.js +53 -12
- package/dist/doctor/doctor.js +230 -12
- package/dist/execution/workItemMainRun.js +1 -1
- package/dist/executor/agentAdapter.js +0 -8
- package/dist/executor/agentExecutor.js +4 -1
- package/dist/executor/fileRoleLaunchPlanner.js +15 -25
- package/dist/message/messageContinuation.js +1 -1
- package/dist/release/releaseHandover.js +77 -25
- package/dist/release/runtimeRelease.js +7 -7
- package/dist/repository/gitWorkspace.js +9 -8
- package/dist/repository/taskWorkspacePreparer.js +15 -57
- package/dist/resources/liveReferences.js +64 -7
- package/dist/runtime/agentEndpointIdentity.js +4 -1
- package/dist/runtime/agentHost.js +13 -9
- package/dist/runtime/codexInteractiveHost.js +1 -1
- package/dist/runtime/codexSharedDaemon.js +211 -0
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/jsonLineChannel.js +6 -1
- package/dist/runtime/localStartup.js +65 -0
- package/dist/runtime/native/darwin-arm64/claude-process-owner +0 -0
- package/dist/runtime/native/darwin-arm64/process-identity +0 -0
- package/dist/runtime/native/darwin-x64/claude-process-owner +0 -0
- package/dist/runtime/native/darwin-x64/process-identity +0 -0
- package/dist/runtime/{claude-process-owner → native/linux-x64/claude-process-owner} +0 -0
- package/dist/runtime/nativeExecutable.js +10 -0
- package/dist/runtime/runtimeCoherence.js +10 -3
- package/dist/runtime/sessionOwnerIdentity.js +19 -0
- package/dist/runtime/sessionTerminationGuard.js +10 -3
- package/dist/runtime/sessionTitle.js +15 -0
- package/dist/runtime/structuredProviderHost.js +94 -29
- package/dist/scheduler/activeRoleRunDelivery.js +2 -11
- package/dist/scheduler/leaderWakeupProcessor.js +1 -2
- package/dist/scheduler/ports.js +1 -8
- package/dist/scheduler/taskExecutionProjection.js +1 -6
- package/dist/storage/sqliteSchema.js +15 -3
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/taskCatalog.js +10 -2
- package/dist/storage/upgrade/taskMainWorkspaceMigration.js +83 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +25 -6
- package/dist/task/task.js +0 -12
- package/dist/tmux/tmuxManager.js +8 -2
- package/dist/tmux/tmuxSocketEndpoint.js +23 -1
- package/dist/web/assets/client/app.js +131 -11
- package/dist/web/assets/client/i18n.js +18 -0
- package/dist/web/assets/client/taskSummary.js +7 -2
- package/dist/web/assets/client/taskSurface.js +54 -5
- package/dist/web/assets/client/view.js +7 -2
- package/dist/web/assets/shell.js +21 -11
- package/dist/web/assets/styles/layout.js +33 -6
- package/dist/web/assets/styles/responsive.js +18 -5
- package/dist/web/assets/styles/tokens.js +1 -1
- package/dist/web/assets/styles/widgets.js +5 -0
- package/docs/managed-turn-and-session-runtime.md +7 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +5 -0
- package/docs/provider-runtime.md +7 -0
- package/docs/provider-runtime.zh-CN.md +5 -0
- package/docs/release-workflow.md +7 -1
- package/docs/task-delivery.md +5 -4
- package/docs/testing/verification-levels.md +9 -7
- package/i18n/README.zh-CN.md +19 -6
- package/package.json +5 -6
- package/skills/yui-leader/SKILL.md +8 -0
- package/skills/yui-leader/references/execution.md +4 -1
- package/skills/yui-operator/SKILL.md +35 -15
|
@@ -5,9 +5,10 @@ export function controllerSocketPath(homeId) {
|
|
|
5
5
|
const identity = validateHomeId(homeId);
|
|
6
6
|
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
7
7
|
const socketName = `${identity}.sock`;
|
|
8
|
-
//
|
|
9
|
-
// root is
|
|
10
|
-
|
|
8
|
+
// Unix-socket paths have a small fixed budget (104 bytes on macOS, 108 on
|
|
9
|
+
// Linux). A stable per-uid system root is short and independent of each
|
|
10
|
+
// caller's isolated TMPDIR, which is a long /var/folders path on macOS.
|
|
11
|
+
const root = process.platform === "win32" ? tmpdir() : "/tmp";
|
|
11
12
|
return join(root, `yui-${uid}`, socketName);
|
|
12
13
|
}
|
|
13
14
|
/**
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { nativeExecutable } from "../runtime/nativeExecutable.js";
|
|
2
4
|
import { readLinuxProcessStartIdentity } from "../controller/domainIdentity.js";
|
|
3
5
|
import { readHomeFilesystemId, validateHomeFilesystemId } from "./homeFilesystemIdentity.js";
|
|
4
6
|
/** Exact live-process fence used at Controller startup and handover. */
|
|
@@ -6,6 +8,22 @@ export function inspectLiveControllerProcess(pid, homeFilesystemId, expectedProc
|
|
|
6
8
|
if (!Number.isSafeInteger(pid) || pid < 1 || pid === process.pid)
|
|
7
9
|
return undefined;
|
|
8
10
|
const expectedFilesystemId = validateHomeFilesystemId(homeFilesystemId);
|
|
11
|
+
if (process.platform === "darwin") {
|
|
12
|
+
const candidate = inspectDarwinController(pid);
|
|
13
|
+
if (candidate === undefined)
|
|
14
|
+
return undefined;
|
|
15
|
+
try {
|
|
16
|
+
if (readHomeFilesystemId(candidate.home) !== expectedFilesystemId)
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
if (expectedProcessStartIdentity !== undefined
|
|
23
|
+
&& candidate.processStartIdentity !== expectedProcessStartIdentity)
|
|
24
|
+
return undefined;
|
|
25
|
+
return Object.freeze({ pid, processStartIdentity: candidate.processStartIdentity });
|
|
26
|
+
}
|
|
9
27
|
try {
|
|
10
28
|
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
11
29
|
const status = readFileSync(`/proc/${pid}/status`, "utf8");
|
|
@@ -42,6 +60,20 @@ export function inspectLiveControllerProcess(pid, homeFilesystemId, expectedProc
|
|
|
42
60
|
/** Finds a Controller whose discovery record was lost. */
|
|
43
61
|
export function findLiveControllerProcessForHome(homeFilesystemId) {
|
|
44
62
|
validateHomeFilesystemId(homeFilesystemId);
|
|
63
|
+
if (process.platform === "darwin") {
|
|
64
|
+
const output = runDarwinIdentityHelper(["--controllers"]);
|
|
65
|
+
if (output === undefined)
|
|
66
|
+
return undefined;
|
|
67
|
+
for (const line of output.trim().split("\n")) {
|
|
68
|
+
const candidate = parseDarwinController(line);
|
|
69
|
+
if (candidate === undefined)
|
|
70
|
+
continue;
|
|
71
|
+
const match = inspectLiveControllerProcess(candidate.pid, homeFilesystemId, candidate.processStartIdentity);
|
|
72
|
+
if (match !== undefined)
|
|
73
|
+
return match;
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
45
77
|
if (process.platform !== "linux")
|
|
46
78
|
return undefined;
|
|
47
79
|
const entries = readdirSync("/proc", { encoding: "utf8" });
|
|
@@ -54,3 +86,25 @@ export function findLiveControllerProcessForHome(homeFilesystemId) {
|
|
|
54
86
|
}
|
|
55
87
|
return undefined;
|
|
56
88
|
}
|
|
89
|
+
function inspectDarwinController(pid) {
|
|
90
|
+
const output = runDarwinIdentityHelper(["--controller", String(pid)]);
|
|
91
|
+
return output === undefined ? undefined : parseDarwinController(output.trim());
|
|
92
|
+
}
|
|
93
|
+
function parseDarwinController(line) {
|
|
94
|
+
const match = /^([1-9][0-9]*)\t([1-9][0-9]{0,31})\t([^\t\n]+)$/u.exec(line);
|
|
95
|
+
if (match === null)
|
|
96
|
+
return undefined;
|
|
97
|
+
const pid = Number(match[1]);
|
|
98
|
+
if (!Number.isSafeInteger(pid))
|
|
99
|
+
return undefined;
|
|
100
|
+
return { pid, processStartIdentity: match[2], home: match[3] };
|
|
101
|
+
}
|
|
102
|
+
function runDarwinIdentityHelper(args) {
|
|
103
|
+
try {
|
|
104
|
+
return execFileSync(nativeExecutable("process-identity"), [...args], { encoding: "utf8", timeout: 2000, maxBuffer: 1024 * 1024,
|
|
105
|
+
stdio: ["ignore", "pipe", "ignore"] });
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -8,6 +8,7 @@ import { CONTROLLER_DISCOVERY_PATH, FILE_TASK_CONTROLLER_PROTOCOL_VERSION, MAX_C
|
|
|
8
8
|
import { controllerSocketPath } from "./controllerEndpoint.js";
|
|
9
9
|
import { readHomeFilesystemId } from "./homeFilesystemIdentity.js";
|
|
10
10
|
import { findLiveControllerProcessForHome } from "./controllerProcessIdentity.js";
|
|
11
|
+
import { currentProcessStartIdentity, processGenerationIsLive } from "./fileLockOwner.js";
|
|
11
12
|
import { validateHomeId } from "../repository/homeIdentity.js";
|
|
12
13
|
import { readCurrentHomeIdentity } from "../storage/currentTaskStore.js";
|
|
13
14
|
import { YUI_VERSION, yuiVersionIdentity } from "../version.js";
|
|
@@ -98,7 +99,9 @@ async function startControllerServerLocked(home, dispatcher, beforeDiscoveryRemo
|
|
|
98
99
|
eventLoopDelay.start();
|
|
99
100
|
try {
|
|
100
101
|
await chmod(socketPath, 0o600);
|
|
101
|
-
const processStartIdentity =
|
|
102
|
+
const processStartIdentity = process.platform === "linux"
|
|
103
|
+
? await readLinuxProcessStartIdentity(process.pid)
|
|
104
|
+
: currentProcessStartIdentity();
|
|
102
105
|
const discovery = Object.freeze({
|
|
103
106
|
schemaVersion: 1,
|
|
104
107
|
protocolVersion: FILE_TASK_CONTROLLER_PROTOCOL_VERSION,
|
|
@@ -202,6 +205,14 @@ async function assertExistingDiscoveryReplaceable(discoveryPath) {
|
|
|
202
205
|
|| !/^[0-9]{1,32}$/u.test(value.processStartIdentity)) {
|
|
203
206
|
throw unsafeExistingDiscovery(discoveryPath);
|
|
204
207
|
}
|
|
208
|
+
if (process.platform !== "linux") {
|
|
209
|
+
// The native helper proves the exact macOS generation; a live PID with
|
|
210
|
+
// unreadable identity cannot authorize discovery replacement.
|
|
211
|
+
if (processGenerationIsLive(pid, value.processStartIdentity)) {
|
|
212
|
+
throw controllerAlreadyRunning();
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
205
216
|
let currentIdentity;
|
|
206
217
|
try {
|
|
207
218
|
currentIdentity = await readLinuxProcessStartIdentity(pid);
|
|
@@ -340,15 +351,7 @@ async function routeRequest(socket, line, token, dispatcher, stop, status, telem
|
|
|
340
351
|
releaseDrifted = true;
|
|
341
352
|
}
|
|
342
353
|
}
|
|
343
|
-
|
|
344
|
-
try {
|
|
345
|
-
processStartIdentity = readLinuxProcessStartIdentitySync(process.pid);
|
|
346
|
-
}
|
|
347
|
-
catch {
|
|
348
|
-
// Non-Linux or an unreadable /proc: the owner cannot be fenced, so the
|
|
349
|
-
// handover path treats it as absent. The rest of the identity stays
|
|
350
|
-
// available for read-only consumers.
|
|
351
|
-
}
|
|
354
|
+
const processStartIdentity = currentProcessStartIdentity();
|
|
352
355
|
send({
|
|
353
356
|
id: request.id,
|
|
354
357
|
ok: true,
|
|
@@ -621,7 +624,9 @@ function currentHandoverOwner() {
|
|
|
621
624
|
const release = detectRunningRelease(fileURLToPath(import.meta.url));
|
|
622
625
|
return Object.freeze({
|
|
623
626
|
pid: process.pid,
|
|
624
|
-
processStartIdentity:
|
|
627
|
+
processStartIdentity: process.platform === "linux"
|
|
628
|
+
? readLinuxProcessStartIdentitySync(process.pid)
|
|
629
|
+
: currentProcessStartIdentity(),
|
|
625
630
|
buildId: release?.manifest.buildId ?? "dev",
|
|
626
631
|
version: YUI_VERSION
|
|
627
632
|
});
|
|
@@ -1,8 +1,41 @@
|
|
|
1
1
|
import { lstatSync, readFileSync } from "node:fs";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
2
3
|
import { join } from "node:path";
|
|
4
|
+
import { nativeExecutable } from "../runtime/nativeExecutable.js";
|
|
3
5
|
/** One process generation, shared by file-lock producers and reclaim checks. */
|
|
4
6
|
export function currentFileLockOwner() {
|
|
5
|
-
return `${process.pid}:${
|
|
7
|
+
return `${process.pid}:${currentProcessStartIdentity()}`;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Identity of the current process. Linux reads the /proc start-time field,
|
|
11
|
+
* which survives PID reuse. macOS uses libproc's process start timestamp.
|
|
12
|
+
*/
|
|
13
|
+
export function currentProcessStartIdentity() {
|
|
14
|
+
const identity = readProcessStartIdentity(process.pid);
|
|
15
|
+
if (identity === undefined)
|
|
16
|
+
throw new Error(`Process start identity is unavailable for PID ${process.pid}.`);
|
|
17
|
+
return identity;
|
|
18
|
+
}
|
|
19
|
+
export function readProcessStartIdentity(pid) {
|
|
20
|
+
if (!Number.isSafeInteger(pid) || pid < 1)
|
|
21
|
+
return undefined;
|
|
22
|
+
if (process.platform === "linux") {
|
|
23
|
+
try {
|
|
24
|
+
return readProcessStart(pid);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (process.platform !== "darwin")
|
|
31
|
+
return undefined;
|
|
32
|
+
try {
|
|
33
|
+
const identity = execFileSync(nativeExecutable("process-identity"), [String(pid)], { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
34
|
+
return /^(0|[1-9][0-9]{0,31})$/u.test(identity) ? identity : undefined;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
6
39
|
}
|
|
7
40
|
/**
|
|
8
41
|
* False requires proof of absence or a different process generation.
|
|
@@ -42,20 +75,28 @@ export function processGenerationIsLive(pid, startIdentity) {
|
|
|
42
75
|
if (!Number.isSafeInteger(pid) || pid < 1 || !/^(0|[1-9][0-9]{0,31})$/u.test(startIdentity)) {
|
|
43
76
|
throw new Error(`Unverified process generation for PID ${pid}.`);
|
|
44
77
|
}
|
|
78
|
+
const actual = readProcessStartIdentity(pid);
|
|
79
|
+
if (actual !== undefined)
|
|
80
|
+
return actual === startIdentity;
|
|
81
|
+
// PID liveness can prove a process is gone, never prove its generation.
|
|
45
82
|
try {
|
|
46
|
-
|
|
83
|
+
process.kill(pid, 0);
|
|
47
84
|
}
|
|
48
|
-
catch {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (hasCode(error, "ESRCH"))
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
throw new Error(`Unverified process generation for PID ${pid}; OS identity could not be read.`);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Owner liveness used by reconciliation. An unreadable identity for a live
|
|
93
|
+
* PID is unknown and must not authorize cleanup or release of durable custody.
|
|
94
|
+
*/
|
|
95
|
+
export function processOwnerIsLive(pid, startIdentity) {
|
|
96
|
+
if (!Number.isSafeInteger(pid) || pid < 1 || !/^(0|[1-9][0-9]{0,31})$/u.test(startIdentity)) {
|
|
97
|
+
throw new Error(`Unverified process generation for PID ${pid}.`);
|
|
58
98
|
}
|
|
99
|
+
return processGenerationIsLive(pid, startIdentity);
|
|
59
100
|
}
|
|
60
101
|
function readProcessStart(pid) {
|
|
61
102
|
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
package/dist/doctor/doctor.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { accessSync, constants, existsSync, lstatSync } from "node:fs";
|
|
1
|
+
import { accessSync, constants, existsSync, lstatSync, realpathSync } from "node:fs";
|
|
2
2
|
import { isAbsolute, join } from "node:path";
|
|
3
3
|
import Database from "better-sqlite3";
|
|
4
4
|
import { configuredAgentToDefinition, resolveAgentEnvironment } from "../agent/agent.js";
|
|
5
5
|
import { operationalAgentEnvironment } from "../agent/launchEnvironment.js";
|
|
6
6
|
import { resolveTmuxBin } from "../config/yuiConfig.js";
|
|
7
7
|
import { compileRoleSessionContext } from "../context/roleSessionContext.js";
|
|
8
|
+
import { callController, ControllerClientError } from "../core/controllerClient.js";
|
|
8
9
|
import { EPHEMERAL_DOMAIN_GRACE_MS, readEphemeralDomainIdentity, readLinuxProcessStartIdentity } from "../controller/domainIdentity.js";
|
|
9
10
|
import { usageError } from "../errors/cliError.js";
|
|
10
11
|
import { inspectAgentCapabilities, resolveAgentAdapter } from "../executor/agentAdapter.js";
|
|
@@ -12,6 +13,8 @@ import { assertCodexLaunchOverridesAvailable, inspectCodexLaunchConfig } from ".
|
|
|
12
13
|
import { resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
|
|
13
14
|
import { nativeAdditionalDirectories, nativeAgentWorkspace, withNativeProjectDirectories } from "../executor/fileRoleLaunchPlanner.js";
|
|
14
15
|
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
16
|
+
import { codexAppNeedsRestart, codexAppProcessIds, codexAppWebSocketUrl, codexSharedSocketPath, findMacCodexAppExecutable, isCodexInspectionPermissionError } from "../runtime/codexSharedDaemon.js";
|
|
17
|
+
import { codexAgentsInUse } from "../runtime/localStartup.js";
|
|
15
18
|
import { CURRENT_DATABASE_FILENAME, openCurrentTaskStore } from "../storage/currentTaskStore.js";
|
|
16
19
|
import { inspectStorageSchema } from "../storage/storageSchema.js";
|
|
17
20
|
import { resolveStoreWorkerEnabledForHome } from "../storage/storeRpc.js";
|
|
@@ -42,8 +45,8 @@ export function summarizeStorageHealth(checks) {
|
|
|
42
45
|
return { healthy: blocking.length === 0, blocking };
|
|
43
46
|
}
|
|
44
47
|
/** Build the full machine-readable doctor report (checks + storage health). */
|
|
45
|
-
export function buildDoctorReport(env, executor) {
|
|
46
|
-
const inspection = inspectDoctor(env, executor);
|
|
48
|
+
export async function buildDoctorReport(env, executor) {
|
|
49
|
+
const inspection = await inspectDoctor(env, executor);
|
|
47
50
|
const home = resolveYuiHome(env);
|
|
48
51
|
return {
|
|
49
52
|
checks: inspection.checks,
|
|
@@ -100,29 +103,30 @@ function inspectStorageDetails(home, env) {
|
|
|
100
103
|
};
|
|
101
104
|
}
|
|
102
105
|
/** Runs the read-only current-storage diagnostics used by `yui doctor`. */
|
|
103
|
-
export function runDoctorCommand(args, env, executor) {
|
|
106
|
+
export async function runDoctorCommand(args, env, executor) {
|
|
104
107
|
if (args.length !== 0)
|
|
105
108
|
throw usageError("Doctor usage: yui doctor");
|
|
106
|
-
const inspection = inspectDoctor(env, executor);
|
|
109
|
+
const inspection = await inspectDoctor(env, executor);
|
|
107
110
|
return renderDoctor(inspection.checks, inspection.review);
|
|
108
111
|
}
|
|
109
|
-
export function getDoctorChecks(env, executor) {
|
|
110
|
-
return inspectDoctor(env, executor).checks;
|
|
112
|
+
export async function getDoctorChecks(env, executor) {
|
|
113
|
+
return (await inspectDoctor(env, executor)).checks;
|
|
111
114
|
}
|
|
112
|
-
function inspectDoctor(env, executor) {
|
|
115
|
+
async function inspectDoctor(env, executor) {
|
|
113
116
|
const home = resolveYuiHome(env);
|
|
114
117
|
const homeCheck = checkHome(home);
|
|
115
118
|
const schema = readSchema(home);
|
|
116
119
|
const compatibility = inspectCompatibility(home, homeCheck, schema);
|
|
117
120
|
const schemaCheck = checkSchema(schema);
|
|
118
121
|
const storage = inspectState(home, homeCheck, schema, compatibility);
|
|
122
|
+
const controller = await checkController(home);
|
|
119
123
|
const domain = checkEphemeralDomain(home);
|
|
120
124
|
const durableConfig = readDurableConfigSafely(home);
|
|
121
125
|
const toolChecks = [
|
|
122
126
|
checkExecutable("git", durableConfig.gitBin, ["--version"], executor),
|
|
123
127
|
checkExecutable("tmux", durableConfig.tmuxBin, ["-V"], executor)
|
|
124
128
|
];
|
|
125
|
-
const agentChecks = storage.agents.flatMap((agent) => checkAgent(agent, executor, env));
|
|
129
|
+
const agentChecks = storage.agents.flatMap((agent) => checkAgent(agent, executor, env, home, storage.activeCodexAgentIds.has(agent.id)));
|
|
126
130
|
const review = inspectReview({ ...storage.review, home }, agentChecks, env);
|
|
127
131
|
return {
|
|
128
132
|
checks: [
|
|
@@ -130,6 +134,7 @@ function inspectDoctor(env, executor) {
|
|
|
130
134
|
schemaCheck,
|
|
131
135
|
compatibility.check,
|
|
132
136
|
storage.check,
|
|
137
|
+
controller,
|
|
133
138
|
...(domain === undefined ? [] : [domain]),
|
|
134
139
|
...toolChecks,
|
|
135
140
|
...agentChecks,
|
|
@@ -138,6 +143,39 @@ function inspectDoctor(env, executor) {
|
|
|
138
143
|
review: review.report
|
|
139
144
|
};
|
|
140
145
|
}
|
|
146
|
+
/** Query the authenticated Controller endpoint without starting it. */
|
|
147
|
+
export async function checkController(home, probe = callController) {
|
|
148
|
+
try {
|
|
149
|
+
const result = await probe(home, "controller.status", {}, { timeoutMs: 1_000 });
|
|
150
|
+
if (result !== null && typeof result === "object" && !Array.isArray(result)) {
|
|
151
|
+
const status = result;
|
|
152
|
+
if (status.running === true && Number.isSafeInteger(status.pid) && Number(status.pid) > 0) {
|
|
153
|
+
return { name: "controller", status: "ok", detail: `Controller is running pid=${status.pid}.` };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
name: "controller", status: "invalid",
|
|
158
|
+
detail: "Controller returned an invalid status; inspect yui controller status --verbose."
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
if (error instanceof ControllerClientError && error.code === "CONTROLLER_NOT_RUNNING") {
|
|
163
|
+
return { name: "controller", status: "missing", detail: `${error.message} Run yui start.` };
|
|
164
|
+
}
|
|
165
|
+
if (error instanceof ControllerClientError && error.code === "CONTROLLER_UNAVAILABLE") {
|
|
166
|
+
return {
|
|
167
|
+
name: "controller",
|
|
168
|
+
status: "invalid",
|
|
169
|
+
detail: `Controller is not reachable: ${error.message} Run yui start. Its process state is unverified.`
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
name: "controller",
|
|
174
|
+
status: "invalid",
|
|
175
|
+
detail: `Controller status could not be verified: ${error instanceof Error ? error.message : String(error)}. Inspect yui controller status --verbose.`
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
141
179
|
function checkEphemeralDomain(home) {
|
|
142
180
|
const identity = readEphemeralDomainIdentity(home);
|
|
143
181
|
if (identity.status === "absent")
|
|
@@ -327,6 +365,7 @@ function inspectState(home, homeCheck, schema, compatibility) {
|
|
|
327
365
|
detail: `yui.db readable agents=${agents.length} tasks=${tasks.length} roles=${roleCount} globalRoles=${globalRoles.length} defaultAgent=${config.defaultAgent ?? "none"}`
|
|
328
366
|
},
|
|
329
367
|
agents,
|
|
368
|
+
activeCodexAgentIds: new Set(codexAgentsInUse(store).map((agent) => agent.id)),
|
|
330
369
|
review: {
|
|
331
370
|
storageReady: true,
|
|
332
371
|
storageDetail: "yui.db is readable",
|
|
@@ -362,6 +401,7 @@ function blockedStorage(status, detail) {
|
|
|
362
401
|
return {
|
|
363
402
|
check: { name: "storage state", status, detail },
|
|
364
403
|
agents: [],
|
|
404
|
+
activeCodexAgentIds: new Set(),
|
|
365
405
|
review: {
|
|
366
406
|
storageReady: false,
|
|
367
407
|
storageDetail: detail
|
|
@@ -655,11 +695,14 @@ function checkExecutable(name, command, args, executor) {
|
|
|
655
695
|
};
|
|
656
696
|
}
|
|
657
697
|
}
|
|
658
|
-
function checkAgent(agent, executor, environment) {
|
|
698
|
+
function checkAgent(agent, executor, environment, home, activeCodex) {
|
|
659
699
|
let snapshot;
|
|
700
|
+
let baseArgs;
|
|
701
|
+
let launchEnvironment;
|
|
660
702
|
try {
|
|
661
703
|
const definition = configuredAgentToDefinition(agent);
|
|
662
|
-
|
|
704
|
+
baseArgs = definition.baseArgs;
|
|
705
|
+
launchEnvironment = resolveDoctorAgentEnvironment(definition, environment);
|
|
663
706
|
snapshot = inspectAgentCapabilities(definition, {
|
|
664
707
|
run: (command, args) => runAgentProbe(executor, command, args, launchEnvironment)
|
|
665
708
|
});
|
|
@@ -686,6 +729,9 @@ function checkAgent(agent, executor, environment) {
|
|
|
686
729
|
const available = snapshot.fields.filter((field) => field.status === "available").length;
|
|
687
730
|
const degraded = snapshot.fields.filter((field) => field.status === "degraded").length;
|
|
688
731
|
const unavailable = snapshot.fields.filter((field) => field.status === "unavailable").length;
|
|
732
|
+
const daemon = activeCodex && status === "ok"
|
|
733
|
+
? checkCodexSharedDaemon(agent.command, baseArgs, launchEnvironment, executor, agent.id)
|
|
734
|
+
: undefined;
|
|
689
735
|
return [
|
|
690
736
|
{ name: `agent:${agent.id}:command`, status, detail: commandDetail },
|
|
691
737
|
{
|
|
@@ -698,9 +744,181 @@ function checkAgent(agent, executor, environment) {
|
|
|
698
744
|
"installation/help inspection only; no model or account capability query",
|
|
699
745
|
...snapshot.warnings.map((warning) => `warning=${warning}`)
|
|
700
746
|
].join(" ")
|
|
701
|
-
}
|
|
747
|
+
},
|
|
748
|
+
...(daemon === undefined ? [] : [daemon]),
|
|
749
|
+
...(daemon !== undefined && process.platform === "darwin"
|
|
750
|
+
? checkMacCodexApp(agent.id, launchEnvironment, home, daemon, executor)
|
|
751
|
+
: [])
|
|
702
752
|
];
|
|
703
753
|
}
|
|
754
|
+
export function checkMacCodexApp(agentId, environment, home, daemon, executor, appExecutable = findMacCodexAppExecutable(environment)) {
|
|
755
|
+
if (appExecutable === undefined)
|
|
756
|
+
return [{
|
|
757
|
+
name: `agent:${agentId}:mac-app`, status: "ok",
|
|
758
|
+
detail: "Codex App was not detected; App sharing is not configured on this Mac."
|
|
759
|
+
}];
|
|
760
|
+
const expectedUrl = codexAppWebSocketUrl(environment, home);
|
|
761
|
+
let appPids;
|
|
762
|
+
try {
|
|
763
|
+
appPids = codexAppProcessIds(executor.run("/bin/ps", ["-axo", "pid=,args="], {
|
|
764
|
+
timeoutMs: 5_000
|
|
765
|
+
}), appExecutable);
|
|
766
|
+
}
|
|
767
|
+
catch (error) {
|
|
768
|
+
return [{
|
|
769
|
+
name: `agent:${agentId}:mac-app`, status: "invalid",
|
|
770
|
+
detail: `Cannot inspect Codex App processes: ${commandFailure(error)}`
|
|
771
|
+
}];
|
|
772
|
+
}
|
|
773
|
+
const processCheck = {
|
|
774
|
+
name: `agent:${agentId}:mac-app`,
|
|
775
|
+
status: appPids.length > 0 ? "ok" : "missing",
|
|
776
|
+
detail: appPids.length > 0
|
|
777
|
+
? `Codex App is running pid=${appPids.join(",")}.`
|
|
778
|
+
: "Codex App is installed but not running."
|
|
779
|
+
};
|
|
780
|
+
let launchCheck;
|
|
781
|
+
try {
|
|
782
|
+
const launchUrl = executor.run("/bin/launchctl", ["getenv", "CODEX_APP_SERVER_WS_URL"], {
|
|
783
|
+
timeoutMs: 5_000
|
|
784
|
+
}).trim();
|
|
785
|
+
launchCheck = launchUrl === expectedUrl
|
|
786
|
+
? {
|
|
787
|
+
name: `agent:${agentId}:mac-app-launch`, status: "ok",
|
|
788
|
+
detail: "Future Codex App launches are configured for this shared app-server."
|
|
789
|
+
}
|
|
790
|
+
: {
|
|
791
|
+
name: `agent:${agentId}:mac-app-launch`, status: "missing",
|
|
792
|
+
detail: "This login is not prepared for App sharing. Run yui start before opening or restarting App."
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
catch (error) {
|
|
796
|
+
launchCheck = {
|
|
797
|
+
name: `agent:${agentId}:mac-app-launch`, status: "invalid",
|
|
798
|
+
detail: `Cannot inspect the App launch environment: ${commandFailure(error)}`
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
const connectionName = `agent:${agentId}:mac-app-connection`;
|
|
802
|
+
if (appPids.length === 0) {
|
|
803
|
+
return [processCheck, launchCheck, {
|
|
804
|
+
name: connectionName, status: "missing", detail: "No detected App process; connection is not verified."
|
|
805
|
+
}];
|
|
806
|
+
}
|
|
807
|
+
if (daemon.status !== "ok") {
|
|
808
|
+
return [processCheck, launchCheck, {
|
|
809
|
+
name: connectionName, status: daemon.status,
|
|
810
|
+
detail: "Shared daemon is unavailable or unverified; App connection to it cannot be confirmed."
|
|
811
|
+
}];
|
|
812
|
+
}
|
|
813
|
+
let configuredCount = 0;
|
|
814
|
+
for (const pid of appPids) {
|
|
815
|
+
try {
|
|
816
|
+
const command = executor.run("/bin/ps", ["eww", "-p", pid, "-o", "command="], {
|
|
817
|
+
timeoutMs: 5_000
|
|
818
|
+
});
|
|
819
|
+
if (!codexAppNeedsRestart(command, expectedUrl, false))
|
|
820
|
+
configuredCount += 1;
|
|
821
|
+
}
|
|
822
|
+
catch (error) {
|
|
823
|
+
return [processCheck, launchCheck, {
|
|
824
|
+
name: connectionName, status: "invalid",
|
|
825
|
+
detail: `Cannot inspect running Codex App: ${commandFailure(error)}`
|
|
826
|
+
}];
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
try {
|
|
830
|
+
const socketTarget = realpathSync(codexSharedSocketPath(environment, home));
|
|
831
|
+
const sockets = parseMacUnixSockets(executor.run("/usr/sbin/lsof", ["-nP", "-U", "-F", "pfdn"], {
|
|
832
|
+
timeoutMs: 5_000
|
|
833
|
+
}));
|
|
834
|
+
const daemonSockets = sockets.filter((socket) => socket.name === socketTarget);
|
|
835
|
+
if (daemonSockets.length === 0) {
|
|
836
|
+
return [processCheck, launchCheck, {
|
|
837
|
+
name: connectionName, status: "invalid",
|
|
838
|
+
detail: "Daemon reports running, but its Unix socket endpoint was not observable."
|
|
839
|
+
}];
|
|
840
|
+
}
|
|
841
|
+
const peer = sockets.find((socket) => appPids.includes(socket.pid)
|
|
842
|
+
&& daemonSockets.some((daemonSocket) => socket.name === `->${daemonSocket.device}`));
|
|
843
|
+
return [processCheck, launchCheck, peer === undefined
|
|
844
|
+
? {
|
|
845
|
+
name: connectionName, status: "missing",
|
|
846
|
+
detail: configuredCount > 0
|
|
847
|
+
? "App has the shared URL, but no live socket peer to this daemon was observed."
|
|
848
|
+
: "No live App socket peer to this daemon was observed; reopen App after Yui prepares sharing."
|
|
849
|
+
}
|
|
850
|
+
: {
|
|
851
|
+
name: connectionName, status: "ok",
|
|
852
|
+
detail: `App pid=${peer.pid} has a live Unix socket peer on shared daemon pid=${daemonSockets[0].pid}.`
|
|
853
|
+
}];
|
|
854
|
+
}
|
|
855
|
+
catch (error) {
|
|
856
|
+
return [processCheck, launchCheck, {
|
|
857
|
+
name: connectionName, status: "invalid",
|
|
858
|
+
detail: `Cannot verify the live App-to-daemon socket: ${commandFailure(error)}`
|
|
859
|
+
}];
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
function parseMacUnixSockets(output) {
|
|
863
|
+
const sockets = [];
|
|
864
|
+
let pid = "";
|
|
865
|
+
let device = "";
|
|
866
|
+
let name = "";
|
|
867
|
+
const finish = () => {
|
|
868
|
+
if (pid !== "" && /^0x[0-9a-f]+$/iu.test(device) && name !== "") {
|
|
869
|
+
sockets.push({ pid, device, name });
|
|
870
|
+
}
|
|
871
|
+
device = "";
|
|
872
|
+
name = "";
|
|
873
|
+
};
|
|
874
|
+
for (const line of output.split("\n")) {
|
|
875
|
+
if (line.startsWith("p")) {
|
|
876
|
+
finish();
|
|
877
|
+
pid = line.slice(1);
|
|
878
|
+
}
|
|
879
|
+
else if (line.startsWith("f"))
|
|
880
|
+
finish();
|
|
881
|
+
else if (line.startsWith("d"))
|
|
882
|
+
device = line.slice(1);
|
|
883
|
+
else if (line.startsWith("n"))
|
|
884
|
+
name = line.slice(1);
|
|
885
|
+
}
|
|
886
|
+
finish();
|
|
887
|
+
return sockets;
|
|
888
|
+
}
|
|
889
|
+
export function checkCodexSharedDaemon(command, baseArgs, environment, executor, agentId) {
|
|
890
|
+
const name = `agent:${agentId}:daemon`;
|
|
891
|
+
const probe = runAgentProbe(executor, command, [...baseArgs, "app-server", "daemon", "version"], environment);
|
|
892
|
+
if (probe.status !== 0) {
|
|
893
|
+
const detail = probe.stderr.trim() || probe.error?.message || "The Codex daemon could not be inspected.";
|
|
894
|
+
if (isCodexInspectionPermissionError(detail))
|
|
895
|
+
return {
|
|
896
|
+
name,
|
|
897
|
+
status: "invalid",
|
|
898
|
+
detail: `Shared Codex app-server state is unverified: this process was denied access during the daemon probe (${detail}). Run Doctor from a local terminal with access to the Codex socket.`
|
|
899
|
+
};
|
|
900
|
+
return {
|
|
901
|
+
name,
|
|
902
|
+
status: /No such file or directory|Connection refused/iu.test(detail) ? "missing" : "invalid",
|
|
903
|
+
detail: `Shared Codex app-server probe failed: ${detail}`
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
try {
|
|
907
|
+
const version = JSON.parse(probe.stdout);
|
|
908
|
+
if (version === null || typeof version !== "object" || Array.isArray(version))
|
|
909
|
+
throw new Error();
|
|
910
|
+
const state = version;
|
|
911
|
+
if (state.status !== "running")
|
|
912
|
+
throw new Error();
|
|
913
|
+
return {
|
|
914
|
+
name, status: "ok",
|
|
915
|
+
detail: `Shared Codex app-server running version=${String(state.appServerVersion ?? "unknown")}`
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
catch {
|
|
919
|
+
return { name, status: "invalid", detail: "Codex daemon version did not confirm a running app-server." };
|
|
920
|
+
}
|
|
921
|
+
}
|
|
704
922
|
function runAgentProbe(executor, command, args, environment) {
|
|
705
923
|
try {
|
|
706
924
|
return {
|
|
@@ -61,7 +61,7 @@ export function dispatchWorkItemSynthesis(store, taskId, workItemId, sourceRunId
|
|
|
61
61
|
if (store.getActiveRun(taskId, role.name) !== null) {
|
|
62
62
|
throw new Error(`WorkItem main Role already has an active AgentRun: ${role.name}.`);
|
|
63
63
|
}
|
|
64
|
-
const workspace = role.name === "leader"
|
|
64
|
+
const workspace = role.name === "leader" || task.projectBindings.length === 0
|
|
65
65
|
? store.getTaskWorkspace(taskId)
|
|
66
66
|
: store.getWorkItemWorkspace(taskId, item.id);
|
|
67
67
|
if (workspace === null) {
|
|
@@ -9,9 +9,6 @@ import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
|
9
9
|
import { CODEX_SANDBOXES as SANDBOXES, CODEX_APPROVALS as APPROVALS, configurationFieldsFromHelp, configurationFieldWarnings, staticAgentConfigurationFields, STATIC_CONFIGURATION_NOTICE } from "./agentConfigurationFields.js";
|
|
10
10
|
const PROBE_TIMEOUT_MS = 2_000;
|
|
11
11
|
const PROBE_MAX_BYTES = 1024 * 1024;
|
|
12
|
-
// Offline producer/type evidence, not a claim of live account/model testing.
|
|
13
|
-
// Keep the existing minimum support floors separate: see provider-protocol-contracts.md.
|
|
14
|
-
const AUDITED_PRODUCER_VERSIONS = { codex: "0.153.4", claude: "2.1.270" };
|
|
15
12
|
class BaseAdapter {
|
|
16
13
|
launchContextArgs(_input) {
|
|
17
14
|
return [];
|
|
@@ -605,11 +602,6 @@ export function inspectAgentCapabilities(agent, optionsOrNow = {}) {
|
|
|
605
602
|
probedAt: at
|
|
606
603
|
}, fields, at, warnings);
|
|
607
604
|
}
|
|
608
|
-
const audited = adapter.id === "acp" ? undefined : AUDITED_PRODUCER_VERSIONS[adapter.id];
|
|
609
|
-
if (audited !== undefined && compareVersions(version, audited) > 0) {
|
|
610
|
-
warnings.push(`Installed ${adapter.label} version ${version} is newer than the latest audited producer `
|
|
611
|
-
+ `${audited}; required CLI flags were detected, not live protocol compatibility.`);
|
|
612
|
-
}
|
|
613
605
|
return snapshot(agent, adapter, {
|
|
614
606
|
status: "installed", command: agent.command, version, probedAt: at
|
|
615
607
|
}, fields, at, warnings);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { hasRecentTurnId, rememberRecentTurnId, validateRecentTurnIds } from "../runtime/recentTurnIds.js";
|
|
3
3
|
import { roleSessionMayContinue, validateEffectiveLaunchSnapshot } from "./effectiveLaunch.js";
|
|
4
|
-
import { validateProviderRuntimeBinding, currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
|
|
4
|
+
import { assertProviderConversationReplaceable, validateProviderRuntimeBinding, currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
|
|
5
5
|
import { builtinDriverIdForAdapter, builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
6
6
|
import { builtinAgentEndpointImplementation, validateAgentEndpointImplementation } from "../runtime/agentEndpointIdentity.js";
|
|
7
7
|
/** Recovery addresses retained execution identity, not a live Session grant.
|
|
@@ -212,6 +212,9 @@ export function retireTaskRoleSessionsForWorkspace(set, now) {
|
|
|
212
212
|
if (live !== undefined) {
|
|
213
213
|
throw new Error(`Task Role session must be stopped before workspace migration: ${live.agentId}.`);
|
|
214
214
|
}
|
|
215
|
+
if (set.providerBinding !== null) {
|
|
216
|
+
assertProviderConversationReplaceable(set.providerBinding);
|
|
217
|
+
}
|
|
215
218
|
const timestamp = now.toISOString();
|
|
216
219
|
return validateRoleSessionSet({
|
|
217
220
|
...set,
|