foundry-task-orchestration-actor 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- foundry_task_orchestration_actor-0.1.0/.github/workflows/ci.yml +124 -0
- foundry_task_orchestration_actor-0.1.0/.github/workflows/release.yml +212 -0
- foundry_task_orchestration_actor-0.1.0/.gitignore +6 -0
- foundry_task_orchestration_actor-0.1.0/CLAUDE.md +92 -0
- foundry_task_orchestration_actor-0.1.0/PKG-INFO +230 -0
- foundry_task_orchestration_actor-0.1.0/README.md +206 -0
- foundry_task_orchestration_actor-0.1.0/adr/ADR-FTOA-0001-the-machinery-leaves-the-capability.md +107 -0
- foundry_task_orchestration_actor-0.1.0/adr/ADR-FTOA-0002-round-zero.md +105 -0
- foundry_task_orchestration_actor-0.1.0/adr/README.md +22 -0
- foundry_task_orchestration_actor-0.1.0/adr/template.md +28 -0
- foundry_task_orchestration_actor-0.1.0/docker/Dockerfile +89 -0
- foundry_task_orchestration_actor-0.1.0/examples/ACME.PARTS.CAP.SUP.007.WID-task-orchestration/Dockerfile +32 -0
- foundry_task_orchestration_actor-0.1.0/examples/ACME.PARTS.CAP.SUP.007.WID-task-orchestration/actor-agentic-context.yaml +43 -0
- foundry_task_orchestration_actor-0.1.0/examples/ACME.PARTS.CAP.SUP.007.WID-task-orchestration/platform-standin/k8s/base/kustomization.yaml +9 -0
- foundry_task_orchestration_actor-0.1.0/examples/ACME.PARTS.CAP.SUP.007.WID-task-orchestration/platform-standin/k8s/base/postgres.yaml +39 -0
- foundry_task_orchestration_actor-0.1.0/examples/ACME.PARTS.CAP.SUP.007.WID-task-orchestration/platform-standin/k8s/base/rabbitmq.yaml +39 -0
- foundry_task_orchestration_actor-0.1.0/examples/ACME.PARTS.CAP.SUP.007.WID-task-orchestration/platform-standin/k8s/overlays/ephemeral/kustomization.yaml +5 -0
- foundry_task_orchestration_actor-0.1.0/examples/README.md +138 -0
- foundry_task_orchestration_actor-0.1.0/pyproject.toml +56 -0
- foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/__init__.py +63 -0
- foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/cards/actor-data.yaml +91 -0
- foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/cards/actor-message.yaml +32 -0
- foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/cards/actor-synchronous-messaging.yaml +38 -0
- foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/cards/actor.yaml +22 -0
- foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/cli.py +186 -0
- foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/config.py +578 -0
- foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/conformance.py +136 -0
- foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/correlation.py +198 -0
- foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/deploy.py +399 -0
- foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/handler.py +257 -0
- foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/instance.py +134 -0
- foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/kubeconfig.py +83 -0
- foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/peers.py +91 -0
- foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/pulls.py +187 -0
- foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/round0.py +187 -0
- foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/schemas/agentic-context.schema.yaml +135 -0
- foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/serve.py +130 -0
- foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/settings.py +124 -0
- foundry_task_orchestration_actor-0.1.0/tests/conftest.py +140 -0
- foundry_task_orchestration_actor-0.1.0/tests/fixtures/broken/actor-agentic-context.yaml +10 -0
- foundry_task_orchestration_actor-0.1.0/tests/test_cards.py +98 -0
- foundry_task_orchestration_actor-0.1.0/tests/test_cli.py +57 -0
- foundry_task_orchestration_actor-0.1.0/tests/test_config.py +241 -0
- foundry_task_orchestration_actor-0.1.0/tests/test_conformance.py +175 -0
- foundry_task_orchestration_actor-0.1.0/tests/test_deploy.py +156 -0
- foundry_task_orchestration_actor-0.1.0/tests/test_instance.py +141 -0
- foundry_task_orchestration_actor-0.1.0/tests/test_kubeconfig.py +68 -0
- foundry_task_orchestration_actor-0.1.0/tests/test_portability.py +71 -0
- foundry_task_orchestration_actor-0.1.0/tests/test_pulls.py +53 -0
- foundry_task_orchestration_actor-0.1.0/tests/test_round0.py +340 -0
- foundry_task_orchestration_actor-0.1.0/uv.lock +424 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
on: [push, pull_request]
|
|
3
|
+
|
|
4
|
+
# NO CREDENTIAL. Nothing here authenticates to anything: no peer actor, no cluster, no GitHub API,
|
|
5
|
+
# no PyPI upload, no image push. Everything this pipeline asserts about THIS package, it asserts
|
|
6
|
+
# from source and from artifacts it built itself — the wheel it just built, and an image built from
|
|
7
|
+
# that wheel rather than from a published one.
|
|
8
|
+
#
|
|
9
|
+
# It does reach the network for inputs: public base images (python, the Kubernetes project's
|
|
10
|
+
# kubectl image) and PyPI. That is fetching, not authenticating — no secret is available to this
|
|
11
|
+
# workflow, so nothing it runs can reach a real capability's repositories or a real cluster.
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
build:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: astral-sh/setup-uv@v5
|
|
19
|
+
|
|
20
|
+
- name: the test suite
|
|
21
|
+
# Includes round 0 against real loopback HTTP peers; Kubernetes and GitHub are replaced.
|
|
22
|
+
run: uv run --extra dev pytest -q
|
|
23
|
+
|
|
24
|
+
- name: build
|
|
25
|
+
run: uv build
|
|
26
|
+
|
|
27
|
+
# ── the gates a contract-shipping package runs against its own artifact ─────────────────
|
|
28
|
+
#
|
|
29
|
+
# The suite above proves the code works in a source checkout. These prove the WHEEL works,
|
|
30
|
+
# which is a different claim and the one a consumer actually depends on. A schema left out
|
|
31
|
+
# of the build passes every test in tests/ and fails here.
|
|
32
|
+
|
|
33
|
+
- name: the wheel must carry its contract
|
|
34
|
+
run: |
|
|
35
|
+
uv venv /tmp/probe
|
|
36
|
+
uv pip install --python /tmp/probe/bin/python -q dist/*.whl
|
|
37
|
+
/tmp/probe/bin/foundry-task-orchestration-actor lint examples/ACME.PARTS.CAP.SUP.007.WID-task-orchestration
|
|
38
|
+
/tmp/probe/bin/foundry-task-orchestration-actor show examples/ACME.PARTS.CAP.SUP.007.WID-task-orchestration \
|
|
39
|
+
--registry reg.example.com | tee /tmp/show.txt
|
|
40
|
+
grep -q 'reg.example.com/acme.parts/sup.007.wid/backend/tests:<version>' /tmp/show.txt
|
|
41
|
+
grep -q 'http://foundry-acme-parts-cap-sup-007-wid-implementation (derived)' /tmp/show.txt
|
|
42
|
+
|
|
43
|
+
- name: the wheel must carry the actor's own cards
|
|
44
|
+
# The `-actor` suffix asserts a papeete-actor underneath, which lint-card can check
|
|
45
|
+
# (ADR-ECO-0022). The suite proves the cards are conformant in a source checkout; this
|
|
46
|
+
# proves they SHIPPED. `papeete-actor-synchronous-messaging` is a runtime dependency, so its
|
|
47
|
+
# gate is already in the probe venv.
|
|
48
|
+
run: |
|
|
49
|
+
/tmp/probe/bin/papeete-actor-synchronous-messaging lint-card \
|
|
50
|
+
"$(/tmp/probe/bin/python -c 'from foundry_task_orchestration_actor import cards_path; print(cards_path())')"
|
|
51
|
+
|
|
52
|
+
- name: the gate must run
|
|
53
|
+
# A gate that cannot fail is not a gate. Lint a deliberately non-conformant sidecar with
|
|
54
|
+
# the INSTALLED wheel and require a non-zero exit.
|
|
55
|
+
run: |
|
|
56
|
+
if /tmp/probe/bin/foundry-task-orchestration-actor lint tests/fixtures/broken; then
|
|
57
|
+
echo "::error::lint accepted a non-conformant sidecar — the gate is not running"
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
60
|
+
echo "the gate refused a non-conformant sidecar, as it should"
|
|
61
|
+
|
|
62
|
+
# ── the other half of what this package ships ─────────────────────────────────────────────────
|
|
63
|
+
#
|
|
64
|
+
# A use is a sidecar (and the platform stand-in it names), and the image is what turns one into a
|
|
65
|
+
# running actor. The job above proves the WHEEL works. This one proves that a folder holding
|
|
66
|
+
# nothing but those builds into an actor that boots and answers its door. Nothing here reaches a
|
|
67
|
+
# registry: the image is built, run and thrown away in the runner.
|
|
68
|
+
image:
|
|
69
|
+
runs-on: ubuntu-latest
|
|
70
|
+
steps:
|
|
71
|
+
- uses: actions/checkout@v4
|
|
72
|
+
- uses: astral-sh/setup-uv@v5
|
|
73
|
+
|
|
74
|
+
- name: build the wheel this image will carry
|
|
75
|
+
run: uv build
|
|
76
|
+
|
|
77
|
+
- name: the base image
|
|
78
|
+
run: docker build -f docker/Dockerfile -t foundry-task-orchestration-actor:ci .
|
|
79
|
+
|
|
80
|
+
- name: the image and the wheel are one release
|
|
81
|
+
run: |
|
|
82
|
+
wheel="$(ls dist/*.whl | sed -E 's/.*-([0-9][^-]*)-py3.*/\1/')"
|
|
83
|
+
in_image="$(docker run --rm --entrypoint python foundry-task-orchestration-actor:ci \
|
|
84
|
+
-c 'from foundry_task_orchestration_actor import version; print(version())')"
|
|
85
|
+
echo "wheel=$wheel image=$in_image"
|
|
86
|
+
[ "$wheel" = "$in_image" ] || { echo "::error::image carries $in_image, wheel is $wheel"; exit 1; }
|
|
87
|
+
|
|
88
|
+
- name: the image carries the kubectl it pins
|
|
89
|
+
# papeete-deploy and deploy.py shell out to it on every attempt; an image without it builds,
|
|
90
|
+
# boots, answers /health, and fails the first task that gets past round 0.
|
|
91
|
+
run: docker run --rm --entrypoint kubectl foundry-task-orchestration-actor:ci version --client
|
|
92
|
+
|
|
93
|
+
- name: a use is a sidecar
|
|
94
|
+
# The example folder holds its sidecar, its platform stand-in and its Dockerfile — nothing
|
|
95
|
+
# else. If this builds, the claim in the README is true of a real folder rather than of prose.
|
|
96
|
+
run: |
|
|
97
|
+
test "$(ls examples/ACME.PARTS.CAP.SUP.007.WID-task-orchestration | sort | tr '\n' ' ')" = \
|
|
98
|
+
"Dockerfile actor-agentic-context.yaml platform-standin "
|
|
99
|
+
docker build --build-arg ACTOR_IMAGE=foundry-task-orchestration-actor:ci \
|
|
100
|
+
-t use:ci examples/ACME.PARTS.CAP.SUP.007.WID-task-orchestration
|
|
101
|
+
|
|
102
|
+
- name: the rendered cards are what a caller is validated against
|
|
103
|
+
run: docker run --rm use:ci foundry-task-orchestration-actor lint /actor
|
|
104
|
+
|
|
105
|
+
- name: it boots, and answers the door its cards declare
|
|
106
|
+
# The credential is deliberately junk and no run is exercised: a 400 means routed and
|
|
107
|
+
# refused the payload, a 404 would mean the route was never built. Outside a cluster the
|
|
108
|
+
# kubeconfig bootstrap has nothing to write, and says so rather than failing the boot.
|
|
109
|
+
run: |
|
|
110
|
+
docker run -d --name smoke -p 18080:8080 -e GITHUB_TOKEN=ci use:ci
|
|
111
|
+
for _ in $(seq 1 30); do
|
|
112
|
+
curl -fsS -m 2 http://localhost:18080/health >/dev/null 2>&1 && break
|
|
113
|
+
sleep 1
|
|
114
|
+
done
|
|
115
|
+
curl -fsS -m 5 http://localhost:18080/health | grep -q ok
|
|
116
|
+
code="$(curl -sS -o /dev/null -w '%{http_code}' -m 5 -X POST \
|
|
117
|
+
-H 'content-type: application/json' -d '{}' http://localhost:18080/orchestrate-task)"
|
|
118
|
+
[ "$code" = "400" ] || { echo "::error::POST /orchestrate-task answered $code, expected 400 (routed, bad payload)"; exit 1; }
|
|
119
|
+
code="$(curl -sS -o /dev/null -w '%{http_code}' -m 5 -X POST \
|
|
120
|
+
-H 'content-type: application/json' -d '{}' http://localhost:18080/no-such-door)"
|
|
121
|
+
[ "$code" = "404" ] || { echo "::error::an undeclared door answered $code, expected 404"; exit 1; }
|
|
122
|
+
docker logs smoke | grep -q actor-started
|
|
123
|
+
docker logs smoke | grep -q '"step": "kubeconfig"'
|
|
124
|
+
docker rm -f smoke
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
tags: ["v*"]
|
|
5
|
+
|
|
6
|
+
# A REGISTRY ADDED AFTER A TAG WAS ALREADY CUT. The image job below publishes to two registries,
|
|
7
|
+
# and the second one was not always there. Running this workflow by hand backfills the image for
|
|
8
|
+
# a version whose wheel is already on PyPI — without inventing a version number whose only
|
|
9
|
+
# content is "the last release missed a registry".
|
|
10
|
+
#
|
|
11
|
+
# THE TAG IS AN INPUT, NOT THE REF THIS RUNS FROM. GitHub takes the workflow FILE from the ref it
|
|
12
|
+
# is dispatched on, and the tag needing a backfill is by definition older than the workflow that
|
|
13
|
+
# can do it — dispatching on that tag would run the very file that lacks this trigger. So run it
|
|
14
|
+
# from the default branch and name the tag here; the checkout below takes the CONTENT from the
|
|
15
|
+
# tag, and the version gate still refuses a tag that disagrees with the `pyproject.toml` beside
|
|
16
|
+
# it.
|
|
17
|
+
workflow_dispatch:
|
|
18
|
+
inputs:
|
|
19
|
+
tag:
|
|
20
|
+
description: "Existing tag to publish an image for, e.g. v0.1.0"
|
|
21
|
+
required: true
|
|
22
|
+
|
|
23
|
+
# PyPI Trusted Publishing (OIDC) — no token is stored anywhere. The one-time setup is a pending
|
|
24
|
+
# publisher on pypi.org naming this repo and this workflow; after the first release it becomes a
|
|
25
|
+
# normal publisher. See README.
|
|
26
|
+
permissions:
|
|
27
|
+
id-token: write # PyPI Trusted Publishing
|
|
28
|
+
contents: read
|
|
29
|
+
packages: write # the image, to this repo's own container registry
|
|
30
|
+
|
|
31
|
+
jobs:
|
|
32
|
+
publish:
|
|
33
|
+
# ONLY ON A TAG PUSH. A manual run exists to backfill an IMAGE (see `on:` above), and PyPI
|
|
34
|
+
# refuses a version it already holds — so re-uploading is not merely redundant, it fails, and
|
|
35
|
+
# would take the image job down with it.
|
|
36
|
+
if: github.event_name == 'push'
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
environment: pypi
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v4
|
|
41
|
+
- uses: astral-sh/setup-uv@v5
|
|
42
|
+
|
|
43
|
+
- name: build
|
|
44
|
+
# No fetch step and no external token: the contract is committed here. A release depends
|
|
45
|
+
# on nothing but this checkout and PyPI.
|
|
46
|
+
run: uv build
|
|
47
|
+
|
|
48
|
+
- name: the wheel must carry its contract
|
|
49
|
+
run: |
|
|
50
|
+
uv venv /tmp/probe
|
|
51
|
+
uv pip install --python /tmp/probe/bin/python -q dist/*.whl
|
|
52
|
+
/tmp/probe/bin/foundry-task-orchestration-actor lint examples/ACME.PARTS.CAP.SUP.007.WID-task-orchestration
|
|
53
|
+
|
|
54
|
+
- name: the wheel must carry the actor's own cards
|
|
55
|
+
# Same reason as the line above, for the other half of what this package ships. The
|
|
56
|
+
# `-actor` suffix asserts a papeete-actor underneath (ADR-ECO-0022); a wheel published
|
|
57
|
+
# without its cards makes the name a claim the artifact cannot honour, and a published
|
|
58
|
+
# version is not something to discover that from.
|
|
59
|
+
run: |
|
|
60
|
+
/tmp/probe/bin/papeete-actor-synchronous-messaging lint-card \
|
|
61
|
+
"$(/tmp/probe/bin/python -c 'from foundry_task_orchestration_actor import cards_path; print(cards_path())')"
|
|
62
|
+
|
|
63
|
+
- name: the gate must run
|
|
64
|
+
# BEFORE the upload, not after. A wheel whose lint silently passes everything, or that
|
|
65
|
+
# shipped without its schema, is not something to discover from a published version.
|
|
66
|
+
run: |
|
|
67
|
+
if /tmp/probe/bin/foundry-task-orchestration-actor lint tests/fixtures/broken; then
|
|
68
|
+
echo "::error::lint accepted a non-conformant sidecar — refusing to publish"
|
|
69
|
+
exit 1
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
- name: publish to PyPI
|
|
73
|
+
run: uv publish --trusted-publishing always
|
|
74
|
+
|
|
75
|
+
# ── the image, from the same tag and the same checkout ────────────────────────────────────────
|
|
76
|
+
#
|
|
77
|
+
# TWO ARTIFACTS, ONE RELEASE (ADR-FIA-0005, ADR-FTOA-0001). A use pins an image; an embedder pins
|
|
78
|
+
# a wheel; both are this package at this version. A tag that produced one and not the other leaves a consumer
|
|
79
|
+
# unable to take the release at all — so this job builds the wheel again from the same checkout
|
|
80
|
+
# rather than installing the one just uploaded: the image must not depend on PyPI having already
|
|
81
|
+
# indexed it, and must not be able to pick up a different build of the same version.
|
|
82
|
+
#
|
|
83
|
+
# It runs AFTER the wheel is published, not beside it. A failed upload should not leave an image
|
|
84
|
+
# published for a version that does not exist on PyPI; the reverse — a wheel with the image still
|
|
85
|
+
# to come — is recoverable by re-running this job alone.
|
|
86
|
+
image:
|
|
87
|
+
needs: publish
|
|
88
|
+
# `needs` still orders this after the wheel on a tag push. On a manual backfill `publish` is
|
|
89
|
+
# skipped, and a skipped dependency skips its dependents too unless `always()` says otherwise —
|
|
90
|
+
# so the condition has to name both cases rather than relying on `needs` alone.
|
|
91
|
+
if: always() && (needs.publish.result == 'success' || github.event_name == 'workflow_dispatch')
|
|
92
|
+
runs-on: ubuntu-latest
|
|
93
|
+
steps:
|
|
94
|
+
- uses: actions/checkout@v4
|
|
95
|
+
with:
|
|
96
|
+
# Empty on a tag push, which is `checkout`'s own default — the ref that triggered the
|
|
97
|
+
# run. On a manual backfill it is the tag being republished, so every step below reads
|
|
98
|
+
# that tag's own tree rather than whatever the default branch has moved on to.
|
|
99
|
+
ref: ${{ inputs.tag }}
|
|
100
|
+
|
|
101
|
+
- uses: astral-sh/setup-uv@v5
|
|
102
|
+
|
|
103
|
+
- name: the version is the tag
|
|
104
|
+
# The tag is what a consumer writes in a FROM line. A tag that disagrees with
|
|
105
|
+
# pyproject.toml would publish an image whose own `version()` contradicts its name, which
|
|
106
|
+
# is exactly the confusion the CI gate above exists to prevent for the wheel. On a backfill
|
|
107
|
+
# this compares the named tag against the `pyproject.toml` checked out FROM that tag, so a
|
|
108
|
+
# typo in the input is refused rather than published under the wrong name.
|
|
109
|
+
id: version
|
|
110
|
+
env:
|
|
111
|
+
TAG: ${{ inputs.tag || github.ref_name }}
|
|
112
|
+
run: |
|
|
113
|
+
tag="${TAG#v}"
|
|
114
|
+
declared="$(uv run --quiet python -c 'import tomllib,pathlib; print(tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["version"])')"
|
|
115
|
+
[ "$tag" = "$declared" ] || { echo "::error::tag $TAG does not match pyproject version $declared"; exit 1; }
|
|
116
|
+
echo "version=$tag" >> "$GITHUB_OUTPUT"
|
|
117
|
+
|
|
118
|
+
- name: build
|
|
119
|
+
run: uv build
|
|
120
|
+
|
|
121
|
+
- uses: docker/login-action@v3
|
|
122
|
+
with:
|
|
123
|
+
registry: ghcr.io
|
|
124
|
+
username: ${{ github.actor }}
|
|
125
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
126
|
+
|
|
127
|
+
- name: the image, tagged and latest
|
|
128
|
+
env:
|
|
129
|
+
VERSION: ${{ steps.version.outputs.version }}
|
|
130
|
+
# `latest` FOLLOWS THE RELEASE STREAM, NOT A BACKFILL. A manual run exists to give an
|
|
131
|
+
# already-released version an image in a registry it missed; run against an older tag it
|
|
132
|
+
# would otherwise drag `latest` backwards onto it, silently, in whichever registry a
|
|
133
|
+
# consumer happens to pull from. The version tag is the whole point of a backfill —
|
|
134
|
+
# `latest` already names something newer, and stays there.
|
|
135
|
+
MOVE_LATEST: ${{ github.event_name == 'push' }}
|
|
136
|
+
run: |
|
|
137
|
+
image="ghcr.io/${{ github.repository }}"
|
|
138
|
+
docker build -f docker/Dockerfile -t "$image:$VERSION" -t "$image:latest" .
|
|
139
|
+
docker push "$image:$VERSION"
|
|
140
|
+
if [ "$MOVE_LATEST" = "true" ]; then docker push "$image:latest"; fi
|
|
141
|
+
|
|
142
|
+
# ── the product's own registry ────────────────────────────────────────────────────────────
|
|
143
|
+
#
|
|
144
|
+
# TWO REGISTRIES, ONE IMAGE. GHCR is where this package publishes; a product that RUNS uses
|
|
145
|
+
# of it pulls from its own registry, which its cluster already authenticates to and which a
|
|
146
|
+
# `FROM` line inside that cluster's builder can reach without a second credential. The base
|
|
147
|
+
# image has to exist in both, because the two are reached by different things: an embedder
|
|
148
|
+
# writing a Dockerfile reads the README and pins GHCR, while a use built by an in-cluster
|
|
149
|
+
# builder resolves its `FROM` against the registry that builder has a push token for.
|
|
150
|
+
#
|
|
151
|
+
# THE SAME IMAGE, NOT ANOTHER BUILD. `docker tag` re-points the build the step above already
|
|
152
|
+
# pushed, so both registries hold one digest. Building a second time would let them diverge —
|
|
153
|
+
# a base-image refresh between two steps of one job is enough — and nothing downstream would
|
|
154
|
+
# ever notice that `:0.1.0` meant two different things depending on where you pulled it.
|
|
155
|
+
#
|
|
156
|
+
# ONE VARIABLE NAMES THE WHOLE TARGET. `vars.PRODUCT_IMAGE` is the full repository path, e.g.
|
|
157
|
+
# `<name>.azurecr.io/<product>/foundry-task-orchestration-actor`; the login server is its first
|
|
158
|
+
# segment. This package is generic across capabilities and names no product of its own — the
|
|
159
|
+
# same rule `tests/test_portability.py` enforces over `src/` — so the product that hosts a
|
|
160
|
+
# registry names itself in a repository variable rather than in this file.
|
|
161
|
+
#
|
|
162
|
+
# UNSET IS A VALID STATE, AND IT SAYS SO. A fork, or this repo before its credentials were
|
|
163
|
+
# added, publishes to GHCR alone; that is a `::notice`, not a failure. Configured-but-broken
|
|
164
|
+
# IS a failure — the push below is not best-effort, because an image the product's cluster
|
|
165
|
+
# cannot pull is the whole reason this block exists.
|
|
166
|
+
- name: is a product registry configured?
|
|
167
|
+
id: product
|
|
168
|
+
env:
|
|
169
|
+
PRODUCT_IMAGE: ${{ vars.PRODUCT_IMAGE }}
|
|
170
|
+
run: |
|
|
171
|
+
if [ -z "$PRODUCT_IMAGE" ]; then
|
|
172
|
+
echo "::notice::PRODUCT_IMAGE is unset — publishing to GHCR only"
|
|
173
|
+
echo "configured=false" >> "$GITHUB_OUTPUT"
|
|
174
|
+
exit 0
|
|
175
|
+
fi
|
|
176
|
+
case "$PRODUCT_IMAGE" in
|
|
177
|
+
*/*/*) : ;;
|
|
178
|
+
*) echo "::error::PRODUCT_IMAGE must be <registry>/<path>/<repository>, got $PRODUCT_IMAGE"; exit 1 ;;
|
|
179
|
+
esac
|
|
180
|
+
echo "server=${PRODUCT_IMAGE%%/*}" >> "$GITHUB_OUTPUT"
|
|
181
|
+
echo "configured=true" >> "$GITHUB_OUTPUT"
|
|
182
|
+
|
|
183
|
+
- uses: docker/login-action@v3
|
|
184
|
+
if: steps.product.outputs.configured == 'true'
|
|
185
|
+
with:
|
|
186
|
+
registry: ${{ steps.product.outputs.server }}
|
|
187
|
+
username: ${{ secrets.ACR_PUSH_USERNAME }}
|
|
188
|
+
password: ${{ secrets.ACR_PUSH_PASSWORD }}
|
|
189
|
+
|
|
190
|
+
- name: the same image, in the product's registry
|
|
191
|
+
if: steps.product.outputs.configured == 'true'
|
|
192
|
+
env:
|
|
193
|
+
PRODUCT_IMAGE: ${{ vars.PRODUCT_IMAGE }}
|
|
194
|
+
VERSION: ${{ steps.version.outputs.version }}
|
|
195
|
+
MOVE_LATEST: ${{ github.event_name == 'push' }}
|
|
196
|
+
run: |
|
|
197
|
+
ghcr="ghcr.io/${{ github.repository }}"
|
|
198
|
+
docker tag "$ghcr:$VERSION" "$PRODUCT_IMAGE:$VERSION"
|
|
199
|
+
docker push "$PRODUCT_IMAGE:$VERSION"
|
|
200
|
+
if [ "$MOVE_LATEST" = "true" ]; then
|
|
201
|
+
docker tag "$ghcr:latest" "$PRODUCT_IMAGE:latest"
|
|
202
|
+
docker push "$PRODUCT_IMAGE:latest"
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
- name: a use built on it is still an actor
|
|
206
|
+
# BEFORE the image is something anyone can pin — the same reason the gates run before the
|
|
207
|
+
# upload above. An image that builds but produces a use that does not conform is
|
|
208
|
+
# not something to discover from a published tag.
|
|
209
|
+
run: |
|
|
210
|
+
docker build --build-arg ACTOR_IMAGE=ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }} \
|
|
211
|
+
-t use:release examples/ACME.PARTS.CAP.SUP.007.WID-task-orchestration
|
|
212
|
+
docker run --rm use:release foundry-task-orchestration-actor lint /actor
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this
|
|
4
|
+
repository.
|
|
5
|
+
|
|
6
|
+
## What this is
|
|
7
|
+
|
|
8
|
+
`foundry-task-orchestration-actor` drives one capability's task through the three amigos round
|
|
9
|
+
(testing proposes, implementation assesses), then implement → ephemeral deploy → black-box test →
|
|
10
|
+
retry, and opens the paired pull requests on a pass. It is the **machinery** — it carries no
|
|
11
|
+
capability of its own and runs no engine. The capability arrives in a sidecar
|
|
12
|
+
(`actor-agentic-context.yaml`, contract `foundry-task-orchestration-actor/agentic-context/v1`).
|
|
13
|
+
|
|
14
|
+
Its shape is copied from `foundry-implementation-actor` (FIA) with names changed — see
|
|
15
|
+
`adr/ADR-FTOA-0001-*.md`. There is deliberately no shared kit between the two.
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uv run --extra dev pytest -q # full suite (what CI runs)
|
|
21
|
+
uv run --extra dev pytest -q tests/test_round0.py # one file
|
|
22
|
+
uv run --extra dev pytest -q tests/test_round0.py::test_name # one test
|
|
23
|
+
uv build # sdist/wheel (hatchling)
|
|
24
|
+
uv run foundry-task-orchestration-actor lint <folder> # validate a sidecar
|
|
25
|
+
uv run foundry-task-orchestration-actor show <folder> --registry r # every derived rendering
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
There is no separate lint/format command configured in this repo.
|
|
29
|
+
|
|
30
|
+
## Architecture
|
|
31
|
+
|
|
32
|
+
Under `src/foundry_task_orchestration_actor/`:
|
|
33
|
+
|
|
34
|
+
- **`config.py`** — `CapabilityConfig`. Loads the sidecar and derives every identifier from
|
|
35
|
+
`capability` and `source_repo`: peers' repos and URLs, registry path, workload names, image
|
|
36
|
+
names, run ids, service URLs; renders `ephemeral.secrets` templates. Also `lint()` / `Report`.
|
|
37
|
+
- **`settings.py`** — `Settings`: operational tuning (attempts, timeouts, kube context, registry,
|
|
38
|
+
pull secret, base branch, peer URL overrides), from constructor or environment. Never sidecar.
|
|
39
|
+
- **`round0.py`** — the three amigos round as a pure function over two injected door calls.
|
|
40
|
+
All the wire contract's stop rules and the commitment attachment rule live here.
|
|
41
|
+
- **`peers.py`** — `call_door`: POST `{from, payload}` with a long timeout and `traceparent`;
|
|
42
|
+
400/transport/non-object → `PeerError`.
|
|
43
|
+
- **`deploy.py`** — one attempt's ephemeral namespace: clone `impl/<task_id>` read-only, pull
|
|
44
|
+
secret, platform stand-in, component secrets + components, test Jobs, best-effort teardown.
|
|
45
|
+
- **`pulls.py`** — the paired PRs; body renders the agreed surface. Implementation PR required,
|
|
46
|
+
the rest best-effort.
|
|
47
|
+
- **`handler.py`** — `make_orchestrate_task(config, settings)`: round 0, the attempt loop, the
|
|
48
|
+
`stage` of every failure.
|
|
49
|
+
- **`kubeconfig.py`** — in-cluster kubeconfig from the ServiceAccount (replaces `entrypoint.sh`).
|
|
50
|
+
- **`correlation.py`** — byte-identical to FIA's below the docstring.
|
|
51
|
+
- **`conformance.py`**, **`instance.py`**, **`serve.py`**, **`cli.py`** — FIA's, names changed;
|
|
52
|
+
`serve` wires no engine and bootstraps the kubeconfig.
|
|
53
|
+
|
|
54
|
+
Committed contract, shipped in the wheel: `schemas/agentic-context.schema.yaml` and `cards/`.
|
|
55
|
+
|
|
56
|
+
## Core invariants that any change must preserve
|
|
57
|
+
|
|
58
|
+
- **The wire contract is shared with two other packages.** Door ids (`propose-acceptance`,
|
|
59
|
+
`assess-task`, `implement-task`, `test-task`, `orchestrate-task`), message names, fields, and the
|
|
60
|
+
round-0 rules are fixed across `foundry-implementation-actor`, `foundry-testing-actor` and this
|
|
61
|
+
package. Do not change a field or a stop rule here alone.
|
|
62
|
+
- **Round 0 stops before implementation is asked** when the tester has open questions. Never
|
|
63
|
+
"assess anyway".
|
|
64
|
+
- **The agreed surface goes to implement-task AND test-task on EVERY attempt.**
|
|
65
|
+
- **A failed result always names its `stage`**, and omits unknown fields rather than sending null
|
|
66
|
+
— both completion schemas are closed and typed.
|
|
67
|
+
- **No capability literal, ever.** `tests/test_portability.py` greps `src/` for the originating
|
|
68
|
+
instance's identifiers (id, workload prefix, database name, the hardcoded kube context). The
|
|
69
|
+
platform stand-in and its credentials are the USE's (`ephemeral:`), never the wheel's.
|
|
70
|
+
- **The image ref is a three-way contract.** `<registry>/<capability_path>/<component>[/tests]:<v>`
|
|
71
|
+
is parsed by path segment in `config.parse_images`; never re-derive or invent a tag.
|
|
72
|
+
- **Teardown is best-effort and always runs**, and deletes the namespace last.
|
|
73
|
+
- **The image is one build in two registries** (FIA's ADR-FIA-0006) — never a second `docker build`
|
|
74
|
+
in `release.yml`, never a hardcoded registry.
|
|
75
|
+
- **`kubectl` is pinned** in `docker/Dockerfile`. Move it deliberately with the clusters it drives.
|
|
76
|
+
|
|
77
|
+
## Scope discipline
|
|
78
|
+
|
|
79
|
+
Generic across **capabilities**, not across actor *kinds*. No hooks or base classes for a
|
|
80
|
+
hypothetical second orchestration shape. A component layout other than
|
|
81
|
+
`<component>/deployment/dev/k8s/overlays/ephemeral` is not supported until a real capability needs
|
|
82
|
+
it — and then it is a sidecar declaration, not a strategy object.
|
|
83
|
+
|
|
84
|
+
Design rationale lives in `adr/`. Add a new ADR (copy `adr/template.md`) for any decision of
|
|
85
|
+
similar weight rather than only writing it into code comments.
|
|
86
|
+
|
|
87
|
+
## Releasing
|
|
88
|
+
|
|
89
|
+
Tag-triggered (`v*`) via `.github/workflows/release.yml`: PyPI through Trusted Publishing, the image
|
|
90
|
+
to GHCR and, when `vars.PRODUCT_IMAGE` names one, to a product's own registry. A manual run with a
|
|
91
|
+
tag input backfills an image only. `ci.yml` runs the suite, the wheel gates, and builds and boots
|
|
92
|
+
the example use — and reaches nothing outside its own checkout.
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: foundry-task-orchestration-actor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Drives one capability's task through the three amigos round, implementation, an ephemeral black-box test run and a paired pull request — a papeete-actor for one use, with the capability supplied by a sidecar.
|
|
5
|
+
Project-URL: Homepage, https://github.com/papeete-hub/foundry-task-orchestration-actor
|
|
6
|
+
Author-email: Papeete Consulting <yoann.remy@outlook.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: actor-model,capability,orchestration,papeete,three-amigos
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Topic :: Software Development :: Testing
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Requires-Dist: papeete-actor-synchronous-messaging>=0.2.1
|
|
15
|
+
Requires-Dist: papeete-deploy>=0.5.0
|
|
16
|
+
Requires-Dist: papeete-version>=0.1.0
|
|
17
|
+
Requires-Dist: pyyaml>=6.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
20
|
+
Provides-Extra: serve
|
|
21
|
+
Requires-Dist: papeete-actor-synchronous-messaging-http>=0.4.0; extra == 'serve'
|
|
22
|
+
Requires-Dist: papeete-observability>=0.1.0; extra == 'serve'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# foundry-task-orchestration-actor
|
|
26
|
+
|
|
27
|
+
Drives one task of one capability from agreement to pull request: a three amigos round between the
|
|
28
|
+
testing and implementation actors, then implement → deploy into an ephemeral namespace → black-box
|
|
29
|
+
test → retry, and the paired pull requests on a pass. It writes no code, writes no tests, judges
|
|
30
|
+
nothing and merges nothing.
|
|
31
|
+
|
|
32
|
+
An **actor, for one use**, with a [`papeete-actor`](https://github.com/papeete-hub/papeete-actor)
|
|
33
|
+
underneath. The `-actor` suffix is that claim (`ADR-ECO-0022`). It is the orchestration kind of the
|
|
34
|
+
shape [`foundry-implementation-actor`](https://github.com/papeete-hub/foundry-implementation-actor)
|
|
35
|
+
established; the machinery was copied from there with names changed (ADR-FTOA-0001).
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install foundry-task-orchestration-actor
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
> **New here?** [`examples/`](examples/) walks through a complete, working use of this actor. Every
|
|
42
|
+
> command in it except `docker build` runs with no credentials and no network.
|
|
43
|
+
|
|
44
|
+
## What it is
|
|
45
|
+
|
|
46
|
+
The actor's **definition** — its four cards, and the machinery behind them. It carries **no
|
|
47
|
+
capability of its own**: the capability arrives in a sidecar the consuming repo writes:
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
# actor-agentic-context.yaml
|
|
51
|
+
context: foundry-task-orchestration-actor/agentic-context/v1
|
|
52
|
+
capability: ACME.PARTS.CAP.SUP.007.WID
|
|
53
|
+
source_repo: acme-lab/ACME.PARTS.CAP.SUP.007.WID-task-orchestration
|
|
54
|
+
components: [backend, stub]
|
|
55
|
+
ephemeral:
|
|
56
|
+
platform: platform-standin
|
|
57
|
+
secrets:
|
|
58
|
+
- name: "{workload}-db"
|
|
59
|
+
data: {dsn: "postgresql://widgets:widgets@{run_id}-platform-postgres:5432/widgets"}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
and, beside it, a Dockerfile of three lines:
|
|
63
|
+
|
|
64
|
+
```dockerfile
|
|
65
|
+
FROM ghcr.io/papeete-hub/foundry-task-orchestration-actor:0.1.0
|
|
66
|
+
COPY actor-agentic-context.yaml /actor/
|
|
67
|
+
COPY platform-standin /actor/platform-standin
|
|
68
|
+
RUN foundry-task-orchestration-actor render-cards /actor && foundry-task-orchestration-actor lint /actor
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**That is the whole repository**, plus the platform stand-in folder if its components need one. No
|
|
72
|
+
cards, no `app.py`, no `entrypoint.sh`, no `handler.py`.
|
|
73
|
+
|
|
74
|
+
### Embedding it instead
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from foundry_task_orchestration_actor import CapabilityConfig, Settings, make_orchestrate_task
|
|
78
|
+
|
|
79
|
+
config = CapabilityConfig.load(".")
|
|
80
|
+
actor = Actor.from_card(".", mailbox=mailbox,
|
|
81
|
+
actions={"orchestrate-task": make_orchestrate_task(config, Settings())})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
No `engines=`: the door names none. The mailbox and observability backend `serve` needs live in the
|
|
85
|
+
`[serve]` extra.
|
|
86
|
+
|
|
87
|
+
## The sidecar — `foundry-task-orchestration-actor/agentic-context/v1`
|
|
88
|
+
|
|
89
|
+
| field | required | what it is | default |
|
|
90
|
+
|---|---|---|---|
|
|
91
|
+
| `context` | yes | the contract string | — |
|
|
92
|
+
| `capability` | yes | `<ENT>.<DOMAIN>.CAP.<TYPE>.<NNN>.<CODE>` | — |
|
|
93
|
+
| `source_repo` | yes | `<owner>/<repo>` of this actor's own repo; the repo half is its name | — |
|
|
94
|
+
| `components` | yes | component names, as the implementation actor's sidecar names them; round 0 sends them to the tester | — |
|
|
95
|
+
| `peers.implementation.repo` | no | where implementation is pushed and the PR is opened | `<owner>/<capability>-implementation` |
|
|
96
|
+
| `peers.implementation.url` | no | base URL of its doors | `http://foundry-<capability lowercased, dots→hyphens>-implementation` |
|
|
97
|
+
| `peers.testing.repo` | no | where tests are pushed and the paired PR is opened | `<owner>/<capability>-testing` |
|
|
98
|
+
| `peers.testing.url` | no | base URL of its doors | `http://foundry-<capability slug>-testing` |
|
|
99
|
+
| `ephemeral.platform` | no | folder beside the sidecar with `k8s/overlays/ephemeral/` — stand-ins applied before any component | none |
|
|
100
|
+
| `ephemeral.secrets[]` | no | `{name, data}` templates created for each touched component; `{run_id}`, `{component}`, `{workload}`, `{capability}` substituted | none |
|
|
101
|
+
|
|
102
|
+
`IMPLEMENTATION_URL` / `TESTING_URL` in the environment override both the declared and the derived
|
|
103
|
+
URL. A misspelt role or key under `peers:` is refused rather than silently falling back to the
|
|
104
|
+
default. What is **not** in the sidecar, on purpose, is operational tuning — see *Settings*.
|
|
105
|
+
|
|
106
|
+
## One door
|
|
107
|
+
|
|
108
|
+
| door | verb | engine | what it does |
|
|
109
|
+
|---|---|---|---|
|
|
110
|
+
| `orchestrate-task` | request | none | round 0, then up to N attempts, then the PRs |
|
|
111
|
+
|
|
112
|
+
Request `orchestrate-task-cmd`: `task_id`, `title`, `definition_of_done`, optional `context`.
|
|
113
|
+
|
|
114
|
+
Completions (closed; a reply matches exactly one):
|
|
115
|
+
|
|
116
|
+
- `orchestration-succeeded-result` — `accepted`, `pr_url`, `branch`, `test_branch`, `attempts`,
|
|
117
|
+
`verdict`, `acceptance_surface`; optional `test_pr_url`.
|
|
118
|
+
- `orchestration-failed-result` — `accepted`, `because`, `stage`, `attempts`; optional `branch`,
|
|
119
|
+
`test_branch`, `verdict`, `acceptance_surface`, `open_questions`, `objections`, `commitments`.
|
|
120
|
+
`stage` ∈ `round-0` | `implementation` | `testing` | `verdict`.
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
round 0 testing.propose-acceptance → expectations, open_questions
|
|
124
|
+
implementation.assess-task → feasible, objections, commitments
|
|
125
|
+
stop (stage round-0) on a transport error, open questions, no expectations,
|
|
126
|
+
infeasible, or any objection — carrying what was said
|
|
127
|
+
attempt n implementation.implement-task (acceptance_surface, remediation_context?)
|
|
128
|
+
testing.test-task (acceptance_surface, touched components)
|
|
129
|
+
clone impl/<task_id> read-only → namespace test-<task_id> → pull Secret →
|
|
130
|
+
platform stand-in → Secrets + each touched component → a test Job per test image →
|
|
131
|
+
teardown
|
|
132
|
+
green → paired PRs, succeed red → remediation context, next attempt
|
|
133
|
+
exhausted stop (stage verdict)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The **agreed surface** is the tester's expectations, with each of the implementer's commitments that
|
|
137
|
+
names an expectation (`id`, `expectation` or `expectation_id`) attached under `commitments`, and
|
|
138
|
+
every other commitment appended as `{id: commitment-<n>, statement, handle, commitment}`. It goes to
|
|
139
|
+
both doors on every attempt and is rendered into the PR body. See ADR-FTOA-0002.
|
|
140
|
+
|
|
141
|
+
## Every rendering, from two fields
|
|
142
|
+
|
|
143
|
+
`foundry-task-orchestration-actor show` prints the table for a given sidecar:
|
|
144
|
+
|
|
145
|
+
| rendering | example |
|
|
146
|
+
|---|---|
|
|
147
|
+
| actor name | `ACME.PARTS.CAP.SUP.007.WID-task-orchestration` |
|
|
148
|
+
| registry path | `acme.parts/sup.007.wid` |
|
|
149
|
+
| component image | `<registry>/acme.parts/sup.007.wid/backend:<version>` |
|
|
150
|
+
| test image | `<registry>/acme.parts/sup.007.wid/backend/tests:<version>` |
|
|
151
|
+
| image name the base manifest carries | `acme.parts.cap.sup.007.wid-backend` |
|
|
152
|
+
| namespace / papeete-deploy product | `test-task-042` |
|
|
153
|
+
| component Deployment / Service | `test-task-042-sup-007-wid-backend` |
|
|
154
|
+
| what the test Job gets | `BACKEND_URL=http://test-task-042-sup-007-wid-backend` |
|
|
155
|
+
|
|
156
|
+
**The image ref is a three-way contract.** Both peers publish refs this actor parses back apart by
|
|
157
|
+
path segment; nothing is re-derived or looked up.
|
|
158
|
+
|
|
159
|
+
## Settings
|
|
160
|
+
|
|
161
|
+
Environment, read once at boot by `serve`; constructor keywords on `Settings` for an embedder.
|
|
162
|
+
|
|
163
|
+
| variable | default | |
|
|
164
|
+
|---|---|---|
|
|
165
|
+
| `IMAGE_REGISTRY` | — | where component and test images are pulled from; required once a run deploys |
|
|
166
|
+
| `IMPLEMENTATION_URL` / `TESTING_URL` | sidecar, else derived | |
|
|
167
|
+
| `MAX_ATTEMPTS` | `3` | |
|
|
168
|
+
| `DOOR_CALL_TIMEOUT_S` | `2400` | implement-task, test-task |
|
|
169
|
+
| `PROPOSE_TIMEOUT_S` / `ASSESS_TIMEOUT_S` | `900` | round 0 |
|
|
170
|
+
| `DEPLOYMENT_READY_TIMEOUT_S` / `TEST_JOB_TIMEOUT_S` | `600` | |
|
|
171
|
+
| `KUBE_CONTEXT` | `in-cluster` | the context `serve` writes from the ServiceAccount |
|
|
172
|
+
| `IMAGE_PULL_SECRET` | `acr-pull` | copied from this Pod's namespace into each run's |
|
|
173
|
+
| `BASE_BRANCH` | `main` | what the PRs target |
|
|
174
|
+
|
|
175
|
+
`GITHUB_TOKEN` is the one credential: read on the implementation repo (the per-attempt clone),
|
|
176
|
+
`pull-requests:write` on both peer repos, `contents:write` on the testing repo (a long test log is
|
|
177
|
+
committed beside the tests and linked). No `CLAUDE_CODE_OAUTH_TOKEN` — nothing here runs a session.
|
|
178
|
+
|
|
179
|
+
## Running in a cluster
|
|
180
|
+
|
|
181
|
+
`serve` writes a kubeconfig from the Pod's projected ServiceAccount (token by path, so a rotation is
|
|
182
|
+
picked up) under the context `KUBE_CONTEXT` names, before the door opens. Outside a cluster it
|
|
183
|
+
writes nothing and says so. The ServiceAccount needs cluster-wide RBAC to create and delete
|
|
184
|
+
namespaces and manage Deployments, Services, Secrets, ServiceAccounts, Jobs and Pods/logs in them —
|
|
185
|
+
the namespace does not exist when the RBAC is written.
|
|
186
|
+
|
|
187
|
+
## CLI
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
foundry-task-orchestration-actor lint . # the sidecar, and the cards if rendered
|
|
191
|
+
foundry-task-orchestration-actor show . --registry reg.example.com # every derived identifier
|
|
192
|
+
foundry-task-orchestration-actor render-cards . # the four cards, from the wheel
|
|
193
|
+
foundry-task-orchestration-actor serve . # boot it (needs the `serve` extra)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Observability
|
|
197
|
+
|
|
198
|
+
The same record schema as the implementation actor — `correlation.py` is byte-identical below its
|
|
199
|
+
docstring: `{event: "step", step, phase, duration_ms}`, `{event: "event", step, **fields}`, with
|
|
200
|
+
`correlation_id` (the W3C trace id, propagated to both peers via `traceparent`), `task_id`,
|
|
201
|
+
`run_id` and `attempt` as structured metadata. Steps: `round-0`, `call-propose-acceptance`,
|
|
202
|
+
`call-assess-task`, `call-implement-task`, `call-test-task`, `clone-implementation`,
|
|
203
|
+
`deploy-pull-secret`, `deploy-platform-standin`, `deploy-component`, `run-test-job`, `teardown`,
|
|
204
|
+
`open-pr`. The step names are free to change; the record shape is not.
|
|
205
|
+
|
|
206
|
+
## Releasing, and which registry to pin
|
|
207
|
+
|
|
208
|
+
Identical to the implementation actor's (ADR-FIA-0006): a `v*` tag publishes the wheel to PyPI
|
|
209
|
+
(Trusted Publishing) and one image build to `ghcr.io/papeete-hub/foundry-task-orchestration-actor`
|
|
210
|
+
and, when `vars.PRODUCT_IMAGE` names one, to a product's own registry. A manual run with a tag input
|
|
211
|
+
backfills an image without touching PyPI or `latest`:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
gh workflow run release.yml --ref main -f tag=v0.1.0
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Where this came from
|
|
218
|
+
|
|
219
|
+
Extracted from one capability's hand-written task-orchestration repository. Behaviour was ported,
|
|
220
|
+
with the capability's literals turned into derivations or sidecar declarations, round 0 added, and
|
|
221
|
+
known drift fixed (ADR-FTOA-0001). Decisions live in `adr/`.
|
|
222
|
+
|
|
223
|
+
## Development
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
uv run --extra dev pytest -q # what CI runs
|
|
227
|
+
uv build
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
There is no separate lint/format command configured in this repo.
|