Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions components.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"schemaVersion": "1",
"contractIdentity": "NeKiro/contracts@e883ad4bcfaf75ffe80e9d9e6fb79b2a8c58b18c",
"contractIdentity": "NeKiro/contracts@6cee25590bb720ed447d70e82204abdad48c5779",
"components": {
"core": {
"repository": "NeKiro-project/NeKiro",
"commitSha": "e883ad4bcfaf75ffe80e9d9e6fb79b2a8c58b18c"
"commitSha": "6cee25590bb720ed447d70e82204abdad48c5779"
},
"console": {
"repository": "NeKiro-project/NeKiro-Console",
Expand All @@ -16,7 +16,7 @@
},
"samples": {
"repository": "NeKiro-project/NeKiro-Samples",
"commitSha": "5bc9dde207732d25e853520f39144d2bfee8c879"
"commitSha": "8f6842eab413d9cc1926cbb247dc5e26cc0a7fa4"
},
"transportGo": {
"repository": "NeKiro-project/nekiro-a2a-transport-go",
Expand Down
80 changes: 78 additions & 2 deletions compose.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,45 @@
name: nekiro

services:
nacos:
image: "nacos/nacos-server:v2.5.1@sha256:8987908cb94ed5f9d30522a64493d35732a6c05f216d667a7addb022f3d92e80"
environment:
MODE: standalone
NACOS_AUTH_ENABLE: "false"
networks:
- platform-internal
- local-access
ports:
- "127.0.0.1:${NACOS_PORT:?NACOS_PORT must be set and non-empty}:8848"
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8848/nacos/v1/console/health/readiness"]
interval: 5s
timeout: 5s
retries: 24
start_period: 20s

nacos-bootstrap:
image: "curlimages/curl:8.16.0@sha256:463eaf6072688fe96ac64fa623fe73e1dbe25d8ad6c34404a669ad3ce1f104b6"
environment:
NACOS_BINDING_CONTENT: '{"schemaVersion":"1","revision":"stack-bootstrap-1","targets":[]}'
command:
- sh
- -eu
- -c
- |
curl -fsS -X POST http://nacos:8848/nacos/v1/console/namespaces \
--data-urlencode customNamespaceId=nekiro \
--data-urlencode namespaceName=nekiro \
--data-urlencode 'namespaceDesc=NeKiro Stack acceptance'
curl -fsS -X POST 'http://nacos:8848/nacos/v1/cs/configs?dataId=router.nacos-bindings&group=NEKIRO&tenant=nekiro' \
--data-urlencode content=$$NACOS_BINDING_CONTENT
depends_on:
nacos:
condition: service_healthy
networks:
- platform-internal
restart: "no"

postgres:
image: postgres:17.9-bookworm@sha256:47f917f7409eacd22fc5dfb1dee634e1b55cf0c01d1a7eb701be2227a03e0641
environment:
Expand Down Expand Up @@ -107,6 +146,12 @@ services:
NEKIRO_ROUTER_AGENT_CREDENTIAL_PRIVATE_KEY_BASE64URL: "${NEKIRO_ROUTER_AGENT_CREDENTIAL_PRIVATE_KEY_BASE64URL:?NEKIRO_ROUTER_AGENT_CREDENTIAL_PRIVATE_KEY_BASE64URL must be set and non-empty}"
NEKIRO_ROUTER_AGENT_CREDENTIAL_TTL_SECONDS: "${NEKIRO_ROUTER_AGENT_CREDENTIAL_TTL_SECONDS:?NEKIRO_ROUTER_AGENT_CREDENTIAL_TTL_SECONDS must be set and non-empty}"
NEKIRO_ROUTER_INSTANCE_ROUTING_MODE: "${NEKIRO_ROUTER_INSTANCE_ROUTING_MODE:?NEKIRO_ROUTER_INSTANCE_ROUTING_MODE must be set and non-empty}"
NEKIRO_ROUTER_NACOS_API_ORIGIN: "http://nacos:8848/nacos"
NEKIRO_ROUTER_NACOS_NAMESPACE_ID: "nekiro"
NEKIRO_ROUTER_NACOS_CONFIG_GROUP: "NEKIRO"
NEKIRO_ROUTER_NACOS_AUTH_MODE: "none"
NEKIRO_ROUTER_NACOS_RESPONSE_LIMIT_BYTES: "${NEKIRO_ROUTER_NACOS_RESPONSE_LIMIT_BYTES:?NEKIRO_ROUTER_NACOS_RESPONSE_LIMIT_BYTES must be set and non-empty}"
NEKIRO_ROUTER_NACOS_REQUEST_TIMEOUT_MS: "${NEKIRO_ROUTER_NACOS_REQUEST_TIMEOUT_MS:?NEKIRO_ROUTER_NACOS_REQUEST_TIMEOUT_MS must be set and non-empty}"
NEKIRO_ROUTER_CONFIG_CENTER_FILE_ROOT: "/var/lib/nekiro/config"
NEKIRO_ROUTER_CONFIG_CENTER_MAX_PAYLOAD_BYTES: "${NEKIRO_ROUTER_CONFIG_CENTER_MAX_PAYLOAD_BYTES:?NEKIRO_ROUTER_CONFIG_CENTER_MAX_PAYLOAD_BYTES must be set and non-empty}"
NEKIRO_ROUTER_INSTANCE_DIRECTORY_KEY: "${NEKIRO_ROUTER_INSTANCE_DIRECTORY_KEY:?NEKIRO_ROUTER_INSTANCE_DIRECTORY_KEY must be set and non-empty}"
Expand All @@ -120,6 +165,8 @@ services:
condition: service_completed_successfully
control-plane:
condition: service_healthy
nacos-bootstrap:
condition: service_completed_successfully
networks:
- platform-internal
- local-access
Expand All @@ -136,6 +183,7 @@ services:
RUNTIME_B_LISTEN_ADDR: "0.0.0.0:8092"
RUNTIME_B_AGENT_ID: "runtime-b"
RUNTIME_B_INSTANCE_ID: "runtime-b-primary"
RUNTIME_B_REGISTRATION_MODE: "disabled"
RUNTIME_B_ROUTER_URL: "http://a2a-router:8081"
RUNTIME_B_ROUTER_TOKEN: "${RUNTIME_B_ROUTER_TOKEN:?RUNTIME_B_ROUTER_TOKEN must be set and non-empty}"
RUNTIME_B_TARGET_AGENT_ID: "runtime-a"
Expand Down Expand Up @@ -165,6 +213,17 @@ services:
RUNTIME_B_LISTEN_ADDR: "0.0.0.0:8092"
RUNTIME_B_AGENT_ID: "runtime-b"
RUNTIME_B_INSTANCE_ID: "runtime-b-directory"
RUNTIME_B_REGISTRATION_MODE: "nacos"
RUNTIME_B_NACOS_API_ORIGIN: "http://nacos:8848/nacos"
RUNTIME_B_NACOS_NAMESPACE_ID: "nekiro"
RUNTIME_B_NACOS_GROUP_NAME: "NEKIRO"
RUNTIME_B_NACOS_SERVICE_NAME: "runtime-b"
RUNTIME_B_NACOS_CLUSTER_NAME: "DEFAULT"
RUNTIME_B_NACOS_ADVERTISED_IP: "172.28.0.12"
RUNTIME_B_NACOS_ADVERTISED_PORT: "8092"
RUNTIME_B_NACOS_HEARTBEAT_INTERVAL_MS: "2000"
RUNTIME_B_NACOS_REQUEST_TIMEOUT_MS: "3000"
RUNTIME_B_NACOS_AUTH_MODE: "none"
RUNTIME_B_ROUTER_URL: "http://a2a-router:8081"
RUNTIME_B_ROUTER_TOKEN: "${RUNTIME_B_ROUTER_TOKEN:?RUNTIME_B_ROUTER_TOKEN must be set and non-empty}"
RUNTIME_B_TARGET_AGENT_ID: "runtime-a"
Expand All @@ -180,7 +239,8 @@ services:
a2a-router:
condition: service_healthy
networks:
- platform-internal
platform-internal:
ipv4_address: 172.28.0.12
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8092/readyz"]
interval: 5s
Expand All @@ -193,6 +253,18 @@ services:
environment:
RUNTIME_A_LISTEN_ADDR: "0.0.0.0:8091"
RUNTIME_A_AGENT_ID: "runtime-a"
RUNTIME_A_INSTANCE_ID: "runtime-a-primary"
RUNTIME_A_REGISTRATION_MODE: "nacos"
RUNTIME_A_NACOS_API_ORIGIN: "http://nacos:8848/nacos"
RUNTIME_A_NACOS_NAMESPACE_ID: "nekiro"
RUNTIME_A_NACOS_GROUP_NAME: "NEKIRO"
RUNTIME_A_NACOS_SERVICE_NAME: "runtime-a"
RUNTIME_A_NACOS_CLUSTER_NAME: "DEFAULT"
RUNTIME_A_NACOS_ADVERTISED_IP: "172.28.0.11"
RUNTIME_A_NACOS_ADVERTISED_PORT: "8091"
RUNTIME_A_NACOS_HEARTBEAT_INTERVAL_MS: "2000"
RUNTIME_A_NACOS_REQUEST_TIMEOUT_MS: "3000"
RUNTIME_A_NACOS_AUTH_MODE: "none"
RUNTIME_A_ROUTER_URL: "http://a2a-router:8081"
RUNTIME_A_ROUTER_TOKEN: "${RUNTIME_A_ROUTER_TOKEN:?RUNTIME_A_ROUTER_TOKEN must be set and non-empty}"
RUNTIME_A_TARGET_AGENT_ID: "runtime-b"
Expand All @@ -210,7 +282,8 @@ services:
runtime-b:
condition: service_healthy
networks:
- platform-internal
platform-internal:
ipv4_address: 172.28.0.11
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8091/readyz"]
interval: 5s
Expand All @@ -224,4 +297,7 @@ volumes:
networks:
platform-internal:
internal: true
ipam:
config:
- subnet: 172.28.0.0/24
local-access:
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/NeKiro-project/NeKiro-Stack
go 1.26.0

require (
github.com/NeKiro-project/NeKiro v0.0.0-20260808045545-e883ad4bcfaf
github.com/NeKiro-project/NeKiro v0.0.0-20260809083232-6cee25590bb7
github.com/jackc/pgx/v5 v5.10.0
)

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/NeKiro-project/NeKiro v0.0.0-20260808045545-e883ad4bcfaf h1:A8V2HXsH4VdiIKjkQKN2gO5abvF3fJykkKTccDoEvFc=
github.com/NeKiro-project/NeKiro v0.0.0-20260808045545-e883ad4bcfaf/go.mod h1:FMQC8bVPgsnMImg7eHaykOpWTOfQs9uDU6DVMDa1UVQ=
github.com/NeKiro-project/NeKiro v0.0.0-20260809083232-6cee25590bb7 h1:ju+I5nKZxgl/22830m+PewVUeqSrw8dYAGHPl7aBZyg=
github.com/NeKiro-project/NeKiro v0.0.0-20260809083232-6cee25590bb7/go.mod h1:FMQC8bVPgsnMImg7eHaykOpWTOfQs9uDU6DVMDa1UVQ=
github.com/a2aproject/a2a-go v0.3.15 h1:h5YpCiPq3jxQ5rIns7oDjPag3ivP8u817AzdA4F+NiI=
github.com/a2aproject/a2a-go v0.3.15/go.mod h1:I7Cm+a1oL+UT6zMoP+roaRE5vdfUa1iQGVN8aSOuZ0I=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down
7 changes: 6 additions & 1 deletion scripts/write-ci-env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ write_common() {
printf '%s\n' '{"schemaVersion":"1","revision":"stack-bootstrap-1","targets":[]}' >"$config_root/cfg-v1-cm91dGVyL2luc3RhbmNlLWRpcmVjdG9yeQ.value"
cat >>"$output" <<'EOF'
POSTGRES_PORT=55432
NACOS_PORT=18848
CONTROL_PLANE_PORT=18080
A2A_ROUTER_PORT=18081
NEKIRO_PUBLIC_AGENT_ORIGIN=https://agents.nekiro.test
Expand Down Expand Up @@ -50,6 +51,8 @@ RUNTIME_B_EVENT_LIMIT_BYTES=65536
NEKIRO_ROUTER_CONFIG_CENTER_MAX_PAYLOAD_BYTES=1048576
NEKIRO_ROUTER_INSTANCE_DIRECTORY_KEY=router/instance-directory
NEKIRO_ROUTER_INSTANCE_PORT_NAME=a2a
NEKIRO_ROUTER_NACOS_RESPONSE_LIMIT_BYTES=1048576
NEKIRO_ROUTER_NACOS_REQUEST_TIMEOUT_MS=3000
EOF
printf 'NEKIRO_ROUTER_CONFIG_CENTER_ROOT=%s\n' "$config_root" >>"$output"
printf 'NEKIRO_E2E_COMPOSE_FILE=%s/compose.yaml\n' "$stack_root" >>"$output"
Expand Down Expand Up @@ -85,9 +88,11 @@ NEKIRO_E2E_OWNER_TOKEN=acceptance-owner-token
NEKIRO_E2E_USER_TOKEN=acceptance-user-token
NEKIRO_E2E_OTHER_TOKEN=acceptance-other-token
NEKIRO_E2E_DATABASE_URL=postgresql://nekiro_acceptance:acceptance-only-password@127.0.0.1:55432/nekiro_acceptance?sslmode=disable
NEKIRO_ROUTER_INSTANCE_ROUTING_MODE=config_center_file
NEKIRO_ROUTER_INSTANCE_ROUTING_MODE=nacos
NEKIRO_ROUTER_INSTANCE_DIRECTORY_KEY=router.nacos-bindings
EOF
printf 'NEKIRO_E2E_CONFIG_CENTER_ROOT=%s\n' "$config_root" >>"$output"
printf 'NEKIRO_E2E_NACOS_URL=http://127.0.0.1:%s/nacos\n' "${NACOS_PORT:-18848}" >>"$output"
;;
browser)
write_common
Expand Down
81 changes: 48 additions & 33 deletions tests/backend/invoke_record_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@ type acceptanceEnv struct {
controlPlane string
publicAgentOrigin string
routerURL string
nacosURL string
routerToken string
ownerToken string
userToken string
otherToken string
databaseURL string
composeFile string
composeProject string
configCenterRoot string
challengeTTL time.Duration
credentialIssuer string
credentialKeyID string
Expand Down Expand Up @@ -137,7 +137,8 @@ func TestInvokeToRecordAcceptance(t *testing.T) {
registerAndPublish(t, client, &env, runtimeTimeout)
registerAndPublish(t, client, &env, runtimeInterrupted)
runtimeLifecycleRelease := registerAndPublish(t, client, &env, runtimeLifecycle)
publishRouterInstanceDirectory(t, env)
assertNacosRegistrations(t, client, env)
publishRouterNacosBindings(t, client, env)
assertPublicAgentInputMatrix(t, client, env, contracts.CatalogEntry{PublicAgentID: env.publicAgentIDs["runtime-a"], PublicURL: env.publicAgentOrigin + "/a/" + env.publicAgentIDs["runtime-a"]})
assertVerificationFailureMatrix(t, client, &env)

Expand Down Expand Up @@ -251,10 +252,6 @@ func loadAcceptanceEnv(t *testing.T) acceptanceEnv {
if !filepath.IsAbs(composeFile) {
t.Fatalf("NEKIRO_E2E_COMPOSE_FILE must be an absolute path")
}
configCenterRoot := requiredEnv(t, "NEKIRO_E2E_CONFIG_CENTER_ROOT")
if !filepath.IsAbs(configCenterRoot) {
t.Fatalf("NEKIRO_E2E_CONFIG_CENTER_ROOT must be an absolute path")
}
ttlSeconds, err := strconv.ParseInt(requiredEnv(t, "NEKIRO_ENDPOINT_CHALLENGE_TTL_SECONDS"), 10, 64)
if err != nil || ttlSeconds < 2 || ttlSeconds > 15 {
t.Fatalf("NEKIRO_ENDPOINT_CHALLENGE_TTL_SECONDS must be an acceptance value from 2 through 15 seconds")
Expand All @@ -263,14 +260,14 @@ func loadAcceptanceEnv(t *testing.T) acceptanceEnv {
controlPlane: requiredEnv(t, "NEKIRO_E2E_CONTROL_PLANE_URL"),
publicAgentOrigin: requiredEnv(t, "NEKIRO_E2E_PUBLIC_AGENT_ORIGIN"),
routerURL: requiredEnv(t, "NEKIRO_E2E_ROUTER_URL"),
nacosURL: requiredEnv(t, "NEKIRO_E2E_NACOS_URL"),
routerToken: requiredEnv(t, "NEKIRO_E2E_ROUTER_TOKEN"),
ownerToken: requiredEnv(t, "NEKIRO_E2E_OWNER_TOKEN"),
userToken: requiredEnv(t, "NEKIRO_E2E_USER_TOKEN"),
otherToken: requiredEnv(t, "NEKIRO_E2E_OTHER_TOKEN"),
databaseURL: requiredEnv(t, "NEKIRO_E2E_DATABASE_URL"),
composeFile: composeFile,
composeProject: requiredEnv(t, "NEKIRO_E2E_COMPOSE_PROJECT"),
configCenterRoot: configCenterRoot,
challengeTTL: time.Duration(ttlSeconds) * time.Second,
credentialIssuer: requiredEnv(t, "NEKIRO_ROUTER_AGENT_CREDENTIAL_ISSUER"),
credentialKeyID: requiredEnv(t, "NEKIRO_ROUTER_AGENT_CREDENTIAL_KEY_ID"),
Expand All @@ -280,55 +277,73 @@ func loadAcceptanceEnv(t *testing.T) acceptanceEnv {
}
}

func publishRouterInstanceDirectory(t *testing.T, env acceptanceEnv) {
func assertNacosRegistrations(t *testing.T, client *http.Client, env acceptanceEnv) {
t.Helper()
for service, instanceID := range map[string]string{"runtime-a": "runtime-a-primary", "runtime-b": "runtime-b-directory"} {
endpoint, err := url.Parse(env.nacosURL + "/v1/ns/instance/list")
if err != nil {
t.Fatal(err)
}
query := endpoint.Query()
query.Set("serviceName", service)
query.Set("groupName", "NEKIRO")
query.Set("clusters", "DEFAULT")
query.Set("namespaceId", "nekiro")
query.Set("healthyOnly", "false")
endpoint.RawQuery = query.Encode()
result := doRequest(t, client, endpoint.String(), http.MethodGet, "", "", nil)
if result.status != http.StatusOK || !bytes.Contains(result.body, []byte(`"nekiro.instanceId":"`+instanceID+`"`)) || !bytes.Contains(result.body, []byte(`"healthy":true`)) {
t.Fatalf("Nacos registration %s status=%d body=%s", service, result.status, result.body)
}
}
}

func publishRouterNacosBindings(t *testing.T, client *http.Client, env acceptanceEnv) {
t.Helper()
agentIDs := make([]string, 0, len(env.releases))
for agentID := range env.releases {
agentIDs = append(agentIDs, agentID)
}
sort.Strings(agentIDs)
document := contracts.RouterInstanceDirectoryV1{SchemaVersion: contracts.RouterInstanceDirectorySchemaVersion, Revision: "stack-runtime-1", Targets: make([]contracts.RouterInstanceDirectoryTargetV1, 0, len(agentIDs))}
document := contracts.RouterNacosInstanceBindingsV1{
SchemaVersion: contracts.RouterNacosInstanceBindingsSchemaVersion,
Revision: "stack-runtime-1",
Targets: make([]contracts.RouterNacosInstanceBindingTargetV1, 0, len(agentIDs)),
}
for _, agentID := range agentIDs {
release := env.releases[agentID]
origin, err := url.Parse(release.EndpointOrigin)
if err != nil {
t.Fatalf("parse Release endpoint origin: %v", err)
}
port, err := strconv.Atoi(origin.Port())
if err != nil || port < 1 || port > 65535 {
t.Fatalf("Release endpoint origin has invalid port: %s", release.EndpointOrigin)
serviceName := origin.Hostname()
if serviceName != "runtime-a" && serviceName != "runtime-b" {
t.Fatalf("Release endpoint origin has no owned Nacos service: %s", release.EndpointOrigin)
}
address := origin.Hostname()
instanceID := agentID + "-primary"
if agentID == "runtime-b" {
address = "runtime-b-directory"
instanceID = "runtime-b-directory"
}
document.Targets = append(document.Targets, contracts.RouterInstanceDirectoryTargetV1{
document.Targets = append(document.Targets, contracts.RouterNacosInstanceBindingTargetV1{
AgentID: agentID, AgentCardVersion: release.AgentCardVersion, ReleaseID: release.ReleaseID,
CardDigest: release.CardDigest, CanonicalEndpoint: release.EndpointOrigin + release.EndpointPath,
Audience: release.EndpointOrigin,
Instances: []contracts.RouterInstanceV1{{
InstanceID: instanceID, Ready: true, Serving: true,
Endpoints: []contracts.RouterNetworkEndpointV1{{AddressType: "DNS", Address: address, PortName: "a2a", Port: port, Protocol: "TCP"}},
}},
Audience: release.EndpointOrigin, ServiceName: serviceName, GroupName: "NEKIRO", ClusterName: "DEFAULT",
})
}
payload, err := json.Marshal(document)
if err != nil {
t.Fatal(err)
}
const leaf = "cfg-v1-cm91dGVyL2luc3RhbmNlLWRpcmVjdG9yeQ.value"
temporary := filepath.Join(env.configCenterRoot, leaf+".tmp")
destination := filepath.Join(env.configCenterRoot, leaf)
if err := os.WriteFile(temporary, payload, 0o644); err != nil {
t.Fatalf("write Router instance directory: %v", err)
form := url.Values{"dataId": {"router.nacos-bindings"}, "group": {"NEKIRO"}, "tenant": {"nekiro"}, "type": {"json"}, "content": {string(payload)}}
request, err := http.NewRequestWithContext(t.Context(), http.MethodPost, env.nacosURL+"/v1/cs/configs", strings.NewReader(form.Encode()))
if err != nil {
t.Fatal(err)
}
if err := os.Chmod(temporary, 0o644); err != nil {
t.Fatalf("make Router instance directory readable by the Router container: %v", err)
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
response, err := client.Do(request)
if err != nil {
t.Fatalf("publish Router Nacos bindings: %v", err)
}
if err := os.Rename(temporary, destination); err != nil {
t.Fatalf("publish Router instance directory: %v", err)
body, readErr := io.ReadAll(io.LimitReader(response.Body, 4097))
closeErr := response.Body.Close()
if readErr != nil || closeErr != nil || response.StatusCode != http.StatusOK || len(body) > 4096 || strings.TrimSpace(string(body)) != "true" {
t.Fatalf("publish Router Nacos bindings status=%d body=%s read=%v close=%v", response.StatusCode, body, readErr, closeErr)
}
}

Expand Down
Loading