The Prerequisites table in examples/integrations/agentcore/README.md picked up
a uv row whose URL is wider than the existing column padding, leaving the table
unaligned against oxfmt (the repo formatter covers .md — see the lint-fix glob
in lefthook.yml). Re-run oxfmt --write on that one file.
locals.tf fails tofu fmt on a pre-existing misalignment in the Lambda
source-path block that this branch did not introduce; since the file is already
in this branch's diff, align it here so every changed .tf file passes
tofu fmt -check. Scoped to locals.tf only — modules/backend/copilotkit_runtime.tf
has the same class of debt but is out of this diff and is left alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`examples/integrations/agentcore/.dockerignore` covered only cdk.out,
node_modules, __pycache__, *.pyc and the two venv layouts. Everything else a
developer generates in this tree — the Terraform provider cache, tfstate,
terraform.tfvars, config.yaml, docker/.env, generated aws-exports.json, the
Vite build output, amplify-deploy.zip, egg-info — was uploaded to the daemon on
every build.
The context is this directory and three consumers share it: the Terraform
local-exec build in infra-terraform/modules/backend/runtime.tf,
infra-terraform/scripts/build-and-push-image.sh, and the CDK DockerImageAsset in
infra-cdk/lib/backend-stack.ts.
Scope of the harm: both agent Dockerfiles COPY explicit paths and never
`COPY . .`, so none of this reached a published image layer — there is no
credential leak. The cost is context transfer on every build, and CDK asset-hash
churn: DockerImageAsset fingerprints the whole context, so an unrelated local
file change re-tags and re-pushes the image.
Measured with a throwaway `FROM alpine / COPY . /ctx` probe against a context
carrying a realistic set of local-only files (871424 KB .terraform provider
cache plus the rest):
before: 165 files, 877112 KB in-image, 897.74 MB transferred in 20.7s
after: 144 files, 2476 KB in-image, 11.26 kB transferred
The 21 dropped paths are exactly the intended ones; nothing else disappeared and
nothing was added. Both agent images then rebuilt clean with --no-cache for
linux/arm64, and `import langgraph_agent` / `import strands_agent` each printed
OK inside the resulting containers. All *.example templates survive.
The file now reaches full parity with the sibling .gitignore, and adds
**/.DS_Store (covered by the repo-root .gitignore).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
25b4fecb rewrote the Stack block of docker/.env.example to say that ./up.sh
overwrites STACK_NAME and MEMORY_ID, and that you fill them in by hand only if
you drive `docker compose` directly. That newly blessed the plain-compose path
while leaving two entries whose value position is empty and whose explanation
sits after it on the same line.
Compose's env-file parser only treats a whitespace-separated `#` as starting a
comment when it follows a NON-empty value. With nothing between `=` and the
hash, leading whitespace is stripped and the rest of the line becomes the value.
Measured, not assumed — a probe env file through `docker compose config`:
C1= # explanatory comment -> '# explanatory comment' (literal)
C2=# explanatory comment -> '# explanatory comment' (literal)
C3=value # explanatory ... -> 'value' (stripped)
C4=value# explanatory comment -> 'value# explanatory ...' (literal)
C5="value" # explanatory ... -> 'value' (stripped)
C6= # explanatory comment -> '# explanatory comment' (literal)
C7="" # explanatory comment -> '' (stripped)
So the affected set is exactly the two empty-valued lines. STACK_NAME's
`# or -st for Strands` looks like the same defect but is not: it follows a
non-empty value and is stripped correctly (case C3). It moved to its own line
for consistency, not because it was broken.
Before, against the example's real docker-compose.yml:
$ docker compose --env-file .env.example -f docker-compose.yml config
AWS_SESSION_TOKEN: '# leave blank if using long-term creds'
MEMORY_ID: '# MemoryArn last segment (after final /)'
STACK_NAME: my-copilotkit-agentcore-lg
After:
$ docker compose --env-file .env.example -f docker-compose.yml config
AWS_SESSION_TOKEN: ""
MEMORY_ID: ""
STACK_NAME: my-copilotkit-agentcore-lg
Confirmed in a real container rather than only in `config`, via a busybox
service given the same two files:
== container env BEFORE ==
P_MEMORY_ID=# MemoryArn last segment (after final /)
P_AWS_SESSION_TOKEN=# leave blank if using long-term creds
== container env AFTER ==
P_MEMORY_ID=
P_AWS_SESSION_TOKEN=
Why ./up.sh never showed it: bash and Compose disagree on these lines. `source`
of the old file yields MEMORY_ID=[] and AWS_SESSION_TOKEN=[] because bash does
treat the trailing hash as a comment, and up.sh's `set -a && source` then exports
them, where the process environment outranks the env file. Running the same old
file through Compose with those exports in place gives MEMORY_ID: "" — the bug
is invisible on the up.sh path and reachable only on the path 25b4fecb
documented.
Audited the rest of the file against the same parser: no remaining inline hash
on an assignment line, no duplicate keys, no quoting, no trailing whitespace, no
CRLF, no BOM. All eleven keys now resolve to what a reader would predict.
up.sh and docker-compose.yml needed no change. up.sh's `cp .env.example .env`
hint and its "auto-fills .env with stack outputs" header stay accurate, and its
`^KEY=.*` rewrite still matches both keys in the new layout — with the added
benefit that own-line comments survive the rewrite, where the inline ones were
destroyed by it. docker-compose.yml's "use ./up.sh instead of docker compose
directly" still holds; .env.example only says what to fill in if you don't.
up.sh's own known defects (AGENT grep/cut, unguarded config read, silent source,
hardcoded region) are deferred and untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`3df7d6764a` hardened `start_local_agent` so a pre-existing listener on 8080
could not be mistaken for the child it just spawned. That hardening never ran.
`main()`'s `--local` branch probed the port first and, on a successful TCP
connect, printed "Agent already running on localhost:8080" and skipped
`start_local_agent` entirely - the function is called from exactly one place,
the `else` of that same probe. So in the one scenario the hardening existed for,
a stranger owning 8080, the hardened code was unreachable and the tester chatted
with the stranger under a success banner, exit 0.
A bare TCP accept only establishes that *some* process is listening. It cannot
establish that the process is this example's agent. The fix removes the check
that made that inference:
- `main()` no longer probes 8080 on the start path at all. Adopting a listener
is now opt-in via `--use-running-agent`, and even then it is announced as
unverified ("did not start it and cannot verify it is an agent") rather than
as "Agent already running". The flag errors out when nothing is listening, and
argparse rejects it without `--local` instead of silently ignoring it.
- The port check moved into `start_local_agent`, before the "Starting local
agent" banner, where it now REFUSES on an occupied port instead of spawning a
child that cannot bind. Because `main()` no longer duplicates the probe, this
is the only port check on the start path, so it is genuinely reachable from
the shipped CLI - which is precisely what the previous attempt was not.
- With the pre-spawn refusal in place, the loop's `not port_already_busy` guard
became a provably-constant conjunct and was folded away. The durable half of
the earlier hardening, polling the child for liveness BEFORE looking at the
port, is unchanged and still reachable.
Same-pattern audit of the file found one more instance: `run_chat` printed
"[Completed in Xs]" purely because `invoke_agent` returned, which it also does
after an HTTP error. `invoke_agent` now returns a bool and the line reports
"[Failed in Xs]" when the exchange did not succeed. The request payload and the
streaming decoder are deliberately untouched (deferred).
Verified by driving the real `main()` via importlib against a foreign HTTP
server bound to 127.0.0.1:8080:
- pre-fix, `--local`: "Agent already running on localhost:8080" then
"Agent: I am a STRANGER on 8080, not the agentcore agent", exit 0.
- post-fix, `--local`: "Port 8080 is already accepting connections" plus how to
proceed, exit 1, nothing spawned.
- post-fix, `--local --use-running-agent`: talks to it, labelled unverified.
- post-fix, `--local --use-running-agent` with nothing listening: exit 1.
- ordinary path (port free, child really binds 8080): "Agent started
successfully", byte-identical to pre-fix output.
- child exits 1 with no listener: still caught in ~1s, not 30s.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The startup wait loop in start_local_agent checked the port before polling
the child, so any process already listening on 8080 satisfied the port check
on the very first iteration. The function printed "Agent started
successfully" and returned while the real child was dying, and the caller
then chatted with the impostor - the exact failure the fail-fast poll() was
added to surface.
Two changes, because reordering alone is not enough: on the first iteration a
doomed child has not exited yet, so a pre-existing listener would still be
mistaken for it.
- poll() now runs before the port check, so an already-exited child is always
reported with its exit code instead of being masked.
- Port ownership is snapshotted before the spawn (check_port_available()
returns True when the port is OCCUPIED, despite its name). When 8080 was
already busy, an open port is no longer accepted as proof this child is
serving; the child that cannot bind will exit and be reported with its real
exit code, and the timeout message names the port conflict.
Fully attributing a listener to a specific child would need a readiness
signal from the agent itself (identity endpoint or handshake); refusing to
trust a pre-existing listener is the smaller change that keeps the reported
outcome truthful.
Verified by driving the real function via importlib with a shimmed child:
- foreign listener on 8080 + child exits 1: was "Agent started successfully"
(returned a process that was dead 0.5s later), now "Agent exited with code 1
before port 8080 opened" and exit 1.
- port free + child really binds 8080: still "Agent started successfully" and
the live process is returned.
- port free + child exits 1: still caught in ~1s, no 30s timeout burn.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The AgentCore example states its Python-tooling and deploy contract in six
places — two READMEs, four script self-docs, terraform.tfvars.example, the
Terraform variable descriptions and .gitignore. There is only one contract, but
each of the last three review rounds corrected a single copy of it, so the
copies drifted apart and now contradict each other. This pass reconciles all of
them against measured behaviour instead of patching one more surface.
What the contract actually is, verified by running each command:
- test-agent.py imports boto3/requests/colorama, so it runs under uv with no
--project flag. `uv run` resolves the script path against the shell's cwd, not
the project root, so `--project ..` is redundant, not required: both forms load
infra-terraform/scripts/test-agent.py and both reach the same
`FileNotFoundError: 'terraform'`. The script's Usage block claimed the flag was
needed; it no longer does.
- deploy-frontend.py (Terraform) is standard-library only with a 3.8 floor, so uv
is optional. `uv run --no-project` and plain `python3` stop identically at
"terraform is not installed". Its "Requires: uv" line said otherwise.
- That same script cannot succeed at all. It requires a Terraform output named
feedback_api_url; no root or module outputs.tf declares one (only an SSM
parameter of that name). Fed the exact output set that outputs.tf does declare,
it exits 1 at "Missing required Terraform outputs: feedback_api_url" before any
build or upload. The README documented it as the working path for a Terraform
deployment; it now says what happens and points at infra-cdk. Repairing the
script or declaring the output is tracked separately.
- agents/ holds two uv projects plus agents/utils/, which both Dockerfiles COPY
in and which has no pyproject.toml or lockfile. "Each agent is its own uv
project" overstated the guarantee.
- docker mode needs Docker running but no separate build step: the apply's
docker_build_push provisioner builds and pushes ARM64 before the runtime
resource, which depends_on it. tfvars.example prescribed
apply -> build script -> apply, contradicting the build script's own header.
- .gitignore covered .venv/ but not venv/, the third and last surface of a guard
.dockerignore and the Terraform image-hash filter already cover. A real
UV_PROJECT_ENVIRONMENT=venv sync produced 2329 committable files (30MB); it is
now ignored, matching the other two.
Also corrected while auditing every command, path, prerequisite and tool version
in the same tree: the frontend is Vite, not Next.js; the CDK tester reads
config.yaml at the example root, not infra-cdk/config.yaml; the CDK frontend
deployer's floor is 3.8, not 3.11, and its usage hint named a path that does not
resolve from the example root; build-and-push-image.sh resolves region from
AWS_REGION/AWS_DEFAULT_REGION/aws-config with no us-east-1 fallback; up.sh
overwrites the STACK_NAME and MEMORY_ID that .env.example told you to fill in;
and backend_pattern's "available patterns" listed two agents this example does
not ship.
Deliberately untouched, tracked elsewhere: the missing docs/ directory and its
links, "Node.js 18+", the duplicated `cd infra-cdk` teardown, the
Memory-and-Gateway-only claim, the undeclared aws_region variable (still the one
remaining README/tfvars.example disagreement), the absent teardown section, the
duplicate deploy-frontend.sh, and every code-behaviour defect in the scripts.
Verified: py_compile on all four touched Python files, bash -n on all five shell
scripts, and every documented command run from the directory its text names.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two concurrent fixes landed different answers for the same command. The README
fix measured that this script's import closure is standard-library only and that
`--no-project` runs it without creating an example-root virtualenv; the
self-documentation fix independently settled on `--project ..`, which also works
but syncs 13 packages the script never imports.
Take the README's form in both places. The point of the finding was that the two
must not disagree, so leaving them on different invocations would have reproduced
the defect.
Verified: `--help` renders the new epilog, and the command strings in
infra-terraform/README.md and scripts/deploy-frontend.py are now identical.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comment "Configure UV for container environment" sat above a six-variable
ENV block, but only three of those are uv settings. The other three are read by
completely unrelated consumers, and the comment silently claimed them:
UV_COMPILE_BYTECODE / UV_LINK_MODE / UV_NO_CACHE uv (confirmed via `uv help
sync`, which lists all three as `[env: ...]` on uv 0.9.30)
DOCKER_CONTAINER=1 bedrock_agentcore runtime
OTEL_PYTHON_LOG_CORRELATION=true opentelemetry logging
instrumentation
PYTHONUNBUFFERED=1 the CPython interpreter
DOCKER_CONTAINER is the dangerous one. In the installed tree it has two
consumers, not one:
.venv/lib/python3.13/site-packages/bedrock_agentcore/runtime/app.py:402
if os.path.exists("/.dockerenv") or os.environ.get("DOCKER_CONTAINER"):
host = "0.0.0.0" # nosec B104 - Docker needs this to expose the port
else:
host = "127.0.0.1"
.venv/lib/python3.13/site-packages/bedrock_agentcore/identity/auth.py:163
if os.getenv("DOCKER_CONTAINER") == "1":
raise ValueError("Workload access token has not been set. ...")
(Line numbers are from the langgraph image, bedrock-agentcore 1.0.6. The strands
image pins 1.2.0, where the same two checks live at app.py:450 and auth.py:284.)
Both agents reach that first path: each builds a BedrockAgentCoreApp and calls
app.run().
The hazard: a reader who trusts the header and prunes "uv config" they don't
recognise unbinds the agent from 0.0.0.0, and nothing tells them. The bind check
is an `or` against /.dockerenv, which plain `docker run` creates -- so a local
smoke test still passes. AgentCore's managed runtime has no /.dockerenv, so the
breakage appears only once deployed. The HEALTHCHECK cannot catch it either: it
reaches the server over localhost from inside the container, which a
127.0.0.1-bound server answers happily.
Split the block into three ENV instructions, each under a comment describing
what actually reads those variables, so no variable's purpose is misattributed.
Two more instances of the same pattern, fixed in both files:
- "Create non-root user" also covered the USER line beneath it, which switches
to that user rather than creating it.
- The strands file said "Copy agent code and shared utilities" above three
COPYs, one of which is tools/. Now matches its langgraph twin.
This is comment-only. No environment variable, value, or ordering changed.
Verification, both images built for linux/arm64 from context
examples/integrations/agentcore:
- `docker run --rm --platform linux/arm64 -e GATEWAY_CREDENTIAL_PROVIDER_NAME=dummy
-e AWS_DEFAULT_REGION=us-east-1 <tag> sh -c 'env | sort'` before vs after is
identical for both agents (modulo the per-container HOSTNAME).
- `docker inspect -f '{{range .Config.Env}}...'` before vs after is identical
for both agents including ordering, so the baked config is unchanged, not
merely equivalent at runtime.
- The DOCKER_CONTAINER claim was reproduced against the real code path with
/.dockerenv masked and uvicorn.run stubbed: set -> host 0.0.0.0, unset ->
host 127.0.0.1.
- The HEALTHCHECK command was run against a 127.0.0.1-bound server inside the
container and passed, confirming the failure mode is silent.
- The two Dockerfiles are byte-identical modulo the agent name.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The `patterns/` -> `agents/` rename in build-and-push-image.sh landed on the
Dockerfile path, the `ls` target and the "Available agents:" label, but left
the `||` fallback on the same `ls` saying "No patterns found". When the agent
directory is missing entirely, the user saw a header and a body that named two
different directories:
Available agents:
No patterns found
Now both say "agents".
Deliberately NOT renamed, because they are established interface names rather
than directory vocabulary:
- the `-p, --pattern` CLI flag, its `case` arm, its help text in both the
header comment and usage(), and the `PATTERN` variable it populates;
- the "Pattern:" line in the config banner, which echoes that flag's value;
- the `backend_pattern` Terraform variable this flag mirrors, which is
declared in variables.tf and consumed across modules/backend.
Verified by running, not reading:
- `bash -n` clean before and after.
- Drove the real script to the missing-Dockerfile branch with a nonexistent
`-p does-not-exist`, an explicit `-s`/`-r`, and a local stub `aws` on PATH
that answers `sts get-caller-identity` with a dummy account id. No AWS API
was contacted and no real credentials were used.
- To fire the `||` arm itself, ran the same script from a copied tree with no
`agents/` directory. Before: "Available agents:" / " No patterns found".
After: "Available agents:" / " No agents found".
- Re-ran against the real repo tree, where `ls` succeeds and lists
langgraph-single-agent and strands-single-agent, confirming the success
path is unaffected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Terraform frontend deploy script still told the reader to run
`python scripts/deploy-frontend.py`, in both its module docstring Usage
block and its argparse epilog. That contradicted two things the example
had already moved on from:
- infra-terraform/README.md documents `uv run --project ..
scripts/deploy-frontend.py`, and
- the sibling infra-terraform/scripts/test-agent.py had already had its
usage text migrated to `uv run --project .. scripts/test-agent.py`.
The whole agentcore example moved to uv; this one file's
self-documentation did not. Aligned it with the README and the sibling
rather than inventing a third convention.
Also corrected the stale prerequisite line, which is the same bug
pattern. It named "Python 3.8+" and no uv. Walking the full import
closure confirms this script imports only the standard library
(argparse, atexit, json, os, re, shutil, subprocess, sys, time,
pathlib, typing) -- it pulls in none of the example-root deps and does
not import scripts/utils.py -- so the "no external dependencies" fact
is preserved in the new wording. But the documented invocation now goes
through the example-root pyproject.toml, whose tooling project pins
`requires-python >= 3.12`, so advertising a 3.8 floor for the
documented command was wrong. The in-file `sys.version_info < (3, 8)`
guard is left alone as the direct-interpreter safety net.
Verified by running, not reading, from examples/integrations/agentcore/
infra-terraform:
- `uv run --project .. scripts/deploy-frontend.py --help` renders the
new epilog.
- `uv run --project .. scripts/deploy-frontend.py` reaches the script
and stops at "terraform is not installed" (the prerequisite loop).
- With a no-op terraform stub on PATH and every AWS credential source
removed, it gets past prerequisites and stops at "AWS credentials not
configured or invalid". No AWS API was called -- credential lookup
failed locally. No real credentials were used at any point.
- `python3 -m py_compile` on the file passes.
Confirmed empirically that `--project ..` does not change the working
directory, so the relative `scripts/...` path in the new text resolves
from infra-terraform/; the interpreter uv provisions is 3.13.2, which
satisfies the >= 3.12 floor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The infra-terraform README justified `uv run --project .. scripts/deploy-frontend.py`
with "The Python dependencies live in the example-root pyproject.toml". Both
halves of that were wrong, and the command did needless work.
What was false:
1. `infra-terraform/scripts/deploy-frontend.py` has no third-party dependencies.
Its full import closure is argparse, atexit, json, os, re, shutil, subprocess,
sys, time, pathlib and typing — all standard library, and it imports no local
module. It never touches boto3/requests/PyYAML/colorama from the example-root
`pyproject.toml`.
2. `--project ..` was not what made the root project reachable. `uv` already
discovers `examples/integrations/agentcore/pyproject.toml` by walking up from
`infra-terraform/` (it is the only pyproject.toml on that walk-up path), so
the flag was redundant even for scripts that do need those packages. What it
did add was a forced sync of the example-root `.venv` — 13 packages — before
a script that imports none of them.
The documented invocation is now `uv run --no-project scripts/deploy-frontend.py`,
with plain `python3 scripts/deploy-frontend.py` noted as equally fine.
The sibling `scripts/test-agent.py` genuinely differs — it imports boto3,
requests and colorama — so it is documented separately as plain `uv run`
(no `--no-project`, and no `--project ..` either), and the README now says so
rather than making the two scripts falsely uniform.
Verified by running, from `infra-terraform/`, with no AWS API calls:
- `uv run --project .. scripts/deploy-frontend.py --help` (old form) printed
"Creating virtual environment at: .../agentcore/.venv" and "Installed 13
packages", then ran
.../agentcore/infra-terraform/scripts/deploy-frontend.py.
- `uv run scripts/deploy-frontend.py --help` (no flag) produced the identical
venv creation, the identical 13-package install and the identical resolved
script path, confirming `--project ..` is a no-op for discovery.
- `uv run --no-project scripts/deploy-frontend.py --help` (new form) resolved the
same absolute script path and left no `.venv` at the example root at all.
- `python3 scripts/deploy-frontend.py --help` likewise ran clean with no sync.
- `uv run --no-project` on `scripts/test-agent.py` fails at
`test-agent.py line 36, in <module> import boto3` (ModuleNotFoundError), while
plain `uv run` syncs the 13 root packages and resolves its imports — which is
why the two scripts are documented differently.
Resolved script paths were captured with a runpy probe run under the very same
`uv run` invocation form, using a non-`__main__` run name so module-level
imports execute but `main()` does not.
Audited the rest of the file for the same class of checkable-and-false claim.
The remaining assertions hold: `amplify_app_id` and `amplify_staging_bucket` are
real outputs in `outputs.tf`; the example-root `scripts/deploy-frontend.py` does
use `aws cloudformation describe-stacks` and does take a stack name via
`sys.argv[1]`; `stack_name_base` and `backend_pattern` are declared in
`variables.tf`; and `--pattern` does override the `backend_pattern` value parsed
from `terraform.tfvars`. Separately-tracked gaps (undeclared `aws_region`,
missing `admin_user_email`, absent teardown section, undocumented
`build-and-push-image.sh` and duplicate `scripts/deploy-frontend.sh`) are left
untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`.dockerignore` excluded `**/.venv/` only, while its counterpart —
`venv_path_regex = "(^|/)\.?venv/"` in
infra-terraform/modules/backend/runtime.tf — deliberately covers BOTH
`.venv/` and `venv/`, the layout `uv sync` produces under a non-default
`UV_PROJECT_ENVIRONMENT=venv`. The two halves of the same guard disagreed,
and the comment above that local justified itself by claiming
`.dockerignore` already excluded the tree — which was false for `venv/`.
Consequence: with `UV_PROJECT_ENVIRONMENT=venv`, the virtualenv tree
entered the docker build context that feeds both Terraform's
`docker build` and the CDK `DockerImageAsset` hash in
infra-cdk/lib/backend-stack.ts, while Terraform's own content hash
ignored it. Compose Watch (docker/docker-compose.yml `agent` service,
context `..`) inherits the same rules.
Auditing the rest of the file surfaced the same
narrower-than-what-it-guards pattern in the CDK output rules:
`cdk.out*/` and `infra-cdk/cdk.out*/` are path-anchored, so a `cdk.out`
directory anywhere else was not excluded. Replaced both with
`**/cdk.out*/`. The path-anchored `infra-cdk/node_modules/` and
`frontend/node_modules/` lines were already subsumed by the
`**/node_modules/` line below them and were dropped for the same reason.
Deliberately NOT touched here (separate tracked finding): `.terraform/`,
`docker/.env`, `config.yaml`, `terraform.tfvars`, `*.tfstate`,
`aws-exports.json`, `.git/`, `frontend/dist`.
Verified by measurement, not by reading. Marker files were planted in
`venv/`, `.venv/`, `cdk.out/`, `cdk.out-lg/`, `infra-cdk/cdk.out/`,
`frontend/cdk.out/`, three `node_modules/` locations and
`__pycache__/`, at root, agent-package and deep-nested depths. A
throwaway `FROM busybox / COPY . /ctx` image then listed the real build
context.
before: 148 files in context, 5 of them leaked —
agents/langgraph-single-agent/venv/CTXPROBE.txt
agents/langgraph-single-agent/venv/CTXPROBE_mod.py
agents/strands-single-agent/venv/lib/python3.13/site-packages/pkg/CTXPROBE.txt
frontend/cdk.out/CTXPROBE.txt
venv/CTXPROBE.txt
after: 143 files in context, zero leaked; `comm` over the two
listings shows those 5 paths as the only difference and no
project file dropped.
`**/.venv/` still works after the edit: the root and agent-package
`.venv` markers are absent from both listings, and the after-listing
contains no `.venv/`, `venv/`, `node_modules`, `cdk.out`, `__pycache__`
or `*.pyc` path at all. The `**/` prefix was confirmed to match zero
path segments (root-level `.venv/` was excluded by `**/.venv/` before
the change), which is what makes `**/cdk.out*/` a strict superset of
the two anchored rules it replaces. All scratch directories were
removed; `git status` is clean apart from this commit.
The runtime.tf comment was re-worded to name both patterns so it is
true again; the filter logic itself is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comment sat above the langgraph/langchain pins but read as a claim that the
whole dependency set tracks examples/integrations/langgraph-python. It does not —
copilotkit and ag-ui-protocol are both behind that example. Say which pair the
statement covers.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Usage section documented a frontend deploy that could not work after a
`terraform apply`. Three defects compounded:
- The bash block ran `cd infra-terraform` and never returned, then the prose
claimed the next command ran "from the repo root" — stated cwd and actual
cwd disagreed.
- There are two `deploy-frontend.py` files. The example-root one is the CDK
variant: it reads stack outputs via `aws cloudformation describe-stacks`.
The Terraform deployment's outputs live in `terraform output -json`, which
only `infra-terraform/scripts/deploy-frontend.py` reads. Followed literally
from the repo/example root, `uv run scripts/deploy-frontend.py` executed the
CDK script.
- The CDK script also requires a stack-name argument (`sys.argv[1]`, or
`STACK_NAME`, or `infra-cdk/config.yaml` — which does not exist in this
example), and the documented invocation passed none. The deploy shell
scripts do pass it; the README did not.
The command is now `uv run --project .. scripts/deploy-frontend.py`, run from
`infra-terraform/`. `--project ..` resolves the example-root `pyproject.toml`
that owns the tooling dependencies, matching the convention already used in
`infra-terraform/scripts/test-agent.py`. The starting cwd for the whole
section is now stated, and the optional `--pattern` override (the only
argument this script accepts) is documented.
Verification, from `examples/integrations/agentcore/`:
# uv resolves the example-root project and does not change cwd
$ cd infra-terraform && uv run --project .. python -c "import os,sys; \
print(os.getcwd()); print(sys.prefix)"
.../agentcore/infra-terraform
.../agentcore/.venv
# which file each command actually executes (path + sha256 printed by a
# sitecustomize probe that exits before the script body — no AWS calls)
old, from example root: .../agentcore/scripts/deploy-frontend.py
sha256 1c3ed4f2… "…script for FAST." (CDK)
new, from infra-terraform: .../infra-terraform/scripts/deploy-frontend.py
sha256 16d3bc3c… "…for Terraform deployments."
# argument signature, real runs (terraform binary absent here, so the run
# stops at the prerequisite check before any AWS call)
$ uv run --project .. scripts/deploy-frontend.py -> exit 1, "terraform is not installed"
$ uv run --project .. scripts/deploy-frontend.py --pattern langgraph-single-agent
-> exit 1, same prereq stop
$ uv run --project .. scripts/deploy-frontend.py some-stack-name
-> exit 2, "unrecognized arguments"
The last case confirms the stack-name positional belongs to the CDK script
only. `--help` on the resolved script prints "Deploy frontend to AWS Amplify
using Terraform outputs". No AWS credentials were used and no AWS API was
called.
The known `aws_region` variable issue and the missing teardown section are
tracked separately and are untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
start_local_agent() launched the agent with stdout=PIPE and stderr=PIPE and
then never read either pipe. The child blocks the moment it fills a ~64KB
pipe buffer, so a chatty agent wedges before it can bind port 8080. Worse,
the 30-second startup-timeout branch called _agent_process.stderr.read() --
a blocking read to EOF -- on a child that was still alive, so the tester hung
forever instead of reporting the timeout.
This mattered more since the command became `uv run --locked --project ...`:
uv writes resolution and install progress to stderr before the agent starts,
and the message uv prints when uv.lock has drifted from pyproject.toml only
reached the developer through that same wedged branch.
Fix: do not pipe the child at all. stdout/stderr are inherited, so agent logs
and uv's errors stream straight to the developer's terminal (this is an
interactive tool), the child can never block on a full pipe, and no reader
threads are needed. The wait loop now also polls the child each second and
fails fast with its exit code when it dies early, instead of burning the full
30 seconds. The timeout branch delegates cleanup to stop_local_agent(), which
is now idempotent (clears the global first) so the timeout path, the SIGINT
handler and the atexit hook cannot double-stop or double-print, and kill() is
followed by wait() so the process is reaped.
Verification (standalone reproductions; this file has no test suite):
- Old code vs a child writing 8000 lines and staying alive: the child never
finished writing (deadlocked on a full pipe) and the parent hung in
stderr.read() until an external timeout killed it (exit 124).
- New code, real start_local_agent() driven against the same child: the child
wrote all 8000 lines, the parent hit its timeout, stopped the agent and
exited 1 after 30.1s.
- Real uv project with a stale uv.lock: the developer sees
"error: The lockfile at `uv.lock` needs to be updated, but `--locked` was
provided." live on the terminal, and the tester reports
"Agent exited with code 2 before port 8080 opened" after 1.0s.
- python3 -m py_compile passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Several packages that agentcore code imports at module scope were never
declared in the pyproject.toml of the project that ships them. They only
resolved because something else happened to pull them in, so the next
`uv lock` that drops the intermediate would silently remove them.
That is newly dangerous: both agent Dockerfiles now install with
`uv sync --locked`, so the installed set is exactly the lockfile rather
than whatever pip incidentally resolved. A dropped transitive would turn
into an ImportError at container start instead of a quiet near-miss.
Undeclared but directly imported:
- boto3 — `agents/utils/ssm.py:12`. `agents/utils/` is COPY'd into BOTH
agent images, so both agent projects need it; neither declared it.
- PyJWT — `agents/utils/auth.py:11`. strands declared it, langgraph did
not and resolved it transitively only. langgraph now matches strands
(`PyJWT[crypto]>=2.10.1`) since it is the same shared module.
- langchain-core — `tools/todos.py:10` imports `langchain_core.messages`
in the langgraph agent; it rode in on `langchain`.
- botocore — `scripts/utils.py:17` imports `botocore.exceptions`; the
example-root project declared boto3 but not botocore.
Floors are set at or below what the existing lockfiles already resolve,
so nothing is bumped. The lock diffs are additive metadata only: zero
resolved versions changed and no new packages entered any lock.
Deliberately not declared: `docker/resolve-env.py` (boto3, PyYAML) is
already covered by the root project; `infra-cdk/lambdas/oauth2-provider/`
uses boto3 from the Lambda runtime and is bundled by CDK, not by any of
these three uv projects.
Verification (run, not read):
$ docker build --platform linux/arm64 \
-f agents/langgraph-single-agent/Dockerfile -t acuv-lg-a2:test .
naming to docker.io/library/acuv-lg-a2:test done
$ docker build --platform linux/arm64 \
-f agents/strands-single-agent/Dockerfile -t acuv-st-a2:test .
naming to docker.io/library/acuv-st-a2:test done
$ docker run --rm --platform linux/arm64 \
-e GATEWAY_CREDENTIAL_PROVIDER_NAME=dummy -e AWS_DEFAULT_REGION=us-east-1 \
acuv-lg-a2:test sh -c 'python -c "import langgraph_agent, boto3, jwt, langchain_core, utils.ssm, utils.auth, tools; ..."'
OK lg 1.43.78 2.13.0 1.6.0
$ docker run --rm --platform linux/arm64 ... acuv-st-a2:test \
sh -c 'python -c "import strands_agent, boto3, jwt, utils.ssm, utils.auth, tools; ..."'
OK st 1.43.78 2.13.0
$ uv run --locked scripts/test-agent.py --help # exit 0, usage printed
$ uv lock --check # passes for all three projects (14 / 144 / 123 packages)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
terraform_data.docker_image_hash hashed every `**/*.py` under
`local.pattern_dir`. Since the example moved to per-agent uv projects,
that directory grows a `.venv/` the moment a developer runs `uv sync` or
the local agent tester, and the virtualenv's dependency sources were
being folded into the hash.
That hash feeds null_resource.docker_build_push.triggers and the
runtime's replace_triggered_by, so a developer who has ever run the
agent locally got a spurious image rebuild plus a forced AgentCore
runtime replacement, and two developers produced different plans from
identical committed sources. The virtualenv never reaches the image
anyway — .dockerignore excludes `**/.venv/` — so it must not reach the
image hash either.
fileset() has no exclude argument, so both comprehensions (pattern dir
and shared utils dir) now filter on local.venv_path_regex,
`(^|/)\.?venv/`, which drops `.venv/` and `venv/` at any depth while
keeping files that merely start or end with those characters
(`tools/venv_helpers.py`, `myvenv/x.py`).
Verified by measurement, not by reading — Terraform is not installed, so
the expression was reproduced exactly (fileset `**/*.py` including
dot-directories, lexicographic set order, sha256 over the joined
filesha256 digests) and evaluated against two worktrees holding
byte-identical agent sources, one with a real 167 MB `.venv` present and
one without:
OLD with .venv: fileset 3114, hashed 3114 -> 5f9a98ef...
OLD without: fileset 5, hashed 5 -> bacab1e1... (DIFFER)
NEW with .venv: fileset 3114, hashed 5 -> bacab1e1...
NEW without: fileset 5, hashed 5 -> bacab1e1... (IDENTICAL)
The two OLD digests reproduce the reported values exactly, and the NEW
value equals the clean-checkout digest, so the fix introduces no new
hash and therefore no replacement for existing state. All 3109 dropped
paths were under `.venv/`; the 5 kept are the real agent sources. A
synthetic fixture additionally confirmed the shared-utils comprehension
and the non-dot `venv/` (UV_PROJECT_ENVIRONMENT) variant are covered.
Audit: these were the only fileset/filesha256 expressions in runtime.tf.
The known undeclared-resource references in this file are untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two type errors the example could not see. It has no check-types target,
and the import is type-only, so esbuild erases it and the build passes.
- `CopilotKitCoreFriendsAccess` came from `@copilotkit/core`, which the
example does not depend on. `react-core/v2` re-exports it.
- `onError` also accepts React's DOM error handler, so the parameter is a
union and reading `.error` / `.context` off it is not allowed. Narrow on
the CopilotKit shape first; a synthetic DOM event has nothing to report.
Example type errors go from 9 to 3. The three left are older: two react
-router codegen paths and one model name.
Prerequisites listed "Python 3.8+", which no longer matches anything: the agents
require 3.13 and uv provisions the interpreter, so uv is the only thing a reader
needs to install. Adds a short section on where dependencies live and the
obligation to commit uv.lock, and updates the Terraform README's frontend deploy
command to match.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Terraform docker-mode image hash read `patterns/<pattern>/requirements.txt`,
`patterns/utils`, a root-level `gateway/` and `tools/`, and a root `pyproject.toml`
— none of which exist in this example. `filesha256` on a missing file is a plan-time
error, so docker mode could not plan at all, and the same `patterns/` prefix was
baked into the build command and the standalone build-and-push script.
Point them at `agents/<pattern>` and `agents/utils`, and hash the agent's
`pyproject.toml` and `uv.lock` now that the dependency set is locked, so a
dependency bump retriggers the image build.
Zip mode is left alone; it references a `basic_agent.py` entry point and a
`lambdas/zip-packager` directory that are also absent, which is a separate
problem from the uv conversion.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The deploy and local-dev scripts called bare `python3` and relied on the caller
already having PyYAML, requests, boto3 and colorama importable. `scripts/requirements.txt`
listed them but nothing installed it, and `uv run scripts/test-agent.py` — the
command the script's own docstring gives — failed because there was no project
for uv to resolve against.
Route every Python entry point through `uv run --project`, backed by the
example-root project added in the previous commit, and drop the orphaned
`requirements.txt`. Preflight now checks for `uv` rather than `python3`; uv
provisions the interpreter itself, so the hand-rolled Python 3.8 version assert
goes away with it.
`test-agent.py` starts a local agent inside that agent's own uv project with
`--locked`, so a locally run agent gets the same dependency set as its image
instead of an ad-hoc resolve.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both AgentCore agents installed from an unlocked `requirements.txt`, so every
image build re-resolved transitive dependencies from scratch. That had already
drifted into a broken state: `langgraph==1.0.10rc1` pulled in a langgraph-prebuilt
that reads `ExecutionInfo` off `langgraph.runtime`, which 1.0.x does not export,
so `import langgraph_agent` failed at container start.
Give each agent a `pyproject.toml` + `uv.lock` and install with `uv sync --locked`,
matching how every other Python integration example is set up. Bump langgraph to
1.1.6 and pin langchain to 1.2.15 — the pair used by
examples/integrations/langgraph-python — to resolve the import failure, and fold
the separately installed `aws-opentelemetry-distro` into the locked dependency
set so it is pinned too.
The example root also gains a `pyproject.toml` + `uv.lock` for the `scripts/`
helpers, whose dependencies were previously declared in a `requirements.txt`
that nothing installed.
Verified by building both images for linux/arm64 and importing the agent module
inside each container.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add chat phrases and lab buttons that fail a frontend tool or emit RUN_ERROR.
Clear the Break threads cookie on load so a refresh does not keep a fake
thread-list failure.
The earlier commits in this PR taught `createCopilotEndpoint` paired with
`handle` from `hono/vercel`, and added `hono` to 20 install commands with a
callout explaining why readers must install it. Both were wrong, and the second
was a consequence of the first.
`createCopilotEndpoint` is a **deprecated alias**. This repo's own handler table
says so — `docs/backend/runtime-endpoints.mdx`:
| Deprecated | Use instead |
| `createCopilotEndpoint` | `createCopilotHonoHandler` |
| `createCopilotEndpointSingleRoute` | ... with mode: "single-route" |
`createCopilotRuntimeHandler` serves the same multi-route mode (it is the
default), returns a plain fetch handler, is not deprecated, and needs **no hono
at all**. So the route collapses to:
const handler = createCopilotRuntimeHandler({
runtime,
basePath: "/api/copilotkit",
});
export const GET = handler;
export const POST = handler;
`hono` was therefore an artifact of the shape, not a requirement of the library.
The install lines and the callout are reverted; nothing tells readers to install
it any more.
## Verified with hono deleted, not merely absent from package.json
`examples/shadcn` converted to this shape, `hono` removed from its
`package.json`, and `node_modules/hono` deleted outright so a hoisted copy could
not mask the result:
GET /api/copilotkit/info -> 200
POST /api/copilotkit/agent/default/run -> 200, chat turn rendered
tsc --noEmit / eslint / next build -> clean
next build route -> ƒ /api/copilotkit/[[...slug]]
The doctest sidecar drops `hono` too, so the CI gate now typechecks the
canonical snippet against `@copilotkit/runtime` alone — proof by construction
that the snippet needs nothing else.
pnpm tsx scripts/doc-tests/run.ts -> 2 passed, 0 failed
showcase/shell-docs: typecheck -> exit 0
showcase/shell-docs: build -> exit 0
structural audit: 31/31 mdx files, fence + JSX identical to HEAD
## Also corrected
`docs/backend/custom-agent.mdx` repeated the same incorrect transport claim the
earlier commit fixed in four other places ("Both `<CopilotKit>` and
`<CopilotKitProvider>` negotiate the transport when the prop is omitted").
Corrected to match released behaviour.
## Left alone deliberately
`snippets/shared/backend/custom-agent.mdx` and `docs/backend/custom-agent.mdx`
still call `createCopilotEndpoint` in three fences each, as
`export default copilotEndpoint` — the Hono-app deployment pattern rather than a
Next.js route handler. That predates this PR, the documented replacement is
`createCopilotHonoHandler`, and I have not run that shape. Recorded as follow-up
rather than guessed at. (The two files have also drifted from each other, which
is a separate problem.)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`examples/shadcn` paired a v1, POST-only runtime route with a
`@copilotkit/react-core/v2` frontend. `GET /api/copilotkit` answered 405 and
`GET /api/copilotkit/info` did not route at all (404), so nothing could probe
the runtime.
Convert the route to the v2 multi-route shape at a catch-all path, exporting
both verbs:
app/api/copilotkit/[[...slug]]/route.ts
createCopilotEndpoint({ runtime, basePath: "/api/copilotkit" })
export const GET = handle(app)
export const POST = handle(app)
Two co-changes this shape requires, both found by running the app rather than
by reading it:
- `hono` becomes a direct dependency. It is a dependency of
`@copilotkit/runtime`, not a peer, so under pnpm's strict layout
`import { handle } from "hono/vercel"` does not resolve from the app
without declaring it.
- the provider must pass `useSingleEndpoint={false}`. The published
`<CopilotKit>` from `@copilotkit/react-core/v2` defaults to the
single-route transport, which posts a single-route envelope to the bare
basePath; a multi-route runtime answers that with 404. The runtime says so
itself in the error body.
`@copilotkit/*` moves 1.61.2 -> 1.68.3 so the app runs the versions a reader
installing today would get.
Verified live (aimock on :4010 as the model backend):
GET /api/copilotkit/info -> 200, runtime /info payload
POST /api/copilotkit/agent/default/run -> 200, chat turn renders
before: GET /api/copilotkit -> 405, GET /api/copilotkit/info -> 404
tsc --noEmit, eslint, next build all clean
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The "Sort out my offsite expenses" beat had three problems on stage: the
harness console was a black slab in a light-mode transcript, the tool
activity grew a stack of finished steps that pushed the report card off
the screen, and the run took a full minute.
Console (banking skin):
- Every colour is now a semantic token, so the pane follows the app into
dark or light instead of being hardcoded dark. It was `bg-ink` with
`text-white/45`-style overlays, which only ever looked right in one mode.
- Collapsed by default. The status strip still carries the live state, so
the run reads as alive while closed.
- Still the FULL log when open. It is the detail view, and windowing it as
well left two lines and nowhere to read the rest.
Tool activity (shell, all skins):
- Rolls to the last two lines; older ones are REMOVED, not collapsed.
- Registration is a layout effect, and that is load-bearing. A new line
renders before it is registered, and registering is what evicts the
oldest, so with a passive effect the browser painted the in-between
state: three rows for one frame on every tool call, and again when the
end-of-run MESSAGES_SNAPSHOT remounts every line at once. Measured per
animation frame over a full run: 12,734 frames, never more than two.
- A shared registry rather than something simpler because CopilotKit
renders one component per tool call and owns the container. CSS
`:nth-last-child` needs siblings and the lines had one parent each;
mount-order counters drift across the snapshot remount.
Agent (run time 1m 0s -> ~45s):
- Research is gated on the offsite dates. A charge dated outside the
window is settled by its date whatever the merchant turns out to be, so
half the researcher dispatches were buying nothing. Travel on the
adjacent days is still kept in scope.
- Filings go out in one command instead of one curl per row. The
researchers already ran concurrently, so the serial per-row round-trips
through the model were most of the wall clock, not the research.
- Fetch and verify are one command; there is nothing to decide between
the halves.
- Analyst reasoning effort defaults to medium, overridable with
BANKING_EXPENSE_EFFORT.
Filing is idempotent, which the batching made necessary: the script got
run twice and every charge was filed twice, so the report card claimed six
filings while the ledger held twelve. The script now writes `filed.json`
and exits early if it exists. That marker is cleared once per run, because
the workspace is a fixed directory shared by every run and a stale marker
would convince the next demo it had already filed and post nothing at all
— the same bug wearing the opposite mask, and a quieter one, since a run
that files nothing still writes a confident report.
Deduping server-side on merchant+amount would have been wrong: Hotel
Verrano legitimately appears twice at the identical 318.55 for the two
nights of the offsite.
Does this change make anything in .claude/skills/reskin/ wrong,
incomplete, or misleading? No. Nothing there documents the harness
console, the tool-activity renderer, or the analyst prompt; its only
matches for "console" are `console.error` in unrelated template code.
Verified on the real path against a live Intelligence stack: six rows
filed, all status=pending, stable across 60s of polling, and present in
the Pending Approval queue with their notes and approve/decline actions.
## Summary
- add an independently runnable Claude Managed Agents finance-assistant
example
- add a cookbook recipe that explains the CopilotKit runtime,
managed-session mapping, and tool rendering flow
- add the recipe to cookbook navigation, the overview grid, sidebar icon
mapping, and render coverage
- use the real Claude vector mark for the cookbook instead of the
text-placeholder SDK asset
- register the example's Vite configuration and managed-agent model with
the repository CI allowlists
- include a compact architecture diagram and links to the relevant
rendering and CopilotKit Intelligence documentation
- disable Claude's complete built-in toolset and expose only the scoped
`show_growth_projection` runtime tool
- make the provisioning model configurable through `ANTHROPIC_MODEL`,
defaulting to `claude-fable-5`
- bound CopilotKit request bodies to 256 KB and managed-agent turns to
90 seconds, while relying on the adapter's per-thread serialization
- cap public run traffic at 20 provider-like attempts per client IP per
minute and 2,000 successful starts per process per 24-hour window
- restrict browser runtime requests with an exact Origin allowlist that
supports same-origin or separately hosted frontends, and restrict iframe
parents with CSP `frame-ancestors`
- validate persisted managed-agent IDs at startup so malformed local
configuration fails immediately
- publish the interactive example on Railway and embed the live
deployment in the cookbook
- align the demo with the existing cookbook chat styling and show the
`Project monthly investing` starter on first load
## Demo

## Why
This gives developers a focused example of connecting CopilotKit to
Anthropic Claude Managed Agents without the extra surface area of a
larger analyst application. The recipe follows the existing cookbook
structure and keeps the live demo compact enough for the standard
cookbook pane. Its managed environment has no outbound network or
package-manager access, and its agent cannot use bash, filesystem,
search, or fetch tools.
The request, turn, per-IP, and process-wide limits bound public demo
traffic without adding authentication or user friction. The traffic
counters are intentionally in memory, reset on process restart, and are
not shared across replicas, so the dedicated Anthropic workspace spend
limit remains the durable cost backstop. The exact-Origin browser check
reduces drive-by use but is explicitly documented as a control rather
than authentication. The model override allows operators to select a
lower-cost supported model during provisioning without editing source
code.
## Validation
- scoped formatting: passed
- scoped lint: 0 warnings, 0 errors
- shell-docs typecheck: passed
- standalone example typecheck: passed
- docs render tests: 26/26 passed
- standalone example tests: 23/23 passed
- shell-docs tests: 375/375 passed
- shell-docs production build: passed (222/222 pages)
- standalone example production build: passed
- standalone npm lockfile validation: passed
- build-config allowlist validator: passed
- docs model-name validator: passed
- exact-Origin regression coverage for run requests plus headerless
same-origin runtime discovery: passed
- malformed persisted agent-ID regression coverage: passed
- live Railway root and iframe CSP: passed
- live Railway runtime discovery, exact welcome copy, and first-load
starter pill: passed
- live three-turn AG-UI managed-agent run with `show_growth_projection`:
passed
- cookbook verified in the browser at desktop and narrow widths with no
console errors or horizontal overflow
## What does this PR do?
Closes the naming and documentation half of
[OSS-881](https://linear.app/copilotkit/issue/OSS-881). Paired with
CopilotKit/Intelligence#890, which adds `copilotkit verify` and tightens
the evaluation rubric.
### 1. One name for the Intelligence key
**Three** names for one value were live in CopilotKit's own
documentation, and following the wrong one with a CLI-provisioned
project yields an undefined key:
| Name | Where | Code readers |
| --- | --- | --- |
| `INTELLIGENCE_API_KEY` | what `copilotkit project select` writes; all
34 integration examples; the docs site | 34 |
| `COPILOTKIT_INTELLIGENCE_API_KEY` | 7 Channels package READMEs +
packaged skills | **0** |
| `COPILOTKIT_API_KEY` | `examples/slack`, `examples/teams`, and the
TSDoc on `CopilotKitIntelligence` itself | 2 |
`INTELLIGENCE_API_KEY` wins — it is the name the CLI provisions, and
changing it would break every scaffolded project in the wild.
- `COPILOTKIT_INTELLIGENCE_API_KEY` is **retired outright**. Nothing
ever read it, so there is nothing to keep compatible.
- `COPILOTKIT_API_KEY` stays **readable as a deprecated alias** in the
two examples that consume it, so an existing `.env` keeps working, and
is documented as deprecated everywhere it appears.
The third name was the worst placed: it was in the TSDoc on
`CopilotKitIntelligence`, which is what an IDE shows on hover.
This was not only untidy. The CLI's own `channels-preflight` accepts
`INTELLIGENCE_API_KEY` or `COPILOTKIT_API_KEY` — **not**
`COPILOTKIT_INTELLIGENCE_API_KEY`, the name the Channels READMEs told
people to set. So following a Channels README verbatim made `copilotkit
channels` warn that no runtime API key was present while the key sat
visibly in `.env`. After this PR the documented name is one preflight
accepts.
> [!NOTE]
> `NEXT_PUBLIC_COPILOTKIT_API_KEY` is a **different value** — the legacy
Copilot Cloud public key — and is deliberately left alone.
### 2. A real defect, not just naming skew
`skills/runtime/references/intelligence-mode.md` documented
`organizationId` as a `CopilotKitIntelligence` option, sourced from two
further env names (`COPILOTKIT_INTELLIGENCE_ORG_ID`,
`COPILOTKIT_ORG_ID`).
`CopilotKitIntelligenceConfig` has no such field — the copy-pasteable
sample it appeared in **would not compile**. Removed from the samples,
and the prose telling readers to fetch a value for it corrected. That
file is the only place those two names ever existed, which is very
likely why the failing validation run reported that "the runtime reads
`COPILOTKIT_INTELLIGENCE_API_KEY` and `COPILOTKIT_INTELLIGENCE_ORG_ID`".
### 3. Publish the Intelligence wiring
The wiring instructions existed only inside
`node_modules/@copilotkit/runtime/skills/`, and the only docs pages
mentioning `CopilotKitIntelligence` at all were the two Channels
frontends — so a developer on the plain web path had no page to reach it
from.
Adds **`/premium/connect-your-runtime`**: the wiring itself, how to
confirm the credential is actually consumed, the self-hosted
both-URLs-or-neither rule, and a troubleshooting table. Linked into both
navs, and the skills reference now points at the published page.
### 4. A guard so it cannot drift back
`scripts/validate-intelligence-env-names.ts` (`pnpm
check:intelligence-env-names`), wired to lefthook and a new workflow.
The workflow is **intentionally unfiltered**. The two workflows that
would otherwise cover this both filter: `plugin-skills-check` by
`paths:`, and `static/quality` by `paths-ignore: examples/**` — which is
exactly where the deprecated alias lives. Scoping the job would re-open
the hole it exists to close. Legitimate alias sites live in
`ALIAS_ALLOWLIST`.
## Related PRs and Issues
- [OSS-881](https://linear.app/copilotkit/issue/OSS-881) — needs
**both** PRs; neither closes it alone
- CopilotKit/Intelligence#890 — items 1 and 4 (`copilotkit verify` +
rubric contract 1.3.0)
## Verification
- Full lefthook pre-commit ran green: `check-plugin-skills`, `lint-fix`,
the new `check-intelligence-env-names`, and `test`/`publint`/`attw`
across **25 projects**.
- `examples/slack` `managed.test.ts` extended to cover **both** the
canonical name and the alias fallback, and proven non-vacuous — removing
the fallback turns the new test red.
- The drift guard proven non-vacuous the same way: reintroducing a
retired name fails it, exit 1.
- `oxfmt` and `oxlint` clean on every file touched (0 errors).
## Checklist
- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation
🤖 Generated with [Claude Code](https://claude.com/claude-code)