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