+
+ Interview Edition · Java 17 · 2026-07-31
+ 从“能调用 SSH 的 AI”升级为可治理、可恢复的 Agent 任务系统
+ 项目真正有面试价值的部分,不是又封装了一次大模型 API,而是把不确定的模型决策放进确定性的工程边界:持久化状态机、严格幂等、人工审批、执行权 CAS、超时取消、安全策略、重启恢复、监控与评测。
+
+ Java 17Spring Boot 3.4.3
+ Spring AI 1.1.5MyBatis-Plus
+ MySQLJSch
+ SSECompletableFuture
+ MicrometerPrometheus
+
+
+
99自动化测试0 失败、0 错误;2 个真实计费模型测试按设计跳过。
+
30/30安全评测场景10 ALLOW、10 ASK、10 DENY,固定样例危险误放行 0。
+
3 层严格幂等HTTP 响应回放、业务唯一键、Step 执行权 CAS。
+
+ 30 秒面试开场:LowenSSH 是一个 Java 17 的 SSH 运维 Agent。模型通过 Function Calling 产生工具意图,但没有执行权。系统先把任务和 Step 持久化,再经过规则链;高风险动作通过 SSE 通知前端审批,独立 HTTP 幂等提交决定,工作线程用 CompletableFuture 等待。批准后还要用数据库 CAS 抢到一次性执行权,执行完成做只读验证。服务崩溃时,执行中的副作用不会自动重放,而是进入 NEEDS_REVIEW。
+
+
+
+ 01. 项目到底是 JDK 多少?
+ 结论:项目的编译目标是 Java 17。当前机器运行 Maven 时使用 JDK 21,但 Maven 按 pom.xml 的 <java.version>17</java.version> 编译,所以项目语言级别和发布字节码仍是 Java 17。
+
+
pom.xml · 7–24,38–97<parent>
+ <artifactId>spring-boot-starter-parent</artifactId>
+ <version>3.4.3</version>
+</parent>
+
+<properties>
+ <java.version>17</java.version>
+ <spring-ai.version>1.1.5</spring-ai.version>
+</properties>
+
+spring-boot-starter-web
+spring-boot-starter-actuator
+micrometer-registry-prometheus
+spring-ai-starter-model-openai
+jsch 0.2.21
+mybatis-plus 3.5.9
+mysql-connector-j
+
+
+ - 先看 parent:Spring Boot 3.4.3 统一依赖版本和插件默认值。
+ - 再看 properties:
java.version=17 是回答 JDK 版本的直接证据。
+ - 再看依赖:Web 提供 REST/SSE;Spring AI 负责模型和 Tool Schema;JSch 负责 SSH/SFTP;MyBatis-Plus/MySQL 负责持久化;Actuator/Micrometer 负责指标。
+ - 测试专用:H2 验证事务与唯一键,Apache MINA SSHD 搭真实协议的内嵌 SSH Server。
+
+
面试回答不要说“本机 java -version 是 21,所以项目是 JDK 21”。项目版本以构建文件的 release/source 目标为准。
+
+
+
+
+
+ 02. 升级前后发生了什么
+
+ | 维度 | 升级前 | 升级后 | 面试价值 |
+
+ | 执行流程 | 模型 → 工具 → 回灌 | Plan → Risk → Approve → Execute → Verify → Summary | 能讲状态机和异常路径 |
+ | ASK | Web 入口自动批准 | 持久化审批 + SSE 事件 + HTTP 决定 + Future 唤醒 | 能讲异步桥接与事务边界 |
+ | 旧接口 | run/stream 对 ASK 自动放行 | 标记弃用并失败关闭;ASK 统一迁移到任务审批 API | 兼容旧响应格式,但不保留安全旁路 |
+ | 幂等 | 没有完整任务级约束 | 请求哈希、响应回放、唯一键、乐观锁、执行权 CAS | 能回答重复点击与网络重试 |
+ | SSH 阻塞 | 等 Channel 自然关闭 | 30 秒命令超时、1MB 输出上限、主动关闭 Channel | 能处理 tail -f / ping / top |
+ | 取消 | 断开流近似结束 | 独立取消 API,信号传播到工作线程、模型 Future、SSH Channel | 区分 UI 断开和后台停止 |
+ | 安全 | 简单正则门禁、Host Key 不校验 | 规则链、known_hosts、私钥认证、SFTP、防默认密钥 | 纵深防御,不夸大正则能力 |
+ | 恢复 | 进程结束后调用栈丢失 | 数据库恢复;EXECUTING 进入 NEEDS_REVIEW | 承认分布式副作用不确定窗口 |
+ | 评估 | 看最终回答 | Token、耗时、工具、安全决定、成功率、固定评测集 | 从“感觉有效”走向可测量 |
+
+
+
+ PLAN→RISK CHECK→APPROVE→
+ EXECUTE→VERIFY→SUMMARY
+
+
+
+
+ 03. 像人类调试一样阅读项目
+ 不要按包名从 A 到 Z 看,也不要逐行翻译。正确方法是选一个真实场景,把断点放在调用链上,跟着数据走。推荐场景:“重启 nginx”会命中 ASK,用户批准后执行并验证。
+
+
1HTTP 入口
TaskController.create:观察 Idempotency-Key 和请求体。
+
2事务创建
TaskCommandService.create:跳入请求哈希、幂等记录、任务、task_created 事件。
+
3异步线程
TaskWorkflowOrchestrator.start → schedule → run:HTTP 已返回 202,Loop 在专用线程继续。
+
4模型决策
AgentService.runInternal:看 Prompt、Function Schema、ChatResponse.toolCalls。
+
5风险检查
screen → CommandGuard → CommandPolicyEngine:返回 ASK、风险等级、规则列表。
+
6审批暂停
PersistentConfirmationHandler → ApprovalCoordinator:写库后进入 future.get。
+
7另一条 HTTP 请求
ApprovalController → ApprovalDecisionService:CAS 决定,事务提交后 complete Future。
+
8原线程恢复
future.get 返回 APPROVED,原调用栈继续,绝不是“重新启动一个 Agent”。
+
9一次性执行
WorkflowPersistenceService.beginExecution 先抢执行权,再进入 JSch Channel。
+
10验证和总结
保存执行结果、只读验证,工具结果回灌模型,最后写 SUCCEEDED 和 summary。
+
+ 调试技巧:每个断点只回答四件事:谁调用我?传入什么?我修改了什么状态?返回后调用方拿到什么?遇到异步时再加两问:当前是哪条线程?恢复靠内存信号还是数据库状态?
+
+
+
+ 04. 项目如何启动和搭建
+ Spring Boot 启动后扫描 Controller、Service、Mapper 和定时任务;SchemaInitializer 建表/迁移;配置从 application.yml 与环境变量合并;SSH Session 只在用户连接主机后创建。
+
+ | 启动步骤 | 发生的事 | 配置/文件 |
+ | 1. Maven 构建 | 按 Java 17 编译,装配 Spring Boot Jar | pom.xml |
+ | 2. Spring Context | 依赖注入 Controller/Service/Mapper/Scheduler | LowenSshApplication.java |
+ | 3. 数据源 | 连接 MySQL lowenssh | spring.datasource.* |
+ | 4. Schema | 建立并迁移任务、Step、审批、事件、幂等表 | schema.sql、SchemaInitializer.java |
+ | 5. 模型 | OpenAI 兼容协议连接 GLM,可换 DeepSeek/Qwen | spring.ai.openai.* |
+ | 6. 安全启动 | 未提供加密 Key 时生产启动失败 | XWSSH_CRYPTO_KEY(S) |
+ | 7. 定时器 | 扫描审批过期、任务超时、重启恢复 | xwssh.agent.*-interval |
+
+
+
+
+ 05. 第一个断点:创建任务与严格幂等
+
+
TaskController.java · 40–50 → TaskCommandService.java · 66–118@PostMapping
+public ResponseEntity<CreateTaskResponse> create(
+ @RequestHeader("Idempotency-Key") String key,
+ @RequestBody CreateTaskRequest request) {
+ CreateResult result = commandService.create(key, request);
+ if (!result.replayed()) {
+ orchestrator.start(result.response().taskId());
+ }
+ return ResponseEntity.accepted()
+ .header("Idempotency-Replayed",
+ Boolean.toString(result.replayed()))
+ .body(result.response());
+}
+
+@Transactional
+public CreateResult create(String key, CreateTaskRequest request) {
+ String requestHash = RequestFingerprint.sha256(...);
+ idempotencyMapper.insertPlaceholder(scope, key, requestHash, expiresAt);
+ IdempotencyRecordEntity record =
+ idempotencyMapper.selectForUpdate(scope, key);
+ if (!requestHash.equals(record.getRequestHash())) throw conflict;
+ if (record.getResponseJson() != null) return replay(record);
+ taskMapper.insert(task);
+ eventService.append(taskId, "task_created", ...);
+ idempotencyMapper.saveResponse(...);
+ return new CreateResult(response, false);
+}
+
+
+ - 进入 Controller:要求客户端提供 Idempotency-Key。网络超时后客户端可以安全重试。
+ - 跳入 Service:把 sessionId、hostId、任务文本规范化后做 SHA-256。Key 一样而请求不同,会返回冲突。
+ - 抢数据库唯一键:并发请求只有一个 Key 记录;随后
SELECT FOR UPDATE 串行处理。
+ - 同一事务:任务、task_created 事件、首次 HTTP 响应一起提交,不会出现“Key 占了但任务没建”的半成品。
+ - 跳回 Controller:首次请求才启动编排器;重放请求只返回之前保存的相同 body,不会启动第二份 Agent Loop。
+
+
返回:202 + taskId/status/phase。是否重放只放在响应头,首次和重放的响应体完全相同。
+
+
+ 为什么只用 UUID 不算严格幂等?
+ UUID 只能降低 ID 冲突概率,不能识别“同一个业务请求重试”。严格幂等必须保存 scope + idempotencyKey + requestHash + response,并利用唯一键和事务处理并发。
+
+
+
+ 06. HTTP 返回后,Agent 在哪里运行?
+
+
TaskWorkflowOrchestrator.java · 107–183public boolean start(String taskId) {
+ return schedule(taskId, RunMode.NORMAL);
+}
+
+private boolean schedule(String taskId, RunMode mode) {
+ if (!scheduled.add(taskId) || runtimeRegistry.isRunning(taskId)) {
+ return false;
+ }
+ workers.execute(() -> run(taskId, mode));
+ return true;
+}
+
+private void run(String taskId, RunMode mode) {
+ try (Registration ignored = runtimeRegistry.register(taskId)) {
+ AgentTaskEntity task = taskMapper.selectById(taskId);
+ LiveSession live = sessionManager.get(task.getSessionId());
+ runtimeRegistry.bindSsh(taskId, live.ssh());
+ transitionService.transition(taskId, PLANNING, PLAN, ...);
+ agentService.run(..., confirmationFactory.create(taskId), observer);
+ } catch (TaskCancelledException e) {
+ cancellationFinalizer.finalizeIfCancelling(taskId);
+ } catch (DuplicateToolExecutionException e) {
+ persistence.needsReview(taskId, e.getMessage());
+ } finally {
+ scheduled.remove(taskId);
+ }
+}
+
+
+ - Controller 不跑 Agent:
workers.execute 把工作交给固定线程池,HTTP 可以立即返回。
+ - 双重去重:
scheduled 防止排队重复,runtimeRegistry 防止运行重复。
+ - 登记运行句柄:记录工作线程和 SSH Client,取消 API 才知道该中断谁。
+ - 按 taskId 创建 confirmer/observer:审批上下文和持久化观察者不会跨任务共享。
+ - 异常分流:取消、重复执行、限额、普通异常分别收敛到不同持久化状态。
+
+
线程关系:HTTP 线程只建任务;agent-task-N 工作线程执行 Loop;审批 HTTP 使用另一条容器线程;SSE 又是独立订阅。
+
+
+
+
+
+ 07. 什么是 Function Calling?项目已经用了
+ Function Calling 不是模型直接执行 Java 方法。Java 把工具名称、参数 Schema 和描述发给模型;模型返回结构化 tool_call 意图;应用校验后才决定是否执行对应方法,并把结果作为 tool_result 回灌。
+
+
AgentService.java · 131–200ToolCallback[] callbacks = ToolCallbacks.from(tools);
+OpenAiChatOptions options = OpenAiChatOptions.builder()
+ .toolCallbacks(callbacks)
+ .internalToolExecutionEnabled(false)
+ .build();
+
+for (int round = 1; round <= maxRounds; round++) {
+ messages = contextManager.truncateToolResponses(messages);
+ messages = contextManager.compressIfNeeded(messages);
+ ChatResponse response = chatModel.call(new Prompt(messages, options));
+
+ if (!response.hasToolCalls()) return finalText;
+
+ AssistantMessage assistant = response.getResult().getOutput();
+ List<ToolResponse> rejected = screen(...);
+ if (!rejected.isEmpty()) {
+ messages.add(assistant);
+ messages.add(rejectedToolMessage);
+ continue;
+ }
+ observer.beforeToolExecution(assistant.getToolCalls());
+ ToolExecutionResult result =
+ toolCallingManager.executeToolCalls(prompt, response);
+ observer.afterToolExecution(lastToolResponses(result));
+ messages = new ArrayList<>(result.conversationHistory());
+}
+
+
+ - ToolCallbacks.from:扫描
SshTools 的 @Tool,生成模型能理解的函数描述。
+ - 关闭内部自动执行:这是最重要的开关。否则 Spring AI 收到 tool_call 就执行,安全门禁和审批插不进去。
+ - 模型返回两类数据:没有 tool_call 就是最终回答;有 tool_call 就是结构化工具意图。
+ - 先 screen:DENY/ASK 在 Java 确定性代码里执行,不信任 Prompt 里的“模型自觉”。
+ - 结果回灌:模型读取 stdout/exitCode 后决定下一步,直到没有工具调用或达到最大轮数。
+
+
一句话:模型拥有建议权,Java 状态机拥有执行权。
+
+
+
+
+
+ 08. Risk Check:规则链如何决定 ALLOW / ASK / DENY
+
+
CommandPolicyEngine.java · 23–45List<PolicyMatch> matches = policies.stream()
+ .map(policy -> policy.evaluate(context))
+ .flatMap(Optional::stream)
+ .toList();
+
+PolicyMatch winner = matches.stream()
+ .min(comparing(decision.ordinal())
+ .thenComparing(match -> -match.riskLevel().ordinal()))
+ .orElse(new PolicyMatch(
+ ASK, MEDIUM,
+ "命令不在只读白名单,需要人工确认",
+ "ask.unknown_command"));
+
+return new PolicyResult(
+ winner.decision(), winner.riskLevel(),
+ winner.reason(), ruleIds, policyVersion);
+
+
+ - 每条
CommandPolicy 独立检查一种风险,便于测试和扩展。
+ - 合并时“最严格决定获胜”:DENY 优先 ASK,ASK 优先 ALLOW;同决定选更高风险。
+ - 未知命令默认 ASK,而不是默认 ALLOW。
+ - 返回的不只是决定,还包含风险、原因、命中规则和策略版本;这些字段会写进 Step/Approval。
+
+
+
+
+ | 规则类型 | 例子 | 结果 |
+ | 绝对破坏 | rm -rf、mkfs、dd、重启机器 | DENY |
+ | 间接执行/混淆 | bash -c、Python/Node 执行、base64 | shell、变量替换 | DENY |
+ | 提权和写操作 | sudo、重启服务、修改权限、重定向写文件 | ASK |
+ | 明确只读 | df、ps、ss、systemctl status | ALLOW |
+ | 未知命令 | 未命中白名单也未命中禁止规则 | ASK |
+
+
+
+
+ 09. ASK 的完整断点跟踪:如何暂停,又如何继续
+ 先给结论:Agent 没有被销毁,也没有轮询前端。它的专用工作线程停在 CompletableFuture.get(timeout);数据库保存审批真相;另一条审批 HTTP 线程更新数据库并在事务提交后调用 future.complete,原线程从原调用栈下一行继续。
+
+
PersistentConfirmationHandler.java · 48–78 → ApprovalCoordinator.java · 29–64public boolean confirm(ConfirmationRequest request) {
+ AgentStepEntity step = stepService.createOrGet(...);
+ ApprovalDecision decision = coordinator.requestAndAwait(
+ new ApprovalRequest(taskId, stepId, ..., timeout));
+ if (decision == APPROVED) {
+ transition(taskId, RISK_CHECKING, RISK_CHECK, ...);
+ }
+ return decision == APPROVED;
+}
+
+public ApprovalDecision requestAndAwait(ApprovalRequest request) {
+ ApprovalView approval = approvalService.request(request);
+ CompletableFuture<ApprovalDecision> future =
+ waitRegistry.register(approval.approvalId());
+ try {
+ ApprovalView latest = approvalService.get(approvalId);
+ if (latest.isTerminal()) return from(latest.status());
+ return future.get(remainingMillis, MILLISECONDS);
+ } catch (TimeoutException e) {
+ return approvalService.expire(approvalId);
+ } finally {
+ waitRegistry.remove(approvalId, future);
+ }
+}
+
+
+ - 断点 A:
confirm 收到完整 toolCallId/toolName/arguments/风险信息。
+ - 断点 B:
createOrGet 用 actionDigest 取得稳定 Step;同一动作不会创建多个逻辑 Step。
+ - 断点 C:
approvalService.request 先完成数据库事务,状态变成 WAITING_APPROVAL。
+ - 断点 D:按 approvalId 注册 Future。注册后立即再读数据库,封住“用户审批比 Future 注册更快”的竞态。
+ - 断点 E:
future.get 阻塞的是 agent-task-N 工作线程,不是审批 HTTP 线程,也不是 SSE 线程。
+ - 断点 F:Future 返回 APPROVED 后跳回
confirm,再返回 true 给 screen,原 Loop 继续。
+
+
重要:“继续”不是重新创建任务;正常情况下是原线程原调用栈恢复。只有服务重启导致 JVM 调用栈丢失时,才由 RecoveryScheduler 按数据库恢复。
+
+
+ 审批请求的持久化事务
+
+
ApprovalService.java · 62–120@Transactional
+public ApprovalView request(ApprovalRequest request) {
+ AgentTaskEntity task = taskMapper.selectForUpdate(taskId);
+ AgentStepEntity step = stepMapper.selectForUpdate(stepId);
+ AgentApprovalEntity existing =
+ approvalMapper.selectByActionForUpdate(
+ taskId, toolCallId, actionDigest);
+ if (existing != null) return toView(existing);
+
+ approval.setApprovalId(UUID.randomUUID().toString());
+ approval.setStatus("PENDING");
+ approvalMapper.insertOrKeepExisting(approval);
+ stepMapper.markApprovalState(stepId, "WAITING_APPROVAL", ...);
+ transition(taskId, WAITING_APPROVAL, APPROVE, ...);
+ eventService.append(taskId, "approval_required", view);
+ return view;
+}
+
+
+ - 先锁 Task,再锁 Step/Approval,所有审批写路径保持相同锁顺序,降低死锁风险。
+ - 唯一业务键是
(taskId, toolCallId, actionDigest)。首次生成 UUID,以后复用,因此 approvalId 稳定。
+ - Approval=PENDING、Step=WAITING_APPROVAL、Task=WAITING_APPROVAL、approval_required 事件同事务提交。
+ - 事务失败时全部回滚;客户端不会看到数据库不存在的“幽灵审批”。
+
+
+
+ 审批通过时的另一条线程
+
+
ApprovalDecisionService.java · 75–158;ApprovalService.java · 202–213@Transactional
+public DecisionResult decide(...) {
+ // 幂等 Key + requestHash + 响应回放
+ AgentApprovalEntity approval =
+ approvalMapper.selectForUpdate(approvalId);
+ int updated = approvalMapper.decidePending(
+ approvalId, target, now, approval.getVersion());
+ if (updated != 1) return existingTerminal(...);
+
+ stepMapper.markApprovalState(
+ stepId, "READY_TO_EXECUTE", ...);
+ eventService.append(taskId, "approval_decided", view);
+ approvalService.completeAfterCommit(approvalId, decision);
+ return persist(idempotency, 200, body, approvalId);
+}
+
+void completeAfterCommit(String id, ApprovalDecision decision) {
+ registerSynchronization(new TransactionSynchronization() {
+ public void afterCommit() {
+ waitRegistry.complete(id, decision);
+ }
+ });
+}
+
+
+ - 审批接口本身也严格幂等:相同 Key/相同 body 回放;相同 Key/不同 body 返回 409。
+ decidePending 带 status/version 条件,只有一个并发请求能把 PENDING 改成终态。
+ - 批准后 Step 只到 READY_TO_EXECUTE,还没有执行。执行还要再过一次 CAS。
+ completeAfterCommit 非常关键:先提交数据库,再唤醒线程。若先唤醒,工作线程可能读取到旧状态甚至执行后审批事务又回滚。
+
+
+
+ 审批超时和重复审批
+
+ | 场景 | 处理 |
+ | 等待达到 expiresAt | future.get 抛 TimeoutException,调用 expire 做 PENDING→EXPIRED CAS。 |
+ | 定时扫描先到 | ExpiryScheduler 同样做 CAS,并 complete Future;两条路径只有一个成功。 |
+ | 50 个并发“批准” | 只有一个 CAS 改状态;其余读取同一终态,不重复推进。 |
+ | 先批准后注册 Future | 注册后再次读库,发现终态立即返回,不会永远等待。 |
+ | 已批准又拒绝 | 409 APPROVAL_ALREADY_DECIDED。 |
+ | 进程重启 | Future 消失,但 Approval/Task/Step 仍在数据库;RecoveryScheduler 重建流程。 |
+
+
+
+
+ 10. 为什么 SSE 只能推送,审批必须另一个 HTTP
+
+
SSE 的职责一条普通 HTTP 长连接,服务器持续向客户端发送事件。浏览器端不能沿同一 SSE 通道反向发送审批命令。适合 task_created、approval_required、执行结果和总结。
+
审批 HTTP 的职责客户端向服务器发送有副作用的决定。独立 POST 便于事务、Idempotency-Key、请求体哈希、409 冲突、审计、超时和重试。
+
+
+
TaskEventService.java · 42–87,129–139;TaskController.java · 69–80@Transactional
+public TaskEventView append(...) {
+ AgentTaskEntity task = taskMapper.selectForUpdate(taskId);
+ long sequence = task.getNextEventSequence();
+ taskMapper.advanceEventSequence(...);
+ eventMapper.insert(event);
+ publishAfterCommit(view);
+ return view;
+}
+
+public Flux<TaskEventView> stream(String taskId, long afterId) {
+ Flux<TaskEventView> history = Flux.fromIterable(replay(taskId, afterId));
+ return Flux.concat(history, publisher.live(taskId))
+ .filter(event -> advance(lastSeen, event.id()));
+}
+
+GET /api/agent/tasks/{taskId}/events
+Last-Event-ID: 17
+
+
+ - 事件先落库,再在事务提交后发布到实时 Sink。
+ - 每个任务的 sequence 单调递增,SSE id 可用于断线续传。
+ - 重连带 Last-Event-ID,服务先查询历史遗漏事件,再衔接实时流。
+ - AtomicLong 去掉“历史查询”和“实时缓冲”交界处的重复事件。
+ - 用户关掉 SSE 只代表“不再看进度”,不会取消后台任务。
+ - 应用关闭事件会主动 complete 全部实时 Sink,让无限 SSE 在 Web Server 停止前结束。
+
+
+
+ 旧接口边界:/api/agent/run 和 /api/agent/stream 没有审批回传通道,现已标记弃用并使用 RejectingConfirmationHandler:ALLOW 仍可执行,ASK 一律失败关闭。需要人工审批的调用必须迁移到 /api/agent/tasks、任务事件 SSE 和独立审批 POST。
+ 为什么不用 WebSocket?WebSocket 可以双向,但本机单用户第一版不值得增加连接状态、重连、消息确认和协议复杂度。SSE + 幂等 POST 的职责更清晰。
+
+
+
+ 11. 批准不等于执行:Step 如何保证只执行一次
+
+
WorkflowPersistenceService.java · 115–152@Transactional
+public void beginExecution(String taskId,
+ List<ExecutionClaim> claims) {
+ AgentTaskEntity task = requireActiveTask(taskId);
+ if (task.toolCalls + claims.size() > maxToolCalls) throw limit;
+
+ for (ExecutionClaim claim : orderedClaims) {
+ AgentStepEntity step = stepMapper.selectForUpdate(claim.stepId());
+ if (!RISK_CHECKED.equals(step.status)
+ && !READY_TO_EXECUTE.equals(step.status)) {
+ throw new DuplicateToolExecutionException(...);
+ }
+ }
+ taskMapper.addToolCalls(taskId, claims.size(), task.version);
+ for (ExecutionClaim claim : orderedClaims) {
+ stepMapper.claimExecution(
+ stepId, preSnapshot, step.version);
+ }
+ transition(taskId, EXECUTING, EXECUTE, "task_executing");
+}
+
+
+ - 先检查整批:模型一次可能返回多个工具调用。任一 Step 不合法,整个事务回滚,不产生半批执行。
+ - 先扣预算:最大工具次数 30,防模型绕路或死循环。
+ - 执行权 CAS:Step 只有从 RISK_CHECKED/READY_TO_EXECUTE 才能变 EXECUTING。
+ - 事务提交后才调用 SSH:数据库已经明确记录“谁取得执行权”。重复请求无法再次取得。
+
+
严格表述:项目保证应用层不重复领取同一 Step;无法宣称跨本地数据库与远端 Linux 的数学意义 exactly-once,因为两边没有分布式事务。
+
+
+
+
+
+ 12. SSH 命令为什么不会再无限等待
+
+
SshClient.java · 171–252ChannelExec channel = (ChannelExec) session.openChannel("exec");
+OutputBudget budget = new OutputBudget(maxOutputBytes);
+ActiveCommand execution = new ActiveCommand(channel);
+activeCommand.compareAndSet(null, execution);
+
+channel.connect(connectTimeout);
+long deadline = now + commandTimeout;
+while (true) {
+ while (stdout.available() > 0) drainIntoBoundedBuffer();
+ if (execution.cancelRequested.get()) { cancelled = true; break; }
+ if (channel.isClosed()) { exitCode = channel.getExitStatus(); break; }
+ if (now >= deadline) { timedOut = true; break; }
+ Thread.sleep(50);
+}
+channel.disconnect();
+if (timedOut || cancelled) verifySessionAfterForcedChannelClose();
+return new ExecResult(stdout, stderr, exitCode,
+ timedOut, cancelled, budget.truncated());
+
+
+ - 每条命令记录 deadline,默认 30 秒。
tail -f、ping、top 到点退出。
+ - stdout 和 stderr 共用 1MB 预算,避免两边各占 1MB。
+ - 超出预算后仍继续排空远端输出,只丢弃额外字节。若不排空,远端可能因管道缓冲写满而卡死。
+ - 取消/超时只关闭当前 exec Channel;随后 keepalive 检查 Session,健康则继续复用,不健康才关闭。
+ - 同一 SshClient 禁止并发命令,上层 LiveSession.lock 串行化。
+
+
+
+
+
+
+ 13. 用户取消:为什么取消 Flux 不等于取消任务
+ RUNNINGPOST cancelCANCELLING资源退出CANCELLED
+
+
TaskCancellationService.java · 59–117 → TaskRuntimeRegistry.java · 48–61@Transactional
+public CancelResult cancel(String taskId, String key) {
+ // 幂等记录 + requestHash + response replay
+ taskMapper.requestCancellation(
+ taskId, "CANCELLING", task.version);
+ eventService.append(taskId, "task_cancelling", ...);
+ idempotencyMapper.saveResponse(...);
+ afterCommit(() -> runtimeRegistry.signalCancellation(taskId));
+}
+
+Future<ChatResponse> future =
+ modelCalls.submit(() -> chatModel.call(prompt));
+observer.onModelCallStarted(future);
+
+public CancellationSignal signalCancellation(String taskId) {
+ handle.cancelRequested.set(true);
+ boolean model = handle.modelCall != null
+ && handle.modelCall.cancel(true);
+ boolean ssh = handle.sshClient != null
+ && handle.sshClient.cancelActiveCommand();
+ handle.worker.interrupt();
+ return new CancellationSignal(true, model, ssh);
+}
+
+
+ - 先持久化“应当取消”,再提交事务,保证重启后也看得到取消意图。
+ - 主决策和历史摘要两类同步模型调用都经过同一个可取消入口;Observer 把真实 Future 绑定到当前 taskId。
+ - 提交后向三处发信号:模型 Future、SSH Channel、工作线程 interrupt。取消先到而 Future 后绑定时,绑定动作会立即补发取消。
+ Future.cancel(true) 只是尽力中断,第三方 HTTP 客户端不一定立即停,所以不虚报“模型已终止”。
+ - 后台资源真正退出后,Finalizer 才把 CANCELLING 收敛到 CANCELLED。
+ - SSE Subscription 从未登记到 RuntimeRegistry,因此关闭浏览器不会误取消任务。
+
+
+
+
+
+
+ 14. 执行前快照、执行后验证与回滚边界
+ 第一版不自动回滚。支持明确验证器的动作先拍状态快照,执行后做只读检查;无法通用验证时明确写 UNSUPPORTED;失败只给回滚建议。用户真要回滚时,新建 Step,重新经过 Risk Check 和 Approve。
+
+ | 阶段 | 保存内容 | 原则 |
+ | 执行前 | 例如 systemctl 服务 active/inactive 状态 | 只读快照,不伪造完整系统快照 |
+ | 执行后 | exitCode、timeout、cancelled、truncated、摘要 | 结果先持久化 |
+ | Verify | 只读命令验证服务/进程/端口等 | 验证本身仍经过安全约束 |
+ | 失败 | 原因、验证结果、回滚建议文本 | 不自动执行高风险回滚 |
+
+
+
+
+ 15. 服务重启后怎么恢复
+
+
TaskRecoveryScheduler.java · 72–126switch (status) {
+ case CREATED, PLANNING -> orchestrator.start(taskId);
+ case VERIFYING, SUMMARIZING ->
+ orchestrator.continueAfterRestart(taskId);
+ case RISK_CHECKING -> recoverRiskChecking(task);
+ case WAITING_APPROVAL -> recoverApproval(task);
+ case EXECUTING -> persistence.needsReview(
+ taskId,
+ "无法证明远端动作是否已经发生,禁止自动重放");
+ case CANCELLING -> finalizer.finalizeIfCancelling(taskId);
+}
+
+switch (approval.status) {
+ case PENDING -> { /* 保持等待 */ }
+ case APPROVED -> orchestrator.resumeApprovedStep(taskId);
+ case REJECTED -> continueWithRejectedToolResult();
+ case EXPIRED -> failTask();
+}
+
+
+ - 定时扫描非终态任务,跳过本 JVM 已经运行的 taskId。
+ - WAITING + PENDING 不需要内存 Future 才能保存状态,数据库继续等待 HTTP 决定或过期扫描。
+ - WAITING + APPROVED 按数据库里的精确 Step 参数恢复,仍要执行权 CAS,不重新让模型猜一个相似命令。
+ - EXECUTING 是最危险状态:可能远端已成功、也可能未执行。自动重试可能造成第二次副作用,所以进入 NEEDS_REVIEW。
+
+
+
+ 面试高分点:承认本地事务与远端 SSH 之间存在不可原子提交的窗口。成熟设计不是声称“绝对 exactly once”,而是识别不确定状态并停止自动化。
+
+
+
+ 16. 每一步如何持久化
+
+ | 表 | 保存什么 | 关键约束 | 恢复作用 |
+
+ t_agent_task | 任务文本、status/phase、deadline、取消标志、计数器、summary/error、version | task_id 主键,乐观锁 version | 知道整项任务停在哪 |
+ t_agent_step | Plan、Tool、风险、参数、actionDigest、快照、执行和验证结果 | (taskId, toolCallId, actionDigest) 唯一 | 精确恢复动作并防重 |
+ t_agent_approval | approvalId、PENDING/APPROVED 等、风险、理由、到期时间 | (taskId, toolCallId, actionDigest) 唯一 | Future 丢失后仍有真相 |
+ t_agent_event | 任务内单调 sequence、事件类型、JSON payload | (taskId, sequenceNo) 唯一 | SSE Last-Event-ID 回放 |
+ t_idempotency_record | scope、Key、requestHash、resourceId、HTTP status/body、过期时间 | (scope, key) 唯一 | 网络重试原样回放 |
+ t_message | 用户、assistant、tool call/result 历史 | 按 session 顺序 | 重启后继续模型上下文 |
+
+
+ 三层幂等必须同时存在
+
+ - API 层:Idempotency-Key + requestHash + 原响应回放,处理用户重复点击、代理重试和网络超时。
+ - 业务层:Step/Approval 唯一业务键,处理同一 Tool Call 重入。
+ - 执行层:Step status/version CAS,处理并发线程和恢复扫描器抢同一执行权。
+
+
+
+
+ 17. SSH 生产安全
+
+
Host Key生产默认 StrictHostKeyChecking=yes。首次通过管理 API 预览 SHA256 指纹,用户核对后信任;Key 变化拒绝覆盖。
+
认证支持密码、私钥路径、私钥口令。SSH Agent 因当前 JSch 缺少可靠连接器明确延期,不伪装完成。
+
文件读取readRemoteFile/tailLog 改用 SFTP,恶意路径不进入 Shell 语法;tail 使用固定环形缓冲。
+
SFTP 写审批deleteFile/makeDir/moveFile 先由 ToolRiskCommand 转成等价命令,与 execCommand 共用 DENY/ASK/ALLOW、持久化审批和重启恢复。
+
凭据加密AES-GCM,随机 12 字节 IV,自带完整性校验;生产无 Key 拒绝启动。
+
密钥轮换密文带 v1:/v2: 前缀,Key Ring 同时保留新旧密钥,活动版本加密、对应版本解密。
+
纵深防御代码门禁不是唯一边界。生产还需要最小权限 Linux 用户、sudo 白名单、容器/主机隔离和日志脱敏。
+
+
+
SshClient.java · 94–148;KnownHostsService.java · 40–75if (strictHostKeyChecking) {
+ prepareKnownHosts();
+ config.put("StrictHostKeyChecking", "yes");
+}
+
+public synchronized KnownHostPreview trust(
+ String hostToken, String line, String expectedFingerprint) {
+ String actual = fingerprint(parsed.keyBytes());
+ if (!MessageDigest.isEqual(actual, expected)) throw mismatch;
+ for (String existing : knownHosts) {
+ if (sameHost && sameLine) return alreadyTrusted;
+ if (sameHost) throw new KnownHostConflictException(hostToken);
+ }
+ append(line);
+}
+
+
+ - 连接时要求 known_hosts 中已有匹配 Key,防止中间人攻击。
+ - 首次信任不是程序替用户信任网络,用户必须通过可信渠道核对指纹。
+ - 同一 Key 重复导入幂等;同一主机出现不同 Key 直接冲突,不能静默覆盖。
+
+
+
+
+
+
+ 18. 可观测性与评测
+
+ | 指标 | 名称/标签 | 回答的问题 |
+ | 模型调用 | lowenssh.agent.model.calls/duration | 模型调用几次、P95 多慢 |
+ | Token | tokens.input/output/cached | 成本多少、上下文缓存是否命中 |
+ | 策略 | decision、risk 标签 | ALLOW/ASK/DENY 分布 |
+ | 工具 | success、timed_out、cancelled | Agent 是否绕路、失败在哪里 |
+ | SSH | outcome + duration | 慢命令、超时、失败率 |
+ | 任务 | status + duration | 成功率和用户总等待时间 |
+ | 上下文 | context.compressions | 压缩触发频率 |
+
+ 指标只使用低基数状态标签,不把命令正文、密码、私钥或 API Key 放进指标。Actuator 暴露 /actuator/metrics 和 /actuator/prometheus。
+ 固定评测结果
+
+ | 类别 | 场景数 | 期望 | 实测 |
+ | 正常只读 | 10 | ALLOW | 10/10 |
+ | 高风险操作 | 10 | ASK | 10/10 |
+ | 对抗/绕过 | 10 | DENY | 10/10 |
+ | 危险误放行 | 0 | 0 |
+ | 安全误拦截 | 0 | 0 |
+
+ 不要夸大:100% 只代表这 30 个固定离线样例,不代表任意 Shell 输入都安全,也不代表模型端到端任务成功率 100%。
+
+
+
+ 19. RAG 和 MCP:能用,但本期为什么没硬接
+ RAG
+ RAG 是“先从知识库检索相关片段,再把片段放进模型上下文”。适合公司 SOP、部署文档、故障案例、服务拓扑。当前没有已确认的真实私有知识源,硬接向量库只会增加切分、Embedding、召回评测和提示注入面,不会提升 SSH 执行可靠性。
+ 将来接入原则:RAG 只能增强 Plan,不能赋予执行权限;知识片段里的命令仍要经过 Risk Check/Approve/Execute;回答必须显示来源,并建立离线召回评测。
+ MCP
+ MCP 是统一连接外部工具/数据源的协议。Prometheus、Grafana、GitHub、CMDB 很适合做 MCP 工具;核心 SSH 远程执行本期保留本地 ToolCallback,因为策略、审批、超时、取消和审计都在同一进程边界内,更容易证明。
+
+ | 能力 | 当前选择 | 原因 |
+ | SSH 命令 | 本地 ToolCallback | 安全控制强,避免远程 MCP 成为第二条执行通道 |
+ | Prometheus/GitHub/CMDB | 后续可接 MCP | 外围或只读能力,统一 Schema/鉴权有价值 |
+ | RAG | 当前不实现 | 无真实知识源,不做演示性堆栈 |
+
+
+
+
+ 20. 配置文件和内容速查
+
+ | 配置 | 默认值 | 作用 |
+ xwssh.agent.max-rounds | 25 | Agent Loop 最大轮数 |
+ idempotency-retention | 24h | 幂等响应保存时间 |
+ approval-timeout | 2m | ASK 最长等待 |
+ task-timeout | 10m | 整体任务截止时间 |
+ max-tool-calls | 30 | 工具调用预算 |
+ max-consecutive-failures | 3 | 连续失败熔断 |
+ xwssh.ssh.connect-timeout | 10s | Session/Channel 建连超时 |
+ command-timeout | 30s | 单命令超时 |
+ max-output-bytes | 1,048,576 | stdout/stderr 共享上限 |
+ strict-host-key-checking | true | 生产严格校验 Host Key |
+ known-hosts-path | ~/.lowenssh/known_hosts | 可信 Host Key 文件 |
+ policy-version | v1 | 审批/actionDigest 绑定策略语义 |
+ max-command-length | 4096 | 拒绝过长混淆命令 |
+ context.max-context-tokens | 12000 | 触发历史摘要压缩 |
+
+ 必须通过环境变量提供的秘密
+ MYSQL_PASSWORD=...
+GLM_API_KEY=...
+XWSSH_CRYPTO_KEY=...
+# 轮换时:
+XWSSH_CRYPTO_KEYS=v2=new-secret,v1=old-secret
+XWSSH_ACTIVE_CRYPTO_KEY_VERSION=v2
+XWSSH_KNOWN_HOSTS=/secure/path/known_hosts
+ docker-compose.yml 已透传上述加密配置,并把容器内 /app/security/known_hosts 挂载到 ssh-security 命名卷。复制 .env.example 为本地 .env 后填写真实值;.env 禁止提交。
+
+
+
+ 21. 验证证据
+
+
99Tests run0 failures,0 errors,2 skipped。
+
50并发审批/取消验证只有一个请求推进状态。
+
100并发创建相同 Idempotency-Key 只生成一个任务。
+
+
+ | 测试 | 覆盖 |
+ TaskPersistenceIntegrationTest | 创建幂等、冲突 Key、事件回放、状态与预算 |
+ ApprovalIntegrationTest | 稳定 approvalId、等待唤醒、竞态、超时、并发决定 |
+ TaskWorkflowOrchestratorIntegrationTest | 完整状态流、ASK、审批后执行、重启恢复、NEEDS_REVIEW |
+ SshClientIntegrationTest | 真实 SSH 协议的超时、取消、输出截断、Session 复用 |
+ SshToolsSftpSafetyTest | 恶意路径不进入 Shell exec |
+ KnownHostsServiceTest | 指纹、重复导入、Host Key 冲突 |
+ CryptoUtilTest | AES-GCM、密钥轮换、旧密文兼容 |
+ AgentSecurityEvaluationTest | 30 个正常/高风险/对抗场景并输出 JSON |
+ RejectingConfirmationHandlerTest | 旧 REST/SSE 的两种 ASK 入口都失败关闭 |
+ TaskRuntimeRegistryTest | 模型 Future、SSH Channel、工作线程取消以及先取消后绑定竞态 |
+ TaskEventPublisherTest | 应用关闭时完成无限 SSE,关闭后不再创建实时流 |
+ ContextManagerTest | 历史摘要同样使用可取消模型入口,取消信号不会被熔断逻辑吞掉 |
+
+ ./mvnw -q test
+# Tests run: 99, Failures: 0, Errors: 0, Skipped: 2
+
+./mvnw -q -DskipTests package
+git diff --check
+# 均通过
+
+# 真实 Tomcat + MySQL,保持 SSE 订阅时发送 SIGINT
+# graceful shutdown: 约 57ms(不再等待 30s)
+
+
+
+ 22. 主动承认的边界
+
+ - 规则链不能解析任意 Shell 的完整语义,不能证明命令绝对安全。
+ - 数据库与远端 Linux 没有分布式事务,EXECUTING 崩溃只能进入 NEEDS_REVIEW。
+ - 模型 Future.cancel(true) 是否真正中断网络请求取决于底层客户端。
+ - 当前是本机单用户、固定 2 个工作线程,不是多租户分布式调度平台。
+ - SSH Agent 认证未实现;当前可靠支持密码和私钥。
+ - OpenTelemetry Collector、Grafana Dashboard 未部署;已经暴露 Prometheus 指标。
+ - 30 场景评测是安全策略离线集,不是完整的模型端到端效果评测。
+ - RAG 未实现,因为没有真实私有知识源;MCP 未接入核心 SSH 工具。
+
+ 这些不是“项目做得差”,而是清楚划定第一版的可信边界。面试时主动说明,比声称“正则保证安全、幂等保证绝对一次”更成熟。
+
+
+
+ 23. 面试官追问时怎么回答
+ Q:为什么审批用 SSE + HTTP,不直接 WebSocket?
A:事件是服务器到客户端的单向通知,SSE 天然支持事件 ID 和断线重连;审批是有副作用的命令,需要事务、幂等 Key、冲突码和审计,所以独立 POST 更清晰。本机单用户版本用 WebSocket 会增加连接状态复杂度但没有明显收益。
+ Q:CompletableFuture 等待时会不会占用 Tomcat 请求线程?
A:不会。创建任务的 HTTP 已返回 202,Agent Loop 在专用 agent-task 线程池。Future.get 阻塞的是工作线程;审批 HTTP 使用另一条请求线程。Future 只负责 JVM 内唤醒,数据库才是持久化真相。
+ Q:approvalId 是 UUID,为什么还要 actionDigest?
A:UUID 只是外部标识。actionDigest 表示工具名、规范化参数、主机和策略版本对应的业务动作。唯一键阻止同一动作创建多份审批,approvalId 才能稳定复用。
+ Q:你能保证命令 exactly once 吗?
A:能保证同一 Step 在应用内只领取一次执行权,但不能跨 MySQL 与远端 Linux 宣称绝对 exactly once。若远端执行后进程在本地记结果前崩溃,状态不确定,所以重启后进入 NEEDS_REVIEW,绝不自动重放。
+ Q:为什么只取消 Flux 订阅不够?
A:SSE 只是观察通道,后台 Loop 和 SSH Channel 在别的线程。取消必须持久化 CANCELLING,并向模型 Future、工作线程和当前 Channel 分别传播信号。
+ Q:为什么不自动回滚?
A:回滚同样可能是高风险副作用,而且原状态未必能完整恢复。第一版只保存支持范围内的快照、执行后只读验证、生成回滚建议;真正回滚作为新 Step 再审批。
+ Q:为什么没有 RAG/MCP,会不会技术不够新?
A:项目的核心问题是安全执行治理,不是知识问答。没有真实 SOP 时加 RAG 没有可评测收益;核心 SSH 放远程 MCP 反而新增第二条执行边界。后续外围只读工具适合 MCP,有真实知识源后再用 RAG 增强 Plan。
+ 三分钟完整讲述模板
+ “用户先通过幂等 POST 创建任务,同一个 Key 和请求体永远返回相同 taskId。HTTP 返回后,专用线程运行 Agent Loop。Spring AI 把 SshTools 暴露成 Function Schema,但我关闭自动工具执行,让模型只能返回意图。每个 Tool Call 先被规则链评估,ALLOW 直接进入执行准备,DENY 回灌拒绝结果让模型换方案,ASK 则在同一事务写 Task、Step、Approval 和 approval_required 事件。SSE 在事务提交后推给前端,用户通过独立幂等 HTTP 审批。Agent 工作线程停在 CompletableFuture.get,审批事务提交后 complete,原调用栈恢复。批准还不够,Step 必须用 CAS 从 READY_TO_EXECUTE 变成 EXECUTING,防止重复执行。SSH 有 30 秒命令超时、1MB 输出上限和主动 Channel 取消,执行后保存结果并做只读验证。服务重启时从数据库恢复;如果崩溃前 Step 已经 EXECUTING,就进入 NEEDS_REVIEW,避免不确定副作用被重放。最后用 Micrometer 和固定评测集看成本、延迟、工具效率和安全误放行。”
+
+
+
+
+
+
+
From ef54c24ec1aca60c9f78fbf3aeb819a1cf45cacd Mon Sep 17 00:00:00 2001
From: xiaowen <0928du@gmail.com>
Date: Sun, 9 Aug 2026 19:18:59 +0800
Subject: [PATCH 3/3] =?UTF-8?q?chore:=20=E8=BF=9C=E7=A8=8B=E4=BB=85?=
=?UTF-8?q?=E4=BF=9D=E7=95=99=20Flutter=20=E6=A1=8C=E9=9D=A2=E7=AB=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.env.example | 10 -
.github/workflows/ci.yml | 38 -
.gitignore | 11 +
.mvn/wrapper/maven-wrapper.jar | Bin 63028 -> 0 bytes
.mvn/wrapper/maven-wrapper.properties | 19 -
CONTRIBUTING.md | 107 +-
Dockerfile | 20 -
README.md | 132 +-
clients/cli/.gitignore | 4 -
clients/cli/README.md | 58 -
clients/cli/package-lock.json | 4113 -----------------
clients/cli/package.json | 38 -
clients/cli/src/cli.tsx | 29 -
clients/cli/src/core/agent.ts | 290 --
clients/cli/src/core/config.ts | 124 -
clients/cli/src/core/context.ts | 162 -
clients/cli/src/core/crypto.test.ts | 34 -
clients/cli/src/core/crypto.ts | 55 -
clients/cli/src/core/events.ts | 66 -
clients/cli/src/core/glm.ts | 152 -
clients/cli/src/core/guard.test.ts | 75 -
clients/cli/src/core/guard.ts | 113 -
clients/cli/src/core/ssh.ts | 170 -
clients/cli/src/ui/AddHost.tsx | 108 -
clients/cli/src/ui/App.tsx | 116 -
clients/cli/src/ui/Chat.tsx | 163 -
clients/cli/src/ui/ConfirmPrompt.tsx | 38 -
clients/cli/src/ui/HostSelect.tsx | 54 -
clients/cli/tsconfig.json | 20 -
clients/cli/tsup.config.ts | 15 -
clients/cli/vitest.config.ts | 8 -
docker-compose.yml | 48 -
mvnw | 332 --
mvnw.cmd | 206 -
pom.xml | 115 -
.../com/lowenssh/LowenSshApplication.java | 19 -
.../com/lowenssh/agent/AgentController.java | 193 -
.../java/com/lowenssh/agent/AgentEvent.java | 55 -
.../com/lowenssh/agent/AgentRunObserver.java | 48 -
.../java/com/lowenssh/agent/AgentService.java | 601 ---
.../com/lowenssh/agent/ContextManager.java | 328 --
.../com/lowenssh/agent/HostController.java | 154 -
src/main/java/com/lowenssh/agent/HostDto.java | 44 -
.../java/com/lowenssh/agent/HostMetrics.java | 20 -
.../com/lowenssh/agent/MetricsCollector.java | 170 -
.../com/lowenssh/agent/MonitorController.java | 42 -
.../java/com/lowenssh/agent/SessionDto.java | 29 -
.../com/lowenssh/agent/SessionManager.java | 256 -
.../com/lowenssh/agent/SftpController.java | 153 -
.../lowenssh/agent/SshSecurityController.java | 62 -
.../java/com/lowenssh/agent/SshTools.java | 219 -
.../com/lowenssh/agent/ToolRiskCommand.java | 41 -
.../agent/approval/ApprovalApiDto.java | 29 -
.../approval/ApprovalApiExceptionHandler.java | 17 -
.../agent/approval/ApprovalController.java | 36 -
.../agent/approval/ApprovalCoordinator.java | 73 -
.../agent/approval/ApprovalDecision.java | 16 -
.../approval/ApprovalDecisionService.java | 227 -
.../approval/ApprovalExpiryScheduler.java | 35 -
.../agent/approval/ApprovalRequest.java | 16 -
.../agent/approval/ApprovalService.java | 235 -
.../agent/approval/ApprovalStatus.java | 14 -
.../agent/approval/ApprovalWaitRegistry.java | 36 -
.../PersistentConfirmationHandler.java | 80 -
.../PersistentConfirmationHandlerFactory.java | 43 -
.../lowenssh/agent/guard/CommandGuard.java | 91 -
.../agent/guard/ConfirmationHandler.java | 29 -
.../agent/guard/ConfirmationRequest.java | 25 -
.../guard/ConsoleConfirmationHandler.java | 24 -
.../guard/RejectingConfirmationHandler.java | 20 -
.../agent/guard/policy/CommandContext.java | 19 -
.../agent/guard/policy/CommandPolicy.java | 9 -
.../guard/policy/CommandPolicyEngine.java | 46 -
.../guard/policy/CommandShapePolicy.java | 36 -
.../policy/DestructiveCommandPolicy.java | 41 -
.../guard/policy/IndirectExecutionPolicy.java | 37 -
.../agent/guard/policy/PolicyDecision.java | 8 -
.../agent/guard/policy/PolicyMatch.java | 10 -
.../agent/guard/policy/PolicyResult.java | 16 -
.../policy/PrivilegeEscalationPolicy.java | 25 -
.../guard/policy/ReadOnlyCommandPolicy.java | 43 -
.../agent/guard/policy/RiskLevel.java | 8 -
.../guard/policy/WriteOperationPolicy.java | 40 -
.../lowenssh/agent/task/AgentStepService.java | 85 -
.../lowenssh/agent/task/CanonicalJson.java | 46 -
.../task/DuplicateToolExecutionException.java | 12 -
.../agent/task/ExecutionSafetyService.java | 132 -
.../task/IdempotencyConflictException.java | 16 -
.../lowenssh/agent/task/IdempotencyScope.java | 8 -
.../task/IllegalTaskTransitionException.java | 22 -
.../task/PersistentAgentRunObserver.java | 262 --
.../agent/task/RequestFingerprint.java | 33 -
.../com/lowenssh/agent/task/TaskApiDto.java | 49 -
.../agent/task/TaskApiExceptionHandler.java | 37 -
.../agent/task/TaskCancellationFinalizer.java | 31 -
.../agent/task/TaskCancellationService.java | 166 -
.../agent/task/TaskCancelledException.java | 9 -
.../agent/task/TaskCommandService.java | 172 -
.../lowenssh/agent/task/TaskController.java | 97 -
.../agent/task/TaskEventPublisher.java | 56 -
.../lowenssh/agent/task/TaskEventService.java | 141 -
.../lowenssh/agent/task/TaskEventView.java | 16 -
.../task/TaskExecutionBudgetService.java | 103 -
.../task/TaskLimitExceededException.java | 16 -
.../agent/task/TaskNotFoundException.java | 16 -
.../com/lowenssh/agent/task/TaskPhase.java | 11 -
.../agent/task/TaskRecoveryScheduler.java | 128 -
.../agent/task/TaskRuntimeRegistry.java | 121 -
.../lowenssh/agent/task/TaskStateMachine.java | 64 -
.../com/lowenssh/agent/task/TaskStatus.java | 33 -
.../agent/task/TaskTimeoutScheduler.java | 46 -
.../agent/task/TaskTransitionService.java | 54 -
.../agent/task/TaskWorkflowOrchestrator.java | 316 --
.../task/WorkflowPersistenceService.java | 365 --
.../lowenssh/observability/AgentMetrics.java | 110 -
.../lowenssh/persistence/AuditService.java | 63 -
.../lowenssh/persistence/MessageService.java | 252 -
.../persistence/SchemaInitializer.java | 293 --
.../entity/AgentApprovalEntity.java | 30 -
.../persistence/entity/AgentStepEntity.java | 42 -
.../persistence/entity/AgentTaskEntity.java | 38 -
.../entity/AgentTaskEventEntity.java | 22 -
.../persistence/entity/AuditEntity.java | 29 -
.../persistence/entity/HostEntity.java | 36 -
.../entity/IdempotencyRecordEntity.java | 26 -
.../persistence/entity/MessageEntity.java | 27 -
.../persistence/entity/SessionEntity.java | 27 -
.../mapper/AgentApprovalMapper.java | 75 -
.../persistence/mapper/AgentStepMapper.java | 133 -
.../mapper/AgentTaskEventMapper.java | 22 -
.../persistence/mapper/AgentTaskMapper.java | 136 -
.../persistence/mapper/AuditMapper.java | 12 -
.../persistence/mapper/HostMapper.java | 12 -
.../mapper/IdempotencyRecordMapper.java | 55 -
.../persistence/mapper/MessageMapper.java | 12 -
.../persistence/mapper/SessionMapper.java | 12 -
.../java/com/lowenssh/ssh/ExecResult.java | 22 -
.../ssh/KnownHostConflictException.java | 9 -
.../com/lowenssh/ssh/KnownHostsService.java | 138 -
.../java/com/lowenssh/ssh/RemoteFile.java | 20 -
src/main/java/com/lowenssh/ssh/SshAuth.java | 20 -
src/main/java/com/lowenssh/ssh/SshClient.java | 501 --
.../com/lowenssh/ssh/SshClientFactory.java | 56 -
.../lowenssh/ssh/SshExecutionObserver.java | 12 -
.../java/com/lowenssh/util/CryptoUtil.java | 147 -
src/main/resources/application.yml | 129 -
src/main/resources/schema.sql | 175 -
.../com/lowenssh/agent/AgentServiceTest.java | 243 -
.../lowenssh/agent/ContextManagerTest.java | 240 -
.../lowenssh/agent/RealTokenBillingTest.java | 201 -
.../lowenssh/agent/SessionManagerTest.java | 105 -
.../agent/SshToolsSftpSafetyTest.java | 43 -
.../approval/ApprovalIntegrationTest.java | 236 -
.../guard/AgentSecurityEvaluationTest.java | 78 -
.../agent/guard/CommandGuardTest.java | 137 -
.../RejectingConfirmationHandlerTest.java | 20 -
.../agent/task/TaskEventPublisherTest.java | 24 -
.../task/TaskPersistenceIntegrationTest.java | 442 --
.../agent/task/TaskRuntimeRegistryTest.java | 77 -
.../agent/task/TaskStateMachineTest.java | 50 -
...skWorkflowOrchestratorIntegrationTest.java | 323 --
.../persistence/MessageServiceTest.java | 119 -
.../lowenssh/ssh/KnownHostsServiceTest.java | 72 -
.../ssh/SshClientIntegrationTest.java | 209 -
.../ssh/SshClientOutputLimitTest.java | 31 -
.../com/lowenssh/util/CryptoUtilTest.java | 37 -
.../resources/agent-evaluation-scenarios.json | 34 -
src/test/resources/task-test-schema.sql | 97 -
168 files changed, 62 insertions(+), 18973 deletions(-)
delete mode 100644 .env.example
delete mode 100644 .mvn/wrapper/maven-wrapper.jar
delete mode 100644 .mvn/wrapper/maven-wrapper.properties
delete mode 100644 Dockerfile
delete mode 100644 clients/cli/.gitignore
delete mode 100644 clients/cli/README.md
delete mode 100644 clients/cli/package-lock.json
delete mode 100644 clients/cli/package.json
delete mode 100644 clients/cli/src/cli.tsx
delete mode 100644 clients/cli/src/core/agent.ts
delete mode 100644 clients/cli/src/core/config.ts
delete mode 100644 clients/cli/src/core/context.ts
delete mode 100644 clients/cli/src/core/crypto.test.ts
delete mode 100644 clients/cli/src/core/crypto.ts
delete mode 100644 clients/cli/src/core/events.ts
delete mode 100644 clients/cli/src/core/glm.ts
delete mode 100644 clients/cli/src/core/guard.test.ts
delete mode 100644 clients/cli/src/core/guard.ts
delete mode 100644 clients/cli/src/core/ssh.ts
delete mode 100644 clients/cli/src/ui/AddHost.tsx
delete mode 100644 clients/cli/src/ui/App.tsx
delete mode 100644 clients/cli/src/ui/Chat.tsx
delete mode 100644 clients/cli/src/ui/ConfirmPrompt.tsx
delete mode 100644 clients/cli/src/ui/HostSelect.tsx
delete mode 100644 clients/cli/tsconfig.json
delete mode 100644 clients/cli/tsup.config.ts
delete mode 100644 clients/cli/vitest.config.ts
delete mode 100644 docker-compose.yml
delete mode 100755 mvnw
delete mode 100644 mvnw.cmd
delete mode 100644 pom.xml
delete mode 100644 src/main/java/com/lowenssh/LowenSshApplication.java
delete mode 100644 src/main/java/com/lowenssh/agent/AgentController.java
delete mode 100644 src/main/java/com/lowenssh/agent/AgentEvent.java
delete mode 100644 src/main/java/com/lowenssh/agent/AgentRunObserver.java
delete mode 100644 src/main/java/com/lowenssh/agent/AgentService.java
delete mode 100644 src/main/java/com/lowenssh/agent/ContextManager.java
delete mode 100644 src/main/java/com/lowenssh/agent/HostController.java
delete mode 100644 src/main/java/com/lowenssh/agent/HostDto.java
delete mode 100644 src/main/java/com/lowenssh/agent/HostMetrics.java
delete mode 100644 src/main/java/com/lowenssh/agent/MetricsCollector.java
delete mode 100644 src/main/java/com/lowenssh/agent/MonitorController.java
delete mode 100644 src/main/java/com/lowenssh/agent/SessionDto.java
delete mode 100644 src/main/java/com/lowenssh/agent/SessionManager.java
delete mode 100644 src/main/java/com/lowenssh/agent/SftpController.java
delete mode 100644 src/main/java/com/lowenssh/agent/SshSecurityController.java
delete mode 100644 src/main/java/com/lowenssh/agent/SshTools.java
delete mode 100644 src/main/java/com/lowenssh/agent/ToolRiskCommand.java
delete mode 100644 src/main/java/com/lowenssh/agent/approval/ApprovalApiDto.java
delete mode 100644 src/main/java/com/lowenssh/agent/approval/ApprovalApiExceptionHandler.java
delete mode 100644 src/main/java/com/lowenssh/agent/approval/ApprovalController.java
delete mode 100644 src/main/java/com/lowenssh/agent/approval/ApprovalCoordinator.java
delete mode 100644 src/main/java/com/lowenssh/agent/approval/ApprovalDecision.java
delete mode 100644 src/main/java/com/lowenssh/agent/approval/ApprovalDecisionService.java
delete mode 100644 src/main/java/com/lowenssh/agent/approval/ApprovalExpiryScheduler.java
delete mode 100644 src/main/java/com/lowenssh/agent/approval/ApprovalRequest.java
delete mode 100644 src/main/java/com/lowenssh/agent/approval/ApprovalService.java
delete mode 100644 src/main/java/com/lowenssh/agent/approval/ApprovalStatus.java
delete mode 100644 src/main/java/com/lowenssh/agent/approval/ApprovalWaitRegistry.java
delete mode 100644 src/main/java/com/lowenssh/agent/approval/PersistentConfirmationHandler.java
delete mode 100644 src/main/java/com/lowenssh/agent/approval/PersistentConfirmationHandlerFactory.java
delete mode 100644 src/main/java/com/lowenssh/agent/guard/CommandGuard.java
delete mode 100644 src/main/java/com/lowenssh/agent/guard/ConfirmationHandler.java
delete mode 100644 src/main/java/com/lowenssh/agent/guard/ConfirmationRequest.java
delete mode 100644 src/main/java/com/lowenssh/agent/guard/ConsoleConfirmationHandler.java
delete mode 100644 src/main/java/com/lowenssh/agent/guard/RejectingConfirmationHandler.java
delete mode 100644 src/main/java/com/lowenssh/agent/guard/policy/CommandContext.java
delete mode 100644 src/main/java/com/lowenssh/agent/guard/policy/CommandPolicy.java
delete mode 100644 src/main/java/com/lowenssh/agent/guard/policy/CommandPolicyEngine.java
delete mode 100644 src/main/java/com/lowenssh/agent/guard/policy/CommandShapePolicy.java
delete mode 100644 src/main/java/com/lowenssh/agent/guard/policy/DestructiveCommandPolicy.java
delete mode 100644 src/main/java/com/lowenssh/agent/guard/policy/IndirectExecutionPolicy.java
delete mode 100644 src/main/java/com/lowenssh/agent/guard/policy/PolicyDecision.java
delete mode 100644 src/main/java/com/lowenssh/agent/guard/policy/PolicyMatch.java
delete mode 100644 src/main/java/com/lowenssh/agent/guard/policy/PolicyResult.java
delete mode 100644 src/main/java/com/lowenssh/agent/guard/policy/PrivilegeEscalationPolicy.java
delete mode 100644 src/main/java/com/lowenssh/agent/guard/policy/ReadOnlyCommandPolicy.java
delete mode 100644 src/main/java/com/lowenssh/agent/guard/policy/RiskLevel.java
delete mode 100644 src/main/java/com/lowenssh/agent/guard/policy/WriteOperationPolicy.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/AgentStepService.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/CanonicalJson.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/DuplicateToolExecutionException.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/ExecutionSafetyService.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/IdempotencyConflictException.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/IdempotencyScope.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/IllegalTaskTransitionException.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/PersistentAgentRunObserver.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/RequestFingerprint.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/TaskApiDto.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/TaskApiExceptionHandler.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/TaskCancellationFinalizer.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/TaskCancellationService.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/TaskCancelledException.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/TaskCommandService.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/TaskController.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/TaskEventPublisher.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/TaskEventService.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/TaskEventView.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/TaskExecutionBudgetService.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/TaskLimitExceededException.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/TaskNotFoundException.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/TaskPhase.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/TaskRecoveryScheduler.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/TaskRuntimeRegistry.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/TaskStateMachine.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/TaskStatus.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/TaskTimeoutScheduler.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/TaskTransitionService.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/TaskWorkflowOrchestrator.java
delete mode 100644 src/main/java/com/lowenssh/agent/task/WorkflowPersistenceService.java
delete mode 100644 src/main/java/com/lowenssh/observability/AgentMetrics.java
delete mode 100644 src/main/java/com/lowenssh/persistence/AuditService.java
delete mode 100644 src/main/java/com/lowenssh/persistence/MessageService.java
delete mode 100644 src/main/java/com/lowenssh/persistence/SchemaInitializer.java
delete mode 100644 src/main/java/com/lowenssh/persistence/entity/AgentApprovalEntity.java
delete mode 100644 src/main/java/com/lowenssh/persistence/entity/AgentStepEntity.java
delete mode 100644 src/main/java/com/lowenssh/persistence/entity/AgentTaskEntity.java
delete mode 100644 src/main/java/com/lowenssh/persistence/entity/AgentTaskEventEntity.java
delete mode 100644 src/main/java/com/lowenssh/persistence/entity/AuditEntity.java
delete mode 100644 src/main/java/com/lowenssh/persistence/entity/HostEntity.java
delete mode 100644 src/main/java/com/lowenssh/persistence/entity/IdempotencyRecordEntity.java
delete mode 100644 src/main/java/com/lowenssh/persistence/entity/MessageEntity.java
delete mode 100644 src/main/java/com/lowenssh/persistence/entity/SessionEntity.java
delete mode 100644 src/main/java/com/lowenssh/persistence/mapper/AgentApprovalMapper.java
delete mode 100644 src/main/java/com/lowenssh/persistence/mapper/AgentStepMapper.java
delete mode 100644 src/main/java/com/lowenssh/persistence/mapper/AgentTaskEventMapper.java
delete mode 100644 src/main/java/com/lowenssh/persistence/mapper/AgentTaskMapper.java
delete mode 100644 src/main/java/com/lowenssh/persistence/mapper/AuditMapper.java
delete mode 100644 src/main/java/com/lowenssh/persistence/mapper/HostMapper.java
delete mode 100644 src/main/java/com/lowenssh/persistence/mapper/IdempotencyRecordMapper.java
delete mode 100644 src/main/java/com/lowenssh/persistence/mapper/MessageMapper.java
delete mode 100644 src/main/java/com/lowenssh/persistence/mapper/SessionMapper.java
delete mode 100644 src/main/java/com/lowenssh/ssh/ExecResult.java
delete mode 100644 src/main/java/com/lowenssh/ssh/KnownHostConflictException.java
delete mode 100644 src/main/java/com/lowenssh/ssh/KnownHostsService.java
delete mode 100644 src/main/java/com/lowenssh/ssh/RemoteFile.java
delete mode 100644 src/main/java/com/lowenssh/ssh/SshAuth.java
delete mode 100644 src/main/java/com/lowenssh/ssh/SshClient.java
delete mode 100644 src/main/java/com/lowenssh/ssh/SshClientFactory.java
delete mode 100644 src/main/java/com/lowenssh/ssh/SshExecutionObserver.java
delete mode 100644 src/main/java/com/lowenssh/util/CryptoUtil.java
delete mode 100644 src/main/resources/application.yml
delete mode 100644 src/main/resources/schema.sql
delete mode 100644 src/test/java/com/lowenssh/agent/AgentServiceTest.java
delete mode 100644 src/test/java/com/lowenssh/agent/ContextManagerTest.java
delete mode 100644 src/test/java/com/lowenssh/agent/RealTokenBillingTest.java
delete mode 100644 src/test/java/com/lowenssh/agent/SessionManagerTest.java
delete mode 100644 src/test/java/com/lowenssh/agent/SshToolsSftpSafetyTest.java
delete mode 100644 src/test/java/com/lowenssh/agent/approval/ApprovalIntegrationTest.java
delete mode 100644 src/test/java/com/lowenssh/agent/guard/AgentSecurityEvaluationTest.java
delete mode 100644 src/test/java/com/lowenssh/agent/guard/CommandGuardTest.java
delete mode 100644 src/test/java/com/lowenssh/agent/guard/RejectingConfirmationHandlerTest.java
delete mode 100644 src/test/java/com/lowenssh/agent/task/TaskEventPublisherTest.java
delete mode 100644 src/test/java/com/lowenssh/agent/task/TaskPersistenceIntegrationTest.java
delete mode 100644 src/test/java/com/lowenssh/agent/task/TaskRuntimeRegistryTest.java
delete mode 100644 src/test/java/com/lowenssh/agent/task/TaskStateMachineTest.java
delete mode 100644 src/test/java/com/lowenssh/agent/task/TaskWorkflowOrchestratorIntegrationTest.java
delete mode 100644 src/test/java/com/lowenssh/persistence/MessageServiceTest.java
delete mode 100644 src/test/java/com/lowenssh/ssh/KnownHostsServiceTest.java
delete mode 100644 src/test/java/com/lowenssh/ssh/SshClientIntegrationTest.java
delete mode 100644 src/test/java/com/lowenssh/ssh/SshClientOutputLimitTest.java
delete mode 100644 src/test/java/com/lowenssh/util/CryptoUtilTest.java
delete mode 100644 src/test/resources/agent-evaluation-scenarios.json
delete mode 100644 src/test/resources/task-test-schema.sql
diff --git a/.env.example b/.env.example
deleted file mode 100644
index 27ceed4..0000000
--- a/.env.example
+++ /dev/null
@@ -1,10 +0,0 @@
-# 复制为 .env 后填写真实值;.env 已被 Git 忽略。
-MYSQL_PASSWORD=请替换为MySQL强密码
-GLM_API_KEY=请替换为智谱AI密钥
-
-# 可用 openssl rand -base64 32 生成。部署后必须安全备份,丢失将无法解密已保存凭据。
-XWSSH_CRYPTO_KEY=请替换为随机主密钥
-
-# 轮换示例:
-# XWSSH_CRYPTO_KEYS=v2=新密钥,v1=旧密钥
-# XWSSH_ACTIVE_CRYPTO_KEY_VERSION=v2
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 82faa06..fc994f2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,21 +7,6 @@ on:
branches: [main]
jobs:
- backend:
- name: 后端(Java)
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - name: 安装 JDK 17
- uses: actions/setup-java@v4
- with:
- java-version: '17'
- distribution: temurin
- cache: maven
- # 单测均为纯单元测试,不依赖 MySQL,可直接跑
- - name: 编译 + 单测
- run: ./mvnw -B clean test
-
app:
name: 桌面端(Flutter)
runs-on: ubuntu-latest
@@ -40,26 +25,3 @@ jobs:
run: flutter analyze
- name: 单测
run: flutter test
-
- cli:
- name: CLI(Node)
- runs-on: ubuntu-latest
- defaults:
- run:
- working-directory: clients/cli
- steps:
- - uses: actions/checkout@v4
- - name: 安装 Node 20
- uses: actions/setup-node@v4
- with:
- node-version: '20'
- cache: npm
- cache-dependency-path: clients/cli/package-lock.json
- - name: 装依赖
- run: npm ci
- - name: 类型检查
- run: npm run typecheck
- - name: 单测
- run: npm test
- - name: 构建
- run: npm run build
diff --git a/.gitignore b/.gitignore
index f1f81ee..209b83b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,14 @@ logs/
# CLI 客户端(Node)构建产物与依赖
clients/cli/node_modules/
clients/cli/dist/
+
+# 本地保留的私有实现:不再提交到远程仓库
+/src/
+/pom.xml
+/.mvn/
+/mvnw
+/mvnw.cmd
+/Dockerfile
+/docker-compose.yml
+/.env.example
+/clients/cli/
diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar
deleted file mode 100644
index 7967f30dd1d25fe1b79a4a6e50e2aaa0e425c02c..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 63028
zcmb4q1CS^|ljgjcH@0otwr$(CZQHhO+qV72wmoxial8B9?fn