mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-03 04:07:04 +00:00
Mask diagnostics before bounds and honor relative launch PATH
This commit is contained in:
parent
ff06711492
commit
b643d0c7fe
6 changed files with 156 additions and 25 deletions
|
|
@ -598,7 +598,7 @@ def resolve_process_node(
|
|||
# idempotent bootstrap; whoever runs first performs the one mutation). The
|
||||
# snapshot is the base of any attested child-env PATH prepend.
|
||||
bootstrap_process_path()
|
||||
from ouroboros.tools.process_facts import selected_process_environment
|
||||
from ouroboros.tools.process_facts import process_path_for_cwd, selected_process_environment
|
||||
path_snapshot = str(selected_process_environment().get("PATH", os.environ.get("PATH", "")) or "")
|
||||
|
||||
constraint = normalize_task_constraint(effective_constraint)
|
||||
|
|
@ -652,12 +652,11 @@ def resolve_process_node(
|
|||
|
||||
surface = _surface_for(ctx, binding, constraint)
|
||||
probe_token = requested if trigger == "runtime" else "node"
|
||||
located = shutil.which(probe_token, path=path_snapshot) or ""
|
||||
located = shutil.which(probe_token, path=process_path_for_cwd(path_snapshot, work_dir)) or ""
|
||||
if located and not os.path.isabs(located):
|
||||
# A relative PATH entry resolves against the WORKER cwd here but against
|
||||
# the command's work_dir at exec time: neither health nor brokenness is
|
||||
# provable from this process, so never substitute on that evidence —
|
||||
# run as written (argv and child env stay byte-identical) (T10).
|
||||
# Search entries are bound to the launch cwd above. If a platform still
|
||||
# cannot establish an absolute candidate, retain the as-written launch
|
||||
# rather than substituting a runtime on unprovable evidence (T10).
|
||||
trace = _node_trace(
|
||||
tool=name,
|
||||
requested_interpreter=requested,
|
||||
|
|
|
|||
|
|
@ -134,6 +134,14 @@ def selected_process_environment():
|
|||
return prepared[0] if prepared is not None else {}
|
||||
|
||||
|
||||
def process_path_for_cwd(path, cwd):
|
||||
"""Resolve PATH search entries against the child cwd, preserving lexical paths."""
|
||||
return os.pathsep.join(
|
||||
str(pathlib.Path(part) if pathlib.Path(part).is_absolute() else pathlib.Path(cwd) / part)
|
||||
for part in path.split(os.pathsep)
|
||||
)
|
||||
|
||||
|
||||
def record_runtime_selection(ctx, argv, cwd, environment):
|
||||
"""Non-executing provenance over the actual launch environment; never rewrite argv."""
|
||||
from ouroboros.workspace_executor import executor_ref_from_ctx, map_host_path
|
||||
|
|
@ -168,8 +176,7 @@ def record_runtime_selection(ctx, argv, cwd, environment):
|
|||
if candidate.is_file() and os.access(candidate, os.X_OK):
|
||||
selected = os.path.abspath(candidate) # lexical venv path, not realpath
|
||||
else:
|
||||
path = os.pathsep.join(str(pathlib.Path(part) if pathlib.Path(part).is_absolute() else pathlib.Path(cwd) / part)
|
||||
for part in environment.get("PATH", os.defpath).split(os.pathsep))
|
||||
path = process_path_for_cwd(environment.get("PATH", os.defpath), cwd)
|
||||
selected = shutil.which(spelling, path=path) or ""
|
||||
source = "PATH" if not trace or not trace.changed else source
|
||||
if not selected:
|
||||
|
|
|
|||
|
|
@ -167,9 +167,11 @@ from ouroboros.tools.process_facts import describe_returncode as _describe_retur
|
|||
|
||||
|
||||
def _format_process_output(stdout: str, stderr: str, *, limit: int = 50_000) -> str:
|
||||
"""Render bounded stdout/stderr sections."""
|
||||
stdout_text = str(stdout or "")
|
||||
stderr_text = str(stderr or "")
|
||||
"""Mask diagnostic copies before bounds can split a selected secret."""
|
||||
from ouroboros.tools.process_facts import redact_process_data
|
||||
|
||||
stdout_text = redact_process_data(str(stdout or ""))
|
||||
stderr_text = redact_process_data(str(stderr or ""))
|
||||
parts: List[str] = []
|
||||
if stdout_text.strip():
|
||||
parts.append(f"STDOUT:\n{stdout_text}")
|
||||
|
|
|
|||
|
|
@ -64,7 +64,9 @@ _RECEIPT_DECLARED_SUMMARY_CAP = 1000
|
|||
|
||||
|
||||
def _bounded(text: Any, cap: int) -> str:
|
||||
t = str(text or "").strip()
|
||||
# Raw output remains available to expected-match evaluation. Only this
|
||||
# diagnostic copy is masked, before a bound could expose a secret fragment.
|
||||
t = redact_process_data(str(text or "")).strip()
|
||||
if len(t) <= cap:
|
||||
return t
|
||||
return t[:cap] + f"\n…[truncated {len(t) - cap} of {len(t)} chars]"
|
||||
|
|
@ -325,7 +327,7 @@ def _compare_files_bytes_equal(
|
|||
rc = int(rc_raw)
|
||||
except (TypeError, ValueError):
|
||||
return False, f"bytes_equal: executor cmp returned no exit status for {a_raw} vs {b_raw}"
|
||||
out = ((res.stdout or "") + ("\n" + res.stderr if res.stderr else "")).strip()
|
||||
out = redact_process_data((res.stdout or "") + ("\n" + res.stderr if res.stderr else "")).strip()
|
||||
if rc == 0:
|
||||
return True, f"bytes_equal: {a_raw} == {b_raw} (executor cmp)"
|
||||
if rc > 1:
|
||||
|
|
|
|||
|
|
@ -877,9 +877,8 @@ def test_whitespace_padded_head_is_not_classified(tmp_path, monkeypatch, quiet_b
|
|||
|
||||
|
||||
def test_relative_path_which_result_is_a_noop(tmp_path, monkeypatch, quiet_bootstrap):
|
||||
"""T10 pin: a which() hit through a RELATIVE PATH entry is unprovable from
|
||||
the worker process (exec resolves it against the command cwd instead), so
|
||||
the resolver must run as written — never substitute bundled node."""
|
||||
"""T10 pin: if which() still returns a relative candidate after binding
|
||||
PATH to the launch cwd, keep the unprovable launch as written."""
|
||||
bundled = _healthy_stub(tmp_path / "bundle" / "node-standalone" / "bin" / "node")
|
||||
monkeypatch.setattr(resolver, "resolve_bundled_node", lambda: str(bundled))
|
||||
monkeypatch.setattr(resolver.shutil, "which", lambda tok, path=None: "bin/node")
|
||||
|
|
|
|||
|
|
@ -73,20 +73,108 @@ def test_verify_matches_raw_selected_value_before_receipt_redaction(process_cont
|
|||
assert receipt["expected"] == "***" and secret not in result.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tool,padding,suffix", [
|
||||
("run_command", 24982, 60000),
|
||||
("run_command", 60000, 24982),
|
||||
("run_script", 24982, 60000),
|
||||
("run_script", 60000, 24982),
|
||||
("verify_and_record", 3990, 60000),
|
||||
("verify_and_record", 19990, 60000),
|
||||
])
|
||||
@pytest.mark.parametrize("returncode,stream", [(0, "stdout"), (7, "stderr")])
|
||||
def test_selected_secret_is_masked_before_diagnostic_bounds(
|
||||
process_context, monkeypatch, tool, padding, suffix, returncode, stream,
|
||||
):
|
||||
from ouroboros.outcomes import verification_receipts_path
|
||||
|
||||
registry, ctx, workspace, data = process_context
|
||||
secret = "UNIQUE_CONFIDENTIAL_FRAGMENT_0123456789_END"
|
||||
monkeypatch.setattr("ouroboros.config.load_settings", lambda: {"CUSTOM_KEY": secret})
|
||||
program = (
|
||||
"import os,sys; "
|
||||
f"print('x'*{padding}+os.environ['TOKEN']+'y'*{suffix}, file=sys.{stream}); "
|
||||
f"sys.exit({returncode})"
|
||||
)
|
||||
args = {"cwd": str(workspace), "env_from_settings": {"TOKEN": "CUSTOM_KEY"}}
|
||||
if tool == "run_script":
|
||||
args.update(script=program, interpreter=sys.executable)
|
||||
elif tool == "verify_and_record":
|
||||
args.update(contract_kind="explicit_command", check=[sys.executable, "-c", program], expected=secret)
|
||||
else:
|
||||
args.update(cmd=[sys.executable, "-c", program])
|
||||
result = registry.execute_result(tool, args)
|
||||
rendered = result.text
|
||||
assert "truncated" in rendered
|
||||
if tool == "verify_and_record":
|
||||
receipt = json.loads(verification_receipts_path(data, ctx.task_id).read_text(encoding="utf-8").splitlines()[-1])
|
||||
assert receipt["returncode"] == returncode and receipt["matched"] is True
|
||||
assert receipt["status"] == ("fail" if returncode else "pass")
|
||||
assert len(receipt["summary"]) < 20100
|
||||
assert len(rendered) < 6000
|
||||
rendered += json.dumps(receipt)
|
||||
else:
|
||||
assert result.meta["exit_code"] == returncode
|
||||
assert result.status == ("error" if returncode else "ok")
|
||||
assert len(rendered) < 52000
|
||||
assert "yyyyyyyyyy" in rendered, "the bounded tail remains available"
|
||||
assert "xxxxxxxxxx" in rendered, "ordinary output remains available"
|
||||
assert secret[:10] not in rendered and secret[-10:] not in rendered
|
||||
|
||||
|
||||
@pytest.mark.parametrize("match_mode", ["exact", "exact_line", "json_equals"])
|
||||
@pytest.mark.parametrize("matches", [True, False])
|
||||
def test_verify_secret_masking_preserves_strict_expected_match(
|
||||
process_context, monkeypatch, match_mode, matches,
|
||||
):
|
||||
from ouroboros.outcomes import verification_receipts_path
|
||||
|
||||
registry, ctx, workspace, data = process_context
|
||||
secret = "synthetic-exact-output-secret"
|
||||
monkeypatch.setattr("ouroboros.config.load_settings", lambda: {"CUSTOM_KEY": secret})
|
||||
output = "json.dumps(os.environ['TOKEN'])" if match_mode == "json_equals" else "os.environ['TOKEN']"
|
||||
expected = secret if matches else "wrong-value"
|
||||
if match_mode == "json_equals":
|
||||
expected = json.dumps(expected)
|
||||
result = registry.execute_result("verify_and_record", {
|
||||
"contract_kind": "explicit_command", "cwd": str(workspace),
|
||||
"check": [sys.executable, "-c", f"import os,json; print({output})"],
|
||||
"expected": expected, "expected_match": match_mode,
|
||||
"env_from_settings": {"TOKEN": "CUSTOM_KEY"},
|
||||
})
|
||||
receipt = json.loads(verification_receipts_path(data, ctx.task_id).read_text(encoding="utf-8").splitlines()[-1])
|
||||
assert receipt["matched"] is matches and receipt["returncode"] == 0
|
||||
assert receipt["status"] == ("pass" if matches else "fail")
|
||||
assert secret not in result.text + json.dumps(receipt)
|
||||
assert "***" in result.text and "***" in receipt["summary"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("failure", ["nonzero", "spawn", "timeout"])
|
||||
def test_selected_secret_does_not_escape_failed_process(process_context, monkeypatch, failure):
|
||||
registry, _ctx, workspace, _data = process_context
|
||||
@pytest.mark.parametrize("tool", ["run_command", "run_script", "verify_and_record"])
|
||||
def test_selected_secret_does_not_escape_failed_process(process_context, monkeypatch, failure, tool):
|
||||
from ouroboros.outcomes import verification_receipts_path
|
||||
|
||||
registry, ctx, workspace, data = process_context
|
||||
secret = "synthetic-failed-process-secret"
|
||||
monkeypatch.setattr("ouroboros.config.load_settings", lambda: {"CUSTOM_KEY": secret})
|
||||
if failure == "spawn":
|
||||
cmd = [str(workspace / "absent")]
|
||||
interpreter = str(workspace / secret) if failure == "spawn" else sys.executable
|
||||
program = "import os,sys,time; print(os.environ['TOKEN'],flush=True); " + ("sys.exit(7)" if failure == "nonzero" else "time.sleep(5)")
|
||||
args = {"cwd": str(workspace), "timeout_sec": 1, "env_from_settings": {"TOKEN": "CUSTOM_KEY"}}
|
||||
if tool == "run_script":
|
||||
args.update(script=program, interpreter=interpreter)
|
||||
elif tool == "verify_and_record":
|
||||
args.update(contract_kind="explicit_command", check=[interpreter, "-c", program])
|
||||
else:
|
||||
cmd = [sys.executable, "-c", "import os,sys,time; print(os.environ['TOKEN'],flush=True); " + ("sys.exit(7)" if failure == "nonzero" else "time.sleep(5)")]
|
||||
result = registry.execute_result("run_command", {"cmd": cmd, "cwd": str(workspace),
|
||||
"timeout_sec": 1, "env_from_settings": {"TOKEN": "CUSTOM_KEY"}})
|
||||
args.update(cmd=[interpreter, "-c", program])
|
||||
result = registry.execute_result(tool, args)
|
||||
assert secret not in result.text
|
||||
assert result.status != "ok"
|
||||
if failure == "nonzero":
|
||||
if tool == "verify_and_record" and failure != "spawn":
|
||||
receipt = json.loads(verification_receipts_path(data, ctx.task_id).read_text(encoding="utf-8").splitlines()[-1])
|
||||
assert receipt["status"] == "fail"
|
||||
assert receipt["returncode"] == (7 if failure == "nonzero" else None)
|
||||
assert secret not in json.dumps(receipt)
|
||||
else:
|
||||
assert result.status != "ok"
|
||||
if failure == "nonzero" and tool != "verify_and_record":
|
||||
assert result.meta["exit_code"] == 7 and "***" in result.text
|
||||
|
||||
|
||||
|
|
@ -131,6 +219,40 @@ def test_runtime_uses_explicit_child_path_not_host_path(process_context, monkeyp
|
|||
assert str(binary) in result.text and '"source": "PATH"' in result.text
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="POSIX executable shims; native Windows execution is not exercised")
|
||||
@pytest.mark.parametrize("backend", ["host", "local"])
|
||||
@pytest.mark.parametrize("relative_dir", ["relative-node-bin", ""])
|
||||
def test_relative_selected_path_reaches_workspace_node(process_context, monkeypatch, backend, relative_dir):
|
||||
from ouroboros import process_interpreters
|
||||
|
||||
registry, ctx, workspace, data = process_context
|
||||
if backend == "local":
|
||||
ctx.executor_ref = {"type": "local", "workspace_host_path": str(workspace), "workspace_backend_path": "/workspace"}
|
||||
selected_dir = workspace / relative_dir
|
||||
selected_dir.mkdir(exist_ok=True)
|
||||
bundled_dir = data / "bundle"
|
||||
bundled_dir.mkdir()
|
||||
for directory, label in ((selected_dir, "chosen-relative-node"), (bundled_dir, "incorrect-bundled-node")):
|
||||
binary = directory / "node"
|
||||
binary.write_text(
|
||||
'#!/bin/sh\nif [ "$1" = "--version" ]; then echo v22.1.0; else echo ' + label + '; fi\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
binary.chmod(0o755)
|
||||
selected_path = os.pathsep.join((relative_dir, "/usr/bin", "/bin"))
|
||||
monkeypatch.setattr("ouroboros.config.load_settings", lambda: {"OPENAI_BASE_URL": selected_path})
|
||||
monkeypatch.setattr(process_interpreters, "resolve_bundled_node", lambda: str(bundled_dir / "node"))
|
||||
result = registry.execute_result("run_command", {"cmd": ["node", "-e", "unused"], "cwd": str(workspace),
|
||||
"env_from_settings": {"PATH": "OPENAI_BASE_URL"}})
|
||||
assert result.status == "ok" and result.meta["exit_code"] == 0, result.text
|
||||
assert "chosen-relative-node" in result.text and "incorrect-bundled-node" not in result.text
|
||||
events = [json.loads(line) for line in (data / "logs/events.jsonl").read_text(encoding="utf-8").splitlines()]
|
||||
trace = next(event for event in reversed(events) if event.get("type") == "node_runtime_resolution")
|
||||
assert trace["requested_interpreter"] == trace["resolved_interpreter"] == "node"
|
||||
assert trace["runtime_path"] == str(selected_dir / "node")
|
||||
assert trace["path_snapshot"] == selected_path and not trace["env_path_prepend"]
|
||||
|
||||
|
||||
def test_unknown_wrapper_provenance_never_invents_a_python_path(process_context):
|
||||
registry, _ctx, workspace, _data = process_context
|
||||
if os.name == "nt":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue