Repository navigation
Harden offline incidents and virtual eIM transitions #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -122,10 +122,12 @@ Decider's options beforehand: | |
| - the fleet owner authorizes switching; | ||
| - a human approved. | ||
|
|
||
| Independently of the gate, the virtual eIM refuses an enable command whose approval does not match the proposal, one | ||
| for an eUICC that cannot enable profiles or a fleet whose owner forbids switching, and one it cannot deliver because | ||
| the management path is down. It does not repeat the gate's evidence checks (reject cause, reselection, coverage). A test | ||
| replaces the gate with one that always passes and checks that the eIM still refuses an unapproved switch. | ||
| Independently of the gate, the virtual eIM refuses an enable command whose target is not an installed, inactive | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: Non-string targets raise Prompt for AI agents |
||
| profile, whose approval does not match the proposal, for an eUICC that cannot enable profiles or a fleet whose owner | ||
| forbids switching, and one it cannot deliver because the management path is down. A command that reuses an approval | ||
| returns the first result for that approval, whatever its target. It does not repeat the gate's evidence checks (reject | ||
| cause, reselection, coverage). A test replaces the gate with one that always passes and checks that the eIM still | ||
| refuses an unapproved switch. | ||
|
|
||
| **A profile change that does not restore service is handed off.** If the gate blocks it for any reason other than a | ||
| pending approval, the eIM cannot deliver it, verification fails and it is rolled back, rollback itself fails, or the | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,8 @@ | ||
| import asyncio | ||
| import json | ||
| import os | ||
| import subprocess | ||
| import sys | ||
| import urllib.request | ||
|
|
||
| import pytest | ||
|
|
@@ -22,6 +25,57 @@ def choose(self, state, instructions, options, name="next_tool"): | |
| return {"choice": c, "probabilities": {o: float(o == c) for o in options}, "confidence": 1.0} | ||
|
|
||
|
|
||
| async def _consume(events): | ||
| async for _ in events: | ||
| pass | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("method", ["stream", "structured_output"]) | ||
| def test_direct_tool_model_is_a_stateless_failing_async_adapter(method): | ||
| from simshift.agent import ProfilePlan, _DirectToolModel | ||
|
|
||
| model = _DirectToolModel() | ||
| model.update_config(region_name="secret-region", credentials="secret-credentials") | ||
| assert model.get_config() == {} | ||
| if method == "stream": | ||
| events = model.stream([], tool_specs=[], system_prompt="secret-prompt", tool_choice={"auto": {}}, | ||
| system_prompt_content=[], invocation_state={"scenario": "secret-scenario"}, | ||
| cancel_signal=None, agent_metadata=None, future_sdk_kwarg="secret-future") | ||
| else: | ||
| events = model.structured_output(ProfilePlan, [], system_prompt="secret-prompt", | ||
| invocation_state={"scenario": "secret-scenario"}, | ||
| future_sdk_kwarg="secret-future") | ||
| with pytest.raises(RuntimeError) as error: | ||
| asyncio.run(_consume(events)) | ||
| assert str(error.value) == "inference is disabled for direct-tool-only incidents" | ||
|
|
||
|
|
||
| def test_offline_incident_skips_bedrock_factory_and_runs_a_direct_tool(monkeypatch): | ||
| import simshift.agent as ag | ||
|
|
||
| monkeypatch.setattr(ag, "bedrock_model", lambda: pytest.fail("offline incident called bedrock_model")) | ||
| inc = ag.Incident("BR_PROFILE", decider=ScriptedDecider(["investigate_device"]), use_bedrock=False) | ||
| step = inc.step() | ||
| assert step["tool"] == TOOLS["investigate_device"] and step["tool_status"] == "success" | ||
| assert inc.llm.get_config() == {} | ||
|
|
||
|
|
||
| def test_bedrock_incident_uses_the_bedrock_model_factory(monkeypatch): | ||
| import simshift.agent as ag | ||
|
|
||
| sentinel, passed_to_tools = ag._DirectToolModel(), [] | ||
| real_build_tools = ag.build_tools | ||
|
|
||
| def capture_tools(twin, approver, llm): | ||
| passed_to_tools.append(llm) | ||
| return real_build_tools(twin, approver, llm) | ||
|
|
||
| monkeypatch.setattr(ag, "bedrock_model", lambda: sentinel) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: This test does not verify caching: the constant lambda returns the same object on every call, and the test constructs only one incident. Track factory calls across multiple enabled incidents and assert one cached result is reused, otherwise a regression to per-incident Bedrock construction will pass. Prompt for AI agents |
||
| monkeypatch.setattr(ag, "build_tools", capture_tools) | ||
| inc = ag.Incident("TRANSIENT", decider=ScriptedDecider(["retry_connection"]), use_bedrock=True) | ||
| assert inc.llm is sentinel and passed_to_tools == [sentinel] | ||
|
|
||
|
|
||
| def test_decider_controls_tool(): | ||
| a = run("BR_PROFILE", decider=ScriptedDecider(["investigate_device"]), use_bedrock=False) | ||
| b = run("BR_PROFILE", decider=ScriptedDecider(["investigate_data_path"]), use_bedrock=False) | ||
|
|
@@ -124,6 +178,44 @@ def test_off_menu_choice_never_reaches_the_eim(): | |
| SAMPLE_RUNS = os.path.join(os.path.dirname(__file__), "..", "docs", "sample-runs") | ||
|
|
||
|
|
||
| def test_all_offline_surfaces_ignore_a_hostile_aws_profile(monkeypatch): | ||
| import simshift.agent as ag | ||
| from simshift.benchmark import RandomChooser, RulesChooser, make, run_one | ||
| from simshift.validate import problems | ||
| from simshift.web import App | ||
|
|
||
| monkeypatch.setenv("AWS_PROFILE", "simshift-profile-that-does-not-exist") | ||
| monkeypatch.setattr(ag, "bedrock_model", lambda: pytest.fail("offline surface called bedrock_model")) | ||
|
|
||
| recording = json.load(open(os.path.join(SAMPLE_RUNS, "TRANSIENT.json"))) | ||
| assert problems(recording) == [] | ||
|
|
||
| sid = "generated-hostile-profile-transient" | ||
| monkeypatch.setitem(SCENARIOS, sid, make("transient", 9007)) | ||
| for chooser in (RulesChooser(), RandomChooser(7)): | ||
| row = run_one(sid, "transient", chooser) | ||
| assert row["incident"] == sid and row["trace"] | ||
|
|
||
| app = App("http://127.0.0.1:1", replay_dir=os.path.join(SAMPLE_RUNS, "orchestrated")) | ||
| snapshot = app.create("TRANSIENT") | ||
| while not snapshot["done"]: | ||
| snapshot = app.act(snapshot["id"], "step", {}) | ||
| assert snapshot["outcome"]["restored"] | ||
|
|
||
|
|
||
| def test_no_bedrock_cli_replay_ignores_a_hostile_aws_profile(): | ||
| command = [sys.executable, "-m", "simshift.agent", "TRANSIENT", "--approve", "--no-bedrock", | ||
| "--replay", "docs/sample-runs/TRANSIENT.json"] | ||
| env = {**os.environ, "AWS_PROFILE": "simshift-profile-that-does-not-exist", | ||
| "DECIDER_URL": "http://127.0.0.1:1"} | ||
| completed = subprocess.run(command, cwd=os.path.dirname(os.path.dirname(__file__)), env=env, | ||
| capture_output=True, text=True, timeout=30) | ||
| output = completed.stdout + completed.stderr | ||
| assert completed.returncode == 0, output | ||
| assert "no Bedrock" in completed.stdout and "restored=True" in completed.stdout | ||
| assert "ProfileNotFound" not in output | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("name", sorted(f for d in ("", "orchestrated") for f in | ||
| (os.path.join(d, x) for x in os.listdir(os.path.join(SAMPLE_RUNS, d))) | ||
| if f.endswith(".json"))) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: The gotcha overstates replay's AWS independence:
--replayalone still constructsBedrockModelunless--no-bedrockis also supplied. Qualify this asmake replayor state that direct replays require--no-bedrock, otherwise a verifier with an invalidAWS_PROFILEcan hitProfileNotFound.Prompt for AI agents