Skip to content
Closed
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
node_modules/
target/
dist/
!examples/babysitter/dist/
*.log
.DS_Store
.agent-relay/
Expand Down
3 changes: 2 additions & 1 deletion examples/babysitter/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
dist/
dist/*
!dist/babysitter-recommended.flow.ts
node_modules/
30 changes: 30 additions & 0 deletions examples/babysitter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,36 @@ two lists to each other. Three of those actions — `pull_request.ready_for_revi
deliberately declares the full contract rather than the routable subset, so
that an install grants exactly the events the flow registers.

## Recommended Flow artifact

`recommended.ts` is the standalone entry point for a second Cloud Recommended
Flow alongside Software Garden. It consumes Cloud's normalized
`{ approver, pullRequest, event }` input and derives the repository and pull
request coordinates from that trusted activation envelope rather than pinning
one PR at build time. `build-recommended.mjs` bundles the authored modules into
the committed one-file catalog artifact at
`dist/babysitter-recommended.flow.ts`; its check mode fails when that artifact
is stale.

The catalog trigger for this artifact is repository-scoped pull-request
routing with the opt-in label:

```json
{"provider":"github","settings":{"events":"pull_request","labels":"babysit"}}
```

Cloud's activation `label` remains only the display name for the grouped
activation. The trigger setting above is the GitHub label contract: adding
`babysit` admits a PR after Cloud rereads it, while removing the label causes a
later delivery to be filtered before launch. Repository selection remains an
activation concern, so one activation may cover selected approved repositories
or all approved repositories.

The recommended artifact is deliberately fail-closed. Automatic merge is off,
its validation command is `false` until Cloud collects explicit repository test
policy, and the existing write/publication capability gates remain false. It
does not claim automatic fixes, review publication, or merge.

## Operator input

Pin configuration outside the PR and webhook, for example:
Expand Down
33 changes: 33 additions & 0 deletions examples/babysitter/build-recommended.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Produce the one-file artifact consumed by the public Recommended Flow catalog.
// Cloud fetches exactly one immutable source blob, so authored modules are
// bundled while the runtime-owned Surface package remains external.
import { build } from '../../packages/sdk/node_modules/esbuild/lib/main.js';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';

const check = process.argv.slice(2).includes('--check');
const root = fileURLToPath(new URL('.', import.meta.url));
const outfile = `${root}dist/babysitter-recommended.flow.ts`;
const result = await build({
entryPoints: [`${root}recommended.ts`],
bundle: true,
platform: 'node',
format: 'esm',
target: 'node22',
external: ['@relayflows/surface'],
minify: false,
write: false,
});
const generated = new TextDecoder().decode(result.outputFiles[0].contents);

if (check) {
const committed = await readFile(outfile, 'utf8').catch(() => '');
if (committed !== generated) {
throw new Error('dist/babysitter-recommended.flow.ts is stale; run npm run build:recommended');
}
console.log('Checked dist/babysitter-recommended.flow.ts');
} else {
await mkdir(`${root}dist`, { recursive: true });
await writeFile(outfile, generated);
console.log('Built examples/babysitter/dist/babysitter-recommended.flow.ts');
}
438 changes: 438 additions & 0 deletions examples/babysitter/dist/babysitter-recommended.flow.ts

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion examples/babysitter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"scripts": {
"test": "node --experimental-strip-types --test tests/*.test.ts",
"typecheck": "tsc -p tsconfig.json",
"build": "node build.mjs"
"build": "node build.mjs",
"build:recommended": "node build-recommended.mjs",
"check:recommended": "node build-recommended.mjs --check"
}
}
65 changes: 65 additions & 0 deletions examples/babysitter/recommended.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { flow, type Ctx } from '@relayflows/surface';
import { babysitConfigured } from './babysitter.flow.ts';
import { parseInput, record, shaValid } from './input.ts';
import { subscriptions } from './subscriptions.ts';
import type { Wake } from './wake.ts';

const BOT_LOGIN = 'agent-relay[bot]';

/**
* Input supplied by Cloud after it has matched a repository-scoped pull-request
* delivery and reread the pull request. The delivery is still only a wake hint:
* Babysitter immediately performs its own authoritative state read.
*/
export async function recommendedBabysitterBody(f: Ctx, value: unknown): Promise<void> {
const input = record(value);
const pullRequest = record(input.pullRequest);
const event = record(input.event);
const approver = typeof input.approver === 'string' ? input.approver.trim() : '';
if (!approver
|| event.provider !== 'github'
|| pullRequest.host !== undefined
|| typeof pullRequest.owner !== 'string'
|| typeof pullRequest.repo !== 'string'
|| !Number.isSafeInteger(pullRequest.number)
|| Number(pullRequest.number) <= 0) {
throw new Error('Recommended Babysitter requires a normalized GitHub pull request and approver');
}
const subscription = subscriptions.find(candidate => candidate.id === event.eventType);
if (!subscription) throw new Error('Recommended Babysitter received an undeclared subscription');
if (typeof event.deliveryId !== 'string' || !/^[A-Za-z0-9_.:-]{1,200}$/.test(event.deliveryId)) {
throw new Error('Recommended Babysitter requires a valid delivery id');
}

// Policy is authored here, not accepted from PR content or the delivery.
// Automatic merge remains off. Review/fix publication also remains held by
// the capability gates in babysitConfigured until the platform proves them.
const configured = parseInput({
owner: pullRequest.owner,
repo: pullRequest.repo,
number: pullRequest.number,
// The recommended activation does not yet collect repository validation
// policy. Keep validation closed even if the write-scope capability is
// later unlocked; a no-op command must never stand in for project tests.
testCommand: 'false',
botLogin: BOT_LOGIN,
approvers: [approver],
organizations: [],
merge: false,
reviewAuthors: [],
skipLabels: ['no-agent-relay-review'],
requiredChecks: [],
});
const wake: Wake = {
id: subscription.id,
family: subscription.family,
action: subscription.action,
...(shaValid(pullRequest.headSha) ? { hintedSha: pullRequest.headSha } : {}),
};
await babysitConfigured(f, configured, wake, event.deliveryId);
}

export default flow<unknown>('babysitter', {
version: '1.0.0',
budget: { dollars: 8, wallclock: '45m' },
}, recommendedBabysitterBody);
97 changes: 97 additions & 0 deletions examples/babysitter/tests/recommended.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { readFile } from 'node:fs/promises';
import { getFlowDefinition } from '@relayflows/surface/runtime';
import type { Ctx } from '@relayflows/surface';
import recommended, { recommendedBabysitterBody } from '../recommended.ts';
import { subscriptionIds } from '../subscriptions.ts';

const liveHead = 'b'.repeat(40);
const live = {
state: 'open', merged: false, draft: false, headSha: liveHead,
baseSha: 'c'.repeat(40), headRepo: 'acme/widgets', headRef: 'work',
author: 'alice', labels: ['babysit'], mergeable: true, mergeState: 'clean',
checks: [], reviews: [], requestedReviewers: [],
};

function input(eventType = 'pull_request.labeled') {
return {
approver: 'alice',
issue: { source: 'github', repository: 'acme/widgets', title: 'A PR', labels: ['babysit'] },
pullRequest: { owner: 'acme', repo: 'widgets', number: 7, headSha: 'a'.repeat(40) },
event: { provider: 'github', eventType, paths: [], deliveryId: 'delivery-7' },
};
}

function context() {
const commands: string[] = [];
const reasons: string[] = [];
return {
commands,
reasons,
f: {
run: async (command: string) => {
commands.push(command);
return command.startsWith('node -e') ? JSON.stringify(live) : '';
},
done: (reason: string) => { reasons.push(reason); },
agent: () => { throw new Error('Unsafe review capability must remain held'); },
} as unknown as Ctx,
};
}

test('recommended artifact is a versioned standalone flow body', () => {
const definition = getFlowDefinition(recommended);
assert.equal(definition.name, 'babysitter');
assert.equal(definition.header.version, '1.0.0');
assert.equal(definition.handlers.length, 0);
});

test('every Cloud change-request subscription binds dynamic repository coordinates and rereads live state', async () => {
for (const id of subscriptionIds) {
const { f, commands } = context();
await recommendedBabysitterBody(f, input(id));
assert.match(commands[0]!, /^node -e /);
assert.match(commands[0]!, /"owner":"acme","repo":"widgets","number":7/);
assert.ok(commands.some(command => command.includes(`wake ${id} hint=stale-hint bind=${liveHead}`)), id);
}
});

test('delivery fields cannot enable merge, redirect coordinates, or bypass held capabilities', async () => {
const { f, commands, reasons } = context();
await recommendedBabysitterBody(f, {
...input(),
owner: 'evil', repo: 'other', number: 99, merge: true,
testCommand: 'touch /untrusted', botLogin: 'alice', organizations: ['acme'],
});
assert.match(commands[0]!, /"owner":"acme","repo":"widgets","number":7/);
assert.ok(commands.every(command => !command.includes('/untrusted')));
assert.ok(commands.every(command => !command.includes('touch')));
assert.deepEqual(reasons, ['needs_human']);
assert.ok(commands.some(command => command.includes('enforce agent workspace and credential scopes')));
});

test('malformed or undeclared Cloud deliveries fail before the first live read', async () => {
for (const value of [
{ ...input(), approver: '' },
{ ...input(), pullRequest: { ...input().pullRequest, host: 'gitlab' } },
{ ...input(), pullRequest: { ...input().pullRequest, number: 0 } },
{ ...input(), event: { ...input().event, provider: 'gitlab' } },
{ ...input(), event: { ...input().event, eventType: 'pull_request.edited' } },
{ ...input(), event: { ...input().event, deliveryId: '' } },
]) {
const { f, commands } = context();
await assert.rejects(recommendedBabysitterBody(f, value));
assert.equal(commands.length, 0);
}
});

test('committed catalog artifact is self-contained and preserves fail-closed capability text', async () => {
const artifact = await readFile(new URL('../dist/babysitter-recommended.flow.ts', import.meta.url), 'utf8');
assert.match(artifact, /from \"@relayflows\/surface\"/);
assert.doesNotMatch(artifact, /from \"\.\//);
assert.match(artifact, /enforce agent workspace and credential scopes/);
assert.match(artifact, /testCommand: \"false\"/);
assert.match(artifact, /merge: false/);
assert.match(artifact, /version: \"1\.0\.0\"/);
});
Loading