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
@@ -0,0 +1,58 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ import { getLocale } from '../hooks/locale.mjs';
5
+ import { writeVaultFileAtomic } from '../packages/vault/src/vault-path-safety.mjs';
6
+ import { validateBridgeProjection } from '../packages/integrations/src/bridge-contract.mjs';
7
+
8
+ const BASELINE_FILE = 'spec-kit-baseline.v1.json';
9
+
10
+ function safeChangeSlug(value) {
11
+ const slug = String(value || '').trim();
12
+ if (!/^[a-z0-9][a-z0-9._-]*$/i.test(slug) || slug.includes('..')) {
13
+ throw Object.assign(new Error('a safe change slug is required for the Spec Kit baseline'), {
14
+ code: 'BRIDGE_BASELINE_CONTEXT_INVALID',
15
+ });
16
+ }
17
+ return slug;
18
+ }
19
+
20
+ export function specKitBaselinePath(vaultBase, changeSlug) {
21
+ return join(vaultBase, getLocale(vaultBase).folders.changes, safeChangeSlug(changeSlug), BASELINE_FILE);
22
+ }
23
+
24
+ export function readSpecKitBaseline(vaultBase, changeSlug) {
25
+ const path = specKitBaselinePath(vaultBase, changeSlug);
26
+ if (!existsSync(path)) return null;
27
+ let projection;
28
+ try { projection = JSON.parse(readFileSync(path, 'utf8')); } catch {
29
+ throw Object.assign(new Error('canonical Spec Kit baseline is invalid JSON'), { code: 'BRIDGE_BASELINE_INVALID' });
30
+ }
31
+ const validation = validateBridgeProjection(projection);
32
+ if (!validation.valid || projection.ok !== true || projection.adapter !== 'spec-kit') {
33
+ throw Object.assign(new Error('canonical Spec Kit baseline is invalid or blocked'), {
34
+ code: 'BRIDGE_BASELINE_INVALID', diagnostics: validation.diagnostics,
35
+ });
36
+ }
37
+ return projection;
38
+ }
39
+
40
+ export function writeSpecKitBaseline(vaultBase, changeSlug, projection) {
41
+ const validation = validateBridgeProjection(projection);
42
+ if (!validation.valid || projection.ok !== true || projection.adapter !== 'spec-kit') {
43
+ throw Object.assign(new Error('only a valid green Spec Kit projection can become baseline'), {
44
+ code: 'BRIDGE_BASELINE_INVALID', diagnostics: validation.diagnostics,
45
+ });
46
+ }
47
+ const path = specKitBaselinePath(vaultBase, changeSlug);
48
+ const changeDir = join(vaultBase, getLocale(vaultBase).folders.changes, safeChangeSlug(changeSlug));
49
+ if (!existsSync(changeDir)) {
50
+ throw Object.assign(new Error(`change not found for Spec Kit baseline: ${changeSlug}`), {
51
+ code: 'BRIDGE_BASELINE_CONTEXT_INVALID',
52
+ });
53
+ }
54
+ writeVaultFileAtomic(vaultBase, path, `${JSON.stringify(projection, null, 2)}\n`, 'utf8', {
55
+ scopeRoot: changeDir, label: 'baseline canônico Spec Kit', code: 'BRIDGE_BASELINE_PATH_UNSAFE',
56
+ });
57
+ return projection;
58
+ }
@@ -0,0 +1,97 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, lstatSync, readFileSync, realpathSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+
5
+ import { getLocale } from '../hooks/locale.mjs';
6
+ import { parseTasks } from '../hooks/change-core.mjs';
7
+ import { buildEffectiveRequirementPackage, tasksHashOf } from '../hooks/spec-core.mjs';
8
+ import { loadSensorsDetailed, requiredSensors } from '../hooks/sensors-core.mjs';
9
+ import { evaluateEvidenceBinding } from '../packages/vault/src/evidence-envelope.mjs';
10
+ import { bridgeSha256, canonicalBridgeJson } from '../packages/integrations/src/index.mjs';
11
+ import { issueCanonicalArtifactProof } from '../packages/integrations/src/canonical-bridge-authority.mjs';
12
+ import { captureGitSnapshot, resolveEvidenceIdentity, sensorConfigSha256 } from './evidence-envelope.mjs';
13
+ import { collectBridgeArtifactEvidence } from './ecosystem-bridge-artifact-collector.mjs';
14
+
15
+ function safeSlug(value) {
16
+ const slug = String(value || '').trim();
17
+ return /^[a-z0-9][a-z0-9._-]*$/i.test(slug) && !slug.includes('..') ? slug : '';
18
+ }
19
+
20
+ export function deriveCanonicalArtifactProof({
21
+ artifact, proof, projectRoot, vaultBase, changeSlug, sessionId = '', spawn = spawnSync,
22
+ } = {}) {
23
+ try {
24
+ const slug = safeSlug(changeSlug);
25
+ if (!slug || proof?.type !== 'evidence-envelope'
26
+ || String(proof?.external_id || '') !== String(artifact?.external_id || '')) return null;
27
+
28
+ const root = realpathSync(resolve(projectRoot));
29
+
30
+ const evidencePath = join(vaultBase, getLocale(vaultBase).folders.changes, slug, 'evidencia.json');
31
+ if (!existsSync(evidencePath) || !lstatSync(evidencePath).isFile()) return null;
32
+ const evidence = JSON.parse(readFileSync(evidencePath, 'utf8'));
33
+ const changeDir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
34
+ const tarefas = readFileSync(join(changeDir, 'tarefas.md'), 'utf8');
35
+ const tasks = parseTasks(tarefas);
36
+ const sensorIds = requiredSensors(tasks);
37
+ const loaded = loadSensorsDetailed(root);
38
+ if (loaded.error || loaded.missing) return null;
39
+ const reqIds = [...new Set(tasks.flatMap((task) => task.reqs ?? []))];
40
+ const effective = buildEffectiveRequirementPackage(vaultBase, changeDir, reqIds);
41
+ const identity = resolveEvidenceIdentity({
42
+ vaultBase, projectRoot: root, changeSlug: slug, sessionId, spawn,
43
+ });
44
+ const snapshot = captureGitSnapshot(root, { spawn });
45
+ const binding = evaluateEvidenceBinding(evidence, {
46
+ change_slug: slug,
47
+ identity,
48
+ snapshot,
49
+ tasks_sha256: tasksHashOf(tarefas),
50
+ effective_spec_sha256: `sha256:${effective.hash}`,
51
+ sensor_config_sha256: sensorConfigSha256(loaded.sensors, sensorIds),
52
+ });
53
+ if (binding.state !== 'bound') return null;
54
+ const envelopeArtifact = (evidence.external_artifacts || []).find((item) => (
55
+ item.source === 'superpowers'
56
+ && item.external_id === artifact.external_id
57
+ && item.kind === artifact.kind
58
+ && item.sha256 === artifact.sha256
59
+ && item.authority === 'verified'
60
+ ));
61
+ if (!envelopeArtifact) return null;
62
+ const currentArtifacts = collectBridgeArtifactEvidence({
63
+ projectRoot: root, tasks, sensors: evidence.sensors || [], spawn,
64
+ });
65
+ const currentArtifact = currentArtifacts.find((item) => item.external_id === artifact.external_id);
66
+ if (!currentArtifact || currentArtifact.authority !== 'verified'
67
+ || canonicalBridgeJson(currentArtifact) !== canonicalBridgeJson(envelopeArtifact)) return null;
68
+ const sensor = (evidence.sensors || []).find((item) => item.id === envelopeArtifact.sensor_id);
69
+ const artifactResult = (sensor?.artifact_results || []).find((item) => (
70
+ item.external_id === artifact.external_id && item.path === envelopeArtifact.path
71
+ && item.algorithm === 'sha256' && item.digest === artifact.sha256
72
+ ));
73
+ if (!sensor || sensor.status !== 'green' || sensor.exit_code !== 0 || !artifactResult) return null;
74
+
75
+ const canonical = {
76
+ schema_version: 1,
77
+ contract_kind: 'proof',
78
+ type: 'evidence-envelope',
79
+ authority: 'verified',
80
+ external_id: artifact.external_id,
81
+ artifact_sha256: artifact.sha256,
82
+ origin: { tool: 'wendkeep', evidence_envelope_id: evidence.envelope_id },
83
+ provenance: { state: 'verified', source: 'wendkeep-evidence-envelope' },
84
+ evidence_envelope_id: evidence.envelope_id,
85
+ sensor_id: envelopeArtifact.sensor_id,
86
+ task_id: envelopeArtifact.task_id,
87
+ path: envelopeArtifact.path,
88
+ head_sha: evidence.head_sha,
89
+ git_blob: envelopeArtifact.git_blob,
90
+ manifest_git_blob: envelopeArtifact.manifest_git_blob,
91
+ };
92
+ canonical.proof_id = bridgeSha256(canonicalBridgeJson(canonical));
93
+ return issueCanonicalArtifactProof(canonical);
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
@@ -0,0 +1,227 @@
1
+ import { existsSync, lstatSync, readFileSync, realpathSync } from 'node:fs';
2
+ import * as bridgeFs from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+
5
+ import {
6
+ importSpecKitProjection,
7
+ buildSpecKitStatusProjection,
8
+ ingestSuperpowersArtifacts,
9
+ inspectEcosystemBridges,
10
+ inspectBridgeAdapterRoot,
11
+ readBridgeConfig,
12
+ verifyExternalArtifact,
13
+ isProjectContainedPath,
14
+ sealBridgeProjection,
15
+ bridgeDiagnostic,
16
+ } from '../packages/integrations/src/index.mjs';
17
+ import { buildCanonicalExternalTaskDispatch } from './task.mjs';
18
+ import { resolveProjectVault } from './project-vault.mjs';
19
+ import { readSpecKitBaseline, writeSpecKitBaseline } from './ecosystem-bridge-baseline.mjs';
20
+ import { deriveCanonicalArtifactProof } from './ecosystem-bridge-proof.mjs';
21
+
22
+ export const BRIDGE_HELP = `wendkeep bridge <status|import-spec-kit|export-status|dispatch-superpowers|verify-artifacts>
23
+
24
+ --project <path> consumer project (default: current directory)
25
+ --config <path> config path (default: .wendkeep/ecosystem-bridges.json)
26
+ --task-id <id> canonical WendKeep task selected from the active context
27
+ --task-contract <path> optional submitted copy; rejected when it differs from canonical state
28
+ --spec-projection <path> optional read-only Spec Kit projection
29
+ --change <slug> causal change holding the canonical Spec Kit baseline/evidence
30
+ --accept-baseline anchor the first green Spec Kit projection in the bound Vault
31
+ --input <path> external artifacts JSON for verify-artifacts
32
+ --proofs <path> artifact IDs to resolve from the canonical Evidence Envelope
33
+ --json emit typed JSON
34
+ `;
35
+
36
+ function opt(argv, name) {
37
+ const index = argv.indexOf(name);
38
+ if (index >= 0) return argv[index + 1] || '';
39
+ return argv.find((item) => item.startsWith(`${name}=`))?.slice(name.length + 1) || '';
40
+ }
41
+
42
+ function inside(root, target) {
43
+ return isProjectContainedPath(root, target);
44
+ }
45
+
46
+ function readProjectJson(projectRoot, pathValue, label, optional = false) {
47
+ if (!pathValue && optional) return null;
48
+ if (!pathValue) throw Object.assign(new Error(`${label} path is required`), { code: 'BRIDGE_ARGUMENT_INVALID' });
49
+ const rootReal = realpathSync(projectRoot);
50
+ const path = resolve(projectRoot, pathValue);
51
+ if (!inside(rootReal, path)) throw Object.assign(new Error(`${label} escapes the project`), { code: 'BRIDGE_INPUT_ESCAPE' });
52
+ if (!existsSync(path) || !lstatSync(path).isFile()) {
53
+ throw Object.assign(new Error(`${label} file not found`), { code: 'BRIDGE_INPUT_MISSING' });
54
+ }
55
+ const pathReal = realpathSync(path);
56
+ if (!inside(rootReal, pathReal)) throw Object.assign(new Error(`${label} escapes the project`), { code: 'BRIDGE_INPUT_ESCAPE' });
57
+ if (lstatSync(pathReal).size > 1024 * 1024) throw Object.assign(new Error(`${label} exceeds 1 MiB`), { code: 'BRIDGE_INPUT_LIMIT' });
58
+ try {
59
+ return JSON.parse(readFileSync(pathReal, 'utf8'));
60
+ } catch (error) {
61
+ throw Object.assign(new Error(`${label} is not valid JSON: ${error.message}`), { code: 'BRIDGE_INPUT_INVALID' });
62
+ }
63
+ }
64
+
65
+ function write(value, json) {
66
+ process.stdout.write(json ? `${JSON.stringify(value)}\n` : `${JSON.stringify(value, null, 2)}\n`);
67
+ }
68
+
69
+ export function runEcosystemBridge(argv = []) {
70
+ const sub = argv[0];
71
+ if (!sub || ['help', '--help', '-h'].includes(sub)) {
72
+ process.stdout.write(BRIDGE_HELP);
73
+ return 0;
74
+ }
75
+ const json = argv.includes('--json');
76
+ const projectRoot = resolve(opt(argv, '--project') || process.cwd());
77
+ try {
78
+ if (sub === 'status') {
79
+ const result = inspectEcosystemBridges({ projectRoot, configPath: opt(argv, '--config'), fs: bridgeFs });
80
+ write(result, json);
81
+ return result.ok ? 0 : 1;
82
+ }
83
+ const loaded = readBridgeConfig(projectRoot, opt(argv, '--config'), { fs: bridgeFs });
84
+ if (sub === 'import-spec-kit') {
85
+ if (opt(argv, '--previous')) {
86
+ throw Object.assign(new Error('--previous cannot replace the canonical Vault baseline'), { code: 'BRIDGE_ARGUMENT_INVALID' });
87
+ }
88
+ const adapter = loaded.config.adapters['spec-kit'];
89
+ if (!adapter.enabled) {
90
+ const result = importSpecKitProjection({ projectRoot, config: loaded.config, fs: bridgeFs });
91
+ write(result, json);
92
+ return result.ok ? 0 : 1;
93
+ }
94
+ const changeSlug = opt(argv, '--change');
95
+ if (!changeSlug) throw Object.assign(new Error('--change is required for the canonical Spec Kit baseline'), { code: 'BRIDGE_BASELINE_CONTEXT_INVALID' });
96
+ const vault = resolveProjectVault({ startDir: projectRoot, explicitVault: opt(argv, '--vault') || '' }).base;
97
+ const baseline = readSpecKitBaseline(vault, changeSlug);
98
+ const current = importSpecKitProjection({ projectRoot, config: loaded.config, previousProjection: baseline, fs: bridgeFs });
99
+ let result = current;
100
+ if (argv.includes('--accept-baseline')) {
101
+ if (baseline && baseline.projection_id !== current.projection_id) {
102
+ result = sealBridgeProjection({
103
+ ...current, ok: false,
104
+ diagnostics: [...current.diagnostics, bridgeDiagnostic('BRIDGE_BASELINE_STALE', {
105
+ adapter: 'spec-kit', message: 'canonical Spec Kit baseline already exists and cannot be replaced implicitly',
106
+ })],
107
+ });
108
+ } else if (!baseline && current.ok) writeSpecKitBaseline(vault, changeSlug, current);
109
+ } else if (!baseline) {
110
+ result = sealBridgeProjection({
111
+ ...current, ok: false,
112
+ diagnostics: [...current.diagnostics, bridgeDiagnostic('BRIDGE_BASELINE_MISSING', {
113
+ adapter: 'spec-kit', message: 'anchor the first projection with --accept-baseline',
114
+ })],
115
+ });
116
+ }
117
+ write(result, json);
118
+ return result.ok ? 0 : 1;
119
+ }
120
+ if (sub === 'export-status') {
121
+ const sourceProjection = readProjectJson(projectRoot, opt(argv, '--spec-projection'), 'Spec Kit projection');
122
+ const taskContract = readProjectJson(projectRoot, opt(argv, '--task-contract'), 'task contract', true);
123
+ const artifactInput = readProjectJson(projectRoot, opt(argv, '--input'), 'artifact input', true);
124
+ const result = buildSpecKitStatusProjection({
125
+ sourceProjection,
126
+ taskContracts: taskContract ? [taskContract] : [],
127
+ artifacts: artifactInput ? (Array.isArray(artifactInput) ? artifactInput : artifactInput.artifacts || []) : [],
128
+ });
129
+ write(result, json);
130
+ return 0;
131
+ }
132
+ if (sub === 'dispatch-superpowers') {
133
+ const taskId = opt(argv, '--task-id');
134
+ if (!taskId) throw Object.assign(new Error('task id is required'), { code: 'BRIDGE_ARGUMENT_INVALID' });
135
+ const submitted = readProjectJson(projectRoot, opt(argv, '--task-contract'), 'task contract', true);
136
+ const specKitProjection = readProjectJson(projectRoot, opt(argv, '--spec-projection'), 'Spec Kit projection', true);
137
+ const adapter = loaded.config.adapters.superpowers;
138
+ const adapterRoot = inspectBridgeAdapterRoot(projectRoot, adapter.root || '.superpowers', { adapter: 'superpowers', fs: bridgeFs });
139
+ if (!adapterRoot.valid) {
140
+ const result = {
141
+ schema_version: 1, adapter: 'superpowers', active: false, ok: false,
142
+ diagnostics: adapterRoot.diagnostics,
143
+ };
144
+ write(result, json);
145
+ return 1;
146
+ }
147
+ const present = adapterRoot.present;
148
+ const vault = resolveProjectVault({
149
+ startDir: projectRoot,
150
+ explicitVault: opt(argv, '--vault') || '',
151
+ }).base;
152
+ const changeSlug = opt(argv, '--change');
153
+ let baselineProjection = null;
154
+ if (loaded.config.adapters['spec-kit'].enabled) {
155
+ if (!changeSlug || !specKitProjection) {
156
+ const result = {
157
+ schema_version: 1, adapter: 'superpowers', active: true, ok: false,
158
+ diagnostics: [bridgeDiagnostic('BRIDGE_BASELINE_MISSING', {
159
+ adapter: 'spec-kit', message: 'dispatch requires --change and --spec-projection when Spec Kit is active',
160
+ })],
161
+ };
162
+ write(result, json);
163
+ return 1;
164
+ }
165
+ baselineProjection = readSpecKitBaseline(vault, changeSlug);
166
+ if (!baselineProjection) {
167
+ const result = {
168
+ schema_version: 1, adapter: 'superpowers', active: true, ok: false,
169
+ diagnostics: [bridgeDiagnostic('BRIDGE_BASELINE_MISSING', {
170
+ adapter: 'spec-kit', message: 'canonical Spec Kit baseline is missing from the bound Vault change',
171
+ })],
172
+ };
173
+ write(result, json);
174
+ return 1;
175
+ }
176
+ }
177
+ const authorityArgs = ['--project', projectRoot, '--vault', vault, '--task-id', taskId];
178
+ for (const name of ['--session', '--change']) {
179
+ const value = opt(argv, name);
180
+ if (value) authorityArgs.push(name, value);
181
+ }
182
+ const result = buildCanonicalExternalTaskDispatch({
183
+ adapter: 'superpowers',
184
+ authorityArgv: authorityArgs,
185
+ taskId,
186
+ submittedTaskContract: submitted,
187
+ projectRoot,
188
+ specKitProjection,
189
+ baselineProjection,
190
+ config: loaded.config,
191
+ detectedVersion: adapter.version || '',
192
+ present,
193
+ });
194
+ write(result, json);
195
+ return result.ok ? 0 : 1;
196
+ }
197
+ if (sub === 'verify-artifacts') {
198
+ const input = readProjectJson(projectRoot, opt(argv, '--input'), 'artifact input');
199
+ const proofs = readProjectJson(projectRoot, opt(argv, '--proofs'), 'proof input');
200
+ const artifacts = ingestSuperpowersArtifacts(Array.isArray(input) ? input : input.artifacts);
201
+ const proofList = (Array.isArray(proofs) ? proofs : proofs.proofs) || [];
202
+ const canonicalRequested = proofList.some((proof) => proof?.type === 'evidence-envelope');
203
+ const changeSlug = opt(argv, '--change');
204
+ const vault = canonicalRequested && changeSlug
205
+ ? resolveProjectVault({ startDir: projectRoot, explicitVault: opt(argv, '--vault') || '' }).base
206
+ : '';
207
+ const verified = artifacts.map((artifact) => {
208
+ const artifactProofs = proofList.filter((proof) => (
209
+ !proof.external_id || proof.external_id === artifact.external_id
210
+ ));
211
+ const canonicalProofs = vault ? artifactProofs.map((proof) => deriveCanonicalArtifactProof({
212
+ artifact, proof, projectRoot, vaultBase: vault, changeSlug,
213
+ sessionId: opt(argv, '--session') || '',
214
+ })).filter(Boolean) : [];
215
+ return verifyExternalArtifact(artifact, { proofs: artifactProofs, canonicalProofs });
216
+ });
217
+ const result = { schema_version: 1, ok: verified.every((item) => item.authority === 'verified'), artifacts: verified };
218
+ write(result, json);
219
+ return result.ok ? 0 : 1;
220
+ }
221
+ throw Object.assign(new Error(`unknown bridge subcommand: ${sub}`), { code: 'BRIDGE_SUBCOMMAND_UNKNOWN' });
222
+ } catch (error) {
223
+ const payload = { ok: false, code: error?.code || 'BRIDGE_COMMAND_FAILED', error: error?.message || String(error) };
224
+ process.stderr.write(json ? `${JSON.stringify(payload)}\n` : `wendkeep bridge: ${payload.code}: ${payload.error}\n`);
225
+ return 2;
226
+ }
227
+ }
@@ -268,6 +268,7 @@ export function buildEvidenceEnvelope({
268
268
  version = '',
269
269
  runtimePlatform = `${process.platform}-${process.arch}`,
270
270
  hostCoverage = null,
271
+ externalArtifacts = [],
271
272
  } = {}) {
272
273
  const envelope = {
273
274
  schema_version: 2,
@@ -287,6 +288,7 @@ export function buildEvidenceEnvelope({
287
288
  started_at: startedAt,
288
289
  finished_at: finishedAt,
289
290
  sensors,
291
+ ...(externalArtifacts.length ? { external_artifacts: structuredClone(externalArtifacts) } : {}),
290
292
  ...(hostCoverage ? { host_coverage: structuredClone(hostCoverage) } : {}),
291
293
  tdd_attestations: tddAttestations,
292
294
  };
@@ -8,3 +8,11 @@ export function observerAuthHeaders(token, headers = {}) {
8
8
  ? { ...headers, authorization: `Bearer ${resolved}` }
9
9
  : { ...headers };
10
10
  }
11
+
12
+ export {
13
+ registerObserverToken,
14
+ resolveObserverPrincipal,
15
+ revokeObserverToken,
16
+ rotateObserverToken,
17
+ } from '../packages/observer/src/token-registry.mjs';
18
+ export { authorizeObserverPrincipal } from '../packages/observer/src/authz.mjs';
@@ -1,4 +1,5 @@
1
1
  import { basename } from 'node:path';
2
+ import { redactObserverText, redactObserverValue } from '../packages/observer/src/redaction.mjs';
2
3
 
3
4
  const TRANSCRIPT_PATH_KEY = /^(?:transcript_path|agent_transcript_path|transcriptPath|agentTranscriptPath)$/i;
4
5
  const TRANSCRIPT_PATH_LINE = /^(\s*["']?(?:transcript_path|agent_transcript_path|transcriptPath|agentTranscriptPath)["']?\s*:\s*)(["']?)(.*?)(\2)(\s*,?\s*)$/gmi;
@@ -8,16 +9,19 @@ function sourceLabel(value) {
8
9
  }
9
10
 
10
11
  export function sanitizeObserverContent(content) {
11
- return String(content || '').replace(TRANSCRIPT_PATH_LINE, (_line, prefix, quote, value, _closing, suffix) => (
12
+ const safePaths = String(content || '').replace(TRANSCRIPT_PATH_LINE, (_line, prefix, quote, value, _closing, suffix) => (
12
13
  `${prefix}${quote}${sourceLabel(value)}${quote}${suffix}`
13
14
  ));
15
+ return redactObserverText(safePaths);
14
16
  }
15
17
 
16
18
  export function sanitizeObserverMetadata(value) {
17
19
  if (Array.isArray(value)) return value.map(sanitizeObserverMetadata);
18
20
  if (!value || typeof value !== 'object') return value;
19
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [
21
+ return redactObserverValue(Object.fromEntries(Object.entries(value).map(([key, item]) => [
20
22
  key,
21
23
  TRANSCRIPT_PATH_KEY.test(key) ? sourceLabel(item) : sanitizeObserverMetadata(item),
22
- ]));
24
+ ])));
23
25
  }
26
+
27
+ export { redactObserverText, redactObserverValue };
@@ -3,10 +3,37 @@ import { join } from 'node:path';
3
3
  import { publishObserverSqlIncremental } from './observer-sql-publish.mjs';
4
4
  import { observerAuthHeaders, resolveObserverToken } from './observer-auth.mjs';
5
5
  import { readProjectForValidation } from '../packages/vault/src/validate-memory.mjs';
6
+ import { createObserverPolicy } from '../packages/observer/src/policy.mjs';
7
+ import { createObserverEncryption } from '../packages/observer/src/encryption.mjs';
6
8
 
7
9
  const OUTBOX_REL = join('.brain', 'observer-outbox');
8
10
  const REQUEST_TIMEOUT_MS = 500;
9
11
 
12
+ function decodeObserverKey(value) {
13
+ const raw = String(value || '').trim();
14
+ const key = /^[a-f0-9]{64}$/i.test(raw) ? Buffer.from(raw, 'hex') : Buffer.from(raw, 'base64');
15
+ if (key.byteLength !== 32) throw Object.assign(new Error('Observer outbox key deve ter 32 bytes em hex/base64.'), { code: 'observer_encryption_key_invalid' });
16
+ return key;
17
+ }
18
+
19
+ export function resolveObserverPublisherSecurity({ env = process.env } = {}) {
20
+ const policyFile = String(env.WENDKEEP_OBSERVER_POLICY_FILE || '').trim();
21
+ const policy = policyFile
22
+ ? createObserverPolicy(JSON.parse(readFileSync(policyFile, 'utf8')))
23
+ : createObserverPolicy();
24
+ const keyEnvName = String(env.WENDKEEP_OBSERVER_OUTBOX_KEY_ENV || '').trim();
25
+ if (!keyEnvName) return { policy, outboxEncryption: null };
26
+ const key = decodeObserverKey(env[keyEnvName]);
27
+ return {
28
+ policy,
29
+ outboxEncryption: createObserverEncryption({
30
+ required: true,
31
+ keyId: String(env.WENDKEEP_OBSERVER_OUTBOX_KEY_ID || keyEnvName),
32
+ keyProvider: () => key,
33
+ }),
34
+ };
35
+ }
36
+
10
37
  function outboxDir(vaultBase) {
11
38
  return join(vaultBase, OUTBOX_REL);
12
39
  }
@@ -92,6 +119,8 @@ export async function publishObserverSnapshot({
92
119
  now = new Date(),
93
120
  input = {},
94
121
  token = process.env.WENDKEEP_OBSERVER_TOKEN || '',
122
+ policy = null,
123
+ outboxEncryption = null,
95
124
  } = {}) {
96
125
  try {
97
126
  const project = readProjectForValidation(vaultBase);
@@ -103,6 +132,8 @@ export async function publishObserverSnapshot({
103
132
  input,
104
133
  now,
105
134
  token,
135
+ policy,
136
+ outboxEncryption,
106
137
  });
107
138
  // SQLite is the only live authority. The legacy snapshot store remains
108
139
  // readable solely as a migration source for pre-SQL installations.