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
81 changes: 81 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
name: Release

on:
push:
tags:
- "v*"

permissions:
contents: write
packages: write

jobs:
images:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Check out annotated release tag
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
fetch-depth: 0

- name: Validate release identity
shell: bash
run: |
if [[ ! "$GITHUB_REF_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo 'release tag must be a stable semantic version such as v0.1.0' >&2
exit 1
fi
test "$(git cat-file -t "refs/tags/$GITHUB_REF_NAME")" = tag
test "$(git rev-list -n 1 "refs/tags/$GITHUB_REF_NAME")" = "$GITHUB_SHA"

- name: Authenticate to GHCR
shell: bash
env:
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: echo "$GHCR_TOKEN" | docker login ghcr.io --username "$GITHUB_ACTOR" --password-stdin

- name: Build and publish immutable images
shell: bash
run: |
docker buildx create --use --name nekiro-release
for component in control-plane a2a-router; do
image="ghcr.io/nekiro-project/nekiro-${component}:${GITHUB_REF_NAME}"
dockerfile="apps/${component}/Dockerfile"
docker buildx build \
--file "$dockerfile" \
--platform linux/amd64,linux/arm64 \
--provenance=mode=max \
--sbom=true \
--tag "$image" \
--push \
.
done

- name: Record immutable image digests
shell: bash
run: |
control_image="ghcr.io/nekiro-project/nekiro-control-plane:${GITHUB_REF_NAME}"
router_image="ghcr.io/nekiro-project/nekiro-a2a-router:${GITHUB_REF_NAME}"
control_digest=$(docker buildx imagetools inspect "$control_image" | awk '/^Digest:/ { print $2; exit }')
router_digest=$(docker buildx imagetools inspect "$router_image" | awk '/^Digest:/ { print $2; exit }')
for digest in "$control_digest" "$router_digest"; do
[[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]
done
jq -n \
--arg tag "$GITHUB_REF_NAME" \
--arg commit "$GITHUB_SHA" \
--arg controlImage "$control_image" \
--arg controlDigest "$control_digest" \
--arg routerImage "$router_image" \
--arg routerDigest "$router_digest" \
'{schemaVersion:"1",tag:$tag,commitSha:$commit,platformApiVersion:"v1",images:{controlPlane:{reference:$controlImage,digest:$controlDigest},a2aRouter:{reference:$routerImage,digest:$routerDigest}}}' \
> images.json
jq -e . images.json >/dev/null
sha256sum images.json > checksums.txt

- name: Publish GitHub release evidence
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release create "$GITHUB_REF_NAME" images.json checksums.txt --repo "$GITHUB_REPOSITORY" --verify-tag --generate-notes --title "NeKiro Core $GITHUB_REF_NAME"
4 changes: 2 additions & 2 deletions .github/workflows/satellite-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ jobs:

stack:
needs: resolve
uses: NeKiro-project/NeKiro-Stack/.github/workflows/core-integration.yml@12a651e887079b0b3ad40fa7c2fd21371d2ee935
uses: NeKiro-project/NeKiro-Stack/.github/workflows/core-integration.yml@723989261c91770f6eb025fe8556331ceb2ed8ee
with:
stack_sha: 12a651e887079b0b3ad40fa7c2fd21371d2ee935
stack_sha: 723989261c91770f6eb025fe8556331ceb2ed8ee
core_sha: ${{ needs.resolve.outputs.core_sha }}
permissions:
contents: read
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ Agents through a framework-owned lifecycle:
Register -> Discover -> Install -> Invoke -> Record
```

The first released HTTP surface uses one version per owned boundary: Gateway
routes are under `/v1`, Control Plane and Router service routes are under
`/internal/v1`, and Agent-to-Router calls are under `/agent/v1`. Pre-release
`/v2`, `/v3`, and `/v4` routes are not served. See the
[Platform API v1 migration](docs/usage/platform-api-v1-migration.md).

- **Runtime agnostic**: an Agent may use tRPC-Agent-Go, `a2a-go`, another
framework, or a custom runtime.
- **Contract first**: Agent Cards, Releases, HTTP APIs, internal APIs, A2A
Expand Down Expand Up @@ -513,6 +519,15 @@ Security and compatibility decisions are documented as ADRs under
credentials, instance discovery, registration leases, and Nacos transport
security.

## Releases

An annotated semantic tag publishes multi-architecture Control Plane and
Router images to GHCR with OCI provenance and SBOM attestations. The release
attaches an `images.json` file containing the exact tag, Core commit, image
references, and immutable manifest digests. NeKiro-Stack owns the compatible
cross-repository manifest and product acceptance; Core images alone are not a
product release.

## Development and verification

PostgreSQL integration suites require an explicit dedicated database whose
Expand Down
22 changes: 11 additions & 11 deletions apps/a2a-router/cmd/a2a-router/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,14 @@ func (failingDoer) Do(*http.Request) (*http.Response, error) {
type ledgerAppenderStub struct{}

func (ledgerAppenderStub) Append(context.Context, contracts.InvocationEventV03) error { return nil }
func (ledgerAppenderStub) GetInvocation(context.Context, string, string) (contracts.InvocationDetailResponseV4, error) {
return contracts.InvocationDetailResponseV4{}, nil
func (ledgerAppenderStub) GetInvocation(context.Context, string, string) (contracts.InvocationDetailResponseV1, error) {
return contracts.InvocationDetailResponseV1{}, nil
}
func (ledgerAppenderStub) GetTrace(context.Context, string, contracts.TraceID) (contracts.TraceResponseV4, error) {
return contracts.TraceResponseV4{}, nil
func (ledgerAppenderStub) GetTrace(context.Context, string, contracts.TraceID) (contracts.TraceResponseV1, error) {
return contracts.TraceResponseV1{}, nil
}
func (ledgerAppenderStub) GetInvocationByParentID(context.Context, string) (contracts.InvocationDetailResponseV4, error) {
return contracts.InvocationDetailResponseV4{}, nil
func (ledgerAppenderStub) GetInvocationByParentID(context.Context, string) (contracts.InvocationDetailResponseV1, error) {
return contracts.InvocationDetailResponseV1{}, nil
}

func TestRunRequiresExplicitCommandAndMigrationDirection(t *testing.T) {
Expand Down Expand Up @@ -92,8 +92,8 @@ func TestNewHandlerAssemblesReadinessWithoutDependencyProbe(t *testing.T) {
ListenAddress: "127.0.0.1:9090",
RouterPrincipals: []auth.Principal{{ID: "router", TokenSHA256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}},
AgentPrincipals: []nested.AgentPrincipal{{WorkspaceID: "workspace-a", AgentID: "runtime-a", TokenSHA256: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"}},
ControlPlaneResolveURL: "https://control.internal/internal/v2/resolve-agent",
ControlPlaneVersionURL: "https://control.internal/internal/v3/resolve-installed-version",
ControlPlaneResolveURL: "https://control.internal/internal/v1/resolve-agent",
ControlPlaneVersionURL: "https://control.internal/internal/v1/resolve-installed-version",
ControlPlaneServiceToken: "control-token",
InternalRequestLimitBytes: 1024,
AgentRequestLimitBytes: 1024,
Expand All @@ -114,7 +114,7 @@ func TestNewHandlerAssemblesReadinessWithoutDependencyProbe(t *testing.T) {
t.Fatalf("status=%d", response.Code)
}
readResponse := httptest.NewRecorder()
handler.ServeHTTP(readResponse, httptest.NewRequest(http.MethodGet, "/internal/v3/workspaces/workspace-a/invocations/inv-a", nil))
handler.ServeHTTP(readResponse, httptest.NewRequest(http.MethodGet, "/internal/v1/workspaces/workspace-a/invocations/inv-a", nil))
if readResponse.Code != http.StatusUnauthorized {
t.Fatalf("metadata read route status=%d, want 401", readResponse.Code)
}
Expand All @@ -124,8 +124,8 @@ func TestNewHandlerRegistersTopologyStatusOnlyForObservedSelector(t *testing.T)
cfg := config.Config{
RouterPrincipals: []auth.Principal{{ID: "router", TokenSHA256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}},
AgentPrincipals: []nested.AgentPrincipal{{WorkspaceID: "workspace-a", AgentID: "runtime-a", TokenSHA256: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"}},
ControlPlaneResolveURL: "https://control.internal/internal/v2/resolve-agent",
ControlPlaneVersionURL: "https://control.internal/internal/v3/resolve-installed-version",
ControlPlaneResolveURL: "https://control.internal/internal/v1/resolve-agent",
ControlPlaneVersionURL: "https://control.internal/internal/v1/resolve-installed-version",
ControlPlaneServiceToken: "control-token",
InternalRequestLimitBytes: 1024,
AgentRequestLimitBytes: 1024,
Expand Down
4 changes: 2 additions & 2 deletions apps/a2a-router/internal/api/agent_invocation_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
)

// VersionResolver resolves the deterministic installed Agent Card version
// from the Control Plane Internal v3 endpoint.
// from the Control Plane Internal v1 endpoint.
type VersionResolver interface {
ResolveInstalledVersion(context.Context, contracts.ResolveInstalledVersionRequest) (contracts.ResolveInstalledVersionResponse, error)
}
Expand All @@ -25,7 +25,7 @@ type VersionResolver interface {
// Ledger by invocation ID only. The authenticated principal is checked
// against both the parent Workspace and target Agent before child derivation.
type NestedLedgerReader interface {
GetInvocationByParentID(context.Context, string) (contracts.InvocationDetailResponseV4, error)
GetInvocationByParentID(context.Context, string) (contracts.InvocationDetailResponseV1, error)
}

// AgentInvocationHandler handles Agent-facing nested invocation requests at
Expand Down
14 changes: 7 additions & 7 deletions apps/a2a-router/internal/api/agent_invocation_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ func agentTokenDigest(token string) string {
}

type mockNestedLedgerReader struct {
invocation contracts.InvocationDetailResponseV4
invocation contracts.InvocationDetailResponseV1
err error
calls int
}

func (m *mockNestedLedgerReader) GetInvocationByParentID(_ context.Context, _ string) (contracts.InvocationDetailResponseV4, error) {
func (m *mockNestedLedgerReader) GetInvocationByParentID(_ context.Context, _ string) (contracts.InvocationDetailResponseV1, error) {
m.calls++
return m.invocation, m.err
}
Expand Down Expand Up @@ -84,9 +84,9 @@ func newTestAgentHandler(t *testing.T, ledgerReader NestedLedgerReader, versionR
return handler, token
}

func runningParentDetail() contracts.InvocationDetailResponseV4 {
return contracts.InvocationDetailResponseV4{
Invocation: contracts.InvocationRecordV4{
func runningParentDetail() contracts.InvocationDetailResponseV1 {
return contracts.InvocationDetailResponseV1{
Invocation: contracts.InvocationRecordV1{
InvocationID: "inv_parent123",
RootTaskID: "task_root456",
TraceID: "trc_abc123_1",
Expand Down Expand Up @@ -643,8 +643,8 @@ func TestAgentHandlerNestedJSONSuccessPath(t *testing.T) {
// Agent ID cannot use a credential bound to one Workspace to reference a
// parent from another Workspace.
func TestAgentHandlerCrossWorkspaceParentMismatch(t *testing.T) {
foreignParent := contracts.InvocationDetailResponseV4{
Invocation: contracts.InvocationRecordV4{
foreignParent := contracts.InvocationDetailResponseV1{
Invocation: contracts.InvocationRecordV1{
InvocationID: "inv_foreign999",
RootTaskID: "task_foreign_root",
TraceID: "trc_foreign_1",
Expand Down
Loading
Loading