instar 1.3.489 → 1.3.491
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 +30 -0
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +1124 -28
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +23 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/CodexResumeMap.d.ts +95 -0
- package/dist/core/CodexResumeMap.d.ts.map +1 -0
- package/dist/core/CodexResumeMap.js +283 -0
- package/dist/core/CodexResumeMap.js.map +1 -0
- package/dist/core/GuardPostureStore.d.ts +38 -0
- package/dist/core/GuardPostureStore.d.ts.map +1 -0
- package/dist/core/GuardPostureStore.js +87 -0
- package/dist/core/GuardPostureStore.js.map +1 -0
- package/dist/core/MachinePoolRegistry.d.ts +16 -0
- package/dist/core/MachinePoolRegistry.d.ts.map +1 -1
- package/dist/core/MachinePoolRegistry.js +24 -1
- package/dist/core/MachinePoolRegistry.js.map +1 -1
- package/dist/core/MeshRpc.d.ts +3 -0
- package/dist/core/MeshRpc.d.ts.map +1 -1
- package/dist/core/MeshRpc.js +5 -0
- package/dist/core/MeshRpc.js.map +1 -1
- package/dist/core/ModelSwapService.d.ts +26 -11
- package/dist/core/ModelSwapService.d.ts.map +1 -1
- package/dist/core/ModelSwapService.js +59 -21
- package/dist/core/ModelSwapService.js.map +1 -1
- package/dist/core/PeerPresencePuller.d.ts +9 -0
- package/dist/core/PeerPresencePuller.d.ts.map +1 -1
- package/dist/core/PeerPresencePuller.js +1 -1
- package/dist/core/PeerPresencePuller.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +16 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +92 -4
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/SessionManager.d.ts +4 -0
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +3 -0
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/SessionRefresh.d.ts +36 -7
- package/dist/core/SessionRefresh.d.ts.map +1 -1
- package/dist/core/SessionRefresh.js +90 -29
- package/dist/core/SessionRefresh.js.map +1 -1
- package/dist/core/TopicProfileOrchestrator.d.ts +480 -0
- package/dist/core/TopicProfileOrchestrator.d.ts.map +1 -0
- package/dist/core/TopicProfileOrchestrator.js +1404 -0
- package/dist/core/TopicProfileOrchestrator.js.map +1 -0
- package/dist/core/TopicProfileResolver.d.ts +104 -0
- package/dist/core/TopicProfileResolver.d.ts.map +1 -0
- package/dist/core/TopicProfileResolver.js +231 -0
- package/dist/core/TopicProfileResolver.js.map +1 -0
- package/dist/core/TopicProfileStore.d.ts +308 -0
- package/dist/core/TopicProfileStore.d.ts.map +1 -0
- package/dist/core/TopicProfileStore.js +781 -0
- package/dist/core/TopicProfileStore.js.map +1 -0
- package/dist/core/TopicProfileTransferCarrier.d.ts +227 -0
- package/dist/core/TopicProfileTransferCarrier.d.ts.map +1 -0
- package/dist/core/TopicProfileTransferCarrier.js +533 -0
- package/dist/core/TopicProfileTransferCarrier.js.map +1 -0
- package/dist/core/TopicResumeMap.d.ts +67 -1
- package/dist/core/TopicResumeMap.d.ts.map +1 -1
- package/dist/core/TopicResumeMap.js +114 -1
- package/dist/core/TopicResumeMap.js.map +1 -1
- package/dist/core/classifyProfileChange.d.ts +83 -0
- package/dist/core/classifyProfileChange.d.ts.map +1 -0
- package/dist/core/classifyProfileChange.js +274 -0
- package/dist/core/classifyProfileChange.js.map +1 -0
- package/dist/core/devGatedFeatures.d.ts.map +1 -1
- package/dist/core/devGatedFeatures.js +6 -0
- package/dist/core/devGatedFeatures.js.map +1 -1
- package/dist/core/frameworkSessionLaunch.d.ts +26 -0
- package/dist/core/frameworkSessionLaunch.d.ts.map +1 -1
- package/dist/core/frameworkSessionLaunch.js +58 -0
- package/dist/core/frameworkSessionLaunch.js.map +1 -1
- package/dist/core/slackRefreshBinding.d.ts +83 -0
- package/dist/core/slackRefreshBinding.d.ts.map +1 -0
- package/dist/core/slackRefreshBinding.js +54 -0
- package/dist/core/slackRefreshBinding.js.map +1 -0
- package/dist/core/topicProfileIngress.d.ts +128 -0
- package/dist/core/topicProfileIngress.d.ts.map +1 -0
- package/dist/core/topicProfileIngress.js +249 -0
- package/dist/core/topicProfileIngress.js.map +1 -0
- package/dist/core/topicProfileValidation.d.ts +109 -0
- package/dist/core/topicProfileValidation.d.ts.map +1 -0
- package/dist/core/topicProfileValidation.js +247 -0
- package/dist/core/topicProfileValidation.js.map +1 -0
- package/dist/core/topicProfileWriteSurface.d.ts +222 -0
- package/dist/core/topicProfileWriteSurface.d.ts.map +1 -0
- package/dist/core/topicProfileWriteSurface.js +631 -0
- package/dist/core/topicProfileWriteSurface.js.map +1 -0
- package/dist/core/types.d.ts +66 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/lifeline/TelegramLifeline.d.ts.map +1 -1
- package/dist/lifeline/TelegramLifeline.js +6 -0
- package/dist/lifeline/TelegramLifeline.js.map +1 -1
- package/dist/messaging/TelegramAdapter.d.ts +10 -1
- package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
- package/dist/messaging/TelegramAdapter.js +41 -1
- package/dist/messaging/TelegramAdapter.js.map +1 -1
- package/dist/monitoring/ActiveWorkSilenceSentinel.d.ts +8 -0
- package/dist/monitoring/ActiveWorkSilenceSentinel.d.ts.map +1 -1
- package/dist/monitoring/ActiveWorkSilenceSentinel.js +8 -0
- package/dist/monitoring/ActiveWorkSilenceSentinel.js.map +1 -1
- package/dist/monitoring/ContextWedgeSentinel.d.ts +8 -0
- package/dist/monitoring/ContextWedgeSentinel.d.ts.map +1 -1
- package/dist/monitoring/ContextWedgeSentinel.js +8 -0
- package/dist/monitoring/ContextWedgeSentinel.js.map +1 -1
- package/dist/monitoring/GuardPostureTripwire.d.ts +3 -27
- package/dist/monitoring/GuardPostureTripwire.d.ts.map +1 -1
- package/dist/monitoring/GuardPostureTripwire.js +8 -76
- package/dist/monitoring/GuardPostureTripwire.js.map +1 -1
- package/dist/monitoring/GuardRegistry.d.ts +48 -0
- package/dist/monitoring/GuardRegistry.d.ts.map +1 -0
- package/dist/monitoring/GuardRegistry.js +49 -0
- package/dist/monitoring/GuardRegistry.js.map +1 -0
- package/dist/monitoring/SessionReaper.d.ts +7 -0
- package/dist/monitoring/SessionReaper.d.ts.map +1 -1
- package/dist/monitoring/SessionReaper.js +9 -0
- package/dist/monitoring/SessionReaper.js.map +1 -1
- package/dist/monitoring/SessionWatchdog.d.ts +8 -0
- package/dist/monitoring/SessionWatchdog.d.ts.map +1 -1
- package/dist/monitoring/SessionWatchdog.js +8 -0
- package/dist/monitoring/SessionWatchdog.js.map +1 -1
- package/dist/monitoring/SocketDisconnectSentinel.d.ts +8 -0
- package/dist/monitoring/SocketDisconnectSentinel.d.ts.map +1 -1
- package/dist/monitoring/SocketDisconnectSentinel.js +8 -0
- package/dist/monitoring/SocketDisconnectSentinel.js.map +1 -1
- package/dist/monitoring/guardManifest.d.ts +67 -0
- package/dist/monitoring/guardManifest.d.ts.map +1 -0
- package/dist/monitoring/guardManifest.js +536 -0
- package/dist/monitoring/guardManifest.js.map +1 -0
- package/dist/monitoring/guardPosture.d.ts +77 -0
- package/dist/monitoring/guardPosture.d.ts.map +1 -0
- package/dist/monitoring/guardPosture.js +198 -0
- package/dist/monitoring/guardPosture.js.map +1 -0
- package/dist/monitoring/guardPostureView.d.ts +100 -0
- package/dist/monitoring/guardPostureView.d.ts.map +1 -0
- package/dist/monitoring/guardPostureView.js +294 -0
- package/dist/monitoring/guardPostureView.js.map +1 -0
- package/dist/monitoring/probes/GuardPostureProbe.d.ts +82 -0
- package/dist/monitoring/probes/GuardPostureProbe.d.ts.map +1 -0
- package/dist/monitoring/probes/GuardPostureProbe.js +384 -0
- package/dist/monitoring/probes/GuardPostureProbe.js.map +1 -0
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +18 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/scheduler/JobScheduler.d.ts +10 -0
- package/dist/scheduler/JobScheduler.d.ts.map +1 -1
- package/dist/scheduler/JobScheduler.js +12 -0
- package/dist/scheduler/JobScheduler.js.map +1 -1
- package/dist/server/AgentServer.d.ts +40 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +54 -3
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +12 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/peerUrlGuard.d.ts +35 -0
- package/dist/server/peerUrlGuard.d.ts.map +1 -0
- package/dist/server/peerUrlGuard.js +93 -0
- package/dist/server/peerUrlGuard.js.map +1 -0
- package/dist/server/routes.d.ts +31 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +407 -1
- package/dist/server/routes.js.map +1 -1
- package/package.json +3 -2
- package/scripts/lint-guard-manifest.js +264 -0
- package/src/data/builtin-manifest.json +67 -67
- package/src/data/state-coherence-registry.json +54 -18
- package/src/scaffold/templates.ts +18 -0
- package/upgrades/1.3.490.md +34 -0
- package/upgrades/1.3.491.md +37 -0
- package/upgrades/side-effects/guard-posture-endpoint.md +101 -0
- package/upgrades/side-effects/topic-profile.md +142 -0
package/dist/server/routes.js
CHANGED
|
@@ -32,6 +32,10 @@ import { CoherenceJournalReader, InvalidCursorError } from '../core/CoherenceJou
|
|
|
32
32
|
import { creditUsherOnOutbound } from '../core/UsherActedCorrelator.js';
|
|
33
33
|
import { validateWriteToken, canPerformOperation } from '../core/StateWriteAuthority.js';
|
|
34
34
|
import { DegradationReporter } from '../monitoring/DegradationReporter.js';
|
|
35
|
+
import { readGuardPostureBootSnapshot, resolveGuardConfigSnapshot, } from '../monitoring/guardPosture.js';
|
|
36
|
+
import { buildGuardInventory } from '../monitoring/guardPostureView.js';
|
|
37
|
+
import { GuardRegistry } from '../monitoring/GuardRegistry.js';
|
|
38
|
+
import { isPeerUrlAllowedForCredentials } from './peerUrlGuard.js';
|
|
35
39
|
import { FailureLedger } from '../monitoring/FailureLedger.js';
|
|
36
40
|
import { FailureAttributionEngine } from '../monitoring/FailureAttributionEngine.js';
|
|
37
41
|
import { FailureAnalyzer } from '../monitoring/FailureAnalyzer.js';
|
|
@@ -4270,6 +4274,138 @@ export function createRoutes(ctx) {
|
|
|
4270
4274
|
const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(Math.floor(rawLimit), 1000) : 200;
|
|
4271
4275
|
res.json({ entries: ctx.reapLog.read(limit) });
|
|
4272
4276
|
});
|
|
4277
|
+
// ── Guard Posture (GUARD-POSTURE-ENDPOINT-SPEC) ─────────────────────
|
|
4278
|
+
// Read-only, Bearer-auth (router-level middleware — NEVER added to any auth
|
|
4279
|
+
// exemption list: posture is an attack-timing oracle, spec §3). Reports
|
|
4280
|
+
// every shipped guard's HONEST effective state: config-on is not working
|
|
4281
|
+
// (the Mini's reaper lesson). Local inventory is fully synchronous (one
|
|
4282
|
+
// disk-read config snapshot + sync in-memory runtime getters, <100ms);
|
|
4283
|
+
// ?scope=pool fans out to peers with classified per-peer failures, never a
|
|
4284
|
+
// 500. Deliberately NO `guards.enabled` config gate — an off-switch on the
|
|
4285
|
+
// guard-visibility surface would itself be an invisible disabled guard
|
|
4286
|
+
// (always-on read-only observability; reap-log/TokenLedger precedent).
|
|
4287
|
+
const guardsPoolLimiter = rateLimiter(60_000, 6);
|
|
4288
|
+
router.get('/guards', async (req, res) => {
|
|
4289
|
+
const stateDir = ctx.config.stateDir;
|
|
4290
|
+
const projectDir = path.dirname(stateDir);
|
|
4291
|
+
const snapshot = resolveGuardConfigSnapshot(projectDir);
|
|
4292
|
+
if (snapshot.readError) {
|
|
4293
|
+
// Config-read failure is a top-level error — never a truthful-looking
|
|
4294
|
+
// empty inventory (spec §2.2).
|
|
4295
|
+
res.status(500).json({ error: `guard inventory unavailable: config read failed (${snapshot.readError})` });
|
|
4296
|
+
return;
|
|
4297
|
+
}
|
|
4298
|
+
const inventory = buildGuardInventory({
|
|
4299
|
+
snapshot,
|
|
4300
|
+
bootSnapshot: readGuardPostureBootSnapshot(stateDir),
|
|
4301
|
+
registry: ctx.guardRegistry ?? new GuardRegistry(),
|
|
4302
|
+
});
|
|
4303
|
+
const selfMachineId = ctx.meshSelfId ?? null;
|
|
4304
|
+
const body = {
|
|
4305
|
+
machineId: selfMachineId,
|
|
4306
|
+
nickname: selfMachineId
|
|
4307
|
+
? (ctx.machinePoolRegistry?.getCapacity(selfMachineId)?.nickname ?? null)
|
|
4308
|
+
: null,
|
|
4309
|
+
version: ProcessIntegrity.getInstance()?.runningVersion || ctx.config.version || '0.0.0',
|
|
4310
|
+
generatedAt: new Date().toISOString(),
|
|
4311
|
+
guards: inventory.guards,
|
|
4312
|
+
summary: inventory.summary,
|
|
4313
|
+
};
|
|
4314
|
+
if (req.query.scope !== 'pool') {
|
|
4315
|
+
res.json(body);
|
|
4316
|
+
return;
|
|
4317
|
+
}
|
|
4318
|
+
// scope=pool — every registered machine accounted for: a posture row or a
|
|
4319
|
+
// NAMED, classified failure row; silent omission re-creates the blind spot
|
|
4320
|
+
// at the pool level (spec §2.3). Non-recursive (peers get plain /guards),
|
|
4321
|
+
// rate-limited (anti-amplification, spec §3(d)).
|
|
4322
|
+
guardsPoolLimiter(req, res, async () => {
|
|
4323
|
+
const known = (ctx.listPoolMachines?.() ?? []).filter((m) => m.machineId !== selfMachineId);
|
|
4324
|
+
if (known.length === 0) {
|
|
4325
|
+
res.json({ ...body, pool: { enabled: false, peersQueried: 0, knownMachines: 0, machines: [], failed: [] } });
|
|
4326
|
+
return;
|
|
4327
|
+
}
|
|
4328
|
+
const extraAllowlist = ctx.config.multiMachine
|
|
4329
|
+
?.peerUrlAllowlist;
|
|
4330
|
+
const machines = [];
|
|
4331
|
+
const failed = [];
|
|
4332
|
+
let peersQueried = 0;
|
|
4333
|
+
await Promise.all(known.map(async (m) => {
|
|
4334
|
+
const capacity = ctx.machinePoolRegistry?.getCapacity(m.machineId) ?? null;
|
|
4335
|
+
const peerVersion = capacity?.hardware?.instarVersion;
|
|
4336
|
+
if (!m.lastKnownUrl) {
|
|
4337
|
+
failed.push({ machineId: m.machineId, reason: 'no-known-url' });
|
|
4338
|
+
return;
|
|
4339
|
+
}
|
|
4340
|
+
if (capacity && capacity.online === false) {
|
|
4341
|
+
// The heartbeat piggyback (durable last-known posture on the pool
|
|
4342
|
+
// view) covers dark peers — a doomed fetch buys nothing but timeout.
|
|
4343
|
+
failed.push({ machineId: m.machineId, reason: 'offline' });
|
|
4344
|
+
return;
|
|
4345
|
+
}
|
|
4346
|
+
const verdict = isPeerUrlAllowedForCredentials(m.lastKnownUrl, extraAllowlist);
|
|
4347
|
+
if (!verdict.ok) {
|
|
4348
|
+
// The Bearer token was NEVER attached (spec §3(c)) — visible, named.
|
|
4349
|
+
failed.push({ machineId: m.machineId, reason: 'url-rejected' });
|
|
4350
|
+
return;
|
|
4351
|
+
}
|
|
4352
|
+
peersQueried++;
|
|
4353
|
+
try {
|
|
4354
|
+
const r = await fetch(`${m.lastKnownUrl}/guards`, {
|
|
4355
|
+
headers: { Authorization: `Bearer ${ctx.config.authToken}` },
|
|
4356
|
+
signal: AbortSignal.timeout(5000),
|
|
4357
|
+
});
|
|
4358
|
+
if (r.status === 404) {
|
|
4359
|
+
// Pre-/guards peer version: "needs update to report", not a
|
|
4360
|
+
// phantom outage (rollout-skew honesty, spec §2.3).
|
|
4361
|
+
failed.push({ machineId: m.machineId, reason: 'route-missing', ...(peerVersion ? { version: peerVersion } : {}) });
|
|
4362
|
+
return;
|
|
4363
|
+
}
|
|
4364
|
+
if (r.status === 401 || r.status === 403) {
|
|
4365
|
+
failed.push({ machineId: m.machineId, reason: 'unauthorized' });
|
|
4366
|
+
return;
|
|
4367
|
+
}
|
|
4368
|
+
if (!r.ok) {
|
|
4369
|
+
failed.push({ machineId: m.machineId, reason: 'error' });
|
|
4370
|
+
return;
|
|
4371
|
+
}
|
|
4372
|
+
const peerBody = (await r.json());
|
|
4373
|
+
machines.push({
|
|
4374
|
+
// Merge identity: the row is keyed on the REGISTRY's machine
|
|
4375
|
+
// identity; the body's self-reported machineId is data and a
|
|
4376
|
+
// mismatch is FLAGGED, never allowed to shadow another row.
|
|
4377
|
+
machineId: m.machineId,
|
|
4378
|
+
nickname: capacity?.nickname ?? m.nickname,
|
|
4379
|
+
...(typeof peerBody.machineId === 'string' && peerBody.machineId !== m.machineId
|
|
4380
|
+
? { identityMismatch: true, claimedMachineId: peerBody.machineId }
|
|
4381
|
+
: {}),
|
|
4382
|
+
version: peerBody.version,
|
|
4383
|
+
generatedAt: peerBody.generatedAt,
|
|
4384
|
+
guards: peerBody.guards,
|
|
4385
|
+
summary: peerBody.summary,
|
|
4386
|
+
});
|
|
4387
|
+
}
|
|
4388
|
+
catch (err) {
|
|
4389
|
+
// Normalized enum only — raw err.message leaks URL/TLS internals.
|
|
4390
|
+
const name = err instanceof Error ? err.name : '';
|
|
4391
|
+
failed.push({
|
|
4392
|
+
machineId: m.machineId,
|
|
4393
|
+
reason: name === 'TimeoutError' || name === 'AbortError' ? 'timeout' : 'unreachable',
|
|
4394
|
+
});
|
|
4395
|
+
}
|
|
4396
|
+
}));
|
|
4397
|
+
res.json({
|
|
4398
|
+
...body,
|
|
4399
|
+
pool: {
|
|
4400
|
+
enabled: !!ctx.machinePoolRegistry,
|
|
4401
|
+
knownMachines: known.length,
|
|
4402
|
+
peersQueried,
|
|
4403
|
+
machines,
|
|
4404
|
+
failed,
|
|
4405
|
+
},
|
|
4406
|
+
});
|
|
4407
|
+
});
|
|
4408
|
+
});
|
|
4273
4409
|
// Coherence Journal merged read API (COHERENCE-JOURNAL-SPEC §3.5). The
|
|
4274
4410
|
// pull-surface answer to "which machine was topic N on / where are its
|
|
4275
4411
|
// artifacts?" — a bounded, honest-about-trust merged view over this machine's
|
|
@@ -4548,7 +4684,19 @@ export function createRoutes(ctx) {
|
|
|
4548
4684
|
return;
|
|
4549
4685
|
}
|
|
4550
4686
|
try {
|
|
4551
|
-
|
|
4687
|
+
// TOPIC-PROFILE-SPEC §9 — the in-flight swap is a live-session mutation:
|
|
4688
|
+
// serialize it through the topic's single-writer lock (orchestrator
|
|
4689
|
+
// runExclusive) so it can never interleave with a profile-triggered
|
|
4690
|
+
// kill/respawn on the same topic. The escalation reconciler hook drives
|
|
4691
|
+
// this same route, so one wrap covers both initiators. No orchestrator /
|
|
4692
|
+
// no topic binding ⇒ today's unserialized behavior (single-machine boot
|
|
4693
|
+
// window / non-topic sessions).
|
|
4694
|
+
const performSwap = () => ctx.modelTierSwap.swap(req.params.name, tier);
|
|
4695
|
+
const orch = ctx.topicProfile?.orchestrator ?? null;
|
|
4696
|
+
const swapTopicId = ctx.telegram?.getTopicForSession?.(req.params.name) ?? null;
|
|
4697
|
+
const result = orch && swapTopicId !== null
|
|
4698
|
+
? await orch.runExclusive(swapTopicId, performSwap)
|
|
4699
|
+
: await performSwap();
|
|
4552
4700
|
const httpStatus = result.status === 'refused'
|
|
4553
4701
|
? result.reason === 'unknown-session'
|
|
4554
4702
|
? 404
|
|
@@ -4566,6 +4714,236 @@ export function createRoutes(ctx) {
|
|
|
4566
4714
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
4567
4715
|
}
|
|
4568
4716
|
});
|
|
4717
|
+
// ── Topic Profile (TOPIC-PROFILE-SPEC §10.1 / §12) ────────────────────────
|
|
4718
|
+
// GET: the read surface (Bearer — profile entries carry the operator's
|
|
4719
|
+
// platform uid in updatedBy). POST: the TOKEN-TRUST write surface —
|
|
4720
|
+
// authenticated by Bearer + X-Instar-Request like sibling mutating routes,
|
|
4721
|
+
// recorded as `updatedBy:'api-token'`, NEVER operator-attributed; a
|
|
4722
|
+
// body-supplied updatedBy is ignored by construction. The §12.5 key clamp:
|
|
4723
|
+
// numeric Telegram topic id, or `slack:<channel>[:<thread>]`.
|
|
4724
|
+
const TOPIC_PROFILE_KEY_RE = /^(?:\d{1,16}|slack:[A-Za-z0-9._-]{1,64}(?::[0-9.]{1,32})?)$/;
|
|
4725
|
+
const topicProfileRefusalStatus = (reason) => {
|
|
4726
|
+
switch (reason) {
|
|
4727
|
+
case 'validation':
|
|
4728
|
+
case 'empty-patch':
|
|
4729
|
+
return 400;
|
|
4730
|
+
case 'no-bound-operator':
|
|
4731
|
+
case 'not-bound-operator':
|
|
4732
|
+
return 403;
|
|
4733
|
+
case 'disabled':
|
|
4734
|
+
case 'local-model-binding-active':
|
|
4735
|
+
case 'lock-timeout':
|
|
4736
|
+
case 'nothing-to-undo':
|
|
4737
|
+
case 'nothing-parked':
|
|
4738
|
+
case 'cooldown-confirm-required':
|
|
4739
|
+
return 409;
|
|
4740
|
+
case 'flush-failed':
|
|
4741
|
+
return 500;
|
|
4742
|
+
default:
|
|
4743
|
+
return 400;
|
|
4744
|
+
}
|
|
4745
|
+
};
|
|
4746
|
+
router.get('/topic-profile/:topicId', (req, res) => {
|
|
4747
|
+
if (!TOPIC_PROFILE_KEY_RE.test(req.params.topicId)) {
|
|
4748
|
+
res.status(400).json({ error: 'Invalid topic key — numeric topic id or slack:<channel>[:<thread>]' });
|
|
4749
|
+
return;
|
|
4750
|
+
}
|
|
4751
|
+
if (!ctx.topicProfile) {
|
|
4752
|
+
res.status(503).json({ error: 'Topic profiles not wired on this server' });
|
|
4753
|
+
return;
|
|
4754
|
+
}
|
|
4755
|
+
const key = req.params.topicId;
|
|
4756
|
+
const entry = ctx.topicProfile.store.get(key);
|
|
4757
|
+
const resolved = ctx.topicProfile.resolver.resolve(key);
|
|
4758
|
+
// §5.3 — reads name their staleness: while a transfer pull from the
|
|
4759
|
+
// previous owner is pending, the local entry is served (a slightly-stale
|
|
4760
|
+
// operator pin beats defaults) but flagged "possibly stale".
|
|
4761
|
+
let pendingTransferPull = false;
|
|
4762
|
+
try {
|
|
4763
|
+
pendingTransferPull = ctx.topicProfile.carrier?.hasPendingPull(key) ?? false;
|
|
4764
|
+
}
|
|
4765
|
+
catch {
|
|
4766
|
+
/* @silent-fallback-ok: the staleness flag is read-surface annotation only — a carrier read error must never fail the profile read; the flag defaults to false (TOPIC-PROFILE-SPEC §5.3) */
|
|
4767
|
+
}
|
|
4768
|
+
res.json({
|
|
4769
|
+
topicId: key,
|
|
4770
|
+
resolved: {
|
|
4771
|
+
framework: resolved.framework,
|
|
4772
|
+
model: resolved.model ?? null,
|
|
4773
|
+
modelTier: resolved.modelTier,
|
|
4774
|
+
thinkingMode: resolved.thinkingMode ?? null,
|
|
4775
|
+
escalationOverride: resolved.escalationOverride,
|
|
4776
|
+
sources: resolved.sources,
|
|
4777
|
+
},
|
|
4778
|
+
// Once-per-transition fallback notices that fired on THIS resolution
|
|
4779
|
+
// (the read consumed them — surfaced here rather than dropped).
|
|
4780
|
+
notices: resolved.notices,
|
|
4781
|
+
pin: entry?.current ?? null,
|
|
4782
|
+
previous: entry?.previous ?? null,
|
|
4783
|
+
parked: entry?.parked ?? null,
|
|
4784
|
+
intendedProfile: entry?.intendedProfile ?? null,
|
|
4785
|
+
breakerCount: entry?.breakerCount ?? 0,
|
|
4786
|
+
// §5.3 staleness annotation: true while a pull from this topic's
|
|
4787
|
+
// previous owner is pending — the entry above is "as of when this topic
|
|
4788
|
+
// last lived here — possibly stale".
|
|
4789
|
+
pendingTransferPull,
|
|
4790
|
+
...(pendingTransferPull && entry?.current
|
|
4791
|
+
? { staleness: 'pending-transfer-pull — entry is as of when this topic last lived on this machine (possibly stale)' }
|
|
4792
|
+
: {}),
|
|
4793
|
+
});
|
|
4794
|
+
});
|
|
4795
|
+
const requireTopicProfileWrite = (req, res) => {
|
|
4796
|
+
if (req.headers['x-instar-request'] !== '1') {
|
|
4797
|
+
res.status(403).json({ error: 'Topic-profile writes require the X-Instar-Request: 1 intent header' });
|
|
4798
|
+
return null;
|
|
4799
|
+
}
|
|
4800
|
+
if (!TOPIC_PROFILE_KEY_RE.test(req.params.topicId)) {
|
|
4801
|
+
res.status(400).json({ error: 'Invalid topic key — numeric topic id or slack:<channel>[:<thread>]' });
|
|
4802
|
+
return null;
|
|
4803
|
+
}
|
|
4804
|
+
if (!ctx.topicProfile) {
|
|
4805
|
+
res.status(503).json({ error: 'Topic profiles not wired on this server' });
|
|
4806
|
+
return null;
|
|
4807
|
+
}
|
|
4808
|
+
return { key: req.params.topicId, surface: ctx.topicProfile.surface };
|
|
4809
|
+
};
|
|
4810
|
+
router.post('/topic-profile/:topicId', async (req, res) => {
|
|
4811
|
+
const gate = requireTopicProfileWrite(req, res);
|
|
4812
|
+
if (!gate)
|
|
4813
|
+
return;
|
|
4814
|
+
const body = (req.body ?? {});
|
|
4815
|
+
const patch = {};
|
|
4816
|
+
for (const field of ['framework', 'model', 'modelTier', 'thinkingMode', 'escalationOverride']) {
|
|
4817
|
+
if (!(field in body))
|
|
4818
|
+
continue;
|
|
4819
|
+
const value = body[field];
|
|
4820
|
+
if (value !== null && typeof value !== 'string') {
|
|
4821
|
+
res.status(400).json({ error: `"${field}" must be a string or null` });
|
|
4822
|
+
return;
|
|
4823
|
+
}
|
|
4824
|
+
patch[field] = value;
|
|
4825
|
+
}
|
|
4826
|
+
// §10.1: a body-supplied updatedBy is IGNORED — the principal is the
|
|
4827
|
+
// shared token, stamped server-side as 'api-token'.
|
|
4828
|
+
try {
|
|
4829
|
+
const result = await gate.surface.applyWrite({
|
|
4830
|
+
topicKey: gate.key,
|
|
4831
|
+
patch,
|
|
4832
|
+
principal: { kind: 'token' },
|
|
4833
|
+
origin: 'http',
|
|
4834
|
+
});
|
|
4835
|
+
if (!result.ok) {
|
|
4836
|
+
res.status(topicProfileRefusalStatus(result.refusal?.reason)).json({
|
|
4837
|
+
ok: false,
|
|
4838
|
+
reason: result.refusal?.reason,
|
|
4839
|
+
...(result.refusal?.validation
|
|
4840
|
+
? { validation: { field: result.refusal.validation.field, failure: result.refusal.validation.failure, reason: result.refusal.validation.reason } }
|
|
4841
|
+
: {}),
|
|
4842
|
+
message: result.reply,
|
|
4843
|
+
});
|
|
4844
|
+
return;
|
|
4845
|
+
}
|
|
4846
|
+
res.json({
|
|
4847
|
+
ok: true,
|
|
4848
|
+
appliedLive: result.appliedLive ?? [],
|
|
4849
|
+
shadowed: result.shadowed ?? [],
|
|
4850
|
+
refusedFields: result.refusedFields ?? [],
|
|
4851
|
+
noop: result.noop ?? false,
|
|
4852
|
+
supersededParked: result.supersededParked ?? false,
|
|
4853
|
+
message: result.reply,
|
|
4854
|
+
pin: ctx.topicProfile?.store.resolve(gate.key) ?? null,
|
|
4855
|
+
});
|
|
4856
|
+
}
|
|
4857
|
+
catch (err) {
|
|
4858
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
4859
|
+
}
|
|
4860
|
+
});
|
|
4861
|
+
// §10.1 propose lane — registers an AGENT-COMPOSED structured delta in the
|
|
4862
|
+
// server-side slot and sends the SERVER-rendered echo to the topic; the
|
|
4863
|
+
// operator's first-party "yes" (recognized by the ingress parse) converts
|
|
4864
|
+
// it to the operator-attributed write. Never writes the store itself.
|
|
4865
|
+
router.post('/topic-profile/:topicId/propose', async (req, res) => {
|
|
4866
|
+
const gate = requireTopicProfileWrite(req, res);
|
|
4867
|
+
if (!gate)
|
|
4868
|
+
return;
|
|
4869
|
+
if (!ctx.topicProfile || !ctx.telegram || !/^\d+$/.test(gate.key)) {
|
|
4870
|
+
res.status(409).json({ error: 'Propose-confirm needs a platform-bound numeric topic on this server' });
|
|
4871
|
+
return;
|
|
4872
|
+
}
|
|
4873
|
+
const body = (req.body ?? {});
|
|
4874
|
+
const patch = {};
|
|
4875
|
+
for (const field of ['framework', 'model', 'modelTier', 'thinkingMode', 'escalationOverride']) {
|
|
4876
|
+
if (!(field in body))
|
|
4877
|
+
continue;
|
|
4878
|
+
const value = body[field];
|
|
4879
|
+
if (value !== null && typeof value !== 'string') {
|
|
4880
|
+
res.status(400).json({ error: `"${field}" must be a string or null` });
|
|
4881
|
+
return;
|
|
4882
|
+
}
|
|
4883
|
+
patch[field] = value;
|
|
4884
|
+
}
|
|
4885
|
+
const rendered = gate.surface.renderProposalEcho(gate.key, patch);
|
|
4886
|
+
if (!rendered.ok) {
|
|
4887
|
+
res.status(400).json({ ok: false, message: rendered.reply });
|
|
4888
|
+
return;
|
|
4889
|
+
}
|
|
4890
|
+
const armed = ctx.topicProfile.confirmSlots.arm(gate.key, 'propose-confirm', rendered.patch, rendered.echo, 'agent-composed');
|
|
4891
|
+
if (!armed.ok) {
|
|
4892
|
+
res.status(429).json({ ok: false, reason: armed.reason, message: 'Proposal churn cooldown — re-state the intent later' });
|
|
4893
|
+
return;
|
|
4894
|
+
}
|
|
4895
|
+
try {
|
|
4896
|
+
const sent = await ctx.telegram.sendToTopic(Number(gate.key), rendered.echo);
|
|
4897
|
+
ctx.topicProfile.confirmSlots.recordEchoMessageId(gate.key, sent.messageId);
|
|
4898
|
+
res.json({ ok: true, echo: rendered.echo, superseded: armed.superseded });
|
|
4899
|
+
}
|
|
4900
|
+
catch (err) {
|
|
4901
|
+
// Echo undelivered — tear the slot down (an unseen echo must never be
|
|
4902
|
+
// confirmable; matchConfirm would refuse on no-echo-id anyway).
|
|
4903
|
+
ctx.topicProfile.confirmSlots.disarm(gate.key);
|
|
4904
|
+
res.status(502).json({ ok: false, error: `Echo delivery failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
4905
|
+
}
|
|
4906
|
+
});
|
|
4907
|
+
// §10.3 / §5.2(b) recovery surfaces (token-trust like the main POST).
|
|
4908
|
+
router.post('/topic-profile/:topicId/undo', async (req, res) => {
|
|
4909
|
+
const gate = requireTopicProfileWrite(req, res);
|
|
4910
|
+
if (!gate)
|
|
4911
|
+
return;
|
|
4912
|
+
const result = await gate.surface.undo({ topicKey: gate.key, principal: { kind: 'token' }, origin: 'http' });
|
|
4913
|
+
if (!result.ok) {
|
|
4914
|
+
res.status(topicProfileRefusalStatus(result.refusal?.reason)).json({ ok: false, reason: result.refusal?.reason, message: result.reply });
|
|
4915
|
+
return;
|
|
4916
|
+
}
|
|
4917
|
+
res.json({ ok: true, message: result.reply, pin: ctx.topicProfile?.store.resolve(gate.key) ?? null });
|
|
4918
|
+
});
|
|
4919
|
+
router.post('/topic-profile/:topicId/clear', async (req, res) => {
|
|
4920
|
+
const gate = requireTopicProfileWrite(req, res);
|
|
4921
|
+
if (!gate)
|
|
4922
|
+
return;
|
|
4923
|
+
const result = await gate.surface.clear({ topicKey: gate.key, principal: { kind: 'token' }, origin: 'http' });
|
|
4924
|
+
if (!result.ok) {
|
|
4925
|
+
res.status(topicProfileRefusalStatus(result.refusal?.reason)).json({ ok: false, reason: result.refusal?.reason, message: result.reply });
|
|
4926
|
+
return;
|
|
4927
|
+
}
|
|
4928
|
+
res.json({ ok: true, message: result.reply, pin: ctx.topicProfile?.store.resolve(gate.key) ?? null });
|
|
4929
|
+
});
|
|
4930
|
+
router.post('/topic-profile/:topicId/reapply', async (req, res) => {
|
|
4931
|
+
const gate = requireTopicProfileWrite(req, res);
|
|
4932
|
+
if (!gate)
|
|
4933
|
+
return;
|
|
4934
|
+
const confirmed = (req.body ?? {}).confirm === true;
|
|
4935
|
+
const result = await gate.surface.reapply({ topicKey: gate.key, principal: { kind: 'token' }, origin: 'http', confirmed });
|
|
4936
|
+
if (!result.ok) {
|
|
4937
|
+
res.status(topicProfileRefusalStatus(result.refusal?.reason)).json({
|
|
4938
|
+
ok: false,
|
|
4939
|
+
reason: result.refusal?.reason,
|
|
4940
|
+
needsConfirm: result.needsConfirm ?? false,
|
|
4941
|
+
message: result.reply,
|
|
4942
|
+
});
|
|
4943
|
+
return;
|
|
4944
|
+
}
|
|
4945
|
+
res.json({ ok: true, message: result.reply, pin: ctx.topicProfile?.store.resolve(gate.key) ?? null });
|
|
4946
|
+
});
|
|
4569
4947
|
// Rate limit session spawning — each session is a real Claude Code process.
|
|
4570
4948
|
// Default: 10 spawns per 60 seconds, which is generous for normal use.
|
|
4571
4949
|
const spawnLimiter = rateLimiter(60_000, 10);
|
|
@@ -8971,6 +9349,21 @@ export function createRoutes(ctx) {
|
|
|
8971
9349
|
};
|
|
8972
9350
|
if (casAndJournal({ type: 'place', machineId: target }, 'tplace')) {
|
|
8973
9351
|
placedOwnership = casAndJournal({ type: 'claim', machineId: target }, 'tclaim');
|
|
9352
|
+
// TOPIC-PROFILE-SPEC §5.3 acquire seam (deterministic-transfer arm):
|
|
9353
|
+
// when THIS machine is the transfer target, the landed tclaim is an
|
|
9354
|
+
// ownership ACQUIRE — pull the topic's profile from its previous
|
|
9355
|
+
// owner. (target !== self: the target machine fires its own acquire
|
|
9356
|
+
// seam when the first forwarded message lands — deliverMessage
|
|
9357
|
+
// onAccepted; firing here would stage a pull on a non-owner, which
|
|
9358
|
+
// the carrier's apply-time ownership recheck would just discard.)
|
|
9359
|
+
if (placedOwnership && target === self && prevOwner && prevOwner !== self) {
|
|
9360
|
+
try {
|
|
9361
|
+
ctx.topicProfile?.carrier?.onTopicAcquired(topicId, prevOwner);
|
|
9362
|
+
}
|
|
9363
|
+
catch {
|
|
9364
|
+
/* @silent-fallback-ok: the §5.3 pull is fire-and-forget enrichment of the transfer — a carrier failure must never fail the transfer itself; the durable retry ledger re-files on the next acquire/tick (TOPIC-PROFILE-SPEC §5.3) */
|
|
9365
|
+
}
|
|
9366
|
+
}
|
|
8974
9367
|
}
|
|
8975
9368
|
else {
|
|
8976
9369
|
// A resting 'placing' record already naming the target (legacy pre-fix
|
|
@@ -8979,6 +9372,15 @@ export function createRoutes(ctx) {
|
|
|
8979
9372
|
const cur = ctx.sessionOwnershipRegistry.read(topicId);
|
|
8980
9373
|
if (cur?.status === 'placing' && cur.ownerMachineId === target) {
|
|
8981
9374
|
placedOwnership = casAndJournal({ type: 'claim', machineId: target }, 'tclaim');
|
|
9375
|
+
// §5.3 acquire seam — same self-target arm as the place→claim path above.
|
|
9376
|
+
if (placedOwnership && target === self && prevOwner && prevOwner !== self) {
|
|
9377
|
+
try {
|
|
9378
|
+
ctx.topicProfile?.carrier?.onTopicAcquired(topicId, prevOwner);
|
|
9379
|
+
}
|
|
9380
|
+
catch {
|
|
9381
|
+
/* @silent-fallback-ok: fire-and-forget §5.3 pull — never fails the transfer; the durable ledger re-files (TOPIC-PROFILE-SPEC §5.3) */
|
|
9382
|
+
}
|
|
9383
|
+
}
|
|
8982
9384
|
}
|
|
8983
9385
|
}
|
|
8984
9386
|
}
|
|
@@ -11409,6 +11811,10 @@ export function createRoutes(ctx) {
|
|
|
11409
11811
|
firstName: fromFirstName,
|
|
11410
11812
|
messageThreadId: topicId,
|
|
11411
11813
|
viaLifeline: true,
|
|
11814
|
+
// TOPIC-PROFILE-SPEC §10.1 round-5: forwarded content never matches
|
|
11815
|
+
// any profile-ingress recognition. An upgraded lifeline sets this;
|
|
11816
|
+
// its absence (older lifeline) reads as not-forwarded.
|
|
11817
|
+
...(req.body.forwarded === true ? { forwarded: true } : {}),
|
|
11412
11818
|
},
|
|
11413
11819
|
};
|
|
11414
11820
|
try {
|