talon-agent 5.14.0 → 5.18.2

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 (116) hide show
  1. package/LICENSE +202 -21
  2. package/LICENSE-MIT +21 -0
  3. package/NOTICE +16 -0
  4. package/README.md +14 -7
  5. package/package.json +6 -4
  6. package/prompts/system/heartbeat-agent.md +1 -1
  7. package/src/backend/agy/factory.ts +3 -0
  8. package/src/backend/agy/mcp/config.ts +14 -2
  9. package/src/backend/claude-sdk/factory.ts +3 -0
  10. package/src/backend/claude-sdk/options.ts +24 -3
  11. package/src/backend/codex/factory.ts +3 -0
  12. package/src/backend/codex/init.ts +4 -0
  13. package/src/backend/codex/mcp-config.ts +10 -0
  14. package/src/backend/codex/oauth-incompat.ts +1 -1
  15. package/src/backend/codex/token-usage.ts +2 -2
  16. package/src/backend/openai-agents/factory.ts +3 -0
  17. package/src/backend/openai-agents/mcp-pool.ts +4 -0
  18. package/src/backend/remote-server/factory.ts +3 -0
  19. package/src/backend/remote-server/mcp.ts +3 -0
  20. package/src/backend/runtime/prompt/prompt-format.ts +3 -3
  21. package/src/bootstrap.ts +8 -0
  22. package/src/cli/commands/backup.ts +61 -5
  23. package/src/cli/commands/mesh.ts +133 -0
  24. package/src/cli/config.ts +3 -1
  25. package/src/cli/daemon-api.ts +22 -0
  26. package/src/cli/index.ts +6 -0
  27. package/src/cli/install-sources.ts +40 -5
  28. package/src/cli/plugin.ts +10 -0
  29. package/src/cli/setup.ts +45 -4
  30. package/src/cli/skill.ts +3 -0
  31. package/src/core/agent-runtime/backend-registry.ts +16 -0
  32. package/src/core/backup/archive/crypt.ts +429 -0
  33. package/src/core/backup/archive/manifest-auth.ts +98 -0
  34. package/src/core/backup/passphrase.ts +130 -0
  35. package/src/core/backup/plan.ts +66 -13
  36. package/src/core/backup/restore-guard.ts +101 -0
  37. package/src/core/backup/restore.ts +249 -32
  38. package/src/core/backup/snapshot.ts +418 -60
  39. package/src/core/backup/sources/plugins.ts +223 -0
  40. package/src/core/backup/sources/relocate.ts +136 -0
  41. package/src/core/backup/sources/sessions.ts +198 -0
  42. package/src/core/backup/store.ts +3 -1
  43. package/src/core/backup/types.ts +66 -0
  44. package/src/core/backup/upload.ts +66 -6
  45. package/src/core/config/index.ts +140 -8
  46. package/src/core/daemon/control.ts +9 -0
  47. package/src/core/daemon/discovery.ts +7 -0
  48. package/src/core/engine/backend-router/headroom.ts +29 -5
  49. package/src/core/engine/backend-router/usage.ts +6 -2
  50. package/src/core/engine/gateway-actions/fetch-url/guard.ts +201 -0
  51. package/src/core/engine/gateway-actions/{fetch-url.ts → fetch-url/index.ts} +60 -32
  52. package/src/core/engine/gateway-actions/index.ts +4 -2
  53. package/src/core/engine/gateway-actions/native/index.ts +24 -0
  54. package/src/core/engine/gateway-actions/whatsapp-account.ts +1 -1
  55. package/src/core/engine/gateway-auth.ts +164 -0
  56. package/src/core/engine/gateway-routes.ts +100 -5
  57. package/src/core/engine/gateway.ts +12 -4
  58. package/src/core/mcp-hub/guest-scope.ts +170 -29
  59. package/src/core/mcp-hub/index.ts +33 -16
  60. package/src/core/mcp-hub/talon-server.ts +71 -14
  61. package/src/core/mesh/credentials/admin.ts +146 -0
  62. package/src/core/mesh/credentials/index.ts +19 -0
  63. package/src/core/mesh/credentials/store.ts +443 -0
  64. package/src/core/mesh/credentials/token.ts +45 -0
  65. package/src/core/mesh/credentials/types.ts +83 -0
  66. package/src/core/mesh/devices/service.ts +58 -6
  67. package/src/core/mesh/links/bridge-links.ts +46 -4
  68. package/src/core/mesh/links/node-binaries.ts +1 -1
  69. package/src/core/mesh/links/node-provision.ts +8 -1
  70. package/src/core/models/active-model.ts +1 -1
  71. package/src/core/plugin/loader.ts +4 -0
  72. package/src/core/plugin/mcp.ts +4 -0
  73. package/src/core/tools/bridge.ts +2 -1
  74. package/src/core/types.ts +13 -0
  75. package/src/core/weaver/weaver.ts +47 -15
  76. package/src/frontend/discord/callbacks/components/agent-buttons.ts +1 -0
  77. package/src/frontend/discord/handlers/delivery.ts +3 -0
  78. package/src/frontend/discord/handlers/queue.ts +1 -0
  79. package/src/frontend/native/bridge/auth-guard.ts +277 -0
  80. package/src/frontend/native/bridge/auth.ts +84 -1
  81. package/src/frontend/native/bridge/credentials/claims.ts +51 -0
  82. package/src/frontend/native/bridge/credentials/principal.ts +200 -0
  83. package/src/frontend/native/bridge/credentials/upgrade.ts +129 -0
  84. package/src/frontend/native/bridge/routes/auth.ts +37 -0
  85. package/src/frontend/native/bridge/routes/chats.ts +20 -2
  86. package/src/frontend/native/bridge/routes/host.ts +11 -1
  87. package/src/frontend/native/bridge/routes/index.ts +2 -0
  88. package/src/frontend/native/bridge/routes/mesh.ts +72 -15
  89. package/src/frontend/native/bridge/routes/table.ts +73 -51
  90. package/src/frontend/native/bridge/server.ts +266 -83
  91. package/src/frontend/native/index.ts +54 -3
  92. package/src/frontend/native/turn/turn.ts +2 -0
  93. package/src/frontend/teams/turn.ts +1 -0
  94. package/src/frontend/telegram/actions/outgoing-log.ts +70 -0
  95. package/src/frontend/telegram/actions/send.ts +4 -0
  96. package/src/frontend/telegram/admin.ts +20 -0
  97. package/src/frontend/telegram/commands/admin.ts +1 -1
  98. package/src/frontend/telegram/commands/state.ts +7 -9
  99. package/src/frontend/telegram/handlers/access.ts +31 -10
  100. package/src/frontend/telegram/handlers/delivery.ts +14 -1
  101. package/src/frontend/telegram/handlers/group-access.ts +50 -0
  102. package/src/frontend/telegram/handlers/messages.ts +1 -0
  103. package/src/frontend/telegram/handlers/queue.ts +14 -0
  104. package/src/frontend/telegram/handlers/state.ts +2 -2
  105. package/src/frontend/telegram/index.ts +32 -9
  106. package/src/frontend/telegram/middleware.ts +2 -2
  107. package/src/frontend/telegram/polling/poll-deadline.ts +52 -0
  108. package/src/frontend/telegram/{stale-command.ts → polling/stale-command.ts} +1 -1
  109. package/src/frontend/telegram/{update-offset.ts → polling/update-offset.ts} +1 -1
  110. package/src/frontend/telegram/userbot.ts +103 -10
  111. package/src/frontend/terminal/index.ts +8 -1
  112. package/src/frontend/whatsapp/commands.ts +3 -3
  113. package/src/frontend/whatsapp/messages/inbound.ts +1 -0
  114. package/src/plugins/playwright/index.ts +1 -1
  115. package/src/storage/backup/index.ts +1 -1
  116. package/src/storage/db.ts +23 -0
@@ -2,6 +2,37 @@ import type { RouteHost } from "./host.js";
2
2
  import type { BridgeRoutes } from "./table.js";
3
3
  import type { ServerResponse } from "node:http";
4
4
  import { deviceIdParam } from "./params.js";
5
+ import { claimDevice } from "../credentials/claims.js";
6
+ import { credentialHint } from "../credentials/upgrade.js";
7
+ import type { RouteContext } from "./table.js";
8
+
9
+ /**
10
+ * Resolve the device a mesh request acts as — the spoofing check. Returns
11
+ * the effective id (a per-device credential's own device when the request
12
+ * named none), or null after answering 403 for a claim the credential may
13
+ * not make.
14
+ */
15
+ function actingDevice(
16
+ host: RouteHost,
17
+ ctx: RouteContext,
18
+ claimed: unknown,
19
+ ): { deviceId: string | undefined } | null {
20
+ const named =
21
+ typeof claimed === "string" && claimed.trim() ? claimed.trim() : undefined;
22
+ const claim = claimDevice(ctx.principal, named, host.credentials);
23
+ if (claim.ok) return { deviceId: claim.deviceId };
24
+ host.json(ctx.res, 403, { ok: false, error: claim.error });
25
+ return null;
26
+ }
27
+
28
+ /** Stamp the acting device onto a body field the handler reads it from. */
29
+ function withDevice(
30
+ body: Record<string, unknown>,
31
+ field: "id" | "deviceId",
32
+ deviceId: string | undefined,
33
+ ): Record<string, unknown> {
34
+ return deviceId === undefined ? body : { ...body, [field]: deviceId };
35
+ }
5
36
 
6
37
  // Streamed device file transfers (see core/mesh/transfers/transfers.ts). The
7
38
  // one-time `transfer` token authorizes exactly one direction+path; the
@@ -34,33 +65,59 @@ export function meshRoutes(
34
65
  return {
35
66
  // ── Mesh ───────────────────────────────────────────────────────────
36
67
 
37
- "POST /devices/register": async ({ req, res }) => {
38
- const body = await readJson(req);
39
- const device = await h.registerDevice(body);
40
- json(res, 200, { ok: true, deviceId: device.id });
68
+ // Registration is also the heartbeat, so its reply is where a device
69
+ // learns it should trade the shared token for its own credential, or
70
+ // rotate the one it has (`credential.action`).
71
+ "POST /devices/register": async (ctx) => {
72
+ const body = await readJson(ctx.req);
73
+ const acting = actingDevice(host, ctx, body.id);
74
+ if (!acting) return;
75
+ const device = await h.registerDevice(
76
+ withDevice(body, "id", acting.deviceId),
77
+ );
78
+ const hint = credentialHint(host.credentials, ctx.principal);
79
+ json(ctx.res, 200, {
80
+ ok: true,
81
+ deviceId: device.id,
82
+ ...(hint ? { credential: hint } : {}),
83
+ });
41
84
  },
42
- "POST /location": async ({ req, res }) => {
43
- const body = await readJson(req);
44
- await h.storeLocation(body);
45
- json(res, 200, { ok: true });
85
+ "POST /location": async (ctx) => {
86
+ const body = await readJson(ctx.req);
87
+ const acting = actingDevice(host, ctx, body.deviceId);
88
+ if (!acting) return;
89
+ await h.storeLocation(withDevice(body, "deviceId", acting.deviceId));
90
+ json(ctx.res, 200, { ok: true });
46
91
  },
47
92
  "GET /devices": async ({ res }) => json(res, 200, await h.listDevices()),
48
- "POST /devices/command-result": async ({ req, res }) => {
49
- const body = await readJson(req);
93
+ "POST /devices/command-result": async (ctx) => {
94
+ const body = await readJson(ctx.req);
95
+ // A credential answers as its own device only — so it can never
96
+ // complete (or forge the result of) another device's command.
97
+ const acting = actingDevice(host, ctx, body.deviceId);
98
+ if (!acting) return;
50
99
  // ok:false for a late/unknown correlation id — not an HTTP error,
51
100
  // the device's POST was well-formed; nothing was waiting anymore.
52
- json(res, 200, { ok: h.completeCommand(body) });
101
+ json(ctx.res, 200, {
102
+ ok: h.completeCommand(withDevice(body, "deviceId", acting.deviceId)),
103
+ });
53
104
  },
54
- "POST /devices/file": async ({ req, res, url }) => {
105
+ "POST /devices/file": async (ctx) => {
106
+ const { req, res, url } = ctx;
107
+ const acting = actingDevice(host, ctx, deviceIdParam(url));
108
+ if (!acting) return;
55
109
  const token = transferToken(host, url, res);
56
110
  if (token === null) return;
57
- const result = await h.acceptFileUpload(token, req, deviceIdParam(url));
111
+ const result = await h.acceptFileUpload(token, req, acting.deviceId);
58
112
  json(res, result.ok ? 200 : 409, result);
59
113
  },
60
- "GET /devices/file": async ({ res, url }) => {
114
+ "GET /devices/file": async (ctx) => {
115
+ const { res, url } = ctx;
116
+ const acting = actingDevice(host, ctx, deviceIdParam(url));
117
+ if (!acting) return;
61
118
  const token = transferToken(host, url, res);
62
119
  if (token === null) return;
63
- const file = await h.openFileDownload(token, deviceIdParam(url));
120
+ const file = await h.openFileDownload(token, acting.deviceId);
64
121
  if (!file)
65
122
  return json(res, 404, {
66
123
  ok: false,
@@ -1,4 +1,11 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type {
3
+ BridgePrincipal,
4
+ BridgeRouteAuth,
5
+ BridgeScope,
6
+ } from "../credentials/principal.js";
7
+
8
+ export type { BridgeRouteAuth };
2
9
 
3
10
  /**
4
11
  * ok: request carries the right token (or none is required).
@@ -7,22 +14,18 @@ import type { IncomingMessage, ServerResponse } from "node:http";
7
14
  */
8
15
  export type AuthState = "ok" | "anonymous" | "bad";
9
16
 
10
- /**
11
- * "public": served without a bearer token. Every entry is gated some other
12
- * way — a single-use grant minted by the daemon, or (for /health) by
13
- * answering only what pairing needs until a token is presented.
14
- * "bearer": the request must carry the bridge token.
15
- */
16
- export type BridgeRouteAuth = "public" | "bearer";
17
+ /** Any authenticated caller — the route itself decides what it may see. */
18
+ const ANY: readonly BridgeScope[] = ["device", "client", "operator"];
17
19
 
18
20
  /**
19
21
  * The bridge's routes and the auth tier of each — declared once, here,
20
22
  * rather than implied by where an `if` sits relative to the auth check.
21
23
  * The security posture of the transport is this table: a route is
22
- * pre-auth only by appearing in it as "public", and the route test walks
23
- * every entry and proves the tier holds on the wire. Adding a route
24
- * without an entry is a type error (`buildRoutes` is exhaustive over
25
- * these keys); adding one as "public" is a diff a reviewer sees.
24
+ * pre-auth only by appearing in it as "public", reachable by a per-device
25
+ * credential only through the scope written next to it, and the route
26
+ * tests walk every entry and prove each tier holds on the wire. Adding a
27
+ * route without an entry is a type error (`buildRoutes` is exhaustive over
28
+ * these keys); widening one is a diff a reviewer sees.
26
29
  */
27
30
  export const BRIDGE_ROUTE_AUTH = {
28
31
  // Pre-auth by design. /health serves pairing data (identity, protocol,
@@ -35,52 +38,69 @@ export const BRIDGE_ROUTE_AUTH = {
35
38
  "GET /node/install": "public",
36
39
  "GET /node/binary": "public",
37
40
 
38
- // Everything a client can do once paired.
39
- "GET /events": "bearer",
40
- "GET /chats": "bearer",
41
- "POST /chats": "bearer",
42
- "POST /chats/rename": "bearer",
43
- "POST /chats/delete": "bearer",
44
- "POST /chats/reset": "bearer",
45
- "POST /chats/interrupt": "bearer",
46
- "POST /chats/pulse": "bearer",
47
- "POST /queue": "bearer",
48
- "GET /history": "bearer",
49
- "GET /search": "bearer",
41
+ // Credential self-service. Any credential may ask who it is, and trade
42
+ // itself (or the shared legacy token) for a fresh per-device credential
43
+ // bound to ONE device id — the in-band upgrade and rotation path.
44
+ "GET /auth/whoami": ANY,
45
+ "POST /auth/upgrade": ANY,
46
+
47
+ // The event stream. A device-only credential receives only what is
48
+ // addressed to its own device (commands, locates) — never chat traffic.
49
+ "GET /events": ["device", "client"],
50
+
51
+ // The chat UI.
52
+ "GET /chats": "client",
53
+ "POST /chats": "client",
54
+ "POST /chats/rename": "client",
55
+ "POST /chats/delete": "client",
56
+ "POST /chats/reset": "client",
57
+ "POST /chats/interrupt": "client",
58
+ "POST /chats/pulse": "client",
59
+ "POST /queue": "client",
60
+ "GET /history": "client",
61
+ "GET /search": "client",
50
62
 
51
63
  // Memory — read-only. The typed memory store is readable over the
52
64
  // bridge but never writable from it: asserting and dropping stay with
53
65
  // the daemon's own write path.
54
- "GET /memory": "bearer",
55
- "GET /memory/why": "bearer",
66
+ "GET /memory": "client",
67
+ "GET /memory/why": "client",
68
+
69
+ "POST /send": "client",
70
+ "POST /upload": "client",
71
+ "GET /media": "client",
72
+ "GET /models": "client",
73
+ "POST /model": "client",
74
+ "GET /backends": "client",
75
+ "POST /backend": "client",
76
+ "GET /effort": "client",
77
+ "POST /effort": "client",
78
+ // Reads of the non-secret, allowlisted settings snapshot and the
79
+ // extension lists are the UI's; changing any of it is the operator's.
80
+ "GET /plugins": "client",
81
+ "GET /skills": "client",
82
+ "GET /config": "client",
56
83
 
57
- "POST /send": "bearer",
58
- "POST /upload": "bearer",
59
- "GET /media": "bearer",
60
- "GET /models": "bearer",
61
- "POST /model": "bearer",
62
- "GET /backends": "bearer",
63
- "POST /backend": "bearer",
64
- "GET /effort": "bearer",
65
- "POST /effort": "bearer",
66
- "GET /logs": "bearer",
67
- "GET /plugins": "bearer",
68
- "POST /plugins/toggle": "bearer",
69
- "GET /skills": "bearer",
70
- "POST /skills/toggle": "bearer",
71
- "GET /config": "bearer",
72
- "POST /config": "bearer",
73
- "POST /control": "bearer",
84
+ // Operator. Logs carry command lines, paths and device output, so they
85
+ // are not a chat-UI read.
86
+ "GET /logs": "operator",
87
+ "POST /plugins/toggle": "operator",
88
+ "POST /skills/toggle": "operator",
89
+ "POST /config": "operator",
90
+ "POST /control": "operator",
74
91
 
75
- // Mesh. The one-time `transfer` token on /devices/file authorizes one
76
- // direction+path, but the route still sits behind the bearer like every
77
- // device route — the token is a scope, not a credential.
78
- "POST /devices/register": "bearer",
79
- "POST /location": "bearer",
80
- "GET /devices": "bearer",
81
- "POST /devices/command-result": "bearer",
82
- "POST /devices/file": "bearer",
83
- "GET /devices/file": "bearer",
92
+ // Mesh — a device acting as itself. Every body/query naming a device id
93
+ // is checked against the credential's own device (credentials/claims.ts).
94
+ // The one-time `transfer` token on /devices/file authorizes one
95
+ // direction+path for one device; the credential still has to be that
96
+ // device's.
97
+ "POST /devices/register": "device",
98
+ "POST /location": "device",
99
+ "POST /devices/command-result": "device",
100
+ "POST /devices/file": "device",
101
+ "GET /devices/file": "device",
102
+ // The fleet view (every device and its last location) is the UI's.
103
+ "GET /devices": "client",
84
104
  } as const satisfies Record<string, BridgeRouteAuth>;
85
105
 
86
106
  export type BridgeRouteKey = keyof typeof BRIDGE_ROUTE_AUTH;
@@ -91,6 +111,8 @@ export type RouteContext = {
91
111
  res: ServerResponse;
92
112
  url: URL;
93
113
  auth: AuthState;
114
+ /** Who is calling — null only on a public route hit without a credential. */
115
+ principal: BridgePrincipal | null;
94
116
  };
95
117
 
96
118
  export type RouteHandler = (ctx: RouteContext) => void | Promise<void>;