@agent-team-foundation/first-tree-hub 0.3.5 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{bootstrap-mhkpeOEc.mjs → bootstrap-BU_7B03u.mjs} +20 -18
- package/dist/cli/index.mjs +39 -34
- package/dist/{core-CHL_dgzu.mjs → core-jjk1xFW_.mjs} +463 -965
- package/dist/drizzle/0007_decouple_context_tree.sql +2 -0
- package/dist/drizzle/0008_uuid_identity.sql +12 -0
- package/dist/drizzle/meta/_journal.json +14 -0
- package/dist/index.mjs +3 -3
- package/dist/web/assets/index-LFh6j4ki.js +280 -0
- package/dist/web/assets/index-vo2Sa6IQ.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-BURu6jt9.css +0 -1
- package/dist/web/assets/index-DhpjUi0Y.js +0 -272
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as setConfigValue, a as getGitHubUsername, b as resolveConfigReadonly, c as DEFAULT_CONFIG_DIR, d as agentConfigSchema, f as clientConfigSchema, g as loadAgents, h as initConfig, p as collectMissingPrompts,
|
|
1
|
+
import { S as setConfigValue, a as getGitHubUsername, b as resolveConfigReadonly, c as DEFAULT_CONFIG_DIR, d as agentConfigSchema, f as clientConfigSchema, g as loadAgents, h as initConfig, p as collectMissingPrompts, s as resolveServerUrl, t as bootstrapToken$1, u as DEFAULT_HOME_DIR$1, x as serverConfigSchema } from "./bootstrap-BU_7B03u.mjs";
|
|
2
2
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { EventEmitter } from "node:events";
|
|
@@ -45,18 +45,19 @@ var FirstTreeHubSDK = class {
|
|
|
45
45
|
async register() {
|
|
46
46
|
const agent = await this.requestJson("/api/v1/agent/me");
|
|
47
47
|
return {
|
|
48
|
-
agentId: agent.
|
|
48
|
+
agentId: agent.uuid,
|
|
49
49
|
inboxId: agent.inboxId,
|
|
50
50
|
status: agent.status,
|
|
51
51
|
displayName: agent.displayName,
|
|
52
52
|
type: agent.type,
|
|
53
53
|
delegateMention: agent.delegateMention ?? null,
|
|
54
|
+
profile: agent.profile ?? null,
|
|
54
55
|
metadata: agent.metadata ?? {}
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
|
-
/** Fetch Context Tree configuration from the server. */
|
|
58
|
+
/** Fetch Context Tree configuration from the server (public endpoint). */
|
|
58
59
|
async getContextTreeConfig() {
|
|
59
|
-
return this.requestJson("/api/v1/
|
|
60
|
+
return this.requestJson("/api/v1/context-tree/info");
|
|
60
61
|
}
|
|
61
62
|
/** Fetch pending inbox entries. */
|
|
62
63
|
async pull(limit = 10) {
|
|
@@ -78,8 +79,8 @@ var FirstTreeHubSDK = class {
|
|
|
78
79
|
});
|
|
79
80
|
}
|
|
80
81
|
/** Send a direct message to another agent. */
|
|
81
|
-
async sendToAgent(
|
|
82
|
-
return this.requestJson(`/api/v1/agent/agents/${
|
|
82
|
+
async sendToAgent(agentName, data) {
|
|
83
|
+
return this.requestJson(`/api/v1/agent/agents/${agentName}/messages`, {
|
|
83
84
|
method: "POST",
|
|
84
85
|
body: JSON.stringify(data)
|
|
85
86
|
});
|
|
@@ -433,27 +434,23 @@ defineConfig({
|
|
|
433
434
|
secret: true
|
|
434
435
|
})
|
|
435
436
|
},
|
|
436
|
-
contextTree: {
|
|
437
|
+
contextTree: optional({
|
|
437
438
|
repo: field(z.string(), {
|
|
438
439
|
env: "FIRST_TREE_HUB_CONTEXT_TREE_REPO",
|
|
439
440
|
prompt: { message: "Context Tree repo URL (e.g. https://github.com/org/first-tree):" }
|
|
440
441
|
}),
|
|
441
|
-
branch: field(z.string().default("main"))
|
|
442
|
-
|
|
443
|
-
},
|
|
442
|
+
branch: field(z.string().default("main"))
|
|
443
|
+
}),
|
|
444
444
|
github: {
|
|
445
|
-
token: field(z.string(), {
|
|
445
|
+
token: field(z.string().optional(), {
|
|
446
446
|
env: "FIRST_TREE_HUB_GITHUB_TOKEN",
|
|
447
|
-
secret: true
|
|
448
|
-
prompt: {
|
|
449
|
-
message: "GitHub token (create at https://github.com/settings/tokens → repo scope):",
|
|
450
|
-
type: "password"
|
|
451
|
-
}
|
|
447
|
+
secret: true
|
|
452
448
|
}),
|
|
453
449
|
webhookSecret: field(z.string().optional(), {
|
|
454
450
|
env: "FIRST_TREE_HUB_GITHUB_WEBHOOK_SECRET",
|
|
455
451
|
secret: true
|
|
456
|
-
})
|
|
452
|
+
}),
|
|
453
|
+
allowedOrg: field(z.string().optional(), { env: "FIRST_TREE_HUB_GITHUB_ALLOWED_ORG" })
|
|
457
454
|
},
|
|
458
455
|
cors: optional({ origin: field(z.string(), { env: "FIRST_TREE_HUB_CORS_ORIGIN" }) }),
|
|
459
456
|
rateLimit: optional({
|
|
@@ -497,14 +494,13 @@ function bootstrapWorkspace(options) {
|
|
|
497
494
|
contextTreePath
|
|
498
495
|
};
|
|
499
496
|
writeFileSync(join(agentDir, "identity.json"), JSON.stringify(identityData, null, 2), "utf-8");
|
|
497
|
+
if (identity.profile) writeFileSync(join(contextDir, "self.md"), identity.profile, "utf-8");
|
|
500
498
|
if (contextTreePath) {
|
|
501
|
-
const selfNodePath = join(contextTreePath, "members", identity.agentId, "NODE.md");
|
|
502
|
-
if (existsSync(selfNodePath)) copyFileSync(selfNodePath, join(contextDir, "self.md"));
|
|
503
499
|
const agentMdPath = join(contextTreePath, "AGENT.md");
|
|
504
500
|
if (existsSync(agentMdPath)) copyFileSync(agentMdPath, join(contextDir, "agent-instructions.md"));
|
|
505
501
|
const rootNodePath = join(contextTreePath, "NODE.md");
|
|
506
502
|
if (existsSync(rootNodePath)) copyFileSync(rootNodePath, join(contextDir, "domain-map.md"));
|
|
507
|
-
}
|
|
503
|
+
}
|
|
508
504
|
writeFileSync(join(agentDir, "tools.md"), generateToolsDoc(), "utf-8");
|
|
509
505
|
}
|
|
510
506
|
function generateToolsDoc() {
|
|
@@ -842,10 +838,9 @@ const createClaudeCodeHandler = (config) => {
|
|
|
842
838
|
/**
|
|
843
839
|
* Generate a CLAUDE.md file from .agent/ bootstrap data.
|
|
844
840
|
*
|
|
845
|
-
*
|
|
846
|
-
*
|
|
847
|
-
*
|
|
848
|
-
* Layer 3 (on-demand): Agent reads specific domain nodes via contextTreePath
|
|
841
|
+
* Layer 1 (always): Agent identity + profile (from Hub)
|
|
842
|
+
* Layer 2 (if Context Tree configured): Operating instructions + domain map
|
|
843
|
+
* Layer 3 (if Context Tree configured): Context Tree location for on-demand reading
|
|
849
844
|
*/
|
|
850
845
|
function generateClaudeMd(workspacePath, identity, contextTreePath) {
|
|
851
846
|
const sections = [];
|
|
@@ -857,25 +852,18 @@ function generateClaudeMd(workspacePath, identity, contextTreePath) {
|
|
|
857
852
|
if (existsSync(selfMdPath)) {
|
|
858
853
|
const selfContent = readFileSync(selfMdPath, "utf-8");
|
|
859
854
|
sections.push(`## Your Profile\n\n${selfContent}\n`);
|
|
860
|
-
}
|
|
855
|
+
}
|
|
861
856
|
const agentInstructionsPath = join(contextDir, "agent-instructions.md");
|
|
862
857
|
if (existsSync(agentInstructionsPath)) {
|
|
863
858
|
const instructions = readFileSync(agentInstructionsPath, "utf-8");
|
|
864
|
-
sections.push(`##
|
|
865
|
-
}
|
|
859
|
+
sections.push(`## Operating Instructions\n\n${instructions}\n`);
|
|
860
|
+
}
|
|
866
861
|
const domainMapPath = join(contextDir, "domain-map.md");
|
|
867
862
|
if (existsSync(domainMapPath)) {
|
|
868
863
|
const domainMap = readFileSync(domainMapPath, "utf-8");
|
|
869
864
|
sections.push(`## Organization Domain Map\n\n${domainMap}\n`);
|
|
870
865
|
}
|
|
871
866
|
if (contextTreePath) sections.push(`## Context Tree Location\n\nThe full Context Tree is available at: \`${contextTreePath}\`\n\nRead specific domain nodes as needed following the operating instructions above.\n`);
|
|
872
|
-
else {
|
|
873
|
-
const degradedPath = join(contextDir, "degraded.md");
|
|
874
|
-
if (existsSync(degradedPath)) {
|
|
875
|
-
const degradedMsg = readFileSync(degradedPath, "utf-8");
|
|
876
|
-
sections.push(`## Context Tree Location\n\nWARNING: ${degradedMsg}\nYou can still use the SDK tools below, but you lack organizational context for decisions.\n`);
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
867
|
const toolsPath = join(workspacePath, ".agent", "tools.md");
|
|
880
868
|
if (existsSync(toolsPath)) {
|
|
881
869
|
const toolsContent = readFileSync(toolsPath, "utf-8");
|
|
@@ -1299,6 +1287,7 @@ var AgentSlot = class {
|
|
|
1299
1287
|
displayName: agent.displayName,
|
|
1300
1288
|
type: agent.type,
|
|
1301
1289
|
delegateMention: agent.delegateMention,
|
|
1290
|
+
profile: agent.profile,
|
|
1302
1291
|
metadata: agent.metadata
|
|
1303
1292
|
},
|
|
1304
1293
|
sdk: this.connection.sdk,
|
|
@@ -2058,14 +2047,32 @@ async function onboardCheck(args) {
|
|
|
2058
2047
|
key: "server_reachable",
|
|
2059
2048
|
label: "Server reachable",
|
|
2060
2049
|
status: res.ok ? "ok" : "error",
|
|
2061
|
-
value: res.ok ? "
|
|
2050
|
+
value: res.ok ? "healthy" : `HTTP ${res.status}`
|
|
2062
2051
|
});
|
|
2052
|
+
if (res.ok) try {
|
|
2053
|
+
const configRes = await fetch(`${serverUrl}/api/v1/bootstrap/config`);
|
|
2054
|
+
if (configRes.ok) {
|
|
2055
|
+
const config = await configRes.json();
|
|
2056
|
+
if (config.allowedOrg) items.push({
|
|
2057
|
+
key: "allowed_org",
|
|
2058
|
+
label: "GitHub org",
|
|
2059
|
+
status: "ok",
|
|
2060
|
+
value: config.allowedOrg
|
|
2061
|
+
});
|
|
2062
|
+
else items.push({
|
|
2063
|
+
key: "allowed_org",
|
|
2064
|
+
label: "GitHub org",
|
|
2065
|
+
status: "error",
|
|
2066
|
+
hint: "FIRST_TREE_HUB_GITHUB_ALLOWED_ORG not configured on server"
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
} catch {}
|
|
2063
2070
|
} catch {
|
|
2064
2071
|
items.push({
|
|
2065
2072
|
key: "server_reachable",
|
|
2066
2073
|
label: "Server reachable",
|
|
2067
2074
|
status: "error",
|
|
2068
|
-
|
|
2075
|
+
hint: "Cannot connect to server"
|
|
2069
2076
|
});
|
|
2070
2077
|
}
|
|
2071
2078
|
} catch {
|
|
@@ -2073,126 +2080,32 @@ async function onboardCheck(args) {
|
|
|
2073
2080
|
key: "server",
|
|
2074
2081
|
label: "Server URL",
|
|
2075
2082
|
status: "missing_required",
|
|
2076
|
-
hint: "--server
|
|
2083
|
+
hint: "Provide via --server, FIRST_TREE_HUB_SERVER, or config"
|
|
2077
2084
|
});
|
|
2078
2085
|
}
|
|
2079
|
-
|
|
2080
|
-
if (repoPath) items.push({
|
|
2081
|
-
key: "repo",
|
|
2082
|
-
label: "Context Tree repo",
|
|
2083
|
-
status: "ok",
|
|
2084
|
-
value: repoPath
|
|
2085
|
-
});
|
|
2086
|
-
else {
|
|
2087
|
-
const serverAvailable = items.some((i) => i.key === "server" && i.status === "ok");
|
|
2088
|
-
items.push({
|
|
2089
|
-
key: "repo",
|
|
2090
|
-
label: "Context Tree repo",
|
|
2091
|
-
status: "missing_required",
|
|
2092
|
-
hint: serverAvailable ? "auto-clone failed (check server Context Tree config and gh auth)" : "configure --server first (repo will be auto-cloned from server)"
|
|
2093
|
-
});
|
|
2094
|
-
}
|
|
2095
|
-
items.push(args.id ? {
|
|
2086
|
+
if (args.id) items.push({
|
|
2096
2087
|
key: "id",
|
|
2097
|
-
label: "
|
|
2088
|
+
label: "Agent ID",
|
|
2098
2089
|
status: "ok",
|
|
2099
2090
|
value: args.id
|
|
2100
|
-
}
|
|
2091
|
+
});
|
|
2092
|
+
else items.push({
|
|
2101
2093
|
key: "id",
|
|
2102
|
-
label: "
|
|
2094
|
+
label: "Agent ID",
|
|
2103
2095
|
status: "missing_required",
|
|
2104
|
-
hint: "
|
|
2096
|
+
hint: "Provide via --id"
|
|
2105
2097
|
});
|
|
2106
|
-
|
|
2098
|
+
if (args.type) items.push({
|
|
2107
2099
|
key: "type",
|
|
2108
|
-
label: "type",
|
|
2100
|
+
label: "Agent type",
|
|
2109
2101
|
status: "ok",
|
|
2110
2102
|
value: args.type
|
|
2111
|
-
} : {
|
|
2112
|
-
key: "type",
|
|
2113
|
-
label: "type",
|
|
2114
|
-
status: "missing_required",
|
|
2115
|
-
hint: "human | personal_assistant | autonomous_agent"
|
|
2116
2103
|
});
|
|
2117
|
-
items.push(args.role ? {
|
|
2118
|
-
key: "role",
|
|
2119
|
-
label: "role",
|
|
2120
|
-
status: "ok",
|
|
2121
|
-
value: args.role
|
|
2122
|
-
} : {
|
|
2123
|
-
key: "role",
|
|
2124
|
-
label: "role",
|
|
2125
|
-
status: "missing_required",
|
|
2126
|
-
hint: "e.g. \"Engineer\""
|
|
2127
|
-
});
|
|
2128
|
-
items.push(args.domains ? {
|
|
2129
|
-
key: "domains",
|
|
2130
|
-
label: "domains",
|
|
2131
|
-
status: "ok",
|
|
2132
|
-
value: args.domains
|
|
2133
|
-
} : {
|
|
2134
|
-
key: "domains",
|
|
2135
|
-
label: "domains",
|
|
2136
|
-
status: "missing_required",
|
|
2137
|
-
hint: "Comma-separated, e.g. \"backend,infra\""
|
|
2138
|
-
});
|
|
2139
|
-
items.push(args.displayName ? {
|
|
2140
|
-
key: "display_name",
|
|
2141
|
-
label: "display-name",
|
|
2142
|
-
status: "ok",
|
|
2143
|
-
value: args.displayName
|
|
2144
|
-
} : {
|
|
2145
|
-
key: "display_name",
|
|
2146
|
-
label: "display-name",
|
|
2147
|
-
status: "missing_optional",
|
|
2148
|
-
hint: `defaults to "${args.id ?? ""}"`
|
|
2149
|
-
});
|
|
2150
|
-
if (args.type === "human") items.push(args.assistant ? {
|
|
2151
|
-
key: "assistant",
|
|
2152
|
-
label: "assistant",
|
|
2153
|
-
status: "ok",
|
|
2154
|
-
value: args.assistant
|
|
2155
|
-
} : {
|
|
2156
|
-
key: "assistant",
|
|
2157
|
-
label: "assistant",
|
|
2158
|
-
status: "missing_optional",
|
|
2159
|
-
hint: "Also create a personal_assistant"
|
|
2160
|
-
});
|
|
2161
|
-
if (args.type !== "human" || args.assistant) items.push(args.feishuBotAppId ? {
|
|
2162
|
-
key: "feishu_bot",
|
|
2163
|
-
label: "feishu-bot-app-id",
|
|
2164
|
-
status: "ok",
|
|
2165
|
-
value: args.feishuBotAppId
|
|
2166
|
-
} : {
|
|
2167
|
-
key: "feishu_bot",
|
|
2168
|
-
label: "feishu-bot-app-id",
|
|
2169
|
-
status: "missing_optional",
|
|
2170
|
-
hint: "Feishu bot App ID"
|
|
2171
|
-
});
|
|
2172
|
-
if (args.id && repoPath) if (existsSync(join(repoPath, "members", args.id))) try {
|
|
2173
|
-
execSync(`git ls-files --error-unmatch members/${args.id}/NODE.md`, {
|
|
2174
|
-
cwd: repoPath,
|
|
2175
|
-
stdio: "pipe"
|
|
2176
|
-
});
|
|
2177
|
-
items.push({
|
|
2178
|
-
key: "conflict",
|
|
2179
|
-
label: `ID "${args.id}" availability`,
|
|
2180
|
-
status: "warning",
|
|
2181
|
-
value: "already exists (will overwrite)"
|
|
2182
|
-
});
|
|
2183
|
-
} catch {
|
|
2184
|
-
items.push({
|
|
2185
|
-
key: "conflict",
|
|
2186
|
-
label: `ID "${args.id}" availability`,
|
|
2187
|
-
status: "ok",
|
|
2188
|
-
value: "resuming (local files from previous run)"
|
|
2189
|
-
});
|
|
2190
|
-
}
|
|
2191
2104
|
else items.push({
|
|
2192
|
-
key: "
|
|
2193
|
-
label:
|
|
2194
|
-
status: "
|
|
2195
|
-
|
|
2105
|
+
key: "type",
|
|
2106
|
+
label: "Agent type",
|
|
2107
|
+
status: "missing_required",
|
|
2108
|
+
hint: "Provide via --type"
|
|
2196
2109
|
});
|
|
2197
2110
|
return items;
|
|
2198
2111
|
}
|
|
@@ -2207,330 +2120,74 @@ function formatCheckReport(items) {
|
|
|
2207
2120
|
return lines.join("\n");
|
|
2208
2121
|
}
|
|
2209
2122
|
async function onboardCreate(args) {
|
|
2210
|
-
const
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
const
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
process.stderr.write(`Member "${args.id}" already exists, skipping NODE.md creation.\n`);
|
|
2218
|
-
if (args.assistant) {
|
|
2219
|
-
const existingContent = readFileSync(humanNodePath, "utf-8");
|
|
2220
|
-
if (!existingContent.includes("delegate_mention")) {
|
|
2221
|
-
writeFileSync(humanNodePath, existingContent.replace(/^(---\n[\s\S]*?)(---)/m, `$1delegate_mention: ${args.assistant}\n$2`));
|
|
2222
|
-
process.stderr.write(`Updated delegate_mention → ${args.assistant}\n`);
|
|
2223
|
-
}
|
|
2224
|
-
}
|
|
2225
|
-
} else createMemberNodeMd(repoPath, {
|
|
2226
|
-
id: args.id,
|
|
2227
|
-
type: args.type,
|
|
2228
|
-
displayName: args.displayName ?? args.id,
|
|
2229
|
-
role: args.role,
|
|
2230
|
-
domains: args.domains.split(",").map((d) => d.trim()),
|
|
2231
|
-
owner: ghUsername,
|
|
2232
|
-
github: githubField,
|
|
2233
|
-
delegateMention: args.assistant ?? args.delegateMention ?? null
|
|
2234
|
-
});
|
|
2235
|
-
if (args.assistant) if (existsSync(join(repoPath, "members", args.id, args.assistant, "NODE.md")) && isTrackedByGit(repoPath, join("members", args.id, args.assistant, "NODE.md"))) process.stderr.write(`Assistant "${args.assistant}" already exists, skipping.\n`);
|
|
2236
|
-
else createMemberNodeMd(repoPath, {
|
|
2237
|
-
parentPath: join("members", args.id),
|
|
2238
|
-
id: args.assistant,
|
|
2239
|
-
type: "personal_assistant",
|
|
2240
|
-
displayName: args.assistant,
|
|
2241
|
-
role: `Personal Assistant to ${args.id}`,
|
|
2242
|
-
domains: ["message triage", "task coordination"],
|
|
2243
|
-
owner: ghUsername,
|
|
2244
|
-
github: null,
|
|
2245
|
-
delegateMention: null
|
|
2246
|
-
});
|
|
2123
|
+
const serverUrl = resolveServerUrl(args.server).replace(/\/+$/, "");
|
|
2124
|
+
getGitHubUsername();
|
|
2125
|
+
process.stderr.write(`Bootstrapping agent "${args.id}"...\n`);
|
|
2126
|
+
const metadata = {};
|
|
2127
|
+
if (args.role) metadata.role = args.role;
|
|
2128
|
+
if (args.domains) metadata.domains = args.domains.split(",").map((d) => d.trim());
|
|
2129
|
+
let token;
|
|
2247
2130
|
try {
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2131
|
+
token = (await bootstrapToken$1(serverUrl, args.id, {
|
|
2132
|
+
saveTo: "agent",
|
|
2133
|
+
type: args.type,
|
|
2134
|
+
displayName: args.displayName ?? args.id,
|
|
2135
|
+
profile: args.profile,
|
|
2136
|
+
delegateMention: args.assistant ?? args.delegateMention,
|
|
2137
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : void 0
|
|
2138
|
+
})).token;
|
|
2252
2139
|
} catch (err) {
|
|
2253
|
-
const
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
if (output.includes("VERSION") || output.includes("AGENT.md") || output.includes("Root NODE.md")) throw new Error("Context Tree repo is not properly initialized.\nRun 'context-tree init' in the repo first, or see:\n https://github.com/agent-team-foundation/first-tree\n\n" + output);
|
|
2257
|
-
throw new Error(`Verification failed:\n${output}`);
|
|
2258
|
-
}
|
|
2259
|
-
const baseBranch = `onboard/${args.id}`;
|
|
2260
|
-
let branch = baseBranch;
|
|
2261
|
-
const branchExists = (name) => {
|
|
2262
|
-
try {
|
|
2263
|
-
execSync(`git rev-parse --verify ${name}`, {
|
|
2264
|
-
cwd: repoPath,
|
|
2265
|
-
stdio: "pipe"
|
|
2266
|
-
});
|
|
2267
|
-
return true;
|
|
2268
|
-
} catch {
|
|
2269
|
-
return false;
|
|
2270
|
-
}
|
|
2271
|
-
};
|
|
2272
|
-
if (branchExists(branch)) branch = `${baseBranch}-${Date.now().toString(36)}`;
|
|
2273
|
-
try {
|
|
2274
|
-
execSync("git checkout main", {
|
|
2275
|
-
cwd: repoPath,
|
|
2276
|
-
stdio: "pipe"
|
|
2277
|
-
});
|
|
2278
|
-
} catch {
|
|
2279
|
-
try {
|
|
2280
|
-
execSync("git checkout master", {
|
|
2281
|
-
cwd: repoPath,
|
|
2282
|
-
stdio: "pipe"
|
|
2283
|
-
});
|
|
2284
|
-
} catch {}
|
|
2285
|
-
}
|
|
2286
|
-
execSync(`git checkout -b ${branch}`, {
|
|
2287
|
-
cwd: repoPath,
|
|
2288
|
-
stdio: "pipe"
|
|
2289
|
-
});
|
|
2290
|
-
execSync(`git add members/${args.id}`, {
|
|
2291
|
-
cwd: repoPath,
|
|
2292
|
-
stdio: "pipe"
|
|
2293
|
-
});
|
|
2294
|
-
execFileSync("git", [
|
|
2295
|
-
"commit",
|
|
2296
|
-
"-m",
|
|
2297
|
-
args.assistant ? `feat: onboard ${args.id} + ${args.assistant}` : `feat: onboard ${args.id}`
|
|
2298
|
-
], {
|
|
2299
|
-
cwd: repoPath,
|
|
2300
|
-
stdio: "pipe"
|
|
2301
|
-
});
|
|
2302
|
-
const pushToken = execSync("gh auth token", {
|
|
2303
|
-
encoding: "utf-8",
|
|
2304
|
-
stdio: "pipe"
|
|
2305
|
-
}).trim();
|
|
2306
|
-
const cleanRemote = execSync("git remote get-url origin", {
|
|
2307
|
-
cwd: repoPath,
|
|
2308
|
-
encoding: "utf-8",
|
|
2309
|
-
stdio: "pipe"
|
|
2310
|
-
}).trim();
|
|
2311
|
-
execSync(`git remote set-url origin "${cleanRemote.replace("https://github.com/", `https://x-access-token:${pushToken}@github.com/`)}"`, {
|
|
2312
|
-
cwd: repoPath,
|
|
2313
|
-
stdio: "pipe"
|
|
2314
|
-
});
|
|
2315
|
-
try {
|
|
2316
|
-
execSync(`git push -u origin ${branch}`, {
|
|
2317
|
-
cwd: repoPath,
|
|
2318
|
-
stdio: "pipe"
|
|
2319
|
-
});
|
|
2320
|
-
} finally {
|
|
2321
|
-
execSync(`git remote set-url origin "${cleanRemote}"`, {
|
|
2322
|
-
cwd: repoPath,
|
|
2323
|
-
stdio: "pipe"
|
|
2324
|
-
});
|
|
2140
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2141
|
+
if (msg.includes("already has") || msg.includes("409")) throw new Error(`Agent "${args.id}" already has an active token.\nAsk an admin to revoke the existing token in the Web UI, then re-run onboard.`);
|
|
2142
|
+
throw err;
|
|
2325
2143
|
}
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
}).trim();
|
|
2330
|
-
const state = {
|
|
2331
|
-
args,
|
|
2332
|
-
branch,
|
|
2333
|
-
prUrl: prOutput
|
|
2334
|
-
};
|
|
2335
|
-
mkdirSync(DEFAULT_HOME_DIR$1, { recursive: true });
|
|
2336
|
-
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
2337
|
-
return { prUrl: prOutput };
|
|
2338
|
-
}
|
|
2339
|
-
async function onboardContinue(args) {
|
|
2340
|
-
let state = null;
|
|
2341
|
-
try {
|
|
2342
|
-
state = JSON.parse(readFileSync(STATE_FILE, "utf-8"));
|
|
2343
|
-
} catch {}
|
|
2344
|
-
if (!state && !args.id) throw new Error("No onboard in progress. Run 'first-tree-hub onboard' first to start a new onboard.");
|
|
2345
|
-
const mergedArgs = {
|
|
2346
|
-
...state?.args,
|
|
2347
|
-
...stripUndefined(args)
|
|
2348
|
-
};
|
|
2349
|
-
const serverUrl = resolveServerUrl(mergedArgs.server).replace(/\/+$/, "");
|
|
2350
|
-
const agentToBootstrap = mergedArgs.assistant ?? mergedArgs.id;
|
|
2351
|
-
if (!agentToBootstrap) throw new Error("Cannot determine which agent to bootstrap. Provide --id or run onboard first.");
|
|
2352
|
-
if (!mergedArgs.id) throw new Error("Cannot determine member ID. Provide --id or run onboard first.");
|
|
2353
|
-
process.stderr.write(`Waiting for agent "${agentToBootstrap}" to be synced...\n`);
|
|
2354
|
-
let synced = false;
|
|
2355
|
-
for (let i = 0; i < 30; i++) {
|
|
2144
|
+
process.stderr.write(`Agent "${args.id}" ready.\n`);
|
|
2145
|
+
if (args.assistant) {
|
|
2146
|
+
process.stderr.write(`Bootstrapping assistant "${args.assistant}"...\n`);
|
|
2356
2147
|
try {
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2148
|
+
token = (await bootstrapToken$1(serverUrl, args.assistant, {
|
|
2149
|
+
saveTo: "agent",
|
|
2150
|
+
type: "personal_assistant",
|
|
2151
|
+
displayName: args.assistant,
|
|
2152
|
+
metadata: {
|
|
2153
|
+
role: `Personal Assistant to ${args.id}`,
|
|
2154
|
+
domains: ["message triage", "task coordination"]
|
|
2155
|
+
}
|
|
2156
|
+
})).token;
|
|
2157
|
+
process.stderr.write(`Assistant "${args.assistant}" ready.\n`);
|
|
2362
2158
|
} catch (err) {
|
|
2363
|
-
|
|
2159
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2160
|
+
process.stderr.write(`Warning: Failed to bootstrap assistant "${args.assistant}": ${msg}\n`);
|
|
2364
2161
|
}
|
|
2365
|
-
await sleep(2e3);
|
|
2366
|
-
}
|
|
2367
|
-
if (!synced) throw new Error(`Agent "${agentToBootstrap}" not found after 60s. Trigger sync manually or wait for auto-sync.`);
|
|
2368
|
-
process.stderr.write(`Bootstrapping token for "${agentToBootstrap}"...\n`);
|
|
2369
|
-
let token;
|
|
2370
|
-
try {
|
|
2371
|
-
token = (await bootstrapToken$1(serverUrl, agentToBootstrap, { saveTo: "agent" })).token;
|
|
2372
|
-
} catch (err) {
|
|
2373
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2374
|
-
if (msg.includes("already has") || msg.includes("409")) throw new Error(`Agent "${agentToBootstrap}" already has an active token.\nAsk an admin to revoke the existing token in the Web UI, then re-run:
|
|
2375
|
-
first-tree-hub onboard --continue`);
|
|
2376
|
-
throw err;
|
|
2377
2162
|
}
|
|
2163
|
+
const agentToBootstrap = args.assistant ?? args.id;
|
|
2378
2164
|
process.stderr.write(`Token saved to ${DEFAULT_HOME_DIR$1}/config/agents/${agentToBootstrap}/agent.yaml\n`);
|
|
2379
|
-
if (
|
|
2165
|
+
if (args.feishuBotAppId && args.feishuBotAppSecret) {
|
|
2380
2166
|
const { bindFeishuBot } = await import("./feishu-Y4m2zFc3.mjs").then((n) => n.r);
|
|
2381
2167
|
process.stderr.write("Binding Feishu bot...\n");
|
|
2382
|
-
await bindFeishuBot(serverUrl, token,
|
|
2168
|
+
await bindFeishuBot(serverUrl, token, args.feishuBotAppId, args.feishuBotAppSecret);
|
|
2383
2169
|
process.stderr.write("Feishu bot bound.\n");
|
|
2384
2170
|
}
|
|
2171
|
+
setConfigValue(join(DEFAULT_CONFIG_DIR, "client.yaml"), "server.url", serverUrl);
|
|
2385
2172
|
try {
|
|
2386
2173
|
const { unlinkSync } = await import("node:fs");
|
|
2387
2174
|
unlinkSync(STATE_FILE);
|
|
2388
2175
|
} catch {}
|
|
2389
|
-
const typeLabel =
|
|
2176
|
+
const typeLabel = args.type === "human" ? "Human" : args.type === "autonomous_agent" ? "Agent" : "Assistant";
|
|
2390
2177
|
process.stderr.write("\n✅ Onboard complete!\n\n");
|
|
2391
|
-
process.stderr.write(` ${typeLabel}:${" ".repeat(Math.max(1, 10 - typeLabel.length))}${
|
|
2392
|
-
if (
|
|
2178
|
+
process.stderr.write(` ${typeLabel}:${" ".repeat(Math.max(1, 10 - typeLabel.length))}${args.id}\n`);
|
|
2179
|
+
if (args.assistant) process.stderr.write(` Assistant: ${args.assistant}\n`);
|
|
2393
2180
|
process.stderr.write(` Token: ${DEFAULT_HOME_DIR$1}/config/agents/${agentToBootstrap}/agent.yaml\n`);
|
|
2394
|
-
if (
|
|
2395
|
-
|
|
2396
|
-
if (mergedArgs.type === "human") {
|
|
2181
|
+
if (args.feishuBotAppId) process.stderr.write(` Feishu: bot bound (${args.feishuBotAppId})\n`);
|
|
2182
|
+
if (args.type === "human") {
|
|
2397
2183
|
process.stderr.write("\n Next step — bind your Feishu account:\n");
|
|
2398
|
-
process.stderr.write(` Send this message to the bot in Feishu: /bind ${
|
|
2399
|
-
if (!
|
|
2184
|
+
process.stderr.write(` Send this message to the bot in Feishu: /bind ${args.id}\n`);
|
|
2185
|
+
if (!args.feishuBotAppId) process.stderr.write(" (requires a Feishu bot to be configured in the system)\n");
|
|
2400
2186
|
}
|
|
2401
2187
|
process.stderr.write("\n Start the agent:\n");
|
|
2402
2188
|
process.stderr.write(" first-tree-hub client start\n");
|
|
2403
2189
|
process.stderr.write("\n");
|
|
2404
2190
|
}
|
|
2405
|
-
function createMemberNodeMd(repoPath, data) {
|
|
2406
|
-
const memberDir = join(repoPath, data.parentPath ?? "members", data.id);
|
|
2407
|
-
mkdirSync(memberDir, { recursive: true });
|
|
2408
|
-
const domainsList = data.domains.map((d) => ` - "${d}"`).join("\n");
|
|
2409
|
-
const githubLine = data.github ? `\ngithub: ${data.github}` : "";
|
|
2410
|
-
const delegateLine = data.delegateMention && data.type === "human" ? `\ndelegate_mention: ${data.delegateMention}` : "";
|
|
2411
|
-
const bodySections = data.type === "autonomous_agent" ? `## About
|
|
2412
|
-
|
|
2413
|
-
## Capabilities
|
|
2414
|
-
|
|
2415
|
-
## Current Focus
|
|
2416
|
-
` : `## About
|
|
2417
|
-
|
|
2418
|
-
## Current Focus
|
|
2419
|
-
`;
|
|
2420
|
-
const content = `---
|
|
2421
|
-
title: "${data.displayName}"
|
|
2422
|
-
owners: [${data.owner}]
|
|
2423
|
-
type: ${data.type}
|
|
2424
|
-
role: "${data.role}"
|
|
2425
|
-
domains:
|
|
2426
|
-
${domainsList}${githubLine}${delegateLine}
|
|
2427
|
-
---
|
|
2428
|
-
|
|
2429
|
-
# ${data.displayName}
|
|
2430
|
-
|
|
2431
|
-
${bodySections}`;
|
|
2432
|
-
writeFileSync(join(memberDir, "NODE.md"), content);
|
|
2433
|
-
}
|
|
2434
|
-
function isTrackedByGit(repoPath, filePath) {
|
|
2435
|
-
try {
|
|
2436
|
-
execSync(`git ls-files --error-unmatch ${filePath}`, {
|
|
2437
|
-
cwd: repoPath,
|
|
2438
|
-
stdio: "pipe"
|
|
2439
|
-
});
|
|
2440
|
-
return true;
|
|
2441
|
-
} catch {
|
|
2442
|
-
return false;
|
|
2443
|
-
}
|
|
2444
|
-
}
|
|
2445
|
-
const CONTEXT_TREE_DIR = join(DEFAULT_HOME_DIR$1, "context-tree");
|
|
2446
|
-
/**
|
|
2447
|
-
* Resolve Context Tree to a **local path** at $FIRST_TREE_HUB_HOME/context-tree/.
|
|
2448
|
-
*
|
|
2449
|
-
* Repo URL is obtained from the Hub server. The local clone is always
|
|
2450
|
-
* managed in the standard location — no custom paths allowed.
|
|
2451
|
-
*/
|
|
2452
|
-
async function resolveContextTreeRepo(serverUrl) {
|
|
2453
|
-
const repoUrl = await fetchRepoUrlFromServer(serverUrl);
|
|
2454
|
-
if (!repoUrl) return null;
|
|
2455
|
-
let ghToken;
|
|
2456
|
-
try {
|
|
2457
|
-
ghToken = execSync("gh auth token", {
|
|
2458
|
-
encoding: "utf-8",
|
|
2459
|
-
stdio: "pipe"
|
|
2460
|
-
}).trim();
|
|
2461
|
-
} catch {
|
|
2462
|
-
return null;
|
|
2463
|
-
}
|
|
2464
|
-
const gitEnv = {
|
|
2465
|
-
...process.env,
|
|
2466
|
-
GIT_ASKPASS: "echo",
|
|
2467
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
2468
|
-
GH_TOKEN: ghToken,
|
|
2469
|
-
GITHUB_TOKEN: ghToken
|
|
2470
|
-
};
|
|
2471
|
-
const gitConfigArgs = `-c url."https://x-access-token:${ghToken}@github.com/".insteadOf="https://github.com/"`;
|
|
2472
|
-
if (existsSync(join(CONTEXT_TREE_DIR, ".git"))) {
|
|
2473
|
-
try {
|
|
2474
|
-
if (execSync("git remote get-url origin", {
|
|
2475
|
-
cwd: CONTEXT_TREE_DIR,
|
|
2476
|
-
encoding: "utf-8",
|
|
2477
|
-
stdio: "pipe"
|
|
2478
|
-
}).trim().includes(repoUrl.replace(/^https?:\/\/github\.com\//, "").replace(/\.git$/, ""))) {
|
|
2479
|
-
process.stderr.write("Updating Context Tree...\n");
|
|
2480
|
-
execSync("git checkout main 2>/dev/null || git checkout master", {
|
|
2481
|
-
cwd: CONTEXT_TREE_DIR,
|
|
2482
|
-
stdio: "pipe"
|
|
2483
|
-
});
|
|
2484
|
-
try {
|
|
2485
|
-
execSync(`git ${gitConfigArgs} pull --ff-only`, {
|
|
2486
|
-
cwd: CONTEXT_TREE_DIR,
|
|
2487
|
-
stdio: "pipe",
|
|
2488
|
-
env: gitEnv
|
|
2489
|
-
});
|
|
2490
|
-
} catch {}
|
|
2491
|
-
return CONTEXT_TREE_DIR;
|
|
2492
|
-
}
|
|
2493
|
-
} catch {}
|
|
2494
|
-
const safePrefix = DEFAULT_HOME_DIR$1;
|
|
2495
|
-
if (!CONTEXT_TREE_DIR.startsWith(safePrefix) || CONTEXT_TREE_DIR === safePrefix) throw new Error(`Refusing to delete unsafe path: ${CONTEXT_TREE_DIR}`);
|
|
2496
|
-
execSync(`rm -rf ${CONTEXT_TREE_DIR}`);
|
|
2497
|
-
}
|
|
2498
|
-
try {
|
|
2499
|
-
process.stderr.write(`Cloning Context Tree to ${CONTEXT_TREE_DIR}...\n`);
|
|
2500
|
-
mkdirSync(DEFAULT_HOME_DIR$1, { recursive: true });
|
|
2501
|
-
execSync(`git ${gitConfigArgs} clone ${repoUrl} ${CONTEXT_TREE_DIR}`, {
|
|
2502
|
-
stdio: "pipe",
|
|
2503
|
-
env: gitEnv
|
|
2504
|
-
});
|
|
2505
|
-
return CONTEXT_TREE_DIR;
|
|
2506
|
-
} catch {
|
|
2507
|
-
return null;
|
|
2508
|
-
}
|
|
2509
|
-
}
|
|
2510
|
-
/** Query server for Context Tree repo URL. */
|
|
2511
|
-
async function fetchRepoUrlFromServer(serverUrl) {
|
|
2512
|
-
if (!serverUrl) try {
|
|
2513
|
-
serverUrl = resolveServerUrl();
|
|
2514
|
-
} catch {
|
|
2515
|
-
return null;
|
|
2516
|
-
}
|
|
2517
|
-
try {
|
|
2518
|
-
const url = `${serverUrl.replace(/\/+$/, "")}/api/v1/context-tree/info`;
|
|
2519
|
-
const res = await fetch(url, { signal: AbortSignal.timeout(5e3) });
|
|
2520
|
-
if (!res.ok) return null;
|
|
2521
|
-
return (await res.json()).repo ?? null;
|
|
2522
|
-
} catch {
|
|
2523
|
-
return null;
|
|
2524
|
-
}
|
|
2525
|
-
}
|
|
2526
|
-
function sleep(ms) {
|
|
2527
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2528
|
-
}
|
|
2529
|
-
function stripUndefined(obj) {
|
|
2530
|
-
const result = {};
|
|
2531
|
-
for (const [key, value] of Object.entries(obj)) if (value !== void 0) result[key] = value;
|
|
2532
|
-
return result;
|
|
2533
|
-
}
|
|
2534
2191
|
//#endregion
|
|
2535
2192
|
//#region src/core/prompt.ts
|
|
2536
2193
|
/**
|
|
@@ -2750,38 +2407,30 @@ const AGENT_STATUSES = {
|
|
|
2750
2407
|
DELETED: "deleted"
|
|
2751
2408
|
};
|
|
2752
2409
|
z.enum(["active", "suspended"]);
|
|
2753
|
-
z.object({
|
|
2754
|
-
|
|
2410
|
+
const createAgentSchema = z.object({
|
|
2411
|
+
name: z.string().min(1).max(100).regex(/^[a-z0-9_-]+$/, "Only lowercase alphanumeric, hyphens, and underscores").optional(),
|
|
2755
2412
|
type: agentTypeSchema,
|
|
2756
2413
|
displayName: z.string().max(200).optional(),
|
|
2414
|
+
delegateMention: z.string().max(100).optional(),
|
|
2415
|
+
profile: z.string().optional(),
|
|
2757
2416
|
organizationId: z.string().max(100).optional(),
|
|
2758
2417
|
metadata: z.record(z.string(), z.unknown()).optional()
|
|
2759
2418
|
});
|
|
2760
|
-
z.object({
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
suspended: z.number(),
|
|
2767
|
-
unchanged: z.number(),
|
|
2768
|
-
errors: z.number()
|
|
2769
|
-
}),
|
|
2770
|
-
created: z.array(z.string()),
|
|
2771
|
-
updated: z.array(z.string()),
|
|
2772
|
-
suspended: z.array(z.string()),
|
|
2773
|
-
errors: z.array(z.object({
|
|
2774
|
-
memberId: z.string(),
|
|
2775
|
-
error: z.string()
|
|
2776
|
-
}))
|
|
2419
|
+
const updateAgentSchema = z.object({
|
|
2420
|
+
type: agentTypeSchema.optional(),
|
|
2421
|
+
displayName: z.string().max(200).nullable().optional(),
|
|
2422
|
+
delegateMention: z.string().max(100).nullable().optional(),
|
|
2423
|
+
profile: z.string().nullable().optional(),
|
|
2424
|
+
metadata: z.record(z.string(), z.unknown()).optional()
|
|
2777
2425
|
});
|
|
2778
2426
|
z.object({
|
|
2779
|
-
|
|
2427
|
+
uuid: z.string(),
|
|
2428
|
+
name: z.string().nullable(),
|
|
2780
2429
|
organizationId: z.string(),
|
|
2781
2430
|
type: agentTypeSchema,
|
|
2782
2431
|
displayName: z.string().nullable(),
|
|
2783
2432
|
delegateMention: z.string().nullable(),
|
|
2784
|
-
|
|
2433
|
+
profile: z.string().nullable(),
|
|
2785
2434
|
inboxId: z.string(),
|
|
2786
2435
|
status: z.string(),
|
|
2787
2436
|
metadata: z.record(z.string(), z.unknown()),
|
|
@@ -2789,15 +2438,21 @@ z.object({
|
|
|
2789
2438
|
createdAt: z.string(),
|
|
2790
2439
|
updatedAt: z.string()
|
|
2791
2440
|
});
|
|
2792
|
-
const bootstrapTokenRequestSchema = z.object({
|
|
2441
|
+
const bootstrapTokenRequestSchema = z.object({
|
|
2442
|
+
name: z.string().max(100).optional(),
|
|
2443
|
+
type: agentTypeSchema.optional(),
|
|
2444
|
+
displayName: z.string().max(200).optional(),
|
|
2445
|
+
delegateMention: z.string().max(100).optional(),
|
|
2446
|
+
profile: z.string().optional(),
|
|
2447
|
+
metadata: z.record(z.string(), z.unknown()).optional()
|
|
2448
|
+
});
|
|
2793
2449
|
z.object({
|
|
2794
2450
|
exists: z.boolean(),
|
|
2795
2451
|
status: z.enum(["active", "suspended"]).nullable()
|
|
2796
2452
|
});
|
|
2797
2453
|
z.object({
|
|
2798
|
-
repo: z.string(),
|
|
2799
|
-
branch: z.string()
|
|
2800
|
-
lastSync: z.string().nullable()
|
|
2454
|
+
repo: z.string().nullable(),
|
|
2455
|
+
branch: z.string().nullable()
|
|
2801
2456
|
});
|
|
2802
2457
|
const createAgentTokenSchema = z.object({
|
|
2803
2458
|
name: z.string().max(100).optional(),
|
|
@@ -2919,7 +2574,7 @@ const SYSTEM_CONFIG_DEFAULTS = {
|
|
|
2919
2574
|
[SYSTEM_CONFIG_KEYS.PRESENCE_CLEANUP_SECONDS]: 60
|
|
2920
2575
|
};
|
|
2921
2576
|
//#endregion
|
|
2922
|
-
//#region ../server/dist/app-
|
|
2577
|
+
//#region ../server/dist/app-dUnTcJpC.mjs
|
|
2923
2578
|
var __defProp = Object.defineProperty;
|
|
2924
2579
|
var __exportAll = (all, no_symbols) => {
|
|
2925
2580
|
let target = {};
|
|
@@ -2932,24 +2587,25 @@ var __exportAll = (all, no_symbols) => {
|
|
|
2932
2587
|
};
|
|
2933
2588
|
/** Agent registration. Each agent owns a unique inboxId for message delivery. */
|
|
2934
2589
|
const agents = pgTable("agents", {
|
|
2935
|
-
|
|
2590
|
+
uuid: text("uuid").primaryKey(),
|
|
2591
|
+
name: text("name"),
|
|
2936
2592
|
organizationId: text("organization_id").notNull().default("default"),
|
|
2937
2593
|
type: text("type").notNull(),
|
|
2938
2594
|
displayName: text("display_name"),
|
|
2939
2595
|
delegateMention: text("delegate_mention"),
|
|
2940
|
-
|
|
2596
|
+
profile: text("profile"),
|
|
2941
2597
|
inboxId: text("inbox_id").unique().notNull(),
|
|
2942
2598
|
status: text("status").notNull().default("active"),
|
|
2943
2599
|
metadata: jsonb("metadata").$type().notNull().default({}),
|
|
2944
2600
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
2945
2601
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
2946
|
-
}, (table) => [index("idx_agents_org").on(table.organizationId)]);
|
|
2602
|
+
}, (table) => [index("idx_agents_org").on(table.organizationId), unique("uq_agents_org_name").on(table.organizationId, table.name)]);
|
|
2947
2603
|
/** Maps external user identities to internal Agents. */
|
|
2948
2604
|
const adapterAgentMappings = pgTable("adapter_agent_mappings", {
|
|
2949
2605
|
id: serial("id").primaryKey(),
|
|
2950
2606
|
platform: text("platform").notNull(),
|
|
2951
2607
|
externalUserId: text("external_user_id").notNull(),
|
|
2952
|
-
agentId: text("agent_id").notNull().references(() => agents.
|
|
2608
|
+
agentId: text("agent_id").notNull().references(() => agents.uuid),
|
|
2953
2609
|
boundVia: text("bound_via"),
|
|
2954
2610
|
displayName: text("display_name"),
|
|
2955
2611
|
metadata: jsonb("metadata").$type().notNull().default({}),
|
|
@@ -3007,7 +2663,7 @@ const chats = pgTable("chats", {
|
|
|
3007
2663
|
/** Chat participants (M:N). */
|
|
3008
2664
|
const chatParticipants = pgTable("chat_participants", {
|
|
3009
2665
|
chatId: text("chat_id").notNull().references(() => chats.id, { onDelete: "cascade" }),
|
|
3010
|
-
agentId: text("agent_id").notNull().references(() => agents.
|
|
2666
|
+
agentId: text("agent_id").notNull().references(() => agents.uuid),
|
|
3011
2667
|
role: text("role").notNull().default("member"),
|
|
3012
2668
|
mode: text("mode").notNull().default("full"),
|
|
3013
2669
|
joinedAt: timestamp("joined_at", { withTimezone: true }).notNull().defaultNow()
|
|
@@ -3124,7 +2780,7 @@ async function findOrCreateChatForChannel(db, data) {
|
|
|
3124
2780
|
const chatId = randomUUID();
|
|
3125
2781
|
const internalType = data.chatType === "p2p" ? "direct" : "group";
|
|
3126
2782
|
return db.transaction(async (tx) => {
|
|
3127
|
-
const [botAgent] = await tx.select({ organizationId: agents.organizationId }).from(agents).where(eq(agents.
|
|
2783
|
+
const [botAgent] = await tx.select({ organizationId: agents.organizationId }).from(agents).where(eq(agents.uuid, data.botAgentId)).limit(1);
|
|
3128
2784
|
const orgId = botAgent?.organizationId ?? "default";
|
|
3129
2785
|
await tx.insert(chats).values({
|
|
3130
2786
|
id: chatId,
|
|
@@ -3207,10 +2863,10 @@ async function adminAdapterMappingRoutes(app) {
|
|
|
3207
2863
|
app.post("/", async (request, reply) => {
|
|
3208
2864
|
const body = createAdapterMappingSchema.parse(request.body);
|
|
3209
2865
|
const [agent] = await app.db.select({
|
|
3210
|
-
id: agents.
|
|
2866
|
+
id: agents.uuid,
|
|
3211
2867
|
type: agents.type,
|
|
3212
2868
|
status: agents.status
|
|
3213
|
-
}).from(agents).where(eq(agents.
|
|
2869
|
+
}).from(agents).where(eq(agents.uuid, body.agentId)).limit(1);
|
|
3214
2870
|
if (!agent || agent.status === "deleted") throw new NotFoundError(`Agent "${body.agentId}" not found`);
|
|
3215
2871
|
if (agent.type !== "human") throw new BadRequestError("User bindings can only be created for human agents");
|
|
3216
2872
|
const row = await createAgentMapping(app.db, {
|
|
@@ -3246,7 +2902,7 @@ async function adminAdapterStatusRoutes(app) {
|
|
|
3246
2902
|
const adapterConfigs = pgTable("adapter_configs", {
|
|
3247
2903
|
id: serial("id").primaryKey(),
|
|
3248
2904
|
platform: text("platform").notNull(),
|
|
3249
|
-
agentId: text("agent_id").notNull().references(() => agents.
|
|
2905
|
+
agentId: text("agent_id").notNull().references(() => agents.uuid),
|
|
3250
2906
|
credentials: jsonb("credentials").$type().notNull(),
|
|
3251
2907
|
status: text("status").notNull().default("active"),
|
|
3252
2908
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
@@ -3308,9 +2964,9 @@ function requireEncryptionKey(key) {
|
|
|
3308
2964
|
}
|
|
3309
2965
|
async function validateAgentId(db, agentId) {
|
|
3310
2966
|
const [agent] = await db.select({
|
|
3311
|
-
id: agents.
|
|
2967
|
+
id: agents.uuid,
|
|
3312
2968
|
type: agents.type
|
|
3313
|
-
}).from(agents).where(and(eq(agents.
|
|
2969
|
+
}).from(agents).where(and(eq(agents.uuid, agentId), ne(agents.status, "deleted"))).limit(1);
|
|
3314
2970
|
if (!agent) throw new NotFoundError(`Agent "${agentId}" not found`);
|
|
3315
2971
|
if (agent.type === "human") throw new BadRequestError("Adapter configs can only be bound to non-human agents");
|
|
3316
2972
|
}
|
|
@@ -3423,292 +3079,9 @@ async function adminAdapterRoutes(app) {
|
|
|
3423
3079
|
return reply.status(204).send();
|
|
3424
3080
|
});
|
|
3425
3081
|
}
|
|
3426
|
-
const GRAPHQL_URL = "https://api.github.com/graphql";
|
|
3427
|
-
const REST_API_URL = "https://api.github.com";
|
|
3428
|
-
/** Parse "owner/repo" or "https://github.com/owner/repo" into { owner, name }. */
|
|
3429
|
-
function parseRepo(input) {
|
|
3430
|
-
const urlMatch = /github\.com\/([^/]+)\/([^/.]+)/.exec(input);
|
|
3431
|
-
if (urlMatch) return {
|
|
3432
|
-
owner: urlMatch[1] ?? "",
|
|
3433
|
-
name: urlMatch[2] ?? ""
|
|
3434
|
-
};
|
|
3435
|
-
const parts = input.split("/");
|
|
3436
|
-
return {
|
|
3437
|
-
owner: parts[0] ?? "",
|
|
3438
|
-
name: parts[1] ?? ""
|
|
3439
|
-
};
|
|
3440
|
-
}
|
|
3441
|
-
/** Step 1: Get the tree OID of the members/ directory via GraphQL. */
|
|
3442
|
-
async function fetchMembersTreeOid(owner, name, branch, token) {
|
|
3443
|
-
const query = `
|
|
3444
|
-
query($owner: String!, $name: String!, $expr: String!) {
|
|
3445
|
-
repository(owner: $owner, name: $name) {
|
|
3446
|
-
object(expression: $expr) {
|
|
3447
|
-
... on Tree { oid }
|
|
3448
|
-
}
|
|
3449
|
-
}
|
|
3450
|
-
}
|
|
3451
|
-
`;
|
|
3452
|
-
const res = await fetch(GRAPHQL_URL, {
|
|
3453
|
-
method: "POST",
|
|
3454
|
-
headers: {
|
|
3455
|
-
Authorization: `Bearer ${token}`,
|
|
3456
|
-
"Content-Type": "application/json"
|
|
3457
|
-
},
|
|
3458
|
-
body: JSON.stringify({
|
|
3459
|
-
query,
|
|
3460
|
-
variables: {
|
|
3461
|
-
owner,
|
|
3462
|
-
name,
|
|
3463
|
-
expr: `${branch}:members`
|
|
3464
|
-
}
|
|
3465
|
-
})
|
|
3466
|
-
});
|
|
3467
|
-
if (!res.ok) throw new Error(`GitHub API returned ${res.status}: ${await res.text()}`);
|
|
3468
|
-
const json = await res.json();
|
|
3469
|
-
if (json.errors) throw new Error(`GitHub GraphQL errors: ${json.errors.map((e) => e.message).join(", ")}`);
|
|
3470
|
-
return json.data?.repository?.object?.oid ?? null;
|
|
3471
|
-
}
|
|
3472
|
-
/** Step 2: Recursively list all entries under the members/ tree via REST API. */
|
|
3473
|
-
async function fetchRecursiveTree(owner, name, treeSha, token) {
|
|
3474
|
-
const url = `${REST_API_URL}/repos/${owner}/${name}/git/trees/${treeSha}?recursive=1`;
|
|
3475
|
-
const res = await fetch(url, { headers: {
|
|
3476
|
-
Authorization: `Bearer ${token}`,
|
|
3477
|
-
Accept: "application/vnd.github+json"
|
|
3478
|
-
} });
|
|
3479
|
-
if (!res.ok) throw new Error(`GitHub REST API returned ${res.status}: ${await res.text()}`);
|
|
3480
|
-
const json = await res.json();
|
|
3481
|
-
if (json.truncated) throw new Error("[context-tree-sync] GitHub REST tree API returned truncated response — members/ subtree is too large. Sync aborted to prevent incorrect agent suspension from partial data.");
|
|
3482
|
-
return json.tree;
|
|
3483
|
-
}
|
|
3484
|
-
/**
|
|
3485
|
-
* Step 3: Batch-fetch NODE.md content for all member directories via GraphQL aliases.
|
|
3486
|
-
* Each alias fetches one NODE.md file by expression.
|
|
3487
|
-
*/
|
|
3488
|
-
async function batchFetchNodeMd(owner, name, branch, memberPaths, token) {
|
|
3489
|
-
if (memberPaths.length === 0) return /* @__PURE__ */ new Map();
|
|
3490
|
-
const query = `
|
|
3491
|
-
query($owner: String!, $name: String!) {
|
|
3492
|
-
repository(owner: $owner, name: $name) {
|
|
3493
|
-
${memberPaths.map((p, i) => {
|
|
3494
|
-
const expr = `${branch}:members/${p}/NODE.md`;
|
|
3495
|
-
return `m${i}: object(expression: ${JSON.stringify(expr)}) { ... on Blob { text } }`;
|
|
3496
|
-
}).join("\n ")}
|
|
3497
|
-
}
|
|
3498
|
-
}
|
|
3499
|
-
`;
|
|
3500
|
-
const res = await fetch(GRAPHQL_URL, {
|
|
3501
|
-
method: "POST",
|
|
3502
|
-
headers: {
|
|
3503
|
-
Authorization: `Bearer ${token}`,
|
|
3504
|
-
"Content-Type": "application/json"
|
|
3505
|
-
},
|
|
3506
|
-
body: JSON.stringify({
|
|
3507
|
-
query,
|
|
3508
|
-
variables: {
|
|
3509
|
-
owner,
|
|
3510
|
-
name
|
|
3511
|
-
}
|
|
3512
|
-
})
|
|
3513
|
-
});
|
|
3514
|
-
if (!res.ok) throw new Error(`GitHub API returned ${res.status}: ${await res.text()}`);
|
|
3515
|
-
const json = await res.json();
|
|
3516
|
-
if (json.errors) throw new Error(`GitHub GraphQL errors: ${json.errors.map((e) => e.message).join(", ")}`);
|
|
3517
|
-
const repo = json.data?.repository ?? {};
|
|
3518
|
-
const result = /* @__PURE__ */ new Map();
|
|
3519
|
-
for (let i = 0; i < memberPaths.length; i++) {
|
|
3520
|
-
const blob = repo[`m${i}`];
|
|
3521
|
-
const path = memberPaths[i];
|
|
3522
|
-
if (blob?.text && path) result.set(path, blob.text);
|
|
3523
|
-
}
|
|
3524
|
-
return result;
|
|
3525
|
-
}
|
|
3526
|
-
/**
|
|
3527
|
-
* Extract member directory paths from a recursive tree listing.
|
|
3528
|
-
* A directory is a member if it contains a NODE.md blob.
|
|
3529
|
-
*/
|
|
3530
|
-
function extractMemberDirs(treeEntries) {
|
|
3531
|
-
const nodeMdPaths = /* @__PURE__ */ new Set();
|
|
3532
|
-
for (const entry of treeEntries) if (entry.type === "blob" && entry.path.endsWith("/NODE.md")) nodeMdPaths.add(entry.path);
|
|
3533
|
-
const memberDirs = [];
|
|
3534
|
-
for (const entry of treeEntries) {
|
|
3535
|
-
if (entry.type !== "tree") continue;
|
|
3536
|
-
if (nodeMdPaths.has(`${entry.path}/NODE.md`)) memberDirs.push(entry.path);
|
|
3537
|
-
}
|
|
3538
|
-
return memberDirs.sort();
|
|
3539
|
-
}
|
|
3540
|
-
/**
|
|
3541
|
-
* Fetch all members from a Context Tree repo via GitHub API.
|
|
3542
|
-
* Uses 3 API calls:
|
|
3543
|
-
* 1. GraphQL: get members/ tree OID
|
|
3544
|
-
* 2. REST: recursive tree listing (scoped to members/ only)
|
|
3545
|
-
* 3. GraphQL: batch-fetch all NODE.md contents via aliases
|
|
3546
|
-
*/
|
|
3547
|
-
async function fetchMembers(repo, branch, token) {
|
|
3548
|
-
const { owner, name } = parseRepo(repo);
|
|
3549
|
-
if (!owner || !name) throw new Error(`Invalid repo format: "${repo}" — expected "owner/repo" or a GitHub URL`);
|
|
3550
|
-
const treeOid = await fetchMembersTreeOid(owner, name, branch, token);
|
|
3551
|
-
if (!treeOid) {
|
|
3552
|
-
console.warn("[context-tree-sync] members/ directory not found in repo");
|
|
3553
|
-
return [];
|
|
3554
|
-
}
|
|
3555
|
-
const memberDirs = extractMemberDirs(await fetchRecursiveTree(owner, name, treeOid, token));
|
|
3556
|
-
if (memberDirs.length === 0) {
|
|
3557
|
-
console.warn("[context-tree-sync] No member directories with NODE.md found");
|
|
3558
|
-
return [];
|
|
3559
|
-
}
|
|
3560
|
-
const nameMap = /* @__PURE__ */ new Map();
|
|
3561
|
-
for (const dir of memberDirs) {
|
|
3562
|
-
const dirName = dir.split("/").pop() ?? dir;
|
|
3563
|
-
const existing = nameMap.get(dirName);
|
|
3564
|
-
if (existing) throw new Error(`[context-tree-sync] Duplicate member directory name '${dirName}' found at 'members/${existing}' and 'members/${dir}' — directory names must be unique across all levels under members/. Fix this in the Context Tree repo.`);
|
|
3565
|
-
nameMap.set(dirName, dir);
|
|
3566
|
-
}
|
|
3567
|
-
const nodeContents = await batchFetchNodeMd(owner, name, branch, memberDirs, token);
|
|
3568
|
-
return memberDirs.map((dir) => ({
|
|
3569
|
-
name: dir.split("/").pop() ?? dir,
|
|
3570
|
-
treePath: dir,
|
|
3571
|
-
nodeContent: nodeContents.get(dir) ?? null
|
|
3572
|
-
}));
|
|
3573
|
-
}
|
|
3574
|
-
/** Parse NODE.md frontmatter for agent metadata. */
|
|
3575
|
-
function parseNodeMetadata(content) {
|
|
3576
|
-
const match = /^---\n([\s\S]*?)\n---/.exec(content);
|
|
3577
|
-
if (!match) return {
|
|
3578
|
-
type: "autonomous_agent",
|
|
3579
|
-
displayName: null,
|
|
3580
|
-
delegateMention: null,
|
|
3581
|
-
owners: [],
|
|
3582
|
-
github: null
|
|
3583
|
-
};
|
|
3584
|
-
const frontmatter = match[1] ?? "";
|
|
3585
|
-
const getValue = (key) => {
|
|
3586
|
-
const lineMatch = new RegExp(`^${key}:\\s*(.+)$`, "m").exec(frontmatter);
|
|
3587
|
-
return lineMatch ? lineMatch[1]?.trim().replace(/^["']|["']$/g, "") ?? null : null;
|
|
3588
|
-
};
|
|
3589
|
-
const ownersRaw = getValue("owners");
|
|
3590
|
-
let owners = [];
|
|
3591
|
-
if (ownersRaw) {
|
|
3592
|
-
const listMatch = /^\[([^\]]*)\]$/.exec(ownersRaw);
|
|
3593
|
-
if (listMatch?.[1]) owners = listMatch[1].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
3594
|
-
}
|
|
3595
|
-
return {
|
|
3596
|
-
type: getValue("type") ?? "autonomous_agent",
|
|
3597
|
-
displayName: getValue("display_name") ?? getValue("title") ?? getValue("name"),
|
|
3598
|
-
delegateMention: getValue("delegate_mention"),
|
|
3599
|
-
owners,
|
|
3600
|
-
github: getValue("github")
|
|
3601
|
-
};
|
|
3602
|
-
}
|
|
3603
|
-
/** Stored for the /status endpoint */
|
|
3604
|
-
let _lastSyncResult;
|
|
3605
|
-
function getLastGraphQLSyncResult() {
|
|
3606
|
-
return _lastSyncResult;
|
|
3607
|
-
}
|
|
3608
|
-
/**
|
|
3609
|
-
* Sync agents from a GitHub Context Tree repo via GraphQL.
|
|
3610
|
-
*
|
|
3611
|
-
* Lifecycle semantics:
|
|
3612
|
-
* - Member in tree, not in DB → create (active)
|
|
3613
|
-
* - Member in tree, in DB as active, fields changed → update
|
|
3614
|
-
* - Member in tree, in DB as active, fields unchanged → unchanged
|
|
3615
|
-
* - Member in tree, in DB as suspended → reactivate (set active)
|
|
3616
|
-
* - Agent in DB as active, NOT in tree → suspend
|
|
3617
|
-
*/
|
|
3618
|
-
async function syncFromGitHub(db, repo, branch, githubToken) {
|
|
3619
|
-
const members = await fetchMembers(repo, branch, githubToken);
|
|
3620
|
-
const memberNames = new Set(members.map((m) => m.name));
|
|
3621
|
-
const result = {
|
|
3622
|
-
created: 0,
|
|
3623
|
-
updated: 0,
|
|
3624
|
-
suspended: 0,
|
|
3625
|
-
reactivated: 0,
|
|
3626
|
-
unchanged: 0,
|
|
3627
|
-
errors: 0,
|
|
3628
|
-
syncedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3629
|
-
};
|
|
3630
|
-
for (const member of members) try {
|
|
3631
|
-
const meta = member.nodeContent ? parseNodeMetadata(member.nodeContent) : {
|
|
3632
|
-
type: "autonomous_agent",
|
|
3633
|
-
displayName: null,
|
|
3634
|
-
delegateMention: null,
|
|
3635
|
-
owners: [],
|
|
3636
|
-
github: null
|
|
3637
|
-
};
|
|
3638
|
-
const metadataJson = JSON.stringify({
|
|
3639
|
-
owners: meta.owners,
|
|
3640
|
-
github: meta.github
|
|
3641
|
-
});
|
|
3642
|
-
const existing = await db.execute(sql`SELECT id, status, type, display_name, delegate_mention, tree_path, metadata FROM agents WHERE id = ${member.name}`);
|
|
3643
|
-
if (existing.length === 0) {
|
|
3644
|
-
await db.execute(sql`
|
|
3645
|
-
INSERT INTO agents (id, type, display_name, delegate_mention, tree_path, status, inbox_id, metadata)
|
|
3646
|
-
VALUES (${member.name}, ${meta.type}, ${meta.displayName}, ${meta.delegateMention}, ${member.treePath}, 'active', ${`inbox_${member.name}`}, ${metadataJson}::jsonb)
|
|
3647
|
-
`);
|
|
3648
|
-
result.created++;
|
|
3649
|
-
} else {
|
|
3650
|
-
const agent = existing[0];
|
|
3651
|
-
const existingMeta = agent.metadata ?? {};
|
|
3652
|
-
const mergedMeta = JSON.stringify({
|
|
3653
|
-
...existingMeta,
|
|
3654
|
-
owners: meta.owners,
|
|
3655
|
-
github: meta.github
|
|
3656
|
-
});
|
|
3657
|
-
if (agent.status === "suspended" || agent.status === "deleted") {
|
|
3658
|
-
await db.execute(sql`
|
|
3659
|
-
UPDATE agents SET status = 'active', type = ${meta.type}, display_name = ${meta.displayName}, delegate_mention = ${meta.delegateMention}, tree_path = ${member.treePath}, metadata = ${mergedMeta}::jsonb
|
|
3660
|
-
WHERE id = ${member.name}
|
|
3661
|
-
`);
|
|
3662
|
-
result.reactivated++;
|
|
3663
|
-
} else if (agent.type !== meta.type || agent.display_name !== meta.displayName || agent.delegate_mention !== meta.delegateMention || agent.tree_path !== member.treePath || JSON.stringify(existingMeta.owners) !== JSON.stringify(meta.owners) || (existingMeta.github ?? null) !== (meta.github ?? null)) {
|
|
3664
|
-
await db.execute(sql`
|
|
3665
|
-
UPDATE agents SET type = ${meta.type}, display_name = ${meta.displayName}, delegate_mention = ${meta.delegateMention}, tree_path = ${member.treePath}, metadata = ${mergedMeta}::jsonb
|
|
3666
|
-
WHERE id = ${member.name}
|
|
3667
|
-
`);
|
|
3668
|
-
result.updated++;
|
|
3669
|
-
} else result.unchanged++;
|
|
3670
|
-
}
|
|
3671
|
-
} catch (err) {
|
|
3672
|
-
console.error(`[context-tree-sync] Failed to sync member "${member.name}" (path: members/${member.treePath}):`, err);
|
|
3673
|
-
result.errors++;
|
|
3674
|
-
}
|
|
3675
|
-
try {
|
|
3676
|
-
const activeAgents = await db.execute(sql`SELECT id FROM agents WHERE status = 'active' AND (metadata->>'managed')::boolean IS NOT TRUE`);
|
|
3677
|
-
for (const row of activeAgents) {
|
|
3678
|
-
const agent = row;
|
|
3679
|
-
if (!memberNames.has(agent.id)) {
|
|
3680
|
-
await db.execute(sql`UPDATE agents SET status = 'suspended' WHERE id = ${agent.id}`);
|
|
3681
|
-
result.suspended++;
|
|
3682
|
-
}
|
|
3683
|
-
}
|
|
3684
|
-
} catch (err) {
|
|
3685
|
-
console.error("[context-tree-sync] Failed to suspend removed agents:", err);
|
|
3686
|
-
result.errors++;
|
|
3687
|
-
}
|
|
3688
|
-
_lastSyncResult = result;
|
|
3689
|
-
return result;
|
|
3690
|
-
}
|
|
3691
|
-
async function adminAgentSyncRoutes(app) {
|
|
3692
|
-
app.post("/", async (_request, reply) => {
|
|
3693
|
-
const { repo, branch } = app.config.contextTree;
|
|
3694
|
-
const { token } = app.config.github;
|
|
3695
|
-
try {
|
|
3696
|
-
const result = await syncFromGitHub(app.db, repo, branch, token);
|
|
3697
|
-
return reply.send({ summary: result });
|
|
3698
|
-
} catch (error) {
|
|
3699
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
3700
|
-
app.log.error(error, "Context Tree sync failed");
|
|
3701
|
-
return reply.status(502).send({ error: msg });
|
|
3702
|
-
}
|
|
3703
|
-
});
|
|
3704
|
-
app.get("/status", async (_request, reply) => {
|
|
3705
|
-
const lastSync = getLastGraphQLSyncResult();
|
|
3706
|
-
return reply.send({ lastSync: lastSync ?? null });
|
|
3707
|
-
});
|
|
3708
|
-
}
|
|
3709
3082
|
/** Agent online status. Tracked via WebSocket connections; stale entries are cleaned up using server_instances heartbeat. */
|
|
3710
3083
|
const agentPresence = pgTable("agent_presence", {
|
|
3711
|
-
agentId: text("agent_id").primaryKey().references(() => agents.
|
|
3084
|
+
agentId: text("agent_id").primaryKey().references(() => agents.uuid, { onDelete: "cascade" }),
|
|
3712
3085
|
status: text("status").notNull().default("offline"),
|
|
3713
3086
|
instanceId: text("instance_id"),
|
|
3714
3087
|
connectedAt: timestamp("connected_at", { withTimezone: true }),
|
|
@@ -3717,7 +3090,7 @@ const agentPresence = pgTable("agent_presence", {
|
|
|
3717
3090
|
/** Agent bearer tokens. Multiple tokens can coexist for zero-downtime rotation. */
|
|
3718
3091
|
const agentTokens = pgTable("agent_tokens", {
|
|
3719
3092
|
id: text("id").primaryKey(),
|
|
3720
|
-
agentId: text("agent_id").notNull().references(() => agents.
|
|
3093
|
+
agentId: text("agent_id").notNull().references(() => agents.uuid, { onDelete: "cascade" }),
|
|
3721
3094
|
tokenHash: text("token_hash").notNull(),
|
|
3722
3095
|
name: text("name"),
|
|
3723
3096
|
expiresAt: timestamp("expires_at", { withTimezone: true }),
|
|
@@ -3725,62 +3098,83 @@ const agentTokens = pgTable("agent_tokens", {
|
|
|
3725
3098
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
3726
3099
|
lastUsedAt: timestamp("last_used_at", { withTimezone: true })
|
|
3727
3100
|
}, (table) => [index("idx_agent_tokens_agent").on(table.agentId), index("idx_agent_tokens_hash").on(table.tokenHash)]);
|
|
3101
|
+
/** Generate a UUID v7 (time-ordered). No external dependency. */
|
|
3102
|
+
function uuidv7() {
|
|
3103
|
+
const now = BigInt(Date.now());
|
|
3104
|
+
const bytes = new Uint8Array(16);
|
|
3105
|
+
bytes[0] = Number(now >> 40n & 255n);
|
|
3106
|
+
bytes[1] = Number(now >> 32n & 255n);
|
|
3107
|
+
bytes[2] = Number(now >> 24n & 255n);
|
|
3108
|
+
bytes[3] = Number(now >> 16n & 255n);
|
|
3109
|
+
bytes[4] = Number(now >> 8n & 255n);
|
|
3110
|
+
bytes[5] = Number(now & 255n);
|
|
3111
|
+
const rand = randomBytes(10);
|
|
3112
|
+
for (let i = 0; i < 10; i++) {
|
|
3113
|
+
const b = rand[i];
|
|
3114
|
+
if (b !== void 0) bytes[6 + i] = b;
|
|
3115
|
+
}
|
|
3116
|
+
bytes[6] = (bytes[6] ?? 0) & 15 | 112;
|
|
3117
|
+
bytes[8] = (bytes[8] ?? 0) & 63 | 128;
|
|
3118
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
3119
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
3120
|
+
}
|
|
3728
3121
|
function hashToken$1(raw) {
|
|
3729
3122
|
return createHash("sha256").update(raw).digest("hex");
|
|
3730
3123
|
}
|
|
3731
3124
|
async function createAgent(db, data) {
|
|
3732
|
-
const
|
|
3733
|
-
const
|
|
3734
|
-
const
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
organizationId: data.organizationId ?? "default",
|
|
3125
|
+
const uuid = uuidv7();
|
|
3126
|
+
const name = data.name ?? null;
|
|
3127
|
+
const inboxId = `inbox_${uuid}`;
|
|
3128
|
+
const orgId = data.organizationId ?? "default";
|
|
3129
|
+
try {
|
|
3130
|
+
const [agent] = await db.insert(agents).values({
|
|
3131
|
+
uuid,
|
|
3132
|
+
name,
|
|
3133
|
+
organizationId: orgId,
|
|
3742
3134
|
type: data.type,
|
|
3743
3135
|
displayName: data.displayName ?? null,
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3136
|
+
delegateMention: data.delegateMention ?? null,
|
|
3137
|
+
profile: data.profile ?? null,
|
|
3138
|
+
inboxId,
|
|
3139
|
+
metadata: data.metadata ?? {}
|
|
3140
|
+
}).returning();
|
|
3141
|
+
if (!agent) throw new Error("Unexpected: INSERT RETURNING produced no row");
|
|
3749
3142
|
return agent;
|
|
3143
|
+
} catch (err) {
|
|
3144
|
+
if ((err?.code ?? err?.cause?.code ?? "") === "23505" && name) throw new ConflictError(`Agent name "${name}" already exists in organization "${orgId}"`);
|
|
3145
|
+
throw err;
|
|
3750
3146
|
}
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
displayName: data.displayName ?? null,
|
|
3756
|
-
inboxId,
|
|
3757
|
-
metadata: data.metadata ?? {}
|
|
3758
|
-
}).returning();
|
|
3759
|
-
if (!agent) throw new Error("Unexpected: INSERT RETURNING produced no row");
|
|
3147
|
+
}
|
|
3148
|
+
async function getAgent(db, uuid) {
|
|
3149
|
+
const [agent] = await db.select().from(agents).where(and(eq(agents.uuid, uuid), ne(agents.status, AGENT_STATUSES.DELETED))).limit(1);
|
|
3150
|
+
if (!agent) throw new NotFoundError(`Agent "${uuid}" not found`);
|
|
3760
3151
|
return agent;
|
|
3761
3152
|
}
|
|
3762
|
-
async function
|
|
3763
|
-
const [agent] = await db.select().from(agents).where(and(eq(agents.
|
|
3764
|
-
if (!agent) throw new NotFoundError(`Agent "${
|
|
3153
|
+
async function getAgentByName(db, orgId, name) {
|
|
3154
|
+
const [agent] = await db.select().from(agents).where(and(eq(agents.organizationId, orgId), eq(agents.name, name), ne(agents.status, AGENT_STATUSES.DELETED))).limit(1);
|
|
3155
|
+
if (!agent) throw new NotFoundError(`Agent "${name}" not found in organization "${orgId}"`);
|
|
3765
3156
|
return agent;
|
|
3766
3157
|
}
|
|
3767
|
-
async function listAgents(db, limit, cursor) {
|
|
3768
|
-
const
|
|
3769
|
-
|
|
3158
|
+
async function listAgents(db, orgId, limit, cursor, type) {
|
|
3159
|
+
const conditions = [ne(agents.status, AGENT_STATUSES.DELETED), eq(agents.organizationId, orgId)];
|
|
3160
|
+
if (cursor) conditions.push(lt(agents.createdAt, new Date(cursor)));
|
|
3161
|
+
if (type) conditions.push(eq(agents.type, type));
|
|
3162
|
+
const where = and(...conditions);
|
|
3770
3163
|
const rows = await db.select({
|
|
3771
|
-
|
|
3164
|
+
uuid: agents.uuid,
|
|
3165
|
+
name: agents.name,
|
|
3772
3166
|
organizationId: agents.organizationId,
|
|
3773
3167
|
type: agents.type,
|
|
3774
3168
|
displayName: agents.displayName,
|
|
3775
3169
|
delegateMention: agents.delegateMention,
|
|
3776
|
-
|
|
3170
|
+
profile: agents.profile,
|
|
3777
3171
|
inboxId: agents.inboxId,
|
|
3778
3172
|
status: agents.status,
|
|
3779
3173
|
metadata: agents.metadata,
|
|
3780
3174
|
createdAt: agents.createdAt,
|
|
3781
3175
|
updatedAt: agents.updatedAt,
|
|
3782
3176
|
presenceStatus: agentPresence.status
|
|
3783
|
-
}).from(agents).leftJoin(agentPresence, eq(agents.
|
|
3177
|
+
}).from(agents).leftJoin(agentPresence, eq(agents.uuid, agentPresence.agentId)).where(where).orderBy(desc(agents.createdAt)).limit(limit + 1);
|
|
3784
3178
|
const hasMore = rows.length > limit;
|
|
3785
3179
|
const items = hasMore ? rows.slice(0, limit) : rows;
|
|
3786
3180
|
const last = items[items.length - 1];
|
|
@@ -3789,45 +3183,115 @@ async function listAgents(db, limit, cursor) {
|
|
|
3789
3183
|
nextCursor: hasMore && last ? last.createdAt.toISOString() : null
|
|
3790
3184
|
};
|
|
3791
3185
|
}
|
|
3186
|
+
async function updateAgent(db, uuid, data) {
|
|
3187
|
+
const agent = await getAgent(db, uuid);
|
|
3188
|
+
const updates = { updatedAt: /* @__PURE__ */ new Date() };
|
|
3189
|
+
if (data.type !== void 0) updates.type = data.type;
|
|
3190
|
+
if (data.displayName !== void 0) updates.displayName = data.displayName;
|
|
3191
|
+
if (data.delegateMention !== void 0) updates.delegateMention = data.delegateMention;
|
|
3192
|
+
if (data.profile !== void 0) updates.profile = data.profile;
|
|
3193
|
+
if (data.metadata !== void 0) updates.metadata = data.metadata;
|
|
3194
|
+
const [updated] = await db.update(agents).set(updates).where(eq(agents.uuid, agent.uuid)).returning();
|
|
3195
|
+
if (!updated) throw new Error("Unexpected: UPDATE RETURNING produced no row");
|
|
3196
|
+
return updated;
|
|
3197
|
+
}
|
|
3198
|
+
/**
|
|
3199
|
+
* Reactivate a suspended agent.
|
|
3200
|
+
*/
|
|
3201
|
+
async function reactivateAgent(db, uuid) {
|
|
3202
|
+
const [existing] = await db.select({
|
|
3203
|
+
uuid: agents.uuid,
|
|
3204
|
+
status: agents.status
|
|
3205
|
+
}).from(agents).where(eq(agents.uuid, uuid)).limit(1);
|
|
3206
|
+
if (!existing || existing.status === AGENT_STATUSES.DELETED) throw new NotFoundError(`Agent "${uuid}" not found`);
|
|
3207
|
+
if (existing.status !== AGENT_STATUSES.SUSPENDED) throw new BadRequestError("Only suspended agents can be reactivated.");
|
|
3208
|
+
const [agent] = await db.update(agents).set({
|
|
3209
|
+
status: AGENT_STATUSES.ACTIVE,
|
|
3210
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3211
|
+
}).where(eq(agents.uuid, uuid)).returning();
|
|
3212
|
+
if (!agent) throw new Error("Unexpected: UPDATE RETURNING produced no row");
|
|
3213
|
+
return agent;
|
|
3214
|
+
}
|
|
3215
|
+
/**
|
|
3216
|
+
* Suspend an agent. Revokes all active tokens so the agent can no longer authenticate.
|
|
3217
|
+
*/
|
|
3218
|
+
async function suspendAgent(db, uuid) {
|
|
3219
|
+
const [agent] = await db.update(agents).set({
|
|
3220
|
+
status: AGENT_STATUSES.SUSPENDED,
|
|
3221
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3222
|
+
}).where(and(eq(agents.uuid, uuid), ne(agents.status, AGENT_STATUSES.DELETED))).returning();
|
|
3223
|
+
if (!agent) throw new NotFoundError(`Agent "${uuid}" not found`);
|
|
3224
|
+
await db.update(agentTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and(eq(agentTokens.agentId, uuid), isNull(agentTokens.revokedAt)));
|
|
3225
|
+
return agent;
|
|
3226
|
+
}
|
|
3792
3227
|
/**
|
|
3793
|
-
* Delete an agent. Only allowed when status is "suspended"
|
|
3228
|
+
* Delete an agent. Only allowed when status is "suspended".
|
|
3229
|
+
* Suspend the agent first to revoke tokens, then delete.
|
|
3230
|
+
* Sets name to NULL to release the name for reuse.
|
|
3794
3231
|
*/
|
|
3795
|
-
async function deleteAgent(db,
|
|
3232
|
+
async function deleteAgent(db, uuid) {
|
|
3796
3233
|
const [existing] = await db.select({
|
|
3797
|
-
|
|
3234
|
+
uuid: agents.uuid,
|
|
3798
3235
|
status: agents.status
|
|
3799
|
-
}).from(agents).where(eq(agents.
|
|
3800
|
-
if (!existing || existing.status === AGENT_STATUSES.DELETED) throw new NotFoundError(`Agent "${
|
|
3801
|
-
if (existing.status !== AGENT_STATUSES.SUSPENDED) throw new BadRequestError("Only suspended agents can be deleted.
|
|
3236
|
+
}).from(agents).where(eq(agents.uuid, uuid)).limit(1);
|
|
3237
|
+
if (!existing || existing.status === AGENT_STATUSES.DELETED) throw new NotFoundError(`Agent "${uuid}" not found`);
|
|
3238
|
+
if (existing.status !== AGENT_STATUSES.SUSPENDED) throw new BadRequestError("Only suspended agents can be deleted. Suspend the agent first.");
|
|
3802
3239
|
const [agent] = await db.update(agents).set({
|
|
3803
3240
|
status: AGENT_STATUSES.DELETED,
|
|
3241
|
+
name: null,
|
|
3804
3242
|
updatedAt: /* @__PURE__ */ new Date()
|
|
3805
|
-
}).where(eq(agents.
|
|
3806
|
-
await db.update(agentTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and(eq(agentTokens.agentId,
|
|
3807
|
-
await db.delete(adapterConfigs).where(eq(adapterConfigs.agentId,
|
|
3808
|
-
await db.delete(adapterAgentMappings).where(eq(adapterAgentMappings.agentId,
|
|
3243
|
+
}).where(eq(agents.uuid, uuid)).returning();
|
|
3244
|
+
await db.update(agentTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and(eq(agentTokens.agentId, uuid), isNull(agentTokens.revokedAt)));
|
|
3245
|
+
await db.delete(adapterConfigs).where(eq(adapterConfigs.agentId, uuid));
|
|
3246
|
+
await db.delete(adapterAgentMappings).where(eq(adapterAgentMappings.agentId, uuid));
|
|
3809
3247
|
if (!agent) throw new Error("Unexpected: UPDATE RETURNING produced no row");
|
|
3810
3248
|
return agent;
|
|
3811
3249
|
}
|
|
3250
|
+
async function bootstrapToken(db, agentName, orgId, githubUsername, options) {
|
|
3251
|
+
let agent;
|
|
3252
|
+
try {
|
|
3253
|
+
agent = await getAgentByName(db, orgId, agentName);
|
|
3254
|
+
} catch (err) {
|
|
3255
|
+
if (err instanceof NotFoundError) {
|
|
3256
|
+
const metadata = {
|
|
3257
|
+
...options?.metadata,
|
|
3258
|
+
owners: [githubUsername]
|
|
3259
|
+
};
|
|
3260
|
+
agent = await createAgent(db, {
|
|
3261
|
+
name: agentName,
|
|
3262
|
+
type: options?.type ?? "autonomous_agent",
|
|
3263
|
+
displayName: options?.displayName ?? agentName,
|
|
3264
|
+
delegateMention: options?.delegateMention,
|
|
3265
|
+
profile: options?.profile,
|
|
3266
|
+
organizationId: orgId,
|
|
3267
|
+
metadata
|
|
3268
|
+
});
|
|
3269
|
+
} else throw err;
|
|
3270
|
+
}
|
|
3271
|
+
if (!(Array.isArray(agent.metadata?.owners) ? agent.metadata.owners : []).includes(githubUsername)) throw new ForbiddenError(`GitHub user "${githubUsername}" is not in the owners list for agent "${agentName}"`);
|
|
3272
|
+
const activeTokens = await db.select({ id: agentTokens.id }).from(agentTokens).where(and(eq(agentTokens.agentId, agent.uuid), isNull(agentTokens.revokedAt)));
|
|
3273
|
+
if (activeTokens.length > 0) throw new ConflictError(`Agent "${agentName}" already has ${activeTokens.length} active token(s). Revoke all tokens first to re-bootstrap.`);
|
|
3274
|
+
return createToken(db, agent.uuid, { name: options?.tokenName ?? "bootstrap" });
|
|
3275
|
+
}
|
|
3812
3276
|
/**
|
|
3813
|
-
*
|
|
3814
|
-
* Only works when the agent has no active (non-revoked, non-expired) tokens.
|
|
3277
|
+
* Check if a GitHub user belongs to a specific organization.
|
|
3815
3278
|
*/
|
|
3816
|
-
async function
|
|
3817
|
-
const
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3279
|
+
async function checkGitHubOrgMembership(githubToken, org) {
|
|
3280
|
+
const res = await fetch(`https://api.github.com/user/orgs`, { headers: {
|
|
3281
|
+
Authorization: `Bearer ${githubToken}`,
|
|
3282
|
+
Accept: "application/vnd.github+json"
|
|
3283
|
+
} });
|
|
3284
|
+
if (!res.ok) return false;
|
|
3285
|
+
return (await res.json()).some((o) => o.login.toLowerCase() === org.toLowerCase());
|
|
3286
|
+
}
|
|
3287
|
+
async function createToken(db, agentUuid, data) {
|
|
3288
|
+
await getAgent(db, agentUuid);
|
|
3825
3289
|
const raw = `aghub_${randomBytes(32).toString("hex")}`;
|
|
3826
3290
|
const tokenHash = hashToken$1(raw);
|
|
3827
|
-
const tokenId =
|
|
3291
|
+
const tokenId = uuidv7();
|
|
3828
3292
|
const [token] = await db.insert(agentTokens).values({
|
|
3829
3293
|
id: tokenId,
|
|
3830
|
-
agentId,
|
|
3294
|
+
agentId: agentUuid,
|
|
3831
3295
|
tokenHash,
|
|
3832
3296
|
name: data.name ?? null,
|
|
3833
3297
|
expiresAt: data.expiresAt ? new Date(data.expiresAt) : null
|
|
@@ -3844,7 +3308,7 @@ async function createToken(db, agentId, data) {
|
|
|
3844
3308
|
token: raw
|
|
3845
3309
|
};
|
|
3846
3310
|
}
|
|
3847
|
-
async function listTokens(db,
|
|
3311
|
+
async function listTokens(db, agentUuid) {
|
|
3848
3312
|
return db.select({
|
|
3849
3313
|
id: agentTokens.id,
|
|
3850
3314
|
agentId: agentTokens.agentId,
|
|
@@ -3853,10 +3317,10 @@ async function listTokens(db, agentId) {
|
|
|
3853
3317
|
revokedAt: agentTokens.revokedAt,
|
|
3854
3318
|
createdAt: agentTokens.createdAt,
|
|
3855
3319
|
lastUsedAt: agentTokens.lastUsedAt
|
|
3856
|
-
}).from(agentTokens).where(eq(agentTokens.agentId,
|
|
3320
|
+
}).from(agentTokens).where(eq(agentTokens.agentId, agentUuid)).orderBy(desc(agentTokens.createdAt));
|
|
3857
3321
|
}
|
|
3858
|
-
async function revokeToken(db,
|
|
3859
|
-
const [token] = await db.update(agentTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and(eq(agentTokens.id, tokenId), eq(agentTokens.agentId,
|
|
3322
|
+
async function revokeToken(db, agentUuid, tokenId) {
|
|
3323
|
+
const [token] = await db.update(agentTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and(eq(agentTokens.id, tokenId), eq(agentTokens.agentId, agentUuid), isNull(agentTokens.revokedAt))).returning();
|
|
3860
3324
|
if (!token) throw new NotFoundError("Token not found or already revoked");
|
|
3861
3325
|
return token;
|
|
3862
3326
|
}
|
|
@@ -3864,9 +3328,9 @@ async function createChat(db, creatorId, data) {
|
|
|
3864
3328
|
const chatId = randomUUID();
|
|
3865
3329
|
const allParticipantIds = new Set([creatorId, ...data.participantIds]);
|
|
3866
3330
|
const existingAgents = await db.select({
|
|
3867
|
-
id: agents.
|
|
3331
|
+
id: agents.uuid,
|
|
3868
3332
|
organizationId: agents.organizationId
|
|
3869
|
-
}).from(agents).where(inArray(agents.
|
|
3333
|
+
}).from(agents).where(inArray(agents.uuid, [...allParticipantIds]));
|
|
3870
3334
|
if (existingAgents.length !== allParticipantIds.size) {
|
|
3871
3335
|
const found = new Set(existingAgents.map((a) => a.id));
|
|
3872
3336
|
throw new BadRequestError(`Agents not found: ${[...allParticipantIds].filter((id) => !found.has(id)).join(", ")}`);
|
|
@@ -3935,9 +3399,9 @@ async function addParticipant(db, chatId, requesterId, data) {
|
|
|
3935
3399
|
const chat = await getChat(db, chatId);
|
|
3936
3400
|
await assertParticipant(db, chatId, requesterId);
|
|
3937
3401
|
const [targetAgent] = await db.select({
|
|
3938
|
-
id: agents.
|
|
3402
|
+
id: agents.uuid,
|
|
3939
3403
|
organizationId: agents.organizationId
|
|
3940
|
-
}).from(agents).where(eq(agents.
|
|
3404
|
+
}).from(agents).where(eq(agents.uuid, data.agentId)).limit(1);
|
|
3941
3405
|
if (!targetAgent) throw new NotFoundError(`Agent "${data.agentId}" not found`);
|
|
3942
3406
|
if (targetAgent.organizationId !== chat.organizationId) throw new BadRequestError("Cannot add agent from different organization");
|
|
3943
3407
|
const [existing] = await db.select({ chatId: chatParticipants.chatId }).from(chatParticipants).where(and(eq(chatParticipants.chatId, chatId), eq(chatParticipants.agentId, data.agentId))).limit(1);
|
|
@@ -3965,7 +3429,7 @@ async function findOrCreateDirectChat(db, agentAId, agentBId) {
|
|
|
3965
3429
|
const directChats = await db.select().from(chats).where(and(inArray(chats.id, commonChatIds), eq(chats.type, "direct")));
|
|
3966
3430
|
if (directChats.length > 0 && directChats[0]) return directChats[0];
|
|
3967
3431
|
}
|
|
3968
|
-
const [agentA] = await db.select({ organizationId: agents.organizationId }).from(agents).where(eq(agents.
|
|
3432
|
+
const [agentA] = await db.select({ organizationId: agents.organizationId }).from(agents).where(eq(agents.uuid, agentAId)).limit(1);
|
|
3969
3433
|
if (!agentA) throw new NotFoundError(`Agent "${agentAId}" not found`);
|
|
3970
3434
|
const chatId = randomUUID();
|
|
3971
3435
|
return db.transaction(async (tx) => {
|
|
@@ -4045,7 +3509,7 @@ async function sendMessage(db, chatId, senderId, data) {
|
|
|
4045
3509
|
const entries = (await tx.select({
|
|
4046
3510
|
agentId: chatParticipants.agentId,
|
|
4047
3511
|
inboxId: agents.inboxId
|
|
4048
|
-
}).from(chatParticipants).innerJoin(agents, eq(chatParticipants.agentId, agents.
|
|
3512
|
+
}).from(chatParticipants).innerJoin(agents, eq(chatParticipants.agentId, agents.uuid)).where(eq(chatParticipants.chatId, chatId))).filter((p) => p.agentId !== senderId).map((p) => ({
|
|
4049
3513
|
inboxId: p.inboxId,
|
|
4050
3514
|
messageId,
|
|
4051
3515
|
chatId
|
|
@@ -4074,18 +3538,15 @@ async function sendMessage(db, chatId, senderId, data) {
|
|
|
4074
3538
|
};
|
|
4075
3539
|
});
|
|
4076
3540
|
}
|
|
4077
|
-
async function sendToAgent(db,
|
|
3541
|
+
async function sendToAgent(db, senderUuid, targetName, data) {
|
|
4078
3542
|
const [sender] = await db.select({
|
|
4079
|
-
|
|
4080
|
-
organizationId: agents.organizationId
|
|
4081
|
-
}).from(agents).where(eq(agents.id, senderId)).limit(1);
|
|
4082
|
-
if (!sender) throw new NotFoundError(`Agent "${senderId}" not found`);
|
|
4083
|
-
const [target] = await db.select({
|
|
4084
|
-
id: agents.id,
|
|
3543
|
+
uuid: agents.uuid,
|
|
4085
3544
|
organizationId: agents.organizationId
|
|
4086
|
-
}).from(agents).where(eq(agents.
|
|
4087
|
-
if (!
|
|
4088
|
-
|
|
3545
|
+
}).from(agents).where(eq(agents.uuid, senderUuid)).limit(1);
|
|
3546
|
+
if (!sender) throw new NotFoundError(`Agent "${senderUuid}" not found`);
|
|
3547
|
+
const [target] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId, sender.organizationId), eq(agents.name, targetName), ne(agents.status, "deleted"))).limit(1);
|
|
3548
|
+
if (!target) throw new NotFoundError(`Agent "${targetName}" not found`);
|
|
3549
|
+
return sendMessage(db, (await findOrCreateDirectChat(db, senderUuid, target.uuid)).id, senderUuid, {
|
|
4089
3550
|
format: data.format,
|
|
4090
3551
|
content: data.content,
|
|
4091
3552
|
metadata: data.metadata,
|
|
@@ -4255,9 +3716,12 @@ function serializeDate$1(d) {
|
|
|
4255
3716
|
return d ? d.toISOString() : null;
|
|
4256
3717
|
}
|
|
4257
3718
|
async function adminAgentRoutes(app) {
|
|
3719
|
+
const listAgentsFilterSchema = z.object({ type: agentTypeSchema.optional() });
|
|
4258
3720
|
app.get("/", async (request) => {
|
|
4259
3721
|
const query = paginationQuerySchema.parse(request.query);
|
|
4260
|
-
const
|
|
3722
|
+
const { type } = listAgentsFilterSchema.parse(request.query);
|
|
3723
|
+
const org = request.query.org ?? "default";
|
|
3724
|
+
const result = await listAgents(app.db, org, query.limit, query.cursor, type);
|
|
4261
3725
|
return {
|
|
4262
3726
|
items: result.items.map((a) => ({
|
|
4263
3727
|
...a,
|
|
@@ -4268,17 +3732,35 @@ async function adminAgentRoutes(app) {
|
|
|
4268
3732
|
nextCursor: result.nextCursor
|
|
4269
3733
|
};
|
|
4270
3734
|
});
|
|
4271
|
-
app.
|
|
4272
|
-
const
|
|
3735
|
+
app.post("/", async (request, reply) => {
|
|
3736
|
+
const body = createAgentSchema.parse(request.body);
|
|
3737
|
+
const agent = await createAgent(app.db, body);
|
|
3738
|
+
return reply.status(201).send({
|
|
3739
|
+
...agent,
|
|
3740
|
+
createdAt: agent.createdAt.toISOString(),
|
|
3741
|
+
updatedAt: agent.updatedAt.toISOString()
|
|
3742
|
+
});
|
|
3743
|
+
});
|
|
3744
|
+
app.patch("/:uuid", async (request) => {
|
|
3745
|
+
const body = updateAgentSchema.parse(request.body);
|
|
3746
|
+
const agent = await updateAgent(app.db, request.params.uuid, body);
|
|
4273
3747
|
return {
|
|
4274
3748
|
...agent,
|
|
4275
3749
|
createdAt: agent.createdAt.toISOString(),
|
|
4276
3750
|
updatedAt: agent.updatedAt.toISOString()
|
|
4277
3751
|
};
|
|
4278
3752
|
});
|
|
4279
|
-
app.
|
|
3753
|
+
app.get("/:uuid", async (request) => {
|
|
3754
|
+
const agent = await getAgent(app.db, request.params.uuid);
|
|
3755
|
+
return {
|
|
3756
|
+
...agent,
|
|
3757
|
+
createdAt: agent.createdAt.toISOString(),
|
|
3758
|
+
updatedAt: agent.updatedAt.toISOString()
|
|
3759
|
+
};
|
|
3760
|
+
});
|
|
3761
|
+
app.post("/:uuid/tokens", async (request, reply) => {
|
|
4280
3762
|
const body = createAgentTokenSchema.parse(request.body);
|
|
4281
|
-
const result = await createToken(app.db, request.params.
|
|
3763
|
+
const result = await createToken(app.db, request.params.uuid, body);
|
|
4282
3764
|
return reply.status(201).send({
|
|
4283
3765
|
...result,
|
|
4284
3766
|
expiresAt: serializeDate$1(result.expiresAt),
|
|
@@ -4287,8 +3769,8 @@ async function adminAgentRoutes(app) {
|
|
|
4287
3769
|
lastUsedAt: serializeDate$1(result.lastUsedAt)
|
|
4288
3770
|
});
|
|
4289
3771
|
});
|
|
4290
|
-
app.get("/:
|
|
4291
|
-
return (await listTokens(app.db, request.params.
|
|
3772
|
+
app.get("/:uuid/tokens", async (request) => {
|
|
3773
|
+
return (await listTokens(app.db, request.params.uuid)).map((t) => ({
|
|
4292
3774
|
...t,
|
|
4293
3775
|
expiresAt: serializeDate$1(t.expiresAt),
|
|
4294
3776
|
revokedAt: serializeDate$1(t.revokedAt),
|
|
@@ -4296,39 +3778,55 @@ async function adminAgentRoutes(app) {
|
|
|
4296
3778
|
lastUsedAt: serializeDate$1(t.lastUsedAt)
|
|
4297
3779
|
}));
|
|
4298
3780
|
});
|
|
4299
|
-
app.delete("/:
|
|
4300
|
-
await revokeToken(app.db, request.params.
|
|
3781
|
+
app.delete("/:uuid/tokens/:tokenId", async (request, reply) => {
|
|
3782
|
+
await revokeToken(app.db, request.params.uuid, request.params.tokenId);
|
|
4301
3783
|
return reply.status(204).send();
|
|
4302
3784
|
});
|
|
4303
|
-
app.post("/:
|
|
4304
|
-
const {
|
|
4305
|
-
await getAgent(app.db,
|
|
4306
|
-
const wasConnected = forceDisconnect(
|
|
4307
|
-
await setOffline(app.db,
|
|
3785
|
+
app.post("/:uuid/disconnect", async (request, reply) => {
|
|
3786
|
+
const { uuid } = request.params;
|
|
3787
|
+
await getAgent(app.db, uuid);
|
|
3788
|
+
const wasConnected = forceDisconnect(uuid);
|
|
3789
|
+
await setOffline(app.db, uuid);
|
|
4308
3790
|
return reply.status(200).send({ disconnected: wasConnected });
|
|
4309
3791
|
});
|
|
4310
|
-
app.
|
|
4311
|
-
await
|
|
3792
|
+
app.post("/:uuid/suspend", async (request) => {
|
|
3793
|
+
const agent = await suspendAgent(app.db, request.params.uuid);
|
|
3794
|
+
return {
|
|
3795
|
+
...agent,
|
|
3796
|
+
createdAt: agent.createdAt.toISOString(),
|
|
3797
|
+
updatedAt: agent.updatedAt.toISOString()
|
|
3798
|
+
};
|
|
3799
|
+
});
|
|
3800
|
+
app.post("/:uuid/reactivate", async (request) => {
|
|
3801
|
+
const agent = await reactivateAgent(app.db, request.params.uuid);
|
|
3802
|
+
return {
|
|
3803
|
+
...agent,
|
|
3804
|
+
createdAt: agent.createdAt.toISOString(),
|
|
3805
|
+
updatedAt: agent.updatedAt.toISOString()
|
|
3806
|
+
};
|
|
3807
|
+
});
|
|
3808
|
+
app.delete("/:uuid", async (request, reply) => {
|
|
3809
|
+
await deleteAgent(app.db, request.params.uuid);
|
|
4312
3810
|
return reply.status(204).send();
|
|
4313
3811
|
});
|
|
4314
|
-
app.post("/:
|
|
4315
|
-
const {
|
|
4316
|
-
const [, presence] = await Promise.all([getAgent(app.db,
|
|
3812
|
+
app.post("/:uuid/test", async (request, reply) => {
|
|
3813
|
+
const { uuid } = request.params;
|
|
3814
|
+
const [, presence] = await Promise.all([getAgent(app.db, uuid), getPresence(app.db, uuid)]);
|
|
4317
3815
|
if (!presence || presence.status !== "online") return reply.status(200).send({
|
|
4318
3816
|
status: "offline",
|
|
4319
3817
|
message: "Agent is not connected. Start the client first."
|
|
4320
3818
|
});
|
|
4321
|
-
const [owner] = await app.db.select({
|
|
4322
|
-
let senderId = owner?.
|
|
3819
|
+
const [owner] = await app.db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.delegateMention, uuid), eq(agents.status, "active"))).limit(1);
|
|
3820
|
+
let senderId = owner?.uuid ?? null;
|
|
4323
3821
|
if (!senderId) {
|
|
4324
|
-
const [other] = await app.db.select({
|
|
4325
|
-
senderId = other?.
|
|
3822
|
+
const [other] = await app.db.select({ uuid: agents.uuid }).from(agents).where(and(ne(agents.uuid, uuid), eq(agents.status, "active"))).limit(1);
|
|
3823
|
+
senderId = other?.uuid ?? null;
|
|
4326
3824
|
}
|
|
4327
3825
|
if (!senderId) return reply.status(200).send({
|
|
4328
3826
|
status: "error",
|
|
4329
3827
|
message: "No suitable sender found. Need at least one other active agent."
|
|
4330
3828
|
});
|
|
4331
|
-
const chat = await findOrCreateDirectChat(app.db, senderId,
|
|
3829
|
+
const chat = await findOrCreateDirectChat(app.db, senderId, uuid);
|
|
4332
3830
|
const testContent = `[System Test] Verify your connection. Respond with your identity and role. Time: ${(/* @__PURE__ */ new Date()).toISOString()}`;
|
|
4333
3831
|
const result = await sendMessage(app.db, chat.id, senderId, {
|
|
4334
3832
|
format: "text",
|
|
@@ -4341,7 +3839,7 @@ async function adminAgentRoutes(app) {
|
|
|
4341
3839
|
const pollStart = Date.now();
|
|
4342
3840
|
while (Date.now() - pollStart < POLL_TIMEOUT) {
|
|
4343
3841
|
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
4344
|
-
const [response] = await app.db.select().from(messages).where(and(eq(messages.chatId, chat.id), eq(messages.senderId,
|
|
3842
|
+
const [response] = await app.db.select().from(messages).where(and(eq(messages.chatId, chat.id), eq(messages.senderId, uuid), gt(messages.createdAt, threshold))).limit(1);
|
|
4345
3843
|
if (response) {
|
|
4346
3844
|
const content = typeof response.content === "string" ? response.content.slice(0, 500) : JSON.stringify(response.content).slice(0, 500);
|
|
4347
3845
|
return reply.status(200).send({
|
|
@@ -4423,7 +3921,10 @@ async function adminChatRoutes(app) {
|
|
|
4423
3921
|
/** List chats with participant count */
|
|
4424
3922
|
app.get("/", async (request) => {
|
|
4425
3923
|
const query = paginationQuerySchema.parse(request.query);
|
|
4426
|
-
const
|
|
3924
|
+
const org = request.query.org ?? "default";
|
|
3925
|
+
const conditions = [eq(chats.organizationId, org)];
|
|
3926
|
+
if (query.cursor) conditions.push(lt(chats.createdAt, new Date(query.cursor)));
|
|
3927
|
+
const where = and(...conditions);
|
|
4427
3928
|
const rows = await app.db.select({
|
|
4428
3929
|
id: chats.id,
|
|
4429
3930
|
organizationId: chats.organizationId,
|
|
@@ -4502,9 +4003,10 @@ async function adminChatRoutes(app) {
|
|
|
4502
4003
|
});
|
|
4503
4004
|
}
|
|
4504
4005
|
async function adminOverviewRoutes(app) {
|
|
4505
|
-
app.get("/", async () => {
|
|
4506
|
-
const
|
|
4507
|
-
const [
|
|
4006
|
+
app.get("/", async (request) => {
|
|
4007
|
+
const org = (request.query ?? {}).org ?? "default";
|
|
4008
|
+
const [agentCount] = await app.db.select({ count: sql`count(*)::int` }).from(agents).where(and(ne(agents.status, "deleted"), eq(agents.organizationId, org)));
|
|
4009
|
+
const [chatCount] = await app.db.select({ count: sql`count(*)::int` }).from(chats).where(eq(chats.organizationId, org));
|
|
4508
4010
|
const onlineCount = await getOnlineCount(app.db);
|
|
4509
4011
|
return {
|
|
4510
4012
|
agents: agentCount?.count ?? 0,
|
|
@@ -4657,7 +4159,7 @@ async function agentChatRoutes(app) {
|
|
|
4657
4159
|
app.post("/", async (request, reply) => {
|
|
4658
4160
|
const identity = requireAgent(request);
|
|
4659
4161
|
const body = createChatSchema.parse(request.body);
|
|
4660
|
-
const result = await createChat(app.db, identity.
|
|
4162
|
+
const result = await createChat(app.db, identity.uuid, body);
|
|
4661
4163
|
return reply.status(201).send({
|
|
4662
4164
|
...serializeChat(result),
|
|
4663
4165
|
participants: result.participants.map((p) => ({
|
|
@@ -4669,7 +4171,7 @@ async function agentChatRoutes(app) {
|
|
|
4669
4171
|
app.get("/", async (request) => {
|
|
4670
4172
|
const identity = requireAgent(request);
|
|
4671
4173
|
const query = paginationQuerySchema.parse(request.query);
|
|
4672
|
-
const result = await listChats(app.db, identity.
|
|
4174
|
+
const result = await listChats(app.db, identity.uuid, query.limit, query.cursor);
|
|
4673
4175
|
return {
|
|
4674
4176
|
items: result.items.map(serializeChat),
|
|
4675
4177
|
nextCursor: result.nextCursor
|
|
@@ -4677,7 +4179,7 @@ async function agentChatRoutes(app) {
|
|
|
4677
4179
|
});
|
|
4678
4180
|
app.get("/:chatId", async (request) => {
|
|
4679
4181
|
const identity = requireAgent(request);
|
|
4680
|
-
await assertParticipant(app.db, request.params.chatId, identity.
|
|
4182
|
+
await assertParticipant(app.db, request.params.chatId, identity.uuid);
|
|
4681
4183
|
const detail = await getChatDetail(app.db, request.params.chatId);
|
|
4682
4184
|
return {
|
|
4683
4185
|
...serializeChat(detail),
|
|
@@ -4690,7 +4192,7 @@ async function agentChatRoutes(app) {
|
|
|
4690
4192
|
app.post("/:chatId/participants", async (request, reply) => {
|
|
4691
4193
|
const identity = requireAgent(request);
|
|
4692
4194
|
const body = addParticipantSchema.parse(request.body);
|
|
4693
|
-
const participants = await addParticipant(app.db, request.params.chatId, identity.
|
|
4195
|
+
const participants = await addParticipant(app.db, request.params.chatId, identity.uuid, body);
|
|
4694
4196
|
return reply.status(201).send(participants.map((p) => ({
|
|
4695
4197
|
...p,
|
|
4696
4198
|
joinedAt: p.joinedAt.toISOString()
|
|
@@ -4698,20 +4200,10 @@ async function agentChatRoutes(app) {
|
|
|
4698
4200
|
});
|
|
4699
4201
|
app.delete("/:chatId/participants/:agentId", async (request, reply) => {
|
|
4700
4202
|
const identity = requireAgent(request);
|
|
4701
|
-
await removeParticipant(app.db, request.params.chatId, identity.
|
|
4203
|
+
await removeParticipant(app.db, request.params.chatId, identity.uuid, request.params.agentId);
|
|
4702
4204
|
return reply.status(204).send();
|
|
4703
4205
|
});
|
|
4704
4206
|
}
|
|
4705
|
-
async function agentContextTreeRoutes(app) {
|
|
4706
|
-
app.get("/", async (_request, reply) => {
|
|
4707
|
-
const { repo, branch } = app.config.contextTree;
|
|
4708
|
-
if (!repo) return reply.status(404).send({ error: "Context Tree not configured" });
|
|
4709
|
-
return reply.send({
|
|
4710
|
-
repo,
|
|
4711
|
-
branch
|
|
4712
|
-
});
|
|
4713
|
-
});
|
|
4714
|
-
}
|
|
4715
4207
|
async function agentFeishuBotRoutes(app) {
|
|
4716
4208
|
/**
|
|
4717
4209
|
* PUT /agent/me/feishu-bot
|
|
@@ -4720,8 +4212,8 @@ async function agentFeishuBotRoutes(app) {
|
|
|
4720
4212
|
app.put("/me/feishu-bot", async (request, reply) => {
|
|
4721
4213
|
const identity = requireAgent(request);
|
|
4722
4214
|
const body = selfServiceFeishuBotSchema.parse(request.body);
|
|
4723
|
-
if ((await getAgent(app.db, identity.
|
|
4724
|
-
const current = (await listAdapterConfigs(app.db)).find((c) => c.agentId === identity.
|
|
4215
|
+
if ((await getAgent(app.db, identity.uuid)).type === "human") throw new BadRequestError("Human agents cannot bind Feishu bots. Use bind-user instead.");
|
|
4216
|
+
const current = (await listAdapterConfigs(app.db)).find((c) => c.agentId === identity.uuid && c.platform === "feishu");
|
|
4725
4217
|
let config;
|
|
4726
4218
|
if (current) config = await updateAdapterConfig(app.db, current.id, {
|
|
4727
4219
|
credentials: {
|
|
@@ -4732,7 +4224,7 @@ async function agentFeishuBotRoutes(app) {
|
|
|
4732
4224
|
}, app.config.secrets.encryptionKey);
|
|
4733
4225
|
else config = await createAdapterConfig(app.db, {
|
|
4734
4226
|
platform: "feishu",
|
|
4735
|
-
agentId: identity.
|
|
4227
|
+
agentId: identity.uuid,
|
|
4736
4228
|
credentials: {
|
|
4737
4229
|
app_id: body.appId,
|
|
4738
4230
|
app_secret: body.appSecret
|
|
@@ -4753,7 +4245,7 @@ async function agentFeishuBotRoutes(app) {
|
|
|
4753
4245
|
*/
|
|
4754
4246
|
app.delete("/me/feishu-bot", async (request, reply) => {
|
|
4755
4247
|
const identity = requireAgent(request);
|
|
4756
|
-
const current = (await listAdapterConfigs(app.db)).find((c) => c.agentId === identity.
|
|
4248
|
+
const current = (await listAdapterConfigs(app.db)).find((c) => c.agentId === identity.uuid && c.platform === "feishu");
|
|
4757
4249
|
if (!current) return reply.status(204).send();
|
|
4758
4250
|
await deleteAdapterConfig(app.db, current.id);
|
|
4759
4251
|
app.adapterManager.reload().catch((err) => app.log.error(err, "Adapter reload failed after self-service unbind"));
|
|
@@ -4772,7 +4264,7 @@ async function agentFeishuUserRoutes(app) {
|
|
|
4772
4264
|
const body = delegateFeishuUserSchema.parse(request.body);
|
|
4773
4265
|
const humanAgent = await getAgent(app.db, humanAgentId);
|
|
4774
4266
|
if (humanAgent.type !== "human") throw new BadRequestError(`Agent "${humanAgentId}" is not a human agent`);
|
|
4775
|
-
if (humanAgent.delegateMention !== identity.
|
|
4267
|
+
if (humanAgent.delegateMention !== identity.uuid) throw new ForbiddenError(`Agent "${identity.uuid}" is not the delegate of "${humanAgentId}". Expected delegate_mention="${identity.uuid}" but found "${humanAgent.delegateMention ?? "(none)"}".`);
|
|
4776
4268
|
const mapping = await createAgentMapping(app.db, {
|
|
4777
4269
|
platform: "feishu",
|
|
4778
4270
|
externalUserId: body.feishuUserId,
|
|
@@ -4797,7 +4289,7 @@ async function agentFeishuUserRoutes(app) {
|
|
|
4797
4289
|
app.delete("/:humanAgentId/feishu-user", async (request, reply) => {
|
|
4798
4290
|
const identity = requireAgent(request);
|
|
4799
4291
|
const { humanAgentId } = request.params;
|
|
4800
|
-
if ((await getAgent(app.db, humanAgentId)).delegateMention !== identity.
|
|
4292
|
+
if ((await getAgent(app.db, humanAgentId)).delegateMention !== identity.uuid) throw new ForbiddenError(`Agent "${identity.uuid}" is not the delegate of "${humanAgentId}"`);
|
|
4801
4293
|
await app.db.delete(adapterAgentMappings).where(and(eq(adapterAgentMappings.platform, "feishu"), eq(adapterAgentMappings.agentId, humanAgentId)));
|
|
4802
4294
|
return reply.status(204).send();
|
|
4803
4295
|
});
|
|
@@ -4906,7 +4398,7 @@ async function agentInboxRoutes(app) {
|
|
|
4906
4398
|
async function agentMeRoutes(app) {
|
|
4907
4399
|
app.get("/me", async (request) => {
|
|
4908
4400
|
const identity = requireAgent(request);
|
|
4909
|
-
const agent = await getAgent(app.db, identity.
|
|
4401
|
+
const agent = await getAgent(app.db, identity.uuid);
|
|
4910
4402
|
return {
|
|
4911
4403
|
...agent,
|
|
4912
4404
|
createdAt: agent.createdAt.toISOString(),
|
|
@@ -4921,9 +4413,9 @@ const editMessageSchema = z.object({
|
|
|
4921
4413
|
async function agentMessageRoutes(app) {
|
|
4922
4414
|
app.post("/:chatId/messages", async (request, reply) => {
|
|
4923
4415
|
const identity = requireAgent(request);
|
|
4924
|
-
await assertParticipant(app.db, request.params.chatId, identity.
|
|
4416
|
+
await assertParticipant(app.db, request.params.chatId, identity.uuid);
|
|
4925
4417
|
const body = sendMessageSchema.parse(request.body);
|
|
4926
|
-
const { message: msg, recipients } = await sendMessage(app.db, request.params.chatId, identity.
|
|
4418
|
+
const { message: msg, recipients } = await sendMessage(app.db, request.params.chatId, identity.uuid, body);
|
|
4927
4419
|
notifyRecipients(app.notifier, recipients, msg.id);
|
|
4928
4420
|
return reply.status(201).send({
|
|
4929
4421
|
...msg,
|
|
@@ -4932,9 +4424,9 @@ async function agentMessageRoutes(app) {
|
|
|
4932
4424
|
});
|
|
4933
4425
|
app.patch("/:chatId/messages/:messageId", async (request) => {
|
|
4934
4426
|
const identity = requireAgent(request);
|
|
4935
|
-
await assertParticipant(app.db, request.params.chatId, identity.
|
|
4427
|
+
await assertParticipant(app.db, request.params.chatId, identity.uuid);
|
|
4936
4428
|
const body = editMessageSchema.parse(request.body);
|
|
4937
|
-
const msg = await editMessage(app.db, request.params.chatId, request.params.messageId, identity.
|
|
4429
|
+
const msg = await editMessage(app.db, request.params.chatId, request.params.messageId, identity.uuid, body);
|
|
4938
4430
|
app.adapterManager.editOutboundMessage(msg.id, msg.format, msg.content).catch((err) => app.log.error({
|
|
4939
4431
|
err,
|
|
4940
4432
|
messageId: msg.id
|
|
@@ -4946,7 +4438,7 @@ async function agentMessageRoutes(app) {
|
|
|
4946
4438
|
});
|
|
4947
4439
|
app.get("/:chatId/messages", async (request) => {
|
|
4948
4440
|
const identity = requireAgent(request);
|
|
4949
|
-
await assertParticipant(app.db, request.params.chatId, identity.
|
|
4441
|
+
await assertParticipant(app.db, request.params.chatId, identity.uuid);
|
|
4950
4442
|
const query = paginationQuerySchema.parse(request.query);
|
|
4951
4443
|
const result = await listMessages(app.db, request.params.chatId, query.limit, query.cursor);
|
|
4952
4444
|
return {
|
|
@@ -4959,10 +4451,10 @@ async function agentMessageRoutes(app) {
|
|
|
4959
4451
|
});
|
|
4960
4452
|
}
|
|
4961
4453
|
async function agentSendToAgentRoutes(app) {
|
|
4962
|
-
app.post("/:
|
|
4454
|
+
app.post("/:name/messages", async (request, reply) => {
|
|
4963
4455
|
const identity = requireAgent(request);
|
|
4964
4456
|
const body = sendToAgentSchema.parse(request.body);
|
|
4965
|
-
const { message: msg, recipients } = await sendToAgent(app.db, identity.
|
|
4457
|
+
const { message: msg, recipients } = await sendToAgent(app.db, identity.uuid, request.params.name, body);
|
|
4966
4458
|
notifyRecipients(app.notifier, recipients, msg.id);
|
|
4967
4459
|
return reply.status(201).send({
|
|
4968
4460
|
...msg,
|
|
@@ -4978,35 +4470,54 @@ function agentWsRoutes(notifier, instanceId) {
|
|
|
4978
4470
|
socket.close(4001, "Unauthorized");
|
|
4979
4471
|
return;
|
|
4980
4472
|
}
|
|
4981
|
-
if (hasActiveConnection(agent.
|
|
4473
|
+
if (hasActiveConnection(agent.uuid)) {
|
|
4982
4474
|
socket.close(WS_CLOSE_ALREADY_CONNECTED, "Agent already connected");
|
|
4983
4475
|
return;
|
|
4984
4476
|
}
|
|
4985
4477
|
const inboxId = agent.inboxId;
|
|
4986
|
-
await setOnline(app.db, agent.
|
|
4987
|
-
setConnection(agent.
|
|
4478
|
+
await setOnline(app.db, agent.uuid, instanceId);
|
|
4479
|
+
setConnection(agent.uuid, socket);
|
|
4988
4480
|
notifier.subscribe(inboxId, socket);
|
|
4989
4481
|
socket.on("close", async () => {
|
|
4990
4482
|
notifier.unsubscribe(inboxId, socket);
|
|
4991
|
-
if (removeConnection(agent.
|
|
4992
|
-
await setOffline(app.db, agent.
|
|
4483
|
+
if (removeConnection(agent.uuid, socket)) try {
|
|
4484
|
+
await setOffline(app.db, agent.uuid);
|
|
4993
4485
|
} catch {}
|
|
4994
4486
|
});
|
|
4995
4487
|
});
|
|
4996
4488
|
};
|
|
4997
4489
|
}
|
|
4490
|
+
async function bootstrapConfigRoutes(app) {
|
|
4491
|
+
/** Public endpoint — returns bootstrap prerequisites for CLI auto-discovery. */
|
|
4492
|
+
app.get("/config", async () => {
|
|
4493
|
+
return { allowedOrg: app.config.github.allowedOrg ?? null };
|
|
4494
|
+
});
|
|
4495
|
+
}
|
|
4998
4496
|
async function bootstrapRoutes(app) {
|
|
4999
4497
|
/**
|
|
5000
|
-
* POST /bootstrap/:
|
|
4498
|
+
* POST /bootstrap/:name/token
|
|
5001
4499
|
* GitHub identity → Agent token.
|
|
4500
|
+
* Auto-creates the agent if it does not exist.
|
|
5002
4501
|
* Only works when the agent has no active tokens.
|
|
5003
4502
|
*/
|
|
5004
|
-
app.post("/:
|
|
5005
|
-
const {
|
|
4503
|
+
app.post("/:name/token", async (request, reply) => {
|
|
4504
|
+
const { name } = request.params;
|
|
5006
4505
|
const githubUser = request.githubUser;
|
|
5007
4506
|
if (!githubUser) throw new ForbiddenError("GitHub authentication required");
|
|
4507
|
+
const allowedOrg = app.config.github.allowedOrg;
|
|
4508
|
+
if (!allowedOrg) throw new ForbiddenError("FIRST_TREE_HUB_GITHUB_ALLOWED_ORG is not configured. Agent registration is disabled.");
|
|
4509
|
+
const githubToken = request.headers["x-github-token"];
|
|
4510
|
+
if (!githubToken || typeof githubToken !== "string") throw new ForbiddenError("Missing GitHub token for org membership check");
|
|
4511
|
+
if (!await checkGitHubOrgMembership(githubToken, allowedOrg)) throw new ForbiddenError(`GitHub user "${githubUser.username}" is not a member of organization "${allowedOrg}"`);
|
|
5008
4512
|
const body = bootstrapTokenRequestSchema.parse(request.body ?? {});
|
|
5009
|
-
const result = await bootstrapToken(app.db,
|
|
4513
|
+
const result = await bootstrapToken(app.db, name, "default", githubUser.username, {
|
|
4514
|
+
tokenName: body.name,
|
|
4515
|
+
type: body.type,
|
|
4516
|
+
displayName: body.displayName,
|
|
4517
|
+
delegateMention: body.delegateMention,
|
|
4518
|
+
profile: body.profile,
|
|
4519
|
+
metadata: body.metadata
|
|
4520
|
+
});
|
|
5010
4521
|
return reply.status(201).send({
|
|
5011
4522
|
id: result.id,
|
|
5012
4523
|
agentId: result.agentId,
|
|
@@ -5017,16 +4528,16 @@ async function bootstrapRoutes(app) {
|
|
|
5017
4528
|
});
|
|
5018
4529
|
});
|
|
5019
4530
|
/**
|
|
5020
|
-
* GET /bootstrap/:
|
|
5021
|
-
* Check if an agent exists and its status (for polling
|
|
4531
|
+
* GET /bootstrap/:name/status
|
|
4532
|
+
* Check if an agent exists and its status (for polling).
|
|
5022
4533
|
*/
|
|
5023
|
-
app.get("/:
|
|
5024
|
-
const {
|
|
4534
|
+
app.get("/:name/status", async (request) => {
|
|
4535
|
+
const { name } = request.params;
|
|
5025
4536
|
const githubUser = request.githubUser;
|
|
5026
4537
|
if (!githubUser) throw new ForbiddenError("GitHub authentication required");
|
|
5027
4538
|
try {
|
|
5028
|
-
const agent = await
|
|
5029
|
-
if (!(Array.isArray(agent.metadata?.owners) ? agent.metadata.owners : []).includes(githubUser.username)) throw new ForbiddenError(`GitHub user "${githubUser.username}" is not in the owners list for agent "${
|
|
4539
|
+
const agent = await getAgentByName(app.db, "default", name);
|
|
4540
|
+
if (!(Array.isArray(agent.metadata?.owners) ? agent.metadata.owners : []).includes(githubUser.username)) throw new ForbiddenError(`GitHub user "${githubUser.username}" is not in the owners list for agent "${name}"`);
|
|
5030
4541
|
return {
|
|
5031
4542
|
exists: true,
|
|
5032
4543
|
status: agent.status
|
|
@@ -5043,11 +4554,9 @@ async function bootstrapRoutes(app) {
|
|
|
5043
4554
|
async function contextTreeInfoRoutes(app) {
|
|
5044
4555
|
/** Public endpoint — returns Context Tree repo metadata for CLI auto-discovery. */
|
|
5045
4556
|
app.get("/info", async () => {
|
|
5046
|
-
const { repo, branch } = app.config.contextTree;
|
|
5047
4557
|
return {
|
|
5048
|
-
repo,
|
|
5049
|
-
branch
|
|
5050
|
-
lastSync: null
|
|
4558
|
+
repo: app.config.contextTree?.repo ?? null,
|
|
4559
|
+
branch: app.config.contextTree?.branch ?? null
|
|
5051
4560
|
};
|
|
5052
4561
|
});
|
|
5053
4562
|
}
|
|
@@ -5093,34 +4602,35 @@ function verifySignature(secret, rawBody, signatureHeader) {
|
|
|
5093
4602
|
if (expectedBuf.length !== receivedBuf.length || !timingSafeEqual(expectedBuf, receivedBuf)) throw new UnauthorizedError("Invalid webhook signature");
|
|
5094
4603
|
}
|
|
5095
4604
|
async function ensureGitHubAdapterAgent(db) {
|
|
5096
|
-
const [existing] = await db.select({
|
|
5097
|
-
if (existing) return existing.
|
|
4605
|
+
const [existing] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId, "default"), eq(agents.name, GITHUB_ADAPTER_ID))).limit(1);
|
|
4606
|
+
if (existing) return existing.uuid;
|
|
5098
4607
|
try {
|
|
5099
4608
|
return (await createAgent(db, {
|
|
5100
|
-
|
|
4609
|
+
name: GITHUB_ADAPTER_ID,
|
|
5101
4610
|
type: "autonomous_agent",
|
|
5102
4611
|
displayName: "GitHub Adapter",
|
|
5103
4612
|
metadata: {
|
|
5104
4613
|
source: "github",
|
|
5105
4614
|
managed: true
|
|
5106
4615
|
}
|
|
5107
|
-
})).
|
|
4616
|
+
})).uuid;
|
|
5108
4617
|
} catch (err) {
|
|
5109
4618
|
if (err instanceof ConflictError) {
|
|
5110
|
-
const [created] = await db.select({
|
|
5111
|
-
if (created) return created.
|
|
4619
|
+
const [created] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId, "default"), eq(agents.name, GITHUB_ADAPTER_ID))).limit(1);
|
|
4620
|
+
if (created) return created.uuid;
|
|
5112
4621
|
}
|
|
5113
4622
|
throw err;
|
|
5114
4623
|
}
|
|
5115
4624
|
}
|
|
5116
4625
|
async function findTargetAgent(db, repoFullName) {
|
|
5117
4626
|
const allAgents = await db.select({
|
|
5118
|
-
id: agents.
|
|
4627
|
+
id: agents.uuid,
|
|
4628
|
+
name: agents.name,
|
|
5119
4629
|
metadata: agents.metadata,
|
|
5120
4630
|
type: agents.type
|
|
5121
4631
|
}).from(agents).where(eq(agents.status, "active"));
|
|
5122
4632
|
for (const agent of allAgents) {
|
|
5123
|
-
if (agent.
|
|
4633
|
+
if (agent.name === GITHUB_ADAPTER_ID) continue;
|
|
5124
4634
|
const meta = agent.metadata;
|
|
5125
4635
|
if (meta && typeof meta === "object" && "github" in meta) {
|
|
5126
4636
|
const github = meta.github;
|
|
@@ -5155,27 +4665,29 @@ function extractMentions(text) {
|
|
|
5155
4665
|
async function routeMentionDelegations(app, mentionedNames, ctx) {
|
|
5156
4666
|
if (mentionedNames.length === 0) return 0;
|
|
5157
4667
|
const delegates = await app.db.select({
|
|
5158
|
-
id: agents.
|
|
4668
|
+
id: agents.uuid,
|
|
4669
|
+
name: agents.name,
|
|
5159
4670
|
delegateMention: agents.delegateMention,
|
|
5160
4671
|
status: agents.status
|
|
5161
|
-
}).from(agents).where(and(inArray(agents.
|
|
4672
|
+
}).from(agents).where(and(inArray(agents.name, mentionedNames), isNotNull(agents.delegateMention)));
|
|
5162
4673
|
let routed = 0;
|
|
5163
4674
|
for (const agent of delegates) {
|
|
5164
4675
|
if (agent.status !== "active" || !agent.delegateMention) continue;
|
|
5165
4676
|
const [target] = await app.db.select({
|
|
5166
|
-
id: agents.
|
|
4677
|
+
id: agents.uuid,
|
|
5167
4678
|
status: agents.status
|
|
5168
|
-
}).from(agents).where(eq(agents.
|
|
4679
|
+
}).from(agents).where(eq(agents.uuid, agent.delegateMention)).limit(1);
|
|
5169
4680
|
if (!target || target.status !== "active") {
|
|
5170
|
-
app.log.warn(`delegate_mention target "${agent.delegateMention}" for "${agent.
|
|
4681
|
+
app.log.warn(`delegate_mention target "${agent.delegateMention}" for "${agent.name}" is not active, skipping`);
|
|
5171
4682
|
continue;
|
|
5172
4683
|
}
|
|
5173
4684
|
try {
|
|
5174
|
-
const
|
|
4685
|
+
const chat = await findOrCreateDirectChat(app.db, agent.id, agent.delegateMention);
|
|
4686
|
+
const { message: msg, recipients } = await sendMessage(app.db, chat.id, agent.id, {
|
|
5175
4687
|
format: "card",
|
|
5176
4688
|
content: {
|
|
5177
4689
|
type: "github_mention",
|
|
5178
|
-
mentionedUser: agent.
|
|
4690
|
+
mentionedUser: agent.name,
|
|
5179
4691
|
event: ctx.event,
|
|
5180
4692
|
repository: ctx.repository,
|
|
5181
4693
|
sender: ctx.sender,
|
|
@@ -5186,13 +4698,13 @@ async function routeMentionDelegations(app, mentionedNames, ctx) {
|
|
|
5186
4698
|
metadata: {
|
|
5187
4699
|
source: "github",
|
|
5188
4700
|
event: "mention_delegation",
|
|
5189
|
-
mentionedUser: agent.
|
|
4701
|
+
mentionedUser: agent.name
|
|
5190
4702
|
}
|
|
5191
4703
|
});
|
|
5192
4704
|
notifyRecipients(app.notifier, recipients, msg.id);
|
|
5193
4705
|
routed++;
|
|
5194
4706
|
} catch (err) {
|
|
5195
|
-
app.log.error(err, `Failed to route mention delegation from "${agent.
|
|
4707
|
+
app.log.error(err, `Failed to route mention delegation from "${agent.name}" to "${agent.delegateMention}"`);
|
|
5196
4708
|
}
|
|
5197
4709
|
}
|
|
5198
4710
|
return routed;
|
|
@@ -5480,7 +4992,8 @@ async function handleIssuesEvent(app, eventType, payload, reply) {
|
|
|
5480
4992
|
event: "issues",
|
|
5481
4993
|
action: data.action
|
|
5482
4994
|
};
|
|
5483
|
-
const
|
|
4995
|
+
const chat = await findOrCreateDirectChat(app.db, senderId, targetAgentId);
|
|
4996
|
+
const { message: msg, recipients } = await sendMessage(app.db, chat.id, senderId, {
|
|
5484
4997
|
format: "card",
|
|
5485
4998
|
content,
|
|
5486
4999
|
metadata
|
|
@@ -5535,7 +5048,8 @@ async function handleIssueCommentEvent(app, eventType, payload, reply) {
|
|
|
5535
5048
|
event: "issue_comment",
|
|
5536
5049
|
action: data.action
|
|
5537
5050
|
};
|
|
5538
|
-
const
|
|
5051
|
+
const chat = await findOrCreateDirectChat(app.db, senderId, targetAgentId);
|
|
5052
|
+
const { message: msg, recipients } = await sendMessage(app.db, chat.id, senderId, {
|
|
5539
5053
|
format: "card",
|
|
5540
5054
|
content,
|
|
5541
5055
|
metadata
|
|
@@ -5609,10 +5123,11 @@ function agentAuthHook(db) {
|
|
|
5609
5123
|
const [tokenDetail] = await db.select({ expiresAt: agentTokens.expiresAt }).from(agentTokens).where(eq(agentTokens.id, tokenRow.tokenId)).limit(1);
|
|
5610
5124
|
if (tokenDetail?.expiresAt && tokenDetail.expiresAt < now) throw new UnauthorizedError("Token has expired");
|
|
5611
5125
|
const [agent] = await db.select({
|
|
5612
|
-
|
|
5126
|
+
uuid: agents.uuid,
|
|
5127
|
+
name: agents.name,
|
|
5613
5128
|
organizationId: agents.organizationId,
|
|
5614
5129
|
inboxId: agents.inboxId
|
|
5615
|
-
}).from(agents).where(and(eq(agents.
|
|
5130
|
+
}).from(agents).where(and(eq(agents.uuid, tokenRow.agentId), eq(agents.status, "active"))).limit(1);
|
|
5616
5131
|
if (!agent) throw new UnauthorizedError("Agent is suspended or not found");
|
|
5617
5132
|
db.update(agentTokens).set({ lastUsedAt: now }).where(eq(agentTokens.id, tokenRow.tokenId)).then(() => {}, () => {});
|
|
5618
5133
|
request.agent = agent;
|
|
@@ -5971,10 +5486,10 @@ async function handleBindCommand(db, bot, event, agentId, log) {
|
|
|
5971
5486
|
return;
|
|
5972
5487
|
}
|
|
5973
5488
|
const [agent] = await db.select({
|
|
5974
|
-
id: agents.
|
|
5489
|
+
id: agents.uuid,
|
|
5975
5490
|
type: agents.type,
|
|
5976
5491
|
status: agents.status
|
|
5977
|
-
}).from(agents).where(eq(agents.
|
|
5492
|
+
}).from(agents).where(eq(agents.uuid, agentId)).limit(1);
|
|
5978
5493
|
if (!agent) {
|
|
5979
5494
|
await reply(`Agent "${agentId}" not found. Check the ID and try again.`);
|
|
5980
5495
|
return;
|
|
@@ -6030,7 +5545,7 @@ async function processFeishuOutbound(db, findBotByAgentId, log) {
|
|
|
6030
5545
|
WHERE id IN (
|
|
6031
5546
|
SELECT ie.id FROM inbox_entries ie
|
|
6032
5547
|
JOIN agents a ON ie.inbox_id = a.inbox_id
|
|
6033
|
-
JOIN adapter_agent_mappings aam ON a.
|
|
5548
|
+
JOIN adapter_agent_mappings aam ON a.uuid = aam.agent_id
|
|
6034
5549
|
WHERE aam.platform = 'feishu' AND ie.status = 'pending'
|
|
6035
5550
|
ORDER BY ie.created_at
|
|
6036
5551
|
LIMIT ${OUTBOUND_BATCH_SIZE$1}
|
|
@@ -6221,8 +5736,8 @@ function createKaelRuntime(db, encryptionKey, kaelEndpoint, kaelApiKey, serverUr
|
|
|
6221
5736
|
}
|
|
6222
5737
|
const configs = await db.select().from(adapterConfigs).where(and(eq(adapterConfigs.platform, "kael"), eq(adapterConfigs.status, "active")));
|
|
6223
5738
|
const configAgentIds = configs.filter((c) => c.credentials).map((c) => c.agentId);
|
|
6224
|
-
const agentRows = configAgentIds.length > 0 ? await db.execute(sql`SELECT
|
|
6225
|
-
const agentInboxMap = new Map(agentRows.map((a) => [a.
|
|
5739
|
+
const agentRows = configAgentIds.length > 0 ? await db.execute(sql`SELECT uuid, inbox_id FROM agents WHERE uuid IN (${sql.join(configAgentIds.map((id) => sql`${id}`), sql`, `)}) AND status = 'active'`) : [];
|
|
5740
|
+
const agentInboxMap = new Map(agentRows.map((a) => [a.uuid, a.inbox_id]));
|
|
6226
5741
|
const seen = /* @__PURE__ */ new Set();
|
|
6227
5742
|
for (const config of configs) {
|
|
6228
5743
|
if (!config.credentials) continue;
|
|
@@ -6282,10 +5797,10 @@ function createKaelRuntime(db, encryptionKey, kaelEndpoint, kaelApiKey, serverUr
|
|
|
6282
5797
|
WHERE id IN (
|
|
6283
5798
|
SELECT ie.id FROM inbox_entries ie
|
|
6284
5799
|
JOIN agents a ON ie.inbox_id = a.inbox_id
|
|
6285
|
-
JOIN adapter_configs ac ON a.
|
|
5800
|
+
JOIN adapter_configs ac ON a.uuid = ac.agent_id
|
|
6286
5801
|
WHERE ac.platform = 'kael' AND ac.status = 'active'
|
|
6287
5802
|
AND ie.status = 'pending'
|
|
6288
|
-
AND a.
|
|
5803
|
+
AND a.uuid IN (${sql.join(agentIds.map((id) => sql`${id}`), sql`, `)})
|
|
6289
5804
|
ORDER BY ie.created_at
|
|
6290
5805
|
LIMIT ${OUTBOUND_BATCH_SIZE}
|
|
6291
5806
|
FOR UPDATE OF ie SKIP LOCKED
|
|
@@ -6420,6 +5935,7 @@ async function buildApp(config) {
|
|
|
6420
5935
|
await api.register(githubWebhookRoutes, { prefix: "/webhooks" });
|
|
6421
5936
|
await api.register(adminAuthRoutes, { prefix: "/admin/auth" });
|
|
6422
5937
|
await api.register(contextTreeInfoRoutes, { prefix: "/context-tree" });
|
|
5938
|
+
await api.register(bootstrapConfigRoutes, { prefix: "/bootstrap" });
|
|
6423
5939
|
await api.register(async (bootstrapApp) => {
|
|
6424
5940
|
bootstrapApp.addHook("onRequest", githubAuth);
|
|
6425
5941
|
await bootstrapApp.register(bootstrapRoutes);
|
|
@@ -6428,10 +5944,6 @@ async function buildApp(config) {
|
|
|
6428
5944
|
adminApp.addHook("onRequest", adminAuth);
|
|
6429
5945
|
await adminApp.register(adminAgentRoutes);
|
|
6430
5946
|
}, { prefix: "/admin/agents" });
|
|
6431
|
-
await api.register(async (adminApp) => {
|
|
6432
|
-
adminApp.addHook("onRequest", adminAuth);
|
|
6433
|
-
await adminApp.register(adminAgentSyncRoutes);
|
|
6434
|
-
}, { prefix: "/admin/agents/sync" });
|
|
6435
5947
|
await api.register(async (adminApp) => {
|
|
6436
5948
|
adminApp.addHook("onRequest", adminAuth);
|
|
6437
5949
|
await adminApp.register(adminSystemConfigRoutes);
|
|
@@ -6467,7 +5979,6 @@ async function buildApp(config) {
|
|
|
6467
5979
|
await agentApp.register(agentMessageRoutes, { prefix: "/chats" });
|
|
6468
5980
|
await agentApp.register(agentSendToAgentRoutes, { prefix: "/agents" });
|
|
6469
5981
|
await agentApp.register(agentInboxRoutes, { prefix: "/inbox" });
|
|
6470
|
-
await agentApp.register(agentContextTreeRoutes, { prefix: "/context-tree" });
|
|
6471
5982
|
await agentApp.register(agentFeishuBotRoutes);
|
|
6472
5983
|
await agentApp.register(agentFeishuUserRoutes, { prefix: "/delegated" });
|
|
6473
5984
|
await agentApp.register(agentWsRoutes(notifier, config.instanceId), { prefix: "/ws" });
|
|
@@ -6503,19 +6014,6 @@ async function buildApp(config) {
|
|
|
6503
6014
|
backgroundTasks.start();
|
|
6504
6015
|
await adapterManager.reload();
|
|
6505
6016
|
await kaelRuntime?.reload();
|
|
6506
|
-
const { repo: ctRepo, branch: ctBranch, syncInterval } = config.contextTree;
|
|
6507
|
-
const { token: ghToken } = config.github;
|
|
6508
|
-
try {
|
|
6509
|
-
const report = await syncFromGitHub(db, ctRepo, ctBranch, ghToken);
|
|
6510
|
-
app.log.info(`Context Tree sync: created=${report.created} updated=${report.updated} unchanged=${report.unchanged} errors=${report.errors}`);
|
|
6511
|
-
} catch (err) {
|
|
6512
|
-
app.log.error(err, "Initial Context Tree sync failed");
|
|
6513
|
-
}
|
|
6514
|
-
const intervalMs = syncInterval * 1e3;
|
|
6515
|
-
const timer = setInterval(() => {
|
|
6516
|
-
syncFromGitHub(db, ctRepo, ctBranch, ghToken).catch((err) => app.log.error(err, "Periodic Context Tree sync failed"));
|
|
6517
|
-
}, intervalMs);
|
|
6518
|
-
app.addHook("onClose", async () => clearInterval(timer));
|
|
6519
6017
|
});
|
|
6520
6018
|
app.addHook("onClose", async () => {
|
|
6521
6019
|
backgroundTasks.stop();
|
|
@@ -6635,4 +6133,4 @@ function resolveWebDist() {
|
|
|
6635
6133
|
} catch {}
|
|
6636
6134
|
}
|
|
6637
6135
|
//#endregion
|
|
6638
|
-
export {
|
|
6136
|
+
export { createAdminUser$1 as A, printResults as C, isDockerAvailable as D, ensurePostgres as E, cleanWorkspaces as F, FirstTreeHubSDK as M, SdkError as N, stopPostgres as O, SessionRegistry as P, checkWebSocket as S, status as T, checkGitHubToken as _, formatCheckReport as a, checkServerHealth as b, onboardCreate as c, checkAgentConfigs as d, checkAgentTokens as f, checkDocker as g, checkDatabase as h, promptMissingFields as i, hasAdminUser as j, ClientRuntime as k, saveOnboardState as l, checkContextTreeRepo as m, isInteractive as n, loadOnboardState as o, checkClientConfig as p, promptAddAgent as r, onboardCheck as s, startServer as t, runMigrations as u, checkNodeVersion as v, blank as w, checkServerReachable as x, checkServerConfig as y };
|