mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 20:46:28 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - A managed instance gets its instance-level environments from the `PAPERCLIP_MANAGED_CONFIG` document, and #19508 added plugin environment entries to it > - Greptile's Apex review of #19508 arrived after it merged, with three findings > - A credential inside an array nested in an array passed the secret check and the response redaction > - Recovery of an archived environment listened on one worker handle, which a restart replaces, and its attempts were not tracked through idle checks and shutdown > - This pull request closes all three findings > - The benefit is that credentials never pass the check, an environment comes back after any worker restart, and shutdown never cuts off a recovery write ## Linked Issues or Issue Description Refs #19508 (the Apex findings this pull request fixes) **Subsystem affected** Server environments: the managed-config parser, the environment routes' response floor, the managed environment bootstrap, and the plugin worker manager. **Problem or motivation** Greptile's Apex review of #19508 found three problems. (1) `findSecretLikeConfigKey` and `redactSecretLikeConfigKeys` skip array elements that are arrays, so `{"groups":[[{"apiToken":"..."}]]}` passes the boot check and is echoed to restricted readers. (2) Recovery subscribes to `ready` on one worker handle. A restart such as a settings save replaces that handle, so the new worker's `ready` never reaches the listener, and the environment stays archived. (3) A recovery attempt is fire-and-forget. The worker manager's work token ends when the worker replies, but the database write continues, so an idle check or the database shutdown can overlap it. **Proposed solution** Walk arrays at any depth in both functions. Add a manager-wide `onWorkerReady` subscription and use it for recovery. Run every recovery attempt through a controller that tracks it as idle work and that shutdown stops and awaits before the database closes. **Alternatives considered** Re-subscribing on each new handle after a restart. That needs a hook in every restart path. The manager-wide subscription covers all of them in one place. **Roadmap alignment** This only fixes code that #19508 added or touched. It adds no product surface. ## What Changed - `server/src/services/managed-config.ts`: `findSecretLikeConfigKey` walks arrays at any depth (new `findSecretLikeConfigKeyInValue`). This covers sandbox `config` and plugin `driverConfig`. - `server/src/routes/environments.ts`: `redactSecretLikeConfigKeys` walks arrays at any depth (new `redactSecretLikeConfigValue`). - `server/src/services/plugin-worker-manager.ts`: new optional `onWorkerReady(listener)` on `PluginWorkerManager`. It fires on every worker `ready`, including a replacement handle from a restart. A listener error is logged and does not affect the worker. - `server/src/services/managed-environments.ts`: - Sandbox and plugin recovery subscribe through `subscribeWorkerReady`. It prefers `onWorkerReady` and falls back to the handle when the manager has no such method. - New `createManagedEnvironmentRecovery()` controller. Each attempt runs inside `trackIdleWork` for its whole duration. `stop()` removes every `ready` subscription, refuses new attempts, and awaits unfinished ones. - `server/src/index.ts`: creates the controller, passes it to `applyManagedEnvironments`, and awaits `stop()` during shutdown, after the delivery workers stop and before the application teardown and database close. ## Verification - `cd server && npx vitest run src/__tests__/managed-config.test.ts`: a secret-like key in an array nested in an array refuses boot, for plugin `driverConfig` and sandbox `config`. - `npx vitest run src/__tests__/environment-routes.test.ts`: a nested-array credential is removed from the response, and other nested values stay. - `npx vitest run src/services/managed-environments-plugin-entries.test.ts`: - With `onWorkerReady`, recovery never consults the handle. It ignores another plugin's `ready`, re-ensures on this plugin's `ready`, and then unsubscribes. - An attempt counts in `idleWorkSnapshot()` until its ensure settles. `stop()` unsubscribes, waits for the unfinished attempt, and later `ready` events start nothing. - `npx vitest run src/services/managed-environments.test.ts src/__tests__/managed-plugin-environments.test.ts`: pass. - `pnpm --filter @paperclipai/server typecheck` passes. - I did not run the full `pnpm test:run`, `pnpm -r typecheck` or `pnpm build`. CI covers them. ## Risks - The redaction now also walks nested arrays in sandbox config responses. Only credential-shaped keys are removed; other values are unchanged. - The boot check now refuses a document that hides a secret-like key in a nested array. A valid document has no such key. - `onWorkerReady` is optional, so other `PluginWorkerManager` implementations and test fakes keep working. - Shutdown now waits for an unfinished recovery attempt. An attempt is one worker call plus one short transaction. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, through Claude Code with tool use (file edits, shell, test runs). ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
533 lines
23 KiB
TypeScript
533 lines
23 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
MANAGED_CONFIG_ENV_KEY,
|
|
MANAGED_ENVIRONMENT_DRIVERS,
|
|
getManagedInstanceConfig,
|
|
managedFeatureKeySet,
|
|
parseManagedConfigEnv,
|
|
} from "../services/managed-config.js";
|
|
|
|
function envWith(raw: string | undefined) {
|
|
return { [MANAGED_CONFIG_ENV_KEY]: raw };
|
|
}
|
|
|
|
function validDoc(overrides: Record<string, unknown> = {}) {
|
|
return JSON.stringify({
|
|
v: 1,
|
|
mode: "cloud",
|
|
catalogVersion: "2026.720.0",
|
|
features: { enableApps: false, enableStatusCards: true },
|
|
plugins: { autoInstall: ["daytona", "kubernetes"] },
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
describe("plugins.workerEnv", () => {
|
|
const plugins = (workerEnv: unknown) => ({ plugins: { autoInstall: ["daytona"], workerEnv } });
|
|
|
|
it("is absent from documents that do not declare it", () => {
|
|
expect(parseManagedConfigEnv(envWith(validDoc()))?.plugins).toEqual({ autoInstall: ["daytona", "kubernetes"] });
|
|
});
|
|
|
|
it("accepts variable names for an auto-installed plugin", () => {
|
|
const parsed = parseManagedConfigEnv(envWith(validDoc(plugins({ daytona: ["PROVIDER_ORIGIN", "PROVIDER_CAPABILITY_2"] }))));
|
|
expect(parsed?.plugins.workerEnv).toEqual({ daytona: ["PROVIDER_ORIGIN", "PROVIDER_CAPABILITY_2"] });
|
|
});
|
|
|
|
it.each([
|
|
"ANTHROPIC_API_KEY", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "BETTER_AUTH_SECRET", "DATABASE_MIGRATION_URL", "DATABASE_URL", "DB_PASSWORD",
|
|
"GH_TOKEN", "GITHUB_TOKEN", "LD_PRELOAD", "NODE_OPTIONS", "PAPERCLIP_AGENT_JWT_SECRET", "PAPERCLIP_AUTH_SECRET", "PAPERCLIP_BACKUP_WORKER_DATABASE_URL",
|
|
"PAPERCLIP_CLOUD_CONNECTOR_SIGN_PRIVATE_KEY", "PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN", "PAPERCLIP_DECISION_SIGNING_SECRET", "PAPERCLIP_MANAGED_CONFIG",
|
|
"PAPERCLIP_SECRETS_MASTER_KEY", "PAPERCLIP_SECRETS_MASTER_KEY_FILE", "PAPERCLIP_TEST_POSTGRES_ADMIN_URL", "PATH", "SENTRY_DSN",
|
|
])("refuses to start with a grant of %s", (name) => {
|
|
expect(() => parseManagedConfigEnv(envWith(validDoc(plugins({ daytona: [name] }))))).toThrow(`must not grant host credential or loader variable "${name}"`);
|
|
});
|
|
|
|
it.each(["PAPERCLIP_CLOUD_API_ORIGIN", "PAPERCLIP_PRIMARY_HOST", "PROVIDER_CAPABILITY", "MONKEY_ORIGIN"])("accepts a grant of %s", (name) => {
|
|
expect(parseManagedConfigEnv(envWith(validDoc(plugins({ daytona: [name] }))))?.plugins.workerEnv).toEqual({ daytona: [name] });
|
|
});
|
|
|
|
it.each([
|
|
["a plugin that is not auto-installed", { other: ["PROVIDER_ORIGIN"] }, /not in "plugins.autoInstall"/],
|
|
["a non-object section", ["PROVIDER_ORIGIN"], /must be an object/],
|
|
["an empty grant", { daytona: [] }, /non-empty array/],
|
|
["a value instead of a name", { daytona: ["https://example.test"] }, /uppercase variable names/],
|
|
["a lowercase name", { daytona: ["database_url"] }, /uppercase variable names/],
|
|
["a duplicate name", { daytona: ["PROVIDER_ORIGIN", "PROVIDER_ORIGIN"] }, /duplicate entry/],
|
|
])("refuses to start with %s", (_label, workerEnv, message) => {
|
|
expect(() => parseManagedConfigEnv(envWith(validDoc(plugins(workerEnv))))).toThrow(message);
|
|
});
|
|
});
|
|
|
|
describe("plugins.enabledCompanies", () => {
|
|
const companyId = "f0679247-e2be-5a76-adb6-6bd360e5a833";
|
|
const plugins = (enabledCompanies: unknown) => ({ plugins: { autoInstall: ["daytona"], enabledCompanies } });
|
|
|
|
it("accepts company ids for an auto-installed plugin", () => {
|
|
expect(parseManagedConfigEnv(envWith(validDoc(plugins({ daytona: [companyId] }))))?.plugins.enabledCompanies).toEqual({ daytona: [companyId] });
|
|
});
|
|
|
|
it.each([
|
|
["a plugin that is not auto-installed", { other: [companyId] }, /not in "plugins.autoInstall"/],
|
|
["a non-object section", [companyId], /must be an object/],
|
|
["an empty list", { daytona: [] }, /non-empty array/],
|
|
["a value that is not a UUID", { daytona: ["all"] }, /lowercase company UUIDs/],
|
|
["an uppercase UUID", { daytona: [companyId.toUpperCase()] }, /lowercase company UUIDs/],
|
|
["a duplicate id", { daytona: [companyId, companyId] }, /duplicate entry/],
|
|
])("refuses to start with %s", (_label, enabledCompanies, message) => {
|
|
expect(() => parseManagedConfigEnv(envWith(validDoc(plugins(enabledCompanies))))).toThrow(message);
|
|
});
|
|
});
|
|
|
|
describe("managedFeatureKeySet", () => {
|
|
it("contains exactly the boolean flags of the experimental schema", () => {
|
|
const keys = managedFeatureKeySet();
|
|
expect(keys.has("enableApps")).toBe(true);
|
|
expect(keys.has("enableWorktreeRunExecution")).toBe(true);
|
|
// Server-managed bookkeeping fields are not overlayable features.
|
|
expect(keys.has("worktreeRunExecutionActivatedAt")).toBe(false);
|
|
expect(keys.has("worktreeRunExecutionActivationInstanceId")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("parseManagedConfigEnv", () => {
|
|
it("returns null when the env var is absent (self-hosted)", () => {
|
|
expect(parseManagedConfigEnv({})).toBeNull();
|
|
expect(parseManagedConfigEnv(envWith(undefined))).toBeNull();
|
|
});
|
|
|
|
it("throws when the env var is present but blank (fail closed)", () => {
|
|
expect(() => parseManagedConfigEnv(envWith(""))).toThrow(/is set but blank/);
|
|
expect(() => parseManagedConfigEnv(envWith(" "))).toThrow(/is set but blank/);
|
|
expect(() => parseManagedConfigEnv(envWith("\n\t"))).toThrow(/is set but blank/);
|
|
});
|
|
|
|
it("parses a complete valid document", () => {
|
|
const config = parseManagedConfigEnv(envWith(validDoc()));
|
|
expect(config).toEqual({
|
|
v: 1,
|
|
mode: "cloud",
|
|
catalogVersion: "2026.720.0",
|
|
features: { enableApps: false, enableStatusCards: true },
|
|
plugins: { autoInstall: ["daytona", "kubernetes"] },
|
|
environments: [],
|
|
pluginEnvironments: [],
|
|
});
|
|
});
|
|
|
|
it("accepts the deprecated runner ingress key for managed-config compatibility", () => {
|
|
const config = parseManagedConfigEnv(
|
|
envWith(validDoc({ features: { enableRunnerPreviewIngress: true } })),
|
|
);
|
|
|
|
expect(config?.features).toEqual({ enableRunnerPreviewIngress: true });
|
|
});
|
|
|
|
it("accepts empty features {} and autoInstall [] sections", () => {
|
|
const config = parseManagedConfigEnv(
|
|
envWith(validDoc({ features: {}, plugins: { autoInstall: [] } })),
|
|
);
|
|
expect(config).toEqual({
|
|
v: 1,
|
|
mode: "cloud",
|
|
catalogVersion: "2026.720.0",
|
|
features: {},
|
|
plugins: { autoInstall: [] },
|
|
environments: [],
|
|
pluginEnvironments: [],
|
|
});
|
|
});
|
|
|
|
it("throws when the features section is missing (fail closed)", () => {
|
|
const doc = { v: 1, mode: "cloud", catalogVersion: "2026.720.0", plugins: { autoInstall: [] } };
|
|
expect(() => parseManagedConfigEnv(envWith(JSON.stringify(doc)))).toThrow(
|
|
/requires a "features" object/,
|
|
);
|
|
});
|
|
|
|
it("throws when the plugins section or autoInstall is missing (fail closed)", () => {
|
|
const noPlugins = {
|
|
v: 1,
|
|
mode: "cloud",
|
|
catalogVersion: "2026.720.0",
|
|
features: {},
|
|
};
|
|
expect(() => parseManagedConfigEnv(envWith(JSON.stringify(noPlugins)))).toThrow(
|
|
/requires a "plugins" object/,
|
|
);
|
|
expect(() => parseManagedConfigEnv(envWith(validDoc({ plugins: {} })))).toThrow(
|
|
/requires a "plugins.autoInstall" array/,
|
|
);
|
|
});
|
|
|
|
it("returns a frozen document", () => {
|
|
const config = parseManagedConfigEnv(envWith(validDoc()));
|
|
expect(Object.isFrozen(config)).toBe(true);
|
|
expect(Object.isFrozen(config?.features)).toBe(true);
|
|
expect(Object.isFrozen(config?.plugins.autoInstall)).toBe(true);
|
|
});
|
|
|
|
it("throws on invalid JSON", () => {
|
|
expect(() => parseManagedConfigEnv(envWith("{not json"))).toThrow(
|
|
/PAPERCLIP_MANAGED_CONFIG is not valid JSON/,
|
|
);
|
|
});
|
|
|
|
it("throws on non-object documents", () => {
|
|
expect(() => parseManagedConfigEnv(envWith("[]"))).toThrow(/must be a JSON object/);
|
|
expect(() => parseManagedConfigEnv(envWith("42"))).toThrow(/must be a JSON object/);
|
|
expect(() => parseManagedConfigEnv(envWith("null"))).toThrow(/must be a JSON object/);
|
|
expect(() => parseManagedConfigEnv(envWith('"cloud"'))).toThrow(/must be a JSON object/);
|
|
});
|
|
|
|
it("throws on an unknown top-level key", () => {
|
|
expect(() => parseManagedConfigEnv(envWith(validDoc({ extra: true })))).toThrow(
|
|
/unknown top-level key "extra"/,
|
|
);
|
|
});
|
|
|
|
it("throws on an unsupported v", () => {
|
|
expect(() => parseManagedConfigEnv(envWith(validDoc({ v: 2 })))).toThrow(
|
|
/unsupported "v" 2; this build supports v=1/,
|
|
);
|
|
expect(() => parseManagedConfigEnv(envWith(validDoc({ v: "1" })))).toThrow(/unsupported "v"/);
|
|
expect(() =>
|
|
parseManagedConfigEnv(
|
|
envWith(JSON.stringify({ mode: "cloud", catalogVersion: "x" })),
|
|
),
|
|
).toThrow(/unsupported "v"/);
|
|
});
|
|
|
|
it("throws on a non-cloud mode", () => {
|
|
expect(() => parseManagedConfigEnv(envWith(validDoc({ mode: "self-hosted" })))).toThrow(
|
|
/invalid "mode" "self-hosted"; expected "cloud"/,
|
|
);
|
|
expect(() =>
|
|
parseManagedConfigEnv(envWith(JSON.stringify({ v: 1, catalogVersion: "x" }))),
|
|
).toThrow(/invalid "mode"/);
|
|
});
|
|
|
|
it("throws on a missing or empty catalogVersion", () => {
|
|
expect(() =>
|
|
parseManagedConfigEnv(envWith(JSON.stringify({ v: 1, mode: "cloud" }))),
|
|
).toThrow(/non-empty string "catalogVersion"/);
|
|
expect(() => parseManagedConfigEnv(envWith(validDoc({ catalogVersion: "" })))).toThrow(
|
|
/non-empty string "catalogVersion"/,
|
|
);
|
|
expect(() => parseManagedConfigEnv(envWith(validDoc({ catalogVersion: 7 })))).toThrow(
|
|
/non-empty string "catalogVersion"/,
|
|
);
|
|
});
|
|
|
|
it("throws on a non-object features section", () => {
|
|
expect(() => parseManagedConfigEnv(envWith(validDoc({ features: ["enableApps"] })))).toThrow(
|
|
/"features" must be an object/,
|
|
);
|
|
});
|
|
|
|
it.each([true, false])("ignores the retired Pipelines flag (%s) from older Cloud catalogs", (enabled) => {
|
|
const config = parseManagedConfigEnv(envWith(validDoc({
|
|
features: { enablePipelines: enabled, enableCases: true },
|
|
})));
|
|
expect(config?.features).toEqual({ enableCases: true });
|
|
expect(managedFeatureKeySet().has("enablePipelines")).toBe(false);
|
|
});
|
|
|
|
it("throws on an unknown feature key", () => {
|
|
expect(() =>
|
|
parseManagedConfigEnv(envWith(validDoc({ features: { enableTimeTravel: true } }))),
|
|
).toThrow(/unknown feature key "enableTimeTravel"/);
|
|
// A server-managed bookkeeping field is not an overlayable feature.
|
|
expect(() =>
|
|
parseManagedConfigEnv(
|
|
envWith(validDoc({ features: { worktreeRunExecutionActivatedAt: true } })),
|
|
),
|
|
).toThrow(/unknown feature key "worktreeRunExecutionActivatedAt"/);
|
|
});
|
|
|
|
it("throws on a feature key the catalog does not mark tier \"managed\"", () => {
|
|
// `enableStreamlinedLeftNavigation` is a real schema flag, but its catalog
|
|
// tier is `preference` (tenant-controllable) — a managed-config document
|
|
// targeting it has incompatible catalog semantics and must fail closed.
|
|
expect(() =>
|
|
parseManagedConfigEnv(
|
|
envWith(validDoc({ features: { enableStreamlinedLeftNavigation: true } })),
|
|
),
|
|
).toThrow(
|
|
/"features" key "enableStreamlinedLeftNavigation" has tier "preference".*only tier "managed" keys/,
|
|
);
|
|
expect(() =>
|
|
parseManagedConfigEnv(envWith(validDoc({ features: { enableDecisions: false } }))),
|
|
).toThrow(/has tier "preference"/);
|
|
});
|
|
|
|
it("throws on non-boolean feature values", () => {
|
|
expect(() =>
|
|
parseManagedConfigEnv(envWith(validDoc({ features: { enableApps: "true" } }))),
|
|
).toThrow(/"features.enableApps" must be a boolean/);
|
|
expect(() =>
|
|
parseManagedConfigEnv(envWith(validDoc({ features: { enableApps: 1 } }))),
|
|
).toThrow(/"features.enableApps" must be a boolean/);
|
|
expect(() =>
|
|
parseManagedConfigEnv(envWith(validDoc({ features: { enableApps: null } }))),
|
|
).toThrow(/"features.enableApps" must be a boolean/);
|
|
});
|
|
|
|
it("throws on malformed plugins sections", () => {
|
|
expect(() => parseManagedConfigEnv(envWith(validDoc({ plugins: [] })))).toThrow(
|
|
/"plugins" must be an object/,
|
|
);
|
|
expect(() =>
|
|
parseManagedConfigEnv(envWith(validDoc({ plugins: { install: [] } }))),
|
|
).toThrow(/"plugins" has unknown key "install"/);
|
|
expect(() =>
|
|
parseManagedConfigEnv(envWith(validDoc({ plugins: { autoInstall: "daytona" } }))),
|
|
).toThrow(/"plugins.autoInstall" must be an array/);
|
|
expect(() =>
|
|
parseManagedConfigEnv(envWith(validDoc({ plugins: { autoInstall: [""] } }))),
|
|
).toThrow(/non-empty strings/);
|
|
expect(() =>
|
|
parseManagedConfigEnv(envWith(validDoc({ plugins: { autoInstall: [" daytona"] } }))),
|
|
).toThrow(/non-empty strings/);
|
|
expect(() =>
|
|
parseManagedConfigEnv(envWith(validDoc({ plugins: { autoInstall: [42] } }))),
|
|
).toThrow(/non-empty strings/);
|
|
expect(() =>
|
|
parseManagedConfigEnv(
|
|
envWith(validDoc({ plugins: { autoInstall: ["daytona", "daytona"] } })),
|
|
),
|
|
).toThrow(/duplicate entry "daytona"/);
|
|
});
|
|
});
|
|
|
|
describe("parseManagedConfigEnv environments section", () => {
|
|
const entry = (overrides: Record<string, unknown> = {}) => ({
|
|
name: "Daytona",
|
|
provider: "daytona",
|
|
config: { target: "us" },
|
|
...overrides,
|
|
});
|
|
|
|
it("defaults to an empty list when the section is absent (pre-section documents keep booting)", () => {
|
|
const config = parseManagedConfigEnv(envWith(validDoc()));
|
|
expect(config?.environments).toEqual([]);
|
|
});
|
|
|
|
it("parses a declared environment and freezes it", () => {
|
|
const config = parseManagedConfigEnv(
|
|
envWith(validDoc({ environments: [entry({ description: "Managed Daytona sandbox." })] })),
|
|
);
|
|
expect(config?.environments).toHaveLength(1);
|
|
const spec = config?.environments[0];
|
|
expect(spec?.name).toBe("Daytona");
|
|
expect(spec?.description).toBe("Managed Daytona sandbox.");
|
|
expect(spec?.provider).toBe("daytona");
|
|
expect(spec?.config).toEqual({ target: "us" });
|
|
expect(Object.isFrozen(config?.environments)).toBe(true);
|
|
expect(Object.isFrozen(spec)).toBe(true);
|
|
expect(Object.isFrozen(spec?.config)).toBe(true);
|
|
});
|
|
|
|
it("treats config and description as optional", () => {
|
|
const config = parseManagedConfigEnv(
|
|
envWith(validDoc({ environments: [{ name: "Daytona", provider: "daytona" }] })),
|
|
);
|
|
expect(config?.environments[0]?.config).toEqual({});
|
|
expect(config?.environments[0]?.description).toBeUndefined();
|
|
});
|
|
|
|
it("rejects a non-array section and non-object entries", () => {
|
|
expect(() =>
|
|
parseManagedConfigEnv(envWith(validDoc({ environments: {} }))),
|
|
).toThrow(/"environments" must be an array/);
|
|
expect(() =>
|
|
parseManagedConfigEnv(envWith(validDoc({ environments: ["daytona"] }))),
|
|
).toThrow(/"environments\[0\]" must be an object/);
|
|
});
|
|
|
|
it("rejects more than one sandbox entry (single managed sandbox slot)", () => {
|
|
expect(() =>
|
|
parseManagedConfigEnv(
|
|
envWith(validDoc({ environments: [entry(), entry({ name: "Other", provider: "kubernetes" })] })),
|
|
),
|
|
).toThrow(/at most one sandbox entry/);
|
|
expect(() =>
|
|
parseManagedConfigEnv(
|
|
envWith(validDoc({ environments: [entry(), entry({ name: "Other", driver: "sandbox", provider: "kubernetes" })] })),
|
|
),
|
|
).toThrow(/at most one sandbox entry/);
|
|
});
|
|
|
|
it("rejects unknown entry keys", () => {
|
|
expect(() =>
|
|
parseManagedConfigEnv(envWith(validDoc({ environments: [entry({ envVars: {} })] }))),
|
|
).toThrow(/unknown key "envVars"/);
|
|
});
|
|
|
|
it("rejects malformed names, descriptions, and providers", () => {
|
|
expect(() =>
|
|
parseManagedConfigEnv(envWith(validDoc({ environments: [entry({ name: "" })] }))),
|
|
).toThrow(/"environments\[0\].name"/);
|
|
expect(() =>
|
|
parseManagedConfigEnv(envWith(validDoc({ environments: [entry({ name: " Daytona" })] }))),
|
|
).toThrow(/"environments\[0\].name"/);
|
|
expect(() =>
|
|
parseManagedConfigEnv(envWith(validDoc({ environments: [entry({ description: " " })] }))),
|
|
).toThrow(/"environments\[0\].description"/);
|
|
expect(() =>
|
|
parseManagedConfigEnv(envWith(validDoc({ environments: [entry({ provider: 7 })] }))),
|
|
).toThrow(/"environments\[0\].provider"/);
|
|
});
|
|
|
|
it("rejects a provider that plugins.autoInstall does not provision", () => {
|
|
expect(() =>
|
|
parseManagedConfigEnv(envWith(validDoc({ environments: [entry({ provider: "modal" })] }))),
|
|
).toThrow(/not in "plugins.autoInstall"/);
|
|
});
|
|
|
|
it("rejects a config that sets provider", () => {
|
|
expect(() =>
|
|
parseManagedConfigEnv(
|
|
envWith(validDoc({ environments: [entry({ config: { provider: "daytona" } })] })),
|
|
),
|
|
).toThrow(/must not set "provider"/);
|
|
});
|
|
|
|
it("rejects secret-bearing config keys at any depth (secrets travel as env vars)", () => {
|
|
expect(() =>
|
|
parseManagedConfigEnv(
|
|
envWith(validDoc({ environments: [entry({ config: { apiKey: "not-a-real-key" } })] })),
|
|
),
|
|
).toThrow(/looks secret-bearing/);
|
|
expect(() =>
|
|
parseManagedConfigEnv(
|
|
envWith(validDoc({ environments: [entry({ config: { auth: { accessToken: "t" } } })] })),
|
|
),
|
|
).toThrow(/auth.accessToken/);
|
|
expect(() =>
|
|
parseManagedConfigEnv(
|
|
envWith(
|
|
validDoc({ environments: [entry({ config: { adapters: [{ clientSecret: "s" }] } })] }),
|
|
),
|
|
),
|
|
).toThrow(/adapters\[0\].clientSecret/);
|
|
expect(() =>
|
|
parseManagedConfigEnv(
|
|
envWith(validDoc({ environments: [entry({ config: { pools: [[], [{ privateKey: { password: "s" } }]] } })] })),
|
|
),
|
|
).toThrow(/pools\[1\]\[0\].privateKey.password/);
|
|
});
|
|
});
|
|
|
|
describe("parseManagedConfigEnv plugin environment entries", () => {
|
|
const PLUGIN_KEY = "example.remote-host-plugin";
|
|
const remoteHost = (overrides: Record<string, unknown> = {}) => ({
|
|
name: "Remote Host",
|
|
description: "Runs the agent on a remote host",
|
|
driver: "plugin",
|
|
pluginKey: PLUGIN_KEY,
|
|
driverKey: "tasks",
|
|
driverConfig: {},
|
|
...overrides,
|
|
});
|
|
const sandbox = { name: "Managed Sandbox", provider: "daytona", config: { target: "us" } };
|
|
// `plugins.autoInstall` holds catalog keys ("remote-host"); the entry's
|
|
// `pluginKey` is the plugin's manifest id, which boot maps through the catalog.
|
|
const docWith = (environments: unknown[], autoInstall = ["daytona", "remote-host"]) =>
|
|
envWith(validDoc({ plugins: { autoInstall }, environments }));
|
|
|
|
it("lists the drivers an entry may declare", () => {
|
|
expect(MANAGED_ENVIRONMENT_DRIVERS).toEqual(["sandbox", "plugin"]);
|
|
});
|
|
|
|
it("parses an old document's sandbox entry unchanged", () => {
|
|
const config = parseManagedConfigEnv(docWith([sandbox]));
|
|
expect(config?.environments).toEqual([{ name: "Managed Sandbox", provider: "daytona", config: { target: "us" } }]);
|
|
expect(config?.pluginEnvironments).toEqual([]);
|
|
});
|
|
|
|
it("parses the sandbox entry beside a plugin entry", () => {
|
|
const config = parseManagedConfigEnv(docWith([sandbox, remoteHost()]));
|
|
expect(config?.environments).toEqual([{ name: "Managed Sandbox", provider: "daytona", config: { target: "us" } }]);
|
|
expect(config?.pluginEnvironments).toEqual([{
|
|
driver: "plugin",
|
|
name: "Remote Host",
|
|
description: "Runs the agent on a remote host",
|
|
pluginKey: PLUGIN_KEY,
|
|
driverKey: "tasks",
|
|
driverConfig: {},
|
|
}]);
|
|
expect(Object.isFrozen(config?.pluginEnvironments)).toBe(true);
|
|
expect(Object.isFrozen(config?.pluginEnvironments[0])).toBe(true);
|
|
});
|
|
|
|
it("parses a plugin entry alone, with optional fields omitted", () => {
|
|
const config = parseManagedConfigEnv(docWith([{ name: "Remote", driver: "plugin", pluginKey: PLUGIN_KEY, driverKey: "tasks" }]));
|
|
expect(config?.environments).toEqual([]);
|
|
expect(config?.pluginEnvironments[0]).toMatchObject({ driverConfig: {} });
|
|
expect(config?.pluginEnvironments[0]?.description).toBeUndefined();
|
|
});
|
|
|
|
it("does not compare the manifest id with autoInstall catalog keys (boot checks the catalog)", () => {
|
|
expect(parseManagedConfigEnv(docWith([remoteHost()]))?.pluginEnvironments[0]?.pluginKey).toBe(PLUGIN_KEY);
|
|
});
|
|
|
|
it("refuses duplicate names, including across drivers", () => {
|
|
expect(() => parseManagedConfigEnv(docWith([sandbox, remoteHost({ name: "Managed Sandbox" })])))
|
|
.toThrow(/"environments\[1\].name" duplicates another entry's name/);
|
|
});
|
|
|
|
it("refuses a duplicate pluginKey and driverKey pair", () => {
|
|
expect(() => parseManagedConfigEnv(docWith([remoteHost(), remoteHost({ name: "Second" })])))
|
|
.toThrow(/duplicates another plugin entry's pluginKey and driverKey/);
|
|
});
|
|
|
|
it("refuses secret-like driverConfig keys at any depth", () => {
|
|
expect(() => parseManagedConfigEnv(docWith([remoteHost({ driverConfig: { apiToken: "t" } })])))
|
|
.toThrow(/"environments\[0\].driverConfig" key "apiToken" looks secret-bearing/);
|
|
expect(() => parseManagedConfigEnv(docWith([remoteHost({ driverConfig: { auth: [{ password: "p" }] } })])))
|
|
.toThrow(/auth\[0\].password/);
|
|
expect(() => parseManagedConfigEnv(docWith([remoteHost({ driverConfig: { groups: [[{ apiToken: "t" }]] } })])))
|
|
.toThrow(/groups\[0\]\[0\].apiToken/);
|
|
});
|
|
|
|
it("refuses unknown keys, including sandbox keys on a plugin entry and the reverse", () => {
|
|
expect(() => parseManagedConfigEnv(docWith([remoteHost({ envVars: {} })]))).toThrow(/unknown key "envVars"/);
|
|
expect(() => parseManagedConfigEnv(docWith([remoteHost({ provider: "daytona" })]))).toThrow(/unknown key "provider"/);
|
|
expect(() => parseManagedConfigEnv(docWith([{ ...sandbox, pluginKey: PLUGIN_KEY }]))).toThrow(/unknown key "pluginKey"/);
|
|
// Not supported by this build on either entry.
|
|
expect(() => parseManagedConfigEnv(docWith([remoteHost({ instanceDefault: true })]))).toThrow(/unknown key "instanceDefault"/);
|
|
expect(() => parseManagedConfigEnv(docWith([{ ...sandbox, instanceDefault: false }]))).toThrow(/unknown key "instanceDefault"/);
|
|
});
|
|
|
|
it("refuses an unknown driver and malformed plugin fields", () => {
|
|
expect(() => parseManagedConfigEnv(docWith([remoteHost({ driver: "ssh" })]))).toThrow(/"environments\[0\].driver" must be one of "sandbox", "plugin"/);
|
|
expect(() => parseManagedConfigEnv(docWith([{ ...sandbox, driver: null }]))).toThrow(/"environments\[0\].driver" must be one of "sandbox", "plugin" when present \(got null\)/);
|
|
expect(() => parseManagedConfigEnv(docWith([remoteHost({ driverKey: "Tasks" })]))).toThrow(/"environments\[0\].driverKey" must start/);
|
|
expect(() => parseManagedConfigEnv(docWith([remoteHost({ driverKey: undefined })]))).toThrow(/"environments\[0\].driverKey" must be a non-empty string/);
|
|
expect(() => parseManagedConfigEnv(docWith([remoteHost({ driverConfig: [] })]))).toThrow(/"environments\[0\].driverConfig" must be an object/);
|
|
});
|
|
});
|
|
|
|
describe("getManagedInstanceConfig", () => {
|
|
it("caches by raw env value and reparses when it changes", () => {
|
|
const raw = validDoc();
|
|
const first = getManagedInstanceConfig(envWith(raw));
|
|
const second = getManagedInstanceConfig(envWith(raw));
|
|
expect(second).toBe(first);
|
|
|
|
const changed = getManagedInstanceConfig(
|
|
envWith(validDoc({ catalogVersion: "2026.721.0" })),
|
|
);
|
|
expect(changed?.catalogVersion).toBe("2026.721.0");
|
|
expect(changed).not.toBe(first);
|
|
|
|
expect(getManagedInstanceConfig(envWith(undefined))).toBeNull();
|
|
});
|
|
|
|
it("rethrows parse failures on every call instead of caching them", () => {
|
|
expect(() => getManagedInstanceConfig(envWith("{bad"))).toThrow(/not valid JSON/);
|
|
expect(() => getManagedInstanceConfig(envWith("{bad"))).toThrow(/not valid JSON/);
|
|
});
|
|
});
|