Skip to content

fix(cli): orchestrator + list/destroy read active.json; sanitize HTML errors#118

Merged
khaliqgant merged 2 commits into
mainfrom
fix-cli/deploy-active-json-and-list-destroy-url
May 13, 2026
Merged

fix(cli): orchestrator + list/destroy read active.json; sanitize HTML errors#118
khaliqgant merged 2 commits into
mainfrom
fix-cli/deploy-active-json-and-list-destroy-url

Conversation

@khaliqgant
Copy link
Copy Markdown
Member

Summary

  • Deploy orchestrator now reads ~/.agentworkforce/active.json + ~/.agent-relay/cloud-auth.json (matching what fix(cli): reuse @agent-relay/cloud auth as Bearer; drop workspace-token mint #113 already did for the cloud launcher + list/destroy). Previously the orchestrator's default resolver was envWorkspaceAuth(), which only honored env vars + a long-dead keychain — so a user who freshly ran agentworkforce login hit no workspace resolved because the freshly-written shared accessToken + active.json pointer were invisible to it.
  • list and destroy defaulted to https://agentrelay.com (missing the /cloud basePath), routing every API call to the Next.js marketing 404 page; the wall-of-HTML body was then dumped verbatim into stderr. Centralized URL resolution + HTML body sanitization fix both.
  • Adds WORKFORCE_DISABLE_SHARED_AUTH=1 for hermetic tests and power users who want strictly env-only operation.

Why now

Reproduced today: agentworkforce login succeeded, whoami + integrations + deployments curls returned 200 with the relay accessToken — but agentworkforce deployments list returned 404 with the full marketing-site HTML, and agentworkforce deploy --no-prompt exited at the workspace-resolution step before ever reaching the network. Live curl of the same endpoints with the same Bearer worked, confirming the auth state was fine — only the CLI plumbing was wrong.

Changes

New helpers (@agentworkforce/deploy):

  • resolveCloudUrl(context) — single source of truth: flag → env → active.json → canonical default, always run through canonicalizeCloudUrl.
  • canonicalizeCloudUrl now also remaps the bare apex https://agentrelay.comhttps://agentrelay.com/cloud so the marketing-site fallthrough can't happen even if a stale config carries the old default.
  • formatHttpErrorBody(body, opts) — HTML detection + truncation. Replaces the wall-of-script-tags with a one-liner that names the likely cause and the offending URL.
  • WORKFORCE_DISABLE_SHARED_AUTH env opt-out in both readSharedAuthForBearer and readWorkspaceTokenFromCloudAuth.

Wiring changes:

  • packages/deploy/src/deploy.ts:148 — default auth resolver swapped from envWorkspaceAuth() to resolveWorkspaceToken(). Explicit resolvers.workspaceAuth (tests, CI harnesses) still wins.
  • packages/cli/src/list-command.ts — uses resolveCloudUrl + readActiveWorkspace + formatHttpErrorBody. Drops the local DEFAULT_CLOUD_URL and normalizeCloudUrl helper.
  • packages/cli/src/destroy-command.ts — same. Also now lets workspace come from active.json (parity with list).

Tests

17 new tests across 4 files (93 deploy + 186 cli all pass):

  • cloud-url.test.ts — apex canonicalization, resolveCloudUrl precedence (flag/env/active/default), whitespace-only candidates skipped.
  • error-format.test.ts — HTML detection, truncation, URL inclusion in hint, bare-angle-bracket false-positive guard.
  • deploy.test.ts — env-Tier 1 happy path through the new default resolver, noPrompt error path with active-file isolation.
  • destroy-command.test.ts — HTML 5xx body sanitization, active.json cloudUrl fallback, isolateAuthFiles() helper for hermetic tests.

Smoke verified

Built locally and ran in ../proactive-agents with clean env:

$ agentworkforce deploy ./.agentworkforce/notion-essay-pr/persona.json --mode cloud --on-exists cancel --no-prompt
workforce deploy → ...persona.json
persona notion-essay-pr: 2 integration(s), 0 schedule(s)
workspace: 50587328-441d-4acb-b8f3-dbe1b3c5de99    ← was: "no workspace resolved"
integrations.notion: already connected             ← was: 401 unauthorized
integrations.github: already connected
bundle: staged to .../runner.mjs (14.5KB)
mode: cloud
(fails downstream on harness creds — M3 scope, not this PR)

$ agentworkforce deployments list
No deployed agents found.                          ← was: 404 + 800-line HTML page
0 agent(s).

Test plan

  • pnpm -F @agentworkforce/deploy run lint clean
  • pnpm -F @agentworkforce/deploy run test — 93/93 pass
  • pnpm -F @agentworkforce/cli run lint clean (the 2 pre-existing local-personas.ts tags errors require a pnpm install to refresh @agentworkforce/persona-kit dist; once that's done, lint is clean)
  • pnpm -F @agentworkforce/cli run test — 186/186 pass
  • pnpm run check (full repo) — green
  • Local-build smoke against ../proactive-agents deploy + list

🤖 Generated with Claude Code

The deploy orchestrator was still using the legacy `envWorkspaceAuth()`
default, which only consults `WORKFORCE_WORKSPACE_TOKEN` env + a long-dead
keychain. A user who freshly ran `agentworkforce login` (which writes the
shared @agent-relay/cloud accessToken + an active.json pointer) would hit
`no workspace resolved` because that flow is invisible to the env-only
resolver. PR #113 fixed the cloud launcher and list/destroy commands but
missed this orchestrator entry point.

The list and destroy CLI commands also defaulted to `https://agentrelay.com`
(missing the `/cloud` basePath), so every API call landed on the marketing
site's Next.js 404 page — and the full HTML response body was dumped
verbatim into the CLI error message.

Comprehensive fix:

* Add `resolveCloudUrl()` as the single source of truth for cloud URL
  resolution (flag → env → active.json → canonical default). All three
  CLI commands and the orchestrator now route through it. The canonical
  default is now applied via `canonicalizeCloudUrl`, which also remaps
  the bare apex `agentrelay.com` → `agentrelay.com/cloud` to prevent
  the marketing-site fallthrough from ever happening again.
* Add `formatHttpErrorBody()` — detects HTML response bodies and replaces
  them with a one-line hint, truncates long non-HTML bodies. list and
  destroy both use it.
* Swap the orchestrator's auth default from `envWorkspaceAuth()` to
  `resolveWorkspaceToken()`, which respects the same env vars (Tier 1
  for CI) but additionally falls through to the shared cloud-auth +
  active.json pointer.
* Add `WORKFORCE_DISABLE_SHARED_AUTH` opt-out for hermetic tests and
  power users who want strictly env-only operation.

Tests: 17 new (cloud-url, error-format, deploy, destroy) covering URL
resolution precedence, apex canonicalization, HTML body sanitization,
the deploy env-Tier 1 path, deploy noPrompt error message, destroy
active.json fallback, and the test isolation hook.

Smoke verified against the local build in proactive-agents:
`agentworkforce deploy ... --no-prompt` now resolves the workspace from
active.json, finds both notion and github already connected, and stages
the bundle (failing later on harness creds, which is M3 scope).
`agentworkforce deployments list` returns clean results instead of a
404 HTML wall.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 13, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 33c1a2bc-61a0-4ee6-8ad5-2a0c3690fd73

📥 Commits

Reviewing files that changed from the base of the PR and between 1578779 and 6c58349.

📒 Files selected for processing (3)
  • packages/cli/src/destroy-command.test.ts
  • packages/deploy/src/deploy.test.ts
  • packages/deploy/src/deploy.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/deploy/src/deploy.ts
  • packages/deploy/src/deploy.test.ts
  • packages/cli/src/destroy-command.test.ts

📝 Walkthrough

Walkthrough

Refactors cloud URL resolution and workspace authentication across deploy and CLI. Adds resolveCloudUrl and formatHttpErrorBody, consults active workspace for cloud URL and token resolution (with a shared-auth opt-out), and updates destroy/list to use these utilities with new tests and isolation helpers.

Changes

Cloud URL Resolution and Auth Workspace Integration

Layer / File(s) Summary
Cloud URL canonicalization and resolution
packages/deploy/src/cloud-url.ts, packages/deploy/src/cloud-url.test.ts
canonicalizeCloudUrl now remaps bare agentrelay.com apex to /cloud. New resolveCloudUrl function implements priority-ordered resolution (flag → env → active workspace → default) with canonicalization. CloudUrlContext interface and firstTruthy helper support resolution.
HTTP error body formatting
packages/deploy/src/error-format.ts, packages/deploy/src/error-format.test.ts
New formatHttpErrorBody utility formats HTTP response bodies for CLI: suppresses raw HTML/Next.js error pages with a concise hint and byte count, truncates non-HTML payloads to configurable maxLength with truncation suffix, and optionally includes request URL. looksLikeHtml detector uses head checks and regex patterns.
Deploy workspace and auth resolution
packages/deploy/src/deploy.ts, packages/deploy/src/deploy.test.ts, packages/deploy/src/login.ts
deploy() now reads active workspace via readActiveWorkspace(), computes cloud URL with resolveCloudUrl(), resolves workspace token via resolveWorkspaceToken() (optionally overridable), and derives final workspace from auth result or options. login.ts adds WORKFORCE_DISABLE_SHARED_AUTH support to bypass shared cloud auth sources via isTruthyEnv. New tests verify default auth resolver path and deterministic failure when noPrompt is set without credentials.
Destroy command cloud URL and auth integration
packages/cli/src/destroy-command.ts, packages/cli/src/destroy-command.test.ts
executeDestroy reads active workspace, computes cloudUrl via resolveCloudUrl(), resolves workspace token with the computed URL, and derives workspace from auth/options. HTTP error responses are formatted using formatHttpErrorBody with a 200-char limit. New test helper isolateAuthFiles() prevents host active.json/cloud-auth.json leakage; regression tests verify HTML error suppression and active.json-derived cloud URL.
List command cloud URL and error formatting
packages/cli/src/list-command.ts
runDeploymentList reads active workspace, computes cloudUrl via resolveCloudUrl(), conditionally passes noPrompt to token resolution, and uses formatHttpErrorBody to format non-OK HTTP response bodies. Local normalizeCloudUrl helper removed.
Public API re-exports
packages/deploy/src/index.ts
Extended re-exports include resolveCloudUrl and CloudUrlContext alongside canonicalizeCloudUrl, and formatHttpErrorBody is re-exported.

Sequence Diagram

sequenceDiagram
  participant CLI as CLI (destroy/list)
  participant Active as readActiveWorkspace
  participant CloudURL as resolveCloudUrl
  participant Auth as resolveWorkspaceToken
  participant API as Cloud API
  participant Formatter as formatHttpErrorBody

  CLI->>Active: readActiveWorkspace()
  CLI->>CloudURL: resolveCloudUrl(flag, env, active)
  CloudURL-->>CLI: canonical cloud URL
  CLI->>Auth: resolveWorkspaceToken(cloudUrl, workspace, noPrompt)
  Auth-->>CLI: workspace + token
  CLI->>API: API request (workspace, token, cloudUrl)
  API-->>CLI: Response (ok or error)
  alt Non-OK response
    CLI->>Formatter: formatHttpErrorBody(responseText, { url, maxLength })
    Formatter-->>CLI: suppressed/truncated hint
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped through active.json, env, and flags today,
Canonized the cloud so requests find their way.
HTML errors hushed to a tiny byte hint,
Tokens resolved or we fail succinct.
Tests and helpers guard the auth path—hip, hop, hooray!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately summarizes the main changes: fixing CLI auth by reading active.json and addressing HTML error sanitization in list/destroy commands.
Description check ✅ Passed The PR description is comprehensive and directly related to the changeset, covering motivation, specific changes, and testing verification for all modified components.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-cli/deploy-active-json-and-list-destroy-url

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/deploy/src/deploy.ts (1)

234-234: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Pass the resolved cloud URL into launcher input.

At Line 234, using opts.cloudUrl bypasses the resolved value from Lines 154–157. That can desync endpoint selection/canonicalization between auth/integration flows and launch.

🔧 Proposed fix
-    ...(opts.cloudUrl ? { cloudUrl: opts.cloudUrl } : {}),
+    ...(cloudUrl ? { cloudUrl } : {}),
🤖 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/deploy/src/deploy.ts` at line 234, The launcher input is currently
using opts.cloudUrl which bypasses the previously resolved/normalized cloud URL
computed earlier; update the spread at ...(opts.cloudUrl ? { cloudUrl:
opts.cloudUrl } : {}) to use the resolved variable produced in the earlier
resolution logic (the canonical cloudUrl/resolvedCloudUrl variable set around
lines 154–157) so the launcher receives the same canonical endpoint (preserve
the conditional/fallback behavior when the resolved value is undefined).
packages/cli/src/destroy-command.test.ts (1)

75-94: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fully restore cloud URL env vars in withTokenEnv cleanup.

The cleanup only restores WORKFORCE_DEPLOY_CLOUD_URL / WORKFORCE_CLOUD_URL when they were previously defined. If a test mutates either var, state can leak into subsequent tests.

Proposed fix
   return () => {
     if (prevToken === undefined) delete process.env.WORKFORCE_WORKSPACE_TOKEN;
     else process.env.WORKFORCE_WORKSPACE_TOKEN = prevToken;
     if (prevWs === undefined) delete process.env.WORKFORCE_WORKSPACE_ID;
     else process.env.WORKFORCE_WORKSPACE_ID = prevWs;
-    if (prevCloudA !== undefined) process.env.WORKFORCE_DEPLOY_CLOUD_URL = prevCloudA;
-    if (prevCloudB !== undefined) process.env.WORKFORCE_CLOUD_URL = prevCloudB;
+    if (prevCloudA === undefined) delete process.env.WORKFORCE_DEPLOY_CLOUD_URL;
+    else process.env.WORKFORCE_DEPLOY_CLOUD_URL = prevCloudA;
+    if (prevCloudB === undefined) delete process.env.WORKFORCE_CLOUD_URL;
+    else process.env.WORKFORCE_CLOUD_URL = prevCloudB;
     restoreIsolate();
   };
 }
🤖 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/cli/src/destroy-command.test.ts` around lines 75 - 94, The cleanup
in withTokenEnv restores WORKFORCE_DEPLOY_CLOUD_URL and WORKFORCE_CLOUD_URL only
when prevCloudA/prevCloudB were defined, causing leaked env state if tests set
them to undefined; update the cleanup to mirror the token/ws logic: if
prevCloudA was undefined then delete process.env.WORKFORCE_DEPLOY_CLOUD_URL else
restore it to prevCloudA, and likewise for prevCloudB/WORKFORCE_CLOUD_URL,
keeping the call to restoreIsolate() and referencing the existing withTokenEnv
and isolateAuthFiles helpers.
🤖 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.

Outside diff comments:
In `@packages/cli/src/destroy-command.test.ts`:
- Around line 75-94: The cleanup in withTokenEnv restores
WORKFORCE_DEPLOY_CLOUD_URL and WORKFORCE_CLOUD_URL only when
prevCloudA/prevCloudB were defined, causing leaked env state if tests set them
to undefined; update the cleanup to mirror the token/ws logic: if prevCloudA was
undefined then delete process.env.WORKFORCE_DEPLOY_CLOUD_URL else restore it to
prevCloudA, and likewise for prevCloudB/WORKFORCE_CLOUD_URL, keeping the call to
restoreIsolate() and referencing the existing withTokenEnv and isolateAuthFiles
helpers.

In `@packages/deploy/src/deploy.ts`:
- Line 234: The launcher input is currently using opts.cloudUrl which bypasses
the previously resolved/normalized cloud URL computed earlier; update the spread
at ...(opts.cloudUrl ? { cloudUrl: opts.cloudUrl } : {}) to use the resolved
variable produced in the earlier resolution logic (the canonical
cloudUrl/resolvedCloudUrl variable set around lines 154–157) so the launcher
receives the same canonical endpoint (preserve the conditional/fallback behavior
when the resolved value is undefined).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9f99e98f-9c70-49da-b935-5c85d95aa76a

📥 Commits

Reviewing files that changed from the base of the PR and between 9a859fd and 1578779.

📒 Files selected for processing (11)
  • packages/cli/src/destroy-command.test.ts
  • packages/cli/src/destroy-command.ts
  • packages/cli/src/list-command.ts
  • packages/deploy/src/cloud-url.test.ts
  • packages/deploy/src/cloud-url.ts
  • packages/deploy/src/deploy.test.ts
  • packages/deploy/src/deploy.ts
  • packages/deploy/src/error-format.test.ts
  • packages/deploy/src/error-format.ts
  • packages/deploy/src/index.ts
  • packages/deploy/src/login.ts

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment on lines +751 to +755
await withWorkspaceEnv({ workspace: undefined, token: undefined }, async () => {
// Point active-workspace file at a definitely-missing path so the test
// doesn't accidentally pick up the host user's `~/.agentworkforce/active.json`.
const previousActiveFile = process.env.WORKFORCE_ACTIVE_WORKSPACE_FILE;
process.env.WORKFORCE_ACTIVE_WORKSPACE_FILE = path.join(os.tmpdir(), 'wf-deploy-test-missing-active.json');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Deploy test missing auth isolation, allowing host credentials to leak in

The new test "deploy: clear error when nothing resolves and noPrompt is set" only isolates WORKFORCE_ACTIVE_WORKSPACE_FILE (line 755) but does NOT set WORKFORCE_DISABLE_SHARED_AUTH or WORKFORCE_LOGIN_FILE to block the other filesystem-backed auth sources. Compare this with the destroy test's isolateAuthFiles() helper (packages/cli/src/destroy-command.test.ts:103-117) which correctly sets all three.

Without full isolation, resolveWorkspaceToken can still resolve credentials through:

  • Tier 2: readSharedAuthForBearer() reads ~/.agent-relay/cloud-auth.json — if the developer has a valid accessToken there, Tier 2 can succeed (though it also needs a workspace, which is blocked by the missing active file, so this particular path is partially mitigated).
  • Tier 3: loadWorkspaceTokenreadWorkspaceTokenFromCloudAuth reads workspace tokens from ~/.agent-relay/cloud-auth.json, and readLoginFile reads ~/.agentworkforce/login.json. If either returns a valid token+workspace, deploy() succeeds instead of rejecting, and the assert.rejects assertion fails.

On a developer machine with existing workforce credentials, this test will fail.

Suggested change
await withWorkspaceEnv({ workspace: undefined, token: undefined }, async () => {
// Point active-workspace file at a definitely-missing path so the test
// doesn't accidentally pick up the host user's `~/.agentworkforce/active.json`.
const previousActiveFile = process.env.WORKFORCE_ACTIVE_WORKSPACE_FILE;
process.env.WORKFORCE_ACTIVE_WORKSPACE_FILE = path.join(os.tmpdir(), 'wf-deploy-test-missing-active.json');
await withWorkspaceEnv({ workspace: undefined, token: undefined }, async () => {
// Point active-workspace file at a definitely-missing path so the test
// doesn't accidentally pick up the host user's `~/.agentworkforce/active.json`.
const previousActiveFile = process.env.WORKFORCE_ACTIVE_WORKSPACE_FILE;
const previousLoginFile = process.env.WORKFORCE_LOGIN_FILE;
const previousDisableShared = process.env.WORKFORCE_DISABLE_SHARED_AUTH;
process.env.WORKFORCE_ACTIVE_WORKSPACE_FILE = path.join(os.tmpdir(), 'wf-deploy-test-missing-active.json');
process.env.WORKFORCE_LOGIN_FILE = path.join(os.tmpdir(), 'wf-deploy-test-missing-login.json');
process.env.WORKFORCE_DISABLE_SHARED_AUTH = '1';
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@khaliqgant khaliqgant merged commit a88cc80 into main May 13, 2026
2 checks passed
@khaliqgant khaliqgant deleted the fix-cli/deploy-active-json-and-list-destroy-url branch May 13, 2026 19:00
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