@consensus-tools/consensus-tools 0.1.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.
@@ -0,0 +1,70 @@
1
+ import type { Assignment, Job, Resolution, Submission, Vote } from '../types';
2
+ import type { JobPostInput, ClaimInput, SubmitInput, ResolveInput, VoteInput } from '../jobs/engine';
3
+
4
+ export class ConsensusToolsClient {
5
+ constructor(private readonly baseUrl: string, private readonly accessToken: string, private readonly logger?: any) {}
6
+
7
+ async postJob(agentId: string, input: JobPostInput): Promise<Job> {
8
+ return this.request('POST', '/jobs', { agentId, ...input });
9
+ }
10
+
11
+ async listJobs(params: Record<string, string | undefined> = {}): Promise<Job[]> {
12
+ const query = new URLSearchParams();
13
+ for (const [key, value] of Object.entries(params)) {
14
+ if (value) query.set(key, value);
15
+ }
16
+ const suffix = query.toString() ? `?${query}` : '';
17
+ return this.request('GET', `/jobs${suffix}`);
18
+ }
19
+
20
+ async getJob(jobId: string): Promise<Job> {
21
+ return this.request('GET', `/jobs/${jobId}`);
22
+ }
23
+
24
+ async getStatus(jobId: string): Promise<any> {
25
+ return this.request('GET', `/jobs/${jobId}/status`);
26
+ }
27
+
28
+ async claimJob(agentId: string, jobId: string, input: ClaimInput): Promise<Assignment> {
29
+ return this.request('POST', `/jobs/${jobId}/claim`, { agentId, ...input });
30
+ }
31
+
32
+ async submitJob(agentId: string, jobId: string, input: SubmitInput): Promise<Submission> {
33
+ return this.request('POST', `/jobs/${jobId}/submit`, { agentId, ...input });
34
+ }
35
+
36
+ async vote(agentId: string, jobId: string, input: VoteInput): Promise<Vote> {
37
+ return this.request('POST', `/jobs/${jobId}/vote`, { agentId, ...input });
38
+ }
39
+
40
+ async resolveJob(agentId: string, jobId: string, input: ResolveInput): Promise<Resolution> {
41
+ return this.request('POST', `/jobs/${jobId}/resolve`, { agentId, ...input });
42
+ }
43
+
44
+ async getLedger(agentId: string): Promise<{ agentId: string; balance: number }>
45
+ {
46
+ return this.request('GET', `/ledger/${agentId}`);
47
+ }
48
+
49
+ private async request(method: string, path: string, body?: any): Promise<any> {
50
+ const url = `${this.baseUrl.replace(/\/$/, '')}${path}`;
51
+ const headers: Record<string, string> = { 'content-type': 'application/json' };
52
+ if (this.accessToken) headers.authorization = `Bearer ${this.accessToken}`;
53
+
54
+ const res = await fetch(url, {
55
+ method,
56
+ headers,
57
+ body: body ? JSON.stringify(body) : undefined
58
+ });
59
+
60
+ if (!res.ok) {
61
+ const text = await res.text();
62
+ this.logger?.warn?.({ status: res.status, path }, 'consensus-tools: network request failed');
63
+ throw new Error(`Network error ${res.status}: ${text || res.statusText}`);
64
+ }
65
+
66
+ const text = await res.text();
67
+ if (!text) return null;
68
+ return JSON.parse(text);
69
+ }
70
+ }
@@ -0,0 +1,143 @@
1
+ import http from 'node:http';
2
+ import type { ConsensusToolsConfig } from '../types';
3
+ import type { JobEngine } from '../jobs/engine';
4
+ import type { LedgerEngine } from '../ledger/ledger';
5
+
6
+ export class ConsensusToolsServer {
7
+ private server?: http.Server;
8
+
9
+ constructor(
10
+ private readonly config: ConsensusToolsConfig,
11
+ private readonly engine: JobEngine,
12
+ private readonly ledger: LedgerEngine,
13
+ private readonly logger?: any
14
+ ) {}
15
+
16
+ async start(): Promise<void> {
17
+ if (this.server) return;
18
+ const { host, port } = this.config.local.server;
19
+ this.server = http.createServer((req, res) => this.handle(req, res));
20
+
21
+ await new Promise<void>((resolve) => {
22
+ this.server?.listen(port, host, () => resolve());
23
+ });
24
+
25
+ this.logger?.info?.({ host, port }, 'consensus-tools embedded server started');
26
+ }
27
+
28
+ async stop(): Promise<void> {
29
+ if (!this.server) return;
30
+ const srv = this.server;
31
+ this.server = undefined;
32
+ await new Promise<void>((resolve, reject) => {
33
+ srv.close((err) => (err ? reject(err) : resolve()));
34
+ });
35
+ this.logger?.info?.('consensus-tools embedded server stopped');
36
+ }
37
+
38
+ private async handle(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
39
+ try {
40
+ if (this.config.local.server.authToken) {
41
+ const auth = req.headers.authorization || '';
42
+ if (auth !== `Bearer ${this.config.local.server.authToken}`) {
43
+ return this.reply(res, 401, { error: 'Unauthorized' });
44
+ }
45
+ }
46
+
47
+ const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
48
+ const path = url.pathname;
49
+ const method = req.method || 'GET';
50
+
51
+ if (method === 'POST' && path === '/jobs') {
52
+ const body = await this.readJson(req);
53
+ const job = await this.engine.postJob(body.agentId, body);
54
+ return this.reply(res, 200, job);
55
+ }
56
+
57
+ if (method === 'GET' && path === '/jobs') {
58
+ const jobs = await this.engine.listJobs({
59
+ status: url.searchParams.get('status') || undefined,
60
+ tag: url.searchParams.get('tag') || undefined,
61
+ mine: url.searchParams.get('mine') || undefined
62
+ });
63
+ return this.reply(res, 200, jobs);
64
+ }
65
+
66
+ const jobMatch = path.match(/^\/jobs\/([^/]+)(?:\/(claim|submit|vote|resolve|status))?$/);
67
+ if (jobMatch) {
68
+ const jobId = jobMatch[1];
69
+ const action = jobMatch[2];
70
+
71
+ if (method === 'GET' && !action) {
72
+ const job = await this.engine.getJob(jobId);
73
+ if (!job) return this.reply(res, 404, { error: 'Job not found' });
74
+ return this.reply(res, 200, job);
75
+ }
76
+
77
+ if (method === 'GET' && action === 'status') {
78
+ const status = await this.engine.getStatus(jobId);
79
+ return this.reply(res, 200, status);
80
+ }
81
+
82
+ if (method === 'POST' && action === 'claim') {
83
+ const body = await this.readJson(req);
84
+ const claim = await this.engine.claimJob(body.agentId, jobId, body);
85
+ return this.reply(res, 200, claim);
86
+ }
87
+
88
+ if (method === 'POST' && action === 'submit') {
89
+ const body = await this.readJson(req);
90
+ const submission = await this.engine.submitJob(body.agentId, jobId, body);
91
+ return this.reply(res, 200, submission);
92
+ }
93
+
94
+ if (method === 'POST' && action === 'vote') {
95
+ const body = await this.readJson(req);
96
+ const vote = await this.engine.vote(body.agentId, jobId, body);
97
+ return this.reply(res, 200, vote);
98
+ }
99
+
100
+ if (method === 'POST' && action === 'resolve') {
101
+ const body = await this.readJson(req);
102
+ const resolution = await this.engine.resolveJob(body.agentId, jobId, body);
103
+ return this.reply(res, 200, resolution);
104
+ }
105
+ }
106
+
107
+ const ledgerMatch = path.match(/^\/ledger\/([^/]+)$/);
108
+ if (method === 'GET' && ledgerMatch) {
109
+ const agentId = ledgerMatch[1];
110
+ const balance = await this.ledger.getBalance(agentId);
111
+ return this.reply(res, 200, { agentId, balance });
112
+ }
113
+
114
+ return this.reply(res, 404, { error: 'Not found' });
115
+ } catch (err: any) {
116
+ this.logger?.warn?.({ err }, 'consensus-tools server error');
117
+ try {
118
+ await this.engine.recordError?.(err?.message || 'Server error', { path: req.url, method: req.method });
119
+ } catch {
120
+ // ignore
121
+ }
122
+ return this.reply(res, 500, { error: err?.message || 'Server error' });
123
+ }
124
+ }
125
+
126
+ private async readJson(req: http.IncomingMessage): Promise<any> {
127
+ const chunks: Buffer[] = [];
128
+ for await (const chunk of req) {
129
+ chunks.push(Buffer.from(chunk));
130
+ if (Buffer.concat(chunks).length > 1024 * 1024) {
131
+ throw new Error('Payload too large');
132
+ }
133
+ }
134
+ const text = Buffer.concat(chunks).toString('utf8');
135
+ return text ? JSON.parse(text) : {};
136
+ }
137
+
138
+ private reply(res: http.ServerResponse, status: number, body: any): void {
139
+ res.statusCode = status;
140
+ res.setHeader('content-type', 'application/json');
141
+ res.end(JSON.stringify(body));
142
+ }
143
+ }
package/src/service.ts ADDED
@@ -0,0 +1,26 @@
1
+ import type { ConsensusToolsConfig, Job } from './types';
2
+
3
+ export interface ConsensusToolsBackend {
4
+ listJobs(filters?: Record<string, string | undefined>): Promise<Job[]>;
5
+ getJob(jobId: string): Promise<Job | undefined>;
6
+ }
7
+
8
+ export function createService(
9
+ config: ConsensusToolsConfig,
10
+ backend: ConsensusToolsBackend,
11
+ agentId: string,
12
+ capabilities: string[],
13
+ logger?: any
14
+ ) {
15
+ return {
16
+ id: 'consensus-tools-service',
17
+ start: async () => {
18
+ if (config.mode === 'global') return;
19
+ logger?.debug?.({ agentId, capabilities }, 'consensus-tools: service started');
20
+ },
21
+ stop: async () => {
22
+ if (config.mode === 'global') return;
23
+ logger?.debug?.('consensus-tools: service stopped');
24
+ }
25
+ };
26
+ }
@@ -0,0 +1,31 @@
1
+ import type { ConsensusToolsConfig, StorageState } from '../types';
2
+ import { JsonStorage } from './JsonStorage';
3
+ import { SqliteStorage } from './SqliteStorage';
4
+
5
+ export interface IStorage {
6
+ init(): Promise<void>;
7
+ getState(): Promise<StorageState>;
8
+ saveState(state: StorageState): Promise<void>;
9
+ update<T>(fn: (state: StorageState) => T | Promise<T>): Promise<{ state: StorageState; result: T }>;
10
+ }
11
+
12
+ export function defaultState(): StorageState {
13
+ return {
14
+ jobs: [],
15
+ bids: [],
16
+ claims: [],
17
+ submissions: [],
18
+ votes: [],
19
+ resolutions: [],
20
+ ledger: [],
21
+ audit: [],
22
+ errors: []
23
+ };
24
+ }
25
+
26
+ export function createStorage(config: ConsensusToolsConfig): IStorage {
27
+ if (config.local.storage.kind === 'sqlite') {
28
+ return new SqliteStorage(config.local.storage.path);
29
+ }
30
+ return new JsonStorage(config.local.storage.path);
31
+ }
@@ -0,0 +1,63 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { Mutex } from '../util/locks';
4
+ import { defaultState, IStorage } from './IStorage';
5
+ import type { StorageState } from '../types';
6
+
7
+ export class JsonStorage implements IStorage {
8
+ private readonly filePath: string;
9
+ private readonly mutex = new Mutex();
10
+
11
+ constructor(filePath: string) {
12
+ this.filePath = filePath;
13
+ }
14
+
15
+ async init(): Promise<void> {
16
+ await this.mutex.runExclusive(async () => {
17
+ await this.ensureFile();
18
+ });
19
+ }
20
+
21
+ async getState(): Promise<StorageState> {
22
+ await this.ensureFile();
23
+ const raw = await fs.readFile(this.filePath, 'utf8');
24
+ if (!raw.trim()) {
25
+ const state = defaultState();
26
+ await this.saveState(state);
27
+ return state;
28
+ }
29
+ try {
30
+ const parsed = JSON.parse(raw) as StorageState;
31
+ return parsed;
32
+ } catch (err) {
33
+ throw new Error(`consensus-tools: storage file corrupt at ${this.filePath}. ${String(err)}`);
34
+ }
35
+ }
36
+
37
+ async saveState(state: StorageState): Promise<void> {
38
+ const dir = path.dirname(this.filePath);
39
+ await fs.mkdir(dir, { recursive: true });
40
+ const temp = `${this.filePath}.tmp`;
41
+ await fs.writeFile(temp, JSON.stringify(state, null, 2), 'utf8');
42
+ await fs.rename(temp, this.filePath);
43
+ }
44
+
45
+ async update<T>(fn: (state: StorageState) => T | Promise<T>): Promise<{ state: StorageState; result: T }> {
46
+ return this.mutex.runExclusive(async () => {
47
+ const state = await this.getState();
48
+ const result = await fn(state);
49
+ await this.saveState(state);
50
+ return { state, result };
51
+ });
52
+ }
53
+
54
+ private async ensureFile(): Promise<void> {
55
+ const dir = path.dirname(this.filePath);
56
+ await fs.mkdir(dir, { recursive: true });
57
+ try {
58
+ await fs.access(this.filePath);
59
+ } catch {
60
+ await fs.writeFile(this.filePath, JSON.stringify(defaultState(), null, 2), 'utf8');
61
+ }
62
+ }
63
+ }
@@ -0,0 +1,67 @@
1
+ import { createRequire } from 'node:module';
2
+ import { Mutex } from '../util/locks';
3
+ import { defaultState, IStorage } from './IStorage';
4
+ import type { StorageState } from '../types';
5
+
6
+ export class SqliteStorage implements IStorage {
7
+ private readonly filePath: string;
8
+ private readonly mutex = new Mutex();
9
+ private db: any;
10
+
11
+ constructor(filePath: string) {
12
+ this.filePath = filePath;
13
+ }
14
+
15
+ async init(): Promise<void> {
16
+ await this.mutex.runExclusive(async () => {
17
+ this.ensureDb();
18
+ this.db.prepare('CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY, value TEXT)').run();
19
+ const row = this.db.prepare('SELECT value FROM kv WHERE key = ?').get('state');
20
+ if (!row) {
21
+ this.db.prepare('INSERT INTO kv (key, value) VALUES (?, ?)').run('state', JSON.stringify(defaultState()));
22
+ }
23
+ });
24
+ }
25
+
26
+ async getState(): Promise<StorageState> {
27
+ this.ensureDb();
28
+ const row = this.db.prepare('SELECT value FROM kv WHERE key = ?').get('state');
29
+ if (!row?.value) {
30
+ const state = defaultState();
31
+ await this.saveState(state);
32
+ return state;
33
+ }
34
+ return JSON.parse(row.value) as StorageState;
35
+ }
36
+
37
+ async saveState(state: StorageState): Promise<void> {
38
+ this.ensureDb();
39
+ this.db.prepare('INSERT INTO kv (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value').run(
40
+ 'state',
41
+ JSON.stringify(state)
42
+ );
43
+ }
44
+
45
+ async update<T>(fn: (state: StorageState) => T | Promise<T>): Promise<{ state: StorageState; result: T }> {
46
+ return this.mutex.runExclusive(async () => {
47
+ const state = await this.getState();
48
+ const result = await fn(state);
49
+ await this.saveState(state);
50
+ return { state, result };
51
+ });
52
+ }
53
+
54
+ private ensureDb(): void {
55
+ if (this.db) return;
56
+ let BetterSqlite3: any;
57
+ try {
58
+ const require = createRequire(import.meta.url);
59
+ BetterSqlite3 = require('better-sqlite3');
60
+ } catch (err) {
61
+ throw new Error(
62
+ 'consensus-tools: sqlite storage selected but better-sqlite3 is not installed. Install it or switch to storage.kind="json".'
63
+ );
64
+ }
65
+ this.db = new BetterSqlite3(this.filePath);
66
+ }
67
+ }
File without changes
package/src/tools.ts ADDED
@@ -0,0 +1,184 @@
1
+ import type { ConsensusToolsConfig } from './types';
2
+ import type { ConsensusToolsBackendCli } from './cli';
3
+
4
+ export function registerTools(api: any, backend: ConsensusToolsBackendCli, config: ConsensusToolsConfig, agentId: string) {
5
+ const optional = config.safety.requireOptionalToolsOptIn;
6
+
7
+ const register = (tool: any, opts?: any) => api.registerTool(tool, opts);
8
+
9
+ register(
10
+ {
11
+ name: 'consensus-tools_post_job',
12
+ description: 'Post a new consensus job to the consensus-tools job board',
13
+ parameters: {
14
+ type: 'object',
15
+ additionalProperties: false,
16
+ properties: {
17
+ boardId: { type: 'string' },
18
+ title: { type: 'string' },
19
+ description: { type: 'string' },
20
+ desc: { type: 'string' },
21
+ inputRef: { type: 'string' },
22
+ mode: { type: 'string' },
23
+ policyKey: { type: 'string' },
24
+ policyConfigJson: { type: 'object' },
25
+ inputs: { type: 'object' },
26
+ reward: { type: 'number' },
27
+ rewardAmount: { type: 'number' },
28
+ stakeRequired: { type: 'number' },
29
+ stakeAmount: { type: 'number' },
30
+ currency: { type: 'string' },
31
+ maxParticipants: { type: 'number' },
32
+ minParticipants: { type: 'number' },
33
+ expiresSeconds: { type: 'number' },
34
+ opensAt: { type: 'string' },
35
+ closesAt: { type: 'string' },
36
+ resolvesAt: { type: 'string' },
37
+ priority: { type: 'number' },
38
+ policies: { type: 'object' },
39
+ tags: { type: 'array', items: { type: 'string' } },
40
+ requiredCapabilities: { type: 'array', items: { type: 'string' } },
41
+ constraints: { type: 'object' },
42
+ artifactSchemaJson: { type: 'object' }
43
+ },
44
+ required: ['title', 'description']
45
+ },
46
+ execute: async (args: any) => {
47
+ const job = await backend.postJob(agentId, {
48
+ boardId: args.boardId,
49
+ title: args.title,
50
+ description: args.description,
51
+ desc: args.desc,
52
+ inputRef: args.inputRef,
53
+ mode: args.mode,
54
+ policyKey: args.policyKey,
55
+ policyConfigJson: args.policyConfigJson,
56
+ inputs: args.inputs,
57
+ reward: args.reward,
58
+ rewardAmount: args.rewardAmount,
59
+ stakeRequired: args.stakeRequired,
60
+ stakeAmount: args.stakeAmount,
61
+ currency: args.currency,
62
+ maxParticipants: args.maxParticipants,
63
+ minParticipants: args.minParticipants,
64
+ expiresSeconds: args.expiresSeconds,
65
+ opensAt: args.opensAt,
66
+ closesAt: args.closesAt,
67
+ resolvesAt: args.resolvesAt,
68
+ priority: args.priority,
69
+ tags: args.tags,
70
+ requiredCapabilities: args.requiredCapabilities,
71
+ constraints: args.constraints,
72
+ artifactSchemaJson: args.artifactSchemaJson,
73
+ consensusPolicy: args.policies?.consensusPolicy,
74
+ slashingPolicy: args.policies?.slashingPolicy
75
+ });
76
+ return { content: [{ type: 'text', text: JSON.stringify(job, null, 2) }] };
77
+ }
78
+ },
79
+ { optional }
80
+ );
81
+
82
+ register(
83
+ {
84
+ name: 'consensus-tools_list_jobs',
85
+ description: 'List jobs on the consensus-tools job board',
86
+ parameters: {
87
+ type: 'object',
88
+ additionalProperties: false,
89
+ properties: {
90
+ filters: { type: 'object' }
91
+ }
92
+ },
93
+ execute: async (args: any) => {
94
+ const jobs = await backend.listJobs(args.filters || {});
95
+ return { content: [{ type: 'text', text: JSON.stringify(jobs, null, 2) }] };
96
+ }
97
+ },
98
+ { optional: false }
99
+ );
100
+
101
+ register(
102
+ {
103
+ name: 'consensus-tools_submit',
104
+ description: 'Submit job artifacts',
105
+ parameters: {
106
+ type: 'object',
107
+ additionalProperties: false,
108
+ properties: {
109
+ jobId: { type: 'string' },
110
+ summary: { type: 'string' },
111
+ artifacts: { type: 'object' },
112
+ artifactRef: { type: 'string' },
113
+ confidence: { type: 'number' },
114
+ requestedPayout: { type: 'number' }
115
+ },
116
+ required: ['jobId', 'summary']
117
+ },
118
+ execute: async (args: any) => {
119
+ const submission = await backend.submitJob(agentId, args.jobId, {
120
+ summary: args.summary,
121
+ artifacts: args.artifacts,
122
+ artifactRef: args.artifactRef,
123
+ confidence: args.confidence ?? 0.5,
124
+ requestedPayout: args.requestedPayout
125
+ });
126
+ return { content: [{ type: 'text', text: JSON.stringify(submission, null, 2) }] };
127
+ }
128
+ },
129
+ { optional }
130
+ );
131
+
132
+ register(
133
+ {
134
+ name: 'consensus-tools_vote',
135
+ description: 'Vote on a job target',
136
+ parameters: {
137
+ type: 'object',
138
+ additionalProperties: false,
139
+ properties: {
140
+ jobId: { type: 'string' },
141
+ submissionId: { type: 'string' },
142
+ choiceKey: { type: 'string' },
143
+ weight: { type: 'number' },
144
+ score: { type: 'number' },
145
+ stakeAmount: { type: 'number' },
146
+ rationale: { type: 'string' }
147
+ },
148
+ required: ['jobId']
149
+ },
150
+ execute: async (args: any) => {
151
+ const vote = await backend.vote(agentId, args.jobId, {
152
+ submissionId: args.submissionId,
153
+ choiceKey: args.choiceKey,
154
+ weight: args.weight ?? args.score,
155
+ score: args.score ?? args.weight,
156
+ stakeAmount: args.stakeAmount,
157
+ rationale: args.rationale
158
+ });
159
+ return { content: [{ type: 'text', text: JSON.stringify(vote, null, 2) }] };
160
+ }
161
+ },
162
+ { optional }
163
+ );
164
+
165
+ register(
166
+ {
167
+ name: 'consensus-tools_status',
168
+ description: 'Get job status, claims, submissions, and resolution',
169
+ parameters: {
170
+ type: 'object',
171
+ additionalProperties: false,
172
+ properties: {
173
+ jobId: { type: 'string' }
174
+ },
175
+ required: ['jobId']
176
+ },
177
+ execute: async (args: any) => {
178
+ const status = backend.getStatus ? await backend.getStatus(args.jobId) : await backend.getJob(args.jobId);
179
+ return { content: [{ type: 'text', text: JSON.stringify(status, null, 2) }] };
180
+ }
181
+ },
182
+ { optional: false }
183
+ );
184
+ }