@mogragsigma/dsh-dual-host-plugin 0.33.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +68 -99
  2. package/bundle/README.md +68 -99
  3. package/bundle/SETUP.md +87 -0
  4. package/bundle/approval-chat.js +2 -2
  5. package/bundle/approval-feishu.js +4 -1
  6. package/bundle/approval-ledger.js +30 -13
  7. package/bundle/approval-workbench.js +2 -0
  8. package/bundle/artifact-media.js +2 -2
  9. package/bundle/compatibility.json +43 -3
  10. package/bundle/configuration-v2.json +15 -0
  11. package/bundle/delivery-state.js +2 -0
  12. package/bundle/development-gate.js +2 -0
  13. package/bundle/distribution-lock.json +2 -2
  14. package/bundle/gateway-maintenance.js +1 -0
  15. package/bundle/global-rules/AGENTS.md +24 -20
  16. package/bundle/index.js +135 -25
  17. package/bundle/intervention-state.js +1 -1
  18. package/bundle/link-contract.js +9 -4
  19. package/bundle/managed-closeout.js +45 -0
  20. package/bundle/managed-cloud-policy.js +19 -0
  21. package/bundle/managed-command.js +71 -0
  22. package/bundle/managed-development-contract.js +43 -0
  23. package/bundle/managed-development.js +86 -0
  24. package/bundle/managed-endpoint.js +282 -0
  25. package/bundle/managed-execution.js +207 -0
  26. package/bundle/managed-policy.js +20 -0
  27. package/bundle/managed-runtime.js +186 -0
  28. package/bundle/managed-search.js +57 -0
  29. package/bundle/managed-tools.js +17 -0
  30. package/bundle/managed-triggers.js +85 -0
  31. package/bundle/native-context.js +4 -2
  32. package/bundle/native-entry.js +6 -2
  33. package/bundle/native-human-context.js +29 -0
  34. package/bundle/native-model-choice.js +12 -0
  35. package/bundle/native-rules/continuity.md +13 -0
  36. package/bundle/native-rules/execution.md +36 -0
  37. package/bundle/native-tools.js +52 -19
  38. package/bundle/notification-artifacts.js +26 -10
  39. package/bundle/notification-endpoint.js +1 -1
  40. package/bundle/notification-feishu.js +2 -2
  41. package/bundle/notification-media-endpoint.js +4 -3
  42. package/bundle/notification-observations.js +45 -0
  43. package/bundle/onboarding-cloud-package.js +1 -1
  44. package/bundle/onboarding-cloud.js +1 -1
  45. package/bundle/onboarding-file-contract.js +3 -2
  46. package/bundle/onboarding-file-transfer.js +4 -4
  47. package/bundle/onboarding-native-probe.js +1 -1
  48. package/bundle/onboarding-runtime-activation.js +1 -1
  49. package/bundle/onboarding-runtime-extension.js +2 -2
  50. package/bundle/package-lock.json +2 -2
  51. package/bundle/package.json +6 -4
  52. package/bundle/peer-file-review.js +1 -1
  53. package/bundle/peer-files.js +8 -4
  54. package/bundle/recovery.js +1 -1
  55. package/bundle/repository-gitee.js +12 -0
  56. package/bundle/repository-managed.js +1 -0
  57. package/bundle/runtime-im-lock.json +3409 -2849
  58. package/bundle/runtime-im-package.json +5 -5
  59. package/bundle/runtime-lock.json +3409 -2849
  60. package/bundle/runtime-package.json +5 -5
  61. package/bundle/schedule-ledger.js +35 -0
  62. package/bundle/search-contract.js +11 -1
  63. package/bundle/skills/task-approval/SKILL.md +20 -1
  64. package/bundle/skills/task-development/SKILL.md +30 -13
  65. package/bundle/skills/task-direct-link/SKILL.md +39 -13
  66. package/bundle/skills/task-file-transfer/SKILL.md +2 -2
  67. package/bundle/skills/task-notify/SKILL.md +2 -0
  68. package/bundle/skills/task-repository/SKILL.md +10 -1
  69. package/bundle/skills/task-research/SKILL.md +17 -2
  70. package/bundle/source-assessment.js +38 -0
  71. package/bundle/source-gitee.js +52 -0
  72. package/bundle/source-intake.js +137 -0
  73. package/bundle/source-policy-envelope.js +29 -0
  74. package/bundle/source-routing-tools.js +12 -0
  75. package/bundle/source-routing.js +218 -0
  76. package/bundle/source-search.js +2 -1
  77. package/bundle/store.js +6 -5
  78. package/bundle/task-artifacts.js +51 -12
  79. package/bundle/task-board.js +8 -4
  80. package/bundle/task-catalog.js +4 -3
  81. package/bundle/task-context.js +2 -2
  82. package/bundle/task-continuity-tools.js +14 -0
  83. package/bundle/task-continuity.js +248 -0
  84. package/bundle/task-envelope.js +31 -20
  85. package/bundle/task-notifications.js +6 -2
  86. package/bundle/task-observer-model.js +25 -0
  87. package/bundle/task-observer.js +120 -0
  88. package/bundle/task-results.js +1 -1
  89. package/bundle/task-scheduler.js +3 -3
  90. package/bundle/task-trigger-time.js +17 -12
  91. package/bundle/task-triggers.js +5 -30
  92. package/bundle/task-worker.js +10 -2
  93. package/bundle/task-workflows.js +2 -1
  94. package/bundle/workbench-client.js +165 -30
  95. package/bundle/workbench-decisions.js +22 -5
  96. package/bundle/worker-tools.js +8 -1
  97. package/compatibility.json +43 -3
  98. package/package.json +3 -3
@@ -8,6 +8,8 @@ export class WorkbenchApprovals {
8
8
  if(!object(args)||Object.keys(args).some(k=>!['task_id','request_id','expected_revision'].includes(k)))throw Error('APPROVAL_WORKBENCH_ARGUMENTS');
9
9
  const scoped=Object.keys(args).length>0;
10
10
  if(scoped&&(!(typeof args.task_id==='string'&&args.task_id.length<=128&&args.task_id)||!(typeof args.request_id==='string'&&args.request_id.length<=128&&args.request_id)||!Number.isSafeInteger(args.expected_revision)||args.expected_revision<1))throw Error('APPROVAL_WORKBENCH_ARGUMENTS');
11
+ if(scoped){const row=this.db.prepare('SELECT task_id,spec_revision FROM approval_requests WHERE id=?').get(args.request_id),head=this.db.prepare('SELECT spec_revision FROM task_catalog WHERE task_id=?').get(args.task_id);if(!row||row.task_id!==args.task_id||row.spec_revision!==args.expected_revision||head?.spec_revision!==args.expected_revision)throw Error('APPROVAL_WORKBENCH_CHANGED');}
12
+ if(scoped&&this.l.isNativeTask(args.task_id)){const r=this.l.view(args.request_id);if(r.task_id!==args.task_id||r.spec_revision!==args.expected_revision||this.l.catalog.head(args.task_id).spec_revision!==args.expected_revision)throw Error('APPROVAL_WORKBENCH_CHANGED');return {schema:2,enabled:true,identity_policy:'task-native',registration_required:false,new_browser_credential_required:false,approval_granted:false,execution_granted:false,read_only:true,next_action:{kind:'use_task_workbench',label:'在任务内处理',message:'通过当前已登录工作台或原生对话说明决定,无需 IM 身份确认。'}};}
11
13
  const chat=this.getChat(),binding=chat?.bindingHash?this.db.prepare('SELECT binding,disabled FROM approval_chat_bindings WHERE id=?').get(chat.bindingHash):null;
12
14
  const active=!!(binding?.disabled===0&&chat?.binding&&binding.binding===canonical(chat.binding)&&digest(chat.binding)===chat.bindingHash&&!chat.closed);
13
15
  const state=!chat?'not_configured':!active?'needs_review':chat.lastError?'unavailable':'configured';
@@ -25,10 +25,10 @@ export async function artifactSnapshot(g,task,id,mode){
25
25
  if(access.artifact.state!=='verified'||a.eligibility(r)!=='needs_verification'||access.artifact.sha256!==r.sha256)throw Error('NOTIFICATION_ARTIFACT_CHANGED');
26
26
  return {artifact_id:r.id,name:r.name,description:r.description,sha256:r.sha256,size:0,mode,spec_revision:r.spec_revision,mime:'text/uri-list',url:access.url};
27
27
  }
28
- const f=r.kind==='report'?a.readLocal(r):await g.repositories.provider.file(r.repository,r.path);a.assertOpen();
28
+ const f=['report','native_file'].includes(r.kind)?a.readLocal(r):await g.repositories.provider.file(r.repository,r.path);a.assertOpen();
29
29
  if(a.eligibility(r)!=='needs_verification'||f.bytes.length!==r.size||mediaHash(f.bytes)!==r.sha256||r.kind==='gitee'&&f.blob_sha!==r.blob_sha)throw Error('NOTIFICATION_ARTIFACT_CHANGED');
30
30
  if(mode==='page'&&r.kind!=='gitee')throw Error('NOTIFICATION_PAGE_UNAVAILABLE');
31
31
  const mime=mode==='image'?imageType(f.bytes):'application/octet-stream';
32
32
  if(mode==='image'&&!/\.(png|jpe?g)$/i.test(r.name))throw Error('ARTIFACT_INVALID_IMAGE');
33
- return {artifact_id:r.id,name:r.name,description:r.description??'暂无介绍',sha256:r.sha256,size:r.size,mode,spec_revision:r.spec_revision,mime,...(mode==='page'?{url:f.url}:{bytes:f.bytes})};
33
+ return {...(r.kind==='native_file'?{native_file:true}:{}),artifact_id:r.id,name:r.name,description:r.description??'暂无介绍',sha256:r.sha256,size:r.size,mode,spec_revision:r.spec_revision,mime,...(mode==='page'?{url:f.url}:{bytes:f.bytes})};
34
34
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema": 1,
3
3
  "plugin": "@mogragsigma/dsh-dual-host-plugin",
4
- "version": "0.33.4",
4
+ "version": "2.0.0",
5
5
  "publicEntry": "native-entry.js",
6
6
  "hostBaseline": "0.2.1-rc.5",
7
7
  "imOptional": {
@@ -979,8 +979,48 @@
979
979
  },
980
980
  "hostCompatibilityPolicy": {
981
981
  "declaredRange": ">=0.2.1-rc.5 <0.2.2-0 || >=0.2.2-rc.1 <0.2.3-0",
982
- "standalonePinnedHost": "0.2.1-rc.5",
982
+ "standalonePinnedHost": "0.2.2-rc.1",
983
983
  "targetHost": "0.2.2-rc.1",
984
- "scope": "R1-P0 Host compatibility only; range admission is not per-version runtime acceptance. No R1 reception core or production rollout."
984
+ "scope": "2.0.0 preparation, not published. Broad range admission is not per-version acceptance; see releaseContract and current acceptance report."
985
+ },
986
+ "releaseContract": {
987
+ "version": "2.0.0",
988
+ "stage": "unreleased-preparation",
989
+ "historicalFields": "Legacy approval/development/onboarding contracts remain for historical compatibility; they are not new-task policy or proof of 2.0 activation.",
990
+ "policy": "cloud-autonomous-v1",
991
+ "owner": "single-user",
992
+ "channels": [
993
+ "local-dsh-to-cloud",
994
+ "owner-local-im-to-cloud",
995
+ "owner-cloud-im-to-cloud"
996
+ ],
997
+ "execution": "cloud-only native DSH",
998
+ "taskImPermissions": "independent",
999
+ "modelCallLimit": null,
1000
+ "uniformTaskDeadline": null,
1001
+ "permissionReapprovalTTL": null,
1002
+ "concurrency": {
1003
+ "ordinary": 3,
1004
+ "humanFollowup": 1
1005
+ },
1006
+ "sessions": {
1007
+ "archiveIdleDays": 7,
1008
+ "automaticDelete": false
1009
+ },
1010
+ "setup": "bundle/SETUP.md and bundle/configuration-v2.json; explicit native profile configuration",
1011
+ "source": {
1012
+ "transport": "gitee-selected-issues",
1013
+ "maxIssuesPerSource": 8,
1014
+ "defaultMode": "off",
1015
+ "newTaskTotal": "explicitly confirmed finite per-period total, never model-call cap"
1016
+ },
1017
+ "legacyWizard": "Owned read-only runtime is historical; not the native cloud execution activation path",
1018
+ "supportEvidence": "DSH 0.2.2-rc.1; 0.2.1-rc.5 lower-bound validation tracked separately; selected Feishu dsh-im 5.9.1 real business evidence; not all platforms",
1019
+ "notSupported": [
1020
+ "reverse-local-managed-worker",
1021
+ "gitee-product-pr",
1022
+ "automatic-session-delete",
1023
+ "stateless-elastic-byom"
1024
+ ]
985
1025
  }
986
1026
  }
@@ -0,0 +1,15 @@
1
+ {
2
+ "local": [
3
+ {"id":"dual-host-onboarding","config":{"role":"local","entryMode":"native","namespace":"__PAIR_NAMESPACE__","stateDir":"__STATE_DIR__","tokenFile":"__TOKEN_FILE__","port":33911,"coordinatorUrl":"http://127.0.0.1:33921","workbench":{"enabled":true},"continuity":{"enabled":true},"managedExecution":{"enabled":true}}},
4
+ {"insert":[{"id":"dual-host-native-tools","name":"@mogragsigma/dsh-dual-host-plugin/native-tools"}]}
5
+ ],
6
+ "cloud": [
7
+ {"id":"dual-host-onboarding","config":{"role":"cloud","entryMode":"native","namespace":"__PAIR_NAMESPACE__","stateDir":"__STATE_DIR__","tokenFile":"__TOKEN_FILE__","port":33921,"workbench":{"enabled":true},"continuity":{"enabled":true},"managedExecution":{"enabled":true,"endpoint":{"lockPort":33923}},"sourceIntake":{"enabled":false,"sources":[]}}},
8
+ {"insert":[{"id":"dual-host-native-tools","name":"@mogragsigma/dsh-dual-host-plugin/native-tools"}]}
9
+ ],
10
+ "selectedSource": {
11
+ "id":"selected-gitee-issues","adapterId":"gitee-issues-v1","namespace":"gitee-repository-42","revision":"1","enabled":false,
12
+ "scopes":["https://gitee.com/example/synthetic-repository"],
13
+ "gitee":{"url":"https://gitee.com/example/synthetic-repository","repository_id":42,"issues":["IEXAMPLE"]}
14
+ }
15
+ }
@@ -1,3 +1,4 @@
1
+ import {managedResourceBusy} from './managed-development-contract.js';
1
2
  // Read-only persisted projection. No Git, filesystem observation, migration,
2
3
  // approval renewal, lease acquisition or execution callback is allowed here.
3
4
  import {canonical,digest} from './approval-ledger.js';
@@ -11,6 +12,7 @@ const codes=new Set(['EFFECT_UNKNOWN','DRAIN_REQUIRED','READBACK_UNKNOWN','READB
11
12
  export function deliveryWaiting(db,r,t){
12
13
  if(r.state==='needs_review'||t.blocked)return 'delivery_review';if(r.state!=='queued')return null;
13
14
  if(t.busy)return 'task_busy';
15
+ if(managedResourceBusy(db,r.resource))return 'managed_resource_busy';
14
16
  const owner=db.prepare("SELECT state FROM delivery_actions WHERE resource=? AND state IN ('active','needs_review')").get(r.resource);
15
17
  if(owner)return owner.state==='needs_review'?'delivery_resource_review':'delivery_resource_busy';
16
18
  if(has(db,'development_requests')){
@@ -1,3 +1,4 @@
1
+ import {managedResourceBusy} from './managed-development-contract.js';
1
2
  import {randomUUID} from 'node:crypto';
2
3
  import {canonical,digest} from './approval-ledger.js';
3
4
  import {approvedDevelopment} from './development-contract.js';
@@ -58,6 +59,7 @@ export class DevelopmentGate {
58
59
  if(this.unresolved(id)||this.inflight.has(id))return 'development_draining';
59
60
  if(r.state!=='queued')return null;
60
61
  if(t.busy||this.busy(r.task_id))return 'task_busy';
62
+ if(managedResourceBusy(this.db,r.resource))return 'managed_resource_busy';
61
63
  const owner=this.db.prepare("SELECT id,state FROM development_requests WHERE resource=? AND state IN ('active','needs_review')").get(r.resource);
62
64
  if(owner)return owner.state==='needs_review'?'development_resource_review':this.unresolved(owner.id)||this.inflight.has(owner.id)?'development_resource_draining':'development_resource_busy';
63
65
  // Optional journal shares the SAME database. Cutoff merges FIFO order across
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@mogragsigma/dsh-dual-host-plugin",
3
- "version": "0.33.4",
3
+ "version": "2.0.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@mogragsigma/dsh-dual-host-plugin",
9
- "version": "0.33.4",
9
+ "version": "2.0.0",
10
10
  "license": "SEE LICENSE IN LICENSE",
11
11
  "dependencies": {
12
12
  "@js-temporal/polyfill": "0.5.1",
@@ -17,6 +17,7 @@ export function createGatewayMaintenance(g,{now=Date.now}={}){
17
17
  deliveryPreparation:g.deliveryRuntime?.preparations.size??0,
18
18
  development:g.development?.gate.inflight.size??0,developmentPeer:g.developmentPeer?.pending.size??0,
19
19
  a2a:+!!g.a2aEndpoint?.pending+ +!!g.a2aEndpoint?.active+ +!!g.a2aEndpoint?.undrained,
20
+ managedExecution:g.execution?.slots.list().length??0,managedEndpoint:(g.executionEndpoint?.active.size??0)+ +!!g.executionEndpoint?.pending,
20
21
  approvals:+!!g.chatApproval?.running,artifacts:g.artifacts?.pending.size??0};
21
22
  }
22
23
  function status(a={}){if(!exact(a,[]))fail('ARGUMENTS');ready();const c=counts(),busy=Object.values(c).some(n=>n!==0);
@@ -2,30 +2,34 @@
2
2
  仅限本项目;保留用户规则、人格、记忆及原生功能。
3
3
  本机身份:__HOST_NAME__(Host role:__HOST_ROLE__);路由以 `task_identity` 为准,非昵称。
4
4
  Host 身份不证明渠道;云端 Web 也可接收 IM。无可靠渠道元数据须说“当前渠道未确认”,不按 localhost、环境提示或第三方 ID 猜测。
5
- ### 职责与入口
6
- - 本地大肥鱼负责本机;云端大肥鱼在 ECS 受理、查询、解释结果。指定本地不改投云端;Mac 离线另联系云端,不接管旧机器人。
7
- - 双端通信走自有插件直连;IM 是入口和通知渠道,不要求机器人同群。Gitee 保存开发版本和成果,不是消息总线、权威任务状态或并发锁。
8
- - 长任务交独立 worker,入口即返回,不用 shell 查实现/配置或 sleep/轮询。task_schedule 必填 max_runs 限总批次,每批≤6次模型请求;worker 处理检查点等待。短问答保留原功能。
9
- - 入口不能代 worker 计算并冒报完成。独立会话不等于独立 OS 进程;不得声称已经实现对 root、原生 Shell 或其他插件的系统级身份隔离。
5
+ ### 当前云端自主任务优先规则
6
+ - cloud-autonomous-v1:仅本地 DSH、本地 IM、云端 IM 三通道交办,统一云端执行;本地普通 DSH 保留,不反向派本地。
7
+ - task_submit 保留必要原文,task_execute 原生持续执行;task_followup 接续指示。无审批前置、调用次数上限、统一300秒截止或两小时到期重批;保留技术超时及业务期限。
8
+ - 云端原生 CLI/文件能力充分使用;IM 权限不决定任务执行。Git/远程托管可选;按约定交付并回读,不从研发推导发布。成果宿主登记,ready非已发送/已读。
9
+ - 以下旧审批、预算、短批、本地执行及Gitee门禁仅限历史合同;不套用于新任务,不迁移、删除或自动续跑历史。
10
+
11
+ ### 历史合同职责与入口
12
+ - 双端插件直连,IM只作入口/通知,不同群接力;Gitee非消息总线/权威状态/并发锁。
13
+ - 旧task_schedule保留max_runs及每批≤6次;入口不代worker冒报完成,不shell轮询。独立会话非OS进程或系统级身份隔离。
10
14
  ### 按需加载 Skill
11
15
  回执为准。
12
16
  | 用户需求 | Skill |
13
17
  | --- | --- |
14
- | 委派、按名称查询或修改任务;选择模型 | `task-direct-link` |
15
- | 周期、未来时间、事件、等待输入、有界子任务 | `task-schedule` |
16
- | 双端自主协作与共享预算 | `task-a2a` |
18
+ | 任务与模型 | `task-direct-link` |
19
+ | 计划与信号 | `task-schedule` |
20
+ | 历史A2A | `task-a2a` |
17
21
  | IM/本地/云端三端文件收发 | `task-file-transfer` |
18
- | 方案、自然语言批准/拒绝/暂缓/修改意见、回执引用 | `task-approval` |
19
- | 仓库关联、交付差异、提交和推送 | `task-repository` |
20
- | 已获授权的受管开发、修复与测试 | `task-development` |
21
- | 来源、联网研究、搜索阶段及证据 | `task-research` |
22
- | 通知目标、未知发送核对、图片/附件与手机入口 | `task-notify` |
23
- | 成果介绍、Gitee 页面或执行会话入口 | `task-artifacts` |
24
- | 云端 Web 工作台 | `task-workbench` |
25
- | 任务、双端、计划和交付状态展示 | `task-sidebar` |
26
- | 协议兼容、状态新鲜度、队列与回执诊断 | `task-diagnostics` |
27
- | 忙闲交接与中断核对 | `task-handoff`、`task-recovery` |
28
- | 导览、路径、凭据、安装与维护恢复、原事务回执 | `task-onboarding` |
22
+ | 历史审批 | `task-approval` |
23
+ | 仓库与交付 | `task-repository` |
24
+ | 研发与验证 | `task-development` |
25
+ | 来源与搜索 | `task-research` |
26
+ | 通知与回执 | `task-notify` |
27
+ | 成果取用 | `task-artifacts` |
28
+ | 云端工作台 | `task-workbench` |
29
+ | 任务展示 | `task-sidebar` |
30
+ | 诊断 | `task-diagnostics` |
31
+ | 交接与恢复 | `task-handoff`、`task-recovery` |
32
+ | 配置与维护 | `task-onboarding` |
29
33
 
30
34
  `task-worker`、`task-a2a-worker` 只在相应独立执行会话加载,入口不冒充 worker。
31
35
  `dsh-dual-end`、`task-intake` 仅作兼容指引;旧 `/task`、`/run`、`/approve`、群内 A2A、旧消费者和补丁流程不恢复、不自动重放。保留旧会话,不擅自改模式或清空历史。
@@ -34,7 +38,7 @@ Host 身份不证明渠道;云端 Web 也可接收 IM。无可靠渠道元数
34
38
  - 按名称交办/查询,无需 ID、命令、哈希或精确 provider;歧义才文字询问。任务名非关联键,内部参数仅取真实工具回执,不编造。
35
39
  - `task_submit` 的 goal 为理解摘要,Host 保全直接用户原文和约束。仅明确引用时补必要前文,不上传整段聊天或禁转内容。补充沿用原任务并修订;超限、非文本、缺能力须说明,不截断需求。
36
40
  - 新任务或显式换模型先用 `task_model_resolve` 核对目标端目录;“ds flash”等简称唯一匹配才用返回策略。歧义用接入名称澄清(导览 clarification),默认不传 provider_hint;不凭记忆、入口清单、近似版本或降级补齐。
37
- - 本项目 ECS 新任务未指定思考程度默认 high;显式值优先,不固定模型。历史任务、已批准方案及恢复批次保持原策略;不能支持所选程度则停止说明。辅助模型与搜索须单独声明策略和预算,既有例外不推广到其他任务。
41
+ - 新任务默认模型复用云端 DSH,显式值优先;恢复不换策略、不降级。辅助搜索单独声明路由;历史预算保留,新任务只统计。
38
42
  - 澄清使用普通聊天文字,不以飞书卡片、精确拼写或 ask_user_question 作为必经入口;此规则不拦截原生权限确认、第三方工具或斜杠命令。普通选择回复不是可信审批。
39
43
 
40
44
  ### 授权与可信审批
package/bundle/index.js CHANGED
@@ -1,3 +1,14 @@
1
+ import {validateAutonomousSearch} from './search-contract.js';
2
+ import {autonomous,CLOUD_TASK_POLICY} from './managed-cloud-policy.js';
3
+ import {TaskObserver,validateObserver} from './task-observer.js';
4
+ import {ManagedExecution,executionSpec} from './managed-execution.js';
5
+ import {ManagedEndpoint,validateManagedConfig} from './managed-endpoint.js';
6
+ import {resolveManagedModel} from './managed-runtime.js';
7
+ import {realpathSync} from 'node:fs';
8
+ import {TaskContinuity,ContinuityOutbox} from './task-continuity.js';
9
+ import {SourceRouting} from './source-routing.js';
10
+ import {GiteeIssueSources} from './source-gitee.js';
11
+ import {SourceIntake,assertSourceDatabasePath} from './source-intake.js';
1
12
  import {taskResults} from './task-results.js';
2
13
  import {ApprovalRelayBroker,LocalApprovalEndpoint,configureLocalApproval} from './approval-local-relay.js';
3
14
  import {feishuNotificationTarget} from './notification-feishu.js';
@@ -15,7 +26,7 @@ import {DevelopmentRuntime} from './development-runtime.js';
15
26
  import {TaskSources} from './task-sources.js';
16
27
  import {TaskArtifacts} from './task-artifacts.js';
17
28
  import {createServer} from 'node:http';
18
- import {mkdirSync,readFileSync,appendFileSync,lstatSync} from 'node:fs';
29
+ import {mkdirSync,existsSync,readFileSync,appendFileSync,lstatSync} from 'node:fs';
19
30
  import {join} from 'node:path';
20
31
  import {timingSafeEqual,randomUUID} from 'node:crypto';
21
32
  import {DatabaseSync} from 'node:sqlite';
@@ -56,7 +67,8 @@ export class Gateway {
56
67
  this.retry={maxAttempts:16,baseMs:2000,maxMs:300000,sendTimeoutMs:15000,...(c.recovery??{})};
57
68
  for(const [k,v] of Object.entries(this.retry))if(!Number.isSafeInteger(v)||v<1||v>3600000)throw Error('INVALID_RECOVERY_CONFIG');
58
69
  if(!['local','cloud'].includes(c.role)||!c.stateDir||!c.tokenFile&&!io.secrets)throw Error('INVALID_DEPLOYMENT');
59
- validateA2AConfig(c.a2a);
70
+ validateA2AConfig(c.a2a);validateManagedConfig(c.managedExecution);validateObserver(c.observation);
71
+ if(c.observation?.enabled&&c.managedExecution?.enabled!==true)throw Error('TASK_OBSERVER_EXECUTION_REQUIRED');
60
72
  if(c.a2a?.enabled&&c.peerEnabled===false)throw Error('A2A_PEER_REQUIRED');
61
73
  if(c.legacyFeishu&&c.imDelivery)throw Error('MULTIPLE_NOTIFICATION_CONSUMERS');
62
74
  if(io.secrets)this.secrets=structuredClone(io.secrets);
@@ -65,6 +77,20 @@ export class Gateway {
65
77
  if(c.role==='cloud'&&equal(this.secrets.localToken,this.secrets.cloudToken))throw Error('DISTINCT_ROLE_TOKENS_REQUIRED');
66
78
  if(c.entryMode==='native'&&Object.keys(c.repositoryBindings??{}).length)throw Error('LEGACY_REPOSITORY_WRITER_DISABLED');
67
79
  const repositoryProvider=c.role==='cloud'?(c.repositoryAccess?.managedOnboarding===true?new ManagedGiteeReadOnly(c.repositoryAccess,io.repository):new GiteeReadOnly(c.repositoryAccess,io.repository)):null;
80
+ if(c.sourceIntake?.enabled===true)assertSourceDatabasePath(c.stateDir);
81
+ if(c.role==='cloud'&&c.continuity?.enabled!==true&&existsSync(join(c.stateDir,'coordinator/tasks.sqlite'))){
82
+ const existing=new DatabaseSync(join(c.stateDir,'coordinator/tasks.sqlite'),{readOnly:true});let managed=false;
83
+ try{managed=!!existing.prepare("SELECT 1 FROM sqlite_master WHERE name='task_continuity'").get()&&!!existing.prepare('SELECT 1 FROM task_continuity LIMIT 1').get();}finally{existing.close();}
84
+ if(managed)throw Error('TASK_CONTINUITY_REQUIRED'); // Do not disable a durable pause/stop fence by toggling configuration.
85
+ }
86
+ for(const [file,table,enabled] of [
87
+ ['coordinator/tasks.sqlite','managed_executions',c.managedExecution?.enabled===true],
88
+ ['gateway.sqlite','managed_endpoint_owner',c.managedExecution?.enabled===true&&!!c.managedExecution.endpoint],
89
+ ])if(!enabled&&existsSync(join(c.stateDir,file))){
90
+ const existing=new DatabaseSync(join(c.stateDir,file),{readOnly:true});let managed=false;
91
+ try{managed=!!existing.prepare('SELECT 1 FROM sqlite_master WHERE name=?').get(table)&&!!existing.prepare('SELECT 1 FROM '+table+' LIMIT 1').get();}finally{existing.close();}
92
+ if(managed)throw Error('TASK_EXECUTION_REQUIRED');
93
+ }
68
94
  mkdirSync(c.stateDir,{recursive:true,mode:0o700});
69
95
  this.trace=(type,data={})=>appendFileSync(join(c.stateDir,'events.jsonl'),JSON.stringify({type,...data,at:this.now()})+'\n',{mode:0o600});
70
96
  this.db=new DatabaseSync(join(c.stateDir,'gateway.sqlite'));
@@ -80,11 +106,11 @@ export class Gateway {
80
106
  this.peerLastError=null;
81
107
  try{if(c.fileTransfers?.enabled)this.files=new PeerFiles(this,c.fileTransfers);}catch(e){this.db.close();throw e;}
82
108
  this.writer=new RepositoryWriter(c.repositoryBindings??{});
83
- if(c.role==='cloud'){this.store=new Coordinator(join(c.stateDir,'coordinator'),this.writer);this.catalog=new TaskCatalog(this.store);this.repositories=new RepositoryCatalog(this.store,this.catalog,repositoryProvider,this.now,id=>this.assertTaskQuiescent(id));this.approvals=new ApprovalLedger(this.store,this.catalog,this.now,this.repositories);if(c.localApproval){this.approvalRelay=new ApprovalRelayBroker(this.store.db,c.localApproval,{now:this.now});this.chatApproval=new ChatApproval(this.approvals,{...c.localApproval,appSecretEnv:'LOCAL_RELAY_NO_SECRET',hostRole:'local',semantic:{enabled:true,...c.localApproval.semantic}},{classify:semanticClassifier(ctx,c.localApproval.semantic),source:this.approvalRelay.source()});}else if(c.chatApproval)this.chatApproval=new ChatApproval(this.approvals,c.chatApproval,{classify:semanticClassifier(ctx,c.chatApproval.semantic),...io.chatApproval});else this.store.db.prepare('UPDATE approval_chat_bindings SET disabled=1').run();this.approvalWindow=new ApprovalWindow();this.workbenchApprovals=new WorkbenchApprovals(this.approvals,()=>this.chatApproval);this.workbenchDecisions=new WorkbenchDecisions(this.approvals,()=>this.chatApproval,{hold:()=>this.maintenance.enter('workbench_interaction')});}
84
- if(c.role==='cloud'&&c.scheduler)this.scheduler=new TaskScheduler(ctx,{...c.scheduler,canTick:()=>!this.maintenance.blocked,stateDir:join(c.stateDir,'scheduler'),validateSchedule:s=>this.assertScheduleCurrent(s),admit:job=>{if(this.store.task(job.task_id).blocked)throw Error('TASK_BLOCKED');const e=parseEnvelope(job.goal),h=this.catalog.head(job.task_id);if(e&&(e.task_context?.spec_revision??1)!==h.spec_revision)throw Error('TASK_REVISION_CONFLICT');this.sources?.searcher.admit(job);}},io.scheduler);
109
+ if(c.role==='cloud'){this.store=new Coordinator(join(c.stateDir,'coordinator'),this.writer);this.catalog=new TaskCatalog(this.store);this.repositories=new RepositoryCatalog(this.store,this.catalog,repositoryProvider,this.now,id=>this.assertTaskQuiescent(id));this.approvals=new ApprovalLedger(this.store,this.catalog,this.now,this.repositories);if(c.localApproval){this.approvalRelay=new ApprovalRelayBroker(this.store.db,c.localApproval,{now:this.now});this.chatApproval=new ChatApproval(this.approvals,{...c.localApproval,appSecretEnv:'LOCAL_RELAY_NO_SECRET',hostRole:'local',semantic:{enabled:true,...c.localApproval.semantic}},{classify:semanticClassifier(ctx,c.localApproval.semantic),source:this.approvalRelay.source()});}else if(c.chatApproval)this.chatApproval=new ChatApproval(this.approvals,c.chatApproval,{classify:semanticClassifier(ctx,c.chatApproval.semantic),...io.chatApproval});else this.store.db.prepare('UPDATE approval_chat_bindings SET disabled=1').run();this.approvalWindow=new ApprovalWindow();this.workbenchApprovals=new WorkbenchApprovals(this.approvals,()=>this.chatApproval);this.workbenchDecisions=new WorkbenchDecisions(this.approvals,()=>this.chatApproval,{hold:()=>this.maintenance.enter('workbench_interaction'),nativeDecision:c.continuity?.enabled===true?(node,a)=>this.continuity.decide(node,a):undefined});}
110
+ if(c.role==='cloud'&&c.scheduler)this.scheduler=new TaskScheduler(ctx,{...c.scheduler,canTick:()=>!this.maintenance.blocked,stateDir:join(c.stateDir,'scheduler'),validateSchedule:s=>this.assertScheduleCurrent(s),admit:job=>{this.continuity?.assertActive(job.task_id);if(this.store.task(job.task_id).blocked)throw Error('TASK_BLOCKED');const e=parseEnvelope(job.goal),h=this.catalog.head(job.task_id);if(e&&(e.task_context?.spec_revision??1)!==h.spec_revision)throw Error('TASK_REVISION_CONFLICT');this.sources?.searcher.admit(job);}},io.scheduler);
85
111
  if(this.store){this.sources=new TaskSources(this,c.readonlySources??{});this.artifacts=new TaskArtifacts(this);if(this.scheduler?.worker){this.scheduler.worker.artifacts=this.artifacts;this.scheduler.worker.sources=this.sources;}}
86
112
  if(c.development?.enabled){this.development=new DevelopmentRuntime(this,c.development);this.scheduler.executors.set('development',{replaySafe:false,run:(j,s)=>this.development.run(j,s)});this.scheduler.c.canClaim=j=>j.kind==='development'?this.development.canClaim(j):!this.store.task(j.task_id).busy;this.scheduler.c.afterRecover=()=>this.development.recover();this.scheduler.c.afterFinish=j=>{if(j.kind==='development')this.development.finished(j);};this.scheduler.c.afterFailure=j=>{if(j.kind==='development')this.development.failed(j);};}
87
- if(this.scheduler){const admit=this.scheduler.c.canClaim;this.scheduler.c.canClaim=j=>!this.closed&&(admit?.(j)??true);}
113
+ if(this.scheduler){const admit=this.scheduler.c.canClaim;this.scheduler.c.canClaim=j=>!this.closed&&!this.execution?.owns(j.task_id)&&(this.continuity?.canRun(j.task_id)??true)&&(admit?.(j)??true);}
88
114
  if(this.store){this.delivery=new DeliveryPreview(this);this.approvals.delivery=this.delivery;}
89
115
  if(c.role==='cloud'&&c.delivery?.enabled===true)this.deliveryRuntime=new DeliveryRuntime(this,c.delivery,io.delivery);
90
116
  this.notifications=new TaskNotifications(this,c.notificationTargets??[]);
@@ -100,10 +126,27 @@ export class Gateway {
100
126
  this.trace('model-request',{agent:agent.id,provider:result.provider,model:result.model});
101
127
  return {...result,maxTokens:2048};
102
128
  },{prepend:true}); // Validate the final UI selection, even after a gateway hot reload.
129
+ if(c.continuity?.enabled===true){
130
+ this.continuityOutbox=new ContinuityOutbox(this);
131
+ if(this.store)this.continuity=new TaskContinuity(this);
132
+ if(this.scheduler){this.scheduler.worker.continuity=this.continuity;this.scheduler.c.beforeTick=()=>this.continuity.reconcileAll();this.scheduler.c.taskCanRun=id=>this.continuity.canRun(id)&&!this.execution?.owns(id);}
133
+ }
134
+ if(c.managedExecution?.enabled===true){
135
+ if(c.continuity?.enabled!==true)throw Error('TASK_CONTINUITY_REQUIRED');
136
+ if(this.store)this.execution=new ManagedExecution(this);
137
+ if(c.role==='cloud'&&c.managedExecution.endpoint)this.executionEndpoint=new ManagedEndpoint(this,c.managedExecution.endpoint,io.managedExecution);
138
+ }
139
+ if(c.observation?.enabled&&this.store)this.observer=new TaskObserver(this,c.observation);
140
+ this.sourceIntake=new SourceIntake(this.db,{enabled:()=>c.sourceIntake?.enabled===true,
141
+ resolveSource:id=>{const sources=c.sourceIntake?.sources;if(!Array.isArray(sources)||sources.length>128)return null;const found=sources.filter(s=>s?.id===id);return found.length===1?(this.sourceRouting?.config(found[0])??found[0]):null;},
142
+ now:this.now,run:(label,fn)=>this.maintenance.run(label,fn)});
143
+ if(this.store&&this.execution){this.sourceRouting=new SourceRouting(this,io.sourceRouting);this.sourceTransports=new GiteeIssueSources(this);}
103
144
  ctx.effect(()=>()=>this.close());
104
145
  }
105
146
  isManaged(agent){return this.c.entryMode!=='native'&&!!agent&&this.ctx.agentPresets.composedPreset(agent.ctx)==='dsh-task-intake';}
106
- assertNativeAgent(agent){if(this.ctx.agents.get(agent?.id)!==agent||!['standard','ptc','cordis',...(this.c.nativePresets??[])].includes(this.ctx.agentPresets.composedPreset(agent.ctx)))throw Error('UNBOUND_NATIVE_SESSION');}
147
+ // Session ownership is persisted before agents.create announces agent/created;
148
+ // its live binding is assigned only after that announcement returns.
149
+ assertNativeAgent(agent){if(this.executionEndpoint?.owned?.(agent?.id)||this.executionEndpoint?.binding(agent)||this.ctx.agents.get(agent?.id)!==agent||!['standard','ptc','cordis',...(this.c.nativePresets??[])].includes(this.ctx.agentPresets.composedPreset(agent.ctx)))throw Error('UNBOUND_NATIVE_SESSION');}
107
150
  assertAgent(agent){if(this.ctx.agents.get(agent?.id)!==agent||!this.isManaged(agent))throw Error('UNBOUND_ENTRY_SESSION');}
108
151
  assertScheduleCurrent(s){
109
152
  const h=this.catalog.head(s.task_id);if(this.store.task(s.task_id).blocked)throw Error('TASK_BLOCKED');
@@ -116,11 +159,18 @@ export class Gateway {
116
159
  const single=db.prepare('SELECT 1 FROM jobs WHERE trigger_id=?').get('task-plan:'+task_id+':'+revision);
117
160
  if(family!=='workflow'&&workflow||family!=='trigger'&&trigger||family!=='single'&&single)throw Error('SCHEDULER_DISPATCH_FAMILY_CONFLICT');
118
161
  }
119
- assertTaskQuiescent(id){if(this.deliveryRuntime?.busy(id)||this.development?.busy(id)||this.a2a?.busy(id)||this.scheduler?.workflows.list(id).some(w=>!['completed','cancelled'].includes(w.status))||this.scheduler?.triggers.list(id).some(s=>!['closed','cancelled'].includes(s.status))||this.store.task(id).busy||this.scheduler?.jobs.list(id).some(j=>!['completed','cancelled'].includes(j.status))||[...(this.scheduler?.active.values()??[])].some(r=>r.task_id===id))throw Error('TASK_NOT_QUIESCENT');}
162
+ assertTaskQuiescent(id){if(this.execution?.busy(id)||this.deliveryRuntime?.busy(id)||this.development?.busy(id)||this.a2a?.busy(id)||this.scheduler?.workflows.list(id).some(w=>!['completed','cancelled'].includes(w.status))||this.scheduler?.triggers.list(id).some(s=>!['closed','cancelled'].includes(s.status))||this.store.task(id).busy||this.scheduler?.jobs.list(id).some(j=>!['completed','cancelled'].includes(j.status))||[...(this.scheduler?.active.values()??[])].some(r=>r.task_id===id))throw Error('TASK_NOT_QUIESCENT');}
120
163
  async call(method,args,node=this.c.role){return this.maintenance.run('rpc',()=>this.callCore(method,args,node));}
121
164
  async callCore(method,args,node=this.c.role){
122
165
  await this.io.authorize?.();
123
166
  if(typeof method!=='string'||!Object.hasOwn(METHODS,method))throw Error('METHOD_DENIED');
167
+ if(this.continuity){
168
+ const dispatch=['trigger_create','workflow_create','development_schedule','workspace_development_schedule','delivery_schedule','delivery_push_schedule','a2a_create'];
169
+ if(dispatch.includes(method)&&args.task_id)this.continuity.assertActive(args.task_id);
170
+ if(method==='a2a_create'&&this.continuity.managed(args.task_id))throw Error('TASK_EXECUTION_ADAPTER_UNAVAILABLE'); // Managed native A2A is R3; old tasks retain their own contract.
171
+ if(['trigger_control','workflow_control'].includes(method)&&args.action==='resume')this.continuity.assertActive((method==='trigger_control'?(this.execution?.schedules.has(args.id)?this.execution.schedules:this.scheduler.triggers):this.scheduler.workflows).get(args.id).task_id);
172
+ if(method==='job_input')this.continuity.assertActive(this.scheduler.jobs.get(args.job_id).task_id);
173
+ }
124
174
  if(method==='onboarding_existing_repository'){
125
175
  if(!args||typeof args!=='object'||Array.isArray(args)||Object.keys(args).join()!=='url')throw Error('INVALID_ARGUMENTS');
126
176
  if(this.c.role!=='cloud'||!this.repositories?.provider?.inspect)throw Error('CAPABILITY_UNAVAILABLE');
@@ -143,14 +193,47 @@ export class Gateway {
143
193
  if(method.startsWith('workbench_')){if(!this.workbench)throw Error('CAPABILITY_UNAVAILABLE');return this.workbench.call(method,args);}
144
194
  if(method==='diagnostics')return this.diagnostics(args?.refresh!==false); // No remote arbitrary writer/admin/probe.
145
195
  if(method.startsWith('a2a_')){if(!this.c.a2a?.enabled)throw Error('CAPABILITY_UNAVAILABLE');if(this.a2a)return this.a2a.call(method,args,node);}
196
+ if(this.store&&['source_routing_status','source_routing_control'].includes(method)){
197
+ if(!this.sourceRouting)throw Error('CAPABILITY_UNAVAILABLE');
198
+ return method==='source_routing_status'?this.sourceRouting.status(args):this.sourceRouting.control(node,args);
199
+ }
200
+ if(this.store&&method.startsWith('execution_')){
201
+ if(!this.execution)throw Error('CAPABILITY_UNAVAILABLE');
202
+ if(method==='execution_start'||method==='execution_prepare'&&autonomous(parseEnvelope(this.catalog.head(args.task_id).goal))){
203
+ if(args.role!==undefined&&args.role!=='cloud')throw Error('TASK_EXECUTION_CLOUD_ONLY');
204
+ if(['max_requests','active_timeout_ms','ttl_seconds','commands'].some(k=>args[k]!==undefined))throw Error('TASK_EXECUTION_RETIRED_LIMIT');
205
+ const policy=await resolveManagedModel(this.ctx,args.model_policy??parseEnvelope(this.catalog.head(args.task_id).goal)?.model_policy,this.c.managedExecution.defaultModel);
206
+ const search=validateAutonomousSearch(args.search??parseEnvelope(this.catalog.head(args.task_id).goal)?.search);
207
+ if(search&&!search.model_policy){const p=this.c.managedExecution.endpoint?.search?.model_policy;if(!p)throw Error('TASK_SEARCH_DEFAULT_UNAVAILABLE');search.model_policy=p;}
208
+ const spec=executionSpec({execution_policy:CLOUD_TASK_POLICY,role:'cloud',cwd:realpathSync(args.cwd),...(search?{search}:{}),...Object.fromEntries(['end_at','period_seconds','schedule','development'].filter(k=>args[k]!==undefined).map(k=>[k,args[k]]))});
209
+ return this.execution.start({task_id:args.task_id,expected_revision:args.expected_revision,spec,policy});
210
+ }
211
+ if(method==='execution_prepare'){
212
+ if(!this.continuity?.managed(args.task_id))throw Error('TASK_EXECUTION_NATIVE_TASK_REQUIRED');
213
+ this.continuity.assertActive(args.task_id);this.assertTaskQuiescent(args.task_id);
214
+ if(args.role==='local'&&!args.model_policy)throw Error('MODEL_TARGET_ENTRY_REQUIRED');
215
+ const policy=args.role==='cloud'?await resolveManagedModel(this.ctx,args.model_policy,this.c.managedExecution.defaultModel):args.model_policy;
216
+ this.continuity.assertActive(args.task_id);this.assertTaskQuiescent(args.task_id);
217
+ const spec=executionSpec({role:args.role,cwd:args.role==='cloud'?realpathSync(args.cwd):args.cwd,max_requests:args.max_requests??200,active_timeout_ms:args.active_timeout_ms??3600000,...(args.period_seconds?{period_seconds:args.period_seconds}:{}),...(args.search!==undefined?{search:args.search}:{}),...(args.commands!==undefined?{commands:args.commands}:{}),...(args.development!==undefined?{development:args.development}:{})});
218
+ const old=this.execution.get(args.task_id);if(old&&(old.role!==spec.role||JSON.parse(old.spec).cwd!==spec.cwd))throw Error('TASK_EXECUTION_SESSION_BINDING_CHANGED');
219
+ if(old&&spec.max_requests<this.execution.budgets.view(args.task_id).used)throw Error('TASK_RUNTIME_BUDGET_BELOW_USED');
220
+ if(typeof args.scope!=='string'||!args.scope.trim()||args.scope.length>3000)throw Error('TASK_EXECUTION_SCOPE_INVALID');
221
+ return this.approvals.request(node,{task_id:args.task_id,expected_revision:args.expected_revision,event_id:args.event_id,ttl_seconds:args.ttl_seconds??7*86400,proposal:{summary:'原生持续执行 · '+(args.role==='cloud'?'云端':'本地'),changes:args.scope+'\n执行合同:'+JSON.stringify(spec)+'\n'+(args.role==='cloud'?'云端账号文件权限;cwd是工作起点,不是文件沙箱。':'仅获准本地任务副本;权限不因云端放宽而扩大。'),validation:'核对实际成果和相关验证,报告整体进展;后台工作结束才释放名额。',risks:'仅按执行合同交付;未约定的提交/推送/PR不授权;不含合并、发布部署、历史改写、大范围不可逆删除、系统/服务/凭据变更或敏感外发。CLI仍依赖执行账号与平台权限,策略不是完整文件沙箱。',rollback:'暂停/停止会围栏新操作并排空,已经发生的文件变更不自动回滚。',model_policy:policy,scopes:['develop'],repository:null,execution:spec}});
222
+ }
223
+ return this.execution.call(method,args,node);
224
+ }
146
225
  if(this.store){
226
+ if(args.task_id&&['schedule','workflow_create','trigger_create','a2a_create','a2a_start','development_schedule','workspace_development_schedule','approval_request'].includes(method)&&autonomous(parseEnvelope(this.catalog.head(args.task_id).goal)))throw Error('TASK_EXECUTION_DIRECT_PATH_REQUIRED');
147
227
  if(method.startsWith('approval_local_')){if(!this.approvalRelay)throw Error('CAPABILITY_UNAVAILABLE');return this.approvalRelay.call(node,method.slice('approval_local_'.length),args);}
148
228
  if(method.startsWith('intervention_action_'))return this.interventionActions.call(node,method.slice('intervention_action_'.length),args);
149
229
  if(method.startsWith('approval_interaction_'))return this.workbenchDecisions.call(node,method.slice('approval_interaction_'.length),args);
150
230
  if(method.startsWith('approval_workbench_')){const service=this.workbenchApprovals;if(!service){if(method==='approval_workbench_describe')return {enabled:false,reason:'APPROVAL_WORKBENCH_DISABLED'};throw Error('APPROVAL_WORKBENCH_DISABLED');}return service.call(node,method.slice('approval_workbench_'.length),args);}
231
+ if(['development_schedule','workspace_development_schedule','delivery_prepare','delivery_schedule','delivery_push_prepare','delivery_push_schedule'].includes(method)&&this.continuity?.managed(args.task_id))throw Error('TASK_EXECUTION_ADAPTER_UNAVAILABLE');
151
232
  if(method.startsWith('development_bridge_')||method.startsWith('workspace_bridge_')){if(!this.development?.bridge)throw Error('CAPABILITY_UNAVAILABLE');return this.development.bridge.call(method,args,node);}
233
+ if(['schedule','workflow_create','trigger_create','a2a_create','development_schedule','workspace_development_schedule'].includes(method)&&args.task_id&&this.execution?.owns(args.task_id))throw Error('TASK_EXECUTION_CONTRACT_ALREADY_EXISTS');
152
234
  if(['schedule','workflow_create','trigger_create','development_schedule','workspace_development_schedule'].includes(method)&&this.a2a?.busy(args.task_id))throw Error('A2A_TASK_BUSY');
153
235
  if(method.startsWith('notification_'))return this.notifications.call(method,args,node);
236
+ if(method==='source_capabilities'&&this.c.managedExecution?.enabled){await this.executionEndpoint?.search?.ready;return {schema:1,host:'cloud',execution_policy:CLOUD_TASK_POLICY,scope:'host-account and user task restrictions; cwd is not a sandbox',default_model:this.c.managedExecution.defaultModel??null,capabilities:{files:{mode:'native-host-account'},search:{enabled:!!this.executionEndpoint?.search?.reader,configured:!!this.c.managedExecution.endpoint?.search,model_policy:this.c.managedExecution.endpoint?.search?.model_policy??null,max_requests:null}},grant:false};}
154
237
  if(method==='source_capabilities')return {schema:1,host:'cloud',capabilities:this.sources.capabilities(args),worker_enabled:this.scheduler?.started===true&&!this.scheduler.closed,scope:'explicit current task references intersect operator read roots',grant:false,model_policy:'per-task; inventory never selects or validates a model'};
155
238
  if(method==='delivery_push_prepare'||method==='delivery_push_schedule'){if(!this.deliveryRuntime)throw Error('CAPABILITY_UNAVAILABLE');return method==='delivery_push_prepare'?this.deliveryRuntime.stagePush(node,args):this.deliveryRuntime.schedulePush(node,args);}
156
239
  if(method==='delivery_prepare'||method==='delivery_schedule'){if(!this.deliveryRuntime)throw Error('CAPABILITY_UNAVAILABLE');return method==='delivery_prepare'?this.deliveryRuntime.stage(node,args):this.deliveryRuntime.schedule(node,args);}
@@ -190,9 +273,11 @@ export class Gateway {
190
273
  return workflows.create({task_id:task.id,revision:h.spec_revision,goal:h.goal,policy:args.policy,nodes:args.nodes,max_total_runs:args.max_total_runs,end_at:args.end_at,event_id:args.event_id,source});
191
274
  }
192
275
  if(method.startsWith('trigger_')){
193
- if(!this.scheduler)throw Error('SCHEDULER_UNAVAILABLE');
194
- const triggers=this.scheduler.triggers;
195
- if(method==='trigger_list'){this.store.task(args.task_id);return {items:triggers.list(args.task_id),authority:'cloud',development:false};}
276
+ if(!this.scheduler&&!this.execution?.schedules)throw Error('SCHEDULER_UNAVAILABLE');
277
+ const managed=this.execution?.schedules;const triggers=args.id&&managed?.has(args.id)?managed:this.scheduler?.triggers;
278
+ if(method==='trigger_list'){this.store.task(args.task_id);return {items:[...(this.scheduler?.triggers.list(args.task_id)??[]),...(managed?.list(args.task_id)??[])],authority:'cloud',development:false};}
279
+ if(!triggers)throw Error('SCHEDULER_NOT_FOUND');
280
+ if(method==='trigger_control'&&triggers===managed)throw Error('SCHEDULER_USE_TASK_LIFECYCLE');
196
281
  if(method==='trigger_control')return triggers.control({...args,actor:'authenticated-'+node+'-entry'});
197
282
  const source=validSources(args.sources);if(!source.length||source.some(s=>s.role!==node))throw Error('CONTEXT_ORIGIN_MISMATCH');
198
283
  if(method==='trigger_signal'){
@@ -215,17 +300,28 @@ export class Gateway {
215
300
  if(method==='repository_bind'||method==='repository_unbind')return this.repositories.mutate(node,args,method==='repository_unbind');
216
301
  if(method==='repository_status')return args.refresh?this.repositories.refresh(args.task_id):this.repositories.status(args.task_id);
217
302
  if(method==='repository_gate')return this.repositories.gate(args.task_id,args,this.approvals);
218
- if(method==='approval_request'){const r=this.approvals.request(node,args);return {...r,chat_confirmation:this.chatApproval?.queue(r.id)??null,confirmation_url:null};}
303
+ if(method==='approval_request'){const r=this.approvals.request(node,args);return {...r,chat_confirmation:this.approvals.isNativeTask(r.task_id)?null:this.chatApproval?.queue(r.id)??null,confirmation_url:null};}
219
304
  if(method==='approval_reference')return approvalReference(this,args);
220
- if(method==='approval_status')return {items:this.approvals.list(args.task_id).map(r=>({...r,chat_confirmation:this.chatApproval?.delivery(r.id)??null})),confirmation_url:null,execution:this.development?'requires_runtime_gate':'disabled'};
305
+ if(method==='approval_status')return {items:this.approvals.list(args.task_id).map(r=>({...r,chat_confirmation:this.approvals.isNativeTask(r.task_id)?null:this.chatApproval?.delivery(r.id)??null})),confirmation_url:null,execution:this.development?'requires_runtime_gate':'disabled'};
306
+ if(METHODS[method]==='continuity'){
307
+ if(!this.continuity)throw Error('CAPABILITY_UNAVAILABLE');
308
+ if(method==='task_decide')return this.continuity.decide(node,args);
309
+ if(method==='task_resolve')return this.continuity.resolve(args);
310
+ if(method==='task_followups')return this.continuity.read(args.task_id,args);
311
+ if(method==='task_followup')return this.continuity.receive(node,args);
312
+ if(method==='task_lifecycle')return this.continuity.control(node,args);
313
+ if(method==='task_followup_reroute')return this.continuity.reroute(node,args);
314
+ return this.continuity.unlink(node,args);
315
+ }
221
316
  if(method==='catalog'){const r=this.catalog.find(args);return {...r,items:r.items.map(t=>({...t,jobs:this.scheduler?.jobs.list(t.task_id).slice(-5)??[]})),authority:'cloud'};}
222
- if(method==='task_amend')return this.catalog.amend(node,args,id=>this.assertTaskQuiescent(id));
223
- if(method==='status')return {...this.store.status(args.task_id),catalog:this.catalog.view(args.task_id),repository:this.repositories.status(args.task_id),approvals:this.approvals.list(args.task_id),...(this.scheduler?{jobs:this.scheduler.jobs.list(args.task_id),schedules:this.scheduler.triggers.list(args.task_id),workflows:this.scheduler.workflows.list(args.task_id)}:{})};
317
+ if(method==='task_amend'){if(this.continuity&&['stopped','resuming'].includes(this.continuity.state(args.task_id).state))throw Error('TASK_CONTROL_NOT_ALLOWED');return this.catalog.amend(node,args,id=>this.assertTaskQuiescent(id));}
318
+ if(method==='status')return {...this.store.status(args.task_id),...(this.execution?{managed_execution:this.execution.view(args.task_id),...(this.observer?{task_observation:this.observer.view(args.task_id)}:{})}:{}),...(this.continuity?{continuity:this.continuity.controlStatus(args.task_id)}:{}),catalog:this.catalog.view(args.task_id),repository:this.repositories.status(args.task_id),approvals:this.approvals.list(args.task_id),...(this.scheduler?{jobs:this.scheduler.jobs.list(args.task_id),schedules:[...this.scheduler.triggers.list(args.task_id),...(this.execution?.schedules.list(args.task_id)??[])],workflows:this.scheduler.workflows.list(args.task_id)}:{})};
224
319
  if(method==='inspect'){const task=this.store.db.prepare('SELECT id FROM tasks WHERE task_key=?').get(args.task_key);if(!task)throw Error('TASK_NOT_FOUND');return this.call('status',{task_id:task.id},node);}
225
320
  if(method==='schedule'){
226
321
  if(!this.scheduler)throw Error('SCHEDULER_UNAVAILABLE');
227
322
  const task=args.task_id?this.store.task(args.task_id):this.store.db.prepare('SELECT * FROM tasks WHERE task_key=?').get(args.task_key);
228
- if(!task)throw Error('TASK_NOT_FOUND');if(task.blocked)throw Error('NEEDS_REVIEW');
323
+ if(!task)throw Error('TASK_NOT_FOUND');this.continuity?.assertActive(task.id);if(task.blocked)throw Error('NEEDS_REVIEW');
324
+ if(this.execution?.owns(task.id))throw Error('TASK_EXECUTION_CONTRACT_ALREADY_EXISTS');
229
325
  if(this.a2a?.busy(task.id))throw Error('A2A_TASK_BUSY');
230
326
  if(this.scheduler.triggers.list(task.id).some(s=>!['closed','cancelled'].includes(s.status)))throw Error('SCHEDULER_ALREADY_EXISTS');
231
327
  if(this.scheduler.workflows.list(task.id).some(w=>!['completed','cancelled'].includes(w.status)))throw Error('WORKFLOW_ALREADY_EXISTS');
@@ -241,10 +337,12 @@ export class Gateway {
241
337
  }
242
338
  if(method==='job_control'){
243
339
  if(!this.scheduler||!['pause','resume','cancel'].includes(args.action))throw Error('METHOD_DENIED');
340
+ if(args.action==='resume')this.continuity?.assertActive(this.scheduler.jobs.get(args.job_id).task_id);
244
341
  return this.scheduler.control({id:args.job_id,action:args.action,event_id:args.event_id,actor:'authenticated-'+node+'-entry'});
245
342
  }
246
- const incoming=parseEnvelope(args.goal);if(incoming&&incoming.origin.role!==node)throw Error('CONTEXT_ORIGIN_MISMATCH');
247
- const t=this.store.accept(node,args);
343
+ const incoming=parseEnvelope(args.goal);if(incoming?.origin.kind==='policy')throw Error('SOURCE_POLICY_INTERNAL_ONLY');if(incoming&&incoming.origin.role!==node)throw Error('CONTEXT_ORIGIN_MISMATCH');
344
+ if(incoming?.continuity===1&&!this.continuity)throw Error('CAPABILITY_UNAVAILABLE');
345
+ const t=this.store.accept(node,args,t=>{this.catalog.ensure(t.id);this.continuity?.register(t,incoming);});
248
346
  if(incoming?.notification_contract===1)this.notifications.track(t.id);
249
347
  // Status is queued intake, not a claim that a worker has run.
250
348
  const h=this.catalog.head(t.id);return {...t,title:h.title,spec_revision:h.spec_revision,execution:'pending_dispatch'};
@@ -253,6 +351,9 @@ export class Gateway {
253
351
  }
254
352
  hello(){
255
353
  const capabilities={...CAPABILITIES};
354
+ if(this.c.managedExecution?.enabled!==true)delete capabilities.source_routing;
355
+ if(this.c.continuity?.enabled!==true)delete capabilities.continuity;
356
+ if(this.c.managedExecution?.enabled!==true)delete capabilities.cloud_autonomy;
256
357
  if(this.c.role==='cloud'&&!this.repositories?.provider?.inspect)delete capabilities.onboarding_existing_repository;
257
358
  if(this.c.role==='cloud'&&!this.onboardingInspection)delete capabilities.onboarding_existing;
258
359
  if(!this.files)delete capabilities.peer_files;
@@ -267,7 +368,8 @@ export class Gateway {
267
368
  if(this.c.role==='cloud'&&!this.development?.bridge)delete capabilities.development_workspace_bridge;
268
369
  if(!this.workbench)delete capabilities.workbench;
269
370
  if(!this.board){delete capabilities.taskboard;delete capabilities.task_results;}
270
- if(this.c.role==='cloud'){if(!this.peer)delete capabilities.peer;if(!this.scheduler){delete capabilities.scheduler;delete capabilities.triggers;delete capabilities.workflows;}}
371
+ if(this.c.managedExecution?.enabled!==true){delete capabilities.managed_execution;delete capabilities.managed_development;delete capabilities.managed_triggers;}
372
+ if(this.c.role==='cloud'){if(!this.peer)delete capabilities.peer;if(!this.scheduler){delete capabilities.scheduler;if(!this.execution)delete capabilities.triggers;delete capabilities.workflows;}}
271
373
  return {schema:1,protocol:{min:1,max:1},plugin_version:VERSION,role:this.c.role,namespace:this.c.namespace,instance_id:this.instanceId,capabilities};
272
374
  }
273
375
  observation(){return {authority:this.c.role,instance_id:this.instanceId,generated_at:this.now(),freshness:'live_response',age_ms:0,usable_for_authorization:false};}
@@ -307,7 +409,7 @@ export class Gateway {
307
409
  async submitCore(args){
308
410
  await this.io.authorize?.();
309
411
  if(this.closed)throw Error('GATEWAY_CLOSED');
310
- if(typeof args.goal!=='string'||!args.goal.trim()||args.goal.length>12000||!(/^[a-z0-9-]{1,64}$/).test(args.task_key)||typeof args.ingress_id!=='string'||!args.ingress_id||args.ingress_id.length>256||!['desktop','native-local','native-cloud',...(this.legacy?['feishu-local','feishu-cloud']:[])].includes(args.source))throw Error('INVALID_SUBMISSION');
412
+ if(typeof args.goal!=='string'||!args.goal.trim()||args.goal.length>1048576||!(/^[a-z0-9-]{1,64}$/).test(args.task_key)||typeof args.ingress_id!=='string'||!args.ingress_id||args.ingress_id.length>256||!['desktop','native-local','native-cloud',...(this.legacy?['feishu-local','feishu-cloud']:[])].includes(args.source))throw Error('INVALID_SUBMISSION');
311
413
  const fp=hash(JSON.stringify(args));
312
414
  this.db.prepare("INSERT OR IGNORE INTO submissions(id,fingerprint,payload,status,created_at) VALUES (?,?,?,'queued',?)").run(args.ingress_id,fp,JSON.stringify(args),this.now());
313
415
  const row=this.db.prepare('SELECT * FROM submissions WHERE id=?').get(args.ingress_id);
@@ -360,6 +462,11 @@ export class Gateway {
360
462
  this.tickPending=this.tickOnce().finally(()=>{this.tickPending=null;});return this.tickPending;
361
463
  }
362
464
  async tickOnce(){
465
+ await this.continuityOutbox?.tick();
466
+ this.continuity?.reconcileAll();this.execution?.reconcile();
467
+ // Do not block peer controls/heartbeats behind semantic assessment.
468
+ this.sourceTransports?.tick().catch(e=>this.trace('source-transport-error',{code:errorInfo(e).code}));
469
+ this.sourceRouting?.tick().catch(e=>this.trace('source-routing-error',{code:errorInfo(e).code}));
363
470
  this.deliveryRuntime?.tick();
364
471
  this.a2a?.reap(); // Expire authority even when the recipient is offline; never dispatch here.
365
472
  if(this.scheduler)for(const n of this.scheduler.jobs.db.prepare('SELECT id,text FROM job_notifications WHERE delivered=0 LIMIT 10').all()){
@@ -376,7 +483,7 @@ export class Gateway {
376
483
  await this.notifications?.tick();
377
484
  await this.chatApproval?.tick();
378
485
  }
379
- snapshot(){return {protocol:this.hello(),connection:this.link?.snapshot()??this.inboundSnapshot(),diagnostics:this.localDiagnostics(),scheduler:this.scheduler?.snapshot()??null,version:VERSION,approval:{chat:this.chatApproval?.snapshot()??null,window:false,identity_policy:'existing-im-only',execution:'disabled'},role:this.c.role,channelStatus:this.legacy?.status()??null,legacyFeishu:!!this.legacy,imDelivery:this.imDelivery?.snapshot()??null,files:this.files?.snapshot()??null,peer:this.peer?{...this.peer.snapshot(),lastError:this.peerLastError}:null,entryMode:this.c.entryMode??'legacy-test',managedPreset:this.c.entryMode==='native'?null:'dsh-task-intake',agents:this.ctx.agents.list().map(a=>({id:a.id,status:a.status,managed:this.isManaged(a)})),submissions:this.db.prepare('SELECT id,status,task_id,attempts,next_attempt_at,last_error FROM submissions').all(),notices:this.db.prepare('SELECT id,status,message_id,last_error FROM notices').all(),receipts:this.db.prepare('SELECT id,status,message_id,last_error FROM receipts').all(),a2a:this.db.prepare('SELECT id FROM a2a').all(),repositoryBindings:this.writer.bindings.size,repositoryAssociations:this.repositories?this.store.db.prepare("SELECT COUNT(*) AS count FROM repository_associations WHERE state<>'unbound'").get().count:null,sendNotices:!!this.c.sendNotices};}
486
+ snapshot(){return {...(this.executionEndpoint?{managed_execution_endpoint:this.executionEndpoint.snapshot()}:{}),...(this.execution?{managed_execution_slots:this.execution.slots.snapshot()}:{}),protocol:this.hello(),connection:this.link?.snapshot()??this.inboundSnapshot(),diagnostics:this.localDiagnostics(),scheduler:this.scheduler?.snapshot()??null,version:VERSION,approval:{chat:this.chatApproval?.snapshot()??null,window:false,identity_policy:this.c.continuity?.enabled===true?'task-native-with-legacy-im':'existing-im-only',execution:'disabled'},role:this.c.role,channelStatus:this.legacy?.status()??null,legacyFeishu:!!this.legacy,imDelivery:this.imDelivery?.snapshot()??null,files:this.files?.snapshot()??null,peer:this.peer?{...this.peer.snapshot(),lastError:this.peerLastError}:null,entryMode:this.c.entryMode??'legacy-test',managedPreset:this.c.entryMode==='native'?null:'dsh-task-intake',agents:this.ctx.agents.list().map(a=>({id:a.id,status:a.status,managed:this.isManaged(a)})),submissions:this.db.prepare('SELECT id,status,task_id,attempts,next_attempt_at,last_error FROM submissions').all(),notices:this.db.prepare('SELECT id,status,message_id,last_error FROM notices').all(),receipts:this.db.prepare('SELECT id,status,message_id,last_error FROM receipts').all(),a2a:this.db.prepare('SELECT id FROM a2a').all(),repositoryBindings:this.writer.bindings.size,repositoryAssociations:this.repositories?this.store.db.prepare("SELECT COUNT(*) AS count FROM repository_associations WHERE state<>'unbound'").get().count:null,sendNotices:!!this.c.sendNotices};}
380
487
  async start(){
381
488
  await this.sources?.ready;
382
489
  if(this.scheduler)await this.scheduler.start();
@@ -393,11 +500,11 @@ export class Gateway {
393
500
  if(req.url==='/health'){if(!own)throw Error('AUTH_DENIED');value=this.snapshot();}
394
501
  else{
395
502
  leave=this.maintenance.enter('http');
396
- let raw='';for await(const chunk of req){raw+=chunk;if(Buffer.byteLength(raw)>131072)throw Error('BODY_TOO_LARGE');}
503
+ let raw='';for await(const chunk of req){raw+=chunk;if(Buffer.byteLength(raw)>2097152)throw Error('BODY_TOO_LARGE');}
397
504
  const body=JSON.parse(raw);if(!body.args||typeof body.args!=='object'||Array.isArray(body.args))throw Error('INVALID_ARGUMENTS');
398
505
  if(body.method==='accept'){
399
506
  // Validate peer RPC as strictly as native submissions; identity is bound to token.
400
- if(typeof body.args.goal!=='string'||!body.args.goal.trim()||body.args.goal.length>12000)throw Error('INVALID_GOAL');
507
+ if(typeof body.args.goal!=='string'||!body.args.goal.trim()||body.args.goal.length>1048576)throw Error('INVALID_GOAL');
401
508
  }
402
509
  if(body.method==='negotiate'){
403
510
  if(this.c.role!=='cloud')throw Error('PROTOCOL_ROLE_MISMATCH');
@@ -423,20 +530,23 @@ export class Gateway {
423
530
  const loop=async()=>{try{await this.tick();}catch{this.trace('tick-error');}if(!this.closed)this.timer=setTimeout(()=>{this.pending=loop();},10000);};this.pending=loop();
424
531
  if(this.c.role==='local'&&this.c.ssh){this.connector=new SshConnector(this.c.ssh,this.trace,this.io.ssh);this.connector.start();}
425
532
  if(this.a2aEndpoint)await this.a2aEndpoint.start();
426
- this.localApprovalEndpoint?.start();
533
+ if(this.executionEndpoint)await this.executionEndpoint.start();
534
+ this.localApprovalEndpoint?.start();this.observer?.start();
427
535
  this.trace('gateway-active',{role:this.c.role,port:this.c.port});
428
536
  }
429
537
  close(){
430
538
  if(this.closing)return this.closing;
431
- this.closed=true;clearTimeout(this.timer);
539
+ this.closed=true;const transportClosed=this.sourceTransports?.close(),routingClosed=this.sourceRouting?.close();this.sourceIntake.close();clearTimeout(this.timer);
432
540
  const serverClosed=this.server?.listening?new Promise(resolve=>{this.server.close(resolve);this.server.closeIdleConnections();}):Promise.resolve();
433
541
  this.closing=(async()=>{
542
+ await transportClosed;await routingClosed;await this.observer?.close();
434
543
  this.approvalRelay?.close();
435
544
  await this.localApprovalEndpoint?.close();
436
545
  await this.deliveryRuntime?.close();
437
546
  await this.developmentPeer?.close();
438
547
  await this.a2aEndpoint?.close();
439
- const settled=await Promise.allSettled([this.pending,this.tickPending,...this.deliveries.values(),this.connector?.close(),this.scheduler?.close(),this.workbenchDecisions?.close(),this.chatApproval?.close(),serverClosed]);
548
+ await this.executionEndpoint?.close();
549
+ const settled=await Promise.allSettled([this.pending,this.tickPending,...this.deliveries.values(),...(this.continuityOutbox?.pending.values()??[]),this.connector?.close(),this.scheduler?.close(),this.workbenchDecisions?.close(),this.chatApproval?.close(),serverClosed]);
440
550
  await this.notifications?.close();await this.artifacts?.close();
441
551
  this.maintenance.completeClosure(()=>{this.store?.close();this.db.close();},settled.every(x=>x.status==='fulfilled'));
442
552
  })();return this.closing;
@@ -37,7 +37,7 @@ export function interventionFacts(item,now){
37
37
  const active=out.some(x=>x.lifecycle==='open'&&!x.historical);
38
38
  // Aggregate fallback cannot hide an unsupported state behind an empty list.
39
39
  const info=guidance('task',item.display_state,'',false);
40
- if(info&&!active)add('task',{id:'state',spec_revision:revision},item.display_state);
40
+ if(info&&!active&&!item.task_observation)add('task',{id:'state',spec_revision:revision},item.display_state);
41
41
  else add('task',{id:'state',spec_revision:revision},'inactive');
42
42
  return out;
43
43
  }