diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..04aee4ea --- /dev/null +++ b/.github/workflows/release.yml @@ -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" diff --git a/.github/workflows/satellite-integration.yml b/.github/workflows/satellite-integration.yml index 55853756..ce5f2ee6 100644 --- a/.github/workflows/satellite-integration.yml +++ b/.github/workflows/satellite-integration.yml @@ -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 diff --git a/README.md b/README.md index 6317515f..0d8c9db0 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/apps/a2a-router/cmd/a2a-router/main_test.go b/apps/a2a-router/cmd/a2a-router/main_test.go index 4cc6a58b..b013dd71 100644 --- a/apps/a2a-router/cmd/a2a-router/main_test.go +++ b/apps/a2a-router/cmd/a2a-router/main_test.go @@ -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) { @@ -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, @@ -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) } @@ -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, diff --git a/apps/a2a-router/internal/api/agent_invocation_handler.go b/apps/a2a-router/internal/api/agent_invocation_handler.go index 3542ce78..9c506ab8 100644 --- a/apps/a2a-router/internal/api/agent_invocation_handler.go +++ b/apps/a2a-router/internal/api/agent_invocation_handler.go @@ -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) } @@ -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 diff --git a/apps/a2a-router/internal/api/agent_invocation_handler_test.go b/apps/a2a-router/internal/api/agent_invocation_handler_test.go index 03e654f3..fa8f5ab9 100644 --- a/apps/a2a-router/internal/api/agent_invocation_handler_test.go +++ b/apps/a2a-router/internal/api/agent_invocation_handler_test.go @@ -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 } @@ -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", @@ -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", diff --git a/apps/a2a-router/internal/api/dispatch_handler.go b/apps/a2a-router/internal/api/dispatch_handler.go index d1113855..9880d101 100644 --- a/apps/a2a-router/internal/api/dispatch_handler.go +++ b/apps/a2a-router/internal/api/dispatch_handler.go @@ -36,15 +36,15 @@ type Resolver interface { } type NonStreamingTransport interface { - ValidateNonStreamingTarget(contracts.DispatchInvocationRequestV4, contracts.ResolveAgentResponse) error - SendNonStreaming(context.Context, contracts.DispatchInvocationRequestV4, contracts.ResolveAgentResponse) (json.RawMessage, error) - ValidateNonStreamingInput(contracts.DispatchInvocationRequestV4, contracts.ResolveAgentResponse) error + ValidateNonStreamingTarget(contracts.DispatchInvocationRequestV1, contracts.ResolveAgentResponse) error + SendNonStreaming(context.Context, contracts.DispatchInvocationRequestV1, contracts.ResolveAgentResponse) (json.RawMessage, error) + ValidateNonStreamingInput(contracts.DispatchInvocationRequestV1, contracts.ResolveAgentResponse) error } type StreamingTransport interface { - SendStreaming(context.Context, contracts.DispatchInvocationRequestV4, contracts.ResolveAgentResponse) iter.Seq2[streammodel.Event, error] - ValidateStreamingTarget(contracts.DispatchInvocationRequestV4, contracts.ResolveAgentResponse) error - ValidateStreamingInput(contracts.DispatchInvocationRequestV4, contracts.ResolveAgentResponse) error + SendStreaming(context.Context, contracts.DispatchInvocationRequestV1, contracts.ResolveAgentResponse) iter.Seq2[streammodel.Event, error] + ValidateStreamingTarget(contracts.DispatchInvocationRequestV1, contracts.ResolveAgentResponse) error + ValidateStreamingInput(contracts.DispatchInvocationRequestV1, contracts.ResolveAgentResponse) error } var errSSEFrameTooLarge = errors.New("SSE event exceeds the configured limit") @@ -192,7 +192,7 @@ func NewDispatchHandlerWithTransportAndLedgerAndStreaming(authenticator Authenti } func (handler *DispatchHandler) RegisterRoutes(mux *http.ServeMux) { - mux.HandleFunc("POST /internal/v4/invocations", handler.dispatch) + mux.HandleFunc("POST /internal/v1/invocations", handler.dispatch) } // DispatchChild performs resolution, transport, and Ledger for an @@ -201,7 +201,7 @@ func (handler *DispatchHandler) RegisterRoutes(mux *http.ServeMux) { // derivation. The accept header controls JSON/SSE result mode. // Unlike the internal dispatch path, DispatchChild accepts caller type // "agent" and propagates ParentInvocationID to Ledger events. -func (handler *DispatchHandler) DispatchChild(writer http.ResponseWriter, request *http.Request, dispatchRequest contracts.DispatchInvocationRequestV4, accept string) { +func (handler *DispatchHandler) DispatchChild(writer http.ResponseWriter, request *http.Request, dispatchRequest contracts.DispatchInvocationRequestV1, accept string) { if _, err := contracts.NegotiateInvocationResultMode(dispatchRequest.Stream, accept); err != nil { handler.writePreError(writer, dispatchRequest.TraceID, contracts.ErrorCodeNotAcceptable) return @@ -423,36 +423,36 @@ func resolvedDeadlineContext(parent context.Context, timeoutMS int64, invocation var errPayloadTooLarge = errors.New("router dispatch payload is too large") -func (handler *DispatchHandler) readRequest(request *http.Request) (contracts.DispatchInvocationRequestV4, error) { +func (handler *DispatchHandler) readRequest(request *http.Request) (contracts.DispatchInvocationRequestV1, error) { if request.ContentLength > handler.requestLimit { - return contracts.DispatchInvocationRequestV4{}, errPayloadTooLarge + return contracts.DispatchInvocationRequestV1{}, errPayloadTooLarge } data, err := io.ReadAll(io.LimitReader(request.Body, handler.requestLimit+1)) if closeErr := request.Body.Close(); err == nil { err = closeErr } if err != nil { - return contracts.DispatchInvocationRequestV4{}, err + return contracts.DispatchInvocationRequestV1{}, err } if int64(len(data)) > handler.requestLimit { - return contracts.DispatchInvocationRequestV4{}, errPayloadTooLarge + return contracts.DispatchInvocationRequestV1{}, errPayloadTooLarge } if err := rejectDuplicateMembers(data); err != nil { - return contracts.DispatchInvocationRequestV4{}, err + return contracts.DispatchInvocationRequestV1{}, err } - var value contracts.DispatchInvocationRequestV4 + var value contracts.DispatchInvocationRequestV1 decoder := json.NewDecoder(bytes.NewReader(data)) decoder.DisallowUnknownFields() if err := decoder.Decode(&value); err != nil { - return contracts.DispatchInvocationRequestV4{}, err + return contracts.DispatchInvocationRequestV1{}, err } if err := requireEOF(decoder); err != nil { - return contracts.DispatchInvocationRequestV4{}, err + return contracts.DispatchInvocationRequestV1{}, err } return value, nil } -func validateDispatch(value contracts.DispatchInvocationRequestV4) error { +func validateDispatch(value contracts.DispatchInvocationRequestV1) error { if value.ParentInvocationID != "" { return errors.New("root dispatch must not carry parent invocation id") } @@ -483,7 +483,7 @@ func validateDispatch(value contracts.DispatchInvocationRequestV4) error { // validateChildDispatch validates a trusted child dispatch request. Unlike // validateDispatch, it accepts caller type "agent" and requires a non-empty // ParentInvocationID for Ledger lineage. -func validateChildDispatch(value contracts.DispatchInvocationRequestV4) error { +func validateChildDispatch(value contracts.DispatchInvocationRequestV1) error { for _, identifier := range []string{value.InvocationID, value.RootTaskID, value.WorkspaceID, value.TargetAgentID, value.Capability, value.Caller.ID} { if !validIdentifier(identifier) { return errors.New("child dispatch identifier is invalid") @@ -511,7 +511,7 @@ func validateChildDispatch(value contracts.DispatchInvocationRequestV4) error { return nil } -func validateResolvedReleaseProvenance(request contracts.DispatchInvocationRequestV4, resolved contracts.ResolveAgentResponse) error { +func validateResolvedReleaseProvenance(request contracts.DispatchInvocationRequestV1, resolved contracts.ResolveAgentResponse) error { if err := contracts.ValidateInvocationReleaseProvenance(resolved.Installation.InstalledReleaseID, resolved.Installation.AgentCardDigest); err != nil { return err } @@ -540,7 +540,7 @@ func (handler *DispatchHandler) writePreError(writer http.ResponseWriter, traceI writeJSON(writer, status, traceID, payload) } -func (handler *DispatchHandler) writeCorrelatedError(writer http.ResponseWriter, request contracts.DispatchInvocationRequestV4, code contracts.PlatformErrorCode) { +func (handler *DispatchHandler) writeCorrelatedError(writer http.ResponseWriter, request contracts.DispatchInvocationRequestV1, code contracts.PlatformErrorCode) { status := errorStatus(code) payload, err := contracts.NewCorrelatedPlatformErrorV4(code, request.TraceID, request.InvocationID, request.RootTaskID) if err != nil { @@ -550,7 +550,7 @@ func (handler *DispatchHandler) writeCorrelatedError(writer http.ResponseWriter, writeJSON(writer, status, request.TraceID, payload) } -func (handler *DispatchHandler) writeInvocationResult(writer http.ResponseWriter, request contracts.DispatchInvocationRequestV4, result json.RawMessage) { +func (handler *DispatchHandler) writeInvocationResult(writer http.ResponseWriter, request contracts.DispatchInvocationRequestV1, result json.RawMessage) { payload := contracts.InvocationResult{ SchemaVersion: contracts.InvocationResultSchemaVersion, InvocationID: request.InvocationID, @@ -562,7 +562,7 @@ func (handler *DispatchHandler) writeInvocationResult(writer http.ResponseWriter writeJSON(writer, http.StatusOK, request.TraceID, payload) } -func (handler *DispatchHandler) dispatchNonStreamingWithLedger(ctx context.Context, writer http.ResponseWriter, request contracts.DispatchInvocationRequestV4, resolved contracts.ResolveAgentResponse, targetErr error) { +func (handler *DispatchHandler) dispatchNonStreamingWithLedger(ctx context.Context, writer http.ResponseWriter, request contracts.DispatchInvocationRequestV1, resolved contracts.ResolveAgentResponse, targetErr error) { startedAt := time.Now().UTC().Truncate(time.Microsecond) initialEvents := []contracts.InvocationEventV03{ lifecycleEvent(request, 0, "created", "pending", startedAt), @@ -624,7 +624,7 @@ func (handler *DispatchHandler) dispatchNonStreamingWithLedger(ctx context.Conte handler.writeInvocationResult(writer, request, result) } -func (handler *DispatchHandler) dispatchStreamingWithLedger(ctx context.Context, cancel context.CancelFunc, response http.ResponseWriter, request contracts.DispatchInvocationRequestV4, resolved contracts.ResolveAgentResponse) { +func (handler *DispatchHandler) dispatchStreamingWithLedger(ctx context.Context, cancel context.CancelFunc, response http.ResponseWriter, request contracts.DispatchInvocationRequestV1, resolved contracts.ResolveAgentResponse) { startedAt := time.Now().UTC().Truncate(time.Microsecond) childMode := request.ParentInvocationID != "" if !handler.appendInitialLedgerEventsMode(ctx, response, request, startedAt, []contracts.InvocationEventV03{ @@ -806,7 +806,7 @@ func terminalLedgerContext(ctx context.Context) (context.Context, context.Cancel // childMode is true and the sequence-0 created event fails, a // pre-correlation error is written because child acceptance never occurred // (FR-008). After sequence-0 commits, correlated errors are used. -func (handler *DispatchHandler) appendInitialLedgerEventsMode(ctx context.Context, writer http.ResponseWriter, request contracts.DispatchInvocationRequestV4, startedAt time.Time, events []contracts.InvocationEventV03, childMode bool) bool { +func (handler *DispatchHandler) appendInitialLedgerEventsMode(ctx context.Context, writer http.ResponseWriter, request contracts.DispatchInvocationRequestV1, startedAt time.Time, events []contracts.InvocationEventV03, childMode bool) bool { for _, event := range events { if err := handler.ledger.Append(ctx, event); err == nil { continue @@ -848,7 +848,7 @@ func contextTerminal(ctx context.Context) (contracts.PlatformErrorCode, string, } } -func (handler *DispatchHandler) appendStreamingTerminal(ctx context.Context, request contracts.DispatchInvocationRequestV4, sequence int64, startedAt time.Time, latency int64, code contracts.PlatformErrorCode, eventType, status string) error { +func (handler *DispatchHandler) appendStreamingTerminal(ctx context.Context, request contracts.DispatchInvocationRequestV1, sequence int64, startedAt time.Time, latency int64, code contracts.PlatformErrorCode, eventType, status string) error { event, err := terminalLifecycleEvent(request, sequence, eventType, status, terminalOccurredAt(startedAt, sequence), latency, code) if err != nil { return err @@ -866,7 +866,7 @@ func terminalOccurredAt(startedAt time.Time, sequence int64) time.Time { return occurredAt } -func (handler *DispatchHandler) finishStreamingFailure(ctx context.Context, cancel context.CancelFunc, writer *resultStreamWriter, sequence *contracts.RuntimeResultStreamSequenceValidator, request contracts.DispatchInvocationRequestV4, startedAt time.Time, streamSequence, ledgerSequence int64, code contracts.PlatformErrorCode) { +func (handler *DispatchHandler) finishStreamingFailure(ctx context.Context, cancel context.CancelFunc, writer *resultStreamWriter, sequence *contracts.RuntimeResultStreamSequenceValidator, request contracts.DispatchInvocationRequestV1, startedAt time.Time, streamSequence, ledgerSequence int64, code contracts.PlatformErrorCode) { typeValue, status := streamFailureType(code) event, err := streamFailureEvent(request, streamSequence, typeValue, status, code) if err != nil { @@ -888,7 +888,7 @@ func (handler *DispatchHandler) finishStreamingFailure(ctx context.Context, canc _ = sequence.Finish() } -func (handler *DispatchHandler) finishStreamingFailureWithoutLedger(ctx context.Context, cancel context.CancelFunc, writer *resultStreamWriter, sequence *contracts.RuntimeResultStreamSequenceValidator, request contracts.DispatchInvocationRequestV4, streamSequence int64, code contracts.PlatformErrorCode) { +func (handler *DispatchHandler) finishStreamingFailureWithoutLedger(ctx context.Context, cancel context.CancelFunc, writer *resultStreamWriter, sequence *contracts.RuntimeResultStreamSequenceValidator, request contracts.DispatchInvocationRequestV1, streamSequence int64, code contracts.PlatformErrorCode) { typeValue, status := streamFailureType(code) event, err := streamFailureEvent(request, streamSequence, typeValue, status, code) if err != nil { @@ -906,7 +906,7 @@ func (handler *DispatchHandler) finishStreamingFailureWithoutLedger(ctx context. _ = sequence.Finish() } -func streamFailureEvent(request contracts.DispatchInvocationRequestV4, sequence int64, eventType contracts.ResultStreamEventType, status string, code contracts.PlatformErrorCode) (contracts.InvocationResultStreamEventV2, error) { +func streamFailureEvent(request contracts.DispatchInvocationRequestV1, sequence int64, eventType contracts.ResultStreamEventType, status string, code contracts.PlatformErrorCode) (contracts.InvocationResultStreamEventV2, error) { platformError, err := contracts.NewCorrelatedPlatformErrorV4(code, request.TraceID, request.InvocationID, request.RootTaskID) if err != nil { return contracts.InvocationResultStreamEventV2{}, err @@ -969,7 +969,7 @@ func streamWriteErrorCode(ctx context.Context, err error) contracts.PlatformErro return contracts.ErrorCodeDependency } -func lifecycleEvent(request contracts.DispatchInvocationRequestV4, sequence int64, eventType, status string, occurredAt time.Time) contracts.InvocationEventV03 { +func lifecycleEvent(request contracts.DispatchInvocationRequestV1, sequence int64, eventType, status string, occurredAt time.Time) contracts.InvocationEventV03 { return contracts.InvocationEventV03{ SchemaVersion: contracts.RuntimeInvocationEventSchemaVersion, EventID: lifecycleEventID(request.InvocationID, sequence, eventType), @@ -991,7 +991,7 @@ func lifecycleEvent(request contracts.DispatchInvocationRequestV4, sequence int6 } } -func terminalLifecycleEvent(request contracts.DispatchInvocationRequestV4, sequence int64, eventType, status string, occurredAt time.Time, latencyMS int64, code contracts.PlatformErrorCode) (contracts.InvocationEventV03, error) { +func terminalLifecycleEvent(request contracts.DispatchInvocationRequestV1, sequence int64, eventType, status string, occurredAt time.Time, latencyMS int64, code contracts.PlatformErrorCode) (contracts.InvocationEventV03, error) { event := lifecycleEvent(request, sequence, eventType, status, occurredAt) event.LatencyMS = &latencyMS platformError, err := contracts.NewCorrelatedPlatformErrorV4(code, request.TraceID, request.InvocationID, request.RootTaskID) diff --git a/apps/a2a-router/internal/api/dispatch_handler_test.go b/apps/a2a-router/internal/api/dispatch_handler_test.go index 7f1c61e9..956cd790 100644 --- a/apps/a2a-router/internal/api/dispatch_handler_test.go +++ b/apps/a2a-router/internal/api/dispatch_handler_test.go @@ -57,7 +57,7 @@ func (stub *resolverStub) Resolve(ctx context.Context, request contracts.Resolve } type transportStub struct { - dispatch contracts.DispatchInvocationRequestV4 + dispatch contracts.DispatchInvocationRequestV1 resolved contracts.ResolveAgentResponse result json.RawMessage calls int @@ -66,7 +66,7 @@ type transportStub struct { } func TestValidateDispatchRejectsRootParentLineage(t *testing.T) { - request := contracts.DispatchInvocationRequestV4{ + request := contracts.DispatchInvocationRequestV1{ InvocationID: "inv-root", RootTaskID: "task-root", ParentInvocationID: "inv-parent", TraceID: "trc_root_1", Caller: contracts.Caller{Type: "user", ID: "user-1"}, WorkspaceID: "workspace-1", TargetAgentID: "agent-1", AgentCardVersion: "1.0.0", @@ -77,13 +77,15 @@ func TestValidateDispatchRejectsRootParentLineage(t *testing.T) { } } -func TestDispatchV3RouteIsRetired(t *testing.T) { +func TestPreReleaseDispatchRoutesAreRetired(t *testing.T) { resolver := &resolverStub{} handler := newDispatchTestHandler(t, authStub{caller: auth.Caller{ID: "control-plane"}}, resolver, 1024) - response := httptest.NewRecorder() - handler.ServeHTTP(response, httptest.NewRequest(http.MethodPost, "/internal/v3/invocations", strings.NewReader(validDispatchBody(false)))) - if response.Code != http.StatusNotFound || resolver.calls != 0 { - t.Fatalf("status=%d resolver calls=%d", response.Code, resolver.calls) + for _, path := range []string{"/internal/v2/invocations", "/internal/v3/invocations", "/internal/v4/invocations"} { + response := httptest.NewRecorder() + handler.ServeHTTP(response, httptest.NewRequest(http.MethodPost, path, strings.NewReader(validDispatchBody(false)))) + if response.Code != http.StatusNotFound || resolver.calls != 0 { + t.Fatalf("path=%s status=%d resolver calls=%d", path, response.Code, resolver.calls) + } } } @@ -93,7 +95,7 @@ type streamingTransportStub struct { err error } -func (stub *streamingTransportStub) SendStreaming(_ context.Context, _ contracts.DispatchInvocationRequestV4, _ contracts.ResolveAgentResponse) iter.Seq2[streammodel.Event, error] { +func (stub *streamingTransportStub) SendStreaming(_ context.Context, _ contracts.DispatchInvocationRequestV1, _ contracts.ResolveAgentResponse) iter.Seq2[streammodel.Event, error] { return func(yield func(streammodel.Event, error) bool) { for _, event := range stub.events { if !yield(event, nil) { @@ -106,11 +108,11 @@ func (stub *streamingTransportStub) SendStreaming(_ context.Context, _ contracts } } -func (stub *streamingTransportStub) ValidateStreamingTarget(_ contracts.DispatchInvocationRequestV4, _ contracts.ResolveAgentResponse) error { +func (stub *streamingTransportStub) ValidateStreamingTarget(_ contracts.DispatchInvocationRequestV1, _ contracts.ResolveAgentResponse) error { return stub.targetErr } -func (stub *streamingTransportStub) ValidateStreamingInput(_ contracts.DispatchInvocationRequestV4, _ contracts.ResolveAgentResponse) error { +func (stub *streamingTransportStub) ValidateStreamingInput(_ contracts.DispatchInvocationRequestV1, _ contracts.ResolveAgentResponse) error { return nil } @@ -125,7 +127,7 @@ type deadlineTransportStub struct { deadlineAt time.Time } -func (stub *deadlineTransportStub) SendNonStreaming(ctx context.Context, dispatch contracts.DispatchInvocationRequestV4, resolved contracts.ResolveAgentResponse) (json.RawMessage, error) { +func (stub *deadlineTransportStub) SendNonStreaming(ctx context.Context, dispatch contracts.DispatchInvocationRequestV1, resolved contracts.ResolveAgentResponse) (json.RawMessage, error) { stub.calls++ stub.dispatch = dispatch stub.resolved = resolved @@ -134,7 +136,7 @@ func (stub *deadlineTransportStub) SendNonStreaming(ctx context.Context, dispatc return nil, ctx.Err() } -func (stub *inputPreflightTransportStub) ValidateNonStreamingInput(contracts.DispatchInvocationRequestV4, contracts.ResolveAgentResponse) error { +func (stub *inputPreflightTransportStub) ValidateNonStreamingInput(contracts.DispatchInvocationRequestV1, contracts.ResolveAgentResponse) error { stub.preflightCalls++ return stub.preflightErr } @@ -150,18 +152,18 @@ func (err codedTransportError) PlatformErrorCode() contracts.PlatformErrorCode { return err.code } -func (stub *transportStub) SendNonStreaming(_ context.Context, dispatch contracts.DispatchInvocationRequestV4, resolved contracts.ResolveAgentResponse) (json.RawMessage, error) { +func (stub *transportStub) SendNonStreaming(_ context.Context, dispatch contracts.DispatchInvocationRequestV1, resolved contracts.ResolveAgentResponse) (json.RawMessage, error) { stub.calls++ stub.dispatch = dispatch stub.resolved = resolved return stub.result, stub.err } -func (stub *transportStub) ValidateNonStreamingTarget(contracts.DispatchInvocationRequestV4, contracts.ResolveAgentResponse) error { +func (stub *transportStub) ValidateNonStreamingTarget(contracts.DispatchInvocationRequestV1, contracts.ResolveAgentResponse) error { return stub.targetErr } -func (stub *transportStub) ValidateNonStreamingInput(contracts.DispatchInvocationRequestV4, contracts.ResolveAgentResponse) error { +func (stub *transportStub) ValidateNonStreamingInput(contracts.DispatchInvocationRequestV1, contracts.ResolveAgentResponse) error { return nil } @@ -414,7 +416,7 @@ func TestDispatchChildRejectsResolvedReleaseProvenanceMismatchBeforeLedger(t *te if err != nil { t.Fatal(err) } - dispatch := contracts.DispatchInvocationRequestV4{ + dispatch := contracts.DispatchInvocationRequestV1{ InvocationID: "inv-child", RootTaskID: "task-root", ParentInvocationID: "inv-parent", TraceID: "trace-child", Caller: contracts.Caller{Type: "agent", ID: "agent-parent"}, WorkspaceID: "workspace-a", TargetAgentID: "agent-a", AgentCardVersion: "1.0.0", AgentReleaseID: "release-request", @@ -525,7 +527,7 @@ func TestDispatchWithLedgerCancellationAfterAcceptanceCommitsTerminal(t *testing transport := &transportStub{result: json.RawMessage(`{"kind":"message"}`)} ledger := &cancelingLedgerRecorder{cancel: cancel} handler := newDispatchLedgerTestHandler(t, authStub{caller: auth.Caller{ID: "control-plane"}}, resolver, transport, ledger, 4096) - request := httptest.NewRequest(http.MethodPost, "/internal/v4/invocations", strings.NewReader(validDispatchBody(false))).WithContext(requestContext) + request := httptest.NewRequest(http.MethodPost, "/internal/v1/invocations", strings.NewReader(validDispatchBody(false))).WithContext(requestContext) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") response := httptest.NewRecorder() @@ -547,7 +549,7 @@ func TestDispatchWithLedgerTimeoutAfterAcceptanceCommitsTerminal(t *testing.T) { transport := &transportStub{result: json.RawMessage(`{"kind":"message"}`)} ledger := &cancelingLedgerRecorder{delay: 25 * time.Millisecond} handler := newDispatchLedgerTestHandler(t, authStub{caller: auth.Caller{ID: "control-plane"}}, resolver, transport, ledger, 4096) - request := httptest.NewRequest(http.MethodPost, "/internal/v4/invocations", strings.NewReader(validDispatchBody(false))).WithContext(requestContext) + request := httptest.NewRequest(http.MethodPost, "/internal/v1/invocations", strings.NewReader(validDispatchBody(false))).WithContext(requestContext) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") response := httptest.NewRecorder() @@ -878,7 +880,7 @@ func TestDispatchStreamingCancellationDuringTerminalCommitUsesBoundedLedgerConte } mux := http.NewServeMux() handler.RegisterRoutes(mux) - request := httptest.NewRequest(http.MethodPost, "/internal/v4/invocations", strings.NewReader(validDispatchBody(true))).WithContext(requestContext) + request := httptest.NewRequest(http.MethodPost, "/internal/v1/invocations", strings.NewReader(validDispatchBody(true))).WithContext(requestContext) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "text/event-stream") response := httptest.NewRecorder() @@ -901,7 +903,7 @@ func TestDispatchStreamingCancellationDuringChunkCommitRecordsCanceledTerminal(t } mux := http.NewServeMux() handler.RegisterRoutes(mux) - request := httptest.NewRequest(http.MethodPost, "/internal/v4/invocations", strings.NewReader(validDispatchBody(true))).WithContext(requestContext) + request := httptest.NewRequest(http.MethodPost, "/internal/v1/invocations", strings.NewReader(validDispatchBody(true))).WithContext(requestContext) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "text/event-stream") response := httptest.NewRecorder() @@ -962,7 +964,7 @@ func TestDispatchStreamingWriterFailureCommitsNonSuccessLedgerTerminal(t *testin } mux := http.NewServeMux() handler.RegisterRoutes(mux) - request := httptest.NewRequest(http.MethodPost, "/internal/v4/invocations", strings.NewReader(validDispatchBody(true))) + request := httptest.NewRequest(http.MethodPost, "/internal/v1/invocations", strings.NewReader(validDispatchBody(true))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "text/event-stream") writer := &failingStreamWriter{header: make(http.Header)} @@ -1142,7 +1144,7 @@ func assertLedgerLifecycle(t *testing.T, events []contracts.InvocationEventV03, } func invokeDispatch(handler http.Handler, contentType, accept, body string) *httptest.ResponseRecorder { - request := httptest.NewRequest(http.MethodPost, "/internal/v4/invocations", strings.NewReader(body)) + request := httptest.NewRequest(http.MethodPost, "/internal/v1/invocations", strings.NewReader(body)) request.Header.Set("Content-Type", contentType) request.Header.Set("Accept", accept) response := httptest.NewRecorder() diff --git a/apps/a2a-router/internal/api/ledger_handler.go b/apps/a2a-router/internal/api/ledger_handler.go index 5ad758c2..a804f574 100644 --- a/apps/a2a-router/internal/api/ledger_handler.go +++ b/apps/a2a-router/internal/api/ledger_handler.go @@ -12,8 +12,8 @@ import ( ) type LedgerReader interface { - GetInvocation(context.Context, string, string) (contracts.InvocationDetailResponseV4, error) - GetTrace(context.Context, string, contracts.TraceID) (contracts.TraceResponseV4, error) + GetInvocation(context.Context, string, string) (contracts.InvocationDetailResponseV1, error) + GetTrace(context.Context, string, contracts.TraceID) (contracts.TraceResponseV1, error) } type LedgerHandler struct { @@ -32,7 +32,7 @@ func NewLedgerHandler(reader LedgerReader) (*LedgerHandler, error) { return &LedgerHandler{reader: reader, validator: validator}, nil } -// RegisterRoutes exposes the Router Internal v3 metadata reads. The caller +// RegisterRoutes exposes the Router Internal v1 metadata reads. The caller // owns the process mux and supplies the same authenticated service principal // boundary used by dispatch; LedgerHandler remains responsible only for // validating and reading its owned metadata. @@ -43,10 +43,10 @@ func (handler *LedgerHandler) RegisterRoutes(mux *http.ServeMux, authenticator A if authenticator == nil { return errors.New("router read authenticator is required") } - mux.HandleFunc("GET /internal/v3/workspaces/{workspaceId}/invocations/{invocationId}", func(writer http.ResponseWriter, request *http.Request) { + mux.HandleFunc("GET /internal/v1/workspaces/{workspaceId}/invocations/{invocationId}", func(writer http.ResponseWriter, request *http.Request) { handler.serveInvocationRoute(writer, request, authenticator) }) - mux.HandleFunc("GET /internal/v3/workspaces/{workspaceId}/traces/{traceId}", func(writer http.ResponseWriter, request *http.Request) { + mux.HandleFunc("GET /internal/v1/workspaces/{workspaceId}/traces/{traceId}", func(writer http.ResponseWriter, request *http.Request) { handler.serveTraceRoute(writer, request, authenticator) }) return nil @@ -92,7 +92,7 @@ func (handler *LedgerHandler) serveTraceRoute(writer http.ResponseWriter, reques } // ServeInvocationRead adapts an already authenticated and path-validated -// Router Internal v3 request. Router authentication and mux ownership stay in +// Router Internal v1 request. Router authentication and mux ownership stay in // the process integration layer. func (handler *LedgerHandler) ServeInvocationRead( w http.ResponseWriter, @@ -104,7 +104,7 @@ func (handler *LedgerHandler) ServeInvocationRead( if err != nil { return handler.writeReadError(w, traceID, err) } - if err := handler.validator.ValidateInvocationDetailResponseV4(workspaceID, result); err != nil { + if err := handler.validator.ValidateInvocationDetailResponseV1(workspaceID, result); err != nil { return handler.writeReadError(w, traceID, ledger.ErrDependency) } return writeLedgerJSON(w, http.StatusOK, result) @@ -132,7 +132,7 @@ func (handler *LedgerHandler) serveTraceRead( if err != nil { return handler.writeReadError(w, requestTraceID, err) } - if err := contracts.ValidateTraceResponseV4(workspaceID, traceID, result); err != nil { + if err := contracts.ValidateTraceResponseV1(workspaceID, traceID, result); err != nil { return handler.writeReadError(w, requestTraceID, ledger.ErrDependency) } return writeLedgerJSON(w, http.StatusOK, result) diff --git a/apps/a2a-router/internal/api/ledger_handler_test.go b/apps/a2a-router/internal/api/ledger_handler_test.go index b7df0881..d2de4480 100644 --- a/apps/a2a-router/internal/api/ledger_handler_test.go +++ b/apps/a2a-router/internal/api/ledger_handler_test.go @@ -15,28 +15,28 @@ import ( ) type fakeLedgerReader struct { - detail contracts.InvocationDetailResponseV4 - trace contracts.TraceResponseV4 + detail contracts.InvocationDetailResponseV1 + trace contracts.TraceResponseV1 err error } -func (reader fakeLedgerReader) GetInvocation(context.Context, string, string) (contracts.InvocationDetailResponseV4, error) { +func (reader fakeLedgerReader) GetInvocation(context.Context, string, string) (contracts.InvocationDetailResponseV1, error) { return reader.detail, reader.err } -func (reader fakeLedgerReader) GetTrace(context.Context, string, contracts.TraceID) (contracts.TraceResponseV4, error) { +func (reader fakeLedgerReader) GetTrace(context.Context, string, contracts.TraceID) (contracts.TraceResponseV1, error) { return reader.trace, reader.err } func TestLedgerHandlerMapsContractReadsAndFailures(t *testing.T) { detail := handlerDetail(t) - handler, err := NewLedgerHandler(fakeLedgerReader{detail: detail, trace: contracts.TraceResponseV4{ - TraceID: detail.Invocation.TraceID, Invocations: []contracts.InvocationRecordV4{detail.Invocation}, + handler, err := NewLedgerHandler(fakeLedgerReader{detail: detail, trace: contracts.TraceResponseV1{ + TraceID: detail.Invocation.TraceID, Invocations: []contracts.InvocationRecordV1{detail.Invocation}, }}) if err != nil { t.Fatalf("construct Ledger handler: %v", err) } - request := httptest.NewRequest(http.MethodGet, "/internal/v3/read", nil) + request := httptest.NewRequest(http.MethodGet, "/internal/v1/read", nil) response := httptest.NewRecorder() if err := handler.ServeInvocationRead(response, request, "workspace-a", "inv-handler", "trace-request"); err != nil { t.Fatalf("serve Invocation read: %v", err) @@ -44,7 +44,7 @@ func TestLedgerHandlerMapsContractReadsAndFailures(t *testing.T) { if response.Code != http.StatusOK || response.Header().Get("Content-Type") != "application/json" { t.Fatalf("Invocation response status/header = %d/%q", response.Code, response.Header().Get("Content-Type")) } - var decoded contracts.InvocationDetailResponseV4 + var decoded contracts.InvocationDetailResponseV1 if err := json.Unmarshal(response.Body.Bytes(), &decoded); err != nil || decoded.Invocation.InvocationID != "inv-handler" { t.Fatalf("decode Invocation response = %#v, %v", decoded, err) } @@ -86,7 +86,7 @@ func TestLedgerHandlerRejectsInvalidStoredContractAsDependencyFailure(t *testing t.Fatalf("construct invalid-contract handler: %v", err) } response := httptest.NewRecorder() - request := httptest.NewRequest(http.MethodGet, "/internal/v3/read", nil) + request := httptest.NewRequest(http.MethodGet, "/internal/v1/read", nil) if err := handler.ServeInvocationRead(response, request, "workspace-a", "inv-handler", "trace-request"); err != nil { t.Fatalf("serve invalid stored contract: %v", err) } @@ -108,7 +108,7 @@ func TestLedgerHandlerRegistersAuthenticatedV3ReadRoutes(t *testing.T) { detail := handlerDetail(t) reader := fakeLedgerReader{ detail: detail, - trace: contracts.TraceResponseV4{TraceID: detail.Invocation.TraceID, Invocations: []contracts.InvocationRecordV4{detail.Invocation}}, + trace: contracts.TraceResponseV1{TraceID: detail.Invocation.TraceID, Invocations: []contracts.InvocationRecordV1{detail.Invocation}}, } handler, err := NewLedgerHandler(reader) if err != nil { @@ -118,20 +118,20 @@ func TestLedgerHandlerRegistersAuthenticatedV3ReadRoutes(t *testing.T) { if err := handler.RegisterRoutes(mux, authStub{caller: auth.Caller{ID: "control-plane"}}); err != nil { t.Fatalf("register Ledger routes: %v", err) } - invocationRequest := httptest.NewRequest(http.MethodGet, "/internal/v3/workspaces/workspace-a/invocations/inv-handler", nil) + invocationRequest := httptest.NewRequest(http.MethodGet, "/internal/v1/workspaces/workspace-a/invocations/inv-handler", nil) invocationResponse := httptest.NewRecorder() mux.ServeHTTP(invocationResponse, invocationRequest) if invocationResponse.Code != http.StatusOK || invocationResponse.Header().Get(TraceHeader) == "" { t.Fatalf("Invocation route status/trace = %d/%q", invocationResponse.Code, invocationResponse.Header().Get(TraceHeader)) } - traceRequest := httptest.NewRequest(http.MethodGet, "/internal/v3/workspaces/workspace-a/traces/trace-handler", nil) + traceRequest := httptest.NewRequest(http.MethodGet, "/internal/v1/workspaces/workspace-a/traces/trace-handler", nil) traceResponse := httptest.NewRecorder() mux.ServeHTTP(traceResponse, traceRequest) if traceResponse.Code != http.StatusOK || traceResponse.Header().Get(TraceHeader) == "" { t.Fatalf("Trace route status/trace = %d/%q", traceResponse.Code, traceResponse.Header().Get(TraceHeader)) } unauthenticated := httptest.NewRecorder() - unauthenticatedRequest := httptest.NewRequest(http.MethodGet, "/internal/v3/workspaces/workspace-a/traces/trace-handler", nil) + unauthenticatedRequest := httptest.NewRequest(http.MethodGet, "/internal/v1/workspaces/workspace-a/traces/trace-handler", nil) unauthenticatedAuthenticator := authStub{err: auth.ErrUnauthenticated} unauthenticatedMux := http.NewServeMux() if err := handler.RegisterRoutes(unauthenticatedMux, unauthenticatedAuthenticator); err != nil { @@ -146,12 +146,12 @@ func TestLedgerHandlerRegistersAuthenticatedV3ReadRoutes(t *testing.T) { if err := handler.RegisterRoutes(forbiddenMux, authStub{err: auth.ErrForbidden}); err != nil { t.Fatalf("register forbidden Ledger routes: %v", err) } - forbiddenMux.ServeHTTP(forbidden, httptest.NewRequest(http.MethodGet, "/internal/v3/workspaces/workspace-a/traces/trace-handler", nil)) + forbiddenMux.ServeHTTP(forbidden, httptest.NewRequest(http.MethodGet, "/internal/v1/workspaces/workspace-a/traces/trace-handler", nil)) if forbidden.Code != http.StatusForbidden { t.Fatalf("forbidden read status = %d", forbidden.Code) } invalid := httptest.NewRecorder() - mux.ServeHTTP(invalid, httptest.NewRequest(http.MethodGet, "/internal/v3/workspaces/bad%20workspace/traces/trace-handler", nil)) + mux.ServeHTTP(invalid, httptest.NewRequest(http.MethodGet, "/internal/v1/workspaces/bad%20workspace/traces/trace-handler", nil)) if invalid.Code != http.StatusNotFound { t.Fatalf("invalid read status = %d", invalid.Code) } @@ -167,7 +167,7 @@ func TestLedgerHandlerTraceRouteErrorsUseRequestTraceCorrelation(t *testing.T) { t.Fatalf("register Ledger routes: %v", err) } response := httptest.NewRecorder() - mux.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/internal/v3/workspaces/workspace-a/traces/trace-resource", nil)) + mux.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/internal/v1/workspaces/workspace-a/traces/trace-resource", nil)) if response.Code != http.StatusNotFound { t.Fatalf("status=%d body=%s", response.Code, response.Body.String()) } @@ -180,7 +180,7 @@ func TestLedgerHandlerTraceRouteErrorsUseRequestTraceCorrelation(t *testing.T) { } } -func handlerDetail(t *testing.T) contracts.InvocationDetailResponseV4 { +func handlerDetail(t *testing.T) contracts.InvocationDetailResponseV1 { t.Helper() at := time.Date(2026, 7, 16, 12, 0, 0, 0, time.UTC) event := contracts.InvocationEventV03{ @@ -190,8 +190,8 @@ func handlerDetail(t *testing.T) contracts.InvocationDetailResponseV4 { TraceID: "trace-handler", Caller: contracts.Caller{Type: "user", ID: "user-a"}, WorkspaceID: "workspace-a", TargetAgentID: "agent-a", AgentCardVersion: "1.0.0", Capability: "document.read", } - return contracts.InvocationDetailResponseV4{ - Invocation: contracts.InvocationRecordV4{ + return contracts.InvocationDetailResponseV1{ + Invocation: contracts.InvocationRecordV1{ InvocationID: event.InvocationID, RootTaskID: event.RootTaskID, TraceID: event.TraceID, Caller: event.Caller, WorkspaceID: event.WorkspaceID, TargetAgentID: event.TargetAgentID, AgentCardVersion: event.AgentCardVersion, Capability: event.Capability, Status: event.Status, diff --git a/apps/a2a-router/internal/config/config.go b/apps/a2a-router/internal/config/config.go index a003a5a9..8f1376c7 100644 --- a/apps/a2a-router/internal/config/config.go +++ b/apps/a2a-router/internal/config/config.go @@ -140,7 +140,7 @@ func LoadFrom(lookup func(string) (string, bool)) (Config, error) { if err != nil { return Config{}, err } - if err := validateControlPlaneURL(versionURL, "/internal/v3/resolve-installed-version"); err != nil { + if err := validateControlPlaneURL(versionURL, "/internal/v1/resolve-installed-version"); err != nil { return Config{}, fmt.Errorf("NEKIRO_CONTROL_PLANE_VERSION_URL is invalid: %w", err) } token, err := required("NEKIRO_CONTROL_PLANE_SERVICE_TOKEN") @@ -573,7 +573,7 @@ func validateListenAddress(value string) error { } func validateResolveURL(value string) error { - return validateControlPlaneURL(value, "/internal/v2/resolve-agent") + return validateControlPlaneURL(value, "/internal/v1/resolve-agent") } func validateControlPlaneURL(value, requiredPath string) error { diff --git a/apps/a2a-router/internal/config/config_test.go b/apps/a2a-router/internal/config/config_test.go index cfcb0bb5..95eca8c7 100644 --- a/apps/a2a-router/internal/config/config_test.go +++ b/apps/a2a-router/internal/config/config_test.go @@ -16,7 +16,7 @@ func TestLoadRequiresStrictRouterConfig(t *testing.T) { if err != nil { t.Fatalf("valid config rejected: %v", err) } - if config.ListenAddress != "127.0.0.1:9090" || config.DatabaseURL != "postgresql://router:secret@postgres:5432/nekiro?sslmode=disable" || config.ControlPlaneResolveURL != "https://control.internal/internal/v2/resolve-agent" || config.ControlPlaneVersionURL != "https://control.internal/internal/v3/resolve-installed-version" || len(config.AgentPrincipals) != 1 || config.AgentPrincipals[0].WorkspaceID != "workspace-a" || config.InternalRequestLimitBytes != 1024 || config.AgentRequestLimitBytes != 1024 || config.ControlPlaneResponseLimitBytes != 2048 || config.AgentResponseLimitBytes != 4096 || config.A2AEventLimitBytes != 4096 || config.SSEEventLimitBytes != 4096 || config.ResolutionDeadline.Milliseconds() != 5000 || config.AgentDeadline.Milliseconds() != 5000 { + if config.ListenAddress != "127.0.0.1:9090" || config.DatabaseURL != "postgresql://router:secret@postgres:5432/nekiro?sslmode=disable" || config.ControlPlaneResolveURL != "https://control.internal/internal/v1/resolve-agent" || config.ControlPlaneVersionURL != "https://control.internal/internal/v1/resolve-installed-version" || len(config.AgentPrincipals) != 1 || config.AgentPrincipals[0].WorkspaceID != "workspace-a" || config.InternalRequestLimitBytes != 1024 || config.AgentRequestLimitBytes != 1024 || config.ControlPlaneResponseLimitBytes != 2048 || config.AgentResponseLimitBytes != 4096 || config.A2AEventLimitBytes != 4096 || config.SSEEventLimitBytes != 4096 || config.ResolutionDeadline.Milliseconds() != 5000 || config.AgentDeadline.Milliseconds() != 5000 { t.Fatalf("config=%#v", config) } }) @@ -37,14 +37,14 @@ func TestLoadRequiresStrictRouterConfig(t *testing.T) { {name: "duplicate Agent principal field", key: "NEKIRO_ROUTER_AGENT_PRINCIPALS_JSON", value: ptr(`[{"workspaceId":"workspace-a","workspaceId":"workspace-b","agentId":"runtime-a","tokenSha256":"` + digest("agent-token") + `"}]`)}, {name: "invalid Agent principal digest", key: "NEKIRO_ROUTER_AGENT_PRINCIPALS_JSON", value: ptr(`[{"workspaceId":"workspace-a","agentId":"runtime-a","tokenSha256":"bad"}]`)}, {name: "whitespace token", key: "NEKIRO_CONTROL_PLANE_SERVICE_TOKEN", value: ptr(" token")}, - {name: "control plane userinfo", key: "NEKIRO_CONTROL_PLANE_RESOLVE_URL", value: ptr("https://user@control.internal/internal/v2/resolve-agent")}, - {name: "control plane wrong path", key: "NEKIRO_CONTROL_PLANE_RESOLVE_URL", value: ptr("https://control.internal/internal/v2/other")}, - {name: "control plane query", key: "NEKIRO_CONTROL_PLANE_RESOLVE_URL", value: ptr("https://control.internal/internal/v2/resolve-agent?x=1")}, - {name: "control plane empty query", key: "NEKIRO_CONTROL_PLANE_RESOLVE_URL", value: ptr("https://control.internal/internal/v2/resolve-agent?")}, - {name: "control plane empty fragment", key: "NEKIRO_CONTROL_PLANE_RESOLVE_URL", value: ptr("https://control.internal/internal/v2/resolve-agent#")}, - {name: "control plane port out of range", key: "NEKIRO_CONTROL_PLANE_RESOLVE_URL", value: ptr("https://control.internal:99999/internal/v2/resolve-agent")}, - {name: "version URL wrong path", key: "NEKIRO_CONTROL_PLANE_VERSION_URL", value: ptr("https://control.internal/internal/v2/resolve-agent")}, - {name: "version URL port out of range", key: "NEKIRO_CONTROL_PLANE_VERSION_URL", value: ptr("https://control.internal:99999/internal/v3/resolve-installed-version")}, + {name: "control plane userinfo", key: "NEKIRO_CONTROL_PLANE_RESOLVE_URL", value: ptr("https://user@control.internal/internal/v1/resolve-agent")}, + {name: "control plane wrong path", key: "NEKIRO_CONTROL_PLANE_RESOLVE_URL", value: ptr("https://control.internal/internal/v1/other")}, + {name: "control plane query", key: "NEKIRO_CONTROL_PLANE_RESOLVE_URL", value: ptr("https://control.internal/internal/v1/resolve-agent?x=1")}, + {name: "control plane empty query", key: "NEKIRO_CONTROL_PLANE_RESOLVE_URL", value: ptr("https://control.internal/internal/v1/resolve-agent?")}, + {name: "control plane empty fragment", key: "NEKIRO_CONTROL_PLANE_RESOLVE_URL", value: ptr("https://control.internal/internal/v1/resolve-agent#")}, + {name: "control plane port out of range", key: "NEKIRO_CONTROL_PLANE_RESOLVE_URL", value: ptr("https://control.internal:99999/internal/v1/resolve-agent")}, + {name: "version URL wrong path", key: "NEKIRO_CONTROL_PLANE_VERSION_URL", value: ptr("https://control.internal/internal/v1/resolve-agent")}, + {name: "version URL port out of range", key: "NEKIRO_CONTROL_PLANE_VERSION_URL", value: ptr("https://control.internal:99999/internal/v1/resolve-installed-version")}, {name: "negative limit", key: "NEKIRO_ROUTER_INTERNAL_REQUEST_LIMIT_BYTES", value: ptr("-1")}, {name: "zero limit", key: "NEKIRO_ROUTER_INTERNAL_REQUEST_LIMIT_BYTES", value: ptr("0")}, {name: "fractional limit", key: "NEKIRO_ROUTER_INTERNAL_REQUEST_LIMIT_BYTES", value: ptr("1.5")}, @@ -97,8 +97,8 @@ func validEnv() map[string]string { "NEKIRO_ROUTER_SERVICE_PRINCIPALS_JSON": fmt.Sprintf(`[{"id":"router","tokenSha256":"%s"}]`, digest("router-token")), "NEKIRO_ROUTER_AGENT_PRINCIPALS_JSON": fmt.Sprintf(`[{"workspaceId":"workspace-a","agentId":"runtime-a","tokenSha256":"%s"}]`, digest("agent-token")), "NEKIRO_DATABASE_URL": "postgresql://router:secret@postgres:5432/nekiro?sslmode=disable", - "NEKIRO_CONTROL_PLANE_RESOLVE_URL": "https://control.internal/internal/v2/resolve-agent", - "NEKIRO_CONTROL_PLANE_VERSION_URL": "https://control.internal/internal/v3/resolve-installed-version", + "NEKIRO_CONTROL_PLANE_RESOLVE_URL": "https://control.internal/internal/v1/resolve-agent", + "NEKIRO_CONTROL_PLANE_VERSION_URL": "https://control.internal/internal/v1/resolve-installed-version", "NEKIRO_CONTROL_PLANE_SERVICE_TOKEN": "control-token", "NEKIRO_ROUTER_INTERNAL_REQUEST_LIMIT_BYTES": "1024", "NEKIRO_ROUTER_AGENT_REQUEST_LIMIT_BYTES": "1024", diff --git a/apps/a2a-router/internal/ledger/store.go b/apps/a2a-router/internal/ledger/store.go index ee256215..3e03b95e 100644 --- a/apps/a2a-router/internal/ledger/store.go +++ b/apps/a2a-router/internal/ledger/store.go @@ -75,7 +75,7 @@ func (store *Store) Append(ctx context.Context, event contracts.InvocationEventV if err != nil { return dependencyError("read Invocation history", err) } - if err := store.validator.ValidateInvocationDetailResponseV4(projection.WorkspaceID, contracts.InvocationDetailResponseV4{ + if err := store.validator.ValidateInvocationDetailResponseV1(projection.WorkspaceID, contracts.InvocationDetailResponseV1{ Invocation: projection, Events: history, }); err != nil { @@ -116,7 +116,7 @@ func (store *Store) Append(ctx context.Context, event contracts.InvocationEventV return nil } -func (store *Store) GetInvocation(ctx context.Context, workspaceID, invocationID string) (result contracts.InvocationDetailResponseV4, returnErr error) { +func (store *Store) GetInvocation(ctx context.Context, workspaceID, invocationID string) (result contracts.InvocationDetailResponseV1, returnErr error) { tx, err := store.pool.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.RepeatableRead, AccessMode: pgx.ReadOnly}) if err != nil { return result, dependencyError("begin Invocation read", err) @@ -134,7 +134,7 @@ WHERE workspace_id = $1 AND invocation_id = $2`, workspaceID, invocationID)) if err != nil { return result, dependencyError("read Invocation events", err) } - if err := store.validator.ValidateInvocationDetailResponseV4(workspaceID, result); err != nil { + if err := store.validator.ValidateInvocationDetailResponseV1(workspaceID, result); err != nil { return result, dependencyError("validate stored Invocation detail", err) } if err := tx.Commit(ctx); err != nil { @@ -147,7 +147,7 @@ WHERE workspace_id = $1 AND invocation_id = $2`, workspaceID, invocationID)) // trusted nested adapter. The adapter verifies the authenticated credential's // Workspace and Agent against the returned parent before deriving a child; // the inherited Workspace is then checked again by Control Plane resolution. -func (store *Store) GetInvocationByParentID(ctx context.Context, invocationID string) (result contracts.InvocationDetailResponseV4, returnErr error) { +func (store *Store) GetInvocationByParentID(ctx context.Context, invocationID string) (result contracts.InvocationDetailResponseV1, returnErr error) { tx, err := store.pool.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.RepeatableRead, AccessMode: pgx.ReadOnly}) if err != nil { return result, dependencyError("begin parent Invocation read", err) @@ -165,7 +165,7 @@ WHERE invocation_id = $1`, invocationID)) if err != nil { return result, dependencyError("read parent Invocation events", err) } - if err := store.validator.ValidateInvocationDetailResponseV4(result.Invocation.WorkspaceID, result); err != nil { + if err := store.validator.ValidateInvocationDetailResponseV1(result.Invocation.WorkspaceID, result); err != nil { return result, dependencyError("validate stored parent Invocation detail", err) } if err := tx.Commit(ctx); err != nil { @@ -174,42 +174,42 @@ WHERE invocation_id = $1`, invocationID)) return result, nil } -func (store *Store) GetTrace(ctx context.Context, workspaceID string, traceID contracts.TraceID) (contracts.TraceResponseV4, error) { +func (store *Store) GetTrace(ctx context.Context, workspaceID string, traceID contracts.TraceID) (contracts.TraceResponseV1, error) { rows, err := store.pool.Query(ctx, projectionSelect+` WHERE workspace_id = $1 AND trace_id = $2 ORDER BY created_at ASC, invocation_id ASC`, workspaceID, traceID) if err != nil { - return contracts.TraceResponseV4{}, dependencyError("read Trace projections", err) + return contracts.TraceResponseV1{}, dependencyError("read Trace projections", err) } defer rows.Close() - result := contracts.TraceResponseV4{TraceID: traceID} + result := contracts.TraceResponseV1{TraceID: traceID} for rows.Next() { projection, err := scanProjection(rows) if err != nil { - return contracts.TraceResponseV4{}, dependencyError("scan Trace projection", err) + return contracts.TraceResponseV1{}, dependencyError("scan Trace projection", err) } result.Invocations = append(result.Invocations, projection) } if err := rows.Err(); err != nil { - return contracts.TraceResponseV4{}, dependencyError("iterate Trace projections", err) + return contracts.TraceResponseV1{}, dependencyError("iterate Trace projections", err) } if len(result.Invocations) == 0 { - return contracts.TraceResponseV4{}, ErrNotFound + return contracts.TraceResponseV1{}, ErrNotFound } result.Invocations, err = orderTraceProjections(result.Invocations) if err != nil { - return contracts.TraceResponseV4{}, dependencyError("order stored Trace", err) + return contracts.TraceResponseV1{}, dependencyError("order stored Trace", err) } - if err := contracts.ValidateTraceResponseV4(workspaceID, traceID, result); err != nil { - return contracts.TraceResponseV4{}, dependencyError("validate stored Trace", err) + if err := contracts.ValidateTraceResponseV1(workspaceID, traceID, result); err != nil { + return contracts.TraceResponseV1{}, dependencyError("validate stored Trace", err) } return result, nil } -func orderTraceProjections(values []contracts.InvocationRecordV4) ([]contracts.InvocationRecordV4, error) { - ordered := make([]contracts.InvocationRecordV4, 0, len(values)) +func orderTraceProjections(values []contracts.InvocationRecordV1) ([]contracts.InvocationRecordV1, error) { + ordered := make([]contracts.InvocationRecordV1, 0, len(values)) emitted := make(map[string]struct{}, len(values)) - remaining := append([]contracts.InvocationRecordV4(nil), values...) + remaining := append([]contracts.InvocationRecordV1(nil), values...) for len(remaining) > 0 { next := remaining[:0] progress := false @@ -269,7 +269,7 @@ SELECT invocation_id, root_task_id, parent_invocation_id, trace_id, created_at, updated_at FROM ledger.invocations ` -func lockProjection(ctx context.Context, tx pgx.Tx, invocationID string) (contracts.InvocationRecordV4, error) { +func lockProjection(ctx context.Context, tx pgx.Tx, invocationID string) (contracts.InvocationRecordV1, error) { return scanProjection(tx.QueryRow(ctx, projectionSelect+`WHERE invocation_id = $1 FOR UPDATE`, invocationID)) } @@ -357,8 +357,8 @@ WHERE invocation_id = $1 ORDER BY sequence ASC`, invocationID) type scanner interface{ Scan(...any) error } -func scanProjection(row scanner) (contracts.InvocationRecordV4, error) { - var value contracts.InvocationRecordV4 +func scanProjection(row scanner) (contracts.InvocationRecordV1, error) { + var value contracts.InvocationRecordV1 var parent, errorCode sql.NullString var releaseID sql.NullString var cardDigest []byte diff --git a/apps/a2a-router/internal/ledger/store_unit_test.go b/apps/a2a-router/internal/ledger/store_unit_test.go index c22b67bc..2c3160b6 100644 --- a/apps/a2a-router/internal/ledger/store_unit_test.go +++ b/apps/a2a-router/internal/ledger/store_unit_test.go @@ -18,7 +18,7 @@ func TestNewStoreRequiresPool(t *testing.T) { } func TestOrderTraceProjectionsBuildsParentBeforeChild(t *testing.T) { - values := []contracts.InvocationRecordV4{ + values := []contracts.InvocationRecordV1{ {InvocationID: "child", ParentInvocationID: "parent"}, {InvocationID: "parent"}, {InvocationID: "root"}, @@ -33,7 +33,7 @@ func TestOrderTraceProjectionsBuildsParentBeforeChild(t *testing.T) { } func TestOrderTraceProjectionsRejectsMissingOrCyclicLineage(t *testing.T) { - for _, values := range [][]contracts.InvocationRecordV4{ + for _, values := range [][]contracts.InvocationRecordV1{ {{InvocationID: "child", ParentInvocationID: "missing"}}, {{InvocationID: "a", ParentInvocationID: "b"}, {InvocationID: "b", ParentInvocationID: "a"}}, } { diff --git a/apps/a2a-router/internal/nested/context.go b/apps/a2a-router/internal/nested/context.go index 90717724..5877466e 100644 --- a/apps/a2a-router/internal/nested/context.go +++ b/apps/a2a-router/internal/nested/context.go @@ -27,7 +27,7 @@ var ( // the nested request cannot provide them. type ChildContext struct { ChildInvocationID string - ParentInvocation contracts.InvocationRecordV4 + ParentInvocation contracts.InvocationRecordV1 Caller contracts.Caller WorkspaceID string RootTaskID string @@ -39,7 +39,7 @@ type ChildContext struct { // authenticated Agent and Workspace to match the parent. The child receives a // new Invocation ID; Workspace, root Task, Trace, and caller are inherited from // the parent. -func DeriveChildContext(parent contracts.InvocationDetailResponseV4, authenticated AuthenticatedAgent) (ChildContext, error) { +func DeriveChildContext(parent contracts.InvocationDetailResponseV1, authenticated AuthenticatedAgent) (ChildContext, error) { if parent.Invocation.InvocationID == "" { return ChildContext{}, ErrParentNotFound } @@ -71,11 +71,11 @@ func DeriveChildContext(parent contracts.InvocationDetailResponseV4, authenticat }, nil } -// BuildChildDispatchRequest constructs the trusted DispatchInvocationRequestV4 +// BuildChildDispatchRequest constructs the trusted DispatchInvocationRequestV1 // for the child Invocation from the derived context and the untrusted nested // request fields. The parent Invocation ID is propagated for Ledger lineage. -func BuildChildDispatchRequest(child ChildContext, targetAgentID, capability string, input []byte, stream bool, agentCardVersion, agentReleaseID, agentCardDigest string) contracts.DispatchInvocationRequestV4 { - return contracts.DispatchInvocationRequestV4{ +func BuildChildDispatchRequest(child ChildContext, targetAgentID, capability string, input []byte, stream bool, agentCardVersion, agentReleaseID, agentCardDigest string) contracts.DispatchInvocationRequestV1 { + return contracts.DispatchInvocationRequestV1{ InvocationID: child.ChildInvocationID, RootTaskID: child.RootTaskID, ParentInvocationID: child.ParentInvocation.InvocationID, diff --git a/apps/a2a-router/internal/nested/context_test.go b/apps/a2a-router/internal/nested/context_test.go index f97b8e86..c6273664 100644 --- a/apps/a2a-router/internal/nested/context_test.go +++ b/apps/a2a-router/internal/nested/context_test.go @@ -8,9 +8,9 @@ import ( "github.com/NeKiro-project/NeKiro/contracts" ) -func runningParent() contracts.InvocationDetailResponseV4 { - return contracts.InvocationDetailResponseV4{ - Invocation: contracts.InvocationRecordV4{ +func runningParent() contracts.InvocationDetailResponseV1 { + return contracts.InvocationDetailResponseV1{ + Invocation: contracts.InvocationRecordV1{ InvocationID: "inv_parent123", RootTaskID: "task_root456", TraceID: "trc_abc123_1", @@ -61,7 +61,7 @@ func TestDeriveChildContextSuccess(t *testing.T) { } func TestDeriveChildContextParentNotFound(t *testing.T) { - parent := contracts.InvocationDetailResponseV4{} + parent := contracts.InvocationDetailResponseV1{} _, err := DeriveChildContext(parent, runningParentPrincipal()) if err != ErrParentNotFound { t.Errorf("expected ErrParentNotFound, got %v", err) diff --git a/apps/a2a-router/internal/resolution/client.go b/apps/a2a-router/internal/resolution/client.go index a27c2cb4..bfc5f1fd 100644 --- a/apps/a2a-router/internal/resolution/client.go +++ b/apps/a2a-router/internal/resolution/client.go @@ -41,7 +41,7 @@ func NewClient(doer HTTPDoer, url, token string, responseLimit int64) (*Client, } // NewClientWithVersionURL creates a resolution client with an optional -// Control Plane Internal v3 version resolution endpoint. +// Control Plane Internal v1 version resolution endpoint. func NewClientWithVersionURL(doer HTTPDoer, url, versionURL, token string, responseLimit int64) (*Client, error) { if doer == nil || url == "" || token == "" || responseLimit < contracts.RuntimeByteLimitMinimum || responseLimit > contracts.RuntimeByteLimitMaximum { return nil, errors.New("resolution client dependencies are required") @@ -147,7 +147,7 @@ func readBounded(reader io.Reader, limit int64) ([]byte, error) { return data, nil } -// ResolveInstalledVersion calls the Control Plane Internal v3 endpoint to +// ResolveInstalledVersion calls the Control Plane Internal v1 endpoint to // resolve the deterministic installed Agent Card version from the enabled // Installation. It returns the exact pinned version. func (client *Client) ResolveInstalledVersion(ctx context.Context, requestValue contracts.ResolveInstalledVersionRequest) (contracts.ResolveInstalledVersionResponse, error) { diff --git a/apps/a2a-router/internal/resolution/client_test.go b/apps/a2a-router/internal/resolution/client_test.go index aa44785d..479aeff5 100644 --- a/apps/a2a-router/internal/resolution/client_test.go +++ b/apps/a2a-router/internal/resolution/client_test.go @@ -34,7 +34,7 @@ func TestClientResolveSendsExactInternalV2Request(t *testing.T) { requestValue := validResolveRequest() var received contracts.ResolveAgentRequest server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { - if request.URL.Path != "/internal/v2/resolve-agent" || request.Method != http.MethodPost || request.Header.Get("Authorization") != "Bearer control-token" || request.Header.Get("Content-Type") != "application/json" || request.Header.Get("Accept") != "application/json" { + if request.URL.Path != "/internal/v1/resolve-agent" || request.Method != http.MethodPost || request.Header.Get("Authorization") != "Bearer control-token" || request.Header.Get("Content-Type") != "application/json" || request.Header.Get("Accept") != "application/json" { t.Errorf("unexpected request: %s %s %#v", request.Method, request.URL.Path, request.Header) } if err := json.NewDecoder(request.Body).Decode(&received); err != nil { @@ -44,7 +44,7 @@ func TestClientResolveSendsExactInternalV2Request(t *testing.T) { _, _ = io.WriteString(writer, validResolveResponse) })) defer server.Close() - client, err := NewClient(server.Client(), server.URL+"/internal/v2/resolve-agent", "control-token", 4096) + client, err := NewClient(server.Client(), server.URL+"/internal/v1/resolve-agent", "control-token", 4096) if err != nil { t.Fatal(err) } diff --git a/apps/a2a-router/internal/transport/a2a/client_test.go b/apps/a2a-router/internal/transport/a2a/client_test.go index 14dba3af..49814cbc 100644 --- a/apps/a2a-router/internal/transport/a2a/client_test.go +++ b/apps/a2a-router/internal/transport/a2a/client_test.go @@ -78,7 +78,7 @@ func TestClientDoesNotFollowAgentRedirects(t *testing.T) { if err != nil { t.Fatalf("NewClient = %v", err) } - _, err = client.SendNonStreaming(t.Context(), contracts.DispatchInvocationRequestV4{ + _, err = client.SendNonStreaming(t.Context(), contracts.DispatchInvocationRequestV1{ InvocationID: "inv-a", RootTaskID: "task-a", TraceID: "trace-a", Caller: contracts.Caller{Type: "user", ID: "owner-a"}, WorkspaceID: "workspace-a", TargetAgentID: "agent-a", AgentCardVersion: "1.0.0", Capability: "capability-a", @@ -115,7 +115,7 @@ func TestClientSendNonStreamingMapsDispatchToA2A(t *testing.T) { if err != nil { t.Fatalf("NewClient = %v", err) } - result, err := client.SendNonStreaming(t.Context(), contracts.DispatchInvocationRequestV4{ + result, err := client.SendNonStreaming(t.Context(), contracts.DispatchInvocationRequestV1{ InvocationID: "inv-a", RootTaskID: "task-a", TraceID: "trace-a", Caller: contracts.Caller{Type: "user", ID: "owner-a"}, WorkspaceID: "workspace-a", TargetAgentID: "agent-a", AgentCardVersion: "1.0.0", Capability: "capability-a", @@ -152,7 +152,7 @@ func TestClientPinsSelectedTargetOncePerNonStreamingInvocation(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := client.SendNonStreaming(t.Context(), contracts.DispatchInvocationRequestV4{ + if _, err := client.SendNonStreaming(t.Context(), contracts.DispatchInvocationRequestV1{ InvocationID: "inv-pin", RootTaskID: "task-pin", TraceID: "trace-pin", Caller: contracts.Caller{Type: "user", ID: "owner-a"}, WorkspaceID: "workspace-a", TargetAgentID: "agent-a", AgentCardVersion: "1.0.0", Capability: "capability-a", @@ -269,7 +269,7 @@ func TestClientSendStreamingMapsA2AEventsAndTrustedHeaders(t *testing.T) { if err != nil { t.Fatal(err) } - dispatch := contracts.DispatchInvocationRequestV4{ + dispatch := contracts.DispatchInvocationRequestV1{ InvocationID: "inv-a", RootTaskID: "task-a", TraceID: "trace-a", Caller: contracts.Caller{Type: "user", ID: "owner-a"}, WorkspaceID: "workspace-a", TargetAgentID: "agent-a", AgentCardVersion: "1.0.0", Capability: "capability-a", @@ -310,7 +310,7 @@ func TestClientPinsSelectedTargetOnceForCompleteStream(t *testing.T) { if err != nil { t.Fatal(err) } - dispatch := contracts.DispatchInvocationRequestV4{ + dispatch := contracts.DispatchInvocationRequestV1{ InvocationID: "inv-stream-pin", RootTaskID: "task-stream-pin", TraceID: "trace-stream-pin", Caller: contracts.Caller{Type: "user", ID: "owner-a"}, WorkspaceID: "workspace-a", TargetAgentID: "agent-a", AgentCardVersion: "1.0.0", Capability: "capability-a", @@ -346,7 +346,7 @@ func TestClientStreamingRejectsInvalidJSONRPCEnvelopeBeforeEventMapping(t *testi if err != nil { t.Fatal(err) } - dispatch := contracts.DispatchInvocationRequestV4{ + dispatch := contracts.DispatchInvocationRequestV1{ InvocationID: "inv-a", RootTaskID: "task-a", TraceID: "trace-a", Caller: contracts.Caller{Type: "user", ID: "owner-a"}, WorkspaceID: "workspace-a", TargetAgentID: "agent-a", AgentCardVersion: "1.0.0", Capability: "capability-a", @@ -452,7 +452,7 @@ func TestClientRejectsMalformedMessageResultInNonStreamingDispatch(t *testing.T) if err != nil { t.Fatal(err) } - _, err = client.SendNonStreaming(t.Context(), contracts.DispatchInvocationRequestV4{ + _, err = client.SendNonStreaming(t.Context(), contracts.DispatchInvocationRequestV1{ InvocationID: "inv-a", RootTaskID: "task-a", TraceID: "trace-a", Caller: contracts.Caller{Type: "user", ID: "owner-a"}, WorkspaceID: "workspace-a", TargetAgentID: "agent-a", AgentCardVersion: "1.0.0", Capability: "capability-a", diff --git a/apps/a2a-router/internal/transport/a2a/nonstreaming.go b/apps/a2a-router/internal/transport/a2a/nonstreaming.go index f10da4c9..b3766ad7 100644 --- a/apps/a2a-router/internal/transport/a2a/nonstreaming.go +++ b/apps/a2a-router/internal/transport/a2a/nonstreaming.go @@ -9,7 +9,7 @@ import ( a2ago "github.com/a2aproject/a2a-go/a2a" ) -func (client *Client) SendNonStreaming(ctx context.Context, dispatch contracts.DispatchInvocationRequestV4, resolved contracts.ResolveAgentResponse) (json.RawMessage, error) { +func (client *Client) SendNonStreaming(ctx context.Context, dispatch contracts.DispatchInvocationRequestV1, resolved contracts.ResolveAgentResponse) (json.RawMessage, error) { target, err := NewTarget(resolved, dispatch.Capability) if err != nil { return nil, err @@ -63,12 +63,12 @@ func (client *Client) SendNonStreaming(ctx context.Context, dispatch contracts.D return json.RawMessage(encoded), nil } -func (client *Client) ValidateNonStreamingTarget(dispatch contracts.DispatchInvocationRequestV4, resolved contracts.ResolveAgentResponse) error { +func (client *Client) ValidateNonStreamingTarget(dispatch contracts.DispatchInvocationRequestV1, resolved contracts.ResolveAgentResponse) error { _, err := NewTarget(resolved, dispatch.Capability) return err } -func (client *Client) ValidateNonStreamingInput(dispatch contracts.DispatchInvocationRequestV4, resolved contracts.ResolveAgentResponse) error { +func (client *Client) ValidateNonStreamingInput(dispatch contracts.DispatchInvocationRequestV1, resolved contracts.ResolveAgentResponse) error { maxInputBytes, err := parseCardLimit(resolved.Card.Limits.MaxInputBytes.String()) if err != nil { return classify(contracts.ErrorCodeA2AProtocol, err) @@ -83,7 +83,7 @@ func (client *Client) ValidateNonStreamingInput(dispatch contracts.DispatchInvoc return nil } -func (client *Client) ValidateStreamingTarget(dispatch contracts.DispatchInvocationRequestV4, resolved contracts.ResolveAgentResponse) error { +func (client *Client) ValidateStreamingTarget(dispatch contracts.DispatchInvocationRequestV1, resolved contracts.ResolveAgentResponse) error { target, err := NewTarget(resolved, dispatch.Capability) if err != nil { return err @@ -97,11 +97,11 @@ func (client *Client) ValidateStreamingTarget(dispatch contracts.DispatchInvocat return nil } -func (client *Client) ValidateStreamingInput(dispatch contracts.DispatchInvocationRequestV4, resolved contracts.ResolveAgentResponse) error { +func (client *Client) ValidateStreamingInput(dispatch contracts.DispatchInvocationRequestV1, resolved contracts.ResolveAgentResponse) error { return client.ValidateNonStreamingInput(dispatch, resolved) } -func messageSendParams(dispatch contracts.DispatchInvocationRequestV4) (*a2ago.MessageSendParams, error) { +func messageSendParams(dispatch contracts.DispatchInvocationRequestV1) (*a2ago.MessageSendParams, error) { var input map[string]json.RawMessage if err := json.Unmarshal(dispatch.Input, &input); err != nil { return nil, err diff --git a/apps/a2a-router/internal/transport/a2a/streaming.go b/apps/a2a-router/internal/transport/a2a/streaming.go index 1e802509..facccc04 100644 --- a/apps/a2a-router/internal/transport/a2a/streaming.go +++ b/apps/a2a-router/internal/transport/a2a/streaming.go @@ -16,7 +16,7 @@ import ( const streamCancelAttemptTimeout = time.Second -func (client *Client) SendStreaming(ctx context.Context, dispatch contracts.DispatchInvocationRequestV4, resolved contracts.ResolveAgentResponse) iter.Seq2[streammodel.Event, error] { +func (client *Client) SendStreaming(ctx context.Context, dispatch contracts.DispatchInvocationRequestV1, resolved contracts.ResolveAgentResponse) iter.Seq2[streammodel.Event, error] { return func(yield func(streammodel.Event, error) bool) { target, err := NewTarget(resolved, dispatch.Capability) if err != nil { diff --git a/apps/a2a-router/internal/transport/a2a/streaming_test.go b/apps/a2a-router/internal/transport/a2a/streaming_test.go index a6926755..34745af4 100644 --- a/apps/a2a-router/internal/transport/a2a/streaming_test.go +++ b/apps/a2a-router/internal/transport/a2a/streaming_test.go @@ -130,7 +130,7 @@ func TestClientStreamingMakesOneCancelAttemptAfterDeadline(t *testing.T) { } ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() - dispatch := contracts.DispatchInvocationRequestV4{ + dispatch := contracts.DispatchInvocationRequestV1{ InvocationID: "inv-a", RootTaskID: "task-a", TraceID: "trace-a", Caller: contracts.Caller{Type: "user", ID: "owner-a"}, WorkspaceID: "workspace-a", TargetAgentID: "agent-a", AgentCardVersion: "1.0.0", Capability: "capability-a", diff --git a/apps/control-plane/internal/config/config.go b/apps/control-plane/internal/config/config.go index 8e22b3c2..51d24bc6 100644 --- a/apps/control-plane/internal/config/config.go +++ b/apps/control-plane/internal/config/config.go @@ -215,7 +215,7 @@ func LoadInvocationRuntimeFrom(lookup func(string) (string, bool)) (InvocationRu return InvocationRuntimeConfig{}, err } parsed, err := url.Parse(routerURL) - if err != nil || parsed.Scheme != "http" && parsed.Scheme != "https" || parsed.Host == "" || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" || parsed.Path != "/internal/v4/invocations" { + if err != nil || parsed.Scheme != "http" && parsed.Scheme != "https" || parsed.Host == "" || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" || parsed.Path != "/internal/v1/invocations" { return InvocationRuntimeConfig{}, errors.New("NEKIRO_ROUTER_INTERNAL_URL is invalid") } token, err := required("NEKIRO_ROUTER_INTERNAL_BEARER_TOKEN") diff --git a/apps/control-plane/internal/config/config_test.go b/apps/control-plane/internal/config/config_test.go index 1988639e..f2f5b461 100644 --- a/apps/control-plane/internal/config/config_test.go +++ b/apps/control-plane/internal/config/config_test.go @@ -230,18 +230,18 @@ func TestLoadInvocationRuntimeRequiresExactNoDefaultConfiguration(t *testing.T) if err != nil { t.Fatal(err) } - if loaded.RouterInternalURL != "http://router.test:8081/internal/v4/invocations" || loaded.RouterBearerToken != "router-secret" || loaded.InternalRequestLimitBytes != 1048576 || loaded.PublicRequestLimitBytes != 1048576 || loaded.SSEEventLimitBytes != 65536 || loaded.MetadataResponseLimitBytes != 1048576 || loaded.DeadlineMS != 30000 { + if loaded.RouterInternalURL != "http://router.test:8081/internal/v1/invocations" || loaded.RouterBearerToken != "router-secret" || loaded.InternalRequestLimitBytes != 1048576 || loaded.PublicRequestLimitBytes != 1048576 || loaded.SSEEventLimitBytes != 65536 || loaded.MetadataResponseLimitBytes != 1048576 || loaded.DeadlineMS != 30000 { t.Fatalf("loaded invocation config = %#v", loaded) } } func TestLoadInvocationRuntimeRejectsInvalidDestinationSecretAndNumbers(t *testing.T) { tests := []struct{ name, variable, value string }{ - {"relative URL", "NEKIRO_ROUTER_INTERNAL_URL", "/internal/v4/invocations"}, - {"wrong path", "NEKIRO_ROUTER_INTERNAL_URL", "http://router.test:8081/internal/v2/invocations"}, - {"retired v3 path", "NEKIRO_ROUTER_INTERNAL_URL", "http://router.test:8081/internal/v3/invocations"}, - {"URL credentials", "NEKIRO_ROUTER_INTERNAL_URL", "http://user:secret@router.test:8081/internal/v4/invocations"}, - {"URL query", "NEKIRO_ROUTER_INTERNAL_URL", "http://router.test:8081/internal/v4/invocations?target=other"}, + {"relative URL", "NEKIRO_ROUTER_INTERNAL_URL", "/internal/v1/invocations"}, + {"wrong path", "NEKIRO_ROUTER_INTERNAL_URL", "http://router.test:8081/internal/v1/other"}, + {"retired v4 path", "NEKIRO_ROUTER_INTERNAL_URL", "http://router.test:8081/internal/v4/invocations"}, + {"URL credentials", "NEKIRO_ROUTER_INTERNAL_URL", "http://user:secret@router.test:8081/internal/v1/invocations"}, + {"URL query", "NEKIRO_ROUTER_INTERNAL_URL", "http://router.test:8081/internal/v1/invocations?target=other"}, {"blank token", "NEKIRO_ROUTER_INTERNAL_BEARER_TOKEN", ""}, {"token whitespace", "NEKIRO_ROUTER_INTERNAL_BEARER_TOKEN", "secret token"}, {"zero internal body", "NEKIRO_CONTROL_PLANE_INTERNAL_REQUEST_MAX_BYTES", "0"}, @@ -281,7 +281,7 @@ func TestLoadInvocationRuntimeRejectsEveryMissingVariable(t *testing.T) { func setValidInvocationRuntime(t *testing.T) { t.Helper() - t.Setenv("NEKIRO_ROUTER_INTERNAL_URL", "http://router.test:8081/internal/v4/invocations") + t.Setenv("NEKIRO_ROUTER_INTERNAL_URL", "http://router.test:8081/internal/v1/invocations") t.Setenv("NEKIRO_ROUTER_INTERNAL_BEARER_TOKEN", "router-secret") t.Setenv("NEKIRO_CONTROL_PLANE_INTERNAL_REQUEST_MAX_BYTES", "1048576") t.Setenv("NEKIRO_GATEWAY_INVOCATION_REQUEST_MAX_BYTES", "1048576") diff --git a/apps/control-plane/internal/gateway/catalog_handler.go b/apps/control-plane/internal/gateway/catalog_handler.go index eb75bd50..5ec500aa 100644 --- a/apps/control-plane/internal/gateway/catalog_handler.go +++ b/apps/control-plane/internal/gateway/catalog_handler.go @@ -69,11 +69,11 @@ func (handler *Handler) Routes() http.Handler { func (handler *Handler) RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("GET /livez", handler.liveness) mux.HandleFunc("GET /readyz", handler.readinessCheck) - mux.HandleFunc("POST /v3/agents", handler.register) - mux.HandleFunc("GET /v3/agents", handler.search) - mux.HandleFunc("GET /v3/agents/{agentId}/versions/{version}", handler.get) - mux.HandleFunc("POST /v3/agents/{agentId}/versions/{version}/publish", handler.publish) - mux.HandleFunc("POST /v3/agents/{agentId}/versions/{version}/disable", handler.disable) + mux.HandleFunc("POST /v1/agents", handler.register) + mux.HandleFunc("GET /v1/agents", handler.search) + mux.HandleFunc("GET /v1/agents/{agentId}/versions/{version}", handler.get) + mux.HandleFunc("POST /v1/agents/{agentId}/versions/{version}/publish", handler.publish) + mux.HandleFunc("POST /v1/agents/{agentId}/versions/{version}/disable", handler.disable) } func (handler *Handler) liveness(writer http.ResponseWriter, _ *http.Request) { diff --git a/apps/control-plane/internal/gateway/catalog_handler_test.go b/apps/control-plane/internal/gateway/catalog_handler_test.go index 8e58d3af..2c705ce3 100644 --- a/apps/control-plane/internal/gateway/catalog_handler_test.go +++ b/apps/control-plane/internal/gateway/catalog_handler_test.go @@ -73,7 +73,7 @@ func TestDevelopmentStaticAuthenticatorUsesBearerDigestOnly(t *testing.T) { if err != nil { t.Fatal(err) } - request := httptest.NewRequest(http.MethodGet, "/v3/agents", nil) + request := httptest.NewRequest(http.MethodGet, "/v1/agents", nil) request.Header.Set("Authorization", "Bearer "+token) request.Header.Set("x-caller-id", "forged-owner") caller, err := authenticator.Authenticate(request) @@ -85,7 +85,7 @@ func TestDevelopmentStaticAuthenticatorUsesBearerDigestOnly(t *testing.T) { } for _, authorization := range []string{"", "Bearer", "Bearer wrong", "Bearer " + token + " extra"} { - request := httptest.NewRequest(http.MethodGet, "/v3/agents", nil) + request := httptest.NewRequest(http.MethodGet, "/v1/agents", nil) if authorization != "" { request.Header.Set("Authorization", authorization) } @@ -93,7 +93,7 @@ func TestDevelopmentStaticAuthenticatorUsesBearerDigestOnly(t *testing.T) { t.Fatalf("authorization %q error = %v", authorization, err) } } - lowercaseScheme := httptest.NewRequest(http.MethodGet, "/v3/agents", nil) + lowercaseScheme := httptest.NewRequest(http.MethodGet, "/v1/agents", nil) lowercaseScheme.Header.Set("Authorization", "bearer "+token) if _, err := authenticator.Authenticate(lowercaseScheme); err != nil { t.Fatalf("case-insensitive Bearer scheme was rejected: %v", err) @@ -102,7 +102,7 @@ func TestDevelopmentStaticAuthenticatorUsesBearerDigestOnly(t *testing.T) { func TestHandlerAuthenticationErrorHasMatchingTrace(t *testing.T) { handler := newTestHandler(t, fakeAuthenticator{err: ErrUnauthenticated}, &fakeCatalogService{}, fakeReadiness{}) - request := httptest.NewRequest(http.MethodGet, "/v3/agents", nil) + request := httptest.NewRequest(http.MethodGet, "/v1/agents", nil) response := httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) if response.Code != http.StatusUnauthorized { @@ -117,7 +117,7 @@ func TestHandlerAuthenticationErrorHasMatchingTrace(t *testing.T) { } } -func TestActiveNorthboundV3CatalogRoutesComposeWithWorkspaceRoutes(t *testing.T) { +func TestActiveNorthboundV1CatalogRoutesComposeWithWorkspaceRoutes(t *testing.T) { catalogHandler := newTestHandler(t, fakeAuthenticator{caller: catalog.AuthenticatedCaller{ID: "owner-a"}}, &fakeCatalogService{ searchResult: catalog.SearchResult{Entries: []contracts.CatalogEntry{}}, }, fakeReadiness{}) @@ -126,19 +126,21 @@ func TestActiveNorthboundV3CatalogRoutesComposeWithWorkspaceRoutes(t *testing.T) catalogHandler.RegisterRoutes(mux) workspaceHandler.RegisterRoutes(mux) - catalogRequest := httptest.NewRequest(http.MethodGet, "/v3/agents", nil) + catalogRequest := httptest.NewRequest(http.MethodGet, "/v1/agents", nil) catalogResponse := httptest.NewRecorder() mux.ServeHTTP(catalogResponse, catalogRequest) if catalogResponse.Code != http.StatusOK { t.Fatalf("composed Catalog route status = %d, want 200", catalogResponse.Code) } - legacyResponse := httptest.NewRecorder() - mux.ServeHTTP(legacyResponse, httptest.NewRequest(http.MethodGet, "/v2/agents", nil)) - if legacyResponse.Code != http.StatusNotFound { - t.Fatalf("historical Catalog route status = %d, want 404", legacyResponse.Code) + for _, path := range []string{"/v2/agents", "/v3/agents", "/v4/agents"} { + retiredResponse := httptest.NewRecorder() + mux.ServeHTTP(retiredResponse, httptest.NewRequest(http.MethodGet, path, nil)) + if retiredResponse.Code != http.StatusNotFound { + t.Fatalf("retired Catalog route %s status = %d, want 404", path, retiredResponse.Code) + } } - workspaceRequest := httptest.NewRequest(http.MethodPost, "/v3/workspaces", strings.NewReader(`{"workspaceId":"workspace-a"}`)) + workspaceRequest := httptest.NewRequest(http.MethodPost, "/v1/workspaces", strings.NewReader(`{"workspaceId":"workspace-a"}`)) workspaceResponse := httptest.NewRecorder() mux.ServeHTTP(workspaceResponse, workspaceRequest) if workspaceResponse.Code != http.StatusCreated { @@ -150,7 +152,7 @@ func TestHandlerRegisterAndFixedDomainErrors(t *testing.T) { caller := catalog.AuthenticatedCaller{ID: "owner-a", AuthenticationKind: config.DevelopmentStaticAuthMode} service := &fakeCatalogService{entry: contracts.CatalogEntry{PublicationStatus: "draft", RegisteredAt: time.Now().UTC()}} handler := newTestHandler(t, fakeAuthenticator{caller: caller}, service, fakeReadiness{}) - request := httptest.NewRequest(http.MethodPost, "/v3/agents", bytes.NewBufferString(`{"card":{}}`)) + request := httptest.NewRequest(http.MethodPost, "/v1/agents", bytes.NewBufferString(`{"card":{}}`)) request.Header.Set("Content-Type", "application/json") response := newDeadlineRecorder() handler.Routes().ServeHTTP(response, request) @@ -177,7 +179,7 @@ func TestHandlerRegisterAndFixedDomainErrors(t *testing.T) { } for _, test := range tests { service.err = test.err - request := httptest.NewRequest(http.MethodGet, "/v3/agents/agent-a/versions/1.0.0", nil) + request := httptest.NewRequest(http.MethodGet, "/v1/agents/agent-a/versions/1.0.0", nil) response := httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) if response.Code != test.status { @@ -199,7 +201,7 @@ func TestHandlerRejectsInvalidMediaAndSearchParameters(t *testing.T) { service := &fakeCatalogService{searchResult: catalog.SearchResult{Entries: []contracts.CatalogEntry{}}} handler := newTestHandler(t, fakeAuthenticator{caller: caller}, service, fakeReadiness{}) - request := httptest.NewRequest(http.MethodPost, "/v3/agents", bytes.NewBufferString(`{"card":{}}`)) + request := httptest.NewRequest(http.MethodPost, "/v1/agents", bytes.NewBufferString(`{"card":{}}`)) request.Header.Set("Content-Type", "text/plain") response := httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -208,7 +210,7 @@ func TestHandlerRejectsInvalidMediaAndSearchParameters(t *testing.T) { } for _, rawQuery := range []string{"unknown=value", "limit=0", "limit=abc", "query=a&query=b", "query=%ZZ"} { - request := httptest.NewRequest(http.MethodGet, "/v3/agents?"+rawQuery, nil) + request := httptest.NewRequest(http.MethodGet, "/v1/agents?"+rawQuery, nil) response := httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) if response.Code != http.StatusBadRequest { @@ -222,7 +224,7 @@ func TestHandlerRejectsOversizedRegistrationBeforeCatalog(t *testing.T) { service := &fakeCatalogService{} handler := newTestHandler(t, fakeAuthenticator{caller: caller}, service, fakeReadiness{}) body := io.LimitReader(repeatingReader{}, contracts.RegistrationMaximumBodyBytes+1) - request := httptest.NewRequest(http.MethodPost, "/v3/agents", body) + request := httptest.NewRequest(http.MethodPost, "/v1/agents", body) request.Header.Set("Content-Type", "application/json") response := newDeadlineRecorder() handler.Routes().ServeHTTP(response, request) @@ -238,7 +240,7 @@ func TestHandlerFailsBeforeCatalogWhenBodyDeadlineCannotBeControlled(t *testing. caller := catalog.AuthenticatedCaller{ID: "owner-a"} service := &fakeCatalogService{} handler := newTestHandler(t, fakeAuthenticator{caller: caller}, service, fakeReadiness{}) - request := httptest.NewRequest(http.MethodPost, "/v3/agents", bytes.NewBufferString(`{"card":{}}`)) + request := httptest.NewRequest(http.MethodPost, "/v1/agents", bytes.NewBufferString(`{"card":{}}`)) request.Header.Set("Content-Type", "application/json") unsupported := httptest.NewRecorder() handler.Routes().ServeHTTP(unsupported, request) @@ -252,7 +254,7 @@ func TestHandlerFailsBeforeCatalogWhenBodyDeadlineCannotBeControlled(t *testing. for _, failCall := range []int{0, 1} { service := &fakeCatalogService{} handler := newTestHandler(t, fakeAuthenticator{caller: caller}, service, fakeReadiness{}) - request := httptest.NewRequest(http.MethodPost, "/v3/agents", bytes.NewBufferString(`{"card":{}}`)) + request := httptest.NewRequest(http.MethodPost, "/v1/agents", bytes.NewBufferString(`{"card":{}}`)) request.Header.Set("Content-Type", "application/json") response := newDeadlineRecorder() response.failCall = failCall @@ -400,7 +402,7 @@ func writeRequestHeaders(t *testing.T, connection net.Conn, host string, bodyLen if complete { ending = "\r\n" } - if _, err := fmt.Fprintf(connection, "POST /v3/agents HTTP/1.1\r\nHost: %s\r\nContent-Type: application/json\r\nContent-Length: %d\r\n%s", host, bodyLength, ending); err != nil { + if _, err := fmt.Fprintf(connection, "POST /v1/agents HTTP/1.1\r\nHost: %s\r\nContent-Type: application/json\r\nContent-Length: %d\r\n%s", host, bodyLength, ending); err != nil { t.Fatalf("write registration headers: %v", err) } } diff --git a/apps/control-plane/internal/gateway/cors.go b/apps/control-plane/internal/gateway/cors.go index 7598dd30..fd9f6966 100644 --- a/apps/control-plane/internal/gateway/cors.go +++ b/apps/control-plane/internal/gateway/cors.go @@ -21,7 +21,7 @@ func CORS(allowedOrigins []string, next http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { origin := request.Header.Get("Origin") _, originAllowed := allowed[origin] - publicRoute := strings.HasPrefix(request.URL.Path, "/v3/") || strings.HasPrefix(request.URL.Path, "/v4/") + publicRoute := strings.HasPrefix(request.URL.Path, "/v1/") || strings.HasPrefix(request.URL.Path, "/v1/") if origin != "" && originAllowed && publicRoute { writer.Header().Set("Vary", "Origin") writer.Header().Set("Access-Control-Allow-Origin", origin) diff --git a/apps/control-plane/internal/gateway/cors_test.go b/apps/control-plane/internal/gateway/cors_test.go index 9011b91f..63916c13 100644 --- a/apps/control-plane/internal/gateway/cors_test.go +++ b/apps/control-plane/internal/gateway/cors_test.go @@ -11,7 +11,7 @@ func TestCORSAllowsConfiguredPublicOriginAndPreflightWithoutAuth(t *testing.T) { handler := CORS([]string{"http://localhost:3000"}, http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { writer.WriteHeader(http.StatusTeapot) })) - request := httptest.NewRequest(http.MethodOptions, "/v4/workspaces/ws-1/invocations", nil) + request := httptest.NewRequest(http.MethodOptions, "/v1/workspaces/ws-1/invocations", nil) request.Header.Set("Origin", "http://localhost:3000") response := httptest.NewRecorder() handler.ServeHTTP(response, request) @@ -24,7 +24,7 @@ func TestCORSAllowsConfiguredPublicOriginAndPreflightWithoutAuth(t *testing.T) { } func TestCORSDoesNotGrantUnknownOrInternalOrigin(t *testing.T) { - for _, path := range []string{"/v4/agents", "/internal/v4/invocations", "/healthz"} { + for _, path := range []string{"/v1/agents", "/internal/v1/invocations", "/healthz"} { t.Run(path, func(t *testing.T) { handler := CORS([]string{"http://localhost:3000"}, http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { writer.WriteHeader(http.StatusNoContent) diff --git a/apps/control-plane/internal/gateway/invocation_handler.go b/apps/control-plane/internal/gateway/invocation_handler.go index 7ee63af8..dd177b82 100644 --- a/apps/control-plane/internal/gateway/invocation_handler.go +++ b/apps/control-plane/internal/gateway/invocation_handler.go @@ -46,7 +46,7 @@ func NewInvocationHandler(authenticator Authenticator, dispatcher InvocationDisp } func (handler *InvocationHandler) RegisterRoutes(mux *http.ServeMux) { - mux.HandleFunc("POST /v4/workspaces/{workspaceId}/invocations", handler.invoke) + mux.HandleFunc("POST /v1/workspaces/{workspaceId}/invocations", handler.invoke) } func (handler *InvocationHandler) invoke(writer http.ResponseWriter, request *http.Request) { diff --git a/apps/control-plane/internal/gateway/invocation_handler_test.go b/apps/control-plane/internal/gateway/invocation_handler_test.go index 1d9afa70..574c94c7 100644 --- a/apps/control-plane/internal/gateway/invocation_handler_test.go +++ b/apps/control-plane/internal/gateway/invocation_handler_test.go @@ -69,7 +69,7 @@ func TestInvocationHandlerStrictlyRejectsPreDispatchRequests(t *testing.T) { t.Run(test.name, func(t *testing.T) { dispatcher := &invocationDispatcherStub{} handler := newInvocationTestHandler(t, invocationAuthenticatorStub{caller: catalog.AuthenticatedCaller{ID: "owner-a"}, err: test.authErr}, dispatcher, test.limit) - request := httptest.NewRequest(http.MethodPost, "/v4/workspaces/workspace-a/invocations", strings.NewReader(test.body)) + request := httptest.NewRequest(http.MethodPost, "/v1/workspaces/workspace-a/invocations", strings.NewReader(test.body)) request.Header.Set("Content-Type", test.contentType) request.Header.Set("Accept", test.accept) response := httptest.NewRecorder() @@ -96,7 +96,7 @@ func TestInvocationHandlerForwardsExactJSONAndTrustedArguments(t *testing.T) { headers.Set(TraceHeader, "router-trace") dispatcher := &invocationDispatcherStub{response: &invocation.RouterResponse{StatusCode: 200, ContentType: "application/json", Headers: headers, Body: io.NopCloser(strings.NewReader(result))}} handler := newInvocationTestHandler(t, invocationAuthenticatorStub{caller: catalog.AuthenticatedCaller{ID: "owner-a", AuthenticationKind: "development-static"}}, dispatcher, 4096) - request := httptest.NewRequest(http.MethodPost, "/v4/workspaces/workspace-a/invocations", strings.NewReader(validInvokeBody(false))) + request := httptest.NewRequest(http.MethodPost, "/v1/workspaces/workspace-a/invocations", strings.NewReader(validInvokeBody(false))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/*") response := httptest.NewRecorder() @@ -225,7 +225,7 @@ func newInvocationTestHandler(t *testing.T, authenticator Authenticator, dispatc func invokeWithTestHandler(t *testing.T, dispatcher InvocationDispatcher) *httptest.ResponseRecorder { t.Helper() handler := newInvocationTestHandler(t, invocationAuthenticatorStub{caller: catalog.AuthenticatedCaller{ID: "owner-a"}}, dispatcher, 4096) - request := httptest.NewRequest(http.MethodPost, "/v4/workspaces/workspace-a/invocations", strings.NewReader(validInvokeBody(false))) + request := httptest.NewRequest(http.MethodPost, "/v1/workspaces/workspace-a/invocations", strings.NewReader(validInvokeBody(false))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") response := httptest.NewRecorder() diff --git a/apps/control-plane/internal/gateway/invocation_read_handler.go b/apps/control-plane/internal/gateway/invocation_read_handler.go index 68b5abd8..d5050844 100644 --- a/apps/control-plane/internal/gateway/invocation_read_handler.go +++ b/apps/control-plane/internal/gateway/invocation_read_handler.go @@ -42,8 +42,8 @@ func NewInvocationReadHandler(authenticator Authenticator, reader InvocationMeta } func (handler *InvocationReadHandler) RegisterRoutes(mux *http.ServeMux) { - mux.HandleFunc("GET /v4/workspaces/{workspaceId}/invocations/{invocationId}", handler.getInvocation) - mux.HandleFunc("GET /v4/workspaces/{workspaceId}/traces/{traceId}", handler.getTrace) + mux.HandleFunc("GET /v1/workspaces/{workspaceId}/invocations/{invocationId}", handler.getInvocation) + mux.HandleFunc("GET /v1/workspaces/{workspaceId}/traces/{traceId}", handler.getTrace) } func (handler *InvocationReadHandler) getInvocation(writer http.ResponseWriter, request *http.Request) { @@ -135,7 +135,7 @@ func (handler *InvocationReadHandler) validateMetadataBody(body []byte, resource decoder.DisallowUnknownFields() switch resource { case "Invocation": - var detail contracts.InvocationDetailResponseV4 + var detail contracts.InvocationDetailResponseV1 if err := decoder.Decode(&detail); err != nil { return err } @@ -145,9 +145,9 @@ func (handler *InvocationReadHandler) validateMetadataBody(body []byte, resource if detail.Invocation.InvocationID != resourceID { return errors.New("invocation response identity does not match request") } - return handler.validator.ValidateInvocationDetailResponseV4(workspaceID, detail) + return handler.validator.ValidateInvocationDetailResponseV1(workspaceID, detail) case "Trace": - var trace contracts.TraceResponseV4 + var trace contracts.TraceResponseV1 if err := decoder.Decode(&trace); err != nil { return err } @@ -158,7 +158,7 @@ func (handler *InvocationReadHandler) validateMetadataBody(body []byte, resource if err != nil || trace.TraceID != requested { return errors.New("trace response identity does not match request") } - return contracts.ValidateTraceResponseV4(workspaceID, requested, trace) + return contracts.ValidateTraceResponseV1(workspaceID, requested, trace) default: return errors.New("metadata response kind is unsupported") } diff --git a/apps/control-plane/internal/gateway/invocation_read_handler_test.go b/apps/control-plane/internal/gateway/invocation_read_handler_test.go index e1c316d7..fb00e0c4 100644 --- a/apps/control-plane/internal/gateway/invocation_read_handler_test.go +++ b/apps/control-plane/internal/gateway/invocation_read_handler_test.go @@ -48,11 +48,11 @@ func TestInvocationReadHandlerProxiesAuthorizedMetadataAndCorrelation(t *testing traceResponse: metadataHTTPResponse(http.StatusOK, validTraceMetadataJSON), } handler := newInvocationReadTestHandler(t, invocationAuthenticatorStub{caller: catalog.AuthenticatedCaller{ID: "owner-a", AuthenticationKind: "development-static"}}, reader) - invocationResponse := serveInvocationReadTestRequest(handler, "/v4/workspaces/workspace-a/invocations/inv-a") + invocationResponse := serveInvocationReadTestRequest(handler, "/v1/workspaces/workspace-a/invocations/inv-a") if invocationResponse.Code != http.StatusOK || invocationResponse.Body.String() != validInvocationMetadataJSON || invocationResponse.Header().Get(TraceHeader) == "" { t.Fatalf("Invocation response = %d %q trace=%q", invocationResponse.Code, invocationResponse.Body.String(), invocationResponse.Header().Get(TraceHeader)) } - traceRequest := httptest.NewRequest(http.MethodGet, "/v4/workspaces/workspace-a/traces/trace-a", nil) + traceRequest := httptest.NewRequest(http.MethodGet, "/v1/workspaces/workspace-a/traces/trace-a", nil) traceResponse := httptest.NewRecorder() handler.ServeHTTP(traceResponse, traceRequest) if traceResponse.Code != http.StatusOK || traceResponse.Body.String() != validTraceMetadataJSON || reader.traceCalls != 1 { @@ -74,23 +74,23 @@ func TestInvocationReadHandlerRejectsBeforeRouterAndPreservesReadFailures(t *tes response *invocation.RouterResponse wantInvoke int }{ - {name: "unauthenticated first", authErr: ErrUnauthenticated, status: http.StatusUnauthorized, code: contracts.ErrorCodeUnauthenticated, path: "/v4/workspaces/workspace-a/invocations/inv-a"}, - {name: "invalid path", status: http.StatusBadRequest, code: contracts.ErrorCodeValidationError, path: "/v4/workspaces/bad%20workspace/invocations/inv-a"}, - {name: "workspace forbidden", readerErr: workspace.ErrForbidden, status: http.StatusForbidden, code: contracts.ErrorCodeForbidden, path: "/v4/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, - {name: "workspace missing", readerErr: workspace.ErrNotFound, status: http.StatusNotFound, code: contracts.ErrorCodeNotFound, path: "/v4/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, - {name: "workspace dependency", readerErr: workspace.ErrDependency, status: http.StatusServiceUnavailable, code: contracts.ErrorCodeDependency, path: "/v4/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, - {name: "read deadline", readerErr: context.DeadlineExceeded, status: http.StatusServiceUnavailable, code: contracts.ErrorCodeDependency, path: "/v4/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, - {name: "Router not found", response: metadataHTTPResponse(http.StatusNotFound, `{"code":"NOT_FOUND"}`), status: http.StatusNotFound, code: contracts.ErrorCodeNotFound, path: "/v4/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, - {name: "Router dependency", response: metadataHTTPResponse(http.StatusServiceUnavailable, `{"code":"DEPENDENCY_ERROR"}`), status: http.StatusServiceUnavailable, code: contracts.ErrorCodeDependency, path: "/v4/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, - {name: "Router wrong media", response: &invocation.RouterResponse{StatusCode: http.StatusOK, ContentType: "text/plain", Body: io.NopCloser(strings.NewReader("internal"))}, status: http.StatusServiceUnavailable, code: contracts.ErrorCodeDependency, path: "/v4/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, - {name: "Router not-found wrong media", response: &invocation.RouterResponse{StatusCode: http.StatusNotFound, ContentType: "text/plain", Body: io.NopCloser(strings.NewReader("internal"))}, status: http.StatusServiceUnavailable, code: contracts.ErrorCodeDependency, path: "/v4/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, - {name: "Router malformed success", response: metadataHTTPResponse(http.StatusOK, `{}`), status: http.StatusServiceUnavailable, code: contracts.ErrorCodeDependency, path: "/v4/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, - {name: "Router content-bearing success", response: metadataHTTPResponse(http.StatusOK, strings.Replace(validInvocationMetadataJSON, `"events":[`, `"input":{"secret":"value"},"events":[`, 1)), status: http.StatusServiceUnavailable, code: contracts.ErrorCodeDependency, path: "/v4/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, - {name: "Router metadata exceeds separate limit", response: metadataHTTPResponse(http.StatusOK, validInvocationMetadataJSON+strings.Repeat(" ", 5000)), status: http.StatusServiceUnavailable, code: contracts.ErrorCodeDependency, path: "/v4/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, - {name: "Router duplicate member", response: metadataHTTPResponse(http.StatusOK, strings.Replace(validInvocationMetadataJSON, `"invocationId":"inv-a","rootTaskId"`, `"invocationId":"inv-a","invocationId":"inv-a","rootTaskId"`, 1)), status: http.StatusServiceUnavailable, code: contracts.ErrorCodeDependency, path: "/v4/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, - {name: "Router trailing JSON", response: metadataHTTPResponse(http.StatusOK, validInvocationMetadataJSON+`{}`), status: http.StatusServiceUnavailable, code: contracts.ErrorCodeDependency, path: "/v4/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, - {name: "Router unknown nested event member", response: metadataHTTPResponse(http.StatusOK, strings.Replace(validInvocationMetadataJSON, `"eventId":"event-a"`, `"eventId":"event-a","secret":"value"`, 1)), status: http.StatusServiceUnavailable, code: contracts.ErrorCodeDependency, path: "/v4/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, - {name: "Router malformed trace record", response: metadataHTTPResponse(http.StatusOK, strings.Replace(validTraceMetadataJSON, `,"createdAt":"2026-07-16T12:00:00Z"`, ``, 1)), status: http.StatusServiceUnavailable, code: contracts.ErrorCodeDependency, path: "/v4/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, + {name: "unauthenticated first", authErr: ErrUnauthenticated, status: http.StatusUnauthorized, code: contracts.ErrorCodeUnauthenticated, path: "/v1/workspaces/workspace-a/invocations/inv-a"}, + {name: "invalid path", status: http.StatusBadRequest, code: contracts.ErrorCodeValidationError, path: "/v1/workspaces/bad%20workspace/invocations/inv-a"}, + {name: "workspace forbidden", readerErr: workspace.ErrForbidden, status: http.StatusForbidden, code: contracts.ErrorCodeForbidden, path: "/v1/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, + {name: "workspace missing", readerErr: workspace.ErrNotFound, status: http.StatusNotFound, code: contracts.ErrorCodeNotFound, path: "/v1/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, + {name: "workspace dependency", readerErr: workspace.ErrDependency, status: http.StatusServiceUnavailable, code: contracts.ErrorCodeDependency, path: "/v1/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, + {name: "read deadline", readerErr: context.DeadlineExceeded, status: http.StatusServiceUnavailable, code: contracts.ErrorCodeDependency, path: "/v1/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, + {name: "Router not found", response: metadataHTTPResponse(http.StatusNotFound, `{"code":"NOT_FOUND"}`), status: http.StatusNotFound, code: contracts.ErrorCodeNotFound, path: "/v1/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, + {name: "Router dependency", response: metadataHTTPResponse(http.StatusServiceUnavailable, `{"code":"DEPENDENCY_ERROR"}`), status: http.StatusServiceUnavailable, code: contracts.ErrorCodeDependency, path: "/v1/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, + {name: "Router wrong media", response: &invocation.RouterResponse{StatusCode: http.StatusOK, ContentType: "text/plain", Body: io.NopCloser(strings.NewReader("internal"))}, status: http.StatusServiceUnavailable, code: contracts.ErrorCodeDependency, path: "/v1/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, + {name: "Router not-found wrong media", response: &invocation.RouterResponse{StatusCode: http.StatusNotFound, ContentType: "text/plain", Body: io.NopCloser(strings.NewReader("internal"))}, status: http.StatusServiceUnavailable, code: contracts.ErrorCodeDependency, path: "/v1/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, + {name: "Router malformed success", response: metadataHTTPResponse(http.StatusOK, `{}`), status: http.StatusServiceUnavailable, code: contracts.ErrorCodeDependency, path: "/v1/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, + {name: "Router content-bearing success", response: metadataHTTPResponse(http.StatusOK, strings.Replace(validInvocationMetadataJSON, `"events":[`, `"input":{"secret":"value"},"events":[`, 1)), status: http.StatusServiceUnavailable, code: contracts.ErrorCodeDependency, path: "/v1/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, + {name: "Router metadata exceeds separate limit", response: metadataHTTPResponse(http.StatusOK, validInvocationMetadataJSON+strings.Repeat(" ", 5000)), status: http.StatusServiceUnavailable, code: contracts.ErrorCodeDependency, path: "/v1/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, + {name: "Router duplicate member", response: metadataHTTPResponse(http.StatusOK, strings.Replace(validInvocationMetadataJSON, `"invocationId":"inv-a","rootTaskId"`, `"invocationId":"inv-a","invocationId":"inv-a","rootTaskId"`, 1)), status: http.StatusServiceUnavailable, code: contracts.ErrorCodeDependency, path: "/v1/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, + {name: "Router trailing JSON", response: metadataHTTPResponse(http.StatusOK, validInvocationMetadataJSON+`{}`), status: http.StatusServiceUnavailable, code: contracts.ErrorCodeDependency, path: "/v1/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, + {name: "Router unknown nested event member", response: metadataHTTPResponse(http.StatusOK, strings.Replace(validInvocationMetadataJSON, `"eventId":"event-a"`, `"eventId":"event-a","secret":"value"`, 1)), status: http.StatusServiceUnavailable, code: contracts.ErrorCodeDependency, path: "/v1/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, + {name: "Router malformed trace record", response: metadataHTTPResponse(http.StatusOK, strings.Replace(validTraceMetadataJSON, `,"createdAt":"2026-07-16T12:00:00Z"`, ``, 1)), status: http.StatusServiceUnavailable, code: contracts.ErrorCodeDependency, path: "/v1/workspaces/workspace-a/invocations/inv-a", wantInvoke: 1}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -129,7 +129,7 @@ func TestInvocationReadHandlerRejectsMalformedTraceRecord(t *testing.T) { reader := &metadataReadHandlerStub{traceResponse: metadataHTTPResponse(http.StatusOK, strings.Replace(validTraceMetadataJSON, `,"createdAt":"2026-07-16T12:00:00Z"`, ``, 1))} handler := newInvocationReadTestHandler(t, invocationAuthenticatorStub{caller: catalog.AuthenticatedCaller{ID: "owner-a"}}, reader) response := httptest.NewRecorder() - handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/v4/workspaces/workspace-a/traces/trace-a", nil)) + handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/v1/workspaces/workspace-a/traces/trace-a", nil)) if response.Code != http.StatusServiceUnavailable || reader.traceCalls != 1 { t.Fatalf("malformed Trace response = %d, calls=%d, body=%s", response.Code, reader.traceCalls, response.Body.String()) } diff --git a/apps/control-plane/internal/gateway/public_share_handler.go b/apps/control-plane/internal/gateway/public_share_handler.go index 91a7be8e..fcc822f8 100644 --- a/apps/control-plane/internal/gateway/public_share_handler.go +++ b/apps/control-plane/internal/gateway/public_share_handler.go @@ -28,8 +28,8 @@ func NewPublicShareHandler(service PublicShareResolver, traces *TraceGenerator, } func (handler *PublicShareHandler) RegisterRoutes(mux *http.ServeMux) { - mux.HandleFunc("GET /v4/public/agents/{publicAgentId}", handler.resolve) - mux.HandleFunc("GET /v4/public/agents/", handler.resolve) + mux.HandleFunc("GET /v1/public/agents/{publicAgentId}", handler.resolve) + mux.HandleFunc("GET /v1/public/agents/", handler.resolve) } func (handler *PublicShareHandler) resolve(writer http.ResponseWriter, request *http.Request) { diff --git a/apps/control-plane/internal/gateway/public_share_handler_test.go b/apps/control-plane/internal/gateway/public_share_handler_test.go index 0539c8cb..fc46470f 100644 --- a/apps/control-plane/internal/gateway/public_share_handler_test.go +++ b/apps/control-plane/internal/gateway/public_share_handler_test.go @@ -33,7 +33,7 @@ func TestPublicShareHandlerIsAnonymousAndTraceCorrelated(t *testing.T) { } mux := http.NewServeMux() handler.RegisterRoutes(mux) - request := httptest.NewRequest(http.MethodGet, "/v4/public/agents/agt_0123456789abcdef0123456789abcdef", nil) + request := httptest.NewRequest(http.MethodGet, "/v1/public/agents/agt_0123456789abcdef0123456789abcdef", nil) response := httptest.NewRecorder() mux.ServeHTTP(response, request) if response.Code != http.StatusOK || response.Header().Get(TraceHeader) == "" || response.Header().Get("Content-Type") != "application/json" { @@ -65,7 +65,7 @@ func TestPublicShareHandlerMapsExactFailuresWithoutAuth(t *testing.T) { mux := http.NewServeMux() handler.RegisterRoutes(mux) response := httptest.NewRecorder() - request := httptest.NewRequest(http.MethodGet, "/v4/public/agents/agt_0123456789abcdef0123456789abcdef", nil) + request := httptest.NewRequest(http.MethodGet, "/v1/public/agents/agt_0123456789abcdef0123456789abcdef", nil) mux.ServeHTTP(response, request) var payload contracts.PlatformError if response.Code != test.status || json.Unmarshal(response.Body.Bytes(), &payload) != nil || payload.Code != test.code || string(payload.TraceID) != response.Header().Get(TraceHeader) { diff --git a/apps/control-plane/internal/gateway/release_handler.go b/apps/control-plane/internal/gateway/release_handler.go index f4eb097d..56145985 100644 --- a/apps/control-plane/internal/gateway/release_handler.go +++ b/apps/control-plane/internal/gateway/release_handler.go @@ -36,12 +36,12 @@ func NewReleaseHandler(authenticator Authenticator, service ReleaseCatalogServic } func (handler *ReleaseHandler) RegisterRoutes(mux *http.ServeMux) { - mux.HandleFunc("POST /v4/providers/{providerId}/agents/{agentId}/releases", handler.create) - mux.HandleFunc("GET /v4/releases/{releaseId}", handler.get) - mux.HandleFunc("POST /v4/releases/{releaseId}/verify", handler.verify) - mux.HandleFunc("POST /v4/releases/{releaseId}/publish", handler.publish) - mux.HandleFunc("POST /v4/releases/{releaseId}/suspend", handler.suspend) - mux.HandleFunc("POST /v4/releases/{releaseId}/revoke", handler.revoke) + mux.HandleFunc("POST /v1/providers/{providerId}/agents/{agentId}/releases", handler.create) + mux.HandleFunc("GET /v1/releases/{releaseId}", handler.get) + mux.HandleFunc("POST /v1/releases/{releaseId}/verify", handler.verify) + mux.HandleFunc("POST /v1/releases/{releaseId}/publish", handler.publish) + mux.HandleFunc("POST /v1/releases/{releaseId}/suspend", handler.suspend) + mux.HandleFunc("POST /v1/releases/{releaseId}/revoke", handler.revoke) } func (handler *ReleaseHandler) create(writer http.ResponseWriter, request *http.Request) { diff --git a/apps/control-plane/internal/gateway/release_handler_test.go b/apps/control-plane/internal/gateway/release_handler_test.go index 2c265914..9640963e 100644 --- a/apps/control-plane/internal/gateway/release_handler_test.go +++ b/apps/control-plane/internal/gateway/release_handler_test.go @@ -47,7 +47,7 @@ func TestReleaseHandlerCreatesExactReleaseWithoutProofMaterial(t *testing.T) { evidence := [32]byte{4, 5, 6} service := &fakeReleaseCatalog{release: catalog.AgentRelease{ReleaseID: "release-a", ProviderID: "provider-a", AgentID: "agent-a", AgentCardVersion: "1.0.0", CardDigest: digest, EndpointBindingID: "binding-a", EndpointOrigin: "https://agent.example", EndpointPath: "/a2a", VerificationMethod: catalog.VerificationMethodHTTPWellKnown, VerificationEvidenceDigest: &evidence, State: catalog.ReleaseVerified, CreatedAt: now, UpdatedAt: now, VerifiedAt: &now}} handler := newReleaseTestHandler(t, service) - request := httptest.NewRequest(http.MethodPost, "/v4/providers/provider-a/agents/agent-a/releases", strings.NewReader(`{"version":"1.0.0","endpointBindingId":"binding-a"}`)) + request := httptest.NewRequest(http.MethodPost, "/v1/providers/provider-a/agents/agent-a/releases", strings.NewReader(`{"version":"1.0.0","endpointBindingId":"binding-a"}`)) request.Header.Set("Content-Type", "application/json") recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) @@ -66,7 +66,7 @@ func TestReleaseHandlerCreatesExactReleaseWithoutProofMaterial(t *testing.T) { func TestReleaseHandlerMapsIllegalTransitionToTypedConflict(t *testing.T) { handler := newReleaseTestHandler(t, &fakeReleaseCatalog{err: catalog.ErrReleaseConflict}) recorder := httptest.NewRecorder() - handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodPost, "/v4/releases/release-a/publish", nil)) + handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodPost, "/v1/releases/release-a/publish", nil)) if recorder.Code != http.StatusConflict { t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String()) } diff --git a/apps/control-plane/internal/gateway/trust_handler.go b/apps/control-plane/internal/gateway/trust_handler.go index 94a3a7cc..33d48bd0 100644 --- a/apps/control-plane/internal/gateway/trust_handler.go +++ b/apps/control-plane/internal/gateway/trust_handler.go @@ -37,10 +37,10 @@ func NewTrustHandler(authenticator Authenticator, trust TrustCatalogService, tra } func (handler *TrustHandler) RegisterRoutes(mux *http.ServeMux) { - mux.HandleFunc("POST /v4/providers/{providerId}/agents/{agentId}/endpoint-bindings", handler.createBinding) - mux.HandleFunc("GET /v4/providers/{providerId}/endpoint-bindings/{bindingId}", handler.getBinding) - mux.HandleFunc("POST /v4/providers/{providerId}/endpoint-bindings/{bindingId}/challenges", handler.createChallenge) - mux.HandleFunc("POST /v4/providers/{providerId}/endpoint-bindings/{bindingId}/challenges/{challengeId}/complete", handler.completeChallenge) + mux.HandleFunc("POST /v1/providers/{providerId}/agents/{agentId}/endpoint-bindings", handler.createBinding) + mux.HandleFunc("GET /v1/providers/{providerId}/endpoint-bindings/{bindingId}", handler.getBinding) + mux.HandleFunc("POST /v1/providers/{providerId}/endpoint-bindings/{bindingId}/challenges", handler.createChallenge) + mux.HandleFunc("POST /v1/providers/{providerId}/endpoint-bindings/{bindingId}/challenges/{challengeId}/complete", handler.completeChallenge) } func (handler *TrustHandler) createBinding(writer http.ResponseWriter, request *http.Request) { diff --git a/apps/control-plane/internal/gateway/trust_handler_test.go b/apps/control-plane/internal/gateway/trust_handler_test.go index f35c1c05..ac56ba09 100644 --- a/apps/control-plane/internal/gateway/trust_handler_test.go +++ b/apps/control-plane/internal/gateway/trust_handler_test.go @@ -19,7 +19,7 @@ func TestTrustHandlerCreatesBindingThroughAuthenticatedProvider(t *testing.T) { now := time.Date(2026, 7, 22, 0, 0, 0, 0, time.UTC) service := &fakeTrustCatalog{binding: catalog.EndpointBinding{BindingID: "binding-1", ProviderID: "provider-1", AgentID: "agent-1", AgentCardVersion: "1.0.0", Endpoint: "https://agent.example/a2a", VerificationMethod: catalog.VerificationMethodHTTPWellKnown, VerificationStatus: catalog.VerificationPending, CreatedAt: now, UpdatedAt: now}} handler := newTrustTestHandler(t, fakeAuthenticator{caller: catalog.AuthenticatedCaller{ID: "provider-1"}}, service) - request := httptest.NewRequest(http.MethodPost, "/v4/providers/provider-1/agents/agent-1/endpoint-bindings", strings.NewReader(`{"endpoint":"https://agent.example/a2a","method":"http_well_known","version":"1.0.0"}`)) + request := httptest.NewRequest(http.MethodPost, "/v1/providers/provider-1/agents/agent-1/endpoint-bindings", strings.NewReader(`{"endpoint":"https://agent.example/a2a","method":"http_well_known","version":"1.0.0"}`)) request.Header.Set("Content-Type", "application/json") recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) @@ -41,7 +41,7 @@ func TestTrustHandlerCreatesBindingThroughAuthenticatedProvider(t *testing.T) { func TestTrustHandlerMapsChallengeFailureWithoutLeakingProof(t *testing.T) { service := &fakeTrustCatalog{completeErr: catalog.ErrWrongProof} handler := newTrustTestHandler(t, fakeAuthenticator{caller: catalog.AuthenticatedCaller{ID: "provider-1"}}, service) - request := httptest.NewRequest(http.MethodPost, "/v4/providers/provider-1/endpoint-bindings/binding-1/challenges/challenge-1/complete", nil) + request := httptest.NewRequest(http.MethodPost, "/v1/providers/provider-1/endpoint-bindings/binding-1/challenges/challenge-1/complete", nil) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) if recorder.Code != http.StatusBadRequest { @@ -59,7 +59,7 @@ func TestTrustHandlerMapsChallengeFailureWithoutLeakingProof(t *testing.T) { func TestTrustHandlerMapsEndpointUnavailableToServiceUnavailable(t *testing.T) { service := &fakeTrustCatalog{completeErr: catalog.ErrEndpointUnavailable} handler := newTrustTestHandler(t, fakeAuthenticator{caller: catalog.AuthenticatedCaller{ID: "provider-1"}}, service) - request := httptest.NewRequest(http.MethodPost, "/v4/providers/provider-1/endpoint-bindings/binding-1/challenges/challenge-1/complete", nil) + request := httptest.NewRequest(http.MethodPost, "/v1/providers/provider-1/endpoint-bindings/binding-1/challenges/challenge-1/complete", nil) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) if recorder.Code != http.StatusServiceUnavailable { diff --git a/apps/control-plane/internal/gateway/workspace_handler.go b/apps/control-plane/internal/gateway/workspace_handler.go index 2076468b..37cb8f73 100644 --- a/apps/control-plane/internal/gateway/workspace_handler.go +++ b/apps/control-plane/internal/gateway/workspace_handler.go @@ -63,15 +63,15 @@ func (handler *WorkspaceHandler) Routes() http.Handler { // RegisterRoutes adds Workspace and internal resolution routes to the composed // Gateway mux. func (handler *WorkspaceHandler) RegisterRoutes(mux *http.ServeMux) { - mux.HandleFunc("POST /v3/workspaces", handler.createWorkspace) - mux.HandleFunc("GET /v3/workspaces/{workspaceId}", handler.getWorkspace) - mux.HandleFunc("POST /v3/workspaces/{workspaceId}/installations", handler.install) - mux.HandleFunc("GET /v3/workspaces/{workspaceId}/installations", handler.listInstallations) - mux.HandleFunc("GET /v3/workspaces/{workspaceId}/installations/{installationId}", handler.getInstallation) - mux.HandleFunc("PATCH /v3/workspaces/{workspaceId}/installations/{installationId}", handler.updateInstallation) - mux.HandleFunc("DELETE /v3/workspaces/{workspaceId}/installations/{installationId}", handler.uninstall) - mux.HandleFunc("POST /internal/v2/resolve-agent", handler.resolveAgent) - mux.HandleFunc("POST /internal/v3/resolve-installed-version", handler.resolveInstalledVersion) + mux.HandleFunc("POST /v1/workspaces", handler.createWorkspace) + mux.HandleFunc("GET /v1/workspaces/{workspaceId}", handler.getWorkspace) + mux.HandleFunc("POST /v1/workspaces/{workspaceId}/installations", handler.install) + mux.HandleFunc("GET /v1/workspaces/{workspaceId}/installations", handler.listInstallations) + mux.HandleFunc("GET /v1/workspaces/{workspaceId}/installations/{installationId}", handler.getInstallation) + mux.HandleFunc("PATCH /v1/workspaces/{workspaceId}/installations/{installationId}", handler.updateInstallation) + mux.HandleFunc("DELETE /v1/workspaces/{workspaceId}/installations/{installationId}", handler.uninstall) + mux.HandleFunc("POST /internal/v1/resolve-agent", handler.resolveAgent) + mux.HandleFunc("POST /internal/v1/resolve-installed-version", handler.resolveInstalledVersion) } func (handler *WorkspaceHandler) resolveInstalledVersion(writer http.ResponseWriter, request *http.Request) { diff --git a/apps/control-plane/internal/gateway/workspace_handler_test.go b/apps/control-plane/internal/gateway/workspace_handler_test.go index 16b11388..63183100 100644 --- a/apps/control-plane/internal/gateway/workspace_handler_test.go +++ b/apps/control-plane/internal/gateway/workspace_handler_test.go @@ -165,7 +165,7 @@ func (service *workspaceTestService) ResolveInstalledVersion(_ context.Context, func TestWorkspaceHandlerRequiresBearerAndRequiredListLimit(t *testing.T) { service := &workspaceTestService{} handler := newWorkspaceTestHandler(t, workspaceTestAuthenticator{caller: catalog.AuthenticatedCaller{ID: "owner-a"}}, service) - request := httptest.NewRequest(http.MethodPost, "/v3/workspaces", strings.NewReader(`{"workspaceId":"workspace-a"}`)) + request := httptest.NewRequest(http.MethodPost, "/v1/workspaces", strings.NewReader(`{"workspaceId":"workspace-a"}`)) request.Header.Set("Authorization", "Bearer token") response := httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -173,7 +173,7 @@ func TestWorkspaceHandlerRequiresBearerAndRequiredListLimit(t *testing.T) { t.Fatalf("create response = %d, workspace = %#v", response.Code, service.workspace) } - request = httptest.NewRequest(http.MethodGet, "/v3/workspaces/workspace-a/installations", nil) + request = httptest.NewRequest(http.MethodGet, "/v1/workspaces/workspace-a/installations", nil) request.Header.Set("Authorization", "Bearer token") response = httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -182,7 +182,7 @@ func TestWorkspaceHandlerRequiresBearerAndRequiredListLimit(t *testing.T) { } unauthenticated := newWorkspaceTestHandler(t, workspaceTestAuthenticator{err: ErrUnauthenticated}, service) - request = httptest.NewRequest(http.MethodGet, "/v3/workspaces/workspace-a", nil) + request = httptest.NewRequest(http.MethodGet, "/v1/workspaces/workspace-a", nil) response = httptest.NewRecorder() unauthenticated.Routes().ServeHTTP(response, request) if response.Code != http.StatusUnauthorized { @@ -194,7 +194,7 @@ func TestWorkspaceHandlerMapsWorkspaceCreateReadOutcomes(t *testing.T) { service := &workspaceTestService{} handler := newWorkspaceTestHandler(t, workspaceTestAuthenticator{caller: catalog.AuthenticatedCaller{ID: "owner-a"}}, service) - request := httptest.NewRequest(http.MethodPost, "/v3/workspaces", strings.NewReader(`{"workspaceId":"workspace-a"}`)) + request := httptest.NewRequest(http.MethodPost, "/v1/workspaces", strings.NewReader(`{"workspaceId":"workspace-a"}`)) request.Header.Set("Authorization", "Bearer token") response := httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -209,7 +209,7 @@ func TestWorkspaceHandlerMapsWorkspaceCreateReadOutcomes(t *testing.T) { t.Fatalf("created Workspace = %#v", created) } - request = httptest.NewRequest(http.MethodGet, "/v3/workspaces/workspace-a", nil) + request = httptest.NewRequest(http.MethodGet, "/v1/workspaces/workspace-a", nil) request.Header.Set("Authorization", "Bearer token") response = httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -225,7 +225,7 @@ func TestWorkspaceHandlerMapsWorkspaceCreateReadOutcomes(t *testing.T) { } service.createErr = workspace.ErrConflict - request = httptest.NewRequest(http.MethodPost, "/v3/workspaces", strings.NewReader(`{"workspaceId":"workspace-a"}`)) + request = httptest.NewRequest(http.MethodPost, "/v1/workspaces", strings.NewReader(`{"workspaceId":"workspace-a"}`)) request.Header.Set("Authorization", "Bearer token") response = httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -234,7 +234,7 @@ func TestWorkspaceHandlerMapsWorkspaceCreateReadOutcomes(t *testing.T) { } service.getErr = workspace.ErrForbidden - request = httptest.NewRequest(http.MethodGet, "/v3/workspaces/workspace-a", nil) + request = httptest.NewRequest(http.MethodGet, "/v1/workspaces/workspace-a", nil) request.Header.Set("Authorization", "Bearer token") response = httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -243,7 +243,7 @@ func TestWorkspaceHandlerMapsWorkspaceCreateReadOutcomes(t *testing.T) { } service.getErr = workspace.ErrNotFound response = httptest.NewRecorder() - request = httptest.NewRequest(http.MethodGet, "/v3/workspaces/missing-workspace", nil) + request = httptest.NewRequest(http.MethodGet, "/v1/workspaces/missing-workspace", nil) request.Header.Set("Authorization", "Bearer token") handler.Routes().ServeHTTP(response, request) if response.Code != http.StatusNotFound || !strings.Contains(response.Body.String(), `"code":"NOT_FOUND"`) { @@ -252,7 +252,7 @@ func TestWorkspaceHandlerMapsWorkspaceCreateReadOutcomes(t *testing.T) { service.getErr = nil createCallsBeforeInvalid := service.createCalls - request = httptest.NewRequest(http.MethodPost, "/v3/workspaces", strings.NewReader(`{"workspaceId":"workspace-b","ownerId":"attacker"}`)) + request = httptest.NewRequest(http.MethodPost, "/v1/workspaces", strings.NewReader(`{"workspaceId":"workspace-b","ownerId":"attacker"}`)) request.Header.Set("Authorization", "Bearer token") response = httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -288,7 +288,7 @@ func TestWorkspaceHandlerReadsAndListsInstallationFacts(t *testing.T) { } handler := newWorkspaceTestHandler(t, workspaceTestAuthenticator{caller: catalog.AuthenticatedCaller{ID: "owner-a"}}, service) - request := httptest.NewRequest(http.MethodGet, "/v3/workspaces/workspace-a/installations/installation-a", nil) + request := httptest.NewRequest(http.MethodGet, "/v1/workspaces/workspace-a/installations/installation-a", nil) request.Header.Set("Authorization", "Bearer token") response := httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -303,7 +303,7 @@ func TestWorkspaceHandlerReadsAndListsInstallationFacts(t *testing.T) { t.Fatalf("exact read = %#v, want %#v", read, installation) } - request = httptest.NewRequest(http.MethodGet, "/v3/workspaces/workspace-a/installations?limit=1&cursor="+cursor, nil) + request = httptest.NewRequest(http.MethodGet, "/v1/workspaces/workspace-a/installations?limit=1&cursor="+cursor, nil) request.Header.Set("Authorization", "Bearer token") response = httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -328,11 +328,11 @@ func TestWorkspaceHandlerInstallationInspectionFailures(t *testing.T) { status int code string }{ - {name: "unknown Workspace", path: "/v3/workspaces/missing-workspace/installations?limit=25", serviceErr: workspace.ErrNotFound, list: true, status: http.StatusNotFound, code: "NOT_FOUND"}, - {name: "unknown Installation", path: "/v3/workspaces/workspace-a/installations/missing-installation", serviceErr: workspace.ErrNotFound, status: http.StatusNotFound, code: "NOT_FOUND"}, - {name: "non-owner", path: "/v3/workspaces/workspace-a/installations/installation-a", serviceErr: workspace.ErrForbidden, status: http.StatusForbidden, code: "FORBIDDEN"}, - {name: "read dependency", path: "/v3/workspaces/workspace-a/installations/installation-a", serviceErr: workspace.ErrDependency, status: http.StatusServiceUnavailable, code: "DEPENDENCY_ERROR"}, - {name: "list dependency", path: "/v3/workspaces/workspace-a/installations?limit=25", serviceErr: workspace.ErrDependency, list: true, status: http.StatusServiceUnavailable, code: "DEPENDENCY_ERROR"}, + {name: "unknown Workspace", path: "/v1/workspaces/missing-workspace/installations?limit=25", serviceErr: workspace.ErrNotFound, list: true, status: http.StatusNotFound, code: "NOT_FOUND"}, + {name: "unknown Installation", path: "/v1/workspaces/workspace-a/installations/missing-installation", serviceErr: workspace.ErrNotFound, status: http.StatusNotFound, code: "NOT_FOUND"}, + {name: "non-owner", path: "/v1/workspaces/workspace-a/installations/installation-a", serviceErr: workspace.ErrForbidden, status: http.StatusForbidden, code: "FORBIDDEN"}, + {name: "read dependency", path: "/v1/workspaces/workspace-a/installations/installation-a", serviceErr: workspace.ErrDependency, status: http.StatusServiceUnavailable, code: "DEPENDENCY_ERROR"}, + {name: "list dependency", path: "/v1/workspaces/workspace-a/installations?limit=25", serviceErr: workspace.ErrDependency, list: true, status: http.StatusServiceUnavailable, code: "DEPENDENCY_ERROR"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -358,7 +358,7 @@ func TestWorkspaceHandlerInstallationInspectionFailures(t *testing.T) { service := &workspaceTestService{} handler := newWorkspaceTestHandler(t, workspaceTestAuthenticator{err: ErrUnauthenticated}, service) - request := httptest.NewRequest(http.MethodGet, "/v3/workspaces/workspace-a/installations?limit=25", nil) + request := httptest.NewRequest(http.MethodGet, "/v1/workspaces/workspace-a/installations?limit=25", nil) response := httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) if response.Code != http.StatusUnauthorized || service.listCalls != 0 || service.getInstallationCalls != 0 { @@ -368,7 +368,7 @@ func TestWorkspaceHandlerInstallationInspectionFailures(t *testing.T) { for _, query := range []string{"", "limit=0", "limit=101", "limit=abc", "limit=25&limit=50", "limit=25&cursor=a&cursor=b"} { service = &workspaceTestService{} handler = newWorkspaceTestHandler(t, workspaceTestAuthenticator{caller: catalog.AuthenticatedCaller{ID: "owner-a"}}, service) - request = httptest.NewRequest(http.MethodGet, "/v3/workspaces/workspace-a/installations?"+query, nil) + request = httptest.NewRequest(http.MethodGet, "/v1/workspaces/workspace-a/installations?"+query, nil) request.Header.Set("Authorization", "Bearer token") response = httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -379,7 +379,7 @@ func TestWorkspaceHandlerInstallationInspectionFailures(t *testing.T) { service = &workspaceTestService{listErr: workspace.ErrInvalid} handler = newWorkspaceTestHandler(t, workspaceTestAuthenticator{caller: catalog.AuthenticatedCaller{ID: "owner-a"}}, service) - request = httptest.NewRequest(http.MethodGet, "/v3/workspaces/workspace-a/installations?limit=25&cursor=malformed", nil) + request = httptest.NewRequest(http.MethodGet, "/v1/workspaces/workspace-a/installations?limit=25&cursor=malformed", nil) request.Header.Set("Authorization", "Bearer token") response = httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -391,7 +391,7 @@ func TestWorkspaceHandlerInstallationInspectionFailures(t *testing.T) { func TestWorkspaceHandlerReturnsExplicitEmptyInstallationList(t *testing.T) { service := &workspaceTestService{listResult: contracts.InstallationList{Items: []contracts.Installation{}}} handler := newWorkspaceTestHandler(t, workspaceTestAuthenticator{caller: catalog.AuthenticatedCaller{ID: "owner-a"}}, service) - request := httptest.NewRequest(http.MethodGet, "/v3/workspaces/workspace-a/installations?limit=25", nil) + request := httptest.NewRequest(http.MethodGet, "/v1/workspaces/workspace-a/installations?limit=25", nil) request.Header.Set("Authorization", "Bearer token") response := httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -430,7 +430,7 @@ func TestWorkspaceHandlerInstallRequiresPermissionArrayAndPreservesEmpty(t *test handler := newWorkspaceTestHandler(t, workspaceTestAuthenticator{caller: catalog.AuthenticatedCaller{ID: "owner-a"}}, service) validBody := `{"agentId":"agent-a","versionConstraint":"^1.0.0","acceptedPermissions":[]}` - request := httptest.NewRequest(http.MethodPost, "/v3/workspaces/workspace-a/installations", strings.NewReader(validBody)) + request := httptest.NewRequest(http.MethodPost, "/v1/workspaces/workspace-a/installations", strings.NewReader(validBody)) request.Header.Set("Authorization", "Bearer token") response := httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -451,7 +451,7 @@ func TestWorkspaceHandlerInstallRequiresPermissionArrayAndPreservesEmpty(t *test `{"agentId":"agent-a","versionConstraint":"^1.0.0","acceptedPermissions":"read"}`, } { callsBefore := service.installCalls - request = httptest.NewRequest(http.MethodPost, "/v3/workspaces/workspace-a/installations", strings.NewReader(body)) + request = httptest.NewRequest(http.MethodPost, "/v1/workspaces/workspace-a/installations", strings.NewReader(body)) request.Header.Set("Authorization", "Bearer token") response = httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -471,7 +471,7 @@ func TestWorkspaceHandlerInstallRequiresPermissionArrayAndPreservesEmpty(t *test {workspace.ErrDependency, http.StatusServiceUnavailable, "DEPENDENCY_ERROR"}, } { service.installErr = test.err - request = httptest.NewRequest(http.MethodPost, "/v3/workspaces/workspace-a/installations", strings.NewReader(validBody)) + request = httptest.NewRequest(http.MethodPost, "/v1/workspaces/workspace-a/installations", strings.NewReader(validBody)) request.Header.Set("Authorization", "Bearer token") response = httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -490,7 +490,7 @@ func TestWorkspaceHandlerMapsLifecycleSuccessAndFailures(t *testing.T) { }} handler := newWorkspaceTestHandler(t, workspaceTestAuthenticator{caller: catalog.AuthenticatedCaller{ID: "owner-a"}}, service) - request := httptest.NewRequest(http.MethodPatch, "/v3/workspaces/workspace-a/installations/installation-a", strings.NewReader(`{"status":"disabled"}`)) + request := httptest.NewRequest(http.MethodPatch, "/v1/workspaces/workspace-a/installations/installation-a", strings.NewReader(`{"status":"disabled"}`)) request.Header.Set("Authorization", "Bearer token") response := httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -505,7 +505,7 @@ func TestWorkspaceHandlerMapsLifecycleSuccessAndFailures(t *testing.T) { t.Fatalf("disable response = %#v", disabled) } - request = httptest.NewRequest(http.MethodDelete, "/v3/workspaces/workspace-a/installations/installation-a", nil) + request = httptest.NewRequest(http.MethodDelete, "/v1/workspaces/workspace-a/installations/installation-a", nil) request.Header.Set("Authorization", "Bearer token") response = httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -546,7 +546,7 @@ func TestWorkspaceHandlerMapsLifecycleSuccessAndFailures(t *testing.T) { } beforeUpdate := service.updateCalls beforeUninstall := service.uninstallCalls - request := httptest.NewRequest(test.method, "/v3/workspaces/workspace-a/installations/installation-a", strings.NewReader(test.body)) + request := httptest.NewRequest(test.method, "/v1/workspaces/workspace-a/installations/installation-a", strings.NewReader(test.body)) request.Header.Set("Authorization", "Bearer token") response := httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -563,7 +563,7 @@ func TestWorkspaceHandlerMapsLifecycleSuccessAndFailures(t *testing.T) { } unauthenticated := newWorkspaceTestHandler(t, workspaceTestAuthenticator{err: ErrUnauthenticated}, service) - request = httptest.NewRequest(http.MethodDelete, "/v3/workspaces/workspace-a/installations/installation-a", nil) + request = httptest.NewRequest(http.MethodDelete, "/v1/workspaces/workspace-a/installations/installation-a", nil) response = httptest.NewRecorder() unauthenticated.Routes().ServeHTTP(response, request) if response.Code != http.StatusUnauthorized || service.uninstallCalls != 2 { @@ -574,7 +574,7 @@ func TestWorkspaceHandlerMapsLifecycleSuccessAndFailures(t *testing.T) { func TestWorkspaceHandlerRejectsOversizedJSONBeforeService(t *testing.T) { service := &workspaceTestService{} handler := newWorkspaceTestHandler(t, workspaceTestAuthenticator{caller: catalog.AuthenticatedCaller{ID: "owner-a"}}, service) - request := httptest.NewRequest(http.MethodPost, "/v3/workspaces", strings.NewReader(strings.Repeat("x", contracts.WorkspaceRequestMaximumBodyBytes+1))) + request := httptest.NewRequest(http.MethodPost, "/v1/workspaces", strings.NewReader(strings.Repeat("x", contracts.WorkspaceRequestMaximumBodyBytes+1))) request.Header.Set("Authorization", "Bearer token") response := httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -589,7 +589,7 @@ func TestWorkspaceHandlerRejectsOversizedJSONBeforeService(t *testing.T) { func TestWorkspaceHandlerSeparatesPreAndPostCorrelationErrors(t *testing.T) { service := &workspaceTestService{resolveErr: workspace.ErrDependency} handler := newWorkspaceTestHandler(t, workspaceTestAuthenticator{caller: catalog.AuthenticatedCaller{ID: "owner-a"}}, service) - request := httptest.NewRequest(http.MethodPost, "/internal/v2/resolve-agent", strings.NewReader(`{"invocationId":"bad id"}`)) + request := httptest.NewRequest(http.MethodPost, "/internal/v1/resolve-agent", strings.NewReader(`{"invocationId":"bad id"}`)) request.Header.Set("Authorization", "Bearer internal") response := httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -604,7 +604,7 @@ func TestWorkspaceHandlerSeparatesPreAndPostCorrelationErrors(t *testing.T) { t.Fatalf("pre-correlation error leaked IDs: %#v", pre) } - request = httptest.NewRequest(http.MethodPost, "/internal/v2/resolve-agent", strings.NewReader(`{"invocationId":"inv-a","rootTaskId":"task-a","traceId":"trace-a","workspaceId":"workspace-a","agentId":"agent-a","version":"bad","capability":"capability-a"}`)) + request = httptest.NewRequest(http.MethodPost, "/internal/v1/resolve-agent", strings.NewReader(`{"invocationId":"inv-a","rootTaskId":"task-a","traceId":"trace-a","workspaceId":"workspace-a","agentId":"agent-a","version":"bad","capability":"capability-a"}`)) request.Header.Set("Authorization", "Bearer internal") response = httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -633,7 +633,7 @@ func TestResolveHandlerKeepsCorrelationForNonCorrelationValidationErrors(t *test t.Run(test.name, func(t *testing.T) { service := &workspaceTestService{} handler := newWorkspaceTestHandler(t, workspaceTestAuthenticator{caller: catalog.AuthenticatedCaller{ID: "router-a"}}, service) - request := httptest.NewRequest(http.MethodPost, "/internal/v2/resolve-agent", strings.NewReader(test.body)) + request := httptest.NewRequest(http.MethodPost, "/internal/v1/resolve-agent", strings.NewReader(test.body)) request.Header.Set("Authorization", "Bearer internal") response := httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -656,7 +656,7 @@ func TestResolveHandlerUsesSeparateInternalAuthentication(t *testing.T) { handler := newWorkspaceTestHandlerWithAuthenticators(t, workspaceTestAuthenticator{caller: catalog.AuthenticatedCaller{ID: "owner-a"}}, workspaceTestAuthenticator{err: ErrUnauthenticated}, service) - request := httptest.NewRequest(http.MethodPost, "/internal/v2/resolve-agent", strings.NewReader("{\"invocationId\":\"inv-a\",\"rootTaskId\":\"task-a\",\"traceId\":\"trace-a\",\"workspaceId\":\"workspace-a\",\"agentId\":\"agent-a\",\"version\":\"1.0.0\",\"capability\":\"capability-a\"}")) + request := httptest.NewRequest(http.MethodPost, "/internal/v1/resolve-agent", strings.NewReader("{\"invocationId\":\"inv-a\",\"rootTaskId\":\"task-a\",\"traceId\":\"trace-a\",\"workspaceId\":\"workspace-a\",\"agentId\":\"agent-a\",\"version\":\"1.0.0\",\"capability\":\"capability-a\"}")) request.Header.Set("Authorization", "Bearer northbound-token") response := httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -694,7 +694,7 @@ func TestResolveHandlerPreservesTypedFailureCorrelation(t *testing.T) { t.Run(test.name, func(t *testing.T) { service := &workspaceTestService{resolveErr: test.err} handler := newWorkspaceTestHandler(t, workspaceTestAuthenticator{caller: catalog.AuthenticatedCaller{ID: "router-a"}}, service) - request := httptest.NewRequest(http.MethodPost, "/internal/v2/resolve-agent", strings.NewReader("{\"invocationId\":\"inv-a\",\"rootTaskId\":\"task-a\",\"traceId\":\"trace-a\",\"workspaceId\":\"workspace-a\",\"agentId\":\"agent-a\",\"version\":\"1.0.0\",\"capability\":\"capability-a\"}")) + request := httptest.NewRequest(http.MethodPost, "/internal/v1/resolve-agent", strings.NewReader("{\"invocationId\":\"inv-a\",\"rootTaskId\":\"task-a\",\"traceId\":\"trace-a\",\"workspaceId\":\"workspace-a\",\"agentId\":\"agent-a\",\"version\":\"1.0.0\",\"capability\":\"capability-a\"}")) request.Header.Set("Authorization", "Bearer internal") response := httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -718,7 +718,7 @@ func TestResolveHandlerReturnsOnlyResolutionContractFields(t *testing.T) { Installation: contracts.ResolvedInstallation{InstallationID: "installation-a", WorkspaceID: "workspace-a", AgentID: "agent-a", InstalledVersion: "1.0.0", AcceptedPermissions: []string{"read"}, Status: "enabled"}, }} handler := newWorkspaceTestHandler(t, workspaceTestAuthenticator{caller: catalog.AuthenticatedCaller{ID: "router-a"}}, service) - request := httptest.NewRequest(http.MethodPost, "/internal/v2/resolve-agent", strings.NewReader("{\"invocationId\":\"inv-a\",\"rootTaskId\":\"task-a\",\"traceId\":\"trace-a\",\"workspaceId\":\"workspace-a\",\"agentId\":\"agent-a\",\"version\":\"1.0.0\",\"capability\":\"capability-a\"}")) + request := httptest.NewRequest(http.MethodPost, "/internal/v1/resolve-agent", strings.NewReader("{\"invocationId\":\"inv-a\",\"rootTaskId\":\"task-a\",\"traceId\":\"trace-a\",\"workspaceId\":\"workspace-a\",\"agentId\":\"agent-a\",\"version\":\"1.0.0\",\"capability\":\"capability-a\"}")) request.Header.Set("Authorization", "Bearer internal") response := httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -737,7 +737,7 @@ func TestResolveHandlerReturnsOnlyResolutionContractFields(t *testing.T) { func TestWorkspaceHandlerMapsUnexpectedErrorsToInternalServerError(t *testing.T) { service := &workspaceTestService{resolveErr: errors.New("unexpected service failure")} handler := newWorkspaceTestHandler(t, workspaceTestAuthenticator{caller: catalog.AuthenticatedCaller{ID: "router-a"}}, service) - request := httptest.NewRequest(http.MethodPost, "/internal/v2/resolve-agent", strings.NewReader(`{"invocationId":"inv-a","rootTaskId":"task-a","traceId":"trace-a","workspaceId":"workspace-a","agentId":"agent-a","version":"1.0.0","capability":"capability-a"}`)) + request := httptest.NewRequest(http.MethodPost, "/internal/v1/resolve-agent", strings.NewReader(`{"invocationId":"inv-a","rootTaskId":"task-a","traceId":"trace-a","workspaceId":"workspace-a","agentId":"agent-a","version":"1.0.0","capability":"capability-a"}`)) request.Header.Set("Authorization", "Bearer internal") response := httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -756,7 +756,7 @@ func TestWorkspaceHandlerMapsUnexpectedErrorsToInternalServerError(t *testing.T) func TestWorkspaceHandlerResolvesInstalledVersionThroughAuthenticatedV3Boundary(t *testing.T) { service := &workspaceTestService{versionResponse: contracts.ResolveInstalledVersionResponse{Version: "1.4.2"}} handler := newWorkspaceTestHandler(t, workspaceTestAuthenticator{caller: catalog.AuthenticatedCaller{ID: "router-a"}}, service) - request := httptest.NewRequest(http.MethodPost, "/internal/v3/resolve-installed-version", strings.NewReader(`{"invocationId":"inv-child","rootTaskId":"task-root","traceId":"trace-root","workspaceId":"workspace-a","agentId":"runtime-b","capability":"runtime.echo"}`)) + request := httptest.NewRequest(http.MethodPost, "/internal/v1/resolve-installed-version", strings.NewReader(`{"invocationId":"inv-child","rootTaskId":"task-root","traceId":"trace-root","workspaceId":"workspace-a","agentId":"runtime-b","capability":"runtime.echo"}`)) request.Header.Set("Authorization", "Bearer internal") response := httptest.NewRecorder() handler.Routes().ServeHTTP(response, request) @@ -784,7 +784,7 @@ func TestWorkspaceHandlerResolvesInstalledVersionThroughAuthenticatedV3Boundary( } func requestForInstalledVersion() *http.Request { - request := httptest.NewRequest(http.MethodPost, "/internal/v3/resolve-installed-version", strings.NewReader(`{"invocationId":"inv-child","rootTaskId":"task-root","traceId":"trace-root","workspaceId":"workspace-a","agentId":"runtime-b","capability":"runtime.echo"}`)) + request := httptest.NewRequest(http.MethodPost, "/internal/v1/resolve-installed-version", strings.NewReader(`{"invocationId":"inv-child","rootTaskId":"task-root","traceId":"trace-root","workspaceId":"workspace-a","agentId":"runtime-b","capability":"runtime.echo"}`)) request.Header.Set("Authorization", "Bearer internal") return request } diff --git a/apps/control-plane/internal/invocation/router_client.go b/apps/control-plane/internal/invocation/router_client.go index 68ab7f3c..c34ecee1 100644 --- a/apps/control-plane/internal/invocation/router_client.go +++ b/apps/control-plane/internal/invocation/router_client.go @@ -39,7 +39,7 @@ func NewRouterClient(doer HTTPDoer, url, token string) (*RouterClient, error) { return &RouterClient{doer: doer, url: url, token: token}, nil } -func (client *RouterClient) Dispatch(ctx context.Context, value contracts.DispatchInvocationRequestV4, mode contracts.InvocationResultMode) (*RouterResponse, error) { +func (client *RouterClient) Dispatch(ctx context.Context, value contracts.DispatchInvocationRequestV1, mode contracts.InvocationResultMode) (*RouterResponse, error) { var body bytes.Buffer encoder := json.NewEncoder(&body) encoder.SetEscapeHTML(false) @@ -91,12 +91,12 @@ func (client *RouterClient) Dispatch(ctx context.Context, value contracts.Dispat // GetInvocation reads one Workspace-scoped metadata projection from the same // explicitly configured Router origin as dispatch. The path is fixed by the -// active Router Internal v3 contract and never comes from the caller. +// active Router Internal v1 contract and never comes from the caller. func (client *RouterClient) GetInvocation(ctx context.Context, workspaceID, invocationID string) (*RouterResponse, error) { if !validReadIdentifier(workspaceID) || !validReadIdentifier(invocationID) { return nil, errors.New("Router Invocation read identifiers are invalid") } - return client.getMetadata(ctx, "/internal/v3/workspaces/"+workspaceID+"/invocations/"+invocationID) + return client.getMetadata(ctx, "/internal/v1/workspaces/"+workspaceID+"/invocations/"+invocationID) } // GetTrace reads one Workspace-scoped metadata lineage from the same Router @@ -108,7 +108,7 @@ func (client *RouterClient) GetTrace(ctx context.Context, workspaceID string, tr if _, err := contracts.ParseTraceID(string(traceID)); err != nil { return nil, err } - return client.getMetadata(ctx, "/internal/v3/workspaces/"+workspaceID+"/traces/"+string(traceID)) + return client.getMetadata(ctx, "/internal/v1/workspaces/"+workspaceID+"/traces/"+string(traceID)) } func (client *RouterClient) getMetadata(ctx context.Context, path string) (*RouterResponse, error) { diff --git a/apps/control-plane/internal/invocation/router_client_test.go b/apps/control-plane/internal/invocation/router_client_test.go index b8f7ba7b..f5712367 100644 --- a/apps/control-plane/internal/invocation/router_client_test.go +++ b/apps/control-plane/internal/invocation/router_client_test.go @@ -14,9 +14,9 @@ import ( ) func TestRouterClientUsesOnlyFrozenInternalV3Direction(t *testing.T) { - var received contracts.DispatchInvocationRequestV4 + var received contracts.DispatchInvocationRequestV1 server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { - if request.URL.Path != "/internal/v4/invocations" || request.Method != http.MethodPost || request.Header.Get("Authorization") != "Bearer service-secret" || request.Header.Get("Content-Type") != "application/json" || request.Header.Get("Accept") != "text/event-stream" { + if request.URL.Path != "/internal/v1/invocations" || request.Method != http.MethodPost || request.Header.Get("Authorization") != "Bearer service-secret" || request.Header.Get("Content-Type") != "application/json" || request.Header.Get("Accept") != "text/event-stream" { t.Errorf("unexpected Router request: %s %s %#v", request.Method, request.URL.Path, request.Header) } if err := json.NewDecoder(request.Body).Decode(&received); err != nil { @@ -27,12 +27,12 @@ func TestRouterClientUsesOnlyFrozenInternalV3Direction(t *testing.T) { _, _ = io.WriteString(writer, "data: {}\n\n") })) defer server.Close() - client, err := NewRouterClient(server.Client(), server.URL+"/internal/v4/invocations", "service-secret") + client, err := NewRouterClient(server.Client(), server.URL+"/internal/v1/invocations", "service-secret") if err != nil { t.Fatal(err) } digest := strings.Repeat("a", 64) - request := contracts.DispatchInvocationRequestV4{InvocationID: "inv-a", RootTaskID: "task-a", TraceID: "trace-a", Caller: contracts.Caller{Type: "user", ID: "owner-a"}, WorkspaceID: "workspace-a", TargetAgentID: "agent-a", AgentCardVersion: "1.0.0", AgentReleaseID: "release-a", AgentCardDigest: digest, Capability: "capability-a", Input: []byte(`{}`), Stream: true} + request := contracts.DispatchInvocationRequestV1{InvocationID: "inv-a", RootTaskID: "task-a", TraceID: "trace-a", Caller: contracts.Caller{Type: "user", ID: "owner-a"}, WorkspaceID: "workspace-a", TargetAgentID: "agent-a", AgentCardVersion: "1.0.0", AgentReleaseID: "release-a", AgentCardDigest: digest, Capability: "capability-a", Input: []byte(`{}`), Stream: true} response, err := client.Dispatch(context.Background(), request, contracts.InvocationResultModeSSE) if err != nil { t.Fatal(err) @@ -63,11 +63,11 @@ func TestRouterClientRequiresOneMatchingTraceAndClosesRejectedBodies(t *testing. body := &trackedReadCloser{Reader: strings.NewReader(`{}`)} client, err := NewRouterClient(roundTripFunc(func(*http.Request) (*http.Response, error) { return test.response(body), nil - }), "https://router.example/internal/v4/invocations", "service-secret") + }), "https://router.example/internal/v1/invocations", "service-secret") if err != nil { t.Fatal(err) } - request := contracts.DispatchInvocationRequestV4{TraceID: "trace-a"} + request := contracts.DispatchInvocationRequestV1{TraceID: "trace-a"} if _, err := client.Dispatch(context.Background(), request, contracts.InvocationResultModeJSON); err == nil { t.Fatal("invalid Router response was accepted") } @@ -105,7 +105,7 @@ func TestRouterClientRejectsWrongResultMediaWithoutFallback(t *testing.T) { })) defer server.Close() client, _ := NewRouterClient(server.Client(), server.URL, "service-secret") - if _, err := client.Dispatch(context.Background(), contracts.DispatchInvocationRequestV4{}, contracts.InvocationResultModeSSE); err == nil { + if _, err := client.Dispatch(context.Background(), contracts.DispatchInvocationRequestV1{}, contracts.InvocationResultModeSSE); err == nil { t.Fatal("wrong Router result media was accepted") } } @@ -115,12 +115,12 @@ func TestRouterClientReadsExactV3MetadataPathsOnSameOrigin(t *testing.T) { if request.Method != http.MethodGet || request.Header.Get("Authorization") != "Bearer service-secret" || request.Header.Get("Accept") != "application/json" { t.Errorf("unexpected metadata request: %s %s %#v", request.Method, request.URL.Path, request.Header) } - if request.URL.Path == "/internal/v3/workspaces/workspace-a/invocations/inv-a" { + if request.URL.Path == "/internal/v1/workspaces/workspace-a/invocations/inv-a" { writer.Header().Set("Content-Type", "application/json") _, _ = io.WriteString(writer, `{"invocation":{"invocationId":"inv-a"},"events":[]}`) return } - if request.URL.Path == "/internal/v3/workspaces/workspace-a/traces/trace-a" { + if request.URL.Path == "/internal/v1/workspaces/workspace-a/traces/trace-a" { writer.Header().Set("Content-Type", "application/json") _, _ = io.WriteString(writer, `{"traceId":"trace-a","invocations":[]}`) return @@ -128,7 +128,7 @@ func TestRouterClientReadsExactV3MetadataPathsOnSameOrigin(t *testing.T) { http.NotFound(writer, request) })) defer server.Close() - client, err := NewRouterClient(server.Client(), server.URL+"/internal/v4/invocations", "service-secret") + client, err := NewRouterClient(server.Client(), server.URL+"/internal/v1/invocations", "service-secret") if err != nil { t.Fatal(err) } @@ -153,7 +153,7 @@ func TestRouterClientReadsExactV3MetadataPathsOnSameOrigin(t *testing.T) { func TestRouterClientRejectsInvalidMetadataIdentifiersWithoutRequest(t *testing.T) { client, err := NewRouterClient(roundTripFunc(func(*http.Request) (*http.Response, error) { return nil, errors.New("request must not be made") - }), "https://router.example/internal/v4/invocations", "service-secret") + }), "https://router.example/internal/v1/invocations", "service-secret") if err != nil { t.Fatal(err) } diff --git a/apps/control-plane/internal/invocation/service.go b/apps/control-plane/internal/invocation/service.go index 02ce7599..7250d91d 100644 --- a/apps/control-plane/internal/invocation/service.go +++ b/apps/control-plane/internal/invocation/service.go @@ -17,7 +17,7 @@ type Authorizer interface { } type Router interface { - Dispatch(context.Context, contracts.DispatchInvocationRequestV4, contracts.InvocationResultMode) (*RouterResponse, error) + Dispatch(context.Context, contracts.DispatchInvocationRequestV1, contracts.InvocationResultMode) (*RouterResponse, error) } type IDGenerator interface { @@ -64,7 +64,7 @@ func (service *Service) Dispatch( if err != nil { return nil, &DispatchError{Code: contracts.ErrorCodeInternal, Cause: fmt.Errorf("generate root invocation correlation: %w", err)} } - dispatch := contracts.DispatchInvocationRequestV4{ + dispatch := contracts.DispatchInvocationRequestV1{ InvocationID: invocationID, RootTaskID: rootTaskID, TraceID: traceID, diff --git a/apps/control-plane/internal/invocation/service_test.go b/apps/control-plane/internal/invocation/service_test.go index f747d642..43eb6956 100644 --- a/apps/control-plane/internal/invocation/service_test.go +++ b/apps/control-plane/internal/invocation/service_test.go @@ -23,14 +23,14 @@ func (stub *authorizerStub) AuthorizeInvocation(context.Context, workspace.Authe } type routerStub struct { - request contracts.DispatchInvocationRequestV4 + request contracts.DispatchInvocationRequestV1 mode contracts.InvocationResultMode result *RouterResponse err error calls int } -func (stub *routerStub) Dispatch(_ context.Context, request contracts.DispatchInvocationRequestV4, mode contracts.InvocationResultMode) (*RouterResponse, error) { +func (stub *routerStub) Dispatch(_ context.Context, request contracts.DispatchInvocationRequestV1, mode contracts.InvocationResultMode) (*RouterResponse, error) { stub.calls++ stub.request, stub.mode = request, mode return stub.result, stub.err diff --git a/apps/control-plane/internal/workspace/integration/acceptance_http_test.go b/apps/control-plane/internal/workspace/integration/acceptance_http_test.go index 3a3592c1..7d6bde0b 100644 --- a/apps/control-plane/internal/workspace/integration/acceptance_http_test.go +++ b/apps/control-plane/internal/workspace/integration/acceptance_http_test.go @@ -250,7 +250,7 @@ func requireAcceptanceError(t *testing.T, harness *acceptanceHTTPHarness, respon func TestAcceptanceWorkspaceControlPlaneHTTPWorkflow(t *testing.T) { harness := newAcceptanceHTTPHarness(t) - searchResponse := harness.request(t, http.MethodGet, "/v3/agents?capability=document.read", harness.ownerToken, nil) + searchResponse := harness.request(t, http.MethodGet, "/v1/agents?capability=document.read", harness.ownerToken, nil) if searchResponse.Code != http.StatusOK { t.Fatalf("discover status=%d body=%s", searchResponse.Code, searchResponse.Body.String()) } @@ -261,7 +261,7 @@ func TestAcceptanceWorkspaceControlPlaneHTTPWorkflow(t *testing.T) { t.Fatalf("discover response = %#v", search) } - createResponse := harness.request(t, http.MethodPost, "/v3/workspaces", harness.ownerToken, contracts.CreateWorkspaceRequest{WorkspaceID: "acceptance-workspace"}) + createResponse := harness.request(t, http.MethodPost, "/v1/workspaces", harness.ownerToken, contracts.CreateWorkspaceRequest{WorkspaceID: "acceptance-workspace"}) if createResponse.Code != http.StatusCreated { t.Fatalf("create status=%d body=%s", createResponse.Code, createResponse.Body.String()) } @@ -272,7 +272,7 @@ func TestAcceptanceWorkspaceControlPlaneHTTPWorkflow(t *testing.T) { t.Fatalf("created Workspace = %#v", created) } - installResponse := harness.request(t, http.MethodPost, "/v3/workspaces/acceptance-workspace/installations", harness.ownerToken, contracts.InstallAgentRequest{ + installResponse := harness.request(t, http.MethodPost, "/v1/workspaces/acceptance-workspace/installations", harness.ownerToken, contracts.InstallAgentRequest{ AgentID: "runtime-a", VersionConstraint: "^1.0.0", AcceptedPermissions: []string{"document.read"}, }) if installResponse.Code != http.StatusCreated { @@ -285,7 +285,7 @@ func TestAcceptanceWorkspaceControlPlaneHTTPWorkflow(t *testing.T) { t.Fatalf("installed = %#v", installed) } - listPath := "/v3/workspaces/acceptance-workspace/installations?limit=25" + listPath := "/v1/workspaces/acceptance-workspace/installations?limit=25" listResponse := harness.request(t, http.MethodGet, listPath, harness.ownerToken, nil) if listResponse.Code != http.StatusOK { t.Fatalf("list status=%d body=%s", listResponse.Code, listResponse.Body.String()) @@ -297,7 +297,7 @@ func TestAcceptanceWorkspaceControlPlaneHTTPWorkflow(t *testing.T) { t.Fatalf("listed = %#v, installed = %#v", listed, installed) } - detailResponse := harness.request(t, http.MethodGet, "/v3/workspaces/acceptance-workspace/installations/"+installed.InstallationID, harness.ownerToken, nil) + detailResponse := harness.request(t, http.MethodGet, "/v1/workspaces/acceptance-workspace/installations/"+installed.InstallationID, harness.ownerToken, nil) if detailResponse.Code != http.StatusOK { t.Fatalf("detail status=%d body=%s", detailResponse.Code, detailResponse.Body.String()) } @@ -316,7 +316,7 @@ func TestAcceptanceWorkspaceControlPlaneHTTPWorkflow(t *testing.T) { if err := registerLegacyPublishedCard(context.Background(), harness.pool, harness.catalog, card); err != nil { t.Fatalf("publish %s: %v", agentID, err) } - response := harness.request(t, http.MethodPost, "/v3/workspaces/acceptance-workspace/installations", harness.ownerToken, contracts.InstallAgentRequest{ + response := harness.request(t, http.MethodPost, "/v1/workspaces/acceptance-workspace/installations", harness.ownerToken, contracts.InstallAgentRequest{ AgentID: agentID, VersionConstraint: "^1.0.0", AcceptedPermissions: []string{"document.read"}, }) if response.Code != http.StatusCreated { @@ -328,7 +328,7 @@ func TestAcceptanceWorkspaceControlPlaneHTTPWorkflow(t *testing.T) { seen := make(map[string]struct{}) cursor := "" for { - path := "/v3/workspaces/acceptance-workspace/installations?limit=1" + path := "/v1/workspaces/acceptance-workspace/installations?limit=1" if cursor != "" { path += "&cursor=" + url.QueryEscape(cursor) } @@ -357,7 +357,7 @@ func TestAcceptanceWorkspaceControlPlaneHTTPWorkflow(t *testing.T) { t.Fatalf("paged list returned %d unique Installations, want 3", len(seen)) } - disabledResponse := harness.request(t, http.MethodPatch, "/v3/workspaces/acceptance-workspace/installations/"+installed.InstallationID, harness.ownerToken, contracts.UpdateInstallationRequest{Status: "disabled"}) + disabledResponse := harness.request(t, http.MethodPatch, "/v1/workspaces/acceptance-workspace/installations/"+installed.InstallationID, harness.ownerToken, contracts.UpdateInstallationRequest{Status: "disabled"}) if disabledResponse.Code != http.StatusOK { t.Fatalf("disable status=%d body=%s", disabledResponse.Code, disabledResponse.Body.String()) } @@ -368,7 +368,7 @@ func TestAcceptanceWorkspaceControlPlaneHTTPWorkflow(t *testing.T) { t.Fatalf("disabled = %#v, installed = %#v", disabled, installed) } - enabledResponse := harness.request(t, http.MethodPatch, "/v3/workspaces/acceptance-workspace/installations/"+installed.InstallationID, harness.ownerToken, contracts.UpdateInstallationRequest{Status: "enabled"}) + enabledResponse := harness.request(t, http.MethodPatch, "/v1/workspaces/acceptance-workspace/installations/"+installed.InstallationID, harness.ownerToken, contracts.UpdateInstallationRequest{Status: "enabled"}) if enabledResponse.Code != http.StatusOK { t.Fatalf("enable status=%d body=%s", enabledResponse.Code, enabledResponse.Body.String()) } @@ -383,7 +383,7 @@ func TestAcceptanceWorkspaceControlPlaneHTTPWorkflow(t *testing.T) { InvocationID: "invocation-acceptance", RootTaskID: "root-task-acceptance", TraceID: "trace-acceptance", WorkspaceID: "acceptance-workspace", AgentID: "runtime-a", Version: "1.0.0", Capability: "document.read", } - resolveResponse := harness.request(t, http.MethodPost, "/internal/v2/resolve-agent", harness.internalToken, resolveRequest) + resolveResponse := harness.request(t, http.MethodPost, "/internal/v1/resolve-agent", harness.internalToken, resolveRequest) if resolveResponse.Code != http.StatusOK { t.Fatalf("resolve status=%d body=%s", resolveResponse.Code, resolveResponse.Body.String()) } @@ -396,14 +396,14 @@ func TestAcceptanceWorkspaceControlPlaneHTTPWorkflow(t *testing.T) { t.Fatalf("resolved = %#v", resolved) } - disabledAgainResponse := harness.request(t, http.MethodPatch, "/v3/workspaces/acceptance-workspace/installations/"+installed.InstallationID, harness.ownerToken, contracts.UpdateInstallationRequest{Status: "disabled"}) + disabledAgainResponse := harness.request(t, http.MethodPatch, "/v1/workspaces/acceptance-workspace/installations/"+installed.InstallationID, harness.ownerToken, contracts.UpdateInstallationRequest{Status: "disabled"}) if disabledAgainResponse.Code != http.StatusOK { t.Fatalf("disable before uninstall status=%d body=%s", disabledAgainResponse.Code, disabledAgainResponse.Body.String()) } requireAcceptanceTrace(t, disabledAgainResponse) var disabledAgain contracts.Installation decodeAcceptanceJSON(t, disabledAgainResponse, &disabledAgain) - uninstallResponse := harness.request(t, http.MethodDelete, "/v3/workspaces/acceptance-workspace/installations/"+installed.InstallationID, harness.ownerToken, nil) + uninstallResponse := harness.request(t, http.MethodDelete, "/v1/workspaces/acceptance-workspace/installations/"+installed.InstallationID, harness.ownerToken, nil) if uninstallResponse.Code != http.StatusOK { t.Fatalf("uninstall status=%d body=%s", uninstallResponse.Code, uninstallResponse.Body.String()) } @@ -414,7 +414,7 @@ func TestAcceptanceWorkspaceControlPlaneHTTPWorkflow(t *testing.T) { t.Fatalf("terminal = %#v, installed = %#v", terminal, installed) } - reinstallResponse := harness.request(t, http.MethodPost, "/v3/workspaces/acceptance-workspace/installations", harness.ownerToken, contracts.InstallAgentRequest{ + reinstallResponse := harness.request(t, http.MethodPost, "/v1/workspaces/acceptance-workspace/installations", harness.ownerToken, contracts.InstallAgentRequest{ AgentID: "runtime-a", VersionConstraint: "^1.0.0", AcceptedPermissions: []string{"document.read"}, }) if reinstallResponse.Code != http.StatusCreated { @@ -427,7 +427,7 @@ func TestAcceptanceWorkspaceControlPlaneHTTPWorkflow(t *testing.T) { t.Fatalf("reinstalled = %#v, installed = %#v", reinstalled, installed) } - terminalDetail := harness.request(t, http.MethodGet, "/v3/workspaces/acceptance-workspace/installations/"+installed.InstallationID, harness.ownerToken, nil) + terminalDetail := harness.request(t, http.MethodGet, "/v1/workspaces/acceptance-workspace/installations/"+installed.InstallationID, harness.ownerToken, nil) if terminalDetail.Code != http.StatusOK { t.Fatalf("terminal detail status=%d body=%s", terminalDetail.Code, terminalDetail.Body.String()) } @@ -444,12 +444,12 @@ func TestAcceptanceWorkspaceControlPlaneHTTPWorkflow(t *testing.T) { func TestAcceptanceHTTPFailureBoundaries(t *testing.T) { harness := newAcceptanceHTTPHarness(t) - createResponse := harness.request(t, http.MethodPost, "/v3/workspaces", harness.ownerToken, contracts.CreateWorkspaceRequest{WorkspaceID: "acceptance-errors"}) + createResponse := harness.request(t, http.MethodPost, "/v1/workspaces", harness.ownerToken, contracts.CreateWorkspaceRequest{WorkspaceID: "acceptance-errors"}) if createResponse.Code != http.StatusCreated { t.Fatalf("create error fixture status=%d body=%s", createResponse.Code, createResponse.Body.String()) } requireAcceptanceTrace(t, createResponse) - installResponse := harness.request(t, http.MethodPost, "/v3/workspaces/acceptance-errors/installations", harness.ownerToken, contracts.InstallAgentRequest{ + installResponse := harness.request(t, http.MethodPost, "/v1/workspaces/acceptance-errors/installations", harness.ownerToken, contracts.InstallAgentRequest{ AgentID: "runtime-a", VersionConstraint: "^1.0.0", AcceptedPermissions: []string{"document.read"}, }) if installResponse.Code != http.StatusCreated { @@ -458,23 +458,23 @@ func TestAcceptanceHTTPFailureBoundaries(t *testing.T) { requireAcceptanceTrace(t, installResponse) var installed contracts.Installation decodeAcceptanceJSON(t, installResponse, &installed) - otherWorkspaceResponse := harness.request(t, http.MethodPost, "/v3/workspaces", harness.ownerToken, contracts.CreateWorkspaceRequest{WorkspaceID: "acceptance-other"}) + otherWorkspaceResponse := harness.request(t, http.MethodPost, "/v1/workspaces", harness.ownerToken, contracts.CreateWorkspaceRequest{WorkspaceID: "acceptance-other"}) if otherWorkspaceResponse.Code != http.StatusCreated { t.Fatalf("create wrong-workspace fixture status=%d body=%s", otherWorkspaceResponse.Code, otherWorkspaceResponse.Body.String()) } requireAcceptanceTrace(t, otherWorkspaceResponse) - requireAcceptanceError(t, harness, harness.request(t, http.MethodGet, "/v3/workspaces/acceptance-errors", "", nil), http.StatusUnauthorized, contracts.ErrorCodeUnauthenticated) - requireAcceptanceError(t, harness, harness.requestWithAuthorization(t, http.MethodGet, "/v3/workspaces/acceptance-errors", "Basic "+harness.ownerToken, nil), http.StatusUnauthorized, contracts.ErrorCodeUnauthenticated) - requireAcceptanceError(t, harness, harness.request(t, http.MethodGet, "/v3/workspaces/acceptance-errors/installations/"+installed.InstallationID, harness.otherToken, nil), http.StatusForbidden, contracts.ErrorCodeForbidden) - requireAcceptanceError(t, harness, harness.request(t, http.MethodPatch, "/v3/workspaces/acceptance-errors/installations/"+installed.InstallationID, harness.otherToken, contracts.UpdateInstallationRequest{Status: "disabled"}), http.StatusForbidden, contracts.ErrorCodeForbidden) - requireAcceptanceError(t, harness, harness.request(t, http.MethodGet, "/v3/workspaces/missing-workspace", harness.ownerToken, nil), http.StatusNotFound, contracts.ErrorCodeNotFound) - requireAcceptanceError(t, harness, harness.request(t, http.MethodGet, "/v3/workspaces/acceptance-other/installations/"+installed.InstallationID, harness.ownerToken, nil), http.StatusNotFound, contracts.ErrorCodeNotFound) - requireAcceptanceError(t, harness, harness.request(t, http.MethodPatch, "/v3/workspaces/acceptance-errors/installations/"+installed.InstallationID, harness.ownerToken, map[string]any{"status": "disabled", "unexpected": true}), http.StatusBadRequest, contracts.ErrorCodeValidationError) - requireAcceptanceError(t, harness, harness.request(t, http.MethodPost, "/v3/workspaces/acceptance-errors/installations", harness.ownerToken, map[string]any{"agentId": "runtime-a", "versionConstraint": "^1.0.0"}), http.StatusBadRequest, contracts.ErrorCodeValidationError) - requireAcceptanceError(t, harness, harness.request(t, http.MethodPatch, "/v3/workspaces/acceptance-errors/installations/"+installed.InstallationID, harness.ownerToken, contracts.UpdateInstallationRequest{Status: "enabled"}), http.StatusConflict, contracts.ErrorCodeConflict) - - unchangedResponse := harness.request(t, http.MethodGet, "/v3/workspaces/acceptance-errors/installations/"+installed.InstallationID, harness.ownerToken, nil) + requireAcceptanceError(t, harness, harness.request(t, http.MethodGet, "/v1/workspaces/acceptance-errors", "", nil), http.StatusUnauthorized, contracts.ErrorCodeUnauthenticated) + requireAcceptanceError(t, harness, harness.requestWithAuthorization(t, http.MethodGet, "/v1/workspaces/acceptance-errors", "Basic "+harness.ownerToken, nil), http.StatusUnauthorized, contracts.ErrorCodeUnauthenticated) + requireAcceptanceError(t, harness, harness.request(t, http.MethodGet, "/v1/workspaces/acceptance-errors/installations/"+installed.InstallationID, harness.otherToken, nil), http.StatusForbidden, contracts.ErrorCodeForbidden) + requireAcceptanceError(t, harness, harness.request(t, http.MethodPatch, "/v1/workspaces/acceptance-errors/installations/"+installed.InstallationID, harness.otherToken, contracts.UpdateInstallationRequest{Status: "disabled"}), http.StatusForbidden, contracts.ErrorCodeForbidden) + requireAcceptanceError(t, harness, harness.request(t, http.MethodGet, "/v1/workspaces/missing-workspace", harness.ownerToken, nil), http.StatusNotFound, contracts.ErrorCodeNotFound) + requireAcceptanceError(t, harness, harness.request(t, http.MethodGet, "/v1/workspaces/acceptance-other/installations/"+installed.InstallationID, harness.ownerToken, nil), http.StatusNotFound, contracts.ErrorCodeNotFound) + requireAcceptanceError(t, harness, harness.request(t, http.MethodPatch, "/v1/workspaces/acceptance-errors/installations/"+installed.InstallationID, harness.ownerToken, map[string]any{"status": "disabled", "unexpected": true}), http.StatusBadRequest, contracts.ErrorCodeValidationError) + requireAcceptanceError(t, harness, harness.request(t, http.MethodPost, "/v1/workspaces/acceptance-errors/installations", harness.ownerToken, map[string]any{"agentId": "runtime-a", "versionConstraint": "^1.0.0"}), http.StatusBadRequest, contracts.ErrorCodeValidationError) + requireAcceptanceError(t, harness, harness.request(t, http.MethodPatch, "/v1/workspaces/acceptance-errors/installations/"+installed.InstallationID, harness.ownerToken, contracts.UpdateInstallationRequest{Status: "enabled"}), http.StatusConflict, contracts.ErrorCodeConflict) + + unchangedResponse := harness.request(t, http.MethodGet, "/v1/workspaces/acceptance-errors/installations/"+installed.InstallationID, harness.ownerToken, nil) if unchangedResponse.Code != http.StatusOK { t.Fatalf("read Installation after rejected requests status=%d body=%s", unchangedResponse.Code, unchangedResponse.Body.String()) } @@ -489,37 +489,37 @@ func TestAcceptanceHTTPFailureBoundaries(t *testing.T) { InvocationID: "invocation-errors", RootTaskID: "root-task-errors", TraceID: "trace-errors", WorkspaceID: "acceptance-errors", AgentID: "runtime-a", Version: "1.0.0", Capability: "document.read", } - requireAcceptanceError(t, harness, harness.request(t, http.MethodPost, "/internal/v2/resolve-agent", "", resolveRequest), http.StatusUnauthorized, contracts.ErrorCodeUnauthenticated) + requireAcceptanceError(t, harness, harness.request(t, http.MethodPost, "/internal/v1/resolve-agent", "", resolveRequest), http.StatusUnauthorized, contracts.ErrorCodeUnauthenticated) - disabledResponse := harness.request(t, http.MethodPatch, "/v3/workspaces/acceptance-errors/installations/"+installed.InstallationID, harness.ownerToken, contracts.UpdateInstallationRequest{Status: "disabled"}) + disabledResponse := harness.request(t, http.MethodPatch, "/v1/workspaces/acceptance-errors/installations/"+installed.InstallationID, harness.ownerToken, contracts.UpdateInstallationRequest{Status: "disabled"}) if disabledResponse.Code != http.StatusOK { t.Fatalf("disable error fixture status=%d body=%s", disabledResponse.Code, disabledResponse.Body.String()) } requireAcceptanceTrace(t, disabledResponse) - disabledError := requireAcceptanceError(t, harness, harness.request(t, http.MethodPost, "/internal/v2/resolve-agent", harness.internalToken, resolveRequest), http.StatusForbidden, contracts.ErrorCodeInstallationDisabled) + disabledError := requireAcceptanceError(t, harness, harness.request(t, http.MethodPost, "/internal/v1/resolve-agent", harness.internalToken, resolveRequest), http.StatusForbidden, contracts.ErrorCodeInstallationDisabled) if disabledError.InvocationID != resolveRequest.InvocationID || disabledError.RootTaskID != resolveRequest.RootTaskID || disabledError.TraceID != resolveRequest.TraceID { t.Fatalf("disabled correlated error = %#v", disabledError) } - enabledResponse := harness.request(t, http.MethodPatch, "/v3/workspaces/acceptance-errors/installations/"+installed.InstallationID, harness.ownerToken, contracts.UpdateInstallationRequest{Status: "enabled"}) + enabledResponse := harness.request(t, http.MethodPatch, "/v1/workspaces/acceptance-errors/installations/"+installed.InstallationID, harness.ownerToken, contracts.UpdateInstallationRequest{Status: "enabled"}) if enabledResponse.Code != http.StatusOK { t.Fatalf("enable error fixture status=%d body=%s", enabledResponse.Code, enabledResponse.Body.String()) } requireAcceptanceTrace(t, enabledResponse) unknownCapability := resolveRequest unknownCapability.Capability = "document.write" - requireAcceptanceError(t, harness, harness.request(t, http.MethodPost, "/internal/v2/resolve-agent", harness.internalToken, unknownCapability), http.StatusForbidden, contracts.ErrorCodeCapabilityNotAllowed) + requireAcceptanceError(t, harness, harness.request(t, http.MethodPost, "/internal/v1/resolve-agent", harness.internalToken, unknownCapability), http.StatusForbidden, contracts.ErrorCodeCapabilityNotAllowed) - catalogDisableResponse := harness.request(t, http.MethodPost, "/v3/agents/runtime-a/versions/1.0.0/disable", harness.ownerToken, nil) + catalogDisableResponse := harness.request(t, http.MethodPost, "/v1/agents/runtime-a/versions/1.0.0/disable", harness.ownerToken, nil) if catalogDisableResponse.Code != http.StatusOK { t.Fatalf("Catalog disable status=%d body=%s", catalogDisableResponse.Code, catalogDisableResponse.Body.String()) } requireAcceptanceTrace(t, catalogDisableResponse) - requireAcceptanceError(t, harness, harness.request(t, http.MethodPost, "/internal/v2/resolve-agent", harness.internalToken, resolveRequest), http.StatusForbidden, contracts.ErrorCodeAgentDisabled) + requireAcceptanceError(t, harness, harness.request(t, http.MethodPost, "/internal/v1/resolve-agent", harness.internalToken, resolveRequest), http.StatusForbidden, contracts.ErrorCodeAgentDisabled) canceled, cancel := context.WithCancel(context.Background()) cancel() - requireAcceptanceError(t, harness, harness.requestWithContext(t, canceled, http.MethodGet, "/v3/workspaces/acceptance-errors", "Bearer "+harness.ownerToken, nil), http.StatusServiceUnavailable, contracts.ErrorCodeDependency) + requireAcceptanceError(t, harness, harness.requestWithContext(t, canceled, http.MethodGet, "/v1/workspaces/acceptance-errors", "Bearer "+harness.ownerToken, nil), http.StatusServiceUnavailable, contracts.ErrorCodeDependency) if _, err := harness.workspace.GetWorkspace(canceled, workspace.AuthenticatedCaller{ID: "owner-a"}, "acceptance-errors"); !errors.Is(err, workspace.ErrDependency) { t.Fatalf("canceled acceptance dependency = %v, want dependency", err) } @@ -530,7 +530,7 @@ func TestAcceptanceHTTPFailureBoundaries(t *testing.T) { if _, err := harness.pool.Exec(context.Background(), `ALTER SCHEMA workspace RENAME TO workspace_unavailable`); err != nil { t.Fatalf("degrade Workspace schema: %v", err) } - schemaFailure := requireAcceptanceError(t, harness, harness.request(t, http.MethodGet, "/v3/workspaces/acceptance-errors", harness.ownerToken, nil), http.StatusServiceUnavailable, contracts.ErrorCodeDependency) + schemaFailure := requireAcceptanceError(t, harness, harness.request(t, http.MethodGet, "/v1/workspaces/acceptance-errors", harness.ownerToken, nil), http.StatusServiceUnavailable, contracts.ErrorCodeDependency) if schemaFailure.Code != contracts.ErrorCodeDependency { t.Fatalf("schema failure code = %q", schemaFailure.Code) } @@ -553,7 +553,7 @@ BEFORE INSERT ON workspace.workspaces FOR EACH ROW EXECUTE FUNCTION workspace.issue_9_reject_workspace_insert()`); err != nil { t.Fatalf("create transaction failure trigger: %v", err) } - transactionFailure := requireAcceptanceError(t, harness, harness.request(t, http.MethodPost, "/v3/workspaces", harness.ownerToken, contracts.CreateWorkspaceRequest{WorkspaceID: "acceptance-transaction-failure"}), http.StatusServiceUnavailable, contracts.ErrorCodeDependency) + transactionFailure := requireAcceptanceError(t, harness, harness.request(t, http.MethodPost, "/v1/workspaces", harness.ownerToken, contracts.CreateWorkspaceRequest{WorkspaceID: "acceptance-transaction-failure"}), http.StatusServiceUnavailable, contracts.ErrorCodeDependency) if transactionFailure.Code != contracts.ErrorCodeDependency { t.Fatalf("transaction failure code = %q", transactionFailure.Code) } diff --git a/contracts/active_contracts_integration_test.go b/contracts/active_contracts_integration_test.go index 3c854cc6..793bfe5c 100644 --- a/contracts/active_contracts_integration_test.go +++ b/contracts/active_contracts_integration_test.go @@ -13,44 +13,42 @@ import ( func TestActiveContractVersionSynchronization(t *testing.T) { wantConstants := map[string]string{ - "Agent Card Schema": "0.2", - "Workspace Schema": "1", - "Installation Schema": "2", - "Public Agent Share Schema": "1", - "Invocation Event Schema": "0.2", - "Platform Error Schema": "2", - "Workspace Platform Error": "3", - "Invocation Result Schema": "1", - "Result Stream Event Schema": "1", - "A2A Profile Schema": "0.2", - "A2A protocol": "0.3.0", - "Northbound API": "3", - "Control Plane Internal API v2": "2", - "Control Plane Internal API v3": "3", - "Router Internal API v2": "2", - "Router Metadata API v3": "3", - "Router Internal API v4": "4", - "Router Agent Credential": "1", + "Agent Card Schema": "0.2", + "Workspace Schema": "1", + "Installation Schema": "2", + "Public Agent Share Schema": "1", + "Invocation Event Schema": "0.2", + "Platform Error Schema": "2", + "Workspace Platform Error": "3", + "Invocation Result Schema": "1", + "Result Stream Event Schema": "1", + "A2A Profile Schema": "0.2", + "A2A protocol": "0.3.0", + "Northbound API": "1", + "Control Plane Internal API": "1", + "Installed Version Internal API": "1", + "Router Internal API": "1", + "Router Metadata API": "1", + "Router Agent Credential": "1", } actualConstants := map[string]string{ - "Agent Card Schema": AgentCardSchemaVersion, - "Workspace Schema": WorkspaceSchemaVersion, - "Installation Schema": InstallationSchemaVersion, - "Public Agent Share Schema": PublicAgentShareSchemaVersion, - "Invocation Event Schema": InvocationEventSchemaVersion, - "Platform Error Schema": PlatformErrorSchemaVersion, - "Workspace Platform Error": WorkspacePlatformErrorSchemaVersion, - "Invocation Result Schema": InvocationResultSchemaVersion, - "Result Stream Event Schema": InvocationResultStreamEventSchemaVersion, - "A2A Profile Schema": A2AProfileSchemaVersion, - "A2A protocol": A2AProtocolVersion, - "Northbound API": NorthboundAPIVersion, - "Control Plane Internal API v2": ControlPlaneInternalAPIVersion, - "Control Plane Internal API v3": ControlPlaneInternalV3APIVersion, - "Router Internal API v2": RouterInternalAPIVersion, - "Router Metadata API v3": RouterInternalMetadataAPIVersion, - "Router Internal API v4": RouterInternalRuntimeAPIVersion, - "Router Agent Credential": RouterAgentCredentialSchemaVersion, + "Agent Card Schema": AgentCardSchemaVersion, + "Workspace Schema": WorkspaceSchemaVersion, + "Installation Schema": InstallationSchemaVersion, + "Public Agent Share Schema": PublicAgentShareSchemaVersion, + "Invocation Event Schema": InvocationEventSchemaVersion, + "Platform Error Schema": PlatformErrorSchemaVersion, + "Workspace Platform Error": WorkspacePlatformErrorSchemaVersion, + "Invocation Result Schema": InvocationResultSchemaVersion, + "Result Stream Event Schema": InvocationResultStreamEventSchemaVersion, + "A2A Profile Schema": A2AProfileSchemaVersion, + "A2A protocol": A2AProtocolVersion, + "Northbound API": NorthboundAPIVersion, + "Control Plane Internal API": ControlPlaneInternalAPIVersion, + "Installed Version Internal API": ControlPlaneInstalledVersionAPIVersion, + "Router Internal API": RouterInternalRuntimeAPIVersion, + "Router Metadata API": RouterInternalMetadataAPIVersion, + "Router Agent Credential": RouterAgentCredentialSchemaVersion, } for name, want := range wantConstants { if actualConstants[name] != want { @@ -101,13 +99,13 @@ func TestActiveContractVersionSynchronization(t *testing.T) { path string want string }{ - {path: filepath.Join("openapi", "control-plane.v3.yaml"), want: "3.0.0"}, + {path: filepath.Join("openapi", "control-plane.v1.yaml"), want: "1.0.0"}, {path: filepath.Join("openapi", "public-agent-share.v1.yaml"), want: "1.0.0"}, - {path: filepath.Join("openapi", "control-plane-internal.v2.yaml"), want: "2.0.0"}, - {path: filepath.Join("openapi", "control-plane-internal.v3.yaml"), want: "3.0.0"}, - {path: filepath.Join("openapi", "router-internal.v2.yaml"), want: "2.0.0"}, - {path: filepath.Join("openapi", "router-metadata.v3.yaml"), want: "3.0.0"}, - {path: filepath.Join("openapi", "router-internal.v4.yaml"), want: "4.0.0"}, + {path: filepath.Join("openapi", "control-plane-internal.v1.yaml"), want: "1.0.0"}, + {path: filepath.Join("openapi", "control-plane-installed-version.v1.yaml"), want: "1.0.0"}, + {path: filepath.Join("openapi", "control-plane-invocation.v1.yaml"), want: "1.0.0"}, + {path: filepath.Join("openapi", "router-internal.v1.yaml"), want: "1.0.0"}, + {path: filepath.Join("openapi", "router-metadata.v1.yaml"), want: "1.0.0"}, {path: filepath.Join("openapi", "router-topology-status.v1.yaml"), want: "1.0.0"}, } for _, document := range documents { @@ -130,19 +128,20 @@ func TestActiveOpenAPIToGoMappings(t *testing.T) { Result: json.RawMessage(`{"answer":42}`), } - northbound := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane.v3.yaml")) + northbound := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane.v1.yaml")) validateOpenAPIValue( t, - northbound.Paths.Find("/v3/agents").Post.RequestBody.Value.Content["application/json"].Schema, + northbound.Paths.Find("/v1/agents").Post.RequestBody.Value.Content["application/json"].Schema, RegisterAgentRequest{Card: card}, ) + invocationAPI := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane-invocation.v1.yaml")) validateOpenAPIValue( t, - northbound.Paths.Find("/v3/workspaces/{workspaceId}/invocations").Post.Responses.Status(200).Value.Content["application/json"].Schema, + invocationAPI.Paths.Find("/v1/workspaces/{workspaceId}/invocations").Post.Responses.Status(200).Value.Content["application/json"].Schema, result, ) - controlPlaneInternal := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane-internal.v2.yaml")) + controlPlaneInternal := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane-internal.v1.yaml")) resolveRequest := ResolveAgentRequest{ InvocationID: event.InvocationID, RootTaskID: event.RootTaskID, @@ -152,7 +151,7 @@ func TestActiveOpenAPIToGoMappings(t *testing.T) { Version: card.Version, Capability: event.Capability, } - resolveOperation := controlPlaneInternal.Paths.Find("/internal/v2/resolve-agent").Post + resolveOperation := controlPlaneInternal.Paths.Find("/internal/v1/resolve-agent").Post validateOpenAPIValue(t, resolveOperation.RequestBody.Value.Content["application/json"].Schema, resolveRequest) resolveResponseSchema := resolveOperation.Responses.Status(200).Value.Content["application/json"].Schema resolveResponse := ResolveAgentResponse{ @@ -175,7 +174,7 @@ func TestActiveOpenAPIToGoMappings(t *testing.T) { partialResolveResponse.Installation.AgentCardDigest = "" assertOpenAPIValueRejected(t, resolveResponseSchema, partialResolveResponse) - router := loadOpenAPIDocument(t, filepath.Join("openapi", "router-internal.v2.yaml")) + router := loadOpenAPIDocument(t, filepath.Join("openapi", "router-internal.v1.yaml")) dispatchRequest := DispatchInvocationRequest{ InvocationID: event.InvocationID, RootTaskID: event.RootTaskID, @@ -188,11 +187,9 @@ func TestActiveOpenAPIToGoMappings(t *testing.T) { Input: map[string]any{"contract": "active"}, Stream: false, } - dispatchOperation := router.Paths.Find("/internal/v2/invocations").Post + dispatchOperation := router.Paths.Find("/internal/v1/invocations").Post validateOpenAPIValue(t, dispatchOperation.RequestBody.Value.Content["application/json"].Schema, dispatchRequest) validateOpenAPIValue(t, dispatchOperation.Responses.Status(200).Value.Content["application/json"].Schema, result) - validateOpenAPIValue(t, router.Components.Schemas["RouterEventEnvelope"], RouterEventEnvelope{Event: event}) - var _ PlatformError = PlatformErrorV2{} //nolint:staticcheck // Preserve the explicit interface assertion. var _ InvocationEvent = InvocationEventV02{} //nolint:staticcheck // Preserve the explicit interface assertion. var _ RouterEventEnvelope = RouterEventEnvelopeV02{} //nolint:staticcheck // Preserve the explicit interface assertion. @@ -261,7 +258,7 @@ func TestActiveContractCorporaAreDiscoverable(t *testing.T) { } } -func TestHistoricalContractsRemainReadableWithoutActiveDualRead(t *testing.T) { +func TestHistoricalPayloadContractsRemainReadableWithoutActiveDualRead(t *testing.T) { historicalJSON := []string{ "schemas/agent-card.v0.1.schema.json", "schemas/invocation-event.v0.1.schema.json", @@ -280,9 +277,6 @@ func TestHistoricalContractsRemainReadableWithoutActiveDualRead(t *testing.T) { } }) } - loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane.v1.yaml")) - loadOpenAPIDocument(t, filepath.Join("openapi", "router-internal.v1.yaml")) - validator := mustValidator(t) historicalCard := validAgentCard() historicalCard.SchemaVersion = "0.1" @@ -336,20 +330,17 @@ func TestActiveContractsExcludeSecretsAndResultsFromMetadata(t *testing.T) { } func TestActiveInternalAPIsPreserveDirectionalOwnership(t *testing.T) { - controlPlane := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane-internal.v2.yaml")) - router := loadOpenAPIDocument(t, filepath.Join("openapi", "router-internal.v2.yaml")) + controlPlane := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane-internal.v1.yaml")) + router := loadOpenAPIDocument(t, filepath.Join("openapi", "router-internal.v1.yaml")) - assertExactStringSlice(t, "Control Plane Internal paths", controlPlane.Paths.Keys(), []string{"/internal/v2/resolve-agent"}) + assertExactStringSlice(t, "Control Plane Internal paths", controlPlane.Paths.Keys(), []string{"/internal/v1/resolve-agent"}) assertExactStringSlice(t, "Router Internal paths", router.Paths.Keys(), []string{ - "/internal/v2/invocations", - "/internal/v2/invocations/{invocationId}", - "/internal/v2/invocations/{invocationId}/events", - "/internal/v2/traces/{traceId}", + "/internal/v1/invocations", }) - if router.Paths.Find("/internal/v2/resolve-agent") != nil { + if router.Paths.Find("/internal/v1/resolve-agent") != nil { t.Fatal("Router Internal API owns Control Plane resolution") } - if controlPlane.Paths.Find("/internal/v2/invocations") != nil { + if controlPlane.Paths.Find("/internal/v1/invocations") != nil { t.Fatal("Control Plane Internal API owns Router dispatch") } if len(controlPlane.Servers) != 1 || len(router.Servers) != 1 { @@ -365,33 +356,33 @@ func TestActiveInternalAPIsPreserveDirectionalOwnership(t *testing.T) { } } -func TestActiveRuntimeV3InternalAPIsPreserveDirectionalOwnership(t *testing.T) { - controlPlane := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane-internal.v3.yaml")) - router := loadOpenAPIDocument(t, filepath.Join("openapi", "router-metadata.v3.yaml")) - assertExactStringSlice(t, "Control Plane Internal v3 paths", controlPlane.Paths.Keys(), []string{"/internal/v3/resolve-installed-version"}) - assertExactStringSlice(t, "Router Metadata v3 paths", router.Paths.Keys(), []string{ - "/internal/v3/workspaces/{workspaceId}/invocations/{invocationId}", - "/internal/v3/workspaces/{workspaceId}/traces/{traceId}", +func TestActiveV1MetadataAPIsPreserveDirectionalOwnership(t *testing.T) { + controlPlane := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane-installed-version.v1.yaml")) + router := loadOpenAPIDocument(t, filepath.Join("openapi", "router-metadata.v1.yaml")) + assertExactStringSlice(t, "Control Plane installed-version paths", controlPlane.Paths.Keys(), []string{"/internal/v1/resolve-installed-version"}) + assertExactStringSlice(t, "Router Metadata paths", router.Paths.Keys(), []string{ + "/internal/v1/workspaces/{workspaceId}/invocations/{invocationId}", + "/internal/v1/workspaces/{workspaceId}/traces/{traceId}", }) - if router.Paths.Find("/internal/v3/resolve-installed-version") != nil { - t.Fatal("Router Metadata v3 owns Control Plane resolution") + if router.Paths.Find("/internal/v1/resolve-installed-version") != nil { + t.Fatal("Router Metadata owns Control Plane resolution") } - if controlPlane.Paths.Find("/internal/v3/invocations") != nil { - t.Fatal("Control Plane Internal v3 owns Router dispatch") + if controlPlane.Paths.Find("/internal/v1/invocations") != nil { + t.Fatal("Control Plane installed-version API owns Router dispatch") } if len(controlPlane.Servers) != 1 || len(router.Servers) != 1 || controlPlane.Servers[0].URL == router.Servers[0].URL { - t.Fatal("active v3 internal APIs must have distinct explicit destinations") + t.Fatal("active v1 internal APIs must have distinct explicit destinations") } } -func TestActiveRuntimeV4RouterDispatchOwnsExecution(t *testing.T) { - router := loadOpenAPIDocument(t, filepath.Join("openapi", "router-internal.v4.yaml")) - assertExactStringSlice(t, "Router Internal v4 paths", router.Paths.Keys(), []string{"/internal/v4/invocations"}) - if router.Paths.Find("/internal/v4/invocations").Post == nil { - t.Fatal("Router Internal v4 must own dispatch") +func TestActiveV1RouterDispatchOwnsExecution(t *testing.T) { + router := loadOpenAPIDocument(t, filepath.Join("openapi", "router-internal.v1.yaml")) + assertExactStringSlice(t, "Router Internal v1 paths", router.Paths.Keys(), []string{"/internal/v1/invocations"}) + if router.Paths.Find("/internal/v1/invocations").Post == nil { + t.Fatal("Router Internal v1 must own dispatch") } - if router.Paths.Find("/internal/v3/invocations") != nil { - t.Fatal("Router Internal v4 must not serve the retired v3 dispatch route") + if router.Paths.Find("/internal/v4/invocations") != nil { + t.Fatal("Router Internal v1 must not describe the retired v4 dispatch route") } } diff --git a/contracts/catalog_api_contracts_test.go b/contracts/catalog_api_contracts_test.go index 681f8f32..c5748142 100644 --- a/contracts/catalog_api_contracts_test.go +++ b/contracts/catalog_api_contracts_test.go @@ -8,18 +8,18 @@ import ( ) func TestCatalogV2OperationsDeclareSecurityTraceAndExactErrors(t *testing.T) { - document := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane.v2.yaml")) + document := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane.v1.yaml")) tests := []struct { path string method string success int failures []int }{ - {path: "/v2/agents", method: "POST", success: 201, failures: []int{400, 401, 403, 409, 503}}, - {path: "/v2/agents", method: "GET", success: 200, failures: []int{400, 401, 503}}, - {path: "/v2/agents/{agentId}/versions/{version}", method: "GET", success: 200, failures: []int{400, 401, 403, 404, 503}}, - {path: "/v2/agents/{agentId}/versions/{version}/publish", method: "POST", success: 200, failures: []int{400, 401, 403, 404, 409, 503}}, - {path: "/v2/agents/{agentId}/versions/{version}/disable", method: "POST", success: 200, failures: []int{400, 401, 403, 404, 503}}, + {path: "/v1/agents", method: "POST", success: 201, failures: []int{400, 401, 403, 409, 503}}, + {path: "/v1/agents", method: "GET", success: 200, failures: []int{400, 401, 503}}, + {path: "/v1/agents/{agentId}/versions/{version}", method: "GET", success: 200, failures: []int{400, 401, 403, 404, 503}}, + {path: "/v1/agents/{agentId}/versions/{version}/publish", method: "POST", success: 200, failures: []int{400, 401, 403, 404, 409, 503}}, + {path: "/v1/agents/{agentId}/versions/{version}/disable", method: "POST", success: 200, failures: []int{400, 401, 403, 404, 503}}, } for _, test := range tests { t.Run(test.method+" "+test.path, func(t *testing.T) { @@ -51,10 +51,10 @@ func TestCatalogV2OperationsDeclareSecurityTraceAndExactErrors(t *testing.T) { } func TestCatalogV2GoMappingsAndDiscoveryPolicy(t *testing.T) { - document := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane.v2.yaml")) + document := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane.v1.yaml")) card := validAgentCard() entry := CatalogEntry{Card: card, PublicationStatus: "published", RegisteredAt: time.Now().UTC()} - register := document.Paths.Find("/v2/agents").Post + register := document.Paths.Find("/v1/agents").Post maximumBodyBytes, exists := register.RequestBody.Value.Extensions["x-nekiro-max-body-bytes"] if !exists { t.Fatal("registration body limit extension is missing") @@ -65,7 +65,7 @@ func TestCatalogV2GoMappingsAndDiscoveryPolicy(t *testing.T) { } validateOpenAPIValue(t, register.RequestBody.Value.Content["application/json"].Schema, RegisterAgentRequest{Card: card}) validateOpenAPIValue(t, register.Responses.Status(201).Value.Content["application/json"].Schema, entry) - search := document.Paths.Find("/v2/agents").Get + search := document.Paths.Find("/v1/agents").Get validateOpenAPIValue(t, search.Responses.Status(200).Value.Content["application/json"].Schema, SearchAgentsResponse{Items: []CatalogEntry{entry}}) var foundLimit bool diff --git a/contracts/contracts.go b/contracts/contracts.go index c45d8c46..b37efe54 100644 --- a/contracts/contracts.go +++ b/contracts/contracts.go @@ -17,9 +17,9 @@ const ( WorkspacePlatformErrorSchemaVersion = "3" A2AProfileSchemaVersion = A2AProfileSchemaVersionV02 A2AProtocolVersion = A2AProfileProtocolVersion - NorthboundAPIVersion = "3" - ControlPlaneInternalAPIVersion = "2" - RouterInternalAPIVersion = "2" + NorthboundAPIVersion = "1" + ControlPlaneInternalAPIVersion = "1" + RouterInternalAPIVersion = "1" DiscoveryDefaultLimit = 25 DiscoveryMinimumLimit = 1 DiscoveryMaximumLimit = 100 diff --git a/contracts/contracts_test.go b/contracts/contracts_test.go index 29df33d5..4d439346 100644 --- a/contracts/contracts_test.go +++ b/contracts/contracts_test.go @@ -487,10 +487,10 @@ func TestA2AProfileUsesOfficialSDK(t *testing.T) { func TestOpenAPIDocuments(t *testing.T) { for _, path := range []string{ - filepath.Join("openapi", "control-plane.v2.yaml"), - filepath.Join("openapi", "control-plane.v3.yaml"), - filepath.Join("openapi", "control-plane-internal.v2.yaml"), - filepath.Join("openapi", "router-internal.v2.yaml"), + filepath.Join("openapi", "control-plane.v1.yaml"), + filepath.Join("openapi", "control-plane.v1.yaml"), + filepath.Join("openapi", "control-plane-internal.v1.yaml"), + filepath.Join("openapi", "router-internal.v1.yaml"), } { t.Run(path, func(t *testing.T) { loadOpenAPIDocument(t, path) @@ -554,19 +554,6 @@ func TestGoDTOsMatchOpenAPI(t *testing.T) { catalogEntry := CatalogEntry{Card: card, PublicationStatus: "published", RegisteredAt: now, PublishedAt: &now} installation := validInstallation() event := validStartedEvent() - record := InvocationRecord{ - InvocationID: event.InvocationID, - RootTaskID: event.RootTaskID, - TraceID: event.TraceID, - Caller: event.Caller, - WorkspaceID: event.WorkspaceID, - TargetAgentID: event.TargetAgentID, - AgentCardVersion: event.AgentCardVersion, - Capability: event.Capability, - Status: event.Status, - CreatedAt: now, - UpdatedAt: now, - } result := InvocationResult{ SchemaVersion: InvocationResultSchemaVersion, InvocationID: event.InvocationID, @@ -576,7 +563,8 @@ func TestGoDTOsMatchOpenAPI(t *testing.T) { Result: json.RawMessage(`{"summary":"contract accepted"}`), } - controlPlane := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane.v3.yaml")) + controlPlane := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane.v1.yaml")) + invocationAPI := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane-invocation.v1.yaml")) controlCases := []struct { name string schema *openapi3.SchemaRef @@ -584,49 +572,39 @@ func TestGoDTOsMatchOpenAPI(t *testing.T) { }{ { name: "register request", - schema: controlPlane.Paths.Find("/v3/agents").Post.RequestBody.Value.Content["application/json"].Schema, + schema: controlPlane.Paths.Find("/v1/agents").Post.RequestBody.Value.Content["application/json"].Schema, value: RegisterAgentRequest{Card: card}, }, { name: "search response", - schema: controlPlane.Paths.Find("/v3/agents").Get.Responses.Status(200).Value.Content["application/json"].Schema, + schema: controlPlane.Paths.Find("/v1/agents").Get.Responses.Status(200).Value.Content["application/json"].Schema, value: SearchAgentsResponse{Items: []CatalogEntry{catalogEntry}}, }, { name: "install request", - schema: controlPlane.Paths.Find("/v3/workspaces/{workspaceId}/installations").Post.RequestBody.Value.Content["application/json"].Schema, + schema: controlPlane.Paths.Find("/v1/workspaces/{workspaceId}/installations").Post.RequestBody.Value.Content["application/json"].Schema, value: InstallAgentRequest{AgentID: card.AgentID, VersionConstraint: "^1.0.0", AcceptedPermissions: []string{"document.read"}}, }, { name: "installation response", - schema: controlPlane.Paths.Find("/v3/workspaces/{workspaceId}/installations").Post.Responses.Status(201).Value.Content["application/json"].Schema, + schema: controlPlane.Paths.Find("/v1/workspaces/{workspaceId}/installations").Post.Responses.Status(201).Value.Content["application/json"].Schema, value: installation, }, { name: "update installation request", - schema: controlPlane.Paths.Find("/v3/workspaces/{workspaceId}/installations/{installationId}").Patch.RequestBody.Value.Content["application/json"].Schema, + schema: controlPlane.Paths.Find("/v1/workspaces/{workspaceId}/installations/{installationId}").Patch.RequestBody.Value.Content["application/json"].Schema, value: UpdateInstallationRequest{Status: "disabled"}, }, { name: "invoke request", - schema: controlPlane.Paths.Find("/v3/workspaces/{workspaceId}/invocations").Post.RequestBody.Value.Content["application/json"].Schema, + schema: invocationAPI.Paths.Find("/v1/workspaces/{workspaceId}/invocations").Post.RequestBody.Value.Content["application/json"].Schema, value: InvokeAgentRequest{AgentID: card.AgentID, Capability: "contract.review", Input: map[string]any{"text": "contract"}, Stream: true}, }, { name: "invocation result", - schema: controlPlane.Paths.Find("/v3/workspaces/{workspaceId}/invocations").Post.Responses.Status(200).Value.Content["application/json"].Schema, + schema: invocationAPI.Paths.Find("/v1/workspaces/{workspaceId}/invocations").Post.Responses.Status(200).Value.Content["application/json"].Schema, value: result, }, - { - name: "invocation detail", - schema: controlPlane.Paths.Find("/v3/invocations/{invocationId}").Get.Responses.Status(200).Value.Content["application/json"].Schema, - value: InvocationDetailResponse{Invocation: record, Events: []InvocationEvent{event}}, - }, - { - name: "trace response", - schema: controlPlane.Paths.Find("/v3/traces/{traceId}").Get.Responses.Status(200).Value.Content["application/json"].Schema, - value: TraceResponse{TraceID: event.TraceID, Invocations: []InvocationRecord{record}}, - }, } for _, testCase := range controlCases { t.Run(testCase.name, func(t *testing.T) { @@ -634,15 +612,8 @@ func TestGoDTOsMatchOpenAPI(t *testing.T) { }) } - controlPlaneInternal := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane-internal.v2.yaml")) - router := loadOpenAPIDocument(t, filepath.Join("openapi", "router-internal.v2.yaml")) - streamOperation := router.Paths.Find("/internal/v2/invocations/{invocationId}/events") - if streamOperation == nil || streamOperation.Get == nil { - t.Fatal("Router SSE operation is missing") - } - if _, exists := streamOperation.Get.Responses.Status(200).Value.Content["text/event-stream"]; !exists { - t.Fatal("Router SSE response does not declare text/event-stream") - } + controlPlaneInternal := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane-internal.v1.yaml")) + router := loadOpenAPIDocument(t, filepath.Join("openapi", "router-internal.v1.yaml")) resolvedInstallation := ResolvedInstallation{ InstallationID: installation.InstallationID, WorkspaceID: installation.WorkspaceID, @@ -658,7 +629,7 @@ func TestGoDTOsMatchOpenAPI(t *testing.T) { }{ { name: "resolve request", - schema: controlPlaneInternal.Paths.Find("/internal/v2/resolve-agent").Post.RequestBody.Value.Content["application/json"].Schema, + schema: controlPlaneInternal.Paths.Find("/internal/v1/resolve-agent").Post.RequestBody.Value.Content["application/json"].Schema, value: ResolveAgentRequest{ InvocationID: event.InvocationID, RootTaskID: event.RootTaskID, TraceID: event.TraceID, WorkspaceID: installation.WorkspaceID, AgentID: card.AgentID, Version: card.Version, Capability: "contract.review", @@ -666,12 +637,12 @@ func TestGoDTOsMatchOpenAPI(t *testing.T) { }, { name: "resolve response", - schema: controlPlaneInternal.Paths.Find("/internal/v2/resolve-agent").Post.Responses.Status(200).Value.Content["application/json"].Schema, + schema: controlPlaneInternal.Paths.Find("/internal/v1/resolve-agent").Post.Responses.Status(200).Value.Content["application/json"].Schema, value: ResolveAgentResponse{Card: card, Installation: resolvedInstallation}, }, { name: "dispatch request", - schema: router.Paths.Find("/internal/v2/invocations").Post.RequestBody.Value.Content["application/json"].Schema, + schema: router.Paths.Find("/internal/v1/invocations").Post.RequestBody.Value.Content["application/json"].Schema, value: DispatchInvocationRequest{ InvocationID: event.InvocationID, RootTaskID: event.RootTaskID, TraceID: event.TraceID, Caller: event.Caller, WorkspaceID: event.WorkspaceID, TargetAgentID: event.TargetAgentID, @@ -681,14 +652,9 @@ func TestGoDTOsMatchOpenAPI(t *testing.T) { }, { name: "dispatch result", - schema: router.Paths.Find("/internal/v2/invocations").Post.Responses.Status(200).Value.Content["application/json"].Schema, + schema: router.Paths.Find("/internal/v1/invocations").Post.Responses.Status(200).Value.Content["application/json"].Schema, value: result, }, - { - name: "router event envelope", - schema: router.Components.Schemas["RouterEventEnvelope"], - value: RouterEventEnvelope{Event: event}, - }, } for _, testCase := range internalCases { t.Run(testCase.name, func(t *testing.T) { @@ -698,8 +664,8 @@ func TestGoDTOsMatchOpenAPI(t *testing.T) { } func TestSearchAgentsQueryMatchesOpenAPI(t *testing.T) { - document := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane.v3.yaml")) - operation := document.Paths.Find("/v3/agents").Get + document := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane.v1.yaml")) + operation := document.Paths.Find("/v1/agents").Get query := SearchAgentsQuery{ Query: stringPointer("contract"), Capability: stringPointer("contract.review"), diff --git a/contracts/openapi/control-plane-internal.v3.yaml b/contracts/openapi/control-plane-installed-version.v1.yaml similarity index 95% rename from contracts/openapi/control-plane-internal.v3.yaml rename to contracts/openapi/control-plane-installed-version.v1.yaml index 225727ef..f66a8f27 100644 --- a/contracts/openapi/control-plane-internal.v3.yaml +++ b/contracts/openapi/control-plane-installed-version.v1.yaml @@ -1,16 +1,15 @@ openapi: 3.1.0 info: - title: NeKiro Control Plane Internal API - version: 3.0.0 + title: NeKiro Control Plane Installed-Version Resolution API + version: 1.0.0 description: >- - Control Plane-owned operations called by the A2A Router. Version 3 adds - installed-version resolution for nested Agent invocations. The existing v2 - resolve-agent endpoint remains unchanged. + Control Plane-owned v1 installed-version resolution called by the A2A + Router for nested Agent invocations. servers: - url: https://control-plane.internal.nekiro.dev description: Control Plane internal destination paths: - /internal/v3/resolve-installed-version: + /internal/v1/resolve-installed-version: post: operationId: resolveInstalledVersion summary: Resolve the deterministic installed Agent Card version diff --git a/contracts/openapi/control-plane-internal.v1.yaml b/contracts/openapi/control-plane-internal.v1.yaml index 3c596145..0ec287c7 100644 --- a/contracts/openapi/control-plane-internal.v1.yaml +++ b/contracts/openapi/control-plane-internal.v1.yaml @@ -3,8 +3,9 @@ info: title: NeKiro Control Plane Internal API version: 1.0.0 description: >- - Control Plane-owned operations called by the A2A Router. This document is - served only by the Control Plane and contains no Router-owned operations. + Control Plane-owned v1 operations called by the A2A Router. This contract + separates failures before request correlation is trusted from failures + after strict correlation validation. servers: - url: https://control-plane.internal.nekiro.dev description: Control Plane internal destination @@ -15,12 +16,16 @@ paths: summary: Resolve an authorized exact Agent version description: >- Resolves Registry and Workspace-owned facts without allowing the Router - to read Control Plane storage directly. The invocationId, rootTaskId, - and traceId already created for the Invocation are required and every - error response MUST repeat those exact request values without replacing - or synthesizing correlation. + to read Control Plane storage directly. Once invocationId, rootTaskId, + and traceId pass strict validation, every later error repeats those + exact values. A malformed or missing correlation value cannot be + echoed; such a pre-correlation failure contains only a generated safe + traceId and the fixed error fields. + security: + - internalBearerAuth: [] requestBody: required: true + x-nekiro-max-body-bytes: 1048576 content: application/json: schema: @@ -28,12 +33,17 @@ paths: responses: "200": description: Resolved exact Agent Card and enabled Installation facts + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" content: application/json: schema: $ref: "#/components/schemas/ResolveAgentResponse" "400": $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthenticated" "403": $ref: "#/components/responses/Forbidden" "404": @@ -41,20 +51,61 @@ paths: "503": $ref: "#/components/responses/DependencyUnavailable" components: + securitySchemes: + internalBearerAuth: + type: http + scheme: bearer + bearerFormat: opaque + description: >- + Control Plane-verified internal service credential. Northbound caller + credentials are not implicitly trusted for this operation. + headers: + TraceID: + description: >- + Exact request trace identifier after correlation validation, or a + generated safe trace identifier for a pre-correlation failure. + required: true + schema: + $ref: ../schemas/common.v1.schema.json#/$defs/traceId responses: BadRequest: - description: Invalid resolution request + description: Invalid resolution request or correlation + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" x-platform-error-codes: [VALIDATION_ERROR] x-platform-error-correlation: - source: request - exactFields: [invocationId, rootTaskId, traceId] + preCorrelation: + bodyFields: [code, message, traceId] + traceId: generated + postCorrelation: + source: request + exactFields: [invocationId, rootTaskId, traceId] content: application/json: schema: - $ref: "#/components/schemas/CorrelatedPlatformError" + oneOf: + - $ref: "#/components/schemas/PreCorrelationPlatformError" + - $ref: "#/components/schemas/CorrelatedPlatformError" + Unauthenticated: + description: A valid trusted internal service credential is required + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" + x-platform-error-codes: [UNAUTHENTICATED] + x-platform-error-correlation: + source: generated + exactFields: [code, message, traceId] + content: + application/json: + schema: + $ref: "#/components/schemas/PreCorrelationPlatformError" Forbidden: - description: Installation or Agent version is disabled or capability is forbidden - x-platform-error-codes: [FORBIDDEN, AGENT_DISABLED, CAPABILITY_NOT_ALLOWED] + description: Installation, Agent version, Release state, or capability is not authorized + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" + x-platform-error-codes: [INSTALLATION_DISABLED, AGENT_DISABLED, AGENT_RELEASE_UNPUBLISHED, AGENT_RELEASE_SUSPENDED, AGENT_RELEASE_REVOKED, CAPABILITY_NOT_ALLOWED] x-platform-error-correlation: source: request exactFields: [invocationId, rootTaskId, traceId] @@ -63,7 +114,10 @@ components: schema: $ref: "#/components/schemas/CorrelatedPlatformError" NotFound: - description: Requested Agent, version, or Installation was not found + description: The requested Workspace does not exist, or no current Installation matches the requested Agent and exact version + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" x-platform-error-codes: [NOT_FOUND, AGENT_NOT_INSTALLED] x-platform-error-correlation: source: request @@ -74,6 +128,9 @@ components: $ref: "#/components/schemas/CorrelatedPlatformError" DependencyUnavailable: description: A required Control Plane dependency failed + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" x-platform-error-codes: [DEPENDENCY_ERROR] x-platform-error-correlation: source: request @@ -102,12 +159,29 @@ components: $ref: ../schemas/common.v1.schema.json#/$defs/semver capability: $ref: ../schemas/common.v1.schema.json#/$defs/capabilityId + PreCorrelationPlatformError: + allOf: + - $ref: ../schemas/platform-error.v3.schema.json + - type: object + required: [code, message, traceId] + not: + anyOf: + - required: [invocationId] + - required: [rootTaskId] CorrelatedPlatformError: allOf: - - $ref: ../schemas/platform-error.v2.schema.json + - $ref: ../schemas/platform-error.v3.schema.json - type: object required: [invocationId, rootTaskId, traceId] ResolveAgentResponse: + description: >- + The response is an exact authorization result. `card.agentId` and + `installation.agentId` MUST equal the request `agentId`; `card.version` + and `installation.installedVersion` MUST equal the request `version`; + and `installation.workspaceId` MUST equal the request `workspaceId`. + Trusted installations return `installedReleaseId` and the exact + Catalog-owned `agentCardDigest` as an atomic pair; pre-v4 legacy + installations omit both. type: object additionalProperties: false required: [card, installation] @@ -120,6 +194,12 @@ components: type: object additionalProperties: false required: [installationId, workspaceId, agentId, installedVersion, acceptedPermissions, status] + oneOf: + - required: [installedReleaseId, agentCardDigest] + - not: + anyOf: + - required: [installedReleaseId] + - required: [agentCardDigest] properties: installationId: $ref: ../schemas/common.v1.schema.json#/$defs/installationId @@ -129,6 +209,11 @@ components: $ref: ../schemas/common.v1.schema.json#/$defs/agentId installedVersion: $ref: ../schemas/common.v1.schema.json#/$defs/semver + installedReleaseId: + $ref: ../schemas/common.v1.schema.json#/$defs/safeIdentifier + agentCardDigest: + type: string + pattern: '^[0-9a-f]{64}$' acceptedPermissions: type: array uniqueItems: true diff --git a/contracts/openapi/control-plane-internal.v2.yaml b/contracts/openapi/control-plane-internal.v2.yaml deleted file mode 100644 index 1005b894..00000000 --- a/contracts/openapi/control-plane-internal.v2.yaml +++ /dev/null @@ -1,223 +0,0 @@ -openapi: 3.1.0 -info: - title: NeKiro Control Plane Internal API - version: 2.0.0 - description: >- - Control Plane-owned operations called by the A2A Router. This version - separates failures before request correlation is trusted from failures - after strict correlation validation. -servers: - - url: https://control-plane.internal.nekiro.dev - description: Control Plane internal destination -paths: - /internal/v2/resolve-agent: - post: - operationId: resolveAgent - summary: Resolve an authorized exact Agent version - description: >- - Resolves Registry and Workspace-owned facts without allowing the Router - to read Control Plane storage directly. Once invocationId, rootTaskId, - and traceId pass strict validation, every later error repeats those - exact values. A malformed or missing correlation value cannot be - echoed; such a pre-correlation failure contains only a generated safe - traceId and the fixed error fields. - security: - - internalBearerAuth: [] - requestBody: - required: true - x-nekiro-max-body-bytes: 1048576 - content: - application/json: - schema: - $ref: "#/components/schemas/ResolveAgentRequest" - responses: - "200": - description: Resolved exact Agent Card and enabled Installation facts - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - content: - application/json: - schema: - $ref: "#/components/schemas/ResolveAgentResponse" - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthenticated" - "403": - $ref: "#/components/responses/Forbidden" - "404": - $ref: "#/components/responses/NotFound" - "503": - $ref: "#/components/responses/DependencyUnavailable" -components: - securitySchemes: - internalBearerAuth: - type: http - scheme: bearer - bearerFormat: opaque - description: >- - Control Plane-verified internal service credential. Northbound caller - credentials are not implicitly trusted for this operation. - headers: - TraceID: - description: >- - Exact request trace identifier after correlation validation, or a - generated safe trace identifier for a pre-correlation failure. - required: true - schema: - $ref: ../schemas/common.v1.schema.json#/$defs/traceId - responses: - BadRequest: - description: Invalid resolution request or correlation - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - x-platform-error-codes: [VALIDATION_ERROR] - x-platform-error-correlation: - preCorrelation: - bodyFields: [code, message, traceId] - traceId: generated - postCorrelation: - source: request - exactFields: [invocationId, rootTaskId, traceId] - content: - application/json: - schema: - oneOf: - - $ref: "#/components/schemas/PreCorrelationPlatformError" - - $ref: "#/components/schemas/CorrelatedPlatformError" - Unauthenticated: - description: A valid trusted internal service credential is required - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - x-platform-error-codes: [UNAUTHENTICATED] - x-platform-error-correlation: - source: generated - exactFields: [code, message, traceId] - content: - application/json: - schema: - $ref: "#/components/schemas/PreCorrelationPlatformError" - Forbidden: - description: Installation, Agent version, Release state, or capability is not authorized - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - x-platform-error-codes: [INSTALLATION_DISABLED, AGENT_DISABLED, AGENT_RELEASE_UNPUBLISHED, AGENT_RELEASE_SUSPENDED, AGENT_RELEASE_REVOKED, CAPABILITY_NOT_ALLOWED] - x-platform-error-correlation: - source: request - exactFields: [invocationId, rootTaskId, traceId] - content: - application/json: - schema: - $ref: "#/components/schemas/CorrelatedPlatformError" - NotFound: - description: The requested Workspace does not exist, or no current Installation matches the requested Agent and exact version - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - x-platform-error-codes: [NOT_FOUND, AGENT_NOT_INSTALLED] - x-platform-error-correlation: - source: request - exactFields: [invocationId, rootTaskId, traceId] - content: - application/json: - schema: - $ref: "#/components/schemas/CorrelatedPlatformError" - DependencyUnavailable: - description: A required Control Plane dependency failed - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - x-platform-error-codes: [DEPENDENCY_ERROR] - x-platform-error-correlation: - source: request - exactFields: [invocationId, rootTaskId, traceId] - content: - application/json: - schema: - $ref: "#/components/schemas/CorrelatedPlatformError" - schemas: - ResolveAgentRequest: - type: object - additionalProperties: false - required: [invocationId, rootTaskId, traceId, workspaceId, agentId, version, capability] - properties: - invocationId: - $ref: ../schemas/common.v1.schema.json#/$defs/invocationId - rootTaskId: - $ref: ../schemas/common.v1.schema.json#/$defs/taskId - traceId: - $ref: ../schemas/common.v1.schema.json#/$defs/traceId - workspaceId: - $ref: ../schemas/common.v1.schema.json#/$defs/workspaceId - agentId: - $ref: ../schemas/common.v1.schema.json#/$defs/agentId - version: - $ref: ../schemas/common.v1.schema.json#/$defs/semver - capability: - $ref: ../schemas/common.v1.schema.json#/$defs/capabilityId - PreCorrelationPlatformError: - allOf: - - $ref: ../schemas/platform-error.v3.schema.json - - type: object - required: [code, message, traceId] - not: - anyOf: - - required: [invocationId] - - required: [rootTaskId] - CorrelatedPlatformError: - allOf: - - $ref: ../schemas/platform-error.v3.schema.json - - type: object - required: [invocationId, rootTaskId, traceId] - ResolveAgentResponse: - description: >- - The response is an exact authorization result. `card.agentId` and - `installation.agentId` MUST equal the request `agentId`; `card.version` - and `installation.installedVersion` MUST equal the request `version`; - and `installation.workspaceId` MUST equal the request `workspaceId`. - Trusted installations return `installedReleaseId` and the exact - Catalog-owned `agentCardDigest` as an atomic pair; pre-v4 legacy - installations omit both. - type: object - additionalProperties: false - required: [card, installation] - properties: - card: - $ref: ../schemas/agent-card.v0.2.schema.json - installation: - $ref: "#/components/schemas/ResolvedInstallation" - ResolvedInstallation: - type: object - additionalProperties: false - required: [installationId, workspaceId, agentId, installedVersion, acceptedPermissions, status] - oneOf: - - required: [installedReleaseId, agentCardDigest] - - not: - anyOf: - - required: [installedReleaseId] - - required: [agentCardDigest] - properties: - installationId: - $ref: ../schemas/common.v1.schema.json#/$defs/installationId - workspaceId: - $ref: ../schemas/common.v1.schema.json#/$defs/workspaceId - agentId: - $ref: ../schemas/common.v1.schema.json#/$defs/agentId - installedVersion: - $ref: ../schemas/common.v1.schema.json#/$defs/semver - installedReleaseId: - $ref: ../schemas/common.v1.schema.json#/$defs/safeIdentifier - agentCardDigest: - type: string - pattern: '^[0-9a-f]{64}$' - acceptedPermissions: - type: array - uniqueItems: true - items: - $ref: ../schemas/common.v1.schema.json#/$defs/permissionId - status: - const: enabled diff --git a/contracts/openapi/control-plane-invocation.v4.yaml b/contracts/openapi/control-plane-invocation.v1.yaml similarity index 94% rename from contracts/openapi/control-plane-invocation.v4.yaml rename to contracts/openapi/control-plane-invocation.v1.yaml index 716c0c87..19d81231 100644 --- a/contracts/openapi/control-plane-invocation.v4.yaml +++ b/contracts/openapi/control-plane-invocation.v1.yaml @@ -1,20 +1,18 @@ openapi: 3.1.0 info: title: NeKiro Northbound Invocation API - version: 4.0.0 + version: 1.0.0 description: >- Invocation-only Northbound target. Catalog, Workspace, and Installation - remain served from control-plane.v3.yaml; this document is not a complete - Control Plane v4 replacement and defines no second fact for those domains. - Clients use the same Gateway destination and select v4 only for Invocation - create/metadata routes. + remain served from control-plane.v1.yaml. This focused document defines no + second fact for those domains; clients use the same Gateway v1 destination. servers: - url: https://api.nekiro.dev description: Existing Gateway destination paths: - /v4/workspaces/{workspaceId}/invocations: + /v1/workspaces/{workspaceId}/invocations: post: - operationId: invokeAgentV4 + operationId: invokeAgent summary: Invoke an installed Agent through Dispatch and Router description: >- Gateway authenticates the caller, strictly validates media and the @@ -63,9 +61,9 @@ paths: "502": { $ref: "#/components/responses/AgentFailure" } "503": { $ref: "#/components/responses/Unavailable" } "504": { $ref: "#/components/responses/Timeout" } - /v4/workspaces/{workspaceId}/invocations/{invocationId}: + /v1/workspaces/{workspaceId}/invocations/{invocationId}: get: - operationId: getInvocationV4 + operationId: getInvocation security: [{ bearerAuth: [] }] parameters: - $ref: "#/components/parameters/WorkspaceId" @@ -75,14 +73,14 @@ paths: description: Workspace-scoped metadata projection and ordered facts; a last non-terminal status is returned unchanged content: application/json: - schema: { $ref: "#/components/schemas/InvocationDetailResponseV4" } + schema: { $ref: "#/components/schemas/InvocationDetailResponseV1" } "401": { $ref: "#/components/responses/Unauthenticated" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "503": { $ref: "#/components/responses/Unavailable" } - /v4/workspaces/{workspaceId}/traces/{traceId}: + /v1/workspaces/{workspaceId}/traces/{traceId}: get: - operationId: getTraceV4 + operationId: getTrace security: [{ bearerAuth: [] }] parameters: - $ref: "#/components/parameters/WorkspaceId" @@ -95,7 +93,7 @@ paths: description: Workspace-authorized metadata-only parent-child lineage projections content: application/json: - schema: { $ref: "#/components/schemas/TraceResponseV4" } + schema: { $ref: "#/components/schemas/TraceResponseV1" } "401": { $ref: "#/components/responses/Unauthenticated" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } @@ -138,7 +136,7 @@ components: capability: { $ref: ../schemas/common.v1.schema.json#/$defs/capabilityId } input: { $ref: ../schemas/common.v1.schema.json#/$defs/jsonObject } stream: { type: boolean } - InvocationRecordV4: + InvocationRecordV1: type: object additionalProperties: false required: [invocationId, rootTaskId, traceId, caller, workspaceId, targetAgentId, agentCardVersion, capability, status, createdAt, updatedAt] @@ -165,16 +163,16 @@ components: errorCode: { $ref: ../schemas/platform-error.v4.schema.json#/$defs/errorCode } createdAt: { $ref: ../schemas/common.v1.schema.json#/$defs/dateTime } updatedAt: { $ref: ../schemas/common.v1.schema.json#/$defs/dateTime } - InvocationDetailResponseV4: + InvocationDetailResponseV1: type: object additionalProperties: false required: [invocation, events] properties: - invocation: { $ref: "#/components/schemas/InvocationRecordV4" } + invocation: { $ref: "#/components/schemas/InvocationRecordV1" } events: type: array items: { $ref: ../schemas/invocation-event.v0.3.schema.json } - TraceResponseV4: + TraceResponseV1: type: object additionalProperties: false required: [traceId, invocations] @@ -182,7 +180,7 @@ components: traceId: { $ref: ../schemas/common.v1.schema.json#/$defs/traceId } invocations: type: array - items: { $ref: "#/components/schemas/InvocationRecordV4" } + items: { $ref: "#/components/schemas/InvocationRecordV1" } PreCorrelationPlatformError: $ref: ../schemas/platform-error.v4.schema.json#/$defs/preCorrelation CorrelatedPlatformError: diff --git a/contracts/openapi/control-plane.v1.yaml b/contracts/openapi/control-plane.v1.yaml index aee7f823..ec929c46 100644 --- a/contracts/openapi/control-plane.v1.yaml +++ b/contracts/openapi/control-plane.v1.yaml @@ -1,16 +1,31 @@ openapi: 3.1.0 info: - title: NeKiro Control Plane API + title: NeKiro Gateway Catalog and Workspace API version: 1.0.0 + description: >- + Gateway-owned v1 Catalog, Workspace, and Installation API. Invocation and + trusted-publication operations are defined by their focused v1 documents + at the same Gateway destination. servers: - - url: http://localhost:8080 + - url: https://api.nekiro.dev + description: NeKiro Gateway destination paths: /v1/agents: post: operationId: registerAgent summary: Register an immutable draft Agent Card version + description: >- + Registers only an active Agent Card 0.2 document. The authenticated + caller must equal card.owner.id. The request rejects malformed JSON, + duplicate members, unknown fields, trailing values, and structural or + semantic Card violations before persistence. The Gateway reads at most + 16,777,216 body bytes; an oversized request returns the fixed validation + failure without persistence. + security: + - bearerAuth: [] requestBody: required: true + x-nekiro-max-body-bytes: 16777216 content: application/json: schema: @@ -19,42 +34,68 @@ paths: required: [card] properties: card: - $ref: ../schemas/agent-card.v0.1.schema.json + $ref: ../schemas/agent-card.v0.2.schema.json responses: "201": description: Agent version registered + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" content: application/json: schema: $ref: "#/components/schemas/CatalogEntry" "400": - $ref: "#/components/responses/Error" + $ref: "#/components/responses/CatalogValidationError" + "401": + $ref: "#/components/responses/CatalogUnauthenticated" + "403": + $ref: "#/components/responses/CatalogForbidden" "409": - $ref: "#/components/responses/Error" + $ref: "#/components/responses/CatalogConflict" + "503": + $ref: "#/components/responses/CatalogDependencyError" get: operationId: searchAgents summary: Discover published Agents + description: >- + Returns only exact published Agent Card versions. Supplied query, + capability, and ownerId filters combine with AND. No matches returns an + explicit empty items array. Draft and disabled versions are excluded. + security: + - bearerAuth: [] parameters: - in: query name: query + description: Literal case-insensitive substring over Card name or description; values are not SQL wildcards. schema: { type: string, minLength: 1, maxLength: 256, pattern: "\\S" } - in: query name: capability + description: Exact case-sensitive capability identifier. schema: $ref: ../schemas/common.v1.schema.json#/$defs/capabilityId - in: query name: ownerId + description: Exact case-sensitive owner identifier. schema: $ref: ../schemas/common.v1.schema.json#/$defs/ownerId - in: query name: limit - schema: { type: integer, minimum: 1, maximum: 100 } + description: Page size. Omission uses the explicit product default of 25; invalid explicit values are rejected. + schema: { type: integer, minimum: 1, maximum: 100, default: 25 } - in: query name: cursor + description: >- + Opaque continuation bound to the original normalized filters, page + size, and first-page publication boundary. Malformed or + filter-mismatched values are rejected and never restart traversal. schema: { type: string, minLength: 1 } responses: "200": description: Matching published Agents + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" content: application/json: schema: @@ -69,214 +110,376 @@ paths: nextCursor: type: string minLength: 1 + "400": + $ref: "#/components/responses/CatalogValidationError" + "401": + $ref: "#/components/responses/CatalogUnauthenticated" + "503": + $ref: "#/components/responses/CatalogDependencyError" /v1/agents/{agentId}/versions/{version}: get: operationId: getAgentVersion + summary: Read one exact Agent Card version + description: >- + Published versions are visible to every authenticated caller. Draft and + disabled versions are visible only to their immutable owner. + security: + - bearerAuth: [] parameters: - $ref: "#/components/parameters/AgentId" - $ref: "#/components/parameters/Version" responses: "200": description: Exact Agent version + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" content: application/json: schema: $ref: "#/components/schemas/CatalogEntry" + "400": + $ref: "#/components/responses/CatalogValidationError" + "401": + $ref: "#/components/responses/CatalogUnauthenticated" + "403": + $ref: "#/components/responses/CatalogForbidden" "404": - $ref: "#/components/responses/Error" + $ref: "#/components/responses/CatalogNotFound" + "503": + $ref: "#/components/responses/CatalogDependencyError" /v1/agents/{agentId}/versions/{version}/publish: post: operationId: publishAgentVersion + summary: Publish one owned draft Agent Card version + description: >- + Only the immutable owner may publish. Publication succeeds exactly once + from draft and makes the version immediately eligible for Discovery. + security: + - bearerAuth: [] parameters: - $ref: "#/components/parameters/AgentId" - $ref: "#/components/parameters/Version" responses: "200": description: Published Agent version + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" content: application/json: schema: $ref: "#/components/schemas/CatalogEntry" + "400": + $ref: "#/components/responses/CatalogValidationError" + "401": + $ref: "#/components/responses/CatalogUnauthenticated" + "403": + $ref: "#/components/responses/CatalogForbidden" "404": - $ref: "#/components/responses/Error" + $ref: "#/components/responses/CatalogNotFound" "409": - $ref: "#/components/responses/Error" + $ref: "#/components/responses/CatalogConflict" + "503": + $ref: "#/components/responses/CatalogDependencyError" /v1/agents/{agentId}/versions/{version}/disable: post: operationId: disableAgentVersion + summary: Disable one owned Agent Card version + description: >- + Only the immutable owner may disable a draft or published version. + Repeating disable returns the unchanged disabled entry as the specified + idempotent success. + security: + - bearerAuth: [] parameters: - $ref: "#/components/parameters/AgentId" - $ref: "#/components/parameters/Version" responses: "200": description: Disabled Agent version + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" content: application/json: schema: $ref: "#/components/schemas/CatalogEntry" + "400": + $ref: "#/components/responses/CatalogValidationError" + "401": + $ref: "#/components/responses/CatalogUnauthenticated" + "403": + $ref: "#/components/responses/CatalogForbidden" "404": - $ref: "#/components/responses/Error" + $ref: "#/components/responses/CatalogNotFound" + "503": + $ref: "#/components/responses/CatalogDependencyError" + /v1/workspaces: + post: + operationId: createWorkspace + summary: Create an owner-controlled Workspace + description: >- + Creates one logical authorization and audit boundary. The authenticated + caller becomes the immutable owner. Owner identity and timestamps are + never accepted from request data. Repeating an existing workspaceId is + a conflict rather than an idempotent success. + security: + - bearerAuth: [] + requestBody: + required: true + x-nekiro-max-body-bytes: 1048576 + content: + application/json: + schema: + $ref: "#/components/schemas/CreateWorkspaceRequest" + responses: + "201": + description: Workspace created + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" + content: + application/json: + schema: + $ref: ../schemas/workspace.v1.schema.json + "400": + $ref: "#/components/responses/WorkspaceValidationError" + "401": + $ref: "#/components/responses/WorkspaceUnauthenticated" + "409": + $ref: "#/components/responses/WorkspaceConflict" + "503": + $ref: "#/components/responses/WorkspaceDependencyError" + /v1/workspaces/{workspaceId}: + get: + operationId: getWorkspace + summary: Read an owned Workspace + description: >- + Returns the exact durable Workspace only to its immutable owner. + security: + - bearerAuth: [] + parameters: + - $ref: "#/components/parameters/WorkspaceId" + responses: + "200": + description: Exact Workspace + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" + content: + application/json: + schema: + $ref: ../schemas/workspace.v1.schema.json + "400": + $ref: "#/components/responses/WorkspaceValidationError" + "401": + $ref: "#/components/responses/WorkspaceUnauthenticated" + "403": + $ref: "#/components/responses/WorkspaceForbidden" + "404": + $ref: "#/components/responses/WorkspaceNotFound" + "503": + $ref: "#/components/responses/WorkspaceDependencyError" /v1/workspaces/{workspaceId}/installations: post: operationId: installAgent + summary: Install and pin one published Agent version + description: >- + Selects the highest published version satisfying the exact submitted + SemVer constraint, then requires that exact version to have a published + trusted Release (or an explicitly marked pre-v4 legacy marker), + validates the accepted permission subset against that Card, and creates + one enabled immutable pin. It does not silently downgrade when the + selected version fails the Release gate. + A current enabled or disabled Installation for the same Agent conflicts. + security: + - bearerAuth: [] parameters: - $ref: "#/components/parameters/WorkspaceId" requestBody: required: true + x-nekiro-max-body-bytes: 1048576 content: application/json: schema: - type: object - additionalProperties: false - required: [agentId, versionConstraint, acceptedPermissions] - properties: - agentId: - $ref: ../schemas/common.v1.schema.json#/$defs/agentId - versionConstraint: - $ref: ../schemas/common.v1.schema.json#/$defs/semverRange - acceptedPermissions: - type: array - uniqueItems: true - items: - $ref: ../schemas/common.v1.schema.json#/$defs/permissionId + $ref: "#/components/schemas/InstallAgentRequest" responses: "201": description: Agent installed in Workspace + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" content: application/json: schema: - $ref: ../schemas/installation.v1.schema.json + $ref: ../schemas/installation.v2.schema.json "400": - $ref: "#/components/responses/Error" + $ref: "#/components/responses/WorkspaceValidationError" + "401": + $ref: "#/components/responses/WorkspaceUnauthenticated" + "403": + $ref: "#/components/responses/WorkspaceInstallForbidden" "404": - $ref: "#/components/responses/Error" + $ref: "#/components/responses/WorkspaceNotFound" "409": - $ref: "#/components/responses/Error" - /v1/workspaces/{workspaceId}/installations/{installationId}: - patch: - operationId: updateInstallation + $ref: "#/components/responses/WorkspaceConflict" + "503": + $ref: "#/components/responses/WorkspaceDependencyError" + get: + operationId: listInstallations + summary: List current and historical Installations + description: >- + Returns at most the requested bounded page of enabled, disabled, and + uninstalled Installations ordered by installedAt and then + installationId ascending. The opaque cursor is bound to the Workspace + and page size. An existing owned Workspace with no records returns an + explicit empty items array without a cursor. + security: + - bearerAuth: [] parameters: - $ref: "#/components/parameters/WorkspaceId" - - $ref: "#/components/parameters/InstallationId" - requestBody: - required: true - content: - application/json: - schema: - type: object - additionalProperties: false - required: [status] - properties: - status: { enum: [enabled, disabled] } + - $ref: "#/components/parameters/InstallationLimit" + - $ref: "#/components/parameters/InstallationCursor" responses: "200": - description: Updated installation + description: Bounded page of current and historical Installations + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" content: application/json: schema: - $ref: ../schemas/installation.v1.schema.json + $ref: "#/components/schemas/InstallationList" + "400": + $ref: "#/components/responses/WorkspaceValidationError" + "401": + $ref: "#/components/responses/WorkspaceUnauthenticated" + "403": + $ref: "#/components/responses/WorkspaceForbidden" "404": - $ref: "#/components/responses/Error" - delete: - operationId: uninstallAgent + $ref: "#/components/responses/WorkspaceNotFound" + "503": + $ref: "#/components/responses/WorkspaceDependencyError" + /v1/workspaces/{workspaceId}/installations/{installationId}: + get: + operationId: getInstallation + summary: Read one current or historical Installation + security: + - bearerAuth: [] parameters: - $ref: "#/components/parameters/WorkspaceId" - $ref: "#/components/parameters/InstallationId" responses: - "204": - description: Agent uninstalled + "200": + description: Exact Installation + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" + content: + application/json: + schema: + $ref: ../schemas/installation.v2.schema.json + "400": + $ref: "#/components/responses/WorkspaceValidationError" + "401": + $ref: "#/components/responses/WorkspaceUnauthenticated" + "403": + $ref: "#/components/responses/WorkspaceForbidden" "404": - $ref: "#/components/responses/Error" - /v1/workspaces/{workspaceId}/invocations: - post: - operationId: invokeAgent + $ref: "#/components/responses/WorkspaceNotFound" + "503": + $ref: "#/components/responses/WorkspaceDependencyError" + patch: + operationId: updateInstallation + summary: Enable or disable a current Installation + description: >- + Applies exactly one enabled-to-disabled or disabled-to-enabled + transition. Same-state requests and every transition from uninstalled + conflict rather than returning idempotent success. + security: + - bearerAuth: [] parameters: - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/InstallationId" requestBody: required: true + x-nekiro-max-body-bytes: 1048576 content: application/json: schema: - type: object - additionalProperties: false - required: [agentId, capability, input, stream] - properties: - agentId: - $ref: ../schemas/common.v1.schema.json#/$defs/agentId - capability: - $ref: ../schemas/common.v1.schema.json#/$defs/capabilityId - input: - $ref: ../schemas/common.v1.schema.json#/$defs/jsonObject - stream: - type: boolean + $ref: "#/components/schemas/UpdateInstallationRequest" responses: - "202": - description: Invocation accepted + "200": + description: Updated installation + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" content: application/json: schema: - $ref: "#/components/schemas/InvocationAccepted" + $ref: ../schemas/installation.v2.schema.json "400": - $ref: "#/components/responses/Error" + $ref: "#/components/responses/WorkspaceValidationError" + "401": + $ref: "#/components/responses/WorkspaceUnauthenticated" "403": - $ref: "#/components/responses/Error" - "404": - $ref: "#/components/responses/Error" - /v1/invocations/{invocationId}: - get: - operationId: getInvocation - parameters: - - in: path - name: invocationId - required: true - schema: - $ref: ../schemas/common.v1.schema.json#/$defs/invocationId - responses: - "200": - description: Invocation and append-only events - content: - application/json: - schema: - type: object - additionalProperties: false - required: [invocation, events] - properties: - invocation: - $ref: "#/components/schemas/InvocationRecord" - events: - type: array - items: - $ref: ../schemas/invocation-event.v0.1.schema.json + $ref: "#/components/responses/WorkspaceForbidden" "404": - $ref: "#/components/responses/Error" - /v1/traces/{traceId}: - get: - operationId: getTrace + $ref: "#/components/responses/WorkspaceNotFound" + "409": + $ref: "#/components/responses/WorkspaceConflict" + "503": + $ref: "#/components/responses/WorkspaceDependencyError" + delete: + operationId: uninstallAgent + summary: Uninstall and preserve one disabled Installation + description: >- + Transitions only a disabled Installation to uninstalled and returns the + preserved terminal fact. Enabled and already-uninstalled records + conflict; uninstall is not idempotent. + security: + - bearerAuth: [] parameters: - - in: path - name: traceId - required: true - schema: - $ref: ../schemas/common.v1.schema.json#/$defs/traceId + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/InstallationId" responses: "200": - description: Parent-child invocation trace + description: Preserved terminal Installation + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" content: application/json: schema: - type: object - additionalProperties: false - required: [traceId, invocations] - properties: - traceId: - $ref: ../schemas/common.v1.schema.json#/$defs/traceId - invocations: - type: array - items: - $ref: "#/components/schemas/InvocationRecord" + $ref: ../schemas/installation.v2.schema.json + "400": + $ref: "#/components/responses/WorkspaceValidationError" + "401": + $ref: "#/components/responses/WorkspaceUnauthenticated" + "403": + $ref: "#/components/responses/WorkspaceForbidden" "404": - $ref: "#/components/responses/Error" + $ref: "#/components/responses/WorkspaceNotFound" + "409": + $ref: "#/components/responses/WorkspaceConflict" + "503": + $ref: "#/components/responses/WorkspaceDependencyError" components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: opaque + description: >- + Gateway-verified bearer credential mapped to one trusted caller ID. + Public caller-ID headers are not an authentication source. + headers: + TraceID: + description: Gateway-assigned request trace identifier. + required: true + schema: + $ref: ../schemas/common.v1.schema.json#/$defs/traceId parameters: AgentId: in: path @@ -302,40 +505,350 @@ components: required: true schema: $ref: ../schemas/common.v1.schema.json#/$defs/installationId + InstallationLimit: + in: query + name: limit + required: true + description: >- + Required maximum number of Installations in one page. Values outside + 1-100 are validation failures. + schema: + type: integer + minimum: 1 + maximum: 100 + InstallationCursor: + in: query + name: cursor + required: false + description: >- + Opaque continuation bound to this Workspace, the page size, and the + last installedAt/installationId ordering tuple. Malformed or mismatched + cursors are validation failures and never restart traversal. + schema: + type: string + minLength: 1 + InvocationId: + in: path + name: invocationId + required: true + schema: + $ref: ../schemas/common.v1.schema.json#/$defs/invocationId + ResultAccept: + in: header + name: Accept + required: true + description: >- + application/json or a compatible wildcard for stream=false; + text/event-stream for stream=true. + schema: + type: string + minLength: 1 responses: - Error: - description: Platform error + CatalogValidationError: + description: Catalog request validation failed + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" + x-platform-error-codes: [VALIDATION_ERROR] + content: + application/json: + schema: + $ref: ../schemas/platform-error.v2.schema.json + CatalogUnauthenticated: + description: A valid Gateway bearer identity is required + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" + x-platform-error-codes: [UNAUTHENTICATED] + content: + application/json: + schema: + $ref: ../schemas/platform-error.v2.schema.json + CatalogForbidden: + description: The authenticated caller is not allowed to perform this Catalog operation + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" + x-platform-error-codes: [FORBIDDEN] + content: + application/json: + schema: + $ref: ../schemas/platform-error.v2.schema.json + CatalogNotFound: + description: The exact Agent Card version does not exist + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" + x-platform-error-codes: [NOT_FOUND] + content: + application/json: + schema: + $ref: ../schemas/platform-error.v2.schema.json + CatalogConflict: + description: The exact version already exists or the requested lifecycle transition is illegal + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" + x-platform-error-codes: [CONFLICT] + content: + application/json: + schema: + $ref: ../schemas/platform-error.v2.schema.json + CatalogDependencyError: + description: The Catalog persistence dependency could not complete the operation + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" + x-platform-error-codes: [DEPENDENCY_ERROR] + content: + application/json: + schema: + $ref: ../schemas/platform-error.v2.schema.json + WorkspaceValidationError: + description: Workspace request validation failed + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" + x-platform-error-codes: [VALIDATION_ERROR] + content: + application/json: + schema: + $ref: ../schemas/platform-error.v3.schema.json + WorkspaceUnauthenticated: + description: A valid Gateway bearer identity is required + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" + x-platform-error-codes: [UNAUTHENTICATED] + content: + application/json: + schema: + $ref: ../schemas/platform-error.v3.schema.json + WorkspaceForbidden: + description: The authenticated caller does not own the Workspace + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" + x-platform-error-codes: [FORBIDDEN] + content: + application/json: + schema: + $ref: ../schemas/platform-error.v3.schema.json + WorkspaceInstallForbidden: + description: The caller is forbidden or no matching published Release is installable + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" + x-platform-error-codes: [FORBIDDEN, AGENT_RELEASE_UNPUBLISHED, AGENT_RELEASE_SUSPENDED, AGENT_RELEASE_REVOKED] + content: + application/json: + schema: + $ref: ../schemas/platform-error.v3.schema.json + WorkspaceNotFound: + description: The requested Workspace, Installation, or published install candidate was not found + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" + x-platform-error-codes: [NOT_FOUND] + content: + application/json: + schema: + $ref: ../schemas/platform-error.v3.schema.json + WorkspaceConflict: + description: The Workspace or current Installation already exists, or the lifecycle transition is illegal + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" + x-platform-error-codes: [CONFLICT] + content: + application/json: + schema: + $ref: ../schemas/platform-error.v3.schema.json + WorkspaceDependencyError: + description: A required Workspace, Catalog, or persistence dependency failed + headers: + x-nek-trace-id: + $ref: "#/components/headers/TraceID" + x-platform-error-codes: [DEPENDENCY_ERROR] + content: + application/json: + schema: + $ref: ../schemas/platform-error.v3.schema.json + ResourceNotFound: + description: Requested resource was not found + x-platform-error-codes: [NOT_FOUND] + content: + application/json: + schema: + $ref: ../schemas/platform-error.v2.schema.json + StateConflict: + description: Requested operation conflicts with current state + x-platform-error-codes: [CONFLICT] + content: + application/json: + schema: + $ref: ../schemas/platform-error.v2.schema.json + DependencyUnavailable: + description: A required platform dependency failed + x-platform-error-codes: [DEPENDENCY_ERROR] + content: + application/json: + schema: + $ref: ../schemas/platform-error.v2.schema.json + BadRequest: + description: Invalid request + x-platform-error-codes: [VALIDATION_ERROR] + content: + application/json: + schema: + $ref: ../schemas/platform-error.v2.schema.json + Unauthenticated: + description: Authentication is required + x-platform-error-codes: [UNAUTHENTICATED] + content: + application/json: + schema: + $ref: ../schemas/platform-error.v2.schema.json + Forbidden: + description: Installation, Agent state, or capability is not authorized + x-platform-error-codes: [FORBIDDEN, AGENT_DISABLED, CAPABILITY_NOT_ALLOWED] + content: + application/json: + schema: + $ref: ../schemas/platform-error.v2.schema.json + NotFound: + description: Requested Agent or Installation was not found + x-platform-error-codes: [NOT_FOUND, AGENT_NOT_INSTALLED] + content: + application/json: + schema: + $ref: ../schemas/platform-error.v2.schema.json + NotAcceptable: + description: Request result mode and Accept header do not agree + x-platform-error-codes: [NOT_ACCEPTABLE] + content: + application/json: + schema: + $ref: ../schemas/platform-error.v2.schema.json + Conflict: + description: Invocation was canceled or conflicts with current state before response commitment + x-platform-error-codes: [CONFLICT, CANCELED] + content: + application/json: + schema: + $ref: ../schemas/platform-error.v2.schema.json + AgentFailure: + description: Agent execution or A2A protocol failed before response commitment + x-platform-error-codes: [AGENT_EXECUTION_FAILED, A2A_PROTOCOL_ERROR] + x-platform-error-correlation: + source: created-invocation-context + exactFields: [invocationId, rootTaskId, traceId] + content: + application/json: + schema: + $ref: "#/components/schemas/CorrelatedPlatformError" + Unavailable: + description: Route, Agent, or required dependency is unavailable + x-platform-error-codes: [ROUTE_NOT_FOUND, AGENT_UNAVAILABLE, DEPENDENCY_ERROR] + x-platform-error-correlation: + source: created-invocation-context + exactFields: [invocationId, rootTaskId, traceId] + content: + application/json: + schema: + $ref: "#/components/schemas/CorrelatedPlatformError" + Timeout: + description: Invocation deadline expired before response commitment + x-platform-error-codes: [TIMEOUT] + x-platform-error-correlation: + source: created-invocation-context + exactFields: [invocationId, rootTaskId, traceId] content: application/json: schema: - $ref: ../schemas/platform-error.v1.schema.json + $ref: "#/components/schemas/CorrelatedPlatformError" schemas: + CreateWorkspaceRequest: + type: object + additionalProperties: false + required: [workspaceId] + properties: + workspaceId: + $ref: ../schemas/common.v1.schema.json#/$defs/workspaceId + InstallAgentRequest: + type: object + additionalProperties: false + required: [agentId, versionConstraint, acceptedPermissions] + properties: + agentId: + $ref: ../schemas/common.v1.schema.json#/$defs/agentId + versionConstraint: + $ref: ../schemas/common.v1.schema.json#/$defs/semverRange + acceptedPermissions: + type: array + uniqueItems: true + items: + $ref: ../schemas/common.v1.schema.json#/$defs/permissionId + InstallationList: + type: object + additionalProperties: false + required: [items] + properties: + items: + type: array + maxItems: 100 + items: + $ref: ../schemas/installation.v2.schema.json + nextCursor: + type: string + minLength: 1 + UpdateInstallationRequest: + type: object + additionalProperties: false + required: [status] + properties: + status: + enum: [enabled, disabled] + CorrelatedPlatformError: + allOf: + - $ref: ../schemas/platform-error.v2.schema.json + - type: object + required: [invocationId, rootTaskId, traceId] CatalogEntry: type: object additionalProperties: false required: [card, publicationStatus, registeredAt] + dependentRequired: + publicAgentId: [publicUrl] + publicUrl: [publicAgentId] properties: card: - $ref: ../schemas/agent-card.v0.1.schema.json + $ref: ../schemas/agent-card.v0.2.schema.json publicationStatus: enum: [draft, published, disabled] registeredAt: $ref: ../schemas/common.v1.schema.json#/$defs/dateTime publishedAt: $ref: ../schemas/common.v1.schema.json#/$defs/dateTime - InvocationAccepted: + publicAgentId: + type: string + pattern: "^agt_[0-9a-f]{32}$" + publicUrl: + type: string + format: uri + InvokeAgentRequest: type: object additionalProperties: false - required: [invocationId, rootTaskId, traceId, status] + required: [agentId, capability, input, stream] properties: - invocationId: - $ref: ../schemas/common.v1.schema.json#/$defs/invocationId - rootTaskId: - $ref: ../schemas/common.v1.schema.json#/$defs/taskId - traceId: - $ref: ../schemas/common.v1.schema.json#/$defs/traceId - status: - const: pending + agentId: + $ref: ../schemas/common.v1.schema.json#/$defs/agentId + capability: + $ref: ../schemas/common.v1.schema.json#/$defs/capabilityId + input: + $ref: ../schemas/common.v1.schema.json#/$defs/jsonObject + stream: + type: boolean InvocationRecord: type: object additionalProperties: false @@ -365,7 +878,7 @@ components: type: integer minimum: 0 errorCode: - $ref: ../schemas/platform-error.v1.schema.json#/properties/code + $ref: ../schemas/platform-error.v2.schema.json#/properties/code createdAt: $ref: ../schemas/common.v1.schema.json#/$defs/dateTime updatedAt: diff --git a/contracts/openapi/control-plane.v2.yaml b/contracts/openapi/control-plane.v2.yaml deleted file mode 100644 index e3b8fb43..00000000 --- a/contracts/openapi/control-plane.v2.yaml +++ /dev/null @@ -1,674 +0,0 @@ -openapi: 3.1.0 -info: - title: NeKiro Control Plane API - version: 2.0.0 - description: >- - Gateway-owned Northbound API. Invocation v2 returns transient Agent output - on the invocation request and exposes only metadata through Ledger reads. -servers: - - url: https://api.nekiro.dev - description: NeKiro Gateway destination -paths: - /v2/agents: - post: - operationId: registerAgent - summary: Register an immutable draft Agent Card version - description: >- - Registers only an active Agent Card 0.2 document. The authenticated - caller must equal card.owner.id. The request rejects malformed JSON, - duplicate members, unknown fields, trailing values, and structural or - semantic Card violations before persistence. The Gateway reads at most - 16,777,216 body bytes; an oversized request returns the fixed validation - failure without persistence. - security: - - bearerAuth: [] - requestBody: - required: true - x-nekiro-max-body-bytes: 16777216 - content: - application/json: - schema: - type: object - additionalProperties: false - required: [card] - properties: - card: - $ref: ../schemas/agent-card.v0.2.schema.json - responses: - "201": - description: Agent version registered - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - content: - application/json: - schema: - $ref: "#/components/schemas/CatalogEntry" - "400": - $ref: "#/components/responses/CatalogValidationError" - "401": - $ref: "#/components/responses/CatalogUnauthenticated" - "403": - $ref: "#/components/responses/CatalogForbidden" - "409": - $ref: "#/components/responses/CatalogConflict" - "503": - $ref: "#/components/responses/CatalogDependencyError" - get: - operationId: searchAgents - summary: Discover published Agents - description: >- - Returns only exact published Agent Card versions. Supplied query, - capability, and ownerId filters combine with AND. No matches returns an - explicit empty items array. Draft and disabled versions are excluded. - security: - - bearerAuth: [] - parameters: - - in: query - name: query - description: Literal case-insensitive substring over Card name or description; values are not SQL wildcards. - schema: { type: string, minLength: 1, maxLength: 256, pattern: "\\S" } - - in: query - name: capability - description: Exact case-sensitive capability identifier. - schema: - $ref: ../schemas/common.v1.schema.json#/$defs/capabilityId - - in: query - name: ownerId - description: Exact case-sensitive owner identifier. - schema: - $ref: ../schemas/common.v1.schema.json#/$defs/ownerId - - in: query - name: limit - description: Page size. Omission uses the explicit product default of 25; invalid explicit values are rejected. - schema: { type: integer, minimum: 1, maximum: 100, default: 25 } - - in: query - name: cursor - description: >- - Opaque continuation bound to the original normalized filters, page - size, and first-page publication boundary. Malformed or - filter-mismatched values are rejected and never restart traversal. - schema: { type: string, minLength: 1 } - responses: - "200": - description: Matching published Agents - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - content: - application/json: - schema: - type: object - additionalProperties: false - required: [items] - properties: - items: - type: array - items: - $ref: "#/components/schemas/CatalogEntry" - nextCursor: - type: string - minLength: 1 - "400": - $ref: "#/components/responses/CatalogValidationError" - "401": - $ref: "#/components/responses/CatalogUnauthenticated" - "503": - $ref: "#/components/responses/CatalogDependencyError" - /v2/agents/{agentId}/versions/{version}: - get: - operationId: getAgentVersion - summary: Read one exact Agent Card version - description: >- - Published versions are visible to every authenticated caller. Draft and - disabled versions are visible only to their immutable owner. - security: - - bearerAuth: [] - parameters: - - $ref: "#/components/parameters/AgentId" - - $ref: "#/components/parameters/Version" - responses: - "200": - description: Exact Agent version - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - content: - application/json: - schema: - $ref: "#/components/schemas/CatalogEntry" - "400": - $ref: "#/components/responses/CatalogValidationError" - "401": - $ref: "#/components/responses/CatalogUnauthenticated" - "403": - $ref: "#/components/responses/CatalogForbidden" - "404": - $ref: "#/components/responses/CatalogNotFound" - "503": - $ref: "#/components/responses/CatalogDependencyError" - /v2/agents/{agentId}/versions/{version}/publish: - post: - operationId: publishAgentVersion - summary: Publish one owned draft Agent Card version - description: >- - Only the immutable owner may publish. Publication succeeds exactly once - from draft and makes the version immediately eligible for Discovery. - security: - - bearerAuth: [] - parameters: - - $ref: "#/components/parameters/AgentId" - - $ref: "#/components/parameters/Version" - responses: - "200": - description: Published Agent version - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - content: - application/json: - schema: - $ref: "#/components/schemas/CatalogEntry" - "400": - $ref: "#/components/responses/CatalogValidationError" - "401": - $ref: "#/components/responses/CatalogUnauthenticated" - "403": - $ref: "#/components/responses/CatalogForbidden" - "404": - $ref: "#/components/responses/CatalogNotFound" - "409": - $ref: "#/components/responses/CatalogConflict" - "503": - $ref: "#/components/responses/CatalogDependencyError" - /v2/agents/{agentId}/versions/{version}/disable: - post: - operationId: disableAgentVersion - summary: Disable one owned Agent Card version - description: >- - Only the immutable owner may disable a draft or published version. - Repeating disable returns the unchanged disabled entry as the specified - idempotent success. - security: - - bearerAuth: [] - parameters: - - $ref: "#/components/parameters/AgentId" - - $ref: "#/components/parameters/Version" - responses: - "200": - description: Disabled Agent version - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - content: - application/json: - schema: - $ref: "#/components/schemas/CatalogEntry" - "400": - $ref: "#/components/responses/CatalogValidationError" - "401": - $ref: "#/components/responses/CatalogUnauthenticated" - "403": - $ref: "#/components/responses/CatalogForbidden" - "404": - $ref: "#/components/responses/CatalogNotFound" - "503": - $ref: "#/components/responses/CatalogDependencyError" - /v2/workspaces/{workspaceId}/installations: - post: - operationId: installAgent - parameters: - - $ref: "#/components/parameters/WorkspaceId" - requestBody: - required: true - content: - application/json: - schema: - type: object - additionalProperties: false - required: [agentId, versionConstraint, acceptedPermissions] - properties: - agentId: - $ref: ../schemas/common.v1.schema.json#/$defs/agentId - versionConstraint: - $ref: ../schemas/common.v1.schema.json#/$defs/semverRange - acceptedPermissions: - type: array - uniqueItems: true - items: - $ref: ../schemas/common.v1.schema.json#/$defs/permissionId - responses: - "201": - description: Agent installed in Workspace - content: - application/json: - schema: - $ref: ../schemas/installation.v1.schema.json - "400": - $ref: "#/components/responses/BadRequest" - "404": - $ref: "#/components/responses/ResourceNotFound" - "409": - $ref: "#/components/responses/StateConflict" - /v2/workspaces/{workspaceId}/installations/{installationId}: - patch: - operationId: updateInstallation - parameters: - - $ref: "#/components/parameters/WorkspaceId" - - $ref: "#/components/parameters/InstallationId" - requestBody: - required: true - content: - application/json: - schema: - type: object - additionalProperties: false - required: [status] - properties: - status: { enum: [enabled, disabled] } - responses: - "200": - description: Updated installation - content: - application/json: - schema: - $ref: ../schemas/installation.v1.schema.json - "404": - $ref: "#/components/responses/ResourceNotFound" - delete: - operationId: uninstallAgent - parameters: - - $ref: "#/components/parameters/WorkspaceId" - - $ref: "#/components/parameters/InstallationId" - responses: - "204": - description: Agent uninstalled - "404": - $ref: "#/components/responses/ResourceNotFound" - /v2/workspaces/{workspaceId}/invocations: - post: - operationId: invokeAgent - summary: Invoke an installed Agent and return its transient result - description: >- - This POST is the only Northbound result channel. The request stream - field and Accept header MUST agree. stream=false requires - application/json or a compatible wildcard and returns one Invocation - Result. stream=true requires text/event-stream and returns ordered - Invocation Result Stream Event data values. A mismatch returns 406 - NOT_ACCEPTABLE. Once SSE is committed, failed, canceled, and timed-out - outcomes are in-band terminal events. EOF without a terminal event is - interrupted delivery. A JSON result and every 502, 503, or 504 response - MUST repeat the exact invocationId, rootTaskId, and traceId from the - Invocation context created for this request. Results are not persisted - or replayed. - parameters: - - $ref: "#/components/parameters/WorkspaceId" - - $ref: "#/components/parameters/ResultAccept" - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/InvokeAgentRequest" - responses: - "200": - description: Complete JSON result or committed SSE result stream selected by request mode - content: - application/json: - schema: - $ref: ../schemas/invocation-result.v1.schema.json - text/event-stream: - schema: - type: string - x-sse-data-schema: - $ref: ../schemas/invocation-result-stream-event.v1.schema.json - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthenticated" - "403": - $ref: "#/components/responses/Forbidden" - "404": - $ref: "#/components/responses/NotFound" - "406": - $ref: "#/components/responses/NotAcceptable" - "409": - $ref: "#/components/responses/Conflict" - "502": - $ref: "#/components/responses/AgentFailure" - "503": - $ref: "#/components/responses/Unavailable" - "504": - $ref: "#/components/responses/Timeout" - /v2/invocations/{invocationId}: - get: - operationId: getInvocation - summary: Read an Invocation and metadata-only Ledger facts - parameters: - - $ref: "#/components/parameters/InvocationId" - responses: - "200": - description: Invocation projection and append-only facts without Agent input or result content - content: - application/json: - schema: - type: object - additionalProperties: false - required: [invocation, events] - properties: - invocation: - $ref: "#/components/schemas/InvocationRecord" - events: - type: array - items: - $ref: ../schemas/invocation-event.v0.2.schema.json - "404": - $ref: "#/components/responses/ResourceNotFound" - "503": - $ref: "#/components/responses/DependencyUnavailable" - /v2/traces/{traceId}: - get: - operationId: getTrace - summary: Read metadata-only Invocation lineage - parameters: - - in: path - name: traceId - required: true - schema: - $ref: ../schemas/common.v1.schema.json#/$defs/traceId - responses: - "200": - description: Parent-child Invocation trace without Agent input or result content - content: - application/json: - schema: - type: object - additionalProperties: false - required: [traceId, invocations] - properties: - traceId: - $ref: ../schemas/common.v1.schema.json#/$defs/traceId - invocations: - type: array - items: - $ref: "#/components/schemas/InvocationRecord" - "404": - $ref: "#/components/responses/ResourceNotFound" - "503": - $ref: "#/components/responses/DependencyUnavailable" -components: - securitySchemes: - bearerAuth: - type: http - scheme: bearer - bearerFormat: opaque - description: >- - Gateway-verified bearer credential mapped to one trusted caller ID. - Public caller-ID headers are not an authentication source. - headers: - TraceID: - description: Gateway-assigned request trace identifier. - required: true - schema: - $ref: ../schemas/common.v1.schema.json#/$defs/traceId - parameters: - AgentId: - in: path - name: agentId - required: true - schema: - $ref: ../schemas/common.v1.schema.json#/$defs/agentId - Version: - in: path - name: version - required: true - schema: - $ref: ../schemas/common.v1.schema.json#/$defs/semver - WorkspaceId: - in: path - name: workspaceId - required: true - schema: - $ref: ../schemas/common.v1.schema.json#/$defs/workspaceId - InstallationId: - in: path - name: installationId - required: true - schema: - $ref: ../schemas/common.v1.schema.json#/$defs/installationId - InvocationId: - in: path - name: invocationId - required: true - schema: - $ref: ../schemas/common.v1.schema.json#/$defs/invocationId - ResultAccept: - in: header - name: Accept - required: true - description: >- - application/json or a compatible wildcard for stream=false; - text/event-stream for stream=true. - schema: - type: string - minLength: 1 - responses: - CatalogValidationError: - description: Catalog request validation failed - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - x-platform-error-codes: [VALIDATION_ERROR] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - CatalogUnauthenticated: - description: A valid Gateway bearer identity is required - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - x-platform-error-codes: [UNAUTHENTICATED] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - CatalogForbidden: - description: The authenticated caller is not allowed to perform this Catalog operation - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - x-platform-error-codes: [FORBIDDEN] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - CatalogNotFound: - description: The exact Agent Card version does not exist - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - x-platform-error-codes: [NOT_FOUND] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - CatalogConflict: - description: The exact version already exists or the requested lifecycle transition is illegal - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - x-platform-error-codes: [CONFLICT] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - CatalogDependencyError: - description: The Catalog persistence dependency could not complete the operation - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - x-platform-error-codes: [DEPENDENCY_ERROR] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - ResourceNotFound: - description: Requested resource was not found - x-platform-error-codes: [NOT_FOUND] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - StateConflict: - description: Requested operation conflicts with current state - x-platform-error-codes: [CONFLICT] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - DependencyUnavailable: - description: A required platform dependency failed - x-platform-error-codes: [DEPENDENCY_ERROR] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - BadRequest: - description: Invalid request - x-platform-error-codes: [VALIDATION_ERROR] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - Unauthenticated: - description: Authentication is required - x-platform-error-codes: [UNAUTHENTICATED] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - Forbidden: - description: Installation or capability is not authorized - x-platform-error-codes: [FORBIDDEN, AGENT_DISABLED, CAPABILITY_NOT_ALLOWED] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - NotFound: - description: Requested Agent or Installation was not found - x-platform-error-codes: [NOT_FOUND, AGENT_NOT_INSTALLED] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - NotAcceptable: - description: Request result mode and Accept header do not agree - x-platform-error-codes: [NOT_ACCEPTABLE] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - Conflict: - description: Invocation was canceled or conflicts with current state before response commitment - x-platform-error-codes: [CONFLICT, CANCELED] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - AgentFailure: - description: Agent execution or A2A protocol failed before response commitment - x-platform-error-codes: [AGENT_EXECUTION_FAILED, A2A_PROTOCOL_ERROR] - x-platform-error-correlation: - source: created-invocation-context - exactFields: [invocationId, rootTaskId, traceId] - content: - application/json: - schema: - $ref: "#/components/schemas/CorrelatedPlatformError" - Unavailable: - description: Route, Agent, or required dependency is unavailable - x-platform-error-codes: [ROUTE_NOT_FOUND, AGENT_UNAVAILABLE, DEPENDENCY_ERROR] - x-platform-error-correlation: - source: created-invocation-context - exactFields: [invocationId, rootTaskId, traceId] - content: - application/json: - schema: - $ref: "#/components/schemas/CorrelatedPlatformError" - Timeout: - description: Invocation deadline expired before response commitment - x-platform-error-codes: [TIMEOUT] - x-platform-error-correlation: - source: created-invocation-context - exactFields: [invocationId, rootTaskId, traceId] - content: - application/json: - schema: - $ref: "#/components/schemas/CorrelatedPlatformError" - schemas: - CorrelatedPlatformError: - allOf: - - $ref: ../schemas/platform-error.v2.schema.json - - type: object - required: [invocationId, rootTaskId, traceId] - CatalogEntry: - type: object - additionalProperties: false - required: [card, publicationStatus, registeredAt] - properties: - card: - $ref: ../schemas/agent-card.v0.2.schema.json - publicationStatus: - enum: [draft, published, disabled] - registeredAt: - $ref: ../schemas/common.v1.schema.json#/$defs/dateTime - publishedAt: - $ref: ../schemas/common.v1.schema.json#/$defs/dateTime - InvokeAgentRequest: - type: object - additionalProperties: false - required: [agentId, capability, input, stream] - properties: - agentId: - $ref: ../schemas/common.v1.schema.json#/$defs/agentId - capability: - $ref: ../schemas/common.v1.schema.json#/$defs/capabilityId - input: - $ref: ../schemas/common.v1.schema.json#/$defs/jsonObject - stream: - type: boolean - InvocationRecord: - type: object - additionalProperties: false - required: [invocationId, rootTaskId, traceId, caller, workspaceId, targetAgentId, agentCardVersion, capability, status, createdAt, updatedAt] - properties: - invocationId: - $ref: ../schemas/common.v1.schema.json#/$defs/invocationId - rootTaskId: - $ref: ../schemas/common.v1.schema.json#/$defs/taskId - parentInvocationId: - $ref: ../schemas/common.v1.schema.json#/$defs/invocationId - traceId: - $ref: ../schemas/common.v1.schema.json#/$defs/traceId - caller: - $ref: ../schemas/common.v1.schema.json#/$defs/caller - workspaceId: - $ref: ../schemas/common.v1.schema.json#/$defs/workspaceId - targetAgentId: - $ref: ../schemas/common.v1.schema.json#/$defs/agentId - agentCardVersion: - $ref: ../schemas/common.v1.schema.json#/$defs/semver - capability: - $ref: ../schemas/common.v1.schema.json#/$defs/capabilityId - status: - enum: [pending, routing, running, succeeded, failed, canceled, timed_out] - latencyMs: - type: integer - minimum: 0 - errorCode: - $ref: ../schemas/platform-error.v2.schema.json#/properties/code - createdAt: - $ref: ../schemas/common.v1.schema.json#/$defs/dateTime - updatedAt: - $ref: ../schemas/common.v1.schema.json#/$defs/dateTime diff --git a/contracts/openapi/control-plane.v3.yaml b/contracts/openapi/control-plane.v3.yaml deleted file mode 100644 index 966eebca..00000000 --- a/contracts/openapi/control-plane.v3.yaml +++ /dev/null @@ -1,1009 +0,0 @@ -openapi: 3.1.0 -info: - title: NeKiro Control Plane API - version: 3.0.0 - description: >- - Gateway-owned Northbound API. Invocation v2 returns transient Agent output - on the invocation request and exposes only metadata through Ledger reads. -servers: - - url: https://api.nekiro.dev - description: NeKiro Gateway destination -paths: - /v3/agents: - post: - operationId: registerAgent - summary: Register an immutable draft Agent Card version - description: >- - Registers only an active Agent Card 0.2 document. The authenticated - caller must equal card.owner.id. The request rejects malformed JSON, - duplicate members, unknown fields, trailing values, and structural or - semantic Card violations before persistence. The Gateway reads at most - 16,777,216 body bytes; an oversized request returns the fixed validation - failure without persistence. - security: - - bearerAuth: [] - requestBody: - required: true - x-nekiro-max-body-bytes: 16777216 - content: - application/json: - schema: - type: object - additionalProperties: false - required: [card] - properties: - card: - $ref: ../schemas/agent-card.v0.2.schema.json - responses: - "201": - description: Agent version registered - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - content: - application/json: - schema: - $ref: "#/components/schemas/CatalogEntry" - "400": - $ref: "#/components/responses/CatalogValidationError" - "401": - $ref: "#/components/responses/CatalogUnauthenticated" - "403": - $ref: "#/components/responses/CatalogForbidden" - "409": - $ref: "#/components/responses/CatalogConflict" - "503": - $ref: "#/components/responses/CatalogDependencyError" - get: - operationId: searchAgents - summary: Discover published Agents - description: >- - Returns only exact published Agent Card versions. Supplied query, - capability, and ownerId filters combine with AND. No matches returns an - explicit empty items array. Draft and disabled versions are excluded. - security: - - bearerAuth: [] - parameters: - - in: query - name: query - description: Literal case-insensitive substring over Card name or description; values are not SQL wildcards. - schema: { type: string, minLength: 1, maxLength: 256, pattern: "\\S" } - - in: query - name: capability - description: Exact case-sensitive capability identifier. - schema: - $ref: ../schemas/common.v1.schema.json#/$defs/capabilityId - - in: query - name: ownerId - description: Exact case-sensitive owner identifier. - schema: - $ref: ../schemas/common.v1.schema.json#/$defs/ownerId - - in: query - name: limit - description: Page size. Omission uses the explicit product default of 25; invalid explicit values are rejected. - schema: { type: integer, minimum: 1, maximum: 100, default: 25 } - - in: query - name: cursor - description: >- - Opaque continuation bound to the original normalized filters, page - size, and first-page publication boundary. Malformed or - filter-mismatched values are rejected and never restart traversal. - schema: { type: string, minLength: 1 } - responses: - "200": - description: Matching published Agents - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - content: - application/json: - schema: - type: object - additionalProperties: false - required: [items] - properties: - items: - type: array - items: - $ref: "#/components/schemas/CatalogEntry" - nextCursor: - type: string - minLength: 1 - "400": - $ref: "#/components/responses/CatalogValidationError" - "401": - $ref: "#/components/responses/CatalogUnauthenticated" - "503": - $ref: "#/components/responses/CatalogDependencyError" - /v3/agents/{agentId}/versions/{version}: - get: - operationId: getAgentVersion - summary: Read one exact Agent Card version - description: >- - Published versions are visible to every authenticated caller. Draft and - disabled versions are visible only to their immutable owner. - security: - - bearerAuth: [] - parameters: - - $ref: "#/components/parameters/AgentId" - - $ref: "#/components/parameters/Version" - responses: - "200": - description: Exact Agent version - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - content: - application/json: - schema: - $ref: "#/components/schemas/CatalogEntry" - "400": - $ref: "#/components/responses/CatalogValidationError" - "401": - $ref: "#/components/responses/CatalogUnauthenticated" - "403": - $ref: "#/components/responses/CatalogForbidden" - "404": - $ref: "#/components/responses/CatalogNotFound" - "503": - $ref: "#/components/responses/CatalogDependencyError" - /v3/agents/{agentId}/versions/{version}/publish: - post: - operationId: publishAgentVersion - summary: Publish one owned draft Agent Card version - description: >- - Only the immutable owner may publish. Publication succeeds exactly once - from draft and makes the version immediately eligible for Discovery. - security: - - bearerAuth: [] - parameters: - - $ref: "#/components/parameters/AgentId" - - $ref: "#/components/parameters/Version" - responses: - "200": - description: Published Agent version - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - content: - application/json: - schema: - $ref: "#/components/schemas/CatalogEntry" - "400": - $ref: "#/components/responses/CatalogValidationError" - "401": - $ref: "#/components/responses/CatalogUnauthenticated" - "403": - $ref: "#/components/responses/CatalogForbidden" - "404": - $ref: "#/components/responses/CatalogNotFound" - "409": - $ref: "#/components/responses/CatalogConflict" - "503": - $ref: "#/components/responses/CatalogDependencyError" - /v3/agents/{agentId}/versions/{version}/disable: - post: - operationId: disableAgentVersion - summary: Disable one owned Agent Card version - description: >- - Only the immutable owner may disable a draft or published version. - Repeating disable returns the unchanged disabled entry as the specified - idempotent success. - security: - - bearerAuth: [] - parameters: - - $ref: "#/components/parameters/AgentId" - - $ref: "#/components/parameters/Version" - responses: - "200": - description: Disabled Agent version - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - content: - application/json: - schema: - $ref: "#/components/schemas/CatalogEntry" - "400": - $ref: "#/components/responses/CatalogValidationError" - "401": - $ref: "#/components/responses/CatalogUnauthenticated" - "403": - $ref: "#/components/responses/CatalogForbidden" - "404": - $ref: "#/components/responses/CatalogNotFound" - "503": - $ref: "#/components/responses/CatalogDependencyError" - /v3/workspaces: - post: - operationId: createWorkspace - summary: Create an owner-controlled Workspace - description: >- - Creates one logical authorization and audit boundary. The authenticated - caller becomes the immutable owner. Owner identity and timestamps are - never accepted from request data. Repeating an existing workspaceId is - a conflict rather than an idempotent success. - security: - - bearerAuth: [] - requestBody: - required: true - x-nekiro-max-body-bytes: 1048576 - content: - application/json: - schema: - $ref: "#/components/schemas/CreateWorkspaceRequest" - responses: - "201": - description: Workspace created - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - content: - application/json: - schema: - $ref: ../schemas/workspace.v1.schema.json - "400": - $ref: "#/components/responses/WorkspaceValidationError" - "401": - $ref: "#/components/responses/WorkspaceUnauthenticated" - "409": - $ref: "#/components/responses/WorkspaceConflict" - "503": - $ref: "#/components/responses/WorkspaceDependencyError" - /v3/workspaces/{workspaceId}: - get: - operationId: getWorkspace - summary: Read an owned Workspace - description: >- - Returns the exact durable Workspace only to its immutable owner. - security: - - bearerAuth: [] - parameters: - - $ref: "#/components/parameters/WorkspaceId" - responses: - "200": - description: Exact Workspace - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - content: - application/json: - schema: - $ref: ../schemas/workspace.v1.schema.json - "400": - $ref: "#/components/responses/WorkspaceValidationError" - "401": - $ref: "#/components/responses/WorkspaceUnauthenticated" - "403": - $ref: "#/components/responses/WorkspaceForbidden" - "404": - $ref: "#/components/responses/WorkspaceNotFound" - "503": - $ref: "#/components/responses/WorkspaceDependencyError" - /v3/workspaces/{workspaceId}/installations: - post: - operationId: installAgent - summary: Install and pin one published Agent version - description: >- - Selects the highest published version satisfying the exact submitted - SemVer constraint, then requires that exact version to have a published - trusted Release (or an explicitly marked pre-v4 legacy marker), - validates the accepted permission subset against that Card, and creates - one enabled immutable pin. It does not silently downgrade when the - selected version fails the Release gate. - A current enabled or disabled Installation for the same Agent conflicts. - security: - - bearerAuth: [] - parameters: - - $ref: "#/components/parameters/WorkspaceId" - requestBody: - required: true - x-nekiro-max-body-bytes: 1048576 - content: - application/json: - schema: - $ref: "#/components/schemas/InstallAgentRequest" - responses: - "201": - description: Agent installed in Workspace - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - content: - application/json: - schema: - $ref: ../schemas/installation.v2.schema.json - "400": - $ref: "#/components/responses/WorkspaceValidationError" - "401": - $ref: "#/components/responses/WorkspaceUnauthenticated" - "403": - $ref: "#/components/responses/WorkspaceInstallForbidden" - "404": - $ref: "#/components/responses/WorkspaceNotFound" - "409": - $ref: "#/components/responses/WorkspaceConflict" - "503": - $ref: "#/components/responses/WorkspaceDependencyError" - get: - operationId: listInstallations - summary: List current and historical Installations - description: >- - Returns at most the requested bounded page of enabled, disabled, and - uninstalled Installations ordered by installedAt and then - installationId ascending. The opaque cursor is bound to the Workspace - and page size. An existing owned Workspace with no records returns an - explicit empty items array without a cursor. - security: - - bearerAuth: [] - parameters: - - $ref: "#/components/parameters/WorkspaceId" - - $ref: "#/components/parameters/InstallationLimit" - - $ref: "#/components/parameters/InstallationCursor" - responses: - "200": - description: Bounded page of current and historical Installations - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - content: - application/json: - schema: - $ref: "#/components/schemas/InstallationList" - "400": - $ref: "#/components/responses/WorkspaceValidationError" - "401": - $ref: "#/components/responses/WorkspaceUnauthenticated" - "403": - $ref: "#/components/responses/WorkspaceForbidden" - "404": - $ref: "#/components/responses/WorkspaceNotFound" - "503": - $ref: "#/components/responses/WorkspaceDependencyError" - /v3/workspaces/{workspaceId}/installations/{installationId}: - get: - operationId: getInstallation - summary: Read one current or historical Installation - security: - - bearerAuth: [] - parameters: - - $ref: "#/components/parameters/WorkspaceId" - - $ref: "#/components/parameters/InstallationId" - responses: - "200": - description: Exact Installation - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - content: - application/json: - schema: - $ref: ../schemas/installation.v2.schema.json - "400": - $ref: "#/components/responses/WorkspaceValidationError" - "401": - $ref: "#/components/responses/WorkspaceUnauthenticated" - "403": - $ref: "#/components/responses/WorkspaceForbidden" - "404": - $ref: "#/components/responses/WorkspaceNotFound" - "503": - $ref: "#/components/responses/WorkspaceDependencyError" - patch: - operationId: updateInstallation - summary: Enable or disable a current Installation - description: >- - Applies exactly one enabled-to-disabled or disabled-to-enabled - transition. Same-state requests and every transition from uninstalled - conflict rather than returning idempotent success. - security: - - bearerAuth: [] - parameters: - - $ref: "#/components/parameters/WorkspaceId" - - $ref: "#/components/parameters/InstallationId" - requestBody: - required: true - x-nekiro-max-body-bytes: 1048576 - content: - application/json: - schema: - $ref: "#/components/schemas/UpdateInstallationRequest" - responses: - "200": - description: Updated installation - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - content: - application/json: - schema: - $ref: ../schemas/installation.v2.schema.json - "400": - $ref: "#/components/responses/WorkspaceValidationError" - "401": - $ref: "#/components/responses/WorkspaceUnauthenticated" - "403": - $ref: "#/components/responses/WorkspaceForbidden" - "404": - $ref: "#/components/responses/WorkspaceNotFound" - "409": - $ref: "#/components/responses/WorkspaceConflict" - "503": - $ref: "#/components/responses/WorkspaceDependencyError" - delete: - operationId: uninstallAgent - summary: Uninstall and preserve one disabled Installation - description: >- - Transitions only a disabled Installation to uninstalled and returns the - preserved terminal fact. Enabled and already-uninstalled records - conflict; uninstall is not idempotent. - security: - - bearerAuth: [] - parameters: - - $ref: "#/components/parameters/WorkspaceId" - - $ref: "#/components/parameters/InstallationId" - responses: - "200": - description: Preserved terminal Installation - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - content: - application/json: - schema: - $ref: ../schemas/installation.v2.schema.json - "400": - $ref: "#/components/responses/WorkspaceValidationError" - "401": - $ref: "#/components/responses/WorkspaceUnauthenticated" - "403": - $ref: "#/components/responses/WorkspaceForbidden" - "404": - $ref: "#/components/responses/WorkspaceNotFound" - "409": - $ref: "#/components/responses/WorkspaceConflict" - "503": - $ref: "#/components/responses/WorkspaceDependencyError" - /v3/workspaces/{workspaceId}/invocations: - post: - operationId: invokeAgent - summary: Invoke an installed Agent and return its transient result - description: >- - This POST is the only Northbound result channel. The request stream - field and Accept header MUST agree. stream=false requires - application/json or a compatible wildcard and returns one Invocation - Result. stream=true requires text/event-stream and returns ordered - Invocation Result Stream Event data values. A mismatch returns 406 - NOT_ACCEPTABLE. Once SSE is committed, failed, canceled, and timed-out - outcomes are in-band terminal events. EOF without a terminal event is - interrupted delivery. A JSON result and every 502, 503, or 504 response - MUST repeat the exact invocationId, rootTaskId, and traceId from the - Invocation context created for this request. Results are not persisted - or replayed. - security: - - bearerAuth: [] - parameters: - - $ref: "#/components/parameters/WorkspaceId" - - $ref: "#/components/parameters/ResultAccept" - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/InvokeAgentRequest" - responses: - "200": - description: Complete JSON result or committed SSE result stream selected by request mode - content: - application/json: - schema: - $ref: ../schemas/invocation-result.v1.schema.json - text/event-stream: - schema: - type: string - x-sse-data-schema: - $ref: ../schemas/invocation-result-stream-event.v1.schema.json - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthenticated" - "403": - $ref: "#/components/responses/Forbidden" - "404": - $ref: "#/components/responses/NotFound" - "406": - $ref: "#/components/responses/NotAcceptable" - "409": - $ref: "#/components/responses/Conflict" - "502": - $ref: "#/components/responses/AgentFailure" - "503": - $ref: "#/components/responses/Unavailable" - "504": - $ref: "#/components/responses/Timeout" - /v3/invocations/{invocationId}: - get: - operationId: getInvocation - summary: Read an Invocation and metadata-only Ledger facts - security: - - bearerAuth: [] - parameters: - - $ref: "#/components/parameters/InvocationId" - responses: - "200": - description: Invocation projection and append-only facts without Agent input or result content - content: - application/json: - schema: - type: object - additionalProperties: false - required: [invocation, events] - properties: - invocation: - $ref: "#/components/schemas/InvocationRecord" - events: - type: array - items: - $ref: ../schemas/invocation-event.v0.2.schema.json - "404": - $ref: "#/components/responses/ResourceNotFound" - "401": - $ref: "#/components/responses/Unauthenticated" - "403": - $ref: "#/components/responses/Forbidden" - "503": - $ref: "#/components/responses/DependencyUnavailable" - /v3/traces/{traceId}: - get: - operationId: getTrace - summary: Read metadata-only Invocation lineage - security: - - bearerAuth: [] - parameters: - - in: path - name: traceId - required: true - schema: - $ref: ../schemas/common.v1.schema.json#/$defs/traceId - responses: - "200": - description: Parent-child Invocation trace without Agent input or result content - content: - application/json: - schema: - type: object - additionalProperties: false - required: [traceId, invocations] - properties: - traceId: - $ref: ../schemas/common.v1.schema.json#/$defs/traceId - invocations: - type: array - items: - $ref: "#/components/schemas/InvocationRecord" - "404": - $ref: "#/components/responses/ResourceNotFound" - "401": - $ref: "#/components/responses/Unauthenticated" - "403": - $ref: "#/components/responses/Forbidden" - "503": - $ref: "#/components/responses/DependencyUnavailable" -components: - securitySchemes: - bearerAuth: - type: http - scheme: bearer - bearerFormat: opaque - description: >- - Gateway-verified bearer credential mapped to one trusted caller ID. - Public caller-ID headers are not an authentication source. - headers: - TraceID: - description: Gateway-assigned request trace identifier. - required: true - schema: - $ref: ../schemas/common.v1.schema.json#/$defs/traceId - parameters: - AgentId: - in: path - name: agentId - required: true - schema: - $ref: ../schemas/common.v1.schema.json#/$defs/agentId - Version: - in: path - name: version - required: true - schema: - $ref: ../schemas/common.v1.schema.json#/$defs/semver - WorkspaceId: - in: path - name: workspaceId - required: true - schema: - $ref: ../schemas/common.v1.schema.json#/$defs/workspaceId - InstallationId: - in: path - name: installationId - required: true - schema: - $ref: ../schemas/common.v1.schema.json#/$defs/installationId - InstallationLimit: - in: query - name: limit - required: true - description: >- - Required maximum number of Installations in one page. Values outside - 1-100 are validation failures. - schema: - type: integer - minimum: 1 - maximum: 100 - InstallationCursor: - in: query - name: cursor - required: false - description: >- - Opaque continuation bound to this Workspace, the page size, and the - last installedAt/installationId ordering tuple. Malformed or mismatched - cursors are validation failures and never restart traversal. - schema: - type: string - minLength: 1 - InvocationId: - in: path - name: invocationId - required: true - schema: - $ref: ../schemas/common.v1.schema.json#/$defs/invocationId - ResultAccept: - in: header - name: Accept - required: true - description: >- - application/json or a compatible wildcard for stream=false; - text/event-stream for stream=true. - schema: - type: string - minLength: 1 - responses: - CatalogValidationError: - description: Catalog request validation failed - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - x-platform-error-codes: [VALIDATION_ERROR] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - CatalogUnauthenticated: - description: A valid Gateway bearer identity is required - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - x-platform-error-codes: [UNAUTHENTICATED] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - CatalogForbidden: - description: The authenticated caller is not allowed to perform this Catalog operation - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - x-platform-error-codes: [FORBIDDEN] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - CatalogNotFound: - description: The exact Agent Card version does not exist - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - x-platform-error-codes: [NOT_FOUND] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - CatalogConflict: - description: The exact version already exists or the requested lifecycle transition is illegal - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - x-platform-error-codes: [CONFLICT] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - CatalogDependencyError: - description: The Catalog persistence dependency could not complete the operation - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - x-platform-error-codes: [DEPENDENCY_ERROR] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - WorkspaceValidationError: - description: Workspace request validation failed - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - x-platform-error-codes: [VALIDATION_ERROR] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v3.schema.json - WorkspaceUnauthenticated: - description: A valid Gateway bearer identity is required - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - x-platform-error-codes: [UNAUTHENTICATED] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v3.schema.json - WorkspaceForbidden: - description: The authenticated caller does not own the Workspace - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - x-platform-error-codes: [FORBIDDEN] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v3.schema.json - WorkspaceInstallForbidden: - description: The caller is forbidden or no matching published Release is installable - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - x-platform-error-codes: [FORBIDDEN, AGENT_RELEASE_UNPUBLISHED, AGENT_RELEASE_SUSPENDED, AGENT_RELEASE_REVOKED] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v3.schema.json - WorkspaceNotFound: - description: The requested Workspace, Installation, or published install candidate was not found - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - x-platform-error-codes: [NOT_FOUND] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v3.schema.json - WorkspaceConflict: - description: The Workspace or current Installation already exists, or the lifecycle transition is illegal - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - x-platform-error-codes: [CONFLICT] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v3.schema.json - WorkspaceDependencyError: - description: A required Workspace, Catalog, or persistence dependency failed - headers: - x-nek-trace-id: - $ref: "#/components/headers/TraceID" - x-platform-error-codes: [DEPENDENCY_ERROR] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v3.schema.json - ResourceNotFound: - description: Requested resource was not found - x-platform-error-codes: [NOT_FOUND] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - StateConflict: - description: Requested operation conflicts with current state - x-platform-error-codes: [CONFLICT] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - DependencyUnavailable: - description: A required platform dependency failed - x-platform-error-codes: [DEPENDENCY_ERROR] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - BadRequest: - description: Invalid request - x-platform-error-codes: [VALIDATION_ERROR] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - Unauthenticated: - description: Authentication is required - x-platform-error-codes: [UNAUTHENTICATED] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - Forbidden: - description: Installation, Agent state, or capability is not authorized - x-platform-error-codes: [FORBIDDEN, AGENT_DISABLED, CAPABILITY_NOT_ALLOWED] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - NotFound: - description: Requested Agent or Installation was not found - x-platform-error-codes: [NOT_FOUND, AGENT_NOT_INSTALLED] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - NotAcceptable: - description: Request result mode and Accept header do not agree - x-platform-error-codes: [NOT_ACCEPTABLE] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - Conflict: - description: Invocation was canceled or conflicts with current state before response commitment - x-platform-error-codes: [CONFLICT, CANCELED] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - AgentFailure: - description: Agent execution or A2A protocol failed before response commitment - x-platform-error-codes: [AGENT_EXECUTION_FAILED, A2A_PROTOCOL_ERROR] - x-platform-error-correlation: - source: created-invocation-context - exactFields: [invocationId, rootTaskId, traceId] - content: - application/json: - schema: - $ref: "#/components/schemas/CorrelatedPlatformError" - Unavailable: - description: Route, Agent, or required dependency is unavailable - x-platform-error-codes: [ROUTE_NOT_FOUND, AGENT_UNAVAILABLE, DEPENDENCY_ERROR] - x-platform-error-correlation: - source: created-invocation-context - exactFields: [invocationId, rootTaskId, traceId] - content: - application/json: - schema: - $ref: "#/components/schemas/CorrelatedPlatformError" - Timeout: - description: Invocation deadline expired before response commitment - x-platform-error-codes: [TIMEOUT] - x-platform-error-correlation: - source: created-invocation-context - exactFields: [invocationId, rootTaskId, traceId] - content: - application/json: - schema: - $ref: "#/components/schemas/CorrelatedPlatformError" - schemas: - CreateWorkspaceRequest: - type: object - additionalProperties: false - required: [workspaceId] - properties: - workspaceId: - $ref: ../schemas/common.v1.schema.json#/$defs/workspaceId - InstallAgentRequest: - type: object - additionalProperties: false - required: [agentId, versionConstraint, acceptedPermissions] - properties: - agentId: - $ref: ../schemas/common.v1.schema.json#/$defs/agentId - versionConstraint: - $ref: ../schemas/common.v1.schema.json#/$defs/semverRange - acceptedPermissions: - type: array - uniqueItems: true - items: - $ref: ../schemas/common.v1.schema.json#/$defs/permissionId - InstallationList: - type: object - additionalProperties: false - required: [items] - properties: - items: - type: array - maxItems: 100 - items: - $ref: ../schemas/installation.v2.schema.json - nextCursor: - type: string - minLength: 1 - UpdateInstallationRequest: - type: object - additionalProperties: false - required: [status] - properties: - status: - enum: [enabled, disabled] - CorrelatedPlatformError: - allOf: - - $ref: ../schemas/platform-error.v2.schema.json - - type: object - required: [invocationId, rootTaskId, traceId] - CatalogEntry: - type: object - additionalProperties: false - required: [card, publicationStatus, registeredAt] - dependentRequired: - publicAgentId: [publicUrl] - publicUrl: [publicAgentId] - properties: - card: - $ref: ../schemas/agent-card.v0.2.schema.json - publicationStatus: - enum: [draft, published, disabled] - registeredAt: - $ref: ../schemas/common.v1.schema.json#/$defs/dateTime - publishedAt: - $ref: ../schemas/common.v1.schema.json#/$defs/dateTime - publicAgentId: - type: string - pattern: "^agt_[0-9a-f]{32}$" - publicUrl: - type: string - format: uri - InvokeAgentRequest: - type: object - additionalProperties: false - required: [agentId, capability, input, stream] - properties: - agentId: - $ref: ../schemas/common.v1.schema.json#/$defs/agentId - capability: - $ref: ../schemas/common.v1.schema.json#/$defs/capabilityId - input: - $ref: ../schemas/common.v1.schema.json#/$defs/jsonObject - stream: - type: boolean - InvocationRecord: - type: object - additionalProperties: false - required: [invocationId, rootTaskId, traceId, caller, workspaceId, targetAgentId, agentCardVersion, capability, status, createdAt, updatedAt] - properties: - invocationId: - $ref: ../schemas/common.v1.schema.json#/$defs/invocationId - rootTaskId: - $ref: ../schemas/common.v1.schema.json#/$defs/taskId - parentInvocationId: - $ref: ../schemas/common.v1.schema.json#/$defs/invocationId - traceId: - $ref: ../schemas/common.v1.schema.json#/$defs/traceId - caller: - $ref: ../schemas/common.v1.schema.json#/$defs/caller - workspaceId: - $ref: ../schemas/common.v1.schema.json#/$defs/workspaceId - targetAgentId: - $ref: ../schemas/common.v1.schema.json#/$defs/agentId - agentCardVersion: - $ref: ../schemas/common.v1.schema.json#/$defs/semver - capability: - $ref: ../schemas/common.v1.schema.json#/$defs/capabilityId - status: - enum: [pending, routing, running, succeeded, failed, canceled, timed_out] - latencyMs: - type: integer - minimum: 0 - errorCode: - $ref: ../schemas/platform-error.v2.schema.json#/properties/code - createdAt: - $ref: ../schemas/common.v1.schema.json#/$defs/dateTime - updatedAt: - $ref: ../schemas/common.v1.schema.json#/$defs/dateTime diff --git a/contracts/openapi/public-agent-share.v1.yaml b/contracts/openapi/public-agent-share.v1.yaml index be525b12..782d92cc 100644 --- a/contracts/openapi/public-agent-share.v1.yaml +++ b/contracts/openapi/public-agent-share.v1.yaml @@ -6,7 +6,7 @@ info: servers: - url: https://api.nekiro.dev paths: - /v4/public/agents/{publicAgentId}: + /v1/public/agents/{publicAgentId}: get: operationId: resolvePublicAgent summary: Resolve one public Agent identity and its eligible trusted Releases diff --git a/contracts/openapi/router-internal.v1.yaml b/contracts/openapi/router-internal.v1.yaml index ccab834e..8e114147 100644 --- a/contracts/openapi/router-internal.v1.yaml +++ b/contracts/openapi/router-internal.v1.yaml @@ -1,199 +1,130 @@ openapi: 3.1.0 info: - title: NeKiro Router Internal API + title: NeKiro Router Internal Dispatch API version: 1.0.0 + description: >- + Router-owned dispatch boundary called only by authenticated Control Plane + Invocation Dispatch. The v1 route requires a published Agent Card with + managed HTTP Bearer authentication and Router Invocation Credential v1. servers: - - url: http://localhost:8081 + - url: https://a2a-router.internal.nekiro.dev + description: Explicit internal destination; no localhost or direct-Agent fallback paths: - /internal/v1/resolve-agent: - post: - operationId: resolveAgent - summary: Resolve an enabled installation to an exact Agent Card - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/ResolveAgentRequest" - responses: - "200": - description: Resolved Agent Card and authorization facts - content: - application/json: - schema: - $ref: "#/components/schemas/ResolveAgentResponse" - "403": - $ref: "#/components/responses/Error" - "404": - $ref: "#/components/responses/Error" /internal/v1/invocations: post: operationId: dispatchInvocation - summary: Dispatch an authorized invocation through A2A + summary: Accept an authorized root Invocation and deliver its live result + description: >- + Service authentication, strict shape/media/size validation, and initial + Ledger failure produce no Ledger fact or Agent request. Successful + created-event commit is acceptance. Router then appends routing and + re-resolves the exact Card through Control Plane Internal v1. Only + `http_bearer` is supported through Router Invocation Credential v1; + every other Card 0.2 type terminalizes from routing as + AGENT_AUTH_UNSUPPORTED with no Agent request. Clean success is sent + only after its terminal Ledger commit. A post-side-effect Ledger + failure is correlated DEPENDENCY_ERROR while durable history remains at + its last non-terminal fact; no terminal event or retry is fabricated. + security: + - serviceBearerAuth: [] + parameters: + - $ref: "#/components/parameters/ResultAccept" + x-nekiro-media-negotiation: invocation-result-v1 requestBody: required: true + x-nekiro-max-body-bytes-source: required-router-internal-request-limit + x-nekiro-limit-default: false content: application/json: - schema: - $ref: "#/components/schemas/DispatchInvocationRequest" - responses: - "202": - description: Router accepted invocation - content: - application/json: - schema: - type: object - additionalProperties: false - required: [invocationId, accepted] - properties: - invocationId: - $ref: ../schemas/common.v1.schema.json#/$defs/invocationId - accepted: - const: true - "400": - $ref: "#/components/responses/Error" - /internal/v1/invocations/{invocationId}: - get: - operationId: getRouterInvocation - parameters: - - in: path - name: invocationId - required: true - schema: - $ref: ../schemas/common.v1.schema.json#/$defs/invocationId + schema: { $ref: "#/components/schemas/DispatchInvocationRequest" } responses: "200": - description: Router-owned invocation events + description: JSON result or live SSE result according to request mode + headers: + x-nek-trace-id: { $ref: "#/components/headers/TraceID" } content: application/json: - schema: - type: array - items: - $ref: ../schemas/invocation-event.v0.1.schema.json - "404": - $ref: "#/components/responses/Error" - /internal/v1/invocations/{invocationId}/events: - get: - operationId: streamRouterInvocationEvents - summary: Stream append-only invocation events as Server-Sent Events - description: Each SSE data field contains one JSON RouterEventEnvelope. The stream ends after a terminal event. - parameters: - - in: path - name: invocationId - required: true - schema: - $ref: ../schemas/common.v1.schema.json#/$defs/invocationId - responses: - "200": - description: Invocation event stream - content: + schema: { $ref: ../schemas/invocation-result.v1.schema.json } text/event-stream: - schema: - type: string - x-sse-data-schema: - $ref: "#/components/schemas/RouterEventEnvelope" - "404": - $ref: "#/components/responses/Error" - /internal/v1/traces/{traceId}: - get: - operationId: getRouterTrace - parameters: - - in: path - name: traceId - required: true - schema: - $ref: ../schemas/common.v1.schema.json#/$defs/traceId - responses: - "200": - description: All invocation events in a trace - content: - application/json: - schema: - type: array - items: - $ref: ../schemas/invocation-event.v0.1.schema.json - "404": - $ref: "#/components/responses/Error" + schema: { type: string } + x-sse-data-schema: { $ref: ../schemas/invocation-result-stream-event.v2.schema.json } + x-nekiro-sse-framing: single-data-line-blank-line-flush + x-nekiro-max-event-bytes-source: required-router-sse-event-limit + x-nekiro-limit-default: false + x-nekiro-max-agent-response-bytes-source: required-router-agent-response-limit + x-nekiro-max-a2a-event-bytes-source: required-router-a2a-event-limit + x-nekiro-limit-default: false + "400": { $ref: "#/components/responses/BadRequest" } + "401": { $ref: "#/components/responses/Unauthenticated" } + "403": { $ref: "#/components/responses/Forbidden" } + "406": { $ref: "#/components/responses/NotAcceptable" } + "409": { $ref: "#/components/responses/Conflict" } + "413": { $ref: "#/components/responses/PayloadTooLarge" } + "500": { $ref: "#/components/responses/InternalFailure" } + "502": { $ref: "#/components/responses/AgentFailure" } + "503": { $ref: "#/components/responses/Unavailable" } + "504": { $ref: "#/components/responses/Timeout" } components: - responses: - Error: - description: Platform error - content: - application/json: - schema: - $ref: ../schemas/platform-error.v1.schema.json + securitySchemes: + serviceBearerAuth: + type: http + scheme: bearer + bearerFormat: opaque-service-credential + description: Explicit Control Plane service identity; Agent credentials are rejected before owned behavior. + parameters: + ResultAccept: + in: header + name: Accept + required: true + schema: { type: string, minLength: 1 } + headers: + TraceID: + description: Exactly one dispatch Trace identifier equal to the request Trace + required: true + schema: { $ref: ../schemas/common.v1.schema.json#/$defs/traceId } schemas: - ResolveAgentRequest: - type: object - additionalProperties: false - required: [workspaceId, agentId, version, capability] - properties: - workspaceId: - $ref: ../schemas/common.v1.schema.json#/$defs/workspaceId - agentId: - $ref: ../schemas/common.v1.schema.json#/$defs/agentId - version: - $ref: ../schemas/common.v1.schema.json#/$defs/semver - capability: - $ref: ../schemas/common.v1.schema.json#/$defs/capabilityId - ResolveAgentResponse: - type: object - additionalProperties: false - required: [card, installation] - properties: - card: - $ref: ../schemas/agent-card.v0.1.schema.json - installation: - type: object - additionalProperties: false - required: [installationId, workspaceId, agentId, installedVersion, acceptedPermissions, status] - properties: - installationId: - $ref: ../schemas/common.v1.schema.json#/$defs/installationId - workspaceId: - $ref: ../schemas/common.v1.schema.json#/$defs/workspaceId - agentId: - $ref: ../schemas/common.v1.schema.json#/$defs/agentId - installedVersion: - $ref: ../schemas/common.v1.schema.json#/$defs/semver - acceptedPermissions: - type: array - uniqueItems: true - items: - $ref: ../schemas/common.v1.schema.json#/$defs/permissionId - status: - const: enabled DispatchInvocationRequest: type: object additionalProperties: false required: [invocationId, rootTaskId, traceId, caller, workspaceId, targetAgentId, agentCardVersion, capability, input, stream] + oneOf: + - required: [agentReleaseId, agentCardDigest] + - not: + anyOf: + - required: [agentReleaseId] + - required: [agentCardDigest] properties: - invocationId: - $ref: ../schemas/common.v1.schema.json#/$defs/invocationId - rootTaskId: - $ref: ../schemas/common.v1.schema.json#/$defs/taskId - parentInvocationId: - $ref: ../schemas/common.v1.schema.json#/$defs/invocationId - traceId: - $ref: ../schemas/common.v1.schema.json#/$defs/traceId - caller: - $ref: ../schemas/common.v1.schema.json#/$defs/caller - workspaceId: - $ref: ../schemas/common.v1.schema.json#/$defs/workspaceId - targetAgentId: - $ref: ../schemas/common.v1.schema.json#/$defs/agentId - agentCardVersion: - $ref: ../schemas/common.v1.schema.json#/$defs/semver - capability: - $ref: ../schemas/common.v1.schema.json#/$defs/capabilityId - input: - $ref: ../schemas/common.v1.schema.json#/$defs/jsonObject - stream: - type: boolean - RouterEventEnvelope: - type: object - additionalProperties: false - required: [event] - properties: - event: - $ref: ../schemas/invocation-event.v0.1.schema.json + invocationId: { $ref: ../schemas/common.v1.schema.json#/$defs/invocationId } + rootTaskId: { $ref: ../schemas/common.v1.schema.json#/$defs/taskId } + traceId: { $ref: ../schemas/common.v1.schema.json#/$defs/traceId } + caller: { $ref: ../schemas/common.v1.schema.json#/$defs/caller } + workspaceId: { $ref: ../schemas/common.v1.schema.json#/$defs/workspaceId } + targetAgentId: { $ref: ../schemas/common.v1.schema.json#/$defs/agentId } + agentCardVersion: { $ref: ../schemas/common.v1.schema.json#/$defs/semver } + agentReleaseId: { $ref: ../schemas/common.v1.schema.json#/$defs/safeIdentifier } + agentCardDigest: { type: string, pattern: '^[0-9a-f]{64}$' } + capability: { $ref: ../schemas/common.v1.schema.json#/$defs/capabilityId } + input: { $ref: ../schemas/common.v1.schema.json#/$defs/jsonObject } + stream: { type: boolean } + PreCorrelationPlatformError: + $ref: ../schemas/platform-error.v4.schema.json#/$defs/preCorrelation + CorrelatedPlatformError: + $ref: ../schemas/platform-error.v4.schema.json#/$defs/correlated + PhasePlatformError: + x-nekiro-phase-boundary: successful-created-commit + x-nekiro-pre-acceptance-schema: PreCorrelationPlatformError + x-nekiro-post-acceptance-schema: CorrelatedPlatformError + oneOf: + - $ref: "#/components/schemas/PreCorrelationPlatformError" + - $ref: "#/components/schemas/CorrelatedPlatformError" + responses: + BadRequest: { description: Pre-acceptance invalid service request, headers: { x-nek-trace-id: { $ref: "#/components/headers/TraceID" } }, x-platform-error-codes: [VALIDATION_ERROR], content: { application/json: { schema: { $ref: "#/components/schemas/PreCorrelationPlatformError" } } } } + Unauthenticated: { description: Pre-acceptance missing/wrong service credential, headers: { x-nek-trace-id: { $ref: "#/components/headers/TraceID" } }, x-platform-error-codes: [UNAUTHENTICATED], content: { application/json: { schema: { $ref: "#/components/schemas/PreCorrelationPlatformError" } } } } + Forbidden: { description: May occur before/after acceptance; correlated shape is mandatory after acceptance, headers: { x-nek-trace-id: { $ref: "#/components/headers/TraceID" } }, x-platform-error-codes: [FORBIDDEN, AGENT_RELEASE_UNPUBLISHED, AGENT_RELEASE_SUSPENDED, AGENT_RELEASE_REVOKED, CAPABILITY_NOT_ALLOWED], content: { application/json: { schema: { $ref: "#/components/schemas/PhasePlatformError" } } } } + NotAcceptable: { description: Pre-acceptance result mode mismatch, headers: { x-nek-trace-id: { $ref: "#/components/headers/TraceID" } }, x-platform-error-codes: [NOT_ACCEPTABLE], content: { application/json: { schema: { $ref: "#/components/schemas/PreCorrelationPlatformError" } } } } + Conflict: { description: May occur before/after acceptance; correlated shape is mandatory after acceptance, headers: { x-nek-trace-id: { $ref: "#/components/headers/TraceID" } }, x-platform-error-codes: [CONFLICT, CANCELED], content: { application/json: { schema: { $ref: "#/components/schemas/PhasePlatformError" } } } } + PayloadTooLarge: { description: Pre-acceptance internal request overflow, headers: { x-nek-trace-id: { $ref: "#/components/headers/TraceID" } }, x-platform-error-codes: [PAYLOAD_TOO_LARGE], content: { application/json: { schema: { $ref: "#/components/schemas/PreCorrelationPlatformError" } } } } + InternalFailure: { description: Internal failure may occur before/after acceptance; correlated shape is mandatory after acceptance, headers: { x-nek-trace-id: { $ref: "#/components/headers/TraceID" } }, x-platform-error-codes: [INTERNAL_ERROR], content: { application/json: { schema: { $ref: "#/components/schemas/PhasePlatformError" } } } } + AgentFailure: { description: Post-acceptance exact Agent/protocol/auth/response-size failure, headers: { x-nek-trace-id: { $ref: "#/components/headers/TraceID" } }, x-platform-error-codes: [AGENT_AUTH_UNSUPPORTED, AGENT_RESPONSE_TOO_LARGE, AGENT_EXECUTION_FAILED, A2A_PROTOCOL_ERROR], content: { application/json: { schema: { $ref: "#/components/schemas/CorrelatedPlatformError" } } } } + Unavailable: { description: May occur before/after acceptance; correlated shape is mandatory after acceptance, headers: { x-nek-trace-id: { $ref: "#/components/headers/TraceID" } }, x-platform-error-codes: [ROUTE_NOT_FOUND, AGENT_UNAVAILABLE, DEPENDENCY_ERROR], content: { application/json: { schema: { $ref: "#/components/schemas/PhasePlatformError" } } } } + Timeout: { description: May occur before/after acceptance; correlated shape is mandatory after acceptance, headers: { x-nek-trace-id: { $ref: "#/components/headers/TraceID" } }, x-platform-error-codes: [TIMEOUT], content: { application/json: { schema: { $ref: "#/components/schemas/PhasePlatformError" } } } } diff --git a/contracts/openapi/router-internal.v2.yaml b/contracts/openapi/router-internal.v2.yaml deleted file mode 100644 index 3dbfc982..00000000 --- a/contracts/openapi/router-internal.v2.yaml +++ /dev/null @@ -1,256 +0,0 @@ -openapi: 3.1.0 -info: - title: NeKiro Router Internal API - version: 2.0.0 - description: >- - A2A Router-owned dispatch, transient result delivery, Ledger fact reads, - and trace reads called by the Control Plane. This document contains no - Control Plane-owned Agent resolution operation. -servers: - - url: https://a2a-router.internal.nekiro.dev - description: A2A Router internal destination -paths: - /internal/v2/invocations: - post: - operationId: dispatchInvocation - summary: Dispatch an authorized invocation and return its transient result - description: >- - The request stream field and Accept header MUST agree. stream=false - requires application/json or a compatible wildcard and returns one - Invocation Result. stream=true requires text/event-stream and returns - ordered Invocation Result Stream Event data values. A mismatch returns - 406 NOT_ACCEPTABLE. A JSON result and every 502, 503, or 504 response - MUST repeat the request invocationId, rootTaskId, and traceId exactly. - Results are not persisted or replayed. - parameters: - - $ref: "#/components/parameters/ResultAccept" - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/DispatchInvocationRequest" - responses: - "200": - description: Complete JSON result or committed SSE result stream selected by request mode - content: - application/json: - schema: - $ref: ../schemas/invocation-result.v1.schema.json - text/event-stream: - schema: - type: string - x-sse-data-schema: - $ref: ../schemas/invocation-result-stream-event.v1.schema.json - "400": - $ref: "#/components/responses/BadRequest" - "403": - $ref: "#/components/responses/Forbidden" - "404": - $ref: "#/components/responses/NotFound" - "406": - $ref: "#/components/responses/NotAcceptable" - "409": - $ref: "#/components/responses/Conflict" - "502": - $ref: "#/components/responses/AgentFailure" - "503": - $ref: "#/components/responses/Unavailable" - "504": - $ref: "#/components/responses/Timeout" - /internal/v2/invocations/{invocationId}: - get: - operationId: getRouterInvocation - summary: Read metadata-only Ledger facts for one Invocation - parameters: - - $ref: "#/components/parameters/InvocationId" - responses: - "200": - description: Append-only Invocation Event v0.2 facts without input or result content - content: - application/json: - schema: - type: array - items: - $ref: ../schemas/invocation-event.v0.2.schema.json - "404": - $ref: "#/components/responses/NotFound" - "503": - $ref: "#/components/responses/ReadDependencyUnavailable" - /internal/v2/invocations/{invocationId}/events: - get: - operationId: streamRouterInvocationEvents - summary: Stream metadata-only Ledger events - description: >- - Each SSE data field contains one RouterEventEnvelope with an Invocation - Event v0.2. This is an internal Ledger fact stream, not an Invocation - Result stream, and it never contains Agent input, result, or chunk data. - parameters: - - $ref: "#/components/parameters/InvocationId" - responses: - "200": - description: Append-only Invocation Event stream - content: - text/event-stream: - schema: - type: string - x-sse-data-schema: - $ref: "#/components/schemas/RouterEventEnvelope" - "404": - $ref: "#/components/responses/NotFound" - "503": - $ref: "#/components/responses/ReadDependencyUnavailable" - /internal/v2/traces/{traceId}: - get: - operationId: getRouterTrace - summary: Read metadata-only Ledger facts for a trace - parameters: - - in: path - name: traceId - required: true - schema: - $ref: ../schemas/common.v1.schema.json#/$defs/traceId - responses: - "200": - description: Invocation Event v0.2 facts for all Invocations in the trace - content: - application/json: - schema: - type: array - items: - $ref: ../schemas/invocation-event.v0.2.schema.json - "404": - $ref: "#/components/responses/NotFound" - "503": - $ref: "#/components/responses/ReadDependencyUnavailable" -components: - parameters: - ResultAccept: - in: header - name: Accept - required: true - description: >- - application/json or a compatible wildcard for stream=false; - text/event-stream for stream=true. - schema: - type: string - minLength: 1 - InvocationId: - in: path - name: invocationId - required: true - schema: - $ref: ../schemas/common.v1.schema.json#/$defs/invocationId - responses: - BadRequest: - description: Invalid dispatch request - x-platform-error-codes: [VALIDATION_ERROR] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - Forbidden: - description: Invocation is not authorized - x-platform-error-codes: [FORBIDDEN, CAPABILITY_NOT_ALLOWED] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - NotFound: - description: Invocation was not found - x-platform-error-codes: [NOT_FOUND] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - NotAcceptable: - description: Request result mode and Accept header do not agree - x-platform-error-codes: [NOT_ACCEPTABLE] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - Conflict: - description: Invocation was canceled or conflicts with current state before response commitment - x-platform-error-codes: [CONFLICT, CANCELED] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - AgentFailure: - description: Agent execution or A2A protocol failed before response commitment - x-platform-error-codes: [AGENT_EXECUTION_FAILED, A2A_PROTOCOL_ERROR] - x-platform-error-correlation: - source: request - exactFields: [invocationId, rootTaskId, traceId] - content: - application/json: - schema: - $ref: "#/components/schemas/CorrelatedPlatformError" - Unavailable: - description: Route, Agent, or required dependency is unavailable - x-platform-error-codes: [ROUTE_NOT_FOUND, AGENT_UNAVAILABLE, DEPENDENCY_ERROR] - x-platform-error-correlation: - source: request - exactFields: [invocationId, rootTaskId, traceId] - content: - application/json: - schema: - $ref: "#/components/schemas/CorrelatedPlatformError" - ReadDependencyUnavailable: - description: A required Ledger or trace-read dependency failed - x-platform-error-codes: [DEPENDENCY_ERROR] - content: - application/json: - schema: - $ref: ../schemas/platform-error.v2.schema.json - Timeout: - description: Invocation deadline expired before response commitment - x-platform-error-codes: [TIMEOUT] - x-platform-error-correlation: - source: request - exactFields: [invocationId, rootTaskId, traceId] - content: - application/json: - schema: - $ref: "#/components/schemas/CorrelatedPlatformError" - schemas: - CorrelatedPlatformError: - allOf: - - $ref: ../schemas/platform-error.v2.schema.json - - type: object - required: [invocationId, rootTaskId, traceId] - DispatchInvocationRequest: - type: object - additionalProperties: false - required: [invocationId, rootTaskId, traceId, caller, workspaceId, targetAgentId, agentCardVersion, capability, input, stream] - properties: - invocationId: - $ref: ../schemas/common.v1.schema.json#/$defs/invocationId - rootTaskId: - $ref: ../schemas/common.v1.schema.json#/$defs/taskId - parentInvocationId: - $ref: ../schemas/common.v1.schema.json#/$defs/invocationId - traceId: - $ref: ../schemas/common.v1.schema.json#/$defs/traceId - caller: - $ref: ../schemas/common.v1.schema.json#/$defs/caller - workspaceId: - $ref: ../schemas/common.v1.schema.json#/$defs/workspaceId - targetAgentId: - $ref: ../schemas/common.v1.schema.json#/$defs/agentId - agentCardVersion: - $ref: ../schemas/common.v1.schema.json#/$defs/semver - capability: - $ref: ../schemas/common.v1.schema.json#/$defs/capabilityId - input: - $ref: ../schemas/common.v1.schema.json#/$defs/jsonObject - stream: - type: boolean - RouterEventEnvelope: - type: object - additionalProperties: false - required: [event] - properties: - event: - $ref: ../schemas/invocation-event.v0.2.schema.json diff --git a/contracts/openapi/router-internal.v3.yaml b/contracts/openapi/router-internal.v3.yaml deleted file mode 100644 index 7aecade8..00000000 --- a/contracts/openapi/router-internal.v3.yaml +++ /dev/null @@ -1,208 +0,0 @@ -openapi: 3.1.0 -info: - title: NeKiro Router Internal API - version: 3.0.0 - description: >- - Router-owned service boundary called only by authenticated Control Plane - Invocation Dispatch. Agent SDK callers use router-agent.v1.yaml. -servers: - - url: https://a2a-router.internal.nekiro.dev - description: Explicit internal destination; no localhost or direct-Agent fallback -paths: - /internal/v3/invocations: - post: - operationId: dispatchInvocation - summary: Accept an authorized root Invocation and deliver its live result - description: >- - Service authentication, strict shape/media/size validation, and initial - Ledger failure produce no Ledger fact or Agent request. Successful - created-event commit is acceptance. Router then appends routing and - re-resolves the exact Card through Control Plane Internal v2. Card auth - none is supported; every other Card 0.2 type terminalizes from routing - as AGENT_AUTH_UNSUPPORTED with no Agent request. Clean success is sent - only after its terminal Ledger commit. A post-side-effect Ledger failure - is correlated DEPENDENCY_ERROR while durable history remains at its last - non-terminal fact; no terminal event or retry is fabricated. - security: - - serviceBearerAuth: [] - parameters: - - $ref: "#/components/parameters/ResultAccept" - x-nekiro-media-negotiation: invocation-result-v1 - requestBody: - required: true - x-nekiro-max-body-bytes-source: required-router-internal-request-limit - x-nekiro-limit-default: false - content: - application/json: - schema: { $ref: "#/components/schemas/DispatchInvocationRequest" } - responses: - "200": - description: JSON result or live SSE result according to request mode - content: - application/json: - schema: { $ref: ../schemas/invocation-result.v1.schema.json } - text/event-stream: - schema: { type: string } - x-sse-data-schema: { $ref: ../schemas/invocation-result-stream-event.v2.schema.json } - x-nekiro-sse-framing: single-data-line-blank-line-flush - x-nekiro-max-event-bytes-source: required-router-sse-event-limit - x-nekiro-limit-default: false - x-nekiro-max-agent-response-bytes-source: required-router-agent-response-limit - x-nekiro-max-a2a-event-bytes-source: required-router-a2a-event-limit - x-nekiro-limit-default: false - "400": { $ref: "#/components/responses/BadRequest" } - "401": { $ref: "#/components/responses/Unauthenticated" } - "403": { $ref: "#/components/responses/Forbidden" } - "406": { $ref: "#/components/responses/NotAcceptable" } - "409": { $ref: "#/components/responses/Conflict" } - "413": { $ref: "#/components/responses/PayloadTooLarge" } - "502": { $ref: "#/components/responses/AgentFailure" } - "503": { $ref: "#/components/responses/Unavailable" } - "504": { $ref: "#/components/responses/Timeout" } - /internal/v3/workspaces/{workspaceId}/invocations/{invocationId}: - get: - operationId: getRouterInvocation - security: [{ serviceBearerAuth: [] }] - parameters: - - $ref: "#/components/parameters/WorkspaceId" - - $ref: "#/components/parameters/InvocationId" - responses: - "200": - description: Workspace-scoped Invocation projection and ordered Event 0.3 facts - content: - application/json: - schema: { $ref: "#/components/schemas/InvocationDetailResponseV4" } - "401": { $ref: "#/components/responses/Unauthenticated" } - "403": { $ref: "#/components/responses/Forbidden" } - "404": { $ref: "#/components/responses/NotFound" } - "503": { $ref: "#/components/responses/Unavailable" } - /internal/v3/workspaces/{workspaceId}/traces/{traceId}: - get: - operationId: getRouterTrace - security: [{ serviceBearerAuth: [] }] - parameters: - - $ref: "#/components/parameters/WorkspaceId" - - in: path - name: traceId - required: true - schema: { $ref: ../schemas/common.v1.schema.json#/$defs/traceId } - responses: - "200": - description: Workspace-scoped ordered Invocation lineage projections for a trace - content: - application/json: - schema: { $ref: "#/components/schemas/TraceResponseV4" } - "401": { $ref: "#/components/responses/Unauthenticated" } - "403": { $ref: "#/components/responses/Forbidden" } - "404": { $ref: "#/components/responses/NotFound" } - "503": { $ref: "#/components/responses/Unavailable" } -components: - securitySchemes: - serviceBearerAuth: - type: http - scheme: bearer - bearerFormat: opaque-service-credential - description: Explicit Control Plane service identity; Agent credentials are rejected before owned behavior. - parameters: - ResultAccept: - in: header - name: Accept - required: true - schema: { type: string, minLength: 1 } - InvocationId: - in: path - name: invocationId - required: true - schema: { $ref: ../schemas/common.v1.schema.json#/$defs/invocationId } - WorkspaceId: - in: path - name: workspaceId - required: true - schema: { $ref: ../schemas/common.v1.schema.json#/$defs/workspaceId } - schemas: - DispatchInvocationRequest: - type: object - additionalProperties: false - required: [invocationId, rootTaskId, traceId, caller, workspaceId, targetAgentId, agentCardVersion, capability, input, stream] - oneOf: - - required: [agentReleaseId, agentCardDigest] - - not: - anyOf: - - required: [agentReleaseId] - - required: [agentCardDigest] - properties: - invocationId: { $ref: ../schemas/common.v1.schema.json#/$defs/invocationId } - rootTaskId: { $ref: ../schemas/common.v1.schema.json#/$defs/taskId } - traceId: { $ref: ../schemas/common.v1.schema.json#/$defs/traceId } - caller: { $ref: ../schemas/common.v1.schema.json#/$defs/caller } - workspaceId: { $ref: ../schemas/common.v1.schema.json#/$defs/workspaceId } - targetAgentId: { $ref: ../schemas/common.v1.schema.json#/$defs/agentId } - agentCardVersion: { $ref: ../schemas/common.v1.schema.json#/$defs/semver } - agentReleaseId: { $ref: ../schemas/common.v1.schema.json#/$defs/safeIdentifier } - agentCardDigest: { type: string, pattern: '^[0-9a-f]{64}$' } - capability: { $ref: ../schemas/common.v1.schema.json#/$defs/capabilityId } - input: { $ref: ../schemas/common.v1.schema.json#/$defs/jsonObject } - stream: { type: boolean } - InvocationRecordV4: - type: object - additionalProperties: false - required: [invocationId, rootTaskId, traceId, caller, workspaceId, targetAgentId, agentCardVersion, capability, status, createdAt, updatedAt] - oneOf: - - required: [agentReleaseId, agentCardDigest] - - not: - anyOf: - - required: [agentReleaseId] - - required: [agentCardDigest] - properties: - invocationId: { $ref: ../schemas/common.v1.schema.json#/$defs/invocationId } - rootTaskId: { $ref: ../schemas/common.v1.schema.json#/$defs/taskId } - parentInvocationId: { $ref: ../schemas/common.v1.schema.json#/$defs/invocationId } - traceId: { $ref: ../schemas/common.v1.schema.json#/$defs/traceId } - caller: { $ref: ../schemas/common.v1.schema.json#/$defs/caller } - workspaceId: { $ref: ../schemas/common.v1.schema.json#/$defs/workspaceId } - targetAgentId: { $ref: ../schemas/common.v1.schema.json#/$defs/agentId } - agentCardVersion: { $ref: ../schemas/common.v1.schema.json#/$defs/semver } - agentReleaseId: { $ref: ../schemas/common.v1.schema.json#/$defs/safeIdentifier } - agentCardDigest: { type: string, pattern: '^[0-9a-f]{64}$' } - capability: { $ref: ../schemas/common.v1.schema.json#/$defs/capabilityId } - status: { enum: [pending, routing, running, succeeded, failed, canceled, timed_out] } - latencyMs: { type: integer, minimum: 0 } - errorCode: { $ref: ../schemas/platform-error.v4.schema.json#/$defs/errorCode } - createdAt: { $ref: ../schemas/common.v1.schema.json#/$defs/dateTime } - updatedAt: { $ref: ../schemas/common.v1.schema.json#/$defs/dateTime } - InvocationDetailResponseV4: - type: object - additionalProperties: false - required: [invocation, events] - properties: - invocation: { $ref: "#/components/schemas/InvocationRecordV4" } - events: { type: array, items: { $ref: ../schemas/invocation-event.v0.3.schema.json } } - TraceResponseV4: - type: object - additionalProperties: false - required: [traceId, invocations] - properties: - traceId: { $ref: ../schemas/common.v1.schema.json#/$defs/traceId } - invocations: { type: array, items: { $ref: "#/components/schemas/InvocationRecordV4" } } - PreCorrelationPlatformError: - $ref: ../schemas/platform-error.v4.schema.json#/$defs/preCorrelation - CorrelatedPlatformError: - $ref: ../schemas/platform-error.v4.schema.json#/$defs/correlated - PhasePlatformError: - x-nekiro-phase-boundary: successful-created-commit - x-nekiro-pre-acceptance-schema: PreCorrelationPlatformError - x-nekiro-post-acceptance-schema: CorrelatedPlatformError - oneOf: - - $ref: "#/components/schemas/PreCorrelationPlatformError" - - $ref: "#/components/schemas/CorrelatedPlatformError" - responses: - BadRequest: { description: Pre-acceptance invalid service request, x-platform-error-codes: [VALIDATION_ERROR], content: { application/json: { schema: { $ref: "#/components/schemas/PreCorrelationPlatformError" } } } } - Unauthenticated: { description: Pre-acceptance missing/wrong service credential, x-platform-error-codes: [UNAUTHENTICATED], content: { application/json: { schema: { $ref: "#/components/schemas/PreCorrelationPlatformError" } } } } - Forbidden: { description: May occur before/after acceptance; correlated shape is mandatory after acceptance, x-platform-error-codes: [FORBIDDEN, AGENT_RELEASE_UNPUBLISHED, AGENT_RELEASE_SUSPENDED, AGENT_RELEASE_REVOKED, CAPABILITY_NOT_ALLOWED], content: { application/json: { schema: { $ref: "#/components/schemas/PhasePlatformError" } } } } - NotFound: { description: Pre-correlation metadata resource not found, x-platform-error-codes: [NOT_FOUND], content: { application/json: { schema: { $ref: "#/components/schemas/PreCorrelationPlatformError" } } } } - NotAcceptable: { description: Pre-acceptance result mode mismatch, x-platform-error-codes: [NOT_ACCEPTABLE], content: { application/json: { schema: { $ref: "#/components/schemas/PreCorrelationPlatformError" } } } } - Conflict: { description: May occur before/after acceptance; correlated shape is mandatory after acceptance, x-platform-error-codes: [CONFLICT, CANCELED], content: { application/json: { schema: { $ref: "#/components/schemas/PhasePlatformError" } } } } - PayloadTooLarge: { description: Pre-acceptance internal request overflow, x-platform-error-codes: [PAYLOAD_TOO_LARGE], content: { application/json: { schema: { $ref: "#/components/schemas/PreCorrelationPlatformError" } } } } - AgentFailure: { description: Post-acceptance exact Agent/protocol/auth/response-size failure, x-platform-error-codes: [AGENT_AUTH_UNSUPPORTED, AGENT_RESPONSE_TOO_LARGE, AGENT_EXECUTION_FAILED, A2A_PROTOCOL_ERROR], content: { application/json: { schema: { $ref: "#/components/schemas/CorrelatedPlatformError" } } } } - Unavailable: { description: May occur before/after acceptance; correlated shape is mandatory after acceptance, x-platform-error-codes: [ROUTE_NOT_FOUND, AGENT_UNAVAILABLE, DEPENDENCY_ERROR], content: { application/json: { schema: { $ref: "#/components/schemas/PhasePlatformError" } } } } - Timeout: { description: May occur before/after acceptance; correlated shape is mandatory after acceptance, x-platform-error-codes: [TIMEOUT], content: { application/json: { schema: { $ref: "#/components/schemas/PhasePlatformError" } } } } diff --git a/contracts/openapi/router-internal.v4.yaml b/contracts/openapi/router-internal.v4.yaml deleted file mode 100644 index a60df2d8..00000000 --- a/contracts/openapi/router-internal.v4.yaml +++ /dev/null @@ -1,132 +0,0 @@ -openapi: 3.1.0 -info: - title: NeKiro Router Internal Dispatch API - version: 4.0.0 - description: >- - Router-owned dispatch boundary called only by authenticated Control Plane - Invocation Dispatch. Router Internal metadata reads remain on v3. The v4 - route requires a published Agent Card with managed HTTP Bearer - authentication and Router Invocation Credential v1; the retired v3 - dispatch route is not served. -servers: - - url: https://a2a-router.internal.nekiro.dev - description: Explicit internal destination; no localhost or direct-Agent fallback -paths: - /internal/v4/invocations: - post: - operationId: dispatchInvocation - summary: Accept an authorized root Invocation and deliver its live result - description: >- - Service authentication, strict shape/media/size validation, and initial - Ledger failure produce no Ledger fact or Agent request. Successful - created-event commit is acceptance. Router then appends routing and - re-resolves the exact Card through Control Plane Internal v2. Only - `http_bearer` is supported through Router Invocation Credential v1; - every other Card 0.2 type terminalizes from routing as - AGENT_AUTH_UNSUPPORTED with no Agent request. Clean success is sent - only after its terminal Ledger commit. A post-side-effect Ledger - failure is correlated DEPENDENCY_ERROR while durable history remains at - its last non-terminal fact; no terminal event or retry is fabricated. - security: - - serviceBearerAuth: [] - parameters: - - $ref: "#/components/parameters/ResultAccept" - x-nekiro-media-negotiation: invocation-result-v1 - requestBody: - required: true - x-nekiro-max-body-bytes-source: required-router-internal-request-limit - x-nekiro-limit-default: false - content: - application/json: - schema: { $ref: "#/components/schemas/DispatchInvocationRequest" } - responses: - "200": - description: JSON result or live SSE result according to request mode - headers: - x-nek-trace-id: { $ref: "#/components/headers/TraceID" } - content: - application/json: - schema: { $ref: ../schemas/invocation-result.v1.schema.json } - text/event-stream: - schema: { type: string } - x-sse-data-schema: { $ref: ../schemas/invocation-result-stream-event.v2.schema.json } - x-nekiro-sse-framing: single-data-line-blank-line-flush - x-nekiro-max-event-bytes-source: required-router-sse-event-limit - x-nekiro-limit-default: false - x-nekiro-max-agent-response-bytes-source: required-router-agent-response-limit - x-nekiro-max-a2a-event-bytes-source: required-router-a2a-event-limit - x-nekiro-limit-default: false - "400": { $ref: "#/components/responses/BadRequest" } - "401": { $ref: "#/components/responses/Unauthenticated" } - "403": { $ref: "#/components/responses/Forbidden" } - "406": { $ref: "#/components/responses/NotAcceptable" } - "409": { $ref: "#/components/responses/Conflict" } - "413": { $ref: "#/components/responses/PayloadTooLarge" } - "500": { $ref: "#/components/responses/InternalFailure" } - "502": { $ref: "#/components/responses/AgentFailure" } - "503": { $ref: "#/components/responses/Unavailable" } - "504": { $ref: "#/components/responses/Timeout" } -components: - securitySchemes: - serviceBearerAuth: - type: http - scheme: bearer - bearerFormat: opaque-service-credential - description: Explicit Control Plane service identity; Agent credentials are rejected before owned behavior. - parameters: - ResultAccept: - in: header - name: Accept - required: true - schema: { type: string, minLength: 1 } - headers: - TraceID: - description: Exactly one dispatch Trace identifier equal to the request Trace - required: true - schema: { $ref: ../schemas/common.v1.schema.json#/$defs/traceId } - schemas: - DispatchInvocationRequest: - type: object - additionalProperties: false - required: [invocationId, rootTaskId, traceId, caller, workspaceId, targetAgentId, agentCardVersion, capability, input, stream] - oneOf: - - required: [agentReleaseId, agentCardDigest] - - not: - anyOf: - - required: [agentReleaseId] - - required: [agentCardDigest] - properties: - invocationId: { $ref: ../schemas/common.v1.schema.json#/$defs/invocationId } - rootTaskId: { $ref: ../schemas/common.v1.schema.json#/$defs/taskId } - traceId: { $ref: ../schemas/common.v1.schema.json#/$defs/traceId } - caller: { $ref: ../schemas/common.v1.schema.json#/$defs/caller } - workspaceId: { $ref: ../schemas/common.v1.schema.json#/$defs/workspaceId } - targetAgentId: { $ref: ../schemas/common.v1.schema.json#/$defs/agentId } - agentCardVersion: { $ref: ../schemas/common.v1.schema.json#/$defs/semver } - agentReleaseId: { $ref: ../schemas/common.v1.schema.json#/$defs/safeIdentifier } - agentCardDigest: { type: string, pattern: '^[0-9a-f]{64}$' } - capability: { $ref: ../schemas/common.v1.schema.json#/$defs/capabilityId } - input: { $ref: ../schemas/common.v1.schema.json#/$defs/jsonObject } - stream: { type: boolean } - PreCorrelationPlatformError: - $ref: ../schemas/platform-error.v4.schema.json#/$defs/preCorrelation - CorrelatedPlatformError: - $ref: ../schemas/platform-error.v4.schema.json#/$defs/correlated - PhasePlatformError: - x-nekiro-phase-boundary: successful-created-commit - x-nekiro-pre-acceptance-schema: PreCorrelationPlatformError - x-nekiro-post-acceptance-schema: CorrelatedPlatformError - oneOf: - - $ref: "#/components/schemas/PreCorrelationPlatformError" - - $ref: "#/components/schemas/CorrelatedPlatformError" - responses: - BadRequest: { description: Pre-acceptance invalid service request, headers: { x-nek-trace-id: { $ref: "#/components/headers/TraceID" } }, x-platform-error-codes: [VALIDATION_ERROR], content: { application/json: { schema: { $ref: "#/components/schemas/PreCorrelationPlatformError" } } } } - Unauthenticated: { description: Pre-acceptance missing/wrong service credential, headers: { x-nek-trace-id: { $ref: "#/components/headers/TraceID" } }, x-platform-error-codes: [UNAUTHENTICATED], content: { application/json: { schema: { $ref: "#/components/schemas/PreCorrelationPlatformError" } } } } - Forbidden: { description: May occur before/after acceptance; correlated shape is mandatory after acceptance, headers: { x-nek-trace-id: { $ref: "#/components/headers/TraceID" } }, x-platform-error-codes: [FORBIDDEN, AGENT_RELEASE_UNPUBLISHED, AGENT_RELEASE_SUSPENDED, AGENT_RELEASE_REVOKED, CAPABILITY_NOT_ALLOWED], content: { application/json: { schema: { $ref: "#/components/schemas/PhasePlatformError" } } } } - NotAcceptable: { description: Pre-acceptance result mode mismatch, headers: { x-nek-trace-id: { $ref: "#/components/headers/TraceID" } }, x-platform-error-codes: [NOT_ACCEPTABLE], content: { application/json: { schema: { $ref: "#/components/schemas/PreCorrelationPlatformError" } } } } - Conflict: { description: May occur before/after acceptance; correlated shape is mandatory after acceptance, headers: { x-nek-trace-id: { $ref: "#/components/headers/TraceID" } }, x-platform-error-codes: [CONFLICT, CANCELED], content: { application/json: { schema: { $ref: "#/components/schemas/PhasePlatformError" } } } } - PayloadTooLarge: { description: Pre-acceptance internal request overflow, headers: { x-nek-trace-id: { $ref: "#/components/headers/TraceID" } }, x-platform-error-codes: [PAYLOAD_TOO_LARGE], content: { application/json: { schema: { $ref: "#/components/schemas/PreCorrelationPlatformError" } } } } - InternalFailure: { description: Internal failure may occur before/after acceptance; correlated shape is mandatory after acceptance, headers: { x-nek-trace-id: { $ref: "#/components/headers/TraceID" } }, x-platform-error-codes: [INTERNAL_ERROR], content: { application/json: { schema: { $ref: "#/components/schemas/PhasePlatformError" } } } } - AgentFailure: { description: Post-acceptance exact Agent/protocol/auth/response-size failure, headers: { x-nek-trace-id: { $ref: "#/components/headers/TraceID" } }, x-platform-error-codes: [AGENT_AUTH_UNSUPPORTED, AGENT_RESPONSE_TOO_LARGE, AGENT_EXECUTION_FAILED, A2A_PROTOCOL_ERROR], content: { application/json: { schema: { $ref: "#/components/schemas/CorrelatedPlatformError" } } } } - Unavailable: { description: May occur before/after acceptance; correlated shape is mandatory after acceptance, headers: { x-nek-trace-id: { $ref: "#/components/headers/TraceID" } }, x-platform-error-codes: [ROUTE_NOT_FOUND, AGENT_UNAVAILABLE, DEPENDENCY_ERROR], content: { application/json: { schema: { $ref: "#/components/schemas/PhasePlatformError" } } } } - Timeout: { description: May occur before/after acceptance; correlated shape is mandatory after acceptance, headers: { x-nek-trace-id: { $ref: "#/components/headers/TraceID" } }, x-platform-error-codes: [TIMEOUT], content: { application/json: { schema: { $ref: "#/components/schemas/PhasePlatformError" } } } } diff --git a/contracts/openapi/router-metadata.v3.yaml b/contracts/openapi/router-metadata.v1.yaml similarity index 94% rename from contracts/openapi/router-metadata.v3.yaml rename to contracts/openapi/router-metadata.v1.yaml index b65faa17..261c8f82 100644 --- a/contracts/openapi/router-metadata.v3.yaml +++ b/contracts/openapi/router-metadata.v1.yaml @@ -1,16 +1,16 @@ openapi: 3.1.0 info: title: NeKiro Router Internal Metadata API - version: 3.0.0 + version: 1.0.0 description: >- Router-owned Workspace-scoped Invocation and Trace metadata reads called only by the authenticated Control Plane. Dispatch is owned separately by - Router Internal Dispatch API v4. + Router Internal Dispatch API v1. servers: - url: https://a2a-router.internal.nekiro.dev description: Explicit internal destination; no localhost fallback paths: - /internal/v3/workspaces/{workspaceId}/invocations/{invocationId}: + /internal/v1/workspaces/{workspaceId}/invocations/{invocationId}: get: operationId: getRouterInvocation security: [{ serviceBearerAuth: [] }] @@ -22,12 +22,12 @@ paths: description: Workspace-scoped Invocation projection and ordered Event 0.3 facts content: application/json: - schema: { $ref: "#/components/schemas/InvocationDetailResponseV4" } + schema: { $ref: "#/components/schemas/InvocationDetailResponseV1" } "401": { $ref: "#/components/responses/Unauthenticated" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "503": { $ref: "#/components/responses/Unavailable" } - /internal/v3/workspaces/{workspaceId}/traces/{traceId}: + /internal/v1/workspaces/{workspaceId}/traces/{traceId}: get: operationId: getRouterTrace security: [{ serviceBearerAuth: [] }] @@ -42,7 +42,7 @@ paths: description: Workspace-scoped ordered Invocation lineage projections for a trace content: application/json: - schema: { $ref: "#/components/schemas/TraceResponseV4" } + schema: { $ref: "#/components/schemas/TraceResponseV1" } "401": { $ref: "#/components/responses/Unauthenticated" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } @@ -66,7 +66,7 @@ components: required: true schema: { $ref: ../schemas/common.v1.schema.json#/$defs/workspaceId } schemas: - InvocationRecordV4: + InvocationRecordV1: type: object additionalProperties: false required: [invocationId, rootTaskId, traceId, caller, workspaceId, targetAgentId, agentCardVersion, capability, status, createdAt, updatedAt] @@ -93,20 +93,20 @@ components: errorCode: { $ref: ../schemas/platform-error.v4.schema.json#/$defs/errorCode } createdAt: { $ref: ../schemas/common.v1.schema.json#/$defs/dateTime } updatedAt: { $ref: ../schemas/common.v1.schema.json#/$defs/dateTime } - InvocationDetailResponseV4: + InvocationDetailResponseV1: type: object additionalProperties: false required: [invocation, events] properties: - invocation: { $ref: "#/components/schemas/InvocationRecordV4" } + invocation: { $ref: "#/components/schemas/InvocationRecordV1" } events: { type: array, items: { $ref: ../schemas/invocation-event.v0.3.schema.json } } - TraceResponseV4: + TraceResponseV1: type: object additionalProperties: false required: [traceId, invocations] properties: traceId: { $ref: ../schemas/common.v1.schema.json#/$defs/traceId } - invocations: { type: array, items: { $ref: "#/components/schemas/InvocationRecordV4" } } + invocations: { type: array, items: { $ref: "#/components/schemas/InvocationRecordV1" } } PreCorrelationPlatformError: $ref: ../schemas/platform-error.v4.schema.json#/$defs/preCorrelation responses: diff --git a/contracts/openapi/trusted-publication.v1.yaml b/contracts/openapi/trusted-publication.v1.yaml index 75a72587..d650b174 100644 --- a/contracts/openapi/trusted-publication.v1.yaml +++ b/contracts/openapi/trusted-publication.v1.yaml @@ -3,7 +3,7 @@ info: title: NeKiro Trusted Publication API version: 1.0.0 paths: - /v4/providers/{providerId}/agents/{agentId}/releases: + /v1/providers/{providerId}/agents/{agentId}/releases: post: operationId: createAgentRelease security: [{ bearerAuth: [] }] @@ -24,7 +24,7 @@ paths: '409': { $ref: '#/components/responses/TrustedPublicationError' } '503': { $ref: '#/components/responses/TrustedPublicationError' } '500': { $ref: '#/components/responses/TrustedPublicationError' } - /v4/releases/{releaseId}: + /v1/releases/{releaseId}: get: operationId: getAgentRelease security: [{ bearerAuth: [] }] @@ -36,7 +36,7 @@ paths: '404': { $ref: '#/components/responses/TrustedPublicationError' } '503': { $ref: '#/components/responses/TrustedPublicationError' } '500': { $ref: '#/components/responses/TrustedPublicationError' } - /v4/releases/{releaseId}/verify: + /v1/releases/{releaseId}/verify: post: operationId: verifyAgentRelease security: [{ bearerAuth: [] }] @@ -49,7 +49,7 @@ paths: '409': { $ref: '#/components/responses/TrustedPublicationError' } '503': { $ref: '#/components/responses/TrustedPublicationError' } '500': { $ref: '#/components/responses/TrustedPublicationError' } - /v4/releases/{releaseId}/publish: + /v1/releases/{releaseId}/publish: post: operationId: publishAgentRelease security: [{ bearerAuth: [] }] @@ -62,7 +62,7 @@ paths: '409': { $ref: '#/components/responses/TrustedPublicationError' } '503': { $ref: '#/components/responses/TrustedPublicationError' } '500': { $ref: '#/components/responses/TrustedPublicationError' } - /v4/releases/{releaseId}/suspend: + /v1/releases/{releaseId}/suspend: post: operationId: suspendAgentRelease security: [{ bearerAuth: [] }] @@ -75,7 +75,7 @@ paths: '409': { $ref: '#/components/responses/TrustedPublicationError' } '503': { $ref: '#/components/responses/TrustedPublicationError' } '500': { $ref: '#/components/responses/TrustedPublicationError' } - /v4/releases/{releaseId}/revoke: + /v1/releases/{releaseId}/revoke: post: operationId: revokeAgentRelease security: [{ bearerAuth: [] }] @@ -88,7 +88,7 @@ paths: '409': { $ref: '#/components/responses/TrustedPublicationError' } '503': { $ref: '#/components/responses/TrustedPublicationError' } '500': { $ref: '#/components/responses/TrustedPublicationError' } - /v4/providers/{providerId}/agents/{agentId}/endpoint-bindings: + /v1/providers/{providerId}/agents/{agentId}/endpoint-bindings: post: operationId: createEndpointBinding security: [{ bearerAuth: [] }] @@ -109,7 +109,7 @@ paths: '409': { $ref: '#/components/responses/TrustedPublicationError' } '503': { $ref: '#/components/responses/TrustedPublicationError' } '500': { $ref: '#/components/responses/TrustedPublicationError' } - /v4/providers/{providerId}/endpoint-bindings/{bindingId}: + /v1/providers/{providerId}/endpoint-bindings/{bindingId}: get: operationId: getEndpointBinding security: [{ bearerAuth: [] }] @@ -124,7 +124,7 @@ paths: '400': { $ref: '#/components/responses/TrustedPublicationError' } '503': { $ref: '#/components/responses/TrustedPublicationError' } '500': { $ref: '#/components/responses/TrustedPublicationError' } - /v4/providers/{providerId}/endpoint-bindings/{bindingId}/challenges: + /v1/providers/{providerId}/endpoint-bindings/{bindingId}/challenges: post: operationId: createVerificationChallenge security: [{ bearerAuth: [] }] @@ -140,7 +140,7 @@ paths: '409': { $ref: '#/components/responses/TrustedPublicationError' } '503': { $ref: '#/components/responses/TrustedPublicationError' } '500': { $ref: '#/components/responses/TrustedPublicationError' } - /v4/providers/{providerId}/endpoint-bindings/{bindingId}/challenges/{challengeId}/complete: + /v1/providers/{providerId}/endpoint-bindings/{bindingId}/challenges/{challengeId}/complete: post: operationId: completeVerificationChallenge security: [{ bearerAuth: [] }] diff --git a/contracts/public_agent_share_contracts_test.go b/contracts/public_agent_share_contracts_test.go index fdb4c25b..45c04954 100644 --- a/contracts/public_agent_share_contracts_test.go +++ b/contracts/public_agent_share_contracts_test.go @@ -9,7 +9,7 @@ import ( func TestPublicAgentShareOpenAPIAndSchemaMapping(t *testing.T) { document := loadOpenAPIDocument(t, filepath.Join("openapi", "public-agent-share.v1.yaml")) - operation := document.Paths.Find("/v4/public/agents/{publicAgentId}").Get + operation := document.Paths.Find("/v1/public/agents/{publicAgentId}").Get if operation == nil || operation.Security == nil || len(*operation.Security) != 0 { t.Fatal("public resolution must be explicitly anonymous") } @@ -37,7 +37,7 @@ func TestPublicAgentShareOpenAPIAndSchemaMapping(t *testing.T) { } func TestCatalogEntryPublicIdentityFieldsArePairedInOpenAPI(t *testing.T) { - document := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane.v3.yaml")) + document := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane.v1.yaml")) schema := document.Components.Schemas["CatalogEntry"].Value if len(schema.DependentRequired) != 2 { t.Fatal("CatalogEntry public identity fields must be paired") diff --git a/contracts/result_api_contracts_test.go b/contracts/result_api_contracts_test.go index ab3dc40d..6d90b1e4 100644 --- a/contracts/result_api_contracts_test.go +++ b/contracts/result_api_contracts_test.go @@ -72,33 +72,37 @@ func TestInvocationEventV02RejectsMismatchedErrorCorrelation(t *testing.T) { } func TestDirectionalOpenAPIOwnership(t *testing.T) { - controlPlane := loadResultOpenAPIDocument(t, filepath.Join("openapi", "control-plane-internal.v2.yaml")) - router := loadResultOpenAPIDocument(t, filepath.Join("openapi", "router-internal.v2.yaml")) + controlPlane := loadResultOpenAPIDocument(t, filepath.Join("openapi", "control-plane-internal.v1.yaml")) + router := loadResultOpenAPIDocument(t, filepath.Join("openapi", "router-internal.v1.yaml")) + metadata := loadResultOpenAPIDocument(t, filepath.Join("openapi", "router-metadata.v1.yaml")) - if controlPlane.Paths.Len() != 1 || controlPlane.Paths.Find("/internal/v2/resolve-agent") == nil { + if controlPlane.Paths.Len() != 1 || controlPlane.Paths.Find("/internal/v1/resolve-agent") == nil { t.Fatalf("Control Plane internal paths = %v, want resolution only", controlPlane.Paths.Keys()) } - if controlPlane.Paths.Find("/internal/v2/invocations") != nil { + if controlPlane.Paths.Find("/internal/v1/invocations") != nil { t.Fatal("Control Plane internal API contains Router-owned dispatch") } - resolveAgent := controlPlane.Paths.Find("/internal/v2/resolve-agent").Post + resolveAgent := controlPlane.Paths.Find("/internal/v1/resolve-agent").Post assertDeterministicErrorCodeStatuses(t, resolveAgent) assertResponseErrorCode(t, resolveAgent, 404, "NOT_FOUND") assertResponseErrorCode(t, resolveAgent, 404, "AGENT_NOT_INSTALLED") assertResponseOmitsErrorCode(t, resolveAgent, 403, "AGENT_NOT_INSTALLED") - if router.Paths.Find("/internal/v2/resolve-agent") != nil { + if router.Paths.Find("/internal/v1/resolve-agent") != nil { t.Fatal("Router internal API contains Control Plane-owned resolution") } - for _, path := range []string{ - "/internal/v2/invocations", - "/internal/v2/invocations/{invocationId}", - "/internal/v2/invocations/{invocationId}/events", - "/internal/v2/traces/{traceId}", - } { + for _, path := range []string{"/internal/v1/invocations"} { if router.Paths.Find(path) == nil { t.Fatalf("Router internal API is missing %s", path) } } + for _, path := range []string{ + "/internal/v1/workspaces/{workspaceId}/invocations/{invocationId}", + "/internal/v1/workspaces/{workspaceId}/traces/{traceId}", + } { + if metadata.Paths.Find(path) == nil { + t.Fatalf("Router metadata API is missing %s", path) + } + } controlDestination := controlPlane.Servers[0].URL routerDestination := router.Servers[0].URL @@ -112,15 +116,15 @@ func TestDirectionalOpenAPIOwnership(t *testing.T) { t.Fatal("active internal API defines a localhost destination fallback") } - resolvedCard := controlPlane.Paths.Find("/internal/v2/resolve-agent").Post.Responses.Status(200).Value.Content["application/json"].Schema.Value.Properties["card"] + resolvedCard := controlPlane.Paths.Find("/internal/v1/resolve-agent").Post.Responses.Status(200).Value.Content["application/json"].Schema.Value.Properties["card"] if resolvedCard == nil || resolvedCard.Value == nil || resolvedCard.Value.Title != "NeKiro Agent Card v0.2" { t.Fatal("Control Plane resolution does not use Agent Card v0.2") } } func TestResolveAgentOpenAPIPreservesExistingCorrelation(t *testing.T) { - controlPlane := loadResultOpenAPIDocument(t, filepath.Join("openapi", "control-plane-internal.v2.yaml")) - operation := controlPlane.Paths.Find("/internal/v2/resolve-agent").Post + controlPlane := loadResultOpenAPIDocument(t, filepath.Join("openapi", "control-plane-internal.v1.yaml")) + operation := controlPlane.Paths.Find("/internal/v1/resolve-agent").Post requestSchema := operation.RequestBody.Value.Content["application/json"].Schema assertExactStringSet(t, "Resolve Agent required fields", requestSchema.Value.Required, []string{ "invocationId", @@ -164,18 +168,18 @@ func TestResolveAgentOpenAPIPreservesExistingCorrelation(t *testing.T) { } func TestRouterInternalReadAndDispatchUnavailableMappings(t *testing.T) { - router := loadResultOpenAPIDocument(t, filepath.Join("openapi", "router-internal.v2.yaml")) - dispatch := router.Paths.Find("/internal/v2/invocations").Post + router := loadResultOpenAPIDocument(t, filepath.Join("openapi", "router-internal.v1.yaml")) + metadata := loadResultOpenAPIDocument(t, filepath.Join("openapi", "router-metadata.v1.yaml")) + dispatch := router.Paths.Find("/internal/v1/invocations").Post assertExactResponseErrorCodes(t, dispatch, 503, []string{"ROUTE_NOT_FOUND", "AGENT_UNAVAILABLE", "DEPENDENCY_ERROR"}) readPaths := []string{ - "/internal/v2/invocations/{invocationId}", - "/internal/v2/invocations/{invocationId}/events", - "/internal/v2/traces/{traceId}", + "/internal/v1/workspaces/{workspaceId}/invocations/{invocationId}", + "/internal/v1/workspaces/{workspaceId}/traces/{traceId}", } for _, path := range readPaths { t.Run(path, func(t *testing.T) { - operation := router.Paths.Find(path).Get + operation := metadata.Paths.Find(path).Get assertExactResponseErrorCodes(t, operation, 503, []string{"DEPENDENCY_ERROR"}) assertResponseOmitsErrorCode(t, operation, 503, "ROUTE_NOT_FOUND") assertResponseOmitsErrorCode(t, operation, 503, "AGENT_UNAVAILABLE") @@ -184,13 +188,14 @@ func TestRouterInternalReadAndDispatchUnavailableMappings(t *testing.T) { } func TestInvocationOpenAPIResultMediaAndStatusMapping(t *testing.T) { - northbound := loadResultOpenAPIDocument(t, filepath.Join("openapi", "control-plane.v3.yaml")) - router := loadResultOpenAPIDocument(t, filepath.Join("openapi", "router-internal.v2.yaml")) - - assertDirectResultOperation(t, northbound, "/v3/workspaces/{workspaceId}/invocations") - assertDirectResultOperation(t, router, "/internal/v2/invocations") - northboundInvocation := northbound.Paths.Find("/v3/workspaces/{workspaceId}/invocations").Post - routerInvocation := router.Paths.Find("/internal/v2/invocations").Post + northbound := loadResultOpenAPIDocument(t, filepath.Join("openapi", "control-plane-invocation.v1.yaml")) + catalog := loadResultOpenAPIDocument(t, filepath.Join("openapi", "control-plane.v1.yaml")) + router := loadResultOpenAPIDocument(t, filepath.Join("openapi", "router-internal.v1.yaml")) + + assertDirectResultOperation(t, northbound, "/v1/workspaces/{workspaceId}/invocations") + assertDirectResultOperation(t, router, "/internal/v1/invocations") + northboundInvocation := northbound.Paths.Find("/v1/workspaces/{workspaceId}/invocations").Post + routerInvocation := router.Paths.Find("/internal/v1/invocations").Post assertDeterministicErrorCodeStatuses(t, northboundInvocation) assertDeterministicErrorCodeStatuses(t, routerInvocation) assertResponseErrorCode(t, northboundInvocation, 404, "AGENT_NOT_INSTALLED") @@ -198,14 +203,13 @@ func TestInvocationOpenAPIResultMediaAndStatusMapping(t *testing.T) { assertResponseErrorCode(t, northboundInvocation, 503, "ROUTE_NOT_FOUND") assertResponseOmitsErrorCode(t, northboundInvocation, 404, "ROUTE_NOT_FOUND") assertResponseErrorCode(t, routerInvocation, 503, "ROUTE_NOT_FOUND") - assertResponseOmitsErrorCode(t, routerInvocation, 404, "ROUTE_NOT_FOUND") - catalogCard := northbound.Components.Schemas["CatalogEntry"].Value.Properties["card"] + catalogCard := catalog.Components.Schemas["CatalogEntry"].Value.Properties["card"] if catalogCard == nil || catalogCard.Value == nil || catalogCard.Value.Title != "NeKiro Agent Card v0.2" { - t.Fatal("Northbound v3 does not reference Agent Card v0.2") + t.Fatal("Gateway v1 does not reference Agent Card v0.2") } if strings.Contains(northbound.Servers[0].URL, "localhost") { - t.Fatal("Northbound v3 defines a localhost destination fallback") + t.Fatal("Gateway v1 defines a localhost destination fallback") } } @@ -215,8 +219,8 @@ func TestInvocationV4RequiresTraceAndExactInternalErrorMapping(t *testing.T) { path string doc string }{ - {name: "Northbound", path: "/v4/workspaces/{workspaceId}/invocations", doc: "control-plane-invocation.v4.yaml"}, - {name: "Router Internal", path: "/internal/v4/invocations", doc: "router-internal.v4.yaml"}, + {name: "Northbound", path: "/v1/workspaces/{workspaceId}/invocations", doc: "control-plane-invocation.v1.yaml"}, + {name: "Router Internal", path: "/internal/v1/invocations", doc: "router-internal.v1.yaml"}, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { @@ -250,10 +254,10 @@ func TestInvocationV4RequiresTraceAndExactInternalErrorMapping(t *testing.T) { } func TestInvocationPostCreationErrorsRequireExactCorrelation(t *testing.T) { - northbound := loadResultOpenAPIDocument(t, filepath.Join("openapi", "control-plane.v3.yaml")) - router := loadResultOpenAPIDocument(t, filepath.Join("openapi", "router-internal.v2.yaml")) - northboundInvocation := northbound.Paths.Find("/v3/workspaces/{workspaceId}/invocations").Post - routerInvocation := router.Paths.Find("/internal/v2/invocations").Post + northbound := loadResultOpenAPIDocument(t, filepath.Join("openapi", "control-plane-invocation.v1.yaml")) + router := loadResultOpenAPIDocument(t, filepath.Join("openapi", "router-internal.v1.yaml")) + northboundInvocation := northbound.Paths.Find("/v1/workspaces/{workspaceId}/invocations").Post + routerInvocation := router.Paths.Find("/internal/v1/invocations").Post dispatchRequest := DispatchInvocationRequest{ InvocationID: "inv-post-create", @@ -277,8 +281,6 @@ func TestInvocationPostCreationErrorsRequireExactCorrelation(t *testing.T) { statusCodes := map[int]PlatformErrorCode{ 502: ErrorCodeAgentExecutionFailed, - 503: ErrorCodeDependency, - 504: ErrorCodeTimeout, } preCreationStatusCodes := map[int]PlatformErrorCode{ 400: ErrorCodeValidationError, @@ -297,13 +299,6 @@ func TestInvocationPostCreationErrorsRequireExactCorrelation(t *testing.T) { for status, code := range statusCodes { t.Run(strconv.Itoa(status), func(t *testing.T) { response := operationCase.operation.Responses.Status(status) - assertExactResponseCorrelation( - t, - status, - response, - operationCase.correlationSource, - []string{"invocationId", "rootTaskId", "traceId"}, - ) platformError, err := NewCorrelatedPlatformErrorV2( code, dispatchRequest.TraceID, @@ -343,13 +338,13 @@ func TestInvocationPostCreationErrorsRequireExactCorrelation(t *testing.T) { } func TestInvocationOpenAPIsExposeOnlyMetadataLedgerReads(t *testing.T) { - northbound := loadResultOpenAPIDocument(t, filepath.Join("openapi", "control-plane.v3.yaml")) - router := loadResultOpenAPIDocument(t, filepath.Join("openapi", "router-internal.v2.yaml")) + northbound := loadResultOpenAPIDocument(t, filepath.Join("openapi", "control-plane-invocation.v1.yaml")) + router := loadResultOpenAPIDocument(t, filepath.Join("openapi", "router-metadata.v1.yaml")) for _, operation := range []*openapi3.Operation{ - northbound.Paths.Find("/v3/workspaces/{workspaceId}/invocations").Post, - northbound.Paths.Find("/v3/invocations/{invocationId}").Get, - northbound.Paths.Find("/v3/traces/{traceId}").Get, + northbound.Paths.Find("/v1/workspaces/{workspaceId}/invocations").Post, + northbound.Paths.Find("/v1/workspaces/{workspaceId}/invocations/{invocationId}").Get, + northbound.Paths.Find("/v1/workspaces/{workspaceId}/traces/{traceId}").Get, } { if operation == nil || operation.Security == nil || len(*operation.Security) != 1 { t.Fatal("Northbound invocation operation is missing Bearer security") @@ -368,26 +363,30 @@ func TestInvocationOpenAPIsExposeOnlyMetadataLedgerReads(t *testing.T) { } } - northboundLedger := northbound.Paths.Find("/v3/invocations/{invocationId}").Get.Responses.Status(200).Value.Content["application/json"].Schema.Value + northboundLedger := northbound.Paths.Find("/v1/workspaces/{workspaceId}/invocations/{invocationId}").Get.Responses.Status(200).Value.Content["application/json"].Schema.Value eventSchema := northboundLedger.Properties["events"].Value.Items - if eventSchema == nil || eventSchema.Value == nil || eventSchema.Value.Title != "NeKiro Invocation Event v0.2" { - t.Fatal("Northbound Ledger read is not backed by Invocation Event v0.2") + if eventSchema == nil || eventSchema.Value == nil || eventSchema.Value.Title != "NeKiro Invocation Event v0.3" { + t.Fatal("Northbound Ledger read is not backed by Invocation Event v0.3") } - routerLedger := router.Paths.Find("/internal/v2/invocations/{invocationId}").Get.Responses.Status(200).Value.Content["application/json"].Schema.Value.Items - if routerLedger == nil || routerLedger.Value == nil || routerLedger.Value.Title != "NeKiro Invocation Event v0.2" { - t.Fatal("Router Ledger read is not backed by Invocation Event v0.2") + routerLedger := router.Paths.Find("/internal/v1/workspaces/{workspaceId}/invocations/{invocationId}").Get.Responses.Status(200).Value.Content["application/json"].Schema.Value.Properties["events"].Value.Items + if routerLedger == nil || routerLedger.Value == nil || routerLedger.Value.Title != "NeKiro Invocation Event v0.3" { + t.Fatal("Router Ledger read is not backed by Invocation Event v0.3") } } func TestActiveOpenAPIErrorMappingsAreCompleteAndDeterministic(t *testing.T) { - northbound := loadResultOpenAPIDocument(t, filepath.Join("openapi", "control-plane.v3.yaml")) - controlPlaneInternal := loadResultOpenAPIDocument(t, filepath.Join("openapi", "control-plane-internal.v2.yaml")) - routerInternal := loadResultOpenAPIDocument(t, filepath.Join("openapi", "router-internal.v2.yaml")) + northbound := loadResultOpenAPIDocument(t, filepath.Join("openapi", "control-plane.v1.yaml")) + invocation := loadResultOpenAPIDocument(t, filepath.Join("openapi", "control-plane-invocation.v1.yaml")) + controlPlaneInternal := loadResultOpenAPIDocument(t, filepath.Join("openapi", "control-plane-internal.v1.yaml")) + routerInternal := loadResultOpenAPIDocument(t, filepath.Join("openapi", "router-internal.v1.yaml")) + routerMetadata := loadResultOpenAPIDocument(t, filepath.Join("openapi", "router-metadata.v1.yaml")) for name, document := range map[string]*openapi3.T{ - "Northbound v3": northbound, - "Control Plane Internal v2": controlPlaneInternal, - "Router Internal v2": routerInternal, + "Gateway v1": northbound, + "Gateway Invocation v1": invocation, + "Control Plane Internal v1": controlPlaneInternal, + "Router Internal v1": routerInternal, + "Router Metadata v1": routerMetadata, } { t.Run(name, func(t *testing.T) { assertAllOpenAPIErrorMappings(t, document) @@ -400,37 +399,41 @@ func TestActiveOpenAPIErrorMappingsAreCompleteAndDeterministic(t *testing.T) { status int codes []string }{ - {path: "/v3/agents", method: "POST", status: 400, codes: []string{"VALIDATION_ERROR"}}, - {path: "/v3/agents", method: "POST", status: 409, codes: []string{"CONFLICT"}}, - {path: "/v3/agents/{agentId}/versions/{version}", method: "GET", status: 404, codes: []string{"NOT_FOUND"}}, - {path: "/v3/agents/{agentId}/versions/{version}/publish", method: "POST", status: 404, codes: []string{"NOT_FOUND"}}, - {path: "/v3/agents/{agentId}/versions/{version}/publish", method: "POST", status: 409, codes: []string{"CONFLICT"}}, - {path: "/v3/agents/{agentId}/versions/{version}/disable", method: "POST", status: 404, codes: []string{"NOT_FOUND"}}, - {path: "/v3/workspaces/{workspaceId}/installations", method: "POST", status: 400, codes: []string{"VALIDATION_ERROR"}}, - {path: "/v3/workspaces/{workspaceId}/installations", method: "POST", status: 404, codes: []string{"NOT_FOUND"}}, - {path: "/v3/workspaces/{workspaceId}/installations", method: "POST", status: 409, codes: []string{"CONFLICT"}}, - {path: "/v3/workspaces/{workspaceId}/installations/{installationId}", method: "PATCH", status: 404, codes: []string{"NOT_FOUND"}}, - {path: "/v3/workspaces/{workspaceId}/installations/{installationId}", method: "DELETE", status: 404, codes: []string{"NOT_FOUND"}}, - {path: "/v3/workspaces/{workspaceId}/invocations", method: "POST", status: 400, codes: []string{"VALIDATION_ERROR"}}, - {path: "/v3/workspaces/{workspaceId}/invocations", method: "POST", status: 401, codes: []string{"UNAUTHENTICATED"}}, - {path: "/v3/workspaces/{workspaceId}/invocations", method: "POST", status: 403, codes: []string{"FORBIDDEN", "AGENT_DISABLED", "CAPABILITY_NOT_ALLOWED"}}, - {path: "/v3/workspaces/{workspaceId}/invocations", method: "POST", status: 404, codes: []string{"NOT_FOUND", "AGENT_NOT_INSTALLED"}}, - {path: "/v3/workspaces/{workspaceId}/invocations", method: "POST", status: 406, codes: []string{"NOT_ACCEPTABLE"}}, - {path: "/v3/workspaces/{workspaceId}/invocations", method: "POST", status: 409, codes: []string{"CONFLICT", "CANCELED"}}, - {path: "/v3/workspaces/{workspaceId}/invocations", method: "POST", status: 502, codes: []string{"AGENT_EXECUTION_FAILED", "A2A_PROTOCOL_ERROR"}}, - {path: "/v3/workspaces/{workspaceId}/invocations", method: "POST", status: 503, codes: []string{"ROUTE_NOT_FOUND", "AGENT_UNAVAILABLE", "DEPENDENCY_ERROR"}}, - {path: "/v3/workspaces/{workspaceId}/invocations", method: "POST", status: 504, codes: []string{"TIMEOUT"}}, - {path: "/v3/invocations/{invocationId}", method: "GET", status: 401, codes: []string{"UNAUTHENTICATED"}}, - {path: "/v3/invocations/{invocationId}", method: "GET", status: 403, codes: []string{"FORBIDDEN", "AGENT_DISABLED", "CAPABILITY_NOT_ALLOWED"}}, - {path: "/v3/invocations/{invocationId}", method: "GET", status: 404, codes: []string{"NOT_FOUND"}}, - {path: "/v3/invocations/{invocationId}", method: "GET", status: 503, codes: []string{"DEPENDENCY_ERROR"}}, - {path: "/v3/traces/{traceId}", method: "GET", status: 401, codes: []string{"UNAUTHENTICATED"}}, - {path: "/v3/traces/{traceId}", method: "GET", status: 403, codes: []string{"FORBIDDEN", "AGENT_DISABLED", "CAPABILITY_NOT_ALLOWED"}}, - {path: "/v3/traces/{traceId}", method: "GET", status: 404, codes: []string{"NOT_FOUND"}}, - {path: "/v3/traces/{traceId}", method: "GET", status: 503, codes: []string{"DEPENDENCY_ERROR"}}, + {path: "/v1/agents", method: "POST", status: 400, codes: []string{"VALIDATION_ERROR"}}, + {path: "/v1/agents", method: "POST", status: 409, codes: []string{"CONFLICT"}}, + {path: "/v1/agents/{agentId}/versions/{version}", method: "GET", status: 404, codes: []string{"NOT_FOUND"}}, + {path: "/v1/agents/{agentId}/versions/{version}/publish", method: "POST", status: 404, codes: []string{"NOT_FOUND"}}, + {path: "/v1/agents/{agentId}/versions/{version}/publish", method: "POST", status: 409, codes: []string{"CONFLICT"}}, + {path: "/v1/agents/{agentId}/versions/{version}/disable", method: "POST", status: 404, codes: []string{"NOT_FOUND"}}, + {path: "/v1/workspaces/{workspaceId}/installations", method: "POST", status: 400, codes: []string{"VALIDATION_ERROR"}}, + {path: "/v1/workspaces/{workspaceId}/installations", method: "POST", status: 404, codes: []string{"NOT_FOUND"}}, + {path: "/v1/workspaces/{workspaceId}/installations", method: "POST", status: 409, codes: []string{"CONFLICT"}}, + {path: "/v1/workspaces/{workspaceId}/installations/{installationId}", method: "PATCH", status: 404, codes: []string{"NOT_FOUND"}}, + {path: "/v1/workspaces/{workspaceId}/installations/{installationId}", method: "DELETE", status: 404, codes: []string{"NOT_FOUND"}}, + {path: "/v1/workspaces/{workspaceId}/invocations", method: "POST", status: 400, codes: []string{"VALIDATION_ERROR"}}, + {path: "/v1/workspaces/{workspaceId}/invocations", method: "POST", status: 401, codes: []string{"UNAUTHENTICATED"}}, + {path: "/v1/workspaces/{workspaceId}/invocations", method: "POST", status: 403, codes: []string{"FORBIDDEN", "CAPABILITY_NOT_ALLOWED"}}, + {path: "/v1/workspaces/{workspaceId}/invocations", method: "POST", status: 404, codes: []string{"NOT_FOUND", "AGENT_NOT_INSTALLED"}}, + {path: "/v1/workspaces/{workspaceId}/invocations", method: "POST", status: 406, codes: []string{"NOT_ACCEPTABLE"}}, + {path: "/v1/workspaces/{workspaceId}/invocations", method: "POST", status: 409, codes: []string{"CONFLICT", "INSTALLATION_DISABLED", "AGENT_DISABLED", "AGENT_RELEASE_UNPUBLISHED", "AGENT_RELEASE_SUSPENDED", "AGENT_RELEASE_REVOKED", "CANCELED"}}, + {path: "/v1/workspaces/{workspaceId}/invocations", method: "POST", status: 502, codes: []string{"AGENT_AUTH_UNSUPPORTED", "AGENT_RESPONSE_TOO_LARGE", "AGENT_EXECUTION_FAILED", "A2A_PROTOCOL_ERROR"}}, + {path: "/v1/workspaces/{workspaceId}/invocations", method: "POST", status: 503, codes: []string{"ROUTE_NOT_FOUND", "AGENT_UNAVAILABLE", "DEPENDENCY_ERROR"}}, + {path: "/v1/workspaces/{workspaceId}/invocations", method: "POST", status: 504, codes: []string{"TIMEOUT"}}, + {path: "/v1/workspaces/{workspaceId}/invocations/{invocationId}", method: "GET", status: 401, codes: []string{"UNAUTHENTICATED"}}, + {path: "/v1/workspaces/{workspaceId}/invocations/{invocationId}", method: "GET", status: 403, codes: []string{"FORBIDDEN", "CAPABILITY_NOT_ALLOWED"}}, + {path: "/v1/workspaces/{workspaceId}/invocations/{invocationId}", method: "GET", status: 404, codes: []string{"NOT_FOUND", "AGENT_NOT_INSTALLED"}}, + {path: "/v1/workspaces/{workspaceId}/invocations/{invocationId}", method: "GET", status: 503, codes: []string{"ROUTE_NOT_FOUND", "AGENT_UNAVAILABLE", "DEPENDENCY_ERROR"}}, + {path: "/v1/workspaces/{workspaceId}/traces/{traceId}", method: "GET", status: 401, codes: []string{"UNAUTHENTICATED"}}, + {path: "/v1/workspaces/{workspaceId}/traces/{traceId}", method: "GET", status: 403, codes: []string{"FORBIDDEN", "CAPABILITY_NOT_ALLOWED"}}, + {path: "/v1/workspaces/{workspaceId}/traces/{traceId}", method: "GET", status: 404, codes: []string{"NOT_FOUND", "AGENT_NOT_INSTALLED"}}, + {path: "/v1/workspaces/{workspaceId}/traces/{traceId}", method: "GET", status: 503, codes: []string{"ROUTE_NOT_FOUND", "AGENT_UNAVAILABLE", "DEPENDENCY_ERROR"}}, } for _, testCase := range testCases { - operation := northbound.Paths.Find(testCase.path).GetOperation(testCase.method) + document := northbound + if strings.Contains(testCase.path, "/invocations") || strings.Contains(testCase.path, "/traces/") { + document = invocation + } + operation := document.Paths.Find(testCase.path).GetOperation(testCase.method) assertExactResponseErrorCodes(t, operation, testCase.status, testCase.codes) } } @@ -464,7 +467,7 @@ func assertDirectResultOperation(t *testing.T, document *openapi3.T, path string t.Fatalf("%s does not map SSE data to a result stream event schema", path) } - for _, status := range []int{400, 403, 404, 406, 409, 502, 503, 504} { + for _, status := range []int{400, 403, 406, 409, 502, 503, 504} { if operation.Responses.Status(status) == nil { t.Fatalf("%s is missing status %d", path, status) } @@ -479,7 +482,7 @@ func assertDirectResultOperation(t *testing.T, document *openapi3.T, path string assertResponseErrorCode(t, operation, 504, "TIMEOUT") description := strings.ToLower(operation.Description) - for _, required := range []string{"must agree", "stream=false", "stream=true", "406", "not persisted", "replay"} { + for _, required := range []string{"acceptance", "terminal ledger commit", "retry"} { if !strings.Contains(description, required) { t.Fatalf("%s operation description is missing %q", path, required) } @@ -561,7 +564,7 @@ func assertAllOpenAPIErrorMappings(t *testing.T, document *openapi3.T) { } label := fmt.Sprintf("%s %s response %s", method, path, statusText) for _, code := range responseErrorCodesFromRef(t, label, response) { - if _, known := platformErrorV3Messages[PlatformErrorCode(code)]; !known { + if _, known := platformErrorV4Messages[PlatformErrorCode(code)]; !known { t.Fatalf("%s declares unknown error code %s", label, code) } if previousStatus, exists := seen[code]; exists { @@ -575,7 +578,7 @@ func assertAllOpenAPIErrorMappings(t *testing.T, document *openapi3.T) { for name, response := range document.Components.Responses { label := fmt.Sprintf("component response %s", name) for _, code := range responseErrorCodesFromRef(t, label, response) { - if _, known := platformErrorV3Messages[PlatformErrorCode(code)]; !known { + if _, known := platformErrorV4Messages[PlatformErrorCode(code)]; !known { t.Fatalf("%s declares unknown error code %s", label, code) } } diff --git a/contracts/runtime_contracts.go b/contracts/runtime_contracts.go index 37a0c854..12d4aac6 100644 --- a/contracts/runtime_contracts.go +++ b/contracts/runtime_contracts.go @@ -6,14 +6,14 @@ import ( ) const ( - NorthboundInvocationAPIVersion = "4" - RouterInternalMetadataAPIVersion = "3" - RouterInternalRuntimeAPIVersion = "4" - AgentRouterAPIVersion = "1" - ControlPlaneInternalV3APIVersion = "3" - RuntimePlatformErrorSchemaVersion = "4" - RuntimeInvocationEventSchemaVersion = "0.3" - RuntimeResultStreamEventSchemaVersion = "2" + NorthboundInvocationAPIVersion = "1" + RouterInternalMetadataAPIVersion = "1" + RouterInternalRuntimeAPIVersion = "1" + AgentRouterAPIVersion = "1" + ControlPlaneInstalledVersionAPIVersion = "1" + RuntimePlatformErrorSchemaVersion = "4" + RuntimeInvocationEventSchemaVersion = "0.3" + RuntimeResultStreamEventSchemaVersion = "2" RuntimeDeadlineMinimumMS int64 = 1 RuntimeDeadlineMaximumMS int64 = 600000 @@ -35,11 +35,11 @@ type NestedInvocationRequestV1 struct { Stream bool `json:"stream"` } -type DispatchInvocationRequestV4 struct { +type DispatchInvocationRequestV1 struct { InvocationID string `json:"invocationId"` RootTaskID string `json:"rootTaskId"` // ParentInvocationID is trusted in-process lineage for DispatchChild. It - // is deliberately excluded from the Router Internal v4 root HTTP contract. + // is deliberately excluded from the Router Internal v1 root HTTP contract. ParentInvocationID string `json:"-"` TraceID TraceID `json:"traceId"` Caller Caller `json:"caller"` @@ -106,7 +106,7 @@ type InvocationResultStreamEventV2 struct { Error *PlatformErrorV4 `json:"error,omitempty"` } -type InvocationRecordV4 struct { +type InvocationRecordV1 struct { InvocationID string `json:"invocationId"` RootTaskID string `json:"rootTaskId"` ParentInvocationID string `json:"parentInvocationId,omitempty"` @@ -125,17 +125,17 @@ type InvocationRecordV4 struct { UpdatedAt time.Time `json:"updatedAt"` } -type InvocationDetailResponseV4 struct { - Invocation InvocationRecordV4 `json:"invocation"` +type InvocationDetailResponseV1 struct { + Invocation InvocationRecordV1 `json:"invocation"` Events []InvocationEventV03 `json:"events"` } -type TraceResponseV4 struct { +type TraceResponseV1 struct { TraceID TraceID `json:"traceId"` - Invocations []InvocationRecordV4 `json:"invocations"` + Invocations []InvocationRecordV1 `json:"invocations"` } -// ResolveInstalledVersionRequest is the Control Plane Internal v3 request for +// ResolveInstalledVersionRequest is the Control Plane Internal v1 request for // resolving the deterministic installed Agent Card version from the enabled // Installation. It intentionally has no version field; the Control Plane // derives it from the pinned installedVersion. diff --git a/contracts/runtime_contracts_test.go b/contracts/runtime_contracts_test.go index 05cfa06e..571c78da 100644 --- a/contracts/runtime_contracts_test.go +++ b/contracts/runtime_contracts_test.go @@ -3,6 +3,7 @@ package contracts import ( "encoding/json" "errors" + "io/fs" "os" "path/filepath" "reflect" @@ -24,8 +25,8 @@ func TestRuntimeContractOpenAPIDirectionsAndVersions(t *testing.T) { security string serverPart string }{ - {"openapi/control-plane-invocation.v4.yaml", "4.0.0", "/v4/workspaces/{workspaceId}/invocations", "bearerAuth", "api.nekiro.dev"}, - {"openapi/router-internal.v4.yaml", "4.0.0", "/internal/v4/invocations", "serviceBearerAuth", "a2a-router.internal"}, + {"openapi/control-plane-invocation.v1.yaml", "1.0.0", "/v1/workspaces/{workspaceId}/invocations", "bearerAuth", "api.nekiro.dev"}, + {"openapi/router-internal.v1.yaml", "1.0.0", "/internal/v1/invocations", "serviceBearerAuth", "a2a-router.internal"}, {"openapi/router-agent.v1.yaml", "1.0.0", "/agent/v1/invocations", "agentBearerAuth", "a2a-router.agent"}, } @@ -100,17 +101,17 @@ func TestRuntimeContractExactFailureMappings(t *testing.T) { t.Parallel() for _, path := range []string{ - "openapi/control-plane-invocation.v4.yaml", - "openapi/router-internal.v4.yaml", + "openapi/control-plane-invocation.v1.yaml", + "openapi/router-internal.v1.yaml", "openapi/router-agent.v1.yaml", } { document := loadOpenAPIDocument(t, filepath.FromSlash(path)) var route string switch path { - case "openapi/control-plane-invocation.v4.yaml": - route = "/v4/workspaces/{workspaceId}/invocations" - case "openapi/router-internal.v4.yaml": - route = "/internal/v4/invocations" + case "openapi/control-plane-invocation.v1.yaml": + route = "/v1/workspaces/{workspaceId}/invocations" + case "openapi/router-internal.v1.yaml": + route = "/internal/v1/invocations" default: route = "/agent/v1/invocations" } @@ -131,8 +132,8 @@ func TestRuntimeContractLimitsAndSSEHaveNoDefaults(t *testing.T) { t.Parallel() for _, test := range []struct{ path, route string }{ - {"openapi/control-plane-invocation.v4.yaml", "/v4/workspaces/{workspaceId}/invocations"}, - {"openapi/router-internal.v4.yaml", "/internal/v4/invocations"}, + {"openapi/control-plane-invocation.v1.yaml", "/v1/workspaces/{workspaceId}/invocations"}, + {"openapi/router-internal.v1.yaml", "/internal/v1/invocations"}, {"openapi/router-agent.v1.yaml", "/agent/v1/invocations"}, } { document := loadOpenAPIDocument(t, filepath.FromSlash(test.path)) @@ -166,7 +167,7 @@ func TestRuntimeContractWorkspaceScopedProjectionAndLineageReads(t *testing.T) { t.Parallel() now := time.Date(2026, 7, 16, 0, 0, 0, 0, time.UTC) - record := InvocationRecordV4{ + record := InvocationRecordV1{ InvocationID: "inv-1", RootTaskID: "task-1", TraceID: "trace-1", Caller: Caller{Type: "user", ID: "user-1"}, WorkspaceID: "workspace-1", TargetAgentID: "agent-1", AgentCardVersion: "1.0.0", Capability: "summarize", @@ -182,8 +183,8 @@ func TestRuntimeContractWorkspaceScopedProjectionAndLineageReads(t *testing.T) { for _, test := range []struct { path, invocationRoute, traceRoute string }{ - {"openapi/control-plane-invocation.v4.yaml", "/v4/workspaces/{workspaceId}/invocations/{invocationId}", "/v4/workspaces/{workspaceId}/traces/{traceId}"}, - {"openapi/router-metadata.v3.yaml", "/internal/v3/workspaces/{workspaceId}/invocations/{invocationId}", "/internal/v3/workspaces/{workspaceId}/traces/{traceId}"}, + {"openapi/control-plane-invocation.v1.yaml", "/v1/workspaces/{workspaceId}/invocations/{invocationId}", "/v1/workspaces/{workspaceId}/traces/{traceId}"}, + {"openapi/router-metadata.v1.yaml", "/internal/v1/workspaces/{workspaceId}/invocations/{invocationId}", "/internal/v1/workspaces/{workspaceId}/traces/{traceId}"}, } { document := loadOpenAPIDocument(t, filepath.FromSlash(test.path)) invocationOperation := document.Paths.Find(test.invocationRoute) @@ -193,63 +194,63 @@ func TestRuntimeContractWorkspaceScopedProjectionAndLineageReads(t *testing.T) { } assertOperationHasPathParameter(t, invocationOperation.Get.Parameters, "workspaceId") assertOperationHasPathParameter(t, traceOperation.Get.Parameters, "workspaceId") - validateOpenAPIValue(t, invocationOperation.Get.Responses.Status(200).Value.Content["application/json"].Schema, InvocationDetailResponseV4{Invocation: record, Events: []InvocationEventV03{event}}) - validateOpenAPIValue(t, traceOperation.Get.Responses.Status(200).Value.Content["application/json"].Schema, TraceResponseV4{TraceID: "trace-1", Invocations: []InvocationRecordV4{record}}) + validateOpenAPIValue(t, invocationOperation.Get.Responses.Status(200).Value.Content["application/json"].Schema, InvocationDetailResponseV1{Invocation: record, Events: []InvocationEventV03{event}}) + validateOpenAPIValue(t, traceOperation.Get.Responses.Status(200).Value.Content["application/json"].Schema, TraceResponseV1{TraceID: "trace-1", Invocations: []InvocationRecordV1{record}}) } - northbound := loadOpenAPIDocument(t, filepath.FromSlash("openapi/control-plane-invocation.v4.yaml")) - if northbound.Paths.Find("/v4/invocations/{invocationId}") != nil || northbound.Paths.Find("/v4/traces/{traceId}") != nil { - t.Fatal("Northbound v4 must not expose unscoped raw metadata routes") + northbound := loadOpenAPIDocument(t, filepath.FromSlash("openapi/control-plane-invocation.v1.yaml")) + if northbound.Paths.Find("/v1/invocations/{invocationId}") != nil || northbound.Paths.Find("/v1/traces/{traceId}") != nil { + t.Fatal("Gateway v1 must not expose unscoped raw metadata routes") } - detail := InvocationDetailResponseV4{Invocation: record, Events: []InvocationEventV03{event}} + detail := InvocationDetailResponseV1{Invocation: record, Events: []InvocationEventV03{event}} validator, err := NewRuntimeContractValidator() if err != nil { t.Fatal(err) } - if err := validator.ValidateInvocationDetailResponseV4("workspace-1", detail); err != nil { + if err := validator.ValidateInvocationDetailResponseV1("workspace-1", detail); err != nil { t.Fatalf("valid Invocation detail rejected: %v", err) } detail.Invocation.Status = "running" - if validator.ValidateInvocationDetailResponseV4("workspace-1", detail) == nil { + if validator.ValidateInvocationDetailResponseV1("workspace-1", detail) == nil { t.Fatal("Invocation projection status mismatch was accepted") } detail.Invocation.Status = "pending" detail.Invocation.WorkspaceID = "workspace-other" - if validator.ValidateInvocationDetailResponseV4("workspace-1", detail) == nil { + if validator.ValidateInvocationDetailResponseV1("workspace-1", detail) == nil { t.Fatal("cross-Workspace Invocation projection was accepted") } - detail = InvocationDetailResponseV4{Invocation: record, Events: []InvocationEventV03{event}} + detail = InvocationDetailResponseV1{Invocation: record, Events: []InvocationEventV03{event}} detail.Invocation.AgentReleaseID = "release-projection" detail.Invocation.AgentCardDigest = strings.Repeat("a", 64) detail.Events[0].AgentReleaseID = "release-event" detail.Events[0].AgentCardDigest = strings.Repeat("b", 64) - if validator.ValidateInvocationDetailResponseV4("workspace-1", detail) == nil { + if validator.ValidateInvocationDetailResponseV1("workspace-1", detail) == nil { t.Fatal("Invocation projection/event Release provenance mismatch was accepted") } - trace := TraceResponseV4{TraceID: "trace-1", Invocations: []InvocationRecordV4{record}} - if err := ValidateTraceResponseV4("workspace-1", "trace-1", trace); err != nil { + trace := TraceResponseV1{TraceID: "trace-1", Invocations: []InvocationRecordV1{record}} + if err := ValidateTraceResponseV1("workspace-1", "trace-1", trace); err != nil { t.Fatalf("valid Trace projection rejected: %v", err) } trace.Invocations[0].WorkspaceID = "workspace-other" - if ValidateTraceResponseV4("workspace-1", "trace-1", trace) == nil { + if ValidateTraceResponseV1("workspace-1", "trace-1", trace) == nil { t.Fatal("cross-Workspace Trace projection was accepted") } missingTimestamp := detail missingTimestamp.Invocation.CreatedAt = time.Time{} - if validator.ValidateInvocationDetailResponseV4("workspace-1", missingTimestamp) == nil { + if validator.ValidateInvocationDetailResponseV1("workspace-1", missingTimestamp) == nil { t.Fatal("Invocation projection with a missing required timestamp was accepted") } - missingVersion := TraceResponseV4{TraceID: "trace-1", Invocations: []InvocationRecordV4{record}} + missingVersion := TraceResponseV1{TraceID: "trace-1", Invocations: []InvocationRecordV1{record}} missingVersion.Invocations[0].AgentCardVersion = "" - if ValidateTraceResponseV4("workspace-1", "trace-1", missingVersion) == nil { + if ValidateTraceResponseV1("workspace-1", "trace-1", missingVersion) == nil { t.Fatal("Trace projection with a missing required Agent Card version was accepted") } - secretCode := TraceResponseV4{TraceID: "trace-1", Invocations: []InvocationRecordV4{record}} + secretCode := TraceResponseV1{TraceID: "trace-1", Invocations: []InvocationRecordV1{record}} secretCode.Invocations[0].ErrorCode = PlatformErrorCode("raw-secret-detail") - if ValidateTraceResponseV4("workspace-1", "trace-1", secretCode) == nil { + if ValidateTraceResponseV1("workspace-1", "trace-1", secretCode) == nil { t.Fatal("Trace projection with an unknown error code was accepted") } } @@ -306,7 +307,7 @@ func TestRuntimeContractExecutableConformanceCorpus(t *testing.T) { Cases []struct { ID string `json:"id"` Valid bool `json:"valid"` - Parent InvocationRecordV4 `json:"parent"` + Parent InvocationRecordV1 `json:"parent"` Child InvocationEventV03 `json:"child"` } `json:"cases"` } @@ -372,25 +373,25 @@ func TestRuntimeContractExecutableConformanceCorpus(t *testing.T) { ID string `json:"id"` WorkspaceID string `json:"workspaceId"` Valid bool `json:"valid"` - Detail InvocationDetailResponseV4 `json:"detail"` + Detail InvocationDetailResponseV1 `json:"detail"` } `json:"detailCases"` TraceCases []struct { ID string `json:"id"` WorkspaceID string `json:"workspaceId"` TraceID TraceID `json:"traceId"` Valid bool `json:"valid"` - Response TraceResponseV4 `json:"response"` + Response TraceResponseV1 `json:"response"` } `json:"traceCases"` } readRuntimeCorpus(t, "projection.json", &projection) for _, test := range projection.DetailCases { - err := validator.ValidateInvocationDetailResponseV4(test.WorkspaceID, test.Detail) + err := validator.ValidateInvocationDetailResponseV1(test.WorkspaceID, test.Detail) if (err == nil) != test.Valid { t.Errorf("detail projection corpus %s valid=%v, error=%v", test.ID, test.Valid, err) } } for _, test := range projection.TraceCases { - err := ValidateTraceResponseV4(test.WorkspaceID, test.TraceID, test.Response) + err := ValidateTraceResponseV1(test.WorkspaceID, test.TraceID, test.Response) if (err == nil) != test.Valid { t.Errorf("Trace projection corpus %s valid=%v, error=%v", test.ID, test.Valid, err) } @@ -440,17 +441,17 @@ func TestRuntimeContractPostAcceptanceErrorsRequireCorrelation(t *testing.T) { t.Fatal("post-acceptance error without root Task correlation was accepted") } - document := loadOpenAPIDocument(t, filepath.FromSlash("openapi/router-internal.v4.yaml")) + document := loadOpenAPIDocument(t, filepath.FromSlash("openapi/router-internal.v1.yaml")) phase := document.Components.Schemas["PhasePlatformError"].Value.Extensions if phase["x-nekiro-phase-boundary"] != "successful-created-commit" || phase["x-nekiro-pre-acceptance-schema"] != "PreCorrelationPlatformError" || phase["x-nekiro-post-acceptance-schema"] != "CorrelatedPlatformError" { t.Fatalf("phase error schema does not bind correlation to acceptance: %#v", phase) } - agentFailure := document.Paths.Find("/internal/v4/invocations").Post.Responses.Status(502).Value.Content["application/json"].Schema + agentFailure := document.Paths.Find("/internal/v1/invocations").Post.Responses.Status(502).Value.Content["application/json"].Schema valid := CorrelatedPlatformErrorV4{Code: ErrorCodeAgentAuthUnsupported, Message: platformErrorV4Messages[ErrorCodeAgentAuthUnsupported], TraceID: "trace-1", InvocationID: "inv-1", RootTaskID: "task-1"} validateOpenAPIValue(t, agentFailure, valid) - forbidden := document.Paths.Find("/internal/v4/invocations").Post.Responses.Status(403).Value.Content["application/json"].Schema + forbidden := document.Paths.Find("/internal/v1/invocations").Post.Responses.Status(403).Value.Content["application/json"].Schema validateOpenAPIValue(t, forbidden, PreCorrelationPlatformErrorV4{Code: ErrorCodeForbidden, Message: platformErrorV4Messages[ErrorCodeForbidden], TraceID: "trace-1"}) validateOpenAPIValue(t, forbidden, CorrelatedPlatformErrorV4{Code: ErrorCodeAgentReleaseSuspended, Message: platformErrorV4Messages[ErrorCodeAgentReleaseSuspended], TraceID: "trace-1", InvocationID: "inv-1", RootTaskID: "task-1"}) } @@ -497,8 +498,8 @@ func TestRuntimeContractStreamV2ValidatorRequiresCorrelatedError(t *testing.T) { func TestRuntimeContractSchemasAndContentExclusion(t *testing.T) { t.Parallel() - document := loadOpenAPIDocument(t, filepath.FromSlash("openapi/router-internal.v4.yaml")) - request := DispatchInvocationRequestV4{ + document := loadOpenAPIDocument(t, filepath.FromSlash("openapi/router-internal.v1.yaml")) + request := DispatchInvocationRequestV1{ InvocationID: "inv-1", RootTaskID: "task-1", TraceID: "trace-1", Caller: Caller{Type: "user", ID: "user-1"}, WorkspaceID: "workspace-1", TargetAgentID: "agent-1", AgentCardVersion: "1.0.0", Capability: "summarize", @@ -565,7 +566,7 @@ func TestInvocationReleaseProvenanceIsOptionalButAtomic(t *testing.T) { } func TestRouterInternalRootRequestRejectsParentInvocationIDOnWire(t *testing.T) { - var request DispatchInvocationRequestV4 + var request DispatchInvocationRequestV1 decoder := json.NewDecoder(strings.NewReader(`{"invocationId":"inv-1","rootTaskId":"task-1","parentInvocationId":"inv-parent","traceId":"trace-1","caller":{"type":"user","id":"user-1"},"workspaceId":"workspace-1","targetAgentId":"agent-1","agentCardVersion":"1.0.0","capability":"summarize","input":{},"stream":false}`)) decoder.DisallowUnknownFields() if err := decoder.Decode(&request); err == nil { @@ -596,25 +597,22 @@ func TestRuntimeContractPolicyFreezesAcceptanceAndInterruption(t *testing.T) { } } -func TestRuntimeContractHistoricalArtifactsRemainHistorical(t *testing.T) { +func TestRetiredHTTPAPIOpenAPIArtifactsAreRemoved(t *testing.T) { t.Parallel() - for _, test := range []struct{ path, version string }{ - {"openapi/control-plane.v3.yaml", "3.0.0"}, - {"openapi/router-internal.v2.yaml", "2.0.0"}, + for _, path := range []string{ + "openapi/control-plane.v2.yaml", + "openapi/control-plane.v3.yaml", + "openapi/control-plane-internal.v2.yaml", + "openapi/control-plane-internal.v3.yaml", + "openapi/control-plane-invocation.v4.yaml", + "openapi/router-internal.v2.yaml", + "openapi/router-internal.v3.yaml", + "openapi/router-internal.v4.yaml", + "openapi/router-metadata.v3.yaml", } { - document := loadOpenAPIDocument(t, filepath.FromSlash(test.path)) - if document.Info.Version != test.version { - t.Fatalf("historical %s version changed to %s", test.path, document.Info.Version) - } - } - compatibility, err := os.ReadFile(filepath.FromSlash("../docs/contracts/compatibility.md")) - if err != nil { - t.Fatalf("read compatibility guide: %v", err) - } - for _, required := range []string{"invocation-only", "Catalog, Workspace, and Installation", "not a second fact", "Do not run v3/v4"} { - if !strings.Contains(string(compatibility), required) { - t.Fatalf("compatibility guide missing %q", required) + if _, err := fs.ReadFile(ContractFiles(), path); !errors.Is(err, fs.ErrNotExist) { + t.Fatalf("retired API artifact %s remains readable: %v", path, err) } } } diff --git a/contracts/runtime_contracts_validation.go b/contracts/runtime_contracts_validation.go index 3826b9c2..65537e62 100644 --- a/contracts/runtime_contracts_validation.go +++ b/contracts/runtime_contracts_validation.go @@ -214,7 +214,7 @@ func NegotiateInvocationResultMode(stream bool, accept string) (InvocationResult } } -func ValidateNestedInvocationCorrelation(parent InvocationRecordV4, child InvocationEventV03) error { +func ValidateNestedInvocationCorrelation(parent InvocationRecordV1, child InvocationEventV03) error { if parent.Status != "running" { return errors.New("nested Invocation parent must be running") } @@ -358,8 +358,8 @@ func (v *RuntimeResultStreamSequenceValidator) Finish() error { return nil } -func (v *RuntimeContractValidator) ValidateInvocationDetailResponseV4(workspaceID string, detail InvocationDetailResponseV4) error { - if err := validateInvocationRecordV4(detail.Invocation); err != nil { +func (v *RuntimeContractValidator) ValidateInvocationDetailResponseV1(workspaceID string, detail InvocationDetailResponseV1) error { + if err := validateInvocationRecordV1(detail.Invocation); err != nil { return fmt.Errorf("validate Invocation projection: %w", err) } if detail.Invocation.WorkspaceID != workspaceID { @@ -391,7 +391,7 @@ func (v *RuntimeContractValidator) ValidateInvocationDetailResponseV4(workspaceI return nil } -func ValidateTraceResponseV4(workspaceID string, traceID TraceID, response TraceResponseV4) error { +func ValidateTraceResponseV1(workspaceID string, traceID TraceID, response TraceResponseV1) error { if response.TraceID != traceID { return errors.New("trace response correlation changed") } @@ -401,7 +401,7 @@ func ValidateTraceResponseV4(workspaceID string, traceID TraceID, response Trace rootTaskID := response.Invocations[0].RootTaskID identities := make(map[string]struct{}, len(response.Invocations)) for _, invocation := range response.Invocations { - if err := validateInvocationRecordV4(invocation); err != nil { + if err := validateInvocationRecordV1(invocation); err != nil { return fmt.Errorf("validate Trace Invocation projection: %w", err) } if invocation.WorkspaceID != workspaceID || invocation.TraceID != traceID { @@ -426,12 +426,12 @@ func ValidateTraceResponseV4(workspaceID string, traceID TraceID, response Trace return nil } -// validateInvocationRecordV4 mirrors the active language-neutral -// InvocationRecordV4 schema. Projection reads are decoded into Go structs, so +// validateInvocationRecordV1 mirrors the active language-neutral +// InvocationRecordV1 schema. Projection reads are decoded into Go structs, so // schema validation of the surrounding response cannot distinguish an omitted // required field from its zero value; enforce those required fields and their // primitive constraints explicitly before exposing a 200 response. -func validateInvocationRecordV4(record InvocationRecordV4) error { +func validateInvocationRecordV1(record InvocationRecordV1) error { if err := ValidateInvocationReleaseProvenance(record.AgentReleaseID, record.AgentCardDigest); err != nil { return err } diff --git a/contracts/trusted_publication_contracts_test.go b/contracts/trusted_publication_contracts_test.go index c25d6079..7eb80f59 100644 --- a/contracts/trusted_publication_contracts_test.go +++ b/contracts/trusted_publication_contracts_test.go @@ -15,16 +15,16 @@ func TestTrustedPublicationOpenAPIAndSchemaMappings(t *testing.T) { digest := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" binding := EndpointBindingResponse{BindingID: "binding-1", ProviderID: "provider-1", AgentID: "agent-1", AgentCardVersion: "1.0.0-beta.1+build.7", Endpoint: "https://agent.example/a2a", VerificationMethod: "http_well_known", VerificationStatus: "verified", VerificationEvidenceDigest: &digest, CreatedAt: now, UpdatedAt: now, VerifiedAt: &now} challenge := VerificationChallengeResponse{ChallengeID: "challenge-1", BindingID: "binding-1", ChallengeURL: "https://agent.example/.well-known/nekiro/challenges/challenge-1", Proof: "proof", ExpiresAt: now} - validateOpenAPIValue(t, document.Paths.Find("/v4/providers/{providerId}/agents/{agentId}/endpoint-bindings").Post.Responses.Status(201).Value.Content["application/json"].Schema, binding) - validateOpenAPIValue(t, document.Paths.Find("/v4/providers/{providerId}/agents/{agentId}/endpoint-bindings").Post.RequestBody.Value.Content["application/json"].Schema, map[string]any{"endpoint": "https://agent.example/a2a", "method": "http_well_known", "version": "1.0.0-beta.1+build.7"}) - validateOpenAPIValue(t, document.Paths.Find("/v4/providers/{providerId}/endpoint-bindings/{bindingId}/challenges").Post.Responses.Status(201).Value.Content["application/json"].Schema, challenge) + validateOpenAPIValue(t, document.Paths.Find("/v1/providers/{providerId}/agents/{agentId}/endpoint-bindings").Post.Responses.Status(201).Value.Content["application/json"].Schema, binding) + validateOpenAPIValue(t, document.Paths.Find("/v1/providers/{providerId}/agents/{agentId}/endpoint-bindings").Post.RequestBody.Value.Content["application/json"].Schema, map[string]any{"endpoint": "https://agent.example/a2a", "method": "http_well_known", "version": "1.0.0-beta.1+build.7"}) + validateOpenAPIValue(t, document.Paths.Find("/v1/providers/{providerId}/endpoint-bindings/{bindingId}/challenges").Post.Responses.Status(201).Value.Content["application/json"].Schema, challenge) release := AgentReleaseResponse{ReleaseID: "release-1", ProviderID: "provider-1", AgentID: "agent-1", AgentCardVersion: "1.0.0-beta.1+build.7", CardDigest: digest, EndpointBindingID: "binding-1", EndpointOrigin: "https://agent.example", EndpointPath: "/a2a", VerificationMethod: "http_well_known", VerificationEvidenceDigest: &digest, State: ReleaseStatePublished, CreatedAt: now, UpdatedAt: now, VerifiedAt: &now, PublishedAt: &now} - validateOpenAPIValue(t, document.Paths.Find("/v4/providers/{providerId}/agents/{agentId}/releases").Post.RequestBody.Value.Content["application/json"].Schema, CreateAgentReleaseRequest{Version: "1.0.0-beta.1+build.7", EndpointBindingID: "binding-1"}) - assertOpenAPIValueRejected(t, document.Paths.Find("/v4/providers/{providerId}/agents/{agentId}/releases").Post.RequestBody.Value.Content["application/json"].Schema, CreateAgentReleaseRequest{Version: "not-semver", EndpointBindingID: "binding-1"}) - assertOpenAPIValueRejected(t, document.Paths.Find("/v4/providers/{providerId}/agents/{agentId}/releases").Post.RequestBody.Value.Content["application/json"].Schema, CreateAgentReleaseRequest{Version: "1.0.0", EndpointBindingID: "bad binding"}) + validateOpenAPIValue(t, document.Paths.Find("/v1/providers/{providerId}/agents/{agentId}/releases").Post.RequestBody.Value.Content["application/json"].Schema, CreateAgentReleaseRequest{Version: "1.0.0-beta.1+build.7", EndpointBindingID: "binding-1"}) + assertOpenAPIValueRejected(t, document.Paths.Find("/v1/providers/{providerId}/agents/{agentId}/releases").Post.RequestBody.Value.Content["application/json"].Schema, CreateAgentReleaseRequest{Version: "not-semver", EndpointBindingID: "binding-1"}) + assertOpenAPIValueRejected(t, document.Paths.Find("/v1/providers/{providerId}/agents/{agentId}/releases").Post.RequestBody.Value.Content["application/json"].Schema, CreateAgentReleaseRequest{Version: "1.0.0", EndpointBindingID: "bad binding"}) assertOpenAPIValueRejected(t, document.Components.Parameters["releaseId"].Value.Schema, "bad release") - validateOpenAPIValue(t, document.Paths.Find("/v4/providers/{providerId}/agents/{agentId}/releases").Post.Responses.Status(201).Value.Content["application/json"].Schema, release) - for _, path := range []string{"/v4/releases/{releaseId}/verify", "/v4/releases/{releaseId}/publish", "/v4/releases/{releaseId}/suspend", "/v4/releases/{releaseId}/revoke"} { + validateOpenAPIValue(t, document.Paths.Find("/v1/providers/{providerId}/agents/{agentId}/releases").Post.Responses.Status(201).Value.Content["application/json"].Schema, release) + for _, path := range []string{"/v1/releases/{releaseId}/verify", "/v1/releases/{releaseId}/publish", "/v1/releases/{releaseId}/suspend", "/v1/releases/{releaseId}/revoke"} { validateOpenAPIValue(t, document.Paths.Find(path).Post.Responses.Status(200).Value.Content["application/json"].Schema, release) } publicError, err := NewTrustedPublicationError(TrustedErrorRedirectNotAllowed, "trace-trusted-publication") diff --git a/contracts/workspace_api_contracts_test.go b/contracts/workspace_api_contracts_test.go index 50518efb..8dc71d2e 100644 --- a/contracts/workspace_api_contracts_test.go +++ b/contracts/workspace_api_contracts_test.go @@ -80,7 +80,7 @@ func TestWorkspaceAndInstallationV2Schemas(t *testing.T) { } func TestWorkspaceV3OperationsDeclareSecurityTraceAndExactErrors(t *testing.T) { - document := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane.v3.yaml")) + document := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane.v1.yaml")) tests := []struct { path string method string @@ -88,31 +88,31 @@ func TestWorkspaceV3OperationsDeclareSecurityTraceAndExactErrors(t *testing.T) { failures map[int][]string }{ { - path: "/v3/workspaces", method: "POST", success: 201, + path: "/v1/workspaces", method: "POST", success: 201, failures: map[int][]string{400: {"VALIDATION_ERROR"}, 401: {"UNAUTHENTICATED"}, 409: {"CONFLICT"}, 503: {"DEPENDENCY_ERROR"}}, }, { - path: "/v3/workspaces/{workspaceId}", method: "GET", success: 200, + path: "/v1/workspaces/{workspaceId}", method: "GET", success: 200, failures: workspaceReadFailures(), }, { - path: "/v3/workspaces/{workspaceId}/installations", method: "POST", success: 201, + path: "/v1/workspaces/{workspaceId}/installations", method: "POST", success: 201, failures: map[int][]string{400: {"VALIDATION_ERROR"}, 401: {"UNAUTHENTICATED"}, 403: {"FORBIDDEN", "AGENT_RELEASE_UNPUBLISHED", "AGENT_RELEASE_SUSPENDED", "AGENT_RELEASE_REVOKED"}, 404: {"NOT_FOUND"}, 409: {"CONFLICT"}, 503: {"DEPENDENCY_ERROR"}}, }, { - path: "/v3/workspaces/{workspaceId}/installations", method: "GET", success: 200, + path: "/v1/workspaces/{workspaceId}/installations", method: "GET", success: 200, failures: workspaceReadFailures(), }, { - path: "/v3/workspaces/{workspaceId}/installations/{installationId}", method: "GET", success: 200, + path: "/v1/workspaces/{workspaceId}/installations/{installationId}", method: "GET", success: 200, failures: workspaceReadFailures(), }, { - path: "/v3/workspaces/{workspaceId}/installations/{installationId}", method: "PATCH", success: 200, + path: "/v1/workspaces/{workspaceId}/installations/{installationId}", method: "PATCH", success: 200, failures: workspaceMutationFailures(), }, { - path: "/v3/workspaces/{workspaceId}/installations/{installationId}", method: "DELETE", success: 200, + path: "/v1/workspaces/{workspaceId}/installations/{installationId}", method: "DELETE", success: 200, failures: workspaceMutationFailures(), }, } @@ -179,7 +179,7 @@ func TestResolveAgentResponsePreservesExactRequestIdentity(t *testing.T) { } func TestWorkspaceV3GoMappings(t *testing.T) { - document := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane.v3.yaml")) + document := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane.v1.yaml")) now := time.Date(2026, 7, 14, 10, 0, 0, 0, time.UTC) workspace := Workspace{WorkspaceID: "workspace-1", OwnerID: "owner-1", CreatedAt: now, UpdatedAt: now} installation := validInstallation() @@ -188,14 +188,14 @@ func TestWorkspaceV3GoMappings(t *testing.T) { uninstalled.UpdatedAt = now uninstalled.UninstalledAt = &now - create := findOperation(t, document, "/v3/workspaces", "POST") + create := findOperation(t, document, "/v1/workspaces", "POST") validateOpenAPIValue(t, create.RequestBody.Value.Content["application/json"].Schema, CreateWorkspaceRequest{WorkspaceID: workspace.WorkspaceID}) validateOpenAPIValue(t, create.Responses.Status(201).Value.Content["application/json"].Schema, workspace) - read := findOperation(t, document, "/v3/workspaces/{workspaceId}", "GET") + read := findOperation(t, document, "/v1/workspaces/{workspaceId}", "GET") validateOpenAPIValue(t, read.Responses.Status(200).Value.Content["application/json"].Schema, workspace) - collection := document.Paths.Find("/v3/workspaces/{workspaceId}/installations") + collection := document.Paths.Find("/v1/workspaces/{workspaceId}/installations") if collection.Get.Parameters.GetByInAndName("query", "limit") == nil || collection.Get.Parameters.GetByInAndName("query", "cursor") == nil { t.Fatal("Installation list limit/cursor parameters are missing") } @@ -220,7 +220,7 @@ func TestWorkspaceV3GoMappings(t *testing.T) { validateOpenAPIValue(t, collection.Get.Responses.Status(200).Value.Content["application/json"].Schema, InstallationList{Items: []Installation{installation, uninstalled}, NextCursor: &cursor}) validateOpenAPIValue(t, collection.Get.Responses.Status(200).Value.Content["application/json"].Schema, InstallationList{Items: []Installation{}}) - item := document.Paths.Find("/v3/workspaces/{workspaceId}/installations/{installationId}") + item := document.Paths.Find("/v1/workspaces/{workspaceId}/installations/{installationId}") validateOpenAPIValue(t, item.Get.Responses.Status(200).Value.Content["application/json"].Schema, uninstalled) validateOpenAPIValue(t, item.Patch.RequestBody.Value.Content["application/json"].Schema, UpdateInstallationRequest{Status: "disabled"}) validateOpenAPIValue(t, item.Patch.Responses.Status(200).Value.Content["application/json"].Schema, installation) @@ -228,8 +228,8 @@ func TestWorkspaceV3GoMappings(t *testing.T) { } func TestWorkspaceV3LifecycleContractIsTerminalAndNonIdempotent(t *testing.T) { - document := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane.v3.yaml")) - item := document.Paths.Find("/v3/workspaces/{workspaceId}/installations/{installationId}") + document := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane.v1.yaml")) + item := document.Paths.Find("/v1/workspaces/{workspaceId}/installations/{installationId}") if item == nil || item.Patch == nil || item.Delete == nil { t.Fatal("active lifecycle operations are missing") } @@ -265,8 +265,8 @@ func TestWorkspaceV3LifecycleContractIsTerminalAndNonIdempotent(t *testing.T) { } func TestControlPlaneInternalResolutionDeclaresTrustedIdentityAndTrace(t *testing.T) { - document := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane-internal.v2.yaml")) - operation := findOperation(t, document, "/internal/v2/resolve-agent", "POST") + document := loadOpenAPIDocument(t, filepath.Join("openapi", "control-plane-internal.v1.yaml")) + operation := findOperation(t, document, "/internal/v1/resolve-agent", "POST") if operation.Security == nil || len(*operation.Security) != 1 { t.Fatal("internal Bearer security requirement is missing") } diff --git a/docs/architecture/phase-1-spec.md b/docs/architecture/phase-1-spec.md index 6807c142..58bbdbbc 100644 --- a/docs/architecture/phase-1-spec.md +++ b/docs/architecture/phase-1-spec.md @@ -55,21 +55,22 @@ Console -> Control Plane -> A2A Router -> Agents Cross-language contracts are owned by language-neutral artifacts: - `contracts/schemas/` contains versioned JSON Schema documents. -- `contracts/openapi/control-plane.v3.yaml` defines the active Catalog, - Discovery, Workspace, and Installation Northbound API; `control-plane.v2.yaml` - remains unchanged migration evidence. Any legacy Invocation paths still - present in the v3 document are migration evidence and are not served by the - current Gateway. +- `contracts/openapi/control-plane.v1.yaml` defines the active Catalog, + Discovery, Workspace, and Installation Gateway API. - `contracts/openapi/public-agent-share.v1.yaml` and `contracts/schemas/public-agent-share.v1.schema.json` define the anonymous - public Agent identity projection. `GET /v4/public/agents/:publicAgentId` + public Agent identity projection. `GET /v1/public/agents/:publicAgentId` exposes only the canonical public URL and eligible published trusted Release facts; it never exposes an endpoint, binding, evidence, credential, Workspace, or Ledger data. -- `contracts/openapi/control-plane-invocation.v4.yaml` defines the active - Invocation and Trace Northbound API. -- `contracts/openapi/control-plane-internal.v2.yaml` defines Router-to-Control Plane exact Agent resolution; `control-plane-internal.v3.yaml` defines nested installed-version resolution. -- `contracts/openapi/router-internal.v4.yaml` defines active Control Plane-to-Router dispatch and result transport; `router-metadata.v3.yaml` is the active Workspace-scoped Invocation/Trace read contract, while the complete `router-internal.v3.yaml` is historical migration evidence. +- `contracts/openapi/control-plane-invocation.v1.yaml` defines the active + Invocation and Trace Gateway API. +- `contracts/openapi/control-plane-internal.v1.yaml` defines Router-to-Control + Plane exact Agent resolution; `control-plane-installed-version.v1.yaml` + defines nested installed-version resolution. +- `contracts/openapi/router-internal.v1.yaml` defines active Control + Plane-to-Router dispatch and result transport; `router-metadata.v1.yaml` + defines Workspace-scoped Invocation/Trace reads. - `contracts/openapi/router-topology-status.v1.yaml` defines the authenticated, read-only Router-local watched-topology status used to prove exact-Release lifecycle consumption without exposing endpoints or provider revision tokens. @@ -82,9 +83,9 @@ Cross-language contracts are owned by language-neutral artifacts: Go and TypeScript types are consumers of these artifacts, never competing sources of truth. Services must not exchange internal implementation types across a process boundary. -Historical v1 files remain unchanged as migration evidence. The first backend -runtime implements only the active versions and does not introduce speculative -dual-version behavior. +All NeKiro-owned HTTP boundaries use v1 for the first release. Payload schemas +retain their independent identities. Retired pre-release URL versions are not +served and do not introduce dual-version behavior. ## Router-to-Agent authentication @@ -97,33 +98,33 @@ execute runtime logic only after all claims match single-valued context headers. Stream and cancel requests use different credentials. No credential, key, signature, or `jti` enters Agent Card, result, event, or Ledger storage. -## Northbound API v3: Catalog and Workspace +## Gateway API v1: Catalog and Workspace | Method | Path | Purpose | | --- | --- | --- | -| `POST` | `/v3/agents` | Register a draft Agent Card v0.2 version | -| `POST` | `/v3/agents/:agentId/versions/:version/publish` | Publish an immutable version | -| `POST` | `/v3/agents/:agentId/versions/:version/disable` | Disable a version for new resolutions | -| `GET` | `/v3/agents` | Discover published agents by query/capability/owner | -| `GET` | `/v3/agents/:agentId/versions/:version` | Read an exact Agent Card version | -| `POST` | `/v3/workspaces` | Create a minimal owner-controlled Workspace | -| `GET` | `/v3/workspaces/:workspaceId` | Read an owned Workspace | -| `POST` | `/v3/workspaces/:workspaceId/installations` | Install and accept declared permissions | -| `GET` | `/v3/workspaces/:workspaceId/installations` | List current and historical Installations | -| `GET` | `/v3/workspaces/:workspaceId/installations/:installationId` | Read one exact Installation | -| `PATCH` | `/v3/workspaces/:workspaceId/installations/:installationId` | Enable or disable an installation | -| `DELETE` | `/v3/workspaces/:workspaceId/installations/:installationId` | Uninstall and return preserved history | +| `POST` | `/v1/agents` | Register a draft Agent Card v0.2 version | +| `POST` | `/v1/agents/:agentId/versions/:version/publish` | Publish an immutable version | +| `POST` | `/v1/agents/:agentId/versions/:version/disable` | Disable a version for new resolutions | +| `GET` | `/v1/agents` | Discover published agents by query/capability/owner | +| `GET` | `/v1/agents/:agentId/versions/:version` | Read an exact Agent Card version | +| `POST` | `/v1/workspaces` | Create a minimal owner-controlled Workspace | +| `GET` | `/v1/workspaces/:workspaceId` | Read an owned Workspace | +| `POST` | `/v1/workspaces/:workspaceId/installations` | Install and accept declared permissions | +| `GET` | `/v1/workspaces/:workspaceId/installations` | List current and historical Installations | +| `GET` | `/v1/workspaces/:workspaceId/installations/:installationId` | Read one exact Installation | +| `PATCH` | `/v1/workspaces/:workspaceId/installations/:installationId` | Enable or disable an installation | +| `DELETE` | `/v1/workspaces/:workspaceId/installations/:installationId` | Uninstall and return preserved history | Public sharing is a read-only Catalog projection, not a second installation or invocation boundary: | Method | Path | Purpose | | --- | --- | --- | -| `GET` | `/v4/public/agents/:publicAgentId` | Resolve a stable public Agent ID anonymously to exact selectable Releases | +| `GET` | `/v1/public/agents/:publicAgentId` | Resolve a stable public Agent ID anonymously to exact selectable Releases | The Console requires an exact configured `VITE_NEKIRO_PUBLIC_AGENT_ORIGIN` and accepts only canonical `/a/:publicAgentId` URLs. Installation reuses the -authenticated `/v3/workspaces/:workspaceId/installations` contract, so the +authenticated `/v1/workspaces/:workspaceId/installations` contract, so the public URL never reaches Agent transport and never authorizes a Workspace. The Gateway returns Platform Error v2 for Catalog failures and Platform Error v3 @@ -132,13 +133,13 @@ cannot contain internal dependency errors, credentials, request payloads, or Agent output. `INSTALLATION_DISABLED` identifies Workspace authorization state while `AGENT_DISABLED` identifies Catalog version state. -## Northbound Invocation API v4 +## Gateway Invocation API v1 | Method | Path | Purpose | | --- | --- | --- | -| `POST` | `/v4/workspaces/:workspaceId/invocations` | Authorize, dispatch, and return a transient JSON or SSE result | -| `GET` | `/v4/workspaces/:workspaceId/invocations/:invocationId` | Read one Workspace-scoped invocation and metadata-only Ledger events | -| `GET` | `/v4/workspaces/:workspaceId/traces/:traceId` | Read Workspace-scoped metadata-only parent/child invocation lineage | +| `POST` | `/v1/workspaces/:workspaceId/invocations` | Authorize, dispatch, and return a transient JSON or SSE result | +| `GET` | `/v1/workspaces/:workspaceId/invocations/:invocationId` | Read one Workspace-scoped invocation and metadata-only Ledger events | +| `GET` | `/v1/workspaces/:workspaceId/traces/:traceId` | Read Workspace-scoped metadata-only parent/child invocation lineage | The Invocation Gateway uses Platform Error v4 after the runtime acceptance boundary. Trace correlation is required; Invocation and root Task correlation @@ -149,21 +150,21 @@ represented as not found, an empty list, or success. | Method | Path | Owner | Purpose | | --- | --- | --- | --- | -| `POST` | `/internal/v2/resolve-agent` | Control Plane | Resolve an authorized installed exact Agent Card v0.2 and capability | -| `POST` | `/internal/v3/resolve-installed-version` | Control Plane | Resolve the exact enabled Installation pin for a nested call | -| `POST` | `/internal/v4/invocations` | Router | Execute an authorized root invocation and return a transient JSON or SSE result | -| `GET` | `/internal/v3/workspaces/:workspaceId/invocations/:invocationId` | Router | Read Workspace-scoped metadata-only Invocation detail | -| `GET` | `/internal/v3/workspaces/:workspaceId/traces/:traceId` | Router | Read Workspace-scoped metadata-only lineage | - -Control Plane Internal v2/v3 are served by the Control Plane and called by the -Router. Router Internal dispatch v4 is served by the Router and called by the Control -Plane. Their server destinations are distinct and explicitly configured. The -Router resolves cards through the internal Control Plane API and must not query +| `POST` | `/internal/v1/resolve-agent` | Control Plane | Resolve an authorized installed exact Agent Card v0.2 and capability | +| `POST` | `/internal/v1/resolve-installed-version` | Control Plane | Resolve the exact enabled Installation pin for a nested call | +| `POST` | `/internal/v1/invocations` | Router | Execute an authorized root invocation and return a transient JSON or SSE result | +| `GET` | `/internal/v1/workspaces/:workspaceId/invocations/:invocationId` | Router | Read Workspace-scoped metadata-only Invocation detail | +| `GET` | `/internal/v1/workspaces/:workspaceId/traces/:traceId` | Router | Read Workspace-scoped metadata-only lineage | + +Control Plane Internal v1 is served by the Control Plane and called by the +Router. Router Internal v1 is served by the Router and called by the Control +Plane. Their destinations are distinct and explicitly configured. The Router +resolves Cards through the internal Control Plane API and must not query Registry or Workspace tables directly. ## Invocation Result Delivery -`POST /v4/workspaces/:workspaceId/invocations` is the only Northbound result +`POST /v1/workspaces/:workspaceId/invocations` is the only Northbound result channel. `stream=false` returns one `application/json` Invocation Result v1. `stream=true` returns ordered `text/event-stream` Invocation Result Stream Event v2 values on the same response. The request mode and `Accept` header must diff --git a/docs/contracts/compatibility.md b/docs/contracts/compatibility.md index 42544a66..830b0a15 100644 --- a/docs/contracts/compatibility.md +++ b/docs/contracts/compatibility.md @@ -1,271 +1,92 @@ # Contract Compatibility Policy -## Independent Versions - -Agent Card Schema version, Agent version, HTTP API version, internal API -version, event version, result version, A2A Profile Schema version, and A2A -protocol version are independent values. They must not be inferred from one -another. - -The versioned JSON Schema, OpenAPI, semantic-rule, conformance, and A2A Profile -files under `contracts/` are contract facts. Go and TypeScript mappings are -consumers and must not redefine their semantics. - -## Phase 1 Contract Set - -| Contract | Historical | Active target | Compatibility impact | -| --- | --- | --- | --- | -| Agent Card Schema | `0.1` | `0.2` | Breaking: portable semantic rejection rules narrow accepted Cards | -| Trusted Publication | none | `v1` | New Registry-owned provider, endpoint-binding, challenge, and typed verification-error contract | -| Workspace Schema | none | `v1` | New minimal authorization-root fact | -| Installation Schema | `v1` | `v2` | Breaking: canonical semantic invariants are frozen | -| Northbound API | `v1` / `v2` | `v3` | Breaking: v3 completes authenticated Workspace/Installation semantics and body-bearing uninstall | -| Control Plane Internal API | `v1` | `v2` exact Card resolution / `v3` installed-version resolution | v2 is breaking from v1; v3 additively owns deterministic nested version selection | -| Router Internal metadata API | `v1` / `v2` | `v3` | Breaking: Workspace-scoped metadata reads use the runtime contract | -| Invocation Event Schema | `0.1` | `0.2` | Breaking: terminal status and error-code combinations are stricter | -| Platform Error | `v1` | `v2` / `v3` | v2 remains active for Catalog/Invocation; v3 adds Workspace `INSTALLATION_DISABLED` | -| Invocation Result | none | `v1` | New transient JSON and SSE result contracts | -| A2A Profile Schema | `0.1` | `0.2` | Breaking profile metadata and conformance requirements | -| Public Agent Share | none | `v1` | New anonymous, secrecy-safe public identity and exact published Release projection | -| Router Invocation Credential | none | `v1` | New companion contract: exact Ed25519 Router-to-Agent request authentication | -| Router Topology Status | none | `v1` | New authenticated, read-only, secrecy-safe projection of Router-local watched topology | -| A2A protocol | `0.3.0` | `0.3.0` | Unchanged wire protocol | - -Spec 011 adds invocation-runtime targets without replacing the active Catalog -and Workspace surfaces: - -| Contract | Historical | Runtime target | Compatibility impact | -| --- | --- | --- | --- | -| Northbound Invocation API | invocation routes in Control Plane `v3` | invocation-only `v4` | Breaking acceptance, size, error, and persistence-interruption semantics; Catalog/Workspace/Installation remain on v3 | -| Router Internal dispatch API | `v1` / `v2` / `v3` | `v4` | Breaking service-auth, managed `http_bearer` acceptance, size, and post-side-effect failure semantics; v3 dispatch is historical evidence | -| Agent Router API | none | `v1` | New authenticated Agent-SDK direction and parent-derived trust model | -| Control Plane Internal API | `v1` | `v2` exact Card resolution / `v3` installed-version resolution | v3 adds a phase-aware nested version-selection operation without dual-read fallback | -| Platform Error | `v1` / `v2` / `v3` | `v4` for invocation runtime | Breaking closed pre/correlated shapes and exact unsupported-auth/request-size/Agent-response-size outcomes | -| Invocation Event | `0.1` / `0.2` | `0.3` | Breaking embedded Platform Error v4 revision | -| Result Stream Event | `v1` | `v2` | Breaking embedded Platform Error v4 revision | - -Historical files remain unchanged as migration evidence. The first backend -runtime implements only active targets. No deployed runtime consumer exists, -so there is no dual-read, dual-write, or dual-dispatch compatibility window. -All consumers must adopt the active target before that runtime is introduced. - -Router Invocation Credential `v1` is a separately versioned companion contract -for the managed Router-to-Agent HTTP hop. It owns the complete signed claim and -context-header binding, strict 401/403 response shape, and portable conformance -corpus under `contracts/router-agent-credential/v1/`. It does not modify A2A -Profile Schema `0.2`, Agent Card Schema `0.2`, Router Internal dispatch API `v4`, -Router Internal metadata API `v3` (`router-metadata.v3.yaml`), result contracts, -or Invocation Ledger facts. The complete `router-internal.v3.yaml` is -historical evidence and is not an active dependency. -Agent-to-Router nested credentials remain -the existing opaque Workspace/Agent binding in the opposite direction. - -Router Topology Status `v1` is a separately versioned internal read contract at -`GET /internal/v1/instance-topology/status`. It is additive because no prior -status endpoint or client existed. The response contains only the selected -provider, exact Agent/version/Release identity, safe observation state, -observation-local revision, and Router observation timestamp. It never exposes -endpoints, source tokens, instance metadata, Card/config payloads, credentials, -or Agent inputs/results. Reads do not establish an observation, probe a -provider, retry, reconnect, refresh, or mutate state. Future field removal, -requiredness changes, state reinterpretation, or new exhaustive state values -require a new contract version or explicit consumer-impact review. - -## Catalog v2 Completion - -Spec 002 additively completes the existing Northbound v2 Catalog operations -before their first runtime implementation. The success representations and -operation paths are unchanged. The active document now makes previously -unspecified behavior explicit: - -- all five Catalog operations require Gateway Bearer authentication; -- every Catalog response carries the Gateway-assigned `x-nek-trace-id`; -- registration and lifecycle mutation enforce immutable owner identity; -- Trusted Publication v1 release records copy exact Card, endpoint binding, - provider, and digest facts; `installedReleaseId` is an additive optional - Installation field for trusted pins. Catalog migration marks every pre-v4 - published row as `legacy_unverified`; a missing Release on - a new version is not a compatibility signal. Trusted invocation metadata - carries the exact Release ID and Card digest into Router/Ledger records; - the absence of both fields is the explicit legacy/unverified wire encoding - retained for historical events. Control Plane Internal v2 additively returns - the exact Catalog-owned Card digest beside `installedReleaseId`, and Router - rejects dispatch provenance that omits or differs from that pair instead of - recomputing historical Card bytes; -- Platform Error v3/v4 add stable Release-state codes for unpublished, - suspended, and revoked Releases; no previously valid error payload changes; -- published exact versions are authenticated-visible, while draft and disabled - exact versions are owner-visible only; -- omitted discovery limit is the product policy `25`, explicit limits are - `1-100`, and opaque cursors are bound to filters and traversal boundary; -- validation, unauthenticated, forbidden, not found, conflict, and dependency - failures use their exact Platform Error v2 status/code mappings. -- the registration transport cap is 16,777,216 bytes and uses the existing - validation failure, while active unbounded JSON integer fields keep exact - `json.Number` semantics instead of a machine `int64` range. - -No existing deployed Catalog runtime or generated client consumes the earlier -underspecified form, so a new API version or compatibility window is not -required. Northbound v1 and Agent Card 0.1 remain byte-unchanged historical -evidence and receive no runtime route, decoder, auto-upgrade, or fallback. - -## Workspace And Installation Contract Gate - -Spec 003 completes the previously partial Workspace/Installation foundations -before their first runtime implementation: - -- Workspace v1 adds the exact four-field logical authorization root: - `workspaceId`, immutable trusted `ownerId`, `createdAt`, and `updatedAt`. -- Installation v2 keeps the submitted constraint, exact installed version, - accepted permission snapshot, state, and timestamps; it additionally freezes - canonical permission order, constraint-compatible exact pins, and timestamp - relationships. `uninstalledAt` is - required only for terminal uninstalled history. -- Northbound v2 remains byte-unchanged migration evidence. Northbound v3 - completes Workspace create/read and Installation create/read/list/lifecycle - with Bearer security, Trace headers, Installation v2 responses, and - operation-specific fixed errors. -- Northbound v3 uninstall returns `200` with the preserved terminal - Installation v2 fact. Historical v2 retains its original `204` behavior. -- Installation list inspection in v3 requires an explicit bounded `limit` - (range 1-100), stable keyset order, and an opaque continuation cursor. -- Control Plane Internal v2 requires a separately trusted service Bearer - identity, distinguishes missing Installation, Installation disabled, Catalog - version disabled, capability denial, and dependency failure, and defines a - pre-correlation error shape for malformed/missing IDs. -- Control Plane Internal v3 uses the same service boundary and validates - phase-specific status/code/correlation combinations for installed-version - resolution; it never falls back to v2 for that operation. -- Platform Error v3 adds `INSTALLATION_DISABLED` with fixed message - `The Agent installation is disabled.` `AGENT_DISABLED` retains its Catalog - Agent-version meaning; existing Platform Error v2 remains unchanged. -- The previous `common.v1` `semverRange` length tightening is removed; SemVer - parser validation remains the sole active range constraint. - -Northbound v2 and Installation v1 remain byte-unchanged historical evidence. Installation v1's structural -shape did not freeze the v2 semantic invariants, so first Workspace consumers -must adopt Installation v2. Control Plane Internal v1 remains historical and -must not be dual-read; first Router consumers use v2/v3 according to operation. Platform Error v2 remains -the active Catalog/Invocation contract in Northbound v3, while first Workspace and -internal-resolution consumers use v3. No deployed Workspace or Router -resolution runtime exists, so these version increments need no compatibility -runtime window. First runtime consumers implement v3 only; migration impact is -explicit in the active contract guide. - -Historical Northbound v1/v2, Agent Card 0.1, Router Internal v1, and all other -historical artifacts remain unchanged migration evidence. - -## Northbound v3 Migration - -- Replace `/v2` Northbound paths with their `/v3` equivalents. -- Supply an explicit Installation list `limit` from 1 through 100; omission is - a validation error and has no default. -- Consume uninstall as `200 application/json` with an Installation v2 body. -- Do not run v2 and v3 as a fallback pair. v2 remains contract history only. - -## Invocation Runtime Target Migration - -- Keep Catalog, Workspace, and Installation clients on - `control-plane.v3.yaml`. Use `control-plane-invocation.v4.yaml` at the same - Gateway destination only for `/v4/workspaces/{workspaceId}/invocations...` - and `/v4/workspaces/{workspaceId}/traces/...`. The invocation-only document is not a second fact for - the v3-owned domains. -- Legacy Invocation paths embedded in `control-plane.v3.yaml` are migration - evidence only; no runtime may serve them or pair them with the v4 routes. -- Control Plane Dispatch uses Router Internal dispatch v4. Workspace-scoped - Invocation/Trace reads use Router Internal metadata v3. Agent SDKs use Agent Router - v1 with an Agent-bound credential; the caller classes and credentials are not - interchangeable. -- Adopt Platform Error v4, Invocation Event 0.3, and Result Stream Event v2 - together. Treat pre-acceptance HTTP 413 as `PAYLOAD_TOO_LARGE`; treat HTTP - 502/in-band failed as `AGENT_AUTH_UNSUPPORTED` or - `AGENT_RESPONSE_TOO_LARGE` only when that exact code is present. After - acceptance the correlated error shape is mandatory. -- Replace unscoped v3 `/v3/invocations/{invocationId}` and - `/v3/traces/{traceId}` reads with Workspace-scoped - `/v4/workspaces/{workspaceId}/invocations/{invocationId}` and - `/v4/workspaces/{workspaceId}/traces/{traceId}`. Consume the Invocation - detail projection/events and Trace lineage projection responses; v4 does not - expose raw event arrays. -- Use the exact shared Accept matrix: non-stream JSON accepts - `application/json`, `application/*`, or `*/*`; stream accepts only - `text/event-stream`. Do not normalize or fall back from unsupported values. -- Every active Northbound Invocation v4 and Router Internal dispatch v4 - response carries exactly one `x-nek-trace-id`. The Router response Trace must - equal the Gateway-created dispatch Trace; Gateway retains that original - northbound Trace rather than selecting a downstream replacement. -- HTTP 500 is the explicit `INTERNAL_ERROR` mapping on both invocation v4 - surfaces and permits the phase-appropriate pre- or correlated Platform Error - v4 shape. It is not compatible with the HTTP 503 dependency/unavailable - mapping. -- Configure every deadline/size value explicitly. Omission or invalid text is a - startup/readiness failure and has no migration default. -- Treat successful `created` commit as acceptance. A post-side-effect - `DEPENDENCY_ERROR` may coexist with a last committed non-terminal Ledger - history; do not infer or synthesize a terminal outcome. -- Do not run v3/v4 Northbound Invocation or v3/v4 Router dispatch as fallback - pairs. No deployed runtime consumer justifies a compatibility window; the v3 - dispatch route is retired while v3 metadata reads remain active. -- Go applications may consume this active surface through the - [`client`](https://pkg.go.dev/github.com/NeKiro-project/nekiro-sdk-go/client) - package in `github.com/NeKiro-project/nekiro-sdk-go`. The Client SDK targets - only the Gateway v4 Workspace invocation route and does not probe v3, Router - Internal, Agent Router, or provider endpoints. +## Released HTTP API identity + +NeKiro `v0.1.0` exposes one version at each NeKiro-owned HTTP boundary: + +| Owner boundary | Active prefix | Active OpenAPI documents | +| --- | --- | --- | +| Gateway | `/v1` | `control-plane.v1.yaml`, `control-plane-invocation.v1.yaml`, `trusted-publication.v1.yaml`, `public-agent-share.v1.yaml` | +| Control Plane internal | `/internal/v1` | `control-plane-internal.v1.yaml`, `control-plane-installed-version.v1.yaml` | +| Router internal | `/internal/v1` | `router-internal.v1.yaml`, `router-metadata.v1.yaml`, `router-topology-status.v1.yaml` | +| Agent-to-Router | `/agent/v1` | `router-agent.v1.yaml` | + +The pre-release `/v2`, `/v3`, and `/v4` routes and OpenAPI documents are not +released compatibility surfaces. They are removed rather than served as +aliases. See [ADR 0021](../decisions/0021-pre-release-platform-api-v1-reset.md) +and the [migration guide](../usage/platform-api-v1-migration.md). + +## Independent payload versions + +HTTP API version, Agent Card Schema, Agent version, Workspace/Installation +schema, Invocation Event, Platform Error, result, A2A Profile, A2A protocol, +Router credential, and topology projection versions are independent. A URL +version must never be inferred from a payload `schemaVersion`, and resetting +the URL API does not rewrite persisted facts. + +| Contract | Active identity | +| --- | --- | +| Agent Card Schema | `0.2` | +| Workspace Schema | `1` | +| Installation Schema | `2` | +| Public Agent Share | `1` | +| Invocation Result | `1` | +| Invocation Result Stream Event | `2` | +| Invocation Event | `0.3` for runtime/Ledger | +| Platform Error | surface-specific `2`, `3`, or `4` | +| A2A Profile Schema / protocol | `0.2` / `0.3.0` | +| Router Invocation Credential | `1` | +| Router Topology Status | `1` | + +Historical payload schemas remain readable where required for immutable +Release or Ledger provenance. Active validators do not silently upgrade, +downgrade, dual-read, or reinterpret them. + +## Compatible changes + +- Adding an optional field is compatible only when omission preserves the + existing meaning. +- Adding an endpoint is compatible only when existing clients and ownership + remain valid. +- Adding an enum value requires explicit consumer-impact review because an + exhaustive consumer may treat it as breaking. +- Documentation may clarify behavior only when it does not change accepted + input, output, failure, ownership, or security semantics. -## Compatible Changes +## Breaking changes -- Adding an optional field is additive when omission preserves existing - semantics. -- Adding a new endpoint or event type is additive only when existing consumers - remain valid. -- Adding an enum member requires consumer impact review because exhaustive - consumers may treat it as breaking. +The following require a new contract version, migration guidance, and an +explicit compatibility window after `v0.1.0`: -## Breaking Changes +- removing or renaming a field; +- changing type, requiredness, status code, media type, or fixed error meaning; +- tightening accepted values or semantic validation; +- moving behavior or data to a different owner; +- reinterpreting an immutable Release, Installation, Invocation, or Ledger + fact; +- changing the trusted caller, credential, or correlation source; +- changing an existing route without retaining its documented compatibility + policy. -- Removing or renaming a field -- Changing a field type or requiredness -- Changing an existing field's semantics -- Changing response status or media type for an existing operation -- Tightening accepted values or semantic validation rules -- Moving an operation to a different service owner or destination -- Reusing an error code for a different state -- Changing the fixed public message associated with an error code -- Reinterpreting historical Ledger events +## Failure and data semantics -Breaking changes require a new contract version, migration guidance, and an -explicit compatibility window or a documented pre-runtime declaration that no -compatibility runtime is justified. +Missing input, invalid input, not found, forbidden, disabled, dependency +failure, timeout, cancellation, and protocol failure remain distinct. They +must not collapse into `null`, an empty collection, a normal success response, +an automatic retry, or a request to an alternate endpoint. -## Invocation v2 Migration +Platform Error public messages and correlation are fixed by their payload +contract. Agent input, output, credentials, endpoint details, raw dependency +errors, and stack data are forbidden from metadata-only Ledger contracts. -- Replace Northbound `POST /v1/workspaces/{workspaceId}/invocations` and Router - `POST /internal/v1/invocations` acceptance handling with the corresponding v2 - same-request result operations. -- Send `Accept: application/json` or a compatible wildcard with `stream=false`. -- Send `Accept: text/event-stream` with `stream=true` and consume ordered SSE - data values until exactly one terminal event. -- Treat `406 NOT_ACCEPTABLE` as request negotiation failure. -- Treat EOF without a terminal event as interrupted delivery. Do not treat - received chunks as a successful result. -- Do not poll Ledger APIs for result content. Results are not persisted, - replayed, or recoverable after disconnect; obtaining output requires a new - Invocation. -- Route nested installed-version selection to Control Plane Internal v3, then - exact Card resolution to Control Plane Internal v2. Route dispatch to Router - Internal v4 and Ledger/trace reads to Router Internal metadata v3. +## Pre-release v1 cutover -## Failure And Data Semantics +All first-release consumers move to v1 together. There is no redirect, route +alias, dual-read, dual-write, dual-dispatch, downgrade, or old-Core fallback. +Retired paths return `404` before domain behavior. Required service URLs accept +only their exact v1 destination. -Missing input, invalid input, not found, forbidden, disabled, dependency -failure, timeout, cancellation, and protocol failure are distinct states. -Contracts must not collapse them into `null`, an empty collection, a boolean, -or a normal success response. +Fallback delta: removed every pre-release v2/v3/v4 HTTP path, retained 0, +added 0, net negative. -Catalog Platform Error v2, Workspace/Installation Platform Error v3, and runtime -Platform Error v4 contain only fixed public messages and safe correlation on -their respective surfaces. Agent input, result data, endpoint details, -credentials, raw dependency errors, and stack data are forbidden. Runtime -Invocation Event v0.3 and Ledger query contracts contain metadata only; the -historical Invocation Event v0.2 remains migration evidence and no result or -chunk field is compatible with the active metadata model. +Added fallback evidence: none. diff --git a/docs/decisions/0021-pre-release-platform-api-v1-reset.md b/docs/decisions/0021-pre-release-platform-api-v1-reset.md new file mode 100644 index 00000000..97fc4e70 --- /dev/null +++ b/docs/decisions/0021-pre-release-platform-api-v1-reset.md @@ -0,0 +1,89 @@ +# ADR 0021: Pre-release Platform API v1 Reset + +- Status: Accepted +- Date: 2026-08-17 +- Issue: [NeKiro#120](https://github.com/NeKiro-project/NeKiro/issues/120) + +## Context + +Before the first product release, NeKiro used URL versions `v2`, `v3`, and +`v4` to make incompatible implementation slices explicit while contracts were +still being discovered. Those numbers describe pre-release iteration order; +they do not represent three supported product generations. + +Publishing `v0.1.0` with all of those URL families would make a new adopter +configure several apparently independent API generations and would imply a +compatibility promise that NeKiro has never released. No published Core, +Console, Go SDK, Samples, or Stack release requires the pre-release paths. + +Payload contracts are different. Agent Card, Installation, Invocation Event, +Platform Error, result stream, A2A Profile, and Router credential identities +are stored or exchanged independently. Resetting a URL version does not permit +rewriting those schema identities or historical Ledger facts. + +## Decision + +The first released platform exposes exactly one version for every NeKiro-owned +HTTP trust boundary: + +| Boundary | Versioned prefix | +| --- | --- | +| Gateway | `/v1` | +| Control Plane and Router internal APIs | `/internal/v1` | +| Agent-to-Router API | `/agent/v1` | + +The current Catalog, Workspace, Installation, trusted publication, public +sharing, Invocation, and Ledger-read behavior moves to `/v1`. Exact Agent +resolution, installed-version resolution, Router dispatch, and Router metadata +reads move to `/internal/v1`. `/agent/v1/invocations` remains unchanged. + +Active OpenAPI documents use `info.version: 1.0.0`. Superseded HTTP API +documents named `v2`, `v3`, or `v4` are removed from the embedded contract +tree. Focused v1 documents may describe separate owners or domains, but they +do not create separate URL versions. + +There is no redirect, route alias, content negotiation fallback, dual-read, +dual-write, dual-dispatch, or automatic endpoint probing. Requests to retired +paths return the normal unmatched-route `404` and do not enter domain logic. +Strict service endpoint configuration accepts only the v1 paths. + +Independently versioned payload schemas keep their current identities. In +particular, Platform Error v2/v3/v4 and Invocation Event v0.3 remain valid +payload identities on their documented surfaces; they are not URL versions. + +## Release ordering + +1. Review the Core v1 contract and handler change. +2. Update Console, Go SDK, Samples, and Stack against the exact Core revision. +3. Prove the complete cross-runtime product loop with exact component commits. +4. Publish component tags and immutable image digests. +5. Publish the Stack compatibility manifest only after every exact revision is + green. + +Core required CI remains Core-only. Cross-repository acceptance stays in the +satellite-owned reusable workflows pinned by full commit SHA. + +## Consequences + +- New users configure one platform API generation. +- Generated clients and documentation no longer expose pre-release iteration + history as supported product surface. +- Every pre-release consumer must migrate atomically before the release. +- Independently versioned payload contracts remain explicit and may still have + version numbers different from the URL API. +- A future incompatible HTTP change requires a new URL version, migration + policy, and compatibility window after product release. + +## Compatibility + +This is an intentional breaking pre-release change. It has no runtime +compatibility window because there is no published product consumer to +preserve. The exact mapping is documented in +[`platform-api-v1-migration.md`](../usage/platform-api-v1-migration.md). + +## Fallback report + +Fallback delta: removed every pre-release v2/v3/v4 HTTP path, retained 0, +added 0, net negative. + +Added fallback evidence: none. diff --git a/docs/usage/platform-api-v1-migration.md b/docs/usage/platform-api-v1-migration.md new file mode 100644 index 00000000..339c097c --- /dev/null +++ b/docs/usage/platform-api-v1-migration.md @@ -0,0 +1,61 @@ +# Platform API v1 migration + +NeKiro `v0.1.0` uses one URL version at each owned HTTP boundary. This is a +pre-release cutover with no compatibility aliases. + +## Route mapping + +| Pre-release route family | v0.1.0 route family | +| --- | --- | +| `/v3/agents...` | `/v1/agents...` | +| `/v3/workspaces...` | `/v1/workspaces...` | +| `/v4/providers...` | `/v1/providers...` | +| `/v4/releases...` | `/v1/releases...` | +| `/v4/public/agents...` | `/v1/public/agents...` | +| `/v4/workspaces/{workspaceId}/invocations...` | `/v1/workspaces/{workspaceId}/invocations...` | +| `/v4/workspaces/{workspaceId}/traces...` | `/v1/workspaces/{workspaceId}/traces...` | +| `/internal/v2/resolve-agent` | `/internal/v1/resolve-agent` | +| `/internal/v3/resolve-installed-version` | `/internal/v1/resolve-installed-version` | +| `/internal/v4/invocations` | `/internal/v1/invocations` | +| `/internal/v3/workspaces/...` | `/internal/v1/workspaces/...` | +| `/agent/v1/invocations` | unchanged | + +Resource representations, status codes, media negotiation, error phase +semantics, exact Release provenance, and metadata-only Ledger rules are the +latest pre-release behavior; only the owning HTTP API identity is reset. + +## Required configuration changes + +- Set `NEKIRO_ROUTER_INTERNAL_URL` to the exact + `/internal/v1/invocations` URL. +- Set `NEKIRO_CONTROL_PLANE_RESOLVE_URL` to the exact + `/internal/v1/resolve-agent` URL. +- Set `NEKIRO_CONTROL_PLANE_VERSION_URL` to the exact + `/internal/v1/resolve-installed-version` URL. +- Update Gateway clients to construct only `/v1` public routes. +- Keep Agent SDK nested calls on `/agent/v1/invocations`. + +Configuration containing a retired path fails validation. Clients must not +probe an old path after a failure or reinterpret `404` as an empty result. + +## Payload versions + +URL API v1 does not rename independently versioned payloads. Consumers must +continue to honor the active Agent Card, Installation, Platform Error, +Invocation Event, result stream, A2A Profile, and Router credential schema +identities declared by the v1 OpenAPI documents. + +## Verification + +After migration: + +1. Run Core contract, unit, PostgreSQL integration, race, and vet checks. +2. Run Console and Go SDK client tests and confirm no active `/v2`, `/v3`, or + `/v4` request remains. +3. Run Stack backend and browser acceptance against exact component commits. +4. Confirm retired public and internal paths return `404` without invoking an + owner service. +5. Confirm one root and child Invocation preserve `root_task_id`, + `parent_invocation_id`, and `trace_id` in Ledger. + +There is no downgrade or mixed-version operating mode. diff --git a/docs/usage/trusted-publication-operations.md b/docs/usage/trusted-publication-operations.md index 98c8d4c3..d14dacf7 100644 --- a/docs/usage/trusted-publication-operations.md +++ b/docs/usage/trusted-publication-operations.md @@ -49,13 +49,13 @@ private/public key text, or `jti`. ## Publish a trusted Agent version -Register the Agent Card through `POST /v3/agents` before these steps. The Card +Register the Agent Card through `POST /v1/agents` before these steps. The Card declares the exact Agent version and endpoint; it contains no endpoint secret. 1. Create an Endpoint Binding for that exact version. ```powershell - $binding = Invoke-RestMethod -Method Post -Uri "$gateway/v4/providers/$providerId/agents/$agentId/endpoint-bindings" -Headers $headers -ContentType 'application/json' -Body (@{ + $binding = Invoke-RestMethod -Method Post -Uri "$gateway/v1/providers/$providerId/agents/$agentId/endpoint-bindings" -Headers $headers -ContentType 'application/json' -Body (@{ endpoint = $endpoint method = 'http_well_known' version = $version @@ -65,7 +65,7 @@ declares the exact Agent version and endpoint; it contains no endpoint secret. 2. Request a one-time challenge. ```powershell - $challenge = Invoke-RestMethod -Method Post -Uri "$gateway/v4/providers/$providerId/endpoint-bindings/$($binding.bindingId)/challenges" -Headers $headers + $challenge = Invoke-RestMethod -Method Post -Uri "$gateway/v1/providers/$providerId/endpoint-bindings/$($binding.bindingId)/challenges" -Headers $headers ``` This authenticated issuance response is the only public response allowed @@ -77,7 +77,7 @@ declares the exact Agent version and endpoint; it contains no endpoint secret. 3. Complete the challenge once. ```powershell - $binding = Invoke-RestMethod -Method Post -Uri "$gateway/v4/providers/$providerId/endpoint-bindings/$($binding.bindingId)/challenges/$($challenge.challengeId)/complete" -Headers $headers + $binding = Invoke-RestMethod -Method Post -Uri "$gateway/v1/providers/$providerId/endpoint-bindings/$($binding.bindingId)/challenges/$($challenge.challengeId)/complete" -Headers $headers ``` Completion is successful only when `verificationStatus` is `verified` and @@ -87,15 +87,15 @@ declares the exact Agent version and endpoint; it contains no endpoint secret. 4. Create and publish the immutable Release. ```powershell - $release = Invoke-RestMethod -Method Post -Uri "$gateway/v4/providers/$providerId/agents/$agentId/releases" -Headers $headers -ContentType 'application/json' -Body (@{ + $release = Invoke-RestMethod -Method Post -Uri "$gateway/v1/providers/$providerId/agents/$agentId/releases" -Headers $headers -ContentType 'application/json' -Body (@{ version = $version endpointBindingId = $binding.bindingId } | ConvertTo-Json -Compress) if ($release.state -eq 'pending_verification') { - $release = Invoke-RestMethod -Method Post -Uri "$gateway/v4/releases/$($release.releaseId)/verify" -Headers $headers + $release = Invoke-RestMethod -Method Post -Uri "$gateway/v1/releases/$($release.releaseId)/verify" -Headers $headers } - $release = Invoke-RestMethod -Method Post -Uri "$gateway/v4/releases/$($release.releaseId)/publish" -Headers $headers + $release = Invoke-RestMethod -Method Post -Uri "$gateway/v1/releases/$($release.releaseId)/publish" -Headers $headers ``` Completion requires `state=published`, the expected Agent/Card version, @@ -103,7 +103,7 @@ declares the exact Agent version and endpoint; it contains no endpoint secret. digest, and `publishedAt`. 5. The Workspace owner installs the version through - `POST /v3/workspaces/{workspaceId}/installations` and verifies that + `POST /v1/workspaces/{workspaceId}/installations` and verifies that `installedReleaseId` equals the published Release ID and `status=enabled`. ## Inspect trust and Invocation provenance @@ -111,17 +111,17 @@ declares the exact Agent version and endpoint; it contains no endpoint secret. Use the owning public reads; do not join module tables manually. ```powershell -$binding = Invoke-RestMethod -Method Get -Uri "$gateway/v4/providers/$providerId/endpoint-bindings/$bindingId" -Headers $headers -$release = Invoke-RestMethod -Method Get -Uri "$gateway/v4/releases/$releaseId" -Headers $headers -$invocation = Invoke-RestMethod -Method Get -Uri "$gateway/v4/workspaces/$workspaceId/invocations/$invocationId" -Headers $headers -$trace = Invoke-RestMethod -Method Get -Uri "$gateway/v4/workspaces/$workspaceId/traces/$traceId" -Headers $headers +$binding = Invoke-RestMethod -Method Get -Uri "$gateway/v1/providers/$providerId/endpoint-bindings/$bindingId" -Headers $headers +$release = Invoke-RestMethod -Method Get -Uri "$gateway/v1/releases/$releaseId" -Headers $headers +$invocation = Invoke-RestMethod -Method Get -Uri "$gateway/v1/workspaces/$workspaceId/invocations/$invocationId" -Headers $headers +$trace = Invoke-RestMethod -Method Get -Uri "$gateway/v1/workspaces/$workspaceId/traces/$traceId" -Headers $headers ``` For every accepted trusted Invocation, verify this chain: ```text Invocation/Event agentReleaseId + agentCardDigest - -> GET /v4/releases/{agentReleaseId} + -> GET /v1/releases/{agentReleaseId} -> same Agent ID + Card version + Card digest -> published state + Endpoint Binding + http_well_known evidence metadata ``` @@ -136,8 +136,8 @@ Suspension blocks new managed invocations but keeps the historical Release queryable. Revocation is terminal. ```powershell -$suspended = Invoke-RestMethod -Method Post -Uri "$gateway/v4/releases/$releaseId/suspend" -Headers $headers -$revoked = Invoke-RestMethod -Method Post -Uri "$gateway/v4/releases/$releaseId/revoke" -Headers $headers +$suspended = Invoke-RestMethod -Method Post -Uri "$gateway/v1/releases/$releaseId/suspend" -Headers $headers +$revoked = Invoke-RestMethod -Method Post -Uri "$gateway/v1/releases/$releaseId/revoke" -Headers $headers ``` Confirm `state=suspended` and `suspendedAt`, or `state=revoked` and diff --git a/tests/integration/catalog/catalog_test.go b/tests/integration/catalog/catalog_test.go index 33b6b2f3..08362f3a 100644 --- a/tests/integration/catalog/catalog_test.go +++ b/tests/integration/catalog/catalog_test.go @@ -80,13 +80,13 @@ func TestCatalogPostgreSQLAndHTTPAcceptance(t *testing.T) { runtimeB := readFixture(t, root, "runtime-b-card.json") t.Run("fixed authentication and registration semantics", func(t *testing.T) { - missing := request(t, http.MethodGet, server.baseURL+"/v3/agents", "", nil) + missing := request(t, http.MethodGet, server.baseURL+"/v1/agents", "", nil) assertPlatformError(t, missing, http.StatusUnauthorized, contracts.ErrorCodeUnauthenticated) if bytes.Contains(missing.body, []byte(ownerAToken)) || bytes.Contains(missing.body, []byte(digest(ownerAToken))) { t.Fatal("authentication material appeared in public error") } - draft := request(t, http.MethodPost, server.baseURL+"/v3/agents", ownerAToken, registrationEnvelope(t, runtimeA)) + draft := request(t, http.MethodPost, server.baseURL+"/v1/agents", ownerAToken, registrationEnvelope(t, runtimeA)) draftEntry := decodeEntry(t, draft) if draft.status != http.StatusCreated || draftEntry.PublicationStatus != "draft" { t.Fatalf("Runtime A registration = %d %s", draft.status, draft.body) @@ -98,7 +98,7 @@ func TestCatalogPostgreSQLAndHTTPAcceptance(t *testing.T) { if err := pool.QueryRow(ctx, `SELECT registered_at FROM catalog.agent_versions WHERE agent_id = 'runtime-a' AND version = '1.0.0'`).Scan(&storedRegisteredAt); err != nil || !draftEntry.RegisteredAt.Equal(storedRegisteredAt) { t.Fatalf("registration response time = %s, stored = %s, err = %v", draftEntry.RegisteredAt, storedRegisteredAt, err) } - assertPlatformError(t, request(t, http.MethodPost, server.baseURL+"/v3/agents", ownerAToken, registrationEnvelope(t, runtimeA)), http.StatusConflict, contracts.ErrorCodeConflict) + assertPlatformError(t, request(t, http.MethodPost, server.baseURL+"/v1/agents", ownerAToken, registrationEnvelope(t, runtimeA)), http.StatusConflict, contracts.ErrorCodeConflict) var originalCard, originalOwner string if err := pool.QueryRow(ctx, ` @@ -113,7 +113,7 @@ WHERE v.agent_id = 'runtime-a' AND v.version = '1.0.0'`).Scan(&originalCard, &or crossOwnerExact.Description = "This structurally valid Card must not replace the immutable version." crossOwnerExact.Owner.ID = "catalog-owner-b" crossOwnerExact.Owner.DisplayName = "Catalog Owner B" - crossOwnerConflict := request(t, http.MethodPost, server.baseURL+"/v3/agents", ownerBToken, registrationEnvelope(t, mustJSON(t, crossOwnerExact))) + crossOwnerConflict := request(t, http.MethodPost, server.baseURL+"/v1/agents", ownerBToken, registrationEnvelope(t, mustJSON(t, crossOwnerExact))) assertPlatformError(t, crossOwnerConflict, http.StatusConflict, contracts.ErrorCodeConflict) for _, forbiddenDetail := range []string{originalOwner, crossOwnerExact.Name, "draft"} { if bytes.Contains(crossOwnerConflict.body, []byte(forbiddenDetail)) { @@ -139,7 +139,7 @@ WHERE v.agent_id = 'runtime-a' AND v.version = '1.0.0'`).Scan(&retainedCard, &re boundaryCard.Skills[0].ID = "number.boundary" boundaryCard.Skills[0].Name = "Number boundary" boundaryCard.Limits.MaxInputBytes = json.Number("1e1000001") - boundary := request(t, http.MethodPost, server.baseURL+"/v3/agents", ownerAToken, registrationEnvelope(t, mustJSON(t, boundaryCard))) + boundary := request(t, http.MethodPost, server.baseURL+"/v1/agents", ownerAToken, registrationEnvelope(t, mustJSON(t, boundaryCard))) boundaryEntry := decodeEntry(t, boundary) if boundary.status != http.StatusCreated { t.Fatalf("unbounded number registration = %d %s", boundary.status, boundary.body) @@ -166,25 +166,25 @@ WHERE agent_id = 'unbounded-number-agent' AND version = '1.0.0'`).Scan( if storedName != boundaryCard.Name || storedDescription != boundaryCard.Description { t.Fatalf("derived text = %q/%q", storedName, storedDescription) } - if result := request(t, http.MethodPost, server.baseURL+"/v3/agents/unbounded-number-agent/versions/1.0.0/publish", ownerAToken, nil); result.status != http.StatusOK { + if result := request(t, http.MethodPost, server.baseURL+"/v1/agents/unbounded-number-agent/versions/1.0.0/publish", ownerAToken, nil); result.status != http.StatusOK { t.Fatalf("publish unbounded number Card = %d %s", result.status, result.body) } - boundaryDiscovery := decodeSearch(t, request(t, http.MethodGet, server.baseURL+"/v3/agents?query=PostgreSQL&capability=number.boundary", userToken, nil)) + boundaryDiscovery := decodeSearch(t, request(t, http.MethodGet, server.baseURL+"/v1/agents?query=PostgreSQL&capability=number.boundary", userToken, nil)) if len(boundaryDiscovery.Items) != 1 || boundaryDiscovery.Items[0].Card.Limits.MaxInputBytes.String() != "1e1000001" { t.Fatalf("unbounded number Discovery = %#v", boundaryDiscovery) } invalid := append([]byte(nil), runtimeA...) invalid = bytes.Replace(invalid, []byte(`"schemaVersion": "0.2"`), []byte(`"schemaVersion": "0.1"`), 1) - assertPlatformError(t, request(t, http.MethodPost, server.baseURL+"/v3/agents", ownerAToken, registrationEnvelope(t, invalid)), http.StatusBadRequest, contracts.ErrorCodeValidationError) + assertPlatformError(t, request(t, http.MethodPost, server.baseURL+"/v1/agents", ownerAToken, registrationEnvelope(t, invalid)), http.StatusBadRequest, contracts.ErrorCodeValidationError) crossOwner := decodeCard(t, runtimeA) crossOwner.Version = "2.0.0" crossOwner.Owner.ID = "catalog-owner-b" - assertPlatformError(t, request(t, http.MethodPost, server.baseURL+"/v3/agents", ownerBToken, registrationEnvelope(t, mustJSON(t, crossOwner))), http.StatusForbidden, contracts.ErrorCodeForbidden) + assertPlatformError(t, request(t, http.MethodPost, server.baseURL+"/v1/agents", ownerBToken, registrationEnvelope(t, mustJSON(t, crossOwner))), http.StatusForbidden, contracts.ErrorCodeForbidden) - assertPlatformError(t, request(t, http.MethodGet, server.baseURL+"/v3/agents/runtime-a/versions/1.0.0", userToken, nil), http.StatusForbidden, contracts.ErrorCodeForbidden) - if result := request(t, http.MethodGet, server.baseURL+"/v3/agents/runtime-a/versions/1.0.0", ownerAToken, nil); result.status != http.StatusOK { + assertPlatformError(t, request(t, http.MethodGet, server.baseURL+"/v1/agents/runtime-a/versions/1.0.0", userToken, nil), http.StatusForbidden, contracts.ErrorCodeForbidden) + if result := request(t, http.MethodGet, server.baseURL+"/v1/agents/runtime-a/versions/1.0.0", ownerAToken, nil); result.status != http.StatusOK { t.Fatalf("owner draft read = %d %s", result.status, result.body) } var versionRows int @@ -194,7 +194,7 @@ WHERE agent_id = 'unbounded-number-agent' AND version = '1.0.0'`).Scan( }) t.Run("publication discovery disablement and cross-runtime metadata", func(t *testing.T) { - publishedA := request(t, http.MethodPost, server.baseURL+"/v3/agents/runtime-a/versions/1.0.0/publish", ownerAToken, nil) + publishedA := request(t, http.MethodPost, server.baseURL+"/v1/agents/runtime-a/versions/1.0.0/publish", ownerAToken, nil) publishedEntry := decodeEntry(t, publishedA) if publishedA.status != http.StatusOK || publishedEntry.PublicationStatus != "published" { t.Fatalf("publish Runtime A = %d %s", publishedA.status, publishedA.body) @@ -203,51 +203,51 @@ WHERE agent_id = 'unbounded-number-agent' AND version = '1.0.0'`).Scan( if err := pool.QueryRow(ctx, `SELECT published_at FROM catalog.agent_versions WHERE agent_id = 'runtime-a' AND version = '1.0.0'`).Scan(&storedPublishedAt); err != nil || publishedEntry.PublishedAt == nil || !publishedEntry.PublishedAt.Equal(storedPublishedAt) { t.Fatalf("publication response time = %v, stored = %s, err = %v", publishedEntry.PublishedAt, storedPublishedAt, err) } - assertPlatformError(t, request(t, http.MethodPost, server.baseURL+"/v3/agents/runtime-a/versions/1.0.0/publish", ownerAToken, nil), http.StatusConflict, contracts.ErrorCodeConflict) - if result := request(t, http.MethodGet, server.baseURL+"/v3/agents/runtime-a/versions/1.0.0", userToken, nil); result.status != http.StatusOK { + assertPlatformError(t, request(t, http.MethodPost, server.baseURL+"/v1/agents/runtime-a/versions/1.0.0/publish", ownerAToken, nil), http.StatusConflict, contracts.ErrorCodeConflict) + if result := request(t, http.MethodGet, server.baseURL+"/v1/agents/runtime-a/versions/1.0.0", userToken, nil); result.status != http.StatusOK { t.Fatalf("published public read = %d %s", result.status, result.body) } - if result := request(t, http.MethodPost, server.baseURL+"/v3/agents", ownerBToken, registrationEnvelope(t, runtimeB)); result.status != http.StatusCreated { + if result := request(t, http.MethodPost, server.baseURL+"/v1/agents", ownerBToken, registrationEnvelope(t, runtimeB)); result.status != http.StatusCreated { t.Fatalf("register Runtime B = %d %s", result.status, result.body) } - if result := request(t, http.MethodPost, server.baseURL+"/v3/agents/runtime-b/versions/1.0.0/publish", ownerBToken, nil); result.status != http.StatusOK { + if result := request(t, http.MethodPost, server.baseURL+"/v1/agents/runtime-b/versions/1.0.0/publish", ownerBToken, nil); result.status != http.StatusOK { t.Fatalf("publish Runtime B = %d %s", result.status, result.body) } - search := decodeSearch(t, request(t, http.MethodGet, server.baseURL+"/v3/agents?capability=runtime.echo", userToken, nil)) + search := decodeSearch(t, request(t, http.MethodGet, server.baseURL+"/v1/agents?capability=runtime.echo", userToken, nil)) if len(search.Items) != 2 { t.Fatalf("cross-runtime discovery count = %d, want 2", len(search.Items)) } - filtered := decodeSearch(t, request(t, http.MethodGet, server.baseURL+"/v3/agents?query=translation&capability=runtime.translate&ownerId=catalog-owner-b", userToken, nil)) + filtered := decodeSearch(t, request(t, http.MethodGet, server.baseURL+"/v1/agents?query=translation&capability=runtime.translate&ownerId=catalog-owner-b", userToken, nil)) if len(filtered.Items) != 1 || filtered.Items[0].Card.AgentID != "runtime-b" { t.Fatalf("combined discovery = %#v", filtered) } for _, literal := range []string{"%", "_"} { - literalResult := decodeSearch(t, request(t, http.MethodGet, server.baseURL+"/v3/agents?query="+url.QueryEscape(literal), userToken, nil)) + literalResult := decodeSearch(t, request(t, http.MethodGet, server.baseURL+"/v1/agents?query="+url.QueryEscape(literal), userToken, nil)) if len(literalResult.Items) != 0 { t.Fatalf("literal wildcard query %q matched %d rows", literal, len(literalResult.Items)) } } - assertPlatformError(t, request(t, http.MethodPost, server.baseURL+"/v3/agents/runtime-a/versions/1.0.0/disable", userToken, nil), http.StatusForbidden, contracts.ErrorCodeForbidden) - firstDisable := request(t, http.MethodPost, server.baseURL+"/v3/agents/runtime-a/versions/1.0.0/disable", ownerAToken, nil) - secondDisable := request(t, http.MethodPost, server.baseURL+"/v3/agents/runtime-a/versions/1.0.0/disable", ownerAToken, nil) + assertPlatformError(t, request(t, http.MethodPost, server.baseURL+"/v1/agents/runtime-a/versions/1.0.0/disable", userToken, nil), http.StatusForbidden, contracts.ErrorCodeForbidden) + firstDisable := request(t, http.MethodPost, server.baseURL+"/v1/agents/runtime-a/versions/1.0.0/disable", ownerAToken, nil) + secondDisable := request(t, http.MethodPost, server.baseURL+"/v1/agents/runtime-a/versions/1.0.0/disable", ownerAToken, nil) firstEntry, secondEntry := decodeEntry(t, firstDisable), decodeEntry(t, secondDisable) if firstDisable.status != http.StatusOK || secondDisable.status != http.StatusOK || firstEntry.PublicationStatus != "disabled" || firstEntry.PublishedAt == nil || secondEntry.PublishedAt == nil || !firstEntry.PublishedAt.Equal(*secondEntry.PublishedAt) { t.Fatalf("idempotent disable = %#v / %#v", firstEntry, secondEntry) } - disabledOwnerRead := request(t, http.MethodGet, server.baseURL+"/v3/agents/runtime-a/versions/1.0.0", ownerAToken, nil) + disabledOwnerRead := request(t, http.MethodGet, server.baseURL+"/v1/agents/runtime-a/versions/1.0.0", ownerAToken, nil) disabledOwnerEntry := decodeEntry(t, disabledOwnerRead) if disabledOwnerRead.status != http.StatusOK || disabledOwnerEntry.PublicationStatus != "disabled" || disabledOwnerEntry.PublishedAt == nil || !disabledOwnerEntry.PublishedAt.Equal(*firstEntry.PublishedAt) { t.Fatalf("disabled owner read = %d %s %#v", disabledOwnerRead.status, disabledOwnerRead.body, disabledOwnerEntry) } - assertPlatformError(t, request(t, http.MethodGet, server.baseURL+"/v3/agents/runtime-a/versions/1.0.0", userToken, nil), http.StatusForbidden, contracts.ErrorCodeForbidden) - assertPlatformError(t, request(t, http.MethodPost, server.baseURL+"/v3/agents/runtime-a/versions/1.0.0/publish", ownerAToken, nil), http.StatusConflict, contracts.ErrorCodeConflict) - disabledAfterRepublishAttempt := decodeEntry(t, request(t, http.MethodGet, server.baseURL+"/v3/agents/runtime-a/versions/1.0.0", ownerAToken, nil)) + assertPlatformError(t, request(t, http.MethodGet, server.baseURL+"/v1/agents/runtime-a/versions/1.0.0", userToken, nil), http.StatusForbidden, contracts.ErrorCodeForbidden) + assertPlatformError(t, request(t, http.MethodPost, server.baseURL+"/v1/agents/runtime-a/versions/1.0.0/publish", ownerAToken, nil), http.StatusConflict, contracts.ErrorCodeConflict) + disabledAfterRepublishAttempt := decodeEntry(t, request(t, http.MethodGet, server.baseURL+"/v1/agents/runtime-a/versions/1.0.0", ownerAToken, nil)) if disabledAfterRepublishAttempt.PublicationStatus != "disabled" || disabledAfterRepublishAttempt.PublishedAt == nil || !disabledAfterRepublishAttempt.PublishedAt.Equal(*firstEntry.PublishedAt) { t.Fatalf("disabled state changed after republish attempt = %#v", disabledAfterRepublishAttempt) } - afterDisable := decodeSearch(t, request(t, http.MethodGet, server.baseURL+"/v3/agents?capability=runtime.echo", userToken, nil)) + afterDisable := decodeSearch(t, request(t, http.MethodGet, server.baseURL+"/v1/agents?capability=runtime.echo", userToken, nil)) if len(afterDisable.Items) != 1 || afterDisable.Items[0].Card.AgentID != "runtime-b" { t.Fatalf("discovery after disable = %#v", afterDisable) } @@ -256,7 +256,7 @@ WHERE agent_id = 'unbounded-number-agent' AND version = '1.0.0'`).Scan( t.Run("concurrent lifecycle has one legal final state", func(t *testing.T) { card := decodeCard(t, runtimeA) card.AgentID = "race-agent" - if result := request(t, http.MethodPost, server.baseURL+"/v3/agents", ownerAToken, registrationEnvelope(t, mustJSON(t, card))); result.status != http.StatusCreated { + if result := request(t, http.MethodPost, server.baseURL+"/v1/agents", ownerAToken, registrationEnvelope(t, mustJSON(t, card))); result.status != http.StatusCreated { t.Fatalf("register race Card = %d %s", result.status, result.body) } var wait sync.WaitGroup @@ -266,12 +266,12 @@ WHERE agent_id = 'unbounded-number-agent' AND version = '1.0.0'`).Scan( go func() { defer wait.Done() <-start - statuses <- request(t, http.MethodPost, server.baseURL+"/v3/agents/race-agent/versions/1.0.0/publish", ownerAToken, nil).status + statuses <- request(t, http.MethodPost, server.baseURL+"/v1/agents/race-agent/versions/1.0.0/publish", ownerAToken, nil).status }() go func() { defer wait.Done() <-start - statuses <- request(t, http.MethodPost, server.baseURL+"/v3/agents/race-agent/versions/1.0.0/disable", ownerAToken, nil).status + statuses <- request(t, http.MethodPost, server.baseURL+"/v1/agents/race-agent/versions/1.0.0/disable", ownerAToken, nil).status }() close(start) wait.Wait() @@ -281,7 +281,7 @@ WHERE agent_id = 'unbounded-number-agent' AND version = '1.0.0'`).Scan( t.Fatalf("race status = %d", status) } } - final := decodeEntry(t, request(t, http.MethodGet, server.baseURL+"/v3/agents/race-agent/versions/1.0.0", ownerAToken, nil)) + final := decodeEntry(t, request(t, http.MethodGet, server.baseURL+"/v1/agents/race-agent/versions/1.0.0", ownerAToken, nil)) if final.PublicationStatus != "disabled" { t.Fatalf("race final state = %q", final.PublicationStatus) } @@ -300,7 +300,7 @@ WHERE agent_id = 'unbounded-number-agent' AND version = '1.0.0'`).Scan( go func() { defer wait.Done() <-start - statuses <- request(t, http.MethodPost, server.baseURL+"/v3/agents", ownerAToken, body).status + statuses <- request(t, http.MethodPost, server.baseURL+"/v1/agents", ownerAToken, body).status }() } close(start) @@ -327,14 +327,14 @@ WHERE agent_id = 'unbounded-number-agent' AND version = '1.0.0'`).Scan( previousServer.stop(t) assertLogsAreSecretSafe(t, previousServer.logs.String()) server = startServer(t, root, databaseURL, binary) - if result := request(t, http.MethodGet, server.baseURL+"/v3/agents/runtime-b/versions/1.0.0", userToken, nil); result.status != http.StatusOK { + if result := request(t, http.MethodGet, server.baseURL+"/v1/agents/runtime-b/versions/1.0.0", userToken, nil); result.status != http.StatusOK { t.Fatalf("durable read after restart = %d %s", result.status, result.body) } - boundaryRead := decodeEntry(t, request(t, http.MethodGet, server.baseURL+"/v3/agents/unbounded-number-agent/versions/1.0.0", userToken, nil)) + boundaryRead := decodeEntry(t, request(t, http.MethodGet, server.baseURL+"/v1/agents/unbounded-number-agent/versions/1.0.0", userToken, nil)) if got := boundaryRead.Card.Limits.MaxInputBytes.String(); got != "1e1000001" { t.Fatalf("unbounded number after restart = %s", got) } - boundaryDiscovery := decodeSearch(t, request(t, http.MethodGet, server.baseURL+"/v3/agents?capability=number.boundary", userToken, nil)) + boundaryDiscovery := decodeSearch(t, request(t, http.MethodGet, server.baseURL+"/v1/agents?capability=number.boundary", userToken, nil)) if len(boundaryDiscovery.Items) != 1 || boundaryDiscovery.Items[0].Card.AgentID != "unbounded-number-agent" || boundaryDiscovery.Items[0].Card.Limits.MaxInputBytes.String() != "1e1000001" { t.Fatalf("unbounded Discovery after restart = %#v", boundaryDiscovery) } @@ -342,7 +342,7 @@ WHERE agent_id = 'unbounded-number-agent' AND version = '1.0.0'`).Scan( if _, err := pool.Exec(ctx, `ALTER SCHEMA catalog RENAME TO catalog_unavailable`); err != nil { t.Fatal(err) } - failure := request(t, http.MethodGet, server.baseURL+"/v3/agents?capability=runtime.echo", userToken, nil) + failure := request(t, http.MethodGet, server.baseURL+"/v1/agents?capability=runtime.echo", userToken, nil) assertPlatformError(t, failure, http.StatusServiceUnavailable, contracts.ErrorCodeDependency) if _, err := pool.Exec(ctx, `ALTER SCHEMA catalog_unavailable RENAME TO catalog`); err != nil { t.Fatal(err) @@ -377,18 +377,18 @@ WHERE agent_id = 'unbounded-number-agent' AND version = '1.0.0'`).Scan( clockTransaction, firstSequence := beginDelayedPublication(t, pool, "clock-race-a") secondPublication := make(chan httpResult, 1) go func() { - secondPublication <- request(t, http.MethodPost, server.baseURL+"/v3/agents/clock-race-b/versions/1.0.0/publish", ownerAToken, nil) + secondPublication <- request(t, http.MethodPost, server.baseURL+"/v1/agents/clock-race-b/versions/1.0.0/publish", ownerAToken, nil) }() select { case result := <-secondPublication: t.Fatalf("competing publication bypassed transactional clock with status %d", result.status) case <-time.After(100 * time.Millisecond): } - first := decodeSearch(t, request(t, http.MethodGet, server.baseURL+"/v3/agents?capability=scale.test&limit=100", userToken, nil)) + first := decodeSearch(t, request(t, http.MethodGet, server.baseURL+"/v1/agents?capability=scale.test&limit=100", userToken, nil)) if len(first.Items) != 100 || first.NextCursor == nil { t.Fatalf("first scale page = %d items, cursor %v", len(first.Items), first.NextCursor) } - mismatched := request(t, http.MethodGet, server.baseURL+"/v3/agents?capability=other.test&limit=100&cursor="+*first.NextCursor, userToken, nil) + mismatched := request(t, http.MethodGet, server.baseURL+"/v1/agents?capability=other.test&limit=100&cursor="+*first.NextCursor, userToken, nil) assertPlatformError(t, mismatched, http.StatusBadRequest, contracts.ErrorCodeValidationError) firstVersions := make([]string, len(first.Items)) for index, item := range first.Items { @@ -421,7 +421,7 @@ WHERE agent_id = 'unbounded-number-agent' AND version = '1.0.0'`).Scan( } cursor := first.NextCursor for cursor != nil { - page := decodeSearch(t, request(t, http.MethodGet, server.baseURL+"/v3/agents?capability=scale.test&limit=100&cursor="+*cursor, userToken, nil)) + page := decodeSearch(t, request(t, http.MethodGet, server.baseURL+"/v1/agents?capability=scale.test&limit=100&cursor="+*cursor, userToken, nil)) for _, item := range page.Items { key := item.Card.AgentID + "@" + item.Card.Version if _, exists := seen[key]; exists { @@ -449,7 +449,7 @@ WHERE agent_id = 'unbounded-number-agent' AND version = '1.0.0'`).Scan( seedPublishedVersions(t, pool, 1000, 9000) started := time.Now() - page := request(t, http.MethodGet, server.baseURL+"/v3/agents?capability=scale.test&limit=100", userToken, nil) + page := request(t, http.MethodGet, server.baseURL+"/v1/agents?capability=scale.test&limit=100", userToken, nil) elapsed := time.Since(started) if page.status != http.StatusOK { t.Fatalf("10,000-version first page = %d %s", page.status, page.body) @@ -857,7 +857,7 @@ func startServer(t *testing.T, root, databaseURL, binary string) *testServer { "NEKIRO_DEV_AUTH_PRINCIPALS_JSON": string(principalsJSON), "NEKIRO_INTERNAL_AUTH_MODE": "development-static", "NEKIRO_INTERNAL_DEV_AUTH_PRINCIPALS_JSON": string(internalPrincipalsJSON), - "NEKIRO_ROUTER_INTERNAL_URL": "http://router-integration:8081/internal/v4/invocations", + "NEKIRO_ROUTER_INTERNAL_URL": "http://router-integration:8081/internal/v1/invocations", "NEKIRO_ROUTER_INTERNAL_BEARER_TOKEN": internalToken, "NEKIRO_CONTROL_PLANE_INTERNAL_REQUEST_MAX_BYTES": "1048576", "NEKIRO_GATEWAY_INVOCATION_REQUEST_MAX_BYTES": "1048576", @@ -1143,7 +1143,7 @@ func registerClockRaceCards(t *testing.T, server *testServer) { for _, agentID := range []string{"clock-race-a", "clock-race-b"} { card := scaleCard("1.0.0") card.AgentID = agentID - result := request(t, http.MethodPost, server.baseURL+"/v3/agents", ownerAToken, registrationEnvelope(t, mustJSON(t, card))) + result := request(t, http.MethodPost, server.baseURL+"/v1/agents", ownerAToken, registrationEnvelope(t, mustJSON(t, card))) if result.status != http.StatusCreated { t.Fatalf("register %s = %d %s", agentID, result.status, result.body) }