activity-core/workplans/ACTIVITY-WP-0032-glas-profile-execution-contract.md
tegwick 202e5048d4
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Record post-fix Glas pilot blocker
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a028de-e2c8-7732-8521-46a7fc5db82f
2026-08-23 01:52:32 +02:00

284 lines
13 KiB
Markdown

---
id: ACTIVITY-WP-0032
type: workplan
title: "Adopt the Glas profile-driven execution contract"
domain: infotech
repo: activity-core
status: active
owner: claude
topic_slug: activity-core
priority: medium
created: "2026-08-21"
updated: "2026-08-23"
related:
- ACT-ADR-006
- ACTIVITY-WP-0026
- ACTIVITY-WP-0029
- ACTIVITY-WP-0031
- ACT-ADR-005
- GLAS-WP-0004
- REIN-A-0002
state_hub_workstream_id: "256dad13-28b4-5361-ab8a-7d1373a5d14b"
---
# 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).
## Answered by glas-harness (2026-08-21)
Both questions are settled; recorded as **ACT-ADR-006**, hub decision
`147beec6-7fe7-4837-8e3a-4264a240379d`.
1. **Coexistence with distinct semantics.** `harness_profile_ref` is the
authoritative execution-constellation selector. `approach_hint` stays only as
a legacy activity/definition-matching hint and must **not** override,
synthesize, or fall back from an absent/invalid profile ref on governed
execution. Deprecate that use once inventory shows no caller depends on it.
2. **The glas-harness catalog is authoritative — do not mirror it.** Glas
exposes deterministic validation through its package and CLI but **no network
validation service** today. Emit-time remote validation would need a
separately scoped Glas API. The execution-side Glas resolver is the mandatory
fail-closed check.
Transport: **preserve the pull queue** in the first migration slice. The exact
versioned profile ref and attribution refs go in the queued payload; the
claiming executor passes the request into Glas, which resolves or refuses before
sandbox creation. This changes the execution contract without also changing
scheduling topology. A direct activity-core → Glas call can be evaluated
independently later.
## Decide the invocation shape
```task
id: ACTIVITY-WP-0032-T01
status: done
priority: high
state_hub_task_id: "a67a896d-ca3f-59c7-b11c-47163505ccd6"
```
Resolved 2026-08-21 as **option A — the queue carries the profile**, on the
advice of glas-harness as contract owner. `ops_run` grows `harness_profile_ref`
plus attribution refs; lease semantics, durability, and the operator console are
unchanged; activity-core stays behind the executor boundary.
Option B (activity-core calls the Glas gateway directly) was rejected for this
slice: it would discard the claim/lease/retry machinery ACTIVITY-WP-0026 just
built and change scheduling topology at the same time as the execution contract.
It remains available as an independent later evaluation.
Written up as `docs/adr/adr-006-glas-profile-execution.md`.
## Carry the profile and attribution refs
```task
id: ACTIVITY-WP-0032-T02
status: done
priority: high
state_hub_task_id: "0b0a5a93-9bd3-5b8a-8f98-97bebf7cc073"
```
Done 2026-08-21. `ops_runs` grows `harness_profile_ref` (text, indexed) and
`execution_refs` (jsonb, default `{}`), migration `0008`, threaded through
`ops_run_queue.py`, `activities.py`, and `run_artifacts.py`.
`src/activity_core/glas_profile.py` holds the selection rules;
`docs/ops-run-queue.md` documents the contract.
The no-fallback rule is enforced in `resolve_execution_selector`, not merely
documented: a malformed ref raises even when a hint is present, and
`ACTIVITY_CORE_REQUIRE_HARNESS_PROFILE` makes an absent one an error once
definitions have migrated. 34 tests, with the `TestNoFallbackToApproachHint`
class covering the property directly.
Migration verified against a real PostgreSQL 16: upgrade → columns and index
present → downgrade 0007 → clean re-upgrade. A legacy-shaped row (approach_hint
only, no profile) still inserts, defaults `execution_refs` to `{}`, and stays
`open`/claimable — existing production runs are unaffected.
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. Per ACT-ADR-006 the two
fields **coexist with distinct semantics**: enforce in code that `approach_hint`
cannot override, synthesize, or fall back from an absent/invalid
`harness_profile_ref` on governed execution, and cover that with a test — a
silent fallback is exactly the failure this workplan removes.
## Validate what we can, fail closed on the rest
```task
id: ACTIVITY-WP-0032-T03
status: done
priority: medium
state_hub_task_id: "50225bac-1798-5cf5-91ad-d4c662de7fc7"
```
Depends on T02. **Corrected after the glas-harness answer:** emit-time *remote*
validation is not available — Glas has no network validation service, and
mirroring its catalogue is explicitly forbidden. So:
- Validate locally and structurally at definition sync and run emission: a
governed run has a profile ref, and it is well-formed and versioned.
- Do **not** resolve the ref against a mirrored list. The execution-side Glas
resolver is the authority and the mandatory fail-closed check.
- An absent or malformed ref is an error at emission. It must never degrade into
"emit anything" or fall back to `approach_hint`.
Record the residual gap honestly: a well-formed but unknown profile is still
caught at execution rather than emission. That is weaker than refusing at emit,
and still strictly better than the claim-time failure of 2026-08-17, because
Glas refuses before sandbox creation. If emit-time remote validation becomes
necessary, raise it as a capability request against glas-harness rather than
solving it locally.
Done 2026-08-22. Definition parsing now validates every declared rule or
instruction profile structurally and normalizes its version-pinned ref before
DB sync. With `ACTIVITY_CORE_REQUIRE_HARNESS_PROFILE=true`, sync refuses
task-emitting declarations that omit a profile; deterministic report-only
instructions remain exempt. Rule and instruction expansion carry the declared
profile, legacy hint, and allowlisted attribution refs into the queued request.
Emission now preflights the complete task batch before opening either the DB or
IssueSink. A missing profile in strict mode or any malformed profile becomes a
non-retryable activity error, so a later invalid item cannot leave earlier
items partially emitted and `approach_hint` cannot rescue it. Tests also prove
that an unknown but structurally valid versioned ref is preserved for the
authoritative execution-side Glas resolver; no local profile catalogue was
introduced. Verification: 59 focused tests and 426 repository tests passed;
the one live NATS-to-Temporal bridge test was deselected because it requires
the local integration stack. Python compilation and `git diff --check` passed.
## Record normalized execution evidence
```task
id: ACTIVITY-WP-0032-T04
status: done
priority: medium
state_hub_task_id: "48ff9bca-6251-59ff-8b6f-6b0d79252624"
```
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.
Done 2026-08-22 against Glas contract 1.0. Both queue completion and failure
normalize the returned `GatewayResult` before persistence. Activity Core keeps
the declared `ExecutionEvidence` constellation, outcome/failure stage, bounded
measurements, commit/artifact references, and allowlisted organizational refs
under `ops_runs.result.execution_evidence`. It preserves meaningful zero values
and explicit `unavailable`, while dropping direct `tool_output`, `tool_error`,
prompts, messages, provider responses, unknown nested blobs, and undeclared
refs. Historic results are normalized again at read time.
The per-run API/UI projection now includes `execution_evidence`, so operators
can see profile, rein, resolved model, sandbox, outcome, duration, tokens, and
artifact facts without provider payloads. Tests cover success, failure,
redaction, invalid enums/measurements, unknown-versus-zero semantics, legacy
artifact compatibility, and the run summary. Verification: 71 focused tests
and 433 repository tests passed; the live NATS-to-Temporal bridge test remained
deselected because it requires the local integration stack.
## Prove on one definition
```task
id: ACTIVITY-WP-0032-T05
status: wait
priority: low
state_hub_task_id: "e71423f9-8baf-566b-8a70-eec195e2f65f"
```
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.
Resumed 2026-08-23. Rein-aharness source `0f01c3e` is deployed on railiance01:
profiled rows now preserve `harness_profile_ref` / `execution_refs`, route
through Glas before legacy selection, refuse without fallback, and return the
full `GatewayResult`. Handoff confirmation
`413a4041-cfb3-4d02-a41a-1d92b3de06af` also identified production schema
drift: the live image expects migration `0008`, while PostgreSQL was still at
`0007`, making `/ops-runs/claim` fail with `UndefinedColumnError`. Apply and
verify the additive migration before scheduling the pilot.
Live progress 2026-08-23 is recorded in
`docs/evidence/ACTIVITY-WP-0032-glas-profile-pilot-2026-08-23.md`. Migration,
current image rollout, definition sync, selector/refs, profile resolution, and
normalized failure evidence are proven. Installing the missing Ubuntu
`bubblewrap` package enabled a direct create/destroy preflight.
Rein-aharness then fixed and deployed the governance mapping in source
`c633291`: the queue claim owner remains `rein-aharness@railiance01`, while the
Glas `ExecutionRequest.actor` is now `agt`. Rein ran the exact disabled pilot
once after that deployment. Ops run `ededc939-266f-473c-8386-ffd3f027f5f0`
preserved `harness.agent-dev-local@1.0.0` and both T05 refs, resolved the
rein/model constellation, created sandbox `d750cd5c`, and failed closed at
`session_start` before provider dispatch. The sandbox reached `destroyed`, its
workspace is absent, and no artifact or commit was produced. This resolves the
actor mismatch without weakening queue ownership identity.
Returned to wait on upstream `GLAS-IN-0002`: the managed consumer is denied
`nsenter`, and the rein/model runtime and egress contract is not yet available
inside the sandbox. Resume after that contract is implemented; do not trigger
another pilot until the upstream blocker changes.
## Acceptance
- [x] Invocation shape decided and recorded as an ADR, with the boundary stated
(ACT-ADR-006; pull queue preserved)
- [x] ops_run carries an approved `harness_profile_ref`; no definition names a
concrete rein
- [x] Malformed/absent profile refs are refused at emission; unknown-but-well-formed
refs are refused by Glas before sandbox creation, never at claim
- [x] `approach_hint` cannot override or substitute for a profile ref, proven by test
- [x] Normalized Glas evidence is visible in production status
- [ ] One definition proven on railiance01 end to end