talon-agent 3.0.5 → 3.1.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.
Files changed (63) hide show
  1. package/package.json +8 -5
  2. package/src/backend/kilo/models/index.ts +4 -1
  3. package/src/backend/kilo/server.ts +17 -4
  4. package/src/backend/openai-agents/session.ts +1 -1
  5. package/src/backend/opencode/models/index.ts +4 -1
  6. package/src/backend/opencode/server.ts +17 -4
  7. package/src/backend/shared/system-prompt.ts +5 -0
  8. package/src/cli/events.ts +1 -1
  9. package/src/cli/index.ts +1 -1
  10. package/src/cli/tasks.ts +5 -1
  11. package/src/core/background/triggers/pid.ts +28 -0
  12. package/src/core/background/triggers/resume.ts +4 -24
  13. package/src/core/background/triggers/spawn.ts +1 -1
  14. package/src/core/engine/gateway-actions/plugins.ts +3 -3
  15. package/src/core/prompt/invalidation.ts +23 -0
  16. package/src/core/types.ts +2 -2
  17. package/src/frontend/discord/callbacks/components.ts +1 -1
  18. package/src/frontend/discord/commands/settings.ts +1 -1
  19. package/src/frontend/native/auth.ts +52 -0
  20. package/src/frontend/native/extensions.ts +2 -2
  21. package/src/frontend/native/index.ts +8 -2
  22. package/src/frontend/native/server.ts +96 -15
  23. package/src/frontend/telegram/callbacks/model.ts +1 -1
  24. package/src/frontend/telegram/commands/admin.ts +1 -1
  25. package/src/frontend/telegram/commands/settings.ts +1 -1
  26. package/src/frontend/telegram/formatting.ts +1 -1
  27. package/src/frontend/terminal/commands.ts +1 -1
  28. package/src/native/htmlents-wasm-bytes.ts +4 -4
  29. package/src/native/htmlents.ts +4 -4
  30. package/src/native/registry.ts +9 -9
  31. package/src/native/runtime.ts +2 -2
  32. package/src/native/sqlguard-wasm-bytes.ts +4 -4
  33. package/src/native/sqlguard.ts +2 -2
  34. package/src/native/strsim-wasm-bytes.ts +4 -4
  35. package/src/native/strsim.ts +3 -3
  36. package/src/plugins/github/index.ts +1 -1
  37. package/src/plugins/mem0/index.ts +1 -1
  38. package/src/plugins/mempalace/index.ts +1 -1
  39. package/src/plugins/playwright/index.ts +1 -1
  40. package/src/storage/chat-settings.ts +3 -60
  41. package/src/storage/cron-store.ts +6 -80
  42. package/src/storage/goal-store.ts +10 -19
  43. package/src/storage/history.ts +3 -14
  44. package/src/storage/journal.ts +20 -9
  45. package/src/storage/media-index.ts +2 -22
  46. package/src/storage/repositories/chat-settings-repo.ts +55 -1
  47. package/src/storage/repositories/cron-repo.ts +82 -6
  48. package/src/storage/repositories/goals-repo.ts +20 -1
  49. package/src/storage/repositories/history-repo.ts +14 -1
  50. package/src/storage/repositories/media-index-repo.ts +23 -1
  51. package/src/storage/repositories/scripts-repo.ts +16 -1
  52. package/src/storage/repositories/sessions-repo.ts +1 -1
  53. package/src/storage/repositories/triggers-repo.ts +60 -5
  54. package/src/storage/script-store.ts +2 -15
  55. package/src/storage/session-record.ts +220 -0
  56. package/src/storage/sessions.ts +22 -210
  57. package/src/storage/trigger-store.ts +6 -60
  58. package/src/types/assets.d.ts +9 -0
  59. package/src/types/effort.ts +12 -0
  60. package/src/util/config.ts +9 -2
  61. package/src/util/harden.ts +50 -0
  62. package/src/util/log.ts +2 -1
  63. package/tsconfig.json +6 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "talon-agent",
3
- "version": "3.0.5",
3
+ "version": "3.1.1",
4
4
  "description": "Multi-frontend AI agent with full tool access, streaming, cron jobs, and plugin system",
5
5
  "author": "Dylan Neve",
6
6
  "license": "MIT",
@@ -42,6 +42,7 @@
42
42
  "src/native/",
43
43
  "src/plugins/",
44
44
  "src/storage/",
45
+ "src/types/",
45
46
  "src/util/",
46
47
  "src/app.ts",
47
48
  "src/bootstrap.ts",
@@ -70,22 +71,22 @@
70
71
  "test:openai-agents:backend": "vitest run --reporter=verbose --reporter=json --outputFile=openai-agents-backend-results.json src/__tests__/integration/openai-agents-live-discovery.test.ts",
71
72
  "build:sql": "tsx scripts/embed-sql.ts",
72
73
  "build:prompts": "tsx scripts/embed-prompts.ts",
73
- "build:wasm": "node native/blake3-wasm/build.mjs",
74
+ "build:wasm": "node native/blake3-wasm/build.mjs && node native/strsim-wasm/build.mjs && node native/sqlguard-wasm/build.mjs && node native/htmlents-wasm/build.mjs",
74
75
  "tarball:check": "node .github/scripts/tarball-check.mjs",
75
76
  "build:stub-sea": "node src/__tests__/integration/stub-claude/build-sea.mjs",
76
77
  "test:watch": "vitest",
77
78
  "test:coverage": "vitest run --coverage",
78
79
  "typecheck": "tsc --noEmit",
79
80
  "lint": "oxlint src/",
81
+ "depcruise": "depcruise src",
82
+ "ratchets": "node scripts/check-ratchets.mjs",
80
83
  "knip": "knip",
81
84
  "format": "prettier --write src/ prompts/",
82
85
  "format:check": "prettier --check src/ prompts/",
83
86
  "ci:protect": "node .github/scripts/enforce-ci-gate.mjs",
84
87
  "build:gleam": "cd native/scheduler-core && gleam build --target javascript && node embed.mjs",
85
88
  "build:zig": "node native/textops-wasm/build.mjs",
86
- "build:c": "node native/strsim-c/build.mjs && node native/sqlguard-c/build.mjs",
87
- "build:cpp": "node native/htmlents-cpp/build.mjs",
88
- "build:native": "npm run build:wasm && npm run build:zig && npm run build:c && npm run build:cpp && npm run build:gleam",
89
+ "build:native": "npm run build:wasm && npm run build:zig && npm run build:gleam",
89
90
  "build:driver": "node native/talon-driver/build.mjs",
90
91
  "build:driver:all": "node native/talon-driver/build.mjs --all",
91
92
  "build:warden": "node native/talon-warden/build.mjs",
@@ -130,9 +131,11 @@
130
131
  "zod": "^4.3.6"
131
132
  },
132
133
  "devDependencies": {
134
+ "@swc/core": "^1.15.46",
133
135
  "@types/node": "^26.0.0",
134
136
  "@types/write-file-atomic": "^4.0.3",
135
137
  "@vitest/coverage-v8": "^4.1.3",
138
+ "dependency-cruiser": "^18.1.0",
136
139
  "fast-check": "^4.6.0",
137
140
  "knip": "^6.3.1",
138
141
  "oxlint": "^1.59.0",
@@ -13,7 +13,7 @@
13
13
  * Kilo ids routinely contain "/" and ":" (e.g. "inclusionai/ling-2.6-1t:free").
14
14
  */
15
15
 
16
- import { ensureServer } from "../server.js";
16
+ import { ensureServer, onServerStop } from "../server.js";
17
17
  import { createRemoteModelCatalogModule } from "../../remote-server/model-catalog/index.js";
18
18
 
19
19
  export type {
@@ -41,6 +41,9 @@ const kiloModels = createRemoteModelCatalogModule({
41
41
  quickPickLimit: 24,
42
42
  });
43
43
 
44
+ // A stopped server invalidates the catalog it served.
45
+ onServerStop(kiloModels.clearCache);
46
+
44
47
  export const getOpenCodeModelCatalog = kiloModels.getCatalog;
45
48
  export const clearModelCatalogCache = kiloModels.clearCache;
46
49
  export const getOpenCodeModelInfo = kiloModels.getModelInfo;
@@ -25,13 +25,12 @@ import type { TalonConfig } from "../../util/config.js";
25
25
  import type { FrontendName } from "../../core/agent-runtime/backend-registry.js";
26
26
  import { logWarn } from "../../util/log.js";
27
27
  import { buildDeliveryContract } from "../shared/delivery-contract.js";
28
- import { clearModelCatalogCache } from "./models/index.js";
29
28
  import {
30
29
  guessProviderID,
31
30
  getBucketPriority,
32
31
  normalizeModelLookup,
33
- parseOpenCodeModelQuery,
34
- } from "./models/index.js";
32
+ parseRemoteModelQuery as parseOpenCodeModelQuery,
33
+ } from "../remote-server/model-catalog/index.js";
35
34
  import {
36
35
  createRemoteServerState,
37
36
  ensureRemoteServer as ensureRemoteServerShared,
@@ -153,6 +152,18 @@ async function prewarmPluginMcpServers(): Promise<void> {
153
152
  await ensurePluginMcpServers(client, "prewarm");
154
153
  }
155
154
 
155
+ /**
156
+ * Callbacks run when the server stops — cache invalidation lives with the
157
+ * caches. The models module registers its clear here at load time, so the
158
+ * server never has to import it (which would be a cycle).
159
+ */
160
+ const stopHooks = new Set<() => void>();
161
+
162
+ /** Register a callback to run whenever the Kilo server is stopped. */
163
+ export function onServerStop(hook: () => void): void {
164
+ stopHooks.add(hook);
165
+ }
166
+
156
167
  /**
157
168
  * Stop the local Kilo server (if we own it) and clear caches.
158
169
  *
@@ -160,7 +171,9 @@ async function prewarmPluginMcpServers(): Promise<void> {
160
171
  * server, this leaves it running — we don't own it.
161
172
  */
162
173
  export function stopKiloServer(): void {
163
- stopRemoteServer(state, clearModelCatalogCache);
174
+ stopRemoteServer(state, () => {
175
+ for (const hook of stopHooks) hook();
176
+ });
164
177
  }
165
178
 
166
179
  // ── Server lifecycle ────────────────────────────────────────────────────────
@@ -251,7 +251,7 @@ export class TalonSession extends MemorySession {
251
251
  return current;
252
252
  }
253
253
 
254
- async addItems(items: AgentInputItem[]): Promise<void> {
254
+ override async addItems(items: AgentInputItem[]): Promise<void> {
255
255
  await super.addItems(items);
256
256
  await this.enforceCap();
257
257
  }
@@ -12,7 +12,7 @@
12
12
  * stay at 4 and only short separator-free ids are embedded raw.
13
13
  */
14
14
 
15
- import { ensureServer } from "../server.js";
15
+ import { ensureServer, onServerStop } from "../server.js";
16
16
  import { createRemoteModelCatalogModule } from "../../remote-server/model-catalog/index.js";
17
17
 
18
18
  export type {
@@ -40,6 +40,9 @@ const opencodeModels = createRemoteModelCatalogModule({
40
40
  quickPickLimit: 4,
41
41
  });
42
42
 
43
+ // A stopped server invalidates the catalog it served.
44
+ onServerStop(opencodeModels.clearCache);
45
+
43
46
  export const getOpenCodeModelCatalog = opencodeModels.getCatalog;
44
47
  export const clearModelCatalogCache = opencodeModels.clearCache;
45
48
  export const getOpenCodeModelInfo = opencodeModels.getModelInfo;
@@ -25,13 +25,12 @@ import type { TalonConfig } from "../../util/config.js";
25
25
  import type { FrontendName } from "../../core/agent-runtime/backend-registry.js";
26
26
  import { logWarn } from "../../util/log.js";
27
27
  import { buildDeliveryContract } from "../shared/delivery-contract.js";
28
- import { clearModelCatalogCache } from "./models/index.js";
29
28
  import {
30
29
  guessProviderID,
31
30
  getBucketPriority,
32
31
  normalizeModelLookup,
33
- parseOpenCodeModelQuery,
34
- } from "./models/index.js";
32
+ parseRemoteModelQuery as parseOpenCodeModelQuery,
33
+ } from "../remote-server/model-catalog/index.js";
35
34
  import {
36
35
  createRemoteServerState,
37
36
  ensureRemoteServer as ensureRemoteServerShared,
@@ -108,8 +107,22 @@ async function prewarmPluginMcpServers(): Promise<void> {
108
107
  await ensurePluginMcpServers(client, "prewarm");
109
108
  }
110
109
 
110
+ /**
111
+ * Callbacks run when the server stops — cache invalidation lives with the
112
+ * caches. The models module registers its clear here at load time, so the
113
+ * server never has to import it (which would be a cycle).
114
+ */
115
+ const stopHooks = new Set<() => void>();
116
+
117
+ /** Register a callback to run whenever the OpenCode server is stopped. */
118
+ export function onServerStop(hook: () => void): void {
119
+ stopHooks.add(hook);
120
+ }
121
+
111
122
  export function stopOpenCodeServer(): void {
112
- stopRemoteServer(state, clearModelCatalogCache);
123
+ stopRemoteServer(state, () => {
124
+ for (const hook of stopHooks) hook();
125
+ });
113
126
  }
114
127
 
115
128
  // ── Server lifecycle ────────────────────────────────────────────────────────
@@ -41,6 +41,7 @@ import {
41
41
  type TalonConfig,
42
42
  } from "../../util/config.js";
43
43
  import { getPluginPromptAdditions } from "../../core/plugin/index.js";
44
+ import { onPromptInputsChanged } from "../../core/prompt/invalidation.js";
44
45
  import { frontendForChatId, nonTerminalFrontends } from "./frontends.js";
45
46
 
46
47
  // ── Types ───────────────────────────────────────────────────────────────────
@@ -112,6 +113,10 @@ export function clearSystemPromptSnapshots(): void {
112
113
  snapshots.clear();
113
114
  }
114
115
 
116
+ // Core and frontends invalidate through the core-side seam — they never
117
+ // import this module (layer rule: nothing above backend/ reaches into it).
118
+ onPromptInputsChanged(clearSystemPromptSnapshots);
119
+
115
120
  // ── Public API ──────────────────────────────────────────────────────────────
116
121
 
117
122
  /**
package/src/cli/events.ts CHANGED
@@ -52,7 +52,7 @@ async function showHistory(limit: number): Promise<void> {
52
52
  const { readJournal } = await import("../storage/journal.js");
53
53
  let entries;
54
54
  try {
55
- entries = readJournal({ limit });
55
+ entries = readJournal<TalonEvent & { at: number }>({ limit });
56
56
  } catch (err) {
57
57
  console.log(
58
58
  ` ${pc.red("✖")} Could not read the journal: ${err instanceof Error ? err.message : err}\n`,
package/src/cli/index.ts CHANGED
@@ -172,7 +172,7 @@ export async function runCli(): Promise<void> {
172
172
  mainMenu();
173
173
  break;
174
174
  default: {
175
- // "did you mean ...?" via the C similarity core (native/strsim-c).
175
+ // "did you mean ...?" via the native similarity core (native/strsim-wasm).
176
176
  const { closestMatch } = await import("../native/strsim.js");
177
177
  const suggestion = closestMatch(command, CLI_COMMANDS);
178
178
  const hint = suggestion
package/src/cli/tasks.ts CHANGED
@@ -16,6 +16,7 @@ import type {
16
16
  TaskRecord,
17
17
  TaskState,
18
18
  } from "../core/tasks/index.js";
19
+ import type { PublishedEvent } from "../core/bus/index.js";
19
20
 
20
21
  function formatDuration(ms: number): string {
21
22
  const seconds = Math.floor(ms / 1000);
@@ -120,7 +121,10 @@ async function journalTasks(
120
121
  const { readJournal } = await import("../storage/journal.js");
121
122
  const seen = new Set(liveTasks.map((t) => `${t.id}:${t.queuedAt}`));
122
123
  const historical: TaskRecord[] = [];
123
- for (const entry of readJournal({ type: "task.settled", limit })) {
124
+ for (const entry of readJournal<PublishedEvent>({
125
+ type: "task.settled",
126
+ limit,
127
+ })) {
124
128
  if (entry.event.type !== "task.settled") continue;
125
129
  const task = entry.event.task;
126
130
  // (id, queuedAt) identifies a run across surfaces — per-process ids
@@ -0,0 +1,28 @@
1
+ /**
2
+ * PID-starttime probe — the /proc read shared by spawn (capture at fork)
3
+ * and resume (compare after restart). Lives alone so neither module has
4
+ * to import the other for it.
5
+ */
6
+
7
+ import { readFileSync } from "node:fs";
8
+
9
+ /**
10
+ * Read field 22 (start time in jiffies since boot) from /proc/<pid>/stat.
11
+ * Returns undefined if /proc isn't available (non-Linux) or the read fails.
12
+ *
13
+ * Parsing note: the `comm` field (2nd) is wrapped in parens and may itself
14
+ * contain ')' — the safe parse finds the LAST ')' and splits the rest on
15
+ * space. After that split, index 19 corresponds to field 22.
16
+ */
17
+ export function readPidStarttimeSync(pid: number): number | undefined {
18
+ try {
19
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf-8");
20
+ const lastParen = stat.lastIndexOf(")");
21
+ if (lastParen < 0) return undefined;
22
+ const tail = stat.slice(lastParen + 2).split(" ");
23
+ const starttime = Number(tail[19]);
24
+ return Number.isFinite(starttime) ? starttime : undefined;
25
+ } catch {
26
+ return undefined;
27
+ }
28
+ }
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Post-restart resume — respawn persistent triggers, fire late wakes for
3
- * recently-terminated ones, plus the /proc-based PID-starttime probe and the
4
- * orphan-kill used to avoid duplicate spawns after an unclean crash.
3
+ * recently-terminated ones, plus the orphan-kill used to avoid duplicate
4
+ * spawns after an unclean crash. The /proc PID-starttime probe both this
5
+ * and spawn use lives in ./pid.ts.
5
6
  */
6
7
 
7
- import { readFileSync } from "node:fs";
8
8
  import {
9
9
  getAllTriggers,
10
10
  updateTrigger,
@@ -15,6 +15,7 @@ import { log, logError } from "../../../util/log.js";
15
15
  import { depsHolder } from "./state.js";
16
16
  import { fireWake } from "./output.js";
17
17
  import { spawnTrigger } from "./spawn.js";
18
+ import { readPidStarttimeSync } from "./pid.js";
18
19
 
19
20
  /**
20
21
  * After the dispatcher is wired, walk the store and clean up leftover state
@@ -68,27 +69,6 @@ export async function resumeAfterRestart(): Promise<void> {
68
69
  }
69
70
  }
70
71
 
71
- /**
72
- * Read field 22 (start time in jiffies since boot) from /proc/<pid>/stat.
73
- * Returns undefined if /proc isn't available (non-Linux) or the read fails.
74
- *
75
- * Parsing note: the `comm` field (2nd) is wrapped in parens and may itself
76
- * contain ')' — the safe parse finds the LAST ')' and splits the rest on
77
- * space. After that split, index 19 corresponds to field 22.
78
- */
79
- export function readPidStarttimeSync(pid: number): number | undefined {
80
- try {
81
- const stat = readFileSync(`/proc/${pid}/stat`, "utf-8");
82
- const lastParen = stat.lastIndexOf(")");
83
- if (lastParen < 0) return undefined;
84
- const tail = stat.slice(lastParen + 2).split(" ");
85
- const starttime = Number(tail[19]);
86
- return Number.isFinite(starttime) ? starttime : undefined;
87
- } catch {
88
- return undefined;
89
- }
90
- }
91
-
92
72
  /**
93
73
  * Probe a stored PID from a previous Talon run and SIGKILL it if it's still
94
74
  * alive AND really is our former child (not a recycled PID). Used by
@@ -25,7 +25,7 @@ import {
25
25
  import { commandForLanguage } from "./command.js";
26
26
  import { handleStdoutLine, handleStderrLine } from "./output.js";
27
27
  import { handleTimeout, finalizeExit, failTrigger } from "./exit.js";
28
- import { readPidStarttimeSync } from "./resume.js";
28
+ import { readPidStarttimeSync } from "./pid.js";
29
29
 
30
30
  /**
31
31
  * Spawn a trigger's script as a supervised child process.
@@ -22,8 +22,8 @@ export async function performPluginReload(
22
22
  const { reloadPlugins, getPluginPromptAdditions } =
23
23
  await import("../../plugin/index.js");
24
24
  const { rebuildSystemPrompt } = await import("../../../util/config.js");
25
- const { clearSystemPromptSnapshots } =
26
- await import("../../../backend/shared/system-prompt.js");
25
+ const { notifyPromptInputsChanged } =
26
+ await import("../../prompt/invalidation.js");
27
27
 
28
28
  // reloadPlugins reads + validates config internally — no double read.
29
29
  // Frontends are derived from config if not explicitly provided.
@@ -37,7 +37,7 @@ export async function performPluginReload(
37
37
  // Plugin prompt additions changed — drop per-session prompt
38
38
  // snapshots so every chat's next turn picks up the new prompt
39
39
  // (deliberate one-time cache re-write per live session).
40
- clearSystemPromptSnapshots();
40
+ notifyPromptInputsChanged();
41
41
 
42
42
  return { names };
43
43
  }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Prompt-input invalidation — the core-side seam callers use when prompt
3
+ * inputs change out from under live sessions on purpose (plugin reload,
4
+ * skill toggle).
5
+ *
6
+ * Whoever caches assembled prompts registers a hook here (today: the
7
+ * per-session snapshot store in backend/shared/system-prompt.ts, at its
8
+ * module load). Core and frontends call `notifyPromptInputsChanged()`
9
+ * and never import the cache — the dependency points backend → core,
10
+ * as the layer rule requires.
11
+ */
12
+
13
+ const hooks = new Set<() => void>();
14
+
15
+ /** Register a callback run whenever prompt inputs are invalidated. */
16
+ export function onPromptInputsChanged(hook: () => void): void {
17
+ hooks.add(hook);
18
+ }
19
+
20
+ /** Invalidate: every registered prompt cache drops its state. */
21
+ export function notifyPromptInputsChanged(): void {
22
+ for (const hook of hooks) hook();
23
+ }
package/src/core/types.ts CHANGED
@@ -29,8 +29,8 @@ export type UnifiedModelInfo = {
29
29
  unavailableReason?: string;
30
30
  };
31
31
 
32
- export type ReasoningEffortLevel =
33
- "off" | "minimal" | "low" | "medium" | "high" | "max" | "xhigh";
32
+ export type { ReasoningEffortLevel } from "../types/effort.js";
33
+ import type { ReasoningEffortLevel } from "../types/effort.js";
34
34
 
35
35
  /** Result of resolving a user's model query. */
36
36
  export type UnifiedModelResolution =
@@ -34,9 +34,9 @@ import {
34
34
  setChatModelForBackend,
35
35
  setChatBackend,
36
36
  setChatEffort,
37
- resolveModelName,
38
37
  type EffortLevel,
39
38
  } from "../../../storage/chat-settings.js";
39
+ import { resolveModelId as resolveModelName } from "../../../core/models/catalog.js";
40
40
  import {
41
41
  registerChat,
42
42
  disablePulse,
@@ -18,9 +18,9 @@ import {
18
18
  setChatBackend,
19
19
  setChatEffort,
20
20
  setChatPulseInterval,
21
- resolveModelName,
22
21
  type EffortLevel,
23
22
  } from "../../../storage/chat-settings.js";
23
+ import { resolveModelId as resolveModelName } from "../../../core/models/catalog.js";
24
24
  import {
25
25
  registerChat,
26
26
  disablePulse,
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Bridge auth token — the shared secret behind every non-/health route.
3
+ *
4
+ * The operator can set one explicitly (`native.token`); this module covers
5
+ * the case where they didn't but the bridge is about to bind a non-loopback
6
+ * host. Serving the full agent API unauthenticated to the LAN is never an
7
+ * acceptable default, so a token is minted once and persisted under
8
+ * ~/.talon/keys/ — stable across restarts so paired clients keep working.
9
+ *
10
+ * The token value itself never goes to the log (SECURITY.md treats
11
+ * credentials in logs as a vulnerability); same-machine clients pick it up
12
+ * from the 0600 discovery file, remote clients read it from the key file.
13
+ */
14
+
15
+ import { randomBytes } from "node:crypto";
16
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
17
+ import { resolve } from "node:path";
18
+ import { dirs } from "../../util/paths.js";
19
+ import { log } from "../../util/log.js";
20
+
21
+ const TOKEN_FILE = "bridge-token";
22
+ /** 32 random bytes → 43 base64url chars; comfortably beyond brute force. */
23
+ const TOKEN_BYTES = 32;
24
+
25
+ /** Path of the persisted token, for operator-facing messages. */
26
+ export function bridgeTokenPath(dir: string = dirs.keys): string {
27
+ return resolve(dir, TOKEN_FILE);
28
+ }
29
+
30
+ /**
31
+ * Load the persisted auto-generated bridge token, minting it on first use.
32
+ * Lives under ~/.talon/keys/ with owner-only permissions, like the TLS
33
+ * identity next to it.
34
+ */
35
+ export function loadOrCreateBridgeToken(dir: string = dirs.keys): string {
36
+ const path = bridgeTokenPath(dir);
37
+ try {
38
+ const existing = readFileSync(path, "utf-8").trim();
39
+ if (existing) return existing;
40
+ } catch {
41
+ // first boot — nothing persisted yet
42
+ }
43
+ const token = randomBytes(TOKEN_BYTES).toString("base64url");
44
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
45
+ writeFileSync(path, `${token}\n`, { mode: 0o600 });
46
+ chmodSync(path, 0o600); // mode above is ignored when the file exists
47
+ log(
48
+ "native",
49
+ `Minted bridge auth token (non-loopback bind with no native.token) — pair remote clients with the value in ${path}`,
50
+ );
51
+ return token;
52
+ }
@@ -21,7 +21,7 @@ import type { Backend } from "../../core/agent-runtime/capabilities.js";
21
21
  import { performPluginReload } from "../../core/engine/gateway-actions/plugins.js";
22
22
  import { getPluginPromptAdditions } from "../../core/plugin/index.js";
23
23
  import { listPluginItems, setPluginEnabled } from "../../core/plugin/manage.js";
24
- import { clearSystemPromptSnapshots } from "../../backend/shared/system-prompt.js";
24
+ import { notifyPromptInputsChanged } from "../../core/prompt/invalidation.js";
25
25
  import { listSkills, setSkillEnabled } from "../../storage/skill-store.js";
26
26
  import { rebuildSystemPrompt, type TalonConfig } from "../../util/config.js";
27
27
  import { log } from "../../util/log.js";
@@ -88,6 +88,6 @@ export function toggleSkill(
88
88
  log("native", `Skill ${name} ${enabled ? "enabled" : "disabled"} via bridge`);
89
89
  rebuildSystemPrompt(config, getPluginPromptAdditions());
90
90
  backend?.control?.updateSystemPrompt?.(config.systemPrompt);
91
- clearSystemPromptSnapshots();
91
+ notifyPromptInputsChanged();
92
92
  return { ok: true };
93
93
  }
@@ -75,6 +75,7 @@ import { createNativeActionHandler } from "./actions.js";
75
75
  import { getMeshService } from "../../core/mesh/index.js";
76
76
  import { removeBridgeDiscovery, writeBridgeDiscovery } from "./discovery.js";
77
77
  import { isLoopbackHost, loadOrCreateBridgeTlsIdentity } from "./tls.js";
78
+ import { loadOrCreateBridgeToken } from "./auth.js";
78
79
  import { readLogEntries } from "./logs.js";
79
80
  import {
80
81
  BRIDGE_PROTOCOL_VERSION,
@@ -1200,11 +1201,16 @@ export function createNativeFrontend(
1200
1201
  // Encrypted by default the moment the bridge leaves the machine; loopback
1201
1202
  // stays plain HTTP unless explicitly opted in (`native.tls`).
1202
1203
  const bridgeTls = nativeCfg.tls ?? !isLoopbackHost(bridgeHost);
1204
+ // Never serve the agent API to the network unauthenticated: a non-loopback
1205
+ // bind with no configured token gets a persistent auto-minted one instead.
1206
+ const bridgeToken =
1207
+ nativeCfg.token ??
1208
+ (isLoopbackHost(bridgeHost) ? undefined : loadOrCreateBridgeToken());
1203
1209
  const server = new BridgeServer(
1204
1210
  {
1205
1211
  host: bridgeHost,
1206
1212
  port: nativeCfg.port ?? 19880,
1207
- token: nativeCfg.token,
1213
+ token: bridgeToken,
1208
1214
  startedAt,
1209
1215
  ...(bridgeTls ? { tls: () => loadOrCreateBridgeTlsIdentity() } : {}),
1210
1216
  },
@@ -1274,7 +1280,7 @@ export function createNativeFrontend(
1274
1280
  const fingerprint = server.getFingerprint();
1275
1281
  await writeBridgeDiscovery({
1276
1282
  port: server.getPort(),
1277
- token: nativeCfg.token,
1283
+ token: bridgeToken,
1278
1284
  scheme: server.getScheme(),
1279
1285
  ...(fingerprint ? { fingerprint } : {}),
1280
1286
  startedAt: Date.parse(startedAt),