diff --git a/mateclaw-server/src/main/resources/docs/en/managed-json-acceptance.md b/mateclaw-server/src/main/resources/docs/en/managed-json-acceptance.md index 0ec7ccaa..cafc95f0 100644 --- a/mateclaw-server/src/main/resources/docs/en/managed-json-acceptance.md +++ b/mateclaw-server/src/main/resources/docs/en/managed-json-acceptance.md @@ -18,7 +18,7 @@ Prefix: `/api/v1/goals/{goalId}/json-acceptance`. An enabled account with conver Publication requires an active or paused goal and a slot referenced by a current requirement. Content must be a strict JSON object: duplicate keys, trailing documents, nesting beyond 32 levels and UTF-8 content over 1 MiB are rejected. Each goal can retain at most 32 versions; the limit rejects new publication instead of overwriting history. Each version expires after 24 hours. Republishing identical bytes still creates a new version. Reload after a generation conflict rather than automatically overwriting another publication. Retries can reuse a suitable current version and refresh its check binding; reaching the quota still permits checking and completing with that version. If the version is expired or its content must change and all 32 versions are used, further publication remains unavailable. -Managed bodies live independently in the database. Ordinary workspace files, cache paths and hashes in text are not substitutes. No publication API edits historical bodies; bodies and pointers commit together. SHA-256 identifies content and supports integrity checks; it does not isolate an attacker with database credentials or host privileges. The database and service host are trusted foundations of this limited protocol. The JSON service contract has been exercised on H2, MySQL 8.0.46 and PostgreSQL 16.14. MySQL and PostgreSQL each passed 72 JSON protocol cases with V201 on cycle062 and 17 opt-in HTTP approval/authentication and scheduled-queue cases on cycle066. The MySQL run isolated an existing V192 migration failure using a test-only migration copy; PostgreSQL used the original Kingbase migration tree while skipping an unrelated bundled-skill import failure. These are protocol tests, not confirmation that an unmodified full installation succeeds. The proprietary Kingbase engine has not been tested. +Managed bodies live independently in the database. Ordinary workspace files, cache paths and hashes in text are not substitutes. No publication API edits historical bodies; bodies and pointers commit together. SHA-256 identifies content and supports integrity checks; it does not isolate an attacker with database credentials or host privileges. The database and service host are trusted foundations of this limited protocol. The JSON service contract has been exercised on H2, MySQL 8.0.46 and PostgreSQL 16.14. MySQL and PostgreSQL each passed 72 JSON protocol cases with V201 on cycle062 and 21 opt-in HTTP approval/authentication and scheduled-queue cases on cycle067, including foreign-Goal rejection before any model call and paused queue delivery after resume. The MySQL run isolated an existing V192 migration failure using a test-only migration copy; PostgreSQL used the original Kingbase migration tree while skipping an unrelated bundled-skill import failure. These are protocol tests, not confirmation that an unmodified full installation succeeds. The proprietary Kingbase engine has not been tested. ## Agent publication diff --git a/mateclaw-server/src/main/resources/docs/zh/managed-json-acceptance.md b/mateclaw-server/src/main/resources/docs/zh/managed-json-acceptance.md index c4842ce1..09715b23 100644 --- a/mateclaw-server/src/main/resources/docs/zh/managed-json-acceptance.md +++ b/mateclaw-server/src/main/resources/docs/zh/managed-json-acceptance.md @@ -18,7 +18,7 @@ 仅当前要求引用的槽可发布,Goal 必须 active 或 paused。正文必须是严格 JSON 对象,拒绝重复键、尾随文档、超过 32 层的嵌套及超过 1 MiB 的 UTF-8 内容。每个 Goal 最多保存 32 个版本;达到配额拒绝继续发布,不覆盖旧版本。每版有效期 24 小时,重复发布同样正文也产生新版本。客户端遇到 generation 冲突应重新读取,不自动覆盖他人发布。重试可以复用适用的当前版本并更新检查绑定;达到配额后仍可检查当前版本并在合格时完成。如果版本已过期或正文必须修改且32个版本均已使用,则不能继续发布。 -这些版本独立存储在数据库,不能用普通工作区文件、缓存路径或文字中的 hash 替代。发布接口不支持更新历史正文;所有版本与槽指针同事务保存。SHA-256 用于标识及完整性核对,不能隔离拥有数据库凭据或宿主权限的攻击者;数据库和服务宿主是此有限协议的可信基础。MySQL与PostgreSQL在带V201的cycle062源码上各通过72项JSON协议案例,在cycle066各通过17项显式启用的HTTP审批/认证及后台队列案例。MySQL 使用仅修正既有 V192 失败的临时迁移副本;PostgreSQL 使用原始 Kingbase 迁移树,跳过无关的内置技能导入失败。这是协议验证,不能代表未修改的完整安装成功;尚未实测 Kingbase 专有引擎。 +这些版本独立存储在数据库,不能用普通工作区文件、缓存路径或文字中的 hash 替代。发布接口不支持更新历史正文;所有版本与槽指针同事务保存。SHA-256 用于标识及完整性核对,不能隔离拥有数据库凭据或宿主权限的攻击者;数据库和服务宿主是此有限协议的可信基础。MySQL与PostgreSQL在带V201的cycle062源码上各通过72项JSON协议案例,在cycle067各通过21项显式启用的HTTP审批/认证及后台队列案例,包含跨Goal错领后模型调用0、暂停恢复后才执行的案例。MySQL 使用仅修正既有 V192 失败的临时迁移副本;PostgreSQL 使用原始 Kingbase 迁移树,跳过无关的内置技能导入失败。这是协议验证,不能代表未修改的完整安装成功;尚未实测 Kingbase 专有引擎。 ## 代理发布 diff --git a/mateclaw-server/src/test/java/vip/mate/goal/GoalJsonExternalApprovalIntegrationTest.java b/mateclaw-server/src/test/java/vip/mate/goal/GoalJsonExternalApprovalIntegrationTest.java index 0868879c..49225ab9 100644 --- a/mateclaw-server/src/test/java/vip/mate/goal/GoalJsonExternalApprovalIntegrationTest.java +++ b/mateclaw-server/src/test/java/vip/mate/goal/GoalJsonExternalApprovalIntegrationTest.java @@ -16,6 +16,8 @@ class GoalJsonExternalApprovalIntegrationTest extends GoalJsonHttpRuntimeIntegra @ParameterizedTest @CsvSource({"false,approval,true", "true,approval,true", "false,scheduled-queued,true", "true,scheduled-queued,true", + "false,scheduled-queued-foreign,true", "true,scheduled-queued-foreign,true", + "false,scheduled-queued-paused,true", "true,scheduled-queued-paused,true", "false,terminal-approval,true", "true,terminal-approval,true", "false,legacy-terminal-approval,true", "true,legacy-terminal-approval,true", "false,originless-terminal-approval,true", "true,originless-terminal-approval,true", diff --git a/mateclaw-server/src/test/java/vip/mate/goal/GoalJsonHttpRuntimeIntegrationTest.java b/mateclaw-server/src/test/java/vip/mate/goal/GoalJsonHttpRuntimeIntegrationTest.java index 8c4706ac..0ad75911 100644 --- a/mateclaw-server/src/test/java/vip/mate/goal/GoalJsonHttpRuntimeIntegrationTest.java +++ b/mateclaw-server/src/test/java/vip/mate/goal/GoalJsonHttpRuntimeIntegrationTest.java @@ -77,7 +77,9 @@ class GoalJsonHttpRuntimeIntegrationTest { @org.junit.jupiter.params.ParameterizedTest @org.junit.jupiter.params.provider.CsvSource({"false,sync,true", "true,sync,true", "false,stream,true", "true,stream,true", - "false,scheduled,true", "true,scheduled,true", "false,scheduled-queued,true", "true,scheduled-queued,true", "false,recovered,true", "true,recovered,true", + "false,scheduled,true", "true,scheduled,true", "false,scheduled-queued,true", "true,scheduled-queued,true", + "false,scheduled-queued-foreign,true", "true,scheduled-queued-foreign,true", + "false,scheduled-queued-paused,true", "true,scheduled-queued-paused,true", "false,recovered,true", "true,recovered,true", "false,scheduled,false", "true,scheduled,false", "false,recovered,false", "true,recovered,false", "false,queued,true", "false,reuse,true", "true,reuse,true", "false,recheck,true", "true,recheck,true", "false,supervised,true", "true,supervised,true", "false,supervised-recovered,true", "true,supervised-recovered,true", @@ -591,6 +593,55 @@ class GoalJsonHttpRuntimeIntegrationTest { runner.cancel(goal.getId()); } } else if (scheduled) { + if (entry.equals("scheduled-queued-paused")) { + var queuedInput = new vip.mate.channel.web.ConversationInputQueueStore(jdbc, json).enqueue( + conversation, agentId, username, message, List.of(), userId, goal.getId(), + java.time.LocalDateTime.now()); + goals.pause(goal.getId(), username); + SegmentOutcome pausedOutcome = runner.run(run, message, false); + assertInstanceOf(SegmentOutcome.Cancelled.class, pausedOutcome); + assertEquals(0, calls.get(), "Paused Goal must not start a model call"); + assertEquals("queued", jdbc.queryForObject( + "SELECT state FROM mate_conversation_input_queue WHERE id=?", String.class, queuedInput.id())); + assertTrue(coordinator.settle(run, pausedOutcome, java.time.LocalDateTime.now())); + goals.resume(goal.getId(), username); + var resumedRun = claim(goal); + SegmentOutcome resumedOutcome = runner.run(resumedRun, message, false); + assertEquals(GoalStatus.COMPLETED, goals.getById(goal.getId()).getStatus(), resumedOutcome.toString()); + assertEquals("consumed", jdbc.queryForObject( + "SELECT state FROM mate_conversation_input_queue WHERE id=?", String.class, queuedInput.id())); + assertTrue(coordinator.settle(resumedRun, resumedOutcome, java.time.LocalDateTime.now())); + assertEquals("completed", continuations.get(goal.getId()).state()); + return; + } + if (entry.equals("scheduled-queued-foreign")) { + String foreignConversation = UUID.randomUUID().toString(); + jdbc.update("INSERT INTO mate_conversation(id,conversation_id,username,workspace_id,agent_id,model_provider,model_name,create_time,update_time,deleted) VALUES (?,?,?,1,?,'dashscope','json-http-fixture',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,0)", + IdWorker.getId(), foreignConversation, username, agentId); + var foreignCreate = new GoalCreateRequest(); foreignCreate.setConversationId(foreignConversation); + foreignCreate.setAgentId(agentId); foreignCreate.setWorkspaceId(1L); + foreignCreate.setTitle("Other selected Goal"); foreignCreate.setDescription("Separate conversation"); + GoalEntity foreignGoal = goals.create(foreignCreate, username); + var queuedInput = new vip.mate.channel.web.ConversationInputQueueStore(jdbc, json).enqueue( + conversation, agentId, username, "Do work for the other Goal", List.of(), userId, + foreignGoal.getId(), java.time.LocalDateTime.now()); + + SegmentOutcome outcome = runner.run(run, message, false); + + assertInstanceOf(SegmentOutcome.Continue.class, outcome); + assertEquals(0, calls.get(), "The foreign queued selection must not start a model call"); + assertEquals(GoalStatus.ACTIVE, goals.getById(goal.getId()).getStatus()); + assertEquals("consumed", jdbc.queryForObject( + "SELECT state FROM mate_conversation_input_queue WHERE id=?", String.class, queuedInput.id())); + assertEquals(1, jdbc.queryForObject("SELECT COUNT(*) FROM mate_message WHERE conversation_id=? AND role='user' AND content=?", + Integer.class, conversation, "Do work for the other Goal")); + assertEquals(1, jdbc.queryForObject("SELECT COUNT(*) FROM mate_message WHERE conversation_id=? AND role='assistant' AND content LIKE ?", + Integer.class, conversation, "%was not run because its selected Goal%")); + assertEquals(0, jdbc.queryForObject("SELECT COUNT(*) FROM mate_goal_json_artifact WHERE goal_id=?", + Integer.class, goal.getId())); + assertTrue(coordinator.settle(run, outcome, java.time.LocalDateTime.now())); + return; + } Long queuedInputId = null; if (entry.equals("scheduled-queued")) { var queuedInput = new vip.mate.channel.web.ConversationInputQueueStore(jdbc, json).enqueue(