@hyperdrive.bot/fleet-server 0.3.148 → 0.3.150
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/server/server/agent/tools/paseo-tools.d.ts +9 -0
- package/dist/server/server/agent/tools/paseo-tools.js +76 -1
- package/dist/server/server/agent/tools/read-only-surface.d.ts +7 -0
- package/dist/server/server/agent/tools/read-only-surface.js +8 -0
- package/dist/server/server/bootstrap.js +71 -1
- package/dist/server/server/config.js +4 -0
- package/dist/server/server/exports.d.ts +2 -0
- package/dist/server/server/exports.js +6 -0
- package/dist/server/server/ingestion/adapters.js +2 -0
- package/dist/server/server/ingestion/errors.d.ts +5 -1
- package/dist/server/server/ingestion/errors.js +8 -0
- package/dist/server/server/ingestion/health.js +10 -0
- package/dist/server/server/ingestion/self-hosted/gateway.d.ts +55 -0
- package/dist/server/server/ingestion/self-hosted/gateway.js +295 -0
- package/dist/server/server/ingestion/subscriptions/notification-prompt.d.ts +83 -0
- package/dist/server/server/ingestion/subscriptions/notification-prompt.js +96 -0
- package/dist/server/server/ingestion/subscriptions/notifier.d.ts +96 -0
- package/dist/server/server/ingestion/subscriptions/notifier.js +209 -0
- package/dist/server/server/ingestion/subscriptions/pending-store.d.ts +111 -0
- package/dist/server/server/ingestion/subscriptions/pending-store.js +254 -0
- package/dist/server/server/ingestion/subscriptions/poller.d.ts +73 -0
- package/dist/server/server/ingestion/subscriptions/poller.js +168 -0
- package/dist/server/server/ingestion/subscriptions/reader.d.ts +39 -0
- package/dist/server/server/ingestion/subscriptions/reader.js +30 -0
- package/dist/server/server/ingestion/subscriptions/store.d.ts +35 -0
- package/dist/server/server/ingestion/subscriptions/store.js +82 -0
- package/dist/server/server/ingestion/types.d.ts +14 -1
- package/dist/server/server/ingestion/types.js +1 -1
- package/dist/server/web-ui/_expo/static/js/web/{index-88d5f130a09403b86ae8c8c5fd1f97d4.js → index-4695e64c38d7ce76a8375d72b95c2c0c.js} +6 -6
- package/dist/server/web-ui/_expo/static/js/web/index-4695e64c38d7ce76a8375d72b95c2c0c.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-4695e64c38d7ce76a8375d72b95c2c0c.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-88d5f130a09403b86ae8c8c5fd1f97d4.js.map.br → index-4695e64c38d7ce76a8375d72b95c2c0c.js.map.br} +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-88d5f130a09403b86ae8c8c5fd1f97d4.js.map.gz → index-4695e64c38d7ce76a8375d72b95c2c0c.js.map.gz} +0 -0
- package/dist/server/web-ui/index.html +1 -1
- package/dist/server/web-ui/index.html.br +0 -0
- package/dist/server/web-ui/index.html.gz +0 -0
- package/package.json +6 -6
- package/dist/server/web-ui/_expo/static/js/web/index-88d5f130a09403b86ae8c8c5fd1f97d4.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-88d5f130a09403b86ae8c8c5fd1f97d4.js.gz +0 -0
|
@@ -8,6 +8,7 @@ import type { TerminalManager } from "../../../terminal/terminal-manager.js";
|
|
|
8
8
|
import type { CreatePaseoWorktreeWorkflowFn } from "../../worktree-session.js";
|
|
9
9
|
import type { JudgeRelayGate } from "../judge-relay-gate.js";
|
|
10
10
|
import type { ScheduleService } from "../../schedule/service.js";
|
|
11
|
+
import type { SubscriptionReader } from "../../ingestion/subscriptions/reader.js";
|
|
11
12
|
import type { ProviderSnapshotManager } from "../provider-snapshot-manager.js";
|
|
12
13
|
import type { GitHubService } from "../../../services/github-service.js";
|
|
13
14
|
import type { WorkspaceGitService } from "../../workspace-git-service.js";
|
|
@@ -20,6 +21,14 @@ export interface PaseoToolHostDependencies {
|
|
|
20
21
|
terminalManager?: TerminalManager | null;
|
|
21
22
|
getDaemonTcpPort?: () => number | null;
|
|
22
23
|
scheduleService?: ScheduleService | null;
|
|
24
|
+
/**
|
|
25
|
+
* Read access to session subscriptions and the items they have announced.
|
|
26
|
+
*
|
|
27
|
+
* Supplied as two narrow stores rather than a service: these tools only ever
|
|
28
|
+
* READ, and handing the catalog something that could arm or notify would put
|
|
29
|
+
* the delivery policy inside a model's reach.
|
|
30
|
+
*/
|
|
31
|
+
subscriptionReader?: SubscriptionReader | null;
|
|
23
32
|
providerSnapshotManager: ProviderSnapshotManager;
|
|
24
33
|
github?: GitHubService;
|
|
25
34
|
workspaceGitService?: Pick<WorkspaceGitService, "getSnapshot" | "listWorktrees" | "resolveRepoRoot">;
|
|
@@ -257,7 +257,7 @@ function resolveTerminalKeyToken(key, literal) {
|
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
export function createPaseoToolCatalog(options) {
|
|
260
|
-
const { agentManager, agentStorage, terminalManager, scheduleService, providerSnapshotManager, callerAgentId, resolveSpeakHandler, resolveCallerContext, logger, } = options;
|
|
260
|
+
const { agentManager, agentStorage, terminalManager, scheduleService, subscriptionReader, providerSnapshotManager, callerAgentId, resolveSpeakHandler, resolveCallerContext, logger, } = options;
|
|
261
261
|
const childLogger = logger.child({ module: "agent", component: "paseo-tool-catalog" });
|
|
262
262
|
const waitTracker = new WaitForAgentTracker(logger);
|
|
263
263
|
const callerContext = callerAgentId ? (resolveCallerContext?.(callerAgentId) ?? null) : null;
|
|
@@ -2060,6 +2060,81 @@ export function createPaseoToolCatalog(options) {
|
|
|
2060
2060
|
structuredContent: ensureValidJson(schedule),
|
|
2061
2061
|
};
|
|
2062
2062
|
});
|
|
2063
|
+
registerTool("list_subscriptions", {
|
|
2064
|
+
title: "List subscriptions",
|
|
2065
|
+
description: "List this daemon's source subscriptions: which filter each one listens to, which agent it notifies, and when it last did.",
|
|
2066
|
+
inputSchema: {},
|
|
2067
|
+
outputSchema: {
|
|
2068
|
+
subscriptions: z.array(z.object({
|
|
2069
|
+
id: z.string(),
|
|
2070
|
+
filterId: z.string(),
|
|
2071
|
+
agentId: z.string(),
|
|
2072
|
+
label: z.string().nullable(),
|
|
2073
|
+
status: z.string(),
|
|
2074
|
+
debounceMs: z.number(),
|
|
2075
|
+
lastNotifiedAt: z.string().nullable(),
|
|
2076
|
+
})),
|
|
2077
|
+
},
|
|
2078
|
+
}, async () => {
|
|
2079
|
+
if (!subscriptionReader) {
|
|
2080
|
+
throw new Error("Subscriptions are not configured on this daemon");
|
|
2081
|
+
}
|
|
2082
|
+
const subscriptions = (await subscriptionReader.list(callerAgentId)).map((subscription) => ({
|
|
2083
|
+
id: subscription.id,
|
|
2084
|
+
filterId: subscription.filterId,
|
|
2085
|
+
agentId: subscription.agentId,
|
|
2086
|
+
label: subscription.label,
|
|
2087
|
+
status: subscription.status,
|
|
2088
|
+
debounceMs: subscription.debounceMs,
|
|
2089
|
+
lastNotifiedAt: subscription.lastNotifiedAt,
|
|
2090
|
+
}));
|
|
2091
|
+
return { content: [], structuredContent: ensureValidJson({ subscriptions }) };
|
|
2092
|
+
});
|
|
2093
|
+
registerTool("get_subscription_items", {
|
|
2094
|
+
title: "Get subscription items",
|
|
2095
|
+
description: "Full content of items a subscription notification announced. Pass the subscription id and the item keys from the notice. Keys that are no longer held come back absent rather than as an error.",
|
|
2096
|
+
inputSchema: {
|
|
2097
|
+
subscriptionId: z.string().min(1),
|
|
2098
|
+
itemKeys: z.array(z.string().min(1)).min(1).max(50),
|
|
2099
|
+
},
|
|
2100
|
+
outputSchema: {
|
|
2101
|
+
items: z.array(z.object({
|
|
2102
|
+
itemKey: z.string(),
|
|
2103
|
+
contentHash: z.string(),
|
|
2104
|
+
title: z.string(),
|
|
2105
|
+
subtitle: z.string(),
|
|
2106
|
+
timestampMs: z.number(),
|
|
2107
|
+
notifiedAt: z.string().nullable(),
|
|
2108
|
+
payload: z.unknown(),
|
|
2109
|
+
})),
|
|
2110
|
+
missingKeys: z.array(z.string()),
|
|
2111
|
+
},
|
|
2112
|
+
}, async (input) => {
|
|
2113
|
+
if (!subscriptionReader) {
|
|
2114
|
+
throw new Error("Subscriptions are not configured on this daemon");
|
|
2115
|
+
}
|
|
2116
|
+
const items = await subscriptionReader.resolveItems(input.subscriptionId, input.itemKeys, callerAgentId);
|
|
2117
|
+
const found = new Set(items.map((item) => item.itemKey));
|
|
2118
|
+
// Named, not silent. A key that is gone is an ordinary consequence of the
|
|
2119
|
+
// TTL sweep, and saying which ones went is what stops a caller reading a
|
|
2120
|
+
// short list as the whole answer.
|
|
2121
|
+
const missingKeys = input.itemKeys.filter((key) => !found.has(key));
|
|
2122
|
+
return {
|
|
2123
|
+
content: [],
|
|
2124
|
+
structuredContent: ensureValidJson({
|
|
2125
|
+
items: items.map((item) => ({
|
|
2126
|
+
itemKey: item.itemKey,
|
|
2127
|
+
contentHash: item.contentHash,
|
|
2128
|
+
title: item.title,
|
|
2129
|
+
subtitle: item.subtitle,
|
|
2130
|
+
timestampMs: item.timestampMs,
|
|
2131
|
+
notifiedAt: item.notifiedAt,
|
|
2132
|
+
payload: item.payload,
|
|
2133
|
+
})),
|
|
2134
|
+
missingKeys,
|
|
2135
|
+
}),
|
|
2136
|
+
};
|
|
2137
|
+
});
|
|
2063
2138
|
registerTool("list_schedules", {
|
|
2064
2139
|
title: "List schedules",
|
|
2065
2140
|
description: "List all schedules managed by the daemon.",
|
|
@@ -45,6 +45,13 @@
|
|
|
45
45
|
* - `run_schedule_once` fires a schedule now, which starts or prompts an agent.
|
|
46
46
|
* - `rename_workspace` mutates.
|
|
47
47
|
* - `speak` an outward side effect, and voice is out of v1 scope.
|
|
48
|
+
* - `get_subscription_items` a FETCHER, not a passive read: it pulls fresh
|
|
49
|
+
* untrusted bytes (raw aggregator event bodies) into
|
|
50
|
+
* the judge's context on demand, widening the very
|
|
51
|
+
* input surface this allowlist bounds. The judge is
|
|
52
|
+
* also the one identity holding an outbound relay
|
|
53
|
+
* channel, and read-only stops mutation, not
|
|
54
|
+
* exfiltration through a channel granted on purpose.
|
|
48
55
|
* - `wait_for_agent` not mutating, but it BLOCKS. An event-woken judge
|
|
49
56
|
* that blocks holds its run open indefinitely.
|
|
50
57
|
*/
|
|
@@ -45,6 +45,13 @@
|
|
|
45
45
|
* - `run_schedule_once` fires a schedule now, which starts or prompts an agent.
|
|
46
46
|
* - `rename_workspace` mutates.
|
|
47
47
|
* - `speak` an outward side effect, and voice is out of v1 scope.
|
|
48
|
+
* - `get_subscription_items` a FETCHER, not a passive read: it pulls fresh
|
|
49
|
+
* untrusted bytes (raw aggregator event bodies) into
|
|
50
|
+
* the judge's context on demand, widening the very
|
|
51
|
+
* input surface this allowlist bounds. The judge is
|
|
52
|
+
* also the one identity holding an outbound relay
|
|
53
|
+
* channel, and read-only stops mutation, not
|
|
54
|
+
* exfiltration through a channel granted on purpose.
|
|
48
55
|
* - `wait_for_agent` not mutating, but it BLOCKS. An event-woken judge
|
|
49
56
|
* that blocks holds its run open indefinitely.
|
|
50
57
|
*/
|
|
@@ -59,6 +66,7 @@ export const READ_ONLY_PASEO_TOOLS = new Set([
|
|
|
59
66
|
"list_pending_permissions",
|
|
60
67
|
"list_providers",
|
|
61
68
|
"list_schedules",
|
|
69
|
+
"list_subscriptions",
|
|
62
70
|
"list_terminals",
|
|
63
71
|
"list_worktrees",
|
|
64
72
|
"schedule_logs",
|
|
@@ -119,6 +119,12 @@ import { ScheduleService } from "./schedule/service.js";
|
|
|
119
119
|
import { IngestionService } from "./ingestion/service.js";
|
|
120
120
|
import { FilterStore, filtersDir } from "./ingestion/filters/store.js";
|
|
121
121
|
import { SourceStore } from "./ingestion/sources/store.js";
|
|
122
|
+
import { formatSystemNotificationPrompt, sendPromptToAgent } from "./agent/agent-prompt.js";
|
|
123
|
+
import { SubscriptionStore } from "./ingestion/subscriptions/store.js";
|
|
124
|
+
import { PendingItemStore } from "./ingestion/subscriptions/pending-store.js";
|
|
125
|
+
import { SubscriptionNotifier } from "./ingestion/subscriptions/notifier.js";
|
|
126
|
+
import { SubscriptionPoller } from "./ingestion/subscriptions/poller.js";
|
|
127
|
+
import { createSubscriptionReader } from "./ingestion/subscriptions/reader.js";
|
|
122
128
|
import { resolveSourceGateway } from "./ingestion/adapters.js";
|
|
123
129
|
import { CapLimiter } from "./ingestion/cap-limiter.js";
|
|
124
130
|
import { IngestionLedger } from "./ingestion/ledger.js";
|
|
@@ -443,6 +449,11 @@ function buildIngestionCollaborators(config, ingestionLedger) {
|
|
|
443
449
|
const ingestionFilterStore = new FilterStore(filtersDir(config.paseoHome));
|
|
444
450
|
const ingestionSourceStore = new SourceStore(path.join(config.paseoHome, "sources"));
|
|
445
451
|
const ingestionGatewayResolver = resolveIngestionGatewayResolver(config, ingestionConfig);
|
|
452
|
+
// Session subscriptions. Declared beside the other ingestion stores so the
|
|
453
|
+
// schedule service below can be handed a live filter resolver rather than the
|
|
454
|
+
// thrower it has been defaulting to since the source cadence was added.
|
|
455
|
+
const ingestionSubscriptionStore = new SubscriptionStore(path.join(config.paseoHome, "subscriptions"));
|
|
456
|
+
const ingestionPendingStore = new PendingItemStore(path.join(config.paseoHome, "subscription-items"));
|
|
446
457
|
return {
|
|
447
458
|
capLimiter,
|
|
448
459
|
ingestionCohortStore,
|
|
@@ -450,6 +461,8 @@ function buildIngestionCollaborators(config, ingestionLedger) {
|
|
|
450
461
|
ingestionFilterStore,
|
|
451
462
|
ingestionSourceStore,
|
|
452
463
|
ingestionGatewayResolver,
|
|
464
|
+
ingestionSubscriptionStore,
|
|
465
|
+
ingestionPendingStore,
|
|
453
466
|
};
|
|
454
467
|
}
|
|
455
468
|
export async function createPaseoDaemon(config, rootLogger) {
|
|
@@ -936,7 +949,7 @@ export async function createPaseoDaemon(config, rootLogger) {
|
|
|
936
949
|
// from being true (someone caches `used` in a field to save a query). Two
|
|
937
950
|
// guards, one of which fails loudly.
|
|
938
951
|
//
|
|
939
|
-
const { capLimiter, ingestionCohortStore, ingestionCohorts, ingestionFilterStore, ingestionSourceStore, ingestionGatewayResolver, } = buildIngestionCollaborators(config, ingestionLedger);
|
|
952
|
+
const { capLimiter, ingestionCohortStore, ingestionCohorts, ingestionFilterStore, ingestionSourceStore, ingestionGatewayResolver, ingestionSubscriptionStore, ingestionPendingStore, } = buildIngestionCollaborators(config, ingestionLedger);
|
|
940
953
|
// Explicit annotation breaks the inference cycle: this options object passes
|
|
941
954
|
// `schedule: () => scheduleService`, so inferring the type from the initializer
|
|
942
955
|
// is circular (TS7022/TS7023). The lazy closure itself is correct and intended.
|
|
@@ -992,6 +1005,62 @@ export async function createPaseoDaemon(config, rootLogger) {
|
|
|
992
1005
|
});
|
|
993
1006
|
await ingestionService.start();
|
|
994
1007
|
logger.info({ elapsed: elapsed() }, "Ingestion service initialized");
|
|
1008
|
+
// Session subscriptions: source change -> thin notice -> one specific session.
|
|
1009
|
+
//
|
|
1010
|
+
// It owns its own timer and never touches the schedule engine. A subscription
|
|
1011
|
+
// has to survive its target being archived, and `sweepOrphanedSchedules`
|
|
1012
|
+
// completes an agent-target schedule terminally at the next boot, which is the
|
|
1013
|
+
// exact opposite of the rule here: sending to a dead session revives it.
|
|
1014
|
+
const subscriptionNotifier = new SubscriptionNotifier({
|
|
1015
|
+
subscriptions: ingestionSubscriptionStore,
|
|
1016
|
+
pending: ingestionPendingStore,
|
|
1017
|
+
agentManager,
|
|
1018
|
+
filterResolver: (filterId) => ingestionFilterStore.get(filterId),
|
|
1019
|
+
// `sendPromptToAgent` is the one entry point where unarchive, reload and
|
|
1020
|
+
// detached-runtime recycling already live, which is what makes "the session
|
|
1021
|
+
// revives" true rather than aspirational. The `route` it returns is READ
|
|
1022
|
+
// here on purpose: the websocket handler discards it and answers
|
|
1023
|
+
// `accepted: true` even for `no-route`, and a subscription that believed
|
|
1024
|
+
// that would mark items announced that nobody ever saw.
|
|
1025
|
+
send: async ({ agentId, body }) => {
|
|
1026
|
+
const result = await sendPromptToAgent({
|
|
1027
|
+
agentManager,
|
|
1028
|
+
agentStorage,
|
|
1029
|
+
agentId,
|
|
1030
|
+
prompt: formatSystemNotificationPrompt(body),
|
|
1031
|
+
// A notice must not resurrect a session the user deliberately archived.
|
|
1032
|
+
unarchive: false,
|
|
1033
|
+
logger,
|
|
1034
|
+
});
|
|
1035
|
+
// Fail CLOSED on a missing route. `sendPromptToAgent` types `route` as
|
|
1036
|
+
// optional and omits it on exactly one branch: an archived agent when
|
|
1037
|
+
// `unarchive` is false (`agent-prompt.ts:291-295`), which is the branch
|
|
1038
|
+
// this caller always takes. Coalescing that to "live" would mark every
|
|
1039
|
+
// waiting item announced, advance `lastNotifiedAt` and log a delivery,
|
|
1040
|
+
// for a prompt that was never written anywhere.
|
|
1041
|
+
return { route: result.route ?? "no-route" };
|
|
1042
|
+
},
|
|
1043
|
+
logger,
|
|
1044
|
+
});
|
|
1045
|
+
const subscriptionPoller = new SubscriptionPoller({
|
|
1046
|
+
subscriptions: ingestionSubscriptionStore,
|
|
1047
|
+
pending: ingestionPendingStore,
|
|
1048
|
+
notifier: subscriptionNotifier,
|
|
1049
|
+
sourceStore: ingestionSourceStore,
|
|
1050
|
+
filterResolver: (filterId) => ingestionFilterStore.get(filterId),
|
|
1051
|
+
gatewayResolver: ingestionGatewayResolver,
|
|
1052
|
+
logger,
|
|
1053
|
+
});
|
|
1054
|
+
const subscriptionReader = createSubscriptionReader({
|
|
1055
|
+
subscriptions: ingestionSubscriptionStore,
|
|
1056
|
+
pending: ingestionPendingStore,
|
|
1057
|
+
});
|
|
1058
|
+
// The retention half of storing payloads at rest. At boot, beside the other
|
|
1059
|
+
// sweeps, rather than on a timer of its own.
|
|
1060
|
+
await subscriptionPoller.sweep().catch((error) => {
|
|
1061
|
+
logger.warn({ err: error }, "Subscription payload sweep failed");
|
|
1062
|
+
});
|
|
1063
|
+
subscriptionPoller.start();
|
|
995
1064
|
// The read-only ingestion PREVIEW collaborators (Story 5.1).
|
|
996
1065
|
//
|
|
997
1066
|
// Wired beside IngestionService rather than through it, because that service
|
|
@@ -1164,6 +1233,7 @@ export async function createPaseoDaemon(config, rootLogger) {
|
|
|
1164
1233
|
terminalManager,
|
|
1165
1234
|
getDaemonTcpPort: () => (boundListenTarget?.type === "tcp" ? boundListenTarget.port : null),
|
|
1166
1235
|
scheduleService,
|
|
1236
|
+
subscriptionReader,
|
|
1167
1237
|
providerSnapshotManager,
|
|
1168
1238
|
github,
|
|
1169
1239
|
workspaceGitService,
|
|
@@ -233,6 +233,10 @@ function resolveIngestionConfig(input) {
|
|
|
233
233
|
composio: {
|
|
234
234
|
apiKey: env.PASEO_COMPOSIO_API_KEY?.trim() || undefined,
|
|
235
235
|
},
|
|
236
|
+
selfHosted: {
|
|
237
|
+
url: env.PASEO_SELF_HOSTED_URL?.trim() || undefined,
|
|
238
|
+
token: env.PASEO_SELF_HOSTED_TOKEN?.trim() || undefined,
|
|
239
|
+
},
|
|
236
240
|
};
|
|
237
241
|
}
|
|
238
242
|
function parseTrustedProxiesEnv(value) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { createPaseoDaemon, type PaseoDaemon, type PaseoDaemonConfig } from "./bootstrap.js";
|
|
2
2
|
export { loadConfig, type CliConfigOverrides } from "./config.js";
|
|
3
3
|
export { resolvePaseoHome } from "./paseo-home.js";
|
|
4
|
+
export { SubscriptionStore } from "./ingestion/subscriptions/store.js";
|
|
5
|
+
export { PendingItemStore } from "./ingestion/subscriptions/pending-store.js";
|
|
4
6
|
export { getOrCreateServerId } from "./server-id.js";
|
|
5
7
|
export { createRootLogger, type LogLevel, type LogFormat } from "./logger.js";
|
|
6
8
|
export { loadPersistedConfig, savePersistedConfig, type PersistedConfig, } from "./persisted-config.js";
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
export { createPaseoDaemon } from "./bootstrap.js";
|
|
3
3
|
export { loadConfig } from "./config.js";
|
|
4
4
|
export { resolvePaseoHome } from "./paseo-home.js";
|
|
5
|
+
// Session subscriptions. Exported for the CLI, which writes the store DIRECTLY
|
|
6
|
+
// rather than through the daemon: both stores re-read from disk on every call
|
|
7
|
+
// and cache nothing, so a file written here is picked up by the poller's next
|
|
8
|
+
// tick with no RPC and no restart.
|
|
9
|
+
export { SubscriptionStore } from "./ingestion/subscriptions/store.js";
|
|
10
|
+
export { PendingItemStore } from "./ingestion/subscriptions/pending-store.js";
|
|
5
11
|
export { getOrCreateServerId } from "./server-id.js";
|
|
6
12
|
export { createRootLogger } from "./logger.js";
|
|
7
13
|
export { loadPersistedConfig, savePersistedConfig, } from "./persisted-config.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createComposioGateway } from "./composio/gateway.js";
|
|
2
2
|
import { createPipedreamGateway } from "./pipedream/gateway.js";
|
|
3
|
+
import { createSelfHostedGateway } from "./self-hosted/gateway.js";
|
|
3
4
|
/**
|
|
4
5
|
* The adapter factory for every source kind.
|
|
5
6
|
*
|
|
@@ -16,6 +17,7 @@ import { createPipedreamGateway } from "./pipedream/gateway.js";
|
|
|
16
17
|
export const SOURCE_ADAPTERS = {
|
|
17
18
|
pipedream: (deps) => createPipedreamGateway(deps.config.pipedream),
|
|
18
19
|
composio: (deps) => createComposioGateway(deps.config.composio),
|
|
20
|
+
"self-hosted": (deps) => createSelfHostedGateway(deps.config.selfHosted),
|
|
19
21
|
};
|
|
20
22
|
/**
|
|
21
23
|
* The gateway for one kind. Total by construction: SOURCE_ADAPTERS covers every
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
* COMPOSIO_OFFLINE | 502 | Composio unreachable
|
|
14
14
|
* COMPOSIO_AUTH | 502 | Composio rejected our platform credential
|
|
15
15
|
* COMPOSIO_ERROR | 502 | Composio answered non-ok for any other reason
|
|
16
|
+
* SELF_HOSTED_OFF | 422 | no bridge URL/token, or the URL is malformed
|
|
17
|
+
* SELF_HOSTED_OFFLINE | 502 | the user's bridge is unreachable or timed out
|
|
18
|
+
* SELF_HOSTED_AUTH | 502 | the bridge rejected this daemon's bearer
|
|
19
|
+
* SELF_HOSTED_ERROR | 502 | the bridge answered non-ok, non-JSON or incomplete
|
|
16
20
|
* SOURCE_GONE | 502 | a resource we cached was deleted upstream
|
|
17
21
|
* SOURCE_ERROR | 502 | any other non-ok aggregator response
|
|
18
22
|
* FILTER_MISSING | 404 | no such filter on THIS daemon
|
|
@@ -37,7 +41,7 @@
|
|
|
37
41
|
* tell the caller to re-authenticate, which cannot fix anything, and would
|
|
38
42
|
* blame the wrong party for an operator's misconfiguration.
|
|
39
43
|
*/
|
|
40
|
-
export declare const INGESTION_ERROR_CODES: readonly ["PIPEDREAM_OFF", "COMPOSIO_OFF", "COMPOSIO_NEEDS_SETUP", "PIPEDREAM_OFFLINE", "PIPEDREAM_AUTH", "COMPOSIO_OFFLINE", "COMPOSIO_AUTH", "COMPOSIO_ERROR", "SOURCE_GONE", "SOURCE_ERROR", "FILTER_MISSING", "SOURCE_MISSING"];
|
|
44
|
+
export declare const INGESTION_ERROR_CODES: readonly ["PIPEDREAM_OFF", "COMPOSIO_OFF", "COMPOSIO_NEEDS_SETUP", "PIPEDREAM_OFFLINE", "PIPEDREAM_AUTH", "COMPOSIO_OFFLINE", "COMPOSIO_AUTH", "COMPOSIO_ERROR", "SELF_HOSTED_OFF", "SELF_HOSTED_OFFLINE", "SELF_HOSTED_AUTH", "SELF_HOSTED_ERROR", "SOURCE_GONE", "SOURCE_ERROR", "FILTER_MISSING", "SOURCE_MISSING"];
|
|
41
45
|
export type IngestionErrorCode = (typeof INGESTION_ERROR_CODES)[number];
|
|
42
46
|
/** A domain error from the ingestion seam, with a message ready for a client. */
|
|
43
47
|
export declare class IngestionError extends Error {
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
* COMPOSIO_OFFLINE | 502 | Composio unreachable
|
|
14
14
|
* COMPOSIO_AUTH | 502 | Composio rejected our platform credential
|
|
15
15
|
* COMPOSIO_ERROR | 502 | Composio answered non-ok for any other reason
|
|
16
|
+
* SELF_HOSTED_OFF | 422 | no bridge URL/token, or the URL is malformed
|
|
17
|
+
* SELF_HOSTED_OFFLINE | 502 | the user's bridge is unreachable or timed out
|
|
18
|
+
* SELF_HOSTED_AUTH | 502 | the bridge rejected this daemon's bearer
|
|
19
|
+
* SELF_HOSTED_ERROR | 502 | the bridge answered non-ok, non-JSON or incomplete
|
|
16
20
|
* SOURCE_GONE | 502 | a resource we cached was deleted upstream
|
|
17
21
|
* SOURCE_ERROR | 502 | any other non-ok aggregator response
|
|
18
22
|
* FILTER_MISSING | 404 | no such filter on THIS daemon
|
|
@@ -46,6 +50,10 @@ export const INGESTION_ERROR_CODES = [
|
|
|
46
50
|
"COMPOSIO_OFFLINE",
|
|
47
51
|
"COMPOSIO_AUTH",
|
|
48
52
|
"COMPOSIO_ERROR",
|
|
53
|
+
"SELF_HOSTED_OFF",
|
|
54
|
+
"SELF_HOSTED_OFFLINE",
|
|
55
|
+
"SELF_HOSTED_AUTH",
|
|
56
|
+
"SELF_HOSTED_ERROR",
|
|
49
57
|
"SOURCE_GONE",
|
|
50
58
|
"SOURCE_ERROR",
|
|
51
59
|
"FILTER_MISSING",
|
|
@@ -22,6 +22,16 @@ const ERROR_CODE_STATUS = {
|
|
|
22
22
|
COMPOSIO_OFFLINE: "degraded",
|
|
23
23
|
COMPOSIO_AUTH: "revoked",
|
|
24
24
|
COMPOSIO_ERROR: "degraded",
|
|
25
|
+
// Same split as the aggregators, for the same reason. OFF/OFFLINE/ERROR are
|
|
26
|
+
// states the next sweep may clear on its own; AUTH is not. For a vendor,
|
|
27
|
+
// AUTH means the user must re-authorize there; for a self-hosted bridge it
|
|
28
|
+
// means the bearer in this daemon's env no longer matches the one the bridge
|
|
29
|
+
// expects. Different cause, identical consequence: a human has to act, so
|
|
30
|
+
// reporting it as `degraded` would leave a dead source looking merely slow.
|
|
31
|
+
SELF_HOSTED_OFF: "degraded",
|
|
32
|
+
SELF_HOSTED_OFFLINE: "degraded",
|
|
33
|
+
SELF_HOSTED_AUTH: "revoked",
|
|
34
|
+
SELF_HOSTED_ERROR: "degraded",
|
|
25
35
|
SOURCE_GONE: "revoked",
|
|
26
36
|
SOURCE_ERROR: "degraded",
|
|
27
37
|
// The 404 pair cannot arise from a health check: `checkSource` is only ever
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The self-hosted source gateway.
|
|
3
|
+
*
|
|
4
|
+
* Composio and Pipedream are aggregators someone else runs: paseo holds a
|
|
5
|
+
* platform credential and talks to a fixed base URL baked into the adapter.
|
|
6
|
+
* This adapter inverts that. The user runs the aggregator, tells paseo where it
|
|
7
|
+
* is, and paseo holds nothing but a bearer token for that one endpoint.
|
|
8
|
+
*
|
|
9
|
+
* That inversion is the whole point. Nothing in this file names an app, a
|
|
10
|
+
* vendor or a protocol beyond HTTP+JSON: the bridge on the other end decides
|
|
11
|
+
* what it fronts. WhatsApp-over-wacli is the first one, but a Matrix bridge, an
|
|
12
|
+
* IMAP box or a signal-cli would implement the same six routes and paseo would
|
|
13
|
+
* not learn a new word.
|
|
14
|
+
*
|
|
15
|
+
* WHAT THE BRIDGE OWES US (v1):
|
|
16
|
+
*
|
|
17
|
+
* GET {base}/v1/sources?query=&cursor= -> SourcesResponse
|
|
18
|
+
* POST {base}/v1/connect -> ConnectResponse
|
|
19
|
+
* GET {base}/v1/accounts?... -> AccountsResponse
|
|
20
|
+
* GET {base}/v1/items?... -> ItemsResponse
|
|
21
|
+
* POST {base}/v1/mcp-target -> McpTargetResponse
|
|
22
|
+
*
|
|
23
|
+
* `listTriggerComponents` / `deployTrigger` are deliberately NOT implemented.
|
|
24
|
+
* They are optional on `SourceGateway`, and `service.ts` already answers
|
|
25
|
+
* `${kind} cannot deploy triggers` for a gateway that lacks them. A bridge that
|
|
26
|
+
* fronts a local daemon has nothing to deploy: it is already watching, or it is
|
|
27
|
+
* not running at all. Pretending otherwise would report a source as "watching"
|
|
28
|
+
* when nothing is.
|
|
29
|
+
*
|
|
30
|
+
* LEDGER IDENTITY IS OURS, NOT THE BRIDGE'S.
|
|
31
|
+
*
|
|
32
|
+
* The bridge returns `externalId` and a raw `payload`; `key` and `contentHash`
|
|
33
|
+
* are computed HERE with `fingerprint()`, byte-identically to the Composio
|
|
34
|
+
* adapter. A bridge that minted its own keys would be a second identity
|
|
35
|
+
* function for the same items, and an identity that disagrees with the ledger
|
|
36
|
+
* re-dispatches a backlog the user already cleared.
|
|
37
|
+
*
|
|
38
|
+
* Note what `key` deliberately does NOT include: `accountId`. Two accounts on
|
|
39
|
+
* the same underlying inbox (the read-only and read-write wacli pair, both
|
|
40
|
+
* linked to one WhatsApp number) see the SAME messages. Keying without the
|
|
41
|
+
* account makes that one ledger row handled once, which is the correct
|
|
42
|
+
* behaviour, and it also means re-pairing an account does not invalidate a
|
|
43
|
+
* backlog the user already worked through.
|
|
44
|
+
*
|
|
45
|
+
* CREDENTIALS.
|
|
46
|
+
*
|
|
47
|
+
* The bearer token never leaves this module, and `mcpTarget` results are per
|
|
48
|
+
* turn: they carry live headers minted by the bridge and are never persisted,
|
|
49
|
+
* cached or returned to a client. A `SourceItem` has no credential field and
|
|
50
|
+
* must never grow one - it is projected into `BackfillItem` and crosses the
|
|
51
|
+
* wire to the app.
|
|
52
|
+
*/
|
|
53
|
+
import type { SelfHostedConfig, SourceGateway } from "../types.js";
|
|
54
|
+
export declare function createSelfHostedGateway(config?: SelfHostedConfig | undefined): SourceGateway;
|
|
55
|
+
//# sourceMappingURL=gateway.d.ts.map
|