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
package/src/observer.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
2
4
|
import { isAbsolute, resolve } from 'node:path';
|
|
3
5
|
import { readObserverIndexSource } from './observer-store.mjs';
|
|
4
6
|
import { buildProjectSnapshot } from './observer-snapshot.mjs';
|
|
@@ -6,15 +8,23 @@ import { compareMemoryParity } from './observer-memory-publish.mjs';
|
|
|
6
8
|
import { publishObserverSql } from './observer-sql-publish.mjs';
|
|
7
9
|
import { migrateObserverData } from './observer-sql-migrate.mjs';
|
|
8
10
|
import { startObserverServer } from './observer-server.mjs';
|
|
9
|
-
import { ensureObserverDatabase,
|
|
11
|
+
import { ensureObserverDatabase, listSqlProjects, readSqlProject, registerSqlProject, upsertSqlProjectSnapshot, OBSERVER_SQL_FILE, OBSERVER_SQL_SCHEMA_VERSION } from './observer-sql-store.mjs';
|
|
10
12
|
import { resolveProjectVault } from '../packages/vault/src/project-vault.mjs';
|
|
11
13
|
import { observerAuthHeaders, resolveObserverToken } from './observer-auth.mjs';
|
|
14
|
+
import { recordObserverAudit } from '../packages/observer/src/authz.mjs';
|
|
15
|
+
import { readObserverPolicy, saveObserverPolicy } from '../packages/observer/src/policy.mjs';
|
|
16
|
+
import { purgeObserverData } from '../packages/observer/src/purge.mjs';
|
|
17
|
+
import { runObserverRetention } from '../packages/observer/src/retention.mjs';
|
|
18
|
+
import { registerObserverToken, revokeObserverToken, rotateObserverToken } from '../packages/observer/src/token-registry.mjs';
|
|
19
|
+
import { observerEncryptionFromEnvironment } from '../packages/observer/src/encryption.mjs';
|
|
12
20
|
|
|
13
21
|
export const OBSERVER_HELP = `wendkeep observer — Observer local multi-projeto
|
|
14
22
|
|
|
15
23
|
Uso:
|
|
16
24
|
wendkeep observer serve [--data-dir P] [--host 127.0.0.1] [--port 8787]
|
|
17
|
-
[--allow-non-loopback] [--token TOKEN]
|
|
25
|
+
[--allow-non-loopback] [--require-loopback-auth] [--require-encryption] [--token TOKEN]
|
|
26
|
+
[--bootstrap-token-id ID] [--bootstrap-role ROLE]
|
|
27
|
+
[--bootstrap-projects P1,P2] [--bootstrap-scopes S1,S2] --bootstrap-expires-at ISO
|
|
18
28
|
wendkeep observer register --project P [--vault V] [--data-dir D] [--json]
|
|
19
29
|
wendkeep observer publish --project P [--vault V] [--data-dir D] [--json]
|
|
20
30
|
wendkeep observer reconcile --project P [--vault V] [--data-dir D] [--url U]
|
|
@@ -22,6 +32,14 @@ Uso:
|
|
|
22
32
|
wendkeep observer memory import --project P [--vault V] [--url U] [--token TOKEN]
|
|
23
33
|
[--capture-level metadata|messages|full-transcript] [--json]
|
|
24
34
|
wendkeep observer status [--data-dir D] [--json]
|
|
35
|
+
wendkeep observer security token create --project-id P --role R --scopes S
|
|
36
|
+
--token-env ENV --expires-at ISO [--token-id ID] [--reason TEXT] [--json]
|
|
37
|
+
wendkeep observer security token rotate --project-id P --token-id ID --token-env ENV
|
|
38
|
+
--expires-at ISO [--new-token-id ID] [--reason TEXT] [--json]
|
|
39
|
+
wendkeep observer security token revoke --project-id P --token-id ID [--reason TEXT] [--json]
|
|
40
|
+
wendkeep observer security policy set --project-id P --file policy.json [--json]
|
|
41
|
+
wendkeep observer security purge --project-id P --before ISO --classes C [--dry-run] [--operation-id ID] [--json]
|
|
42
|
+
wendkeep observer security retention run --project-id P [--dry-run] [--operation-id ID] [--observed-at ISO] [--json]
|
|
25
43
|
|
|
26
44
|
O Observer local pode manter snapshots operacionais e uma cópia completa da memória em volume
|
|
27
45
|
Docker. O comando memory import faz a primeira migração de um vault para o container.
|
|
@@ -34,6 +52,10 @@ function optionValue(argv, name) {
|
|
|
34
52
|
return argv.find((item) => item.startsWith(`${name}=`))?.slice(name.length + 1) || '';
|
|
35
53
|
}
|
|
36
54
|
|
|
55
|
+
function csv(value) {
|
|
56
|
+
return String(value || '').split(',').map((item) => item.trim()).filter(Boolean);
|
|
57
|
+
}
|
|
58
|
+
|
|
37
59
|
function dataDir(argv) {
|
|
38
60
|
return resolve(optionValue(argv, '--data-dir')
|
|
39
61
|
|| process.env.WENDKEEP_OBSERVER_DATA_DIR
|
|
@@ -62,23 +84,22 @@ function summary(index) {
|
|
|
62
84
|
};
|
|
63
85
|
}
|
|
64
86
|
|
|
65
|
-
function databaseSummary(dir) {
|
|
66
|
-
const db = ensureObserverDatabase(dir);
|
|
87
|
+
function databaseSummary(dir, security) {
|
|
88
|
+
const db = ensureObserverDatabase(dir, { security });
|
|
67
89
|
try {
|
|
68
|
-
const migrations = migrateObserverDatabase(db);
|
|
69
90
|
return {
|
|
70
91
|
engine: 'sqlite',
|
|
71
92
|
file: OBSERVER_SQL_FILE,
|
|
72
93
|
schema_version: OBSERVER_SQL_SCHEMA_VERSION,
|
|
73
|
-
migrations:
|
|
94
|
+
migrations: Number(db.prepare('SELECT COUNT(*) AS count FROM schema_migrations').get().count),
|
|
74
95
|
projects: listSqlProjects(db).length,
|
|
75
96
|
ready: true,
|
|
76
97
|
};
|
|
77
98
|
} finally { db.close(); }
|
|
78
99
|
}
|
|
79
100
|
|
|
80
|
-
function sqlProjectsSummary(dir) {
|
|
81
|
-
const db = ensureObserverDatabase(dir);
|
|
101
|
+
function sqlProjectsSummary(dir, security) {
|
|
102
|
+
const db = ensureObserverDatabase(dir, { security });
|
|
82
103
|
try {
|
|
83
104
|
return listSqlProjects(db).map((project) => ({
|
|
84
105
|
projectId: project.project_id,
|
|
@@ -99,21 +120,133 @@ export async function runObserver(argv = [], { write = (chunk) => process.stdout
|
|
|
99
120
|
}
|
|
100
121
|
const dir = dataDir(argv);
|
|
101
122
|
const token = resolveObserverToken(optionValue(argv, '--token'));
|
|
123
|
+
const databaseSecurity = {
|
|
124
|
+
encryption: observerEncryptionFromEnvironment({
|
|
125
|
+
required: argv.includes('--require-encryption') || process.env.WENDKEEP_OBSERVER_REQUIRE_ENCRYPTION === '1',
|
|
126
|
+
}),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
if (sub === 'security') {
|
|
130
|
+
const domain = argv[1] || '';
|
|
131
|
+
const action = argv[2] || '';
|
|
132
|
+
const projectId = optionValue(argv, '--project-id');
|
|
133
|
+
if (!projectId) throw new Error('observer security: --project-id é obrigatório.');
|
|
134
|
+
const db = ensureObserverDatabase(dir, { security: databaseSecurity });
|
|
135
|
+
try {
|
|
136
|
+
readSqlProject(db, projectId);
|
|
137
|
+
if (domain === 'token' && action === 'create') {
|
|
138
|
+
const envName = optionValue(argv, '--token-env');
|
|
139
|
+
const rawToken = envName ? String(process.env[envName] || '') : '';
|
|
140
|
+
if (!envName || !rawToken) throw new Error('observer security token create: --token-env deve apontar para um segredo não vazio.');
|
|
141
|
+
const created = registerObserverToken(db, {
|
|
142
|
+
tokenId: optionValue(argv, '--token-id') || randomBytes(12).toString('hex'),
|
|
143
|
+
token: rawToken,
|
|
144
|
+
role: optionValue(argv, '--role'),
|
|
145
|
+
projectIds: [projectId],
|
|
146
|
+
scopes: optionValue(argv, '--scopes').split(',').map((item) => item.trim()).filter(Boolean),
|
|
147
|
+
expiresAt: optionValue(argv, '--expires-at'),
|
|
148
|
+
});
|
|
149
|
+
recordObserverAudit(db, {
|
|
150
|
+
projectId, tokenId: created.token_id, capability: 'security:recovery', outcome: 'created',
|
|
151
|
+
metadata: { route: 'offline-cli', method: 'LOCAL', reason: optionValue(argv, '--reason') || 'token create' },
|
|
152
|
+
});
|
|
153
|
+
print(created, asJson, write);
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
if (domain === 'token' && action === 'revoke') {
|
|
157
|
+
const tokenId = optionValue(argv, '--token-id');
|
|
158
|
+
const revoked = revokeObserverToken(db, { tokenId });
|
|
159
|
+
recordObserverAudit(db, {
|
|
160
|
+
projectId, tokenId, capability: 'security:recovery', outcome: revoked.revoked ? 'revoked' : 'not-found',
|
|
161
|
+
metadata: { route: 'offline-cli', method: 'LOCAL', reason: optionValue(argv, '--reason') || 'token revoke' },
|
|
162
|
+
});
|
|
163
|
+
print(revoked, asJson, write);
|
|
164
|
+
return revoked.revoked ? 0 : 1;
|
|
165
|
+
}
|
|
166
|
+
if (domain === 'token' && action === 'rotate') {
|
|
167
|
+
const envName = optionValue(argv, '--token-env');
|
|
168
|
+
const rawToken = envName ? String(process.env[envName] || '') : '';
|
|
169
|
+
if (!envName || !rawToken) throw new Error('observer security token rotate: --token-env deve apontar para um segredo não vazio.');
|
|
170
|
+
const rotated = rotateObserverToken(db, {
|
|
171
|
+
tokenId: optionValue(argv, '--token-id'),
|
|
172
|
+
newTokenId: optionValue(argv, '--new-token-id') || randomBytes(12).toString('hex'),
|
|
173
|
+
newToken: rawToken,
|
|
174
|
+
expiresAt: optionValue(argv, '--expires-at'),
|
|
175
|
+
});
|
|
176
|
+
recordObserverAudit(db, {
|
|
177
|
+
projectId, tokenId: rotated.token_id, capability: 'security:recovery', outcome: 'rotated',
|
|
178
|
+
metadata: { route: 'offline-cli', method: 'LOCAL', reason: optionValue(argv, '--reason') || 'token rotate' },
|
|
179
|
+
});
|
|
180
|
+
print(rotated, asJson, write);
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
if (domain === 'policy' && action === 'set') {
|
|
184
|
+
const path = optionValue(argv, '--file');
|
|
185
|
+
if (!path) throw new Error('observer security policy set: --file é obrigatório.');
|
|
186
|
+
const policy = saveObserverPolicy(db, projectId, JSON.parse(readFileSync(resolve(path), 'utf8')));
|
|
187
|
+
print({ schema_version: 1, project_id: projectId, policy }, asJson, write);
|
|
188
|
+
return 0;
|
|
189
|
+
}
|
|
190
|
+
if (domain === 'policy' && action === 'show') {
|
|
191
|
+
print({ schema_version: 1, project_id: projectId, policy: readObserverPolicy(db, projectId) }, asJson, write);
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
if (domain === 'purge') {
|
|
195
|
+
const result = purgeObserverData(db, {
|
|
196
|
+
projectId,
|
|
197
|
+
before: optionValue(argv, '--before'),
|
|
198
|
+
classes: optionValue(argv, '--classes').split(',').map((item) => item.trim()).filter(Boolean),
|
|
199
|
+
operationId: optionValue(argv, '--operation-id'),
|
|
200
|
+
dryRun: argv.includes('--dry-run'),
|
|
201
|
+
});
|
|
202
|
+
print(result, asJson, write);
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
if (domain === 'retention' && action === 'run') {
|
|
206
|
+
const policy = readObserverPolicy(db, projectId);
|
|
207
|
+
const observedAt = optionValue(argv, '--observed-at');
|
|
208
|
+
const result = runObserverRetention(db, {
|
|
209
|
+
projectId,
|
|
210
|
+
policy: policy.retention,
|
|
211
|
+
clock: () => observedAt ? new Date(observedAt) : new Date(),
|
|
212
|
+
operationId: optionValue(argv, '--operation-id'),
|
|
213
|
+
dryRun: argv.includes('--dry-run'),
|
|
214
|
+
});
|
|
215
|
+
print(result, asJson, write);
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
throw new Error(`observer security: operação desconhecida: ${domain} ${action}`.trim());
|
|
219
|
+
} finally { db.close(); }
|
|
220
|
+
}
|
|
102
221
|
|
|
103
222
|
if (sub === 'status') {
|
|
104
223
|
const legacy = summary(readObserverIndexSource(dir));
|
|
105
|
-
print({ ...legacy, projects: sqlProjectsSummary(dir), legacy_projects: legacy.projects, database: databaseSummary(dir) }, asJson, write);
|
|
224
|
+
print({ ...legacy, projects: sqlProjectsSummary(dir, databaseSecurity), legacy_projects: legacy.projects, database: databaseSummary(dir, databaseSecurity) }, asJson, write);
|
|
106
225
|
return 0;
|
|
107
226
|
}
|
|
108
227
|
|
|
109
228
|
if (sub === 'serve') {
|
|
110
229
|
const host = optionValue(argv, '--host') || '127.0.0.1';
|
|
230
|
+
const encryption = databaseSecurity.encryption;
|
|
231
|
+
const secureMode = argv.includes('--require-loopback-auth') || Boolean(encryption);
|
|
111
232
|
const server = await startObserverServer({
|
|
112
233
|
dataDir: dir,
|
|
113
234
|
host,
|
|
114
235
|
port: Number(optionValue(argv, '--port') || 8787),
|
|
115
236
|
allowNonLoopback: argv.includes('--allow-non-loopback'),
|
|
116
237
|
token,
|
|
238
|
+
bootstrap: {
|
|
239
|
+
tokenId: optionValue(argv, '--bootstrap-token-id') || process.env.WENDKEEP_OBSERVER_BOOTSTRAP_TOKEN_ID || '',
|
|
240
|
+
role: optionValue(argv, '--bootstrap-role') || process.env.WENDKEEP_OBSERVER_BOOTSTRAP_ROLE || 'admin',
|
|
241
|
+
projectIds: csv(optionValue(argv, '--bootstrap-projects') || process.env.WENDKEEP_OBSERVER_BOOTSTRAP_PROJECTS),
|
|
242
|
+
scopes: csv(optionValue(argv, '--bootstrap-scopes') || process.env.WENDKEEP_OBSERVER_BOOTSTRAP_SCOPES || '*'),
|
|
243
|
+
expiresAt: optionValue(argv, '--bootstrap-expires-at') || process.env.WENDKEEP_OBSERVER_BOOTSTRAP_EXPIRES_AT || '',
|
|
244
|
+
},
|
|
245
|
+
security: {
|
|
246
|
+
enabled: secureMode,
|
|
247
|
+
requireLoopbackAuth: argv.includes('--require-loopback-auth'),
|
|
248
|
+
encryption,
|
|
249
|
+
},
|
|
117
250
|
});
|
|
118
251
|
const address = server.address();
|
|
119
252
|
process.stdout.write(`wendkeep observer listening: http://${address.address}:${address.port}\n`);
|
|
@@ -179,7 +312,7 @@ export async function runObserver(argv = [], { write = (chunk) => process.stdout
|
|
|
179
312
|
const snapshot = buildProjectSnapshot({ vaultBase: vault, projectRoot: root });
|
|
180
313
|
const url = optionValue(argv, '--url') || process.env.WENDKEEP_OBSERVER_URL || '';
|
|
181
314
|
if (!url) {
|
|
182
|
-
const db = ensureObserverDatabase(dir);
|
|
315
|
+
const db = ensureObserverDatabase(dir, { security: databaseSecurity });
|
|
183
316
|
let migration;
|
|
184
317
|
try {
|
|
185
318
|
migration = migrateObserverData({
|
|
@@ -231,7 +364,7 @@ export async function runObserver(argv = [], { write = (chunk) => process.stdout
|
|
|
231
364
|
const snapshot = buildProjectSnapshot({ vaultBase: vault, projectRoot: root });
|
|
232
365
|
|
|
233
366
|
if (sub === 'register') {
|
|
234
|
-
const db = ensureObserverDatabase(dir);
|
|
367
|
+
const db = ensureObserverDatabase(dir, { security: databaseSecurity });
|
|
235
368
|
let sql;
|
|
236
369
|
try {
|
|
237
370
|
sql = registerSqlProject(db, {
|
|
@@ -245,7 +378,7 @@ export async function runObserver(argv = [], { write = (chunk) => process.stdout
|
|
|
245
378
|
return 0;
|
|
246
379
|
}
|
|
247
380
|
|
|
248
|
-
const db = ensureObserverDatabase(dir);
|
|
381
|
+
const db = ensureObserverDatabase(dir, { security: databaseSecurity });
|
|
249
382
|
let migration;
|
|
250
383
|
try {
|
|
251
384
|
migration = migrateObserverData({
|
package/src/sync-protocol.mjs
CHANGED
|
@@ -73,9 +73,25 @@ function validatePrivatePayload(payload) {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
function normalizePolicyRef(value) {
|
|
77
|
+
if (value == null) return null;
|
|
78
|
+
const keys = Object.keys(value).sort();
|
|
79
|
+
if (keys.join(',') !== 'hash,policy_id,version'
|
|
80
|
+
|| !HASH_PATTERN.test(String(value.hash || ''))
|
|
81
|
+
|| !Number.isSafeInteger(Number(value.version)) || Number(value.version) < 1) {
|
|
82
|
+
throw syncError('WENDKEEP_SYNC_POLICY_REF_INVALID', 'policy_ref must contain only policy_id, version and sha256 hash');
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
policy_id: requiredText(value.policy_id, 'policy_ref.policy_id', 160),
|
|
86
|
+
version: Number(value.version),
|
|
87
|
+
hash: String(value.hash),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
76
91
|
export function createSyncEvent({
|
|
77
92
|
projectId, recordKey, revision, baseRevision, payload = null, causalParentIds = [],
|
|
78
93
|
actorId, deviceId, leaseId = '', observedAt, operation = 'put', privacy = 'shared',
|
|
94
|
+
policyRef = null,
|
|
79
95
|
} = {}) {
|
|
80
96
|
const project_id = requiredText(projectId, 'project_id', 160);
|
|
81
97
|
const record_key = requiredText(recordKey, 'record_key', 2048);
|
|
@@ -108,6 +124,7 @@ export function createSyncEvent({
|
|
|
108
124
|
observed_at: timestamp.toISOString(),
|
|
109
125
|
operation,
|
|
110
126
|
privacy,
|
|
127
|
+
...(policyRef ? { policy_ref: normalizePolicyRef(policyRef) } : {}),
|
|
111
128
|
payload: operation === 'tombstone' ? null : structuredClone(payload),
|
|
112
129
|
};
|
|
113
130
|
return { ...draft, event_id: syncSha256(draft).slice(7) };
|
|
@@ -137,6 +154,7 @@ export function validateSyncEvent(event, { projectId = '' } = {}) {
|
|
|
137
154
|
observedAt: event.observed_at,
|
|
138
155
|
operation: event.operation,
|
|
139
156
|
privacy: event.privacy,
|
|
157
|
+
policyRef: event.policy_ref || null,
|
|
140
158
|
});
|
|
141
159
|
return event;
|
|
142
160
|
}
|
|
@@ -153,6 +171,7 @@ function eventCandidate(event) {
|
|
|
153
171
|
observed_at: event.observed_at,
|
|
154
172
|
operation: event.operation,
|
|
155
173
|
privacy: event.privacy,
|
|
174
|
+
...(event.policy_ref ? { policy_ref: structuredClone(event.policy_ref) } : {}),
|
|
156
175
|
payload: structuredClone(event.payload),
|
|
157
176
|
};
|
|
158
177
|
}
|
|
@@ -168,6 +187,7 @@ function recordFromEvent(event, { conflicted = false } = {}) {
|
|
|
168
187
|
observed_at: event.observed_at,
|
|
169
188
|
operation: event.operation,
|
|
170
189
|
privacy: event.privacy,
|
|
190
|
+
...(event.policy_ref ? { policy_ref: structuredClone(event.policy_ref) } : {}),
|
|
171
191
|
payload: structuredClone(event.payload),
|
|
172
192
|
tombstone: event.operation === 'tombstone',
|
|
173
193
|
conflicted,
|
package/src/task-contracts.mjs
CHANGED
|
@@ -43,6 +43,25 @@ function uniqueStrings(values) {
|
|
|
43
43
|
return [...new Set(list.map((value) => String(value).trim()).filter(Boolean))];
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
export function normalizeExternalArtifactEvidence(input = {}) {
|
|
47
|
+
const source = String(input.source || '').trim();
|
|
48
|
+
const externalId = String(input.external_id || '').trim();
|
|
49
|
+
const kind = String(input.kind || '').trim();
|
|
50
|
+
const sha256Value = String(input.sha256 || '').trim();
|
|
51
|
+
if (!source || !externalId || !['artifact', 'review', 'commit'].includes(kind)
|
|
52
|
+
|| !/^[a-f0-9]{64}$/.test(sha256Value)) {
|
|
53
|
+
throw Object.assign(new Error('external artifact evidence is incomplete'), { code: 'TASK_EXTERNAL_ARTIFACT_INVALID' });
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
schema_version: 1,
|
|
57
|
+
source,
|
|
58
|
+
external_id: externalId,
|
|
59
|
+
kind,
|
|
60
|
+
sha256: sha256Value,
|
|
61
|
+
authority: 'reported',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
46
65
|
function bindingFrom(input) {
|
|
47
66
|
return {
|
|
48
67
|
project_id: String(input.projectId || ''),
|
package/src/task.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as bridgeFs from 'node:fs';
|
|
1
2
|
import { isAbsolute, resolve } from 'node:path';
|
|
2
3
|
import { activeChange } from '../hooks/change-core.mjs';
|
|
3
4
|
import { resolveActiveContext } from '../hooks/active-context-store.mjs';
|
|
@@ -6,6 +7,19 @@ import { buildTaskContractSnapshot, evaluateTaskContracts } from './task-contrac
|
|
|
6
7
|
import { claimTaskLease, releaseTaskLease } from './task-leases.mjs';
|
|
7
8
|
import { findProjectRoot } from '../packages/harness/src/sensors-core.mjs';
|
|
8
9
|
import { resolveHookOperatingProfile } from '../hooks/operating-profile-runtime.mjs';
|
|
10
|
+
import { buildSuperpowersDispatch } from '../packages/integrations/src/superpowers-adapter.mjs';
|
|
11
|
+
import { importSpecKitProjection } from '../packages/integrations/src/spec-kit-adapter.mjs';
|
|
12
|
+
import { validateBridgeProjection } from '../packages/integrations/src/bridge-contract.mjs';
|
|
13
|
+
import { issueCanonicalDispatchAuthority } from '../packages/integrations/src/canonical-bridge-authority.mjs';
|
|
14
|
+
|
|
15
|
+
export function buildExternalTaskDispatch({ adapter = '', taskContract, ...options } = {}) {
|
|
16
|
+
if (String(adapter) !== 'superpowers') {
|
|
17
|
+
throw Object.assign(new Error(`unsupported task adapter: ${adapter || '(missing)'}`), {
|
|
18
|
+
code: 'TASK_ADAPTER_UNSUPPORTED',
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return buildSuperpowersDispatch({ taskContract, ...options });
|
|
22
|
+
}
|
|
9
23
|
|
|
10
24
|
const HELP = `wendkeep task <list|show|evaluate|claim|release> [task-id]
|
|
11
25
|
|
|
@@ -56,6 +70,74 @@ function commandState(argv) {
|
|
|
56
70
|
};
|
|
57
71
|
}
|
|
58
72
|
|
|
73
|
+
function resolveCanonicalTaskAuthority(argv = [], taskIdOverride = '') {
|
|
74
|
+
const state = commandState(argv);
|
|
75
|
+
const snapshot = buildTaskContractSnapshot(state);
|
|
76
|
+
const taskId = String(taskIdOverride || opt(argv, '--task-id') || '').trim();
|
|
77
|
+
const contract = snapshot.contracts.find((item) => item.task_id === taskId);
|
|
78
|
+
if (!contract) {
|
|
79
|
+
throw Object.assign(new Error(`task not found: ${taskId || '(missing id)'}`), { code: 'TASK_NOT_FOUND' });
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
authority: 'wendkeep-canonical',
|
|
83
|
+
task_contract: structuredClone(contract),
|
|
84
|
+
active_context: structuredClone(snapshot.binding),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function buildCanonicalExternalTaskDispatch({
|
|
89
|
+
adapter = '', authorityArgv = [], taskId = '', submittedTaskContract = null,
|
|
90
|
+
projectRoot = '', config, specKitProjection = null, baselineProjection = null, ...options
|
|
91
|
+
} = {}) {
|
|
92
|
+
if (adapter !== 'superpowers') return buildExternalTaskDispatch({ adapter, taskContract: null, ...options });
|
|
93
|
+
if (specKitProjection) {
|
|
94
|
+
const supplied = validateBridgeProjection(specKitProjection);
|
|
95
|
+
if (!supplied.valid) {
|
|
96
|
+
return {
|
|
97
|
+
schema_version: 1, adapter: 'superpowers', active: true, ok: false,
|
|
98
|
+
diagnostics: [{
|
|
99
|
+
schema_version: 1, code: 'BRIDGE_PROJECTION_INVALID', adapter: 'spec-kit', blocking: true,
|
|
100
|
+
message: 'submitted Spec Kit projection is not sealed by its complete decision state',
|
|
101
|
+
}],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (config?.adapters?.['spec-kit']?.enabled) {
|
|
106
|
+
if (!specKitProjection || !baselineProjection) {
|
|
107
|
+
return {
|
|
108
|
+
schema_version: 1, adapter: 'superpowers', active: true, ok: false,
|
|
109
|
+
diagnostics: [{
|
|
110
|
+
schema_version: 1, code: 'BRIDGE_BASELINE_MISSING', adapter: 'spec-kit', blocking: true,
|
|
111
|
+
message: 'active Spec Kit dispatch requires its canonical baseline and submitted projection',
|
|
112
|
+
}],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const baseline = validateBridgeProjection(baselineProjection);
|
|
116
|
+
if (!baseline.valid || baselineProjection.projection_id !== specKitProjection.projection_id) {
|
|
117
|
+
return {
|
|
118
|
+
schema_version: 1, adapter: 'superpowers', active: true, ok: false,
|
|
119
|
+
diagnostics: [{
|
|
120
|
+
schema_version: 1, code: 'BRIDGE_BASELINE_STALE', adapter: 'spec-kit', blocking: true,
|
|
121
|
+
message: 'submitted projection does not match the canonical Spec Kit baseline',
|
|
122
|
+
}],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const canonical = resolveCanonicalTaskAuthority(authorityArgv, taskId);
|
|
127
|
+
const canonicalReceipt = issueCanonicalDispatchAuthority(canonical);
|
|
128
|
+
const liveProjection = specKitProjection
|
|
129
|
+
? importSpecKitProjection({ projectRoot, config, previousProjection: baselineProjection || specKitProjection, fs: bridgeFs })
|
|
130
|
+
: null;
|
|
131
|
+
return buildExternalTaskDispatch({
|
|
132
|
+
adapter,
|
|
133
|
+
taskContract: submittedTaskContract || canonical.task_contract,
|
|
134
|
+
canonicalAuthority: canonicalReceipt,
|
|
135
|
+
specKitProjection: liveProjection,
|
|
136
|
+
config,
|
|
137
|
+
...options,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
59
141
|
function write(value, json, line = '') {
|
|
60
142
|
if (json) process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
61
143
|
else process.stdout.write(`${line || String(value)}\n`);
|
package/src/verify.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import { resolveCommandActiveContext } from './active-context-runtime.mjs';
|
|
|
25
25
|
import { resolveHookOperatingProfile } from '../hooks/operating-profile-runtime.mjs';
|
|
26
26
|
import { evaluateTddAttestation } from './tdd-attestation.mjs';
|
|
27
27
|
import { readTddAttestationStore } from './tdd-attestation-store.mjs';
|
|
28
|
+
import { collectBridgeArtifactEvidence } from './ecosystem-bridge-artifact-collector.mjs';
|
|
28
29
|
import { writeVaultFileAtomic } from '../packages/vault/src/vault-path-safety.mjs';
|
|
29
30
|
import { evaluateHostCoverage } from '../packages/integrations/src/capabilities.mjs';
|
|
30
31
|
import { evidenceCheckoutBinding } from '../packages/vault/src/evidence-envelope.mjs';
|
|
@@ -151,6 +152,13 @@ export function runVerify(argv) {
|
|
|
151
152
|
process.stderr.write(`wendkeep verify: ${error.code || 'WENDKEEP_EVIDENCE_BINDING_FAILED'}: ${error.message}\n`);
|
|
152
153
|
process.exit(2);
|
|
153
154
|
}
|
|
155
|
+
let externalArtifacts;
|
|
156
|
+
try {
|
|
157
|
+
externalArtifacts = collectBridgeArtifactEvidence({ projectRoot, tasks, sensors: evidence });
|
|
158
|
+
} catch (error) {
|
|
159
|
+
process.stderr.write(`wendkeep verify: ${error.code || 'BRIDGE_ARTIFACT_COLLECTION_FAILED'}: ${error.message}\n`);
|
|
160
|
+
process.exit(2);
|
|
161
|
+
}
|
|
154
162
|
const envelope = buildEvidenceEnvelope({
|
|
155
163
|
identity,
|
|
156
164
|
changeSlug: slug,
|
|
@@ -159,6 +167,7 @@ export function runVerify(argv) {
|
|
|
159
167
|
effectiveSpecSha256: `sha256:${effective.hash}`,
|
|
160
168
|
sensorConfigSha256: sensorConfigSha256(sensors, ids),
|
|
161
169
|
sensors: evidence,
|
|
170
|
+
externalArtifacts,
|
|
162
171
|
tddAttestations,
|
|
163
172
|
startedAt,
|
|
164
173
|
finishedAt: new Date().toISOString(),
|