mirror of
https://github.com/CopilotKit/CopilotKit.git
synced 2026-09-14 16:26:20 +08:00
fix(integrations): preserve managed endpoint defaults
This commit is contained in:
committed by
Maximiliano Korp
parent
db01ffaa14
commit
ade274ce29
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user