instar 1.3.310 → 1.3.311

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.
@@ -17,6 +17,7 @@ import { planTransferByNickname } from '../core/TransferByNickname.js';
17
17
  import { IntelligenceRouter } from '../core/IntelligenceRouter.js';
18
18
  import { knownComponents } from '../core/componentCategories.js';
19
19
  import { SecretStore } from '../core/SecretStore.js';
20
+ import { writeConfigAtomic, readSelfKnowledgeFlags } from '../core/BootSelfKnowledge.js';
20
21
  import { rateLimiter, signViewPath } from './middleware.js';
21
22
  import { writeLifelineRestartSignal } from '../core/version-skew.js';
22
23
  import { readSessionClocks } from '../core/SessionClockReader.js';
@@ -11225,6 +11226,140 @@ export function createRoutes(ctx) {
11225
11226
  res.status(500).json({ error: err instanceof Error ? err.message : 'Failed to read preferences' });
11226
11227
  }
11227
11228
  });
11229
+ // ── Session Boot Self-Knowledge (spec: session-boot-self-knowledge.md) ──
11230
+ //
11231
+ // The "what I already have" block the session-start hook injects at boot:
11232
+ // vault secret NAMES (never values; the same secretKeyPaths derivation as
11233
+ // /secrets/sync-status, presented depth-capped) + self-asserted operational
11234
+ // facts. Deterministic config/capability discovery — SIGNAL-ONLY, gates
11235
+ // nothing. NOTE: independent of the SelfKnowledgeTree (search/validate/
11236
+ // health above) — both are self-knowledge surfaces; this one is the
11237
+ // deterministic boot inventory.
11238
+ //
11239
+ // Availability rides the developmentAgent gate (`enabled ?? !!developmentAgent`
11240
+ // — dark fleet / live dev-agent; the live-fleet flip is a tracked follow-up,
11241
+ // spec §Availability). Flags + facts are read FRESH from disk per request
11242
+ // (BootSelfKnowledge re-reads config.json) so enable/disable + fact edits
11243
+ // take effect without a server restart.
11244
+ //
11245
+ // A decrypt failure is a 200 with `vaultState:'decrypt-failed'` and the
11246
+ // hands-off warning block — NEVER a 500 (the hook's `curl -sf` swallows 5xx,
11247
+ // which would silently hide the exact warning the honesty rule exists to
11248
+ // deliver).
11249
+ router.get('/self-knowledge/session-context', async (req, res) => {
11250
+ try {
11251
+ const configPath = path.join(ctx.config.projectDir, '.instar', 'config.json');
11252
+ const { BootSelfKnowledge, DEFAULT_MAX_BYTES } = await import('../core/BootSelfKnowledge.js');
11253
+ const freshFlags = readSelfKnowledgeFlags(configPath);
11254
+ const enabled = freshFlags.enabled ?? Boolean(ctx.config.developmentAgent);
11255
+ if (!enabled) {
11256
+ res.status(503).json({ error: 'self-knowledge session-context disabled' });
11257
+ return;
11258
+ }
11259
+ const maxBytes = typeof freshFlags.maxInjectedBytes === 'number' && freshFlags.maxInjectedBytes > 0
11260
+ ? freshFlags.maxInjectedBytes
11261
+ : DEFAULT_MAX_BYTES;
11262
+ const full = String(req.query.full ?? '') === '1';
11263
+ const bsk = new BootSelfKnowledge({ stateDir: ctx.config.stateDir, configPath });
11264
+ const result = bsk.sessionContext(maxBytes, { full });
11265
+ console.log(`[boot-self-knowledge] served names=${result.names.length} facts=${result.factCount} vaultState=${result.vaultState} bytes=${Buffer.byteLength(result.block, 'utf8')}`);
11266
+ if (result.vaultState === 'decrypt-failed') {
11267
+ console.warn('[boot-self-knowledge] vault DECRYPT-FAILED — served hands-off warning block (not an empty vault)');
11268
+ }
11269
+ res.json(result);
11270
+ }
11271
+ catch (err) {
11272
+ res.status(500).json({ error: err instanceof Error ? err.message : 'Failed to build boot self-knowledge' });
11273
+ }
11274
+ });
11275
+ // Operational-facts writer (No-Manual-Work: nobody hand-edits config.json).
11276
+ // POST appends a fact (stamped {fact, updatedAt, machine}); DELETE removes by
11277
+ // {match} (preferred; 409 when ambiguous) or {index, expect} (409 on
11278
+ // mismatch — closes the read-then-delete index race). Both write through
11279
+ // writeConfigAtomic() (re-read → mutate → temp+rename; last-writer-wins
11280
+ // semantics vs the other, pre-existing non-atomic config writers).
11281
+ router.post('/self-knowledge/facts', async (req, res) => {
11282
+ try {
11283
+ const { MAX_FACT_CHARS, MAX_FACTS_STORED } = await import('../core/BootSelfKnowledge.js');
11284
+ const fact = typeof req.body?.fact === 'string' ? req.body.fact.trim() : '';
11285
+ if (!fact) {
11286
+ res.status(400).json({ error: 'fact must be a non-empty string' });
11287
+ return;
11288
+ }
11289
+ if (fact.length > MAX_FACT_CHARS) {
11290
+ res.status(400).json({ error: `fact exceeds ${MAX_FACT_CHARS} chars` });
11291
+ return;
11292
+ }
11293
+ const configPath = path.join(ctx.config.projectDir, '.instar', 'config.json');
11294
+ const outcome = writeConfigAtomic(configPath, (cfg) => {
11295
+ const sk = (cfg.selfKnowledge ??= {});
11296
+ const facts = (sk.operationalFacts ??= []);
11297
+ const existing = facts.map((f) => (typeof f === 'string' ? f : f?.fact));
11298
+ if (existing.includes(fact))
11299
+ return { error: { status: 409, message: 'duplicate fact' } };
11300
+ if (facts.length >= MAX_FACTS_STORED) {
11301
+ return { error: { status: 409, message: `fact cap reached (${MAX_FACTS_STORED}) — remove one first` } };
11302
+ }
11303
+ facts.push({ fact, updatedAt: new Date().toISOString(), machine: os.hostname() });
11304
+ return { value: facts };
11305
+ });
11306
+ if (outcome.error) {
11307
+ res.status(outcome.error.status).json({ error: outcome.error.message });
11308
+ return;
11309
+ }
11310
+ console.log(`[boot-self-knowledge] fact-added (${fact.slice(0, 60)}${fact.length > 60 ? '…' : ''})`);
11311
+ res.json({ success: true, facts: outcome.value });
11312
+ }
11313
+ catch (err) {
11314
+ res.status(500).json({ error: err instanceof Error ? err.message : 'Failed to add fact' });
11315
+ }
11316
+ });
11317
+ router.delete('/self-knowledge/facts', (req, res) => {
11318
+ try {
11319
+ const match = typeof req.body?.match === 'string' ? req.body.match : null;
11320
+ const index = typeof req.body?.index === 'number' ? req.body.index : null;
11321
+ const expect = typeof req.body?.expect === 'string' ? req.body.expect : null;
11322
+ if (!match && index === null) {
11323
+ res.status(400).json({ error: 'provide {match} or {index, expect}' });
11324
+ return;
11325
+ }
11326
+ const configPath = path.join(ctx.config.projectDir, '.instar', 'config.json');
11327
+ const outcome = writeConfigAtomic(configPath, (cfg) => {
11328
+ const sk = cfg.selfKnowledge;
11329
+ const facts = sk?.operationalFacts ?? [];
11330
+ const text = (f) => (typeof f === 'string' ? f : (f?.fact ?? ''));
11331
+ let removeAt = -1;
11332
+ if (match) {
11333
+ const hits = facts.map((f, i) => [text(f), i]).filter(([t]) => t.includes(match));
11334
+ if (hits.length === 0)
11335
+ return { error: { status: 404, message: 'no fact matches' } };
11336
+ if (hits.length > 1)
11337
+ return { error: { status: 409, message: `ambiguous match (${hits.length} facts) — narrow it` } };
11338
+ removeAt = hits[0][1];
11339
+ }
11340
+ else {
11341
+ if (index === null || index < 0 || index >= facts.length) {
11342
+ return { error: { status: 404, message: 'index out of range' } };
11343
+ }
11344
+ if (!expect || text(facts[index]) !== expect) {
11345
+ return { error: { status: 409, message: 'expect does not match the fact at that index (it may have moved) — re-read and retry' } };
11346
+ }
11347
+ removeAt = index;
11348
+ }
11349
+ const [removed] = facts.splice(removeAt, 1);
11350
+ return { value: { removed: text(removed), facts } };
11351
+ });
11352
+ if (outcome.error) {
11353
+ res.status(outcome.error.status).json({ error: outcome.error.message });
11354
+ return;
11355
+ }
11356
+ console.log(`[boot-self-knowledge] fact-removed (${String(outcome.value?.removed ?? '').slice(0, 60)})`);
11357
+ res.json({ success: true, ...outcome.value });
11358
+ }
11359
+ catch (err) {
11360
+ res.status(500).json({ error: err instanceof Error ? err.message : 'Failed to remove fact' });
11361
+ }
11362
+ });
11228
11363
  // ── Corrections (Correction & Preference Learning Sentinel, Slice 1b) ──
11229
11364
  //
11230
11365
  // Read surface over the CorrectionLedger — distilled, scrubbed correction /