talon-agent 5.14.0 → 5.18.1
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 +1 -1
- package/README.md +8 -5
- package/package.json +3 -3
- 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/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 +56 -5
- 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
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
[](#frontends)
|
|
11
11
|
[](#backends)
|
|
12
12
|
[](LICENSE)
|
|
13
|
-
[](https://github.com/thefalconry/talon/actions/workflows/ci.yml)
|
|
14
14
|
|
|
15
15
|
Multi-platform agentic AI harness. Runs on **Telegram**, **WhatsApp**, **Discord**, **Microsoft Teams**, the **Terminal**, and a **cross-platform Desktop/Mobile companion app** (Flutter), with a pluggable backend (**Claude Agent SDK**, **Kilo**, **OpenCode**, **Codex**, **Antigravity**, or **OpenAI Agents**) and full tool access through MCP.
|
|
16
16
|
|
|
@@ -39,7 +39,7 @@ Multi-platform agentic AI harness. Runs on **Telegram**, **WhatsApp**, **Discord
|
|
|
39
39
|
## Quick Start
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
git clone https://github.com/
|
|
42
|
+
git clone https://github.com/thefalconry/talon.git && cd talon
|
|
43
43
|
npm install
|
|
44
44
|
|
|
45
45
|
# Interactive setup (select frontend, configure tokens, pick model)
|
|
@@ -73,7 +73,7 @@ modules are embedded in the binary.
|
|
|
73
73
|
|
|
74
74
|
```bash
|
|
75
75
|
# Homebrew (macOS / Linux)
|
|
76
|
-
brew install
|
|
76
|
+
brew install thefalconry/talon/talon
|
|
77
77
|
|
|
78
78
|
# Debian / Ubuntu — download the .deb for your arch from the release, then:
|
|
79
79
|
sudo apt install ./talon_<version>_amd64.deb # or _arm64.deb
|
|
@@ -498,6 +498,9 @@ Config file: `~/.talon/config.json`
|
|
|
498
498
|
| `disabledTools` | --- | Hide individual tools by name (`end_turn` cannot be disabled) |
|
|
499
499
|
| `adminUserId` | --- | Telegram user ID for `/admin` commands |
|
|
500
500
|
| `allowedUsers` | --- | Whitelist of Telegram user IDs |
|
|
501
|
+
| `allowedGroups` | --- | Telegram group IDs the bot serves. Unset: groups the admin is in are still admitted (legacy, warned at startup) |
|
|
502
|
+
| `operatorIds` | --- | Extra operator identities (`"123"`, `"wa_dm_<number>"`, `"discord:<id>"`, `"teams:<id>"`) — only operators get the full tool set |
|
|
503
|
+
| `guestDmScope` | on | Conversation-only tools for non-operator senders. Always applied in groups; `enabled: false` exempts DMs (legacy) |
|
|
501
504
|
| `apiId` / `apiHash` | --- | Telegram API credentials for full message history |
|
|
502
505
|
| `whatsapp` | --- | WhatsApp frontend: pairing, allowlists, group policy ([above](#whatsapp)) |
|
|
503
506
|
| `discord` | --- | Discord frontend: bot token, application ID, guild / channel allowlists |
|
|
@@ -564,7 +567,7 @@ Commands: `/model`, `/effort`, `/context`, `/status`, `/reset`, `/rename`, `/res
|
|
|
564
567
|
|
|
565
568
|
## Production
|
|
566
569
|
|
|
567
|
-
**Docker:** the image runs the daemon on Bun (`bun src/index.ts`); `~/.talon` and `~/.claude` are bind-mounted from the host into the container's `HOME=/home/bun`. Prebuilt images are on GHCR (`ghcr.io/
|
|
570
|
+
**Docker:** the image runs the daemon on Bun (`bun src/index.ts`); `~/.talon` and `~/.claude` are bind-mounted from the host into the container's `HOME=/home/bun`. Prebuilt images are on GHCR (`ghcr.io/thefalconry/talon:latest`), and a first boot can be configured entirely from `TALON_*` environment variables — see **[docs/docker.md](docs/docker.md)** for the quick install, and **[docs/truenas.md](docs/truenas.md)** for a step-by-step TrueNAS SCALE install.
|
|
568
571
|
|
|
569
572
|
```bash
|
|
570
573
|
docker compose up -d
|
|
@@ -576,7 +579,7 @@ The image is also ready for the **Antigravity (`agy`) backend**: add `-f docker-
|
|
|
576
579
|
|
|
577
580
|
**Systemd:** unit file at `packaging/systemd/talon.service` — copy to `/etc/systemd/system/`, set `User=` and `WorkingDirectory=`, then `systemctl enable --now talon`.
|
|
578
581
|
|
|
579
|
-
**Health endpoint:** `GET http://localhost:19876/health` returns
|
|
582
|
+
**Health endpoint:** `GET http://localhost:19876/health` returns the daemon's identity (`app`, `mode`, `pid`, `port`, `startedAt`, `ok`). With the gateway token (`Authorization: Bearer $(cat ~/.talon/keys/gateway-token)`) it also returns uptime, memory, queue depth, active sessions, and last activity timestamp. Every other gateway route requires that token.
|
|
580
583
|
|
|
581
584
|
**Logging:** Structured JSON via pino to `~/.talon/talon.log`. Rotated on startup when the file exceeds 10MB.
|
|
582
585
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "talon-agent",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.18.1",
|
|
4
4
|
"description": "Multi-frontend AI agent with full tool access, streaming, cron jobs, and plugin system",
|
|
5
|
-
"author": "
|
|
5
|
+
"author": "The Falconry",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/
|
|
9
|
+
"url": "https://github.com/thefalconry/talon.git"
|
|
10
10
|
},
|
|
11
11
|
"keywords": [
|
|
12
12
|
"telegram",
|
|
@@ -40,7 +40,7 @@ is a primary responsibility, not an optional extra. Goal tools
|
|
|
40
40
|
(`update_goal`, `list_goals`, …) require an explicit `chat_id` parameter
|
|
41
41
|
in heartbeat mode — use the chat id shown next to each goal.
|
|
42
42
|
|
|
43
|
-
OUTBOUND MESSAGING: You also have access to the frontend tool servers — {{toolList}} — which expose `send`, `react`, and the rest of the messaging surface. Because there is NO ambient chat in heartbeat mode, every outbound tool call MUST include an explicit `chat_id` parameter. The bridge promotes that chat_id to the routing target, so `send(type="text", text="...", chat_id=N)` from `{{exampleFrontend}}-tools` delivers a message to chat N on that frontend. Known chat IDs live in your memory.md (per-frontend — for Telegram,
|
|
43
|
+
OUTBOUND MESSAGING: You also have access to the frontend tool servers — {{toolList}} — which expose `send`, `react`, and the rest of the messaging surface. Because there is NO ambient chat in heartbeat mode, every outbound tool call MUST include an explicit `chat_id` parameter. The bridge promotes that chat_id to the routing target, so `send(type="text", text="...", chat_id=N)` from `{{exampleFrontend}}-tools` delivers a message to chat N on that frontend. Known chat IDs live in your memory.md (per-frontend — for Telegram, Ada's DM ID and group IDs are recorded; other frontends list their own). Without `chat_id`, the gateway returns 'No active chat context and no explicit numeric chat_id'. Cross-frontend: every tool server also carries `send_via(frontend=..., target=..., text=...)`, which delivers through ANY enabled frontend — target is the numeric chat id as a string, or for WhatsApp a phone number ("+353871234567") or wa_dm_/wa_group_ id.
|
|
44
44
|
|
|
45
45
|
Reaching out is part of the job, not an exception: when a run turns up
|
|
46
46
|
something a user would genuinely want to know — a goal completed or
|
|
@@ -52,6 +52,9 @@ import {
|
|
|
52
52
|
const agyFactory: BackendFactory = {
|
|
53
53
|
id: "agy",
|
|
54
54
|
label: "Antigravity",
|
|
55
|
+
// Its own shell/file tools can't be withheld per turn, so guest-scoped
|
|
56
|
+
// turns (non-operator senders) are refused rather than run with them.
|
|
57
|
+
guestToolScope: "refused",
|
|
55
58
|
doctor: (config, isActive) => agyDoctorChecks(config, isActive),
|
|
56
59
|
|
|
57
60
|
async init(config, ctx) {
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
} from "../../../core/mcp-hub/index.js";
|
|
46
46
|
import { frontendsForChat } from "../../runtime/frontends.js";
|
|
47
47
|
import { logWarn } from "../../../util/log.js";
|
|
48
|
+
import { gatewayAuthHeaders } from "../../../core/engine/gateway-auth.js";
|
|
48
49
|
import { AGY_MCP_PREFIX } from "../constants.js";
|
|
49
50
|
|
|
50
51
|
// ── Paths ───────────────────────────────────────────────────────────────────
|
|
@@ -77,6 +78,8 @@ function agyMcpSnapshotDir(override?: string): string {
|
|
|
77
78
|
*/
|
|
78
79
|
export interface AgyMcpServer {
|
|
79
80
|
disabled: boolean;
|
|
81
|
+
/** Gateway bearer token (`agy mcp add --header` writes this shape). */
|
|
82
|
+
headers: Record<string, string>;
|
|
80
83
|
serverUrl: string;
|
|
81
84
|
}
|
|
82
85
|
|
|
@@ -129,8 +132,13 @@ export function buildAgyMcpServers(args: {
|
|
|
129
132
|
const { chatId, bridgeUrl, frontends, braveApiKey } = args;
|
|
130
133
|
const scope = args.scope ?? agyScopeSlug(chatId);
|
|
131
134
|
const servers: Record<string, AgyMcpServer> = {};
|
|
135
|
+
const headers = gatewayAuthHeaders();
|
|
132
136
|
const add = (name: string, url: string) => {
|
|
133
|
-
servers[agyServerKey(scope, name)] = {
|
|
137
|
+
servers[agyServerKey(scope, name)] = {
|
|
138
|
+
disabled: false,
|
|
139
|
+
headers,
|
|
140
|
+
serverUrl: url,
|
|
141
|
+
};
|
|
134
142
|
};
|
|
135
143
|
|
|
136
144
|
for (const frontend of frontendsForChat(chatId, frontends)) {
|
|
@@ -198,7 +206,11 @@ function writeConfigAtomic(
|
|
|
198
206
|
mkdirSync(dirname(path), { recursive: true });
|
|
199
207
|
const tmp = `${path}.talon-${randomBytes(6).toString("hex")}.tmp`;
|
|
200
208
|
try {
|
|
201
|
-
|
|
209
|
+
// Owner-only: Talon's entries carry the gateway token.
|
|
210
|
+
writeFileSync(tmp, `${JSON.stringify(file, null, 2)}\n`, {
|
|
211
|
+
encoding: "utf-8",
|
|
212
|
+
mode: 0o600,
|
|
213
|
+
});
|
|
202
214
|
renameSync(tmp, path);
|
|
203
215
|
} catch (err) {
|
|
204
216
|
try {
|
|
@@ -41,6 +41,9 @@ const claudeSdkFactory: BackendFactory = {
|
|
|
41
41
|
// no migration is needed.
|
|
42
42
|
id: "claude",
|
|
43
43
|
label: "Anthropic",
|
|
44
|
+
// Guest turns run with no SDK built-ins and only guest-allowed MCP
|
|
45
|
+
// servers (see options.ts), so the hub's guest scope is the whole surface.
|
|
46
|
+
guestToolScope: "enforced",
|
|
44
47
|
doctor: (config, isActive) => claudeDoctorChecks(config, isActive),
|
|
45
48
|
|
|
46
49
|
async init(config, ctx) {
|
|
@@ -32,10 +32,14 @@ import {
|
|
|
32
32
|
import { toolFingerprint } from "../runtime/cache/cache-telemetry.js";
|
|
33
33
|
import { reportToolFingerprint } from "../runtime/cache/cache-metrics.js";
|
|
34
34
|
import { log, logError } from "../../util/log.js";
|
|
35
|
+
import {
|
|
36
|
+
GATEWAY_TOKEN_ENV,
|
|
37
|
+
gatewayToken,
|
|
38
|
+
} from "../../core/engine/gateway-auth.js";
|
|
35
39
|
import { getConfig, getBridgePort } from "./state.js";
|
|
36
40
|
import { ALLOWED_TOOLS_CHAT, EFFORT_MAP } from "./constants.js";
|
|
37
41
|
import {
|
|
38
|
-
|
|
42
|
+
isGuestTurn,
|
|
39
43
|
isGuestPluginAllowed,
|
|
40
44
|
} from "../../core/mcp-hub/guest-scope.js";
|
|
41
45
|
import { VALID_TOOL_FRONTENDS } from "../../core/mcp-hub/talon-server.js";
|
|
@@ -92,6 +96,8 @@ export function getActiveFrontends(): readonly string[] {
|
|
|
92
96
|
export type HubMcpEntry = {
|
|
93
97
|
type: "http";
|
|
94
98
|
url: string;
|
|
99
|
+
/** Gateway bearer token — see `hubAuthHeaders`. */
|
|
100
|
+
headers: Record<string, string>;
|
|
95
101
|
alwaysLoad?: boolean;
|
|
96
102
|
/** Per-server tool-call timeout in ms (overrides MCP_TOOL_TIMEOUT). */
|
|
97
103
|
timeout?: number;
|
|
@@ -106,6 +112,18 @@ export type HubMcpEntry = {
|
|
|
106
112
|
*/
|
|
107
113
|
const FRONTEND_TOOL_CALL_TIMEOUT_MS = 3_900_000; // 65 min
|
|
108
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Hub auth header for the Claude CLI's MCP config. The SDK hands that config
|
|
117
|
+
* to the CLI on its command line, which other local accounts can read — so
|
|
118
|
+
* the header carries a `${TALON_GATEWAY_TOKEN}` reference the CLI expands
|
|
119
|
+
* from its environment (inherited from the daemon), never the value itself.
|
|
120
|
+
* `gatewayToken()` runs first so the variable is guaranteed to be exported.
|
|
121
|
+
*/
|
|
122
|
+
function hubAuthHeaders(): Record<string, string> {
|
|
123
|
+
gatewayToken();
|
|
124
|
+
return { Authorization: `Bearer \${${GATEWAY_TOKEN_ENV}}` };
|
|
125
|
+
}
|
|
126
|
+
|
|
109
127
|
/**
|
|
110
128
|
* Build the MCP servers map for a chat query.
|
|
111
129
|
* Includes frontend-specific tool servers and Brave Search, if configured.
|
|
@@ -130,6 +148,7 @@ export function buildMcpServers(chatId: string): Record<string, HubMcpEntry> {
|
|
|
130
148
|
servers[`${frontend}-tools`] = {
|
|
131
149
|
type: "http",
|
|
132
150
|
url: talonHubUrl(bridgeUrl, frontend, chatId),
|
|
151
|
+
headers: hubAuthHeaders(),
|
|
133
152
|
// Always include the frontend's tools in the turn-1 prompt instead of
|
|
134
153
|
// deferring them behind the SDK's tool search. These are the bot's
|
|
135
154
|
// primary surface — it needs `end_turn`/`send`/`react` on EVERY turn to
|
|
@@ -155,6 +174,7 @@ export function buildMcpServers(chatId: string): Record<string, HubMcpEntry> {
|
|
|
155
174
|
servers["brave-search"] = {
|
|
156
175
|
type: "http",
|
|
157
176
|
url: pluginHubUrl(bridgeUrl, "brave-search", chatId),
|
|
177
|
+
headers: hubAuthHeaders(),
|
|
158
178
|
};
|
|
159
179
|
}
|
|
160
180
|
|
|
@@ -176,6 +196,7 @@ export function buildPluginMcpServers(
|
|
|
176
196
|
servers[name] = {
|
|
177
197
|
type: "http",
|
|
178
198
|
url: pluginHubUrl(bridgeUrl, name, chatId),
|
|
199
|
+
headers: hubAuthHeaders(),
|
|
179
200
|
};
|
|
180
201
|
}
|
|
181
202
|
return servers;
|
|
@@ -393,10 +414,10 @@ export function buildSdkOptions(
|
|
|
393
414
|
const { postToolUseFailureHook, postToolBatchHook } =
|
|
394
415
|
buildTurnTerminatorHooks();
|
|
395
416
|
|
|
396
|
-
// Guest
|
|
417
|
+
// Guest turns (non-operator sender) get no SDK built-ins at all — Bash/Read/
|
|
397
418
|
// Write/Skill live outside the hub, so the hub's guest scope can't hide
|
|
398
419
|
// them — and only the plugin servers the guest scope allows.
|
|
399
|
-
const guest =
|
|
420
|
+
const guest = isGuestTurn(chatId);
|
|
400
421
|
const builtinTools = guest
|
|
401
422
|
? []
|
|
402
423
|
: config.nativeTools
|
|
@@ -42,6 +42,9 @@ import {
|
|
|
42
42
|
const codexFactory: BackendFactory = {
|
|
43
43
|
id: "codex",
|
|
44
44
|
label: "Codex",
|
|
45
|
+
// Its own shell/file tools can't be withheld per turn, so guest-scoped
|
|
46
|
+
// turns (non-operator senders) are refused rather than run with them.
|
|
47
|
+
guestToolScope: "refused",
|
|
45
48
|
doctor: (config) => codexDoctorChecks(config),
|
|
46
49
|
|
|
47
50
|
async init(config, ctx) {
|
|
@@ -30,6 +30,7 @@ import { getState } from "./state.js";
|
|
|
30
30
|
import { asCodexConfig, buildCodexMcpServers } from "./mcp-config.js";
|
|
31
31
|
import { detectCodexAuth, type CodexAuthInfo } from "./auth.js";
|
|
32
32
|
import { startDiscovery } from "./discovery.js";
|
|
33
|
+
import { gatewayToken } from "../../core/engine/gateway-auth.js";
|
|
33
34
|
import {
|
|
34
35
|
computeAuthFingerprint,
|
|
35
36
|
loadOAuthIncompatStore,
|
|
@@ -199,6 +200,9 @@ export function ensureCodex(chatId: string): Codex {
|
|
|
199
200
|
const codexPathOverride =
|
|
200
201
|
process.env.TALON_CODEX_BINARY || state.config.codexBinary || undefined;
|
|
201
202
|
|
|
203
|
+
// Export TALON_GATEWAY_TOKEN before snapshotting the env: the MCP entries
|
|
204
|
+
// name it via `bearer_token_env_var`.
|
|
205
|
+
gatewayToken();
|
|
202
206
|
const codex = new Codex({
|
|
203
207
|
apiKey,
|
|
204
208
|
baseUrl,
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
hubPluginServerNames,
|
|
31
31
|
} from "../../core/mcp-hub/index.js";
|
|
32
32
|
import type { ToolExclusionConfig } from "../../core/tools/mcp-env.js";
|
|
33
|
+
import { GATEWAY_TOKEN_ENV } from "../../core/engine/gateway-auth.js";
|
|
33
34
|
import { frontendsForChat } from "../runtime/frontends.js";
|
|
34
35
|
|
|
35
36
|
/**
|
|
@@ -69,6 +70,12 @@ export interface CodexMcpServer {
|
|
|
69
70
|
* Serialised as `default_tools_approval_mode` in the TOML config.
|
|
70
71
|
*/
|
|
71
72
|
default_tools_approval_mode?: CodexToolApprovalMode;
|
|
73
|
+
/**
|
|
74
|
+
* Env var holding the hub's bearer token. Codex reads the value from its
|
|
75
|
+
* own environment (inherited from the daemon), so the token never lands
|
|
76
|
+
* in the `--config` flags on its command line.
|
|
77
|
+
*/
|
|
78
|
+
bearer_token_env_var: string;
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
/**
|
|
@@ -140,6 +147,7 @@ export function buildCodexMcpServers(args: {
|
|
|
140
147
|
servers[`${frontend}-tools`] = {
|
|
141
148
|
url: talonHubUrl(bridgeUrl, frontend, chatId),
|
|
142
149
|
default_tools_approval_mode: TALON_MCP_DEFAULT_APPROVAL,
|
|
150
|
+
bearer_token_env_var: GATEWAY_TOKEN_ENV,
|
|
143
151
|
};
|
|
144
152
|
}
|
|
145
153
|
|
|
@@ -149,6 +157,7 @@ export function buildCodexMcpServers(args: {
|
|
|
149
157
|
servers["brave-search"] = {
|
|
150
158
|
url: pluginHubUrl(bridgeUrl, "brave-search", chatId),
|
|
151
159
|
default_tools_approval_mode: TALON_MCP_DEFAULT_APPROVAL,
|
|
160
|
+
bearer_token_env_var: GATEWAY_TOKEN_ENV,
|
|
152
161
|
};
|
|
153
162
|
}
|
|
154
163
|
|
|
@@ -157,6 +166,7 @@ export function buildCodexMcpServers(args: {
|
|
|
157
166
|
servers[name] = {
|
|
158
167
|
url: pluginHubUrl(bridgeUrl, name, chatId),
|
|
159
168
|
default_tools_approval_mode: TALON_MCP_DEFAULT_APPROVAL,
|
|
169
|
+
bearer_token_env_var: GATEWAY_TOKEN_ENV,
|
|
160
170
|
};
|
|
161
171
|
}
|
|
162
172
|
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* Keyed by auth fingerprint
|
|
40
40
|
* ─────────────────────────
|
|
41
41
|
*
|
|
42
|
-
* If
|
|
42
|
+
* If Ada switches OAuth accounts (e.g. work vs personal) the cache
|
|
43
43
|
* file at `~/.codex/auth.json` changes, and what we learned about
|
|
44
44
|
* account A doesn't necessarily apply to account B. To stay correct
|
|
45
45
|
* across credential changes we tag the store with a fingerprint of the
|
|
@@ -71,7 +71,7 @@ interface CodexTokenTotals {
|
|
|
71
71
|
* "rate_limit_reached_type": null
|
|
72
72
|
* }
|
|
73
73
|
*
|
|
74
|
-
* Exhausted (the silent-exit case on
|
|
74
|
+
* Exhausted (the silent-exit case on Ada's free ChatGPT OAuth):
|
|
75
75
|
* {
|
|
76
76
|
* "limit_id": "premium",
|
|
77
77
|
* "primary": null,
|
|
@@ -242,7 +242,7 @@ export async function readLastTokenCount(
|
|
|
242
242
|
* Returns `null` only when the file can't be found or no `token_count`
|
|
243
243
|
* event exists in it. Returns `{ usage: undefined, rateLimits: {...} }`
|
|
244
244
|
* when a usage-exhausted turn writes a `token_count` event with
|
|
245
|
-
* `info: null` but populated rate_limits (the literal shape on
|
|
245
|
+
* `info: null` but populated rate_limits (the literal shape on Ada's
|
|
246
246
|
* exhausted OAuth account, 2026-05-21).
|
|
247
247
|
*/
|
|
248
248
|
export async function readLastRolloutSnapshot(
|
|
@@ -40,6 +40,9 @@ import {
|
|
|
40
40
|
const openAIAgentsFactory: BackendFactory = {
|
|
41
41
|
id: "openai-agents",
|
|
42
42
|
label: "OpenAI Agents",
|
|
43
|
+
// Its own shell/file tools can't be withheld per turn, so guest-scoped
|
|
44
|
+
// turns (non-operator senders) are refused rather than run with them.
|
|
45
|
+
guestToolScope: "refused",
|
|
43
46
|
doctor: (config) => openAIAgentsDoctorChecks(config),
|
|
44
47
|
|
|
45
48
|
async init(config, ctx) {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
import { connectMcpServers, MCPServerStreamableHttp } from "@openai/agents";
|
|
27
27
|
import type { MCPServer } from "@openai/agents";
|
|
28
|
+
import { gatewayAuthHeaders } from "../../core/engine/gateway-auth.js";
|
|
28
29
|
import {
|
|
29
30
|
talonHubUrl,
|
|
30
31
|
pluginHubUrl,
|
|
@@ -185,6 +186,7 @@ async function buildBundle(args: BundleInputs): Promise<OpenAIAgentsMcpBundle> {
|
|
|
185
186
|
name: `${frontend}-tools`,
|
|
186
187
|
url: talonHubUrl(bridgeUrl, frontend, chatId),
|
|
187
188
|
cacheToolsList: true,
|
|
189
|
+
requestInit: { headers: gatewayAuthHeaders() },
|
|
188
190
|
}),
|
|
189
191
|
);
|
|
190
192
|
}
|
|
@@ -197,6 +199,7 @@ async function buildBundle(args: BundleInputs): Promise<OpenAIAgentsMcpBundle> {
|
|
|
197
199
|
name: "brave-search",
|
|
198
200
|
url: pluginHubUrl(bridgeUrl, "brave-search", chatId),
|
|
199
201
|
cacheToolsList: true,
|
|
202
|
+
requestInit: { headers: gatewayAuthHeaders() },
|
|
200
203
|
}),
|
|
201
204
|
);
|
|
202
205
|
}
|
|
@@ -208,6 +211,7 @@ async function buildBundle(args: BundleInputs): Promise<OpenAIAgentsMcpBundle> {
|
|
|
208
211
|
name,
|
|
209
212
|
url: pluginHubUrl(bridgeUrl, name, chatId),
|
|
210
213
|
cacheToolsList: true,
|
|
214
|
+
requestInit: { headers: gatewayAuthHeaders() },
|
|
211
215
|
}),
|
|
212
216
|
);
|
|
213
217
|
}
|
|
@@ -68,6 +68,9 @@ export function createRemoteBackendFactory(
|
|
|
68
68
|
return {
|
|
69
69
|
id,
|
|
70
70
|
label,
|
|
71
|
+
// The server's built-in shell/file tools can't be withheld per turn,
|
|
72
|
+
// so guest-scoped turns (non-operator senders) are refused.
|
|
73
|
+
guestToolScope: "refused",
|
|
71
74
|
// The SDK ships as an npm dep but only talks to a server it spawns from
|
|
72
75
|
// the CLI of the same name (`cross-spawn` → PATH lookup). A present
|
|
73
76
|
// package with an absent binary fails at the first turn with a bare
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
describeHubChildExit,
|
|
43
43
|
} from "../../core/mcp-hub/index.js";
|
|
44
44
|
import type { RemoteAgentClient } from "./client.js";
|
|
45
|
+
import { gatewayAuthHeaders } from "../../core/engine/gateway-auth.js";
|
|
45
46
|
import type { RemoteServerState } from "./state.js";
|
|
46
47
|
import { errMsg } from "./state.js";
|
|
47
48
|
import type { TalonConfig } from "../../core/config/index.js";
|
|
@@ -222,6 +223,7 @@ export async function ensureChatMcpServer<TClient extends RemoteAgentClient>(
|
|
|
222
223
|
name: serverName,
|
|
223
224
|
config: {
|
|
224
225
|
type: "remote",
|
|
226
|
+
headers: gatewayAuthHeaders(),
|
|
225
227
|
// Talon's MCP hub serves the chat's tool set in-process over
|
|
226
228
|
// streamable HTTP — no subprocess on the agent-server side, and
|
|
227
229
|
// the (frontend, chatId) binding travels in the URL instead of
|
|
@@ -305,6 +307,7 @@ export async function ensurePluginMcpServers<TClient extends RemoteAgentClient>(
|
|
|
305
307
|
name: serverName,
|
|
306
308
|
config: {
|
|
307
309
|
type: "remote",
|
|
310
|
+
headers: gatewayAuthHeaders(),
|
|
308
311
|
// Hub-managed child, shared across sessions and idle-reaped —
|
|
309
312
|
// the agent server holds an HTTP connection, not a subprocess.
|
|
310
313
|
url: pluginHubUrl(bridgeUrl, name, chatId),
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* doesn't" drift that motivated the shared-framework refactor.
|
|
11
11
|
*
|
|
12
12
|
* Examples:
|
|
13
|
-
* group: "[2026-05-15 11:01:23] [
|
|
13
|
+
* group: "[2026-05-15 11:01:23] [Ada (@ada)] [msg_id:2485]: actual text"
|
|
14
14
|
* DM: "[2026-05-15 11:01:23] [msg_id:2485] actual text"
|
|
15
15
|
* DM (no msg_id): "[2026-05-15 11:01:23] actual text"
|
|
16
16
|
*/
|
|
@@ -23,10 +23,10 @@ import { formatFullDatetime } from "../../../util/time.js";
|
|
|
23
23
|
export type PromptFormatInputs = {
|
|
24
24
|
/** Raw user text — passed through verbatim. */
|
|
25
25
|
text: string;
|
|
26
|
-
/** Display name of the sender (e.g. "
|
|
26
|
+
/** Display name of the sender (e.g. "Ada"). */
|
|
27
27
|
senderName: string;
|
|
28
28
|
/**
|
|
29
|
-
* Platform handle of the sender WITHOUT the leading `@` (e.g. `
|
|
29
|
+
* Platform handle of the sender WITHOUT the leading `@` (e.g. `ada`).
|
|
30
30
|
* Rendered next to the display name in group chats so the model can address
|
|
31
31
|
* or mention someone correctly — display names are not addressable, handles
|
|
32
32
|
* are. Absent for users who have no handle set.
|
package/src/bootstrap.ts
CHANGED
|
@@ -139,6 +139,10 @@ export async function bootstrap(
|
|
|
139
139
|
// trimming + brave key come from config; endpoints mount on the
|
|
140
140
|
// gateway HTTP server).
|
|
141
141
|
const { initHub } = await import("./core/mcp-hub/index.js");
|
|
142
|
+
const { setNativeToolsEnabled } =
|
|
143
|
+
await import("./core/engine/gateway-actions/native/index.js");
|
|
144
|
+
// The native shell/fs actions only answer when the tool set is on.
|
|
145
|
+
setNativeToolsEnabled(config.nativeTools);
|
|
142
146
|
initHub({
|
|
143
147
|
disabledTools: config.disabledTools,
|
|
144
148
|
disabledToolTags: config.disabledToolTags,
|
|
@@ -146,6 +150,10 @@ export async function bootstrap(
|
|
|
146
150
|
nativeTools: config.nativeTools,
|
|
147
151
|
guestDmScope: config.guestDmScope,
|
|
148
152
|
adminUserId: config.adminUserId,
|
|
153
|
+
operatorIds: [
|
|
154
|
+
...(config.operatorIds ?? []),
|
|
155
|
+
...(config.discord?.adminUserIds ?? []).map((id) => `discord:${id}`),
|
|
156
|
+
],
|
|
149
157
|
});
|
|
150
158
|
|
|
151
159
|
initWorkspace(config.workspace);
|
|
@@ -22,7 +22,12 @@
|
|
|
22
22
|
|
|
23
23
|
import pc from "picocolors";
|
|
24
24
|
import { createInterface } from "node:readline/promises";
|
|
25
|
-
import {
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
import {
|
|
27
|
+
getFrontends,
|
|
28
|
+
loadBackupConfig,
|
|
29
|
+
loadConfig,
|
|
30
|
+
} from "../../core/config/index.js";
|
|
26
31
|
import { findRunningInstance } from "../../core/daemon/discovery.js";
|
|
27
32
|
import {
|
|
28
33
|
collectBackupStatus,
|
|
@@ -40,7 +45,9 @@ import {
|
|
|
40
45
|
} from "../../core/backup/index.js";
|
|
41
46
|
import { resolveBackupSettings } from "../../core/backup/plan.js";
|
|
42
47
|
import { buildSnapshot } from "../../core/backup/snapshot.js";
|
|
43
|
-
import { pruneLocal } from "../../core/backup/store.js";
|
|
48
|
+
import { pruneLocal, reconcileIndex } from "../../core/backup/store.js";
|
|
49
|
+
import { generatePassphraseFile } from "../../core/backup/passphrase.js";
|
|
50
|
+
import { dirs } from "../../util/paths.js";
|
|
44
51
|
import { fetchGateway } from "../daemon-api.js";
|
|
45
52
|
|
|
46
53
|
const USAGE = `
|
|
@@ -50,10 +57,13 @@ const USAGE = `
|
|
|
50
57
|
${pc.cyan("list")} [--remote <targetId>] list snapshots
|
|
51
58
|
${pc.cyan("show")} <id> everything about one snapshot
|
|
52
59
|
${pc.cyan("pin")} <id> | ${pc.cyan("unpin")} <id> keep past retention, or release
|
|
53
|
-
${pc.cyan("restore")} <id> [--from <target>] [--yes]
|
|
60
|
+
${pc.cyan("restore")} <id> [--from <target>] [--yes] [--allow-unauthenticated]
|
|
61
|
+
restore (daemon must be stopped)
|
|
62
|
+
[--clone] …onto a new machine: relocate sessions + plugin paths
|
|
54
63
|
${pc.cyan("prune")} apply the local retention policy
|
|
55
64
|
${pc.cyan("targets")} remote targets and their readiness
|
|
56
65
|
${pc.cyan("status")} schedule, sizes, targets
|
|
66
|
+
${pc.cyan("keygen")} [path] new passphrase file (mode 600)
|
|
57
67
|
`;
|
|
58
68
|
|
|
59
69
|
type Flags = { values: string[]; flags: Map<string, string | true> };
|
|
@@ -169,6 +179,9 @@ async function backupNow(flags: Flags): Promise<void> {
|
|
|
169
179
|
|
|
170
180
|
async function backupList(flags: Flags): Promise<void> {
|
|
171
181
|
const target = flags.flags.get("remote");
|
|
182
|
+
// The manifests on disk are authoritative: a fresh host with a copied-in
|
|
183
|
+
// backups/ directory has an empty index until something reconciles it.
|
|
184
|
+
await reconcileIndex();
|
|
172
185
|
const snapshots = await listSnapshots();
|
|
173
186
|
const filtered =
|
|
174
187
|
typeof target === "string"
|
|
@@ -312,10 +325,23 @@ async function backupRestore(id: string, flags: Flags): Promise<void> {
|
|
|
312
325
|
return;
|
|
313
326
|
}
|
|
314
327
|
}
|
|
315
|
-
|
|
328
|
+
// Only the backup block: on a fresh host the real config is inside the
|
|
329
|
+
// snapshot, and a full load would reject the placeholder one.
|
|
330
|
+
const settings = resolveBackupSettings(loadBackupConfig());
|
|
316
331
|
const target =
|
|
317
332
|
typeof from === "string" ? await resolveTarget(from) : undefined;
|
|
318
|
-
|
|
333
|
+
if (typeof from === "string" && !target) {
|
|
334
|
+
process.exitCode = 1;
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const clone = flags.flags.get("clone") === true;
|
|
338
|
+
const report = await restoreSnapshot({
|
|
339
|
+
id,
|
|
340
|
+
settings,
|
|
341
|
+
target,
|
|
342
|
+
allowUnauthenticated: flags.flags.get("allow-unauthenticated") === true,
|
|
343
|
+
clone,
|
|
344
|
+
});
|
|
319
345
|
const written = Object.entries(report.written)
|
|
320
346
|
.map(([root, count]) => `${root} (${count})`)
|
|
321
347
|
.join(", ");
|
|
@@ -330,6 +356,12 @@ async function backupRestore(id: string, flags: Flags): Promise<void> {
|
|
|
330
356
|
console.log(
|
|
331
357
|
` Database: ${report.databaseReplaced ? "replaced" : "left in place"}\n`,
|
|
332
358
|
);
|
|
359
|
+
if (clone) {
|
|
360
|
+
console.log(
|
|
361
|
+
` Clone: config.json paths ${report.configRewritten ? "rewritten for this machine" : "unchanged"}.\n` +
|
|
362
|
+
` Reinstall fetched plugins from ${pc.cyan("~/.talon/plugins-manifest.json")} (docs/backups.md).\n`,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
333
365
|
}
|
|
334
366
|
|
|
335
367
|
/**
|
|
@@ -354,6 +386,27 @@ async function resolveTarget(id: string): Promise<BackupTarget | undefined> {
|
|
|
354
386
|
return target;
|
|
355
387
|
}
|
|
356
388
|
|
|
389
|
+
/** Write a fresh passphrase file. Prints where, never what. */
|
|
390
|
+
async function backupKeygen(path: string | undefined): Promise<void> {
|
|
391
|
+
try {
|
|
392
|
+
const dest = await generatePassphraseFile(
|
|
393
|
+
path ?? join(dirs.root, "backup.key"),
|
|
394
|
+
);
|
|
395
|
+
console.log(
|
|
396
|
+
` ${pc.green("●")} Wrote a new backup passphrase to ${pc.bold(dest)} (mode 600).\n` +
|
|
397
|
+
` Enable it in config.json:\n` +
|
|
398
|
+
` "backup": { "encryption": { "passphraseFile": "${dest}" } }\n` +
|
|
399
|
+
` Keep a copy OFF this machine — encrypted snapshots cannot be\n` +
|
|
400
|
+
` restored without it.\n`,
|
|
401
|
+
);
|
|
402
|
+
} catch (err) {
|
|
403
|
+
console.log(
|
|
404
|
+
` ${pc.red("●")} ${err instanceof Error ? err.message : String(err)}\n`,
|
|
405
|
+
);
|
|
406
|
+
process.exitCode = 1;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
357
410
|
// ── Entry ───────────────────────────────────────────────────────────────────
|
|
358
411
|
|
|
359
412
|
export async function runBackupCommand(argv: readonly string[]): Promise<void> {
|
|
@@ -386,6 +439,9 @@ export async function runBackupCommand(argv: readonly string[]): Promise<void> {
|
|
|
386
439
|
case "targets":
|
|
387
440
|
await backupTargets();
|
|
388
441
|
return;
|
|
442
|
+
case "keygen":
|
|
443
|
+
await backupKeygen(id);
|
|
444
|
+
return;
|
|
389
445
|
case "status":
|
|
390
446
|
case undefined:
|
|
391
447
|
await backupStatus();
|