Harden SSH and profile resolution boundaries
Some checks failed
ci / validate (push) Has been cancelled

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0233b-178d-7162-b92f-31a31ea8ca9b
This commit is contained in:
tegwick 2026-08-23 12:53:05 +02:00
parent 60564fda68
commit 695438019c
8 changed files with 176 additions and 10 deletions

View file

@ -18,6 +18,7 @@
| workplan | GLAS-WP-0007 | finished | — | workplans/GLAS-WP-0007-capability-scope-truth.md |
| workplan | GLAS-WP-0008 | finished | — | workplans/GLAS-WP-0008-profile-operational-readiness.md |
| workplan | GLAS-WP-0009 | finished | — | workplans/GLAS-WP-0009-operational-handoff-truth.md |
| workplan | GLAS-WP-0010 | finished | — | workplans/GLAS-WP-0010-transport-and-resolution-hardening.md |
| task | GLAS-0001-T01 | done | — | workplans/GLAS-0001-statehub-bootstrap.md |
| task | GLAS-0001-T02 | done | — | workplans/GLAS-0001-statehub-bootstrap.md |
| task | GLAS-0001-T03 | done | — | workplans/GLAS-0001-statehub-bootstrap.md |
@ -56,6 +57,9 @@
| task | GLAS-WP-0008-T03 | done | — | workplans/GLAS-WP-0008-profile-operational-readiness.md |
| task | GLAS-WP-0009-T01 | done | — | workplans/GLAS-WP-0009-operational-handoff-truth.md |
| task | GLAS-WP-0009-T02 | done | — | workplans/GLAS-WP-0009-operational-handoff-truth.md |
| task | GLAS-WP-0010-T01 | done | — | workplans/GLAS-WP-0010-transport-and-resolution-hardening.md |
| task | GLAS-WP-0010-T02 | done | — | workplans/GLAS-WP-0010-transport-and-resolution-hardening.md |
| task | GLAS-WP-0010-T03 | done | — | workplans/GLAS-WP-0010-transport-and-resolution-hardening.md |
| intake | GLAS-IN-0001 | done | blue | docs/intakes/residuals.md |
| intake | GLAS-IN-0002 | todo | red | docs/intakes/residuals.md |
| intake | GLAS-IN-0003 | todo | green | docs/intakes/residuals.md |

View file

@ -34,7 +34,9 @@ Each YAML document under `profiles/` declares:
The catalog discovers files deterministically, forbids unknown schema fields,
rejects duplicate revisions, and checks the selected profile against
`registry/reins/*.yaml`. Unpinned lookup is accepted only when exactly one
enabled revision exists. Inline secrets and token-looking values are refused.
revision exists; disabled revisions still make a multi-revision selector
ambiguous so callers must pin deliberately. A pinned disabled revision is
refused. Inline secrets and token-looking values are refused.
Use `glas-harness profiles` as the catalog/packaging validation command.

View file

@ -79,6 +79,11 @@ subprocess with the profile timeout. A host must make the selected rein command
and its dependencies available inside that transport; host-only installation is
not treated as sandbox availability.
SSH reachability accepts only one non-option host or `user@host` target whose
components begin with an alphanumeric character. Glas also terminates SSH
option parsing with `--`; a reachability descriptor cannot reinterpret its
target as an SSH flag.
There is no production default rein. Direct `Rein` injection remains a narrow
library/test seam but still requires a valid profile so profile, sandbox,
model, tool policy, and evidence are explicit.

View file

@ -25,7 +25,9 @@ class TransportError(RuntimeError):
"""The sandbox reachability descriptor cannot safely execute commands."""
_SSH_TARGET = re.compile(r"^(?:[A-Za-z0-9._-]+@)?[A-Za-z0-9._-]+$")
_SSH_TARGET = re.compile(
r"^(?:[A-Za-z0-9][A-Za-z0-9._-]*@)?[A-Za-z0-9][A-Za-z0-9._-]*$"
)
@dataclass(frozen=True)
@ -63,9 +65,11 @@ class ExecutionTransport:
*scoped_command,
]
if self.kind == "ssh":
if not self.ssh_target:
raise TransportError("SSH transport is missing target")
return ["ssh", self.ssh_target, shlex.join(scoped_command)]
if not self.ssh_target or not _SSH_TARGET.fullmatch(self.ssh_target):
raise TransportError(
"SSH target must be a single non-option host or user@host target"
)
return ["ssh", "--", self.ssh_target, shlex.join(scoped_command)]
raise TransportError(f"unsupported execution transport: {self.kind}")
def run(
@ -175,7 +179,9 @@ def transport_from_sandbox(sandbox: SandboxHandle) -> ExecutionTransport:
"incomplete remote sandbox reachability: ssh and remote_dir are required"
)
if not _SSH_TARGET.fullmatch(str(ssh_target)):
raise TransportError("SSH reachability must be a single host or user@host target")
raise TransportError(
"SSH reachability must be a single non-option host or user@host target"
)
if not PurePosixPath(str(remote_dir)).is_absolute():
raise TransportError("remote sandbox workspace must be an absolute path")
return ExecutionTransport(

View file

@ -161,6 +161,45 @@ def test_run_execution_refuses_unknown_profile_before_sandbox() -> None:
manager.create.assert_not_called()
def test_run_execution_refuses_disabled_profile_before_sandbox() -> None:
manager = MagicMock()
catalog = ProfileCatalog()
profile, _ = catalog.resolve(PROFILE)
catalog.profiles()[(profile.id, profile.version)] = profile.model_copy(
update={"status": "disabled"}
)
result = run_execution(_request(), catalog=catalog, rein=_FakeRein(), manager=manager)
assert result.ok is False
assert result.evidence.outcome == "refused"
assert result.evidence.failure_stage == "resolution"
assert "profile disabled" in (result.evidence.error or "")
manager.create.assert_not_called()
def test_run_execution_refuses_ambiguous_profile_before_sandbox() -> None:
manager = MagicMock()
catalog = ProfileCatalog()
profile, _ = catalog.resolve(PROFILE)
catalog.profiles()[(profile.id, "2.0.0")] = profile.model_copy(
update={"version": "2.0.0"}
)
result = run_execution(
_request(profile=profile.id),
catalog=catalog,
rein=_FakeRein(),
manager=manager,
)
assert result.ok is False
assert result.evidence.outcome == "refused"
assert result.evidence.failure_stage == "resolution"
assert "pin one of" in (result.evidence.error or "")
manager.create.assert_not_called()
def test_run_execution_refuses_blocked_profile_before_sandbox() -> None:
manager = MagicMock()

View file

@ -22,6 +22,7 @@ def _profile(
profile_id: str = "harness.test",
version: str = "1.0.0",
contract: str = "1.0",
status: str = "enabled",
rein: str = "rein-test",
required: str = "session_style: unattended",
extra: str = "",
@ -30,7 +31,7 @@ def _profile(
id: {profile_id}
version: \"{version}\"
contract_version: \"{contract}\"
status: enabled
status: {status}
rein:
id: {rein}
required_capabilities:
@ -150,6 +151,26 @@ def test_unpinned_multi_version_profile_is_ambiguous(tmp_path) -> None:
ProfileCatalog(profiles, reins).resolve("harness.test")
def test_disabled_revision_still_requires_pinned_selection(tmp_path) -> None:
profiles = tmp_path / "profiles"
reins = tmp_path / "reins"
profiles.mkdir()
reins.mkdir()
_write(profiles, "enabled.yaml", _profile(version="1.0.0"))
_write(
profiles,
"disabled.yaml",
_profile(version="2.0.0", status="disabled"),
)
_write(reins, "rein.yaml", _rein())
catalog = ProfileCatalog(profiles, reins)
with pytest.raises(AmbiguousProfileError, match="pin one of"):
catalog.resolve("harness.test")
with pytest.raises(IncompatibleProfileError, match="profile disabled"):
catalog.resolve("harness.test@2.0.0")
@pytest.mark.parametrize(
("profile_text", "match"),
[

View file

@ -49,11 +49,34 @@ def test_remote_transport_wraps_command_without_local_shell() -> None:
assert transport.kind == "ssh"
assert transport.command(["git", "-C", "/tmp/sbx", "status"]) == [
"ssh",
"--",
"agent@sandboxer01",
"sh -c 'cd \"$1\" && shift && exec \"$@\"' sh /tmp/sbx git -C /tmp/sbx status",
]
@pytest.mark.parametrize(
"ssh_target",
["-v", "-oProxyCommand", "agent@-host", "-agent@host", "agent@host extra"],
)
def test_remote_transport_rejects_ssh_option_and_invalid_targets(ssh_target) -> None:
sandbox = SandboxHandle(
sandbox_id="sbx",
host="sandboxer01",
reachability={"ssh": ssh_target, "remote_dir": "/tmp/sbx"},
)
with pytest.raises(TransportError, match="single non-option host"):
transport_from_sandbox(sandbox)
def test_direct_remote_transport_cannot_bypass_ssh_target_validation() -> None:
transport = ExecutionTransport(kind="ssh", workspace="/tmp/sbx", ssh_target="-v")
with pytest.raises(TransportError, match="single non-option host"):
transport.command(["true"])
@pytest.mark.parametrize(
"reachability",
[
@ -103,10 +126,10 @@ def test_remote_task_file_uses_ssh_stdin_and_cleanup() -> None:
transport.remove_file(task_path)
create = run.call_args_list[0]
assert create.args[0][:2] == ["ssh", "agent@sandboxer01"]
assert create.args[0][:3] == ["ssh", "--", "agent@sandboxer01"]
assert create.kwargs["input"] == '{"title": "bounded"}'
assert "cat >" in create.args[0][2]
assert "rm -f" in run.call_args_list[1].args[0][2]
assert "cat >" in create.args[0][3]
assert "rm -f" in run.call_args_list[1].args[0][3]
def test_resolve_executable_fails_inside_transport() -> None:

View file

@ -0,0 +1,66 @@
---
id: GLAS-WP-0010
type: workplan
title: "Harden SSH transport and profile refusal coverage"
domain: infotech
repo: glas-harness
status: finished
owner: codex
topic_slug: transport-and-resolution-hardening
created: "2026-08-23"
updated: "2026-08-23"
---
# Harden SSH transport and profile refusal coverage
## Reject option-like SSH targets
```task
id: GLAS-WP-0010-T01
status: done
priority: high
```
Require the optional SSH user and host components to begin with an
alphanumeric character, terminate SSH option parsing explicitly, and cover
both descriptor validation and direct transport construction with regression
tests.
**Completed 2026-08-23:** SSH user and host components must now start with an
alphanumeric character, the constructed command includes the `--` option
terminator, and both factory-created and directly constructed transports reject
option-like targets. Focused transport tests pass.
## Pin multi-revision profile selection semantics
```task
id: GLAS-WP-0010-T02
status: done
priority: medium
```
Retain the safer resolver behavior in which every revision, including a
disabled revision, makes an unpinned selector ambiguous. Correct the profile
documentation and add mixed enabled/disabled coverage.
**Completed 2026-08-23:** documentation now matches the existing fail-closed
resolver: every revision participates in unpinned ambiguity, while a pinned
disabled revision refuses. A mixed enabled/disabled catalog test locks in both
properties.
## Prove pre-sandbox refusal and publish
```task
id: GLAS-WP-0010-T03
status: done
priority: high
```
Add gateway tests proving disabled and ambiguous selections cannot call
sand-boxer, run the full suite and catalog validation, synchronize the
workplan, publish, and retain any actionable leftovers as live residuals.
**Completed 2026-08-23:** real-catalog gateway tests prove disabled and
ambiguous selectors return normalized resolution refusals without calling
sand-boxer. All 72 tests, catalog validation, and whitespace checks pass. No
actionable leftover was discovered, so no residual was created.