fix(integrations): preserve managed endpoint defaults

This commit is contained in:
Mike Ryan
2026-08-26 12:46:20 -07:00
committed by Maximiliano Korp
parent db01ffaa14
commit ade274ce29
10 changed files with 109 additions and 66 deletions
+2 -1
View File
@@ -36,7 +36,8 @@ Per-instance `allowedDivergence` list in `manifest.json`:
- `src/app/api/copilotkit/**` — north-star uses `LangGraphAgent`, Docker
instances use `HttpAgent`. Different routes. The Intelligence wiring block
inside those routes is held to one shape by
`scripts/validate-intelligence-wiring-block.ts`, which covers all 22 starters
`scripts/validate-intelligence-wiring-block.ts`, which covers the 21 shared
starter wiring blocks. AgentCore has a separate request-identity contract test
rather than the 7 tracked here (OSS-982).
- `Dockerfile`, `docker/Dockerfile.agent`, `serve.py`, `scripts/**`
language-specific build/run tooling.
+4 -3
View File
@@ -10,6 +10,7 @@ CPK_TELEMETRY_ID=
# Required by the pinned SDK for Threads entitlement checks.
COPILOTKIT_LICENSE_TOKEN=
# Docker Compose maps this name to the host running local Intelligence.
INTELLIGENCE_API_URL=http://host.docker.internal:4201
INTELLIGENCE_GATEWAY_WS_URL=ws://host.docker.internal:4401
# Docker Compose maps these names to the host running local Intelligence.
# Leave them unset when you use managed Intelligence.
# INTELLIGENCE_API_URL=http://host.docker.internal:4201
# INTELLIGENCE_GATEWAY_WS_URL=ws://host.docker.internal:4401
+15 -6
View File
@@ -41,25 +41,33 @@ identity and can stay blank. The pinned SDK token setup is below.
configured AWS Secrets Manager secrets. CDK resolves both only for the
CopilotKit runtime Lambda.
Before deploying, provide managed or self-hosted Intelligence endpoints that are reachable from AWS. AWS deployments must not use `localhost` or `127.0.0.1`; they also must not use the Docker-only `host.docker.internal` name from `.env.example`.
Managed Intelligence uses its default endpoints. For self-hosted
Intelligence, set endpoint overrides that AWS can reach. Do not use
`localhost`, `127.0.0.1`, or the Docker-only `host.docker.internal`
name from `.env.example`.
2. **Deploy:**
```bash
INTELLIGENCE_API_URL=https://intelligence.example.com \
INTELLIGENCE_GATEWAY_WS_URL=wss://gateway.example.com \
./deploy-langgraph.sh # LangGraph agent (infra + frontend)
./deploy-langgraph.sh --skip-frontend # infra/agent only
./deploy-langgraph.sh --skip-backend # frontend only
# or
INTELLIGENCE_API_URL=https://intelligence.example.com \
INTELLIGENCE_GATEWAY_WS_URL=wss://gateway.example.com \
./deploy-strands.sh # AWS Strands agent
./deploy-strands.sh --skip-frontend
./deploy-strands.sh --skip-backend
# Self-hosted Intelligence only:
INTELLIGENCE_API_URL=https://intelligence.example.com \
INTELLIGENCE_GATEWAY_WS_URL=wss://gateway.example.com \
./deploy-langgraph.sh
INTELLIGENCE_API_URL=https://intelligence.example.com \
INTELLIGENCE_GATEWAY_WS_URL=wss://gateway.example.com \
./deploy-strands.sh
```
The command-prefixed endpoint values override the local defaults sourced from `.env`. Use the same prefix with `--skip-frontend` or `--skip-backend` when needed.
The command-prefixed endpoint values override the managed defaults. Use the
same prefix with `--skip-frontend` or `--skip-backend` when needed.
3. **Open** the Amplify URL printed at the end. Sign in with your email.
@@ -70,6 +78,7 @@ cp .env.example .env
cp docker/.env.example docker/.env
cd docker
# Fill in docker/.env AWS creds — STACK_NAME, MEMORY_ID, and aws-exports.json are auto-resolved
# For local Intelligence, uncomment the host.docker.internal URLs in ../.env.
./up.sh --build
```
@@ -63,11 +63,11 @@ echo "── CopilotKit + AWS AgentCore (LangGraph) ─────────
check_command() {
command -v "$1" >/dev/null 2>&1 || { echo "ERROR: $1 is required but not installed."; exit 1; }
}
require_remote_endpoint() {
validate_remote_override() {
local name="$1"
local value="$2"
local example="$3"
if [[ -z "$value" || "$value" =~ ^[a-zA-Z][a-zA-Z0-9+.-]*://(localhost|127\.0\.0\.1|host\.docker\.internal)([:/]|$) ]]; then
if [[ -n "$value" && "$value" =~ ^[a-zA-Z][a-zA-Z0-9+.-]*://(localhost|127\.0\.0\.1|host\.docker\.internal)([:/]|$) ]]; then
echo "ERROR: $name must be a non-local endpoint reachable from AWS (for example, $example). Set it in .env or prefix the deploy command."
exit 1
fi
@@ -75,8 +75,8 @@ require_remote_endpoint() {
check_command aws
check_command uv
if [ "$SKIP_BACKEND" = false ]; then
require_remote_endpoint INTELLIGENCE_API_URL "${INTELLIGENCE_API_URL:-}" "https://intelligence.example.com"
require_remote_endpoint INTELLIGENCE_GATEWAY_WS_URL "${INTELLIGENCE_GATEWAY_WS_URL:-}" "wss://gateway.example.com"
validate_remote_override INTELLIGENCE_API_URL "${INTELLIGENCE_API_URL:-}" "https://intelligence.example.com"
validate_remote_override INTELLIGENCE_GATEWAY_WS_URL "${INTELLIGENCE_GATEWAY_WS_URL:-}" "wss://gateway.example.com"
check_command node
check_command docker
fi
@@ -63,11 +63,11 @@ echo "── CopilotKit + AWS AgentCore (Strands) ──────────
check_command() {
command -v "$1" >/dev/null 2>&1 || { echo "ERROR: $1 is required but not installed."; exit 1; }
}
require_remote_endpoint() {
validate_remote_override() {
local name="$1"
local value="$2"
local example="$3"
if [[ -z "$value" || "$value" =~ ^[a-zA-Z][a-zA-Z0-9+.-]*://(localhost|127\.0\.0\.1|host\.docker\.internal)([:/]|$) ]]; then
if [[ -n "$value" && "$value" =~ ^[a-zA-Z][a-zA-Z0-9+.-]*://(localhost|127\.0\.0\.1|host\.docker\.internal)([:/]|$) ]]; then
echo "ERROR: $name must be a non-local endpoint reachable from AWS (for example, $example). Set it in .env or prefix the deploy command."
exit 1
fi
@@ -75,8 +75,8 @@ require_remote_endpoint() {
check_command aws
check_command uv
if [ "$SKIP_BACKEND" = false ]; then
require_remote_endpoint INTELLIGENCE_API_URL "${INTELLIGENCE_API_URL:-}" "https://intelligence.example.com"
require_remote_endpoint INTELLIGENCE_GATEWAY_WS_URL "${INTELLIGENCE_GATEWAY_WS_URL:-}" "wss://gateway.example.com"
validate_remote_override INTELLIGENCE_API_URL "${INTELLIGENCE_API_URL:-}" "https://intelligence.example.com"
validate_remote_override INTELLIGENCE_GATEWAY_WS_URL "${INTELLIGENCE_GATEWAY_WS_URL:-}" "wss://gateway.example.com"
check_command node
check_command docker
fi
@@ -444,10 +444,15 @@ export class BackendStack extends cdk.NestedStack {
},
).unsafeUnwrap(),
CPK_TELEMETRY_ID: process.env.CPK_TELEMETRY_ID ?? "",
INTELLIGENCE_API_URL:
process.env.INTELLIGENCE_API_URL ?? "http://localhost:4201",
INTELLIGENCE_GATEWAY_WS_URL:
process.env.INTELLIGENCE_GATEWAY_WS_URL ?? "ws://localhost:4401",
...(process.env.INTELLIGENCE_API_URL
? { INTELLIGENCE_API_URL: process.env.INTELLIGENCE_API_URL }
: {}),
...(process.env.INTELLIGENCE_GATEWAY_WS_URL
? {
INTELLIGENCE_GATEWAY_WS_URL:
process.env.INTELLIGENCE_GATEWAY_WS_URL,
}
: {}),
},
timeout: cdk.Duration.seconds(30),
memorySize: 1024,
@@ -138,15 +138,15 @@ test("only the explicit local resolver supplies a demo user", () => {
});
});
test("AgentCore Docker reaches host Intelligence without allowing that host in AWS", () => {
test("AgentCore documents Docker host overrides without allowing that host in AWS", () => {
const environmentSource = readAgentCoreSource(".env.example");
const composeSource = readAgentCoreSource("docker/docker-compose.yml");
expect(environmentSource).toContain(
"INTELLIGENCE_API_URL=http://host.docker.internal:4201",
"# INTELLIGENCE_API_URL=http://host.docker.internal:4201",
);
expect(environmentSource).toContain(
"INTELLIGENCE_GATEWAY_WS_URL=ws://host.docker.internal:4401",
"# INTELLIGENCE_GATEWAY_WS_URL=ws://host.docker.internal:4401",
);
expect(composeSource).toContain("extra_hosts:");
expect(composeSource).toContain("host.docker.internal:host-gateway");
@@ -751,6 +751,26 @@ function objectPropertyAssignment(
return null;
}
/** Returns a direct value or conditional spread that reads one env name. */
function objectEnvironmentExpression(
objectLiteral: ts.ObjectLiteralExpression,
name: string,
): ts.Expression | null {
const direct = objectPropertyAssignment(objectLiteral, name);
if (direct) return direct.initializer;
for (const property of objectLiteral.properties) {
if (
ts.isSpreadAssignment(property) &&
expressionContainsEnvRead(property.expression, name)
) {
return property.expression;
}
}
return null;
}
/** Returns whether one expression contains the exact managed env read. */
function expressionContainsEnvRead(
expression: ts.Expression,
@@ -2337,7 +2357,7 @@ function expectAgentCoreVariantBehavior(
/npx cdk@latest deploy --all --require-approval never/,
);
expect(contents).toMatch(
/python3 scripts\/deploy-frontend\.py "\$STACK_NAME"/,
/uv run --project "\$SCRIPT_DIR" "\$SCRIPT_DIR\/scripts\/deploy-frontend\.py" "\$STACK_NAME"/,
);
}
@@ -2427,11 +2447,14 @@ function expectAgentCoreRuntimeDeploymentContract(contents: string): void {
: null;
const apiUrl =
environmentObject && ts.isObjectLiteralExpression(environmentObject)
? objectPropertyAssignment(environmentObject, INTELLIGENCE_API_URL)
? objectEnvironmentExpression(environmentObject, INTELLIGENCE_API_URL)
: null;
const gatewayWsUrl =
environmentObject && ts.isObjectLiteralExpression(environmentObject)
? objectPropertyAssignment(environmentObject, INTELLIGENCE_GATEWAY_WS_URL)
? objectEnvironmentExpression(
environmentObject,
INTELLIGENCE_GATEWAY_WS_URL,
)
: null;
expect(managedKey).not.toBeNull();
expect(telemetryId).not.toBeNull();
@@ -2461,14 +2484,9 @@ function expectAgentCoreRuntimeDeploymentContract(contents: string): void {
expect(expressionContainsSecretResolution(telemetryId.initializer)).toBe(
false,
);
expect(expressionContainsEnvRead(apiUrl, INTELLIGENCE_API_URL)).toBe(true);
expect(
expressionContainsEnvRead(apiUrl.initializer, INTELLIGENCE_API_URL),
).toBe(true);
expect(
expressionContainsEnvRead(
gatewayWsUrl.initializer,
INTELLIGENCE_GATEWAY_WS_URL,
),
expressionContainsEnvRead(gatewayWsUrl, INTELLIGENCE_GATEWAY_WS_URL),
).toBe(true);
}
@@ -3233,14 +3251,6 @@ for (const scriptName of [
},
expectedVariable: INTELLIGENCE_API_URL,
},
{
label: "empty gateway URL",
envFile: {
apiUrl: "https://intelligence.example.com/api",
gatewayWsUrl: "",
},
expectedVariable: INTELLIGENCE_GATEWAY_WS_URL,
},
] as const) {
test(`${scriptName} rejects ${invalidEndpoint.label} without exposing the managed key`, () => {
const result = runAgentCoreDeployHarness({
@@ -3257,6 +3267,18 @@ for (const scriptName of [
});
}
test(`${scriptName} uses managed endpoint defaults when overrides are omitted`, () => {
const result = runAgentCoreDeployHarness({
scriptName,
envFile: {},
});
expect(result.status).toBe(0);
expect(result.cdkEnvironment).toBe("\n\n");
expect(result.output).not.toContain(MANAGED_API_KEY_SENTINEL);
expect(result.output).not.toContain(LEGACY_LICENSE_TOKEN_SENTINEL);
});
test(`${scriptName} accepts sourced HTTPS and WSS endpoints and passes them to CDK`, () => {
const result = runAgentCoreDeployHarness({
scriptName,
@@ -4270,12 +4292,13 @@ for (const contract of INTELLIGENCE_TEMPLATE_CONTRACTS) {
}
}
/** Assert AgentCore's AWS instructions require caller-provided remote endpoints. */
/** Assert AgentCore's AWS instructions document optional remote overrides. */
function expectAgentCoreAwsEndpointReadmeContract(contents: string): void {
expect(contents).toMatch(/managed or self-hosted Intelligence endpoints/i);
expect(contents).toMatch(/reachable from AWS/i);
expect(contents).toMatch(/Managed Intelligence uses its default endpoints/i);
expect(contents).toMatch(/For self-hosted\s+Intelligence/i);
expect(contents).toMatch(/endpoint overrides that AWS can reach/i);
expect(contents).toMatch(
/must not use[\s`]*localhost[\s\S]{0,40}127\.0\.0\.1/i,
/do not use\s+`localhost`[\s\S]{0,40}`127\.0\.0\.1`/i,
);
for (const scriptName of [
@@ -4302,10 +4325,10 @@ function expectAgentCoreAwsEndpointDeployContract(contents: string): void {
),
);
const apiGuardIndex = lines.findIndex((line) =>
/require_remote_endpoint\s+INTELLIGENCE_API_URL\b/.test(line),
/validate_remote_override\s+INTELLIGENCE_API_URL\b/.test(line),
);
const gatewayGuardIndex = lines.findIndex((line) =>
/require_remote_endpoint\s+INTELLIGENCE_GATEWAY_WS_URL\b/.test(line),
/validate_remote_override\s+INTELLIGENCE_GATEWAY_WS_URL\b/.test(line),
);
const firstAwsWriteIndex = lines.findIndex((line) =>
/aws\s+secretsmanager\s+(?:create-secret|put-secret-value)\b/.test(line),
@@ -17,10 +17,12 @@ import {
* every instance it tracks, and no smoke test sets `COPILOTKIT_LICENSE_TOKEN`,
* so the `intelligence:` arm has never executed in CI (OSS-982).
*
* The block was already byte-identical in 21 of 22 starters when the check was
* written, so this is a ratchet rather than a migration. What had drifted was
* the warning comment into five variants, two of them missing outright and
* that drift is how the localhost default of OSS-981 survived in all 22 copies.
* The block was already byte-identical in the 21 starters that use the shared
* demo-user setup when the check was written, so this is a ratchet rather than
* a migration. AgentCore uses request-bound Cognito identity and has a separate
* runtime security test. What had drifted was the warning comment into five
* variants, two of them missing outright and that drift is how the localhost
* default of OSS-981 survived in all 21 shared copies.
*/
const CANONICAL = ` // --- copilotkit:intelligence (remove this block to opt out) ---
@@ -169,8 +171,8 @@ describe("blockDiff", () => {
* asserted against the starter inventory as well.
*/
describe("the repository's wiring sites", () => {
it("finds one marked site in every starter that wires Intelligence", () => {
expect(markerFiles().length).toBeGreaterThanOrEqual(22);
it("finds one marked site in every starter with shared Intelligence wiring", () => {
expect(markerFiles().length).toBeGreaterThanOrEqual(21);
});
it("holds every site to one shape", () => {
+13 -11
View File
@@ -5,11 +5,13 @@ import * as path from "node:path";
/**
* Holds the starters' Intelligence wiring block to one shape.
*
* Every starter ends its runtime construction with the same marked region: a
* spread that reads `COPILOTKIT_LICENSE_TOKEN` and either wires the managed
* platform or falls back to a local runner. It is the region a hosted reader
* copies verbatim, and nothing gated it (OSS-982). Both gaps that could have
* caught drift are deliberate:
* Each starter that uses the shared demo-user setup ends its runtime
* construction with the same marked region: a spread that reads
* `COPILOTKIT_LICENSE_TOKEN` and either wires the managed platform or falls
* back to a local runner. AgentCore uses request-bound Cognito identity instead,
* so its separate runtime security test holds that path. The shared region is
* what a hosted reader copies verbatim, and nothing gated it (OSS-982). Both
* gaps that could have caught drift are deliberate:
*
* - The parity manifest lists `src/app/api/copilotkit/**` under
* `allowedDivergence` for every instance it tracks, so the drift check skips
@@ -19,12 +21,12 @@ import * as path from "node:path";
* smoke-tested starter takes the else arm. The `intelligence:` arm has never
* run in CI.
*
* The cost was already visible. The block's code was byte-identical in 21 of 22
* starters, but its warning comment had drifted into five variants and two
* starters shipped the `demo-user` stub with no warning at all. Comment drift
* is harmless by itself; it is the tracer showing nothing held the region
* still, and it is how the localhost default of OSS-981 survived in all 22
* copies at once.
* The cost was already visible. The block's code was byte-identical in the 21
* shared starters, but its warning comment had drifted into five variants and
* two starters shipped the `demo-user` stub with no warning at all. Comment
* drift is harmless by itself; it shows that nothing held the region still,
* and it is how the localhost default of OSS-981 survived in all 21 copies at
* once.
*
* The check compares each site against the north-star starter rather than
* against a literal kept here, so improving the block means editing the north