From 2164ccb8bf620ad210ff81835523b1315d35eb3f Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Mon, 19 Oct 2026 06:16:37 +0000 Subject: [PATCH 1/2] Let Codex return structured output Declare structured output for Codex and send the frozen schema as the native outputSchema on every root turn/start. The native strict handling, provider errors and final message text pass through unchanged. --- .../agent/codex/harness_config_test.go | 49 +++++++++++++++++++ apps/daemon/internal/agent/codex/options.go | 7 +++ .../internal/agent/codex/preparation.go | 3 -- apps/daemon/internal/agent/codex/protocol.go | 2 + .../internal/agent/codex/session_run.go | 5 +- contracts/agents-api/execution-tools.md | 2 + contracts/agents-api/harness-capabilities.md | 2 +- contracts/agents-api/zh/execution-tools.md | 4 +- .../agents-api/zh/harness-capabilities.md | 4 +- internal/harnessconfig/codex/declaration.go | 2 +- .../harnessconfig/codex/declaration_test.go | 20 ++++++++ .../execution/structured_output_test.go | 2 +- 12 files changed, 91 insertions(+), 11 deletions(-) create mode 100644 internal/harnessconfig/codex/declaration_test.go diff --git a/apps/daemon/internal/agent/codex/harness_config_test.go b/apps/daemon/internal/agent/codex/harness_config_test.go index a72417722..cf2c28dfb 100644 --- a/apps/daemon/internal/agent/codex/harness_config_test.go +++ b/apps/daemon/internal/agent/codex/harness_config_test.go @@ -97,3 +97,52 @@ func TestReasoningEffortReachesEveryNativeTurn(t *testing.T) { }) } } + +func TestOutputSchemaReachesEveryNativeTurn(t *testing.T) { + for _, resumeID := range []string{"", "fixture-native-thread"} { + t.Run("resume="+resumeID, func(t *testing.T) { + req, cfg, root := preparationFixture(t) + t.Setenv("OAC_TEST_EXECUTOR_MODE", "complete") + req.AgentSessionID = resumeID + schema := json.RawMessage(`{"type":"object","properties":{"answer":{"type":"string"}},"required":["answer"],"additionalProperties":false}`) + req.ExecutionControls = &proto.ExecutionControls{OutputFormat: &proto.OutputFormat{Type: "json_schema", Schema: schema}} + ownerCtx, cancelOwner := context.WithCancel(context.Background()) + t.Cleanup(cancelOwner) + e, err := newExecutor(ownerCtx, req, cfg) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := e.Close(ctx); err != nil { + t.Error(err) + } + }) + for _, id := range []string{"one", "two"} { + out := make(chan proto.Envelope, 20) + turn, err := e.StartTurn(t.Context(), id, proto.TextInput("answer"), out) + if err != nil { + t.Fatal(err) + } + awaitExecutorTurn(t, turn, out) + } + turns := 0 + for _, frame := range preparationFrames(t, root) { + if frame.Method != "turn/start" { + continue + } + turns++ + var wire struct { + OutputSchema json.RawMessage `json:"outputSchema"` + } + if json.Unmarshal(frame.Params, &wire) != nil || string(wire.OutputSchema) != string(schema) { + t.Fatal("Turn lost the frozen output schema", string(frame.Params)) + } + } + if turns != 2 { + t.Fatal("expected two Turns", turns) + } + }) + } +} diff --git a/apps/daemon/internal/agent/codex/options.go b/apps/daemon/internal/agent/codex/options.go index bd5b29a9a..0b8900da8 100644 --- a/apps/daemon/internal/agent/codex/options.go +++ b/apps/daemon/internal/agent/codex/options.go @@ -55,6 +55,10 @@ type SessionPlan struct { // ModelReasoningEffort is frozen for launch and every native Turn. ModelReasoningEffort string + // OutputSchema is the frozen public JSON Schema sent as outputSchema on every + // native root Turn. + OutputSchema json.RawMessage + // Cleanup is the deferred housekeeping the session must run after the child exits. Cleanup func() } @@ -161,6 +165,9 @@ func applyExecutionControls(plan *SessionPlan, controls *proto.ExecutionControls } } } + if format := controls.OutputFormat; format != nil { + plan.OutputSchema = format.Schema + } if controls.ReasoningEffort != "" { plan.ModelReasoningEffort = controls.ReasoningEffort set("model_reasoning_effort", strconv(controls.ReasoningEffort)) diff --git a/apps/daemon/internal/agent/codex/preparation.go b/apps/daemon/internal/agent/codex/preparation.go index 9c299de8b..d16310b83 100644 --- a/apps/daemon/internal/agent/codex/preparation.go +++ b/apps/daemon/internal/agent/codex/preparation.go @@ -12,9 +12,6 @@ import ( ) func newPreparation(parent context.Context, req proto.PromptRequestPayload, cfg sessionConfig) (*Prepared, error) { - if req.ExecutionControls != nil && req.ExecutionControls.OutputFormat != nil { - return nil, errors.New("codex: structured output is not qualified") - } if req.WorkspaceReadOnly { return nil, errors.New("codex: workspace reads use the local Runtime interface") } diff --git a/apps/daemon/internal/agent/codex/protocol.go b/apps/daemon/internal/agent/codex/protocol.go index 2daa4acb7..fbf8b2c2d 100644 --- a/apps/daemon/internal/agent/codex/protocol.go +++ b/apps/daemon/internal/agent/codex/protocol.go @@ -167,6 +167,8 @@ type TurnStartParams struct { ThreadID string `json:"threadId"` Input []UserInput `json:"input"` CollaborationMode *CollaborationMode `json:"collaborationMode,omitempty"` + // OutputSchema constrains this Turn's final assistant message natively. + OutputSchema json.RawMessage `json:"outputSchema,omitempty"` } type CollaborationModeKind string diff --git a/apps/daemon/internal/agent/codex/session_run.go b/apps/daemon/internal/agent/codex/session_run.go index 3394acf90..d370ccb67 100644 --- a/apps/daemon/internal/agent/codex/session_run.go +++ b/apps/daemon/internal/agent/codex/session_run.go @@ -21,8 +21,9 @@ func (s *Session) startNative(ctx context.Context, plan SessionPlan, req proto.P return err } turnParams := TurnStartParams{ - ThreadID: s.currentThreadID(), - Input: input, + ThreadID: s.currentThreadID(), + Input: input, + OutputSchema: plan.OutputSchema, } model := strings.TrimSpace(s.resolvedModel) if model == "" { diff --git a/contracts/agents-api/execution-tools.md b/contracts/agents-api/execution-tools.md index 913f8df1f..5da10f5f7 100644 --- a/contracts/agents-api/execution-tools.md +++ b/contracts/agents-api/execution-tools.md @@ -43,6 +43,8 @@ Core carries the schema in `ExecutionControls.OutputFormat` and requires structu The Claude adapter passes `outputFormat` to the pinned SDK and allows its native `StructuredOutput` terminal tool, which is internal and never an extra caller function. A matching live root tool result and an attributed successful SDK result confirm the output. The adapter publishes the native `result.result` string unchanged as a completed `final_answer` message with the native tool-use ID; parent assistant prose keeps its own ID. Unvalidated retries and cancelled candidates never become the answer, and the adapter never serializes `structured_output` back to JSON. The stream follows the official message sequence with the whole text in one `output_text.delta`. The bridge advertises the operation only when it reports `structured_output`, and a workspace Runtime also needs `workspace_structured_output`. +The Codex adapter sends the schema unchanged as the native `outputSchema` on every root Turn, including Turns after resume or recovery, and publishes the native final agent message text unchanged. Codex reads JSON numbers losslessly, so Session admission applies no number or root-type limit beyond the common rules. A steered input joins the running Turn under its schema, and Subagent threads do not receive the schema. Codex passes the schema to the model provider as a strict Responses `text.format`; a provider that cannot run it returns its own error unchanged, and a provider that ignores it can return text that does not match the schema. + ## Deferred function discovery A `tool_search` tool has only `type`; Responses-only execution fields are rejected. Function `defer_loading` marks which definitions load lazily. Discovery requires both: `tool_search` without a deferred function, or deferred functions without `tool_search`, is rejected. The saved-Agent tool union keeps `tool_search`; the pinned Session response union omits it, so Session and SSE resources project it out while the frozen configuration keeps it. The pinned Items union has no tool-search Item, and Core invents none. diff --git a/contracts/agents-api/harness-capabilities.md b/contracts/agents-api/harness-capabilities.md index 57636381c..5a755516a 100644 --- a/contracts/agents-api/harness-capabilities.md +++ b/contracts/agents-api/harness-capabilities.md @@ -37,7 +37,7 @@ Native model parameters and provider protocols per Harness are in [model executi | --- | --- | --- | --- | | [Public functions](./execution-tools.md#functions) with text results | Verified: `none`, hosted; admitted: self-hosted | Verified on all placements; object-root schemas only | Rejected | | [Function results with images](./message-content.md#function-results) | Verified: `none`, hosted; admitted: self-hosted | Verified on all placements; successful inline PNG or JPEG results only | Rejected | -| [Structured output](./execution-tools.md#structured-output) | Rejected | Verified on all placements | Rejected | +| [Structured output](./execution-tools.md#structured-output) | Admitted on all placements | Verified on all placements | Rejected | | [Deferred function discovery](./execution-tools.md#deferred-function-discovery) | Rejected | Verified: `none`, self-hosted; admitted: hosted | Rejected | | [Disabled web search and programmatic tool calling](./execution-tools.md#web-search-and-programmatic-tool-calling) | Verified: `none`; admitted: hosted, self-hosted | Verified: `none`; admitted: hosted, self-hosted | Verified: `none`; admitted: hosted, self-hosted | | Web search `cached` or `live` and programmatic tool calling `enabled` | Admitted on all placements | `live` admitted on hosted and self-hosted, rejected on `none`; `cached` and `enabled` rejected | Rejected | diff --git a/contracts/agents-api/zh/execution-tools.md b/contracts/agents-api/zh/execution-tools.md index d64bb977d..7364a0839 100644 --- a/contracts/agents-api/zh/execution-tools.md +++ b/contracts/agents-api/zh/execution-tools.md @@ -1,7 +1,7 @@ --- title: "执行工具" source: contracts/agents-api/execution-tools.md -source_hash: 6b0ae99f9482a8082d20369ebe1facbc262718426dfc5d697ae63bc47ae39853 +source_hash: 7608c08f707c1970f1365d9b5d5f0979a71af35c4b9ca82d64ec6d2d95344a2a --- Agent 在 `tools` 中声明应用函数、控制项和 MCP 服务器,并可在 `text.format` 中声明输出 schema。本契约说明 Core 如何验证声明、哪些内容跨越 Runtime 边界,以及调用方如何恢复待执行操作。[Harness 能力](harness-capabilities.md)列出各 Harness 在不同部署位置支持的操作。原生工作区工具和 Environment Plugin MCP 属于 [Environment](environments.md#skills-plugins-and-environment-mcp)。 @@ -45,6 +45,8 @@ Core 在 `ExecutionControls.OutputFormat` 中携带 schema,仅对使用该选 Claude 适配器将 `outputFormat` 传给固定版本 SDK,并允许原生 `StructuredOutput` 终态工具;该工具属于内部,不是额外的调用方函数。匹配的实时根工具结果和已归属的成功 SDK 结果确认输出。适配器将原生 `result.result` 字符串原样发布为已完成的 `final_answer` 消息,使用原生 tool-use ID;父 assistant 文本保留自己的 ID。未验证重试和已取消候选不会成为答案,适配器不会把 `structured_output` 重新序列化为 JSON。流遵循官方消息顺序,将整段文本放入一个 `output_text.delta`。桥接层仅在报告 `structured_output` 时声明该操作,工作区 Runtime 还需要 `workspace_structured_output`。 +Codex 适配器在每个根 Turn(包括 resume 或恢复后的 Turn)上将 schema 原样作为原生 `outputSchema` 发送,并原样发布原生最终 agent 消息文本。Codex 无损读取 JSON 数字,因此 Session 准入除通用规则外不附加数字或根类型限制。引导(steer)输入并入正在运行的 Turn 并沿用其 schema,Subagent 线程不接收该 schema。Codex 将 schema 作为 strict 的 Responses `text.format` 传给模型提供商;无法运行它的提供商原样返回自己的错误,忽略它的提供商可能返回与 schema 不符的文本。 + ## 延迟函数发现 {#deferred-function-discovery} `tool_search` 工具仅包含 `type`;仅适用于 Responses 的执行字段被拒绝。函数 `defer_loading` 标记延迟加载的定义。发现要求两者同时存在:没有延迟函数的 `tool_search`,或没有 `tool_search` 的延迟函数均被拒绝。已保存 Agent 的工具联合类型保留 `tool_search`;固定版本 Session 响应联合类型省略它,因此 Session 与 SSE 资源投影移除它,冻结配置仍保留它。固定版本 Item 联合类型没有 tool-search Item,Core 不自行创建。 diff --git a/contracts/agents-api/zh/harness-capabilities.md b/contracts/agents-api/zh/harness-capabilities.md index df837f84d..cef752996 100644 --- a/contracts/agents-api/zh/harness-capabilities.md +++ b/contracts/agents-api/zh/harness-capabilities.md @@ -1,7 +1,7 @@ --- title: "Harness 能力" source: contracts/agents-api/harness-capabilities.md -source_hash: c471671c3e4518b2968fc36fd5310edf449de5684fc976ebf0c015996aca3b6c +source_hash: 818fecdaf4bca14f5d37b6135af4eed6c13b48c801e4dd1034317a86ef5a247c --- 本页列出每个 Harness 在每种部署位置支持的能力。Core 根据 `internal/harnessconfig//declaration.go` 中 Harness 的声明决定准入,运行 Session 的 Runtime 也必须声明操作。所链接契约定义各操作;[Harness 接入](harness-onboarding.md#qualify-the-adapter)说明资格验证方法。 @@ -39,7 +39,7 @@ source_hash: c471671c3e4518b2968fc36fd5310edf449de5684fc976ebf0c015996aca3b6c | --- | --- | --- | --- | | 文本结果的[公开函数](execution-tools.md#functions) | 已验证:`none`、托管;已准入:自托管 | 所有位置已验证;仅对象根 schema | 已拒绝 | | [含图像函数结果](message-content.md#function-results) | 已验证:`none`、托管;已准入:自托管 | 所有位置已验证;仅成功结果中的内联 PNG 或 JPEG | 已拒绝 | -| [结构化输出](execution-tools.md#structured-output) | 已拒绝 | 所有位置已验证 | 已拒绝 | +| [结构化输出](execution-tools.md#structured-output) | 所有位置已准入 | 所有位置已验证 | 已拒绝 | | [延迟函数发现](execution-tools.md#deferred-function-discovery) | 已拒绝 | 已验证:`none`、自托管;已准入:托管 | 已拒绝 | | [禁用 Web 搜索与程序化工具调用](execution-tools.md#web-search-and-programmatic-tool-calling) | 已验证:`none`;已准入:托管、自托管 | 已验证:`none`;已准入:托管、自托管 | 已验证:`none`;已准入:托管、自托管 | | Web 搜索 `cached` 或 `live`,以及程序化工具调用 `enabled` | 所有位置已准入 | `live` 在托管和自托管已准入,在 `none` 已拒绝;`cached` 和 `enabled` 已拒绝 | 已拒绝 | diff --git a/internal/harnessconfig/codex/declaration.go b/internal/harnessconfig/codex/declaration.go index f906e99d5..484be6fe3 100644 --- a/internal/harnessconfig/codex/declaration.go +++ b/internal/harnessconfig/codex/declaration.go @@ -33,7 +33,7 @@ var Declaration = harnessconfig.Declaration{ WorkspaceReadPreparation: proto.CapabilitySupported, WorkspaceOutputExport: proto.CapabilitySupported, ExecutionControls: proto.CapabilitySupported, - StructuredOutput: proto.CapabilityUnsupported, + StructuredOutput: proto.CapabilitySupported, ToolSearch: proto.CapabilityUnsupported, MessageImages: proto.CapabilitySupported, FunctionResultImages: proto.CapabilitySupported, diff --git a/internal/harnessconfig/codex/declaration_test.go b/internal/harnessconfig/codex/declaration_test.go new file mode 100644 index 000000000..622f28e8c --- /dev/null +++ b/internal/harnessconfig/codex/declaration_test.go @@ -0,0 +1,20 @@ +package codex + +import ( + "testing" + + "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" +) + +func TestDeclarationAdmitsStructuredOutput(t *testing.T) { + if !Declaration.Capabilities.StructuredOutput.IsSupported() { + t.Fatal("structured output is not declared") + } + err := Declaration.ValidateConfiguration(harnessconfig.Admission{ + Model: "model", TextFormat: "json_schema", TextSchema: []byte(`{"type":"object"}`), + Environment: &harnessconfig.AdmissionEnvironment{Type: "none"}, + }) + if err != nil { + t.Fatalf("json_schema rejected: %v", err) + } +} diff --git a/services/core/internal/execution/structured_output_test.go b/services/core/internal/execution/structured_output_test.go index ce1b9fafa..e4a7d9414 100644 --- a/services/core/internal/execution/structured_output_test.go +++ b/services/core/internal/execution/structured_output_test.go @@ -16,7 +16,7 @@ import ( func TestStructuredOutputNeedsOperationQualification(t *testing.T) { raw := json.RawMessage(`{"agent":{"model":"model","text":{"format":{"type":"json_schema","schema":{"type":"object"}}}},"environment":{"type":"none"}}`) for _, kind := range []string{"codex", "claude_sdk", "mcode"} { - if err := (Policy{}).ValidateSessionConfiguration(kind, raw); (err == nil) != (kind == "claude_sdk") { + if err := (Policy{}).ValidateSessionConfiguration(kind, raw); (err == nil) != (kind != "mcode") { t.Fatalf("%s: %v", kind, err) } } From 364725c394c3296f204980a86cc189b02886ea24 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Mon, 19 Oct 2026 06:38:44 +0000 Subject: [PATCH 2/2] Update tests that used Codex structured output as unsupported --- services/core/internal/api/configuration_validation_test.go | 1 - services/core/internal/api/text_configuration_test.go | 2 +- .../tests/integration/configuration_validation_public_test.go | 2 +- services/core/tests/official_agent_reference_retry.py | 2 +- services/core/tests/official_agent_references.py | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/services/core/internal/api/configuration_validation_test.go b/services/core/internal/api/configuration_validation_test.go index 3126f3170..87a6525bd 100644 --- a/services/core/internal/api/configuration_validation_test.go +++ b/services/core/internal/api/configuration_validation_test.go @@ -260,7 +260,6 @@ func TestAgentConfigurationLocalLimitsKeepCodes(t *testing.T) { session := configurationOperations()[2] for _, tc := range []struct{ fields, message string }{ {`"tools":[{"type":"programmatic_tool_calling","enabled":false},{"type":"programmatic_tool_calling","enabled":false}]`, "Execution requires distinct tool controls."}, - {`"text":{"format":{"type":"json_schema","schema":{"type":"object"}}}`, "Harness codex does not support the requested Agent/environment configuration: Structured output is not supported by this engine."}, {`"multi_agent":{"enabled":true},"tools":[{"type":"programmatic_tool_calling","enabled":true}]`, "Harness codex does not support the requested Agent/environment configuration: programmatic_tool_calling.enabled=true is not supported with multi_agent by this Harness."}, {`"tools":[{"type":"function","name":"","description":"","parameters":{}}]`, "Function names must be nonempty, unique and at most 512 bytes."}, } { diff --git a/services/core/internal/api/text_configuration_test.go b/services/core/internal/api/text_configuration_test.go index 7b2eff4a7..5565a7c11 100644 --- a/services/core/internal/api/text_configuration_test.go +++ b/services/core/internal/api/text_configuration_test.go @@ -43,7 +43,7 @@ func TestTextConfigurationHTTP(t *testing.T) { } }) } - for _, invalid := range []string{`{"verbosity":""}`, `{"verbosity":"verbose"}`, `{"verbosity":4}`, `{"format":{}}`, `{"format":{"type":"json_schema","schema":{}}}`, `{"format":{"type":"text","extra":true}}`, `{"unknown":true}`} { + for _, invalid := range []string{`{"verbosity":""}`, `{"verbosity":"verbose"}`, `{"verbosity":4}`, `{"format":{}}`, `{"format":{"type":"json_schema","schema":{"type":"string"}}}`, `{"format":{"type":"text","extra":true}}`, `{"unknown":true}`} { h, s, _ := testHandler(t) req := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions", strings.NewReader(`{"agent":{"model":"example","text":`+invalid+`},"environment":{"type":"none"},"input":"Describe the configured response format."}`)) req.Header.Set("Authorization", "Bearer test-api-key") diff --git a/services/core/tests/integration/configuration_validation_public_test.go b/services/core/tests/integration/configuration_validation_public_test.go index 8c37f6bbf..052d5aa40 100644 --- a/services/core/tests/integration/configuration_validation_public_test.go +++ b/services/core/tests/integration/configuration_validation_public_test.go @@ -121,7 +121,7 @@ func TestAgentConfigurationValidationRejectsWithoutWritesPostgres(t *testing.T) // K2 keeps the local configuration code; saved enabled web_search (TV-05) is // covered by TestSavedWebSearchPostgres. for _, request := range []struct{ path, body, message string }{ - {"/v1/agents/sessions", `{"agent":{"model":"m","text":{"format":{"type":"json_schema","schema":{"type":"object"}}}},"environment":{"type":"none"},"input":"hi"}`, "Harness codex does not support the requested Agent/environment configuration: Structured output is not supported by this engine."}, + {"/v1/agents/sessions", `{"agent":{"model":"m","multi_agent":{"enabled":true},"tools":[{"type":"programmatic_tool_calling","enabled":true}]},"environment":{"type":"none"},"input":"hi"}`, "Harness codex does not support the requested Agent/environment configuration: programmatic_tool_calling.enabled=true is not supported with multi_agent by this Harness."}, } { encoded, _ := json.Marshal(request.message) want := `{"error":{"message":` + string(encoded) + `,"type":"invalid_request_error","code":"unsupported_or_invalid_configuration","param":null}}` + "\n" diff --git a/services/core/tests/official_agent_reference_retry.py b/services/core/tests/official_agent_reference_retry.py index 2e3811b85..fedc07591 100644 --- a/services/core/tests/official_agent_reference_retry.py +++ b/services/core/tests/official_agent_reference_retry.py @@ -38,7 +38,7 @@ def mutate(**values): fresh = sessions.create(**spec) assert fresh.agent.model == "changed-model" and fresh.agent.instructions == "changed" assert first.agent.model == "original-model" and first.agent.instructions == "frozen" - mutate(patch={"text":{"format":{"type":"json_schema","schema":{"type":"object"}},"verbosity":"medium"}}) + mutate(patch={"tools":[{"type":"tool_search"}]}) expect(BadRequestError, lambda:sessions.create(**spec)) assert sessions.create(**spec, extra_headers=headers) == first for changed in ({"input":"different"}, {"metadata":{"changed":"yes"}}, {"agent":{"instructions":"frozen"}}, {"agent_id":fresh.id}): diff --git a/services/core/tests/official_agent_references.py b/services/core/tests/official_agent_references.py index 54f121794..ab4fb731f 100644 --- a/services/core/tests/official_agent_references.py +++ b/services/core/tests/official_agent_references.py @@ -59,7 +59,6 @@ def verify_agent_references(client, other, expect_error): # Configuration storage is broader than execution. Never silently drop an # unsupported saved option, but admit a supported whole-field replacement. for field, value, replacement in ( - ("text", {"format": {"type": "json_schema", "schema": {"type": "object"}}}, {"verbosity": "medium"}), ("tools", [dict(tool, defer_loading=True)], None), ("tools", [{"type": "tool_search"}], []), ): @@ -78,6 +77,7 @@ def verify_agent_references(client, other, expect_error): ("reasoning", {"effort": "high"}, {"effort": "low"}), ("reasoning", {"summary": "auto"}, {"summary": "concise"}), ("service_tier", "fast", "auto"), + ("text", {"format": {"type": "json_schema", "schema": {"type": "object"}}, "verbosity": "medium"}, {"format": {"type": "text"}, "verbosity": "medium"}), ): supported = agents.create(model="model", **{field: value}) reference = {"input": "Verify agent references fixture admission.", "agent_id": supported.id, "environment": {"type": "none"}}