@coda-console/protocol 2.6.0 → 3.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.
- package/CHANGELOG.md +37 -0
- package/README.md +38 -10
- package/docs/agent-protocol.md +26 -21
- package/docs/protocol.md +363 -0
- package/package.json +5 -4
- package/protocol.mjs +140 -0
- package/schemas/agent/wait.json +4 -0
- package/schemas/plugin/extension-points.json +2 -0
- package/schemas/plugin/plugin.json +2 -0
- package/schemas/plugin/protocol-implementations.json +10 -0
- package/schemas/plugin/protocols.json +11 -0
- package/schemas/protocol/v1/agent.json +69 -0
- package/schemas/protocol/v1/definition.json +57 -0
- package/schemas/protocol/v1/environment.json +105 -0
- package/schemas/protocol/v1/executor.json +279 -0
- package/schemas/protocol/v1/request.json +29 -0
- package/schemas/protocol/v1/response.json +25 -0
- package/schemas/protocol/v1/run.json +75 -0
- package/schemas/protocol/v1/session.json +206 -0
- package/validate.mjs +4 -1
- package/workflow/actions.json +0 -2
- package/workflow/moddle.json +1 -2
- package/workflow/values.json +2 -2
- package/docs/executor-protocol.md +0 -261
- package/executor.mjs +0 -21
- package/schemas/executor/v1/common.json +0 -89
- package/schemas/executor/v1/execution.json +0 -246
- package/schemas/executor/v1/executor.json +0 -98
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "coda/session@1",
|
|
3
|
+
"title": "会话",
|
|
4
|
+
"description": "插件提供的外部会话:参与者、消息与发消息。Provider 每追加一条消息就推进 status.head,观察方 watch 记录后用 read 读游标之后的消息;撤回、编辑、表情回应都是一条用 about 指向旧消息的新消息。",
|
|
5
|
+
"spec": {
|
|
6
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"title": {"type": "string", "maxLength": 200, "description": "给人看的名字"},
|
|
10
|
+
"container": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"description": "Provider 给的容器,例如插件登录的微信号;列表按 provider 与 container.id 分组",
|
|
13
|
+
"properties": {
|
|
14
|
+
"id": {"type": "string", "minLength": 1, "maxLength": 2048},
|
|
15
|
+
"title": {"type": "string", "maxLength": 200}
|
|
16
|
+
},
|
|
17
|
+
"required": ["id", "title"],
|
|
18
|
+
"additionalProperties": false
|
|
19
|
+
},
|
|
20
|
+
"environment": {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"description": "外部会话自带的环境",
|
|
23
|
+
"properties": {
|
|
24
|
+
"kind": {"const": "coda/environment@1"},
|
|
25
|
+
"id": {"type": "string", "minLength": 1, "maxLength": 2048}
|
|
26
|
+
},
|
|
27
|
+
"required": ["kind", "id"],
|
|
28
|
+
"additionalProperties": false
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"required": ["title", "container"],
|
|
32
|
+
"additionalProperties": false
|
|
33
|
+
},
|
|
34
|
+
"status": {
|
|
35
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": {
|
|
38
|
+
"head": {"type": "string", "minLength": 1, "maxLength": 2048, "description": "最新一条消息的游标,不透明;Provider 每追加一条就写,还没有消息时不写"},
|
|
39
|
+
"participants": {
|
|
40
|
+
"type": "array",
|
|
41
|
+
"items": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"properties": {
|
|
44
|
+
"id": {"type": "string", "minLength": 1, "maxLength": 2048},
|
|
45
|
+
"name": {"type": "string", "maxLength": 200},
|
|
46
|
+
"kind": {"enum": ["person", "agent", "bot"]},
|
|
47
|
+
"self": {"const": true, "description": "这个 Provider 登录的账号,即 Coda 的 Bot"}
|
|
48
|
+
},
|
|
49
|
+
"required": ["id", "name", "kind"],
|
|
50
|
+
"additionalProperties": false
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"state": {"enum": ["active", "unreachable"]},
|
|
54
|
+
"reason": {"type": "string", "maxLength": 4096, "description": "状态说明,可选"}
|
|
55
|
+
},
|
|
56
|
+
"required": ["participants"],
|
|
57
|
+
"additionalProperties": false
|
|
58
|
+
},
|
|
59
|
+
"operations": {
|
|
60
|
+
"read": {
|
|
61
|
+
"description": "读消息:after、before 是游标,limit 为 1 到 100;立即结果。",
|
|
62
|
+
"safe": true,
|
|
63
|
+
"input": {
|
|
64
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
65
|
+
"type": "object",
|
|
66
|
+
"properties": {
|
|
67
|
+
"after": {"type": "string", "minLength": 1, "maxLength": 2048},
|
|
68
|
+
"before": {"type": "string", "minLength": 1, "maxLength": 2048},
|
|
69
|
+
"limit": {"type": "integer", "minimum": 1, "maximum": 100}
|
|
70
|
+
},
|
|
71
|
+
"additionalProperties": false
|
|
72
|
+
},
|
|
73
|
+
"result": {
|
|
74
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
75
|
+
"type": "object",
|
|
76
|
+
"$defs": {
|
|
77
|
+
"id": {"type": "string", "minLength": 1, "maxLength": 2048},
|
|
78
|
+
"part": {
|
|
79
|
+
"oneOf": [
|
|
80
|
+
{
|
|
81
|
+
"type": "object",
|
|
82
|
+
"properties": {"type": {"const": "text"}, "text": {"type": "string"}},
|
|
83
|
+
"required": ["type", "text"],
|
|
84
|
+
"additionalProperties": false
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"type": "object",
|
|
88
|
+
"properties": {
|
|
89
|
+
"type": {"enum": ["image", "video", "file"]},
|
|
90
|
+
"name": {"type": "string", "maxLength": 1024},
|
|
91
|
+
"size": {"type": "integer", "minimum": 0},
|
|
92
|
+
"mime": {"type": "string", "maxLength": 255},
|
|
93
|
+
"service": {"type": "string", "minLength": 1, "maxLength": 200, "description": "插件登记的流式服务 ID"}
|
|
94
|
+
},
|
|
95
|
+
"required": ["type", "name"],
|
|
96
|
+
"additionalProperties": false
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"type": "object",
|
|
100
|
+
"properties": {"type": {"enum": ["thinking", "tool"]}, "text": {"type": "string"}},
|
|
101
|
+
"required": ["type", "text"],
|
|
102
|
+
"additionalProperties": false
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
},
|
|
106
|
+
"message": {
|
|
107
|
+
"type": "object",
|
|
108
|
+
"properties": {
|
|
109
|
+
"id": {"$ref": "#/$defs/id"},
|
|
110
|
+
"cursor": {"$ref": "#/$defs/id"},
|
|
111
|
+
"sender": {"$ref": "#/$defs/id", "description": "参与者 ID"},
|
|
112
|
+
"text": {"type": "string", "description": "纯文本兜底"},
|
|
113
|
+
"parts": {"type": "array", "items": {"$ref": "#/$defs/part"}},
|
|
114
|
+
"replyTo": {"$ref": "#/$defs/id", "description": "回复的消息 ID"},
|
|
115
|
+
"mentions": {"type": "array", "items": {"$ref": "#/$defs/id"}, "description": "@ 到的参与者 ID;回复一条消息就算 @ 了它的发送者,Provider 把被回复消息的发送者也写进来"},
|
|
116
|
+
"about": {
|
|
117
|
+
"type": "object",
|
|
118
|
+
"description": "这条消息改的是哪条旧消息",
|
|
119
|
+
"properties": {
|
|
120
|
+
"message": {"$ref": "#/$defs/id"},
|
|
121
|
+
"change": {"enum": ["edit", "recall", "reaction"]},
|
|
122
|
+
"value": {"description": "编辑后的内容或表情等,可选"}
|
|
123
|
+
},
|
|
124
|
+
"required": ["message", "change"],
|
|
125
|
+
"additionalProperties": false
|
|
126
|
+
},
|
|
127
|
+
"createdAt": {"type": "string", "minLength": 1, "maxLength": 40, "description": "只供显示"}
|
|
128
|
+
},
|
|
129
|
+
"required": ["id", "cursor", "sender", "text"],
|
|
130
|
+
"additionalProperties": false
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"properties": {
|
|
134
|
+
"messages": {"type": "array", "items": {"$ref": "#/$defs/message"}},
|
|
135
|
+
"cursor": {"oneOf": [{"$ref": "#/$defs/id"}, {"type": "null"}], "description": "下一次读从哪里接着读;没有消息时为 null"},
|
|
136
|
+
"more": {"type": "boolean"}
|
|
137
|
+
},
|
|
138
|
+
"required": ["messages", "cursor", "more"],
|
|
139
|
+
"additionalProperties": false
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
"post": {
|
|
143
|
+
"description": "以 self 发一条消息;Provider 按 requestId 只发一次,立即结果。",
|
|
144
|
+
"input": {
|
|
145
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
146
|
+
"type": "object",
|
|
147
|
+
"$defs": {
|
|
148
|
+
"id": {"type": "string", "minLength": 1, "maxLength": 2048},
|
|
149
|
+
"part": {
|
|
150
|
+
"oneOf": [
|
|
151
|
+
{
|
|
152
|
+
"type": "object",
|
|
153
|
+
"properties": {"type": {"const": "text"}, "text": {"type": "string"}},
|
|
154
|
+
"required": ["type", "text"],
|
|
155
|
+
"additionalProperties": false
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"type": "object",
|
|
159
|
+
"properties": {
|
|
160
|
+
"type": {"enum": ["image", "video", "file"]},
|
|
161
|
+
"name": {"type": "string", "maxLength": 1024},
|
|
162
|
+
"size": {"type": "integer", "minimum": 0},
|
|
163
|
+
"mime": {"type": "string", "maxLength": 255},
|
|
164
|
+
"service": {"type": "string", "minLength": 1, "maxLength": 200, "description": "插件登记的流式服务 ID"}
|
|
165
|
+
},
|
|
166
|
+
"required": ["type", "name"],
|
|
167
|
+
"additionalProperties": false
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"type": "object",
|
|
171
|
+
"properties": {"type": {"enum": ["thinking", "tool"]}, "text": {"type": "string"}},
|
|
172
|
+
"required": ["type", "text"],
|
|
173
|
+
"additionalProperties": false
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
"properties": {
|
|
179
|
+
"text": {"type": "string", "description": "纯文本兜底"},
|
|
180
|
+
"parts": {"type": "array", "items": {"$ref": "#/$defs/part"}},
|
|
181
|
+
"replyTo": {"$ref": "#/$defs/id", "description": "回复的消息 ID"},
|
|
182
|
+
"mentions": {"type": "array", "items": {"$ref": "#/$defs/id"}, "description": "@ 到的参与者 ID"}
|
|
183
|
+
},
|
|
184
|
+
"required": ["text"],
|
|
185
|
+
"additionalProperties": false
|
|
186
|
+
},
|
|
187
|
+
"result": {
|
|
188
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
189
|
+
"type": "object",
|
|
190
|
+
"properties": {
|
|
191
|
+
"message": {
|
|
192
|
+
"type": "object",
|
|
193
|
+
"properties": {
|
|
194
|
+
"id": {"type": "string", "minLength": 1, "maxLength": 2048},
|
|
195
|
+
"cursor": {"type": "string", "minLength": 1, "maxLength": 2048}
|
|
196
|
+
},
|
|
197
|
+
"required": ["id", "cursor"],
|
|
198
|
+
"additionalProperties": false
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
"required": ["message"],
|
|
202
|
+
"additionalProperties": false
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
package/validate.mjs
CHANGED
|
@@ -32,6 +32,8 @@ const BASE = 'https://coda.pub/schemas/plugin/v2/';
|
|
|
32
32
|
// The schemas, by file name.
|
|
33
33
|
export const SCHEMAS = Object.fromEntries(readdirSync(SCHEMA_DIR).filter(file => file.endsWith('.json')).sort()
|
|
34
34
|
.map(file => [file, JSON.parse(readFileSync(join(SCHEMA_DIR, file), 'utf8'))]));
|
|
35
|
+
// The protocol definition schema the manifest schemas reference; it is not a contributes type of its own.
|
|
36
|
+
const PROTOCOL_DEFINITION = JSON.parse(readFileSync(new URL('./schemas/protocol/v1/definition.json', import.meta.url), 'utf8'));
|
|
35
37
|
// The types a plugin may contribute to Coda itself, as the manifest's schema names them.
|
|
36
38
|
export const TYPES = Object.keys(SCHEMAS['plugin.json'].properties.contributes.properties);
|
|
37
39
|
|
|
@@ -193,6 +195,7 @@ export function newAjv(options = {}) {
|
|
|
193
195
|
return made;
|
|
194
196
|
}
|
|
195
197
|
const ajv = newAjv();
|
|
198
|
+
ajv.addSchema(PROTOCOL_DEFINITION);
|
|
196
199
|
for (const schema of Object.values(SCHEMAS)) ajv.addSchema(schema);
|
|
197
200
|
// The validators of the schemas here, by file.
|
|
198
201
|
const compiled = new Map();
|
|
@@ -304,7 +307,7 @@ export function explain(errors, base = '', schemas = [], { style = 'keyword', re
|
|
|
304
307
|
// A manifest checked against its schema, the files it names in `dir` when given. Returns null, or what is wrong.
|
|
305
308
|
export function manifestProblem(manifest, dir = null) {
|
|
306
309
|
const validate = validator('plugin.json');
|
|
307
|
-
return validate.call({ dir }, manifest) ? null : explain(validate.errors);
|
|
310
|
+
return validate.call({ dir }, manifest) ? null : explain(validate.errors, '', [PROTOCOL_DEFINITION]);
|
|
308
311
|
}
|
|
309
312
|
// A value checked against one of the schemas (`ref`: a file, with a fragment, as `composer-modes.json#/$defs/parameters`),
|
|
310
313
|
// as a plugin gives it while it runs: null, or what is wrong, at `where`.
|
package/workflow/actions.json
CHANGED
|
@@ -7,8 +7,6 @@
|
|
|
7
7
|
"description": "整个移除运行主体的会话的工作树,分支保留提交;result 是 removed、absent、in-use、busy 或 failed"},
|
|
8
8
|
"stop-process": {"params": [],
|
|
9
9
|
"description": "停掉运行主体的会话托管的 Agent 进程,下次发消息时透明恢复;result 是 stopped、absent 或 busy"},
|
|
10
|
-
"hand-to-user": {"params": [],
|
|
11
|
-
"description": "把运行主体的会话的 Agent 已被告知、还没答复的流程任务都转给用户"},
|
|
12
10
|
"compact-session": {"params": [],
|
|
13
11
|
"description": "请运行主体的会话的 Agent 压缩上下文(_coda/session/compact,见 docs/agent-protocol.md),从不失败:没压缩就写明原因"},
|
|
14
12
|
"hold-group": {"params": ["group", "capacity", "borrow", "exclusive"],
|
package/workflow/moddle.json
CHANGED
|
@@ -108,10 +108,9 @@
|
|
|
108
108
|
"name": "UserTaskProperties",
|
|
109
109
|
"isAbstract": true,
|
|
110
110
|
"extends": ["bpmn:UserTask"],
|
|
111
|
-
"description": "人工任务的:assignee
|
|
111
|
+
"description": "人工任务的:assignee 交给谁,三个按钮的名字",
|
|
112
112
|
"properties": [
|
|
113
113
|
{"name": "assignee", "isAttr": true, "type": "String"},
|
|
114
|
-
{"name": "handback", "isAttr": true, "type": "Boolean"},
|
|
115
114
|
{"name": "completeLabel", "isAttr": true, "type": "String"},
|
|
116
115
|
{"name": "rejectLabel", "isAttr": true, "type": "String"},
|
|
117
116
|
{"name": "noteLabel", "isAttr": true, "type": "String"}
|
package/workflow/values.json
CHANGED
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"UserTaskProperties": {
|
|
82
82
|
"type": "object",
|
|
83
83
|
"properties": {
|
|
84
|
-
"assignee": {"enum": ["agent", "user"], "description": "coda:assignee 只能是 agent
|
|
84
|
+
"assignee": {"enum": ["agent", "user"], "description": "coda:assignee 只能是 agent(先给运行所属会话的 Agent 一轮,没答就逐层往上,最上面是用户;有输入字段的直接到用户)或 user(直接交给用户)"}
|
|
85
85
|
}
|
|
86
86
|
},
|
|
87
87
|
"Input": {
|
|
@@ -140,7 +140,7 @@
|
|
|
140
140
|
"Choice": {
|
|
141
141
|
"type": "object",
|
|
142
142
|
"properties": {
|
|
143
|
-
"id": {"type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_-]{0,63}$", "
|
|
143
|
+
"id": {"type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_-]{0,63}$", "description": "id 须为字母开头的字母、数字、「_」「-」"},
|
|
144
144
|
"name": {"type": "string", "pattern": "\\S", "maxLength": 40, "description": "name 须为 1–40 个字"},
|
|
145
145
|
"detail": {"type": "string", "maxLength": 200, "x-feel": true, "description": "detail 不超过 200 字"},
|
|
146
146
|
"choices": {"type": "array", "maxItems": 6, "description": "最多 6 个按住出现的选项"},
|
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
# 运行器与执行请求协议 v1
|
|
2
|
-
|
|
3
|
-
`executor`(运行器)与 `execution`(一次执行尝试)是 Coda 预定的公共协议,唯一形状定义在
|
|
4
|
-
`@coda-console/protocol`。依据是 [coda#2078 已验收草案](https://github.com/coda-console/coda/issues/2078#issuecomment-6554881338),
|
|
5
|
-
2.2.0 按[第 7 步方案](https://github.com/coda-console/coda/issues/2078#issuecomment-6559423071)补齐流程接入:
|
|
6
|
-
**无资源或一个资源的一单位**,可独占所申请资源;多单位、多资源原子申请仍未开放。
|
|
7
|
-
|
|
8
|
-
协议对象版本 `protocolVersion: 1`、本目录的 v1、npm 包版本 2.5.0 和插件清单的 v2 分开演进。
|
|
9
|
-
本次只增加契约;不改变 `contributes.executors`,也不宣称 host、scheduler、空间引用解析或账本接口已经实现。
|
|
10
|
-
|
|
11
|
-
## 定义与登记
|
|
12
|
-
|
|
13
|
-
`schemas/executor/v1/{executor,execution}.json` 分别描述 `{spec,status}`,公共定义在 `common.json`。
|
|
14
|
-
原始 JSON 采用 draft-07,读取原文件时先向 Ajv 登记 `common.json`。模块导出把所需定义包含在每份 spec/status 中,
|
|
15
|
-
只留本地 `$ref`;四份 schema 均可由一个全新的默认 Ajv 直接编译,不需自定义关键字、format 或外部 schema 注册。
|
|
16
|
-
`spec`、`status` 顶层均为 object。不支持的 `protocolVersion`、未知字段与不合资源命名约定的键明确拒绝。
|
|
17
|
-
|
|
18
|
-
```js
|
|
19
|
-
import { EXECUTOR_PROTOCOL, EXECUTION_PROTOCOL } from '@coda-console/protocol/executor';
|
|
20
|
-
|
|
21
|
-
// 现行 API 的装配示例:由 host 在提交端和执行端登记,提交端可关闭接活。
|
|
22
|
-
await coda.protocols.define(EXECUTOR_PROTOCOL);
|
|
23
|
-
await coda.protocols.define({ ...EXECUTION_PROTOCOL, controllers: ['scheduler'] });
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
`EXECUTOR_PROTOCOL` 没有 `target`;`EXECUTION_PROTOCOL.target` 为 `executor`。
|
|
27
|
-
两种类型均**没有补充栏**,不声明 append/or 合并;任务、标签、取消和绑定均不经补充者修改。
|
|
28
|
-
`controllers: ['scheduler']` 是现行接口的插件 ID 装配,不是协议对 host/scheduler 实现路径的依赖。
|
|
29
|
-
其他生产者通过 provider 的本机创建/取消入口操作,不能冒用 `put`。
|
|
30
|
-
|
|
31
|
-
当前 `protocols.define` 只允许一个插件登记同 kind,定义持久化后不能改变;尚不支持按协议依赖、多实现登记或同 kind 版本迁移。
|
|
32
|
-
后续替换 provider 或演进声明须先解决 Coda 通用登记机制,不能仅更换 schema 文件接管既存类型。
|
|
33
|
-
|
|
34
|
-
信封沿用 `{kind,id,instance,spec,status,revision}`。基座维护 `instance` 与不透明 `revision`;插件原样传回观察到的 revision,
|
|
35
|
-
不能解析或自行递增。删除后同 id 重建是新 instance,旧回答不能复用。这里的 schema 校验 spec/status,不校验或重定义信封。
|
|
36
|
-
所有 status 都允许 `{}`,表示镜像状态尚未到达,**不能据此判定未开始**;非空 status 必须是一份完整的已知状态。
|
|
37
|
-
|
|
38
|
-
## executor:配置与本机事实
|
|
39
|
-
|
|
40
|
-
一个对象对应一个后端实例的执行环境,身份不随显示名或插件版本改变。
|
|
41
|
-
|
|
42
|
-
| 区域 | 字段 |
|
|
43
|
-
| --- | --- |
|
|
44
|
-
| spec 必填 | `protocolVersion: 1`、`enabled`、`capacity: Record<resource, 非负整数>`、`labels: string[]` |
|
|
45
|
-
| spec 可选 | `title`(不超过 200 字符) |
|
|
46
|
-
| status 能力 | `detectedLabels`、`capabilities: {os,arch,nodeVersion,graphicalSession}`;可选 `xcode: {version,build}`、`simulatorRuntimes: [{id,version,available}]` |
|
|
47
|
-
| status 资源 | `capacity`、`occupied`、`available`,各为 `Record<resource, 非负整数>`;`accepting` |
|
|
48
|
-
| status 观察 | `observedAt`、`observedRevision`;可选 `reason: {code,message}` |
|
|
49
|
-
| status 支持 | `executionFeatures: {multiResource: false,multiUnit: false,exclusive: boolean}`;可选 `noResources`、`namedResources`、`context` 布尔值 |
|
|
50
|
-
|
|
51
|
-
非空 status 除明确可选项外全部必填。`graphicalSession` 是 boolean 或 null;null 表示无法确定。
|
|
52
|
-
探测失败写 `reason`,不将失败变成用户标签或伪造能力;可选探测项未知时省略。
|
|
53
|
-
探测只读,不启动模拟器。`observedRevision` 对应探测时采用的配置版本,而非要求它等于之后每次 status 写入产生的 revision。
|
|
54
|
-
|
|
55
|
-
标签使用小写字母、数字、`.`、`_`、`-`,以字母或数字起始,每项最多 40 字符、每列表最多 64 项、不重复。
|
|
56
|
-
`requirements.labels` 沿用 runsOn 的**全部满足**语义,匹配用户标签与探测标签的并集。
|
|
57
|
-
保留 OS 标签 `darwin/linux/win32/aix/android/freebsd/openbsd/sunos/cygwin/netbsd/haiku`、架构标签
|
|
58
|
-
`arm/arm64/ia32/x64/loong64/mips/mipsel/ppc/ppc64/riscv64/s390/s390x`、`gui`,以及 `xcode-`、`ios-`、`node-` 前缀给探测结果。
|
|
59
|
-
spec.labels 只能加自定义标签,不能写这些保留值。精确版本和 simulator runtime ID 另外保存在结构化能力中。
|
|
60
|
-
|
|
61
|
-
`slots` 的单位是执行名额,host 映射到同一本机 `host` 账本;scheduler 比较资源键与数量,不硬编码 `group === 'host'`。
|
|
62
|
-
容量可大于 1;**每次请求量仍只能为 1**。其他本机命名组用 `group:<原名>`,原名为 1–100 个非空白、非控制字符;
|
|
63
|
-
`group:host` 拒绝,host 唯一映射是 slots。容量 map 可以为空或含至多 128 个独立资源,这不表示支持一个请求同时申请多个资源。
|
|
64
|
-
未报告的资源不能按无限容量或 0 占用处理。有能力不代表有名额。
|
|
65
|
-
|
|
66
|
-
`noResources`、`namedResources`、`context` 只有显式 true 才表示支持无资源请求、命名组请求、受控运行上下文;
|
|
67
|
-
省略表示未声明支持。scheduler 必须按请求用到的能力筛选,不能将旧执行器当作支持新形状。
|
|
68
|
-
命名组在本版本仅允许本机执行,由适配层判断归属;不凭带 `group:` 的字符串授权远端资源映射。
|
|
69
|
-
缩容量允许 occupied 暂时大于 capacity,此时 available 归零并停止新准入,不强杀已有任务。
|
|
70
|
-
占用含旧流程与 run-held;借用或独占按账本事实计算,不能简单数 host 自己启动的进程,也不能要求独占时 available 等于 capacity − occupied。
|
|
71
|
-
这些跨栏计算和准入状态由实现检查,不加动态 schema 关键字。
|
|
72
|
-
|
|
73
|
-
对象无 target,配置经运行器持有端更新;**status 只由持有后端的实现者写**,空间层单向复制。
|
|
74
|
-
accepting、available、observedAt 都只是最近观察事实,不保证在线,也不是预留凭证;新鲜度阈值与选择策略属于 scheduler。
|
|
75
|
-
|
|
76
|
-
```json
|
|
77
|
-
{
|
|
78
|
-
"spec": {
|
|
79
|
-
"protocolVersion": 1,
|
|
80
|
-
"enabled": true,
|
|
81
|
-
"capacity": {"slots": 2},
|
|
82
|
-
"labels": ["build-lab"],
|
|
83
|
-
"title": "Mac mini"
|
|
84
|
-
},
|
|
85
|
-
"status": {
|
|
86
|
-
"detectedLabels": ["darwin", "arm64", "xcode-26.1", "node-26"],
|
|
87
|
-
"capabilities": {"os": "darwin", "arch": "arm64", "nodeVersion": "26.8.2", "graphicalSession": null, "xcode": {"version": "26.1", "build": "17B55"}},
|
|
88
|
-
"capacity": {"slots": 2},
|
|
89
|
-
"occupied": {"slots": 1},
|
|
90
|
-
"available": {"slots": 1},
|
|
91
|
-
"accepting": true,
|
|
92
|
-
"observedAt": "2026-11-01T07:00:00Z",
|
|
93
|
-
"observedRevision": "opaque-config-revision",
|
|
94
|
-
"executionFeatures": {"multiResource": false, "multiUnit": false, "exclusive": true}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
## execution:一次尝试
|
|
100
|
-
|
|
101
|
-
正本在发起后端。`run: ResourceRef` 关联已有运行对象,`step?` 关联步骤,`attempt` 是正整数。
|
|
102
|
-
重新执行创建新 execution ID 和新 attempt,保留上一尝试,不另造 Run 或会话命令队列。取消不是新尝试。
|
|
103
|
-
|
|
104
|
-
| spec 区域 | 形状与约束 |
|
|
105
|
-
| --- | --- |
|
|
106
|
-
| 身份 | `protocolVersion: 1`、`run`、`attempt` 必填;`step` 可选 |
|
|
107
|
-
| 源码 | `source` 必填:null、`{snapshot: ResourceRef<sourceSnapshot>}`、`{repository: ResourceRef<gitSource>,commit: 完整 SHA}` 三选一;SHA 为 40 或 64 位十六进制 |
|
|
108
|
-
| 能力 | `requirements: {labels: string[]}`,可为空列表 |
|
|
109
|
-
| 资源 | `resources: {}`、`{slots: 1}` 或 `{ "group:<原名>": 1 }`;`exclusiveResources` 默认 `[]`,至多独占申请的那一个资源 |
|
|
110
|
-
| 定位(可选) | `placement: {locality?: "local"或"any", executor?: ResourceRef<executor>}`;省略 locality 等同 any |
|
|
111
|
-
| 上下文(可选) | `context: {workflow?,plugin?,audience?,outputNames?}`,见下文 |
|
|
112
|
-
| 任务 | `{script,shell}` 或 `{command,args}`,严格二选一;程序名与参数不隐含 shell |
|
|
113
|
-
| 环境 | `workingDirectory` 默认 `.`;`env: Record<string,string>` 必填,只含声明变量,可为空对象 |
|
|
114
|
-
| 时限 | `timeoutMs?` 正整数;`cancelGraceMs?` 非负整数;缺省策略由执行实现决定 |
|
|
115
|
-
| 取消 | `cancel` 默认 false,是单向意图 |
|
|
116
|
-
| 绑定 | `executor: ResourceRef<executor>\|null`、`assignmentId: string\|null` 必填,两者同为 null 或同时有值 |
|
|
117
|
-
|
|
118
|
-
独占是 `exclusiveResources: [资源键]`,而非把“全部名额”换成某次观察到的容量数字。
|
|
119
|
-
空 resources 只允许空 exclusiveResources;非空时独占键必须与请求键一致,schema 检查数量和命名,执行方核对二者关系。
|
|
120
|
-
生产端必须先看 `executionFeatures.exclusive`;不支持独占的执行方明确拒绝,不降级为普通名额。
|
|
121
|
-
原 coda:group 由生产端/资源 provider 声明映射;runsOn 放入 labels,两者独立。
|
|
122
|
-
|
|
123
|
-
workingDirectory 是源码根下的相对路径;没有源码时相对执行方隔离目录。
|
|
124
|
-
schema 拒绝绝对路径、盘符、反斜线、控制字符和 `..` 路径段;执行方仍须检查真实落点、符号链接和来源权限。
|
|
125
|
-
工作树先由提交端制成 ready 快照,执行端读取快照的精确 commit/ref,不重新抓取“最新工作树”。
|
|
126
|
-
env 不等于提交机进程环境;沿用 hook 的声明变量、密文与鉴权边界,不把本机凭据或绝对路径放进跨机 spec。
|
|
127
|
-
|
|
128
|
-
JSON Schema 的 default 只声明缺省值。单独使用默认 Ajv 校验不会填值;现行 Provider 用 `useDefaults` 补齐
|
|
129
|
-
`workingDirectory`、`exclusiveResources`、`cancel`。不使用 default 填造任何 status 事实。
|
|
130
|
-
|
|
131
|
-
| 非空 status | 约束 |
|
|
132
|
-
| --- | --- |
|
|
133
|
-
| 基本 | `phase: queued\|preparing\|running\|finished` 与 `assignmentId` 必填;可选 `reason` |
|
|
134
|
-
| 进度 | 可选 `startedAt`、`executionRun: ResourceRef`、`admission: {lease,slots}`;running 三者必填 |
|
|
135
|
-
| 准入 | 无资源请求 admission 为 null;占资源时为 `{lease,slots,resource?}`,resource 省略表示 slots。slots 是非空且不重复的非负名额序号数组,独占可记录多个序号 |
|
|
136
|
-
| 完成 | finished 必须给 `finishedAt`、`outcome: succeeded\|failed\|cancelled\|rejected`;其他 phase 不接受 finishedAt、outcome、exitCode |
|
|
137
|
-
| 结果 | 可选 `exitCode: integer\|null`、`outputs`、`summary`、`outputTail`、`logs`、`artifacts` |
|
|
138
|
-
|
|
139
|
-
preparing/running 必须有 assignmentId;succeeded/failed 还必须有 startedAt 与 assignmentId。
|
|
140
|
-
取消可在未绑定、未启动时完成;rejected 表示本次 assignment 不会启动,不接受 startedAt 或非 null exitCode。
|
|
141
|
-
没启动或无法确定退出码时省略或写 null,不能造 0。这些字段的静态组合不能代替实际进程、收尾和状态迁移检查。
|
|
142
|
-
独立 spec/status schema 不比较请求与结果:执行方必须保证 admission 为 null 当且仅当请求 resources 为空;
|
|
143
|
-
非空准入的 resource 必须等于请求资源,不能用 null、别的组的租约或虚构 lease 绕过占额。
|
|
144
|
-
|
|
145
|
-
文本限制使用 JSON Schema 的 Unicode 字符数,不是 UTF-8 字节或 UTF-16 长度:summary 最多 4096、outputTail 最多 16384;
|
|
146
|
-
outputs 最多 64 项,键 1–128 字符、值最多 4096。logs/artifacts 各最多 64 项。
|
|
147
|
-
logs 项为 `{ref: ResourceRef,cursor?}`;artifacts 项为 `{ref: ResourceRef,name,sha256?,size?,mediaType?}`。
|
|
148
|
-
引用不接受本机绝对路径代替对象;sha256 为 64 位十六进制,size 为非负安全整数。
|
|
149
|
-
任务脚本最多 65536 字符,command/shell 最多 4096,args 最多 256 项、每项最多 16384;
|
|
150
|
-
env 最多 128 项,变量名使用字母/下划线起始的常规环境变量名、最多 128 字符,值最多 16384。
|
|
151
|
-
数组、键和值都有上限,不能把无限日志复制进 status;完整限制见 schema。
|
|
152
|
-
时间统一使用 UTC `...Z`,schema 仅检查日期时间文字的形状,写入方保证日期真实、时序合理。
|
|
153
|
-
|
|
154
|
-
```json
|
|
155
|
-
{
|
|
156
|
-
"spec": {
|
|
157
|
-
"protocolVersion": 1,
|
|
158
|
-
"run": {"kind": "runs", "id": "opaque-run-ref"},
|
|
159
|
-
"step": "check",
|
|
160
|
-
"attempt": 1,
|
|
161
|
-
"source": {"snapshot": {"kind": "sourceSnapshot", "id": "opaque-snapshot-ref"}},
|
|
162
|
-
"requirements": {"labels": ["darwin", "arm64"]},
|
|
163
|
-
"resources": {"slots": 1},
|
|
164
|
-
"exclusiveResources": ["slots"],
|
|
165
|
-
"task": {"command": "npm", "args": ["test"]},
|
|
166
|
-
"workingDirectory": ".",
|
|
167
|
-
"env": {"CI": "true"},
|
|
168
|
-
"cancel": false,
|
|
169
|
-
"executor": {"kind": "executor", "id": "opaque-executor-ref"},
|
|
170
|
-
"assignmentId": "assignment-1"
|
|
171
|
-
},
|
|
172
|
-
"status": {
|
|
173
|
-
"phase": "finished",
|
|
174
|
-
"assignmentId": "assignment-1",
|
|
175
|
-
"startedAt": "2026-11-01T07:00:00Z",
|
|
176
|
-
"finishedAt": "2026-11-01T07:01:00Z",
|
|
177
|
-
"executionRun": {"kind": "runs", "id": "opaque-execution-run-ref"},
|
|
178
|
-
"admission": {"lease": "opaque-lease", "slots": [0, 1]},
|
|
179
|
-
"outcome": "succeeded",
|
|
180
|
-
"exitCode": 0,
|
|
181
|
-
"summary": "检查通过",
|
|
182
|
-
"logs": [{"ref": {"kind": "logs", "id": "opaque-log-ref"}}]
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
ResourceRef 的插件可见形状严格为 `{kind,id}`,id 是适配层提供的不透明引用。
|
|
188
|
-
示例里的 `runs`、`logs` 是引用示意,不在本协议登记新类型;实际使用现有运行和日志对象的 kind。
|
|
189
|
-
适配层必须保持源码、运行、日志和产物的持有者归属,不能把本机 `remote:…` 字符串原样传给另一台便认为能解析。
|
|
190
|
-
日志衔接现有运行日志读面;产物这里只预留引用、大小与摘要,字节传输和 sha256 核验仍属后续 artifacts 实施步骤。
|
|
191
|
-
|
|
192
|
-
## 流程定位与受控上下文
|
|
193
|
-
|
|
194
|
-
placement 是生产者约束,顶层 executor/assignmentId 才是 scheduler 的实际绑定,两者不可混用。
|
|
195
|
-
`placement.executor` 指定具体环境,`locality=local` 限定请求持有端,二者同时满足;约束冲突或目标不可用时排队并报告原因,
|
|
196
|
-
不能丢弃绑定后自动选本机。`locality=any` 只放开位置,标签、能力、资源与准入仍须满足。归属由适配层判断,不解析 ID、拼机器参数。
|
|
197
|
-
绑定前后执行方都应检查 placement;schema 不检查引用的实际归属或两个 executor 引用相等。
|
|
198
|
-
|
|
199
|
-
流程方言可在 process、subProcess、scriptTask 写 `coda:placement="local|any"`。
|
|
200
|
-
任何祖先的 local 都是硬限制,子节点 any 不能放宽。未写 placement 且无有效 runsOn 的脚本由流程生产者设置 local;
|
|
201
|
-
有 runsOn 才默认 any。显式项目绑定与此约束相交,不覆盖本机限制。需要 CODA_ARTIFACTS、桌面或本机安装状态的流程显式写 local。
|
|
202
|
-
用户任务和 Coda 动作仍由流程持有端处理,不接受 placement。这些继承与准入规则由宿主实现,解析器只校验和保留声明。
|
|
203
|
-
|
|
204
|
-
context 是可信流程生产者冻结的事实,不是权限证明,也不是任意环境变量 map:
|
|
205
|
-
|
|
206
|
-
- `workflow` 若存在,必须完整给出 `name`、`job`、`runId`、`runAttempt`、`sha`、`ref`、`repository`。
|
|
207
|
-
sha 为完整 SHA 或空字符串(不检出);ref/repository 可为空。执行方生成 GITHUB_WORKFLOW/JOB/RUN_ID/RUN_ATTEMPT/SHA/REF/REPOSITORY,
|
|
208
|
-
并根据本机工作区生成 GITHUB_WORKSPACE、输出与临时路径。生产者必须核对这些值与 run/step/源码相符。
|
|
209
|
-
- `plugin: {id,version}` 标记插件脚本来源,不给绝对路径。实际执行还须满足本机安装、版本和项目授权;本阶段插件流程仍由生产者限定本机。
|
|
210
|
-
- `audience: "user"` 沿用用户专属结果边界。持有端与执行端必须在对象、日志及结果读面实施可见性,不能仅靠此字符串保护私有内容。
|
|
211
|
-
- `outputNames` 是最多 64 个不重复的输出名;明确给空数组表示不返回脚本变量。给出时只回填声明键,未写出的声明输出为空字符串。
|
|
212
|
-
省略沿用普通执行请求的输出收集;不能凭未声明变量泄露私有数据。
|
|
213
|
-
|
|
214
|
-
所有本机路径、凭据、插件设置、完整进程环境和事件密文均不进入 context。实际组准入后才由执行方生成 CODA_GROUPS,不能信任提交者自报已占额。
|
|
215
|
-
context 字段的文本边界见 schema;执行方仅在显式声明 executionFeatures.context 时接收它。
|
|
216
|
-
|
|
217
|
-
## 写权限、绑定与改派
|
|
218
|
-
|
|
219
|
-
1. executor 无 target,status 权威是持有端。execution 未绑定时,本机 provider/获准 scheduler 可写 queued 与原因;
|
|
220
|
-
创建时可给 queued,缺失状态是未知。scheduler 只处理本机正本,按观察到的 revision 同次 CAS 写入 executor 与新的 assignmentId。
|
|
221
|
-
绑定是顶层单值,**不走 append/or 补充合并**,不将多个候选合并成绑定。
|
|
222
|
-
2. 绑定后任务、源码、身份、资源、需求、placement 与 context 冻结,只可单向设置 cancel;安全改派仅更换绑定两栏。
|
|
223
|
-
空间桥将 executor 翻译成 `coda.pub/target`。执行方只观察 assigned,并核对具体 executor ID 是自己实现的;
|
|
224
|
-
指向后端不表示该后端所有执行环境都可领单。**绑定后只有执行方完整回传 status**,包括字段删除;
|
|
225
|
-
其他节点无权写,持有者不能伪造远端完成。旧 status 按 assignmentId 区分,不能拿旧绑定结果解释新绑定。
|
|
226
|
-
3. 未绑定请求可直接选择。已绑定请求只有当前 assignment 明确拒绝,或撤销已确认且证明不会再启动,才可改派。
|
|
227
|
-
对齐 assignmentId、验证拒绝/撤销事实和权限是机制责任,单凭符合 schema 的对象不能授权改派。
|
|
228
|
-
旧 queued、缺 startedAt、空 status、租约超时或节点掉线均不能证明未开始;已开始或是否开始不明,保留原绑定与运行引用并恢复观察。
|
|
229
|
-
4. 持有端将不可达作为观察状态展示,不代写执行结果。取消或撤下镜像也不是原进程已停证明。
|
|
230
|
-
自动重试依任务幂等性和重试政策,创建新 attempt;协议不承诺网络分区中的 exactly-once。
|
|
231
|
-
|
|
232
|
-
## 开跑前的本机账本准入与恢复
|
|
233
|
-
|
|
234
|
-
执行方须在 prepare/启动前,持久登记 **execution 的 instance + assignmentId**,经同一本机账本原子获得资源与固定名额序号。
|
|
235
|
-
调度看到 available 或 binding 成功均不等于准入。普通占资源请求只占指定资源的一单位;独占须走账本独占动作,不能连续读 available 或分批占额模拟。
|
|
236
|
-
无资源请求仍须持久登记并确认执行实现,只是不申请租约;不得给无 group 的步骤暗加 slots 限流。
|
|
237
|
-
容量冲突可排队或明确拒绝,scheduler 决定是否在满足改派条件后换目标。
|
|
238
|
-
多键/多数量原子准入尚未开放,schema 和 executionFeatures 均不宣称支持。
|
|
239
|
-
|
|
240
|
-
进程和 cleanup 确认结束后才释放;故障期间保留待恢复占用。重启 list-watch 的 ADDED 只能对账,不得再次启动同一 instance + assignment。
|
|
241
|
-
旧镜像指纹/revision 防回写不能代替防重复启动。被拒绝的旧结果不能简单换成最新 revision 重交。
|
|
242
|
-
|
|
243
|
-
`control` 对同对象串行回调;不能 await 整段长任务堵住取消。
|
|
244
|
-
办事员登记并启动/跟随持久运行后返回,后续观察更新取消进程组。取消只改 spec.cancel,执行方重读同 assignment 的当前意图,
|
|
245
|
-
完成取消与收尾后报告 cancelled;取消不能回退为 false,也不能回写旧请求的结果。
|
|
246
|
-
|
|
247
|
-
## 与现有执行链的衔接边界
|
|
248
|
-
|
|
249
|
-
执行保留 `prepare_job / run_script_step / cleanup_job` 三段 hook,以及 `{command,responseFile,args,state}`、准备 state、
|
|
250
|
-
日志、退出码、取消进程组、失败也收尾的语义。清单 `command` 是 hook 入口,与 `task.command` 不同。
|
|
251
|
-
源码 provider 给出精确 commit/ref 与本机鉴权代理入口;hook 本机路径在执行方生成,不进入跨机 spec。
|
|
252
|
-
|
|
253
|
-
缓存目录建议 `$CODA_PLUGIN_STATE/workspaces/<source-key>/<slot-index>/`:source-key 来自来源工作副本身份及对象 instance,
|
|
254
|
-
不能用 origin URL、提交号或快照 ID 代替;同仓库不同 linked worktree 也隔离。
|
|
255
|
-
slot-index 来自本机账本,scheduler 不猜序号;借用也不得使目录重入。日志/响应文件另放尝试目录。
|
|
256
|
-
准入后 fetch 精确 ref/commit、核对实际 commit,再 reset --hard 与 clean -fd,保留忽略缓存;cleanup 清理本次临时资源并放开资源,保留缓存副本。
|
|
257
|
-
发布/部署仍由对应流程和插件检查已合并提交准入,不因支持快照删除 origin/发布检查。
|
|
258
|
-
|
|
259
|
-
旧 Executor 发布与 steps/Step 跨机请求由后续消费方切换撤除,不永久保留两套调度真相。
|
|
260
|
-
本包只交付标准 JSON Schema 和规范;引用可移植解析、本机鉴权入口、账本准入/取消/释放及稳定序号、恢复日志跟随、
|
|
261
|
-
host/scheduler 实现与流程切换均属于各自仓库后续工作。来源权限、时间先后、容量计算、状态迁移和真实路径不伪装成 schema 动态审核器。
|
package/executor.mjs
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
// Public object contracts. Each spec/status can be compiled by a fresh, default Ajv (draft-07).
|
|
2
|
-
// Only local references remain; no validator keywords, provider identity or scheduling policy are added here.
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
4
|
-
|
|
5
|
-
const base = 'https://coda.pub/schemas/executor/v1/';
|
|
6
|
-
const read = name => JSON.parse(readFileSync(new URL(`./schemas/executor/v1/${name}.json`, import.meta.url), 'utf8'));
|
|
7
|
-
const common = read('common');
|
|
8
|
-
const local = value => JSON.parse(JSON.stringify(value).replaceAll('common.json#/definitions/', '#/definitions/'));
|
|
9
|
-
function protocol(kind) {
|
|
10
|
-
const schema = read(kind);
|
|
11
|
-
const part = name => ({
|
|
12
|
-
$schema: schema.$schema,
|
|
13
|
-
$id: `${base}${kind}.${name}.json`,
|
|
14
|
-
...local(schema.properties[name]),
|
|
15
|
-
definitions: local(common.definitions),
|
|
16
|
-
});
|
|
17
|
-
return { kind, spec: part('spec'), status: part('status') };
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export const EXECUTOR_PROTOCOL = protocol('executor');
|
|
21
|
-
export const EXECUTION_PROTOCOL = { ...protocol('execution'), target: 'executor' };
|