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
3 changes: 2 additions & 1 deletion .github/workflows/api-acceptance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ jobs:
OAC_TEST_OFFICIAL_SDK_PYTHON: python
run: |
python services/core/tests/official_schema_test.py
python services/core/tests/official_client.py
# The Go Workers need an unclaimed deployment; Core claims it for its installation ID.
go test ./services/core/tests/integration -run '^(TestFunctionStateOfficialClientReadsAndLiveEvents|TestSavedReferenceRetryOfficialClient|TestAgentUpdateOfficialClient|TestAgentDeletionOfficialClient|TestSessionAgentFilterOfficialClient|TestSessionDeletionOfficialClient|TestEnvironmentInitialFailureOfficialClient|TestSelfHostedInitialCreationOfficialClient|TestSelfHostedCancellationOfficialClient)$' -count=1
python services/core/tests/official_client.py
- uses: ./.github/actions/e2b-provider
if: inputs.container
- name: Verify the distribution's Core image
Expand Down
8 changes: 3 additions & 5 deletions apps/web/e2e/console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,15 @@ export const FIXTURE_CORE_KEY = "fixture-core-key-3f9a2c71";
* `sandbox` the sandbox deployment, `nodes: "none"` a deployment no node has joined, and
* `installation` how config.json's public_url is set: "public" (HTTPS, the default), "local"
* (loopback: only the Core machine reaches the API, and E2B is rejected) or "stale" (public,
* with a node enrolled with an earlier address), `credentials: "none"` a Core without a
* credential encryption key, which cannot store a model provider's key, and
* `installers: "none"` a console without its node installation payload, so it serves neither
* with a node enrolled with an earlier address), and `installers: "none"` a console without its node installation payload, so it serves neither
* the node nor the self-hosted installer. `nodeArtifacts` lists the providers the console has
* node files for, both by default; as in the console, microsandbox needs Docker's files too.
*/
export interface FixtureOptions { fresh?: boolean; sandbox?: "configured" | "none" | "e2b"; nodes?: "none"; installation?: "public" | "local" | "stale"; credentials?: "none"; installers?: "none"; nodeArtifacts?: ("docker" | "microsandbox")[] }
export interface FixtureOptions { fresh?: boolean; sandbox?: "configured" | "none" | "e2b"; nodes?: "none"; installation?: "public" | "local" | "stale"; installers?: "none"; nodeArtifacts?: ("docker" | "microsandbox")[] }

/** Fresh fixture state: signed out ("login") or already signed in ("authenticated"). */
export async function resetFixture(request: APIRequestContext, auth: "login" | "authenticated" = "authenticated", options: FixtureOptions = {}) {
await request.post(`${fixture}/__fixture/reset?auth=${auth}${options.fresh ? "&projects=none" : ""}&sandbox=${options.sandbox ?? "configured"}${options.nodes ? `&nodes=${options.nodes}` : ""}&installation=${options.installation ?? "public"}${options.credentials ? `&credentials=${options.credentials}` : ""}${options.installers ? `&installers=${options.installers}` : ""}${options.nodeArtifacts ? `&artifacts=${options.nodeArtifacts.join(",")}` : ""}`);
await request.post(`${fixture}/__fixture/reset?auth=${auth}${options.fresh ? "&projects=none" : ""}&sandbox=${options.sandbox ?? "configured"}${options.nodes ? `&nodes=${options.nodes}` : ""}&installation=${options.installation ?? "public"}${options.installers ? `&installers=${options.installers}` : ""}${options.nodeArtifacts ? `&artifacts=${options.nodeArtifacts.join(",")}` : ""}`);
}

const v1Requests: string[] = [];
Expand Down
8 changes: 2 additions & 6 deletions apps/web/e2e/fixture-console.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ function e2bDeployment() {
return { ...configuredDeployment(), provider: "e2b", mode: "direct", rollout: noNodeRollout(), resources: { allocations: 3, pending: 1 }, specification: { resources: { cpus: 2, memory_mib: 2048 } }, configuration: { template: "oac-runtime:0f1e2d3c-4b5a-6978-8a9b-0c1d2e3f4a5b", api_url: "https://api.e2b.app", domain: "e2b.app" } , credential_configured: true, metadata: { template_build: templateBuild } };
}

function reset(mode = "login", fresh = false, sandbox = "configured", nodes = "demo", address = "public", credentials = "configured", installers = true, artifacts = "docker,microsandbox") {
function reset(mode = "login", fresh = false, sandbox = "configured", nodes = "demo", address = "public", installers = true, artifacts = "docker,microsandbox") {
// Self-hosted Sessions get their remote_url from public_url, as in Core.
const screenshots = process.env.OAC_WEB_SCREENSHOT_DEMO === "1";
const now = Math.floor(Date.now() / 1000);
Expand All @@ -112,8 +112,6 @@ function reset(mode = "login", fresh = false, sandbox = "configured", nodes = "d
executorCredentials: new Map(),
// How config.json's public_url is set: "public", "local" or "stale".
installation: address,
// "none": Core has no credential encryption key, so it cannot store a provider's key.
credentialKey: credentials !== "none",
// Startup state and deployment default model provider per harness; API keys are never kept.
// The demo deployment's default harness has a default model; a fresh install has none.
harnesses: {
Expand Down Expand Up @@ -574,8 +572,6 @@ async function harnessRoute(request, response, path) {
if (!support(harness).protocols.includes(input.model_provider?.protocol)) return error(response, 400, "This harness does not support this protocol.", "model_provider_protocol_unsupported");
const problem = providerProblem(harness, input.model_provider ?? {});
if (problem) return error(response, 400, problem, "invalid_request_error");
// As Core's error mapping: sealing the key needs the credential encryption key.
if (!state.credentialKey) return error(response, 503, "Credential encryption is not configured on this service.", "credential_storage_unavailable");
entry.provider = {
object: "core.model_configuration", harness, model: input.model, harness_config: input.harness_config ?? {},
model_provider: { protocol: input.model_provider.protocol, base_url: input.model_provider.base_url, api_key_configured: true,
Expand All @@ -596,7 +592,7 @@ async function fixtureRoute(request, response, url) {
}
if (url.pathname === "/__fixture/health") return send(response, 200, { ok: true });
if (url.pathname === "/__fixture/reset" && request.method === "POST") {
reset(url.searchParams.get("auth") ?? "login", url.searchParams.get("projects") === "none", url.searchParams.get("sandbox") ?? "configured", url.searchParams.get("nodes") ?? "demo", url.searchParams.get("installation") ?? "public", url.searchParams.get("credentials") ?? "configured", url.searchParams.get("installers") !== "none", url.searchParams.get("artifacts") ?? undefined);
reset(url.searchParams.get("auth") ?? "login", url.searchParams.get("projects") === "none", url.searchParams.get("sandbox") ?? "configured", url.searchParams.get("nodes") ?? "demo", url.searchParams.get("installation") ?? "public", url.searchParams.get("installers") !== "none", url.searchParams.get("artifacts") ?? undefined);
return send(response, 200, { ok: true });
}
if (url.pathname === "/__fixture/deployment" && request.method === "POST") {
Expand Down
19 changes: 0 additions & 19 deletions apps/web/e2e/system.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,25 +129,6 @@ test("sets, replaces and clears a harness's default model configuration, and kee
expect(browserState).not.toContain(KEY);
});

test("reports a Core without a credential key as a configuration error, without rereading", async ({ page, request }) => {
await openConsole(page, request, "system", { fresh: true, credentials: "none" });
const codex = page.getByRole("region", { name: "Default model configuration" }).getByRole("article", { name: "Codex" });
await codex.getByRole("button", { name: "Set the default model configuration for Codex" }).click();
const set = page.getByRole("dialog", { name: "Set default model configuration for Codex" });
await set.getByLabel("Base URL").fill("https://model.example/v1");
await set.getByLabel("API key").fill(KEY);
await set.getByLabel("Default model ID").fill("fixture-model");
const reads: string[] = [];
page.on("request", (sent) => { if (sent.method() === "GET" && new URL(sent.url()).pathname === "/core/v1/harnesses") reads.push(sent.url()); });
await set.getByRole("button", { name: "Save" }).click();
await expect(set.getByRole("alert")).toHaveText("Core has no credential encryption key configured, so it can't store keys. Installer-based installs configure this automatically; for manual deployments, set OAC_CREDENTIAL_KEY_FILE for Core.");
await expect(set.getByRole("button", { name: "Save" })).toBeEnabled();
expect(reads).toEqual([]);
expect(await writes(request)).toEqual(["PUT /core/v1/harnesses/codex/model-configuration"]);
await set.getByRole("button", { name: "Cancel" }).click();
await expect(codex).toContainText("Not set");
});

test("reports an unconfirmed save, reads the default model configurations again once and never repeats the write", async ({ page, request }) => {
await openConsole(page, request, "system", { fresh: true });
const codex = page.getByRole("region", { name: "Default model configuration" }).getByRole("article", { name: "Codex" });
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/InstallationNotice.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest";
import { InstallationNotice } from "./InstallationNotice";

const installation: CoreInstallation = {
object: "core.installation", installation_id: null, public_url: "http://127.0.0.1:8091", api_base_url: "http://127.0.0.1:8091/v1",
object: "core.installation", installation_id: "94be54a1-138c-4f30-bc87-b13686272dbe", public_url: "http://127.0.0.1:8091", api_base_url: "http://127.0.0.1:8091/v1",
source_commit: null, local_only: true, configuration: { settings: [] },
address_bindings: { nodes: 0, nodes_on_other_address: 0, hosted_sandboxes: 0, self_hosted_executors: 0 },
};
Expand Down
7 changes: 2 additions & 5 deletions apps/web/src/features/api-keys/HowToCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,23 +64,20 @@ export function callSamples(apiBaseUrl: string, apiKey: string | null, keyPlaceh

/**
* The samples, or why there are none. The console never sends these requests.
* When Core is reachable only on its own machine it says so, and without a
* public address it says to set one instead of guessing.
* When Core is reachable only on its own machine it says so.
*/
function HowToCallBody({ apiKey }: { apiKey: string | null }) {
const { t } = useTranslation("keys");
const { t: tCommon } = useTranslation("common");
const installation = useQuery(installationQuery);
const base = installation.data?.api_base_url ?? null;

if (installation.data === undefined) {
return installation.isError && !installation.isFetching
? <p className="how-to-call-note" role="alert">{t("howToCall.failed")} <button className="text-action" type="button" onClick={() => void installation.refetch()}>{tCommon("actions.retry")}</button></p>
// The first sample's place, as the console's other first reads hold theirs.
: <div className="how-to-call-sample how-to-call-skeleton" role="status" aria-label={t("howToCall.loading")} aria-busy="true"><span className="skeleton-bar" /><span className="skeleton-bar" /></div>;
}
if (base === null) return <p className="how-to-call-note" role="note">{t("howToCall.noAddress")}</p>;
const samples = callSamples(base, apiKey, t("howToCall.keyPlaceholder"));
const samples = callSamples(installation.data.api_base_url, apiKey, t("howToCall.keyPlaceholder"));
return (
<>
{installation.data.local_only ? <p className="how-to-call-note" role="note">{t("howToCall.localOnly")}</p> : null}
Expand Down
6 changes: 2 additions & 4 deletions apps/web/src/features/sandbox/NodeCleanupDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,8 @@ export function NodeCleanupDialog({ cleanup, open, onClose }: { cleanup: NodeCle
{installation.isError
? <p role="alert">{join(stays, t("The installation couldn't be read, so no command can be issued."))} <button className="text-action" type="button" disabled={installation.isFetching} onClick={() => void installation.refetch()}>{t("Try again")}</button></p>
: <p role="status">{t("Checking this installation's public URL…")}</p>}
</div> : cleanup && !sourceUrl ? <div className="sandbox-add-node form-stack">
<p>{join(stays, installation.data?.local_only && installation.data.public_url
? t("Other machines can't reach this installation's public URL, {{url}}, so no uninstall command can be given.", { url: installation.data.public_url })
: t("An uninstall command needs a public URL that other machines can reach, and this installation has none."))}</p>
</div> : cleanup && installation.data && !sourceUrl ? <div className="sandbox-add-node form-stack">
<p>{join(stays, t("Other machines can't reach this installation's public URL, {{url}}, so no uninstall command can be given.", { url: installation.data.public_url }))}</p>
</div> : cleanup ? <div className="sandbox-add-node form-stack">
<p>{t("{{name}} is removed from Core. To remove its service and files from the host, run:", { name: cleanup.name })}</p>
<CommandBlock key={command()} value={command()} label={t("Uninstall command")} autoFocus />
Expand Down
1 change: 0 additions & 1 deletion apps/web/src/features/sandbox/core-origin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,5 @@ describe("node command source", () => {
expect(nodeSourceUrl({ public_url: "https://core.example.com:8443", local_only: false })).toBe("https://core.example.com:8443");
expect(nodeSourceUrl({ public_url: "http://10.0.0.5:8080", local_only: false })).toBe("http://10.0.0.5:8080");
expect(nodeSourceUrl({ public_url: "http://localhost:8080", local_only: true })).toBeNull();
expect(nodeSourceUrl({ public_url: null, local_only: false })).toBeNull();
});
});
2 changes: 1 addition & 1 deletion apps/web/src/features/sandbox/core-origin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { CoreInstallation } from "@oac/agents-client";
* pass it: the installation's public URL, whose reverse proxy sends
* `/node-install/*` to this console. Unlike the browser's address, it is the
* same from every machine. Core accepts only origins nodes may use, so it is
* null only when other machines can't reach it (`local_only`) or it is missing.
* null only when other machines can't reach it (`local_only`).
*/
export function nodeSourceUrl(installation: Pick<CoreInstallation, "public_url" | "local_only">): string | null {
if (installation.local_only) return null;
Expand Down
3 changes: 0 additions & 3 deletions apps/web/src/features/system/ModelProviderDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,6 @@ export function ModelProviderDialog({ harness, onClose, onSaved, onReread }: {
setRejection(caught);
if (caught.param === "harness_config" || ["context_window", "max_output_tokens", "model_provider.context_window", "model_provider.max_output_tokens"].includes(caught.param ?? "")) setAdvancedOpen(true);
setError(coreError(caught, tCommon));
} else if (caught instanceof AgentCoreError && caught.code === "credential_storage_unavailable") {
// A deployment without a credential key stores nothing: a configuration error, not an unknown outcome.
setError(t("models.form.noCredentialKey"));
} else {
setError(t("models.form.uncertain"));
onReread();
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/features/system/SystemPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ export function SystemPage() {
const facts = about ? (
<Section headingId="system-installation-heading" title={t("installation.title")}>
<dl className="system-facts">
<Fact label={t("installation.publicUrl")} help={t("installation.publicUrlHelp")}>{about.public_url ? <code className="system-code">{about.public_url}</code> : <span className="system-muted">{t("installation.notSet")}</span>}</Fact>
<Fact label={t("installation.publicUrl")} help={t("installation.publicUrlHelp")}><code className="system-code">{about.public_url}</code></Fact>
<Fact label={t("installation.apiBaseUrl")} help={t("installation.apiBaseUrlHelp")}>
{about.api_base_url ? <CopyableId id={about.api_base_url} label={t("installation.copyApiBaseUrl")} /> : <span className="system-muted">{t("installation.notSet")}</span>}
<CopyableId id={about.api_base_url} label={t("installation.copyApiBaseUrl")} />
</Fact>
<Fact label={t("installation.id")}>{about.installation_id ? <CopyableId id={about.installation_id} /> : <span className="system-muted">{t("installation.unknown")}</span>}</Fact>
<Fact label={t("installation.id")}><CopyableId id={about.installation_id} /></Fact>
<Fact label={t("installation.sourceCommit")}>{about.source_commit ? <code className="system-code" title={about.source_commit}>{about.source_commit.slice(0, 12)}</code> : <span className="system-muted">{t("installation.unknown")}</span>}</Fact>
</dl>
</Section>
Expand Down
3 changes: 1 addition & 2 deletions apps/web/src/i18n/locales/en/core-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export const coreErrors = {
"project_exists": "A Project with this name already exists.",
"project_api_key_exists": "An active API key with this name already exists.",
"executor_credential_exists": "An executor credential with this name already exists.",
"credential_storage_unavailable": "Core credential storage is unavailable. Check its credential encryption configuration.",
"internal_error": "Core could not complete the request.",
"sandbox_generation_stale": "Core has a newer sandbox configuration. Refresh and review it before submitting again.",
"sandbox_reset_required": "Reset the sandbox deployment before changing this configuration.",
Expand All @@ -37,7 +36,7 @@ export const coreErrors = {
"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.",
"execution_unavailable": "Execution is temporarily unavailable. Try again later.",
"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.",
Expand Down
1 change: 0 additions & 1 deletion apps/web/src/i18n/locales/en/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ export const keys = {
loading: "Reading the API address",
failed: "The API address couldn't be read.",
localOnly: "For access from other machines, set OAC_PUBLIC_URL to an address they can reach.",
noAddress: "Core has no public API address yet. Set OAC_PUBLIC_URL.",
model: "Replace {{model}} with a model name your model provider serves, or remove the model field to use this deployment's default model configuration. Running an Agent needs a model provider: pass one in each request, save one on the Agent, or rely on the deployment default. Self-hosted Sessions never use the deployment default.",
keyPlaceholder: "<project API key>",
projectKey: "Set OPENAI_API_KEY to an API key issued for this project. A key is shown only once, when it is issued; if it's lost, issue a new one.",
Expand Down
2 changes: 0 additions & 2 deletions apps/web/src/i18n/locales/en/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export const system = {
apiBaseUrlHelp: "Applications use it as OPENAI_BASE_URL, with a Project API key as OPENAI_API_KEY.",
copyApiBaseUrl: "Copy API base URL",
localOnly: "Only reachable on the Core machine",
notSet: "Not set",
id: "Installation ID",
sourceCommit: "Source commit",
unknown: "Unknown",
Expand Down Expand Up @@ -87,7 +86,6 @@ export const system = {
save: "Save",
saving: "Saving…",
uncertain: "Core did not confirm the change. The default model configurations were read again; check them before trying again.",
noCredentialKey: "Core has no credential encryption key configured, so it can't store keys. Installer-based installs configure this automatically; for manual deployments, set OAC_CREDENTIAL_KEY_FILE for Core.",
},
clearDialog: {
title: "Clear default model configuration",
Expand Down
1 change: 0 additions & 1 deletion apps/web/src/i18n/locales/en/vaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ export const vaults = {
newToken: "New bearer token", token: "Bearer token", tokenHelp: "Write only. It is sent once, immediately cleared, and never stored in browser state, metadata, previews, or logs.",
},
errors: {
storageUnavailable: "Credential encryption is not configured on this Core. Configure OAC_CREDENTIAL_KEY_FILE and restart Core before creating or replacing a token.",
auth: "Core authentication failed. The Credential was not confirmed.", missing: "The Vault or Credential is no longer available. Refresh before trying again.",
tooLarge: "The Credential request exceeded Core's accepted size.", invalidFields: "Core rejected the Credential fields. Check the name, exact HTTPS URL, and token format.",
credentialUncertain: "The Credential write outcome was not confirmed. The catalog was refreshed; review it before explicitly trying again.",
Expand Down
Loading
Loading