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.
Files changed (51) hide show
  1. foundry_task_orchestration_actor-0.1.0/.github/workflows/ci.yml +124 -0
  2. foundry_task_orchestration_actor-0.1.0/.github/workflows/release.yml +212 -0
  3. foundry_task_orchestration_actor-0.1.0/.gitignore +6 -0
  4. foundry_task_orchestration_actor-0.1.0/CLAUDE.md +92 -0
  5. foundry_task_orchestration_actor-0.1.0/PKG-INFO +230 -0
  6. foundry_task_orchestration_actor-0.1.0/README.md +206 -0
  7. foundry_task_orchestration_actor-0.1.0/adr/ADR-FTOA-0001-the-machinery-leaves-the-capability.md +107 -0
  8. foundry_task_orchestration_actor-0.1.0/adr/ADR-FTOA-0002-round-zero.md +105 -0
  9. foundry_task_orchestration_actor-0.1.0/adr/README.md +22 -0
  10. foundry_task_orchestration_actor-0.1.0/adr/template.md +28 -0
  11. foundry_task_orchestration_actor-0.1.0/docker/Dockerfile +89 -0
  12. foundry_task_orchestration_actor-0.1.0/examples/ACME.PARTS.CAP.SUP.007.WID-task-orchestration/Dockerfile +32 -0
  13. foundry_task_orchestration_actor-0.1.0/examples/ACME.PARTS.CAP.SUP.007.WID-task-orchestration/actor-agentic-context.yaml +43 -0
  14. foundry_task_orchestration_actor-0.1.0/examples/ACME.PARTS.CAP.SUP.007.WID-task-orchestration/platform-standin/k8s/base/kustomization.yaml +9 -0
  15. foundry_task_orchestration_actor-0.1.0/examples/ACME.PARTS.CAP.SUP.007.WID-task-orchestration/platform-standin/k8s/base/postgres.yaml +39 -0
  16. foundry_task_orchestration_actor-0.1.0/examples/ACME.PARTS.CAP.SUP.007.WID-task-orchestration/platform-standin/k8s/base/rabbitmq.yaml +39 -0
  17. 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
  18. foundry_task_orchestration_actor-0.1.0/examples/README.md +138 -0
  19. foundry_task_orchestration_actor-0.1.0/pyproject.toml +56 -0
  20. foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/__init__.py +63 -0
  21. foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/cards/actor-data.yaml +91 -0
  22. foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/cards/actor-message.yaml +32 -0
  23. foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/cards/actor-synchronous-messaging.yaml +38 -0
  24. foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/cards/actor.yaml +22 -0
  25. foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/cli.py +186 -0
  26. foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/config.py +578 -0
  27. foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/conformance.py +136 -0
  28. foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/correlation.py +198 -0
  29. foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/deploy.py +399 -0
  30. foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/handler.py +257 -0
  31. foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/instance.py +134 -0
  32. foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/kubeconfig.py +83 -0
  33. foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/peers.py +91 -0
  34. foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/pulls.py +187 -0
  35. foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/round0.py +187 -0
  36. foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/schemas/agentic-context.schema.yaml +135 -0
  37. foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/serve.py +130 -0
  38. foundry_task_orchestration_actor-0.1.0/src/foundry_task_orchestration_actor/settings.py +124 -0
  39. foundry_task_orchestration_actor-0.1.0/tests/conftest.py +140 -0
  40. foundry_task_orchestration_actor-0.1.0/tests/fixtures/broken/actor-agentic-context.yaml +10 -0
  41. foundry_task_orchestration_actor-0.1.0/tests/test_cards.py +98 -0
  42. foundry_task_orchestration_actor-0.1.0/tests/test_cli.py +57 -0
  43. foundry_task_orchestration_actor-0.1.0/tests/test_config.py +241 -0
  44. foundry_task_orchestration_actor-0.1.0/tests/test_conformance.py +175 -0
  45. foundry_task_orchestration_actor-0.1.0/tests/test_deploy.py +156 -0
  46. foundry_task_orchestration_actor-0.1.0/tests/test_instance.py +141 -0
  47. foundry_task_orchestration_actor-0.1.0/tests/test_kubeconfig.py +68 -0
  48. foundry_task_orchestration_actor-0.1.0/tests/test_portability.py +71 -0
  49. foundry_task_orchestration_actor-0.1.0/tests/test_pulls.py +53 -0
  50. foundry_task_orchestration_actor-0.1.0/tests/test_round0.py +340 -0
  51. 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,6 @@
1
+ dist/
2
+ *.egg-info/
3
+ __pycache__/
4
+ .venv/
5
+ .pytest_cache/
6
+ .serena/
@@ -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.