Skip to content

Commit 3abfd96

Browse files
committed
Declare required native installation credentials
1 parent 7d8e7a2 commit 3abfd96

4 files changed

Lines changed: 83 additions & 1 deletion

File tree

‎contracts/agents-api/runtime.openapi.yaml‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ definitions:
44
properties:
55
executor_token:
66
type: string
7+
required:
8+
- executor_token
79
type: object
810
deployment.Enrollment:
911
properties:
@@ -483,6 +485,12 @@ paths:
483485
/api/v1/agent-daemon/installation:
484486
post:
485487
description: Accepts a short-lived Environment installation Bearer authorization, not a Project or Core key. Returns frozen connection constraints; it does not claim or rotate credentials.
488+
parameters:
489+
- description: Bearer installation grant
490+
in: header
491+
name: Authorization
492+
required: true
493+
type: string
486494
produces:
487495
- application/json
488496
responses:
@@ -511,6 +519,11 @@ paths:
511519
- application/json
512520
description: A valid installation Bearer authorization can claim one connect-only key. The client persists its generated secret before submitting it. Retries must present that same secret; a different, rotated or revoked credential is never replaced.
513521
parameters:
522+
- description: Bearer installation grant
523+
in: header
524+
name: Authorization
525+
required: true
526+
type: string
514527
- description: Locally persisted executor secret
515528
in: body
516529
name: body

‎services/core/internal/api/contract_routes_test.go‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,39 @@ func TestContractsPublishExactlyTheRegisteredCoreAndMachineRoutes(t *testing.T)
129129
}
130130
}
131131
}
132+
133+
func TestInstallationContractRequiresGrantAndExecutorToken(t *testing.T) {
134+
raw, err := os.ReadFile("../../../../contracts/agents-api/runtime.openapi.yaml")
135+
if err != nil {
136+
t.Fatal(err)
137+
}
138+
var contract struct {
139+
Paths map[string]map[string]struct {
140+
Parameters []struct {
141+
Name string `yaml:"name"`
142+
In string `yaml:"in"`
143+
Required bool `yaml:"required"`
144+
}
145+
} `yaml:"paths"`
146+
Definitions map[string]struct {
147+
Required []string `yaml:"required"`
148+
} `yaml:"definitions"`
149+
}
150+
if err := yaml.Unmarshal(raw, &contract); err != nil {
151+
t.Fatal(err)
152+
}
153+
for _, path := range []string{"/api/v1/agent-daemon/installation", "/api/v1/agent-daemon/installation/claim"} {
154+
present := false
155+
for _, parameter := range contract.Paths[path]["post"].Parameters {
156+
if parameter.In == "header" && parameter.Name == "Authorization" && parameter.Required {
157+
present = true
158+
}
159+
}
160+
if !present {
161+
t.Errorf("%s does not require its installation grant", path)
162+
}
163+
}
164+
if !slices.Contains(contract.Definitions["api.NativeInstallationClaim"].Required, "executor_token") {
165+
t.Fatal("claim schema does not require the executor secret")
166+
}
167+
}

‎services/core/internal/api/environment_installation.go‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ func (h *Handler) installationAuthorization(w http.ResponseWriter, r *http.Reque
8484
// @Description Accepts a short-lived Environment installation Bearer authorization, not a Project or Core key. Returns frozen connection constraints; it does not claim or rotate credentials.
8585
// @Tags Native Installation
8686
// @Produce json
87+
// @Param Authorization header string true "Bearer installation grant"
8788
// @Success 200 {object} v1.NativeInstallationContext
8889
// @Failure 401,404,503 {object} v1.ErrorResponse
8990
// @Router /api/v1/agent-daemon/installation [post]
@@ -111,13 +112,14 @@ func (h *Handler) prepareNativeInstallation(w http.ResponseWriter, r *http.Reque
111112
}
112113

113114
type NativeInstallationClaim struct {
114-
ExecutorToken string `json:"executor_token"`
115+
ExecutorToken string `json:"executor_token" binding:"required"`
115116
}
116117

117118
// @Summary Claim an Environment's installation credential
118119
// @Description A valid installation Bearer authorization can claim one connect-only key. The client persists its generated secret before submitting it. Retries must present that same secret; a different, rotated or revoked credential is never replaced.
119120
// @Tags Native Installation
120121
// @Accept json
122+
// @Param Authorization header string true "Bearer installation grant"
121123
// @Param body body api.NativeInstallationClaim true "Locally persisted executor secret"
122124
// @Success 204
123125
// @Failure 400,401,409,503 {object} v1.ErrorResponse

‎services/core/tests/integration/environment_installation_test.go‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@ import (
44
"encoding/base64"
55
"encoding/json"
66
"errors"
7+
"net/http"
8+
"net/http/httptest"
79
"strings"
810
"sync"
911
"testing"
1012
"time"
1113

14+
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api"
15+
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/nativeinstaller"
1216
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions"
1317
"github.com/google/uuid"
1418
)
@@ -32,6 +36,28 @@ func TestEnvironmentInstallationClaimLifetimeAndRetries(t *testing.T) {
3236
if err != nil || expires <= time.Now().Unix() || expires > time.Now().Add(31*time.Minute).Unix() {
3337
t.Fatal("authorization", err)
3438
}
39+
handler, err := publicHandler(t, s, fixtureKeyResolver{}, "codex", func(d *api.Dependencies) {
40+
d.Execution.NativeInstaller = &api.NativeInstaller{Version: "build", Base: "https://core.example/api/v1/agent-daemon/install/", Catalog: &nativeinstaller.Catalog{Version: "build"}}
41+
})
42+
if err != nil {
43+
t.Fatal(err)
44+
}
45+
claimHTTP := func(path, grant, body string, status int) {
46+
t.Helper()
47+
request := httptest.NewRequest(http.MethodPost, "/api/v1/agent-daemon/"+path, strings.NewReader(body))
48+
if grant != "" {
49+
request.Header.Set("Authorization", "Bearer "+grant)
50+
}
51+
request.Header.Set("Content-Type", "application/json")
52+
response := httptest.NewRecorder()
53+
handler.ServeHTTP(response, request)
54+
if response.Code != status {
55+
t.Fatalf("%s returned %d, want %d: %s", path, response.Code, status, response.Body)
56+
}
57+
}
58+
claimHTTP("installation", "", "", http.StatusUnauthorized)
59+
claimHTTP("installation/claim", "", "{}", http.StatusUnauthorized)
60+
claimHTTP("installation/claim", token, "{}", http.StatusBadRequest)
3561
for _, pair := range [][2]string{{token + "x", "build"}, {token, "other-build"}, {"", "build"}} {
3662
if _, err := installations.ValidateEnvironmentInstallation(ctx, pair[0], pair[1]); !errors.Is(err, sessions.ErrInstallationAuthorization) {
3763
t.Fatal("accepted invalid authorization", err)
@@ -78,6 +104,11 @@ func TestEnvironmentInstallationClaimLifetimeAndRetries(t *testing.T) {
78104
if err := installations.ClaimEnvironmentInstallation(ctx, token, "build", secrets[winner]); err != nil {
79105
t.Fatal("lost-response retry", err)
80106
}
107+
claimBody, err := json.Marshal(api.NativeInstallationClaim{ExecutorToken: secrets[winner]})
108+
if err != nil {
109+
t.Fatal(err)
110+
}
111+
claimHTTP("installation/claim", token, string(claimBody), http.StatusNoContent)
81112
if _, err := sessionAdapter(s).AuthenticateEnvironmentExecutor(ctx, environment.ID, executorDigest(secrets[winner])); err != nil {
82113
t.Fatal(err)
83114
}

0 commit comments

Comments
 (0)