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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/daemon/internal/cli/connect_environment_binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ func bindEnvironmentRuntime(remote string, bound machineapi.Enrollment, workspac
if err := localworkspace.Bind(bound.EnvironmentID, workspace, capabilityDirectory); err != nil {
return errors.New("connect: local Runtime layout unavailable")
}
bound.SessionID = "" // The Runtime binds to its Environment; the receipt's Session is not part of it.
return saveEnvironmentBinding(root, environmentBinding{RemoteURL: remote, Enrollment: bound, LocalWorkspace: workspace, CapabilityDirectory: capabilityDirectory})
}

Expand Down
9 changes: 3 additions & 6 deletions apps/daemon/internal/cli/connect_environment_binding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func environmentRuntimeFixture(t *testing.T) (root, workspace, credential string
}
t.Setenv("OAC_RUNTIME_HOME", root)
t.Cleanup(localworkspace.Unbind)
return root, workspace, credential, machineapi.Enrollment{DeviceID: uuid.NewString(), SessionID: uuid.NewString(), EnvironmentID: uuid.NewString(), WorkspaceDirectory: "/workspace"}
return root, workspace, credential, machineapi.Enrollment{DeviceID: uuid.NewString(), EnvironmentID: uuid.NewString(), WorkspaceDirectory: "/workspace"}
}

func TestBindEnvironmentRuntimeUsesCanonicalOperatorWorkspace(t *testing.T) {
Expand All @@ -44,14 +44,11 @@ func TestBindEnvironmentRuntimeUsesCanonicalOperatorWorkspace(t *testing.T) {
t.Fatal(err)
}
var receipt environmentBinding
want := bound
want.SessionID = ""
if decodeEnvironmentJSON(raw, &receipt) != nil || receipt.LocalWorkspace != workspace || receipt.Enrollment != want {
if decodeEnvironmentJSON(raw, &receipt) != nil || receipt.LocalWorkspace != workspace || receipt.Enrollment != bound {
t.Fatal("workspace identity changed", receipt)
}
bound.SessionID = uuid.NewString()
if err := bindEnvironmentRuntime(receipt.RemoteURL, bound, workspace, localworkspace.CapabilityDirectory); err != nil {
t.Fatal("same Environment refused for another Session", err)
t.Fatal("same Environment refused on rebind", err)
}
replacement := filepath.Join(filepath.Dir(workspace), "replacement")
if err := os.Mkdir(replacement, 0700); err != nil {
Expand Down
16 changes: 8 additions & 8 deletions apps/daemon/internal/cli/connect_environment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func TestEnvironmentConnectionURL(t *testing.T) {

func TestEnvironmentEnrollmentAndBootstrap(t *testing.T) {
environment := uuid.NewString()
want := machineapi.Enrollment{DeviceID: uuid.NewString(), SessionID: uuid.NewString(), EnvironmentID: environment, WorkspaceDirectory: "/workspace"}
want := machineapi.Enrollment{DeviceID: uuid.NewString(), EnvironmentID: environment, WorkspaceDirectory: "/workspace"}
var remote string
var enrolls, bootstraps int
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -156,7 +156,7 @@ func TestEnvironmentBindingPreservesIdentityAndHistory(t *testing.T) {
if err := os.Chmod(root, 0700); err != nil {
t.Fatal(err)
}
want := environmentBinding{"wss://core/api/v1/agent-daemon/ws", machineapi.Enrollment{DeviceID: uuid.NewString(), SessionID: uuid.NewString(), EnvironmentID: uuid.NewString(), WorkspaceDirectory: "/workspace"}, "/environment/workspace", "/environment/initialization/capabilities"}
want := environmentBinding{"wss://core/api/v1/agent-daemon/ws", machineapi.Enrollment{DeviceID: uuid.NewString(), EnvironmentID: uuid.NewString(), WorkspaceDirectory: "/workspace"}, "/environment/workspace", "/environment/initialization/capabilities"}
if err := saveEnvironmentBinding(root, want); err != nil {
t.Fatal(err)
}
Expand All @@ -170,7 +170,7 @@ func TestEnvironmentBindingPreservesIdentityAndHistory(t *testing.T) {
if err := saveEnvironmentBinding(root, want); err != nil {
t.Fatal(err)
}
for _, mutate := range []func(*environmentBinding){func(b *environmentBinding) { b.Enrollment.SessionID = uuid.NewString() }, func(b *environmentBinding) { b.Enrollment.EnvironmentID = uuid.NewString() }, func(b *environmentBinding) { b.Enrollment.DeviceID = uuid.NewString() }, func(b *environmentBinding) { b.RemoteURL = "wss://other/api/v1/agent-daemon/ws" }} {
for _, mutate := range []func(*environmentBinding){func(b *environmentBinding) { b.Enrollment.EnvironmentID = uuid.NewString() }, func(b *environmentBinding) { b.Enrollment.DeviceID = uuid.NewString() }, func(b *environmentBinding) { b.RemoteURL = "wss://other/api/v1/agent-daemon/ws" }} {
changed := want
mutate(&changed)
if err := saveEnvironmentBinding(root, changed); err == nil {
Expand All @@ -193,7 +193,7 @@ func TestEnvironmentBindingAllowsOperatorFilePermissions(t *testing.T) {
if err := os.Chmod(root, 0700); err != nil {
t.Fatal(err)
}
want := environmentBinding{"wss://core/api/v1/agent-daemon/ws", machineapi.Enrollment{DeviceID: uuid.NewString(), SessionID: uuid.NewString(), EnvironmentID: uuid.NewString(), WorkspaceDirectory: "/workspace"}, "/environment/workspace", "/environment/initialization/capabilities"}
want := environmentBinding{"wss://core/api/v1/agent-daemon/ws", machineapi.Enrollment{DeviceID: uuid.NewString(), EnvironmentID: uuid.NewString(), WorkspaceDirectory: "/workspace"}, "/environment/workspace", "/environment/initialization/capabilities"}
if err := os.WriteFile(filepath.Join(root, "installed-bundle"), []byte("bundle"), 0600); err != nil {
t.Fatal(err)
}
Expand All @@ -215,7 +215,7 @@ func TestEnvironmentTargetCheckedBeforeCredentialTransmission(t *testing.T) {
t.Fatal(err)
}
t.Setenv("OAC_RUNTIME_HOME", root)
want := environmentBinding{"wss://core/api/v1/agent-daemon/ws", machineapi.Enrollment{DeviceID: uuid.NewString(), SessionID: uuid.NewString(), EnvironmentID: uuid.NewString(), WorkspaceDirectory: "/workspace"}, "/environment/workspace", "/environment/initialization/capabilities"}
want := environmentBinding{"wss://core/api/v1/agent-daemon/ws", machineapi.Enrollment{DeviceID: uuid.NewString(), EnvironmentID: uuid.NewString(), WorkspaceDirectory: "/workspace"}, "/environment/workspace", "/environment/initialization/capabilities"}
if err := saveEnvironmentBinding(root, want); err != nil {
t.Fatal(err)
}
Expand All @@ -232,8 +232,8 @@ func TestEnvironmentTargetCheckedBeforeCredentialTransmission(t *testing.T) {

func TestEnvironmentEnrollmentRejectsWrongIdentityAndWorkspace(t *testing.T) {
environment := uuid.NewString()
good := machineapi.Enrollment{DeviceID: uuid.NewString(), SessionID: uuid.NewString(), EnvironmentID: environment, WorkspaceDirectory: "/workspace"}
for _, mutate := range []func(*machineapi.Enrollment){func(b *machineapi.Enrollment) { b.EnvironmentID = uuid.NewString() }, func(b *machineapi.Enrollment) { b.DeviceID = "" }, func(b *machineapi.Enrollment) { b.SessionID = "invalid" }, func(b *machineapi.Enrollment) { b.WorkspaceDirectory = "relative" }} {
good := machineapi.Enrollment{DeviceID: uuid.NewString(), EnvironmentID: environment, WorkspaceDirectory: "/workspace"}
for _, mutate := range []func(*machineapi.Enrollment){func(b *machineapi.Enrollment) { b.EnvironmentID = uuid.NewString() }, func(b *machineapi.Enrollment) { b.DeviceID = "" }, func(b *machineapi.Enrollment) { b.WorkspaceDirectory = "relative" }} {
bad := good
mutate(&bad)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(bad) }))
Expand All @@ -247,7 +247,7 @@ func TestEnvironmentEnrollmentRejectsWrongIdentityAndWorkspace(t *testing.T) {

func TestEnvironmentEnrollmentAcceptsPhysicalWorkspaceSelection(t *testing.T) {
environment := uuid.NewString()
want := machineapi.Enrollment{DeviceID: uuid.NewString(), SessionID: uuid.NewString(), EnvironmentID: environment, WorkspaceDirectory: "/srv/runtime/workspace"}
want := machineapi.Enrollment{DeviceID: uuid.NewString(), EnvironmentID: environment, WorkspaceDirectory: "/srv/runtime/workspace"}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(want) }))
defer server.Close()
actual, err := enrollEnvironment(t.Context(), environmentClient(), server.URL, environment, "secret")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ func TestNativeStartInterruptCancelsEnrollment(t *testing.T) {
}
_ = json.NewEncoder(w).Encode(machineapi.Enrollment{
DeviceID: "aaaaaaaa-1111-4111-8111-aaaaaaaaaaaa",
SessionID: "bbbbbbbb-1111-4111-8111-bbbbbbbbbbbb",
EnvironmentID: config.Environment,
WorkspaceDirectory: config.Workspace,
})
Expand Down
2 changes: 1 addition & 1 deletion contracts/agents-api/machine-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ The bootstrap and WebSocket routes share one error body, `{"error": code, "detai

### Enroll a self-hosted daemon

`POST /api/v1/agent-daemon/enroll` with the executor credential and exactly `{"environment_id": "…"}` (no query) binds one dedicated device to the Environment's Session and returns `device_id`, `session_id`, `environment_id` and `workspace_directory`. It never returns another credential: the executor credential becomes the daemon credential of that device. A retry with the same credential returns the same binding. A successful response carries `Cache-Control: no-store`.
`POST /api/v1/agent-daemon/enroll` with the executor credential and exactly `{"environment_id": "…"}` (no query) binds one dedicated device to the Environment and returns `device_id`, `environment_id` and `workspace_directory`. It never returns another credential: the executor credential becomes the daemon credential of that device. A retry with the same credential returns the same binding. A successful response carries `Cache-Control: no-store`.

| HTTP | When |
| --- | --- |
Expand Down
4 changes: 2 additions & 2 deletions contracts/agents-api/zh/machine-api.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "机器连接 API"
source: contracts/agents-api/machine-api.md
source_hash: 97ca1b4a9d8e7785afe6b3a84c9a56ff7bc08d24e2dfc31ed4e9a827eced9f6d
source_hash: a039b464b2e68843f1612d295aa0e5ee628ffc7891b1bd283139c2924796780a
---

机器通过 `/api/v1` 调用 Core:包括沙箱节点、Runtime daemon 和自托管安装程序。各路由仅接受所列凭据,不接受 Core 密钥或项目 API 密钥;控制台登录也不授予此处权限。反向代理将 `/api/v1` 直接发送给 Core;Web 不提供这些路由。
Expand Down Expand Up @@ -111,7 +111,7 @@ Core 在一个事务中检查 token 有效、部署已初始化且为节点型

### 登记自托管 daemon {#enroll-a-self-hosted-daemon}

`POST /api/v1/agent-daemon/enroll` 携带执行器凭据及精确正文 `{"environment_id": "…"}`(无查询),将一个专用设备绑定到 Environment 的 Session,返回 `device_id`、`session_id`、`environment_id` 和 `workspace_directory`。不返回其他凭据:执行器凭据成为该设备的 daemon 凭据。相同凭据重试返回相同绑定。成功响应包含 `Cache-Control: no-store`。
`POST /api/v1/agent-daemon/enroll` 携带执行器凭据及精确正文 `{"environment_id": "…"}`(无查询),将一个专用设备绑定到 Environment,返回 `device_id`、`environment_id` 和 `workspace_directory`。不返回其他凭据:执行器凭据成为该设备的 daemon 凭据。相同凭据重试返回相同绑定。成功响应包含 `Cache-Control: no-store`。

| HTTP | 时机 |
| --- | --- |
Expand Down
5 changes: 2 additions & 3 deletions internal/machineapi/machineapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,16 @@ type EnrollRequest struct {
EnvironmentID string `json:"environment_id"`
}

// Enrollment answers POST agent-daemon/enroll: the device bound to the Environment's Session.
// Enrollment answers POST agent-daemon/enroll: the device bound to the Environment.
type Enrollment struct {
DeviceID string `json:"device_id"`
SessionID string `json:"session_id"`
EnvironmentID string `json:"environment_id"`
WorkspaceDirectory string `json:"workspace_directory"`
}

// Validate checks that the binding names canonical IDs and the requested Environment.
func (e Enrollment) Validate(environment string) error {
if !CanonicalUUID(e.DeviceID) || !CanonicalUUID(e.SessionID) || e.EnvironmentID != environment {
if !CanonicalUUID(e.DeviceID) || e.EnvironmentID != environment {
return errors.New("machineapi: invalid enrollment binding")
}
return nil
Expand Down
4 changes: 2 additions & 2 deletions scripts/native-onboarding-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const windows = process.platform === 'win32';
const bundle = join(root, 'native-installer');
const manifest = JSON.parse(await readFile(join(bundle, 'bundle.json'), 'utf8'));
const protocol = (await readFile('internal/agentdaemon/proto/version.go', 'utf8')).match(/const Version = "([^"]+)"/)[1];
const environment = randomUUID(), session = randomUUID(), device = randomUUID();
const environment = randomUUID(), device = randomUUID();
const workspace = join(root, 'onboarding-workspace');
const installation = join(root, 'onboarding-installation');
// Core serves the base archive and one archive per component, as the release publishes them.
Expand Down Expand Up @@ -54,7 +54,7 @@ const server = createServer(async (request, response) => {
response.writeHead(204); return response.end();
}
if (!secret || request.headers.authorization !== `Bearer ${secret}`) return json(401, {});
if (url.pathname.endsWith('/enroll')) return json(200, { device_id: device, session_id: session, environment_id: environment, workspace_directory: workspace });
if (url.pathname.endsWith('/enroll')) return json(200, { device_id: device, environment_id: environment, workspace_directory: workspace });
if (url.pathname.endsWith('/bootstrap')) return json(200, { device_id: device, ws_url: remote, heartbeat_seconds: 15 });
if (url.pathname.endsWith('/connection')) return json(200, { environment_id: environment, status: connected ? 'connected' : 'disconnected' });
return json(404, {});
Expand Down
2 changes: 1 addition & 1 deletion services/core/internal/runtimeenrollment/enrollment.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func EnrollmentHandler(s EnrollmentStore) http.Handler {
default:
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
_ = json.NewEncoder(w).Encode(machineapi.Enrollment{DeviceID: binding.DeviceID, SessionID: binding.SessionID, EnvironmentID: binding.EnvironmentID, WorkspaceDirectory: binding.WorkspaceDirectory})
_ = json.NewEncoder(w).Encode(machineapi.Enrollment{DeviceID: binding.DeviceID, EnvironmentID: binding.EnvironmentID, WorkspaceDirectory: binding.WorkspaceDirectory})
}
})
}
4 changes: 2 additions & 2 deletions services/core/internal/runtimeenrollment/enrollment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func (s *enrollmentStub) EnrollRuntime(_ context.Context, environment, digest st
if environment != "environment" || digest != runtimedevice.HashCredential("private-test-token") {
return sessions.RuntimeEnrollment{}, errors.New("unexpected enrollment input")
}
return sessions.RuntimeEnrollment{DeviceID: "device", SessionID: "session", EnvironmentID: environment, WorkspaceDirectory: "/workspace"}, s.err
return sessions.RuntimeEnrollment{DeviceID: "device", EnvironmentID: environment, WorkspaceDirectory: "/workspace"}, s.err
}

func TestEnrollmentConnectionContract(t *testing.T) {
Expand Down Expand Up @@ -50,7 +50,7 @@ func TestEnrollmentConnectionContract(t *testing.T) {
if strings.Contains(response.Body.String(), "private") {
t.Fatal("enrollment leaked confidential details")
}
if response.Code == 200 && (response.Body.String() != `{"device_id":"device","session_id":"session","environment_id":"environment","workspace_directory":"/workspace"}`+"\n" || response.Header().Get("Cache-Control") != "no-store") {
if response.Code == 200 && (response.Body.String() != `{"device_id":"device","environment_id":"environment","workspace_directory":"/workspace"}`+"\n" || response.Header().Get("Cache-Control") != "no-store") {
t.Fatalf("binding response %s", response.Body.String())
}
})
Expand Down
3 changes: 1 addition & 2 deletions services/core/internal/sessions/devices.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ func NewDeviceRegistration(name, credentialHash string) (DeviceRegistration, err
// receives. It never carries another secret.
type RuntimeEnrollment struct {
DeviceID string
SessionID string
EnvironmentID string
WorkspaceDirectory string
}
Expand Down Expand Up @@ -180,7 +179,7 @@ func (s *Service) EnrollRuntime(ctx context.Context, environment, credentialHash
if err := tx.BindDevice(ctx, device); err != nil {
return err
}
result = RuntimeEnrollment{DeviceID: device, SessionID: current.SessionID, EnvironmentID: current.ID, WorkspaceDirectory: authority.WorkspaceDirectory}
result = RuntimeEnrollment{DeviceID: device, EnvironmentID: current.ID, WorkspaceDirectory: authority.WorkspaceDirectory}
return nil
})
return result, err
Expand Down
4 changes: 2 additions & 2 deletions services/core/internal/sessions/devices_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ func TestDeviceUseCases(t *testing.T) {
}

func TestEnrollRuntimeBindsUnderTheSessionLock(t *testing.T) {
environment := Environment{ID: "environment", SessionID: "session"}
environment := Environment{ID: "environment"}
authorized := returns(EnrollmentAuthority{KeyID: "key", WorkspaceDirectory: "/workspace"})
for _, test := range []struct {
name string
Expand All @@ -224,7 +224,7 @@ func TestEnrollRuntimeBindsUnderTheSessionLock(t *testing.T) {
if test.want == nil && err != nil || test.want != nil && !errors.Is(err, test.want) {
t.Fatalf("got %v, want %v", err, test.want)
}
if want := (RuntimeEnrollment{DeviceID: "device", SessionID: "session", EnvironmentID: "environment", WorkspaceDirectory: "/workspace"}); test.want == nil && enrolled != want {
if want := (RuntimeEnrollment{DeviceID: "device", EnvironmentID: "environment", WorkspaceDirectory: "/workspace"}); test.want == nil && enrolled != want {
t.Fatalf("enrollment %+v", enrolled)
}
if test.want != nil && enrolled != (RuntimeEnrollment{}) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func enrollFixtureSession(t *testing.T, s *Store, tenant string, session session
t.Fatal(err)
}
enrolled, err := sessionService(t, s).EnrollRuntime(t.Context(), environment.ID, runtimedevice.HashCredential(key.Token))
if err != nil || enrolled.EnvironmentID != environment.ID || enrolled.SessionID != session.ID || enrolled.WorkspaceDirectory != "/workspace" {
if err != nil || enrolled.EnvironmentID != environment.ID || enrolled.WorkspaceDirectory != "/workspace" {
t.Fatalf("Runtime enrollment: %+v %v", enrolled, err)
}
bound, err := sessionAdapter(s).GetSessionDevice(t.Context(), tenant, session.ID)
Expand Down
2 changes: 1 addition & 1 deletion services/core/tests/integration/runtime_enrollment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func TestRuntimeEnrollmentAuthorityAndRotation(t *testing.T) {
}
}
bound, err := sessionService(t, s).EnrollRuntime(ctx, environment.ID, executorDigest(key.Token))
if err != nil || bound.SessionID != session.ID || bound.EnvironmentID != environment.ID || bound.WorkspaceDirectory != "/workspace" {
if err != nil || bound.EnvironmentID != environment.ID || bound.WorkspaceDirectory != "/workspace" {
t.Fatalf("enrollment: %+v %v", bound, err)
}
if again, err := sessionService(t, s).EnrollRuntime(ctx, environment.ID, executorDigest(key.Token)); err != nil || again != bound {
Expand Down
Loading