zyndo 0.2.1 → 0.3.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/dist/commands/referee.d.ts +8 -0
- package/dist/commands/referee.js +86 -0
- package/dist/config.d.ts +28 -0
- package/dist/config.js +106 -2
- package/dist/connection.d.ts +27 -1
- package/dist/connection.js +28 -10
- package/dist/identity.d.ts +23 -0
- package/dist/identity.js +51 -0
- package/dist/index.js +6 -0
- package/dist/init.js +346 -10
- package/dist/scopeContract.d.ts +44 -0
- package/dist/scopeContract.js +148 -0
- package/dist/sellerDaemon.js +123 -23
- package/dist/state.d.ts +2 -0
- package/dist/state.js +23 -0
- package/package.json +1 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type SellerConfig } from '../config.js';
|
|
2
|
+
export type RefereeCommandDeps = Readonly<{
|
|
3
|
+
loadConfig: () => SellerConfig;
|
|
4
|
+
httpFetch: typeof fetch;
|
|
5
|
+
stderr: (msg: string) => void;
|
|
6
|
+
stdout: (msg: string) => void;
|
|
7
|
+
}>;
|
|
8
|
+
export declare function handleRefereeCommand(args: ReadonlyArray<string>, deps?: RefereeCommandDeps): Promise<void>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// zyndo referee — Slice 3b
|
|
3
|
+
//
|
|
4
|
+
// Opt a skill into (or out of) Kimi K2.5 referee sampling. The command
|
|
5
|
+
// connects as the configured seller to obtain a short-lived session token,
|
|
6
|
+
// then POSTs to /agent/skills/:skillId/referee.
|
|
7
|
+
//
|
|
8
|
+
// Both the config loader and HTTP client are injectable so the unit test can
|
|
9
|
+
// drive the command without hitting the network.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
import { loadSellerConfig } from '../config.js';
|
|
12
|
+
const DEFAULT_DEPS = {
|
|
13
|
+
loadConfig: () => loadSellerConfig(),
|
|
14
|
+
httpFetch: fetch,
|
|
15
|
+
stderr: (msg) => process.stderr.write(msg),
|
|
16
|
+
stdout: (msg) => process.stdout.write(msg)
|
|
17
|
+
};
|
|
18
|
+
export async function handleRefereeCommand(args, deps = DEFAULT_DEPS) {
|
|
19
|
+
const [subcommand, skillId] = args;
|
|
20
|
+
if (subcommand !== 'enable' && subcommand !== 'disable') {
|
|
21
|
+
deps.stderr('Usage: zyndo referee <enable|disable> <skillId>\n');
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (skillId === undefined || skillId.length === 0) {
|
|
26
|
+
deps.stderr('Missing skillId. Usage: zyndo referee <enable|disable> <skillId>\n');
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const enabled = subcommand === 'enable';
|
|
31
|
+
let config;
|
|
32
|
+
try {
|
|
33
|
+
config = deps.loadConfig();
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
deps.stderr(`Failed to load seller config: ${err.message}\n`);
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// Step 1 — fresh connect to obtain a session bearer token. We re-use the
|
|
41
|
+
// existing seller config (skills, categories, name). The broker will
|
|
42
|
+
// register a new ephemeral session; this is fine for a one-shot CLI call.
|
|
43
|
+
const connectRes = await deps.httpFetch(`${config.bridgeUrl}/agent/connect`, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: {
|
|
46
|
+
'content-type': 'application/json',
|
|
47
|
+
'x-zyndo-api-key': config.apiKey
|
|
48
|
+
},
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
role: 'seller',
|
|
51
|
+
name: config.name,
|
|
52
|
+
description: config.description,
|
|
53
|
+
skills: config.skills.map((s) => ({
|
|
54
|
+
id: s.id,
|
|
55
|
+
name: s.name,
|
|
56
|
+
description: s.description,
|
|
57
|
+
priceCents: s.priceCents
|
|
58
|
+
})),
|
|
59
|
+
categories: config.categories,
|
|
60
|
+
maxConcurrentTasks: config.maxConcurrentTasks
|
|
61
|
+
})
|
|
62
|
+
});
|
|
63
|
+
if (!connectRes.ok) {
|
|
64
|
+
const body = await connectRes.text().catch(() => '');
|
|
65
|
+
deps.stderr(`Connect failed (${connectRes.status}): ${body}\n`);
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const session = (await connectRes.json());
|
|
70
|
+
// Step 2 — POST the opt-in state.
|
|
71
|
+
const refereeRes = await deps.httpFetch(`${config.bridgeUrl}/agent/skills/${encodeURIComponent(skillId)}/referee`, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: {
|
|
74
|
+
'content-type': 'application/json',
|
|
75
|
+
authorization: `Bearer ${session.token}`
|
|
76
|
+
},
|
|
77
|
+
body: JSON.stringify({ enabled })
|
|
78
|
+
});
|
|
79
|
+
if (!refereeRes.ok) {
|
|
80
|
+
const body = await refereeRes.text().catch(() => '');
|
|
81
|
+
deps.stderr(`Referee ${subcommand} failed (${refereeRes.status}): ${body}\n`);
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
deps.stdout(`Referee ${enabled ? 'enabled' : 'disabled'} for skill ${skillId}.\n`);
|
|
86
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -2,15 +2,42 @@ export type ProviderName = 'anthropic' | 'openai' | 'ollama' | 'claude-code';
|
|
|
2
2
|
export type HarnessName = 'claude' | 'codex' | 'generic';
|
|
3
3
|
export declare const MIN_SKILL_PRICE_CENTS = 10;
|
|
4
4
|
export declare const MAX_SKILL_PRICE_CENTS = 1000000;
|
|
5
|
+
export type DeliverableItemConfig = Readonly<{
|
|
6
|
+
name: string;
|
|
7
|
+
quantity: number;
|
|
8
|
+
unit: string;
|
|
9
|
+
format: string;
|
|
10
|
+
specHints?: string;
|
|
11
|
+
}>;
|
|
12
|
+
export type RevisionPolicyConfig = Readonly<{
|
|
13
|
+
maxRevisions: number;
|
|
14
|
+
revisionScope: 'same-deliverable' | 'minor-tweaks-only';
|
|
15
|
+
}>;
|
|
16
|
+
export type SkillDeliverablesConfig = Readonly<{
|
|
17
|
+
summary: string;
|
|
18
|
+
items: ReadonlyArray<DeliverableItemConfig>;
|
|
19
|
+
inScope: ReadonlyArray<string>;
|
|
20
|
+
outOfScope: ReadonlyArray<string>;
|
|
21
|
+
revisionPolicy: RevisionPolicyConfig;
|
|
22
|
+
turnaroundHours: number;
|
|
23
|
+
}>;
|
|
5
24
|
export type SellerSkillConfig = Readonly<{
|
|
6
25
|
id: string;
|
|
7
26
|
name: string;
|
|
8
27
|
description: string;
|
|
9
28
|
priceCents: number;
|
|
29
|
+
deliverables?: SkillDeliverablesConfig;
|
|
10
30
|
}>;
|
|
31
|
+
export declare const MAX_DELIVERABLE_QUANTITY = 1000;
|
|
32
|
+
export declare const MAX_DELIVERABLE_ITEMS = 10;
|
|
33
|
+
export declare const MIN_SCOPE_BULLETS = 3;
|
|
34
|
+
export declare const MAX_SCOPE_BULLETS = 10;
|
|
35
|
+
export declare const MAX_TURNAROUND_HOURS = 168;
|
|
36
|
+
export declare const MAX_REVISIONS = 5;
|
|
11
37
|
export type SellerConfig = Readonly<{
|
|
12
38
|
apiKey: string;
|
|
13
39
|
bridgeUrl: string;
|
|
40
|
+
id?: string;
|
|
14
41
|
name: string;
|
|
15
42
|
description: string;
|
|
16
43
|
skills: ReadonlyArray<SellerSkillConfig>;
|
|
@@ -26,6 +53,7 @@ export type SellerConfig = Readonly<{
|
|
|
26
53
|
claudeCodeBinary?: string;
|
|
27
54
|
claudeCodeTimeoutMs?: number;
|
|
28
55
|
claudeCodeMaxBudgetUsd?: number;
|
|
56
|
+
identityKeyPath?: string;
|
|
29
57
|
}>;
|
|
30
58
|
export type BuyerConfig = Readonly<{
|
|
31
59
|
apiKey: string;
|
package/dist/config.js
CHANGED
|
@@ -6,6 +6,13 @@ import { parse as parseYaml } from 'yaml';
|
|
|
6
6
|
// in sync if the contract floor or cap ever changes.
|
|
7
7
|
export const MIN_SKILL_PRICE_CENTS = 10;
|
|
8
8
|
export const MAX_SKILL_PRICE_CENTS = 10_000_00;
|
|
9
|
+
// Keep in sync with @zyndo/contracts SkillDeliverables caps.
|
|
10
|
+
export const MAX_DELIVERABLE_QUANTITY = 1_000;
|
|
11
|
+
export const MAX_DELIVERABLE_ITEMS = 10;
|
|
12
|
+
export const MIN_SCOPE_BULLETS = 3;
|
|
13
|
+
export const MAX_SCOPE_BULLETS = 10;
|
|
14
|
+
export const MAX_TURNAROUND_HOURS = 168;
|
|
15
|
+
export const MAX_REVISIONS = 5;
|
|
9
16
|
// ---------------------------------------------------------------------------
|
|
10
17
|
// Loader
|
|
11
18
|
// ---------------------------------------------------------------------------
|
|
@@ -30,6 +37,10 @@ export function loadSellerConfig(configPath) {
|
|
|
30
37
|
const data = parseYaml(raw);
|
|
31
38
|
const apiKey = optionalString(data, 'api_key') ?? '';
|
|
32
39
|
const bridgeUrl = optionalString(data, 'bridge_url') ?? DEFAULT_BRIDGE_URL;
|
|
40
|
+
const id = optionalString(data, 'id');
|
|
41
|
+
if (id !== undefined && !/^[a-z0-9][a-z0-9-]*[a-z0-9]$/i.test(id)) {
|
|
42
|
+
throw new Error(`Config: "id" must be a slug (letters, digits, hyphens; must start and end alphanumeric). Got: "${id}"`);
|
|
43
|
+
}
|
|
33
44
|
const name = requireString(data, 'name');
|
|
34
45
|
const description = requireString(data, 'description');
|
|
35
46
|
const provider = requireEnum(data, 'provider', ['anthropic', 'openai', 'ollama', 'claude-code']);
|
|
@@ -77,6 +88,7 @@ export function loadSellerConfig(configPath) {
|
|
|
77
88
|
}
|
|
78
89
|
const claudeCodeTimeoutMs = typeof data.claude_code_timeout_ms === 'number' ? data.claude_code_timeout_ms : undefined;
|
|
79
90
|
const claudeCodeMaxBudgetUsd = typeof data.claude_code_max_budget_usd === 'number' ? data.claude_code_max_budget_usd : undefined;
|
|
91
|
+
const identityKeyPath = optionalString(data, 'identity_key_path');
|
|
80
92
|
const skills = requireArray(data, 'skills').map((s) => {
|
|
81
93
|
const skill = s;
|
|
82
94
|
const id = requireString(skill, 'id');
|
|
@@ -96,7 +108,8 @@ export function loadSellerConfig(configPath) {
|
|
|
96
108
|
id,
|
|
97
109
|
name: requireString(skill, 'name'),
|
|
98
110
|
description: requireString(skill, 'description'),
|
|
99
|
-
priceCents: rawPrice
|
|
111
|
+
priceCents: rawPrice,
|
|
112
|
+
deliverables: parseDeliverables(skill, id)
|
|
100
113
|
};
|
|
101
114
|
});
|
|
102
115
|
const categories = Array.isArray(data.categories)
|
|
@@ -108,6 +121,7 @@ export function loadSellerConfig(configPath) {
|
|
|
108
121
|
return {
|
|
109
122
|
apiKey,
|
|
110
123
|
bridgeUrl,
|
|
124
|
+
id,
|
|
111
125
|
name,
|
|
112
126
|
description,
|
|
113
127
|
skills,
|
|
@@ -122,7 +136,8 @@ export function loadSellerConfig(configPath) {
|
|
|
122
136
|
harness,
|
|
123
137
|
claudeCodeBinary,
|
|
124
138
|
claudeCodeTimeoutMs,
|
|
125
|
-
claudeCodeMaxBudgetUsd
|
|
139
|
+
claudeCodeMaxBudgetUsd,
|
|
140
|
+
identityKeyPath
|
|
126
141
|
};
|
|
127
142
|
}
|
|
128
143
|
// ---------------------------------------------------------------------------
|
|
@@ -146,6 +161,95 @@ function requireEnum(data, key, allowed) {
|
|
|
146
161
|
}
|
|
147
162
|
return value;
|
|
148
163
|
}
|
|
164
|
+
function parseDeliverables(skill, skillId) {
|
|
165
|
+
const raw = skill.deliverables;
|
|
166
|
+
if (raw === undefined || raw === null) {
|
|
167
|
+
process.stderr.write(`[zyndo] Warning: skill "${skillId}" has no "deliverables" block. The broker will hide this skill from the marketplace until you publish a scope contract. Run \`zyndo init\` and follow the deliverables prompts, or edit seller.yaml directly. See skills.md "Scope Discipline".\n`);
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
171
|
+
throw new Error(`Skill "${skillId}": "deliverables" must be an object.`);
|
|
172
|
+
}
|
|
173
|
+
const d = raw;
|
|
174
|
+
const summary = requireString(d, 'summary');
|
|
175
|
+
if (summary.length > 240) {
|
|
176
|
+
throw new Error(`Skill "${skillId}": deliverables.summary must be <= 240 characters.`);
|
|
177
|
+
}
|
|
178
|
+
const rawItems = d.items;
|
|
179
|
+
if (!Array.isArray(rawItems) || rawItems.length === 0) {
|
|
180
|
+
throw new Error(`Skill "${skillId}": deliverables.items must be a non-empty array.`);
|
|
181
|
+
}
|
|
182
|
+
if (rawItems.length > MAX_DELIVERABLE_ITEMS) {
|
|
183
|
+
throw new Error(`Skill "${skillId}": deliverables.items must have at most ${MAX_DELIVERABLE_ITEMS} entries.`);
|
|
184
|
+
}
|
|
185
|
+
const items = rawItems.map((it, idx) => {
|
|
186
|
+
if (typeof it !== 'object' || it === null || Array.isArray(it)) {
|
|
187
|
+
throw new Error(`Skill "${skillId}": deliverables.items[${idx}] must be an object.`);
|
|
188
|
+
}
|
|
189
|
+
const item = it;
|
|
190
|
+
const name = requireString(item, 'name');
|
|
191
|
+
const quantity = item.quantity;
|
|
192
|
+
if (typeof quantity !== 'number' || !Number.isInteger(quantity) || quantity < 1 || quantity > MAX_DELIVERABLE_QUANTITY) {
|
|
193
|
+
throw new Error(`Skill "${skillId}": deliverables.items[${idx}].quantity must be an integer between 1 and ${MAX_DELIVERABLE_QUANTITY}.`);
|
|
194
|
+
}
|
|
195
|
+
const unit = requireString(item, 'unit');
|
|
196
|
+
const format = requireString(item, 'format');
|
|
197
|
+
const specHints = optionalString(item, 'spec_hints') ?? optionalString(item, 'specHints');
|
|
198
|
+
return specHints !== undefined
|
|
199
|
+
? { name, quantity, unit, format, specHints }
|
|
200
|
+
: { name, quantity, unit, format };
|
|
201
|
+
});
|
|
202
|
+
const inScope = parseScopeBullets(d, 'in_scope', skillId) ?? parseScopeBullets(d, 'inScope', skillId);
|
|
203
|
+
const outOfScope = parseScopeBullets(d, 'out_of_scope', skillId) ?? parseScopeBullets(d, 'outOfScope', skillId);
|
|
204
|
+
if (inScope === undefined) {
|
|
205
|
+
throw new Error(`Skill "${skillId}": deliverables.in_scope must be an array of ${MIN_SCOPE_BULLETS}-${MAX_SCOPE_BULLETS} strings.`);
|
|
206
|
+
}
|
|
207
|
+
if (outOfScope === undefined) {
|
|
208
|
+
throw new Error(`Skill "${skillId}": deliverables.out_of_scope must be an array of ${MIN_SCOPE_BULLETS}-${MAX_SCOPE_BULLETS} strings.`);
|
|
209
|
+
}
|
|
210
|
+
const rawRevision = d.revision_policy ?? d.revisionPolicy;
|
|
211
|
+
if (typeof rawRevision !== 'object' || rawRevision === null || Array.isArray(rawRevision)) {
|
|
212
|
+
throw new Error(`Skill "${skillId}": deliverables.revision_policy must be an object with max_revisions and revision_scope.`);
|
|
213
|
+
}
|
|
214
|
+
const rp = rawRevision;
|
|
215
|
+
const maxRevisions = rp.max_revisions ?? rp.maxRevisions;
|
|
216
|
+
if (typeof maxRevisions !== 'number' || !Number.isInteger(maxRevisions) || maxRevisions < 0 || maxRevisions > MAX_REVISIONS) {
|
|
217
|
+
throw new Error(`Skill "${skillId}": deliverables.revision_policy.max_revisions must be an integer between 0 and ${MAX_REVISIONS}.`);
|
|
218
|
+
}
|
|
219
|
+
const revisionScope = rp.revision_scope ?? rp.revisionScope;
|
|
220
|
+
if (revisionScope !== 'same-deliverable' && revisionScope !== 'minor-tweaks-only') {
|
|
221
|
+
throw new Error(`Skill "${skillId}": deliverables.revision_policy.revision_scope must be "same-deliverable" or "minor-tweaks-only".`);
|
|
222
|
+
}
|
|
223
|
+
const turnaroundRaw = d.turnaround_hours ?? d.turnaroundHours;
|
|
224
|
+
if (typeof turnaroundRaw !== 'number' || !Number.isInteger(turnaroundRaw) || turnaroundRaw < 1 || turnaroundRaw > MAX_TURNAROUND_HOURS) {
|
|
225
|
+
throw new Error(`Skill "${skillId}": deliverables.turnaround_hours must be an integer between 1 and ${MAX_TURNAROUND_HOURS}.`);
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
summary,
|
|
229
|
+
items,
|
|
230
|
+
inScope,
|
|
231
|
+
outOfScope,
|
|
232
|
+
revisionPolicy: { maxRevisions, revisionScope },
|
|
233
|
+
turnaroundHours: turnaroundRaw
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function parseScopeBullets(d, key, skillId) {
|
|
237
|
+
const raw = d[key];
|
|
238
|
+
if (raw === undefined)
|
|
239
|
+
return undefined;
|
|
240
|
+
if (!Array.isArray(raw)) {
|
|
241
|
+
throw new Error(`Skill "${skillId}": deliverables.${key} must be an array.`);
|
|
242
|
+
}
|
|
243
|
+
if (raw.length < MIN_SCOPE_BULLETS || raw.length > MAX_SCOPE_BULLETS) {
|
|
244
|
+
throw new Error(`Skill "${skillId}": deliverables.${key} must have between ${MIN_SCOPE_BULLETS} and ${MAX_SCOPE_BULLETS} bullets.`);
|
|
245
|
+
}
|
|
246
|
+
return raw.map((b, idx) => {
|
|
247
|
+
if (typeof b !== 'string' || b.length === 0 || b.length > 200) {
|
|
248
|
+
throw new Error(`Skill "${skillId}": deliverables.${key}[${idx}] must be a non-empty string <= 200 chars.`);
|
|
249
|
+
}
|
|
250
|
+
return b;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
149
253
|
function requireArray(data, key) {
|
|
150
254
|
const value = data[key];
|
|
151
255
|
if (!Array.isArray(value) || value.length === 0) {
|
package/dist/connection.d.ts
CHANGED
|
@@ -11,6 +11,24 @@ export type AgentEvent = Readonly<{
|
|
|
11
11
|
payload: Record<string, unknown>;
|
|
12
12
|
createdAt: string;
|
|
13
13
|
}>;
|
|
14
|
+
export type DeliverableItemDetail = Readonly<{
|
|
15
|
+
name: string;
|
|
16
|
+
quantity: number;
|
|
17
|
+
unit: string;
|
|
18
|
+
format: string;
|
|
19
|
+
specHints?: string;
|
|
20
|
+
}>;
|
|
21
|
+
export type SkillDeliverablesSnapshot = Readonly<{
|
|
22
|
+
summary: string;
|
|
23
|
+
items: ReadonlyArray<DeliverableItemDetail>;
|
|
24
|
+
inScope: ReadonlyArray<string>;
|
|
25
|
+
outOfScope: ReadonlyArray<string>;
|
|
26
|
+
revisionPolicy: Readonly<{
|
|
27
|
+
maxRevisions: number;
|
|
28
|
+
revisionScope: 'same-deliverable' | 'minor-tweaks-only';
|
|
29
|
+
}>;
|
|
30
|
+
turnaroundHours: number;
|
|
31
|
+
}>;
|
|
14
32
|
export type TaskDetail = Readonly<{
|
|
15
33
|
taskId: string;
|
|
16
34
|
buyerAgentId: string;
|
|
@@ -24,6 +42,7 @@ export type TaskDetail = Readonly<{
|
|
|
24
42
|
content: string;
|
|
25
43
|
}>;
|
|
26
44
|
createdAt: string;
|
|
45
|
+
deliverablesSnapshot?: SkillDeliverablesSnapshot;
|
|
27
46
|
}>;
|
|
28
47
|
export type TaskMessage = Readonly<{
|
|
29
48
|
messageId: string;
|
|
@@ -45,6 +64,7 @@ export declare function connect(bridgeUrl: string, apiKey: string, opts: {
|
|
|
45
64
|
}>;
|
|
46
65
|
categories?: ReadonlyArray<string>;
|
|
47
66
|
maxConcurrentTasks?: number;
|
|
67
|
+
sellerSlug?: string;
|
|
48
68
|
}): Promise<AgentSession>;
|
|
49
69
|
export declare function reconnect(session: AgentSession, apiKey: string, opts?: {
|
|
50
70
|
role?: 'buyer' | 'seller';
|
|
@@ -54,7 +74,13 @@ export declare function reconnect(session: AgentSession, apiKey: string, opts?:
|
|
|
54
74
|
export declare function heartbeat(session: AgentSession): Promise<void>;
|
|
55
75
|
export declare function pollEvents(session: AgentSession, ack?: number): Promise<ReadonlyArray<AgentEvent>>;
|
|
56
76
|
export declare function acceptTask(session: AgentSession, taskId: string): Promise<void>;
|
|
57
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Slice 3b — register the seller's Ed25519 public key with the broker so
|
|
79
|
+
* subsequent signed deliveries can be verified. Safe to call on every daemon
|
|
80
|
+
* start; the broker upserts and tracks rotations.
|
|
81
|
+
*/
|
|
82
|
+
export declare function registerIdentity(session: AgentSession, publicKeyB64: string): Promise<void>;
|
|
83
|
+
export declare function deliverTask(session: AgentSession, taskId: string, content: string, signatureB64?: string): Promise<void>;
|
|
58
84
|
export declare function sendTaskMessage(session: AgentSession, taskId: string, type: 'question' | 'answer' | 'info', content: string): Promise<void>;
|
|
59
85
|
export declare function getTaskMessages(session: AgentSession, taskId: string): Promise<ReadonlyArray<TaskMessage>>;
|
|
60
86
|
export declare function getTaskDetail(session: AgentSession, taskId: string): Promise<TaskDetail | undefined>;
|
package/dist/connection.js
CHANGED
|
@@ -29,17 +29,20 @@ export async function connect(bridgeUrl, apiKey, opts) {
|
|
|
29
29
|
'content-type': 'application/json',
|
|
30
30
|
'x-zyndo-api-key': apiKey
|
|
31
31
|
};
|
|
32
|
+
const body = {
|
|
33
|
+
role: opts.role,
|
|
34
|
+
name: opts.name,
|
|
35
|
+
description: opts.description,
|
|
36
|
+
skills: opts.skills ?? [],
|
|
37
|
+
categories: opts.categories ?? [],
|
|
38
|
+
maxConcurrentTasks: opts.maxConcurrentTasks ?? 3
|
|
39
|
+
};
|
|
40
|
+
if (opts.sellerSlug !== undefined)
|
|
41
|
+
body.sellerSlug = opts.sellerSlug;
|
|
32
42
|
const res = await fetch(`${bridgeUrl}/agent/connect`, {
|
|
33
43
|
method: 'POST',
|
|
34
44
|
headers,
|
|
35
|
-
body: JSON.stringify(
|
|
36
|
-
role: opts.role,
|
|
37
|
-
name: opts.name,
|
|
38
|
-
description: opts.description,
|
|
39
|
-
skills: opts.skills ?? [],
|
|
40
|
-
categories: opts.categories ?? [],
|
|
41
|
-
maxConcurrentTasks: opts.maxConcurrentTasks ?? 3
|
|
42
|
-
})
|
|
45
|
+
body: JSON.stringify(body)
|
|
43
46
|
});
|
|
44
47
|
if (!res.ok) {
|
|
45
48
|
const body = await res.text();
|
|
@@ -104,8 +107,23 @@ export async function acceptTask(session, taskId) {
|
|
|
104
107
|
throw new Error(`Accept failed (${res.status}): ${await res.text()}`);
|
|
105
108
|
}
|
|
106
109
|
}
|
|
107
|
-
|
|
108
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Slice 3b — register the seller's Ed25519 public key with the broker so
|
|
112
|
+
* subsequent signed deliveries can be verified. Safe to call on every daemon
|
|
113
|
+
* start; the broker upserts and tracks rotations.
|
|
114
|
+
*/
|
|
115
|
+
export async function registerIdentity(session, publicKeyB64) {
|
|
116
|
+
const res = await jsonPost(`${session.bridgeUrl}/agent/identity/register`, { publicKeyB64 }, session.token);
|
|
117
|
+
if (!res.ok) {
|
|
118
|
+
throw new Error(`Identity register failed (${res.status}): ${await res.text()}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export async function deliverTask(session, taskId, content, signatureB64) {
|
|
122
|
+
const body = { output: { type: 'text', content } };
|
|
123
|
+
if (signatureB64 !== undefined) {
|
|
124
|
+
body.signature = signatureB64;
|
|
125
|
+
}
|
|
126
|
+
const res = await jsonPost(`${session.bridgeUrl}/agent/tasks/${taskId}/deliver`, body, session.token);
|
|
109
127
|
if (!res.ok) {
|
|
110
128
|
throw new Error(`Deliver failed (${res.status}): ${await res.text()}`);
|
|
111
129
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type KeyObject } from 'node:crypto';
|
|
2
|
+
export type IdentityKeypair = Readonly<{
|
|
3
|
+
publicKeyB64: string;
|
|
4
|
+
privateKey: KeyObject;
|
|
5
|
+
}>;
|
|
6
|
+
/**
|
|
7
|
+
* Load an existing Ed25519 keypair from `keyPath`, or generate and persist a
|
|
8
|
+
* new one if the file does not yet exist. The private key is written as a
|
|
9
|
+
* PKCS#8 PEM with 0600 permissions. The public key is returned as SPKI DER
|
|
10
|
+
* base64 — the exact shape the broker's `seller_identities` projection
|
|
11
|
+
* stores.
|
|
12
|
+
*/
|
|
13
|
+
export declare function ensureIdentityKeypair(keyPath: string): IdentityKeypair;
|
|
14
|
+
/**
|
|
15
|
+
* Sign a delivery payload for the broker. The signature covers
|
|
16
|
+
* `sha256(${taskId}|${content})` so the broker can reproduce the digest
|
|
17
|
+
* from the parsed request body.
|
|
18
|
+
*/
|
|
19
|
+
export declare function signDelivery(input: {
|
|
20
|
+
taskId: string;
|
|
21
|
+
content: string;
|
|
22
|
+
privateKey: KeyObject;
|
|
23
|
+
}): string;
|
package/dist/identity.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Ed25519 identity helpers for the zyndo CLI.
|
|
3
|
+
//
|
|
4
|
+
// Sellers generate a keypair once at `init` time and reuse it to sign every
|
|
5
|
+
// delivery body. The broker verifies the signature against a registered
|
|
6
|
+
// SPKI DER public key so it can prove the delivery actually came from the
|
|
7
|
+
// seller that owns the agent id, not a compromised API key.
|
|
8
|
+
//
|
|
9
|
+
// Zero external deps: everything is `node:crypto` + `node:fs`.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
import { generateKeyPairSync, createHash, createPrivateKey, createPublicKey, sign as cryptoSign } from 'node:crypto';
|
|
12
|
+
import { writeFileSync, readFileSync, mkdirSync, chmodSync, existsSync } from 'node:fs';
|
|
13
|
+
import { dirname } from 'node:path';
|
|
14
|
+
/**
|
|
15
|
+
* Load an existing Ed25519 keypair from `keyPath`, or generate and persist a
|
|
16
|
+
* new one if the file does not yet exist. The private key is written as a
|
|
17
|
+
* PKCS#8 PEM with 0600 permissions. The public key is returned as SPKI DER
|
|
18
|
+
* base64 — the exact shape the broker's `seller_identities` projection
|
|
19
|
+
* stores.
|
|
20
|
+
*/
|
|
21
|
+
export function ensureIdentityKeypair(keyPath) {
|
|
22
|
+
if (!existsSync(keyPath)) {
|
|
23
|
+
mkdirSync(dirname(keyPath), { recursive: true, mode: 0o700 });
|
|
24
|
+
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
|
25
|
+
const pem = privateKey.export({ type: 'pkcs8', format: 'pem' }).toString();
|
|
26
|
+
writeFileSync(keyPath, pem, { mode: 0o600 });
|
|
27
|
+
try {
|
|
28
|
+
chmodSync(keyPath, 0o600);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Windows chmod is a no-op for non-owner bits; ignore failures.
|
|
32
|
+
}
|
|
33
|
+
const publicKeyB64 = publicKey.export({ type: 'spki', format: 'der' }).toString('base64');
|
|
34
|
+
return { publicKeyB64, privateKey };
|
|
35
|
+
}
|
|
36
|
+
const pem = readFileSync(keyPath, 'utf8');
|
|
37
|
+
const privateKey = createPrivateKey(pem);
|
|
38
|
+
const publicKey = createPublicKey(privateKey);
|
|
39
|
+
const publicKeyB64 = publicKey.export({ type: 'spki', format: 'der' }).toString('base64');
|
|
40
|
+
return { publicKeyB64, privateKey };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Sign a delivery payload for the broker. The signature covers
|
|
44
|
+
* `sha256(${taskId}|${content})` so the broker can reproduce the digest
|
|
45
|
+
* from the parsed request body.
|
|
46
|
+
*/
|
|
47
|
+
export function signDelivery(input) {
|
|
48
|
+
const digest = createHash('sha256').update(`${input.taskId}|${input.content}`).digest();
|
|
49
|
+
const signature = cryptoSign(null, digest, input.privateKey);
|
|
50
|
+
return signature.toString('base64');
|
|
51
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { startSellerDaemon } from './sellerDaemon.js';
|
|
|
10
10
|
import { startMcpServer } from './mcp/mcpServer.js';
|
|
11
11
|
import { printBanner } from './banner.js';
|
|
12
12
|
import { handleWalletCommand, handleCashoutCommand, handleConnectCommand } from './commands/wallet.js';
|
|
13
|
+
import { handleRefereeCommand } from './commands/referee.js';
|
|
13
14
|
// Read version from package.json so it never drifts from the published
|
|
14
15
|
// npm version. dist/index.js lives at <pkg>/dist/index.js, so the package
|
|
15
16
|
// manifest is one level up. Fall back to "unknown" if the file is missing
|
|
@@ -65,6 +66,10 @@ async function main() {
|
|
|
65
66
|
handleConnectCommand(args.slice(1));
|
|
66
67
|
break;
|
|
67
68
|
}
|
|
69
|
+
case 'referee': {
|
|
70
|
+
await handleRefereeCommand(args.slice(1));
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
68
73
|
case 'version':
|
|
69
74
|
process.stdout.write(`zyndo ${getPackageVersion()}\n`);
|
|
70
75
|
break;
|
|
@@ -90,6 +95,7 @@ Usage:
|
|
|
90
95
|
zyndo wallet [balance|topup|ledger] Manage wallet (opens dashboard)
|
|
91
96
|
zyndo cashout View earnings and cashout (opens dashboard)
|
|
92
97
|
zyndo connect [onboard|status] Stripe Connect setup (opens dashboard)
|
|
98
|
+
zyndo referee <enable|disable> <skillId> Toggle Kimi K2.5 referee sampling on a skill
|
|
93
99
|
zyndo version Show version
|
|
94
100
|
zyndo help Show this help
|
|
95
101
|
|