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 CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ The role needs `CREATE DATABASE`: tests of database-wide state, such as the exec

### Contract and schema rules

- `internal/harnessconfig/builtin/catalog.json` is the single authored public Harness registration list. `make generate-harness-catalog` generates Go configuration/profile registration, client identifiers/names and the reference; `make openapi` derives the matching enums. `make check-harness-catalog` verifies freshness in the full gate. Native configuration rules stay in their adapter declarations; Core qualification and Runtime availability stay separate.
- `internal/harnessconfig/builtin/catalog.json` is the single authored public Harness registration list. `make generate-harness-catalog` generates Go configuration/profile registration, client identifiers/names and the reference, and projects the model-provider protocol names of `internal/modelprovider/config.go` to the client; `make openapi` derives the matching enums. `make check-harness-catalog` verifies freshness in the full gate. Native configuration rules stay in their adapter declarations; Core qualification and Runtime availability stay separate.
- `make sqlc-generate` owns only `services/core/internal/db/sqlc` (sqlc v1.29.0). Do not rewrite landed migrations.
- `make check-runtime-contract` is the focused Core–Runtime contract entry point; see [Contract verification](docs/runtime-protocol.md#contract-verification). It also runs through `check-go` and `check-core`.

Expand Down
9 changes: 5 additions & 4 deletions apps/web/src/features/sandbox/SandboxSetupWizard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AgentCoreError, type InitializeSandboxDeployment, type UpdateSandboxDeployment, type SandboxE2BReadyBuild, type SandboxE2BTemplate, type SandboxProvider, type SandboxResources, type SandboxRuntimeRelease, type SandboxSpecification } from "@oac/agents-client";
import { AgentCoreError, deploymentContract, type InitializeSandboxDeployment, type UpdateSandboxDeployment, type SandboxE2BReadyBuild, type SandboxE2BTemplate, type SandboxProvider, type SandboxResources, type SandboxRuntimeRelease, type SandboxSpecification } from "@oac/agents-client";
import { useQuery } from "@tanstack/react-query";
import { AnimatePresence } from "motion/react";
import * as m from "motion/react-m";
Expand All @@ -15,9 +15,8 @@ import { formatBytes } from "../../lib/format";
import { installationQuery } from "../../lib/installation";
import type { MessageKey } from "../../lib/locale-strings";
import { sandboxConfigurationRejection } from "../../lib/sandbox-labels";
import { defaultSandboxResources, distributionRuntime, savedSpecification, validSandboxResources } from "./deployment-specification";
import { defaultSandboxResources, distributionRuntime, isRuntimeRelease, isRuntimeReleaseField, RUNTIME_RELEASE_FIELDS, savedSpecification, validSandboxResources } from "./deployment-specification";
import { e2bKeyReady, e2bUpdateSelection } from "./sandbox-update";
import { isRuntimeRelease, isRuntimeReleaseField, RUNTIME_RELEASE_FIELDS } from "./runtime-release";
import { sandboxAdmin } from "./sandbox-queries";
import "./sandbox-wizard.css";

Expand All @@ -41,6 +40,8 @@ function e2bService(apiURL?: string): E2BService {
}

const MIB = 2 ** 20;
const bounds = Object.fromEntries(deploymentContract.resources.map(({ name, min, max }) => [name, `${min}–${max}`]));
const resourceBounds = { cpus: bounds.cpus, memory: bounds.memory_mib, disk: deploymentContract.minimum_disk };
const EASE = [0.16, 1, 0.3, 1] as const;
// Core accepts a template ID of up to 128 characters and a canonical, non-nil build UUID.
const TEMPLATE = /^[a-zA-Z0-9_-]{1,128}:([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/;
Expand Down Expand Up @@ -413,7 +414,7 @@ export function SandboxSetupWizard({ coreUrl, expectedGeneration, current, disab
<Question title={t("Advanced settings")}>
<form className="wizard-fields" onSubmit={(event) => { event.preventDefault(); setStep("review"); }}>
{sized ? <fieldset className="wizard-group">
<legend>{t("Each sandbox")}<HelpTip>{t("1–255 CPUs, 512–1048576 MiB of memory. microsandbox disks are at least 1024 MiB.")}</HelpTip></legend>
<legend>{t("Each sandbox")}<HelpTip>{t("{{cpus}} CPUs, {{memory}} MiB of memory. microsandbox disks are at least {{disk}} MiB.", resourceBounds)}</HelpTip></legend>
<div className="wizard-grid">
<NumberField error={fieldError("resources.cpus")} id={`${id}-cpus`} label={t("CPUs")} value={resources.cpus} onChange={(cpus) => { setSize("custom"); setResources({ ...resources, cpus }); setFieldRejection(null); }} />
<NumberField error={fieldError("resources.memory_mib")} id={`${id}-memory`} label={t("Memory (MiB)")} value={resources.memory_mib} onChange={(memory_mib) => { setSize("custom"); setResources({ ...resources, memory_mib }); setFieldRejection(null); }} />
Expand Down
16 changes: 7 additions & 9 deletions apps/web/src/features/sandbox/deployment-specification.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { defaultSandboxResources, distributionRuntime, sandboxesThatFit, savedSpecification, validSandboxResources } from "./deployment-specification";
import { isRuntimeReleaseField } from "./runtime-release";
import { defaultSandboxResources, distributionRuntime, isRuntimeReleaseField, sandboxesThatFit, savedSpecification, validSandboxResources } from "./deployment-specification";
import standardSizes from "./standard-sizes.json";
import type { SandboxSpecification } from "@oac/agents-client";

Expand Down Expand Up @@ -49,14 +48,13 @@ describe("deployment resources and Runtime", () => {
expect(validSandboxResources("e2b", { cpus: 2, memory_mib: 2048, root_disk_mib: 1024 })).toBe(false);
expect(validSandboxResources("microsandbox", { cpus: 2, memory_mib: 2048, root_disk_mib: 1023, environment_disk_mib: 8192 })).toBe(false);
});
it("accepts any well-formed Runtime image name in the Runtime reference", async () => {
it("accepts only Core's Runtime image in the Runtime reference", async () => {
const digest = "b".repeat(64);
for (const runtime_ref of [`oac-runtime@sha256:${digest}`, `custom-runtime@sha256:${digest}`]) {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response(JSON.stringify({ ...manifest, runtime_ref }))));
expect((await distributionRuntime(new AbortController().signal)).microsandbox_ref).toBe(runtime_ref);
expect(isRuntimeReleaseField("microsandbox_ref", runtime_ref)).toBe(true);
}
for (const runtime_ref of [`oac-runtime:${digest}`, `oac-runtime@sha256:${"b".repeat(63)}`, `oac-runtime@sha256:${"B".repeat(64)}`, `@sha256:${digest}`]) {
const runtime_ref = `oac-runtime@sha256:${digest}`;
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response(JSON.stringify({ ...manifest, runtime_ref }))));
expect((await distributionRuntime(new AbortController().signal)).microsandbox_ref).toBe(runtime_ref);
expect(isRuntimeReleaseField("microsandbox_ref", runtime_ref)).toBe(true);
for (const runtime_ref of [`custom-runtime@sha256:${digest}`, `oac-runtime:${digest}`, `oac-runtime@sha256:${"b".repeat(63)}`, `oac-runtime@sha256:${"B".repeat(64)}`, `@sha256:${digest}`]) {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response(JSON.stringify({ ...manifest, runtime_ref }))));
await expect(distributionRuntime(new AbortController().signal)).rejects.toThrow();
expect(isRuntimeReleaseField("microsandbox_ref", runtime_ref)).toBe(false);
Expand Down
38 changes: 23 additions & 15 deletions apps/web/src/features/sandbox/deployment-specification.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { SandboxDeployment, SandboxE2BTemplateBuild, SandboxProvider, SandboxResources, SandboxRuntimeRelease, SandboxSpecification } from "@oac/agents-client";
import { RUNTIME_REF_PATTERN } from "./runtime-release";
import { deploymentContract, type SandboxDeployment, type SandboxE2BTemplateBuild, type SandboxProvider, type SandboxResources, type SandboxRuntimeRelease, type SandboxSpecification } from "@oac/agents-client";
import standardSizes from "./standard-sizes.json";

interface Manifest {
Expand All @@ -15,12 +14,25 @@ export function defaultSandboxResources(provider: SandboxProvider): SandboxResou
return { ...(provider === "microsandbox" ? standardSizes.microsandbox : standardSizes.docker) };
}

/** Core's resource rule: optional disk fields stay zero unless the provider supports disk limits. */
export function validSandboxResources(provider: SandboxProvider, resources: SandboxResources): boolean {
const bounded = (value: number | undefined, minimum: number, maximum: number) => Number.isInteger(value) && value! >= minimum && value! <= maximum;
if (!bounded(resources.cpus, 1, 255) || !bounded(resources.memory_mib, 512, 1048576)) return false;
return provider === "microsandbox"
? bounded(resources.root_disk_mib, 1024, 4294967295) && bounded(resources.environment_disk_mib, 1024, 4294967295)
: (resources.root_disk_mib ?? 0) === 0 && (resources.environment_disk_mib ?? 0) === 0;
return deploymentContract.resources.every((rule) => {
const [min, max] = !rule.omit_zero ? [rule.min, rule.max] : provider === "microsandbox" ? [deploymentContract.minimum_disk, rule.max] : [0, 0];
const value = resources[rule.name] ?? 0;
return Number.isInteger(value) && value >= min && value <= max;
});
}

const releasePatterns = Object.fromEntries(deploymentContract.runtime.map(({ name, pattern }) => [name, new RegExp(`^(?:${pattern})$`)])) as Record<keyof SandboxRuntimeRelease, RegExp>;

export const RUNTIME_RELEASE_FIELDS = deploymentContract.runtime.map(({ name }) => name);

export function isRuntimeReleaseField(field: keyof SandboxRuntimeRelease, value: string): boolean {
return releasePatterns[field].test(value);
}

export function isRuntimeRelease(value: Partial<SandboxRuntimeRelease>): value is SandboxRuntimeRelease {
return RUNTIME_RELEASE_FIELDS.every((field) => typeof value[field] === "string" && releasePatterns[field].test(value[field]));
}

export function savedSpecification(provider: SandboxProvider, savedProvider?: SandboxProvider | "", specification?: SandboxSpecification): SandboxSpecification | null {
Expand Down Expand Up @@ -59,12 +71,8 @@ export async function distributionRuntime(signal: AbortSignal): Promise<SandboxR
const response = await fetch("/node-install/manifest.json", { signal, credentials: "include", redirect: "error" });
if (!response.ok) throw new Error("distribution unavailable");
const manifest = await response.json() as Manifest;
const hash = /^[a-f0-9]{64}$/;
const image = /^sha256:[a-f0-9]{64}$/;
if (manifest.platform !== "linux/amd64" || !/^[a-f0-9]{40}$/.test(manifest.source_commit ?? "")
|| !image.test(manifest.images?.runtime ?? "") || !image.test(manifest.image_manifest_digests?.runtime ?? "")
|| !RUNTIME_REF_PATTERN.test(manifest.runtime_ref ?? "")
|| !hash.test(manifest.microsandbox?.runtime_sha256 ?? "") || !hash.test(manifest.microsandbox?.firmware_sha256 ?? "")) throw new Error("invalid distribution");
return { source_commit: manifest.source_commit!, image_id: manifest.images!.runtime!, image_manifest_digest: manifest.image_manifest_digests!.runtime!,
microsandbox_ref: manifest.runtime_ref!, runtime_sha256: manifest.microsandbox!.runtime_sha256!, firmware_sha256: manifest.microsandbox!.firmware_sha256! };
const release = { source_commit: manifest.source_commit, image_id: manifest.images?.runtime, image_manifest_digest: manifest.image_manifest_digests?.runtime,
microsandbox_ref: manifest.runtime_ref, runtime_sha256: manifest.microsandbox?.runtime_sha256, firmware_sha256: manifest.microsandbox?.firmware_sha256 };
if (manifest.platform !== "linux/amd64" || !isRuntimeRelease(release)) throw new Error("invalid distribution");
return release;
}
23 changes: 0 additions & 23 deletions apps/web/src/features/sandbox/runtime-release.ts

This file was deleted.

3 changes: 2 additions & 1 deletion apps/web/src/i18n/locales/en/common.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { coreErrors } from "./core-errors";
import { coreErrorDetails, coreErrors } from "./core-errors";

export const common = {
coreErrors,
coreErrorDetails,
readFailure: {
title: "Could not read the data",
partial: "Some reads failed. Any figures and rows shown cover only data already read; they may be incomplete or out of date.",
Expand Down
34 changes: 24 additions & 10 deletions apps/web/src/i18n/locales/en/core-errors.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,20 @@
/** One message for each code in Core's shared catalog, services/core/internal/api/testdata/core-errors.json. */
export const coreErrors = {
"invalid_admin_key": "The console's Core key was rejected. Rotate it on the Core host, then sign in again.",
"console_sign_in_required": "Sign in to the console again.",
"console_origin_rejected": "Open the console at its configured address.",
"console_request_invalid": "The console request was rejected. Reload the page.",
"core_unreachable": "Core is unreachable. Check the Core service, then refresh.",
"invalid_name": "Enter a nonempty name without control characters.",
"nameLimit": "Enter a name of at most {{max}} characters without control characters.",
"nodeNameLimit": "Enter a name of at most {{max}} UTF-8 bytes.",
"invalid_node_capacity": "Enter a valid whole-number capacity; retained capacity must be at least active capacity.",
"capacityRange": "Enter a whole number from {{min}} to {{max}}; retained capacity must be at least active capacity.",
"model_configuration_model_invalid": "Enter a model ID of at most 1024 UTF-8 bytes without control characters.",
"harness_config_invalid": "Check the supported native fields and their values. The JSON object must be at most 16 KiB and cannot redefine Core-managed settings.",
"invalid_model_provider": "Enter the complete model provider configuration.",
"model_provider_base_url_invalid": "Use an HTTPS URL without credentials, query parameters or a fragment.",
"model_provider_protocol_unsupported": "This protocol is not supported by the harness.",
"protocols": "Allowed protocols: {{protocols}}.",
"model_provider_api_key_invalid": "Enter a valid API key without control characters.",
"keyLimit": "Enter a valid API key of at most {{max}} characters.",
"model_provider_token_limits_invalid": "Use valid token limits; output cannot exceed context, and required limits must be positive.",
"invalid_sandbox_configuration": "Check the sandbox resources and Runtime release.",
"resourceRange": "Enter a whole number from {{min}} to {{max}}.",
"resourceMin": "Enter a whole number of at least {{min}}.",
"runtime": "Use a valid immutable Runtime release for this backend.",
"project_archived": "This Project is archived.",
"project_exists": "A Project with this name already exists.",
"project_api_key_exists": "An active API key with this name already exists.",
Expand All @@ -35,10 +28,31 @@ export const coreErrors = {
"sandbox_in_use": "Hosted resources remain. Wait for confirmed cleanup before changing the configuration.",
"runtime_node_in_use": "The node has active allocations or retained resources. Clear allocations, snapshots, reservations and pending cleanup before removal.",
"runtime_node_unavailable": "The selected sandbox node is unavailable or has no capacity.",
"sandbox_admin_not_configured": "Sandbox administration is not configured on this console.",
"sandbox_credential_ownership": "This E2B key cannot manage the retained deployment. Reset before changing teams.",
"sandbox_credential_invalid": "The E2B API key was rejected. The saved configuration is unchanged.",
"sandbox_configuration_invalid": "Select a ready immutable E2B template build with matching resources.",
"sandbox_verification_unconfirmed": "E2B verification could not be confirmed. Refresh before submitting again.",
"sandbox_configuration_error": "E2B sandboxes need a public HTTPS address. Set OAC_PUBLIC_URL to an HTTPS origin."
"sandbox_configuration_error": "E2B sandboxes need a public HTTPS address. Set OAC_PUBLIC_URL to an HTTPS origin.",
"sandbox_deployment_conflict": "The sandbox deployment cannot change in its current state. Refresh and check its reset and resource state.",
"sandbox_specification_mismatch": "The saved sandbox specification does not match the deployment. Refresh to check the configuration.",
"sandbox_operation_unsupported": "The selected sandbox provider does not support this operation.",
"environment_unavailable": "The Session's environment is no longer available.",
"execution_unavailable": "Execution is not available on this Core.",
"runtime_history_unavailable": "Runtime history is unavailable on this Core.",
"runtime_history_unsupported": "Runtime history is not supported for this Session.",
"core_metrics_unavailable": "Core metrics could not be read. Try again later.",
"file_transfer_unavailable": "The file transfer is unavailable. Try again later.",
"not_found": "The requested Core resource does not exist."
} as const;

/** Messages that format a catalogued code's typed details. */
export const coreErrorDetails = {
"nameLimit": "Enter a name of at most {{max}} characters without control characters.",
"nodeNameLimit": "Enter a name of at most {{max}} UTF-8 bytes.",
"capacityRange": "Enter a whole number from {{min}} to {{max}}; retained capacity must be at least active capacity.",
"protocols": "Allowed protocols: {{protocols}}.",
"keyLimit": "Enter a valid API key of at most {{max}} characters.",
"resourceRange": "Enter a whole number from {{min}} to {{max}}.",
"resourceMin": "Enter a whole number of at least {{min}}.",
"runtime": "Use a valid immutable Runtime release for this backend."
} as const;
3 changes: 2 additions & 1 deletion apps/web/src/i18n/locales/zh-CN/common.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { coreErrors } from "./core-errors";
import { coreErrorDetails, coreErrors } from "./core-errors";

export const common = {
coreErrors,
coreErrorDetails,
readFailure: {
title: "无法读取数据",
partial: "部分读取失败。当前数字和列表仅来自已读取的数据,可能不完整或已过期。",
Expand Down
Loading
Loading