@tenex-chat/backend 0.9.5 → 0.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/daemon-wrapper.cjs +47 -0
- package/dist/index.js +59268 -0
- package/dist/wrapper.js +171 -0
- package/package.json +19 -27
- package/src/agents/AgentRegistry.ts +9 -7
- package/src/agents/AgentStorage.ts +24 -1
- package/src/agents/agent-installer.ts +6 -0
- package/src/agents/agent-loader.ts +7 -2
- package/src/agents/constants.ts +10 -2
- package/src/agents/execution/AgentExecutor.ts +35 -6
- package/src/agents/execution/StreamCallbacks.ts +53 -13
- package/src/agents/execution/StreamExecutionHandler.ts +110 -16
- package/src/agents/execution/StreamSetup.ts +19 -9
- package/src/agents/execution/ToolEventHandlers.ts +112 -0
- package/src/agents/role-categories.ts +53 -0
- package/src/agents/types/runtime.ts +7 -0
- package/src/agents/types/storage.ts +7 -0
- package/src/commands/agent/import/openclaw-distiller.ts +63 -7
- package/src/commands/agent/import/openclaw-reader.ts +54 -0
- package/src/commands/agent/import/openclaw.ts +120 -29
- package/src/commands/agent/index.ts +83 -2
- package/src/commands/setup/display.ts +123 -0
- package/src/commands/setup/embed.ts +13 -13
- package/src/commands/setup/global-system-prompt.ts +15 -17
- package/src/commands/setup/image.ts +17 -20
- package/src/commands/setup/interactive.ts +37 -20
- package/src/commands/setup/llm.ts +12 -7
- package/src/commands/setup/onboarding.ts +1580 -248
- package/src/commands/setup/providers.ts +3 -3
- package/src/conversations/ConversationStore.ts +23 -2
- package/src/conversations/MessageBuilder.ts +51 -73
- package/src/conversations/formatters/utils/conversation-transcript-formatter.ts +425 -0
- package/src/conversations/search/embeddings/ConversationEmbeddingService.ts +40 -98
- package/src/conversations/search/embeddings/ConversationIndexingJob.ts +40 -52
- package/src/conversations/services/ConversationSummarizer.ts +1 -2
- package/src/conversations/types.ts +11 -0
- package/src/daemon/Daemon.ts +78 -57
- package/src/daemon/ProjectRuntime.ts +6 -12
- package/src/daemon/SubscriptionManager.ts +13 -0
- package/src/daemon/index.ts +0 -1
- package/src/event-handler/index.ts +1 -0
- package/src/index.ts +20 -1
- package/src/llm/ChunkHandler.ts +1 -1
- package/src/llm/FinishHandler.ts +28 -4
- package/src/llm/LLMConfigEditor.ts +218 -106
- package/src/llm/index.ts +0 -4
- package/src/llm/meta/MetaModelResolver.ts +3 -18
- package/src/llm/middleware/message-sanitizer.ts +153 -0
- package/src/llm/providers/ollama-models.ts +0 -38
- package/src/llm/service.ts +50 -15
- package/src/llm/types.ts +0 -12
- package/src/llm/utils/ConfigurationManager.ts +88 -465
- package/src/llm/utils/ConfigurationTester.ts +42 -185
- package/src/llm/utils/ModelSelector.ts +156 -92
- package/src/llm/utils/ProviderConfigUI.ts +10 -141
- package/src/llm/utils/models-dev-cache.ts +102 -23
- package/src/llm/utils/provider-select-prompt.ts +284 -0
- package/src/llm/utils/provider-setup.ts +81 -34
- package/src/llm/utils/variant-list-prompt.ts +361 -0
- package/src/nostr/AgentEventDecoder.ts +1 -0
- package/src/nostr/AgentEventEncoder.ts +37 -0
- package/src/nostr/AgentProfilePublisher.ts +13 -0
- package/src/nostr/AgentPublisher.ts +26 -0
- package/src/nostr/kinds.ts +1 -0
- package/src/nostr/ndkClient.ts +4 -1
- package/src/nostr/types.ts +12 -0
- package/src/prompts/fragments/25-rag-instructions.ts +22 -21
- package/src/prompts/fragments/31-agents-md-guidance.ts +7 -21
- package/src/prompts/fragments/index.ts +2 -0
- package/src/prompts/utils/systemPromptBuilder.ts +18 -28
- package/src/services/AgentDefinitionMonitor.ts +8 -0
- package/src/services/ConfigService.ts +34 -0
- package/src/services/PubkeyService.ts +7 -1
- package/src/services/compression/CompressionService.ts +133 -74
- package/src/services/compression/compression-utils.ts +110 -19
- package/src/services/config/types.ts +0 -6
- package/src/services/dispatch/AgentDispatchService.ts +79 -0
- package/src/services/intervention/InterventionService.ts +78 -5
- package/src/services/nip46/Nip46SigningService.ts +30 -1
- package/src/services/projects/ProjectContext.ts +8 -6
- package/src/services/rag/RAGCollectionRegistry.ts +199 -0
- package/src/services/rag/RAGDatabaseService.ts +2 -7
- package/src/services/rag/RAGOperations.ts +25 -45
- package/src/services/rag/RAGService.ts +0 -31
- package/src/services/rag/RagSubscriptionService.ts +71 -122
- package/src/services/rag/rag-utils.ts +13 -0
- package/src/services/ral/RALRegistry.ts +25 -184
- package/src/services/reports/ReportEmbeddingService.ts +63 -113
- package/src/services/search/UnifiedSearchService.ts +115 -4
- package/src/services/search/index.ts +1 -0
- package/src/services/search/projectFilter.ts +20 -4
- package/src/services/search/providers/ConversationSearchProvider.ts +1 -0
- package/src/services/search/providers/GenericCollectionSearchProvider.ts +81 -0
- package/src/services/search/providers/LessonSearchProvider.ts +1 -8
- package/src/services/search/providers/ReportSearchProvider.ts +1 -0
- package/src/services/search/types.ts +24 -3
- package/src/services/trust-pubkeys/SystemPubkeyListService.ts +148 -0
- package/src/services/trust-pubkeys/TrustPubkeyService.ts +70 -9
- package/src/telemetry/setup.ts +2 -13
- package/src/tools/implementations/ask.ts +3 -3
- package/src/tools/implementations/conversation_get.ts +28 -268
- package/src/tools/implementations/fs_grep.ts +6 -6
- package/src/tools/implementations/fs_read.ts +2 -0
- package/src/tools/implementations/fs_write.ts +2 -0
- package/src/tools/implementations/learn.ts +38 -50
- package/src/tools/implementations/rag_add_documents.ts +6 -4
- package/src/tools/implementations/rag_create_collection.ts +37 -4
- package/src/tools/implementations/rag_delete_collection.ts +9 -0
- package/src/tools/implementations/{search.ts → rag_search.ts} +31 -25
- package/src/tools/registry.ts +7 -8
- package/src/tools/types.ts +11 -2
- package/src/tools/utils/transcript-args.ts +13 -0
- package/src/utils/cli-theme.ts +13 -0
- package/src/utils/logger.ts +55 -0
- package/src/utils/metadataKeys.ts +17 -0
- package/src/utils/sqlEscaping.ts +39 -0
- package/src/wrapper.ts +7 -3
- package/dist/src/index.js +0 -46790
- package/dist/tenex-backend-wrapper.cjs +0 -3
- package/src/agents/execution/constants.ts +0 -16
- package/src/agents/execution/index.ts +0 -3
- package/src/agents/index.ts +0 -4
- package/src/commands/agent.ts +0 -235
- package/src/conversations/formatters/DelegationXmlFormatter.ts +0 -64
- package/src/conversations/formatters/index.ts +0 -9
- package/src/conversations/index.ts +0 -2
- package/src/conversations/utils/content-utils.ts +0 -69
- package/src/daemon/UnixSocketTransport.ts +0 -318
- package/src/event-handler/newConversation.ts +0 -165
- package/src/events/NDKProjectStatus.ts +0 -384
- package/src/events/index.ts +0 -4
- package/src/lib/json-parser.ts +0 -30
- package/src/llm/RecordingState.ts +0 -37
- package/src/llm/StreamPublisher.ts +0 -40
- package/src/llm/middleware/flight-recorder.ts +0 -188
- package/src/llm/utils/claudeCodePromptCompiler.ts +0 -141
- package/src/nostr/constants.ts +0 -38
- package/src/prompts/core/index.ts +0 -3
- package/src/prompts/index.ts +0 -21
- package/src/services/image/index.ts +0 -12
- package/src/services/status/index.ts +0 -11
- package/src/telemetry/diagnostics.ts +0 -27
- package/src/tools/implementations/rag_query.ts +0 -107
- package/src/types/index.ts +0 -46
- package/src/utils/agentFetcher.ts +0 -107
- package/src/utils/conversation-utils.ts +0 -1
- package/src/utils/process.ts +0 -49
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Centralized SQL project filter for RAG queries.
|
|
3
3
|
*
|
|
4
|
-
* Shared utility for
|
|
5
|
-
*
|
|
4
|
+
* Shared utility for project-scoped metadata filtering across all RAG collections
|
|
5
|
+
* (reports, conversations, lessons, and generic collections).
|
|
6
6
|
*
|
|
7
7
|
* Applied DURING vector search (prefilter) to ensure proper project isolation.
|
|
8
|
+
*
|
|
9
|
+
* Matches both "projectId" (canonical, used by specialized services) and
|
|
10
|
+
* "project_id" (legacy, used by older rag_add_documents ingestion).
|
|
8
11
|
*/
|
|
9
12
|
|
|
13
|
+
import { PROJECT_ID_KEYS } from "@/utils/metadataKeys";
|
|
14
|
+
import { SQL_LIKE_ESCAPE_CLAUSE, escapeSqlLikeValue } from "@/utils/sqlEscaping";
|
|
15
|
+
|
|
10
16
|
/**
|
|
11
17
|
* Build a SQL prefilter string for project isolation in LanceDB queries.
|
|
12
18
|
*
|
|
19
|
+
* Matches documents where metadata contains EITHER:
|
|
20
|
+
* - "projectId":"<id>" (canonical camelCase, used by specialized services)
|
|
21
|
+
* - "project_id":"<id>" (legacy snake_case, used by older rag_add_documents)
|
|
22
|
+
*
|
|
23
|
+
* Uses proper SQL LIKE escaping so that project IDs containing wildcards
|
|
24
|
+
* (%, _) or quotes don't broaden or break the filter.
|
|
25
|
+
*
|
|
13
26
|
* @param projectId - The project ID to filter by. Pass 'ALL' or undefined to skip filtering.
|
|
14
27
|
* @returns SQL filter string or undefined if no filtering needed.
|
|
15
28
|
*/
|
|
@@ -17,6 +30,9 @@ export function buildProjectFilter(projectId?: string): string | undefined {
|
|
|
17
30
|
if (!projectId || projectId.toLowerCase() === "all") {
|
|
18
31
|
return undefined;
|
|
19
32
|
}
|
|
20
|
-
const
|
|
21
|
-
|
|
33
|
+
const escaped = escapeSqlLikeValue(projectId);
|
|
34
|
+
const clauses = PROJECT_ID_KEYS
|
|
35
|
+
.map((key) => `metadata LIKE '%"${key}":"${escaped}"%' ${SQL_LIKE_ESCAPE_CLAUSE}`)
|
|
36
|
+
.join(" OR ");
|
|
37
|
+
return `(${clauses})`;
|
|
22
38
|
}
|
|
@@ -10,6 +10,7 @@ import type { SearchProvider, SearchResult } from "../types";
|
|
|
10
10
|
|
|
11
11
|
export class ConversationSearchProvider implements SearchProvider {
|
|
12
12
|
readonly name = "conversations";
|
|
13
|
+
readonly collectionName = "conversation_embeddings";
|
|
13
14
|
readonly description = "Past conversation threads and discussions";
|
|
14
15
|
|
|
15
16
|
async search(
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GenericCollectionSearchProvider - Search provider for any RAG collection.
|
|
3
|
+
*
|
|
4
|
+
* Created dynamically for RAG collections that don't have a dedicated
|
|
5
|
+
* specialized provider. Queries via RAGService with basic project-scoped
|
|
6
|
+
* filtering.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { logger } from "@/utils/logger";
|
|
10
|
+
import { RAGService, type RAGQueryResult } from "@/services/rag/RAGService";
|
|
11
|
+
import { buildProjectFilter } from "../projectFilter";
|
|
12
|
+
import type { SearchProvider, SearchResult } from "../types";
|
|
13
|
+
|
|
14
|
+
export class GenericCollectionSearchProvider implements SearchProvider {
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly description: string;
|
|
17
|
+
|
|
18
|
+
/** The actual RAG collection name to query */
|
|
19
|
+
readonly collectionName: string;
|
|
20
|
+
|
|
21
|
+
constructor(collectionName: string) {
|
|
22
|
+
this.collectionName = collectionName;
|
|
23
|
+
this.name = collectionName;
|
|
24
|
+
this.description = `RAG collection: ${collectionName}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async search(
|
|
28
|
+
query: string,
|
|
29
|
+
projectId: string,
|
|
30
|
+
limit: number,
|
|
31
|
+
minScore: number
|
|
32
|
+
): Promise<SearchResult[]> {
|
|
33
|
+
const ragService = RAGService.getInstance();
|
|
34
|
+
|
|
35
|
+
const filter = buildProjectFilter(projectId);
|
|
36
|
+
|
|
37
|
+
const results = await ragService.queryWithFilter(
|
|
38
|
+
this.collectionName,
|
|
39
|
+
query,
|
|
40
|
+
limit * 2, // Request more to account for minScore filtering
|
|
41
|
+
filter
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
logger.debug(`[GenericSearchProvider:${this.collectionName}] Search complete`, {
|
|
45
|
+
query,
|
|
46
|
+
projectId,
|
|
47
|
+
rawResults: results.length,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const filtered = results
|
|
51
|
+
.filter((result: RAGQueryResult) => result.score >= minScore && !!result.document.id)
|
|
52
|
+
.slice(0, limit)
|
|
53
|
+
.map((result: RAGQueryResult) => this.transformResult(result, projectId));
|
|
54
|
+
|
|
55
|
+
if (filtered.length < results.length) {
|
|
56
|
+
const dropped = results.length - filtered.length;
|
|
57
|
+
logger.debug(`[GenericSearchProvider:${this.collectionName}] Dropped ${dropped} result(s) (low score or missing ID)`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return filtered;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private transformResult(result: RAGQueryResult, fallbackProjectId: string): SearchResult {
|
|
64
|
+
const metadata = result.document.metadata || {};
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
source: this.collectionName,
|
|
68
|
+
id: result.document.id || "",
|
|
69
|
+
projectId: String(metadata.projectId || fallbackProjectId),
|
|
70
|
+
relevanceScore: result.score,
|
|
71
|
+
title: String(metadata.title || result.document.id || ""),
|
|
72
|
+
summary: result.document.content?.substring(0, 200) || "",
|
|
73
|
+
createdAt: metadata.timestamp ? Number(metadata.timestamp) : undefined,
|
|
74
|
+
author: metadata.agentPubkey ? String(metadata.agentPubkey) : undefined,
|
|
75
|
+
authorName: metadata.agentName ? String(metadata.agentName) : undefined,
|
|
76
|
+
tags: Array.isArray(metadata.hashtags) ? (metadata.hashtags as string[]) : undefined,
|
|
77
|
+
retrievalTool: "rag_search" as const,
|
|
78
|
+
retrievalArg: result.document.id || "",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -16,6 +16,7 @@ const LESSONS_COLLECTION = "lessons";
|
|
|
16
16
|
|
|
17
17
|
export class LessonSearchProvider implements SearchProvider {
|
|
18
18
|
readonly name = "lessons";
|
|
19
|
+
readonly collectionName = "lessons";
|
|
19
20
|
readonly description = "Agent lessons and insights";
|
|
20
21
|
|
|
21
22
|
async search(
|
|
@@ -25,14 +26,6 @@ export class LessonSearchProvider implements SearchProvider {
|
|
|
25
26
|
minScore: number
|
|
26
27
|
): Promise<SearchResult[]> {
|
|
27
28
|
const ragService = RAGService.getInstance();
|
|
28
|
-
|
|
29
|
-
// Check if the lessons collection exists
|
|
30
|
-
const collections = await ragService.listCollections();
|
|
31
|
-
if (!collections.includes(LESSONS_COLLECTION)) {
|
|
32
|
-
logger.debug("[LessonSearchProvider] Lessons collection does not exist");
|
|
33
|
-
return [];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
29
|
const filter = buildProjectFilter(projectId);
|
|
37
30
|
|
|
38
31
|
const results = await ragService.queryWithFilter(
|
|
@@ -10,6 +10,7 @@ import type { SearchProvider, SearchResult } from "../types";
|
|
|
10
10
|
|
|
11
11
|
export class ReportSearchProvider implements SearchProvider {
|
|
12
12
|
readonly name = "reports";
|
|
13
|
+
readonly collectionName = "project_reports";
|
|
13
14
|
readonly description = "Project reports and documentation";
|
|
14
15
|
|
|
15
16
|
async search(
|
|
@@ -51,7 +51,7 @@ export interface SearchResult {
|
|
|
51
51
|
tags?: string[];
|
|
52
52
|
|
|
53
53
|
/** Which tool to use to retrieve full content */
|
|
54
|
-
retrievalTool: "report_read" | "lesson_get" | "conversation_get";
|
|
54
|
+
retrievalTool: "report_read" | "lesson_get" | "conversation_get" | "rag_search";
|
|
55
55
|
|
|
56
56
|
/** The argument to pass to the retrieval tool */
|
|
57
57
|
retrievalArg: string;
|
|
@@ -79,8 +79,21 @@ export interface SearchOptions {
|
|
|
79
79
|
*/
|
|
80
80
|
prompt?: string;
|
|
81
81
|
|
|
82
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* Filter by **provider name** (defaults to all).
|
|
84
|
+
* Well-known provider names: "reports", "conversations", "lessons".
|
|
85
|
+
* Dynamically discovered RAG collections use their collection name as provider name
|
|
86
|
+
* (e.g., "custom_knowledge").
|
|
87
|
+
*
|
|
88
|
+
* When provided, searches exactly those collections (no scope filtering).
|
|
89
|
+
*/
|
|
83
90
|
collections?: string[];
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Agent pubkey for scope-aware collection filtering.
|
|
94
|
+
* Used to include `personal` collections belonging to this agent.
|
|
95
|
+
*/
|
|
96
|
+
agentPubkey?: string;
|
|
84
97
|
}
|
|
85
98
|
|
|
86
99
|
/**
|
|
@@ -88,12 +101,20 @@ export interface SearchOptions {
|
|
|
88
101
|
* Each provider wraps a specific RAG collection.
|
|
89
102
|
*/
|
|
90
103
|
export interface SearchProvider {
|
|
91
|
-
/** Unique name for this provider (
|
|
104
|
+
/** Unique name for this provider (used for filtering via `collections` parameter) */
|
|
92
105
|
readonly name: string;
|
|
93
106
|
|
|
94
107
|
/** Human-readable description */
|
|
95
108
|
readonly description: string;
|
|
96
109
|
|
|
110
|
+
/**
|
|
111
|
+
* The underlying RAG collection name this provider covers.
|
|
112
|
+
* Used to prevent duplicate generic providers for collections that already
|
|
113
|
+
* have a specialized provider. When undefined, the provider is not
|
|
114
|
+
* associated with a specific RAG collection (or uses its `name` directly).
|
|
115
|
+
*/
|
|
116
|
+
readonly collectionName?: string;
|
|
117
|
+
|
|
97
118
|
/**
|
|
98
119
|
* Perform semantic search within this provider's collection.
|
|
99
120
|
*
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { agentStorage } from "@/agents/AgentStorage";
|
|
4
|
+
import { ensureDirectory, readFile } from "@/lib/fs";
|
|
5
|
+
import { config } from "@/services/ConfigService";
|
|
6
|
+
import { logger } from "@/utils/logger";
|
|
7
|
+
|
|
8
|
+
export interface SyncSystemPubkeyListOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Extra pubkeys to force-include in the output file.
|
|
11
|
+
* Useful for "about to publish" contexts where the pubkey must be present
|
|
12
|
+
* even if other registries haven't synced yet.
|
|
13
|
+
*/
|
|
14
|
+
additionalPubkeys?: Iterable<string>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Maintains `$TENEX_BASE_DIR/daemon/whitelist.txt` as the canonical list of
|
|
19
|
+
* pubkeys known to belong to this TENEX system.
|
|
20
|
+
*
|
|
21
|
+
* The file contains one pubkey per line and is rebuilt from:
|
|
22
|
+
* - daemon/user whitelist from config
|
|
23
|
+
* - backend pubkey
|
|
24
|
+
* - all known agent pubkeys from storage
|
|
25
|
+
* - optional call-site additions (e.g., the pubkey being published right now)
|
|
26
|
+
*/
|
|
27
|
+
export class SystemPubkeyListService {
|
|
28
|
+
private static instance: SystemPubkeyListService;
|
|
29
|
+
|
|
30
|
+
static getInstance(): SystemPubkeyListService {
|
|
31
|
+
if (!SystemPubkeyListService.instance) {
|
|
32
|
+
SystemPubkeyListService.instance = new SystemPubkeyListService();
|
|
33
|
+
}
|
|
34
|
+
return SystemPubkeyListService.instance;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Rebuild and persist daemon whitelist file.
|
|
39
|
+
* Idempotent: skips writes when content is unchanged.
|
|
40
|
+
*/
|
|
41
|
+
async syncWhitelistFile(options: SyncSystemPubkeyListOptions = {}): Promise<void> {
|
|
42
|
+
const daemonDir = config.getConfigPath("daemon");
|
|
43
|
+
const whitelistPath = path.join(daemonDir, "whitelist.txt");
|
|
44
|
+
const pubkeys = await this.collectKnownSystemPubkeys(options.additionalPubkeys);
|
|
45
|
+
const content = this.serialize(pubkeys);
|
|
46
|
+
|
|
47
|
+
await ensureDirectory(daemonDir);
|
|
48
|
+
|
|
49
|
+
const existing = await this.safeReadFile(whitelistPath);
|
|
50
|
+
if (existing === content) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await this.atomicWrite(whitelistPath, content);
|
|
55
|
+
logger.debug("[SYSTEM_PUBKEY_LIST] Updated daemon whitelist.txt", {
|
|
56
|
+
path: whitelistPath,
|
|
57
|
+
pubkeyCount: pubkeys.length,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private async collectKnownSystemPubkeys(additionalPubkeys?: Iterable<string>): Promise<string[]> {
|
|
62
|
+
const pubkeys = new Set<string>();
|
|
63
|
+
|
|
64
|
+
// Whitelisted daemon pubkeys from loaded config
|
|
65
|
+
try {
|
|
66
|
+
const loadedConfig = config.getConfig();
|
|
67
|
+
const whitelisted = config.getWhitelistedPubkeys(undefined, loadedConfig);
|
|
68
|
+
for (const pubkey of whitelisted) {
|
|
69
|
+
this.addPubkey(pubkeys, pubkey);
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
logger.debug("[SYSTEM_PUBKEY_LIST] Failed to load whitelisted pubkeys", {
|
|
73
|
+
error: error instanceof Error ? error.message : String(error),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// TENEX backend pubkey
|
|
78
|
+
try {
|
|
79
|
+
const backendSigner = await config.getBackendSigner();
|
|
80
|
+
this.addPubkey(pubkeys, backendSigner.pubkey);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
logger.debug("[SYSTEM_PUBKEY_LIST] Failed to load backend pubkey", {
|
|
83
|
+
error: error instanceof Error ? error.message : String(error),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// All known agents (across projects) from storage
|
|
88
|
+
try {
|
|
89
|
+
const knownAgentPubkeys = await agentStorage.getAllKnownPubkeys();
|
|
90
|
+
for (const pubkey of knownAgentPubkeys) {
|
|
91
|
+
this.addPubkey(pubkeys, pubkey);
|
|
92
|
+
}
|
|
93
|
+
} catch (error) {
|
|
94
|
+
logger.debug("[SYSTEM_PUBKEY_LIST] Failed to load known agent pubkeys", {
|
|
95
|
+
error: error instanceof Error ? error.message : String(error),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Call-site additions (e.g., pubkey being published right now)
|
|
100
|
+
if (additionalPubkeys) {
|
|
101
|
+
for (const pubkey of additionalPubkeys) {
|
|
102
|
+
this.addPubkey(pubkeys, pubkey);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return Array.from(pubkeys).sort();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private addPubkey(pubkeys: Set<string>, candidate: string | undefined): void {
|
|
110
|
+
if (!candidate) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const trimmed = candidate.trim();
|
|
115
|
+
if (!trimmed) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
pubkeys.add(trimmed);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private serialize(pubkeys: string[]): string {
|
|
123
|
+
if (pubkeys.length === 0) {
|
|
124
|
+
return "";
|
|
125
|
+
}
|
|
126
|
+
return `${pubkeys.join("\n")}\n`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private async safeReadFile(filePath: string): Promise<string | null> {
|
|
130
|
+
try {
|
|
131
|
+
return await readFile(filePath, "utf-8");
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private async atomicWrite(filePath: string, content: string): Promise<void> {
|
|
141
|
+
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
142
|
+
await fs.writeFile(tempPath, content, "utf-8");
|
|
143
|
+
await fs.rename(tempPath, filePath);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export const getSystemPubkeyListService = (): SystemPubkeyListService =>
|
|
148
|
+
SystemPubkeyListService.getInstance();
|
|
@@ -24,9 +24,17 @@ export interface TrustResult {
|
|
|
24
24
|
* A pubkey is trusted if it is:
|
|
25
25
|
* - In the whitelisted pubkeys from config
|
|
26
26
|
* - The backend's own pubkey
|
|
27
|
-
* - An agent in the system (registered in ProjectContext)
|
|
27
|
+
* - An agent in the system (registered in ProjectContext or globally across all projects)
|
|
28
28
|
*
|
|
29
29
|
* Trust precedence (highest to lowest): whitelisted > backend > agent
|
|
30
|
+
*
|
|
31
|
+
* ## Agent Trust: Two-Tier Lookup + Daemon Seeding
|
|
32
|
+
* 1. **Project context** (sync): Check the current project's agent registry (fast, scoped)
|
|
33
|
+
* 2. **Global agent set** (sync): Check daemon-level set of all agent pubkeys across all projects
|
|
34
|
+
*
|
|
35
|
+
* The global agent set is seeded by the Daemon at startup from AgentStorage (covering
|
|
36
|
+
* not-yet-running projects) and kept in sync as projects start/stop. Each sync unions
|
|
37
|
+
* the active runtime pubkeys with the stored seed, so trust is never dropped.
|
|
30
38
|
*/
|
|
31
39
|
export class TrustPubkeyService {
|
|
32
40
|
private static instance: TrustPubkeyService;
|
|
@@ -37,6 +45,13 @@ export class TrustPubkeyService {
|
|
|
37
45
|
/** Cached whitelist Set for O(1) lookups */
|
|
38
46
|
private cachedWhitelistSet?: Set<Hexpubkey>;
|
|
39
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Global set of agent pubkeys across ALL projects (running and discovered).
|
|
50
|
+
* Pushed by the Daemon whenever projects start/stop or agents are added.
|
|
51
|
+
* Frozen for safe concurrent reads.
|
|
52
|
+
*/
|
|
53
|
+
private globalAgentPubkeys: ReadonlySet<Hexpubkey> = Object.freeze(new Set<Hexpubkey>());
|
|
54
|
+
|
|
40
55
|
private constructor() {}
|
|
41
56
|
|
|
42
57
|
/**
|
|
@@ -165,6 +180,20 @@ export class TrustPubkeyService {
|
|
|
165
180
|
// Note: getBackendPubkey already logs debug message on failure
|
|
166
181
|
}
|
|
167
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Set the global agent pubkeys set (daemon-level, cross-project).
|
|
185
|
+
* Called by the Daemon when projects start/stop or agents are dynamically added.
|
|
186
|
+
* The set is frozen for safe concurrent reads.
|
|
187
|
+
*
|
|
188
|
+
* @param pubkeys Set of all known agent pubkeys across all projects
|
|
189
|
+
*/
|
|
190
|
+
setGlobalAgentPubkeys(pubkeys: Set<Hexpubkey>): void {
|
|
191
|
+
this.globalAgentPubkeys = Object.freeze(new Set(pubkeys));
|
|
192
|
+
logger.debug("[TRUST_PUBKEY] Global agent pubkeys updated", {
|
|
193
|
+
count: pubkeys.size,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
168
197
|
/**
|
|
169
198
|
* Get all currently trusted pubkeys.
|
|
170
199
|
* Useful for debugging or displaying trust status.
|
|
@@ -196,6 +225,7 @@ export class TrustPubkeyService {
|
|
|
196
225
|
}
|
|
197
226
|
|
|
198
227
|
// 3. Add agent pubkeys (lowest priority)
|
|
228
|
+
// 3a. Current project context agents
|
|
199
229
|
const projectCtx = projectContextStore.getContext();
|
|
200
230
|
if (projectCtx) {
|
|
201
231
|
for (const [_slug, agent] of projectCtx.agents) {
|
|
@@ -205,6 +235,13 @@ export class TrustPubkeyService {
|
|
|
205
235
|
}
|
|
206
236
|
}
|
|
207
237
|
|
|
238
|
+
// 3b. Global agent pubkeys (cross-project)
|
|
239
|
+
for (const pubkey of this.globalAgentPubkeys) {
|
|
240
|
+
if (!trustedMap.has(pubkey)) {
|
|
241
|
+
trustedMap.set(pubkey, "agent");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
208
245
|
// Convert map to array
|
|
209
246
|
return Array.from(trustedMap.entries()).map(([pubkey, reason]) => ({
|
|
210
247
|
pubkey,
|
|
@@ -296,25 +333,49 @@ export class TrustPubkeyService {
|
|
|
296
333
|
}
|
|
297
334
|
|
|
298
335
|
/**
|
|
299
|
-
* Check if pubkey belongs to an agent in the system.
|
|
300
|
-
*
|
|
301
|
-
*
|
|
336
|
+
* Check if pubkey belongs to an agent in the system (synchronous, two-tier).
|
|
337
|
+
*
|
|
338
|
+
* Tier 1: Current project context (fast, scoped to the active project)
|
|
339
|
+
* Tier 2: Global agent pubkeys set (daemon-level, covers all projects including non-running)
|
|
340
|
+
*
|
|
341
|
+
* The global set is maintained by the Daemon via setGlobalAgentPubkeys(),
|
|
342
|
+
* which unions active runtime pubkeys with the AgentStorage seed.
|
|
302
343
|
*/
|
|
303
344
|
private isAgentPubkey(pubkey: Hexpubkey): boolean {
|
|
345
|
+
// Tier 1: Current project context (existing fast path)
|
|
304
346
|
const projectCtx = projectContextStore.getContext();
|
|
305
|
-
if (
|
|
306
|
-
return
|
|
347
|
+
if (projectCtx?.getAgentByPubkey(pubkey) !== undefined) {
|
|
348
|
+
return true;
|
|
307
349
|
}
|
|
308
|
-
|
|
350
|
+
|
|
351
|
+
// Tier 2: Global agent pubkeys (daemon-level, cross-project)
|
|
352
|
+
if (this.globalAgentPubkeys.has(pubkey)) {
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return false;
|
|
309
357
|
}
|
|
310
358
|
|
|
311
359
|
/**
|
|
312
|
-
* Clear
|
|
360
|
+
* Clear config-derived caches (useful for testing and config reloads).
|
|
361
|
+
* Does NOT clear globalAgentPubkeys — that state is managed by the Daemon
|
|
362
|
+
* via setGlobalAgentPubkeys() and is not derived from config.
|
|
313
363
|
*/
|
|
314
364
|
clearCache(): void {
|
|
315
365
|
this.cachedBackendPubkey = undefined;
|
|
316
366
|
this.cachedWhitelistSet = undefined;
|
|
317
|
-
logger.debug("[TRUST_PUBKEY]
|
|
367
|
+
logger.debug("[TRUST_PUBKEY] Config cache cleared");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Reset all state including daemon-managed global agent pubkeys.
|
|
372
|
+
* Use only in tests to fully reset the service.
|
|
373
|
+
*/
|
|
374
|
+
resetAll(): void {
|
|
375
|
+
this.cachedBackendPubkey = undefined;
|
|
376
|
+
this.cachedWhitelistSet = undefined;
|
|
377
|
+
this.globalAgentPubkeys = Object.freeze(new Set<Hexpubkey>());
|
|
378
|
+
logger.debug("[TRUST_PUBKEY] Full state reset");
|
|
318
379
|
}
|
|
319
380
|
}
|
|
320
381
|
|
package/src/telemetry/setup.ts
CHANGED
|
@@ -17,10 +17,7 @@ const DEFAULT_ENDPOINT = "http://localhost:4318/v1/traces";
|
|
|
17
17
|
class ErrorHandlingExporterWrapper implements SpanExporter {
|
|
18
18
|
private disabled = false;
|
|
19
19
|
|
|
20
|
-
constructor(
|
|
21
|
-
private traceExporter: OTLPTraceExporter,
|
|
22
|
-
private exporterUrl: string
|
|
23
|
-
) {}
|
|
20
|
+
constructor(private traceExporter: OTLPTraceExporter) {}
|
|
24
21
|
|
|
25
22
|
export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {
|
|
26
23
|
// Once disabled, drop all spans silently
|
|
@@ -31,14 +28,6 @@ class ErrorHandlingExporterWrapper implements SpanExporter {
|
|
|
31
28
|
|
|
32
29
|
this.traceExporter.export(spans, (result) => {
|
|
33
30
|
if (result.error && !this.disabled) {
|
|
34
|
-
const errorMessage = result.error?.message || String(result.error);
|
|
35
|
-
const isConnectionError = errorMessage.includes("ECONNREFUSED") || errorMessage.includes("connect");
|
|
36
|
-
if (isConnectionError) {
|
|
37
|
-
console.warn(`[Telemetry] ⚠️ Collector not available at ${this.exporterUrl}`);
|
|
38
|
-
} else {
|
|
39
|
-
console.error("[Telemetry] Export error:", errorMessage);
|
|
40
|
-
}
|
|
41
|
-
console.warn("[Telemetry] Disabling trace export");
|
|
42
31
|
this.disabled = true;
|
|
43
32
|
}
|
|
44
33
|
resultCallback(result);
|
|
@@ -106,7 +95,7 @@ export function initializeTelemetry(
|
|
|
106
95
|
});
|
|
107
96
|
|
|
108
97
|
// Wrap the exporter with error handling
|
|
109
|
-
const wrappedExporter = new ErrorHandlingExporterWrapper(traceExporter
|
|
98
|
+
const wrappedExporter = new ErrorHandlingExporterWrapper(traceExporter);
|
|
110
99
|
|
|
111
100
|
sdk = createSDK(serviceName, wrappedExporter);
|
|
112
101
|
sdk.start();
|
|
@@ -12,7 +12,6 @@ import { z } from "zod";
|
|
|
12
12
|
import { RALRegistry } from "@/services/ral";
|
|
13
13
|
import type { PendingDelegation } from "@/services/ral/types";
|
|
14
14
|
import { APNsService } from "@/services/apns";
|
|
15
|
-
import { streamPublisher } from "@/llm";
|
|
16
15
|
|
|
17
16
|
/**
|
|
18
17
|
* Schema for a single-select question.
|
|
@@ -303,10 +302,11 @@ async function executeAsk(input: AskInput, context: ToolExecutionContext): Promi
|
|
|
303
302
|
});
|
|
304
303
|
}
|
|
305
304
|
|
|
306
|
-
// Send APNs push notification if
|
|
305
|
+
// Send APNs push notification if APNs is enabled.
|
|
306
|
+
// Unix socket presence detection was removed with local socket streaming.
|
|
307
307
|
try {
|
|
308
308
|
const apnsService = APNsService.getInstance();
|
|
309
|
-
if (apnsService.isEnabled()
|
|
309
|
+
if (apnsService.isEnabled()) {
|
|
310
310
|
const bodyPreview = askContext.length > 100 ? askContext.substring(0, 100) + "…" : askContext;
|
|
311
311
|
await apnsService.notifyIfNeeded(ownerPubkey, {
|
|
312
312
|
title: "Agent needs your input",
|