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,122 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const ROLES = new Set(['viewer', 'auditor', 'publisher', 'admin']);
|
|
4
|
+
|
|
5
|
+
export function hashObserverToken(token) {
|
|
6
|
+
const value = String(token || '');
|
|
7
|
+
if (!value) throw Object.assign(new Error('token vazio.'), { code: 'observer_token_invalid' });
|
|
8
|
+
return `sha256:${createHash('sha256').update(value, 'utf8').digest('hex')}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function json(value) { return JSON.stringify(value); }
|
|
12
|
+
function parse(value) { try { return JSON.parse(value || '[]'); } catch { return []; } }
|
|
13
|
+
function validTime(value, field) {
|
|
14
|
+
const parsed = new Date(value);
|
|
15
|
+
if (Number.isNaN(parsed.getTime())) throw Object.assign(new Error(`${field} inválido.`), { code: 'observer_token_invalid' });
|
|
16
|
+
return parsed.toISOString();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function registerObserverToken(db, {
|
|
20
|
+
tokenId = randomBytes(12).toString('hex'), token, role, projectIds = [], scopes = [],
|
|
21
|
+
createdAt = new Date().toISOString(), expiresAt, rotatedFrom = null,
|
|
22
|
+
} = {}) {
|
|
23
|
+
if (!ROLES.has(role)) throw Object.assign(new Error('role inválida.'), { code: 'observer_role_invalid' });
|
|
24
|
+
if (!Array.isArray(projectIds) || projectIds.length === 0) throw Object.assign(new Error('projectIds é obrigatório.'), { code: 'observer_projects_invalid' });
|
|
25
|
+
if (!Array.isArray(scopes) || scopes.length === 0) throw Object.assign(new Error('scopes é obrigatório.'), { code: 'observer_scopes_invalid' });
|
|
26
|
+
const row = {
|
|
27
|
+
token_id: String(tokenId), token_hash: hashObserverToken(token), role,
|
|
28
|
+
project_ids_json: json([...new Set(projectIds.map(String))].sort()),
|
|
29
|
+
scopes_json: json([...new Set(scopes.map(String))].sort()),
|
|
30
|
+
created_at: validTime(createdAt, 'createdAt'), expires_at: validTime(expiresAt, 'expiresAt'),
|
|
31
|
+
revoked_at: null, rotated_from: rotatedFrom ? String(rotatedFrom) : null,
|
|
32
|
+
};
|
|
33
|
+
db.prepare(`INSERT INTO observer_tokens(token_id, token_hash, role, project_ids_json, scopes_json, created_at, expires_at, revoked_at, rotated_from)
|
|
34
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(...Object.values(row));
|
|
35
|
+
return { token_id: row.token_id, role, project_ids: parse(row.project_ids_json), scopes: parse(row.scopes_json), expires_at: row.expires_at, rotated_from: row.rotated_from };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function resolveObserverPrincipal(db, token, { now = new Date().toISOString() } = {}) {
|
|
39
|
+
let tokenHash;
|
|
40
|
+
try { tokenHash = hashObserverToken(token); } catch { return { ok: false, code: 'observer_token_missing' }; }
|
|
41
|
+
const row = db.prepare('SELECT * FROM observer_tokens WHERE token_hash = ?').get(tokenHash);
|
|
42
|
+
if (!row) return { ok: false, code: 'observer_token_invalid' };
|
|
43
|
+
const timestamp = new Date(now).getTime();
|
|
44
|
+
if (row.revoked_at && new Date(row.revoked_at).getTime() <= timestamp) return { ok: false, code: 'observer_token_revoked', token_id: row.token_id };
|
|
45
|
+
if (new Date(row.expires_at).getTime() <= timestamp) return { ok: false, code: 'observer_token_expired', token_id: row.token_id };
|
|
46
|
+
return {
|
|
47
|
+
ok: true, token_id: row.token_id, role: row.role,
|
|
48
|
+
project_ids: parse(row.project_ids_json), scopes: parse(row.scopes_json), expires_at: row.expires_at,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function ensureObserverBootstrapToken(db, {
|
|
53
|
+
token, tokenId = '', role, projectIds = [], scopes = [], expiresAt, createdAt = new Date().toISOString(), now = createdAt,
|
|
54
|
+
} = {}) {
|
|
55
|
+
const tokenHash = hashObserverToken(token);
|
|
56
|
+
const existing = db.prepare('SELECT * FROM observer_tokens WHERE token_hash = ?').get(tokenHash);
|
|
57
|
+
if (existing) {
|
|
58
|
+
const expired = new Date(existing.expires_at).getTime() <= new Date(validTime(now, 'now')).getTime();
|
|
59
|
+
return {
|
|
60
|
+
created: false,
|
|
61
|
+
token_id: existing.token_id,
|
|
62
|
+
role: existing.role,
|
|
63
|
+
project_ids: parse(existing.project_ids_json),
|
|
64
|
+
scopes: parse(existing.scopes_json),
|
|
65
|
+
expires_at: existing.expires_at,
|
|
66
|
+
revoked: Boolean(existing.revoked_at),
|
|
67
|
+
expired,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (!Array.isArray(projectIds) || projectIds.length === 0 || projectIds.includes('*')) {
|
|
71
|
+
throw Object.assign(new Error('bootstrap exige projectIds explícitos e não aceita wildcard.'), { code: 'observer_bootstrap_projects_invalid' });
|
|
72
|
+
}
|
|
73
|
+
const normalizedExpiry = validTime(expiresAt, 'expiresAt');
|
|
74
|
+
if (new Date(normalizedExpiry).getTime() <= new Date(validTime(now, 'now')).getTime()) {
|
|
75
|
+
throw Object.assign(new Error('bootstrap exige expiração futura.'), { code: 'observer_bootstrap_expired' });
|
|
76
|
+
}
|
|
77
|
+
const id = String(tokenId || `bootstrap-${tokenHash.slice(-24)}`);
|
|
78
|
+
if (db.prepare('SELECT token_id FROM observer_tokens WHERE token_id = ?').get(id)) {
|
|
79
|
+
throw Object.assign(new Error('tokenId de bootstrap já pertence a outra credencial.'), { code: 'observer_bootstrap_token_id_conflict' });
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
created: true,
|
|
83
|
+
...registerObserverToken(db, {
|
|
84
|
+
tokenId: id,
|
|
85
|
+
token,
|
|
86
|
+
role,
|
|
87
|
+
projectIds,
|
|
88
|
+
scopes,
|
|
89
|
+
createdAt,
|
|
90
|
+
expiresAt: normalizedExpiry,
|
|
91
|
+
}),
|
|
92
|
+
revoked: false,
|
|
93
|
+
expired: false,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function revokeObserverToken(db, { tokenId, revokedAt = new Date().toISOString() } = {}) {
|
|
98
|
+
const result = db.prepare('UPDATE observer_tokens SET revoked_at = ? WHERE token_id = ? AND revoked_at IS NULL')
|
|
99
|
+
.run(validTime(revokedAt, 'revokedAt'), String(tokenId || ''));
|
|
100
|
+
return { token_id: String(tokenId || ''), revoked: Number(result.changes) > 0 };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function rotateObserverToken(db, {
|
|
104
|
+
tokenId, newTokenId, newToken, rotatedAt = new Date().toISOString(), expiresAt,
|
|
105
|
+
} = {}) {
|
|
106
|
+
const current = db.prepare('SELECT * FROM observer_tokens WHERE token_id = ?').get(String(tokenId || ''));
|
|
107
|
+
if (!current || current.revoked_at) throw Object.assign(new Error('token não pode ser rotacionado.'), { code: 'observer_token_rotation_invalid' });
|
|
108
|
+
db.exec('BEGIN IMMEDIATE');
|
|
109
|
+
try {
|
|
110
|
+
revokeObserverToken(db, { tokenId, revokedAt: rotatedAt });
|
|
111
|
+
const created = registerObserverToken(db, {
|
|
112
|
+
tokenId: newTokenId, token: newToken, role: current.role,
|
|
113
|
+
projectIds: parse(current.project_ids_json), scopes: parse(current.scopes_json),
|
|
114
|
+
createdAt: rotatedAt, expiresAt, rotatedFrom: tokenId,
|
|
115
|
+
});
|
|
116
|
+
db.exec('COMMIT');
|
|
117
|
+
return created;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
db.exec('ROLLBACK');
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://wendkeep.dev/schema/ecosystem-bridge-artifact-manifest-v1.schema.json",
|
|
4
|
+
"title": "WendKeep ecosystem bridge artifact manifest v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schema_version", "artifacts"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schema_version": { "const": 1 },
|
|
10
|
+
"artifacts": {
|
|
11
|
+
"type": "array",
|
|
12
|
+
"items": { "$ref": "#/$defs/artifact" }
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"$defs": {
|
|
16
|
+
"artifact": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"additionalProperties": false,
|
|
19
|
+
"required": ["source", "external_id", "kind", "path", "sensor_id", "task_id"],
|
|
20
|
+
"properties": {
|
|
21
|
+
"source": { "const": "superpowers" },
|
|
22
|
+
"external_id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$" },
|
|
23
|
+
"kind": { "enum": ["artifact", "review", "commit"] },
|
|
24
|
+
"path": { "type": "string", "minLength": 1 },
|
|
25
|
+
"sensor_id": { "type": "string", "minLength": 1 },
|
|
26
|
+
"task_id": { "type": "string", "minLength": 1 }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://wendkeep.dev/schema/ecosystem-bridge-v1.schema.json",
|
|
4
|
+
"title": "WendKeep ecosystem bridge contracts v1",
|
|
5
|
+
"oneOf": [
|
|
6
|
+
{ "$ref": "#/$defs/specProjection" }, { "$ref": "#/$defs/dispatch" },
|
|
7
|
+
{ "$ref": "#/$defs/handoff" }, { "$ref": "#/$defs/externalArtifact" },
|
|
8
|
+
{ "$ref": "#/$defs/proof" }, { "$ref": "#/$defs/statusProjection" }
|
|
9
|
+
],
|
|
10
|
+
"$defs": {
|
|
11
|
+
"sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
12
|
+
"envelopeId": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
|
|
13
|
+
"origin": {
|
|
14
|
+
"type": "object", "required": ["tool"],
|
|
15
|
+
"properties": { "tool": { "type": "string", "minLength": 1 }, "version": { "type": "string" }, "root": { "type": "string" }, "external_id": { "type": "string" }, "source_projection_id": { "$ref": "#/$defs/sha256" }, "evidence_envelope_id": { "$ref": "#/$defs/envelopeId" } }
|
|
16
|
+
},
|
|
17
|
+
"compatibility": {
|
|
18
|
+
"type": "object", "additionalProperties": false,
|
|
19
|
+
"required": ["range", "detected_version", "supported"],
|
|
20
|
+
"properties": { "range": { "type": "string", "minLength": 1 }, "detected_version": { "type": "string", "minLength": 1 }, "supported": { "type": "boolean" } }
|
|
21
|
+
},
|
|
22
|
+
"provenance": {
|
|
23
|
+
"type": "object", "additionalProperties": false, "required": ["state", "source"],
|
|
24
|
+
"properties": { "state": { "enum": ["reported", "verified"] }, "source": { "type": "string", "minLength": 1 } }
|
|
25
|
+
},
|
|
26
|
+
"mapping": {
|
|
27
|
+
"type": "object", "additionalProperties": false,
|
|
28
|
+
"required": ["source_id", "source_kind", "capability", "change_slug", "task_ids"],
|
|
29
|
+
"properties": { "source_id": { "type": "string", "minLength": 1 }, "source_kind": { "enum": ["story", "requirement"] }, "capability": { "type": "string", "minLength": 1 }, "change_slug": { "type": "string", "minLength": 1 }, "task_ids": { "type": "array", "items": { "type": "string", "minLength": 1 }, "uniqueItems": true } }
|
|
30
|
+
},
|
|
31
|
+
"reference": {
|
|
32
|
+
"type": "object", "required": ["kind", "source_id", "path", "sha256", "authority"],
|
|
33
|
+
"properties": { "kind": { "enum": ["constitution", "spec", "plan", "task", "artifact", "review", "commit"] }, "source_id": { "type": "string", "minLength": 1 }, "path": { "type": "string", "minLength": 1 }, "sha256": { "$ref": "#/$defs/sha256" }, "authority": { "const": "reported" }, "title": { "type": "string", "maxLength": 300 } }
|
|
34
|
+
},
|
|
35
|
+
"ownership": {
|
|
36
|
+
"type": "object", "required": ["concept", "owner"],
|
|
37
|
+
"properties": { "concept": { "enum": ["spec_source", "plan", "task", "execution", "artifact", "evidence"] }, "owner": { "const": "wendkeep" } }
|
|
38
|
+
},
|
|
39
|
+
"specProjection": {
|
|
40
|
+
"type": "object", "required": ["schema_version", "contract_kind", "projection_id", "adapter", "adapter_version", "authority", "origin", "compatibility", "provenance", "ownership", "references", "mappings", "diagnostics", "ok"],
|
|
41
|
+
"properties": { "schema_version": { "const": 1 }, "contract_kind": { "const": "spec-projection" }, "projection_id": { "$ref": "#/$defs/sha256" }, "adapter": { "const": "spec-kit" }, "adapter_version": { "type": "string", "minLength": 1 }, "authority": { "const": "reported" }, "origin": { "$ref": "#/$defs/origin" }, "compatibility": { "$ref": "#/$defs/compatibility" }, "provenance": { "$ref": "#/$defs/provenance" }, "ownership": { "type": "array", "items": { "$ref": "#/$defs/ownership" } }, "references": { "type": "array", "items": { "$ref": "#/$defs/reference" } }, "mappings": { "type": "array", "items": { "$ref": "#/$defs/mapping" } }, "diagnostics": { "type": "array" }, "ok": { "type": "boolean" } }
|
|
42
|
+
},
|
|
43
|
+
"dispatch": {
|
|
44
|
+
"type": "object", "required": ["schema_version", "contract_kind", "dispatch_id", "adapter", "authority", "canonical_owner", "origin", "compatibility", "provenance", "task_contract", "worktree"],
|
|
45
|
+
"properties": { "schema_version": { "const": 1 }, "contract_kind": { "const": "dispatch" }, "dispatch_id": { "$ref": "#/$defs/sha256" }, "adapter": { "const": "superpowers" }, "authority": { "const": "reported" }, "canonical_owner": { "const": "wendkeep" }, "origin": { "$ref": "#/$defs/origin" }, "compatibility": { "$ref": "#/$defs/compatibility" }, "provenance": { "$ref": "#/$defs/provenance" }, "task_contract": { "type": "object" }, "worktree": { "type": "object" } }
|
|
46
|
+
},
|
|
47
|
+
"handoff": {
|
|
48
|
+
"type": "object", "required": ["schema_version", "contract_kind", "handoff_id", "task_contract_id", "head_sha", "authority", "origin", "provenance"],
|
|
49
|
+
"properties": { "schema_version": { "const": 1 }, "contract_kind": { "const": "handoff" }, "handoff_id": { "type": "string", "minLength": 1 }, "task_contract_id": { "$ref": "#/$defs/sha256" }, "head_sha": { "type": "string", "pattern": "^[a-f0-9]{40}$" }, "authority": { "const": "reported" }, "origin": { "$ref": "#/$defs/origin" }, "provenance": { "$ref": "#/$defs/provenance" } }
|
|
50
|
+
},
|
|
51
|
+
"externalArtifact": {
|
|
52
|
+
"type": "object", "required": ["schema_version", "contract_kind", "source", "external_id", "kind", "sha256", "authority", "origin", "provenance"],
|
|
53
|
+
"properties": { "schema_version": { "const": 1 }, "contract_kind": { "const": "external-artifact" }, "source": { "const": "superpowers" }, "external_id": { "type": "string", "minLength": 1 }, "kind": { "enum": ["artifact", "review", "commit"] }, "sha256": { "$ref": "#/$defs/sha256" }, "authority": { "enum": ["reported", "verified"] }, "origin": { "$ref": "#/$defs/origin" }, "provenance": { "$ref": "#/$defs/provenance" }, "proof": { "$ref": "#/$defs/proof" } },
|
|
54
|
+
"allOf": [{ "if": { "properties": { "authority": { "const": "verified" } }, "required": ["authority"] }, "then": { "required": ["proof"] } }]
|
|
55
|
+
},
|
|
56
|
+
"proof": {
|
|
57
|
+
"type": "object", "required": ["schema_version", "contract_kind", "proof_id", "type", "external_id", "artifact_sha256", "authority", "origin", "provenance", "evidence_envelope_id", "sensor_id", "task_id", "path", "head_sha", "git_blob", "manifest_git_blob"],
|
|
58
|
+
"properties": { "schema_version": { "const": 1 }, "contract_kind": { "const": "proof" }, "proof_id": { "$ref": "#/$defs/sha256" }, "type": { "const": "evidence-envelope" }, "external_id": { "type": "string", "minLength": 1 }, "artifact_sha256": { "$ref": "#/$defs/sha256" }, "authority": { "const": "verified" }, "origin": { "$ref": "#/$defs/origin" }, "provenance": { "$ref": "#/$defs/provenance" }, "evidence_envelope_id": { "$ref": "#/$defs/envelopeId" }, "sensor_id": { "type": "string", "minLength": 1 }, "task_id": { "type": "string", "minLength": 1 }, "path": { "type": "string", "minLength": 1 }, "head_sha": { "type": "string", "pattern": "^[a-f0-9]{40}$" }, "git_blob": { "type": "string", "pattern": "^[a-f0-9]{40}$" }, "manifest_git_blob": { "type": "string", "pattern": "^[a-f0-9]{40}$" } }
|
|
59
|
+
},
|
|
60
|
+
"statusProjection": {
|
|
61
|
+
"type": "object", "required": ["schema_version", "contract_kind", "status_projection_id", "adapter", "authority", "canonical_owner", "origin", "provenance", "source_projection_id", "tasks", "evidence"],
|
|
62
|
+
"properties": { "schema_version": { "const": 1 }, "contract_kind": { "const": "status-projection" }, "status_projection_id": { "$ref": "#/$defs/sha256" }, "adapter": { "const": "spec-kit" }, "authority": { "const": "reported" }, "canonical_owner": { "const": "wendkeep" }, "origin": { "$ref": "#/$defs/origin" }, "provenance": { "$ref": "#/$defs/provenance" }, "source_projection_id": { "$ref": "#/$defs/sha256" }, "tasks": { "type": "array" }, "evidence": { "type": "array" } }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
-- wendkeep:structural
|
|
2
|
+
-- Security lifecycle: hash-only credentials, access audit, retention, purge receipts and encrypted columns.
|
|
3
|
+
|
|
4
|
+
ALTER TABLE llm_calls ADD COLUMN prompt_envelope TEXT NOT NULL DEFAULT '';
|
|
5
|
+
ALTER TABLE llm_calls ADD COLUMN response_envelope TEXT NOT NULL DEFAULT '';
|
|
6
|
+
ALTER TABLE llm_calls ADD COLUMN metadata_envelope TEXT NOT NULL DEFAULT '';
|
|
7
|
+
ALTER TABLE documents ADD COLUMN content_envelope TEXT NOT NULL DEFAULT '';
|
|
8
|
+
ALTER TABLE documents ADD COLUMN metadata_envelope TEXT NOT NULL DEFAULT '';
|
|
9
|
+
ALTER TABLE transcripts ADD COLUMN metadata_envelope TEXT NOT NULL DEFAULT '';
|
|
10
|
+
ALTER TABLE project_snapshots ADD COLUMN snapshot_envelope TEXT NOT NULL DEFAULT '';
|
|
11
|
+
|
|
12
|
+
CREATE TABLE IF NOT EXISTS observer_tokens (
|
|
13
|
+
token_id TEXT PRIMARY KEY,
|
|
14
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
15
|
+
role TEXT NOT NULL CHECK(role IN ('viewer', 'auditor', 'publisher', 'admin')),
|
|
16
|
+
project_ids_json TEXT NOT NULL,
|
|
17
|
+
scopes_json TEXT NOT NULL,
|
|
18
|
+
created_at TEXT NOT NULL,
|
|
19
|
+
expires_at TEXT NOT NULL,
|
|
20
|
+
revoked_at TEXT,
|
|
21
|
+
rotated_from TEXT REFERENCES observer_tokens(token_id) ON DELETE SET NULL
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
CREATE INDEX IF NOT EXISTS idx_observer_tokens_hash_active ON observer_tokens(token_hash, revoked_at, expires_at);
|
|
25
|
+
|
|
26
|
+
CREATE TABLE IF NOT EXISTS observer_access_audit (
|
|
27
|
+
audit_id TEXT PRIMARY KEY,
|
|
28
|
+
project_id TEXT NOT NULL,
|
|
29
|
+
token_id TEXT REFERENCES observer_tokens(token_id) ON DELETE SET NULL,
|
|
30
|
+
capability TEXT NOT NULL,
|
|
31
|
+
outcome TEXT NOT NULL,
|
|
32
|
+
occurred_at TEXT NOT NULL,
|
|
33
|
+
metadata_json TEXT NOT NULL DEFAULT '{}'
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_observer_audit_project_time ON observer_access_audit(project_id, occurred_at);
|
|
37
|
+
|
|
38
|
+
CREATE TABLE IF NOT EXISTS observer_retention_policies (
|
|
39
|
+
project_id TEXT PRIMARY KEY REFERENCES projects(project_id) ON DELETE CASCADE,
|
|
40
|
+
policy_json TEXT NOT NULL,
|
|
41
|
+
updated_at TEXT NOT NULL
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
CREATE TABLE IF NOT EXISTS observer_purge_receipts (
|
|
45
|
+
receipt_id TEXT PRIMARY KEY,
|
|
46
|
+
project_id TEXT NOT NULL,
|
|
47
|
+
request_hash TEXT NOT NULL,
|
|
48
|
+
cutoff_at TEXT NOT NULL,
|
|
49
|
+
classes_json TEXT NOT NULL,
|
|
50
|
+
counts_json TEXT NOT NULL,
|
|
51
|
+
purged_at TEXT NOT NULL,
|
|
52
|
+
receipt_hash TEXT NOT NULL UNIQUE,
|
|
53
|
+
receipt_json TEXT NOT NULL
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_observer_purge_project_time ON observer_purge_receipts(project_id, purged_at);
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_observer_purge_request ON observer_purge_receipts(project_id, request_hash, purged_at);
|
|
58
|
+
|
|
59
|
+
CREATE TABLE IF NOT EXISTS observer_security_backfill (
|
|
60
|
+
project_id TEXT PRIMARY KEY REFERENCES projects(project_id) ON DELETE CASCADE,
|
|
61
|
+
status TEXT NOT NULL,
|
|
62
|
+
protected_rows INTEGER NOT NULL DEFAULT 0,
|
|
63
|
+
updated_at TEXT NOT NULL
|
|
64
|
+
);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://wendkeep.dev/schema/observer-policy-v1.schema.json",
|
|
4
|
+
"title": "WendKeep Observer policy v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"properties": {
|
|
8
|
+
"document_capture": { "enum": ["none", "metadata", "selected", "full"] },
|
|
9
|
+
"transcript_capture": { "enum": ["none", "metadata", "messages", "full"] },
|
|
10
|
+
"prompt_capture": { "enum": ["none", "redacted", "full"] },
|
|
11
|
+
"response_capture": { "enum": ["none", "redacted", "full"] },
|
|
12
|
+
"usage_capture": { "enum": ["none", "aggregate", "calls"] },
|
|
13
|
+
"require_loopback_auth": { "type": "boolean" },
|
|
14
|
+
"encryption_required": { "type": "boolean" },
|
|
15
|
+
"rules": {
|
|
16
|
+
"type": "array",
|
|
17
|
+
"items": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"additionalProperties": false,
|
|
20
|
+
"required": ["data_class", "capture"],
|
|
21
|
+
"properties": {
|
|
22
|
+
"project_id": { "type": "string", "minLength": 1, "maxLength": 121 },
|
|
23
|
+
"data_class": { "enum": ["document", "transcript", "prompt", "response", "usage"] },
|
|
24
|
+
"path": { "type": "string", "maxLength": 2048 },
|
|
25
|
+
"entity_type": { "type": "string", "maxLength": 160 },
|
|
26
|
+
"capture": { "enum": ["none", "metadata", "selected", "full", "messages", "redacted", "aggregate", "calls"] }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"redaction": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"additionalProperties": false,
|
|
33
|
+
"properties": {
|
|
34
|
+
"rules": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"items": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"additionalProperties": false,
|
|
39
|
+
"required": ["pattern"],
|
|
40
|
+
"properties": {
|
|
41
|
+
"pattern": { "type": "string", "minLength": 1, "maxLength": 512 },
|
|
42
|
+
"replacement": { "type": "string", "maxLength": 160 }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"retention": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"additionalProperties": false,
|
|
51
|
+
"properties": {
|
|
52
|
+
"document": { "type": "integer", "minimum": 0 },
|
|
53
|
+
"transcript": { "type": "integer", "minimum": 0 },
|
|
54
|
+
"prompt": { "type": "integer", "minimum": 0 },
|
|
55
|
+
"response": { "type": "integer", "minimum": 0 },
|
|
56
|
+
"usage": { "type": "integer", "minimum": 0 },
|
|
57
|
+
"documents": { "type": "integer", "minimum": 0 },
|
|
58
|
+
"calls": { "type": "integer", "minimum": 0 },
|
|
59
|
+
"transcripts": { "type": "integer", "minimum": 0 }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -20,6 +20,16 @@
|
|
|
20
20
|
"observed_at": { "type": "string", "format": "date-time" },
|
|
21
21
|
"operation": { "enum": ["put", "tombstone", "resolve"] },
|
|
22
22
|
"privacy": { "enum": ["shared", "private"] },
|
|
23
|
+
"policy_ref": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"additionalProperties": false,
|
|
26
|
+
"required": ["policy_id", "version", "hash"],
|
|
27
|
+
"properties": {
|
|
28
|
+
"policy_id": { "type": "string", "minLength": 1, "maxLength": 160 },
|
|
29
|
+
"version": { "type": "integer", "minimum": 1 },
|
|
30
|
+
"hash": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }
|
|
31
|
+
}
|
|
32
|
+
},
|
|
23
33
|
"payload": {}
|
|
24
34
|
}
|
|
25
35
|
}
|
|
@@ -52,6 +52,10 @@
|
|
|
52
52
|
"type": "array",
|
|
53
53
|
"items": { "$ref": "#/$defs/sensor" }
|
|
54
54
|
},
|
|
55
|
+
"external_artifacts": {
|
|
56
|
+
"type": "array",
|
|
57
|
+
"items": { "$ref": "#/$defs/externalArtifact" }
|
|
58
|
+
},
|
|
55
59
|
"host_coverage": { "$ref": "host-coverage-v1.schema.json" },
|
|
56
60
|
"tdd_attestations": {
|
|
57
61
|
"type": "array",
|
|
@@ -100,10 +104,45 @@
|
|
|
100
104
|
"exit_code": { "type": ["integer", "null"] },
|
|
101
105
|
"output_sha256": { "$ref": "#/$defs/sha256" },
|
|
102
106
|
"output_tail": { "type": "string", "maxLength": 2000 },
|
|
107
|
+
"artifact_results": {
|
|
108
|
+
"type": "array",
|
|
109
|
+
"items": { "$ref": "#/$defs/sensorArtifactResult" }
|
|
110
|
+
},
|
|
103
111
|
"note": { "type": "string", "maxLength": 2000 },
|
|
104
112
|
"survivors": { "type": "array" },
|
|
105
113
|
"ts": { "type": "string", "format": "date-time" }
|
|
106
114
|
}
|
|
115
|
+
},
|
|
116
|
+
"sensorArtifactResult": {
|
|
117
|
+
"type": "object",
|
|
118
|
+
"additionalProperties": false,
|
|
119
|
+
"required": ["schema_version", "external_id", "path", "algorithm", "digest"],
|
|
120
|
+
"properties": {
|
|
121
|
+
"schema_version": { "const": 1 },
|
|
122
|
+
"external_id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$" },
|
|
123
|
+
"path": { "type": "string", "minLength": 1 },
|
|
124
|
+
"algorithm": { "const": "sha256" },
|
|
125
|
+
"digest": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"externalArtifact": {
|
|
129
|
+
"type": "object",
|
|
130
|
+
"additionalProperties": false,
|
|
131
|
+
"required": ["schema_version", "source", "external_id", "kind", "path", "sha256", "authority", "sensor_id", "task_id", "git_blob", "manifest_path", "manifest_git_blob"],
|
|
132
|
+
"properties": {
|
|
133
|
+
"schema_version": { "const": 1 },
|
|
134
|
+
"source": { "const": "superpowers" },
|
|
135
|
+
"external_id": { "type": "string", "minLength": 1 },
|
|
136
|
+
"kind": { "enum": ["artifact", "review", "commit"] },
|
|
137
|
+
"path": { "type": "string", "minLength": 1 },
|
|
138
|
+
"sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
139
|
+
"authority": { "enum": ["reported", "verified"] },
|
|
140
|
+
"sensor_id": { "type": "string", "minLength": 1 },
|
|
141
|
+
"task_id": { "type": "string", "minLength": 1 },
|
|
142
|
+
"git_blob": { "$ref": "#/$defs/gitObject" },
|
|
143
|
+
"manifest_path": { "const": ".wendkeep/bridge-artifacts.json" },
|
|
144
|
+
"manifest_git_blob": { "$ref": "#/$defs/gitObject" }
|
|
145
|
+
}
|
|
107
146
|
}
|
|
108
147
|
}
|
|
109
148
|
}
|
|
@@ -40,6 +40,20 @@
|
|
|
40
40
|
"severity": { "enum": ["critical", "warning"], "default": "critical" },
|
|
41
41
|
"type": { "enum": ["command", "mutation"], "default": "command" },
|
|
42
42
|
"command": { "type": "string", "minLength": 1 },
|
|
43
|
+
"artifact_results": {
|
|
44
|
+
"type": "array",
|
|
45
|
+
"items": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"additionalProperties": false,
|
|
48
|
+
"required": ["schema_version", "external_id", "path", "algorithm"],
|
|
49
|
+
"properties": {
|
|
50
|
+
"schema_version": { "const": 1 },
|
|
51
|
+
"external_id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$" },
|
|
52
|
+
"path": { "type": "string", "minLength": 1 },
|
|
53
|
+
"algorithm": { "const": "sha256" }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
43
57
|
"report": {
|
|
44
58
|
"type": "string",
|
|
45
59
|
"description": "type: mutation only — path (project-relative) to the mutation-testing-elements report."
|
package/src/doctor.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// `wendkeep doctor` — vault/session integrity (hooks/vault-health.mjs) PLUS the a2
|
|
2
2
|
// harness integrity check (hooks/harness-doctor.mjs). Exits 1 on any error.
|
|
3
|
+
import * as bridgeFs from 'node:fs';
|
|
3
4
|
import { resolve } from 'node:path';
|
|
4
5
|
import { checkHarness, checkVaultLinks, checkSessionActivity, checkStackedFrontmatter, renderStackedFrontmatterLines, checkUnpricedModels, renderUnpricedModelLines, checkStaleDerivedSections, renderStaleDerivedSectionLines, checkSessionObservability, renderSessionObservabilityLines } from '../hooks/harness-doctor.mjs';
|
|
5
6
|
import { diagnoseManagedWorktrees } from './worktree.mjs';
|
|
@@ -20,6 +21,7 @@ import { inspectPortableState } from './portable.mjs';
|
|
|
20
21
|
import { inspectSyncOutbox, readLocalSyncState } from './sync-outbox.mjs';
|
|
21
22
|
import { readProjectForValidation } from '../packages/vault/src/validate-memory.mjs';
|
|
22
23
|
import { inspectGitCommitHooks } from './git-commit-hooks.mjs';
|
|
24
|
+
import { inspectEcosystemBridges, renderEcosystemBridgeLines } from '../packages/integrations/src/ecosystem-bridge.mjs';
|
|
23
25
|
|
|
24
26
|
const healthStatusLabel = (status) => ({
|
|
25
27
|
healthy: 'saudável', warning: 'atenção', degraded: 'degradada', blocked: 'bloqueada', legacy: 'legado',
|
|
@@ -178,6 +180,8 @@ export function runDoctor(argv) {
|
|
|
178
180
|
for (const w of warnings) process.stdout.write(` ! ${w}\n`);
|
|
179
181
|
|
|
180
182
|
const worktrees = diagnoseManagedWorktrees({ startDir: projectRoot });
|
|
183
|
+
const bridges = inspectEcosystemBridges({ projectRoot, fs: bridgeFs });
|
|
184
|
+
process.stdout.write(`\n${renderEcosystemBridgeLines(bridges).join('\n')}\n`);
|
|
181
185
|
process.stdout.write(`\n[worktrees] ${worktrees.initialized ? `${worktrees.issues.length} problema(s)` : 'não inicializado'}\n`);
|
|
182
186
|
for (const issue of worktrees.issues) {
|
|
183
187
|
process.stdout.write(` → ${issue.slug}: ${issue.errorCode} — ${issue.repair}\n`);
|
|
@@ -264,6 +268,7 @@ export function runDoctor(argv) {
|
|
|
264
268
|
|| warnings.length
|
|
265
269
|
|| worktrees.issues.length
|
|
266
270
|
|| (commitHooks.configured && commitHooks.status !== 'healthy')
|
|
271
|
+
|| !bridges.ok
|
|
267
272
|
|| activeContexts.issues.length
|
|
268
273
|
|| ['diverged', 'invalid'].includes(portable.status)
|
|
269
274
|
|| sync.status === 'corrupt'
|
|
@@ -276,5 +281,5 @@ export function runDoctor(argv) {
|
|
|
276
281
|
|| (staleDerived.notes || staleDerived.items || []).length
|
|
277
282
|
|| !observability.ok
|
|
278
283
|
);
|
|
279
|
-
return (scope !== 'runtime' && (healthStatus !== 0 || recallStatus !== 0)) || errors.length || strictDebt ? 1 : 0;
|
|
284
|
+
return (scope !== 'runtime' && (healthStatus !== 0 || recallStatus !== 0)) || errors.length || !bridges.ok || strictDebt ? 1 : 0;
|
|
280
285
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { existsSync, lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { relative, resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { isProjectContainedPath } from '../packages/integrations/src/bridge-config.mjs';
|
|
7
|
+
|
|
8
|
+
const MANIFEST_PATH = '.wendkeep/bridge-artifacts.json';
|
|
9
|
+
const MAX_ARTIFACT_BYTES = 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
function fail(code, message) {
|
|
12
|
+
throw Object.assign(new Error(message), { code });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function gitText(projectRoot, args, spawn = spawnSync) {
|
|
16
|
+
const result = spawn('git', args, {
|
|
17
|
+
cwd: projectRoot, encoding: 'utf8', windowsHide: true,
|
|
18
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
19
|
+
});
|
|
20
|
+
return result.status === 0 && !result.error ? String(result.stdout || '').trim() : '';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function trackedBlob(projectRoot, path, spawn) {
|
|
24
|
+
if (!gitText(projectRoot, ['ls-files', '--error-unmatch', '--', path], spawn)) return '';
|
|
25
|
+
const working = gitText(projectRoot, ['hash-object', '--', path], spawn);
|
|
26
|
+
const indexed = gitText(projectRoot, ['rev-parse', `:${path}`], spawn);
|
|
27
|
+
return working && working === indexed ? indexed : '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function safeProjectFile(projectRoot, path, code) {
|
|
31
|
+
const candidate = resolve(projectRoot, String(path || ''));
|
|
32
|
+
if (!String(path || '').trim() || !isProjectContainedPath(projectRoot, candidate)
|
|
33
|
+
|| !existsSync(candidate) || !lstatSync(candidate).isFile()) {
|
|
34
|
+
fail(code, `bridge artifact path is unavailable: ${path || '<missing>'}`);
|
|
35
|
+
}
|
|
36
|
+
const file = realpathSync(candidate);
|
|
37
|
+
if (!isProjectContainedPath(projectRoot, file) || lstatSync(file).size > MAX_ARTIFACT_BYTES) {
|
|
38
|
+
fail(code, `bridge artifact path is unsafe: ${path}`);
|
|
39
|
+
}
|
|
40
|
+
return { file, path: relative(projectRoot, file).replaceAll('\\', '/') };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readManifest(projectRoot, spawn) {
|
|
44
|
+
const candidate = resolve(projectRoot, MANIFEST_PATH);
|
|
45
|
+
const tracked = gitText(projectRoot, ['ls-files', '--error-unmatch', '--', MANIFEST_PATH], spawn);
|
|
46
|
+
if (!tracked && !existsSync(candidate)) return null;
|
|
47
|
+
if (!tracked || !existsSync(candidate)) {
|
|
48
|
+
fail('BRIDGE_ARTIFACT_MANIFEST_UNTRACKED', 'bridge artifact manifest must exist and match the Git index');
|
|
49
|
+
}
|
|
50
|
+
const manifest = safeProjectFile(projectRoot, MANIFEST_PATH, 'BRIDGE_ARTIFACT_MANIFEST_INVALID');
|
|
51
|
+
const gitBlob = trackedBlob(projectRoot, manifest.path, spawn);
|
|
52
|
+
if (!gitBlob) fail('BRIDGE_ARTIFACT_MANIFEST_UNTRACKED', 'bridge artifact manifest must match the Git index');
|
|
53
|
+
let parsed;
|
|
54
|
+
try { parsed = JSON.parse(readFileSync(manifest.file, 'utf8')); }
|
|
55
|
+
catch { fail('BRIDGE_ARTIFACT_MANIFEST_INVALID', 'bridge artifact manifest must be valid JSON'); }
|
|
56
|
+
if (parsed?.schema_version !== 1 || !Array.isArray(parsed?.artifacts)) {
|
|
57
|
+
fail('BRIDGE_ARTIFACT_MANIFEST_INVALID', 'bridge artifact manifest requires schema_version 1 and artifacts');
|
|
58
|
+
}
|
|
59
|
+
if (Object.keys(parsed).some((key) => !['schema_version', 'artifacts'].includes(key))) {
|
|
60
|
+
fail('BRIDGE_ARTIFACT_MANIFEST_INVALID', 'bridge artifact manifest contains unsupported fields');
|
|
61
|
+
}
|
|
62
|
+
return { artifacts: parsed.artifacts, gitBlob, path: manifest.path };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function collectBridgeArtifactEvidence({
|
|
66
|
+
projectRoot, tasks = [], sensors = [], spawn = spawnSync,
|
|
67
|
+
} = {}) {
|
|
68
|
+
const root = realpathSync(resolve(projectRoot));
|
|
69
|
+
const manifest = readManifest(root, spawn);
|
|
70
|
+
if (!manifest) return [];
|
|
71
|
+
const taskById = new Map((tasks || []).map((task) => [String(task.id || ''), task]));
|
|
72
|
+
const sensorById = new Map((sensors || []).map((sensor) => [String(sensor.id || ''), sensor]));
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
return manifest.artifacts.map((entry) => {
|
|
75
|
+
const externalId = String(entry?.external_id || '').trim();
|
|
76
|
+
const kind = String(entry?.kind || '').trim();
|
|
77
|
+
const sensorId = String(entry?.sensor_id || '').trim();
|
|
78
|
+
const taskId = String(entry?.task_id || '').trim();
|
|
79
|
+
if (Object.keys(entry || {}).some((key) => ![
|
|
80
|
+
'source', 'external_id', 'kind', 'path', 'sensor_id', 'task_id',
|
|
81
|
+
].includes(key)) || entry?.source !== 'superpowers' || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(externalId)
|
|
82
|
+
|| !['artifact', 'review', 'commit'].includes(kind) || !sensorId || !taskId || seen.has(externalId)) {
|
|
83
|
+
fail('BRIDGE_ARTIFACT_MANIFEST_INVALID', `invalid or duplicate bridge artifact: ${externalId || '<missing>'}`);
|
|
84
|
+
}
|
|
85
|
+
seen.add(externalId);
|
|
86
|
+
const task = taskById.get(taskId);
|
|
87
|
+
const requiredSensors = Array.isArray(task?.sensors) && task.sensors.length ? task.sensors : [task?.sensor].filter(Boolean);
|
|
88
|
+
if (!task || !requiredSensors.includes(sensorId)) {
|
|
89
|
+
fail('BRIDGE_ARTIFACT_TASK_UNBOUND', `bridge artifact ${externalId} is not bound to task ${taskId} and sensor ${sensorId}`);
|
|
90
|
+
}
|
|
91
|
+
const artifact = safeProjectFile(root, entry.path, 'BRIDGE_ARTIFACT_FORGED');
|
|
92
|
+
const gitBlob = trackedBlob(root, artifact.path, spawn);
|
|
93
|
+
if (!gitBlob) fail('BRIDGE_ARTIFACT_FORGED', `bridge artifact ${externalId} must match the Git index`);
|
|
94
|
+
const digest = createHash('sha256').update(readFileSync(artifact.file)).digest('hex');
|
|
95
|
+
const sensor = sensorById.get(sensorId);
|
|
96
|
+
const result = (sensor?.artifact_results || []).find((item) => (
|
|
97
|
+
item?.schema_version === 1 && item.external_id === externalId && item.path === artifact.path
|
|
98
|
+
&& item.algorithm === 'sha256' && item.digest === digest
|
|
99
|
+
));
|
|
100
|
+
if (sensor?.status === 'green' && sensor?.exit_code === 0 && !result) {
|
|
101
|
+
fail('BRIDGE_ARTIFACT_RESULT_MISSING', `green sensor ${sensorId} did not produce the bound digest for ${externalId}`);
|
|
102
|
+
}
|
|
103
|
+
const verified = sensor?.status === 'green' && sensor?.exit_code === 0 && Boolean(result);
|
|
104
|
+
return {
|
|
105
|
+
schema_version: 1,
|
|
106
|
+
source: 'superpowers', external_id: externalId, kind, path: artifact.path,
|
|
107
|
+
sha256: digest, authority: verified ? 'verified' : 'reported', sensor_id: sensorId, task_id: taskId,
|
|
108
|
+
git_blob: gitBlob, manifest_path: manifest.path, manifest_git_blob: manifest.gitBlob,
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
}
|