instar 1.3.583 → 1.3.585

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.
Files changed (46) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +15 -1
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  5. package/dist/config/ConfigDefaults.js +13 -0
  6. package/dist/config/ConfigDefaults.js.map +1 -1
  7. package/dist/core/PlaywrightProfileRegistry.d.ts +269 -0
  8. package/dist/core/PlaywrightProfileRegistry.d.ts.map +1 -0
  9. package/dist/core/PlaywrightProfileRegistry.js +640 -0
  10. package/dist/core/PlaywrightProfileRegistry.js.map +1 -0
  11. package/dist/core/PostUpdateMigrator.d.ts +21 -0
  12. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  13. package/dist/core/PostUpdateMigrator.js +195 -0
  14. package/dist/core/PostUpdateMigrator.js.map +1 -1
  15. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  16. package/dist/core/devGatedFeatures.js +6 -0
  17. package/dist/core/devGatedFeatures.js.map +1 -1
  18. package/dist/core/types.d.ts +13 -0
  19. package/dist/core/types.d.ts.map +1 -1
  20. package/dist/core/types.js.map +1 -1
  21. package/dist/messaging/MessageProcessingLedger.d.ts +19 -2
  22. package/dist/messaging/MessageProcessingLedger.d.ts.map +1 -1
  23. package/dist/messaging/MessageProcessingLedger.js +25 -4
  24. package/dist/messaging/MessageProcessingLedger.js.map +1 -1
  25. package/dist/messaging/stuckMessageRecovery.d.ts +10 -0
  26. package/dist/messaging/stuckMessageRecovery.d.ts.map +1 -1
  27. package/dist/messaging/stuckMessageRecovery.js +13 -5
  28. package/dist/messaging/stuckMessageRecovery.js.map +1 -1
  29. package/dist/scaffold/templates.d.ts.map +1 -1
  30. package/dist/scaffold/templates.js +8 -1
  31. package/dist/scaffold/templates.js.map +1 -1
  32. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  33. package/dist/server/CapabilityIndex.js +1 -0
  34. package/dist/server/CapabilityIndex.js.map +1 -1
  35. package/dist/server/routes.d.ts +8 -0
  36. package/dist/server/routes.d.ts.map +1 -1
  37. package/dist/server/routes.js +341 -0
  38. package/dist/server/routes.js.map +1 -1
  39. package/package.json +1 -1
  40. package/src/data/builtin-manifest.json +63 -63
  41. package/src/scaffold/templates.ts +9 -1
  42. package/upgrades/1.3.584.md +84 -0
  43. package/upgrades/1.3.585.md +48 -0
  44. package/upgrades/side-effects/playwright-profile-registry.md +140 -0
  45. package/upgrades/side-effects/wedge-recovery-abandoned-notice.md +47 -0
  46. package/upgrades/1.3.583.md +0 -51
@@ -24,6 +24,8 @@ import { planTransferByNickname } from '../core/TransferByNickname.js';
24
24
  import { IntelligenceRouter } from '../core/IntelligenceRouter.js';
25
25
  import { knownComponents } from '../core/componentCategories.js';
26
26
  import { SecretStore } from '../core/SecretStore.js';
27
+ import { secretKeyPaths } from '../core/SecretSync.js';
28
+ import { PlaywrightProfileRegistry, PlaywrightRegistryError, PlaywrightRegistryCorruptError, DEFAULT_BLOCK_MAX_BYTES as PLAYWRIGHT_BLOCK_MAX_BYTES, } from '../core/PlaywrightProfileRegistry.js';
27
29
  import { writeConfigAtomic, readSelfKnowledgeFlags } from '../core/BootSelfKnowledge.js';
28
30
  import { rateLimiter, signViewPath, OUTBOUND_GATE_REVIEW_BUDGET_MS } from './middleware.js';
29
31
  import { reviewWithinBudget } from './outboundGateBudget.js';
@@ -15100,6 +15102,345 @@ export function createRoutes(ctx) {
15100
15102
  res.status(500).json({ error: err instanceof Error ? err.message : 'Failed to remove fact' });
15101
15103
  }
15102
15104
  });
15105
+ // ── Playwright Profile Registry (docs/specs/playwright-profile-registry.md) ──
15106
+ //
15107
+ // The durable per-agent map of which Playwright browser PROFILE holds which
15108
+ // logged-in ACCOUNT (by vault secret NAME only — NEVER value), plus the compact
15109
+ // boot-awareness surface and the activate switch. The WHOLE feature is dev-gated
15110
+ // (D4): the `enabled` flag is read FRESH from disk per request and resolved
15111
+ // through resolveDevAgentGate (LIVE on a dev agent, DARK on the fleet → 503),
15112
+ // exactly like /self-knowledge/session-context. The registry is constructed
15113
+ // per-request from ctx.config (so flags + the vault-name list are always fresh),
15114
+ // with listVaultNames sourced from the SAME secretKeyPaths derivation the boot
15115
+ // self-knowledge surface uses (vault unreadable → null → ref-validation fails
15116
+ // CLOSED per D17).
15117
+ //
15118
+ // Errors: PlaywrightRegistryError.status → HTTP status (400/404/409/422);
15119
+ // PlaywrightRegistryCorruptError → 500 "registry file corrupt — will not
15120
+ // overwrite" (writes fail CLOSED, never auto-overwrite — D15).
15121
+ //
15122
+ // Audit (D20): every successful write (create/assign/patch/delete/activate)
15123
+ // appends ONE JSON line to logs/playwright-profiles.jsonl (writer session id if
15124
+ // available, action, old→new summary, dryRun flag). Vault NAMES only.
15125
+ /** Fresh per-request read of the playwrightRegistry config block (no restart needed). */
15126
+ const readPlaywrightFlags = () => {
15127
+ try {
15128
+ const configPath = path.join(ctx.config.projectDir, '.instar', 'config.json');
15129
+ const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
15130
+ return raw.playwrightRegistry ?? {};
15131
+ }
15132
+ catch {
15133
+ // @silent-fallback-ok — unreadable/absent config means no explicit flags; the
15134
+ // route then resolves the developmentAgent gate default + the dryRun:true default.
15135
+ return {};
15136
+ }
15137
+ };
15138
+ /** True when the whole feature is live for this agent (D4 dev-gate, fresh per request). */
15139
+ const playwrightFeatureEnabled = () => resolveDevAgentGate(readPlaywrightFlags().enabled, ctx.config);
15140
+ /** Construct the registry for this request (test override via ctx.playwrightRegistry). */
15141
+ const buildPlaywrightRegistry = () => {
15142
+ if (ctx.playwrightRegistry)
15143
+ return ctx.playwrightRegistry();
15144
+ const stateDir = ctx.config.stateDir;
15145
+ return new PlaywrightProfileRegistry({
15146
+ stateDir,
15147
+ projectDir: ctx.config.projectDir,
15148
+ // listVaultNames returns NAMES only (D3), via the SAME derivation BootSelfKnowledge
15149
+ // uses. A vault that is absent/decrypt-failed → null so assign fails CLOSED (D17).
15150
+ listVaultNames: () => {
15151
+ const vaultPath = path.join(stateDir, 'secrets', 'config.secrets.enc');
15152
+ if (!fs.existsSync(vaultPath))
15153
+ return []; // no vault yet → no names, but readable (empty)
15154
+ try {
15155
+ const store = new SecretStore({ stateDir, forceFileKey: ctx.config.secrets?.forceFileKey });
15156
+ return secretKeyPaths(store.read());
15157
+ }
15158
+ catch {
15159
+ // @silent-fallback-ok — decrypt-failed/unreadable → null signals "fail closed" to assign (D17).
15160
+ return null;
15161
+ }
15162
+ },
15163
+ });
15164
+ };
15165
+ /** Best-effort audit append to logs/playwright-profiles.jsonl (D20). NAMES only. */
15166
+ const appendPlaywrightAudit = (action, profileId, detail) => {
15167
+ try {
15168
+ const logsDir = path.join(ctx.config.stateDir, '..', 'logs');
15169
+ fs.mkdirSync(logsDir, { recursive: true });
15170
+ const auditPath = path.join(logsDir, 'playwright-profiles.jsonl');
15171
+ const writerSession = typeof ctx.config.sessionId === 'string'
15172
+ ? ctx.config.sessionId
15173
+ : null;
15174
+ fs.appendFileSync(auditPath, `${JSON.stringify({ ts: new Date().toISOString(), action, profileId, writerSession, ...detail })}\n`);
15175
+ }
15176
+ catch {
15177
+ // @silent-fallback-ok — the audit sink is best-effort; a write must NEVER fail on an
15178
+ // audit-log fault (full disk / transient fs error can't break the registry — D20).
15179
+ }
15180
+ };
15181
+ /** Map a thrown registry error to the right HTTP status; returns true if it handled it. */
15182
+ const handlePlaywrightError = (err, res) => {
15183
+ if (err instanceof PlaywrightRegistryCorruptError) {
15184
+ res.status(500).json({ error: 'registry file corrupt — will not overwrite' });
15185
+ return true;
15186
+ }
15187
+ if (err instanceof PlaywrightRegistryError) {
15188
+ res.status(err.status).json({ error: err.message });
15189
+ return true;
15190
+ }
15191
+ return false;
15192
+ };
15193
+ // Per-session activate loop-guard (D19): a per-session cooldown + a per-window
15194
+ // breaker, mirroring the credential-repointing per-pair cooldown. Module-scope so
15195
+ // it survives across requests for the life of the router.
15196
+ const PLAYWRIGHT_ACTIVATE_COOLDOWN_MS = 30_000;
15197
+ const PLAYWRIGHT_ACTIVATE_WINDOW_MS = 5 * 60_000;
15198
+ const PLAYWRIGHT_ACTIVATE_MAX_PER_WINDOW = 5;
15199
+ const playwrightActivateLog = new Map();
15200
+ const playwrightActivateGuard = (sessionKey) => {
15201
+ const now = Date.now();
15202
+ const history = (playwrightActivateLog.get(sessionKey) ?? []).filter((t) => now - t < PLAYWRIGHT_ACTIVATE_WINDOW_MS);
15203
+ const last = history.length > 0 ? history[history.length - 1] : 0;
15204
+ if (last && now - last < PLAYWRIGHT_ACTIVATE_COOLDOWN_MS) {
15205
+ return { ok: false, reason: 'activate cooldown — too soon since the last switch', retryAfterMs: PLAYWRIGHT_ACTIVATE_COOLDOWN_MS - (now - last) };
15206
+ }
15207
+ if (history.length >= PLAYWRIGHT_ACTIVATE_MAX_PER_WINDOW) {
15208
+ return { ok: false, reason: 'activate breaker — too many switches in the window', retryAfterMs: PLAYWRIGHT_ACTIVATE_WINDOW_MS - (now - history[0]) };
15209
+ }
15210
+ history.push(now);
15211
+ playwrightActivateLog.set(sessionKey, history);
15212
+ return { ok: true };
15213
+ };
15214
+ // GET /playwright-profiles — the FULL detail surface (vault NAMES, dangling-ref flags).
15215
+ router.get('/playwright-profiles', (req, res) => {
15216
+ if (!playwrightFeatureEnabled()) {
15217
+ res.status(503).json({ error: 'playwright profile registry disabled' });
15218
+ return;
15219
+ }
15220
+ try {
15221
+ const profiles = buildPlaywrightRegistry().listProfiles();
15222
+ res.json({ profiles });
15223
+ }
15224
+ catch (err) {
15225
+ if (handlePlaywrightError(err, res))
15226
+ return;
15227
+ res.status(500).json({ error: err instanceof Error ? err.message : 'Failed to list playwright profiles' });
15228
+ }
15229
+ });
15230
+ // GET /playwright-profiles/session-context — the compact boot pointer (?full=1 bypasses the cap).
15231
+ router.get('/playwright-profiles/session-context', (req, res) => {
15232
+ if (!playwrightFeatureEnabled()) {
15233
+ res.status(503).json({ error: 'playwright profile registry disabled' });
15234
+ return;
15235
+ }
15236
+ try {
15237
+ const full = String(req.query.full ?? '') === '1';
15238
+ const result = buildPlaywrightRegistry().buildSessionContextBlock(PLAYWRIGHT_BLOCK_MAX_BYTES, { full });
15239
+ res.json({ ...result, full });
15240
+ }
15241
+ catch (err) {
15242
+ // buildSessionContextBlock is fail-open internally; this catch is belt-and-suspenders.
15243
+ res.status(500).json({ error: err instanceof Error ? err.message : 'Failed to build playwright boot block' });
15244
+ }
15245
+ });
15246
+ // GET /playwright-profiles/resolve?service=&identity= — the selector (D18 ambiguity).
15247
+ router.get('/playwright-profiles/resolve', (req, res) => {
15248
+ if (!playwrightFeatureEnabled()) {
15249
+ res.status(503).json({ error: 'playwright profile registry disabled' });
15250
+ return;
15251
+ }
15252
+ const service = typeof req.query.service === 'string' ? req.query.service : '';
15253
+ if (!service) {
15254
+ res.status(400).json({ error: 'service query param is required' });
15255
+ return;
15256
+ }
15257
+ const identity = typeof req.query.identity === 'string' && req.query.identity ? req.query.identity : undefined;
15258
+ try {
15259
+ res.json(buildPlaywrightRegistry().resolve(service, identity));
15260
+ }
15261
+ catch (err) {
15262
+ if (handlePlaywrightError(err, res))
15263
+ return;
15264
+ res.status(500).json({ error: err instanceof Error ? err.message : 'Failed to resolve playwright profile' });
15265
+ }
15266
+ });
15267
+ // POST /playwright-profiles — create a custom profile (id charset/length-clamped, jailed userDataDir).
15268
+ router.post('/playwright-profiles', (req, res) => {
15269
+ if (!playwrightFeatureEnabled()) {
15270
+ res.status(503).json({ error: 'playwright profile registry disabled' });
15271
+ return;
15272
+ }
15273
+ const { id, description, userDataDir } = req.body || {};
15274
+ try {
15275
+ const profile = buildPlaywrightRegistry().createProfile({ id, description, userDataDir });
15276
+ appendPlaywrightAudit('create', profile.id, { userDataDir: profile.userDataDir, dryRun: false });
15277
+ res.json({ profile });
15278
+ }
15279
+ catch (err) {
15280
+ if (handlePlaywrightError(err, res))
15281
+ return;
15282
+ res.status(500).json({ error: err instanceof Error ? err.message : 'Failed to create playwright profile' });
15283
+ }
15284
+ });
15285
+ // POST /playwright-profiles/:id/accounts — assign an account (owner REQUIRED; ref-validation D17).
15286
+ router.post('/playwright-profiles/:id/accounts', (req, res) => {
15287
+ if (!playwrightFeatureEnabled()) {
15288
+ res.status(503).json({ error: 'playwright profile registry disabled' });
15289
+ return;
15290
+ }
15291
+ const profileId = req.params.id;
15292
+ const { service, identity, owner, vaultRefs, loginMethod, note } = req.body || {};
15293
+ try {
15294
+ const account = buildPlaywrightRegistry().assignAccount(profileId, {
15295
+ service, identity, owner, vaultRefs, loginMethod, note,
15296
+ });
15297
+ appendPlaywrightAudit('assign', profileId, {
15298
+ service: account.service, identity: account.identity, owner: account.owner, vaultRefs: account.vaultRefs, dryRun: false,
15299
+ });
15300
+ res.json({ account });
15301
+ }
15302
+ catch (err) {
15303
+ if (handlePlaywrightError(err, res))
15304
+ return;
15305
+ res.status(500).json({ error: err instanceof Error ? err.message : 'Failed to assign account' });
15306
+ }
15307
+ });
15308
+ // PATCH /playwright-profiles/:id/accounts — update lastAsserted/lastVerifiedAt/note.
15309
+ router.patch('/playwright-profiles/:id/accounts', (req, res) => {
15310
+ if (!playwrightFeatureEnabled()) {
15311
+ res.status(503).json({ error: 'playwright profile registry disabled' });
15312
+ return;
15313
+ }
15314
+ const profileId = req.params.id;
15315
+ const { service, identity, lastAsserted, lastVerifiedAt, note } = req.body || {};
15316
+ if (typeof service !== 'string' || !service || typeof identity !== 'string' || !identity) {
15317
+ res.status(400).json({ error: 'service and identity are required' });
15318
+ return;
15319
+ }
15320
+ try {
15321
+ const account = buildPlaywrightRegistry().patchAccount(profileId, service, identity, { lastAsserted, lastVerifiedAt, note });
15322
+ appendPlaywrightAudit('patch', profileId, {
15323
+ service, identity, lastAsserted: account.lastAsserted, lastVerifiedAt: account.lastVerifiedAt, dryRun: false,
15324
+ });
15325
+ res.json({ account });
15326
+ }
15327
+ catch (err) {
15328
+ if (handlePlaywrightError(err, res))
15329
+ return;
15330
+ res.status(500).json({ error: err instanceof Error ? err.message : 'Failed to patch account' });
15331
+ }
15332
+ });
15333
+ // DELETE /playwright-profiles/:id — delete a custom profile (refuses the default, 409).
15334
+ router.delete('/playwright-profiles/:id', (req, res) => {
15335
+ if (!playwrightFeatureEnabled()) {
15336
+ res.status(503).json({ error: 'playwright profile registry disabled' });
15337
+ return;
15338
+ }
15339
+ const profileId = req.params.id;
15340
+ try {
15341
+ buildPlaywrightRegistry().deleteProfile(profileId);
15342
+ appendPlaywrightAudit('delete-profile', profileId, { dryRun: false });
15343
+ res.json({ success: true });
15344
+ }
15345
+ catch (err) {
15346
+ if (handlePlaywrightError(err, res))
15347
+ return;
15348
+ res.status(500).json({ error: err instanceof Error ? err.message : 'Failed to delete playwright profile' });
15349
+ }
15350
+ });
15351
+ // DELETE /playwright-profiles/:id/accounts — delete one account by (service, identity).
15352
+ router.delete('/playwright-profiles/:id/accounts', (req, res) => {
15353
+ if (!playwrightFeatureEnabled()) {
15354
+ res.status(503).json({ error: 'playwright profile registry disabled' });
15355
+ return;
15356
+ }
15357
+ const profileId = req.params.id;
15358
+ const { service, identity } = req.body || {};
15359
+ if (typeof service !== 'string' || !service || typeof identity !== 'string' || !identity) {
15360
+ res.status(400).json({ error: 'service and identity are required' });
15361
+ return;
15362
+ }
15363
+ try {
15364
+ buildPlaywrightRegistry().deleteAccount(profileId, service, identity);
15365
+ appendPlaywrightAudit('delete-account', profileId, { service, identity, dryRun: false });
15366
+ res.json({ success: true });
15367
+ }
15368
+ catch (err) {
15369
+ if (handlePlaywrightError(err, res))
15370
+ return;
15371
+ res.status(500).json({ error: err instanceof Error ? err.message : 'Failed to delete account' });
15372
+ }
15373
+ });
15374
+ // POST /playwright-profiles/:id/activate — the COMPLETE switch, with the dry-run canary (D5),
15375
+ // already-active fast path (D19), and loop-guard (D19). Activation is an identity switch, NOT
15376
+ // authorization — it does NOT bypass any external-operation / coherence gate (Adversarial#4).
15377
+ router.post('/playwright-profiles/:id/activate', (req, res) => {
15378
+ if (!playwrightFeatureEnabled()) {
15379
+ res.status(503).json({ error: 'playwright profile registry disabled' });
15380
+ return;
15381
+ }
15382
+ const profileId = req.params.id;
15383
+ const sessionName = typeof req.body?.sessionName === 'string' ? req.body.sessionName : '';
15384
+ const dryRun = readPlaywrightFlags().dryRun !== false; // default TRUE (D5)
15385
+ try {
15386
+ const reg = buildPlaywrightRegistry();
15387
+ const plan = reg.computeActivation(profileId);
15388
+ // Already-active: skip BOTH the write and the refresh (D19) — kills the repeat-call loop.
15389
+ if (plan.alreadyActive) {
15390
+ res.json({ alreadyActive: true, userDataDir: plan.userDataDir, dirExists: plan.dirExists });
15391
+ return;
15392
+ }
15393
+ // Dry-run (default-on-dev): LOG the intended rewrite + refresh, perform NEITHER (D5).
15394
+ if (dryRun) {
15395
+ appendPlaywrightAudit('activate', profileId, {
15396
+ dryRun: true, wouldWriteFile: plan.file, userDataDir: plan.userDataDir, dirExists: plan.dirExists,
15397
+ });
15398
+ res.json({
15399
+ dryRun: true,
15400
+ wouldWriteFile: plan.file,
15401
+ wouldRefresh: true,
15402
+ userDataDir: plan.userDataDir,
15403
+ dirExists: plan.dirExists,
15404
+ alreadyActive: false,
15405
+ });
15406
+ return;
15407
+ }
15408
+ // Real switch (dryRun:false). Loop-guard the per-session activate (D19).
15409
+ const guardKey = sessionName || 'unkeyed';
15410
+ const guard = playwrightActivateGuard(guardKey);
15411
+ if (!guard.ok) {
15412
+ res.status(429).json({ error: guard.reason, retryAfterMs: guard.retryAfterMs });
15413
+ return;
15414
+ }
15415
+ const written = reg.writeActivation(plan);
15416
+ appendPlaywrightAudit('activate', profileId, {
15417
+ dryRun: false, file: written.file, userDataDir: plan.userDataDir, dirExists: plan.dirExists,
15418
+ });
15419
+ // Trigger the session refresh server-side (activation takes effect at the next MCP boot).
15420
+ // Reversible by activating `default`. Refresh is best-effort (202-scheduled) and only when
15421
+ // a session name + the refresh orchestrator are both present.
15422
+ let refresh = { scheduled: false, reason: 'no sessionName or no session-refresh wired' };
15423
+ if (sessionName && ctx.sessionRefresh && SESSION_NAME_RE.test(sessionName)) {
15424
+ const sessionRefresh = ctx.sessionRefresh;
15425
+ refresh = { scheduled: true };
15426
+ setTimeout(() => {
15427
+ sessionRefresh
15428
+ .refreshSession({ sessionName, reason: `playwright profile activate: ${profileId}` })
15429
+ .then((r) => {
15430
+ if (!r.ok)
15431
+ console.warn(`[playwright-profiles/activate] refresh refused sessionName=${sessionName} code=${r.code}`);
15432
+ })
15433
+ .catch((e) => console.error(`[playwright-profiles/activate] refresh error sessionName=${sessionName}:`, e));
15434
+ }, 500);
15435
+ }
15436
+ res.json({ activated: true, userDataDir: plan.userDataDir, dirExists: plan.dirExists, refresh });
15437
+ }
15438
+ catch (err) {
15439
+ if (handlePlaywrightError(err, res))
15440
+ return;
15441
+ res.status(500).json({ error: err instanceof Error ? err.message : 'Failed to activate playwright profile' });
15442
+ }
15443
+ });
15103
15444
  // ── Corrections (Correction & Preference Learning Sentinel, Slice 1b) ──
15104
15445
  //
15105
15446
  // Read surface over the CorrectionLedger — distilled, scrubbed correction /