@agfpd/iapeer 0.2.38 → 0.2.39
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/package.json
CHANGED
package/src/transport/index.ts
CHANGED
|
@@ -487,6 +487,46 @@ function validateAttachments(value: readonly string[] = []): Result<string[]> {
|
|
|
487
487
|
return ok(out)
|
|
488
488
|
}
|
|
489
489
|
|
|
490
|
+
// ─── Telegram-domain sender policy (defect track 11.06, sanctioned by Артур) ─
|
|
491
|
+
|
|
492
|
+
/** A SENDER face in the telegram domain: a non-empty interfaces.telegram.bot (the
|
|
493
|
+
* bot machine-key) in the peer's registry passport. Deliberately bot-ONLY, not
|
|
494
|
+
* bot|user_id — the bridge picks the SENDING bot from the sender's bot key
|
|
495
|
+
* (owner fact, telegram-runtime сверка 11.06): a user_id grants a receive/operator
|
|
496
|
+
* identity but no ability to send, so a user_id-only sender passing the guard
|
|
497
|
+
* would still die at the bridge — a late asynchronous failure where the guard
|
|
498
|
+
* exists to give a synchronous one. The guard passes exactly what the bridge can
|
|
499
|
+
* deliver. */
|
|
500
|
+
export function hasTelegramPresence(record: PeerRecord): boolean {
|
|
501
|
+
const tg = record.interfaces?.telegram
|
|
502
|
+
if (!tg || typeof tg !== 'object' || Array.isArray(tg)) return false
|
|
503
|
+
const t = tg as Record<string, unknown>
|
|
504
|
+
return typeof t.bot === 'string' && t.bot.trim() !== ''
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** Sender policy for the telegram domain: a peer with NO declared telegram presence
|
|
508
|
+
* must not write into it — the recipient (a human in Telegram) would face a message
|
|
509
|
+
* from a peer that has no telegram face (identity confusion: who writes, by what
|
|
510
|
+
* right?). Bot channel = declared presence; no presence — no right to the domain.
|
|
511
|
+
* Enforced at the ROUTER, not the bridge: the refusal is synchronous and lands in
|
|
512
|
+
* the sender's tool result; a bridge-side check would fire AFTER the router's
|
|
513
|
+
* ok:true — a false-OK + silent loss, the class this contract forbids. Applied to
|
|
514
|
+
* the INTENDED channel (override / target default — before any wake side-effect,
|
|
515
|
+
* since a wake delivers the envelope as the boot first-message) AND to the RESOLVED
|
|
516
|
+
* channel at both delivery points (the only-live-session fallback can land on
|
|
517
|
+
* telegram without either). */
|
|
518
|
+
function telegramSenderGuard(
|
|
519
|
+
caller: ResolvedCaller,
|
|
520
|
+
targetRuntime: string,
|
|
521
|
+
targetPersonality: string,
|
|
522
|
+
): Result<void> {
|
|
523
|
+
if (targetRuntime !== 'telegram') return ok(undefined)
|
|
524
|
+
if (hasTelegramPresence(caller.record)) return ok(undefined)
|
|
525
|
+
return err(
|
|
526
|
+
`telegram policy: sender "${caller.personality}" has no telegram face (no interfaces.telegram.bot in its passport) — message to "${targetPersonality}" NOT delivered; route via a telegram-faced peer (e.g. boris) or provision a bot for this sender`,
|
|
527
|
+
)
|
|
528
|
+
}
|
|
529
|
+
|
|
490
530
|
/**
|
|
491
531
|
* Route a send from a request-resolved caller to a target peer and deliver it.
|
|
492
532
|
* hit (live) → build envelope from the CALLER → deliverViaTmux → {woke:false}
|
|
@@ -529,6 +569,12 @@ export async function routeSend(
|
|
|
529
569
|
return err(`peer "${personality}" is not in the iapeer peers index; message NOT delivered`)
|
|
530
570
|
}
|
|
531
571
|
|
|
572
|
+
// Telegram sender policy — INTENDED channel (explicit override, or the target's
|
|
573
|
+
// default). Checked BEFORE any wake side-effect: a wake delivers the envelope as
|
|
574
|
+
// the boot first-message, past the resolved-target guards further down.
|
|
575
|
+
const intendedGuard = telegramSenderGuard(caller, runtime ?? peer.runtime, personality)
|
|
576
|
+
if (!intendedGuard.ok) return intendedGuard
|
|
577
|
+
|
|
532
578
|
// Built once — it is both the live-delivery payload and, on a miss, the wake
|
|
533
579
|
// first-message (the woken session receives it as its boot task).
|
|
534
580
|
const envelope = buildEnvelope({
|
|
@@ -554,6 +600,10 @@ export async function routeSend(
|
|
|
554
600
|
const target = resolvePeerDeliveryTarget(personality, runtime, peer)
|
|
555
601
|
if (target.ok) {
|
|
556
602
|
if (target.value.address === caller.address) return err('cannot send to self')
|
|
603
|
+
// Telegram sender policy — RESOLVED channel (the only-live-session fallback can
|
|
604
|
+
// land on telegram without an override and without telegram being the default).
|
|
605
|
+
const liveGuard = telegramSenderGuard(caller, target.value.runtime, personality)
|
|
606
|
+
if (!liveGuard.ok) return liveGuard
|
|
557
607
|
// peer.cwd enables the Ф-B transcript-mtime liveness probe (busy-session case).
|
|
558
608
|
const delivered = deliverViaTmux(target.value, envelope, peer.cwd)
|
|
559
609
|
if (!delivered.ok) return delivered
|
|
@@ -581,6 +631,11 @@ export async function routeSend(
|
|
|
581
631
|
return err(`woke "${personality}" but the session is not live (verify-before-act): ${live.error.message}`)
|
|
582
632
|
}
|
|
583
633
|
if (live.value.address === caller.address) return err('cannot send to self')
|
|
634
|
+
// Telegram sender policy — post-wake RESOLVED channel (airtight: a wake should
|
|
635
|
+
// never bring up a telegram session — they are launchd-held — but the policy is
|
|
636
|
+
// enforced on every delivery point, not on an assumption about the wake).
|
|
637
|
+
const postWakeGuard = telegramSenderGuard(caller, live.value.runtime, personality)
|
|
638
|
+
if (!postWakeGuard.ok) return postWakeGuard
|
|
584
639
|
// taskDelivered:false — the wake took the idempotent live-session fast path (a
|
|
585
640
|
// CONCURRENT sender's boot won the wake.lock and delivered only ITS envelope as
|
|
586
641
|
// the boot first-message). OUR envelope was not delivered by anything yet —
|
|
@@ -17,7 +17,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs'
|
|
|
17
17
|
import { tmpdir } from 'os'
|
|
18
18
|
import { join } from 'path'
|
|
19
19
|
import { spawnSync } from 'child_process'
|
|
20
|
-
import { routeSend, type WakeFn } from './index.ts'
|
|
20
|
+
import { hasTelegramPresence, routeSend, type WakeFn } from './index.ts'
|
|
21
21
|
import type { ResolvedCaller } from '../identity/index.ts'
|
|
22
22
|
import type { PeerRecord } from '../registry/index.ts'
|
|
23
23
|
|
|
@@ -48,6 +48,10 @@ const callerRecord = {
|
|
|
48
48
|
description: '',
|
|
49
49
|
intelligence: 'artificial',
|
|
50
50
|
cwd: '/tmp/boris',
|
|
51
|
+
// Telegram sender policy: the target of these suites is a telegram peer, so the
|
|
52
|
+
// caller must declare a telegram presence (the bot machine-key) — these suites
|
|
53
|
+
// double as the "bot-faced sender delivers as before" half of the policy criterion.
|
|
54
|
+
interfaces: { telegram: { bot: 'boris' } },
|
|
51
55
|
} as unknown as PeerRecord
|
|
52
56
|
const caller: ResolvedCaller = {
|
|
53
57
|
personality: 'boris',
|
|
@@ -213,3 +217,89 @@ describe.if(tmuxAvailable)('routeSend noteLiveTopic seam (live-delivered topic)'
|
|
|
213
217
|
killTargetSession()
|
|
214
218
|
})
|
|
215
219
|
})
|
|
220
|
+
|
|
221
|
+
// ─── Telegram sender policy (defect track 11.06) ─────────────────────────────
|
|
222
|
+
// A sender with NO declared telegram presence (interfaces.telegram.bot|user_id)
|
|
223
|
+
// must get a SYNCHRONOUS refusal when the delivery channel is telegram — the
|
|
224
|
+
// incident class: the bridge silently dropped such envelopes while the router
|
|
225
|
+
// reported ok:true. The bot-faced "delivers as before" half of the criterion is
|
|
226
|
+
// carried by the suites above (their caller declares interfaces.telegram.bot).
|
|
227
|
+
|
|
228
|
+
const facelessCaller: ResolvedCaller = {
|
|
229
|
+
personality: 'ghost',
|
|
230
|
+
runtime: 'claude',
|
|
231
|
+
address: 'claude-ghost',
|
|
232
|
+
description: '',
|
|
233
|
+
intelligence: 'artificial',
|
|
234
|
+
cwd: '/tmp/ghost',
|
|
235
|
+
record: {
|
|
236
|
+
personality: 'ghost',
|
|
237
|
+
runtime: 'claude',
|
|
238
|
+
runtimes: ['claude'],
|
|
239
|
+
description: '',
|
|
240
|
+
intelligence: 'artificial',
|
|
241
|
+
cwd: '/tmp/ghost',
|
|
242
|
+
} as unknown as PeerRecord,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
describe('telegram sender policy — hasTelegramPresence', () => {
|
|
246
|
+
const rec = (tg: unknown): PeerRecord =>
|
|
247
|
+
({ personality: 'x', runtime: 'claude', runtimes: ['claude'], description: '', intelligence: 'artificial', cwd: '/tmp/x', ...(tg !== undefined ? { interfaces: { telegram: tg } } : {}) }) as unknown as PeerRecord
|
|
248
|
+
test('bot machine-key (agent peers) → sender face present', () => {
|
|
249
|
+
expect(hasTelegramPresence(rec({ bot: 'boris' }))).toBe(true)
|
|
250
|
+
})
|
|
251
|
+
test('user_id alone (natural peers — arthur) → NO sender face: the bridge sends FROM the bot key; user_id is a receive/operator identity (owner fact, bot-only predicate)', () => {
|
|
252
|
+
expect(hasTelegramPresence(rec({ user_id: '409502965' }))).toBe(false)
|
|
253
|
+
expect(hasTelegramPresence(rec({ user_id: 409502965 }))).toBe(false)
|
|
254
|
+
})
|
|
255
|
+
test('no interfaces / no telegram / empty bot → absent', () => {
|
|
256
|
+
expect(hasTelegramPresence(rec(undefined))).toBe(false)
|
|
257
|
+
expect(hasTelegramPresence(rec({}))).toBe(false)
|
|
258
|
+
expect(hasTelegramPresence(rec({ bot: '' }))).toBe(false)
|
|
259
|
+
expect(hasTelegramPresence(rec({ bot: ' ' }))).toBe(false)
|
|
260
|
+
expect(hasTelegramPresence(rec({ aliases: { '/alias-new': 'x' } }))).toBe(false)
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
describe('telegram sender policy — routeSend refusal', () => {
|
|
265
|
+
test('INTENDED channel (target default = telegram, peer offline): refused BEFORE the wake — wake never invoked', async () => {
|
|
266
|
+
let wakeCalled = false
|
|
267
|
+
const wake: WakeFn = async () => {
|
|
268
|
+
wakeCalled = true
|
|
269
|
+
return { status: 'READY', woke: true, runtime: 'telegram', taskDelivered: true }
|
|
270
|
+
}
|
|
271
|
+
const r = await routeSend(facelessCaller, { personality: TARGET, message: 'm' }, { wake })
|
|
272
|
+
expect(r.ok).toBe(false)
|
|
273
|
+
if (!r.ok) {
|
|
274
|
+
expect(r.error.message).toContain('telegram policy')
|
|
275
|
+
expect(r.error.message).toContain('"ghost"')
|
|
276
|
+
expect(r.error.message).toContain('NOT delivered')
|
|
277
|
+
}
|
|
278
|
+
expect(wakeCalled).toBe(false)
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
test('explicit runtime=telegram override: refused with the policy line, not "offline"', async () => {
|
|
282
|
+
const r = await routeSend(facelessCaller, { personality: TARGET, runtime: 'telegram', message: 'm' })
|
|
283
|
+
expect(r.ok).toBe(false)
|
|
284
|
+
if (!r.ok) expect(r.error.message).toContain('telegram policy')
|
|
285
|
+
})
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
describe.if(tmuxAvailable)('telegram sender policy — live telegram session', () => {
|
|
289
|
+
test('faceless sender + LIVE telegram target → refused, envelope NEVER reaches the pane', async () => {
|
|
290
|
+
startTargetSession()
|
|
291
|
+
const r = await routeSend(facelessCaller, { personality: TARGET, message: 'POLICY-MUST-BLOCK' })
|
|
292
|
+
expect(r.ok).toBe(false)
|
|
293
|
+
if (!r.ok) expect(r.error.message).toContain('telegram policy')
|
|
294
|
+
expect(capturePane()).not.toContain('POLICY-MUST-BLOCK')
|
|
295
|
+
killTargetSession()
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
test('bot-faced sender (boris) + LIVE telegram target → delivered as before', async () => {
|
|
299
|
+
startTargetSession()
|
|
300
|
+
const r = await routeSend(caller, { personality: TARGET, message: 'BOT-FACED-OK' })
|
|
301
|
+
expect(r.ok).toBe(true)
|
|
302
|
+
expect(capturePane()).toContain('BOT-FACED-OK')
|
|
303
|
+
killTargetSession()
|
|
304
|
+
})
|
|
305
|
+
})
|