wendkeep 0.87.0 → 0.89.0

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 (65) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.en.md +3 -2
  3. package/README.md +3 -2
  4. package/bin/wendkeep.mjs +1 -0
  5. package/docs/en/commands/ecosystem-bridges.md +172 -0
  6. package/docs/en/commands/observer-security.md +154 -0
  7. package/docs/en/commands/observer.md +30 -12
  8. package/docs/en/commands/verify.md +6 -0
  9. package/docs/pt-BR/commands/ecosystem-bridges.md +169 -0
  10. package/docs/pt-BR/commands/observer-security.md +154 -0
  11. package/docs/pt-BR/commands/observer.md +30 -12
  12. package/docs/pt-BR/commands/verify.md +6 -0
  13. package/hooks/observer-publish.mjs +3 -1
  14. package/package.json +2 -1
  15. package/packages/cli/src/index.mjs +10 -1
  16. package/packages/harness/src/sensors-core.mjs +49 -3
  17. package/packages/integrations/src/bridge-config.mjs +139 -0
  18. package/packages/integrations/src/bridge-contract.mjs +316 -0
  19. package/packages/integrations/src/bridge-diagnostics.mjs +45 -0
  20. package/packages/integrations/src/canonical-bridge-authority.mjs +32 -0
  21. package/packages/integrations/src/capabilities.mjs +34 -0
  22. package/packages/integrations/src/ecosystem-bridge.mjs +82 -0
  23. package/packages/integrations/src/index.mjs +6 -0
  24. package/packages/integrations/src/spec-kit-adapter.mjs +259 -0
  25. package/packages/integrations/src/superpowers-adapter.mjs +269 -0
  26. package/packages/mcp/src/executor.mjs +35 -2
  27. package/packages/observer/package.json +16 -0
  28. package/packages/observer/src/audit.mjs +1 -0
  29. package/packages/observer/src/authz.mjs +38 -0
  30. package/packages/observer/src/encryption.mjs +75 -0
  31. package/packages/observer/src/index.mjs +7 -0
  32. package/packages/observer/src/policy.mjs +305 -0
  33. package/packages/observer/src/purge.mjs +100 -0
  34. package/packages/observer/src/redaction.mjs +54 -0
  35. package/packages/observer/src/retention.mjs +39 -0
  36. package/packages/observer/src/token-registry.mjs +122 -0
  37. package/schema/ecosystem-bridge-artifact-manifest-v1.schema.json +30 -0
  38. package/schema/ecosystem-bridge-v1.schema.json +65 -0
  39. package/schema/observer/006-observer-security.sql +64 -0
  40. package/schema/observer-policy-v1.schema.json +63 -0
  41. package/schema/sync-event-v1.schema.json +10 -0
  42. package/schema/wendkeep.evidence-envelope-v2.schema.json +39 -0
  43. package/schema/wendkeep.sensors.schema.json +14 -0
  44. package/src/doctor.mjs +6 -1
  45. package/src/ecosystem-bridge-artifact-collector.mjs +111 -0
  46. package/src/ecosystem-bridge-baseline.mjs +58 -0
  47. package/src/ecosystem-bridge-proof.mjs +97 -0
  48. package/src/ecosystem-bridges.mjs +227 -0
  49. package/src/evidence-envelope.mjs +2 -0
  50. package/src/observer-auth.mjs +8 -0
  51. package/src/observer-privacy.mjs +7 -3
  52. package/src/observer-publish.mjs +31 -0
  53. package/src/observer-server.mjs +179 -20
  54. package/src/observer-sql-migrate.mjs +5 -2
  55. package/src/observer-sql-publish.mjs +114 -39
  56. package/src/observer-sql-store.mjs +299 -45
  57. package/src/observer-transcript-store.mjs +23 -8
  58. package/src/observer.mjs +145 -12
  59. package/src/sync-protocol.mjs +20 -0
  60. package/src/task-contracts.mjs +19 -0
  61. package/src/task.mjs +82 -0
  62. package/src/verify.mjs +9 -0
  63. package/web/observer/app.mjs +107 -31
  64. package/web/observer/index.html +7 -0
  65. package/web/observer/styles.css +5 -0
@@ -1,5 +1,4 @@
1
1
  import { createServer } from 'node:http';
2
- import { createHash, timingSafeEqual } from 'node:crypto';
3
2
  import { readFileSync } from 'node:fs';
4
3
  import { fileURLToPath } from 'node:url';
5
4
  import { gunzipSync } from 'node:zlib';
@@ -12,9 +11,8 @@ import { validateMemoryEvent } from './observer-memory.mjs';
12
11
  import {
13
12
  OBSERVER_SQL_FILE,
14
13
  OBSERVER_SQL_SCHEMA_VERSION,
15
- ensureObserverDatabase,
14
+ bootstrapObserverDatabase,
16
15
  ingestObserverEvents,
17
- migrateObserverDatabase,
18
16
  readSqlProject,
19
17
  readSqlProjectOverview,
20
18
  readSqlProjectSnapshot,
@@ -32,6 +30,11 @@ import {
32
30
  upsertSqlProjectSnapshot,
33
31
  } from './observer-sql-store.mjs';
34
32
  import { migrateObserverContainerData } from './observer-sql-migrate.mjs';
33
+ import { authorizeObserverPrincipal, recordObserverAudit } from '../packages/observer/src/authz.mjs';
34
+ import { ensureObserverBootstrapToken, resolveObserverPrincipal } from '../packages/observer/src/token-registry.mjs';
35
+ import { readObserverPolicy, saveObserverPolicy } from '../packages/observer/src/policy.mjs';
36
+ import { purgeObserverData } from '../packages/observer/src/purge.mjs';
37
+ import { runObserverRetention } from '../packages/observer/src/retention.mjs';
35
38
 
36
39
  const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
37
40
  const MAX_BODY_BYTES = MAX_SNAPSHOT_BYTES + 4096;
@@ -50,13 +53,6 @@ function loopbackOnly(host) {
50
53
  return LOOPBACK_HOSTS.has(String(host || '').toLowerCase());
51
54
  }
52
55
 
53
- function safeTokenEqual(actual, expected) {
54
- if (!actual || !expected) return false;
55
- const left = createHash('sha256').update(String(actual)).digest();
56
- const right = createHash('sha256').update(String(expected)).digest();
57
- return timingSafeEqual(left, right);
58
- }
59
-
60
56
  function bearerToken(req) {
61
57
  const match = String(req.headers.authorization || '').match(/^Bearer\s+(.+)$/i);
62
58
  return match?.[1] || '';
@@ -173,6 +169,32 @@ function projectIdFrom(parts) {
173
169
  return parts[0] === 'v1' && parts[1] === 'projects' && parts[2] ? parts[2] : '';
174
170
  }
175
171
 
172
+ function observerEndpointCapability(method, parts) {
173
+ const action = String(method || 'GET').toUpperCase();
174
+ const resource = parts[3] || '';
175
+ if (parts.length === 2 && action === 'GET') return 'project:read';
176
+ if (parts.length === 3) return action === 'GET' ? 'project:read' : 'project:write';
177
+ if (resource === 'ingest') return 'ingest:write';
178
+ if (resource === 'usage') {
179
+ if (parts[4] === 'calls') return 'usage:calls:read';
180
+ if (parts[4] === 'breakdown') return 'usage:breakdown:read';
181
+ return 'usage:summary:read';
182
+ }
183
+ if (resource === 'transcripts') return 'transcript:read';
184
+ if (resource === 'sync') return action === 'GET' ? 'sync:read' : 'sync:write';
185
+ if (resource === 'memory') {
186
+ if (action !== 'GET') return 'memory:write';
187
+ return parts[4] === 'tree' ? 'memory:metadata:read' : 'memory:content:read';
188
+ }
189
+ if (resource === 'snapshot' || resource === 'snapshots') return 'snapshot:write';
190
+ if (resource === 'security') return 'security:admin';
191
+ return 'project:read';
192
+ }
193
+
194
+ function sensitiveCapability(capability) {
195
+ return ['usage:calls:read', 'transcript:read', 'memory:content:read', 'audit:read', 'security:admin'].includes(capability);
196
+ }
197
+
176
198
  function ensureSqlProjectRegistration(dataDir, sqlDb, projectId) {
177
199
  try {
178
200
  if (readSqlProject(sqlDb, projectId)) return true;
@@ -215,7 +237,9 @@ export async function startObserverServer({
215
237
  port = 8787,
216
238
  dataDir,
217
239
  allowNonLoopback = false,
218
- token = process.env.WENDKEEP_OBSERVER_TOKEN || '',
240
+ token = '',
241
+ bootstrap = {},
242
+ security = {},
219
243
  } = {}) {
220
244
  if (!loopbackOnly(host) && !allowNonLoopback) {
221
245
  throw new Error(`Observer HTTP aceita somente host loopback; recebido: ${host}`);
@@ -226,9 +250,30 @@ export async function startObserverServer({
226
250
  throw error;
227
251
  }
228
252
  if (!dataDir) throw new Error('dataDir é obrigatório.');
229
- const sqlDb = ensureObserverDatabase(dataDir);
230
- const databaseMigration = migrateObserverDatabase(sqlDb);
231
- const legacyMigration = migrateObserverContainerData(dataDir, { database: sqlDb });
253
+ const { db: sqlDb, databaseMigration, protectedDataMigration } = bootstrapObserverDatabase(dataDir, { security: {
254
+ policy: security.policy || null,
255
+ encryption: security.encryption || null,
256
+ enforcePolicy: Boolean(security.enabled),
257
+ } });
258
+ const bootstrapToken = token
259
+ ? ensureObserverBootstrapToken(sqlDb, {
260
+ token,
261
+ tokenId: bootstrap.tokenId,
262
+ role: bootstrap.role,
263
+ projectIds: bootstrap.projectIds,
264
+ scopes: bootstrap.scopes,
265
+ expiresAt: bootstrap.expiresAt,
266
+ now: security.clock?.().toISOString?.() || new Date().toISOString(),
267
+ })
268
+ : null;
269
+ const legacyMigration = migrateObserverContainerData(dataDir, {
270
+ database: sqlDb,
271
+ security: {
272
+ policy: security.policy || null,
273
+ encryption: security.encryption || null,
274
+ enforcePolicy: Boolean(security.enabled),
275
+ },
276
+ });
232
277
  const registered = [
233
278
  ...listRegisteredObserverProjects(dataDir),
234
279
  ...readObserverIndexSource(dataDir).projects.map((item) => ({
@@ -249,12 +294,7 @@ export async function startObserverServer({
249
294
  errorResponse(res, authority.status, authority.code, authority.message);
250
295
  return;
251
296
  }
252
- const authenticated = safeTokenEqual(bearerToken(req), token);
253
297
  const mutating = !['GET', 'HEAD', 'OPTIONS'].includes(String(req.method || '').toUpperCase());
254
- if ((mutating || !loopbackOnly(host)) && !authenticated) {
255
- errorResponse(res, 401, 'observer_auth_required', 'Bearer token válido é obrigatório para esta operação.');
256
- return;
257
- }
258
298
  if (req.method === 'GET' && pathname === '/healthz') {
259
299
  json(res, 200, {
260
300
  ok: true,
@@ -265,9 +305,17 @@ export async function startObserverServer({
265
305
  file: OBSERVER_SQL_FILE,
266
306
  schema_version: OBSERVER_SQL_SCHEMA_VERSION,
267
307
  migrations: databaseMigration.applied.length,
308
+ protected_data_migration: protectedDataMigration,
268
309
  legacy_migration: legacyMigration,
269
310
  ready: true,
270
311
  },
312
+ bootstrap: bootstrapToken ? {
313
+ token_id: bootstrapToken.token_id,
314
+ role: bootstrapToken.role,
315
+ project_ids: bootstrapToken.project_ids,
316
+ expires_at: bootstrapToken.expires_at,
317
+ active: !bootstrapToken.revoked && !bootstrapToken.expired,
318
+ } : null,
271
319
  });
272
320
  return;
273
321
  }
@@ -285,10 +333,57 @@ export async function startObserverServer({
285
333
  return;
286
334
  }
287
335
 
336
+ let requestPrincipal = null;
337
+ {
338
+ const projectId = projectIdFrom(parts);
339
+ const capability = observerEndpointCapability(req.method, parts);
340
+ const suppliedToken = bearerToken(req);
341
+ const mustAuthenticate = mutating || !loopbackOnly(host) || Boolean(security.requireLoopbackAuth)
342
+ || sensitiveCapability(capability) || Boolean(suppliedToken);
343
+ const authorizationActive = Boolean(security.enabled) || mustAuthenticate;
344
+ const principal = resolveObserverPrincipal(sqlDb, suppliedToken, {
345
+ now: security.clock?.().toISOString?.() || new Date().toISOString(),
346
+ });
347
+ if (!principal.ok && authorizationActive) {
348
+ if (projectId && sensitiveCapability(capability)) recordObserverAudit(sqlDb, {
349
+ projectId,
350
+ tokenId: principal.token_id || '',
351
+ capability,
352
+ outcome: 'denied',
353
+ occurredAt: security.clock?.().toISOString?.() || new Date().toISOString(),
354
+ metadata: { route: pathname, method: req.method || 'GET', reason: principal.code || 'observer_auth_required' },
355
+ });
356
+ const authenticationCode = principal.code === 'observer_token_missing'
357
+ ? 'observer_auth_required'
358
+ : principal.code || 'observer_auth_required';
359
+ errorResponse(res, 401, authenticationCode, 'Bearer token válido é obrigatório para esta operação.');
360
+ return;
361
+ }
362
+ if (principal.ok && authorizationActive) {
363
+ const authorized = authorizeObserverPrincipal(principal, { projectId: projectId || principal.project_ids?.[0] || '*', capability });
364
+ if (!authorized.ok) {
365
+ if (projectId) recordObserverAudit(sqlDb, {
366
+ projectId, tokenId: principal.token_id, capability, outcome: 'denied',
367
+ occurredAt: security.clock?.().toISOString?.() || new Date().toISOString(),
368
+ metadata: { route: pathname, method: req.method || 'GET', reason: authorized.code },
369
+ });
370
+ errorResponse(res, authorized.status, authorized.code, 'Token sem autorização para este projeto/capability.');
371
+ return;
372
+ }
373
+ requestPrincipal = principal;
374
+ if (projectId && sensitiveCapability(capability)) recordObserverAudit(sqlDb, {
375
+ projectId, tokenId: principal.token_id, capability, outcome: 'allowed',
376
+ occurredAt: security.clock?.().toISOString?.() || new Date().toISOString(),
377
+ metadata: { route: pathname, method: req.method || 'GET' },
378
+ });
379
+ }
380
+ }
381
+
288
382
  if (parts.length === 2 && req.method === 'GET') {
289
383
  json(res, 200, {
290
384
  schema_version: 1,
291
385
  projects: listSqlProjects(sqlDb)
386
+ .filter((project) => !requestPrincipal || requestPrincipal.project_ids?.includes('*') || requestPrincipal.project_ids?.includes(project.project_id))
292
387
  .map((project) => readSqlProjectOverview(sqlDb, project.project_id))
293
388
  .sort((a, b) => a.projectId.localeCompare(b.projectId)),
294
389
  });
@@ -361,6 +456,66 @@ export async function startObserverServer({
361
456
  return;
362
457
  }
363
458
 
459
+ if (parts[3] === 'security') {
460
+ if (!ensureSqlProjectRegistration(dataDir, sqlDb, projectId)) {
461
+ errorResponse(res, 404, 'project_not_found', 'projeto não encontrado: ' + projectId);
462
+ return;
463
+ }
464
+ if (parts.length === 4 && req.method === 'GET') {
465
+ const currentTime = security.clock?.().toISOString?.() || new Date().toISOString();
466
+ const tokens = sqlDb.prepare(`SELECT
467
+ COUNT(*) AS total,
468
+ SUM(CASE WHEN revoked_at IS NULL AND expires_at > ? THEN 1 ELSE 0 END) AS active,
469
+ SUM(CASE WHEN revoked_at IS NOT NULL THEN 1 ELSE 0 END) AS revoked,
470
+ SUM(CASE WHEN revoked_at IS NULL AND expires_at <= ? THEN 1 ELSE 0 END) AS expired
471
+ FROM observer_tokens WHERE project_ids_json LIKE ? OR project_ids_json LIKE '%"*"%'`)
472
+ .get(currentTime, currentTime, `%"${projectId}"%`);
473
+ const recentAudit = sqlDb.prepare(`SELECT audit_id, token_id, capability, outcome, occurred_at, metadata_json
474
+ FROM observer_access_audit WHERE project_id = ? ORDER BY occurred_at DESC LIMIT 50`).all(projectId)
475
+ .map((row) => ({ ...row, metadata: parseJson(row.metadata_json), metadata_json: undefined }));
476
+ json(res, 200, {
477
+ schema_version: 1,
478
+ project_id: projectId,
479
+ policy: readObserverPolicy(sqlDb, projectId),
480
+ tokens: { total: Number(tokens.total) || 0, active: Number(tokens.active) || 0, revoked: Number(tokens.revoked) || 0, expired: Number(tokens.expired) || 0 },
481
+ audit: recentAudit,
482
+ encryption: { configured: Boolean(security.encryption), required: Boolean(security.encryption?.required), key_id: security.encryption?.keyId || '' },
483
+ });
484
+ return;
485
+ }
486
+ if (parts.length === 5 && parts[4] === 'policy' && req.method === 'PUT') {
487
+ const body = parseJson(await readBody(req));
488
+ json(res, 200, { schema_version: 1, project_id: projectId, policy: saveObserverPolicy(sqlDb, projectId, body) });
489
+ return;
490
+ }
491
+ if (parts.length === 5 && parts[4] === 'purge' && req.method === 'POST') {
492
+ const body = parseJson(await readBody(req));
493
+ const result = purgeObserverData(sqlDb, {
494
+ projectId,
495
+ before: body.before,
496
+ classes: body.classes,
497
+ dryRun: body.dry_run === true,
498
+ operationId: body.operation_id || '',
499
+ now: security.clock?.().toISOString?.() || new Date().toISOString(),
500
+ });
501
+ json(res, body.dry_run === true ? 200 : 201, result);
502
+ return;
503
+ }
504
+ if (parts.length === 5 && parts[4] === 'retention' && req.method === 'POST') {
505
+ const body = parseJson(await readBody(req));
506
+ const storedPolicy = readObserverPolicy(sqlDb, projectId);
507
+ const result = runObserverRetention(sqlDb, {
508
+ projectId,
509
+ policy: storedPolicy.retention,
510
+ clock: () => security.clock?.() || new Date(),
511
+ dryRun: body.dry_run === true,
512
+ operationId: body.operation_id || '',
513
+ });
514
+ json(res, body.dry_run === true ? 200 : 201, result);
515
+ return;
516
+ }
517
+ }
518
+
364
519
  if (parts.length === 4 && parts[3] === 'sync' && req.method === 'GET') {
365
520
  if (!ensureSqlProjectRegistration(dataDir, sqlDb, projectId)) {
366
521
  errorResponse(res, 404, 'project_not_found', 'projeto não encontrado: ' + projectId);
@@ -511,7 +666,11 @@ export async function startObserverServer({
511
666
  errorResponse(res, 404, 'not_found', 'rota não encontrada.');
512
667
  } catch (error) {
513
668
  if (res.headersSent) return;
514
- const status = error?.code === 'payload_too_large' ? 413 : ['invalid_json', 'invalid_content_encoding'].includes(error?.code) ? 400 : 500;
669
+ const status = error?.code === 'payload_too_large'
670
+ ? 413
671
+ : ['invalid_json', 'invalid_content_encoding', 'observer_policy_invalid', 'observer_purge_invalid', 'observer_retention_invalid'].includes(error?.code)
672
+ ? 400
673
+ : 500;
515
674
  errorResponse(res, status, error?.code || 'observer_error', error?.message || 'erro interno do Observer.');
516
675
  }
517
676
  });
@@ -3,6 +3,7 @@ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from '
3
3
  import { join } from 'node:path';
4
4
  import { parseSessionCost } from './cost.mjs';
5
5
  import {
6
+ configureObserverDatabaseSecurity,
6
7
  ensureObserverDatabase,
7
8
  ingestObserverEvents,
8
9
  registerSqlProject,
@@ -237,11 +238,12 @@ export function sessionEvents({ projectId, logicalPath, content, cost, revision
237
238
  return { events, sessionId, rollups: events.filter((event) => event.kind === 'usage.rollup').length, summaryOnly: transcriptId ? 1 : 0 };
238
239
  }
239
240
 
240
- export function migrateObserverData({ dataDir, vaultBase, projectId, projectName = projectId, transcriptSources = {}, database = null } = {}) {
241
+ export function migrateObserverData({ dataDir, vaultBase, projectId, projectName = projectId, transcriptSources = {}, database = null, security = null } = {}) {
241
242
  if (!dataDir || !vaultBase || !projectId) throw new Error('dataDir, vaultBase e projectId são obrigatórios.');
242
243
  const db = database || ensureObserverDatabase(dataDir);
243
244
  const ownsDatabase = !database;
244
245
  try {
246
+ if (security) configureObserverDatabaseSecurity(db, security);
245
247
  registerSqlProject(db, { projectId, projectName });
246
248
  const stats = { project_id: projectId, documents: 0, sessions: 0, rollups: 0, summary_only_transcripts: 0, accepted: 0, duplicates: 0, conflicts: 0, rejected: 0 };
247
249
  const sourceEvents = readMemoryEvents(dataDir, projectId);
@@ -298,7 +300,7 @@ export function migrateObserverData({ dataDir, vaultBase, projectId, projectName
298
300
  } finally { if (ownsDatabase) db.close(); }
299
301
  }
300
302
 
301
- export function migrateObserverContainerData(dataDir, { database = null } = {}) {
303
+ export function migrateObserverContainerData(dataDir, { database = null, security = null } = {}) {
302
304
  const memoryRoot = join(dataDir, 'memory');
303
305
  if (!existsSync(memoryRoot)) return { skipped: true, projects: 0, documents: 0, events: 0 };
304
306
  const markerPath = join(dataDir, 'observer-sql-legacy-migration.json');
@@ -321,6 +323,7 @@ export function migrateObserverContainerData(dataDir, { database = null } = {})
321
323
  projectId,
322
324
  projectName: project.project_name || projectId,
323
325
  database,
326
+ security,
324
327
  });
325
328
  stats.projects += 1;
326
329
  stats.documents += result.documents;