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
35 changes: 35 additions & 0 deletions docs/evidence/spec-X/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Slice X verification

Implemented mapping-driven Slack/GitHub declarations, provider envelope ingress,
and lowering to the existing webhook inbox executor. The generator also accepts
an adapter checkout. No kernel production code changed.

Commands and captured outputs:

- [Surface typecheck](surface-typecheck.txt): exit 0.
- [Surface regression typecheck and generated helper check](surface-regressions.txt): exit 0.
- [SDK source and type contracts](sdk-typecheck.txt): exit 0.
- [SDK test typecheck](sdk-test-types.txt): exit 0.
- [Surface suite](surface-tests.txt): 28 passed.
- [Provider executor and codegen tests](provider-tests.txt): 7 passed. These
exercise the compiled subscriptions through the real kernel CLI, including
provider/type/payload nonmatches, distinct event IDs, and durable deduplication.
- [Existing inbox watcher tests](inbox-watcher.txt): 3 passed.
- [Full SDK suite attempt](sdk.txt): 185 failed, 1109 passed, 10 skipped,
14 errors. This is **not a green full-suite result**. The provider HTTP tests
fail at socket creation with `listen EPERM: operation not permitted 127.0.0.1`.
The full transcript also contains Unix socket permission failures, a filesystem
watch `EMFILE`, and a wrapper identification timeout. The SDK transcript excerpt
links the complete local log. The additional provider kernel CLI tests were
completed separately after this full-suite attempt.

Local dependencies were installed from the npm cache for the SDK. The surface's
pre-existing npm lockfile omits its relay-helpers peer dependency; its installed
dependencies were copied from the local helpers worktree, with `ai-hist` copied
from the SDK installation. The SDK used this worktree's built surface through a
local node_modules link. These setup changes do not modify tracked lockfiles.

One surface regression attempt hit a transient `ENOSPC` while creating a temp
directory; the captured rerun passed. Socket restrictions remain unresolved:
this session cannot request execution outside the sandbox. The HTTP/daemon tests
are retained and must be rerun in an environment that permits local sockets.
23 changes: 23 additions & 0 deletions docs/evidence/spec-X/inbox-watcher.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Command: PATH=/Users/khaliqgant/.cargo/bin:$PATH sh ../ops/cargo.sh test --offline -p relayflowd --test trigger_watcher
Working directory: /Users/khaliqgant/fl-slice-X/kernel

Compiling bitflags v2.13.1
Compiling getrandom v0.4.3
Compiling rustix v1.1.4
Compiling errno v0.3.14
Compiling fastrand v2.5.0
Compiling rusqlite v0.37.0
Compiling relayflowd-journal v0.1.0 (/Users/khaliqgant/fl-slice-X/kernel/relayflowd-journal)
Compiling tempfile v3.27.0
Compiling relayflowd v0.1.0 (/Users/khaliqgant/fl-slice-X/kernel/relayflowd)
Finished `test` profile [unoptimized + debuginfo] target(s) in 13.38s
Running tests/trigger_watcher.rs (/Users/khaliqgant/.relayflows-toolchain/target/1463204285/debug/deps/trigger_watcher-dcffffe2d25ba6f1)

running 3 tests
test retains_bad_and_unregistered_events_while_consuming_filter_nonmatches ... ok
test failed_archive_retries_the_same_durable_run ... ok
test journals_payload_and_filename_key_then_archives_and_dedupes_replay ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s

Exit code: 0
15 changes: 15 additions & 0 deletions docs/evidence/spec-X/provider-tests.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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
Working directory: /Users/khaliqgant/fl-slice-X/packages/sdk

RUN v2.1.9 /Users/khaliqgant/fl-slice-X/packages/sdk

✓ tests/provider-trigger-executor.test.ts (4 tests) 138ms
✓ tests/generate-triggers.test.ts (3 tests) 645ms
✓ discovers new adapters, preserves exact event names, and prefers adapter-local mappings 369ms

Test Files 2 passed (2)
Tests 7 passed (7)
Start at 23:55:10
Duration 881ms (transform 82ms, setup 0ms, collect 128ms, tests 784ms, environment 0ms, prepare 69ms)

Exit code: 0
9 changes: 9 additions & 0 deletions docs/evidence/spec-X/sdk-test-types.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Command: npm run typecheck:tests
Working directory: /Users/khaliqgant/fl-slice-X/packages/sdk


> @relayflows/sdk@2.0.8 typecheck:tests
> tsc -p tsconfig.tests.json


Exit code: 0
9 changes: 9 additions & 0 deletions docs/evidence/spec-X/sdk-typecheck.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Command: npm run typecheck
Working directory: /Users/khaliqgant/fl-slice-X/packages/sdk


> @relayflows/sdk@2.0.8 typecheck
> tsc --noEmit && tsc -p tsconfig.type-tests.json


Exit code: 0
99 changes: 99 additions & 0 deletions docs/evidence/spec-X/sdk.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
Command: PATH=/Users/khaliqgant/.cargo/bin:/Users/khaliqgant/.bun/bin:$PATH RELAYFLOWD_BIN=/Users/khaliqgant/.relayflows-toolchain/target/1463204285/debug/relayflowd npm test
Working directory: /Users/khaliqgant/fl-slice-X/packages/sdk

Full captured transcript: /private/tmp/fl-slice-X-sdk-tests-yk4hiyhm.log

Captured webhook failure excerpt:
FAIL tests/webhook-live.test.ts > executes and deduplicates 'app_mention' only for its provider and matching payload
FAIL tests/webhook-live.test.ts > executes and deduplicates 'pull_request' only for its provider and matching payload
Error: webhook integration timed out: FAILED [webhook_server] listen EPERM: operation not permitted 127.0.0.1

❯ until tests/webhook-live.test.ts:39:9
37| const deadline = Date.now() + 10_000;
38| while (Date.now() < deadline) { if (await predicate()) return; await…
39| throw new Error(`webhook integration timed out: ${detail()}`);
| ^
40| }
41| async function daemon(dir: string): Promise<ChildProcess> {
❯ setup tests/webhook-live.test.ts:60:3
❯ tests/webhook-live.test.ts:84:25

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[107/187]⎯

FAIL tests/webhook-live.test.ts > executes and deduplicates 'reaction_added' only for its provider and matching payload
Error: webhook integration timed out: FAILED [webhook_server] listen EPERM: operation not permitted 127.0.0.1

❯ until tests/webhook-live.test.ts:39:9
37| const deadline = Date.now() + 10_000;
38| while (Date.now() < deadline) { if (await predicate()) return; await…
39| throw new Error(`webhook integration timed out: ${detail()}`);
| ^
40| }
41| async function daemon(dir: string): Promise<ChildProcess> {
❯ setup tests/webhook-live.test.ts:60:3
❯ tests/webhook-live.test.ts:84:25

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[108/187]⎯

FAIL tests/webhook-live.test.ts > flows serve-webhook writes JSON before the daemon starts, then journals and archives exactly once
Error: webhook integration timed out: FAILED [webhook_server] listen EPERM: operation not permitted 127.0.0.1

❯ until tests/webhook-live.test.ts:39:9
37| const deadline = Date.now() + 10_000;
38| while (Date.now() < deadline) { if (await predicate()) return; await…
39| throw new Error(`webhook integration timed out: ${detail()}`);
| ^
40| }
41| async function daemon(dir: string): Promise<ChildProcess> {
❯ setup tests/webhook-live.test.ts:60:3
❯ tests/webhook-live.test.ts:116:25

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[109/187]⎯

FAIL tests/webhook-live.test.ts > replays a dropped file after SIGKILL before spawn
Error: webhook integration timed out: FAILED [webhook_server] listen EPERM: operation not permitted 127.0.0.1

❯ until tests/webhook-live.test.ts:39:9
37| const deadline = Date.now() + 10_000;
38| while (Date.now() < deadline) { if (await predicate()) return; await…
39| throw new Error(`webhook integration timed out: ${detail()}`);
| ^
40| }
41| async function daemon(dir: string): Promise<ChildProcess> {
❯ setup tests/webhook-live.test.ts:60:3
❯ tests/webhook-live.test.ts:136:25

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[110/187]⎯

FAIL tests/webhook-live.test.ts > resumes the same journal after SIGKILL after spawn and before acknowledgement
Error: webhook integration timed out: FAILED [webhook_server] listen EPERM: operation not permitted 127.0.0.1

❯ until tests/webhook-live.test.ts:39:9
37| const deadline = Date.now() + 10_000;
38| while (Date.now() < deadline) { if (await predicate()) return; await…
39| throw new Error(`webhook integration timed out: ${detail()}`);
| ^
40| }
41| async function daemon(dir: string): Promise<ChildProcess> {
❯ setup tests/webhook-live.test.ts:60:3
❯ tests/webhook-live.test.ts:149:25

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[111/187]⎯

FAIL tests/webhook.test.ts > webhook ingress > routes provider envelopes to isolated inboxes and rejects spoofed or unsupported events
FAIL tests/webhook.test.ts > webhook ingress > accepts JSON through atomic files without creating a daemon
FAIL tests/webhook.test.ts > webhook ingress > rejects malformed, oversized, traversal, and non-POST requests
FAIL tests/webhook.test.ts > webhook ingress > fails closed on a symlink inbox target
Error: listen EPERM: operation not permitted 127.0.0.1
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[112/187]⎯


Captured suite summary:
Test Files 25 failed | 55 passed | 1 skipped (81)
Tests 185 failed | 1109 passed | 10 skipped (1304)
Errors 14 errors
Start at 23:52:24
Duration 68.17s (transform 1.54s, setup 0ms, collect 15.17s, tests 323.65s, environment 9ms, prepare 2.95s)


Exit code: 1
10 changes: 10 additions & 0 deletions docs/evidence/spec-X/surface-regressions.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Command: npm run typecheck:regressions
Working directory: /Users/khaliqgant/fl-slice-X/packages/surface


> @relayflows/surface@2.0.8 typecheck:regressions
> tsc -p ../../regressions/tsconfig.json && tsc -p tsconfig.test.json && node scripts/check-generated-helpers.mjs

HELPERS_GENERATED_OK index.ts, slack.ts

Exit code: 0
22 changes: 22 additions & 0 deletions docs/evidence/spec-X/surface-tests.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Command: PATH=/Users/khaliqgant/.bun/bin:$PATH npm test
Working directory: /Users/khaliqgant/fl-slice-X/packages/surface

> @relayflows/surface@2.0.8 test
> bun run build && tsc -p tsconfig.test.json && vitest run

$ tsc

RUN v2.1.9 /Users/khaliqgant/fl-slice-X/packages/surface

✓ tests/triggers.test.ts (4 tests) 4ms
✓ tests/provider-triggers.test.ts (3 tests) 5ms
✓ tests/flow.test.ts (20 tests) 7ms
✓ tests/helpers.snapshot.test.ts (1 test) 661ms
✓ regenerates helpers byte-identically from the pinned adapter 660ms

Test Files 4 passed (4)
Tests 28 passed (28)
Start at 23:51:18
Duration 1.55s (transform 64ms, setup 0ms, collect 913ms, tests 677ms, environment 0ms, prepare 387ms)

Exit code: 0
9 changes: 9 additions & 0 deletions docs/evidence/spec-X/surface-typecheck.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Command: npm run typecheck
Working directory: /Users/khaliqgant/fl-slice-X/packages/surface


> @relayflows/surface@2.0.8 typecheck
> tsc --noEmit


Exit code: 0
15 changes: 13 additions & 2 deletions packages/sdk/src/cli/serve-webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createServer, type Server, type ServerResponse } from 'node:http';
import { join, resolve } from 'node:path';
import { TextDecoder } from 'node:util';
import type { CliIo } from '../cli.js';
import { providerInboxEvent } from '../trigger-executor.js';

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

/** POST /<name> accepts JSON, including scalar values. No daemon connection. */
/** POST /<name> accepts JSON; /providers/<provider> accepts typed event envelopes. */
export async function startWebhookServer(dataDir: string, port: number): Promise<Server> {
const inbox = join(resolve(dataDir), 'inbox');
await directory(inbox);
Expand All @@ -45,7 +46,8 @@ export async function startWebhookServer(dataDir: string, port: number): Promise
reply(response, 405, { error: 'method_not_allowed' });
return;
}
const name = request.url?.slice(1);
const providerRoute = request.url?.startsWith('/providers/') ?? false;
const name = request.url?.slice(providerRoute ? '/providers/'.length : 1);
if (!name || !NAME.test(name)) {
request.resume();
reply(response, 404, { error: 'invalid_webhook_name' });
Expand Down Expand Up @@ -74,6 +76,15 @@ export async function startWebhookServer(dataDir: string, port: number): Promise
reply(response, 400, { error: 'invalid_json' });
return;
}
if (providerRoute) {
try {
payload = providerInboxEvent(name, payload);
} catch (error) {
reply(response, 400, { error: 'invalid_provider_event',
message: error instanceof Error ? error.message : 'invalid provider event' });
return;
}
}
const target = join(inbox, name);
await directory(target);
const id = randomUUID();
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,4 @@ export {
export { createFlow, type CreateFlowOptions, type CreatedFlow } from './create-flow.js';

export { renderProgress, type ProgressEvent } from './progress.js';
export { webhookTriggerSpec } from './trigger-executor.js';
34 changes: 34 additions & 0 deletions packages/sdk/src/trigger-executor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { providerEventTypes, webhook, type TriggerSource, type WebhookFilter } from '@relayflows/surface';
import type { TriggerSpec } from './spec.js';

/** Lower a surface subscription to the existing inbox executor contract. */
export function webhookTriggerSpec(id: string, source: TriggerSource): TriggerSpec {
if (source.kind !== 'webhook') throw new TypeError('unsupported trigger kind');
const trigger = webhook(source.name, source.filter);
return {
id,
executor: trigger.name,
eventType: trigger.name,
...(trigger.filter === undefined ? {} : { pattern: trigger.filter }),
// The inbox watcher supplies the durable file ID as the event key.
dedupeKeyTemplate: '{{event.type}}',
};
}

/** Provider ingress uses the path as authority; it never trusts a body to reroute. */
export function providerInboxEvent(provider: string, value: unknown): WebhookFilter {
if (!Object.hasOwn(providerEventTypes, provider)) throw new TypeError(`unknown provider: ${provider}`);
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
throw new TypeError('provider event must be an object');
}
const event = value as Record<string, unknown>;
const types: readonly string[] = providerEventTypes[provider as keyof typeof providerEventTypes];
if (typeof event.type !== 'string' || !types.includes(event.type)) {
throw new TypeError(`unknown event type for ${provider}`);
}
if (event.provider !== undefined && event.provider !== provider) throw new TypeError('provider does not match inbox');
if (event.payload === null || typeof event.payload !== 'object' || Array.isArray(event.payload)) {
throw new TypeError('provider event payload must be an object');
}
return webhook(provider, { ...event, provider } as WebhookFilter).filter!;
}
62 changes: 62 additions & 0 deletions packages/sdk/tests/generate-triggers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { execFileSync } from 'node:child_process';
import { mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { afterEach, expect, it } from 'vitest';

const generator = fileURLToPath(new URL('../../../scripts/generate-triggers.mjs', import.meta.url));
const dirs: string[] = [];
afterEach(() => { for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true }); });
function temporary(): string {
const dir = mkdtempSync(join(tmpdir(), 'provider-codegen-'));
dirs.push(dir);
return dir;
}
function generate(...args: string[]): string {
return execFileSync(process.execPath, [generator, ...args], { encoding: 'utf8', stdio: 'pipe' });
}
function mapping(root: string, path: string, value: unknown): void {
const target = join(root, 'packages', path);
mkdirSync(target, { recursive: true });
writeFileSync(join(target, 'test.mapping.yaml'), JSON.stringify(value));
}

it('reproduces all checked-in modules from the pinned adapter mappings', () => {
expect(generate('--check')).toContain('Checked 2 provider trigger modules');
});

it('discovers new adapters, preserves exact event names, and prefers adapter-local mappings', () => {
const root = temporary();
const out = join(root, 'generated');
mapping(root, 'core/mappings', { adapter: { name: 'github' }, webhooks: { stale: {} } });
mapping(root, 'github', { adapter: { name: 'github' }, webhooks: { pull_request: { extract: ['action'] } } });
mapping(root, 'new-provider', { provider: 'new-provider', webhooks: { 'file.created': {}, 'file.deleted': {} } });
mapping(root, 'no-events', { provider: 'no-events', webhooks: {} });
generate('--adapters-dir', root, '--out-dir', out);
expect(readdirSync(out).sort()).toEqual(['github.ts', 'index.ts', 'new-provider.ts']);
expect(readFileSync(join(out, 'github.ts'), 'utf8')).toContain('pull_request(action?: string)');
expect(readFileSync(join(out, 'github.ts'), 'utf8')).not.toContain('stale');
const provider = readFileSync(join(out, 'new-provider.ts'), 'utf8');
expect(provider).toContain('export const new_provider');
expect(provider).toContain('file_created(filter?: WebhookFilter)');
expect(provider).toContain('providerTrigger("new-provider", "file.created", filter)');
generate('--adapters-dir', root, '--out-dir', out, '--check');
mapping(root, 'no-events', { provider: 'no-events', webhooks: { added: {} } });
generate('--adapters-dir', root, '--out-dir', out);
expect(readdirSync(out)).toContain('no-events.ts');
mapping(root, 'no-events', { provider: 'no-events', webhooks: {} });
generate('--adapters-dir', root, '--out-dir', out);
expect(readdirSync(out)).not.toContain('no-events.ts');
writeFileSync(join(out, 'new-provider.ts'), 'stale');
expect(() => generate('--adapters-dir', root, '--out-dir', out, '--check')).toThrow(/drifted/);
});

it('fails closed on malformed mappings and colliding method names before writing output', () => {
for (const webhooks of [[], { broken: null }, { 'file.created': {}, file_created: {} }]) {
const root = temporary();
mapping(root, 'example', { provider: 'example', webhooks });
expect(() => generate('--adapters-dir', root, '--out-dir', join(root, 'out'))).toThrow();
expect(readdirSync(root)).toEqual(['packages']);
}
});
Loading
Loading