diff --git a/scripts/prod_automation_status.sh b/scripts/prod_automation_status.sh index c420231..9249251 100755 --- a/scripts/prod_automation_status.sh +++ b/scripts/prod_automation_status.sh @@ -9,6 +9,22 @@ SINCE_ARG="${1:-sunday}" SSH_HOST="${PROD_AUTOMATION_SSH_HOST:-railiance01}" 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 # Floor of most recent Sunday 00:00 Europe/Berlin in UTC (portable enough for ops). SINCE_UTC="$(python3 - <<'PY' @@ -22,7 +38,27 @@ print(sunday.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S%z")) PY )" 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 echo "=== prod automation status (railiance01) since ${SINCE_UTC} ===" diff --git a/workplans/ACTIVITY-WP-0032-glas-profile-execution-contract.md b/workplans/ACTIVITY-WP-0032-glas-profile-execution-contract.md new file mode 100644 index 0000000..5d74866 --- /dev/null +++ b/workplans/ACTIVITY-WP-0032-glas-profile-execution-contract.md @@ -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