@envarai/connect 2026.1031.1 → 2026.1107.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 +7 -0
- package/dist/connector/state.d.ts +2 -1
- package/dist/connector/state.js +69 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,6 +34,13 @@ Use `--framework openclaw` for OpenClaw. Restart the existing native gateway whe
|
|
|
34
34
|
|
|
35
35
|
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.
|
|
36
36
|
|
|
37
|
+
Linux receivers require `flock` from util-linux and a filesystem that supports
|
|
38
|
+
file locks. The kernel releases the receiver lock when its process exits, so
|
|
39
|
+
container restarts and reused PIDs cannot strand the connection. When upgrading
|
|
40
|
+
an orphaned legacy PID lock, first stop the old receiver and verify no other
|
|
41
|
+
receiver uses that journal, then move only `receiver.lock` aside once. Preserve
|
|
42
|
+
the SQLite journal, credentials and native binding. Never remove an active lock.
|
|
43
|
+
|
|
37
44
|
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.
|
|
38
45
|
|
|
39
46
|
## API and development
|
|
@@ -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;
|
package/dist/connector/state.js
CHANGED
|
@@ -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
|
}
|