instar 1.3.800 → 1.3.802
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/dashboard/index.html +25 -0
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +63 -1
- package/dist/commands/server.js.map +1 -1
- package/dist/core/BackupManager.d.ts.map +1 -1
- package/dist/core/BackupManager.js +8 -0
- package/dist/core/BackupManager.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +8 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +45 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/ProactiveSwapMonitor.d.ts +10 -0
- package/dist/core/ProactiveSwapMonitor.d.ts.map +1 -1
- package/dist/core/ProactiveSwapMonitor.js +39 -0
- package/dist/core/ProactiveSwapMonitor.js.map +1 -1
- package/dist/core/SessionManager.d.ts +26 -4
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +76 -22
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/machineCoherenceManifest.d.ts +10 -0
- package/dist/core/machineCoherenceManifest.d.ts.map +1 -1
- package/dist/core/machineCoherenceManifest.js +58 -0
- package/dist/core/machineCoherenceManifest.js.map +1 -1
- package/dist/core/types.d.ts +53 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/monitoring/ExternalHogScanTick.d.ts +9 -0
- package/dist/monitoring/ExternalHogScanTick.d.ts.map +1 -1
- package/dist/monitoring/ExternalHogScanTick.js +29 -0
- package/dist/monitoring/ExternalHogScanTick.js.map +1 -1
- package/dist/monitoring/PromiseBeacon.d.ts +6 -0
- package/dist/monitoring/PromiseBeacon.d.ts.map +1 -1
- package/dist/monitoring/PromiseBeacon.js +42 -6
- package/dist/monitoring/PromiseBeacon.js.map +1 -1
- package/dist/monitoring/guardManifest.d.ts.map +1 -1
- package/dist/monitoring/guardManifest.js +31 -0
- package/dist/monitoring/guardManifest.js.map +1 -1
- package/dist/monitoring/guardPosture.d.ts.map +1 -1
- package/dist/monitoring/guardPosture.js +22 -0
- package/dist/monitoring/guardPosture.js.map +1 -1
- package/dist/monitoring/selfaction/anchor.d.ts +82 -0
- package/dist/monitoring/selfaction/anchor.d.ts.map +1 -0
- package/dist/monitoring/selfaction/anchor.js +113 -0
- package/dist/monitoring/selfaction/anchor.js.map +1 -0
- package/dist/monitoring/selfaction/governor.d.ts +242 -0
- package/dist/monitoring/selfaction/governor.d.ts.map +1 -0
- package/dist/monitoring/selfaction/governor.js +1475 -0
- package/dist/monitoring/selfaction/governor.js.map +1 -0
- package/dist/monitoring/selfaction/policies.d.ts +86 -0
- package/dist/monitoring/selfaction/policies.d.ts.map +1 -0
- package/dist/monitoring/selfaction/policies.js +339 -0
- package/dist/monitoring/selfaction/policies.js.map +1 -0
- package/dist/monitoring/selfaction/types.d.ts +208 -0
- package/dist/monitoring/selfaction/types.d.ts.map +1 -0
- package/dist/monitoring/selfaction/types.js +11 -0
- package/dist/monitoring/selfaction/types.js.map +1 -0
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +8 -1
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +6 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +9 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts +4 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +271 -3
- package/dist/server/routes.js.map +1 -1
- package/dist/testing/selfActionRegistry.d.ts +16 -0
- package/dist/testing/selfActionRegistry.d.ts.map +1 -1
- package/dist/testing/selfActionRegistry.js +16 -0
- package/dist/testing/selfActionRegistry.js.map +1 -1
- package/package.json +2 -2
- package/scripts/lint-emit-without-admit.js +303 -0
- package/src/data/builtin-manifest.json +66 -66
- package/src/data/state-coherence-registry.json +1296 -1241
- package/src/scaffold/templates.ts +8 -1
- package/upgrades/1.3.801.md +38 -0
- package/upgrades/1.3.802.md +36 -0
- package/upgrades/session-listing-hygiene.eli16.md +26 -0
- package/upgrades/side-effects/self-action-governor.md +111 -0
- package/upgrades/side-effects/session-listing-hygiene.md +93 -0
package/dist/server/routes.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { Router } from 'express';
|
|
8
8
|
import { execFileSync } from 'node:child_process';
|
|
9
9
|
import { createHash, timingSafeEqual, randomUUID } from 'node:crypto';
|
|
10
|
+
import { governor as selfActionGovernorSurface } from '../monitoring/selfaction/governor.js';
|
|
10
11
|
import { classifyActionClaim } from '../core/action-claim.js';
|
|
11
12
|
import { sharedG3SoakLedger, decideLeaseGatedSpawn } from '../core/leaseGatedSpawn.js';
|
|
12
13
|
import { getHostSpawnSemaphore, configuredSpawnAcquireMs, configuredSpawnWaitersMax } from '../core/hostSpawnSemaphore.js';
|
|
@@ -6058,9 +6059,24 @@ export function createRoutes(ctx) {
|
|
|
6058
6059
|
router.get('/sessions', async (req, res) => {
|
|
6059
6060
|
const status = req.query.status;
|
|
6060
6061
|
const validStatuses = ['starting', 'running', 'completed', 'failed', 'killed'];
|
|
6061
|
-
|
|
6062
|
-
|
|
6062
|
+
// Session-listing hygiene (CMT-1936): the DEFAULT listing is ACTIVE
|
|
6063
|
+
// sessions only (starting/running). Finished runs (completed/failed/
|
|
6064
|
+
// killed) stay fully readable behind an explicit opt-in — `?include=all`
|
|
6065
|
+
// for the whole registry, or `?status=<terminal>` for one class — so the
|
|
6066
|
+
// ~50 retained finished mentor/job records never read as "50 running
|
|
6067
|
+
// sessions" on the dashboard / pool / agent surfaces (the 2026-07-05
|
|
6068
|
+
// "duplicate sessions" operator misread).
|
|
6069
|
+
const explicitStatus = status && validStatuses.includes(status)
|
|
6070
|
+
? status
|
|
6071
|
+
: undefined;
|
|
6072
|
+
const includeAll = req.query.include === 'all';
|
|
6073
|
+
const isActiveStatus = (s) => s === 'running' || s === 'starting';
|
|
6074
|
+
let sessions = explicitStatus
|
|
6075
|
+
? ctx.state.listSessions({ status: explicitStatus })
|
|
6063
6076
|
: ctx.state.listSessions();
|
|
6077
|
+
if (!explicitStatus && !includeAll) {
|
|
6078
|
+
sessions = sessions.filter((s) => isActiveStatus(s.status));
|
|
6079
|
+
}
|
|
6064
6080
|
// Which machine answered — so a session row can say where it runs. Absent
|
|
6065
6081
|
// on single-machine installs (no pool wired), so the dashboard hides the badge.
|
|
6066
6082
|
const selfMachineId = ctx.meshSelfId ?? null;
|
|
@@ -6138,9 +6154,14 @@ export function createRoutes(ctx) {
|
|
|
6138
6154
|
// classifier so an idle peer reads "online — no active sessions" but a
|
|
6139
6155
|
// dark peer reads "unreachable", never a fabricated state).
|
|
6140
6156
|
const peerFetchReason = new Map();
|
|
6157
|
+
// Forward the caller's visibility opt-in to each peer so the pool view
|
|
6158
|
+
// keeps ONE semantic (active-only by default, everything on include=all,
|
|
6159
|
+
// one class on ?status=). explicitStatus comes from the validated
|
|
6160
|
+
// allowlist above, so it is URL-safe by construction.
|
|
6161
|
+
const peerQuery = includeAll ? '?include=all' : explicitStatus ? `?status=${explicitStatus}` : '';
|
|
6141
6162
|
await Promise.all(peers.map(async (p) => {
|
|
6142
6163
|
try {
|
|
6143
|
-
const r = await fetch(`${p.url}/sessions`, {
|
|
6164
|
+
const r = await fetch(`${p.url}/sessions${peerQuery}`, {
|
|
6144
6165
|
headers: { Authorization: `Bearer ${ctx.config.authToken}` },
|
|
6145
6166
|
signal: AbortSignal.timeout(5000),
|
|
6146
6167
|
});
|
|
@@ -6153,6 +6174,16 @@ export function createRoutes(ctx) {
|
|
|
6153
6174
|
peerFetchReason.set(p.machineId, null);
|
|
6154
6175
|
const nickname = ctx.machinePoolRegistry?.getCapacity(p.machineId)?.nickname ?? null;
|
|
6155
6176
|
for (const s of list) {
|
|
6177
|
+
// Defensive mirror of the default-visibility filter for OLDER
|
|
6178
|
+
// peers that still answer the plain route with their FULL registry
|
|
6179
|
+
// (the exact client-side filter dashboard/index.html carries for
|
|
6180
|
+
// the same reason — 2026-06-11: five closed Mac Mini sessions
|
|
6181
|
+
// reappeared as live tiles). Without this, one stale peer
|
|
6182
|
+
// re-introduces the finished-session wall into the pool view.
|
|
6183
|
+
if (!includeAll && !explicitStatus && !isActiveStatus(s.status))
|
|
6184
|
+
continue;
|
|
6185
|
+
if (explicitStatus && s.status !== explicitStatus)
|
|
6186
|
+
continue;
|
|
6156
6187
|
remote.push({
|
|
6157
6188
|
...s,
|
|
6158
6189
|
machineId: s.machineId ?? p.machineId,
|
|
@@ -6218,6 +6249,55 @@ export function createRoutes(ctx) {
|
|
|
6218
6249
|
}
|
|
6219
6250
|
machines.push(entry);
|
|
6220
6251
|
}
|
|
6252
|
+
// GENUINE cross-machine duplicate detection (CMT-1936 part c): the SAME
|
|
6253
|
+
// conversation (platform + platformId) with a LIVE session on two or
|
|
6254
|
+
// more machines at once is the real incoherency. Each machine running
|
|
6255
|
+
// its OWN copy of a recurring job is benign, BY DESIGN, and never flags
|
|
6256
|
+
// here — job/headless sessions carry no platform binding (they enrich as
|
|
6257
|
+
// platform:'headless', which is excluded), so identical `job-*` names on
|
|
6258
|
+
// two machines stay unflagged. Only ACTIVE rows count: a finished record
|
|
6259
|
+
// on one machine + a live session on another is the normal topic-move
|
|
6260
|
+
// case, not a duplicate.
|
|
6261
|
+
const byConversation = new Map();
|
|
6262
|
+
for (const s of allSessionsMerged) {
|
|
6263
|
+
if (!isActiveStatus(s.status))
|
|
6264
|
+
continue;
|
|
6265
|
+
const platform = typeof s.platform === 'string' ? s.platform : '';
|
|
6266
|
+
const platformId = s.platformId;
|
|
6267
|
+
if (!platform || platform === 'headless' || platformId == null)
|
|
6268
|
+
continue;
|
|
6269
|
+
const key = `${platform}:${String(platformId)}`;
|
|
6270
|
+
const mid = typeof s.machineId === 'string' ? s.machineId : (selfMachineId ?? '__local__');
|
|
6271
|
+
const entry = byConversation.get(key)
|
|
6272
|
+
?? { platform, platformId: platformId, machineIds: new Set(), sessions: [] };
|
|
6273
|
+
entry.machineIds.add(mid);
|
|
6274
|
+
entry.sessions.push(String(s.name ?? s.tmuxSession ?? s.id ?? ''));
|
|
6275
|
+
byConversation.set(key, entry);
|
|
6276
|
+
}
|
|
6277
|
+
const duplicateTopics = [];
|
|
6278
|
+
const duplicateKeys = new Set();
|
|
6279
|
+
for (const [key, e] of byConversation) {
|
|
6280
|
+
if (e.machineIds.size >= 2) {
|
|
6281
|
+
duplicateKeys.add(key);
|
|
6282
|
+
duplicateTopics.push({
|
|
6283
|
+
platform: e.platform,
|
|
6284
|
+
platformId: e.platformId,
|
|
6285
|
+
machineIds: [...e.machineIds],
|
|
6286
|
+
sessions: e.sessions,
|
|
6287
|
+
});
|
|
6288
|
+
}
|
|
6289
|
+
}
|
|
6290
|
+
if (duplicateKeys.size > 0) {
|
|
6291
|
+
for (const s of allSessionsMerged) {
|
|
6292
|
+
if (!isActiveStatus(s.status))
|
|
6293
|
+
continue;
|
|
6294
|
+
const platform = typeof s.platform === 'string' ? s.platform : '';
|
|
6295
|
+
if (!platform || s.platformId == null)
|
|
6296
|
+
continue;
|
|
6297
|
+
if (duplicateKeys.has(`${platform}:${String(s.platformId)}`))
|
|
6298
|
+
s.duplicateTopic = true;
|
|
6299
|
+
}
|
|
6300
|
+
}
|
|
6221
6301
|
res.json({
|
|
6222
6302
|
sessions: allSessionsMerged,
|
|
6223
6303
|
pool: {
|
|
@@ -6232,6 +6312,9 @@ export function createRoutes(ctx) {
|
|
|
6232
6312
|
// multi-machine case the dashboard renders; single-machine installs
|
|
6233
6313
|
// still get the (lone) self row, harmlessly.
|
|
6234
6314
|
machines,
|
|
6315
|
+
// Always an array (usually empty) — additive, never breaking. Each
|
|
6316
|
+
// entry is one conversation with a live session on >=2 machines.
|
|
6317
|
+
duplicateTopics,
|
|
6235
6318
|
},
|
|
6236
6319
|
});
|
|
6237
6320
|
return;
|
|
@@ -6818,6 +6901,25 @@ export function createRoutes(ctx) {
|
|
|
6818
6901
|
}, 500 + i * STAGGER_MS);
|
|
6819
6902
|
});
|
|
6820
6903
|
});
|
|
6904
|
+
/** FD13 (unified-self-action-backpressure §4): verify the optional
|
|
6905
|
+
* `x-instar-principal-pin` proof and — when it is the operator's dashboard
|
|
6906
|
+
* PIN — record the principal admission on the ALWAYS-ALLOW audited lane.
|
|
6907
|
+
* Never blocks; never throws (a throwing principalAdmit resolves OPEN). */
|
|
6908
|
+
const recordPrincipalKillProvenance = (req, target) => {
|
|
6909
|
+
try {
|
|
6910
|
+
const proof = req.headers['x-instar-principal-pin'];
|
|
6911
|
+
if (typeof proof !== 'string' || !ctx.config.dashboardPin)
|
|
6912
|
+
return;
|
|
6913
|
+
const ha = createHash('sha256').update(proof).digest();
|
|
6914
|
+
const hb = createHash('sha256').update(ctx.config.dashboardPin).digest();
|
|
6915
|
+
if (ha.length === hb.length && timingSafeEqual(ha, hb)) {
|
|
6916
|
+
selfActionGovernorSurface.principalAdmit('dashboard-pin-session', { actionVerb: 'session-kill', target });
|
|
6917
|
+
}
|
|
6918
|
+
}
|
|
6919
|
+
catch {
|
|
6920
|
+
/* provenance recording is observability — never blocks the kill */
|
|
6921
|
+
}
|
|
6922
|
+
};
|
|
6821
6923
|
router.delete('/sessions/:id', async (req, res) => {
|
|
6822
6924
|
if (!SESSION_NAME_RE.test(req.params.id)) {
|
|
6823
6925
|
res.status(400).json({ error: 'Invalid session ID format' });
|
|
@@ -6846,6 +6948,14 @@ export function createRoutes(ctx) {
|
|
|
6846
6948
|
// those read the route-stamped `origin` below. Sanitized + bounded.
|
|
6847
6949
|
const viaHeader = req.headers['x-instar-close-via'];
|
|
6848
6950
|
const via = typeof viaHeader === 'string' && /^[a-z0-9-]{1,40}$/.test(viaHeader) ? viaHeader : undefined;
|
|
6951
|
+
// FD13 principal provenance (unified-self-action-backpressure §4): a
|
|
6952
|
+
// PIN-proof header distinguishes the OPERATOR's dashboard session from a
|
|
6953
|
+
// bare Bearer call (the agent itself holds Bearer, so Bearer alone is
|
|
6954
|
+
// structurally insufficient — Know Your Principal). A valid proof rides
|
|
6955
|
+
// the ALWAYS-ALLOW, always-audited principal lane; absent/invalid stays
|
|
6956
|
+
// agent-origin ('self') and rides the normal governor ceilings once the
|
|
6957
|
+
// kill sink enforces.
|
|
6958
|
+
recordPrincipalKillProvenance(req, req.params.id);
|
|
6849
6959
|
const result = await ctx.sessionManager.terminateSession(target.id, 'operator-kill', {
|
|
6850
6960
|
origin: 'operator',
|
|
6851
6961
|
finalStatus: 'killed',
|
|
@@ -6901,6 +7011,9 @@ export function createRoutes(ctx) {
|
|
|
6901
7011
|
res.status(502).json({ error: 'url-rejected', machineId });
|
|
6902
7012
|
return;
|
|
6903
7013
|
}
|
|
7014
|
+
// FD13 principal provenance (same PIN-proof distinguishability as the
|
|
7015
|
+
// local DELETE route — a bare Bearer relay stays agent-origin 'self').
|
|
7016
|
+
recordPrincipalKillProvenance(req, sessionUuid);
|
|
6904
7017
|
try {
|
|
6905
7018
|
// The peer's PLAIN local close — no relay params forwarded (single-hop
|
|
6906
7019
|
// by construction, §2.1); UUID-targeted (the peer's existing route is
|
|
@@ -7598,6 +7711,97 @@ export function createRoutes(ctx) {
|
|
|
7598
7711
|
}
|
|
7599
7712
|
return out;
|
|
7600
7713
|
};
|
|
7714
|
+
// ── SelfActionGovernor (unified-self-action-backpressure Increment B) ──
|
|
7715
|
+
// LOCK-FREE + WRITE-FREE pure read of the in-memory aggregates (the
|
|
7716
|
+
// /test-runner-limiter PURE-read precedent — never /spawn-limiter's
|
|
7717
|
+
// lock-taking status()). Projection is SCRUBBED: aggregate per-class counts
|
|
7718
|
+
// + verdict/breaker state ONLY — no target identities (account ids,
|
|
7719
|
+
// session/topic names) and no absolute quota values (spec SEC6).
|
|
7720
|
+
// `?scope=pool` merges pool-shared class counters from online peers
|
|
7721
|
+
// (dark-peer-tolerant: a failed peer is a named row, never a 500).
|
|
7722
|
+
const selfActionPoolLimiter = rateLimiter(60_000, 6);
|
|
7723
|
+
router.get('/self-action-governor', (req, res) => {
|
|
7724
|
+
const gov = ctx.selfActionGovernor;
|
|
7725
|
+
if (!gov) {
|
|
7726
|
+
res.status(503).json({ error: 'self-action governor unavailable (not initialized on this build)' });
|
|
7727
|
+
return;
|
|
7728
|
+
}
|
|
7729
|
+
let posture;
|
|
7730
|
+
try {
|
|
7731
|
+
posture = gov.getPosture();
|
|
7732
|
+
}
|
|
7733
|
+
catch (err) {
|
|
7734
|
+
res.status(500).json({ error: `self-action governor read failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
7735
|
+
return;
|
|
7736
|
+
}
|
|
7737
|
+
const selfMachineId = ctx.meshSelfId ?? null;
|
|
7738
|
+
const body = {
|
|
7739
|
+
machineId: selfMachineId,
|
|
7740
|
+
generatedAt: new Date().toISOString(),
|
|
7741
|
+
emergencyDisable: posture.emergencyDisable,
|
|
7742
|
+
initialized: posture.initialized,
|
|
7743
|
+
classes: posture.classes,
|
|
7744
|
+
};
|
|
7745
|
+
if (req.query.scope !== 'pool') {
|
|
7746
|
+
res.json(body);
|
|
7747
|
+
return;
|
|
7748
|
+
}
|
|
7749
|
+
// Pool scope: pool-shared class counters are answerable POOL-WIDE (the
|
|
7750
|
+
// observability boundary matches the resource boundary); hardware-bound
|
|
7751
|
+
// counters stay machine-local. Non-recursive (peers get the plain route),
|
|
7752
|
+
// rate-limited (anti-amplification).
|
|
7753
|
+
selfActionPoolLimiter(req, res, async () => {
|
|
7754
|
+
const known = (ctx.listPoolMachines?.() ?? []).filter((m) => m.machineId !== selfMachineId);
|
|
7755
|
+
const machines = [];
|
|
7756
|
+
const failed = [];
|
|
7757
|
+
let peersQueried = 0;
|
|
7758
|
+
await Promise.all(known.map(async (m) => {
|
|
7759
|
+
const capacity = ctx.machinePoolRegistry?.getCapacity(m.machineId) ?? null;
|
|
7760
|
+
if (!m.lastKnownUrl) {
|
|
7761
|
+
failed.push({ machineId: m.machineId, reason: 'no-known-url' });
|
|
7762
|
+
return;
|
|
7763
|
+
}
|
|
7764
|
+
if (capacity && capacity.online === false) {
|
|
7765
|
+
failed.push({ machineId: m.machineId, reason: 'offline' });
|
|
7766
|
+
return;
|
|
7767
|
+
}
|
|
7768
|
+
const extraAllowlist = ctx.config.multiMachine?.peerUrlAllowlist;
|
|
7769
|
+
const verdict = isPeerUrlAllowedForCredentials(m.lastKnownUrl, extraAllowlist);
|
|
7770
|
+
if (!verdict.ok) {
|
|
7771
|
+
failed.push({ machineId: m.machineId, reason: 'url-rejected' });
|
|
7772
|
+
return;
|
|
7773
|
+
}
|
|
7774
|
+
peersQueried++;
|
|
7775
|
+
try {
|
|
7776
|
+
const r = await fetch(`${m.lastKnownUrl}/self-action-governor`, {
|
|
7777
|
+
headers: { Authorization: `Bearer ${ctx.config.authToken}` },
|
|
7778
|
+
signal: AbortSignal.timeout(5000),
|
|
7779
|
+
});
|
|
7780
|
+
if (r.status === 404) {
|
|
7781
|
+
failed.push({ machineId: m.machineId, reason: 'route-missing' });
|
|
7782
|
+
return;
|
|
7783
|
+
}
|
|
7784
|
+
if (!r.ok) {
|
|
7785
|
+
failed.push({ machineId: m.machineId, reason: r.status === 401 || r.status === 403 ? 'unauthorized' : 'error' });
|
|
7786
|
+
return;
|
|
7787
|
+
}
|
|
7788
|
+
const peerBody = (await r.json());
|
|
7789
|
+
machines.push({
|
|
7790
|
+
machineId: m.machineId,
|
|
7791
|
+
nickname: capacity?.nickname ?? m.nickname,
|
|
7792
|
+
emergencyDisable: peerBody.emergencyDisable ?? null,
|
|
7793
|
+
// Pool-shared classes ONLY (the resource boundary IS the
|
|
7794
|
+
// observability boundary — hardware-bound stays machine-local).
|
|
7795
|
+
classes: (peerBody.classes ?? []).filter((c) => c.resource === 'pool-shared'),
|
|
7796
|
+
});
|
|
7797
|
+
}
|
|
7798
|
+
catch {
|
|
7799
|
+
failed.push({ machineId: m.machineId, reason: 'unreachable' });
|
|
7800
|
+
}
|
|
7801
|
+
}));
|
|
7802
|
+
res.json({ ...body, scope: 'pool', pool: { selfMachineId, peersQueried, peersOk: machines.length, machines, failed } });
|
|
7803
|
+
});
|
|
7804
|
+
});
|
|
7601
7805
|
router.get('/test-runner-limiter', (_req, res) => {
|
|
7602
7806
|
try {
|
|
7603
7807
|
const status = getHostTestRunnerSemaphore().status();
|
|
@@ -20667,6 +20871,41 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
|
|
|
20667
20871
|
// Single source of truth (module-scope export) so the enableAction-validity
|
|
20668
20872
|
// test stays in lock-step with what the API actually accepts.
|
|
20669
20873
|
const allowedKeys = PATCHABLE_CONFIG_KEYS;
|
|
20874
|
+
// ── NESTED-PATH validator for the SelfActionGovernor subtree
|
|
20875
|
+
// (unified-self-action-backpressure INT9-1): top-level `intelligence` is
|
|
20876
|
+
// deliberately NOT allowlisted (that would Bearer-expose spawnCap etc.);
|
|
20877
|
+
// EXACTLY `intelligence.selfActionGovernor` is patchable, with DEEP merge
|
|
20878
|
+
// for this subtree (the generic one-level-deep merge would clobber
|
|
20879
|
+
// sibling per-class overrides — the full-block hazard). The DISABLE
|
|
20880
|
+
// direction (`emergencyDisable: true`) on this API path is
|
|
20881
|
+
// dashboard-PIN-gated (ADV9-4 — two verifier-independent valves remain:
|
|
20882
|
+
// the conversational config edit and the raw file edit, so this costs no
|
|
20883
|
+
// emergency availability); re-enable is Bearer-OK.
|
|
20884
|
+
let sagPatch = null;
|
|
20885
|
+
if ('intelligence' in patch) {
|
|
20886
|
+
const intel = patch.intelligence;
|
|
20887
|
+
const intelKeys = intel && typeof intel === 'object' && !Array.isArray(intel) ? Object.keys(intel) : null;
|
|
20888
|
+
if (!intelKeys || intelKeys.some((k) => k !== 'selfActionGovernor')) {
|
|
20889
|
+
res.status(400).json({
|
|
20890
|
+
error: 'Only intelligence.selfActionGovernor is patchable under `intelligence` via API (nested-path validator; other intelligence.* keys are not Bearer-exposed)',
|
|
20891
|
+
});
|
|
20892
|
+
return;
|
|
20893
|
+
}
|
|
20894
|
+
const sag = intel.selfActionGovernor;
|
|
20895
|
+
if (!sag || typeof sag !== 'object' || Array.isArray(sag)) {
|
|
20896
|
+
res.status(400).json({ error: 'intelligence.selfActionGovernor must be an object' });
|
|
20897
|
+
return;
|
|
20898
|
+
}
|
|
20899
|
+
if (sag.emergencyDisable === true) {
|
|
20900
|
+
// Know Your Principal: disarming the flood brake over the API needs
|
|
20901
|
+
// the operator's PIN — a Bearer token is structurally insufficient.
|
|
20902
|
+
if (!checkMandatePin(req, res))
|
|
20903
|
+
return; // response already sent
|
|
20904
|
+
}
|
|
20905
|
+
sagPatch = sag;
|
|
20906
|
+
delete patch.intelligence;
|
|
20907
|
+
delete patch.pin; // PIN is auth, never config
|
|
20908
|
+
}
|
|
20670
20909
|
const disallowed = Object.keys(patch).filter(k => !allowedKeys.has(k));
|
|
20671
20910
|
if (disallowed.length > 0) {
|
|
20672
20911
|
res.status(400).json({
|
|
@@ -20681,6 +20920,35 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
|
|
|
20681
20920
|
if (fs.existsSync(configPath)) {
|
|
20682
20921
|
fileConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
20683
20922
|
}
|
|
20923
|
+
// Deep merge for the validated selfActionGovernor subtree (multi-level
|
|
20924
|
+
// by design — INT9-1's one-level-deep-merge hazard note).
|
|
20925
|
+
if (sagPatch) {
|
|
20926
|
+
const deepMerge = (base, overlay) => {
|
|
20927
|
+
for (const [k, v] of Object.entries(overlay)) {
|
|
20928
|
+
if (v && typeof v === 'object' && !Array.isArray(v) && base[k] && typeof base[k] === 'object' && !Array.isArray(base[k])) {
|
|
20929
|
+
deepMerge(base[k], v);
|
|
20930
|
+
}
|
|
20931
|
+
else {
|
|
20932
|
+
base[k] = v;
|
|
20933
|
+
}
|
|
20934
|
+
}
|
|
20935
|
+
return base;
|
|
20936
|
+
};
|
|
20937
|
+
fileConfig.intelligence = fileConfig.intelligence && typeof fileConfig.intelligence === 'object' ? fileConfig.intelligence : {};
|
|
20938
|
+
fileConfig.intelligence.selfActionGovernor = deepMerge(fileConfig.intelligence.selfActionGovernor && typeof fileConfig.intelligence.selfActionGovernor === 'object'
|
|
20939
|
+
? fileConfig.intelligence.selfActionGovernor
|
|
20940
|
+
: {}, sagPatch);
|
|
20941
|
+
const rt = ctx.config;
|
|
20942
|
+
rt.intelligence = rt.intelligence && typeof rt.intelligence === 'object' ? rt.intelligence : {};
|
|
20943
|
+
rt.intelligence.selfActionGovernor = deepMerge(rt.intelligence.selfActionGovernor && typeof rt.intelligence.selfActionGovernor === 'object'
|
|
20944
|
+
? rt.intelligence.selfActionGovernor
|
|
20945
|
+
: {}, sagPatch);
|
|
20946
|
+
if (Object.keys(patch).length === 0) {
|
|
20947
|
+
fs.writeFileSync(configPath, JSON.stringify(fileConfig, null, 2) + '\n');
|
|
20948
|
+
res.json({ success: true, patched: ['intelligence.selfActionGovernor'], note: 'emergencyDisable is read live by the governor; per-class overrides apply on its next slow tick.' });
|
|
20949
|
+
return;
|
|
20950
|
+
}
|
|
20951
|
+
}
|
|
20684
20952
|
// Deep merge (one level deep — sufficient for feature toggles)
|
|
20685
20953
|
for (const [key, value] of Object.entries(patch)) {
|
|
20686
20954
|
if (typeof value === 'object' && value !== null && !Array.isArray(value) &&
|