xuym-codex-collab 0.3.0 → 0.4.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/README.md +23 -7
- package/apps/onboarding/manager.mjs +165 -24
- package/apps/onboarding/server.mjs +1 -1
- package/apps/onboarding/web/app.js +44 -18
- package/apps/onboarding/web/index.html +5 -3
- package/apps/peer/desktop.mjs +21 -0
- package/apps/peer/mcp.mjs +4 -2
- package/apps/peer/wecom.mjs +58 -12
- package/bin/codex-collab.mjs +8 -0
- package/docs/desktop-poll.md +32 -8
- package/docs/local-automatic-test.md +1 -1
- package/docs/local-peer-test.md +1 -1
- package/docs/onboarding-audit.md +88 -0
- package/docs/onboarding.md +33 -17
- package/docs/peer.md +11 -2
- package/docs/team-registry.md +13 -3
- package/docs/terminal-onboarding.md +1 -1
- package/package.json +1 -1
- package/packages/codex-kit/templates/.agents/skills/team-collab-desktop/SKILL.md +14 -9
- package/packages/peer/config.mjs +3 -2
- package/packages/peer/inbox.mjs +48 -0
- package/packages/peer/registry.mjs +5 -2
- package/packages/peer/review.mjs +2 -1
- package/packages/peer/service.mjs +70 -1
- package/packages/peer/store.mjs +90 -2
- package/schemas/peer-inbox-event.schema.json +284 -0
- package/schemas/peer-inbox.schema.json +63 -0
- package/schemas/team-invitation.schema.json +23 -1
- package/schemas/team-policy.schema.json +1 -1
- package/scripts/peer/onboarding.mjs +15 -3
- package/scripts/peer/refresh-project.mjs +76 -0
- package/scripts/peer/setup-desktop-project.mjs +1 -1
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "urn:codex-collab:peer-inbox-event:v1",
|
|
4
|
+
"title": "Codex Collab Signed Natural Language Inbox Event v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"schema_version",
|
|
9
|
+
"request_id",
|
|
10
|
+
"seq",
|
|
11
|
+
"event_id",
|
|
12
|
+
"actor_id",
|
|
13
|
+
"occurred_at",
|
|
14
|
+
"idempotency_key",
|
|
15
|
+
"team_hash",
|
|
16
|
+
"signature",
|
|
17
|
+
"type"
|
|
18
|
+
],
|
|
19
|
+
"properties": {
|
|
20
|
+
"schema_version": {
|
|
21
|
+
"const": 1
|
|
22
|
+
},
|
|
23
|
+
"request_id": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"pattern": "^N-[A-F0-9]{32}$"
|
|
26
|
+
},
|
|
27
|
+
"seq": {
|
|
28
|
+
"type": "integer",
|
|
29
|
+
"minimum": 1
|
|
30
|
+
},
|
|
31
|
+
"event_id": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"pattern": "^[a-f0-9-]{36}$"
|
|
34
|
+
},
|
|
35
|
+
"actor_id": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"pattern": "^[a-zA-Z0-9_-]{1,64}$"
|
|
38
|
+
},
|
|
39
|
+
"occurred_at": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"format": "date-time",
|
|
42
|
+
"pattern": "Z$"
|
|
43
|
+
},
|
|
44
|
+
"idempotency_key": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"minLength": 1,
|
|
47
|
+
"maxLength": 300,
|
|
48
|
+
"pattern": "\\S"
|
|
49
|
+
},
|
|
50
|
+
"team_hash": {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
53
|
+
},
|
|
54
|
+
"signature": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"minLength": 1
|
|
57
|
+
},
|
|
58
|
+
"type": {
|
|
59
|
+
"enum": [
|
|
60
|
+
"received",
|
|
61
|
+
"clarified",
|
|
62
|
+
"replied",
|
|
63
|
+
"task_created",
|
|
64
|
+
"answered"
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
"spec_hash": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
70
|
+
},
|
|
71
|
+
"content": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"minLength": 1,
|
|
74
|
+
"maxLength": 4000,
|
|
75
|
+
"pattern": "\\S"
|
|
76
|
+
},
|
|
77
|
+
"message_key": {
|
|
78
|
+
"type": "string",
|
|
79
|
+
"minLength": 1,
|
|
80
|
+
"maxLength": 300,
|
|
81
|
+
"pattern": "\\S"
|
|
82
|
+
},
|
|
83
|
+
"response": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"minLength": 1,
|
|
86
|
+
"maxLength": 2000,
|
|
87
|
+
"pattern": "\\S"
|
|
88
|
+
},
|
|
89
|
+
"task_id": {
|
|
90
|
+
"type": "string",
|
|
91
|
+
"pattern": "^T-[A-F0-9]{32}$"
|
|
92
|
+
},
|
|
93
|
+
"task_hash": {
|
|
94
|
+
"type": "string",
|
|
95
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"oneOf": [
|
|
99
|
+
{
|
|
100
|
+
"properties": {
|
|
101
|
+
"type": {
|
|
102
|
+
"const": "received"
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"required": [
|
|
106
|
+
"spec_hash"
|
|
107
|
+
],
|
|
108
|
+
"not": {
|
|
109
|
+
"anyOf": [
|
|
110
|
+
{
|
|
111
|
+
"required": [
|
|
112
|
+
"content"
|
|
113
|
+
]
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"required": [
|
|
117
|
+
"message_key"
|
|
118
|
+
]
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"required": [
|
|
122
|
+
"response"
|
|
123
|
+
]
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"required": [
|
|
127
|
+
"task_hash"
|
|
128
|
+
]
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"required": [
|
|
132
|
+
"task_id"
|
|
133
|
+
]
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"properties": {
|
|
140
|
+
"type": {
|
|
141
|
+
"const": "clarified"
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"required": [
|
|
145
|
+
"response"
|
|
146
|
+
],
|
|
147
|
+
"not": {
|
|
148
|
+
"anyOf": [
|
|
149
|
+
{
|
|
150
|
+
"required": [
|
|
151
|
+
"content"
|
|
152
|
+
]
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"required": [
|
|
156
|
+
"message_key"
|
|
157
|
+
]
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"required": [
|
|
161
|
+
"spec_hash"
|
|
162
|
+
]
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"required": [
|
|
166
|
+
"task_hash"
|
|
167
|
+
]
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"required": [
|
|
171
|
+
"task_id"
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
]
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"properties": {
|
|
179
|
+
"type": {
|
|
180
|
+
"const": "replied"
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
"required": [
|
|
184
|
+
"content",
|
|
185
|
+
"message_key"
|
|
186
|
+
],
|
|
187
|
+
"not": {
|
|
188
|
+
"anyOf": [
|
|
189
|
+
{
|
|
190
|
+
"required": [
|
|
191
|
+
"response"
|
|
192
|
+
]
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"required": [
|
|
196
|
+
"spec_hash"
|
|
197
|
+
]
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"required": [
|
|
201
|
+
"task_hash"
|
|
202
|
+
]
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"required": [
|
|
206
|
+
"task_id"
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
]
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"properties": {
|
|
214
|
+
"type": {
|
|
215
|
+
"const": "task_created"
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
"required": [
|
|
219
|
+
"task_id",
|
|
220
|
+
"task_hash",
|
|
221
|
+
"response"
|
|
222
|
+
],
|
|
223
|
+
"not": {
|
|
224
|
+
"anyOf": [
|
|
225
|
+
{
|
|
226
|
+
"required": [
|
|
227
|
+
"content"
|
|
228
|
+
]
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"required": [
|
|
232
|
+
"message_key"
|
|
233
|
+
]
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"required": [
|
|
237
|
+
"spec_hash"
|
|
238
|
+
]
|
|
239
|
+
}
|
|
240
|
+
]
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
"properties": {
|
|
245
|
+
"type": {
|
|
246
|
+
"const": "answered"
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
"required": [
|
|
250
|
+
"response"
|
|
251
|
+
],
|
|
252
|
+
"not": {
|
|
253
|
+
"anyOf": [
|
|
254
|
+
{
|
|
255
|
+
"required": [
|
|
256
|
+
"content"
|
|
257
|
+
]
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
"required": [
|
|
261
|
+
"message_key"
|
|
262
|
+
]
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
"required": [
|
|
266
|
+
"spec_hash"
|
|
267
|
+
]
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"required": [
|
|
271
|
+
"task_hash"
|
|
272
|
+
]
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"required": [
|
|
276
|
+
"task_id"
|
|
277
|
+
]
|
|
278
|
+
}
|
|
279
|
+
]
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
],
|
|
283
|
+
"$comment": "Runtime also verifies Ed25519 signatures and historical team hash, receiving-agent authorship, exact sequence, unique IDs and idempotency keys, nondecreasing UTC timestamps, signed spec digest and allowed transitions. received opens pending; clarified waits for a reply; replied returns to pending; task_created and answered resolve pending. A task_created event and its desktop_poll task commit atomically; runtime validates the exact task digest, deterministic task ID, original group, creator and creation key. Replies are scoped to the same bot, group and sender."
|
|
284
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "urn:codex-collab:peer-inbox:v1",
|
|
4
|
+
"title": "Codex Collab Natural Language Inbox Request v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"schema_version",
|
|
9
|
+
"id",
|
|
10
|
+
"repo",
|
|
11
|
+
"target_agent",
|
|
12
|
+
"chat_id",
|
|
13
|
+
"sender_id",
|
|
14
|
+
"content",
|
|
15
|
+
"message_key",
|
|
16
|
+
"admin_dispatch"
|
|
17
|
+
],
|
|
18
|
+
"properties": {
|
|
19
|
+
"schema_version": {
|
|
20
|
+
"const": 1
|
|
21
|
+
},
|
|
22
|
+
"id": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"pattern": "^N-[A-F0-9]{32}$"
|
|
25
|
+
},
|
|
26
|
+
"repo": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"minLength": 1
|
|
29
|
+
},
|
|
30
|
+
"target_agent": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"pattern": "^[a-zA-Z0-9_-]{1,64}$"
|
|
33
|
+
},
|
|
34
|
+
"chat_id": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"minLength": 1,
|
|
37
|
+
"maxLength": 200,
|
|
38
|
+
"pattern": "\\S"
|
|
39
|
+
},
|
|
40
|
+
"sender_id": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"minLength": 1,
|
|
43
|
+
"maxLength": 200,
|
|
44
|
+
"pattern": "\\S"
|
|
45
|
+
},
|
|
46
|
+
"content": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"minLength": 1,
|
|
49
|
+
"maxLength": 4000,
|
|
50
|
+
"pattern": "\\S"
|
|
51
|
+
},
|
|
52
|
+
"message_key": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"minLength": 1,
|
|
55
|
+
"maxLength": 300,
|
|
56
|
+
"pattern": "\\S"
|
|
57
|
+
},
|
|
58
|
+
"admin_dispatch": {
|
|
59
|
+
"type": "boolean"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"$comment": "Runtime also verifies the repo and target against the pinned team, a deterministic request ID derived from target_agent and message_key, and bot-owner or allowed administrator authorization. The original group request is shared in Git; private Codex transcripts, bindings and credentials stay local."
|
|
63
|
+
}
|
|
@@ -80,7 +80,29 @@
|
|
|
80
80
|
"signature": {
|
|
81
81
|
"type": "string",
|
|
82
82
|
"minLength": 1
|
|
83
|
+
},
|
|
84
|
+
"owner_userid": {
|
|
85
|
+
"type": "string",
|
|
86
|
+
"minLength": 1,
|
|
87
|
+
"maxLength": 200,
|
|
88
|
+
"description": "Optional owner pinned to the issuing administrator for adding their own independent agent."
|
|
83
89
|
}
|
|
84
90
|
},
|
|
85
|
-
"$comment": "Runtime verifies administrator signature under team_hash, Ed25519 claim key, expiry, and conflicts with existing members or assignments. No bot Secret or private keys are allowed here."
|
|
91
|
+
"$comment": "Runtime verifies administrator signature under team_hash, Ed25519 claim key, expiry, and conflicts with existing members or assignments. No bot Secret or private keys are allowed here. A self-owned invitation pins owner_userid to the issuing administrator; enrollment must prove the same account. Existing invitations without this optional field remain valid.",
|
|
92
|
+
"allOf": [
|
|
93
|
+
{
|
|
94
|
+
"if": {
|
|
95
|
+
"required": [
|
|
96
|
+
"owner_userid"
|
|
97
|
+
]
|
|
98
|
+
},
|
|
99
|
+
"then": {
|
|
100
|
+
"properties": {
|
|
101
|
+
"commands_enabled": {
|
|
102
|
+
"const": true
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
]
|
|
86
108
|
}
|
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
},
|
|
158
|
-
"$comment": "Runtime verifies distinct Ed25519 keys and owner IDs, valid reviewer and route capabilities, and clock skew below half the lease. v3 requires a review rule for every route; specific members require a verified owner. v2 forbids review_rules so old clients cannot silently bypass gates.",
|
|
158
|
+
"$comment": "Runtime verifies distinct Ed25519 keys and valid owner IDs (several agents may share the same human owner), valid reviewer and route capabilities, and clock skew below half the lease. v3 requires a review rule for every route; specific members require a verified owner. v2 forbids review_rules so old clients cannot silently bypass gates.",
|
|
159
159
|
"allOf": [
|
|
160
160
|
{
|
|
161
161
|
"if": {
|
|
@@ -187,7 +187,13 @@ export async function attachProject({ configPath, repoPath }) {
|
|
|
187
187
|
const block = `[mcp_servers.${serverName}]\ncommand = ${JSON.stringify(process.execPath)}\nargs = [${JSON.stringify(join(root, 'apps/peer/mcp-main.mjs'))}, ${JSON.stringify(configPath)}]`;
|
|
188
188
|
const next = mergeMcpToml(before, serverName, block);
|
|
189
189
|
const excludePath = await git(repo, 'rev-parse', '--path-format=absolute', '--git-path', 'info/exclude');
|
|
190
|
-
|
|
190
|
+
let oldExclude = '';
|
|
191
|
+
let excludeExisted = true;
|
|
192
|
+
try { oldExclude = await readFile(excludePath, 'utf8'); }
|
|
193
|
+
catch (error) {
|
|
194
|
+
if (error.code !== 'ENOENT') throw error;
|
|
195
|
+
excludeExisted = false;
|
|
196
|
+
}
|
|
191
197
|
const rules = [`/.codex/config.toml`, `/.agents/skills/${skillName}/`, '/.codex-collab-worktrees/'];
|
|
192
198
|
const addedRules = rules.filter((rule) => !oldExclude.split(/\r?\n/).includes(rule));
|
|
193
199
|
const template = await readFile(join(root, `packages/codex-kit/templates/.agents/skills/${skillName}/SKILL.md`), 'utf8');
|
|
@@ -197,12 +203,18 @@ export async function attachProject({ configPath, repoPath }) {
|
|
|
197
203
|
await writeFile(configFile, next, { mode: 0o600 });
|
|
198
204
|
await chmod(configFile, 0o600);
|
|
199
205
|
await writeFile(skillFile, template, { mode: 0o600, flag: 'wx' });
|
|
200
|
-
if (addedRules.length)
|
|
206
|
+
if (addedRules.length) {
|
|
207
|
+
await mkdir(dirname(excludePath), { recursive: true, mode: 0o700 });
|
|
208
|
+
await writeFile(excludePath, `${oldExclude}${oldExclude.endsWith('\n') || !oldExclude ? '' : '\n'}${addedRules.join('\n')}\n`);
|
|
209
|
+
}
|
|
201
210
|
} catch (error) {
|
|
202
211
|
if (before) await writeFile(configFile, before, { mode: 0o600 });
|
|
203
212
|
else await rm(configFile, { force: true });
|
|
204
213
|
await rm(skillDir, { recursive: true, force: true });
|
|
205
|
-
if (addedRules.length)
|
|
214
|
+
if (addedRules.length) {
|
|
215
|
+
if (excludeExisted) await writeFile(excludePath, oldExclude);
|
|
216
|
+
else await rm(excludePath, { force: true });
|
|
217
|
+
}
|
|
206
218
|
throw error;
|
|
207
219
|
}
|
|
208
220
|
return { repo, mcp_config: configFile, skill: skillFile };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { chmod, lstat, mkdir, readFile, realpath, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { readPeerConfig } from '../../apps/peer/runtime.mjs';
|
|
6
|
+
import { git } from '../../packages/peer/store.mjs';
|
|
7
|
+
import { bindingPrompt } from './setup-desktop-project.mjs';
|
|
8
|
+
const root = fileURLToPath(new URL('../../', import.meta.url));
|
|
9
|
+
const legacyHashes = new Set(['f98ad425e2ea3c8ab6fb86febef7d9cb288029ef55bea6937dda6de8b467fece']);
|
|
10
|
+
const hash = (value) => createHash('sha256').update(value).digest('hex');
|
|
11
|
+
const inside = (parent, child) => { const path = relative(parent, child); return !path || path !== '..' && !path.startsWith('../') && !isAbsolute(path); };
|
|
12
|
+
async function plainPath(base, path) {
|
|
13
|
+
let current = base;
|
|
14
|
+
for (const part of relative(base, path).split('/').filter(Boolean)) {
|
|
15
|
+
current = join(current, part);
|
|
16
|
+
if ((await lstat(current)).isSymbolicLink()) throw new Error('协作配置或 Skill 是符号链接,请先手动检查;未覆盖文件。');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function privateWrite(path, contents) {
|
|
20
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
21
|
+
try { await writeFile(temporary, contents, { mode: 0o600, flag: 'wx' }); await rename(temporary, path); await chmod(path, 0o600); }
|
|
22
|
+
finally { await rm(temporary, { force: true }); }
|
|
23
|
+
}
|
|
24
|
+
// Only recognize our generated MCP entry. Never replace another identity's entry.
|
|
25
|
+
export async function installedProjectConfig({ repoPath, agentId }) {
|
|
26
|
+
const repo = await realpath(repoPath);
|
|
27
|
+
const path = join(repo, '.codex/config.toml'); await plainPath(repo, path);
|
|
28
|
+
const before = await readFile(path, 'utf8');
|
|
29
|
+
const server = `team-collab-${agentId}`;
|
|
30
|
+
const headers = [...before.matchAll(/^\s*\[([^\]\n]+)\][^\n]*$/gm)];
|
|
31
|
+
if (headers.some((item) => item[1].startsWith(`mcp_servers.${server}.`))) throw new Error('协作 MCP 有自定义嵌套配置,请手动检查;未覆盖文件。');
|
|
32
|
+
const matching = headers.filter((item) => item[1] === `mcp_servers.${server}`);
|
|
33
|
+
if (matching.length !== 1) throw new Error('未找到唯一的已安装协作 MCP,请先完成项目接入。');
|
|
34
|
+
const header = matching[0]; const end = headers.find((item) => item.index > header.index)?.index ?? before.length;
|
|
35
|
+
const section = before.slice(header.index, end);
|
|
36
|
+
const argsLine = section.match(/^args\s*=\s*(\[[^\n]+\])\s*$/m);
|
|
37
|
+
const commandLine = section.match(/^command\s*=\s*("[^\n]+")\s*$/m);
|
|
38
|
+
let args, command;
|
|
39
|
+
try { args = JSON.parse(argsLine?.[1]); command = JSON.parse(commandLine?.[1]); } catch { /* reject below */ }
|
|
40
|
+
if (!Array.isArray(args) || args.length !== 2 || typeof args[0] !== 'string' || !args[0].endsWith('/apps/peer/mcp-main.mjs') || !isAbsolute(args[1] || '') || typeof command !== 'string' || !command) throw new Error('协作 MCP 不是可识别的安装配置,请手动检查;未覆盖文件。');
|
|
41
|
+
const keys = section.split(/\r?\n/).filter((line) => line.trim() && !/^\s*(?:#|\[)/.test(line));
|
|
42
|
+
if (keys.length !== 2) throw new Error('协作 MCP 有自定义设置,请手动检查;未覆盖文件。');
|
|
43
|
+
return { repo, path, before, section, config_path: args[1], command, args };
|
|
44
|
+
}
|
|
45
|
+
export async function refreshProject({ configPath }) {
|
|
46
|
+
if (!isAbsolute(configPath || '')) throw new Error('usage: codex-collab refresh-project /absolute/private-peer.json');
|
|
47
|
+
const { config } = await readPeerConfig(configPath);
|
|
48
|
+
if (config.execution_mode !== 'desktop_poll' || config.auto_execute !== false) throw new Error('refresh-project 需要 desktop_poll 配置且 SDK 已关闭。');
|
|
49
|
+
const installed = await installedProjectConfig({ repoPath: config.repo, agentId: config.agent_id });
|
|
50
|
+
const { repo, path: configFile, before, section } = installed;
|
|
51
|
+
if (await git(repo, 'rev-parse', '--show-toplevel') !== repo || await realpath(installed.config_path) !== await realpath(configPath)) throw new Error('项目 MCP 已绑定其他配置,请使用该 MCP 实际使用的个人配置路径;未更换身份。');
|
|
52
|
+
const skillFile = join(repo, '.agents/skills/team-collab-desktop/SKILL.md');
|
|
53
|
+
await plainPath(repo, skillFile);
|
|
54
|
+
if (await git(repo, 'ls-files', '--', '.codex/config.toml', '.agents/skills/team-collab-desktop')) throw new Error('协作配置或 Skill 已提交到项目 Git,请手动合并;未覆盖共享文件。');
|
|
55
|
+
const oldSkill = await readFile(skillFile, 'utf8');
|
|
56
|
+
const template = await readFile(join(root, 'packages/codex-kit/templates/.agents/skills/team-collab-desktop/SKILL.md'), 'utf8');
|
|
57
|
+
if (oldSkill !== template && !legacyHashes.has(hash(oldSkill))) throw new Error('协作 Skill 已自定义,请手动合并自然语言处理说明;未覆盖文件。');
|
|
58
|
+
const nextSection = section.replace(/^command\s*=.*$/m, `command = ${JSON.stringify(process.execPath)}`).replace(/^args\s*=.*$/m, `args = ${JSON.stringify([join(root, 'apps/peer/mcp-main.mjs'), installed.config_path])}`);
|
|
59
|
+
const next = before.replace(section, () => nextSection);
|
|
60
|
+
if (inside(repo, resolve(config.data_dir))) throw new Error('个人数据目录必须位于项目仓库之外。');
|
|
61
|
+
await mkdir(config.data_dir, { recursive: true, mode: 0o700 });
|
|
62
|
+
const dataDir = await realpath(config.data_dir);
|
|
63
|
+
if (inside(repo, dataDir)) throw new Error('个人数据目录必须位于项目仓库之外。');
|
|
64
|
+
const prompt = join(dataDir, 'desktop-binding-prompt.txt');
|
|
65
|
+
const changed = next !== before || oldSkill !== template;
|
|
66
|
+
let backup = null;
|
|
67
|
+
if (changed) {
|
|
68
|
+
backup = join(dataDir, `project-refresh-${randomUUID()}.json`);
|
|
69
|
+
await writeFile(backup, JSON.stringify({ repo, config_toml: before, skill: oldSkill, created_at: new Date().toISOString() }, null, 2) + '\n', { mode: 0o600, flag: 'wx' });
|
|
70
|
+
try { await privateWrite(configFile, next); await privateWrite(skillFile, template); }
|
|
71
|
+
catch (error) { await privateWrite(configFile, before); await privateWrite(skillFile, oldSkill); throw error; }
|
|
72
|
+
}
|
|
73
|
+
await privateWrite(prompt, bindingPrompt(config.agent_id, repo));
|
|
74
|
+
return { agent_id: config.agent_id, repo, changed, mcp_config: configFile, skill: skillFile, backup, binding_prompt: prompt,
|
|
75
|
+
next_step: '重新加载项目 MCP;把 binding_prompt 内容发送到原绑定对话,以更新原协作 heartbeat。', heartbeat_updated: false };
|
|
76
|
+
}
|
|
@@ -8,7 +8,7 @@ const absent = async (path) => { try { await stat(path); return false; } catch (
|
|
|
8
8
|
const readOptional = async (path) => { try { return await readFile(path, 'utf8'); } catch (error) { if (error.code === 'ENOENT') return null; throw error; } };
|
|
9
9
|
|
|
10
10
|
export function bindingPrompt(agentId, repo) {
|
|
11
|
-
return `请把当前已有 Codex 对话设为 ${agentId} 的企微协作执行对话,不要新建独立执行线程。\n项目:${repo}\n读取 ${join(repo, '.agents/skills/team-collab-desktop/SKILL.md')}。\n1. 用本项目 MCP 的 get_identity 确认身份是 ${agentId},execution_mode 是 desktop_poll;若不匹配,停止并提示重新加载 MCP。\n2. 获取当前真实对话 ID:优先使用已提供的应用上下文,或当前终端 CODEX_THREAD_ID(如果存在);不可猜 ID 或读取/修改应用数据库。没有可用 ID 时请让我复制当前对话 ID/链接。\n3. 调用 bind_desktop_thread 绑定当前真实对话,然后用应用 automation_update 工具在当前已有对话创建一个 heartbeat:每分钟检查(仅当工具接受该周期;否则说明限制),不要创建 cron、新对话或后台 SDK。先查现有 automation,避免重复;不要修改无关 automation 或 CC Switch。\n4. heartbeat 的保存提示词必须包含 ${agentId}、当前真实 thread_id 和该 skill 路径;每次 get_identity → poll_desktop_task。job=null 时保持安静;有任务就只在本对话和返回的隔离 worktree 执行本阶段,主动续租、测试、明确提交,然后 finish_desktop_task;失败时按 skill
|
|
11
|
+
return `请把当前已有 Codex 对话设为 ${agentId} 的企微协作执行对话,不要新建独立执行线程。\n项目:${repo}\n读取 ${join(repo, '.agents/skills/team-collab-desktop/SKILL.md')}。\n1. 用本项目 MCP 的 get_identity 确认身份是 ${agentId},execution_mode 是 desktop_poll;若不匹配,停止并提示重新加载 MCP。\n2. 获取当前真实对话 ID:优先使用已提供的应用上下文,或当前终端 CODEX_THREAD_ID(如果存在);不可猜 ID 或读取/修改应用数据库。没有可用 ID 时请让我复制当前对话 ID/链接。\n3. 调用 bind_desktop_thread 绑定当前真实对话,然后用应用 automation_update 工具在当前已有对话创建一个 heartbeat:每分钟检查(仅当工具接受该周期;否则说明限制),不要创建 cron、新对话或后台 SDK。先查现有 automation,避免重复;不要修改无关 automation 或 CC Switch。\n4. heartbeat 的保存提示词必须包含 ${agentId}、当前真实 thread_id 和该 skill 路径;每次 get_identity → poll_desktop_task。job=null 且 requests 为空 时保持安静;有任务就只在本对话和返回的隔离 worktree 执行本阶段,主动续租、测试、明确提交,然后 finish_desktop_task;失败时按 skill 处理。requests 非空时按 Skill 理解群消息,调用 resolve_desktop_request 创建任务、追问或回答;使用 team_context 的现有路由、能力与默认分工,不能猜测不存在的同事。解决后再次 poll 开始工作。自然语言“通过/完成”不能代替人工审核命令。复用或更新当前对话已有的协作 heartbeat,避免重复创建。禁止启动 SDK/CLI 代替当前对话。仅新任务、结果、失败或需我操作时通知。\n5. 创建完成后立即调用一次 poll_desktop_task 验证,无任务应正常返回,不要创建测试任务。告诉我绑定身份、对话 ID 和 heartbeat 状态;若工具不可用明确说明未启用自动轮询。\n`;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
// An explicit project attachment: preserve unrelated TOML settings, keep a local
|