Skip to content
Open
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
2 changes: 1 addition & 1 deletion scripts/pack-release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ try {
: name === 'relayflows'
? ['bin/flows.js']
: ['dist/index.js', 'dist/index.d.ts'];
if (name === 'surface') required.push('dist/runtime.js', 'dist/runtime.d.ts');
if (name === 'surface') required.push('dist/runtime.js', 'dist/runtime.d.ts', 'dist/signals.js', 'dist/signals.d.ts');
Comment thread
miyaontherelay marked this conversation as resolved.
Comment thread
miyaontherelay marked this conversation as resolved.
if (name === 'sdk') required.push('dist/cli.js');
for (const file of required) {
assert(packed.files.some((entry) => entry.path === file), `missing package/${file}`);
Expand Down
7 changes: 7 additions & 0 deletions scripts/publish.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ test('actual npm tarballs reject missing dist and local dependencies, then accep
for (const file of ['index.js', 'index.d.ts', 'runtime.js', 'runtime.d.ts']) {
writeFileSync(join(dist, file), 'export {};\n');
}
// The published Surface must also ship the hosted signal helper (#677).
const noSignals = run();
assert.notEqual(noSignals.status, 0);
assert.match(noSignals.stderr, /missing package\/dist\/signals\.js/);
for (const file of ['signals.js', 'signals.d.ts']) {
writeFileSync(join(dist, file), 'export {};\n');
}
const path = join(root, 'packages/surface/package.json');
const pkg = read(path);
pkg.dependencies = { external: 'file:../external' };
Expand Down
46 changes: 46 additions & 0 deletions scripts/surface-package-gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,42 @@ try {
console.log(`PACKED_RUNTIME_REFUSAL_OK invalidHeaders=${invalidHeaders.length} forgedHandle=refused`);
NODE

# Hosted flows get only this package, so the signal helper must resolve from
# the tarball alone and import nothing but Node built-ins.
RELAYFLOW_V2_CLOUD_RUN_ID=packed-run RELAYFLOW_V2_SIGNAL_READ_TOKEN=packed-read RELAYFLOW_RUN_ID=engine-run \
node --input-type=module - <<'NODE'
Comment on lines +112 to +113

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Local settings fail the gate

If the shell has FLOWS_CLOUD_URL set to another endpoint, waitForSignal uses it, but this smoke test always expects the production URL. That makes a correct package fail the gate.

Unset FLOWS_CLOUD_URL for this Node process so the test actually checks the fallback.

Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/surface-package-gate.sh
Line: 112-113

Comment:
**Local settings fail the gate**

If the shell has `FLOWS_CLOUD_URL` set to another endpoint, `waitForSignal` uses it, but this smoke test always expects the production URL. That makes a correct package fail the gate.

Unset `FLOWS_CLOUD_URL` for this Node process so the test actually checks the fallback.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex Fix in Claude Code

import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { waitForSignal, SignalError } from '@relayflows/surface/signals';

const require = createRequire(import.meta.url);
const entry = require.resolve('@relayflows/surface/signals');
// Statement-level only: the module's doc comment shows an example import.
const specifiers = [...readFileSync(entry, 'utf8')
.matchAll(/^\s*(?:import|export)\b[^;]*?\bfrom\s+["']([^"']+)["']|\bimport\s*\(\s*["']([^"']+)["']/gm)]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Bare imports escape the gate

The regex skips imports such as import "ai-hist"; because it requires either from or import(...). The existing node:timers/promises import keeps specifiers nonempty, so adding that dependency would still pass. The packed consumer installs dependencies, so loading the module would not catch it either.

Include side-effect imports in the check and add a test that rejects one.

Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/surface-package-gate.sh
Line: 122

Comment:
**Bare imports escape the gate**

The regex skips imports such as `import "ai-hist";` because it requires either `from` or `import(...)`. The existing `node:timers/promises` import keeps `specifiers` nonempty, so adding that dependency would still pass. The packed consumer installs dependencies, so loading the module would not catch it either.

Include side-effect imports in the check and add a test that rejects one.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex Fix in Claude Code

.map((match) => match[1] ?? match[2]);
if (specifiers.length === 0 || specifiers.some((specifier) => !specifier.startsWith('node:'))) {
throw new Error(`packed signals helper imports beyond Node built-ins: ${specifiers.join(', ')}`);
}
let requested;
globalThis.fetch = async (url, init) => {
requested = { url: String(url), auth: new Headers(init.headers).get('authorization') };
return Response.json({ payload: { done: true }, acceptedAt: 1 });
};
const payload = await waitForSignal('native_done');
if (requested.url !== 'https://agentrelay.com/cloud/api/v1/workflows/runs/packed-run/signals/native_done?waitMs=25000'
|| requested.auth !== 'Bearer packed-read' || payload?.done !== true) {
throw new Error(`packed signals helper used the wrong hosted defaults: ${JSON.stringify(requested)}`);
}
try {
await waitForSignal('bad/name');
throw new Error('packed signals helper accepted an invalid name');
} catch (error) {
if (!(error instanceof SignalError) || error.code !== 'invalid_input') throw error;
}
console.log(`PACKED_SIGNALS_OK imports=${specifiers.join(',')}`);
NODE

cat > consume.mts <<'TS'
import {
flow,
Expand All @@ -126,6 +162,11 @@ import {
getFlowDefinition,
type AuthoredFlowDefinition,
} from '@relayflows/surface/runtime';
import {
waitForSignal,
type SignalError,
type WaitForSignalOptions,
} from '@relayflows/surface/signals';

const body = async (f: Ctx): Promise<void> => {
const ran: Step<string> = f.run('true');
Expand All @@ -137,6 +178,9 @@ const body = async (f: Ctx): Promise<void> => {
const post: Step<SlackReceipt> = slack.post('#test', 'hi');
await post;
await f.slack.post('#test', 'typed');
const signalOptions: WaitForSignalOptions = { waitMs: 25_000 };
const signalled: unknown = await waitForSignal('native_done', signalOptions);
void signalled;
f.done('success');
};
const header: FlowHeader = { identity: 'packed-type-consumer' };
Expand All @@ -150,6 +194,8 @@ const refuseStepReason = (f: Ctx, reason: CompletionReason): void => {
f.done(reason);
};
const helper: CloudHelper | undefined = undefined;
const signalFailure: SignalError['code'] = 'transient_error';
void signalFailure;
void definition;
void helper;
void stepReason;
Expand Down
Loading