From 59f7b9e5f8402d5f54e2465ed22df3b3a0f75121 Mon Sep 17 00:00:00 2001 From: Paul O'Reilly Date: Fri, 18 Sep 2026 12:28:06 +1200 Subject: [PATCH] feat(forge-credential): add gitea-ssh-hatchmate/v1 layer + 4 hatchmate composite forks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M2-CRS W4 unblock: dispatched agents could not clone hatchmate/hatchmate-agents because the shared gitea-ssh/v1 context mounts the ai_enablement deploy key, which is scoped to skynet/* only. Same bug would hit every project whose working repo lives outside the skynet org. This implements the per-org pattern the framework already uses for accelerators (gitea-ssh-accelerators/v1) and homelab (gitea-ssh-homelab/v1): - harnesses/contexts/gitea-ssh-hatchmate/v1/{init.sh,harness.yaml} — NEW forge-credential context mirroring accelerators, with account_ref 'hatchmate.space:platform' and write_targets covering hatchmate/*. The init.sh stages the ESO-mounted private key to ~/.ssh/gitea-oreillyit-nz-hatchmate (matching the ssh alias declared in harness.yaml). URL host pattern 'gitea.oreillyit.nz-hatchmate' (per-org alias) drives SSH config block selection — already auto-written by harness_init.py from each layer's ssh_hosts: declarations. - harnesses/composites/code-*-tdd-repo-hatchmate/v1/ — NEW composites for fable/minimax/airouter/opus that compose BOTH gitea-ssh/v1 (for reference clones, skynet org) AND gitea-ssh-hatchmate/v1 (for working-repo clone + push, hatchmate org). Composites are unchanged for non-hatchmate projects. - tests/invariants/test_forge_credentials_declare_write_targets.py — the existing xfail(strict=True) marker was wrong: the 4 existing layers all declare write_targets today, so the test was XPASSing. Removed the xfail marker so HC-11 is now a real positive gate (5 layers instead of 4). This was a pre-existing test bug surfaced by the 5th layer. W4 next step: dispatcher's account_ref resolution needs the hatchmate CP account + grant to be created (via the new scripts/provision-hatchmate-fork-key.sh in the hatchmate app repo). --- .../v1/harness.yaml | 20 +++++++++++++ .../v1/harness.yaml | 14 ++++++++++ .../v1/harness.yaml | 15 ++++++++++ .../v1/harness.yaml | 15 ++++++++++ .../gitea-ssh-hatchmate/v1/harness.yaml | 28 +++++++++++++++++++ .../contexts/gitea-ssh-hatchmate/v1/init.sh | 21 ++++++++++++++ ...forge_credentials_declare_write_targets.py | 15 ++++++---- 7 files changed, 123 insertions(+), 5 deletions(-) create mode 100644 harnesses/composites/code-airouter-tdd-repo-hatchmate/v1/harness.yaml create mode 100644 harnesses/composites/code-fable-tdd-repo-hatchmate/v1/harness.yaml create mode 100644 harnesses/composites/code-minimax-tdd-repo-hatchmate/v1/harness.yaml create mode 100644 harnesses/composites/code-opus-tdd-repo-hatchmate/v1/harness.yaml create mode 100644 harnesses/contexts/gitea-ssh-hatchmate/v1/harness.yaml create mode 100755 harnesses/contexts/gitea-ssh-hatchmate/v1/init.sh diff --git a/harnesses/composites/code-airouter-tdd-repo-hatchmate/v1/harness.yaml b/harnesses/composites/code-airouter-tdd-repo-hatchmate/v1/harness.yaml new file mode 100644 index 0000000..45f36d2 --- /dev/null +++ b/harnesses/composites/code-airouter-tdd-repo-hatchmate/v1/harness.yaml @@ -0,0 +1,20 @@ +kind: composite +name: code-airouter-tdd-repo-hatchmate +version: 1 +description: "Airouter Qwen3.6 code agent (hatchmate fork) — adds gitea-ssh-hatchmate for write access to hatchmate/hatchmate-agents. Use this for hatchmate work; the un-suffixed composite uses the shared ai-enablement key which has no access to the hatchmate org." + +# Inherits airouter context's label gate — only the airouter dispatcher +# advertises this composite (see dispatcher/poller._collect_supported_harnesses). +requires_labels: [airouter] + +layers: + - context: qwen-code-methodology/v1 + - context: best-practices/v1 + - context: airouter/v1 + - context: gitea-ssh/v1 + - context: gitea-ssh-hatchmate/v1 + - context: agent-repo/v1 + - context: tdd-protect/v1 + - context: tdd-gate/v1 + - capability: python-dev/v1 + - capability: tdd-file-lock/v1 diff --git a/harnesses/composites/code-fable-tdd-repo-hatchmate/v1/harness.yaml b/harnesses/composites/code-fable-tdd-repo-hatchmate/v1/harness.yaml new file mode 100644 index 0000000..2cc4184 --- /dev/null +++ b/harnesses/composites/code-fable-tdd-repo-hatchmate/v1/harness.yaml @@ -0,0 +1,14 @@ +kind: composite +name: code-fable-tdd-repo-hatchmate +version: 1 +description: "Fable code agent (hatchmate fork) — adds gitea-ssh-hatchmate for write access to hatchmate/hatchmate-agents. Use this for hatchmate work; the un-suffixed composite uses the shared ai-enablement key which has no access to the hatchmate org." +layers: + - context: code-methodology/v1 + - context: anthropic-cloud-paul-oauth/v1 + - context: gitea-ssh/v1 + - context: gitea-ssh-hatchmate/v1 + - context: agent-repo/v1 + - context: tdd-protect/v1 + - context: tdd-gate/v1 + - capability: python-dev/v1 + - capability: tdd-file-lock/v1 diff --git a/harnesses/composites/code-minimax-tdd-repo-hatchmate/v1/harness.yaml b/harnesses/composites/code-minimax-tdd-repo-hatchmate/v1/harness.yaml new file mode 100644 index 0000000..d540d73 --- /dev/null +++ b/harnesses/composites/code-minimax-tdd-repo-hatchmate/v1/harness.yaml @@ -0,0 +1,15 @@ +kind: composite +name: code-minimax-tdd-repo-hatchmate +version: 1 +description: "MiniMax code agent (hatchmate fork) — adds gitea-ssh-hatchmate for write access to hatchmate/hatchmate-agents. Use this for hatchmate work; the un-suffixed composite uses the shared ai-enablement key which has no access to the hatchmate org." + +layers: + - context: minimax-code-methodology/v1 + - context: minimax/v1 + - context: gitea-ssh/v1 + - context: gitea-ssh-hatchmate/v1 + - context: agent-repo/v1 + - context: tdd-protect/v1 + - context: tdd-gate/v1 + - capability: python-dev/v1 + - capability: tdd-file-lock/v1 diff --git a/harnesses/composites/code-opus-tdd-repo-hatchmate/v1/harness.yaml b/harnesses/composites/code-opus-tdd-repo-hatchmate/v1/harness.yaml new file mode 100644 index 0000000..1c263f8 --- /dev/null +++ b/harnesses/composites/code-opus-tdd-repo-hatchmate/v1/harness.yaml @@ -0,0 +1,15 @@ +kind: composite +name: code-opus-tdd-repo-hatchmate +version: 1 +description: "Opus code agent (hatchmate fork) — adds gitea-ssh-hatchmate for write access to hatchmate/hatchmate-agents. Use this for hatchmate work; the un-suffixed composite uses the shared ai-enablement key which has no access to the hatchmate org." + +layers: + - context: code-methodology/v1 + - context: anthropic-cloud-paul-oauth/v1 + - context: gitea-ssh/v1 + - context: gitea-ssh-hatchmate/v1 + - context: agent-repo/v1 + - context: tdd-protect/v1 + - context: tdd-gate/v1 + - capability: python-dev/v1 + - capability: tdd-file-lock/v1 diff --git a/harnesses/contexts/gitea-ssh-hatchmate/v1/harness.yaml b/harnesses/contexts/gitea-ssh-hatchmate/v1/harness.yaml new file mode 100644 index 0000000..347c754 --- /dev/null +++ b/harnesses/contexts/gitea-ssh-hatchmate/v1/harness.yaml @@ -0,0 +1,28 @@ +kind: context +name: gitea-ssh-hatchmate +version: 1 +description: "Gitea SSH: clone/push via SSH key for gitea.oreillyit.nz (hatchmate org — agent working-repo key for hatchmate/hatchmate-agents)" +requires: [] +provides: [git-access] +# HC-11: repos this layer's forge credential may push to (host-qualified; CRS-103 gate). +write_targets: + - "gitea.oreillyit.nz/hatchmate/*" + +git_identity: + name: "agent-runtimes" + email: "agent@oreillyit.nz" + +ssh_hosts: + - alias: gitea.oreillyit.nz-hatchmate + hostname: gitea.oreillyit.nz + user: git + identity_secret: GITEA_SSH_KEY + +scripts: + init: "./init.sh" + +secrets_required: + - name: gitea-ssh-hatchmate + account_ref: "hatchmate.space:platform" + mount_path: /run/agent/secrets/gitea-ssh-hatchmate + mode: "0400" diff --git a/harnesses/contexts/gitea-ssh-hatchmate/v1/init.sh b/harnesses/contexts/gitea-ssh-hatchmate/v1/init.sh new file mode 100755 index 0000000..f73a85c --- /dev/null +++ b/harnesses/contexts/gitea-ssh-hatchmate/v1/init.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# gitea-ssh-hatchmate init: copy the ESO-mounted SSH key to the path the +# harness-init `~/.ssh/config` expects (`IdentityFile /home/agent/.ssh/gitea-oreillyit-nz-hatchmate`). +# +# Per H-SECRET-4: NO `export` of credentials. (The earlier draft set +# `GIT_SSH_COMMAND` here — a path, not a credential — but that export dies +# with the subshell and the harness-init's SSH config is the load-bearing +# path anyway.) +set -euo pipefail + +SSH_KEY_SRC="/run/agent/secrets/gitea-ssh-hatchmate/private_key" +SSH_KEY_DST="/home/agent/.ssh/gitea-oreillyit-nz-hatchmate" + +if [ ! -r "$SSH_KEY_SRC" ]; then + echo "ERROR: SSH key not found at $SSH_KEY_SRC — ESO ExternalSecret not Ready?" >&2 + exit 1 +fi + +mkdir -p /home/agent/.ssh +install -m 0600 -o agent -g agent "$SSH_KEY_SRC" "$SSH_KEY_DST" +echo "gitea-ssh-hatchmate: SSH key staged at $SSH_KEY_DST (0600 agent:agent)" diff --git a/tests/invariants/test_forge_credentials_declare_write_targets.py b/tests/invariants/test_forge_credentials_declare_write_targets.py index 4a3ea8f..7913c70 100644 --- a/tests/invariants/test_forge_credentials_declare_write_targets.py +++ b/tests/invariants/test_forge_credentials_declare_write_targets.py @@ -34,8 +34,17 @@ def test_hc11_framework_layers_with_forge_secrets_found_nonzero(): assert found, "expected >=1 forge-credential layer on the real tree, found 0" -@pytest.mark.xfail(strict=True, reason="HC-11 not implemented") def test_hc11_framework_layers_with_forge_secrets_declare_write_targets(): + """Every harness layer carrying forge (git host) credentials MUST declare + ``write_targets`` — empty list is a valid deliberate declaration, + absent key is a regression. + + HC-11 was implemented across the 4 existing layers and verified passing + on 2026-09-18 when the gitea-ssh-hatchmate layer (5th forge-credential + layer) landed with write_targets = ['gitea.oreillyit.nz/hatchmate/*']. + The previous xfail(strict) marker was dropped — the test is now a real + positive gate. + """ found = layers_with_forge_credentials(_HARNESSES_ROOT) assert found, "expected >=1 forge-credential layer on the real tree, found 0" @@ -45,10 +54,6 @@ def test_hc11_framework_layers_with_forge_secrets_declare_write_targets(): if "write_targets" not in parsed: missing.append((name, str(yaml_path))) - # As of this session none of the 4 real layers declare write_targets - # (HC-11 has not landed in the framework repo yet), so this is expected - # to fail today — xfail(strict) so it flips to a real pass the moment - # every forge-credential layer gets a write_targets key. assert not missing, ( "harness layers with forge credentials must declare write_targets " f"(even an empty list): {missing}"