Compare commits

...

10 Commits

Author SHA1 Message Date
technical-writer 9976ad081b docs: fix remote-workspaces-faq CLI commands and update staging doc
Secret scan / Scan diff for credential-shaped strings (pull_request) Failing after 2s
sop-tier-check / tier-check (pull_request) Failing after 1s
audit-force-merge / audit (pull_request) Has been skipped
[technical-writer-agent]

- remote-workspaces-faq.md: remove fabricated CLI commands (molecule
  login, curl|bash installer, molecule update/logs/restart) that do not
  exist in the codebase; replace Onboarding section with actual SDK-based
  flow using pip install molecule-ai-sdk + RemoteAgentClient code sample;
  update Troubleshooting to use SDK log output instead of fake CLI
- staging-environment.md: update status from "Planned" to "In Progress";
  document CI image pipeline (staging-<sha>, staging-latest) as live;
  note Railway/Neon/Vercel items are tracked in molecule-controlplane
2026-05-10 12:07:05 +00:00
core-lead 08a929c740 Merge pull request 'test(canvas): structural tests for TIER_CONFIG and COMM_TYPE_LABELS' (#245) from test/canvas-design-tokens-config into main
publish-workspace-server-image / build-and-push (push) Failing after 9s
Secret scan / Scan diff for credential-shaped strings (push) Has been cancelled
2026-05-10 05:58:28 +00:00
Molecule AI Core Platform Lead 64c7af2968 trigger
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 5s
sop-tier-check / tier-check (pull_request) Successful in 5s
audit-force-merge / audit (pull_request) Successful in 4s
2026-05-10 05:58:09 +00:00
core-fe 814c7cc460 test(canvas): add structural tests for TIER_CONFIG and COMM_TYPE_LABELS
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 4s
sop-tier-check / tier-check (pull_request) Failing after 4s
Both are data constants exported from design-tokens.ts — TIER_CONFIG
maps tier levels 1-4 to label/color/border CSS classes, and
COMM_TYPE_LABELS maps a2a_send/a2a_receive/task_update to display
labels. No logic to test; structural shape coverage.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 05:51:40 +00:00
core-lead 2b1c51d837 Merge pull request 'feat(canvas): document all keyboard shortcuts in Toolbar help dialog' (#244) from feat/canvas-keyboard-shortcuts-help into main
Secret scan / Scan diff for credential-shaped strings (push) Successful in 4s
publish-workspace-server-image / build-and-push (push) Failing after 9s
2026-05-10 05:33:52 +00:00
Molecule AI Core Platform Lead 5327866847 trigger
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 4s
sop-tier-check / tier-check (pull_request) Successful in 4s
audit-force-merge / audit (pull_request) Successful in 4s
2026-05-10 05:33:34 +00:00
core-fe 3c934dfce0 feat(canvas): document all keyboard shortcuts and interactions in the help dialog
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 4s
sop-tier-check / tier-check (pull_request) Failing after 4s
Issue: MEDIUM priority from canvas accessibility audit (2026-05-09).
The existing Quick Start help dialog in Toolbar omitted most keyboard shortcuts
from useKeyboardShortcuts.ts — users couldn't discover them visually.

Changes:
- Toolbar.tsx: enhance the help dialog (role="dialog") to include all
  documented shortcuts: Esc, Enter, Shift+Enter, Cmd+], Cmd+[, Z, plus
  mouse interaction tips for Palette, Right-click, Dbl-click, Shift+click.
  Renamed from "Quick start" to "Shortcuts & tips".
- canvas-audit-items.md: update Keyboard Shortcuts section from PARTIAL
  to complete; mark help dialog item as done.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 05:26:06 +00:00
claude-ceo-assistant 6153d47d8f Merge pull request 'test(canvas): add cssVar unit tests for ColorToken → CSS variable mapping' (#239) from test/canvas-cssvar-tests into main
Secret scan / Scan diff for credential-shaped strings (push) Successful in 4s
publish-workspace-server-image / build-and-push (push) Failing after 9s
2026-05-10 05:23:13 +00:00
claude-ceo-assistant 71abd72e70 Merge pull request 'fix(sop-tier-check): clause splitter strips newlines — every tier:low PR fails (#229)' (#243) from fix/internal-229-sop-tier-check-tier-low-relaxation into main
Secret scan / Scan diff for credential-shaped strings (push) Has been cancelled
2026-05-10 05:23:11 +00:00
dev-lead b75187d11c fix(sop-tier-check): clause splitter strips newlines, OR-set collapses to one token (#229)
sop-tier-check / tier-check (pull_request) Failing after 5s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 5s
audit-force-merge / audit (pull_request) Successful in 4s
PR #225 introduced the AND-composition clause evaluator. PR #231
patched the per-team case-pattern matching but did NOT fix the
underlying clause-splitter bug. This PR fixes the actual root cause
behind issue #229.

Root cause (.gitea/scripts/sop-tier-check.sh ~line 289):

    _clause=$(echo "$_raw_clause" \
      | tr -d '()' \
      | tr ',' '\n' \
      | tr -d '[:space:]' \
      | grep -v '^$')

`tr -d '[:space:]'` strips the newlines that `tr ',' '\n'` just
inserted. For tier:low (expression "engineers,managers,ceo") the
intermediate value is:

    engineers\nmanagers\nceo

then `tr -d '[:space:]'` flattens it to:

    engineersmanagersceo

The for-loop iterates ONCE over this single bogus token. The case
pattern `*engineersmanagersceo*` never matches APPROVER_TEAMS values
like " managers ", so EVERY tier:low PR fails:

    ::error::clause [engineers/managers/ceo]: FAIL — no approving
    reviewer belongs to any of these teamsengineersmanagersceo
    ::error::sop-tier-check FAILED for tier:low

(Note: the missing separators in the error string `teamsengineersmanagersceo`
were a SECOND, masked bug — `_clause_names="${_clause_names:+, }${_t}"`
overwrites the variable on every iteration instead of appending. With
the splitter bug, the inner loop only ran once so the overwrite was
invisible. Fixing the splitter unmasks the accumulator bug, so we fix
both atomically.)

Fix:

  _no_parens=${_raw_clause//[()]/}
  _clause=${_no_parens//,/ }   # comma -> space, bash word-split iterates

  # Append, don't overwrite:
  _clause_names="${_clause_names}${_clause_names:+, }${_t}"
  _passed_clauses="${_passed_clauses}${_passed_clauses:+, }$_label"
  _failed_clauses="${_failed_clauses}${_failed_clauses:+, }$_label"

Per-tier policy is UNCHANGED — this is a parser fix, not a policy
relaxation:

  tier:low    — engineers,managers,ceo   (OR-set, ANY ONE suffices)
  tier:medium — managers AND engineers AND qa???,security???
  tier:high   — ceo

Test: .gitea/scripts/tests/test_sop_tier_check_clause_split.sh
asserts the splitter, accumulators, and end-to-end OR-gate matching
against APPROVER_TEAMS=" managers " (the exact shape PRs #233-238 hit).
7/7 pass on the new logic.

Refs: #229, supersedes attempted fix in #231 for the same root cause.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 22:03:12 -07:00
7 changed files with 287 additions and 47 deletions
+21 -6
View File
@@ -285,12 +285,26 @@ _passed_clauses=""
_failed_clauses=""
for _raw_clause in $EXPR; do
# Normalise: strip parens, split on comma, trim whitespace.
_clause=$(echo "$_raw_clause" | tr -d '()' | tr ',' '\n' | tr -d '[:space:]' | grep -v '^$')
# Normalise: strip parens, replace commas with spaces so bash word-split
# can iterate the OR-set members. The previous form
# _clause=$(echo ... | tr ',' '\n' | tr -d '[:space:]' | grep -v '^$')
# collapsed every member into one concatenated token because
# `tr -d '[:space:]'` strips the very newlines that just separated them
# ("engineers,managers,ceo" -> "engineersmanagersceo"), so the OR-clause
# only ever evaluated as a single nonsense team name and never matched
# APPROVER_TEAMS. Fixed in #229: leave the comma-separated members as
# space-separated tokens for `for _t in $_clause`.
_no_parens=${_raw_clause//[()]/}
_clause=${_no_parens//,/ }
_clause_passed="no"
_clause_names=""
for _t in $_clause; do
_clause_names="${_clause_names:+, }${_t}"
# Append (don't overwrite) team name to the human-readable accumulator.
# The previous form `_clause_names="${_clause_names:+, }${_t}"`
# rewrote the variable on every iteration, so the FAIL message only
# ever showed the LAST team. Fixed: prepend prior value before the
# comma-separator, then append the new team name.
_clause_names="${_clause_names}${_clause_names:+, }${_t}"
# Skip teams not yet in Gitea (qa??? / security??? placeholders).
[[ "$_t" == *"???" ]] && debug "clause \"$_t\": skipped (team pending creation)" && continue
[ -z "${TEAM_ID[$_t]:-}" ] && debug "clause \"$_t\": no ID resolved, skipping" && continue
@@ -311,11 +325,12 @@ for _raw_clause in $EXPR; do
_label=$(echo "$_raw_clause" | tr -d '()' | tr ',' '/' | tr -d '[:space:]' | sed 's/???//g')
if [ "$_clause_passed" = "yes" ]; then
_passed_clauses="${_passed_clauses:+, }$_label"
# Append (don't overwrite) — same accumulator bug as _clause_names above.
_passed_clauses="${_passed_clauses}${_passed_clauses:+, }$_label"
echo "::notice::clause [$_label]: PASS — satisfied by approving reviewer(s)"
else
_failed_clauses="${_failed_clauses:+, }$_label"
echo "::error::clause [$_label]: FAIL — no approving reviewer belongs to any of these teams${_clause_names}. Set SOP_DEBUG=1 to see per-team probe results."
_failed_clauses="${_failed_clauses}${_failed_clauses:+, }$_label"
echo "::error::clause [$_label]: FAIL — no approving reviewer belongs to any of these teams (${_clause_names}). Set SOP_DEBUG=1 to see per-team probe results."
fi
done
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env bash
# Regression test for #229 — sop-tier-check tier:low OR-clause splitter.
#
# Bug (PR #225 → still broken after PR #231):
# Line ~289 of sop-tier-check.sh used:
# _clause=$(echo "$_raw_clause" | tr -d '()' | tr ',' '\n' | tr -d '[:space:]' | grep -v '^$')
# `tr -d '[:space:]'` strips the newlines that `tr ',' '\n'` just
# inserted, collapsing "engineers,managers,ceo" into a single token
# "engineersmanagersceo". The for-loop then iterates ONCE on a name
# that matches no team, so every tier:low PR fails:
# ::error::clause [engineers/managers/ceo]: FAIL — no approving
# reviewer belongs to any of these teamsengineersmanagersceo
# (note also: missing separators in the error string is bug #2 —
# `_clause_names` used "${var:+, }$x" which OVERWRITES per iteration).
#
# Fix shape (this PR):
# _no_parens=${_raw_clause//[()]/}
# _clause=${_no_parens//,/ } # comma -> space, bash word-split iterates
# _clause_names="${_clause_names}${_clause_names:+, }${_t}" # APPEND, not overwrite
#
# This test extracts the splitter logic and asserts it produces the right
# token list for each of the three tier expressions live in the script.
set -euo pipefail
PASS=0
FAIL=0
assert_eq() {
local label="$1"
local expected="$2"
local got="$3"
if [ "$expected" = "$got" ]; then
echo " PASS $label"
PASS=$((PASS + 1))
else
echo " FAIL $label"
echo " expected: <$expected>"
echo " got: <$got>"
FAIL=$((FAIL + 1))
fi
}
# ----- Splitter under test (mirrors the fixed sop-tier-check.sh block) -----
split_clause() {
local raw="$1"
local no_parens=${raw//[()]/}
local clause=${no_parens//,/ }
local out=""
for _t in $clause; do
out="${out}${out:+|}$_t"
done
echo "$out"
}
echo "test: tier:low OR-clause splits to 3 tokens"
assert_eq "tier:low" "engineers|managers|ceo" "$(split_clause "engineers,managers,ceo")"
echo "test: tier:medium AND-expression — bash word-split on \$EXPR yields 5 tokens"
EXPR="managers AND engineers AND qa???,security???"
out=""
for _raw in $EXPR; do
out="${out}${out:+ ; }$(split_clause "$_raw")"
done
assert_eq "tier:medium" "managers ; AND ; engineers ; AND ; qa???|security???" "$out"
echo "test: tier:high single-team OR-clause"
assert_eq "tier:high" "ceo" "$(split_clause "ceo")"
echo "test: paren-wrapped OR-set unwraps + splits"
assert_eq "paren OR" "managers|ceo" "$(split_clause "(managers,ceo)")"
# ----- _clause_names accumulator (was overwriting per iteration) -----
acc=""
for t in engineers managers ceo; do
acc="${acc}${acc:+, }${t}"
done
assert_eq "_clause_names append" "engineers, managers, ceo" "$acc"
# ----- _failed_clauses / _passed_clauses accumulator across raw clauses -----
acc=""
for c in clauseA clauseB clauseC; do
acc="${acc}${acc:+, }${c}"
done
assert_eq "_failed_clauses append" "clauseA, clauseB, clauseC" "$acc"
# ----- End-to-end OR-gate: simulate APPROVER_TEAMS[core-lead]=' managers ' -----
# The script's case pattern is *${_t}* with a space-padded value.
APPROVER_TEAMS_VAL=" managers "
matched=""
for _t in $(split_clause "engineers,managers,ceo" | tr '|' ' '); do
case "$APPROVER_TEAMS_VAL" in
*${_t}*) matched="$_t"; break ;;
esac
done
assert_eq "OR-gate matches managers" "managers" "$matched"
echo
echo "------"
echo "PASS=$PASS FAIL=$FAIL"
[ "$FAIL" -eq 0 ]
+19 -8
View File
@@ -317,7 +317,7 @@ export function Toolbar() {
onClick={() => setHelpOpen((open) => !open)}
className="flex items-center justify-center w-7 h-7 bg-surface-card hover:bg-surface-card/70 border border-line rounded-lg transition-colors text-ink-mid hover:text-ink focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
aria-expanded={helpOpen}
aria-label="Open quick help"
aria-label="Open shortcuts and tips"
title="Help — shortcuts & quick start"
>
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true">
@@ -327,24 +327,35 @@ export function Toolbar() {
</button>
{helpOpen && (
<div className="absolute right-0 top-full mt-2 w-72 rounded-xl border border-line/60 bg-surface/95 p-3 shadow-2xl shadow-black/50 backdrop-blur-md">
<div className="mb-2 flex items-center justify-between">
<span className="text-[10px] font-semibold uppercase tracking-[0.24em] text-ink-mid">Quick start</span>
<div
role="dialog"
aria-label="Shortcuts and tips"
aria-modal="false"
className="absolute right-0 top-full mt-2 w-80 rounded-xl border border-line/60 bg-surface/95 p-3 shadow-2xl shadow-black/50 backdrop-blur-md z-50"
>
<div className="mb-3 flex items-center justify-between">
<span className="text-[10px] font-semibold uppercase tracking-[0.24em] text-ink-mid">Shortcuts & tips</span>
<button
type="button"
onClick={() => setHelpOpen(false)}
aria-label="Close help dialog"
className="text-[10px] text-ink-mid hover:text-ink transition-colors focus:outline-none focus-visible:underline"
>
Close
</button>
</div>
<div className="space-y-2">
<div className="space-y-1.5">
<HelpRow shortcut="⌘K" text="Search workspaces and jump straight into Details or Chat." />
<HelpRow shortcut="Esc" text="Clear selection, close menus, dismiss dialogs." />
<HelpRow shortcut="Enter" text="Zoom into selected team and select its first child node." />
<HelpRow shortcut="Shift+Enter" text="Select the parent of the selected node." />
<HelpRow shortcut="⌘]" text="Bring selected node forward in the z-order." />
<HelpRow shortcut="⌘[" text="Send selected node backward in the z-order." />
<HelpRow shortcut="Z" text="Zoom canvas to fit a team node and all its sub-workspaces." />
<HelpRow shortcut="Palette" text="Open the template palette to deploy a new workspace." />
<HelpRow shortcut="Right-click" text="Use node actions for duplicate, export, restart, or delete." />
<HelpRow shortcut="Chat" text="If a task is still running, the chat tab resumes that session automatically." />
<HelpRow shortcut="Config" text="Use the Config tab for skills, model, secrets, and runtime settings." />
<HelpRow shortcut="Dbl-click / Z" text="Zoom canvas to fit a team node and all its sub-workspaces." />
<HelpRow shortcut="Dbl-click" text="On a team node: expand and zoom to show all sub-workspaces." />
<HelpRow shortcut="Shift+click" text="Multi-select: add or remove a node from the batch selection." />
</div>
{/* Link to the full keyboard shortcuts dialog */}
<button
@@ -4,7 +4,7 @@
* CSS tailwind class used on the status indicator dot.
*/
import { describe, it, expect } from "vitest";
import { statusDotClass } from "../design-tokens";
import { statusDotClass, TIER_CONFIG, COMM_TYPE_LABELS } from "../design-tokens";
describe("statusDotClass", () => {
it('returns "bg-emerald-400" for "online"', () => {
@@ -50,3 +50,57 @@ describe("statusDotClass", () => {
}
});
});
// ── TIER_CONFIG ────────────────────────────────────────────────────────────────
describe("TIER_CONFIG", () => {
it("has entries for all four tier levels", () => {
expect(TIER_CONFIG).toHaveProperty(1);
expect(TIER_CONFIG).toHaveProperty(2);
expect(TIER_CONFIG).toHaveProperty(3);
expect(TIER_CONFIG).toHaveProperty(4);
});
it("each tier has label, color, and border fields", () => {
for (const tier of [1, 2, 3, 4]) {
expect(TIER_CONFIG[tier]).toHaveProperty("label");
expect(TIER_CONFIG[tier]).toHaveProperty("color");
expect(TIER_CONFIG[tier]).toHaveProperty("border");
}
});
it("tier labels match expected values", () => {
expect(TIER_CONFIG[1].label).toBe("T1");
expect(TIER_CONFIG[2].label).toBe("T2");
expect(TIER_CONFIG[3].label).toBe("T3");
expect(TIER_CONFIG[4].label).toBe("T4");
});
it("is immutable at runtime — same key always returns same shape", () => {
const result = TIER_CONFIG[2];
expect(TIER_CONFIG[2]).toBe(result);
});
});
// ── COMM_TYPE_LABELS ────────────────────────────────────────────────────────
describe("COMM_TYPE_LABELS", () => {
it("has labels for all known communication types", () => {
expect(COMM_TYPE_LABELS).toHaveProperty("a2a_send");
expect(COMM_TYPE_LABELS).toHaveProperty("a2a_receive");
expect(COMM_TYPE_LABELS).toHaveProperty("task_update");
});
it("labels are non-empty strings", () => {
for (const key of Object.keys(COMM_TYPE_LABELS)) {
expect(typeof COMM_TYPE_LABELS[key]).toBe("string");
expect(COMM_TYPE_LABELS[key].length).toBeGreaterThan(0);
}
});
it("is a static map — same key always returns same label", () => {
expect(COMM_TYPE_LABELS["a2a_send"]).toBe("sent");
expect(COMM_TYPE_LABELS["a2a_receive"]).toBe("received");
expect(COMM_TYPE_LABELS["task_update"]).toBe("task update");
});
});
+33 -8
View File
@@ -1,10 +1,12 @@
# Staging Environment Design
> **Status:** Planned — gates all future infra changes (Tunnel migration,
> security fixes, etc.)
> **Status:** In Progress — Phase 36. Partially implemented. The image pipeline
> (`:staging-<sha>`, `:staging-latest` tags on ECR) is live. Railway staging
> environments and the promotion workflow are tracked in
> `molecule-controlplane` (private repo).
>
> **Problem:** We merge directly to main and auto-deploy to production.
> Today's session broke CI twice and caused hours of Cloudflare edge cache
> The 2026-04-17 session broke CI twice and caused hours of Cloudflare edge cache
> issues because there was no staging to test infra changes first.
>
> **Goal:** Full staging environment that mirrors production. Every change
@@ -53,6 +55,28 @@ Developer pushes to PR branch
## Components
### 0. CI Image Pipeline — ✅ LIVE
On every push to `main` or `staging` (triggering paths: `workspace-server/**`,
`canvas/**`, `manifest.json`, `scripts/**`), the Gitea Actions workflow
(`.gitea/workflows/publish-workspace-server-image.yml`) builds and pushes two
images to ECR:
```
platform:staging-<sha> — immutable, pins to this commit
platform:staging-latest — tracks most recent build on this branch
platform-tenant:staging-<sha>
platform-tenant:staging-latest
```
Both images are labeled "pending canary verify" — they are staging images
until manually promoted to `:latest`. See the workflow file for the full
pre-clone step (manifest deps → `.tenant-bundle-deps/`), ECR auth, and build
args.
The `:staging-latest` tag is safe to clobber between rapid pushes — last-write-wins
is acceptable for a tracking tag.
### 1. Railway: two environments
Railway supports multiple environments per project. Create a `staging`
@@ -195,15 +219,16 @@ Until the automated workflow is built:
## Implementation order
1. **Railway staging environment** — create + configure vars (~30 min)
2. **Neon staging branch** — create from main (~5 min)
3. **Staging DNS**`staging.api.moleculesai.app` CNAME to Railway (~5 min)
4. **Publish workflow** — push `:staging` tag instead of `:latest` (~15 min)
1. **Publish workflow** — ✅ DONE. `.gitea/workflows/publish-workspace-server-image.yml`
pushes `:staging-<sha>` + `:staging-latest` on every `main`/`staging` push.
2. **Railway staging environment** — in `molecule-controlplane` (private)
3. **Neon staging branch** — in `molecule-controlplane` (private)
4. **Staging DNS**`staging.api.moleculesai.app` CNAME to Railway (~5 min)
5. **Promotion workflow** — manual trigger to promote staging → production (~30 min)
6. **Vercel staging** — configure preview deployment URL (~15 min)
7. **Staging smoke test** — automated test after staging deploy (~30 min)
**Total:** ~2.5 hours for full staging pipeline.
**Done in public repo:** items 1. **Remaining:** items 27 (tracked in `molecule-controlplane`).
## Cost
+1
View File
@@ -88,6 +88,7 @@ PR: `fix/ink-soft-wcag-contrast`.
- Arrow keys move selected node 10px (50px with Shift) — keyboard node drag (PR #182) ✅
- `Cmd/Ctrl+Arrow` resize selected node (↑↓ height, ←→ width, 10px, Shift 2px) ✅
- Hierarchy navigation (Enter/Shift+Enter), z-order (Cmd+]/[), zoom-to-team (Z) ✅
- Toolbar help dialog ("Shortcuts & tips") documents all shortcuts + mouse interactions ✅
### Focus Management ✅ (strong)
- Skip link → `#canvas-main` ✅
+57 -24
View File
@@ -1,7 +1,7 @@
# Phase 30 Remote Workspaces — Customer FAQ
> **Cycle:** Marketing work cycle — offline content prep
> **Status:** Draft — needs review from Marketing Lead and Doc Specialist before publishing
> **Status:** Live — updated 2026-05-10 to reflect actual onboarding path
Top customer and sales-engineer questions about Phase 30 Remote Workspaces, answered in a format ready to drop into the docs site or adapt for the support team.
@@ -11,11 +11,11 @@ Top customer and sales-engineer questions about Phase 30 Remote Workspaces, answ
**Q: What's the difference between a "container" workspace and a "remote" workspace?**
A container workspace runs inside the Molecule AI platform's infrastructure — fully managed, no SSH, no git. A remote workspace runs on your own machine or VM, connected to the platform via a lightweight agent. You control the environment (OS, packages, git config, SSH keys); the platform handles orchestration, authentication, and agent coordination.
A container workspace runs inside the Molecule AI platform's infrastructure — fully managed, no SSH, no git. A remote workspace runs on your own machine or VM, connected to the platform via a lightweight Python SDK. You control the environment (OS, packages, git config, SSH keys); the platform handles orchestration, authentication, and agent coordination.
**Q: Do remote workspaces still appear in the Canvas UI?**
Yes. Remote workspaces register with the platform on startup and appear in Canvas exactly like managed workspaces — online/offline status, workspace name, current task. The platform doesn't care where the agent runs, only that it's reachable.
Yes. Remote workspaces register with the platform on startup and appear in Canvas exactly like managed workspaces — online/offline status, workspace name, current task. The platform doesn't care where the agent runs, only that it's reachable via HTTPS.
**Q: Can I run both container and remote workspaces in the same org?**
@@ -23,7 +23,7 @@ Yes — in fact that's the primary pattern. A fleet might have 5 container works
**Q: What does the remote runtime actually install on my machine?**
The agent binary (~30MB) plus a minimal bootstrap script. No root required. The agent connects to `wss://[your-org].moleculesai.app`, authenticates with your org token, and registers its A2A endpoint. That's it — no VPN, no firewall holes beyond outbound HTTPS.
The `molecule-ai-sdk` Python package (~1MB, only `requests` as a dependency). The SDK wraps all Phase 30 protocol calls. Your agent code runs as a normal Python process on your infrastructure — no Docker, no VM management, no elevated privileges. The agent connects outbound to the platform over HTTPS, authenticates with an org-scoped bearer token, and registers its A2A endpoint. That's it — no VPN, no inbound firewall holes beyond outbound HTTPS.
---
@@ -31,15 +31,15 @@ The agent binary (~30MB) plus a minimal bootstrap script. No root required. The
**Q: How does the platform authenticate a remote workspace?**
Remote workspaces authenticate with an org-scoped bearer token (not a personal token). The platform validates the token against the tenant and provisions a session-scoped credential for A2A communication. If the remote machine is revoked from the org, the token is invalidated and the workspace goes offline within one heartbeat cycle (~15s).
Remote workspaces authenticate with a workspace-scoped bearer token. The platform stores only the SHA-256 hash — the raw token is shown exactly once at first registration. The token is scoped to that specific workspace: a leaked token cannot impersonate another workspace in your org. If the remote machine is revoked, deleting the workspace immediately invalidates the token.
**Q: Can a remote workspace make outbound connections my firewall would block?**
The agent only makes outbound HTTPS/WSS connections to the platform. It does not accept inbound connections. Your firewall only needs to allow `*.moleculesai.app` outbound — same as a browser.
The SDK only makes outbound HTTPS calls to the platform. It does not accept inbound connections. Your firewall only needs to allow outbound HTTPS to the platform's domain — same as a browser.
**Q: What happens to data if the remote workspace is disconnected or the machine is wiped?**
Workspace state lives in the platform unless explicitly persisted. For remote workspaces, you can attach a Cloudflare Artifacts repo to snapshot state to disk on your own infrastructure. If the agent reconnects, it re-registers and Canvas picks up where it left off.
Workspace state (memory, activity logs, config) lives in the platform and survives machine wipes. If the agent reconnects, it re-registers and Canvas picks up where it left off. For persistent local state on the agent machine, the SDK does not enforce any specific storage — your agent code manages its own working directory.
**Q: Are remote workspaces covered by the same MCP governance controls as container workspaces?**
@@ -51,26 +51,59 @@ Yes. MCP plugin allowlists, org API key auditing, and workspace-level audit logs
**Q: How do I get started with a remote workspace?**
1. Install the agent: `curl -sSL https://get.moleculesai.app | bash`
2. Authenticate: `molecule login --org your-org`
3. Bootstrap: `molecule workspace init --name my-agent --runtime remote`
4. The workspace registers with the platform and appears in Canvas within ~10 seconds.
1. **Install the SDK:** `pip install molecule-ai-sdk`
2. **Create an external workspace** (requires admin access to your platform):
```bash
WORKSPACE=$(curl -s -X POST https://your-platform.example.com/workspaces \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"my-agent","runtime":"external","tier":2}')
WORKSPACE_ID=$(echo $WORKSPACE | jq -r '.id')
echo $WORKSPACE_ID # save this — needed by the agent
```
3. **Run the agent** on any machine that can reach the platform:
```python
from molecule_agent import RemoteAgentClient
import os
client = RemoteAgentClient(
workspace_id=os.environ["WORKSPACE_ID"],
platform_url=os.environ["PLATFORM_URL"],
agent_card={"name": "my-agent", "skills": ["research"]},
)
client.register() # issues + caches bearer token
secrets = client.pull_secrets() # fetch workspace secrets
print("Secrets:", list(secrets.keys()))
# Heartbeat loop — keeps workspace visible on Canvas
client.run_heartbeat_loop()
```
4. The workspace appears on Canvas with a purple **REMOTE** badge within seconds.
For the full protocol reference (direct HTTP, Node.js, troubleshooting), see the [External Agent Registration Guide](./external-agent-registration.md).
**Q: Can I use my existing SSH keys and git config with a remote workspace?**
Yes. The remote runtime does not virtualize or override your shell environment. SSH keys, git config, dotfiles — all persist across sessions and are available to the agent.
Yes. The remote SDK does not virtualize or override your shell environment. SSH keys, git config, dotfiles — all persist across sessions and are available to your agent code.
**Q: How do I update the remote agent when a new version ships?**
**Q: How do I update the remote agent when a new SDK version ships?**
`molecule update` — pulls the latest agent binary from the platform, does a rolling restart. Zero downtime if the agent reconnects within the heartbeat window.
```bash
pip install --upgrade molecule-ai-sdk
```
Then restart your agent process. Zero downtime if the agent reconnects within the heartbeat window (~30s).
**Q: What's the latency like for A2A coordination between a remote workspace and a container workspace?**
A2A messages route through the platform's relay, so latency is essentially internet RTT between the remote machine and the platform's edge (~2080ms depending on geography). For comparison, container workspaces on-platform have <5ms RTT. The practical difference for most coordination patterns is imperceptible.
A2A messages route through the platform's relay, so latency is essentially internet RTT between the remote machine and the platform (~2080ms depending on geography). For comparison, container workspaces on-platform have <5ms RTT. The practical difference for most coordination patterns is imperceptible.
**Q: Can I run a remote workspace on a machine that's behind NAT with no public IP?**
Yes. The agent initiates the outbound WebSocket connection to the platform — no inbound ports needed. This is the primary design reason remote workspaces use WSS rather than HTTP.
Yes. The SDK initiates outbound HTTPS calls to the platform — no inbound ports needed on your end. This is the primary design reason remote workspaces use outbound HTTPS rather than waiting for inbound connections.
---
@@ -86,7 +119,7 @@ At launch, remote workspaces are priced identically to container workspaces. Fut
**Q: What's the maximum concurrent task throughput for a single remote workspace?**
Same as a container workspace — up to 5 concurrent delegated tasks. Remote runtime adds no throughput cap.
Same as a container workspace — up to 5 concurrent delegated tasks. The remote SDK adds no throughput cap.
---
@@ -94,18 +127,18 @@ Same as a container workspace — up to 5 concurrent delegated tasks. Remote run
**Q: Remote workspace shows offline in Canvas but the process is running on my machine.**
1. Check the agent log: `molecule logs --workspace my-agent`
2. Confirm the machine has outbound internet access: `curl -s https://[your-org].moleculesai.app/health`
3. Check token validity: `molecule auth status` — re-authenticate if expired
4. Restart the agent: `molecule restart --workspace my-agent`
1. Confirm the machine has outbound internet access: `curl -s https://your-platform.example.com/health`
2. Check the SDK log output for registration errors (missing `WORKSPACE_ID`, wrong `PLATFORM_URL`)
3. Verify the bearer token is valid — re-register with `client.register()` to confirm
4. Check network path: `curl -v -X POST https://your-platform.example.com/registry/heartbeat` with the token
**Q: A2A messages to my remote workspace are timing out.**
Remote workspaces must maintain the outbound WebSocket connection. If the machine sleeps or loses connectivity, the connection drops and A2A messages queue for up to 5 minutes before failing. The agent will re-register on reconnect — Canvas will show it back online.
The agent must call `/registry/heartbeat` every 30 seconds to stay online. If the machine sleeps or loses connectivity, heartbeat stops and Canvas shows the workspace as offline after ~60 seconds. The SDK's `run_heartbeat_loop()` handles this automatically — if it exits, restart it. On reconnect, the agent re-registers and Canvas returns to online.
**Q: My remote workspace is online but can't reach internal APIs.**
The remote runtime does not inherit VPN credentials from the machine by default. If internal APIs require VPN, you'll need to either configure the VPN on the host machine outside the agent, or use the platform's `/cp/*` reverse proxy for same-origin access (same-origin-canvas-fetches.md).
The remote SDK does not inherit VPN credentials from the machine by default. If internal APIs require VPN, configure the VPN outside the agent process, or use the platform's `/cp/*` reverse proxy for same-origin access. See [same-origin-canvas-fetches](./same-origin-canvas-fetches.md) for details.
---
@@ -121,4 +154,4 @@ Modal and Railway are inference platforms — they run your code on their infras
---
*Needs review from: Marketing Lead (voice + accuracy), Doc Specialist (technical accuracy), possibly Support for the troubleshooting section.*
*Technical accuracy review: Technical Writer — 2026-05-10. Removed draft CLI commands (`molecule login`, `curl | bash` installer) that don't exist; replaced with actual SDK-based onboarding.*