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
2 changes: 1 addition & 1 deletion contracts/agents-api/environments.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ stateDiagram-v2

Session environment events carry Session and Environment identity and an optional Turn identity, and never configuration, credentials, registration IDs or revisions. A Session's `required_actions` can contain `{type: "environment_connection", environment_id}`, separate from function calls. Environment connection, Session status and Turn status are independent.

Core creates the Environment record in the Session creation transaction; the Session upsert picks the retry winner, and retries never create or repair an Environment. The Environment derives its Project and immutable configuration from the owning Session. Its first state is `pending`. After the Session is deleted, reads hide the Environment while Core keeps the record for settlement and cleanup. A Session with `none` has no Environment.
Core creates the Environment record in the Session creation transaction; the Session upsert picks the retry winner, and retries never create or repair an Environment. The Environment derives its Project and immutable configuration from the owning Session. Its first state is `pending`; a `self_hosted` Environment reads `disconnected` until its first connection. After the Session is deleted, reads hide the Environment while Core keeps the record for settlement and cleanup. A Session with `none` has no Environment.

A managed outer Environment must exclude broader application credentials and other tenants’ secrets.

Expand Down
4 changes: 2 additions & 2 deletions contracts/agents-api/zh/environments.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "Environment 与模板"
source: contracts/agents-api/environments.md
source_hash: eb7282383dcac86723189824b0391c7dd50336257db922a1fce2b2f2d2153049
source_hash: 15550304b85596e09cbeb4cc6d7253a4bdd64ffd6adc58a539511e0c939f7ad6
---

Environment 是 Session 的执行资源,包括 Harness 运行所在的机器、工作区以及已完成准备的能力。Session 通过其 `environment` 配置创建 Environment;不存在独立的 create 调用。Environment 模板是 Session 创建时解析的可复用准备配置。本契约涵盖这两类资源、两种放置方式、输入接纳、能力准备、Skills、Plugins 和 MCP 连接来源。
Expand Down Expand Up @@ -55,7 +55,7 @@ stateDiagram-v2

Session environment 事件携带 Session 和 Environment 的身份标识以及可选的 Turn 身份标识,绝不携带配置、凭据、注册 ID 或修订号。Session 的 `required_actions` 可以包含 `{type: "environment_connection", environment_id}`,这与函数调用相互独立。Environment 连接、Session 状态和 Turn 状态相互独立。

Core 在 Session 创建事务中创建 Environment 记录;Session upsert 会选出重试的胜者,而重试绝不创建或修复 Environment。Environment 从所属 Session 派生其项目和不可变配置。其首个状态为 `pending`。Session 删除后,读取会隐藏 Environment,而 Core 仍保留该记录,用于结算和清理。Session 为 `none` 时没有 Environment。
Core 在 Session 创建事务中创建 Environment 记录;Session upsert 会选出重试的胜者,而重试绝不创建或修复 Environment。Environment 从所属 Session 派生其项目和不可变配置。其首个状态为 `pending`;`self_hosted` Environment 在首次连接前读作 `disconnected`。Session 删除后,读取会隐藏 Environment,而 Core 仍保留该记录,用于结算和清理。Session 为 `none` 时没有 Environment。

托管的外层 Environment 必须排除权限范围更广的应用程序凭据和其他租户的机密。

Expand Down
7 changes: 6 additions & 1 deletion services/core/internal/api/environments.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ func environmentResponse(environment sessions.Environment) (v1.EnvironmentInfo,
default:
return v1.EnvironmentInfo{}, errors.New("unsupported stored environment resource status")
}
status := environment.Status
if status == "pending" && configuration.Type == "self_hosted" {
// A self_hosted Environment that has never connected reads disconnected.
status = "disconnected"
}
files := configuration.Files
if files == nil {
files = []json.RawMessage{}
Expand All @@ -66,7 +71,7 @@ func environmentResponse(environment sessions.Environment) (v1.EnvironmentInfo,
configuration.Plugins = []json.RawMessage{}
}
return v1.EnvironmentInfo{
ID: environment.ID, Object: "agent.environment", Type: configuration.Type, Status: environment.Status,
ID: environment.ID, Object: "agent.environment", Type: configuration.Type, Status: status,
Files: files, Plugins: configuration.Plugins, Skills: configuration.Skills,
}, nil
}
2 changes: 1 addition & 1 deletion services/core/internal/api/environments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func TestEnvironmentResourceExactProjectionWithoutExecution(t *testing.T) {
t.Fatal("resource read failed", w.Code, w.Body)
}
want := map[string]any{
"id": f.environment.ID, "object": "agent.environment", "type": "self_hosted", "status": status,
"id": f.environment.ID, "object": "agent.environment", "type": "self_hosted", "status": strings.Replace(status, "pending", "disconnected", 1),
"files": []any{}, "plugins": []any{}, "skills": []any{},
}
if !reflect.DeepEqual(got, want) || f.calls != 1 || f.tenant != f.environment.TenantID || f.id != f.environment.ID {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def observe(name):
assert list(api.beta.agents.sessions.turns.list(session_id)) == []
assert list(api.beta.agents.sessions.items.list(session_id)) == []
environment = api.beta.agents.environments.retrieve(settings["environment_id"])
assert environment.status == "pending"
assert environment.status == "disconnected"
for suffix in ("", "/events", "/turns", "/items"):
assert raw.get("/agents/sessions/" + session_id + suffix, headers={"Authorization": "Bearer " + settings["foreign_token"]}).status_code == 404
print(json.dumps({"status": "private_initial_failure_public_reads_and_live_events_verified", "session_id": session_id,
Expand Down
6 changes: 3 additions & 3 deletions services/core/tests/official_environment_retrieve.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,14 @@ def client(key):
other = foreign.beta.agents.sessions.create(**creation, extra_body=PROVIDER)
result = {"environment_id": session.environment.id, "deleted_environment_id": removed.environment.id,
"foreign_environment_id": other.environment.id}
verify_environment(api.beta.agents.environments.retrieve(removed.environment.id).to_dict(), removed.environment.id, "pending")
verify_environment(api.beta.agents.environments.retrieve(removed.environment.id).to_dict(), removed.environment.id, "disconnected")
api.beta.agents.sessions.delete(removed.id)
assert list(api.beta.agents.sessions.turns.list(session.id)) == []
assert list(api.beta.agents.sessions.items.list(session.id)) == []

environment_id = result["environment_id"]
endpoint = base + "/v1/agents/environments/" + environment_id
expected = verify_environment(api.beta.agents.environments.retrieve(environment_id).to_dict(), environment_id, "pending")
expected = verify_environment(api.beta.agents.environments.retrieve(environment_id).to_dict(), environment_id, "disconnected")
assert peer.beta.agents.environments.retrieve(environment_id).to_dict() == expected
raw = api.beta.agents.environments.with_raw_response.retrieve(environment_id.upper())
assert raw.status_code == 200 and raw.http_response.json() == expected and raw.parse().to_dict() == expected
Expand Down Expand Up @@ -112,7 +112,7 @@ def rejected(url, status, code, request_headers=headers, method="GET"):
for method in ("POST", "PATCH", "DELETE"):
rejected(endpoint, 405, "unsupported_operation", method=method)
assert api.beta.agents.environments.retrieve(environment_id).to_dict() == expected
verify_environment(foreign.beta.agents.environments.retrieve(result["foreign_environment_id"]).to_dict(), result["foreign_environment_id"], "pending")
verify_environment(foreign.beta.agents.environments.retrieve(result["foreign_environment_id"]).to_dict(), result["foreign_environment_id"], "disconnected")
print(json.dumps(result))


Expand Down
2 changes: 1 addition & 1 deletion services/core/tests/official_self_hosted_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ def observe(name):
raise AssertionError("public initial failure observer failed") from failures[0]
assert observations["sdk"] == observations["raw"]
assert retry(case, "failed") == observations["sdk"]["session"]
assert api.beta.agents.environments.retrieve(case["environment_id"]).status == "pending"
assert api.beta.agents.environments.retrieve(case["environment_id"]).status == "disconnected"
for retained in cases[1:]:
assert retry(retained) == retained["snapshot"]
result["controlled_deadline_expiry"] = True
Expand Down
Loading