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.
- package/CHANGELOG.md +35 -0
- package/README.en.md +3 -2
- package/README.md +3 -2
- package/bin/wendkeep.mjs +1 -0
- package/docs/en/commands/ecosystem-bridges.md +172 -0
- package/docs/en/commands/observer-security.md +154 -0
- package/docs/en/commands/observer.md +30 -12
- package/docs/en/commands/verify.md +6 -0
- package/docs/pt-BR/commands/ecosystem-bridges.md +169 -0
- package/docs/pt-BR/commands/observer-security.md +154 -0
- package/docs/pt-BR/commands/observer.md +30 -12
- package/docs/pt-BR/commands/verify.md +6 -0
- package/hooks/observer-publish.mjs +3 -1
- package/package.json +2 -1
- package/packages/cli/src/index.mjs +10 -1
- package/packages/harness/src/sensors-core.mjs +49 -3
- package/packages/integrations/src/bridge-config.mjs +139 -0
- package/packages/integrations/src/bridge-contract.mjs +316 -0
- package/packages/integrations/src/bridge-diagnostics.mjs +45 -0
- package/packages/integrations/src/canonical-bridge-authority.mjs +32 -0
- package/packages/integrations/src/capabilities.mjs +34 -0
- package/packages/integrations/src/ecosystem-bridge.mjs +82 -0
- package/packages/integrations/src/index.mjs +6 -0
- package/packages/integrations/src/spec-kit-adapter.mjs +259 -0
- package/packages/integrations/src/superpowers-adapter.mjs +269 -0
- package/packages/mcp/src/executor.mjs +35 -2
- package/packages/observer/package.json +16 -0
- package/packages/observer/src/audit.mjs +1 -0
- package/packages/observer/src/authz.mjs +38 -0
- package/packages/observer/src/encryption.mjs +75 -0
- package/packages/observer/src/index.mjs +7 -0
- package/packages/observer/src/policy.mjs +305 -0
- package/packages/observer/src/purge.mjs +100 -0
- package/packages/observer/src/redaction.mjs +54 -0
- package/packages/observer/src/retention.mjs +39 -0
- package/packages/observer/src/token-registry.mjs +122 -0
- package/schema/ecosystem-bridge-artifact-manifest-v1.schema.json +30 -0
- package/schema/ecosystem-bridge-v1.schema.json +65 -0
- package/schema/observer/006-observer-security.sql +64 -0
- package/schema/observer-policy-v1.schema.json +63 -0
- package/schema/sync-event-v1.schema.json +10 -0
- package/schema/wendkeep.evidence-envelope-v2.schema.json +39 -0
- package/schema/wendkeep.sensors.schema.json +14 -0
- package/src/doctor.mjs +6 -1
- package/src/ecosystem-bridge-artifact-collector.mjs +111 -0
- package/src/ecosystem-bridge-baseline.mjs +58 -0
- package/src/ecosystem-bridge-proof.mjs +97 -0
- package/src/ecosystem-bridges.mjs +227 -0
- package/src/evidence-envelope.mjs +2 -0
- package/src/observer-auth.mjs +8 -0
- package/src/observer-privacy.mjs +7 -3
- package/src/observer-publish.mjs +31 -0
- package/src/observer-server.mjs +179 -20
- package/src/observer-sql-migrate.mjs +5 -2
- package/src/observer-sql-publish.mjs +114 -39
- package/src/observer-sql-store.mjs +299 -45
- package/src/observer-transcript-store.mjs +23 -8
- package/src/observer.mjs +145 -12
- package/src/sync-protocol.mjs +20 -0
- package/src/task-contracts.mjs +19 -0
- package/src/task.mjs +82 -0
- package/src/verify.mjs +9 -0
- package/web/observer/app.mjs +107 -31
- package/web/observer/index.html +7 -0
- package/web/observer/styles.css +5 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { basename, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { assessBridgeAdapter } from './bridge-config.mjs';
|
|
4
|
+
import {
|
|
5
|
+
bridgeSha256, canonicalBridgeJson, createBridgeProjection, detectBridgeDrift,
|
|
6
|
+
sealBridgeProjection, validateBridgeProjection, validateBridgeRuntimeEnvelope,
|
|
7
|
+
} from './bridge-contract.mjs';
|
|
8
|
+
import { bridgeDiagnostic } from './bridge-diagnostics.mjs';
|
|
9
|
+
|
|
10
|
+
const MAX_SOURCE_FILES = 256;
|
|
11
|
+
const MAX_SOURCE_BYTES = 1024 * 1024;
|
|
12
|
+
|
|
13
|
+
function inside(root, target) {
|
|
14
|
+
const rel = relative(root, target);
|
|
15
|
+
return rel === '' || (!rel.startsWith(`..${sep}`) && rel !== '..' && !isAbsolute(rel));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function findMarkdownFiles(root, fs) {
|
|
19
|
+
const { existsSync, lstatSync, readdirSync } = fs;
|
|
20
|
+
const files = [];
|
|
21
|
+
function visit(dir) {
|
|
22
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
23
|
+
const path = resolve(dir, entry.name);
|
|
24
|
+
if (entry.isSymbolicLink()) continue;
|
|
25
|
+
if (entry.isDirectory()) visit(path);
|
|
26
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
27
|
+
files.push(path);
|
|
28
|
+
if (files.length > MAX_SOURCE_FILES) throw Object.assign(new Error('Spec Kit source exceeds file limit'), { code: 'BRIDGE_SOURCE_LIMIT' });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (existsSync(root)) {
|
|
33
|
+
const stat = lstatSync(root);
|
|
34
|
+
if (stat.isDirectory() && !stat.isSymbolicLink()) visit(root);
|
|
35
|
+
}
|
|
36
|
+
return files.sort();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function sourceKind(path) {
|
|
40
|
+
const name = basename(path).toLowerCase();
|
|
41
|
+
if (name === 'constitution.md') return 'constitution';
|
|
42
|
+
if (name === 'tasks.md') return 'task';
|
|
43
|
+
if (name === 'plan.md') return 'plan';
|
|
44
|
+
return 'spec';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function idsFor(kind, content, path) {
|
|
48
|
+
const patterns = kind === 'task'
|
|
49
|
+
? [/\b(T\d{3,})\b/g]
|
|
50
|
+
: kind === 'constitution'
|
|
51
|
+
? [/\b(CONST-[A-Z0-9-]+)\b/g]
|
|
52
|
+
: kind === 'spec'
|
|
53
|
+
? [/\b([A-Z][A-Z0-9]*-\d+)\b/g, /\b(US\d+)\b/g]
|
|
54
|
+
: [];
|
|
55
|
+
const ids = [];
|
|
56
|
+
for (const pattern of patterns) {
|
|
57
|
+
for (const match of content.matchAll(pattern)) if (!ids.includes(match[1])) ids.push(match[1]);
|
|
58
|
+
}
|
|
59
|
+
if (ids.length) return ids;
|
|
60
|
+
const rel = path.replaceAll('\\', '/');
|
|
61
|
+
return [`${rel.replace(/\.md$/i, '').replace(/[^a-zA-Z0-9_-]+/g, ':')}`];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function featureFor(projectRoot, path) {
|
|
65
|
+
const parts = relative(projectRoot, path).replaceAll('\\', '/').split('/');
|
|
66
|
+
const specsIndex = parts.lastIndexOf('specs');
|
|
67
|
+
const change_slug = specsIndex >= 0 ? String(parts[specsIndex + 1] || '') : '';
|
|
68
|
+
return { change_slug, capability: change_slug.replace(/^\d+[-_]?/, '') || change_slug };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function linkedTasks(content) {
|
|
72
|
+
const links = new Map();
|
|
73
|
+
for (const line of String(content).split(/\r?\n/)) {
|
|
74
|
+
const taskId = line.match(/\b(T\d{3,})\b/)?.[1];
|
|
75
|
+
if (!taskId) continue;
|
|
76
|
+
for (const match of line.matchAll(/\b([A-Z][A-Z0-9]*-\d+|US\d+)\b/g)) {
|
|
77
|
+
const ids = links.get(match[1]) || [];
|
|
78
|
+
if (!ids.includes(taskId)) ids.push(taskId);
|
|
79
|
+
links.set(match[1], ids);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return links;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function titleFor(content) {
|
|
86
|
+
return String(content).match(/^#\s+(.+)$/m)?.[1]?.trim().slice(0, 300) || '';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function detectSpecKit({ projectRoot, config, fs = null } = {}) {
|
|
90
|
+
if (!fs || ['existsSync', 'lstatSync', 'readFileSync', 'realpathSync'].some((name) => typeof fs[name] !== 'function')) {
|
|
91
|
+
return {
|
|
92
|
+
present: false,
|
|
93
|
+
source: resolve(projectRoot, config?.adapters?.['spec-kit']?.root || '.specify'),
|
|
94
|
+
version: '',
|
|
95
|
+
assessment: { available: false, diagnostics: [bridgeDiagnostic('BRIDGE_SOURCE_INVALID', {
|
|
96
|
+
adapter: 'spec-kit', message: 'bridge filesystem capability is unavailable',
|
|
97
|
+
})] },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const { existsSync, lstatSync, readFileSync, realpathSync } = fs;
|
|
101
|
+
const adapter = config?.adapters?.['spec-kit'] || { enabled: false };
|
|
102
|
+
const source = resolve(projectRoot, adapter.root || '.specify');
|
|
103
|
+
if (!adapter.enabled) {
|
|
104
|
+
return { present: existsSync(source), source, version: '', assessment: assessBridgeAdapter('spec-kit', { config, present: existsSync(source) }) };
|
|
105
|
+
}
|
|
106
|
+
if (!existsSync(source) || !lstatSync(source).isDirectory()) {
|
|
107
|
+
return { present: false, source, version: '', assessment: assessBridgeAdapter('spec-kit', { config, present: false }) };
|
|
108
|
+
}
|
|
109
|
+
const projectReal = realpathSync(projectRoot);
|
|
110
|
+
const sourceReal = realpathSync(source);
|
|
111
|
+
if (!inside(projectReal, sourceReal)) {
|
|
112
|
+
return {
|
|
113
|
+
present: false,
|
|
114
|
+
source,
|
|
115
|
+
version: '',
|
|
116
|
+
assessment: { available: false, diagnostics: [bridgeDiagnostic('BRIDGE_SOURCE_INVALID', { adapter: 'spec-kit', path: source })] },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const versionPath = resolve(sourceReal, 'version');
|
|
120
|
+
const version = adapter.version || (existsSync(versionPath) ? readFileSync(versionPath, 'utf8').trim() : '');
|
|
121
|
+
return { present: true, source: sourceReal, version, assessment: assessBridgeAdapter('spec-kit', { config, detectedVersion: version }) };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function importSpecKitProjection({ projectRoot, config, previousProjection = null, fs = null } = {}) {
|
|
125
|
+
const detected = detectSpecKit({ projectRoot: resolve(projectRoot), config, fs });
|
|
126
|
+
if (!detected.assessment.available) {
|
|
127
|
+
const disabled = detected.assessment.diagnostics.every((item) => !item.blocking);
|
|
128
|
+
return {
|
|
129
|
+
schema_version: 1,
|
|
130
|
+
adapter: 'spec-kit',
|
|
131
|
+
authority: 'reported',
|
|
132
|
+
active: false,
|
|
133
|
+
ok: disabled,
|
|
134
|
+
references: [],
|
|
135
|
+
diagnostics: detected.assessment.diagnostics,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
if (!fs || ['lstatSync', 'readFileSync', 'readdirSync'].some((name) => typeof fs[name] !== 'function')) {
|
|
140
|
+
throw Object.assign(new Error('bridge filesystem capability is unavailable'), { code: 'BRIDGE_SOURCE_INVALID' });
|
|
141
|
+
}
|
|
142
|
+
const { lstatSync, readFileSync } = fs;
|
|
143
|
+
const files = [
|
|
144
|
+
...findMarkdownFiles(resolve(detected.source, 'memory'), fs),
|
|
145
|
+
...findMarkdownFiles(resolve(detected.source, 'specs'), fs),
|
|
146
|
+
];
|
|
147
|
+
const references = [];
|
|
148
|
+
const taskLinks = new Map();
|
|
149
|
+
for (const path of [...new Set(files)]) {
|
|
150
|
+
const stat = lstatSync(path);
|
|
151
|
+
if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES) {
|
|
152
|
+
throw Object.assign(new Error(`Spec Kit source exceeds byte limit: ${path}`), { code: 'BRIDGE_SOURCE_LIMIT' });
|
|
153
|
+
}
|
|
154
|
+
const content = readFileSync(path, 'utf8');
|
|
155
|
+
const kind = sourceKind(path);
|
|
156
|
+
const relativePath = relative(projectRoot, path).replaceAll('\\', '/');
|
|
157
|
+
const sha256 = bridgeSha256(content);
|
|
158
|
+
if (kind === 'task') {
|
|
159
|
+
for (const [sourceId, taskIds] of linkedTasks(content)) {
|
|
160
|
+
const current = taskLinks.get(sourceId) || [];
|
|
161
|
+
taskLinks.set(sourceId, [...new Set([...current, ...taskIds])]);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
for (const source_id of idsFor(kind, content, relativePath)) {
|
|
165
|
+
references.push({ kind, source_id, path: relativePath, sha256, title: titleFor(content) });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const duplicateDiagnostics = [];
|
|
169
|
+
const seenIds = new Map();
|
|
170
|
+
for (const reference of references) {
|
|
171
|
+
const firstPath = seenIds.get(reference.source_id);
|
|
172
|
+
if (firstPath && firstPath !== reference.path) {
|
|
173
|
+
duplicateDiagnostics.push(bridgeDiagnostic('BRIDGE_SOURCE_ID_DUPLICATE', {
|
|
174
|
+
adapter: 'spec-kit', path: reference.path, expected: firstPath, observed: reference.path,
|
|
175
|
+
message: `external source id is duplicated: ${reference.source_id}`,
|
|
176
|
+
}));
|
|
177
|
+
} else seenIds.set(reference.source_id, reference.path);
|
|
178
|
+
}
|
|
179
|
+
const mappings = references.filter((item) => item.kind === 'spec').map((item) => {
|
|
180
|
+
const feature = featureFor(projectRoot, resolve(projectRoot, item.path));
|
|
181
|
+
return {
|
|
182
|
+
source_id: item.source_id,
|
|
183
|
+
source_kind: /^(?:US-?\d+|STORY-\d+)$/.test(item.source_id) ? 'story' : 'requirement',
|
|
184
|
+
capability: feature.capability,
|
|
185
|
+
change_slug: feature.change_slug,
|
|
186
|
+
task_ids: taskLinks.get(item.source_id) || [],
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
const adapterClaims = config?.adapters?.['spec-kit']?.ownership_claims || [];
|
|
190
|
+
const projection = createBridgeProjection({
|
|
191
|
+
adapter: 'spec-kit',
|
|
192
|
+
adapterVersion: detected.version,
|
|
193
|
+
sourceRoot: relative(projectRoot, detected.source),
|
|
194
|
+
claims: [
|
|
195
|
+
{ concept: 'spec_source', owner: 'wendkeep' },
|
|
196
|
+
{ concept: 'plan', owner: 'wendkeep' },
|
|
197
|
+
{ concept: 'task', owner: 'wendkeep' },
|
|
198
|
+
...adapterClaims,
|
|
199
|
+
],
|
|
200
|
+
references,
|
|
201
|
+
mappings,
|
|
202
|
+
});
|
|
203
|
+
const drift = previousProjection ? detectBridgeDrift(previousProjection, projection) : { ok: true, diagnostics: [] };
|
|
204
|
+
return sealBridgeProjection({
|
|
205
|
+
...projection,
|
|
206
|
+
active: true,
|
|
207
|
+
ok: projection.ok && drift.ok && duplicateDiagnostics.length === 0,
|
|
208
|
+
diagnostics: [...projection.diagnostics, ...duplicateDiagnostics, ...drift.diagnostics],
|
|
209
|
+
});
|
|
210
|
+
} catch (error) {
|
|
211
|
+
return {
|
|
212
|
+
schema_version: 1,
|
|
213
|
+
adapter: 'spec-kit',
|
|
214
|
+
authority: 'reported',
|
|
215
|
+
active: true,
|
|
216
|
+
ok: false,
|
|
217
|
+
references: [],
|
|
218
|
+
diagnostics: [bridgeDiagnostic(error?.code || 'BRIDGE_SOURCE_INVALID', {
|
|
219
|
+
adapter: 'spec-kit', path: detected.source, message: error?.message || String(error),
|
|
220
|
+
})],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function buildSpecKitStatusProjection({ sourceProjection, taskContracts = [], artifacts = [] } = {}) {
|
|
226
|
+
const sourceValidation = validateBridgeProjection(sourceProjection);
|
|
227
|
+
if (sourceProjection?.adapter !== 'spec-kit' || sourceProjection?.ok !== true || !sourceValidation.valid) {
|
|
228
|
+
throw Object.assign(new Error('a valid Spec Kit source projection is required'), { code: 'BRIDGE_CONTRACT_INVALID' });
|
|
229
|
+
}
|
|
230
|
+
const projection = {
|
|
231
|
+
schema_version: 1,
|
|
232
|
+
contract_kind: 'status-projection',
|
|
233
|
+
adapter: 'spec-kit',
|
|
234
|
+
mode: 'status-projection',
|
|
235
|
+
source_projection_id: String(sourceProjection.projection_id),
|
|
236
|
+
authority: 'reported',
|
|
237
|
+
canonical_owner: 'wendkeep',
|
|
238
|
+
origin: { tool: 'wendkeep', source_projection_id: String(sourceProjection.projection_id) },
|
|
239
|
+
provenance: { state: 'reported', source: 'canonical-status-export' },
|
|
240
|
+
tasks: (Array.isArray(taskContracts) ? taskContracts : []).map((task) => ({
|
|
241
|
+
task_id: String(task?.task_id || ''),
|
|
242
|
+
contract_id: String(task?.contract_id || ''),
|
|
243
|
+
status: String(task?.status || ''),
|
|
244
|
+
})),
|
|
245
|
+
evidence: (Array.isArray(artifacts) ? artifacts : []).map((artifact) => ({
|
|
246
|
+
external_id: String(artifact?.external_id || ''),
|
|
247
|
+
sha256: String(artifact?.sha256 || ''),
|
|
248
|
+
authority: 'reported',
|
|
249
|
+
})),
|
|
250
|
+
};
|
|
251
|
+
projection.status_projection_id = bridgeSha256(canonicalBridgeJson(projection));
|
|
252
|
+
const validation = validateBridgeRuntimeEnvelope(projection);
|
|
253
|
+
if (!validation.valid) {
|
|
254
|
+
throw Object.assign(new Error('generated Spec Kit status projection is invalid'), {
|
|
255
|
+
code: 'BRIDGE_CONTRACT_INVALID', diagnostics: validation.diagnostics,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return projection;
|
|
259
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { assessBridgeAdapter } from './bridge-config.mjs';
|
|
2
|
+
import {
|
|
3
|
+
bridgeSha256, canonicalBridgeJson, validateBridgeOwnership, validateBridgeProjection,
|
|
4
|
+
validateBridgeRuntimeEnvelope,
|
|
5
|
+
} from './bridge-contract.mjs';
|
|
6
|
+
import { bridgeDiagnostic } from './bridge-diagnostics.mjs';
|
|
7
|
+
import {
|
|
8
|
+
readCanonicalArtifactProof, readCanonicalDispatchAuthority,
|
|
9
|
+
} from './canonical-bridge-authority.mjs';
|
|
10
|
+
|
|
11
|
+
function publicTaskContract(contract) {
|
|
12
|
+
return {
|
|
13
|
+
schema_version: 1,
|
|
14
|
+
contract_id: String(contract.contract_id),
|
|
15
|
+
task_id: String(contract.task_id),
|
|
16
|
+
change_slug: String(contract.change_slug || ''),
|
|
17
|
+
title: String(contract.title || '').slice(0, 500),
|
|
18
|
+
phase: String(contract.phase || ''),
|
|
19
|
+
status: String(contract.status || ''),
|
|
20
|
+
inputs: [...(contract.inputs || [])].map(String),
|
|
21
|
+
expected_outputs: [...(contract.expected_outputs || [])].map(String),
|
|
22
|
+
acceptance_criteria: [...(contract.acceptance_criteria || [])].map(String),
|
|
23
|
+
requirement_ids: [...(contract.requirement_ids || [])].map(String),
|
|
24
|
+
required_sensors: [...(contract.required_sensors || [])].map(String),
|
|
25
|
+
required_artifacts: [...(contract.required_artifacts || [])].map(String),
|
|
26
|
+
dependencies: [...(contract.dependencies || [])].map(String),
|
|
27
|
+
authored_sha256: String(contract.authored_sha256 || ''),
|
|
28
|
+
binding: {
|
|
29
|
+
project_id: String(contract.binding?.project_id || ''),
|
|
30
|
+
active_context_id: String(contract.binding?.active_context_id || ''),
|
|
31
|
+
head_sha: String(contract.binding?.head_sha || ''),
|
|
32
|
+
tasks_sha256: String(contract.binding?.tasks_sha256 || ''),
|
|
33
|
+
effective_spec_sha256: String(contract.binding?.effective_spec_sha256 || ''),
|
|
34
|
+
artifact_manifest_sha256: String(contract.binding?.artifact_manifest_sha256 || ''),
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function validateDispatchInput(taskContract, activeContext) {
|
|
40
|
+
const diagnostics = [];
|
|
41
|
+
if (taskContract?.schema_version !== 1 || !taskContract?.contract_id || !taskContract?.task_id
|
|
42
|
+
|| !taskContract?.authored_sha256 || !taskContract?.binding?.active_context_id || !taskContract?.binding?.head_sha) {
|
|
43
|
+
diagnostics.push(bridgeDiagnostic('BRIDGE_CONTRACT_INVALID', {
|
|
44
|
+
adapter: 'superpowers', message: 'a complete canonical task contract is required',
|
|
45
|
+
}));
|
|
46
|
+
return diagnostics;
|
|
47
|
+
}
|
|
48
|
+
for (const field of ['active_context_id', 'head_sha']) {
|
|
49
|
+
if (activeContext?.[field] && String(activeContext[field]) !== String(taskContract.binding[field])) {
|
|
50
|
+
diagnostics.push(bridgeDiagnostic('BRIDGE_CONTRACT_INVALID', {
|
|
51
|
+
adapter: 'superpowers',
|
|
52
|
+
expected: taskContract.binding[field],
|
|
53
|
+
observed: activeContext[field],
|
|
54
|
+
message: `canonical task binding is stale: ${field}`,
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return diagnostics;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function canonicalDispatchAuthority(submitted, authority) {
|
|
62
|
+
const diagnostics = [];
|
|
63
|
+
const issued = readCanonicalDispatchAuthority(authority);
|
|
64
|
+
if (!issued?.task_contract || !issued?.active_context) {
|
|
65
|
+
diagnostics.push(bridgeDiagnostic('BRIDGE_CANONICAL_AUTHORITY_REQUIRED', {
|
|
66
|
+
adapter: 'superpowers', message: 'dispatch requires a task contract rederived from canonical WendKeep state',
|
|
67
|
+
}));
|
|
68
|
+
return { taskContract: null, activeContext: null, diagnostics };
|
|
69
|
+
}
|
|
70
|
+
const canonical = issued.task_contract;
|
|
71
|
+
const activeContext = issued.active_context;
|
|
72
|
+
for (const field of ['contract_id', 'authored_sha256', 'task_id', 'change_slug']) {
|
|
73
|
+
if (String(submitted?.[field] || '') !== String(canonical?.[field] || '')) {
|
|
74
|
+
diagnostics.push(bridgeDiagnostic('BRIDGE_CONTRACT_STALE', {
|
|
75
|
+
adapter: 'superpowers', expected: canonical?.[field], observed: submitted?.[field],
|
|
76
|
+
message: `submitted task contract differs from canonical ${field}`,
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const field of ['project_id', 'active_context_id', 'head_sha', 'tasks_sha256', 'effective_spec_sha256', 'artifact_manifest_sha256']) {
|
|
81
|
+
if (String(submitted?.binding?.[field] || '') !== String(canonical?.binding?.[field] || '')) {
|
|
82
|
+
diagnostics.push(bridgeDiagnostic('BRIDGE_CONTRACT_STALE', {
|
|
83
|
+
adapter: 'superpowers', expected: canonical?.binding?.[field], observed: submitted?.binding?.[field],
|
|
84
|
+
message: `submitted task binding differs from canonical ${field}`,
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
diagnostics.push(...validateDispatchInput(canonical, activeContext));
|
|
89
|
+
return { taskContract: canonical, activeContext, diagnostics };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function buildSuperpowersDispatch({
|
|
93
|
+
taskContract, canonicalAuthority = null, handoffContract = null, specKitProjection = null,
|
|
94
|
+
config, detectedVersion = '', present = true,
|
|
95
|
+
} = {}) {
|
|
96
|
+
const assessment = assessBridgeAdapter('superpowers', { config, detectedVersion, present });
|
|
97
|
+
if (!assessment.available) {
|
|
98
|
+
const disabled = assessment.diagnostics.every((item) => !item.blocking);
|
|
99
|
+
return { schema_version: 1, adapter: 'superpowers', active: false, ok: disabled, diagnostics: assessment.diagnostics };
|
|
100
|
+
}
|
|
101
|
+
const canonical = canonicalDispatchAuthority(taskContract, canonicalAuthority);
|
|
102
|
+
const authoritativeTask = canonical.taskContract;
|
|
103
|
+
const diagnostics = [...canonical.diagnostics];
|
|
104
|
+
const ownership = validateBridgeOwnership({
|
|
105
|
+
adapter: 'superpowers', claims: config?.adapters?.superpowers?.ownership_claims || [],
|
|
106
|
+
});
|
|
107
|
+
diagnostics.push(...ownership.diagnostics);
|
|
108
|
+
const handoffEnvelope = handoffContract ? {
|
|
109
|
+
schema_version: 1,
|
|
110
|
+
contract_kind: 'handoff',
|
|
111
|
+
handoff_id: String(handoffContract.handoff_id || ''),
|
|
112
|
+
task_contract_id: String(handoffContract.task_contract_id || ''),
|
|
113
|
+
task_id: String(handoffContract.task_id || ''),
|
|
114
|
+
head_sha: String(handoffContract.head_sha || ''),
|
|
115
|
+
authority: 'reported',
|
|
116
|
+
origin: { tool: 'wendkeep' },
|
|
117
|
+
provenance: { state: 'reported', source: 'canonical-handoff-reference' },
|
|
118
|
+
} : null;
|
|
119
|
+
const handoffValidation = handoffEnvelope ? validateBridgeRuntimeEnvelope(handoffEnvelope) : { valid: true };
|
|
120
|
+
if (handoffContract && (!handoffValidation.valid || handoffContract.schema_version !== 1
|
|
121
|
+
|| (handoffContract.task_contract_id && handoffContract.task_contract_id !== authoritativeTask?.contract_id)
|
|
122
|
+
|| (handoffContract.task_id && handoffContract.task_id !== authoritativeTask?.task_id))) {
|
|
123
|
+
diagnostics.push(bridgeDiagnostic('BRIDGE_CONTRACT_INVALID', {
|
|
124
|
+
adapter: 'superpowers', message: 'handoff does not bind the selected canonical task contract',
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
if (specKitProjection) {
|
|
128
|
+
const blockers = (Array.isArray(specKitProjection.diagnostics) ? specKitProjection.diagnostics : [])
|
|
129
|
+
.filter((item) => item?.blocking)
|
|
130
|
+
.map((item) => ({
|
|
131
|
+
schema_version: 1,
|
|
132
|
+
code: String(item.code || 'BRIDGE_SOURCE_DRIFT'),
|
|
133
|
+
adapter: 'spec-kit',
|
|
134
|
+
blocking: true,
|
|
135
|
+
message: String(item.message || item.code || 'external source blocks dispatch'),
|
|
136
|
+
...(item.expected ? { expected: String(item.expected) } : {}),
|
|
137
|
+
...(item.observed ? { observed: String(item.observed) } : {}),
|
|
138
|
+
}));
|
|
139
|
+
const projectionValidation = validateBridgeProjection(specKitProjection);
|
|
140
|
+
if (!projectionValidation.valid) {
|
|
141
|
+
diagnostics.push(bridgeDiagnostic('BRIDGE_PROJECTION_INVALID', {
|
|
142
|
+
adapter: 'spec-kit', message: 'Spec Kit projection_id/schema does not match its canonical content',
|
|
143
|
+
}));
|
|
144
|
+
diagnostics.push(...blockers);
|
|
145
|
+
} else if (specKitProjection.ok !== true) {
|
|
146
|
+
diagnostics.push(...(blockers.length ? blockers : [bridgeDiagnostic('BRIDGE_PROJECTION_INVALID', {
|
|
147
|
+
adapter: 'spec-kit', message: 'Spec Kit projection must be explicitly green before dispatch',
|
|
148
|
+
})]));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (diagnostics.length) {
|
|
152
|
+
return { schema_version: 1, adapter: 'superpowers', active: true, ok: false, diagnostics };
|
|
153
|
+
}
|
|
154
|
+
const dispatch = {
|
|
155
|
+
schema_version: 1,
|
|
156
|
+
contract_kind: 'dispatch',
|
|
157
|
+
adapter: 'superpowers',
|
|
158
|
+
adapter_version: assessment.version,
|
|
159
|
+
active: true,
|
|
160
|
+
canonical_owner: 'wendkeep',
|
|
161
|
+
executor: 'superpowers',
|
|
162
|
+
authority: 'reported',
|
|
163
|
+
origin: { tool: 'superpowers', version: assessment.version },
|
|
164
|
+
compatibility: {
|
|
165
|
+
range: assessment.manifest.compatibility_range,
|
|
166
|
+
detected_version: assessment.version,
|
|
167
|
+
supported: true,
|
|
168
|
+
},
|
|
169
|
+
provenance: { state: 'reported', source: 'wendkeep-canonical-dispatch' },
|
|
170
|
+
task_contract: publicTaskContract(authoritativeTask),
|
|
171
|
+
...(handoffContract ? {
|
|
172
|
+
handoff_ref: {
|
|
173
|
+
handoff_id: String(handoffContract.handoff_id || ''),
|
|
174
|
+
task_contract_id: String(handoffContract.task_contract_id || ''),
|
|
175
|
+
head_sha: String(handoffContract.head_sha || ''),
|
|
176
|
+
},
|
|
177
|
+
} : {}),
|
|
178
|
+
spec_refs: (specKitProjection?.references || [])
|
|
179
|
+
.filter((item) => ['spec', 'constitution'].includes(item.kind))
|
|
180
|
+
.map((item) => ({ source_id: String(item.source_id), sha256: String(item.sha256) })),
|
|
181
|
+
worktree: {
|
|
182
|
+
provider: 'wendkeep',
|
|
183
|
+
mode: 'reuse-or-create',
|
|
184
|
+
create_argv: ['wendkeep', 'worktree', 'create', String(authoritativeTask.change_slug)],
|
|
185
|
+
finish_argv: ['wendkeep', 'worktree', 'finish', String(authoritativeTask.change_slug), '--pr', '<number-or-url>'],
|
|
186
|
+
},
|
|
187
|
+
diagnostics: [],
|
|
188
|
+
};
|
|
189
|
+
dispatch.dispatch_id = bridgeSha256(canonicalBridgeJson(dispatch));
|
|
190
|
+
const runtime = validateBridgeRuntimeEnvelope(dispatch);
|
|
191
|
+
if (!runtime.valid) {
|
|
192
|
+
return { schema_version: 1, adapter: 'superpowers', active: true, ok: false, diagnostics: runtime.diagnostics };
|
|
193
|
+
}
|
|
194
|
+
return { ...dispatch, ok: true };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function ingestSuperpowersArtifacts(items = []) {
|
|
198
|
+
return (Array.isArray(items) ? items : []).map((item, index) => {
|
|
199
|
+
const externalId = String(item?.external_id || '').trim();
|
|
200
|
+
const kind = String(item?.kind || 'artifact').trim();
|
|
201
|
+
if (!externalId || !['artifact', 'review', 'commit'].includes(kind)) {
|
|
202
|
+
throw Object.assign(new Error(`invalid Superpowers artifact at index ${index}`), { code: 'BRIDGE_CONTRACT_INVALID' });
|
|
203
|
+
}
|
|
204
|
+
const bytes = item.content === undefined ? String(item.sha256 || '') : String(item.content);
|
|
205
|
+
const sha256 = /^[a-f0-9]{64}$/.test(String(item.sha256 || '')) && item.content === undefined
|
|
206
|
+
? String(item.sha256)
|
|
207
|
+
: bridgeSha256(bytes);
|
|
208
|
+
const artifact = {
|
|
209
|
+
schema_version: 1,
|
|
210
|
+
contract_kind: 'external-artifact',
|
|
211
|
+
source: 'superpowers',
|
|
212
|
+
external_id: externalId,
|
|
213
|
+
kind,
|
|
214
|
+
sha256,
|
|
215
|
+
authority: 'reported',
|
|
216
|
+
origin: { tool: 'superpowers', external_id: externalId },
|
|
217
|
+
provenance: { state: 'reported', source: 'external-ingest' },
|
|
218
|
+
proof: null,
|
|
219
|
+
diagnostics: [],
|
|
220
|
+
};
|
|
221
|
+
const runtime = validateBridgeRuntimeEnvelope(artifact);
|
|
222
|
+
if (!runtime.valid) {
|
|
223
|
+
throw Object.assign(new Error(`invalid Superpowers artifact at index ${index}`), {
|
|
224
|
+
code: 'BRIDGE_CONTRACT_INVALID', diagnostics: runtime.diagnostics,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return artifact;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function verifyExternalArtifact(artifact, { proofs = [], canonicalProofs = [] } = {}) {
|
|
232
|
+
const canonical = (Array.isArray(canonicalProofs) ? canonicalProofs : [])
|
|
233
|
+
.map(readCanonicalArtifactProof)
|
|
234
|
+
.find((proof) => (
|
|
235
|
+
proof?.external_id === artifact?.external_id
|
|
236
|
+
&& proof?.artifact_sha256 === artifact?.sha256
|
|
237
|
+
&& proof?.authority === 'verified'
|
|
238
|
+
&& proof?.evidence_envelope_id
|
|
239
|
+
&& proof?.sensor_id
|
|
240
|
+
&& proof?.head_sha
|
|
241
|
+
&& proof?.proof_id
|
|
242
|
+
));
|
|
243
|
+
if (canonical) {
|
|
244
|
+
return {
|
|
245
|
+
...artifact,
|
|
246
|
+
authority: 'verified',
|
|
247
|
+
origin: { tool: 'wendkeep', evidence_envelope_id: canonical.evidence_envelope_id },
|
|
248
|
+
provenance: { state: 'verified', source: 'wendkeep-evidence-envelope' },
|
|
249
|
+
proof: { ...canonical },
|
|
250
|
+
diagnostics: [],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
const matching = (Array.isArray(proofs) ? proofs : []).find((proof) => (
|
|
254
|
+
['git', 'ci', 'evidence-envelope'].includes(String(proof?.type || ''))
|
|
255
|
+
&& proof?.state === 'verified'
|
|
256
|
+
&& String(proof?.artifact_sha256 || '') === String(artifact?.sha256 || '')
|
|
257
|
+
));
|
|
258
|
+
return {
|
|
259
|
+
...artifact,
|
|
260
|
+
authority: 'reported',
|
|
261
|
+
proof: null,
|
|
262
|
+
diagnostics: [bridgeDiagnostic(matching ? 'BRIDGE_PROOF_UNVERIFIED' : 'BRIDGE_PROOF_MISSING', {
|
|
263
|
+
adapter: 'superpowers',
|
|
264
|
+
message: matching
|
|
265
|
+
? 'self-declared proof cannot promote authority; use the WendKeep Evidence Envelope/provenance gate'
|
|
266
|
+
: `no independent proof binds ${artifact?.external_id || 'artifact'}`,
|
|
267
|
+
})],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
@@ -155,13 +155,16 @@ function changeResult(tool, args, vaultBase) {
|
|
|
155
155
|
|
|
156
156
|
async function observerQuery(args, ctx) {
|
|
157
157
|
const {
|
|
158
|
-
|
|
158
|
+
bootstrapObserverDatabase,
|
|
159
159
|
readSqlProjectOverview,
|
|
160
160
|
readUsageBreakdown,
|
|
161
161
|
readUsageCalls,
|
|
162
162
|
readUsageSummary,
|
|
163
163
|
searchSqlDocuments,
|
|
164
164
|
} = await import('../../../src/observer-sql-store.mjs');
|
|
165
|
+
const { authorizeObserverPrincipal, recordObserverAudit } = await import('../../observer/src/authz.mjs');
|
|
166
|
+
const { resolveObserverPrincipal } = await import('../../observer/src/token-registry.mjs');
|
|
167
|
+
const { observerEncryptionFromEnvironment } = await import('../../observer/src/encryption.mjs');
|
|
165
168
|
const kind = String(args.query || 'overview').trim();
|
|
166
169
|
const filters = args.payload?.filters && typeof args.payload.filters === 'object'
|
|
167
170
|
? args.payload.filters
|
|
@@ -169,7 +172,37 @@ async function observerQuery(args, ctx) {
|
|
|
169
172
|
const dataDir = resolve(process.env.WENDKEEP_OBSERVER_DATA_DIR || join(homedir(), '.wendkeep-observer'));
|
|
170
173
|
let db;
|
|
171
174
|
try {
|
|
172
|
-
|
|
175
|
+
const encryption = observerEncryptionFromEnvironment();
|
|
176
|
+
({ db } = bootstrapObserverDatabase(dataDir, { security: { encryption } }));
|
|
177
|
+
const capability = {
|
|
178
|
+
overview: 'project:read',
|
|
179
|
+
usage_summary: 'usage:summary:read',
|
|
180
|
+
usage_breakdown: 'usage:breakdown:read',
|
|
181
|
+
usage_calls: 'usage:calls:read',
|
|
182
|
+
memory_search: 'memory:content:read',
|
|
183
|
+
}[kind] || 'project:read';
|
|
184
|
+
const rawToken = String(process.env.WENDKEEP_OBSERVER_TOKEN || '');
|
|
185
|
+
const requireToken = ['usage:calls:read', 'memory:content:read'].includes(capability)
|
|
186
|
+
|| process.env.WENDKEEP_OBSERVER_REQUIRE_LOOPBACK_AUTH === '1';
|
|
187
|
+
if (rawToken || requireToken) {
|
|
188
|
+
const principal = resolveObserverPrincipal(db, rawToken);
|
|
189
|
+
const authorized = authorizeObserverPrincipal(principal, {
|
|
190
|
+
projectId: ctx.resolution.projectId,
|
|
191
|
+
capability,
|
|
192
|
+
});
|
|
193
|
+
if (!authorized.ok) {
|
|
194
|
+
throw Object.assign(new Error('Observer query requires a project-scoped token and capability'), {
|
|
195
|
+
code: authorized.code || 'MCP_OBSERVER_AUTH_REQUIRED',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (['usage:calls:read', 'memory:content:read'].includes(capability)) recordObserverAudit(db, {
|
|
199
|
+
projectId: ctx.resolution.projectId,
|
|
200
|
+
tokenId: principal.token_id,
|
|
201
|
+
capability,
|
|
202
|
+
outcome: 'allowed',
|
|
203
|
+
metadata: { route: `mcp:${kind}`, method: 'MCP' },
|
|
204
|
+
});
|
|
205
|
+
}
|
|
173
206
|
switch (kind) {
|
|
174
207
|
case 'overview': return sanitize(readSqlProjectOverview(db, ctx.resolution.projectId));
|
|
175
208
|
case 'usage_summary': return sanitize(readUsageSummary(db, ctx.resolution.projectId, filters));
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wendkeep/observer",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": "./src/index.mjs",
|
|
7
|
+
"./policy": "./src/policy.mjs",
|
|
8
|
+
"./redaction": "./src/redaction.mjs",
|
|
9
|
+
"./authz": "./src/authz.mjs",
|
|
10
|
+
"./token-registry": "./src/token-registry.mjs",
|
|
11
|
+
"./encryption": "./src/encryption.mjs",
|
|
12
|
+
"./retention": "./src/retention.mjs",
|
|
13
|
+
"./purge": "./src/purge.mjs",
|
|
14
|
+
"./audit": "./src/audit.mjs"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { recordObserverAudit } from './authz.mjs';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { sanitizeObserverAuditMetadata } from './redaction.mjs';
|
|
3
|
+
|
|
4
|
+
const ROLE_CAPABILITIES = {
|
|
5
|
+
viewer: new Set(['project:read', 'usage:summary:read', 'usage:breakdown:read', 'memory:metadata:read', 'sync:read']),
|
|
6
|
+
auditor: new Set(['project:read', 'usage:summary:read', 'usage:breakdown:read', 'usage:calls:read', 'transcript:read', 'memory:metadata:read', 'memory:content:read', 'audit:read', 'sync:read']),
|
|
7
|
+
publisher: new Set(['project:read', 'project:write', 'ingest:write', 'memory:write', 'snapshot:write', 'sync:write']),
|
|
8
|
+
admin: new Set(['*']),
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function scopeMatches(scopes, capability) {
|
|
12
|
+
return scopes.includes('*') || scopes.includes(capability)
|
|
13
|
+
|| scopes.some((scope) => scope.endsWith(':*') && capability.startsWith(scope.slice(0, -1)));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function authorizeObserverPrincipal(principal, { projectId, capability } = {}) {
|
|
17
|
+
if (!principal?.ok) return { ok: false, status: 401, code: principal?.code || 'observer_auth_required' };
|
|
18
|
+
if (!principal.project_ids?.includes('*') && !principal.project_ids?.includes(projectId)) {
|
|
19
|
+
return { ok: false, status: 403, code: 'observer_project_forbidden' };
|
|
20
|
+
}
|
|
21
|
+
const grants = ROLE_CAPABILITIES[principal.role] || new Set();
|
|
22
|
+
if (!grants.has('*') && !grants.has(capability)) return { ok: false, status: 403, code: 'observer_role_forbidden' };
|
|
23
|
+
if (!scopeMatches(principal.scopes || [], capability)) return { ok: false, status: 403, code: 'observer_scope_forbidden' };
|
|
24
|
+
return { ok: true, status: 200, code: 'observer_authorized' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function recordObserverAudit(db, {
|
|
28
|
+
auditId, projectId, tokenId = '', capability, outcome, occurredAt = new Date().toISOString(), metadata = {},
|
|
29
|
+
} = {}) {
|
|
30
|
+
const safeMetadata = sanitizeObserverAuditMetadata(metadata);
|
|
31
|
+
const id = String(auditId || createHash('sha256').update(JSON.stringify({ projectId, tokenId, capability, outcome, occurredAt, safeMetadata })).digest('hex').slice(0, 32));
|
|
32
|
+
const persistedTokenId = tokenId && db.prepare('SELECT token_id FROM observer_tokens WHERE token_id = ?').get(tokenId)
|
|
33
|
+
? tokenId
|
|
34
|
+
: null;
|
|
35
|
+
db.prepare(`INSERT OR IGNORE INTO observer_access_audit(audit_id, project_id, token_id, capability, outcome, occurred_at, metadata_json)
|
|
36
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(id, projectId, persistedTokenId, capability, outcome, new Date(occurredAt).toISOString(), JSON.stringify(safeMetadata));
|
|
37
|
+
return { audit_id: id, project_id: projectId, token_id: tokenId, capability, outcome, occurred_at: new Date(occurredAt).toISOString(), metadata: safeMetadata };
|
|
38
|
+
}
|