feat(runtime): enforce governed mutation boundaries

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a06ba0-10aa-7ea0-b20a-4f3fac39efe9
This commit is contained in:
tegwick 2026-09-04 11:25:07 +02:00
parent e3c6124e22
commit 20e6f381f6
28 changed files with 1068 additions and 154 deletions

View file

@ -51,6 +51,7 @@ rein-aharness run --task-file examples/task-hello-sandbox.json \
# Primary production intake: activity-core ops_run claim (ACT-ADR-005 / REIN-A-0002)
export ACTIVITY_CORE_URL=http://127.0.0.1:8010
export ACTIVITY_CORE_WORKER_TOKEN=…
export AGENT_HARNESS_LEGACY_APPROACHES_UNTIL=2026-12-31 # temporary profile-absent routes
export AGENT_HARNESS_REPO_MAP='{"freedom-intelligence":"~/freedom-intelligence","binky-control":"~/binky-control"}'
rein-aharness poll --source=ops-run --no-claim
rein-aharness run --from-ops-run
@ -88,8 +89,11 @@ Railiance deploy: [deploy/README.md](deploy/README.md).
Each `run` resolves the agent's tool profile + budget from the target
repo's instance manifest (default `green-commit-only`), loads a persona
bundle (`kaizen-agentic schedule prepare`), runs a bounded agentic
session (cwd-pinned, hard allow-list, never pushes), verifies a local
commit, writes `.kaizen/metrics`, and posts a State Hub progress event.
session (cwd-pinned, hard allow-list, never pushes), verifies a local commit,
records metrics, and posts a State Hub progress event. Local task files with a
v1 `repository_grant` receive full repository acceptance and mandatory durable
external metrics; grant-absent compatibility runs retain repository-local
`.kaizen/metrics` during migration.
When invoked through `glas-harness`, the explicit versioned Glas profile may
override model, tool profile, and budget for that bounded run. Workforce and
activity consumers should reference the Glas profile rather than encode these
@ -98,6 +102,7 @@ rein CLI details in an assignment.
Instance manifest contract: [docs/instance-manifest.md](docs/instance-manifest.md).
Example: [examples/schedule.harness.yml](examples/schedule.harness.yml).
Repository mutation authority: [docs/repository-grant.md](docs/repository-grant.md).
Granted-run metrics: [docs/external-metrics.md](docs/external-metrics.md).
Durable result-close design: [docs/close-evidence-outbox.md](docs/close-evidence-outbox.md).
Tests: `PYTHONPATH=".:$HOME/llm-connect" python3 -m pytest tests/ -q`

View file

@ -1,6 +1,6 @@
# SCOPE
> Capability snapshot: 2026-08-23. `INTENT.md` states the destination and
> Capability snapshot: 2026-09-04. `INTENT.md` states the destination and
> boundaries; this file records what the repository currently implements and
> has evidence for.
@ -22,8 +22,10 @@ retains a legacy set of local task executors for unattended repository work.
actor `agt`, returns the complete `GatewayResult`, and never falls back to a
legacy approach after profile refusal or failure.
- Profile-absent rows use the coexistence registry in `approaches.py`.
Unmatched rows fail visibly. JSON task files remain available for local
development, and issue-core polling remains as a legacy compatibility path.
Dispatch requires an explicit, non-expired
`AGENT_HARNESS_LEGACY_APPROACHES_UNTIL` date; unmatched or disabled routes
fail visibly. JSON task files remain available for local development, and
issue-core polling remains as a legacy compatibility path.
- The production claim loop is a single-concurrency user systemd service on
railiance01. Claim failures use the configured poll interval and active runs
receive lease heartbeats.
@ -34,6 +36,9 @@ retains a legacy set of local task executors for unattended repository work.
kaizen-agentic persona bundle, invoke Claude Code in the target checkout,
impose a wall-clock timeout and named Claude tool allow-list, and require a
new Git `HEAD` before reporting success.
- Local task files may carry an explicit v1 `repository_grant`. These runs
validate commit ancestry/count, changed paths, clean post-state, protected Git
metadata, and remote refs under the repository transaction before success.
- Optional Claude stream JSON is reduced to tool/hook audit events and can be
reported to State Hub.
- Versioned profiled rows delegate profile, rein, model, tool, sandbox, and
@ -58,9 +63,10 @@ retains a legacy set of local task executors for unattended repository work.
session to repository read/edit operations and selected local Git commands;
lane itself is metadata and consistency validation, not an OS security
boundary.
- Runs can append `.kaizen/metrics/<agent>/executions.jsonl`, regenerate the
summary, emit State Hub progress/tool/token events, and close an associated
Hub task. These reporting operations are best-effort.
- Grant-absent compatibility runs can append repository-local kaizen metrics.
Granted runs instead require private, durable external metrics with a
projection descriptor so accepted checkouts remain clean. State Hub
progress/tool/token events and task closing remain best-effort.
- llm-connect HTTP errors retain only bounded, allowlisted provider diagnosis.
### Packaging and deployment
@ -87,12 +93,11 @@ retains a legacy set of local task executors for unattended repository work.
- Direct legacy agent sessions rely on Claude Code's own tool mediation and
the host checkout. They are not an OS-level sandbox and do not prove that
no push/network activity occurred after the run. Unattended mutation now
takes a process-safe repository lock (ADR-002 / T02); commit acceptance
is still the `HEAD`-changed check until T03.
- Commit acceptance currently means only that `HEAD` changed. It does not yet
validate the changed-file set, branch/parent shape, clean working tree,
remote state, or provenance of the new commit. Metrics are written after
this check and can leave target-repo changes for a later commit.
takes a process-safe repository lock (ADR-002 / T02).
- Explicitly granted local task files have full repository acceptance and
clean external metrics. Grant-absent compatibility and profiled queue runs
still use their prior result contracts because Activity Core carries no
authoritative repository grant. Durable queue-close replay is also not live.
- The worker is sequential and repository mapping is host configuration. There
is no multi-worker repository lease, per-tenant process isolation, generic
credential broker, or tenant onboarding API.
@ -109,8 +114,9 @@ retains a legacy set of local task executors for unattended repository work.
- Profile routing and rein selection belong to Glas.
- Sandbox lifecycle and isolation belong to sand-boxer.
- Provider abstraction belongs to llm-connect.
- Durable work history and decisions belong to State Hub; agent memory and
metrics remain in the consuming repository.
- Durable work history and decisions belong to State Hub; agent memory remains
in the consuming repository. Granted-run metrics remain external until a
separately granted kaizen projection commits them to the instance.
## Orientation

View file

@ -3,6 +3,21 @@
Single shared harness instance on **railiance01**. Secrets stay on the host
(Lanes 23); the container image is the portable runtime package.
## Supported runtime topology
The authoritative production runtime is the railiance01 user service
`rein-aharness-claim-loop.service`. It owns the configured repository
checkouts, private runtime state, worker credential, lease heartbeats, and
signal-driven shutdown behavior.
The Kubernetes Deployment is explicitly labeled `packaging-smoke` and runs
`sleep infinity`. It proves that the image can be scheduled with its hardened
container settings; it is not ready to claim work and is not a failover worker.
Do not give it an Activity Core worker credential or scale it as execution
capacity. Promoting Kubernetes requires a separate reviewed cutover with real
workspace, credential, repository-lock, Glas/sandbox, shutdown, and recovery
semantics.
> **Renamed from agent-harness (HARNESS-WP-0002-T02) — cutover done
> 2026-07-26.** Railiance now runs `rein-aharness` end to end: image tag,
> k8s namespace, CLI command, Python package, host secrets dir, and
@ -48,6 +63,9 @@ Single shared harness instance on **railiance01**. Secrets stay on the host
- Lane 3 AppRole under `~/.local/rein-aharness/approle-binky-mail`
- `source ~/.local/rein-aharness/env`
- Hub: `http://127.0.0.1:18000` (ops-bridge) or in-cluster `state-hub.state-hub.svc`
- While profile-absent tenant definitions remain, set a reviewed ISO expiry in
`AGENT_HARNESS_LEGACY_APPROACHES_UNTIL`. The checked-in example expires
2026-12-31; missing or expired values refuse compatibility dispatch.
## Build & load image (workstation → railiance01)

View file

@ -1,5 +1,5 @@
# Long-lived instance placeholder until T03 task intake polls issue-core.
# Keeps one ready replica with harness CLI + git tools; no LLM session here.
# Packaging-smoke placeholder only. The supported production runtime is the
# railiance01 user service; this Deployment never claims or executes work.
apiVersion: apps/v1
kind: Deployment
metadata:
@ -8,6 +8,8 @@ metadata:
labels:
app.kubernetes.io/name: rein-aharness
app.kubernetes.io/part-of: rein-aharness
app.kubernetes.io/component: packaging-smoke
rein-aharness.railiance.io/runtime-role: packaging-smoke
spec:
replicas: 1
selector:
@ -18,6 +20,8 @@ spec:
labels:
app.kubernetes.io/name: rein-aharness
app.kubernetes.io/part-of: rein-aharness
app.kubernetes.io/component: packaging-smoke
rein-aharness.railiance.io/runtime-role: packaging-smoke
spec:
securityContext:
fsGroup: 10001

View file

@ -9,6 +9,7 @@ AGENT_HARNESS_OPS_LABELS=automated
AGENT_HARNESS_OPS_LABELS_MODE=any
AGENT_HARNESS_OPS_LEASE_SECONDS=900
AGENT_HARNESS_CLAIM_INTERVAL=30
AGENT_HARNESS_LEGACY_APPROACHES_UNTIL=2026-12-31
AGENT_HARNESS_REPO_MAP={"freedom-intelligence":"/home/tegwick/freedom-intelligence","binky-control":"/home/tegwick/binky-control"}
AGENT_HARNESS_REPO_ROOTS=/home/tegwick:/home/tegwick/work
LLM_CONNECT_URL=k8s://activity-core/llm-connect:8080

View file

@ -9,6 +9,8 @@ Type=simple
WorkingDirectory=%h/rein-aharness
Environment=PYTHONUNBUFFERED=1
Environment=KUBECONFIG=/etc/rancher/k3s/k3s.yaml
# Temporary profile-absent compatibility; remove after the four tenant migrations.
Environment=AGENT_HARNESS_LEGACY_APPROACHES_UNTIL=2026-12-31
# Ensure kubectl is available for k8s:// URL resolution in rein-aharness-claim
Environment=PATH=/usr/local/bin:/usr/bin:/bin:%h/bin
# Wrapper loads claim-loop.env (JSON-safe) and resolves k8s:// ClusterIPs

View file

@ -37,8 +37,8 @@
| Task intake | `intake.py` + `taskspec.py` | issue-core GET/PATCH poll+claim; JSON task-file for local dev | NATS when activity-core migrates |
| Persona | `persona.py` | `kaizen-agentic schedule prepare` (ADR-005) | unchanged, plus phase-memory profile hook |
| Session | `adapter.py` | `AgenticClaudeCodeAdapter` (cwd-pinned, profile allow-list) | + hosted adapters |
| Orchestration | `runner.py` | profile/budget → session → commit → metrics → hub | activity-core intake |
| Metrics | `metrics.py` | ADR-004 `.kaizen/metrics` write per run | correlate with Helix fleet metrics |
| Orchestration | `runner.py` | profile/budget → transaction → session → grant acceptance → metrics → hub | activity-core intake |
| Metrics | `metrics.py` | compatibility `.kaizen/metrics`; durable external ledger for granted runs | kaizen-owned projection |
| Mail lane | `mailscan.py` | deterministic credentialed pre-step outside the session | pattern generalizes to other credentialed pre-steps |
| Hub reporting | `hub.py` | REST progress event + task close + token event | unchanged |
@ -71,7 +71,7 @@ non-interactive AppRole. See binky-control
- **Regulation:** manifest declares, harness enforces. flex-auth gates
apply at credential acquisition; ops-warden catalogs every lane; each
instance runs under a named hub identity (`agt-…`).
- **Evolution:** the harness emits `.kaizen/metrics` per run so the
- **Evolution:** the harness emits kaizen-compatible metrics per run so the
kaizen optimization loop covers both blueprints and the harness itself.
Harness releases are versioned; instances pin majors; blueprint
conformance tests run before rollout.

36
docs/external-metrics.md Normal file
View file

@ -0,0 +1,36 @@
# Durable external metrics
Repository-granted runs must not dirty a checkout after acceptance. Their
kaizen-compatible execution record and summary are therefore written beneath:
```text
$REIN_AHARNESS_STATE_DIR/execution-metrics/<repository-id>/<agent-id>/
```
When the explicit state variable is absent, the runtime uses
`$XDG_STATE_HOME/rein-aharness` or `~/.local/state/rein-aharness`. Repository
and agent directory names are SHA-256-derived identifiers rather than caller
text. Directories are mode `0700`; records and lock files are mode `0600`.
Each directory contains:
- `executions.jsonl`: append-only kaizen `ExecutionRecord` values;
- `summary.json`: regenerated aggregate metrics; and
- `projection.json`: repository id/name, agent name, and intended consuming
path `.kaizen/metrics/<agent>`.
Writes serialize on a process-safe lock. The execution record is flushed and
`fsync`ed before the summary and projection descriptors are atomically replaced
and directory metadata is `fsync`ed. Replaying the same transaction identity
regenerates the derived files without adding a duplicate ledger record. Failure
to persist this evidence makes a granted run unsuccessful; granted execution
cannot opt out with `--no-metrics`.
Projection is a separate repository mutation. This harness does not copy these
files back automatically, because doing so would invalidate the accepted clean
post-state. A future kaizen-owned projection lane may consume
`projection.json`, copy `executions.jsonl` and `summary.json` to the declared
target, and commit them under its own explicit repository grant. Until that
lane exists, the external files are the durable source of truth for granted
runs. Grant-absent compatibility runs continue to write repository-local
metrics directly.

View file

@ -1,15 +1,16 @@
# Legacy profile-absent runtime inventory
Status: preparatory inventory for `HARNESS-WP-0003-T04`
Status: active migration inventory for `REINAH-WP-0003-T04`
Captured: 2026-08-23
Captured: 2026-08-23; compatibility guard added 2026-09-04
Runtime behavior source: rein-aharness `6e0e23c`
Initial inventory source: rein-aharness `6e0e23c`; guard behavior updated in
the implementation recorded by this workplan.
This inventory records the tenant-specific execution paths selected when an
Activity Core `ops_run` has no `harness_profile_ref`. It is deliberately
read-only preparation: T04 remains `wait` until ADR-002 is accepted, and this
document neither enables a definition nor changes a production route.
Activity Core `ops_run` has no `harness_profile_ref`. It began as read-only
preparation; after ADR-002 acceptance it also records the dated compatibility
guard. This document does not itself enable a definition.
The definition state below is the committed file state in Activity Core,
freedom-intelligence, and binky-control. Live schedule projection, timer state,
@ -24,12 +25,15 @@ id, title, hint, labels, source id, and target repo.
None of the four committed definitions below declares `harness_profile_ref` or
`approach_hint`; their current route therefore depends on labels and substring
matching. A present profile ref bypasses this table and must never fall back to
it.
matching. Dispatch now additionally requires an explicit, non-expired ISO date
in `AGENT_HARNESS_LEGACY_APPROACHES_UNTIL`. Missing, malformed, or expired
values fail terminally before target resolution or executor invocation. A
present profile ref bypasses both this table and the compatibility flag and
must never fall back to either.
| Selector | Tenant coupling | Direct executor |
|---|---|---|
| `fi-research-brief` | FI labels, names, output schema, completion event, literal result repo, default push | `fi_research_brief.run_fi_research_brief` |
| `fi-research-brief` | FI labels, names, output schema, completion event, literal result repo | `fi_research_brief.run_fi_research_brief` |
| `brief-daily` | Binky labels, files, prompt/output schema, completion event, literal result repo | `brief_daily.run_brief_daily` |
| `brief-weekly` | Binky labels, milestone/RISK-005 semantics, completion event, literal result repo | `brief_weekly.run_brief_weekly` |
| `mail-scan+triage` | Binky labels, mailbox config/path, OpenBao path, report/log schema and events | `mailscan.run_mail_scan` then `mail_triage.run_mail_triage` |
@ -38,9 +42,10 @@ it.
An unmatched route fails terminally. Most matched executor failures set
`reopen=true`; an unexpected exception also reopens. Activity Core applies its
attempt limit, but the harness currently has no shared repository transaction
identity with which to distinguish a safe retry from a repeat after partial
mutation.
attempt limit. The harness now records a repository transaction identity, but
Activity Core does not yet carry an accepted repository grant or reconcile a
response-lost terminal close, so a retry after ambiguous completion remains a
T03 gate.
## Scheduled definitions
@ -55,13 +60,13 @@ mutation.
| Model/credential lane | HTTP `LLM_CONNECT_URL`; model from `FI_RESEARCH_BRIEF_MODEL`, then Binky-named `BRIEF_DAILY_MODEL` / `MAIL_TRIAGE_MODEL`; provider credential remains behind llm-connect |
| Output | `briefs/YYYY/MM/YYYY-MM-DD.md`; model JSON is rendered deterministically |
| Repository mutation | Stages only the brief path and creates one local commit |
| Publication | **Pushes `origin HEAD` by default** unless `FI_RESEARCH_BRIEF_PUSH` is false; push failure is swallowed and the run remains successful |
| Publication | Local commit only. The former `FI_RESEARCH_BRIEF_PUSH` path was removed 2026-09-04; publication requires a separate owner/grant. |
| Completion | Best-effort State Hub `fi_daily_brief` with path/date/candidate count; `fi_brief_status` uses it to clear due state |
| Failure/retry | Normal generation/commit failure reopens; an existing daily path succeeds idempotently |
| Current rollback material | Disable/pause the Activity Core definition. FI documents a disabled 07:35 host timer as break-glass, but its installer still enables that timer and must not be invoked casually |
Migration must remove the implicit push. Publication, if still needed, becomes
a separately named and granted capability with remote-ref and result evidence.
Publication, if still needed, must become a separately named and granted
capability with remote-ref and result evidence.
The Binky-named fallback model environment variables must not survive in an FI
owned declaration.
@ -148,15 +153,17 @@ a repository transaction or remote-ref check.
2. **Tenant code in the shared runtime:** prompts, output schemas, file paths,
event types, FI publication, Binky risk logic, mailbox paths, and
credential lanes require edits to rein-aharness.
3. **No shared repository transaction:** dirty baselines, partial writes,
unexpected commits/paths, lease loss, and retries after mutation are not
bounded. T02/T03 remain prerequisites for any retained direct executor.
3. **Queue acceptance is incomplete:** T02 now bounds dirty baselines,
transaction identity, and lease-loss cancellation, and local granted tasks
have T03 acceptance. Activity Core still lacks authoritative grant carriage
and terminal-close reconciliation for queued execution.
4. **Required evidence is best-effort:** Hub delivery failure does not fail or
reconcile a run even though the due resolvers depend on those events.
5. **Completion meanings diverge:** mailbox scan and triage have different
events, while the Activity definition describes one intake task.
6. **Publication is implicit in FI:** default push plus swallowed failure cannot
meet an explicit grant/evidence contract.
6. **FI publication is now separated:** the compatibility executor is
local-commit-only; any future publish lane still needs an explicit owner,
grant, and evidence contract.
7. **Mutable sibling/runtime dependencies:** mail scan defaults to a sibling
email-connect checkout, and model selection depends on process environment
rather than a versioned execution declaration.
@ -169,8 +176,9 @@ a repository transaction or remote-ref check.
This sequence is preparatory and does not choose a profile or capability owner
before ADR-002 acknowledgement.
1. Land T02/T03 repository transaction, lease-loss, acceptance, and evidence
reconciliation for any compatibility executor that remains callable.
1. Carry T02's transaction/lease evidence and T03's repository grant through
Activity Core, then activate reconciled close evidence for any compatibility
executor that remains callable.
2. For each scheduled definition, obtain an owner-approved declaration of the
exact input schema, allowed paths, model route, credential route, completion
event, commit policy, publication policy, and rollback.
@ -184,6 +192,6 @@ before ADR-002 acknowledgement.
5. Enable the replacement, verify the due resolver and durable evidence, then
remove that definition from substring routing.
6. After all four definitions migrate, enable
`ACTIVITY_CORE_REQUIRE_HARNESS_PROFILE=true` for governed execution, delete
`_DEFINITION_HINTS` and tenant approach rules, and reconcile or retire the
tenant host timers.
`ACTIVITY_CORE_REQUIRE_HARNESS_PROFILE=true` for governed execution, remove
`AGENT_HARNESS_LEGACY_APPROACHES_UNTIL`, delete `_DEFINITION_HINTS` and
tenant approach rules, and reconcile or retire the tenant host timers.

View file

@ -38,6 +38,7 @@ the legacy coexistence path until the migration flag is enabled upstream.
| `AGENT_HARNESS_OPS_LABELS_MODE` | `any` | `any` or `all` |
| `AGENT_HARNESS_OPS_LEASE_SECONDS` | `900` | claim lease |
| `AGENT_HARNESS_CLAIM_INTERVAL` | `30` | empty-queue poll interval |
| `AGENT_HARNESS_LEGACY_APPROACHES_UNTIL` | unset | ISO expiry date required for profile-absent compatibility routing |
| `AGENT_HARNESS_REPO_MAP` | `{}` | JSON slug→checkout path |
| `AGENT_HARNESS_REPO_ROOTS` | `~:~/work` | slug search roots |
@ -48,6 +49,7 @@ export AGENT_HARNESS_REPO_MAP='{
"freedom-intelligence":"/home/tegwick/freedom-intelligence",
"binky-control":"/home/tegwick/binky-control"
}'
export AGENT_HARNESS_LEGACY_APPROACHES_UNTIL=2026-12-31
export ACTIVITY_CORE_URL=http://127.0.0.1:8010 # or ClusterIP via tunnel
export ACTIVITY_CORE_WORKER_TOKEN=… # from actcore-runtime-secret
```

View file

@ -1,6 +1,7 @@
# Repository grant contract
Status: **v1 parsed, validation-ready, not execution-enabled**.
Status: **v1 enabled for local `TaskSpec` files; queued/profiled carriage is
not yet available**.
`repository_grant` is the explicit authority envelope for a bounded local Git
mutation. It is separate from task prose, labels, organizational attribution,
@ -39,20 +40,28 @@ The repository acceptance validator converts the grant into the policy used to
check descendant commits, changed paths, clean post-state, protected Git
metadata, and local remote-tracking refs.
## Current fail-closed posture
## Current execution posture
`TaskSpec.from_file` parses this contract, but `run_task` deliberately refuses
any task that supplies it before adapter dispatch. The Activity Core and
issue-core adapters do not currently populate it. Existing grant-absent direct
and compatibility runs retain their prior behavior while the transaction path
remains production-inert.
`TaskSpec.from_file` parses this contract and `run_task` executes it under the
canonical repository transaction. A successful adapter result is accepted only
when the commit ancestry/count, changed paths, clean post-state, protected Git
metadata, and remote-tracking refs satisfy the exact grant. The result carries
bounded grant, baseline, policy, commit, and path evidence without raw grant
paths, prompts, or provider output.
Execution may be enabled only after:
Granted runs require durable external metrics and refuse `--no-metrics`. Their
metrics are written outside the checkout only after acceptance, so a successful
run remains clean. See [external-metrics.md](external-metrics.md).
Activity Core and issue-core adapters do not populate `repository_grant`.
Queued/profiled runs therefore cannot claim repository acceptance under this
contract. Existing grant-absent direct and compatibility runs retain their
legacy `HEAD`-changed behavior during migration.
Queued/profiled execution may use this contract only after:
1. an authoritative Activity Core/profile field carries the reviewed grant;
2. the lease-bound transaction wraps adapter dispatch and result close;
3. the runner validates the accepted result against this exact grant; and
4. required close evidence durably records the grant, transaction, and accepted
2. required close evidence durably records the grant, transaction, and accepted
result identities.
Task descriptions, labels, `execution_refs`, consuming-repo defaults, and

View file

@ -3,8 +3,8 @@
Consuming repos declare instances in `.kaizen/schedule.yml`; this package
runs them: resolve tool profile + budget, load persona orientation
(kaizen-agentic schedule prepare), run a bounded agentic session via an
llm-connect adapter, verify the local commit, write `.kaizen/metrics`,
and report to the Custodian State Hub.
llm-connect adapter, validate granted local commits, record metrics, and report
to the Custodian State Hub.
"""
__version__ = "0.1.0"

View file

@ -12,7 +12,9 @@ How to add a row:
from __future__ import annotations
import os
from dataclasses import dataclass, field
from datetime import date
from pathlib import Path
from typing import Any
@ -35,6 +37,21 @@ APPROACH_MAIL_TRIAGE = "mail-triage"
APPROACH_MAIL_PIPELINE = "mail-scan+triage"
APPROACH_AGENT_SESSION = "agent-session"
APPROACH_UNMATCHED = "unmatched"
LEGACY_APPROACHES_UNTIL_ENV = "AGENT_HARNESS_LEGACY_APPROACHES_UNTIL"
def legacy_approaches_enabled(
value: str | None = None,
*,
today: date | None = None,
) -> bool:
"""Require an explicit, non-expired ISO date for profile-absent routing."""
raw = os.environ.get(LEGACY_APPROACHES_UNTIL_ENV, "") if value is None else value
try:
expires = date.fromisoformat(raw.strip())
except (AttributeError, ValueError):
return False
return (today or date.today()) <= expires
@dataclass(frozen=True)
@ -185,6 +202,19 @@ def execute_approach(
reopen=False,
)
if not legacy_approaches_enabled():
configured = os.environ.get(LEGACY_APPROACHES_UNTIL_ENV, "").strip()
state = f"expired at {configured}" if configured else "not configured"
return ApproachResult(
ok=False,
approach=name,
reason=(
"refused: profile-absent compatibility routing is disabled "
f"({LEGACY_APPROACHES_UNTIL_ENV} {state})"
),
reopen=False,
)
try:
target = resolve_ops_target(run, cfg)
except TaskSpecError as exc:
@ -266,6 +296,7 @@ def execute_approach(
reopen=True,
)
def _run_fi(target: Path, *, report_to_hub: bool, commit: bool) -> ApproachResult:
from rein_aharness.fi_research_brief import run_fi_research_brief

View file

@ -305,7 +305,10 @@ def main(argv: list[str] | None = None) -> int:
run.add_argument(
"--no-metrics",
action="store_true",
help="Skip writing .kaizen/metrics in the target repo",
help=(
"Skip compatibility .kaizen/metrics writes "
"(not permitted with repository_grant)"
),
)
run.add_argument(
"--stream-tool-events",

View file

@ -420,20 +420,6 @@ def run_fi_research_brief(
)
committed = True
head_after = _git(repo, "rev-parse", "HEAD")
# Best-effort push so workstation / Forgejo see the brief.
# Diverged branches must not fail the run; log via reason only if push fails
# after a successful write.
if committed and os.environ.get("FI_RESEARCH_BRIEF_PUSH", "1").strip().lower() not in {
"0",
"false",
"no",
"off",
}:
try:
_git(repo, "push", "origin", "HEAD")
except FiResearchBriefError:
# Leave brief committed locally; operators reconcile git separately.
pass
except FiResearchBriefError as exc:
result = FiResearchBriefResult(
ok=False,

View file

@ -1,7 +1,8 @@
"""Write per-run records into the target repo's `.kaizen/metrics` tree.
"""Write per-run metrics to compatibility or durable external storage.
Follows kaizen-agentic ADR-004 conventions so the optimization loop can
observe harness-run agents:
Legacy grant-absent runs retain the kaizen-agentic ADR-004 repository layout.
Accepted grant runs use private external state and a projection descriptor so
metrics cannot dirty the validated checkout.
.kaizen/metrics/<agent>/
executions.jsonl # append-only
@ -10,7 +11,11 @@ observe harness-run agents:
from __future__ import annotations
import fcntl
import hashlib
import json
import os
import uuid
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from pathlib import Path
@ -46,6 +51,20 @@ def metrics_dir(project_root: Path, agent: str) -> Path:
return Path(project_root) / ".kaizen" / "metrics" / agent
def external_metrics_dir(
project_root: Path | str,
agent: str,
*,
state_dir: Path | None = None,
) -> Path:
"""Return the private durable metrics directory for a granted run."""
root = Path(project_root).expanduser().resolve()
base = state_dir.expanduser().resolve() if state_dir else _state_dir()
repo_id = hashlib.sha256(str(root).encode("utf-8")).hexdigest()[:32]
agent_id = hashlib.sha256(str(agent).encode("utf-8")).hexdigest()[:32]
return base / "execution-metrics" / repo_id / agent_id
def _utc_now() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace(
"+00:00", "Z"
@ -133,18 +152,17 @@ def record_execution(
directory = metrics_dir(root, agent)
directory.mkdir(parents=True, exist_ok=True)
record = ExecutionRecord(
timestamp=_utc_now(),
agent=agent,
record = _execution_record(
root,
agent,
success=success,
execution_time_s=float(execution_time_s),
session_id=session_id,
metadata=metadata or {},
repo=root.name,
execution_time_s=execution_time_s,
tokens=tokens,
committed=committed,
head_after=head_after,
reason=reason,
metadata=metadata,
session_id=session_id,
)
executions_path = directory / "executions.jsonl"
@ -158,3 +176,189 @@ def record_execution(
encoding="utf-8",
)
return executions_path
def record_external_execution(
project_root: Path | str,
agent: str,
*,
success: bool,
execution_time_s: float = 0.0,
tokens: int | None = None,
committed: bool | None = None,
head_after: str | None = None,
reason: str | None = None,
metadata: dict[str, Any] | None = None,
session_id: str | None = None,
state_dir: Path | None = None,
) -> Path:
"""Durably record granted-run metrics without dirtying the target checkout."""
root = Path(project_root).expanduser().resolve()
projection_target = _projection_target(agent)
directory = external_metrics_dir(root, agent, state_dir=state_dir)
_ensure_private_directory(directory)
lock_path = directory / ".lock"
lock_fd = os.open(lock_path, os.O_RDWR | os.O_CREAT, 0o600)
try:
os.fchmod(lock_fd, 0o600)
fcntl.flock(lock_fd, fcntl.LOCK_EX)
record = _execution_record(
root,
agent,
success=success,
execution_time_s=execution_time_s,
tokens=tokens,
committed=committed,
head_after=head_after,
reason=reason,
metadata=metadata,
session_id=session_id,
)
executions_path = directory / "executions.jsonl"
existing = (
executions_path.read_text(encoding="utf-8")
if executions_path.exists()
else ""
)
if existing and not existing.endswith("\n"):
raise OSError("external metrics ledger has an incomplete record")
records = _load_external_executions(executions_path) if existing else []
already_recorded = session_id is not None and any(
item.get("session_id") == session_id for item in records
)
if not already_recorded:
record_line = record.to_json_line()
_atomic_write_text(executions_path, existing + record_line + "\n")
records.append(json.loads(record_line))
_atomic_write_text(
directory / "summary.json",
json.dumps(regenerate_summary(agent, records), indent=2, sort_keys=True)
+ "\n",
)
repo_id = directory.parent.name
_atomic_write_text(
directory / "projection.json",
json.dumps(
{
"agent": agent,
"repository_id": repo_id,
"repository_name": root.name,
"target_relative_directory": projection_target,
},
indent=2,
sort_keys=True,
)
+ "\n",
)
_fsync_directory(directory)
return executions_path
finally:
try:
fcntl.flock(lock_fd, fcntl.LOCK_UN)
finally:
os.close(lock_fd)
def _projection_target(agent: str) -> str:
"""Return a checkout-relative projection target for a safe agent identity."""
if (
not agent
or agent in {".", ".."}
or "/" in agent
or "\\" in agent
or "\x00" in agent
or len(agent) > 200
):
raise OSError("agent identity cannot form a safe metrics projection")
return f".kaizen/metrics/{agent}"
def _execution_record(
root: Path,
agent: str,
*,
success: bool,
execution_time_s: float,
tokens: int | None,
committed: bool | None,
head_after: str | None,
reason: str | None,
metadata: dict[str, Any] | None,
session_id: str | None,
) -> ExecutionRecord:
return ExecutionRecord(
timestamp=_utc_now(),
agent=agent,
success=success,
execution_time_s=float(execution_time_s),
session_id=session_id,
metadata=metadata or {},
repo=root.name,
tokens=tokens,
committed=committed,
head_after=head_after,
reason=reason,
)
def _load_external_executions(path: Path) -> list[dict[str, Any]]:
records: list[dict[str, Any]] = []
for line in path.read_text(encoding="utf-8").splitlines():
try:
value = json.loads(line)
except json.JSONDecodeError as exc:
raise OSError("external metrics ledger contains invalid JSON") from exc
if not isinstance(value, dict):
raise OSError("external metrics ledger contains a non-object record")
records.append(value)
return records
def _state_dir() -> Path:
explicit = os.environ.get("REIN_AHARNESS_STATE_DIR", "").strip()
if explicit:
return Path(explicit).expanduser().resolve()
xdg = os.environ.get("XDG_STATE_HOME", "").strip()
if xdg:
return (Path(xdg).expanduser() / "rein-aharness").resolve()
return (Path.home() / ".local" / "state" / "rein-aharness").resolve()
def _ensure_private_directory(path: Path) -> None:
path.mkdir(parents=True, exist_ok=True)
current = path
while current.name and current != current.parent:
os.chmod(current, 0o700)
if current.name == "execution-metrics":
break
current = current.parent
def _atomic_write_text(path: Path, value: str) -> None:
temporary = path.parent / f".{path.name}.{uuid.uuid4().hex}.tmp"
fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
try:
with os.fdopen(fd, "w", encoding="utf-8") as handle:
fd = -1
handle.write(value)
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary, path)
os.chmod(path, 0o600)
_fsync_directory(path.parent)
except BaseException:
if fd >= 0:
os.close(fd)
try:
temporary.unlink()
except FileNotFoundError:
pass
raise
def _fsync_directory(path: Path) -> None:
fd = os.open(path, os.O_RDONLY)
try:
os.fsync(fd)
finally:
os.close(fd)

View file

@ -1,10 +1,4 @@
"""Versioned authority contract for repository mutation.
The grant is parsed by TaskSpec but is not yet wired into run execution. A
supplied grant therefore causes run_task to refuse before adapter dispatch.
This keeps the contract reviewable without implying enforcement that the live
runner does not yet provide.
"""
"""Versioned authority contract for accepted local repository mutation."""
from __future__ import annotations

View file

@ -1,9 +1,9 @@
"""Process-safe transaction boundary for a local Git checkout.
Wired into `run_task`, profile-absent `execute_approach` mutators, and the
profiled claim path under REINAH-WP-0003-T02 / ADR-002. Repository
acceptance (T03) remains a separate read-only validator and is not applied
to live results yet.
profiled claim path under REINAH-WP-0003-T02 / ADR-002. Explicit local
TaskSpec grants activate repository acceptance; queued/profiled grant carriage
remains an upstream contract dependency.
"""
from __future__ import annotations

View file

@ -2,10 +2,10 @@
Flow: lock target repo resolve tool profile / budget from instance
manifest snapshot HEAD persona bundle prompt agentic session
verify a new commit exists kaizen metrics + hub progress event
(+ task close). The run *fails* if the session pushed anywhere or left
the repo dirty in a way it should not the worker never pushes;
publishing is a separate, explicitly-granted lane.
validate an explicit repository grant when present metrics + hub progress
event (+ task close). Granted runs use durable external metrics so repository
acceptance remains clean. The worker never pushes; publishing is a separate,
explicitly-granted lane.
"""
from __future__ import annotations
@ -24,6 +24,7 @@ from rein_aharness.profiles import UnknownToolProfileError, get_profile
from rein_aharness.repository_transaction import (
DirtyRepositoryError,
GitRepositoryError,
RepositoryAcceptanceError,
RepositoryBusyError,
RepositoryTransaction,
RepositoryTransactionError,
@ -116,11 +117,11 @@ def run_task(
budget_tokens_override: int | None = None,
transaction: RepositoryTransaction | None = None,
) -> RunResult:
if spec.repository_grant is not None:
if spec.repository_grant is not None and not write_metrics:
return _refused_run(
reason=(
"refused: repository_grant enforcement is not enabled; "
"no adapter was dispatched"
"refused: repository_grant runs require durable external metrics; "
"--no-metrics is incompatible"
),
model=model,
)
@ -255,11 +256,62 @@ def _execute_locked_task(
head_after = _git(spec.target_repo, "rev-parse", "HEAD")
committed = head_after != head_before
if session_ok and spec.repository_grant is not None:
try:
tx.validate_acceptance(spec.repository_grant.acceptance_policy())
except RepositoryAcceptanceError as exc:
session_ok = False
reason = str(exc)
ok = session_ok and committed
if session_ok and not committed:
if session_ok and not committed and spec.repository_grant is None:
reason = "session completed without committing"
tokens_spent = budget_tracker.spent if budget_tracker is not None else None
transaction_evidence = tx.evidence()
if spec.repository_grant is not None:
transaction_evidence["repository_grant"] = spec.repository_grant.evidence()
metric_metadata = {
"task_title": spec.title,
"tool_profile": profile.name,
"labels": list(spec.labels),
"completion_event_type": spec.completion_event_type,
}
if spec.repository_grant is not None:
metric_metadata["repository_grant_id"] = spec.repository_grant.grant_id
if write_metrics:
try:
recorder = (
metrics.record_external_execution
if spec.repository_grant is not None
else metrics.record_execution
)
recorder(
spec.target_repo,
spec.agent,
success=ok,
execution_time_s=execution_time_s,
tokens=tokens_spent,
committed=committed,
head_after=head_after,
reason=reason or None,
metadata=metric_metadata,
session_id=tx.transaction_id,
)
if spec.repository_grant is not None:
transaction_evidence["metrics"] = {
"storage": "external",
"session_id": tx.transaction_id,
"projection_ready": True,
}
except OSError as exc:
if spec.repository_grant is not None:
ok = False
reason = (
"required external metrics persistence failed "
f"({type(exc).__name__})"
)
result = RunResult(
ok=ok,
@ -275,30 +327,9 @@ def _execute_locked_task(
tokens_spent=tokens_spent,
execution_time_s=execution_time_s,
tool_events=collected_events,
transaction=tx.evidence(),
transaction=transaction_evidence,
)
if write_metrics:
try:
metrics.record_execution(
spec.target_repo,
spec.agent,
success=ok,
execution_time_s=execution_time_s,
tokens=tokens_spent,
committed=committed,
head_after=head_after,
reason=reason or None,
metadata={
"task_title": spec.title,
"tool_profile": profile.name,
"labels": list(spec.labels),
"completion_event_type": spec.completion_event_type,
},
)
except OSError:
pass # metrics must not block run completion reporting
if report_to_hub:
detail = {
"repo": spec.target_repo.name,
@ -314,6 +345,7 @@ def _execute_locked_task(
"budget_tokens": budget_tokens,
"tokens_spent": tokens_spent,
"execution_time_s": round(execution_time_s, 3),
"repository_transaction": transaction_evidence,
}
hub.post_progress_event(
summary=f"executor run: {spec.title} ({'ok' if ok else 'failed'})",

12
tests/conftest.py Normal file
View file

@ -0,0 +1,12 @@
from __future__ import annotations
from pathlib import Path
import pytest
@pytest.fixture(autouse=True)
def isolated_runtime_state(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Keep repository locks and close evidence out of operator-owned state."""
monkeypatch.setenv("REIN_AHARNESS_STATE_DIR", str(tmp_path / "runtime-state"))
monkeypatch.setenv("AGENT_HARNESS_LEGACY_APPROACHES_UNTIL", "2099-12-31")

View file

@ -2,6 +2,8 @@
from __future__ import annotations
from datetime import date
from rein_aharness.approaches import (
APPROACH_AGENT_SESSION,
APPROACH_BRIEF_DAILY,
@ -9,6 +11,8 @@ from rein_aharness.approaches import (
APPROACH_FI_RESEARCH_BRIEF,
APPROACH_MAIL_PIPELINE,
APPROACH_UNMATCHED,
execute_approach,
legacy_approaches_enabled,
select_approach,
)
from rein_aharness.ops_run_client import OpsRun
@ -113,3 +117,26 @@ def test_fi_before_generic_automated() -> None:
assert (
select_approach(_run(labels=["research-brief"])) == APPROACH_FI_RESEARCH_BRIEF
)
def test_legacy_approaches_require_non_expired_explicit_date() -> None:
today = date(2026, 9, 4)
assert legacy_approaches_enabled("2026-09-04", today=today) is True
assert legacy_approaches_enabled("2026-09-03", today=today) is False
assert legacy_approaches_enabled("true", today=today) is False
assert legacy_approaches_enabled("", today=today) is False
def test_execute_approach_refuses_profile_absent_route_without_flag(
monkeypatch,
) -> None:
monkeypatch.delenv("AGENT_HARNESS_LEGACY_APPROACHES_UNTIL")
result = execute_approach(
_run(labels=["research-brief"], target_repo="not-resolved"),
report_to_hub=False,
)
assert result.ok is False
assert result.reopen is False
assert "compatibility routing is disabled" in result.reason

View file

@ -2,8 +2,10 @@
from __future__ import annotations
from unittest.mock import MagicMock, patch
import subprocess
import time
from pathlib import Path
from unittest.mock import MagicMock, patch
from rein_aharness.approaches import ApproachResult, APPROACH_FI_RESEARCH_BRIEF
from rein_aharness.claim_loop import (
@ -20,6 +22,7 @@ from rein_aharness.ops_run_client import (
OpsRunConfig,
OpsRunError,
)
from rein_aharness.repository_transaction import RepositoryTransaction
def _claimed_run() -> OpsRun:
@ -37,6 +40,41 @@ def _claimed_run() -> OpsRun:
)
def _profiled_case(
tmp_path: Path,
) -> tuple[Path, OpsRun, MagicMock]:
repo = tmp_path / "freedom-intelligence"
repo.mkdir()
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
(repo / "README.md").write_text("controlled target\n", encoding="utf-8")
subprocess.run(["git", "add", "."], cwd=repo, check=True)
subprocess.run(
[
"git",
"-c",
"user.email=test@example.invalid",
"-c",
"user.name=test",
"commit",
"-qm",
"baseline",
],
cwd=repo,
check=True,
)
run = _claimed_run()
run.harness_profile_ref = "harness.agent-dev-local@1.0.0"
client = MagicMock(spec=ActivityCoreOpsClient)
client.config = OpsRunConfig(
worker_id="w",
lease_seconds=90,
repo_roots=(str(tmp_path),),
)
client.claim.return_value = [run]
return repo, run, client
def test_process_one_empty() -> None:
client = MagicMock(spec=ActivityCoreOpsClient)
client.config = OpsRunConfig(worker_id="w", lease_seconds=90)
@ -231,12 +269,8 @@ def test_process_one_records_adapter_exception_without_unbound_result() -> None:
client.fail.assert_not_called()
def test_profiled_exception_after_lease_loss_skips_close() -> None:
client = MagicMock(spec=ActivityCoreOpsClient)
client.config = OpsRunConfig(worker_id="w", lease_seconds=90)
run = _claimed_run()
run.harness_profile_ref = "harness.agent-dev-local@1.0.0"
client.claim.return_value = [run]
def test_profiled_exception_after_lease_loss_skips_close(tmp_path: Path) -> None:
repo, _run, client = _profiled_case(tmp_path)
client.heartbeat.side_effect = OpsRunError("lease rejected", status_code=409)
def slow_profile(*_args, **_kwargs):
@ -253,6 +287,8 @@ def test_profiled_exception_after_lease_loss_skips_close() -> None:
assert result.reason.startswith("lease lost")
client.complete.assert_not_called()
client.fail.assert_not_called()
with RepositoryTransaction(repo) as retry:
assert retry.locked is True
def test_poll_peek() -> None:
@ -263,13 +299,11 @@ def test_poll_peek() -> None:
assert rows[0]["approach"] == APPROACH_FI_RESEARCH_BRIEF
def test_profiled_run_uses_glas_and_completes_with_full_result() -> None:
client = MagicMock(spec=ActivityCoreOpsClient)
client.config = OpsRunConfig(worker_id="w", lease_seconds=90)
run = _claimed_run()
run.harness_profile_ref = "harness.agent-dev-local@1.0.0"
def test_profiled_run_uses_glas_and_completes_with_full_result(
tmp_path: Path,
) -> None:
_repo, run, client = _profiled_case(tmp_path)
run.approach_hint = "fi-research-brief"
client.claim.return_value = [run]
client.complete.return_value = OpsRun(
id=run.id,
activity_definition_id="def",
@ -313,13 +347,12 @@ def test_profiled_run_uses_glas_and_completes_with_full_result() -> None:
execute.assert_not_called()
def test_profile_refusal_fails_terminally_without_legacy_fallback() -> None:
client = MagicMock(spec=ActivityCoreOpsClient)
client.config = OpsRunConfig(worker_id="w", lease_seconds=90)
run = _claimed_run()
def test_profile_refusal_fails_terminally_without_legacy_fallback(
tmp_path: Path,
) -> None:
_repo, run, client = _profiled_case(tmp_path)
run.harness_profile_ref = "harness.unknown@9.9.9"
run.approach_hint = "fi-research-brief"
client.claim.return_value = [run]
client.fail.return_value = OpsRun(
id=run.id,
activity_definition_id="def",
@ -346,6 +379,49 @@ def test_profile_refusal_fails_terminally_without_legacy_fallback() -> None:
execute.assert_not_called()
def test_profiled_signal_cancellation_releases_repository_lock(
tmp_path: Path,
) -> None:
repo, _run, client = _profiled_case(tmp_path)
def cancel_during_gateway(*_args, **_kwargs):
_cancel_active_run("signal")
return {"ok": True, "evidence": {"outcome": "late-success"}}
with patch(
"rein_aharness.claim_loop.execute_profiled_run",
side_effect=cancel_during_gateway,
):
result = process_one(client)
assert result.ok is False
assert result.reason == "execution cancelled (signal)"
client.complete.assert_not_called()
client.fail.assert_not_called()
with RepositoryTransaction(repo) as retry:
assert retry.locked is True
def test_profiled_close_failure_happens_after_repository_lock_release(
tmp_path: Path,
) -> None:
repo, _run, client = _profiled_case(tmp_path)
client.complete.side_effect = OpsRunError("complete transport failed")
gateway_result = {"ok": True, "evidence": {"outcome": "succeeded"}}
with patch(
"rein_aharness.claim_loop.execute_profiled_run",
return_value=gateway_result,
):
result = process_one(client)
assert result.ok is False
assert result.reason.startswith("close ops_run failed:")
assert "repository_transaction" in result.detail
with RepositoryTransaction(repo) as retry:
assert retry.locked is True
def test_poll_peek_reports_authoritative_profile_route() -> None:
client = MagicMock(spec=ActivityCoreOpsClient)
run = _claimed_run()

View file

@ -0,0 +1,67 @@
from __future__ import annotations
import json
import subprocess
from datetime import date
from pathlib import Path
from rein_aharness import fi_research_brief
def _repo(tmp_path: Path) -> Path:
repo = tmp_path / "freedom-intelligence"
repo.mkdir()
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
(repo / "README.md").write_text("baseline\n", encoding="utf-8")
subprocess.run(["git", "add", "."], cwd=repo, check=True)
subprocess.run(
[
"git",
"-c",
"user.email=test@example.invalid",
"-c",
"user.name=test",
"commit",
"-qm",
"baseline",
],
cwd=repo,
check=True,
)
return repo
def test_fi_brief_never_pushes_even_when_legacy_env_requests_it(
tmp_path: Path,
monkeypatch,
) -> None:
repo = _repo(tmp_path)
monkeypatch.setenv("FI_RESEARCH_BRIEF_PUSH", "1")
original_git = fi_research_brief._git
calls: list[tuple[str, ...]] = []
def observed_git(target: Path, *args: str) -> str:
calls.append(args)
return original_git(target, *args)
monkeypatch.setattr(fi_research_brief, "_git", observed_git)
result = fi_research_brief.run_fi_research_brief(
repo,
day=date(2026, 9, 4),
report_to_hub=False,
complete_fn=lambda _prompt: json.dumps(
{
"headline_deltas": ["No material delta after allowlist review."],
"axis_a": [],
"axis_b": [],
"axis_c": [],
"axis_d": [],
"collection_candidates": [],
"lab_implications": ["Recheck tomorrow."],
}
),
)
assert result.ok is True
assert result.committed is True
assert not any(args and args[0] == "push" for args in calls)

View file

@ -3,7 +3,14 @@ from __future__ import annotations
import json
from pathlib import Path
from rein_aharness.metrics import record_execution, regenerate_summary
import pytest
from rein_aharness.metrics import (
external_metrics_dir,
record_execution,
record_external_execution,
regenerate_summary,
)
def test_record_execution_writes_jsonl_and_summary(tmp_path: Path) -> None:
@ -49,3 +56,89 @@ def test_summary_aggregates_multiple(tmp_path: Path) -> None:
def test_regenerate_summary_empty() -> None:
assert regenerate_summary("x", [])["execution_count"] == 0
def test_external_metrics_are_durable_and_projection_ready(tmp_path: Path) -> None:
repo = tmp_path / "repo"
repo.mkdir()
state_dir = tmp_path / "state"
path = record_external_execution(
repo,
"coach",
success=True,
committed=True,
head_after="abc123",
metadata={"repository_grant_id": "grant-1"},
session_id="transaction-1",
state_dir=state_dir,
)
directory = external_metrics_dir(repo, "coach", state_dir=state_dir)
assert path == directory / "executions.jsonl"
assert path.stat().st_mode & 0o777 == 0o600
assert directory.stat().st_mode & 0o777 == 0o700
record = json.loads(path.read_text(encoding="utf-8").strip())
assert record["metadata"]["repository_grant_id"] == "grant-1"
assert record["session_id"] == "transaction-1"
projection = json.loads(
(directory / "projection.json").read_text(encoding="utf-8")
)
assert projection["repository_name"] == "repo"
assert projection["target_relative_directory"] == ".kaizen/metrics/coach"
assert not (repo / ".kaizen").exists()
def test_external_metrics_deduplicate_transaction_replay(tmp_path: Path) -> None:
repo = tmp_path / "repo"
repo.mkdir()
state_dir = tmp_path / "state"
for _attempt in range(2):
path = record_external_execution(
repo,
"coach",
success=True,
session_id="transaction-1",
state_dir=state_dir,
)
assert len(path.read_text(encoding="utf-8").splitlines()) == 1
summary = json.loads((path.parent / "summary.json").read_text(encoding="utf-8"))
assert summary["execution_count"] == 1
def test_external_metrics_refuse_an_incomplete_ledger(tmp_path: Path) -> None:
repo = tmp_path / "repo"
repo.mkdir()
state_dir = tmp_path / "state"
directory = external_metrics_dir(repo, "coach", state_dir=state_dir)
directory.mkdir(parents=True)
ledger = directory / "executions.jsonl"
ledger.write_text('{"incomplete":true}', encoding="utf-8")
with pytest.raises(OSError, match="incomplete record"):
record_external_execution(
repo,
"coach",
success=True,
state_dir=state_dir,
)
assert ledger.read_text(encoding="utf-8") == '{"incomplete":true}'
def test_external_metrics_refuse_unsafe_projection_agent(tmp_path: Path) -> None:
repo = tmp_path / "repo"
repo.mkdir()
state_dir = tmp_path / "state"
with pytest.raises(OSError, match="safe metrics projection"):
record_external_execution(
repo,
"../escape",
success=True,
state_dir=state_dir,
)
assert not state_dir.exists()

View file

@ -6,6 +6,7 @@ from pathlib import Path
import pytest
from rein_aharness.metrics import external_metrics_dir
from rein_aharness.repository_grant import RepositoryGrant, RepositoryGrantError
from rein_aharness.repository_transaction import RepositoryTransaction
from rein_aharness.runner import run_task
@ -242,18 +243,129 @@ def test_taskspec_file_wraps_invalid_repository_grant(tmp_path: Path) -> None:
TaskSpec.from_file(task_file)
def test_runner_refuses_grant_before_adapter_dispatch_or_mutation(tmp_path: Path) -> None:
def test_runner_accepts_granted_commit_and_keeps_checkout_clean(tmp_path: Path) -> None:
repo = _make_repo(tmp_path)
class Adapter:
def execute_prompt(self, prompt: str, config: object):
docs = repo / "docs"
docs.mkdir()
(docs / "result.md").write_text("accepted\n", encoding="utf-8")
subprocess.run(["git", "add", "."], cwd=repo, check=True)
subprocess.run(
[
"git",
"-c",
"user.email=test@example.invalid",
"-c",
"user.name=test",
"commit",
"-qm",
"accepted result",
],
cwd=repo,
check=True,
)
from llm_connect.models import LLMResponse
return LLMResponse(
content="done",
model="fake",
usage={},
finish_reason="stop",
)
result = run_task(
TaskSpec(
title="bounded change",
description="update docs",
target_repo=repo,
repository_grant=RepositoryGrant.from_mapping(_grant()),
),
adapter=Adapter(),
report_to_hub=False,
write_metrics=True,
)
assert result.ok is True
assert result.committed is True
assert result.transaction is not None
assert result.transaction["acceptance"]["accepted"] is True
assert result.transaction["acceptance"]["changed_paths"] == ["docs/result.md"]
assert result.transaction["repository_grant"]["grant_id"]
assert result.transaction["metrics"] == {
"storage": "external",
"session_id": result.transaction["transaction_id"],
"projection_ready": True,
}
assert _git(repo, "status", "--porcelain=v2") == ""
assert not (repo / ".kaizen" / "metrics").exists()
metric_path = external_metrics_dir(repo, "coach") / "executions.jsonl"
metric_record = json.loads(metric_path.read_text(encoding="utf-8").strip())
assert metric_record["session_id"] == result.transaction["transaction_id"]
def test_runner_rejects_commit_outside_repository_grant(tmp_path: Path) -> None:
repo = _make_repo(tmp_path)
class Adapter:
def execute_prompt(self, prompt: str, config: object):
(repo / "UNRELATED.md").write_text("not granted\n", encoding="utf-8")
subprocess.run(["git", "add", "."], cwd=repo, check=True)
subprocess.run(
[
"git",
"-c",
"user.email=test@example.invalid",
"-c",
"user.name=test",
"commit",
"-qm",
"ungranted result",
],
cwd=repo,
check=True,
)
from llm_connect.models import LLMResponse
return LLMResponse(
content="done",
model="fake",
usage={},
finish_reason="stop",
)
result = run_task(
TaskSpec(
title="bounded change",
description="update docs",
target_repo=repo,
repository_grant=RepositoryGrant.from_mapping(_grant()),
),
adapter=Adapter(),
report_to_hub=False,
write_metrics=True,
)
assert result.ok is False
assert result.committed is True
assert result.reason.startswith("repository acceptance failed: path-not-granted")
assert result.transaction is not None
assert "acceptance" not in result.transaction
def test_runner_refuses_grant_when_durable_metrics_are_disabled(
tmp_path: Path,
) -> None:
repo = _make_repo(tmp_path)
called = False
class Adapter:
def execute_prompt(self, prompt: str, config: object) -> None:
def execute_prompt(self, prompt: str, config: object):
nonlocal called
called = True
raise AssertionError("adapter must not be dispatched")
head_before = _git(repo, "rev-parse", "HEAD")
status_before = _git(repo, "status", "--porcelain=v2")
result = run_task(
TaskSpec(
title="bounded change",
@ -267,11 +379,69 @@ def test_runner_refuses_grant_before_adapter_dispatch_or_mutation(tmp_path: Path
)
assert result.ok is False
assert result.committed is False
assert result.reason.startswith("refused: repository_grant enforcement")
assert "require durable external metrics" in result.reason
assert called is False
assert _git(repo, "rev-parse", "HEAD") == head_before
assert _git(repo, "status", "--porcelain=v2") == status_before
def test_runner_fails_granted_result_when_external_metrics_cannot_persist(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
repo = _make_repo(tmp_path)
class Adapter:
def execute_prompt(self, prompt: str, config: object):
docs = repo / "docs"
docs.mkdir()
(docs / "result.md").write_text("accepted\n", encoding="utf-8")
subprocess.run(["git", "add", "."], cwd=repo, check=True)
subprocess.run(
[
"git",
"-c",
"user.email=test@example.invalid",
"-c",
"user.name=test",
"commit",
"-qm",
"accepted result",
],
cwd=repo,
check=True,
)
from llm_connect.models import LLMResponse
return LLMResponse(
content="done",
model="fake",
usage={},
finish_reason="stop",
)
def fail_metrics(*args: object, **kwargs: object) -> None:
raise OSError("state volume unavailable")
monkeypatch.setattr(
"rein_aharness.runner.metrics.record_external_execution",
fail_metrics,
)
result = run_task(
TaskSpec(
title="bounded change",
description="update docs",
target_repo=repo,
repository_grant=RepositoryGrant.from_mapping(_grant()),
),
adapter=Adapter(),
report_to_hub=False,
write_metrics=True,
)
assert result.ok is False
assert result.reason == "required external metrics persistence failed (OSError)"
assert result.transaction is not None
assert result.transaction["acceptance"]["accepted"] is True
assert _git(repo, "status", "--porcelain=v2") == ""
def _git(repo: Path, *args: str) -> str:

View file

@ -78,9 +78,15 @@ def test_transaction_captures_clean_branch_and_remote_refs(tmp_path: Path) -> No
def test_dirty_baseline_is_refused_without_changing_user_files(tmp_path: Path) -> None:
repo = _make_repo(tmp_path)
subprocess.run(
["git", "update-ref", "refs/remotes/origin/main", "HEAD"],
cwd=repo,
check=True,
)
changed = repo / "README.md"
changed.write_text("operator change\n", encoding="utf-8")
before = _status(repo)
remote_refs_before = _remote_refs(repo)
with pytest.raises(DirtyRepositoryError) as excinfo:
with RepositoryTransaction(repo, state_dir=tmp_path / "state"):
@ -90,6 +96,7 @@ def test_dirty_baseline_is_refused_without_changing_user_files(tmp_path: Path) -
assert excinfo.value.baseline.dirty_entries == 1
assert changed.read_text(encoding="utf-8") == "operator change\n"
assert _status(repo) == before
assert _remote_refs(repo) == remote_refs_before
# Refusal released the lock; inspection can explicitly opt into dirty state.
with RepositoryTransaction(
@ -469,3 +476,18 @@ def _status(repo: Path) -> str:
capture_output=True,
text=True,
).stdout
def _remote_refs(repo: Path) -> str:
return subprocess.run(
[
"git",
"for-each-ref",
"--format=%(refname) %(objectname)",
"refs/remotes",
],
cwd=repo,
check=True,
capture_output=True,
text=True,
).stdout

View file

@ -8,6 +8,7 @@ import pytest
import yaml
from rein_aharness.manifest import HARNESS_MAJOR
from rein_aharness.repository_transaction import RepositoryTransaction
from rein_aharness.runner import RunResult, run_task
from rein_aharness.taskspec import TaskSpec, TaskSpecError
@ -148,6 +149,26 @@ def test_run_task_records_bounded_cancellation_without_session_output(tmp_path)
assert result.reason == "execution cancelled (lease-loss)"
def test_run_task_timeout_releases_repository_lock(tmp_path) -> None:
repo = _make_repo(tmp_path)
class TimingOutAdapter:
def execute_prompt(self, prompt, config):
raise TimeoutError("adapter deadline elapsed")
result = run_task(
_spec(repo),
adapter=TimingOutAdapter(),
report_to_hub=False,
write_metrics=False,
)
assert result.ok is False
assert result.reason == "session failed: adapter deadline elapsed"
with RepositoryTransaction(repo) as retry:
assert retry.locked is True
def test_run_task_refuses_unknown_tool_profile(tmp_path) -> None:
repo = _make_repo(tmp_path)
_write_manifest(

View file

@ -158,7 +158,7 @@ and T02T06 can cite stable decisions rather than infer ownership from code.
```task
id: REINAH-WP-0003-T02
status: progress
status: done
priority: high
state_hub_task_id: "31da4226-27c0-5e3c-baeb-de01e07cebb1"
```
@ -258,6 +258,22 @@ affected profiled-result and clean-baseline test setup. Full verification passes
`209 passed, 1 skipped`. T02 remains `progress` until the named timeout, signal,
and result-close lock-release cases are covered directly.
### Completion — 2026-09-04
Added direct integration proof that the canonical repository lock is released
after an adapter timeout, Activity Core lease loss, process-signal cancellation,
and a terminal result-close transport failure. The close-failure test also
confirms transaction evidence survives in the bounded process result. Refusal
coverage now proves both the checkout status and local remote-tracking refs are
unchanged. Profiled tests use disposable repositories, and an autouse fixture
keeps runtime state outside operator-owned paths. The documented full-suite
command passes without an environment override: `212 passed, 1 skipped`.
Together with the existing cross-process contention, distinct-repository,
dirty/staged/untracked, detached/moved-HEAD, metadata, remote-ref, bounded
evidence, and lease-cancellation cases, all T02 exit criteria are met. T02 is
`done`; T03 is unblocked.
## Verify accepted commits and reconcile metrics/reporting
```task
@ -371,11 +387,41 @@ terminal evidence, and expired-lease behavior in message
`1c13bfb1-9015-4b8a-8b30-ee748c1d8dc5`. No claim-loop or deployment change
was requested.
### Local grant activation and metrics atomicity — 2026-09-04
Enabled the reviewed v1 `repository_grant` for local `TaskSpec` files now that
T02 supplies the lease-bound transaction. Successful granted runs validate the
exact commit count/ancestry, changed paths, clean post-state, protected Git
metadata, and remote-tracking refs before reporting success. Their bounded
transaction evidence includes grant and acceptance identities without raw
grant paths, prompts, or provider output. Adversarial runner coverage rejects
out-of-grant commits.
Chose the durable-external metrics branch of ADR-002's T03 decision. Granted
runs cannot use `--no-metrics`; they atomically append a kaizen-compatible
record under private `REIN_AHARNESS_STATE_DIR` storage and regenerate a summary
plus projection descriptor without touching the accepted checkout. Ledger
corruption or persistence failure is terminal for the granted result. A future
kaizen-owned, separately granted projection may commit those files to the
instance; the rein does not invalidate its own accepted post-state. Legacy
grant-absent runs retain repository-local metrics during migration.
Full verification passes: `222 passed, 1 skipped`. T03 remains `wait`, not
`done`, on two verified Activity Core contract gaps at current main `b72fdb5`:
the queue schema carries no authoritative repository grant, and complete/fail
accept only an actively leased claimed row while normalized result storage
drops transaction/grant/acceptance identity. A response-lost retry therefore
returns an ambiguous 409 and cannot be safely treated as delivered. Requested
a separately typed grant field plus identity-preserving, conflict-detecting
terminal reconciliation in State Hub message
`6c4189a5-5971-4eb9-a7bd-62fe368f9a02`. Live outbox activation stays
fail-closed until that exact upstream contract is available.
## Remove tenant logic from the shared runtime
```task
id: REINAH-WP-0003-T04
status: todo
status: wait
priority: high
state_hub_task_id: "f6c7f80c-3807-54ac-b44a-ef582433b927"
```
@ -407,11 +453,36 @@ and owner files. It identifies inputs, outputs, credentials, completion events,
commit/push behavior, retry posture, rollback material, and migration gates.
This is read-only preparation; T04 remains `wait` until T01 accepts ADR-002.
### Dated compatibility guard — 2026-09-04
Profile-absent `ops_run` dispatch now requires an explicit, non-expired ISO
date in `AGENT_HARNESS_LEGACY_APPROACHES_UNTIL`. Missing, malformed, or expired
values refuse terminally before target resolution or tenant executor dispatch.
Authoritative `harness_profile_ref` rows bypass the legacy registry and this
flag entirely. The deployment example currently declares `2026-12-31`, making
the remaining four tenant migrations visible and time-bounded rather than a
silent permanent default. Focused tests cover boundary-day, expired, malformed,
missing, and pre-dispatch refusal behavior.
Removed the FI compatibility executor's `git push origin HEAD` path entirely.
`FI_RESEARCH_BRIEF_PUSH` no longer widens behavior even when set; the executor
creates a local commit only. A regression test observes every Git invocation
and proves no push occurs. Any publication now requires a separate owner,
grant, and evidence contract.
T04 remains `wait`: the FI and Binky definition owners still need to move
their four scheduled behaviors to approved profiles/tenant-owned capabilities
before the compatibility flag and tenant modules can be removed. Requests:
Freedom Intelligence `d0b45acb-0002-405b-9620-cc44568170fb`, Binky
`c941f47b-d100-4df3-b4f4-feb1e187d2d8`, kaizen-agentic
`29a010f4-7a5c-42cd-a13e-3a13326f75ff`, and Glas
`e91f3f67-2d5d-4ad2-923c-431ebafed77e`.
## Align deployment, recovery, and conformance with the supported runtime
```task
id: REINAH-WP-0003-T05
status: todo
status: progress
priority: high
state_hub_task_id: "c4a3f08f-2874-5672-9f29-aecddd697d90"
```
@ -441,6 +512,20 @@ runtime boundary is unavailable; recovery drills leave no held repo lock,
claimed row, orphan sandbox, or lost required evidence; and CI/release gates
exercise the same contract versions as production.
### Supported topology declaration — 2026-09-04
Selected the existing railiance01 user systemd service as the authoritative
production topology. It is the only runtime that owns real checkouts, private
state, worker credentials, heartbeats, and signal shutdown. The Kubernetes
Deployment is now explicitly labeled `packaging-smoke`; its source comments and
deployment guide state that `sleep infinity` is neither ready to claim nor a
failover worker and must never receive the Activity Core worker credential.
T05 remains `progress`. Reproducible pinned sibling packages, startup readiness,
recovery/outbox controls, and non-skipped cross-package CI still need
implementation; the currently catalogued local Glas profiles are also
operationally `blocked` on `GLAS-IN-0002`.
## Re-prove one governed profiled run and close residuals
```task