Skip to content

Commit 5abac69

Browse files
feat(surface,sdk): provider-specific event triggers via codegen (#346)
1 parent 5ab55e0 commit 5abac69

26 files changed

Lines changed: 822 additions & 10 deletions

‎docs/evidence/spec-X/README.md‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Slice X verification
2+
3+
Implemented mapping-driven Slack/GitHub declarations, provider envelope ingress,
4+
and lowering to the existing webhook inbox executor. The generator also accepts
5+
an adapter checkout. No kernel production code changed.
6+
7+
Commands and captured outputs:
8+
9+
- [Surface typecheck](surface-typecheck.txt): exit 0.
10+
- [Surface regression typecheck and generated helper check](surface-regressions.txt): exit 0.
11+
- [SDK source and type contracts](sdk-typecheck.txt): exit 0.
12+
- [SDK test typecheck](sdk-test-types.txt): exit 0.
13+
- [Surface suite](surface-tests.txt): 28 passed.
14+
- [Provider executor and codegen tests](provider-tests.txt): 7 passed. These
15+
exercise the compiled subscriptions through the real kernel CLI, including
16+
provider/type/payload nonmatches, distinct event IDs, and durable deduplication.
17+
- [Existing inbox watcher tests](inbox-watcher.txt): 3 passed.
18+
- [Full SDK suite attempt](sdk.txt): 185 failed, 1109 passed, 10 skipped,
19+
14 errors. This is **not a green full-suite result**. The provider HTTP tests
20+
fail at socket creation with `listen EPERM: operation not permitted 127.0.0.1`.
21+
The full transcript also contains Unix socket permission failures, a filesystem
22+
watch `EMFILE`, and a wrapper identification timeout. The SDK transcript excerpt
23+
links the complete local log. The additional provider kernel CLI tests were
24+
completed separately after this full-suite attempt.
25+
26+
Local dependencies were installed from the npm cache for the SDK. The surface's
27+
pre-existing npm lockfile omits its relay-helpers peer dependency; its installed
28+
dependencies were copied from the local helpers worktree, with `ai-hist` copied
29+
from the SDK installation. The SDK used this worktree's built surface through a
30+
local node_modules link. These setup changes do not modify tracked lockfiles.
31+
32+
One surface regression attempt hit a transient `ENOSPC` while creating a temp
33+
directory; the captured rerun passed. Socket restrictions remain unresolved:
34+
this session cannot request execution outside the sandbox. The HTTP/daemon tests
35+
are retained and must be rerun in an environment that permits local sockets.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
Command: PATH=/Users/khaliqgant/.cargo/bin:$PATH sh ../ops/cargo.sh test --offline -p relayflowd --test trigger_watcher
2+
Working directory: /Users/khaliqgant/fl-slice-X/kernel
3+
4+
Compiling bitflags v2.13.1
5+
Compiling getrandom v0.4.3
6+
Compiling rustix v1.1.4
7+
Compiling errno v0.3.14
8+
Compiling fastrand v2.5.0
9+
Compiling rusqlite v0.37.0
10+
Compiling relayflowd-journal v0.1.0 (/Users/khaliqgant/fl-slice-X/kernel/relayflowd-journal)
11+
Compiling tempfile v3.27.0
12+
Compiling relayflowd v0.1.0 (/Users/khaliqgant/fl-slice-X/kernel/relayflowd)
13+
Finished `test` profile [unoptimized + debuginfo] target(s) in 13.38s
14+
Running tests/trigger_watcher.rs (/Users/khaliqgant/.relayflows-toolchain/target/1463204285/debug/deps/trigger_watcher-dcffffe2d25ba6f1)
15+
16+
running 3 tests
17+
test retains_bad_and_unregistered_events_while_consuming_filter_nonmatches ... ok
18+
test failed_archive_retries_the_same_durable_run ... ok
19+
test journals_payload_and_filename_key_then_archives_and_dedupes_replay ... ok
20+
21+
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s
22+
23+
Exit code: 0
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Command: RELAYFLOWD_BIN=/Users/khaliqgant/.relayflows-toolchain/target/1463204285/debug/relayflowd ./node_modules/.bin/vitest run tests/provider-trigger-executor.test.ts tests/generate-triggers.test.ts
2+
Working directory: /Users/khaliqgant/fl-slice-X/packages/sdk
3+
4+
RUN v2.1.9 /Users/khaliqgant/fl-slice-X/packages/sdk
5+
6+
✓ tests/provider-trigger-executor.test.ts (4 tests) 138ms
7+
✓ tests/generate-triggers.test.ts (3 tests) 645ms
8+
✓ discovers new adapters, preserves exact event names, and prefers adapter-local mappings 369ms
9+
10+
Test Files 2 passed (2)
11+
Tests 7 passed (7)
12+
Start at 23:55:10
13+
Duration 881ms (transform 82ms, setup 0ms, collect 128ms, tests 784ms, environment 0ms, prepare 69ms)
14+
15+
Exit code: 0
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Command: npm run typecheck:tests
2+
Working directory: /Users/khaliqgant/fl-slice-X/packages/sdk
3+
4+
5+
> @relayflows/sdk@2.0.8 typecheck:tests
6+
> tsc -p tsconfig.tests.json
7+
8+
9+
Exit code: 0
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Command: npm run typecheck
2+
Working directory: /Users/khaliqgant/fl-slice-X/packages/sdk
3+
4+
5+
> @relayflows/sdk@2.0.8 typecheck
6+
> tsc --noEmit && tsc -p tsconfig.type-tests.json
7+
8+
9+
Exit code: 0

‎docs/evidence/spec-X/sdk.txt‎

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
Command: PATH=/Users/khaliqgant/.cargo/bin:/Users/khaliqgant/.bun/bin:$PATH RELAYFLOWD_BIN=/Users/khaliqgant/.relayflows-toolchain/target/1463204285/debug/relayflowd npm test
2+
Working directory: /Users/khaliqgant/fl-slice-X/packages/sdk
3+
4+
Full captured transcript: /private/tmp/fl-slice-X-sdk-tests-yk4hiyhm.log
5+
6+
Captured webhook failure excerpt:
7+
FAIL tests/webhook-live.test.ts > executes and deduplicates 'app_mention' only for its provider and matching payload
8+
FAIL tests/webhook-live.test.ts > executes and deduplicates 'pull_request' only for its provider and matching payload
9+
Error: webhook integration timed out: FAILED [webhook_server] listen EPERM: operation not permitted 127.0.0.1
10+
11+
❯ until tests/webhook-live.test.ts:39:9
12+
37| const deadline = Date.now() + 10_000;
13+
38| while (Date.now() < deadline) { if (await predicate()) return; await…
14+
39| throw new Error(`webhook integration timed out: ${detail()}`);
15+
| ^
16+
40| }
17+
41| async function daemon(dir: string): Promise<ChildProcess> {
18+
❯ setup tests/webhook-live.test.ts:60:3
19+
❯ tests/webhook-live.test.ts:84:25
20+
21+
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[107/187]⎯
22+
23+
FAIL tests/webhook-live.test.ts > executes and deduplicates 'reaction_added' only for its provider and matching payload
24+
Error: webhook integration timed out: FAILED [webhook_server] listen EPERM: operation not permitted 127.0.0.1
25+
26+
❯ until tests/webhook-live.test.ts:39:9
27+
37| const deadline = Date.now() + 10_000;
28+
38| while (Date.now() < deadline) { if (await predicate()) return; await…
29+
39| throw new Error(`webhook integration timed out: ${detail()}`);
30+
| ^
31+
40| }
32+
41| async function daemon(dir: string): Promise<ChildProcess> {
33+
❯ setup tests/webhook-live.test.ts:60:3
34+
❯ tests/webhook-live.test.ts:84:25
35+
36+
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[108/187]⎯
37+
38+
FAIL tests/webhook-live.test.ts > flows serve-webhook writes JSON before the daemon starts, then journals and archives exactly once
39+
Error: webhook integration timed out: FAILED [webhook_server] listen EPERM: operation not permitted 127.0.0.1
40+
41+
❯ until tests/webhook-live.test.ts:39:9
42+
37| const deadline = Date.now() + 10_000;
43+
38| while (Date.now() < deadline) { if (await predicate()) return; await…
44+
39| throw new Error(`webhook integration timed out: ${detail()}`);
45+
| ^
46+
40| }
47+
41| async function daemon(dir: string): Promise<ChildProcess> {
48+
❯ setup tests/webhook-live.test.ts:60:3
49+
❯ tests/webhook-live.test.ts:116:25
50+
51+
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[109/187]⎯
52+
53+
FAIL tests/webhook-live.test.ts > replays a dropped file after SIGKILL before spawn
54+
Error: webhook integration timed out: FAILED [webhook_server] listen EPERM: operation not permitted 127.0.0.1
55+
56+
❯ until tests/webhook-live.test.ts:39:9
57+
37| const deadline = Date.now() + 10_000;
58+
38| while (Date.now() < deadline) { if (await predicate()) return; await…
59+
39| throw new Error(`webhook integration timed out: ${detail()}`);
60+
| ^
61+
40| }
62+
41| async function daemon(dir: string): Promise<ChildProcess> {
63+
❯ setup tests/webhook-live.test.ts:60:3
64+
❯ tests/webhook-live.test.ts:136:25
65+
66+
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[110/187]⎯
67+
68+
FAIL tests/webhook-live.test.ts > resumes the same journal after SIGKILL after spawn and before acknowledgement
69+
Error: webhook integration timed out: FAILED [webhook_server] listen EPERM: operation not permitted 127.0.0.1
70+
71+
❯ until tests/webhook-live.test.ts:39:9
72+
37| const deadline = Date.now() + 10_000;
73+
38| while (Date.now() < deadline) { if (await predicate()) return; await…
74+
39| throw new Error(`webhook integration timed out: ${detail()}`);
75+
| ^
76+
40| }
77+
41| async function daemon(dir: string): Promise<ChildProcess> {
78+
❯ setup tests/webhook-live.test.ts:60:3
79+
❯ tests/webhook-live.test.ts:149:25
80+
81+
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[111/187]⎯
82+
83+
FAIL tests/webhook.test.ts > webhook ingress > routes provider envelopes to isolated inboxes and rejects spoofed or unsupported events
84+
FAIL tests/webhook.test.ts > webhook ingress > accepts JSON through atomic files without creating a daemon
85+
FAIL tests/webhook.test.ts > webhook ingress > rejects malformed, oversized, traversal, and non-POST requests
86+
FAIL tests/webhook.test.ts > webhook ingress > fails closed on a symlink inbox target
87+
Error: listen EPERM: operation not permitted 127.0.0.1
88+
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[112/187]⎯
89+
90+
91+
Captured suite summary:
92+
Test Files 25 failed | 55 passed | 1 skipped (81)
93+
Tests 185 failed | 1109 passed | 10 skipped (1304)
94+
Errors 14 errors
95+
Start at 23:52:24
96+
Duration 68.17s (transform 1.54s, setup 0ms, collect 15.17s, tests 323.65s, environment 9ms, prepare 2.95s)
97+
98+
99+
Exit code: 1
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Command: npm run typecheck:regressions
2+
Working directory: /Users/khaliqgant/fl-slice-X/packages/surface
3+
4+
5+
> @relayflows/surface@2.0.8 typecheck:regressions
6+
> tsc -p ../../regressions/tsconfig.json && tsc -p tsconfig.test.json && node scripts/check-generated-helpers.mjs
7+
8+
HELPERS_GENERATED_OK index.ts, slack.ts
9+
10+
Exit code: 0
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Command: PATH=/Users/khaliqgant/.bun/bin:$PATH npm test
2+
Working directory: /Users/khaliqgant/fl-slice-X/packages/surface
3+
4+
> @relayflows/surface@2.0.8 test
5+
> bun run build && tsc -p tsconfig.test.json && vitest run
6+
7+
$ tsc
8+
9+
RUN v2.1.9 /Users/khaliqgant/fl-slice-X/packages/surface
10+
11+
✓ tests/triggers.test.ts (4 tests) 4ms
12+
✓ tests/provider-triggers.test.ts (3 tests) 5ms
13+
✓ tests/flow.test.ts (20 tests) 7ms
14+
✓ tests/helpers.snapshot.test.ts (1 test) 661ms
15+
✓ regenerates helpers byte-identically from the pinned adapter 660ms
16+
17+
Test Files 4 passed (4)
18+
Tests 28 passed (28)
19+
Start at 23:51:18
20+
Duration 1.55s (transform 64ms, setup 0ms, collect 913ms, tests 677ms, environment 0ms, prepare 387ms)
21+
22+
Exit code: 0
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Command: npm run typecheck
2+
Working directory: /Users/khaliqgant/fl-slice-X/packages/surface
3+
4+
5+
> @relayflows/surface@2.0.8 typecheck
6+
> tsc --noEmit
7+
8+
9+
Exit code: 0

‎packages/sdk/src/cli/serve-webhook.ts‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createServer, type Server, type ServerResponse } from 'node:http';
44
import { join, resolve } from 'node:path';
55
import { TextDecoder } from 'node:util';
66
import type { CliIo } from '../cli.js';
7+
import { providerInboxEvent } from '../trigger-executor.js';
78

89
const MAX_BODY_BYTES = 1024 * 1024;
910
const NAME = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
@@ -32,7 +33,7 @@ function reply(response: ServerResponse, status: number, body: object): void {
3233
response.end(JSON.stringify(body));
3334
}
3435

35-
/** POST /<name> accepts JSON, including scalar values. No daemon connection. */
36+
/** POST /<name> accepts JSON; /providers/<provider> accepts typed event envelopes. */
3637
export async function startWebhookServer(dataDir: string, port: number): Promise<Server> {
3738
const inbox = join(resolve(dataDir), 'inbox');
3839
await directory(inbox);
@@ -45,7 +46,8 @@ export async function startWebhookServer(dataDir: string, port: number): Promise
4546
reply(response, 405, { error: 'method_not_allowed' });
4647
return;
4748
}
48-
const name = request.url?.slice(1);
49+
const providerRoute = request.url?.startsWith('/providers/') ?? false;
50+
const name = request.url?.slice(providerRoute ? '/providers/'.length : 1);
4951
if (!name || !NAME.test(name)) {
5052
request.resume();
5153
reply(response, 404, { error: 'invalid_webhook_name' });
@@ -74,6 +76,15 @@ export async function startWebhookServer(dataDir: string, port: number): Promise
7476
reply(response, 400, { error: 'invalid_json' });
7577
return;
7678
}
79+
if (providerRoute) {
80+
try {
81+
payload = providerInboxEvent(name, payload);
82+
} catch (error) {
83+
reply(response, 400, { error: 'invalid_provider_event',
84+
message: error instanceof Error ? error.message : 'invalid provider event' });
85+
return;
86+
}
87+
}
7788
const target = join(inbox, name);
7889
await directory(target);
7990
const id = randomUUID();

0 commit comments

Comments
 (0)