Skip to content

Add the workflow canvas HTTP API contract and its test - #80

Open
knihit wants to merge 10 commits into
wc/02-e2efrom
wc/03-api-contract
Open

knihit wants to merge 10 commits into
wc/02-e2efrom
wc/03-api-contract

Conversation

@knihit

@knihit knihit commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Description

API contract (workflow-canvas/api/openapi.yaml, OpenAPI 3.1):

  • Added the HTTP API the canvas calls, with six operations: start a session (POST /api/session), load a workflow (GET /api/workflow), save it (PUT /api/workflow), check it (POST /api/validate), start a run (POST /api/run) and get the status of a run (GET /api/runs/{id}). Anything else is left out until it is needed, so the contract only grows.
  • A workflow in this version is a name, an intent and stages, and each stage is a name and an agent. The only harness is kiro-cli. The file holds small named schemas (Name, Stage, WorkflowView, Diagnostic, Run and so on) that the operations reuse through $ref.
  • The contract stands on its own for someone who implements it: it states the accepted workflow text (YAML 1.2 read with the core schema, so on and no are strings and true is a boolean; keys, limits, reserved names, the plain YAML subset), the checks that make a text invalid, and a table of the diagnostic codes with the path and position each reports. A key that appears twice in one mapping is the new DUPLICATE_KEY diagnostic, reported at the later key. Keys compare by their text before it is read as a number, boolean or null (1 and '1' are one key; true and True, or 1 and 1.0, are two), a key that is not a scalar is YAML_UNSUPPORTED, and an unknown key that is repeated has UNKNOWN_KEY at the first and DUPLICATE_KEY at each later one. I chose a new code over YAML_SYNTAX because YAML_SYNTAX reports where a parser stopped, which differs between parsers, while DUPLICATE_KEY has one position the stub and the backend can both give.
  • It says how a run goes: stages run one at a time in file order, each in a fresh kiro-cli session given the intent's outcome and the goal; a stage passes on exit status 0 and is otherwise blocked (including an agent id that does not exist), which halts the run with exit_code 1; a run left running by a server stop reads as halted after the restart.
  • It says how files are handled: the workflow file is opened by name only, and a link, a directory, a file over 262144 bytes or one that is not valid UTF-8 is 404 not_found on load, save and run. The server writes text exactly as sent and loads it unchanged, so a hash is the hash of that text. A base_hash of null on an existing file is a 409 conflict, and so is a base_hash that is a hash for a file that no longer exists (that one has no current_hash). A length counts Unicode code points, and a line and column are defined. It also gives the accepted Host and Origin, the order in which a call is checked (a run checks the saved file first, then the workflow, then whether a run is active, then the harness), the strength of the launch code and token, where the launch code is found (the 60 second window starts when the server prints the launch address), and the run id in a 409 already_running.
  • It also decides the edge cases: only the workflow the server was started on can be loaded, saved, checked and run, a run of a workflow that was never saved is a 404, a lost save is recognised by comparing hashes, and the run schema states what holds between status, exit_code, finished_at and the stages.
  • Every error response uses one Error schema: a sentence for a person (error), a stable code, and a few optional extras such as current_hash for a save conflict. Common responses are defined once and referenced. Every response declares the X-Konductor-Api header.
  • Every operation has an operationId, a summary and examples for each request and response body.
  • Saving uses the file's hash for optimistic concurrency: base_hash must match the hash that load returned, or the save is a 409 conflict. A base_hash of null creates a file that must not exist yet. Checking base_hash and writing are one step with respect to other saves, so of two saves with the same base_hash one is a 409, and a create is exclusive.
  • Run status is read by polling GET /api/runs/{id}, about once a second. A push stream for run events is not part of this version.
  • A response may gain fields, so response schemas allow extra properties and clients ignore them. A request with a field the contract does not name is refused.
  • This version starts every run without a trust step. The contract says that a later version refuses an untrusted run with 409, so clients handle an unknown 409.

Contract test (workflow-canvas/api/openapi.test.ts):

  • Parses the file and validates it against the OpenAPI 3.1 specification with @apidevtools/swagger-parser, which also fails on a $ref that does not resolve.
  • Pins the operation list to the six above, so an added operation is a deliberate change to the test.
  • Requires an operationId and summary on every operation, an example on every request and response body, the version header on every response, and the one Error schema on every error status.
  • Checks that each error example has a code that fits its status, and that the diagnostic codes in the schema and in the description table are the same.
  • Checks that request schemas are closed and response schemas are open.
  • Validates every example, including the ones inside schemas, against its schema with ajv (JSON Schema 2020-12, the dialect of OpenAPI 3.1) and ajv-formats.
  • Checks that the examples obey the contract: a hash is the SHA-256 of its text, a view lists what its own text says (each answer is paired with the text it answers, not with any text in the file), a diagnostic points at the name in its message, the counts of a check add up, and the stages of a run follow its status.
  • Runs in npm test and on its own as npm run api:lint, which runs only this test file. Vitest also picks up api/**/*.test.ts, and tsconfig.node.json and ESLint cover api/.

Dependencies:

  • Added @apidevtools/swagger-parser 13.1.0, ajv 8.20.0 and ajv-formats 3.0.1, exact-pinned dev dependencies in the existing package.json (still one package). ESLint keeps its own ajv 6, which npm now nests under eslint. npm audit reports 0 vulnerabilities.

CI:

  • .github/workflows/workflow-canvas.yml: added an API contract step (npm run api:lint) to the existing lint/typecheck/test/build job, so it runs on Node 24 and 26. It repeats the contract test that npm test runs, to give a failure its own label.

Docs:

  • The order of checks now places the unknown path (404 not_found) and the wrong method (405 method_not_allowed) between Host and Origin, and the YAML_UNSUPPORTED row says a second YAML document is reported at its --- line.
  • workflow-canvas/README.md: an "API contract" section (what the file is, how to validate it, and that every server of the API follows it), and api/ moved out of the "later PRs" list. The root README.md project layout lists workflow-canvas/.
  • CHANGELOG.md: entry under Unreleased. VERSION is not bumped.

Screenshots

No UI change in this PR, so there are no new screenshots. The Designer and Execution screenshots in the previous PR still apply.

Type of Change

  • Dependency change (package.json, Cargo.toml, or CLI dependency)
  • Infrastructure / pipeline
  • Documentation
  • Other: API contract and contract test for workflow-canvas/

Testing

  • Root npm test (runs tests/scripts/**/*.test.js) gives 119 passing and 17 failing. The 17 failures are the same ones as before this PR, in claude-code-agent-runner.test.js, generate-agent-files.test.js and run-claude-subset.test.js, which this PR does not touch. It is not one of the CI checks.
  • In workflow-canvas/: npm run lint, npm run typecheck, npm test (39 tests: 3 existing and 36 for the contract), npm run build and npm run test:e2e (5 tests) pass locally on Node 24
  • I checked the contract test can fail: a wrong type in an example, a closed response schema, an open request schema, an error code under the wrong status, a wrong hash, wrong counts in a check answer and a broken example inside a schema each make it fail. The same holds for the new checks: a missing DUPLICATE_KEY in the schema, a repeated-key diagnostic at the first key, a save answer with a different view than its text, a missing YAML statement and a check answer without a request of its name. I reverted each edit.
  • New code files carry the required SPDX header; JSON is exempt

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
  • I have added or updated tests that prove my change works

Notes for Reviewers

  • Draft, and the third PR in a stack: integration/workflow-canvas <- Add workflow-canvas scaffold: Vite, React, Cloudscape, CI #77 (wc/01-scaffold) <- Add Playwright end-to-end tests and screenshots for workflow-canvas #78 (wc/02-e2e) <- this PR. Base is wc/02-e2e; GitHub retargets it as Add Playwright end-to-end tests and screenshots for workflow-canvas #78 merges.
  • Not in this PR: the stub server that serves this contract, a typed client, and any UI change. They follow in later PRs and are checked against this file. The real backend is built against it too.
  • The contract is meant to be read on its own: the info.description lists the request checks, the size limits, the workflow text and the diagnostic codes.
  • Judgement calls worth a look: POST /api/session is in the contract so the stub and the real server hand out the token the same way; a check with errors has no view; the diagnostic codes are a closed list for this version that clients treat as open; the run state keeps order plus a stages map keyed by stage name; times are ISO 8601 strings; base_hash, text, hash and view are present-but-null rather than absent where a file does not exist yet.
  • package-lock.json accounts for about 200 of the added lines.

@knihit
knihit added this pull request to stack #79 October 24, 2026 04:14
@knihit
knihit force-pushed the wc/03-api-contract branch 2 times, most recently from ac20dc4 to 84da3ee Compare October 24, 2026 04:58
@knihit
knihit force-pushed the wc/03-api-contract branch from 84da3ee to 8521072 Compare October 24, 2026 13:12
@knihit
knihit force-pushed the wc/03-api-contract branch 2 times, most recently from 393f220 to 6a848d6 Compare October 24, 2026 18:25
@knihit
knihit marked this pull request as ready for review October 25, 2026 18:18
knihit added 10 commits October 25, 2026 20:03
Describe the first-release API in workflow-canvas/api/openapi.yaml
(OpenAPI 3.1): load, save and check a workflow, start a run, and get
a run's status. A workflow is a name, an intent and stages of name and
agent; the only harness is kiro-cli. Every error response uses one
Error schema.

A vitest test validates the file against the OpenAPI 3.1 specification,
pins the operation list, requires an operationId, summary and example on
every operation, and checks each example against its schema. It is also
available as `npm run api:lint`, which CI runs.

Adds @apidevtools/swagger-parser, ajv and ajv-formats as exact-pinned
dev dependencies.
Add POST /api/session, so a client can get the token that every other call
needs. Say how the contract grows: a response may gain fields and a client
ignores the ones it does not know, so the response schemas no longer forbid
extra properties. A request still refuses them.

State what a backend has to decide: which workflow is loaded, saved and run
(the one the server started on, and a 404 for another name or for a run with
no saved file), the accepted workflow text, the checks that make a text
invalid, and a table of the diagnostic codes. Add the request guards, the
retry rule for a lost save, the size limits, the run invariants and the
exit codes. A check without errors has a view, and one with errors has none.
Note that a later version will refuse an untrusted run with 409.

Declare the X-Konductor-Api header on every response. Replace the invalid
example with a one-error case.

Extend the contract test: schema-level examples, the code that fits each
error status, open response and closed request schemas, the diagnostic
codes against their table, and the rules that the examples must obey (a
hash is the SHA-256 of its text, counts add up, a run's stages follow its
status). api:lint now runs only the contract test.
Describe what a run does stage by stage: the order, what each stage is given,
what makes a stage pass or block, the exit code of a halted run, an unknown
agent and a run left running by a server stop. State that a file that is not
a plain readable file is 404 and that the server writes text as sent.

Also fix the accepted Host and Origin values, the order of checks, the launch
code, bad_name for a body name, the run id in already_running, the goal limit
in characters, UTC timestamps, and the open cases of the check rules. The test
now ties each view and diagnostic position to the example text.
State that the workflow text is YAML 1.2 read with the core schema, and add
a DUPLICATE_KEY diagnostic for a key that appears twice in one mapping.

Also say how a call is checked when it is a run, that a null base_hash on an
existing file is a conflict, that a load returns the file's bytes unchanged,
what a character, column and line are, when a suggestion is given, which
Content-Type values are accepted, what a crashed run reads as when every
stage had passed, and how strong a launch code and token are. Make the
tests pair each view and diagnostic with its own text.
A save with a base_hash for a file that does not exist is a 409
conflict without current_hash, shown by a second example. The repeated
key test now checks the exact line and columns, and the contract says
keys are compared as strings and how an unknown repeated key is
reported.
…I contract

Keys compare by their text before typing, a non-scalar key is unsupported, the base_hash check and the write are one step with an exclusive create for null, and the 60 second window starts when the launch address is printed. Each rule has a check example or a test, and the unsupported running-run check is dropped.
…e API contract

The order of checks now names the unknown path (404) and the wrong method
(405) between Host and Origin. A second YAML document is reported at its
--- line. The README and CHANGELOG no longer describe the stub as a later
change, and the root README lists workflow-canvas in the project layout.
The prose promised a 405 with an Allow header and the error code was
listed, but no operation had a 405 response. Add a shared
MethodNotAllowed response and an Allow header, reference them from every
operation, and check that every status with an error code is documented
and that each operation documents the Allow header.
The one shared Allow example said GET, PUT for every path, but only
/api/workflow accepts both. Each path now points at a 405 response whose
Allow header names its real methods, and the contract test compares that
header to the methods the path declares. In each operation the 405 entry
now follows 404.
@knihit
knihit force-pushed the wc/03-api-contract branch from 1bedac4 to 3db3e30 Compare October 25, 2026 20:05
@knihit

knihit commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor Author

405 is now documented on every operation in openapi.yaml, with a per-path Allow header. A contract test checks each path's Allow against the methods it accepts. Fixed in d41549b and 3db3e30.

This branch has not been deployed

No deployments
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.

1 participant