-
Notifications
You must be signed in to change notification settings - Fork 0
feat(sdk): flows deploy — publish digest to bucket + run from digest (#333) #337
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
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| # Adep minimal slice for #333 | ||
|
|
||
| This branch implements file-bucket deploy and digest-run for self-contained | ||
| YAML/declarative deterministic flows. It does not close all of #333. | ||
|
|
||
| The new tests cover signed payload layout, idempotency, missing/unwritable | ||
| buckets, corruption before transport or journal creation, cache reuse and | ||
| corruption, bucket configuration/override, exact canonical spec submission, | ||
| and explicit refusal of asset execution. A real-kernel test deletes the local | ||
| source and build output before running the deployed digest to success. | ||
|
|
||
| `verification.txt` contains literal commands, output, and exit codes. The live | ||
| check uses an already-built local relayflowd via the recorded RELAYFLOWD_BIN; | ||
| this branch does not modify the kernel. The initial regression run exposed | ||
| local dependency drift, then Bun missing from PATH. The failed outputs and | ||
| the reruns after restoring dependencies and adding the installed Bun to PATH | ||
| are retained. | ||
|
|
||
| Follow-up: S3 transport; trigger digest pinning/conflict validation; authored TS, | ||
| assets and placement; agent/LLM environment checks; separate environment-only | ||
| preflight (this slice retains the existing checks). Deploy-time execution support | ||
| is deliberately refused for those bundle kinds. Linux artifact and packed-consumer | ||
| CI are left to the lead's PR workflow; they were not run on this node. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,186 @@ | ||
| Working directory: packages/sdk | ||
| RELAYFLOWD_BIN=/Users/khaliqgant/.relayflows-toolchain/target/1475297677/debug/relayflowd | ||
|
|
||
| $ npm run typecheck | ||
|
|
||
| > @relayflows/sdk@2.0.8 typecheck | ||
| > tsc --noEmit && tsc -p tsconfig.type-tests.json | ||
|
|
||
| exit=0 | ||
|
|
||
| $ npm run build | ||
|
|
||
| > @relayflows/sdk@2.0.8 build | ||
| > tsc && node scripts/make-cli-executable.mjs | ||
|
|
||
| exit=0 | ||
|
|
||
| $ npm run typecheck:tests | ||
|
|
||
| > @relayflows/sdk@2.0.8 typecheck:tests | ||
| > tsc -p tsconfig.tests.json | ||
|
|
||
| exit=0 | ||
|
|
||
| $ npx vitest run tests/deploy.test.ts tests/run-from-digest.test.ts tests/run-digest-live.test.ts tests/cli.test.ts tests/bundle.test.ts | ||
|
|
||
| RUN v2.1.9 /Users/khaliqgant/flows-spec-Adep-deploy/packages/sdk | ||
|
|
||
| ✓ tests/run-digest-live.test.ts (1 test) 535ms | ||
| ✓ executes a deployed digest on the real kernel after deleting the authoring tree 535ms | ||
| ✓ tests/deploy.test.ts (8 tests) 1990ms | ||
| ✓ flows deploy file buckets > publishes the full signed layout byte-for-byte and redeploys as a noop 451ms | ||
| ✓ flows deploy file buckets > never labels a corrupt existing deployment as a noop 374ms | ||
| ✓ tests/run-from-digest.test.ts (6 tests) 2192ms | ||
| ✓ flows run digest input > resolves deploy.bucket from flows.json and honors explicit override 602ms | ||
| ✓ flows run digest input > refuses an unconfigured bucket 382ms | ||
| ✓ flows run digest input > refuses tampered spec.canonical.json before creating run data 375ms | ||
| ✓ flows run digest input > refuses tampered identity.json before creating run data 361ms | ||
| ❯ tests/bundle.test.ts (21 tests | 1 failed) 2849ms | ||
| ✓ immutable bundles > builds and verifies the canonical YAML fixture through the compiled CLI 612ms | ||
| ✓ immutable bundles > emits the ephemeral warning on CLI stderr and uses the default output directory 387ms | ||
| × immutable bundles > builds a standalone TS fixture twice with identical executable hashes 185ms | ||
| → expected 'REFUSED [bundle_invalid] package-lock…' to be '' // Object.is equality | ||
| ✓ tests/cli.test.ts (63 tests) 13407ms | ||
| ✓ flows check CLI > binds a checked relative wrapper to the flow directory for worker execution 314ms | ||
| ✓ flows check CLI > refuses a typo model before probing or contacting relayflowd 933ms | ||
| ✓ flows check CLI > maps every input refusal path to its declared kind without raw exceptions 2535ms | ||
| ✓ flows run/resume CLI over the journal protocol > parses run options, submits the kernel dialect, and exits 0 on success 684ms | ||
| ✓ flows run/resume CLI over the journal protocol > exits 1 and emits the declared completionReason for a failed run 674ms | ||
| ✓ flows run/resume CLI over the journal protocol > exits 3 and names the parked llm step 759ms | ||
| ✓ flows run/resume CLI over the journal protocol > reports a needs_human agent step as parked for human recovery 713ms | ||
| ✓ flows run/resume CLI over the journal protocol > classifies a typed hello refusal as a protocol error, not an unreachable daemon 1011ms | ||
| ✓ flows run/resume CLI over the journal protocol > follows a dispatched worker step instead of reporting a protocol error 672ms | ||
| ✓ flows run/resume CLI over the journal protocol > bounds a worker wait by its lease and reports what it is waiting for 659ms | ||
| ✓ flows run/resume CLI over the journal protocol > resumes a parked run from snapshot step types without reading journal sequence one 678ms | ||
| ✓ flows run/resume CLI over the journal protocol > maps only run_not_found resumes to exit 2 1948ms | ||
|
|
||
| ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ | ||
|
|
||
| FAIL tests/bundle.test.ts > immutable bundles > builds a standalone TS fixture twice with identical executable hashes | ||
| AssertionError: expected 'REFUSED [bundle_invalid] package-lock…' to be '' // Object.is equality | ||
|
|
||
| - Expected | ||
| + Received | ||
|
|
||
| + REFUSED [bundle_invalid] package-lock.json: node_modules/@jridgewell/sourcemap-codec does not match its pinned version; run npm ci before building | ||
| + | ||
|
|
||
| ❯ tests/bundle.test.ts:210:27 | ||
| 208| it('builds a standalone TS fixture twice with identical executable h… | ||
| 209| const result = invoke(['--out', await temp(), 'packages/sdk/tests/… | ||
| 210| expect(result.stderr).toBe(''); expect(result.status).toBe(0); | ||
| | ^ | ||
| 211| const bundle = result.stdout.trim(); | ||
| 212| const second = invoke(['--out', await temp(), 'packages/sdk/tests/… | ||
|
|
||
| ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ | ||
|
|
||
| Test Files 1 failed | 4 passed (5) | ||
| Tests 1 failed | 98 passed (99) | ||
| Start at 19:08:34 | ||
| Duration 14.15s (transform 390ms, setup 0ms, collect 1.81s, tests 20.97s, environment 0ms, prepare 172ms) | ||
|
|
||
| exit=1 | ||
|
|
||
| Dependency repair: npm ci in packages/sdk, then link only node_modules/@relayflows/surface to the built packages/surface (same version). No lockfile or dependency manifest changes. | ||
|
|
||
| $ npm run typecheck | ||
|
|
||
| > @relayflows/sdk@2.0.8 typecheck | ||
| > tsc --noEmit && tsc -p tsconfig.type-tests.json | ||
|
|
||
| exit=0 | ||
|
|
||
| $ npm run build | ||
|
|
||
| > @relayflows/sdk@2.0.8 build | ||
| > tsc && node scripts/make-cli-executable.mjs | ||
|
|
||
| exit=0 | ||
|
|
||
| $ npm run typecheck:tests | ||
|
|
||
| > @relayflows/sdk@2.0.8 typecheck:tests | ||
| > tsc -p tsconfig.tests.json | ||
|
|
||
| exit=0 | ||
|
|
||
| $ npx vitest run tests/deploy.test.ts tests/run-from-digest.test.ts tests/run-digest-live.test.ts tests/cli.test.ts tests/bundle.test.ts | ||
|
|
||
| RUN v2.1.9 /Users/khaliqgant/flows-spec-Adep-deploy/packages/sdk | ||
|
|
||
| ✓ tests/run-digest-live.test.ts (1 test) 556ms | ||
| ✓ executes a deployed digest on the real kernel after deleting the authoring tree 556ms | ||
| ✓ tests/deploy.test.ts (8 tests) 2154ms | ||
| ✓ flows deploy file buckets > publishes the full signed layout byte-for-byte and redeploys as a noop 487ms | ||
| ✓ flows deploy file buckets > never labels a corrupt existing deployment as a noop 395ms | ||
| ✓ tests/run-from-digest.test.ts (6 tests) 2337ms | ||
| ✓ flows run digest input > resolves deploy.bucket from flows.json and honors explicit override 666ms | ||
| ✓ flows run digest input > refuses an unconfigured bucket 385ms | ||
| ✓ flows run digest input > refuses tampered spec.canonical.json before creating run data 382ms | ||
| ✓ flows run digest input > refuses tampered identity.json before creating run data 369ms | ||
| ❯ tests/bundle.test.ts (21 tests | 1 failed) 3075ms | ||
| ✓ immutable bundles > builds and verifies the canonical YAML fixture through the compiled CLI 682ms | ||
| ✓ immutable bundles > emits the ephemeral warning on CLI stderr and uses the default output directory 408ms | ||
| × immutable bundles > builds a standalone TS fixture twice with identical executable hashes 218ms | ||
| → expected 'REFUSED [bundle_invalid] TypeScript b…' to be '' // Object.is equality | ||
| ✓ tests/cli.test.ts (63 tests) 12718ms | ||
| ✓ flows check CLI > binds a checked relative wrapper to the flow directory for worker execution 319ms | ||
| ✓ flows check CLI > uses the raw Claude adapter model flag instead of accepting auth status as model proof 315ms | ||
| ✓ flows check CLI > refuses a typo model before probing or contacting relayflowd 1015ms | ||
| ✓ flows check CLI > maps every input refusal path to its declared kind without raw exceptions 2032ms | ||
| ✓ flows run/resume CLI over the journal protocol > parses run options, submits the kernel dialect, and exits 0 on success 616ms | ||
| ✓ flows run/resume CLI over the journal protocol > exits 1 and emits the declared completionReason for a failed run 567ms | ||
| ✓ flows run/resume CLI over the journal protocol > exits 3 and names the parked llm step 697ms | ||
| ✓ flows run/resume CLI over the journal protocol > reports a needs_human agent step as parked for human recovery 1152ms | ||
| ✓ flows run/resume CLI over the journal protocol > classifies a typed hello refusal as a protocol error, not an unreachable daemon 610ms | ||
| ✓ flows run/resume CLI over the journal protocol > follows a dispatched worker step instead of reporting a protocol error 549ms | ||
| ✓ flows run/resume CLI over the journal protocol > bounds a worker wait by its lease and reports what it is waiting for 750ms | ||
| ✓ flows run/resume CLI over the journal protocol > resumes a parked run from snapshot step types without reading journal sequence one 612ms | ||
| ✓ flows run/resume CLI over the journal protocol > maps only run_not_found resumes to exit 2 2254ms | ||
|
|
||
| ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ | ||
|
|
||
| FAIL tests/bundle.test.ts > immutable bundles > builds a standalone TS fixture twice with identical executable hashes | ||
| AssertionError: expected 'REFUSED [bundle_invalid] TypeScript b…' to be '' // Object.is equality | ||
|
|
||
| - Expected | ||
| + Received | ||
|
|
||
| + REFUSED [bundle_invalid] TypeScript build requires Bun: spawnSync bun ENOENT | ||
| + | ||
|
|
||
| ❯ tests/bundle.test.ts:210:27 | ||
| 208| it('builds a standalone TS fixture twice with identical executable h… | ||
| 209| const result = invoke(['--out', await temp(), 'packages/sdk/tests/… | ||
| 210| expect(result.stderr).toBe(''); expect(result.status).toBe(0); | ||
| | ^ | ||
| 211| const bundle = result.stdout.trim(); | ||
| 212| const second = invoke(['--out', await temp(), 'packages/sdk/tests/… | ||
|
|
||
| ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ | ||
|
|
||
| Test Files 1 failed | 4 passed (5) | ||
| Tests 1 failed | 98 passed (99) | ||
| Start at 19:09:41 | ||
| Duration 13.42s (transform 350ms, setup 0ms, collect 1.74s, tests 20.84s, environment 0ms, prepare 201ms) | ||
|
|
||
| exit=1 | ||
|
|
||
| Bun was installed but absent from PATH. | ||
| $ PATH=/Users/khaliqgant/.bun/bin:$PATH npx vitest run tests/bundle.test.ts | ||
|
|
||
| RUN v2.1.9 /Users/khaliqgant/flows-spec-Adep-deploy/packages/sdk | ||
|
|
||
| ✓ tests/bundle.test.ts (21 tests) 3717ms | ||
| ✓ immutable bundles > builds and verifies the canonical YAML fixture through the compiled CLI 542ms | ||
| ✓ immutable bundles > emits the ephemeral warning on CLI stderr and uses the default output directory 346ms | ||
| ✓ immutable bundles > builds a standalone TS fixture twice with identical executable hashes 1234ms | ||
|
|
||
| Test Files 1 passed (1) | ||
| Tests 21 passed (21) | ||
| Start at 19:10:12 | ||
| Duration 4.20s (transform 99ms, setup 0ms, collect 289ms, tests 3.72s, environment 0ms, prepare 38ms) | ||
|
|
||
| exit=0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| import { constants } from 'node:fs'; | ||
| import { access, cp, lstat, mkdir, mkdtemp, rename, rm } from 'node:fs/promises'; | ||
| import { dirname, isAbsolute, join } from 'node:path'; | ||
| import { homedir } from 'node:os'; | ||
| import { fileURLToPath } from 'node:url'; | ||
| import { verifyBundle } from './bundle.js'; | ||
|
|
||
| export type BundleFailureKind = 'bucket_unconfigured' | 'bucket_unreachable' | ||
| | 'bundle_missing_locally' | 'bundle_signature_invalid' | 'bundle_unsupported' | 'deploy_partial'; | ||
| export class BundleFailure extends Error { | ||
| constructor(readonly kind: BundleFailureKind, message: string) { super(message); } | ||
| } | ||
| export interface DigestReference { name: string; digest: string } | ||
| export function parseDigestReference(value: string): DigestReference | undefined { | ||
| // Match the safe single-component name grammar used by sealBundle. | ||
| const match = /^([A-Za-z0-9][A-Za-z0-9._-]*)@sha256:([0-9a-f]{64})$/.exec(value); | ||
| return match ? { name: match[1]!, digest: match[2]! } : undefined; | ||
| } | ||
| export function bucketDirectory(uri: string, ref: DigestReference): string { | ||
| try { | ||
| const url = new URL(uri); | ||
| if (url.protocol !== 'file:' || url.search || url.hash) throw new Error('expected file:// URI'); | ||
| const path = fileURLToPath(url); | ||
| if (!isAbsolute(path)) throw new Error('expected absolute bucket path'); | ||
| return join(path, ref.name, 'sha256', ref.digest); | ||
| } catch { | ||
| throw new BundleFailure('bucket_unreachable', 'This slice requires an absolute file:// bucket URI.'); | ||
| } | ||
| } | ||
| export async function exists(path: string): Promise<boolean> { | ||
| try { await lstat(path); return true; } | ||
| catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false; throw error; } | ||
| } | ||
| export async function verifyDigest(directory: string, digest: string): Promise<void> { | ||
| try { await verifyBundle(directory, digest); } | ||
| catch (error) { throw new BundleFailure('bundle_signature_invalid', String(error)); } | ||
| } | ||
| /** Publish only a complete, verified directory. Concurrent identical writers converge. */ | ||
| export async function copyBundle(source: string, target: string, digest: string): Promise<boolean> { | ||
| if (await exists(target)) { await verifyDigest(target, digest); return false; } | ||
| await mkdir(dirname(target), { recursive: true }); | ||
| const staging = await mkdtemp(join(dirname(target), '.bundle-')); | ||
| try { | ||
| await cp(source, staging, { recursive: true, dereference: false, verbatimSymlinks: true }); | ||
| await verifyDigest(staging, digest); | ||
| try { await rename(staging, target); } | ||
| catch (error) { | ||
| if (!['EEXIST', 'ENOTEMPTY'].includes((error as NodeJS.ErrnoException).code ?? '')) throw error; | ||
| await verifyDigest(target, digest); | ||
| return false; | ||
| } | ||
| return true; | ||
| } finally { await rm(staging, { recursive: true, force: true }); } | ||
| } | ||
| export async function writableBucket(target: string): Promise<void> { | ||
| try { | ||
| await mkdir(dirname(target), { recursive: true }); | ||
| await access(dirname(target), constants.W_OK); | ||
| const probe = await mkdtemp(join(dirname(target), '.probe-')); | ||
| await rm(probe, { recursive: true }); | ||
| } catch (error) { throw new BundleFailure('bucket_unreachable', String(error)); } | ||
| } | ||
| export async function fetchBundle(ref: DigestReference, bucket: string): Promise<string> { | ||
| const cacheRoot = process.env['XDG_CACHE_HOME']; | ||
| const cache = join(cacheRoot && isAbsolute(cacheRoot) ? cacheRoot : join(homedir(), '.cache'), | ||
| 'flows', 'bundles', ref.digest); | ||
| // Verify every hit; a corrupt cache never executes or silently falls back. | ||
| if (await exists(cache)) { await verifyDigest(cache, ref.digest); return cache; } | ||
| const source = bucketDirectory(bucket, ref); | ||
| try { | ||
| if (!await exists(source)) throw new BundleFailure('bucket_unreachable', `Bundle is absent at ${bucket}.`); | ||
| await verifyDigest(source, ref.digest); | ||
| await copyBundle(source, cache, ref.digest); | ||
| return cache; | ||
| } catch (error) { | ||
| if (error instanceof BundleFailure) throw error; | ||
| throw new BundleFailure('bucket_unreachable', String(error)); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.