Skip to content

fix(server): accept namespaced workflow names on HTTP launch and read routes#2047

Open
truffle-dev wants to merge 4 commits into
coleam00:devfrom
truffle-dev:fix/api-namespaced-workflow-names
Open

fix(server): accept namespaced workflow names on HTTP launch and read routes#2047
truffle-dev wants to merge 4 commits into
coleam00:devfrom
truffle-dev:fix/api-namespaced-workflow-names

Conversation

@truffle-dev

@truffle-dev truffle-dev commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Problem: Workflows can be namespaced one subfolder deep on disk (triage/foo), are discovered at MAX_DISCOVERY_DEPTH = 1, and the CLI launches them by that name — but the HTTP POST /api/workflows/{name}/run and GET /api/workflows/{name} routes validate the name with isValidCommandName, which rejects any /. So namespaced workflows can only be run from the CLI, never over the API (HTTP API cannot launch namespaced workflows: POST /api/workflows/{name}/run rejects names containing '/' #2007).
  • Why it matters: The web UI and every forge/chat adapter reach workflows through these HTTP routes. A documented, CLI-supported layout (~/.archon/workflows/triage/foo.yaml) is silently unreachable from the API surface, with two failure modes: an unencoded slash 404s (Hono :name matches one segment), and a percent-encoded %2F decodes and hits the validator, returning 400 Invalid workflow name.
  • What changed: Added isValidWorkflowName to @archon/workflows/command-validation. It allows a single / by splitting on it and requiring every segment to itself be a valid command name — so .., \, leading dots, and empty segments (leading / trailing / double slash) all stay rejected and the path-traversal protection is unchanged. Wired it into the /run and GET routes.
  • What did NOT change (scope boundary): PUT and DELETE /api/workflows/{name} keep isValidCommandName. Those write/remove a file, and creating (or cleaning up) a subfolder on save is a separate concern from resolving an existing one. isValidCommandName is untouched, so DAG step-command validation is unaffected.

Label Snapshot

  • Risk: risk: low
  • Size: size: S
  • Scope: server
  • Module: server:api-workflows

Change Metadata

  • Change type: bug
  • Primary scope: server

Linked Issue

Validation Evidence

bun run type-check                                               # exit 0
bunx eslint packages/server/src/routes/api.ts \
  packages/workflows/src/command-validation.ts --max-warnings 0  # exit 0
bun run format:check                                             # All matched files use Prettier code style
bun test packages/workflows/src/command-validation.test.ts      # 52 pass / 0 fail
bun test packages/server/src/routes/api.workflows.test.ts       # 39 pass / 0 fail
bun test packages/server/src/routes/api.providers.test.ts \
         packages/server/src/routes/api.health.test.ts          # 33 pass / 0 fail (mock factory updated)

Guards proven red, not just green:

  • Mutating isValidWorkflowName to delegate to isValidCommandName (reject all /) turns the two namespace-accepting unit tests red.
  • Reverting the GET route back to isValidCommandName turns the new triage%2Freview integration test (writes .archon/workflows/triage/review.yaml, expects 200 source:project) red.

Security Impact

  • New permissions/capabilities? No. New network calls? No. Secrets handling changed? No. File system access scope changed? No — isValidWorkflowName is strictly narrower than "allow any name": it permits exactly one extra character class (a single interior /) and still rejects .., \, leading dots, and empty segments, so no new path-traversal surface is opened. The resolved paths are the same ones the CLI already reads.

Compatibility / Migration

  • Backward compatible — names that were valid before stay valid (non-namespaced names have one segment). No config / env / DB changes.

Side Effects / Blast Radius

  • Affects the /run and GET workflow-name routes only. A name that resolves to a namespaced workflow now reaches discovery/readFile instead of 400ing; everything else behaves identically.

Rollback Plan

  • Revert this PR's merge commit. The two routes return to isValidCommandName and namespaced workflows are CLI-only again. No flags/config.

Risks and Mitigations

  • Risk: a name with a single / that does not correspond to an on-disk namespaced workflow.
    • Mitigation: it falls through to the existing 404 path (no file found) exactly like any other unknown name; the /run route resolves through resolveWorkflowName, which already handles misses.

Summary by CodeRabbit

  • New Features

    • Added workflow-name validation with up to one level of namespacing (e.g., triage/review).
    • Workflow endpoints now apply workflow-specific name rules for both fetching and running workflows.
  • Bug Fixes

    • Invalid workflow names continue to return the same 400 “Invalid workflow name” response.
    • Improved handling for edge cases like empty input, extra/double slashes, hidden segments, and path-traversal-style values.
  • Tests

    • Expanded validation tests and added coverage for percent-encoded namespaced workflow paths in retrieval and run endpoints.

… routes

Workflows can be namespaced one subfolder deep on disk (e.g. triage/foo),
discovered at MAX_DISCOVERY_DEPTH = 1, and the CLI launches them by that name.
The HTTP /run and GET routes validated the name with isValidCommandName, which
rejects any '/', so namespaced workflows could only be run from the CLI (coleam00#2007).

Add isValidWorkflowName: allows a single '/' by requiring every slash-separated
segment to be a valid command name, so '..', '\', leading dots, and empty
segments (leading/trailing/double slash) stay rejected and path traversal is
unchanged. Wire it into the non-mutating /run and GET routes only; PUT and
DELETE keep isValidCommandName since creating a subfolder on save is separate.
@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds isValidWorkflowName for one-level namespaced workflows, switches workflow API routes to use it, and updates discovery plus server tests and mocks to match the new validation path.

Namespaced workflow name validation

Layer / File(s) Summary
Validator implementation and tests
packages/workflows/src/command-validation.ts, packages/workflows/src/command-validation.test.ts, packages/workflows/src/workflow-discovery.ts
isValidWorkflowName and MAX_DISCOVERY_DEPTH are added, and tests cover valid names plus invalid slash, traversal, dot, and empty cases. Discovery now imports the shared depth constant.
API route handlers
packages/server/src/routes/api.ts
POST /api/workflows/:name/run and GET /api/workflows/:name now use isValidWorkflowName and keep the same 400 Invalid workflow name response.
Mocks and route tests
packages/server/src/test/workflow-mock-factories.ts, packages/server/src/routes/api.workflows.test.ts, packages/server/src/routes/api.workflow-runs.test.ts
The command-validation mock gains isValidWorkflowName, and route tests cover namespaced workflow fetch plus run behavior and invalid-name handling.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • coleam00/Archon#1405: Also touches GET /api/workflows/:name in packages/server/src/routes/api.ts, but for a different workflow-resolution path.

Poem

🐇 I hop through names with one / in sight,
One folder deep feels just right.
No .. tricks, no hidden maze,
Just workflow paths in sunny praise.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: allowing namespaced workflows through the HTTP launch and read routes.
Description check ✅ Passed The description is mostly complete and covers summary, scope, validation, risks, rollback, and impact, despite missing some template sections.
Linked Issues check ✅ Passed The code changes satisfy #2007 by allowing one-level namespaced workflow names and wiring the validator into the run and read routes.
Out of Scope Changes check ✅ Passed The additional validator sharing, mocks, and test updates are all directly related to the workflow-name API fix.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/server/src/routes/api.workflows.test.ts (1)

333-360: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a /run namespaced-route regression test.

This change updates both GET /api/workflows/:name and POST /api/workflows/:name/run, but the new integration coverage only locks down the GET path. The issue being fixed is launchability over HTTP, so a percent-encoded namespaced name should be exercised on the run endpoint too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server/src/routes/api.workflows.test.ts` around lines 333 - 360, Add
a regression test for the namespaced run path in api.workflows.test.ts so the
percent-encoded workflow name is covered for POST /api/workflows/:name/run as
well as GET. Reuse the existing triage/review setup in the same test area, then
call the run endpoint with triage%2Freview and assert the request succeeds and
targets the expected workflow. Keep the coverage aligned with registerApiRoutes
and the workflow lookup behavior used by the existing namespaced GET test.
packages/workflows/src/command-validation.ts (1)

33-35: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Share the namespace-depth rule with workflow discovery.

segments.length > 2 duplicates the one-subfolder contract from packages/workflows/src/workflow-discovery.ts (MAX_DISCOVERY_DEPTH = 1). If that depth ever changes, the API validator can drift and start rejecting discoverable workflows or accepting names the loader never finds.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/workflows/src/command-validation.ts` around lines 33 - 35, The
namespace-depth check in the command validator is duplicating the workflow
discovery contract and can drift from the loader. Update the validation logic in
command-validation.ts to reuse the same depth rule/source of truth as
workflow-discovery.ts, ideally by sharing MAX_DISCOVERY_DEPTH or a common
helper, so name validation stays aligned with workflow discovery behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/server/src/routes/api.workflows.test.ts`:
- Around line 333-360: Add a regression test for the namespaced run path in
api.workflows.test.ts so the percent-encoded workflow name is covered for POST
/api/workflows/:name/run as well as GET. Reuse the existing triage/review setup
in the same test area, then call the run endpoint with triage%2Freview and
assert the request succeeds and targets the expected workflow. Keep the coverage
aligned with registerApiRoutes and the workflow lookup behavior used by the
existing namespaced GET test.

In `@packages/workflows/src/command-validation.ts`:
- Around line 33-35: The namespace-depth check in the command validator is
duplicating the workflow discovery contract and can drift from the loader.
Update the validation logic in command-validation.ts to reuse the same depth
rule/source of truth as workflow-discovery.ts, ideally by sharing
MAX_DISCOVERY_DEPTH or a common helper, so name validation stays aligned with
workflow discovery behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f4f6fa13-44c2-420c-b9ad-c7b50922c542

📥 Commits

Reviewing files that changed from the base of the PR and between 59bbd00 and a540d5d.

📒 Files selected for processing (5)
  • packages/server/src/routes/api.ts
  • packages/server/src/routes/api.workflows.test.ts
  • packages/server/src/test/workflow-mock-factories.ts
  • packages/workflows/src/command-validation.test.ts
  • packages/workflows/src/command-validation.ts

The POST /api/workflows/:name/run route now validates with
isValidWorkflowName, but this test still mocked isValidCommandName to
reject, so validation passed and the route returned 200 instead of 400.
Mock the validator the route actually calls.
…loader

The workflow-name validator hard-coded a two-segment cap to mirror the
loader's discovery depth, duplicating the one-subfolder contract. If the
loader's MAX_DISCOVERY_DEPTH ever changed, the validator would silently
drift and start rejecting discoverable names or accepting names the loader
never finds.

Move MAX_DISCOVERY_DEPTH into command-validation (the dependency-free leaf
module) as the single source of truth, derive the segment cap from it, and
import it back into workflow-discovery. The import direction keeps the leaf
import-free, so the executor/dag-executor cycle this module breaks stays
broken.
The namespaced-name fix updated both GET /api/workflows/:name and
POST /api/workflows/:name/run, but the new coverage only locked down the
GET path. Since launchability over HTTP is the point of the fix, exercise a
percent-encoded namespaced name on the run endpoint too: assert it is
accepted and that the decoded `triage/review` reaches the orchestrator as
`/workflow run triage/review ...`. The test installs the real validator
logic so it goes red if the run route validates with isValidCommandName
instead of isValidWorkflowName.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/server/src/routes/api.workflow-runs.test.ts (1)

381-389: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add explicit return types to the new test callbacks.

Line 381, Line 384, and Line 393 introduce new functions without explicit return annotations, which breaks the repo’s strict TypeScript rule for complete function typings.

Suggested diff
-    const segmentOk = (seg: string) =>
+    const segmentOk = (seg: string): boolean =>
       !!seg && !seg.startsWith('.') && !seg.includes('\\') && !seg.includes('..');
@@
-    (isValidWorkflowName as ReturnType<typeof mock>).mockImplementationOnce((name: string) => {
+    (isValidWorkflowName as ReturnType<typeof mock>).mockImplementationOnce((name: string): boolean => {
       if (!name) return false;
       const segments = name.split('/');
       if (segments.length > 2) return false;
       return segments.every(segmentOk);
     });
@@
-      (name: string) => segmentOk(name) && !name.includes('/')
+      (name: string): boolean => segmentOk(name) && !name.includes('/')
     );

As per coding guidelines, **/*.{ts,tsx} requires that “all functions must have complete type annotations; no any types without explicit justification”.

Also applies to: 392-393

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server/src/routes/api.workflow-runs.test.ts` around lines 381 - 389,
The new test callbacks in api.workflow-runs.test.ts are missing explicit return
type annotations, which violates the repo’s strict TypeScript typing rule.
Update the local helpers and mock implementations around segmentOk and
isValidWorkflowName so every newly introduced function has a complete signature,
including explicit return types for the arrow functions and
mockImplementationOnce callback.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/server/src/routes/api.workflow-runs.test.ts`:
- Around line 381-389: The new test callbacks in api.workflow-runs.test.ts are
missing explicit return type annotations, which violates the repo’s strict
TypeScript typing rule. Update the local helpers and mock implementations around
segmentOk and isValidWorkflowName so every newly introduced function has a
complete signature, including explicit return types for the arrow functions and
mockImplementationOnce callback.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 62946b2d-7874-4d86-a517-d84af4c7bc5a

📥 Commits

Reviewing files that changed from the base of the PR and between 4300d9e and f7a2d1a.

📒 Files selected for processing (3)
  • packages/server/src/routes/api.workflow-runs.test.ts
  • packages/workflows/src/command-validation.ts
  • packages/workflows/src/workflow-discovery.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/workflows/src/command-validation.ts

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.

HTTP API cannot launch namespaced workflows: POST /api/workflows/{name}/run rejects names containing '/'

1 participant