gm-skill 2.0.1517 → 2.0.1518
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/AGENTS.md +2 -0
- package/bin/plugkit-supervisor.js +22 -13
- package/gm-plugkit/package.json +1 -1
- package/gm-plugkit/supervisor.js +22 -11
- package/gm.json +1 -1
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -32,6 +32,8 @@ The plugkit stack runs as a wasm cdylib loaded by `plugkit-wasm-wrapper.js` unde
|
|
|
32
32
|
|
|
33
33
|
**`plugkit-wasm-wrapper.js` is ESM; never use inline `require()` for a node builtin — import it at module scope.** The wrapper runs under both node and bun, and the supervisor's `resolveRuntime()` prefers bun. Under bun's ESM, `require` is not a global, so an inline `const x = require('crypto'|'net'|'http'|'https'|'child_process')` throws `require is not defined` — and because those calls sit in `catch(_){}` blocks, the failure is silent: it broke `_ownWrapperSha12` (status.wrapper_sha stayed null, leaving the supervisor wrapper-sha-drift recycle inert), `_wrapperShaAtBoot` and its self-drift-restart, the synthetic-session cwd-hash, and the file-index sha — all only under the bun watcher, which is why it hid for so long (node-run watchers have `require` via CJS interop). Every node builtin is imported once at the top (`import crypto from 'crypto'`, etc.); inline `require` of a builtin is forbidden. Full incident in rs-learn (`recall: wrapper require not defined under bun`).
|
|
34
34
|
|
|
35
|
+
**Every single-instance / lock guard is atomic, never check-then-act.** A guard that does `existsSync` -> read -> decide -> `writeFileSync` is TOCTOU: under a concurrent burst (the bootstrap spawns several supervisors in the same millisecond per skill-load) every caller passes the check before any writes, so all proceed and the duplicate it was meant to prevent happens anyway. The supervisor single-instance guard, the `.watcher.lock`, and any future pid/lock file all acquire via an atomic primitive — `fs.openSync(path, 'wx')` (O_EXCL exclusive-create, succeeds for exactly one racer) or atomic-rename — then on `EEXIST` read the holder and refuse-if-alive / take-over-if-stale. The trap: a check-then-write guard passes sequential testing (a later boot sees the prior holder) and silently fails only under concurrency, so when a guard is in place and the duplicate STILL occurs, suspect non-atomicity before suspecting absence. Full incident (three mis-diagnoses) in rs-learn (`recall: supervisor churn TOCTOU atomic guard`).
|
|
36
|
+
|
|
35
37
|
## Spool dispatch ABI
|
|
36
38
|
|
|
37
39
|
Agents dispatch verbs by writing to `.gm/exec-spool/in/<verb>/<N>.txt` (request body) and reading the response from `.gm/exec-spool/out/<verb>-<N>.json` (nested verbs) or `.gm/exec-spool/out/<N>.json` (root verbs). The wasm orchestrator services every possible verb; the harness never executes side effects directly.
|
|
@@ -90,22 +90,31 @@ function statusMtime() {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
function acquireSingleInstance() {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
93
|
+
// Atomic via O_EXCL ('wx'): exclusive-create fails if the file exists, so when N supervisors
|
|
94
|
+
// race to start in the same instant exactly one wins. A plain existsSync->write is TOCTOU and
|
|
95
|
+
// lets a concurrent burst all pass, which is the duplicate-supervisor churn this guards against.
|
|
96
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
97
|
+
try {
|
|
98
|
+
const fd = fs.openSync(SUPERVISOR_PID_PATH, 'wx');
|
|
99
|
+
try { fs.writeSync(fd, String(process.pid)); } finally { fs.closeSync(fd); }
|
|
100
|
+
return true;
|
|
101
|
+
} catch (e) {
|
|
102
|
+
if (e && e.code === 'EEXIST') {
|
|
103
|
+
let other = NaN;
|
|
104
|
+
try { other = parseInt(fs.readFileSync(SUPERVISOR_PID_PATH, 'utf-8').trim(), 10); } catch (_) {}
|
|
105
|
+
if (Number.isFinite(other) && other !== process.pid && pidAlive(other)) {
|
|
106
|
+
logEvent('supervisor.refused-duplicate', { existing_pid: other, severity: 'warn' });
|
|
107
|
+
process.stderr.write(`[plugkit-supervisor] another supervisor is alive (pid=${other}); exiting\n`);
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
try { fs.unlinkSync(SUPERVISOR_PID_PATH); } catch (_) {}
|
|
111
|
+
continue;
|
|
101
112
|
}
|
|
113
|
+
logEvent('supervisor.pid-write-failed', { error: e && e.message, severity: 'warn' });
|
|
114
|
+
return true;
|
|
102
115
|
}
|
|
103
|
-
fs.writeFileSync(SUPERVISOR_PID_PATH, String(process.pid));
|
|
104
|
-
return true;
|
|
105
|
-
} catch (e) {
|
|
106
|
-
logEvent('supervisor.pid-write-failed', { error: e.message, severity: 'warn' });
|
|
107
|
-
return true;
|
|
108
116
|
}
|
|
117
|
+
return true;
|
|
109
118
|
}
|
|
110
119
|
|
|
111
120
|
function releaseSingleInstance() {
|
package/gm-plugkit/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-plugkit",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1518",
|
|
4
4
|
"description": "Bootstrap and daemon-spawn tool for gm plugkit binary. Downloads the correct platform binary, verifies SHA256, and starts the spool watcher daemon. Includes plugkit-wasm-wrapper for WASM-based spool watching.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
package/gm-plugkit/supervisor.js
CHANGED
|
@@ -72,20 +72,31 @@ function pidAlive(pid) {
|
|
|
72
72
|
// and duplicates spawn duplicate watchers that lock-fight in an endless spawn-reject churn. Write the
|
|
73
73
|
// pid file on startup and refuse to start if a live peer already holds it.
|
|
74
74
|
function acquireSingleInstance() {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
// Atomic via O_EXCL ('wx'): exclusive-create fails if the file exists, so when N supervisors
|
|
76
|
+
// race to start in the same instant exactly one wins. A plain existsSync->write is TOCTOU and
|
|
77
|
+
// lets a concurrent burst all pass, which is the duplicate-supervisor churn this guards against.
|
|
78
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
79
|
+
try {
|
|
80
|
+
const fd = fs.openSync(SUPERVISOR_PID_PATH, 'wx');
|
|
81
|
+
try { fs.writeSync(fd, String(process.pid)); } finally { fs.closeSync(fd); }
|
|
82
|
+
return true;
|
|
83
|
+
} catch (e) {
|
|
84
|
+
if (e && e.code === 'EEXIST') {
|
|
85
|
+
let other = NaN;
|
|
86
|
+
try { other = parseInt(fs.readFileSync(SUPERVISOR_PID_PATH, 'utf-8').trim(), 10); } catch (_) {}
|
|
87
|
+
if (Number.isFinite(other) && other !== process.pid && pidAlive(other)) {
|
|
88
|
+
logEvent('supervisor.refused-duplicate', { existing_pid: other, severity: 'warn' });
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
// Holder is dead/stale: remove and retry the exclusive create once.
|
|
92
|
+
try { fs.unlinkSync(SUPERVISOR_PID_PATH); } catch (_) {}
|
|
93
|
+
continue;
|
|
81
94
|
}
|
|
95
|
+
logEvent('supervisor.pid-write-failed', { error: e && e.message, severity: 'warn' });
|
|
96
|
+
return true;
|
|
82
97
|
}
|
|
83
|
-
fs.writeFileSync(SUPERVISOR_PID_PATH, String(process.pid));
|
|
84
|
-
return true;
|
|
85
|
-
} catch (e) {
|
|
86
|
-
logEvent('supervisor.pid-write-failed', { error: e.message, severity: 'warn' });
|
|
87
|
-
return true;
|
|
88
98
|
}
|
|
99
|
+
return true;
|
|
89
100
|
}
|
|
90
101
|
|
|
91
102
|
function releaseSingleInstance() {
|
package/gm.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-skill",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1518",
|
|
4
4
|
"description": "Canonical universal harness — AI-native software engineering via skill-driven orchestration; bootstraps plugkit for task execution and session isolation. Install in any AI coding agent host.",
|
|
5
5
|
"author": "AnEntrypoint",
|
|
6
6
|
"license": "MIT",
|