Skip to content
Draft
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
53 changes: 34 additions & 19 deletions docs/BABYSITTER-CATALOG-HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,20 @@ The reviewed native artifact shipped in Flows 2.0.26. The existing
`examples/babysitter` declares Claude, GitHub comment writes, and a merge-gate
hook; it is not the native existing-session package. Ordinary authored dispatch
still refuses matched extension handlers with `plugin_unsupported`, because it
imports tenant JavaScript in the host. The external exact-target Relay/Flows
runtime must instead call `runHostedSoftwareGardenBabysitter`, which captures
the reviewed Software Garden base and complete lock-backed installation as one
generation before it runs the exact matched native handler in the capability
sandbox. Keep enabled activation blocked with zero writes until that runtime
calls the entrypoint.
imports tenant JavaScript in the host. An owning exact-target Relay/Flows action
must instead use the normal embedded `runCli(["run", flowPath, "--input", ...])`
surface and inject `RunCliOptions.hostedSoftwareGardenBabysitter`. That
non-serializable option carries the host-verified dispatch and the single queue
capability; no CLI flag or flow input can mint either. The canonical authored
run preflights the reviewed Software Garden base and complete lock-backed
installation as one generation before trigger inspection, tenant import, or
daemon attachment. It then admits one journaled effect step under a delivery-
and-pin-bound key and runs the exact matched native handler in the capability
sandbox. The queue write uses the journal's record/perform/confirm protocol;
the returned run ID, terminal reason, and completed-step count come from that
journal rather than an in-memory synthetic result. Every other command or path
surface refuses the hosted authority. Keep enabled activation blocked with zero
writes until that action is released and deployed.

The native handler must consume host-verified delivery authority, normalize the
repository/PR event, and call the Cloud lineage path. Cloud must recheck the
Expand All @@ -31,8 +39,9 @@ through the generic executor: #549 still refuses it. The SDK has a separate Linu
capability sandbox that injects exactly
`capabilities.cloud.babysitterTurn.queue` without exposing the base context,
workspace, environment credentials, network, helpers, MCP, or harnesses. It is
reached by the canonical composition entrypoint, but no deployed runtime calls
that entrypoint and this must not be treated as enablement. The package's
reached by the canonical authored `run` surface when the owning hosted action
injects verified authority, but no deployed runtime consumes this contract and
this must not be treated as enablement. The package's
`compat` requires the published 2.0.26 Surface/SDK release that routes
`labeled`, `unlabeled`, and `ready_for_review`. Export it only from the reviewed
release commit pinned below. The Software Factory flow's own independently
Expand All @@ -59,11 +68,12 @@ normalized input against non-serializable verified dispatch authority, and
permits one queue call. The parent capability adapter
receives that original authority plus immutable extension provenance; the
capability request never carries workspace, activation, listener, session,
lineage, label, head, prompt, merge, route, or config authority. Cloud PR #4002
at `25412782bf148ff8dd8018bdbafd719d4e8347fa` deliberately supplies no execution
lineage, label, head, prompt, merge, route, or config authority. Merged Cloud
PR #4002 at merge commit `ced414ab40424c7bbd4cd780ad01751d7fc85685`
(reviewed head `6201470228b23c225290d0eee356eb1c0006e31d`) deliberately supplies no execution
authority: it emits the exact-target `relay:hosted-flow-extension:v1` action for
an external Relay/Flows runtime. That runtime owns this entrypoint; only its
validated capability call reaches `relay:native-existing-session:v1`
an external Relay/Flows runtime. That runtime owns the embedded hosted-run
option; only its validated capability call reaches `relay:native-existing-session:v1`
downstream. Merged Cloud PR #3942 owns that downstream lineage/authority core
and must inject workspace, activation, and listener from persisted dispatch
context, re-read live PR/label/head state, and return only `{ receiptId, status:
Expand All @@ -80,14 +90,19 @@ the branded dispatch, and waits for the adapter's authoritative outcome before
settling any premature child terminal frame. An authoritative adapter rejection
settles immediately with its original typed error even if the child hangs.

Before replacing #549's refusal, the hosted caller must obtain an opaque base
and installation as one generation with `loadHostedExtensionRuntime`, then call
`runHostedCapabilityExtension` with both values. Every dispatch rechecks the
current extension declarations and complete project source tree against that
generation. Directory entries are streamed beneath a shared entry bound;
The normal embedded run deliberately does not replace #549's standalone
refusal. Its hosted option calls the canonical composition boundary, which
obtains an opaque base and installation as one generation with
`loadHostedExtensionRuntime`, then calls `runHostedCapabilityExtension` with
both values from its journal-attached worker. Unsupported local-worker flags
are refused instead of ignored. Every dispatch rechecks the current extension
declarations and complete project source tree against that generation.
Directory entries are streamed beneath a shared entry bound;
nonblocking no-follow descriptors and explicitly bounded reads enforce the
cumulative-byte limit before source contents are buffered. The loader never imports
tenant base code to derive authority. It
cumulative-byte limit before source contents are buffered. Only runtime/control
directories (`.flows`, `.git`, `.relayflowd`, and `node_modules`) are excluded,
so starting the standard journal daemon cannot invalidate the generation it is
executing. The loader never imports tenant base code to derive authority. It
requires the exact reviewed Software Factory flow-file SHA-256 and assigns its
pinned name/version in the parent; project `node_modules`, relative imports,
stdout, process termination, globals, and module caches therefore cannot forge
Expand Down
48 changes: 45 additions & 3 deletions packages/sdk/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ import {
import { answerFlow } from './cli/answer.js';
import { checkAuthoredTriggers } from './cli/check-triggers.js';
import { parseWebhookArgs, runServeWebhook } from './cli/serve-webhook.js';
import { runDirectFlow } from './cli/direct-run.js';
import {
runDirectFlow,
type HostedSoftwareGardenRunOptions,
} from './cli/direct-run.js';
import { parseReplayArgs, replayJournal, type ReplayArgs } from './cli/replay.js';
import { parseStatusArgs, runStatus, type StatusArgs } from './cli/status.js';
import {
Expand Down Expand Up @@ -182,6 +185,13 @@ export interface RunCliOptions {
* SIGINT/SIGTERM are handled here, for the duration of that verb only.
*/
signal?: AbortSignal;

/**
* Verified delivery authority and the only capability exposed to the
* canonical hosted Software Garden + Babysitter run. The standalone binary
* never constructs this option; an owning hosted action must inject it.
*/
hostedSoftwareGardenBabysitter?: HostedSoftwareGardenRunOptions;
}

/**
Expand Down Expand Up @@ -213,11 +223,13 @@ export async function runCli(
io: CliIo = PROCESS_IO,
options: RunCliOptions = {},
): Promise<CliExitCode> {
if (args.length === 1 && (args[0] === '--version' || args[0] === '-V')) {
if (options.hostedSoftwareGardenBabysitter === undefined
&& args.length === 1 && (args[0] === '--version' || args[0] === '-V')) {
io.stdout(options.version ?? packageVersion());
return 0;
}
if (args.length === 1 && (args[0] === '--help' || args[0] === '-h')) {
if (options.hostedSoftwareGardenBabysitter === undefined
&& args.length === 1 && (args[0] === '--help' || args[0] === '-h')) {
io.stdout(USAGE);
return 0;
}
Expand All @@ -229,6 +241,33 @@ export async function runCli(
return 2;
}

if (options.hostedSoftwareGardenBabysitter !== undefined
&& (parsed.command !== 'run' || !isAuthoredFlowPath(parsed.value))) {
const report = inputFailureReport({
kind: 'invalid_invocation',
message: 'Hosted Software Garden authority is accepted only by an authored flow run.',
}, 'value' in parsed && typeof parsed.value === 'string' ? parsed.value : undefined);
emitCheckReport(report, 'json' in parsed && parsed.json === true, io);
return 2;
}
if (options.hostedSoftwareGardenBabysitter !== undefined && parsed.command === 'run') {
const ignoredFlag = parsed.localAgent
? '--local-agent'
: parsed.agentCapacity !== undefined
? '--agent-capacity'
: parsed.allowHumanInfluenced
? '--allow-human-influenced'
: undefined;
if (ignoredFlag !== undefined) {
const report = inputFailureReport({
kind: 'invalid_invocation',
message: `${ignoredFlag} is not supported by a hosted Software Garden run.`,
}, parsed.value);
emitCheckReport(report, parsed.json, io);
return 2;
}
}

if (parsed.command === 'add') return addPlugin(parsed.value, io);
if (parsed.command === 'plugin') return runPluginCommand(parsed, io);

Expand Down Expand Up @@ -389,6 +428,9 @@ export async function runCli(
elapsedMs: now - startedSteps.get(progress.stepId)! });
},
daemon: { spawn: parsed.spawn && spawnAllowedByEnv() },
...(options.hostedSoftwareGardenBabysitter === undefined ? {} : {
hostedSoftwareGardenBabysitter: options.hostedSoftwareGardenBabysitter,
}),
};
const execution = parsed.command === 'run'
? isAuthoredFlowPath(parsed.value)
Expand Down
28 changes: 27 additions & 1 deletion packages/sdk/src/cli/direct-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import { DirectInputError, parseDirectInput } from '../direct-input.js';
import { JournalClient } from '../journal-client.js';
import { inputFailureReport } from './check.js';
import { checkAuthoredTriggers } from './check-triggers.js';
import {
runHostedSoftwareGardenFlow,
type HostedSoftwareGardenRunOptions,
} from './hosted-software-garden-run.js';
import { authoredInput, authoredWorkerRemedy, localAgentRemedy } from './local-agent-remedy.js';
import {
authoredCompletion,
Expand All @@ -31,11 +35,22 @@ import {
type RunReport,
} from './run.js';

export type { HostedSoftwareGardenRunOptions } from './hosted-software-garden-run.js';

export interface RunDirectFlowOptions extends RunLifecycleOptions {
/**
* Host-verified authority for the canonical Software Garden + Babysitter
* delivery path. This is deliberately an in-process option: neither flow
* input nor CLI flags can mint the branded dispatch or queue capability.
*/
hostedSoftwareGardenBabysitter?: HostedSoftwareGardenRunOptions;
}

export async function runDirectFlow(
path: string,
inputArgument: string | undefined,
dataDir: string,
options: RunLifecycleOptions = {},
options: RunDirectFlowOptions = {},
): Promise<RunExecution> {
let input: unknown;
try {
Expand All @@ -51,6 +66,17 @@ export async function runDirectFlow(
};
}

// A hosted Software Garden delivery is still a normal authored `run`, but
// it must branch before the ordinary trigger checker imports tenant code or
// a daemon is attached. The caller supplies only authority that was minted
// from its verified delivery and its exact queue capability; the loader
// independently resolves the reviewed base plus installed, lock-backed
// Babysitter generation and the sandbox selects the exact matched handler.
const hostedSoftwareGarden = options.hostedSoftwareGardenBabysitter;
if (hostedSoftwareGarden !== undefined) {
return runHostedSoftwareGardenFlow(path, input, dataDir, hostedSoftwareGarden, options);
}

// Declared triggers are knowable before any daemon or step is started.
// Importing the authored module is unavoidable here — trigger sources
// are only observable after `flow(...).on(webhook(...))` has run — but
Expand Down
Loading
Loading