Skip to content

Commit c00849d

Browse files
committed
feat(byoc): runtime config, sessions and vaults updates
Change-Id: I5d340bb58f1d0d9a68607024aa49fe0b37c1eab6 Co-developed-by: Qoder <noreply@qoder.com>
1 parent f00cad2 commit c00849d

27 files changed

Lines changed: 195 additions & 148 deletions

File tree

apps/server/src/lib/build-runtime-config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ export async function buildRuntimeConfig(): Promise<LoadedProjectConfig> {
3333
// Every provider provisions a cloud sandbox environment; bailian additionally installs
3434
// `bailian-cli` in it. The vault is provider-conditional: bailian holds its DASHSCOPE_API_KEY;
3535
// other providers currently define no vault.
36+
//
37+
// Metadata stamps (`agents.base` / `agents.vault`) let the webui identify managed base
38+
// resources via remote listing (findBaseEnvironment / findBaseVault), complementing the
39+
// state-tracked identity the plan/apply engine provides.
3640
const vaults = vault
3741
? {
3842
[vault.name]: {
@@ -45,13 +49,15 @@ export async function buildRuntimeConfig(): Promise<LoadedProjectConfig> {
4549
secret_value: requireEnv(cred.secret_name),
4650
...(cred.networking ? { networking: cred.networking } : {}),
4751
})),
52+
metadata: { "agents.vault": "true" },
4853
},
4954
}
5055
: {};
5156
const environments = {
5257
[environment.name]: {
5358
...(environment.description ? { description: environment.description } : {}),
5459
config: environment.config,
60+
metadata: { "agents.base": "true" },
5561
},
5662
};
5763

apps/server/src/lib/state-scope.ts

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,75 @@
1-
import { resolve } from "node:path";
1+
import { copyFileSync, existsSync, mkdirSync, renameSync } from "node:fs";
2+
import { homedir } from "node:os";
3+
import { dirname, join, resolve } from "node:path";
24
import { LocalFileStateBackend, type StateScope } from "@openagentpack/sdk";
35
import { RUNTIME_PROJECT_NAME } from "@/lib/build-runtime-config";
46

5-
// Historical on-disk state anchor. Previously derived via deriveStatePath() from the
6-
// demo file examples/bailian/bailian-cli/agents.yaml → agents.state.json. We pin the default
7-
// to that exact location so previously provisioned remote_ids (agents/vault/environment)
8-
// keep resolving and are not re-created. Override with AGENTS_STATE_PATH.
9-
const DEFAULT_STATE_PATH = "examples/bailian/bailian-cli/agents.state.json";
7+
/**
8+
* Default playground state lives in the user home directory, alongside the
9+
* provider config (~/.agents/config.json). This avoids colliding with CLI
10+
* example configs that happen to live in the repo's examples/ directory.
11+
*
12+
* Override with AGENTS_STATE_PATH for custom deployment layouts.
13+
*/
14+
const DEFAULT_STATE_PATH = join(homedir(), ".agents", "playground.state.json");
15+
16+
/**
17+
* Legacy state path. Before this migration the server stored its state inside
18+
* the repo's example directory. We migrate it once so previously-provisioned
19+
* remote_ids are preserved.
20+
*/
21+
const LEGACY_STATE_PATH = "examples/bailian/bailian-cli/agents.state.json";
22+
23+
let migrationChecked = false;
24+
25+
/**
26+
* One-time migration: if the legacy state file exists and the new default does
27+
* not, copy it over and log a warning. Idempotent — subsequent calls after a
28+
* successful migration (or when no migration is needed) are no-ops. A failed
29+
* copy allows retry on the next call so transient I/O errors don't permanently
30+
* disable migration for the process lifetime.
31+
*/
32+
function ensureMigrated(newPath: string, cwd: string): void {
33+
if (migrationChecked) return;
34+
35+
if (existsSync(newPath)) {
36+
migrationChecked = true;
37+
return;
38+
}
39+
const legacyPath = resolve(cwd, LEGACY_STATE_PATH);
40+
if (!existsSync(legacyPath)) {
41+
migrationChecked = true;
42+
return;
43+
}
44+
45+
try {
46+
mkdirSync(dirname(newPath), { recursive: true });
47+
copyFileSync(legacyPath, newPath);
48+
// Rename the legacy file so a version rollback won't silently read stale state.
49+
try {
50+
renameSync(legacyPath, `${legacyPath}.migrated`);
51+
} catch {
52+
// Non-fatal: the copy succeeded, state is in the new location.
53+
}
54+
migrationChecked = true;
55+
console.warn(
56+
`[state] Migrated playground state from legacy path:\n` +
57+
` ${legacyPath}\n` +
58+
` → ${newPath}\n` +
59+
` The legacy file has been renamed to ${legacyPath}.migrated.`,
60+
);
61+
} catch (error) {
62+
// Don't set migrationChecked — allow retry on next call.
63+
console.warn(`[state] Failed to migrate legacy state file: ${error instanceof Error ? error.message : error}`);
64+
}
65+
}
1066

1167
export function resolveStatePath(env: NodeJS.ProcessEnv = process.env, cwd: string = process.cwd()): string {
1268
const configured = env.AGENTS_STATE_PATH?.trim();
13-
return configured ? resolve(cwd, configured) : resolve(cwd, DEFAULT_STATE_PATH);
69+
if (configured) return resolve(cwd, configured);
70+
71+
ensureMigrated(DEFAULT_STATE_PATH, cwd);
72+
return DEFAULT_STATE_PATH;
1473
}
1574

1675
export function deriveWebUiStateScope(): StateScope {

apps/server/src/routes/vaults.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ vaultsRoute.openapi(createVaultRoute, async (c) => {
6868
const secretValue = key ?? (primarySecret ? requireEnv(primarySecret) : "");
6969
const vault = await withAgentRuntime(DEFAULT_AGENT_ID, (ctx) =>
7070
createCloudVault(ctx, name, {
71-
// display_name carries the base-vault identity (Agents/secrets) — a vault has no
72-
// separate `name` field, so findBaseVault nets it by display_name + the stamp.
71+
// display_name is used as the vault's human-readable label on the provider.
72+
// findBaseVault identifies the managed vault by its metadata stamp (agents.vault).
7373
display_name: name,
7474
metadata,
7575
credentials: structure.credentials.map((cred) => ({

apps/server/src/schemas/sessions.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const SessionDeleteResponseSchema = z
3838
})
3939
.openapi("SessionDeleteResponse");
4040

41-
// Mode A REST request ergonomics (server-owned input, not the shared DTO).
41+
// REST request ergonomics (server-owned input, not the shared DTO).
4242
export const SessionsQuerySchema = z.object({
4343
// Non-numeric values resolve to `undefined` so the handler clamps to a
4444
// default instead of the request being rejected with a 400.
@@ -71,8 +71,7 @@ export const SessionParamsSchema = z.object({
7171
export const CreateSessionBodySchema = z.object({
7272
agentId: z.string(),
7373
prompt: z.string().min(1),
74-
// Required: a session must be pinned to a cloud environment (sandbox). Both transports
75-
// enforce this so Mode A (REST/OpenAPI) and Mode B (console) reject env-less creates.
74+
// Required: a session must be pinned to a cloud environment (sandbox).
7675
environmentId: z.string().min(1),
7776
// Optional: cloud vault ids to bind a user-supplied credential so the sandbox receives it.
7877
// Top-level binding shape matches the console createSession.

apps/server/src/schemas/vaults.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ export const CloudVaultsResponseSchema = z.object({
1010
export const CreateVaultBodySchema = z.object({
1111
name: z.string().min(1),
1212
metadata: z.record(z.string(), z.string()).optional(),
13-
// The DASHSCOPE_API_KEY stored as the vault's credential secret value. Optional: Mode B
14-
// supplies the user's key; Mode A (local) omits it and the server injects it from its own
15-
// DASHSCOPE_API_KEY env. (Mode B never reaches this REST route — it goes via console RPC.)
13+
// The DASHSCOPE_API_KEY stored as the vault's credential secret value. Optional:
14+
// when omitted the server injects it from its own DASHSCOPE_API_KEY env.
1615
key: z.string().min(1).optional(),
1716
});
1817

apps/server/src/services/agents/catalog.ts

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
DEFAULT_PLAYBOOK_PROVIDER,
44
getDefaultPlaybook,
55
getPlaybook,
6+
getVaultProfile,
67
type PlaybookTemplate,
78
type ResolvedPlaybook,
89
resolvePlaybookModel,
@@ -80,9 +81,10 @@ export function compileAgentRuntime(
8081
const config = cloneConfig(baseConfig);
8182
const resolved = resolveSeedPlaybook(effectiveId, provider);
8283
const runtimeAgentId = resolved.agent.name;
83-
const built = buildAgentDecl(undefined, toAgentBuildInput(resolved, provider, modelOverride));
84-
// The agent declares neither environment nor vault: base resources are provisioned eagerly
85-
// and bound per-session via explicit environment_id + vault_ids.
84+
const built = buildAgentDecl(undefined, toAgentBuildInput(resolved, provider, baseConfig, modelOverride));
85+
// The agent declares environment and vault so syncAgentResources manages them
86+
// through the plan/apply engine — giving base resources state tracking, drift
87+
// detection, and the same content-hash identity as agent/skill resources.
8688

8789
config.agents = {
8890
...(config.agents ?? {}),
@@ -154,12 +156,45 @@ export function computeAgentConfigHash(config: ResolvedProjectConfig, agentId: s
154156
return createHash("sha256").update(stableStringify({ agentId, config })).digest("hex").slice(0, 16);
155157
}
156158

157-
function toAgentBuildInput(resolved: ResolvedPlaybook, provider: string, modelOverride?: string): AgentBuildInput {
159+
function toAgentBuildInput(
160+
resolved: ResolvedPlaybook,
161+
provider: string,
162+
baseConfig: ResolvedProjectConfig,
163+
modelOverride?: string,
164+
): AgentBuildInput {
158165
const model = resolvePlaybookModel(resolved, provider, modelOverride);
166+
// Resolve environment and vault names from the assembled config so the compiled
167+
// agent declares them. This lets syncAgentResources manage base resources
168+
// through the plan/apply engine, giving them state tracking and drift detection.
169+
// Invariant: buildRuntimeConfig produces exactly one environment (and at most one
170+
// vault for providers that need credentials). If this assumption breaks, the agent
171+
// would silently bind the wrong resource — fail fast instead.
172+
const envKeys = Object.keys(baseConfig.environments ?? {});
173+
if (envKeys.length !== 1) {
174+
throw new Error(
175+
`Expected exactly 1 environment in runtime config, got ${envKeys.length}. ` +
176+
`The agent compile path assumes a single base environment per provider.`,
177+
);
178+
}
179+
const environmentName = envKeys[0]!;
180+
const vaultProfile = getVaultProfile(provider);
181+
let vaultName: string | undefined;
182+
if (vaultProfile) {
183+
const vaultKeys = Object.keys(baseConfig.vaults ?? {});
184+
if (vaultKeys.length !== 1) {
185+
throw new Error(
186+
`Expected exactly 1 vault in runtime config for provider '${provider}', got ${vaultKeys.length}. ` +
187+
`The agent compile path assumes a single base vault per provider.`,
188+
);
189+
}
190+
vaultName = vaultKeys[0]!;
191+
}
159192
return {
160193
description: resolved.agent.description,
161194
model,
162195
instructions: resolved.agent.system,
196+
environment: environmentName,
197+
vault: vaultName,
163198
provider,
164199
builtinTools: resolved.agent.builtinTools,
165200
skills: resolved.agent.skills.map((skill) =>

apps/server/src/services/files/upload.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,7 @@ export async function listUserFiles(): Promise<ProviderFileInfo[]> {
4747

4848
/**
4949
* Resolve a short-lived presigned download URL for a file (e.g. an agent-delivered artifact).
50-
* Throws when the resolved provider has no download endpoint (bailian/claude); the webui's Mode A
51-
* transport surfaces that as an error, while Mode B never calls this route.
50+
* Throws when the resolved provider has no download endpoint (bailian/claude).
5251
*/
5352
export async function getUserFileDownloadUrl(id: string): Promise<{ url: string; expires_at?: string }> {
5453
return withAgentRuntime(DEFAULT_AGENT_ID, async (ctx, compiled) => {

apps/server/src/services/sessions/playbook-session-adapter/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ import {
2626
sendPlaybookSessionMessage,
2727
} from "./sessions";
2828

29-
export type { ModeAPlaybookSessionDetail } from "./sessions";
29+
export type { PlaybookSessionDetail } from "./sessions";
3030

31-
export function createModeAPlaybookSessionRuntime() {
32-
return createPlaybookSessionRuntime<ModeAPlaybookSessionDetail, ProviderSessionEvent, Session, RemotePlaybookAgent>({
31+
export function createServerPlaybookSessionRuntime() {
32+
return createPlaybookSessionRuntime<PlaybookSessionDetail, ProviderSessionEvent, Session, RemotePlaybookAgent>({
3333
identity: {
3434
appId: getPlaybookAppId(),
3535
expectedAgentName: getSeedPlaybookAgentName,
@@ -95,4 +95,4 @@ export function createModeAPlaybookSessionRuntime() {
9595
});
9696
}
9797

98-
type ModeAPlaybookSessionDetail = import("./sessions").ModeAPlaybookSessionDetail;
98+
type PlaybookSessionDetail = import("./sessions").PlaybookSessionDetail;

apps/server/src/services/sessions/playbook-session-adapter/sessions.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,26 @@ import { withAgentRuntime } from "@/services/runtime-factory";
1616
import { createEventBuffer, seedCompletedBuffer } from "@/services/sessions/event-buffer";
1717
import { sortByUpdatedDesc, toSession } from "./dto";
1818

19-
export type ModeAPlaybookSessionDetail = {
19+
export type PlaybookSessionDetail = {
2020
session: Session;
2121
events: ProviderSessionEvent[];
2222
eventsNextPageToken?: string;
2323
};
2424

25-
export type ModeASessionEventsPage = {
25+
export type SessionEventsPage = {
2626
events: ProviderSessionEvent[];
2727
eventsNextPageToken?: string;
2828
};
2929

30-
type ModeAListPlaybookSessionsInput = {
30+
type ListPlaybookSessionsInput = {
3131
playbookId?: string;
3232
remoteAgentId?: string;
3333
limit?: number;
3434
pageToken?: string;
3535
};
3636

3737
export async function listPlaybookSessions(
38-
input: ModeAListPlaybookSessionsInput,
38+
input: ListPlaybookSessionsInput,
3939
): Promise<{ sessions: Session[]; nextPageToken?: string }> {
4040
const limit = input.limit ?? 50;
4141
const page = input.pageToken?.trim() || undefined;
@@ -77,7 +77,7 @@ async function listProviderSessionEventsPage(
7777
sessionId: string,
7878
provider: string,
7979
options: { pageToken?: string; limit?: number } = {},
80-
): Promise<ModeASessionEventsPage> {
80+
): Promise<SessionEventsPage> {
8181
const limit = options.limit ?? 100;
8282
const eventList = await listSessionEvents(ctx, sessionId, {
8383
provider,
@@ -96,7 +96,7 @@ export async function listProviderSessionEvents(
9696
agentId = DEFAULT_AGENT_ID,
9797
pageToken?: string,
9898
limit?: number,
99-
): Promise<ModeASessionEventsPage> {
99+
): Promise<SessionEventsPage> {
100100
const catalogAgentId = getSessionAgent(agentId) ? agentId : DEFAULT_AGENT_ID;
101101
return withAgentRuntime(catalogAgentId, async (ctx, compiled) => {
102102
const agent = getAgent(ctx, compiled.agentId);
@@ -107,7 +107,7 @@ export async function listProviderSessionEvents(
107107
export async function readProviderSessionDetail(
108108
sessionId: string,
109109
agentId = DEFAULT_AGENT_ID,
110-
): Promise<ModeAPlaybookSessionDetail> {
110+
): Promise<PlaybookSessionDetail> {
111111
const catalogAgentId = getSessionAgent(agentId) ? agentId : DEFAULT_AGENT_ID;
112112
return withAgentRuntime(catalogAgentId, async (ctx, compiled) => {
113113
const agent = getAgent(ctx, compiled.agentId);

apps/server/src/services/sessions/runner.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ import {
1616
import { DEFAULT_AGENT_ID, getSessionAgent } from "@/services/agents/catalog";
1717
import { loadAgentRuntimeInput, withAgentRuntime } from "@/services/runtime-factory";
1818
import {
19-
createModeAPlaybookSessionRuntime,
20-
type ModeAPlaybookSessionDetail,
19+
createServerPlaybookSessionRuntime,
20+
type PlaybookSessionDetail,
2121
} from "@/services/sessions/playbook-session-adapter";
2222
import { listProviderSessionEvents } from "@/services/sessions/playbook-session-adapter/sessions";
2323

@@ -43,9 +43,9 @@ export async function updatePlaybookAgentModel(slug: string, model: string): Pro
4343
import { createEventBuffer, seedCompletedBuffer } from "@/services/sessions/event-buffer";
4444

4545
/** A session plus its raw provider events (events are mapped to the contract at the route boundary). */
46-
export type SessionWithEvents = ModeAPlaybookSessionDetail;
46+
export type SessionWithEvents = PlaybookSessionDetail;
4747

48-
const playbookSessionRuntime = createModeAPlaybookSessionRuntime();
48+
const playbookSessionRuntime = createServerPlaybookSessionRuntime();
4949

5050
export async function listSessionsForAgent(input: {
5151
agentId?: string;

0 commit comments

Comments
 (0)