diff --git a/.agents/skills/unidesk-agentrun/SKILL.md b/.agents/skills/unidesk-agentrun/SKILL.md index e1dbc3bf..8f569aa6 100644 --- a/.agents/skills/unidesk-agentrun/SKILL.md +++ b/.agents/skills/unidesk-agentrun/SKILL.md @@ -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 diff --git a/config/agentrun.yaml b/config/agentrun.yaml index a79f22a2..1c7ea73e 100644 --- a/config/agentrun.yaml +++ b/config/agentrun.yaml @@ -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 diff --git a/config/hwlab-node-lanes.yaml b/config/hwlab-node-lanes.yaml index a9f28eb9..70e0e48c 100644 --- a/config/hwlab-node-lanes.yaml +++ b/config/hwlab-node-lanes.yaml @@ -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: diff --git a/docs/reference/agentrun.md b/docs/reference/agentrun.md index 57e5283e..7c8c0dd8 100644 --- a/docs/reference/agentrun.md +++ b/docs/reference/agentrun.md @@ -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 声明。 diff --git a/project-management/PJ2026-01/specs/PJ2026-010201-agentrun-core.md b/project-management/PJ2026-01/specs/PJ2026-010201-agentrun-core.md index 4697644c..119b65ba 100644 --- a/project-management/PJ2026-01/specs/PJ2026-010201-agentrun-core.md +++ b/project-management/PJ2026-01/specs/PJ2026-010201-agentrun-core.md @@ -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 | 编号 | 短名 | 主责模块 | 关联模块 | diff --git a/scripts/src/agentrun/entry.ts b/scripts/src/agentrun/entry.ts index 6984ed05..3f95e479 100644 --- a/scripts/src/agentrun/entry.ts +++ b/scripts/src/agentrun/entry.ts @@ -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/ --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") { diff --git a/scripts/src/agentrun/native-development.ts b/scripts/src/agentrun/native-development.ts new file mode 100644 index 00000000..ed483692 --- /dev/null +++ b/scripts/src/agentrun/native-development.ts @@ -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 { + 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 { + 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): Record { + 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 + : 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(record: Record, fields: readonly T[], path: string): Record { + return Object.fromEntries(fields.map((field) => [field, positiveInteger(record, field, path)])) as Record; +} + +function positiveInteger(record: Record, 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 { + 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"; +} diff --git a/scripts/src/help.ts b/scripts/src/help.ts index d6a7a7fe..56e9eb6f 100644 --- a/scripts/src/help.ts +++ b/scripts/src/help.ts @@ -76,7 +76,7 @@ export function rootHelp(): unknown { { command: "web-probe run|script|console-verify|observe|sentinel --node --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 --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." },