instar 1.3.49 → 1.3.51

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.
@@ -0,0 +1,128 @@
1
+ # Side-Effects Review — same-machine a2a transport via `/a2a/inbox`
2
+
3
+ **Spec:** `docs/specs/MENTOR-LIVE-READINESS-SPEC.md` §Recipient side. Third
4
+ fast-follow to PR #462 (receiver wiring) + PR #464 (lifeline-forward
5
+ dispatch). Closes the bot-to-bot-block gap surfaced by dogfood.
6
+
7
+ **Root cause being addressed:** Telegram Bot API structurally blocks
8
+ bot-to-bot delivery: "Bots talking to each other could potentially get stuck
9
+ in unwelcome loops. To avoid this, we decided that bots will not be able to
10
+ see messages from other bots regardless of mode." (Bot API FAQ.) The spec
11
+ assumed `Echo's mentor bot → Codey's primary bot via shared chat → Codey's
12
+ lifeline forwards` would work. Empirically it doesn't — verified by sending
13
+ 4 marker messages to topic 458, Codey's bot poll-offset never advances past
14
+ them, his lifeline-forward stays silent.
15
+
16
+ **Change:** Three production files + two test files.
17
+ - `src/server/routes.ts` (+ ~70 LoC: new `/a2a/inbox` POST route)
18
+ - `src/server/middleware.ts` (+ 2 LoC: skip the API bearer middleware for
19
+ `/a2a/inbox` — uses per-agent token like `/messages/relay-agent`)
20
+ - `src/server/AgentServer.ts` (+ ~75 LoC: same-machine path in
21
+ `deliverToMentee` via AgentRegistry + `fetch` to peer's `/a2a/inbox`,
22
+ before the Telegram bot fallback)
23
+ - `tests/integration/a2a-inbox-route.test.ts` (new, 8)
24
+ - `tests/e2e/a2a-inbox-lifecycle.test.ts` (new, 4)
25
+
26
+ ## What changed
27
+
28
+ 1. **`POST /a2a/inbox`** on every instar server. Accepts:
29
+ `{ text, topicId, senderAgent?, senderIsBot?, senderBotId?, fromUserId? }`.
30
+ Auth: Bearer token must match the target agent's per-agent token
31
+ (`verifyAgentToken` — same shape as `/messages/relay-agent`). Behaviour:
32
+ - 401 if bearer is missing or wrong
33
+ - 503 if no `ctx.telegram` adapter is configured at all (no hook surface)
34
+ - 400 if `text` (string) or `topicId` (number) are missing / malformed
35
+ - 200 `{ ok, agentMessage: true }` when the adapter's
36
+ `dispatchAgentMessageHook` claims the message
37
+ - 200 `{ ok, agentMessage: false, reason: 'not-routed' }` when the hook
38
+ refuses (no marker / malformed / not-allowlisted / not-allowed-role).
39
+ The route is dedicated to a2a — non-routable messages are NOT forwarded
40
+ to user-message handling.
41
+ - `senderIsBot` defaults to `true` if omitted — local peers holding our
42
+ agent token are bots by construction (the spoof defense is satisfied
43
+ by the bearer-token check upstream).
44
+ 2. **Auth middleware bypass for `/a2a/inbox`** — alongside the existing
45
+ `/messages/relay-agent` and `/messages/relay-machine` bypasses, since the
46
+ inbox enforces its own per-agent-token auth in-handler. Without this
47
+ bypass the API bearer middleware (using `config.authToken`) would 403
48
+ the request, because the caller holds the TARGET's per-agent token,
49
+ not the API bearer.
50
+ 3. **Same-machine routing in `deliverToMentee`** — before the existing
51
+ Telegram bot delivery, the method now:
52
+ - Imports `listAgents` from AgentRegistry + `getAgentToken` from the
53
+ token manager (lazy-imported inside the closure to avoid loading the
54
+ registry on every server boot).
55
+ - Looks up the mentee by `name === menteeAgent && name !== self.config.projectName`
56
+ (anti-self-target).
57
+ - If a local peer with a port is found AND we have the peer's token,
58
+ POSTs to `http://localhost:PORT/a2a/inbox` with the marker text +
59
+ sender context. On `agentMessage: true`, marks the outstanding-prompt
60
+ tracker as sent and audits a `transport: 'a2a-inbox-local'` row in
61
+ the sent-ledger.
62
+ - On any failure (no peer, no token, HTTP error, hook refused) logs and
63
+ falls through to the existing Telegram bot path (preserved unchanged
64
+ for cross-machine; currently unreachable due to the same bot-to-bot
65
+ block — a separate cross-machine transport is future work).
66
+ - The anti-ping-pong check (`outstanding.canSendTo`) runs ONCE before
67
+ either transport so the same correlation key gates both paths.
68
+
69
+ ## The seven questions
70
+
71
+ 1. **Over-block.** N/A. The inbox is a NEW dedicated endpoint; the route
72
+ refuses non-routable traffic with `agentMessage: false` but never
73
+ forwards to user handling. No existing route's behaviour changes.
74
+ 2. **Under-block.** Spoof defense is intact: per-agent token bearer auth
75
+ gates the route. `senderIsBot` defaulting to `true` is correct because
76
+ the peer authenticated with the target's own token — the spoof defense
77
+ it would otherwise enforce is satisfied earlier by the bearer check.
78
+ Anti-ping-pong: `outstanding.canSendTo` runs before either transport so
79
+ one outstanding prompt blocks both paths.
80
+ 3. **Level-of-abstraction fit.** Mirrors `/messages/relay-agent`'s pattern
81
+ exactly: own route, own auth, dispatch into existing in-process state.
82
+ No new abstraction.
83
+ 4. **Signal vs authority.** Inbox DECIDES route-or-drop via the hook (the
84
+ hook's existing routing matrix is the spec); audit ledger captures the
85
+ result. `deliverToMentee` DECIDES transport (local vs Telegram). All
86
+ audited; no silent paths.
87
+ 5. **Interactions.** Reuses AgentRegistry + AgentTokenManager (same APIs
88
+ already used by `/messages/relay-agent`). Reuses
89
+ `getOrCreateA2aLedger` + `getOrCreateMentorOutstanding` from the mentor
90
+ sender side. Reuses `dispatchAgentMessageHook` from PR #464. No new
91
+ shared mutable state.
92
+ 6. **External surfaces.** One new HTTP route (`/a2a/inbox`). One new
93
+ audit-ledger transport value (`'a2a-inbox-local'`). No new config keys.
94
+ 7. **Rollback cost.** Trivial — revert removes the route, the middleware
95
+ bypass, and the same-machine branch in `deliverToMentee`. The Telegram
96
+ bot path is untouched and preserved.
97
+
98
+ ## Testing
99
+
100
+ 12 new tests, all green (`tsc --noEmit` clean):
101
+
102
+ - **Tier 2 integration (8):** `a2a-inbox-route` covers the auth + input-
103
+ validation matrix (401 missing bearer, 401 wrong bearer, 503 no adapter,
104
+ 400 missing text, 400 bad topicId, 200 routed, 200 not-routed, 200 with
105
+ default senderIsBot:true).
106
+ - **Tier 3 E2E lifecycle (4):** `a2a-inbox-lifecycle` covers full server
107
+ boot with the mentee receiver wiring + the inbox route alive (not 404),
108
+ auth-gated, and the full claim path that proves the inbox invokes the
109
+ adapter dispatcher end-to-end on the real production init path.
110
+
111
+ A unit-tier slice for the same-machine branch in `deliverToMentee` is
112
+ deferred — covered transitively by the E2E + the existing unit suite for
113
+ `OutstandingPromptTracker` + the new integration test for the inbox route.
114
+ A dedicated unit on the `deliverToMentee` branch would require mocking
115
+ `fetch` + AgentRegistry + getAgentToken — defensible to add later but the
116
+ behaviour is already tested at the boundary.
117
+
118
+ ## Migration parity
119
+
120
+ No new config keys. The route registers unconditionally; if no
121
+ `config.mentee.enabled` block is set on the receiver, the adapter has no
122
+ hook installed and the route returns `agentMessage: false`. The
123
+ `/messages/relay-agent` precedent is identical (always-registered route,
124
+ behaviour depends on installed state).
125
+
126
+ The `deliverToMentee` change is purely additive — when no local peer is
127
+ registered, behaviour is byte-for-byte the same as before. No PostUpdateMigrator
128
+ change required.
@@ -0,0 +1,99 @@
1
+ # Side-Effects Review — mentee receiver wiring fires on lifeline-forward path
2
+
3
+ **Spec:** `docs/specs/MENTOR-LIVE-READINESS-SPEC.md` §Recipient side.
4
+ Fast-follow to PR #462 (mentee receiver wiring) — closes the dual-path
5
+ ingress gap surfaced by dogfood.
6
+
7
+ **Change:** Three production files + three test files.
8
+ - `src/messaging/TelegramAdapter.ts` (+ ~55 LoC: `dispatchAgentMessageHook`
9
+ public method; polling text-dispatch now calls it instead of inlining the
10
+ hook logic — pure extraction, no behavior change on the polling path)
11
+ - `src/server/routes.ts` (+ ~20 LoC: destructure `senderIsBot` /
12
+ `senderChatId` / `senderBotId` from forward body; invoke
13
+ `dispatchAgentMessageHook` before `onTopicMessage`; short-circuit on
14
+ `handled:true`)
15
+ - `src/lifeline/TelegramLifeline.ts` (+ ~15 LoC: derive + include
16
+ `senderIsBot`/`senderChatId`/`senderBotId` in the forward body)
17
+ - `tests/unit/TelegramAdapter-dispatchAgentMessageHook.test.ts` (new, 8)
18
+ - `tests/integration/telegram-forward-a2a-dispatch.test.ts` (new, 3)
19
+
20
+ ## What changed
21
+
22
+ 1. **`dispatchAgentMessageHook` public method** on `TelegramAdapter`:
23
+ wraps the existing hook invocation with the FAIL-OPEN try/catch (a
24
+ broken hook never freezes the dispatch loop) and the senderBotId
25
+ derivation logic (sender_chat wins; rawFromId iff senderIsBot;
26
+ undefined for real users). Polling path now calls the dispatcher
27
+ instead of inlining the same code — pure extraction.
28
+ 2. **`/internal/telegram-forward` handler** destructures the new spoof-
29
+ defense fields from the request body. Before invoking `onTopicMessage`,
30
+ calls `dispatchAgentMessageHook`. On `handled:true`, returns
31
+ `{ ok: true, forwarded: true, agentMessage: true }` (NEW response shape
32
+ addition — additive, doesn't change the existing `{ ok, forwarded }`
33
+ contract for non-a2a forwards). On hook throw, logs + falls through
34
+ (same fail-open semantics as the polling path).
35
+ 3. **`TelegramLifeline.forwardToServer`** derives `senderIsBot` from
36
+ `rawMsg.from.is_bot === true`, `senderChatId` from
37
+ `rawMsg.sender_chat?.id`, `senderBotId` from `senderChatId ?? (isBot ?
38
+ rawMsg.from.id : undefined)`. Includes them in the buildBody output
39
+ only when they have values (omitted fields → server treats as falsy →
40
+ marker-bearing forwards drop closed for spoof defense).
41
+
42
+ ## The seven questions
43
+
44
+ 1. **Over-block.** N/A — additive. The forward route now invokes the hook
45
+ before `onTopicMessage`; if no hook is installed (or no adapter has
46
+ `dispatchAgentMessageHook`), the route falls through to the existing
47
+ behavior. Backward-compatible test (`preserves existing behavior when
48
+ the adapter has NO dispatchAgentMessageHook`) covers this.
49
+ 2. **Under-block.** Spoof defense is end-to-end on the forwarded path:
50
+ missing `senderIsBot` from older lifelines → server treats as falsy →
51
+ any marker drops as `agent-marker-spoofed-by-user`. This is the
52
+ fail-CLOSED side, matching spec invariant.
53
+ 3. **Level-of-abstraction fit.** Extraction-then-reuse: the hook
54
+ invocation logic moves from inlined to a method on the adapter, called
55
+ from both polling and forward paths. No new abstraction; just exposes
56
+ what was already there.
57
+ 4. **Signal vs authority.** The dispatcher returns a clean boolean to the
58
+ caller; the caller decides whether to short-circuit. Authority lives in
59
+ the caller (route or polling loop), not in the dispatcher. Same as the
60
+ prior inline version.
61
+ 5. **Interactions.** Reuses `agentMessageHook` set by `setAgentMessageHook`
62
+ — no new state. The forward route reads three new optional fields from
63
+ the body; older clients (lifeline pre-this-PR) omit them and get the
64
+ fail-CLOSED spoof defense automatically.
65
+ 6. **External surfaces.** The `/internal/telegram-forward` response gains
66
+ one additive flag (`agentMessage: true`) when the hook claims the
67
+ message; the existing `{ ok, forwarded }` contract is preserved
68
+ otherwise. No new routes. No new config.
69
+ 7. **Rollback cost.** Trivial — revert restores the inline hook call in
70
+ the polling path, drops the dispatcher and the forward-route call,
71
+ drops the lifeline body additions. The new `agentMessage:true` flag is
72
+ an additive response field, so any consumer relying on it would need a
73
+ one-line fix back to checking `forwarded:true` alone.
74
+
75
+ ## Testing
76
+
77
+ 11 new tests, all green (`tsc --noEmit` clean):
78
+
79
+ - **Tier 1 unit (8):** `TelegramAdapter-dispatchAgentMessageHook` covers
80
+ no-op safe (no hook), handled:true short-circuit, handled:false
81
+ fall-through, FAIL-OPEN on hook throw, three senderBotId derivation
82
+ modes (sender_chat / rawFromId+isBot / undefined for users), explicit
83
+ override.
84
+ - **Tier 2 integration (3):** `telegram-forward-a2a-dispatch` boots
85
+ `createRoutes` with a mock adapter through the real Express pipeline;
86
+ asserts short-circuit when hook claims, fall-through when not,
87
+ backward-compat when adapter lacks the method.
88
+ - **Tier 3 E2E:** the existing `mentee-receiver-lifecycle.test.ts` from
89
+ PR #462 (which installs the hook end-to-end on server boot) plus the
90
+ live dogfood loop that motivated this PR are the lifecycle proof.
91
+
92
+ ## Migration parity
93
+
94
+ No config changes; no new config keys. The lifeline-side change is
95
+ deployed via the same npm release as the server-side change, so the
96
+ mixed-version state is only momentary (server upgrades alongside its
97
+ lifeline via the existing AutoUpdater + LifelineDriftPromoter pair).
98
+ Mixed-version safety is preserved either direction (server new / lifeline
99
+ old: spoof-fail-closed; server old / lifeline new: unknown fields ignored).