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:
parent
e3c6124e22
commit
20e6f381f6
28 changed files with 1068 additions and 154 deletions
|
|
@ -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`
|
||||
|
|
|
|||
34
SCOPE.md
34
SCOPE.md
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,21 @@
|
|||
Single shared harness instance on **railiance01**. Secrets stay on the host
|
||||
(Lanes 2–3); 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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
36
docs/external-metrics.md
Normal 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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
12
tests/conftest.py
Normal 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")
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
67
tests/test_fi_research_brief.py
Normal file
67
tests/test_fi_research_brief.py
Normal 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)
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ and T02–T06 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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue