|
1 | 1 | import json |
| 2 | +import subprocess |
| 3 | +import sys |
2 | 4 | from concurrent.futures import ThreadPoolExecutor |
3 | 5 |
|
4 | 6 | import pytest |
|
9 | 11 | _write, |
10 | 12 | acknowledge, |
11 | 13 | authority, |
| 14 | + configure_delivery_target, |
12 | 15 | deliver, |
13 | 16 | pending, |
14 | 17 | register_ingress, |
@@ -226,6 +229,130 @@ def test_external_authority_requires_exact_sender_source_and_recipient(fixture): |
226 | 229 | assert receipt["status"] == "delivered" |
227 | 230 |
|
228 | 231 |
|
| 232 | +def test_operator_delivery_target_preview_grant_revoke_and_live_authority(fixture): |
| 233 | + root, registry, session, turn, request = fixture |
| 234 | + channel = "manager.external." + "a" * 24 |
| 235 | + session["channel_id"] = channel |
| 236 | + turn["origin"] = "lark" |
| 237 | + other = {"goal_id": "other", "agent_id": "peer"} |
| 238 | + policy_path = _root(root) / "policy.json" |
| 239 | + _write(policy_path, { |
| 240 | + "schema_version": POLICY_SCHEMA, |
| 241 | + "sources": {channel: { |
| 242 | + "sender_ids": ["owner"], "targets": [other], |
| 243 | + "evidence_goal_ids": ["research", "other"], |
| 244 | + "evidence_ssh_hosts": {"example-host": ["research"]}, |
| 245 | + }}, |
| 246 | + }) |
| 247 | + before = policy_path.read_bytes() |
| 248 | + preview = configure_delivery_target( |
| 249 | + root, registry, channel=channel, **request, grant=True |
| 250 | + ) |
| 251 | + assert preview["would_change"] and not preview["executed"] |
| 252 | + assert preview["resulting_target_count"] == 2 |
| 253 | + assert policy_path.read_bytes() == before |
| 254 | + |
| 255 | + register_ingress( |
| 256 | + root, session_id=session["session_id"], client_turn_id=turn["client_turn_id"], |
| 257 | + channel=channel, sender_id="owner", message=turn["message"], |
| 258 | + source_id="lark:original", |
| 259 | + ) |
| 260 | + assert authority(root, registry, session, turn)["targets"] == [other] |
| 261 | + applied = configure_delivery_target( |
| 262 | + root, registry, channel=channel, **request, grant=True, execute=True |
| 263 | + ) |
| 264 | + assert applied["changed"] and applied["granted_after"] and applied["readback_verified"] |
| 265 | + assert authority(root, registry, session, turn)["targets"] == [other, request] |
| 266 | + assert not configure_delivery_target( |
| 267 | + root, registry, channel=channel, **request, grant=True, execute=True |
| 268 | + )["changed"] |
| 269 | + saved = json.loads(policy_path.read_text()) |
| 270 | + assert saved["sources"][channel]["sender_ids"] == ["owner"] |
| 271 | + assert saved["sources"][channel]["evidence_ssh_hosts"] == {"example-host": ["research"]} |
| 272 | + |
| 273 | + revoked = configure_delivery_target( |
| 274 | + root, registry, channel=channel, **request, grant=False, execute=True |
| 275 | + ) |
| 276 | + assert revoked["changed"] and not revoked["granted_after"] and revoked["readback_verified"] |
| 277 | + assert authority(root, registry, session, turn)["targets"] == [other] |
| 278 | + assert not configure_delivery_target( |
| 279 | + root, registry, channel=channel, **request, grant=False, execute=True |
| 280 | + )["changed"] |
| 281 | + |
| 282 | + # Older policy rows may carry metadata; recipient identity is still the pair. |
| 283 | + saved = json.loads(policy_path.read_text()) |
| 284 | + saved["sources"][channel]["targets"] = [other, {**request, "note": "legacy"}, request] |
| 285 | + _write(policy_path, saved) |
| 286 | + assert not configure_delivery_target( |
| 287 | + root, registry, channel=channel, **request, grant=True, execute=True |
| 288 | + )["changed"] |
| 289 | + assert authority(root, registry, session, turn)["targets"] == [other, request] |
| 290 | + assert configure_delivery_target( |
| 291 | + root, registry, channel=channel, **request, grant=False, execute=True |
| 292 | + )["readback_verified"] |
| 293 | + assert json.loads(policy_path.read_text())["sources"][channel]["targets"] == [other] |
| 294 | + |
| 295 | + |
| 296 | +def test_operator_target_grant_fails_closed_without_audited_source_or_agent(fixture): |
| 297 | + root, registry, _, _, request = fixture |
| 298 | + channel = "manager.external." + "b" * 24 |
| 299 | + with pytest.raises((OSError, ValueError)): |
| 300 | + configure_delivery_target(root, registry, channel=channel, **request, grant=True, execute=True) |
| 301 | + |
| 302 | + policy_path = _root(root) / "policy.json" |
| 303 | + source = {"sender_ids": ["owner"], "evidence_goal_ids": ["other"], "targets": []} |
| 304 | + _write(policy_path, {"schema_version": POLICY_SCHEMA, "sources": {channel: source}}) |
| 305 | + with pytest.raises(ValueError, match="outside the channel read scope"): |
| 306 | + configure_delivery_target(root, registry, channel=channel, **request, grant=True, execute=True) |
| 307 | + source["evidence_goal_ids"] = ["research"] |
| 308 | + _write(policy_path, {"schema_version": POLICY_SCHEMA, "sources": {channel: source}}) |
| 309 | + with pytest.raises(ValueError, match="registered Agent"): |
| 310 | + configure_delivery_target(root, registry, channel=channel, goal_id="research", |
| 311 | + agent_id="unknown", grant=True, execute=True) |
| 312 | + source["evidence_goal_ids"] = ["research", "*"] |
| 313 | + _write(policy_path, {"schema_version": POLICY_SCHEMA, "sources": {channel: source}}) |
| 314 | + with pytest.raises(ValueError, match="outside the channel read scope"): |
| 315 | + configure_delivery_target(root, registry, channel=channel, **request, grant=True, execute=True) |
| 316 | + source["evidence_goal_ids"] = ["research"] |
| 317 | + _write(policy_path, {"schema_version": POLICY_SCHEMA, "sources": {channel: source}}) |
| 318 | + data = json.loads(registry.read_text()) |
| 319 | + data["goals"][0]["activation_state"] = "stopped" |
| 320 | + registry.write_text(json.dumps(data)) |
| 321 | + with pytest.raises(ValueError, match="active Goal"): |
| 322 | + configure_delivery_target(root, registry, channel=channel, **request, grant=True, execute=True) |
| 323 | + assert json.loads(policy_path.read_text())["sources"][channel]["targets"] == [] |
| 324 | + |
| 325 | + |
| 326 | +def test_manager_inbox_cli_previews_and_applies_one_delivery_target(fixture): |
| 327 | + root, registry, _, _, request = fixture |
| 328 | + channel = "manager.external." + "c" * 24 |
| 329 | + policy_path = _root(root) / "policy.json" |
| 330 | + _write(policy_path, { |
| 331 | + "schema_version": POLICY_SCHEMA, |
| 332 | + "sources": {channel: {"sender_ids": ["owner"], "targets": []}}, |
| 333 | + }) |
| 334 | + base = [ |
| 335 | + sys.executable, "-m", "loopx.cli", "--registry", str(registry), |
| 336 | + "--runtime-root", str(root), "manager-inbox", |
| 337 | + ] |
| 338 | + options = ["--channel-id", channel, "--goal-id", request["goal_id"], |
| 339 | + "--agent-id", request["agent_id"]] |
| 340 | + |
| 341 | + def call(action, execute=False): |
| 342 | + completed = subprocess.run( |
| 343 | + [*base, action, *options, *(["--execute"] if execute else [])], |
| 344 | + capture_output=True, text=True, check=True, |
| 345 | + ) |
| 346 | + return json.loads(completed.stdout) |
| 347 | + |
| 348 | + original = policy_path.read_bytes() |
| 349 | + assert call("grant-delivery-target")["would_change"] |
| 350 | + assert policy_path.read_bytes() == original |
| 351 | + assert call("grant-delivery-target", execute=True)["granted_after"] |
| 352 | + assert call("revoke-delivery-target", execute=True)["granted_after"] is False |
| 353 | + assert json.loads(policy_path.read_text())["sources"][channel]["targets"] == [] |
| 354 | + |
| 355 | + |
229 | 356 | def test_same_goal_recipients_keep_inboxes_and_decisions_separate(fixture): |
230 | 357 | root, registry, session, turn, request = fixture |
231 | 358 | data = json.loads(registry.read_text()) |
|
0 commit comments