@khorsheed/dsh-ankh-guard 0.1.0 → 0.1.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/CHANGELOG.md +4 -0
- package/lib/cli.js +73 -66
- package/lib/index.js +2 -1
- package/lib/types/cli.js +97 -90
- package/lib/types/index.js +1 -1
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
package/lib/cli.js
CHANGED
|
@@ -74,17 +74,36 @@ function sandboxGate(verb, options, io) {
|
|
|
74
74
|
io.stderr(`${verb} refused: this environment is sandboxed (a probe write outside the workspace was denied), so a detached restart/watchdog process would be reaped when the turn ends. You cannot switch the sandbox yourself — ask the user to run /permission danger-full-access in THIS session (a yes/no "authorization" changes nothing), then verify with \`dsh-ankh-guard check-env\` and retry. Certain the probe is wrong? Re-run with --force\n`);
|
|
75
75
|
return false;
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Whether the pid named by this raw pid/lock-file content is alive. Empty
|
|
79
|
+
* content reads as NO holder: Number('') is 0 and kill(0, 0) probes our own
|
|
80
|
+
* process group (always succeeds), which once read as "alive" and refused
|
|
81
|
+
* every restart forever — the bug that had to be fixed in two copies of this
|
|
82
|
+
* logic before it was consolidated here.
|
|
83
|
+
*/
|
|
84
|
+
function pidAlive(raw) {
|
|
85
|
+
const pid = Number(raw);
|
|
86
|
+
if (raw === "" || !Number.isInteger(pid) || pid <= 0) return false;
|
|
87
|
+
try {
|
|
88
|
+
process.kill(pid, 0);
|
|
89
|
+
return true;
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** The live pid named by a pid/lock file (as the raw string), or null when absent/stale. */
|
|
95
|
+
function livePidIn(file) {
|
|
96
|
+
try {
|
|
97
|
+
const raw = readFileSync(file, "utf8").trim();
|
|
98
|
+
return pidAlive(raw) ? raw : null;
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
77
103
|
/** The live supervising watchdog's pid, or null when none is (pidfile + kill 0). */
|
|
78
104
|
function liveWatchdogPid(stateDir) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const pid = Number(raw);
|
|
82
|
-
if (raw !== "" && Number.isInteger(pid) && pid > 0) {
|
|
83
|
-
process.kill(pid, 0);
|
|
84
|
-
return pid;
|
|
85
|
-
}
|
|
86
|
-
} catch {}
|
|
87
|
-
return null;
|
|
105
|
+
const raw = livePidIn(stateFile(stateDir, "watchdogPid"));
|
|
106
|
+
return raw === null ? null : Number(raw);
|
|
88
107
|
}
|
|
89
108
|
/**
|
|
90
109
|
* Cross-session restart mutual exclusion: two concurrent restarts would both
|
|
@@ -116,14 +135,10 @@ function acquireRestartLock(stateDir, holderPid = process.pid) {
|
|
|
116
135
|
holder: `unreadable (${String(error)})`
|
|
117
136
|
};
|
|
118
137
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
ok: false,
|
|
124
|
-
holder
|
|
125
|
-
};
|
|
126
|
-
} catch {}
|
|
138
|
+
if (pidAlive(holder)) return {
|
|
139
|
+
ok: false,
|
|
140
|
+
holder
|
|
141
|
+
};
|
|
127
142
|
try {
|
|
128
143
|
unlinkSync(file);
|
|
129
144
|
} catch (error) {
|
|
@@ -456,18 +471,6 @@ function restartMarkerState(stateDir) {
|
|
|
456
471
|
return "stale";
|
|
457
472
|
}
|
|
458
473
|
}
|
|
459
|
-
/** The restart lock's live holder pid (as a string), or null when free/stale. */
|
|
460
|
-
function liveRestartLockHolder(stateDir) {
|
|
461
|
-
try {
|
|
462
|
-
const raw = readFileSync(stateFile(stateDir, "restartLock"), "utf8").trim();
|
|
463
|
-
const pid = Number(raw);
|
|
464
|
-
if (raw !== "" && Number.isInteger(pid) && pid > 0) try {
|
|
465
|
-
process.kill(pid, 0);
|
|
466
|
-
return raw;
|
|
467
|
-
} catch {}
|
|
468
|
-
} catch {}
|
|
469
|
-
return null;
|
|
470
|
-
}
|
|
471
474
|
/** Captured preflight output is diagnostics, not a log — cap it before it can grow without bound. */
|
|
472
475
|
const PREFLIGHT_OUTPUT_CAP = 65536;
|
|
473
476
|
/**
|
|
@@ -1150,46 +1153,50 @@ async function runCli(argv, io) {
|
|
|
1150
1153
|
if (!sandboxGate("schedule-exit", options, io)) return 1;
|
|
1151
1154
|
if (!await preflightGate("schedule-exit", resolveProfileName(options), options.preflightTimeoutMs ?? DEFAULT_PREFLIGHT_TIMEOUT_MS, io, resolveHarnessRoot(options.repoDir))) return 1;
|
|
1152
1155
|
if (liveWatchdogPid(stateDir) === null) io.stderr(NO_WATCHDOG_HINT);
|
|
1153
|
-
const
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
io.stderr("schedule-exit refused: a restart is already scheduled (restart-requested.json still pending); a stale marker expires on its own after 15 minutes\n");
|
|
1157
|
-
return 1;
|
|
1158
|
-
}
|
|
1159
|
-
if (markerState === "stale" && existsSync(markerFile)) io.stderr("warning: overwriting a stale restart marker (a previous schedule never completed)\n");
|
|
1160
|
-
const inFlight = liveRestartLockHolder(stateDir);
|
|
1161
|
-
if (inFlight !== null) {
|
|
1162
|
-
io.stderr(`schedule-exit refused: a restart is in flight (pid ${inFlight}) — the exit agent would kill the instance it is starting\n`);
|
|
1156
|
+
const lock = acquireRestartLock(stateDir);
|
|
1157
|
+
if (!lock.ok) {
|
|
1158
|
+
io.stderr(`schedule-exit refused: a restart is in flight (pid ${lock.holder}) — the exit agent would kill the instance it is starting\n`);
|
|
1163
1159
|
return 1;
|
|
1164
1160
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
})}\n`);
|
|
1172
|
-
const resultFile = stateFile(stateDir, "lastRestart");
|
|
1173
|
-
const logPath = options.log ?? stateFile(stateDir, "scheduleExitLog");
|
|
1174
|
-
mkdirSync(dirname(logPath), { recursive: true });
|
|
1175
|
-
const child = spawn(process.execPath, exitAgentInvocation(), {
|
|
1176
|
-
detached: true,
|
|
1177
|
-
stdio: [
|
|
1178
|
-
"ignore",
|
|
1179
|
-
openSync(logPath, "a"),
|
|
1180
|
-
openSync(logPath, "a")
|
|
1181
|
-
],
|
|
1182
|
-
env: {
|
|
1183
|
-
...process.env,
|
|
1184
|
-
WD_PORT: String(port),
|
|
1185
|
-
WD_DELAY_MS: String(delayMs),
|
|
1186
|
-
WD_RESULT_FILE: resultFile,
|
|
1187
|
-
...initiator !== void 0 ? { WD_INITIATOR: initiator } : {}
|
|
1161
|
+
try {
|
|
1162
|
+
const markerFile = stateFile(stateDir, "restartRequested");
|
|
1163
|
+
const markerState = restartMarkerState(stateDir);
|
|
1164
|
+
if (markerState === "fresh") {
|
|
1165
|
+
io.stderr("schedule-exit refused: a restart is already scheduled (restart-requested.json still pending); a stale marker expires on its own after 15 minutes\n");
|
|
1166
|
+
return 1;
|
|
1188
1167
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1168
|
+
if (markerState === "stale" && existsSync(markerFile)) io.stderr("warning: overwriting a stale restart marker (a previous schedule never completed)\n");
|
|
1169
|
+
const initiator = options.initiator ?? process.env.DSH_SESSION_ID;
|
|
1170
|
+
mkdirSync(stateDir, { recursive: true });
|
|
1171
|
+
writeFileSync(stateFile(stateDir, "restartRequested"), `${JSON.stringify({
|
|
1172
|
+
reason: "scheduled self-restart",
|
|
1173
|
+
requestedAt: Date.now(),
|
|
1174
|
+
...initiator !== void 0 ? { initiator } : {}
|
|
1175
|
+
})}\n`);
|
|
1176
|
+
const resultFile = stateFile(stateDir, "lastRestart");
|
|
1177
|
+
const logPath = options.log ?? stateFile(stateDir, "scheduleExitLog");
|
|
1178
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
1179
|
+
const child = spawn(process.execPath, exitAgentInvocation(), {
|
|
1180
|
+
detached: true,
|
|
1181
|
+
stdio: [
|
|
1182
|
+
"ignore",
|
|
1183
|
+
openSync(logPath, "a"),
|
|
1184
|
+
openSync(logPath, "a")
|
|
1185
|
+
],
|
|
1186
|
+
env: {
|
|
1187
|
+
...process.env,
|
|
1188
|
+
WD_PORT: String(port),
|
|
1189
|
+
WD_DELAY_MS: String(delayMs),
|
|
1190
|
+
WD_RESULT_FILE: resultFile,
|
|
1191
|
+
...initiator !== void 0 ? { WD_INITIATOR: initiator } : {}
|
|
1192
|
+
}
|
|
1193
|
+
});
|
|
1194
|
+
child.unref();
|
|
1195
|
+
io.stdout(`exit scheduled in ${delayMs} ms (agent pid ${child.pid ?? "unknown"}) — watchdog will respawn and run the canary\n`);
|
|
1196
|
+
return 0;
|
|
1197
|
+
} finally {
|
|
1198
|
+
lock.release();
|
|
1199
|
+
}
|
|
1193
1200
|
}
|
|
1194
1201
|
default:
|
|
1195
1202
|
io.stderr(`unknown command ${command}\n\n${USAGE}`);
|
package/lib/index.js
CHANGED
package/lib/types/cli.js
CHANGED
|
@@ -80,20 +80,39 @@ function sandboxGate(verb, options, io) {
|
|
|
80
80
|
io.stderr(`${verb} refused: this environment is sandboxed (a probe write outside the workspace was denied), so a detached restart/watchdog process would be reaped when the turn ends. You cannot switch the sandbox yourself — ask the user to run /permission danger-full-access in THIS session (a yes/no "authorization" changes nothing), then verify with \`dsh-ankh-guard check-env\` and retry. Certain the probe is wrong? Re-run with --force\n`);
|
|
81
81
|
return false;
|
|
82
82
|
}
|
|
83
|
-
/**
|
|
84
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Whether the pid named by this raw pid/lock-file content is alive. Empty
|
|
85
|
+
* content reads as NO holder: Number('') is 0 and kill(0, 0) probes our own
|
|
86
|
+
* process group (always succeeds), which once read as "alive" and refused
|
|
87
|
+
* every restart forever — the bug that had to be fixed in two copies of this
|
|
88
|
+
* logic before it was consolidated here.
|
|
89
|
+
*/
|
|
90
|
+
function pidAlive(raw) {
|
|
91
|
+
const pid = Number(raw);
|
|
92
|
+
if (raw === '' || !Number.isInteger(pid) || pid <= 0)
|
|
93
|
+
return false;
|
|
85
94
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
process.kill(pid, 0);
|
|
92
|
-
return pid;
|
|
93
|
-
}
|
|
95
|
+
process.kill(pid, 0);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return false;
|
|
94
100
|
}
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
}
|
|
102
|
+
/** The live pid named by a pid/lock file (as the raw string), or null when absent/stale. */
|
|
103
|
+
function livePidIn(file) {
|
|
104
|
+
try {
|
|
105
|
+
const raw = readFileSync(file, 'utf8').trim();
|
|
106
|
+
return pidAlive(raw) ? raw : null;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** The live supervising watchdog's pid, or null when none is (pidfile + kill 0). */
|
|
113
|
+
function liveWatchdogPid(stateDir) {
|
|
114
|
+
const raw = livePidIn(stateFile(stateDir, 'watchdogPid'));
|
|
115
|
+
return raw === null ? null : Number(raw);
|
|
97
116
|
}
|
|
98
117
|
/**
|
|
99
118
|
* Cross-session restart mutual exclusion: two concurrent restarts would both
|
|
@@ -125,14 +144,8 @@ function acquireRestartLock(stateDir, holderPid = process.pid) {
|
|
|
125
144
|
catch (error) {
|
|
126
145
|
return { ok: false, holder: `unreadable (${String(error)})` };
|
|
127
146
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
try {
|
|
131
|
-
process.kill(pid, 0);
|
|
132
|
-
return { ok: false, holder };
|
|
133
|
-
}
|
|
134
|
-
catch { /* dead holder — reclaim below */ }
|
|
135
|
-
}
|
|
147
|
+
if (pidAlive(holder))
|
|
148
|
+
return { ok: false, holder };
|
|
136
149
|
try {
|
|
137
150
|
unlinkSync(file);
|
|
138
151
|
}
|
|
@@ -443,22 +456,6 @@ function restartMarkerState(stateDir) {
|
|
|
443
456
|
return 'stale'; // unparseable is stale by definition
|
|
444
457
|
}
|
|
445
458
|
}
|
|
446
|
-
/** The restart lock's live holder pid (as a string), or null when free/stale. */
|
|
447
|
-
function liveRestartLockHolder(stateDir) {
|
|
448
|
-
try {
|
|
449
|
-
const raw = readFileSync(stateFile(stateDir, 'restartLock'), 'utf8').trim();
|
|
450
|
-
const pid = Number(raw);
|
|
451
|
-
if (raw !== '' && Number.isInteger(pid) && pid > 0) {
|
|
452
|
-
try {
|
|
453
|
-
process.kill(pid, 0);
|
|
454
|
-
return raw;
|
|
455
|
-
}
|
|
456
|
-
catch { /* dead holder */ }
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
catch { /* no lock file */ }
|
|
460
|
-
return null;
|
|
461
|
-
}
|
|
462
459
|
/** Captured preflight output is diagnostics, not a log — cap it before it can grow without bound. */
|
|
463
460
|
const PREFLIGHT_OUTPUT_CAP = 64 * 1024;
|
|
464
461
|
/**
|
|
@@ -1282,64 +1279,74 @@ export async function runCli(argv, io) {
|
|
|
1282
1279
|
if (liveWatchdogPid(stateDir) === null) {
|
|
1283
1280
|
io.stderr(NO_WATCHDOG_HINT);
|
|
1284
1281
|
}
|
|
1285
|
-
// One scheduled restart at a time
|
|
1286
|
-
//
|
|
1287
|
-
//
|
|
1288
|
-
//
|
|
1289
|
-
//
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1282
|
+
// One scheduled restart at a time, and never while a restart is in
|
|
1283
|
+
// flight. Both checks must hold ATOMICALLY with writing the marker and
|
|
1284
|
+
// spawning the exit agent: an earlier version checked without holding
|
|
1285
|
+
// the lock, and the narrow read→write window let a concurrent restart
|
|
1286
|
+
// pass its own checks in between — the scheduled exit agent then
|
|
1287
|
+
// SIGTERMed the instance that restart had just started. Taking the
|
|
1288
|
+
// restart lock here serializes the two verbs on the same primitive
|
|
1289
|
+
// (restart's own marker check stays: it guards against an exit agent
|
|
1290
|
+
// scheduled BEFORE its acquisition, already past this window).
|
|
1291
|
+
const lock = acquireRestartLock(stateDir);
|
|
1292
|
+
if (!lock.ok) {
|
|
1293
|
+
io.stderr(`schedule-exit refused: a restart is in flight (pid ${lock.holder}) — the exit agent would kill the instance it is starting\n`);
|
|
1294
1294
|
return 1;
|
|
1295
1295
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1296
|
+
try {
|
|
1297
|
+
// The marker carries a single initiator, so overwriting a FRESH one
|
|
1298
|
+
// would silently reassign the pending report. A marker past the TTL
|
|
1299
|
+
// is stale (the watchdog died mid-flow without clearing it) —
|
|
1300
|
+
// overwrite with a warning instead of refusing forever.
|
|
1301
|
+
const markerFile = stateFile(stateDir, 'restartRequested');
|
|
1302
|
+
const markerState = restartMarkerState(stateDir);
|
|
1303
|
+
if (markerState === 'fresh') {
|
|
1304
|
+
io.stderr('schedule-exit refused: a restart is already scheduled (restart-requested.json still pending); a stale marker expires on its own after 15 minutes\n');
|
|
1305
|
+
return 1;
|
|
1306
|
+
}
|
|
1307
|
+
if (markerState === 'stale' && existsSync(markerFile)) {
|
|
1308
|
+
io.stderr('warning: overwriting a stale restart marker (a previous schedule never completed)\n');
|
|
1309
|
+
}
|
|
1310
|
+
// Intentional-restart marker: the supervising watchdog runs the canary
|
|
1311
|
+
// after the respawn and clears this on pass. The initiator (the session
|
|
1312
|
+
// that requested the exit) rides along so the restart report can return
|
|
1313
|
+
// to that session instead of racing to whichever root agent resumes
|
|
1314
|
+
// first. Everything lands in stateDir directly — the same directory the
|
|
1315
|
+
// plugin reads (see the supervise case for why no home is derived).
|
|
1316
|
+
const initiator = options.initiator ?? process.env.DSH_SESSION_ID;
|
|
1317
|
+
mkdirSync(stateDir, { recursive: true });
|
|
1318
|
+
writeFileSync(stateFile(stateDir, 'restartRequested'), `${JSON.stringify({
|
|
1319
|
+
reason: 'scheduled self-restart',
|
|
1320
|
+
requestedAt: Date.now(),
|
|
1321
|
+
...(initiator !== undefined ? { initiator } : {}),
|
|
1322
|
+
})}\n`);
|
|
1323
|
+
// A DETACHED exit agent (setsid via node spawn): it cannot be reaped by
|
|
1324
|
+
// the sandbox/harness process group, so the scheduled kill actually
|
|
1325
|
+
// lands even after the scheduling turn ends — the fix for "the kill
|
|
1326
|
+
// never happened" seen with `(sleep N; kill) &` from a managed shell.
|
|
1327
|
+
// The agent is a real shipped file (typechecked, linted, unit-tested),
|
|
1328
|
+
// spawned with the same source/built split as guardInvocation().
|
|
1329
|
+
const resultFile = stateFile(stateDir, 'lastRestart');
|
|
1330
|
+
const logPath = options.log ?? stateFile(stateDir, 'scheduleExitLog');
|
|
1331
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
1332
|
+
const child = spawn(process.execPath, exitAgentInvocation(), {
|
|
1333
|
+
detached: true,
|
|
1334
|
+
stdio: ['ignore', openSync(logPath, 'a'), openSync(logPath, 'a')],
|
|
1335
|
+
env: {
|
|
1336
|
+
...process.env,
|
|
1337
|
+
WD_PORT: String(port),
|
|
1338
|
+
WD_DELAY_MS: String(delayMs),
|
|
1339
|
+
WD_RESULT_FILE: resultFile,
|
|
1340
|
+
...(initiator !== undefined ? { WD_INITIATOR: initiator } : {}),
|
|
1341
|
+
},
|
|
1342
|
+
});
|
|
1343
|
+
child.unref();
|
|
1344
|
+
io.stdout(`exit scheduled in ${delayMs} ms (agent pid ${child.pid ?? 'unknown'}) — watchdog will respawn and run the canary\n`);
|
|
1345
|
+
return 0;
|
|
1298
1346
|
}
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
// now — scheduling an exit would SIGTERM the one it just started.
|
|
1302
|
-
const inFlight = liveRestartLockHolder(stateDir);
|
|
1303
|
-
if (inFlight !== null) {
|
|
1304
|
-
io.stderr(`schedule-exit refused: a restart is in flight (pid ${inFlight}) — the exit agent would kill the instance it is starting\n`);
|
|
1305
|
-
return 1;
|
|
1347
|
+
finally {
|
|
1348
|
+
lock.release();
|
|
1306
1349
|
}
|
|
1307
|
-
// Intentional-restart marker: the supervising watchdog runs the canary
|
|
1308
|
-
// after the respawn and clears this on pass. The initiator (the session
|
|
1309
|
-
// that requested the exit) rides along so the restart report can return
|
|
1310
|
-
// to that session instead of racing to whichever root agent resumes
|
|
1311
|
-
// first. Everything lands in stateDir directly — the same directory the
|
|
1312
|
-
// plugin reads (see the supervise case for why no home is derived).
|
|
1313
|
-
const initiator = options.initiator ?? process.env.DSH_SESSION_ID;
|
|
1314
|
-
mkdirSync(stateDir, { recursive: true });
|
|
1315
|
-
writeFileSync(stateFile(stateDir, 'restartRequested'), `${JSON.stringify({
|
|
1316
|
-
reason: 'scheduled self-restart',
|
|
1317
|
-
requestedAt: Date.now(),
|
|
1318
|
-
...(initiator !== undefined ? { initiator } : {}),
|
|
1319
|
-
})}\n`);
|
|
1320
|
-
// A DETACHED exit agent (setsid via node spawn): it cannot be reaped by
|
|
1321
|
-
// the sandbox/harness process group, so the scheduled kill actually
|
|
1322
|
-
// lands even after the scheduling turn ends — the fix for "the kill
|
|
1323
|
-
// never happened" seen with `(sleep N; kill) &` from a managed shell.
|
|
1324
|
-
// The agent is a real shipped file (typechecked, linted, unit-tested),
|
|
1325
|
-
// spawned with the same source/built split as guardInvocation().
|
|
1326
|
-
const resultFile = stateFile(stateDir, 'lastRestart');
|
|
1327
|
-
const logPath = options.log ?? stateFile(stateDir, 'scheduleExitLog');
|
|
1328
|
-
mkdirSync(dirname(logPath), { recursive: true });
|
|
1329
|
-
const child = spawn(process.execPath, exitAgentInvocation(), {
|
|
1330
|
-
detached: true,
|
|
1331
|
-
stdio: ['ignore', openSync(logPath, 'a'), openSync(logPath, 'a')],
|
|
1332
|
-
env: {
|
|
1333
|
-
...process.env,
|
|
1334
|
-
WD_PORT: String(port),
|
|
1335
|
-
WD_DELAY_MS: String(delayMs),
|
|
1336
|
-
WD_RESULT_FILE: resultFile,
|
|
1337
|
-
...(initiator !== undefined ? { WD_INITIATOR: initiator } : {}),
|
|
1338
|
-
},
|
|
1339
|
-
});
|
|
1340
|
-
child.unref();
|
|
1341
|
-
io.stdout(`exit scheduled in ${delayMs} ms (agent pid ${child.pid ?? 'unknown'}) — watchdog will respawn and run the canary\n`);
|
|
1342
|
-
return 0;
|
|
1343
1350
|
}
|
|
1344
1351
|
default:
|
|
1345
1352
|
io.stderr(`unknown command ${command}\n\n${USAGE}`);
|
package/lib/types/index.js
CHANGED
|
@@ -56,7 +56,7 @@ function registerRestartSkill(ctx, stateDir) {
|
|
|
56
56
|
writeSkillRegistration(stateDir, { registered: false, reason: 'shipped SKILL.md malformed', at: Date.now() });
|
|
57
57
|
return;
|
|
58
58
|
}
|
|
59
|
-
ctx.effect(() => skills.register({ name, description, content }));
|
|
59
|
+
ctx.effect(() => skills.register({ name, description, content, source: 'runtime' }));
|
|
60
60
|
writeSkillRegistration(stateDir, { registered: true, at: Date.now() });
|
|
61
61
|
}
|
|
62
62
|
catch (error) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khorsheed/dsh-ankh-guard",
|
|
3
3
|
"description": "Hard gate for self-modification restarts: a green-build credential bound to the git HEAD, checked before any restart of the running instance",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"@types/node": "^22.0.0",
|
|
58
58
|
"tsdown": "^0.22.2",
|
|
59
59
|
"typescript": "^5.9.0",
|
|
60
|
-
"vitest": "^3.0.0"
|
|
60
|
+
"vitest": "^3.0.0",
|
|
61
|
+
"@deepseek-ai/dsh-skill": "^0.1.0-rc.8"
|
|
61
62
|
},
|
|
62
63
|
"keywords": [
|
|
63
64
|
"dsh",
|