Skip to content

Add a stub API for the workflow canvas contract - #81

Merged
knihit merged 20 commits into
integration/workflow-canvasfrom
wc/04-stub-api
Oct 28, 2026
Merged

knihit merged 20 commits into
integration/workflow-canvasfrom
wc/04-stub-api

Conversation

@knihit

@knihit knihit commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Description

Stub API (workflow-canvas/stub-api/):

  • Added a local TypeScript server that serves the six calls of api/openapi.yaml: POST /api/session, GET and PUT /api/workflow, POST /api/validate, POST /api/run and GET /api/runs/{id}. The UI and the real backend can then be built against one contract.
  • First-release scope only: an intent, stages that each have a name and an agent, and the kiro-cli harness. The data is fixed: a hello workflow with a draft stage (k-developer) and a review stage (k-quality-assurance).
  • A run is faked and calls no agent. Each stage takes 3 seconds, one after the other, and all of them pass. The clock is a parameter, so tests move it by hand and never sleep.
  • Split in two parts so the rules can be tested without a server. core/ holds the workflow check, the file store and the run simulator, and does no IO. http/ is a thin adapter on node:http that checks headers and bodies and hands the rest to the core. A test fails if a core file imports anything but its own modules, yaml and createHash, or uses Date, Math.random, performance, a random function or the environment. It walks core/ at any depth, and a set of bad snippets proves each rule.
  • The server listens on 127.0.0.1 only, and a test reads the address the socket is bound to and checks that a non-loopback address of the machine refuses the connection. It trades a one-time launch code for a session token, as the contract says, and the other calls need that token. It is a local development stub with no real auth, and the README says so.

API backend switch:

  • WC_API_TARGET (an http or https origin) picks the server behind /api. It is read in one helper, stub-api/target.ts. Unset, it is the stub. A bad value stops the command that needs it with a one-line error that hides credentials, not a stack trace; build and npm test never read it. STUB_PORT must be 1-65535, so STUB_PORT=0 or any other bad value is rejected: the stub needs a fixed port. The page still calls the relative /api; only the proxy target changes.
  • The Vite proxy sends the target's own Origin, not always the stub's. Playwright starts the stub only when WC_API_TARGET is unset; when it is set, the same tests run against that server.
  • npm run dev:stub starts the stub and Vite together (a small node launcher, no new dependency). Ctrl+C stops both. npm run dev stays Vite only.
  • A real server must pass the same Origin check and the POST /api/session step from openapi.yaml, so the same e2e tests can show it matches the stub. The README has one line per mode: stubbed dev, real dev, stubbed e2e, real e2e.

Tests:

  • Functional tests start the stub on a free port, call it over HTTP and check each reply against the schema of its status in openapi.yaml. They cover all six calls, the DUPLICATE_KEY and other diagnostics of the contract's examples, the 409 conflicts (file changed, file already exists, file gone), the header and body rules, and the order of checks.
  • Unit tests of the core cover each of the 12 diagnostic codes with its line and column, and the simulated run against a fake clock.
  • Tests cover the target helper (default, set, bad values), the dev proxy (it moves to a new target and sends that target's Origin; a bad value is refused), the Playwright server list in stub-api/web-servers.ts, which reads no environment (no stub when a target is set), the shared Playwright config loaded fresh with and without WC_API_TARGET, the STUB_PORT checks (including port 80), the redaction of secrets in the error (including spaces and /, # or ? in a password; an @ after the authority masks everything after ://), the dev:stub commands (Vite gets an empty WC_API_TARGET) and the launcher (SIGKILL after a grace time, on an injected timer; its test child exits when its parent dies). None of them sleeps.
  • Playwright starts the stub beside the app, and a test shows the config reaches it. No page calls the stub. A test starts the Vite dev server and checks that /api reaches the stub through the proxy.
  • A drift test compares the error codes (with their statuses), diagnostic codes, patterns and limits of the stub with openapi.yaml. A test of a 250 KB single-line flow mapping checks the position lookup against a 2 second bound, with no sleep.
  • CI runs npm run test:stub on Node 24 and 26.

Dependency (new):

  • yaml 2.9.1, pinned to the exact version, as a dev dependency. The contract fixes YAML 1.2 with the core schema and asks for exact line and column positions, which this parser gives. It has no dependencies of its own. The HTTP adapter uses only node:http.

Docs and config:

  • README: the whole workflow-canvas/README.md is brought up to date: layout, every npm script, the API contract, the stub and how to run the page against it.
  • CHANGELOG: an entry under Unreleased. VERSION is untouched.
  • New scripts: npm run stub, npm run dev:stub and npm run test:stub. tsconfig.node.json includes stub-api/, and Vitest runs api/ and stub-api/ in Node as a separate project from the UI tests. npm run dev forwards /api to the stub, or to WC_API_TARGET.

Fixes from the hands-on QA run

  • Reload was a dead end in npm run dev:stub. A reload drops the session token and the launch code from the page, and the printed address then failed with "The launch code is not valid", so the only way out was to restart the stub. The stub that dev:stub starts, and the one that Playwright starts, now take the launch code again and give the same token, with no time limit (SessionGate option reusableLaunchCode, switched on by the --with-dev-server flag). npm run stub on its own still follows the contract: one use, within 60 seconds. This is a deliberate, documented gap between the dev stub and the contract; a real server is unchanged. After a reload the person opens the printed address again.
  • Tests: three HTTP tests of POST /api/session on a reusable stub (same token again, still taken after the 60 seconds, a wrong code still refused), and an end-to-end test that the stub gives the same session for the same code. The first two failed before the change.
  • Checked on this head: npm test (387 tests), npm run test:e2e (7 tests) and npm run screenshots pass; lint and typecheck are clean.

Type of Change

  • Dependency change (package.json, Cargo.toml, or CLI dependency)
  • Documentation
  • Other: a local development stub and its tests, in workflow-canvas/

Testing

  • Root npm test (tests/scripts/**/*.test.js): not run. node --test tests/scripts/readme-no-internal-wording.test.js passes 4 of 4. It is not one of the CI checks.
  • If cli/ changed: cd cli && make test passes (Rust + Python + conformance suites)
  • Smoke tested affected agent(s): built and installed via cli/README.md's synth/install
    steps, then ran kiro-cli chat --agent <agent-name> (or the Claude Code equivalent)
  • If an agent's or skill's behavior changed: ran the benchmark harness for the affected agent
    — npm run benchmark -- --agent <agent-name> --replicates 1 (see docs/guides/benchmarking.md)
  • New or changed code files carry the required SPDX header (see AGENTS.md's License headers
    rule); formats without comment syntax (e.g. JSON) are exempt

Checked in workflow-canvas/ on Node 24 (the CI matrix also runs Node 26): npm run lint, npm run typecheck, npm test (382 tests, also with a bad WC_API_TARGET and with a bad STUB_PORT in the shell), npm run test:stub, npm run api:lint, npm run build and npm run test:e2e (6 tests, against the stub and again with WC_API_TARGET set to another running server) all pass. The committed screenshots are unchanged.

Screenshots: none. There is no UI change in this PR.

Checklist

  • My code follows the style guidelines of this project (see AGENTS.md's Code Style & Conventions)
  • I have performed a self-review of my own changes
  • I have commented my code where necessary
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings — build is clean, and for cli/ changes,
    cd cli && make lint passes (cargo clippy -- -D warnings + cargo fmt --check)
  • I have added or updated tests that prove my change works (see Testing above for the exact commands)

Agents Affected

None. This changes workflow-canvas/ only. The agent names in the stub are sample data in a fixed workflow file.

Notes for Reviewers

@knihit
knihit added this pull request to stack #79 October 24, 2026 19:00
@knihit
knihit force-pushed the wc/04-stub-api branch 2 times, most recently from a5b0ca0 to 66fa3a7 Compare October 25, 2026 19:56
@knihit
knihit marked this pull request as ready for review October 25, 2026 20:03
@knihit
knihit removed this pull request from stack #79 October 27, 2026 02:47
@knihit
knihit added this pull request to stack #101 October 27, 2026 02:47
@knihit
knihit removed this pull request from stack #101 October 27, 2026 02:48
@knihit
knihit added this pull request to stack #102 October 27, 2026 02:48
Comment thread workflow-canvas/vite.config.ts Outdated
@knihit
knihit force-pushed the wc/04-stub-api branch 4 times, most recently from 539cbd3 to 8a31a83 Compare October 27, 2026 21:25
Base automatically changed from wc/03-api-contract to integration/workflow-canvas October 27, 2026 22:05
@knihit
knihit removed this pull request from stack #102 October 27, 2026 22:40
@knihit
knihit added this pull request to stack #117 October 27, 2026 22:42
The core holds the rules of the API contract and does no IO: a check of
workflow text that reports every diagnostic code with its position, an
in-memory workflow store with hash-based saves, and a run simulator that
works out each stage from an injected clock. A test fails if a core file
imports a module that does IO or reads the clock, randomness or network.

Adds the yaml parser (2.9.1, exact version) as a dev dependency, and the
`test:stub` script. Type stripping runs the stub on Node 24 without a
build step, so tsconfig.node.json checks it with erasableSyntaxOnly.
A thin adapter on node:http, with no new dependency. It checks the Host,
Origin, token, content type and body, reads the request into typed values
and hands the rest to the core, in the order the contract fixes. The stub
trades a one-time launch code for a session token, as the contract says.
It is for local development and keeps no real credentials.

`npm run stub` starts it on 127.0.0.1:5174 and prints the launch address.
Each test starts the stub on a free port, calls it, and checks the reply
against the schema of its status in api/openapi.yaml. They cover the six
calls, DUPLICATE_KEY and the other diagnostics, the 409 conflicts, the
header and body rules, and the order of checks. A fake clock moves the
launch window and the stages of a run, so no test waits.
The config starts the stub after the app and exports its URL and launch
code, so an end-to-end test can call it. One test shows the wiring. No
page uses the stub yet. CI runs `npm run test:stub` on both Node versions.
Adds a README section on running it, what it serves and how it is split,
and a changelog entry under Unreleased.
knihit added 15 commits October 27, 2026 23:37
The test now allows only the named createHash import from node:crypto,
forbids Date, Math.random, performance, the random functions and
globalThis.crypto, and walks core/ at any depth. A set of bad snippets
proves each rule. Run ids and timestamps no longer use Date, so the core
formats time with its own date arithmetic, which is tested against Date.
A second document is reported at its --- line instead of 1:1. The line
and column lookup counts code points from a prefix table, so a 250 KB
single-line flow mapping is checked in well under 2 seconds instead of
over a minute. A key is named in a message only when it is short and plain,
so a key that looks like an access key or a hash is not echoed.
badRequest(), badName() and codePointLength() replace repeated literals,
and the test literal for a one-stage workflow is one helper. A test
compares the error codes, their statuses, the diagnostic codes, the
patterns and the limits of the stub with api/openapi.yaml.
The loopback test reads the address the socket is bound to and checks that
a non-loopback address of the machine refuses the connection. The launch
window starts when the stub listens. A body past 16 MiB is cut off.
npm run dev forwards /api to the stub with the stub's own Origin, so the
page and the API share one origin; this is a few lines of config where
serving the built page would need a file server with path checks. The stub
prints the dev server address, which is a live link. Vitest runs the API and
stub tests in Node through a project, so a test file needs no pragma.
Describes the scaffold, every npm script, the API contract and how to run
the page against the stub, without notes about later changes.
The page still calls the relative /api. WC_API_TARGET, read in one helper
(stub-api/target.ts), picks the server behind the dev proxy and the Playwright
run. Unset, it is the stub. The proxy sends the target's own Origin.
npm run dev:stub starts the stub and Vite together.
…its error

A bad WC_API_TARGET now stops dev, preview and the Playwright configs,
but not build or the unit tests, which never use the proxy. The error
message no longer repeats a user, password, query or fragment.

A bad STUB_PORT gives the same one-line error instead of a stack trace,
and port 80 reaches the stub as 80. A test loads playwright.shared.ts
with and without WC_API_TARGET, so dropping the env read fails it.
The dev:stub launcher is covered by tests, including that Vite gets an
empty WC_API_TARGET. A command that ignores SIGTERM gets SIGKILL after
a grace time, set by an injectable timer so the tests need no sleeps.
The stub prints the right hint when the dev server runs beside it. The
README says the launch code works once.
The Playwright server list moves to a module that reads no environment, so a
mistyped WC_API_TARGET or STUB_PORT in the shell no longer fails npm test. The
shared config is tested through a dynamic import with the environment set by
the test. The redaction masks everything up to the last @, so spaces and
unencoded /, # and ? in a password do not leak. The test child exits when its
IPC channel closes. The README notes that a SIGKILLed launcher can orphan its
children, and the changelog notes that STUB_PORT=0 is now rejected.
redact() masks the userinfo only when the authority holds an `@`. An `@`
that appears only after the authority ends (`http://h:1/?k=a@x`) cannot be
told from a password with a `/`, `?` or `#`, so everything after `://` is
masked in that case. Rewrap a README line and say that STUB_PORT must be
1-65535 without "now".
…d end

A reload drops the session token and the launch code from the page. The
printed address then failed with 'launch code is not valid'. The stub of
dev:stub, and the one that Playwright starts, now give the same token for the
same code, with no time limit. A stub started on its own still follows the
contract: one use, within 60 seconds.
The dev proxy set Origin to the target's on every request, so any page
on another localhost origin could drive POST /api/run through the dev
server. Swap the Origin only when it is the page's own (a loopback name
on the port the server listens on) and forward any other Origin, or
none, unchanged so the API refuses it. Add tests for a foreign and a
missing Origin through the proxy, and the Origin header to the README
curl example.
@knihit
knihit requested a review from ihmaws October 27, 2026 23:37
@knihit
knihit merged commit dae3650 into integration/workflow-canvas Oct 28, 2026
3 checks passed
@knihit
knihit deleted the wc/04-stub-api branch October 28, 2026 14:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants