glas-harness/docs/harness-contract.md
tegwick 695438019c
Some checks failed
ci / validate (push) Has been cancelled
Harden SSH and profile resolution boundaries
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0233b-178d-7162-b92f-31a31ea8ca9b
2026-08-23 12:53:05 +02:00

5.8 KiB

Harness contract

Contract version: 1.0 (glas_harness.contract.CONTRACT_VERSION).

This is the stable boundary between Glas, its consumers, and concrete harness backends (reins). Glas owns profile resolution and the outer lifecycle; a rein owns its inner agentic loop. Sand-boxer owns environment provisioning.

Consumer boundary

ExecutionRequest requires an explicit harness_profile_ref, repository, title, and description. It may carry correlation and organizational references (assignment_ref, role_ref, duty_ref, goal_refs, and resource_envelope_refs). Those references link execution evidence to leadership/workforce records; they do not transfer organizational authority to Glas.

actor is the governed sandbox consumer type and must be adm, agt, or atm. It is not a worker instance identifier. Queue consumers retain values such as rein-aharness@railiance01 as upstream worker_id ownership metadata and map governed agent execution to actor="agt". Glas validates this before sandbox creation and returns a normalized resolution refusal for invalid channel/library requests; the CLI rejects invalid choices during argument parsing.

The gateway returns GatewayResult with:

  • ok, derived from normalized outcome;
  • evidence, safe for compact State Hub reporting; and
  • direct-caller-only tool_output and tool_error.

Raw prompts, model output, tool output, and credential material are excluded from the State Hub detail.

HarnessProfile.status governs catalog selection. The separate OperationalReadiness model records runtime status, a reason, its owner, and an evidence reference. ready requires positive evidence; blocked requires an owner and evidence and is refused during resolution before sandbox creation; unverified permits only a labeled proof attempt. The selected state is copied into ResolvedExecutionContext and ExecutionEvidence.

Rein lifecycle

class Rein(ABC):
    def start_session(
        self, profile: HarnessProfile, inputs: dict[str, str], sandbox: SandboxHandle
    ) -> dict[str, Any]: ...

    def dispatch_tool(
        self, session: dict[str, Any], tool_call: ToolCall
    ) -> ToolResult: ...

    def end_session(self, session: dict[str, Any]) -> ExecutionSummary: ...

    def cleanup_session(self, session: dict[str, Any]) -> None: ...
Glas                                  rein                 sand-boxer
----                                  ----                 ----------
resolve profile + rein descriptor
create sandbox ------------------------------------------> create
derive workspace + transport from handle
start_session(profile, inputs, handle) -> sandbox-local setup
dispatch_tool(session, call)          -> inner loop across transport
end_session(session)                  -> normalized facts
cleanup_session(session)              -> remove ephemeral task material
destroy sandbox -----------------------------------------> destroy
publish compact ExecutionEvidence

After sandbox creation, the caller's source checkout is no longer an execution path. A same-host descriptor must contain pid plus workspace_dir and every rein command is wrapped with nsenter; a remote descriptor must contain ssh plus remote_dir and every command crosses SSH. Incomplete, mixed, or unknown reachability refuses at session start. The transport also bounds the outer rein 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.

Stability and extension rules

All contract models use Pydantic with unknown fields forbidden. Stable 1.0 fields are the declared fields in src/glas_harness/contract.py. Optional measurements such as tokens, duration, resolved model, commit, and artifacts remain null when unknown; unsupported tool-event visibility is unavailable, not an empty claim of completeness.

Rein-specific, non-secret additions belong only in declared metadata fields. Adding a required field, changing a field meaning/type, or removing a field requires a new contract version. Adding an optional field may remain compatible only when old consumers can safely ignore it and every strict boundary model is updated together. A profile and rein descriptor must both declare the exact supported contract version.

Sensitive values are never contract extensions. Profiles may contain credential-route references, while credential resolution and provider setup remain rein-local under ADR-002.

Failure semantics

The gateway returns evidence for every normal refusal/failure path:

  • resolution: unknown, disabled, ambiguous, incompatible, operationally blocked, or unsafe profile;
  • sandbox_create;
  • session_start;
  • execution (including a rein-declared unsuccessful result);
  • session_end; and
  • teardown.

Profile resolution happens before sandbox creation. Once a sandbox exists, rein cleanup and sandbox teardown are attempted on every path. refused means governed execution did not proceed; failed means an attempted lifecycle did not complete successfully.

Deliberate non-goals

The contract does not schedule work, source blueprints, allocate workers, resolve leadership, broker credentials, select an unapproved model by price, or standardize rein internals. Composable rein middleware remains deferred by ADR-004 until a second concrete need exists.