refactor(managed-agent)!: replace Git versioning with local snapshots

Replace @openagentpack/local-git with @openagentpack/project-versions so
the CLI and Workbench share a Git-independent, lock-protected YAML
snapshot store.

- create versions only after a successful Apply
- migrate version commands to local version IDs
- remove Git project scaffolding and CI-specific Apply policy
- update tests and managed-agent skill references

BREAKING CHANGE: remove `managed-agent init --git` and
`managed-agent apply --ci`; version preview and restore now use
`--version-id` instead of `--commit`.
This commit is contained in:
chenanran555
2026-08-26 18:00:05 +08:00
parent b93e0d586d
commit 9469556671
13 changed files with 347 additions and 1093 deletions
+1 -1
View File
@@ -40,7 +40,7 @@
"check": "vp check"
},
"dependencies": {
"@openagentpack/local-git": "0.4.0",
"@openagentpack/project-versions": "0.4.0",
"@openagentpack/sdk": "0.4.0",
"bailian-cli-core": "workspace:*",
"bailian-cli-runtime": "workspace:*",
@@ -1,505 +0,0 @@
import { execFile } from "node:child_process";
import { existsSync } from "node:fs";
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
import { basename, resolve } from "node:path";
import { promisify } from "node:util";
import { BailianError, ExitCode } from "bailian-cli-core";
const execFileAsync = promisify(execFile);
const REPOSITORY_GITIGNORE = `# Dependencies
node_modules/
# Bailian CLI local runs
.openagentpack/state/
.openagentpack/runs/
# Local credentials
.env
.env.*
!.env.example
`;
const REPOSITORY_GITIGNORE_PATTERNS = [
"node_modules/",
".openagentpack/state/",
".openagentpack/runs/",
".env",
".env.*",
"!.env.example",
] as const;
const PROJECT_SCRIPTS = {
"agents:validate": "bl managed-agent validate --file agents.yaml",
"agents:plan": "bl managed-agent plan --file agents.yaml",
"agents:plan:ci": "bl managed-agent plan --file agents.yaml --output json",
"agents:apply:ci": "bl managed-agent apply --file agents.yaml --ci",
"agents:workbench": "bl managed-agent workbench --file agents.yaml",
} as const;
const INITIAL_STATE = `${JSON.stringify({ resources: [] }, null, 2)}\n`;
export interface GitProjectResult {
targetDirectory: string;
mode: "created" | "upgraded";
initializedGit: boolean;
createdFiles: string[];
updatedFiles: string[];
preservedFiles: string[];
}
interface CreateGitProjectOptions {
config: string;
cliVersion: string;
}
type ProjectTargetMode = "new" | "existing";
export async function inspectGitProjectTarget(targetDirectory: string): Promise<ProjectTargetMode> {
if (!existsSync(targetDirectory)) return "new";
const targetStat = await stat(targetDirectory);
if (!targetStat.isDirectory()) {
throw new BailianError(`Target '${targetDirectory}' is not a directory.`, ExitCode.USAGE);
}
const entries = await readdir(targetDirectory);
if (entries.length === 0) return "new";
if (existsSync(resolve(targetDirectory, "agents.yaml"))) return "existing";
throw new BailianError(
`Target directory '${targetDirectory}' is not empty and does not contain agents.yaml.`,
ExitCode.USAGE,
);
}
export async function createGitProject(
directory: string,
options: CreateGitProjectOptions,
): Promise<GitProjectResult> {
const targetDirectory = resolve(directory);
const targetMode = await inspectGitProjectTarget(targetDirectory);
const shouldInitializeGit = !existsSync(resolve(targetDirectory, ".git"));
if (shouldInitializeGit) await assertGitAvailable();
const createdFiles: string[] = [];
const updatedFiles: string[] = [];
const preservedFiles: string[] = [];
await mkdir(resolve(targetDirectory, ".aoneci"), { recursive: true });
const config =
targetMode === "new"
? options.config
: await readFile(resolve(targetDirectory, "agents.yaml"), "utf8");
if (targetMode === "new") {
await writeFile(resolve(targetDirectory, "agents.yaml"), config, "utf8");
createdFiles.push("agents.yaml");
} else {
preservedFiles.push("agents.yaml");
}
await mergeOrCreateTextFile(
resolve(targetDirectory, ".gitignore"),
REPOSITORY_GITIGNORE,
mergeGitignore,
".gitignore",
createdFiles,
updatedFiles,
);
await mergeOrCreateTextFile(
resolve(targetDirectory, ".env.example"),
environmentExample(config),
(current) => mergeEnvironmentExample(current, config),
".env.example",
createdFiles,
updatedFiles,
);
const packagePath = resolve(targetDirectory, "package.json");
if (existsSync(packagePath)) {
const current = await readFile(packagePath, "utf8");
const merged = mergePackageJson(current, basename(targetDirectory), options.cliVersion);
if (merged.content !== current) {
await writeFile(packagePath, merged.content, "utf8");
updatedFiles.push("package.json");
}
preservedFiles.push(...merged.preservedSettings);
} else {
await writeFile(
packagePath,
buildPackageJson(basename(targetDirectory), options.cliVersion),
"utf8",
);
createdFiles.push("package.json");
}
await createIfMissing(
resolve(targetDirectory, "agents.state.json"),
INITIAL_STATE,
"agents.state.json",
createdFiles,
preservedFiles,
);
await createIfMissing(
resolve(targetDirectory, ".aoneci/bailian-cli.yml"),
buildAoneWorkflow(config),
".aoneci/bailian-cli.yml",
createdFiles,
preservedFiles,
);
await createIfMissing(
resolve(targetDirectory, ".aoneci/bailian-cli-check.yml"),
buildAoneCheckWorkflow(config),
".aoneci/bailian-cli-check.yml",
createdFiles,
preservedFiles,
);
await createIfMissing(
resolve(targetDirectory, "README.md"),
buildReadme(basename(targetDirectory), config),
"README.md",
createdFiles,
preservedFiles,
);
if (shouldInitializeGit) await initializeGitRepository(targetDirectory);
return {
targetDirectory,
mode: targetMode === "new" ? "created" : "upgraded",
initializedGit: shouldInitializeGit,
createdFiles,
updatedFiles,
preservedFiles,
};
}
async function assertGitAvailable(): Promise<void> {
try {
await execFileAsync("git", ["--version"]);
} catch {
throw new BailianError(
"Git is required to initialize a repository.",
ExitCode.USAGE,
"Install Git and retry.",
);
}
}
async function initializeGitRepository(targetDirectory: string): Promise<void> {
try {
await execFileAsync("git", ["init", "--initial-branch", "main"], {
cwd: targetDirectory,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new BailianError(
`Could not initialize the local Git repository: ${message}`,
ExitCode.GENERAL,
);
}
}
async function mergeOrCreateTextFile(
path: string,
initialContent: string,
merge: (current: string) => string,
label: string,
createdFiles: string[],
updatedFiles: string[],
): Promise<void> {
if (!existsSync(path)) {
await writeFile(path, initialContent, "utf8");
createdFiles.push(label);
return;
}
const current = await readFile(path, "utf8");
const next = merge(current);
if (next !== current) {
await writeFile(path, next, "utf8");
updatedFiles.push(label);
}
}
async function createIfMissing(
path: string,
content: string,
label: string,
createdFiles: string[],
preservedFiles: string[],
): Promise<void> {
if (existsSync(path)) {
preservedFiles.push(label);
return;
}
await writeFile(path, content, "utf8");
createdFiles.push(label);
}
function mergeGitignore(content: string): string {
const repositoryContent = content
.split(/\r?\n/)
.filter((line) => line.trim() !== "agents.state.json")
.join("\n");
const existingPatterns = new Set(
repositoryContent
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean),
);
const missingPatterns = REPOSITORY_GITIGNORE_PATTERNS.filter(
(pattern) => !existingPatterns.has(pattern),
);
if (missingPatterns.length === 0) return repositoryContent;
return appendBlock(
repositoryContent,
`# Bailian CLI local files\n${missingPatterns.join("\n")}\n`,
);
}
function environmentExample(config: string): string {
return `${extractEnvironmentVariables(config)
.map((variable) => `${variable}=replace-me`)
.join("\n")}\n`;
}
function mergeEnvironmentExample(content: string, config: string): string {
const existingVariables = new Set<string>();
for (const line of content.split(/\r?\n/)) {
const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/);
if (match?.[1]) existingVariables.add(match[1]);
}
const missingVariables = extractEnvironmentVariables(config).filter(
(variable) => !existingVariables.has(variable),
);
if (missingVariables.length === 0) return content;
return appendBlock(
content,
`${missingVariables.map((variable) => `${variable}=replace-me`).join("\n")}\n`,
);
}
function extractEnvironmentVariables(config: string): string[] {
const variables = new Set<string>();
for (const match of config.matchAll(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-[^}]*)?\}/g)) {
if (match[1]) variables.add(match[1]);
}
return [...variables];
}
function appendBlock(content: string, block: string): string {
if (!content) return block;
if (content.endsWith("\n\n")) return `${content}${block}`;
if (content.endsWith("\n")) return `${content}\n${block}`;
return `${content}\n\n${block}`;
}
function buildPackageJson(projectName: string, cliVersion: string): string {
return `${JSON.stringify(
{
name: npmPackageName(projectName),
private: true,
version: "0.0.0",
type: "module",
scripts: PROJECT_SCRIPTS,
devDependencies: { "bailian-cli": cliVersion },
},
null,
2,
)}\n`;
}
function mergePackageJson(
content: string,
projectName: string,
cliVersion: string,
): { content: string; preservedSettings: string[] } {
let manifest: unknown;
try {
manifest = JSON.parse(content);
} catch {
throw new BailianError(
"Cannot upgrade package.json because it is not valid JSON.",
ExitCode.USAGE,
);
}
if (!isRecord(manifest)) {
throw new BailianError(
"Cannot upgrade package.json because its root is not an object.",
ExitCode.USAGE,
);
}
const preservedSettings: string[] = [];
if (manifest.name === undefined) manifest.name = npmPackageName(projectName);
if (manifest.private === undefined) manifest.private = true;
const scripts = manifest.scripts === undefined ? {} : manifest.scripts;
if (!isRecord(scripts)) {
throw new BailianError(
"Cannot upgrade package.json because 'scripts' is not an object.",
ExitCode.USAGE,
);
}
manifest.scripts = scripts;
for (const [name, command] of Object.entries(PROJECT_SCRIPTS)) {
if (scripts[name] === undefined) scripts[name] = command;
else if (scripts[name] !== command) preservedSettings.push(`package.json scripts.${name}`);
}
const developmentDependencies =
manifest.devDependencies === undefined ? {} : manifest.devDependencies;
if (!isRecord(developmentDependencies)) {
throw new BailianError(
"Cannot upgrade package.json because 'devDependencies' is not an object.",
ExitCode.USAGE,
);
}
manifest.devDependencies = developmentDependencies;
if (developmentDependencies["bailian-cli"] === undefined) {
developmentDependencies["bailian-cli"] = cliVersion;
} else if (developmentDependencies["bailian-cli"] !== cliVersion) {
preservedSettings.push("package.json bailian-cli version");
}
return { content: `${JSON.stringify(manifest, null, 2)}\n`, preservedSettings };
}
function buildAoneEnvironmentBlock(config: string): string {
const variables = extractEnvironmentVariables(config);
if (variables.length === 0) {
return " # Add provider variables referenced by agents.yaml in Aone Flow.";
}
return variables.map((variable) => ` ${variable}: \${{secrets.${variable}}}`).join("\n");
}
function buildAoneWorkflow(config: string): string {
const environmentBlock = buildAoneEnvironmentBlock(config);
return `name: Bailian CLI Managed Agent
triggers:
push:
branches:
- main
jobs:
apply:
name: Validate, plan, and apply Agent resources
image: alios-8u
timeout: 30m
steps:
- id: checkout
uses: checkout
- id: setup-env
uses: setup-env
inputs:
node-version: 22
tnpm-version: 10
tnpm-cache: true
- id: install
run: npm install --ignore-scripts --no-audit --no-fund
- id: validate-and-plan
envs:
${environmentBlock}
run: |
npm run agents:validate
npm run agents:plan:ci > bailian-cli-plan.json
- id: upload-plan
uses: upload-artifact
inputs:
name: bailian-cli-plan
path: bailian-cli-plan.json
- id: apply-and-persist-state
envs:
${environmentBlock}
run: |
set +e
npm run agents:apply:ci
apply_status=$?
set -e
if ! git diff --quiet -- agents.state.json; then
git config user.name "Bailian CLI CI"
git config user.email "bailian-cli-ci@alibaba-inc.com"
git add -- agents.state.json
git commit -m "chore: update Bailian CLI Agent state [skip ci]"
git push origin HEAD:main
fi
exit "$apply_status"
`;
}
function buildAoneCheckWorkflow(config: string): string {
const environmentBlock = buildAoneEnvironmentBlock(config);
return `name: Bailian CLI Managed Agent Check
# Bind this pipeline to Codeup merge-request new/update events in Aone Flow.
jobs:
check:
name: Validate and plan Agent resources
image: alios-8u
timeout: 20m
steps:
- id: checkout
uses: checkout
- id: setup-env
uses: setup-env
inputs:
node-version: 22
tnpm-version: 10
tnpm-cache: true
- id: install
run: npm install --ignore-scripts --no-audit --no-fund
- id: validate-and-plan
envs:
${environmentBlock}
run: |
npm run agents:validate
npm run agents:plan:ci > bailian-cli-plan.json
- id: upload-plan
uses: upload-artifact
inputs:
name: bailian-cli-plan
path: bailian-cli-plan.json
`;
}
function buildReadme(projectName: string, config: string): string {
const variableList = extractEnvironmentVariables(config)
.map((variable) => `- \`${variable}\``)
.join("\n");
return `# ${projectName}
This repository declares cloud Agent resources with Bailian CLI.
## Local Workbench
1. Copy \`.env.example\` to \`.env\` and replace placeholder credentials.
2. Run \`npm install\`.
3. Run \`npm run agents:workbench\`.
## Aone CI
\`.aoneci/bailian-cli-check.yml\` validates and plans merge requests without applying. \`.aoneci/bailian-cli.yml\` applies non-destructive local changes after a push to \`main\` and commits the resulting \`agents.state.json\` back to \`main\`.
Configure these values as secret variables in Aone Flow:
${variableList || "- Add the provider variables referenced by agents.yaml."}
Set pipeline concurrency to 1, protect the main branch, and require approval where appropriate. Workbench and CI should use isolated credentials, resource namespaces, and State scopes.
Create the remote repository yourself, then push this local repository:
\`\`\`bash
git add .
git commit -m "Initialize Bailian CLI Agent project"
git remote add origin <your-codeup-repository-url>
git push -u origin main
\`\`\`
`;
}
function npmPackageName(projectName: string): string {
const normalized = projectName
.toLowerCase()
.replace(/[^a-z0-9._-]+/g, "-")
.replace(/^[._-]+|[._-]+$/g, "");
return normalized || "bailian-agent-project";
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
@@ -6,12 +6,7 @@ import {
type FlagsDef,
} from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import {
executePlannedProject,
planProjectContext,
type PlannedAction,
UserError,
} from "@openagentpack/sdk";
import { executePlannedProject, planProjectContext } from "@openagentpack/sdk";
import { formatResourceLabel } from "./_engine/address-utils.ts";
import {
assertProviderConfigured,
@@ -22,11 +17,12 @@ import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts";
import { renderAgentFeedback } from "./_engine/feedback.ts";
import {
commitAutomaticVersion,
type PreparedAutomaticVersion,
prepareAutomaticVersion,
readVersionSource,
} from "@openagentpack/local-git";
commitPreparedProjectVersion,
type PreparedProjectVersion,
prepareProjectVersion,
readProjectVersionSource,
releasePreparedProjectVersion,
} from "@openagentpack/project-versions";
const APPLY_FLAGS = {
file: {
@@ -52,13 +48,6 @@ const APPLY_FLAGS = {
"zh-CN": "无需交互提示直接确认并应用(执行变更时必填)",
},
},
ci: {
type: "switch",
description: {
"en-US": "Run non-interactively while blocking deletes and remote drift",
"zh-CN": "以非交互模式运行,并阻止删除和远端漂移覆盖",
},
},
noRefresh: {
type: "switch",
description: {
@@ -90,17 +79,10 @@ export default defineCommand({
},
auth: "apiKey",
usageArgs:
"[--file <path>] [--provider <name>] [--yes | --ci] [--no-refresh] [--refresh-only] [--concurrency <n>]",
"[--file <path>] [--provider <name>] [--yes] [--no-refresh] [--refresh-only] [--concurrency <n>]",
flags: APPLY_FLAGS,
exampleArgs: ["--yes", "--provider bailian --yes", "--ci"],
exampleArgs: ["--yes", "--provider bailian --yes"],
notes: CREDENTIALS_NOTE,
validate(flags) {
if (flags.ci && flags.yes) return "--ci cannot be combined with --yes.";
if (flags.ci && flags.noRefresh) {
return "--ci requires remote state refresh and cannot be combined with --no-refresh.";
}
return undefined;
},
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
@@ -113,7 +95,6 @@ export default defineCommand({
provider: flags.provider ?? "all",
refresh: !flags.noRefresh,
concurrency: flags.concurrency,
ci: flags.ci,
refresh_only: flags.refreshOnly,
},
config_file: file,
@@ -124,7 +105,7 @@ export default defineCommand({
return;
}
const versionSource = await readVersionSource(file);
const versionSource = await readProjectVersionSource(file);
const { planned, runtime } = await withAgentErrors(() =>
withStdoutProtected(async () => {
@@ -157,7 +138,7 @@ export default defineCommand({
const actionable = plan.actions.filter((action) => action.action !== "no-op");
if (actionable.length === 0) {
if (!flags.refreshOnly) {
const preparedVersion = await prepareAutomaticVersion(
const preparedVersion = await prepareProjectVersion(
runtime.configPath,
versionSource.source,
);
@@ -172,8 +153,6 @@ export default defineCommand({
const creates = actionable.filter((action) => action.action === "create").length;
const updates = actionable.filter((action) => action.action === "update").length;
const deletes = planned.destructiveActions;
if (flags.ci) assertCiApplyPolicy(actionable);
for (const action of actionable) {
const icon = action.action === "create" ? "+" : action.action === "update" ? "~" : "-";
emitProgress(` ${icon} ${formatResourceLabel(action.address)}`);
@@ -197,7 +176,7 @@ export default defineCommand({
return;
}
if (!flags.yes && !flags.ci) {
if (!flags.yes) {
throw new BailianError(
`Refusing to apply ${actionable.length} change(s) (${creates} create, ${updates} update, ${deletes.length} destroy) without confirmation.`,
ExitCode.USAGE,
@@ -205,62 +184,50 @@ export default defineCommand({
);
}
const preparedVersion = await prepareAutomaticVersion(runtime.configPath, versionSource.source);
const result = await withAgentErrors(() =>
withStdoutProtected(() =>
executePlannedProject(planned, {
onFeedback: renderAgentFeedback,
policy: "force",
concurrency: flags.concurrency,
}),
),
);
const succeeded = result.results.filter((entry) => entry.status === "success").length;
const failed = result.results.filter((entry) => entry.status === "failed").length;
const skipped = result.results.filter((entry) => entry.status === "skipped").length;
if (format === "json") {
emitResult({ succeeded, failed, skipped, results: result.results }, format);
} else {
emitBare(`\nApply finished: ${succeeded} succeeded, ${failed} failed, ${skipped} skipped.`);
}
if (failed > 0 || skipped > 0) {
throw new BailianError(
failed > 0 ? "Apply failed." : "Apply incomplete: one or more actions were skipped.",
ExitCode.GENERAL,
const preparedVersion = await prepareProjectVersion(runtime.configPath, versionSource.source);
let versionCommitted = false;
try {
const result = await withAgentErrors(() =>
withStdoutProtected(() =>
executePlannedProject(planned, {
onFeedback: renderAgentFeedback,
policy: "force",
concurrency: flags.concurrency,
}),
),
);
const succeeded = result.results.filter((entry) => entry.status === "success").length;
const failed = result.results.filter((entry) => entry.status === "failed").length;
const skipped = result.results.filter((entry) => entry.status === "skipped").length;
if (format === "json") {
emitResult({ succeeded, failed, skipped, results: result.results }, format);
} else {
emitBare(`\nApply finished: ${succeeded} succeeded, ${failed} failed, ${skipped} skipped.`);
}
if (failed > 0 || skipped > 0) {
throw new BailianError(
failed > 0 ? "Apply failed." : "Apply incomplete: one or more actions were skipped.",
ExitCode.GENERAL,
);
}
await commitSuccessfulApplyVersion(preparedVersion, format);
versionCommitted = true;
} finally {
if (!versionCommitted) await releasePreparedProjectVersion(preparedVersion);
}
await commitSuccessfulApplyVersion(preparedVersion, format);
},
});
export function assertCiApplyPolicy(actions: PlannedAction[]): void {
const deletes = actions.filter((action) => action.action === "delete");
if (deletes.length > 0) {
throw new UserError(
`CI policy blocked ${deletes.length} delete action(s). Review the plan and apply this destructive change through an explicitly approved workflow.`,
);
}
const drifted = actions.filter(
(action) => action.driftKind === "remote" || action.driftKind === "both",
);
if (drifted.length > 0) {
throw new UserError(
`CI policy blocked ${drifted.length} action(s) with remote drift. Review the remote changes before deciding whether YAML should overwrite them.`,
);
}
}
async function commitSuccessfulApplyVersion(
prepared: PreparedAutomaticVersion | null,
prepared: PreparedProjectVersion | null,
format: "text" | "json",
): Promise<void> {
if (!prepared) return;
const version = await commitAutomaticVersion(prepared);
const version = await commitPreparedProjectVersion(prepared);
if (version && format !== "json") {
emitBare(`Created local version ${version.short_commit} (${version.message}).`);
emitBare(`Created local version ${version.short_version} (${version.message}).`);
}
}
@@ -8,11 +8,11 @@ import {
type FlagsDef,
} from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { createGitProject, inspectGitProjectTarget } from "./_engine/git-project.ts";
const GITIGNORE_ADDITIONS = `
# agents
agents.state.json
.openagentpack/versions/
.env
`;
@@ -101,14 +101,6 @@ const INIT_FLAGS = {
"zh-CN": "输出配置路径默认agents.yaml",
},
},
git: {
type: "string",
valueHint: "<directory>",
description: {
"en-US": "Create or add CI/Git scaffolding in this project directory",
"zh-CN": "在此项目目录中创建或补充 CI/Git 脚手架",
},
},
force: {
type: "switch",
description: { "en-US": "Overwrite an existing config file", "zh-CN": "覆盖已有配置文件" },
@@ -117,19 +109,13 @@ const INIT_FLAGS = {
export default defineCommand({
description: {
"en-US": "Create an agents.yaml template or a local CI/Git project",
"zh-CN": "创建 agents.yaml 模板或本地 CI/Git 项目",
"en-US": "Create an agents.yaml template",
"zh-CN": "创建 agents.yaml 模板",
},
auth: "none",
usageArgs:
"[--provider <name>] [--agent-name <name>] [--file <path>] [--git <directory>] [--force]",
usageArgs: "[--provider <name>] [--agent-name <name>] [--file <path>] [--force]",
flags: INIT_FLAGS,
exampleArgs: ["", "--provider bailian --agent-name assistant", "--git ./my-agents", "--git ."],
validate(flags) {
if (flags.git && flags.file) return "--git cannot be combined with --file.";
if (flags.git && flags.force) return "--git cannot be combined with --force.";
return undefined;
},
exampleArgs: ["", "--provider bailian --agent-name assistant"],
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
@@ -137,45 +123,6 @@ export default defineCommand({
const agentName = flags.agentName ?? "assistant";
const file = flags.file ?? "agents.yaml";
if (flags.git) {
const targetMode = await inspectGitProjectTarget(flags.git);
if (settings.dryRun) {
emitResult(
{
would_initialize_git_project: flags.git,
mode: targetMode === "new" ? "create" : "upgrade",
provider,
agent: agentName,
},
format,
);
return;
}
const template = buildTemplate({ provider, agentName });
const result = await createGitProject(flags.git, {
config: template,
cliVersion: ctx.identity.version,
});
if (format === "json") {
emitResult(result, format);
} else {
const action =
result.mode === "created" ? "Created CI/Git project" : "Added CI/Git scaffolding";
emitBare(`${action} at ${result.targetDirectory}`);
if (result.createdFiles.length > 0) {
emitBare(`Created: ${result.createdFiles.join(", ")}`);
}
if (result.updatedFiles.length > 0) {
emitBare(`Updated: ${result.updatedFiles.join(", ")}`);
}
if (result.preservedFiles.length > 0) {
emitBare(`Preserved: ${result.preservedFiles.join(", ")}`);
}
emitBare("Next: add credentials to .env, install dependencies, and open the Workbench.");
}
return;
}
if (existsSync(file) && !flags.force) {
throw new BailianError(
`${file} already exists.`,
@@ -8,16 +8,16 @@ import {
import { confirmDangerousAction, emitBare, emitResult } from "bailian-cli-runtime";
import chalk from "chalk";
import {
disableLocalVersioning,
enableLocalVersioning,
getLocalVersionStatus,
type LocalProjectVersion,
type LocalVersionPreview,
type LocalVersionStatus,
listLocalVersions,
previewLocalVersion,
restoreLocalVersion,
} from "@openagentpack/local-git";
disableProjectVersioning,
enableProjectVersioning,
getProjectVersionStatus,
type ProjectVersion,
type ProjectVersionPreview,
type ProjectVersionStatus,
listProjectVersions,
previewProjectVersion,
restoreProjectVersion,
} from "@openagentpack/project-versions";
const FILE_FLAG = {
file: {
@@ -30,22 +30,22 @@ const FILE_FLAG = {
},
} satisfies FlagsDef;
const COMMIT_FLAG = {
commit: {
const VERSION_FLAG = {
versionId: {
type: "string",
valueHint: "<full-sha>",
valueHint: "<full-version>",
required: true,
description: {
"en-US": "Full commit SHA from the current branch",
"zh-CN": "当前分支中的完整 Commit SHA",
"en-US": "Full local version ID",
"zh-CN": "完整的本地版本 ID",
},
},
} satisfies FlagsDef;
export const managedAgentVersionEnable = defineCommand({
description: {
"en-US": "Enable Apply-time Git versioning for agents.yaml",
"zh-CN": "为 agents.yaml 启用 Apply 后自动 Git 版本管理",
"en-US": "Enable Apply-time local snapshots for agents.yaml",
"zh-CN": "为 agents.yaml 启用 Apply 后自动本地快照",
},
auth: "none",
usageArgs: "[--file <path>]",
@@ -55,28 +55,30 @@ export const managedAgentVersionEnable = defineCommand({
const file = ctx.flags.file ?? "agents.yaml";
const format = detectOutputFormat(ctx.settings.output);
if (ctx.settings.dryRun) {
emitResult({ would_enable: file, git: await getLocalVersionStatus(file) }, format);
emitResult({ would_enable: file, versioning: await getProjectVersionStatus(file) }, format);
return;
}
const result = await enableLocalVersioning(file, "Enable Bailian CLI versioning");
const result = await enableProjectVersioning(file, "Enable Bailian CLI versioning");
if (format === "json") {
emitResult(result, format);
return;
}
if (result.version) {
emitBare(`Created baseline version ${result.version.short_commit} ${result.version.message}`);
emitBare(
`Created baseline version ${result.version.short_version} ${result.version.message}`,
);
} else {
emitBare("Current agents.yaml is already versioned; no commit was created.");
emitBare("Current agents.yaml is already versioned; no snapshot was created.");
}
emitBare("Automatic versioning is enabled for this agents.yaml.");
renderStatus(result.git);
renderStatus(result.versioning);
},
});
export const managedAgentVersionDisable = defineCommand({
description: {
"en-US": "Disable Apply-time Git versioning without removing history",
"zh-CN": "关闭 Apply 后自动 Git 版本管理,但保留历史",
"en-US": "Disable Apply-time local snapshots without removing history",
"zh-CN": "关闭 Apply 后自动本地快照,但保留历史",
},
auth: "none",
usageArgs: "[--file <path>]",
@@ -86,10 +88,10 @@ export const managedAgentVersionDisable = defineCommand({
const file = ctx.flags.file ?? "agents.yaml";
const format = detectOutputFormat(ctx.settings.output);
if (ctx.settings.dryRun) {
emitResult({ would_disable: file, git: await getLocalVersionStatus(file) }, format);
emitResult({ would_disable: file, versioning: await getProjectVersionStatus(file) }, format);
return;
}
const status = await disableLocalVersioning(file);
const status = await disableProjectVersioning(file);
if (format === "json") {
emitResult(status, format);
return;
@@ -101,15 +103,15 @@ export const managedAgentVersionDisable = defineCommand({
export const managedAgentVersionStatus = defineCommand({
description: {
"en-US": "Show local Git versioning status for agents.yaml",
"zh-CN": "显示 agents.yaml 的本地 Git 版本管理状态",
"en-US": "Show local snapshot versioning status for agents.yaml",
"zh-CN": "显示 agents.yaml 的本地快照版本管理状态",
},
auth: "none",
usageArgs: "[--file <path>]",
flags: FILE_FLAG,
exampleArgs: ["", "--file agents.yaml --output json"],
async run(ctx) {
const status = await getLocalVersionStatus(ctx.flags.file ?? "agents.yaml");
const status = await getProjectVersionStatus(ctx.flags.file ?? "agents.yaml");
const format = detectOutputFormat(ctx.settings.output);
if (format === "json") emitResult(status, format);
else renderStatus(status);
@@ -138,15 +140,15 @@ const LIST_FLAGS = {
export const managedAgentVersionList = defineCommand({
description: {
"en-US": "List current-branch commits that changed agents.yaml",
"zh-CN": "列出当前分支中修改过 agents.yaml 的 Commit",
"en-US": "List local snapshots of agents.yaml",
"zh-CN": "列出 agents.yaml 的本地快照",
},
auth: "none",
usageArgs: "[--file <path>] [--limit <n>] [--cursor <cursor>]",
flags: LIST_FLAGS,
exampleArgs: ["", "--limit 20 --output json"],
async run(ctx) {
const page = await listLocalVersions(ctx.flags.file ?? "agents.yaml", {
const page = await listProjectVersions(ctx.flags.file ?? "agents.yaml", {
limit: ctx.flags.limit,
cursor: ctx.flags.cursor,
});
@@ -156,7 +158,7 @@ export const managedAgentVersionList = defineCommand({
return;
}
if (page.versions.length === 0) {
emitBare("No versions of agents.yaml exist on the current branch.");
emitBare("No local versions of agents.yaml exist.");
return;
}
for (const version of page.versions) emitBare(formatVersion(version));
@@ -166,7 +168,7 @@ export const managedAgentVersionList = defineCommand({
const PREVIEW_FLAGS = {
...FILE_FLAG,
...COMMIT_FLAG,
...VERSION_FLAG,
} satisfies FlagsDef;
export const managedAgentVersionPreview = defineCommand({
@@ -175,11 +177,14 @@ export const managedAgentVersionPreview = defineCommand({
"zh-CN": "预览 agents.yaml 的历史版本",
},
auth: "none",
usageArgs: "--commit <full-sha> [--file <path>]",
usageArgs: "--version-id <full-version> [--file <path>]",
flags: PREVIEW_FLAGS,
exampleArgs: ["--commit <full-sha>", "--commit <full-sha> --output json"],
exampleArgs: ["--version-id <full-version>", "--version-id <full-version> --output json"],
async run(ctx) {
const preview = await previewLocalVersion(ctx.flags.file ?? "agents.yaml", ctx.flags.commit);
const preview = await previewProjectVersion(
ctx.flags.file ?? "agents.yaml",
ctx.flags.versionId,
);
const format = detectOutputFormat(ctx.settings.output);
if (format === "json") emitResult(preview, format);
else renderPreview(preview);
@@ -203,12 +208,12 @@ export const managedAgentVersionRestore = defineCommand({
"zh-CN": "将 agents.yaml 历史版本恢复到工作区",
},
auth: "none",
usageArgs: "--commit <full-sha> [--file <path>] [--yes]",
usageArgs: "--version-id <full-version> [--file <path>] [--yes]",
flags: RESTORE_FLAGS,
exampleArgs: ["--commit <full-sha>", "--commit <full-sha> --yes --output json"],
exampleArgs: ["--version-id <full-version>", "--version-id <full-version> --yes --output json"],
async run(ctx) {
const file = ctx.flags.file ?? "agents.yaml";
const preview = await previewLocalVersion(file, ctx.flags.commit);
const preview = await previewProjectVersion(file, ctx.flags.versionId);
const format = detectOutputFormat(ctx.settings.output);
if (format !== "json") renderPreview(preview);
if (!preview.can_restore) {
@@ -220,49 +225,47 @@ export const managedAgentVersionRestore = defineCommand({
);
}
if (ctx.settings.dryRun) {
emitResult({ would_restore: ctx.flags.commit, preview }, format);
emitResult({ would_restore: ctx.flags.versionId, preview }, format);
return;
}
await confirmDangerousAction(
"Restore this version to the agents.yaml working tree? HEAD and agents.state.json will not change.",
"Restore this version to the agents.yaml working tree? Version history and agents.state.json will not change.",
ctx.flags.yes,
);
const restored = await restoreLocalVersion(file, ctx.flags.commit, {
head: preview.base_head,
const restored = await restoreProjectVersion(file, ctx.flags.versionId, {
headVersion: preview.base_head_version,
sourceRevision: preview.base_source_revision,
});
if (format === "json") {
emitResult({ restored: ctx.flags.commit, preview: restored }, format);
emitResult({ restored: ctx.flags.versionId, preview: restored }, format);
} else {
emitBare(
`Restored ${ctx.flags.commit.slice(0, 12)} to the working tree. HEAD was not changed.`,
`Restored ${ctx.flags.versionId.slice(0, 12)} to the working tree. Version history was not changed.`,
);
}
},
});
function renderStatus(status: LocalVersionStatus): void {
emitBare(`Git available: ${status.git_available ? "yes" : "no"}`);
function renderStatus(status: ProjectVersionStatus): void {
emitBare(`Automatic versioning: ${status.enabled ? "enabled" : "disabled"}`);
emitBare(`Repository: ${status.repository_root ?? "none"}`);
emitBare(`Config path: ${status.config_path ?? "none"}`);
emitBare(`Branch: ${status.branch ?? "none"}`);
emitBare(`HEAD: ${status.head ?? "none"}`);
emitBare(`Version store: ${status.initialized ? status.store_root : "not initialized"}`);
emitBare(`Config path: ${status.config_path}`);
emitBare(`Current version: ${status.head_version ?? "none"}`);
emitBare(
`agents.yaml: ${status.config_status}${status.config_versioned ? ", versioned" : ", unversioned"}`,
`agents.yaml: ${status.source_status}${status.source_versioned ? ", versioned" : ", unversioned"}`,
);
const blockers = [...new Set([...status.commit_blockers, ...status.restore_blockers])];
const blockers = [...new Set([...status.write_blockers, ...status.restore_blockers])];
for (const blocker of blockers) emitBare(chalk.yellow(`Blocker: ${blocker}`));
}
function formatVersion(version: LocalProjectVersion): string {
return `${chalk.yellow(version.short_commit)} ${version.authored_at} ${version.message} ${chalk.dim(`(${version.author_name})`)}`;
function formatVersion(version: ProjectVersion): string {
return `${chalk.yellow(version.short_version)} ${version.created_at} ${version.message} ${chalk.dim(`(${version.created_by})`)}`;
}
function renderPreview(preview: LocalVersionPreview): void {
emitBare(chalk.bold(`Version ${preview.commit}`));
function renderPreview(preview: ProjectVersionPreview): void {
emitBare(chalk.bold(`Version ${preview.version_id}`));
emitBare(chalk.red("--- working tree"));
emitBare(chalk.green(`+++ ${preview.commit}`));
emitBare(chalk.green(`+++ ${preview.version_id}`));
for (const line of buildLineDiff(preview.before_yaml, preview.after_yaml)) {
if (line.kind === "deletion") emitBare(chalk.red(`-${line.text}`));
else if (line.kind === "addition") emitBare(chalk.green(`+${line.text}`));
@@ -45,9 +45,9 @@ const WORKBENCH_NOTES = [
...CREDENTIALS_NOTE,
{
"en-US":
"Workbench requires Node.js 22+ and starts the shared @openagentpack/playground package locally. It does not push Git commits or switch branches.",
"Workbench requires Node.js 22+ and starts the shared @openagentpack/playground package locally. Local versions use the shared .openagentpack/versions project store and do not require Git.",
"zh-CN":
"Workbench 需要 Node.js 22+,并在本地启动共享的 @openagentpack/playground 包;不会推送 Git Commit 或切换分支。",
"Workbench 需要 Node.js 22+,并在本地启动共享的 @openagentpack/playground 包;本地版本使用共享的 .openagentpack/versions 项目版本存储,不依赖 Git。",
},
];
@@ -130,6 +130,17 @@ describe("e2e: managed-agent", () => {
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--file|--provider|--yes/i);
expect(stderr).not.toContain("--ci");
});
test("managed-agent init 不再暴露 Git 仓库脚手架", async () => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"init",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).not.toContain("--git");
});
test("managed-agent version 暴露共享版本管理子命令", async () => {
@@ -142,7 +153,7 @@ describe("e2e: managed-agent", () => {
expect(stderr).toMatch(/enable|disable|status|list|preview|restore/i);
});
test("managed-agent version preview 缺少 --commit 时退出为用法错误 (2)", async () => {
test("managed-agent version preview 缺少 --version-id 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"version",
@@ -150,7 +161,7 @@ describe("e2e: managed-agent", () => {
"--quiet",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--commit|Missing required/i);
expect(stderr).toMatch(/--version-id|Missing required/i);
});
test("managed-agent session delete 缺少 --session-id 时退出为用法错误 (2)", async () => {
@@ -239,29 +250,6 @@ describe("e2e: managed-agent--dry-run 短路,不联网不写盘)", () =>
expect(data.provider).toBe("bailian");
});
test("init --git --dry-run 仅输出仓库脚手架计划", async () => {
const targetDirectory = join(
process.cwd(),
`.managed-agent-git-dry-run-${process.pid}-${Date.now()}`,
);
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"init",
"--git",
targetDirectory,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
would_initialize_git_project?: string;
mode?: string;
}>(stdout);
expect(data.would_initialize_git_project).toBe(targetDirectory);
expect(data.mode).toBe("create");
});
test("workbench --dry-run 仅输出启动计划", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
@@ -1,67 +0,0 @@
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, test } from "vite-plus/test";
import { createGitProject } from "../src/commands/managed-agent/_engine/git-project.ts";
const temporaryDirectories: string[] = [];
afterEach(async () => {
for (const directory of temporaryDirectories.splice(0)) {
await rm(directory, { recursive: true, force: true });
}
});
describe("managed-agent init --git project scaffolding", () => {
test("creates a main-branch Git project without committing or configuring a remote", async () => {
const parentDirectory = await mkdtemp(join(tmpdir(), "bailian-cli-git-project-"));
temporaryDirectories.push(parentDirectory);
const targetDirectory = join(parentDirectory, "agent-project");
const result = await createGitProject(targetDirectory, {
config: projectYaml(),
cliVersion: "1.17.1",
});
expect(result.mode).toBe("created");
expect(result.initializedGit).toBe(true);
expect(result.createdFiles).toContain("agents.yaml");
expect(await readFile(join(targetDirectory, "agents.yaml"), "utf8")).toContain("assistant:");
expect(await readFile(join(targetDirectory, ".aoneci/bailian-cli.yml"), "utf8")).toContain(
"agents:apply:ci",
);
expect(await readFile(join(targetDirectory, "README.md"), "utf8")).toContain(
"Create the remote repository yourself",
);
});
test("upgrades an initialized config directory without overwriting agents.yaml", async () => {
const targetDirectory = await mkdtemp(join(tmpdir(), "bailian-cli-git-upgrade-"));
temporaryDirectories.push(targetDirectory);
const originalSource = projectYaml().replace("assistant", "reviewer");
await writeFile(join(targetDirectory, "agents.yaml"), originalSource);
const result = await createGitProject(targetDirectory, {
config: projectYaml(),
cliVersion: "1.17.1",
});
expect(result.mode).toBe("upgraded");
expect(result.preservedFiles).toContain("agents.yaml");
expect(await readFile(join(targetDirectory, "agents.yaml"), "utf8")).toBe(originalSource);
});
});
function projectYaml(): string {
return `version: "1"
providers:
bailian:
api_key: \${DASHSCOPE_API_KEY}
defaults:
provider: bailian
agents:
assistant:
model: qwen3.8-max
instructions: You are helpful.
`;
}
@@ -1,202 +0,0 @@
import { execFile } from "node:child_process";
import { chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import type { PlannedAction } from "@openagentpack/sdk";
import { afterEach, describe, expect, test } from "vite-plus/test";
import {
commitAutomaticVersion,
disableLocalVersioning,
enableLocalVersioning,
getLocalVersionStatus,
prepareAutomaticVersion,
previewLocalVersion,
restoreLocalVersion,
} from "@openagentpack/local-git";
import { playgroundBrowserTargetFromSummary } from "../src/commands/managed-agent/_engine/playground-launcher.ts";
import { assertCiApplyPolicy } from "../src/commands/managed-agent/apply.ts";
const execFileAsync = promisify(execFile);
const temporaryDirectories: string[] = [];
const gitIdentity = {
GIT_AUTHOR_NAME: "Bailian CLI Test",
GIT_AUTHOR_EMAIL: "bailian-cli@example.com",
GIT_COMMITTER_NAME: "Bailian CLI Test",
GIT_COMMITTER_EMAIL: "bailian-cli@example.com",
};
afterEach(async () => {
for (const directory of temporaryDirectories.splice(0)) {
await rm(directory, { recursive: true, force: true });
}
});
describe("managed-agent local Git versions", () => {
test("uses the shared path-scoped switch and commits only agents.yaml", async () => {
const root = await temporaryDirectory();
const configPath = join(root, "agents.yaml");
const nestedDirectory = join(root, "nested");
const nestedConfigPath = join(nestedDirectory, "agents.yaml");
await mkdir(nestedDirectory);
await writeFile(configPath, projectYaml("First"));
await writeFile(nestedConfigPath, projectYaml("Second"));
await git(root, ["init", "--initial-branch", "main"]);
await writeFile(join(root, "staged.txt"), "staged\n");
await git(root, ["add", "staged.txt"]);
const stagedBefore = await git(root, ["status", "--porcelain=v1", "--", "staged.txt"]);
const enabled = await withGitIdentity(() =>
enableLocalVersioning(configPath, "Enable Bailian CLI versioning"),
);
expect(enabled.git.enabled).toBe(true);
expect((await getLocalVersionStatus(nestedConfigPath)).enabled).toBe(false);
expect((await git(root, ["show", "--pretty=", "--name-only", "HEAD"])).trim()).toBe(
"agents.yaml",
);
expect(await git(root, ["status", "--porcelain=v1", "--", "staged.txt"])).toBe(stagedBefore);
expect(
await git(root, ["rev-parse", "--git-path", "openagentpack/local-git/versions"]),
).toContain("openagentpack/local-git/versions");
await writeFile(configPath, projectYaml("First updated"));
const repeated = await withGitIdentity(() =>
enableLocalVersioning(configPath, "Enable Bailian CLI versioning"),
);
expect(repeated.version?.message).toBe("Enable Bailian CLI versioning");
expect(await git(root, ["status", "--porcelain=v1", "--", "staged.txt"])).toBe(stagedBefore);
const disabled = await disableLocalVersioning(configPath);
expect(disabled.enabled).toBe(false);
});
test("auto-commits after success and restores without moving HEAD or changing permissions", async () => {
const root = await temporaryDirectory();
const configPath = join(root, "agents.yaml");
await writeFile(configPath, projectYaml("Version one"));
await chmod(configPath, 0o640);
const enabled = await withGitIdentity(() =>
enableLocalVersioning(configPath, "Enable Bailian CLI versioning"),
);
const firstCommit = enabled.version!.commit;
const secondSource = projectYaml("Version two");
await writeFile(configPath, secondSource);
const prepared = await withGitIdentity(() => prepareAutomaticVersion(configPath, secondSource));
const version = await withGitIdentity(() => commitAutomaticVersion(prepared!));
const headBeforeRestore = (await git(root, ["rev-parse", "HEAD"])).trim();
expect(version?.message).toBe("Apply agents.yaml");
const preview = await previewLocalVersion(configPath, firstCommit);
expect(preview.can_restore).toBe(true);
expect(preview.after_yaml).toContain("Version one");
await restoreLocalVersion(configPath, firstCommit, {
head: preview.base_head,
sourceRevision: preview.base_source_revision,
});
expect(await readFile(configPath, "utf8")).toContain("Version one");
expect((await git(root, ["rev-parse", "HEAD"])).trim()).toBe(headBeforeRestore);
expect((await stat(configPath)).mode & 0o777).toBe(0o640);
});
test("rejects short SHAs and plaintext credentials", async () => {
const root = await temporaryDirectory();
const configPath = join(root, "agents.yaml");
await writeFile(configPath, projectYaml("Safe"));
const enabled = await withGitIdentity(() =>
enableLocalVersioning(configPath, "Enable Bailian CLI versioning"),
);
await expect(previewLocalVersion(configPath, enabled.version!.short_commit)).rejects.toThrow(
/full hexadecimal commit SHA/i,
);
await disableLocalVersioning(configPath);
await writeFile(
configPath,
projectYaml("Unsafe").replace("qoder: {}", "qoder:\n api_key: plaintext-secret"),
);
await expect(
withGitIdentity(() => enableLocalVersioning(configPath, "Enable Bailian CLI versioning")),
).rejects.toThrow(/environment variable reference/i);
});
});
describe("managed-agent CI and Workbench policies", () => {
test("CI blocks delete actions and remote drift", () => {
expect(() => assertCiApplyPolicy([plannedAction("delete")])).toThrow(/blocked.*delete/i);
expect(() => assertCiApplyPolicy([plannedAction("update", "remote")])).toThrow(
/blocked.*remote drift/i,
);
expect(() => assertCiApplyPolicy([plannedAction("update", "local")])).not.toThrow();
});
test("Session Preview opens the requested Agent or falls back to Workbench", () => {
const summary = {
status: "valid",
agents: [{ agent: { id: "assistant" } }, { agent: { id: "reviewer" } }],
};
expect(
playgroundBrowserTargetFromSummary("http://localhost:4848", summary, "reviewer"),
).toEqual({ url: "http://localhost:4848/agents/reviewer/preview" });
expect(playgroundBrowserTargetFromSummary("http://localhost:4848", summary)).toEqual(
expect.objectContaining({ url: "http://localhost:4848", warning: expect.any(String) }),
);
});
});
async function temporaryDirectory(): Promise<string> {
const directory = await mkdtemp(join(tmpdir(), "bailian-cli-local-git-"));
temporaryDirectories.push(directory);
return directory;
}
function projectYaml(instructions: string): string {
return `version: "1"
providers:
qoder: {}
defaults:
provider: qoder
agents:
assistant:
model: ultimate
instructions: ${instructions}
`;
}
function plannedAction(
action: "create" | "update" | "delete",
driftKind: "none" | "local" | "remote" | "both" = "none",
): PlannedAction {
return {
action,
driftKind,
address: { provider: "bailian", type: "agent", name: "assistant" },
} as PlannedAction;
}
async function git(workingDirectory: string, arguments_: string[]): Promise<string> {
const result = await execFileAsync("git", arguments_, {
cwd: workingDirectory,
encoding: "utf8",
env: { ...process.env, ...gitIdentity },
});
return result.stdout;
}
async function withGitIdentity<Result>(operation: () => Promise<Result>): Promise<Result> {
const previousEnvironment = Object.fromEntries(
Object.keys(gitIdentity).map((key) => [key, process.env[key]]),
);
Object.assign(process.env, gitIdentity);
try {
return await operation();
} finally {
for (const key of Object.keys(gitIdentity)) {
const value = previousEnvironment[key];
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
}
}
@@ -0,0 +1,139 @@
import { chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
commitPreparedProjectVersion,
disableProjectVersioning,
enableProjectVersioning,
getProjectVersionStatus,
listProjectVersions,
prepareProjectVersion,
previewProjectVersion,
restoreProjectVersion,
} from "@openagentpack/project-versions";
import { afterEach, describe, expect, test } from "vite-plus/test";
import { playgroundBrowserTargetFromSummary } from "../src/commands/managed-agent/_engine/playground-launcher.ts";
const temporaryDirectories: string[] = [];
afterEach(async () => {
for (const directory of temporaryDirectories.splice(0)) {
await rm(directory, { recursive: true, force: true });
}
});
describe("managed-agent local snapshot versions", () => {
test("uses the shared path-scoped switch and stores full YAML outside store.json", async () => {
const root = await temporaryDirectory();
const configPath = join(root, "agents.yaml");
const siblingDirectory = join(root, "nested");
const siblingConfigPath = join(siblingDirectory, "agents.yaml");
await mkdir(siblingDirectory);
await writeFile(configPath, projectYaml("First"));
await writeFile(siblingConfigPath, projectYaml("Second"));
const enabled = await enableProjectVersioning(configPath, "Enable Bailian CLI versioning");
expect(enabled.versioning.enabled).toBe(true);
expect(enabled.version?.message).toBe("Enable Bailian CLI versioning");
expect((await getProjectVersionStatus(siblingConfigPath)).enabled).toBe(false);
const storeSource = await readFile(join(root, ".openagentpack/versions/store.json"), "utf8");
expect(storeSource).not.toContain("instructions: First");
const snapshotSource = await readFile(
join(root, ".openagentpack/versions/blobs", `${enabled.version!.source_hash}.yaml`),
"utf8",
);
expect(snapshotSource).toContain("instructions: First");
await writeFile(configPath, projectYaml("First updated"));
const repeated = await enableProjectVersioning(configPath, "Enable Bailian CLI versioning");
expect(repeated.version?.message).toBe("Enable Bailian CLI versioning");
expect((await listProjectVersions(configPath)).versions).toHaveLength(2);
const disabled = await disableProjectVersioning(configPath);
expect(disabled.enabled).toBe(false);
});
test("auto-snapshots after success and restores without changing history or permissions", async () => {
const root = await temporaryDirectory();
const configPath = join(root, "agents.yaml");
await writeFile(configPath, projectYaml("Version one"));
await chmod(configPath, 0o640);
const enabled = await enableProjectVersioning(configPath, "Enable Bailian CLI versioning");
const firstVersion = enabled.version!.version_id;
const secondSource = projectYaml("Version two");
await writeFile(configPath, secondSource);
const prepared = await prepareProjectVersion(configPath, secondSource);
const version = await commitPreparedProjectVersion(prepared!);
const currentVersionBeforeRestore = (await getProjectVersionStatus(configPath)).head_version;
expect(version?.message).toBe("Apply agents.yaml");
const preview = await previewProjectVersion(configPath, firstVersion);
expect(preview.can_restore).toBe(true);
expect(preview.after_yaml).toContain("Version one");
await restoreProjectVersion(configPath, firstVersion, {
headVersion: preview.base_head_version,
sourceRevision: preview.base_source_revision,
});
expect(await readFile(configPath, "utf8")).toContain("Version one");
expect((await getProjectVersionStatus(configPath)).head_version).toBe(
currentVersionBeforeRestore,
);
expect((await stat(configPath)).mode & 0o777).toBe(0o640);
});
test("rejects abbreviated version IDs and plaintext credentials", async () => {
const root = await temporaryDirectory();
const configPath = join(root, "agents.yaml");
await writeFile(configPath, projectYaml("Safe"));
const enabled = await enableProjectVersioning(configPath, "Enable Bailian CLI versioning");
await expect(previewProjectVersion(configPath, enabled.version!.short_version)).rejects.toThrow(
/full 64-character hexadecimal/i,
);
await disableProjectVersioning(configPath);
await writeFile(
configPath,
projectYaml("Unsafe").replace("qoder: {}", "qoder:\n api_key: plaintext-secret"),
);
await expect(
enableProjectVersioning(configPath, "Enable Bailian CLI versioning"),
).rejects.toThrow(/environment variable reference/i);
});
});
describe("managed-agent Workbench policy", () => {
test("Session Preview opens the requested Agent or falls back to Workbench", () => {
const summary = {
status: "valid",
agents: [{ agent: { id: "assistant" } }, { agent: { id: "reviewer" } }],
};
expect(
playgroundBrowserTargetFromSummary("http://localhost:4848", summary, "reviewer"),
).toEqual({ url: "http://localhost:4848/agents/reviewer/preview" });
expect(playgroundBrowserTargetFromSummary("http://localhost:4848", summary)).toEqual(
expect.objectContaining({ url: "http://localhost:4848", warning: expect.any(String) }),
);
});
});
async function temporaryDirectory(): Promise<string> {
const directory = await mkdtemp(join(tmpdir(), "bailian-cli-local-versions-"));
temporaryDirectories.push(directory);
return directory;
}
function projectYaml(instructions: string): string {
return `version: "1"
providers:
qoder: {}
defaults:
provider: qoder
agents:
assistant:
model: ultimate
instructions: ${instructions}
`;
}
+15 -17
View File
@@ -6,8 +6,8 @@ metadata:
bins: ["bl"]
description: >-
阿里云百炼托管 Agent 声明式基础设施入口用户要创建agent、初始化 agents.yaml、校验或预览 agent 配置变更、
创建/更新/销毁百炼托管 Agent 或 Deployment、在 Workbench 编辑和调试已有声明、管理 agents.yaml 本地 Git 版本、
生成 CI 仓库、和托管 agent 对话、查会话事件历史、导入或取消跟踪远端资源时使用
创建/更新/销毁百炼托管 Agent 或 Deployment、在 Workbench 编辑和调试已有声明、管理 agents.yaml 本地快照版本、
和托管 agent 对话、查会话事件历史、导入或取消跟踪远端资源时使用
`bl managed-agent`。以 agents.yaml 为唯一事实源做 IaCinit 建脚手架、validate 离线校验、plan 预览 diff、
apply / destroy 变更远端资源且必须带 `--yes`,务必先 plan 给用户看 diff 再让其确认。
反触发:调用已上线的百炼应用/智能体走 bailian-app-call 或 `bl app`;宿主 agent 自身的记忆、技能、
@@ -21,12 +21,11 @@ description: >-
## Safety guardrail (the most important rule)
`apply` / `destroy` **mutate remote resources**. Interactive execution requires `--yes`; `apply --ci` is only for an already approved CI workflow:
`apply` / `destroy` **mutate remote resources**. Execution requires `--yes`:
1. Always run `bl managed-agent plan` first and show the diff to the user.
2. Only after explicit user confirmation, retry `apply` / `destroy` with `--yes`.
3. Never add `--yes` on your own initiative before the user has confirmed.
4. Never use `--ci` to bypass user confirmation in an interactive task. CI mode blocks deletes and remote drift, but still mutates remote resources.
## IaC lifecycle
@@ -38,22 +37,21 @@ description: >-
5. Destroy bl managed-agent destroy --yes # only after user confirmation
```
## Workbench, local versions, and CI
## Workbench and local versions
| Intent | Command |
| ------------------------------------------- | ---------------------------------------------- |
| Launch project resource editing | `bl managed-agent workbench` |
| Launch one Agent Session Preview | `bl managed-agent playground --agent <id>` |
| Create or upgrade a local Git/CI repository | `bl managed-agent init --git <directory>` |
| Enable/disable shared automatic versions | `bl managed-agent version enable` / `disable` |
| Inspect local version state and history | `bl managed-agent version status` / `list` |
| Preview or restore a historical YAML | `bl managed-agent version preview` / `restore` |
| Intent | Command |
| ---------------------------------------- | ---------------------------------------------- |
| Launch project resource editing | `bl managed-agent workbench` |
| Launch one Agent Session Preview | `bl managed-agent playground --agent <id>` |
| Enable/disable shared automatic versions | `bl managed-agent version enable` / `disable` |
| Inspect local version state and history | `bl managed-agent version status` / `list` |
| Preview or restore a historical YAML | `bl managed-agent version preview` / `restore` |
- Bailian CLI and Workbench use the same repository-local switch for the same Git worktree and `agents.yaml` path. The switch lives in private Git metadata and is not cloned or pushed.
- When enabled, a fully successful Apply creates a local commit containing only `agents.yaml`. Failed, partial, cancelled, and `--refresh-only` Apply runs do not commit.
- `version restore` writes the historical YAML to the working tree. It does not move `HEAD`, restore `agents.state.json`, create a commit, or Apply remote changes.
- Bailian CLI and Workbench use the same `.openagentpack/versions` store and enable switch for the same `agents.yaml`. Git is not required.
- `store.json` contains only the switch and head metadata. Immutable linked entries live under `entries/`, while complete YAML is stored as content-addressed blobs under `blobs/`. Neither `agents.state.json` nor referenced files are versioned.
- When enabled, a fully successful Apply creates a local snapshot only when `agents.yaml` changed. Failed, partial, cancelled, and `--refresh-only` Apply runs do not create one.
- `version restore` writes the historical YAML to the working tree. It does not move version history, restore `agents.state.json`, create a new snapshot, or Apply remote changes.
- Workbench can edit local drafts while Apply is running, but saving/version mutations are blocked until Apply completes. External file edits are detected through revision checks.
- `init --git` never creates a remote repository or pushes. The generated Aone CI uses `apply --ci`, which blocks deletes and remote drift; review destructive changes in a separately approved workflow.
## Deployment as IaC
@@ -13,7 +13,7 @@ Use this index for the skill-scoped quick index and global flags.
| ---------------------------------- | -------------- | ------------------------------------------------------------- | ------------------------------------ |
| `bl managed-agent apply` | API Key | Apply planned changes to create/update/delete agent resources | [managed-agent.md](managed-agent.md) |
| `bl managed-agent destroy` | API Key | Destroy all managed agent resources tracked in state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent init` | No Auth | Create an agents.yaml template or a local CI/Git project | [managed-agent.md](managed-agent.md) |
| `bl managed-agent init` | No Auth | Create an agents.yaml template | [managed-agent.md](managed-agent.md) |
| `bl managed-agent plan` | API Key | Show what changes would be applied to agent infrastructure | [managed-agent.md](managed-agent.md) |
| `bl managed-agent playground` | API Key | Launch a Session Preview for an agents.yaml Agent | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session create` | API Key | Create a new session for an agent | [managed-agent.md](managed-agent.md) |
@@ -29,12 +29,12 @@ Use this index for the skill-scoped quick index and global flags.
| `bl managed-agent state rm` | No Auth | Remove a resource from state without destroying it remotely | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state show` | No Auth | Show details of a resource in agents state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent validate` | No Auth | Validate an agents.yaml configuration (offline) | [managed-agent.md](managed-agent.md) |
| `bl managed-agent version disable` | No Auth | Disable Apply-time Git versioning without removing history | [managed-agent.md](managed-agent.md) |
| `bl managed-agent version enable` | No Auth | Enable Apply-time Git versioning for agents.yaml | [managed-agent.md](managed-agent.md) |
| `bl managed-agent version list` | No Auth | List current-branch commits that changed agents.yaml | [managed-agent.md](managed-agent.md) |
| `bl managed-agent version disable` | No Auth | Disable Apply-time local snapshots without removing history | [managed-agent.md](managed-agent.md) |
| `bl managed-agent version enable` | No Auth | Enable Apply-time local snapshots for agents.yaml | [managed-agent.md](managed-agent.md) |
| `bl managed-agent version list` | No Auth | List local snapshots of agents.yaml | [managed-agent.md](managed-agent.md) |
| `bl managed-agent version preview` | No Auth | Preview a historical agents.yaml version | [managed-agent.md](managed-agent.md) |
| `bl managed-agent version restore` | No Auth | Restore a historical agents.yaml version to the working tree | [managed-agent.md](managed-agent.md) |
| `bl managed-agent version status` | No Auth | Show local Git versioning status for agents.yaml | [managed-agent.md](managed-agent.md) |
| `bl managed-agent version status` | No Auth | Show local snapshot versioning status for agents.yaml | [managed-agent.md](managed-agent.md) |
| `bl managed-agent workbench` | API Key | Launch the agents.yaml project Workbench | [managed-agent.md](managed-agent.md) |
## By group
@@ -11,7 +11,7 @@ Index: [index.md](index.md)
| ---------------------------------- | -------------- | ------------------------------------------------------------- |
| `bl managed-agent apply` | API Key | Apply planned changes to create/update/delete agent resources |
| `bl managed-agent destroy` | API Key | Destroy all managed agent resources tracked in state |
| `bl managed-agent init` | No Auth | Create an agents.yaml template or a local CI/Git project |
| `bl managed-agent init` | No Auth | Create an agents.yaml template |
| `bl managed-agent plan` | API Key | Show what changes would be applied to agent infrastructure |
| `bl managed-agent playground` | API Key | Launch a Session Preview for an agents.yaml Agent |
| `bl managed-agent session create` | API Key | Create a new session for an agent |
@@ -27,24 +27,24 @@ Index: [index.md](index.md)
| `bl managed-agent state rm` | No Auth | Remove a resource from state without destroying it remotely |
| `bl managed-agent state show` | No Auth | Show details of a resource in agents state |
| `bl managed-agent validate` | No Auth | Validate an agents.yaml configuration (offline) |
| `bl managed-agent version disable` | No Auth | Disable Apply-time Git versioning without removing history |
| `bl managed-agent version enable` | No Auth | Enable Apply-time Git versioning for agents.yaml |
| `bl managed-agent version list` | No Auth | List current-branch commits that changed agents.yaml |
| `bl managed-agent version disable` | No Auth | Disable Apply-time local snapshots without removing history |
| `bl managed-agent version enable` | No Auth | Enable Apply-time local snapshots for agents.yaml |
| `bl managed-agent version list` | No Auth | List local snapshots of agents.yaml |
| `bl managed-agent version preview` | No Auth | Preview a historical agents.yaml version |
| `bl managed-agent version restore` | No Auth | Restore a historical agents.yaml version to the working tree |
| `bl managed-agent version status` | No Auth | Show local Git versioning status for agents.yaml |
| `bl managed-agent version status` | No Auth | Show local snapshot versioning status for agents.yaml |
| `bl managed-agent workbench` | API Key | Launch the agents.yaml project Workbench |
## Command details
### `bl managed-agent apply`
| Field | Value |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| **Name** | `managed-agent apply` |
| **Description** | Apply planned changes to create/update/delete agent resources |
| **Authentication** | API Key |
| **Usage** | `bl managed-agent apply [--file <path>] [--provider <name>] [--yes \| --ci] [--no-refresh] [--refresh-only] [--concurrency <n>]` |
| Field | Value |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| **Name** | `managed-agent apply` |
| **Description** | Apply planned changes to create/update/delete agent resources |
| **Authentication** | API Key |
| **Usage** | `bl managed-agent apply [--file <path>] [--provider <name>] [--yes] [--no-refresh] [--refresh-only] [--concurrency <n>]` |
#### Flags
@@ -53,7 +53,6 @@ Index: [index.md](index.md)
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
| `--provider <name>` | string | no | Target provider (default: all configured) |
| `--yes` | switch | no | Confirm and apply without an interactive prompt (required to mutate) |
| `--ci` | switch | no | Run non-interactively while blocking deletes and remote drift |
| `--no-refresh` | switch | no | Skip refreshing state from remote before planning |
| `--refresh-only` | switch | no | Refresh state without mutating remote resources |
| `--concurrency <n>` | number | no | Max independent resources to apply in parallel (default 6, max 10) |
@@ -76,10 +75,6 @@ bl managed-agent apply --yes
bl managed-agent apply --provider bailian --yes
```
```bash
bl managed-agent apply --ci
```
### `bl managed-agent destroy`
| Field | Value |
@@ -117,12 +112,12 @@ bl managed-agent destroy --yes --cascade
### `bl managed-agent init`
| Field | Value |
| ------------------ | --------------------------------------------------------------------------------------------------------------- |
| **Name** | `managed-agent init` |
| **Description** | Create an agents.yaml template or a local CI/Git project |
| **Authentication** | No Auth |
| **Usage** | `bl managed-agent init [--provider <name>] [--agent-name <name>] [--file <path>] [--git <directory>] [--force]` |
| Field | Value |
| ------------------ | ------------------------------------------------------------------------------------------- |
| **Name** | `managed-agent init` |
| **Description** | Create an agents.yaml template |
| **Authentication** | No Auth |
| **Usage** | `bl managed-agent init [--provider <name>] [--agent-name <name>] [--file <path>] [--force]` |
#### Flags
@@ -131,7 +126,6 @@ bl managed-agent destroy --yes --cascade
| `--provider <bailian\|claude\|qoder\|ark\|all>` | string | no | Provider: bailian, claude, qoder, ark, all (default: bailian) |
| `--agent-name <name>` | string | no | Name of the first agent (default: assistant) |
| `--file <path>` | string | no | Output config path (default: agents.yaml) |
| `--git <directory>` | string | no | Create or add CI/Git scaffolding in this project directory |
| `--force` | switch | no | Overwrite an existing config file |
#### Examples
@@ -144,14 +138,6 @@ bl managed-agent init
bl managed-agent init --provider bailian --agent-name assistant
```
```bash
bl managed-agent init --git ./my-agents
```
```bash
bl managed-agent init --git .
```
### `bl managed-agent plan`
| Field | Value |
@@ -218,7 +204,7 @@ bl managed-agent plan --no-refresh
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
- Workbench requires Node.js 22+ and starts the shared @openagentpack/playground package locally. It does not push Git commits or switch branches.
- Workbench requires Node.js 22+ and starts the shared @openagentpack/playground package locally. Local versions use the shared .openagentpack/versions project store and do not require Git.
#### Examples
@@ -681,12 +667,12 @@ bl managed-agent validate --file agents.yaml
### `bl managed-agent version disable`
| Field | Value |
| ------------------ | ---------------------------------------------------------- |
| **Name** | `managed-agent version disable` |
| **Description** | Disable Apply-time Git versioning without removing history |
| **Authentication** | No Auth |
| **Usage** | `bl managed-agent version disable [--file <path>]` |
| Field | Value |
| ------------------ | ----------------------------------------------------------- |
| **Name** | `managed-agent version disable` |
| **Description** | Disable Apply-time local snapshots without removing history |
| **Authentication** | No Auth |
| **Usage** | `bl managed-agent version disable [--file <path>]` |
#### Flags
@@ -709,7 +695,7 @@ bl managed-agent version disable --file agents.yaml
| Field | Value |
| ------------------ | ------------------------------------------------- |
| **Name** | `managed-agent version enable` |
| **Description** | Enable Apply-time Git versioning for agents.yaml |
| **Description** | Enable Apply-time local snapshots for agents.yaml |
| **Authentication** | No Auth |
| **Usage** | `bl managed-agent version enable [--file <path>]` |
@@ -734,7 +720,7 @@ bl managed-agent version enable --file agents.yaml
| Field | Value |
| ------------------ | --------------------------------------------------------------------------------- |
| **Name** | `managed-agent version list` |
| **Description** | List current-branch commits that changed agents.yaml |
| **Description** | List local snapshots of agents.yaml |
| **Authentication** | No Auth |
| **Usage** | `bl managed-agent version list [--file <path>] [--limit <n>] [--cursor <cursor>]` |
@@ -758,65 +744,65 @@ bl managed-agent version list --limit 20 --output json
### `bl managed-agent version preview`
| Field | Value |
| ------------------ | ---------------------------------------------------------------------- |
| **Name** | `managed-agent version preview` |
| **Description** | Preview a historical agents.yaml version |
| **Authentication** | No Auth |
| **Usage** | `bl managed-agent version preview --commit <full-sha> [--file <path>]` |
| Field | Value |
| ------------------ | ------------------------------------------------------------------------------ |
| **Name** | `managed-agent version preview` |
| **Description** | Preview a historical agents.yaml version |
| **Authentication** | No Auth |
| **Usage** | `bl managed-agent version preview --version-id <full-version> [--file <path>]` |
#### Flags
| Flag | Type | Required | Description |
| --------------------- | ------ | -------- | --------------------------------------- |
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
| `--commit <full-sha>` | string | yes | Full commit SHA from the current branch |
| Flag | Type | Required | Description |
| ----------------------------- | ------ | -------- | --------------------------------------- |
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
| `--version-id <full-version>` | string | yes | Full local version ID |
#### Examples
```bash
bl managed-agent version preview --commit <full-sha>
bl managed-agent version preview --version-id <full-version>
```
```bash
bl managed-agent version preview --commit <full-sha> --output json
bl managed-agent version preview --version-id <full-version> --output json
```
### `bl managed-agent version restore`
| Field | Value |
| ------------------ | ------------------------------------------------------------------------------ |
| **Name** | `managed-agent version restore` |
| **Description** | Restore a historical agents.yaml version to the working tree |
| **Authentication** | No Auth |
| **Usage** | `bl managed-agent version restore --commit <full-sha> [--file <path>] [--yes]` |
| Field | Value |
| ------------------ | -------------------------------------------------------------------------------------- |
| **Name** | `managed-agent version restore` |
| **Description** | Restore a historical agents.yaml version to the working tree |
| **Authentication** | No Auth |
| **Usage** | `bl managed-agent version restore --version-id <full-version> [--file <path>] [--yes]` |
#### Flags
| Flag | Type | Required | Description |
| --------------------- | ------ | -------- | ------------------------------------------- |
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
| `--commit <full-sha>` | string | yes | Full commit SHA from the current branch |
| `--yes` | switch | no | Restore without an interactive confirmation |
| Flag | Type | Required | Description |
| ----------------------------- | ------ | -------- | ------------------------------------------- |
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
| `--version-id <full-version>` | string | yes | Full local version ID |
| `--yes` | switch | no | Restore without an interactive confirmation |
#### Examples
```bash
bl managed-agent version restore --commit <full-sha>
bl managed-agent version restore --version-id <full-version>
```
```bash
bl managed-agent version restore --commit <full-sha> --yes --output json
bl managed-agent version restore --version-id <full-version> --yes --output json
```
### `bl managed-agent version status`
| Field | Value |
| ------------------ | ------------------------------------------------- |
| **Name** | `managed-agent version status` |
| **Description** | Show local Git versioning status for agents.yaml |
| **Authentication** | No Auth |
| **Usage** | `bl managed-agent version status [--file <path>]` |
| Field | Value |
| ------------------ | ----------------------------------------------------- |
| **Name** | `managed-agent version status` |
| **Description** | Show local snapshot versioning status for agents.yaml |
| **Authentication** | No Auth |
| **Usage** | `bl managed-agent version status [--file <path>]` |
#### Flags
@@ -858,7 +844,7 @@ bl managed-agent version status --file agents.yaml --output json
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
- Workbench requires Node.js 22+ and starts the shared @openagentpack/playground package locally. It does not push Git commits or switch branches.
- Workbench requires Node.js 22+ and starts the shared @openagentpack/playground package locally. Local versions use the shared .openagentpack/versions project store and do not require Git.
#### Examples