feat: add secure Buzz connection diagnostics (#946)

This commit is contained in:
Brad Groux 2026-07-23 22:07:25 -05:00 committed by GitHub
parent 32517e4df6
commit 54417357f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 4225 additions and 76 deletions

View file

@ -18,6 +18,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added a reference-only Buzz communication adapter with typed configuration,
host-preserving HTTP/WS normalization, SSRF-aware bounded relay probes,
NIP-11 software/version/community checks, NIP-98 identity and membership/read
diagnostics with built-in hexadecimal/`nsec` signing, stable machine-readable
reason codes, evidence invalidation, shell-free optional command discovery,
Settings controls, `vk doctor` reporting, and operator/API/security
documentation. Buzz message delivery and reply subscription remain
fail-closed for their dedicated follow-up issue (#905).
- Added transactional, remote-safe worktree lifecycle management with
versioned manifests, exact fetched base commits, reasoned offline fallback,
task/attempt ownership leases, active-run locks, recovery states, and

View file

@ -71,6 +71,10 @@ const baseRoutes: Record<string, Response> = {
cli: { installed: true, authenticated: true },
recommendations: [],
}),
'/api/integrations/communication/adapters/buzz-default/health': jsonResponse(
{ error: 'not found' },
404
),
};
describe('vk doctor', () => {
@ -250,4 +254,121 @@ describe('vk doctor', () => {
status: 'warn',
});
});
it('reports exact Buzz health in human and JSON output', async () => {
const routes = {
...baseRoutes,
'/api/integrations/communication/adapters/buzz-default/health': jsonResponse({
adapterId: 'buzz-default',
status: 'healthy',
configured: true,
canSend: false,
canReceiveReplies: false,
checkedAt: '2026-07-23T18:00:00.000Z',
detail:
'Buzz relay identity, configured signing identity, membership posture, and read capabilities are compatible.',
reasonCode: 'ok',
buzz: {
schemaVersion: 'buzz-compatibility/v1',
probeRevision: 1,
testedRelease: '0.4.24',
testedCommit: '710ed9fff57878a1d69f809b80a6ee0416c53fc4',
status: 'healthy',
reasonCode: 'ok',
detail: 'compatible',
configuredRelayHttpUrl: 'https://relay.example.test',
resolvedRelayHttpUrl: 'https://relay.example.test',
resolvedRelayWebSocketUrl: 'wss://relay.example.test',
expectedCommunity: 'relay.example.test',
observedCommunity: 'relay.example.test',
publicKeyFingerprint: 'abc123abc123',
checks: {
relayIdentity: 'verified',
communityBinding: 'verified',
configuredIdentity: 'verified',
authentication: 'verified',
membership: 'verified',
channelRead: 'verified',
messageRead: 'verified',
},
commands: [],
evidenceKey: 'safe-evidence',
checkedAt: '2026-07-23T18:00:00.000Z',
},
}),
};
const report = await runDoctorChecks(
{ apiBase: 'http://vk.test', cwd: '/repo', timeoutMs: 1000 },
{
fetch: doctorFetch(routes),
env: {},
findProjectRoot: async () => '/repo',
countPromptTemplateFiles: async () => 1,
resolveCommand: async (command) =>
command === 'vk' ? '/repo/cli/dist/index.js' : `/usr/bin/${command}`,
now: () => new Date('2026-07-23T18:00:00.000Z'),
}
);
expect(report.checks.find((check) => check.id === 'buzz')).toMatchObject({
status: 'pass',
details: {
status: 'healthy',
reasonCode: 'ok',
expectedCommunity: 'relay.example.test',
observedCommunity: 'relay.example.test',
publicKeyFingerprint: 'abc123abc123',
testedRelease: '0.4.24',
buzz: {
checks: {
channelRead: 'verified',
messageRead: 'verified',
},
evidenceKey: 'safe-evidence',
},
},
});
expect(formatDoctorReport(report)).toContain('[PASS] Buzz compatibility');
});
it('fails doctor for an enabled Buzz membership denial with remediation', async () => {
const routes = {
...baseRoutes,
'/api/integrations/communication/adapters/buzz-default/health': jsonResponse({
adapterId: 'buzz-default',
status: 'not_member',
configured: true,
canSend: false,
canReceiveReplies: false,
checkedAt: '2026-07-23T18:00:00.000Z',
detail: 'Buzz authenticated the identity but denied relay membership.',
reasonCode: 'relay_membership_required',
remediation: 'Add the public identity as a relay member.',
}),
};
const report = await runDoctorChecks(
{ apiBase: 'http://vk.test', cwd: '/repo', timeoutMs: 1000 },
{
fetch: doctorFetch(routes),
env: {},
findProjectRoot: async () => '/repo',
countPromptTemplateFiles: async () => 1,
resolveCommand: async (command) =>
command === 'vk' ? '/repo/cli/dist/index.js' : `/usr/bin/${command}`,
now: () => new Date('2026-07-23T18:00:00.000Z'),
}
);
expect(report.ok).toBe(false);
expect(report.checks.find((check) => check.id === 'buzz')).toMatchObject({
status: 'fail',
details: {
status: 'not_member',
reasonCode: 'relay_membership_required',
},
remediation: 'Add the public identity as a relay member.',
});
});
});

View file

@ -5,7 +5,7 @@ import { readFile, readdir, stat } from 'node:fs/promises';
import path from 'node:path';
import { promisify } from 'node:util';
import { API_BASE, buildApiHeaders } from '../utils/api.js';
import type { HarnessSupportStatus } from '@veritas-kanban/shared';
import type { CommunicationAdapterHealth, HarnessSupportStatus } from '@veritas-kanban/shared';
const execFileAsync = promisify(execFile);
const CRITICAL_STATUSES = new Set<DoctorCheck['id']>([
@ -16,6 +16,7 @@ const CRITICAL_STATUSES = new Set<DoctorCheck['id']>([
'agents',
'harness-support',
'routing',
'buzz',
]);
export type DoctorStatus = 'pass' | 'warn' | 'fail' | 'skip';
@ -540,6 +541,53 @@ function buildCodexCheck(health: CodexHealthResponse | null): DoctorCheck {
);
}
function buildBuzzCheck(
health: CommunicationAdapterHealth | null,
responseStatus: number
): DoctorCheck {
if (!health) {
if (responseStatus === 404) {
return check('buzz', 'Buzz compatibility', 'skip', 'Buzz is not configured');
}
return check(
'buzz',
'Buzz compatibility',
'warn',
'Buzz compatibility diagnostics are unavailable',
{ status: responseStatus },
'Verify settings-read permission and the communication adapter API.'
);
}
const details = {
status: health.status,
reasonCode: health.reasonCode,
configured: health.configured,
canSend: health.canSend,
canReceiveReplies: health.canReceiveReplies,
checkedAt: health.checkedAt,
expectedCommunity: health.buzz?.expectedCommunity,
observedCommunity: health.buzz?.observedCommunity,
publicKeyFingerprint: health.buzz?.publicKeyFingerprint,
testedRelease: health.buzz?.testedRelease,
testedCommit: health.buzz?.testedCommit,
probeRevision: health.buzz?.probeRevision,
commands: health.buzz?.commands,
buzz: health.buzz,
};
if (health.status === 'disabled') {
return check('buzz', 'Buzz compatibility', 'skip', health.detail, details, health.remediation);
}
if (health.status === 'healthy') {
return check('buzz', 'Buzz compatibility', 'pass', health.detail, details);
}
if (health.status === 'degraded' || health.status === 'warning') {
return check('buzz', 'Buzz compatibility', 'warn', health.detail, details, health.remediation);
}
return check('buzz', 'Buzz compatibility', 'fail', health.detail, details, health.remediation);
}
function buildHarnessSupportCheck(statuses: HarnessSupportStatus[] | null): DoctorCheck {
if (!statuses) {
return check(
@ -809,6 +857,13 @@ export async function runDoctorChecks(
'/api/settings/codex/health'
);
checks.push(buildCodexCheck(codexHealth.ok ? codexHealth.data : null));
const buzzHealth = await requestJson<CommunicationAdapterHealth>(
deps,
options,
'/api/integrations/communication/adapters/buzz-default/health'
);
checks.push(buildBuzzCheck(buzzHealth.ok ? buzzHealth.data : null, buzzHealth.status));
} else {
checks.push(
check('api-auth', 'API authentication', 'skip', 'Skipped because API is unreachable')
@ -825,6 +880,7 @@ export async function runDoctorChecks(
checks.push(
check('harness-support', 'Harness support', 'skip', 'Skipped because API is unreachable')
);
checks.push(check('buzz', 'Buzz compatibility', 'skip', 'Skipped because API is unreachable'));
}
const agentResult = await buildAgentCheck(deps, agents);

View file

@ -15,6 +15,16 @@ Fresh v5 installs use OpenAI Codex as the default agent:
Existing configs keep the user's chosen default agent. Missing built-in profiles are added during config normalization without overwriting customized commands, arguments, or enabled states.
## Buzz communication harness
Buzz is integrated as a `buzz` communication adapter, not as an
`AgentProvider`. Settings and `vk doctor` can verify its relay/community
identity, NIP-98 signing identity, relay membership, read capabilities, tested
release contract, and optional local command versions without sending a
message. Veritas does not route task execution through Buzz or start
`buzz-acp`/`buzz-agent` in this slice. See
[Buzz Connection Diagnostics](BUZZ-INTEGRATION.md).
## Harness Support Profiles And Tiers
Every configured agent is normalized to a `harness-support-profile/v1` contract.

View file

@ -729,9 +729,11 @@ Search returns redacted snippets only. Mention notifications link back to the sq
### Communication Adapters
Bidirectional human reply adapters live under integrations. The first adapter
contract is provider-neutral with Microsoft Teams posture fields and a local
ingest API. It stores external thread mappings separately from message content.
Communication adapters live under integrations. Microsoft Teams provides the
existing human reply/send posture. Buzz provides a reference-only connection
configuration and read-only relay compatibility probe. It does not send or
subscribe to messages in this slice. External thread mappings remain separate
from message content.
```
GET /api/integrations/communication/adapters
@ -773,6 +775,41 @@ Responses never return credentials or raw webhook query strings. Redacted
posture appears as `webhookUrlConfigured`, `webhookUrlRedacted`, and
`hasCredential`.
**Buzz configure body**:
```json
{
"kind": "buzz",
"displayName": "Buzz",
"enabled": true,
"relayHttpUrl": "https://community.example.com",
"relayWebSocketUrl": "wss://community.example.com",
"expectedCommunity": "community.example.com",
"publicKey": "<64-hex-public-key>",
"credentialRef": "env:BUZZ_PRIVATE_KEY",
"authTagRef": "env:BUZZ_AUTH_TAG",
"allowLocalhost": false,
"allowPrivateNetwork": false,
"command": {
"executable": "buzz",
"args": []
}
}
```
Buzz rejects raw credentials, URL userinfo/query/fragment components, and
credential-like command arguments. Health returns an exact status and
`reasonCode`, redacted configured/resolved endpoints, expected/observed
community, public-key fingerprint, tested Buzz release/commit, probe revision,
relay contract, independent verification checks, and optional command
diagnostics. `POST .../buzz-default/test` runs the same read-only probe;
`canSend` and `canReceiveReplies` remain `false`. See
[Buzz Connection Diagnostics](BUZZ-INTEGRATION.md).
Send `null` for `relayWebSocketUrl`, `expectedCommunity`, `authTagRef`, or
`command` to clear that optional setting. Omitting one preserves its current
value.
**Reply ingest body**:
```json

173
docs/BUZZ-INTEGRATION.md Normal file
View file

@ -0,0 +1,173 @@
# Buzz Connection Diagnostics
Veritas Kanban can register a Buzz relay as a communication adapter and verify
its compatibility without sending a message or changing relay state. This
first integration slice covers connection configuration, relay/community
identity, NIP-98 authentication, relay membership, channel/message read
capability, and optional local command discovery.
Buzz is a communication harness in this integration. It is not an
`AgentProvider`, and Veritas does not start `buzz`, `buzz-acp`, or `buzz-agent`.
Message send, reply subscription, persona import, and composed agent execution
are separate roadmap capabilities.
## Supported contract
The probe is fixture-pinned to:
- Buzz release `0.4.24`
- Buzz commit `710ed9fff57878a1d69f809b80a6ee0416c53fc4`
- Veritas probe revision `1`
- Relay software identity `https://github.com/block/buzz`
- Required NIPs `11`, `29`, and `42`
- Optional enforced relay membership advertised as NIP `43`
An unknown Buzz version is reported as `unsupported`. Veritas still reads
public NIP-11 metadata, but it does not treat that version as safe evidence for
later message delivery.
## Configure the identity
Use a dedicated Buzz/Nostr identity. Keep the private key in the Veritas server
environment and store only its environment-variable reference in Veritas.
```dotenv
BUZZ_PRIVATE_KEY=<set outside source control>
BUZZ_AUTH_TAG=<optional NIP-OA owner attestation>
```
The signing key may be a 64-character hexadecimal private key or its `nsec`
encoding and must match the configured 64-character hexadecimal public key.
Veritas signs the Buzz-specific NIP-98 event with its pinned `nostr-tools`
runtime. `BUZZ_AUTH_TAG` is needed only when an agent identity receives relay
membership through a NIP-OA owner. Never put an `nsec`, private-key hex, auth
tag, token, or authorization header in the Settings form, API body, task, log,
screenshot, or support packet.
In **Settings -> Notifications -> Buzz Connection**, configure:
- Relay HTTP URL, such as `https://community.example.com`
- Optional matching WebSocket URL; Veritas derives it when omitted
- Expected community host and optional non-default port
- Public key hex
- `env:BUZZ_PRIVATE_KEY`
- Optional `env:BUZZ_AUTH_TAG`
- Explicit localhost/private-network allowances when required
- Optional `buzz`, `buzz-acp`, or `buzz-agent` executable for version
diagnostics
HTTP/WS endpoints must have the same host, port, path, and TLS posture.
Credentials, query strings, and fragments are rejected. Veritas preserves a
configured path and non-default port because Buzz binds the community to the
request host.
## API setup
`settings:write` is required to configure, test, disable, or disconnect an
adapter. `settings:read` is sufficient to read the adapter and health result.
```bash
curl -X PUT http://localhost:3001/api/integrations/communication/adapters/buzz-default \
-H 'Content-Type: application/json' \
-H 'X-API-Key: <veritas-api-key>' \
--data '{
"kind": "buzz",
"displayName": "Buzz",
"enabled": true,
"relayHttpUrl": "https://community.example.com",
"expectedCommunity": "community.example.com",
"publicKey": "<64-hex-public-key>",
"credentialRef": "env:BUZZ_PRIVATE_KEY",
"authTagRef": "env:BUZZ_AUTH_TAG"
}'
```
The response returns public configuration and reference posture only. It never
resolves or returns an environment value.
Run the read-only probe:
```bash
curl http://localhost:3001/api/integrations/communication/adapters/buzz-default/health \
-H 'X-API-Key: <veritas-api-key>'
vk doctor --json
```
`POST .../buzz-default/test` runs the same read-only compatibility probe. It
does not use the generic test-send path.
## Probe sequence
The probe:
1. validates and normalizes HTTP/HTTPS and WS/WSS endpoints;
2. fetches bounded NIP-11 metadata from `/info` through the SSRF-protected,
DNS-pinned outbound client;
3. verifies Buzz software, version, relay public identity, NIPs, and the
configured/observed community authority;
4. resolves the signing key and optional auth tag only for the active probe;
5. signs a fresh, host-bound NIP-98 `POST /query` request with the required
URL, method, random nonce, and exact request-body hash tags;
6. performs separately signed, read-only channel metadata and message filters
so each capability has independent evidence;
7. classifies authentication, relay membership, and read capability
independently; and
8. probes optional commands with executable-plus-argv process spawning, a
timeout, bounded output, no shell, and a minimal environment that excludes
provider and Buzz credentials.
Configured endpoint strings and separately resolved endpoints are retained in
the redacted result. The compatibility evidence key changes when the endpoint,
expected community, configured or observed public identity, secret reference,
auth-tag reference, network allowance, command configuration/version, Buzz
contract, or Veritas probe revision changes. Persisted evidence from an older
probe release, commit, or revision is not restored as current.
## Status and remediation
| Status | Meaning |
| --------------- | ---------------------------------------------------------------- |
| `healthy` | Relay, identity, membership posture, and reads were verified. |
| `degraded` | Public contract is usable, but a bounded diagnostic is partial. |
| `unsupported` | Relay software, version, or contract is outside tested support. |
| `unauthorized` | Identity proof or read authorization was rejected. |
| `not_member` | Identity was authenticated but is not a relay member. |
| `misconfigured` | Endpoints, community, identity, or secret reference disagree. |
| `unreachable` | Network policy, DNS, TLS, timeout, or relay availability failed. |
| `disabled` | Configuration is retained but no probe is run. |
Machine-readable `reasonCode` values distinguish endpoint mismatch,
community mismatch, missing credential reference, malformed NIP-OA auth tags,
public-key mismatch, authentication rejection, membership denial,
read-capability denial, rate-limiting, oversized/invalid responses, and
unsupported builds.
The probe never claims send/reply verification. `canSend` and
`canReceiveReplies` remain `false` for this integration slice.
## Local and private relays
Public HTTPS is the default. Plain HTTP requires an explicit localhost or
private-network allowance. Localhost and RFC1918/IPv6 ULA private ranges are
denied unless the operator enables the matching setting. Link-local, cloud
metadata, and CGNAT ranges remain blocked even when private-network access is
enabled. Both allowances are explicit because a Buzz relay URL is an outbound
request target. DNS is still resolved and pinned for every request, redirects
remain disabled, reads are bounded, and the probe has a fixed timeout.
Enable only the narrow network class required by the relay. Do not use a
private-network allowance to reach cloud metadata or unrelated internal
services.
## Upgrade, disable, and rollback
After a Buzz upgrade, run `vk doctor --json`. A version/build change invalidates
the prior evidence and must pass the pinned compatibility fixtures before it is
considered supported.
Use **Disable** or the disconnect endpoint to stop probes. Buzz reference-only
configuration is retained so rollback does not destroy operator setup. Remove
the environment secrets separately if the identity is being retired. Veritas
does not remove relay membership, change communities, or modify Buzz desktop
state.

View file

@ -1224,6 +1224,7 @@ Notification and broadcast features provide local visibility and optional delive
- **Broadcast messages** — Durable system-wide messages at `/api/broadcasts` with `info`, `action-required`, and `urgent` priorities
- **External delivery boundary** — Local notifications, broadcasts, and Squad Chat can work while external webhook delivery is disabled
- **Human reply adapter health** — Settings -> Notifications shows Teams reply posture, redacted webhook state, recent delivery audit, test send, and disconnect controls
- **Buzz compatibility diagnostics** — Reference-only Buzz relay setup verifies host-derived community identity, NIP-98 authentication, relay membership, channel/message read capability, tested release evidence, and optional command versions without sending a message
### API Endpoints

View file

@ -154,6 +154,22 @@ limits are tracked in
Compatibility errors and debug bundles must redact tokens, cookies, private
keys, local private paths, raw chat content, and task body text.
Buzz communication diagnostics store environment-variable references and
public/redacted metadata only. The Nostr private key and optional NIP-OA auth
tag are resolved only for the active read-only probe and are never returned.
The built-in signer accepts hexadecimal or `nsec` private-key material,
constructs the Buzz-required nonce and exact request-body hash, and clears its
decoded key bytes after signing on a best-effort basis.
Relay URLs reject userinfo, query strings, and fragments. Outbound requests use
scheme validation, explicit plaintext/local/RFC1918/ULA opt-ins, DNS pinning,
manual redirects, fixed timeouts, and bounded response reads. Link-local,
cloud-metadata, and CGNAT ranges remain blocked under the private-network
opt-in. The default probe reads NIP-11 metadata and authenticated query filters
only; it does not send a Buzz event. Optional command discovery runs without a
shell and receives a minimal environment that excludes provider and Buzz
credentials. See
[Buzz Connection Diagnostics](BUZZ-INTEGRATION.md).
## Provider Runtime Capability Enforcement
Provider runtime manifests are authorization evidence, not display metadata.

70
pnpm-lock.yaml generated
View file

@ -207,6 +207,9 @@ importers:
nanoid:
specifier: ^6.0.0
version: 6.0.0
nostr-tools:
specifier: 2.24.1
version: 2.24.1(typescript@6.0.3)
pino:
specifier: ^10.3.1
version: 10.3.1
@ -1053,6 +1056,14 @@ packages:
'@emnapi/core': ^1.7.1
'@emnapi/runtime': ^1.7.1
'@noble/ciphers@2.1.1':
resolution: {integrity: sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==}
engines: {node: '>= 20.19.0'}
'@noble/curves@2.0.1':
resolution: {integrity: sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==}
engines: {node: '>= 20.19.0'}
'@noble/hashes@1.4.0':
resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==}
engines: {node: '>= 16'}
@ -1061,6 +1072,10 @@ packages:
resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
engines: {node: ^14.21.3 || >=16}
'@noble/hashes@2.0.1':
resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==}
engines: {node: '>= 20.19.0'}
'@noble/hashes@2.2.0':
resolution: {integrity: sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==}
engines: {node: '>= 20.19.0'}
@ -1250,6 +1265,15 @@ packages:
'@scarf/scarf@1.4.0':
resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==}
'@scure/base@2.0.0':
resolution: {integrity: sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==}
'@scure/bip32@2.0.1':
resolution: {integrity: sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA==}
'@scure/bip39@2.0.1':
resolution: {integrity: sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg==}
'@simple-git/args-pathspec@1.0.3':
resolution: {integrity: sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==}
@ -3874,6 +3898,17 @@ packages:
resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==}
engines: {node: '>=10'}
nostr-tools@2.24.1:
resolution: {integrity: sha512-KdrKjC74n/rr6J3eCSfZj8dcbZFvolHYe4S22SefNZ5YWbhHiB0KL/mmJjEZ0u6B9mZK0YcQtl+WQ46KzwapeQ==}
peerDependencies:
typescript: '>=5.0.0'
peerDependenciesMeta:
typescript:
optional: true
nostr-wasm@0.1.0:
resolution: {integrity: sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==}
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
@ -5907,10 +5942,18 @@ snapshots:
'@tybys/wasm-util': 0.10.3
optional: true
'@noble/ciphers@2.1.1': {}
'@noble/curves@2.0.1':
dependencies:
'@noble/hashes': 2.0.1
'@noble/hashes@1.4.0': {}
'@noble/hashes@1.8.0': {}
'@noble/hashes@2.0.1': {}
'@noble/hashes@2.2.0': {}
'@openai/codex-sdk@0.144.3':
@ -6043,6 +6086,19 @@ snapshots:
'@scarf/scarf@1.4.0': {}
'@scure/base@2.0.0': {}
'@scure/bip32@2.0.1':
dependencies:
'@noble/curves': 2.0.1
'@noble/hashes': 2.0.1
'@scure/base': 2.0.0
'@scure/bip39@2.0.1':
dependencies:
'@noble/hashes': 2.0.1
'@scure/base': 2.0.0
'@simple-git/args-pathspec@1.0.3': {}
'@simple-git/argv-parser@1.1.1':
@ -9199,6 +9255,20 @@ snapshots:
normalize-url@6.1.0: {}
nostr-tools@2.24.1(typescript@6.0.3):
dependencies:
'@noble/ciphers': 2.1.1
'@noble/curves': 2.0.1
'@noble/hashes': 2.0.1
'@scure/base': 2.0.0
'@scure/bip32': 2.0.1
'@scure/bip39': 2.0.1
nostr-wasm: 0.1.0
optionalDependencies:
typescript: 6.0.3
nostr-wasm@0.1.0: {}
object-assign@4.1.1: {}
object-inspect@1.13.4: {}

View file

@ -35,6 +35,7 @@
"mime-types": "^3.0.2",
"multer": "^2.2.0",
"nanoid": "^6.0.0",
"nostr-tools": "2.24.1",
"pino": "^10.3.1",
"pino-pretty": "^13.1.3",
"sanitize-filename": "^1.6.4",

View file

@ -0,0 +1,133 @@
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { BuzzCompatibilityService } from '../services/buzz-compatibility-service.js';
import { CommunicationAdapterService } from '../services/communication-adapter-service.js';
const PUBLIC_KEY = 'ab'.repeat(32);
const FIXTURE_PRIVATE_MATERIAL = 'fixture-private-material';
let closeServer: (() => Promise<void>) | undefined;
let temporaryDirectory: string | undefined;
afterEach(async () => {
await closeServer?.();
closeServer = undefined;
if (temporaryDirectory) {
await fs.rm(temporaryDirectory, { recursive: true, force: true });
temporaryDirectory = undefined;
}
});
function sendJson(response: ServerResponse, body: unknown): void {
response.statusCode = 200;
response.setHeader('content-type', 'application/json');
response.end(JSON.stringify(body));
}
describe('Buzz compatibility fake relay', () => {
it('proves the authenticated read path without persisting or returning secret material', async () => {
const requests: Array<{
method?: string;
url?: string;
authorizationPresent: boolean;
}> = [];
const server = createServer((request: IncomingMessage, response: ServerResponse) => {
requests.push({
method: request.method,
url: request.url,
authorizationPresent: Boolean(request.headers.authorization),
});
if (request.url === '/info') {
sendJson(response, {
software: 'https://github.com/block/buzz',
version: '0.4.24',
supported_nips: [1, 11, 29, 42, 43],
supported_extensions: ['nip-er'],
self: 'cd'.repeat(32),
limitation: { auth_required: true },
});
return;
}
if (request.url === '/query' && request.method === 'POST') {
sendJson(response, []);
return;
}
response.statusCode = 404;
response.end();
});
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
closeServer = () => new Promise<void>((resolve) => server.close(() => resolve()));
const address = server.address();
if (!address || typeof address === 'string') throw new Error('fixture relay did not bind');
const compatibility = new BuzzCompatibilityService({
resolveSecret: async () => FIXTURE_PRIVATE_MATERIAL,
signer: {
async sign() {
return { authorization: 'Nostr fixture-signature', publicKey: PUBLIC_KEY };
},
},
runCommand: async () => {
throw Object.assign(new Error('not installed'), { code: 'ENOENT' });
},
});
temporaryDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'buzz-compatibility-'));
const audit = vi.fn().mockResolvedValue(undefined);
const adapters = new CommunicationAdapterService({
storageDir: temporaryDirectory,
persist: true,
buzzCompatibility: compatibility,
audit,
});
await adapters.configureAdapter('buzz-default', {
kind: 'buzz',
enabled: true,
relayHttpUrl: `http://127.0.0.1:${address.port}`,
expectedCommunity: `127.0.0.1:${address.port}`,
publicKey: PUBLIC_KEY,
credentialRef: 'env:BUZZ_PRIVATE_KEY',
allowLocalhost: true,
});
const result = await adapters.checkHealth('buzz-default');
const publicAdapter = await adapters.getAdapter('buzz-default');
const persisted = await fs.readFile(path.join(temporaryDirectory, 'state.json'), 'utf8');
expect(result).toMatchObject({
status: 'healthy',
reasonCode: 'ok',
canSend: false,
canReceiveReplies: false,
});
expect(result.buzz).toMatchObject({
checks: {
relayIdentity: 'verified',
communityBinding: 'verified',
configuredIdentity: 'verified',
authentication: 'verified',
membership: 'verified',
channelRead: 'verified',
messageRead: 'verified',
},
});
expect(requests).toEqual([
{ method: 'GET', url: '/info', authorizationPresent: false },
{ method: 'POST', url: '/query', authorizationPresent: true },
{ method: 'POST', url: '/query', authorizationPresent: true },
]);
const artifacts = JSON.stringify({
result,
publicAdapter,
persisted,
audit: audit.mock.calls,
requests,
});
expect(artifacts).not.toContain(FIXTURE_PRIVATE_MATERIAL);
expect(artifacts).not.toContain('fixture-signature');
expect(publicAdapter).toMatchObject({
credentialRef: 'env:BUZZ_PRIVATE_KEY',
hasCredential: true,
});
});
});

View file

@ -0,0 +1,693 @@
import { describe, expect, it, vi } from 'vitest';
import { verifyEvent } from 'nostr-tools';
import {
BuzzCompatibilityService,
buildBuzzCommandEnvironment,
normalizeBuzzEndpoints,
} from '../services/buzz-compatibility-service.js';
const PUBLIC_KEY = 'ab'.repeat(32);
const SIGNING_KEY = `${'0'.repeat(63)}1`;
const SIGNING_PUBLIC_KEY = '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798';
const AUTH_TAG = JSON.stringify(['auth', 'cd'.repeat(32), 'kind=9', 'ef'.repeat(64)]);
const BASE_CONFIG = {
enabled: true,
relayHttpUrl: 'https://relay.example.test/team',
expectedCommunity: 'relay.example.test',
publicKey: PUBLIC_KEY,
credentialRef: 'env:BUZZ_PRIVATE_KEY',
};
function relayInfo(overrides: Record<string, unknown> = {}) {
return {
software: 'https://github.com/block/buzz',
version: '0.4.24',
supported_nips: [1, 11, 29, 42, 43],
supported_extensions: ['nip-er'],
self: 'cd'.repeat(32),
limitation: { auth_required: true },
...overrides,
};
}
function response(body: unknown, status = 200): Response {
return new Response(typeof body === 'string' ? body : JSON.stringify(body), { status });
}
describe('normalizeBuzzEndpoints', () => {
it.each([
[
'https://relay.example.test',
undefined,
'https://relay.example.test',
'wss://relay.example.test',
'relay.example.test',
],
[
'http://127.0.0.1:3000/team/',
'ws://127.0.0.1:3000/team',
'http://127.0.0.1:3000/team',
'ws://127.0.0.1:3000/team',
'127.0.0.1:3000',
],
[
'https://[2001:db8::1]:8443/buzz',
'wss://[2001:db8::1]:8443/buzz',
'https://[2001:db8::1]:8443/buzz',
'wss://[2001:db8::1]:8443/buzz',
'[2001:db8::1]:8443',
],
])(
'normalizes %s without losing relay identity',
(relayHttpUrl, relayWebSocketUrl, httpUrl, webSocketUrl, community) => {
expect(normalizeBuzzEndpoints({ relayHttpUrl, relayWebSocketUrl })).toMatchObject({
httpUrl,
webSocketUrl,
community,
});
}
);
it.each([
{
relayHttpUrl: 'https://relay.example.test',
relayWebSocketUrl: 'wss://other.example.test',
},
{
relayHttpUrl: 'https://relay.example.test/a',
relayWebSocketUrl: 'wss://relay.example.test/b',
},
{ relayHttpUrl: 'https://user:secret@relay.example.test' },
{ relayHttpUrl: 'ftp://relay.example.test' },
])('rejects ambiguous endpoint pairs', (input) => {
expect(() => normalizeBuzzEndpoints(input)).toThrow();
});
});
describe('buildBuzzCommandEnvironment', () => {
it('passes only process-discovery essentials and strips credentials', () => {
expect(
buildBuzzCommandEnvironment({
PATH: '/usr/local/bin:/usr/bin',
LANG: 'en_US.UTF-8',
BUZZ_PRIVATE_KEY: 'must-not-pass',
BUZZ_AUTH_TAG: 'must-not-pass',
OPENAI_API_KEY: 'must-not-pass',
})
).toEqual({
PATH: '/usr/local/bin:/usr/bin',
LANG: 'en_US.UTF-8',
NO_COLOR: '1',
});
});
});
describe('BuzzCompatibilityService', () => {
it('verifies relay, identity, membership, and read capability without a mutation', async () => {
const fetch = vi
.fn()
.mockResolvedValueOnce(response(relayInfo()))
.mockResolvedValueOnce(response([]))
.mockResolvedValueOnce(response([]));
const service = new BuzzCompatibilityService({
fetch,
resolveSecret: async (reference) =>
reference === 'env:BUZZ_PRIVATE_KEY' ? 'private-secret' : undefined,
signer: {
sign: vi.fn().mockResolvedValue({
authorization: 'Nostr signed-event',
publicKey: PUBLIC_KEY,
}),
},
runCommand: vi
.fn()
.mockRejectedValue(Object.assign(new Error('missing'), { code: 'ENOENT' })),
now: () => new Date('2026-07-23T18:00:00.000Z'),
});
const result = await service.probe(BASE_CONFIG);
expect(result).toMatchObject({
status: 'healthy',
reasonCode: 'ok',
configuredRelayHttpUrl: 'https://relay.example.test/team',
resolvedRelayHttpUrl: 'https://relay.example.test/team',
resolvedRelayWebSocketUrl: 'wss://relay.example.test/team',
observedCommunity: 'relay.example.test',
publicKeyFingerprint: expect.stringMatching(/^[a-f0-9]{12}$/),
checks: {
relayIdentity: 'verified',
communityBinding: 'verified',
configuredIdentity: 'verified',
authentication: 'verified',
membership: 'verified',
channelRead: 'verified',
messageRead: 'verified',
},
contract: {
software: 'https://github.com/block/buzz',
version: '0.4.24',
},
});
expect(fetch).toHaveBeenNthCalledWith(
2,
'https://relay.example.test/team/query',
expect.objectContaining({
method: 'POST',
body: JSON.stringify([{ kinds: [39000], limit: 1 }]),
}),
expect.any(Object)
);
expect(fetch).toHaveBeenNthCalledWith(
3,
'https://relay.example.test/team/query',
expect.objectContaining({
method: 'POST',
body: JSON.stringify([{ kinds: [9], limit: 1 }]),
}),
expect.any(Object)
);
expect(JSON.stringify(result)).not.toContain('private-secret');
expect(JSON.stringify(result)).not.toContain('signed-event');
});
it('uses the built-in Buzz signer for the authenticated read-only probe', async () => {
const fetch = vi
.fn()
.mockResolvedValueOnce(response(relayInfo()))
.mockResolvedValueOnce(response([]))
.mockResolvedValueOnce(response([]));
const service = new BuzzCompatibilityService({
fetch,
resolveSecret: async () => SIGNING_KEY,
runCommand: vi
.fn()
.mockRejectedValue(Object.assign(new Error('missing'), { code: 'ENOENT' })),
});
const result = await service.probe({
...BASE_CONFIG,
publicKey: SIGNING_PUBLIC_KEY,
});
expect(result).toMatchObject({
status: 'healthy',
reasonCode: 'ok',
checks: {
configuredIdentity: 'verified',
authentication: 'verified',
membership: 'verified',
channelRead: 'verified',
messageRead: 'verified',
},
});
const queryCall = fetch.mock.calls[1];
const authorization = new Headers(queryCall?.[1]?.headers).get('authorization');
expect(authorization).toMatch(/^Nostr [A-Za-z0-9+/]+=*$/);
if (!authorization) throw new Error('Expected Buzz NIP-98 authorization');
const event = JSON.parse(
Buffer.from(authorization.slice('Nostr '.length), 'base64').toString('utf8')
);
expect(verifyEvent(event)).toBe(true);
expect(event).toMatchObject({
kind: 27_235,
pubkey: SIGNING_PUBLIC_KEY,
content: '',
tags: expect.arrayContaining([
['u', 'https://relay.example.test/team/query'],
['method', 'POST'],
['payload', expect.stringMatching(/^[a-f0-9]{64}$/)],
['nonce', expect.stringMatching(/^[a-f0-9-]{36}$/)],
]),
});
expect(JSON.stringify(result)).not.toContain(SIGNING_KEY);
expect(JSON.stringify(queryCall)).not.toContain(SIGNING_KEY);
});
it('classifies relay-side authentication rejection without attempting reads', async () => {
const fetch = vi
.fn()
.mockResolvedValueOnce(response(relayInfo()))
.mockResolvedValueOnce(response({ error: 'invalid_or_expired_auth' }, 401));
const service = new BuzzCompatibilityService({
fetch,
resolveSecret: async () => 'private-secret',
signer: {
sign: vi.fn().mockResolvedValue({
authorization: 'Nostr signed-event',
publicKey: PUBLIC_KEY,
}),
},
runCommand: vi
.fn()
.mockRejectedValue(Object.assign(new Error('missing'), { code: 'ENOENT' })),
});
expect(await service.probe(BASE_CONFIG)).toMatchObject({
status: 'unauthorized',
reasonCode: 'authentication_rejected',
checks: {
authentication: 'failed',
channelRead: 'unverified',
messageRead: 'unverified',
},
});
expect(fetch).toHaveBeenCalledTimes(2);
});
it('classifies membership denial independently from authentication', async () => {
const fetch = vi
.fn()
.mockResolvedValueOnce(response(relayInfo()))
.mockResolvedValueOnce(
response(
{
error: 'relay_membership_required',
message: 'You must be a relay member',
},
403
)
);
const service = new BuzzCompatibilityService({
fetch,
resolveSecret: async () => 'private-secret',
signer: {
sign: vi.fn().mockResolvedValue({
authorization: 'Nostr signed-event',
publicKey: PUBLIC_KEY,
}),
},
runCommand: vi
.fn()
.mockRejectedValue(Object.assign(new Error('missing'), { code: 'ENOENT' })),
});
expect(await service.probe(BASE_CONFIG)).toMatchObject({
status: 'not_member',
reasonCode: 'relay_membership_required',
checks: {
relayIdentity: 'verified',
communityBinding: 'unverified',
configuredIdentity: 'verified',
authentication: 'verified',
membership: 'failed',
},
});
});
it('preserves a verified channel read when message reads are denied', async () => {
const fetch = vi
.fn()
.mockResolvedValueOnce(response(relayInfo()))
.mockResolvedValueOnce(response([]))
.mockResolvedValueOnce(response({ error: 'read_denied' }, 403));
const service = new BuzzCompatibilityService({
fetch,
resolveSecret: async () => 'private-secret',
signer: {
sign: vi.fn().mockResolvedValue({
authorization: 'Nostr signed-event',
publicKey: PUBLIC_KEY,
}),
},
runCommand: vi
.fn()
.mockRejectedValue(Object.assign(new Error('missing'), { code: 'ENOENT' })),
});
expect(await service.probe(BASE_CONFIG)).toMatchObject({
status: 'unauthorized',
reasonCode: 'read_capability_rejected',
checks: {
authentication: 'verified',
membership: 'verified',
channelRead: 'verified',
messageRead: 'failed',
},
});
});
it('fails before network access when the expected community does not match', async () => {
const fetch = vi.fn();
const service = new BuzzCompatibilityService({
fetch,
runCommand: vi
.fn()
.mockRejectedValue(Object.assign(new Error('missing'), { code: 'ENOENT' })),
});
expect(
await service.probe({
...BASE_CONFIG,
expectedCommunity: 'other.example.test',
})
).toMatchObject({
status: 'misconfigured',
reasonCode: 'community_mismatch',
checks: { communityBinding: 'failed' },
});
expect(fetch).not.toHaveBeenCalled();
});
it('validates a referenced NIP-OA auth tag before forwarding it', async () => {
const fetch = vi
.fn()
.mockResolvedValueOnce(response(relayInfo()))
.mockResolvedValueOnce(response([]))
.mockResolvedValueOnce(response([]));
const service = new BuzzCompatibilityService({
fetch,
resolveSecret: async (reference) =>
reference === 'env:BUZZ_AUTH_TAG'
? JSON.stringify(JSON.parse(AUTH_TAG), null, 2)
: 'private-secret',
signer: {
sign: vi.fn().mockResolvedValue({
authorization: 'Nostr signed-event',
publicKey: PUBLIC_KEY,
}),
},
runCommand: vi
.fn()
.mockRejectedValue(Object.assign(new Error('missing'), { code: 'ENOENT' })),
});
expect(await service.probe({ ...BASE_CONFIG, authTagRef: 'env:BUZZ_AUTH_TAG' })).toMatchObject({
status: 'healthy',
reasonCode: 'ok',
});
expect(fetch).toHaveBeenNthCalledWith(
2,
expect.any(String),
expect.objectContaining({
headers: expect.objectContaining({ 'x-auth-tag': AUTH_TAG }),
}),
expect.any(Object)
);
expect(fetch).toHaveBeenNthCalledWith(
3,
expect.any(String),
expect.objectContaining({
headers: expect.objectContaining({ 'x-auth-tag': AUTH_TAG }),
}),
expect.any(Object)
);
});
it.each([
'not-json',
JSON.stringify(['auth', 'CD'.repeat(32), '', 'ef'.repeat(64)]),
JSON.stringify(['auth', 'cd'.repeat(32), 'kind=01', 'ef'.repeat(64)]),
JSON.stringify(['auth', 'cd'.repeat(32), '', 'ef'.repeat(64), 'extra']),
'x'.repeat(1_025),
])('fails closed on an invalid referenced NIP-OA auth tag', async (authTag) => {
const fetch = vi.fn().mockResolvedValue(response(relayInfo()));
const service = new BuzzCompatibilityService({
fetch,
resolveSecret: async (reference) =>
reference === 'env:BUZZ_AUTH_TAG' ? authTag : 'private-secret',
signer: {
sign: vi.fn().mockResolvedValue({
authorization: 'Nostr signed-event',
publicKey: PUBLIC_KEY,
}),
},
runCommand: vi
.fn()
.mockRejectedValue(Object.assign(new Error('missing'), { code: 'ENOENT' })),
});
const result = await service.probe({ ...BASE_CONFIG, authTagRef: 'env:BUZZ_AUTH_TAG' });
expect(result).toMatchObject({ status: 'misconfigured', reasonCode: 'auth_tag_invalid' });
expect(fetch).toHaveBeenCalledTimes(1);
expect(JSON.stringify(result)).not.toContain(authTag);
});
it('distinguishes an invalid query response from invalid relay metadata', async () => {
const service = new BuzzCompatibilityService({
fetch: vi
.fn()
.mockResolvedValueOnce(response(relayInfo()))
.mockResolvedValueOnce(response({ unexpected: true })),
resolveSecret: async () => 'private-secret',
signer: {
sign: vi.fn().mockResolvedValue({
authorization: 'Nostr signed-event',
publicKey: PUBLIC_KEY,
}),
},
runCommand: vi
.fn()
.mockRejectedValue(Object.assign(new Error('missing'), { code: 'ENOENT' })),
});
expect(await service.probe(BASE_CONFIG)).toMatchObject({
status: 'unsupported',
reasonCode: 'query_response_invalid',
checks: {
relayIdentity: 'verified',
communityBinding: 'verified',
authentication: 'verified',
membership: 'verified',
channelRead: 'unverified',
messageRead: 'unverified',
},
});
});
it('fails closed on unsupported builds while retaining safe relay evidence', async () => {
const service = new BuzzCompatibilityService({
fetch: vi.fn().mockResolvedValue(response(relayInfo({ version: '0.5.0' }))),
resolveSecret: async () => 'must-not-be-read',
runCommand: vi
.fn()
.mockRejectedValue(Object.assign(new Error('missing'), { code: 'ENOENT' })),
});
expect(await service.probe(BASE_CONFIG)).toMatchObject({
status: 'unsupported',
reasonCode: 'relay_version_unsupported',
contract: { version: '0.5.0' },
checks: { relayIdentity: 'verified', authentication: 'unverified' },
});
});
it('does not leak secrets from signer or relay errors', async () => {
const secret = 'nsec1thismustneverappear';
const service = new BuzzCompatibilityService({
fetch: vi.fn().mockResolvedValue(response(relayInfo())),
resolveSecret: async () => secret,
signer: {
sign: vi.fn().mockRejectedValue(new Error(`invalid private key ${secret}`)),
},
runCommand: vi
.fn()
.mockRejectedValue(Object.assign(new Error('missing'), { code: 'ENOENT' })),
});
const result = await service.probe(BASE_CONFIG);
expect(result.status).toBe('unreachable');
expect(JSON.stringify(result)).not.toContain(secret);
});
it('invalidates evidence when identity, reference, command, or contract changes', async () => {
const probe = async (
change: Partial<typeof BASE_CONFIG> & { command?: { executable: string } } = {},
version = '0.4.24',
observedPublicKey = change.publicKey ?? PUBLIC_KEY
) => {
const service = new BuzzCompatibilityService({
fetch: vi
.fn()
.mockResolvedValueOnce(response(relayInfo({ version })))
.mockResolvedValueOnce(response([]))
.mockResolvedValueOnce(response([])),
resolveSecret: async () => 'private-secret',
signer: {
sign: vi.fn().mockResolvedValue({
authorization: 'Nostr signed-event',
publicKey: observedPublicKey,
}),
},
runCommand: vi.fn().mockResolvedValue({ stdout: 'buzz 0.4.24', stderr: '' }),
});
return service.probe({ ...BASE_CONFIG, ...change });
};
const baseline = await probe();
expect((await probe({ publicKey: 'cd'.repeat(32) })).evidenceKey).not.toBe(
baseline.evidenceKey
);
expect((await probe({ credentialRef: 'env:OTHER_KEY' })).evidenceKey).not.toBe(
baseline.evidenceKey
);
expect((await probe({ command: { executable: '/opt/buzz' } })).evidenceKey).not.toBe(
baseline.evidenceKey
);
expect((await probe({}, '0.5.0')).evidenceKey).not.toBe(baseline.evidenceKey);
const rotatedIdentity = await probe({}, '0.4.24', 'cd'.repeat(32));
expect(rotatedIdentity).toMatchObject({
status: 'misconfigured',
reasonCode: 'public_key_mismatch',
});
expect(rotatedIdentity.evidenceKey).not.toBe(baseline.evidenceKey);
});
it('bounds relay response bodies', async () => {
const service = new BuzzCompatibilityService({
fetch: vi.fn().mockResolvedValue(response('x'.repeat(65))),
resolveSecret: async () => 'unused',
runCommand: vi
.fn()
.mockRejectedValue(Object.assign(new Error('missing'), { code: 'ENOENT' })),
maxResponseBytes: 64,
});
expect(await service.probe(BASE_CONFIG)).toMatchObject({
status: 'unsupported',
reasonCode: 'response_too_large',
});
});
it('enforces a request timeout without exposing the signing-key reference value', async () => {
const fetch = vi.fn(
async (_url: string, init?: RequestInit) =>
new Promise<Response>((_resolve, reject) => {
init?.signal?.addEventListener('abort', () =>
reject(Object.assign(new Error('aborted'), { name: 'AbortError' }))
);
})
);
const service = new BuzzCompatibilityService({
fetch,
resolveSecret: async () => 'unused',
runCommand: vi
.fn()
.mockRejectedValue(Object.assign(new Error('missing'), { code: 'ENOENT' })),
timeoutMs: 5,
});
const result = await service.probe(BASE_CONFIG);
expect(result).toMatchObject({
status: 'unreachable',
reasonCode: 'relay_unreachable',
});
expect(JSON.stringify(result)).not.toContain('unused');
});
it('enforces the timeout while a relay response body is stalled', async () => {
const stalledBody = new ReadableStream<Uint8Array>({
start() {
// Intentionally never enqueue or close.
},
});
const service = new BuzzCompatibilityService({
fetch: vi.fn().mockResolvedValue(
new Response(stalledBody, {
status: 200,
headers: { 'content-type': 'application/json' },
})
),
resolveSecret: async () => 'unused',
runCommand: vi
.fn()
.mockRejectedValue(Object.assign(new Error('missing'), { code: 'ENOENT' })),
timeoutMs: 5,
});
expect(await service.probe(BASE_CONFIG)).toMatchObject({
status: 'unreachable',
reasonCode: 'relay_unreachable',
});
});
it('does not let the private-network opt-in reach link-local metadata ranges', async () => {
const service = new BuzzCompatibilityService({
resolveSecret: async () => 'unused',
runCommand: vi
.fn()
.mockRejectedValue(Object.assign(new Error('missing'), { code: 'ENOENT' })),
});
expect(
await service.probe({
...BASE_CONFIG,
relayHttpUrl: 'http://169.254.170.2',
expectedCommunity: '169.254.170.2',
allowPrivateNetwork: true,
})
).toMatchObject({
status: 'unreachable',
reasonCode: 'network_policy_blocked',
});
});
it('requires an explicit local or private-network opt-in for plaintext HTTP', async () => {
const fetch = vi.fn().mockResolvedValue(null);
const service = new BuzzCompatibilityService({
fetch,
resolveSecret: async () => 'unused',
runCommand: vi
.fn()
.mockRejectedValue(Object.assign(new Error('missing'), { code: 'ENOENT' })),
});
expect(
await service.probe({
...BASE_CONFIG,
relayHttpUrl: 'http://relay.example.test',
expectedCommunity: 'relay.example.test',
})
).toMatchObject({
status: 'unreachable',
reasonCode: 'network_policy_blocked',
});
expect(fetch).toHaveBeenCalledWith(
'http://relay.example.test/info',
expect.any(Object),
expect.objectContaining({ allowHttp: false })
);
});
it.each([
['/opt/buzz/bin/buzz-agent', ['--profile', 'linux']],
['/Applications/Buzz.app/Contents/MacOS/Buzz', ['--profile', 'macOS']],
['C:\\Program Files\\Buzz\\buzz.exe', ['--profile', 'Local Profile']],
])(
'passes cross-platform executable path %s and argv without a shell',
async (executable, args) => {
const runCommand = vi.fn().mockResolvedValue({ stdout: 'buzz 0.4.24', stderr: '' });
const service = new BuzzCompatibilityService({
fetch: vi.fn().mockResolvedValue(response(relayInfo({ version: '0.5.0' }))),
resolveSecret: async () => 'unused',
runCommand,
});
await service.probe({
...BASE_CONFIG,
command: {
executable,
args,
},
});
expect(runCommand).toHaveBeenCalledWith(executable, [...args, '--version']);
}
);
it('strips terminal controls from optional command diagnostics', async () => {
const service = new BuzzCompatibilityService({
fetch: vi.fn().mockResolvedValue(response(relayInfo({ version: '0.5.0' }))),
resolveSecret: async () => 'unused',
runCommand: vi.fn().mockResolvedValue({ stdout: '\u001b[2Jbuzz 0.4.24', stderr: '' }),
});
const result = await service.probe(BASE_CONFIG);
expect(result.commands[0]).toMatchObject({
available: true,
version: 'buzz 0.4.24',
});
expect(JSON.stringify(result)).not.toContain('\u001b');
});
});

View file

@ -0,0 +1,82 @@
import { describe, expect, it } from 'vitest';
import { verifyEvent, type VerifiedEvent } from 'nostr-tools';
import { NostrToolsBuzzNip98Signer } from '../services/buzz-nip98-signer.js';
const PRIVATE_KEY_HEX = `${'0'.repeat(63)}1`;
const PRIVATE_KEY_NSEC = 'nsec1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsmhltgl';
const PUBLIC_KEY_HEX = '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798';
const QUERY_BODY = '[{"kinds":[39000],"limit":1},{"kinds":[9],"limit":1}]';
const QUERY_BODY_SHA256 = '3cf7aa0af5a4e941e20c0e001869ee85e8c5888566b30c03696c1d52fdbb2355';
const NONCE = '03ef8155-d4c5-4f67-9cb8-bfcf94ed702c';
function decodeAuthorization(value: string): VerifiedEvent {
expect(value.startsWith('Nostr ')).toBe(true);
return JSON.parse(Buffer.from(value.slice('Nostr '.length), 'base64').toString('utf8'));
}
function signer(): NostrToolsBuzzNip98Signer {
return new NostrToolsBuzzNip98Signer({
now: () => 1_770_000_000_000,
createNonce: () => NONCE,
});
}
describe('NostrToolsBuzzNip98Signer', () => {
it('creates the exact Buzz NIP-98 proof for a hexadecimal private key', async () => {
const result = await signer().sign({
privateKey: PRIVATE_KEY_HEX,
method: 'POST',
url: 'https://relay.example.test/team/query',
body: QUERY_BODY,
});
const event = decodeAuthorization(result.authorization);
expect(result.publicKey).toBe(PUBLIC_KEY_HEX);
expect(event).toMatchObject({
kind: 27_235,
created_at: 1_770_000_000,
content: '',
pubkey: PUBLIC_KEY_HEX,
tags: [
['u', 'https://relay.example.test/team/query'],
['method', 'POST'],
['nonce', NONCE],
['payload', QUERY_BODY_SHA256],
],
});
expect(verifyEvent(event)).toBe(true);
});
it('accepts the equivalent nsec identity without changing the public contract', async () => {
const result = await signer().sign({
privateKey: PRIVATE_KEY_NSEC,
method: 'POST',
url: 'https://relay.example.test/query',
body: '[]',
});
expect(result.publicKey).toBe(PUBLIC_KEY_HEX);
expect(verifyEvent(decodeAuthorization(result.authorization))).toBe(true);
});
it.each(['not-a-private-key', '1'.repeat(63), '0'.repeat(64), `nsec1${'secret'.repeat(20)}`])(
'rejects invalid private material without echoing it',
async (privateKey) => {
let error: unknown;
try {
await signer().sign({
privateKey,
method: 'POST',
url: 'https://relay.example.test/query',
body: '[]',
});
} catch (caught) {
error = caught;
}
expect(error).toBeInstanceOf(Error);
expect((error as Error).message).toBe('Invalid Buzz private key format');
expect((error as Error).message).not.toContain(privateKey);
}
);
});

View file

@ -0,0 +1,107 @@
import { describe, expect, it } from 'vitest';
import {
buzzAdapterConfigSchema,
communicationAdapterConfigSchema,
} from '../schemas/communication-adapter-schemas.js';
describe('communication adapter schemas', () => {
it('accepts a reference-only Buzz configuration', () => {
expect(
buzzAdapterConfigSchema.parse({
kind: 'buzz',
relayHttpUrl: 'https://relay.example.test/team',
relayWebSocketUrl: 'wss://relay.example.test/team',
expectedCommunity: 'relay.example.test',
publicKey: 'ab'.repeat(32),
credentialRef: 'env:BUZZ_PRIVATE_KEY',
authTagRef: 'env:BUZZ_AUTH_TAG',
command: {
executable: 'C:\\Program Files\\Buzz\\buzz.exe',
args: ['--json'],
},
})
).toMatchObject({
kind: 'buzz',
credentialRef: 'env:BUZZ_PRIVATE_KEY',
});
});
it('accepts explicit nulls to clear optional Buzz configuration', () => {
expect(
buzzAdapterConfigSchema.parse({
kind: 'buzz',
relayHttpUrl: 'https://relay.example.test',
publicKey: 'ab'.repeat(32),
credentialRef: 'env:BUZZ_PRIVATE_KEY',
relayWebSocketUrl: null,
expectedCommunity: null,
authTagRef: null,
command: null,
})
).toMatchObject({
relayWebSocketUrl: null,
expectedCommunity: null,
authTagRef: null,
command: null,
});
});
it.each([
['raw secret field', { credential: 'secret' }],
['URL userinfo', { relayHttpUrl: 'https://user:secret@relay.example.test' }],
['URL query', { relayHttpUrl: 'https://relay.example.test?token=secret' }],
['ambiguous secret reference', { credentialRef: 'BUZZ_PRIVATE_KEY' }],
['invalid public key', { publicKey: 'npub-not-accepted-here' }],
['unknown field', { apiTokenRef: 'env:BUZZ_TOKEN' }],
[
'credential-bearing command argument',
{ command: { executable: 'buzz', args: ['--private-key', 'nsec1secret'] } },
],
[
'credential-bearing executable path',
{ command: { executable: '/tmp/nsec1secret/buzz', args: [] } },
],
[
'bare hexadecimal private key argument',
{ command: { executable: 'buzz', args: ['ab'.repeat(32)] } },
],
[
'serialized NIP-OA auth tag argument',
{
command: {
executable: 'buzz',
args: [JSON.stringify(['auth', 'ab'.repeat(32), 'kind=9', 'cd'.repeat(64)])],
},
},
],
[
'shell executable with user-controlled arguments',
{ command: { executable: '/bin/sh', args: ['-c', 'echo unsafe'] } },
],
['command control characters', { command: { executable: 'buzz\u001b[31m', args: [] } }],
['relay URL control characters', { relayHttpUrl: 'https://relay.example.test/\u001b' }],
])('rejects %s', (_name, change) => {
const result = buzzAdapterConfigSchema.safeParse({
kind: 'buzz',
relayHttpUrl: 'https://relay.example.test',
publicKey: 'ab'.repeat(32),
credentialRef: 'env:BUZZ_PRIVATE_KEY',
...change,
});
expect(result.success).toBe(false);
});
it('keeps the existing Microsoft Teams input compatible', () => {
expect(
communicationAdapterConfigSchema.parse({
displayName: 'Microsoft Teams',
deliveryMode: 'webhook',
webhookUrl: 'https://example.test/hook',
credential: 'write-only-secret',
})
).toMatchObject({
displayName: 'Microsoft Teams',
deliveryMode: 'webhook',
});
});
});

View file

@ -5,6 +5,7 @@ import path from 'path';
import type { ChatService } from '../services/chat-service.js';
import type { OutboundIntegrationService } from '../services/outbound-integration-service.js';
import { CommunicationAdapterService } from '../services/communication-adapter-service.js';
import type { BuzzCompatibilityService } from '../services/buzz-compatibility-service.js';
describe('CommunicationAdapterService', () => {
let tmpDir: string;
@ -180,4 +181,222 @@ describe('CommunicationAdapterService', () => {
expect(chatService.sendSquadMessage).not.toHaveBeenCalled();
expect(await service.listMappings('msteams-default')).toEqual([]);
});
it('stores only Buzz secret references and returns exact probe health', async () => {
const buzzCompatibility = {
probe: vi.fn().mockResolvedValue({
schemaVersion: 'buzz-compatibility/v1',
probeRevision: 1,
testedRelease: '0.4.24',
testedCommit: '710ed9fff57878a1d69f809b80a6ee0416c53fc4',
status: 'healthy',
reasonCode: 'ok',
detail: 'Buzz relay and read capabilities are compatible.',
configuredRelayHttpUrl: 'https://relay.example.test',
resolvedRelayHttpUrl: 'https://relay.example.test',
resolvedRelayWebSocketUrl: 'wss://relay.example.test',
expectedCommunity: 'relay.example.test',
observedCommunity: 'relay.example.test',
publicKeyFingerprint: 'abc123abc123',
checks: {
relayIdentity: 'verified',
communityBinding: 'verified',
configuredIdentity: 'verified',
authentication: 'verified',
membership: 'verified',
channelRead: 'verified',
messageRead: 'verified',
},
commands: [],
evidenceKey: 'evidence-key',
checkedAt: '2026-07-23T18:00:00.000Z',
}),
} as unknown as BuzzCompatibilityService;
service = new CommunicationAdapterService({
storageDir: tmpDir,
persist: true,
chatService: chatService as ChatService,
outboundIntegrations: outboundIntegrations as OutboundIntegrationService,
buzzCompatibility,
audit,
});
const adapter = await service.configureAdapter('buzz-default', {
kind: 'buzz',
enabled: true,
relayHttpUrl: 'https://relay.example.test',
expectedCommunity: 'relay.example.test',
publicKey: 'ab'.repeat(32),
credentialRef: 'env:BUZZ_PRIVATE_KEY',
authTagRef: 'env:BUZZ_AUTH_TAG',
});
const health = await service.checkHealth('buzz-default');
const persisted = await fs.readFile(path.join(tmpDir, 'state.json'), 'utf-8');
expect(adapter).toMatchObject({
kind: 'buzz',
relayHttpUrl: 'https://relay.example.test',
credentialRef: 'env:BUZZ_PRIVATE_KEY',
authTagConfigured: true,
hasCredential: true,
});
expect(adapter.relayWebSocketUrl).toBeUndefined();
expect(health).toMatchObject({
status: 'healthy',
reasonCode: 'ok',
canSend: false,
canReceiveReplies: false,
});
expect(persisted).toContain('env:BUZZ_PRIVATE_KEY');
expect(persisted).not.toContain('nsec');
expect(persisted).not.toContain('raw-access-token');
const cleared = await service.configureAdapter('buzz-default', {
kind: 'buzz',
relayHttpUrl: 'https://relay.example.test',
publicKey: 'ab'.repeat(32),
credentialRef: 'env:BUZZ_PRIVATE_KEY',
relayWebSocketUrl: null,
expectedCommunity: null,
authTagRef: null,
command: null,
});
expect(cleared).toMatchObject({
authTagConfigured: false,
});
expect(cleared.relayWebSocketUrl).toBeUndefined();
expect(cleared.expectedCommunity).toBeUndefined();
expect(cleared.authTagRef).toBeUndefined();
expect(cleared.command).toBeUndefined();
});
it('quarantines an invalid persisted Buzz command before it can execute', async () => {
await service.configureAdapter('buzz-default', {
kind: 'buzz',
enabled: true,
relayHttpUrl: 'https://relay.example.test',
publicKey: 'ab'.repeat(32),
credentialRef: 'env:BUZZ_PRIVATE_KEY',
command: { executable: '/opt/buzz/bin/buzz-agent', args: ['--profile', 'safe'] },
});
const statePath = path.join(tmpDir, 'state.json');
const persisted = JSON.parse(await fs.readFile(statePath, 'utf8'));
persisted.adapters['buzz-default'].command = {
executable: '/bin/sh',
args: ['-c', 'echo unsafe'],
};
await fs.writeFile(statePath, JSON.stringify(persisted));
const probe = vi.fn();
const reloaded = new CommunicationAdapterService({
storageDir: tmpDir,
persist: true,
chatService: chatService as ChatService,
outboundIntegrations: outboundIntegrations as OutboundIntegrationService,
buzzCompatibility: { probe } as unknown as BuzzCompatibilityService,
audit,
});
expect(await reloaded.getAdapter('buzz-default')).toMatchObject({
kind: 'buzz',
enabled: false,
hasCredential: false,
});
expect((await reloaded.getAdapter('buzz-default'))?.command).toBeUndefined();
expect(await reloaded.checkHealth('buzz-default')).toMatchObject({
status: 'misconfigured',
reasonCode: 'configuration_invalid',
});
expect(probe).not.toHaveBeenCalled();
expect(await fs.readFile(statePath, 'utf8')).not.toContain('/bin/sh');
});
it('invalidates persisted Buzz compatibility from an older probe contract', async () => {
const buzzCompatibility = {
probe: vi.fn().mockResolvedValue({
schemaVersion: 'buzz-compatibility/v1',
probeRevision: 1,
testedRelease: '0.4.24',
testedCommit: '710ed9fff57878a1d69f809b80a6ee0416c53fc4',
status: 'healthy',
reasonCode: 'ok',
detail: 'compatible',
configuredRelayHttpUrl: 'https://relay.example.test',
publicKeyFingerprint: 'abc123abc123',
checks: {
relayIdentity: 'verified',
communityBinding: 'verified',
configuredIdentity: 'verified',
authentication: 'verified',
membership: 'verified',
channelRead: 'verified',
messageRead: 'verified',
},
commands: [],
evidenceKey: 'evidence-key',
checkedAt: '2026-07-23T18:00:00.000Z',
}),
} as unknown as BuzzCompatibilityService;
const configured = new CommunicationAdapterService({
storageDir: tmpDir,
persist: true,
chatService: chatService as ChatService,
outboundIntegrations: outboundIntegrations as OutboundIntegrationService,
buzzCompatibility,
audit,
});
await configured.configureAdapter('buzz-default', {
kind: 'buzz',
relayHttpUrl: 'https://relay.example.test',
publicKey: 'ab'.repeat(32),
credentialRef: 'env:BUZZ_PRIVATE_KEY',
});
await configured.checkHealth('buzz-default');
const statePath = path.join(tmpDir, 'state.json');
const persisted = JSON.parse(await fs.readFile(statePath, 'utf8'));
persisted.adapters['buzz-default'].compatibility.probeRevision = 0;
await fs.writeFile(statePath, JSON.stringify(persisted));
const reloaded = new CommunicationAdapterService({
storageDir: tmpDir,
persist: true,
chatService: chatService as ChatService,
outboundIntegrations: outboundIntegrations as OutboundIntegrationService,
audit,
});
const adapter = await reloaded.getAdapter('buzz-default');
expect(adapter?.compatibility).toBeUndefined();
expect(adapter?.lastHealth).toBeUndefined();
});
it('fails closed for Buzz sends while leaving Microsoft Teams behavior intact', async () => {
await service.configureAdapter('buzz-default', {
kind: 'buzz',
enabled: true,
relayHttpUrl: 'https://relay.example.test',
publicKey: 'ab'.repeat(32),
credentialRef: 'env:BUZZ_PRIVATE_KEY',
});
const result = await service.send('buzz-default', {
target: { kind: 'squad', squadMessageId: 'message-1' },
message: 'must not leave Veritas',
});
expect(result.delivery).toMatchObject({
operation: 'send',
status: 'blocked',
error: expect.stringContaining('not implemented'),
});
expect(await service.listMappings('buzz-default')).toEqual([]);
const poll = await service.pollReplies('buzz-default');
expect(poll.delivery).toMatchObject({
operation: 'poll',
status: 'skipped',
error: expect.stringContaining('Buzz reply polling is not implemented'),
});
expect(outboundIntegrations.deliver).not.toHaveBeenCalled();
});
});

View file

@ -323,6 +323,100 @@ describe('integrations routes', () => {
});
});
it('configures a reference-only Buzz adapter and runs a read-only test probe', async () => {
const buzzRecord = {
id: 'buzz-default',
kind: 'buzz',
displayName: 'Buzz',
enabled: true,
deliveryMode: 'manual',
replyMode: 'ingest-api',
destinationType: 'channel',
relayHttpUrl: 'https://relay.example.test',
relayWebSocketUrl: 'wss://relay.example.test',
expectedCommunity: 'relay.example.test',
publicKey: 'ab'.repeat(32),
publicKeyFingerprint: 'public-fp',
credentialRef: 'env:BUZZ_PRIVATE_KEY',
authTagConfigured: false,
hasCredential: true,
createdAt: '2026-07-23T18:00:00.000Z',
updatedAt: '2026-07-23T18:00:00.000Z',
};
mockCommunicationAdapters.configureAdapter.mockResolvedValueOnce(buzzRecord);
const configured = await request(app)
.put('/api/integrations/communication/adapters/buzz-default')
.send({
kind: 'buzz',
relayHttpUrl: 'https://relay.example.test',
expectedCommunity: 'relay.example.test',
publicKey: 'ab'.repeat(32),
credentialRef: 'env:BUZZ_PRIVATE_KEY',
});
expect(configured.status).toBe(200);
expect(configured.body).toMatchObject({
kind: 'buzz',
credentialRef: 'env:BUZZ_PRIVATE_KEY',
hasCredential: true,
});
expect(JSON.stringify(configured.body)).not.toContain('nsec');
mockCommunicationAdapters.getAdapter
.mockResolvedValueOnce(buzzRecord)
.mockResolvedValueOnce(buzzRecord);
mockCommunicationAdapters.checkHealth.mockResolvedValueOnce({
adapterId: 'buzz-default',
status: 'healthy',
configured: true,
canSend: false,
canReceiveReplies: false,
checkedAt: '2026-07-23T18:01:00.000Z',
detail: 'Buzz read-only compatibility probe passed.',
reasonCode: 'ok',
});
const tested = await request(app)
.post('/api/integrations/communication/adapters/buzz-default/test')
.send({ message: 'must not be sent' });
expect(tested.status).toBe(200);
expect(tested.body).toMatchObject({
status: 'healthy',
canSend: false,
canReceiveReplies: false,
});
expect(mockCommunicationAdapters.checkHealth).toHaveBeenCalledWith('buzz-default');
expect(mockCommunicationAdapters.send).not.toHaveBeenCalledWith(
'buzz-default',
expect.anything()
);
});
it('rejects raw Buzz credentials and API-token fields', async () => {
const res = await request(app)
.put('/api/integrations/communication/adapters/buzz-default')
.send({
kind: 'buzz',
relayHttpUrl: 'https://relay.example.test',
publicKey: 'ab'.repeat(32),
credentialRef: 'env:BUZZ_PRIVATE_KEY',
credential: 'raw-secret',
apiTokenRef: 'env:BUZZ_TOKEN',
});
expect(res.status).toBe(400);
expect(res.body).toMatchObject({
code: 'VALIDATION_ERROR',
message: 'Validation failed',
});
expect(mockCommunicationAdapters.configureAdapter).not.toHaveBeenCalledWith(
'buzz-default',
expect.anything()
);
});
it('ingests communication replies and broadcasts the resulting squad message', async () => {
const res = await request(app)
.post('/api/integrations/communication/adapters/msteams-default/replies')

View file

@ -8,6 +8,7 @@ import {
agentRoutingAccess,
agentTaskAccess,
diffAccess,
integrationsAccess,
maintenanceAccess,
policyAccess,
previewAccess,
@ -109,6 +110,36 @@ describe('v1 REST permission guard presets', () => {
);
});
it('allows Buzz diagnostics to settings readers and guards Buzz mutations', () => {
expect(
runGuard(
integrationsAccess,
mockRequest('GET', '/communication/adapters/buzz-default/health', 'read-only', [
'settings:read',
])
).next
).toHaveBeenCalled();
for (const path of [
'/communication/adapters/buzz-default',
'/communication/adapters/buzz-default/test',
'/communication/adapters/buzz-default/disconnect',
]) {
const method = path.endsWith('buzz-default') ? 'PUT' : 'POST';
const { res, next } = runGuard(
integrationsAccess,
mockRequest(method, path, 'read-only', ['settings:read'])
);
expect(next).not.toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(403);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
details: expect.objectContaining({ required: ['settings:write'] }),
})
);
}
});
it('requires admin permission for destructive telemetry maintenance routes', () => {
const { res, next } = runGuard(activityAccess, mockRequest('DELETE', '/'));

View file

@ -10,11 +10,12 @@ vi.mock('node:dns/promises', () => ({
import { safeFetch, validateWebhookUrl } from '../utils/url-validation.js';
async function listenLocalServer(
handler: Parameters<typeof createServer>[0]
handler: Parameters<typeof createServer>[0],
host = '127.0.0.1'
): Promise<{ server: Server; port: number }> {
const server = createServer(handler);
await new Promise<void>((resolve) => {
server.listen(0, '127.0.0.1', resolve);
server.listen(0, host, resolve);
});
const address = server.address();
if (!address || typeof address === 'string') {
@ -77,6 +78,28 @@ describe('url validation', () => {
}
});
it('connects to an explicitly allowed bracketed IPv6 loopback address', async () => {
const { server, port } = await listenLocalServer((req, res) => {
expect(req.headers.host).toBe(`[::1]:${port}`);
res.writeHead(202, { 'content-type': 'text/plain' });
res.end('accepted-ipv6');
}, '::1');
try {
const response = await safeFetch(
`http://[::1]:${port}/hook`,
{ method: 'POST', body: 'payload' },
{ allowHttp: true, allowLocalhost: true }
);
expect(response?.status).toBe(202);
await expect(response?.text()).resolves.toBe('accepted-ipv6');
expect(mockLookup).not.toHaveBeenCalled();
} finally {
await closeServer(server);
}
});
it.each(['10.0.0.1', '172.16.0.1', '192.168.0.1'])(
'does not treat allowLocalhost as private-network approval for %s',
async (address) => {
@ -106,6 +129,58 @@ describe('url validation', () => {
expect(fetchSpy).not.toHaveBeenCalled();
});
it.each(['10.0.0.1', '172.16.0.1', '192.168.0.1', '[fd00::1]'])(
'allows only private network classes through the narrow private-network option for %s',
(address) => {
expect(
validateWebhookUrl(`https://${address}/hook`, {
allowPrivateNetwork: true,
logFailures: false,
}).valid
).toBe(true);
}
);
it.each([
'169.254.170.2',
'100.64.0.1',
'[fe80::1]',
'[fe90::1]',
'[fea0::1]',
'[febf::1]',
'[::ffff:a9fe:a9fe]',
])(
'keeps link-local and CGNAT destinations blocked under private-network approval for %s',
(address) => {
expect(
validateWebhookUrl(`https://${address}/hook`, {
allowPrivateNetwork: true,
logFailures: false,
}).valid
).toBe(false);
}
);
it.each([
['[::ffff:7f00:1]', { allowLocalhost: true }],
['[::ffff:a00:1]', { allowPrivateNetwork: true }],
])('requires the matching opt-in for IPv4-mapped IPv6 destination %s', (address, allowance) => {
const url = `http://${address}/hook`;
expect(validateWebhookUrl(url, { allowHttp: true, logFailures: false }).valid).toBe(false);
expect(
validateWebhookUrl(url, { allowHttp: true, logFailures: false, ...allowance }).valid
).toBe(true);
});
it('blocks deprecated IPv4-compatible IPv6 local addresses', () => {
expect(
validateWebhookUrl('http://[::7f00:1]/hook', {
allowHttp: true,
logFailures: false,
}).valid
).toBe(false);
});
it('pins outbound fetches to the validated DNS answer', async () => {
const fetchSpy = vi.fn();
vi.stubGlobal('fetch', fetchSpy);

View file

@ -11,6 +11,7 @@ import { z } from 'zod';
import { ConfigService } from '../services/config-service.js';
import { asyncHandler } from '../middleware/async-handler.js';
import { ForbiddenError, NotFoundError } from '../middleware/error-handler.js';
import { validate } from '../middleware/validate.js';
import { hasPermission, type AuthenticatedRequest } from '../middleware/auth.js';
import type { CoolifyServiceConfig, CoolifyServicesConfig } from '@veritas-kanban/shared';
import { broadcastSquadMessage } from '../services/broadcast-service.js';
@ -18,6 +19,10 @@ import {
DEFAULT_ADAPTER_ID,
getCommunicationAdapterService,
} from '../services/communication-adapter-service.js';
import {
communicationAdapterConfigSchema,
type CommunicationAdapterConfig,
} from '../schemas/communication-adapter-schemas.js';
import { getOutboundIntegrationService } from '../services/outbound-integration-service.js';
import { externalTrackerRoutes } from './external-trackers.js';
import { safeFetch } from '../utils/url-validation.js';
@ -54,20 +59,6 @@ const replyTargetSchema = z.object({
notificationId: z.string().optional(),
});
const adapterConfigSchema = z.object({
kind: z.enum(['msteams']).optional(),
displayName: z.string().optional(),
enabled: z.boolean().optional(),
deliveryMode: z.enum(['manual', 'webhook']).optional(),
destinationType: z.enum(['channel', 'direct']).optional(),
tenantId: z.string().optional(),
teamId: z.string().optional(),
channelId: z.string().optional(),
chatId: z.string().optional(),
webhookUrl: z.string().optional(),
credential: z.string().optional(),
});
const sendSchema = z.object({
target: replyTargetSchema,
message: z.string().min(1),
@ -271,9 +262,10 @@ router.get(
// PUT /api/integrations/communication/adapters/:adapterId
router.put(
'/communication/adapters/:adapterId',
validate({ body: communicationAdapterConfigSchema }),
asyncHandler(async (req, res) => {
const adapterId = adapterIdParam(req.params.adapterId);
const input = adapterConfigSchema.parse(req.body);
const input = req.validated?.body as CommunicationAdapterConfig;
res.json(await communicationAdapters.configureAdapter(adapterId, input));
})
);
@ -294,6 +286,11 @@ router.post(
asyncHandler(async (req, res) => {
const adapterId = adapterIdParam(req.params.adapterId);
await ensureAdapterExists(adapterId);
const adapter = await communicationAdapters.getAdapter(adapterId);
if (adapter?.kind === 'buzz') {
res.json(await communicationAdapters.checkHealth(adapterId));
return;
}
const message =
typeof req.body?.message === 'string' && req.body.message.trim()
? req.body.message

View file

@ -0,0 +1,133 @@
import { z } from 'zod';
const optionalTrimmed = (max: number) => z.string().trim().min(1).max(max).optional();
function hasControlCharacters(value: string): boolean {
return Array.from(value).some((character) => {
const code = character.charCodeAt(0);
return code <= 0x1f || (code >= 0x7f && code <= 0x9f);
});
}
const commandTextSchema = z
.string()
.trim()
.min(1)
.max(1000)
.refine(
(value) => !hasControlCharacters(value),
'Command values cannot contain control characters'
);
function isBuzzExecutable(value: string): boolean {
const executableName = value.split(/[\\/]/).at(-1)?.toLowerCase();
return /^(?:buzz|buzz-acp|buzz-agent)(?:\.exe)?$/.test(executableName ?? '');
}
const buzzCommandSchema = z
.object({
executable: commandTextSchema,
args: z.array(commandTextSchema).max(64).optional(),
})
.strict()
.superRefine((command, context) => {
if (!isBuzzExecutable(command.executable)) {
context.addIssue({
code: 'custom',
path: ['executable'],
message: 'Configured command must be a buzz, buzz-acp, or buzz-agent executable',
});
}
const credentialPattern = /(?:private[-_]?key|secret|token|authorization|bearer|nsec1)/i;
const secretHexPattern = /\b(?:[a-f0-9]{128}|[a-f0-9]{64})\b/i;
const executableUnsafe =
credentialPattern.test(command.executable) || secretHexPattern.test(command.executable);
const argumentUnsafe = command.args?.some(
(value) => credentialPattern.test(value) || secretHexPattern.test(value)
);
if (executableUnsafe || argumentUnsafe) {
context.addIssue({
code: 'custom',
path: [executableUnsafe ? 'executable' : 'args'],
message: 'Command configuration cannot contain credential material or credential flags',
});
}
});
const environmentReferenceSchema = z
.string()
.trim()
.regex(/^env:[A-Za-z_][A-Za-z0-9_]*$/, 'Secret references must use env:VARIABLE_NAME syntax');
const relayUrlSchema = z
.string()
.trim()
.min(1)
.max(2048)
.refine((value) => !hasControlCharacters(value), 'Relay URL cannot contain control characters')
.superRefine((value, context) => {
try {
const parsed = new URL(value);
if (!['http:', 'https:', 'ws:', 'wss:'].includes(parsed.protocol)) {
context.addIssue({
code: 'custom',
message: 'Relay URL must use http, https, ws, or wss',
});
}
if (parsed.username || parsed.password || parsed.search || parsed.hash) {
context.addIssue({
code: 'custom',
message: 'Relay URL cannot contain credentials, a query string, or a fragment',
});
}
if (!parsed.hostname) {
context.addIssue({ code: 'custom', message: 'Relay URL must include a host' });
}
} catch {
context.addIssue({ code: 'custom', message: 'Relay URL is invalid' });
}
});
const teamsAdapterConfigSchema = z
.object({
kind: z.literal('msteams').optional(),
displayName: optionalTrimmed(200),
enabled: z.boolean().optional(),
deliveryMode: z.enum(['manual', 'webhook']).optional(),
destinationType: z.enum(['channel', 'direct']).optional(),
tenantId: optionalTrimmed(500),
teamId: optionalTrimmed(500),
channelId: optionalTrimmed(500),
chatId: optionalTrimmed(500),
webhookUrl: optionalTrimmed(2048),
credential: optionalTrimmed(4096),
})
.strict();
export const buzzAdapterConfigSchema = z
.object({
kind: z.literal('buzz'),
displayName: optionalTrimmed(200),
enabled: z.boolean().optional(),
relayHttpUrl: relayUrlSchema,
relayWebSocketUrl: relayUrlSchema.nullable().optional(),
expectedCommunity: optionalTrimmed(500).nullable(),
publicKey: z
.string()
.trim()
.regex(/^[a-fA-F0-9]{64}$/, 'Buzz public key must be 64 hexadecimal characters'),
credentialRef: environmentReferenceSchema,
authTagRef: environmentReferenceSchema.nullable().optional(),
allowLocalhost: z.boolean().optional(),
allowPrivateNetwork: z.boolean().optional(),
command: buzzCommandSchema.nullable().optional(),
})
.strict();
export const communicationAdapterConfigSchema = z.union([
buzzAdapterConfigSchema,
teamsAdapterConfigSchema,
]);
export type CommunicationAdapterConfig = z.infer<typeof communicationAdapterConfigSchema>;
export type BuzzAdapterConfig = z.infer<typeof buzzAdapterConfigSchema>;

View file

@ -13,6 +13,7 @@ export * from './time-schemas.js';
export * from './archive-schemas.js';
export * from './config-schemas.js';
export * from './agent-schemas.js';
export * from './communication-adapter-schemas.js';
export * from './auth-schemas.js';
export * from './shared-resources-schemas.js';
export * from './doc-freshness-schemas.js';

View file

@ -0,0 +1,906 @@
import { createHash } from 'node:crypto';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import type {
BuzzCommandConfig,
BuzzCommandDiagnostic,
BuzzCompatibilityChecks,
BuzzCompatibilityReasonCode,
BuzzCompatibilityResult,
BuzzCompatibilityStatus,
BuzzRelayContract,
} from '@veritas-kanban/shared';
import {
BUZZ_COMPATIBILITY_SCHEMA_VERSION,
BUZZ_PROBE_REVISION,
BUZZ_TESTED_COMMIT,
BUZZ_TESTED_RELEASE,
} from '@veritas-kanban/shared';
import { redactString } from '../lib/redact.js';
import { NostrToolsBuzzNip98Signer, type BuzzNip98Signer } from './buzz-nip98-signer.js';
import { EnvironmentCredentialSecretSource } from './credential-broker-service.js';
import { safeFetch, type UrlValidationOptions } from '../utils/url-validation.js';
const execFileAsync = promisify(execFile);
const BUZZ_REPOSITORY = 'https://github.com/block/buzz';
const DEFAULT_TIMEOUT_MS = 5_000;
const MAX_RESPONSE_BYTES = 256 * 1024;
const MAX_AUTH_TAG_BYTES = 1_024;
const REQUIRED_NIPS = [11, 29, 42];
const COMMAND_ENV_ALLOWLIST = [
'PATH',
'Path',
'PATHEXT',
'SystemRoot',
'SYSTEMROOT',
'WINDIR',
'COMSPEC',
'TMPDIR',
'TEMP',
'TMP',
'LANG',
'LC_ALL',
] as const;
export interface BuzzProbeConfig {
enabled: boolean;
relayHttpUrl: string;
relayWebSocketUrl?: string;
expectedCommunity?: string;
publicKey: string;
credentialRef: string;
authTagRef?: string;
allowLocalhost?: boolean;
allowPrivateNetwork?: boolean;
command?: BuzzCommandConfig;
}
export interface NormalizedBuzzEndpoints {
configuredHttpUrl: string;
configuredWebSocketUrl?: string;
httpUrl: string;
webSocketUrl: string;
community: string;
expectedCommunity?: string;
}
export interface BuzzCommandRunner {
(executable: string, args: string[]): Promise<{ stdout: string; stderr: string }>;
}
export function buildBuzzCommandEnvironment(
environment: NodeJS.ProcessEnv = process.env
): NodeJS.ProcessEnv {
const result: NodeJS.ProcessEnv = { NO_COLOR: '1' };
for (const key of COMMAND_ENV_ALLOWLIST) {
const value = environment[key];
if (typeof value === 'string') result[key] = value;
}
return result;
}
interface BuzzCompatibilityServiceOptions {
fetch?: (
url: string,
init?: RequestInit,
validationOptions?: UrlValidationOptions
) => Promise<Response | null>;
resolveSecret?: (reference: string) => Promise<string | undefined>;
signer?: BuzzNip98Signer;
runCommand?: BuzzCommandRunner;
now?: () => Date;
timeoutMs?: number;
maxResponseBytes?: number;
}
class BuzzProbeError extends Error {
constructor(
readonly status: BuzzCompatibilityStatus,
readonly reasonCode: BuzzCompatibilityReasonCode,
message: string,
readonly remediation?: string
) {
super(message);
}
}
function trimTrailingSlash(pathname: string): string {
if (pathname === '/') return '';
return pathname.endsWith('/') ? pathname.slice(0, -1) : pathname;
}
function normalizedAuthority(url: URL): string {
let hostname = url.hostname.toLowerCase();
if (hostname.endsWith('.')) hostname = hostname.slice(0, -1);
return `${hostname}${url.port ? `:${url.port}` : ''}`;
}
function normalizeCommunity(value: string): string {
const candidate = value.includes('://') ? value : `https://${value}`;
let parsed: URL;
try {
parsed = new URL(candidate);
} catch {
throw new BuzzProbeError(
'misconfigured',
'endpoint_invalid',
'Expected community is not a valid host or URL.',
'Use the Buzz relay host, with an explicit non-default port when required.'
);
}
if (
parsed.username ||
parsed.password ||
parsed.search ||
parsed.hash ||
trimTrailingSlash(parsed.pathname)
) {
throw new BuzzProbeError(
'misconfigured',
'endpoint_invalid',
'Expected community must identify only a host and optional port.',
'Remove credentials, path, query, and fragment components.'
);
}
return normalizedAuthority(parsed);
}
function parseRelayUrl(value: string, label: string): URL {
let parsed: URL;
try {
parsed = new URL(value);
} catch {
throw new BuzzProbeError(
'misconfigured',
'endpoint_invalid',
`${label} is not a valid URL.`,
'Use an http, https, ws, or wss Buzz relay URL.'
);
}
if (
!['http:', 'https:', 'ws:', 'wss:'].includes(parsed.protocol) ||
!parsed.hostname ||
parsed.username ||
parsed.password ||
parsed.search ||
parsed.hash
) {
throw new BuzzProbeError(
'misconfigured',
'endpoint_invalid',
`${label} contains an unsupported or ambiguous component.`,
'Use an http, https, ws, or wss URL without credentials, query, or fragment.'
);
}
parsed.pathname = trimTrailingSlash(parsed.pathname);
return parsed;
}
function baseUrl(url: URL): string {
return `${url.protocol}//${url.host}${trimTrailingSlash(url.pathname)}`;
}
function withProtocol(url: URL, protocol: 'http:' | 'https:' | 'ws:' | 'wss:'): URL {
const copy = new URL(url.toString());
copy.protocol = protocol;
return copy;
}
function httpProtocol(protocol: string): 'http:' | 'https:' {
return protocol === 'https:' || protocol === 'wss:' ? 'https:' : 'http:';
}
function webSocketProtocol(protocol: string): 'ws:' | 'wss:' {
return protocol === 'https:' || protocol === 'wss:' ? 'wss:' : 'ws:';
}
export function normalizeBuzzEndpoints(input: {
relayHttpUrl: string;
relayWebSocketUrl?: string;
expectedCommunity?: string;
}): NormalizedBuzzEndpoints {
const primary = parseRelayUrl(input.relayHttpUrl, 'Buzz relay HTTP URL');
const http = withProtocol(primary, httpProtocol(primary.protocol));
const derivedWebSocket = withProtocol(primary, webSocketProtocol(primary.protocol));
let webSocket = derivedWebSocket;
if (input.relayWebSocketUrl) {
const configuredWebSocket = parseRelayUrl(input.relayWebSocketUrl, 'Buzz relay WebSocket URL');
webSocket = withProtocol(configuredWebSocket, webSocketProtocol(configuredWebSocket.protocol));
const wsAsHttp = withProtocol(webSocket, httpProtocol(webSocket.protocol));
if (
normalizedAuthority(http) !== normalizedAuthority(wsAsHttp) ||
trimTrailingSlash(http.pathname) !== trimTrailingSlash(wsAsHttp.pathname) ||
(http.protocol === 'https:') !== (wsAsHttp.protocol === 'https:')
) {
throw new BuzzProbeError(
'misconfigured',
'endpoint_mismatch',
'Buzz HTTP and WebSocket endpoints do not identify the same relay authority and path.',
'Configure matching http/ws or https/wss endpoints for the same host, port, and path.'
);
}
}
const community = normalizedAuthority(http);
const expectedCommunity = input.expectedCommunity
? normalizeCommunity(input.expectedCommunity)
: undefined;
return {
configuredHttpUrl: input.relayHttpUrl,
configuredWebSocketUrl: input.relayWebSocketUrl,
httpUrl: baseUrl(http),
webSocketUrl: baseUrl(webSocket),
community,
expectedCommunity,
};
}
export function fingerprintBuzzPublicKey(publicKey: string): string {
return createHash('sha256').update(publicKey.toLowerCase()).digest('hex').slice(0, 12);
}
function endpoint(base: string, path: string): string {
return `${base}${path}`;
}
function emptyChecks(): BuzzCompatibilityChecks {
return {
relayIdentity: 'unverified',
communityBinding: 'unverified',
configuredIdentity: 'unverified',
authentication: 'unverified',
membership: 'unverified',
channelRead: 'unverified',
messageRead: 'unverified',
};
}
function stripTerminalControlSequences(value: string): string {
let result = '';
let index = 0;
while (index < value.length) {
const code = value.charCodeAt(index);
if (code === 0x1b) {
index += 1;
const introducer = value[index];
if (introducer === '[') {
index += 1;
while (index < value.length) {
const sequenceCode = value.charCodeAt(index);
index += 1;
if (sequenceCode >= 0x40 && sequenceCode <= 0x7e) break;
}
} else if (introducer === ']') {
index += 1;
while (index < value.length) {
if (value.charCodeAt(index) === 0x07) {
index += 1;
break;
}
if (value.charCodeAt(index) === 0x1b && value.charCodeAt(index + 1) === 0x5c) {
index += 2;
break;
}
index += 1;
}
} else if (index < value.length) {
index += 1;
}
continue;
}
if (code <= 0x1f || (code >= 0x7f && code <= 0x9f)) {
index += 1;
continue;
}
result += value[index];
index += 1;
}
return result;
}
function sanitizeDetail(value: unknown): string {
const raw = value instanceof Error ? value.message : String(value);
const withoutControls = stripTerminalControlSequences(raw);
return redactString(withoutControls)
.replace(/nsec1[a-z0-9]+/gi, '[REDACTED]')
.slice(0, 500);
}
function sanitizeConfiguredUrl(value: string | undefined): string | undefined {
if (!value) return undefined;
try {
const parsed = new URL(value);
parsed.username = '';
parsed.password = '';
parsed.search = '';
parsed.hash = '';
return baseUrl(parsed);
} catch {
return '[invalid-url]';
}
}
function sanitizeConfiguredCommunity(value: string | undefined): string | undefined {
if (!value) return undefined;
try {
return normalizeCommunity(value);
} catch {
return '[invalid-community]';
}
}
function parseCanonicalDecimal(value: string, max: number): number | undefined {
if (!/^(?:0|[1-9][0-9]*)$/.test(value)) return undefined;
const parsed = Number(value);
return Number.isSafeInteger(parsed) && parsed <= max ? parsed : undefined;
}
function isValidAuthTagConditions(value: string): boolean {
if (!value) return true;
return value.split('&').every((clause) => {
if (clause.startsWith('kind=')) {
return parseCanonicalDecimal(clause.slice('kind='.length), 65_535) !== undefined;
}
if (clause.startsWith('created_at<')) {
return parseCanonicalDecimal(clause.slice('created_at<'.length), 4_294_967_295) !== undefined;
}
if (clause.startsWith('created_at>')) {
return parseCanonicalDecimal(clause.slice('created_at>'.length), 4_294_967_295) !== undefined;
}
return false;
});
}
export function validateBuzzAuthTag(value: string): string {
if (Buffer.byteLength(value, 'utf8') > MAX_AUTH_TAG_BYTES) {
throw new BuzzProbeError(
'misconfigured',
'auth_tag_invalid',
'The configured Buzz NIP-OA auth tag exceeds the 1,024-byte limit.',
'Replace the referenced value with a valid Buzz NIP-OA auth tag.'
);
}
let parsed: unknown;
try {
parsed = JSON.parse(value);
} catch {
throw new BuzzProbeError(
'misconfigured',
'auth_tag_invalid',
'The configured Buzz NIP-OA auth tag is not valid JSON.',
'Set the referenced value to the four-field Buzz NIP-OA auth-tag array.'
);
}
if (
!Array.isArray(parsed) ||
parsed.length !== 4 ||
parsed[0] !== 'auth' ||
typeof parsed[1] !== 'string' ||
typeof parsed[2] !== 'string' ||
typeof parsed[3] !== 'string' ||
!/^[a-f0-9]{64}$/.test(parsed[1]) ||
!isValidAuthTagConditions(parsed[2]) ||
!/^[a-f0-9]{128}$/.test(parsed[3])
) {
throw new BuzzProbeError(
'misconfigured',
'auth_tag_invalid',
'The configured Buzz NIP-OA auth tag does not match the supported four-field contract.',
'Regenerate the auth tag with Buzz and update the referenced environment secret.'
);
}
return JSON.stringify(parsed);
}
async function readBoundedBody(
response: Response,
limit: number,
signal?: AbortSignal
): Promise<string> {
if (!response.body) return '';
const reader = response.body.getReader();
const decoder = new TextDecoder();
let bytes = 0;
let result = '';
const read = async () => {
if (!signal) return reader.read();
if (signal.aborted) throw Object.assign(new Error('aborted'), { name: 'AbortError' });
return new Promise<Awaited<ReturnType<typeof reader.read>>>((resolve, reject) => {
const onAbort = () => {
void reader.cancel().catch(() => {});
reject(Object.assign(new Error('aborted'), { name: 'AbortError' }));
};
signal.addEventListener('abort', onAbort, { once: true });
reader
.read()
.then(resolve, reject)
.finally(() => {
signal.removeEventListener('abort', onAbort);
});
});
};
try {
while (true) {
const chunk = await read();
if (chunk.done) break;
bytes += chunk.value.byteLength;
if (bytes > limit) {
await reader.cancel();
throw new BuzzProbeError(
'unsupported',
'response_too_large',
`Buzz relay response exceeded the ${limit}-byte diagnostic limit.`,
'Inspect the relay or reverse proxy response before retrying.'
);
}
result += decoder.decode(chunk.value, { stream: true });
}
return result + decoder.decode();
} finally {
reader.releaseLock();
}
}
function parseRelayContract(body: string): BuzzRelayContract {
let value: unknown;
try {
value = JSON.parse(body);
} catch {
throw new BuzzProbeError(
'unsupported',
'relay_info_invalid',
'Buzz relay metadata was not valid JSON.',
'Confirm the configured endpoint serves Buzz NIP-11 metadata.'
);
}
if (!value || typeof value !== 'object') {
throw new BuzzProbeError(
'unsupported',
'relay_info_invalid',
'Buzz relay metadata was not an object.',
'Confirm the configured endpoint serves Buzz NIP-11 metadata.'
);
}
const record = value as Record<string, unknown>;
const supportedNips = Array.isArray(record.supported_nips)
? record.supported_nips.filter((item): item is number => Number.isInteger(item))
: [];
const supportedExtensions = Array.isArray(record.supported_extensions)
? record.supported_extensions.filter((item): item is string => typeof item === 'string')
: [];
const limitation =
record.limitation && typeof record.limitation === 'object'
? (record.limitation as Record<string, unknown>)
: {};
if (
typeof record.software !== 'string' ||
typeof record.version !== 'string' ||
typeof record.self !== 'string' ||
!/^[a-fA-F0-9]{64}$/.test(record.self) ||
REQUIRED_NIPS.some((nip) => !supportedNips.includes(nip))
) {
throw new BuzzProbeError(
'unsupported',
'relay_info_invalid',
'Relay metadata is missing the Buzz software/version or required NIP contract.',
`Use a supported Buzz ${BUZZ_TESTED_RELEASE} relay or update the compatibility policy.`
);
}
return {
software: record.software,
version: record.version.replace(/^v/, ''),
supportedNips,
supportedExtensions,
relayPublicKey:
typeof record.self === 'string' && /^[a-fA-F0-9]{64}$/.test(record.self)
? record.self.toLowerCase()
: undefined,
authRequired: limitation.auth_required === true,
};
}
function evidenceKey(input: {
config: BuzzProbeConfig;
endpoints?: NormalizedBuzzEndpoints;
contract?: BuzzRelayContract;
commands: BuzzCommandDiagnostic[];
observedSigningPublicKey?: string;
}): string {
return createHash('sha256')
.update(
JSON.stringify({
probeRevision: BUZZ_PROBE_REVISION,
relayHttpUrl: input.endpoints?.httpUrl ?? input.config.relayHttpUrl,
relayWebSocketUrl: input.endpoints?.webSocketUrl ?? input.config.relayWebSocketUrl,
expectedCommunity: input.endpoints?.expectedCommunity ?? input.config.expectedCommunity,
publicKey: input.config.publicKey.toLowerCase(),
observedSigningPublicKey: input.observedSigningPublicKey,
credentialRef: input.config.credentialRef,
authTagRef: input.config.authTagRef,
allowLocalhost: Boolean(input.config.allowLocalhost),
allowPrivateNetwork: Boolean(input.config.allowPrivateNetwork),
command: input.config.command,
contract: input.contract,
commands: input.commands,
})
)
.digest('hex');
}
export class BuzzCompatibilityService {
private readonly fetch: NonNullable<BuzzCompatibilityServiceOptions['fetch']>;
private readonly resolveSecret: NonNullable<BuzzCompatibilityServiceOptions['resolveSecret']>;
private readonly signer: BuzzNip98Signer;
private readonly runCommand: BuzzCommandRunner;
private readonly now: () => Date;
private readonly timeoutMs: number;
private readonly maxResponseBytes: number;
constructor(options: BuzzCompatibilityServiceOptions = {}) {
this.fetch = options.fetch ?? safeFetch;
this.resolveSecret =
options.resolveSecret ??
(async (reference) => {
if (!reference.startsWith('env:')) return undefined;
return new EnvironmentCredentialSecretSource().resolve({
kind: 'environment',
reference: reference.slice(4),
});
});
this.signer = options.signer ?? new NostrToolsBuzzNip98Signer();
this.runCommand =
options.runCommand ??
(async (executable, args) => {
const result = await execFileAsync(executable, args, {
timeout: this.timeoutMs,
maxBuffer: 64 * 1024,
windowsHide: true,
env: buildBuzzCommandEnvironment(),
});
return { stdout: result.stdout, stderr: result.stderr };
});
this.now = options.now ?? (() => new Date());
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
this.maxResponseBytes = options.maxResponseBytes ?? MAX_RESPONSE_BYTES;
}
async probe(config: BuzzProbeConfig): Promise<BuzzCompatibilityResult> {
const checkedAt = this.now().toISOString();
const commands = await this.discoverCommands(config.command);
const checks = emptyChecks();
let endpoints: NormalizedBuzzEndpoints | undefined;
let contract: BuzzRelayContract | undefined;
let observedSigningPublicKey: string | undefined;
const result = (
status: BuzzCompatibilityStatus,
reasonCode: BuzzCompatibilityReasonCode,
detail: string,
remediation?: string
): BuzzCompatibilityResult => ({
schemaVersion: BUZZ_COMPATIBILITY_SCHEMA_VERSION,
probeRevision: BUZZ_PROBE_REVISION,
testedRelease: BUZZ_TESTED_RELEASE,
testedCommit: BUZZ_TESTED_COMMIT,
status,
reasonCode,
detail: sanitizeDetail(detail),
remediation: remediation ? sanitizeDetail(remediation) : undefined,
configuredRelayHttpUrl: sanitizeConfiguredUrl(config.relayHttpUrl) ?? '[not-configured]',
resolvedRelayHttpUrl: endpoints?.httpUrl,
configuredRelayWebSocketUrl: sanitizeConfiguredUrl(config.relayWebSocketUrl),
resolvedRelayWebSocketUrl: endpoints?.webSocketUrl,
expectedCommunity:
endpoints?.expectedCommunity ?? sanitizeConfiguredCommunity(config.expectedCommunity),
observedCommunity: endpoints?.community,
publicKeyFingerprint: fingerprintBuzzPublicKey(config.publicKey),
contract,
checks: { ...checks },
commands,
evidenceKey: evidenceKey({
config,
endpoints,
contract,
commands,
observedSigningPublicKey,
}),
checkedAt,
});
try {
endpoints = normalizeBuzzEndpoints(config);
if (endpoints.expectedCommunity && endpoints.expectedCommunity !== endpoints.community) {
checks.communityBinding = 'failed';
throw new BuzzProbeError(
'misconfigured',
'community_mismatch',
`Configured relay resolves to community ${endpoints.community}, not the expected community.`,
'Correct the relay host or expected community. Buzz binds community identity to the request host.'
);
}
const infoResult = await this.fetchBoundedWithTimeout(
endpoint(endpoints.httpUrl, '/info'),
{
method: 'GET',
headers: { Accept: 'application/nostr+json' },
},
config
);
if (!infoResult) {
throw new BuzzProbeError(
'unreachable',
'network_policy_blocked',
'The relay endpoint was blocked by the outbound network policy or DNS validation.',
'Use a public HTTPS relay or explicitly allow the required local/private network class.'
);
}
const { response: infoResponse, body: infoBody } = infoResult;
if (!infoResponse.ok) {
throw new BuzzProbeError(
'unreachable',
'relay_unreachable',
`Buzz relay metadata request returned HTTP ${infoResponse.status}.`,
'Verify the relay endpoint, reverse proxy, and host mapping.'
);
}
contract = parseRelayContract(infoBody);
if (contract.software.replace(/\/$/, '') !== BUZZ_REPOSITORY) {
checks.relayIdentity = 'failed';
throw new BuzzProbeError(
'unsupported',
'relay_software_mismatch',
'The configured endpoint does not identify itself as the supported Buzz relay.',
`Point Veritas at a Buzz ${BUZZ_TESTED_RELEASE} relay.`
);
}
if (contract.version !== BUZZ_TESTED_RELEASE) {
checks.relayIdentity = 'verified';
throw new BuzzProbeError(
'unsupported',
'relay_version_unsupported',
`Buzz relay ${contract.version} is outside the tested ${BUZZ_TESTED_RELEASE} contract.`,
'Upgrade or pin Buzz to the tested release, or update the Veritas compatibility policy with fixtures.'
);
}
checks.relayIdentity = 'verified';
const verifiedContract = contract;
const privateKey = await this.resolveSecret(config.credentialRef);
if (!privateKey) {
throw new BuzzProbeError(
'misconfigured',
'credential_unavailable',
'The configured Buzz signing-key reference is unavailable.',
`Inject the environment secret named by ${config.credentialRef}.`
);
}
const authTag = config.authTagRef ? await this.resolveSecret(config.authTagRef) : undefined;
if (config.authTagRef && !authTag) {
throw new BuzzProbeError(
'misconfigured',
'credential_unavailable',
'The configured Buzz NIP-OA auth-tag reference is unavailable.',
`Inject the environment secret named by ${config.authTagRef}.`
);
}
const validatedAuthTag = authTag ? validateBuzzAuthTag(authTag) : undefined;
const queryUrl = endpoint(endpoints.httpUrl, '/query');
const verifyReadCapability = async (
kind: 39_000 | 9,
capability: 'channelRead' | 'messageRead',
label: string
): Promise<void> => {
const body = JSON.stringify([{ kinds: [kind], limit: 1 }]);
const signed = await this.signer.sign({
privateKey,
method: 'POST',
url: queryUrl,
body,
});
observedSigningPublicKey = signed.publicKey.toLowerCase();
if (observedSigningPublicKey !== config.publicKey.toLowerCase()) {
checks.configuredIdentity = 'failed';
throw new BuzzProbeError(
'misconfigured',
'public_key_mismatch',
'The configured signing secret does not match the configured public identity.',
'Correct the public key or signing-key secret reference.'
);
}
checks.configuredIdentity = 'verified';
const headers: Record<string, string> = {
Accept: 'application/json',
Authorization: signed.authorization,
'Content-Type': 'application/json',
};
if (validatedAuthTag) headers['x-auth-tag'] = validatedAuthTag;
const queryResult = await this.fetchBoundedWithTimeout(
queryUrl,
{ method: 'POST', headers, body },
config
);
if (!queryResult) {
throw new BuzzProbeError(
'unreachable',
'network_policy_blocked',
`The authenticated Buzz ${label} query was blocked by network policy.`,
'Review the relay URL and explicit local/private network allowances.'
);
}
const { response: queryResponse, body: queryBody } = queryResult;
if (queryResponse.status === 401) {
checks.authentication = 'failed';
throw new BuzzProbeError(
'unauthorized',
'authentication_rejected',
'Buzz rejected the NIP-98 identity proof.',
'Verify the signing key, relay URL host, system clock, and Buzz NIP-98 configuration.'
);
}
if (queryResponse.status === 403) {
checks.authentication = 'verified';
if (queryBody.includes('relay_membership_required')) {
checks.membership = 'failed';
throw new BuzzProbeError(
'not_member',
'relay_membership_required',
'Buzz authenticated the identity but denied relay membership.',
'Add the public identity as a relay member or provide a valid NIP-OA auth tag.'
);
}
checks.membership = verifiedContract.supportedNips.includes(43)
? 'verified'
: 'not_enforced';
checks[capability] = 'failed';
throw new BuzzProbeError(
'unauthorized',
'read_capability_rejected',
`Buzz authenticated the identity but rejected the ${label} read probe.`,
'Review channel membership and relay read policy for the configured identity.'
);
}
if (queryResponse.status === 404) {
checks.communityBinding = 'failed';
throw new BuzzProbeError(
'misconfigured',
'community_mismatch',
'Buzz could not bind the authenticated query host to a community.',
'Use the exact configured community host, including any non-default port.'
);
}
if (queryResponse.status === 429) {
throw new BuzzProbeError(
'degraded',
'relay_rate_limited',
`Buzz rate-limited the ${label} read probe.`,
'Retry after the relay rate-limit window.'
);
}
if (!queryResponse.ok) {
throw new BuzzProbeError(
'degraded',
'relay_error',
`Buzz returned HTTP ${queryResponse.status} for the ${label} read probe.`,
'Inspect relay health and retry without enabling message delivery.'
);
}
checks.authentication = 'verified';
checks.communityBinding = 'verified';
checks.membership = verifiedContract.supportedNips.includes(43)
? 'verified'
: 'not_enforced';
try {
const parsed = JSON.parse(queryBody);
if (!Array.isArray(parsed)) throw new Error('query response is not an array');
} catch {
throw new BuzzProbeError(
'unsupported',
'query_response_invalid',
`Buzz returned an invalid ${label} query response.`,
'Verify the relay build and reverse proxy response handling.'
);
}
checks[capability] = 'verified';
};
await verifyReadCapability(39_000, 'channelRead', 'channel metadata');
await verifyReadCapability(9, 'messageRead', 'message');
return result(
'healthy',
'ok',
'Buzz relay identity, configured signing identity, membership posture, and read capabilities are compatible.'
);
} catch (error) {
if (error instanceof BuzzProbeError) {
return result(error.status, error.reasonCode, error.message, error.remediation);
}
return result(
'unreachable',
'relay_unreachable',
`Buzz compatibility probe failed: ${sanitizeDetail(error)}`,
'Verify relay reachability, DNS, TLS, and the configured network-policy allowances.'
);
}
}
private async fetchBoundedWithTimeout(
url: string,
init: RequestInit,
config: BuzzProbeConfig
): Promise<{ response: Response; body: string } | null> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
try {
const response = await this.fetch(
url,
{ ...init, signal: controller.signal, redirect: 'manual' },
{
allowHttp: Boolean(config.allowLocalhost || config.allowPrivateNetwork),
allowLocalhost: Boolean(config.allowLocalhost),
allowPrivateNetwork: Boolean(config.allowPrivateNetwork),
}
);
if (!response) return null;
const body = await readBoundedBody(response, this.maxResponseBytes, controller.signal);
return { response, body };
} finally {
clearTimeout(timeout);
}
}
private async discoverCommands(command?: BuzzCommandConfig): Promise<BuzzCommandDiagnostic[]> {
const candidates: Array<{
command: BuzzCommandDiagnostic['command'];
executable: string;
args: string[];
}> = [
{ command: 'buzz', executable: 'buzz', args: ['--version'] },
{ command: 'buzz-acp', executable: 'buzz-acp', args: ['--version'] },
{ command: 'buzz-agent', executable: 'buzz-agent', args: ['--version'] },
];
if (command) {
candidates.push({
command: 'configured',
executable: command.executable,
args: [...(command.args ?? []), '--version'],
});
}
return Promise.all(
candidates.map(async (candidate): Promise<BuzzCommandDiagnostic> => {
try {
const output = await this.runCommand(candidate.executable, candidate.args);
const version = `${output.stdout}\n${output.stderr}`.trim().split(/\r?\n/, 1)[0];
return {
command: candidate.command,
executable: candidate.executable,
available: true,
version: sanitizeDetail(version || 'version not reported'),
};
} catch (error) {
const code =
error && typeof error === 'object' && 'code' in error
? String((error as { code?: unknown }).code)
: undefined;
return {
command: candidate.command,
executable: candidate.executable,
available: false,
detail:
code === 'ENOENT'
? 'not found'
: sanitizeDetail(code ? `command failed (${code})` : 'command failed'),
};
}
})
);
}
}

View file

@ -0,0 +1,87 @@
import { createHash, randomUUID } from 'node:crypto';
import { finalizeEvent, nip19 } from 'nostr-tools';
const NIP_98_KIND = 27_235;
export interface BuzzNip98Signer {
sign(input: {
privateKey: string;
method: 'POST';
url: string;
body: string;
}): Promise<{ authorization: string; publicKey: string }>;
}
interface NostrToolsBuzzNip98SignerOptions {
now?: () => number;
createNonce?: () => string;
}
function invalidPrivateKey(): Error {
return new Error('Invalid Buzz private key format');
}
function decodePrivateKey(value: string): Uint8Array {
const candidate = value.trim();
if (/^[a-fA-F0-9]{64}$/.test(candidate)) {
return Uint8Array.from(Buffer.from(candidate, 'hex'));
}
if (!candidate.startsWith('nsec1')) throw invalidPrivateKey();
try {
const decoded = nip19.decode(candidate);
if (
decoded.type !== 'nsec' ||
!(decoded.data instanceof Uint8Array) ||
decoded.data.byteLength !== 32
) {
throw invalidPrivateKey();
}
return decoded.data;
} catch {
throw invalidPrivateKey();
}
}
export class NostrToolsBuzzNip98Signer implements BuzzNip98Signer {
private readonly now: () => number;
private readonly createNonce: () => string;
constructor(options: NostrToolsBuzzNip98SignerOptions = {}) {
this.now = options.now ?? Date.now;
this.createNonce = options.createNonce ?? randomUUID;
}
async sign(input: {
privateKey: string;
method: 'POST';
url: string;
body: string;
}): Promise<{ authorization: string; publicKey: string }> {
const secretKey = decodePrivateKey(input.privateKey);
try {
const event = finalizeEvent(
{
kind: NIP_98_KIND,
created_at: Math.floor(this.now() / 1000),
tags: [
['u', input.url],
['method', input.method],
['nonce', this.createNonce()],
['payload', createHash('sha256').update(input.body, 'utf8').digest('hex')],
],
content: '',
},
secretKey
);
return {
authorization: `Nostr ${Buffer.from(JSON.stringify(event), 'utf8').toString('base64')}`,
publicKey: event.pubkey,
};
} catch {
throw invalidPrivateKey();
} finally {
secretKey.fill(0);
}
}
}

View file

@ -1,4 +1,3 @@
import fs from 'fs/promises';
import path from 'path';
import { nanoid } from 'nanoid';
import type {
@ -16,6 +15,12 @@ import type {
CommunicationThreadMapping,
SquadMessage,
} from '@veritas-kanban/shared';
import {
BUZZ_COMPATIBILITY_SCHEMA_VERSION,
BUZZ_PROBE_REVISION,
BUZZ_TESTED_COMMIT,
BUZZ_TESTED_RELEASE,
} from '@veritas-kanban/shared';
import { auditLog, type AuditEvent } from './audit-service.js';
import { getChatService, type ChatService } from './chat-service.js';
import {
@ -26,6 +31,18 @@ import { withFileLock } from './file-lock.js';
import { redactString } from '../lib/redact.js';
import { ensureWithinBase, sanitizeCommentText, validatePathSegment } from '../utils/sanitize.js';
import { getRuntimeDir } from '../utils/paths.js';
import { atomicWriteFile, mkdir, readFile } from '../storage/fs-helpers.js';
import {
BuzzCompatibilityService,
fingerprintBuzzPublicKey,
normalizeBuzzEndpoints,
type BuzzProbeConfig,
type NormalizedBuzzEndpoints,
} from './buzz-compatibility-service.js';
import {
buzzAdapterConfigSchema,
type BuzzAdapterConfig,
} from '../schemas/communication-adapter-schemas.js';
const DEFAULT_ADAPTER_ID = 'msteams-default';
const MAX_DELIVERIES = 500;
@ -33,6 +50,7 @@ const MAX_MESSAGE_LENGTH = 4000;
interface InternalCommunicationAdapterRecord extends CommunicationAdapterRecord {
webhookUrlRaw?: string;
buzzConfigKey?: string;
}
interface CommunicationAdapterState {
@ -49,6 +67,7 @@ export interface CommunicationAdapterServiceOptions {
persist?: boolean;
chatService?: ChatService;
outboundIntegrations?: OutboundIntegrationService;
buzzCompatibility?: BuzzCompatibilityService;
audit?: (event: AuditEvent) => Promise<void>;
}
@ -56,11 +75,98 @@ function nowIso(): string {
return new Date().toISOString();
}
function trimOrUndefined(value?: string): string | undefined {
function trimOrUndefined(value?: string | null): string | undefined {
const trimmed = value?.trim();
return trimmed ? trimmed : undefined;
}
interface ValidatedBuzzAdapterConfig {
config: BuzzAdapterConfig;
endpoints: NormalizedBuzzEndpoints;
probeConfig: BuzzProbeConfig;
configKey: string;
}
function buzzConfigKey(config: BuzzProbeConfig, endpoints: NormalizedBuzzEndpoints): string {
return JSON.stringify({
probeRevision: BUZZ_PROBE_REVISION,
testedRelease: BUZZ_TESTED_RELEASE,
testedCommit: BUZZ_TESTED_COMMIT,
relayHttpUrl: endpoints.httpUrl,
relayWebSocketUrl: endpoints.webSocketUrl,
expectedCommunity: endpoints.expectedCommunity,
publicKey: config.publicKey,
credentialRef: config.credentialRef,
authTagRef: config.authTagRef,
allowLocalhost: Boolean(config.allowLocalhost),
allowPrivateNetwork: Boolean(config.allowPrivateNetwork),
command: config.command,
});
}
function validateStoredBuzzConfig(
adapter: InternalCommunicationAdapterRecord
): ValidatedBuzzAdapterConfig | null {
const parsed = buzzAdapterConfigSchema.safeParse({
kind: 'buzz',
displayName: adapter.displayName,
enabled: adapter.enabled,
relayHttpUrl: adapter.relayHttpUrl,
relayWebSocketUrl: adapter.relayWebSocketUrl,
expectedCommunity: adapter.expectedCommunity,
publicKey: adapter.publicKey,
credentialRef: adapter.credentialRef,
authTagRef: adapter.authTagRef,
allowLocalhost: adapter.allowLocalhost,
allowPrivateNetwork: adapter.allowPrivateNetwork,
command: adapter.command,
});
if (!parsed.success) return null;
try {
const endpoints = normalizeBuzzEndpoints({
relayHttpUrl: parsed.data.relayHttpUrl,
relayWebSocketUrl: parsed.data.relayWebSocketUrl ?? undefined,
expectedCommunity: parsed.data.expectedCommunity ?? undefined,
});
const probeConfig: BuzzProbeConfig = {
enabled: parsed.data.enabled ?? true,
relayHttpUrl: parsed.data.relayHttpUrl,
relayWebSocketUrl: parsed.data.relayWebSocketUrl ?? undefined,
expectedCommunity: parsed.data.expectedCommunity ?? undefined,
publicKey: parsed.data.publicKey.toLowerCase(),
credentialRef: parsed.data.credentialRef,
authTagRef: parsed.data.authTagRef ?? undefined,
allowLocalhost: parsed.data.allowLocalhost,
allowPrivateNetwork: parsed.data.allowPrivateNetwork,
command: parsed.data.command ?? undefined,
};
return {
config: parsed.data,
endpoints,
probeConfig,
configKey: buzzConfigKey(probeConfig, endpoints),
};
} catch {
return null;
}
}
function isCurrentBuzzCompatibility(
adapter: InternalCommunicationAdapterRecord,
validated: ValidatedBuzzAdapterConfig
): boolean {
const compatibility = adapter.compatibility;
return Boolean(
compatibility &&
compatibility.schemaVersion === BUZZ_COMPATIBILITY_SCHEMA_VERSION &&
compatibility.probeRevision === BUZZ_PROBE_REVISION &&
compatibility.testedRelease === BUZZ_TESTED_RELEASE &&
compatibility.testedCommit === BUZZ_TESTED_COMMIT &&
adapter.buzzConfigKey === validated.configKey
);
}
function sanitizeUrl(url?: string): string | undefined {
if (!url) return undefined;
try {
@ -124,6 +230,7 @@ export class CommunicationAdapterService {
private readonly persist: boolean;
private readonly chatService: ChatService;
private readonly outboundIntegrations: OutboundIntegrationService;
private readonly buzzCompatibility: BuzzCompatibilityService;
private readonly audit: (event: AuditEvent) => Promise<void>;
private loaded = false;
private state: CommunicationAdapterState = this.emptyState();
@ -133,6 +240,7 @@ export class CommunicationAdapterService {
this.persist = options.persist ?? process.env.VITEST !== 'true';
this.chatService = options.chatService || getChatService();
this.outboundIntegrations = options.outboundIntegrations || getOutboundIntegrationService();
this.buzzCompatibility = options.buzzCompatibility || new BuzzCompatibilityService();
this.audit = options.audit || auditLog;
}
@ -158,11 +266,20 @@ export class CommunicationAdapterService {
const timestamp = nowIso();
const existing = this.state.adapters[adapterId];
const kind = input.kind ?? existing?.kind ?? 'msteams';
if (existing && input.kind && input.kind !== existing.kind) {
throw new Error('Communication adapter kind cannot be changed after creation');
}
if (kind === 'buzz') {
const validated = buzzAdapterConfigSchema.parse({ ...input, kind: 'buzz' });
return this.configureBuzzAdapter(adapterId, validated, existing, timestamp);
}
const rawWebhook =
input.webhookUrl !== undefined ? trimOrUndefined(input.webhookUrl) : existing?.webhookUrlRaw;
const adapter: InternalCommunicationAdapterRecord = {
id: adapterId,
kind: input.kind ?? existing?.kind ?? 'msteams',
kind,
displayName: trimOrUndefined(input.displayName) ?? existing?.displayName ?? 'Microsoft Teams',
enabled: input.enabled ?? existing?.enabled ?? true,
deliveryMode: input.deliveryMode ?? existing?.deliveryMode ?? 'manual',
@ -201,12 +318,18 @@ export class CommunicationAdapterService {
const disconnected: InternalCommunicationAdapterRecord = {
...adapter,
enabled: false,
webhookUrl: undefined,
webhookUrlRaw: undefined,
webhookUrlConfigured: false,
webhookUrlRedacted: false,
hasCredential: false,
...(adapter.kind === 'msteams'
? {
webhookUrl: undefined,
webhookUrlRaw: undefined,
webhookUrlConfigured: false,
webhookUrlRedacted: false,
hasCredential: false,
}
: {}),
updatedAt: timestamp,
lastHealth: undefined,
compatibility: undefined,
};
this.state.adapters[adapterId] = disconnected;
const delivery = this.recordDelivery({
@ -223,6 +346,9 @@ export class CommunicationAdapterService {
validatePathSegment(adapterId);
await this.ensureLoaded();
const adapter = this.requireAdapter(adapterId);
if (adapter.kind === 'buzz') {
return this.checkBuzzHealth(adapter);
}
const configured = this.hasDestination(adapter);
const health: CommunicationAdapterHealth = {
adapterId,
@ -253,6 +379,33 @@ export class CommunicationAdapterService {
validatePathSegment(adapterId);
await this.ensureLoaded();
const adapter = this.requireAdapter(adapterId);
if (adapter.kind === 'buzz') {
const timestamp = nowIso();
const target = normalizeTarget(input.target);
const mapping: CommunicationThreadMapping = {
id: `map_${nanoid(10)}`,
adapterId,
externalThreadId:
trimOrUndefined(input.externalThreadId) ?? this.buildExternalThreadId(adapterId, target),
externalUrl: sanitizeUrl(input.externalUrl),
target,
createdAt: timestamp,
updatedAt: timestamp,
createdBy: trimOrUndefined(input.actor),
};
const delivery = this.recordDelivery({
adapterId,
operation: 'send',
status: 'blocked',
target: mapping.target,
externalThreadId: mapping.externalThreadId,
actor: input.actor,
error: 'Buzz message delivery is not implemented by the connection diagnostic adapter.',
});
await this.saveState();
await this.auditDelivery(delivery);
return { delivery, mapping };
}
const target = normalizeTarget(input.target);
const externalThreadId =
trimOrUndefined(input.externalThreadId) ?? this.buildExternalThreadId(adapterId, target);
@ -295,6 +448,31 @@ export class CommunicationAdapterService {
if (!externalThreadId) {
throw new Error('externalThreadId is required');
}
if (adapter.kind === 'buzz') {
const timestamp = nowIso();
const mapping: CommunicationThreadMapping = {
id: `map_${nanoid(10)}`,
adapterId,
externalThreadId,
externalUrl: sanitizeUrl(input.externalUrl),
target: normalizeTarget(input.target ?? { kind: 'squad' }),
createdAt: timestamp,
updatedAt: timestamp,
createdBy: trimOrUndefined(input.actor),
};
const delivery = this.recordDelivery({
adapterId,
operation: 'reply-ingest',
status: 'blocked',
target: mapping.target,
externalThreadId,
actor: input.actor,
error: 'Buzz reply ingestion is not implemented by the connection diagnostic adapter.',
});
await this.saveState();
await this.auditDelivery(delivery);
return { delivery, mapping, squadMessageId: '' };
}
const existing = this.findMapping(adapterId, externalThreadId);
const target = normalizeTarget(input.target ?? existing?.target ?? { kind: 'squad' });
@ -412,12 +590,15 @@ export class CommunicationAdapterService {
): Promise<{ delivery: CommunicationDeliveryAudit; replies: [] }> {
validatePathSegment(adapterId);
await this.ensureLoaded();
this.requireAdapter(adapterId);
const adapter = this.requireAdapter(adapterId);
const delivery = this.recordDelivery({
adapterId,
operation: 'poll',
status: 'skipped',
error: 'Reply polling is adapter-defined; this adapter uses the ingest API.',
error:
adapter.kind === 'buzz'
? 'Buzz reply polling is not implemented by the connection diagnostic adapter.'
: 'Reply polling is adapter-defined; this adapter uses the ingest API.',
});
await this.saveState();
await this.auditDelivery(delivery);
@ -559,6 +740,7 @@ export class CommunicationAdapterService {
}
private hasDestination(adapter: InternalCommunicationAdapterRecord): boolean {
if (adapter.kind === 'buzz') return false;
if (adapter.deliveryMode === 'webhook') {
return Boolean(adapter.webhookUrlRaw);
}
@ -577,7 +759,56 @@ export class CommunicationAdapterService {
}
private publicAdapter(adapter: InternalCommunicationAdapterRecord): CommunicationAdapterRecord {
const { webhookUrlRaw: _webhookUrlRaw, ...publicRecord } = adapter;
if (adapter.kind === 'buzz') {
const validated = validateStoredBuzzConfig(adapter);
if (!validated) {
return {
id: adapter.id,
kind: 'buzz',
displayName: 'Buzz',
enabled: false,
deliveryMode: 'manual',
replyMode: 'ingest-api',
destinationType: 'channel',
hasCredential: false,
createdAt: adapter.createdAt,
updatedAt: adapter.updatedAt,
};
}
const config = validated.config;
return {
id: adapter.id,
kind: 'buzz',
displayName: config.displayName ?? 'Buzz',
enabled: config.enabled ?? true,
deliveryMode: 'manual',
replyMode: 'ingest-api',
destinationType: 'channel',
hasCredential: true,
relayHttpUrl: config.relayHttpUrl,
relayWebSocketUrl: config.relayWebSocketUrl ?? undefined,
expectedCommunity: config.expectedCommunity ?? undefined,
publicKey: config.publicKey.toLowerCase(),
publicKeyFingerprint: fingerprintBuzzPublicKey(config.publicKey),
credentialRef: config.credentialRef,
authTagRef: config.authTagRef ?? undefined,
authTagConfigured: Boolean(config.authTagRef),
allowLocalhost: config.allowLocalhost,
allowPrivateNetwork: config.allowPrivateNetwork,
command: config.command ?? undefined,
compatibility: isCurrentBuzzCompatibility(adapter, validated)
? adapter.compatibility
: undefined,
lastHealth: isCurrentBuzzCompatibility(adapter, validated) ? adapter.lastHealth : undefined,
createdAt: adapter.createdAt,
updatedAt: adapter.updatedAt,
};
}
const {
webhookUrlRaw: _webhookUrlRaw,
buzzConfigKey: _buzzConfigKey,
...publicRecord
} = adapter;
return {
...publicRecord,
webhookUrl: adapter.webhookUrlRaw ? sanitizeUrl(adapter.webhookUrlRaw) : undefined,
@ -594,9 +825,9 @@ export class CommunicationAdapterService {
return;
}
await fs.mkdir(this.storageDir, { recursive: true });
await mkdir(this.storageDir, { recursive: true });
try {
const raw = await fs.readFile(this.statePath, 'utf-8');
const raw = await readFile(this.statePath, 'utf-8');
const parsed = JSON.parse(raw) as Partial<CommunicationAdapterState>;
this.state = {
version: 1,
@ -627,9 +858,9 @@ export class CommunicationAdapterService {
private async saveState(): Promise<void> {
this.state.updatedAt = nowIso();
if (!this.persist) return;
await fs.mkdir(this.storageDir, { recursive: true });
await mkdir(this.storageDir, { recursive: true });
await withFileLock(this.statePath, async () => {
await fs.writeFile(this.statePath, JSON.stringify(this.state, null, 2), 'utf-8');
await atomicWriteFile(this.statePath, JSON.stringify(this.state, null, 2));
});
}
@ -685,6 +916,198 @@ export class CommunicationAdapterService {
},
});
}
private async configureBuzzAdapter(
adapterId: string,
input: BuzzAdapterConfig,
existing: InternalCommunicationAdapterRecord | undefined,
timestamp: string
): Promise<CommunicationAdapterRecord> {
const relayHttpUrl = trimOrUndefined(input.relayHttpUrl) ?? existing?.relayHttpUrl;
const publicKey = (trimOrUndefined(input.publicKey) ?? existing?.publicKey)?.toLowerCase();
const credentialRef = trimOrUndefined(input.credentialRef) ?? existing?.credentialRef;
if (!relayHttpUrl || !publicKey || !credentialRef) {
throw new Error('Buzz relayHttpUrl, publicKey, and credentialRef are required');
}
const relayWebSocketUrl =
input.relayWebSocketUrl !== undefined
? trimOrUndefined(input.relayWebSocketUrl)
: existing?.relayWebSocketUrl;
const expectedCommunity =
input.expectedCommunity !== undefined
? trimOrUndefined(input.expectedCommunity)
: existing?.expectedCommunity;
const authTagRef =
input.authTagRef !== undefined ? trimOrUndefined(input.authTagRef) : existing?.authTagRef;
const endpoints = normalizeBuzzEndpoints({
relayHttpUrl,
relayWebSocketUrl,
expectedCommunity,
});
const command = input.command !== undefined ? input.command : existing?.command;
const allowLocalhost = input.allowLocalhost ?? existing?.allowLocalhost ?? false;
const allowPrivateNetwork = input.allowPrivateNetwork ?? existing?.allowPrivateNetwork ?? false;
const probeConfig: BuzzProbeConfig = {
enabled: input.enabled ?? existing?.enabled ?? true,
relayHttpUrl: endpoints.configuredHttpUrl.trim(),
relayWebSocketUrl: endpoints.configuredWebSocketUrl?.trim(),
expectedCommunity,
publicKey,
credentialRef,
authTagRef,
allowLocalhost,
allowPrivateNetwork,
command: command ?? undefined,
};
const nextBuzzConfigKey = buzzConfigKey(probeConfig, endpoints);
const evidenceIsCurrent =
existing?.buzzConfigKey === nextBuzzConfigKey &&
existing.compatibility?.probeRevision === BUZZ_PROBE_REVISION &&
existing.compatibility.testedRelease === BUZZ_TESTED_RELEASE &&
existing.compatibility.testedCommit === BUZZ_TESTED_COMMIT;
const adapter: InternalCommunicationAdapterRecord = {
id: adapterId,
kind: 'buzz',
displayName: trimOrUndefined(input.displayName) ?? existing?.displayName ?? 'Buzz',
enabled: input.enabled ?? existing?.enabled ?? true,
deliveryMode: 'manual',
replyMode: 'ingest-api',
destinationType: 'channel',
hasCredential: true,
relayHttpUrl: probeConfig.relayHttpUrl,
relayWebSocketUrl: probeConfig.relayWebSocketUrl,
expectedCommunity,
publicKey,
publicKeyFingerprint: fingerprintBuzzPublicKey(publicKey),
credentialRef,
authTagRef,
authTagConfigured: Boolean(authTagRef),
allowLocalhost,
allowPrivateNetwork,
command: command ?? undefined,
buzzConfigKey: nextBuzzConfigKey,
createdAt: existing?.createdAt ?? timestamp,
updatedAt: timestamp,
lastHealth: evidenceIsCurrent ? existing?.lastHealth : undefined,
compatibility: evidenceIsCurrent ? existing?.compatibility : undefined,
};
this.state.adapters[adapterId] = adapter;
const delivery = this.recordDelivery({
adapterId,
operation: 'configure',
status: 'success',
});
await this.saveState();
await this.auditAdapter('communication_adapter.configured', adapter, delivery);
return this.publicAdapter(adapter);
}
private async checkBuzzHealth(
adapter: InternalCommunicationAdapterRecord
): Promise<CommunicationAdapterHealth> {
const validated = validateStoredBuzzConfig(adapter);
if (!validated) {
const health: CommunicationAdapterHealth = {
adapterId: adapter.id,
status: 'misconfigured',
configured: false,
canSend: false,
canReceiveReplies: false,
checkedAt: nowIso(),
detail: 'The persisted Buzz configuration is invalid and was quarantined.',
reasonCode: 'configuration_invalid',
remediation: 'Resave a complete reference-only Buzz connection configuration.',
};
adapter.enabled = false;
adapter.command = undefined;
adapter.lastHealth = health;
adapter.compatibility = undefined;
adapter.updatedAt = health.checkedAt;
this.recordDelivery({
adapterId: adapter.id,
operation: 'health',
status: 'failed',
error: health.detail,
});
await this.saveState();
return health;
}
if (!validated.probeConfig.enabled) {
const health: CommunicationAdapterHealth = {
adapterId: adapter.id,
status: 'disabled',
configured: Boolean(adapter.relayHttpUrl && adapter.publicKey && adapter.credentialRef),
canSend: false,
canReceiveReplies: false,
checkedAt: nowIso(),
detail: 'Buzz adapter is disabled. Configuration references are retained.',
reasonCode: 'adapter_disabled',
remediation: 'Enable the adapter to run a read-only compatibility probe.',
};
adapter.lastHealth = health;
adapter.compatibility = undefined;
adapter.updatedAt = health.checkedAt;
this.recordDelivery({
adapterId: adapter.id,
operation: 'health',
status: 'skipped',
});
await this.saveState();
return health;
}
if (!adapter.relayHttpUrl || !adapter.publicKey || !adapter.credentialRef) {
const health: CommunicationAdapterHealth = {
adapterId: adapter.id,
status: 'misconfigured',
configured: false,
canSend: false,
canReceiveReplies: false,
checkedAt: nowIso(),
detail: 'Buzz relay URL, public key, or credential reference is missing.',
reasonCode: 'configuration_missing',
remediation: 'Save a complete reference-only Buzz connection configuration.',
};
adapter.lastHealth = health;
adapter.compatibility = undefined;
adapter.updatedAt = health.checkedAt;
this.recordDelivery({
adapterId: adapter.id,
operation: 'health',
status: 'failed',
error: health.detail,
});
await this.saveState();
return health;
}
const compatibility = await this.buzzCompatibility.probe(validated.probeConfig);
const health: CommunicationAdapterHealth = {
adapterId: adapter.id,
status: compatibility.status,
configured: true,
canSend: false,
canReceiveReplies: false,
checkedAt: compatibility.checkedAt,
detail: compatibility.detail,
reasonCode: compatibility.reasonCode,
remediation: compatibility.remediation,
buzz: compatibility,
};
adapter.compatibility = compatibility;
adapter.lastHealth = health;
adapter.updatedAt = health.checkedAt;
this.recordDelivery({
adapterId: adapter.id,
operation: 'health',
status: compatibility.status === 'healthy' ? 'success' : 'failed',
error: compatibility.status === 'healthy' ? undefined : compatibility.detail,
});
await this.saveState();
return health;
}
}
let communicationAdapterService: CommunicationAdapterService | null = null;

View file

@ -14,7 +14,7 @@
import { lookup } from 'node:dns/promises';
import { request as httpRequest, type IncomingHttpHeaders, type RequestOptions } from 'node:http';
import { request as httpsRequest } from 'node:https';
import { isIP } from 'node:net';
import { BlockList, isIP } from 'node:net';
import { Readable } from 'node:stream';
import { createLogger } from '../lib/logger.js';
@ -55,6 +55,33 @@ const BLOCKED_IPV4_RANGES: Array<{
{ start: 0x64400000, end: 0x647fffff, name: 'cgnat', addressClass: 'cgnat' },
];
function ipv4BlockList(subnets: Array<[address: string, prefix: number]>): BlockList {
const blockList = new BlockList();
for (const [address, prefix] of subnets) {
blockList.addSubnet(address, prefix, 'ipv4');
}
return blockList;
}
const MAPPED_IPV4_LOCAL = ipv4BlockList([
['0.0.0.0', 32],
['127.0.0.0', 8],
]);
const MAPPED_IPV4_PRIVATE = ipv4BlockList([
['10.0.0.0', 8],
['172.16.0.0', 12],
['192.168.0.0', 16],
]);
const MAPPED_IPV4_LINK_LOCAL = ipv4BlockList([['169.254.0.0', 16]]);
const MAPPED_IPV4_CGNAT = ipv4BlockList([['100.64.0.0', 10]]);
const IPV6_LOCAL = new BlockList();
IPV6_LOCAL.addSubnet('::', 96, 'ipv6');
const IPV6_LINK_LOCAL = new BlockList();
IPV6_LINK_LOCAL.addSubnet('fe80::', 10, 'ipv6');
const IPV6_UNIQUE_LOCAL = new BlockList();
IPV6_UNIQUE_LOCAL.addSubnet('fc00::', 7, 'ipv6');
/**
* Convert IPv4 address string to 32-bit integer
*/
@ -97,27 +124,26 @@ function isBlockedIPv4(ip: string): BlockedAddressCheck {
* Check if a hostname is a blocked IPv6 address
*/
function isBlockedIPv6(host: string): BlockedAddressCheck {
const normalized = host.toLowerCase();
const normalized = unbracketHostname(host).toLowerCase();
// Loopback
if (normalized === '::1' || normalized === '[::1]') {
return { blocked: true, reason: 'IPv6 loopback', addressClass: 'local' };
if (IPV6_LOCAL.check(normalized, 'ipv6') || MAPPED_IPV4_LOCAL.check(normalized, 'ipv6')) {
return { blocked: true, reason: 'IPv6 local address', addressClass: 'local' };
}
// Link-local (fe80::/10)
if (normalized.startsWith('fe8') || normalized.startsWith('[fe8')) {
if (
IPV6_LINK_LOCAL.check(normalized, 'ipv6') ||
MAPPED_IPV4_LINK_LOCAL.check(normalized, 'ipv6')
) {
return { blocked: true, reason: 'IPv6 link-local', addressClass: 'link-local' };
}
// Unique local (fc00::/7)
if (
normalized.startsWith('fc') ||
normalized.startsWith('fd') ||
normalized.startsWith('[fc') ||
normalized.startsWith('[fd')
) {
if (IPV6_UNIQUE_LOCAL.check(normalized, 'ipv6')) {
return { blocked: true, reason: 'IPv6 unique-local', addressClass: 'unique-local' };
}
if (MAPPED_IPV4_PRIVATE.check(normalized, 'ipv6')) {
return { blocked: true, reason: 'IPv4-mapped private address', addressClass: 'private' };
}
if (MAPPED_IPV4_CGNAT.check(normalized, 'ipv6')) {
return { blocked: true, reason: 'IPv4-mapped CGNAT address', addressClass: 'cgnat' };
}
return { blocked: false };
}
@ -129,7 +155,13 @@ function isAllowedBlockedAddress(check: BlockedAddressCheck, opts: UrlValidation
if (opts.allowPrivateIp) {
return true;
}
return Boolean(opts.allowLocalhost && check.addressClass === 'local');
if (opts.allowLocalhost && check.addressClass === 'local') {
return true;
}
return Boolean(
opts.allowPrivateNetwork &&
(check.addressClass === 'private' || check.addressClass === 'unique-local')
);
}
function isBlockedIpAddress(address: string, opts: UrlValidationOptions): BlockedAddressCheck {
@ -163,6 +195,10 @@ function isLocalhostHostname(hostname: string): boolean {
);
}
function unbracketHostname(hostname: string): string {
return hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname;
}
// ─── URL Validation ────────────────────────────────────────────────────────
export interface UrlValidationResult {
@ -187,6 +223,8 @@ export interface UrlValidationOptions {
allowLocalhost?: boolean;
/** Allow private IP ranges (default: false) */
allowPrivateIp?: boolean;
/** Allow RFC1918/ULA networks while continuing to block link-local and CGNAT ranges */
allowPrivateNetwork?: boolean;
/** Log validation failures (default: true) */
logFailures?: boolean;
}
@ -195,6 +233,7 @@ const DEFAULT_OPTIONS: UrlValidationOptions = {
allowHttp: process.env.NODE_ENV === 'development',
allowLocalhost: process.env.NODE_ENV === 'development',
allowPrivateIp: false,
allowPrivateNetwork: false,
logFailures: true,
};
@ -290,7 +329,7 @@ async function validateResolvedHostname(
parsed: URL,
opts: UrlValidationOptions
): Promise<ResolvedUrlValidationResult> {
const hostname = parsed.hostname;
const hostname = unbracketHostname(parsed.hostname);
const directIpFamily = isIP(hostname);
if (directIpFamily === 4 || directIpFamily === 6) {
@ -407,9 +446,10 @@ async function fetchPinnedUrl(
cb(null, resolvedAddress.address, resolvedAddress.family);
};
const requestBody = await bodyFromInit(init?.body ?? undefined);
const requestHostname = unbracketHostname(parsed.hostname);
const requestOptions: RequestOptions & { servername?: string } = {
protocol: parsed.protocol,
hostname: parsed.hostname,
hostname: requestHostname,
port: parsed.port || undefined,
path: `${parsed.pathname}${parsed.search}`,
method: init?.method ?? 'GET',
@ -419,7 +459,7 @@ async function fetchPinnedUrl(
};
if (parsed.protocol === 'https:') {
requestOptions.servername = parsed.hostname;
requestOptions.servername = isIP(requestHostname) ? undefined : requestHostname;
}
const request = parsed.protocol === 'https:' ? httpsRequest : httpRequest;

View file

@ -1,17 +1,113 @@
import type { SquadMessage } from './chat.types.js';
export type CommunicationAdapterKind = 'msteams';
export type CommunicationAdapterKind = 'msteams' | 'buzz';
export type CommunicationAdapterDeliveryMode = 'manual' | 'webhook';
export type CommunicationAdapterDestinationType = 'channel' | 'direct';
export type CommunicationAdapterHealthStatus = 'ok' | 'warning' | 'disabled' | 'error';
export type CommunicationAdapterHealthStatus =
'ok' | 'warning' | 'disabled' | 'error' | BuzzCompatibilityStatus;
export type CommunicationAdapterReplyMode = 'ingest-api';
export type CommunicationReplyTargetKind = 'squad' | 'task' | 'run' | 'approval' | 'notification';
export const BUZZ_COMPATIBILITY_SCHEMA_VERSION = 'buzz-compatibility/v1' as const;
export const BUZZ_PROBE_REVISION = 1;
export const BUZZ_TESTED_RELEASE = '0.4.24';
export const BUZZ_TESTED_COMMIT = '710ed9fff57878a1d69f809b80a6ee0416c53fc4';
export type BuzzCompatibilityStatus =
| 'healthy'
| 'degraded'
| 'unsupported'
| 'unauthorized'
| 'not_member'
| 'misconfigured'
| 'unreachable';
export type BuzzCompatibilityReasonCode =
| 'ok'
| 'adapter_disabled'
| 'configuration_missing'
| 'configuration_invalid'
| 'endpoint_invalid'
| 'endpoint_mismatch'
| 'network_policy_blocked'
| 'relay_unreachable'
| 'response_too_large'
| 'relay_info_invalid'
| 'query_response_invalid'
| 'relay_software_mismatch'
| 'relay_version_unsupported'
| 'community_mismatch'
| 'credential_unavailable'
| 'auth_tag_invalid'
| 'public_key_mismatch'
| 'authentication_rejected'
| 'relay_membership_required'
| 'read_capability_rejected'
| 'relay_rate_limited'
| 'relay_error';
export type BuzzVerificationState = 'verified' | 'not_enforced' | 'unverified' | 'failed';
export interface BuzzCommandConfig {
executable: string;
args?: string[];
}
export interface BuzzCommandDiagnostic {
command: 'buzz' | 'buzz-acp' | 'buzz-agent' | 'configured';
executable: string;
available: boolean;
version?: string;
detail?: string;
}
export interface BuzzCompatibilityChecks {
relayIdentity: BuzzVerificationState;
communityBinding: BuzzVerificationState;
configuredIdentity: BuzzVerificationState;
authentication: BuzzVerificationState;
membership: BuzzVerificationState;
channelRead: BuzzVerificationState;
messageRead: BuzzVerificationState;
}
export interface BuzzRelayContract {
software: string;
version: string;
supportedNips: number[];
supportedExtensions: string[];
relayPublicKey?: string;
authRequired: boolean;
}
export interface BuzzCompatibilityResult {
schemaVersion: typeof BUZZ_COMPATIBILITY_SCHEMA_VERSION;
probeRevision: typeof BUZZ_PROBE_REVISION;
testedRelease: typeof BUZZ_TESTED_RELEASE;
testedCommit: typeof BUZZ_TESTED_COMMIT;
status: BuzzCompatibilityStatus;
reasonCode: BuzzCompatibilityReasonCode;
detail: string;
remediation?: string;
configuredRelayHttpUrl: string;
resolvedRelayHttpUrl?: string;
configuredRelayWebSocketUrl?: string;
resolvedRelayWebSocketUrl?: string;
expectedCommunity?: string;
observedCommunity?: string;
publicKeyFingerprint: string;
contract?: BuzzRelayContract;
checks: BuzzCompatibilityChecks;
commands: BuzzCommandDiagnostic[];
evidenceKey: string;
checkedAt: string;
}
export interface CommunicationReplyTarget {
kind: CommunicationReplyTargetKind;
squadMessageId?: string;
@ -37,6 +133,18 @@ export interface CommunicationAdapterRecord {
webhookUrlConfigured?: boolean;
webhookUrlRedacted?: boolean;
hasCredential: boolean;
relayHttpUrl?: string;
relayWebSocketUrl?: string;
expectedCommunity?: string;
publicKey?: string;
publicKeyFingerprint?: string;
credentialRef?: string;
authTagRef?: string;
authTagConfigured?: boolean;
allowLocalhost?: boolean;
allowPrivateNetwork?: boolean;
command?: BuzzCommandConfig;
compatibility?: BuzzCompatibilityResult;
createdAt: string;
updatedAt: string;
lastHealth?: CommunicationAdapterHealth;
@ -54,6 +162,15 @@ export interface CommunicationAdapterInput {
chatId?: string;
webhookUrl?: string;
credential?: string;
relayHttpUrl?: string;
relayWebSocketUrl?: string | null;
expectedCommunity?: string | null;
publicKey?: string;
credentialRef?: string;
authTagRef?: string | null;
allowLocalhost?: boolean;
allowPrivateNetwork?: boolean;
command?: BuzzCommandConfig | null;
}
export interface CommunicationAdapterHealth {
@ -64,6 +181,9 @@ export interface CommunicationAdapterHealth {
canReceiveReplies: boolean;
checkedAt: string;
detail: string;
reasonCode?: BuzzCompatibilityReasonCode;
remediation?: string;
buzz?: BuzzCompatibilityResult;
}
export interface CommunicationThreadMapping {
@ -78,12 +198,7 @@ export interface CommunicationThreadMapping {
}
export type CommunicationDeliveryOperation =
| 'configure'
| 'health'
| 'send'
| 'reply-ingest'
| 'poll'
| 'disconnect';
'configure' | 'health' | 'send' | 'reply-ingest' | 'poll' | 'disconnect';
export type CommunicationDeliveryStatus = 'success' | 'queued' | 'failed' | 'blocked' | 'skipped';
@ -113,6 +228,8 @@ export interface CommunicationSendResult {
mapping: CommunicationThreadMapping;
}
export type CommunicationAdapterTestResult = CommunicationSendResult | CommunicationAdapterHealth;
export interface CommunicationReplyIngestInput {
externalThreadId: string;
externalReplyId?: string;

View file

@ -1,5 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { cleanup, fireEvent, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { BoardTab } from '@/components/settings/tabs/BoardTab';
import { EnforcementTab } from '@/components/settings/tabs/EnforcementTab';
import { NotificationsTab } from '@/components/settings/tabs/NotificationsTab';
@ -92,17 +93,93 @@ const mocks = vi.hoisted(() => ({
'Adapter can send through the configured delivery path and receive replies through the ingest API.',
},
},
{
id: 'buzz-default',
kind: 'buzz',
displayName: 'Buzz',
enabled: true,
deliveryMode: 'manual',
replyMode: 'ingest-api',
destinationType: 'channel',
relayHttpUrl: 'https://relay.example.test',
relayWebSocketUrl: 'wss://relay.example.test',
expectedCommunity: 'relay.example.test',
publicKey: 'ab'.repeat(32),
publicKeyFingerprint: 'abc123abc123',
credentialRef: 'env:BUZZ_PRIVATE_KEY',
authTagConfigured: false,
hasCredential: true,
createdAt: '2026-07-23T18:00:00.000Z',
updatedAt: '2026-07-23T18:00:00.000Z',
lastHealth: {
adapterId: 'buzz-default',
status: 'healthy',
configured: true,
canSend: false,
canReceiveReplies: false,
checkedAt: '2026-07-23T18:00:00.000Z',
detail: 'Buzz relay and read capabilities are compatible.',
reasonCode: 'ok',
},
},
]),
communicationHealth: vi.fn(async () => ({
adapterId: 'msteams-default',
status: 'ok',
configured: true,
canSend: true,
canReceiveReplies: true,
checkedAt: '2026-06-04T08:00:00.000Z',
detail:
'Adapter can send through the configured delivery path and receive replies through the ingest API.',
})),
communicationHealth: vi.fn(async (adapterId: string) =>
adapterId === 'buzz-default'
? {
adapterId: 'buzz-default',
status: 'healthy',
configured: true,
canSend: false,
canReceiveReplies: false,
checkedAt: '2026-07-23T18:00:00.000Z',
detail: 'Buzz relay and read capabilities are compatible.',
reasonCode: 'ok',
buzz: {
schemaVersion: 'buzz-compatibility/v1',
probeRevision: 1,
testedRelease: '0.4.24',
testedCommit: '710ed9fff57878a1d69f809b80a6ee0416c53fc4',
status: 'healthy',
reasonCode: 'ok',
detail: 'compatible',
configuredRelayHttpUrl: 'https://relay.example.test',
resolvedRelayHttpUrl: 'https://relay.example.test',
resolvedRelayWebSocketUrl: 'wss://relay.example.test',
expectedCommunity: 'relay.example.test',
observedCommunity: 'relay.example.test',
publicKeyFingerprint: 'abc123abc123',
checks: {
relayIdentity: 'verified',
communityBinding: 'verified',
configuredIdentity: 'verified',
authentication: 'verified',
membership: 'verified',
channelRead: 'verified',
messageRead: 'verified',
},
commands: [
{
command: 'buzz',
executable: 'buzz',
available: false,
detail: 'not found',
},
],
evidenceKey: 'safe-evidence',
checkedAt: '2026-07-23T18:00:00.000Z',
},
}
: {
adapterId: 'msteams-default',
status: 'ok',
configured: true,
canSend: true,
canReceiveReplies: true,
checkedAt: '2026-06-04T08:00:00.000Z',
detail:
'Adapter can send through the configured delivery path and receive replies through the ingest API.',
}
),
communicationDeliveries: vi.fn(async () => [
{
id: 'comm_1',
@ -297,6 +374,19 @@ describe('Settings tab Mantine controls', () => {
expect(await screen.findByText('Communication Health')).toBeDefined();
expect(screen.getByText('Local Squad Chat')).toBeDefined();
expect(screen.getByText('Human Reply Adapter')).toBeDefined();
expect(screen.getByText('Buzz Connection')).toBeDefined();
expect(screen.getByLabelText(/Relay HTTP URL/)).toBeDefined();
expect(screen.getByLabelText(/Relay WebSocket URL/)).toBeDefined();
expect(screen.getByLabelText(/Expected community/)).toBeDefined();
expect(screen.getByLabelText(/Public key/)).toBeDefined();
expect(screen.getByLabelText(/Signing key reference/)).toBeDefined();
expect(screen.getByText('Signing reference:').parentElement?.textContent).toContain(
'configured'
);
expect(await screen.findByText('Buzz 0.4.24, probe 1')).toBeDefined();
expect(screen.getByText('Compatibility chain')).toBeDefined();
expect(screen.getByText('Community binding')).toBeDefined();
expect(screen.queryByText(/nsec1/)).toBeNull();
expect(screen.getAllByText('Squad Chat Webhook').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText(/Visually verified: not recorded in VK/).length).toBeGreaterThan(0);
expect(screen.getByText('X-VK-Signature')).toBeDefined();
@ -318,6 +408,47 @@ describe('Settings tab Mantine controls', () => {
});
});
it('shows unconfigured Buzz placeholders and preserves keyboard field order', async () => {
mocks.communicationAdapters.mockResolvedValueOnce([]);
const user = userEvent.setup();
renderWithProviders(<NotificationsTab />);
expect((await screen.findAllByText('not configured')).length).toBeGreaterThanOrEqual(2);
expect(screen.getByText('Signing reference:').parentElement?.textContent).toContain(
'not configured'
);
const relayHttp = screen.getByRole('textbox', { name: /Relay HTTP URL/ });
relayHttp.focus();
await user.tab();
expect(document.activeElement).toBe(
screen.getByRole('textbox', { name: /Relay WebSocket URL/ })
);
await user.tab();
expect(document.activeElement).toBe(
screen.getByRole('textbox', { name: /Expected community/ })
);
});
it('shows Buzz failure status and visible remediation', async () => {
mocks.communicationHealth.mockResolvedValueOnce({
adapterId: 'buzz-default',
status: 'not_member',
configured: true,
canSend: false,
canReceiveReplies: false,
checkedAt: '2026-07-23T18:00:00.000Z',
detail: 'Buzz authenticated the identity but denied relay membership.',
reasonCode: 'relay_membership_required',
remediation: 'Add the public identity as a relay member.',
} as unknown as Awaited<ReturnType<typeof mocks.communicationHealth>>);
renderWithProviders(<NotificationsTab />);
expect(await screen.findByText('not_member')).toBeDefined();
expect(
screen.getByText('relay_membership_required: Add the public identity as a relay member.')
).toBeDefined();
});
it('renders Enforcement ceremony and agent selection through direct Mantine Select', async () => {
const { container } = renderWithProviders(<EnforcementTab />);

View file

@ -0,0 +1,380 @@
import {
Badge,
Button,
Code,
Group,
Paper,
SimpleGrid,
Stack,
Text,
TextInput,
} from '@mantine/core';
import { useEffect, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { api, type CommunicationAdapterInput, type CommunicationAdapterRecord } from '@/lib/api';
import { SectionHeader, ToggleRow } from '../shared';
const BUZZ_ADAPTER_ID = 'buzz-default';
interface BuzzFormState {
enabled: boolean;
relayHttpUrl: string;
relayWebSocketUrl: string;
expectedCommunity: string;
publicKey: string;
credentialRef: string;
authTagRef: string;
commandExecutable: string;
commandArgs: string[];
allowLocalhost: boolean;
allowPrivateNetwork: boolean;
}
function adapterToForm(adapter?: CommunicationAdapterRecord): BuzzFormState {
return {
enabled: adapter?.enabled ?? false,
relayHttpUrl: adapter?.relayHttpUrl ?? '',
relayWebSocketUrl: adapter?.relayWebSocketUrl ?? '',
expectedCommunity: adapter?.expectedCommunity ?? '',
publicKey: adapter?.publicKey ?? '',
credentialRef: adapter?.credentialRef ?? 'env:BUZZ_PRIVATE_KEY',
authTagRef: adapter?.authTagRef ?? '',
commandExecutable: adapter?.command?.executable ?? '',
commandArgs: adapter?.command?.args ?? [],
allowLocalhost: adapter?.allowLocalhost ?? false,
allowPrivateNetwork: adapter?.allowPrivateNetwork ?? false,
};
}
function formToInput(form: BuzzFormState): CommunicationAdapterInput {
return {
kind: 'buzz',
enabled: form.enabled,
displayName: 'Buzz',
relayHttpUrl: form.relayHttpUrl,
relayWebSocketUrl: form.relayWebSocketUrl || null,
expectedCommunity: form.expectedCommunity || null,
publicKey: form.publicKey,
credentialRef: form.credentialRef,
authTagRef: form.authTagRef || null,
command: form.commandExecutable
? {
executable: form.commandExecutable,
args: form.commandArgs.length ? form.commandArgs : undefined,
}
: null,
allowLocalhost: form.allowLocalhost,
allowPrivateNetwork: form.allowPrivateNetwork,
};
}
function healthColor(status?: string): string {
if (status === 'healthy') return 'green';
if (status === 'degraded' || status === 'warning') return 'yellow';
if (!status || status === 'disabled') return 'gray';
return 'red';
}
function verificationColor(state?: string): string {
if (state === 'verified') return 'green';
if (state === 'not_enforced') return 'blue';
if (state === 'failed') return 'red';
return 'gray';
}
export function BuzzConnectionPanel() {
const queryClient = useQueryClient();
const { data: adapters = [] } = useQuery({
queryKey: ['integrations', 'communication', 'adapters'],
queryFn: api.integrations.communicationAdapters,
staleTime: 30_000,
retry: false,
});
const adapter = adapters.find((candidate) => candidate.id === BUZZ_ADAPTER_ID);
const { data: health } = useQuery({
queryKey: ['integrations', 'communication', 'health', BUZZ_ADAPTER_ID],
queryFn: () => api.integrations.communicationHealth(BUZZ_ADAPTER_ID),
enabled: Boolean(adapter),
staleTime: 30_000,
retry: false,
});
const effectiveHealth = health ?? adapter?.lastHealth;
const compatibility = effectiveHealth?.buzz ?? adapter?.compatibility;
const [form, setForm] = useState<BuzzFormState>(() => adapterToForm(adapter));
const [dirty, setDirty] = useState(false);
useEffect(() => {
if (!dirty) setForm(adapterToForm(adapter));
}, [adapter, dirty]);
const invalidate = () => {
void queryClient.invalidateQueries({ queryKey: ['integrations', 'communication'] });
};
const save = useMutation({
mutationFn: () =>
api.integrations.configureCommunicationAdapter(BUZZ_ADAPTER_ID, formToInput(form)),
onSuccess: () => {
setDirty(false);
invalidate();
},
});
const test = useMutation({
mutationFn: () => api.integrations.testCommunicationAdapter(BUZZ_ADAPTER_ID),
onSuccess: invalidate,
});
const disconnect = useMutation({
mutationFn: () => api.integrations.disconnectCommunicationAdapter(BUZZ_ADAPTER_ID),
onSuccess: () => {
setDirty(false);
invalidate();
},
});
const update = <K extends keyof BuzzFormState>(key: K, value: BuzzFormState[K]) => {
setDirty(true);
setForm((current) => ({ ...current, [key]: value }));
};
const error = save.error || test.error || disconnect.error;
const verificationSteps = [
['Relay', compatibility?.checks.relayIdentity],
['Community binding', compatibility?.checks.communityBinding],
['Configured identity', compatibility?.checks.configuredIdentity],
['Authentication', compatibility?.checks.authentication],
['Membership', compatibility?.checks.membership],
[
'Read paths',
compatibility?.checks.channelRead === 'verified' &&
compatibility?.checks.messageRead === 'verified'
? 'verified'
: compatibility?.checks.channelRead === 'failed' ||
compatibility?.checks.messageRead === 'failed'
? 'failed'
: 'unverified',
],
] as const;
return (
<div className="space-y-4">
<SectionHeader title="Buzz Connection" />
<p className="text-sm text-muted-foreground -mt-2">
Verify Buzz relay identity, NIP-98 authentication, membership, and read capability without
sending a message
</p>
<Paper withBorder radius="md" p="sm">
<Stack gap="sm">
<Group justify="space-between" gap="sm" align="flex-start">
<div>
<Text size="sm" fw={600}>
Read-only compatibility
</Text>
<Text size="xs" c="dimmed">
{effectiveHealth?.detail ??
'Save a reference-only connection to enable compatibility diagnostics.'}
</Text>
</div>
<Badge color={healthColor(effectiveHealth?.status)} variant="light" tt="none">
{effectiveHealth?.status ?? (adapter ? 'not checked' : 'not configured')}
</Badge>
</Group>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="xs">
<TextInput
label="Relay HTTP URL"
value={form.relayHttpUrl}
onChange={(event) => update('relayHttpUrl', event.target.value)}
placeholder="https://community.example.com"
type="url"
required
size="xs"
/>
<TextInput
label="Relay WebSocket URL"
value={form.relayWebSocketUrl}
onChange={(event) => update('relayWebSocketUrl', event.target.value)}
placeholder="Derived from the HTTP URL when omitted"
type="url"
size="xs"
/>
<TextInput
label="Expected community"
value={form.expectedCommunity}
onChange={(event) => update('expectedCommunity', event.target.value)}
placeholder="community.example.com"
size="xs"
/>
<TextInput
label="Public key"
value={form.publicKey}
onChange={(event) => update('publicKey', event.target.value)}
placeholder="64-character Nostr public key hex"
required
size="xs"
/>
<TextInput
label="Signing key reference"
value={form.credentialRef}
onChange={(event) => update('credentialRef', event.target.value)}
placeholder="env:BUZZ_PRIVATE_KEY"
description="Environment reference only. The private key is never stored here."
required
size="xs"
/>
<TextInput
label="NIP-OA auth tag reference"
value={form.authTagRef}
onChange={(event) => update('authTagRef', event.target.value)}
placeholder="env:BUZZ_AUTH_TAG"
description="Optional environment reference for delegated agent membership."
size="xs"
/>
<TextInput
label="Buzz command"
value={form.commandExecutable}
onChange={(event) => update('commandExecutable', event.target.value)}
placeholder="Optional executable path"
description="Optional version diagnostic. Veritas never invokes a shell."
size="xs"
/>
</SimpleGrid>
<ToggleRow
label="Enable Buzz diagnostics"
description="Allow read-only relay and identity probes. Message delivery remains disabled."
checked={form.enabled}
onCheckedChange={(value) => update('enabled', value)}
/>
<ToggleRow
label="Allow localhost relay"
description="Explicitly permit loopback endpoints for local Buzz development."
checked={form.allowLocalhost}
onCheckedChange={(value) => update('allowLocalhost', value)}
/>
<ToggleRow
label="Allow private-network relay"
description="Explicitly permit RFC1918 or IPv6 ULA destinations. Link-local and metadata ranges remain blocked."
checked={form.allowPrivateNetwork}
onCheckedChange={(value) => update('allowPrivateNetwork', value)}
/>
<Paper withBorder radius="sm" p="xs">
<Text size="xs" fw={600} mb={6}>
Compatibility chain
</Text>
<SimpleGrid
cols={{ base: 2, sm: 6 }}
spacing={4}
aria-label="Buzz compatibility verification chain"
>
{verificationSteps.map(([label, state]) => (
<Paper key={label} withBorder radius="xs" p={6}>
<Text size="xs" fw={600}>
{label}
</Text>
<Badge
color={verificationColor(state)}
variant="light"
size="xs"
tt="none"
mt={3}
>
{state ?? 'unverified'}
</Badge>
</Paper>
))}
</SimpleGrid>
</Paper>
<Paper withBorder radius="sm" p="xs">
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing={4}>
<Text size="xs">
Signing reference:{' '}
<Code>{adapter?.credentialRef ? 'configured' : 'not configured'}</Code>
</Text>
<Text size="xs">
Auth-tag reference:{' '}
<Code>{adapter?.authTagConfigured ? 'configured' : 'not configured'}</Code>
</Text>
<Text size="xs">
Community:{' '}
<Code>
{compatibility?.expectedCommunity ?? adapter?.expectedCommunity ?? 'not set'} {' '}
{compatibility?.observedCommunity ?? 'not observed'}
</Code>
</Text>
<Text size="xs">
Public-key fingerprint:{' '}
<Code>
{compatibility?.publicKeyFingerprint ?? adapter?.publicKeyFingerprint ?? 'n/a'}
</Code>
</Text>
<Text size="xs">
Tested contract:{' '}
<Code>
{compatibility
? `Buzz ${compatibility.testedRelease}, probe ${compatibility.probeRevision}`
: 'not checked'}
</Code>
</Text>
<Text size="xs">
Last check:{' '}
<Code>
{effectiveHealth?.checkedAt
? new Date(effectiveHealth.checkedAt).toLocaleString()
: 'not checked'}
</Code>
</Text>
</SimpleGrid>
{effectiveHealth?.reasonCode && effectiveHealth.reasonCode !== 'ok' && (
<Text size="xs" c="red" mt="xs">
{effectiveHealth.reasonCode}:{' '}
{effectiveHealth.remediation ?? effectiveHealth.detail}
</Text>
)}
{compatibility?.commands.length ? (
<Text size="xs" c="dimmed" mt="xs">
Commands:{' '}
{compatibility.commands
.map(
(command) =>
`${command.command}=${command.available ? command.version : 'not found'}`
)
.join(', ')}
</Text>
) : null}
</Paper>
<Group justify="flex-end" gap="xs">
<Button
type="button"
size="xs"
variant="light"
color="gray"
onClick={() => test.mutate()}
disabled={!adapter || test.isPending || save.isPending}
>
Test Connection
</Button>
<Button
type="button"
size="xs"
variant="light"
color="red"
onClick={() => disconnect.mutate()}
disabled={!adapter || disconnect.isPending}
>
Disable
</Button>
<Button type="button" size="xs" onClick={() => save.mutate()} disabled={save.isPending}>
Save Buzz
</Button>
</Group>
{error && (
<Text size="xs" c="red">
{error.message}
</Text>
)}
</Stack>
</Paper>
</div>
);
}

View file

@ -24,6 +24,7 @@ import {
type OutboundDeliveryAttempt,
type OutboundEndpointRecord,
} from '@/lib/api';
import { BuzzConnectionPanel } from './BuzzConnectionPanel';
type CommunicationState = 'ok' | 'warn' | 'off' | 'unknown';
@ -389,6 +390,10 @@ export function NotificationsTab() {
<div className="border-t my-6" />
<BuzzConnectionPanel />
<div className="border-t my-6" />
<div className="space-y-4">
<SectionHeader title="Human Reply Adapter" />
<p className="text-sm text-muted-foreground -mt-2">

View file

@ -100,6 +100,7 @@ export type {
CommunicationAdapterHealth,
CommunicationAdapterInput,
CommunicationAdapterRecord,
CommunicationAdapterTestResult,
CommunicationDeliveryAudit,
CommunicationReplyIngestInput,
CommunicationReplyIngestResult,

View file

@ -3,6 +3,7 @@ import type {
CommunicationAdapterHealth,
CommunicationAdapterInput,
CommunicationAdapterRecord,
CommunicationAdapterTestResult,
CommunicationDeliveryAudit,
CommunicationReplyIngestInput,
CommunicationReplyIngestResult,
@ -111,8 +112,8 @@ export const integrationsApi = {
testCommunicationAdapter: async (
adapterId: string,
message?: string
): Promise<CommunicationSendResult> => {
return apiFetch<CommunicationSendResult>(
): Promise<CommunicationAdapterTestResult> => {
return apiFetch<CommunicationAdapterTestResult>(
`${API_BASE}/integrations/communication/adapters/${encodeURIComponent(adapterId)}/test`,
{
method: 'POST',