@agent-team-foundation/first-tree-hub 0.10.2 → 0.10.3
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/dist/{bootstrap-Ca5Fiqz6.mjs → bootstrap-CBAVWQUT.mjs} +1 -2
- package/dist/cli/index.mjs +60 -7
- package/dist/{dist-CLiN7cVS.mjs → dist-DUCelK3Z.mjs} +116 -13
- package/dist/drizzle/0027_runtime_provider.sql +10 -0
- package/dist/drizzle/0028_auth_identity_user_github_unique.sql +12 -0
- package/dist/drizzle/meta/_journal.json +14 -0
- package/dist/{feishu-FTWnoOsc.mjs → feishu-Boy3n8CT.mjs} +1 -1
- package/dist/index.mjs +4 -4
- package/dist/{invitation-BTlGMy0o-dIoR8JRj.mjs → invitation-BTlGMy0o-Coj07kYi.mjs} +1 -1
- package/dist/{saas-connect-idjpoPTk.mjs → saas-connect-3p-vBkuY.mjs} +1244 -173
- package/dist/web/assets/index-CHoaSIzI.js +21 -0
- package/dist/web/assets/index-CP8uLPyO.css +1 -0
- package/dist/web/assets/index-D7OzKrI2.js +387 -0
- package/dist/web/index.html +2 -2
- package/package.json +3 -2
- package/dist/web/assets/index-CEAPwdg7.js +0 -377
- package/dist/web/assets/index-CzWeWItA.css +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { m as __toESM } from "./esm-CYu4tXXn.mjs";
|
|
2
2
|
import { _ as withSpan, a as endWsConnectionSpan, b as require_pino, c as messageAttrs, d as rootLogger$1, g as startWsConnectionSpan, i as currentTraceId, n as applyLoggerConfig, o as getFastifyOtelPlugin, p as setWsConnectionAttrs, r as createLogger$1, t as adapterAttrs, u as observabilityPlugin, v as withWsMessageSpan, y as FIRST_TREE_HUB_ATTR } from "./observability-DPyf745N-BSc8QNcR.mjs";
|
|
3
|
-
import { C as serverConfigSchema, S as resolveConfigReadonly, _ as loadAgents, b as resetConfig, c as saveCredentials, d as DEFAULT_HOME_DIR$1, f as agentConfigSchema, g as initConfig, i as loadCredentials, l as DEFAULT_CONFIG_DIR, m as collectMissingPrompts, n as ensureFreshAccessToken, o as resolveServerUrl, p as clientConfigSchema, s as saveAgentConfig, u as DEFAULT_DATA_DIR$1, v as migrateLegacyHome, w as setConfigValue, x as resetConfigMeta } from "./bootstrap-
|
|
4
|
-
import { $ as
|
|
3
|
+
import { C as serverConfigSchema, S as resolveConfigReadonly, _ as loadAgents, b as resetConfig, c as saveCredentials, d as DEFAULT_HOME_DIR$1, f as agentConfigSchema, g as initConfig, i as loadCredentials, l as DEFAULT_CONFIG_DIR, m as collectMissingPrompts, n as ensureFreshAccessToken, o as resolveServerUrl, p as clientConfigSchema, s as saveAgentConfig, u as DEFAULT_DATA_DIR$1, v as migrateLegacyHome, w as setConfigValue, x as resetConfigMeta } from "./bootstrap-CBAVWQUT.mjs";
|
|
4
|
+
import { $ as safeRedirectPath, A as createOrgFromMeSchema, B as imageInlineContentSchema, C as clientRegisterSchema, D as createAgentSchema, E as createAdapterMappingSchema, F as dryRunAgentRuntimeConfigSchema, G as linkTaskChatSchema, H as isRedactedEnvValue, I as extractMentions, J as notificationQuerySchema, K as loginSchema, L as githubCallbackQuerySchema, M as createTaskSchema, N as defaultRuntimeConfigPayload, O as createChatSchema, P as delegateFeishuUserSchema, Q as runtimeStateMessageSchema, R as githubDevCallbackQuerySchema, S as clientCapabilitiesSchema$1, T as createAdapterConfigSchema, U as isReservedAgentName$1, V as inboxPollQuerySchema, W as joinByInvitationSchema, X as rebindAgentSchema, Y as paginationQuerySchema, Z as refreshTokenSchema, _ as adminUpdateTaskSchema, _t as updateOrganizationSchema, a as AGENT_STATUSES, at as sessionEventMessageSchema, b as agentRuntimeConfigPayloadSchema$1, bt as wsAuthFrameSchema, ct as sessionStateMessageSchema, d as TASK_HEALTH_SIGNALS, dt as updateAdapterConfigSchema, et as scanMentionTokens, f as TASK_STATUSES, ft as updateAgentRuntimeConfigSchema, g as adminCreateTaskSchema, gt as updateMemberSchema, h as addParticipantSchema, ht as updateClientCapabilitiesSchema, i as AGENT_SOURCES, it as sessionCompletionMessageSchema, j as createOrganizationSchema, k as createMemberSchema, l as SYSTEM_CONFIG_DEFAULTS, lt as switchOrgSchema, m as WS_AUTH_FRAME_TIMEOUT_MS, mt as updateChatSchema, n as AGENT_NAME_REGEX$1, nt as sendMessageSchema, o as AGENT_TYPES, ot as sessionEventSchema$1, p as TASK_TERMINAL_STATUSES, pt as updateAgentSchema, q as messageSourceSchema$1, r as AGENT_SELECTOR_HEADER$1, rt as sendToAgentSchema, s as AGENT_VISIBILITY, st as sessionReconcileRequestSchema, t as AGENT_BIND_REJECT_REASONS, tt as selfServiceFeishuBotSchema, u as TASK_CREATOR_TYPES, ut as taskListQuerySchema, v as agentBindRequestSchema, vt as updateSystemConfigSchema, w as connectTokenExchangeSchema, x as agentTypeSchema$1, y as agentPinnedMessageSchema$1, yt as updateTaskStatusSchema, z as githubStartQuerySchema } from "./dist-DUCelK3Z.mjs";
|
|
5
5
|
import { a as ForbiddenError, c as buildInviteUrl, d as getActiveInvitation, f as invitationRedemptions, g as recordRedemption, i as ConflictError, l as ensureActiveInvitation, m as organizations, n as BadRequestError, o as NotFoundError, p as invitations, r as ClientOrgMismatchError$1, s as UnauthorizedError, t as AppError, u as findActiveByToken, v as users, y as uuidv7 } from "./invitation-C_zAhB8x-8Khychlu.mjs";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { ZodError, z } from "zod";
|
|
@@ -16,13 +16,14 @@ import { mkdir, writeFile } from "node:fs/promises";
|
|
|
16
16
|
import { parse, stringify } from "yaml";
|
|
17
17
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
18
18
|
import { execFileSync, execSync, spawn, spawnSync } from "node:child_process";
|
|
19
|
+
import { Codex } from "@openai/codex-sdk";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
19
21
|
import * as semver from "semver";
|
|
20
22
|
import bcrypt from "bcrypt";
|
|
21
23
|
import { and, asc, count, desc, eq, gt, inArray, isNotNull, isNull, lt, ne, or, sql } from "drizzle-orm";
|
|
22
24
|
import { drizzle } from "drizzle-orm/postgres-js";
|
|
23
25
|
import postgres from "postgres";
|
|
24
26
|
import { confirm, input, password, select } from "@inquirer/prompts";
|
|
25
|
-
import { fileURLToPath } from "node:url";
|
|
26
27
|
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
|
27
28
|
import { bigserial, boolean, index, integer, jsonb, pgTable, primaryKey, serial, text, timestamp, unique, uniqueIndex } from "drizzle-orm/pg-core";
|
|
28
29
|
import cors from "@fastify/cors";
|
|
@@ -393,7 +394,8 @@ z.enum([
|
|
|
393
394
|
"not_owned",
|
|
394
395
|
"agent_suspended",
|
|
395
396
|
"wrong_org",
|
|
396
|
-
"unknown_agent"
|
|
397
|
+
"unknown_agent",
|
|
398
|
+
"runtime_provider_mismatch"
|
|
397
399
|
]);
|
|
398
400
|
/** Header used on agent-scoped HTTP calls to select which managed agent the JWT acts as. */
|
|
399
401
|
const AGENT_SELECTOR_HEADER = "x-agent-id";
|
|
@@ -421,6 +423,7 @@ z.object({
|
|
|
421
423
|
}),
|
|
422
424
|
clients: z.number().int()
|
|
423
425
|
});
|
|
426
|
+
const runtimeProviderSchema = z.enum(["claude-code", "codex"]);
|
|
424
427
|
const agentTypeSchema = z.enum([
|
|
425
428
|
"human",
|
|
426
429
|
"personal_assistant",
|
|
@@ -462,7 +465,8 @@ z.object({
|
|
|
462
465
|
visibility: agentVisibilitySchema.optional(),
|
|
463
466
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
464
467
|
managerId: z.string().optional(),
|
|
465
|
-
clientId: z.string().min(1).max(100).optional()
|
|
468
|
+
clientId: z.string().min(1).max(100).optional(),
|
|
469
|
+
runtimeProvider: runtimeProviderSchema.optional()
|
|
466
470
|
});
|
|
467
471
|
z.object({
|
|
468
472
|
type: agentTypeSchema.optional(),
|
|
@@ -473,6 +477,11 @@ z.object({
|
|
|
473
477
|
managerId: z.string().nullable().optional(),
|
|
474
478
|
clientId: z.string().min(1).max(100).nullable().optional()
|
|
475
479
|
});
|
|
480
|
+
z.object({
|
|
481
|
+
clientId: z.string().min(1).max(100),
|
|
482
|
+
runtimeProvider: runtimeProviderSchema,
|
|
483
|
+
force: z.boolean().optional()
|
|
484
|
+
});
|
|
476
485
|
z.object({
|
|
477
486
|
uuid: z.string(),
|
|
478
487
|
name: z.string().nullable(),
|
|
@@ -487,6 +496,7 @@ z.object({
|
|
|
487
496
|
metadata: z.record(z.string(), z.unknown()),
|
|
488
497
|
managerId: z.string().nullable(),
|
|
489
498
|
clientId: z.string().nullable(),
|
|
499
|
+
runtimeProvider: runtimeProviderSchema,
|
|
490
500
|
presenceStatus: presenceStatusSchema.optional(),
|
|
491
501
|
createdAt: z.string(),
|
|
492
502
|
updatedAt: z.string()
|
|
@@ -506,14 +516,16 @@ const agentPinnedMessageSchema = z.object({
|
|
|
506
516
|
agentId: z.string(),
|
|
507
517
|
name: z.string().nullable(),
|
|
508
518
|
displayName: z.string(),
|
|
509
|
-
agentType: agentTypeSchema
|
|
519
|
+
agentType: agentTypeSchema,
|
|
520
|
+
runtimeProvider: runtimeProviderSchema
|
|
510
521
|
});
|
|
511
522
|
/**
|
|
512
|
-
* Agent runtime configuration
|
|
523
|
+
* Agent runtime configuration.
|
|
513
524
|
*
|
|
514
525
|
* Defines the 5 user-tunable field groups that the Hub centrally manages
|
|
515
526
|
* and pushes down to the client runtime: prompt append, model, MCP servers,
|
|
516
|
-
* env vars, and Git repos.
|
|
527
|
+
* env vars, and Git repos. Tagged by `kind` (a runtime provider) so future
|
|
528
|
+
* provider-specific fields can land on a dedicated variant.
|
|
517
529
|
*
|
|
518
530
|
* NOTE: do not co-locate with `packages/shared/src/config/` — that namespace
|
|
519
531
|
* is reserved for the local YAML config (`agent.yaml` / server / client) and
|
|
@@ -557,9 +569,11 @@ const gitRepoSchema = z.object({
|
|
|
557
569
|
localPath: z.string().min(1).optional()
|
|
558
570
|
});
|
|
559
571
|
/**
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
-
*
|
|
572
|
+
* Untagged base shape — 5 user-tunable fields, no `kind` discriminator.
|
|
573
|
+
* Used for `.partial()` derivations on the PATCH side, where `kind` is
|
|
574
|
+
* pinned to `agents.runtime_provider` and never changes via config PATCH.
|
|
575
|
+
* Zod 4 forbids `.partial()` on a refined object, so we keep refinements
|
|
576
|
+
* on the tagged schema below.
|
|
563
577
|
*/
|
|
564
578
|
const agentRuntimeConfigPayloadShape = z.object({
|
|
565
579
|
prompt: promptConfigSchema.default({ append: "" }),
|
|
@@ -568,6 +582,17 @@ const agentRuntimeConfigPayloadShape = z.object({
|
|
|
568
582
|
env: z.array(envEntrySchema).default([]),
|
|
569
583
|
gitRepos: z.array(gitRepoSchema).default([])
|
|
570
584
|
});
|
|
585
|
+
/**
|
|
586
|
+
* Tagged variants — read-side, full payload including `kind`. Adding a new
|
|
587
|
+
* provider means adding a variant here, plus a handler factory and a
|
|
588
|
+
* capability probe module on the client side.
|
|
589
|
+
*
|
|
590
|
+
* Provider-specific fields (e.g. codex `sandboxMode`) belong on the
|
|
591
|
+
* matching variant, not on the base shape.
|
|
592
|
+
*/
|
|
593
|
+
const claudeRuntimeConfigPayloadShape = agentRuntimeConfigPayloadShape.extend({ kind: z.literal("claude-code") });
|
|
594
|
+
const codexRuntimeConfigPayloadShape = agentRuntimeConfigPayloadShape.extend({ kind: z.literal("codex") });
|
|
595
|
+
const taggedPayloadUnion = z.discriminatedUnion("kind", [claudeRuntimeConfigPayloadShape, codexRuntimeConfigPayloadShape]);
|
|
571
596
|
const payloadDuplicatesRefinement = (payload, ctx) => {
|
|
572
597
|
const seenMcp = /* @__PURE__ */ new Set();
|
|
573
598
|
payload.mcpServers.forEach((server, idx) => {
|
|
@@ -612,7 +637,21 @@ const payloadDuplicatesRefinement = (payload, ctx) => {
|
|
|
612
637
|
seenPaths.add(path);
|
|
613
638
|
});
|
|
614
639
|
};
|
|
615
|
-
|
|
640
|
+
/**
|
|
641
|
+
* Read-side full payload schema. Rows persisted before 0026 do not carry
|
|
642
|
+
* `kind`; `z.preprocess` injects `"claude-code"` so they parse cleanly into
|
|
643
|
+
* the claude variant. The service layer separately enforces
|
|
644
|
+
* `payload.kind === agents.runtime_provider` on writes.
|
|
645
|
+
*/
|
|
646
|
+
const agentRuntimeConfigPayloadSchema = z.preprocess((input) => {
|
|
647
|
+
if (input && typeof input === "object" && !Array.isArray(input) && !("kind" in input)) return {
|
|
648
|
+
...input,
|
|
649
|
+
kind: "claude-code"
|
|
650
|
+
};
|
|
651
|
+
return input;
|
|
652
|
+
}, taggedPayloadUnion).superRefine((payload, ctx) => {
|
|
653
|
+
payloadDuplicatesRefinement(payload, ctx);
|
|
654
|
+
});
|
|
616
655
|
const agentRuntimeConfigSchema = z.object({
|
|
617
656
|
agentId: z.string(),
|
|
618
657
|
version: z.number().int().positive(),
|
|
@@ -721,6 +760,37 @@ z.object({
|
|
|
721
760
|
os: z.string().max(50).optional(),
|
|
722
761
|
sdkVersion: z.string().max(50).optional()
|
|
723
762
|
});
|
|
763
|
+
const capabilityStateSchema = z.enum([
|
|
764
|
+
"ok",
|
|
765
|
+
"missing",
|
|
766
|
+
"unauthenticated",
|
|
767
|
+
"error"
|
|
768
|
+
]);
|
|
769
|
+
const capabilityAuthMethodSchema = z.enum([
|
|
770
|
+
"api_key",
|
|
771
|
+
"oauth",
|
|
772
|
+
"auth_json",
|
|
773
|
+
"none"
|
|
774
|
+
]);
|
|
775
|
+
const capabilityEntrySchema = z.object({
|
|
776
|
+
state: capabilityStateSchema,
|
|
777
|
+
available: z.boolean(),
|
|
778
|
+
authenticated: z.boolean(),
|
|
779
|
+
sdkVersion: z.string().nullable().optional(),
|
|
780
|
+
authMethod: capabilityAuthMethodSchema,
|
|
781
|
+
error: z.string().nullable().optional(),
|
|
782
|
+
detectedAt: z.string()
|
|
783
|
+
});
|
|
784
|
+
/**
|
|
785
|
+
* Capabilities snapshot keyed by runtime provider name. Recorded as a plain
|
|
786
|
+
* `Record<string, CapabilityEntry>` — every entry is optional (a client may
|
|
787
|
+
* report only the runtimes it actually probed) and the key set evolves
|
|
788
|
+
* naturally as new providers ship without a schema migration. Service-layer
|
|
789
|
+
* lookups (`agents.runtime_provider ∈ keys(capabilities)`) treat the keys
|
|
790
|
+
* as `RuntimeProvider` strings.
|
|
791
|
+
*/
|
|
792
|
+
const clientCapabilitiesSchema = z.record(z.string(), capabilityEntrySchema);
|
|
793
|
+
z.object({ capabilities: clientCapabilitiesSchema });
|
|
724
794
|
z.object({
|
|
725
795
|
limit: z.coerce.number().int().min(1).max(100).default(20),
|
|
726
796
|
cursor: z.string().optional()
|
|
@@ -1410,8 +1480,7 @@ defineConfig({
|
|
|
1410
1480
|
clientSecret: field(z.string(), {
|
|
1411
1481
|
env: "FIRST_TREE_HUB_GITHUB_OAUTH_CLIENT_SECRET",
|
|
1412
1482
|
secret: true
|
|
1413
|
-
})
|
|
1414
|
-
devCallbackEnabled: field(z.boolean().default(false), { env: "FIRST_TREE_HUB_GITHUB_OAUTH_DEV_CALLBACK" })
|
|
1483
|
+
})
|
|
1415
1484
|
}) }),
|
|
1416
1485
|
cors: optional({ origin: field(z.string(), { env: "FIRST_TREE_HUB_CORS_ORIGIN" }) }),
|
|
1417
1486
|
rateLimit: optional({
|
|
@@ -1536,6 +1605,25 @@ var FirstTreeHubSDK = class {
|
|
|
1536
1605
|
async fetchAgentConfig() {
|
|
1537
1606
|
return this.requestJson("/api/v1/agent/config");
|
|
1538
1607
|
}
|
|
1608
|
+
/**
|
|
1609
|
+
* Member-scoped: report this client's runtime-provider capabilities. The
|
|
1610
|
+
* server stores them under `clients.metadata.capabilities` after checking
|
|
1611
|
+
* that the connected member owns the client.
|
|
1612
|
+
*/
|
|
1613
|
+
async updateCapabilities(clientId, capabilities) {
|
|
1614
|
+
await this.requestVoid(`/api/v1/clients/${encodeURIComponent(clientId)}/capabilities`, {
|
|
1615
|
+
method: "PATCH",
|
|
1616
|
+
body: JSON.stringify({ capabilities })
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1620
|
+
* Member-scoped: every agent pinned to a client owned by the calling user.
|
|
1621
|
+
* Used by client startup to reconcile the local `agent.yaml::runtime` with
|
|
1622
|
+
* the authoritative `agents.runtime_provider` before spawning handlers.
|
|
1623
|
+
*/
|
|
1624
|
+
async listMyAgents() {
|
|
1625
|
+
return this.requestJson("/api/v1/clients/me/agents");
|
|
1626
|
+
}
|
|
1539
1627
|
async isHubReachable(timeoutMs = 3e3) {
|
|
1540
1628
|
try {
|
|
1541
1629
|
const url = `${this._baseUrl}/api/v1/health`;
|
|
@@ -2175,13 +2263,23 @@ function getHandlerFactory(type) {
|
|
|
2175
2263
|
}
|
|
2176
2264
|
join(DEFAULT_DATA_DIR, "context-tree");
|
|
2177
2265
|
/**
|
|
2178
|
-
*
|
|
2266
|
+
* Marker file written into every workspace so the Codex CLI's project-root
|
|
2267
|
+
* detection (configured via `project_root_markers: ["first-tree-workspace"]`)
|
|
2268
|
+
* stops at the workspace boundary instead of walking up the filesystem and
|
|
2269
|
+
* loading an unintended `AGENTS.md` from the operator's home or repo root.
|
|
2270
|
+
*/
|
|
2271
|
+
const FIRST_TREE_WORKSPACE_MARKER = ".first-tree-workspace";
|
|
2272
|
+
/**
|
|
2273
|
+
* Bootstrap a workspace with `.agent/` directory files plus the workspace
|
|
2274
|
+
* root marker (and an optional provider-specific briefing).
|
|
2179
2275
|
*
|
|
2180
|
-
* Writes identity.json, context/
|
|
2181
|
-
*
|
|
2276
|
+
* Writes identity.json, context/agent-instructions.md (if context tree
|
|
2277
|
+
* available), tools.md, the `.first-tree-workspace` marker, and — for
|
|
2278
|
+
* Codex — `AGENTS.md`. Idempotent: safe to call on every handler start()
|
|
2279
|
+
* and on resume().
|
|
2182
2280
|
*/
|
|
2183
2281
|
function bootstrapWorkspace(options) {
|
|
2184
|
-
const { workspacePath, identity, contextTreePath, serverUrl, chatId } = options;
|
|
2282
|
+
const { workspacePath, identity, contextTreePath, serverUrl, chatId, briefing } = options;
|
|
2185
2283
|
const agentDir = join(workspacePath, ".agent");
|
|
2186
2284
|
const contextDir = join(agentDir, "context");
|
|
2187
2285
|
if (existsSync(contextDir)) rmSync(contextDir, {
|
|
@@ -2207,6 +2305,8 @@ function bootstrapWorkspace(options) {
|
|
|
2207
2305
|
if (existsSync(rootNodePath)) copyFileSync(rootNodePath, join(contextDir, "domain-map.md"));
|
|
2208
2306
|
}
|
|
2209
2307
|
writeFileSync(join(agentDir, "tools.md"), generateToolsDoc(), "utf-8");
|
|
2308
|
+
writeFileSync(join(workspacePath, FIRST_TREE_WORKSPACE_MARKER), "", "utf-8");
|
|
2309
|
+
if (briefing?.format === "agents-md") writeFileSync(join(workspacePath, "AGENTS.md"), briefing.content, "utf-8");
|
|
2210
2310
|
}
|
|
2211
2311
|
function defaultInstallExec(command, args, options) {
|
|
2212
2312
|
execFileSync(command, args, {
|
|
@@ -2932,7 +3032,7 @@ function splitPathExt(pathext) {
|
|
|
2932
3032
|
}
|
|
2933
3033
|
const MAX_RETRIES = 2;
|
|
2934
3034
|
const TOOL_RESULT_PREVIEW_LIMIT = 400;
|
|
2935
|
-
const ASSISTANT_TEXT_EVENT_LIMIT = 8e3;
|
|
3035
|
+
const ASSISTANT_TEXT_EVENT_LIMIT$1 = 8e3;
|
|
2936
3036
|
const SUPPORTED_IMAGE_MIMES = new Set(SUPPORTED_IMAGE_MIMES$1);
|
|
2937
3037
|
const MIME_TO_EXT = {
|
|
2938
3038
|
"image/png": "png",
|
|
@@ -3058,7 +3158,7 @@ function createToolCallProcessor(emit) {
|
|
|
3058
3158
|
if (text.length === 0) continue;
|
|
3059
3159
|
emit({
|
|
3060
3160
|
kind: "assistant_text",
|
|
3061
|
-
payload: { text: text.slice(0, ASSISTANT_TEXT_EVENT_LIMIT) }
|
|
3161
|
+
payload: { text: text.slice(0, ASSISTANT_TEXT_EVENT_LIMIT$1) }
|
|
3062
3162
|
});
|
|
3063
3163
|
} else if (isThinkingBlock(block)) emit({
|
|
3064
3164
|
kind: "thinking",
|
|
@@ -3602,6 +3702,457 @@ function generateClaudeMd(workspacePath, identity, contextTreePath) {
|
|
|
3602
3702
|
}
|
|
3603
3703
|
writeFileSync(join(workspacePath, "CLAUDE.md"), sections.join("\n"), "utf-8");
|
|
3604
3704
|
}
|
|
3705
|
+
const ASSISTANT_TEXT_EVENT_LIMIT = 8e3;
|
|
3706
|
+
const RESULT_PREVIEW_LIMIT = 400;
|
|
3707
|
+
/**
|
|
3708
|
+
* Build the per-turn `ThreadOptions` Codex consumes. Exported so unit tests
|
|
3709
|
+
* can lock the auth-mode-friendly defaults (notably `model` only set when
|
|
3710
|
+
* the operator chose one).
|
|
3711
|
+
*/
|
|
3712
|
+
function buildCodexThreadOptions(payload, workspaceCwd) {
|
|
3713
|
+
const additionalDirectories = [];
|
|
3714
|
+
for (const repo of payload.gitRepos) {
|
|
3715
|
+
const localPath = repo.localPath ?? deriveRepoLocalPath(repo.url);
|
|
3716
|
+
if (!localPath) continue;
|
|
3717
|
+
additionalDirectories.push(join(workspaceCwd, localPath));
|
|
3718
|
+
}
|
|
3719
|
+
const opts = {
|
|
3720
|
+
workingDirectory: workspaceCwd,
|
|
3721
|
+
skipGitRepoCheck: true,
|
|
3722
|
+
sandboxMode: "workspace-write",
|
|
3723
|
+
approvalPolicy: "never",
|
|
3724
|
+
modelReasoningEffort: "high",
|
|
3725
|
+
webSearchEnabled: false,
|
|
3726
|
+
additionalDirectories
|
|
3727
|
+
};
|
|
3728
|
+
if (payload.model) opts.model = payload.model;
|
|
3729
|
+
return opts;
|
|
3730
|
+
}
|
|
3731
|
+
/**
|
|
3732
|
+
* Codex Handler — session-oriented handler using `@openai/codex-sdk`.
|
|
3733
|
+
*
|
|
3734
|
+
* Each instance owns one Thread for one chat. Each turn is a fresh
|
|
3735
|
+
* `runStreamed()` call (Codex CLI is run-to-completion per turn). Inject
|
|
3736
|
+
* during an active turn buffers messages and runs them as a follow-up turn
|
|
3737
|
+
* the moment the current one completes.
|
|
3738
|
+
*
|
|
3739
|
+
* Key footguns observed end-to-end (private plan §10.7):
|
|
3740
|
+
* - F1: providing `env` to Codex SDK does NOT inherit `process.env`; we
|
|
3741
|
+
* explicitly merge.
|
|
3742
|
+
* - F2: `resumeThread(id)` does NOT inherit `ThreadOptions`; we re-pass
|
|
3743
|
+
* them every time.
|
|
3744
|
+
* - F3: `modelReasoningEffort: "minimal"` is incompatible with default
|
|
3745
|
+
* tools; we default to `"high"` with `webSearchEnabled: false`.
|
|
3746
|
+
* - F6: `Thread` has no close/dispose — shutdown is exclusively
|
|
3747
|
+
* `AbortController.abort()`.
|
|
3748
|
+
*/
|
|
3749
|
+
const createCodexHandler = (config) => {
|
|
3750
|
+
const workspaceRoot = config.workspaceRoot;
|
|
3751
|
+
const agentConfigCache = config.agentConfigCache ?? null;
|
|
3752
|
+
const gitMirrorManager = config.gitMirrorManager ?? null;
|
|
3753
|
+
const contextTreePath = config.contextTreePath ?? null;
|
|
3754
|
+
let cwd = null;
|
|
3755
|
+
let codex = null;
|
|
3756
|
+
let thread = null;
|
|
3757
|
+
let threadId = null;
|
|
3758
|
+
let currentAbort = null;
|
|
3759
|
+
let currentTurnPromise = null;
|
|
3760
|
+
let ctx = null;
|
|
3761
|
+
let drainScheduled = false;
|
|
3762
|
+
const queuedMessages = [];
|
|
3763
|
+
const ownedWorktrees = [];
|
|
3764
|
+
function buildEnv(sessionCtx) {
|
|
3765
|
+
const env = {};
|
|
3766
|
+
for (const [k, v] of Object.entries(process.env)) if (typeof v === "string") env[k] = v;
|
|
3767
|
+
const payload = agentConfigCache?.get(sessionCtx.agent.agentId)?.payload;
|
|
3768
|
+
if (payload) for (const e of payload.env) env[e.key] = e.value;
|
|
3769
|
+
const merged = sessionCtx.buildAgentEnv(env);
|
|
3770
|
+
const out = {};
|
|
3771
|
+
for (const [k, v] of Object.entries(merged)) if (typeof v === "string") out[k] = v;
|
|
3772
|
+
return out;
|
|
3773
|
+
}
|
|
3774
|
+
function buildCodexConfig(payload) {
|
|
3775
|
+
const cfg = { project_root_markers: [FIRST_TREE_WORKSPACE_MARKER] };
|
|
3776
|
+
if (payload.mcpServers.length === 0) return cfg;
|
|
3777
|
+
const mcpServers = {};
|
|
3778
|
+
for (const m of payload.mcpServers) if (m.transport === "stdio") mcpServers[m.name] = {
|
|
3779
|
+
command: m.command,
|
|
3780
|
+
args: m.args ?? []
|
|
3781
|
+
};
|
|
3782
|
+
else {
|
|
3783
|
+
const entry = { url: m.url };
|
|
3784
|
+
if (m.headers) entry.headers = m.headers;
|
|
3785
|
+
mcpServers[m.name] = entry;
|
|
3786
|
+
}
|
|
3787
|
+
cfg.mcp_servers = mcpServers;
|
|
3788
|
+
return cfg;
|
|
3789
|
+
}
|
|
3790
|
+
function buildAgentBriefing(payload) {
|
|
3791
|
+
const lines = [];
|
|
3792
|
+
lines.push("# Agent Briefing");
|
|
3793
|
+
lines.push("");
|
|
3794
|
+
if (payload.prompt.append.trim()) {
|
|
3795
|
+
lines.push(payload.prompt.append.trim());
|
|
3796
|
+
lines.push("");
|
|
3797
|
+
}
|
|
3798
|
+
lines.push("Refer to `.agent/identity.json` for your agent identity, `.agent/tools.md` for the");
|
|
3799
|
+
lines.push("first-tree-hub SDK reference, and `.agent/context/` for organisational context");
|
|
3800
|
+
lines.push("(when configured).");
|
|
3801
|
+
return lines.join("\n").concat("\n");
|
|
3802
|
+
}
|
|
3803
|
+
function toCodexInput(message, sessionCtx) {
|
|
3804
|
+
return sessionCtx.formatInboundContent(message).then((text) => text);
|
|
3805
|
+
}
|
|
3806
|
+
async function prepareGitWorktrees(payload, workspaceCwd, chatId) {
|
|
3807
|
+
if (!gitMirrorManager) return;
|
|
3808
|
+
for (const repo of payload.gitRepos) {
|
|
3809
|
+
const localPath = repo.localPath ?? deriveRepoLocalPath(repo.url);
|
|
3810
|
+
if (!localPath) continue;
|
|
3811
|
+
const targetPath = join(workspaceCwd, localPath);
|
|
3812
|
+
if (existsSync(targetPath)) continue;
|
|
3813
|
+
try {
|
|
3814
|
+
await gitMirrorManager.ensureMirror(repo.url);
|
|
3815
|
+
await gitMirrorManager.fetchMirror(repo.url);
|
|
3816
|
+
const result = await gitMirrorManager.createWorktree({
|
|
3817
|
+
url: repo.url,
|
|
3818
|
+
ref: repo.ref,
|
|
3819
|
+
targetPath,
|
|
3820
|
+
sessionKey: chatId
|
|
3821
|
+
});
|
|
3822
|
+
ownedWorktrees.push({
|
|
3823
|
+
url: repo.url,
|
|
3824
|
+
path: targetPath,
|
|
3825
|
+
branchName: result.branchName
|
|
3826
|
+
});
|
|
3827
|
+
} catch (err) {
|
|
3828
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3829
|
+
ctx?.log(`codex git materialisation skipped (${repo.url}): ${msg}`);
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
function emitToolCall(sessionCtx, payload) {
|
|
3834
|
+
const event = {
|
|
3835
|
+
kind: "tool_call",
|
|
3836
|
+
payload: {
|
|
3837
|
+
toolUseId: payload.toolUseId,
|
|
3838
|
+
name: payload.name,
|
|
3839
|
+
args: payload.args,
|
|
3840
|
+
status: payload.status,
|
|
3841
|
+
...payload.resultPreview ? { resultPreview: payload.resultPreview.slice(0, RESULT_PREVIEW_LIMIT) } : {}
|
|
3842
|
+
}
|
|
3843
|
+
};
|
|
3844
|
+
sessionCtx.emitEvent(event);
|
|
3845
|
+
}
|
|
3846
|
+
/**
|
|
3847
|
+
* Translate one terminal `item.completed` payload into the runtime's event
|
|
3848
|
+
* stream and, when the item is the assistant's final message, return the
|
|
3849
|
+
* raw text so `runTurn` can stitch the per-turn reply together.
|
|
3850
|
+
*/
|
|
3851
|
+
function processItem(item, sessionCtx) {
|
|
3852
|
+
switch (item.type) {
|
|
3853
|
+
case "agent_message":
|
|
3854
|
+
if (!item.text.trim()) return "";
|
|
3855
|
+
sessionCtx.emitEvent({
|
|
3856
|
+
kind: "assistant_text",
|
|
3857
|
+
payload: { text: item.text.slice(0, ASSISTANT_TEXT_EVENT_LIMIT) }
|
|
3858
|
+
});
|
|
3859
|
+
return item.text;
|
|
3860
|
+
case "command_execution": {
|
|
3861
|
+
const status = item.status === "completed" ? "ok" : item.status === "failed" ? "error" : "pending";
|
|
3862
|
+
emitToolCall(sessionCtx, {
|
|
3863
|
+
toolUseId: item.id,
|
|
3864
|
+
name: "command",
|
|
3865
|
+
args: { command: item.command },
|
|
3866
|
+
status,
|
|
3867
|
+
resultPreview: item.aggregated_output
|
|
3868
|
+
});
|
|
3869
|
+
return "";
|
|
3870
|
+
}
|
|
3871
|
+
case "file_change": {
|
|
3872
|
+
const status = item.status === "completed" ? "ok" : "error";
|
|
3873
|
+
emitToolCall(sessionCtx, {
|
|
3874
|
+
toolUseId: item.id,
|
|
3875
|
+
name: "file_change",
|
|
3876
|
+
args: { changes: item.changes },
|
|
3877
|
+
status
|
|
3878
|
+
});
|
|
3879
|
+
return "";
|
|
3880
|
+
}
|
|
3881
|
+
case "mcp_tool_call": {
|
|
3882
|
+
const status = item.status === "completed" ? "ok" : item.status === "failed" ? "error" : "pending";
|
|
3883
|
+
const resultPreview = item.error ? `error: ${item.error.message}` : item.result ? JSON.stringify(item.result.structured_content ?? item.result.content) : void 0;
|
|
3884
|
+
emitToolCall(sessionCtx, {
|
|
3885
|
+
toolUseId: item.id,
|
|
3886
|
+
name: `mcp:${item.server}/${item.tool}`,
|
|
3887
|
+
args: item.arguments,
|
|
3888
|
+
status,
|
|
3889
|
+
resultPreview
|
|
3890
|
+
});
|
|
3891
|
+
return "";
|
|
3892
|
+
}
|
|
3893
|
+
case "web_search":
|
|
3894
|
+
emitToolCall(sessionCtx, {
|
|
3895
|
+
toolUseId: item.id,
|
|
3896
|
+
name: "web_search",
|
|
3897
|
+
args: { query: item.query },
|
|
3898
|
+
status: "ok"
|
|
3899
|
+
});
|
|
3900
|
+
return "";
|
|
3901
|
+
case "todo_list":
|
|
3902
|
+
emitToolCall(sessionCtx, {
|
|
3903
|
+
toolUseId: item.id,
|
|
3904
|
+
name: "todo_list",
|
|
3905
|
+
args: { items: item.items },
|
|
3906
|
+
status: "ok"
|
|
3907
|
+
});
|
|
3908
|
+
return "";
|
|
3909
|
+
case "reasoning":
|
|
3910
|
+
sessionCtx.emitEvent({
|
|
3911
|
+
kind: "thinking",
|
|
3912
|
+
payload: {}
|
|
3913
|
+
});
|
|
3914
|
+
return "";
|
|
3915
|
+
case "error":
|
|
3916
|
+
sessionCtx.emitEvent({
|
|
3917
|
+
kind: "error",
|
|
3918
|
+
payload: {
|
|
3919
|
+
source: "tool",
|
|
3920
|
+
message: item.message
|
|
3921
|
+
}
|
|
3922
|
+
});
|
|
3923
|
+
return "";
|
|
3924
|
+
default: return "";
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
async function runTurn(input, sessionCtx) {
|
|
3928
|
+
const activeThread = thread;
|
|
3929
|
+
if (!activeThread) return;
|
|
3930
|
+
const abort = new AbortController();
|
|
3931
|
+
currentAbort = abort;
|
|
3932
|
+
sessionCtx.setRuntimeState("working");
|
|
3933
|
+
const assistantTexts = [];
|
|
3934
|
+
let turnFailed = false;
|
|
3935
|
+
const promise = (async () => {
|
|
3936
|
+
try {
|
|
3937
|
+
const streamed = await activeThread.runStreamed(input, { signal: abort.signal });
|
|
3938
|
+
for await (const event of streamed.events) {
|
|
3939
|
+
if (abort.signal.aborted) break;
|
|
3940
|
+
sessionCtx.touch();
|
|
3941
|
+
if (event.type === "thread.started") threadId = event.thread_id;
|
|
3942
|
+
else if (event.type === "turn.started") {} else if (event.type === "item.completed") {
|
|
3943
|
+
const text = processItem(event.item, sessionCtx);
|
|
3944
|
+
if (text) assistantTexts.push(text);
|
|
3945
|
+
} else if (event.type === "item.started" || event.type === "item.updated") {} else if (event.type === "turn.completed") {} else if (event.type === "turn.failed") {
|
|
3946
|
+
turnFailed = true;
|
|
3947
|
+
sessionCtx.emitEvent({
|
|
3948
|
+
kind: "error",
|
|
3949
|
+
payload: {
|
|
3950
|
+
source: "sdk",
|
|
3951
|
+
message: event.error.message
|
|
3952
|
+
}
|
|
3953
|
+
});
|
|
3954
|
+
} else if (event.type === "error") sessionCtx.emitEvent({
|
|
3955
|
+
kind: "error",
|
|
3956
|
+
payload: {
|
|
3957
|
+
source: "sdk",
|
|
3958
|
+
message: event.message
|
|
3959
|
+
}
|
|
3960
|
+
});
|
|
3961
|
+
}
|
|
3962
|
+
} catch (err) {
|
|
3963
|
+
if (abort.signal.aborted) return;
|
|
3964
|
+
turnFailed = true;
|
|
3965
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3966
|
+
sessionCtx.emitEvent({
|
|
3967
|
+
kind: "error",
|
|
3968
|
+
payload: {
|
|
3969
|
+
source: "sdk",
|
|
3970
|
+
message: msg
|
|
3971
|
+
}
|
|
3972
|
+
});
|
|
3973
|
+
}
|
|
3974
|
+
})();
|
|
3975
|
+
currentTurnPromise = promise;
|
|
3976
|
+
try {
|
|
3977
|
+
await promise;
|
|
3978
|
+
} finally {
|
|
3979
|
+
currentAbort = null;
|
|
3980
|
+
currentTurnPromise = null;
|
|
3981
|
+
}
|
|
3982
|
+
if (abort.signal.aborted) return;
|
|
3983
|
+
const accumulated = assistantTexts.join("\n\n");
|
|
3984
|
+
let forwardFailed = false;
|
|
3985
|
+
if (accumulated.trim()) try {
|
|
3986
|
+
await sessionCtx.forwardResult(accumulated);
|
|
3987
|
+
} catch (err) {
|
|
3988
|
+
forwardFailed = true;
|
|
3989
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3990
|
+
sessionCtx.emitEvent({
|
|
3991
|
+
kind: "error",
|
|
3992
|
+
payload: {
|
|
3993
|
+
source: "runtime",
|
|
3994
|
+
message: `forwardResult failed: ${msg}`
|
|
3995
|
+
}
|
|
3996
|
+
});
|
|
3997
|
+
}
|
|
3998
|
+
const succeeded = !turnFailed && !forwardFailed;
|
|
3999
|
+
sessionCtx.emitEvent({
|
|
4000
|
+
kind: "turn_end",
|
|
4001
|
+
payload: { status: succeeded ? "success" : "error" }
|
|
4002
|
+
});
|
|
4003
|
+
if (succeeded && accumulated.trim()) sessionCtx.reportSessionCompletion();
|
|
4004
|
+
sessionCtx.setRuntimeState("idle");
|
|
4005
|
+
if (queuedMessages.length > 0 && !drainScheduled) {
|
|
4006
|
+
drainScheduled = true;
|
|
4007
|
+
setImmediate(() => {
|
|
4008
|
+
drainScheduled = false;
|
|
4009
|
+
const drained = queuedMessages.splice(0);
|
|
4010
|
+
if (drained.length === 0 || !ctx || !thread) return;
|
|
4011
|
+
mergeAndRun(drained, ctx);
|
|
4012
|
+
});
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
4015
|
+
async function mergeAndRun(drained, sessionCtx) {
|
|
4016
|
+
const inputs = [];
|
|
4017
|
+
for (const m of drained) try {
|
|
4018
|
+
inputs.push(await sessionCtx.formatInboundContent(m));
|
|
4019
|
+
} catch (err) {
|
|
4020
|
+
sessionCtx.log(`codex inject formatInboundContent failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4021
|
+
}
|
|
4022
|
+
if (inputs.length === 0) return;
|
|
4023
|
+
await runTurn(inputs.join("\n\n"), sessionCtx);
|
|
4024
|
+
}
|
|
4025
|
+
return {
|
|
4026
|
+
async start(message, sessionCtx) {
|
|
4027
|
+
ctx = sessionCtx;
|
|
4028
|
+
cwd = await acquireWorkspace(workspaceRoot, sessionCtx.chatId);
|
|
4029
|
+
let payload = null;
|
|
4030
|
+
if (agentConfigCache) payload = (await agentConfigCache.refresh(sessionCtx.agent.agentId)).payload;
|
|
4031
|
+
if (!payload) payload = {
|
|
4032
|
+
kind: "codex",
|
|
4033
|
+
prompt: { append: "" },
|
|
4034
|
+
model: "",
|
|
4035
|
+
mcpServers: [],
|
|
4036
|
+
env: [],
|
|
4037
|
+
gitRepos: []
|
|
4038
|
+
};
|
|
4039
|
+
bootstrapWorkspace({
|
|
4040
|
+
workspacePath: cwd,
|
|
4041
|
+
identity: sessionCtx.agent,
|
|
4042
|
+
contextTreePath,
|
|
4043
|
+
serverUrl: sessionCtx.sdk.serverUrl,
|
|
4044
|
+
chatId: sessionCtx.chatId,
|
|
4045
|
+
briefing: {
|
|
4046
|
+
format: "agents-md",
|
|
4047
|
+
content: buildAgentBriefing(payload)
|
|
4048
|
+
}
|
|
4049
|
+
});
|
|
4050
|
+
await prepareGitWorktrees(payload, cwd, sessionCtx.chatId);
|
|
4051
|
+
codex = new Codex({
|
|
4052
|
+
env: buildEnv(sessionCtx),
|
|
4053
|
+
config: buildCodexConfig(payload)
|
|
4054
|
+
});
|
|
4055
|
+
thread = codex.startThread(buildCodexThreadOptions(payload, cwd));
|
|
4056
|
+
await runTurn(await toCodexInput(message, sessionCtx), sessionCtx);
|
|
4057
|
+
if (!threadId) threadId = thread.id ?? null;
|
|
4058
|
+
if (!threadId) throw new Error("codex did not assign a thread id during the first turn");
|
|
4059
|
+
return threadId;
|
|
4060
|
+
},
|
|
4061
|
+
async resume(message, sessionId, sessionCtx) {
|
|
4062
|
+
ctx = sessionCtx;
|
|
4063
|
+
cwd = await acquireWorkspace(workspaceRoot, sessionCtx.chatId);
|
|
4064
|
+
let payload = null;
|
|
4065
|
+
if (agentConfigCache) payload = (await agentConfigCache.refresh(sessionCtx.agent.agentId)).payload;
|
|
4066
|
+
if (!payload) payload = {
|
|
4067
|
+
kind: "codex",
|
|
4068
|
+
prompt: { append: "" },
|
|
4069
|
+
model: "",
|
|
4070
|
+
mcpServers: [],
|
|
4071
|
+
env: [],
|
|
4072
|
+
gitRepos: []
|
|
4073
|
+
};
|
|
4074
|
+
bootstrapWorkspace({
|
|
4075
|
+
workspacePath: cwd,
|
|
4076
|
+
identity: sessionCtx.agent,
|
|
4077
|
+
contextTreePath,
|
|
4078
|
+
serverUrl: sessionCtx.sdk.serverUrl,
|
|
4079
|
+
chatId: sessionCtx.chatId,
|
|
4080
|
+
briefing: {
|
|
4081
|
+
format: "agents-md",
|
|
4082
|
+
content: buildAgentBriefing(payload)
|
|
4083
|
+
}
|
|
4084
|
+
});
|
|
4085
|
+
await prepareGitWorktrees(payload, cwd, sessionCtx.chatId);
|
|
4086
|
+
codex = new Codex({
|
|
4087
|
+
env: buildEnv(sessionCtx),
|
|
4088
|
+
config: buildCodexConfig(payload)
|
|
4089
|
+
});
|
|
4090
|
+
thread = codex.resumeThread(sessionId, buildCodexThreadOptions(payload, cwd));
|
|
4091
|
+
threadId = sessionId;
|
|
4092
|
+
if (message) await runTurn(await toCodexInput(message, sessionCtx), sessionCtx);
|
|
4093
|
+
return sessionId;
|
|
4094
|
+
},
|
|
4095
|
+
inject(message) {
|
|
4096
|
+
if (currentTurnPromise) {
|
|
4097
|
+
queuedMessages.push(message);
|
|
4098
|
+
return;
|
|
4099
|
+
}
|
|
4100
|
+
const sessionCtx = ctx;
|
|
4101
|
+
if (!sessionCtx || !thread) return;
|
|
4102
|
+
(async () => {
|
|
4103
|
+
try {
|
|
4104
|
+
await runTurn(await toCodexInput(message, sessionCtx), sessionCtx);
|
|
4105
|
+
} catch (err) {
|
|
4106
|
+
sessionCtx.log(`codex inject failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4107
|
+
}
|
|
4108
|
+
})();
|
|
4109
|
+
},
|
|
4110
|
+
async suspend() {
|
|
4111
|
+
currentAbort?.abort();
|
|
4112
|
+
try {
|
|
4113
|
+
await currentTurnPromise;
|
|
4114
|
+
} catch {}
|
|
4115
|
+
currentAbort = null;
|
|
4116
|
+
currentTurnPromise = null;
|
|
4117
|
+
thread = null;
|
|
4118
|
+
codex = null;
|
|
4119
|
+
},
|
|
4120
|
+
async shutdown() {
|
|
4121
|
+
currentAbort?.abort();
|
|
4122
|
+
try {
|
|
4123
|
+
await currentTurnPromise;
|
|
4124
|
+
} catch {}
|
|
4125
|
+
currentAbort = null;
|
|
4126
|
+
currentTurnPromise = null;
|
|
4127
|
+
thread = null;
|
|
4128
|
+
codex = null;
|
|
4129
|
+
if (gitMirrorManager) {
|
|
4130
|
+
for (const wt of ownedWorktrees) try {
|
|
4131
|
+
await gitMirrorManager.removeWorktree({
|
|
4132
|
+
url: wt.url,
|
|
4133
|
+
path: wt.path,
|
|
4134
|
+
branchName: wt.branchName
|
|
4135
|
+
});
|
|
4136
|
+
} catch (err) {
|
|
4137
|
+
ctx?.log(`codex worktree cleanup failed (${wt.path}): ${err instanceof Error ? err.message : String(err)}`);
|
|
4138
|
+
}
|
|
4139
|
+
ownedWorktrees.length = 0;
|
|
4140
|
+
}
|
|
4141
|
+
if (cwd && existsSync(cwd)) try {
|
|
4142
|
+
rmSync(cwd, {
|
|
4143
|
+
recursive: true,
|
|
4144
|
+
force: true
|
|
4145
|
+
});
|
|
4146
|
+
} catch (err) {
|
|
4147
|
+
ctx?.log(`codex workspace cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4148
|
+
}
|
|
4149
|
+
cwd = null;
|
|
4150
|
+
threadId = null;
|
|
4151
|
+
ctx = null;
|
|
4152
|
+
queuedMessages.length = 0;
|
|
4153
|
+
}
|
|
4154
|
+
};
|
|
4155
|
+
};
|
|
3605
4156
|
/** Register all built-in handlers. Call once at startup. */
|
|
3606
4157
|
function registerBuiltinHandlers() {
|
|
3607
4158
|
const resolution = resolveClaudeCodeExecutable();
|
|
@@ -3611,6 +4162,7 @@ function registerBuiltinHandlers() {
|
|
|
3611
4162
|
...config,
|
|
3612
4163
|
claudeCodeExecutable: resolution.path
|
|
3613
4164
|
}));
|
|
4165
|
+
registerHandler("codex", (config) => createCodexHandler(config));
|
|
3614
4166
|
}
|
|
3615
4167
|
function createAgentConfigCache(opts) {
|
|
3616
4168
|
const { sdk } = opts;
|
|
@@ -3645,6 +4197,7 @@ function createAgentConfigCache(opts) {
|
|
|
3645
4197
|
agentId,
|
|
3646
4198
|
version: 0,
|
|
3647
4199
|
payload: {
|
|
4200
|
+
kind: "claude-code",
|
|
3648
4201
|
prompt: { append: "" },
|
|
3649
4202
|
model: "",
|
|
3650
4203
|
mcpServers: [],
|
|
@@ -4662,6 +5215,226 @@ var AgentSlot = class {
|
|
|
4662
5215
|
}
|
|
4663
5216
|
};
|
|
4664
5217
|
/**
|
|
5218
|
+
* Top-level marker file Claude Code writes after a successful OAuth login.
|
|
5219
|
+
* Path is platform-agnostic (`~/.claude.json`); the access token itself lives
|
|
5220
|
+
* in the platform credential store (macOS Keychain entry "Claude Code-
|
|
5221
|
+
* credentials", or libsecret on Linux), so we treat the presence of an
|
|
5222
|
+
* `oauthAccount.accountUuid` field as the canonical "logged in" signal.
|
|
5223
|
+
*/
|
|
5224
|
+
const CLAUDE_PROFILE_PATH = () => join(homedir(), ".claude.json");
|
|
5225
|
+
function hasClaudeOAuthAccount() {
|
|
5226
|
+
try {
|
|
5227
|
+
const path = CLAUDE_PROFILE_PATH();
|
|
5228
|
+
if (!existsSync(path)) return false;
|
|
5229
|
+
const raw = readFileSync(path, "utf-8");
|
|
5230
|
+
const obj = JSON.parse(raw);
|
|
5231
|
+
return typeof obj.oauthAccount?.accountUuid === "string" && obj.oauthAccount.accountUuid.length > 0;
|
|
5232
|
+
} catch {
|
|
5233
|
+
return false;
|
|
5234
|
+
}
|
|
5235
|
+
}
|
|
5236
|
+
async function readSdkVersion$1() {
|
|
5237
|
+
try {
|
|
5238
|
+
let dir = dirname(fileURLToPath(await import.meta.resolve("@anthropic-ai/claude-agent-sdk")));
|
|
5239
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
5240
|
+
const candidate = join(dir, "package.json");
|
|
5241
|
+
if (existsSync(candidate)) {
|
|
5242
|
+
const pkg = JSON.parse(readFileSync(candidate, "utf-8"));
|
|
5243
|
+
if (pkg.name === "@anthropic-ai/claude-agent-sdk" && typeof pkg.version === "string") return pkg.version;
|
|
5244
|
+
}
|
|
5245
|
+
const parent = dirname(dir);
|
|
5246
|
+
if (parent === dir) break;
|
|
5247
|
+
dir = parent;
|
|
5248
|
+
}
|
|
5249
|
+
} catch {}
|
|
5250
|
+
return null;
|
|
5251
|
+
}
|
|
5252
|
+
function detectAuth$1() {
|
|
5253
|
+
if (process.env.ANTHROPIC_API_KEY && process.env.ANTHROPIC_API_KEY.length > 0) return {
|
|
5254
|
+
authenticated: true,
|
|
5255
|
+
method: "api_key"
|
|
5256
|
+
};
|
|
5257
|
+
if (hasClaudeOAuthAccount()) return {
|
|
5258
|
+
authenticated: true,
|
|
5259
|
+
method: "oauth"
|
|
5260
|
+
};
|
|
5261
|
+
return {
|
|
5262
|
+
authenticated: false,
|
|
5263
|
+
method: "none"
|
|
5264
|
+
};
|
|
5265
|
+
}
|
|
5266
|
+
/**
|
|
5267
|
+
* Probe whether the Claude Code runtime is usable on this machine.
|
|
5268
|
+
*
|
|
5269
|
+
* `state` is the authoritative field; `available` and `authenticated` are
|
|
5270
|
+
* derived booleans kept around for simple consumers (e.g. capability lookup
|
|
5271
|
+
* in service-layer guards).
|
|
5272
|
+
*/
|
|
5273
|
+
async function probeClaudeCodeCapability() {
|
|
5274
|
+
const detectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5275
|
+
try {
|
|
5276
|
+
let sdkPresent = false;
|
|
5277
|
+
try {
|
|
5278
|
+
await import("@anthropic-ai/claude-agent-sdk");
|
|
5279
|
+
sdkPresent = true;
|
|
5280
|
+
} catch {
|
|
5281
|
+
sdkPresent = false;
|
|
5282
|
+
}
|
|
5283
|
+
if (!sdkPresent) return {
|
|
5284
|
+
state: "missing",
|
|
5285
|
+
available: false,
|
|
5286
|
+
authenticated: false,
|
|
5287
|
+
sdkVersion: null,
|
|
5288
|
+
authMethod: "none",
|
|
5289
|
+
detectedAt
|
|
5290
|
+
};
|
|
5291
|
+
const sdkVersion = await readSdkVersion$1();
|
|
5292
|
+
const auth = detectAuth$1();
|
|
5293
|
+
if (!auth.authenticated) return {
|
|
5294
|
+
state: "unauthenticated",
|
|
5295
|
+
available: true,
|
|
5296
|
+
authenticated: false,
|
|
5297
|
+
sdkVersion,
|
|
5298
|
+
authMethod: "none",
|
|
5299
|
+
detectedAt
|
|
5300
|
+
};
|
|
5301
|
+
return {
|
|
5302
|
+
state: "ok",
|
|
5303
|
+
available: true,
|
|
5304
|
+
authenticated: true,
|
|
5305
|
+
sdkVersion,
|
|
5306
|
+
authMethod: auth.method,
|
|
5307
|
+
detectedAt
|
|
5308
|
+
};
|
|
5309
|
+
} catch (err) {
|
|
5310
|
+
return {
|
|
5311
|
+
state: "error",
|
|
5312
|
+
available: false,
|
|
5313
|
+
authenticated: false,
|
|
5314
|
+
sdkVersion: null,
|
|
5315
|
+
authMethod: "none",
|
|
5316
|
+
error: err instanceof Error ? err.message : String(err),
|
|
5317
|
+
detectedAt
|
|
5318
|
+
};
|
|
5319
|
+
}
|
|
5320
|
+
}
|
|
5321
|
+
function codexAuthPath() {
|
|
5322
|
+
return join(process.env.CODEX_HOME ?? join(homedir(), ".codex"), "auth.json");
|
|
5323
|
+
}
|
|
5324
|
+
async function readSdkVersion() {
|
|
5325
|
+
try {
|
|
5326
|
+
let dir = dirname(fileURLToPath(await import.meta.resolve("@openai/codex-sdk")));
|
|
5327
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
5328
|
+
const candidate = join(dir, "package.json");
|
|
5329
|
+
if (existsSync(candidate)) {
|
|
5330
|
+
const pkg = JSON.parse(readFileSync(candidate, "utf-8"));
|
|
5331
|
+
if (pkg.name === "@openai/codex-sdk" && typeof pkg.version === "string") return pkg.version;
|
|
5332
|
+
}
|
|
5333
|
+
const parent = dirname(dir);
|
|
5334
|
+
if (parent === dir) break;
|
|
5335
|
+
dir = parent;
|
|
5336
|
+
}
|
|
5337
|
+
} catch {}
|
|
5338
|
+
return null;
|
|
5339
|
+
}
|
|
5340
|
+
function detectAuth() {
|
|
5341
|
+
if (process.env.CODEX_API_KEY && process.env.CODEX_API_KEY.length > 0) return {
|
|
5342
|
+
authenticated: true,
|
|
5343
|
+
method: "api_key"
|
|
5344
|
+
};
|
|
5345
|
+
if (existsSync(codexAuthPath())) return {
|
|
5346
|
+
authenticated: true,
|
|
5347
|
+
method: "auth_json"
|
|
5348
|
+
};
|
|
5349
|
+
return {
|
|
5350
|
+
authenticated: false,
|
|
5351
|
+
method: "none"
|
|
5352
|
+
};
|
|
5353
|
+
}
|
|
5354
|
+
/**
|
|
5355
|
+
* Probe whether the OpenAI Codex runtime is usable on this machine.
|
|
5356
|
+
* Treats `~/.codex/auth.json` (set by `codex login`) as the canonical local
|
|
5357
|
+
* auth source; CODEX_API_KEY env shortcuts that for ephemeral use.
|
|
5358
|
+
*/
|
|
5359
|
+
async function probeCodexCapability() {
|
|
5360
|
+
const detectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5361
|
+
try {
|
|
5362
|
+
let sdkPresent = false;
|
|
5363
|
+
try {
|
|
5364
|
+
await import("@openai/codex-sdk");
|
|
5365
|
+
sdkPresent = true;
|
|
5366
|
+
} catch {
|
|
5367
|
+
sdkPresent = false;
|
|
5368
|
+
}
|
|
5369
|
+
if (!sdkPresent) return {
|
|
5370
|
+
state: "missing",
|
|
5371
|
+
available: false,
|
|
5372
|
+
authenticated: false,
|
|
5373
|
+
sdkVersion: null,
|
|
5374
|
+
authMethod: "none",
|
|
5375
|
+
detectedAt
|
|
5376
|
+
};
|
|
5377
|
+
const sdkVersion = await readSdkVersion();
|
|
5378
|
+
const auth = detectAuth();
|
|
5379
|
+
if (!auth.authenticated) return {
|
|
5380
|
+
state: "unauthenticated",
|
|
5381
|
+
available: true,
|
|
5382
|
+
authenticated: false,
|
|
5383
|
+
sdkVersion,
|
|
5384
|
+
authMethod: "none",
|
|
5385
|
+
detectedAt
|
|
5386
|
+
};
|
|
5387
|
+
return {
|
|
5388
|
+
state: "ok",
|
|
5389
|
+
available: true,
|
|
5390
|
+
authenticated: true,
|
|
5391
|
+
sdkVersion,
|
|
5392
|
+
authMethod: auth.method,
|
|
5393
|
+
detectedAt
|
|
5394
|
+
};
|
|
5395
|
+
} catch (err) {
|
|
5396
|
+
return {
|
|
5397
|
+
state: "error",
|
|
5398
|
+
available: false,
|
|
5399
|
+
authenticated: false,
|
|
5400
|
+
sdkVersion: null,
|
|
5401
|
+
authMethod: "none",
|
|
5402
|
+
error: err instanceof Error ? err.message : String(err),
|
|
5403
|
+
detectedAt
|
|
5404
|
+
};
|
|
5405
|
+
}
|
|
5406
|
+
}
|
|
5407
|
+
/**
|
|
5408
|
+
* Run every built-in capability probe and aggregate the results.
|
|
5409
|
+
*
|
|
5410
|
+
* Each provider gets its own module under this directory; the orchestrator
|
|
5411
|
+
* is intentionally simple — adding a new provider means importing the new
|
|
5412
|
+
* probe here and registering its key. The probe modules themselves are
|
|
5413
|
+
* deliberately not part of the `HandlerFactory` interface so capability
|
|
5414
|
+
* detection stays decoupled from runtime instantiation (so we can probe
|
|
5415
|
+
* whether a runtime is usable before spawning anything).
|
|
5416
|
+
*/
|
|
5417
|
+
async function probeCapabilities() {
|
|
5418
|
+
const probes = [["claude-code", probeClaudeCodeCapability()], ["codex", probeCodexCapability()]];
|
|
5419
|
+
const out = {};
|
|
5420
|
+
await Promise.all(probes.map(async ([provider, p]) => {
|
|
5421
|
+
try {
|
|
5422
|
+
out[provider] = await p;
|
|
5423
|
+
} catch (err) {
|
|
5424
|
+
out[provider] = {
|
|
5425
|
+
state: "error",
|
|
5426
|
+
available: false,
|
|
5427
|
+
authenticated: false,
|
|
5428
|
+
sdkVersion: null,
|
|
5429
|
+
authMethod: "none",
|
|
5430
|
+
error: err instanceof Error ? err.message : String(err),
|
|
5431
|
+
detectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5432
|
+
};
|
|
5433
|
+
}
|
|
5434
|
+
}));
|
|
5435
|
+
return out;
|
|
5436
|
+
}
|
|
5437
|
+
/**
|
|
4665
5438
|
* Runtime-wide constants (Step 7 + Step 11).
|
|
4666
5439
|
*
|
|
4667
5440
|
* After Step 11 these values are fixed at code level — the previously
|
|
@@ -5285,7 +6058,7 @@ var ClientRuntime = class {
|
|
|
5285
6058
|
});
|
|
5286
6059
|
const yaml = stringify({
|
|
5287
6060
|
agentId: message.agentId,
|
|
5288
|
-
runtime:
|
|
6061
|
+
runtime: message.runtimeProvider
|
|
5289
6062
|
});
|
|
5290
6063
|
writeFileSync(join(agentDir, "agent.yaml"), yaml, { mode: 384 });
|
|
5291
6064
|
print.check(true, `auto-added agent "${localName}"`, `${message.agentId} (from server push)`);
|
|
@@ -6598,7 +7371,7 @@ async function onboardCreate(args) {
|
|
|
6598
7371
|
}
|
|
6599
7372
|
const runtimeAgent = args.type === "human" ? args.assistant : args.id;
|
|
6600
7373
|
if (args.feishuBotAppId && args.feishuBotAppSecret) {
|
|
6601
|
-
const { bindFeishuBot } = await import("./feishu-
|
|
7374
|
+
const { bindFeishuBot } = await import("./feishu-Boy3n8CT.mjs").then((n) => n.r);
|
|
6602
7375
|
const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
|
|
6603
7376
|
if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
|
|
6604
7377
|
else {
|
|
@@ -6761,17 +7534,77 @@ function setNestedByDot(obj, dotPath, value) {
|
|
|
6761
7534
|
if (lastKey !== void 0) current[lastKey] = value;
|
|
6762
7535
|
}
|
|
6763
7536
|
//#endregion
|
|
6764
|
-
//#region
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
const
|
|
6774
|
-
|
|
7537
|
+
//#region src/core/runtime-provider-reconcile.ts
|
|
7538
|
+
/**
|
|
7539
|
+
* Pre-flight reconciliation called before the agents loop spawns. Pulls
|
|
7540
|
+
* authoritative `runtime_provider` for every agent the calling user owns and
|
|
7541
|
+
* rewrites any local `agent.yaml` whose `runtime` field disagrees. Best-
|
|
7542
|
+
* effort: a transient hub failure logs and falls back to the local YAML
|
|
7543
|
+
* value (the in-band repair path catches any remaining drift on first bind).
|
|
7544
|
+
*/
|
|
7545
|
+
async function reconcileLocalRuntimeProviders(opts) {
|
|
7546
|
+
const res = await fetch(`${opts.serverUrl}/api/v1/clients/me/agents`, { headers: { Authorization: `Bearer ${opts.accessToken}` } });
|
|
7547
|
+
if (!res.ok) throw new Error(`hub returned ${res.status} on /clients/me/agents`);
|
|
7548
|
+
const items = await res.json();
|
|
7549
|
+
const byAgentId = new Map(items.map((it) => [it.agentId, it]));
|
|
7550
|
+
if (!existsSync(opts.agentsDir)) return;
|
|
7551
|
+
const subdirs = readdirSync(opts.agentsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
7552
|
+
for (const subdir of subdirs) {
|
|
7553
|
+
const yamlPath = join(opts.agentsDir, subdir.name, "agent.yaml");
|
|
7554
|
+
if (!existsSync(yamlPath)) continue;
|
|
7555
|
+
let parsed;
|
|
7556
|
+
try {
|
|
7557
|
+
parsed = parse(readFileSync(yamlPath, "utf-8")) ?? {};
|
|
7558
|
+
} catch (err) {
|
|
7559
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
7560
|
+
opts.log?.("warn", `agent ${subdir.name}: cannot parse yaml — ${msg}`);
|
|
7561
|
+
continue;
|
|
7562
|
+
}
|
|
7563
|
+
if (!parsed.agentId) continue;
|
|
7564
|
+
const auth = byAgentId.get(parsed.agentId);
|
|
7565
|
+
if (!auth) continue;
|
|
7566
|
+
if (parsed.runtime === auth.runtimeProvider) continue;
|
|
7567
|
+
const next = {
|
|
7568
|
+
...parsed,
|
|
7569
|
+
runtime: auth.runtimeProvider
|
|
7570
|
+
};
|
|
7571
|
+
try {
|
|
7572
|
+
writeFileSync(yamlPath, stringify(next), { mode: 384 });
|
|
7573
|
+
opts.log?.("info", `agent ${parsed.agentId}: yaml runtime "${parsed.runtime ?? "(unset)"}" → "${auth.runtimeProvider}" (hub authoritative)`);
|
|
7574
|
+
} catch (err) {
|
|
7575
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
7576
|
+
opts.log?.("warn", `agent ${parsed.agentId}: failed to rewrite yaml — ${msg}`);
|
|
7577
|
+
}
|
|
7578
|
+
}
|
|
7579
|
+
}
|
|
7580
|
+
/**
|
|
7581
|
+
* Member-scoped capabilities upload. Server stores the snapshot under
|
|
7582
|
+
* `clients.metadata.capabilities`. Best-effort: failure does not block
|
|
7583
|
+
* client startup since capabilities only matter for UI / admin checks.
|
|
7584
|
+
*/
|
|
7585
|
+
async function uploadClientCapabilities(opts) {
|
|
7586
|
+
const res = await fetch(`${opts.serverUrl}/api/v1/clients/${encodeURIComponent(opts.clientId)}/capabilities`, {
|
|
7587
|
+
method: "PATCH",
|
|
7588
|
+
headers: {
|
|
7589
|
+
Authorization: `Bearer ${opts.accessToken}`,
|
|
7590
|
+
"Content-Type": "application/json"
|
|
7591
|
+
},
|
|
7592
|
+
body: JSON.stringify({ capabilities: opts.capabilities })
|
|
7593
|
+
});
|
|
7594
|
+
if (!res.ok) throw new Error(`hub returned ${res.status} on PATCH /clients/${opts.clientId}/capabilities`);
|
|
7595
|
+
}
|
|
7596
|
+
//#endregion
|
|
7597
|
+
//#region ../../node_modules/.pnpm/hearback-core@0.1.1/node_modules/hearback-core/dist/schema.js
|
|
7598
|
+
const FeedbackType = z.enum(["bug", "feature"]);
|
|
7599
|
+
const BrowserContext = z.object({
|
|
7600
|
+
url: z.string().optional(),
|
|
7601
|
+
userAgent: z.string().optional(),
|
|
7602
|
+
browser: z.string().optional(),
|
|
7603
|
+
os: z.string().optional(),
|
|
7604
|
+
viewport: z.string().optional()
|
|
7605
|
+
});
|
|
7606
|
+
const ConsoleError = z.object({
|
|
7607
|
+
message: z.string(),
|
|
6775
7608
|
stack: z.string().optional(),
|
|
6776
7609
|
timestamp: z.string().optional()
|
|
6777
7610
|
});
|
|
@@ -7518,7 +8351,7 @@ function createFeedbackHandler(config) {
|
|
|
7518
8351
|
return { handle };
|
|
7519
8352
|
}
|
|
7520
8353
|
//#endregion
|
|
7521
|
-
//#region ../server/dist/app-
|
|
8354
|
+
//#region ../server/dist/app-D-aIvdiQ.mjs
|
|
7522
8355
|
var __defProp = Object.defineProperty;
|
|
7523
8356
|
var __exportAll = (all, no_symbols) => {
|
|
7524
8357
|
let target = {};
|
|
@@ -7572,6 +8405,7 @@ const agents = pgTable("agents", {
|
|
|
7572
8405
|
metadata: jsonb("metadata").$type().notNull().default({}),
|
|
7573
8406
|
managerId: text("manager_id").notNull(),
|
|
7574
8407
|
clientId: text("client_id").references(() => clients.id, { onDelete: "restrict" }),
|
|
8408
|
+
runtimeProvider: text("runtime_provider").notNull().default("claude-code"),
|
|
7575
8409
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
7576
8410
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
7577
8411
|
}, (table) => [
|
|
@@ -8241,7 +9075,7 @@ async function deleteAdapterConfig(db, id) {
|
|
|
8241
9075
|
const [row] = await db.delete(adapterConfigs).where(eq(adapterConfigs.id, id)).returning();
|
|
8242
9076
|
if (!row) throw new NotFoundError(`Adapter config "${id}" not found`);
|
|
8243
9077
|
}
|
|
8244
|
-
const log$
|
|
9078
|
+
const log$5 = createLogger$1("AdminAdapters");
|
|
8245
9079
|
function parseId(raw) {
|
|
8246
9080
|
const id = Number(raw);
|
|
8247
9081
|
if (!Number.isInteger(id) || id <= 0) throw new BadRequestError(`Invalid adapter ID: "${raw}"`);
|
|
@@ -8261,7 +9095,7 @@ async function adminAdapterRoutes(app) {
|
|
|
8261
9095
|
const scope = memberScope(request);
|
|
8262
9096
|
await assertCanManage(app.db, scope, body.agentId);
|
|
8263
9097
|
const config = await createAdapterConfig(app.db, body, app.config.secrets.encryptionKey);
|
|
8264
|
-
app.adapterManager.reload().catch((err) => log$
|
|
9098
|
+
app.adapterManager.reload().catch((err) => log$5.error({ err }, "adapter reload failed after create"));
|
|
8265
9099
|
app.notifier.notifyConfigChange("adapter_configs").catch(() => {});
|
|
8266
9100
|
return reply.status(201).send({
|
|
8267
9101
|
...config,
|
|
@@ -8285,7 +9119,7 @@ async function adminAdapterRoutes(app) {
|
|
|
8285
9119
|
const existing = await getAdapterConfig(app.db, id);
|
|
8286
9120
|
await assertCanManage(app.db, scope, existing.agentId);
|
|
8287
9121
|
const config = await updateAdapterConfig(app.db, id, body, app.config.secrets.encryptionKey);
|
|
8288
|
-
app.adapterManager.reload().catch((err) => log$
|
|
9122
|
+
app.adapterManager.reload().catch((err) => log$5.error({ err }, "adapter reload failed after update"));
|
|
8289
9123
|
app.notifier.notifyConfigChange("adapter_configs").catch(() => {});
|
|
8290
9124
|
return {
|
|
8291
9125
|
...config,
|
|
@@ -8299,7 +9133,7 @@ async function adminAdapterRoutes(app) {
|
|
|
8299
9133
|
const existing = await getAdapterConfig(app.db, id);
|
|
8300
9134
|
await assertCanManage(app.db, scope, existing.agentId);
|
|
8301
9135
|
await deleteAdapterConfig(app.db, id);
|
|
8302
|
-
app.adapterManager.reload().catch((err) => log$
|
|
9136
|
+
app.adapterManager.reload().catch((err) => log$5.error({ err }, "adapter reload failed after delete"));
|
|
8303
9137
|
app.notifier.notifyConfigChange("adapter_configs").catch(() => {});
|
|
8304
9138
|
return reply.status(204).send();
|
|
8305
9139
|
});
|
|
@@ -8400,6 +9234,38 @@ const members = pgTable("members", {
|
|
|
8400
9234
|
* real account.
|
|
8401
9235
|
*/
|
|
8402
9236
|
const RESERVED_AGENT_NAME_PREFIX = "__";
|
|
9237
|
+
/**
|
|
9238
|
+
* True iff `clients.metadata.capabilities` is a non-empty object — i.e. the
|
|
9239
|
+
* client has reported at least one runtime probe result. Used to distinguish
|
|
9240
|
+
* "we don't know what's installed yet" (empty / never reported) from
|
|
9241
|
+
* "client explicitly reports this provider is missing".
|
|
9242
|
+
*/
|
|
9243
|
+
function clientCapabilitiesReported(metadata) {
|
|
9244
|
+
if (!metadata || typeof metadata !== "object") return false;
|
|
9245
|
+
const caps = metadata.capabilities;
|
|
9246
|
+
if (!caps || typeof caps !== "object") return false;
|
|
9247
|
+
return Object.keys(caps).length > 0;
|
|
9248
|
+
}
|
|
9249
|
+
/**
|
|
9250
|
+
* Inspect a `clients.metadata.capabilities` blob (jsonb) for a specific
|
|
9251
|
+
* runtime provider entry. Capabilities live under the `metadata.capabilities`
|
|
9252
|
+
* subkey (Option C); the column is unstructured at the DB layer, so we
|
|
9253
|
+
* defensively narrow before key access.
|
|
9254
|
+
*
|
|
9255
|
+
* "Supports" requires the entry's SDK to be **available** — `state: "ok"` or
|
|
9256
|
+
* `state: "unauthenticated"`. A `missing` or `error` entry is *reported* but
|
|
9257
|
+
* not usable, so we explicitly reject those rather than treating mere key
|
|
9258
|
+
* presence as support. Auth state is left to the user to fix at runtime
|
|
9259
|
+
* (the re-bind dialog surfaces an `unauthenticated` hint).
|
|
9260
|
+
*/
|
|
9261
|
+
function clientSupportsRuntimeProvider(metadata, provider) {
|
|
9262
|
+
if (!metadata || typeof metadata !== "object") return false;
|
|
9263
|
+
const caps = metadata.capabilities;
|
|
9264
|
+
if (!caps || typeof caps !== "object") return false;
|
|
9265
|
+
const entry = caps[provider];
|
|
9266
|
+
if (!entry || typeof entry !== "object") return false;
|
|
9267
|
+
return entry.available === true;
|
|
9268
|
+
}
|
|
8403
9269
|
/** Default visibility per agent type. */
|
|
8404
9270
|
function defaultVisibility(type) {
|
|
8405
9271
|
switch (type) {
|
|
@@ -8420,6 +9286,32 @@ function defaultVisibility(type) {
|
|
|
8420
9286
|
* - When a non-human agent IS created with a `clientId`, the pinned client
|
|
8421
9287
|
* must already be owned by the manager's user (Rule R-RUN).
|
|
8422
9288
|
*/
|
|
9289
|
+
/**
|
|
9290
|
+
* Check that a client's reported capabilities show the given runtime provider
|
|
9291
|
+
* as **available** (SDK installed, regardless of auth state).
|
|
9292
|
+
*
|
|
9293
|
+
* Tri-state semantics by `clients.metadata.capabilities` shape:
|
|
9294
|
+
* - empty / absent — client hasn't probed yet (newly registered or pre-P2
|
|
9295
|
+
* install). Treat as "unknown" and allow; the in-band repair path
|
|
9296
|
+
* (RUNTIME_PROVIDER_MISMATCH on bind) catches actual incompatibility.
|
|
9297
|
+
* - reported, entry shows `state: ok | unauthenticated` (i.e. `available:
|
|
9298
|
+
* true`) — allow.
|
|
9299
|
+
* - reported, entry missing OR `state: missing | error` — block unless
|
|
9300
|
+
* `force` is set. We deliberately do NOT treat mere key presence as
|
|
9301
|
+
* support: probeCapabilities() always emits an entry per built-in
|
|
9302
|
+
* provider, including `{ state: "missing" }` for absent SDKs.
|
|
9303
|
+
*
|
|
9304
|
+
* Skipped entirely for human agents (no clientId) and when `force` is set
|
|
9305
|
+
* (e.g. operator overrides for an offline client).
|
|
9306
|
+
*/
|
|
9307
|
+
async function ensureClientSupportsRuntimeProvider(db, clientId, runtimeProvider, options = {}) {
|
|
9308
|
+
if (clientId === null) return;
|
|
9309
|
+
if (options.force) return;
|
|
9310
|
+
const [client] = await db.select({ metadata: clients.metadata }).from(clients).where(eq(clients.id, clientId)).limit(1);
|
|
9311
|
+
if (!client) return;
|
|
9312
|
+
if (!clientCapabilitiesReported(client.metadata)) return;
|
|
9313
|
+
if (!clientSupportsRuntimeProvider(client.metadata, runtimeProvider)) throw new BadRequestError(`Client "${clientId}" does not have runtime provider "${runtimeProvider}" available. Install the matching SDK on that machine and re-run capability detection, or retry with \`force: true\` if the client is offline / capabilities are stale.`);
|
|
9314
|
+
}
|
|
8423
9315
|
async function resolveAgentClient(db, data) {
|
|
8424
9316
|
if (data.type === "human") {
|
|
8425
9317
|
if (data.clientId) throw new BadRequestError("Human agents cannot be pinned to a client");
|
|
@@ -8452,9 +9344,10 @@ async function resolveFallbackManagerId(db, orgId) {
|
|
|
8452
9344
|
if (!row) throw new BadRequestError(`Cannot create agent in organization "${orgId}" — no admin member exists. Create an admin member first (see \`first-tree-hub onboard\`).`);
|
|
8453
9345
|
return row.id;
|
|
8454
9346
|
}
|
|
8455
|
-
async function createAgent(db, data) {
|
|
9347
|
+
async function createAgent(db, data, options = {}) {
|
|
8456
9348
|
const uuid = uuidv7();
|
|
8457
9349
|
const name = data.name ?? null;
|
|
9350
|
+
const runtimeProvider = data.runtimeProvider ?? "claude-code";
|
|
8458
9351
|
if (name?.startsWith(RESERVED_AGENT_NAME_PREFIX)) throw new BadRequestError(`Agent name "${name}" is reserved — names starting with "${RESERVED_AGENT_NAME_PREFIX}" are Hub-internal`);
|
|
8459
9352
|
if (name && isReservedAgentName$1(name)) throw new BadRequestError(`Agent name "${name}" is reserved — pick a different one.`);
|
|
8460
9353
|
const inboxId = `inbox_${uuid}`;
|
|
@@ -8480,6 +9373,7 @@ async function createAgent(db, data) {
|
|
|
8480
9373
|
managerId,
|
|
8481
9374
|
type: data.type
|
|
8482
9375
|
});
|
|
9376
|
+
await ensureClientSupportsRuntimeProvider(db, clientId, runtimeProvider, { force: options.force });
|
|
8483
9377
|
const [org] = await db.select({ maxAgents: organizations.maxAgents }).from(organizations).where(eq(organizations.id, orgId)).limit(1);
|
|
8484
9378
|
if (org && org.maxAgents > 0) {
|
|
8485
9379
|
if (((await db.select({ value: count() }).from(agents).where(and(eq(agents.organizationId, orgId), ne(agents.status, AGENT_STATUSES.DELETED))))[0]?.value ?? 0) >= org.maxAgents) throw new ForbiddenError(`Organization "${orgId}" has reached its agent limit (${org.maxAgents}). Upgrade your plan or delete unused agents.`);
|
|
@@ -8498,13 +9392,14 @@ async function createAgent(db, data) {
|
|
|
8498
9392
|
visibility: data.visibility ?? defaultVisibility(data.type),
|
|
8499
9393
|
metadata: data.metadata ?? {},
|
|
8500
9394
|
managerId,
|
|
8501
|
-
clientId
|
|
9395
|
+
clientId,
|
|
9396
|
+
runtimeProvider
|
|
8502
9397
|
}).returning();
|
|
8503
9398
|
if (!agent) throw new Error("Unexpected: INSERT RETURNING produced no row");
|
|
8504
9399
|
await db.insert(agentConfigs).values({
|
|
8505
9400
|
agentId: agent.uuid,
|
|
8506
9401
|
version: 1,
|
|
8507
|
-
payload:
|
|
9402
|
+
payload: defaultRuntimeConfigPayload(runtimeProvider),
|
|
8508
9403
|
updatedBy: "system"
|
|
8509
9404
|
}).onConflictDoNothing();
|
|
8510
9405
|
return agent;
|
|
@@ -8558,6 +9453,7 @@ async function listAgentsForAdmin(db, scope, limit, cursor) {
|
|
|
8558
9453
|
metadata: agents.metadata,
|
|
8559
9454
|
managerId: agents.managerId,
|
|
8560
9455
|
clientId: agents.clientId,
|
|
9456
|
+
runtimeProvider: agents.runtimeProvider,
|
|
8561
9457
|
createdAt: agents.createdAt,
|
|
8562
9458
|
updatedAt: agents.updatedAt,
|
|
8563
9459
|
presenceStatus: agentPresence.status,
|
|
@@ -8595,6 +9491,7 @@ async function listAgentsForMember(db, scope, limit, cursor, type) {
|
|
|
8595
9491
|
metadata: agents.metadata,
|
|
8596
9492
|
managerId: agents.managerId,
|
|
8597
9493
|
clientId: agents.clientId,
|
|
9494
|
+
runtimeProvider: agents.runtimeProvider,
|
|
8598
9495
|
createdAt: agents.createdAt,
|
|
8599
9496
|
updatedAt: agents.updatedAt,
|
|
8600
9497
|
presenceStatus: agentPresence.status,
|
|
@@ -8614,7 +9511,7 @@ async function updateAgent(db, uuid, data) {
|
|
|
8614
9511
|
const agent = await getAgent(db, uuid);
|
|
8615
9512
|
if (data.clientId !== void 0) {
|
|
8616
9513
|
if (data.clientId === null) throw new BadRequestError("clientId cannot be cleared — once bound, an agent stays bound to its client");
|
|
8617
|
-
if (agent.clientId !== null && agent.clientId !== data.clientId) throw new BadRequestError("clientId is immutable
|
|
9514
|
+
if (agent.clientId !== null && agent.clientId !== data.clientId) throw new BadRequestError("clientId is immutable through this entry — cross-client moves go through rebindAgent (PATCH /admin/agents/:agentId/rebind), which runs owner / org / capability checks atomically.");
|
|
8618
9515
|
}
|
|
8619
9516
|
const updates = { updatedAt: /* @__PURE__ */ new Date() };
|
|
8620
9517
|
if (data.type !== void 0) updates.type = data.type;
|
|
@@ -8645,6 +9542,39 @@ async function updateAgent(db, uuid, data) {
|
|
|
8645
9542
|
return updated;
|
|
8646
9543
|
}
|
|
8647
9544
|
/**
|
|
9545
|
+
* Atomically re-bind an agent to a new client and/or runtime provider.
|
|
9546
|
+
*
|
|
9547
|
+
* Validations: agent must exist and not be human; new client must belong to
|
|
9548
|
+
* the same owner (manager.userId) and same organization; client must report
|
|
9549
|
+
* the requested runtime provider in its capabilities (skipped under `force`).
|
|
9550
|
+
*
|
|
9551
|
+
* Intended caller: PATCH /admin/agents/:agentId/rebind. The Web "Re-bind"
|
|
9552
|
+
* dialog routes both same-client runtime-only switches and cross-client
|
|
9553
|
+
* moves through this single entry.
|
|
9554
|
+
*
|
|
9555
|
+
* NOTE: active sessions on the previous client are not auto-suspended in P1.
|
|
9556
|
+
* P3 will wire in cross-service coordination (inbox + presence + session)
|
|
9557
|
+
* so the destination client can resume cleanly.
|
|
9558
|
+
*/
|
|
9559
|
+
async function rebindAgent(db, uuid, data) {
|
|
9560
|
+
const agent = await getAgent(db, uuid);
|
|
9561
|
+
if (agent.type === "human") throw new BadRequestError("Human agents have no runtime — they cannot be re-bound to a client.");
|
|
9562
|
+
const newClientId = await resolveAgentClient(db, {
|
|
9563
|
+
clientId: data.clientId,
|
|
9564
|
+
managerId: agent.managerId,
|
|
9565
|
+
type: agent.type
|
|
9566
|
+
});
|
|
9567
|
+
if (newClientId === null) throw new BadRequestError("Rebind requires a non-null clientId.");
|
|
9568
|
+
await ensureClientSupportsRuntimeProvider(db, newClientId, data.runtimeProvider, { force: data.force });
|
|
9569
|
+
const [updated] = await db.update(agents).set({
|
|
9570
|
+
clientId: newClientId,
|
|
9571
|
+
runtimeProvider: data.runtimeProvider,
|
|
9572
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
9573
|
+
}).where(eq(agents.uuid, uuid)).returning();
|
|
9574
|
+
if (!updated) throw new Error("Unexpected: UPDATE RETURNING produced no row");
|
|
9575
|
+
return updated;
|
|
9576
|
+
}
|
|
9577
|
+
/**
|
|
8648
9578
|
* Reactivate a suspended agent.
|
|
8649
9579
|
*/
|
|
8650
9580
|
async function reactivateAgent(db, uuid) {
|
|
@@ -9224,10 +10154,45 @@ async function listActiveAgentsPinnedToClient(db, clientId) {
|
|
|
9224
10154
|
uuid: agents.uuid,
|
|
9225
10155
|
name: agents.name,
|
|
9226
10156
|
displayName: agents.displayName,
|
|
9227
|
-
type: agents.type
|
|
10157
|
+
type: agents.type,
|
|
10158
|
+
runtimeProvider: agents.runtimeProvider
|
|
9228
10159
|
}).from(agents).where(and(eq(agents.clientId, clientId), ne(agents.status, "deleted")));
|
|
9229
10160
|
}
|
|
9230
10161
|
/**
|
|
10162
|
+
* Member-scoped: every active agent pinned to a client owned by this user
|
|
10163
|
+
* within the given organization. Used by client startup to reconcile its
|
|
10164
|
+
* local YAML against the authoritative `agents.runtime_provider`.
|
|
10165
|
+
*/
|
|
10166
|
+
async function listMyPinnedAgents(db, scope) {
|
|
10167
|
+
return (await db.select({
|
|
10168
|
+
agentId: agents.uuid,
|
|
10169
|
+
clientId: agents.clientId,
|
|
10170
|
+
runtimeProvider: agents.runtimeProvider
|
|
10171
|
+
}).from(agents).innerJoin(clients, eq(agents.clientId, clients.id)).where(and(eq(clients.userId, scope.userId), eq(clients.organizationId, scope.organizationId), ne(agents.status, "deleted")))).filter((r) => r.clientId !== null).map((r) => ({
|
|
10172
|
+
agentId: r.agentId,
|
|
10173
|
+
clientId: r.clientId,
|
|
10174
|
+
runtimeProvider: r.runtimeProvider
|
|
10175
|
+
}));
|
|
10176
|
+
}
|
|
10177
|
+
/**
|
|
10178
|
+
* Replace this client's capabilities snapshot. Capabilities live under
|
|
10179
|
+
* `clients.metadata.capabilities` (Option C — no dedicated column); other
|
|
10180
|
+
* `metadata` subkeys are preserved on merge.
|
|
10181
|
+
*
|
|
10182
|
+
* Caller is expected to have already passed `assertClientOwner`.
|
|
10183
|
+
*/
|
|
10184
|
+
async function updateClientCapabilities(db, clientId, capabilities) {
|
|
10185
|
+
const parsed = clientCapabilitiesSchema$1.safeParse(capabilities);
|
|
10186
|
+
if (!parsed.success) throw new BadRequestError(`Invalid capabilities payload: ${parsed.error.message}`);
|
|
10187
|
+
const [client] = await db.select({ metadata: clients.metadata }).from(clients).where(eq(clients.id, clientId)).limit(1);
|
|
10188
|
+
if (!client) throw new NotFoundError(`Client "${clientId}" not found`);
|
|
10189
|
+
const merged = {
|
|
10190
|
+
...client.metadata ?? {},
|
|
10191
|
+
capabilities: parsed.data
|
|
10192
|
+
};
|
|
10193
|
+
await db.update(clients).set({ metadata: merged }).where(eq(clients.id, clientId));
|
|
10194
|
+
}
|
|
10195
|
+
/**
|
|
9231
10196
|
* Scope-aware client listing.
|
|
9232
10197
|
*
|
|
9233
10198
|
* - member: rows where `user_id = scope.userId` AND `organization_id = scope.organizationId`
|
|
@@ -9440,6 +10405,126 @@ const inboxEntries = pgTable("inbox_entries", {
|
|
|
9440
10405
|
index("idx_inbox_pending_notify").on(table.inboxId, table.createdAt).where(sql`status = 'pending' AND notify = true`),
|
|
9441
10406
|
index("idx_inbox_chat_silent").on(table.inboxId, table.chatId, table.notify, table.status)
|
|
9442
10407
|
]);
|
|
10408
|
+
/** Per-session state snapshot. One row per (agent, chat) pair, upserted on each session:state message. */
|
|
10409
|
+
const agentChatSessions = pgTable("agent_chat_sessions", {
|
|
10410
|
+
agentId: text("agent_id").notNull().references(() => agents.uuid, { onDelete: "cascade" }),
|
|
10411
|
+
chatId: text("chat_id").notNull().references(() => chats.id, { onDelete: "cascade" }),
|
|
10412
|
+
state: text("state").notNull(),
|
|
10413
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
10414
|
+
}, (table) => [primaryKey({ columns: [table.agentId, table.chatId] })]);
|
|
10415
|
+
/**
|
|
10416
|
+
* Upsert session state + refresh presence aggregates + NOTIFY.
|
|
10417
|
+
*
|
|
10418
|
+
* `agent_chat_sessions.(agent_id, chat_id)` is a single-row "current session
|
|
10419
|
+
* state" cache, not a session history log. A new runtime session starting on
|
|
10420
|
+
* the same (agent, chat) pair MUST overwrite whatever ended before — including
|
|
10421
|
+
* an `evicted` row left by a previous terminate. The previous "revival
|
|
10422
|
+
* defense" conflated two concerns: "this runtime session ended" (which is
|
|
10423
|
+
* what `evicted` actually means) and "this chat is permanently archived for
|
|
10424
|
+
* this agent" (a chat-level decision that should live on `chats`, not here).
|
|
10425
|
+
* See proposals/hub-agent-messaging-reply-and-mentions §M2-session-lifecycle.
|
|
10426
|
+
*
|
|
10427
|
+
* Presence row contract: this function tolerates a missing `agent_presence`
|
|
10428
|
+
* row by using `INSERT ... ON CONFLICT DO UPDATE`. The predictive-write path
|
|
10429
|
+
* (sendMessage on first message) may target an agent whose client has never
|
|
10430
|
+
* bound, so a prior `update agent_presence ... where agentId` would silently
|
|
10431
|
+
* drop the activeSessions/totalSessions refresh. See PR #198 review §2.
|
|
10432
|
+
*/
|
|
10433
|
+
async function upsertSessionState(db, agentId, chatId, state, organizationId, notifier, options) {
|
|
10434
|
+
const now = /* @__PURE__ */ new Date();
|
|
10435
|
+
let wrote = false;
|
|
10436
|
+
await db.transaction(async (tx) => {
|
|
10437
|
+
await tx.insert(agentChatSessions).values({
|
|
10438
|
+
agentId,
|
|
10439
|
+
chatId,
|
|
10440
|
+
state,
|
|
10441
|
+
updatedAt: now
|
|
10442
|
+
}).onConflictDoUpdate({
|
|
10443
|
+
target: [agentChatSessions.agentId, agentChatSessions.chatId],
|
|
10444
|
+
set: {
|
|
10445
|
+
state,
|
|
10446
|
+
updatedAt: now
|
|
10447
|
+
},
|
|
10448
|
+
setWhere: ne(agentChatSessions.state, state)
|
|
10449
|
+
});
|
|
10450
|
+
const [counts] = await tx.select({
|
|
10451
|
+
active: sql`count(*) FILTER (WHERE ${agentChatSessions.state} = 'active')::int`,
|
|
10452
|
+
total: sql`count(*) FILTER (WHERE ${agentChatSessions.state} != 'evicted')::int`
|
|
10453
|
+
}).from(agentChatSessions).where(eq(agentChatSessions.agentId, agentId));
|
|
10454
|
+
const activeSessions = counts?.active ?? 0;
|
|
10455
|
+
const totalSessions = counts?.total ?? 0;
|
|
10456
|
+
const presenceSet = options?.touchPresenceLastSeen ?? true ? {
|
|
10457
|
+
activeSessions,
|
|
10458
|
+
totalSessions,
|
|
10459
|
+
lastSeenAt: now
|
|
10460
|
+
} : {
|
|
10461
|
+
activeSessions,
|
|
10462
|
+
totalSessions
|
|
10463
|
+
};
|
|
10464
|
+
await tx.insert(agentPresence).values({
|
|
10465
|
+
agentId,
|
|
10466
|
+
activeSessions,
|
|
10467
|
+
totalSessions
|
|
10468
|
+
}).onConflictDoUpdate({
|
|
10469
|
+
target: [agentPresence.agentId],
|
|
10470
|
+
set: presenceSet
|
|
10471
|
+
});
|
|
10472
|
+
wrote = true;
|
|
10473
|
+
});
|
|
10474
|
+
if (wrote && notifier) notifier.notifySessionStateChange(agentId, chatId, state, organizationId).catch(() => {});
|
|
10475
|
+
}
|
|
10476
|
+
async function resetActivity(db, agentId) {
|
|
10477
|
+
const now = /* @__PURE__ */ new Date();
|
|
10478
|
+
await db.update(agentPresence).set({
|
|
10479
|
+
runtimeState: "idle",
|
|
10480
|
+
runtimeUpdatedAt: now
|
|
10481
|
+
}).where(eq(agentPresence.agentId, agentId));
|
|
10482
|
+
}
|
|
10483
|
+
async function getActivityOverview(db) {
|
|
10484
|
+
const [agentCounts] = await db.select({
|
|
10485
|
+
total: sql`count(*)::int`,
|
|
10486
|
+
running: sql`count(*) FILTER (WHERE ${agentPresence.runtimeState} IS NOT NULL)::int`,
|
|
10487
|
+
idle: sql`count(*) FILTER (WHERE ${agentPresence.runtimeState} = 'idle')::int`,
|
|
10488
|
+
working: sql`count(*) FILTER (WHERE ${agentPresence.runtimeState} = 'working')::int`,
|
|
10489
|
+
blocked: sql`count(*) FILTER (WHERE ${agentPresence.runtimeState} = 'blocked')::int`,
|
|
10490
|
+
error: sql`count(*) FILTER (WHERE ${agentPresence.runtimeState} = 'error')::int`
|
|
10491
|
+
}).from(agentPresence);
|
|
10492
|
+
const [clientCounts] = await db.select({ count: sql`count(*)::int` }).from(clients).where(eq(clients.status, "connected"));
|
|
10493
|
+
return {
|
|
10494
|
+
total: agentCounts?.total ?? 0,
|
|
10495
|
+
running: agentCounts?.running ?? 0,
|
|
10496
|
+
byState: {
|
|
10497
|
+
idle: agentCounts?.idle ?? 0,
|
|
10498
|
+
working: agentCounts?.working ?? 0,
|
|
10499
|
+
blocked: agentCounts?.blocked ?? 0,
|
|
10500
|
+
error: agentCounts?.error ?? 0
|
|
10501
|
+
},
|
|
10502
|
+
clients: clientCounts?.count ?? 0
|
|
10503
|
+
};
|
|
10504
|
+
}
|
|
10505
|
+
/**
|
|
10506
|
+
* List agents with active runtime state.
|
|
10507
|
+
* When scope is provided, filters to agents visible to the member.
|
|
10508
|
+
*/
|
|
10509
|
+
async function listAgentsWithRuntime(db, scope) {
|
|
10510
|
+
if (!scope) return db.select().from(agentPresence).where(isNotNull(agentPresence.runtimeState));
|
|
10511
|
+
return db.select({
|
|
10512
|
+
agentId: agentPresence.agentId,
|
|
10513
|
+
status: agentPresence.status,
|
|
10514
|
+
instanceId: agentPresence.instanceId,
|
|
10515
|
+
connectedAt: agentPresence.connectedAt,
|
|
10516
|
+
lastSeenAt: agentPresence.lastSeenAt,
|
|
10517
|
+
clientId: agentPresence.clientId,
|
|
10518
|
+
runtimeType: agentPresence.runtimeType,
|
|
10519
|
+
runtimeVersion: agentPresence.runtimeVersion,
|
|
10520
|
+
runtimeState: agentPresence.runtimeState,
|
|
10521
|
+
activeSessions: agentPresence.activeSessions,
|
|
10522
|
+
totalSessions: agentPresence.totalSessions,
|
|
10523
|
+
runtimeUpdatedAt: agentPresence.runtimeUpdatedAt,
|
|
10524
|
+
type: agents.type
|
|
10525
|
+
}).from(agentPresence).innerJoin(agents, eq(agentPresence.agentId, agents.uuid)).where(and(isNotNull(agentPresence.runtimeState), agentVisibilityCondition(scope)));
|
|
10526
|
+
}
|
|
10527
|
+
const log$4 = createLogger$1("message");
|
|
9443
10528
|
async function sendMessage(db, chatId, senderId, data, options = {}) {
|
|
9444
10529
|
return withSpan("inbox.enqueue", messageAttrs({
|
|
9445
10530
|
chatId,
|
|
@@ -9448,17 +10533,24 @@ async function sendMessage(db, chatId, senderId, data, options = {}) {
|
|
|
9448
10533
|
}), () => sendMessageInner(db, chatId, senderId, data, options));
|
|
9449
10534
|
}
|
|
9450
10535
|
async function sendMessageInner(db, chatId, senderId, data, options) {
|
|
9451
|
-
|
|
9452
|
-
const [participants, [chatRow]] = await Promise.all([
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
10536
|
+
const txResult = await db.transaction(async (tx) => {
|
|
10537
|
+
const [participants, [chatRow], [senderRow]] = await Promise.all([
|
|
10538
|
+
tx.select({
|
|
10539
|
+
agentId: chatParticipants.agentId,
|
|
10540
|
+
inboxId: agents.inboxId,
|
|
10541
|
+
mode: chatParticipants.mode,
|
|
10542
|
+
name: agents.name
|
|
10543
|
+
}).from(chatParticipants).innerJoin(agents, eq(chatParticipants.agentId, agents.uuid)).where(eq(chatParticipants.chatId, chatId)),
|
|
10544
|
+
tx.select({ type: chats.type }).from(chats).where(eq(chats.id, chatId)).limit(1),
|
|
10545
|
+
tx.select({
|
|
10546
|
+
inboxId: agents.inboxId,
|
|
10547
|
+
organizationId: agents.organizationId
|
|
10548
|
+
}).from(agents).where(eq(agents.uuid, senderId)).limit(1)
|
|
10549
|
+
]);
|
|
9458
10550
|
const chatType = chatRow?.type ?? null;
|
|
10551
|
+
if (!senderRow) throw new NotFoundError(`Sender agent "${senderId}" not found`);
|
|
9459
10552
|
if (data.replyToInbox !== void 0 && data.replyToInbox !== null) {
|
|
9460
|
-
|
|
9461
|
-
if (!senderRow || senderRow.inboxId !== data.replyToInbox) throw new BadRequestError("replyToInbox must reference the sender's own inbox");
|
|
10553
|
+
if (senderRow.inboxId !== data.replyToInbox) throw new BadRequestError("replyToInbox must reference the sender's own inbox");
|
|
9462
10554
|
}
|
|
9463
10555
|
const incomingMeta = data.metadata ?? {};
|
|
9464
10556
|
const explicitMentionsRaw = incomingMeta.mentions;
|
|
@@ -9503,14 +10595,20 @@ async function sendMessageInner(db, chatId, senderId, data, options) {
|
|
|
9503
10595
|
source: data.source ?? null
|
|
9504
10596
|
}).returning();
|
|
9505
10597
|
const mentionSet = new Set(mergedMentions);
|
|
9506
|
-
const
|
|
10598
|
+
const fanout = participants.filter((p) => p.agentId !== senderId).map((p) => ({
|
|
10599
|
+
agentId: p.agentId,
|
|
9507
10600
|
inboxId: p.inboxId,
|
|
9508
|
-
messageId,
|
|
9509
|
-
chatId,
|
|
9510
10601
|
notify: p.mode !== "mention_only" || mentionSet.has(p.agentId)
|
|
9511
10602
|
}));
|
|
9512
|
-
if (
|
|
9513
|
-
|
|
10603
|
+
if (fanout.length > 0) await tx.insert(inboxEntries).values(fanout.map((f) => ({
|
|
10604
|
+
inboxId: f.inboxId,
|
|
10605
|
+
messageId,
|
|
10606
|
+
chatId,
|
|
10607
|
+
notify: f.notify
|
|
10608
|
+
})));
|
|
10609
|
+
const notified = fanout.filter((f) => f.notify);
|
|
10610
|
+
const recipients = notified.map((f) => f.inboxId);
|
|
10611
|
+
const recipientAgentIds = notified.map((f) => f.agentId);
|
|
9514
10612
|
if (data.inReplyTo) {
|
|
9515
10613
|
const [original] = await tx.select({
|
|
9516
10614
|
replyToInbox: messages.replyToInbox,
|
|
@@ -9529,9 +10627,24 @@ async function sendMessageInner(db, chatId, senderId, data, options) {
|
|
|
9529
10627
|
if (!msg) throw new Error("Unexpected: INSERT RETURNING produced no row");
|
|
9530
10628
|
return {
|
|
9531
10629
|
message: msg,
|
|
9532
|
-
recipients
|
|
10630
|
+
recipients,
|
|
10631
|
+
recipientAgentIds,
|
|
10632
|
+
organizationId: senderRow.organizationId
|
|
9533
10633
|
};
|
|
9534
10634
|
});
|
|
10635
|
+
const settled = await Promise.allSettled(txResult.recipientAgentIds.map((agentId) => upsertSessionState(db, agentId, chatId, "active", txResult.organizationId, void 0, { touchPresenceLastSeen: false })));
|
|
10636
|
+
for (let i = 0; i < settled.length; i++) {
|
|
10637
|
+
const r = settled[i];
|
|
10638
|
+
if (r?.status === "rejected") log$4.error({
|
|
10639
|
+
err: r.reason,
|
|
10640
|
+
chatId,
|
|
10641
|
+
agentId: txResult.recipientAgentIds[i]
|
|
10642
|
+
}, "predictive session activation failed");
|
|
10643
|
+
}
|
|
10644
|
+
return {
|
|
10645
|
+
message: txResult.message,
|
|
10646
|
+
recipients: txResult.recipients
|
|
10647
|
+
};
|
|
9535
10648
|
}
|
|
9536
10649
|
async function sendToAgent(db, senderUuid, targetName, data) {
|
|
9537
10650
|
const [sender] = await db.select({
|
|
@@ -9758,7 +10871,8 @@ async function adminAgentRoutes(app) {
|
|
|
9758
10871
|
agentId: agent.uuid,
|
|
9759
10872
|
name: agent.name,
|
|
9760
10873
|
displayName: agent.displayName,
|
|
9761
|
-
agentType: agent.type
|
|
10874
|
+
agentType: agent.type,
|
|
10875
|
+
runtimeProvider: agent.runtimeProvider
|
|
9762
10876
|
});
|
|
9763
10877
|
if (!parsed.success) {
|
|
9764
10878
|
app.log.warn({
|
|
@@ -9861,6 +10975,23 @@ async function adminAgentRoutes(app) {
|
|
|
9861
10975
|
updatedAt: agent.updatedAt.toISOString()
|
|
9862
10976
|
};
|
|
9863
10977
|
});
|
|
10978
|
+
/**
|
|
10979
|
+
* Rebind an agent to a new client and/or runtime provider. Re-runs owner /
|
|
10980
|
+
* org / capability checks atomically. Capability mismatch can be overridden
|
|
10981
|
+
* with `force: true` (e.g. client offline, capabilities stale).
|
|
10982
|
+
*/
|
|
10983
|
+
app.patch("/:uuid/rebind", async (request) => {
|
|
10984
|
+
const scope = memberScope(request);
|
|
10985
|
+
await assertCanManage(app.db, scope, request.params.uuid);
|
|
10986
|
+
const body = rebindAgentSchema.parse(request.body);
|
|
10987
|
+
const agent = await rebindAgent(app.db, request.params.uuid, body);
|
|
10988
|
+
notifyClientAgentPinned(agent);
|
|
10989
|
+
return {
|
|
10990
|
+
...agent,
|
|
10991
|
+
createdAt: agent.createdAt.toISOString(),
|
|
10992
|
+
updatedAt: agent.updatedAt.toISOString()
|
|
10993
|
+
};
|
|
10994
|
+
});
|
|
9864
10995
|
app.get("/:uuid", async (request) => {
|
|
9865
10996
|
const scope = memberScope(request);
|
|
9866
10997
|
await assertAgentVisible(app.db, scope, request.params.uuid);
|
|
@@ -10315,107 +11446,6 @@ async function adminChatRoutes(app) {
|
|
|
10315
11446
|
});
|
|
10316
11447
|
});
|
|
10317
11448
|
}
|
|
10318
|
-
/** Per-session state snapshot. One row per (agent, chat) pair, upserted on each session:state message. */
|
|
10319
|
-
const agentChatSessions = pgTable("agent_chat_sessions", {
|
|
10320
|
-
agentId: text("agent_id").notNull().references(() => agents.uuid, { onDelete: "cascade" }),
|
|
10321
|
-
chatId: text("chat_id").notNull().references(() => chats.id, { onDelete: "cascade" }),
|
|
10322
|
-
state: text("state").notNull(),
|
|
10323
|
-
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
10324
|
-
}, (table) => [primaryKey({ columns: [table.agentId, table.chatId] })]);
|
|
10325
|
-
/**
|
|
10326
|
-
* Upsert session state + refresh presence aggregates + NOTIFY.
|
|
10327
|
-
*
|
|
10328
|
-
* `agent_chat_sessions.(agent_id, chat_id)` is a single-row "current session
|
|
10329
|
-
* state" cache, not a session history log. A new runtime session starting on
|
|
10330
|
-
* the same (agent, chat) pair MUST overwrite whatever ended before — including
|
|
10331
|
-
* an `evicted` row left by a previous terminate. The previous "revival
|
|
10332
|
-
* defense" conflated two concerns: "this runtime session ended" (which is
|
|
10333
|
-
* what `evicted` actually means) and "this chat is permanently archived for
|
|
10334
|
-
* this agent" (a chat-level decision that should live on `chats`, not here).
|
|
10335
|
-
* See proposals/hub-agent-messaging-reply-and-mentions §M2-session-lifecycle.
|
|
10336
|
-
*/
|
|
10337
|
-
async function upsertSessionState(db, agentId, chatId, state, organizationId, notifier) {
|
|
10338
|
-
const now = /* @__PURE__ */ new Date();
|
|
10339
|
-
let wrote = false;
|
|
10340
|
-
await db.transaction(async (tx) => {
|
|
10341
|
-
await tx.insert(agentChatSessions).values({
|
|
10342
|
-
agentId,
|
|
10343
|
-
chatId,
|
|
10344
|
-
state,
|
|
10345
|
-
updatedAt: now
|
|
10346
|
-
}).onConflictDoUpdate({
|
|
10347
|
-
target: [agentChatSessions.agentId, agentChatSessions.chatId],
|
|
10348
|
-
set: {
|
|
10349
|
-
state,
|
|
10350
|
-
updatedAt: now
|
|
10351
|
-
}
|
|
10352
|
-
});
|
|
10353
|
-
const [counts] = await tx.select({
|
|
10354
|
-
active: sql`count(*) FILTER (WHERE ${agentChatSessions.state} = 'active')::int`,
|
|
10355
|
-
total: sql`count(*) FILTER (WHERE ${agentChatSessions.state} != 'evicted')::int`
|
|
10356
|
-
}).from(agentChatSessions).where(eq(agentChatSessions.agentId, agentId));
|
|
10357
|
-
const activeSessions = counts?.active ?? 0;
|
|
10358
|
-
const totalSessions = counts?.total ?? 0;
|
|
10359
|
-
await tx.update(agentPresence).set({
|
|
10360
|
-
activeSessions,
|
|
10361
|
-
totalSessions,
|
|
10362
|
-
lastSeenAt: now
|
|
10363
|
-
}).where(eq(agentPresence.agentId, agentId));
|
|
10364
|
-
wrote = true;
|
|
10365
|
-
});
|
|
10366
|
-
if (wrote && notifier) notifier.notifySessionStateChange(agentId, chatId, state, organizationId).catch(() => {});
|
|
10367
|
-
}
|
|
10368
|
-
async function resetActivity(db, agentId) {
|
|
10369
|
-
const now = /* @__PURE__ */ new Date();
|
|
10370
|
-
await db.update(agentPresence).set({
|
|
10371
|
-
runtimeState: "idle",
|
|
10372
|
-
runtimeUpdatedAt: now
|
|
10373
|
-
}).where(eq(agentPresence.agentId, agentId));
|
|
10374
|
-
}
|
|
10375
|
-
async function getActivityOverview(db) {
|
|
10376
|
-
const [agentCounts] = await db.select({
|
|
10377
|
-
total: sql`count(*)::int`,
|
|
10378
|
-
running: sql`count(*) FILTER (WHERE ${agentPresence.runtimeState} IS NOT NULL)::int`,
|
|
10379
|
-
idle: sql`count(*) FILTER (WHERE ${agentPresence.runtimeState} = 'idle')::int`,
|
|
10380
|
-
working: sql`count(*) FILTER (WHERE ${agentPresence.runtimeState} = 'working')::int`,
|
|
10381
|
-
blocked: sql`count(*) FILTER (WHERE ${agentPresence.runtimeState} = 'blocked')::int`,
|
|
10382
|
-
error: sql`count(*) FILTER (WHERE ${agentPresence.runtimeState} = 'error')::int`
|
|
10383
|
-
}).from(agentPresence);
|
|
10384
|
-
const [clientCounts] = await db.select({ count: sql`count(*)::int` }).from(clients).where(eq(clients.status, "connected"));
|
|
10385
|
-
return {
|
|
10386
|
-
total: agentCounts?.total ?? 0,
|
|
10387
|
-
running: agentCounts?.running ?? 0,
|
|
10388
|
-
byState: {
|
|
10389
|
-
idle: agentCounts?.idle ?? 0,
|
|
10390
|
-
working: agentCounts?.working ?? 0,
|
|
10391
|
-
blocked: agentCounts?.blocked ?? 0,
|
|
10392
|
-
error: agentCounts?.error ?? 0
|
|
10393
|
-
},
|
|
10394
|
-
clients: clientCounts?.count ?? 0
|
|
10395
|
-
};
|
|
10396
|
-
}
|
|
10397
|
-
/**
|
|
10398
|
-
* List agents with active runtime state.
|
|
10399
|
-
* When scope is provided, filters to agents visible to the member.
|
|
10400
|
-
*/
|
|
10401
|
-
async function listAgentsWithRuntime(db, scope) {
|
|
10402
|
-
if (!scope) return db.select().from(agentPresence).where(isNotNull(agentPresence.runtimeState));
|
|
10403
|
-
return db.select({
|
|
10404
|
-
agentId: agentPresence.agentId,
|
|
10405
|
-
status: agentPresence.status,
|
|
10406
|
-
instanceId: agentPresence.instanceId,
|
|
10407
|
-
connectedAt: agentPresence.connectedAt,
|
|
10408
|
-
lastSeenAt: agentPresence.lastSeenAt,
|
|
10409
|
-
clientId: agentPresence.clientId,
|
|
10410
|
-
runtimeType: agentPresence.runtimeType,
|
|
10411
|
-
runtimeVersion: agentPresence.runtimeVersion,
|
|
10412
|
-
runtimeState: agentPresence.runtimeState,
|
|
10413
|
-
activeSessions: agentPresence.activeSessions,
|
|
10414
|
-
totalSessions: agentPresence.totalSessions,
|
|
10415
|
-
runtimeUpdatedAt: agentPresence.runtimeUpdatedAt,
|
|
10416
|
-
type: agents.type
|
|
10417
|
-
}).from(agentPresence).innerJoin(agents, eq(agentPresence.agentId, agents.uuid)).where(and(isNotNull(agentPresence.runtimeState), agentVisibilityCondition(scope)));
|
|
10418
|
-
}
|
|
10419
11449
|
/** Serialize a Date to ISO string, or null. */
|
|
10420
11450
|
function serializeDate(d) {
|
|
10421
11451
|
return d ? d.toISOString() : null;
|
|
@@ -10439,11 +11469,27 @@ async function adminClientRoutes(app) {
|
|
|
10439
11469
|
lastSeenAt: c.lastSeenAt.toISOString()
|
|
10440
11470
|
}));
|
|
10441
11471
|
});
|
|
11472
|
+
app.get("/me/agents", async (request) => {
|
|
11473
|
+
const scope = memberScope(request);
|
|
11474
|
+
return await listMyPinnedAgents(app.db, {
|
|
11475
|
+
userId: scope.userId,
|
|
11476
|
+
organizationId: scope.organizationId
|
|
11477
|
+
});
|
|
11478
|
+
});
|
|
11479
|
+
app.patch("/:clientId/capabilities", async (request, reply) => {
|
|
11480
|
+
const scope = memberScope(request);
|
|
11481
|
+
await assertClientOwner(app.db, request.params.clientId, scope);
|
|
11482
|
+
const body = updateClientCapabilitiesSchema.parse(request.body);
|
|
11483
|
+
await updateClientCapabilities(app.db, request.params.clientId, body.capabilities);
|
|
11484
|
+
return reply.status(204).send();
|
|
11485
|
+
});
|
|
10442
11486
|
app.get("/:clientId", async (request) => {
|
|
10443
11487
|
const scope = memberScope(request);
|
|
10444
11488
|
await assertClientOwner(app.db, request.params.clientId, scope);
|
|
10445
11489
|
const client = await getClient(app.db, request.params.clientId);
|
|
10446
11490
|
if (!client) throw new Error("unreachable: client missing after owner check");
|
|
11491
|
+
const metadata = client.metadata ?? {};
|
|
11492
|
+
const capabilities = metadata.capabilities && typeof metadata.capabilities === "object" ? metadata.capabilities : {};
|
|
10447
11493
|
return {
|
|
10448
11494
|
id: client.id,
|
|
10449
11495
|
userId: client.userId,
|
|
@@ -10452,7 +11498,8 @@ async function adminClientRoutes(app) {
|
|
|
10452
11498
|
hostname: client.hostname,
|
|
10453
11499
|
os: client.os,
|
|
10454
11500
|
connectedAt: serializeDate(client.connectedAt),
|
|
10455
|
-
lastSeenAt: client.lastSeenAt.toISOString()
|
|
11501
|
+
lastSeenAt: client.lastSeenAt.toISOString(),
|
|
11502
|
+
capabilities
|
|
10456
11503
|
};
|
|
10457
11504
|
});
|
|
10458
11505
|
app.post("/:clientId/disconnect", async (request) => {
|
|
@@ -12751,7 +13798,8 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
12751
13798
|
agentId: agent.uuid,
|
|
12752
13799
|
name: agent.name,
|
|
12753
13800
|
displayName: agent.displayName,
|
|
12754
|
-
agentType: agent.type
|
|
13801
|
+
agentType: agent.type,
|
|
13802
|
+
runtimeProvider: agent.runtimeProvider
|
|
12755
13803
|
});
|
|
12756
13804
|
if (!parsed.success) {
|
|
12757
13805
|
app.log.warn({
|
|
@@ -12787,6 +13835,7 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
12787
13835
|
inboxId: agents.inboxId,
|
|
12788
13836
|
status: agents.status,
|
|
12789
13837
|
clientId: agents.clientId,
|
|
13838
|
+
runtimeProvider: agents.runtimeProvider,
|
|
12790
13839
|
clientUserId: clients.userId,
|
|
12791
13840
|
managerUserId: members.userId
|
|
12792
13841
|
}).from(agents).leftJoin(clients, eq(agents.clientId, clients.id)).leftJoin(members, eq(agents.managerId, members.id)).where(and(eq(agents.uuid, bindRequest.agentId))).limit(1);
|
|
@@ -12821,6 +13870,10 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
12821
13870
|
sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.NOT_OWNED);
|
|
12822
13871
|
return;
|
|
12823
13872
|
}
|
|
13873
|
+
if (bindRequest.runtimeType !== agent.runtimeProvider) {
|
|
13874
|
+
sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.RUNTIME_PROVIDER_MISMATCH);
|
|
13875
|
+
return;
|
|
13876
|
+
}
|
|
12824
13877
|
await bindAgent(app.db, agent.id, {
|
|
12825
13878
|
clientId,
|
|
12826
13879
|
instanceId,
|
|
@@ -13169,7 +14222,8 @@ const authIdentities = pgTable("auth_identities", {
|
|
|
13169
14222
|
}, (table) => [
|
|
13170
14223
|
unique("uq_auth_identities_provider_identifier").on(table.provider, table.identifier),
|
|
13171
14224
|
index("idx_auth_identities_user").on(table.userId),
|
|
13172
|
-
index("idx_auth_identities_email").on(table.email)
|
|
14225
|
+
index("idx_auth_identities_email").on(table.email),
|
|
14226
|
+
uniqueIndex("uq_auth_identities_user_github").on(table.userId).where(sql`provider = 'github'`)
|
|
13173
14227
|
]);
|
|
13174
14228
|
/**
|
|
13175
14229
|
* Find or create the user backing a GitHub OAuth identity. Idempotent —
|
|
@@ -13640,9 +14694,7 @@ async function githubOauthRoutes(app) {
|
|
|
13640
14694
|
return completeOauthFlow(app, request, reply, profile, next);
|
|
13641
14695
|
});
|
|
13642
14696
|
app.get("/dev-callback", async (request, reply) => {
|
|
13643
|
-
|
|
13644
|
-
const devEnabled = oauthCfg?.devCallbackEnabled === true;
|
|
13645
|
-
if (isProd || !devEnabled) return reply.status(404).send({ error: "Not found" });
|
|
14697
|
+
if (process.env.NODE_ENV === "production") return reply.status(404).send({ error: "Not found" });
|
|
13646
14698
|
const params = githubDevCallbackQuerySchema.parse(request.query);
|
|
13647
14699
|
const next = safeRedirectPath(params.next ?? null);
|
|
13648
14700
|
return completeOauthFlow(app, request, reply, {
|
|
@@ -14046,7 +15098,7 @@ async function inferWizardStep(app, m) {
|
|
|
14046
15098
|
* landing page.
|
|
14047
15099
|
*/
|
|
14048
15100
|
async function publicInvitePreviewRoute(app) {
|
|
14049
|
-
const { previewInvitation } = await import("./invitation-BTlGMy0o-
|
|
15101
|
+
const { previewInvitation } = await import("./invitation-BTlGMy0o-Coj07kYi.mjs");
|
|
14050
15102
|
app.get("/:token/preview", async (request, reply) => {
|
|
14051
15103
|
if (!request.params.token) throw new UnauthorizedError("Token required");
|
|
14052
15104
|
const preview = await previewInvitation(app.db, request.params.token);
|
|
@@ -14076,7 +15128,7 @@ async function adminInvitationRoutes(app) {
|
|
|
14076
15128
|
const m = requireMember(request);
|
|
14077
15129
|
if (m.role !== "admin") throw new ForbiddenError("Admin role required");
|
|
14078
15130
|
if (request.params.id !== m.organizationId) throw new ForbiddenError("Cannot rotate invitations for another organization");
|
|
14079
|
-
const { rotateInvitation } = await import("./invitation-BTlGMy0o-
|
|
15131
|
+
const { rotateInvitation } = await import("./invitation-BTlGMy0o-Coj07kYi.mjs");
|
|
14080
15132
|
const inv = await rotateInvitation(app.db, m.organizationId, m.userId);
|
|
14081
15133
|
return {
|
|
14082
15134
|
id: inv.id,
|
|
@@ -15443,6 +16495,7 @@ function createConfigService(opts) {
|
|
|
15443
16495
|
*/
|
|
15444
16496
|
function applyPatch(current, patch) {
|
|
15445
16497
|
return {
|
|
16498
|
+
kind: current.kind,
|
|
15446
16499
|
prompt: patch.prompt ?? current.prompt,
|
|
15447
16500
|
model: patch.model ?? current.model,
|
|
15448
16501
|
mcpServers: patch.mcpServers ?? current.mcpServers,
|
|
@@ -15468,13 +16521,26 @@ function createConfigService(opts) {
|
|
|
15468
16521
|
async function readRow(agentId) {
|
|
15469
16522
|
const [row] = await db.select().from(agentConfigs).where(eq(agentConfigs.agentId, agentId)).limit(1);
|
|
15470
16523
|
if (!row) throw new NotFoundError(`Agent config "${agentId}" not found`);
|
|
15471
|
-
|
|
16524
|
+
const payload = agentRuntimeConfigPayloadSchema$1.parse(row.payload);
|
|
16525
|
+
return {
|
|
16526
|
+
...row,
|
|
16527
|
+
payload
|
|
16528
|
+
};
|
|
16529
|
+
}
|
|
16530
|
+
async function readRuntimeProviderFor(agentId) {
|
|
16531
|
+
const [row] = await db.select({ runtimeProvider: agents.runtimeProvider }).from(agents).where(eq(agents.uuid, agentId)).limit(1);
|
|
16532
|
+
if (!row) throw new NotFoundError(`Agent "${agentId}" not found`);
|
|
16533
|
+
return row.runtimeProvider;
|
|
15472
16534
|
}
|
|
15473
16535
|
async function commitWrite(agentId, patch, expectedVersion, updatedBy) {
|
|
15474
16536
|
const current = await readRow(agentId);
|
|
15475
16537
|
if (current.version !== expectedVersion) throw new ConflictError(`Agent config "${agentId}" version mismatch: expected ${expectedVersion}, got ${current.version}`);
|
|
15476
|
-
const
|
|
15477
|
-
const
|
|
16538
|
+
const provider = await readRuntimeProviderFor(agentId);
|
|
16539
|
+
const synced = {
|
|
16540
|
+
...applyPatch(current.payload, patch),
|
|
16541
|
+
kind: provider
|
|
16542
|
+
};
|
|
16543
|
+
const validated = agentRuntimeConfigPayloadSchema$1.parse(synced);
|
|
15478
16544
|
const [updated] = await db.update(agentConfigs).set({
|
|
15479
16545
|
version: sql`${agentConfigs.version} + 1`,
|
|
15480
16546
|
payload: validated,
|
|
@@ -15581,7 +16647,12 @@ function createConfigService(opts) {
|
|
|
15581
16647
|
},
|
|
15582
16648
|
async dryRun(agentId, patch) {
|
|
15583
16649
|
const row = await readRow(agentId);
|
|
15584
|
-
const
|
|
16650
|
+
const provider = await readRuntimeProviderFor(agentId);
|
|
16651
|
+
const synced = {
|
|
16652
|
+
...applyPatch(row.payload, patch),
|
|
16653
|
+
kind: provider
|
|
16654
|
+
};
|
|
16655
|
+
const next = agentRuntimeConfigPayloadSchema$1.parse(synced);
|
|
15585
16656
|
const diff = computeDiff(row.payload, next);
|
|
15586
16657
|
return {
|
|
15587
16658
|
current: {
|
|
@@ -16672,4 +17743,4 @@ function registerSaaSConnectCommand(program) {
|
|
|
16672
17743
|
});
|
|
16673
17744
|
}
|
|
16674
17745
|
//#endregion
|
|
16675
|
-
export {
|
|
17746
|
+
export { status as $, checkServerHealth as A, isDockerAvailable as B, checkAgentConfigs as C, checkDocker as D, checkDatabase as E, installClientService as F, createOwner as G, ClientRuntime as H, isServiceSupported as I, fail as J, hasUser as K, resolveCliInvocation as L, checkWebSocket as M, printResults as N, checkNodeVersion as O, getClientServiceStatus as P, setJsonMode as Q, uninstallClientService as R, runMigrations as S, checkClientConfig as T, handleClientOrgMismatch as U, stopPostgres as V, rotateClientIdWithBackup as W, blank as X, success as Y, print as Z, onboardCreate as _, declineUpdate as a, probeCapabilities as at, createApiNameResolver as b, COMMAND_VERSION as c, isInteractive as d, ClientOrgMismatchError as et, promptAddAgent as f, onboardCheck as g, loadOnboardState as h, createExecuteUpdate as i, cleanWorkspaces as it, checkServerReachable as j, checkServerConfig as k, reconcileLocalRuntimeProviders as l, formatCheckReport as m, deriveHubUrlFromToken as n, SdkError as nt, promptUpdate as o, applyClientLoggerConfig as ot, promptMissingFields as p, resolveReplyToFromEnv as q, registerSaaSConnectCommand as r, SessionRegistry as rt, startServer as s, configureClientLoggerForService as st, HubUrlDerivationError as t, FirstTreeHubSDK as tt, uploadClientCapabilities as u, saveOnboardState as v, checkBackgroundService as w, migrateLocalAgentDirs as x, runHomeMigration as y, ensurePostgres as z };
|