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.
- package/LICENSE +202 -21
- package/LICENSE-MIT +21 -0
- package/NOTICE +16 -0
- package/README.md +14 -7
- package/package.json +6 -4
- package/prompts/system/heartbeat-agent.md +1 -1
- package/src/backend/agy/factory.ts +3 -0
- package/src/backend/agy/mcp/config.ts +14 -2
- package/src/backend/claude-sdk/factory.ts +3 -0
- package/src/backend/claude-sdk/options.ts +24 -3
- package/src/backend/codex/factory.ts +3 -0
- package/src/backend/codex/init.ts +4 -0
- package/src/backend/codex/mcp-config.ts +10 -0
- package/src/backend/codex/oauth-incompat.ts +1 -1
- package/src/backend/codex/token-usage.ts +2 -2
- package/src/backend/openai-agents/factory.ts +3 -0
- package/src/backend/openai-agents/mcp-pool.ts +4 -0
- package/src/backend/remote-server/factory.ts +3 -0
- package/src/backend/remote-server/mcp.ts +3 -0
- package/src/backend/runtime/prompt/prompt-format.ts +3 -3
- package/src/bootstrap.ts +8 -0
- package/src/cli/commands/backup.ts +61 -5
- package/src/cli/commands/mesh.ts +133 -0
- package/src/cli/config.ts +3 -1
- package/src/cli/daemon-api.ts +22 -0
- package/src/cli/index.ts +6 -0
- package/src/cli/install-sources.ts +40 -5
- package/src/cli/plugin.ts +10 -0
- package/src/cli/setup.ts +45 -4
- package/src/cli/skill.ts +3 -0
- package/src/core/agent-runtime/backend-registry.ts +16 -0
- package/src/core/backup/archive/crypt.ts +429 -0
- package/src/core/backup/archive/manifest-auth.ts +98 -0
- package/src/core/backup/passphrase.ts +130 -0
- package/src/core/backup/plan.ts +66 -13
- package/src/core/backup/restore-guard.ts +101 -0
- package/src/core/backup/restore.ts +249 -32
- package/src/core/backup/snapshot.ts +418 -60
- package/src/core/backup/sources/plugins.ts +223 -0
- package/src/core/backup/sources/relocate.ts +136 -0
- package/src/core/backup/sources/sessions.ts +198 -0
- package/src/core/backup/store.ts +3 -1
- package/src/core/backup/types.ts +66 -0
- package/src/core/backup/upload.ts +66 -6
- package/src/core/config/index.ts +140 -8
- package/src/core/daemon/control.ts +9 -0
- package/src/core/daemon/discovery.ts +7 -0
- package/src/core/engine/backend-router/headroom.ts +29 -5
- package/src/core/engine/backend-router/usage.ts +6 -2
- package/src/core/engine/gateway-actions/fetch-url/guard.ts +201 -0
- package/src/core/engine/gateway-actions/{fetch-url.ts → fetch-url/index.ts} +60 -32
- package/src/core/engine/gateway-actions/index.ts +4 -2
- package/src/core/engine/gateway-actions/native/index.ts +24 -0
- package/src/core/engine/gateway-actions/whatsapp-account.ts +1 -1
- package/src/core/engine/gateway-auth.ts +164 -0
- package/src/core/engine/gateway-routes.ts +100 -5
- package/src/core/engine/gateway.ts +12 -4
- package/src/core/mcp-hub/guest-scope.ts +170 -29
- package/src/core/mcp-hub/index.ts +33 -16
- package/src/core/mcp-hub/talon-server.ts +71 -14
- package/src/core/mesh/credentials/admin.ts +146 -0
- package/src/core/mesh/credentials/index.ts +19 -0
- package/src/core/mesh/credentials/store.ts +443 -0
- package/src/core/mesh/credentials/token.ts +45 -0
- package/src/core/mesh/credentials/types.ts +83 -0
- package/src/core/mesh/devices/service.ts +58 -6
- package/src/core/mesh/links/bridge-links.ts +46 -4
- package/src/core/mesh/links/node-binaries.ts +1 -1
- package/src/core/mesh/links/node-provision.ts +8 -1
- package/src/core/models/active-model.ts +1 -1
- package/src/core/plugin/loader.ts +4 -0
- package/src/core/plugin/mcp.ts +4 -0
- package/src/core/tools/bridge.ts +2 -1
- package/src/core/types.ts +13 -0
- package/src/core/weaver/weaver.ts +47 -15
- package/src/frontend/discord/callbacks/components/agent-buttons.ts +1 -0
- package/src/frontend/discord/handlers/delivery.ts +3 -0
- package/src/frontend/discord/handlers/queue.ts +1 -0
- package/src/frontend/native/bridge/auth-guard.ts +277 -0
- package/src/frontend/native/bridge/auth.ts +84 -1
- package/src/frontend/native/bridge/credentials/claims.ts +51 -0
- package/src/frontend/native/bridge/credentials/principal.ts +200 -0
- package/src/frontend/native/bridge/credentials/upgrade.ts +129 -0
- package/src/frontend/native/bridge/routes/auth.ts +37 -0
- package/src/frontend/native/bridge/routes/chats.ts +20 -2
- package/src/frontend/native/bridge/routes/host.ts +11 -1
- package/src/frontend/native/bridge/routes/index.ts +2 -0
- package/src/frontend/native/bridge/routes/mesh.ts +72 -15
- package/src/frontend/native/bridge/routes/table.ts +73 -51
- package/src/frontend/native/bridge/server.ts +266 -83
- package/src/frontend/native/index.ts +54 -3
- package/src/frontend/native/turn/turn.ts +2 -0
- package/src/frontend/teams/turn.ts +1 -0
- package/src/frontend/telegram/actions/outgoing-log.ts +70 -0
- package/src/frontend/telegram/actions/send.ts +4 -0
- package/src/frontend/telegram/admin.ts +20 -0
- package/src/frontend/telegram/commands/admin.ts +1 -1
- package/src/frontend/telegram/commands/state.ts +7 -9
- package/src/frontend/telegram/handlers/access.ts +31 -10
- package/src/frontend/telegram/handlers/delivery.ts +14 -1
- package/src/frontend/telegram/handlers/group-access.ts +50 -0
- package/src/frontend/telegram/handlers/messages.ts +1 -0
- package/src/frontend/telegram/handlers/queue.ts +14 -0
- package/src/frontend/telegram/handlers/state.ts +2 -2
- package/src/frontend/telegram/index.ts +32 -9
- package/src/frontend/telegram/middleware.ts +2 -2
- package/src/frontend/telegram/polling/poll-deadline.ts +52 -0
- package/src/frontend/telegram/{stale-command.ts → polling/stale-command.ts} +1 -1
- package/src/frontend/telegram/{update-offset.ts → polling/update-offset.ts} +1 -1
- package/src/frontend/telegram/userbot.ts +103 -10
- package/src/frontend/terminal/index.ts +8 -1
- package/src/frontend/whatsapp/commands.ts +3 -3
- package/src/frontend/whatsapp/messages/inbound.ts +1 -0
- package/src/plugins/playwright/index.ts +1 -1
- package/src/storage/backup/index.ts +1 -1
- 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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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 (
|
|
43
|
-
const body = await readJson(req);
|
|
44
|
-
|
|
45
|
-
|
|
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 (
|
|
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, {
|
|
101
|
+
json(ctx.res, 200, {
|
|
102
|
+
ok: h.completeCommand(withDevice(body, "deviceId", acting.deviceId)),
|
|
103
|
+
});
|
|
53
104
|
},
|
|
54
|
-
"POST /devices/file": async (
|
|
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,
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
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",
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"
|
|
42
|
-
"POST /
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"GET /
|
|
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": "
|
|
55
|
-
"GET /memory/why": "
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
"GET /
|
|
60
|
-
"
|
|
61
|
-
"POST /
|
|
62
|
-
"
|
|
63
|
-
"POST /
|
|
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
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
"
|
|
81
|
-
"POST /
|
|
82
|
-
"POST /devices/
|
|
83
|
-
"
|
|
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>;
|