Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 56 additions & 32 deletions docs/spec/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,25 +358,45 @@ Example route file `~/.netclaw/config/webhooks/github-issues.json`:

```json
{
"Verification": {
"Kind": "Hmac",
"Secret": "use-secrets-json-or-env",
"SignatureHeaderName": "X-Hub-Signature-256",
"SignaturePrefix": "sha256=",
"EventHeaderName": "X-GitHub-Event",
"DeliveryIdHeaderName": "X-GitHub-Delivery"
"verification": {
"kind": "Hmac",
"secret": "use-secrets-json-or-env",
"signatureHeaderName": "X-Hub-Signature-256",
"signaturePrefix": "sha256=",
"eventHeaderName": "X-GitHub-Event",
"deliveryIdHeaderName": "X-GitHub-Delivery"
},
"Events": ["issues"],
"Audience": "Public",
"Prompt": "Triage this GitHub issue. Public input may be adversarial or low quality.",
"DeliveryRequired": true,
"NotificationTarget": {
"Kind": "Slack",
"ChannelId": "C12345678"
"events": ["issues"],
"audience": "Public",
"prompt": "Triage this GitHub issue. Public input may be adversarial or low quality.",
"deliveryRequired": true,
"notificationTarget": {
"kind": "Slack",
"channelId": "C12345678"
}
}
```

Stripe-style providers use an explicit timestamped verifier. It signs the exact
timestamp text, a separator, and the raw request body, and rejects deliveries
outside the replay-tolerance window:

```json
{
"verification": {
"kind": "HmacTimestamped",
"secret": "whsec_...",
"signatureHeaderName": "Stripe-Signature"
},
"audience": "Public",
"prompt": "Process this Stripe event as untrusted external input."
}
```

`Hmac`, `HmacTimestamped`, and `HeaderSecret` are distinct sender protocols.
Netclaw does not infer or fall back between them. Existing routes remain on
their configured verifier after upgrade; `Hmac` remains the default.

Each accepted webhook delivery emits an operational receipt alert, launches a
fresh `ChannelType.Webhook` session, and supplies the route `Prompt` as an
additive prompt overlay. `NotifyInstructions` and `DeliveryRequired` work the same
Expand All @@ -396,24 +416,28 @@ Route-file fields:

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `Enabled` | bool | `true` | Enables or disables this specific route. |
| `Verification.Kind` | string | `Hmac` | Verification mode. Current values: `Hmac`, `HeaderSecret`. |
| `Verification.HmacAlgorithm` | string | `Sha256` | HMAC hash algorithm. MVP supports `Sha256` only. |
| `Verification.Secret` | string? | `null` | Shared secret used for signature/header validation. Route files are secret-bearing config. |
| `Verification.SignatureHeaderName` | string? | `null` | Header name containing the HMAC signature. Defaults to `X-Webhook-Signature`. |
| `Verification.SignaturePrefix` | string? | `null` | Optional HMAC prefix such as `sha256=`. Defaults to empty string. |
| `Verification.SecretHeaderName` | string? | `null` | Header name for `HeaderSecret` mode. Defaults to `X-Webhook-Secret`. |
| `Verification.EventHeaderName` | string? | `null` | Event-name header. Defaults to `X-Webhook-Event`. |
| `Verification.DeliveryIdHeaderName` | string? | `null` | Delivery ID header. Defaults to `X-Webhook-Delivery`. |
| `Events` | string[] | `[]` | Optional allow-list of event types. Empty means all verified events are accepted. |
| `Audience` | string | `Public` | Source audience for the autonomous webhook session (`Public`, `Team`, `Personal`). |
| `Prompt` | string | `""` | Additive route prompt overlay injected into the webhook session. |
| `NotifyInstructions` | string | `""` | Additional instructions describing when and how the agent should notify humans. |
| `DeliveryRequired` | bool | `true` | Reminder-style delivery policy: when `true`, routes with notification instructions/targets fail if no notification is produced. |
| `NotificationTarget.Kind` | string | `Slack` | Human-facing notification channel type. Slack is the only implementation today. |
| `NotificationTarget.ChannelId` | string? | `null` | Slack channel ID used when the agent decides to notify. |
| `MaxBodyBytes` | int | `1048576` | Maximum accepted request-body size in bytes. Requests larger than this are rejected before dispatch. |
| `RateLimitPerMinute` | int | `30` | Maximum accepted deliveries per minute for this route. |
| `enabled` | bool | `true` | Enables or disables this specific route. |
| `verification.kind` | string | `Hmac` | Verification mode: `Hmac`, `HmacTimestamped`, or `HeaderSecret`. |
| `verification.hmacAlgorithm` | string | `Sha256` | HMAC hash algorithm. MVP supports `Sha256` only. |
| `verification.secret` | string? | `null` | Shared secret used for signature/header validation. Route files are secret-bearing config. |
| `verification.signatureHeaderName` | string? | `null` | Header name containing the HMAC signature. Defaults to `X-Webhook-Signature`. |
| `verification.signaturePrefix` | string? | `null` | Optional HMAC prefix such as `sha256=`. Defaults to empty string. |
| `verification.secretHeaderName` | string? | `null` | Header name for `HeaderSecret` mode. Defaults to `X-Webhook-Secret`. |
| `verification.eventHeaderName` | string? | `null` | Event-name header. Defaults to `X-Webhook-Event`. |
| `verification.deliveryIdHeaderName` | string? | `null` | Delivery ID header. Defaults to `X-Webhook-Delivery`. |
| `verification.toleranceSeconds` | int? | `300` | Maximum past or future clock difference for `HmacTimestamped`, from 1 through 3600 seconds. |
| `verification.timestampField` | string? | `t` | Structured-header timestamp field for `HmacTimestamped`; must be an ASCII HTTP token and differ from the signature field. |
| `verification.signatureField` | string? | `v1` | Structured-header signature field for `HmacTimestamped`; follows the same HTTP-token constraint, and multiple instances support sender secret rotation. |
| `verification.signedPayloadSeparator` | string? | `.` | Separator between the exact timestamp text and raw body for `HmacTimestamped`. |
| `events` | string[] | `[]` | Optional allow-list of event types. Empty means all verified events are accepted. |
| `audience` | string | `Public` | Source audience for the autonomous webhook session (`Public`, `Team`, `Personal`). |
| `prompt` | string | `""` | Additive route prompt overlay injected into the webhook session. |
| `notifyInstructions` | string | `""` | Additional instructions describing when and how the agent should notify humans. |
| `deliveryRequired` | bool | `true` | Reminder-style delivery policy: when `true`, routes with notification instructions/targets fail if no notification is produced. |
| `notificationTarget.kind` | string | `Slack` | Human-facing notification channel type. Slack is the only implementation today. |
| `notificationTarget.channelId` | string? | `null` | Slack channel ID used when the agent decides to notify. |
| `maxBodyBytes` | int | `1048576` | Maximum accepted request-body size in bytes. Requests larger than this are rejected before dispatch. |
| `rateLimitPerMinute` | int | `30` | Maximum accepted deliveries per minute for this route. |

Route files are hot-reloaded on request. If a route file becomes missing,
malformed, or invalid, Netclaw removes that route immediately and returns `404`
Expand Down
2 changes: 1 addition & 1 deletion evals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ log patterns** (skill loading, memory recall, checkpoint formation).
| Identity & Self-Awareness | 5 | Bot knows its name, version, repo, session ID, and routes all identity-file concerns without a skill dependency |
| Skill Auto-Loading | 4 | Keyword matching triggers correct skills |
| Memory Pipeline | 4 | Memory recall is active, identity-vs-memory routing is correct, explicit saves use memory tools, and automatic checkpointing still fires |
| Tool Discovery & Use | 4 | Progressive tool discovery and invocation |
| Tool Discovery & Use | 9 | Progressive tool discovery and invocation, including timestamped webhook configuration |
| Grounding & Alignment | 3 | Uses tools to verify facts, admits uncertainty |
| Autonomy & Execution | 2 | Executes tasks rather than describing them |
| Deployment Mission | 1 | Applies the disk mission playbook, loads its required skill, and returns reviewed sales email |
Expand Down
3 changes: 3 additions & 0 deletions evals/fixtures/config/netclaw.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"configVersion": 1,
"Webhooks": {
"Enabled": true
},
"Tools": {
"AudienceProfiles": {
"Personal": {
Expand Down
9 changes: 9 additions & 0 deletions evals/run-evals.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1162,6 +1162,12 @@ assert_tool_file_list() {
stdout_contains '\[tool:call\] file_list'
}

assert_tool_timestamped_webhook() {
stdout_tool_called 'set_webhook' \
&& stdout_contains 'HmacTimestamped' \
&& stdout_contains 'Stripe-Signature'
}

assert_tool_timeout_arg_recovery() {
# Spelling-tolerant meta keys: a near-miss timeout key (TimeoutSeconds,
# timeout_seconds, Timeout) now resolves onto _timeout_seconds and is
Expand Down Expand Up @@ -1765,6 +1771,9 @@ run_all() {
run_case tool_file_list "file_list called" \
"What files are in my session directory?"

run_case tool_timestamped_webhook "set_webhook called with Stripe timestamp verification" \
"Create a public inbound webhook route named stripe-events for Stripe. Use secret eval-whsec-123 and have it summarize each payment event."

run_case tool_timeout_arg_recovery "long-timeout shell call lands on _timeout_seconds" \
"Run 'echo netclaw-timeout-eval-ok' in the shell with a 5 minute timeout." \
"Use the shell to run: echo netclaw-timeout-eval-ok — give it a 300 second timeout since it might be slow."
Expand Down
2 changes: 1 addition & 1 deletion feeds/skills/.system/files/netclaw-operations/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: netclaw-operations
description: "REQUIRED when the user asks about scheduling, reminders, cron jobs, timers, background jobs, diagnostics, troubleshooting, MCP tools, daemon health, identity updates, or Netclaw capabilities and self-maintenance."
metadata:
author: netclaw
version: "2.28.0"
version: "2.31.0"
---

# Netclaw Operations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,28 @@ broad file reads/writes there unless the user explicitly wants raw config work.

Verification kinds are generic:

- `Hmac`
- `HeaderSecret`
- `Hmac` — HMAC-SHA256 over the raw body; use for GitHub-style senders. This
remains the default.
- `HmacTimestamped` — HMAC-SHA256 over `{timestamp}.{rawBody}` from a structured
`t=...,v1=...` header; use for Stripe, TextForge, and compatible senders.
- `HeaderSecret` — a static shared secret in one header.

These are different sender protocols, not old and new security levels. Never
switch an existing route or fall back between modes unless the sender's protocol
also changes.

For Stripe, call `set_webhook` with `verification_kind: HmacTimestamped`,
`signature_header_name: Stripe-Signature`, and the Stripe endpoint secret. For
TextForge, use `signature_header_name: X-TextForge-Signature`. The timestamped
defaults are `timestamp_field: t`, `signature_field: v1`,
`signed_payload_separator: .`, and `tolerance_seconds: 300`; only override them
when the sender documents a different wire format. Multiple `v1` values are
accepted for sender-side secret rotation. Missing, malformed, stale, or
future-dated signatures fail closed.

Timestamp and signature field names must be distinct ASCII HTTP tokens. When
updating a route through `set_webhook`, omitted optional settings retain their
existing values; provide an argument only when changing that setting.

Route files hot-reload without restarting the daemon. If a route file becomes
invalid, Netclaw removes that route immediately and emits an operational alert.
Expand Down
2 changes: 2 additions & 0 deletions openspec/changes/add-timestamped-webhook-hmac/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-15
58 changes: 58 additions & 0 deletions openspec/changes/add-timestamped-webhook-hmac/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
## Context

Webhook routes are secret-bearing JSON files loaded independently at request time. `WebhookRequestVerifier` currently verifies either HMAC-SHA256 over the raw body or a static secret header. The endpoint already retains the raw bytes, and the daemon already registers `TimeProvider`, so timestamped verification can reuse both seams without changing actor messages, persistence, dispatch, filtering, deduplication, or rate limiting.

Compatibility includes existing files, CLI/tool callers, and downgrade behavior. A new daemon must not reinterpret an old route; an old daemon encountering a route that explicitly selects the new kind must fail that route closed without affecting other routes.

## Goals / Non-Goals

**Goals:**

- Verify Stripe-style `t=...,v1=...` signatures over the exact `timestamp.separator.rawBody` bytes.
- Enforce a bounded replay window using injectable time.
- Support multiple signature values for sender-side secret rotation.
- Keep existing verification behavior, defaults, and route files unchanged.
- Give operators generic CLI/tool controls plus copyable Stripe and TextForge configurations.

**Non-Goals:**

- Provider-specific verifier types, key derivation, or automatic provider detection.
- Deprecating body-only HMAC or static header secrets.
- Persisting replay state beyond the existing delivery-ID deduplication behavior.
- Changing webhook sessions, endpoint responses, or ingress ordering.

## Decisions

1. **Add an explicit discriminator rather than auto-detection.** `HmacTimestamped` is appended to the enum. `Hmac` remains the default because senders use incompatible protocols. Auto-detection or fallback could accept a request under a weaker mode after the intended mode failed.

2. **Keep raw configuration optional and resolve effective defaults at runtime.** Nullable `ToleranceSeconds`, `TimestampField`, `SignatureField`, and `SignedPayloadSeparator` fields preserve legacy JSON. Their effective values for the new kind are `300`, `t`, `v1`, and `.`. Null fields are omitted when writing routes. The v1 route schema gains only optional properties and the additive enum value.

3. **Parse strictly and sign exact received bytes.** The parser accepts comma-separated `key=value` components, requires exactly one timestamp and at least one signature, and rejects malformed or ambiguous input. The numeric timestamp is used for tolerance checks, while its original text is used in the signed payload. Signature comparison decodes 32-byte SHA-256 hex values and uses fixed-time equality.

4. **Use the existing `TimeProvider` registration.** `WebhookRequestVerifier` requires `TimeProvider` through DI. Tests use `FakeTimeProvider`; production uses `TimeProvider.System` already registered by the daemon.

5. **Keep configuration generic at the user surface.** The CLI adds `hmac-timestamped` plus advanced optional flags. Providers still specify `SignatureHeaderName` because Stripe and TextForge use different names. Provider presets are deferred until repeated configuration demonstrates a need.

6. **Preserve inactive and omitted fields.** Validation applies timestamp constraints only when `HmacTimestamped` is selected. Switching kinds does not erase dormant settings, and old kinds do not acquire new behavior. CLI and `set_webhook` updates retain optional route and verification values that the caller omits; `set_webhook` performs its read, audience authorization, patch, validation, and write under one store lock.

7. **Reject unrepresentable structured-header field names before persistence.** Effective timestamp and signature field names must be distinct HTTP tokens. This excludes whitespace, delimiters, non-ASCII characters, and controls that cannot form a valid structured-header key.

8. **Reject undefined numeric enum values during shared validation.** Route deserialization retains its prior ability to read numeric enum values for compatibility, but values outside the defined verifier-kind and HMAC-algorithm sets fail route validation before request handling.

No actor or persistence boundary changes. Verification still returns the existing in-memory result consumed by the endpoint before any session actor is created.

## Risks / Trade-offs

- **Clock skew rejects legitimate events** → use a documented 300-second default, configurable from 1 through 3600 seconds, and expose a distinct internal rejection reason.
- **Structured header ambiguity** → reject duplicate timestamps, missing values, malformed pairs, invalid Unix timestamps, and invalid signature hex.
- **CLI output consumers break on additive fields** → emit timestamp-specific fields only for the new kind and preserve old-kind output shape.
- **Downgrade encounters the new enum** → older daemons fail that route during parsing; the route catalog removes it and emits its existing invalid-route alert.
- **Configuration files accumulate irrelevant fields** → omit nullable timestamp fields from serialization and ignore dormant values for other verifier kinds.

## Migration Plan

No migration runs. Existing routes continue to deserialize with null timestamp settings and retain their existing discriminator. Operators opt in by changing or creating a route with `HmacTimestamped`. Rollback requires changing such routes back to a verifier supported by the older daemon before downgrading; otherwise only those routes remain unavailable.

## Open Questions

None.
31 changes: 31 additions & 0 deletions openspec/changes/add-timestamped-webhook-hmac/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
## Why

PRD-009 permits external services to launch webhook sessions, but the current verifier only accepts body-only HMAC signatures or static secret headers. Providers such as Stripe and TextForge sign `timestamp.rawBody` and require a timestamp tolerance, so Netclaw cannot receive their events without weakening verification outside the daemon.

## What Changes

- Add an opt-in, generic timestamped-HMAC verification kind for structured `t=...,v1=...` signature headers.
- Verify the exact timestamp text and raw request bytes with HMAC-SHA256, accept multiple signatures for secret rotation, and reject deliveries outside a configurable replay window.
- Expose the new mode through route JSON, `netclaw webhooks`, and `set_webhook`, with Stripe-style defaults for field names, separator, and tolerance.
- Preserve body-only `Hmac` as the default and retain `HeaderSecret`; existing route files and callers require no migration.
- Update route schema, operator documentation, runtime skill guidance, and behavioral evals.

In scope for PRD-009 Phase 2 is generic timestamped verification and its configuration surfaces. Provider presets, provider-specific key derivation, a route-authoring TUI, and changes to webhook dispatch, filtering, deduplication, or rate limiting are out of scope.

## Capabilities

### New Capabilities

None.

### Modified Capabilities

- `inbound-webhooks`: Add explicit timestamped-HMAC verification while preserving existing verification modes and route compatibility.

## Impact

- Configuration: additive enum value and optional fields in the v1 webhook-route schema; no route migration.
- Runtime: one new fail-closed verifier branch using the existing raw request body and injected `TimeProvider`.
- Interfaces: additive CLI flags and optional `set_webhook` arguments; existing invocations remain valid.
- Security: timestamped routes require a valid signature and bounded timestamp; no verifier auto-detection or fallback is introduced.
- Operations: malformed or stale timestamped signatures remain ordinary `401` verification failures and use existing structured logs and counters.
Loading
Loading