diff --git a/.github/workflows/api-acceptance.yml b/.github/workflows/api-acceptance.yml
index ee010ed18..0111a8d4f 100644
--- a/.github/workflows/api-acceptance.yml
+++ b/.github/workflows/api-acceptance.yml
@@ -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
diff --git a/apps/web/e2e/console.ts b/apps/web/e2e/console.ts
index 1e50de4db..2735ebc8c 100644
--- a/apps/web/e2e/console.ts
+++ b/apps/web/e2e/console.ts
@@ -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[] = [];
diff --git a/apps/web/e2e/fixture-console.mjs b/apps/web/e2e/fixture-console.mjs
index 1145cd52d..69c03a8f5 100644
--- a/apps/web/e2e/fixture-console.mjs
+++ b/apps/web/e2e/fixture-console.mjs
@@ -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);
@@ -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: {
@@ -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,
@@ -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") {
diff --git a/apps/web/e2e/system.spec.ts b/apps/web/e2e/system.spec.ts
index a731ed974..50a77be5e 100644
--- a/apps/web/e2e/system.spec.ts
+++ b/apps/web/e2e/system.spec.ts
@@ -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" });
diff --git a/apps/web/src/components/InstallationNotice.test.tsx b/apps/web/src/components/InstallationNotice.test.tsx
index 81a8ab535..5545cd40b 100644
--- a/apps/web/src/components/InstallationNotice.test.tsx
+++ b/apps/web/src/components/InstallationNotice.test.tsx
@@ -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 },
};
diff --git a/apps/web/src/features/api-keys/HowToCall.tsx b/apps/web/src/features/api-keys/HowToCall.tsx
index a7c14739e..8c49e2bca 100644
--- a/apps/web/src/features/api-keys/HowToCall.tsx
+++ b/apps/web/src/features/api-keys/HowToCall.tsx
@@ -64,14 +64,12 @@ 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
@@ -79,8 +77,7 @@ function HowToCallBody({ apiKey }: { apiKey: string | null }) {
// The first sample's place, as the console's other first reads hold theirs.
:
;
}
- if (base === null) return {t("howToCall.noAddress")}
;
- 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 ? {t("howToCall.localOnly")}
: null}
diff --git a/apps/web/src/features/sandbox/NodeCleanupDialog.tsx b/apps/web/src/features/sandbox/NodeCleanupDialog.tsx
index a4de4b299..cc8a41c0f 100644
--- a/apps/web/src/features/sandbox/NodeCleanupDialog.tsx
+++ b/apps/web/src/features/sandbox/NodeCleanupDialog.tsx
@@ -41,10 +41,8 @@ export function NodeCleanupDialog({ cleanup, open, onClose }: { cleanup: NodeCle
{installation.isError
? {join(stays, t("The installation couldn't be read, so no command can be issued."))} void installation.refetch()}>{t("Try again")}
: {t("Checking this installation's public URL…")}
}
- : cleanup && !sourceUrl ?
-
{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."))}
+
: cleanup && installation.data && !sourceUrl ?
+
{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 }))}
: cleanup ?
{t("{{name}} is removed from Core. To remove its service and files from the host, run:", { name: cleanup.name })}
diff --git a/apps/web/src/features/sandbox/core-origin.test.ts b/apps/web/src/features/sandbox/core-origin.test.ts
index 36232bab9..ef555ea5d 100644
--- a/apps/web/src/features/sandbox/core-origin.test.ts
+++ b/apps/web/src/features/sandbox/core-origin.test.ts
@@ -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();
});
});
diff --git a/apps/web/src/features/sandbox/core-origin.ts b/apps/web/src/features/sandbox/core-origin.ts
index dea953213..6d4de7982 100644
--- a/apps/web/src/features/sandbox/core-origin.ts
+++ b/apps/web/src/features/sandbox/core-origin.ts
@@ -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
): string | null {
if (installation.local_only) return null;
diff --git a/apps/web/src/features/system/ModelProviderDialog.tsx b/apps/web/src/features/system/ModelProviderDialog.tsx
index 3e555058e..9a145278f 100644
--- a/apps/web/src/features/system/ModelProviderDialog.tsx
+++ b/apps/web/src/features/system/ModelProviderDialog.tsx
@@ -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();
diff --git a/apps/web/src/features/system/SystemPage.tsx b/apps/web/src/features/system/SystemPage.tsx
index 0fc42d3cd..2213ae7bb 100644
--- a/apps/web/src/features/system/SystemPage.tsx
+++ b/apps/web/src/features/system/SystemPage.tsx
@@ -42,11 +42,11 @@ export function SystemPage() {
const facts = about ? (
- {about.public_url ? {about.public_url} : {t("installation.notSet")} }
+ {about.public_url}
- {about.api_base_url ? : {t("installation.notSet")} }
+
- {about.installation_id ? : {t("installation.unknown")} }
+
{about.source_commit ? {about.source_commit.slice(0, 12)} : {t("installation.unknown")} }
diff --git a/apps/web/src/i18n/locales/en/core-errors.ts b/apps/web/src/i18n/locales/en/core-errors.ts
index d263d3e24..77b2cd7f2 100644
--- a/apps/web/src/i18n/locales/en/core-errors.ts
+++ b/apps/web/src/i18n/locales/en/core-errors.ts
@@ -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.",
@@ -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.",
diff --git a/apps/web/src/i18n/locales/en/keys.ts b/apps/web/src/i18n/locales/en/keys.ts
index 7a7faf8d1..5f15a8187 100644
--- a/apps/web/src/i18n/locales/en/keys.ts
+++ b/apps/web/src/i18n/locales/en/keys.ts
@@ -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: "",
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.",
diff --git a/apps/web/src/i18n/locales/en/system.ts b/apps/web/src/i18n/locales/en/system.ts
index dda2183a5..cacaa5427 100644
--- a/apps/web/src/i18n/locales/en/system.ts
+++ b/apps/web/src/i18n/locales/en/system.ts
@@ -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",
@@ -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",
diff --git a/apps/web/src/i18n/locales/en/vaults.ts b/apps/web/src/i18n/locales/en/vaults.ts
index 4160c743c..3155078da 100644
--- a/apps/web/src/i18n/locales/en/vaults.ts
+++ b/apps/web/src/i18n/locales/en/vaults.ts
@@ -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.",
diff --git a/apps/web/src/i18n/locales/zh-CN/core-errors.ts b/apps/web/src/i18n/locales/zh-CN/core-errors.ts
index 705b712d4..bc0350a15 100644
--- a/apps/web/src/i18n/locales/zh-CN/core-errors.ts
+++ b/apps/web/src/i18n/locales/zh-CN/core-errors.ts
@@ -18,7 +18,6 @@ export const coreErrors = {
"project_exists": "此项目名称已被使用。",
"project_api_key_exists": "此名称已被使用中的 API Key 占用。",
"executor_credential_exists": "此名称的执行器凭证已存在。",
- "credential_storage_unavailable": "Core 凭证存储不可用,请检查凭证加密配置。",
"internal_error": "Core 未能完成请求。",
"sandbox_generation_stale": "Core 的沙箱配置已更新。请刷新并检查后再提交。",
"sandbox_reset_required": "请先重置沙箱部署,再更改此配置。",
@@ -36,7 +35,7 @@ export const coreErrors = {
"sandbox_specification_mismatch": "已保存的沙箱规格与部署不一致。请刷新检查配置。",
"sandbox_operation_unsupported": "所选沙箱提供商不支持此操作。",
"environment_unavailable": "此 Session 的环境已不可用。",
- "execution_unavailable": "此 Core 不提供执行功能。",
+ "execution_unavailable": "执行暂时不可用,请稍后重试。",
"runtime_history_unavailable": "此 Core 上的 Runtime 历史不可用。",
"runtime_history_unsupported": "此 Session 不支持 Runtime 历史。",
"core_metrics_unavailable": "无法读取 Core 指标,请稍后重试。",
diff --git a/apps/web/src/i18n/locales/zh-CN/keys.ts b/apps/web/src/i18n/locales/zh-CN/keys.ts
index 1a458c22b..b5340bd6a 100644
--- a/apps/web/src/i18n/locales/zh-CN/keys.ts
+++ b/apps/web/src/i18n/locales/zh-CN/keys.ts
@@ -110,7 +110,6 @@ export const keys: TranslationShape = {
loading: "正在读取 API 地址",
failed: "无法读取 API 地址。",
localOnly: "从其他机器调用前,请先把 OAC_PUBLIC_URL 设为它们能访问的地址。",
- noAddress: "Core 尚未配置公开 API 地址,请设置 OAC_PUBLIC_URL。",
model: "把 {{model}} 换成模型服务提供的模型名;也可以删掉 model 字段,使用本部署的默认模型配置。运行 Agent 需要模型服务:在每个请求里传入、保存在 Agent 上,或使用部署默认值。自托管 Session 不使用部署默认值。",
keyPlaceholder: "<项目 API key>",
projectKey: "把 OPENAI_API_KEY 设为这个项目签发的 API key。key 只在签发时显示一次;丢失后请签发新 key。",
diff --git a/apps/web/src/i18n/locales/zh-CN/system.ts b/apps/web/src/i18n/locales/zh-CN/system.ts
index 770c992ce..5bb291887 100644
--- a/apps/web/src/i18n/locales/zh-CN/system.ts
+++ b/apps/web/src/i18n/locales/zh-CN/system.ts
@@ -13,7 +13,6 @@ export const system: TranslationShape = {
apiBaseUrlHelp: "应用把它设为 OPENAI_BASE_URL,并把项目 API key 设为 OPENAI_API_KEY。",
copyApiBaseUrl: "复制 API 基础地址",
localOnly: "只能在 Core 所在的机器上访问",
- notSet: "未设置",
id: "安装 ID",
sourceCommit: "源码提交",
unknown: "未知",
@@ -89,7 +88,6 @@ export const system: TranslationShape = {
save: "保存",
saving: "正在保存…",
uncertain: "Core 没有确认这次修改。已重新读取默认模型配置,请先核对再重试。",
- noCredentialKey: "Core 没有配置凭据加密密钥,因此无法保存 key。用安装器安装的会自动配置;手动部署时,请为 Core 设置 OAC_CREDENTIAL_KEY_FILE。",
},
clearDialog: {
title: "清除默认模型配置",
diff --git a/apps/web/src/i18n/locales/zh-CN/vaults.ts b/apps/web/src/i18n/locales/zh-CN/vaults.ts
index 52a7ef2ef..ca25fd7c0 100644
--- a/apps/web/src/i18n/locales/zh-CN/vaults.ts
+++ b/apps/web/src/i18n/locales/zh-CN/vaults.ts
@@ -8,5 +8,5 @@ export const vaults: TranslationShape = {
detail: { back: "返回", facts: "Vault 详情", id: "ID", created: "创建时间", metadata: "元数据", credentials: "Credential", credentialsHelp: "静态 bearer 令牌只可写入:Core 不会返回令牌,所以这里只显示元数据。替换令牌不会在 provider 侧撤销旧令牌。", name: "名称", url: "MCP 服务 URL", auth: "认证方式", updated: "更新时间", addCredential: "添加 Credential", deleteVault: "删除 Vault", staticBearer: "静态 bearer", oauth: "OAuth", tokenHidden: "令牌已隐藏" },
createForm: { name: "名称", namePlaceholder: "运行时 Credential", nameHelp: "必填。去除首尾空格后的名称最多 256 个 UTF-8 字节。", metadata: "元数据", optional: "可选", metadataHelp: "输入值为字符串的 JSON 对象。留空会转为 {{emptyObject}};编码后的元数据最大为 64 KiB。", metadataWarning: "Vault 元数据是公开的。切勿在此填写密钥、令牌、密码、凭据或私有连接信息。", creating: "创建中…", create: "创建 Vault" },
credentialDialog: { replaceTitle: "替换令牌 · {{name}}", addTitle: "添加静态 bearer Credential", saving: "保存中…", create: "创建 Credential", notConfirmed: "Credential 未确认", name: "名称", namePlaceholder: "内部 MCP", exactUrl: "精确 MCP 服务 URL", urlHelp: "此 URL 必须与 Agent MCP 定义完全一致。创建此资源不会发起网络请求。", replaceWarning: "旧令牌不会被读取或显示。正在运行的工作可能已经持有它;替换操作不会撤销 provider 侧令牌。", newToken: "新 bearer 令牌", token: "Bearer 令牌", tokenHelp: "只可写入。令牌只发送一次,随后立即清除,且绝不会保存到浏览器状态、元数据、预览或日志中。" },
- errors: { storageUnavailable: "此 Core 未配置 Credential 加密。请配置 OAC_CREDENTIAL_KEY_FILE 并重启 Core,再创建或替换令牌。", auth: "Core 认证失败,Credential 未确认。", missing: "Vault 或 Credential 已不存在。请刷新后重试。", tooLarge: "Credential 请求超过 Core 接受的大小。", invalidFields: "Core 拒绝了 Credential 字段。请检查名称、精确 HTTPS URL 和令牌格式。", credentialUncertain: "未能确认 Credential 写入结果。目录已刷新;再次明确重试前请先检查。", bearer: "请输入非空的 RFC 6750 bearer 令牌。空白或其他仅能保存的不透明值无法在当前运行时执行。", credentialName: "Credential 名称必须为 1 至 256 个 UTF-8 字节。", exactUrl: "请输入不含凭据、查询参数、片段、空白或反斜杠的精确 HTTPS MCP URL。", createUncertain: "未能确认 Vault 创建结果。草稿保持不变;再次明确重试前请检查当前 Core 状态。", vaultName: "名称必须为 1 至 256 个 UTF-8 字节。", metadataUnknown: "无法验证元数据。", metadataJson: "元数据必须是有效 JSON。", metadataShape: "元数据必须是值为字符串的 JSON 对象。", metadataStrings: "元数据值必须全部为字符串;不支持嵌套值、数组、数字、布尔值和 null。", metadataLarge: "元数据经 UTF-8 JSON 编码后最大为 64 KiB。", mismatchedMetadata: "OpenAgentCore 返回的 Vault 元数据不匹配。", uniqueAttachments: "附加的 Vault 不能重复。", catalogIncomplete: "尚未从此 Core 完整加载 Credential 元数据。", vaultMissing: "所选 Vault 已不在当前目录中。", credentialMismatch: "MCP 服务 {{server}} 引用了不可用或 URL 不匹配的 Credential。", credentialVaultMissing: "MCP 服务 {{server}} 引用的 Credential 所属 Vault 不可用。", multipleCredentials: "匿名 MCP 服务 {{server}} 在所选 Vault 中匹配到多个 Credential。请选择唯一匹配的 Vault,或明确配置一个 Credential。", matchedVaultMissing: "MCP 服务 {{server}} 匹配到的 Credential 所属 Vault 不可用。" },
+ errors: { auth: "Core 认证失败,Credential 未确认。", missing: "Vault 或 Credential 已不存在。请刷新后重试。", tooLarge: "Credential 请求超过 Core 接受的大小。", invalidFields: "Core 拒绝了 Credential 字段。请检查名称、精确 HTTPS URL 和令牌格式。", credentialUncertain: "未能确认 Credential 写入结果。目录已刷新;再次明确重试前请先检查。", bearer: "请输入非空的 RFC 6750 bearer 令牌。空白或其他仅能保存的不透明值无法在当前运行时执行。", credentialName: "Credential 名称必须为 1 至 256 个 UTF-8 字节。", exactUrl: "请输入不含凭据、查询参数、片段、空白或反斜杠的精确 HTTPS MCP URL。", createUncertain: "未能确认 Vault 创建结果。草稿保持不变;再次明确重试前请检查当前 Core 状态。", vaultName: "名称必须为 1 至 256 个 UTF-8 字节。", metadataUnknown: "无法验证元数据。", metadataJson: "元数据必须是有效 JSON。", metadataShape: "元数据必须是值为字符串的 JSON 对象。", metadataStrings: "元数据值必须全部为字符串;不支持嵌套值、数组、数字、布尔值和 null。", metadataLarge: "元数据经 UTF-8 JSON 编码后最大为 64 KiB。", mismatchedMetadata: "OpenAgentCore 返回的 Vault 元数据不匹配。", uniqueAttachments: "附加的 Vault 不能重复。", catalogIncomplete: "尚未从此 Core 完整加载 Credential 元数据。", vaultMissing: "所选 Vault 已不在当前目录中。", credentialMismatch: "MCP 服务 {{server}} 引用了不可用或 URL 不匹配的 Credential。", credentialVaultMissing: "MCP 服务 {{server}} 引用的 Credential 所属 Vault 不可用。", multipleCredentials: "匿名 MCP 服务 {{server}} 在所选 Vault 中匹配到多个 Credential。请选择唯一匹配的 Vault,或明确配置一个 Credential。", matchedVaultMissing: "MCP 服务 {{server}} 匹配到的 Credential 所属 Vault 不可用。" },
};
diff --git a/apps/web/src/lib/locale-strings.ts b/apps/web/src/lib/locale-strings.ts
index cd7648aff..160dc0a8e 100644
--- a/apps/web/src/lib/locale-strings.ts
+++ b/apps/web/src/lib/locale-strings.ts
@@ -164,7 +164,6 @@ export const chinese = {
"Clean up the host": "清理主机",
"{{name}} is removed from Core. To remove its service and files from the host, run:": "{{name}} 已从 Core 移除。要删除它在主机上的服务和文件,请运行:",
"{{name}} is removed from Core, but its service and files stay on the host.": "{{name}} 已从 Core 移除,但它的服务和文件仍留在主机上。",
- "An uninstall command needs a public URL that other machines can reach, and this installation has none.": "卸载命令需要其他机器能访问的公开地址,而当前安装没有。",
"Other machines can't reach this installation's public URL, {{url}}, so no uninstall command can be given.": "其他机器无法访问本安装的公开地址 {{url}},因此无法生成卸载命令。",
"Uninstall command": "卸载命令",
"Copy {{command}}": "复制 {{command}}",
diff --git a/contracts/agents-api/admin-api.md b/contracts/agents-api/admin-api.md
index ca0356b5c..16ca46a78 100644
--- a/contracts/agents-api/admin-api.md
+++ b/contracts/agents-api/admin-api.md
@@ -132,9 +132,9 @@ Core writes this record in the same transaction that creates the Session. Later
| Field | Meaning |
| --- | --- |
| `object` | `core.installation` |
-| `installation_id` | The installation ID from `OAC_INSTALLATION_ID_FILE` ([Compose installations](../../docs/configuration.md#compose-installations)); null when Core runs without the sandbox manager |
-| `public_url` | The [`public_url`](../../docs/configuration.md#settings) setting: the origin applications, nodes, sandboxes and self-hosted executors use. Null when unset |
-| `api_base_url` | `public_url` followed by `/v1`, the `OPENAI_BASE_URL` for Project API keys. Null when `public_url` is null |
+| `installation_id` | The installation ID from `OAC_INSTALLATION_ID_FILE` ([Compose installations](../../docs/configuration.md#compose-installations)) |
+| `public_url` | The [`public_url`](../../docs/configuration.md#settings) setting: the origin applications, nodes, sandboxes and self-hosted executors use |
+| `api_base_url` | `public_url` followed by `/v1`, the `OPENAI_BASE_URL` for Project API keys |
| `local_only` | True when `public_url` names a loopback host, which only the Core host reaches |
| `source_commit` | The full source commit Core was built from; null for development builds |
| `configuration` | The process settings Core loaded from its environment, under `settings` |
diff --git a/contracts/agents-api/core-errors.md b/contracts/agents-api/core-errors.md
index 3873dcdf5..33ee247ae 100644
--- a/contracts/agents-api/core-errors.md
+++ b/contracts/agents-api/core-errors.md
@@ -90,12 +90,11 @@ These codes have null `param` and no `details`. [Sandbox deployment](./sandbox-d
| 409 | `sandbox_deployment_conflict` | The sandbox deployment cannot change in its current state |
| 409 | `sandbox_specification_mismatch` | The saved deployment specification is no longer valid for its provider |
| 409 | `runtime_node_in_use` | The node still holds allocations, snapshots, reservations or pending cleanup |
-| 409 | `environment_unavailable` | The Session's environment is no longer available, such as an archive on a Core without execution |
+| 409 | `environment_unavailable` | The Session's environment is no longer available, such as a hosted environment that failed to provision |
| 409 | `runtime_history_unsupported` | Runtime history is not supported for the Session |
| 500 | `internal_error` | Core could not complete the operation |
| 503 | `runtime_node_unavailable` | No sandbox node is available or has capacity |
-| 503 | `credential_storage_unavailable` | Core has no credential encryption key |
-| 503 | `execution_unavailable` | Execution is not available on this Core |
+| 503 | `execution_unavailable` | Execution is not available, such as while Core shuts down |
| 503 | `runtime_history_unavailable` | Durable Runtime history is not configured or temporarily unavailable |
| 503 | `core_metrics_unavailable` | Core metrics could not be read |
| 503 | `file_transfer_unavailable` | Bounded content transfer is unavailable |
diff --git a/contracts/agents-api/core.openapi.yaml b/contracts/agents-api/core.openapi.yaml
index 9a1ef6ea1..90fbd2970 100644
--- a/contracts/agents-api/core.openapi.yaml
+++ b/contracts/agents-api/core.openapi.yaml
@@ -427,17 +427,15 @@ definitions:
address_bindings:
$ref: '#/definitions/deployment.AddressBindings'
api_base_url:
- description: public_url followed by /v1; null when public_url is null.
+ description: public_url followed by /v1.
type: string
- x-nullable: true
configuration:
allOf:
- $ref: '#/definitions/api.InstallationConfiguration'
description: The process settings Core loaded.
installation_id:
- description: The ID in OAC_INSTALLATION_ID_FILE; null when Core runs without the sandbox manager.
+ description: The ID in OAC_INSTALLATION_ID_FILE.
type: string
- x-nullable: true
local_only:
description: True when public_url names a loopback host, reachable only from the Core host.
type: boolean
@@ -446,9 +444,8 @@ definitions:
- core.installation
type: string
public_url:
- description: 'OAC_PUBLIC_URL: the origin applications, nodes, sandboxes and self-hosted executors use. Null when unset.'
+ description: 'OAC_PUBLIC_URL: the origin applications, nodes, sandboxes and self-hosted executors use.'
type: string
- x-nullable: true
source_commit:
description: Full source commit Core was built from; null for development builds.
type: string
diff --git a/contracts/agents-api/environment-executor-credentials.md b/contracts/agents-api/environment-executor-credentials.md
index bd58ce767..90e7d518f 100644
--- a/contracts/agents-api/environment-executor-credentials.md
+++ b/contracts/agents-api/environment-executor-credentials.md
@@ -35,7 +35,7 @@ The installer calls these machine routes on Core:
| `POST /api/v1/agent-daemon/installation` | Grant | The frozen binding: `version`, `protocol_version`, `environment_id`, `remote_url`, `workspace_directory`, `harness` |
| `POST /api/v1/agent-daemon/installation/claim` | Grant | `{"executor_token":"SECRET"}`; 204 |
-An invalid or expired grant returns 401 `installation_authorization_invalid`. Without matching installers the grant routes return 503 `installation_unavailable`. Core signs each grant with the installation's [`secrets/core/credential.key`](../../docs/configuration.md#compose-installations); without a configured key, the Session responses and Core-key read above and the grant routes return 503 `credential_storage_unavailable`. A malformed secret returns 400. Artifact routes carry no credential, and the grant is sent only to Core, never to an artifact host.
+An invalid or expired grant returns 401 `installation_authorization_invalid`. Without matching installers the grant routes return 503 `installation_unavailable`. Core signs each grant with the installation's [`secrets/core/credential.key`](../../docs/configuration.md#compose-installations). A malformed secret returns 400. Artifact routes carry no credential, and the grant is sent only to Core, never to an artifact host.
## Core-key routes
diff --git a/contracts/agents-api/model-execution.md b/contracts/agents-api/model-execution.md
index 8d8751577..f843c7ecc 100644
--- a/contracts/agents-api/model-execution.md
+++ b/contracts/agents-api/model-execution.md
@@ -57,7 +57,7 @@ The deployment default holds the operator's key, so it stays on operator compute
An empty Session execution extension is invalid. An explicit null provider requests inheritance; an empty or partial provider object is invalid. Unknown, duplicate or output-only saved-provider fields are rejected. A saved Agent without a Harness may save a valid bundle; its Harness compatibility is checked at Session admission. A provider-only Agent update keeps the saved Harness and validates the merged combination under the row lock. The Session's inline `agent.x_agents_core` accepts `harness` and `harness_config`; the provider override belongs at the request's top level.
-Core reads the Agent configuration and encrypted bundle from one database snapshot; an explicit complete Session override needs no decryption of the saved bundle. The Session's own encrypted snapshot is written atomically with the Session and its Environment. Existing Sessions never consult the Agent again: edits, key replacement, deletion, suspension and restarts cannot change their model, Harness or provider. A missing or wrong encryption key fails closed; keep the same [credential key](../../docs/configuration.md#compose-installations) across restarts. There is no Turn-level override.
+Core reads the Agent configuration and encrypted bundle from one database snapshot; an explicit complete Session override needs no decryption of the saved bundle. The Session's own encrypted snapshot is written atomically with the Session and its Environment. Existing Sessions never consult the Agent again: edits, key replacement, deletion, suspension and restarts cannot change their model, Harness or provider. A wrong encryption key fails closed; keep the same [credential key](../../docs/configuration.md#compose-installations) across restarts. There is no Turn-level override.
New hosted requests, and requests that omit the inline model, record caller intent before resolving mutable defaults. Other inline requests, such as `none`, keep the resolved-request retry rule; that hash leaves out the deployment default, so changing the default does not change their retry identity. A matching creation retry recovers the committed Session before resolving the Agent or provider again and enqueues no further input. Streaming is outside the retry identity. The [TypeScript client](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/packages/agents-client/README.md#saved-agent-and-deployment-defaults) shows saved Agents and deployment defaults.
@@ -119,7 +119,7 @@ The deployment default is a runtime setting stored in Core: one complete model c
| `PUT /core/v1/harnesses/{harness}/model-configuration` | Replace `{model_provider, model, harness_config}` using the shared provider and native-parameter validators |
| `DELETE /core/v1/harnesses/{harness}/model-configuration` | Remove it; idempotent, 204 |
-PUT requires `model` and a complete `model_provider`; `harness_config` defaults to `{}`. Reads return `model`, `harness_config`, the safe `model_provider` view (`protocol`, `base_url`, optional token limits and `api_key_configured`), observations and `updated_at`, never the key. The bundle is encrypted with its own purpose and bound to the Harness. Each write records an administrator audit entry (`resource_type: deployment_model_provider`, the Harness as `resource_id`, action `set` or `delete`, `project_id` null) without the key. A missing or wrong encryption key fails closed: writes, and Session creation that needs the default, return 503 `credential_storage_unavailable`.
+PUT requires `model` and a complete `model_provider`; `harness_config` defaults to `{}`. Reads return `model`, `harness_config`, the safe `model_provider` view (`protocol`, `base_url`, optional token limits and `api_key_configured`), observations and `updated_at`, never the key. The bundle is encrypted with its own purpose and bound to the Harness. Each write records an administrator audit entry (`resource_type: deployment_model_provider`, the Harness as `resource_id`, action `set` or `delete`, `project_id` null) without the key. A default sealed under another encryption key fails closed: reading it, and Session creation that needs it, return 500 `internal_error` until the default is set again.
Session creation decrypts the default of the resolved Harness and freezes it in the Session's encrypted snapshot like any other bundle, so changing or removing the default never reaches existing Sessions. The execution-configuration read shows the frozen safe view with source `deployment`. A missing or invalid provider snapshot fails closed, with no fallback to another model or provider.
diff --git a/contracts/agents-api/runtime-observability.md b/contracts/agents-api/runtime-observability.md
index a30353900..c91351dac 100644
--- a/contracts/agents-api/runtime-observability.md
+++ b/contracts/agents-api/runtime-observability.md
@@ -83,7 +83,7 @@ CPU quietness, heartbeat age, connection state and keepalive time are not idle t
### Periodic sampling
-Periodic collection runs only with the execution worker (Core started with `OAC_PUBLIC_URL`; see the [Core environment](../../docs/configuration.md#appendix-core-environment-without-the-installer)) and under the worker's database lease. A Core without it stores no history and answers every history read with 503; current reads work on either.
+Periodic collection runs in the execution Worker, under its database lease.
The sampler sweeps once at startup and again each sampling interval after the previous sweep ends. A sweep is a keyset scan, in Session ID order, of the Sessions that are not deleted, are `openai_hosted` and have no released allocation. It reads pages of 32 Sessions through the same resolver and sources as current reads, with eight concurrent reads and two seconds per source. The sampler checks the lease before each page and every 100 ms during a sweep, cancels in-flight reads when ownership is lost, and checks it again before handing each record to export. A failed row does not stop the sweep, and an incomplete sweep is repeated at the next interval.
diff --git a/contracts/agents-api/sandbox-deployment.md b/contracts/agents-api/sandbox-deployment.md
index c13eeba34..3e813ad98 100644
--- a/contracts/agents-api/sandbox-deployment.md
+++ b/contracts/agents-api/sandbox-deployment.md
@@ -217,7 +217,6 @@ Some fields keep one name across providers but differ in meaning, or do not appl
| 409 | `sandbox_deployment_conflict` | Another state the change cannot apply to |
| 409 | `runtime_node_in_use` | Node removal while it holds resources |
| 503 | `execution_unavailable` | Provider preparation is unavailable |
-| 503 | `credential_storage_unavailable` | Core has no credential encryption key |
Storage and credential failures stay errors: an empty or failed read never proves cleanup. The [machine connection API](./machine-api.md#node-route-errors) lists the errors of the node routes.
diff --git a/contracts/agents-api/sessions-events.md b/contracts/agents-api/sessions-events.md
index 7615b7384..de744cf36 100644
--- a/contracts/agents-api/sessions-events.md
+++ b/contracts/agents-api/sessions-events.md
@@ -39,7 +39,7 @@ A Session stays usable after a Turn fails: new input starts a new Turn. Later re
- **Cancellation.** A queued Turn is cancelled without a live Runtime. A running Turn is cancelled when the Runtime confirms it; completion can win that race. The Turn has stopped when it reads `cancelled`, not when the request returns. A cancellation on an idle Session with no pending input is accepted and has no effect; while an input reservation is pending, it returns 409.
- **Function results.** `turn_id`, `call_id` and `success` are required; `output` and `error` are optional and nullable ([content rules](./message-content.md#function-results)). An identical repeated result returns 202 without another application or event. The result Item appears when the harness applies the result; a result that cancellation prevents from being applied stays stored but produces no Item.
- **Queueing.** A queued Turn starts when a Runtime that supports the Session's harness and configuration is connected and one of Core's [`core.execution_concurrency`](../../docs/configuration.md#settings) work slots is free. A Session stays bound to the Runtime that first ran it.
-- **Execution availability.** A service without execution returns 503 `execution_unavailable`, and a Worker that loses execution ownership returns 503.
+- **Execution availability.** While Core shuts down, or after its Worker loses execution ownership, requests return 503 `execution_unavailable`.
### Sessions with an Environment
diff --git a/contracts/agents-api/vaults.md b/contracts/agents-api/vaults.md
index a05e76511..aaa3a9bac 100644
--- a/contracts/agents-api/vaults.md
+++ b/contracts/agents-api/vaults.md
@@ -139,10 +139,10 @@ Token endpoints must be HTTPS. Core resolves the host, rejects loopback, private
Core seals every token, refresh token and client secret with AES-256-GCM under the installation's [`secrets/core/credential.key`](../../docs/configuration.md#compose-installations), bound to the Project, Vault, Credential, auth type and `mcp_server_url`. A wrong key, a modified row or a row moved to another binding fails to decrypt. Names are metadata outside the binding. The key and plaintext tokens exist in trusted service memory; encryption protects stored secrets and does not protect against a compromised service host.
-Without a configured key, Credential creation and replacement return 503 `credential_storage_unavailable` before writing; reads, lists, deletion and Vault operations still work. An unreadable or malformed key file stops Core at startup. Losing or replacing the key makes every stored secret unusable; Core supports one key, with no rotation or re-encryption.
+An unreadable or malformed key file stops Core at startup. Losing or replacing the key makes every stored secret unusable. Reads, lists, deletion, Vault operations, new Credentials and `static_bearer` token replacements still work; an `mcp_oauth` update returns 500 `internal_error`, and a dispatch that needs an old secret fails. A saved E2B key is lost too, so hosted Sessions return 503 `execution_unavailable` until you open **System** → **Manage sandbox configuration**, choose **Reset deployment** (with **Force — cancel remaining work now** if needed) and set up the backend again; sandboxes left behind expire under their E2B timeout. Core supports one key, with no rotation or re-encryption.
## Deletion
-Deleting a Credential, or its Vault, removes the credential rows. It does not erase secrets from PostgreSQL pages, write-ahead logs, backups or native history. Afterwards its reads, updates and repeated deletion return 404, lists omit it, new Sessions cannot select it and new Credentials cannot be created in a deleted Vault (a missing storage key still answers 503 first).
+Deleting a Credential, or its Vault, removes the credential rows. It does not erase secrets from PostgreSQL pages, write-ahead logs, backups or native history. Afterwards its reads, updates and repeated deletion return 404, lists omit it, new Sessions cannot select it and new Credentials cannot be created in a deleted Vault.
Existing Sessions keep their frozen attachments and selections, and their history stays readable. The next dispatch that needs a deleted Credential fails; Core neither selects another Credential nor connects anonymously. Deletion does not cancel running work, withdraw a token already sent to a Runtime or revoke the grant at its provider: cancel the Session and revoke the grant yourself when needed. Revoked or invalid OAuth grants likewise fail until replaced.
diff --git a/contracts/agents-api/zh/admin-api.md b/contracts/agents-api/zh/admin-api.md
index 8ce891d43..886d98255 100644
--- a/contracts/agents-api/zh/admin-api.md
+++ b/contracts/agents-api/zh/admin-api.md
@@ -1,7 +1,7 @@
---
title: "Core 管理 API"
source: contracts/agents-api/admin-api.md
-source_hash: 7eb295db6402db8dae91fcdd97f90a5900f740925caaee9d0172b9886544f6ec
+source_hash: 8a781b20f0a359de77594ca570c4e1723332ac16a60d9a9be405ca5e2422e87d
---
Core 管理 API(`/core/v1`)用于管理安装实例:Project 及其 API 密钥、Project 资源的读取和删除、执行器凭据、部署默认模型、沙箱部署及其节点、监控和审计。Web 的[控制台服务器](../../../docs/zh/web/console-server.md#forwarding-to-core)会为已登录的管理员调用它;运维人员则从 Core 主机上的脚本调用它([编写 Core API 脚本](../../../docs/zh/getting-started/operations.md#script-the-core-api))。生成的架构是 [core.openapi.yaml](../core.openapi.yaml),所有错误都使用 [Core 错误封装](core-errors.md)。
@@ -134,9 +134,9 @@ Core 会在创建 Session 的同一事务中写入此记录。之后的 Agent
| 字段 | 含义 |
| --- | --- |
| `object` | `core.installation` |
-| `installation_id` | `OAC_INSTALLATION_ID_FILE` 中的安装 ID([Compose 安装](../../../docs/zh/configuration.md#compose-installations));Core 在不使用沙箱管理器运行时为 null |
-| `public_url` | `public_url` 设置([设置](../../../docs/zh/configuration.md#settings)):应用程序、节点、沙箱和自托管执行器使用的源地址。未设置时为 null |
-| `api_base_url` | 在 `public_url` 后附加 `/v1`,即 Project API 密钥使用的 `OPENAI_BASE_URL`。当 `public_url` 为 null 时为 null |
+| `installation_id` | `OAC_INSTALLATION_ID_FILE` 中的安装 ID([Compose 安装](../../../docs/zh/configuration.md#compose-installations)) |
+| `public_url` | `public_url` 设置([设置](../../../docs/zh/configuration.md#settings)):应用程序、节点、沙箱和自托管执行器使用的源地址 |
+| `api_base_url` | 在 `public_url` 后附加 `/v1`,即 Project API 密钥使用的 `OPENAI_BASE_URL` |
| `local_only` | 当 `public_url` 指向回环主机时为 True,该主机只能由 Core 主机访问 |
| `source_commit` | Core 构建所依据的完整源代码提交;开发构建为 null |
| `configuration` | Core 从环境加载的进程设置,位于 `settings` 中 |
diff --git a/contracts/agents-api/zh/core-errors.md b/contracts/agents-api/zh/core-errors.md
index a0d533547..b7ba9abb3 100644
--- a/contracts/agents-api/zh/core-errors.md
+++ b/contracts/agents-api/zh/core-errors.md
@@ -1,7 +1,7 @@
---
title: "Core 管理错误"
source: contracts/agents-api/core-errors.md
-source_hash: 78fcbde855beafae4d1f5eb38b87596eeca25c99c025c6c54978db988d2a534a
+source_hash: 46b6e7eb76739b9473576113aad3eee64fff3fddd429c3232ede67f671d60ed7
---
`/core/v1` 上的错误使用此封装结构。`message` 是安全的英文文本;`code` 和 `param` 可以为 null。客户端依据稳定的 `code` 和可选的 `param` 进行处理,对未知代码显示 `message`,绝不解析消息,也绝不自动重试被拒绝的写操作。
@@ -92,12 +92,11 @@ Web 的控制台服务器在 `/core` 路径上发生自身故障时使用此封
| 409 | `sandbox_deployment_conflict` | 沙箱部署在当前状态下无法更改 |
| 409 | `sandbox_specification_mismatch` | 已保存的部署规格对其提供商不再有效 |
| 409 | `runtime_node_in_use` | 节点仍有资源分配、快照、预留资源或待清理项 |
-| 409 | `environment_unavailable` | Session 的环境已不可用,例如在不提供执行的 Core 上归档 |
+| 409 | `environment_unavailable` | Session 的环境已不可用,例如托管环境创建失败 |
| 409 | `runtime_history_unsupported` | 该 Session 不支持 Runtime 历史 |
| 500 | `internal_error` | Core 未能完成操作 |
| 503 | `runtime_node_unavailable` | 没有可用或有剩余容量的沙箱节点 |
-| 503 | `credential_storage_unavailable` | Core 没有凭证加密密钥 |
-| 503 | `execution_unavailable` | 此 Core 不提供执行功能 |
+| 503 | `execution_unavailable` | 执行不可用,例如 Core 正在关闭 |
| 503 | `runtime_history_unavailable` | 持久 Runtime 历史未配置或暂时不可用 |
| 503 | `core_metrics_unavailable` | 无法读取 Core 指标 |
| 503 | `file_transfer_unavailable` | 有界内容传输不可用 |
diff --git a/contracts/agents-api/zh/environment-executor-credentials.md b/contracts/agents-api/zh/environment-executor-credentials.md
index 3d1fea6bf..59ea8d22c 100644
--- a/contracts/agents-api/zh/environment-executor-credentials.md
+++ b/contracts/agents-api/zh/environment-executor-credentials.md
@@ -1,7 +1,7 @@
---
title: "Environment 执行器凭证"
source: contracts/agents-api/environment-executor-credentials.md
-source_hash: 725257a92518431a4b942ea35187e37bb171f890d7797e843a9f4eb62f47ad4b
+source_hash: c929752dc1113b777308855ab416bc12aef07ae2d26df71c2e691cbef2832b8b
---
执行器凭证允许 `oac-daemon` 为一个 `self_hosted` Environment 注册并连接。它只授权该 Environment 的私有 daemon 传输(`/api/v1/agent-daemon/*`),不授权 `/v1`、`/core/v1`、sandbox node 注册或 Project 资源。Project 的 principal 是其执行 principal。Core 只保存密钥摘要。
@@ -37,7 +37,7 @@ grant 绑定 Environment、Session 创建者的 principal 和 Core 构建版本
| `POST /api/v1/agent-daemon/installation` | Grant | 固定绑定:`version`、`protocol_version`、`environment_id`、`remote_url`、`workspace_directory`、`harness` |
| `POST /api/v1/agent-daemon/installation/claim` | Grant | `{"executor_token":"SECRET"}`;204 |
-无效或过期的 grant 返回 401 `installation_authorization_invalid`。没有匹配安装器时,grant 路由返回 503 `installation_unavailable`。Core 用安装的 [`secrets/core/credential.key`](../../../docs/zh/configuration.md#compose-installations) 签名每个 grant;未配置 key 时,上述 Session 响应、Core-key 查询和 grant 路由返回 503 `credential_storage_unavailable`。格式错误的密钥返回 400。产物路由不携带凭证,grant 只发送给 Core,不发送给产物主机。
+无效或过期的 grant 返回 401 `installation_authorization_invalid`。没有匹配安装器时,grant 路由返回 503 `installation_unavailable`。Core 用安装的 [`secrets/core/credential.key`](../../../docs/zh/configuration.md#compose-installations) 签名每个 grant。格式错误的密钥返回 400。产物路由不携带凭证,grant 只发送给 Core,不发送给产物主机。
## Core-key 路由 {#core-key-routes}
diff --git a/contracts/agents-api/zh/model-execution.md b/contracts/agents-api/zh/model-execution.md
index ca4231c01..7072ecff6 100644
--- a/contracts/agents-api/zh/model-execution.md
+++ b/contracts/agents-api/zh/model-execution.md
@@ -1,7 +1,7 @@
---
title: "模型执行"
source: contracts/agents-api/model-execution.md
-source_hash: ee651cc7bb506eab33a819aa40a97095270574a793dea95784104f19692fa4ec
+source_hash: 92f743c0ada60d86a2a107eb6528a9c6d89e7db2e664f522a99ad3711a53f9e8
---
每个 Session 都运行一个 Harness,并使用一个模型提供商。Core 通过三个固定版本上游协议未定义的 Core 扩展来选择它们:`x_agents_core.harness` 选择 Harness,`x_agents_core.model_provider` 提供端点和密钥,`x_agents_core.harness_config` 携带原生模型参数。Core 没有提供商目录、模型别名解析或产品权限模型;除 Session 和已保存 Agent 配置包外,唯一存储的配置包是每个 Harness 的一个 [deployment default](#deployment-defaults)。本文档定义 Harness—模型提供商协议:[`internal/modelprovider/config.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/modelprovider/config.go) 负责验证冻结的提供商连接,每个 Harness 则通过 [`internal/harnessconfig/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/harness.go) 声明其协议和原生参数。
@@ -59,7 +59,7 @@ MiniMax Code 要求上下文限制和输出限制均为正数。Core 会在写
空的 Session 执行扩展无效。显式 null 提供商会请求继承;空的或不完整的提供商对象无效。已保存提供商配置中的未知字段、重复字段或只读输出字段均会被拒绝。没有 Harness 的已保存 Agent 可以保存有效配置包;其 Harness 兼容性会在 Session 准入时检查。仅更新提供商的 Agent 更新会保留已保存的 Harness,并在行锁保护下验证合并后的组合。Session 内联的 `agent.x_agents_core` 接受 `harness` 和 `harness_config`;提供商覆盖值必须放在请求顶层。
-Core 从同一个数据库快照读取 Agent 配置和加密配置包;显式提供完整 Session 覆盖值时,无需解密已保存的配置包。Session 自身的加密快照会与 Session 及其 Environment 原子写入。现有 Session 绝不会再次查询 Agent:Agent 编辑、密钥替换、删除、暂停和重启均无法改变其模型、Harness 或提供商。加密密钥缺失或错误时会安全失败;重启前后应保持相同的 [credential key](../../../docs/zh/configuration.md#compose-installations)。不存在 Turn 级覆盖。
+Core 从同一个数据库快照读取 Agent 配置和加密配置包;显式提供完整 Session 覆盖值时,无需解密已保存的配置包。Session 自身的加密快照会与 Session 及其 Environment 原子写入。现有 Session 绝不会再次查询 Agent:Agent 编辑、密钥替换、删除、暂停和重启均无法改变其模型、Harness 或提供商。加密密钥错误时会安全失败;重启前后应保持相同的 [credential key](../../../docs/zh/configuration.md#compose-installations)。不存在 Turn 级覆盖。
新的托管请求以及省略内联模型的请求,会在解析可变默认值之前记录调用方意图。其他内联请求,例如 `none`,继续遵循已解析请求的重试规则;该哈希不包含部署默认值,因此更改默认值不会改变其重试标识。匹配的创建重试会在再次解析 Agent 或提供商之前恢复已提交的 Session,并且不会进一步加入输入。流式传输不参与重试标识的计算。[TypeScript client](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/packages/agents-client/README.md#saved-agent-and-deployment-defaults) 展示了已保存 Agent 和部署默认值。
@@ -121,7 +121,7 @@ Core 会在写入 Session 前验证解析后的配置,并将其冻结在 Sessi
| `PUT /core/v1/harnesses/{harness}/model-configuration` | 使用共享的提供商和原生参数校验器替换 `{model_provider, model, harness_config}` |
| `DELETE /core/v1/harnesses/{harness}/model-configuration` | 移除配置;幂等,返回 204 |
-PUT 要求提供 `model` 和完整的 `model_provider`;`harness_config` 默认为 `{}`。读取操作返回 `model`、`harness_config`、安全的 `model_provider` 视图(`protocol`、`base_url`、可选的 token 限制和 `api_key_configured`)、observations 和 `updated_at`,绝不返回密钥。该配置包使用自己的加密用途加密并绑定到 Harness。每次写入都会记录一条管理员审计条目(`resource_type: deployment_model_provider`,Harness 作为 `resource_id`,操作为 `set` 或 `delete`,`project_id` 为 null),且不包含密钥。加密密钥缺失或错误时会安全失败:写操作以及需要使用默认值的 Session 创建都会返回 503 `credential_storage_unavailable`。
+PUT 要求提供 `model` 和完整的 `model_provider`;`harness_config` 默认为 `{}`。读取操作返回 `model`、`harness_config`、安全的 `model_provider` 视图(`protocol`、`base_url`、可选的 token 限制和 `api_key_configured`)、observations 和 `updated_at`,绝不返回密钥。该配置包使用自己的加密用途加密并绑定到 Harness。每次写入都会记录一条管理员审计条目(`resource_type: deployment_model_provider`,Harness 作为 `resource_id`,操作为 `set` 或 `delete`,`project_id` 为 null),且不包含密钥。在其他加密密钥下密封的默认值会安全失败:读取该默认值以及需要它的 Session 创建都会返回 500 `internal_error`,直到重新设置默认值。
创建 Session 时,Core 会解密解析后 Harness 的默认值,并像处理其他配置包一样将其冻结在 Session 的加密快照中,因此更改或移除默认值绝不会影响现有 Session。execution-configuration 读取结果会显示冻结的安全视图,其来源为 `deployment`。提供商快照缺失或无效时会安全失败,并且不会回退到其他模型或提供商。
diff --git a/contracts/agents-api/zh/runtime-observability.md b/contracts/agents-api/zh/runtime-observability.md
index b4ef0f415..223c463c9 100644
--- a/contracts/agents-api/zh/runtime-observability.md
+++ b/contracts/agents-api/zh/runtime-observability.md
@@ -1,7 +1,7 @@
---
title: "运行时可观测性"
source: contracts/agents-api/runtime-observability.md
-source_hash: d18a476b06248dedfa5630ccf0f8a29dff59677a1e161d1847cc7d05e0c48de8
+source_hash: 31fa597224d654f347c7f438bea78d8e548e7588849d4346c599aac8a8f8054c
---
这是面向贡献者的契约,规定 Core 如何观测 Runtime 并保留其历史。路由和响应字段见 [Runtime telemetry API](runtime-observability-api.md)。代码位于 `services/core/internal/runtimeobs`(解析、源、采样器和导出)、`internal/runtimehistory`(历史查询和 PostgreSQL 存储)以及 `internal/runtimeobs/otlpexporter`。
@@ -85,7 +85,7 @@ CPU 静默状态、心跳时龄、连接状态和保活时间都不是空闲时
### 周期采样 {#periodic-sampling}
-周期采集仅随执行工作器运行而执行(Core 需使用 `OAC_PUBLIC_URL` 启动;见 [Core environment](../../../docs/zh/configuration.md#appendix-core-environment-without-the-installer)),并受该工作器的数据库租约保护。未运行该工作器的 Core 不存储历史记录,所有历史读取都返回 503;当前读取在两种情况下均可正常工作。
+周期采集在执行工作器中运行,并受其数据库租约保护。
采样器在启动时扫描一次,此后每次扫描结束后再经过一个采样间隔再次扫描。一次扫描按 Session ID 顺序,对未删除、状态为 `openai_hosted` 且没有已释放分配的 Session 执行 keyset 扫描。它通过与当前读取相同的解析器和源,以 32 个 Session 为一页进行读取,并发数为 8,每个源时限为 2 秒。采样器在每页之前以及扫描期间每 100 ms 检查租约;失去所有权时取消进行中的读取;将每条记录交给导出之前再次检查租约。失败的行不会停止扫描;未完成的扫描会在下一个间隔重复。
diff --git a/contracts/agents-api/zh/sandbox-deployment.md b/contracts/agents-api/zh/sandbox-deployment.md
index 9e09f084b..f14dc75eb 100644
--- a/contracts/agents-api/zh/sandbox-deployment.md
+++ b/contracts/agents-api/zh/sandbox-deployment.md
@@ -1,7 +1,7 @@
---
title: "沙箱部署"
source: contracts/agents-api/sandbox-deployment.md
-source_hash: 2a6b114b7b4f324b2a68ee1f5bde5e9f229a2d44c06aa7c0dd4560bb4ed60166
+source_hash: f4ecc42b24dd85d2bfe3e054358aa2087331aa3110ba031bbc824cece58a54c3
---
沙箱部署为 Core 管理的 `openai_hosted` 执行选择 Sandbox Provider、每个沙箱的资源以及不可变的 Runtime 发行版。PostgreSQL 为每个安装维护一个当前有效选择;Web 和 Core API 写入同一配置。节点文件保存其已安装副本和特定于主机的路径,且不能覆盖其资源或 Runtime。该选择独立于 Harness;部署可以保持未配置状态,既无节点,也不接受托管准入。
@@ -219,7 +219,6 @@ POST 会在持久保存候选配置之前对其进行验证,并且不会创建
| 409 | `sandbox_deployment_conflict` | 更改无法应用于另一种状态 |
| 409 | `runtime_node_in_use` | 节点仍持有资源时移除节点 |
| 503 | `execution_unavailable` | 无法准备提供商 |
-| 503 | `credential_storage_unavailable` | Core 没有凭据加密密钥 |
存储和凭据故障始终作为错误处理:读取为空或读取失败绝不能证明清理完成。[机器连接 API](machine-api.md#node-route-errors)列出了节点路由的错误。
diff --git a/contracts/agents-api/zh/sessions-events.md b/contracts/agents-api/zh/sessions-events.md
index cb7230f99..99688def4 100644
--- a/contracts/agents-api/zh/sessions-events.md
+++ b/contracts/agents-api/zh/sessions-events.md
@@ -1,7 +1,7 @@
---
title: "会话、事件和历史"
source: contracts/agents-api/sessions-events.md
-source_hash: c6141811b426fb0dfb95105b27cc381af5f22e3a7923a171f113f228ae0a5b33
+source_hash: 93ace7d112cbc671039624ec9c999addce715bb08036a50246442a9e14549021
---
本契约涵盖会话(Session)内部发生的事情:发送输入、实时事件流,以及读取轮次(Turn)、条目(Item)和使用量的持久化历史。会话资源本身(创建配置、重试标识、更新、列出和删除)见 [Core 线协议行为](wire-semantics.md)。消息和函数结果内容见[消息内容](message-content.md)。[Agents API 指南](../../../docs/zh/api/public-agent-api.md)展示了使用 SDK 和 HTTP 的调用方式。
@@ -41,7 +41,7 @@ Turn 失败后会话仍可使用:新输入会启动一个新 Turn。后来预
- **取消。** 排队的 Turn 无需活动 Runtime 即可取消。正在运行的 Turn 只有在 Runtime 确认后才会取消;完成操作可能赢得该竞争。读取到 `cancelled` 时才表示该 Turn 已停止,而不是请求返回时。在没有待处理输入的情况下,对空闲会话执行的取消会被接受且不产生任何效果;而在输入预留待处理期间,取消会返回 409。
- **函数结果。** `turn_id`、`call_id` 和 `success` 为必填项;`output` 和 `error` 为可选项且可为空([内容规则](message-content.md#function-results))。重复提交完全相同的结果会返回 202,不会再次应用或发出事件。harness 应用结果时才会出现结果 Item;如果取消操作导致结果无法应用,结果仍会存储,但不会产生 Item。
- **排队。** 当一个已连接且支持该会话 harness 和配置的 Runtime 接入,并且 Core 的 [`core.execution_concurrency`](../../../docs/zh/configuration.md#settings) 工作槽位有一个空闲时,排队的 Turn 才会启动。会话始终绑定到首次运行它的 Runtime。
-- **执行可用性。** 不具备执行能力的服务会返回 503 `execution_unavailable`,失去执行所有权的 Worker 会返回 503。
+- **执行可用性。** Core 关闭期间,或其 Worker 失去执行所有权后,请求返回 503 `execution_unavailable`。
### 包含 Environment 的会话 {#sessions-with-an-environment}
diff --git a/contracts/agents-api/zh/vaults.md b/contracts/agents-api/zh/vaults.md
index 81434de67..fb5e55ee9 100644
--- a/contracts/agents-api/zh/vaults.md
+++ b/contracts/agents-api/zh/vaults.md
@@ -1,7 +1,7 @@
---
title: "Vault 与 Credential"
source: contracts/agents-api/vaults.md
-source_hash: 95626340ee36faee3418dd1155a0e50c378ead09c09c91e86f30e09bd8f409e0
+source_hash: fe706a5c3fd5f03ccfe25d0b075de3ad9360b8e0512b3303220fc9a707852181
---
Vault 是 Project 所有的 Credential 容器。Credential 保存一个 HTTPS MCP server 的秘密:`static_bearer` token 或 `mcp_oauth` grant。Session 在 `vault_ids` 中关联 Vault;Core 在创建 Session 时为每个 HTTP MCP server 选择一个 Credential,只在分派工作时将解密 token 交给 Runtime。秘密只能写入:任何读取都不返回 token、refresh token、client secret 或密文。
@@ -141,10 +141,10 @@ Token endpoint 必须为 HTTPS。Core 解析主机,拒绝回环、私有、链
Core 使用安装的 [`secrets/core/credential.key`](../../../docs/zh/configuration.md#compose-installations),以 AES-256-GCM 加密每个 token、refresh token 和 client secret,绑定 Project、Vault、Credential、auth type 和 `mcp_server_url`。错误密钥、修改的行或移动到其他绑定的行均无法解密。名称是不参与绑定的元数据。key 和明文 token 存在于可信服务内存中;加密保护存储的秘密,不保护已被攻破的服务主机。
-未配置 key 时,Credential 创建和替换在写入前返回 503 `credential_storage_unavailable`;读取、列表、删除和 Vault 操作仍可用。key 文件不可读或格式错误会使 Core 启动失败。丢失或替换 key 使全部已存储秘密无法使用;Core 只支持一个 key,不支持轮换或重新加密。
+key 文件不可读或格式错误会使 Core 启动失败。丢失或替换 key 使全部已存储秘密无法使用。读取、列表、删除、Vault 操作、新建 Credential 和替换 `static_bearer` token 仍可用;`mcp_oauth` 更新返回 500 `internal_error`,需要旧秘密的派发会失败。已保存的 E2B 密钥同样失效:托管 Session 返回 503 `execution_unavailable`,直到你打开 **System** → **Manage sandbox configuration**,选择 **Reset deployment**(必要时选择 **Force — cancel remaining work now**)并重新配置后端;遗留的沙箱按其 E2B 超时过期。Core 只支持一个 key,不支持轮换或重新加密。
## 删除 {#deletion}
-删除 Credential 或其 Vault 会移除凭证行,但不会擦除 PostgreSQL 页面、WAL、备份或原生历史中的秘密。此后查询、更新和重复删除返回 404;列表不再包含它;新 Session 不能选择它;不能在已删除 Vault 中创建新 Credential(缺失 storage key 时仍先返回 503)。
+删除 Credential 或其 Vault 会移除凭证行,但不会擦除 PostgreSQL 页面、WAL、备份或原生历史中的秘密。此后查询、更新和重复删除返回 404;列表不再包含它;新 Session 不能选择它;不能在已删除 Vault 中创建新 Credential。
现有 Session 保留固定的关联和选择,历史仍可读。下一次需要已删除 Credential 的分派失败;Core 不选择其他 Credential,也不匿名连接。删除不取消运行中的工作、不收回已发送 Runtime 的 token,也不在服务方撤销 grant;需要时自行取消 Session 并撤销 grant。已撤销或无效 OAuth grant 同样失败,直到被替换。
diff --git a/docs/configuration.md b/docs/configuration.md
index 1a248b7e3..50378039e 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -43,7 +43,7 @@ Model providers are not process settings; see [Default models](#default-models).
| Variable | Default | Meaning |
| --- | --- | --- |
-| `OAC_PUBLIC_URL` | `http://localhost:8080`, set by `compose.yaml`. Core started without it runs no Runtime gateway and executes no Sessions | Origin applications, nodes, sandboxes and self-hosted executors use. See [changing the public URL](#changing-the-public-url) |
+| `OAC_PUBLIC_URL` | Required; `compose.yaml` sets `http://localhost:8080` | Origin applications, nodes, sandboxes and self-hosted executors use. See [changing the public URL](#changing-the-public-url) |
| `OAC_HOST` | `127.0.0.1` | Web bind address published by `compose.yaml`. The installer sets `0.0.0.0` |
| `OAC_WEB_PORT` | `8080` | Host port of Web |
| `OAC_LOG_LEVEL` | `info` | `debug`, `info`, `warn` or `error` |
@@ -150,13 +150,13 @@ Core reads its process environment. Compose interpolates `.env` into it and moun
| Variable | Set from |
| --- | --- |
-| `OAC_PUBLIC_URL` | The [public URL](#settings). Core validates it once and derives the Agents API base, the daemon WebSocket URL, the self-hosted `remote_url`, the installer downloads, the hosted sandbox address and the deployment's read-only `core_url` from it, never from request headers |
+| `OAC_PUBLIC_URL` | Required. The [public URL](#settings). Core validates it once and derives the Agents API base, the daemon WebSocket URL, the self-hosted `remote_url`, the installer downloads, the hosted sandbox address and the deployment's read-only `core_url` from it, never from request headers |
| `OAC_ADDR` | The image sets `:8091`. Independently started Core defaults to `127.0.0.1:8091` when unset or empty |
| `OAC_DATABASE_URL` | Required. PostgreSQL without a password |
| `OAC_DATABASE_PASSWORD_FILE` | `/run/database/password`. The URL must then carry no password |
-| `OAC_CREDENTIAL_KEY_FILE` | `/run/oac/credential.key` |
+| `OAC_CREDENTIAL_KEY_FILE` | Required. `/run/oac/credential.key`: a base64-encoded random 32-byte key. Core seals stored credentials with it |
| `OAC_CORE_KEY_DIGESTS_FILE` | Required. `/run/oac/core-key-digests.json`: a JSON array with the SHA-256 of the Core key |
-| `OAC_INSTALLATION_ID_FILE` | `/run/oac/installation.id`: the installation ID, a canonical UUID. It enables the sandbox deployment and node routes and requires `OAC_PUBLIC_URL`. Core refuses an ID other than the one its database recorded |
+| `OAC_INSTALLATION_ID_FILE` | Required. `/run/oac/installation.id`: the installation ID, a canonical UUID. Core refuses an ID other than the one its database recorded |
| `OAC_EXECUTION_CONCURRENCY`, `OAC_DEFAULT_HARNESS`, `OAC_HARNESSES`, `OAC_WRITE_AUDIT_RETENTION`, `OAC_OAUTH_TRUSTED_ORIGINS`, `OAC_HISTORY_SETTINGS_FILE`, `OAC_LOG_LEVEL`, `OAC_LOG_FORMAT`, `OAC_LOG_ADD_SOURCE` | The matching [process settings](#settings). Web reads the three log settings too |
| `OAC_PROVIDER_ROOT` | Absolute adapter artifact root. The Core image sets `/opt/oac`. Each adapter owns its helper paths beneath this root. Core serves self-hosted daemon installers from its `native-installers/` directory when that holds a `catalog.json`, after checking the catalog against its own release. Adapter state lives at `/state`, the data volume's [`state/`](#compose-installations) |
diff --git a/docs/web/console-api-usage.md b/docs/web/console-api-usage.md
index 60c2b623f..84eb6fb27 100644
--- a/docs/web/console-api-usage.md
+++ b/docs/web/console-api-usage.md
@@ -86,7 +86,7 @@ Summary figures are cumulative per Session and are not billing records. Sessions
| Operation | Route | Console use |
| --- | --- | --- |
| List harnesses | `GET /core/v1/harnesses` | System's Default model cards: each harness's read-only `enabled` and `default`, its model configuration without the key, and Usage details from the configuration's `last_used_at`, `last_error_code` and `last_error_at`; the Overview's Getting started (a default model on the default harness, or on any enabled harness when none is default) |
-| Set or replace | `PUT /core/v1/harnesses/{harness}/model-configuration` | **Set** or **Replace**: the complete model configuration with its write-only provider key, never prefilled and never retried; a 400 shows Core's message in the form, and a 503 `credential_storage_unavailable` says Core has no credential encryption key; then the list is read again |
+| Set or replace | `PUT /core/v1/harnesses/{harness}/model-configuration` | **Set** or **Replace**: the complete model configuration with its write-only provider key, never prefilled and never retried; a 400 shows Core's message in the form; then the list is read again |
| Clear | `DELETE /core/v1/harnesses/{harness}/model-configuration` | **Clear**, confirmed, then the list is read again |
The list carries each harness's configuration, so the console does not read `GET /core/v1/harnesses/{harness}/model-configuration`.
diff --git a/docs/zh/configuration.md b/docs/zh/configuration.md
index fcde3f949..1c1bbea6e 100644
--- a/docs/zh/configuration.md
+++ b/docs/zh/configuration.md
@@ -1,7 +1,7 @@
---
title: "配置参考"
source: docs/configuration.md
-source_hash: 95519fc0cac3ac31f2cd08cdfadf20be92a03d998093a651411240c95d29176d
+source_hash: d4c7fc0fddf79b32ee628cc641df148d873380ecf62c59bb6b6a323750d0a41b
---
Core 安装的每项设置都恰好只有一个归属位置,分属以下三类:
@@ -47,7 +47,7 @@ Web 的 **System** 页面显示该安装的地址、默认模型和沙箱配置
| Variable | Default | Meaning |
| --- | --- | --- |
-| `OAC_PUBLIC_URL` | `http://localhost:8080`,由 `compose.yaml` 设置。未设置时启动的 Core 不运行 Runtime 网关,也不执行任何 Session | 应用、节点、沙箱和自托管执行器使用的源地址。参阅[更改公共 URL](#changing-the-public-url) |
+| `OAC_PUBLIC_URL` | 必填;`compose.yaml` 设为 `http://localhost:8080` | 应用、节点、沙箱和自托管执行器使用的源地址。参阅[更改公共 URL](#changing-the-public-url) |
| `OAC_HOST` | `127.0.0.1` | `compose.yaml` 发布的 Web 绑定地址。安装器设置为 `0.0.0.0` |
| `OAC_WEB_PORT` | `8080` | Host port of Web |
| `OAC_LOG_LEVEL` | `info` | `debug`, `info`, `warn` or `error` |
@@ -154,13 +154,13 @@ Core 读取进程环境。Compose 将 `.env` 插值到环境中,并把机密
| 变量 | 设置来源 |
| --- | --- |
-| `OAC_PUBLIC_URL` | [公共 URL](#settings)。Core 只校验一次,并从中派生 Agents API 基地址、守护进程 WebSocket URL、自托管 `remote_url`、安装程序下载地址、托管沙箱地址和部署的只读 `core_url`,绝不从请求标头派生 |
+| `OAC_PUBLIC_URL` | 必填。[公共 URL](#settings)。Core 只校验一次,并从中派生 Agents API 基地址、守护进程 WebSocket URL、自托管 `remote_url`、安装程序下载地址、托管沙箱地址和部署的只读 `core_url`,绝不从请求标头派生 |
| `OAC_ADDR` | 安装程序在容器中设置为 `:8091`。独立启动的 Core 在未设置或为空时,默认使用 `127.0.0.1:8091` |
| `OAC_DATABASE_URL` | 必填。不含密码的 PostgreSQL URL |
| `OAC_DATABASE_PASSWORD_FILE` | `/run/database/password`。此时 URL 不得包含密码 |
-| `OAC_CREDENTIAL_KEY_FILE` | `/run/oac/credential.key` |
+| `OAC_CREDENTIAL_KEY_FILE` | 必填。`/run/oac/credential.key`:Base64 编码的 32 字节随机密钥。Core 用它加密存储的凭据 |
| `OAC_CORE_KEY_DIGESTS_FILE` | 必填。`/run/oac/core-key-digests.json`:一个包含 Core 密钥 SHA-256 的 JSON 数组 |
-| `OAC_INSTALLATION_ID_FILE` | `/run/oac/installation.id`:安装 ID,采用规范 UUID 格式。它会启用沙箱部署和节点路由,并要求设置 `OAC_PUBLIC_URL`。如果 ID 与数据库记录的 ID 不一致,Core 会拒绝它 |
+| `OAC_INSTALLATION_ID_FILE` | 必填。`/run/oac/installation.id`:安装 ID,采用规范 UUID 格式。如果 ID 与数据库记录的 ID 不一致,Core 会拒绝它 |
| `OAC_EXECUTION_CONCURRENCY`、`OAC_DEFAULT_HARNESS`、`OAC_HARNESSES`、`OAC_WRITE_AUDIT_RETENTION`、`OAC_OAUTH_TRUSTED_ORIGINS`、`OAC_HISTORY_SETTINGS_FILE`、`OAC_LOG_LEVEL`、`OAC_LOG_FORMAT`、`OAC_LOG_ADD_SOURCE` | 对应的[进程设置](#settings)。Web 也读取三个日志设置 |
| `OAC_PROVIDER_ROOT` | 适配器构件的绝对根目录。Core 镜像设置为 `/opt/oac`。每个适配器都拥有此根目录下的辅助路径。当其中的 `native-installers/` 目录包含 `catalog.json` 时,Core 在核对该目录清单与自身发行版后提供自托管守护进程安装程序。适配器状态位于 `/state`,即数据卷的 [`state/`](#compose-installations) |
diff --git a/docs/zh/web/console-api-usage.md b/docs/zh/web/console-api-usage.md
index 02d8896f6..b0031fb65 100644
--- a/docs/zh/web/console-api-usage.md
+++ b/docs/zh/web/console-api-usage.md
@@ -1,7 +1,7 @@
---
title: "控制台 API 使用"
source: docs/web/console-api-usage.md
-source_hash: 071c5b80262cdc89ed3517cf6a63d1c657ee8d0f98133630bad0850595003a2c
+source_hash: 3f233005176090ef87c13442f439296001950bb835262969a0b48970b51af7f4
---
本页列出各控制台页面读取和写入的 Core 路由,以及控制台如何限定读取范围。[administrator API contract](../../../contracts/agents-api/zh/admin-api.md) 定义了路由、响应结构、分页和审计记录;[API namespaces and credentials](../api/index.md) 定义了本文使用的术语。
@@ -88,7 +88,7 @@ source_hash: 071c5b80262cdc89ed3517cf6a63d1c657ee8d0f98133630bad0850595003a2c
| 操作 | 路由 | 控制台用途 |
| --- | --- | --- |
| 列出 Harnesses | `GET /core/v1/harnesses` | System 的 Default model 卡片:每个 Harness 的只读 `enabled` 和 `default`、不含密钥的模型配置,以及来自配置中 `last_used_at`、`last_error_code` 和 `last_error_at` 的 Usage details;Overview 的 Getting started(默认 Harness 上的默认模型;如果没有默认模型,则为任意已启用 Harness 上的默认模型) |
-| 设置或替换 | `PUT /core/v1/harnesses/{harness}/model-configuration` | **Set** 或 **Replace**:提交包含只写提供商密钥的完整模型配置;该密钥绝不预填,写入也绝不重试;400 会在表单中显示 Core 的消息,503 `credential_storage_unavailable` 表示 Core 没有凭据加密密钥;随后再次读取列表 |
+| 设置或替换 | `PUT /core/v1/harnesses/{harness}/model-configuration` | **Set** 或 **Replace**:提交包含只写提供商密钥的完整模型配置;该密钥绝不预填,写入也绝不重试;400 会在表单中显示 Core 的消息;随后再次读取列表 |
| 清除 | `DELETE /core/v1/harnesses/{harness}/model-configuration` | **Clear**,需确认,随后再次读取列表 |
列表会返回每个 Harness 的配置,因此控制台不会读取 `GET /core/v1/harnesses/{harness}/model-configuration`。
diff --git a/packages/agents-client/src/admin-client.test.ts b/packages/agents-client/src/admin-client.test.ts
index 9b0d05466..8884b4328 100644
--- a/packages/agents-client/src/admin-client.test.ts
+++ b/packages/agents-client/src/admin-client.test.ts
@@ -331,7 +331,7 @@ describe("AdminClient installation", () => {
};
it("reads installation facts before any deployment and rejects inconsistent snapshots", async () => {
expect(await clientWith(installation).client.retrieveInstallation()).toEqual(installation);
- expect(await clientWith({ ...installation, installation_id: null, public_url: null, api_base_url: null, source_commit: null }).client.retrieveInstallation()).toMatchObject({ public_url: null });
+ expect(await clientWith({ ...installation, source_commit: null }).client.retrieveInstallation()).toMatchObject({ source_commit: null });
const configuration = (settings: unknown[]) => ({ ...installation, configuration: { settings } });
for (const invalid of [
configuration([port, { ...headers, value: { authorization: "leak" } }]),
@@ -340,6 +340,8 @@ describe("AdminClient installation", () => {
{ ...installation, address_bindings: { ...installation.address_bindings, nodes_on_other_address: 3 } },
{ ...installation, token: "leak" },
{ ...installation, configuration: null },
+ { ...installation, installation_id: null },
+ { ...installation, public_url: null, api_base_url: null },
configuration([{ ...port, configured: true }]),
]) {
await expect(clientWith(invalid).client.retrieveInstallation()).rejects.toMatchObject({ code: "invalid_admin_response" });
diff --git a/packages/agents-client/src/admin-projection.ts b/packages/agents-client/src/admin-projection.ts
index 206502a28..28051cae1 100644
--- a/packages/agents-client/src/admin-projection.ts
+++ b/packages/agents-client/src/admin-projection.ts
@@ -269,8 +269,8 @@ function projectInstallationSetting(value: unknown): CoreInstallationSetting {
export function projectInstallation(value: unknown): CoreInstallation {
const installation = record(value, ["object", "installation_id", "public_url", "api_base_url", "local_only", "source_commit", "configuration", "address_bindings"]);
const origin = installation.public_url;
- if (installation.object !== "core.installation" || (installation.installation_id !== null && canonicalUuid(installation.installation_id) === null) ||
- (origin !== null && typeof origin !== "string") || installation.api_base_url !== (typeof origin === "string" ? `${origin}/v1` : null) ||
+ if (installation.object !== "core.installation" || canonicalUuid(installation.installation_id) === null ||
+ typeof origin !== "string" || installation.api_base_url !== `${origin}/v1` ||
typeof installation.local_only !== "boolean" ||
(installation.source_commit !== null && (typeof installation.source_commit !== "string" || !/^[0-9a-f]{40}$/.test(installation.source_commit)))) return invalidAdminResponse();
const bindings = record(installation.address_bindings, ["nodes", "nodes_on_other_address", "hosted_sandboxes", "self_hosted_executors"]);
diff --git a/packages/agents-client/src/admin-types.ts b/packages/agents-client/src/admin-types.ts
index 6949f9c4c..815592d83 100644
--- a/packages/agents-client/src/admin-types.ts
+++ b/packages/agents-client/src/admin-types.ts
@@ -194,11 +194,11 @@ export interface CoreInstallationConfiguration {
/** `GET /core/v1/installation`: available before any sandbox deployment exists. */
export interface CoreInstallation {
object: "core.installation";
- installation_id: string | null;
- /** The origin applications, nodes, sandboxes and self-hosted executors use; null when Core runs without one. */
- public_url: string | null;
+ installation_id: string;
+ /** The origin applications, nodes, sandboxes and self-hosted executors use. */
+ public_url: string;
/** `public_url` followed by `/v1`; the base URL for application API keys. */
- api_base_url: string | null;
+ api_base_url: string;
/** True when `public_url` is a loopback origin that only the Core host reaches. */
local_only: boolean;
/** Full source commit Core was built from; null for development builds. */
diff --git a/services/core/IMPLEMENTATION.md b/services/core/IMPLEMENTATION.md
index e609040ff..86892c4c7 100644
--- a/services/core/IMPLEMENTATION.md
+++ b/services/core/IMPLEMENTATION.md
@@ -6,15 +6,15 @@ These are the code-level rules of `services/core` that no contract states. Contr
The domain packages own their vocabulary, pure rules and use cases: the domain owners listed below, `items`, `adminaudit`, `writeaudit`, and the shared vocabulary packages `environmentconfig`, `metadata` and `jsonobject`. No `internal` package except `persistence` and `db` imports an `internal/persistence`, `internal/db` or pgx package: they reach storage only through interfaces that the PostgreSQL adapters under `internal/persistence/postgres` implement and the commands under `cmd/` wire in. `TestLayering` in `cmd/server` checks these imports.
-`api.NewHandler` takes one `api.Dependencies` value, built only in `cmd/server`. Each application area is one field typed as an interface declared in `api` beside its handlers, listing exactly the methods they call. Every field is required and `NewHandler` rejects a missing one, except the optional groups whose comments say what nil means: `Execution` is nil without an execution Worker, `Sandboxes` is nil without a managed sandbox installation and requires `Execution`, and `Execution.NativeInstaller` is nil for a build without a source revision. Handlers never discover a capability by type assertion or fall back to another implementation. API tests use one strict fake per area, `fake `, which fails the test on any call the test did not set.
+`api.NewHandler` takes one `api.Dependencies` value, built only in `cmd/server`. Each application area is one field typed as an interface declared in `api` beside its handlers, listing exactly the methods they call. Every field is required and `NewHandler` rejects a missing one, except `Execution.NativeInstaller`, which is nil for a build without a source revision. Handlers never discover a capability by type assertion or fall back to another implementation. API tests use one strict fake per area, `fake `, which fails the test on any call the test did not set.
`internal/persistence/postgres/pgunit` owns the PostgreSQL mechanics that adapters share: pooled read-write and snapshot transactions; pool-bound queries, used only for a single-statement read that needs no transaction, such as the per-request key lookup; the execution lease (its dedicated connection and gate, the ownership check, the cancellation fence, close, and the execution deadline); identifier parsing (`ParseID`, `PathID`, `LookupCursor`); and detection of text PostgreSQL cannot store (`IsUnstorableText`). Persistence code runs every transaction through it. Outside `persistence`, only the commands under `cmd/`, which build the adapters' pool, and test fixtures import it; `cmd/server` also acquires the lease. `internal/persistence/postgres/pgtest` is test support: it opens the dedicated test database under the `oac_*_tests` guard, applies the migrations, and creates isolated databases for database-wide state such as the execution lease. Only test files import it.
`internal/persistence/postgres/auditpg` is the one adapter that other adapters call directly. Audit rows are written inside the business transaction, so each adapter calls `RecordWriteAudit`, `RecordAdminMutation` or `RecordDeploymentMutation` with its own transaction's queries; the audit provenance travels in the context. `writeaudit` and `adminaudit` own the sources, their validation, `ErrInvalidSource` and the read models, and `auditpg.Store` serves the audit reads.
-The adapter that stores a secret seals and opens it with the credential key `cmd/server` builds it with: domain storage interfaces carry plaintext, domain service constructors never take the key, a missing key is `credentialcrypto.ErrUnavailable`, and a ciphertext that fails to open or authenticate is an internal error, never a missing key, value or row.
+The adapter that stores a secret seals and opens it with the credential key `cmd/server` builds it with: domain storage interfaces carry plaintext, domain service constructors never take the key, and a ciphertext that fails to open or authenticate is an internal error, never a missing key, value or row.
-Two errors are shared across domains, each with one `api` helper: `textvalue.ErrUnstorable` (400, `writeTextValueError`) for text PostgreSQL cannot store, and `credentialcrypto.ErrUnavailable` (503, `writeCredentialUnavailableError`) for a missing credential key. The audit `ErrInvalidSource` errors pass through adapters unchanged, and `writeAuditSourceError` maps both to 400. Each handler maps its operation's errors through that domain's mapper; Session operations that also meet the sandbox deployment, namely creation, input admission, archive and Runtime observation, map through `writeOperationError`, which tries `writeSandboxError` and then `writeSessionsError`.
+One error is shared across domains, with one `api` helper: `textvalue.ErrUnstorable` (400, `writeTextValueError`) for text PostgreSQL cannot store. The audit `ErrInvalidSource` errors pass through adapters unchanged, and `writeAuditSourceError` maps both to 400. Each handler maps its operation's errors through that domain's mapper; Session operations that also meet the sandbox deployment, namely creation, input admission, archive and Runtime observation, map through `writeOperationError`, which tries `writeSandboxError` and then `writeSessionsError`.
Shared vocabulary has one owner each, and domains use it rather than copy it. `internal/environmentconfig` owns Environment setup, Skills, Plugins and initial files with their validation and public metadata; `Setup.Validate` checks requested configuration, where a Skill may be an unresolved reference, and `Setup.ValidateInstalled` checks frozen, installable configuration. `internal/skills` owns `ParseVersion`, the canonical positive decimal Skill version. `internal/metadata` owns the metadata rules: `Validate` for the pair, key and value limits and U+0000, `ValidateStorable` for U+0000 alone, and `Encode` with its 64 KiB bound. `internal/jsonobject` owns `Normalize`, the stable encoding of stored JSON objects that snapshots and retry identities compare.
@@ -131,7 +131,7 @@ Provider input validation uses the adapter rules in `internal/harnessconfig`: on
[Vaults and credentials](../../contracts/agents-api/vaults.md) describes the resources, selection rules, refresh and deletion. `vaults` implements them, with `vaultpg` as its storage, under these rules:
- Credentials are children of tenant-owned Vaults. Creation admits the owner in the same SQL statement as the insert; retrieval joins the owning Vault; listing enforces Project and Vault ownership on the parent, cursor and row query. Metadata queries never select ciphertext and need no encryption key.
-- `vaultpg` seals secret values before they reach SQL, with Core's separately configured random 32-byte key and the standard library's random-nonce AES-GCM. The versioned authenticated binding covers tenant, Vault, Credential, authentication purpose and exact destination. Never reuse daemon transport encryption for this storage. A missing key disables credential writes with `credentialcrypto.ErrUnavailable`; a malformed configured key fails startup.
+- `vaultpg` seals secret values before they reach SQL, with Core's separately configured random 32-byte key and the standard library's random-nonce AES-GCM. The versioned authenticated binding covers tenant, Vault, Credential, authentication purpose and exact destination. Never reuse daemon transport encryption for this storage. A missing, unreadable or malformed key fails startup.
- A static replacement is one SQL mutation scoped by tenant, Vault, Credential, auth type and destination, reusing the safe metadata for the immutable binding; it never decrypts the previous token, and a failed write keeps the old row.
- OAuth refresh and replacement serialize on the Credential row lock, authenticate the stored grant metadata against its encrypted copy before using an endpoint, and persist the refreshed grant before returning an access token, so a stale refresh cannot undo a deletion or Vault cascade.
- Credential deletion is one mutation checked by tenant, Vault and ID; Vault deletion removes the parent and its Credentials through the foreign-key cascade in one SQL statement, without decrypting, needing the key or calling providers.
@@ -181,7 +181,7 @@ Item merging never mutates the incoming observation or the previous snapshot: pu
## Worker ownership
-Enabling the daemon gateway with `OAC_PUBLIC_URL` also starts the execution Worker. One Worker owns an execution database through a `pgunit.Lease`: the PostgreSQL advisory lock held by one dedicated connection. A second Core on the same database cannot acquire the lease and starts no Worker. The Worker's execution writer runs every Session transaction on that connection: binding, claim and reconciliation, journal, Items and usage, function callbacks and receipts, and terminal state. The lease gate serializes these short transactions and the ownership pings, and `pgunit.ExecutionTimeout` (five seconds) bounds each one, including its gate and Session-lock waits. Cancelling an in-flight pgx operation can close the connection that owns the lock, so coordinator-owned work is cancelled only through the lease's cancellation fence, between leased operations. Never hold a transaction across daemon or model work, reconnect the writer or fall back to the pool after losing the lease. Public admission and device maintenance use pooled connections, and pooled reads grant no write authority. Transactions state their isolation: read committed for writes, read-only repeatable read for snapshots.
+Core always starts the execution Worker. One Worker owns an execution database through a `pgunit.Lease`: the PostgreSQL advisory lock held by one dedicated connection. A second Core on the same database cannot acquire the lease and fails to start with `pgunit.ErrLeaseHeld`. The Worker's execution writer runs every Session transaction on that connection: binding, claim and reconciliation, journal, Items and usage, function callbacks and receipts, and terminal state. The lease gate serializes these short transactions and the ownership pings, and `pgunit.ExecutionTimeout` (five seconds) bounds each one, including its gate and Session-lock waits. Cancelling an in-flight pgx operation can close the connection that owns the lock, so coordinator-owned work is cancelled only through the lease's cancellation fence, between leased operations. Never hold a transaction across daemon or model work, reconnect the writer or fall back to the pool after losing the lease. Public admission and device maintenance use pooled connections, and pooled reads grant no write authority. Transactions state their isolation: read committed for writes, read-only repeatable read for snapshots.
Sandbox reset snapshots bind the deployment relation explicitly to its single row before joining resources, so that even on a fresh database without statistics an inflated join estimate cannot trigger JIT compilation inside the lease deadline.
diff --git a/services/core/README.md b/services/core/README.md
index 3ca7b19ec..d8b3d8623 100644
--- a/services/core/README.md
+++ b/services/core/README.md
@@ -22,19 +22,23 @@ Core uses its own PostgreSQL database and account and shares no tables with an a
1. Create a development database. Core applies the migrations when it starts.
-2. Create a Core key of at least 32 characters and a digest file holding its SHA-256, which Core uses to authenticate `/core/v1`:
+2. Create a Core key of at least 32 characters and a digest file holding its SHA-256, which Core uses to authenticate `/core/v1`, the credential key that seals stored credentials, and the installation ID. Keep the credential key and the ID with the database:
```sh
umask 077; mkdir -p ~/.oac/dev
openssl rand -hex 32 > ~/.oac/dev/core.key
printf '["%s"]\n' "$(tr -d '\n' < ~/.oac/dev/core.key | sha256sum | cut -d' ' -f1)" > ~/.oac/dev/core-key-digests.json
+ openssl rand -base64 32 > ~/.oac/dev/credential.key
+ uuidgen | tr '[:upper:]' '[:lower:]' > ~/.oac/dev/installation.id
```
-3. Start Core. `OAC_PUBLIC_URL` enables the Runtime gateway and the Worker; without it Core executes nothing. The [Core environment table](../../docs/configuration.md#appendix-core-environment-without-the-installer) lists every variable.
+3. Start Core. The [Core environment table](../../docs/configuration.md#appendix-core-environment-without-the-installer) lists every variable.
```sh
OAC_DATABASE_URL='postgres://oac:…@127.0.0.1:5432/oac_dev' \
OAC_CORE_KEY_DIGESTS_FILE="$HOME/.oac/dev/core-key-digests.json" \
+ OAC_CREDENTIAL_KEY_FILE="$HOME/.oac/dev/credential.key" \
+ OAC_INSTALLATION_ID_FILE="$HOME/.oac/dev/installation.id" \
OAC_PUBLIC_URL=http://127.0.0.1:8091 \
go run ./services/core/cmd/server
```
diff --git a/services/core/cmd/server/core_metrics.go b/services/core/cmd/server/core_metrics.go
index 52f81462a..74b92a0cb 100644
--- a/services/core/cmd/server/core_metrics.go
+++ b/services/core/cmd/server/core_metrics.go
@@ -26,16 +26,11 @@ type coreMetricsSource struct {
func metricPtr[T any](value T) *T { return &value }
func (s *coreMetricsSource) Live() coremetrics.Live {
stat := s.pool.Stat()
- live := coremetrics.Live{Pool: coremetrics.Pool{InUse: metricPtr(int64(stat.AcquiredConns())), Idle: metricPtr(int64(stat.IdleConns())), Max: metricPtr(int64(stat.MaxConns()))}, Scheduler: coremetrics.Job{ID: "scheduler", Status: "stopped"}, ExecutionOwner: metricPtr(false), SlotsTotal: metricPtr(int64(0)), SlotsInUse: metricPtr(int64(0))}
- if s.registry != nil {
- live.ConnectedDaemons = metricPtr(int64(len(s.registry.Devices())))
- }
- if s.worker != nil {
- w := s.worker.MetricsSnapshot()
- live.SlotsInUse, live.SlotsTotal, live.ExecutionOwner = w.SlotsInUse, w.SlotsTotal, w.ExecutionOwner
- live.Scheduler = coremetrics.Job{ID: "scheduler", Status: w.Scheduler.Status, LastRunAt: w.Scheduler.LastRunAt, Processed: w.Scheduler.Processed, Failed: w.Scheduler.Failed}
- }
- return live
+ w := s.worker.MetricsSnapshot()
+ return coremetrics.Live{Pool: coremetrics.Pool{InUse: metricPtr(int64(stat.AcquiredConns())), Idle: metricPtr(int64(stat.IdleConns())), Max: metricPtr(int64(stat.MaxConns()))},
+ Scheduler: coremetrics.Job{ID: "scheduler", Status: w.Scheduler.Status, LastRunAt: w.Scheduler.LastRunAt, Processed: w.Scheduler.Processed, Failed: w.Scheduler.Failed},
+ ExecutionOwner: w.ExecutionOwner, SlotsTotal: w.SlotsTotal, SlotsInUse: w.SlotsInUse,
+ ConnectedDaemons: metricPtr(int64(len(s.registry.Devices())))}
}
func (s *coreMetricsSource) Sample(ctx context.Context) coremetrics.Sample {
sample := coremetrics.Sample{Healthy: true, PoolInUse: metricPtr(int64(s.pool.Stat().AcquiredConns()))}
@@ -48,19 +43,12 @@ func (s *coreMetricsSource) Sample(ctx context.Context) coremetrics.Sample {
} else {
sample.Healthy = false
}
- devices := []string{}
- if s.registry != nil {
- devices = s.registry.Devices()
- }
- counts, err := s.store.ReadExecutionSnapshot(ctx, time.Now(), devices)
+ counts, err := s.store.ReadExecutionSnapshot(ctx, time.Now(), s.registry.Devices())
if err != nil {
sample.Healthy = false
} else {
sample.Queued, sample.InProgress = metricPtr(counts.QueuedTurns), metricPtr(counts.InProgressTurns)
- sample.OldestQueuedSeconds = counts.OldestQueuedSeconds
- if s.registry != nil {
- sample.WaitingForDaemon = metricPtr(counts.WaitingForDaemon)
- }
+ sample.OldestQueuedSeconds, sample.WaitingForDaemon = counts.OldestQueuedSeconds, metricPtr(counts.WaitingForDaemon)
}
size, err := s.store.ReadDatabaseSize(ctx)
if err != nil {
diff --git a/services/core/cmd/server/executor_connections.go b/services/core/cmd/server/executor_connections.go
index bd38a0dcc..45e480e7d 100644
--- a/services/core/cmd/server/executor_connections.go
+++ b/services/core/cmd/server/executor_connections.go
@@ -9,7 +9,6 @@ import (
)
// executorConnections observes enrolled executors through the Runtime gateway.
-// registry is nil when this Core has no gateway; no executor is then connected.
type executorConnections struct {
sessions sessions.Reader
registry *runtimegateway.Registry
diff --git a/services/core/cmd/server/http_routes.go b/services/core/cmd/server/http_routes.go
index 536bdd549..3154facff 100644
--- a/services/core/cmd/server/http_routes.go
+++ b/services/core/cmd/server/http_routes.go
@@ -19,9 +19,6 @@ type daemonRoutes struct {
// the exact daemon prefix /api/v1/agent-daemon to /api/v1/agent-daemon/. The API
// handler canonicalizes again when served alone; the operation is idempotent.
func serverHandler(apiHandler http.Handler, daemon *daemonRoutes) http.Handler {
- if daemon == nil {
- return apiHandler
- }
mux := http.NewServeMux()
mux.Handle("/api/v1/agent-daemon/", daemon.gateway)
mux.Handle("/api/v1/agent-daemon/enroll", daemon.enrollment)
@@ -29,9 +26,7 @@ func serverHandler(apiHandler http.Handler, daemon *daemonRoutes) http.Handler {
mux.Handle("/api/v1/agent-daemon/install/", apiHandler)
mux.Handle("/api/v1/agent-daemon/installation", apiHandler)
mux.Handle("/api/v1/agent-daemon/installation/", apiHandler)
- if daemon.nodeConnect != nil {
- mux.Handle("/api/v1/sandbox-node/connect", daemon.nodeConnect)
- }
+ mux.Handle("/api/v1/sandbox-node/connect", daemon.nodeConnect)
mux.Handle("/", apiHandler)
return api.CanonicalPaths(mux)
}
diff --git a/services/core/cmd/server/http_routes_test.go b/services/core/cmd/server/http_routes_test.go
index f31600030..7458a372a 100644
--- a/services/core/cmd/server/http_routes_test.go
+++ b/services/core/cmd/server/http_routes_test.go
@@ -108,14 +108,14 @@ func daemonComposition(t testing.TB) http.Handler {
SessionAdmin: struct{ api.SessionAdmin }{}, Environments: struct{ api.Environments }{}, EnvironmentsReader: struct{ api.EnvironmentsReader }{}, ExecutorConnections: struct{ api.ExecutorConnections }{},
Admin: struct{ api.Admin }{}, AdminAudit: struct{ api.AdminAudit }{}, WriteAudit: struct{ api.WriteAudit }{}, Metrics: struct{ api.Metrics }{},
RuntimeObservations: struct{ api.RuntimeObservations }{}, RuntimeHistory: struct{ api.RuntimeHistory }{},
- Execution: &api.Execution{
+ Execution: api.Execution{
ExecutorURL: "wss://core.example/api/v1/agent-daemon/ws",
SessionAdmission: struct{ api.SessionAdmission }{},
InputAdmission: struct{ api.InputAdmission }{},
SessionArchive: struct{ api.SessionArchive }{},
Workspaces: struct{ api.EnvironmentWorkspaces }{},
},
- Sandboxes: &api.Sandboxes{Deployment: struct{ api.Deployment }{}, NodeAllocations: unusedNodeAllocations{}, DeploymentChanges: struct{ api.DeploymentChanges }{},
+ Sandboxes: api.Sandboxes{Deployment: struct{ api.Deployment }{}, NodeAllocations: unusedNodeAllocations{}, DeploymentChanges: struct{ api.DeploymentChanges }{},
DeploymentReset: struct{ api.DeploymentReset }{}, ConfigurationDiscovery: struct{ api.ConfigurationDiscovery }{}},
})
if err != nil {
diff --git a/services/core/cmd/server/installation.go b/services/core/cmd/server/installation.go
index 11097db7f..1bab2cae5 100644
--- a/services/core/cmd/server/installation.go
+++ b/services/core/cmd/server/installation.go
@@ -13,14 +13,9 @@ var sourceCommit = regexp.MustCompile(`^[0-9a-f]{40}$`)
// installationFacts reports what GET /core/v1/installation serves: Core's own
// environment and build, plus the process settings it loaded.
func installationFacts(config processconfig.Config) api.Installation {
- facts := api.Installation{Configuration: api.InstallationConfiguration{Settings: config.Settings()}}
- if config.InstallationID != "" {
- facts.InstallationID = &config.InstallationID
- }
- if origin := config.PublicOrigin; origin != nil {
- public, base := origin.String(), origin.API()
- facts.PublicURL, facts.APIBaseURL, facts.LocalOnly = &public, &base, placement.LoopbackOrigin(public)
- }
+ public := config.PublicOrigin.String()
+ facts := api.Installation{InstallationID: config.InstallationID, PublicURL: public, APIBaseURL: config.PublicOrigin.API(), LocalOnly: placement.LoopbackOrigin(public),
+ Configuration: api.InstallationConfiguration{Settings: config.Settings()}}
if sourceCommit.MatchString(buildRevision) {
revision := buildRevision
facts.SourceCommit = &revision
diff --git a/services/core/cmd/server/main.go b/services/core/cmd/server/main.go
index f45a3a2f9..a14ba9891 100644
--- a/services/core/cmd/server/main.go
+++ b/services/core/cmd/server/main.go
@@ -59,7 +59,6 @@ import (
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/projects"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtime"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimeenrollment"
- "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimehistory"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimeobs"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/providers"
@@ -150,13 +149,9 @@ func run(config processconfig.Config) error {
return err
}
sandboxProviders := providers.Builtin()
- var public string
- if config.PublicOrigin != nil {
- public = config.PublicOrigin.String()
- }
// The placement rules are built once: the provider declarations and the
// public URL never change while Core runs.
- placementRules, err := placement.NewRules(sandboxProviders, public)
+ placementRules, err := placement.NewRules(sandboxProviders, config.PublicOrigin.String())
if err != nil {
return err
}
@@ -170,26 +165,18 @@ func run(config processconfig.Config) error {
if err != nil {
return err
}
- var workerDone chan error
+ // Node callbacks run only once the HTTP server serves, after the Worker starts.
var worker *execution.Worker
managedNodes := configureManagedNodes(deploymentService, deploymentStore, sandboxProviders, config, func(ctx context.Context) error {
- if worker == nil {
- return errors.New("sandbox execution owner is unavailable")
- }
return worker.CheckOwnership(ctx)
})
- defer managedNodes.close()
- var managed *execution.RuntimeProvider
- var observationSource func(context.Context) (runtimeobs.Source, string, error)
- if managedNodes != nil {
- managed = managedNodes.runtime
- observationSource = managedNodes.setup.observationSource
- }
+ defer managedNodes.hub.Close()
+ observationSource := managedNodes.setup.observationSource
observationResolver, err := deployment.NewObservationResolver(sessionStore, deploymentStore)
if err != nil {
return err
}
- history, err := runtimeHistory(ctx, units, config.RuntimeHistory, config.PublicOrigin != nil)
+ history, err := runtimeHistory(ctx, units, config.RuntimeHistory)
if err != nil {
return err
}
@@ -217,90 +204,79 @@ func run(config processconfig.Config) error {
if err != nil {
return err
}
- var daemonHandler http.Handler
- var registry *runtimegateway.Registry
- var executorURL string
- var nativeInstaller *api.NativeInstaller
- if origin := config.PublicOrigin; origin != nil {
- executorURL = origin.DaemonWebSocket()
- daemonHandler, registry, err = runtime.NewGateway(sessionStore, sessionService, sessionStore, executorURL)
- if err != nil {
- return err
- }
- defer runtime.CloseConnections(registry)
- var catalog *nativeinstaller.Catalog
- if config.NativeInstallers != "" {
- catalog, err = nativeinstaller.Load(config.NativeInstallers, buildRevision)
- if err != nil {
- return err
- }
- }
- if buildRevision != "" {
- nativeInstaller = &api.NativeInstaller{Version: buildRevision, Base: origin.InstallerBase(), Catalog: catalog}
- }
+ executorURL := config.PublicOrigin.DaemonWebSocket()
+ daemonHandler, registry, err := runtime.NewGateway(sessionStore, sessionService, sessionStore, executorURL)
+ if err != nil {
+ return err
}
- var deploymentExecution *deployment.ExecutionOperations
- if registry != nil {
- dispatcher := &execution.Dispatcher{Registry: registry,
- Credentials: vaultService, Observer: modelConfigurationStore, Deployment: deploymentService, DeploymentReader: deploymentStore,
- Sessions: sessionService,
- SessionsReader: sessionStore,
- ManagedRuntimes: managed, MaxConcurrentExecutions: config.ExecutionConcurrency}
- lease, err := pgunit.AcquireLease(ctx, pool)
+ defer runtime.CloseConnections(registry)
+ var catalog *nativeinstaller.Catalog
+ if config.NativeInstallers != "" {
+ catalog, err = nativeinstaller.Load(config.NativeInstallers, buildRevision)
if err != nil {
return err
}
- deploymentExecution, err = deployment.NewExecutionOperations(deploymentService, deploymentpg.NewExecution(lease, credentialKey))
- if err != nil {
- return errors.Join(err, lease.Close(ctx))
- }
- sessionExecution, err := sessions.NewExecutionOperations(sessionpg.NewExecution(lease))
- if err != nil {
- return errors.Join(err, lease.Close(ctx))
- }
- // From this call on the Worker closes the lease, even when it fails to start.
- worker, err = execution.StartWorker(ctx, dispatcher, execution.Owner{
- Lease: lease,
- Deployment: deploymentExecution,
- Sessions: sessionExecution,
- })
- if err != nil {
- return err
+ }
+ var nativeInstaller *api.NativeInstaller
+ if buildRevision != "" {
+ nativeInstaller = &api.NativeInstaller{Version: buildRevision, Base: config.PublicOrigin.InstallerBase(), Catalog: catalog}
+ }
+ dispatcher := &execution.Dispatcher{Registry: registry,
+ Credentials: vaultService, Observer: modelConfigurationStore, Deployment: deploymentService, DeploymentReader: deploymentStore,
+ Sessions: sessionService,
+ SessionsReader: sessionStore,
+ ManagedRuntimes: managedNodes.runtime, MaxConcurrentExecutions: config.ExecutionConcurrency}
+ lease, err := pgunit.AcquireLease(ctx, pool)
+ if err != nil {
+ return err
+ }
+ deploymentExecution, err := deployment.NewExecutionOperations(deploymentService, deploymentpg.NewExecution(lease, credentialKey))
+ if err != nil {
+ return errors.Join(err, lease.Close(ctx))
+ }
+ sessionExecution, err := sessions.NewExecutionOperations(sessionpg.NewExecution(lease))
+ if err != nil {
+ return errors.Join(err, lease.Close(ctx))
+ }
+ // From this call on the Worker closes the lease, even when it fails to start.
+ worker, err = execution.StartWorker(ctx, dispatcher, execution.Owner{
+ Lease: lease,
+ Deployment: deploymentExecution,
+ Sessions: sessionExecution,
+ })
+ if err != nil {
+ return err
+ }
+ workerDone := make(chan error, 1)
+ go func() { workerDone <- worker.Run(ctx) }()
+ defer func() {
+ stop()
+ if workerDone != nil {
+ <-workerDone
}
- workerDone = make(chan error, 1)
- go func() { workerDone <- worker.Run(ctx) }()
- defer func() {
- stop()
- if workerDone != nil {
- <-workerDone
- }
- }()
- }
- // Sampling runs only with the Worker, which owns every sweep.
- sampling := coremetrics.Periodic{ID: "runtime_sampler", Every: history.SampleInterval}
- if worker != nil {
- sampler, err := runtimeobs.NewSampler(observationResolver, observationService, worker, runtimeobs.SamplerOptions{})
- if err != nil {
- return err
+ }()
+ // The Worker owns every sampling sweep.
+ sampler, err := runtimeobs.NewSampler(observationResolver, observationService, worker, runtimeobs.SamplerOptions{})
+ if err != nil {
+ return err
+ }
+ sampling := coremetrics.Periodic{ID: "runtime_sampler", Every: history.SampleInterval, Run: func(ctx context.Context) (*int64, int64, error) {
+ result := sampler.Sweep(ctx)
+ sampleCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
+ err := worker.CheckOwnership(sampleCtx)
+ if err == nil {
+ _, err = deploymentStore.SampleHostHistory(sampleCtx)
}
- sampling.Run = func(ctx context.Context) (*int64, int64, error) {
- result := sampler.Sweep(ctx)
- sampleCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
- err := worker.CheckOwnership(sampleCtx)
- if err == nil {
- _, err = deploymentStore.SampleHostHistory(sampleCtx)
- }
- cancel()
- fields := []any{"listed", result.Listed, "observed", result.Observed, "failed", result.Failed, "complete", result.Complete}
- if !result.Complete {
- log.Bg().Warn("Runtime history sampling sweep incomplete", fields...)
- err = errors.New("incomplete Runtime sampling sweep")
- } else {
- log.Bg().Debug("Runtime history sampling sweep complete", fields...)
- }
- return metricPtr(int64(result.Observed)), int64(result.Failed), err
+ cancel()
+ fields := []any{"listed", result.Listed, "observed", result.Observed, "failed", result.Failed, "complete", result.Complete}
+ if !result.Complete {
+ log.Bg().Warn("Runtime history sampling sweep incomplete", fields...)
+ err = errors.New("incomplete Runtime sampling sweep")
+ } else {
+ log.Bg().Debug("Runtime history sampling sweep complete", fields...)
}
- }
+ return metricPtr(int64(result.Observed)), int64(result.Failed), err
+ }}
metricsSource := &coreMetricsSource{store: coremetricspg.New(units), pool: pool, worker: worker, registry: registry}
metrics, err := coremetrics.New(processStartedAt, buildRevision, metricsSource, sampling,
prune("history_cleanup", 2*time.Second, history.Prune),
@@ -342,39 +318,30 @@ func run(config processconfig.Config) error {
Environments: sessionService, EnvironmentsReader: sessionStore, ExecutorConnections: executorConnections{sessions: sessionStore, registry: registry},
Admin: sessionStore, AdminAudit: auditStore, WriteAudit: auditStore, Metrics: metrics,
RuntimeObservations: observationService, RuntimeHistory: historyService,
- }
- if worker != nil {
- deps.Execution = &api.Execution{
+ Execution: api.Execution{
ExecutorURL: executorURL,
SessionAdmission: worker,
InputAdmission: worker,
SessionArchive: deploymentExecution,
Workspaces: worker,
NativeInstaller: nativeInstaller,
- }
- }
- if managedNodes != nil {
- deps.Sandboxes = &api.Sandboxes{
+ },
+ Sandboxes: api.Sandboxes{
Deployment: deploymentService,
NodeAllocations: deploymentStore,
DeploymentChanges: worker,
DeploymentReset: worker,
ConfigurationDiscovery: managedNodes.setup,
- }
+ },
}
- handler, err := api.NewHandler(deps)
+ apiHandler, err := api.NewHandler(deps)
if err != nil {
return err
}
- if daemonHandler != nil {
- routes := daemonRoutes{gateway: daemonHandler,
- enrollment: runtimeenrollment.EnrollmentHandler(sessionService),
- connection: runtimeenrollment.ConnectionHandler(sessionStore, registry)}
- if managedNodes != nil {
- routes.nodeConnect = managedNodes.hub
- }
- handler = serverHandler(handler, &routes)
- }
+ handler := serverHandler(apiHandler, &daemonRoutes{gateway: daemonHandler,
+ enrollment: runtimeenrollment.EnrollmentHandler(sessionService),
+ connection: runtimeenrollment.ConnectionHandler(sessionStore, registry),
+ nodeConnect: managedNodes.hub})
server := &http.Server{Addr: config.Addr, Handler: handler, ReadHeaderTimeout: 10 * time.Second, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 60 * time.Second}
done := make(chan error, 1)
go func() { done <- server.ListenAndServe() }()
diff --git a/services/core/cmd/server/managed_nodes.go b/services/core/cmd/server/managed_nodes.go
index a6d8f434b..518dcd37b 100644
--- a/services/core/cmd/server/managed_nodes.go
+++ b/services/core/cmd/server/managed_nodes.go
@@ -22,12 +22,8 @@ type managedNodes struct {
// configureManagedNodes serves the nodes of the Web-managed deployment. Node
// presence and health and the generation of each allocation go through the
// deployment service; the owner epoch that fences connections and the
-// allocations each generation retains are read from the deployment reader. It
-// returns nil without an installation ID.
+// allocations each generation retains are read from the deployment reader.
func configureManagedNodes(nodes *deployment.Service, reader deployment.Reader, registry *providers.Registry, config processconfig.Config, owner func(context.Context) error) *managedNodes {
- if config.InstallationID == "" {
- return nil
- }
result := &managedNodes{}
result.hub = node.NewHub(node.HubOptions{
Generations: func(ctx context.Context, n node.Identity, connection string, epoch uint64, health node.Health) error {
@@ -74,19 +70,12 @@ func configureManagedNodes(nodes *deployment.Service, reader deployment.Reader,
return nodes.Heartbeat(ctx, n.NodeID, connection, epoch, nodeHealthRecord(health))
},
})
- // Load requires OAC_PUBLIC_URL with an installation ID.
result.setup = &managedSetup{processPaths: config.ProviderPaths, registry: registry, deployment: nodes, allocations: reader, hub: result.hub, installationID: config.InstallationID, runtimeAPI: config.PublicOrigin.RuntimeAPI()}
result.runtime = execution.NewDeferredRuntimeProvider(config.InstallationID, result.setup.load, result.setup.prepare)
result.runtime.PublishUnconfigured = result.setup.publishUnconfigured
return result
}
-func (m *managedNodes) close() {
- if m != nil {
- m.hub.Close()
- }
-}
-
func nodeHealthRecord(health node.Health) deployment.NodeHealth {
return deployment.NodeHealth{Host: &deployment.NodeHost{EffectiveCPUCores: health.EffectiveCPUCores, CPUUtilization: health.CPUUtilization, TotalMemoryBytes: health.TotalMemoryBytes, AvailableMemoryBytes: health.AvailableMemoryBytes, AvailableDiskBytes: health.AvailableDiskBytes, ObservedAt: &health.ObservedAt}, ProviderReady: health.ProviderReady, Diagnostic: health.Diagnostic, CPUCount: health.CPUCount, AvailableMemoryBytes: health.AvailableMemoryBytes, AvailableDiskBytes: health.AvailableDiskBytes}
}
diff --git a/services/core/cmd/server/managed_setup.go b/services/core/cmd/server/managed_setup.go
index e1328ba2b..81a9ffdef 100644
--- a/services/core/cmd/server/managed_setup.go
+++ b/services/core/cmd/server/managed_setup.go
@@ -86,6 +86,11 @@ func (s *managedSetup) publishUnconfigured(generation uint64) { s.publishSelecti
func (s *managedSetup) load(ctx context.Context) (*execution.RuntimeProvider, error) {
setup, err := s.deployment.Setup(ctx)
+ if errors.Is(err, deployment.ErrCredentialUnreadable) {
+ // A replaced credential key blocks hosted execution, not Core.
+ log.Warn(ctx, "Hosted provider credential is unreadable; administrator recovery remains available", "error", err)
+ return nil, fmt.Errorf("%w: %w", execution.ErrExecutionUnavailable, err)
+ }
if err != nil {
return nil, err
}
diff --git a/services/core/cmd/server/managed_setup_test.go b/services/core/cmd/server/managed_setup_test.go
index 259ed35cb..f4a022aa8 100644
--- a/services/core/cmd/server/managed_setup_test.go
+++ b/services/core/cmd/server/managed_setup_test.go
@@ -10,6 +10,7 @@ import (
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment/placement"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/deploymentpg"
+ "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/processconfig"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimeobs"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker"
@@ -24,15 +25,12 @@ import (
)
func TestWebSetupCreatesManagerWithoutLocalProvider(t *testing.T) {
- if configureManagedNodes(nil, nil, providers.Builtin(), processconfig.Config{}, nil) != nil {
- t.Fatal("sandbox manager started without an installation ID")
- }
origin, err := deployment.NewPublicOrigin("https://core.example")
if err != nil {
t.Fatal(err)
}
- m := configureManagedNodes(nil, nil, providers.Builtin(), processconfig.Config{InstallationID: uuid.NewString(), PublicOrigin: &origin}, func(context.Context) error { return nil })
- defer m.close()
+ m := configureManagedNodes(nil, nil, providers.Builtin(), processconfig.Config{InstallationID: uuid.NewString(), PublicOrigin: origin}, func(context.Context) error { return nil })
+ defer m.hub.Close()
if m.setup == nil || m.hub == nil || m.runtime == nil || m.runtime.Provider != nil || m.setup.runtimeAPI != "https://core.example/api/v1" {
t.Fatal("zero-node setup unexpectedly instantiated local compute or omitted management")
}
@@ -113,7 +111,7 @@ func credentialService(t *testing.T) func(owner, candidate deployment.Setup) (de
if err != nil {
t.Fatal(err)
}
- service, err := deployment.NewService(deploymentpg.New(nil, nil), deploymentpg.New(nil, nil), providers.Builtin(), rules)
+ service, err := deployment.NewService(deploymentpg.New(nil, pgtest.CredentialKey(t)), deploymentpg.New(nil, pgtest.CredentialKey(t)), providers.Builtin(), rules)
if err != nil {
t.Fatal(err)
}
@@ -131,8 +129,12 @@ func TestManagedSetupNeverReusesAnotherGenerationOrUnverifiedState(t *testing.T)
t.Fatal("matching immutable selection was not reused")
}
loadErr = errors.New("database unavailable")
- if _, err := s.load(t.Context()); err == nil {
- t.Fatal("stale cached selection hid storage failure")
+ if _, err := s.load(t.Context()); err == nil || errors.Is(err, execution.ErrExecutionUnavailable) {
+ t.Fatal("stale cached selection hid storage failure", err)
+ }
+ loadErr = deployment.ErrCredentialUnreadable
+ if _, err := s.load(t.Context()); !errors.Is(err, execution.ErrExecutionUnavailable) || !errors.Is(err, deployment.ErrCredentialUnreadable) {
+ t.Fatal("an unreadable credential must block execution without stopping Core", err)
}
loadErr = nil
value.Generation = 2
diff --git a/services/core/cmd/server/runtime_history.go b/services/core/cmd/server/runtime_history.go
index 7d333c097..792d04769 100644
--- a/services/core/cmd/server/runtime_history.go
+++ b/services/core/cmd/server/runtime_history.go
@@ -25,15 +25,10 @@ type runtimeHistoryExporter interface {
Close(context.Context) error
}
-func runtimeHistory(ctx context.Context, units *pgunit.Pool, config processconfig.RuntimeHistory, executionEnabled bool) (runtimeHistorySetup, error) {
+func runtimeHistory(ctx context.Context, units *pgunit.Pool, config processconfig.RuntimeHistory) (runtimeHistorySetup, error) {
interval := config.SampleInterval
- mode := runtimehistory.CollectionPeriodic
- if !executionEnabled {
- interval = 0
- mode = runtimehistory.CollectionOnRead
- }
capabilities := runtimehistory.Capabilities{
- CollectionMode: mode, SampleInterval: interval, Retention: 7 * 24 * time.Hour,
+ CollectionMode: runtimehistory.CollectionPeriodic, SampleInterval: interval, Retention: 7 * 24 * time.Hour,
MinimumStep: max(30*time.Second, interval), MaximumRange: 24 * time.Hour,
MaximumPoints: 1000, MaximumSeries: 64, MaximumTotalPoints: 10000,
Metrics: []runtimehistory.Metric{runtimehistory.MetricCPU, runtimehistory.MetricMemory, runtimehistory.MetricTokens},
diff --git a/services/core/cmd/server/runtime_history_test.go b/services/core/cmd/server/runtime_history_test.go
index 17c748cb5..c81e5b3e4 100644
--- a/services/core/cmd/server/runtime_history_test.go
+++ b/services/core/cmd/server/runtime_history_test.go
@@ -18,31 +18,26 @@ func (panicHistoryExporter) Close(context.Context) error
var defaultHistory = processconfig.RuntimeHistory{QueueCapacity: 256, Timeout: 2 * time.Second, SampleInterval: 30 * time.Second}
func TestRuntimeHistoryUsesCoreDatabaseByDefault(t *testing.T) {
- for _, enabled := range []bool{true, false} {
- setup, err := runtimeHistory(t.Context(), pgunit.NewPool(nil), defaultHistory, enabled)
- if err != nil {
- t.Fatal(err)
- }
- if len(setup.Options) != 1 || setup.Exporter != nil || setup.Reader == nil || setup.Prune == nil {
- t.Fatal("default history requires extra deployment")
- }
- capabilities := setup.Reader.Capabilities()
- if capabilities.Durable() != enabled || capabilities.Retention != 7*24*time.Hour {
- t.Fatalf("incorrect default capabilities: %+v", capabilities)
- }
- if enabled && setup.SampleInterval != 30*time.Second {
- t.Fatal("default cadence missing")
- }
- if !enabled && setup.SampleInterval != 0 {
- t.Fatal("sampler requires an execution owner")
- }
+ setup, err := runtimeHistory(t.Context(), pgunit.NewPool(nil), defaultHistory)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(setup.Options) != 1 || setup.Exporter != nil || setup.Reader == nil || setup.Prune == nil {
+ t.Fatal("default history requires extra deployment")
+ }
+ capabilities := setup.Reader.Capabilities()
+ if !capabilities.Durable() || capabilities.Retention != 7*24*time.Hour {
+ t.Fatalf("incorrect default capabilities: %+v", capabilities)
+ }
+ if setup.SampleInterval != 30*time.Second {
+ t.Fatal("default cadence missing")
}
}
func TestRuntimeHistoryOptionalExportAndSamplingConfiguration(t *testing.T) {
config := defaultHistory
config.Endpoint, config.Headers, config.SampleInterval = "https://collector.example.test/v1/metrics", map[string]string{"Authorization": "Bearer private"}, time.Minute
- setup, err := runtimeHistory(t.Context(), pgunit.NewPool(nil), config, true)
+ setup, err := runtimeHistory(t.Context(), pgunit.NewPool(nil), config)
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/internal/agents/storage.go b/services/core/internal/agents/storage.go
index 7203ea761..ad88085a9 100644
--- a/services/core/internal/agents/storage.go
+++ b/services/core/internal/agents/storage.go
@@ -12,8 +12,7 @@ import (
// Agent.
type Storage interface {
// CreateAgent assigns the Agent's ID and saves it with its sealed model
- // provider bundle. Sealing without a credential key is
- // credentialcrypto.ErrUnavailable.
+ // provider bundle.
CreateAgent(context.Context, NewAgent) (Agent, error)
// WithAgentUpdate locks the tenant's Agent and runs update; the revision
// it applies commits only when update returns nil.
@@ -56,8 +55,7 @@ type Reader interface {
GetAgent(ctx context.Context, tenantID, agentID string) (Agent, error)
ListAgents(context.Context, ListQuery) (Page, error)
// GetAgentWithModelProvider reads the Agent and its opened model provider
- // bundle from one snapshot. The bundle is nil when the Agent has none.
- // Opening one without a credential key is credentialcrypto.ErrUnavailable;
- // a bundle that fails to open is an internal error.
+ // bundle from one snapshot. The bundle is nil when the Agent has none; a
+ // bundle that fails to open is an internal error.
GetAgentWithModelProvider(ctx context.Context, tenantID, agentID string) (Agent, *v1.ModelProviderInput, error)
}
diff --git a/services/core/internal/api/admin_session_archive.go b/services/core/internal/api/admin_session_archive.go
index fcf8ae8f8..2a7b0e46e 100644
--- a/services/core/internal/api/admin_session_archive.go
+++ b/services/core/internal/api/admin_session_archive.go
@@ -40,10 +40,6 @@ func (h *Handler) adminArchiveSession(w http.ResponseWriter, r *http.Request) {
writeOperationError(w, r, sessions.ErrInvalidInput)
return
}
- if h.Execution == nil {
- writeOperationError(w, r, sessions.ErrEnvironmentUnavailable)
- return
- }
result, err := h.Execution.SessionArchive.ArchiveSession(r.Context(), tenantID(r), chi.URLParam(r, "session_id"), input.ExpectedGeneration)
if err != nil {
writeOperationError(w, r, err)
diff --git a/services/core/internal/api/admin_session_archive_test.go b/services/core/internal/api/admin_session_archive_test.go
index 4d12f04a4..d3a45ac4f 100644
--- a/services/core/internal/api/admin_session_archive_test.go
+++ b/services/core/internal/api/admin_session_archive_test.go
@@ -38,7 +38,6 @@ func TestAdminSessionArchiveAuthorityAndValidation(t *testing.T) {
key := callerBinding()
deps, fakes := managementFakes(t, key)
fixture := &archiveManagementFixture{}
- deps.Execution = fakes.execution()
fakes.sessionArchive.archiveSession = fixture.ArchiveSession
fakes.sessionAdmin.getManagedSessionArchive = fixture.GetManagedSessionArchive
h := newTestHandler(t, deps)
diff --git a/services/core/internal/api/admin_summary.go b/services/core/internal/api/admin_summary.go
index d24efa7c0..83c1a8188 100644
--- a/services/core/internal/api/admin_summary.go
+++ b/services/core/internal/api/admin_summary.go
@@ -125,7 +125,7 @@ func (h *Handler) adminSummary(w http.ResponseWriter, r *http.Request) {
groups[""] = &AdminSummaryRow{ProjectID: project.ID}
}
counts, err := h.Admin.ReadAdminSummary(ctx, project.TenantID, sessions.AdminSummaryFilter{CreatedAfter: after, CreatedBefore: before}, func(session sessions.Session, creationKeyID *string) error {
- projected, err := sessionResponse(session, h.executorURL())
+ projected, err := sessionResponse(session, h.Execution.ExecutorURL)
if err != nil {
return err
}
diff --git a/services/core/internal/api/contract_routes_test.go b/services/core/internal/api/contract_routes_test.go
index 6b589bceb..65b557b1c 100644
--- a/services/core/internal/api/contract_routes_test.go
+++ b/services/core/internal/api/contract_routes_test.go
@@ -66,9 +66,8 @@ func contractOperations(t *testing.T, file, prefix string) map[string]bool {
// The pinned upstream /v1 set is checked by TestEveryRouteAuthenticatesItsCanonicalPath
// and the contract tests.
func TestContractsPublishExactlyTheRegisteredCoreAndMachineRoutes(t *testing.T) {
- // Every optional group that gates a route registration, as the server enables them.
- deps, fakes := testDependencies(t)
- deps.Execution, deps.Sandboxes = fakes.execution(), fakes.sandboxes()
+ // The native installer gates its routes, as a release build enables them.
+ deps, _ := testDependencies(t)
deps.Execution.NativeInstaller = &NativeInstaller{Version: "contract-test", Base: "https://core.example/api/v1/agent-daemon/install/", Catalog: &nativeinstaller.Catalog{}}
h := &Handler{Dependencies: deps}
contracts := map[string]string{"/v1": "openapi.yaml", "/core/v1": "core.openapi.yaml", "/api/v1": "runtime.openapi.yaml"}
diff --git a/services/core/internal/api/core_store_validation_test.go b/services/core/internal/api/core_store_validation_test.go
index 326967bb7..91cb319d0 100644
--- a/services/core/internal/api/core_store_validation_test.go
+++ b/services/core/internal/api/core_store_validation_test.go
@@ -13,6 +13,7 @@ import (
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment/placement"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/deploymentpg"
+ "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/projects"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/providers"
@@ -25,7 +26,7 @@ func TestCoreStoreValidationFieldsAndPublicFallback(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- nodes, err := deployment.NewService(deploymentpg.New(nil, nil), deploymentpg.New(nil, nil), providers.Builtin(), rules)
+ nodes, err := deployment.NewService(deploymentpg.New(nil, pgtest.CredentialKey(t)), deploymentpg.New(nil, pgtest.CredentialKey(t)), providers.Builtin(), rules)
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/internal/api/credentials_oauth_test.go b/services/core/internal/api/credentials_oauth_test.go
index 779bbfc2a..ae3170806 100644
--- a/services/core/internal/api/credentials_oauth_test.go
+++ b/services/core/internal/api/credentials_oauth_test.go
@@ -8,7 +8,6 @@ import (
"strings"
"testing"
- "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/vaults"
)
@@ -149,7 +148,7 @@ func TestOAuthCredentialStoreFailuresUseSafeExistingErrors(t *testing.T) {
for _, tc := range []struct {
err error
code int
- }{{vaults.ErrNotFound, 404}, {vaults.ErrInvalidInput, 400}, {credentialcrypto.ErrUnavailable, 503}, {errors.New("access-canary"), 500}} {
+ }{{vaults.ErrNotFound, 404}, {vaults.ErrInvalidInput, 400}, {errors.New("access-canary"), 500}} {
for _, update := range []bool{false, true} {
h, f, _ := credentialHandler(t)
f.err = tc.err
diff --git a/services/core/internal/api/credentials_test.go b/services/core/internal/api/credentials_test.go
index 0c2ace584..fcc60c811 100644
--- a/services/core/internal/api/credentials_test.go
+++ b/services/core/internal/api/credentials_test.go
@@ -11,7 +11,6 @@ import (
"testing"
"time"
- "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/vaults"
"github.com/google/uuid"
)
@@ -113,7 +112,7 @@ func TestCredentialStorageErrorsStaySafe(t *testing.T) {
for _, test := range []struct {
err error
status int
- }{{vaults.ErrNotFound, 404}, {credentialcrypto.ErrUnavailable, 503}, {errors.New("credential-canary"), 500}} {
+ }{{vaults.ErrNotFound, 404}, {errors.New("credential-canary"), 500}} {
h, f, _ := credentialHandler(t)
f.err = test.err
w := credentialRequest(h, "POST", "/v1/vaults/"+f.credential.VaultID+"/credentials", `{"name":"n","auth":{"type":"static_bearer","mcp_server_url":"https://example.invalid","token":"credential-canary"}}`)
diff --git a/services/core/internal/api/credentials_update_test.go b/services/core/internal/api/credentials_update_test.go
index 2cd883056..cd7f65050 100644
--- a/services/core/internal/api/credentials_update_test.go
+++ b/services/core/internal/api/credentials_update_test.go
@@ -10,7 +10,6 @@ import (
"strings"
"testing"
- "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/vaults"
"github.com/google/uuid"
)
@@ -107,7 +106,7 @@ func TestCredentialUpdateUsesExistingBoundariesAndSafeErrors(t *testing.T) {
for _, tc := range []struct {
err error
status int
- }{{vaults.ErrNotFound, 404}, {credentialcrypto.ErrUnavailable, 503}, {errors.New("credential-canary"), 500}} {
+ }{{vaults.ErrNotFound, 404}, {errors.New("credential-canary"), 500}} {
h, f, _ := credentialHandler(t)
f.err = tc.err
w := credentialRequest(h, "POST", "/v1/vaults/"+f.credential.VaultID+"/credentials/"+f.credential.ID, body)
diff --git a/services/core/internal/api/dependencies.go b/services/core/internal/api/dependencies.go
index 5dcb4f981..ba2cac514 100644
--- a/services/core/internal/api/dependencies.go
+++ b/services/core/internal/api/dependencies.go
@@ -64,14 +64,8 @@ type Dependencies struct {
EnvironmentTemplatesReader EnvironmentTemplatesReader
- // Execution is nil when this Core runs without a Runtime gateway, and so
- // without an execution Worker. Work that needs one then answers 503
- // execution_unavailable.
- Execution *Execution
- // Sandboxes is nil when this Core has no managed sandbox installation. The
- // sandbox node and manager routes are then absent, and openai_hosted
- // Sessions answer 503 execution_unavailable. It requires Execution.
- Sandboxes *Sandboxes
+ Execution Execution
+ Sandboxes Sandboxes
}
// Execution is the execution Worker's surface. Every field is required unless
@@ -147,35 +141,24 @@ func (d Dependencies) validate() error {
); err != nil {
return err
}
- if e := d.Execution; e != nil {
- if e.ExecutorURL == "" {
- return errors.New("api: Execution.ExecutorURL is required")
- }
- if e.NativeInstaller != nil && (e.NativeInstaller.Version == "" || e.NativeInstaller.Base == "") {
- return errors.New("api: Execution.NativeInstaller.Version and Base are required")
- }
- if err := required(
- field{"Execution.SessionAdmission", e.SessionAdmission},
- field{"Execution.InputAdmission", e.InputAdmission},
- field{"Execution.SessionArchive", e.SessionArchive},
- field{"Execution.Workspaces", e.Workspaces},
- ); err != nil {
- return err
- }
+ e, s := d.Execution, d.Sandboxes
+ if e.ExecutorURL == "" {
+ return errors.New("api: Execution.ExecutorURL is required")
}
- if s := d.Sandboxes; s != nil {
- if d.Execution == nil {
- return errors.New("api: Sandboxes requires Execution")
- }
- return required(
- field{"Sandboxes.Deployment", s.Deployment},
- field{"Sandboxes.NodeAllocations", s.NodeAllocations},
- field{"Sandboxes.DeploymentChanges", s.DeploymentChanges},
- field{"Sandboxes.DeploymentReset", s.DeploymentReset},
- field{"Sandboxes.ConfigurationDiscovery", s.ConfigurationDiscovery},
- )
+ if e.NativeInstaller != nil && (e.NativeInstaller.Version == "" || e.NativeInstaller.Base == "") {
+ return errors.New("api: Execution.NativeInstaller.Version and Base are required")
}
- return nil
+ return required(
+ field{"Execution.SessionAdmission", e.SessionAdmission},
+ field{"Execution.InputAdmission", e.InputAdmission},
+ field{"Execution.SessionArchive", e.SessionArchive},
+ field{"Execution.Workspaces", e.Workspaces},
+ field{"Sandboxes.Deployment", s.Deployment},
+ field{"Sandboxes.NodeAllocations", s.NodeAllocations},
+ field{"Sandboxes.DeploymentChanges", s.DeploymentChanges},
+ field{"Sandboxes.DeploymentReset", s.DeploymentReset},
+ field{"Sandboxes.ConfigurationDiscovery", s.ConfigurationDiscovery},
+ )
}
type field struct {
@@ -191,12 +174,3 @@ func required(fields ...field) error {
}
return nil
}
-
-// executorURL is the daemon URL self-hosted Sessions report, or empty when
-// this Core cannot execute.
-func (h *Handler) executorURL() string {
- if h.Execution == nil {
- return ""
- }
- return h.Execution.ExecutorURL
-}
diff --git a/services/core/internal/api/dependencies_test.go b/services/core/internal/api/dependencies_test.go
index a044d4988..e3d585561 100644
--- a/services/core/internal/api/dependencies_test.go
+++ b/services/core/internal/api/dependencies_test.go
@@ -63,9 +63,8 @@ type testFakes struct {
// testDependencies returns Dependencies in which every area is a strict fake.
// A test sets the funcs it expects on the returned fakes; any other call fails
-// it. Engine is "codex", CoreKeys accepts "Bearer admin", and Execution and
-// Sandboxes are disabled until the test sets fakes.execution() or
-// fakes.sandboxes().
+// it. Engine is "codex", CoreKeys accepts "Bearer admin", and Execution reports
+// testExecutorURL without a native installer.
func testDependencies(t testing.TB) (Dependencies, *testFakes) {
t.Helper()
f := &testFakes{
@@ -116,33 +115,23 @@ func testDependencies(t testing.TB) (Dependencies, *testFakes) {
SessionAdmin: f.sessionAdmin,
Environments: f.environments, EnvironmentsReader: f.environmentsReader, ExecutorConnections: f.executorConnections, Admin: f.admin, AdminAudit: f.adminAudit, WriteAudit: f.writeAudit,
Metrics: f.metrics, RuntimeObservations: f.runtimeObservations, RuntimeHistory: f.runtimeHistory,
+ Execution: Execution{
+ ExecutorURL: testExecutorURL,
+ SessionAdmission: f.sessionAdmission,
+ InputAdmission: f.inputAdmission,
+ SessionArchive: f.sessionArchive,
+ Workspaces: f.workspaces,
+ },
+ Sandboxes: Sandboxes{
+ Deployment: f.deployment,
+ NodeAllocations: f.nodeAllocations,
+ DeploymentChanges: f.deploymentChanges,
+ DeploymentReset: f.deploymentReset,
+ ConfigurationDiscovery: f.configurationDiscovery,
+ },
}, f
}
-// execution is an Execution group backed by f's strict fakes, reporting
-// testExecutorURL and without a native installer.
-func (f *testFakes) execution() *Execution {
- return &Execution{
- ExecutorURL: testExecutorURL,
- SessionAdmission: f.sessionAdmission,
- InputAdmission: f.inputAdmission,
- SessionArchive: f.sessionArchive,
- Workspaces: f.workspaces,
- }
-}
-
-// sandboxes is a Sandboxes group backed by f's strict fakes. It requires
-// Execution.
-func (f *testFakes) sandboxes() *Sandboxes {
- return &Sandboxes{
- Deployment: f.deployment,
- NodeAllocations: f.nodeAllocations,
- DeploymentChanges: f.deploymentChanges,
- DeploymentReset: f.deploymentReset,
- ConfigurationDiscovery: f.configurationDiscovery,
- }
-}
-
// coreKeys accepts each token as a Core key.
func coreKeys(t testing.TB, tokens ...string) *DeploymentAuthenticator {
t.Helper()
@@ -174,13 +163,11 @@ func noDeploymentModelProvider(context.Context, string) (*modelconfiguration.Sna
}
func TestNewHandlerAcceptsCompleteDependencies(t *testing.T) {
- deps, f := testDependencies(t)
+ deps, _ := testDependencies(t)
if _, err := NewHandler(deps); err != nil {
t.Fatal(err)
}
- deps.Execution = f.execution()
deps.Execution.NativeInstaller = &NativeInstaller{Version: "build", Base: "https://core.example/api/v1/agent-daemon/install/"}
- deps.Sandboxes = f.sandboxes()
if _, err := NewHandler(deps); err != nil {
t.Fatal(err)
}
@@ -200,27 +187,11 @@ func TestNewHandlerRejectsIncompleteDependencies(t *testing.T) {
{"Turns", func(d *Dependencies, _ *testFakes) { d.Turns = nil }},
{"Items", func(d *Dependencies, _ *testFakes) { d.Items = nil }},
{"RuntimeHistory", func(d *Dependencies, _ *testFakes) { d.RuntimeHistory = nil }},
- {"Execution.ExecutorURL", func(d *Dependencies, f *testFakes) {
- d.Execution = f.execution()
- d.Execution.ExecutorURL = ""
- }},
- {"Execution.SessionAdmission", func(d *Dependencies, f *testFakes) {
- d.Execution = f.execution()
- d.Execution.SessionAdmission = nil
- }},
- {"Execution.InputAdmission", func(d *Dependencies, f *testFakes) {
- d.Execution = f.execution()
- d.Execution.InputAdmission = nil
- }},
- {"Execution.NativeInstaller.Version", func(d *Dependencies, f *testFakes) {
- d.Execution = f.execution()
- d.Execution.NativeInstaller = &NativeInstaller{}
- }},
- {"Sandboxes requires Execution", func(d *Dependencies, f *testFakes) { d.Sandboxes = f.sandboxes() }},
- {"Sandboxes.ConfigurationDiscovery", func(d *Dependencies, f *testFakes) {
- d.Execution, d.Sandboxes = f.execution(), f.sandboxes()
- d.Sandboxes.ConfigurationDiscovery = nil
- }},
+ {"Execution.ExecutorURL", func(d *Dependencies, _ *testFakes) { d.Execution.ExecutorURL = "" }},
+ {"Execution.SessionAdmission", func(d *Dependencies, _ *testFakes) { d.Execution.SessionAdmission = nil }},
+ {"Execution.InputAdmission", func(d *Dependencies, _ *testFakes) { d.Execution.InputAdmission = nil }},
+ {"Execution.NativeInstaller.Version", func(d *Dependencies, _ *testFakes) { d.Execution.NativeInstaller = &NativeInstaller{} }},
+ {"Sandboxes.ConfigurationDiscovery", func(d *Dependencies, _ *testFakes) { d.Sandboxes.ConfigurationDiscovery = nil }},
} {
t.Run(test.missing, func(t *testing.T) {
deps, f := testDependencies(t)
diff --git a/services/core/internal/api/environment_creation_test.go b/services/core/internal/api/environment_creation_test.go
index 7feca96c7..e9f03cd95 100644
--- a/services/core/internal/api/environment_creation_test.go
+++ b/services/core/internal/api/environment_creation_test.go
@@ -66,10 +66,8 @@ func environmentCreationHandler(t *testing.T, engine string, configure ...func(*
return newTestHandler(t, deps), fixture
}
-// selfHostedExecution enables Execution reporting environmentOrigin to
-// self-hosted Sessions.
+// selfHostedExecution reports environmentOrigin to self-hosted Sessions.
func selfHostedExecution(d *Dependencies, f *testFakes) {
- d.Execution = f.execution()
d.Execution.ExecutorURL = environmentOrigin
}
@@ -202,41 +200,3 @@ func TestSelfHostedCreationRejectsBeforePersistence(t *testing.T) {
}
}
}
-
-// Execution, which carries the executor URL, is required; a URL without
-// Execution cannot be configured (TestNewHandlerRejectsIncompleteDependencies).
-func TestSelfHostedCreationRequiresOperatorExecution(t *testing.T) {
- for _, stream := range []bool{false, true} {
- unavailable := 0
- handler, fixture := environmentCreationHandler(t, "codex", func(_ *Dependencies, f *testFakes) { f.metrics.recordUnavailable = func() { unavailable++ } })
- body := fmt.Sprintf(`{"agent":{"model":"MiniMax-M3"},"environment":{"type":"self_hosted","workspace_directory":"/workspace"},"stream":%t,%s}`, stream, fixtureSessionProvider)
- request := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions", strings.NewReader(body))
- request.Header.Set("Authorization", "Bearer key")
- request.Header.Set("OpenAI-Beta", "agents=v1")
- request.Header.Set("Content-Type", "application/json")
- response := httptest.NewRecorder()
- handler.ServeHTTP(response, request)
- var failure v1.ErrorResponse
- if response.Code != http.StatusServiceUnavailable || json.Unmarshal(response.Body.Bytes(), &failure) != nil || failure.Error.Code == nil || *failure.Error.Code != "execution_unavailable" || fixture.input.Engine != "" || unavailable != 1 {
- t.Fatal("operator prerequisites did not fail before persistence", response.Code, response.Body.String(), fixture.input)
- }
- }
-}
-
-func TestHostedCreationRequiresOperatorExecution(t *testing.T) {
- for _, stream := range []bool{false, true} {
- unavailable := 0
- handler, fixture := environmentCreationHandler(t, "codex", selfHostedExecution, func(_ *Dependencies, f *testFakes) { f.metrics.recordUnavailable = func() { unavailable++ } })
- body := fmt.Sprintf(`{"agent":{"model":"model"},"environment":{"type":"openai_hosted"},"stream":%t,"input":"Initialize the hosted execution."}`, stream)
- request := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions", strings.NewReader(body))
- request.Header.Set("Authorization", "Bearer key")
- request.Header.Set("OpenAI-Beta", "agents=v1")
- request.Header.Set("Content-Type", "application/json")
- response := httptest.NewRecorder()
- handler.ServeHTTP(response, request)
- var failure v1.ErrorResponse
- if response.Code != http.StatusServiceUnavailable || json.Unmarshal(response.Body.Bytes(), &failure) != nil || failure.Error.Code == nil || *failure.Error.Code != "execution_unavailable" || fixture.input.Engine != "" || unavailable != 1 {
- t.Fatal("hosted configuration bypassed operator prerequisites", response.Code, response.Body.String())
- }
- }
-}
diff --git a/services/core/internal/api/environment_files.go b/services/core/internal/api/environment_files.go
index d829775e0..e507f348c 100644
--- a/services/core/internal/api/environment_files.go
+++ b/services/core/internal/api/environment_files.go
@@ -33,7 +33,7 @@ func (h *Handler) listEnvironmentFiles(w http.ResponseWriter, r *http.Request) {
if !ok || !environmentFilesAccessible(w, environment) {
return
}
- if h.Execution == nil || !execution.LocalWorkspaceConfiguration(environment.Configuration) {
+ if !execution.LocalWorkspaceConfiguration(environment.Configuration) {
writeSessionsError(w, r, execution.ErrExecutionUnavailable)
return
}
diff --git a/services/core/internal/api/environment_files_completeness_test.go b/services/core/internal/api/environment_files_completeness_test.go
index df17a8793..c63854d2f 100644
--- a/services/core/internal/api/environment_files_completeness_test.go
+++ b/services/core/internal/api/environment_files_completeness_test.go
@@ -23,7 +23,7 @@ func TestEnvironmentFilesRejectsIncompleteOrMalformedDirectories(t *testing.T) {
} {
t.Run(name, func(t *testing.T) {
unavailable := 0
- h, f := environmentFilesHandler(t, true, countEnvironmentFilesUnavailable(&unavailable))
+ h, f := environmentFilesHandler(t, countEnvironmentFilesUnavailable(&unavailable))
f.result = result
w := requestEnvironmentFiles(h, f.environment.ID, "?limit=1", "files-key")
if w.Code != 503 || unavailable != 1 || strings.Contains(w.Body.String(), `"data"`) || strings.Contains(w.Body.String(), `"next"`) || strings.Contains(w.Body.String(), "secret") {
@@ -36,7 +36,7 @@ func TestEnvironmentFilesRejectsIncompleteOrMalformedDirectories(t *testing.T) {
func TestEnvironmentFilesCursorRejectsChangesAndAcceptsNativeReordering(t *testing.T) {
for _, change := range []string{"limit", "order", "path", "environment", "size", "name", "removed", "added", "native order"} {
t.Run(change, func(t *testing.T) {
- h, f := environmentFilesHandler(t, true)
+ h, f := environmentFilesHandler(t)
f.result.Entries = []proto.WorkspaceDirectoryEntry{environmentFileEntry("A", 1), environmentFileEntry("B", 2)}
page := decodeEnvironmentFiles(t, requestEnvironmentFiles(h, f.environment.ID, "?limit=1", "files-key"))
q := url.Values{"limit": {"1"}, "page": {*page.Next}}
@@ -73,7 +73,7 @@ func TestEnvironmentFilesCursorRejectsChangesAndAcceptsNativeReordering(t *testi
func TestEnvironmentFilesMalformedCursorBounds(t *testing.T) {
for _, change := range []string{"version", "binding", "fingerprint", "negative", "overflow", "unknown", "trailing", "non-page offset"} {
t.Run(change, func(t *testing.T) {
- h, f := environmentFilesHandler(t, true)
+ h, f := environmentFilesHandler(t)
f.result.Entries = []proto.WorkspaceDirectoryEntry{environmentFileEntry("A", 1), environmentFileEntry("B", 2), environmentFileEntry("C", 3)}
page := decodeEnvironmentFiles(t, requestEnvironmentFiles(h, f.environment.ID, "?limit=2", "files-key"))
raw, _ := base64.RawURLEncoding.DecodeString(*page.Next)
diff --git a/services/core/internal/api/environment_files_create.go b/services/core/internal/api/environment_files_create.go
index 0cec14045..9a2608ddc 100644
--- a/services/core/internal/api/environment_files_create.go
+++ b/services/core/internal/api/environment_files_create.go
@@ -105,7 +105,7 @@ func (h *Handler) createEnvironmentFile(w http.ResponseWriter, r *http.Request)
return
}
}
- if h.Execution == nil || !execution.LocalWorkspaceConfiguration(environment.Configuration) {
+ if !execution.LocalWorkspaceConfiguration(environment.Configuration) {
writeSessionsError(w, r, execution.ErrExecutionUnavailable)
return
}
diff --git a/services/core/internal/api/environment_files_create_test.go b/services/core/internal/api/environment_files_create_test.go
index ebeebd16a..1e8a6a737 100644
--- a/services/core/internal/api/environment_files_create_test.go
+++ b/services/core/internal/api/environment_files_create_test.go
@@ -48,7 +48,6 @@ func environmentFileCreateHandler(t *testing.T, configure ...func(*Dependencies,
APIKey{OrganizationID: "org", ProjectID: "other", SubjectKind: "user", SubjectID: "other", TokenSHA256: runtimedevice.HashCredential("other-key"), TenantID: uuid.NewString()},
).ResolveAPIKey
fakes.environmentsReader.getEnvironment = f.GetEnvironment
- deps.Execution = fakes.execution()
fakes.workspaces.readEnvironmentDirectory, fakes.workspaces.writeEnvironmentFile = f.ReadEnvironmentDirectory, f.WriteEnvironmentFile
for _, c := range configure {
c(&deps, fakes)
diff --git a/services/core/internal/api/environment_files_deadline_test.go b/services/core/internal/api/environment_files_deadline_test.go
index 6f1f2cd3b..a868b27d0 100644
--- a/services/core/internal/api/environment_files_deadline_test.go
+++ b/services/core/internal/api/environment_files_deadline_test.go
@@ -13,7 +13,7 @@ import (
func TestEnvironmentFilesReadOutlivesDefaultHTTPWriteDeadline(t *testing.T) {
for _, status := range []int{http.StatusOK, http.StatusServiceUnavailable} {
t.Run(http.StatusText(status), func(t *testing.T) {
- handler, fixture := environmentFilesHandler(t, true, func(_ *Dependencies, f *testFakes) { f.metrics.recordUnavailable = func() {} })
+ handler, fixture := environmentFilesHandler(t, func(_ *Dependencies, f *testFakes) { f.metrics.recordUnavailable = func() {} })
fixture.readDelay = 100 * time.Millisecond
if status == http.StatusServiceUnavailable {
fixture.readError = execution.ErrExecutionUnavailable
diff --git a/services/core/internal/api/environment_files_test.go b/services/core/internal/api/environment_files_test.go
index 4ba1ebad1..eab54eb9f 100644
--- a/services/core/internal/api/environment_files_test.go
+++ b/services/core/internal/api/environment_files_test.go
@@ -62,9 +62,8 @@ func newEnvironmentFilesFixture() *environmentFilesFixture {
}
}
-// environmentFilesHandler serves f's Environment. Without enabled, Core has no
-// execution Worker.
-func environmentFilesHandler(t *testing.T, enabled bool, configure ...func(*Dependencies, *testFakes)) (http.Handler, *environmentFilesFixture) {
+// environmentFilesHandler serves f's Environment.
+func environmentFilesHandler(t *testing.T, configure ...func(*Dependencies, *testFakes)) (http.Handler, *environmentFilesFixture) {
t.Helper()
f := newEnvironmentFilesFixture()
keys := []APIKey{}
@@ -80,10 +79,7 @@ func environmentFilesHandler(t *testing.T, enabled bool, configure ...func(*Depe
deps.Engine = "fake_alpha"
fakes.projectsReader.resolveAPIKey = projectKeys(t, keys...).ResolveAPIKey
fakes.environmentsReader.getEnvironment = f.GetEnvironment
- if enabled {
- deps.Execution = fakes.execution()
- fakes.workspaces.readEnvironmentDirectory = f.ReadEnvironmentDirectory
- }
+ fakes.workspaces.readEnvironmentDirectory = f.ReadEnvironmentDirectory
for _, c := range configure {
c(&deps, fakes)
}
@@ -131,7 +127,7 @@ func decodeEnvironmentFiles(t *testing.T, w *httptest.ResponseRecorder) v1.Envir
func TestEnvironmentFilesOrderingPaginationAndProjection(t *testing.T) {
for _, order := range []string{"", "asc", "desc"} {
t.Run("order="+order, func(t *testing.T) {
- h, f := environmentFilesHandler(t, true)
+ h, f := environmentFilesHandler(t)
f.result.Entries = []proto.WorkspaceDirectoryEntry{
environmentFileEntry("a.txt", 14), environmentFileEntry("z.txt", 5), environmentFileEntry("A.txt", 0), environmentFileEntry("a-b.txt", 6),
{Name: "directory", Kind: "directory"}, {Name: "symlink", Kind: "symlink"}, {Name: "socket", Kind: "other"},
@@ -173,7 +169,7 @@ func TestEnvironmentFilesOrderingPaginationAndProjection(t *testing.T) {
}
func TestEnvironmentFilesEmptyRootDefaultsAndSharedAccess(t *testing.T) {
- h, f := environmentFilesHandler(t, true)
+ h, f := environmentFilesHandler(t)
page := decodeEnvironmentFiles(t, requestEnvironmentFiles(h, f.environment.ID, "", "shared-key"))
if len(page.Data) != 0 || page.Next != nil || f.directory != "" {
t.Fatal("invalid empty root", page, f.directory)
@@ -193,16 +189,14 @@ func TestEnvironmentFilesEmptyRootDefaultsAndSharedAccess(t *testing.T) {
}
func TestEnvironmentFilesAuthorizationPrecedesInspection(t *testing.T) {
- for _, enabled := range []bool{false, true} {
- for _, query := range []string{"", "?path=/foreign-secret/../&page=invalid&limit=999", "?bad=%GG"} {
- h, f := environmentFilesHandler(t, enabled)
- w := requestEnvironmentFiles(h, f.environment.ID, query, "other-key")
- if w.Code != 404 || f.lookups != 1 || f.reads != 0 || strings.Contains(w.Body.String(), "foreign-secret") || strings.Contains(w.Body.String(), f.environment.ID) {
- t.Fatal("foreign resource inspected", w.Code, w.Body, f)
- }
+ for _, query := range []string{"", "?path=/foreign-secret/../&page=invalid&limit=999", "?bad=%GG"} {
+ h, f := environmentFilesHandler(t)
+ w := requestEnvironmentFiles(h, f.environment.ID, query, "other-key")
+ if w.Code != 404 || f.lookups != 1 || f.reads != 0 || strings.Contains(w.Body.String(), "foreign-secret") || strings.Contains(w.Body.String(), f.environment.ID) {
+ t.Fatal("foreign resource inspected", w.Code, w.Body, f)
}
}
- h, f := environmentFilesHandler(t, true)
+ h, f := environmentFilesHandler(t)
w := requestEnvironmentFiles(h, f.environment.ID, "", "invalid")
if w.Code != 401 || f.lookups != 0 || f.reads != 0 {
t.Fatal("unauthenticated read", w.Code, f)
@@ -214,7 +208,7 @@ func TestEnvironmentFilesRejectsInvalidRequestsBeforeRead(t *testing.T) {
"limit=0", "limit=101", "limit=no", "limit=", "limit=1&limit=2", "order=ASC", "order=", "path=", "path=relative", "path=/workspace-sibling", "path=/workspace/../workspace", "path=/workspace/a/../../workspace", "path=/workspace/%00", "path=/workspace/%5C", "path=/workspace/%0A", "path=/workspace/%FF", "path=x&path=y", "path=" + strings.Repeat("a", 4097), "page=", "page=not-json", "page=" + strings.Repeat("a", 1025), "bad=%GG", "foo=1;bar=2",
} {
t.Run(query[:min(len(query), 70)], func(t *testing.T) {
- h, f := environmentFilesHandler(t, true)
+ h, f := environmentFilesHandler(t)
w := requestEnvironmentFiles(h, f.environment.ID, "?"+query, "files-key")
if w.Code != 400 || f.lookups != 1 || f.reads != 0 {
t.Fatal("invalid query reached runtime", w.Code, w.Body, f)
@@ -235,7 +229,7 @@ func TestEnvironmentFilesSafeStoreAndReaderFailures(t *testing.T) {
{sessions.ErrNotFound, 404}, {sessions.ErrInvalidInput, 400}, {execution.ErrExecutionUnavailable, 503}, {errors.New("private-native-secret"), 500},
} {
unavailable := 0
- h, f := environmentFilesHandler(t, true, countEnvironmentFilesUnavailable(&unavailable))
+ h, f := environmentFilesHandler(t, countEnvironmentFilesUnavailable(&unavailable))
if target == "store" {
f.storeError = test.err
} else {
@@ -250,9 +244,4 @@ func TestEnvironmentFilesSafeStoreAndReaderFailures(t *testing.T) {
}
}
}
- unavailable := 0
- h, f := environmentFilesHandler(t, false, countEnvironmentFilesUnavailable(&unavailable))
- if w := requestEnvironmentFiles(h, f.environment.ID, "", "files-key"); w.Code != 503 || f.reads != 0 || unavailable != 1 {
- t.Fatal("missing reader accepted", w.Code, f)
- }
}
diff --git a/services/core/internal/api/environment_files_wire_test.go b/services/core/internal/api/environment_files_wire_test.go
index b47a0dc49..84426cdca 100644
--- a/services/core/internal/api/environment_files_wire_test.go
+++ b/services/core/internal/api/environment_files_wire_test.go
@@ -14,7 +14,7 @@ import (
// Official Environment Files wire rows F1–F9 of the environment-files-wire batch.
func TestEnvironmentFilesPageEnvelope(t *testing.T) {
- h, f := environmentFilesHandler(t, true)
+ h, f := environmentFilesHandler(t)
w := requestEnvironmentFiles(h, f.environment.ID, "", "files-key")
if w.Code != 200 || strings.TrimSpace(w.Body.String()) != `{"object":"page","data":[],"next":null,"has_more":false}` {
t.Fatalf("empty page: %d %s", w.Code, w.Body)
@@ -33,7 +33,7 @@ func TestEnvironmentFilesPageEnvelope(t *testing.T) {
}
func TestEnvironmentFilesIgnoresUnknownQueryKeys(t *testing.T) {
- h, f := environmentFilesHandler(t, true)
+ h, f := environmentFilesHandler(t)
f.result.Entries = []proto.WorkspaceDirectoryEntry{environmentFileEntry("a", 1)}
want := requestEnvironmentFiles(h, f.environment.ID, "", "files-key").Body.String()
for _, query := range []string{"?foo=bar", "?after=x&after=y", "?tenant_id=" + uuid.NewString(), "?include=all&foo", "?Path=/workspace/secret"} {
@@ -52,7 +52,7 @@ func TestEnvironmentFilesIgnoresUnknownQueryKeys(t *testing.T) {
func TestEnvironmentFilesRejectsRepeatedQueryKeys(t *testing.T) {
for _, key := range []string{"path", "limit", "order", "page"} {
t.Run(key, func(t *testing.T) {
- h, f := environmentFilesHandler(t, true)
+ h, f := environmentFilesHandler(t)
query := "?" + key + "=1&" + key + "=2"
w := requestEnvironmentFiles(h, f.environment.ID, query, "files-key")
assertListQueryError(t, w, "invalid_request_error", nil, "Failed to deserialize query string: duplicate field `"+key+"`")
@@ -92,7 +92,7 @@ func TestEnvironmentFilesPathErrors(t *testing.T) {
"/": directory,
} {
t.Run(path[:min(len(path), 40)], func(t *testing.T) {
- h, f := environmentFilesHandler(t, true)
+ h, f := environmentFilesHandler(t)
w := requestEnvironmentFiles(h, f.environment.ID, "?"+url.Values{"path": {path}}.Encode(), "files-key")
assertListQueryError(t, w, "invalid_request_error", nil, message)
if f.reads != 0 {
@@ -101,7 +101,7 @@ func TestEnvironmentFilesPathErrors(t *testing.T) {
})
}
for path, relative := range map[string]string{"/workspace": "", "/workspace/a": "a", "/workspace/a/b c": "a/b c"} {
- h, f := environmentFilesHandler(t, true)
+ h, f := environmentFilesHandler(t)
decodeEnvironmentFiles(t, requestEnvironmentFiles(h, f.environment.ID, "?"+url.Values{"path": {path}}.Encode(), "files-key"))
if f.directory != relative {
t.Fatal("canonical path changed", path, f.directory)
@@ -110,7 +110,7 @@ func TestEnvironmentFilesPathErrors(t *testing.T) {
}
func TestEnvironmentFilesPageTokenErrors(t *testing.T) {
- h, f := environmentFilesHandler(t, true)
+ h, f := environmentFilesHandler(t)
f.result.Entries = []proto.WorkspaceDirectoryEntry{environmentFileEntry("a", 1), environmentFileEntry("b", 2)}
page := decodeEnvironmentFiles(t, requestEnvironmentFiles(h, f.environment.ID, "?limit=1", "files-key"))
for name, query := range map[string]url.Values{
diff --git a/services/core/internal/api/environment_installation.go b/services/core/internal/api/environment_installation.go
index caa666ef7..3899bbf4d 100644
--- a/services/core/internal/api/environment_installation.go
+++ b/services/core/internal/api/environment_installation.go
@@ -25,17 +25,8 @@ type NativeInstaller struct {
Catalog *nativeinstaller.Catalog
}
-// nativeInstaller returns this Core's native installer, or nil when it serves
-// none.
-func (h *Handler) nativeInstaller() *NativeInstaller {
- if h.Execution == nil {
- return nil
- }
- return h.Execution.NativeInstaller
-}
-
func (h *Handler) installationFor(ctx context.Context, principal identity.Principal, environment string) (*v1.EnvironmentInstallation, error) {
- installer := h.nativeInstaller()
+ installer := h.Execution.NativeInstaller
result := &v1.EnvironmentInstallation{Status: "unavailable", Message: "This Core has no matching native installation distribution. Ask its operator to install the qualified release artifacts."}
if installer == nil {
return result, nil
@@ -52,7 +43,7 @@ func (h *Handler) installationFor(ctx context.Context, principal identity.Princi
}
func (h *Handler) addSessionInstallation(w http.ResponseWriter, r *http.Request, response *v1.Session) error {
- if response.Environment.Type != "self_hosted" || h.nativeInstaller() == nil {
+ if response.Environment.Type != "self_hosted" || h.Execution.NativeInstaller == nil {
return nil
}
principal, ok := r.Context().Value(principalContextKey{}).(identity.Principal)
@@ -69,7 +60,7 @@ func (h *Handler) addSessionInstallation(w http.ResponseWriter, r *http.Request,
}
func (h *Handler) registerNativeInstallationRoutes(r chi.Router) {
- installer := h.nativeInstaller()
+ installer := h.Execution.NativeInstaller
if installer == nil {
return
}
@@ -123,7 +114,7 @@ func (h *Handler) prepareNativeInstallation(w http.ResponseWriter, r *http.Reque
writeSessionsError(w, r, err)
return
}
- response, err := sessionResponse(session, h.executorURL())
+ response, err := sessionResponse(session, h.Execution.ExecutorURL)
if err != nil {
writeSessionsError(w, r, err)
return
diff --git a/services/core/internal/api/environment_installation_test.go b/services/core/internal/api/environment_installation_test.go
index 4cf35ef2d..453bcd263 100644
--- a/services/core/internal/api/environment_installation_test.go
+++ b/services/core/internal/api/environment_installation_test.go
@@ -39,7 +39,6 @@ func TestSelfHostedCreationReturnsInstallationWithoutWebCredential(t *testing.T)
fakes.sessionCreation.findSessionCreation, fakes.sessionCreation.createSession = f.FindSessionCreation, f.CreateSession
fakes.modelProviders.resolve = fixtureDeploymentProvider
fakes.environments.authorizeEnvironmentInstallation, fakes.environments.validateEnvironmentInstallation = f.AuthorizeEnvironmentInstallation, f.ValidateEnvironmentInstallation
- deps.Execution = fakes.execution()
deps.Execution.NativeInstaller = &NativeInstaller{Version: "build", Base: "https://core.example/api/v1/agent-daemon/install/", Catalog: &nativeinstaller.Catalog{Version: "build"}}
handler := newTestHandler(t, deps)
body := `{"agent":{"model":"model"},"environment":{"type":"self_hosted","workspace_directory":"/workspace"},"x_agents_core":{"model_provider":{"protocol":"responses","base_url":"https://model.example/v1","api_key":"fixture-model"}}}`
diff --git a/services/core/internal/api/environment_templates_test.go b/services/core/internal/api/environment_templates_test.go
index 90f5d08dc..077694c45 100644
--- a/services/core/internal/api/environment_templates_test.go
+++ b/services/core/internal/api/environment_templates_test.go
@@ -11,7 +11,6 @@ import (
"strings"
"testing"
- "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmenttemplates"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/textvalue"
@@ -26,7 +25,6 @@ func TestEnvironmentTemplatesErrors(t *testing.T) {
{environmenttemplates.ErrNotFound, 404, "not_found_error"},
{fmt.Errorf("create: %w", environmenttemplates.ErrInvalidInput), 400, invalidInputMessage},
{textvalue.ErrUnstorable, 400, unstorableTextMessage},
- {credentialcrypto.ErrUnavailable, 503, "credential_storage_unavailable"},
{errors.New("template-canary"), 500, "internal_error"},
} {
response := httptest.NewRecorder()
diff --git a/services/core/internal/api/errors.go b/services/core/internal/api/errors.go
index 2e92da06d..ff91dce09 100644
--- a/services/core/internal/api/errors.go
+++ b/services/core/internal/api/errors.go
@@ -8,7 +8,6 @@ import (
v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1"
"github.com/MiniMax-AI/OpenAgentCore/internal/obs/log"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/adminaudit"
- "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/textvalue"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit"
)
@@ -139,17 +138,6 @@ func writeTextValueError(w http.ResponseWriter, r *http.Request, err error) bool
return true
}
-// writeCredentialUnavailableError reports a request that needs credential
-// encryption on a service without a credential key, and returns false for any
-// other error.
-func writeCredentialUnavailableError(w http.ResponseWriter, r *http.Request, err error) bool {
- if !errors.Is(err, credentialcrypto.ErrUnavailable) {
- return false
- }
- writeError(w, http.StatusServiceUnavailable, "credential_storage_unavailable", "Credential encryption is not configured on this service.")
- return true
-}
-
// writeAuditSourceError reports write or administrator provenance that cannot
// be recorded, so the write failed closed, and returns false for any other
// error.
diff --git a/services/core/internal/api/errors_agents.go b/services/core/internal/api/errors_agents.go
index 3fc3a8df4..f8577c622 100644
--- a/services/core/internal/api/errors_agents.go
+++ b/services/core/internal/api/errors_agents.go
@@ -10,7 +10,7 @@ import (
// writeAgentsError reports an error of the Agent operations.
func writeAgentsError(w http.ResponseWriter, r *http.Request, err error) {
- if writeStoredDataError(w, r, err) || writeTextValueError(w, r, err) || writeAuditSourceError(w, r, err) || writeCredentialUnavailableError(w, r, err) {
+ if writeStoredDataError(w, r, err) || writeTextValueError(w, r, err) || writeAuditSourceError(w, r, err) {
return
}
switch {
diff --git a/services/core/internal/api/errors_deployment.go b/services/core/internal/api/errors_deployment.go
index 0da81d951..76646108d 100644
--- a/services/core/internal/api/errors_deployment.go
+++ b/services/core/internal/api/errors_deployment.go
@@ -23,7 +23,7 @@ func writeDeploymentError(w http.ResponseWriter, r *http.Request, err error) {
case errors.Is(err, deployment.ErrNotFound):
writeError(w, http.StatusNotFound, "not_found_error", "Resource not found.")
default:
- if writeAuditSourceError(w, r, err) || writeTextValueError(w, r, err) || writeCredentialUnavailableError(w, r, err) {
+ if writeAuditSourceError(w, r, err) || writeTextValueError(w, r, err) {
return
}
writeInternalError(w, r)
diff --git a/services/core/internal/api/errors_environmenttemplates.go b/services/core/internal/api/errors_environmenttemplates.go
index f48c1abb0..2d935592e 100644
--- a/services/core/internal/api/errors_environmenttemplates.go
+++ b/services/core/internal/api/errors_environmenttemplates.go
@@ -16,7 +16,7 @@ func writeEnvironmentTemplatesError(w http.ResponseWriter, r *http.Request, err
case errors.Is(err, environmenttemplates.ErrInvalidInput):
writeError(w, http.StatusBadRequest, "invalid_request", invalidInputMessage)
default:
- if writeAuditSourceError(w, r, err) || writeTextValueError(w, r, err) || writeCredentialUnavailableError(w, r, err) {
+ if writeAuditSourceError(w, r, err) || writeTextValueError(w, r, err) {
return
}
writeInternalError(w, r)
diff --git a/services/core/internal/api/errors_files.go b/services/core/internal/api/errors_files.go
index ed21fc37c..0b8d1ab9b 100644
--- a/services/core/internal/api/errors_files.go
+++ b/services/core/internal/api/errors_files.go
@@ -12,7 +12,7 @@ import (
// writeFilesError maps a files error to its public response. notFoundParam
// names the parameter a missing File came from.
func writeFilesError(w http.ResponseWriter, r *http.Request, err error, notFoundParam ...string) {
- if writeAuditSourceError(w, r, err) || writeTextValueError(w, r, err) || writeCredentialUnavailableError(w, r, err) {
+ if writeAuditSourceError(w, r, err) || writeTextValueError(w, r, err) {
return
}
switch {
diff --git a/services/core/internal/api/errors_modelconfiguration.go b/services/core/internal/api/errors_modelconfiguration.go
index 88af07ce3..cb6fc7707 100644
--- a/services/core/internal/api/errors_modelconfiguration.go
+++ b/services/core/internal/api/errors_modelconfiguration.go
@@ -19,7 +19,7 @@ func writeModelConfigurationError(w http.ResponseWriter, r *http.Request, err er
}
return
}
- if writeAuditSourceError(w, r, err) || writeTextValueError(w, r, err) || writeCredentialUnavailableError(w, r, err) {
+ if writeAuditSourceError(w, r, err) || writeTextValueError(w, r, err) {
return
}
writeInternalError(w, r)
diff --git a/services/core/internal/api/errors_projects.go b/services/core/internal/api/errors_projects.go
index 9882c604a..58aa882ff 100644
--- a/services/core/internal/api/errors_projects.go
+++ b/services/core/internal/api/errors_projects.go
@@ -31,7 +31,7 @@ func writeProjectsError(w http.ResponseWriter, r *http.Request, err error) {
case errors.Is(err, projects.ErrAPIKeyExists):
writeError(w, http.StatusConflict, "project_api_key_exists", "This API key ID already exists. List its metadata and revoke it explicitly if the secret was not saved.")
default:
- if writeAuditSourceError(w, r, err) || writeTextValueError(w, r, err) || writeCredentialUnavailableError(w, r, err) {
+ if writeAuditSourceError(w, r, err) || writeTextValueError(w, r, err) {
return
}
writeInternalError(w, r)
diff --git a/services/core/internal/api/errors_sessions.go b/services/core/internal/api/errors_sessions.go
index 64ed5b977..32a2328a2 100644
--- a/services/core/internal/api/errors_sessions.go
+++ b/services/core/internal/api/errors_sessions.go
@@ -72,7 +72,7 @@ func writeSessionsError(w http.ResponseWriter, r *http.Request, err error) {
case errors.Is(err, sessions.ErrInvalidInput), errors.Is(err, environmentconfig.ErrInvalid):
writeError(w, http.StatusBadRequest, "invalid_request", invalidInputMessage)
default:
- if writeAuditSourceError(w, r, err) || writeTextValueError(w, r, err) || writeCredentialUnavailableError(w, r, err) {
+ if writeAuditSourceError(w, r, err) || writeTextValueError(w, r, err) {
return
}
writeInternalError(w, r)
diff --git a/services/core/internal/api/errors_skills.go b/services/core/internal/api/errors_skills.go
index 7877587a3..89109e2af 100644
--- a/services/core/internal/api/errors_skills.go
+++ b/services/core/internal/api/errors_skills.go
@@ -31,7 +31,7 @@ func writeSkillsError(w http.ResponseWriter, r *http.Request, err error) {
writeError(w, http.StatusNotFound, code, "Resource not found.")
case errors.Is(err, skills.ErrInvalidInput):
writeError(w, http.StatusBadRequest, "invalid_request", invalidInputMessage)
- case writeAuditSourceError(w, r, err) || writeTextValueError(w, r, err) || writeCredentialUnavailableError(w, r, err):
+ case writeAuditSourceError(w, r, err) || writeTextValueError(w, r, err):
default:
writeInternalError(w, r)
}
diff --git a/services/core/internal/api/errors_skills_test.go b/services/core/internal/api/errors_skills_test.go
index 7ff09785b..f08aaa25d 100644
--- a/services/core/internal/api/errors_skills_test.go
+++ b/services/core/internal/api/errors_skills_test.go
@@ -7,7 +7,6 @@ import (
"net/http/httptest"
"testing"
- "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/skills"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/textvalue"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit"
@@ -41,8 +40,6 @@ func TestWriteSkillsError(t *testing.T) {
`{"error":{"message":"` + unstorableTextMessage + `","type":"invalid_request_error","code":"invalid_request_error","param":null}}`},
{"audit source", "/v1/skills", fmt.Errorf("record: %w", writeaudit.ErrInvalidSource), http.StatusBadRequest,
`{"error":{"message":"` + invalidInputMessage + `","type":"invalid_request_error","code":"invalid_request","param":null}}`},
- {"credential key missing", "/v1/skills", credentialcrypto.ErrUnavailable, http.StatusServiceUnavailable,
- `{"error":{"message":"Credential encryption is not configured on this service.","type":"server_error","code":"credential_storage_unavailable","param":null}}`},
{"unknown", "/v1/skills", errors.New("connection reset"), http.StatusInternalServerError,
`{"error":{"message":"The operation could not be completed.","type":"server_error","code":"internal_error","param":null}}`},
} {
diff --git a/services/core/internal/api/errors_test.go b/services/core/internal/api/errors_test.go
index 25cc78eb9..9ade94cb3 100644
--- a/services/core/internal/api/errors_test.go
+++ b/services/core/internal/api/errors_test.go
@@ -11,7 +11,6 @@ import (
v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/adminaudit"
- "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/files"
@@ -188,7 +187,6 @@ func TestSharedPersistenceErrors(t *testing.T) {
{writeSessionsError, fmt.Errorf("write: %w", textvalue.ErrUnstorable), 400, unstorableTextMessage},
{writeAuditError, textvalue.ErrUnstorable, 400, unstorableTextMessage},
{writeDeploymentError, deployment.ErrInvalidInput, 400, invalid},
- {writeSessionsError, credentialcrypto.ErrUnavailable, 503, "credential_storage_unavailable"},
{writeAuditError, errors.New("canary"), 500, "internal_error"},
} {
response := respond(test.write, test.err)
diff --git a/services/core/internal/api/errors_vaults.go b/services/core/internal/api/errors_vaults.go
index 430e2272d..c98cd10fc 100644
--- a/services/core/internal/api/errors_vaults.go
+++ b/services/core/internal/api/errors_vaults.go
@@ -10,7 +10,7 @@ import (
// writeVaultsError maps a Vault, Credential or Session credential selection
// error to its public error response.
func writeVaultsError(w http.ResponseWriter, r *http.Request, err error) {
- if writeAuditSourceError(w, r, err) || writeTextValueError(w, r, err) || writeCredentialUnavailableError(w, r, err) {
+ if writeAuditSourceError(w, r, err) || writeTextValueError(w, r, err) {
return
}
var selection *vaults.MCPCredentialSelectionError
diff --git a/services/core/internal/api/handler.go b/services/core/internal/api/handler.go
index 33d76be58..7d848e47e 100644
--- a/services/core/internal/api/handler.go
+++ b/services/core/internal/api/handler.go
@@ -241,14 +241,6 @@ func (h *Handler) createSession(w http.ResponseWriter, r *http.Request) {
}
return
}
- if input.Environment.Type == "self_hosted" && h.Execution == nil {
- writeError(w, http.StatusServiceUnavailable, "execution_unavailable", "Self-hosted execution is not configured on this service.")
- return
- }
- if input.Environment.Type == "openai_hosted" && h.Sandboxes == nil {
- writeError(w, http.StatusServiceUnavailable, "execution_unavailable", "Hosted execution is not configured on this service.")
- return
- }
executionConfiguration := sessionExecutionProjection(input, saved, inheritedProvider, provider, selectedEngine, configuration)
createInput := sessions.CreateSession{
ExecutionConfiguration: &executionConfiguration,
@@ -260,10 +252,6 @@ func (h *Handler) createSession(w http.ResponseWriter, r *http.Request) {
}
create := h.SessionCreation.CreateSession
if len(initialInputs) > 0 || input.Environment.Type == "openai_hosted" {
- if h.Execution == nil {
- writeError(w, http.StatusServiceUnavailable, "execution_unavailable", "Execution input is not enabled on this service.")
- return
- }
create = h.Execution.SessionAdmission.CreateSession
}
result, err := create(r.Context(), tenantID(r), createInput)
@@ -292,7 +280,7 @@ func (h *Handler) respondSession(w http.ResponseWriter, r *http.Request, session
}
func (h *Handler) respondSessionStatus(w http.ResponseWriter, r *http.Request, session sessions.Session, status int) {
- response, err := sessionResponse(session, h.executorURL())
+ response, err := sessionResponse(session, h.Execution.ExecutorURL)
if err != nil {
writeSessionsError(w, r, err)
return
@@ -320,7 +308,7 @@ func (h *Handler) listSessions(w http.ResponseWriter, r *http.Request) {
}
response := v1.SessionList{Data: make([]v1.Session, 0, len(page.Sessions)), HasMore: page.NextCursor != ""}
for _, session := range page.Sessions {
- item, err := sessionResponse(session, h.executorURL())
+ item, err := sessionResponse(session, h.Execution.ExecutorURL)
if err != nil {
writeSessionsError(w, r, err)
return
diff --git a/services/core/internal/api/handler_test.go b/services/core/internal/api/handler_test.go
index 04b28b3f6..a8909dc2c 100644
--- a/services/core/internal/api/handler_test.go
+++ b/services/core/internal/api/handler_test.go
@@ -75,10 +75,9 @@ func testHandler(t *testing.T, configure ...func(*Dependencies, *testFakes)) (ht
return newTestHandler(t, deps), s, tenant
}
-// admitSessions enables Execution whose Worker admits Session creation, as it
-// does for a Session with initial input, into the recording store.
+// admitSessions makes the Worker admit Session creation, as it does for a
+// Session with initial input, into the recording store.
func admitSessions(d *Dependencies, f *testFakes) {
- d.Execution = f.execution()
f.sessionAdmission.createSession = f.sessionCreation.createSession
}
@@ -144,10 +143,7 @@ func TestHTTPRejectsUntrustedOrUnsupportedRequests(t *testing.T) {
{"invalid auth", "Bearer wrong", "agents=v1", "/v1/agents/sessions", valid, 401},
{"missing beta", "Bearer test-api-key", "", "/v1/agents/sessions", valid, 400},
{"tenant body", "Bearer test-api-key", "agents=v1", "/v1/agents/sessions", strings.Replace(valid, `"agent":`, `"tenant_id":"other","agent":`, 1), 400},
- {"hosted environment without managed deployment", "Bearer test-api-key", "agents=v1", "/v1/agents/sessions", strings.Replace(strings.Replace(valid, `"none"`, `"openai_hosted"`, 1), `"input":`, fixtureSessionProvider+`,"input":`, 1), 503},
{"self-hosted environment", "Bearer test-api-key", "agents=v1", "/v1/agents/sessions", strings.Replace(valid, `"none"`, `"self_hosted"`, 1), 400},
- {"initial input", "Bearer test-api-key", "agents=v1", "/v1/agents/sessions", valid, 503},
- {"stream unavailable", "Bearer test-api-key", "agents=v1", "/v1/agents/sessions", strings.Replace(valid, `"agent":`, `"stream":true,"agent":`, 1), 503},
{"unknown saved agent", "Bearer test-api-key", "agents=v1", "/v1/agents/sessions", strings.Replace(valid, `"agent":`, `"agent_id":"saved","agent":`, 1), 404},
{"saved agent without its model provider bundle", "Bearer test-api-key", "agents=v1", "/v1/agents/sessions", strings.Replace(valid, `"agent":`, `"agent_id":"saved","agent":`, 1), 500},
{"unknown agent option", "Bearer test-api-key", "agents=v1", "/v1/agents/sessions", strings.Replace(valid, `"model":`, `"tools":[{}],"model":`, 1), 400},
diff --git a/services/core/internal/api/harness_test.go b/services/core/internal/api/harness_test.go
index 57828de4f..38c7715ca 100644
--- a/services/core/internal/api/harness_test.go
+++ b/services/core/internal/api/harness_test.go
@@ -26,18 +26,13 @@ func TestSessionHarnessAdmission(t *testing.T) {
{"empty", `,"x_agents_core":{}`, "", `{"type":"none"}`, "", true, 400},
{"unknown nested", `,"x_agents_core":{"harness":"codex","model":"wrong"}`, "", `{"type":"none"}`, "", true, 400},
{"claude verbosity", `,"x_agents_core":{"harness":"claude_sdk"}`, `,"text":{"verbosity":"high"}`, `{"type":"none"}`, "", true, 400},
- {"mcode self-hosted requires executor configuration", `,"x_agents_core":{"harness":"mcode"}`, "", `{"type":"self_hosted","workspace_directory":"/workspace"},` + fixtureAnthropicSessionProvider, "", true, 503},
} {
t.Run(tc.name, func(t *testing.T) {
h, s, _ := testHandler(t, func(d *Dependencies, f *testFakes) {
if tc.enabled {
d.Harnesses = []string{"claude_sdk", "mcode"}
}
- // Self-hosted execution needs an executor, which this Core lacks.
- if !strings.Contains(tc.environment, "self_hosted") {
- admitSessions(d, f)
- }
- f.metrics.recordUnavailable = func() {}
+ admitSessions(d, f)
})
r := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions", strings.NewReader(`{"agent":{"model":"fixture"`+tc.extension+tc.extra+`},"environment":`+tc.environment+`,"input":"Run on the selected harness."}`))
r.Header.Set("Authorization", "Bearer test-api-key")
diff --git a/services/core/internal/api/hosted_environment_test.go b/services/core/internal/api/hosted_environment_test.go
index bcc69d9fa..322223cca 100644
--- a/services/core/internal/api/hosted_environment_test.go
+++ b/services/core/internal/api/hosted_environment_test.go
@@ -89,7 +89,6 @@ func TestHostedCreationUsesExecutionAdmission(t *testing.T) {
for _, stream := range []bool{false, true} {
recorder := &hostedCreationRecorder{}
handler, fixture := environmentCreationHandler(t, "codex", func(d *Dependencies, f *testFakes) {
- d.Execution, d.Sandboxes = f.execution(), f.sandboxes()
f.sessionAdmission.createSession = recorder.CreateSession
})
input := ""
diff --git a/services/core/internal/api/inputs.go b/services/core/internal/api/inputs.go
index 84a7a0adf..5bc15f86e 100644
--- a/services/core/internal/api/inputs.go
+++ b/services/core/internal/api/inputs.go
@@ -57,10 +57,6 @@ func (h *Handler) createEvents(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusAccepted)
return
}
- if h.Execution == nil {
- writeError(w, http.StatusServiceUnavailable, "execution_unavailable", "Execution is not enabled on this service.")
- return
- }
inputs, err := executionInputs(request.Events)
if err != nil {
writeSessionsError(w, r, err)
diff --git a/services/core/internal/api/inputs_test.go b/services/core/internal/api/inputs_test.go
index 4af2099d2..f6cb61e63 100644
--- a/services/core/internal/api/inputs_test.go
+++ b/services/core/internal/api/inputs_test.go
@@ -25,9 +25,8 @@ func (s *inputRecorder) SubmitInputs(_ context.Context, tenant, session, key str
return nil, s.err
}
-// admit enables Execution whose Worker records submitted inputs in s.
+// admit makes the Worker record submitted inputs in s.
func (s *inputRecorder) admit(d *Dependencies, f *testFakes) {
- d.Execution = f.execution()
f.inputAdmission.submitInputs = s.SubmitInputs
}
diff --git a/services/core/internal/api/installation.go b/services/core/internal/api/installation.go
index ec1ba9f55..8d18088ac 100644
--- a/services/core/internal/api/installation.go
+++ b/services/core/internal/api/installation.go
@@ -11,12 +11,12 @@ import (
// environment and build; configuration is the process settings it loaded.
type Installation struct {
Object string `json:"object" enums:"core.installation"`
- // The ID in OAC_INSTALLATION_ID_FILE; null when Core runs without the sandbox manager.
- InstallationID *string `json:"installation_id" extensions:"x-nullable"`
- // OAC_PUBLIC_URL: the origin applications, nodes, sandboxes and self-hosted executors use. Null when unset.
- PublicURL *string `json:"public_url" extensions:"x-nullable"`
- // public_url followed by /v1; null when public_url is null.
- APIBaseURL *string `json:"api_base_url" extensions:"x-nullable"`
+ // The ID in OAC_INSTALLATION_ID_FILE.
+ InstallationID string `json:"installation_id"`
+ // OAC_PUBLIC_URL: the origin applications, nodes, sandboxes and self-hosted executors use.
+ PublicURL string `json:"public_url"`
+ // public_url followed by /v1.
+ APIBaseURL string `json:"api_base_url"`
// True when public_url names a loopback host, reachable only from the Core host.
LocalOnly bool `json:"local_only"`
// Full source commit Core was built from; null for development builds.
diff --git a/services/core/internal/api/installation_test.go b/services/core/internal/api/installation_test.go
index c8c047e89..8f65dcce9 100644
--- a/services/core/internal/api/installation_test.go
+++ b/services/core/internal/api/installation_test.go
@@ -24,7 +24,7 @@ func TestInstallationReadNeedsOnlyTheCoreKey(t *testing.T) {
return deployment.AddressBindings{Nodes: 2, NodesOnOtherAddress: 1}, nil
}
// No sandbox deployment: the read is available before any deployment exists.
- deps.Installation = Installation{InstallationID: &id, PublicURL: &public, Configuration: settings}
+ deps.Installation = Installation{InstallationID: id, PublicURL: public, APIBaseURL: public + "/v1", Configuration: settings}
h := newTestHandler(t, deps)
get := func(token string) *httptest.ResponseRecorder {
request := httptest.NewRequest(http.MethodGet, "/core/v1/installation", nil)
diff --git a/services/core/internal/api/model_provider_fixture_test.go b/services/core/internal/api/model_provider_fixture_test.go
index 845218400..6402ba177 100644
--- a/services/core/internal/api/model_provider_fixture_test.go
+++ b/services/core/internal/api/model_provider_fixture_test.go
@@ -27,6 +27,3 @@ func fixtureDeploymentProvider(_ context.Context, harness string) (*modelconfigu
// fixtureSessionProvider is a top-level Session request member for Codex.
const fixtureSessionProvider = `"x_agents_core":{"model_provider":{"protocol":"responses","base_url":"https://model.fixture.example/v1","api_key":"fixture-model-key"}}`
-
-// fixtureAnthropicSessionProvider is the Claude Code and MiniMax Code equivalent.
-const fixtureAnthropicSessionProvider = `"x_agents_core":{"model_provider":{"protocol":"anthropic","base_url":"https://model.fixture.example/anthropic","api_key":"fixture-model-key","context_window":200000,"max_output_tokens":8000}}`
diff --git a/services/core/internal/api/native_classification_integration_test.go b/services/core/internal/api/native_classification_integration_test.go
index 0a13c9e78..197922fd5 100644
--- a/services/core/internal/api/native_classification_integration_test.go
+++ b/services/core/internal/api/native_classification_integration_test.go
@@ -9,6 +9,7 @@ import (
"github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity"
+ "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/sessionpg"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions"
@@ -17,8 +18,8 @@ import (
func TestNativeClassificationPostgresRoundTripAndPublicPrivacy(t *testing.T) {
s, pool := diagnosticDatabase(t)
- h, _, tenant := adminTestHandler(t, databaseSessionReads(pool))
- reader := sessionpg.New(pgunit.NewPool(pool), nil)
+ h, _, tenant := adminTestHandler(t, databaseSessionReads(t, pool))
+ reader := sessionpg.New(pgunit.NewPool(pool), pgtest.CredentialKey(t))
for _, code := range []string{"authentication_error", "connection_failed", "secret-canary"} {
t.Run(code, func(t *testing.T) {
created, err := s.CreateSession(t.Context(), tenant, sessions.CreateSession{Creator: identity.Subject{Kind: "service_account", ID: "native-classification"}, Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"id":"agent_root","model":"test"},"environment":{"type":"none"}}`)})
diff --git a/services/core/internal/api/routing_test.go b/services/core/internal/api/routing_test.go
index c17942692..910246b45 100644
--- a/services/core/internal/api/routing_test.go
+++ b/services/core/internal/api/routing_test.go
@@ -97,7 +97,6 @@ func routingFixture(t *testing.T) (http.Handler, *chi.Mux, *routingStore) {
return files.File{}, files.ErrNotFound
}
deps.CoreKeys = coreKeys(t, routingAdminKey)
- deps.Execution, deps.Sandboxes = fakes.execution(), fakes.sandboxes()
return newTestHandler(t, deps), (&Handler{Dependencies: deps}).routes(), s
}
diff --git a/services/core/internal/api/sandbox_manager.go b/services/core/internal/api/sandbox_manager.go
index 99c814d47..aa1143b3c 100644
--- a/services/core/internal/api/sandbox_manager.go
+++ b/services/core/internal/api/sandbox_manager.go
@@ -59,9 +59,6 @@ type NodeAllocations interface {
// registerSandboxNodeRoutes serves node machine connections. They authenticate
// with an enrollment token or node credential, never the Core key.
func (h *Handler) registerSandboxNodeRoutes(r chi.Router) {
- if h.Sandboxes == nil {
- return
- }
r.Post("/api/v1/sandbox-node/enroll", h.enrollSandboxNode)
r.Get("/api/v1/sandbox-node/identity", h.sandboxNodeIdentity)
r.Get("/api/v1/sandbox-node/configuration", h.sandboxNodeConfiguration)
@@ -70,9 +67,6 @@ func (h *Handler) registerSandboxNodeRoutes(r chi.Router) {
// registerSandboxManagerRoutes adds sandbox deployment and node administration
// to the Core-key-authenticated /core/v1 router.
func (h *Handler) registerSandboxManagerRoutes(r chi.Router) {
- if h.Sandboxes == nil {
- return
- }
r.Get("/sandbox/deployment", h.sandboxDeployment)
r.Post("/sandbox/providers/{provider}/discovery", h.discoverSandboxConfiguration)
r.Post("/sandbox/deployment", h.initializeSandboxDeployment)
diff --git a/services/core/internal/api/sandbox_manager_test.go b/services/core/internal/api/sandbox_manager_test.go
index 6e7959622..aff6e084b 100644
--- a/services/core/internal/api/sandbox_manager_test.go
+++ b/services/core/internal/api/sandbox_manager_test.go
@@ -18,7 +18,6 @@ func sandboxFakes(t testing.TB) (Dependencies, *testFakes) {
deps, fakes := testDependencies(t)
fakes.projectsReader.resolveAPIKey = projectKeys(t, callerBinding()).ResolveAPIKey
deps.CoreKeys = coreKeys(t, "administrator")
- deps.Execution, deps.Sandboxes = fakes.execution(), fakes.sandboxes()
return deps, fakes
}
diff --git a/services/core/internal/api/sandbox_selector_test.go b/services/core/internal/api/sandbox_selector_test.go
index f70dc9dd9..00bf02154 100644
--- a/services/core/internal/api/sandbox_selector_test.go
+++ b/services/core/internal/api/sandbox_selector_test.go
@@ -33,7 +33,6 @@ func TestSessionCreationRejectsSandboxNodeSelector(t *testing.T) {
} {
recorder := &sandboxCreationRecorder{}
handler, _ := environmentCreationHandler(t, "codex", func(d *Dependencies, f *testFakes) {
- d.Execution, d.Sandboxes = f.execution(), f.sandboxes()
f.sessionAdmission.createSession = recorder.CreateSession
})
request := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions", strings.NewReader(body))
diff --git a/services/core/internal/api/session_admission_test.go b/services/core/internal/api/session_admission_test.go
index aedd04a7b..66e6ce827 100644
--- a/services/core/internal/api/session_admission_test.go
+++ b/services/core/internal/api/session_admission_test.go
@@ -28,7 +28,7 @@ func TestSessionAdmissionRejectsBeforeResourceOrExecutionAccess(t *testing.T) {
}
t.Run(fmt.Sprintf("%s/%s/stream=%t", environment, input, stream), func(t *testing.T) {
// Any resource access, including creation retry lookup, fails the test.
- handler, _, _ := testHandler(t, forbidSessionAccess, func(d *Dependencies, f *testFakes) { d.Execution = f.execution() })
+ handler, _, _ := testHandler(t, forbidSessionAccess)
body := fmt.Sprintf(`{"agent":{"model":"example"},"environment":{"type":%q},"stream":%t%s}`, environment, stream, input)
for _, token := range []string{"test-api-key", "invalid"} {
request := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions", strings.NewReader(body))
diff --git a/services/core/internal/api/session_creation_stream.go b/services/core/internal/api/session_creation_stream.go
index 7ee530d23..73039138e 100644
--- a/services/core/internal/api/session_creation_stream.go
+++ b/services/core/internal/api/session_creation_stream.go
@@ -22,7 +22,7 @@ func (h *Handler) respondSessionCreationStream(w http.ResponseWriter, r *http.Re
openEventStream(w, http.StatusCreated)
return
}
- response, err := sessionResponse(result.Session, h.executorURL())
+ response, err := sessionResponse(result.Session, h.Execution.ExecutorURL)
if err != nil {
writeSessionsError(w, r, err)
return
@@ -45,7 +45,7 @@ func (h *Handler) respondSessionCreationStream(w http.ResponseWriter, r *http.Re
if err != nil {
return false, 0, err
}
- response, err := sessionResponse(session, h.executorURL())
+ response, err := sessionResponse(session, h.Execution.ExecutorURL)
return err == nil && sessionSettled(session, response), cursor, err
}
h.serveSessionEvents(w, r, result.Session, result.Cursor, &created, http.StatusCreated, settlement)
diff --git a/services/core/internal/api/session_creation_stream_test.go b/services/core/internal/api/session_creation_stream_test.go
index ada43b580..8ecd6cd2f 100644
--- a/services/core/internal/api/session_creation_stream_test.go
+++ b/services/core/internal/api/session_creation_stream_test.go
@@ -149,7 +149,6 @@ func newCreationStreamHarness(t *testing.T) *creationStreamHarness {
fakes.sessionCreation.findSessionCreation = fixture.FindSessionCreation
fakes.sessionEvents.sessionEventCursor, fakes.sessionEvents.sessionStreamSnapshot, fakes.sessionEvents.listSessionEvents = fixture.SessionEventCursor, fixture.SessionStreamSnapshot, fixture.ListSessionEvents
fakes.modelProviders.resolve = noDeploymentModelProvider
- deps.Execution = fakes.execution()
fakes.sessionAdmission.createSession = fixture.CreateSession
handler := newTestHandler(t, deps)
h := &creationStreamHarness{t: t, fixture: fixture}
diff --git a/services/core/internal/api/session_diagnostics.go b/services/core/internal/api/session_diagnostics.go
index 8eef13aac..9e14f19b5 100644
--- a/services/core/internal/api/session_diagnostics.go
+++ b/services/core/internal/api/session_diagnostics.go
@@ -72,7 +72,7 @@ func (h *Handler) getSessionDiagnostics(w http.ResponseWriter, r *http.Request)
writeSessionsError(w, r, err)
return
}
- public, err := sessionResponse(session, h.executorURL())
+ public, err := sessionResponse(session, h.Execution.ExecutorURL)
if err != nil {
writeSessionsError(w, r, err)
return
diff --git a/services/core/internal/api/session_diagnostics_public_compat_test.go b/services/core/internal/api/session_diagnostics_public_compat_test.go
index 4394dd0b2..78715c1ea 100644
--- a/services/core/internal/api/session_diagnostics_public_compat_test.go
+++ b/services/core/internal/api/session_diagnostics_public_compat_test.go
@@ -12,6 +12,7 @@ import (
v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity"
+ "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/sessionpg"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions"
@@ -27,7 +28,7 @@ func TestDiagnosticPublicCompatibility(t *testing.T) {
key := callerBinding()
deps, fakes := testDependencies(t)
fakes.projectsReader.resolveAPIKey = projectKeys(t, key).ResolveAPIKey
- databaseSessionReads(pool)(&deps, fakes)
+ databaseSessionReads(t, pool)(&deps, fakes)
h := newTestHandler(t, deps)
created, err := s.CreateSession(t.Context(), key.TenantID, sessions.CreateSession{Creator: identity.Subject{Kind: "service_account", ID: "compat-test"}, Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"id":"agent_root","model":"test"},"environment":{"type":"none"}}`)})
if err != nil {
@@ -67,9 +68,9 @@ func diagnosticRequest(handler http.Handler, path, token string) *httptest.Respo
// databaseSessionReads serves Session, Turn, diagnostic and Item reads from
// the Session adapter on pool.
-func databaseSessionReads(pool *pgxpool.Pool) func(*Dependencies, *testFakes) {
+func databaseSessionReads(t *testing.T, pool *pgxpool.Pool) func(*Dependencies, *testFakes) {
return func(d *Dependencies, _ *testFakes) {
- reader := sessionpg.New(pgunit.NewPool(pool), nil)
+ reader := sessionpg.New(pgunit.NewPool(pool), pgtest.CredentialKey(t))
d.SessionsReader, d.SessionAdmin, d.Items, d.Turns = reader, reader, reader, reader
}
}
@@ -78,7 +79,7 @@ func databaseSessionReads(pool *pgxpool.Pool) func(*Dependencies, *testFakes) {
// pool.
func submitMessage(t *testing.T, pool *pgxpool.Pool, tenant, session, key, text string) sessions.InputReceipt {
t.Helper()
- service, err := sessions.NewService(sessionpg.New(pgunit.NewPool(pool), nil), nil)
+ service, err := sessions.NewService(sessionpg.New(pgunit.NewPool(pool), pgtest.CredentialKey(t)), nil)
if err != nil {
t.Fatal(err)
}
@@ -138,7 +139,7 @@ func diagnosticDatabase(t *testing.T) (*sessions.Service, *pgxpool.Pool) {
if err = migrations.Apply(t.Context(), dsn); err != nil {
t.Fatal(err)
}
- service, err := sessions.NewService(sessionpg.New(pgunit.NewPool(pool), nil), nil)
+ service, err := sessions.NewService(sessionpg.New(pgunit.NewPool(pool), pgtest.CredentialKey(t)), nil)
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/internal/api/session_diagnostics_test.go b/services/core/internal/api/session_diagnostics_test.go
index 21ad01899..3265feb8a 100644
--- a/services/core/internal/api/session_diagnostics_test.go
+++ b/services/core/internal/api/session_diagnostics_test.go
@@ -14,7 +14,7 @@ import (
func TestDiagnosticsCoreHandlerDatabaseBoundary(t *testing.T) {
s, pool := diagnosticDatabase(t)
- h, _, tenant := adminTestHandler(t, databaseSessionReads(pool))
+ h, _, tenant := adminTestHandler(t, databaseSessionReads(t, pool))
created, err := s.CreateSession(t.Context(), tenant, sessions.CreateSession{Creator: identity.Subject{Kind: "service_account", ID: "diagnostic-test"}, Engine: "codex", IdempotencyKey: "diagnostics", Configuration: json.RawMessage(`{"agent":{"id":"agent_root","model":"test"},"environment":{"type":"none"}}`)})
if err != nil {
t.Fatal(err)
diff --git a/services/core/internal/api/session_environment_http_test.go b/services/core/internal/api/session_environment_http_test.go
index d607345c2..ab7d088a1 100644
--- a/services/core/internal/api/session_environment_http_test.go
+++ b/services/core/internal/api/session_environment_http_test.go
@@ -49,8 +49,7 @@ func TestSelfHostedSessionHTTPReadListMetadataAndLiveStream(t *testing.T) {
}).ResolveAPIKey
fixture.serve(fakes)
fakes.sessionsReader.listSessions, fakes.sessions.updateSessionMetadata = fixture.ListSessions, fixture.UpdateSessionMetadata
- // Self-hosted Sessions report the executor URL of the enabled Execution.
- deps.Execution = fakes.execution()
+ // Self-hosted Sessions report the executor URL.
deps.Execution.ExecutorURL = environmentOrigin
handler := newTestHandler(t, deps)
want, err := sessionResponse(session, environmentOrigin)
diff --git a/services/core/internal/api/session_initial_input_test.go b/services/core/internal/api/session_initial_input_test.go
index 58f716c3a..b778c836f 100644
--- a/services/core/internal/api/session_initial_input_test.go
+++ b/services/core/internal/api/session_initial_input_test.go
@@ -8,12 +8,11 @@ import (
"testing"
)
-// admitInto enables Execution whose Worker admits Session creation into s,
+// admitInto makes the Worker admit Session creation into s,
// apart from the store that creates Sessions without execution work. Input
// submission stays unexpected.
func admitInto(s *recordingStore) func(*Dependencies, *testFakes) {
return func(d *Dependencies, f *testFakes) {
- d.Execution = f.execution()
f.sessionAdmission.createSession = s.CreateSession
}
}
diff --git a/services/core/internal/api/stream.go b/services/core/internal/api/stream.go
index cfda35b6f..983472b0d 100644
--- a/services/core/internal/api/stream.go
+++ b/services/core/internal/api/stream.go
@@ -31,7 +31,7 @@ func (h *Handler) streamEvents(w http.ResponseWriter, r *http.Request) {
writeSessionsError(w, r, err)
return
}
- if _, err = sessionResponse(session, h.executorURL()); err != nil {
+ if _, err = sessionResponse(session, h.Execution.ExecutorURL); err != nil {
writeSessionsError(w, r, err)
return
}
@@ -113,7 +113,7 @@ func (h *Handler) serveSessionEvents(w http.ResponseWriter, r *http.Request, ses
if limit >= 0 && change.Sequence > limit {
return
}
- event, err := streamResponse(session, change, h.executorURL())
+ event, err := streamResponse(session, change, h.Execution.ExecutorURL)
if err != nil {
writeStreamFailure(write, id)
return
diff --git a/services/core/internal/api/testdata/core-errors.json b/services/core/internal/api/testdata/core-errors.json
index 256654003..74e75ca84 100644
--- a/services/core/internal/api/testdata/core-errors.json
+++ b/services/core/internal/api/testdata/core-errors.json
@@ -4,7 +4,6 @@
"console_sign_in_required",
"core_metrics_unavailable",
"core_unreachable",
- "credential_storage_unavailable",
"environment_unavailable",
"execution_unavailable",
"executor_credential_exists",
diff --git a/services/core/internal/api/validation_errors_test.go b/services/core/internal/api/validation_errors_test.go
index e0ec582f7..0c84bda60 100644
--- a/services/core/internal/api/validation_errors_test.go
+++ b/services/core/internal/api/validation_errors_test.go
@@ -61,7 +61,6 @@ func (s *validationStore) UpdateEnvironmentTemplate(_ context.Context, command e
// serve takes every write from the handler, and the Worker admits Sessions
// with initial input into s. Session reads are unexpected.
func (s *validationStore) serve(d *Dependencies, f *testFakes) {
- d.Execution = f.execution()
f.sessionAdmission.createSession = s.CreateSession
f.agents.create, f.agents.update = s.CreateAgent, s.UpdateAgent
f.vaults.createVault = s.CreateVault
diff --git a/services/core/internal/credentialcrypto/cipher.go b/services/core/internal/credentialcrypto/cipher.go
index 552bfb7fb..8a9136545 100644
--- a/services/core/internal/credentialcrypto/cipher.go
+++ b/services/core/internal/credentialcrypto/cipher.go
@@ -18,10 +18,6 @@ const (
bindingDomain = "parsar.agents-api.credential"
)
-// ErrUnavailable reports that this service has no credential encryption key,
-// so it can neither store nor read credential secrets.
-var ErrUnavailable = errors.New("credential encryption is not configured")
-
var (
errInvalidKey = errors.New("credentialcrypto: invalid encryption key")
errUnavailable = errors.New("credentialcrypto: cipher unavailable")
diff --git a/services/core/internal/deployment/errors.go b/services/core/internal/deployment/errors.go
index ff90ef7b4..e9001c419 100644
--- a/services/core/internal/deployment/errors.go
+++ b/services/core/internal/deployment/errors.go
@@ -20,6 +20,9 @@ var (
ErrNotConfigured = errors.New("the sandbox deployment is not configured")
ErrNodeInUse = errors.New("sandbox node retains resources")
ErrNodeCredential = errors.New("invalid sandbox node credential")
+ // ErrCredentialUnreadable reports a stored credential the credential key
+ // cannot open or authenticate, such as after the key was replaced.
+ ErrCredentialUnreadable = errors.New("sandbox deployment credential decryption failed")
// ErrAllocationConflict rejects an allocation change whose owner no longer
// matches the stored allocation, device binding, state or compute revision,
// or a replay for another installation.
diff --git a/services/core/internal/deployment/placement/placement.go b/services/core/internal/deployment/placement/placement.go
index 9b0c4277a..2a8b0ad55 100644
--- a/services/core/internal/deployment/placement/placement.go
+++ b/services/core/internal/deployment/placement/placement.go
@@ -51,7 +51,7 @@ type Rules struct {
}
// NewRules returns the rules for the provider declarations and the
-// installation public URL, which is empty when the installation has none.
+// installation public URL.
func NewRules(declarations Declarations, publicURL string) (*Rules, error) {
if declarations == nil {
return nil, errors.New("placement rules require provider declarations")
diff --git a/services/core/internal/deployment/storage.go b/services/core/internal/deployment/storage.go
index 662f7e926..c1f17785a 100644
--- a/services/core/internal/deployment/storage.go
+++ b/services/core/internal/deployment/storage.go
@@ -301,8 +301,7 @@ type DeploymentTx interface {
// previous owner epoch's node presence.
ClaimInstallation(installationID string) error
// SaveSelection stores the next generation. It seals a secret bound to the
- // installation and generation; without a key it returns
- // credentialcrypto.ErrUnavailable.
+ // installation and generation.
SaveSelection(selection SelectionRecord) error
RecordConfigurationMetadata(metadata json.RawMessage) error
// RetainGeneration keeps the current generation for the allocations that
@@ -348,9 +347,8 @@ type Record struct {
// credential is stored and could be opened, its secret.
Configuration sandbox.ConfigurationRecord
CredentialStored bool
- // CredentialError is why the stored credential could not be opened: no key
- // (credentialcrypto.ErrUnavailable) or a ciphertext the key cannot open or
- // authenticate (an internal error).
+ // CredentialError is why the stored credential could not be opened: a
+ // ciphertext the key cannot open or authenticate (an internal error).
CredentialError error
// Reset is nil unless a reset is in progress.
Reset *ResetState
diff --git a/services/core/internal/environmenttemplates/template.go b/services/core/internal/environmenttemplates/template.go
index dfb868cc0..7c6209ced 100644
--- a/services/core/internal/environmenttemplates/template.go
+++ b/services/core/internal/environmenttemplates/template.go
@@ -11,7 +11,7 @@ import (
)
// Template is a saved Template's safe metadata: it holds no confidential
-// setup, file contents or archives, so reading it needs no credential key.
+// setup, file contents or archives.
type Template struct {
ID string
Name *string
diff --git a/services/core/internal/execution/archive_cancellation_cleanup_test.go b/services/core/internal/execution/archive_cancellation_cleanup_test.go
index f6e658f96..a8b899ae7 100644
--- a/services/core/internal/execution/archive_cancellation_cleanup_test.go
+++ b/services/core/internal/execution/archive_cancellation_cleanup_test.go
@@ -10,6 +10,7 @@ import (
"time"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment"
+ "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/projectpg"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/projects"
@@ -77,7 +78,7 @@ func TestArchiveWaitingCleanupReceiptBarrier(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- _, service := testSessions(t, pool, testCredentialCipher(t))
+ _, service := testSessions(t, pool, pgtest.CredentialKey(t))
created, err := service.CreateSession(t.Context(), project.TenantID, sessions.CreateSession{Creator: identity.Subject{Kind: "service_account", ID: "fixture"}, Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"test-model"},"environment":{"type":"openai_hosted","network":{"access":"disabled"}}}`), ModelProvider: &v1.ModelProviderInput{Protocol: "responses", BaseURL: "https://model.fixture.example/v1", APIKey: "fixture-key"}, ModelProviderSource: v1.ModelProviderSourceSession})
if err != nil {
t.Fatal(err)
@@ -117,7 +118,7 @@ func TestArchiveWaitingCleanupReceiptBarrier(t *testing.T) {
if scenario.delivery {
server := httptest.NewUnstartedServer(nil)
wsURL := "ws://" + server.Listener.Addr().String() + "/api/v1/agent-daemon/ws"
- credentials, heartbeat := testSessions(t, pool, testCredentialCipher(t))
+ credentials, heartbeat := testSessions(t, pool, pgtest.CredentialKey(t))
handler, liveRegistry, err := runtime.NewGateway(credentials, heartbeat, credentials, wsURL)
if err != nil {
t.Fatal(err)
@@ -157,7 +158,7 @@ func TestArchiveWaitingCleanupReceiptBarrier(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- sessionReader, _ := testSessions(t, pool, testCredentialCipher(t))
+ sessionReader, _ := testSessions(t, pool, pgtest.CredentialKey(t))
kills := 0
expectedStatus := sessions.TurnWaiting
provider := waitingCleanupProvider{beforeKill: func() {
diff --git a/services/core/internal/execution/deployment_fixture_test.go b/services/core/internal/execution/deployment_fixture_test.go
index 6562e189a..1cb8c27b5 100644
--- a/services/core/internal/execution/deployment_fixture_test.go
+++ b/services/core/internal/execution/deployment_fixture_test.go
@@ -35,7 +35,6 @@ func fixtureRules(t *testing.T) *placement.Rules {
// testDeployment builds the pooled deployment service and reader and the
// deployment execution operations on lease, as cmd/server does for the Worker.
-// cipher is nil when the owner has no credential key.
func testDeployment(t *testing.T, pool *pgxpool.Pool, cipher *credentialcrypto.Cipher, lease *pgunit.Lease) (*deployment.Service, deployment.Reader, *deployment.ExecutionOperations) {
t.Helper()
adapter := deploymentpg.New(pgunit.NewPool(pool), cipher)
diff --git a/services/core/internal/execution/deployment_provider_observations_test.go b/services/core/internal/execution/deployment_provider_observations_test.go
index d2b3c7539..440da13b2 100644
--- a/services/core/internal/execution/deployment_provider_observations_test.go
+++ b/services/core/internal/execution/deployment_provider_observations_test.go
@@ -15,6 +15,7 @@ import (
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/modelconfiguration"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/modelconfigurationpg"
+ "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/sessionpg"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions"
@@ -81,7 +82,7 @@ func newFinishObservationFixture(t *testing.T, maxConnections int32) finishObser
}
func (f finishObservationFixture) start(t *testing.T) sessions.InputReceipt {
t.Helper()
- _, service := testSessions(t, f.pool, nil)
+ _, service := testSessions(t, f.pool, pgtest.CredentialKey(t))
receipts, err := service.SubmitInputs(t.Context(), f.tenant, f.session.ID, uuid.NewString(), []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"input":[{"role":"user","content":[{"type":"input_text","text":"fixture"}]}]}`)}})
if err != nil {
t.Fatal(err)
@@ -200,7 +201,7 @@ func TestFinishRunObservationLockTimeoutAndFailureKeepLease(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- d := Dispatcher{Observer: modelconfigurationpg.New(pgunit.NewPool(pool), nil)}
+ d := Dispatcher{Observer: modelconfigurationpg.New(pgunit.NewPool(pool), pgtest.CredentialKey(t))}
started := time.Now()
d.observeDeploymentProvider(f.tenant, f.session.ID, turn)
if elapsed := time.Since(started); elapsed < 900*time.Millisecond || elapsed > 2*time.Second {
@@ -226,7 +227,7 @@ func TestFinishRunObservationLockTimeoutAndFailureKeepLease(t *testing.T) {
if cleanup != nil {
cleanup()
}
- persisted, err := sessionpg.New(pgunit.NewPool(f.pool), nil).GetTurn(t.Context(), f.tenant, f.session.ID, receipt.TurnID)
+ persisted, err := sessionpg.New(pgunit.NewPool(f.pool), pgtest.CredentialKey(t)).GetTurn(t.Context(), f.tenant, f.session.ID, receipt.TurnID)
if err != nil || persisted.Status != sessions.TurnCompleted {
t.Fatal("terminal outcome lost", err)
}
diff --git a/services/core/internal/execution/owner_test.go b/services/core/internal/execution/owner_test.go
index 9da8bdb4b..e7e577f9e 100644
--- a/services/core/internal/execution/owner_test.go
+++ b/services/core/internal/execution/owner_test.go
@@ -8,6 +8,7 @@ import (
"time"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/modelconfiguration"
+ "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions"
@@ -192,7 +193,7 @@ func TestWorkerRunClosesLeaseAfterDrain(t *testing.T) {
lease := &closeCountingLease{t: t, inner: owner.Lease}
id := uuid.NewString()
// The Worker's first reconciliation scans the Session work.
- reader, service := testSessions(t, pool, nil)
+ reader, service := testSessions(t, pool, pgtest.CredentialKey(t))
dispatcher := &Dispatcher{Registry: runtimegateway.NewRegistry(), Credentials: &recordingCredentials{}, Observer: unusedObserver{t}, Deployment: deployments, DeploymentReader: deploymentReader, Sessions: service, SessionsReader: reader, ManagedRuntimes: NewDeferredRuntimeProvider(id, func(context.Context) (*RuntimeProvider, error) { return nil, nil }, unusedPreparation(t))}
worker, err := StartWorker(t.Context(), dispatcher, Owner{Lease: lease, Deployment: owner.Deployment, Sessions: owner.Sessions})
if err != nil {
diff --git a/services/core/internal/execution/sandbox_deployment_drain_test.go b/services/core/internal/execution/sandbox_deployment_drain_test.go
index 4ace349be..4c7ff3d7d 100644
--- a/services/core/internal/execution/sandbox_deployment_drain_test.go
+++ b/services/core/internal/execution/sandbox_deployment_drain_test.go
@@ -70,7 +70,7 @@ func delayedReadWriter(t *testing.T, armed *atomic.Bool, reading chan struct{},
t.Error(err)
}
})
- deployments, reader, operations := testDeployment(t, pool, nil, lease)
+ deployments, reader, operations := testDeployment(t, pool, pgtest.CredentialKey(t), lease)
return Owner{Lease: lease, Deployment: operations, Sessions: sessionExecution(t, lease)}, deployments, reader, pool
}
diff --git a/services/core/internal/execution/sandbox_reset_test.go b/services/core/internal/execution/sandbox_reset_test.go
index 439b16dcb..0af1e93a7 100644
--- a/services/core/internal/execution/sandbox_reset_test.go
+++ b/services/core/internal/execution/sandbox_reset_test.go
@@ -1,7 +1,6 @@
package execution
import (
- "bytes"
"context"
"errors"
"reflect"
@@ -45,21 +44,10 @@ func resetManagerConfig(t *testing.T, configure func(*pgxpool.Config)) (Owner, *
func resetManagerDB(t *testing.T, configure func(*pgxpool.Config)) (Owner, *deployment.Service, deployment.Reader, *pgxpool.Pool) {
t.Helper()
pool := pgtest.OpenIsolated(t, configure)
- owner, deployments, reader := testOwner(t, pool, testCredentialCipher(t))
+ owner, deployments, reader := testOwner(t, pool, pgtest.CredentialKey(t))
return owner, deployments, reader, pool
}
-// testCredentialCipher is the credential key of the adapters these tests
-// build on one database.
-func testCredentialCipher(t *testing.T) *credentialcrypto.Cipher {
- t.Helper()
- cipher, err := credentialcrypto.New(bytes.Repeat([]byte{8}, 32))
- if err != nil {
- t.Fatal(err)
- }
- return cipher
-}
-
// testOwner acquires the execution lease on pool and builds the deployment and
// Session execution operations on it, and the pooled deployment service and
// reader, as cmd/server does. The lease closes when the test ends.
@@ -195,7 +183,7 @@ func TestSandboxResetPublishesCommittedGenerationWithoutReading(t *testing.T) {
id := initializeE2BDeployment(t, owner)
// The manager reads the deployment through a reader that fails every read
// of the committed reset, so publication cannot depend on one.
- adapter := deploymentpg.New(pgunit.NewPool(pool), nil)
+ adapter := deploymentpg.New(pgunit.NewPool(pool), pgtest.CredentialKey(t))
errCommittedRead := errors.New("committed deployment read failed")
committedReads := 0
reader := &strictDeploymentReader{t: t, snapshot: func(ctx context.Context) (deployment.Snapshot, error) {
diff --git a/services/core/internal/modelconfiguration/errors.go b/services/core/internal/modelconfiguration/errors.go
index 1c4719fc9..b118885fc 100644
--- a/services/core/internal/modelconfiguration/errors.go
+++ b/services/core/internal/modelconfiguration/errors.go
@@ -3,8 +3,7 @@ package modelconfiguration
import "errors"
// Replace and Resolve report a configuration the Harness declaration rejects
-// with the contract's *v1.ModelProviderError, which names the field, and a
-// missing credential key with credentialcrypto.ErrUnavailable. A bundle that
+// with the contract's *v1.ModelProviderError, which names the field. A bundle that
// fails to open is an internal error. Storage passes textvalue.ErrUnstorable
// and adminaudit.ErrInvalidSource through unchanged.
var (
diff --git a/services/core/internal/modelconfiguration/service.go b/services/core/internal/modelconfiguration/service.go
index fa79b9b61..7b52a5148 100644
--- a/services/core/internal/modelconfiguration/service.go
+++ b/services/core/internal/modelconfiguration/service.go
@@ -40,8 +40,7 @@ func (s *Service) Delete(ctx context.Context, harness string) error {
}
// Resolve opens the Harness's default for Session creation. It returns nil
-// when the Harness has none, and credentialcrypto.ErrUnavailable without a
-// credential key.
+// when the Harness has none.
func (s *Service) Resolve(ctx context.Context, harness string) (*Snapshot, error) {
bundle, err := s.storage.LoadBundle(ctx, harness)
if errors.Is(err, ErrNotFound) {
diff --git a/services/core/internal/modelconfiguration/service_test.go b/services/core/internal/modelconfiguration/service_test.go
index 3f23c5ad1..909df0ed6 100644
--- a/services/core/internal/modelconfiguration/service_test.go
+++ b/services/core/internal/modelconfiguration/service_test.go
@@ -7,7 +7,6 @@ import (
"testing"
v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1"
- "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto"
"github.com/google/uuid"
)
@@ -77,12 +76,6 @@ func TestReplacePassesTheCompleteBundleWithItsSafeColumns(t *testing.T) {
if !reflect.DeepEqual(record.Configuration, validConfiguration()) {
t.Fatalf("bundle: %+v", record.Configuration)
}
- storage.replace = func(context.Context, Record) (Configuration, error) {
- return Configuration{}, credentialcrypto.ErrUnavailable
- }
- if _, err := newService(t, storage).Replace(t.Context(), Replacement{Harness: "codex", Configuration: validConfiguration()}); !errors.Is(err, credentialcrypto.ErrUnavailable) {
- t.Fatal("missing credential key", err)
- }
}
func TestReplaceRejectsBeforeStorage(t *testing.T) {
@@ -124,10 +117,9 @@ func TestResolveValidatesTheOpenedBundle(t *testing.T) {
if snapshot, err := newService(t, loaded(v1.ModelConfigurationInput{}, ErrNotFound)).Resolve(t.Context(), "codex"); snapshot != nil || err != nil {
t.Fatal("missing default", snapshot, err)
}
- for _, failure := range []error{errors.New("storage failed"), credentialcrypto.ErrUnavailable} {
- if _, err := newService(t, loaded(v1.ModelConfigurationInput{}, failure)).Resolve(t.Context(), "codex"); !errors.Is(err, failure) {
- t.Fatal("storage failure", err)
- }
+ failure := errors.New("storage failed")
+ if _, err := newService(t, loaded(v1.ModelConfigurationInput{}, failure)).Resolve(t.Context(), "codex"); !errors.Is(err, failure) {
+ t.Fatal("storage failure", err)
}
invalid := validConfiguration()
invalid.Model = ""
diff --git a/services/core/internal/modelconfiguration/storage.go b/services/core/internal/modelconfiguration/storage.go
index b132e80cc..49622417f 100644
--- a/services/core/internal/modelconfiguration/storage.go
+++ b/services/core/internal/modelconfiguration/storage.go
@@ -3,8 +3,7 @@ package modelconfiguration
import "context"
// Storage keeps one deployment default per Harness and seals and opens its
-// bundle; without a credential key, Replace and LoadBundle return
-// credentialcrypto.ErrUnavailable. Replace and Delete record the administrator
+// bundle. Replace and Delete record the administrator
// mutation in the same transaction, from the provenance the context carries;
// an audit failure aborts the change.
type Storage interface {
diff --git a/services/core/internal/persistence/postgres/agentpg/store.go b/services/core/internal/persistence/postgres/agentpg/store.go
index a52b917af..a2f519a27 100644
--- a/services/core/internal/persistence/postgres/agentpg/store.go
+++ b/services/core/internal/persistence/postgres/agentpg/store.go
@@ -27,10 +27,8 @@ type Store struct {
cipher *credentialcrypto.Cipher
}
-// New returns the Agent store. cipher is nil when Core has no credential key;
-// saving or opening a model provider bundle then fails with
-// credentialcrypto.ErrUnavailable. A bundle the key cannot open is an internal
-// error.
+// New returns the Agent store, which seals model provider bundles with cipher.
+// A bundle the key cannot open is an internal error.
func New(pool *pgunit.Pool, cipher *credentialcrypto.Cipher) *Store {
return &Store{pool: pool, cipher: cipher}
}
@@ -247,9 +245,6 @@ func (s *Store) GetAgentWithModelProvider(ctx context.Context, tenantID, agentID
if row.EncryptedConfig == nil {
return agent, nil, nil
}
- if s.cipher == nil {
- return agents.Agent{}, nil, credentialcrypto.ErrUnavailable
- }
raw, err := s.cipher.OpenAgentModelExecution(row.EncryptedConfig, agent.TenantID, agent.ID)
if err != nil {
return agents.Agent{}, nil, errors.New("agent model provider decryption failed")
@@ -268,7 +263,7 @@ func (s *Store) saveModelProvider(ctx context.Context, q *sqlc.Queries, tenant,
}
sealed, err := s.cipher.SealAgentModelExecution(raw, uuid.UUID(tenant.Bytes).String(), uuid.UUID(agent.Bytes).String())
if err != nil {
- return credentialcrypto.ErrUnavailable
+ return errors.New("agent model provider encryption failed")
}
return q.SaveAgentModelExecution(ctx, sqlc.SaveAgentModelExecutionParams{AgentID: agent, EncryptedConfig: sealed})
}
diff --git a/services/core/internal/persistence/postgres/agentpg/store_test.go b/services/core/internal/persistence/postgres/agentpg/store_test.go
index 2d30e5b2e..97e765d32 100644
--- a/services/core/internal/persistence/postgres/agentpg/store_test.go
+++ b/services/core/internal/persistence/postgres/agentpg/store_test.go
@@ -70,7 +70,7 @@ func count(t *testing.T, pool *pgxpool.Pool, query string, args ...any) int {
func TestAgentsPersistIndependentlyAndStayTenantScoped(t *testing.T) {
pool := pgtest.Open(t)
- store, service := open(t, pool, nil)
+ store, service := open(t, pool, pgtest.CredentialKey(t))
ctx := t.Context()
tenantA, tenantB := uuid.NewString(), uuid.NewString()
// Configuration is preserved without applying one harness's capabilities.
@@ -116,7 +116,7 @@ func TestAgentsPersistIndependentlyAndStayTenantScoped(t *testing.T) {
func TestAgentsRejectInvalidTenantsAndEmptyMetadataIsAMap(t *testing.T) {
pool := pgtest.Open(t)
- store, service := open(t, pool, nil)
+ store, service := open(t, pool, pgtest.CredentialKey(t))
ctx := t.Context()
tenant := uuid.NewString()
valid := agents.CreateCommand{Configuration: []byte(`{"model":"x"}`)}
@@ -144,7 +144,7 @@ func TestAgentsRejectInvalidTenantsAndEmptyMetadataIsAMap(t *testing.T) {
func TestAgentListPaginationIsolationAndReconnect(t *testing.T) {
pool := pgtest.Open(t)
- store, service := open(t, pool, nil)
+ store, service := open(t, pool, pgtest.CredentialKey(t))
ctx := t.Context()
tenant, other := uuid.NewString(), uuid.NewString()
empty, err := store.ListAgents(ctx, agents.ListQuery{TenantID: tenant, Limit: 2})
@@ -230,7 +230,7 @@ func TestAgentListPaginationIsolationAndReconnect(t *testing.T) {
func TestAgentUpdateRollbackAndCompleteSizeBound(t *testing.T) {
pool := pgtest.Open(t)
- store, service := open(t, pool, nil)
+ store, service := open(t, pool, pgtest.CredentialKey(t))
ctx := t.Context()
tenant := uuid.NewString()
configuration, err := json.Marshal(map[string]any{"model": "original", "instructions": strings.Repeat("x", 400*1024), "number": json.Number("9007199254740993")})
@@ -286,7 +286,7 @@ func TestAgentUpdateRollbackAndCompleteSizeBound(t *testing.T) {
func TestAgentDeleteIsTenantScoped(t *testing.T) {
pool := pgtest.Open(t)
- store, service := open(t, pool, nil)
+ store, service := open(t, pool, pgtest.CredentialKey(t))
ctx := t.Context()
tenant := uuid.NewString()
agent, err := service.Create(ctx, agents.CreateCommand{TenantID: tenant, Configuration: []byte(`{"model":"x"}`)})
@@ -347,23 +347,12 @@ func TestAgentModelExecutionAtomicEncryptedSnapshot(t *testing.T) {
if _, inherited := snapshot(store); inherited == nil || *inherited != *provider {
t.Fatal("provider snapshot mismatch")
}
- // Omitted provider updates and plain reads do not require the encryption key.
- keylessStore, keyless := open(t, pool, nil)
- if _, err := keyless.Update(ctx, agents.UpdateCommand{TenantID: tenant, AgentID: agent.ID, Configuration: []byte(`{"model":"new-model"}`)}); err != nil {
+ if _, err := service.Update(ctx, agents.UpdateCommand{TenantID: tenant, AgentID: agent.ID, Configuration: []byte(`{"model":"new-model"}`)}); err != nil {
t.Fatal(err)
}
- if _, err := keylessStore.GetAgent(ctx, tenant, agent.ID); err != nil {
- t.Fatal("plain read required Agent decryption", err)
- }
- if _, _, err := keylessStore.GetAgentWithModelProvider(ctx, tenant, agent.ID); !errors.Is(err, credentialcrypto.ErrUnavailable) {
- t.Fatal("missing key opened the bundle", err)
- }
if _, _, err := agentpg.New(pgunit.NewPool(pool), testCipher(t, 99)).GetAgentWithModelProvider(ctx, tenant, agent.ID); err == nil || err.Error() != "agent model provider decryption failed" {
t.Fatal("wrong key was not a decryption failure", err)
}
- if _, err := keyless.Create(ctx, create); !errors.Is(err, credentialcrypto.ErrUnavailable) {
- t.Fatal("unencrypted Agent create accepted", err)
- }
replacement := providerFixture(1)
replace := func(tenant string) agents.UpdateCommand {
return agents.UpdateCommand{TenantID: tenant, AgentID: agent.ID, Configuration: providerConfiguration(t, replacement, "codex"), ModelProvider: &agents.ModelProviderChange{Provider: replacement}}
@@ -371,9 +360,6 @@ func TestAgentModelExecutionAtomicEncryptedSnapshot(t *testing.T) {
if _, err := service.Update(ctx, replace(uuid.NewString())); !errors.Is(err, agents.ErrNotFound) {
t.Fatal("foreign tenant replacement accepted", err)
}
- if _, err := keyless.Update(ctx, replace(tenant)); !errors.Is(err, credentialcrypto.ErrUnavailable) {
- t.Fatal("unencrypted replacement accepted", err)
- }
if current, inherited := snapshot(store); inherited == nil || *inherited != *provider || !bytes.Contains(current.Configuration, []byte("new-model")) {
t.Fatal("failed replacement changed snapshot")
}
@@ -385,9 +371,6 @@ func TestAgentModelExecutionAtomicEncryptedSnapshot(t *testing.T) {
if current, inherited := snapshot(store); inherited == nil || *inherited != *provider || !bytes.Contains(current.Configuration, []byte("new-model")) {
t.Fatal("database rejection left a partial replacement")
}
- if n := count(t, pool, "SELECT count(*) FROM agents WHERE tenant_id=$1", tenant); n != 1 {
- t.Fatal("failed create persisted partial Agent", n)
- }
if _, err := service.Update(ctx, agents.UpdateCommand{TenantID: tenant, AgentID: agent.ID, Configuration: []byte(`{"x_agents_core":{"harness":"claude_sdk"}}`)}); !errors.Is(err, agents.ErrInvalidInput) {
t.Fatal("incompatible Harness-only update accepted", err)
}
@@ -399,13 +382,13 @@ func TestAgentModelExecutionAtomicEncryptedSnapshot(t *testing.T) {
if current, inherited := snapshot(store); inherited == nil || *inherited != *replacement || !bytes.Contains(current.Configuration, []byte(`"harness": "codex"`)) {
t.Fatal("provider-only replacement failed")
}
- if _, err := keyless.Update(ctx, agents.UpdateCommand{TenantID: tenant, AgentID: agent.ID, Configuration: []byte(`{"x_agents_core":{"harness":"codex"}}`)}); err != nil {
+ if _, err := service.Update(ctx, agents.UpdateCommand{TenantID: tenant, AgentID: agent.ID, Configuration: []byte(`{"x_agents_core":{"harness":"codex"}}`)}); err != nil {
t.Fatal(err)
}
if _, inherited := snapshot(store); inherited == nil || *inherited != *replacement {
t.Fatal("harness-only update lost provider")
}
- if _, err := keyless.Update(ctx, agents.UpdateCommand{TenantID: tenant, AgentID: agent.ID, Configuration: []byte(`{"x_agents_core":{"model_provider":null}}`), ModelProvider: &agents.ModelProviderChange{}}); err != nil {
+ if _, err := service.Update(ctx, agents.UpdateCommand{TenantID: tenant, AgentID: agent.ID, Configuration: []byte(`{"x_agents_core":{"model_provider":null}}`), ModelProvider: &agents.ModelProviderChange{}}); err != nil {
t.Fatal(err)
}
if current, inherited := snapshot(store); inherited != nil || !bytes.Contains(current.Configuration, []byte(`"harness": "codex"`)) {
@@ -645,7 +628,7 @@ func TestAgentWritesAuditInTheirTransaction(t *testing.T) {
// after another key updates and deletes the Agent.
func TestAgentAuditReadsFailuresAndStableOwnership(t *testing.T) {
pool := pgtest.Open(t)
- store, service := open(t, pool, nil)
+ store, service := open(t, pool, pgtest.CredentialKey(t))
tenant := uuid.NewString()
first, err := service.Create(auditContext(t.Context(), tenant, uuid.NewString(), "a"), agents.CreateCommand{TenantID: tenant, Configuration: []byte(`{"model":"fixture"}`)})
if err != nil {
@@ -686,7 +669,7 @@ func TestAgentAuditReadsFailuresAndStableOwnership(t *testing.T) {
// Malformed supplied provenance fails the write closed.
func TestAgentWriteRejectsInvalidAuditSource(t *testing.T) {
pool := pgtest.Open(t)
- _, service := open(t, pool, nil)
+ _, service := open(t, pool, pgtest.CredentialKey(t))
tenant := uuid.NewString()
ctx := writeaudit.WithSource(t.Context(), writeaudit.Source{KeyID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", Prefix: "pc_aaaaaaaa", Kind: "issued", TenantID: tenant, RequestID: uuid.NewString()})
if _, err := service.Create(ctx, agents.CreateCommand{TenantID: tenant, Configuration: []byte(`{"model":"x"}`)}); !errors.Is(err, writeaudit.ErrInvalidSource) {
diff --git a/services/core/internal/persistence/postgres/deploymentpg/records.go b/services/core/internal/persistence/postgres/deploymentpg/records.go
index c1e2cb3c5..0fc164c42 100644
--- a/services/core/internal/persistence/postgres/deploymentpg/records.go
+++ b/services/core/internal/persistence/postgres/deploymentpg/records.go
@@ -55,18 +55,15 @@ func translate(err error) error {
}
// record converts the stored deployment. open opens a stored credential; a
-// view never needs it. Without a key the credential error is
-// credentialcrypto.ErrUnavailable; a credential the key cannot open or
-// authenticate is an internal decryption error.
+// view never needs it. A credential the key cannot open or authenticate is an
+// internal decryption error.
func record(d sqlc.RuntimeDeployment, cipher *credentialcrypto.Cipher, open bool) deployment.Record {
r := deployment.Record{InstallationID: uuidString(d.InstallationID), Provider: d.ProviderKind, BackendFingerprint: d.BackendFingerprint,
Generation: uint64(d.Generation), OwnerEpoch: uint64(d.OwnerEpoch), Mode: d.Mode, Specification: d.Specification,
Configuration: sandbox.ConfigurationRecord{Public: d.ProviderConfig, Metadata: d.ProviderMetadata}, CredentialStored: len(d.ProviderCredential) > 0}
if r.CredentialStored && open {
- if cipher == nil {
- r.CredentialError = credentialcrypto.ErrUnavailable
- } else if secret, err := cipher.OpenSandboxDeployment(d.ProviderCredential, r.InstallationID, r.Generation); err != nil {
- r.CredentialError = errors.New("sandbox deployment credential decryption failed")
+ if secret, err := cipher.OpenSandboxDeployment(d.ProviderCredential, r.InstallationID, r.Generation); err != nil {
+ r.CredentialError = deployment.ErrCredentialUnreadable
} else {
r.Configuration.Secret = secret
}
diff --git a/services/core/internal/persistence/postgres/deploymentpg/seal_test.go b/services/core/internal/persistence/postgres/deploymentpg/seal_test.go
index f52e389ca..0fed62306 100644
--- a/services/core/internal/persistence/postgres/deploymentpg/seal_test.go
+++ b/services/core/internal/persistence/postgres/deploymentpg/seal_test.go
@@ -4,23 +4,23 @@ import (
"errors"
"testing"
- "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/deploymentpg"
+ "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit"
)
-// A missing key is credentialcrypto.ErrUnavailable, and a credential sealed
-// with another binding is a decryption failure, never a missing key. Node
-// transactions and snapshots never open the credential, so they need no key.
+// A credential sealed with another key or binding is a decryption failure.
+// Node transactions and snapshots never open the credential, so they keep
+// working under a replaced key.
func TestStoredCredentialNeedsTheKeyAndItsBinding(t *testing.T) {
f := newFixture(t)
changes, _ := f.execution(t)
id, view := f.initialize(t, changes, setupE2BSelection())
- keyless := deploymentpg.New(pgunit.NewPool(f.pool), nil)
- service := newService(t, keyless, fixturePublicURL)
- if _, err := service.Setup(t.Context()); !errors.Is(err, credentialcrypto.ErrUnavailable) {
- t.Fatal("a keyless Store opened the credential", err)
+ replaced := deploymentpg.New(pgunit.NewPool(f.pool), pgtest.CredentialKey(t))
+ service := newService(t, replaced, fixturePublicURL)
+ if _, err := service.Setup(t.Context()); !errors.Is(err, deployment.ErrCredentialUnreadable) {
+ t.Fatal("a replaced key opened the credential", err)
}
withoutCredential := func(reads deployment.NodeReads) error {
d, err := reads.LoadDeployment()
@@ -29,10 +29,10 @@ func TestStoredCredentialNeedsTheKeyAndItsBinding(t *testing.T) {
}
return err
}
- if err := keyless.WithNodes(t.Context(), func(tx deployment.NodeTx) error { return withoutCredential(tx) }); err != nil {
+ if err := replaced.WithNodes(t.Context(), func(tx deployment.NodeTx) error { return withoutCredential(tx) }); err != nil {
t.Fatal(err)
}
- if err := keyless.ReadNodes(t.Context(), withoutCredential); err != nil {
+ if err := replaced.ReadNodes(t.Context(), withoutCredential); err != nil {
t.Fatal(err)
}
sealed, err := f.cipher.SealSandboxDeployment([]byte("fixture-private-api-key"), id, view.Generation+1)
@@ -42,7 +42,7 @@ func TestStoredCredentialNeedsTheKeyAndItsBinding(t *testing.T) {
if _, err := f.pool.Exec(t.Context(), "UPDATE runtime_deployment SET provider_credential=$1", sealed); err != nil {
t.Fatal(err)
}
- if _, err := f.service.Setup(t.Context()); err == nil || err.Error() != "sandbox deployment credential decryption failed" || errors.Is(err, credentialcrypto.ErrUnavailable) {
+ if _, err := f.service.Setup(t.Context()); !errors.Is(err, deployment.ErrCredentialUnreadable) {
t.Fatal("a wrong binding was not a decryption failure", err)
}
}
diff --git a/services/core/internal/persistence/postgres/deploymentpg/store.go b/services/core/internal/persistence/postgres/deploymentpg/store.go
index decc3aaa9..612af96bc 100644
--- a/services/core/internal/persistence/postgres/deploymentpg/store.go
+++ b/services/core/internal/persistence/postgres/deploymentpg/store.go
@@ -25,8 +25,8 @@ type Store struct {
cipher *credentialcrypto.Cipher
}
-// New returns the deployment store. cipher is nil when Core has no credential
-// key; a stored credential then reads as credentialcrypto.ErrUnavailable.
+// New returns the deployment store, which seals the provider credential with
+// cipher.
func New(pool *pgunit.Pool, cipher *credentialcrypto.Cipher) *Store {
return &Store{pool: pool, cipher: cipher}
}
diff --git a/services/core/internal/persistence/postgres/deploymentpg/tx.go b/services/core/internal/persistence/postgres/deploymentpg/tx.go
index f48a83cbf..28f35e1c5 100644
--- a/services/core/internal/persistence/postgres/deploymentpg/tx.go
+++ b/services/core/internal/persistence/postgres/deploymentpg/tx.go
@@ -303,9 +303,6 @@ func (t *deploymentTx) SaveSelection(selection deployment.SelectionRecord) error
}
params := sqlc.InitializeSandboxDeploymentParams{ProviderKind: selection.Provider, BackendFingerprint: selection.BackendFingerprint, Generation: int64(selection.Generation), Mode: selection.Mode, ProviderConfig: selection.Configuration.Public, ProviderMetadata: selection.Configuration.Metadata, Specification: selection.Specification}
if len(selection.Configuration.Secret) > 0 {
- if t.cipher == nil {
- return credentialcrypto.ErrUnavailable
- }
sealed, err := t.cipher.SealSandboxDeployment(selection.Configuration.Secret, selection.InstallationID, selection.Generation)
if err != nil {
return errors.New("sandbox deployment credential encryption failed")
diff --git a/services/core/internal/persistence/postgres/modelconfigurationpg/seal_test.go b/services/core/internal/persistence/postgres/modelconfigurationpg/seal_test.go
index b482438dc..181e8a5f8 100644
--- a/services/core/internal/persistence/postgres/modelconfigurationpg/seal_test.go
+++ b/services/core/internal/persistence/postgres/modelconfigurationpg/seal_test.go
@@ -8,30 +8,23 @@ import (
v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1"
- "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/modelconfiguration"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/modelconfigurationpg"
+ "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit"
)
// The Store seals the bundle to its Harness as Core always has, so a bundle
-// sealed before the Store owned the key still opens. A missing key is
-// credentialcrypto.ErrUnavailable, and a wrong binding is a decryption
-// failure, never a missing key or default.
+// sealed before the Store owned the key still opens. A bundle sealed under
+// another key or binding is a decryption failure, never a missing default.
func TestBundlesKeepTheirSealedFormat(t *testing.T) {
f := newFixture(t)
- keyless := modelconfigurationpg.New(pgunit.NewPool(f.pool), nil)
+ replaced := modelconfigurationpg.New(pgunit.NewPool(f.pool), pgtest.CredentialKey(t))
configuration := v1.ModelConfigurationInput{ModelProvider: fixtureProvider, Model: "fixture"}
- if _, err := keyless.LoadBundle(t.Context(), "codex"); !errors.Is(err, modelconfiguration.ErrNotFound) {
- t.Fatal("a missing default was not reported before the key", err)
+ if _, err := replaced.LoadBundle(t.Context(), "codex"); !errors.Is(err, modelconfiguration.ErrNotFound) {
+ t.Fatal("a missing default was not reported before decryption", err)
}
record := modelconfiguration.Record{Harness: "codex", Provider: *fixtureProvider.SafeView(), Model: "fixture", HarnessConfig: json.RawMessage(`{}`), Configuration: configuration}
- if _, err := keyless.Replace(admin(t), record); !errors.Is(err, credentialcrypto.ErrUnavailable) {
- t.Fatal("a keyless Store sealed a bundle", err)
- }
- if listed := f.list(t); len(listed) != 0 {
- t.Fatal("a keyless replacement was stored", listed)
- }
if _, err := f.adapter.Replace(admin(t), record); err != nil {
t.Fatal(err)
}
@@ -39,8 +32,8 @@ func TestBundlesKeepTheirSealedFormat(t *testing.T) {
if err != nil || !reflect.DeepEqual(loaded.Configuration, configuration) {
t.Fatal("the bundle did not round-trip", err)
}
- if _, err := keyless.LoadBundle(t.Context(), "codex"); !errors.Is(err, credentialcrypto.ErrUnavailable) {
- t.Fatal("a keyless Store opened a bundle", err)
+ if _, err := replaced.LoadBundle(t.Context(), "codex"); err == nil || err.Error() != "deployment model configuration decryption failed" {
+ t.Fatal("a replaced key opened a bundle", err)
}
// write stores a bundle sealed the way Core sealed it before this Store
// owned the key.
diff --git a/services/core/internal/persistence/postgres/modelconfigurationpg/store.go b/services/core/internal/persistence/postgres/modelconfigurationpg/store.go
index 1b6db598f..02a0a7d0d 100644
--- a/services/core/internal/persistence/postgres/modelconfigurationpg/store.go
+++ b/services/core/internal/persistence/postgres/modelconfigurationpg/store.go
@@ -43,9 +43,7 @@ var (
_ modelconfiguration.Observer = (*Store)(nil)
)
-// New returns a Store. Without a credential key (cipher nil), Replace and
-// LoadBundle fail with credentialcrypto.ErrUnavailable; List, Delete and
-// observations keep working.
+// New returns a Store that seals and opens bundles with cipher.
func New(pool *pgunit.Pool, cipher *credentialcrypto.Cipher) *Store {
return &Store{pool: pool, cipher: cipher}
}
@@ -67,16 +65,13 @@ func (s *Store) List(ctx context.Context) ([]modelconfiguration.Configuration, e
// new revision, which clears the replaced revision's observations, and audits
// the write in the same transaction.
func (s *Store) Replace(ctx context.Context, record modelconfiguration.Record) (modelconfiguration.Configuration, error) {
- if s.cipher == nil {
- return modelconfiguration.Configuration{}, credentialcrypto.ErrUnavailable
- }
raw, err := json.Marshal(record.Configuration)
if err != nil {
return modelconfiguration.Configuration{}, err
}
sealed, err := s.cipher.SealDeploymentModelProvider(raw, record.Harness)
if err != nil {
- return modelconfiguration.Configuration{}, credentialcrypto.ErrUnavailable
+ return modelconfiguration.Configuration{}, errors.New("model provider encryption failed")
}
var result modelconfiguration.Configuration
err = s.pool.Transaction(ctx, func(ctx context.Context, tx pgx.Tx) error {
@@ -121,9 +116,6 @@ func (s *Store) LoadBundle(ctx context.Context, harness string) (modelconfigurat
if err != nil {
return modelconfiguration.Bundle{}, translate(err)
}
- if s.cipher == nil {
- return modelconfiguration.Bundle{}, credentialcrypto.ErrUnavailable
- }
raw, err := s.cipher.OpenDeploymentModelProvider(row.EncryptedConfig, harness)
if err != nil {
return modelconfiguration.Bundle{}, errors.New("deployment model configuration decryption failed")
diff --git a/services/core/internal/persistence/postgres/pgtest/pgtest.go b/services/core/internal/persistence/postgres/pgtest/pgtest.go
index 8a9fcdcb3..da2094abe 100644
--- a/services/core/internal/persistence/postgres/pgtest/pgtest.go
+++ b/services/core/internal/persistence/postgres/pgtest/pgtest.go
@@ -3,6 +3,7 @@
package pgtest
import (
+ "bytes"
"context"
"errors"
"os"
@@ -15,9 +16,21 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"github.com/jackc/pgx/v5/stdlib"
+ "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto"
"github.com/MiniMax-AI/OpenAgentCore/services/core/migrations"
)
+// CredentialKey returns a cipher under a fixed test credential key, for tests
+// that need a key but not a particular one.
+func CredentialKey(t testing.TB) *credentialcrypto.Cipher {
+ t.Helper()
+ cipher, err := credentialcrypto.New(bytes.Repeat([]byte{0x7e}, 32))
+ if err != nil {
+ t.Fatal(err)
+ }
+ return cipher
+}
+
// Open returns a pool on the dedicated test database named by
// OAC_TEST_DATABASE_URL, with Core's migrations applied, and skips the test when
// the variable is unset. Tests on this shared database isolate their data with
diff --git a/services/core/internal/persistence/postgres/sessionpg/admin_test.go b/services/core/internal/persistence/postgres/sessionpg/admin_test.go
index 43d37f264..f6a1c6ba8 100644
--- a/services/core/internal/persistence/postgres/sessionpg/admin_test.go
+++ b/services/core/internal/persistence/postgres/sessionpg/admin_test.go
@@ -15,7 +15,7 @@ import (
func TestListAdminRuntimeTargetsPagesLiveSessionsOfTenants(t *testing.T) {
pool := pgtest.Open(t)
- store := New(pgunit.NewPool(pool), nil)
+ store := New(pgunit.NewPool(pool), pgtest.CredentialKey(t))
first, second, foreign := uuid.NewString(), uuid.NewString(), uuid.NewString()
created := time.Now().UTC().Add(-time.Hour)
session := func(tenant string, offset time.Duration, deleted bool) sessions.AdminRuntimeTarget {
@@ -64,7 +64,7 @@ func TestListAdminRuntimeTargetsPagesLiveSessionsOfTenants(t *testing.T) {
func TestReadAdminSummaryVisitsTheSelectedSessions(t *testing.T) {
pool := pgtest.Open(t)
- store := New(pgunit.NewPool(pool), nil)
+ store := New(pgunit.NewPool(pool), pgtest.CredentialKey(t))
tenant, created := uuid.NewString(), time.Now().UTC().Truncate(time.Second).Add(-time.Hour)
session := func(offset time.Duration, deleted bool) string {
id := uuid.NewString()
diff --git a/services/core/internal/persistence/postgres/sessionpg/artifacts_test.go b/services/core/internal/persistence/postgres/sessionpg/artifacts_test.go
index 30a299d63..4e17c6e55 100644
--- a/services/core/internal/persistence/postgres/sessionpg/artifacts_test.go
+++ b/services/core/internal/persistence/postgres/sessionpg/artifacts_test.go
@@ -67,7 +67,7 @@ func stagedRows(t *testing.T, pool *pgxpool.Pool, turn string) int {
func stagingService(t *testing.T, pool *pgxpool.Pool) (*Store, *sessions.Service) {
t.Helper()
- store := New(pgunit.NewPool(pool), nil)
+ store := New(pgunit.NewPool(pool), pgtest.CredentialKey(t))
service, err := sessions.NewService(store, nil)
if err != nil {
t.Fatal(err)
diff --git a/services/core/internal/persistence/postgres/sessionpg/creation.go b/services/core/internal/persistence/postgres/sessionpg/creation.go
index 5c3a93c57..be18c68d7 100644
--- a/services/core/internal/persistence/postgres/sessionpg/creation.go
+++ b/services/core/internal/persistence/postgres/sessionpg/creation.go
@@ -33,9 +33,6 @@ const modelProviderKeyPurpose = "parsar.agents-api.model-provider-api-key.v1"
var _ sessions.CreationTx = (*creationTx)(nil)
func (s *Store) FingerprintProviderKey(secret string) (string, error) {
- if s.cipher == nil {
- return "", credentialcrypto.ErrUnavailable
- }
return s.cipher.Fingerprint(modelProviderKeyPurpose, secret)
}
@@ -133,9 +130,6 @@ func skillError(err error) error {
}
func (t *creationTx) SaveModelExecution(ctx context.Context, provider v1.ModelProviderInput) error {
- if t.cipher == nil {
- return credentialcrypto.ErrUnavailable
- }
raw, err := json.Marshal(provider)
if err != nil {
return err
@@ -160,9 +154,6 @@ func (t *creationTx) SaveExecutionConfiguration(ctx context.Context, projection
}
func (t *creationTx) SaveInitialFiles(ctx context.Context, initial []environmentconfig.InitialFile) error {
- if t.cipher == nil {
- return credentialcrypto.ErrUnavailable
- }
metadata := environmentconfig.InitialFilesMetadata(initial)
for i, f := range initial {
body := f.Data
@@ -200,9 +191,6 @@ func (t *creationTx) SaveInitialFiles(ctx context.Context, initial []environment
}
func (t *creationTx) SaveSetup(ctx context.Context, setup environmentconfig.Setup) error {
- if t.cipher == nil {
- return credentialcrypto.ErrUnavailable
- }
plaintext, err := json.Marshal(setup)
if err != nil {
return err
diff --git a/services/core/internal/persistence/postgres/sessionpg/creation_test.go b/services/core/internal/persistence/postgres/sessionpg/creation_test.go
index 1552275b8..58174a25a 100644
--- a/services/core/internal/persistence/postgres/sessionpg/creation_test.go
+++ b/services/core/internal/persistence/postgres/sessionpg/creation_test.go
@@ -39,13 +39,13 @@ var hostedConfiguration = json.RawMessage(`{"agent":{"model":"m"},"environment":
// creationService returns the Session store and service over pool with the
// built-in placement rules.
-func creationService(t *testing.T, pool *pgxpool.Pool, cipher *credentialcrypto.Cipher) (*Store, *sessions.Service) {
+func creationService(t *testing.T, pool *pgxpool.Pool) (*Store, *sessions.Service) {
t.Helper()
rules, err := placement.NewRules(providers.Builtin(), "")
if err != nil {
t.Fatal(err)
}
- store := New(pgunit.NewPool(pool), cipher)
+ store := New(pgunit.NewPool(pool), pgtest.CredentialKey(t))
service, err := sessions.NewService(store, rules)
if err != nil {
t.Fatal(err)
@@ -53,9 +53,9 @@ func creationService(t *testing.T, pool *pgxpool.Pool, cipher *credentialcrypto.
return store, service
}
-func skillService(t *testing.T, pool *pgxpool.Pool, cipher *credentialcrypto.Cipher) *skills.Service {
+func skillService(t *testing.T, pool *pgxpool.Pool) *skills.Service {
t.Helper()
- store := skillpg.New(pgunit.NewPool(pool), cipher)
+ store := skillpg.New(pgunit.NewPool(pool), pgtest.CredentialKey(t))
service, err := skills.NewService(store, store)
if err != nil {
t.Fatal(err)
@@ -133,8 +133,8 @@ func awaitWaiters(t *testing.T, pool *pgxpool.Pool, holder int32, count int) {
// submitting the input again.
func TestCreationRetriesNeverReplayInitialInput(t *testing.T) {
pool := pgtest.Open(t)
- store, service := creationService(t, pool, nil)
- _, other := creationService(t, pgtest.Open(t), nil)
+ store, service := creationService(t, pool)
+ _, other := creationService(t, pgtest.Open(t))
ctx := t.Context()
tenant := uuid.NewString()
input := sessions.CreateSession{Creator: creator, Engine: "codex", IdempotencyKey: "initial", CreationRequest: json.RawMessage(`{"agent_id":"source"}`), InitialInputs: []sessions.Input{messageInput("first"), messageInput("second")}}
@@ -232,7 +232,7 @@ func TestCreationRetriesNeverReplayInitialInput(t *testing.T) {
// one creates.
func TestCreationIdentity(t *testing.T) {
pool := pgtest.Open(t)
- _, service := creationService(t, pool, nil)
+ _, service := creationService(t, pool)
ctx := t.Context()
tenant := uuid.NewString()
request := json.RawMessage(`{"agent_id":"source","agent":{"tools":[{"parameters":{"const":9007199254740993}}]}}`)
@@ -326,7 +326,7 @@ func TestCreationIdentity(t *testing.T) {
}
// The provider-key fingerprint in retry identities depends on the credential
-// key and needs one.
+// key.
func TestProviderKeyFingerprintIsKeyed(t *testing.T) {
fingerprint := func(seed byte) string {
cipher, err := credentialcrypto.New(bytes.Repeat([]byte{seed}, 32))
@@ -342,9 +342,6 @@ func TestProviderKeyFingerprintIsKeyed(t *testing.T) {
if fingerprint(71) == fingerprint(72) {
t.Fatal("fingerprint ignores the credential key")
}
- if _, err := New(nil, nil).FingerprintProviderKey("provider-key"); !errors.Is(err, credentialcrypto.ErrUnavailable) {
- t.Fatal(err)
- }
}
// A new Session freezes its provider, Skills and initial files sealed under
@@ -352,9 +349,8 @@ func TestProviderKeyFingerprintIsKeyed(t *testing.T) {
// bytes conflict, and a foreign source is missing.
func TestCreationFreezesResourcesOnce(t *testing.T) {
pool := pgtest.Open(t)
- cipher := frozenCipher(t)
- store, service := creationService(t, pool, cipher)
- skillsService := skillService(t, pool, cipher)
+ store, service := creationService(t, pool)
+ skillsService := skillService(t, pool)
filesService, err := files.NewService(filepg.New(pgunit.NewPool(pool)))
if err != nil {
t.Fatal(err)
@@ -444,21 +440,13 @@ func TestCreationFreezesResourcesOnce(t *testing.T) {
if _, err := service.CreateSession(ctx, tenant, changed); !errors.Is(err, sessions.ErrIdempotencyConflict) {
t.Fatal("changed bytes accepted", err)
}
- _, keyless := creationService(t, pool, nil)
- sealedInput := sessions.CreateSession{Creator: creator, Engine: "codex", IdempotencyKey: "keyless", Configuration: hostedConfiguration, InitialFiles: changed.InitialFiles[:1]}
- if _, err := keyless.CreateSession(ctx, tenant, sealedInput); !errors.Is(err, credentialcrypto.ErrUnavailable) {
- t.Fatal("sealed without a credential key", err)
- }
- if countRows(t, pool, "SELECT count(*) FROM sessions WHERE tenant_id=$1 AND idempotency_key='keyless'", tenant) != 0 {
- t.Fatal("a failed creation left a Session")
- }
}
// An Environment Session reserves its initial input, tracking its activity,
// instead of admitting a Turn.
func TestEnvironmentCreationReservesItsInitialInput(t *testing.T) {
pool := pgtest.Open(t)
- _, service := creationService(t, pool, nil)
+ _, service := creationService(t, pool)
input := sessions.CreateSession{Creator: creator, Engine: "codex", IdempotencyKey: "environment", InitialInputs: []sessions.Input{messageInput("first")},
Configuration: json.RawMessage(`{"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`)}
created, err := service.CreateSession(t.Context(), uuid.NewString(), input)
@@ -479,7 +467,7 @@ func TestEnvironmentCreationReservesItsInitialInput(t *testing.T) {
// nothing and still returns its Session.
func TestHostedCreationAdmitsAndPlacesUnderTheDeploymentLock(t *testing.T) {
pool := pgtest.OpenIsolated(t, nil)
- _, service := creationService(t, pool, nil)
+ _, service := creationService(t, pool)
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
defer cancel()
tenant := uuid.NewString()
@@ -531,9 +519,8 @@ func TestHostedCreationAdmitsAndPlacesUnderTheDeploymentLock(t *testing.T) {
// whichever goes first decides, and no Session refers to a missing version.
func TestSkillFreezeSerializesWithVersionDeletion(t *testing.T) {
pool := pgtest.Open(t)
- cipher := frozenCipher(t)
- store, service := creationService(t, pool, cipher)
- skillsService := skillService(t, pool, cipher)
+ store, service := creationService(t, pool)
+ skillsService := skillService(t, pool)
ctx := t.Context()
tenant := uuid.NewString()
for _, freezeFirst := range []bool{true, false} {
@@ -605,7 +592,7 @@ func TestSkillFreezeSerializesWithVersionDeletion(t *testing.T) {
// creation closed and rolls it back.
func TestCreationAudit(t *testing.T) {
pool := pgtest.Open(t)
- _, service := creationService(t, pool, nil)
+ _, service := creationService(t, pool)
tenant := uuid.NewString()
source := writeaudit.Source{KeyID: uuid.NewString(), Prefix: "pc_aaaaaaaa", Name: "test key", Kind: "issued", TenantID: uuid.NewString(), RequestID: uuid.NewString(), TraceID: uuid.NewString()}
input := sessions.CreateSession{Creator: creator, Engine: "codex", IdempotencyKey: "audited", InitialInputs: []sessions.Input{messageInput("first")},
diff --git a/services/core/internal/persistence/postgres/sessionpg/execution_turns_test.go b/services/core/internal/persistence/postgres/sessionpg/execution_turns_test.go
index 6e06a2780..5b2270441 100644
--- a/services/core/internal/persistence/postgres/sessionpg/execution_turns_test.go
+++ b/services/core/internal/persistence/postgres/sessionpg/execution_turns_test.go
@@ -72,7 +72,7 @@ func TestConcurrentTerminalTransitionsKeepOneOutcome(t *testing.T) {
if _, err := transition(t.Context(), pool, tenant, session, turn, sessions.TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnFailed, Outcome: json.RawMessage(`{"late":true}`)}); !errors.Is(err, sessions.ErrTurnConflict) {
t.Fatalf("late terminal callback accepted: %v", err)
}
- got, err := New(pgunit.NewPool(pgtest.Open(t)), nil).GetTurn(t.Context(), text(tenant), text(session), turn)
+ got, err := New(pgunit.NewPool(pgtest.Open(t)), pgtest.CredentialKey(t)).GetTurn(t.Context(), text(tenant), text(session), turn)
if err != nil || !reflect.DeepEqual(got, winner) {
t.Fatalf("terminal outcome changed: %+v, %v", got, err)
}
@@ -154,7 +154,7 @@ func TestCompleteExecutionSettlesTheTurnOnTheLease(t *testing.T) {
// and a cursor or Session outside the tenant's Session is missing.
func TestTurnPagesKeepScopeAndOrder(t *testing.T) {
pool := pgtest.Open(t)
- store := New(pgunit.NewPool(pool), nil)
+ store := New(pgunit.NewPool(pool), pgtest.CredentialKey(t))
ctx := t.Context()
tenantID, sessionID, first := newTurn(t, pool, sessions.TurnCancelled)
tenant, session := text(tenantID), text(sessionID)
diff --git a/services/core/internal/persistence/postgres/sessionpg/executor_credentials.go b/services/core/internal/persistence/postgres/sessionpg/executor_credentials.go
index b47335ecd..6d77d7a8b 100644
--- a/services/core/internal/persistence/postgres/sessionpg/executor_credentials.go
+++ b/services/core/internal/persistence/postgres/sessionpg/executor_credentials.go
@@ -12,7 +12,6 @@ import (
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
- "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/auditpg"
@@ -176,11 +175,8 @@ func (s *Store) VerifyInstallation(_ context.Context, payload, signature string)
}
// installationSignature is the keyed digest of an installation authorization
-// payload. A service without the credential key cannot sign or verify one.
+// payload.
func (s *Store) installationSignature(payload string) (string, error) {
- if s.cipher == nil {
- return "", credentialcrypto.ErrUnavailable
- }
return s.cipher.Fingerprint(installationPurpose, payload)
}
diff --git a/services/core/internal/persistence/postgres/sessionpg/executor_credentials_test.go b/services/core/internal/persistence/postgres/sessionpg/executor_credentials_test.go
index fc5f3ade4..ec8d0c810 100644
--- a/services/core/internal/persistence/postgres/sessionpg/executor_credentials_test.go
+++ b/services/core/internal/persistence/postgres/sessionpg/executor_credentials_test.go
@@ -9,7 +9,6 @@ import (
"github.com/google/uuid"
- "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit"
@@ -17,7 +16,7 @@ import (
)
func TestInstallationSignaturesUseTheCredentialKey(t *testing.T) {
- keyed := New(nil, frozenCipher(t))
+ keyed := New(nil, pgtest.CredentialKey(t))
signature, err := keyed.SignInstallation(t.Context(), "payload")
if err != nil {
t.Fatal(err)
@@ -28,18 +27,11 @@ func TestInstallationSignaturesUseTheCredentialKey(t *testing.T) {
if err := keyed.VerifyInstallation(t.Context(), "other", signature); !errors.Is(err, sessions.ErrInstallationAuthorization) {
t.Fatalf("another payload: %v", err)
}
- keyless := New(nil, nil)
- if _, err := keyless.SignInstallation(t.Context(), "payload"); !errors.Is(err, credentialcrypto.ErrUnavailable) {
- t.Fatalf("keyless signature: %v", err)
- }
- if err := keyless.VerifyInstallation(t.Context(), "payload", signature); !errors.Is(err, credentialcrypto.ErrUnavailable) {
- t.Fatalf("keyless verification: %v", err)
- }
}
func TestExecutorCredentialTxTranslatesOutcomes(t *testing.T) {
pool := pgtest.Open(t)
- store := New(pgunit.NewPool(pool), nil)
+ store := New(pgunit.NewPool(pool), pgtest.CredentialKey(t))
tenantID, sessionID, environmentID := newEnvironment(t, pool, "self_hosted", "pending")
tenant, environment := uuidText(tenantID), uuidText(environmentID)
exec(t, pool, `INSERT INTO execution_project_scopes(tenant_id, organization_id, project_id) VALUES ($1, 'org', $2)`, tenantID, tenant)
diff --git a/services/core/internal/persistence/postgres/sessionpg/frozen.go b/services/core/internal/persistence/postgres/sessionpg/frozen.go
index d7a638f78..ad2217270 100644
--- a/services/core/internal/persistence/postgres/sessionpg/frozen.go
+++ b/services/core/internal/persistence/postgres/sessionpg/frozen.go
@@ -34,9 +34,6 @@ func (s *Store) ReadEnvironmentSetup(ctx context.Context, tenant, session string
if len(encrypted) == 0 {
return setup, nil
}
- if s.cipher == nil {
- return environmentconfig.Setup{}, credentialcrypto.ErrUnavailable
- }
plaintext, err := s.cipher.OpenEnvironmentSetup(encrypted, setupBinding(lookup.TenantID, lookup.ID))
if err != nil {
return environmentconfig.Setup{}, fmt.Errorf("open frozen environment setup: %w", err)
@@ -62,9 +59,6 @@ func (s *Store) ReadInitialEnvironmentFile(ctx context.Context, tenant, session
if err != nil {
return environmentconfig.InitialFileMetadata{}, nil, err
}
- if s.cipher == nil {
- return environmentconfig.InitialFileMetadata{}, nil, credentialcrypto.ErrUnavailable
- }
id := uuid.UUID(row.ID.Bytes).String()
body, err := s.cipher.OpenEnvironmentFile(row.Contents, fileBinding(lookup.TenantID, lookup.ID, id))
if err != nil {
diff --git a/services/core/internal/persistence/postgres/sessionpg/frozen_test.go b/services/core/internal/persistence/postgres/sessionpg/frozen_test.go
index da87dc7c9..99cdb5d04 100644
--- a/services/core/internal/persistence/postgres/sessionpg/frozen_test.go
+++ b/services/core/internal/persistence/postgres/sessionpg/frozen_test.go
@@ -1,7 +1,6 @@
package sessionpg
import (
- "bytes"
"errors"
"reflect"
"testing"
@@ -15,25 +14,16 @@ import (
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions"
)
-func frozenCipher(t *testing.T) *credentialcrypto.Cipher {
- t.Helper()
- cipher, err := credentialcrypto.New(bytes.Repeat([]byte{5}, 32))
- if err != nil {
- t.Fatal(err)
- }
- return cipher
-}
-
// corruptFrozenData reports whether err classifies frozen data as corrupt
-// stored data: an internal error, never the caller's input, a missing record
-// or a missing key.
+// stored data: an internal error, never the caller's input or a missing
+// record.
func corruptFrozenData(err error) bool {
- return err != nil && !errors.Is(err, sessions.ErrInvalidInput) && !errors.Is(err, sessions.ErrNotFound) && !errors.Is(err, credentialcrypto.ErrUnavailable)
+ return err != nil && !errors.Is(err, sessions.ErrInvalidInput) && !errors.Is(err, sessions.ErrNotFound)
}
func TestReadEnvironmentSetupClassifiesFrozenData(t *testing.T) {
pool := pgtest.Open(t)
- cipher := frozenCipher(t)
+ cipher := pgtest.CredentialKey(t)
adapter := New(pgunit.NewPool(pool), cipher)
tenantID, sessionID, _ := newEnvironment(t, pool, "self_hosted", "pending")
tenant, session := uuidText(tenantID), uuidText(sessionID)
@@ -61,9 +51,6 @@ func TestReadEnvironmentSetupClassifiesFrozenData(t *testing.T) {
if err != nil || setup.Env["GREETING"] != "hello" || !reflect.DeepEqual(setup.Packages.NPM, []string{"left-pad"}) {
t.Fatalf("frozen setup %+v, %v", setup, err)
}
- if _, err := New(pgunit.NewPool(pool), nil).ReadEnvironmentSetup(t.Context(), tenant, session); !errors.Is(err, credentialcrypto.ErrUnavailable) {
- t.Fatalf("without a credential key: %v", err)
- }
otherSession := binding
otherSession.OwnerID = uuid.NewString()
@@ -91,7 +78,7 @@ func TestReadEnvironmentSetupClassifiesFrozenData(t *testing.T) {
func TestReadInitialEnvironmentFileClassifiesFrozenData(t *testing.T) {
pool := pgtest.Open(t)
- cipher := frozenCipher(t)
+ cipher := pgtest.CredentialKey(t)
adapter := New(pgunit.NewPool(pool), cipher)
tenantID, sessionID, _ := newEnvironment(t, pool, "self_hosted", "pending")
tenant, session := uuidText(tenantID), uuidText(sessionID)
@@ -117,9 +104,6 @@ func TestReadInitialEnvironmentFileClassifiesFrozenData(t *testing.T) {
if _, _, err := adapter.ReadInitialEnvironmentFile(t.Context(), uuid.NewString(), session, 0); !errors.Is(err, sessions.ErrNotFound) {
t.Fatalf("other tenant: %v", err)
}
- if _, _, err := New(pgunit.NewPool(pool), nil).ReadInitialEnvironmentFile(t.Context(), tenant, session, 0); !errors.Is(err, credentialcrypto.ErrUnavailable) {
- t.Fatalf("without a credential key: %v", err)
- }
otherFile := binding
otherFile.FileID = uuid.NewString()
diff --git a/services/core/internal/persistence/postgres/sessionpg/model_execution.go b/services/core/internal/persistence/postgres/sessionpg/model_execution.go
index 8f5cfbfbb..68e91a3a7 100644
--- a/services/core/internal/persistence/postgres/sessionpg/model_execution.go
+++ b/services/core/internal/persistence/postgres/sessionpg/model_execution.go
@@ -11,7 +11,6 @@ import (
"github.com/jackc/pgx/v5"
v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1"
- "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions"
)
@@ -34,9 +33,6 @@ func (s *Store) SessionModelExecution(ctx context.Context, tenant, session strin
if err != nil {
return nil, err
}
- if s.cipher == nil {
- return nil, credentialcrypto.ErrUnavailable
- }
raw, err := s.cipher.OpenModelExecution(ciphertext, tenant, session)
if err != nil {
return nil, fmt.Errorf("open session model execution: %w", err)
diff --git a/services/core/internal/persistence/postgres/skillpg/skillpg.go b/services/core/internal/persistence/postgres/skillpg/skillpg.go
index 8453bbb55..b65dfd8eb 100644
--- a/services/core/internal/persistence/postgres/skillpg/skillpg.go
+++ b/services/core/internal/persistence/postgres/skillpg/skillpg.go
@@ -33,8 +33,7 @@ var (
_ skills.Reader = (*Store)(nil)
)
-// New builds the Skill store. Without a cipher, uploads and content reads
-// fail with credentialcrypto.ErrUnavailable and metadata reads still work.
+// New builds the Skill store, which seals archives with cipher.
func New(pool *pgunit.Pool, cipher *credentialcrypto.Cipher) *Store {
return &Store{pool: pool, cipher: cipher}
}
@@ -301,9 +300,6 @@ func (s *Store) DefaultVersionContent(ctx context.Context, tenantID string, skil
// insertVersion seals the archive under a new version ID and stores it.
func (s *Store) insertVersion(ctx context.Context, q *sqlc.Queries, tenant, skill pgtype.UUID, version int64, name, description string, archive []byte) (skills.Version, error) {
- if s.cipher == nil {
- return skills.Version{}, credentialcrypto.ErrUnavailable
- }
id := newID()
body, err := s.cipher.SealSkill(archive, credentialcrypto.NewSkillBinding(tenant.Bytes, skill.Bytes, id.Bytes, version))
if err != nil {
@@ -338,8 +334,7 @@ func LockSkills(ctx context.Context, q *sqlc.Queries, tenant pgtype.UUID, ids []
// ReadVersionForFreeze reads, on q, a version of the tenant's Skill, opens
// it with cipher and verifies it is still the archive the version records. A
-// missing version is skills.ErrNotFound and a missing cipher
-// credentialcrypto.ErrUnavailable; one that does not open or verify is
+// missing version is skills.ErrNotFound; one that does not open or verify is
// corrupt stored data, an internal error.
func ReadVersionForFreeze(ctx context.Context, q *sqlc.Queries, cipher *credentialcrypto.Cipher, tenant pgtype.UUID, skillID string, version int64) (skills.Content, error) {
key, err := skills.ParseID(skillID)
@@ -361,9 +356,6 @@ func ReadVersionForFreeze(ctx context.Context, q *sqlc.Queries, cipher *credenti
}
func open(cipher *credentialcrypto.Cipher, row sqlc.SkillVersion) (skills.Content, error) {
- if cipher == nil {
- return skills.Content{}, credentialcrypto.ErrUnavailable
- }
archive, err := cipher.OpenSkill(row.Contents, credentialcrypto.NewSkillBinding(row.TenantID.Bytes, row.SkillID.Bytes, row.ID.Bytes, row.Version))
if err != nil {
return skills.Content{}, err
diff --git a/services/core/internal/persistence/postgres/skillpg/skillpg_test.go b/services/core/internal/persistence/postgres/skillpg/skillpg_test.go
index f4fee68ef..8824d4534 100644
--- a/services/core/internal/persistence/postgres/skillpg/skillpg_test.go
+++ b/services/core/internal/persistence/postgres/skillpg/skillpg_test.go
@@ -106,9 +106,9 @@ func TestOwnershipEncryptionAndVersions(t *testing.T) {
if created.DefaultVersion != 1 || created.LatestVersion != 1 {
t.Fatal("initial pointers", created)
}
- metadata, err := skillpg.New(pgunit.NewPool(f.pool), nil).Skill(ctx, tenant, id)
+ metadata, err := skillpg.New(pgunit.NewPool(f.pool), pgtest.CredentialKey(t)).Skill(ctx, tenant, id)
if err != nil || metadata.Name != "proof" {
- t.Fatal("metadata requires no content key", err)
+ t.Fatal("metadata required the content key", err)
}
var contents []byte
if err = f.pool.QueryRow(ctx, "SELECT contents FROM skill_versions WHERE tenant_id=$1", tenant).Scan(&contents); err != nil {
@@ -288,8 +288,8 @@ func TestListsAcceptLimitZero(t *testing.T) {
func TestMetadataTracksDefaultVersion(t *testing.T) {
pool := pgtest.Open(t)
f := newFixture(t, pool, testCipher(t, 74))
- // Metadata reads and default changes need no content key.
- metadataOnly := newFixture(t, pool, nil)
+ // Metadata reads and default changes work under a replaced key.
+ replaced := newFixture(t, pool, pgtest.CredentialKey(t))
ctx := t.Context()
tenant, foreign := uuid.NewString(), uuid.NewString()
names := []string{"first-proof", "second-proof", "third-proof"}
@@ -308,12 +308,12 @@ func TestMetadataTracksDefaultVersion(t *testing.T) {
}
assertStored := func(version, latest int64) {
t.Helper()
- value, err := metadataOnly.store.Skill(ctx, tenant, id)
+ value, err := replaced.store.Skill(ctx, tenant, id)
if err != nil {
t.Fatal("metadata read without content key", err)
}
assertMetadata(value, version, latest)
- page, err := metadataOnly.service.ListSkills(ctx, skills.ListSkills{TenantID: tenant, Limit: 10, Ascending: true})
+ page, err := replaced.service.ListSkills(ctx, skills.ListSkills{TenantID: tenant, Limit: 10, Ascending: true})
if err != nil || len(page.Skills) != 1 {
t.Fatal("metadata list without content key", page, err)
}
@@ -329,28 +329,23 @@ func TestMetadataTracksDefaultVersion(t *testing.T) {
}
assertStored(1, 2)
for _, version := range []string{"2", "1"} {
- updated, err := metadataOnly.service.SetDefaultVersion(ctx, skills.SetDefaultVersion{TenantID: tenant, SkillID: id, Version: version})
+ updated, err := replaced.service.SetDefaultVersion(ctx, skills.SetDefaultVersion{TenantID: tenant, SkillID: id, Version: version})
if err != nil {
t.Fatal("default update without content key", err)
}
assertMetadata(updated, updated.DefaultVersion, 2)
assertStored(updated.DefaultVersion, 2)
}
- if _, err := metadataOnly.service.SetDefaultVersion(ctx, skills.SetDefaultVersion{TenantID: foreign, SkillID: id, Version: "2"}); !errors.Is(err, skills.ErrNotFound) {
+ if _, err := replaced.service.SetDefaultVersion(ctx, skills.SetDefaultVersion{TenantID: foreign, SkillID: id, Version: "2"}); !errors.Is(err, skills.ErrNotFound) {
t.Fatal("foreign default update", err)
}
- if _, err := metadataOnly.service.SetDefaultVersion(ctx, skills.SetDefaultVersion{TenantID: tenant, SkillID: id, Version: "999"}); !errors.Is(err, skills.ErrNotFound) {
+ if _, err := replaced.service.SetDefaultVersion(ctx, skills.SetDefaultVersion{TenantID: tenant, SkillID: id, Version: "999"}); !errors.Is(err, skills.ErrNotFound) {
t.Fatal("missing default update", err)
}
assertStored(1, 2)
- // Uploads and content reads need the key; the failed upload stores nothing.
- if _, err := metadataOnly.service.CreateVersion(ctx, skills.CreateVersion{TenantID: tenant, SkillID: id, Archive: archives[2]}); !errors.Is(err, credentialcrypto.ErrUnavailable) {
- t.Fatal("upload without content key", err)
+ if _, err := replaced.service.ReadDefaultVersion(ctx, skills.ReadDefaultVersion{TenantID: tenant, SkillID: id}); err == nil || errors.Is(err, skills.ErrNotFound) {
+ t.Fatal("a replaced key opened content", err)
}
- if _, err := metadataOnly.service.ReadDefaultVersion(ctx, skills.ReadDefaultVersion{TenantID: tenant, SkillID: id}); !errors.Is(err, credentialcrypto.ErrUnavailable) {
- t.Fatal("content read without content key", err)
- }
- assertStored(1, 2)
if _, err := f.service.CreateVersion(ctx, skills.CreateVersion{TenantID: tenant, SkillID: id, Archive: archives[2], MakeDefault: true}); err != nil {
t.Fatal(err)
}
@@ -502,10 +497,7 @@ func TestFreezeReads(t *testing.T) {
if _, err := skillpg.ReadVersionForFreeze(ctx, q, cipher, tenantID, first.ID, 2); !errors.Is(err, skills.ErrNotFound) {
t.Fatal("missing version", err)
}
- if _, err := skillpg.ReadVersionForFreeze(ctx, q, nil, tenantID, first.ID, 1); !errors.Is(err, credentialcrypto.ErrUnavailable) {
- t.Fatal("read without a key", err)
- }
- if _, err := skillpg.ReadVersionForFreeze(ctx, q, testCipher(t, 49), tenantID, first.ID, 1); err == nil || errors.Is(err, skills.ErrNotFound) || errors.Is(err, credentialcrypto.ErrUnavailable) {
+ if _, err := skillpg.ReadVersionForFreeze(ctx, q, testCipher(t, 49), tenantID, first.ID, 1); err == nil || errors.Is(err, skills.ErrNotFound) {
t.Fatal("another key opened the version", err)
}
}
diff --git a/services/core/internal/persistence/postgres/templatepg/audit_test.go b/services/core/internal/persistence/postgres/templatepg/audit_test.go
index b4611b238..2dc1e8de4 100644
--- a/services/core/internal/persistence/postgres/templatepg/audit_test.go
+++ b/services/core/internal/persistence/postgres/templatepg/audit_test.go
@@ -114,7 +114,7 @@ func TestInvalidWriteAuditSourcePassesThrough(t *testing.T) {
if !errors.Is(err, writeaudit.ErrInvalidSource) {
t.Fatal("mismatched source tenant", err)
}
- if page, err := f.keyless.List(t.Context(), tenant, environmenttemplates.ListQuery{Limit: 1}); err != nil || len(page.Templates) != 0 {
+ if page, err := f.replaced.List(t.Context(), tenant, environmenttemplates.ListQuery{Limit: 1}); err != nil || len(page.Templates) != 0 {
t.Fatal("rejected source left a Template", err)
}
}
@@ -168,7 +168,7 @@ func TestAdminDeleteAuditCommitsWithTheDeletion(t *testing.T) {
if err := f.pool.QueryRow(t.Context(), `SELECT (SELECT count(*) FROM write_audit_operations WHERE tenant_id=$1 AND action='delete'),(SELECT count(*) FROM write_audit_owners WHERE tenant_id=$1 AND resource_type='environment_template' AND resource_id=$2)`, tenant, id).Scan(&operations, &owners); err != nil || operations != 0 || owners != 0 {
t.Fatal("administrator impersonated public-key provenance", err)
}
- if _, err := f.keyless.Get(t.Context(), tenant, id); !errors.Is(err, environmenttemplates.ErrNotFound) {
+ if _, err := f.replaced.Get(t.Context(), tenant, id); !errors.Is(err, environmenttemplates.ErrNotFound) {
t.Fatal("deleted Template is visible", err)
}
}
diff --git a/services/core/internal/persistence/postgres/templatepg/seal.go b/services/core/internal/persistence/postgres/templatepg/seal.go
index 06b2f0749..c9e45ac9e 100644
--- a/services/core/internal/persistence/postgres/templatepg/seal.go
+++ b/services/core/internal/persistence/postgres/templatepg/seal.go
@@ -12,8 +12,7 @@ import (
)
// sealed is an Input's column values. Empty confidential fields are stored as
-// NULL without using the key, so Templates without them need none; any other
-// confidential field without a key fails with credentialcrypto.ErrUnavailable.
+// NULL without using the key.
type sealed struct {
files, fileContents []byte
packages, env, commands []byte
@@ -28,9 +27,6 @@ func (s *Store) seal(tenant, id pgtype.UUID, in environmenttemplates.Input) (sea
return out, err
}
if len(in.Files) > 0 {
- if s.cipher == nil {
- return out, credentialcrypto.ErrUnavailable
- }
plaintext, err := json.Marshal(in.Files)
if err != nil {
return out, err
@@ -62,9 +58,6 @@ func (s *Store) seal(tenant, id pgtype.UUID, in environmenttemplates.Input) (sea
if field.empty {
continue
}
- if s.cipher == nil {
- return out, credentialcrypto.ErrUnavailable
- }
plaintext, err := json.Marshal(field.value)
if err != nil {
return out, err
@@ -80,9 +73,6 @@ func (s *Store) openSetup(tenant, id pgtype.UUID, field string, ciphertext []byt
if len(ciphertext) == 0 {
return nil
}
- if s.cipher == nil {
- return credentialcrypto.ErrUnavailable
- }
plaintext, err := s.cipher.OpenEnvironmentSetup(ciphertext, setupBinding(tenant, id, field))
if err != nil {
return err
diff --git a/services/core/internal/persistence/postgres/templatepg/store.go b/services/core/internal/persistence/postgres/templatepg/store.go
index cb3d1d502..b4afbb0dc 100644
--- a/services/core/internal/persistence/postgres/templatepg/store.go
+++ b/services/core/internal/persistence/postgres/templatepg/store.go
@@ -36,9 +36,8 @@ var (
_ environmenttemplates.Reader = (*Store)(nil)
)
-// New returns a Store. Without a credential key (cipher nil), writes and
-// resolutions that seal or open confidential configuration fail with
-// credentialcrypto.ErrUnavailable; safe metadata stays readable.
+// New returns a Store that seals and opens confidential configuration with
+// cipher.
func New(pool *pgunit.Pool, cipher *credentialcrypto.Cipher) *Store {
return &Store{pool: pool, cipher: cipher}
}
@@ -217,9 +216,6 @@ func (s *Store) Resolve(ctx context.Context, tenantID, templateID string) (envir
}
return resolved, nil
}
- if s.cipher == nil {
- return environmenttemplates.Resolved{}, credentialcrypto.ErrUnavailable
- }
plaintext, err := s.cipher.OpenEnvironmentFile(row.FileContents, fileBinding(tenant, id))
if err != nil {
return environmenttemplates.Resolved{}, err
diff --git a/services/core/internal/persistence/postgres/templatepg/store_test.go b/services/core/internal/persistence/postgres/templatepg/store_test.go
index 7710eb447..589462c3f 100644
--- a/services/core/internal/persistence/postgres/templatepg/store_test.go
+++ b/services/core/internal/persistence/postgres/templatepg/store_test.go
@@ -22,13 +22,13 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
)
-// fixture is a Template adapter with the credential key and one without it,
-// on the same database, plus the service that validates writes.
+// fixture is a Template adapter under the credential key and one under a
+// replaced key, on the same database, plus the service that validates writes.
type fixture struct {
- pool *pgxpool.Pool
- keyed *templatepg.Store
- keyless *templatepg.Store
- service *environmenttemplates.Service
+ pool *pgxpool.Pool
+ keyed *templatepg.Store
+ replaced *templatepg.Store
+ service *environmenttemplates.Service
}
func newFixture(t *testing.T, pool *pgxpool.Pool) fixture {
@@ -42,7 +42,7 @@ func newFixture(t *testing.T, pool *pgxpool.Pool) fixture {
if err != nil {
t.Fatal(err)
}
- return fixture{pool: pool, keyed: keyed, keyless: templatepg.New(pgunit.NewPool(pool), nil), service: service}
+ return fixture{pool: pool, keyed: keyed, replaced: templatepg.New(pgunit.NewPool(pool), pgtest.CredentialKey(t)), service: service}
}
func (f fixture) create(t *testing.T, tenant string, in environmenttemplates.Input) environmenttemplates.Template {
@@ -172,12 +172,12 @@ func TestEmptyUpdateTouchesTimeWithoutKeyOrContents(t *testing.T) {
return contents
}
before := row()
- if _, err := f.keyless.Update(t.Context(), uuid.NewString(), original.ID, environmenttemplates.Input{}); !errors.Is(err, environmenttemplates.ErrNotFound) {
+ if _, err := f.replaced.Update(t.Context(), uuid.NewString(), original.ID, environmenttemplates.Input{}); !errors.Is(err, environmenttemplates.ErrNotFound) {
t.Fatal("foreign empty update was admitted", err)
}
- updated, err := f.keyless.Update(t.Context(), tenant, original.ID, environmenttemplates.Input{})
+ updated, err := f.replaced.Update(t.Context(), tenant, original.ID, environmenttemplates.Input{})
if err != nil || !updated.UpdatedAt.After(original.UpdatedAt) {
- t.Fatal("empty update did not advance the timestamp without a key", err)
+ t.Fatal("empty update did not advance the timestamp under a replaced key", err)
}
original.UpdatedAt = updated.UpdatedAt
if !reflect.DeepEqual(updated, original) || !bytes.Equal(before, row()) {
@@ -201,9 +201,9 @@ func TestNetworkPolicyRoundTripAndReplacement(t *testing.T) {
}
created := f.create(t, tenant, environmenttemplates.Input{SetNetwork: true, NetworkAccess: "restricted", AllowedDomains: domains})
check(created, nil)
- check(f.keyless.Get(ctx, tenant, created.ID))
+ check(f.replaced.Get(ctx, tenant, created.ID))
check(f.resolve(t, tenant, created.ID).Template, nil)
- page, err := f.keyless.List(ctx, tenant, environmenttemplates.ListQuery{Limit: 1, Ascending: true})
+ page, err := f.replaced.List(ctx, tenant, environmenttemplates.ListQuery{Limit: 1, Ascending: true})
if err != nil || len(page.Templates) != 1 {
t.Fatal(page, err)
}
@@ -220,7 +220,7 @@ func TestNetworkPolicyRoundTripAndReplacement(t *testing.T) {
if _, err := f.service.Update(ctx, environmenttemplates.UpdateCommand{TenantID: tenant, TemplateID: created.ID, Input: in}); !errors.Is(err, environmenttemplates.ErrInvalidInput) {
t.Fatal("invalid policy replacement", err)
}
- check(f.keyless.Get(ctx, tenant, created.ID))
+ check(f.replaced.Get(ctx, tenant, created.ID))
}
domains = []string{"other.example.com"}
check(f.update(t, tenant, created.ID, environmenttemplates.Input{SetNetwork: true, NetworkAccess: "restricted", AllowedDomains: domains}), nil)
@@ -237,7 +237,7 @@ func TestSetupSealedAndReplacedPerField(t *testing.T) {
tenant, foreign := uuid.NewString(), uuid.NewString()
setup := environmentconfig.Setup{Env: map[string]string{"SECRET": "template-env-canary"}, Commands: []environmentconfig.SetupCommand{{Command: "printf template-command-canary > result"}}, Packages: v1.EnvironmentPackages{NPM: []string{"is-number@7.0.0"}}}
template := f.create(t, tenant, environmenttemplates.Input{Setup: setup, SetEnv: true, SetCommands: true, SetPackages: true})
- public, err := f.keyless.Get(t.Context(), tenant, template.ID)
+ public, err := f.replaced.Get(t.Context(), tenant, template.ID)
if err != nil || !reflect.DeepEqual(public.Packages.NPM, setup.Packages.NPM) {
t.Fatal("public metadata requires plaintext or key", err)
}
@@ -266,7 +266,7 @@ func TestInitialFilesSealedWithNoncanonicalIDs(t *testing.T) {
canary := []byte("private-initial-file-canary\x00\xff")
files := []environmentconfig.InitialFile{{Type: "inline", Path: "/workspace/a/data", Data: canary}, {Type: "file_id", Path: "/workspace/b", FileID: "file-" + uuid.NewString()}}
template := f.create(t, tenant, environmenttemplates.Input{SetFiles: true, Files: files})
- public, err := f.keyless.Get(t.Context(), tenant, template.ID)
+ public, err := f.replaced.Get(t.Context(), tenant, template.ID)
if err != nil || len(public.Files) != 2 || *public.Files[0].SizeBytes != int64(len(canary)) {
t.Fatal("public read depends on the key", err)
}
@@ -294,11 +294,11 @@ func TestSkillsAndPluginsSealedAndPreserved(t *testing.T) {
}
tenant, foreign := uuid.NewString(), uuid.NewString()
template := f.create(t, tenant, environmenttemplates.Input{SetSkills: true, SetPlugins: true, SetDirectories: true, Setup: setup})
- public, err := f.keyless.Get(ctx, tenant, template.ID)
+ public, err := f.replaced.Get(ctx, tenant, template.ID)
if err != nil || len(public.Skills) != 1 || len(public.Plugins) != 1 || !reflect.DeepEqual(public.CapabilityDirectories, setup.CapabilityDirectories) {
t.Fatal("safe metadata without the key", err)
}
- if page, err := f.keyless.List(ctx, tenant, environmenttemplates.ListQuery{Limit: 20}); err != nil || len(page.Templates) != 1 || len(page.Templates[0].Plugins) != 1 {
+ if page, err := f.replaced.List(ctx, tenant, environmenttemplates.ListQuery{Limit: 20}); err != nil || len(page.Templates) != 1 || len(page.Templates[0].Plugins) != 1 {
t.Fatal("list", err)
}
f.requireSealed(t, template.ID, "skill_contents", "skill-private-canary")
@@ -354,18 +354,15 @@ func TestUnstorableTextIsRejected(t *testing.T) {
}
}
-func TestMissingKeyIsUnavailable(t *testing.T) {
+func TestReplacedKeyCannotResolve(t *testing.T) {
f := newFixture(t, pgtest.Open(t))
tenant := uuid.NewString()
setup := environmenttemplates.Input{Setup: environmentconfig.Setup{Env: map[string]string{"PRIVATE_SETUP": "env-canary"}}}
files := environmenttemplates.Input{SetFiles: true, Files: []environmentconfig.InitialFile{{Type: "inline", Path: "/workspace/input.txt", Data: []byte("file-canary")}}}
for _, in := range []environmenttemplates.Input{setup, files} {
- if _, err := f.keyless.Create(t.Context(), tenant, in); !errors.Is(err, credentialcrypto.ErrUnavailable) {
- t.Fatal("confidential create without a key", err)
- }
template := f.create(t, tenant, in)
- if _, err := f.keyless.Resolve(t.Context(), tenant, template.ID); !errors.Is(err, credentialcrypto.ErrUnavailable) {
- t.Fatal("confidential resolve without a key", err)
+ if _, err := f.replaced.Resolve(t.Context(), tenant, template.ID); err == nil || errors.Is(err, environmenttemplates.ErrNotFound) || strings.Contains(err.Error(), "canary") {
+ t.Fatal("confidential resolve under a replaced key", err)
}
}
}
@@ -387,10 +384,10 @@ func TestStoredPackagesRejected(t *testing.T) {
internal := func(err error) bool { return err != nil && !errors.Is(err, environmenttemplates.ErrInvalidInput) }
for _, value := range []string{`null`, `[]`, `["jq"]`} {
store(`{"npm":[],"python":[],"system":` + value + `}`)
- if _, err := f.keyless.Get(ctx, tenant, template.ID); !internal(err) {
+ if _, err := f.replaced.Get(ctx, tenant, template.ID); !internal(err) {
t.Fatal("get did not fail internally on removed system packages", value, err)
}
- if _, err := f.keyless.List(ctx, tenant, environmenttemplates.ListQuery{Limit: 1}); !internal(err) {
+ if _, err := f.replaced.List(ctx, tenant, environmenttemplates.ListQuery{Limit: 1}); !internal(err) {
t.Fatal("list did not fail internally on removed system packages", value, err)
}
if _, err := f.keyed.Resolve(ctx, tenant, template.ID); !internal(err) {
@@ -402,7 +399,7 @@ func TestStoredPackagesRejected(t *testing.T) {
t.Fatal("resolve accepted packages that fail validation", err)
}
store(`{"npm":["semver"],"python":["packaging"]}`)
- if got, err := f.keyless.Get(ctx, tenant, template.ID); err != nil || !reflect.DeepEqual(got.Packages, v1.EnvironmentPackages{NPM: []string{"semver"}, Python: []string{"packaging"}}) {
+ if got, err := f.replaced.Get(ctx, tenant, template.ID); err != nil || !reflect.DeepEqual(got.Packages, v1.EnvironmentPackages{NPM: []string{"semver"}, Python: []string{"packaging"}}) {
t.Fatal("supported stored package managers rejected", got.Packages, err)
}
}
diff --git a/services/core/internal/persistence/postgres/vaultpg/credentials_test.go b/services/core/internal/persistence/postgres/vaultpg/credentials_test.go
index a299087b4..a6516b503 100644
--- a/services/core/internal/persistence/postgres/vaultpg/credentials_test.go
+++ b/services/core/internal/persistence/postgres/vaultpg/credentials_test.go
@@ -16,6 +16,7 @@ import (
"github.com/google/uuid"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto"
+ "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/vaults"
)
@@ -23,10 +24,10 @@ func TestStaticCredentialsPersistEncryptedAndRemainScoped(t *testing.T) {
store, pool := openStore(t)
ctx := t.Context()
tenant, foreignTenant := uuid.NewString(), uuid.NewString()
- keyless := newService(t, pool, nil, nil)
+ replaced := newService(t, pool, pgtest.CredentialKey(t), nil)
var owned []vaults.Vault
for _, owner := range []string{tenant, tenant, foreignTenant} {
- owned = append(owned, createVault(t, keyless, owner))
+ owned = append(owned, createVault(t, replaced, owner))
}
key, randomToken := make([]byte, 32), make([]byte, 32)
if _, err := rand.Read(key); err != nil {
@@ -60,9 +61,6 @@ func TestStaticCredentialsPersistEncryptedAndRemainScoped(t *testing.T) {
t.Fatal("separate creates reused a credential identity")
}
valid := vaults.CreateStaticCredential{TenantID: tenant, VaultID: owned[0].ID, Name: "Rejected", MCPServerURL: "https://mcp.example/tools", Token: opaque}
- if _, err := keyless.CreateStaticCredential(ctx, valid); !errors.Is(err, credentialcrypto.ErrUnavailable) {
- t.Fatal("missing encryption key did not fail writes closed")
- }
for _, target := range []struct{ tenant, vault string }{{tenant, owned[2].ID}, {foreignTenant, owned[0].ID}, {tenant, uuid.NewString()}, {tenant, "invalid"}} {
command := valid
command.TenantID, command.VaultID = target.tenant, target.vault
@@ -137,7 +135,7 @@ func TestStaticCredentialsPersistEncryptedAndRemainScoped(t *testing.T) {
}
}
-func TestCredentialListFilteringOwnershipAndKeylessReconnect(t *testing.T) {
+func TestCredentialListFilteringOwnershipAndReplacedKeyReconnect(t *testing.T) {
store, pool := openStore(t)
ctx := t.Context()
tenant, foreign := uuid.NewString(), uuid.NewString()
@@ -255,7 +253,7 @@ func TestCredentialListFilteringOwnershipAndKeylessReconnect(t *testing.T) {
pool.Close()
reopened, pool := openStore(t)
if got := read(reopened, true, nil, 20); !reflect.DeepEqual(got, all) || snapshot() != before {
- t.Fatal("keyless reads/restart changed metadata, classification or ciphertext")
+ t.Fatal("reads or a restart under a replaced key changed metadata, classification or ciphertext")
}
}
@@ -400,7 +398,7 @@ func TestCredentialDeletionScopeBindingAndRestart(t *testing.T) {
tenant, foreign := uuid.NewString(), uuid.NewString()
key := bytes.Repeat([]byte{41}, 32)
service := newService(t, pool, newCipher(t, key), nil)
- keyless := newService(t, pool, nil, nil)
+ replaced := newService(t, pool, pgtest.CredentialKey(t), nil)
vault, wrong := createVault(t, service, tenant), createVault(t, service, tenant)
original := createStatic(t, service, tenant, vault.ID, "original", "https://mcp.example/tools", "original-secret")
attached := []string{vault.ID}
@@ -417,12 +415,12 @@ func TestCredentialDeletionScopeBindingAndRestart(t *testing.T) {
{foreign, vault.ID, original.ID}, {tenant, wrong.ID, original.ID},
{tenant, vault.ID, uuid.NewString()}, {tenant, "invalid", original.ID}, {tenant, vault.ID, "invalid"},
} {
- if _, err := remove(keyless, scope.tenant, scope.vault, scope.id); !errors.Is(err, vaults.ErrNotFound) {
+ if _, err := remove(replaced, scope.tenant, scope.vault, scope.id); !errors.Is(err, vaults.ErrNotFound) {
t.Fatal("foreign or invalid delete was accepted", err)
}
}
// An actual database write failure must leave the resource and token intact.
- _, deletionErr := remove(newService(t, readOnlyPool(t, pool), nil, nil), tenant, vault.ID, original.ID)
+ _, deletionErr := remove(newService(t, readOnlyPool(t, pool), pgtest.CredentialKey(t), nil), tenant, vault.ID, original.ID)
if !isReadOnlyFailure(deletionErr) {
t.Fatal("failed mutation was accepted or translated", deletionErr)
}
@@ -432,12 +430,12 @@ func TestCredentialDeletionScopeBindingAndRestart(t *testing.T) {
if token, err := bearerToken(t.Context(), service, tenant, attached, selected[0]); err != nil || token != retained {
t.Fatal("rejected deletion changed the stored token")
}
- // Delete without a key, even if the stored payload is damaged.
+ // Delete under a replaced key, even if the stored payload is damaged.
if _, err := pool.Exec(t.Context(), "UPDATE vault_credentials SET token_ciphertext=decode('00','hex') WHERE id=$1", original.ID); err != nil {
t.Fatal(err)
}
- if id, err := remove(keyless, tenant, vault.ID, original.ID); err != nil || id != original.ID {
- t.Fatal("keyless deletion failed", err)
+ if id, err := remove(replaced, tenant, vault.ID, original.ID); err != nil || id != original.ID {
+ t.Fatal("deletion under a replaced key failed", err)
}
var count int
if err := pool.QueryRow(t.Context(), "SELECT count(*) FROM vault_credentials WHERE id=$1", original.ID).Scan(&count); err != nil || count != 0 {
diff --git a/services/core/internal/persistence/postgres/vaultpg/fixture_test.go b/services/core/internal/persistence/postgres/vaultpg/fixture_test.go
index 6d23ff73e..853529a4d 100644
--- a/services/core/internal/persistence/postgres/vaultpg/fixture_test.go
+++ b/services/core/internal/persistence/postgres/vaultpg/fixture_test.go
@@ -18,11 +18,11 @@ import (
)
// openStore opens the shared test database, as a restarted Core would. The
-// Store has no credential key, which reads never need.
+// Store has the shared test key, which reads never need.
func openStore(t *testing.T) (*vaultpg.Store, *pgxpool.Pool) {
t.Helper()
pool := pgtest.Open(t)
- return vaultpg.New(pgunit.NewPool(pool), nil), pool
+ return vaultpg.New(pgunit.NewPool(pool), pgtest.CredentialKey(t)), pool
}
// readOnlyPool fails every write with a real PostgreSQL error.
@@ -54,8 +54,7 @@ func newCipher(t *testing.T, key []byte) *credentialcrypto.Cipher {
return cipher
}
-// keyedStore is a new Store on pool; a nil cipher is a Core without a
-// credential key.
+// keyedStore is a new Store on pool under cipher.
func keyedStore(pool *pgxpool.Pool, cipher *credentialcrypto.Cipher) *vaultpg.Store {
return vaultpg.New(pgunit.NewPool(pool), cipher)
}
diff --git a/services/core/internal/persistence/postgres/vaultpg/oauth_test.go b/services/core/internal/persistence/postgres/vaultpg/oauth_test.go
index 5b4c47efe..18ed72bc5 100644
--- a/services/core/internal/persistence/postgres/vaultpg/oauth_test.go
+++ b/services/core/internal/persistence/postgres/vaultpg/oauth_test.go
@@ -97,11 +97,11 @@ func TestOAuthCredentialMetadataEncryptionAndScope(t *testing.T) {
credential := f.create(t, input)
got, err := f.store.GetCredential(t.Context(), f.tenant, f.vault.ID, credential.ID)
if err != nil || !reflect.DeepEqual(got, credential) {
- t.Fatal("keyless safe metadata changed", err)
+ t.Fatal("safe metadata changed", err)
}
page, err := f.store.ListCredentials(t.Context(), f.tenant, f.vault.ID, vaults.PageQuery{Limit: 20, Ascending: true})
if err != nil || len(page.Credentials) != 1 || !reflect.DeepEqual(page.Credentials[0], credential) {
- t.Fatal("keyless listing failed", err)
+ t.Fatal("listing failed", err)
}
encoded, _ := json.Marshal(page)
var ciphertext []byte
@@ -137,13 +137,6 @@ func TestOAuthCredentialMetadataEncryptionAndScope(t *testing.T) {
if _, err := f.service.CreateOAuthCredential(t.Context(), foreign); !errors.Is(err, vaults.ErrNotFound) {
t.Fatal("foreign creation admitted")
}
- keyless := f.otherService(t, nil, counting)
- if _, err := keyless.CreateOAuthCredential(t.Context(), input); !errors.Is(err, credentialcrypto.ErrUnavailable) {
- t.Fatal("keyless creation admitted")
- }
- if token, err := f.token(t.Context(), keyless, credential); !errors.Is(err, credentialcrypto.ErrUnavailable) || token != "" {
- t.Fatal("keyless execution admitted")
- }
wrong := f.otherService(t, newCipher(t, bytes.Repeat([]byte{18}, 32)), counting)
if token, err := f.token(t.Context(), wrong, credential); err == nil || token != "" {
t.Fatal("wrong key executed")
diff --git a/services/core/internal/persistence/postgres/vaultpg/seal.go b/services/core/internal/persistence/postgres/vaultpg/seal.go
index 58e73e2d0..0ea527470 100644
--- a/services/core/internal/persistence/postgres/vaultpg/seal.go
+++ b/services/core/internal/persistence/postgres/vaultpg/seal.go
@@ -32,9 +32,6 @@ type oauthPayload struct {
// sealStatic seals a static_bearer token.
func (s *Store) sealStatic(scope credentialcrypto.Binding, token string) ([]byte, error) {
- if s.cipher == nil {
- return nil, credentialcrypto.ErrUnavailable
- }
ciphertext, err := s.cipher.Seal([]byte(token), scope)
if err != nil {
return nil, errors.New("credential encryption failed")
@@ -44,9 +41,6 @@ func (s *Store) sealStatic(scope credentialcrypto.Binding, token string) ([]byte
// openStatic opens a static_bearer token.
func (s *Store) openStatic(scope credentialcrypto.Binding, ciphertext []byte) (string, error) {
- if s.cipher == nil {
- return "", credentialcrypto.ErrUnavailable
- }
plaintext, err := s.cipher.Open(ciphertext, scope)
if err != nil {
return "", errors.New("MCP credential decryption failed")
@@ -57,9 +51,6 @@ func (s *Store) openStatic(scope credentialcrypto.Binding, ciphertext []byte) (s
// sealOAuth encodes the grant's metadata for its column and seals the whole
// grant.
func (s *Store) sealOAuth(scope credentialcrypto.Binding, grant vaults.OAuthGrant) (metadata, ciphertext []byte, err error) {
- if s.cipher == nil {
- return nil, nil, credentialcrypto.ErrUnavailable
- }
metadata, err = json.Marshal(grant.Metadata)
if err != nil {
return nil, nil, errors.New("credential encoding failed")
@@ -79,9 +70,6 @@ func (s *Store) sealOAuth(scope credentialcrypto.Binding, grant vaults.OAuthGran
// openOAuth opens a grant and authenticates the stored metadata against the
// sealed copy.
func (s *Store) openOAuth(scope credentialcrypto.Binding, stored *vaults.OAuthMetadata, ciphertext []byte) (vaults.OAuthGrant, error) {
- if s.cipher == nil {
- return vaults.OAuthGrant{}, credentialcrypto.ErrUnavailable
- }
plaintext, err := s.cipher.Open(ciphertext, scope)
if err != nil {
return vaults.OAuthGrant{}, errors.New("OAuth credential decryption failed")
diff --git a/services/core/internal/persistence/postgres/vaultpg/seal_test.go b/services/core/internal/persistence/postgres/vaultpg/seal_test.go
index e04ff428c..d77172ab8 100644
--- a/services/core/internal/persistence/postgres/vaultpg/seal_test.go
+++ b/services/core/internal/persistence/postgres/vaultpg/seal_test.go
@@ -11,17 +11,17 @@ import (
"github.com/google/uuid"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto"
+ "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/vaults"
)
// The Store seals to the canonical binding Core has always used, so secrets
-// sealed before the Store owned the key still open. A missing key is
-// credentialcrypto.ErrUnavailable, and a wrong binding is a decryption
-// failure, never a missing key, row or token.
+// sealed before the Store owned the key still open. A replaced key or a wrong
+// binding is a decryption failure, never a missing row or token.
func TestCredentialSecretsKeepTheirSealedFormat(t *testing.T) {
_, pool := openStore(t)
cipher := newCipher(t, bytes.Repeat([]byte{61}, 32))
- service, keyless := newService(t, pool, cipher, nil), newService(t, pool, nil, nil)
+ service, replaced := newService(t, pool, cipher, nil), newService(t, pool, pgtest.CredentialKey(t), nil)
tenant, url := uuid.NewString(), "https://mcp.example/tools"
vault := createVault(t, service, tenant)
expiry := time.Now().Add(time.Hour).UTC().Format(time.RFC3339Nano)
@@ -77,14 +77,14 @@ func TestCredentialSecretsKeepTheirSealedFormat(t *testing.T) {
write(static, []byte("old-static"), scope(static))
write(oauth, legacy, scope(oauth))
for _, tc := range []struct {
- credential vaults.Credential
- want string
- }{{static, "old-static"}, {oauth, "old-access"}} {
+ credential vaults.Credential
+ want, fails string
+ }{{static, "old-static", "MCP credential decryption failed"}, {oauth, "old-access", "OAuth credential decryption failed"}} {
if got, err := token(service, tc.credential); err != nil || got != tc.want {
t.Fatal("a secret sealed before the move did not open", err)
}
- if got, err := token(keyless, tc.credential); !errors.Is(err, credentialcrypto.ErrUnavailable) || got != "" {
- t.Fatal("a keyless Store opened a secret", err)
+ if got, err := token(replaced, tc.credential); err == nil || err.Error() != tc.fails || got != "" {
+ t.Fatal("a replaced key opened a secret", err)
}
}
if _, err := service.UpdateOAuthCredential(t.Context(), vaults.UpdateOAuthCredential{TenantID: tenant, VaultID: vault.ID, CredentialID: oauth.ID, AccessToken: ptr("patched-access")}); err != nil {
@@ -93,21 +93,11 @@ func TestCredentialSecretsKeepTheirSealedFormat(t *testing.T) {
if grant := readGrant(t, pool, cipher, tenant, oauth); grant.AccessToken != "patched-access" || grant.RefreshToken != "refresh" {
t.Fatal("the replaced grant lost its material")
}
- // Without a key, every write that seals fails first, before a malformed Vault.
for _, create := range []func(*vaults.Service, string) (vaults.Credential, error){createToken, createOAuth} {
- if _, err := create(keyless, "not-a-vault"); !errors.Is(err, credentialcrypto.ErrUnavailable) {
- t.Fatal("a keyless creation did not fail closed", err)
- }
if _, err := create(service, "not-a-vault"); !errors.Is(err, vaults.ErrNotFound) {
t.Fatal("a malformed Vault named one", err)
}
}
- if _, err := keyless.UpdateStaticCredential(t.Context(), vaults.UpdateStaticCredential{TenantID: tenant, VaultID: vault.ID, CredentialID: static.ID, Token: "rejected"}); !errors.Is(err, credentialcrypto.ErrUnavailable) {
- t.Fatal("a keyless replacement did not fail closed", err)
- }
- if _, err := keyless.UpdateOAuthCredential(t.Context(), vaults.UpdateOAuthCredential{TenantID: tenant, VaultID: vault.ID, CredentialID: oauth.ID, AccessToken: ptr("rejected")}); !errors.Is(err, credentialcrypto.ErrUnavailable) {
- t.Fatal("a keyless OAuth replacement did not fail closed", err)
- }
// A secret sealed to another Credential or destination does not open.
wrongID, wrongDestination := scope(static), scope(oauth)
wrongID.CredentialID = oauth.ID
diff --git a/services/core/internal/persistence/postgres/vaultpg/selection_test.go b/services/core/internal/persistence/postgres/vaultpg/selection_test.go
index 81f0a658e..449057c61 100644
--- a/services/core/internal/persistence/postgres/vaultpg/selection_test.go
+++ b/services/core/internal/persistence/postgres/vaultpg/selection_test.go
@@ -10,7 +10,7 @@ import (
"github.com/google/uuid"
- "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto"
+ "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/vaults"
)
@@ -22,7 +22,7 @@ func TestMCPCredentialSelectionAndScopedDecryption(t *testing.T) {
t.Fatal(err)
}
service := newService(t, pool, newCipher(t, key), nil)
- keyless := newService(t, pool, nil, nil)
+ replaced := newService(t, pool, pgtest.CredentialKey(t), nil)
var owned []vaults.Vault
for _, owner := range []string{tenant, tenant, foreign} {
owned = append(owned, createVault(t, service, owner))
@@ -39,8 +39,8 @@ func TestMCPCredentialSelectionAndScopedDecryption(t *testing.T) {
selectFor := func(service *vaults.Service, tenant string, attached []string, requests []vaults.MCPCredentialRequest) ([]vaults.MCPCredentialBinding, error) {
return service.ResolveMCPCredentials(t.Context(), vaults.ResolveMCPCredentials{TenantID: tenant, VaultIDs: attached, Requests: requests})
}
- // Selection reads metadata only, so a keyless Core can select.
- bindings, err := selectFor(keyless, tenant, attached, requests)
+ // Selection reads metadata only, so it works under a replaced key.
+ bindings, err := selectFor(replaced, tenant, attached, requests)
if err != nil || len(bindings) != 2 || bindings[0].CredentialID != first.ID || bindings[0].AuthType != vaults.AuthStaticBearer || bindings[1].CredentialID != "" {
t.Fatal("metadata selection or frozen anonymous decision differs", err)
}
@@ -49,11 +49,11 @@ func TestMCPCredentialSelectionAndScopedDecryption(t *testing.T) {
t.Fatal("private binding contains secret material")
}
second := create(owned[1])
- if _, err := selectFor(keyless, tenant, attached, requests); !isSelectionError(err, true, "multiple attached vault credentials match MCP server_url "+destination+"; specify credential_id") {
+ if _, err := selectFor(replaced, tenant, attached, requests); !isSelectionError(err, true, "multiple attached vault credentials match MCP server_url "+destination+"; specify credential_id") {
t.Fatal("ambiguous selection was admitted", err)
}
requests[0].CredentialID = &second.ID
- explicit, err := selectFor(keyless, tenant, attached, requests)
+ explicit, err := selectFor(replaced, tenant, attached, requests)
if err != nil || explicit[0].CredentialID != second.ID || requests[1].CredentialID != nil {
t.Fatal("explicit selection did not disambiguate", err)
}
@@ -72,25 +72,21 @@ func TestMCPCredentialSelectionAndScopedDecryption(t *testing.T) {
{tenant, []string{owned[0].ID, owned[2].ID}, first.ID, destination, ""},
{tenant, []string{uuid.NewString()}, first.ID, destination, ""},
} {
- _, err := selectFor(keyless, tc.owner, tc.vaults, []vaults.MCPCredentialRequest{{ServerLabel: "tools", ServerURL: tc.url, CredentialID: &tc.id}})
+ _, err := selectFor(replaced, tc.owner, tc.vaults, []vaults.MCPCredentialRequest{{ServerLabel: "tools", ServerURL: tc.url, CredentialID: &tc.id}})
if tc.message == "" && !errors.Is(err, vaults.ErrNotFound) || tc.message != "" && !isSelectionError(err, false, tc.message) {
t.Fatal("unowned, unattached or wrong-destination selection was admitted", err)
}
}
- if _, err := selectFor(keyless, tenant, nil, []vaults.MCPCredentialRequest{{ServerLabel: "tools", ServerURL: destination, CredentialID: &first.ID}}); !isSelectionError(err, false, "MCP credential_id requires an attached vault") {
+ if _, err := selectFor(replaced, tenant, nil, []vaults.MCPCredentialRequest{{ServerLabel: "tools", ServerURL: destination, CredentialID: &first.ID}}); !isSelectionError(err, false, "MCP credential_id requires an attached vault") {
t.Fatal("a reference without attachments was admitted", err)
}
pool.Close()
store, pool = openStore(t)
service = newService(t, pool, newCipher(t, bytes.Clone(key)), nil)
- keyless = newService(t, pool, nil, nil)
got, err := bearerToken(t.Context(), service, tenant, attached, bindings[0])
if err != nil || got != token {
t.Fatal("frozen selection or opaque bytes changed across restart", err)
}
- if got, err := bearerToken(t.Context(), keyless, tenant, attached, bindings[0]); !errors.Is(err, credentialcrypto.ErrUnavailable) || got != "" {
- t.Fatal("missing key did not fail execution closed")
- }
key[0] ^= 1
if got, err := bearerToken(t.Context(), newService(t, pool, newCipher(t, key), nil), tenant, attached, bindings[0]); err == nil || got != "" || strings.Contains(err.Error(), token) {
t.Fatal("wrong key leaked or decrypted a credential")
diff --git a/services/core/internal/persistence/postgres/vaultpg/vaultpg.go b/services/core/internal/persistence/postgres/vaultpg/vaultpg.go
index f8863309e..1adb7e834 100644
--- a/services/core/internal/persistence/postgres/vaultpg/vaultpg.go
+++ b/services/core/internal/persistence/postgres/vaultpg/vaultpg.go
@@ -30,9 +30,7 @@ type Store struct {
var _ vaults.Storage = (*Store)(nil)
-// New returns a Store. Without a credential key (cipher nil), operations that
-// seal or open a secret fail with credentialcrypto.ErrUnavailable; Vaults,
-// Credential metadata, selection and deletion keep working.
+// New returns a Store that seals and opens secrets with cipher.
func New(pool *pgunit.Pool, cipher *credentialcrypto.Cipher) *Store {
return &Store{pool: pool, cipher: cipher}
}
diff --git a/services/core/internal/persistence/postgres/vaultpg/vaults_test.go b/services/core/internal/persistence/postgres/vaultpg/vaults_test.go
index f95282651..6a7ce4da8 100644
--- a/services/core/internal/persistence/postgres/vaultpg/vaults_test.go
+++ b/services/core/internal/persistence/postgres/vaultpg/vaults_test.go
@@ -13,13 +13,14 @@ import (
"github.com/google/uuid"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc"
+ "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/vaults"
)
func TestVaultsPersistAndStayTenantScoped(t *testing.T) {
store, pool := openStore(t)
- service := newService(t, pool, nil, nil)
+ service := newService(t, pool, pgtest.CredentialKey(t), nil)
ctx := t.Context()
tenantA, tenantB := uuid.NewString(), uuid.NewString()
before := time.Now().Add(-time.Second)
@@ -67,7 +68,7 @@ func TestVaultsPersistAndStayTenantScoped(t *testing.T) {
func TestVaultsRejectInvalidInputWithoutWrites(t *testing.T) {
store, pool := openStore(t)
- service := newService(t, pool, nil, nil)
+ service := newService(t, pool, pgtest.CredentialKey(t), nil)
ctx := t.Context()
tenant := uuid.NewString()
for _, name := range []string{"", strings.Repeat("x", 257), strings.Repeat("é", 129), string([]byte{0xff})} {
@@ -97,7 +98,7 @@ func TestVaultsRejectInvalidInputWithoutWrites(t *testing.T) {
func TestVaultListFilteringPaginationAndReconnect(t *testing.T) {
store, pool := openStore(t)
- service := newService(t, pool, nil, nil)
+ service := newService(t, pool, pgtest.CredentialKey(t), nil)
ctx := t.Context()
tenant, other := uuid.NewString(), uuid.NewString()
empty, err := store.ListVaults(ctx, tenant, vaults.PageQuery{Limit: 20})
@@ -206,7 +207,7 @@ func TestVaultDeletionCascadeBindingAndRestart(t *testing.T) {
tenant, foreign := uuid.NewString(), uuid.NewString()
key := bytes.Repeat([]byte{43}, 32)
service := newService(t, pool, newCipher(t, key), nil)
- keyless := newService(t, pool, nil, nil)
+ replaced := newService(t, pool, pgtest.CredentialKey(t), nil)
vault, retained, empty := createVault(t, service, tenant), createVault(t, service, tenant), createVault(t, service, tenant)
original := createStatic(t, service, tenant, vault.ID, "original", "https://mcp.example/tools", "original-secret")
attached := []string{vault.ID, retained.ID}
@@ -220,11 +221,11 @@ func TestVaultDeletionCascadeBindingAndRestart(t *testing.T) {
t.Fatal(err)
}
for _, scope := range []struct{ tenant, id string }{{foreign, vault.ID}, {tenant, uuid.NewString()}, {tenant, "invalid"}, {"invalid", vault.ID}} {
- if _, err := keyless.DeleteVault(t.Context(), vaults.DeleteVault{TenantID: scope.tenant, VaultID: scope.id}); !errors.Is(err, vaults.ErrNotFound) {
+ if _, err := replaced.DeleteVault(t.Context(), vaults.DeleteVault{TenantID: scope.tenant, VaultID: scope.id}); !errors.Is(err, vaults.ErrNotFound) {
t.Fatal("foreign or invalid deletion was accepted", err)
}
}
- _, deletionErr := newService(t, readOnlyPool(t, pool), nil, nil).DeleteVault(t.Context(), vaults.DeleteVault{TenantID: tenant, VaultID: vault.ID})
+ _, deletionErr := newService(t, readOnlyPool(t, pool), pgtest.CredentialKey(t), nil).DeleteVault(t.Context(), vaults.DeleteVault{TenantID: tenant, VaultID: vault.ID})
if !isReadOnlyFailure(deletionErr) {
t.Fatal("failed mutation was accepted or translated", deletionErr)
}
@@ -259,8 +260,8 @@ func TestVaultDeletionCascadeBindingAndRestart(t *testing.T) {
t.Fatal(err)
}
for _, target := range []vaults.Vault{empty, vault} {
- if id, err := keyless.DeleteVault(t.Context(), vaults.DeleteVault{TenantID: tenant, VaultID: target.ID}); err != nil || id != target.ID {
- t.Fatal("keyless deletion failed", err)
+ if id, err := replaced.DeleteVault(t.Context(), vaults.DeleteVault{TenantID: tenant, VaultID: target.ID}); err != nil || id != target.ID {
+ t.Fatal("deletion under a replaced key failed", err)
}
}
if err := pool.QueryRow(t.Context(), "SELECT (SELECT count(*) FROM vaults WHERE id=$1)+(SELECT count(*) FROM vault_credentials WHERE vault_id=$1)", vault.ID).Scan(&count); err != nil || count != 0 {
diff --git a/services/core/internal/processconfig/config.go b/services/core/internal/processconfig/config.go
index eb5eb0117..23ab40c92 100644
--- a/services/core/internal/processconfig/config.go
+++ b/services/core/internal/processconfig/config.go
@@ -44,17 +44,15 @@ const (
type Config struct {
// Addr is OAC_ADDR, the listener address.
Addr string
- // PublicOrigin is OAC_PUBLIC_URL. Nil disables the Runtime gateway and
- // the execution Worker.
- PublicOrigin *deployment.PublicOrigin
+ // PublicOrigin is OAC_PUBLIC_URL.
+ PublicOrigin deployment.PublicOrigin
// DatabaseURL is OAC_DATABASE_URL with the password from
// OAC_DATABASE_PASSWORD_FILE.
DatabaseURL string
- // InstallationID comes from OAC_INSTALLATION_ID_FILE. Empty leaves the
- // sandbox deployment and node routes off.
+ // InstallationID comes from OAC_INSTALLATION_ID_FILE.
InstallationID string
- // CredentialKey seals stored credentials. Nil when
- // OAC_CREDENTIAL_KEY_FILE is unset.
+ // CredentialKey seals stored credentials; it comes from
+ // OAC_CREDENTIAL_KEY_FILE.
CredentialKey *credentialcrypto.Cipher
// CoreKeys authenticates the Core key from OAC_CORE_KEY_DIGESTS_FILE.
CoreKeys *api.DeploymentAuthenticator
@@ -101,7 +99,7 @@ type runtimeHistoryFile struct {
}
// Load reads and validates the process environment. An unset or empty
-// variable selects its default.
+// optional variable selects its default.
func Load() (Config, error) {
var c Config
var err error
@@ -109,12 +107,12 @@ func Load() (Config, error) {
return Config{}, err
}
c.Addr = cmp.Or(os.Getenv("OAC_ADDR"), defaultAddr)
- if value := os.Getenv("OAC_PUBLIC_URL"); value != "" {
- origin, err := deployment.NewPublicOrigin(value)
- if err != nil {
- return Config{}, configError("OAC_PUBLIC_URL must be a canonical http or https origin without path, credentials, query or fragment, such as https://core.example")
- }
- c.PublicOrigin = &origin
+ value := os.Getenv("OAC_PUBLIC_URL")
+ if value == "" {
+ return Config{}, configError("OAC_PUBLIC_URL is required; applications, nodes and sandboxes reach Core at this origin")
+ }
+ if c.PublicOrigin, err = deployment.NewPublicOrigin(value); err != nil {
+ return Config{}, configError("OAC_PUBLIC_URL must be a canonical http or https origin without path, credentials, query or fragment, such as https://core.example")
}
if c.DatabaseURL, err = databaseurl.FromEnvironment(); err != nil {
return Config{}, err
@@ -125,9 +123,6 @@ func Load() (Config, error) {
if c.InstallationID, err = installationID(); err != nil {
return Config{}, err
}
- if c.InstallationID != "" && c.PublicOrigin == nil {
- return Config{}, configError("OAC_INSTALLATION_ID_FILE requires OAC_PUBLIC_URL, the origin nodes and sandboxes use to reach Core")
- }
if c.CredentialKey, err = credentialKey(); err != nil {
return Config{}, err
}
@@ -163,10 +158,6 @@ func Load() (Config, error) {
// Settings projects the configuration that GET /core/v1/installation
// reports. Sensitive file settings report only whether they are configured.
func (c Config) Settings() []api.InstallationSetting {
- var public any
- if c.PublicOrigin != nil {
- public = c.PublicOrigin.String()
- }
format := c.Log.Format
if format == "" {
format = "auto"
@@ -176,7 +167,7 @@ func (c Config) Settings() []api.InstallationSetting {
origins = []string{}
}
return []api.InstallationSetting{
- setting("public_url", public, nil, []string{"core", "web"}),
+ setting("public_url", c.PublicOrigin.String(), nil, []string{"core", "web"}),
setting("log.level", strings.ToLower(c.Log.Level.String()), "info", []string{"core", "web"}),
setting("log.format", format, "auto", []string{"core", "web"}),
setting("log.add_source", c.Log.AddSource, false, []string{"core", "web"}),
@@ -213,7 +204,7 @@ func sensitive(key string, configured bool, restarts []string) api.InstallationS
func installationID() (string, error) {
path := os.Getenv("OAC_INSTALLATION_ID_FILE")
if path == "" {
- return "", nil
+ return "", configError("OAC_INSTALLATION_ID_FILE is required; it names the file that holds this installation's ID")
}
raw, err := os.ReadFile(path)
if err != nil {
@@ -229,7 +220,7 @@ func installationID() (string, error) {
func credentialKey() (*credentialcrypto.Cipher, error) {
path := os.Getenv("OAC_CREDENTIAL_KEY_FILE")
if path == "" {
- return nil, nil
+ return nil, configError("OAC_CREDENTIAL_KEY_FILE is required; Core seals stored credentials with this key")
}
content, err := os.ReadFile(path)
if err != nil {
diff --git a/services/core/internal/processconfig/config_test.go b/services/core/internal/processconfig/config_test.go
index d66edb2bc..0c535ab9b 100644
--- a/services/core/internal/processconfig/config_test.go
+++ b/services/core/internal/processconfig/config_test.go
@@ -23,11 +23,16 @@ func required(t *testing.T) func(name, content string) string {
}
return path
}
+ t.Setenv("OAC_PUBLIC_URL", "https://core.example")
t.Setenv("OAC_DATABASE_URL", "postgres://core@database/core")
+ t.Setenv("OAC_INSTALLATION_ID_FILE", write("installation.id", testInstallationID+"\n"))
+ t.Setenv("OAC_CREDENTIAL_KEY_FILE", write("credential.key", base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{0x91}, 32))+"\n"))
t.Setenv("OAC_CORE_KEY_DIGESTS_FILE", write("digests.json", `["`+strings.Repeat("ab", 32)+`"]`))
return write
}
+const testInstallationID = "8c5f4f5e-2c55-4c43-9a49-7f2f3f2d1d10"
+
// rejects asserts that Load fails, names variable and does not echo secret.
func rejects(t *testing.T, variable, secret string) {
t.Helper()
@@ -43,7 +48,7 @@ func TestLoadAppliesDefaults(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- if c.Addr != "127.0.0.1:8091" || c.PublicOrigin != nil || c.InstallationID != "" || c.CredentialKey != nil || c.CoreKeys == nil ||
+ if c.Addr != "127.0.0.1:8091" || c.PublicOrigin.String() != "https://core.example" || c.InstallationID != testInstallationID || c.CredentialKey == nil || c.CoreKeys == nil ||
c.ExecutionConcurrency != 4 || c.DefaultHarness != "codex" || strings.Join(c.Harnesses, ",") != "claude_sdk,codex,mcode" ||
c.WriteAuditRetention != 90*24*time.Hour || c.OAuthTrustedOrigins != nil || c.NativeInstallers != "" || c.ProviderPaths.StateRoot != "/state" {
t.Fatalf("%+v", c)
@@ -59,11 +64,12 @@ func TestLoadAppliesDefaults(t *testing.T) {
func TestLoadRejectsInvalidValuesWithoutEchoingThem(t *testing.T) {
write := required(t)
- t.Setenv("OAC_DATABASE_URL", "")
- rejects(t, "OAC_DATABASE_URL", "")
- required(t)
- t.Setenv("OAC_CORE_KEY_DIGESTS_FILE", "")
- rejects(t, "OAC_CORE_KEY_DIGESTS_FILE", "")
+ for _, variable := range []string{"OAC_PUBLIC_URL", "OAC_DATABASE_URL", "OAC_INSTALLATION_ID_FILE", "OAC_CREDENTIAL_KEY_FILE", "OAC_CORE_KEY_DIGESTS_FILE"} {
+ t.Run(variable+" missing", func(t *testing.T) {
+ t.Setenv(variable, "")
+ rejects(t, variable, "")
+ })
+ }
t.Setenv("OAC_CORE_KEY_DIGESTS_FILE", write("bad-digests.json", `["synthetic-secret"]`))
rejects(t, "OAC_CORE_KEY_DIGESTS_FILE", "synthetic-secret")
required(t)
@@ -75,8 +81,8 @@ func TestLoadRejectsInvalidValuesWithoutEchoingThem(t *testing.T) {
"OAC_WRITE_AUDIT_RETENTION": "synthetic-secret",
"OAC_OAUTH_TRUSTED_ORIGINS": "https://synthetic-secret.example/token",
"OAC_LOG_LEVEL": "verbose",
- "OAC_INSTALLATION_ID_FILE": write("installation.id", "synthetic-secret"),
- "OAC_CREDENTIAL_KEY_FILE": write("credential.key", "synthetic-secret"),
+ "OAC_INSTALLATION_ID_FILE": write("bad.id", "synthetic-secret"),
+ "OAC_CREDENTIAL_KEY_FILE": write("bad.key", "synthetic-secret"),
"OAC_HISTORY_SETTINGS_FILE": write("history.json", `{"secret":"synthetic-secret"}`),
} {
t.Run(variable, func(t *testing.T) {
@@ -84,12 +90,6 @@ func TestLoadRejectsInvalidValuesWithoutEchoingThem(t *testing.T) {
rejects(t, variable, "synthetic-secret")
})
}
- t.Setenv("OAC_INSTALLATION_ID_FILE", write("valid.id", "8c5f4f5e-2c55-4c43-9a49-7f2f3f2d1d10\n"))
- rejects(t, "OAC_PUBLIC_URL", "")
- t.Setenv("OAC_PUBLIC_URL", "https://core.example")
- if c, err := Load(); err != nil || c.InstallationID != "8c5f4f5e-2c55-4c43-9a49-7f2f3f2d1d10" {
- t.Fatal(c.InstallationID, err)
- }
}
func TestPublicURLMustBeACanonicalOrigin(t *testing.T) {
@@ -151,13 +151,6 @@ func TestOAuthTrustedOrigins(t *testing.T) {
func TestCredentialKey(t *testing.T) {
write := required(t)
- t.Setenv("OAC_CREDENTIAL_KEY_FILE", filepath.Join(t.TempDir(), "missing.key"))
- rejects(t, "OAC_CREDENTIAL_KEY_FILE", "")
- for _, content := range []string{"", base64.StdEncoding.EncodeToString(make([]byte, 31))} {
- t.Setenv("OAC_CREDENTIAL_KEY_FILE", write("short.key", content))
- rejects(t, "OAC_CREDENTIAL_KEY_FILE", "")
- }
- t.Setenv("OAC_CREDENTIAL_KEY_FILE", write("credential.key", base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{0x91}, 32))+"\n"))
first, err := Load()
if err != nil {
t.Fatal(err)
@@ -174,6 +167,12 @@ func TestCredentialKey(t *testing.T) {
if got, err := reopened.CredentialKey.Open(sealed, binding); err != nil || string(got) != "opaque storage test" {
t.Fatal("persisted key did not recover ciphertext", err)
}
+ t.Setenv("OAC_CREDENTIAL_KEY_FILE", filepath.Join(t.TempDir(), "missing.key"))
+ rejects(t, "OAC_CREDENTIAL_KEY_FILE", "")
+ for _, content := range []string{"", base64.StdEncoding.EncodeToString(make([]byte, 31))} {
+ t.Setenv("OAC_CREDENTIAL_KEY_FILE", write("short.key", content))
+ rejects(t, "OAC_CREDENTIAL_KEY_FILE", "")
+ }
}
func TestRuntimeHistoryFile(t *testing.T) {
@@ -207,7 +206,6 @@ func TestRuntimeHistoryFile(t *testing.T) {
func TestSettingsReportEffectiveValuesAndHideHistory(t *testing.T) {
write := required(t)
- t.Setenv("OAC_PUBLIC_URL", "https://core.example")
t.Setenv("OAC_EXECUTION_CONCURRENCY", "8")
t.Setenv("OAC_LOG_LEVEL", "warn")
t.Setenv("OAC_WRITE_AUDIT_RETENTION", "1440m")
diff --git a/services/core/internal/runtimeenrollment/connection.go b/services/core/internal/runtimeenrollment/connection.go
index 8cbb2b0f6..7bebb425f 100644
--- a/services/core/internal/runtimeenrollment/connection.go
+++ b/services/core/internal/runtimeenrollment/connection.go
@@ -102,9 +102,6 @@ func RuntimeConnected(ctx context.Context, s ConnectionStore, registry *runtimeg
if credential.CredentialHash != digest {
return false, sessions.ErrDeviceBindingConflict
}
- if registry == nil {
- return false, nil
- }
peer, err := registry.LookupDevice(bound.ID)
if errors.Is(err, runtimegateway.ErrDeviceNotRegistered) {
return false, nil
diff --git a/services/core/internal/runtimeenrollment/connection_test.go b/services/core/internal/runtimeenrollment/connection_test.go
index e10519f92..4db7b557e 100644
--- a/services/core/internal/runtimeenrollment/connection_test.go
+++ b/services/core/internal/runtimeenrollment/connection_test.go
@@ -111,7 +111,7 @@ func (s *liveConnectionStore) GetDeviceCredential(context.Context, string) (runt
return runtimedevice.Credential{ID: "device", WorkspaceID: "tenant", Type: runtimedevice.RuntimeTypeAgentDaemon, CredentialHash: s.digest}, true, nil
}
func TestRuntimeConnectedCurrentAuthorityAfterPeer(t *testing.T) {
- for _, name := range []string{"connected", "rotated before read", "revoked after peer", "device revoked after peer", "retired after peer", "store error after peer", "device store error after peer", "closed", "no registry"} {
+ for _, name := range []string{"connected", "rotated before read", "revoked after peer", "device revoked after peer", "retired after peer", "store error after peer", "device store error after peer", "closed"} {
t.Run(name, func(t *testing.T) {
digest := runtimedevice.HashCredential("fixture-key")
s := &liveConnectionStore{digest: digest}
@@ -153,8 +153,6 @@ func TestRuntimeConnectedCurrentAuthorityAfterPeer(t *testing.T) {
wantErr = s.credentialRecheckError
case "closed":
peer.Close("closed before observation")
- case "no registry":
- registry = nil
}
connected, err := RuntimeConnected(t.Context(), s, registry, "environment", digest)
if connected != want || !errors.Is(err, wantErr) {
diff --git a/services/core/internal/sandbox/providers/configuration_flow_test.go b/services/core/internal/sandbox/providers/configuration_flow_test.go
index 12b338067..68624d30c 100644
--- a/services/core/internal/sandbox/providers/configuration_flow_test.go
+++ b/services/core/internal/sandbox/providers/configuration_flow_test.go
@@ -98,7 +98,7 @@ func TestAdditionalConfigurationProviderUsesCommonAPIAndStore(t *testing.T) {
// The deployment reaches the registered configuration only through the
// registry it is built with.
deployments := func() *deployment.Service {
- storage := deploymentpg.New(pgunit.NewPool(pool), nil)
+ storage := deploymentpg.New(pgunit.NewPool(pool), pgtest.CredentialKey(t))
rules, err := placement.NewRules(registry, "")
if err != nil {
t.Fatal(err)
@@ -115,7 +115,7 @@ func TestAdditionalConfigurationProviderUsesCommonAPIAndStore(t *testing.T) {
t.Fatal(err)
}
defer lease.Close(context.Background())
- changes, err := deployment.NewExecutionOperations(service, deploymentpg.NewExecution(lease, nil))
+ changes, err := deployment.NewExecutionOperations(service, deploymentpg.NewExecution(lease, pgtest.CredentialKey(t)))
if err != nil {
t.Fatal(err)
}
@@ -151,14 +151,14 @@ func TestAdditionalConfigurationProviderUsesCommonAPIAndStore(t *testing.T) {
Environments: struct{ api.Environments }{}, EnvironmentsReader: struct{ api.EnvironmentsReader }{}, Admin: struct{ api.Admin }{}, AdminAudit: struct{ api.AdminAudit }{}, WriteAudit: struct{ api.WriteAudit }{},
ExecutorConnections: struct{ api.ExecutorConnections }{},
Metrics: struct{ api.Metrics }{}, RuntimeObservations: struct{ api.RuntimeObservations }{}, RuntimeHistory: struct{ api.RuntimeHistory }{},
- Execution: &api.Execution{
+ Execution: api.Execution{
ExecutorURL: "wss://core.example/api/v1/agent-daemon/ws",
SessionAdmission: struct{ api.SessionAdmission }{},
InputAdmission: struct{ api.InputAdmission }{},
SessionArchive: struct{ api.SessionArchive }{},
Workspaces: struct{ api.EnvironmentWorkspaces }{},
},
- Sandboxes: &api.Sandboxes{Deployment: service, NodeAllocations: deploymentpg.New(pgunit.NewPool(pool), nil), DeploymentChanges: leaseSetup{t: t, changes: changes, installation: installation},
+ Sandboxes: api.Sandboxes{Deployment: service, NodeAllocations: deploymentpg.New(pgunit.NewPool(pool), pgtest.CredentialKey(t)), DeploymentChanges: leaseSetup{t: t, changes: changes, installation: installation},
DeploymentReset: leaseSetup{t: t, changes: changes, installation: installation}, ConfigurationDiscovery: struct{ api.ConfigurationDiscovery }{}},
})
if err != nil {
diff --git a/services/core/internal/sessions/creation.go b/services/core/internal/sessions/creation.go
index a970db327..f53c6fca9 100644
--- a/services/core/internal/sessions/creation.go
+++ b/services/core/internal/sessions/creation.go
@@ -15,7 +15,6 @@ import (
"github.com/google/uuid"
v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1"
- "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment/placement"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity"
@@ -29,7 +28,6 @@ import (
type CreationStorage interface {
// FingerprintProviderKey returns the keyed fingerprint of a model
// provider key, so retry identities tell keys apart without hashing a key.
- // Without a credential key it is credentialcrypto.ErrUnavailable.
FingerprintProviderKey(secret string) (string, error)
// WithCreation runs apply in one pooled transaction and commits only when
// apply returns nil. A malformed tenant is ErrInvalidInput.
@@ -534,12 +532,6 @@ func (s *Service) FindSessionCreation(ctx context.Context, tenant, key string, r
return Creation{}, ErrInvalidInput
}
hash, err := intentHash(request, s.storage.FingerprintProviderKey)
- if errors.Is(err, credentialcrypto.ErrUnavailable) {
- // Without the credential key no Session with a provider bundle can
- // have been committed or can be created; creation reports the missing
- // key after request validation.
- return Creation{}, ErrNotFound
- }
if err != nil {
return Creation{}, err
}
diff --git a/services/core/internal/sessions/creation_test.go b/services/core/internal/sessions/creation_test.go
index 7fe34ff68..5bb22c0fc 100644
--- a/services/core/internal/sessions/creation_test.go
+++ b/services/core/internal/sessions/creation_test.go
@@ -14,7 +14,6 @@ import (
"github.com/google/uuid"
v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1"
- "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment/placement"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity"
@@ -141,7 +140,10 @@ func (s *fakeStorage) FindCreation(_ context.Context, tenant, key string) (Creat
// fingerprints is a fake FingerprintProviderKey that keeps keys apart.
func fingerprints(secret string) (string, error) { return "fp-" + secret, nil }
-func unavailable(string) (string, error) { return "", credentialcrypto.ErrUnavailable }
+var errFingerprint = errors.New("fingerprint failed")
+
+// failing is a fake FingerprintProviderKey that fails.
+func failing(string) (string, error) { return "", errFingerprint }
// declarations accept every provider and specification.
type declarations struct{}
@@ -202,8 +204,8 @@ func TestPrepareCreation(t *testing.T) {
if prepare(t, withProvider("self_hosted", "one", "session"), fingerprints).RequestHash == prepare(t, withProvider("self_hosted", "two", "session"), fingerprints).RequestHash {
t.Fatal("caller keys share an identity")
}
- deployed := prepare(t, withProvider("none", "deployment-key", v1.ModelProviderSourceDeployment), unavailable)
- plain := prepare(t, func(input *CreateSession) { input.Configuration = creationInput("none").Configuration }, unavailable)
+ deployed := prepare(t, withProvider("none", "deployment-key", v1.ModelProviderSourceDeployment), failing)
+ plain := prepare(t, func(input *CreateSession) { input.Configuration = creationInput("none").Configuration }, failing)
if !strings.Contains(string(deployed.Configuration), `"model_provider_configured":true`) || deployed.RequestHash != plain.RequestHash {
t.Fatalf("the deployment default joined the identity: %s", deployed.Configuration)
}
@@ -226,7 +228,7 @@ func TestPrepareCreation(t *testing.T) {
"configuration array": {func(input *CreateSession) { input.Configuration = json.RawMessage(`[]`) }, fingerprints, ErrInvalidInput},
"cancel initial input": {func(input *CreateSession) { input.InitialInputs = []Input{cancelInput} }, fingerprints, ErrInvalidInput},
"deployment self_hosted": {withProvider("self_hosted", "key", v1.ModelProviderSourceDeployment), fingerprints, ErrInvalidInput},
- "no credential key": {withProvider("self_hosted", "key", "session"), unavailable, credentialcrypto.ErrUnavailable},
+ "fingerprint failure": {withProvider("self_hosted", "key", "session"), failing, errFingerprint},
"unreadable intent": {func(input *CreateSession) { input.CreationRequest = json.RawMessage(`[]`) }, fingerprints, ErrInvalidInput},
} {
t.Run(name, func(t *testing.T) {
@@ -264,10 +266,10 @@ func TestIntentHash(t *testing.T) {
"over 16 MiB": {`"` + strings.Repeat("x", 16<<20) + `"`, fingerprints, ErrInvalidInput},
"extension not object": {`{"x_agents_core":[]}`, fingerprints, ErrInvalidInput},
"provider unreadable": {`{"x_agents_core":{"model_provider":[]}}`, fingerprints, ErrInvalidInput},
- "provider no key": {`{"x_agents_core":{"model_provider":{"api_key":"k"}}}`, unavailable, credentialcrypto.ErrUnavailable},
- "null extension": {`{"x_agents_core":null}`, unavailable, nil},
- "null provider": {`{"x_agents_core":{"model_provider":null}}`, unavailable, nil},
- "intent without key": {`{"agent_id":"a"}`, unavailable, nil},
+ "provider fingerprint": {`{"x_agents_core":{"model_provider":{"api_key":"k"}}}`, failing, errFingerprint},
+ "null extension": {`{"x_agents_core":null}`, failing, nil},
+ "null provider": {`{"x_agents_core":{"model_provider":null}}`, failing, nil},
+ "intent without key": {`{"agent_id":"a"}`, failing, nil},
} {
if _, err := hash(test.raw, test.fingerprint); test.want == nil && err != nil || test.want != nil && !errors.Is(err, test.want) {
t.Errorf("%s: got %v, want %v", name, err, test.want)
@@ -538,7 +540,6 @@ func TestFindSessionCreation(t *testing.T) {
{"recorded intent", "create", request, creator, fingerprints, found, nil, []string{"FindCreation tenant create"}},
{"missing creation", "create", request, creator, fingerprints, func() (CreationRecord, error) { return CreationRecord{}, ErrNotFound }, ErrNotFound, []string{"FindCreation tenant create"}},
{"another creator", "create", request, identity.Subject{Kind: "user", ID: "stranger"}, fingerprints, found, ErrIdempotencyConflict, []string{"FindCreation tenant create"}},
- {"no credential key", "create", json.RawMessage(`{"x_agents_core":{"model_provider":{"api_key":"k"}}}`), creator, unavailable, nil, ErrNotFound, []string{"FingerprintProviderKey"}},
{"no intent", "create", nil, creator, nil, nil, ErrInvalidInput, nil},
{"invalid key", " ", request, creator, nil, nil, ErrInvalidInput, nil},
{"invalid creator", "create", request, identity.Subject{}, nil, nil, ErrInvalidInput, nil},
diff --git a/services/core/internal/sessions/environment.go b/services/core/internal/sessions/environment.go
index 3ec6c03fe..2a11a64b8 100644
--- a/services/core/internal/sessions/environment.go
+++ b/services/core/internal/sessions/environment.go
@@ -26,14 +26,12 @@ type EnvironmentReader interface {
// pending or running.
ListEnvironmentInitializations(ctx context.Context, after string) ([]EnvironmentInitialization, error)
// ReadEnvironmentSetup opens the setup frozen for the Session's
- // Environment. A missing Session is ErrNotFound and a missing credential
- // key credentialcrypto.ErrUnavailable; frozen data that does not open or
- // validate is an internal error.
+ // Environment. A missing Session is ErrNotFound; frozen data that does not
+ // open or validate is an internal error.
ReadEnvironmentSetup(ctx context.Context, tenant, session string) (environmentconfig.Setup, error)
// ReadInitialEnvironmentFile opens the initial file frozen at position for
// the Session's Environment, so an installation holds one file at a time.
- // A missing file is ErrNotFound and a missing credential key
- // credentialcrypto.ErrUnavailable; a file that does not open or match its
+ // A missing file is ErrNotFound; a file that does not open or match its
// recorded size is an internal error.
ReadInitialEnvironmentFile(ctx context.Context, tenant, session string, position int) (environmentconfig.InitialFileMetadata, []byte, error)
}
diff --git a/services/core/internal/sessions/executor_credentials.go b/services/core/internal/sessions/executor_credentials.go
index dcde3026d..9fb1ea412 100644
--- a/services/core/internal/sessions/executor_credentials.go
+++ b/services/core/internal/sessions/executor_credentials.go
@@ -88,13 +88,11 @@ type ExecutorCredentialStorage interface {
// principal or Project, or an unknown one, is ErrNotFound.
LoadExecutorCredentialRestriction(ctx context.Context, principal identity.Principal, key string) (string, error)
// SignInstallation returns the signature of an installation authorization
- // payload under the credential key. Without that key it is
- // credentialcrypto.ErrUnavailable.
+ // payload under the credential key.
SignInstallation(ctx context.Context, payload string) (string, error)
// VerifyInstallation checks the signature of an installation
// authorization payload: another signature is
- // ErrInstallationAuthorization, and a service without the credential key
- // credentialcrypto.ErrUnavailable.
+ // ErrInstallationAuthorization.
VerifyInstallation(ctx context.Context, payload, signature string) error
// WithExecutorCredentials runs apply in a transaction of the tenant.
WithExecutorCredentials(ctx context.Context, tenant string, apply func(context.Context, ExecutorCredentialTx) error) error
diff --git a/services/core/internal/sessions/model_execution.go b/services/core/internal/sessions/model_execution.go
index 632ac47bb..9cf88a726 100644
--- a/services/core/internal/sessions/model_execution.go
+++ b/services/core/internal/sessions/model_execution.go
@@ -9,8 +9,7 @@ import (
// ModelExecutionReader reads the model execution a Session froze at creation.
type ModelExecutionReader interface {
// SessionModelExecution opens the model provider the tenant's Session
- // froze at creation. A Session without one is ErrNotFound, and a service
- // without the credential key credentialcrypto.ErrUnavailable; a frozen
+ // froze at creation. A Session without one is ErrNotFound; a frozen
// provider that does not open, decode or validate is an internal error.
SessionModelExecution(ctx context.Context, tenant, session string) (*v1.ModelProviderInput, error)
}
diff --git a/services/core/internal/vaults/service.go b/services/core/internal/vaults/service.go
index d314066f4..6143432ea 100644
--- a/services/core/internal/vaults/service.go
+++ b/services/core/internal/vaults/service.go
@@ -18,8 +18,7 @@ import (
const oauthRefreshTimeout = 20 * time.Second
// Service runs the Vault and Credential operations. Storage seals and opens
-// the secrets; without a credential key, operations that need a secret return
-// credentialcrypto.ErrUnavailable and the others keep working.
+// the secrets.
type Service struct {
storage Storage
refresher oauthrefresh.Refresher
diff --git a/services/core/internal/vaults/service_test.go b/services/core/internal/vaults/service_test.go
index 4b0e49d97..28f5d952f 100644
--- a/services/core/internal/vaults/service_test.go
+++ b/services/core/internal/vaults/service_test.go
@@ -10,7 +10,6 @@ import (
"github.com/google/uuid"
- "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/oauthrefresh"
)
@@ -163,6 +162,9 @@ func (f refreshFunc) Refresh(ctx context.Context, request oauthrefresh.Request)
return f(ctx, request)
}
+// errStorage is a storage failure the Service returns unchanged.
+var errStorage = errors.New("storage failed")
+
func unexpectedRefresh(t testing.TB) refreshFunc {
return func(context.Context, oauthrefresh.Request) (oauthrefresh.Token, error) {
t.Fatal("unexpected call to Refresh")
@@ -236,9 +238,9 @@ func TestCredentialCreationValidationOrder(t *testing.T) {
if credential.VaultID != "not-a-vault" {
t.Fatalf("%s creation changed the Vault ID %q", kind, credential.VaultID)
}
- return Credential{}, credentialcrypto.ErrUnavailable
+ return Credential{}, errStorage
}}, unexpectedRefresh(t))
- if err := run(service, "valid", "not-a-vault"); !errors.Is(err, credentialcrypto.ErrUnavailable) {
+ if err := run(service, "valid", "not-a-vault"); !errors.Is(err, errStorage) {
t.Fatalf("%s creation: got %v", kind, err)
}
}
@@ -295,9 +297,9 @@ func TestUpdateStaticCredential(t *testing.T) {
if replacement != (StaticTokenReplacement{CredentialKey: CredentialKey{tenant, vault, id}, MCPServerURL: url, Token: "replacement"}) {
t.Fatalf("unexpected replacement %+v", replacement)
}
- return Credential{}, credentialcrypto.ErrUnavailable
+ return Credential{}, errStorage
}}, unexpectedRefresh(t))
- if _, err := service.UpdateStaticCredential(t.Context(), command); !errors.Is(err, credentialcrypto.ErrUnavailable) {
+ if _, err := service.UpdateStaticCredential(t.Context(), command); !errors.Is(err, errStorage) {
t.Fatal("the storage result was not returned", err)
}
}
@@ -348,8 +350,8 @@ func TestStaticBearerToken(t *testing.T) {
if token, err := testService(t, &fakeStorage{staticToken: lookup("private-token", nil)}, unexpectedRefresh(t)).MCPBearerToken(t.Context(), command); err != nil || token != "private-token" {
t.Fatal("static token was not returned", err)
}
- if token, err := testService(t, &fakeStorage{staticToken: lookup("", credentialcrypto.ErrUnavailable)}, unexpectedRefresh(t)).MCPBearerToken(t.Context(), command); !errors.Is(err, credentialcrypto.ErrUnavailable) || token != "" {
- t.Fatal("a keyless lookup returned a token", err)
+ if token, err := testService(t, &fakeStorage{staticToken: lookup("", errStorage)}, unexpectedRefresh(t)).MCPBearerToken(t.Context(), command); !errors.Is(err, errStorage) || token != "" {
+ t.Fatal("a failed lookup returned a token", err)
}
unscoped := command
unscoped.Binding.CredentialID = "not-a-credential"
@@ -468,10 +470,10 @@ func TestOAuthBearerTokenFailuresReturnNoToken(t *testing.T) {
storage := scenario.service.storage.(*fakeStorage)
storage.withOAuthCredential = func(ctx context.Context, _ CredentialKey, apply func(OAuthTx) error) error {
return apply(&fakeOAuthTx{t: t, load: func(context.Context, string) (OAuthGrant, error) {
- return OAuthGrant{}, credentialcrypto.ErrUnavailable
+ return OAuthGrant{}, errStorage
}})
}
- if token, err := scenario.service.MCPBearerToken(t.Context(), scenario.command); !errors.Is(err, credentialcrypto.ErrUnavailable) || token != "" {
+ if token, err := scenario.service.MCPBearerToken(t.Context(), scenario.command); !errors.Is(err, errStorage) || token != "" {
t.Fatal("a failed load was not returned unchanged", err)
}
})
diff --git a/services/core/internal/vaults/storage.go b/services/core/internal/vaults/storage.go
index 1598bd12e..af97af86c 100644
--- a/services/core/internal/vaults/storage.go
+++ b/services/core/internal/vaults/storage.go
@@ -28,12 +28,10 @@ type Storage interface {
// DeleteVault deletes a Vault with all of its Credentials and returns its ID.
DeleteVault(ctx context.Context, tenantID, vaultID string) (string, error)
// CreateCredential seals the Credential's secret and stores it in a Vault
- // of the tenant. A missing credential key is
- // credentialcrypto.ErrUnavailable, checked after the new Credential ID and
- // before the Vault.
+ // of the tenant.
CreateCredential(ctx context.Context, credential NewCredential) (Credential, error)
// ReplaceStaticToken seals and replaces a static_bearer Credential's
- // token. Without a credential key it is credentialcrypto.ErrUnavailable.
+ // token.
ReplaceStaticToken(ctx context.Context, replacement StaticTokenReplacement) (Credential, error)
// DeleteCredential deletes a Credential and its sealed secret and returns its ID.
DeleteCredential(ctx context.Context, key CredentialKey) (string, error)
@@ -48,8 +46,7 @@ type Storage interface {
// Vaults that the query selects, ordered by ID.
FindMCPCredentials(ctx context.Context, query MCPCredentialQuery) ([]MCPCredentialMatch, error)
// StaticToken opens a static_bearer Credential's token when the complete
- // frozen scope still names it. A scope that names none is ErrNotFound,
- // then a missing credential key is credentialcrypto.ErrUnavailable.
+ // frozen scope still names it. A scope that names none is ErrNotFound.
StaticToken(ctx context.Context, query StaticTokenQuery) (string, error)
}
@@ -59,9 +56,8 @@ type OAuthTx interface {
// LoadOAuthGrant locks the Credential until the transaction ends, so
// competing refreshes, replacements and deletions, including the parent
// Vault's, wait. A non-empty destination must match the stored one, or
- // the Credential is ErrNotFound. Only then is the grant opened: a missing
- // credential key is credentialcrypto.ErrUnavailable, and stored metadata
- // that differs from the sealed copy fails authentication.
+ // the Credential is ErrNotFound. Only then is the grant opened: stored
+ // metadata that differs from the sealed copy fails authentication.
LoadOAuthGrant(ctx context.Context, destination string) (OAuthGrant, error)
// ApplyOAuthRefresh seals and stores a refreshed grant for the loaded
// Credential. Execution refreshes are not caller writes and record no
diff --git a/services/core/tests/container_server.py b/services/core/tests/container_server.py
index 072a6a557..292bf72b4 100755
--- a/services/core/tests/container_server.py
+++ b/services/core/tests/container_server.py
@@ -3,22 +3,20 @@
import os
-# Match ownership of the suite's private Core key digest file without granting root access.
+# Match ownership of the suite's private installation files without granting root access.
# The image's default UID is separately exercised by deployment acceptance.
assert os.getuid() != 0, "Run container acceptance as an unprivileged host user"
-core_key_digests = os.environ["OAC_CORE_KEY_DIGESTS_FILE"]
args = [
"docker", "run", "--rm", "--read-only", "--network=host",
"--cap-drop=ALL", "--security-opt=no-new-privileges",
"--user", f"{os.getuid()}:{os.getgid()}",
- "--mount", f"type=bind,source={core_key_digests},target=/run/core-key-digests.json,readonly",
- "--env", "OAC_CORE_KEY_DIGESTS_FILE=/run/core-key-digests.json",
]
-credential_key = os.environ.get("OAC_CREDENTIAL_KEY_FILE")
-if credential_key:
+for name, target in (("OAC_CORE_KEY_DIGESTS_FILE", "/run/core-key-digests.json"),
+ ("OAC_CREDENTIAL_KEY_FILE", "/run/credential.key"),
+ ("OAC_INSTALLATION_ID_FILE", "/run/installation.id")):
args.extend([
- "--mount", f"type=bind,source={credential_key},target=/run/credential.key,readonly",
- "--env", "OAC_CREDENTIAL_KEY_FILE=/run/credential.key",
+ "--mount", f"type=bind,source={os.environ[name]},target={target},readonly",
+ "--env", f"{name}={target}",
])
for name in ("OAC_DATABASE_URL", "OAC_ADDR", "OAC_DEFAULT_HARNESS", "OAC_PUBLIC_URL"):
args.extend(["--env", name])
diff --git a/services/core/tests/integration/agent_execution_defaults_http_test.go b/services/core/tests/integration/agent_execution_defaults_http_test.go
index 28f587c6c..fe4fa7db8 100644
--- a/services/core/tests/integration/agent_execution_defaults_http_test.go
+++ b/services/core/tests/integration/agent_execution_defaults_http_test.go
@@ -23,7 +23,7 @@ func TestAgentExecutionDefaultsPublicSnapshotAndPrecedence(t *testing.T) {
auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "defaults-test", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}})
deployment := &v1.ModelProviderInput{Protocol: "responses", BaseURL: "https://deployment.example/v1", APIKey: "deployment-canary"}
defaultsCalls := 0
- handler, err := publicHandler(t, st, auth, "codex", storeExecution(t, st), managedSandboxes(t, st), withHarnesses([]string{"codex", "claude_sdk", "mcode"}), modelProviderDefaults(func(context.Context, string) (*modelconfiguration.Snapshot, error) {
+ handler, err := publicHandler(t, st, auth, "codex", withHarnesses([]string{"codex", "claude_sdk", "mcode"}), modelProviderDefaults(func(context.Context, string) (*modelconfiguration.Snapshot, error) {
defaultsCalls++
copy := *deployment
return &modelconfiguration.Snapshot{Model: "fixture", Provider: ©, Revision: uuid.New()}, nil
diff --git a/services/core/tests/integration/agents_delete_public_test.go b/services/core/tests/integration/agents_delete_public_test.go
index 0f37041d4..187c2f139 100644
--- a/services/core/tests/integration/agents_delete_public_test.go
+++ b/services/core/tests/integration/agents_delete_public_test.go
@@ -23,14 +23,14 @@ func TestAgentDeletionOfficialClient(t *testing.T) {
{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()},
{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()},
})
- h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s))
+ h, err := publicHandler(t, s, auth, "codex")
if err != nil {
t.Fatal(err)
}
server := httptest.NewServer(h)
defer server.Close()
- recoveredStore := New(s.pool)
- h, err = publicHandler(t, recoveredStore, auth, "codex", storeExecution(t, recoveredStore))
+ recoveredStore := New(t, s.pool)
+ h, err = publicHandler(t, recoveredStore, auth, "codex")
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/agents_update_public_test.go b/services/core/tests/integration/agents_update_public_test.go
index 651a044b1..74ed7a6fc 100644
--- a/services/core/tests/integration/agents_update_public_test.go
+++ b/services/core/tests/integration/agents_update_public_test.go
@@ -23,14 +23,14 @@ func TestAgentUpdateOfficialClient(t *testing.T) {
{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()},
{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()},
})
- h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s))
+ h, err := publicHandler(t, s, auth, "codex")
if err != nil {
t.Fatal(err)
}
server := httptest.NewServer(h)
defer server.Close()
- recoveredStore := New(s.pool)
- h, err = publicHandler(t, recoveredStore, auth, "codex", storeExecution(t, recoveredStore))
+ recoveredStore := New(t, s.pool)
+ h, err = publicHandler(t, recoveredStore, auth, "codex")
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/command_output_test.go b/services/core/tests/integration/command_output_test.go
index 647d05650..df69ac1fe 100644
--- a/services/core/tests/integration/command_output_test.go
+++ b/services/core/tests/integration/command_output_test.go
@@ -73,7 +73,7 @@ func TestCommandOutputCommitsFragmentsSnapshotsAndRecovery(t *testing.T) {
t.Fatal(err)
}
// Reopening the Store recovers committed Items without creating events.
- reopened := New(pool)
+ reopened := New(t, pool)
before, _ = sessionAdapter(s).SessionEventCursor(ctx, tenant, session.ID)
page, err = sessionAdapter(s).ListItems(ctx, tenant, session.ID, "", 100, true)
if err != nil || len(page.Items) != 3 {
diff --git a/services/core/tests/integration/configuration_validation_public_test.go b/services/core/tests/integration/configuration_validation_public_test.go
index e1bbe8f26..a04885f0d 100644
--- a/services/core/tests/integration/configuration_validation_public_test.go
+++ b/services/core/tests/integration/configuration_validation_public_test.go
@@ -30,7 +30,7 @@ func TestAgentConfigurationValidationRejectsWithoutWritesPostgres(t *testing.T)
{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "config-owner", TokenSHA256: runtimedevice.HashCredential(owner), TenantID: ownerTenant},
{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "config-foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()},
})
- h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s))
+ h, err := publicHandler(t, s, auth, "codex")
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/creation_stream_settlement_public_test.go b/services/core/tests/integration/creation_stream_settlement_public_test.go
index 954300f88..bfa7d7f30 100644
--- a/services/core/tests/integration/creation_stream_settlement_public_test.go
+++ b/services/core/tests/integration/creation_stream_settlement_public_test.go
@@ -113,10 +113,10 @@ func (s sseLines) open(t *testing.T) {
// creation stream whose initial reservation is cancelled without a Session event
// ends through the committed projection, while GET stays open.
func TestCreationStreamPublicLifetimes(t *testing.T) {
- s, _ := NewModelTestStore(t)
+ s, _ := testStore(t)
tenant, token := uuid.NewString(), uuid.NewString()
auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: tenant, SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}})
- handler, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), executorURL("https://offline-executor.example"))
+ handler, err := publicHandler(t, s, auth, "codex", executorURL("https://offline-executor.example"))
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/credential_matrix_http_test.go b/services/core/tests/integration/credential_matrix_http_test.go
index 068423282..96e630f29 100644
--- a/services/core/tests/integration/credential_matrix_http_test.go
+++ b/services/core/tests/integration/credential_matrix_http_test.go
@@ -36,7 +36,7 @@ func TestCredentialNamespaceMatrix(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- handler, err := publicHandler(t, s, nil, "codex", storeKeys(s), storeExecution(t, s), managedSandboxes(t, s), withCoreKeys(admin))
+ handler, err := publicHandler(t, s, nil, "codex", storeKeys(s), withCoreKeys(admin))
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/deployment_model_providers_http_test.go b/services/core/tests/integration/deployment_model_providers_http_test.go
index 8dc8687af..be9c35db1 100644
--- a/services/core/tests/integration/deployment_model_providers_http_test.go
+++ b/services/core/tests/integration/deployment_model_providers_http_test.go
@@ -36,7 +36,7 @@ func TestDeploymentModelProvidersHTTP(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- handler, err := publicHandler(t, st, auth, "codex", storeExecution(t, st), managedSandboxes(t, st), withCoreKeys(admin), withHarnesses([]string{"codex", "mcode"}))
+ handler, err := publicHandler(t, st, auth, "codex", withCoreKeys(admin), withHarnesses([]string{"codex", "mcode"}))
if err != nil {
t.Fatal(err)
}
@@ -170,8 +170,8 @@ func TestDeploymentModelProvidersHTTP(t *testing.T) {
t.Fatal(err)
}
incompatible := call("POST", "/v1/agents/sessions", projectKey, hosted, 400)
- if !strings.Contains(string(incompatible["error"]), "does not support this model provider protocol") || strings.Contains(string(incompatible["error"]), "credential_storage_unavailable") {
- t.Fatal("unsupported stored protocol was reported as a credential failure")
+ if !strings.Contains(string(incompatible["error"]), "does not support this model provider protocol") {
+ t.Fatal("unsupported stored protocol was not reported as such")
}
if text(providerView(call("GET", path, coreKey, "", 200))["protocol"]) != "anthropic" {
t.Fatal("unsupported default was rewritten")
@@ -229,13 +229,13 @@ func TestDeploymentModelProvidersHTTP(t *testing.T) {
// recorded first: a same-key retry returns the committed Session after the
// default was replaced or removed.
func TestNoneSessionRetryAfterDeploymentDefaultChanges(t *testing.T) {
- st, _ := NewModelTestStore(t)
+ st, _ := testStore(t)
if _, err := st.pool.Exec(t.Context(), "DELETE FROM deployment_model_providers"); err != nil {
t.Fatal(err)
}
tenant, token := uuid.NewString(), uuid.NewString()
auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "none-retry", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}})
- handler, err := publicHandler(t, st, auth, "codex", storeExecution(t, st))
+ handler, err := publicHandler(t, st, auth, "codex")
if err != nil {
t.Fatal(err)
}
@@ -321,7 +321,7 @@ func TestDeploymentProviderResolutionPairsRevisionDuringReplacement(t *testing.T
_, err = defaults.Replace(admin, modelconfiguration.Replacement{Harness: harness, Configuration: v1.ModelConfigurationInput{ModelProvider: replacement, Model: "fixture"}})
return snapshot, err
}
- handler, err := publicHandler(t, st, auth, "codex", storeExecution(t, st), modelProviderDefaults(resolver))
+ handler, err := publicHandler(t, st, auth, "codex", modelProviderDefaults(resolver))
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/dispatch_test.go b/services/core/tests/integration/dispatch_test.go
index 7e733aad4..2f9ab27f3 100644
--- a/services/core/tests/integration/dispatch_test.go
+++ b/services/core/tests/integration/dispatch_test.go
@@ -50,7 +50,7 @@ func newDispatchHarness(t *testing.T) *dispatchHarness {
func newDispatchHarnessForSession(t *testing.T, configuration []byte, local bool) *dispatchHarness {
t.Helper()
- s, _ := NewModelTestStore(t)
+ s, _ := testStore(t)
h := &dispatchHarness{t: t, s: s, tenant: uuid.NewString(), environments: map[string]*dispatchHarness{}}
ctx := context.Background()
var err error
@@ -74,7 +74,7 @@ func newDispatchHarnessForSession(t *testing.T, configuration []byte, local bool
if getErr != nil {
t.Fatal(getErr)
}
- h.device, err = FixtureEnvironmentDevice(ctx, s.pool, h.tenant, environment.ID, "local runtime", runtimedevice.HashCredential(secret))
+ h.device, err = FixtureEnvironmentDevice(t, ctx, s.pool, h.tenant, environment.ID, "local runtime", runtimedevice.HashCredential(secret))
} else {
h.device, err = sessionService(t, s).CreateDevice(ctx, h.tenant, "isolated executor", runtimedevice.HashCredential(secret))
}
diff --git a/services/core/tests/integration/environment_file_writes_test.go b/services/core/tests/integration/environment_file_writes_test.go
index 2c4f598d3..276e346ad 100644
--- a/services/core/tests/integration/environment_file_writes_test.go
+++ b/services/core/tests/integration/environment_file_writes_test.go
@@ -28,7 +28,7 @@ func newFileWriteFixture(t *testing.T) fileWriteFixture {
lease := executionWriter(t, s).lease
tenant := uuid.NewString()
session, env := localEnvironment(t, s, tenant)
- host, err := FixtureEnvironmentDevice(t.Context(), pool, tenant, env.ID, "file owner", runtimedevice.HashCredential(uuid.NewString()))
+ host, err := FixtureEnvironmentDevice(t, t.Context(), pool, tenant, env.ID, "file owner", runtimedevice.HashCredential(uuid.NewString()))
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/environment_initial_public_test.go b/services/core/tests/integration/environment_initial_public_test.go
index 30559672c..a034328e6 100644
--- a/services/core/tests/integration/environment_initial_public_test.go
+++ b/services/core/tests/integration/environment_initial_public_test.go
@@ -44,7 +44,7 @@ func TestEnvironmentInitialFailureOfficialClient(t *testing.T) {
t.Error(err)
}
})
- handler, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), executorURL("https://executor.example"))
+ handler, err := publicHandler(t, s, auth, "codex", executorURL("https://executor.example"))
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/environment_retrieve_public_test.go b/services/core/tests/integration/environment_retrieve_public_test.go
index 2169efcef..79e87ba7a 100644
--- a/services/core/tests/integration/environment_retrieve_public_test.go
+++ b/services/core/tests/integration/environment_retrieve_public_test.go
@@ -21,7 +21,7 @@ func TestEnvironmentRetrievalOfficialClient(t *testing.T) {
if python == "" {
t.Skip("pinned official Python SDK required")
}
- s, _ := NewModelTestStore(t)
+ s, _ := testStore(t)
tenant, foreignTenant := uuid.NewString(), uuid.NewString()
principal := FixtureExecutorPrincipal(t, s, tenant)
token, peer, foreign := uuid.NewString(), uuid.NewString(), uuid.NewString()
@@ -47,7 +47,7 @@ func TestEnvironmentRetrievalOfficialClient(t *testing.T) {
}
}
}()
- handler, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), executorURL("https://private-registry.example"))
+ handler, err := publicHandler(t, s, auth, "codex", executorURL("https://private-registry.example"))
if err != nil {
t.Fatal(err)
}
@@ -85,7 +85,7 @@ func TestEnvironmentRetrievalOfficialClient(t *testing.T) {
revoked = true
server.Close()
s.pool.Close()
- reopened, _ := NewModelTestStore(t)
+ reopened, _ := testStore(t)
handler, err = publicHandler(t, reopened, auth, "codex")
if err != nil {
t.Fatal(err)
diff --git a/services/core/tests/integration/executor_principals_migration_test.go b/services/core/tests/integration/executor_principals_migration_test.go
index 808e69a2a..4157d1422 100644
--- a/services/core/tests/integration/executor_principals_migration_test.go
+++ b/services/core/tests/integration/executor_principals_migration_test.go
@@ -103,7 +103,7 @@ func TestExecutorPrincipalMigrationRetiresUnknownAuthority(t *testing.T) {
t.Fatal(err)
}
t.Cleanup(migrated.Close)
- s := New(migrated)
+ s := New(t, migrated)
p := FixtureExecutorPrincipal(t, s, tenant)
credentials := sessionService(t, s)
if _, err := sessionAdapter(s).AuthenticateEnvironmentExecutor(ctx, environment, digest); !errors.Is(err, sessions.ErrNotFound) {
diff --git a/services/core/tests/integration/fixtures_test.go b/services/core/tests/integration/fixtures_test.go
index 6fecfab2c..bf530806d 100644
--- a/services/core/tests/integration/fixtures_test.go
+++ b/services/core/tests/integration/fixtures_test.go
@@ -1,7 +1,6 @@
package integration
import (
- "bytes"
"context"
"encoding/json"
"errors"
@@ -9,7 +8,6 @@ import (
"testing"
v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1"
- "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit"
@@ -20,15 +18,6 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
)
-var fixtureCipher, _ = credentialcrypto.New(bytes.Repeat([]byte{61}, 32))
-
-// NewModelTestStore is testStore with a fixture credential key, so Sessions
-// can freeze a model provider.
-func NewModelTestStore(t *testing.T) (*Store, *pgxpool.Pool) {
- _, pool := testStore(t)
- return NewWithCredentialCipher(pool, fixtureCipher), pool
-}
-
// FixtureModelProvider is a valid bundle for the harness. Hosted and self-hosted
// Sessions cannot run without one, so fixtures supply it instead of relaxing
// that check.
diff --git a/services/core/tests/integration/function_inputs_public_test.go b/services/core/tests/integration/function_inputs_public_test.go
index 5b512cffe..842e27263 100644
--- a/services/core/tests/integration/function_inputs_public_test.go
+++ b/services/core/tests/integration/function_inputs_public_test.go
@@ -47,7 +47,7 @@ func TestFunctionInputsOfficialClientAtomicAdmission(t *testing.T) {
}
}
auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}, {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()}})
- handler, err := publicHandler(t, s, auth, "codex", storeExecution(t, s))
+ handler, err := publicHandler(t, s, auth, "codex")
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/initial_files_http_test.go b/services/core/tests/integration/initial_files_http_test.go
index 9fee427b4..25d67b80a 100644
--- a/services/core/tests/integration/initial_files_http_test.go
+++ b/services/core/tests/integration/initial_files_http_test.go
@@ -23,7 +23,7 @@ func TestInitialFilesHTTPInlineLimitsAndRetry(t *testing.T) {
tenant, token := uuid.NewString(), uuid.NewString()
auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}})
// Exercise HTTP parsing and durable storage without starting a Runtime.
- handler, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), managedSandboxes(t, s), fixtureDeploymentProvider())
+ handler, err := publicHandler(t, s, auth, "codex", fixtureDeploymentProvider())
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/input_conflicts_public_test.go b/services/core/tests/integration/input_conflicts_public_test.go
index f390b0419..7ec79a89b 100644
--- a/services/core/tests/integration/input_conflicts_public_test.go
+++ b/services/core/tests/integration/input_conflicts_public_test.go
@@ -44,7 +44,7 @@ func TestSessionInputConflictsAndResultTargetsPostgres(t *testing.T) {
{OrganizationID: "test-org", ProjectID: tenant, SubjectKind: "service_account", SubjectID: "conflict-owner", TokenSHA256: runtimedevice.HashCredential(owner), TenantID: tenant},
{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "conflict-foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()},
})
- h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), executorURL("https://executor.example"))
+ h, err := publicHandler(t, s, auth, "codex", executorURL("https://executor.example"))
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/item_order_migration_test.go b/services/core/tests/integration/item_order_migration_test.go
index 31b3abf22..f47deb1d8 100644
--- a/services/core/tests/integration/item_order_migration_test.go
+++ b/services/core/tests/integration/item_order_migration_test.go
@@ -148,11 +148,11 @@ func TestItemOrderMigrationPreservesIndexedHistory(t *testing.T) {
t.Fatal(err)
}
t.Cleanup(migratedPool.Close)
- s := New(migratedPool)
+ s := New(t, migratedPool)
if _, err = transitionTurn(ctx, s, tenant, session, turn, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil {
t.Fatal(err)
}
- if err = executionOwner(t, New(migratedPool)).Sessions.AppendTurnEvents(ctx, tenant, session, turn, 1, []sessions.ExecutionEvent{{Kind: "delta", Payload: json.RawMessage(`{"item_id":"after-upgrade","delta":"continued"}`)}}); err != nil {
+ if err = executionOwner(t, New(t, migratedPool)).Sessions.AppendTurnEvents(ctx, tenant, session, turn, 1, []sessions.ExecutionEvent{{Kind: "delta", Payload: json.RawMessage(`{"item_id":"after-upgrade","delta":"continued"}`)}}); err != nil {
t.Fatal(err)
}
addedID := items.Identity(turn, "message:after-upgrade")
diff --git a/services/core/tests/integration/list_cursor_public_test.go b/services/core/tests/integration/list_cursor_public_test.go
index 9cf08ae1a..9dadd476f 100644
--- a/services/core/tests/integration/list_cursor_public_test.go
+++ b/services/core/tests/integration/list_cursor_public_test.go
@@ -233,7 +233,7 @@ func TestListCursorErrorsPostgres(t *testing.T) {
{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "cursor-owner", TokenSHA256: runtimedevice.HashCredential(owner), TenantID: ownerTenant},
{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "cursor-foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: foreignTenant},
})
- h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s))
+ h, err := publicHandler(t, s, auth, "codex")
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/local_environment_devices_test.go b/services/core/tests/integration/local_environment_devices_test.go
index 88a04c240..24f24c486 100644
--- a/services/core/tests/integration/local_environment_devices_test.go
+++ b/services/core/tests/integration/local_environment_devices_test.go
@@ -34,10 +34,10 @@ func TestEnvironmentDeviceAuthorityAndLifecycle(t *testing.T) {
sibling, _ := localEnvironment(t, s, tenant)
foreign, _ := localEnvironment(t, s, foreignTenant)
digest := runtimedevice.HashCredential(uuid.NewString())
- if _, err := FixtureEnvironmentDevice(t.Context(), pool, foreignTenant, environment.ID, "foreign", digest); !errors.Is(err, sessions.ErrNotFound) {
+ if _, err := FixtureEnvironmentDevice(t, t.Context(), pool, foreignTenant, environment.ID, "foreign", digest); !errors.Is(err, sessions.ErrNotFound) {
t.Fatalf("foreign provisioning: %v", err)
}
- bound, err := FixtureEnvironmentDevice(t.Context(), pool, tenant, environment.ID, "dedicated", digest)
+ bound, err := FixtureEnvironmentDevice(t, t.Context(), pool, tenant, environment.ID, "dedicated", digest)
if err != nil || bound.EnvironmentID != environment.ID {
t.Fatalf("provision: %+v %v", bound, err)
}
@@ -81,7 +81,7 @@ func TestEnvironmentDeviceProvisioningHasOneWinner(t *testing.T) {
wg.Add(1)
go func() {
defer wg.Done()
- _, err := FixtureEnvironmentDevice(t.Context(), pool, tenant, environment.ID, "runtime", runtimedevice.HashCredential(uuid.NewString()))
+ _, err := FixtureEnvironmentDevice(t, t.Context(), pool, tenant, environment.ID, "runtime", runtimedevice.HashCredential(uuid.NewString()))
results <- err
}()
}
@@ -105,7 +105,7 @@ func TestEnvironmentDeviceProvisioningHasOneWinner(t *testing.T) {
if err := sessionService(t, s).RevokeDevice(t.Context(), tenant, bound.ID); err != nil {
t.Fatal(err)
}
- if _, err := FixtureEnvironmentDevice(t.Context(), pool, tenant, environment.ID, "replacement", runtimedevice.HashCredential(uuid.NewString())); !errors.Is(err, sessions.ErrDeviceBindingConflict) {
+ if _, err := FixtureEnvironmentDevice(t, t.Context(), pool, tenant, environment.ID, "replacement", runtimedevice.HashCredential(uuid.NewString())); !errors.Is(err, sessions.ErrDeviceBindingConflict) {
t.Fatalf("silent placement replacement: %v", err)
}
}
diff --git a/services/core/tests/integration/managed_fixture_test.go b/services/core/tests/integration/managed_fixture_test.go
index 27cac8f77..9248f7d2b 100644
--- a/services/core/tests/integration/managed_fixture_test.go
+++ b/services/core/tests/integration/managed_fixture_test.go
@@ -13,6 +13,5 @@ import (
func newManagedTestStore(t *testing.T) (*Store, *pgxpool.Pool) {
t.Helper()
pool := pgtest.OpenIsolated(t, nil)
- // Hosted Sessions freeze a model provider, which needs a credential key.
- return NewWithCredentialCipher(pool, fixtureCipher), pool
+ return New(t, pool), pool
}
diff --git a/services/core/tests/integration/mcp_credential_selection_public_test.go b/services/core/tests/integration/mcp_credential_selection_public_test.go
index b047c7608..407bbcafb 100644
--- a/services/core/tests/integration/mcp_credential_selection_public_test.go
+++ b/services/core/tests/integration/mcp_credential_selection_public_test.go
@@ -36,7 +36,7 @@ func TestMCPCredentialSelectionPublicPostgres(t *testing.T) {
{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "selection-a", TokenSHA256: runtimedevice.HashCredential(tokenA), TenantID: tenantA},
{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "selection-b", TokenSHA256: runtimedevice.HashCredential(tokenB), TenantID: uuid.NewString()},
})
- h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s))
+ h, err := publicHandler(t, s, auth, "codex")
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/path_id_semantics_public_test.go b/services/core/tests/integration/path_id_semantics_public_test.go
index ebb0b24df..288df95a7 100644
--- a/services/core/tests/integration/path_id_semantics_public_test.go
+++ b/services/core/tests/integration/path_id_semantics_public_test.go
@@ -98,7 +98,7 @@ func TestMalformedPathIDsMatchMissingPostgres(t *testing.T) {
{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "path-owner", TokenSHA256: runtimedevice.HashCredential(owner), TenantID: ownerTenant},
{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "path-foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()},
})
- h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s))
+ h, err := publicHandler(t, s, auth, "codex")
if err != nil {
t.Fatal(err)
}
@@ -359,28 +359,6 @@ func TestMalformedPathIDsMatchMissingPostgres(t *testing.T) {
t.Fatalf("route matrix checked only %d cases", checked)
}
- // Storage availability checks also run before the lookup of a missing identifier.
- h, err = publicHandler(t, New(pool), auth, "codex")
- if err != nil {
- t.Fatal(err)
- }
- unconfigured := httptest.NewServer(h)
- defer unconfigured.Close()
- keyless := pathIDClient{t: t, server: unconfigured}
- for _, r := range []route{
- {method: "POST", body: `{"name":"path","auth":{"type":"static_bearer","mcp_server_url":"https://mcp.example/mcp","token":"t"}}`, segments: []string{"/v1/vaults/", vault, "/credentials"}},
- {method: "POST", body: `{"auth":{"type":"static_bearer","token":"t"}}`, segments: []string{"/v1/vaults/", vault, "/credentials/", credential}},
- {method: "POST", body: `{"env":{"PATH_ID":"value"}}`, segments: []string{"/v1/agents/environments/templates/", template}},
- } {
- for index := 1; index < len(r.segments); index += 2 {
- wantStatus, wantBody := keyless.do(owner, r.method, path(r.segments, index, uuid.NewString()), "application/json", []byte(r.body))
- status, body := keyless.do(owner, r.method, path(r.segments, index, "not-a-uuid"), "application/json", []byte(r.body))
- if status != wantStatus || body != wantBody {
- t.Errorf("keyless %s %s: malformed %d %s; missing %d %s", r.method, path(r.segments, index, "{id}"), status, body, wantStatus, wantBody)
- }
- }
- }
-
// A malformed list cursor answers like any other unresolved cursor of that
// list: 404 on lookup-family lists and the family's 400 elsewhere (see
// list_cursor_public_test.go). A malformed parent still answers first.
diff --git a/services/core/tests/integration/public_handler_fixture_test.go b/services/core/tests/integration/public_handler_fixture_test.go
index 382dfec57..008544e7b 100644
--- a/services/core/tests/integration/public_handler_fixture_test.go
+++ b/services/core/tests/integration/public_handler_fixture_test.go
@@ -36,9 +36,11 @@ const testExecutorURL = "wss://core.example/api/v1/agent-daemon/ws"
// publicHandler serves s through api.NewHandler, with every area built on s's
// database, credential key and placement rules as cmd/server builds it. keys
-// authenticate as Project keys and "admin" as the Core key. Metrics, Runtime
-// observation and history, and executor connections are strict stand-ins.
-// Execution and Sandboxes stay disabled unless configure sets them.
+// authenticate as Project keys and "admin" as the Core key. Execution admits
+// Sessions and inputs through the Session service without a Worker, so nothing
+// runs them. Metrics, Runtime observation and history, executor connections,
+// Session archive, workspaces, and deployment changes, reset and discovery
+// are strict stand-ins. configure replaces any of them.
func publicHandler(t testing.TB, s *Store, keys fixtureKeyResolver, engine string, configure ...func(*api.Dependencies)) (http.Handler, error) {
t.Helper()
admin, err := api.NewDeploymentAuthenticator([]string{runtimedevice.HashCredential("admin")})
@@ -74,8 +76,9 @@ func publicHandler(t testing.TB, s *Store, keys fixtureKeyResolver, engine strin
if err != nil {
return nil, err
}
+ deployments := deploymentService(t, s)
deps := api.Dependencies{
- Engine: engine, CoreKeys: admin, InstallationBindings: deploymentService(t, s),
+ Engine: engine, CoreKeys: admin, InstallationBindings: deployments,
Projects: projectService, ProjectsReader: fixtureProjectsReader{Reader: projectStore, keys: keys},
ModelProviders: modelConfigurationService, ModelProvidersReader: modelConfigurationStore,
Vaults: vaultService, VaultsReader: vaultStore,
@@ -94,6 +97,8 @@ func publicHandler(t testing.TB, s *Store, keys fixtureKeyResolver, engine strin
ArtifactsReader: sessionStore,
SessionAdmin: sessionStore, Environments: service, EnvironmentsReader: sessionStore, Admin: sessionStore, AdminAudit: audit, WriteAudit: audit,
ExecutorConnections: strict, Metrics: strict, RuntimeObservations: strict, RuntimeHistory: strict,
+ Execution: api.Execution{ExecutorURL: testExecutorURL, SessionAdmission: service, InputAdmission: service, SessionArchive: strict, Workspaces: strict},
+ Sandboxes: api.Sandboxes{Deployment: deployments, NodeAllocations: deploymentStore(s), DeploymentChanges: strict, DeploymentReset: strict, ConfigurationDiscovery: strict},
}
for _, c := range configure {
c(&deps)
@@ -143,26 +148,11 @@ func withPolicy(policy execution.Policy) func(*api.Dependencies) {
return func(d *api.Dependencies) { d.Policy = policy }
}
-// storeExecution admits Sessions and inputs through the Session service on s
-// without a Worker, so nothing runs them.
-func storeExecution(t testing.TB, s *Store) func(*api.Dependencies) {
- return func(d *api.Dependencies) {
- service := sessionService(t, s)
- d.Execution = &api.Execution{
- ExecutorURL: testExecutorURL,
- SessionAdmission: service,
- InputAdmission: service,
- SessionArchive: strictStandIn{t},
- Workspaces: strictStandIn{t},
- }
- }
-}
-
// workerExecution runs Sessions through worker. It wires no archive; an
// archive request fails the test.
func workerExecution(t testing.TB, worker *execution.Worker) func(*api.Dependencies) {
return func(d *api.Dependencies) {
- d.Execution = &api.Execution{
+ d.Execution = api.Execution{
ExecutorURL: testExecutorURL,
SessionAdmission: worker,
InputAdmission: worker,
@@ -173,27 +163,11 @@ func workerExecution(t testing.TB, worker *execution.Worker) func(*api.Dependenc
}
// executorURL replaces the daemon URL self-hosted Sessions report. It follows
-// the option that enables Execution.
+// any option that replaces Execution.
func executorURL(url string) func(*api.Dependencies) {
return func(d *api.Dependencies) { d.Execution.ExecutorURL = url }
}
-// managedSandboxes enables the managed sandbox deployment on s: its
-// administration and node routes and openai_hosted Environments. Deployment
-// changes, reset and discovery need the Worker and are strict stand-ins. It
-// follows the option that enables Execution.
-func managedSandboxes(t testing.TB, s *Store) func(*api.Dependencies) {
- return func(d *api.Dependencies) {
- d.Sandboxes = &api.Sandboxes{
- Deployment: deploymentService(t, s),
- NodeAllocations: deploymentStore(s),
- DeploymentChanges: strictStandIn{t},
- DeploymentReset: strictStandIn{t},
- ConfigurationDiscovery: strictStandIn{t},
- }
- }
-}
-
// modelProviderDefaults resolves deployment model provider defaults with
// resolve instead of the stored deployment configuration.
func modelProviderDefaults(resolve func(context.Context, string) (*modelconfiguration.Snapshot, error)) func(*api.Dependencies) {
diff --git a/services/core/tests/integration/remote_mcp_credentials_test.go b/services/core/tests/integration/remote_mcp_credentials_test.go
index 393e50c9b..73c136cc5 100644
--- a/services/core/tests/integration/remote_mcp_credentials_test.go
+++ b/services/core/tests/integration/remote_mcp_credentials_test.go
@@ -16,7 +16,7 @@ func TestSelfHostedServiceMCPRejectionDoesNotRequireCredentialDecryption(t *test
s, tenant, vault, credential := selfHostedMCPAdmissionFixture(t)
switch mode {
case "missing key":
- s = New(s.pool)
+ s = New(t, s.pool)
case "deleted":
_, service, err := fixtureVaults(s)
if err != nil {
diff --git a/services/core/tests/integration/remote_mcp_test.go b/services/core/tests/integration/remote_mcp_test.go
index 0344b0c0a..1bd8d3b94 100644
--- a/services/core/tests/integration/remote_mcp_test.go
+++ b/services/core/tests/integration/remote_mcp_test.go
@@ -99,7 +99,7 @@ func selfHostedMCPAdmissionFixture(t *testing.T) (*Store, string, vaults.Vault,
func selfHostedMCPAdmissionHandler(t *testing.T, s *Store, tenant string) http.Handler {
t.Helper()
auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: tenant, SubjectKind: "service_account", SubjectID: "test", TenantID: tenant, TokenSHA256: runtimedevice.HashCredential("test-token")}})
- handler, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), executorURL("https://executor.example"))
+ handler, err := publicHandler(t, s, auth, "codex", executorURL("https://executor.example"))
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/request_body_public_test.go b/services/core/tests/integration/request_body_public_test.go
index f075d4ce3..2b6a72fe4 100644
--- a/services/core/tests/integration/request_body_public_test.go
+++ b/services/core/tests/integration/request_body_public_test.go
@@ -37,7 +37,7 @@ func TestRequestBodyGateRejectsWithoutWritesPostgres(t *testing.T) {
})
// No Runtime is connected, so a file write that passes the gate is unavailable.
unavailable := func(d *api.Dependencies) { d.Execution.Workspaces = unavailableWorkspaces{strictStandIn{t}} }
- h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), unavailable, acceptUnavailable(t))
+ h, err := publicHandler(t, s, auth, "codex", unavailable, acceptUnavailable(t))
if err != nil {
t.Fatal(err)
}
@@ -173,7 +173,7 @@ func TestRequestBodyGateExcludedRoutesPostgres(t *testing.T) {
s := NewWithCredentialCipher(pool, cipher)
token, tenant := uuid.NewString(), uuid.NewString()
auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "excluded-owner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}})
- h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s))
+ h, err := publicHandler(t, s, auth, "codex")
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/root_fixture_test.go b/services/core/tests/integration/root_fixture_test.go
index 2fde53550..e1ad7512b 100644
--- a/services/core/tests/integration/root_fixture_test.go
+++ b/services/core/tests/integration/root_fixture_test.go
@@ -3,6 +3,7 @@ package integration
import (
"context"
"fmt"
+ "testing"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
@@ -10,6 +11,7 @@ import (
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment/placement"
+ "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/sessionpg"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/providers"
@@ -34,18 +36,16 @@ type Store struct {
// providers and an unset public URL.
var defaultPlacement, _ = placement.NewRules(providers.Builtin(), "")
-// New is the fixture on pool without a credential key, under defaultPlacement.
-func New(pool *pgxpool.Pool) *Store {
- pooled := pgunit.NewPool(pool)
- return &Store{queries: sqlc.New(pool), pool: pool, pooled: pooled, writer: pooled, placement: defaultPlacement}
+// New is the fixture on pool under the shared test credential key and
+// defaultPlacement.
+func New(t testing.TB, pool *pgxpool.Pool) *Store {
+ return NewWithCredentialCipher(pool, pgtest.CredentialKey(t))
}
-// NewWithCredentialCipher is New with a credential key, so Sessions can freeze
-// sealed resources.
+// NewWithCredentialCipher is New under another credential key.
func NewWithCredentialCipher(pool *pgxpool.Pool, cipher *credentialcrypto.Cipher) *Store {
- s := New(pool)
- s.credentialCipher = cipher
- return s
+ pooled := pgunit.NewPool(pool)
+ return &Store{queries: sqlc.New(pool), pool: pool, pooled: pooled, writer: pooled, credentialCipher: cipher, placement: defaultPlacement}
}
// NewExecution is s with its Session transactions on lease.
diff --git a/services/core/tests/integration/sandbox_node_auth_order_http_test.go b/services/core/tests/integration/sandbox_node_auth_order_http_test.go
index 4e152b8a3..338390099 100644
--- a/services/core/tests/integration/sandbox_node_auth_order_http_test.go
+++ b/services/core/tests/integration/sandbox_node_auth_order_http_test.go
@@ -22,7 +22,7 @@ func TestSandboxNodeRoutesAuthenticateBeforeDeploymentState(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- handler, err := publicHandler(t, s, nil, "codex", storeKeys(s), storeExecution(t, s), managedSandboxes(t, s), withCoreKeys(admin))
+ handler, err := publicHandler(t, s, nil, "codex", storeKeys(s), withCoreKeys(admin))
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/saved_web_search_public_test.go b/services/core/tests/integration/saved_web_search_public_test.go
index dd31102ac..a509660f3 100644
--- a/services/core/tests/integration/saved_web_search_public_test.go
+++ b/services/core/tests/integration/saved_web_search_public_test.go
@@ -25,7 +25,7 @@ func TestSavedWebSearchPostgres(t *testing.T) {
{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "search-owner", TokenSHA256: runtimedevice.HashCredential(owner), TenantID: ownerTenant},
{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "search-foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()},
})
- h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s))
+ h, err := publicHandler(t, s, auth, "codex")
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/self_hosted_cancel_public_test.go b/services/core/tests/integration/self_hosted_cancel_public_test.go
index bf9551100..d8fb260a2 100644
--- a/services/core/tests/integration/self_hosted_cancel_public_test.go
+++ b/services/core/tests/integration/self_hosted_cancel_public_test.go
@@ -23,7 +23,7 @@ func TestSelfHostedCancellationOfficialClient(t *testing.T) {
if python == "" {
t.Skip("pinned official Python SDK required")
}
- s, _ := NewModelTestStore(t)
+ s, _ := testStore(t)
tenant, foreignTenant := uuid.NewString(), uuid.NewString()
token, foreign := uuid.NewString(), uuid.NewString()
auth := newTestAuthenticator(t, []testAPIKey{
@@ -178,7 +178,7 @@ func TestSelfHostedCancellationOfficialClient(t *testing.T) {
awaitRelease()
server.Close()
s.pool.Close()
- s, _ = NewModelTestStore(t)
+ s, _ = testStore(t)
server, stop = serve()
settings["base"] = server.URL
}
diff --git a/services/core/tests/integration/self_hosted_initial_public_test.go b/services/core/tests/integration/self_hosted_initial_public_test.go
index be5360247..329d15a6a 100644
--- a/services/core/tests/integration/self_hosted_initial_public_test.go
+++ b/services/core/tests/integration/self_hosted_initial_public_test.go
@@ -30,7 +30,7 @@ func TestSelfHostedInitialCreationOfficialClient(t *testing.T) {
if python == "" {
t.Skip("pinned official Python SDK required")
}
- s, _ := NewModelTestStore(t)
+ s, _ := testStore(t)
tenant, foreignTenant := uuid.NewString(), uuid.NewString()
token, peer, foreign := uuid.NewString(), uuid.NewString(), uuid.NewString()
auth := newTestAuthenticator(t, []testAPIKey{
@@ -47,7 +47,7 @@ func TestSelfHostedInitialCreationOfficialClient(t *testing.T) {
} else {
// Without a Worker, Core keeps its executor URL but admits nothing.
enabled = append(enabled, func(d *api.Dependencies) {
- d.Execution = &api.Execution{
+ d.Execution = api.Execution{
ExecutorURL: origin,
SessionAdmission: unavailableAdmission{},
InputAdmission: unavailableAdmission{},
@@ -155,7 +155,7 @@ func TestSelfHostedInitialCreationOfficialClient(t *testing.T) {
awaitRelease()
server.Close()
s.pool.Close()
- reopened, _ := NewModelTestStore(t)
+ reopened, _ := testStore(t)
worker, stop = publicInitialWorker(t, reopened)
server = serve(reopened, worker)
settings["base"], settings["accepted"] = server.URL, accepted
diff --git a/services/core/tests/integration/session_agent_filter_public_test.go b/services/core/tests/integration/session_agent_filter_public_test.go
index e6239c311..6911adec0 100644
--- a/services/core/tests/integration/session_agent_filter_public_test.go
+++ b/services/core/tests/integration/session_agent_filter_public_test.go
@@ -23,14 +23,14 @@ func TestSessionAgentFilterOfficialClient(t *testing.T) {
{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()},
{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()},
})
- h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s))
+ h, err := publicHandler(t, s, auth, "codex")
if err != nil {
t.Fatal(err)
}
server := httptest.NewServer(h)
defer server.Close()
- recoveredStore := New(s.pool)
- h, err = publicHandler(t, recoveredStore, auth, "codex", storeExecution(t, recoveredStore))
+ recoveredStore := New(t, s.pool)
+ h, err = publicHandler(t, recoveredStore, auth, "codex")
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/session_artifacts_test.go b/services/core/tests/integration/session_artifacts_test.go
index 6db473180..a998c8da7 100644
--- a/services/core/tests/integration/session_artifacts_test.go
+++ b/services/core/tests/integration/session_artifacts_test.go
@@ -156,7 +156,7 @@ func testSessionArtifactsPublishVersionScopeAndLifetime(t *testing.T, kind strin
t.Fatal(err)
}
for _, a := range page.Artifacts {
- if err := sessionAdapter(New(pool)).ReadSessionArtifact(t.Context(), tenant, session, a.ID, func(meta sessions.Artifact, r io.Reader) error {
+ if err := sessionAdapter(New(t, pool)).ReadSessionArtifact(t.Context(), tenant, session, a.ID, func(meta sessions.Artifact, r io.Reader) error {
if err := sessionAdapter(s).DeleteSessionArtifact(t.Context(), tenant, session, a.ID); err != nil {
return err
}
diff --git a/services/core/tests/integration/session_deletion_lifecycle_public_test.go b/services/core/tests/integration/session_deletion_lifecycle_public_test.go
index 440acbe68..112f33ff9 100644
--- a/services/core/tests/integration/session_deletion_lifecycle_public_test.go
+++ b/services/core/tests/integration/session_deletion_lifecycle_public_test.go
@@ -31,7 +31,7 @@ func TestSessionDeletionLifecyclePostgres(t *testing.T) {
{OrganizationID: "test-org", ProjectID: tenant, SubjectKind: "service_account", SubjectID: "deletion-owner", TokenSHA256: runtimedevice.HashCredential(owner), TenantID: tenant},
{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "deletion-foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()},
})
- h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s), executorURL("https://executor.example"))
+ h, err := publicHandler(t, s, auth, "codex", executorURL("https://executor.example"))
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/session_deletion_public_test.go b/services/core/tests/integration/session_deletion_public_test.go
index 65fae5e91..e8abb5bed 100644
--- a/services/core/tests/integration/session_deletion_public_test.go
+++ b/services/core/tests/integration/session_deletion_public_test.go
@@ -23,13 +23,13 @@ func TestSessionDeletionOfficialClient(t *testing.T) {
{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()},
{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()},
})
- h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s))
+ h, err := publicHandler(t, s, auth, "codex")
if err != nil {
t.Fatal(err)
}
server := httptest.NewServer(h)
defer server.Close()
- h, err = publicHandler(t, New(s.pool), auth, "codex", storeExecution(t, New(s.pool)))
+ h, err = publicHandler(t, New(t, s.pool), auth, "codex")
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/session_deletion_test.go b/services/core/tests/integration/session_deletion_test.go
index 724ad4555..f23b20593 100644
--- a/services/core/tests/integration/session_deletion_test.go
+++ b/services/core/tests/integration/session_deletion_test.go
@@ -116,7 +116,7 @@ func TestSessionDeletionWaitsForSettledTurnAndRejectsAdmission(t *testing.T) {
t.Fatal(err)
}
marker := sessionDeletedAt(t, pool, session.ID)
- fresh := New(pool)
+ fresh := New(t, pool)
// The owner's repeated deletion confirms again without another write.
for _, repeat := range []*Store{s, fresh} {
if err := sessionService(t, repeat).DeleteSession(ctx, sessions.DeleteSessionCommand{TenantID: tenant, SessionID: session.ID}); err != nil {
@@ -282,7 +282,7 @@ func TestSessionDeletionRacesAdmissionUnderSessionLock(t *testing.T) {
t.Fatal(err)
}
t.Cleanup(instrumented.Close)
- return New(instrumented)
+ return New(t, instrumented)
}
for _, kind := range admissions {
t.Run(kind.name+"/admission-first", func(t *testing.T) {
@@ -339,7 +339,7 @@ func TestSessionDeletionRacesAdmissionUnderSessionLock(t *testing.T) {
t.Run(kind.name+"/concurrent", func(t *testing.T) {
for range 8 {
tenant, session := kind.setup(t, plain)
- other := New(pool)
+ other := New(t, pool)
start := make(chan struct{})
results := make(chan error, 2)
go func() {
diff --git a/services/core/tests/integration/session_events_test.go b/services/core/tests/integration/session_events_test.go
index 210b755ab..5a14b6aa1 100644
--- a/services/core/tests/integration/session_events_test.go
+++ b/services/core/tests/integration/session_events_test.go
@@ -102,7 +102,7 @@ func TestSessionEventsCommitSnapshotsRetriesAndIsolation(t *testing.T) {
var all []sessions.SessionChange
cursor := int64(0)
for {
- page, err := sessionAdapter(New(pool)).ListSessionEvents(ctx, tenant, session.ID, cursor)
+ page, err := sessionAdapter(New(t, pool)).ListSessionEvents(ctx, tenant, session.ID, cursor)
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/session_execution_configuration_test.go b/services/core/tests/integration/session_execution_configuration_test.go
index 3f6f5f45a..0043ac053 100644
--- a/services/core/tests/integration/session_execution_configuration_test.go
+++ b/services/core/tests/integration/session_execution_configuration_test.go
@@ -60,7 +60,7 @@ func TestSessionExecutionConfigurationFrozenAcrossCreationPathsAndRetry(t *testi
t.Fatal(err)
}
// A reader without the encryption key can use the safe snapshot after restart.
- reader := New(pool)
+ reader := New(t, pool)
frozen, err := sessionAdapter(reader).GetSessionExecutionConfiguration(t.Context(), tenant, session.ID)
if err != nil {
t.Fatal(err)
diff --git a/services/core/tests/integration/session_model_execution_http_test.go b/services/core/tests/integration/session_model_execution_http_test.go
index 9a1e6c2f1..3300e84a7 100644
--- a/services/core/tests/integration/session_model_execution_http_test.go
+++ b/services/core/tests/integration/session_model_execution_http_test.go
@@ -18,7 +18,7 @@ func TestModelExecutionHTTPWriteOnlyAndStrictAdmission(t *testing.T) {
st := NewWithCredentialCipher(pool, cipher)
tenant, token := uuid.NewString(), uuid.NewString()
auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "catalog-test", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant}})
- handler, err := publicHandler(t, st, auth, "codex", storeExecution(t, st), managedSandboxes(t, st))
+ handler, err := publicHandler(t, st, auth, "codex")
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/session_reads_fixture_test.go b/services/core/tests/integration/session_reads_fixture_test.go
index 6dbc04a1b..3aa8e060e 100644
--- a/services/core/tests/integration/session_reads_fixture_test.go
+++ b/services/core/tests/integration/session_reads_fixture_test.go
@@ -2,6 +2,7 @@ package integration
import (
"context"
+ "testing"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
@@ -14,8 +15,8 @@ import (
// FixtureEnvironmentDevice provisions the dedicated Runtime device of the
// tenant's hosted Environment on pool through the procedure the managed
// Runtime allocation runs, without the allocation.
-func FixtureEnvironmentDevice(ctx context.Context, pool *pgxpool.Pool, tenant, environment, name, credentialHash string) (sessions.ExecutionDevice, error) {
- s := New(pool)
+func FixtureEnvironmentDevice(t testing.TB, ctx context.Context, pool *pgxpool.Pool, tenant, environment, name, credentialHash string) (sessions.ExecutionDevice, error) {
+ s := New(t, pool)
current, err := sessionAdapter(s).GetEnvironment(ctx, tenant, environment)
if err != nil {
return sessions.ExecutionDevice{}, err
diff --git a/services/core/tests/integration/session_reference_retry_public_test.go b/services/core/tests/integration/session_reference_retry_public_test.go
index 5ee70df0a..e3b066edf 100644
--- a/services/core/tests/integration/session_reference_retry_public_test.go
+++ b/services/core/tests/integration/session_reference_retry_public_test.go
@@ -37,7 +37,7 @@ func TestSavedReferenceRetryOfficialClient(t *testing.T) {
}
server := httptest.NewServer(handler)
defer server.Close()
- recovered, err := publicHandler(t, New(s.pool), auth, "codex")
+ recovered, err := publicHandler(t, New(t, s.pool), auth, "codex")
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/sessions_test.go b/services/core/tests/integration/sessions_test.go
index 9e945672c..d222712f4 100644
--- a/services/core/tests/integration/sessions_test.go
+++ b/services/core/tests/integration/sessions_test.go
@@ -19,7 +19,7 @@ import (
func testStore(t *testing.T) (*Store, *pgxpool.Pool) {
t.Helper()
pool := pgtest.Open(t)
- return New(pool), pool
+ return New(t, pool), pool
}
// sessionAdapter is the Session adapter on s's database with s's credential
diff --git a/services/core/tests/integration/stream_authority_http_test.go b/services/core/tests/integration/stream_authority_http_test.go
index 3ca7984c7..6c7ebde70 100644
--- a/services/core/tests/integration/stream_authority_http_test.go
+++ b/services/core/tests/integration/stream_authority_http_test.go
@@ -42,7 +42,7 @@ func TestLiveStreamClosesAfterKeyRevocationOrProjectArchive(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- h, err := publicHandler(t, s, nil, "codex", storeKeys(s), storeExecution(t, s))
+ h, err := publicHandler(t, s, nil, "codex", storeKeys(s))
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/subagent_resources_test.go b/services/core/tests/integration/subagent_resources_test.go
index 474f4cb6e..1cfbf33c8 100644
--- a/services/core/tests/integration/subagent_resources_test.go
+++ b/services/core/tests/integration/subagent_resources_test.go
@@ -146,7 +146,7 @@ func TestSubagentResourcesNativeOwnershipLifecycleAndRecovery(t *testing.T) {
t.Fatal(value, err)
}
appendFacts(subagentFact(proto.TypeSubagentLifecycle, resumed), subagentFact(proto.TypeSubagentLifecycle, resumed))
- reopened := sessionAdapter(New(pool))
+ reopened := sessionAdapter(New(t, pool))
value, err = reopened.GetSubagent(ctx, tenant, session.ID, child.ID)
if err != nil || value.Status != "active" || value.ClosedAt != nil || value.OpenedAt != 100 {
t.Fatal(value, err)
diff --git a/services/core/tests/integration/subagent_visibility_public_test.go b/services/core/tests/integration/subagent_visibility_public_test.go
index b6b7ec69e..e8140f88f 100644
--- a/services/core/tests/integration/subagent_visibility_public_test.go
+++ b/services/core/tests/integration/subagent_visibility_public_test.go
@@ -76,7 +76,7 @@ func TestSubagentVisibilityPublic(t *testing.T) {
{OrganizationID: "test-org", ProjectID: tenant, SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: tenant},
{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "foreign", TokenSHA256: runtimedevice.HashCredential(foreign), TenantID: uuid.NewString()},
})
- handler, err := publicHandler(t, s, auth, "codex", storeExecution(t, s))
+ handler, err := publicHandler(t, s, auth, "codex")
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/template_composition_public_test.go b/services/core/tests/integration/template_composition_public_test.go
index a958e47ac..1a483d6fb 100644
--- a/services/core/tests/integration/template_composition_public_test.go
+++ b/services/core/tests/integration/template_composition_public_test.go
@@ -39,7 +39,7 @@ func TestTemplateCompositionOfficialClientPostgres(t *testing.T) {
serve := func(current *Store) *httptest.Server {
t.Helper()
// Hosted admission and freezing use the real Store; no Runtime or model runs.
- h, err := publicHandler(t, current, auth, "codex", storeExecution(t, current), managedSandboxes(t, current), fixtureDeploymentProvider())
+ h, err := publicHandler(t, current, auth, "codex", fixtureDeploymentProvider())
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/template_null_selection_public_test.go b/services/core/tests/integration/template_null_selection_public_test.go
index 368e7f5e5..f35cfecfd 100644
--- a/services/core/tests/integration/template_null_selection_public_test.go
+++ b/services/core/tests/integration/template_null_selection_public_test.go
@@ -40,7 +40,7 @@ func TestTemplateNullSelectionOfficialClientPostgres(t *testing.T) {
})
serve := func(current *Store) *httptest.Server {
t.Helper()
- h, err := publicHandler(t, current, auth, "codex", storeExecution(t, current), managedSandboxes(t, current), fixtureDeploymentProvider())
+ h, err := publicHandler(t, current, auth, "codex", fixtureDeploymentProvider())
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/token_usage_integration_test.go b/services/core/tests/integration/token_usage_integration_test.go
index 434b4f869..a43ad2dfa 100644
--- a/services/core/tests/integration/token_usage_integration_test.go
+++ b/services/core/tests/integration/token_usage_integration_test.go
@@ -83,7 +83,7 @@ func TestTokenUsageDurableSnapshotsAndSessionTotals(t *testing.T) {
t.Fatal(err)
}
defer restored.Close()
- fresh := New(restored)
+ fresh := New(t, restored)
got, err := sessionAdapter(fresh).GetSession(ctx, tenant, session.ID)
if err != nil {
t.Fatal(err)
diff --git a/services/core/tests/integration/unified_model_configuration_http_test.go b/services/core/tests/integration/unified_model_configuration_http_test.go
index bfcea9301..acc25954d 100644
--- a/services/core/tests/integration/unified_model_configuration_http_test.go
+++ b/services/core/tests/integration/unified_model_configuration_http_test.go
@@ -21,7 +21,7 @@ func TestUnifiedModelConfigurationHTTP(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- handler, err := publicHandler(t, st, auth, "codex", storeExecution(t, st), managedSandboxes(t, st), withCoreKeys(admin), withHarnesses([]string{"codex", "claude_sdk"}))
+ handler, err := publicHandler(t, st, auth, "codex", withCoreKeys(admin), withHarnesses([]string{"codex", "claude_sdk"}))
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/unstorable_text_public_test.go b/services/core/tests/integration/unstorable_text_public_test.go
index 01f1f9804..e98651f8a 100644
--- a/services/core/tests/integration/unstorable_text_public_test.go
+++ b/services/core/tests/integration/unstorable_text_public_test.go
@@ -27,7 +27,7 @@ func TestUnstorableTextRejectsWithoutWritesPostgres(t *testing.T) {
s := NewWithCredentialCipher(pool, cipher)
token := uuid.NewString()
auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "nul-owner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()}})
- h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s))
+ h, err := publicHandler(t, s, auth, "codex")
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/vaults_fixture_test.go b/services/core/tests/integration/vaults_fixture_test.go
index 5249ca3ea..f547fea5a 100644
--- a/services/core/tests/integration/vaults_fixture_test.go
+++ b/services/core/tests/integration/vaults_fixture_test.go
@@ -8,7 +8,6 @@ import (
)
// fixtureVaults builds the Vault adapter and service on s, as cmd/server does.
-// A keyless s leaves the operations that need no credential key available.
func fixtureVaults(s *Store) (*vaultpg.Store, *vaults.Service, error) {
refresher, err := oauthrefresh.NewClient(nil)
if err != nil {
diff --git a/services/core/tests/integration/whitespace_input_public_test.go b/services/core/tests/integration/whitespace_input_public_test.go
index 3954c0b49..8229359e7 100644
--- a/services/core/tests/integration/whitespace_input_public_test.go
+++ b/services/core/tests/integration/whitespace_input_public_test.go
@@ -22,7 +22,7 @@ func TestWhitespaceInputStoredVerbatimPostgres(t *testing.T) {
s, _ := newManagedTestStore(t)
token := uuid.NewString()
auth := newTestAuthenticator(t, []testAPIKey{{OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "whitespace-owner", TokenSHA256: runtimedevice.HashCredential(token), TenantID: uuid.NewString()}})
- h, err := publicHandler(t, s, auth, "codex", storeExecution(t, s))
+ h, err := publicHandler(t, s, auth, "codex")
if err != nil {
t.Fatal(err)
}
diff --git a/services/core/tests/integration/worker_input_race_test.go b/services/core/tests/integration/worker_input_race_test.go
index 76030a770..f828ce90f 100644
--- a/services/core/tests/integration/worker_input_race_test.go
+++ b/services/core/tests/integration/worker_input_race_test.go
@@ -59,7 +59,7 @@ func TestWorkerInputReadSkipsConcurrentlyCancelledCandidate(t *testing.T) {
defer instrumented.Close()
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
- worker := startWorker(t, ctx, New(instrumented), h.d)
+ worker := startWorker(t, ctx, New(t, instrumented), h.d)
done := make(chan error, 1)
go func() { done <- worker.Run(ctx) }()
defer func() {
diff --git a/services/core/tests/integration/worker_wakeup_test.go b/services/core/tests/integration/worker_wakeup_test.go
index 3f4543fb6..5ab16df8d 100644
--- a/services/core/tests/integration/worker_wakeup_test.go
+++ b/services/core/tests/integration/worker_wakeup_test.go
@@ -45,7 +45,7 @@ func TestWorkerSchedulerCommittedAdmissionWakesBeforeMaintenance(t *testing.T) {
defer instrumented.Close()
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
- worker := startWorker(t, ctx, NewWithCredentialCipher(instrumented, fixtureCipher), h.d)
+ worker := startWorker(t, ctx, New(t, instrumented), h.d)
done := make(chan error, 1)
started := false
defer func() {
diff --git a/services/core/tests/official_client.py b/services/core/tests/official_client.py
index b47c1fe15..2b9bfe0a4 100644
--- a/services/core/tests/official_client.py
+++ b/services/core/tests/official_client.py
@@ -23,12 +23,12 @@
from official_agents import verify_agents
from official_vaults import verify_vaults, verify_vault_recovery
from official_vault_list import verify_vault_list, verify_vault_list_recovery
-from official_credentials import verify_credentials, verify_credential_recovery, verify_credential_storage_disabled
+from official_credentials import verify_credentials, verify_credential_recovery, create_old_key_credential, verify_old_key_credential
from official_credential_list import verify_credential_list, verify_credential_list_recovery
from official_mcp_credentials import verify_mcp_credentials, verify_mcp_credential_recovery
from official_credential_rotation import verify_credential_rotation, verify_rotation_recovery
-from official_credential_delete import verify_credential_deletion, verify_credential_deletion_recovery, verify_keyless_credential_deletion
-from official_vault_delete import verify_vault_deletion, verify_vault_deletion_recovery, verify_keyless_vault_deletion
+from official_credential_delete import verify_credential_deletion, verify_credential_deletion_recovery, verify_key_lost_credential_deletion
+from official_vault_delete import verify_vault_deletion, verify_vault_deletion_recovery, verify_key_lost_vault_deletion
from official_agent_list import verify_agent_list
from official_http_routing import verify_http_routing
from official_agent_references import verify_agent_references
@@ -80,12 +80,16 @@ def project_bindings(directory):
core_key_digests.write_text(json.dumps([hashlib.sha256(admin_token.encode()).hexdigest()]))
credential_key = Path(directory) / "credential-key.txt"
credential_key.touch(mode=0o600)
- credential_key_value = base64.b64encode(secrets.token_bytes(32)).decode()
- credential_key.write_text(credential_key_value + "\n")
+ credential_key_values = [base64.b64encode(secrets.token_bytes(32)).decode()]
+ credential_key.write_text(credential_key_values[0] + "\n")
+ # The database records the first installation ID it sees, so every run uses this one.
+ installation_id = Path(directory) / "installation.id"
+ installation_id.touch(mode=0o600)
+ installation_id.write_text("3f8e2c71-5b0d-4e6a-9c47-1d2a8b6f0e53\n")
env = dict(os.environ, OAC_DATABASE_URL=dsn, OAC_CORE_KEY_DIGESTS_FILE=str(core_key_digests), OAC_ADDR=f"127.0.0.1:{port}", OAC_DEFAULT_HARNESS="codex")
env["OAC_CREDENTIAL_KEY_FILE"] = str(credential_key)
- # Enable the real Worker/gateway admission path without connecting a daemon.
- # Synthetic fixture inputs remain queued; this is not live model acceptance.
+ env["OAC_INSTALLATION_ID_FILE"] = str(installation_id)
+ # No daemon connects: synthetic fixture inputs remain queued; this is not live model acceptance.
env["OAC_PUBLIC_URL"] = f"http://127.0.0.1:{port}"
with (Path(directory) / "server.log").open("w+") as log:
def start():
@@ -326,24 +330,27 @@ def issue_key(project_id, name):
process = start()
with client(tokens[0]) as a:
verify_mcp_credential_recovery(a, claude_credentials)
+ old_key_credential = create_old_key_credential(a, credential_canary)
process.terminate()
process.wait(timeout=15)
env["OAC_DEFAULT_HARNESS"] = "codex"
- env.pop("OAC_CREDENTIAL_KEY_FILE")
+ # The operator lost the key: Core restarts under a new one.
+ credential_key_values.append(base64.b64encode(secrets.token_bytes(32)).decode())
+ credential_key.write_text(credential_key_values[-1] + "\n")
process = start()
- with client(tokens[0]) as without_key, client(tokens[1]) as other, client(peer_key) as peer:
- verify_credential_storage_disabled(without_key, saved_credentials[0][0], credential_canary, expect_error)
- verify_keyless_credential_deletion(without_key, credential_deletion, expect_error)
- verify_keyless_vault_deletion(without_key, vault_deletion, expect_error)
- verify_credential_list_recovery(without_key, other, peer, listed_credentials,
- credential_canary, phase="restart without the storage key")
+ with client(tokens[0]) as a, client(tokens[1]) as other, client(peer_key) as peer:
+ verify_old_key_credential(a, old_key_credential, credential_canary, expect_error)
+ verify_key_lost_credential_deletion(a, credential_deletion, expect_error)
+ verify_key_lost_vault_deletion(a, vault_deletion, expect_error)
+ verify_credential_list_recovery(a, other, peer, listed_credentials,
+ credential_canary, phase="restart under a replaced credential key")
print("Caller principal: SDK/raw HTTP scope checks, shared Project access and persistent scope recovery passed.")
print("Official Turn client: lifecycle, Agent identity, safe errors, restart recovery, pagination and tenant/Session isolation passed.")
print("Official Go client: creation/retries, retrieval, bidirectional pagination and tenant isolation passed.")
print("Official client: upstream and generated response schemas, persistence/restart, retries, pagination, tenant isolation and explicit unsupported options passed.")
finally:
finish_server(process, log, [admin_token, *tokens, credential_canary,
- credential_key_value], sys.exc_info()[1])
+ *credential_key_values], sys.exc_info()[1])
if __name__ == "__main__":
diff --git a/services/core/tests/official_credential_delete.py b/services/core/tests/official_credential_delete.py
index aef43b2d5..2d7a01b4d 100644
--- a/services/core/tests/official_credential_delete.py
+++ b/services/core/tests/official_credential_delete.py
@@ -12,7 +12,7 @@ def verify_credential_deletion(client, other, invalid, peer, canary, expect_erro
credentials = client.beta.agents.vaults.credentials
auth = {"type": "static_bearer", "mcp_server_url": "https://example.invalid/delete", "token": canary}
values = [credentials.create(vault.id, name=name, auth=auth)
- for name in ["SDK target", "HTTP target", "Keyless target", "Retained sibling"]]
+ for name in ["SDK target", "HTTP target", "Key-lost target", "Retained sibling"]]
foreign = other.beta.agents.vaults.credentials.create(foreign_vault.id, name="Foreign", auth=auth)
headers = {"Authorization": f"Bearer {client.api_key}", "OpenAI-Beta": "agents=v1"}
endpoint = str(client.base_url).rstrip("/") + "/vaults/" + vault.id + "/credentials"
@@ -69,7 +69,7 @@ def verify_credential_deletion_recovery(client, other, saved, expect_error):
print("Credential deletion: absent resources and unaffected siblings survived API restart.")
-def verify_keyless_credential_deletion(client, saved, expect_error):
+def verify_key_lost_credential_deletion(client, saved, expect_error):
values, _ = saved
target, sibling = values[2:]
credentials = client.beta.agents.vaults.credentials
@@ -78,4 +78,4 @@ def verify_keyless_credential_deletion(client, saved, expect_error):
expect_error(NotFoundError, lambda: credentials.retrieve(target.id, vault_id=target.vault_id))
assert credentials.retrieve(sibling.id, vault_id=sibling.vault_id) == sibling
assert list(credentials.list(target.vault_id)) == [sibling]
- print("Credential deletion: no storage key required; safe sibling metadata remains available.")
+ print("Credential deletion: works under a replaced key; safe sibling metadata remains available.")
diff --git a/services/core/tests/official_credentials.py b/services/core/tests/official_credentials.py
index a3f1efc47..7fadff761 100644
--- a/services/core/tests/official_credentials.py
+++ b/services/core/tests/official_credentials.py
@@ -147,27 +147,22 @@ def verify_credential_recovery(client, other, peer, saved):
print("Static credentials: exact SDK/raw metadata and shared-project reads survived the service restart.")
-def verify_credential_storage_disabled(client, value, canary, expect_error):
+def create_old_key_credential(client, canary):
+ vault = client.beta.agents.vaults.create(name="Sealed under the old key")
+ return client.beta.agents.vaults.credentials.create(vault.id, name="Old key", auth={
+ "type": "mcp_oauth", "mcp_server_url": "https://example.invalid/old-key", "access_token": canary})
+
+
+def verify_old_key_credential(client, value, canary, expect_error):
credentials = client.beta.agents.vaults.credentials
assert credentials.retrieve(value.id, vault_id=value.vault_id) == value
- request = {"name": "Disabled storage", "auth": {**value.auth.to_dict(), "token": canary}}
- error = expect_error(InternalServerError, lambda: credentials.create(value.vault_id, **request))
- assert error.status_code == 503 and error.body["code"] == "credential_storage_unavailable"
+ error = expect_error(InternalServerError, lambda: credentials.update(
+ value.id, vault_id=value.vault_id, auth={"type": "mcp_oauth", "access_token": canary + "replacement"}))
+ assert error.status_code == 500 and error.body["code"] == "internal_error"
assert canary not in error.response.text
- with httpx2.Client(trust_env=False, timeout=10) as raw:
- response = raw.post(str(client.base_url).rstrip("/") + "/vaults/" + value.vault_id + "/credentials",
- headers={"Authorization": f"Bearer {client.api_key}", "OpenAI-Beta": "agents=v1"},
- json=request)
- assert response.status_code == 503 and canary not in response.text
- replacement = {"auth": {"type": "static_bearer", "token": canary + "replacement"}}
- error = expect_error(InternalServerError, lambda: credentials.update(value.id, vault_id=value.vault_id, **replacement))
- assert error.status_code == 503 and error.body["code"] == "credential_storage_unavailable"
- assert canary not in error.response.text
- response = raw.post(str(client.base_url).rstrip("/") + "/vaults/" + value.vault_id + "/credentials/" + value.id,
- headers={"Authorization": f"Bearer {client.api_key}", "OpenAI-Beta": "agents=v1"},
- json=replacement)
- assert response.status_code == 503 and canary not in response.text
- assert credentials.retrieve(value.id, vault_id=value.vault_id) == value
- vault = client.beta.agents.vaults.create(name="Non-secret resource without credential key")
- assert client.beta.agents.vaults.retrieve(vault.id) == vault
- print("Static credentials: absent key rejects SDK/raw writes while safe reads and Vault creation remain available.")
+ assert credentials.retrieve(value.id, vault_id=value.vault_id) == value
+ created = credentials.create(value.vault_id, name="New key", auth={
+ "type": "static_bearer", "mcp_server_url": "https://example.invalid/new-key", "token": canary})
+ assert canary not in created.model_dump_json()
+ assert credentials.delete(created.id, vault_id=created.vault_id).deleted
+ print("Credential key replacement: an old secret fails closed without leaking while new secrets are stored.")
diff --git a/services/core/tests/official_vault_delete.py b/services/core/tests/official_vault_delete.py
index b69154cfe..301a7d478 100644
--- a/services/core/tests/official_vault_delete.py
+++ b/services/core/tests/official_vault_delete.py
@@ -1,4 +1,4 @@
-"""Pinned Vault deletion, child removal, scoped retries and keyless recovery."""
+"""Pinned Vault deletion, child removal, scoped retries and deletion under a replaced key."""
import uuid
@@ -8,11 +8,11 @@
def verify_vault_deletion(client, other, invalid, peer, canary, expect_error):
vaults = client.beta.agents.vaults
- values = [vaults.create(name=name) for name in ["Cascade target", "Empty HTTP target", "Keyless target", "Retained Vault"]]
+ values = [vaults.create(name=name) for name in ["Cascade target", "Empty HTTP target", "Key-lost target", "Retained Vault"]]
foreign = other.beta.agents.vaults.create(name="Foreign Vault deletion")
auth = {"type": "static_bearer", "mcp_server_url": "https://example.invalid/vault-delete", "token": canary}
children = [vaults.credentials.create(values[0].id, name=str(i), auth=auth) for i in range(3)]
- keyless = vaults.credentials.create(values[2].id, name="Keyless child", auth=auth)
+ key_lost = vaults.credentials.create(values[2].id, name="Key-lost child", auth=auth)
retained = vaults.credentials.create(values[3].id, name="Retained child", auth=auth)
foreign_child = other.beta.agents.vaults.credentials.create(foreign.id, name="Foreign child", auth=auth)
spec = {"input": "Verify vault delete fixture admission.", "agent": {"model": "requested-model"}, "environment": {"type": "none"}, "vault_ids": [values[0].id, values[3].id]}
@@ -73,11 +73,11 @@ def verify_vault_deletion(client, other, invalid, peer, canary, expect_error):
assert other.beta.agents.vaults.retrieve(foreign.id) == foreign
assert other.beta.agents.vaults.credentials.retrieve(foreign_child.id, vault_id=foreign.id) == foreign_child
print("Vault deletion: SDK/raw confirmation, cascade visibility, scope and retained Session identity passed.")
- return values, keyless, retained, foreign, foreign_child, spec, headers, session
+ return values, key_lost, retained, foreign, foreign_child, spec, headers, session
def verify_vault_deletion_recovery(client, other, saved, expect_error):
- values, keyless, retained, foreign, foreign_child, spec, headers, session = saved
+ values, key_lost, retained, foreign, foreign_child, spec, headers, session = saved
vaults = client.beta.agents.vaults
for target in values[:2]:
expect_error(NotFoundError, lambda: vaults.retrieve(target.id))
@@ -85,7 +85,7 @@ def verify_vault_deletion_recovery(client, other, saved, expect_error):
expect_error(NotFoundError, lambda: vaults.credentials.list(target.id))
for value in values[2:]:
assert vaults.retrieve(value.id) == value
- for child in [keyless, retained]:
+ for child in [key_lost, retained]:
assert vaults.credentials.retrieve(child.id, vault_id=child.vault_id) == child
assert other.beta.agents.vaults.retrieve(foreign.id) == foreign
assert other.beta.agents.vaults.credentials.retrieve(foreign_child.id, vault_id=foreign.id) == foreign_child
@@ -95,7 +95,7 @@ def verify_vault_deletion_recovery(client, other, saved, expect_error):
print("Vault deletion: absent parents/children, unaffected resources and Session retry survived API restart.")
-def verify_keyless_vault_deletion(client, saved, expect_error):
+def verify_key_lost_vault_deletion(client, saved, expect_error):
values, child, retained, *_ = saved
vaults, target = client.beta.agents.vaults, values[2]
assert vaults.delete(target.id).to_dict() == {"id": target.id, "deleted": True, "object": "vault.deleted"}
@@ -104,4 +104,4 @@ def verify_keyless_vault_deletion(client, saved, expect_error):
expect_error(NotFoundError, lambda: vaults.credentials.list(target.id))
assert vaults.retrieve(values[3].id) == values[3]
assert vaults.credentials.retrieve(retained.id, vault_id=retained.vault_id) == retained
- print("Vault deletion: keyless cascade removed stored children while another Vault remained usable.")
+ print("Vault deletion: the cascade under a replaced key removed stored children while another Vault remained usable.")