copilot-hub 0.1.17 → 0.1.19
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/apps/agent-engine/.env.example +1 -1
- package/apps/agent-engine/dist/agent-worker.js +9 -1
- package/apps/agent-engine/dist/codex-account-refresh.js +42 -0
- package/apps/agent-engine/dist/config.js +2 -4
- package/apps/agent-engine/dist/index.js +34 -49
- package/apps/agent-engine/dist/test/codex-account-refresh.test.js +48 -0
- package/apps/control-plane/.env.example +1 -1
- package/apps/control-plane/dist/agent-worker.js +9 -1
- package/apps/control-plane/dist/channels/hub-model-utils.js +34 -0
- package/apps/control-plane/dist/channels/hub-ops-commands.js +134 -67
- package/apps/control-plane/dist/channels/telegram-channel.js +1 -0
- package/apps/control-plane/dist/config.js +2 -4
- package/apps/control-plane/dist/test/hub-model-utils.test.js +25 -1
- package/package.json +1 -1
- package/packages/core/dist/agent-supervisor.d.ts +2 -0
- package/packages/core/dist/agent-supervisor.js +64 -36
- package/packages/core/dist/agent-supervisor.js.map +1 -1
- package/packages/core/dist/bot-manager.d.ts +1 -0
- package/packages/core/dist/bot-manager.js +4 -0
- package/packages/core/dist/bot-manager.js.map +1 -1
- package/packages/core/dist/bot-runtime.d.ts +4 -0
- package/packages/core/dist/bot-runtime.js +81 -20
- package/packages/core/dist/bot-runtime.js.map +1 -1
- package/packages/core/dist/codex-app-client.js +4 -0
- package/packages/core/dist/codex-app-client.js.map +1 -1
- package/packages/core/dist/codex-app-utils.d.ts +1 -0
- package/packages/core/dist/codex-app-utils.js +43 -2
- package/packages/core/dist/codex-app-utils.js.map +1 -1
- package/packages/core/dist/provider-options.d.ts +1 -0
- package/packages/core/dist/provider-options.js +47 -0
- package/packages/core/dist/provider-options.js.map +1 -0
- package/packages/core/package.json +4 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BotRuntime } from "@copilot-hub/core/bot-runtime";
|
|
2
2
|
const rawBotConfig = String(process.env.AGENT_BOT_CONFIG_JSON ?? "").trim();
|
|
3
3
|
const rawProviderDefaults = String(process.env.AGENT_PROVIDER_DEFAULTS_JSON ?? "").trim();
|
|
4
|
-
const turnActivityTimeoutMs = Number.parseInt(String(process.env.AGENT_TURN_ACTIVITY_TIMEOUT_MS ?? "
|
|
4
|
+
const turnActivityTimeoutMs = Number.parseInt(String(process.env.AGENT_TURN_ACTIVITY_TIMEOUT_MS ?? "0"), 10);
|
|
5
5
|
const maxMessages = Number.parseInt(String(process.env.AGENT_MAX_MESSAGES ?? "200"), 10);
|
|
6
6
|
const initialWebPublicBaseUrl = String(process.env.AGENT_WEB_PUBLIC_BASE_URL ?? "http://127.0.0.1:8787").trim();
|
|
7
7
|
const kernelRequestTimeoutMs = Number.parseInt(String(process.env.AGENT_KERNEL_REQUEST_TIMEOUT_MS ?? "20000"), 10);
|
|
@@ -120,6 +120,14 @@ async function handleWorkerRequest(message) {
|
|
|
120
120
|
result = await runtime.reloadCapabilities(capabilityDefinitions);
|
|
121
121
|
break;
|
|
122
122
|
}
|
|
123
|
+
case "setProviderOptions": {
|
|
124
|
+
result = await runtime.setProviderOptions(payload);
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
case "refreshProviderSession": {
|
|
128
|
+
result = await runtime.refreshProviderSession(String(payload.reason ?? ""));
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
123
131
|
case "setProjectRoot": {
|
|
124
132
|
const projectRoot = String(payload.projectRoot ?? "").trim();
|
|
125
133
|
if (!projectRoot) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export async function refreshRunningBotProviders({ botManager, reason = "codex account switched", }) {
|
|
2
|
+
const statuses = await botManager.listBotsLive();
|
|
3
|
+
const runningBotIds = collectRunningBotIds(statuses);
|
|
4
|
+
const refreshedBotIds = [];
|
|
5
|
+
const failures = [];
|
|
6
|
+
for (const botId of runningBotIds) {
|
|
7
|
+
try {
|
|
8
|
+
await botManager.refreshBotProviderSession(botId, reason);
|
|
9
|
+
refreshedBotIds.push(botId);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
failures.push({
|
|
13
|
+
botId,
|
|
14
|
+
error: sanitizeError(error),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
refreshedBotIds,
|
|
20
|
+
failures,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function collectRunningBotIds(statuses) {
|
|
24
|
+
const runningBotIds = [];
|
|
25
|
+
for (const entry of Array.isArray(statuses) ? statuses : []) {
|
|
26
|
+
if (!isRecord(entry) || entry.running !== true) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const botId = String(entry.id ?? "").trim();
|
|
30
|
+
if (botId) {
|
|
31
|
+
runningBotIds.push(botId);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return runningBotIds;
|
|
35
|
+
}
|
|
36
|
+
function isRecord(value) {
|
|
37
|
+
return value !== null && typeof value === "object";
|
|
38
|
+
}
|
|
39
|
+
function sanitizeError(error) {
|
|
40
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
41
|
+
return raw.split(/\r?\n/).slice(0, 12).join("\n");
|
|
42
|
+
}
|
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import dotenv from "dotenv";
|
|
4
4
|
import { createWorkspaceBoundaryPolicy, assertWorkspaceAllowed, parseWorkspaceAllowedRoots, } from "@copilot-hub/core/workspace-policy";
|
|
5
|
+
import { parseTurnActivityTimeoutSetting } from "@copilot-hub/core/codex-app-utils";
|
|
5
6
|
import { getDefaultExternalWorkspaceBasePath, getKernelRootPath, } from "@copilot-hub/core/workspace-paths";
|
|
6
7
|
dotenv.config();
|
|
7
8
|
const kernelRootPath = getKernelRootPath();
|
|
@@ -36,10 +37,7 @@ const codexBin = resolveCodexBin(process.env.CODEX_BIN);
|
|
|
36
37
|
const codexHomeDir = resolveOptionalPath(process.env.CODEX_HOME_DIR);
|
|
37
38
|
const codexSandbox = normalizeCodexSandbox(process.env.CODEX_SANDBOX ?? "danger-full-access");
|
|
38
39
|
const codexApprovalPolicy = normalizeApprovalPolicy(process.env.CODEX_APPROVAL_POLICY ?? "never");
|
|
39
|
-
const turnActivityTimeoutMs =
|
|
40
|
-
if (!Number.isFinite(turnActivityTimeoutMs) || turnActivityTimeoutMs < 10000) {
|
|
41
|
-
throw new Error("TURN_ACTIVITY_TIMEOUT_MS must be an integer >= 10000.");
|
|
42
|
-
}
|
|
40
|
+
const turnActivityTimeoutMs = parseTurnActivityTimeoutSetting(process.env.TURN_ACTIVITY_TIMEOUT_MS ?? "0", 0);
|
|
43
41
|
const maxMessages = Number.parseInt(process.env.MAX_THREAD_MESSAGES ?? "200", 10);
|
|
44
42
|
if (!Number.isFinite(maxMessages) || maxMessages < 20) {
|
|
45
43
|
throw new Error("MAX_THREAD_MESSAGES must be an integer >= 20.");
|
|
@@ -11,6 +11,7 @@ import { InstanceLock } from "@copilot-hub/core/instance-lock";
|
|
|
11
11
|
import { KernelControlPlane } from "@copilot-hub/core/kernel-control-plane";
|
|
12
12
|
import { CONTROL_ACTIONS } from "@copilot-hub/core/control-plane-actions";
|
|
13
13
|
import { KernelSecretStore } from "@copilot-hub/core/secret-store";
|
|
14
|
+
import { refreshRunningBotProviders } from "./codex-account-refresh.js";
|
|
14
15
|
let activeWebPort = config.webPort;
|
|
15
16
|
let runtimeWebPublicBaseUrl = config.webPublicBaseUrl;
|
|
16
17
|
let shuttingDown = false;
|
|
@@ -211,15 +212,19 @@ function buildApiApp({ botManager, controlPlane, registryFilePath, }) {
|
|
|
211
212
|
});
|
|
212
213
|
return;
|
|
213
214
|
}
|
|
214
|
-
const
|
|
215
|
+
const refreshed = await refreshRunningBotProviders({
|
|
216
|
+
botManager: requireBotManager(),
|
|
217
|
+
});
|
|
215
218
|
res.json({
|
|
216
219
|
ok: true,
|
|
217
220
|
switched: true,
|
|
218
221
|
configured: true,
|
|
219
222
|
codexBin: status.codexBin,
|
|
220
223
|
detail: status.detail,
|
|
221
|
-
|
|
222
|
-
|
|
224
|
+
refreshedBots: refreshed.refreshedBotIds,
|
|
225
|
+
refreshFailures: refreshed.failures,
|
|
226
|
+
restartedBots: refreshed.refreshedBotIds,
|
|
227
|
+
restartFailures: refreshed.failures,
|
|
223
228
|
});
|
|
224
229
|
}));
|
|
225
230
|
app.get("/api/extensions/contract", wrapAsync(async (req, res) => {
|
|
@@ -454,7 +459,7 @@ function parseDeleteModeFromRequest(body) {
|
|
|
454
459
|
return "soft";
|
|
455
460
|
}
|
|
456
461
|
function startCodexDeviceAuthSession() {
|
|
457
|
-
if (codexDeviceAuthSession &&
|
|
462
|
+
if (codexDeviceAuthSession && isDeviceAuthBusy(codexDeviceAuthSession.status)) {
|
|
458
463
|
return codexDeviceAuthSession;
|
|
459
464
|
}
|
|
460
465
|
const codexBin = String(config.codexBin ?? "codex").trim() || "codex";
|
|
@@ -468,8 +473,8 @@ function startCodexDeviceAuthSession() {
|
|
|
468
473
|
logLines: [],
|
|
469
474
|
error: "",
|
|
470
475
|
child: null,
|
|
471
|
-
|
|
472
|
-
|
|
476
|
+
refreshedBots: [],
|
|
477
|
+
refreshFailures: [],
|
|
473
478
|
};
|
|
474
479
|
const child = spawn(codexBin, ["login", "--device-auth"], {
|
|
475
480
|
cwd: config.kernelRootPath,
|
|
@@ -497,14 +502,19 @@ function startCodexDeviceAuthSession() {
|
|
|
497
502
|
return;
|
|
498
503
|
}
|
|
499
504
|
if (code === 0) {
|
|
500
|
-
session.status = "
|
|
501
|
-
void
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
+
session.status = "applying";
|
|
506
|
+
void refreshRunningBotProviders({
|
|
507
|
+
botManager: requireBotManager(),
|
|
508
|
+
})
|
|
509
|
+
.then((refreshed) => {
|
|
510
|
+
session.refreshedBots = refreshed.refreshedBotIds;
|
|
511
|
+
session.refreshFailures = refreshed.failures;
|
|
512
|
+
session.status = "succeeded";
|
|
505
513
|
})
|
|
506
514
|
.catch((error) => {
|
|
507
|
-
session.
|
|
515
|
+
session.refreshFailures = [{ botId: "*", error: sanitizeError(error) }];
|
|
516
|
+
session.status = "failed";
|
|
517
|
+
session.error = sanitizeError(error);
|
|
508
518
|
});
|
|
509
519
|
return;
|
|
510
520
|
}
|
|
@@ -516,7 +526,7 @@ function startCodexDeviceAuthSession() {
|
|
|
516
526
|
return session;
|
|
517
527
|
}
|
|
518
528
|
function cancelCodexDeviceAuthSession() {
|
|
519
|
-
if (!codexDeviceAuthSession || !
|
|
529
|
+
if (!codexDeviceAuthSession || !isDeviceAuthCancelable(codexDeviceAuthSession.status)) {
|
|
520
530
|
return false;
|
|
521
531
|
}
|
|
522
532
|
codexDeviceAuthSession.status = "canceled";
|
|
@@ -541,8 +551,10 @@ function getCodexDeviceAuthSnapshot() {
|
|
|
541
551
|
...(session.loginUrl ? { loginUrl: session.loginUrl } : {}),
|
|
542
552
|
...(session.code ? { code: session.code } : {}),
|
|
543
553
|
...(session.error ? { detail: session.error } : {}),
|
|
544
|
-
|
|
545
|
-
|
|
554
|
+
refreshedBots: session.refreshedBots,
|
|
555
|
+
refreshFailures: session.refreshFailures,
|
|
556
|
+
restartedBots: session.refreshedBots,
|
|
557
|
+
restartFailures: session.refreshFailures,
|
|
546
558
|
};
|
|
547
559
|
}
|
|
548
560
|
async function waitForDeviceCode(session, timeoutMs) {
|
|
@@ -620,7 +632,13 @@ function findDeviceCode(lines) {
|
|
|
620
632
|
function stripAnsi(value) {
|
|
621
633
|
return String(value ?? "").replace(ANSI_ESCAPE_PATTERN, "");
|
|
622
634
|
}
|
|
623
|
-
function
|
|
635
|
+
function isDeviceAuthBusy(status) {
|
|
636
|
+
const value = String(status ?? "")
|
|
637
|
+
.trim()
|
|
638
|
+
.toLowerCase();
|
|
639
|
+
return value === "starting" || value === "pending" || value === "applying";
|
|
640
|
+
}
|
|
641
|
+
function isDeviceAuthCancelable(status) {
|
|
624
642
|
const value = String(status ?? "")
|
|
625
643
|
.trim()
|
|
626
644
|
.toLowerCase();
|
|
@@ -781,36 +799,3 @@ function isRecord(value) {
|
|
|
781
799
|
function isErrnoException(error) {
|
|
782
800
|
return isRecord(error);
|
|
783
801
|
}
|
|
784
|
-
async function restartRunningBots() {
|
|
785
|
-
const manager = requireBotManager();
|
|
786
|
-
const statuses = await manager.listBotsLive();
|
|
787
|
-
const runningBotIds = [];
|
|
788
|
-
for (const entry of statuses) {
|
|
789
|
-
if (!isRecord(entry) || entry.running !== true) {
|
|
790
|
-
continue;
|
|
791
|
-
}
|
|
792
|
-
const botId = String(entry.id ?? "").trim();
|
|
793
|
-
if (botId) {
|
|
794
|
-
runningBotIds.push(botId);
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
const restartedBotIds = [];
|
|
798
|
-
const failures = [];
|
|
799
|
-
for (const botId of runningBotIds) {
|
|
800
|
-
try {
|
|
801
|
-
await manager.stopBot(botId);
|
|
802
|
-
await manager.startBot(botId);
|
|
803
|
-
restartedBotIds.push(botId);
|
|
804
|
-
}
|
|
805
|
-
catch (error) {
|
|
806
|
-
failures.push({
|
|
807
|
-
botId,
|
|
808
|
-
error: sanitizeError(error),
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
return {
|
|
813
|
-
restartedBotIds,
|
|
814
|
-
failures,
|
|
815
|
-
};
|
|
816
|
-
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
let refreshHelpersPromise = null;
|
|
4
|
+
async function loadRefreshHelpers() {
|
|
5
|
+
if (!refreshHelpersPromise) {
|
|
6
|
+
const specifier = ["..", "codex-account-refresh.js"].join("/");
|
|
7
|
+
refreshHelpersPromise = import(specifier);
|
|
8
|
+
}
|
|
9
|
+
return refreshHelpersPromise;
|
|
10
|
+
}
|
|
11
|
+
test("collectRunningBotIds keeps only running bots with ids", async () => {
|
|
12
|
+
const { collectRunningBotIds } = await loadRefreshHelpers();
|
|
13
|
+
const botIds = collectRunningBotIds([
|
|
14
|
+
{ id: "agent-a", running: true },
|
|
15
|
+
{ id: "agent-b", running: false },
|
|
16
|
+
{ id: "agent-c", running: true },
|
|
17
|
+
{ running: true },
|
|
18
|
+
null,
|
|
19
|
+
]);
|
|
20
|
+
assert.deepEqual(botIds, ["agent-a", "agent-c"]);
|
|
21
|
+
});
|
|
22
|
+
test("refreshRunningBotProviders refreshes running bots and collects failures", async () => {
|
|
23
|
+
const { refreshRunningBotProviders } = await loadRefreshHelpers();
|
|
24
|
+
const refreshed = [];
|
|
25
|
+
const result = await refreshRunningBotProviders({
|
|
26
|
+
botManager: {
|
|
27
|
+
async listBotsLive() {
|
|
28
|
+
return [
|
|
29
|
+
{ id: "agent-a", running: true },
|
|
30
|
+
{ id: "agent-b", running: false },
|
|
31
|
+
{ id: "agent-c", running: true },
|
|
32
|
+
];
|
|
33
|
+
},
|
|
34
|
+
async refreshBotProviderSession(botId) {
|
|
35
|
+
if (botId === "agent-c") {
|
|
36
|
+
throw new Error("refresh failed");
|
|
37
|
+
}
|
|
38
|
+
refreshed.push(botId);
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
reason: "codex account switched",
|
|
42
|
+
});
|
|
43
|
+
assert.deepEqual(refreshed, ["agent-a"]);
|
|
44
|
+
assert.deepEqual(result.refreshedBotIds, ["agent-a"]);
|
|
45
|
+
assert.equal(result.failures.length, 1);
|
|
46
|
+
assert.equal(result.failures[0]?.botId, "agent-c");
|
|
47
|
+
assert.match(result.failures[0]?.error ?? "", /refresh failed/);
|
|
48
|
+
});
|
|
@@ -2,7 +2,7 @@ import { BotRuntime } from "@copilot-hub/core/bot-runtime";
|
|
|
2
2
|
import { createChannelAdapter } from "./channels/channel-factory.js";
|
|
3
3
|
const rawBotConfig = String(process.env.AGENT_BOT_CONFIG_JSON ?? "").trim();
|
|
4
4
|
const rawProviderDefaults = String(process.env.AGENT_PROVIDER_DEFAULTS_JSON ?? "").trim();
|
|
5
|
-
const turnActivityTimeoutMs = Number.parseInt(String(process.env.AGENT_TURN_ACTIVITY_TIMEOUT_MS ?? "
|
|
5
|
+
const turnActivityTimeoutMs = Number.parseInt(String(process.env.AGENT_TURN_ACTIVITY_TIMEOUT_MS ?? "0"), 10);
|
|
6
6
|
const maxMessages = Number.parseInt(String(process.env.AGENT_MAX_MESSAGES ?? "200"), 10);
|
|
7
7
|
const initialWebPublicBaseUrl = String(process.env.AGENT_WEB_PUBLIC_BASE_URL ?? "http://127.0.0.1:8787").trim();
|
|
8
8
|
const kernelRequestTimeoutMs = Number.parseInt(String(process.env.AGENT_KERNEL_REQUEST_TIMEOUT_MS ?? "20000"), 10);
|
|
@@ -122,6 +122,14 @@ async function handleWorkerRequest(message) {
|
|
|
122
122
|
result = await runtime.reloadCapabilities(capabilityDefinitions);
|
|
123
123
|
break;
|
|
124
124
|
}
|
|
125
|
+
case "setProviderOptions": {
|
|
126
|
+
result = await runtime.setProviderOptions(payload);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case "refreshProviderSession": {
|
|
130
|
+
result = await runtime.refreshProviderSession(String(payload.reason ?? ""));
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
125
133
|
case "setProjectRoot": {
|
|
126
134
|
const projectRoot = String(payload.projectRoot ?? "").trim();
|
|
127
135
|
if (!projectRoot) {
|
|
@@ -245,6 +245,40 @@ export async function applyBotModelPolicy({ apiPost, botId, botState, model, })
|
|
|
245
245
|
model,
|
|
246
246
|
});
|
|
247
247
|
}
|
|
248
|
+
export function getRuntimeModel(runtime) {
|
|
249
|
+
if (!runtime || typeof runtime.getProviderOptions !== "function") {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
const options = runtime.getProviderOptions();
|
|
253
|
+
if (!isObject(options)) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
const model = String(options.model ?? "").trim();
|
|
257
|
+
return model || null;
|
|
258
|
+
}
|
|
259
|
+
export async function applyRuntimeModelPolicy({ runtime, model, }) {
|
|
260
|
+
if (!runtime || typeof runtime.setProviderOptions !== "function") {
|
|
261
|
+
throw new Error("Hub model update is not available on this runtime.");
|
|
262
|
+
}
|
|
263
|
+
await runtime.setProviderOptions({ model });
|
|
264
|
+
}
|
|
265
|
+
export function resolveSharedModel(models) {
|
|
266
|
+
let normalizedModel;
|
|
267
|
+
for (const entry of Array.isArray(models) ? models : []) {
|
|
268
|
+
const nextModel = String(entry ?? "").trim() || null;
|
|
269
|
+
if (normalizedModel === undefined) {
|
|
270
|
+
normalizedModel = nextModel;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (normalizedModel !== nextModel) {
|
|
274
|
+
return { mode: "mixed" };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
mode: "uniform",
|
|
279
|
+
model: normalizedModel ?? null,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
248
282
|
export async function applyModelPolicyToBots({ apiPost, bots, model, }) {
|
|
249
283
|
const updatedBotIds = [];
|
|
250
284
|
const failures = [];
|