feat: add AgentRun L1 manager lifecycle
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 manager;AgentRun 固定 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 L1;AgentRun 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
|
||||
|
||||
| 编号 | 短名 | 主责模块 | 关联模块 |
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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
@@ -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." },
|
||||
|
||||
Reference in New Issue
Block a user