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
9 changes: 5 additions & 4 deletions apps/daemon/internal/agent/mcode/environment_mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,14 @@ func TestEnvironmentMCPRejectsUnqualifiedAuthorityBeforePreparation(t *testing.T
case "duplicate":
req.LocalEnvironment.MCP = append(req.LocalEnvironment.MCP, environmentMCPFixture())
}
_, err := prepareWorkspaceOptions(testStateRoot(t), c, req)
if err == nil {
_, err = runtimeMCP(req)
}
root := t.TempDir()
_, err := NewExecutorFactory(root, &c)(context.Background(), req)
if err == nil || strings.Contains(err.Error(), "confidential-http-token") {
t.Fatal("unqualified declaration accepted or credential exposed")
}
if entries, _ := os.ReadDir(root); len(entries) != 0 {
t.Fatalf("native files written before MCP was rejected: %v", entries)
}
})
}
}
Expand Down
10 changes: 6 additions & 4 deletions apps/daemon/internal/agent/mcode/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ func NewExecutorFactory(runtimeRoot string, config *WorkspaceConfig) agent.Execu
if req.RunID != "" || len(req.Input) != 0 {
return nil, fmt.Errorf("mcode: Executor configuration cannot contain Turn input")
}
var err error
// MCP is checked before any native file is written.
mcp, err := runtimeMCP(req)
if err != nil {
return nil, err
}
var opts launchOptions
binary := defaultBinary()
if frozen == nil {
Expand All @@ -50,12 +54,10 @@ func NewExecutorFactory(runtimeRoot string, config *WorkspaceConfig) agent.Execu
binary = frozen.Binary
opts, err = prepareWorkspaceOptions(runtimeRoot, *frozen, req)
}
if err == nil {
opts.MCP, err = runtimeMCP(req)
}
if err != nil {
return nil, err
}
opts.MCP = mcp
resource, err := newExecutor(ctx, req, opts, binary)
if resource == nil {
return nil, err
Expand Down
1 change: 0 additions & 1 deletion apps/daemon/internal/agent/mcode/model_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ func TestOptionsRejectInvalidProvider(t *testing.T) {
{"protocol alias", "protocol", "chat-completions"},
{"remote HTTP", "base_url", "http://provider.example/v1"},
{"empty key", "api_key", ""},
{"output without context", "context_window", 0},
{"excess output", "max_output_tokens", 64001},
{"native options", "options", map[string]any{"apiKey": "fixture-key"}},
} {
Expand Down
3 changes: 1 addition & 2 deletions apps/daemon/internal/agent/mcode/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func agentCapabilities(req proto.PromptRequestPayload, workspace *workspaceProfi
tools = append(tools, "web_fetch")
}
}
capabilities := map[string]any{
return map[string]any{
"tools": tools,
"features": map[string]bool{
// The mavis tool carries cron scheduling that runs beyond a Turn, plus session send, agent create and MCP create.
Expand All @@ -124,7 +124,6 @@ func agentCapabilities(req proto.PromptRequestPayload, workspace *workspaceProfi
"webSearch": false,
},
}
return capabilities
}

func optionString(options map[string]any, key string) string {
Expand Down
6 changes: 3 additions & 3 deletions apps/web/e2e/system.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ test("sets, replaces and clears a harness's default model configuration, and kee
// Codex accepts no native Harness configuration.
await set.getByText("Advanced settings", { exact: true }).click();
await expect(set.getByLabel("Harness configuration (JSON)")).toHaveCount(0);
// Limits are Core's 32-bit whole numbers, and max output needs a context window at least as large.
// Limits are Core's 32-bit whole numbers, and max output fits a set context window.
await set.getByLabel("Max output tokens").fill("32000");
await expect(set.getByLabel("Context window", { exact: true })).not.toHaveAttribute("aria-invalid", "true");
await set.getByLabel("Context window", { exact: true }).fill("1000");
await expect(set.getByText("Set a context window at least this large.")).toBeVisible();
await expect(set.getByLabel("Context window", { exact: true })).toHaveAttribute("aria-invalid", "true");
await expect(set.getByLabel("Context window", { exact: true })).toHaveAccessibleDescription(/Set a context window at least this large/);
await expect(set.getByLabel("Max output tokens", { exact: true })).not.toHaveAttribute("aria-invalid", "true");
await set.getByLabel("Context window", { exact: true }).fill("1000");
await expect(set.getByLabel("Context window", { exact: true })).toHaveAttribute("aria-invalid", "true");
await set.getByLabel("Context window").fill("2147483648");
await expect(set.getByText("Enter a whole number up to 2147483647.")).toBeVisible();
await expect(set.getByRole("button", { name: "Save" })).toBeDisabled();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {
EnvironmentTemplateSkill,
} from "@oac/agents-client";

import { PageBody, PageHeader, RefreshButton, Section, StatusDot } from "../../components/console-ui";
import { HelpTip, PageBody, PageHeader, RefreshButton, Section, StatusDot } from "../../components/console-ui";
import { formatBytes, formatDateTime, MISSING } from "../../lib/format";
import { CopyableId, CopyIdButton } from "../../components/list-ui";
import { templateName } from "./template-name";
Expand Down Expand Up @@ -169,12 +169,12 @@ export function TemplateDetailPage({
<div><dt>{t("detail.updated")}</dt><dd>{formatDateTime(template.updated_at, locale)}</dd></div>
<div>
<dt>{t("detail.network")}</dt>
<dd>{network ? t(`access.${network.access}`) : <StatusDot tone="warning" label={t("unrecognizedValue")} />}</dd>
<dd>{network ? <>{t(`access.${network.access}`)}{network.access !== "enabled" ? <HelpTip>{t("accessHelp")}</HelpTip> : null}</> : <StatusDot tone="warning" label={t("unrecognizedValue")} />}</dd>
</div>
</dl>

{network?.access === "restricted" ? (
<Section headingId="template-domains-heading" title={<>{t("detail.allowedDomains")} <Count value={network.allowed_domains.length} /></>} help={t("accessHelp.restricted")}>
<Section headingId="template-domains-heading" title={<>{t("detail.allowedDomains")} <Count value={network.allowed_domains.length} /></>}>
<Values values={network.allowed_domains} />
</Section>
) : null}
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/features/system/ModelProviderDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ export function ModelProviderDialog({ harness, onClose, onSaved, onReread }: {
const output = tokenLimit(maxOutputTokens);
const limitProblem = (limit: ReturnType<typeof tokenLimit>) => (limit.problem === "whole" ? t("models.form.wholeNumber") : limit.problem === "large" ? t("models.form.tooLarge") : null);
const outputProblem = limitProblem(output);
// The missing or undersized window is the field the administrator must fix.
const contextProblem = limitProblem(context) ?? (!outputProblem && output.value !== undefined && output.value > (context.value ?? 0) ? t("models.form.needsContext") : null);
// An undersized window is the field the administrator must fix.
const contextProblem = limitProblem(context) ?? (!outputProblem && output.value !== undefined && context.value !== undefined && output.value > context.value ? t("models.form.needsContext") : null);
const ready = harness !== null && !busy && url !== "" && !urlProblem && apiKey.trim() !== "" && model.trim() !== "" && nativeConfig !== null && !configTooLarge && !protocolProblem && !contextProblem && !outputProblem;

function clearNativeConfig() {
Expand Down
4 changes: 1 addition & 3 deletions apps/web/src/i18n/locales/en/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ export const templates = {
plugins: "Plugins", updated: "Updated", actions: "Actions",
},
access: { enabled: "Enabled", disabled: "Disabled", restricted: "Restricted" },
accessHelp: {
restricted: "Sessions with this policy are rejected, because the Runtime does not restrict network access.",
},
accessHelp: "Sessions with this policy are rejected, because the Runtime does not restrict network access.",
edit: "Edit",
delete: "Delete",
deleteLabel: "Delete {{name}}",
Expand Down
4 changes: 1 addition & 3 deletions apps/web/src/i18n/locales/zh-CN/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ export const templates: TranslationShape<typeof english> = {
emptyTitle: "还没有 Environment 模板", noMatch: "没有匹配的模板", clearFilter: "清除筛选", listLabel: "Environment 模板",
columns: { name: "名称", network: "网络", packages: "依赖包", files: "文件", skills: "Skill", plugins: "Plugin", updated: "更新时间", actions: "操作" },
access: { enabled: "已启用", disabled: "已禁用", restricted: "受限" },
accessHelp: {
restricted: "使用此策略的 Session 会被拒绝,因为 Runtime 不限制网络访问。",
},
accessHelp: "使用此策略的 Session 会被拒绝,因为 Runtime 不限制网络访问。",
edit: "编辑", delete: "删除", deleteLabel: "删除 {{name}}", open: "打开 {{name}}", back: "返回",
unrecognized: "含无法识别的配置",
unrecognizedHelp: "Core 返回了控制台无法识别的配置。可识别的部分照常显示,其余部分不做推测。",
Expand Down
2 changes: 1 addition & 1 deletion contracts/agents-api/core-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Each code returns HTTP 400 with `type: "invalid_request_error"`. A missing, malf
| `model_provider_base_url_invalid` | `base_url` | omitted | Requires HTTPS without credentials, query or fragment |
| `model_provider_protocol_unsupported` | `protocol` | `harness` and `allowed_protocols`, from the build's adapter catalog | The protocol is unknown or unsupported by the selected Harness |
| `model_provider_api_key_invalid` | `api_key` | `max_length`: 16384 | The key is empty, too long or contains a prohibited character |
| `model_provider_token_limits_invalid` | `context_window` or `max_output_tokens` | omitted | A limit is negative, or `max_output_tokens` exceeds `context_window` (0 when omitted) |
| `model_provider_token_limits_invalid` | `context_window` or `max_output_tokens` | omitted | A limit is negative, or `max_output_tokens` exceeds a set `context_window` |
| `model_configuration_model_invalid` | `model` | omitted | The deployment default's model is not a nonempty model identifier |
| `harness_config_invalid` | `harness_config` | omitted | The deployment default's native parameters are unsupported or invalid |
| `invalid_sandbox_configuration` | `resources.cpus` | `min`: 1, `max`: 255 | The CPU count is outside the supported bounds |
Expand Down
2 changes: 1 addition & 1 deletion contracts/agents-api/v1/model_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func (p *ModelProviderInput) validate(registry harnessconfig.Registry) error {
if strings.TrimSpace(p.APIKey) == "" || len(p.APIKey) > 16384 || strings.ContainsAny(p.APIKey, "\x00\r\n") {
return &ModelProviderError{Code: "model_provider_api_key_invalid", Param: "api_key", message: "invalid model provider API key"}
}
if p.ContextWindow < 0 || p.MaxOutputTokens < 0 || (p.MaxOutputTokens > p.ContextWindow) {
if p.ContextWindow < 0 || p.MaxOutputTokens < 0 || (p.ContextWindow > 0 && p.MaxOutputTokens > p.ContextWindow) {
param := "max_output_tokens"
if p.ContextWindow < 0 {
param = "context_window"
Expand Down
7 changes: 6 additions & 1 deletion contracts/agents-api/v1/model_provider_error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func TestModelProviderErrorMessagesAndPrecedence(t *testing.T) {
{"protocol before key", "codex", "model_provider_protocol_unsupported", "protocol", "unsupported model provider protocol", func(p *ModelProviderInput) { p.Protocol = "private"; p.APIKey = "" }},
{"key", "claude_sdk", "model_provider_api_key_invalid", "api_key", "invalid model provider API key", func(p *ModelProviderInput) { p.Protocol = "anthropic"; p.APIKey = "" }},
{"context before output", "codex", "model_provider_token_limits_invalid", "context_window", "invalid model token limits", func(p *ModelProviderInput) { p.ContextWindow = -1; p.MaxOutputTokens = -2 }},
{"output", "codex", "model_provider_token_limits_invalid", "max_output_tokens", "invalid model token limits", func(p *ModelProviderInput) { p.MaxOutputTokens = 1 }},
{"output", "codex", "model_provider_token_limits_invalid", "max_output_tokens", "invalid model token limits", func(p *ModelProviderInput) { p.ContextWindow = 1; p.MaxOutputTokens = 2 }},
} {
t.Run(tc.name, func(t *testing.T) {
p := valid
Expand All @@ -36,6 +36,11 @@ func TestModelProviderErrorMessagesAndPrecedence(t *testing.T) {
if err := missing.Validate(); !errors.As(err, &field) || field.Code != "invalid_model_provider" || field.Param != "" || err.Error() != "model_provider is required" {
t.Fatal(err)
}
outputOnly := valid
outputOnly.MaxOutputTokens = 32000
if err := outputOnly.ValidateHarness("codex"); err != nil {
t.Fatal(err) // An omitted context window keeps the native default.
}
if err := valid.ValidateHarness("codex"); err != nil {
t.Fatal(err)
}
Expand Down
4 changes: 2 additions & 2 deletions contracts/agents-api/zh/core-errors.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "Core 管理错误"
source: contracts/agents-api/core-errors.md
source_hash: 73b7865736df763df48d1b419594feec04ef1c05b57e3d8e0c176e54ab336b7c
source_hash: 8799a6f1ba80217f77e7adac3412e60482be731fcea665dfb9ec19fad92a8d54
---

`/core/v1` 上的错误使用此封装结构。`message` 是安全的英文文本;`code` 和 `param` 可以为 null。客户端依据稳定的 `code` 和可选的 `param` 进行处理,对未知代码显示 `message`,绝不解析消息,也绝不自动重试被拒绝的写操作。
Expand Down Expand Up @@ -63,7 +63,7 @@ Web 的控制台服务器在 `/core` 路径上发生自身故障时使用此封
| `model_provider_base_url_invalid` | `base_url` | 省略 | 必须使用 HTTPS,且不得包含凭据、查询或片段 |
| `model_provider_protocol_unsupported` | `protocol` | `harness` 和 `allowed_protocols`,来自该构建的适配器目录 | 协议未知,或所选 Harness 不支持该协议 |
| `model_provider_api_key_invalid` | `api_key` | `max_length`:16384 | 密钥为空、过长或包含禁止字符 |
| `model_provider_token_limits_invalid` | `context_window` 或 `max_output_tokens` | 省略 | 限制为负数,或 `max_output_tokens` 超过 `context_window`(省略时为 0) |
| `model_provider_token_limits_invalid` | `context_window` 或 `max_output_tokens` | 省略 | 限制为负数,或 `max_output_tokens` 超过已设置的 `context_window` |
| `model_configuration_model_invalid` | `model` | 省略 | 部署默认配置的 model 不是非空模型标识符 |
| `harness_config_invalid` | `harness_config` | 省略 | 部署默认配置的原生参数不受支持或无效 |
| `invalid_sandbox_configuration` | `resources.cpus` | `min`:1,`max`:255 | CPU 数量超出支持范围 |
Expand Down
2 changes: 1 addition & 1 deletion internal/modelprovider/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func (p Provider) Validate() error {
if strings.TrimSpace(p.APIKey) == "" || len(p.APIKey) > 16384 || strings.ContainsAny(p.APIKey, "\x00\r\n") {
return ErrConfiguration
}
if p.ContextWindow < 0 || p.MaxOutputTokens < 0 || p.MaxOutputTokens > p.ContextWindow {
if p.ContextWindow < 0 || p.MaxOutputTokens < 0 || (p.ContextWindow > 0 && p.MaxOutputTokens > p.ContextWindow) {
return ErrConfiguration
}
return nil
Expand Down
Loading