feat: add AgentRun L1 manager lifecycle

This commit is contained in:
pikastech
2026-07-20 07:55:55 +02:00
parent 88d25dd7db
commit 65d6ccdf1d
8 changed files with 497 additions and 2 deletions
+25
View File
@@ -45,6 +45,31 @@ bun scripts/cli.ts agentrun create task \
--prompt-stdin
```
AgentRun L1 native manager 只使用 YAML-first 生命周期:
```bash
bun scripts/cli.ts agentrun native-development manager start
bun scripts/cli.ts agentrun native-development manager status
bun scripts/cli.ts agentrun native-development manager logs
bun scripts/cli.ts agentrun native-development manager restart
```
- 配置唯一来源是 `config/agentrun.yaml#nativeDevelopment.manager`
- 固定 workspace、port、state、store、Kafka、runner、Kubernetes transport
和 SecretRef 不得由命令行或临时环境变量覆盖;
- host-native manager 使用 YAML 声明的 `kubeconfig`
`kubectlCommand` 创建 Kubernetes Job;集群部署未声明
`kubectlCommand` 时继续使用既有 in-cluster HTTPS transport
- Workbench L1 直连 AgentRun L1 manager,不依赖 AgentRun L2 rollout
或固定 HTTPS 公共入口完成内部业务回归;
- Workbench smoke 必须通过产品 `--over-api` 路径提交,并只从
`hwlab.event.v1` 的 Kafka retention replay 与 live SSE 判断进展和终态;
- `runner-dispatch-*` 表示 manager 装配、观察或创建 Job 的阶段;
- `runner-startup-*` 表示 Job 已创建后的 runner 启动和 backend 连接阶段;
- Pod `Running` 但 runner 未 claim 时仍未进入 provider 调用,不能归因给 provider
- 固定 HTTPS 公共入口未就绪时单独登记公共服务面问题,禁止修改
Workbench 到 AgentRun L1 的内部连接绕过该问题。
HWLAB Agent Observer 分页快照验收使用目标 node/lane HWLAB workspace 内的 typed CLI
```bash
+79
View File
@@ -26,6 +26,85 @@ auth:
file: /root/.unidesk/.env/agentrun.env
header: Authorization
scheme: Bearer
nativeDevelopment:
manager:
workspace: /root/agentrun-v02
bindHost: 0.0.0.0
probeHost: 127.0.0.1
port: 6680
store: memory
stateDir: .state
apiKeySourceRef: hwlab/nc01-v03-admin.env
apiKeySourceKey: HWLAB_API_KEY
kubeconfig: /etc/rancher/k3s/k3s.yaml
kubectlCommand: kubectl
runtimeNamespace: agentrun-v02
lane: v0.2
callbackBaseUrl: http://152.53.229.148:6680
unideskSshEndpointEnv:
name: UNIDESK_MAIN_SERVER_IP
value: 152.53.229.148
runner:
image: 127.0.0.1:5000/agentrun/agentrun-mgr-env@sha256:929c350b07b48aac8450ff52e5ca575686016cf85192af435700e8efb85b0e7b
serviceAccount: agentrun-nc01-v02-runner
jobNamePrefix: agentrun-nc01-v02-l1-runner
apiKeySecretRef:
name: agentrun-v02-api-key
key: HWLAB_API_KEY
idleTimeoutMs: 600000
backendRetry:
maxAttempts: 5
initialBackoffMs: 1000
maxBackoffMs: 30000
egressProxyUrl: http://10.42.0.1:10808
noProxyExtra:
- localhost
- 127.0.0.1
- "::1"
- .svc
- .svc.cluster.local
- .cluster.local
- hyueapi.com
- .hyueapi.com
- api.hwpod.com
- api.pikapython.com
- .pikapython.com
dispatcher:
intervalMs: 1000
batchSize: 10
leaseMs: 120000
attemptTimeoutMs: 90000
maxAttempts: 5
retryBackoffMs: 5000
reconciler:
intervalMs: 2000
batchSize: 20
startupRetry:
maxAttempts: 5
initialBackoffMs: 2000
maxBackoffMs: 16000
multiplier: 2
totalDeadlineMs: 60000
jitterRatio: 0.2
kafka:
bootstrapServers: 10.43.4.158:9092
dnsServers:
- 10.43.0.10
dnsSearchDomains:
- cluster.local
eventTopic: agentrun.event.v1
clientId: agentrun-v02-native-l1-manager
stdioTopic: codex-stdio.raw.v1
stdioClientId: agentrun-v02-native-l1-runner
outboxIntervalMs: 500
outboxBatchSize: 100
outboxLeaseMs: 60000
outboxRetryBackoffMs: 5000
publicExposure:
publicBaseUrl: https://agentrun-dev.hwpod.com
hostname: agentrun-dev.hwpod.com
expectedA: 152.53.229.148
upstream: http://127.0.0.1:6680
client:
role: render-only
transport: target-trans
+1 -1
View File
@@ -152,7 +152,7 @@ lanes:
retryMaximumIntervalMs: 10000
retryMaximumAttempts: 1
agentRun:
managerUrl: http://10.43.171.18:8080
managerUrl: http://127.0.0.1:6680
runtimeApiUrl: http://hwlab-cloud-api.hwlab-v03.svc.cluster.local:6667
runtimeWebUrl: http://hwlab-cloud-web.hwlab-v03.svc.cluster.local:8080
kafka:
+38
View File
@@ -76,6 +76,44 @@ bun scripts/cli.ts agentrun control-plane plan --node NC01 --lane nc01-release
不得把临时 NodePort、host port、pod IP、provider-gateway 业务 HTTP proxy 或一次性 port-forward 固化为 AgentRun 部署路径。任何公网入口、UniDesk/HWLAB 集成入口或跨服务访问路径,都必须先通过 AgentRun 仓库内经过审查的变更引入;UniDesk 只在后续记录对应运维入口。
## L1 Native Manager
AgentRun L1 manager 用于不依赖 CI/CD、GitOps rollout 或集群 manager
Deployment 的 native 联调。生命周期统一走:
```bash
bun scripts/cli.ts agentrun native-development manager start
bun scripts/cli.ts agentrun native-development manager status
bun scripts/cli.ts agentrun native-development manager logs
bun scripts/cli.ts agentrun native-development manager restart
```
- 唯一配置来源是 `config/agentrun.yaml#nativeDevelopment.manager`
- CLI 负责解析固定 workspace、port、state directory、store、Kafka、
runner、Kubernetes transport 和 SecretRef
- host-native manager 使用 YAML 声明的 `kubeconfig`
`kubectlCommand` 装配既有 explicit-kubectl transport
- 集群 manager 未设置 `kubectlCommand` 时继续使用既有
in-cluster HTTPS transport
- L1 使用独立 store 和 runner Job 前缀,不能与 development 或
release manager 共用 queue、lease 或 reconciler 写侧;
- Workbench L1 直接连接 AgentRun L1 managerAgentRun 固定 HTTPS
公共入口未就绪时单独登记公共服务面问题,不能改写内部连接关系;
- Workbench smoke 只通过产品 `--over-api` 入口提交;用户可见事件只由
`hwlab.event.v1` 的 Kafka retention replay 和 live SSE 提供;
- 禁止通过 REST result 合成、数据库快照或第二条 SSE 路径补终态。
故障定位先按阶段判定,避免重复进入大回环:
- `runner-dispatch-*`manager 的配置装配、Kubernetes 观察或 Job 创建;
- `runner-startup-*`Job 创建后的 runner 启动、claim 和 backend 连接;
- Pod 已进入 `Running` 但 runner 未 claim:仍处于 provider 调用之前;
- `schema-invalid``auth-missing``auth-failed`
`tenant-policy-denied``secret-unavailable`:确定性故障,首次失败即终态;
- DNS、连接超时、临时 `5xx` 和 Kubernetes observation:按 owning YAML
的有限重试策略退让,耗尽后发布语义化 `infra-failed`
- provider 尚未启动时禁止把耗时或失败归因到 provider。
## 受控 CI/CD 入口
AgentRun 目标必须从 `config/agentrun.yaml` 解析,不得从 service repo 的 `deploy.json` 或 URL 片段推断。Delivery authority 还必须与 `config/platform-infra/gitea.yaml``config/platform-infra/pipelines-as-code.yaml` 的 consumer/repository 组合校验。当前默认 `NC01/nc01-v02` 是 PaC migrated consumer;目标 source branch 的 GitHub PR merge 是唯一正式交付触发。`nc01-release` 的独立 PaC consumer 属于 #2154 后续阶段,R1 仅完成 namespace 与 lane 声明。
@@ -262,6 +262,19 @@ runner 启动和 backend 执行故障必须由 manager/reconciler 统一归类
重试策略属于核心执行配置,必须由 owning YAML 声明基础退让、倍数、最大退让、最大次数或总截止时间以及 jitter。连接拒绝、临时 DNS、超时和上游 `5xx` 等瞬时故障可以指数退让;镜像不存在、鉴权拒绝、非法镜像名和缺失必需 Secret 等确定性故障必须立即形成不可重试终态。重试耗尽必须在策略窗口内结束 command/run,禁止继续依赖通用 retention 或 stale GC 让用户长时间停留在无解释的 running 状态。
#### 6.6.1 L1 native manager
AgentRun 必须提供不依赖 CI/CD、GitOps rollout 或集群 manager Deployment 的 L1 native manager 生命周期。
启动、停止、重启、状态和日志统一由 UniDesk 受控 CLI 管理,并从 `config/agentrun.yaml` 解析固定 workspace、端口、状态目录、公开 HTTPS 入口、Secret sourceRef、Kafka topic 和 runner 装配参数。
L1 manager 必须使用独立 store 和独立 runner job 前缀,禁止与 development 或 release 集群 manager 共用 durable queue、lease 或 reconciler 写侧。
L1 可以复用既有 Kubernetes Job runner 执行模型和 Kafka event topic,但不得引入 host-native runner、第二套终态或第二套用户可见事件路径。
Workbench L1 必须直连 AgentRun L1AgentRun L1 的启动和基础健康不得依赖 PaC、镜像发布或集群 manager rollout。
L1 使用内存 store 时只允许作为显式 native development 模式,并且仍必须启用 Kafka outbox,保证 `PostgreSQL/outbox -> Kafka` 在开发态对应为 `memory/outbox -> Kafka`,前端继续只消费 Kafka 重放与纯 SSE。
runner 回调 manager 必须使用 owning YAML 声明且 Pod 可达的固定公开地址,不能使用 `127.0.0.1`
固定 HTTPS 用户入口由共享 public-edge owning YAML 聚合;其 reconcile 未完成时只能形成非阻塞 warning,不能阻塞 L1 通过固定公网 IP 和 port 完成业务回归。
### 6.7 AR-CORE-REQ-007 runner terminal durable outbox
| 编号 | 短名 | 主责模块 | 关联模块 |
+4
View File
@@ -37,6 +37,7 @@ import { decideCicdDeliveryMutation, resolveCicdDeliveryAuthority } from "../cic
import type { AgentRunResourceVerb, AgentRunRestCompatGroup } from "./utils";
import { controlPlaneApply, controlPlanePlan, parseCleanupReleasedPvOptions, parseCleanupRunnersOptions, parseCleanupRunsOptions, parseCleanupSessionPvcsOptions, parseConfirmOptions, parseGitMirrorOptions, parseLaneConfirmOptions, parseRefreshOptions, parseSecretSyncOptions, status } from "./control-plane";
import { gitMirrorStatus } from "./git-mirror";
import { agentRunNativeDevelopmentHelp, runAgentRunNativeDevelopmentCommand } from "./native-development";
import { agentRunExplain, isRecord, parseGitMirrorStatusOptions, parseStatusOptions, parseTriggerOptions } from "./options";
import { renderAgentRunControlPlaneActionSummary, renderAgentRunControlPlanePlanSummary, renderAgentRunControlPlaneStatusSummary } from "./public-exposure";
import { providerProfileHelp, runProviderProfileCommand } from "./provider-profile";
@@ -74,6 +75,7 @@ export function agentRunHelp(): unknown {
"bun scripts/cli.ts agentrun send session/<sessionId> --aipod Artificer --prompt-stdin",
"bun scripts/cli.ts agentrun explain task",
"bun scripts/cli.ts agentrun explain session-policy",
"bun scripts/cli.ts agentrun native-development manager start|stop|restart|status|logs",
"bun scripts/cli.ts agentrun control-plane plan --node NC01 --lane nc01-v02",
"bun scripts/cli.ts agentrun control-plane status --node NC01 --lane nc01-v02",
"bun scripts/cli.ts platform-infra pipelines-as-code history --target NC01 --consumer agentrun-nc01-v02 --limit 10",
@@ -110,11 +112,13 @@ export async function runAgentRunCommand(config: UniDeskConfig | null, args: str
const route = normalizeAgentRunCommandArgs(args);
const { group, action, actionArgs } = route;
if (group === undefined || isHelpArg(group)) return renderAgentRunHelp(route.canonicalArgs);
if (group === "native-development" && (isHelpArg(action) || actionArgs.some(isHelpArg))) return agentRunNativeDevelopmentHelp();
if (isHelpArg(action) || actionArgs.some(isHelpArg)) return renderAgentRunHelp(route.canonicalArgs);
if (isResourceVerb(group)) return await runAgentRunResourceCommand(config, group, action, actionArgs, route.canonicalArgs);
if (isAgentRunRestCompatGroup(group)) {
return await runAgentRunRestCompatCommand(config, group, route.forwardArgs, route.canonicalArgs);
}
if (group === "native-development") return runAgentRunNativeDevelopmentCommand(route.forwardArgs);
if (config === null) throw new Error("agentrun control-plane and git-mirror commands require UniDesk config");
if (group === "control-plane") {
if (action === "plan") {
+336
View File
@@ -0,0 +1,336 @@
import { existsSync } from "node:fs";
import { isAbsolute, join } from "node:path";
import { rootPath } from "../config";
import { runCommand } from "../command";
import {
asRecord,
integerField,
numberField,
readYamlRecord,
recordField,
stringArrayField,
stringField,
} from "../platform-infra-ops-library";
import { readEnvSourceFile, requiredEnvValue } from "../secrets";
type NativeAction = "start" | "stop" | "restart" | "status" | "logs";
interface AgentRunNativeManagerSpec {
workspace: string;
bindHost: string;
probeHost: string;
port: number;
store: "memory";
stateDir: string;
apiKeySourceRef: string;
apiKeySourceKey: string;
kubeconfig: string;
kubectlCommand: string;
runtimeNamespace: string;
lane: string;
callbackBaseUrl: string;
unideskSshEndpointEnv: { name: string; value: string };
runner: {
image: string;
serviceAccount: string;
jobNamePrefix: string;
apiKeySecretRef: { name: string; key: string };
idleTimeoutMs: number;
backendRetry: { maxAttempts: number; initialBackoffMs: number; maxBackoffMs: number };
egressProxyUrl: string;
noProxyExtra: string[];
};
dispatcher: {
intervalMs: number;
batchSize: number;
leaseMs: number;
attemptTimeoutMs: number;
maxAttempts: number;
retryBackoffMs: number;
};
reconciler: { intervalMs: number; batchSize: number };
startupRetry: {
maxAttempts: number;
initialBackoffMs: number;
maxBackoffMs: number;
multiplier: number;
totalDeadlineMs: number;
jitterRatio: number;
};
kafka: {
bootstrapServers: string;
dnsServers: string[];
dnsSearchDomains: string[];
eventTopic: string;
clientId: string;
stdioTopic: string;
stdioClientId: string;
outboxIntervalMs: number;
outboxBatchSize: number;
outboxLeaseMs: number;
outboxRetryBackoffMs: number;
};
publicExposure: {
publicBaseUrl: string;
hostname: string;
expectedA: string;
upstream: string;
};
}
const CONFIG_PATH = "config/agentrun.yaml";
export function agentRunNativeDevelopmentHelp(): Record<string, unknown> {
return {
ok: true,
command: "agentrun native-development manager",
usage: [
"bun scripts/cli.ts agentrun native-development manager start",
"bun scripts/cli.ts agentrun native-development manager stop|restart|status|logs",
],
contract: "AgentRun L1 manager 由 owning YAML 独立启动,使用独立 store/job 前缀,并通过固定 HTTPS 域名供 runner 回调。",
configSource: `${CONFIG_PATH}#nativeDevelopment.manager`,
};
}
export function runAgentRunNativeDevelopmentCommand(args: string[]): Record<string, unknown> {
if (args.length === 0 || args.some(isHelpArg)) return agentRunNativeDevelopmentHelp();
if (args[0] !== "manager") throw new Error("agentrun native-development service must be manager");
const action = nativeAction(args[1]);
const spec = readNativeManagerSpec();
if (action === "restart") {
const stopped = runManagerAction(spec, "stop");
if (stopped.ok === false) return { ...stopped, action: "restart", restartStage: "stop" };
const started = runManagerAction(spec, "start");
return { ...started, action: "restart", stopResult: stopped.serviceResult };
}
return runManagerAction(spec, action);
}
function runManagerAction(spec: AgentRunNativeManagerSpec, action: Exclude<NativeAction, "restart">): Record<string, unknown> {
validateLocalInputs(spec);
const command = [
"bun",
"scripts/agentrun-cli.ts",
"server",
action,
"--port",
String(spec.port),
...(action === "start" ? ["--host", spec.bindHost, "--store", spec.store] : []),
];
const result = runCommand(command, spec.workspace, {
timeoutMs: 15_000,
env: { ...process.env, ...nativeEnvironment(spec) },
});
const payload = parsePayload(result.stdout);
const serviceResult = payload.data && typeof payload.data === "object" && !Array.isArray(payload.data)
? payload.data as Record<string, unknown>
: payload;
const serviceOk = result.exitCode === 0 && payload.ok !== false;
return {
ok: serviceOk,
command: `agentrun native-development manager ${action}`,
service: "manager",
action,
configSource: `${CONFIG_PATH}#nativeDevelopment.manager`,
userEndpoint: { url: spec.publicExposure.publicBaseUrl },
debugEndpoint: { url: spec.callbackBaseUrl },
endpoints: {
bind: { host: spec.bindHost, port: spec.port },
probe: { host: spec.probeHost, port: spec.port, url: `http://${spec.probeHost}:${spec.port}/health/readiness` },
public: { url: spec.publicExposure.publicBaseUrl },
},
serviceResult,
warnings: [{
code: "agentrun-native-public-edge-reconcile-not-required-for-l1",
blocking: false,
detail: `L1 runner callback uses ${spec.callbackBaseUrl}; fixed HTTPS exposure remains owned by config/platform-infra/public-edge.yaml.`,
}],
...(result.exitCode === 0 ? {} : {
error: {
code: "agentrun_native_service_failed",
exitCode: result.exitCode,
stderr: result.stderr.trim().slice(-2000),
},
}),
};
}
function nativeEnvironment(spec: AgentRunNativeManagerSpec): NodeJS.ProcessEnv {
const sourceCommitResult = runCommand(["git", "rev-parse", "HEAD"], spec.workspace, { timeoutMs: 5_000 });
const sourceCommit = sourceCommitResult.stdout.trim();
if (sourceCommitResult.exitCode !== 0 || !/^[0-9a-f]{40}$/u.test(sourceCommit)) {
throw new Error(`cannot resolve AgentRun source commit from ${spec.workspace}`);
}
return {
AGENTRUN_STORE: spec.store,
AGENTRUN_API_KEY_FILE: secretSourcePath(spec.apiKeySourceRef),
KUBECONFIG: spec.kubeconfig,
AGENTRUN_KUBECTL_COMMAND: spec.kubectlCommand,
AGENTRUN_SOURCE_COMMIT: sourceCommit,
AGENTRUN_RUNTIME_NAMESPACE: spec.runtimeNamespace,
AGENTRUN_LANE: spec.lane,
AGENTRUN_INTERNAL_MGR_URL: spec.callbackBaseUrl,
[`AGENTRUN_${spec.unideskSshEndpointEnv.name}`]: spec.unideskSshEndpointEnv.value,
AGENTRUN_RUNNER_IMAGE: spec.runner.image,
AGENTRUN_RUNNER_SERVICE_ACCOUNT: spec.runner.serviceAccount,
AGENTRUN_RUNNER_JOB_NAME_PREFIX: spec.runner.jobNamePrefix,
AGENTRUN_RUNNER_API_KEY_SECRET_NAME: spec.runner.apiKeySecretRef.name,
AGENTRUN_RUNNER_API_KEY_SECRET_KEY: spec.runner.apiKeySecretRef.key,
AGENTRUN_RUNNER_IDLE_TIMEOUT_MS: String(spec.runner.idleTimeoutMs),
AGENTRUN_BACKEND_RETRY_MAX_ATTEMPTS: String(spec.runner.backendRetry.maxAttempts),
AGENTRUN_BACKEND_RETRY_INITIAL_BACKOFF_MS: String(spec.runner.backendRetry.initialBackoffMs),
AGENTRUN_BACKEND_RETRY_MAX_BACKOFF_MS: String(spec.runner.backendRetry.maxBackoffMs),
AGENTRUN_RUNNER_EGRESS_PROXY_URL: spec.runner.egressProxyUrl,
AGENTRUN_RUNNER_NO_PROXY_EXTRA: spec.runner.noProxyExtra.join(","),
AGENTRUN_RUNNER_DISPATCHER_ENABLED: "true",
AGENTRUN_RUNNER_DISPATCHER_INTERVAL_MS: String(spec.dispatcher.intervalMs),
AGENTRUN_RUNNER_DISPATCHER_BATCH_SIZE: String(spec.dispatcher.batchSize),
AGENTRUN_RUNNER_DISPATCHER_LEASE_MS: String(spec.dispatcher.leaseMs),
AGENTRUN_RUNNER_DISPATCHER_ATTEMPT_TIMEOUT_MS: String(spec.dispatcher.attemptTimeoutMs),
AGENTRUN_RUNNER_DISPATCHER_MAX_ATTEMPTS: String(spec.dispatcher.maxAttempts),
AGENTRUN_RUNNER_DISPATCHER_RETRY_BACKOFF_MS: String(spec.dispatcher.retryBackoffMs),
AGENTRUN_MANAGER_RECONCILER_ENABLED: "true",
AGENTRUN_MANAGER_RECONCILER_INTERVAL_MS: String(spec.reconciler.intervalMs),
AGENTRUN_MANAGER_RECONCILER_BATCH_SIZE: String(spec.reconciler.batchSize),
AGENTRUN_RUNNER_STARTUP_RETRY_MAX_ATTEMPTS: String(spec.startupRetry.maxAttempts),
AGENTRUN_RUNNER_STARTUP_RETRY_INITIAL_BACKOFF_MS: String(spec.startupRetry.initialBackoffMs),
AGENTRUN_RUNNER_STARTUP_RETRY_MAX_BACKOFF_MS: String(spec.startupRetry.maxBackoffMs),
AGENTRUN_RUNNER_STARTUP_RETRY_MULTIPLIER: String(spec.startupRetry.multiplier),
AGENTRUN_RUNNER_STARTUP_RETRY_TOTAL_DEADLINE_MS: String(spec.startupRetry.totalDeadlineMs),
AGENTRUN_RUNNER_STARTUP_RETRY_JITTER_RATIO: String(spec.startupRetry.jitterRatio),
AGENTRUN_KAFKA_ENABLED: "true",
AGENTRUN_KAFKA_BOOTSTRAP_SERVERS: spec.kafka.bootstrapServers,
AGENTRUN_KAFKA_DNS_SERVERS: spec.kafka.dnsServers.join(","),
AGENTRUN_KAFKA_DNS_SEARCH_DOMAINS: spec.kafka.dnsSearchDomains.join(","),
AGENTRUN_KAFKA_EVENT_TOPIC: spec.kafka.eventTopic,
AGENTRUN_KAFKA_CLIENT_ID: spec.kafka.clientId,
AGENTRUN_KAFKA_OUTBOX_INTERVAL_MS: String(spec.kafka.outboxIntervalMs),
AGENTRUN_KAFKA_OUTBOX_BATCH_SIZE: String(spec.kafka.outboxBatchSize),
AGENTRUN_KAFKA_OUTBOX_LEASE_MS: String(spec.kafka.outboxLeaseMs),
AGENTRUN_KAFKA_OUTBOX_RETRY_BACKOFF_MS: String(spec.kafka.outboxRetryBackoffMs),
AGENTRUN_KAFKA_STDIO_PRODUCE_ENABLED: "true",
AGENTRUN_KAFKA_STDIO_TOPIC: spec.kafka.stdioTopic,
AGENTRUN_KAFKA_STDIO_CLIENT_ID: spec.kafka.stdioClientId,
};
}
function validateLocalInputs(spec: AgentRunNativeManagerSpec): void {
if (!existsSync(spec.workspace)) throw new Error(`AgentRun native workspace is missing: ${spec.workspace}`);
if (!existsSync(spec.kubeconfig)) throw new Error(`AgentRun native kubeconfig is missing: ${spec.kubeconfig}`);
if (spec.stateDir !== ".state") throw new Error(`${CONFIG_PATH}#nativeDevelopment.manager.stateDir must be .state for the AgentRun server lifecycle`);
const sourcePath = secretSourcePath(spec.apiKeySourceRef);
const source = readEnvSourceFile({ root: "/root/.unidesk/.state/secrets", sourceRef: spec.apiKeySourceRef });
requiredEnvValue(source.values, spec.apiKeySourceKey, spec.apiKeySourceRef);
if (!existsSync(sourcePath)) throw new Error(`AgentRun native API key source is missing: ${spec.apiKeySourceRef}`);
}
function secretSourcePath(sourceRef: string): string {
if (sourceRef.includes("..")) throw new Error("AgentRun native API key sourceRef must not contain ..");
return isAbsolute(sourceRef) ? sourceRef : join("/root/.unidesk/.state/secrets", ...sourceRef.split("/"));
}
function readNativeManagerSpec(): AgentRunNativeManagerSpec {
const root = readYamlRecord(rootPath(CONFIG_PATH), "AgentRunConfig");
const manager = recordField(recordField(root, "nativeDevelopment", CONFIG_PATH), "manager", `${CONFIG_PATH}.nativeDevelopment`);
const runner = recordField(manager, "runner", `${CONFIG_PATH}.nativeDevelopment.manager`);
const runnerSecret = recordField(runner, "apiKeySecretRef", `${CONFIG_PATH}.nativeDevelopment.manager.runner`);
const backendRetry = recordField(runner, "backendRetry", `${CONFIG_PATH}.nativeDevelopment.manager.runner`);
const dispatcher = recordField(manager, "dispatcher", `${CONFIG_PATH}.nativeDevelopment.manager`);
const reconciler = recordField(manager, "reconciler", `${CONFIG_PATH}.nativeDevelopment.manager`);
const startupRetry = recordField(manager, "startupRetry", `${CONFIG_PATH}.nativeDevelopment.manager`);
const kafka = recordField(manager, "kafka", `${CONFIG_PATH}.nativeDevelopment.manager`);
const exposure = recordField(manager, "publicExposure", `${CONFIG_PATH}.nativeDevelopment.manager`);
const unideskSshEndpointEnv = recordField(manager, "unideskSshEndpointEnv", `${CONFIG_PATH}.nativeDevelopment.manager`);
const store = stringField(manager, "store", `${CONFIG_PATH}.nativeDevelopment.manager`);
if (store !== "memory") throw new Error(`${CONFIG_PATH}.nativeDevelopment.manager.store must be memory`);
const publicBaseUrl = stringField(exposure, "publicBaseUrl", `${CONFIG_PATH}.nativeDevelopment.manager.publicExposure`);
if (!publicBaseUrl.startsWith("https://")) throw new Error(`${CONFIG_PATH}.nativeDevelopment.manager.publicExposure.publicBaseUrl must use https`);
const callbackBaseUrl = stringField(manager, "callbackBaseUrl", `${CONFIG_PATH}.nativeDevelopment.manager`);
const callbackHost = new URL(callbackBaseUrl).hostname;
if (callbackHost === "127.0.0.1" || callbackHost === "localhost") throw new Error(`${CONFIG_PATH}.nativeDevelopment.manager.callbackBaseUrl must be reachable from runner Pods`);
const unideskSshEndpointName = stringField(unideskSshEndpointEnv, "name", `${CONFIG_PATH}.nativeDevelopment.manager.unideskSshEndpointEnv`);
if (!["UNIDESK_MAIN_SERVER_IP", "UNIDESK_MAIN_SERVER_HOST", "UNIDESK_FRONTEND_URL"].includes(unideskSshEndpointName)) {
throw new Error(`${CONFIG_PATH}.nativeDevelopment.manager.unideskSshEndpointEnv.name is unsupported`);
}
return {
workspace: stringField(manager, "workspace", `${CONFIG_PATH}.nativeDevelopment.manager`),
bindHost: stringField(manager, "bindHost", `${CONFIG_PATH}.nativeDevelopment.manager`),
probeHost: stringField(manager, "probeHost", `${CONFIG_PATH}.nativeDevelopment.manager`),
port: positiveInteger(manager, "port", `${CONFIG_PATH}.nativeDevelopment.manager`),
store,
stateDir: stringField(manager, "stateDir", `${CONFIG_PATH}.nativeDevelopment.manager`),
apiKeySourceRef: stringField(manager, "apiKeySourceRef", `${CONFIG_PATH}.nativeDevelopment.manager`),
apiKeySourceKey: stringField(manager, "apiKeySourceKey", `${CONFIG_PATH}.nativeDevelopment.manager`),
kubeconfig: stringField(manager, "kubeconfig", `${CONFIG_PATH}.nativeDevelopment.manager`),
kubectlCommand: stringField(manager, "kubectlCommand", `${CONFIG_PATH}.nativeDevelopment.manager`),
runtimeNamespace: stringField(manager, "runtimeNamespace", `${CONFIG_PATH}.nativeDevelopment.manager`),
lane: stringField(manager, "lane", `${CONFIG_PATH}.nativeDevelopment.manager`),
callbackBaseUrl,
unideskSshEndpointEnv: {
name: unideskSshEndpointName,
value: stringField(unideskSshEndpointEnv, "value", `${CONFIG_PATH}.nativeDevelopment.manager.unideskSshEndpointEnv`),
},
runner: {
image: stringField(runner, "image", `${CONFIG_PATH}.nativeDevelopment.manager.runner`),
serviceAccount: stringField(runner, "serviceAccount", `${CONFIG_PATH}.nativeDevelopment.manager.runner`),
jobNamePrefix: stringField(runner, "jobNamePrefix", `${CONFIG_PATH}.nativeDevelopment.manager.runner`),
apiKeySecretRef: {
name: stringField(runnerSecret, "name", `${CONFIG_PATH}.nativeDevelopment.manager.runner.apiKeySecretRef`),
key: stringField(runnerSecret, "key", `${CONFIG_PATH}.nativeDevelopment.manager.runner.apiKeySecretRef`),
},
idleTimeoutMs: positiveInteger(runner, "idleTimeoutMs", `${CONFIG_PATH}.nativeDevelopment.manager.runner`),
backendRetry: positiveIntegerFields(backendRetry, ["maxAttempts", "initialBackoffMs", "maxBackoffMs"], `${CONFIG_PATH}.nativeDevelopment.manager.runner.backendRetry`),
egressProxyUrl: stringField(runner, "egressProxyUrl", `${CONFIG_PATH}.nativeDevelopment.manager.runner`),
noProxyExtra: stringArrayField(runner, "noProxyExtra", `${CONFIG_PATH}.nativeDevelopment.manager.runner`),
},
dispatcher: positiveIntegerFields(dispatcher, ["intervalMs", "batchSize", "leaseMs", "attemptTimeoutMs", "maxAttempts", "retryBackoffMs"], `${CONFIG_PATH}.nativeDevelopment.manager.dispatcher`),
reconciler: positiveIntegerFields(reconciler, ["intervalMs", "batchSize"], `${CONFIG_PATH}.nativeDevelopment.manager.reconciler`),
startupRetry: {
...positiveIntegerFields(startupRetry, ["maxAttempts", "initialBackoffMs", "maxBackoffMs", "totalDeadlineMs"], `${CONFIG_PATH}.nativeDevelopment.manager.startupRetry`),
multiplier: numberField(startupRetry, "multiplier", `${CONFIG_PATH}.nativeDevelopment.manager.startupRetry`),
jitterRatio: numberField(startupRetry, "jitterRatio", `${CONFIG_PATH}.nativeDevelopment.manager.startupRetry`),
},
kafka: {
bootstrapServers: stringField(kafka, "bootstrapServers", `${CONFIG_PATH}.nativeDevelopment.manager.kafka`),
dnsServers: stringArrayField(kafka, "dnsServers", `${CONFIG_PATH}.nativeDevelopment.manager.kafka`),
dnsSearchDomains: stringArrayField(kafka, "dnsSearchDomains", `${CONFIG_PATH}.nativeDevelopment.manager.kafka`),
eventTopic: stringField(kafka, "eventTopic", `${CONFIG_PATH}.nativeDevelopment.manager.kafka`),
clientId: stringField(kafka, "clientId", `${CONFIG_PATH}.nativeDevelopment.manager.kafka`),
stdioTopic: stringField(kafka, "stdioTopic", `${CONFIG_PATH}.nativeDevelopment.manager.kafka`),
stdioClientId: stringField(kafka, "stdioClientId", `${CONFIG_PATH}.nativeDevelopment.manager.kafka`),
...positiveIntegerFields(kafka, ["outboxIntervalMs", "outboxBatchSize", "outboxLeaseMs", "outboxRetryBackoffMs"], `${CONFIG_PATH}.nativeDevelopment.manager.kafka`),
},
publicExposure: {
publicBaseUrl,
hostname: stringField(exposure, "hostname", `${CONFIG_PATH}.nativeDevelopment.manager.publicExposure`),
expectedA: stringField(exposure, "expectedA", `${CONFIG_PATH}.nativeDevelopment.manager.publicExposure`),
upstream: stringField(exposure, "upstream", `${CONFIG_PATH}.nativeDevelopment.manager.publicExposure`),
},
};
}
function positiveIntegerFields<T extends string>(record: Record<string, unknown>, fields: readonly T[], path: string): Record<T, number> {
return Object.fromEntries(fields.map((field) => [field, positiveInteger(record, field, path)])) as Record<T, number>;
}
function positiveInteger(record: Record<string, unknown>, key: string, path: string): number {
const value = integerField(record, key, path);
if (value < 1) throw new Error(`${path}.${key} must be positive`);
return value;
}
function parsePayload(stdout: string): Record<string, unknown> {
const parsed = JSON.parse(stdout) as unknown;
return asRecord(parsed, "AgentRun native CLI output");
}
function nativeAction(value: string | undefined): NativeAction {
if (value === "start" || value === "stop" || value === "restart" || value === "status" || value === "logs") return value;
throw new Error("agentrun native-development manager action must be start, stop, restart, status, or logs");
}
function isHelpArg(value: string): boolean {
return value === "--help" || value === "-h" || value === "help";
}
+1 -1
View File
@@ -76,7 +76,7 @@ export function rootHelp(): unknown {
{ command: "web-probe run|script|console-verify|observe|sentinel --node <node> --lane <lane>", description: "Run YAML-selected HWLAB Web probes, Cloud Console route matrices, long observe/analyze sessions, project-management MDTODO commands, and Web sentinel control through the single top-level web-probe entrypoint." },
{ command: "hwlab nodes control-plane|git-mirror|hwpod-preinstall|secret|test-accounts --node <node> --lane <lane>", description: "Manage HWLAB node/lane runtime prerequisites, including NC01 YAML-declared k3s infra/tools-image/Argo bootstrap, HWPOD preinstall configRefs, redacted test-account preparation, and NC01 v0.3 runtime lane, with the node identity passed as data." },
{ command: "hwlab nodes control-plane|git-mirror|hwpod-preinstall|secret|test-accounts --node NC01 --lane v03", description: "Operate the NC01 HWLAB v0.3 development and deployment lane from YAML source truth." },
{ command: "agentrun get|describe|events|logs|result|ack|cancel|dispatch|create|apply|send|control-plane|git-mirror", description: "Use AgentRun v0.1 resource primitives with low-noise human output by default; session follow-up uses send only and the server decides internal steer vs turn." },
{ command: "agentrun get|describe|events|logs|result|ack|cancel|dispatch|create|apply|send|native-development|control-plane|git-mirror", description: "Use AgentRun resource primitives and the YAML-first L1 manager lifecycle with low-noise human output by default; session follow-up uses send only and the server decides internal steer vs turn." },
{ command: "platform-infra sub2api|langbot|n8n|webterm|wechat-archive|hyperv-vm|k3s-cluster ...", description: "Deploy YAML-controlled platform services, Hyper-V Ubuntu nodes and multi-host k3s clusters without printing secrets." },
{ command: "pikaoa attachments-backup ...", description: "管理 PikaOA 生产附件离机备份控制面;开发与生产运行面由 PaC 自动交付。" },
{ command: "secrets plan|sync|status", description: "Plan, push and inspect YAML-declared local secret source keys to Kubernetes Secrets without printing or reverse-engineering values." },