@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.
Files changed (39) hide show
  1. package/dist/server/server/agent/tools/paseo-tools.d.ts +9 -0
  2. package/dist/server/server/agent/tools/paseo-tools.js +76 -1
  3. package/dist/server/server/agent/tools/read-only-surface.d.ts +7 -0
  4. package/dist/server/server/agent/tools/read-only-surface.js +8 -0
  5. package/dist/server/server/bootstrap.js +71 -1
  6. package/dist/server/server/config.js +4 -0
  7. package/dist/server/server/exports.d.ts +2 -0
  8. package/dist/server/server/exports.js +6 -0
  9. package/dist/server/server/ingestion/adapters.js +2 -0
  10. package/dist/server/server/ingestion/errors.d.ts +5 -1
  11. package/dist/server/server/ingestion/errors.js +8 -0
  12. package/dist/server/server/ingestion/health.js +10 -0
  13. package/dist/server/server/ingestion/self-hosted/gateway.d.ts +55 -0
  14. package/dist/server/server/ingestion/self-hosted/gateway.js +295 -0
  15. package/dist/server/server/ingestion/subscriptions/notification-prompt.d.ts +83 -0
  16. package/dist/server/server/ingestion/subscriptions/notification-prompt.js +96 -0
  17. package/dist/server/server/ingestion/subscriptions/notifier.d.ts +96 -0
  18. package/dist/server/server/ingestion/subscriptions/notifier.js +209 -0
  19. package/dist/server/server/ingestion/subscriptions/pending-store.d.ts +111 -0
  20. package/dist/server/server/ingestion/subscriptions/pending-store.js +254 -0
  21. package/dist/server/server/ingestion/subscriptions/poller.d.ts +73 -0
  22. package/dist/server/server/ingestion/subscriptions/poller.js +168 -0
  23. package/dist/server/server/ingestion/subscriptions/reader.d.ts +39 -0
  24. package/dist/server/server/ingestion/subscriptions/reader.js +30 -0
  25. package/dist/server/server/ingestion/subscriptions/store.d.ts +35 -0
  26. package/dist/server/server/ingestion/subscriptions/store.js +82 -0
  27. package/dist/server/server/ingestion/types.d.ts +14 -1
  28. package/dist/server/server/ingestion/types.js +1 -1
  29. package/dist/server/web-ui/_expo/static/js/web/{index-88d5f130a09403b86ae8c8c5fd1f97d4.js → index-4695e64c38d7ce76a8375d72b95c2c0c.js} +6 -6
  30. package/dist/server/web-ui/_expo/static/js/web/index-4695e64c38d7ce76a8375d72b95c2c0c.js.br +0 -0
  31. package/dist/server/web-ui/_expo/static/js/web/index-4695e64c38d7ce76a8375d72b95c2c0c.js.gz +0 -0
  32. package/dist/server/web-ui/_expo/static/js/web/{index-88d5f130a09403b86ae8c8c5fd1f97d4.js.map.br → index-4695e64c38d7ce76a8375d72b95c2c0c.js.map.br} +0 -0
  33. package/dist/server/web-ui/_expo/static/js/web/{index-88d5f130a09403b86ae8c8c5fd1f97d4.js.map.gz → index-4695e64c38d7ce76a8375d72b95c2c0c.js.map.gz} +0 -0
  34. package/dist/server/web-ui/index.html +1 -1
  35. package/dist/server/web-ui/index.html.br +0 -0
  36. package/dist/server/web-ui/index.html.gz +0 -0
  37. package/package.json +6 -6
  38. package/dist/server/web-ui/_expo/static/js/web/index-88d5f130a09403b86ae8c8c5fd1f97d4.js.br +0 -0
  39. 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