talon-agent 5.13.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/presentation/session-status.ts +7 -4
- 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/src/util/version.ts +33 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `talon mesh` — per-device credentials from the outside.
|
|
3
|
+
*
|
|
4
|
+
* talon mesh [list] every credential + devices still on
|
|
5
|
+
* the shared token
|
|
6
|
+
* talon mesh revoke <device|credId> revoke now; live sessions drop
|
|
7
|
+
* talon mesh rotate <device> the device swaps credentials on its
|
|
8
|
+
* next heartbeat (old one: 7-day grace)
|
|
9
|
+
* talon mesh scopes <device> <list> e.g. device,client,operator
|
|
10
|
+
*
|
|
11
|
+
* Everything goes through the running daemon's loopback gateway
|
|
12
|
+
* (core/engine/gateway-routes.ts → core/mesh/credentials/admin.ts): the
|
|
13
|
+
* daemon owns the store and the live sessions a revocation must drop, so
|
|
14
|
+
* editing the file behind its back could never take effect in time.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import pc from "picocolors";
|
|
18
|
+
import { fetchGateway, requireGatewayPort } from "../daemon-api.js";
|
|
19
|
+
import type { DeviceCredential } from "../../core/mesh/credentials/index.js";
|
|
20
|
+
|
|
21
|
+
const USAGE = `
|
|
22
|
+
${pc.bold("talon mesh")} — per-device mesh credentials
|
|
23
|
+
|
|
24
|
+
${pc.cyan("list")} credentials, scopes, last use (default)
|
|
25
|
+
${pc.cyan("revoke")} <device|credential-id> revoke now; drops its live sessions
|
|
26
|
+
${pc.cyan("rotate")} <device> re-issue on the device's next heartbeat
|
|
27
|
+
${pc.cyan("scopes")} <device> <scope,scope> set scopes: device, client, operator
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
type Overview = {
|
|
31
|
+
ok: boolean;
|
|
32
|
+
error?: string;
|
|
33
|
+
credentials?: DeviceCredential[];
|
|
34
|
+
legacyDevices?: { deviceId: string; lastSeen: number }[];
|
|
35
|
+
legacySharedToken?: boolean;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type AdminReply = { ok: boolean; text?: string; error?: string };
|
|
39
|
+
|
|
40
|
+
function ago(ts: number | undefined, now: number): string {
|
|
41
|
+
if (ts === undefined) return "never";
|
|
42
|
+
const s = Math.max(0, Math.round((now - ts) / 1000));
|
|
43
|
+
if (s < 90) return `${s}s ago`;
|
|
44
|
+
if (s < 5400) return `${Math.round(s / 60)}m ago`;
|
|
45
|
+
if (s < 172_800) return `${Math.round(s / 3600)}h ago`;
|
|
46
|
+
return `${Math.round(s / 86_400)}d ago`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function stateOf(c: DeviceCredential, now: number): string {
|
|
50
|
+
if (c.revokedAt !== undefined) return pc.dim(`revoked (${c.revokeReason})`);
|
|
51
|
+
if (c.expiresAt !== undefined && now >= c.expiresAt) return pc.dim("expired");
|
|
52
|
+
if (c.supersededBy) return pc.yellow(`superseded by ${c.supersededBy}`);
|
|
53
|
+
if (c.rotateRequestedAt !== undefined) return pc.yellow("rotation pending");
|
|
54
|
+
if (c.deviceId === null) return pc.yellow("unbound (awaiting first use)");
|
|
55
|
+
return pc.green("active");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function renderOverview(o: Overview): void {
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
const creds = o.credentials ?? [];
|
|
61
|
+
console.log(`\n ${pc.bold("Mesh credentials")}\n`);
|
|
62
|
+
if (creds.length === 0) console.log(` ${pc.dim("(none issued yet)")}`);
|
|
63
|
+
for (const c of creds) {
|
|
64
|
+
console.log(
|
|
65
|
+
` ${pc.cyan(c.id)} ${c.deviceId ?? pc.dim("—")} [${c.scopes.join(", ")}] ${stateOf(c, now)}`,
|
|
66
|
+
);
|
|
67
|
+
console.log(
|
|
68
|
+
pc.dim(
|
|
69
|
+
` via ${c.origin}, issued ${ago(c.createdAt, now)}, last used ${ago(c.lastUsedAt, now)}`,
|
|
70
|
+
),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const legacy = o.legacyDevices ?? [];
|
|
74
|
+
if (legacy.length > 0) {
|
|
75
|
+
console.log(`\n ${pc.yellow("Still on the shared native.token:")}`);
|
|
76
|
+
for (const d of legacy) {
|
|
77
|
+
console.log(` ${d.deviceId} ${pc.dim(`seen ${ago(d.lastSeen, now)}`)}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
console.log(
|
|
81
|
+
`\n Legacy shared token from remote clients: ${
|
|
82
|
+
o.legacySharedToken ? pc.yellow("accepted") : pc.green("refused")
|
|
83
|
+
}\n`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function adminOp(
|
|
88
|
+
port: number,
|
|
89
|
+
body: Record<string, unknown>,
|
|
90
|
+
): Promise<void> {
|
|
91
|
+
const reply = (await fetchGateway(port, "/mesh/credentials", {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: { "Content-Type": "application/json" },
|
|
94
|
+
body: JSON.stringify(body),
|
|
95
|
+
})) as AdminReply;
|
|
96
|
+
if (reply.ok) {
|
|
97
|
+
console.log(`\n ${pc.green("✔")} ${reply.text}\n`);
|
|
98
|
+
} else {
|
|
99
|
+
console.error(`\n ${pc.red("✖")} ${reply.error}\n`);
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function runMeshCommand(args: string[]): Promise<void> {
|
|
105
|
+
const [sub = "list", target, scopes] = args;
|
|
106
|
+
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
107
|
+
console.log(USAGE);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const known = ["list", "revoke", "rotate", "scopes"];
|
|
111
|
+
if (!known.includes(sub)) {
|
|
112
|
+
console.error(` Unknown mesh command: ${sub}\n${USAGE}`);
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (sub !== "list" && !target) {
|
|
117
|
+
console.error(
|
|
118
|
+
` ${pc.red("✖")} talon mesh ${sub} needs a device.\n${USAGE}`,
|
|
119
|
+
);
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const port = await requireGatewayPort();
|
|
124
|
+
if (port === null) {
|
|
125
|
+
process.exitCode = 1;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (sub === "list") {
|
|
129
|
+
renderOverview((await fetchGateway(port, "/mesh/credentials")) as Overview);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
await adminOp(port, { op: sub, device: target, scopes });
|
|
133
|
+
}
|
package/src/cli/config.ts
CHANGED
|
@@ -46,6 +46,8 @@ export type Config = {
|
|
|
46
46
|
pulse: boolean;
|
|
47
47
|
pulseIntervalMs: number;
|
|
48
48
|
adminUserId?: number;
|
|
49
|
+
/** Telegram DM allowlist; defaults to `[adminUserId]` when set up. */
|
|
50
|
+
allowedUsers?: number[];
|
|
49
51
|
apiId?: number;
|
|
50
52
|
apiHash?: string;
|
|
51
53
|
maxMessageLength: number;
|
|
@@ -222,7 +224,7 @@ export function isConfigured(config: Config): boolean {
|
|
|
222
224
|
? config.frontend
|
|
223
225
|
: [config.frontend];
|
|
224
226
|
return fes.every((fe) => {
|
|
225
|
-
if (fe === "telegram") return !!config.botToken;
|
|
227
|
+
if (fe === "telegram") return !!config.botToken && !!config.adminUserId;
|
|
226
228
|
if (fe === "teams") return !!config.teamsWebhookUrl;
|
|
227
229
|
if (fe === "discord") return !!config.discord?.botToken;
|
|
228
230
|
// WhatsApp's credentials live in ~/.talon/whatsapp-auth/, not the
|
package/src/cli/daemon-api.ts
CHANGED
|
@@ -6,19 +6,41 @@
|
|
|
6
6
|
|
|
7
7
|
import pc from "picocolors";
|
|
8
8
|
import { findRunningInstance } from "../core/daemon/discovery.js";
|
|
9
|
+
import {
|
|
10
|
+
gatewayAuthHeaders,
|
|
11
|
+
gatewayTokenPath,
|
|
12
|
+
readGatewayToken,
|
|
13
|
+
} from "../core/engine/gateway-auth.js";
|
|
9
14
|
|
|
10
15
|
const REQUEST_TIMEOUT_MS = 3_000;
|
|
11
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Call a gateway endpoint as the operator: every request carries the gateway
|
|
19
|
+
* token (read from ~/.talon/keys/gateway-token), and a POST always declares a
|
|
20
|
+
* JSON body — the gateway refuses anything else.
|
|
21
|
+
*/
|
|
12
22
|
export async function fetchGateway(
|
|
13
23
|
port: number,
|
|
14
24
|
path: string,
|
|
15
25
|
init?: RequestInit,
|
|
16
26
|
timeoutMs: number = REQUEST_TIMEOUT_MS,
|
|
17
27
|
): Promise<unknown> {
|
|
28
|
+
const isPost = init?.method === "POST";
|
|
18
29
|
const response = await fetch(`http://127.0.0.1:${port}${path}`, {
|
|
19
30
|
...init,
|
|
31
|
+
...(isPost && init?.body === undefined ? { body: "{}" } : {}),
|
|
32
|
+
headers: {
|
|
33
|
+
...(isPost ? { "Content-Type": "application/json" } : {}),
|
|
34
|
+
...(init?.headers as Record<string, string> | undefined),
|
|
35
|
+
...gatewayAuthHeaders(readGatewayToken()),
|
|
36
|
+
},
|
|
20
37
|
signal: AbortSignal.timeout(timeoutMs),
|
|
21
38
|
});
|
|
39
|
+
if (response.status === 401) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`gateway refused the request (401) — is ${gatewayTokenPath()} readable by this user?`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
22
44
|
if (!response.ok) throw new Error(`gateway answered ${response.status}`);
|
|
23
45
|
return response.json();
|
|
24
46
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -39,6 +39,7 @@ import { runSkillCommand } from "./skill.js";
|
|
|
39
39
|
import { runMemoryCommand } from "./memory.js";
|
|
40
40
|
import { mainMenu } from "./menu.js";
|
|
41
41
|
import { runBackupCommand } from "./commands/backup.js";
|
|
42
|
+
import { runMeshCommand } from "./commands/mesh.js";
|
|
42
43
|
|
|
43
44
|
export * from "./context.js";
|
|
44
45
|
export * from "./config.js";
|
|
@@ -62,6 +63,7 @@ const CLI_COMMANDS = [
|
|
|
62
63
|
"skill",
|
|
63
64
|
"memory",
|
|
64
65
|
"backup",
|
|
66
|
+
"mesh",
|
|
65
67
|
];
|
|
66
68
|
|
|
67
69
|
/** `talon events [-f] [--history [N]]` → the tail options. */
|
|
@@ -110,6 +112,9 @@ function printHelp(): void {
|
|
|
110
112
|
console.log(
|
|
111
113
|
` ${pc.cyan("backup")} Snapshots and checkpoints (now/list/show/pin/restore)`,
|
|
112
114
|
);
|
|
115
|
+
console.log(
|
|
116
|
+
` ${pc.cyan("mesh")} Device credentials (list/revoke/rotate/scopes)`,
|
|
117
|
+
);
|
|
113
118
|
console.log(` ${pc.cyan("config")} View/edit configuration`);
|
|
114
119
|
console.log(` ${pc.cyan("logs")} Tail log file`);
|
|
115
120
|
console.log(` ${pc.cyan("doctor")} Validate environment`);
|
|
@@ -173,6 +178,7 @@ const COMMANDS: Record<string, CommandHandler> = {
|
|
|
173
178
|
kill: (args) => killTask(args[0]),
|
|
174
179
|
events: (args) => showEvents(eventsOptions(args)),
|
|
175
180
|
backup: (args) => runBackupCommand(args),
|
|
181
|
+
mesh: (args) => runMeshCommand(args),
|
|
176
182
|
plugin: (args) => runPluginCommand(args),
|
|
177
183
|
skill: (args) => runSkillCommand(args),
|
|
178
184
|
memory: (args) => runMemoryCommand(args),
|
|
@@ -10,13 +10,15 @@
|
|
|
10
10
|
* 4. anything else → { kind: "other" } — the caller
|
|
11
11
|
* decides (plugins treat it as an npm spec, skills reject it)
|
|
12
12
|
*
|
|
13
|
-
* Cloning always uses `--depth=1` (installs never need history)
|
|
13
|
+
* Cloning always uses `--depth=1` (installs never need history), ends the
|
|
14
|
+
* options with `--` so a URL can never be read as a git flag, and reports
|
|
15
|
+
* the commit it got so the install can be pinned/audited later. It spawns
|
|
14
16
|
* `git`/`npm` via cross-spawn, which resolves the `.cmd`/`.exe` shims on
|
|
15
17
|
* Windows — never assume a POSIX shell here.
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
20
|
import crossSpawn from "cross-spawn";
|
|
19
|
-
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
21
|
+
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
20
22
|
import { tmpdir } from "node:os";
|
|
21
23
|
import { join, resolve } from "node:path";
|
|
22
24
|
|
|
@@ -91,16 +93,49 @@ export function runTool(
|
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
export type CloneOutcome =
|
|
94
|
-
{ ok: true; dir: string; cleanup: () => void }
|
|
96
|
+
| { ok: true; dir: string; commit?: string; cleanup: () => void }
|
|
97
|
+
| { ok: false; error: string };
|
|
98
|
+
|
|
99
|
+
/** The checked-out commit of a clone, when git can tell us. */
|
|
100
|
+
function headCommit(dir: string): string | undefined {
|
|
101
|
+
const result = crossSpawn.sync("git", ["-C", dir, "rev-parse", "HEAD"], {
|
|
102
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
103
|
+
});
|
|
104
|
+
const sha = result.status === 0 ? result.stdout?.toString().trim() : "";
|
|
105
|
+
return sha && /^[0-9a-f]{40,64}$/.test(sha) ? sha : undefined;
|
|
106
|
+
}
|
|
95
107
|
|
|
96
108
|
/** Shallow-clone into a fresh temp directory. Caller must run `cleanup`. */
|
|
97
109
|
export function cloneShallow(url: string): CloneOutcome {
|
|
110
|
+
// `--` below already stops option parsing; refusing a leading dash too
|
|
111
|
+
// means a hostile "URL" never even reaches git.
|
|
112
|
+
if (url.startsWith("-")) {
|
|
113
|
+
return { ok: false, error: `Refusing a git URL that starts with "-"` };
|
|
114
|
+
}
|
|
98
115
|
const dir = mkdtempSync(join(tmpdir(), "talon-install-"));
|
|
99
116
|
const cleanup = () => rmSync(dir, { recursive: true, force: true });
|
|
100
|
-
const outcome = runTool("git", ["clone", "--depth=1", url, dir]);
|
|
117
|
+
const outcome = runTool("git", ["clone", "--depth=1", "--", url, dir]);
|
|
101
118
|
if (!outcome.ok) {
|
|
102
119
|
cleanup();
|
|
103
120
|
return { ok: false, error: `Clone failed: ${outcome.error}` };
|
|
104
121
|
}
|
|
105
|
-
return { ok: true, dir, cleanup };
|
|
122
|
+
return { ok: true, dir, commit: headCommit(dir), cleanup };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The file a git-installed plugin keeps its provenance in. */
|
|
126
|
+
const INSTALL_RECORD = ".talon-install.json";
|
|
127
|
+
|
|
128
|
+
/** Write where an install came from and exactly which commit it is. */
|
|
129
|
+
export function writeInstallRecord(
|
|
130
|
+
dir: string,
|
|
131
|
+
record: { source: string; subpath?: string; commit?: string },
|
|
132
|
+
): void {
|
|
133
|
+
writeFileSync(
|
|
134
|
+
join(dir, INSTALL_RECORD),
|
|
135
|
+
JSON.stringify(
|
|
136
|
+
{ ...record, installedAt: new Date().toISOString() },
|
|
137
|
+
null,
|
|
138
|
+
2,
|
|
139
|
+
) + "\n",
|
|
140
|
+
);
|
|
106
141
|
}
|
package/src/cli/plugin.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { fetchGateway } from "./daemon-api.js";
|
|
|
25
25
|
import { loadConfig, saveConfig, type Config } from "./config.js";
|
|
26
26
|
import {
|
|
27
27
|
cloneShallow,
|
|
28
|
+
writeInstallRecord,
|
|
28
29
|
resolveSource,
|
|
29
30
|
runTool,
|
|
30
31
|
type ResolvedSource,
|
|
@@ -258,7 +259,16 @@ function installFromGit(
|
|
|
258
259
|
}
|
|
259
260
|
// An install is a snapshot, not a checkout — updates go through
|
|
260
261
|
// `install --force`, so the clone's history has no business here.
|
|
262
|
+
// What it WAS a snapshot of is kept, so the exact commit is auditable.
|
|
261
263
|
rmSync(join(stage, ".git"), { recursive: true, force: true });
|
|
264
|
+
writeInstallRecord(stage, {
|
|
265
|
+
source: source.url,
|
|
266
|
+
...(source.subpath ? { subpath: source.subpath } : {}),
|
|
267
|
+
...(clone.commit ? { commit: clone.commit } : {}),
|
|
268
|
+
});
|
|
269
|
+
if (clone.commit) {
|
|
270
|
+
console.log(` ${pc.dim(`Commit ${clone.commit}`)}`);
|
|
271
|
+
}
|
|
262
272
|
|
|
263
273
|
if (existsSync(join(stage, "package.json"))) {
|
|
264
274
|
console.log(` ${pc.dim("Installing dependencies…")}`);
|
package/src/cli/setup.ts
CHANGED
|
@@ -94,6 +94,20 @@ type TelegramAnswers = Pick<
|
|
|
94
94
|
"botToken" | "adminId" | "apiId" | "apiHash"
|
|
95
95
|
>;
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* The Telegram admin id is required: it is the bot's owner (admin commands)
|
|
99
|
+
* and the default DM allowlist. The daemon refuses to start without it.
|
|
100
|
+
*/
|
|
101
|
+
export function validateTelegramAdminId(
|
|
102
|
+
value: string | undefined,
|
|
103
|
+
): string | undefined {
|
|
104
|
+
const v = value?.trim() ?? "";
|
|
105
|
+
if (!v) return "Required — the bot answers only its admin (and allowedUsers)";
|
|
106
|
+
if (!/^\d+$/.test(v) || Number(v) <= 0 || !Number.isSafeInteger(Number(v)))
|
|
107
|
+
return "Must be your numeric Telegram user ID";
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
97
111
|
async function askTelegram(config: Config): Promise<TelegramAnswers> {
|
|
98
112
|
const botToken = await askOrExit(
|
|
99
113
|
p.text({
|
|
@@ -110,8 +124,9 @@ async function askTelegram(config: Config): Promise<TelegramAnswers> {
|
|
|
110
124
|
const adminId = await askOrExit(
|
|
111
125
|
p.text({
|
|
112
126
|
message: "Your Telegram user ID",
|
|
113
|
-
placeholder: "
|
|
127
|
+
placeholder: "message @userinfobot to find yours",
|
|
114
128
|
initialValue: config.adminUserId ? String(config.adminUserId) : "",
|
|
129
|
+
validate: validateTelegramAdminId,
|
|
115
130
|
}),
|
|
116
131
|
);
|
|
117
132
|
|
|
@@ -619,6 +634,34 @@ export type SetupAnswers = {
|
|
|
619
634
|
whatsapp?: Config["whatsapp"];
|
|
620
635
|
};
|
|
621
636
|
|
|
637
|
+
/**
|
|
638
|
+
* Admin + DM allowlist for the saved config. With Telegram on, the admin id
|
|
639
|
+
* is the one just entered, and an empty allowlist defaults to that admin —
|
|
640
|
+
* a fresh bot answers its owner and no one else. With Telegram off, both are
|
|
641
|
+
* left as they were (other surfaces read `adminUserId` too).
|
|
642
|
+
*/
|
|
643
|
+
function telegramAccess(
|
|
644
|
+
existing: Config,
|
|
645
|
+
answers: SetupAnswers,
|
|
646
|
+
telegramOn: boolean,
|
|
647
|
+
): Pick<Config, "adminUserId" | "allowedUsers"> {
|
|
648
|
+
const adminUserId = answers.adminId
|
|
649
|
+
? parseInt(answers.adminId, 10) || undefined
|
|
650
|
+
: undefined;
|
|
651
|
+
if (!telegramOn || adminUserId === undefined) {
|
|
652
|
+
return {
|
|
653
|
+
adminUserId: adminUserId ?? existing.adminUserId,
|
|
654
|
+
allowedUsers: existing.allowedUsers,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
return {
|
|
658
|
+
adminUserId,
|
|
659
|
+
allowedUsers: existing.allowedUsers?.length
|
|
660
|
+
? existing.allowedUsers
|
|
661
|
+
: [adminUserId],
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
622
665
|
/**
|
|
623
666
|
* Fold the wizard's answers onto the existing config.
|
|
624
667
|
*
|
|
@@ -674,9 +717,7 @@ export function buildSetupConfig(
|
|
|
674
717
|
concurrency: existing.concurrency,
|
|
675
718
|
pulse: answers.pulse,
|
|
676
719
|
pulseIntervalMs: existing.pulseIntervalMs,
|
|
677
|
-
|
|
678
|
-
? parseInt(answers.adminId, 10) || undefined
|
|
679
|
-
: undefined,
|
|
720
|
+
...telegramAccess(existing, answers, on("telegram")),
|
|
680
721
|
apiId: answers.apiId,
|
|
681
722
|
apiHash: answers.apiHash,
|
|
682
723
|
maxMessageLength: existing.maxMessageLength,
|
package/src/cli/skill.ts
CHANGED
|
@@ -171,6 +171,9 @@ async function cmdInstall(args: string[]): Promise<void> {
|
|
|
171
171
|
return;
|
|
172
172
|
}
|
|
173
173
|
installed = installAll(dirs, force);
|
|
174
|
+
if (clone.commit) {
|
|
175
|
+
console.log(` ${pc.dim(`From ${resolved.url} @ ${clone.commit}`)}`);
|
|
176
|
+
}
|
|
174
177
|
} finally {
|
|
175
178
|
clone.cleanup();
|
|
176
179
|
}
|
|
@@ -76,6 +76,14 @@ export interface BackendFactory {
|
|
|
76
76
|
id: string;
|
|
77
77
|
/** Display label used in `/status` and agent logs (e.g. "Kilo"). */
|
|
78
78
|
label: string;
|
|
79
|
+
/**
|
|
80
|
+
* Can this backend run a guest-scoped turn (a non-operator sender) with
|
|
81
|
+
* nothing but the hub's conversation-only tools? `"enforced"` means its
|
|
82
|
+
* own built-ins (shell, file tools) are dropped for guest turns;
|
|
83
|
+
* `"refused"` means they can't be, so the weaver refuses guest turns on
|
|
84
|
+
* it. Absent is treated as `"refused"` — fail closed.
|
|
85
|
+
*/
|
|
86
|
+
guestToolScope?: "enforced" | "refused";
|
|
79
87
|
/** Initialise the backend; called exactly once per Talon process. */
|
|
80
88
|
init(config: TalonConfig, ctx: BackendInitContext): Promise<BackendInstance>;
|
|
81
89
|
/**
|
|
@@ -136,6 +144,14 @@ export function clearBackends(): void {
|
|
|
136
144
|
backends.clear();
|
|
137
145
|
}
|
|
138
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Whether guest-scoped turns may run on this backend. Unknown ids and
|
|
149
|
+
* factories that don't declare `guestToolScope` fail closed.
|
|
150
|
+
*/
|
|
151
|
+
export function backendEnforcesGuestScope(id: string): boolean {
|
|
152
|
+
return backends.get(id)?.guestToolScope === "enforced";
|
|
153
|
+
}
|
|
154
|
+
|
|
139
155
|
/** Whether a backend with this id is currently registered. */
|
|
140
156
|
export function hasBackend(id: string): boolean {
|
|
141
157
|
return backends.has(id);
|