foundry-implementation-actor 0.5.0__tar.gz → 0.5.1__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_implementation_actor-0.5.1/.github/workflows/release.yml +210 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/CLAUDE.md +14 -4
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/PKG-INFO +31 -3
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/README.md +30 -2
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/adr/ADR-FIA-0004-the-three-amigos-round.md +12 -3
- foundry_implementation_actor-0.5.1/adr/ADR-FIA-0006-the-image-is-published-to-two-registries.md +104 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/adr/README.md +1 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/examples/ACME.PARTS.CAP.SUP.007.WID-implementation/Dockerfile +8 -1
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/examples/README.md +18 -1
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/pyproject.toml +1 -1
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/src/foundry_implementation_actor/engine.py +18 -1
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/tests/test_engine.py +41 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/uv.lock +1 -1
- foundry_implementation_actor-0.5.0/.github/workflows/release.yml +0 -106
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/.github/workflows/ci.yml +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/.gitignore +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/adr/ADR-FIA-0001-the-machinery-leaves-the-capability.md +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/adr/ADR-FIA-0002-testing-leaves-this-actors-contract.md +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/adr/ADR-FIA-0003-the-dev-test-agreement-has-no-home-yet.md +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/adr/ADR-FIA-0005-the-actor-ships-an-image.md +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/adr/template.md +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/docker/Dockerfile +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/examples/ACME.PARTS.CAP.SUP.007.WID-implementation/actor-agentic-context.yaml +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/scripts/probe_grounding.py +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/src/foundry_implementation_actor/__init__.py +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/src/foundry_implementation_actor/cards/actor-data.yaml +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/src/foundry_implementation_actor/cards/actor-message.yaml +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/src/foundry_implementation_actor/cards/actor-synchronous-messaging.yaml +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/src/foundry_implementation_actor/cards/actor.yaml +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/src/foundry_implementation_actor/cli.py +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/src/foundry_implementation_actor/config.py +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/src/foundry_implementation_actor/conformance.py +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/src/foundry_implementation_actor/correlation.py +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/src/foundry_implementation_actor/grounding.py +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/src/foundry_implementation_actor/handler.py +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/src/foundry_implementation_actor/instance.py +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/src/foundry_implementation_actor/schemas/agentic-context.schema.yaml +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/src/foundry_implementation_actor/serve.py +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/tests/conftest.py +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/tests/fixtures/broken/actor-agentic-context.yaml +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/tests/test_cards.py +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/tests/test_cli.py +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/tests/test_config.py +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/tests/test_conformance.py +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/tests/test_grounding.py +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/tests/test_handler.py +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/tests/test_instance.py +0 -0
- {foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/tests/test_portability.py +0 -0
|
@@ -0,0 +1,210 @@
|
|
|
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.5.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-implementation-actor lint examples/ACME.PARTS.CAP.SUP.007.WID-implementation
|
|
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_implementation_actor import cards_path; print(cards_path())')"
|
|
62
|
+
|
|
63
|
+
- name: grounding renders a CLAUDE.md whose imports resolve
|
|
64
|
+
# BEFORE the upload, not after. A wheel that ships without its schema, or whose grounding
|
|
65
|
+
# emits an @-import to a file it never wrote, produces sessions grounded in nothing that
|
|
66
|
+
# look identical to correct ones. That is not something to discover from a published
|
|
67
|
+
# version.
|
|
68
|
+
run: /tmp/probe/bin/python scripts/probe_grounding.py examples/ACME.PARTS.CAP.SUP.007.WID-implementation
|
|
69
|
+
|
|
70
|
+
- name: publish to PyPI
|
|
71
|
+
run: uv publish --trusted-publishing always
|
|
72
|
+
|
|
73
|
+
# ── the image, from the same tag and the same checkout ────────────────────────────────────────
|
|
74
|
+
#
|
|
75
|
+
# TWO ARTIFACTS, ONE RELEASE (ADR-FIA-0005). A use pins an image; an embedder pins a wheel; both
|
|
76
|
+
# are this package at this version. A tag that produced one and not the other leaves a consumer
|
|
77
|
+
# unable to take the release at all — so this job builds the wheel again from the same checkout
|
|
78
|
+
# rather than installing the one just uploaded: the image must not depend on PyPI having already
|
|
79
|
+
# indexed it, and must not be able to pick up a different build of the same version.
|
|
80
|
+
#
|
|
81
|
+
# It runs AFTER the wheel is published, not beside it. A failed upload should not leave an image
|
|
82
|
+
# published for a version that does not exist on PyPI; the reverse — a wheel with the image still
|
|
83
|
+
# to come — is recoverable by re-running this job alone.
|
|
84
|
+
image:
|
|
85
|
+
needs: publish
|
|
86
|
+
# `needs` still orders this after the wheel on a tag push. On a manual backfill `publish` is
|
|
87
|
+
# skipped, and a skipped dependency skips its dependents too unless `always()` says otherwise —
|
|
88
|
+
# so the condition has to name both cases rather than relying on `needs` alone.
|
|
89
|
+
if: always() && (needs.publish.result == 'success' || github.event_name == 'workflow_dispatch')
|
|
90
|
+
runs-on: ubuntu-latest
|
|
91
|
+
steps:
|
|
92
|
+
- uses: actions/checkout@v4
|
|
93
|
+
with:
|
|
94
|
+
# Empty on a tag push, which is `checkout`'s own default — the ref that triggered the
|
|
95
|
+
# run. On a manual backfill it is the tag being republished, so every step below reads
|
|
96
|
+
# that tag's own tree rather than whatever the default branch has moved on to.
|
|
97
|
+
ref: ${{ inputs.tag }}
|
|
98
|
+
|
|
99
|
+
- uses: astral-sh/setup-uv@v5
|
|
100
|
+
|
|
101
|
+
- name: the version is the tag
|
|
102
|
+
# The tag is what a consumer writes in a FROM line. A tag that disagrees with
|
|
103
|
+
# pyproject.toml would publish an image whose own `version()` contradicts its name, which
|
|
104
|
+
# is exactly the confusion the CI gate above exists to prevent for the wheel. On a backfill
|
|
105
|
+
# this compares the named tag against the `pyproject.toml` checked out FROM that tag, so a
|
|
106
|
+
# typo in the input is refused rather than published under the wrong name.
|
|
107
|
+
id: version
|
|
108
|
+
env:
|
|
109
|
+
TAG: ${{ inputs.tag || github.ref_name }}
|
|
110
|
+
run: |
|
|
111
|
+
tag="${TAG#v}"
|
|
112
|
+
declared="$(uv run --quiet python -c 'import tomllib,pathlib; print(tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["version"])')"
|
|
113
|
+
[ "$tag" = "$declared" ] || { echo "::error::tag $TAG does not match pyproject version $declared"; exit 1; }
|
|
114
|
+
echo "version=$tag" >> "$GITHUB_OUTPUT"
|
|
115
|
+
|
|
116
|
+
- name: build
|
|
117
|
+
run: uv build
|
|
118
|
+
|
|
119
|
+
- uses: docker/login-action@v3
|
|
120
|
+
with:
|
|
121
|
+
registry: ghcr.io
|
|
122
|
+
username: ${{ github.actor }}
|
|
123
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
124
|
+
|
|
125
|
+
- name: the image, tagged and latest
|
|
126
|
+
env:
|
|
127
|
+
VERSION: ${{ steps.version.outputs.version }}
|
|
128
|
+
# `latest` FOLLOWS THE RELEASE STREAM, NOT A BACKFILL. A manual run exists to give an
|
|
129
|
+
# already-released version an image in a registry it missed; run against an older tag it
|
|
130
|
+
# would otherwise drag `latest` backwards onto it, silently, in whichever registry a
|
|
131
|
+
# consumer happens to pull from. The version tag is the whole point of a backfill —
|
|
132
|
+
# `latest` already names something newer, and stays there.
|
|
133
|
+
MOVE_LATEST: ${{ github.event_name == 'push' }}
|
|
134
|
+
run: |
|
|
135
|
+
image="ghcr.io/${{ github.repository }}"
|
|
136
|
+
docker build -f docker/Dockerfile -t "$image:$VERSION" -t "$image:latest" .
|
|
137
|
+
docker push "$image:$VERSION"
|
|
138
|
+
if [ "$MOVE_LATEST" = "true" ]; then docker push "$image:latest"; fi
|
|
139
|
+
|
|
140
|
+
# ── the product's own registry ────────────────────────────────────────────────────────────
|
|
141
|
+
#
|
|
142
|
+
# TWO REGISTRIES, ONE IMAGE. GHCR is where this package publishes; a product that RUNS uses
|
|
143
|
+
# of it pulls from its own registry, which its cluster already authenticates to and which a
|
|
144
|
+
# `FROM` line inside that cluster's builder can reach without a second credential. The base
|
|
145
|
+
# image has to exist in both, because the two are reached by different things: an embedder
|
|
146
|
+
# writing a Dockerfile reads the README and pins GHCR, while a use built by an in-cluster
|
|
147
|
+
# builder resolves its `FROM` against the registry that builder has a push token for.
|
|
148
|
+
#
|
|
149
|
+
# THE SAME IMAGE, NOT ANOTHER BUILD. `docker tag` re-points the build the step above already
|
|
150
|
+
# pushed, so both registries hold one digest. Building a second time would let them diverge —
|
|
151
|
+
# a base-image refresh between two steps of one job is enough — and nothing downstream would
|
|
152
|
+
# ever notice that `:0.5.0` meant two different things depending on where you pulled it.
|
|
153
|
+
#
|
|
154
|
+
# ONE VARIABLE NAMES THE WHOLE TARGET. `vars.PRODUCT_IMAGE` is the full repository path, e.g.
|
|
155
|
+
# `<name>.azurecr.io/<product>/foundry-implementation-actor`; the login server is its first
|
|
156
|
+
# segment. This package is generic across capabilities and names no product of its own — the
|
|
157
|
+
# same rule `tests/test_portability.py` enforces over `src/` — so the product that hosts a
|
|
158
|
+
# registry names itself in a repository variable rather than in this file.
|
|
159
|
+
#
|
|
160
|
+
# UNSET IS A VALID STATE, AND IT SAYS SO. A fork, or this repo before its credentials were
|
|
161
|
+
# added, publishes to GHCR alone; that is a `::notice`, not a failure. Configured-but-broken
|
|
162
|
+
# IS a failure — the push below is not best-effort, because an image the product's cluster
|
|
163
|
+
# cannot pull is the whole reason this block exists.
|
|
164
|
+
- name: is a product registry configured?
|
|
165
|
+
id: product
|
|
166
|
+
env:
|
|
167
|
+
PRODUCT_IMAGE: ${{ vars.PRODUCT_IMAGE }}
|
|
168
|
+
run: |
|
|
169
|
+
if [ -z "$PRODUCT_IMAGE" ]; then
|
|
170
|
+
echo "::notice::PRODUCT_IMAGE is unset — publishing to GHCR only"
|
|
171
|
+
echo "configured=false" >> "$GITHUB_OUTPUT"
|
|
172
|
+
exit 0
|
|
173
|
+
fi
|
|
174
|
+
case "$PRODUCT_IMAGE" in
|
|
175
|
+
*/*/*) : ;;
|
|
176
|
+
*) echo "::error::PRODUCT_IMAGE must be <registry>/<path>/<repository>, got $PRODUCT_IMAGE"; exit 1 ;;
|
|
177
|
+
esac
|
|
178
|
+
echo "server=${PRODUCT_IMAGE%%/*}" >> "$GITHUB_OUTPUT"
|
|
179
|
+
echo "configured=true" >> "$GITHUB_OUTPUT"
|
|
180
|
+
|
|
181
|
+
- uses: docker/login-action@v3
|
|
182
|
+
if: steps.product.outputs.configured == 'true'
|
|
183
|
+
with:
|
|
184
|
+
registry: ${{ steps.product.outputs.server }}
|
|
185
|
+
username: ${{ secrets.ACR_PUSH_USERNAME }}
|
|
186
|
+
password: ${{ secrets.ACR_PUSH_PASSWORD }}
|
|
187
|
+
|
|
188
|
+
- name: the same image, in the product's registry
|
|
189
|
+
if: steps.product.outputs.configured == 'true'
|
|
190
|
+
env:
|
|
191
|
+
PRODUCT_IMAGE: ${{ vars.PRODUCT_IMAGE }}
|
|
192
|
+
VERSION: ${{ steps.version.outputs.version }}
|
|
193
|
+
MOVE_LATEST: ${{ github.event_name == 'push' }}
|
|
194
|
+
run: |
|
|
195
|
+
ghcr="ghcr.io/${{ github.repository }}"
|
|
196
|
+
docker tag "$ghcr:$VERSION" "$PRODUCT_IMAGE:$VERSION"
|
|
197
|
+
docker push "$PRODUCT_IMAGE:$VERSION"
|
|
198
|
+
if [ "$MOVE_LATEST" = "true" ]; then
|
|
199
|
+
docker tag "$ghcr:latest" "$PRODUCT_IMAGE:latest"
|
|
200
|
+
docker push "$PRODUCT_IMAGE:latest"
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
- name: a use built on it is still an actor
|
|
204
|
+
# BEFORE the image is something anyone can pin — the same reason the grounding probe runs
|
|
205
|
+
# before the upload above. An image that builds but produces a use that does not conform is
|
|
206
|
+
# not something to discover from a published tag.
|
|
207
|
+
run: |
|
|
208
|
+
docker build --build-arg ACTOR_IMAGE=ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }} \
|
|
209
|
+
-t use:release examples/ACME.PARTS.CAP.SUP.007.WID-implementation
|
|
210
|
+
docker run --rm use:release foundry-implementation-actor lint /actor
|
|
@@ -88,6 +88,11 @@ Beside them, two folders of committed contract, both shipped in the wheel:
|
|
|
88
88
|
- **The clone is full, never `--depth 1`.** `papeete_version.compute()` runs `git describe --tags`
|
|
89
89
|
against it and needs the matching tag's commit reachable.
|
|
90
90
|
- **The generated `CLAUDE.md` appends** to one the repo already commits. Never overwrite.
|
|
91
|
+
- **The image is one build in two registries.** GHCR is where this package publishes; a product's
|
|
92
|
+
own registry is where the in-cluster builder that resolves a use's `FROM` line can actually
|
|
93
|
+
authenticate (ADR-FIA-0006). `release.yml` `docker tag`s one build into both so they hold one
|
|
94
|
+
digest — never add a second `docker build`, and never hardcode a registry: the product names
|
|
95
|
+
itself in `vars.PRODUCT_IMAGE`, the same reason `src/` names no capability.
|
|
91
96
|
- **Never set `ANTHROPIC_API_KEY` in a container running this.** In `claude -p` non-interactive
|
|
92
97
|
mode an API key in the environment is always preferred over `CLAUDE_CODE_OAUTH_TOKEN`, silently
|
|
93
98
|
routing every session through metered billing. There is no warning; the only symptom is the bill.
|
|
@@ -104,7 +109,12 @@ similar weight rather than only writing it into code comments.
|
|
|
104
109
|
## Releasing
|
|
105
110
|
|
|
106
111
|
Tag-triggered (`v*`) via `.github/workflows/release.yml`, publishing to PyPI through Trusted
|
|
107
|
-
Publishing (OIDC) — no stored token. The
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
Publishing (OIDC) — no stored token. The image goes to GHCR and, when `vars.PRODUCT_IMAGE` names
|
|
113
|
+
one, to a product's own registry as well (ADR-FIA-0006). The workflow also accepts a manual run
|
|
114
|
+
that takes a tag as an INPUT — dispatched from the default branch, because the file comes from the
|
|
115
|
+
dispatched ref and a tag needing a backfill predates the workflow that can do it — to give an
|
|
116
|
+
already-released version an image in a registry it missed; that run skips PyPI and does not move
|
|
117
|
+
`latest`. The release job builds the wheel, installs it into
|
|
118
|
+
a throwaway venv, and renders a `CLAUDE.md` from a fixture sidecar, asserting its `@`-imports
|
|
119
|
+
resolve, before publishing. `ci.yml` runs the suite plus two gates — *the wheel must carry its
|
|
120
|
+
contract* and *the gate must run* — and reaches nothing outside its own checkout.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: foundry-implementation-actor
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: Runs a headless Claude Code implementation session against one capability's own repo — a papeete-actor for one use, with the capability supplied by a sidecar.
|
|
5
5
|
Project-URL: Homepage, https://github.com/papeete-hub/foundry-implementation-actor
|
|
6
6
|
Author-email: Papeete Consulting <yoann.remy@outlook.com>
|
|
@@ -70,7 +70,7 @@ ground_in:
|
|
|
70
70
|
and, beside it, a four-line Dockerfile:
|
|
71
71
|
|
|
72
72
|
```dockerfile
|
|
73
|
-
FROM ghcr.io/papeete-hub/foundry-implementation-actor:0.5.
|
|
73
|
+
FROM ghcr.io/papeete-hub/foundry-implementation-actor:0.5.1
|
|
74
74
|
RUN pip install --no-cache-dir kpack==2.0.1 kontract==0.1.0 # what this sidecar's ground_in names
|
|
75
75
|
COPY actor-agentic-context.yaml /actor/
|
|
76
76
|
RUN foundry-implementation-actor render-cards /actor && foundry-implementation-actor lint /actor
|
|
@@ -193,7 +193,9 @@ task does not determine this"* is a legitimate answer, and it is the most useful
|
|
|
193
193
|
|
|
194
194
|
`assess-task` is a **query**, not an action — *"a promise to answer, from this actor's own state
|
|
195
195
|
and nothing invented."* It clones read-only, grounds itself exactly as the implement door does, and
|
|
196
|
-
is invoked with
|
|
196
|
+
is invoked with `--tools Read,Glob,Grep`, so `Write`, `Edit` and `Bash` are absent from the session
|
|
197
|
+
rather than merely unapproved (0.5.0 passed only `--allowedTools`, which removes nothing — see
|
|
198
|
+
ADR-FIA-0004's amendment). A door that **cannot** write
|
|
197
199
|
beats a door asked not to. It registers no handler either: with an engine and no handler,
|
|
198
200
|
`Actor.receive()` returns the judged dict as the reply, and there is nothing to contain because
|
|
199
201
|
nothing is produced.
|
|
@@ -329,6 +331,32 @@ move, not a rewrite."*
|
|
|
329
331
|
|
|
330
332
|
`adr/` records the decisions. Design rationale belongs there, not in commit messages.
|
|
331
333
|
|
|
334
|
+
## Releasing, and which registry to pin
|
|
335
|
+
|
|
336
|
+
A tag (`v*`) publishes two artifacts at one version — the wheel to PyPI, and the image to **two
|
|
337
|
+
registries** holding the same digest (ADR-FIA-0006):
|
|
338
|
+
|
|
339
|
+
| Registry | Pin it when |
|
|
340
|
+
|---|---|
|
|
341
|
+
| `ghcr.io/papeete-hub/foundry-implementation-actor` | you are writing a use's Dockerfile yourself, and build it with your own Docker |
|
|
342
|
+
| a product's own registry, `vars.PRODUCT_IMAGE` | the use is built **in-cluster**, by a builder whose one registry credential is that product's |
|
|
343
|
+
|
|
344
|
+
The second exists because `buildctl` resolves a `FROM` line client-side against the single registry
|
|
345
|
+
credential it was handed — so a use built by the cluster's shared builder can only reach the
|
|
346
|
+
product's own registry, whatever the README says. The push is skipped, with a notice, when
|
|
347
|
+
`vars.PRODUCT_IMAGE` is unset.
|
|
348
|
+
|
|
349
|
+
To give an already-released version an image in a registry it missed, run the workflow by hand from
|
|
350
|
+
the default branch with the tag as its input — it checks that tag out, and refuses it if it
|
|
351
|
+
disagrees with the `pyproject.toml` beside it:
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
gh workflow run release.yml --ref main -f tag=v0.5.0
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
That run skips PyPI (which refuses a version it already holds) and does not move `latest` in either
|
|
358
|
+
registry.
|
|
359
|
+
|
|
332
360
|
## Development
|
|
333
361
|
|
|
334
362
|
```bash
|
|
@@ -47,7 +47,7 @@ ground_in:
|
|
|
47
47
|
and, beside it, a four-line Dockerfile:
|
|
48
48
|
|
|
49
49
|
```dockerfile
|
|
50
|
-
FROM ghcr.io/papeete-hub/foundry-implementation-actor:0.5.
|
|
50
|
+
FROM ghcr.io/papeete-hub/foundry-implementation-actor:0.5.1
|
|
51
51
|
RUN pip install --no-cache-dir kpack==2.0.1 kontract==0.1.0 # what this sidecar's ground_in names
|
|
52
52
|
COPY actor-agentic-context.yaml /actor/
|
|
53
53
|
RUN foundry-implementation-actor render-cards /actor && foundry-implementation-actor lint /actor
|
|
@@ -170,7 +170,9 @@ task does not determine this"* is a legitimate answer, and it is the most useful
|
|
|
170
170
|
|
|
171
171
|
`assess-task` is a **query**, not an action — *"a promise to answer, from this actor's own state
|
|
172
172
|
and nothing invented."* It clones read-only, grounds itself exactly as the implement door does, and
|
|
173
|
-
is invoked with
|
|
173
|
+
is invoked with `--tools Read,Glob,Grep`, so `Write`, `Edit` and `Bash` are absent from the session
|
|
174
|
+
rather than merely unapproved (0.5.0 passed only `--allowedTools`, which removes nothing — see
|
|
175
|
+
ADR-FIA-0004's amendment). A door that **cannot** write
|
|
174
176
|
beats a door asked not to. It registers no handler either: with an engine and no handler,
|
|
175
177
|
`Actor.receive()` returns the judged dict as the reply, and there is nothing to contain because
|
|
176
178
|
nothing is produced.
|
|
@@ -306,6 +308,32 @@ move, not a rewrite."*
|
|
|
306
308
|
|
|
307
309
|
`adr/` records the decisions. Design rationale belongs there, not in commit messages.
|
|
308
310
|
|
|
311
|
+
## Releasing, and which registry to pin
|
|
312
|
+
|
|
313
|
+
A tag (`v*`) publishes two artifacts at one version — the wheel to PyPI, and the image to **two
|
|
314
|
+
registries** holding the same digest (ADR-FIA-0006):
|
|
315
|
+
|
|
316
|
+
| Registry | Pin it when |
|
|
317
|
+
|---|---|
|
|
318
|
+
| `ghcr.io/papeete-hub/foundry-implementation-actor` | you are writing a use's Dockerfile yourself, and build it with your own Docker |
|
|
319
|
+
| a product's own registry, `vars.PRODUCT_IMAGE` | the use is built **in-cluster**, by a builder whose one registry credential is that product's |
|
|
320
|
+
|
|
321
|
+
The second exists because `buildctl` resolves a `FROM` line client-side against the single registry
|
|
322
|
+
credential it was handed — so a use built by the cluster's shared builder can only reach the
|
|
323
|
+
product's own registry, whatever the README says. The push is skipped, with a notice, when
|
|
324
|
+
`vars.PRODUCT_IMAGE` is unset.
|
|
325
|
+
|
|
326
|
+
To give an already-released version an image in a registry it missed, run the workflow by hand from
|
|
327
|
+
the default branch with the tag as its input — it checks that tag out, and refuses it if it
|
|
328
|
+
disagrees with the `pyproject.toml` beside it:
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
gh workflow run release.yml --ref main -f tag=v0.5.0
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
That run skips PyPI (which refuses a version it already holds) and does not move `latest` in either
|
|
335
|
+
registry.
|
|
336
|
+
|
|
309
337
|
## Development
|
|
310
338
|
|
|
311
339
|
```bash
|
|
@@ -69,6 +69,13 @@ with `Read,Glob,Grep` and no `Write`, `Edit` or `Bash`. **A door that cannot wri
|
|
|
69
69
|
asked not to** — the same discipline as `handler.py`'s containment check standing behind the
|
|
70
70
|
implement door's write boundary.
|
|
71
71
|
|
|
72
|
+
*Amended in 0.5.1, after the first live run.* Up to 0.5.0 that list was passed only as
|
|
73
|
+
`--allowedTools`, which PRE-APPROVES the tools it names and removes nothing: a live assess session
|
|
74
|
+
ran Bash three times. The door was asked not to write after all. It is now also passed as
|
|
75
|
+
`--tools`, which is what takes every other built-in out of the session, and a test pins both flags.
|
|
76
|
+
The same run showed commitments coming back as prose strings ("E1: …", "E4/E5: …") that nothing
|
|
77
|
+
could attach to an expectation, so the prompt now requires one `{id, commitment}` object per entry.
|
|
78
|
+
|
|
72
79
|
**4. `acceptance_surface` is an OPTIONAL payload field on `implement-task`.** The door works
|
|
73
80
|
exactly as before without it. The DoD is what the caller asked for; the surface is what dev and
|
|
74
81
|
test agreed it means, and where the surface is more specific, it wins.
|
|
@@ -124,9 +131,11 @@ tuning, and it is a constructor keyword.
|
|
|
124
131
|
- **Expectation ids are worth more than this round.** Because each carries a stable `id`, a failing
|
|
125
132
|
verdict can name agreed expectations instead of scraped pytest lines, and `remediation_context`
|
|
126
133
|
can stop being free prose. Neither is done here.
|
|
127
|
-
- **The other two halves are specified
|
|
128
|
-
|
|
129
|
-
|
|
134
|
+
- **The other two halves are specified here and built elsewhere.** The testing actor's
|
|
135
|
+
`propose-acceptance` is `foundry-testing-actor` (ADR-FTA-0002), and the orchestrating actor's
|
|
136
|
+
round 0 is `foundry-task-orchestration-actor` (ADR-FTOA-0002), both extracted from their
|
|
137
|
+
instance repos the way ADR-FIA-0005 extracted this one. The round runs after that actor unpacks
|
|
138
|
+
its payload and before its attempt loop begins.
|
|
130
139
|
- **Not decided here:** whether the agreed surface should also be committed somewhere durable
|
|
131
140
|
rather than only passed between doors, and whether `definition_of_done` should eventually be
|
|
132
141
|
absorbed into it rather than sitting beside it.
|
foundry_implementation_actor-0.5.1/adr/ADR-FIA-0006-the-image-is-published-to-two-registries.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: ADR-FIA-0006
|
|
3
|
+
title: "The image is published to two registries, and names neither in its source"
|
|
4
|
+
status: Proposed
|
|
5
|
+
date: 2026-09-11
|
|
6
|
+
supersedes: []
|
|
7
|
+
references:
|
|
8
|
+
- ../.github/workflows/release.yml
|
|
9
|
+
- ADR-FIA-0005-the-actor-ships-an-image.md
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# ADR-FIA-0006 — The image is published to two registries, and names neither in its source
|
|
13
|
+
|
|
14
|
+
## Context
|
|
15
|
+
|
|
16
|
+
ADR-FIA-0005 made the base image half of what a release is: a use is one sidecar and a `FROM`
|
|
17
|
+
line, so the image is not a convenience beside the wheel, it is the thing a use is built out of.
|
|
18
|
+
The first release to carry one (0.5.0) published it to GHCR, because that is the registry a
|
|
19
|
+
GitHub-hosted package gets for free and the only one the workflow had a credential for.
|
|
20
|
+
|
|
21
|
+
That is the wrong registry for the consumer that actually exists. A use is built **inside a
|
|
22
|
+
cluster**, by the shared rootless builder `papeete-platform`'s `modules/buildkit` installs, which
|
|
23
|
+
resolves a `FROM` line client-side against the registry it holds a push token for — an Azure
|
|
24
|
+
Container Registry, provisioned by that repo's `modules/acr` with scope maps over the product's own
|
|
25
|
+
repository prefixes. `buildctl` has exactly one registry credential, mounted as its
|
|
26
|
+
`$DOCKER_CONFIG/config.json`, and it is not a GHCR one. So a `FROM ghcr.io/...` line inside that
|
|
27
|
+
builder resolves against a registry it cannot authenticate to, and the failure is a pull error at
|
|
28
|
+
build time rather than anything a gate here would catch.
|
|
29
|
+
|
|
30
|
+
Meanwhile GHCR is the right registry for the other consumer: someone reading this README and
|
|
31
|
+
writing a Dockerfile on their laptop, who has a GitHub account and no relationship with any
|
|
32
|
+
product's Azure subscription.
|
|
33
|
+
|
|
34
|
+
Two consumers, reached by two different credentials. The image has to be in both.
|
|
35
|
+
|
|
36
|
+
## Decision
|
|
37
|
+
|
|
38
|
+
The release workflow's `image` job pushes **one build to two registries**:
|
|
39
|
+
|
|
40
|
+
- **GHCR**, unconditionally, at `ghcr.io/<this repo>` — unchanged.
|
|
41
|
+
- **A product's own registry**, when `vars.PRODUCT_IMAGE` names one. The variable is the full
|
|
42
|
+
repository path (`<name>.azurecr.io/<product>/foundry-implementation-actor`); the login server
|
|
43
|
+
is its first segment; `secrets.ACR_PUSH_USERNAME` / `ACR_PUSH_PASSWORD` are the scope-mapped push
|
|
44
|
+
token `modules/acr` already emits as outputs.
|
|
45
|
+
|
|
46
|
+
`docker tag` re-points the build GHCR already holds, so both registries carry **one digest**. The
|
|
47
|
+
job does not build twice.
|
|
48
|
+
|
|
49
|
+
`vars.PRODUCT_IMAGE` unset is a valid state: a fork, or this repo before its credentials existed,
|
|
50
|
+
publishes to GHCR alone and emits a `::notice` saying so. Set-but-broken is a hard failure.
|
|
51
|
+
|
|
52
|
+
The workflow also gains `workflow_dispatch`, so a version whose wheel is already on PyPI can be
|
|
53
|
+
given an image in a registry it missed without inventing a version number. It **takes the tag as an
|
|
54
|
+
input** and is run from the default branch: GitHub takes the workflow file from the ref it is
|
|
55
|
+
dispatched on, and a tag old enough to need a backfill is by definition older than the workflow
|
|
56
|
+
that can perform one — so the checkout takes its content from the named tag while the file comes
|
|
57
|
+
from the branch. On a manual run the `publish` job is skipped — PyPI refuses a version it already
|
|
58
|
+
holds — and `latest` is **not** moved in either registry, so backfilling an old tag cannot drag it
|
|
59
|
+
backwards.
|
|
60
|
+
|
|
61
|
+
## Rationale
|
|
62
|
+
|
|
63
|
+
**Why a variable and not a literal.** This package is generic across capabilities and carries no
|
|
64
|
+
product's name; `tests/test_portability.py` enforces exactly that over `src/`, and a product name
|
|
65
|
+
hardcoded in `release.yml` would be the same mistake one directory over. A product that hosts a
|
|
66
|
+
registry names itself in its own repository variable. It also makes "no product registry
|
|
67
|
+
configured" expressible rather than a broken default.
|
|
68
|
+
|
|
69
|
+
**Why one build rather than two.** Two `docker build` invocations in one job can produce two
|
|
70
|
+
different images — a base-image tag refreshed between them is enough — and nothing downstream
|
|
71
|
+
would ever notice that `:0.5.0` meant different bytes depending on where it was pulled from. That
|
|
72
|
+
is the same failure the existing gate *the image and the wheel are one release* exists to prevent,
|
|
73
|
+
one level up.
|
|
74
|
+
|
|
75
|
+
**Why not mirror instead.** ACR can import from another registry (`az acr import`), which would
|
|
76
|
+
keep one publishing path. It needs a credential for the source, which means making the GHCR package
|
|
77
|
+
public or minting a PAT for it, and it puts a second system between the tag and the image. Pushing
|
|
78
|
+
the build we already have in hand is fewer moving parts.
|
|
79
|
+
|
|
80
|
+
**Why this repo decides this at all.** Where an ecosystem's artifacts live is an `ADR-ECO-*`
|
|
81
|
+
question, and there is no ecosystem ADR about registries today. What this ADR owns is narrower and
|
|
82
|
+
squarely this repo's: that a release publishes to more than one place, and that this package names
|
|
83
|
+
none of them in its own source.
|
|
84
|
+
|
|
85
|
+
## Consequences
|
|
86
|
+
|
|
87
|
+
- **Two secrets and one variable must exist** on this repo before the product push does anything:
|
|
88
|
+
`PRODUCT_IMAGE`, `ACR_PUSH_USERNAME`, `ACR_PUSH_PASSWORD`. The credentials come from
|
|
89
|
+
`terraform output` on `papeete-platform`'s `examples/acr-local`, and they are set by a human —
|
|
90
|
+
`papeete-foundry-product/GetSecrets.sh` is TTY-only by design so no credential passes through an
|
|
91
|
+
assistant's context, and that convention holds here.
|
|
92
|
+
- **0.5.0 shipped to GHCR only.** Its image exists at
|
|
93
|
+
`ghcr.io/papeete-hub/foundry-implementation-actor:0.5.0` and nowhere else until the manual run
|
|
94
|
+
above is used to backfill it.
|
|
95
|
+
- **The token's scope map must admit the repository.** `modules/acr`'s `repository_patterns` is the
|
|
96
|
+
caller's input, and a path outside it is refused at push time with a permissions error rather
|
|
97
|
+
than at configuration time. Widening it is a `papeete-platform` change, not one here.
|
|
98
|
+
- **A rotated push token silently stops releases.** The token password is created by Terraform with
|
|
99
|
+
no expiry by default; when that changes, the secret here has to change with it. Nothing in this
|
|
100
|
+
repo can detect the drift — the first symptom is a failed release job.
|
|
101
|
+
- **Still to realize:** a use's `FROM` line points at GHCR today
|
|
102
|
+
(`examples/…/Dockerfile`, and the real use in `papeete-foundry`). Pointing it at the product
|
|
103
|
+
registry is what makes the in-cluster builder work, and it belongs in those repos rather than
|
|
104
|
+
here — this ADR only makes the image available to be pointed at.
|
|
@@ -16,6 +16,7 @@ are named or placed (`ADR-ECO-*` in
|
|
|
16
16
|
| [ADR-FIA-0003](./ADR-FIA-0003-the-dev-test-agreement-has-no-home-yet.md) | The dev↔test agreement has no home yet — and it is not this actor's to emit | Superseded by 0004 |
|
|
17
17
|
| [ADR-FIA-0004](./ADR-FIA-0004-the-three-amigos-round.md) | The three amigos round — the tester proposes, this actor answers, a human breaks the tie | Accepted |
|
|
18
18
|
| [ADR-FIA-0005](./ADR-FIA-0005-the-actor-ships-an-image.md) | The actor ships an image, and renders its own cards into it — a use is one sidecar | Proposed |
|
|
19
|
+
| [ADR-FIA-0006](./ADR-FIA-0006-the-image-is-published-to-two-registries.md) | The image is published to two registries, and names neither in its source | Proposed |
|
|
19
20
|
|
|
20
21
|
## Authoring
|
|
21
22
|
|
|
@@ -11,7 +11,14 @@
|
|
|
11
11
|
# The whole ref is one ARG, not just the tag: this repo's own CI builds the base image from the
|
|
12
12
|
# checkout and points this line at it, which is how the example is proven to build against the
|
|
13
13
|
# version being released rather than against the last one published.
|
|
14
|
-
|
|
14
|
+
#
|
|
15
|
+
# The default names the product registry rather than GHCR for one reason: it is the copy a reader
|
|
16
|
+
# can actually pull. The GHCR package is private, so an unauthenticated `docker build` here fails
|
|
17
|
+
# with a 401 and the example teaches nothing. ACR is where the pull token, the node bypass and the
|
|
18
|
+
# `acr-pull` Secret already point (ADR-FIA-0006 publishes the same digest to both). Override it if
|
|
19
|
+
# you are somewhere else:
|
|
20
|
+
# docker build --build-arg ACTOR_IMAGE=ghcr.io/papeete-hub/foundry-implementation-actor:0.5.1 .
|
|
21
|
+
ARG ACTOR_IMAGE=papeetefoundry.azurecr.io/foundry/foundry-implementation-actor:0.5.1
|
|
15
22
|
FROM ${ACTOR_IMAGE}
|
|
16
23
|
|
|
17
24
|
# THE KNOWLEDGE TOOLS THIS SIDECAR NAMES, and the one thing the base image deliberately does not
|
{foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/examples/README.md
RENAMED
|
@@ -172,6 +172,22 @@ This runs the real grounding path — fetch, write the envelopes into a clone, r
|
|
|
172
172
|
`CLAUDE.md`, then check every `@`-import resolves. A session grounded in nothing looks exactly
|
|
173
173
|
like a correctly grounded one, which is why this is a gate rather than a hope.
|
|
174
174
|
|
|
175
|
+
**4. Build it — this one needs a credential.** The three above run offline; a `docker build` has to
|
|
176
|
+
pull the base image, and both registries that carry it are private. The `ARG ACTOR_IMAGE` default
|
|
177
|
+
names the product registry, so log into that one:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
ACR_TF=../../papeete-platform/examples/acr-local # wherever modules/acr was applied
|
|
181
|
+
terraform -chdir="$ACR_TF" output -raw pull_password | docker login papeetefoundry.azurecr.io \
|
|
182
|
+
--username "$(terraform -chdir="$ACR_TF" output -raw pull_username)" --password-stdin
|
|
183
|
+
|
|
184
|
+
docker build -t acme-wid-actor ACME.PARTS.CAP.SUP.007.WID-implementation
|
|
185
|
+
docker run --rm acme-wid-actor foundry-implementation-actor lint /actor
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The read-only pull token is enough — nothing here pushes. The last line is the interesting one: it
|
|
189
|
+
lints the cards the image actually rendered, which is what a caller will be validated against.
|
|
190
|
+
|
|
175
191
|
---
|
|
176
192
|
|
|
177
193
|
## What a real request looks like
|
|
@@ -233,7 +249,8 @@ docker build -t my-actor . && docker run --rm my-actor foundry-implementation-ac
|
|
|
233
249
|
```
|
|
234
250
|
|
|
235
251
|
The last one is the interesting one: it lints the cards the image actually rendered, which is what
|
|
236
|
-
a caller will be validated against.
|
|
252
|
+
a caller will be validated against. It needs the same registry login as step 4 above — or your own
|
|
253
|
+
`--build-arg ACTOR_IMAGE=`, if you mirror the base image somewhere of your own.
|
|
237
254
|
|
|
238
255
|
**Embedding it instead.** If you need your own base image, or the actor inside a larger process,
|
|
239
256
|
install the wheel and wire four lines yourself (`assess-task` needs no entry — it is a query with
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "foundry-implementation-actor"
|
|
3
|
-
version = "0.5.
|
|
3
|
+
version = "0.5.1"
|
|
4
4
|
description = "Runs a headless Claude Code implementation session against one capability's own repo — a papeete-actor for one use, with the capability supplied by a sidecar."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -569,6 +569,18 @@ class ClaudeCodeEngine:
|
|
|
569
569
|
if schema else
|
|
570
570
|
" with `feasible` (boolean), `objections` (one entry per expectation you cannot "
|
|
571
571
|
"meet, each naming its `id`) and `commitments` (what you undertake to pin).")
|
|
572
|
+
# THE ENTRY SHAPE, said in words whether or not a schema was rendered above. The card
|
|
573
|
+
# types both lists as `list` and stops there, so a rendered schema says nothing about
|
|
574
|
+
# what goes inside — and a live session, left to choose, answered commitments as
|
|
575
|
+
# prose strings prefixed "E1: …", which the orchestrating actor cannot attach to the
|
|
576
|
+
# expectation they name. One object per entry, keyed by the expectation's own `id`.
|
|
577
|
+
+ "\n\nWrite each entry as an object keyed by the expectation it is about:\n"
|
|
578
|
+
"- `objections`: `{\"id\": \"<expectation id>\", \"reason\": \"...\", "
|
|
579
|
+
"\"counter_proposal\": \"...\"}` (omit `counter_proposal` when you have none).\n"
|
|
580
|
+
"- `commitments`: `{\"id\": \"<expectation id>\", \"commitment\": \"<the exact "
|
|
581
|
+
"value you will pin, and where>\"}` — one entry per expectation, never several ids in "
|
|
582
|
+
"one entry and never a bare string. A commitment that concerns no single expectation "
|
|
583
|
+
"takes `\"id\": null`."
|
|
572
584
|
)
|
|
573
585
|
return "\n\n".join(sections)
|
|
574
586
|
|
|
@@ -595,9 +607,14 @@ class ClaudeCodeEngine:
|
|
|
595
607
|
self.claude_bin, "--print", "--output-format", "stream-json", "--verbose",
|
|
596
608
|
"--append-system-prompt", system,
|
|
597
609
|
"--permission-mode", "acceptEdits",
|
|
598
|
-
#
|
|
610
|
+
# `--tools`, not only `--allowedTools`. The second merely PRE-APPROVES the tools it names;
|
|
611
|
+
# every other built-in stays available, and a live assess session was seen reaching
|
|
612
|
+
# for Bash through it. `--tools` is what removes the rest from the session, so a door
|
|
613
|
+
# passed a list with no Write, Edit or Bash in it genuinely has none. That is the
|
|
599
614
|
# enforcement, not the prompt's own "you are reading only" — the same discipline as
|
|
600
615
|
# handler.py's containment check standing behind the implement door's write boundary.
|
|
616
|
+
# Both are variadic: each is followed by another option, never by the prompt.
|
|
617
|
+
"--tools", allowed_tools,
|
|
601
618
|
"--allowedTools", allowed_tools,
|
|
602
619
|
"--max-turns", str(max_turns),
|
|
603
620
|
situational_prompt,
|
{foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/tests/test_engine.py
RENAMED
|
@@ -7,6 +7,7 @@ hardcoded, and that no line it emits can exceed the budget Loki rejects outright
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import json
|
|
10
|
+
import sys
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
|
|
12
13
|
import pytest
|
|
@@ -190,6 +191,18 @@ def test_the_answers_shape_is_the_cards_own_when_one_is_given(engine):
|
|
|
190
191
|
assert "`feasible` (boolean)" in without
|
|
191
192
|
|
|
192
193
|
|
|
194
|
+
def test_every_entry_is_keyed_by_the_expectation_it_is_about(engine):
|
|
195
|
+
"""A live assess session answered commitments as prose strings ("E1: …", "E4/E5: …"), which
|
|
196
|
+
the orchestrating actor could not attach to anything. The card types the lists and no more,
|
|
197
|
+
so the shape is said in words — with a schema rendered and without."""
|
|
198
|
+
schema = {"properties": {"feasible": {"type": "boolean"}}, "required": ["feasible"]}
|
|
199
|
+
for prompt in (engine._assessment_prompt(PAYLOAD, schema),
|
|
200
|
+
engine._assessment_prompt(PAYLOAD, None)):
|
|
201
|
+
assert '`{"id": "<expectation id>", "commitment":' in prompt
|
|
202
|
+
assert '`{"id": "<expectation id>", "reason":' in prompt
|
|
203
|
+
assert "never a bare string" in prompt
|
|
204
|
+
|
|
205
|
+
|
|
193
206
|
def test_an_empty_surface_is_said_out_loud(engine):
|
|
194
207
|
"""A caller that proposed nothing gets asked what it expected, rather than a bare yes."""
|
|
195
208
|
assert "empty" in engine._assessment_prompt(PAYLOAD, None)
|
|
@@ -267,6 +280,34 @@ def test_the_assess_session_is_given_no_tool_that_writes(assessed):
|
|
|
267
280
|
assert IMPLEMENT_TOOLS != ASSESS_TOOLS
|
|
268
281
|
|
|
269
282
|
|
|
283
|
+
FAKE_CLAUDE = """
|
|
284
|
+
import json, sys
|
|
285
|
+
open(sys.argv[0] + ".argv", "w").write(json.dumps(sys.argv[1:]))
|
|
286
|
+
print(json.dumps({"type": "result", "subtype": "success", "is_error": False, "result": "done"}))
|
|
287
|
+
"""
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def test_the_tool_list_removes_tools_rather_than_only_approving_some(config, tmp_path,
|
|
291
|
+
monkeypatch):
|
|
292
|
+
"""`--allowedTools` only pre-approves what it names; every other built-in stays available,
|
|
293
|
+
and a live assess session reached for Bash through it. `--tools` is what takes the rest
|
|
294
|
+
away, so it has to carry the same list — and, being variadic, never sit last."""
|
|
295
|
+
script = tmp_path / "claude"
|
|
296
|
+
script.write_text(f"#!{sys.executable}\n{FAKE_CLAUDE}")
|
|
297
|
+
script.chmod(0o755)
|
|
298
|
+
monkeypatch.setenv("GIT_CONFIG_GLOBAL", str(tmp_path / "gitconfig"))
|
|
299
|
+
engine = ClaudeCodeEngine(config, github_token="ghs_fake", claude_bin=str(script))
|
|
300
|
+
|
|
301
|
+
assert engine._invoke_claude(tmp_path, "system", "THE PROMPT",
|
|
302
|
+
allowed_tools=ASSESS_TOOLS) == "done"
|
|
303
|
+
argv = json.loads((tmp_path / "claude.argv").read_text())
|
|
304
|
+
|
|
305
|
+
assert argv[argv.index("--tools") + 1] == ASSESS_TOOLS
|
|
306
|
+
assert argv[argv.index("--allowedTools") + 1] == ASSESS_TOOLS
|
|
307
|
+
assert argv[-1] == "THE PROMPT"
|
|
308
|
+
assert argv[argv.index("--tools") + 2].startswith("--")
|
|
309
|
+
|
|
310
|
+
|
|
270
311
|
def test_the_assess_session_gets_a_smaller_budget(assessed, engine):
|
|
271
312
|
_, seen = assessed('```json\n{"feasible": true}\n```')
|
|
272
313
|
assert seen["max_turns"] == engine.assess_max_turns < engine.max_turns
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
name: release
|
|
2
|
-
on:
|
|
3
|
-
push:
|
|
4
|
-
tags: ["v*"]
|
|
5
|
-
|
|
6
|
-
# PyPI Trusted Publishing (OIDC) — no token is stored anywhere. The one-time setup is a pending
|
|
7
|
-
# publisher on pypi.org naming this repo and this workflow; after the first release it becomes a
|
|
8
|
-
# normal publisher. See README.
|
|
9
|
-
permissions:
|
|
10
|
-
id-token: write # PyPI Trusted Publishing
|
|
11
|
-
contents: read
|
|
12
|
-
packages: write # the image, to this repo's own container registry
|
|
13
|
-
|
|
14
|
-
jobs:
|
|
15
|
-
publish:
|
|
16
|
-
runs-on: ubuntu-latest
|
|
17
|
-
environment: pypi
|
|
18
|
-
steps:
|
|
19
|
-
- uses: actions/checkout@v4
|
|
20
|
-
- uses: astral-sh/setup-uv@v5
|
|
21
|
-
|
|
22
|
-
- name: build
|
|
23
|
-
# No fetch step and no external token: the contract is committed here. A release depends
|
|
24
|
-
# on nothing but this checkout and PyPI.
|
|
25
|
-
run: uv build
|
|
26
|
-
|
|
27
|
-
- name: the wheel must carry its contract
|
|
28
|
-
run: |
|
|
29
|
-
uv venv /tmp/probe
|
|
30
|
-
uv pip install --python /tmp/probe/bin/python -q dist/*.whl
|
|
31
|
-
/tmp/probe/bin/foundry-implementation-actor lint examples/ACME.PARTS.CAP.SUP.007.WID-implementation
|
|
32
|
-
|
|
33
|
-
- name: the wheel must carry the actor's own cards
|
|
34
|
-
# Same reason as the line above, for the other half of what this package ships. The
|
|
35
|
-
# `-actor` suffix asserts a papeete-actor underneath (ADR-ECO-0022); a wheel published
|
|
36
|
-
# without its cards makes the name a claim the artifact cannot honour, and a published
|
|
37
|
-
# version is not something to discover that from.
|
|
38
|
-
run: |
|
|
39
|
-
/tmp/probe/bin/papeete-actor-synchronous-messaging lint-card \
|
|
40
|
-
"$(/tmp/probe/bin/python -c 'from foundry_implementation_actor import cards_path; print(cards_path())')"
|
|
41
|
-
|
|
42
|
-
- name: grounding renders a CLAUDE.md whose imports resolve
|
|
43
|
-
# BEFORE the upload, not after. A wheel that ships without its schema, or whose grounding
|
|
44
|
-
# emits an @-import to a file it never wrote, produces sessions grounded in nothing that
|
|
45
|
-
# look identical to correct ones. That is not something to discover from a published
|
|
46
|
-
# version.
|
|
47
|
-
run: /tmp/probe/bin/python scripts/probe_grounding.py examples/ACME.PARTS.CAP.SUP.007.WID-implementation
|
|
48
|
-
|
|
49
|
-
- name: publish to PyPI
|
|
50
|
-
run: uv publish --trusted-publishing always
|
|
51
|
-
|
|
52
|
-
# ── the image, from the same tag and the same checkout ────────────────────────────────────────
|
|
53
|
-
#
|
|
54
|
-
# TWO ARTIFACTS, ONE RELEASE (ADR-FIA-0005). A use pins an image; an embedder pins a wheel; both
|
|
55
|
-
# are this package at this version. A tag that produced one and not the other leaves a consumer
|
|
56
|
-
# unable to take the release at all — so this job builds the wheel again from the same checkout
|
|
57
|
-
# rather than installing the one just uploaded: the image must not depend on PyPI having already
|
|
58
|
-
# indexed it, and must not be able to pick up a different build of the same version.
|
|
59
|
-
#
|
|
60
|
-
# It runs AFTER the wheel is published, not beside it. A failed upload should not leave an image
|
|
61
|
-
# published for a version that does not exist on PyPI; the reverse — a wheel with the image still
|
|
62
|
-
# to come — is recoverable by re-running this job alone.
|
|
63
|
-
image:
|
|
64
|
-
needs: publish
|
|
65
|
-
runs-on: ubuntu-latest
|
|
66
|
-
steps:
|
|
67
|
-
- uses: actions/checkout@v4
|
|
68
|
-
- uses: astral-sh/setup-uv@v5
|
|
69
|
-
|
|
70
|
-
- name: the version is the tag
|
|
71
|
-
# The tag is what a consumer writes in a FROM line. A tag that disagrees with
|
|
72
|
-
# pyproject.toml would publish an image whose own `version()` contradicts its name, which
|
|
73
|
-
# is exactly the confusion the CI gate above exists to prevent for the wheel.
|
|
74
|
-
id: version
|
|
75
|
-
run: |
|
|
76
|
-
tag="${GITHUB_REF_NAME#v}"
|
|
77
|
-
declared="$(uv run --quiet python -c 'import tomllib,pathlib; print(tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["version"])')"
|
|
78
|
-
[ "$tag" = "$declared" ] || { echo "::error::tag $GITHUB_REF_NAME does not match pyproject version $declared"; exit 1; }
|
|
79
|
-
echo "version=$tag" >> "$GITHUB_OUTPUT"
|
|
80
|
-
|
|
81
|
-
- name: build
|
|
82
|
-
run: uv build
|
|
83
|
-
|
|
84
|
-
- uses: docker/login-action@v3
|
|
85
|
-
with:
|
|
86
|
-
registry: ghcr.io
|
|
87
|
-
username: ${{ github.actor }}
|
|
88
|
-
password: ${{ secrets.GITHUB_TOKEN }}
|
|
89
|
-
|
|
90
|
-
- name: the image, tagged and latest
|
|
91
|
-
run: |
|
|
92
|
-
image="ghcr.io/${{ github.repository }}"
|
|
93
|
-
docker build -f docker/Dockerfile \
|
|
94
|
-
-t "$image:${{ steps.version.outputs.version }}" \
|
|
95
|
-
-t "$image:latest" .
|
|
96
|
-
docker push "$image:${{ steps.version.outputs.version }}"
|
|
97
|
-
docker push "$image:latest"
|
|
98
|
-
|
|
99
|
-
- name: a use built on it is still an actor
|
|
100
|
-
# BEFORE the image is something anyone can pin — the same reason the grounding probe runs
|
|
101
|
-
# before the upload above. An image that builds but produces a use that does not conform is
|
|
102
|
-
# not something to discover from a published tag.
|
|
103
|
-
run: |
|
|
104
|
-
docker build --build-arg ACTOR_IMAGE=ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }} \
|
|
105
|
-
-t use:release examples/ACME.PARTS.CAP.SUP.007.WID-implementation
|
|
106
|
-
docker run --rm use:release foundry-implementation-actor lint /actor
|
{foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/.github/workflows/ci.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/scripts/probe_grounding.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/tests/test_cards.py
RENAMED
|
File without changes
|
|
File without changes
|
{foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/tests/test_config.py
RENAMED
|
File without changes
|
{foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/tests/test_conformance.py
RENAMED
|
File without changes
|
{foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/tests/test_grounding.py
RENAMED
|
File without changes
|
{foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/tests/test_handler.py
RENAMED
|
File without changes
|
{foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/tests/test_instance.py
RENAMED
|
File without changes
|
{foundry_implementation_actor-0.5.0 → foundry_implementation_actor-0.5.1}/tests/test_portability.py
RENAMED
|
File without changes
|