xuym-codex-collab 0.1.1 → 0.3.1

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 (46) hide show
  1. package/README.md +64 -109
  2. package/apps/onboarding/bundle.mjs +24 -0
  3. package/apps/onboarding/discovery.mjs +44 -0
  4. package/apps/onboarding/errors.mjs +1 -0
  5. package/apps/onboarding/main.mjs +32 -0
  6. package/apps/onboarding/manager.mjs +532 -0
  7. package/apps/onboarding/server.mjs +56 -0
  8. package/apps/onboarding/web/app.js +154 -0
  9. package/apps/onboarding/web/index.html +15 -0
  10. package/apps/onboarding/web/style.css +3 -0
  11. package/apps/peer/desktop.mjs +8 -6
  12. package/apps/peer/mcp.mjs +1 -1
  13. package/apps/peer/runner.mjs +5 -5
  14. package/apps/peer/runtime.mjs +4 -1
  15. package/apps/peer/wecom.mjs +76 -21
  16. package/bin/codex-collab.mjs +10 -7
  17. package/docs/architecture.md +3 -1
  18. package/docs/desktop-poll.md +16 -8
  19. package/docs/legacy-center.md +109 -0
  20. package/docs/local-automatic-test.md +1 -1
  21. package/docs/local-peer-test.md +1 -1
  22. package/docs/onboarding-audit.md +88 -0
  23. package/docs/onboarding.md +92 -69
  24. package/docs/peer.md +13 -7
  25. package/docs/protocol.md +2 -0
  26. package/docs/team-registry.md +65 -0
  27. package/docs/terminal-onboarding.md +98 -0
  28. package/package.json +6 -3
  29. package/packages/codex-kit/templates/.agents/skills/team-collab-desktop/SKILL.md +3 -3
  30. package/packages/peer/config.mjs +7 -3
  31. package/packages/peer/protocol.mjs +22 -7
  32. package/packages/peer/registry.mjs +133 -0
  33. package/packages/peer/review.mjs +52 -0
  34. package/packages/peer/service.mjs +24 -16
  35. package/packages/peer/store.mjs +59 -7
  36. package/schemas/encrypted-invite.schema.json +42 -0
  37. package/schemas/event.schema.json +51 -0
  38. package/schemas/peer-event.schema.json +44 -0
  39. package/schemas/peer-task.schema.json +21 -0
  40. package/schemas/task.schema.json +18 -0
  41. package/schemas/team-enrollment.schema.json +61 -0
  42. package/schemas/team-invitation.schema.json +108 -0
  43. package/schemas/team-policy.schema.json +182 -0
  44. package/schemas/team-version.schema.json +70 -0
  45. package/scripts/peer/cli.mjs +5 -2
  46. package/scripts/peer/onboarding.mjs +16 -4
@@ -5,18 +5,27 @@ import { join, resolve } from 'node:path';
5
5
  import { randomUUID } from 'node:crypto';
6
6
  import { digest, makeSigner, validateTeam } from './config.mjs';
7
7
  import { reducePeerTask, taskIdPattern } from './protocol.mjs';
8
+ import { readRegistry, recordHash, registryPath } from './registry.mjs';
8
9
  const exec = promisify(execFile);
9
10
  const pause = (ms) => new Promise((r) => setTimeout(r, ms));
10
11
  export async function git(cwd, ...args) {
11
- const { stdout } = await exec('git', ['-c', 'core.hooksPath=/dev/null', '-C', cwd, ...args], { env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, timeout: 30000, maxBuffer: 4 * 1024 * 1024 });
12
- return stdout.trim();
12
+ try {
13
+ const { stdout } = await exec('git', ['-c', 'core.hooksPath=/dev/null', '-C', cwd, ...args], { env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, timeout: 30000, maxBuffer: 4 * 1024 * 1024 });
14
+ return stdout.trim();
15
+ } catch (error) {
16
+ // Keep stderr available to callers that can turn it into a safe, actionable message.
17
+ // Never expose the raw value directly to the browser: it may contain credentials.
18
+ error.git_args = args;
19
+ throw error;
20
+ }
13
21
  }
14
22
 
15
23
  /** Disposable ledger clone, cross-process local lock, remote fast-forward CAS and signed replay. */
16
24
  export class PeerGitStore {
17
- constructor({ repo, dataDir, team, agentId, privateKey, clock = Date.now, maxRetries = 8 }) {
25
+ constructor({ repo, dataDir, team, agentId, privateKey, clock = Date.now, maxRetries = 8, registryRoot, enrollmentOnly = false }) {
18
26
  this.repo = resolve(repo); this.dataDir = resolve(dataDir); this.checkout = join(this.dataDir, 'peer-ledger');
19
- this.team = validateTeam(team); this.agentId = agentId; this.sign = makeSigner(team, agentId, privateKey);
27
+ this.team = validateTeam(team); this.agentId = agentId; this.enrollmentOnly = enrollmentOnly; this.sign = enrollmentOnly ? null : makeSigner(team, agentId, privateKey);
28
+ this.privateKey = privateKey; this.registryRoot = registryRoot; this.registry = null;
20
29
  this.clock = clock; this.maxRetries = maxRetries; this.push = true;
21
30
  }
22
31
  async locked(fn) {
@@ -63,6 +72,23 @@ export class PeerGitStore {
63
72
  await git(this.checkout, 'checkout', '-B', 'agent-collab', 'refs/remotes/origin/agent-collab');
64
73
  await git(this.checkout, 'reset', '--hard', 'refs/remotes/origin/agent-collab');
65
74
  await git(this.checkout, 'clean', '-fd');
75
+ if (this.registryRoot) {
76
+ const registry = await readRegistry(this.checkout, this.registryRoot);
77
+ const checkpointPath = join(this.dataDir, 'team-checkpoint.json');
78
+ let checkpoint;
79
+ try { checkpoint = JSON.parse(await readFile(checkpointPath, 'utf8')); }
80
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
81
+ if (checkpoint && (registry.records.length < checkpoint.version || recordHash(registry.records[checkpoint.version - 1]) !== checkpoint.hash)) throw new Error('team registry rolled back or forked; refusing to trust it');
82
+ if (!this.enrollmentOnly) {
83
+ if (!registry.latest.team.agents[this.agentId]) throw new Error('member is not yet approved in the team registry');
84
+ this.sign = makeSigner(registry.latest.team, this.agentId, this.privateKey);
85
+ }
86
+ this.team = registry.latest.team; this.registry = registry;
87
+ const temporary = `${checkpointPath}.${process.pid}.tmp`;
88
+ await writeFile(temporary, JSON.stringify({ version: registry.latest.version, hash: recordHash(registry.latest) }) + '\n', { mode: 0o600 });
89
+ const { rename } = await import('node:fs/promises');
90
+ await rename(temporary, checkpointPath);
91
+ }
66
92
  }
67
93
  root(id) {
68
94
  if (!taskIdPattern.test(id ?? '')) throw new Error('invalid peer task ID');
@@ -74,7 +100,9 @@ export class PeerGitStore {
74
100
  const files = (await readdir(join(root, 'events'))).sort();
75
101
  if (files.some((f) => !/^\d{6}-[a-f0-9-]{36}\.json$/.test(f))) throw new Error('unexpected peer event file');
76
102
  const events = await Promise.all(files.map(async (file) => JSON.parse(await readFile(join(root, 'events', file), 'utf8'))));
77
- return { spec, events, state: reducePeerTask(spec, events, this.team) };
103
+ const team = this.registry ? this.registry.teams.get(events[0]?.team_hash) : this.team;
104
+ if (!team) throw new Error('task references an untrusted team policy');
105
+ return { spec, events, state: reducePeerTask(spec, events, team), ...(this.registry ? { team } : {}) };
78
106
  }
79
107
  async listLocal() {
80
108
  let entries;
@@ -85,7 +113,29 @@ export class PeerGitStore {
85
113
  }
86
114
  async list() { return this.locked(async () => { await this.sync(); return this.listLocal(); }); }
87
115
  async read(id) { return this.locked(async () => { await this.sync(); return this.readLocal(id); }); }
116
+ async registryTransaction(build) {
117
+ return this.locked(async () => {
118
+ for (let attempt = 0; attempt < this.maxRetries; attempt++) {
119
+ await this.sync();
120
+ const result = await build(this.registry);
121
+ if (!result.files?.length) return result.value;
122
+ for (const [path, value] of result.files) {
123
+ if (!new RegExp(`^${registryPath.replace('.', '\\.')}/(?:versions/\\d{6}|(?:invitations|requests)/[a-f0-9-]{36})\\.json$`).test(path)) throw new Error('unsupported team registry file');
124
+ const target = join(this.checkout, path);
125
+ await mkdir(join(target, '..'), { recursive: true });
126
+ await writeFile(target, JSON.stringify(value, null, 2) + '\n', { flag: 'wx' });
127
+ await git(this.checkout, 'add', '--', path);
128
+ }
129
+ await git(this.checkout, 'commit', '-m', 'collab: update signed team enrollment');
130
+ try { await git(this.checkout, 'push', 'origin', 'HEAD:refs/heads/agent-collab'); }
131
+ catch { await pause(20 * (attempt + 1)); continue; }
132
+ await this.sync(); return result.value;
133
+ }
134
+ throw new Error('team enrollment not accepted; check Git access and retry');
135
+ });
136
+ }
88
137
  async transact(id, idempotencyKey, build) {
138
+ if (this.enrollmentOnly) throw new Error('enrollment client cannot write tasks');
89
139
  if (typeof idempotencyKey !== 'string' || !idempotencyKey || idempotencyKey.length > 300) throw new Error('invalid idempotency key');
90
140
  return this.locked(async () => {
91
141
  for (let attempt = 0; attempt < this.maxRetries; attempt++) {
@@ -102,10 +152,12 @@ export class PeerGitStore {
102
152
  if (!built) return null;
103
153
  const spec = current?.spec || built.spec;
104
154
  if (!spec || spec.id !== id) throw new Error('transaction task mismatch');
105
- const event = this.sign({ schema_version: 2, task_id: id, seq: (current?.state.seq || 0) + 1,
155
+ const taskTeam = current?.team || this.team;
156
+ const sign = makeSigner(taskTeam, this.agentId, this.privateKey);
157
+ const event = sign({ schema_version: 2, task_id: id, seq: (current?.state.seq || 0) + 1,
106
158
  event_id: randomUUID(), actor_id: this.agentId, occurred_at: new Date(now).toISOString(), idempotency_key: idempotencyKey,
107
159
  ...built.event, ...(current ? {} : { spec_hash: digest(spec) }) });
108
- reducePeerTask(spec, [...(current?.events || []), event], this.team);
160
+ reducePeerTask(spec, [...(current?.events || []), event], taskTeam);
109
161
  const root = this.root(id); await mkdir(join(root, 'events'), { recursive: true });
110
162
  if (!current) await writeFile(join(root, 'spec.json'), JSON.stringify(spec, null, 2) + '\n', { flag: 'wx' });
111
163
  await writeFile(join(root, 'events', `${String(event.seq).padStart(6, '0')}-${event.event_id}.json`), JSON.stringify(event, null, 2) + '\n', { flag: 'wx' });
@@ -0,0 +1,42 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:codex-collab:encrypted-invite:v1",
4
+ "title": "Codex Collab Encrypted Private Onboarding Package v1",
5
+ "type": "object",
6
+ "additionalProperties": true,
7
+ "required": [
8
+ "schema_version",
9
+ "kind",
10
+ "salt",
11
+ "iv",
12
+ "tag",
13
+ "ciphertext"
14
+ ],
15
+ "properties": {
16
+ "schema_version": {
17
+ "const": 1
18
+ },
19
+ "kind": {
20
+ "const": "codex-collab-encrypted-invite"
21
+ },
22
+ "salt": {
23
+ "type": "string",
24
+ "contentEncoding": "base64"
25
+ },
26
+ "iv": {
27
+ "type": "string",
28
+ "contentEncoding": "base64"
29
+ },
30
+ "tag": {
31
+ "type": "string",
32
+ "contentEncoding": "base64"
33
+ },
34
+ "ciphertext": {
35
+ "type": "string",
36
+ "minLength": 1,
37
+ "maxLength": 500000,
38
+ "contentEncoding": "base64"
39
+ }
40
+ },
41
+ "$comment": "This file is private, not a Git ledger record. Runtime validates decoded salt=16 bytes, IV=12 bytes and tag=16 bytes; scrypt derives AES-256-GCM key from a separate password, then verifies signed history and invitation in the decrypted payload."
42
+ }
@@ -0,0 +1,51 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:codex-collab:event:v1",
4
+ "title": "Codex Collab Event v1",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schema_version", "event_id", "task_id", "seq", "type", "actor_id", "occurred_at", "idempotency_key"],
8
+ "allOf": [{
9
+ "if": { "properties": { "type": { "const": "handoff" } }, "required": ["type"] },
10
+ "then": {
11
+ "required": ["summary", "refs"],
12
+ "properties": {
13
+ "summary": { "type": "string", "minLength": 1, "maxLength": 4000 },
14
+ "refs": {
15
+ "type": "object", "required": ["branch", "commit", "tests"],
16
+ "properties": {
17
+ "branch": { "type": "string", "pattern": "^codex/[A-Za-z0-9][A-Za-z0-9._/-]{0,150}$" },
18
+ "commit": { "type": "string", "pattern": "^[a-fA-F0-9]{40}([a-fA-F0-9]{24})?$" },
19
+ "tests": { "type": "string", "minLength": 1, "maxLength": 1000 }
20
+ }
21
+ }
22
+ }
23
+ }
24
+ }],
25
+ "properties": {
26
+ "schema_version": { "const": 1 },
27
+ "event_id": { "type": "string", "minLength": 1 },
28
+ "task_id": { "type": "string", "pattern": "^[A-Z][A-Z0-9-]{1,63}$" },
29
+ "seq": { "type": "integer", "minimum": 1 },
30
+ "type": { "enum": ["created", "assigned", "run_started", "progress", "blocked", "run_failed", "handoff", "review_requested", "review_started", "review_note", "reviewed", "completed", "cancelled"] },
31
+ "actor_id": { "type": "string", "minLength": 1 },
32
+ "occurred_at": { "type": "string", "format": "date-time" },
33
+ "idempotency_key": { "type": "string", "minLength": 1 },
34
+ "summary": { "type": "string", "maxLength": 4000 },
35
+ "assignee": { "type": "string", "minLength": 1 },
36
+ "chat_id": { "type": "string", "minLength": 1 },
37
+ "channel": { "enum": ["feishu", "wecom"] },
38
+ "run_id": { "type": "string", "minLength": 1 },
39
+ "decision": { "enum": ["approved", "changes_requested"] },
40
+ "refs": {
41
+ "type": "object",
42
+ "additionalProperties": false,
43
+ "properties": {
44
+ "branch": { "type": "string" },
45
+ "commit": { "type": "string" },
46
+ "pr": { "type": "string" },
47
+ "tests": { "type": "string" }
48
+ }
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:codex-collab:peer-event:v2",
4
+ "title": "Codex Collab Signed Peer Event v2",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schema_version", "event_id", "task_id", "seq", "type", "actor_id", "occurred_at", "idempotency_key", "team_hash", "signature"],
8
+ "properties": {
9
+ "schema_version": { "const": 2 },
10
+ "event_id": { "type": "string", "minLength": 1 },
11
+ "task_id": { "type": "string", "pattern": "^T-[A-Z0-9-]{1,62}$" },
12
+ "seq": { "type": "integer", "minimum": 1 },
13
+ "type": { "enum": ["created", "desktop_claimed", "claimed", "renewed", "progress", "checkpoint", "handoff", "stage_review_requested", "review_requested", "run_failed", "blocked", "reopened", "reviewed", "completed", "cancelled"] },
14
+ "actor_id": { "type": "string", "pattern": "^[a-zA-Z0-9_-]{1,64}$" },
15
+ "occurred_at": { "type": "string", "format": "date-time", "pattern": "Z$" },
16
+ "idempotency_key": { "type": "string", "minLength": 1 },
17
+ "team_hash": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
18
+ "signature": { "type": "string", "minLength": 1 },
19
+ "spec_hash": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
20
+ "epoch": { "type": "integer", "minimum": 1 },
21
+ "lease_id": { "type": "string", "minLength": 1 },
22
+ "expires_at": { "type": "string", "format": "date-time" },
23
+ "summary": { "type": "string", "maxLength": 4000 },
24
+ "next_stage": { "type": "integer", "minimum": 1 },
25
+ "decision": { "enum": ["approved", "changes_requested"] },
26
+ "refs": {
27
+ "type": "object", "additionalProperties": false, "required": ["branch", "commit", "tests"],
28
+ "properties": {
29
+ "branch": { "type": "string", "pattern": "^codex/[A-Za-z0-9][A-Za-z0-9._/-]{0,150}$" },
30
+ "commit": { "type": "string", "pattern": "^[a-fA-F0-9]{40}([a-fA-F0-9]{24})?$" },
31
+ "tests": { "type": "string", "minLength": 1, "maxLength": 1000, "pattern": "\\S" }
32
+ }
33
+ }
34
+ },
35
+ "allOf": [
36
+ { "if": { "properties": { "type": { "const": "created" } }, "required": ["type"] }, "then": { "required": ["spec_hash"] } },
37
+ { "if": { "properties": { "type": { "enum": ["claimed", "renewed"] } }, "required": ["type"] }, "then": { "required": ["epoch", "lease_id", "expires_at"] } },
38
+ { "if": { "properties": { "type": { "enum": ["checkpoint", "handoff", "stage_review_requested", "review_requested", "completed"] } }, "required": ["type"] }, "then": { "required": ["refs"] } },
39
+ { "if": { "properties": { "type": { "enum": ["handoff", "stage_review_requested"] } }, "required": ["type"] }, "then": { "required": ["summary", "next_stage"], "properties": { "summary": { "minLength": 1, "pattern": "\\S" } } } },
40
+ { "if": { "properties": { "type": { "const": "reviewed" } }, "required": ["type"] }, "then": { "required": ["decision"] } },
41
+ { "if": { "properties": { "type": { "const": "reopened" } }, "required": ["type"] }, "then": { "required": ["summary"], "properties": { "summary": { "minLength": 1, "pattern": "\\S" } } } }
42
+ ],
43
+ "$comment": "Runtime additionally verifies Ed25519 signatures, pinned team hash, event sequence, role/route/capacity, exact shared branch-tip SHA and state transitions. Manual desktop reservations have no lease and require manual continuation or cancellation. desktop_poll tasks use the claimed/renewed lease events and existing fencing; bound conversation IDs stay private, never in event fields or idempotency keys. Team policy v3 requires nonempty rejection feedback and a current expected_seq on human review writes; runtime checks these policy-dependent requirements."
44
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:codex-collab:peer-task:v2",
4
+ "title": "Codex Collab Peer Task v2",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schema_version", "id", "repo", "title", "base_branch", "acceptance", "workflow"],
8
+ "properties": {
9
+ "schema_version": { "const": 2 },
10
+ "id": { "type": "string", "pattern": "^T-[A-Z0-9-]{1,62}$" },
11
+ "repo": { "type": "string", "minLength": 1 },
12
+ "title": { "type": "string", "minLength": 1, "maxLength": 200, "pattern": "\\S" },
13
+ "description": { "type": "string", "maxLength": 4000 },
14
+ "base_branch": { "const": "main" },
15
+ "execution_mode": { "enum": ["desktop", "automatic", "desktop_poll"] },
16
+ "acceptance": { "type": "array", "minItems": 1, "maxItems": 32, "items": { "type": "string", "minLength": 1, "maxLength": 1000, "pattern": "\\S" } },
17
+ "workflow": { "type": "array", "minItems": 1, "maxItems": 16, "items": { "type": "string", "pattern": "^[a-zA-Z0-9_-]{1,64}$" } },
18
+ "source": { "type": "object", "additionalProperties": false, "required": ["channel", "chat_id"], "properties": { "channel": { "const": "wecom" }, "chat_id": { "type": "string", "minLength": 1 } } }
19
+ },
20
+ "$comment": "Runtime additionally verifies repo identity and every workflow route against the pinned team policy. desktop_poll starts open and uses signed execution leases for existing-thread heartbeat workers; automatic SDK dispatch excludes that mode."
21
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:codex-collab:task:v1",
4
+ "title": "Codex Collab Task v1",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schema_version", "id", "repo", "title", "base_branch", "acceptance"],
8
+ "properties": {
9
+ "schema_version": { "const": 1 },
10
+ "id": { "type": "string", "pattern": "^[A-Z][A-Z0-9-]{1,63}$" },
11
+ "repo": { "type": "string", "minLength": 1 },
12
+ "title": { "type": "string", "minLength": 1, "maxLength": 200 },
13
+ "description": { "type": "string" },
14
+ "base_branch": { "type": "string", "minLength": 1 },
15
+ "acceptance": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } },
16
+ "source": { "type": "object", "additionalProperties": { "type": "string" } }
17
+ }
18
+ }
@@ -0,0 +1,61 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:codex-collab:team-enrollment:v1",
4
+ "title": "Codex Collab Dual-Signed Enrollment Request v1",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "schema_version",
9
+ "request_id",
10
+ "invite_id",
11
+ "agent_id",
12
+ "public_key",
13
+ "wecom_userid",
14
+ "chat_id",
15
+ "signature",
16
+ "invitation_signature"
17
+ ],
18
+ "properties": {
19
+ "schema_version": {
20
+ "const": 1
21
+ },
22
+ "request_id": {
23
+ "type": "string",
24
+ "pattern": "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$"
25
+ },
26
+ "invite_id": {
27
+ "type": "string",
28
+ "pattern": "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$"
29
+ },
30
+ "agent_id": {
31
+ "type": "string",
32
+ "pattern": "^[a-zA-Z0-9_-]{1,64}$"
33
+ },
34
+ "public_key": {
35
+ "type": "string",
36
+ "minLength": 1
37
+ },
38
+ "wecom_userid": {
39
+ "type": [
40
+ "string",
41
+ "null"
42
+ ],
43
+ "minLength": 1,
44
+ "maxLength": 200
45
+ },
46
+ "chat_id": {
47
+ "type": "string",
48
+ "minLength": 1,
49
+ "maxLength": 200
50
+ },
51
+ "signature": {
52
+ "type": "string",
53
+ "minLength": 1
54
+ },
55
+ "invitation_signature": {
56
+ "type": "string",
57
+ "minLength": 1
58
+ }
59
+ },
60
+ "$comment": "Runtime checks both member and invitation claim signatures, invite identity, allowed chat, expiry and userid semantics. Admin approval consumes invite_id once in a team version."
61
+ }
@@ -0,0 +1,108 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:codex-collab:team-invitation:v1",
4
+ "title": "Codex Collab Signed Member Invitation v1",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "schema_version",
9
+ "invite_id",
10
+ "agent_id",
11
+ "capabilities",
12
+ "bot_id",
13
+ "chat_ids",
14
+ "commands_enabled",
15
+ "prefer",
16
+ "claim_public_key",
17
+ "expires_at",
18
+ "team_hash",
19
+ "actor_id",
20
+ "signature"
21
+ ],
22
+ "properties": {
23
+ "schema_version": {
24
+ "const": 1
25
+ },
26
+ "invite_id": {
27
+ "type": "string",
28
+ "pattern": "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$"
29
+ },
30
+ "agent_id": {
31
+ "type": "string",
32
+ "pattern": "^[a-zA-Z0-9_-]{1,64}$"
33
+ },
34
+ "capabilities": {
35
+ "type": "array",
36
+ "minItems": 1,
37
+ "maxItems": 16,
38
+ "uniqueItems": true,
39
+ "items": {
40
+ "type": "string",
41
+ "pattern": "^[a-zA-Z0-9_-]{1,64}$"
42
+ }
43
+ },
44
+ "bot_id": {
45
+ "type": "string",
46
+ "minLength": 1,
47
+ "maxLength": 200
48
+ },
49
+ "chat_ids": {
50
+ "type": "array",
51
+ "minItems": 1,
52
+ "items": {
53
+ "type": "string",
54
+ "minLength": 1,
55
+ "maxLength": 200
56
+ }
57
+ },
58
+ "commands_enabled": {
59
+ "type": "boolean"
60
+ },
61
+ "prefer": {
62
+ "type": "boolean"
63
+ },
64
+ "claim_public_key": {
65
+ "type": "string",
66
+ "minLength": 1
67
+ },
68
+ "expires_at": {
69
+ "type": "string",
70
+ "format": "date-time"
71
+ },
72
+ "team_hash": {
73
+ "type": "string",
74
+ "pattern": "^[a-f0-9]{64}$"
75
+ },
76
+ "actor_id": {
77
+ "type": "string",
78
+ "pattern": "^[a-zA-Z0-9_-]{1,64}$"
79
+ },
80
+ "signature": {
81
+ "type": "string",
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."
89
+ }
90
+ },
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
+ ]
108
+ }
@@ -0,0 +1,182 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:codex-collab:team-policy:v3",
4
+ "title": "Codex Collab Peer Team Policy v2/v3",
5
+ "type": "object",
6
+ "additionalProperties": true,
7
+ "required": [
8
+ "schema_version",
9
+ "repo",
10
+ "default_route",
11
+ "reviewer",
12
+ "agents",
13
+ "routes",
14
+ "policy"
15
+ ],
16
+ "properties": {
17
+ "schema_version": {
18
+ "enum": [
19
+ 2,
20
+ 3
21
+ ]
22
+ },
23
+ "repo": {
24
+ "type": "string",
25
+ "minLength": 1
26
+ },
27
+ "default_route": {
28
+ "type": "string",
29
+ "pattern": "^[a-zA-Z0-9_-]{1,64}$"
30
+ },
31
+ "reviewer": {
32
+ "type": "string",
33
+ "pattern": "^[a-zA-Z0-9_-]{1,64}$"
34
+ },
35
+ "agents": {
36
+ "type": "object",
37
+ "minProperties": 1,
38
+ "propertyNames": {
39
+ "type": "string",
40
+ "pattern": "^[a-zA-Z0-9_-]{1,64}$"
41
+ },
42
+ "additionalProperties": {
43
+ "type": "object",
44
+ "additionalProperties": true,
45
+ "required": [
46
+ "role",
47
+ "capabilities",
48
+ "max_concurrent_tasks",
49
+ "public_key"
50
+ ],
51
+ "properties": {
52
+ "role": {
53
+ "enum": [
54
+ "admin",
55
+ "member"
56
+ ]
57
+ },
58
+ "capabilities": {
59
+ "type": "array",
60
+ "items": {
61
+ "type": "string",
62
+ "pattern": "^[a-zA-Z0-9_-]{1,64}$"
63
+ }
64
+ },
65
+ "max_concurrent_tasks": {
66
+ "type": "integer",
67
+ "minimum": 1,
68
+ "maximum": 16
69
+ },
70
+ "public_key": {
71
+ "type": "string",
72
+ "minLength": 1
73
+ },
74
+ "wecom_userid": {
75
+ "type": "string",
76
+ "minLength": 1
77
+ }
78
+ }
79
+ }
80
+ },
81
+ "routes": {
82
+ "type": "object",
83
+ "propertyNames": {
84
+ "type": "string",
85
+ "pattern": "^[a-zA-Z0-9_-]{1,64}$"
86
+ },
87
+ "additionalProperties": {
88
+ "type": "array",
89
+ "minItems": 1,
90
+ "uniqueItems": true,
91
+ "items": {
92
+ "type": "string",
93
+ "pattern": "^[a-zA-Z0-9_-]{1,64}$"
94
+ }
95
+ }
96
+ },
97
+ "policy": {
98
+ "type": "object",
99
+ "additionalProperties": true,
100
+ "required": [
101
+ "claim_timeout_ms",
102
+ "lease_ms",
103
+ "clock_skew_ms",
104
+ "max_attempts"
105
+ ],
106
+ "properties": {
107
+ "claim_timeout_ms": {
108
+ "type": "integer",
109
+ "minimum": 10,
110
+ "maximum": 3600000
111
+ },
112
+ "lease_ms": {
113
+ "type": "integer",
114
+ "minimum": 100,
115
+ "maximum": 3600000
116
+ },
117
+ "clock_skew_ms": {
118
+ "type": "integer",
119
+ "minimum": 0,
120
+ "maximum": 60000
121
+ },
122
+ "max_attempts": {
123
+ "type": "integer",
124
+ "minimum": 1,
125
+ "maximum": 20
126
+ }
127
+ }
128
+ },
129
+ "review_rules": {
130
+ "type": "object",
131
+ "propertyNames": {
132
+ "pattern": "^[a-zA-Z0-9_-]{1,64}$"
133
+ },
134
+ "additionalProperties": {
135
+ "type": "object",
136
+ "additionalProperties": false,
137
+ "required": [
138
+ "required",
139
+ "reviewer",
140
+ "notify"
141
+ ],
142
+ "properties": {
143
+ "required": {
144
+ "type": "boolean"
145
+ },
146
+ "reviewer": {
147
+ "type": "string",
148
+ "pattern": "^(owner|admin|member:[a-zA-Z0-9_-]{1,64})$"
149
+ },
150
+ "notify": {
151
+ "type": "string",
152
+ "pattern": "^(owner|reviewer|none|member:[a-zA-Z0-9_-]{1,64})$"
153
+ }
154
+ }
155
+ }
156
+ }
157
+ },
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
+ "allOf": [
160
+ {
161
+ "if": {
162
+ "properties": {
163
+ "schema_version": {
164
+ "const": 3
165
+ }
166
+ }
167
+ },
168
+ "then": {
169
+ "required": [
170
+ "review_rules"
171
+ ]
172
+ },
173
+ "else": {
174
+ "not": {
175
+ "required": [
176
+ "review_rules"
177
+ ]
178
+ }
179
+ }
180
+ }
181
+ ]
182
+ }