@agentproto/runtime 3.6.1 → 4.1.0
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/README.md +8 -3
- package/dist/app-ui-build.d.ts +96 -0
- package/dist/app-ui-build.mjs +250 -0
- package/dist/app-ui-build.mjs.map +1 -0
- package/dist/app-ui-delivery.d.ts +75 -0
- package/dist/app-ui-delivery.mjs +144 -0
- package/dist/app-ui-delivery.mjs.map +1 -0
- package/dist/app-ui-placeholder.d.ts +73 -0
- package/dist/app-ui-placeholder.mjs +89 -0
- package/dist/app-ui-placeholder.mjs.map +1 -0
- package/dist/catalog-models.d.ts +5 -3
- package/dist/catalog-models.mjs.map +1 -1
- package/dist/config-schema.d.ts +134 -0
- package/dist/config-schema.mjs +3697 -0
- package/dist/config-schema.mjs.map +1 -0
- package/dist/config.d.ts +22 -2
- package/dist/config.mjs +3615 -4
- package/dist/config.mjs.map +1 -1
- package/dist/{context-continuity-ib9_bVYM.d.ts → context-continuity-Bn99UtVC.d.ts} +40 -1
- package/dist/{heartbeat-COGpMrJS.d.ts → heartbeat-BJtxbDb9.d.ts} +13 -1
- package/dist/heartbeat.d.ts +1 -1
- package/dist/index.d.ts +3057 -151
- package/dist/index.mjs +17669 -7188
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-imports.d.ts +2 -1
- package/dist/resume-strategies.mjs +27 -1
- package/dist/resume-strategies.mjs.map +1 -1
- package/dist/{session-config-DbWP9RRj.d.ts → session-config-ByfVeEQJ.d.ts} +1 -1
- package/dist/session-presence.mjs +3613 -6
- package/dist/session-presence.mjs.map +1 -1
- package/dist/session-story.d.ts +10 -0
- package/dist/{spawn-defaults-CwI9Puwy.d.ts → spawn-defaults-CZj39Znq.d.ts} +146 -2
- package/dist/user-presets.d.ts +8 -2
- package/dist/user-presets.mjs +2 -1
- package/dist/user-presets.mjs.map +1 -1
- package/docs/TRANSMITTER.md +356 -0
- package/docs/mcp-tools/agent_start.md +476 -0
- package/docs/mcp-tools/app_run.md +47 -0
- package/package.json +47 -23
package/dist/session-story.d.ts
CHANGED
|
@@ -26,6 +26,16 @@
|
|
|
26
26
|
interface ExportedMessage {
|
|
27
27
|
role: "user" | "assistant" | "tool" | "system";
|
|
28
28
|
text?: string;
|
|
29
|
+
/** Set on a `user`-role message that is a typed inter-session message
|
|
30
|
+
* (a `session-message` record) rather than the human: the daemon-attested
|
|
31
|
+
* sender. Absent for a human prompt. */
|
|
32
|
+
from?: {
|
|
33
|
+
sessionId?: string;
|
|
34
|
+
label?: string;
|
|
35
|
+
relation: "child" | "parent" | "sibling" | "human" | "system";
|
|
36
|
+
kind: string;
|
|
37
|
+
messageId: string;
|
|
38
|
+
};
|
|
29
39
|
reasoning?: string;
|
|
30
40
|
toolName?: string;
|
|
31
41
|
toolCalls?: {
|
|
@@ -1,5 +1,107 @@
|
|
|
1
|
+
import { C as ContextContinuityPolicy, S as SpawnBrowserMode } from './context-continuity-Bn99UtVC.js';
|
|
1
2
|
import { CatalogProvider } from '@agentproto/model-catalog';
|
|
2
|
-
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Deferred/lazy MCP tool loading (harness-parity item 3).
|
|
6
|
+
*
|
|
7
|
+
* Motivation: a root `/mcp` session sees every tool the daemon registers —
|
|
8
|
+
* today that's 60+ tools across families (agent_*, session_*, policy_*,
|
|
9
|
+
* routine_*, workflow_*, terminal_*, command_*, file_*, directory_*,
|
|
10
|
+
* browser_*, tunnel_*, mcp_*, inbound_watcher_*) — all with full schemas,
|
|
11
|
+
* loaded upfront, whether or not the session ever touches most of them.
|
|
12
|
+
*
|
|
13
|
+
* NB: imported third-party MCP servers (`mcp_import`) do NOT have this
|
|
14
|
+
* problem — they're already proxied behind `mcp_imported_tool_list` /
|
|
15
|
+
* `mcp_imported_call` (session-tools.ts), a search-then-call pattern that
|
|
16
|
+
* never mounts an upstream server's tools directly. This module addresses
|
|
17
|
+
* the daemon's OWN gateway tools, the part of the surface still eager.
|
|
18
|
+
*
|
|
19
|
+
* ## Why this does NOT use the SDK's enable()/disable()
|
|
20
|
+
*
|
|
21
|
+
* `@modelcontextprotocol/sdk` 1.29.0's `server.tool(...)` returns a
|
|
22
|
+
* `RegisteredTool` with `.enable()`/`.disable()`, `tools/list` filters on
|
|
23
|
+
* `.enabled`, and the server auto-advertises `tools: { listChanged: true }`
|
|
24
|
+
* — a real, SDK-supported dynamic-registration mechanism. An earlier draft
|
|
25
|
+
* of this module used exactly that: register tools disabled, `.enable()`
|
|
26
|
+
* them from `tool_search`.
|
|
27
|
+
*
|
|
28
|
+
* It does not work here. `http-server.ts` documents (and a live daemon
|
|
29
|
+
* run confirmed): "MCP transport: stateless mode for v1 — each request is
|
|
30
|
+
* independent" — a FRESH `McpServer` is built via `mcpServerFactory()` for
|
|
31
|
+
* EVERY request, including `tools/call`. An `.enable()` mutation made
|
|
32
|
+
* during a `tool_search` call lives only on that call's own throwaway
|
|
33
|
+
* server instance; the very next `tools/call` for the newly-"enabled"
|
|
34
|
+
* tool gets a brand-new instance where it's disabled again, and fails
|
|
35
|
+
* with `Tool X disabled`. Confirmed by running exactly that sequence
|
|
36
|
+
* against a live gateway — the mutation never survives past the request
|
|
37
|
+
* that made it, so a real orchestrator would search successfully, read
|
|
38
|
+
* a schema, then have the follow-up call rejected. Worse than doing
|
|
39
|
+
* nothing: it looks like it works, right up until the second call.
|
|
40
|
+
*
|
|
41
|
+
* ## What this does instead
|
|
42
|
+
*
|
|
43
|
+
* Every tool stays fully registered AND enabled (`tools/call` always
|
|
44
|
+
* works for any tool name, deferred or not — the daemon is stateless
|
|
45
|
+
* regardless, so there is no session-scoped "have they earned the right
|
|
46
|
+
* to call this" state to enforce even if it were desirable). "Deferred"
|
|
47
|
+
* only means: excluded from the `tools/list` response, so a session
|
|
48
|
+
* doesn't pay the schema-loading token tax for tools it never asked
|
|
49
|
+
* about. The `tools/list` handler is overridden directly on the
|
|
50
|
+
* underlying low-level `Server` (`mcpServer.server.setRequestHandler` —
|
|
51
|
+
* the SDK's own documented escape hatch: "For advanced usage ... use the
|
|
52
|
+
* underlying Server instance") to return only the `alwaysOn` set plus
|
|
53
|
+
* `tool_search` itself; this is a pure per-request read, so it's
|
|
54
|
+
* naturally correct under statelessness — there's no history to lose.
|
|
55
|
+
* `tool_search` matches a keyword (or `select:name1,name2` for exact
|
|
56
|
+
* names) against a catalog of every OTHER registered tool and returns
|
|
57
|
+
* full JSON Schema for the hits directly in its own result text — the
|
|
58
|
+
* one channel that reliably survives the stateless boundary, since it's
|
|
59
|
+
* read from the SAME call that produced it.
|
|
60
|
+
*
|
|
61
|
+
* Opt-in and backward-compatible by construction: nothing calls this
|
|
62
|
+
* unless a host explicitly wires `deferredTools` into `createGateway` —
|
|
63
|
+
* see `index.ts`. Every existing daemon keeps registering (and exposing)
|
|
64
|
+
* everything eagerly, exactly as before.
|
|
65
|
+
*
|
|
66
|
+
* Both SDK registration entry points are intercepted — `server.tool(...)`
|
|
67
|
+
* AND `server.registerTool(...)` (the config-object overload; e.g.
|
|
68
|
+
* mcp-apps-adapter.ts's `agentproto_sessions` panel tool uses the latter,
|
|
69
|
+
* and would silently skip the catalog if only one were patched).
|
|
70
|
+
*
|
|
71
|
+
* Tools registered on the server BEFORE it's wrapped — e.g. `self_inspect`,
|
|
72
|
+
* added inside `@agentproto/mcp-server`'s `createMcpServer()` — are
|
|
73
|
+
* automatically excluded from `tools/list` too: the override only lists
|
|
74
|
+
* names present in `registry` (built from tools that passed through the
|
|
75
|
+
* proxy), so anything registered earlier is simply absent from that set,
|
|
76
|
+
* with no extra bookkeeping needed. They stay fully callable, same as
|
|
77
|
+
* every other tool here — the override only ever touches what `tools/list`
|
|
78
|
+
* shows.
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* `~/.agentproto/config.json`'s `defaults.mcp.deferredTools` shape —
|
|
83
|
+
* `false`/absent (the default) keeps every daemon boot eager, byte-identical
|
|
84
|
+
* to pre-existing behaviour. `true` turns deferred tools on gateway-wide
|
|
85
|
+
* using the daemon's own default always-on set (see `DEFAULT_ALWAYS_ON_TOOLS`
|
|
86
|
+
* in index.ts). `{ alwaysOn }` turns it on with a custom always-on set
|
|
87
|
+
* instead of the default one.
|
|
88
|
+
*/
|
|
89
|
+
type DeferredToolsConfig = boolean | {
|
|
90
|
+
alwaysOn: string[];
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Convert the config-file value (tri-state: off / on-with-default /
|
|
94
|
+
* on-with-custom-set) into the shape `CreateGatewayOptions.deferredTools`
|
|
95
|
+
* expects. Pure — no fs, easy to unit test independent of `loadConfig`.
|
|
96
|
+
* `undefined`/`false` ⇒ `undefined` (omit the key entirely, so
|
|
97
|
+
* `createGateway` never wraps `withDeferredTools` — today's eager
|
|
98
|
+
* behaviour). `true` ⇒ `{}` (deferred, `alwaysOn` left for the gateway's
|
|
99
|
+
* own `DEFAULT_ALWAYS_ON_TOOLS` fallback). `{ alwaysOn }` ⇒ passed through
|
|
100
|
+
* verbatim.
|
|
101
|
+
*/
|
|
102
|
+
declare function resolveDeferredToolsGatewayOption(config: DeferredToolsConfig | undefined): {
|
|
103
|
+
alwaysOn?: readonly string[];
|
|
104
|
+
} | undefined;
|
|
3
105
|
|
|
4
106
|
/**
|
|
5
107
|
* Pure resolver for `~/.agentproto/config.json`'s `defaults` block —
|
|
@@ -25,6 +127,22 @@ import { C as ContextContinuityPolicy } from './context-continuity-ib9_bVYM.js';
|
|
|
25
127
|
* store — never read from the ambient shell env.
|
|
26
128
|
*/
|
|
27
129
|
|
|
130
|
+
/**
|
|
131
|
+
* `defaults.mcp` block — daemon-wide MCP gateway policy, distinct from the
|
|
132
|
+
* per-adapter spawn defaults above (it's read once at daemon boot, not
|
|
133
|
+
* per-spawn — see `serve.ts`'s `createGateway({ deferredTools })` call).
|
|
134
|
+
*/
|
|
135
|
+
interface McpDefaultsConfig {
|
|
136
|
+
/** Gateway-wide deferred/lazy `tools/list` loading (harness-parity item
|
|
137
|
+
* 3 — see `deferred-tools.ts`). Default false/absent: every daemon boot
|
|
138
|
+
* stays eager, byte-identical to pre-existing behaviour — turning this
|
|
139
|
+
* on is a deliberate operator opt-in. Independent of the per-role
|
|
140
|
+
* default (`RoleProfile.deferredTools`, on for `executor`) and the
|
|
141
|
+
* per-spawn `agent_start.deferredTools` override, and independent of
|
|
142
|
+
* the per-mount `?deferred=1|0` query override on `/mcp` — this is only
|
|
143
|
+
* the BOOT-TIME default for connections that specify neither. */
|
|
144
|
+
deferredTools?: DeferredToolsConfig;
|
|
145
|
+
}
|
|
28
146
|
/**
|
|
29
147
|
* Deterministic billing-auth config for one adapter slug (today, only
|
|
30
148
|
* claude-code interprets it — see `AgentCliStartOptions.auth` in
|
|
@@ -95,6 +213,14 @@ interface SpawnDefaultsConfig {
|
|
|
95
213
|
* omits `trace`. Default false — sessions trace only when they opt in or
|
|
96
214
|
* this is on. See `filterSessionObserver` / `SpawnAgentInput.trace`. */
|
|
97
215
|
langfuseTracing?: boolean;
|
|
216
|
+
/** Wake an idle agent session when one of its background tasks settles
|
|
217
|
+
* (see `SessionsRegistryOptions.backgroundTaskWake`). On by default;
|
|
218
|
+
* `{ enabled: false }` opts out. `graceMs` is how long the daemon waits
|
|
219
|
+
* for the agent to wake ITSELF (Claude Code does) before prompting it. */
|
|
220
|
+
backgroundTaskWake?: {
|
|
221
|
+
enabled?: boolean;
|
|
222
|
+
graceMs?: number;
|
|
223
|
+
};
|
|
98
224
|
/** Redactor slug applied to traced session content before it's sent to
|
|
99
225
|
* Langfuse (see `@agentproto/redaction`'s registry). Default "secrets"
|
|
100
226
|
* (deny-list by key + value-scan for secret shapes). */
|
|
@@ -107,6 +233,24 @@ interface SpawnDefaultsConfig {
|
|
|
107
233
|
* EXPLICIT `interrupt` on the call (true OR false) always wins over this
|
|
108
234
|
* default. */
|
|
109
235
|
agentPromptInterrupt?: boolean;
|
|
236
|
+
/** Inter-session messaging policy (AIP-46 §Session messages). */
|
|
237
|
+
messaging?: {
|
|
238
|
+
/** Let `message_send` / `message_reply` reach a sibling session (same
|
|
239
|
+
* parent). Default false — only child↔parent. */
|
|
240
|
+
allowSiblings?: boolean;
|
|
241
|
+
/** Whether a SESSION sender may use urgency `interrupt` (cancel the
|
|
242
|
+
* recipient's in-flight turn). Default "deny": downgraded to `steer`
|
|
243
|
+
* and reported as such. Human (HTTP/CLI) senders always may. */
|
|
244
|
+
agentInterrupt?: "allow" | "deny";
|
|
245
|
+
};
|
|
246
|
+
/** Daemon-wide MCP gateway policy. See {@link McpDefaultsConfig}. */
|
|
247
|
+
mcp?: McpDefaultsConfig;
|
|
248
|
+
/** Spawn-shape defaults applied when neither the `agent_start` call, its
|
|
249
|
+
* role nor its user preset says otherwise. */
|
|
250
|
+
spawn?: {
|
|
251
|
+
/** Default `agent_start.browser` (`"headless"` | `false`). Default off. */
|
|
252
|
+
browser?: SpawnBrowserMode;
|
|
253
|
+
};
|
|
110
254
|
}
|
|
111
255
|
interface ResolveSpawnDefaultsInput {
|
|
112
256
|
/** Explicit-call `skills`. Undefined ⇒ caller expressed no preference,
|
|
@@ -433,4 +577,4 @@ interface DeclaredAdapterOption {
|
|
|
433
577
|
*/
|
|
434
578
|
declare function normalizeSkillsOption(skills: string[], options: Record<string, boolean | number | string>, declaredOptions: readonly DeclaredAdapterOption[] | undefined): Record<string, boolean | number | string>;
|
|
435
579
|
|
|
436
|
-
export { type AdapterAuthDescriptor as A, CLAUDE_CODE_OAUTH_SOURCE as C, type
|
|
580
|
+
export { type AdapterAuthDescriptor as A, CLAUDE_CODE_OAUTH_SOURCE as C, type DefaultsAdapterAuthConfig as D, type ResolvedAuthSpec as R, type SpawnDefaultsConfig as S, type DeclaredAdapterOption as a, type AuthEcho as b, AuthResolutionError as c, type CredentialSource as d, type DefaultsAdapterConfig as e, type DeferredToolsConfig as f, type ResolveSubscriptionCredentialInput as g, type ResolvedSpawnAuthMaterial as h, type ResolvedSpawnDefaults as i, type SubscriptionCredentialResolution as j, SubscriptionSourceError as k, credentialFingerprint as l, resolveDeferredToolsGatewayOption as m, normalizeSkillsOption as n, resolveSpawnDefaults as o, resolveSubscriptionCredential as p, resolveAuthSpec as r };
|
package/dist/user-presets.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { S as SessionConfig, R as RouteSpec, P as Posture, E as EffortLevel, C as ContextProfile } from './session-config-
|
|
2
|
+
import { S as SessionConfig, R as RouteSpec, P as Posture, E as EffortLevel, C as ContextProfile } from './session-config-ByfVeEQJ.js';
|
|
3
|
+
import { S as SpawnBrowserMode } from './context-continuity-Bn99UtVC.js';
|
|
3
4
|
import '@agentproto/auth';
|
|
4
|
-
import '
|
|
5
|
+
import '@agentproto/acp';
|
|
6
|
+
import '@agentproto/command-sandbox';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* User-owned spawn presets.
|
|
@@ -38,6 +40,9 @@ interface UserPreset extends Partial<SessionConfig> {
|
|
|
38
40
|
* `SpawnAgentSessionInput.skills`. Omitted means the adapter/defaults
|
|
39
41
|
* decide. */
|
|
40
42
|
skills?: string[];
|
|
43
|
+
/** `agent_start.browser` for spawns from this preset. Ranks below the
|
|
44
|
+
* role's own default (see `resolveBrowserMode`). */
|
|
45
|
+
browser?: SpawnBrowserMode;
|
|
41
46
|
}
|
|
42
47
|
declare const userPresetsFileSchema: z.ZodObject<{
|
|
43
48
|
version: z.ZodLiteral<1>;
|
|
@@ -74,6 +79,7 @@ declare const userPresetsFileSchema: z.ZodObject<{
|
|
|
74
79
|
contextProfile: z.ZodOptional<z.ZodString>;
|
|
75
80
|
cwd: z.ZodOptional<z.ZodString>;
|
|
76
81
|
skills: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
82
|
+
browser: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"headless">, z.ZodLiteral<false>]>>;
|
|
77
83
|
}, z.core.$strip>>;
|
|
78
84
|
}, z.core.$strip>;
|
|
79
85
|
type UserPresetsFile = z.infer<typeof userPresetsFileSchema>;
|
package/dist/user-presets.mjs
CHANGED
|
@@ -29,7 +29,8 @@ var userPresetSchema = z.object({
|
|
|
29
29
|
effort: effortSchema.optional(),
|
|
30
30
|
contextProfile: z.string().min(1).optional(),
|
|
31
31
|
cwd: z.string().min(1).optional(),
|
|
32
|
-
skills: z.array(z.string().min(1)).optional()
|
|
32
|
+
skills: z.array(z.string().min(1)).optional(),
|
|
33
|
+
browser: z.union([z.literal("headless"), z.literal(false)]).optional()
|
|
33
34
|
});
|
|
34
35
|
var userPresetsFileSchema = z.object({
|
|
35
36
|
version: z.literal(1),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/user-presets.ts"],"names":[],"mappings":";;;;;;;;;;
|
|
1
|
+
{"version":3,"sources":["../src/user-presets.ts"],"names":[],"mappings":"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","file":"user-presets.mjs","sourcesContent":["/**\n * User-owned spawn presets.\n *\n * This is deliberately separate from `preset-tools.ts`: provider presets are\n * static gateway definitions shipped by packages, while a UserPreset is a\n * private saved combination of the orthogonal session-config axes.\n */\n\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\"\nimport { homedir } from \"node:os\"\nimport { join, resolve } from \"node:path\"\nimport { z } from \"zod\"\nimport type {\n ContextProfile,\n EffortLevel,\n Posture,\n RouteSpec,\n SessionConfig,\n} from \"./session-config.js\"\nimport type { SpawnBrowserMode } from \"./browser-mount.js\"\n\nconst effortSchema = z.enum([\"low\", \"medium\", \"high\", \"xhigh\", \"max\", \"ultracode\"])\nconst postureSchema = z.union([\n z.enum([\"default\", \"plan\", \"accept-edits\", \"bypass\", \"read-only\"]),\n z.object({ harnessModeId: z.string().min(1) }),\n])\nconst routeSchema = z.object({\n gateway: z.string().min(1),\n baseUrl: z.string().url().optional(),\n})\n\n/** A reusable, user-scoped subset of the spawn/session config axes. */\nexport interface UserPreset extends Partial<SessionConfig> {\n /** Stable machine-local id, e.g. `fast-deepseek`. */\n id: string\n /** Human-readable name shown by CLI and editor pickers. */\n label: string\n /** Adapter harness to use. Omitted means the caller selects one. */\n adapter?: string\n /** Canonical harness slug — alias for `adapter`. */\n harness?: string\n model?: string\n route?: RouteSpec\n access?: { profileRef?: string }\n posture?: Posture\n effort?: EffortLevel\n contextProfile?: ContextProfile\n /** Working directory the favorite pins to. When set, a spawn from this\n * preset lands here regardless of the caller's active folder — the axis\n * that makes a favorite fully location-pinned (true zero-input). Omitted\n * means the caller's cwd ladder resolves it as before. */\n cwd?: string\n /** Skills to preload for a spawn from this preset — the same axis as\n * `SpawnAgentSessionInput.skills`. Omitted means the adapter/defaults\n * decide. */\n skills?: string[]\n /** `agent_start.browser` for spawns from this preset. Ranks below the\n * role's own default (see `resolveBrowserMode`). */\n browser?: SpawnBrowserMode\n}\n\nconst userPresetSchema = z.object({\n id: z.string().regex(/^[a-z0-9][a-z0-9-]*$/),\n label: z.string().min(1),\n adapter: z.string().min(1).optional(),\n harness: z.string().min(1).optional(),\n model: z.string().min(1).optional(),\n route: routeSchema.optional(),\n access: z.object({ profileRef: z.string().min(1).optional() }).optional(),\n posture: postureSchema.optional(),\n effort: effortSchema.optional(),\n contextProfile: z.string().min(1).optional(),\n cwd: z.string().min(1).optional(),\n skills: z.array(z.string().min(1)).optional(),\n browser: z.union([z.literal(\"headless\"), z.literal(false)]).optional(),\n}) satisfies z.ZodType<UserPreset>\n\nconst userPresetsFileSchema = z.object({\n version: z.literal(1),\n presets: z.array(userPresetSchema),\n})\n\nexport type UserPresetsFile = z.infer<typeof userPresetsFileSchema>\n\nfunction emptyFile(): UserPresetsFile {\n return { version: 1, presets: [] }\n}\n\nexport function userPresetsPath(): string {\n return resolve(homedir(), \".agentproto\", \"presets.json\")\n}\n\n/** Missing or malformed user config is treated as empty — a bad preset must\n * never prevent the daemon from starting. Writes always restore valid JSON. */\nexport async function loadUserPresets(): Promise<UserPresetsFile> {\n try {\n return userPresetsFileSchema.parse(JSON.parse(await readFile(userPresetsPath(), \"utf8\")))\n } catch {\n return emptyFile()\n }\n}\n\nasync function writeUserPresets(file: UserPresetsFile): Promise<void> {\n const dir = join(homedir(), \".agentproto\")\n await mkdir(dir, { recursive: true })\n await writeFile(userPresetsPath(), JSON.stringify(file, null, 2) + \"\\n\", {\n encoding: \"utf8\",\n mode: 0o600,\n })\n}\n\nexport async function listUserPresets(): Promise<UserPreset[]> {\n return (await loadUserPresets()).presets\n}\n\nexport async function getUserPreset(id: string): Promise<UserPreset | undefined> {\n return (await loadUserPresets()).presets.find(preset => preset.id === id)\n}\n\n/** Add or replace a preset by id. The parser makes this the single validation\n * boundary for CLI, MCP and editor callers. */\nexport async function saveUserPreset(preset: UserPreset): Promise<void> {\n const validated = userPresetSchema.parse(preset)\n const file = await loadUserPresets()\n const index = file.presets.findIndex(existing => existing.id === validated.id)\n if (index === -1) file.presets.push(validated)\n else file.presets[index] = validated\n await writeUserPresets(file)\n}\n\nexport async function deleteUserPreset(id: string): Promise<boolean> {\n const file = await loadUserPresets()\n const index = file.presets.findIndex(preset => preset.id === id)\n if (index === -1) return false\n file.presets.splice(index, 1)\n await writeUserPresets(file)\n return true\n}\n"]}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# The agentpush ⇄ agentproto transmitter
|
|
2
|
+
|
|
3
|
+
Bidirectional bridge between an imported agentpush MCP server (Telegram,
|
|
4
|
+
etc.) and a live agentproto session: outbound sends can bind a contact to
|
|
5
|
+
the session that sent them, and inbound replies from that contact then
|
|
6
|
+
route straight into the SAME session — as a real user turn — instead of
|
|
7
|
+
spawning a brand-new agent every time.
|
|
8
|
+
|
|
9
|
+
## The model
|
|
10
|
+
|
|
11
|
+
A single `TransmitterBindingStore` (`transmitter-bindings.ts`) maps
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
(alias, source, contactRef) -> { sessionId, mode, provider?, lastSeenTs }
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- `alias` — the imported MCP alias (agentpush) or bot token alias (telegram).
|
|
18
|
+
- `source` — the channel/phone/chat id the message is scoped to.
|
|
19
|
+
- `contactRef` — the sender's id within that source.
|
|
20
|
+
- `sessionId` — the agentproto session future inbound replies from this
|
|
21
|
+
contact should route into.
|
|
22
|
+
- `mode` — the routing mode this binding was created under (`"route"` or
|
|
23
|
+
`"route-or-spawn"; see below).
|
|
24
|
+
- `provider` — the outbound provider this binding was created through
|
|
25
|
+
(`"agentpush"`, `"telegram"`, etc.). Defaults to `"agentpush"` for
|
|
26
|
+
backward compatibility.
|
|
27
|
+
|
|
28
|
+
One shared store instance is constructed in `index.ts` and injected into
|
|
29
|
+
three places: the inbound poll watcher, the `transmit_message` tool, and
|
|
30
|
+
the `POST /inbound` push route. Bindings persist (debounced JSON write) to
|
|
31
|
+
`~/.agentproto/transmitter-bindings.json`, load-on-construct, and start
|
|
32
|
+
empty on a missing/corrupt file rather than throwing.
|
|
33
|
+
|
|
34
|
+
## Outbound provider abstraction
|
|
35
|
+
|
|
36
|
+
`sendOutbound` in `outbound-adapters.ts` is the provider-agnostic send primitive.
|
|
37
|
+
It mirrors `inbound-adapters.ts` on the read side: a single dispatch function
|
|
38
|
+
that branches by `OutboundProvider` ("agentpush", "telegram", "whatsapp",
|
|
39
|
+
"slack", "generic", "native"). Adding a new outbound dialect means adding a case
|
|
40
|
+
to the switch in `sendOutbound` — the MCP tool and HTTP routes stay unchanged.
|
|
41
|
+
|
|
42
|
+
## Outbound: `transmit_message`
|
|
43
|
+
|
|
44
|
+
MCP tool that sends a message out through a provider-specific outbound channel
|
|
45
|
+
and, by default, binds the recipient to the calling session:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"provider": "agentpush",
|
|
50
|
+
"alias": "agentpush",
|
|
51
|
+
"source": "+33600000000",
|
|
52
|
+
"contact_ref": "alice",
|
|
53
|
+
"text": "your PR is ready for review",
|
|
54
|
+
"sessionId": "sess_abc123",
|
|
55
|
+
"bind": true
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- `provider` — optional, defaults to `"agentpush"`. Supported values:
|
|
60
|
+
`"agentpush"`, `"telegram"`, `"whatsapp"`, `"slack"`, `"generic"`, `"native"`.
|
|
61
|
+
- `alias` — for `agentpush`, the imported MCP alias (required). For
|
|
62
|
+
`telegram`, the bot-token alias (defaults to `"default"`).
|
|
63
|
+
- `source` — for `agentpush`, the channel/phone. For `telegram`, the chat id.
|
|
64
|
+
- `contact_ref` — recipient id (`agentpush` contact_ref) or chat id (`telegram`).
|
|
65
|
+
- `text` — message text. Optional when `attachments` is provided; at least one
|
|
66
|
+
of `text` or `attachments` is required.
|
|
67
|
+
- `attachments` — optional array of local files to send as media:
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"type": "photo",
|
|
71
|
+
"path": "/tmp/screenshot.png",
|
|
72
|
+
"caption": "optional caption"
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
Supported types: `photo`, `document`, `video`, `audio`. Multiple attachments
|
|
76
|
+
are sent as an album/media group where the provider supports it.
|
|
77
|
+
- `bind` defaults to `true` — on a successful send, upserts a binding
|
|
78
|
+
`(alias, source, contact_ref) -> sessionId` with `mode: "route-or-spawn"` and
|
|
79
|
+
the chosen `provider`. Pass `bind: false` to send without binding.
|
|
80
|
+
- Returns `{ sent: boolean, bound: boolean, message_id?: string }` on success,
|
|
81
|
+
or `{ sent: false, bound: false, error: string, blocked_reason?: string,
|
|
82
|
+
suggestion?: string }` on failure. A send failure (`sent: false`) never
|
|
83
|
+
binds. `message_id` is the provider's own id for the sent message — pass it
|
|
84
|
+
to `check_delivery` to verify the send landed.
|
|
85
|
+
- **agentpush never throws for a send it can't make** — it replies HTTP 200
|
|
86
|
+
with `status: "blocked"` (opt-out, expired session, unapproved template…)
|
|
87
|
+
or `status: "failed"`. `transmit_message` reads that status instead of
|
|
88
|
+
treating MCP transport success as delivery: a `blocked` or `failed` reply
|
|
89
|
+
is surfaced as `sent: false` with `blocked_reason`/`suggestion` (or
|
|
90
|
+
`error`) propagated, never as a false `sent: true`. Telegram gets the same
|
|
91
|
+
treatment for its own backward-compat case — an HTTP 200 body with
|
|
92
|
+
`ok: false` is also reported as `sent: false`, not `sent: true`.
|
|
93
|
+
|
|
94
|
+
### Provider-specific behaviour
|
|
95
|
+
|
|
96
|
+
**agentpush** calls the real `send_message` MCP tool on the imported alias:
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"to": { "channel": "<source>", "address": "<contact_ref>" },
|
|
100
|
+
"content": { "text": "<text>" }
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
When `attachments` are provided, each file is first uploaded via
|
|
105
|
+
`upload_media` (channel = `<source>`), then referenced by `providerMediaId`
|
|
106
|
+
in `content.media`:
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"to": { "channel": "<source>", "address": "<contact_ref>" },
|
|
110
|
+
"content": {
|
|
111
|
+
"text": "<text>",
|
|
112
|
+
"media": [
|
|
113
|
+
{ "type": "image", "providerMediaId": "<media_id>", "caption": "..." }
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**telegram** POSTs directly to the Telegram Bot API. Text-only messages use
|
|
120
|
+
`sendMessage`:
|
|
121
|
+
```
|
|
122
|
+
POST https://api.telegram.org/bot<token>/sendMessage
|
|
123
|
+
{ "chat_id": "<source>", "text": "<text>" }
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
With one attachment, the matching method is used (`sendPhoto`,
|
|
127
|
+
`sendDocument`, `sendVideo`, or `sendAudio`) with a `multipart/form-data`
|
|
128
|
+
body. With several attachments, `sendMediaGroup` is used. The bot token is
|
|
129
|
+
resolved from a `TelegramBotCredsStore` (configured separately via
|
|
130
|
+
`telegram_bot_token_set`).
|
|
131
|
+
|
|
132
|
+
### Telegram bot wiring recipe
|
|
133
|
+
|
|
134
|
+
1. Create the bot with @BotFather and copy the token.
|
|
135
|
+
2. Store the token in the daemon's credential store:
|
|
136
|
+
```json
|
|
137
|
+
{ "tool": "telegram_bot_token_set", "arguments": { "token": "..." } }
|
|
138
|
+
```
|
|
139
|
+
3. Create a provider-agnostic inbound endpoint:
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"tool": "inbound_endpoint_create",
|
|
143
|
+
"arguments": {
|
|
144
|
+
"slug": "telegram-agentproto",
|
|
145
|
+
"provider": "telegram",
|
|
146
|
+
"alias": "default",
|
|
147
|
+
"mode": "route-or-spawn"
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
The response includes the endpoint `secret`.
|
|
152
|
+
4. Start the narrow public ingress proxy (see below) and get its public URL.
|
|
153
|
+
5. Point Telegram's webhook at the proxy:
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"tool": "telegram_bot_set_webhook",
|
|
157
|
+
"arguments": {
|
|
158
|
+
"url": "https://<proxy-public-url>/inbound/telegram-agentproto",
|
|
159
|
+
"secret_token": "<endpoint-secret>"
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Inbound routing modes
|
|
165
|
+
|
|
166
|
+
Both the poll watcher (`inbound-watcher.ts`, `inbound_watcher_start`'s
|
|
167
|
+
`mode` field) and `POST /inbound` share one decision function,
|
|
168
|
+
`routeInboundMessage` (`inbound-router.ts`):
|
|
169
|
+
|
|
170
|
+
| mode | binding exists, session alive | binding exists, session dead | no binding |
|
|
171
|
+
|---|---|---|---|
|
|
172
|
+
| `spawn` (poll watcher default) | *(ignored)* spawns a fresh agent | *(ignored)* spawns a fresh agent | spawns a fresh agent |
|
|
173
|
+
| `route` | routes into it (`"routed"`) | resurrects it, then routes (`"restarted-routed"`) | skips (`"skipped"`) |
|
|
174
|
+
| `route-or-spawn` (`POST /inbound` default) | routes into it (`"routed"`) | resurrects it, then routes (`"restarted-routed"`) | spawns a fresh agent if a spawn path is configured, else skips |
|
|
175
|
+
|
|
176
|
+
"Resurrects" means: an agent-cli session that's no longer alive is
|
|
177
|
+
restarted in place (same primitive `cron-scheduler.ts`'s `prompt-session`
|
|
178
|
+
job uses — `restartAgentSession(..., { forceAgentResume: true })`), and the
|
|
179
|
+
reply is delivered to the (possibly new) resulting session id. A session
|
|
180
|
+
counts as alive unless it's missing entirely or its `processAlive` field
|
|
181
|
+
is explicitly `false` — a pid-less ACP-native/remote session (no OS
|
|
182
|
+
process to probe) is treated as alive, not dead.
|
|
183
|
+
|
|
184
|
+
Every successful route into a bound session refreshes that binding's
|
|
185
|
+
`lastSeenTs`.
|
|
186
|
+
|
|
187
|
+
## `POST /inbound` — push ingress contract
|
|
188
|
+
|
|
189
|
+
Bearer-gated exactly like the mutating `/sessions/*` routes (read the
|
|
190
|
+
token from `<workspace>/.agentproto/runtime.json`):
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
POST /inbound
|
|
194
|
+
Authorization: Bearer <token>
|
|
195
|
+
Content-Type: application/json
|
|
196
|
+
|
|
197
|
+
{
|
|
198
|
+
"alias": "agentpush",
|
|
199
|
+
"source": "+33600000000",
|
|
200
|
+
"contact_ref": "alice",
|
|
201
|
+
"text": "sounds good, ship it",
|
|
202
|
+
"messages": [ /* optional: raw agentpush events, for the spawn-fallback template */ ],
|
|
203
|
+
"mode": "route-or-spawn" // optional, default "route-or-spawn"
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Responses:
|
|
208
|
+
|
|
209
|
+
- `200 { "action": "routed" | "spawned" | "restarted-routed" | "skipped", "sessionId"?: string }`
|
|
210
|
+
- `400` — a required field (`alias`/`source`/`contact_ref`/`text`) is
|
|
211
|
+
missing, or `mode` isn't one of the three valid values.
|
|
212
|
+
- `401` — missing/invalid bearer token.
|
|
213
|
+
- `501` — the daemon wasn't wired with a `routeInboundMessage` handler
|
|
214
|
+
(i.e. `POST /inbound` isn't enabled on this host).
|
|
215
|
+
|
|
216
|
+
Note: the daemon's own wiring never sets a `spawnForContact` for this
|
|
217
|
+
route — an unauthenticated webhook payload carries no adapter/prompt
|
|
218
|
+
template to spawn an agent with, so an unbound contact under
|
|
219
|
+
`route-or-spawn` is `"skipped"` rather than spawning an arbitrary agent
|
|
220
|
+
from push input. Only the poll watcher (which already has an
|
|
221
|
+
`adapter`/`promptTemplate` configured via `inbound_watcher_start`) spawns.
|
|
222
|
+
|
|
223
|
+
## Simulating an inbound message
|
|
224
|
+
|
|
225
|
+
### 1. The sim script (no live daemon needed)
|
|
226
|
+
|
|
227
|
+
`packages/runtime/scripts/simulate-inbound.mjs` composes the real wiring
|
|
228
|
+
in-process — real `SessionsRegistry`, real `TransmitterBindingStore`, real
|
|
229
|
+
`routeInboundMessage`, real `startHttpServer` — with one fake session
|
|
230
|
+
double standing in for a real adapter, so you can see the whole path
|
|
231
|
+
(HTTP → routing → the session actually receiving the turn) without an
|
|
232
|
+
agentpush server, an adapter install, or a running daemon:
|
|
233
|
+
|
|
234
|
+
```sh
|
|
235
|
+
pnpm --filter @agentproto/runtime sim:inbound
|
|
236
|
+
# or with overrides:
|
|
237
|
+
pnpm --filter @agentproto/runtime sim:inbound -- --contact=bob --text="ping from bob"
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Flags (or matching env vars `ALIAS`/`SOURCE`/`CONTACT`/`TEXT`/
|
|
241
|
+
`SESSION_ID`/`PORT`/`TOKEN`): `--alias`, `--source`, `--contact`, `--text`,
|
|
242
|
+
`--session-id`, `--port`, `--token`. It prints the HTTP status, the
|
|
243
|
+
`{action, sessionId}` response, and the exact text the bound session
|
|
244
|
+
received, then exits non-zero if either doesn't match expectations.
|
|
245
|
+
|
|
246
|
+
### 2. `curl` against a live daemon
|
|
247
|
+
|
|
248
|
+
Bind a contact first (via `transmit_message`, or by hand-seeding
|
|
249
|
+
`~/.agentproto/transmitter-bindings.json`), then:
|
|
250
|
+
|
|
251
|
+
```sh
|
|
252
|
+
TOKEN=$(jq -r .token <workspace>/.agentproto/runtime.json)
|
|
253
|
+
|
|
254
|
+
curl -sS -X POST "http://127.0.0.1:<port>/inbound" \
|
|
255
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
256
|
+
-H "Content-Type: application/json" \
|
|
257
|
+
-d '{
|
|
258
|
+
"alias": "agentpush",
|
|
259
|
+
"source": "+33600000000",
|
|
260
|
+
"contact_ref": "alice",
|
|
261
|
+
"text": "sounds good, ship it"
|
|
262
|
+
}'
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
A `200` with `{"action":"routed","sessionId":"..."}` means the reply
|
|
266
|
+
landed as a user turn in that session — check it with `agent_output` /
|
|
267
|
+
`GET /sessions/:id/stream`.
|
|
268
|
+
|
|
269
|
+
## Provider-agnostic inbound endpoints (`POST /inbound/:slug`)
|
|
270
|
+
|
|
271
|
+
As an alternative to the single shared bearer-gated `POST /inbound`, the
|
|
272
|
+
daemon now supports per-provider webhook endpoints:
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
POST /inbound/<slug>
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Each endpoint is configured with dialect (`agentpush`, `telegram`,
|
|
279
|
+
`whatsapp`, `slack`, `generic`, `native`), an imported MCP alias, an
|
|
280
|
+
optional forced source, an optional webhook signing secret, and a routing
|
|
281
|
+
mode. The MCP tools `inbound_endpoint_create`, `inbound_endpoint_list`,
|
|
282
|
+
and `inbound_endpoint_delete` manage them. All endpoints are created via
|
|
283
|
+
`createInboundEndpointStore()` in `createGateway()` and share the same
|
|
284
|
+
`routeInboundMessage` router as the poll watcher and the legacy push route.
|
|
285
|
+
|
|
286
|
+
### Signature verification
|
|
287
|
+
|
|
288
|
+
When a secret is configured, the secret is REQUIRED and the bearer gate
|
|
289
|
+
is bypassed, so a publicly exposed webhook endpoint is not accidentally
|
|
290
|
+
opened by the loopback Origin allowlist.
|
|
291
|
+
|
|
292
|
+
| provider | header | algorithm/format |
|
|
293
|
+
|---|---|---|
|
|
294
|
+
| `agentpush` | `X-Agentpush-Signature` | `sha256=<hex-hmac-sha256>` |
|
|
295
|
+
| `telegram` | `X-Telegram-Bot-Api-Secret-Token` | exact secret, constant-time compare |
|
|
296
|
+
| `whatsapp` | `X-Hub-Signature-256` | `sha256=<hex-hmac-sha256>` |
|
|
297
|
+
| `slack` | `X-Slack-Signature` | Slack request signature v0 with replay window |
|
|
298
|
+
| `generic` | `X-Agentproto-Signature` | `sha256=<hex-hmac-sha256>` |
|
|
299
|
+
| `native` | n/a | uses the sessions bearer gate |
|
|
300
|
+
|
|
301
|
+
### Normalized envelope
|
|
302
|
+
|
|
303
|
+
After verification, the webhook JSON is normalized by `normalizeInbound()"
|
|
304
|
+
(`inbound-adapters.ts`) into the same `InboundMessage` shape the rest of
|
|
305
|
+
the transmitter expects:
|
|
306
|
+
|
|
307
|
+
```json
|
|
308
|
+
{
|
|
309
|
+
"alias": "<configured alias>",
|
|
310
|
+
"source": "<channel/phone/chat id>",
|
|
311
|
+
"contactRef": "<sender id>",
|
|
312
|
+
"text": "<message text>",
|
|
313
|
+
"messages": [ /* optional: raw provider event */ ]
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Provider-specific behaviour:
|
|
318
|
+
|
|
319
|
+
- `agentpush` and `slack` support a `challenge` handshake; the challenge is
|
|
320
|
+
echoed as `{ "challenge": "..." }` and no session is touched.
|
|
321
|
+
- `telegram` and `slack` ignore bot/self messages.
|
|
322
|
+
- Missing text returns `200 { "action": "ignored", "reason": "no_text" }` so
|
|
323
|
+
webhook providers do not retry forever.
|
|
324
|
+
|
|
325
|
+
### Deduplication
|
|
326
|
+
|
|
327
|
+
Each endpoint keeps an in-memory FIFO of the last ~500 seen provider
|
|
328
|
+
message ids via `markSeen()`. A duplicate returns `200 { "action":
|
|
329
|
+
"duplicate" }`.
|
|
330
|
+
|
|
331
|
+
### MCP tools
|
|
332
|
+
|
|
333
|
+
```json
|
|
334
|
+
{
|
|
335
|
+
"tool": "inbound_endpoint_create",
|
|
336
|
+
"arguments": {
|
|
337
|
+
"slug": "telegram-bot",
|
|
338
|
+
"provider": "telegram",
|
|
339
|
+
"alias": "agentpush",
|
|
340
|
+
"source": "@mybot",
|
|
341
|
+
"mode": "route-or-spawn",
|
|
342
|
+
"secret": "<optional>"
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
`inbound_endpoint_list` returns endpoints without exposing the secret.
|
|
348
|
+
`inbound_endpoint_delete` removes by slug.
|
|
349
|
+
|
|
350
|
+
## Follow-up: agentpush-side wiring (not covered by this repo)
|
|
351
|
+
|
|
352
|
+
This repo only adds the ingress endpoint. For the push path to actually
|
|
353
|
+
fire in production, agentpush itself needs to be configured to `POST` a
|
|
354
|
+
webhook payload to `<daemon-url>/inbound` (with the daemon's bearer
|
|
355
|
+
token) whenever it receives an inbound message, instead of relying solely
|
|
356
|
+
on `inbound_watcher_start`'s 5-second poll loop.
|