feat: guard prod status since-arg; plan Glas contract adoption
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Successful in 5s

scripts/prod_automation_status.sh took a positional since value, so the
Makefile's own `SINCE=sunday` form passed the literal string into timestamptz:
all five sections errored while the run still looked like it produced a report.
Now `SINCE=` is accepted and the value is parsed and normalised to UTC up
front, failing fast with usage and exit 2 instead of five parse errors.

ACTIVITY-WP-0032 plans adoption of the glas-harness contract 1.0 reported in
GLAS-WP-0004. WP-0026 is finished, so this gets its own plan. The motivation is
concrete: ops_run.approach_hint binds at claim time, which produced a failed
run on 2026-08-17 ("no approach matched labels/definition") after it had
already consumed a claim and a lease.

T01 is deliberately blocking: our claim path is pull-based and the Glas
contract is a call, so the invocation shape is an architectural decision, not a
port. Two questions are outstanding with glas-harness.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-21 09:05:04 +02:00
parent 459a272974
commit 17f2caee01
2 changed files with 210 additions and 1 deletions

View file

@ -9,6 +9,22 @@ SINCE_ARG="${1:-sunday}"
SSH_HOST="${PROD_AUTOMATION_SSH_HOST:-railiance01}" SSH_HOST="${PROD_AUTOMATION_SSH_HOST:-railiance01}"
NS="${PROD_AUTOMATION_NS:-activity-core}" NS="${PROD_AUTOMATION_NS:-activity-core}"
# The Makefile target is `make prod-automation-status SINCE=sunday`, so operators
# reasonably type the same `SINCE=` form when calling the script directly. Accept
# it rather than passing the literal into timestamptz, where every section fails
# with a separate parse error while the run still looks like it produced a report.
if [[ "$SINCE_ARG" == SINCE=* ]]; then
SINCE_ARG="${SINCE_ARG#SINCE=}"
fi
usage() {
cat >&2 <<'USAGE'
Usage: ./scripts/prod_automation_status.sh [since]
since: "sunday" (default) | an ISO-8601 timestamp, e.g. 2026-08-17T00:00:00Z
Read-only. Requires SSH to the production host.
USAGE
}
if [[ "$SINCE_ARG" == "sunday" ]]; then if [[ "$SINCE_ARG" == "sunday" ]]; then
# Floor of most recent Sunday 00:00 Europe/Berlin in UTC (portable enough for ops). # Floor of most recent Sunday 00:00 Europe/Berlin in UTC (portable enough for ops).
SINCE_UTC="$(python3 - <<'PY' SINCE_UTC="$(python3 - <<'PY'
@ -22,7 +38,27 @@ print(sunday.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S%z"))
PY PY
)" )"
else else
SINCE_UTC="$SINCE_ARG" # Validate before the value reaches five separate SQL/JSON consumers: an
# unparseable `since` must fail fast and loudly, not produce a report whose
# every section is an error the reader has to notice.
if ! SINCE_UTC="$(python3 - "$SINCE_ARG" <<'PYVALIDATE'
import sys
from datetime import datetime, timezone
raw = sys.argv[1].strip()
try:
parsed = datetime.fromisoformat(raw.replace("Z", "+00:00"))
except ValueError:
sys.exit(1)
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=timezone.utc)
print(parsed.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S%z"))
PYVALIDATE
)"; then
echo "error: could not parse since value: ${SINCE_ARG}" >&2
usage
exit 2
fi
fi fi
echo "=== prod automation status (railiance01) since ${SINCE_UTC} ===" echo "=== prod automation status (railiance01) since ${SINCE_UTC} ==="

View file

@ -0,0 +1,173 @@
---
id: ACTIVITY-WP-0032
type: workplan
title: "Adopt the Glas profile-driven execution contract"
domain: infotech
repo: activity-core
status: proposed
owner: claude
topic_slug: activity-core
priority: medium
created: "2026-08-21"
updated: "2026-08-21"
related:
- ACTIVITY-WP-0026
- ACTIVITY-WP-0029
- ACTIVITY-WP-0031
- ACT-ADR-005
- GLAS-WP-0004
- REIN-A-0002
---
# Adopt the Glas profile-driven execution contract
## Origin
`glas-harness` reported (2026-08-20) that GLAS-WP-0004 contract 1.0 is
implemented and proven: the same bounded task completed through **two**
different reins (`rein-aharness`, `rein-openweights`) via an explicit
`harness_profile_ref`, with real commits and verified sandbox destruction.
Their guidance to us: activity-core stays scheduler and task source, and should
name an **approved profile**, never a concrete rein.
ACTIVITY-WP-0026 is `finished`, so this does not belong as an amendment to it.
## Why this is worth doing
Today an ops_run carries `approach_hint` (`orm.py:172`), a free-text string the
executor interprets at claim time. That binding is too late and too weak. Live
example from railiance01, 2026-08-17:
```
state=failed attempt=1 claim_owner=rein-aharness@railiance01
title=Run SBOM rescan for binect-js
failure=no approach matched labels/definition;
labels=['sbom','security','automated'] def='2969816c-…'
```
The run was claimed, then died because nothing could execute it. A
`harness_profile_ref` is resolved and **refused before sandbox creation**, so
an unroutable request fails at emission with a clear reason instead of
consuming a claim and a lease.
Second motivation: we are currently coupled to one backend in practice —
every claim in the last week is `rein-aharness@railiance01`. The dual-rein
proof means that coupling is now avoidable rather than inherent.
## Boundary (must not drift)
activity-core stays **when / what / where**. Adopting this contract must not
turn it into an executor — SCOPE drift risk #1 ("convenience execution"). We
emit an authorized, profile-named request and record normalized evidence. We do
not select reins, provision sandboxes, acquire credentials, or run the inner
agentic loop. Glas owns profile resolution and the outer loop; the rein owns its
own inner loop; `sand-boxer` owns isolation.
We also do not author the attribution refs the contract accepts
(`assignment_ref`, `role_ref`, `duty_ref`, `goal_refs`,
`resource_envelope_refs`). Those come from workforce/leadership vocabulary in
`info-tech-canon`; activity-core carries them through, and must not invent org
roles here (ACTIVITY-WP-0029 responsibility map).
## Open questions (asked of glas-harness 2026-08-21, not yet answered)
1. Is `approach_hint` expected to be **replaced** by `harness_profile_ref`, or
to coexist?
2. Is the profile registry authoritative in glas-harness, so we validate refs by
calling them rather than mirroring a catalogue that can drift?
T01 is blocked on these; the rest follows the answer.
## Decide the invocation shape
```task
id: ACTIVITY-WP-0032-T01
status: wait
priority: high
```
Our claim path is **pull-based**: we insert `ops_run(open)` and
rein-aharness claims it. The Glas contract is shaped as a **call**. Those do not
compose automatically, and the choice is architectural, not mechanical. Record
a decision (ACT-ADR) between:
- **A — queue carries the profile.** `ops_run` grows `harness_profile_ref` plus
the attribution refs; a Glas-aware claimer resolves the profile. Keeps lease
semantics, durability, and the operator console unchanged. Keeps one more
indirection between authorization and execution.
- **B — activity-core calls the Glas gateway.** Direct invocation with the
approved profile. Matches the contract as designed and fails unroutable
requests before sandbox creation. Costs us the claim/lease/retry machinery
ACTIVITY-WP-0026 just built, and moves activity-core closer to the executor
boundary we deliberately hold.
Do not start T02+ before this is resolved. Prefer A unless glas-harness states
the profile must be resolved at emission time to keep its refusal guarantee.
## Carry the profile and attribution refs
```task
id: ACTIVITY-WP-0032-T02
status: wait
priority: medium
```
Depends on T01. Add `harness_profile_ref` and the correlation/attribution refs
to the ops_run model, emission path (`activities.py`), queue projection
(`ops_run_queue.py`), and run artefacts (`run_artifacts.py`), with a migration.
Definitions declare the profile; rules pass it through. Decide `approach_hint`'s
fate per open question 1 — if it coexists, document which wins; if it is
replaced, strangle it rather than leaving two routing fields.
## Validate refs at emission
```task
id: ACTIVITY-WP-0032-T03
status: wait
priority: medium
```
Depends on T01/T02. An unknown or incompatible profile must be refused when the
ActivityDefinition is synced or the run is emitted — not discovered at claim
time, which is the failure mode this workplan exists to remove. Per open
question 2, prefer calling the glas-harness registry over mirroring it. Include
the offline/unreachable behaviour: a registry we cannot reach must not silently
downgrade to "emit anything".
## Record normalized execution evidence
```task
id: ACTIVITY-WP-0032-T04
status: wait
priority: low
```
Depends on T02. Glas emits normalized, non-secret execution evidence. Land it in
`ops_runs.result` and the evidence sink so the production status surface
(ACTIVITY-WP-0031-T03) shows what actually executed, through which profile and
rein. Keep the field allowlist discipline from ACTIVITY-WP-0031: no provider
blobs, no credential material in run artefacts.
## Prove on one definition
```task
id: ACTIVITY-WP-0032-T05
status: wait
priority: low
```
Depends on T03/T04. Convert exactly one low-risk definition, run it on
railiance01, and capture evidence. Do not convert the fleet before one
definition is proven end to end. Note that FI and Binky are unsuitable as the
pilot while ACTIVITY-WP-0031-T01 is unresolved — their failures are currently
provider-credential failures and would mask the result.
## Acceptance
- [ ] Invocation shape decided and recorded as an ADR, with the boundary stated
- [ ] ops_run carries an approved `harness_profile_ref`; no definition names a
concrete rein
- [ ] Unroutable profiles are refused at emission, not at claim
- [ ] `approach_hint` is either strangled or documented as subordinate
- [ ] Normalized Glas evidence is visible in production status
- [ ] One definition proven on railiance01 end to end