instar 1.3.28 → 1.3.29
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/dist/messaging/AgentTelegramComms.d.ts +156 -0
- package/dist/messaging/AgentTelegramComms.d.ts.map +1 -0
- package/dist/messaging/AgentTelegramComms.js +170 -0
- package/dist/messaging/AgentTelegramComms.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.29.md +42 -0
- package/upgrades/side-effects/agent-telegram-comms-primitive.md +75 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentTelegramComms — agent-to-agent Telegram comms primitive.
|
|
3
|
+
*
|
|
4
|
+
* Spec: docs/specs/MENTOR-LIVE-READINESS-SPEC.md §Fix 2a (approved 2026-05-27).
|
|
5
|
+
*
|
|
6
|
+
* A robust, recipient-knows-it's-from-an-agent channel any agent can use to message
|
|
7
|
+
* another agent's bot over Telegram, with the indicator AND the anti-loop machinery as
|
|
8
|
+
* first-class infra. Justin's framing: "robust infra that indicates the message is from
|
|
9
|
+
* another agent… leverage infra to prevent the ping pong trap." The mentor is its first
|
|
10
|
+
* consumer (see MentorOnboarding*), but nothing here is mentor-specific.
|
|
11
|
+
*
|
|
12
|
+
* Every message carries a VISIBLE marker prefix in the body so (a) humans can audit at a
|
|
13
|
+
* glance and (b) the Telegram chat history alone reconstructs a round-trip when ledgers
|
|
14
|
+
* are unavailable:
|
|
15
|
+
*
|
|
16
|
+
* [a2a:from=<sender> to=<recipient> role=<role> id=<id> corr=<corr> ts=<unix-ms> v=1]
|
|
17
|
+
*
|
|
18
|
+
* <body>
|
|
19
|
+
*
|
|
20
|
+
* This file is the PURE logic (marker parse/format + routing decision + cycle-detection
|
|
21
|
+
* key). I/O (the actual Telegram send + the audit ledgers + the processed-id store) is
|
|
22
|
+
* injected so every branch is unit-testable. See §Fix 2a for the routing matrix and the
|
|
23
|
+
* anti-loop invariants this enforces.
|
|
24
|
+
*/
|
|
25
|
+
/** Current marker schema version. Bumps are explicit (reader-first rule — see spec). */
|
|
26
|
+
export declare const A2A_VERSION = 1;
|
|
27
|
+
export interface A2aMarker {
|
|
28
|
+
from: string;
|
|
29
|
+
to: string;
|
|
30
|
+
role: string;
|
|
31
|
+
id: string;
|
|
32
|
+
corr: string;
|
|
33
|
+
ts: number;
|
|
34
|
+
v: number;
|
|
35
|
+
}
|
|
36
|
+
export interface A2aMessage extends A2aMarker {
|
|
37
|
+
body: string;
|
|
38
|
+
}
|
|
39
|
+
export interface A2aSendFields {
|
|
40
|
+
from: string;
|
|
41
|
+
to: string;
|
|
42
|
+
role: string;
|
|
43
|
+
/** Stable id; minted by the caller/sender if absent. */
|
|
44
|
+
id: string;
|
|
45
|
+
/** Correlation id. Prompts self-correlate (corr === id); replies carry the prompt's id. */
|
|
46
|
+
corr: string;
|
|
47
|
+
ts: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Result of parsing an inbound Telegram message body for an a2a marker.
|
|
51
|
+
* - `no-marker`: not an agent message → caller falls through to normal user handling.
|
|
52
|
+
* - `malformed`: marker-shaped but invalid → caller DROPS (security event), never falls
|
|
53
|
+
* through to user handling (spoof / broken-sender defense).
|
|
54
|
+
*/
|
|
55
|
+
export type ParseResult = {
|
|
56
|
+
ok: true;
|
|
57
|
+
msg: A2aMessage;
|
|
58
|
+
} | {
|
|
59
|
+
ok: false;
|
|
60
|
+
kind: 'no-marker';
|
|
61
|
+
} | {
|
|
62
|
+
ok: false;
|
|
63
|
+
kind: 'malformed';
|
|
64
|
+
detail: string;
|
|
65
|
+
};
|
|
66
|
+
/** Format a marker + body for sending. The inverse of {@link parseMarker}. */
|
|
67
|
+
export declare function formatMarker(fields: A2aSendFields, body: string): string;
|
|
68
|
+
/** Parse an inbound message body. Strict — see {@link ParseResult}. */
|
|
69
|
+
export declare function parseMarker(raw: string): ParseResult;
|
|
70
|
+
/** Per-recipient routing configuration (spec §Fix 2a recipient + anti-loop). */
|
|
71
|
+
export interface RecipientConfig {
|
|
72
|
+
/** This agent's own name (the `to` an inbound message must target). */
|
|
73
|
+
localAgent: string;
|
|
74
|
+
/** Known agents → their Telegram bot identity. Only senders here are trusted. */
|
|
75
|
+
knownAgents: Record<string, {
|
|
76
|
+
botId: string;
|
|
77
|
+
}>;
|
|
78
|
+
/**
|
|
79
|
+
* Per-source role acceptance (round-2 adversarial F6): `{ fromAgent: allowedRoles[] }`.
|
|
80
|
+
* A compromised/buggy known agent sending an unexpected role is dropped, even if that
|
|
81
|
+
* role is accepted from a DIFFERENT source. Scoped admission.
|
|
82
|
+
*/
|
|
83
|
+
acceptRoles: Record<string, string[]>;
|
|
84
|
+
/** Replay window: reject markers whose ts is outside ±this from now. Default 24h. */
|
|
85
|
+
skewWindowMs: number;
|
|
86
|
+
/** Highest marker version this recipient understands. Newer → dead-letter, not crash. */
|
|
87
|
+
maxVersion: number;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Inbound message context the caller resolves from the Telegram update. The bot-identity
|
|
91
|
+
* fields are the SPOOF DEFENSE: a human user typing a marker-shaped string has
|
|
92
|
+
* `senderIsBot === false` and no `senderChatId` → dropped (round-2 adversarial F1).
|
|
93
|
+
*/
|
|
94
|
+
export interface IncomingContext {
|
|
95
|
+
/** The raw message body (marker + body, or arbitrary user text). */
|
|
96
|
+
raw: string;
|
|
97
|
+
/** `from.is_bot` from the Telegram update. */
|
|
98
|
+
senderIsBot: boolean;
|
|
99
|
+
/** `sender_chat.id` from the Telegram update when present (group bot-as-channel relay).
|
|
100
|
+
* Its mere presence is part of the spoof defense (a human user has none). */
|
|
101
|
+
senderChatId?: string;
|
|
102
|
+
/** The effective sender bot identity = `sender_chat.id ?? from.id`. Matched against the
|
|
103
|
+
* allowlist's `botId`. The caller resolves this from the Telegram update. */
|
|
104
|
+
senderBotId?: string;
|
|
105
|
+
/** Wall-clock now (ms) — injected for testability. */
|
|
106
|
+
now: number;
|
|
107
|
+
}
|
|
108
|
+
export type RouteDropReason = 'agent-marker-malformed' | 'agent-marker-stale-or-future' | 'agent-marker-spoofed-by-user' | 'agent-marker-wrong-recipient' | 'agent-marker-unsupported-version' | 'agent-marker-unknown' | 'agent-marker-duplicate' | 'agent-marker-role-not-allowed-from-source' | 'agent-marker-unexpected-role' | 'agent-marker-unknown-role';
|
|
109
|
+
export type RouteDecision = {
|
|
110
|
+
action: 'fall-through';
|
|
111
|
+
} | {
|
|
112
|
+
action: 'route';
|
|
113
|
+
msg: A2aMessage;
|
|
114
|
+
} | {
|
|
115
|
+
action: 'drop';
|
|
116
|
+
reason: RouteDropReason;
|
|
117
|
+
detail?: string;
|
|
118
|
+
msg?: A2aMessage;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* The recipient routing matrix (spec §Fix 2a "Routing matrix"). PURE — every branch is a
|
|
122
|
+
* decision the caller then acts on (route to a role-handler, or drop + write an audit
|
|
123
|
+
* row). `knownRole` tells whether the role is one this recipient has a handler for at all;
|
|
124
|
+
* `isProcessed` is the idempotency check against the processed-id store.
|
|
125
|
+
*/
|
|
126
|
+
export declare function decideRoute(ctx: IncomingContext, cfg: RecipientConfig, deps: {
|
|
127
|
+
isProcessed: (id: string) => boolean;
|
|
128
|
+
knownRole: (role: string) => boolean;
|
|
129
|
+
}): RouteDecision;
|
|
130
|
+
/**
|
|
131
|
+
* Cycle-detection key (spec §Fix 2a anti-loop #3): keyed on the full tuple so legitimate
|
|
132
|
+
* unrelated traffic never collides. `corr` is always present (required in the marker), so
|
|
133
|
+
* the key cannot collapse to `undefined`.
|
|
134
|
+
*/
|
|
135
|
+
export declare function cycleKey(fields: {
|
|
136
|
+
fromBotId: string;
|
|
137
|
+
toBotId: string;
|
|
138
|
+
topicId: number | string;
|
|
139
|
+
role: string;
|
|
140
|
+
corr: string;
|
|
141
|
+
}): string;
|
|
142
|
+
/**
|
|
143
|
+
* In-memory cycle detector. A send is refused (unless `cycleOk`) when the same key was
|
|
144
|
+
* seen within `windowMs` — the structural guard against tight role↔reply ping-pong.
|
|
145
|
+
*/
|
|
146
|
+
export declare class CycleDetector {
|
|
147
|
+
private readonly windowMs;
|
|
148
|
+
private readonly recent;
|
|
149
|
+
constructor(windowMs: number);
|
|
150
|
+
/** Returns true if sending this key now would trip cycle-detection. */
|
|
151
|
+
wouldTrip(key: string, now: number): boolean;
|
|
152
|
+
/** Record a send/receive of this key at `now`. */
|
|
153
|
+
mark(key: string, now: number): void;
|
|
154
|
+
private evict;
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=AgentTelegramComms.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AgentTelegramComms.d.ts","sourceRoot":"","sources":["../../src/messaging/AgentTelegramComms.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentTelegramComms — agent-to-agent Telegram comms primitive.
|
|
3
|
+
*
|
|
4
|
+
* Spec: docs/specs/MENTOR-LIVE-READINESS-SPEC.md §Fix 2a (approved 2026-05-27).
|
|
5
|
+
*
|
|
6
|
+
* A robust, recipient-knows-it's-from-an-agent channel any agent can use to message
|
|
7
|
+
* another agent's bot over Telegram, with the indicator AND the anti-loop machinery as
|
|
8
|
+
* first-class infra. Justin's framing: "robust infra that indicates the message is from
|
|
9
|
+
* another agent… leverage infra to prevent the ping pong trap." The mentor is its first
|
|
10
|
+
* consumer (see MentorOnboarding*), but nothing here is mentor-specific.
|
|
11
|
+
*
|
|
12
|
+
* Every message carries a VISIBLE marker prefix in the body so (a) humans can audit at a
|
|
13
|
+
* glance and (b) the Telegram chat history alone reconstructs a round-trip when ledgers
|
|
14
|
+
* are unavailable:
|
|
15
|
+
*
|
|
16
|
+
* [a2a:from=<sender> to=<recipient> role=<role> id=<id> corr=<corr> ts=<unix-ms> v=1]
|
|
17
|
+
*
|
|
18
|
+
* <body>
|
|
19
|
+
*
|
|
20
|
+
* This file is the PURE logic (marker parse/format + routing decision + cycle-detection
|
|
21
|
+
* key). I/O (the actual Telegram send + the audit ledgers + the processed-id store) is
|
|
22
|
+
* injected so every branch is unit-testable. See §Fix 2a for the routing matrix and the
|
|
23
|
+
* anti-loop invariants this enforces.
|
|
24
|
+
*/
|
|
25
|
+
/** Current marker schema version. Bumps are explicit (reader-first rule — see spec). */
|
|
26
|
+
export const A2A_VERSION = 1;
|
|
27
|
+
/** Marker field-value charset (spec: deterministic parsing, avoids invisible/escaping
|
|
28
|
+
* cases). Agent/role/id/corr match this; ts and v are positive integers. */
|
|
29
|
+
const TOKEN = '[A-Za-z0-9._:-]+';
|
|
30
|
+
/**
|
|
31
|
+
* Strict marker regex — anchored to the start, consumes exactly the first line plus the
|
|
32
|
+
* required blank-line separator, then captures the body. Field ORDER is fixed (so the
|
|
33
|
+
* parse is deterministic and a reordered/extra-field marker is "malformed", not silently
|
|
34
|
+
* accepted). All of from/to/role/id/corr/ts/v are REQUIRED (corr + ts required per
|
|
35
|
+
* round-2/3 convergence — missing corr would collapse the cycle-detection key; missing ts
|
|
36
|
+
* disables replay-window defense).
|
|
37
|
+
*/
|
|
38
|
+
const MARKER_RE = new RegExp(`^\\[a2a:from=(${TOKEN}) to=(${TOKEN}) role=(${TOKEN}) id=(${TOKEN}) corr=(${TOKEN}) ts=([0-9]+) v=([0-9]+)\\]\\n\\n([\\s\\S]*)$`);
|
|
39
|
+
/** A marker-shaped first line WITHOUT the strict full match — used only to distinguish
|
|
40
|
+
* "not an agent message at all" (fall through to user handling) from "looks like one but
|
|
41
|
+
* is malformed" (drop as a security event, NEVER fall through). */
|
|
42
|
+
const MARKER_PREFIX_RE = /^\[a2a:/;
|
|
43
|
+
/** Format a marker + body for sending. The inverse of {@link parseMarker}. */
|
|
44
|
+
export function formatMarker(fields, body) {
|
|
45
|
+
for (const [k, v] of Object.entries({
|
|
46
|
+
from: fields.from,
|
|
47
|
+
to: fields.to,
|
|
48
|
+
role: fields.role,
|
|
49
|
+
id: fields.id,
|
|
50
|
+
corr: fields.corr,
|
|
51
|
+
})) {
|
|
52
|
+
if (!new RegExp(`^${TOKEN}$`).test(String(v))) {
|
|
53
|
+
throw new Error(`AgentTelegramComms.formatMarker: field "${k}" value "${v}" violates the a2a charset ${TOKEN}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!Number.isInteger(fields.ts) || fields.ts <= 0) {
|
|
57
|
+
throw new Error(`AgentTelegramComms.formatMarker: ts must be a positive integer, got ${fields.ts}`);
|
|
58
|
+
}
|
|
59
|
+
return (`[a2a:from=${fields.from} to=${fields.to} role=${fields.role} id=${fields.id} ` +
|
|
60
|
+
`corr=${fields.corr} ts=${fields.ts} v=${A2A_VERSION}]\n\n${body}`);
|
|
61
|
+
}
|
|
62
|
+
/** Parse an inbound message body. Strict — see {@link ParseResult}. */
|
|
63
|
+
export function parseMarker(raw) {
|
|
64
|
+
if (typeof raw !== 'string' || !MARKER_PREFIX_RE.test(raw)) {
|
|
65
|
+
return { ok: false, kind: 'no-marker' };
|
|
66
|
+
}
|
|
67
|
+
const m = MARKER_RE.exec(raw);
|
|
68
|
+
if (!m) {
|
|
69
|
+
return { ok: false, kind: 'malformed', detail: 'marker prefix present but failed strict parse' };
|
|
70
|
+
}
|
|
71
|
+
const [, from, to, role, id, corr, tsStr, vStr, body] = m;
|
|
72
|
+
const ts = Number(tsStr);
|
|
73
|
+
const v = Number(vStr);
|
|
74
|
+
if (!Number.isInteger(ts) || ts <= 0) {
|
|
75
|
+
return { ok: false, kind: 'malformed', detail: `invalid ts "${tsStr}"` };
|
|
76
|
+
}
|
|
77
|
+
if (!Number.isInteger(v) || v <= 0) {
|
|
78
|
+
return { ok: false, kind: 'malformed', detail: `invalid v "${vStr}"` };
|
|
79
|
+
}
|
|
80
|
+
return { ok: true, msg: { from, to, role, id, corr, ts, v, body } };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The recipient routing matrix (spec §Fix 2a "Routing matrix"). PURE — every branch is a
|
|
84
|
+
* decision the caller then acts on (route to a role-handler, or drop + write an audit
|
|
85
|
+
* row). `knownRole` tells whether the role is one this recipient has a handler for at all;
|
|
86
|
+
* `isProcessed` is the idempotency check against the processed-id store.
|
|
87
|
+
*/
|
|
88
|
+
export function decideRoute(ctx, cfg, deps) {
|
|
89
|
+
const parsed = parseMarker(ctx.raw);
|
|
90
|
+
if (!parsed.ok && parsed.kind === 'no-marker')
|
|
91
|
+
return { action: 'fall-through' };
|
|
92
|
+
if (!parsed.ok)
|
|
93
|
+
return { action: 'drop', reason: 'agent-marker-malformed', detail: parsed.detail };
|
|
94
|
+
const msg = parsed.msg;
|
|
95
|
+
// Replay / clock-skew window (round-2 adversarial F2).
|
|
96
|
+
if (Math.abs(ctx.now - msg.ts) > cfg.skewWindowMs) {
|
|
97
|
+
return { action: 'drop', reason: 'agent-marker-stale-or-future', msg };
|
|
98
|
+
}
|
|
99
|
+
// Unsupported version → dead-letter, never crash (reader-first rule).
|
|
100
|
+
if (msg.v > cfg.maxVersion) {
|
|
101
|
+
return { action: 'drop', reason: 'agent-marker-unsupported-version', msg };
|
|
102
|
+
}
|
|
103
|
+
// SPOOF DEFENSE (round-2 adversarial F1): a real user typing a marker-shaped string is
|
|
104
|
+
// not a bot and has no sender_chat. Drop before any allowlist/role check — a human-typed
|
|
105
|
+
// marker must NEVER reach a role-handler, even if from/id happen to match.
|
|
106
|
+
if (!ctx.senderIsBot && ctx.senderChatId === undefined) {
|
|
107
|
+
return { action: 'drop', reason: 'agent-marker-spoofed-by-user', msg };
|
|
108
|
+
}
|
|
109
|
+
// Recipient targeting.
|
|
110
|
+
if (msg.to !== cfg.localAgent) {
|
|
111
|
+
return { action: 'drop', reason: 'agent-marker-wrong-recipient', msg };
|
|
112
|
+
}
|
|
113
|
+
// Sender allowlist + bot-identity match.
|
|
114
|
+
const known = cfg.knownAgents[msg.from];
|
|
115
|
+
if (!known || (ctx.senderBotId !== undefined && known.botId !== ctx.senderBotId)) {
|
|
116
|
+
return { action: 'drop', reason: 'agent-marker-unknown', msg };
|
|
117
|
+
}
|
|
118
|
+
// Idempotency (round-2 adversarial: Telegram retry / adapter restart).
|
|
119
|
+
if (deps.isProcessed(msg.id)) {
|
|
120
|
+
return { action: 'drop', reason: 'agent-marker-duplicate', msg };
|
|
121
|
+
}
|
|
122
|
+
// Per-source role acceptance (round-2 adversarial F6): scoped admission.
|
|
123
|
+
const allowedFromSource = cfg.acceptRoles[msg.from] ?? [];
|
|
124
|
+
if (!allowedFromSource.includes(msg.role)) {
|
|
125
|
+
// Distinguish "this recipient has no handler for the role at all" from "the role
|
|
126
|
+
// exists but isn't allowed from THIS source" — both drop, different audit codes.
|
|
127
|
+
if (!deps.knownRole(msg.role)) {
|
|
128
|
+
return { action: 'drop', reason: 'agent-marker-unknown-role', msg };
|
|
129
|
+
}
|
|
130
|
+
return { action: 'drop', reason: 'agent-marker-role-not-allowed-from-source', msg };
|
|
131
|
+
}
|
|
132
|
+
return { action: 'route', msg };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Cycle-detection key (spec §Fix 2a anti-loop #3): keyed on the full tuple so legitimate
|
|
136
|
+
* unrelated traffic never collides. `corr` is always present (required in the marker), so
|
|
137
|
+
* the key cannot collapse to `undefined`.
|
|
138
|
+
*/
|
|
139
|
+
export function cycleKey(fields) {
|
|
140
|
+
return `${fields.fromBotId}|${fields.toBotId}|${fields.topicId}|${fields.role}|${fields.corr}`;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* In-memory cycle detector. A send is refused (unless `cycleOk`) when the same key was
|
|
144
|
+
* seen within `windowMs` — the structural guard against tight role↔reply ping-pong.
|
|
145
|
+
*/
|
|
146
|
+
export class CycleDetector {
|
|
147
|
+
windowMs;
|
|
148
|
+
recent = new Map();
|
|
149
|
+
constructor(windowMs) {
|
|
150
|
+
this.windowMs = windowMs;
|
|
151
|
+
}
|
|
152
|
+
/** Returns true if sending this key now would trip cycle-detection. */
|
|
153
|
+
wouldTrip(key, now) {
|
|
154
|
+
this.evict(now);
|
|
155
|
+
const last = this.recent.get(key);
|
|
156
|
+
return last !== undefined && now - last < this.windowMs;
|
|
157
|
+
}
|
|
158
|
+
/** Record a send/receive of this key at `now`. */
|
|
159
|
+
mark(key, now) {
|
|
160
|
+
this.recent.set(key, now);
|
|
161
|
+
this.evict(now);
|
|
162
|
+
}
|
|
163
|
+
evict(now) {
|
|
164
|
+
for (const [k, t] of this.recent) {
|
|
165
|
+
if (now - t >= this.windowMs)
|
|
166
|
+
this.recent.delete(k);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=AgentTelegramComms.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AgentTelegramComms.js","sourceRoot":"","sources":["../../src/messaging/AgentTelegramComms.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-05-27T21:
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-05-27T21:27:57.206Z",
|
|
5
|
+
"instarVersion": "1.3.29",
|
|
6
6
|
"entryCount": 192,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Instar Upgrade Guide — NEXT
|
|
2
|
+
|
|
3
|
+
<!-- bump: minor -->
|
|
4
|
+
|
|
5
|
+
## What Changed
|
|
6
|
+
|
|
7
|
+
**New building block: agent-to-agent Telegram comms primitive (core logic).** This is the
|
|
8
|
+
first piece of a robust, reusable way for two Instar agents to talk over Telegram *knowingly*
|
|
9
|
+
— every agent-to-agent message carries a visible marker
|
|
10
|
+
(`[a2a:from=… to=… role=… id=… corr=… ts=… v=1]`) so the receiving side can tell it's from
|
|
11
|
+
another agent and apply anti-loop machinery, rather than treating it like a human user
|
|
12
|
+
message. This increment ships the pure, security-critical core: the strict marker
|
|
13
|
+
parser/formatter, the recipient routing decision (with the full drop matrix — malformed,
|
|
14
|
+
replay-window, user-spoof, wrong-recipient, unknown-sender, duplicate, and per-source role
|
|
15
|
+
admission), and the cycle-detection key. It ships **dark** — nothing wires it yet (the
|
|
16
|
+
TelegramAdapter integration and the first consumer, the mentor, land in follow-up PRs).
|
|
17
|
+
|
|
18
|
+
## What to Tell Your User
|
|
19
|
+
|
|
20
|
+
- Nothing changes in how your agent behaves today — this is groundwork. It's the foundation
|
|
21
|
+
for agents being able to message each other over Telegram safely (with built-in defenses
|
|
22
|
+
so they can't spoof each other or get stuck in a reply loop), which a later update will
|
|
23
|
+
switch on for the mentor feature.
|
|
24
|
+
|
|
25
|
+
## Summary of New Capabilities
|
|
26
|
+
|
|
27
|
+
| Capability | How to Use |
|
|
28
|
+
|-----------|-----------|
|
|
29
|
+
| `AgentTelegramComms` primitive (core) | Internal `parseMarker` / `formatMarker` / `decideRoute` / `CycleDetector` in `src/messaging/AgentTelegramComms.ts` — pure logic, I/O injected; not wired yet |
|
|
30
|
+
|
|
31
|
+
## Evidence
|
|
32
|
+
|
|
33
|
+
**Net-new feature, not a bug fix** — a new pure-logic module that nothing imports yet
|
|
34
|
+
(ships dark). Proven by 20 unit tests covering every branch of the security-critical logic:
|
|
35
|
+
marker parse/format (valid; no-marker vs malformed incl. missing `corr`/`ts`, charset
|
|
36
|
+
violation, no separator; round-trip); the full routing matrix (route + fall-through + every
|
|
37
|
+
drop reason, including the **user-spoof defense** — a human typing a marker-shaped string is
|
|
38
|
+
dropped even when from/id match an allowlisted bot — and **per-source role acceptance** — a
|
|
39
|
+
known role from the wrong source is dropped); and cycle-detection (key never collapses,
|
|
40
|
+
trips within window, no false collision). `tsc --noEmit` clean. The integration + e2e tiers
|
|
41
|
+
(real TelegramAdapter wiring, the bidirectional contract test, the supervised live cycle)
|
|
42
|
+
land with the follow-up PRs per the spec's staged plan.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Side-Effects Review — Agent-to-Agent Telegram comms primitive (core logic, PR 1)
|
|
2
|
+
|
|
3
|
+
**Spec:** `docs/specs/MENTOR-LIVE-READINESS-SPEC.md` §Fix 2a (converged, approved by Justin
|
|
4
|
+
2026-05-27). Convergence report: `docs/specs/reports/mentor-live-readiness-convergence.md`.
|
|
5
|
+
**Change:** New module `src/messaging/AgentTelegramComms.ts` — the PURE, security-critical
|
|
6
|
+
core of the agent-to-agent Telegram comms primitive: strict marker parse/format, the
|
|
7
|
+
recipient routing matrix (every drop branch), and cycle-detection. I/O (the actual Telegram
|
|
8
|
+
send, audit ledgers, processed-id store) is **injected**, so this PR is pure logic + tests;
|
|
9
|
+
the TelegramAdapter wiring + ledger persistence is PR 2, the mentor consumer is PR 3.
|
|
10
|
+
**Files:** `src/messaging/AgentTelegramComms.ts` (new), `tests/unit/messaging/AgentTelegramComms.test.ts` (new).
|
|
11
|
+
**Ships dark:** nothing imports the module yet except its test — zero runtime behavior change.
|
|
12
|
+
|
|
13
|
+
## Principle check (Phase 1)
|
|
14
|
+
|
|
15
|
+
Decision point? Yes — this is the recipient admission/routing decision (`decideRoute`) and
|
|
16
|
+
the anti-loop cycle key. It is built as PURE functions returning a decision the caller acts
|
|
17
|
+
on; the blocking authority (drop vs route) is explicit + exhaustively tested. No existing
|
|
18
|
+
gate is loosened (nothing wired yet).
|
|
19
|
+
|
|
20
|
+
## The seven questions
|
|
21
|
+
|
|
22
|
+
1. **Over-block.** `decideRoute` drops on every ambiguous/unrecognized shape (malformed,
|
|
23
|
+
stale, spoofed, wrong-recipient, unknown-version, unknown-sender, duplicate,
|
|
24
|
+
role-not-allowed). Correct posture for a security boundary — a marker-shaped string is
|
|
25
|
+
guilty until allowlisted. The only fall-through is a genuine no-marker (normal user msg).
|
|
26
|
+
2. **Under-block.** The spoof defense (`!senderIsBot && senderChatId===undefined` → drop)
|
|
27
|
+
is the round-2 adversarial F1 closure — a human typing a marker can NEVER reach a
|
|
28
|
+
role-handler, even if from/id match. Tested explicitly. `corr`/`ts` are required in the
|
|
29
|
+
parser so neither the cycle key nor the replay window can be bypassed by omission.
|
|
30
|
+
3. **Level-of-abstraction fit.** Pure logic separated from I/O — the security decisions are
|
|
31
|
+
unit-testable in isolation (20 tests, every branch). I/O injection deferred to PR 2.
|
|
32
|
+
4. **Signal vs authority.** `decideRoute` IS the authority (drop/route); it returns a typed
|
|
33
|
+
decision + audit reason for every branch (no silent drops — caller writes the audit row).
|
|
34
|
+
5. **Interactions.** Module is dark (imported only by its test). No interaction with the
|
|
35
|
+
running system yet. PR 2 wires it into TelegramAdapter (multi-instance + handler-chain);
|
|
36
|
+
PR 3 wires the mentor consumer. Each subsequent PR re-runs the gate against the same spec.
|
|
37
|
+
6. **External surfaces.** None in this PR (no routes, no config consumed yet). The marker
|
|
38
|
+
format is the wire contract for PR 2/3 + Codey's side; `A2A_VERSION` is exported for
|
|
39
|
+
version pinning.
|
|
40
|
+
7. **Rollback cost.** Trivial — revert removes an unused module. No data, no migration.
|
|
41
|
+
|
|
42
|
+
## Build-time refinements beyond the spec text
|
|
43
|
+
|
|
44
|
+
- The spec mentioned `state/a2a-*.jsonl` (append-only JSONL audit) + a processed-id JSON
|
|
45
|
+
store. Convention note (MessageProcessingLedger header: "NOT a new ad-hoc JSON file") —
|
|
46
|
+
PR 2 will implement the processed-id store on the SQLite path (matching
|
|
47
|
+
PendingRelayStore/CommitmentTracker) rather than a JSON file; the append-only audit
|
|
48
|
+
ledgers stay JSONL (Codey designed the audit-row schema as JSONL, and append-only JSONL
|
|
49
|
+
is the right shape for a forensic trail). Flagged here so PR 2's choice is intentional.
|
|
50
|
+
- `senderBotId` resolution (`sender_chat.id ?? from.id`) + `senderIsBot` are passed in by
|
|
51
|
+
the caller (PR 2 resolves them from the Telegram update, after the Message type is
|
|
52
|
+
extended to expose `is_bot`/`sender_chat`). The pure core only consumes them.
|
|
53
|
+
|
|
54
|
+
## Testing
|
|
55
|
+
|
|
56
|
+
- 20 unit tests (`tests/unit/messaging/AgentTelegramComms.test.ts`), all passing:
|
|
57
|
+
- **Marker parse/format**: valid parse; no-marker (fall-through) vs malformed (drop) —
|
|
58
|
+
incl. missing `corr`, missing `ts`, charset violation, no blank-line separator;
|
|
59
|
+
format↔parse round-trip; format rejects charset-violating fields.
|
|
60
|
+
- **Routing matrix**: route success; fall-through; and every drop branch — malformed,
|
|
61
|
+
stale/future (replay), **user-spoof** (human-typed marker dropped even when from/id
|
|
62
|
+
match), group bot-as-channel accept, wrong-recipient, unsupported-version, unknown
|
|
63
|
+
sender, bot-id mismatch, duplicate, **role-not-allowed-from-source** (notify from echo
|
|
64
|
+
dropped even though notify is a known role), unknown-role.
|
|
65
|
+
- **Cycle-detection**: key never collapses (corr always present); trips within window,
|
|
66
|
+
clears after; no collision on different corr.
|
|
67
|
+
- tsc --noEmit clean.
|
|
68
|
+
- PR 2/3 add the integration + e2e tiers (TelegramAdapter wiring, mentor consumer, the
|
|
69
|
+
bidirectional contract test, the supervised live cycle).
|
|
70
|
+
|
|
71
|
+
## Migration parity
|
|
72
|
+
|
|
73
|
+
None for this PR — a new unwired module. No agent-installed file changes, no config, no
|
|
74
|
+
routes. PR 2/3 carry the config defaults + migration (file-outbox retirement, dead-config
|
|
75
|
+
removal) per the spec's §Migration parity.
|