-
Notifications
You must be signed in to change notification settings - Fork 0
feat(surface,sdk): lower postfix .gate(config) to slice-P named gates #372
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| import type { AuthoredBudget } from './authored-budget.js'; | ||
| import { parseBudget } from './budget.js'; | ||
| import type { AgentOptions, AgentResult, LlmOptions } from '@relayflows/surface'; | ||
| import type { AgentOptions, AgentResult, LlmOptions, NamedGate } from '@relayflows/surface'; | ||
| import { compileSpec, toKernelSpec } from './compile.js'; | ||
| import { checkAuthoredFlow } from './cli/check.js'; | ||
| import { classifyOutcome, type RunLifecycleOptions } from './cli/run.js'; | ||
|
|
@@ -83,7 +83,7 @@ export function authoredWorkerRunner( | |
| } | ||
|
|
||
| return { | ||
| async agent(id: string, options: AgentOptions): Promise<AgentResult> { | ||
| async agent(id: string, options: AgentOptions, verification?: NamedGate): Promise<AgentResult> { | ||
| if (options.workspace !== undefined && localAgentStream !== undefined) { | ||
| throw new AuthoredFlowExecutionError('unsupported_workspace_permission', | ||
| 'The local agent worker accepts stream-only steps. Remove workspace or attach a worker that holds its revision pins.'); | ||
|
|
@@ -125,14 +125,15 @@ export function authoredWorkerRunner( | |
| ...(options.cli === undefined ? {} : { cli: options.cli }), | ||
| ...(options.model === undefined ? {} : { model: options.model }), | ||
| ...(options.cwd === undefined ? {} : { cwd: options.cwd }), | ||
| ...(verification === undefined ? {} : { verification }), | ||
| }); | ||
| if (typeof output !== 'object' || output === null || Array.isArray(output)) { | ||
| throw new AuthoredFlowExecutionError('journal_protocol_violation', `step "${id}" produced a non-object output`); | ||
| } | ||
| const stdout = 'stdout_tail' in output ? output.stdout_tail : undefined; | ||
| return { summary: typeof stdout === 'string' ? stdout : JSON.stringify(output), artifacts: [] }; | ||
| }, | ||
| async llm(id: string, prompt: string, options?: LlmOptions): Promise<unknown> { | ||
| async llm(id: string, prompt: string, options?: LlmOptions, verification?: NamedGate): Promise<unknown> { | ||
| if (options !== undefined && (typeof options !== 'object' || options === null || options.output === undefined)) { | ||
| throw new AuthoredFlowExecutionError('llm_cli_unresolved', 'f.llm(prompt, options) requires an output JSON Schema.'); | ||
| } | ||
|
|
@@ -141,7 +142,10 @@ export function authoredWorkerRunner( | |
| || Object.keys(snapshot).some(key => !['output', 'cli', 'model'].includes(key)))) { | ||
| throw new AuthoredFlowExecutionError('llm_cli_unresolved', 'f.llm options accepts only output, cli, and model.'); | ||
| } | ||
| const output = await run({ id, type: 'llm', prompt, ...snapshot as unknown as LlmOptions }); | ||
| const output = await run({ | ||
| id, type: 'llm', prompt, ...snapshot as unknown as LlmOptions, | ||
| ...(verification === undefined ? {} : { verification }), | ||
| }); | ||
| if (options === undefined && typeof output !== 'string') { | ||
| throw new AuthoredFlowExecutionError('journal_protocol_violation', `text step "${id}" produced a non-string output`); | ||
| } | ||
|
|
@@ -154,11 +158,15 @@ export function authoredWorkerRunner( | |
| export function authoredDeterministicRunner( | ||
| name: string, journal: JournalClient, journalSteps: AuthoredFlowJournalStep[], budget: AuthoredBudget, | ||
| ) { | ||
| return async (id: string, command: string, terminal = false, leaseMs?: number): Promise<string> => { | ||
| return async (id: string, command: string, terminal = false, leaseMs?: number, verification?: NamedGate): Promise<string> => { | ||
| const spec = toKernelSpec(compileSpec({ | ||
| version: SPEC_SCHEMA_VERSION, | ||
| name: `${name}/${id}`, | ||
| steps: [{ id, type: 'deterministic', command, ...(leaseMs === undefined ? {} : { lease_ms: leaseMs }) }], | ||
| steps: [{ | ||
| id, type: 'deterministic', command, | ||
| ...(leaseMs === undefined ? {} : { lease_ms: leaseMs }), | ||
| ...(verification === undefined ? {} : { verification }), | ||
| }], | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Run gates ignore verification failureHigh Severity
Additional Locations (1)Reviewed by Cursor Bugbot for commit 0bcd7f5. Configure here. |
||
| })); | ||
| if (terminal) return readSuccessfulOutput(journal, await journal.runStart(spec), id, journalSteps); | ||
| return budget.execute(journal, spec, outcome => readSuccessfulOutput(journal, outcome, id, journalSteps)); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,56 @@ | ||
| /** A journal-backed step result with its postfix verification gate. */ | ||
| export interface Step<T> extends PromiseLike<T> { | ||
| /** Fail the step with `verification_failed` when the predicate is false. */ | ||
| /** | ||
| * Attach a named data gate to this step's `verification:` field. The gate is | ||
| * lowered by the SDK into a slice-P journal-honest check that survives | ||
| * replay — its shape is data, not code, so covenant 1 (journal-as-truth) | ||
| * holds. See `packages/sdk/src/named-gates.ts` for the enforcement. | ||
| */ | ||
| gate(config: NamedGate): Step<T>; | ||
| /** | ||
| * Predicate gates cannot be journaled — the JavaScript closure would not | ||
| * survive replay — so the executor refuses this branch with | ||
| * `unsupported_gate`. Use a `NamedGate` config-object gate above, or the | ||
| * declarative `verification:` field on the compiled step spec, when you | ||
| * need journal-honest verification. | ||
| */ | ||
| gate(predicate: (value: T) => boolean, because?: string): Step<T>; | ||
| } | ||
|
|
||
| /** | ||
| * Slice-P named data gates the authored surface can attach postfix via | ||
| * `.gate(config)`. The SDK's named-gate lowering enforces these exactly (see | ||
| * `packages/sdk/src/named-gates.ts`); the shapes here are the surface-visible | ||
| * subset kept in sync with `NamedDataGate` in the SDK spec. | ||
| */ | ||
| export type NamedGate = | ||
| | ReferencesInputNamedGate | ||
| | SubprocessNamedGate | ||
| | WordCountBoundsNamedGate | ||
| | RegexMatchNamedGate; | ||
|
|
||
| export interface ReferencesInputNamedGate { | ||
| type: 'references_input'; | ||
| input_key: string; | ||
| in_output_at?: Array<string | number>; | ||
| } | ||
|
|
||
| export interface SubprocessNamedGate { | ||
| type: 'subprocess_gate'; | ||
| command: string; | ||
| from_output?: Array<string | number>; | ||
| } | ||
|
|
||
| export interface WordCountBoundsNamedGate { | ||
| type: 'word_count_bounds'; | ||
| min?: number; | ||
| max?: number; | ||
| } | ||
|
|
||
| export interface RegexMatchNamedGate { | ||
| type: 'regex_match'; | ||
| pattern: string; | ||
| /** Only i, m, and s; evaluated by a non-backtracking RE2 engine. */ | ||
| flags?: string; | ||
| in_output_at?: Array<string | number>; | ||
| } |


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Config gates skip helper steps
Medium Severity
.gate(config)now accepts a named gate on everyStep, but onlyf.run,f.llm, andf.agentreadnamedGate. Slack, helper, MCP, and plugin starts ignore it, so the call succeeds and the step runs with no verification.Additional Locations (2)
packages/sdk/src/authored-flow-executor.ts#L212-L222packages/sdk/src/authored-flow-executor.ts#L225-L252Reviewed by Cursor Bugbot for commit 0bcd7f5. Configure here.