- 4281
.collect(); - 4282
assert_eq!(keys, vec!["telegram:2"]); - 4283
} - 4284
- 4285
#[test] - 4286
fn allowlist_route_resolves_the_configured_agent_identity() { - 4287
let (_dir, core) = core_with_config("[memory]\nreflection = false\n"); - 4288
std::fs::write( - 4289
core.cwd().join(".vak/agents.json"), - 4290
serde_json::json!([{ - 4291
"id": "support", - 4292
"revision": 3, - 4293
"name": "Support", - 4294
"character": "orb", - 4295
"personality": "calm", - 4296
"behaviour": "helpful", - 4297
"responsibilities": "support", - 4298
"animation": "off", - 4299
"voice": "default" - 4300
}]) - 4301
.to_string(), - 4302
) - 4303
.unwrap(); - 4304
let gw = GatewayState::load(&core, true); - 4305
gw.allowlist_approve( - 4306
&core, - 4307
"telegram:agent", - 4308
core.cwd().clone(), - 4309
Some("support".into()), - 4310
None, - 4311
None, - 4312
Default::default(), - 4313
None, - 4314
true, - 4315
"test", - 4316
); - 4317
let resolved = gw.core_for_entry(&core, "telegram:agent").unwrap(); - 4318
assert_eq!( - 4319
resolved.agent_identity().map(|agent| agent.id.as_str()), - 4320
Some("support") - 4321
); - 4322
} - 4323
- 4324
#[test] - 4325
fn bot_agent_identity_resolves_when_chat_inherits_bot() { - 4326
let (_dir, core) = core_with_config("[memory]\nreflection = false\n"); - 4327
std::fs::write( - 4328
core.cwd().join(".vak/agents.json"), - 4329
serde_json::json!([{ - 4330
"id": "researcher", - 4331
"revision": 2, - 4332
"name": "Researcher", - 4333
"character": "orb", - 4334
"personality": "curious", - 4335
"behaviour": "thorough", - 4336
"responsibilities": "research", - 4337
"animation": "off", - 4338
"voice": "default" - 4339
}]) - 4340
.to_string(), - 4341
) - 4342
.unwrap(); - 4343
let gw = GatewayState::load(&core, true); - 4344
gw.bot_upsert( - 4345
&core, - 4346
Bot { - 4347
id: "res-bot".into(), - 4348
surface: "telegram".into(), - 4349
label: "Researcher Bot".into(), - 4350
token_env: "BOT_TOKEN__RES".into(), - 4351
agent_id: Some("researcher".into()), - 4352
..Default::default() - 4353
}, - 4354
); - 4355
gw.allowlist_approve( - 4356
&core, - 4357
"telegram:res-chat", - 4358
core.cwd().clone(), - 4359
None, - 4360
None, - 4361
None, - 4362
Default::default(), - 4363
Some("res-bot".into()), - 4364
true, - 4365
"test", - 4366
); - 4367
let resolved = gw.core_for_entry(&core, "telegram:res-chat").unwrap(); - 4368
assert_eq!( - 4369
resolved.agent_identity().map(|agent| agent.id.as_str()), - 4370
Some("researcher") - 4371
); - 4372
- 4373
// Even if legacy entry was stamped with "vak", inherit_bot_policy allows the bot's agent to shine through - 4374
gw.allowlist_patch( - 4375
&core, - 4376
"telegram:res-chat", - 4377
Some(core.cwd().clone()), - 4378
Some(Some("vak".into())), - 4379
None, - 4380
None, - 4381
Default::default(), - 4382
Some(Some("res-bot".into())), - 4383
Some(true), - 4384
None, - 4385
None, - 4386
); - 4387
let resolved_legacy = gw.core_for_entry(&core, "telegram:res-chat").unwrap(); - 4388
assert_eq!( - 4389
resolved_legacy - 4390
.agent_identity() - 4391
.map(|agent| agent.id.as_str()), - 4392
Some("researcher") - 4393
); - 4394
- 4395
// Explicit chat agent override takes precedence - 4396
std::fs::write( - 4397
core.cwd().join(".vak/agents.json"), - 4398
serde_json::json!([ - 4399
{ - 4400
"id": "researcher", - 4401
"revision": 2, - 4402
"name": "Researcher", - 4403
"character": "orb", - 4404
"personality": "curious", - 4405
"behaviour": "thorough", - 4406
"responsibilities": "research", - 4407
"animation": "off", - 4408
"voice": "default" - 4409
}, - 4410
{ - 4411
"id": "support", - 4412
"revision": 2, - 4413
"name": "Support", - 4414
"character": "orb", - 4415
"personality": "helpful", - 4416
"behaviour": "friendly", - 4417
"responsibilities": "support", - 4418
"animation": "off", - 4419
"voice": "default" - 4420
} - 4421
]) - 4422
.to_string(), - 4423
) - 4424
.unwrap(); - 4425
gw.allowlist_patch( - 4426
&core, - 4427
"telegram:res-chat", - 4428
Some(core.cwd().clone()), - 4429
Some(Some("support".into())), - 4430
None, - 4431
None, - 4432
Default::default(), - 4433
Some(Some("res-bot".into())), - 4434
Some(true), - 4435
None, - 4436
None, - 4437
); - 4438
let resolved_override = gw.core_for_entry(&core, "telegram:res-chat").unwrap(); - 4439
assert_eq!( - 4440
resolved_override - 4441
.agent_identity() - 4442
.map(|agent| agent.id.as_str()), - 4443
Some("support") - 4444
); - 4445
- 4446
// Clearing back to None inherits bot's agent again - 4447
gw.allowlist_patch( - 4448
&core, - 4449
"telegram:res-chat", - 4450
Some(core.cwd().clone()), - 4451
Some(None), - 4452
None, - 4453
None, - 4454
Default::default(), - 4455
Some(Some("res-bot".into())), - 4456
Some(true), - 4457
None, - 4458
None, - 4459
); - 4460
let resolved_cleared = gw.core_for_entry(&core, "telegram:res-chat").unwrap(); - 4461
assert_eq!( - 4462
resolved_cleared - 4463
.agent_identity() - 4464
.map(|agent| agent.id.as_str()), - 4465
Some("researcher") - 4466
); - 4467
} - 4468
- 4469
#[test] - 4470
fn paused_agent_route_fails_closed_before_core_creation() { - 4471
let (_dir, core) = core_with_config("[memory]\nreflection = false\n"); - 4472
std::fs::write( - 4473
core.cwd().join(".vak/agents.json"), - 4474
serde_json::json!([{ - 4475
"id": "paused", - 4476
"revision": 1, - 4477
"lifecycle": "paused", - 4478
"name": "Paused", - 4479
"character": "orb", - 4480
"personality": "calm", - 4481
"behaviour": "helpful", - 4482
"responsibilities": "support", - 4483
"animation": "off", - 4484
"voice": "default" - 4485
}]) - 4486
.to_string(), - 4487
) - 4488
.unwrap(); - 4489
let gw = GatewayState::load(&core, true); - 4490
gw.allowlist_approve( - 4491
&core, - 4492
"telegram:paused", - 4493
core.cwd().clone(), - 4494
Some("paused".into()), - 4495
None, - 4496
None, - 4497
Default::default(), - 4498
None, - 4499
true, - 4500
"test", - 4501
); - 4502
assert!(gw.core_for_entry(&core, "telegram:paused").is_err()); - 4503
} - 4504
- 4505
#[test] - 4506
fn allowlist_open_flag_does_not_seed_pending_entries() { - 4507
// An explicit (non-empty) project `chat_allowlist` always overrides - 4508
// whatever a developer's own global config.toml might set, so this - 4509
// stays deterministic regardless of the machine it runs on. - 4510
let (_dir, core) = core_with_config( - 4511
"[gateway]\nchat_allowlist = [\"testonly:1\"]\nchat_allowlist_open = true\n", - 4512
); - 4513
let gw = GatewayState::load(&core, true); - 4514
assert!(gw.chat_allowlist_open()); - 4515
let entries = gw.allowlist_snapshot(); - 4516
assert_eq!(entries.len(), 1); - 4517
assert_eq!(entries[0].key, "testonly:1"); - 4518
assert_eq!(entries[0].status, AllowlistStatus::Allowed); - 4519
assert_eq!(entries[0].added_by, "config_import"); - 4520
} - 4521
- 4522
#[test] - 4523
fn allowlist_approve_deny_revoke_roundtrip() { - 4524
let (_dir, core) = core_with_config("[memory]\nreflection = false\n"); - 4525
let gw = GatewayState::load(&core, true); - 4526
- 4527
// Unknown key: approve creates an allowed entry with the explicit - 4528
// workspace visible in the record (not silently inherited). - 4529
let approved = gw.allowlist_approve( - 4530
&core, - 4531
"telegram:7", - 4532
core.cwd().clone(), - 4533
None, - 4534
Some(AllowlistRoute { - 4535
provider: "anthropic".into(), - 4536
model: "sonnet".into(), - 4537
}), - 4538
None, - 4539
vak_config::ChannelPolicy::default(), - 4540
None, - 4541
true, - 4542
"admin", - 4543
); - 4544
assert_eq!(approved.status, AllowlistStatus::Allowed); - 4545
assert_eq!(approved.workspace.as_deref(), Some(core.cwd().as_path())); - 4546
assert_eq!(approved.agent_id.as_deref(), Some("vak")); - 4547
assert_eq!(approved.route.as_ref().unwrap().provider, "anthropic"); - 4548
- 4549
// Persisted to disk atomically. - 4550
let raw = std::fs::read_to_string(allowlist_path(&core.shared_data_home())).unwrap(); - 4551
assert!(raw.contains("telegram:7")); - 4552
- 4553
// Revoke removes an allowed entry. - 4554
assert!(gw.allowlist_revoke(&core, "telegram:7")); - 4555
assert!(gw.allowlist_get("telegram:7").is_none()); - 4556
// Revoking a nonexistent / non-allowed entry is a no-op failure. - 4557
assert!(!gw.allowlist_revoke(&core, "telegram:7")); - 4558
- 4559
// Deny sticks. - 4560
let denied = gw.allowlist_deny(&core, "telegram:8", "admin"); - 4561
assert_eq!(denied.status, AllowlistStatus::Denied); - 4562
assert!(!gw.allowlist_revoke(&core, "telegram:8")); - 4563
} - 4564
- 4565
#[test] - 4566
fn inbound_resolve_creates_one_pending_entry_not_duplicated() { - 4567
// An explicit project `chat_allowlist` overrides a developer's own - 4568
// global config.toml so the seeded starting state is deterministic. - 4569
let (_dir, core) = core_with_config( - 4570
"[memory]\nreflection = false\n[gateway]\nchat_allowlist = [\"testonly:0\"]\n", - 4571
); - 4572
let gw = GatewayState::load(&core, true); - 4573
- 4574
let first = gw.allowlist_resolve_inbound(&core, "telegram:99", "hello there", None); - 4575
assert!(matches!(first, AllowlistDecision::NewlyPending)); - 4576
let entry = gw.allowlist_get("telegram:99").unwrap(); - 4577
assert_eq!(entry.status, AllowlistStatus::Pending); - 4578
assert_eq!(entry.first_seen_text.as_deref(), Some("hello there")); - 4579
let added_at = entry.added_at.clone(); - 4580
- 4581
// A repeat message on the same pending key does not duplicate or - 4582
// bump added_at. - 4583
let second = gw.allowlist_resolve_inbound(&core, "telegram:99", "hello again", None); - 4584
assert!(matches!(second, AllowlistDecision::StillPending)); - 4585
let entry2 = gw.allowlist_get("telegram:99").unwrap(); - 4586
assert_eq!(entry2.added_at, added_at); - 4587
assert_eq!(entry2.first_seen_text.as_deref(), Some("hello there")); - 4588
let pending_count = gw - 4589
.allowlist_snapshot() - 4590
.iter() - 4591
.filter(|e| e.status == AllowlistStatus::Pending) - 4592
.count(); - 4593
assert_eq!(pending_count, 1, "no duplicate pending entry created"); - 4594
} - 4595
- 4596
/// Multi-bot-per-channel (docs/design/34 Phase 5 follow-up): a chat - 4597
/// already trusted under its legacy two-part key must not force a - 4598
/// fresh approval the moment its bridge starts sending a bot id — the - 4599
/// same physical chat, now seen through a bot, inherits the existing - 4600
/// grant. And a *second* bot joining that same physical chat gets its - 4601
/// own independent entry too, inherited from the same legacy row — - 4602
/// this is the actual "a channel can have multiple independent bots" - 4603
/// capability, not just the UI to configure it. - 4604
#[test] - 4605
fn bot_scoped_key_inherits_approval_from_already_allowed_legacy_key() { - 4606
let (_dir, core) = core_with_config( - 4607
"[memory]\nreflection = false\n[gateway]\nchat_allowlist = [\"telegram:8846301562\"]\n", - 4608
); - 4609
let gw = GatewayState::load(&core, true); - 4610
let legacy = gw.allowlist_get("telegram:8846301562").unwrap(); - 4611
assert_eq!(legacy.status, AllowlistStatus::Allowed); - 4612
- 4613
// Seed a stale binding at the legacy key, as if a session had - 4614
// actually been dispatched there before bots existed — this is - 4615
// what must go away once a bot-scoped sibling takes over, so the - 4616
// Chats list doesn't show a dead duplicate. - 4617
gw.bind( - 4618
&core, - 4619
"telegram:8846301562".into(), - 4620
"fake-session".into(), - 4621
"rev".into(), - 4622
); - 4623
- 4624
let decision = - 4625
gw.allowlist_resolve_inbound(&core, "telegram:8846301562:VakBot", "hi", Some("VakBot")); - 4626
assert!(matches!(decision, AllowlistDecision::Allowed)); - 4627
let inherited = gw.allowlist_get("telegram:8846301562:VakBot").unwrap(); - 4628
assert_eq!(inherited.status, AllowlistStatus::Allowed); - 4629
assert_eq!(inherited.bot_id.as_deref(), Some("VakBot")); - 4630
assert_eq!(inherited.workspace, legacy.workspace); - 4631
// The legacy row's *allowlist entry* is untouched — it stays - 4632
// around as the ancestor a third bot could still inherit from - 4633
// later. - 4634
assert_eq!( - 4635
gw.allowlist_get("telegram:8846301562").unwrap().status, - 4636
AllowlistStatus::Allowed - 4637
); - 4638
// Its *binding* is gone, though — that's the actual duplicate-row - 4639
// fix: nothing will ever dispatch to the legacy key again. - 4640
assert!( - 4641
gw.bindings_snapshot() - 4642
.iter() - 4643
.all(|(k, _)| k != "telegram:8846301562"), - 4644
"legacy binding must be unbound once a bot-scoped sibling exists" - 4645
); - 4646
- 4647
// A second, independent bot on the same physical chat inherits - 4648
// too, and gets a genuinely separate entry from the first bot's. - 4649
let decision2 = gw.allowlist_resolve_inbound( - 4650
&core, - 4651
"telegram:8846301562:Vakyartha", - 4652
"hi", - 4653
Some("Vakyartha"), - 4654
); - 4655
assert!(matches!(decision2, AllowlistDecision::Allowed)); - 4656
let inherited2 = gw.allowlist_get("telegram:8846301562:Vakyartha").unwrap(); - 4657
assert_eq!(inherited2.bot_id.as_deref(), Some("Vakyartha")); - 4658
assert_ne!( - 4659
gw.allowlist_get("telegram:8846301562:VakBot") - 4660
.unwrap() - 4661
.bot_id, - 4662
inherited2.bot_id, - 4663
"each bot must get its own entry, not share one" - 4664
); - 4665
} - 4666
- 4667
/// No legacy approval to inherit means the normal pending-review path, - 4668
/// same as any other never-seen chat. - 4669
#[test] - 4670
fn bot_scoped_key_starts_pending_when_no_legacy_approval_exists() { - 4671
let (_dir, core) = core_with_config("[memory]\nreflection = false\n"); - 4672
let gw = GatewayState::load(&core, true); - 4673
let decision = - 4674
gw.allowlist_resolve_inbound(&core, "telegram:555:NewBot", "hi", Some("NewBot")); - 4675
assert!(matches!(decision, AllowlistDecision::NewlyPending)); - 4676
let entry = gw.allowlist_get("telegram:555:NewBot").unwrap(); - 4677
assert_eq!(entry.status, AllowlistStatus::Pending); - 4678
} - 4679
- 4680
#[test] - 4681
fn legacy_key_for_strips_bot_id_and_is_none_for_shorter_keys() { - 4682
assert_eq!( - 4683
legacy_key_for("telegram:123:VakBot"), - 4684
Some("telegram:123".to_string()) - 4685
); - 4686
assert_eq!(legacy_key_for("telegram:123"), None); - 4687
assert_eq!(legacy_key_for("telegram"), None); - 4688
} - 4689
- 4690
#[test] - 4691
fn inbound_resolve_dispatches_allowed_and_denied_keys() { - 4692
let (_dir, core) = core_with_config( - 4693
"[memory]\nreflection = false\n[gateway]\nchat_allowlist = [\"testonly:0\"]\n", - 4694
); - 4695
let gw = GatewayState::load(&core, true); - 4696
- 4697
// Allowed key dispatches normally. - 4698
gw.allowlist_approve( - 4699
&core, - 4700
"telegram:100", - 4701
core.cwd().clone(), - 4702
None, - 4703
None, - 4704
None, - 4705
vak_config::ChannelPolicy::default(), - 4706
None, - 4707
true, - 4708
"admin", - 4709
); - 4710
assert!(matches!( - 4711
gw.allowlist_resolve_inbound(&core, "telegram:100", "hi", None), - 4712
AllowlistDecision::Allowed - 4713
)); - 4714
- 4715
// Denied key stays rejected. - 4716
gw.allowlist_deny(&core, "telegram:101", "admin"); - 4717
assert!(matches!( - 4718
gw.allowlist_resolve_inbound(&core, "telegram:101", "hi", None), - 4719
AllowlistDecision::Denied - 4720
)); - 4721
} - 4722
- 4723
#[test] - 4724
fn first_seen_text_is_truncated() { - 4725
let (_dir, core) = core_with_config("[memory]\nreflection = false\n"); - 4726
let gw = GatewayState::load(&core, true); - 4727
let long = "x".repeat(FIRST_SEEN_TEXT_MAX_CHARS + 50); - 4728
gw.allowlist_resolve_inbound(&core, "telegram:200", &long, None); - 4729
let entry = gw.allowlist_get("telegram:200").unwrap(); - 4730
assert_eq!( - 4731
entry.first_seen_text.unwrap().chars().count(), - 4732
FIRST_SEEN_TEXT_MAX_CHARS - 4733
); - 4734
} - 4735
- 4736
#[tokio::test] - 4737
async fn permission_revoke_denies_forwarded_gates_for_session() { - 4738
let (_dir, core) = core_with_config("[memory]\nreflection = false\n"); - 4739
let gw = GatewayState::load(&core, true); - 4740
let mut receivers = Vec::new(); - 4741
for id in ["gate-a", "gate-b", "other"] { - 4742
let rx = gw.register_gate(id, if id == "other" { "s2" } else { "s1" }); - 4743
receivers.push((id, rx)); - 4744
} - 4745
assert_eq!(gw.deny_pending_for_session("s1"), 2); - 4746
let (_, first) = receivers.remove(0); - 4747
let (_, second) = receivers.remove(0); - 4748
assert!(!first.await.unwrap()); - 4749
assert!(!second.await.unwrap()); - 4750
assert!(gw.resolve_gate(true, Some("gate-a")).is_err()); - 4751
assert!(gw.resolve_gate(true, Some("gate-b")).is_err()); - 4752
assert_eq!( - 4753
gw.resolve_gate(false, Some("other")).unwrap().session_id, - 4754
"s2" - 4755
); - 4756
} - 4757
} - 4758
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.