Skip to content
Closed
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
9 changes: 6 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,15 @@ NEKIRO_ROUTER_AGENT_CREDENTIAL_PRIVATE_KEY_BASE64URL=
NEKIRO_ROUTER_AGENT_CREDENTIAL_TTL_SECONDS=
A2A_ROUTER_PORT=

# Runtime A is the only sample that needs a Router Agent binding. The token
# digest is configured in NEKIRO_ROUTER_AGENT_PRINCIPALS_JSON; this raw value
# is supplied only to Runtime A and must never be logged or persisted.
# Runtime A and Runtime B use separate Router Agent bindings. Each token digest
# is configured in NEKIRO_ROUTER_AGENT_PRINCIPALS_JSON; raw values are supplied
# only to the matching sample and must never be logged or persisted.
RUNTIME_A_ROUTER_TOKEN=
RUNTIME_A_RESPONSE_LIMIT_BYTES=
RUNTIME_A_EVENT_LIMIT_BYTES=
RUNTIME_B_ROUTER_TOKEN=
RUNTIME_B_RESPONSE_LIMIT_BYTES=
RUNTIME_B_EVENT_LIMIT_BYTES=
NEKIRO_AGENT_ROUTER_ISSUER=
NEKIRO_AGENT_ROUTER_KEY_ID=
NEKIRO_AGENT_ROUTER_PUBLIC_KEY_BASE64URL=
151 changes: 148 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Verify Console workspace package
run: >-
node -e "const fs=require('fs'); const p=JSON.parse(fs.readFileSync('apps/console/package.json','utf8')); for (const name of ['typecheck','test','build']) { if (typeof p.scripts?.[name] !== 'string' || p.scripts[name].trim() === '') throw new Error('apps/console script is missing: ' + name); }"

- name: Typecheck frontend
run: pnpm typecheck

Expand All @@ -121,6 +125,132 @@ jobs:
- name: Build frontend
run: pnpm build

console-browser-acceptance:
needs: frontend
runs-on: ubuntu-latest
timeout-minutes: 40
env:
# Deterministic CI-only fixtures. These values are never production
# configuration and are not written into the imported Console source.
POSTGRES_USER: nekiro_console_acceptance
POSTGRES_PASSWORD: nekiro-console-acceptance-only
POSTGRES_DB: nekiro_console_acceptance
POSTGRES_PORT: "55432"
CONTROL_PLANE_PORT: "18080"
A2A_ROUTER_PORT: "18081"
NEKIRO_COMPOSE_DATABASE_URL: postgresql://nekiro_console_acceptance:nekiro-console-acceptance-only@postgres:5432/nekiro_console_acceptance?sslmode=disable
NEKIRO_DEV_AUTH_PRINCIPALS_JSON: '[{"id":"root-console-provider","tokenSha256":"4def860d949646b1515e6d28096af112224f06cf6a5941ab0ac51b9a458b1252"},{"id":"root-console-owner","tokenSha256":"4162f45cb0487cc2205850cc622fbecaa976a87f7aae8a96fa1676e2a984d2ac"}]'
NEKIRO_INTERNAL_DEV_AUTH_PRINCIPALS_JSON: '[{"id":"root-console-router-internal","tokenSha256":"4285e7349a2517fbdbfac9c1bc072a5ff1ef702d6cfe1826d62f02d73334bdc0"}]'
NEKIRO_ROUTER_SERVICE_PRINCIPALS_JSON: '[{"id":"root-console-control-plane","tokenSha256":"e3a864d8b9d70000e50e42d94761cc5d5996a36f39137735dca3a830f23cf4ba"}]'
NEKIRO_ROUTER_AGENT_PRINCIPALS_JSON: '[{"workspaceId":"root-console-workspace","agentId":"runtime-a","tokenSha256":"fd59489764dad19b9c276a8fb0187fdb187f3de6b426467c0af9659edbe4159f"},{"workspaceId":"root-console-workspace","agentId":"runtime-b","tokenSha256":"43a0b80acc0b4436c4433b8313bc4c41cbb5534751c458f6e2cfe4af602ca34f"}]'
NEKIRO_ROUTER_INTERNAL_BEARER_TOKEN: root-console-router-internal-token
NEKIRO_CONTROL_PLANE_SERVICE_TOKEN: root-console-control-plane-token
NEKIRO_CORS_ALLOWED_ORIGINS: http://127.0.0.1:4173
NEKIRO_ENDPOINT_CHALLENGE_TTL_SECONDS: "300"
NEKIRO_ENDPOINT_VERIFICATION_TIMEOUT_MS: "10000"
NEKIRO_ENDPOINT_ALLOWED_PRIVATE_HOSTS_JSON: '["runtime-a","runtime-b"]'
NEKIRO_CONTROL_PLANE_INTERNAL_REQUEST_MAX_BYTES: "1048576"
NEKIRO_GATEWAY_INVOCATION_REQUEST_MAX_BYTES: "1048576"
NEKIRO_GATEWAY_SSE_EVENT_MAX_BYTES: "65536"
NEKIRO_GATEWAY_METADATA_RESPONSE_MAX_BYTES: "1048576"
NEKIRO_GATEWAY_INVOCATION_DEADLINE_MS: "30000"
NEKIRO_ROUTER_INTERNAL_REQUEST_LIMIT_BYTES: "1048576"
NEKIRO_ROUTER_AGENT_REQUEST_LIMIT_BYTES: "1048576"
NEKIRO_ROUTER_CONTROL_PLANE_RESPONSE_LIMIT_BYTES: "1048576"
NEKIRO_ROUTER_AGENT_RESPONSE_LIMIT_BYTES: "1048576"
NEKIRO_ROUTER_A2A_EVENT_LIMIT_BYTES: "1048576"
NEKIRO_ROUTER_SSE_EVENT_LIMIT_BYTES: "65536"
NEKIRO_ROUTER_RESOLUTION_DEADLINE_MS: "30000"
NEKIRO_ROUTER_AGENT_DEADLINE_MS: "30000"
NEKIRO_ROUTER_AGENT_CREDENTIAL_ISSUER: https://a2a-router.nekiro.test
NEKIRO_ROUTER_AGENT_CREDENTIAL_KEY_ID: root-console-browser-key-1
NEKIRO_ROUTER_AGENT_CREDENTIAL_PRIVATE_KEY_BASE64URL: AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8DoQe_884Qvh1w3RjnS8CZZ-TWMJulDV8d3IZkElUxuA
NEKIRO_ROUTER_AGENT_CREDENTIAL_TTL_SECONDS: "30"
NEKIRO_AGENT_ROUTER_ISSUER: https://a2a-router.nekiro.test
NEKIRO_AGENT_ROUTER_KEY_ID: root-console-browser-key-1
NEKIRO_AGENT_ROUTER_PUBLIC_KEY_BASE64URL: A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg
RUNTIME_A_ROUTER_TOKEN: root-console-runtime-a-token
RUNTIME_A_RESPONSE_LIMIT_BYTES: "1048576"
RUNTIME_A_EVENT_LIMIT_BYTES: "65536"
RUNTIME_B_ROUTER_TOKEN: root-console-runtime-b-token
RUNTIME_B_RESPONSE_LIMIT_BYTES: "1048576"
RUNTIME_B_EVENT_LIMIT_BYTES: "65536"
NEKIRO_E2E_BASE_URL: http://127.0.0.1:4173
NEKIRO_E2E_COMPOSE_PROJECT: nekiro-root-console-browser
NEKIRO_E2E_COMPOSE_FILE: ${{ github.workspace }}/deploy/compose.yaml
VITE_NEKIRO_API_BASE_URL: http://gateway.nekiro.test:18080
VITE_NEKIRO_PROVIDER_ID: root-console-provider
VITE_NEKIRO_PROVIDER_NAME: Root Console Provider
VITE_NEKIRO_PROVIDER_TOKEN: root-console-provider-token
VITE_NEKIRO_OWNER_TOKEN: root-console-owner-token
VITE_NEKIRO_DEFAULT_WORKSPACE_ID: root-console-workspace

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 11.3.0
run_install: false

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 24.16.0
cache: pnpm
cache-dependency-path: pnpm-lock.yaml

- name: Install frontend dependencies
run: pnpm install --frozen-lockfile

- name: Map the Gateway to an explicit non-IP origin
run: echo "127.0.0.1 gateway.nekiro.test" | sudo tee -a /etc/hosts

- name: Start fresh platform Compose
run: docker compose --project-name "$NEKIRO_E2E_COMPOSE_PROJECT" --file "$NEKIRO_E2E_COMPOSE_FILE" up --build --detach --wait --wait-timeout 120

- name: Install Chromium
run: pnpm --dir apps/console exec playwright install --with-deps chromium

- name: Build production Console with explicit browser configuration
run: pnpm --dir apps/console run build

- name: Run production browser acceptance
run: pnpm --dir apps/console run test:e2e

- name: Capture sanitized console acceptance backend logs
if: always()
shell: bash
run: |
docker compose --project-name "$NEKIRO_E2E_COMPOSE_PROJECT" --file "$NEKIRO_E2E_COMPOSE_FILE" logs --no-color 2>&1 |
perl -pe '
BEGIN {
@secret_names = qw(
POSTGRES_PASSWORD NEKIRO_COMPOSE_DATABASE_URL
NEKIRO_ROUTER_INTERNAL_BEARER_TOKEN NEKIRO_CONTROL_PLANE_SERVICE_TOKEN
NEKIRO_ROUTER_AGENT_CREDENTIAL_KEY_ID NEKIRO_ROUTER_AGENT_CREDENTIAL_PRIVATE_KEY_BASE64URL
NEKIRO_AGENT_ROUTER_KEY_ID NEKIRO_AGENT_ROUTER_PUBLIC_KEY_BASE64URL
RUNTIME_A_ROUTER_TOKEN RUNTIME_B_ROUTER_TOKEN
VITE_NEKIRO_PROVIDER_TOKEN VITE_NEKIRO_OWNER_TOKEN
);
@secrets = grep { defined($_) && length($_) } @ENV{@secret_names};
@fixtures = qw(
browser-json browser-sse
);
}
for my $value (@secrets, @fixtures) { s/\Q$value\E/[REDACTED]/g; }
s/\b[0-9a-f]{64}\b/[REDACTED-64-HEX]/g;
s{(?:[A-Za-z0-9_-]+\.){2}[A-Za-z0-9_-]+}{[REDACTED-ROUTER-CREDENTIAL]}g;
s/\b[A-Za-z0-9_-]{86}\b/[REDACTED-ED25519-SIGNATURE]/g;
s/\brtj_[A-Za-z0-9._:-]*/[REDACTED-ROUTER-JTI]/g;
'

- name: Tear down fresh platform Compose
if: always()
run: docker compose --project-name "$NEKIRO_E2E_COMPOSE_PROJECT" --file "$NEKIRO_E2E_COMPOSE_FILE" down --volumes --remove-orphans

compose-config:
runs-on: ubuntu-latest
timeout-minutes: 15
Expand All @@ -139,7 +269,7 @@ jobs:
NEKIRO_DEV_AUTH_PRINCIPALS_JSON: '[{"id":"compose-check","tokenSha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}]'
NEKIRO_INTERNAL_DEV_AUTH_PRINCIPALS_JSON: '[{"id":"compose-router-check","tokenSha256":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}]'
NEKIRO_ROUTER_SERVICE_PRINCIPALS_JSON: '[{"id":"compose-control-plane","tokenSha256":"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"}]'
NEKIRO_ROUTER_AGENT_PRINCIPALS_JSON: '[{"workspaceId":"workspace-acceptance","agentId":"runtime-a","tokenSha256":"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"}]'
NEKIRO_ROUTER_AGENT_PRINCIPALS_JSON: '[{"workspaceId":"workspace-acceptance","agentId":"runtime-a","tokenSha256":"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"},{"workspaceId":"workspace-acceptance","agentId":"runtime-b","tokenSha256":"cc7c23956db7e7fbf8d5ee1948b5f056d62599d6fbd4af42c8a73ed9a1dff7e0"}]'
NEKIRO_ROUTER_INTERNAL_BEARER_TOKEN: compose-router-token
NEKIRO_CONTROL_PLANE_SERVICE_TOKEN: compose-control-plane-token
NEKIRO_CORS_ALLOWED_ORIGINS: http://127.0.0.1:3000
Expand Down Expand Up @@ -170,6 +300,9 @@ jobs:
RUNTIME_A_ROUTER_TOKEN: runtime-a-token
RUNTIME_A_RESPONSE_LIMIT_BYTES: "1048576"
RUNTIME_A_EVENT_LIMIT_BYTES: "65536"
RUNTIME_B_ROUTER_TOKEN: runtime-b-router-check
RUNTIME_B_RESPONSE_LIMIT_BYTES: "1048576"
RUNTIME_B_EVENT_LIMIT_BYTES: "65536"
CONTROL_PLANE_PORT: "18080"
A2A_ROUTER_PORT: "18081"
run: docker compose --file deploy/compose.yaml config --quiet
Expand Down Expand Up @@ -201,6 +334,15 @@ jobs:
working-directory: agents/runtime-a
run: go test -race ./...

- name: Test Runtime B sample
run: go test -count=1 ./agents/runtime-b ./agents/runtime-b/cmd/runtime-b

- name: Vet Runtime B sample
run: go vet ./agents/runtime-b ./agents/runtime-b/cmd/runtime-b

- name: Race test Runtime B sample
run: go test -race ./agents/runtime-b ./agents/runtime-b/cmd/runtime-b

backend-acceptance:
runs-on: ubuntu-latest
timeout-minutes: 35
Expand All @@ -215,7 +357,7 @@ jobs:
NEKIRO_DEV_AUTH_PRINCIPALS_JSON: '[{"id":"acceptance-owner","tokenSha256":"465aedffb32a2cb642cbca8fc75b806bcd33f703d70c49dcfb05e9db88df32d2"},{"id":"acceptance-user","tokenSha256":"2af4f9af4fa535905378ccee817aa532244dcf102f3d3ebeaf9a2a92abdeb42d"},{"id":"acceptance-other","tokenSha256":"7f85fe19123d4f88c475cb754dd30f422877ba3b3e2d5eed8ff8c2f9453ebeaf"}]'
NEKIRO_INTERNAL_DEV_AUTH_PRINCIPALS_JSON: '[{"id":"router-internal","tokenSha256":"f9232718425b5ebee721187a79703448bce513ecf0600eb161f9256ddac27c4d"}]'
NEKIRO_ROUTER_SERVICE_PRINCIPALS_JSON: '[{"id":"control-plane","tokenSha256":"5abfd00de27c6b2f57d45fdc90999134e4e088414ba1f39bf67ee0d1c9cec554"}]'
NEKIRO_ROUTER_AGENT_PRINCIPALS_JSON: '[{"workspaceId":"workspace-acceptance","agentId":"runtime-a","tokenSha256":"e304d0370532633d535824a897d5c03445b636e8d1649064aa35a8fb50fef200"}]'
NEKIRO_ROUTER_AGENT_PRINCIPALS_JSON: '[{"workspaceId":"workspace-acceptance","agentId":"runtime-a","tokenSha256":"e304d0370532633d535824a897d5c03445b636e8d1649064aa35a8fb50fef200"},{"workspaceId":"workspace-acceptance","agentId":"runtime-b","tokenSha256":"9b990de9bb74efd4e1d26a43a01e132deb60d563d49faf6878dca4af40858a38"}]'
NEKIRO_ROUTER_INTERNAL_BEARER_TOKEN: router-internal-token
NEKIRO_CONTROL_PLANE_SERVICE_TOKEN: control-plane-internal-token
NEKIRO_CORS_ALLOWED_ORIGINS: http://127.0.0.1:3000
Expand Down Expand Up @@ -248,6 +390,9 @@ jobs:
RUNTIME_A_ROUTER_TOKEN: runtime-a-router-token
RUNTIME_A_RESPONSE_LIMIT_BYTES: "1048576"
RUNTIME_A_EVENT_LIMIT_BYTES: "65536"
RUNTIME_B_ROUTER_TOKEN: runtime-b-router-token
RUNTIME_B_RESPONSE_LIMIT_BYTES: "1048576"
RUNTIME_B_EVENT_LIMIT_BYTES: "65536"
NEKIRO_E2E_CONTROL_PLANE_URL: http://127.0.0.1:18080
NEKIRO_E2E_ROUTER_URL: http://127.0.0.1:18081
NEKIRO_E2E_ROUTER_TOKEN: router-internal-token
Expand Down Expand Up @@ -285,7 +430,7 @@ jobs:
NEKIRO_ROUTER_INTERNAL_BEARER_TOKEN NEKIRO_CONTROL_PLANE_SERVICE_TOKEN
NEKIRO_ROUTER_AGENT_CREDENTIAL_KEY_ID NEKIRO_ROUTER_AGENT_CREDENTIAL_PRIVATE_KEY_BASE64URL
NEKIRO_AGENT_ROUTER_KEY_ID NEKIRO_AGENT_ROUTER_PUBLIC_KEY_BASE64URL
RUNTIME_A_ROUTER_TOKEN NEKIRO_E2E_ROUTER_TOKEN NEKIRO_E2E_OWNER_TOKEN
RUNTIME_A_ROUTER_TOKEN RUNTIME_B_ROUTER_TOKEN NEKIRO_E2E_ROUTER_TOKEN NEKIRO_E2E_OWNER_TOKEN
NEKIRO_E2E_USER_TOKEN NEKIRO_E2E_OTHER_TOKEN NEKIRO_E2E_DATABASE_URL
);
@secrets = grep { defined($_) && length($_) } @ENV{@secret_names};
Expand Down
2 changes: 1 addition & 1 deletion .specify/feature.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"feature_directory": "specs/026-trusted-publication-acceptance"
"feature_directory": "specs/027-console-trusted-publication"
}
13 changes: 12 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,20 @@
<!-- SPECKIT START -->
For additional context about technologies to be used, project structure,
shell commands, and other important information, read the current plan
at specs/026-trusted-publication-acceptance/plan.md
at specs/027-console-trusted-publication/plan.md
<!-- SPECKIT END -->

## Current delivery status

Spec 027 has implemented the production Console, trusted publication operations,
the reverse Runtime B -> Router -> Runtime A acceptance, and real frontend CI.
The reviewed Console is imported under `apps/console`; root CI run
`30319275997` passed all seven checks. The prior Spec 026-only status sentence
below is historical and is superseded for the current checkout. Slice C and
Slice D are currently combined in root PR #62, so the literal independent-PR
closure gate remains open until that delivery scope is split or explicitly
re-approved.

本文件是整个仓库的长期项目宪章,适用于所有目录、模块和参与者。它记录稳定的产品目标、领域语言、架构边界和交付标准,不替代具体需求、API 文档或 ADR。

本文件应根据项目当前状态、已验证需求和正式架构决策持续自我更新迭代,但任何更新都必须说明原因与影响,并保持核心边界和兼容性变化清晰可追溯。
Expand Down
20 changes: 12 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,18 @@ capability, Invocation, Task, parent lineage, Trace, and endpoint origin.
Both sample Runtimes verify the credential and reject direct execution before
runtime logic; stream cancellation receives a separate one-time `jti`.

Frontend Console work remains paused and `apps/console` is not yet present. The
thin Go Agent SDK, Router-owned nested adapter, isolated Runtime A, cross-Runtime
nested invocation, and process/Compose wiring are implemented. CI run
`30060752722` passed root build/test/race/vet/lint, Runtime A test/vet/race,
PostgreSQL integration, Compose configuration, Frontend, Codecov, and the real
authenticated Invoke-to-Record acceptance. The repository therefore proves
the backend/headless Phase 1 loop, but not yet the user-facing Console or the
later production governance and deployment integration stages.
The production Console is now imported under `apps/console` and exercises the
Gateway-only trusted workflow: Register -> Verify -> Publish -> Discover ->
Install -> Invoke -> Record. It preserves the four isolated comparison demo
routes, keeps provider and Workspace-owner credentials separate and transient,
and displays Gateway-provided Invocation/Trace lineage. Root CI run
`30319275997` passed all seven checks, including the fresh Compose backend
acceptance and production `console-browser-acceptance`. The standalone Console
source and its independently reviewed UI/browser PRs remain in
[NeKiro-Console](https://github.com/NeKiro-project/NeKiro-Console); root
integration is tracked by [PR #62](https://github.com/NeKiro-project/NeKiro/pull/62).
Independent root PR separation for the backend reverse fixture and frontend
integration remains a delivery-closure item, not a runtime behavior gap.

The Go Workspace Client SDK under `sdks/client-sdk` is the application-facing
entry point for invoking an installed Agent through Gateway. One immutable
Expand Down
14 changes: 14 additions & 0 deletions agents/runtime-a/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,20 @@ func (handler *Handler) OnSendMessage(ctx context.Context, params *a2a.MessageSe
if !ok {
return nil, invalidParams("managed A2A call context is required")
}
claims, ok := routerauth.ClaimsFromContext(ctx)
if !ok {
return nil, invalidParams("managed Router credential context is required")
}
if claims.AgentID != handler.config.AgentID {
return nil, invalidParams("managed Router credential Agent identity is invalid")
}
if claims.Capability == "runtime.echo" {
value, err := rootInputValue(params.Message)
if err != nil {
return nil, err
}
return responderMessage(params.Message, value), nil
}
platformContext, err := handler.service.platformContext(callContext.RequestMeta())
if err != nil {
return nil, err
Expand Down
29 changes: 29 additions & 0 deletions agents/runtime-a/nested.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,35 @@ func rootInput(message *a2a.Message) (json.RawMessage, error) {
return input, nil
}

func rootInputValue(message *a2a.Message) (json.RawMessage, error) {
input, err := rootInput(message)
if err != nil {
return nil, err
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(input, &fields); err != nil || fields == nil || len(fields) != 2 {
return nil, errors.New("runtime-a responder input is invalid")
}
value, ok := fields["value"]
if !ok || len(value) == 0 || !json.Valid(value) {
return nil, errors.New("runtime-a responder value is missing")
}
return value, nil
}

func responderMessage(input *a2a.Message, value json.RawMessage) *a2a.Message {
return &a2a.Message{
ID: "runtime-a-responder-result-" + input.ID,
ContextID: input.ContextID,
Role: a2a.MessageRoleAgent,
Parts: []a2a.Part{a2a.DataPart{Data: map[string]any{
"agent": "runtime-a",
"fixture": "success",
"value": value,
}}},
}
}

func combinedResult(result *agentsdk.NestedResult) (json.RawMessage, error) {
if result == nil {
return nil, errors.New("runtime-a nested result is required")
Expand Down
Loading
Loading