@envarai/envarpay 2026.1029.1 → 2026.1108.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.
- package/README.md +31 -2
- package/examples/local-free-service/README.md +17 -0
- package/examples/local-free-service/demo.mjs +178 -0
- package/examples/local-free-service/server.mjs +94 -0
- package/examples/local-free-service/shared.mjs +86 -0
- package/node_modules/@envarai/connect/CONTRIBUTING.md +9 -0
- package/node_modules/@envarai/connect/README.md +41 -1
- package/node_modules/@envarai/connect/SECURITY.md +9 -0
- package/node_modules/@envarai/connect/dist/connector/state.d.ts +2 -1
- package/node_modules/@envarai/connect/dist/connector/state.js +69 -8
- package/node_modules/@envarai/connect/docs/adapter-development.md +60 -0
- package/node_modules/@envarai/connect/docs/compatibility.md +61 -0
- package/node_modules/@envarai/connect/docs/contribution-tasks.md +34 -0
- package/node_modules/@envarai/connect/docs/migration.md +10 -0
- package/node_modules/@envarai/connect/docs/native-connector.md +79 -0
- package/node_modules/@envarai/connect/docs/releasing.md +19 -0
- package/node_modules/@envarai/connect/package.json +15 -2
- package/package.json +14 -3
package/README.md
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @envarai/envarpay
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The current TypeScript package serves free A2A capabilities and verifies Envar
|
|
4
|
+
account-balance claims for paid services. It does not sign buyer payments.
|
|
5
|
+
The Python distribution named envarpay is
|
|
6
|
+
[a separate legacy MCP/x402 implementation](https://github.com/EnvarAI/EnvarPay/blob/main/docs/legacy/python.md).
|
|
7
|
+
|
|
8
|
+
## Try a local free service
|
|
9
|
+
|
|
10
|
+
From a directory where this package is installed:
|
|
11
|
+
|
|
12
|
+
~~~sh
|
|
13
|
+
node "$(npm root)/@envarai/envarpay/examples/local-free-service/demo.mjs"
|
|
14
|
+
~~~
|
|
15
|
+
|
|
16
|
+
From this source directory:
|
|
17
|
+
|
|
18
|
+
~~~sh
|
|
19
|
+
npm ci --ignore-scripts
|
|
20
|
+
npm run build
|
|
21
|
+
node examples/local-free-service/demo.mjs
|
|
22
|
+
~~~
|
|
23
|
+
|
|
24
|
+
The demo needs no Envar account, integration credential or payment. It creates
|
|
25
|
+
two loopback A2A services and private SQLite state, then verifies one original
|
|
26
|
+
Task/result across replay and restart. Expect capability_executions: 1,
|
|
27
|
+
paid_operations: 0 and matching task_id, replay_task_id, restarted_get_task_id
|
|
28
|
+
and restarted_replay_task_id. This is a deterministic contract example, not AI
|
|
29
|
+
inference or in-flight native execution recovery.
|
|
30
|
+
|
|
31
|
+
[Full local guide](https://github.com/EnvarAI/EnvarPay/blob/main/docs/local-free-service.md) ·
|
|
32
|
+
[Cloud commerce guide](https://envar.ai/guide/commerce)
|
|
4
33
|
|
|
5
34
|
## Native Agent connection
|
|
6
35
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Free local A2A service
|
|
2
|
+
|
|
3
|
+
Run this example with Node.js 22.14+ (24 recommended):
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
node "$(npm root)/@envarai/envarpay/examples/local-free-service/demo.mjs"
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
From a built source checkout, use `node examples/local-free-service/demo.mjs` in `packages/typescript`.
|
|
10
|
+
|
|
11
|
+
It runs two authenticated loopback A2A services using the real CommerceServer, native A2A executor and SQLite stores. A private deterministic capability uppercases a fixed input; there is no model, Envar account or payment. The driver checks one actual capability execution, identical Task IDs on replay, and the same completed result after a new server process opens the original stores.
|
|
12
|
+
|
|
13
|
+
Use `--state-dir /private/demo-state` to choose an empty/private state directory. Otherwise a private temporary directory is created and retained. The result includes its path, but never bearer tokens. Repeating the command with that same directory reads the original result.
|
|
14
|
+
|
|
15
|
+
This is completed-result persistence, not a claim that unfinished external model tasks recover after a restart. Paid services retain their normal payment gate.
|
|
16
|
+
|
|
17
|
+
[Full walkthrough](https://github.com/EnvarAI/EnvarPay/blob/main/docs/local-free-service.md) · [Contributing](https://github.com/EnvarAI/EnvarPay/blob/main/CONTRIBUTING.md)
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { once } from 'node:events';
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
5
|
+
import { existsSync, mkdtempSync, readFileSync, writeFileSync, realpathSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { parseArgs } from 'node:util';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
11
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
12
|
+
import { EXPECTED_TEXT, INPUT, MESSAGE_ID, SERVICE_PATH, prepareState } from './shared.mjs';
|
|
13
|
+
|
|
14
|
+
export async function launchServer(stateDirectory) {
|
|
15
|
+
const child = spawn(process.execPath, [
|
|
16
|
+
fileURLToPath(new URL('./server.mjs', import.meta.url)), '--state-dir', stateDirectory,
|
|
17
|
+
], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
18
|
+
const closed = once(child, 'close');
|
|
19
|
+
let stderr = '';
|
|
20
|
+
child.stderr.setEncoding('utf8');
|
|
21
|
+
child.stderr.on('data', chunk => { stderr = (stderr + chunk).slice(-4096); });
|
|
22
|
+
const lines = createInterface({ input: child.stdout });
|
|
23
|
+
let timer;
|
|
24
|
+
try {
|
|
25
|
+
const ready = await Promise.race([
|
|
26
|
+
new Promise((resolve, reject) => {
|
|
27
|
+
lines.on('line', line => {
|
|
28
|
+
try { const row = JSON.parse(line); if (row.event === 'ready') resolve(row); }
|
|
29
|
+
catch { reject(new Error('Example server emitted an invalid readiness record.')); }
|
|
30
|
+
});
|
|
31
|
+
child.once('error', reject);
|
|
32
|
+
child.once('exit', code => reject(new Error('Example server exited before ready (' + code + '): ' + stderr)));
|
|
33
|
+
}),
|
|
34
|
+
new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('Example server readiness timed out.')), 15000); }),
|
|
35
|
+
]);
|
|
36
|
+
return {
|
|
37
|
+
...ready,
|
|
38
|
+
async stop() {
|
|
39
|
+
child.kill('SIGTERM');
|
|
40
|
+
const force = setTimeout(() => child.kill('SIGKILL'), 5000);
|
|
41
|
+
try {
|
|
42
|
+
const [code, signal] = await closed;
|
|
43
|
+
assert.equal(code, 0, 'Example shutdown failed: ' + signal + ' ' + stderr);
|
|
44
|
+
} finally { clearTimeout(force); lines.close(); }
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
} catch (error) {
|
|
48
|
+
child.kill('SIGKILL');
|
|
49
|
+
await closed;
|
|
50
|
+
lines.close();
|
|
51
|
+
throw error;
|
|
52
|
+
} finally { clearTimeout(timer); }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function rpc(origin, token, method, params) {
|
|
56
|
+
assert.equal(new URL(origin).hostname, '127.0.0.1');
|
|
57
|
+
const response = await fetch(origin + SERVICE_PATH + '/a2a', {
|
|
58
|
+
method: 'POST', redirect: 'error', signal: AbortSignal.timeout(10000),
|
|
59
|
+
headers: {
|
|
60
|
+
Authorization: 'Bearer ' + token, 'Content-Type': 'application/json', 'A2A-Version': '1.0',
|
|
61
|
+
},
|
|
62
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: method + '-local-demo', method, params }),
|
|
63
|
+
});
|
|
64
|
+
assert.equal(response.status, 200);
|
|
65
|
+
return response.json();
|
|
66
|
+
}
|
|
67
|
+
export const message = (input = INPUT) => ({
|
|
68
|
+
message: { messageId: MESSAGE_ID, role: 'ROLE_USER', parts: [{ data: input }] },
|
|
69
|
+
configuration: { returnImmediately: true },
|
|
70
|
+
});
|
|
71
|
+
export function resultText(task) {
|
|
72
|
+
return (task.artifacts ?? []).flatMap(a => a.parts ?? []).map(p => p.text ?? '').join('\n');
|
|
73
|
+
}
|
|
74
|
+
async function completed(origin, token, taskId) {
|
|
75
|
+
const deadline = Date.now() + 10000;
|
|
76
|
+
while (Date.now() < deadline) {
|
|
77
|
+
const response = await rpc(origin, token, 'GetTask', { id: taskId });
|
|
78
|
+
assert.ok(!response.error, JSON.stringify(response.error));
|
|
79
|
+
if (response.result.status.state === 'TASK_STATE_COMPLETED') return response.result;
|
|
80
|
+
if (response.result.status.state === 'TASK_STATE_FAILED' || response.result.metadata?.recoveryRequired) {
|
|
81
|
+
throw new Error('Inspect the original demo Task; no replacement was submitted.');
|
|
82
|
+
}
|
|
83
|
+
await sleep(50);
|
|
84
|
+
}
|
|
85
|
+
throw new Error('The original demo Task is still unresolved; keep this state directory.');
|
|
86
|
+
}
|
|
87
|
+
function executionCount(directory) {
|
|
88
|
+
const path = join(directory, 'executions.jsonl');
|
|
89
|
+
return existsSync(path) ? readFileSync(path, 'utf8').trim().split('\n').filter(Boolean).length : 0;
|
|
90
|
+
}
|
|
91
|
+
function inspectLedger(directory, name) {
|
|
92
|
+
const db = new DatabaseSync(join(directory, name + '.sqlite3'), { readOnly: true });
|
|
93
|
+
try {
|
|
94
|
+
return {
|
|
95
|
+
orders: db.prepare('SELECT message_id, payment_state, execution_state FROM commerce_orders').all(),
|
|
96
|
+
payment_attempts: db.prepare('SELECT COUNT(*) AS count FROM commerce_payment_attempts').get().count,
|
|
97
|
+
tasks: db.prepare('SELECT COUNT(*) AS count FROM commerce_tasks').get().count,
|
|
98
|
+
};
|
|
99
|
+
} finally { db.close(); }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Real HTTP + process restart demonstration; completed-result replay, not an in-flight recovery claim. */
|
|
103
|
+
export async function runDemo(directory) {
|
|
104
|
+
const state = await prepareState(directory);
|
|
105
|
+
let server;
|
|
106
|
+
try {
|
|
107
|
+
server = await launchServer(state.stateDirectory);
|
|
108
|
+
const firstPid = server.pid;
|
|
109
|
+
const cardResponse = await fetch(server.origin + SERVICE_PATH + '/agent-card.json', { signal: AbortSignal.timeout(10000) });
|
|
110
|
+
assert.equal(cardResponse.status, 200);
|
|
111
|
+
const card = await cardResponse.json();
|
|
112
|
+
assert.equal(card.supportedInterfaces[0].protocolVersion, '1.0');
|
|
113
|
+
const unauthorized = await fetch(server.origin + SERVICE_PATH + '/a2a', { method: 'POST', signal: AbortSignal.timeout(10000) });
|
|
114
|
+
assert.equal(unauthorized.status, 401);
|
|
115
|
+
const first = await rpc(server.origin, state.tokens.buyer, 'SendMessage', message());
|
|
116
|
+
assert.ok(first.result?.task?.id, JSON.stringify(first.error));
|
|
117
|
+
const taskId = first.result.task.id;
|
|
118
|
+
const delivered = await completed(server.origin, state.tokens.buyer, taskId);
|
|
119
|
+
assert.equal(resultText(delivered), EXPECTED_TEXT);
|
|
120
|
+
const replay = await rpc(server.origin, state.tokens.buyer, 'SendMessage', message());
|
|
121
|
+
assert.equal(replay.result.task.id, taskId);
|
|
122
|
+
const conflict = await rpc(server.origin, state.tokens.buyer, 'SendMessage', message({ text: 'Changed input must not run' }));
|
|
123
|
+
assert.ok(conflict.error, 'Changed input using the original message ID must be rejected.');
|
|
124
|
+
assert.equal(executionCount(state.stateDirectory), 1);
|
|
125
|
+
await server.stop();
|
|
126
|
+
server = undefined;
|
|
127
|
+
|
|
128
|
+
// A different OS process reopens the original SQLite stores and original auth files.
|
|
129
|
+
server = await launchServer(state.stateDirectory);
|
|
130
|
+
const secondPid = server.pid;
|
|
131
|
+
const restored = await rpc(server.origin, state.tokens.buyer, 'GetTask', { id: taskId });
|
|
132
|
+
assert.equal(restored.result.id, taskId);
|
|
133
|
+
assert.equal(restored.result.status.state, 'TASK_STATE_COMPLETED');
|
|
134
|
+
assert.equal(resultText(restored.result), EXPECTED_TEXT);
|
|
135
|
+
const replayAfterRestart = await rpc(server.origin, state.tokens.buyer, 'SendMessage', message());
|
|
136
|
+
assert.equal(replayAfterRestart.result.task.id, taskId);
|
|
137
|
+
assert.equal(resultText(replayAfterRestart.result.task), EXPECTED_TEXT);
|
|
138
|
+
assert.equal(executionCount(state.stateDirectory), 1);
|
|
139
|
+
await server.stop();
|
|
140
|
+
server = undefined;
|
|
141
|
+
|
|
142
|
+
const seller = inspectLedger(state.stateDirectory, 'seller');
|
|
143
|
+
const runtime = inspectLedger(state.stateDirectory, 'runtime');
|
|
144
|
+
for (const ledger of [seller, runtime]) {
|
|
145
|
+
assert.equal(ledger.orders.length, 1);
|
|
146
|
+
assert.equal(ledger.orders[0].message_id, MESSAGE_ID);
|
|
147
|
+
assert.equal(ledger.orders[0].payment_state, 'not_required');
|
|
148
|
+
assert.equal(ledger.orders[0].execution_state, 'completed');
|
|
149
|
+
assert.equal(ledger.payment_attempts, 0);
|
|
150
|
+
assert.equal(ledger.tasks, 1);
|
|
151
|
+
}
|
|
152
|
+
const proof = {
|
|
153
|
+
example: 'local-free-service', capability: 'deterministic uppercase, not AI inference',
|
|
154
|
+
message_id: MESSAGE_ID, task_id: taskId,
|
|
155
|
+
replay_task_id: replay.result.task.id,
|
|
156
|
+
restarted_get_task_id: restored.result.id,
|
|
157
|
+
restarted_replay_task_id: replayAfterRestart.result.task.id,
|
|
158
|
+
server_processes: [firstPid, secondPid],
|
|
159
|
+
result: EXPECTED_TEXT, capability_executions: executionCount(state.stateDirectory),
|
|
160
|
+
seller, runtime, paid_operations: 0, state_directory: state.stateDirectory,
|
|
161
|
+
scope: 'Completed result survives process restart. No in-flight runtime recovery, model quality, payment or production acceptance claim.',
|
|
162
|
+
};
|
|
163
|
+
writeFileSync(join(state.stateDirectory, 'result.json'), JSON.stringify(proof, null, 2) + '\n', { mode: 0o600, flush: true });
|
|
164
|
+
return proof;
|
|
165
|
+
} finally { await server?.stop(); }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
|
|
169
|
+
try {
|
|
170
|
+
const { values } = parseArgs({ options: { 'state-dir': { type: 'string' } } });
|
|
171
|
+
const directory = values['state-dir'] ?? mkdtempSync(join(tmpdir(), 'envarpay-local-free-'));
|
|
172
|
+
const proof = await runDemo(directory);
|
|
173
|
+
console.log(JSON.stringify(proof, null, 2));
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.error('Local free example failed: ' + error.message);
|
|
176
|
+
process.exitCode = 1;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { appendFileSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { once } from 'node:events';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { parseArgs } from 'node:util';
|
|
6
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
7
|
+
import { Task } from '@a2a-js/sdk';
|
|
8
|
+
import { CommerceServer, CommerceStore, bearerAuthenticator, listenCommerce, nativeA2AExecutor } from '@envarai/envarpay/server';
|
|
9
|
+
import { HOST, SERVICE_PATH, prepareState, sellerConfig } from './shared.mjs';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Both A2A endpoints and both SQLite stores are real SDK components.
|
|
13
|
+
* The only example-specific capability is deterministic string transformation.
|
|
14
|
+
* There is no Envar claim, wallet, model credential or paid offer.
|
|
15
|
+
*/
|
|
16
|
+
export async function startDemoServer(directory) {
|
|
17
|
+
const state = await prepareState(directory);
|
|
18
|
+
const privateOrigin = 'http://' + HOST + ':' + state.privatePort;
|
|
19
|
+
const origin = 'http://' + HOST + ':' + state.publicPort;
|
|
20
|
+
const cardUrl = privateOrigin + SERVICE_PATH + '/agent-card.json';
|
|
21
|
+
const upstreamStore = new CommerceStore(join(state.stateDirectory, 'runtime.sqlite3'));
|
|
22
|
+
let sellerStore, upstream, seller, upstreamHttp, sellerHttp;
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
const closeHttp = async http => {
|
|
25
|
+
if (!http) return;
|
|
26
|
+
http.closeIdleConnections();
|
|
27
|
+
await new Promise(resolve => http.close(resolve));
|
|
28
|
+
};
|
|
29
|
+
const stop = async () => {
|
|
30
|
+
seller?.stop();
|
|
31
|
+
upstream?.stop();
|
|
32
|
+
controller.abort();
|
|
33
|
+
await Promise.all([closeHttp(sellerHttp), closeHttp(upstreamHttp)]);
|
|
34
|
+
while (seller?.isRunning || upstream?.isRunning) await sleep(5);
|
|
35
|
+
sellerStore?.close();
|
|
36
|
+
upstreamStore.close();
|
|
37
|
+
};
|
|
38
|
+
try {
|
|
39
|
+
upstream = new CommerceServer({
|
|
40
|
+
config: sellerConfig('local-private-demo', cardUrl),
|
|
41
|
+
origin: privateOrigin, store: upstreamStore,
|
|
42
|
+
authenticate: bearerAuthenticator({ [state.tokens.runtime]: 'local-seller' }),
|
|
43
|
+
execute: async function* (order) {
|
|
44
|
+
appendFileSync(join(state.stateDirectory, 'executions.jsonl'),
|
|
45
|
+
JSON.stringify({ message_id: order.messageId, order_id: order.id }) + '\n',
|
|
46
|
+
{ mode: 0o600, flush: true });
|
|
47
|
+
yield Task.fromJSON({
|
|
48
|
+
id: 'deterministic-' + order.id, contextId: 'demo-' + order.id,
|
|
49
|
+
status: { state: 'TASK_STATE_COMPLETED' },
|
|
50
|
+
artifacts: [{ artifactId: 'uppercase-result', name: 'Deterministic demo result',
|
|
51
|
+
parts: [{ text: 'DEMO ONLY: ' + order.input.text.toUpperCase() }] }],
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
upstreamHttp = listenCommerce(upstream, privateOrigin, HOST, state.privatePort);
|
|
56
|
+
await once(upstreamHttp, 'listening');
|
|
57
|
+
sellerStore = new CommerceStore(join(state.stateDirectory, 'seller.sqlite3'));
|
|
58
|
+
seller = new CommerceServer({
|
|
59
|
+
config: sellerConfig('local-free-demo', cardUrl), origin, store: sellerStore,
|
|
60
|
+
authenticate: bearerAuthenticator({ [state.tokens.buyer]: 'local-buyer' }),
|
|
61
|
+
execute: nativeA2AExecutor({ uppercase: state.tokens.runtime }, fetch, controller.signal),
|
|
62
|
+
});
|
|
63
|
+
sellerHttp = listenCommerce(seller, origin, HOST, state.publicPort);
|
|
64
|
+
await once(sellerHttp, 'listening');
|
|
65
|
+
return { origin, privateOrigin, stateDirectory: state.stateDirectory, stop };
|
|
66
|
+
} catch (error) {
|
|
67
|
+
await stop();
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
|
|
73
|
+
const { values } = parseArgs({ options: { 'state-dir': { type: 'string' } } });
|
|
74
|
+
if (!values['state-dir']) throw new Error('Usage: node server.mjs --state-dir /private/demo-state');
|
|
75
|
+
let server;
|
|
76
|
+
try {
|
|
77
|
+
server = await startDemoServer(values['state-dir']);
|
|
78
|
+
let stopping = false;
|
|
79
|
+
const shutdown = async () => {
|
|
80
|
+
if (stopping) return;
|
|
81
|
+
stopping = true;
|
|
82
|
+
await server.stop();
|
|
83
|
+
};
|
|
84
|
+
process.once('SIGINT', shutdown);
|
|
85
|
+
process.once('SIGTERM', shutdown);
|
|
86
|
+
process.stdout.write(JSON.stringify({
|
|
87
|
+
event: 'ready', pid: process.pid, origin: server.origin,
|
|
88
|
+
private_origin: server.privateOrigin, state_directory: server.stateDirectory,
|
|
89
|
+
}) + '\n');
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error('Local free example could not start: ' + error.message);
|
|
92
|
+
process.exitCode = 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { createServer } from 'node:http';
|
|
4
|
+
import { once } from 'node:events';
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
|
+
|
|
7
|
+
export const MESSAGE_ID = 'local-free-service-original-v1';
|
|
8
|
+
export const INPUT = Object.freeze({ text: 'Hello from the local free service' });
|
|
9
|
+
export const EXPECTED_TEXT = 'DEMO ONLY: HELLO FROM THE LOCAL FREE SERVICE';
|
|
10
|
+
export const SERVICE_PATH = '/services/uppercase/v1/offers/free';
|
|
11
|
+
export const HOST = '127.0.0.1';
|
|
12
|
+
|
|
13
|
+
export function privateFile(path) {
|
|
14
|
+
const stat = lstatSync(path);
|
|
15
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077)) {
|
|
16
|
+
throw new Error('Use an owner-only regular demo state file: ' + path);
|
|
17
|
+
}
|
|
18
|
+
return readFileSync(path, 'utf8');
|
|
19
|
+
}
|
|
20
|
+
export function readJson(path) { return JSON.parse(privateFile(path)); }
|
|
21
|
+
|
|
22
|
+
async function reservePorts() {
|
|
23
|
+
const servers = [createServer(), createServer()];
|
|
24
|
+
try {
|
|
25
|
+
for (const server of servers) {
|
|
26
|
+
server.listen(0, HOST);
|
|
27
|
+
await once(server, 'listening');
|
|
28
|
+
}
|
|
29
|
+
return servers.map(server => server.address().port);
|
|
30
|
+
} finally {
|
|
31
|
+
await Promise.all(servers.map(server => new Promise(resolve => server.close(resolve))));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Example state is private and deliberately kept after the demonstration. */
|
|
36
|
+
export async function prepareState(directory) {
|
|
37
|
+
const stateDirectory = resolve(directory);
|
|
38
|
+
mkdirSync(stateDirectory, { recursive: true, mode: 0o700 });
|
|
39
|
+
const stat = lstatSync(stateDirectory);
|
|
40
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o077)) {
|
|
41
|
+
throw new Error('Choose a private demo directory (mode 700), not a symlink.');
|
|
42
|
+
}
|
|
43
|
+
const metadataPath = join(stateDirectory, 'demo.json');
|
|
44
|
+
if (!existsSync(metadataPath)) {
|
|
45
|
+
if (readdirSync(stateDirectory).length) throw new Error('Choose an empty directory or this example’s original state directory.');
|
|
46
|
+
const [privatePort, publicPort] = await reservePorts();
|
|
47
|
+
writeFileSync(join(stateDirectory, 'tokens.json'), JSON.stringify({
|
|
48
|
+
buyer: randomBytes(32).toString('hex'),
|
|
49
|
+
runtime: randomBytes(32).toString('hex'),
|
|
50
|
+
}), { mode: 0o600, flag: 'wx', flush: true });
|
|
51
|
+
writeFileSync(metadataPath, JSON.stringify({
|
|
52
|
+
example: 'envarpay-local-free-service', version: 1, privatePort, publicPort,
|
|
53
|
+
}), { mode: 0o600, flag: 'wx', flush: true });
|
|
54
|
+
}
|
|
55
|
+
const metadata = readJson(metadataPath);
|
|
56
|
+
const tokens = readJson(join(stateDirectory, 'tokens.json'));
|
|
57
|
+
if (metadata.example !== 'envarpay-local-free-service' || metadata.version !== 1 ||
|
|
58
|
+
![metadata.privatePort, metadata.publicPort].every(p => Number.isInteger(p) && p > 0 && p < 65536) ||
|
|
59
|
+
metadata.privatePort === metadata.publicPort ||
|
|
60
|
+
![tokens.buyer, tokens.runtime].every(t => typeof t === 'string' && /^[a-f0-9]{64}$/.test(t))) {
|
|
61
|
+
throw new Error('Preserve the original demo state; its identity or local credentials are invalid.');
|
|
62
|
+
}
|
|
63
|
+
return { stateDirectory, ...metadata, tokens };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function sellerConfig(agentId, cardUrl) {
|
|
67
|
+
return {
|
|
68
|
+
configVersion: 1,
|
|
69
|
+
agent: { id: agentId, name: 'Local deterministic demonstration — no model' },
|
|
70
|
+
paymentProfiles: {},
|
|
71
|
+
services: [{
|
|
72
|
+
id: 'uppercase', name: 'Deterministic uppercase demo', revision: 1,
|
|
73
|
+
execution: { type: 'a2a', cardUrl },
|
|
74
|
+
contract: {
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: 'object', additionalProperties: false,
|
|
77
|
+
properties: { text: { type: 'string', minLength: 1, maxLength: 500 } },
|
|
78
|
+
required: ['text'],
|
|
79
|
+
},
|
|
80
|
+
deliverables: ['Uppercase text prefixed with DEMO ONLY'],
|
|
81
|
+
targetDurationSeconds: 30, includedRevisions: 0,
|
|
82
|
+
},
|
|
83
|
+
offers: [{ id: 'free', pricing: { kind: 'free' }, collection: { kind: 'none' } }],
|
|
84
|
+
}],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Use Node.js 22.14+ (24 recommended). Run `npm ci --ignore-scripts`, `npm test`, and verify a packed tarball with `node scripts/verify-package.mjs ARTIFACT_DIRECTORY`.
|
|
4
|
+
|
|
5
|
+
Keep task identities, durable recovery, owner-scoped authorization and local credentials intact. Native models/tools remain owned by the native runtime. Add adapter and recovery tests with isolated profiles; never use real user credentials in fixtures. Describe the reproduced behavior and tests in your PR. Changes are MIT licensed; preserve upstream notices.
|
|
6
|
+
|
|
7
|
+
Start with the [code map and adapter contract](docs/adapter-development.md), then pick a [scoped contribution brief](docs/contribution-tasks.md). Include a reproduction, file locations, expected original-task behavior and acceptance commands. Public tests use isolated fixtures and need no production account.
|
|
8
|
+
|
|
9
|
+
Package and website versions have separate release owners; see [compatibility](docs/compatibility.md). The normal test suite checks current install examples against package.json.
|
|
@@ -2,10 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
Connect an Agent you run to [Envar](https://envar.ai). Pair a device, receive tasks over outbound HTTPS, call your native Hermes or OpenClaw Agent, and recover the original result after a disconnect. Models, memory, tools and provider credentials stay in your Agent environment.
|
|
4
4
|
|
|
5
|
+
## What is open source
|
|
6
|
+
|
|
7
|
+
| Project | Public scope | License |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| [EnvarConnect](https://github.com/EnvarAI/EnvarConnect) | Native connectors, task SDK, Agent Work v1 contracts | MIT |
|
|
10
|
+
| [EnvarPay](https://github.com/EnvarAI/EnvarPay) | Seller quotes, account-balance claim verification, paid execution gate | MIT |
|
|
11
|
+
|
|
12
|
+
The hosted Envar website, account service and ledger are maintained separately.
|
|
13
|
+
This repository does not include a self-hosted copy of that platform. Installing
|
|
14
|
+
Connect grants no account access or spending authority; approve each Agent
|
|
15
|
+
connection and any spending grant separately.
|
|
16
|
+
|
|
17
|
+
## Choose your starting point
|
|
18
|
+
|
|
19
|
+
| Goal | Start |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| Connect an existing Hermes/OpenClaw to Envar | [Native setup](docs/native-connector.md) |
|
|
22
|
+
| Develop or inspect the Connector without cloud credentials | [Adapter development and local contract tests](docs/adapter-development.md) |
|
|
23
|
+
| Understand package, website and bundled Pay versions | [Compatibility](docs/compatibility.md) |
|
|
24
|
+
| Make a first contribution | [Concrete task briefs](docs/contribution-tasks.md) |
|
|
25
|
+
|
|
26
|
+
For the independent seller-side developer path, start with [EnvarPay](https://github.com/EnvarAI/EnvarPay). Connect itself is the client and native adapter; a compatible enrollment/WorkService server is still required for a live connection.
|
|
27
|
+
|
|
5
28
|
## Install
|
|
6
29
|
|
|
7
30
|
```sh
|
|
8
|
-
npm install --global @envarai/connect
|
|
31
|
+
npm install --global --ignore-scripts @envarai/connect@2026.1107.2
|
|
9
32
|
envar-connect --version
|
|
10
33
|
```
|
|
11
34
|
|
|
@@ -22,6 +45,13 @@ Use `--framework openclaw` for OpenClaw. Restart the existing native gateway whe
|
|
|
22
45
|
|
|
23
46
|
The default state remains `~/.envarpay/connectors/AGENT_UUID` for compatibility. Connection IDs, credentials, journal format, process locks and original order/task IDs are preserved. Run only one receiver for a journal. Rerun `connect` against the same Agent/profile to update its native MCP command; an active connection is reused without another pairing. Keep the original directory when reinstalling or rolling back.
|
|
24
47
|
|
|
48
|
+
Linux receivers require `flock` from util-linux and a filesystem that supports
|
|
49
|
+
file locks. The kernel releases the receiver lock when its process exits, so
|
|
50
|
+
container restarts and reused PIDs cannot strand the connection. When upgrading
|
|
51
|
+
an orphaned legacy PID lock, first stop the old receiver and verify no other
|
|
52
|
+
receiver uses that journal, then move only `receiver.lock` aside once. Preserve
|
|
53
|
+
the SQLite journal, credentials and native binding. Never remove an active lock.
|
|
54
|
+
|
|
25
55
|
EnvarPay retains seller charging and exposes the old `envarpay connector ...` command as a compatibility entry. EnvarConnect does not depend on EnvarPay. Free tasks need no payment setup. Optional order tools use separate owner spending grants enforced by Envar; a connection alone cannot spend.
|
|
26
56
|
|
|
27
57
|
## API and development
|
|
@@ -34,4 +64,14 @@ npm test
|
|
|
34
64
|
npm run version:date
|
|
35
65
|
```
|
|
36
66
|
|
|
67
|
+
If your npm registry is a corporate mirror, check `npm config get registry`.
|
|
68
|
+
The connection command on your Envar Agent page also offers a fixed official
|
|
69
|
+
bundled package with integrity verification; it installs without fetching npm
|
|
70
|
+
dependencies. A mirror returning 404 does not establish that the public package
|
|
71
|
+
is missing.
|
|
72
|
+
|
|
37
73
|
See [native setup](docs/native-connector.md), [migration](docs/migration.md), [release automation](docs/releasing.md), and [contributing](CONTRIBUTING.md). The non-root container is `ghcr.io/envarai/connect`; it contains the Connector, not a native Agent runtime.
|
|
74
|
+
GHCR package access is separate from repository visibility. Anonymous image pulls
|
|
75
|
+
are not currently part of the verified public installation path. Use the npm
|
|
76
|
+
package, the official bundled download, or build the checked-out `Dockerfile`
|
|
77
|
+
locally; a managed image may require an authorized GHCR login.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
EnvarConnect runs beside a user-owned Agent and holds its scoped Envar credential, A2A access tokens, model-key references, encrypted balance claims, and local Task ledger. Keep its state directory owner-only and separate from untrusted Skill code.
|
|
4
|
+
|
|
5
|
+
Paid execution must wait for a captured claim that matches the original Envar order, seller, service revision, quote, amount, input digest, and Task ID. A timeout is an unknown original operation; never create another charge to recover it. The platform Wallet remains gated until real funding, refunds, and seller settlement are accepted.
|
|
6
|
+
|
|
7
|
+
Report a vulnerability using [GitHub private vulnerability reporting](https://github.com/EnvarAI/EnvarConnect/security/advisories/new). Include the affected version and a synthetic reproduction. Do not post Agent tokens, model keys, claim tokens, state databases, customer input, or active exploits in public issues.
|
|
8
|
+
|
|
9
|
+
The latest stable calendar release is the maintenance target. There is no promised response SLA or production security certification.
|
|
@@ -35,8 +35,9 @@ export declare class ConnectorJournal {
|
|
|
35
35
|
private readonly db;
|
|
36
36
|
private lock?;
|
|
37
37
|
constructor(path: string, binding: ConnectorBinding);
|
|
38
|
-
/**
|
|
38
|
+
/** One receiver per journal; Linux kernel locks survive PID reuse safely. */
|
|
39
39
|
acquireProcessLock(path: string): void;
|
|
40
|
+
private acquireKernelLock;
|
|
40
41
|
ingest(delivery: Delivery): void;
|
|
41
42
|
save(record: LocalWork): void;
|
|
42
43
|
get(id: string): LocalWork | undefined;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Owner-only durable journal. Transport receipts never imply a second execution. */
|
|
2
|
-
import { chmodSync, closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { chmodSync, closeSync, constants, existsSync, lstatSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
3
4
|
import { dirname } from 'node:path';
|
|
4
5
|
import { randomUUID, createHash } from 'node:crypto';
|
|
5
6
|
import { DatabaseSync } from 'node:sqlite';
|
|
@@ -49,10 +50,14 @@ export class ConnectorJournal {
|
|
|
49
50
|
}
|
|
50
51
|
this.db.prepare("INSERT OR IGNORE INTO connector_meta VALUES ('binding',?)").run(JSON.stringify(binding));
|
|
51
52
|
}
|
|
52
|
-
/**
|
|
53
|
+
/** One receiver per journal; Linux kernel locks survive PID reuse safely. */
|
|
53
54
|
acquireProcessLock(path) {
|
|
54
55
|
if (this.lock)
|
|
55
56
|
throw new Error('Connector already owns a process lock');
|
|
57
|
+
if (process.platform === 'linux') {
|
|
58
|
+
this.acquireKernelLock(path);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
56
61
|
if (existsSync(path)) {
|
|
57
62
|
privateFile(path);
|
|
58
63
|
const value = parseStored(readFileSync(path, 'utf8'));
|
|
@@ -77,6 +82,55 @@ export class ConnectorJournal {
|
|
|
77
82
|
}
|
|
78
83
|
this.lock = { path, nonce };
|
|
79
84
|
}
|
|
85
|
+
acquireKernelLock(path) {
|
|
86
|
+
const guard = `${path}.flock`;
|
|
87
|
+
if (existsSync(guard))
|
|
88
|
+
privateFile(guard);
|
|
89
|
+
const fd = openSync(guard, constants.O_CREAT | constants.O_RDWR | constants.O_NOFOLLOW, 0o600);
|
|
90
|
+
try {
|
|
91
|
+
// The child inherits this open file description. Its exit keeps the lock
|
|
92
|
+
// held by our descriptor; process death releases it even after SIGKILL.
|
|
93
|
+
const result = spawnSync('flock', ['--nonblock', '3'], { stdio: ['ignore', 'ignore', 'ignore', fd] });
|
|
94
|
+
if (result.status === 1)
|
|
95
|
+
throw new Error('Another Connector process is using this journal');
|
|
96
|
+
if (result.error || result.status !== 0)
|
|
97
|
+
throw new Error('Linux Connector locking requires util-linux flock and filesystem lock support');
|
|
98
|
+
if (existsSync(path)) {
|
|
99
|
+
privateFile(path);
|
|
100
|
+
const stored = readFileSync(path, 'utf8');
|
|
101
|
+
const value = parseStored(stored);
|
|
102
|
+
if (value.locking !== 'flock') {
|
|
103
|
+
// Do not steal a legacy PID lock during an upgrade. An operator must
|
|
104
|
+
// confirm an orphaned container lock before moving it aside once.
|
|
105
|
+
if (!Number.isSafeInteger(value.pid) || value.pid < 1)
|
|
106
|
+
throw new Error('Inspect the original Connector lock');
|
|
107
|
+
try {
|
|
108
|
+
process.kill(value.pid, 0);
|
|
109
|
+
throw new Error('Another Connector process is using this journal (legacy PID lock)');
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (error.code !== 'ESRCH')
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
unlinkSync(path);
|
|
117
|
+
}
|
|
118
|
+
const nonce = randomUUID();
|
|
119
|
+
// Exclusive creation also excludes a legacy receiver racing this upgrade.
|
|
120
|
+
const metadata = openSync(path, 'wx', 0o600);
|
|
121
|
+
try {
|
|
122
|
+
writeFileSync(metadata, JSON.stringify({ pid: process.pid, nonce, locking: 'flock' }), { flush: true });
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
closeSync(metadata);
|
|
126
|
+
}
|
|
127
|
+
this.lock = { path, nonce, fd };
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
closeSync(fd);
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
80
134
|
ingest(delivery) {
|
|
81
135
|
const work = delivery.work;
|
|
82
136
|
const incoming = ['work', 'input', 'cancel'].includes(delivery.kind);
|
|
@@ -176,12 +230,19 @@ export class ConnectorJournal {
|
|
|
176
230
|
sameObservation(record, observation) { return record.lastObservation === digest(observation); }
|
|
177
231
|
close() {
|
|
178
232
|
this.db.close();
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
233
|
+
try {
|
|
234
|
+
if (this.lock && existsSync(this.lock.path)) {
|
|
235
|
+
privateFile(this.lock.path);
|
|
236
|
+
const value = parseStored(readFileSync(this.lock.path, 'utf8'));
|
|
237
|
+
if (value.nonce === this.lock.nonce)
|
|
238
|
+
unlinkSync(this.lock.path);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
// The guard inode stays in place while the kernel releases its lock.
|
|
243
|
+
if (this.lock?.fd !== undefined)
|
|
244
|
+
closeSync(this.lock.fd);
|
|
245
|
+
this.lock = undefined;
|
|
184
246
|
}
|
|
185
|
-
this.lock = undefined;
|
|
186
247
|
}
|
|
187
248
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Build and verify a runtime adapter
|
|
2
|
+
|
|
3
|
+
Start with the existing interfaces, rather than inventing another task protocol:
|
|
4
|
+
|
|
5
|
+
| Code | Responsibility |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| `src/adapters/types.ts` | RuntimeAdapter, capabilities, observation and original execution reference |
|
|
8
|
+
| `src/adapters/a2a-runtime.ts` | Existing Hermes/OpenClaw A2A implementation |
|
|
9
|
+
| `src/adapters/native-a2a.ts` | Native Card and endpoint validation, SendMessage/GetTask transport |
|
|
10
|
+
| `src/connector/loop.ts` | Claim, admission, heartbeat, dispatch, pending reports and recovery |
|
|
11
|
+
| `src/connector/state.ts` | Durable journal and original runtime binding |
|
|
12
|
+
| `contracts/agent-work/v1/schema.json` | Public work/report contract |
|
|
13
|
+
|
|
14
|
+
## Adapter contract
|
|
15
|
+
|
|
16
|
+
- `probe()` reports the runtime and capabilities it can actually demonstrate.
|
|
17
|
+
- `start(work, executionKey)` receives the original work ID after durable admission.
|
|
18
|
+
Call the native operation once. Do not retry an uncertain POST.
|
|
19
|
+
- `observe(reference)` only reads the original Task/session handle. The reference
|
|
20
|
+
must bind the original runtime and must contain no credential.
|
|
21
|
+
- `provideInput` and `cancel` must distinguish a request from a confirmed native
|
|
22
|
+
action. An unsupported capability is not a successful cancellation.
|
|
23
|
+
- `close()` releases the adapter's client resources; the runtime lifecycle belongs
|
|
24
|
+
to its operator.
|
|
25
|
+
|
|
26
|
+
Do not reuse display names as authorization, move model/provider credentials
|
|
27
|
+
into the platform, or create a substitute execution when the original is unknown.
|
|
28
|
+
|
|
29
|
+
## Work locally without a production account
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
npm ci --ignore-scripts
|
|
33
|
+
npm test
|
|
34
|
+
# Focused iteration:
|
|
35
|
+
node --test test/connector-adapters.test.mjs test/connector-loop.test.mjs
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
These tests run actual local transports and journals against isolated fixtures.
|
|
39
|
+
They require no Envar account or model payment. They are protocol tests, not a
|
|
40
|
+
claim that a public WorkService server or a real model ran.
|
|
41
|
+
|
|
42
|
+
Use the current fixtures in those files as the reference for a new adapter.
|
|
43
|
+
An implementation should show:
|
|
44
|
+
|
|
45
|
+
1. One native dispatch for an admitted original task.
|
|
46
|
+
2. Restart reads the same saved handle and replays the same pending event.
|
|
47
|
+
3. A lost send response remains unknown, without another SendMessage.
|
|
48
|
+
4. A changed Card/origin/reference cannot redirect the native credential.
|
|
49
|
+
5. Result delivery persists before acknowledgement.
|
|
50
|
+
6. Unsupported input/cancel is reported honestly.
|
|
51
|
+
|
|
52
|
+
For a real native integration, use an isolated profile you own and document its
|
|
53
|
+
exact version. Keep keys, prompts, private files and personal profiles out of the
|
|
54
|
+
PR. Include sanitized original IDs and observed state transitions, not secrets.
|
|
55
|
+
|
|
56
|
+
## First contributions
|
|
57
|
+
|
|
58
|
+
The maintained task briefs are in [contribution tasks](contribution-tasks.md).
|
|
59
|
+
Discuss a new runtime or incompatible schema change before building it. Contract
|
|
60
|
+
tests can be reviewed publicly; access to production credentials is not needed.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Versions and compatibility
|
|
2
|
+
|
|
3
|
+
Install the release documented by this checkout:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install --global --ignore-scripts @envarai/connect@2026.1107.2
|
|
7
|
+
envar-connect --version
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
The version above comes from package.json. `npm run version:date` updates it and
|
|
11
|
+
the other current installation examples; the normal test suite rejects stale
|
|
12
|
+
commands. Historical migration examples keep their original versions.
|
|
13
|
+
|
|
14
|
+
## Which version controls what?
|
|
15
|
+
|
|
16
|
+
| Component | Source of truth | Upgrade behavior |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| Standalone Connector | This package's package.json / npm release | Upgrade the package, preserve the original directory, and restart its receiver and native MCP process as instructed. |
|
|
19
|
+
| Envar's browser-generated command | The deployed server/web release's fixed package and SHA-512 manifest | The website may deliberately pin a reviewed version. It is not automatically npm latest. |
|
|
20
|
+
| Legacy `envarpay connector` | The Connect dependency bundled in that installed Pay package | Updating standalone Connect does not replace the version inside Pay. Use standalone Connect for a new native connection. |
|
|
21
|
+
| Agent Work protocol | [v1 schema](../contracts/agent-work/v1/schema.json) | Package calendar versions do not change the protocol version. Incompatible changes require a new protocol version. |
|
|
22
|
+
| Native runtime | The existing Hermes/OpenClaw installation | Installing Connect does not upgrade the runtime, change its model, or grant tool permissions. |
|
|
23
|
+
|
|
24
|
+
Keep only one receiver for a journal. The website and Pay dependency should be
|
|
25
|
+
promoted only after that consumer's contract and installation checks pass.
|
|
26
|
+
A newer package release alone does not establish that an existing receiver was
|
|
27
|
+
upgraded or that every supported runtime was retested.
|
|
28
|
+
|
|
29
|
+
## Runtime scope
|
|
30
|
+
|
|
31
|
+
Node.js 22.14+ is required; Node 24 is recommended. CI runs the minimum 22.14.0
|
|
32
|
+
and current Node 22/24 lines. Linux process locks require util-linux `flock`;
|
|
33
|
+
the managed container includes it. Preserve the journal on a filesystem that
|
|
34
|
+
supports the locks.
|
|
35
|
+
|
|
36
|
+
The last recorded native setup baseline is Hermes 2026.9.24 and OpenClaw
|
|
37
|
+
2026.10.4. The local adapter tests cover the protocol and recovery boundaries;
|
|
38
|
+
they do not call a model or prove acceptance on every later native version.
|
|
39
|
+
Report your exact native version with any compatibility issue.
|
|
40
|
+
|
|
41
|
+
Both native adapters currently observe original Tasks, but do not advertise
|
|
42
|
+
input continuation or confirmed cancellation. A gateway restart may lose its
|
|
43
|
+
in-memory Task. Keep that operation unknown and inspect it; never create a
|
|
44
|
+
replacement simply because observation timed out.
|
|
45
|
+
|
|
46
|
+
## Another Envar origin
|
|
47
|
+
|
|
48
|
+
The Connector accepts an explicitly selected HTTPS origin:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
envar-connect connect --origin https://your-envar.example --agent AGENT_UUID --framework hermes
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The origin must implement the same enrollment and Agent Work contract. This
|
|
55
|
+
package does not contain that server. Loopback HTTP is a deliberate development
|
|
56
|
+
option, not a way to send credentials to arbitrary HTTP hosts. The original
|
|
57
|
+
journal binds the origin, connection, Agent and native adapter; do not edit those
|
|
58
|
+
fields to reuse another connection's task or spending authority.
|
|
59
|
+
|
|
60
|
+
Read [native setup](native-connector.md), [migration](migration.md), and
|
|
61
|
+
[adapter development](adapter-development.md) before changing an installation.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Contribution tasks
|
|
2
|
+
|
|
3
|
+
These are scoped contribution briefs, not promises that every runtime is
|
|
4
|
+
supported. Open an issue describing the brief and your proposed work before
|
|
5
|
+
starting a large change; maintainers can confirm whether someone is already
|
|
6
|
+
working on it.
|
|
7
|
+
|
|
8
|
+
| Brief | Reproduce / inspect | Files | Acceptance |
|
|
9
|
+
| --- | --- | --- | --- |
|
|
10
|
+
| C1 — Document one additional native version | Use an isolated profile with the [native guide](native-connector.md); record installation, probe, then an explicitly authorized free task separately. | `docs/native-connector.md`, `docs/compatibility.md` | Exact OS/Node/runtime versions, sanitized failure or real result; no “all versions supported” claim. A model call is optional and its own operator's responsibility. |
|
|
11
|
+
| C2 — Multi-byte result boundary coverage | The adapter limits text by UTF-8 bytes and rejects empty completion. Add a case straddling that limit with multi-byte text. | `src/adapters/a2a-runtime.ts`, `test/connector-adapters.test.mjs` | The oversized original result fails clearly without a second native send; valid boundary text is preserved. |
|
|
12
|
+
| C3 — Actionable missing-runtime diagnostics | Reproduce missing executable, ambiguous OpenClaw profile or missing Linux flock in an isolated setup. Improve only a demonstrated unclear error. | `src/adapters/configure-native.ts`, `src/connector/state.ts`, `test/connector-setup.test.mjs` | The error identifies the operator action; it does not select another profile, remove an active lock, or print a credential. |
|
|
13
|
+
| C4 — Consumer compatibility recipe | Build a small consumer using the public EnvarWorkClient/RuntimeAdapter contracts against a local test fixture. | `src/index.ts`, `src/adapters/types.ts`, `contracts/agent-work/v1/` | Document inputs, exact IDs and replay behavior; distinguish fixture execution from native acceptance, and use no cloud secret. |
|
|
14
|
+
|
|
15
|
+
Every PR should state the problem, before/after behavior, tests, and compatibility
|
|
16
|
+
impact. Run `npm test`; focused file tests help iteration but do not replace the
|
|
17
|
+
normal CI discovery command.
|
|
18
|
+
|
|
19
|
+
## Maintenance and feedback
|
|
20
|
+
|
|
21
|
+
GitHub Issues is the public support and design entry. Maintainers aim to give
|
|
22
|
+
an initial response within two business days and a substantive review or an
|
|
23
|
+
explicit next step within five business days. This is a best-effort maintenance
|
|
24
|
+
target, not a service-level agreement; availability and scope should be stated
|
|
25
|
+
on the issue when a response takes longer.
|
|
26
|
+
|
|
27
|
+
Issues are triaged as reproducible bug, documentation, compatible enhancement,
|
|
28
|
+
design discussion, or outside current scope. A decision to defer is useful when
|
|
29
|
+
it includes the reason and the supported alternative. Security reports follow
|
|
30
|
+
[SECURITY.md](../SECURITY.md), not a public issue with credentials.
|
|
31
|
+
|
|
32
|
+
Current focus: reproducible setup, compatibility, original-operation recovery,
|
|
33
|
+
and useful adapters. Hosted models, implicit spending permissions and automatic
|
|
34
|
+
resubmission of unknown work are outside this Connector's responsibility.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Moving from EnvarPay Connector
|
|
2
|
+
|
|
3
|
+
EnvarConnect is extracted from the MIT EnvarPay Connector at `2a9e03ebfd0beb9a167ad57473ead56ec5dd9bf5`. It retains the v1 protocol and SQLite journal layout.
|
|
4
|
+
|
|
5
|
+
1. Keep the original state directory and its owner-only permissions. Stop the old receiver before starting the new one; do not remove its journal or binding files.
|
|
6
|
+
2. Install the reviewed `@envarai/connect` version. `envar-connect run --agent ID` reads the existing `~/.envarpay/connectors/ID` binding. A custom installation uses the original `--directory PATH`.
|
|
7
|
+
3. Run `envar-connect connect` with the same Agent/framework/profile to update the native MCP entry. It preserves the active connection and native peer credential. Restart the existing native gateway when requested, then start one receiver.
|
|
8
|
+
4. Read the original task/order after restart. Reuse persisted request IDs; never buy again to recover a delayed result.
|
|
9
|
+
|
|
10
|
+
Rollback uses the prior EnvarPay package against the same state directory, after stopping the new receiver. Both implementations understand the same journal. Rerun its original connect command if its native MCP command needs restoring. Connection permissions and spending grants are unchanged.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Native Agent Connector
|
|
2
|
+
|
|
3
|
+
Requires the matching Envar native enrollment/WorkService deployment. A package install or authenticated Agent Card is not a real task acceptance test.
|
|
4
|
+
|
|
5
|
+
## 1. Open the correct terminal
|
|
6
|
+
|
|
7
|
+
Use the same OS user and profile as the native Agent. It must already have a working model and Node.js 22.14+ (24 recommended).
|
|
8
|
+
|
|
9
|
+
| Runtime | Terminal in an official container |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| Hermes | `docker exec --user hermes -e HOME=/opt/data -it CONTAINER sh` |
|
|
12
|
+
| OpenClaw | `docker exec --user node -e HOME=/home/node -it CONTAINER sh` |
|
|
13
|
+
|
|
14
|
+
Replace `CONTAINER` with your container name. Run `node --version` there. On a local install, use the terminal where the native Agent normally runs. Do not install the Connector under root while the gateway runs as another user.
|
|
15
|
+
|
|
16
|
+
## 2. Connect and approve
|
|
17
|
+
|
|
18
|
+
Create a private Envar Agent and copy its connection command. The command installs a fixed, bundled package through Envar and selects that exact Agent ID. The official npm package is also available:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm install --prefix "$HOME/.envarpay/cli" --ignore-scripts --no-audit --no-fund @envarai/connect@2026.1107.2
|
|
22
|
+
"$HOME/.envarpay/cli/node_modules/.bin/envar-connect" connect --agent AGENT_UUID --framework hermes
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
For OpenClaw choose `--framework openclaw`. The command prints a short code and an Envar approval link. Sign in as the owner, verify the Agent and code, and approve. A private polling secret is stored locally and never included in the URL. The request expires in ten minutes. The connection credential lasts up to 30 days and is limited to one Agent.
|
|
26
|
+
|
|
27
|
+
Optional free outgoing permission does not permit spending. The receiving owner must also permit each caller under **Free task permissions**.
|
|
28
|
+
|
|
29
|
+
For multiple OpenClaw Agents, select `--native-agent ID`. Use `--home PATH` for a secondary profile. Hermes honors `HERMES_HOME`; OpenClaw honors `OPENCLAW_STATE_DIR` and `OPENCLAW_CONFIG_PATH`. Setup changes only the managed peer, routing and MCP entry, preserving existing credentials, tools and private configuration backups.
|
|
30
|
+
|
|
31
|
+
### Managed Ace Data Cloud instances
|
|
32
|
+
|
|
33
|
+
Reuse an existing instance when its subscription and model configuration are
|
|
34
|
+
valid. Keep the existing EnvarPay seller profile and ledger; a new private Envar
|
|
35
|
+
Agent must have its own Connector directory. A seller Agent Card is not a native
|
|
36
|
+
Connector pairing.
|
|
37
|
+
|
|
38
|
+
If Hermes reports that A2A peers are supplied by the deployment environment,
|
|
39
|
+
finish the setup through that deployment's peer configuration. Editing `.env`
|
|
40
|
+
alone cannot override those values. Preserve existing peers, use the generated
|
|
41
|
+
private `native.token` for this Connector only, and never paste it into Envar or
|
|
42
|
+
logs. Have the deployment supervisor run the receiver against its durable
|
|
43
|
+
original directory; an interactive terminal alone does not survive replacement
|
|
44
|
+
of the container.
|
|
45
|
+
|
|
46
|
+
## 3. Start receiving
|
|
47
|
+
|
|
48
|
+
When setup says a restart is needed, restart the existing native gateway using its normal service/container controls. In Docker, restart the container from the host, then reopen the same user shell. Do not start a second gateway alongside an already supervised one.
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
"$HOME/.envarpay/cli/node_modules/.bin/envar-connect" run --agent AGENT_UUID
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Keep this process and the gateway running. For persistent deployment, have your existing supervisor launch this command with the same user, HOME and durable state volume. It uses outbound HTTPS; no public A2A tunnel is required.
|
|
55
|
+
|
|
56
|
+
## 4. Verify the actual result
|
|
57
|
+
|
|
58
|
+
Send the free connection test from the Agent page. Wait for the exact returned result. `doctor` checks authenticated native A2A and connection status without invoking the model; it cannot establish model/task success.
|
|
59
|
+
|
|
60
|
+
After enabling free outgoing tasks and the recipient's permission, ask the native Agent to use `list_peers`, `send_message`, then `wait_work`/`get_work`. Preserve the same request ID after any lost response. Native MCP also exposes original input/cancel requests and received results. Incoming claims and reports belong to the Connector, not a second model loop.
|
|
61
|
+
|
|
62
|
+
## Recovery and disconnection
|
|
63
|
+
|
|
64
|
+
- Restart the Connector using its original directory. Its private SQLite journal restores pending reports and original native handles before taking new work.
|
|
65
|
+
- Both currently tested native A2A servers hold Tasks in memory. A native gateway restart may lose the original Task. That state stays **unknown** and is never resubmitted automatically.
|
|
66
|
+
- A lost response after a native send also stays unknown when no original handle can be recovered. Do not replace it with a new paid order.
|
|
67
|
+
- Native input continuation and cancellation are not advertised in this release. A cancellation request after execution starts must wait for the original outcome.
|
|
68
|
+
- `envar-connect disconnect --agent AGENT_UUID` revokes this credential and preserves journals. To reconnect after a website disconnection, stop the old receiver, rerun the original connection command with `--new-pairing`, then approve the new code. A fresh connection cannot recover another connection's admitted work.
|
|
69
|
+
- Expired credentials need owner reauthorization. Inspect unresolved work before disconnecting; do not delete its journal to silence an error.
|
|
70
|
+
|
|
71
|
+
## Verified versions and remaining acceptance
|
|
72
|
+
|
|
73
|
+
Fresh-container preflight uses OpenClaw `2026.10.4` and the Hermes `2026.9.24` CLI. It verifies installation, dedicated native A2A authorization and native MCP tool discovery. Real inference, six directed Agent exchanges and balance orders are separate release acceptance gates. Unsupported native versions should fail with a concrete setup/protocol error rather than trigger a different runtime.
|
|
74
|
+
|
|
75
|
+
## Service discovery and budgeted purchases
|
|
76
|
+
|
|
77
|
+
With the companion Envar commerce release, the native Agent can use `list_services`, `quote_service`, `confirm_order`, `get_order`, `wait_order`, `list_orders` and `recover_order`. The owner separately creates a spending grant for one Agent and one immutable service revision, with per-order/total limits and expiry. The connection's optional sending permission is not a spending grant. Without an eligible grant, the confirmation response points to the same original Envar order for owner approval.
|
|
78
|
+
|
|
79
|
+
The Connector persists quote request identities before its network call. Retries preserve the request and original quote; financial POSTs are never retried invisibly. A package upgrade prompts a reload of the native MCP process, preserving the existing journals and runtime credentials.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Stable releases
|
|
2
|
+
|
|
3
|
+
Versions use `YEAR.MONTHDAY.SEQUENCE` in Asia/Shanghai, matching EnvarPay. Run `npm run version:date` when package or image contents change; commit package.json, package-lock.json and the updated current install examples together. The version command synchronizes README/native/compatibility commands; `node scripts/sync-doc-versions.mjs --check` is also covered by `npm test`.
|
|
4
|
+
|
|
5
|
+
Pushes to main that change runtime/package/container files run `publish-npm.yml`. The workflow tests Node 22 and 24, packs once per Node version, installs the actual tarball offline with an empty cache, and builds/runs containers on amd64 and arm64. After those checks it publishes `ghcr.io/envarai/connect:VERSION`, verifies its source/version labels, publishes the tested Node 24 tarball to npm with provenance, verifies version and integrity, updates image latest, and attaches the tarball to GitHub release `vVERSION`.
|
|
6
|
+
|
|
7
|
+
A retry reuses the same npm artifact and source commit. Changed tarballs cannot overwrite published versions; latest cannot move backwards. Rerun failed jobs on the original run. Workflow-only changes do not publish automatically.
|
|
8
|
+
|
|
9
|
+
## One-time npm setup
|
|
10
|
+
|
|
11
|
+
Configure the npm Trusted Publisher for `@envarai/connect`: GitHub owner `EnvarAI`, repository `EnvarConnect`, workflow `publish-npm.yml`, environment `npm`. The GitHub environment is restricted to main. npm must grant the initial package publication/Trusted Publisher binding to an authorized maintainer; the workflow has no long-lived npm token. Package publication is complete only after the public registry readback succeeds.
|
|
12
|
+
|
|
13
|
+
## Contracts
|
|
14
|
+
|
|
15
|
+
`contracts/agent-work/v1/schema.json` is the source. Run `node scripts/generate-work-contract.mjs` after editing it. Commit generated types. Publish compatible contracts before updating EnvarAI's pinned copy; incompatible v1 changes require a new contract version. Runtime code never downloads a schema.
|
|
16
|
+
|
|
17
|
+
## Promote consumers after publication
|
|
18
|
+
|
|
19
|
+
Read the public npm version and SHA-512 integrity before updating the website download manifest or Pay's pinned/bundled Connect dependency. Test each consumer and preserve the original state directory. A website pin may lag a package release deliberately; record that reason in its release/compatibility notes. No receiver is upgraded merely by publishing npm latest.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@envarai/connect",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.1107.2",
|
|
4
4
|
"description": "Connect your own Agent to Envar: tasks, native adapters and recovery",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,7 +31,10 @@
|
|
|
31
31
|
"dist",
|
|
32
32
|
"contracts",
|
|
33
33
|
"README.md",
|
|
34
|
-
"LICENSE"
|
|
34
|
+
"LICENSE",
|
|
35
|
+
"docs",
|
|
36
|
+
"CONTRIBUTING.md",
|
|
37
|
+
"SECURITY.md"
|
|
35
38
|
],
|
|
36
39
|
"repository": {
|
|
37
40
|
"type": "git",
|
|
@@ -73,5 +76,15 @@
|
|
|
73
76
|
"json5",
|
|
74
77
|
"yaml",
|
|
75
78
|
"zod"
|
|
79
|
+
],
|
|
80
|
+
"homepage": "https://github.com/EnvarAI/EnvarConnect#readme",
|
|
81
|
+
"keywords": [
|
|
82
|
+
"ai-agents",
|
|
83
|
+
"a2a",
|
|
84
|
+
"mcp",
|
|
85
|
+
"agent-work",
|
|
86
|
+
"hermes",
|
|
87
|
+
"openclaw",
|
|
88
|
+
"typescript"
|
|
76
89
|
]
|
|
77
90
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@envarai/envarpay",
|
|
3
|
-
"version": "2026.
|
|
4
|
-
"description": "A2A services
|
|
3
|
+
"version": "2026.1108.1",
|
|
4
|
+
"description": "Free A2A services and Envar account-balance seller gates",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"engines": {
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"@a2a-js/sdk": "1.2.1",
|
|
61
|
-
"@envarai/connect": "2026.
|
|
61
|
+
"@envarai/connect": "2026.1107.2",
|
|
62
62
|
"@modelcontextprotocol/sdk": "1.31.0",
|
|
63
63
|
"ajv": "8.20.0",
|
|
64
64
|
"ajv-formats": "3.0.1",
|
|
@@ -84,5 +84,16 @@
|
|
|
84
84
|
"yaml",
|
|
85
85
|
"zod",
|
|
86
86
|
"@envarai/connect"
|
|
87
|
+
],
|
|
88
|
+
"homepage": "https://github.com/EnvarAI/EnvarPay#readme",
|
|
89
|
+
"keywords": [
|
|
90
|
+
"ai-agents",
|
|
91
|
+
"agent-services",
|
|
92
|
+
"agent-payments",
|
|
93
|
+
"a2a",
|
|
94
|
+
"mcp",
|
|
95
|
+
"typescript",
|
|
96
|
+
"hermes",
|
|
97
|
+
"openclaw"
|
|
87
98
|
]
|
|
88
99
|
}
|