mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
server-ts
128
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7cbec20359 | fix: restore static resource mutation origin checks | ||
|
|
32fa0c23bb |
feat(daemon): Critique Theater Phase 6.2 (artifact extraction + endpoint) (#1085)
The orchestrator was leaving artifactPath = null on every shipped run because the SHIP <ARTIFACT> body never made it past the parser. Reviewers caught this on PR #1006: a rerun-style endpoint built on top of that null could not return a usable prior-art reference, and tests that synthesized artifactPath via insertCritiqueRun were hiding the gap rather than covering the feature. This PR closes that gap. The parser now hands the orchestrator a ShipArtifactPayload (round, mime, body) through a side-channel callback, and the orchestrator writes the bytes to <artifactsDir>/<projectId>/<runId>/ artifact.<ext> via a new artifact-writer module. The row's artifactPath is the absolute on-disk path. The web layer never sees that path: it fetches the bytes through GET /api/projects/:projectId/critique/:runId/artifact, which the new artifact-handler module serves with a mime-derived Content-Type, X-Content-Type-Options: nosniff, a CSP header for HTML and SVG, and the same cross-project leak guard pattern the interrupt handler uses. The body and mime intentionally never travel on the SSE wire. The SHIP PanelEvent (which doubles as the SSE payload shape) keeps its lightweight artifactRef, and the orchestrator strips body/mime before bus.emit, so a multi-megabyte artifact does not broadcast to every subscriber. The new orchestrator test asserts this explicitly. Defense in depth in the writer + handler: - mime allowlist with text/html, text/css, text/markdown, text/plain, application/json, image/svg+xml; everything else falls through to application/octet-stream + .bin so unknown payloads can't be misinterpreted as a known type; - UTF-8 byte-length cap, configurable via cfg.parserMaxBlockBytes, so multi-byte payloads can't sneak past a JS .length check; - atomic write through a sibling tmp file + rename so a daemon crash mid-write can't leave a half-written artifact under the canonical name; - path-traversal guard on the GET endpoint that resolves the row's artifactPath against the artifacts root and refuses anything that escapes it, refuses non-regular files (symlinks, dirs), and refuses files larger than the response cap. Folded in two non-blocking notes lefarcen left on PR #1016 (the contracts move) since persistence.ts was already in scope here: - P2: introduced CritiquePersistedStatus = CritiqueRunStatus | 'running' in the contracts package. CritiqueRunRow.status and CritiqueRunInsert. status now use it, and the inline `as CritiqueRunStatus | 'running'` widen in interrupt-handler.ts is gone. Public DTOs continue to use the terminal-only CritiqueRunStatus so a future endpoint can't leak a 'running' row through the wire. - P3: added AssertExhaustiveValues + a compile-time assertion that CRITIQUE_RUN_STATUSES covers every CritiqueRunStatus variant. Adding a value to ShipStatus or CritiqueRunStatus without updating the array now fails the build with a tuple naming the missing variants instead of silently dropping out of UI filters. Coverage: 174 critique tests across 14 files pass locally, including the new critique-artifact-writer (13 cases) and critique-artifact-endpoint (11 cases) suites, the inverted critique-lifecycle artifact-persistence test, and the orchestrator happy-path that asserts the SSE ship payload does NOT carry body or mime. Validated: pnpm guard, pnpm --filter @open-design/contracts build, pnpm --filter @open-design/daemon build (full tsc), pnpm --filter @open-design/web typecheck, pnpm --filter @open-design/daemon exec vitest run tests/critique (all green). This is step (b) of the four-step plan that PR #1006's closing comment laid out. Step (a) was the contracts move in PR #1016. Steps (c) (persist original_message_id / agent_id / model_id) and (d) (real rerun endpoint on top of (a)+(b)+(c)) follow. Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
bfedbeca0f |
fix(prompts): add "When NOT to emit" guardrail to artifact handoff (#1143) (#1145)
* fix(prompts): add "When NOT to emit <artifact>" clauses (#1143) #1143 现场报告:当本轮只用 Edit 工具修改已有 HTML 文件、没有写出新 canonical HTML 时,AI 仍按 system prompt 的 "non-negotiable output rule" 字面收尾,把一句中文总结塞进 `<artifact type="text/html">` 块 里。下游持久化路径会把这一句 prose 当合法 HTML 落盘,污染项目文件 面板(截图见 #50 评论)。 根因在 prompt 缺少免发条款。本次修改: - 把 "Artifact handoff (non-negotiable output rule)" 改为条件化措辞 "Artifact handoff" + "When you ship a fresh deliverable…" - Workflow step 5 ("Finish") 增加 in-place edit 不发 artifact 的分支 - 新增 "When NOT to emit `<artifact>`" 子段,明确三条免发条件: - in-place edits only:本轮无新 canonical HTML 产出 → 直接说改了 哪个文件、改了什么 - body 必须是完整 `<!doctype html>` 文档 → 总结/路径/bash 输出/ 说明用普通回复,不要包标签 - 拿不准就别发 → 重发未变 artifact 没价值,发空壳 artifact 反而 误导用户、污染面板 测试:apps/daemon/tests/prompts/system.test.ts 新增 describe 块 "artifact handoff no-emit clauses (#1143)" 4 例,断言 composed prompt 含必要短语。 #50 持久化层兜底(pre-write HTML gate)由 #1144 单独跟进,与本 PR 互补:本 PR 让 AI 不去发空壳 artifact,#1144 在写盘前再挡一道, 即使 prompt 失守也不会污染项目面板。 * fix(prompts): 让 discovery 主导层也支持 artifact 免发例外 (方案 C) review (lefarcen P1, mrcfps blocker): base 层新增的 "When NOT to emit <artifact>" 例外被更高优先级的 DISCOVERY_AND_PHILOSOPHY 层中的无条件 emit 指令盖过去,导致 #1143 主路径仍可能产出空壳 artifact。 按 review 中的方案 C 修补: - discovery.ts RULE 3 之前新增 "Artifact emission is conditional" 主导层不变式段落(条件式 emit 在主导层声明一次,base 层保持详细规则) - discovery.ts:17 arc 注释 / :143 plan 模板 step 9 / :262 default arc recap 全部改为条件式(仅在本轮写出新 canonical HTML 时 emit) - deck-framework.ts:327 deck workflow step 7 同步改为条件式 测试加 2 条断言: - 负断言:组装后 prompt 不再含未限定的 "Emit single <artifact>" / "emit a single <artifact>." 行 - 正断言:discovery 层包含 "only when this turn wrote a new canonical HTML" 与 "only edited an existing HTML file" 等价表述 * test(prompts): 补 deck-mode 负断言覆盖 deck-framework.ts:327 review (lefarcen P2): 上一轮的负断言用 composeSystemPrompt({}) 调用, 不会触发 DECK_FRAMEWORK_DIRECTIVE 的拼接(仅在 skillMode === 'deck' 或 metadata.kind === 'deck' 时追加)。如果 deck-framework.ts:327 后续回 退到 "Emit single <artifact>",无参负断言依然假绿。 补一条显式的 deck-mode 断言: - 负断言:deck-mode prompt 不含未限定的 "7. Emit single <artifact>" 行 - 正断言:含本次改的 "Emit single <artifact> if a new canonical deck HTML" |
||
|
|
9079c51ba3 |
feat(daemon): HTTP 206 range request support for video/audio files Fixes #784 (#1105)
* feat(daemon): HTTP 206 range request support for video/audio files (#784) Stream video and audio via fs.createReadStream with Accept-Ranges: bytes and 206 Partial Content responses so browsers can play and seek media inline. Non-media files keep the existing buffer path unchanged. Add parseByteRange (RFC 7233-compliant) and resolveProjectFilePath to projects.ts, and 23 unit tests covering all range edge cases. * fix(daemon): move range streaming to /raw/* route used by media viewers The inline VideoViewer and AudioViewer components fetch /api/projects/:id/raw/* (via projectRawUrl), not /files/*. Apply the HTTP 206 / Accept-Ranges streaming path to the raw route while preserving its Origin: null CORS behaviour for sandboxed iframes. Add 7 route-level HTTP tests against a real startServer() instance covering 200 full, 206 partial, suffix range, open-ended range, 416 unsatisfiable, non-media passthrough, and 404 cases. --------- Co-authored-by: mrcfps <mrc@powerformer.com> |
||
|
|
587c783dc0 |
feat(web): add Finalize design package + Continue in CLI buttons (#451) (#974)
* feat(daemon): expose resolvedDir on GET /api/projects/:id (#451 prereq)
Native projects (no metadata.baseDir) live at <projects root>/<id>, where
projects root is daemon-side state. The web client cannot reconstruct an
absolute path on its own, and shell.openPath on a relative path is
undefined behavior. Without resolvedDir, the upcoming Continue in CLI
button (#451) would render permanently disabled for native projects.
Mirrors PR #832's pattern of exposing designMdPath in its response.
Computed via the existing resolveProjectDir(...) helper. No behavior
change to existing callers; they ignore the new field.
Adds ProjectDetailResponse contract type and a focused projects-routes
test covering imported-folder, native, and unknown-id paths.
* feat(web): add parseProvenance helper for DESIGN.md staleness checks
Pure helper that extracts Project ID, design system, current artifact,
transcript message count, and generated UTC timestamp from the
`## Provenance` section emitted by the daemon's finalize synthesis
prompt (apps/daemon/src/finalize-design.ts). Used by useDesignMdState
to derive the Continue in CLI button's stale/fresh state without an
additional daemon endpoint.
Handles missing section, "none" sentinels for design system /
artifact, and malformed timestamps without throwing. Tests cover all
four branches.
* feat(web): add buildClipboardPrompt template for Continue in CLI
Inline single-source-of-truth template per #451 spec §3.4. Names the
project, the working directory, and the DESIGN.md-first operating
contract for the receiving `claude` CLI session. Trailing TODO is
the blank task slot the issue body specifies — left empty so the user
fills it in before submitting.
Also lands the shared copyToClipboard helper (jsdom-safe canonical path
+ execCommand fallback) so the new button and any future caller share
one fallback path, mirroring the inline pattern in FileViewer.tsx.
Tests cover happy-path field rendering, "none"/"unknown" sentinels
when DESIGN.md fields are absent, and both clipboard branches.
* feat(web): add useProjectDetail + useDesignMdState hooks
useProjectDetail wraps GET /api/projects/:id, surfacing the resolvedDir
field and falling back to metadata.baseDir for older daemons that don't
include it. Continue in CLI needs an absolute working directory so the
desktop bridge can openPath it; the web client never reconstructs the
path itself.
useDesignMdState fetches the project's file list, downloads DESIGN.md
when present, parses the Provenance section, and computes a stale
verdict by comparing the recorded generatedAt against the max mtime of
non-DESIGN.md files and the max conversation updatedAt. Drives the
button's three-state UI (disabled / fresh / stale) without a
daemon-side endpoint.
Tests cover happy path, fallback, and both stale branches plus the
pure computeStale helper for the null-timestamp edge case.
* feat(web): add useFinalizeProject hook with cancel + error-code mapping
Wraps POST /api/projects/:id/finalize/anthropic for the Finalize design
package button. Three concerns:
1. Lifecycle: idle → pending → success | error. Double-clicking the
button aborts the prior in-flight request before starting a new
one so the daemon never sees stacked finalize calls per project.
2. Cancellation: AbortController plumbed through fetch + a 130 s
timer (daemon timeout 120 s + 10 s buffer). Cancel returns to idle
cleanly — it's a user gesture, not an error surface.
3. Daemon error mapping: when the response is non-OK, body.error.code
drives the canonical user-facing toast string (table covers all
7 codes the daemon emits today plus a network-error catch-all).
body.error.details, when a string, surfaces alongside the category
message so account-usage-cap responses (Anthropic 400 →
UPSTREAM_UNAVAILABLE) can show the upstream's own reason instead
of just the daemon's category label — committed to lefarcen on
#450 verification reply.
Tests cover request body shape, all 8 error codes via it.each, the
network-error path, the details-surfacing branch, the cancel ⇒ idle
flow, and the unknown-code → catch-all message branch.
* feat(web): add useTerminalLaunch with electron/web detection
Capability-detected wrapper around window.electronAPI.openPath. On
desktop the bridge forwards to shell.openPath, which opens the OS
file manager at the project working directory (per Electron's
contract for directory paths — it is NOT a terminal launcher;
spawning a terminal application is deferred per #451 Non-goals). On
browser builds the hook reports web-fallback so the caller renders
a manual-instruction toast naming the working directory.
Treats any non-empty string return from shell.openPath as ok: false
so platform-specific failures surface the manual fallback toast.
Behavior is exercised end-to-end by the upcoming
ContinueInCliButton tests.
* feat(desktop): expose shell.openPath via electronAPI bridge
Adds an openPath bridge method that the Continue in CLI button (#451)
uses to surface the project working directory in the OS file manager.
shell.openPath is part of Electron's contract and resolves to '' on
success / a non-empty error string on failure; the IPC handler
forwards the result so the renderer can decide between the success
toast and the manual fallback toast without a separate error channel.
Empty / non-string inputs short-circuit to a self-describing error
string so the renderer never needs to worry about undefined-input
crashes from the main process.
Web side: extracts Window.electronAPI into a single global declaration
at apps/web/src/types/electron.d.ts so future bridge methods land in
one place. Two pre-existing inline declare-global blocks
(NewProjectPanel.tsx, providers/registry.ts) are deleted in favor of
that single source of truth — the inline ones each carried a partial
shape of the bridge and were diverging from the desktop preload.
* feat(web): add FinalizeDesignButton, ContinueInCliButton, ProjectActionsToolbar
Project-level toolbar that hosts the two new actions from #451.
Mounted between AppChromeHeader and the chat/workspace split (wiring
lands in the next commit). Per-file actions (Export PDF/PPTX/ZIP,
Deploy) stay in the FileViewer share menu.
FinalizeDesignButton has three idle labels driven by DESIGN.md
existence + staleness, plus a pending state with a spinner and a
cancel link that maps to useFinalizeProject's AbortController. Error
toasts are owned by ProjectView so the button doesn't carry its own
toast surface.
ContinueInCliButton renders disabled with a Finalize-pointing
tooltip when DESIGN.md is missing (so the workflow is discoverable
rather than hidden), enabled when fresh, and enabled with a stale
chip otherwise. Chip text is the spec's canonical "Spec is stale —
regenerate?" — N-turns-ago is deferred per spec §4.6.
Toast.tsx is a tiny transient component that mirrors
PromptTemplatePreviewModal's state-based toast pattern; supports a
secondary details line so daemon error envelopes that carry an
upstream explanation (e.g. Anthropic account-usage cap) can surface
the real reason alongside the daemon's category label.
CSS appends one block to apps/web/src/index.css mirroring the
existing app-project-title token usage; no CSS modules in this
repo (verified by grep).
* test(web): cover ContinueInCliButton states + interaction wiring
Three rendered states (DESIGN.md missing → disabled with the
Finalize-pointing tooltip; DESIGN.md fresh → enabled, no chip;
DESIGN.md stale → enabled with the canonical "Spec is stale —
regenerate?" chip), plus three onClick branches (no-op when
disabled, fires once when fresh, fires once when stale).
Click-handler integration with clipboard / shell.openPath / toast
lives in ProjectView (the button is presentational and takes the
handler in via props), so those are covered by Phase K's wiring +
the manual smoke test rather than the per-component test.
* feat(web): wire Continue in CLI + Finalize buttons into ProjectView
Mounts the new project-actions toolbar between AppChromeHeader and
the chat/workspace split, hidden when workspaceFocused so the
focus-mode artifact view stays uncluttered.
Wires the four hooks (useProjectDetail, useDesignMdState,
useFinalizeProject, useTerminalLaunch) to a single shared toast
surface. handleFinalize reads the request body from the existing
config: AppConfig prop and uses effectiveMaxTokens(config) to match
the chat-flow's maxTokens defaulting; on success it refreshes
useDesignMdState so the toolbar re-renders with the new chip state.
handleContinueInCli builds the literal clipboard prompt, copies it,
opens the working directory via shell.openPath on desktop /
falls through to a manual-instruction toast on browser, and surfaces
shell.openPath failures with a fallback toast that names the path.
Errors lift into the same toast surface (a useEffect tied to
finalize.error) so the daemon's category message + body.error.details
reach the user as the spec's two-line render — covered by hook test
16a in the prior commit.
⌘+Shift+K (mac) / Ctrl+Shift+K (others) is the keyboard
accelerator for Continue in CLI; capture-phase, platform-gated,
no-op when DESIGN.md is missing. Mirrors the existing FileWorkspace
shortcut idiom and does not collide with ⌘+P (Quick Switcher).
* fix(web): distinguish timeout abort from user cancel in useFinalizeProject
Addresses codex P2 finding on PR #974: the catch block treated every
AbortError as a user-initiated cancel and reset to idle silently. If
the internal 130 s timeout fired, users saw no failure signal but the
daemon's synthesis call may still have been in flight.
Adds a timedOutRef set inside the setTimeout callback before
controller.abort(), and branches in the catch: timeout → status
'error' with new TIMEOUT code ("Finalize timed out after 130 s. The
daemon may still be running."), user cancel → existing idle reset.
Reset the ref at the start of every trigger() so a previous timeout
doesn't poison the next call.
Adds one test using vi.useFakeTimers() that advances past 130_001 ms
and asserts the TIMEOUT error surface.
* fix(web): surface clipboard failures by rendering the prompt in the toast
Addresses codex P2 finding on PR #974: handleContinueInCli ignored
copyToClipboard's return value, so when both clipboard paths failed
(restricted browser context / insecure origin) the toast still said
"paste the prompt" though nothing had been copied — leaving users
with no manual-copy recourse in exactly the environments where the
fallback should help.
handleContinueInCli now branches on copyToClipboard's boolean return.
On failure the toast renders the prepared prompt in a scrollable
<pre> block and pins itself open (no auto-dismiss) so the user has
time to select-and-copy manually. Includes a Dismiss button + the
working directory in the secondary details line so the user has the
information needed to proceed.
The folder-open call is skipped on copy failure because there's
nothing to paste yet; the user copies first, then re-clicks Continue
in CLI when they're ready.
Toast component grows an optional Updating VS Code Server to version 41dd792b5e652393e7787322889ed5fdc58bd75b
Removing previous installation...
Installing VS Code Server for Linux x64 (41dd792b5e652393e7787322889ed5fdc58bd75b)
Downloading: 0% 0% 0% 0% 0% 0% 0% 0% 0% 0% 0% 0% 0% 0% 0% 0% 0% 1% 1% 1% 1% 1% 1% 1% 1% 1% 1% 1% 1% 1% 1% 1% 1% 1% 1% 2% 2% 2% 2% 2% 2% 2% 2% 2% 2% 2% 2% 2% 2% 2% 2% 2% 2% 3% 3% 3% 3% 3% 3% 3% 3% 3% 3% 3% 3% 3% 3% 3% 3% 3% 3% 4% 4% 4% 4% 4% 4% 4% 4% 4% 4% 4% 4% 4% 4% 4% 4% 4% 4% 5% 5% 5% 5% 5% 5% 5% 5% 5% 5% 5% 5% 5% 5% 5% 5% 5% 5% 6% 6% 6% 6% 6% 6% 6% 6% 6% 6% 6% 6% 6% 6% 6% 6% 6% 6% 7% 7% 7% 7% 7% 7% 7% 7% 7% 7% 7% 7% 7% 7% 7% 7% 7% 7% 8% 8% 8% 8% 8% 8% 8% 8% 8% 8% 8% 8% 8% 8% 8% 8% 8% 8% 9% 9% 9% 9% 9% 9% 9% 9% 9% 9% 9% 9% 9% 9% 9% 9% 9% 10% 10% 10% 10% 10% 10% 10% 10% 10% 10% 10% 10% 10% 10% 10% 10% 10% 10% 11% 11% 11% 11% 11% 11% 11% 11% 11% 11% 11% 11% 11% 11% 11% 11% 11% 11% 12% 12% 12% 12% 12% 12% 12% 12% 12% 12% 12% 12% 12% 12% 12% 12% 12% 12% 13% 13% 13% 13% 13% 13% 13% 13% 13% 13% 13% 13% 13% 13% 13% 13% 13% 13% 14% 14% 14% 14% 14% 14% 14% 14% 14% 14% 14% 14% 14% 14% 14% 14% 14% 14% 15% 15% 15% 15% 15% 15% 15% 15% 15% 15% 15% 15% 15% 15% 15% 15% 15% 15% 16% 16% 16% 16% 16% 16% 16% 16% 16% 16% 16% 16% 16% 16% 16% 16% 16% 16% 17% 17% 17% 17% 17% 17% 17% 17% 17% 17% 17% 17% 17% 17% 17% 17% 17% 17% 18% 18% 18% 18% 18% 18% 18% 18% 18% 18% 18% 18% 18% 18% 18% 18% 18% 19% 19% 19% 19% 19% 19% 19% 19% 19% 19% 19% 19% 19% 19% 19% 19% 19% 19% 20% 20% 20% 20% 20% 20% 20% 20% 20% 20% 20% 20% 20% 20% 20% 20% 20% 20% 21% 21% 21% 21% 21% 21% 21% 21% 21% 21% 21% 21% 21% 21% 21% 21% 21% 21% 22% 22% 22% 22% 22% 22% 22% 22% 22% 22% 22% 22% 22% 22% 22% 22% 22% 22% 23% 23% 23% 23% 23% 23% 23% 23% 23% 23% 23% 23% 23% 23% 23% 23% 23% 23% 24% 24% 24% 24% 24% 24% 24% 24% 24% 24% 24% 24% 24% 24% 24% 24% 24% 24% 25% 25% 25% 25% 25% 25% 25% 25% 25% 25% 25% 25% 25% 25% 25% 25% 25% 25% 26% 26% 26% 26% 26% 26% 26% 26% 26% 26% 26% 26% 26% 26% 26% 26% 26% 26% 27% 27% 27% 27% 27% 27% 27% 27% 27% 27% 27% 27% 27% 27% 27% 27% 27% 28% 28% 28% 28% 28% 28% 28% 28% 28% 28% 28% 28% 28% 28% 28% 28% 28% 28% 29% 29% 29% 29% 29% 29% 29% 29% 29% 29% 29% 29% 29% 29% 29% 29% 29% 29% 30% 30% 30% 30% 30% 30% 30% 30% 30% 30% 30% 30% 30% 30% 30% 30% 30% 30% 31% 31% 31% 31% 31% 31% 31% 31% 31% 31% 31% 31% 31% 31% 31% 31% 31% 31% 32% 32% 32% 32% 32% 32% 32% 32% 32% 32% 32% 32% 32% 32% 32% 32% 32% 32% 33% 33% 33% 33% 33% 33% 33% 33% 33% 33% 33% 33% 33% 33% 33% 33% 33% 33% 34% 34% 34% 34% 34% 34% 34% 34% 34% 34% 34% 34% 34% 34% 34% 34% 34% 34% 35% 35% 35% 35% 35% 35% 35% 35% 35% 35% 35% 35% 35% 35% 35% 35% 35% 35% 36% 36% 36% 36% 36% 36% 36% 36% 36% 36% 36% 36% 36% 36% 36% 36% 36% 37% 37% 37% 37% 37% 37% 37% 37% 37% 37% 37% 37% 37% 37% 37% 37% 37% 37% 38% 38% 38% 38% 38% 38% 38% 38% 38% 38% 38% 38% 38% 38% 38% 38% 38% 38% 39% 39% 39% 39% 39% 39% 39% 39% 39% 39% 39% 39% 39% 39% 39% 39% 39% 39% 40% 40% 40% 40% 40% 40% 40% 40% 40% 40% 40% 40% 40% 40% 40% 40% 40% 40% 41% 41% 41% 41% 41% 41% 41% 41% 41% 41% 41% 41% 41% 41% 41% 41% 41% 41% 42% 42% 42% 42% 42% 42% 42% 42% 42% 42% 42% 42% 42% 42% 42% 42% 42% 42% 43% 43% 43% 43% 43% 43% 43% 43% 43% 43% 43% 43% 43% 43% 43% 43% 43% 43% 44% 44% 44% 44% 44% 44% 44% 44% 44% 44% 44% 44% 44% 44% 44% 44% 44% 44% 45% 45% 45% 45% 45% 45% 45% 45% 45% 45% 45% 45% 45% 45% 45% 45% 45% 46% 46% 46% 46% 46% 46% 46% 46% 46% 46% 46% 46% 46% 46% 46% 46% 46% 46% 47% 47% 47% 47% 47% 47% 47% 47% 47% 47% 47% 47% 47% 47% 47% 47% 47% 47% 48% 48% 48% 48% 48% 48% 48% 48% 48% 48% 48% 48% 48% 48% 48% 48% 48% 48% 49% 49% 49% 49% 49% 49% 49% 49% 49% 49% 49% 49% 49% 49% 49% 49% 49% 49% 50% 50% 50% 50% 50% 50% 50% 50% 50% 50% 50% 50% 50% 50% 50% 50% 50% 50% 51% 51% 51% 51% 51% 51% 51% 51% 51% 51% 51% 51% 51% 51% 51% 51% 51% 51% 52% 52% 52% 52% 52% 52% 52% 52% 52% 52% 52% 52% 52% 52% 52% 52% 52% 52% 53% 53% 53% 53% 53% 53% 53% 53% 53% 53% 53% 53% 53% 53% 53% 53% 53% 53% 54% 54% 54% 54% 54% 54% 54% 54% 54% 54% 54% 54% 54% 54% 54% 54% 54% 55% 55% 55% 55% 55% 55% 55% 55% 55% 55% 55% 55% 55% 55% 55% 55% 55% 55% 56% 56% 56% 56% 56% 56% 56% 56% 56% 56% 56% 56% 56% 56% 56% 56% 56% 56% 57% 57% 57% 57% 57% 57% 57% 57% 57% 57% 57% 57% 57% 57% 57% 57% 57% 57% 58% 58% 58% 58% 58% 58% 58% 58% 58% 58% 58% 58% 58% 58% 58% 58% 58% 58% 59% 59% 59% 59% 59% 59% 59% 59% 59% 59% 59% 59% 59% 59% 59% 59% 59% 59% 60% 60% 60% 60% 60% 60% 60% 60% 60% 60% 60% 60% 60% 60% 60% 60% 60% 60% 61% 61% 61% 61% 61% 61% 61% 61% 61% 61% 61% 61% 61% 61% 61% 61% 61% 61% 62% 62% 62% 62% 62% 62% 62% 62% 62% 62% 62% 62% 62% 62% 62% 62% 62% 62% 63% 63% 63% 63% 63% 63% 63% 63% 63% 63% 63% 63% 63% 63% 63% 63% 63% 64% 64% 64% 64% 64% 64% 64% 64% 64% 64% 64% 64% 64% 64% 64% 64% 64% 64% 65% 65% 65% 65% 65% 65% 65% 65% 65% 65% 65% 65% 65% 65% 65% 65% 65% 65% 66% 66% 66% 66% 66% 66% 66% 66% 66% 66% 66% 66% 66% 66% 66% 66% 66% 66% 67% 67% 67% 67% 67% 67% 67% 67% 67% 67% 67% 67% 67% 67% 67% 67% 67% 67% 68% 68% 68% 68% 68% 68% 68% 68% 68% 68% 68% 68% 68% 68% 68% 68% 68% 68% 69% 69% 69% 69% 69% 69% 69% 69% 69% 69% 69% 69% 69% 69% 69% 69% 69% 69% 70% 70% 70% 70% 70% 70% 70% 70% 70% 70% 70% 70% 70% 70% 70% 70% 70% 70% 71% 71% 71% 71% 71% 71% 71% 71% 71% 71% 71% 71% 71% 71% 71% 71% 71% 71% 72% 72% 72% 72% 72% 72% 72% 72% 72% 72% 72% 72% 72% 72% 72% 72% 72% 73% 73% 73% 73% 73% 73% 73% 73% 73% 73% 73% 73% 73% 73% 73% 73% 73% 73% 74% 74% 74% 74% 74% 74% 74% 74% 74% 74% 74% 74% 74% 74% 74% 74% 74% 74% 75% 75% 75% 75% 75% 75% 75% 75% 75% 75% 75% 75% 75% 75% 75% 75% 75% 75% 76% 76% 76% 76% 76% 76% 76% 76% 76% 76% 76% 76% 76% 76% 76% 76% 76% 76% 77% 77% 77% 77% 77% 77% 77% 77% 77% 77% 77% 77% 77% 77% 77% 77% 77% 77% 78% 78% 78% 78% 78% 78% 78% 78% 78% 78% 78% 78% 78% 78% 78% 78% 78% 78% 79% 79% 79% 79% 79% 79% 79% 79% 79% 79% 79% 79% 79% 79% 79% 79% 79% 79% 80% 80% 80% 80% 80% 80% 80% 80% 80% 80% 80% 80% 80% 80% 80% 80% 80% 80% 81% 81% 81% 81% 81% 81% 81% 81% 81% 81% 81% 81% 81% 81% 81% 81% 81% 81% 82% 82% 82% 82% 82% 82% 82% 82% 82% 82% 82% 82% 82% 82% 82% 82% 82% 83% 83% 83% 83% 83% 83% 83% 83% 83% 83% 83% 83% 83% 83% 83% 83% 83% 83% 84% 84% 84% 84% 84% 84% 84% 84% 84% 84% 84% 84% 84% 84% 84% 84% 84% 84% 85% 85% 85% 85% 85% 85% 85% 85% 85% 85% 85% 85% 85% 85% 85% 85% 85% 85% 86% 86% 86% 86% 86% 86% 86% 86% 86% 86% 86% 86% 86% 86% 86% 86% 86% 86% 87% 87% 87% 87% 87% 87% 87% 87% 87% 87% 87% 87% 87% 87% 87% 87% 87% 87% 88% 88% 88% 88% 88% 88% 88% 88% 88% 88% 88% 88% 88% 88% 88% 88% 88% 88% 89% 89% 89% 89% 89% 89% 89% 89% 89% 89% 89% 89% 89% 89% 89% 89% 89% 89% 90% 90% 90% 90% 90% 90% 90% 90% 90% 90% 90% 90% 90% 90% 90% 90% 90% 90% 91% 91% 91% 91% 91% 91% 91% 91% 91% 91% 91% 91% 91% 91% 91% 91% 91% 92% 92% 92% 92% 92% 92% 92% 92% 92% 92% 92% 92% 92% 92% 92% 92% 92% 92% 93% 93% 93% 93% 93% 93% 93% 93% 93% 93% 93% 93% 93% 93% 93% 93% 93% 93% 94% 94% 94% 94% 94% 94% 94% 94% 94% 94% 94% 94% 94% 94% 94% 94% 94% 94% 95% 95% 95% 95% 95% 95% 95% 95% 95% 95% 95% 95% 95% 95% 95% 95% 95% 95% 96% 96% 96% 96% 96% 96% 96% 96% 96% 96% 96% 96% 96% 96% 96% 96% 96% 96% 97% 97% 97% 97% 97% 97% 97% 97% 97% 97% 97% 97% 97% 97% 97% 97% 97% 97% 98% 98% 98% 98% 98% 98% 98% 98% 98% 98% 98% 98% 98% 98% 98% 98% 98% 98% 99% 99% 99% 99% 99% 99% 99% 99% 99% 99% 99% 99% 99% 99% 99% 99% 99% 99%100%100%
Unpacking: 0% 1% 2% 3% 4% 5% 6% 7% 8% 9% 10% 11% 12% 13% 14% 15% 16% 17% 18% 19% 20% 21% 22% 23% 24% 25% 26% 27% 28% 29% 30% 31% 32% 33% 34% 35% 36% 37% 38% 39% 40% 41% 42% 43% 44% 45% 46% 47% 48% 49% 50% 51% 52% 53% 54% 55% 56% 57% 58% 59% 60% 61% 62% 63% 64% 65% 66% 67% 68% 69% 70% 71% 72% 73% 74% 75% 76% 77% 78% 79% 80% 81% 82% 83% 84% 85% 86% 87% 88% 89% 90% 91% 92% 93% 94% 95% 96% 97% 98% 99%100%
Unpacked 4009 files and folders to /home/bryan/.vscode-server/bin/41dd792b5e652393e7787322889ed5fdc58bd75b.
Looking for compatibility check script at /home/bryan/.vscode-server/bin/41dd792b5e652393e7787322889ed5fdc58bd75b/bin/helpers/check-requirements.sh
Running compatibility check script
Compatibility check successful (0) prop and the auto-dismiss
TTL is suppressed whenever code is present. CSS adds .od-toast-code
(monospace, max-height 240 with overflow-auto) and .od-toast-dismiss
styling.
Six new Toast tests cover details rendering, code rendering,
no-auto-dismiss when code is present, auto-dismiss when code is
absent, and the Dismiss button affordance.
* fix(web): make ContinueInCliButton disabled-state guidance visible
Addresses mrcfps's PR #974 review: native <button disabled> does
not fire hover/focus events in browsers we ship against, so a
`title` tooltip on the disabled button never surfaces. The only
guidance for the missing-DESIGN.md state was effectively invisible —
defeating the spec's "discoverable, not hidden" intent.
Renders the help text as a visible sibling <span> next to the
disabled button instead. Adds aria-describedby pointing the button
at the hint's id so assistive tech announces the explanation when
the disabled button gets focus. The native `disabled` attribute
stays so the button still can't be clicked or submitted.
CSS adds .project-actions-disabled-hint (muted italic, 11.5px,
matches the existing meta/secondary text style on this surface).
Test asserts the role="note" hint is in the DOM with the canonical
text and that the button's aria-describedby links to its id.
* fix(web): keep ProjectActionsToolbar at natural height inside the .app grid
The .app container was `grid-template-rows: auto 1fr` — only two
rows. Adding ProjectActionsToolbar as a third child between
AppChromeHeader and the chat/workspace split made the toolbar the
2nd grid item, so it took the `1fr` row (filling roughly half the
viewport) while the split got pushed into an implicit auto row at
its content's natural height. Surfaced as a screenshot from Bryan
showing the toolbar's background bleeding across most of the screen.
Extend grid-template-rows to `auto auto 1fr` and pin the split to
`grid-row: 3` explicitly. Now:
- Toolbar visible: row 1 = header (auto), row 2 = toolbar (auto),
row 3 = split (1fr, fills remaining viewport).
- Toolbar hidden via hidden=workspaceFocused → ProjectActionsToolbar
returns null, row 2 collapses to 0px (auto with no content), split
still fills row 3.
No JS changes; existing 609 tests still green.
* fix(web): guard useFinalizeProject state writes against superseded triggers
Addresses mrcfps's PR #974 P1 review on useFinalizeProject.ts:132
(also called out as P1.3 in lefarcen's deep-dive review).
Calling trigger() twice in quick succession aborted the first
controller and swapped abortRef to the new one, but the first
request's later AbortError catch still unconditionally called
setStatus('idle') / setError(null). That cleared the spinner and
re-enabled both toolbar buttons while the replacement finalize was
still pending — defeating the de-duplication this hook was meant to
enforce.
Adds an isCurrent() closure (`abortRef.current === controller`)
and gates every state-write site after the await: success path,
non-OK envelope path, AbortError-timeout, AbortError-cancel, and
network-error all bail early when the trigger has been superseded.
Per mrcfps: "make every state write request-scoped."
Regression test triggers twice in quick succession with a
never-resolving fetch, awaits the first promise (it rejects with
AbortError), and asserts status stays 'pending' rather than
collapsing to 'idle' under the replacement's lifetime.
* fix(desktop): allowlist-validate shell.openPath against registered project roots
Addresses mrcfps's PR #974 P1 review on runtime.ts:305 (also called
out as P1.2 in lefarcen's deep-dive review): the new
`shell:open-path` IPC handler accepted any renderer-supplied
string and forwarded it straight into Electron's `shell.openPath`,
widening the renderer→main trust boundary so XSS or a compromised
renderer dependency could open arbitrary local paths to the user.
Adds an explicit gate around the bridge:
1. validateExistingDirectory(p) — floor check that rejects empty
strings, relative paths, files, apps, and non-existent paths;
realpath-resolves so symlink games can't be used to register
one path and reach another.
2. createProjectRootGate() — Set-backed allowlist of
daemon-validated project working directories. The renderer
calls registerProjectRoot(absDir) once per project mount via
a new IPC method (preload bridge); the main process only
opens paths that pass both the floor check and the allowlist.
ProjectView wires the registration via a useEffect tied to
projectDetail.resolvedDir, so the active project's daemon-supplied
working directory is always the one being approved (not a renderer-
synthesized string).
Threat-model caveat documented in the runtime.ts comment block: an
attacker that fully controls the renderer can also call register
with arbitrary paths. Closing that gap fully requires a daemon-side
round-trip to derive the canonical resolvedDir from the daemon's
project registry, which is deferred to keep this PR focused.
Today's allowlist still defends against accidental misuse, bugs,
and common XSS payloads that don't know to call register first.
Adds apps/packaged/tests/desktop-project-root-gate.test.ts with 13
cases: floor-validation rejection cases (empty / relative / missing
/ file), happy-path resolution, symlink realpath canonicalization,
and the allowlist's register/isApproved/reset semantics. Mirrors
the existing apps/packaged/tests/desktop-url-allowlist.test.ts
pattern from PR #911 — the packaged workspace hosts the test
because apps/desktop has no vitest setup yet.
* fix(daemon): wire request-lifecycle abort signal through finalize route
Addresses mrcfps's PR #974 P1 review on
apps/daemon/src/server.ts:3831-3837 (also called out as P1.1 in
lefarcen's deep-dive review): `POST /api/projects/:id/finalize/anthropic`
called `finalizeDesignPackage(...)` without threading any
request-lifecycle abort, so cancelling the browser fetch only
aborted the UI-side request — the daemon's 60–120 s Anthropic call
kept running and still wrote DESIGN.md after the UI returned to idle.
Adds an AbortController inside the route handler, fired from
`res.on('close')`, and threads its signal into the existing
`signal?: AbortSignal` parameter on `FinalizeOptions`
(finalize-design.ts:70). `callAnthropicWithRetry` already passes
the signal through to the underlying fetch, so a client disconnect
now propagates all the way to the Anthropic SDK call.
Listener-event choice: `res.on('close')` is the canonical event
for "client disconnected before response was sent" in Express. The
common alternative `req.on('close')` fires whenever the *request*
stream finishes — for POST routes that means as soon as the
body-parser middleware drains the body, well before the route does
any work. Using req.on('close') would have flipped the abort
controller in every successful run; the test caught this empirically.
Caveat documented in the route's comment block: an abort fired
*after* the upstream response has been received but *before* the
atomic write completes still allows the write to land. The SDK
contract bounds the network round-trip, not the post-network disk
handoff.
Adds tests/finalize-route-abort.test.ts: spins up the test server,
mocks global fetch to capture the daemon-side AbortSignal at the
Anthropic call, sends the request via raw http (so we can destroy
the underlying socket), waits until the server reaches the
Anthropic call, then destroys the socket and asserts that the
daemon-side signal received an abort event within 5 s.
Three pre-existing project-watchers chokidar tests show flaky
timeouts under full-suite concurrency but pass in isolation;
unrelated to this fix.
* fix(daemon): refactor finalize-route-abort test to satisfy strict TS narrowing
The CI typecheck (`pnpm --filter @open-design/daemon typecheck`,
which runs both tsconfig.json and tsconfig.tests.json) caught what
my pre-push validation missed: TS narrowed `capturedSignal` to
literal `null` because vitest's mockImplementation closure can't
prove its callback runs, leaving the bare `let capturedSignal:
AbortSignal | null = null` permanently typed at its initial value.
At line 184 (`expect(capturedSignal?.aborted).toBe(true)`) the
right-hand side of the optional-chain became unreachable, and TS
flagged it as `Property 'aborted' does not exist on type 'never'`.
Switches to the standard ref-object pattern
(`const capture: { signal: AbortSignal | null } = { signal: null }`).
TS narrows let bindings inside closures conservatively but treats
object-property writes as opaque, so `capture.signal` reads
correctly across the closure boundary. Logic is unchanged.
(Pre-push oversight: ran `pnpm --filter @open-design/web typecheck`
but not the full repo `pnpm typecheck` after the daemon test
landed; the daemon's own typecheck would have caught this. Adding
`pnpm typecheck` back into the standard pre-push checklist.)
* fix(desktop): make shell.openPath gate daemon-controlled and reject .app bundles
Addresses lefarcen + mrcfps PR #974 P1 reviews on the previous path
allowlist (commit
|
||
|
|
020ecb1d6a |
feat(provider-models): sort fetched models alphabetically (#1097)
Co-authored-by: haolin122 <hl6593@nyu.edu> |
||
|
|
617fb043fe |
feat(settings): add fetch models button for BYOK providers (#1034)
* feat(settings): add fetch models button for BYOK providers * fix(settings): exclude Ollama from fetch models, add manual-entry hint * fix(provider-models): classify non-JSON upstream errors by HTTP status * fix(i18n): drop redundant English overrides from non-English locales * fix(provider-models): allow ollama through allowlist, return unsupported_protocol --------- Co-authored-by: haolin122 <hl6593@nyu.edu> |
||
|
|
63a9a08b83 |
fix(daemon): prefer opencode-cli over opencode binary so OpenCode Desktop installs resolve to the CLI (#814) (#818)
OpenCode Desktop ships two binaries on PATH after install:
- `opencode` — GUI launcher; double-clicking it opens the
desktop app, never reads stdin.
- `opencode-cli` — headless CLI; speaks the
`run --format json --dangerously-skip-permissions -`
protocol the daemon expects.
The OpenCode AGENT_DEF set `bin: 'opencode'`, so
`resolveAgentExecutable` resolved the GUI launcher for every user
with the desktop install. The daemon happily spawned that binary,
but the GUI launcher discards stdin and never emits the JSON event
stream — so the agent silently produced no output for the entire
conversation. Issue #814 tracks the user-visible symptom.
Fix: declare `bin: 'opencode-cli'` and `fallbackBins: ['opencode']`
on the OpenCode adapter. `resolveAgentExecutable` walks `bin`
first, then each fallback in order, so this gives us:
- Desktop install (both binaries on PATH) → resolves
`opencode-cli` (the CLI), GUI is ignored. The #814 fix path.
- Legacy CLI-only install (only `opencode` on PATH, no
`-cli` suffix) → falls through to `opencode`, which is the
actual CLI in that install shape, so the daemon keeps working
unchanged.
- Neither installed → falls through to null, same as before.
Same mechanism Claude Code uses to fall back to `openclaude`
(issue #235), so this is well-trodden ground in the codebase.
Tests:
- One declarative test asserts `opencode.bin === 'opencode-cli'`
and `opencode.fallbackBins.includes('opencode')` — runs on every
platform including Windows, catches anyone who accidentally
flips `bin` back to the GUI name.
- Two filesystem-backed tests pin the resolution priority: when
both binaries are on PATH (the desktop case) `opencode-cli`
wins; when only the legacy `opencode` is on PATH the fallback
takes over.
- Existing comment guarding "most agents have a single binary"
updated to remove `opencode` from the example list.
Verified locally:
- daemon vitest: 993/993
- daemon `tsc -p tsconfig.json` and `tsc -p tsconfig.tests.json`: both clean
- i18n-check passes
Reported by @giuliastro, diagnosed by @lefarcen who also asked for
the `opencode-cli --version` output in the issue thread — happy to
narrow this further if their reply turns up subcommand differences
between the two binaries.
Fixes #814
|
||
|
|
5390541f34 |
fix daemon browser opener on Windows (#953)
Co-authored-by: Sapientropic <194952804+Sapientropic@users.noreply.github.com> |
||
|
|
724d071c01 |
feat: add design file rename support (#894)
* feat(contracts): add project file rename contract * feat(daemon): add safe project file rename API * feat(web): support renaming design files * fix(daemon): handle case-only file renames * fix(web): prevent rename collisions with pending sketches * fix(daemon): preserve source names during rename * test(daemon): cover rename symlink escapes * fix(daemon): avoid clobbering rename targets * test(web): align rename tests after rebase * test(web): align rename tests with latest main |
||
|
|
fe879036fb |
fix(web): restore media config from daemon on startup (#687)
* fix(web): restore media config from daemon on startup * fix(media): preserve stored keys on settings save * fix(web): harden daemon media restore flow * fix(web): unify media provider empty-state rules * fix(desktop): retry loading discovered web url * fix(web): preserve local media providers on partial daemon reload * fix(web): preserve media providers on daemon reload * fix(web): skip media migration for masked-only local state * fix(web): preserve daemon media state across reloads |
||
|
|
643d0cf637 |
feat: add scheduled routines for unattended agent runs (#1033)
* feat: add scheduled routines for unattended agent runs
Generalizes Orbit's single hard-coded daily-digest scheduler into
user-defined routines: each one fires on a schedule (hourly / daily /
weekdays / weekly with IANA timezone) and starts a fresh agent
conversation, either inside an existing project or in a new project
minted on the spot.
Backend:
- New RoutineService with timezone-aware nextRunAt computed via
Intl.DateTimeFormat (no new dependency); two-pass tzWallToUtc so
DST transitions stay correct. Each fire chains rescheduleOne in
finally() to keep the cadence alive.
- routines + routine_runs SQLite tables; schedule_json is the
authoritative form, with legacy schedule_kind/value kept populated.
- /api/routines CRUD + /api/routines/:id/run + /api/routines/:id/runs.
- Run handler resolves agent (routine override -> app config -> first
available), creates project (or reuses configured one) and a fresh
conversation per fire, then dispatches into startChatRun.
UI (Settings -> Routines):
- Pill-chip schedule kind picker, time + timezone fields, weekday
picker for Weekly. Live preview line ("Runs daily at 9:00 AM GMT+8").
- Routine list with inline status pill, next/last meta, expandable run
history; each history row links into the project the run wrote to
via the existing router primitive.
* fix(daemon): swallow trailing finally rejection for inflight cleanup
Without a terminal `.catch`, the promise returned by `promise.finally(...)`
mirrors the original rejection and produces an unhandled rejection — fatal
in modern Node — when the run handler rejects before producing a start
handle. Callers still see the rejection on the returned `promise`.
Generated-By: looper 0.6.1 (runner=fixer, agent=claude-code)
* fix(daemon): handle DST spring-forward gap in tzWallToUtc
The two-pass conversion picked the pre-gap candidate when the requested
wall time fell inside a spring-forward gap (e.g. 02:30 in
America/New_York on 2026-03-08), so the resulting instant rendered back
as 01:30 local and a 02:30 routine fired an hour early on the
transition day. Routines are local wall-clock schedules, so firing
before the requested time breaks the contract.
Now we round-trip both candidates through partsInTimezone, return the
one whose wall-clock matches the request, and on a gap day where
neither matches return the later candidate so the routine fires at the
first valid post-gap instant on the same day.
Generated-By: looper 0.6.1 (runner=fixer, agent=claude-code)
* fix(daemon): preserve both wall-time candidates on DST fall-back day
On a fall-back day, the requested wall time inside the repeated hour
(e.g. 01:30 America/New_York) maps to two distinct UTC instants. The
previous tzWallToUtc collapsed them to the first (pre-transition) one,
so a daemon that woke between the two instants would skip the second
01:30 entirely and fire a day late once per fall-back. Replace it with
tzWallToUtcCandidates (returns all valid instants, ascending) plus a
gap-only fallback for spring-forward, and have nextWallTimeMatching
walk both ambiguous candidates before advancing to the next day. Adds
fixtures for the repeated-hour case so the intended behavior stays
locked in.
Generated-By: looper 0.6.1 (runner=fixer, agent=claude-code)
* fix(web): make routine timezone picker IANA-complete and DST-truthful
The timezone dropdown was a hardcoded subset, but the backend validator
accepts any IANA zone — so users could not pick zones like
`America/Phoenix` or `Africa/Johannesburg` unless they happened to be
local. And `gmtLabel()` always derived the offset from `new Date()`,
which drifted seasonally for DST-observing zones (a New York routine
created in winter rendered `GMT-5` while it would actually fire on
`GMT-4` after DST started).
Source the picker from `Intl.supportedValuesOf('timeZone')` (with a
curated fallback for older runtimes) and anchor the GMT label to the
routine's next fire time. When the next fire time is unknown (e.g.
the live preview while the form is open) and in the dropdown itself,
fall back to the IANA city, which is stable year-round.
Generated-By: looper 0.6.1 (runner=fixer, agent=claude-code)
* fix(web): always include UTC in routine timezone picker
`Intl.supportedValuesOf('timeZone')` returns only canonical region
names on current runtimes (Node 24, recent browsers) and omits `UTC`,
so the previous picker dropped the most common non-local zone unless
the runtime itself was already UTC. The backend validator and the
contract examples still accept `UTC`, so a user on a non-UTC machine
could not create a documented UTC routine from Settings.
Prepend `UTC` inside `listSupportedTimezones()` when the runtime list
omits it, so the picker stays aligned with the supported schedule
surface.
Generated-By: looper 0.6.1 (runner=fixer, agent=claude-code)
|
||
|
|
21dcd74abb | Increase agent inactivity timeout (#1071) | ||
|
|
9ef136ced5 |
fix: sync Orbit last run with selected prompt template (#937)
* fix(orbit): scope last run to selected template * fix(orbit): preserve legacy last run on upgrade * fix(orbit): pin legacy last-run fallback on refresh * fix(orbit): pin template id at run start * test(web): sync orbit fixtures with skill summary |
||
|
|
afb331a288 |
feat: add opt-in Langfuse telemetry (#800)
* docs(specs): add langfuse telemetry change spec
Captures the design for forwarding completed agent runs to Langfuse,
including data-model mapping, field-budget caps, privacy gates,
build-secret injection, GDPR right-to-deletion approach, and the
resolved decisions on default consent, identifier shape, region, and
ownership.
* feat(daemon): add langfuse-trace module and telemetry prefs
Adds the dependency-free building blocks for forwarding completed
agent runs to Langfuse. Two layers:
- AppConfigPrefs gains installationId and a TelemetryPrefs object with
metrics / content / artifactManifest gates. The daemon validator
treats telemetry like agentModels — replace-on-write, drop-when-empty,
reject non-boolean inner values.
- New langfuse-trace.ts builds a {trace-create, generation-create}
pair from a ReportContext, capping prompt at 8 KB, output at 16 KB,
artifacts at 50 entries, and dropping any batch larger than 1 MB
before send. reportRunCompleted is no-op when LANGFUSE_PUBLIC_KEY /
LANGFUSE_SECRET_KEY are unset (so dev runs and forks never emit) and
short-circuits on prefs.metrics === false.
Server-side wiring into the run-close path lands in a follow-up.
* fix(langfuse): default to US Langfuse region
End-to-end smoke against the project's actual dev key on 2026-05-07
returned 401 from cloud.langfuse.com (EU) and 207 from
us.cloud.langfuse.com (US), confirming the org lives in US. Update the
default base URL, the matching test, and the spec's Q3 decision row to
match. Self-hosted or EU-region operators can still override via the
LANGFUSE_BASE_URL env var.
* feat(daemon): wire langfuse trace forwarding into run-close
Adds the daemon-side glue to forward completed agent runs:
- runs.ts gains an optional onTerminate hook fired once per run after it
reaches a terminal state. Errors thrown from the hook are caught and
logged, never propagated, so telemetry can never break the run path.
- New langfuse-bridge.ts assembles a ReportContext from the in-memory
run record, the conversation's persisted assistant message, and the
user's app-config preferences. It tolerates a missing message (e.g.
when web has not yet PUT the final delta) and a missing app-config.
- server.ts stashes the original user prompt on the run object inside
startChatRun so the bridge can include it without crossing the
createChatRunService boundary, and registers the hook callback when
building the run service.
Behavior remains a no-op unless LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY
are set in the daemon env AND telemetry.metrics is true in app-config.
A live smoke against us.cloud.langfuse.com on 2026-05-07 confirmed the
matching trace + generation schema is accepted (HTTP 207, both events
201 created).
* fix(langfuse): address PR #800 review feedback
P1 — Move trace forwarding off the daemon-internal run-close hook and
onto the message-persistence path. The original onTerminate hook ran
inside finish() the moment the SSE 'end' event was emitted, which is
*before* the web client's onDone handler refreshes project files and
PUTs producedFiles + final assistant content back to SQLite. Reading
SQLite at that moment routinely missed both. The fix: drop the runs.ts
hook entirely and trigger from PUT /api/projects/:id/conversations/:cid/
messages/:mid when the saved row carries a terminal runStatus. A
reportedRuns Set guards against the multiple PUT calls web makes per
turn (each retry / state update). Set entries auto-evict after the same
30 min TTL the runs map uses. Web persists a terminal-status message in
all three completion paths — onDone (succeeded), onError (failed), and
cancel (canceled) — so this catches every run shape.
P2 — postLangfuseBatch now parses the 207 Multi-Status response body.
Langfuse legacy ingestion always returns 207, and response.ok is true
for 207, so per-event validation errors used to slip through silently.
We now warn when body.errors is non-empty. Two new unit tests.
P2 — truncate() and the HARD_BATCH cap now compare UTF-8 byte length,
not String.length (which counts UTF-16 code units). A 4096-character
CJK prompt occupies 12 KB, well over the 8 KB input cap. truncate also
walks backwards to a UTF-8 leading byte so the cut never lands inside a
multi-byte codepoint. New unit test covers '设'.repeat(4096).
P2 — Spec R7 now lists the actual Langfuse trace deletion endpoint
(DELETE /api/public/traces/{traceId} for single, DELETE /api/public/traces
with body for batch). Verified by curl on us.cloud.langfuse.com:
DELETE /api/public/traces/X → 200; the path the original spec named
(POST /api/public/trace/X) returns 404. Reference link points at
langfuse.com/docs/administration/data-deletion.
P3 — Q4 (legacy ingestion vs OTel) moved from Open Questions to
Resolved Decisions. The implementation already commits to legacy and
the trade-off was discussed during design; the open-question status was
stale.
* feat(web): privacy consent surface + Settings → Privacy tab
Adds the user-facing half of the telemetry feature so the daemon-side
hook from PR #800 has something to talk to.
- AppConfig gains optional `installationId` (anonymous v4 uuid generated
on first opt-in; null after explicit decline; undefined when the user
has never seen the consent surface) and `telemetry: TelemetryConfig`
({metrics, content, artifactManifest}). syncConfigToDaemon round-trips
both fields so the bridge module sees the same prefs.
- SettingsDialog grows a Privacy section with two states. When the user
has never made a consent decision (typical first-run path), the
section renders the GDPR-aligned consent card: a kicker, the disclosure
body listing both metrics and conversation content as separate bullets,
and two equally-prominent buttons ("Share usage data" / "Don't share").
The Don't-share path keeps the app fully usable (core app must work
with all tracking declined). After a decision the same panel switches
to three independent toggles + the anonymous ID + a "Delete my data"
button that rotates the ID and turns everything off.
- App.tsx points the welcome modal at the new Privacy section so the
consent decision is the first thing a fresh installation sees.
- 17 i18n keys land in en + zh-CN + zh-TW with hand-translated copy,
and as English placeholders in the remaining 14 locales — enough for
the parity check to pass while leaving room for proper localisation
in a follow-up. Dict type updated.
- Minimal index.css for the consent card + toggle rows so the panel is
legible without depending on follow-up design polish.
Telemetry remains a no-op end-to-end until the user clicks Share usage
data: the daemon gate (prefs.metrics === true) keeps every code path
short-circuited otherwise.
* refactor(web): rebuild Privacy panel using project-native settings primitives
The first cut used custom .settings-privacy-* classes + raw HTML
checkboxes that didn't match any other Settings tab. Replace with the
shell other sections already use:
- settings-subsection containers with section-head + h4 + .hint
- seg-control / seg-btn pill toggles ("active" / "offline") for each of
the three telemetry preferences, mirroring NotificationsSection
- a 2-cell seg-control for the consent card so Share usage data and
Don't share carry identical visual weight (the GDPR equal-prominence
requirement that the previous accent / outline split missed)
- ghost button + readonly text input for the installation id row,
mirroring the API-key field pattern elsewhere
Drop the bespoke CSS block in favor of inheriting the existing
settings-section / seg-control / ghost styling. The only privacy-
specific style left is a tight definition list inside the consent card
for the metrics + content disclosure rows.
* refactor(web): use .toggle-row iOS switch for Privacy preferences
Active/offline pills (the seg-control single-cell pattern that
NotificationsSection uses) read awkwardly for a flat preference list.
Switch the three telemetry toggles to .toggle-row — the same control
NewProjectPanel uses for "speaker notes" / "animations": label + hint
on the left, iOS-style sliding switch on the right, full-row click
target. The consent card's two-button seg-control stays as-is — there
the equal-weight pill pair is exactly what GDPR equal-prominence wants.
* feat(web): standalone first-run privacy consent banner
Replaces the Settings-dialog-as-onboarding hack with a dedicated
bottom-right banner card that mounts whenever the user has never made
a privacy decision (cfg.installationId === undefined). The banner is
prominent (anchored to the corner with a soft shadow) but
non-blocking, mirrors cookie-consent UX, and shares the project's
panel styling — same .modal-elevated background, --radius-lg corners,
--shadow-lg lift.
Wiring:
- App.tsx imports PrivacyConsentModal and renders it at the root,
gated on installationId === undefined && !settingsOpen so it doesn't
double up with the Privacy tab's own consent card when Settings is
already showing.
- Share / Don't share both go through handleConfigPersist, so the
resulting installationId + telemetry prefs land in localStorage and
the daemon at the same time, reusing the existing autosave plumbing.
- The previous attempt that pinned the welcome SettingsDialog to the
Privacy section is reverted; onboarding now stays focused on agent
configuration, and the consent decision lives in its own surface.
* fix(web): keep privacy banner visible while Settings welcome modal is open
The banner gated itself on `!settingsOpen` to avoid double-rendering
with the Privacy tab's consent card. But the first-run path opens the
Settings welcome modal automatically when `onboardingCompleted=false`,
which fired immediately after bootstrap — so the banner flashed for a
moment and then vanished behind the modal backdrop.
Drop the `!settingsOpen` clause so the banner stays mounted whenever
the user has not yet made a privacy decision, and bump its z-index
above the modal backdrop (200 vs 100) so first-run users can actually
reach the consent buttons. The minor visual overlap with the Privacy
tab's own card is fine: clicking either copy resolves both surfaces.
* copy(privacy): soften consent button labels
Banner action buttons now read "Help improve Open Design" / "Not now"
(en, with hand translations in zh-CN / zh-TW and English placeholders
in the other 13 locales) instead of "Share usage data" / "Don't share".
The new wording aligns the affirmative action with the kicker copy
("Help us improve Open Design") and reads less alarming, while the
disclosure list above still names both data categories explicitly so
the consent stays informed under GDPR. The decline button stays as a
soft "Not now" rather than an aggressive "Don't share" so the reject
path doesn't read as hostile to the user.
No structural change — the two-cell seg-control still gives the buttons
identical visual weight, and the underlying side-effects are unchanged
(installationId is generated on Help / nulled on Not now, and the
telemetry prefs flip the same way).
* feat(telemetry): expand trace fields for evals & dataset construction
Each Langfuse trace now ships the full per-turn + per-install fact
sheet that the eval/dataset workflow needs, instead of only the bare
turn id + token count from before. Everything below is gated by
`prefs.metrics === true`; nothing here is content (those gates remain
separate).
Per-turn:
- model — first-class generation.model field, drives Langfuse cost
lookup and model-grouping in the UI; also mirrored in trace.metadata
and trace.tags so list-view filters work.
- reasoning — generation.modelParameters.{ reasoning } so the Model
Parameters card lights up; mirrored in metadata.
- skillId / designSystemId — metadata + tags, so dataset slices can
group by which skill/DS produced which output.
Per-process / build (constant within one daemon run, cached at start):
- appVersion / appChannel / packaged from app-version.ts
- nodeVersion (process.version), os (platform()), osRelease,
arch (os.arch())
- clientType — desktop vs web, derived from a new X-OD-Client header
the web layer sets in providers/daemon.ts (with a User-Agent sniff
fallback for third-party callers).
Plumbing:
- startChatRun stashes model / reasoning / skillId / designSystemId
on the run object alongside the existing userPrompt stash.
- POST /api/runs reads X-OD-Client and stores run.clientType.
- langfuse-bridge collects RuntimeInfo once per process and merges
per-run client carrier; ReportContext gains optional `turn` +
`runtime` blocks; existing fields stay backward compatible.
Spec gains a "Telemetry Fields Catalog" section enumerating every
field, its source, and the gate it lives under, so the eval team has a
single place to look up what's available without reading the trace
schema by example.
Tests:
- new langfuse-trace tests cover turn tags, runtime tags, generation
model/modelParameters promotion, modelParameters omission when
reasoning is unset, and metadata mirroring.
- langfuse-bridge gains an end-to-end "turn-level config" test that
threads model/reasoning/skill/DS/clientType + appVersion through
the bridge and asserts the Langfuse payload shape.
- existing tests adjusted to tolerate host-dependent os tag.
* copy(privacy): trim Share button to verb phrase only
"Help improve Open Design" overflowed the equal-width 2-cell
seg-control on the consent banner — the product name is already in
the kicker + headline above the buttons, so the button itself only
needs the verb phrase. Drop the product name from all locales:
- en: Help improve Open Design → Help improve
- zh-CN: 帮助改进 Open Design → 帮助改进
- zh-TW: 協助改進 Open Design → 協助改進
The decline button ("Not now" / "暂不" / "暫不") was already short, so
the two buttons now have comparable length and the equal-prominence
seg-control fits cleanly. Standalone Settings → Privacy panel uses
the same labels for consistency.
* fix(web): defer Settings welcome modal until privacy decision is made
Previously bootstrap raced two surfaces against each other on first
launch: the privacy consent banner (gated on installationId ===
undefined) and the Settings welcome modal (gated on
onboardingCompleted === false). The banner's higher z-index kept it
above the backdrop visually, but having two foreground surfaces at
once is still confusing UX.
Sequence them instead: bootstrap only opens the welcome modal when
the user has *already* resolved consent (installationId !== undefined).
Until then the banner owns the foreground alone. Once the user clicks
Help improve / Not now, the corresponding handler hands off to the
welcome modal if onboarding is still pending. End state matches what
it was before — just without the simultaneous-render flash.
* debug(privacy): log banner gate state to track sudden disappearance
Two console.log points to find which setCfg call (or stale bundle) is
flipping cfg.installationId from undefined to a value while the banner
is visible. To remove once the regression is reproduced.
* fix(privacy): keep installationId + telemetry out of localStorage
Daemon is now the single source of truth for the privacy decision.
Why this matters: the consent banner gates on
\`config.installationId === undefined\`, but loadConfig() merges
localStorage on top of the daemon's reply, so a stale uuid in
\`open-design:config\` (left over from a previous opt-in) was
re-hydrating the React state and immediately syncing back to the
daemon — defeating "Delete my data" and re-suppressing the banner
within milliseconds of every page load.
The deeper reason to fix it here, not just patch the gate: a privacy
identifier persisted in browser storage that the user can't see or
clear without DevTools is a compliance liability. Anything users can
revoke needs one canonical place to store it. Daemon \`app-config.json\`
already serves that role for everything else gated through
syncConfigToDaemon, so installationId + telemetry now ride that path
exclusively:
- saveConfig() strips both keys before writing localStorage.
- loadConfig() strips both keys when reading older stale payloads,
so existing installs migrate transparently on next launch.
- syncConfigToDaemon() / mergeDaemonConfig still round-trip them, so
the React state stays in sync with the daemon as before.
Net effect: clearing app-config.json (or hitting "Delete my data") now
fully resets the install identity, with no residual cohort key in
browser storage.
* feat(privacy): scrub secrets + PII from prompt/output before send
When prefs.content is on, daemon now runs the prompt and assistant
text through a regex scrubber (apps/daemon/src/redact.ts) before
posting to Langfuse. The scrubber is the simplest thing that gives
the user-facing copy a truthful claim — pure regex, zero new
dependencies, fully auditable in this Apache-2.0 repo (vs. pulling a
single-maintainer 5-month-old npm package into a core process).
Categories covered (each replaced with [REDACTED:<kind>]):
- Anthropic / OpenAI sk- keys (incl. proj/live/test/ant variants)
- Langfuse pk-lf- / sk-lf- (specific rule wins over generic sk-)
- GitHub gh[opsur]_ tokens
- AWS access key ids (AKIA + 16 uppercase)
- Google API keys (AIza + 35)
- Slack xox[abprs]- tokens
- Stripe live/test keys
- JWT header.payload.signature triples
- Bearer-header values (scheme word stays readable)
- Emails, IPv4, US-style phone numbers
- Credit cards — 13–19 digit runs that pass a Luhn check, so order ids
and unix-nanos timestamps that fail Luhn pass through unchanged
Not covered, stated openly in spec + i18n: names, postal addresses,
business-secret semantics, raw 40-hex tokens (too high a false-positive
cost for artifact slugs). Those would require an ML layer.
Wired in:
- apps/daemon/src/redact.ts — exports redactSecrets() +
redactSecretsWithCounts() helper for future audit-summary metadata.
- apps/daemon/src/langfuse-bridge.ts — runs both prompt and output
through redactSecrets() before they reach the trace builder.
- 18 unit tests cover every pattern plus negative cases (Luhn-failing
digit runs, out-of-range IPv4 octets, idempotence on re-redacted
text, ordinary prose passthrough).
- i18n privacyContentHint on en + zh-CN + zh-TW (plus 14 locale
placeholders) enumerates the categories so the consent disclosure
matches the implementation — the GDPR informed-consent requirement.
- spec gains a Pre-send Redaction subsection with the regex shape
table + intentional non-coverage list.
Drive-by: dropped the [privacy] debug logs that traced the now-fixed
bootstrap regression.
* fix(telemetry): make Langfuse reporting resilient
* feat(telemetry): nest Langfuse turn observations
* feat(telemetry): emit Langfuse tool spans
* fix(telemetry): report after finalized message writes
* fix(telemetry): honor persisted terminal status
* fix(web): let consent banner yield page clicks
* fix(telemetry): report current turn prompt only
|
||
|
|
5c67651bfd |
fix: make Azure api version optional (#941)
Co-authored-by: haocn-ops <259245673+haocn-ops@users.noreply.github.com> |
||
|
|
3c94fa575c |
feat(daemon): expose gemini-3 preview models and 2.5-flash-lite in the Gemini picker (#986)
* feat(daemon): expose gemini-3 preview models and 2.5-flash-lite in the Gemini fallback list Issue #981: the Gemini agent picker only offered 2.5 Pro / 2.5 Flash even though the Gemini CLI ships gemini-3-pro-preview, gemini-3-flash-preview, and gemini-2.5-flash-lite. Users had to fall back to "Default" or type the id by hand to reach the newer generation. Add the three model ids to fallbackModels with the same shape as the existing entries so the picker surfaces them inline. * test(daemon): pin Gemini picker fallbackModels order mrcfps flagged on PR #986 that the new fallback ids only had implicit coverage via the static array, which means a future reorder/removal in AGENT_DEFS could silently reshape the Settings UI without tripping CI. Mirror the codex/qoder regression-test pattern so the Gemini picker contents stay locked to the documented priority order: default, gemini-3-pro-preview, gemini-3-flash-preview, gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite. This is especially worth covering for Gemini because the CLI accepts arbitrary custom ids, so a regression in the picker would not surface during manual QA. --------- Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
b578b93f3f |
Bug FIx: Media generation task state is volatile and lost on daemon restart #648 (#884)
* feat: implement media tasks persistence
* fix(daemon): satisfy exactOptionalPropertyTypes in media-tasks-routes test
`process.env.OD_DATA_DIR` is `string | undefined`, but `openDatabase`'s
options accept `{ dataDir?: string }`. Under the daemon tsconfig's
exactOptionalPropertyTypes the two are not assignable. Spread the key
in only when defined.
* fix(daemon): restore mcp-config / mcp-oauth / mcp-tokens imports in server.ts
The earlier 'Merge branch main into main' resolved the import-block
conflict by keeping only the new media-tasks import and dropping the
three pre-existing import blocks. server.ts still uses 13+ symbols
from those modules (PendingAuthCache, MCP_TEMPLATES, beginAuth,
readMcpConfig, getToken, etc.), so the daemon crashed at startup with
'ReferenceError: PendingAuthCache is not defined' the moment Playwright
booted it. Restore the three import blocks verbatim from main.
---------
Co-authored-by: lefarcen <935902669@qq.com>
|
||
|
|
05461c64fd |
fix(connectors): show stable curated tool count in connector card badge (#748) (#767)
* fix(connectors): show stable curated tool count in connector card badge (#748)
The connector card's "N tools" badge in `apps/web/src/components/
EntryView.tsx` rendered `connector.tools.length` for both pre- and
post-Composio-hydration states, so the displayed count lurched
without explanation:
- Before configuring a Composio API key, GitHub showed "2 tools"
(the static fallback catalog at `composio.ts:21-53`).
- After hydration, the daemon merged in the full Composio provider
inventory (`composio.ts:725-778`, ~868 GitHub tools), and the
same badge jumped to "868 tools".
Same field name, two different concepts — `connector.tools` is
"everything the provider ships" while the agent-callable subset
is `definition.allowedToolNames` (read-only auto-approval tools
that pass `isAgentPreviewListableTool()`).
Fix: surface `allowedToolNames` on the wire `ConnectorDetail` and
have the badge use that count instead. The detail drawer below
the card still iterates over `connector.tools` to enumerate the
full inventory — the count and the list are intentionally
different surfaces.
The badge now stays close to "tools the agent can actually
invoke" (≈2-30 for GitHub, depending on auto-allowed read tools)
instead of the raw provider inventory (~868). No 800x jump on
hydration.
Wire format change:
- packages/contracts/src/api/connectors.ts: add
`allowedToolNames: string[]` to ConnectorDetail
- apps/daemon/src/connectors/catalog.ts: same field on the
daemon-internal type, populated in `connectorDefinitionToDetail()`
as a defensive copy of `definition.allowedToolNames`
- packages/contracts/src/examples.ts: extend the example fixture
- apps/web/src/components/EntryView.tsx: badge call sites switch
to `connector.allowedToolNames?.length ?? connector.tools.length`
(the `??` keeps the badge alive against any older daemon build
that hasn't shipped the field yet)
Tests:
- 4 new daemon tests in connectors-service.test.ts pin the
contract: getConnector() emits the field, the array is a
defensive copy, and the #748 regression guard simulates the
Composio post-hydration shape (tools.length=801,
allowedToolNames.length=1) to prove the badge invariant
- web EntryView.test.ts fixtures updated to satisfy the new
required field
Verified locally:
- daemon vitest: 925/925
- web vitest: 332/332
- daemon/web/contracts typecheck clean
- i18n-check passes
- Live `/api/connectors/discovery` returns the new field; pre-
hydration GitHub/Notion/Google Drive badges all read "2 tools"
(no regression vs before this change)
Fixes #748
* fix(connectors): split drawer badge vs inventory counts; fix daemon test typecheck (#767 review)
Two follow-ups for @mrcfps's review on PR #767.
1) **P1: Drawer empty-state regression.** The previous commit reused
the curated `toolCount` for both the header badge AND the inventory
section's loading gate / empty-state branch / section count. The
inventory section renders `connector.tools` directly, so a hydrated
connector with raw provider tools but an empty allowlist (e.g. a
write-only Composio surface) would render "no tools available" and
hide the actual inventory list — exactly the contradiction my own
PR description warned against.
Fix splits the two surfaces:
- `badgeToolCount` (curated, via the new exported helper
`getConnectorBadgeToolCount`) feeds the card and drawer header
badge — the summary count, where the #748 stability matters.
- `inventoryToolCount = connector.tools.length` (inline) drives the
drawer's loading gate, section count, and empty-state branch —
the surfaces describing the actual rendered list.
The card has no inventory section so it stays on the badge helper
unchanged.
2) **CI: daemon test typecheck failed.** The connectors-service test's
`provisionedTools[0].safety` index access tripped daemon
`tsconfig.tests.json`'s `noUncheckedIndexedAccess` strict setting,
even though my local `tsc -p tsconfig.json --noEmit` was clean —
that config is a separate compilation. Bind through a defined-checked
local before reading `.safety`, per @mrcfps's exact suggestion.
Tests:
- 4 new web tests in `EntryView.test.ts` pin the
`getConnectorBadgeToolCount` contract, including the explicit
regression: a connector with `allowedToolNames=[]` and
`tools.length=800` returns badge=0 but the inventory length
stays at 800 — the drawer's empty-state branch must use the
inventory count, never the badge count.
- Existing daemon test fixed without losing assertion coverage.
Verified locally:
- daemon vitest: 921/921
- web vitest: 336/336 (was 332, +4)
- daemon `tsc -p tsconfig.json` and `tsc -p tsconfig.tests.json` (the CI killer): both clean
- web `tsc -b --noEmit` clean
- i18n-check passes
Process learning baked into this PR: from now on I'll always run the
`tsconfig.tests.json` separately before pushing, since the workspace
typecheck script chains both and the second one is what CI fails on.
* fix(connectors): pin badge to curated catalog count, not the dynamic execution allowlist (#767 review v2)
@lefarcen and @mrcfps both flagged that the previous iteration of
this PR (commit
|
||
|
|
109722de3a |
feat(desktop): export artifacts directly to PDF (#532)
* feat(desktop): export artifacts directly to PDF * fix(desktop): PDF 내보내기 기본 여백 제거 |
||
|
|
2005a5b727 |
fix: add gpt-5.1 Codex picker options (#946)
* fix: add gpt-5.1 Codex picker options * test: clarify gpt-5.1 picker coverage --------- Co-authored-by: leprincep35700 <leprincep35700@users.noreply.github.com> |
||
|
|
0f586c410d |
Fix Cloudflare Pages custom domain lookup (#958)
* Support Cloudflare Pages custom domains without hiding pages.dev fallback Keep the default Pages preview as the first public link while optional owned-zone binding provisions DNS and Pages custom-domain state in parallel. Constraint: Cloudflare deploys must use the existing direct-upload API path with no Wrangler dependency. Constraint: pages.dev must stay visible even while custom-domain verification is pending. Rejected: Vercel custom-domain support | outside requested Cloudflare-only scope. Rejected: overwriting arbitrary CNAME records | risks taking over user-managed DNS. Confidence: high Scope-risk: moderate Directive: Do not expose providerMetadata through public deploy contracts; keep custom-domain DNS ownership checks conservative. Tested: pnpm --dir apps/daemon exec vitest run -c vitest.config.ts tests/deploy.test.ts tests/deploy-routes.test.ts Tested: pnpm --filter @open-design/contracts build && pnpm --filter @open-design/contracts typecheck && pnpm --filter @open-design/contracts test Tested: pnpm --filter @open-design/web typecheck && pnpm --filter @open-design/web test -- providers/registry.test.ts components/FileViewer.test.tsx i18n/locales.test.ts Tested: pnpm i18n:check && pnpm guard && pnpm typecheck Tested: pnpm --filter @open-design/daemon build && pnpm --filter @open-design/web build && git diff --check Not-tested: real Cloudflare account/token/domain smoke test * Preserve Cloudflare fallback correctness under large accounts and races Constraint: Cloudflare Pages keeps pages.dev as the primary usable fallback while custom domains remain optional typed metadata. Rejected: Treating custom-domain DNS or binding failure as a top-level deployment failure | pages.dev can still be ready and usable. Confidence: high Scope-risk: moderate Directive: Keep custom-domain finality tied to Cloudflare Pages API active status plus URL reachability; do not expose providerMetadata. Tested: pnpm --dir apps/daemon exec vitest run -c vitest.config.ts tests/deploy.test.ts tests/deploy-routes.test.ts; pnpm --filter @open-design/web test -- components/FileViewer.test.tsx i18n/locales.test.ts providers/registry.test.ts; pnpm --filter @open-design/daemon typecheck; pnpm --filter @open-design/web typecheck; pnpm i18n:check; git diff --check; pnpm guard; pnpm typecheck; pnpm --filter @open-design/daemon build; pnpm --filter @open-design/web build Not-tested: Real Cloudflare token/account/zone smoke test. * Keep impeccable design notes local Constraint: .impeccable.md is local assistant/design context and should not be part of the PR diff. Rejected: Keeping the file tracked while adding it to .gitignore | tracked files are not ignored by Git. Confidence: high Scope-risk: narrow Directive: Keep .impeccable.md untracked and ignored; do not rely on it for required project documentation. Tested: git check-ignore -v .impeccable.md; git diff --check Not-tested: Full workspace tests not rerun for ignore-only metadata change. * Use direct Pages domain lookup for custom bindings Cloudflare rejects list-style options on Pages custom-domain lookup in some accounts, so the deploy path now reads the selected hostname directly before creating a binding. This keeps pages.dev deployment success intact while avoiding a failed custom-domain branch caused by page/per_page query parameters. Constraint: Cloudflare Pages custom-domain lookup must not send unsupported page/per_page list options Rejected: Continue paginating /domains | Cloudflare returns invalid list options before the binding can be created Confidence: high Scope-risk: narrow Directive: Keep pages.dev as the primary deployment URL and treat custom-domain setup as a recoverable secondary branch Tested: pnpm --dir apps/daemon exec vitest run -c vitest.config.ts tests/deploy.test.ts tests/deploy-routes.test.ts; pnpm --filter @open-design/daemon typecheck; pnpm guard; pnpm typecheck; git diff --check; pnpm --filter @open-design/daemon build Not-tested: Live Cloudflare deployment was not retriggered from the browser |
||
|
|
c2facb0e02 |
fix: serve python files as text (#947)
Co-authored-by: leprincep35700 <leprincep35700@users.noreply.github.com> |
||
|
|
208f09c60e |
fix: settle completed runs and clean up shutdown children (#924)
* fix: clean up completed and shutting down runs * fix: bound daemon CLI shutdown Generated-By: looper 0.6.0 (runner=fixer, agent=codex) * fix: harden daemon shutdown cleanup Generated-By: looper 0.6.0 (runner=fixer, agent=codex) * fix: harden daemon shutdown cleanup Generated-By: looper 0.6.0 (runner=fixer, agent=codex) * test: align acp abort fake with typed child |
||
|
|
ef9ca7baff | fix(daemon): typecheck core server paths (#952) | ||
|
|
32d820e4ee |
fix(daemon): typecheck leaf modules (#943)
* update drift * fix(daemon): typecheck leaf modules * fix(daemon): decode Qoder stdout buffers Generated-By: looper 0.5.6 (runner=fixer, agent=opencode) |
||
|
|
4f647f56ba |
[codex] Optimize Composio connector previews (#907)
* Optimize Composio connector previews * Fix partial connector tool preview hydration * Cancel pending connector authorization on daemon * Preserve Composio cached tool counts * Avoid pending state after OAuth launch failure * Preserve static tool count fallback * Fix connector preview retry state * Remove Composio auth config metrics * Hydrate unknown connector tool previews * Fix remaining connector review threads * Stop failed connector preview spinner * Hydrate only targeted agent connectors |
||
|
|
e13adf2e63 |
feat(daemon): finalize design package endpoint (closes #450) (#832)
* feat(daemon): scaffold /api/projects/:id/finalize/anthropic (refs #450) Phase C of the PR 2 plan for issue #450: scaffold the route + module shape so subsequent phases (D-I) land function bodies and tests against a stable surface that already passes typecheck. What lands here: - apps/daemon/src/finalize-design.ts: module-level constants (DEFAULT_BASE_URL, DEFAULT_MAX_TOKENS=16000, INPUT_BODY_CAP_BYTES=384KiB, LOCK_FILENAME=.finalize.lock, OUTPUT_FILENAME=DESIGN.md, DEFAULT_TIMEOUT_MS=120s); inline interfaces for the request/response shape (kept out of packages/contracts per scope rules); two error classes - FinalizePackageLockedError (mirrors PR #493's TranscriptExportLockedError) and FinalizeUpstreamError (carries upstream HTTP status for the route's error mapping); function stub that throws "not yet implemented". - apps/daemon/tests/finalize-design.test.ts: vitest harness with describe.skip placeholder so the file imports cleanly. Real cases land in phases D-I. Default-import of node:fs (per memory: vi.spyOn cannot redefine on the frozen ESM Module Namespace; CJS exports object is mutable). - apps/daemon/src/server.ts: route handler at POST /api/projects/:id/finalize/anthropic, slotted next to the existing :id/deploy* family. Validates apiKey/model non-empty, optional baseUrl via the existing validateExternalApiBaseUrl closure (forbidden -> 403, invalid -> 400), optional maxTokens positive number; calls getProject (404 on miss); calls finalizeDesignPackage (which throws, caught and mapped to 500 for now); maps known error classes (FinalizePackageLockedError -> 409, FinalizeUpstreamError -> 502) pre-emptively. Path shape rationale (Bryan-confirmed): project-scoped path matches every sibling /api/projects/:id/* route in server.ts (deploy, deployments, deploy/preflight); provider-namespaced segment leaves a clean expansion line for /api/projects/:id/finalize/openai etc. as follow-ups. Field-name rationale: apiKey, baseUrl, model, maxTokens match ProxyStreamRequest verbatim (packages/contracts/src/api/proxy.ts:8-19) so a future caller can reuse the same body shape. baseUrl is optional here (intentional divergence from the proxy at server.ts which requires it) so standard Anthropic users do not need to set it; Bedrock / self-hosted-proxy users still can. Verification: pnpm --filter @open-design/daemon typecheck exits 0; finalize-design.test.ts loads cleanly with 1 skipped placeholder; no other tests touched. Refs nexu-io/open-design#450 (PR 2 scaffold; pipeline body in subsequent commits) * feat(daemon): transcript truncation helper for /finalize prompt Phase D of the PR 2 plan for issue #450: lands the helper that bounds the transcript section of the synthesis prompt. Why this exists: real-world signal at authoring time was a local project transcript already at 3.95 MB. Anthropic's claude-opus-4-7 context cap is roughly 200K tokens (~700 KB at typical density). Inserting an unbounded transcript would 4xx upstream on the first real call. This helper keeps the on-disk .transcript.jsonl lossless (PR #493's contract) while making the prompt-inclusion bounded. Strategy: - Cap output at INPUT_BODY_CAP_BYTES (384 KiB) so the prompt has room for the system prompt + design system body + current artifact + room for the synthesis output. - Always preserve the header line - it carries projectId, schemaVersion, conversation/message counts, attachment counts; synthesis quality depends on knowing the original sizes. - Split equal byte budgets between head and tail so both project genesis and most-recent intent survive. Two thinking segments separated only by mid-session truncation lose the same kind of boundary that PR #493 preserves between thinking blocks - that's accepted; smarter semantic chunking is a follow-up. - Insert a single `{"kind":"truncated","reason":"size","omittedBytes":N}` sentinel JSON line between the head and tail so a synthesis consumer can detect the gap. omittedBytes is the difference between the original UTF-8 byte length and the output's UTF-8 byte length. - If the head + tail budgets together cover the whole body (e.g. all message lines are tiny), no marker is emitted - the output is the input verbatim. Tests: - "returns the input verbatim when the JSONL fits under the 384 KiB cap" pins that small transcripts pass through unchanged with no marker. - "head+tail truncates with a single marker line when the JSONL exceeds the 384 KiB cap" pins that output is bounded, header survives, exactly one marker emitted with non-zero omittedBytes, both ends of the body preserved, and at least one middle message omitted. Suite delta: +2 tests in finalize-design.test.ts. Refs nexu-io/open-design#450 * fix(daemon): resolve noUncheckedIndexedAccess in truncateTranscriptForPrompt D1 (0eaa123) shipped with `body[headIndex]` and `body[i]` typed as `string | undefined` under TypeScript's `noUncheckedIndexedAccess` strict mode. Local typecheck would have caught it but the prior verification piped through `tail` which masked the non-zero exit code of `tsc`. Coalesce each access via `?? ''` (the array is from `String.split('\n')` so undefined elements are not actually reachable; the coalesce is a type-narrowing convenience, not a behavior change). Verification: `pnpm --filter @open-design/daemon typecheck` exits 0; `pnpm --filter @open-design/daemon test finalize-design` shows 2/2 + 1 skipped, identical to the pre-fix run. Refs nexu-io/open-design#450 * feat(daemon): current-artifact resolver for /finalize Phase E of the PR 2 plan for issue #450: resolves which artifact (if any) accompanies the transcript + design system in the synthesis prompt. Priority order (Bryan-locked in plan §6): 1. The file referenced by tabs.is_active = 1 IF an <name>.artifact.json sidecar exists on disk. Sidecar presence is the discriminator: an inferred manifest from `inferLegacyManifest` (e.g. for a bare .html with no sidecar) does NOT count, and an active tab pointing at a non-artifact file (.md, .txt) falls through. 2. Newest project file with a real .artifact.json sidecar, sorted by manifest.updatedAt descending. Files without an updatedAt sort last so legacy pre-streaming manifests do not get accidentally promoted. 3. Returns null - "no artifact in scope". The Phase H caller will emit `artifact: null` in the response and the prompt's "Current artifact" section will read "none". Sidecar presence is checked via `existsSync` on the on-disk path, NOT via the `artifactManifest` field returned by readProjectFile/listFiles (those run inferLegacyManifest as a fallback for known kinds, which would otherwise cause a bare .html with no sidecar to look like an artifact). Tests: - "returns the active-tab artifact when its sidecar is present, even if a newer artifact exists elsewhere": pinned.html (older updatedAt) is in the active tab; newer.html (newer updatedAt) is not. Resolver returns pinned.html - intent (active tab) beats recency. - "falls through to newest .artifact.json when active tab points at a non-artifact file": README.md is the active tab (no sidecar); design.html has a real sidecar. Resolver falls through and returns design.html. - "returns null when no active tab and no .artifact.json sidecars exist": only a README.md is in the project; no tabs row. Resolver returns null. Suite delta: +3 tests in finalize-design.test.ts (5 active total). Refs nexu-io/open-design#450 * feat(daemon): synthesis prompt construction for /finalize Phase F of the PR 2 plan for issue #450: builds the system + user prompts that get sent to Anthropic's Messages API in the synthesis call. Pure function; no IO, no side effects. System prompt (literal, stored as a module-level constant): instructs Claude to emit a DESIGN.md document with a fixed 7-heading structure (# DESIGN.md / ## Summary / ## Brand & Voice / ## Information Architecture / ## Components & Patterns / ## Visual System / ## Open Questions / ## Provenance). The Provenance section is required to list project ID, design system, current artifact, transcript message count, and the UTC generation timestamp. User prompt (built at runtime): structured payload with the truncated transcript JSONL, the design system body, and the current artifact body, each under a ## heading. Missing inputs (no design system selected, no artifact in scope) produce explicit "none" headings + parenthetical placeholder body so Claude does not hallucinate content for absent sections. Truncation is the caller's concern - this function does not re-truncate. The caller (Phase H pipeline) feeds in a JSONL that has already been bounded by truncateTranscriptForPrompt. Tests: - "includes the transcript JSONL verbatim and the generation context": pins all section headings, the transcript body verbatim, the design system body verbatim, the artifact body verbatim, and every generation-context line. - "falls back to \"none\" + parenthetical when no design system is selected": designSystemId=null and designSystemBody=null -> heading reads "## Active design system: none" with the parenthetical body. - "falls back to \"none\" + parenthetical when no artifact is in scope": artifact=null -> heading reads "## Current artifact: none" with the parenthetical body. Suite delta: +3 tests in finalize-design.test.ts (8 active total). Refs nexu-io/open-design#450 * feat(daemon): Anthropic call + retry strategy for /finalize Phase G of the PR 2 plan for issue #450: lands the upstream Claude Messages API call with a single transient-error retry, plus the response extractor that turns Anthropic's content array into the DESIGN.md body. What lands here: - appendVersionedApiPath: inlined from the connectionTest helper at apps/daemon/src/connectionTest.ts:188-195 (it is not exported there). Appends /v1/messages when the base URL has no /vN segment, otherwise appends /messages directly. Same semantics; ~5 lines. - callAnthropicWithRetry: POSTs to <base>/v1/messages with the canonical Anthropic headers (content-type, x-api-key, anthropic-version: 2023-06-01) and body shape ({ model, max_tokens, system, messages, stream:false }). One retry on transient (HTTP 429 or 5xx); on terminal failure throws FinalizeUpstreamError carrying the upstream HTTP status and raw body text. The route handler in Phase I maps status to AUTH_FAILED / RATE_LIMITED / UPSTREAM_FAILED and runs the body through redactSecrets before exposing it as `details`. - extractDesignMd: concatenates content[].text for every block where type === 'text', preserving order. Throws FinalizeUpstreamError(502) on three malformed-response shapes: non-object payload, missing content array, zero text blocks. The route handler maps the throw to 502 UPSTREAM_FAILED so synthesis cannot land a half-empty DESIGN.md on disk. - Test-only `_sleepMs` injection on the call params so the retry-delay sleep is instant under vitest. Default sleep uses setTimeout. Retry posture (1 retry on transient) is opinionated; the maintainer's "standard exponential backoff" answer was directional and a single retry matches the existing daemon's posture (transcript export and connectionTest do zero retries) while staying inside the daemon's blocking-fast posture for /finalize. Tests: - callAnthropicWithRetry: throws on 401 with no retry; retries once on 429 and resolves on second 200; throws after both 5xx attempts; propagates AbortError when signal is pre-aborted. - extractDesignMd: concatenates ordered text blocks; throws on missing content array; throws on content with zero text blocks. A spurious typecheck error from `exactOptionalPropertyTypes` (signal typed as AbortSignal | undefined where RequestInit expects AbortSignal | null) was resolved by conditionally spreading signal into the RequestInit literal. Suite delta: +7 tests in finalize-design.test.ts (15 active total). Refs nexu-io/open-design#450 * feat(daemon): wire /finalize pipeline end-to-end Phase H of the PR 2 plan for issue #450: stitches together every phase D-G primitive into the full finalizeDesignPackage pipeline that the route handler in Phase I will expose over HTTP. Pipeline (in execution order, all inside a try/finally that always releases the lockfile): 1. getProject(db, projectId): defensive 404 (the route validates first; this throw catches direct CLI/script callers). 2. mkdirSync(<projectDir>, { recursive: true }): some projects have DB rows but no on-disk dir yet (PR #493's same fix). 3. fs.openSync(.finalize.lock, 'wx'): EEXIST -> FinalizePackageLockedError (mirror PR #493's TranscriptExportLockedError). 4. exportProjectTranscript(db, projectsRoot, projectId, { now }): produces .transcript.jsonl on disk; we read the body and run it through truncateTranscriptForPrompt to bound the prompt-inclusion size. 5. readDesignSystem(designSystemsRoot, designSystemId): returns null when the project has no design_system_id selected, when the design system directory does not exist, or when the DESIGN.md file is missing. 6. resolveCurrentArtifact(db, projectsRoot, projectId): active tab -> newest .artifact.json by manifest.updatedAt -> null. 7. buildSynthesisPrompt({...}): system + user prompt (per Phase F). 8. callAnthropicWithRetry({...}): one retry on 429/5xx; throws FinalizeUpstreamError on terminal failure. 9. extractDesignMd(payload): concatenates content[].text blocks; throws FinalizeUpstreamError(502) on malformed shape. 10. Atomic write: writeFileSync({flag:'wx'}) -> reopen for fsync -> rename. Errors unlink tmp before rethrowing. 11. Lock release in finally (always closeSync + unlinkSync). Bounded blocking: the function uses its own AbortController + 120s timeout when the caller does not supply a signal. Caller-supplied signal takes precedence. Type tightening: switched the local Db interface to `type Db = Database.Database` (better-sqlite3) so the function signature is compatible with `exportProjectTranscript`'s typed parameter. Source file already had a `better-sqlite3` import in claude-design-import area of the daemon, so no new dependency. Tests: - "writes DESIGN.md atomically on the happy path": end-to-end with seeded project + conversation + 2 messages + design system on disk; asserts file at exact path + body bytes match the fetch mock. - "response carries every documented field with correct types": designMdPath/bytesWritten/model/inputTokens/outputTokens/artifact/ transcriptMessageCount/designSystemId all present and typed. - "emits design system 'none' in the prompt when no design_system_id is set": fetch mock asserts on the body it receives. - "throws FinalizePackageLockedError when .finalize.lock is already held": pre-create lockfile; assert throw + DESIGN.md not written + pre-existing lock NOT unlinked (we did not own it). - "replaces an existing DESIGN.md atomically on a second finalize": inject a sentinel between two finalize calls; assert sentinel is gone after second run. - "cleans up tmp file AND lock file on every error path": mock fs.writeFileSync to throw on the tmp path; assert no DESIGN.md.tmp.* remain, no DESIGN.md, no .finalize.lock. - "uses the default https://api.anthropic.com baseUrl when baseUrl is omitted": fetch URL begins with the default; baseUrl=undefined path. vi.restoreAllMocks() now runs in afterEach so the writeFileSync spy from the cleanup test does not leak into subsequent tests. Suite delta: +7 tests in finalize-design.test.ts (22 active total). Refs nexu-io/open-design#450 * feat(daemon): /finalize HTTP route handler + error mapping Phase I of the PR 2 plan for issue #450: replaces the Phase C stub's catch-all 500 with status-aware error mapping that surfaces the right HTTP status + error code for each documented failure mode, and adds HTTP-layer tests that boot startServer to exercise the route's validation branches. Route handler changes: - :id format guard: an inline regex matching isSafeId at apps/daemon/src/projects.ts:556-558 rejects unsafe ids with 400 BAD_REQUEST before any DB or filesystem work. Without this, an id like 'bad!id' would either fail getProject as 404 (wrong code) or reach the function and throw 'invalid project id' (mapped to 500). - FinalizeUpstreamError mapping is now status-aware: - upstream 401 -> 401 AUTH_FAILED - upstream 429 -> 429 RATE_LIMITED - upstream 5xx (or our own 502 sentinel for malformed responses) -> 502 UPSTREAM_FAILED In all cases the upstream raw text is run through redactSecrets so the apiKey cannot leak through `details` even if the upstream echoes the inbound headers. - AbortError mapping: when the 120s AbortController fires (or the caller pre-aborted the signal), surface as 503 TIMEOUT. - Default case: console.error the error per daemon convention; client sees 500 INTERNAL with the message routed through redactSecrets. - Imported redactSecrets alongside the existing connectionTest imports (apps/daemon/src/server.ts:51). HTTP-layer tests (boot startServer({port:0,returnServer:true}) once in beforeAll, mirror the proxy-routes.test.ts pattern): - "400 BAD_REQUEST when baseUrl is not a valid URL (test #13)": baseUrl='not-a-url'. - "403 FORBIDDEN when baseUrl points at a private internal IP (test #14)": baseUrl='http://10.0.0.1'. Note: validateBaseUrl explicitly allows loopback (for local OpenAI-compatible servers) and only blocks non-loopback private IPs (10/8, 172.16/12, 192.168/16, fc00::/7, fe80::/10). - "400 BAD_REQUEST when apiKey is missing (test #15)": apiKey omitted. - "400 BAD_REQUEST when :id contains characters outside the safe-id regex (test #16)": id='bad!id' contains '!' which is not in [A-Za-z0-9._-]. Suite delta: +4 tests (26 active in finalize-design.test.ts). Full daemon suite: 1078/1078 pass; baseline+26 (the +5 above plan target reflects retry+extract split into more granular unit tests than originally enumerated; all real, none skipped). Refs nexu-io/open-design#450 * fix(daemon): tighten isSafeId to reject pure-dot project ids Addresses the P1 path-traversal finding from @lefarcen on PR #832 (https://github.com/nexu-io/open-design/pull/832#discussion_r3202512644). The pre-fix `isSafeId` at apps/daemon/src/projects.ts:556-558 used regex `/^[A-Za-z0-9._-]{1,128}$/` which permitted pure-dot ids (`.`, `..`, `...`) because `.` is in the character class. `projectDir` and `resolveProjectDir` both delegated to `isSafeId`, so an id of `..` would resolve to the PARENT of `.od/projects/` via `path.join`. Threat model (per @lefarcen): - An attacker creates a project row whose stored id is `..` (or another pure-dot variant) — for instance via a workflow that writes the row directly without going through the API. Subsequent finalize/write ops keyed by that id then escape the project tree. - A direct CLI / scripted caller passing `..` as the project id reaches the function without HTTP normalization saving us. (Express normalizes %2e%2e to .. and collapses path segments, which yields 404 for the URL `/api/projects/%2e%2e/...` in practice — but that's Express's protection, not ours.) Fix: - isSafeId now explicitly rejects pure-dot ids (`/^\.+$/.test(id)`) before the char-class regex check. Empty string and inputs longer than 128 chars are also rejected explicitly so the function fails closed on edge cases. - isSafeId is now exported from apps/daemon/src/projects.ts so the /finalize route handler in apps/daemon/src/server.ts can use the same validator instead of re-implementing the regex inline. This prevents drift between the route guard and the projectDir guard, which was how this hole originally appeared. Tests (in finalize-design.test.ts because that's where the threat was flagged; isSafeId is daemon-wide so a dedicated test file would also work): - isSafeId rejects `.`, `..`, `...`, `....` - isSafeId rejects ids with `/`, `\`, `!`, leading whitespace - isSafeId rejects empty string and >128 chars - isSafeId rejects non-string inputs (null/undefined/number) - isSafeId accepts plain ids, ids with mid-string dots, UUIDs, single chars Suite delta: +7 tests (33 active in finalize-design.test.ts). Full daemon suite: 1085/1085. Refs nexu-io/open-design#832 * fix(daemon): address PR #832 P1 findings — imported folders + network 502 Addresses two of the three P1 findings from @lefarcen on PR #832: 1. Imported-folder projects route DESIGN.md to metadata.baseDir (https://github.com/nexu-io/open-design/pull/832#discussion_r3202512656, also flagged independently by @chatgpt-codex-connector at #discussion_r3202430470) The pipeline previously called `projectDir(projectsRoot, projectId)` unconditionally, which resolves to `.od/projects/<id>`. For projects created via /api/import/folder the project row's `metadata.baseDir` carries the user's actual folder; without threading metadata through, finalize would silently land DESIGN.md in the hidden daemon data dir and the current-artifact resolver would miss the user's real files. Fix: switch from `projectDir` to `resolveProjectDir(projectsRoot, projectId, metadata)` in both `finalizeDesignPackage` and `resolveCurrentArtifact`. Thread `project.metadata` (from `getProject`'s normalized row) through both call paths. The resolver gets a new optional `metadata` parameter; native projects pass null and get identical behavior. 2. Network failures and JSON parse errors now map to 502 UPSTREAM_FAILED (https://github.com/nexu-io/open-design/pull/832#discussion_r3202512661) Pre-fix, only HTTP-non-OK responses were wrapped as FinalizeUpstreamError. DNS failures (ECONNREFUSED, ENOTFOUND), fetch TypeErrors, and `response.json()` SyntaxErrors fell through to the route's catch-all and surfaced as 500 INTERNAL — incorrect: those are upstream-level failures, not daemon bugs. Fix: - Wrap callAnthropicWithRetry in a try/catch that passes FinalizeUpstreamError and AbortError through verbatim, but rewraps any other thrown error as FinalizeUpstreamError(502, '', message). - Wrap response.json() in a try/catch that rewraps SyntaxError as FinalizeUpstreamError(502, '', "upstream Anthropic returned non-JSON body: ..."). - The route handler's existing FinalizeUpstreamError mapping then correctly maps these to 502 with the message in `details` (run through redactSecrets first). Tests: - "writes DESIGN.md under metadata.baseDir for imported-folder projects": inserts a project row with metadata.baseDir pointing at a user-folder temp dir; asserts result.designMdPath lands there AND the hidden .od/projects/<id> dir does NOT contain a DESIGN.md. - "rewraps fetch network rejection as FinalizeUpstreamError(502)": fetchImpl throws TypeError with cause.code='ENOTFOUND'; assert thrown error has name=FinalizeUpstreamError and status=502. - "rewraps 200 with non-JSON body as FinalizeUpstreamError(502)": fetchImpl returns 200 with text/html body; response.json() throws SyntaxError internally; assert FinalizeUpstreamError(502). Suite delta: +3 tests (36 active in finalize-design.test.ts). Full daemon suite: green at last check; will re-verify before push. Refs nexu-io/open-design#832 * refactor(daemon): move /finalize DTOs to contracts + map error codes + validate active-tab Addresses the P2 and P3 findings from @lefarcen on PR #832: P2 — Error codes + DTOs not in packages/contracts https://github.com/nexu-io/open-design/pull/832#discussion_r3202512673 Reverses my plan's locked decision #10 ("no contracts changes in this PR; inline the request/response types"). That rule came from the predecessor PROMPT brief's anti-pattern table; @lefarcen's review is fresher signal and supersedes it. Drift risk between the daemon's inline types and any future PR 3 web client is real. - New contracts module: packages/contracts/src/api/finalize.ts with FinalizeAnthropicRequest / FinalizeArtifactRef / FinalizeAnthropicResponse. Re-exported from the package root and made addressable via `@open-design/contracts/api/finalize` subpath. - Daemon source imports the canonical types from contracts and re-exports the public type names so internal references keep working without touching every call site. - Daemon-local error codes remapped to existing ApiErrorCode union members (apps/daemon/src/server.ts), per @lefarcen's suggested mapping: FINALIZE_IN_PROGRESS -> CONFLICT AUTH_FAILED -> UNAUTHORIZED UPSTREAM_FAILED -> UPSTREAM_UNAVAILABLE TIMEOUT -> UPSTREAM_UNAVAILABLE (status 503) INTERNAL -> INTERNAL_ERROR HTTP status codes are unchanged; only the `code` field in the error JSON body changed. P3 — Active-tab name not validated before sidecar probe https://github.com/nexu-io/open-design/pull/832#discussion_r3202512684 resolveCurrentArtifact now runs the active tab's name through validateProjectPath BEFORE composing it into a path.join expression. An invalid tab (traversal segments, absolute path, null byte, reserved segment) causes resolveCurrentArtifact to fall through to the newest-artifact branch rather than abort or probe outside the project directory. Tests: - "falls through (does not throw) when active tab name contains traversal segments": injects a malformed `tabs.name = '../../../etc/passwd'` row directly via SQL (bypassing production tab-creation validation), seeds a real artifact, asserts the resolver returns the real artifact rather than the malformed name. Suite delta: +1 test (37 active in finalize-design.test.ts). Full daemon suite: 1089/1089 green. Refs nexu-io/open-design#832 * fix(contracts): publish /api/finalize as standalone runtime entrypoint Addresses @mrcfps's CI-red review on PR #832 (https://github.com/nexu-io/open-design/pull/832, inline comment on packages/contracts/package.json). The previous J3 commit added `./api/finalize` as a type-only subpath: the entry had only a `types` field, no `default`. That broke the contracts package-runtime gate (packages/contracts/tests/package- runtime.test.ts:38-47) which asserts every exports entry exposes both a `.mjs` runtime and a `.d.ts` types target. mrcfps proposed two fixes; this commit takes path B — make finalize a first-class published module rather than a type-only re-export from the package root. Path B vs path A (a peer-AI second opinion via /collaborate confirmed): under NodeNext + ESM with exports-map semantics, TypeScript validates re-exported symbols against the published module-identity surface. Because the previous J3 had `./api/finalize` neither declared as an exports-map entry nor materialized as a standalone .mjs, TS omitted the re-exported names during package boundary analysis. Even at runtime `import('@open-design/contracts').FINALIZE_SCHEMA_VERSION` worked from the bundled index.mjs but the type-checker rejected it. Path B aligns the runtime and declaration surfaces. Changes: - packages/contracts/esbuild.config.mjs: add `./src/api/finalize.ts` to entryPoints so dist/api/finalize.mjs is generated as a standalone module rather than only inlined into the bundled root. - packages/contracts/package.json: re-add `./api/finalize` to the exports map with both `default: ./dist/api/finalize.mjs` AND `types: ./dist/api/finalize.d.ts`. Mirrors `./api/connectionTest`'s shape (the canonical pattern for first-class submodule entries). - packages/contracts/src/api/finalize.ts: keep the runtime export `FINALIZE_SCHEMA_VERSION = 1` (giving the standalone module a real value to emit beyond the type-only interfaces) and update the doc-comment now that the standalone .mjs is wired. - apps/daemon/src/finalize-design.ts: switch the type import from the inline declarations introduced in the prior J3 fallback to `import type { ... } from '@open-design/contracts/api/finalize'`. Re-export the names so internal references inside finalize-design.ts keep working without touching every call site. Verified: - node --input-type=module -e "import('@open-design/contracts/api/finalize').then(m=>console.log(JSON.stringify(Object.keys(m))))" prints ["FINALIZE_SCHEMA_VERSION"] — runtime resolution clean. - pnpm --filter @open-design/contracts test: 6/6 (including both package-runtime.test.ts cases on the rebuilt exports map). - pnpm --filter @open-design/daemon typecheck: exits 0. - pnpm --filter @open-design/daemon test: 1089/1089 (no regression vs the prior J3 number). Refs nexu-io/open-design#832 --------- Co-authored-by: DevForgeAI CI/CD Engineer <devforge-ai@development.ai> |
||
|
|
1e8926271b |
Harden security scan findings and upgrade dependencies (#806)
* feat: add accent color control and launcher for Open Design * fix: remove launcher binary from PR * test: cover accent appearance edge cases * Harden security scan findings and upgrade deps * Address proxy security review * Pin jsdom for web test stability --------- Co-authored-by: ferasbusiness666 <ferasbusiness666@users.noreply.github.com> Co-authored-by: lefarcen <935902669@qq.com> |
||
|
|
d592f6087f |
feat(mcp): external MCP client with daemon-managed OAuth and 39 design-focused templates (#898)
* feat(mcp): add external MCP client with daemon-managed OAuth and 17 design-focused templates
Open Design now acts as an MCP CLIENT and surfaces tools from third-party
MCP servers to the underlying agent (Claude Code, Hermes, Kimi).
Daemon
- New mcp-config / mcp-oauth / mcp-tokens modules: persist server entries
to .od/mcp-config.json, run the OAuth dance for HTTP/SSE servers
end-to-end on the daemon (so cloud deployments work and tokens
survive across turns), and inject Authorization: Bearer headers into
the per-spawn .mcp.json the daemon writes for Claude Code (or the
ACP mcpServers map for Hermes/Kimi).
- /api/mcp/servers and /api/mcp/oauth/{start,status,disconnect}
endpoints, plus spawn-time wiring in agents that hands the configured
servers to the active agent CLI.
- System-prompt directive for connected external MCPs so the model
does not chase Claude Code's synthetic *_authenticate /
*_complete_authentication tools when the Bearer is already pinned.
Web
- Settings -> External MCP servers panel with per-row OAuth Connect /
Disconnect / Refresh affordances and per-row template hints.
- New "Add server" picker categorized into 7 groups
(image-generation, image-editing, web-capture, ui-components,
data-viz, publishing, utilities) with a search box, sticky close
button, collapsible <details> sections (auto-expand on search),
60vh capped scroll region, and a pinned Custom-server footer.
- ChatComposer /mcp slash and MCP picker button forward to the new
Settings tab; AssistantMessage renders MCP tool calls inline;
markdown autolinker handles bare http(s) URLs (incl. OAuth links)
before italic markers so OAuth callback URLs do not get
italic-fragmented mid-token.
Contracts
- packages/contracts/src/api/mcp.ts owns the wire shapes
(McpServerConfig, McpTemplate with stable McpTemplateCategory
enum, McpServersResponse, OAuth start/status/disconnect bodies, the
postMessage payload from the OAuth callback).
Templates (17 built-in)
- image-generation: Higgsfield (OpenClaw, OAuth HTTP), Pollinations,
Allyson (animated SVG), AWS Bedrock Image (uvx).
- image-editing: Imagician, ImageSorcery.
- web-capture: just-every screenshot-website-fast, ScreenshotOne.
- ui-components: 21st.dev Magic, shadcn/ui, FlyonUI.
- data-viz: AntV Chart, Mermaid.
- publishing: EdgeOne Pages.
- utilities: Filesystem, GitHub, Fetch.
Tests
- apps/daemon/tests/mcp-{config,oauth,tokens,spawn}.test.ts cover
storage round-trip, OAuth helpers, token persistence, spawn-time
wiring, every template's transport / command / args / env-field
invariants, and the canonical category enum.
- apps/web/tests/runtime/markdown.test.tsx covers the new autolinker
ordering rules.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(mcp): add 21 more design-focused templates and a `design-systems` category
Expands the built-in MCP picker from 17 to 38 templates so users can compose
the full Open Design craft loop (design-system intake → generate → edit →
audit → publish) without leaving the Settings dialog. Every install spec is
verified live against the upstream README; templates that needed Go binaries,
multi-step `init` ceremonies, or massive runtime stacks (PostgreSQL + Redis
+ Ollama) are intentionally deferred so picking a template still resolves to
a working server in one click.
New `design-systems` category between `web-capture` and `ui-components`
(reflects the upstream-of-components position in the workflow). Mirrored in
`McpTemplateCategory` on both contracts and daemon, and `CATEGORY_ORDER` on
the web side.
New templates by category:
- image-generation (+4): prompt-to-asset (icons / favicons / OG / logos with
free-tier routing across Cloudflare AI / NVIDIA NIM / HF / Stable Horde),
Nano Banana (hosted streamable HTTP, virtual try-on + product placement),
Seedream (hosted streamable HTTP, ByteDance Seedream v3-v5 + SeedEdit),
fal.ai (uvx, 600+ models incl. FLUX / Kling / Hunyuan / MusicGen).
- image-editing (+3): Photopea (34 layered-editor tools — closes the PSD
gap), Topaz Labs (AI upscale / denoise / sharpen), Transloadit (86+ media
pipeline robots).
- web-capture (+1): Pagecast (browser → demo GIF / MP4 with auto-zoom).
- design-systems (+4, NEW category): Figma-Context (Framelink, designs →
code), Design Token Bridge (Tailwind ⇄ CSS ⇄ Figma ⇄ M3 / SwiftUI / W3C
DTCG + WCAG contrast), Design System Extractor (Storybook scrape),
Aesthetics Wiki (cottagecore / dark-academia / y2k / … moodboards).
- data-viz (+2): MCP Dashboards (45+ chart types + KPI dashboards),
Excalidraw Architect (hand-drawn architecture diagrams).
- publishing (+6): PageDrop, PDFSpark, OGForge, QRMint, Slideshot
(HTML → PDF / PPTX / PNG with 7 themes), Deckrun (Markdown → PDF / video,
hosted free tier with no key required).
- utilities (+1): A11y axe-core (WCAG 2.0/2.1/2.2 + color-contrast + ARIA).
Tests cover every new template's wiring (command, args, env / header
required-vs-optional, secret flag), the category enum invariant, and
in-category declaration order for image-generation, design-systems and
publishing buckets where the order is what users see in the picker. 21 new
test cases pass; full mcp-config suite is green.
Templates intentionally deferred (documented in PR body): figma-use
(needs Figma desktop with --remote-debugging-port=9222), m-moire (multi-step
`memi suite init` + daemon ceremony), gemini-media-mcp + trident-mcp (Go
binaries — no npx / uvx path), Pixelle-MCP (full app with web UI + ComfyUI
backend), storybook-addon-mcp (lives inside user's Storybook, not standalone),
primitiv (multi-step init / build / serve), ReftrixMCP (PostgreSQL + Redis +
Ollama + DINOv2), narasimhaponnada/mermaid (overlap with peng-shawn).
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(mcp): add figma-use template (write designs from chat) under design-systems
figma-use is the natural counterpart to Figma-Context already in this PR:
where Framelink reads Figma designs into the model, figma-use writes back
into the canvas (90+ tools — create frames / text / components / variants,
render JSX into Figma, export PNG/SVG, query nodes via XPath, lint for
WCAG / auto-layout / hardcoded colors, analyze design systems).
Wired as an HTTP MCP template (`http://localhost:38451/mcp`) because
`figma-use mcp serve` only exposes HTTP — there's no stdio mode in the
upstream `serve.ts`. No API key. Two prerequisites the user owns are
spelled out in the description so picking the template still resolves to
a working server: (1) start Figma with `--remote-debugging-port=9222`
(or `figma-use daemon start --pipe` on Figma 126+), and (2) leave
`npx figma-use mcp serve` running in a terminal.
Inserted between `design-system-extractor` and `aesthetics-wiki` so the
design-systems category reads as a workflow: read existing design (Figma
Context) → translate tokens (Token Bridge) → extract from Storybook
(Extractor) → write back to Figma (figma-use) → break creative block
(Aesthetics Wiki).
Tests cover the new template's transport (`http`), endpoint URL, the
empty header-fields invariant (no auth required), and bump the
design-systems group order to include it.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(settings): i18n the External MCP / MCP server / Connectors sidebar entries and make the dialog header track the active section
The External MCP sidebar entry this PR introduces was hardcoded English
("External MCP / Add MCP tools (Higgsfield, GitHub…)"). Same for the
adjacent Connectors and MCP server entries. The dialog header was also
pinned to "Execution & model" copy, so opening Settings → External MCP
showed a header that lied about which section the user was on.
Adds six translation keys — `settings.connectorsTitle/Hint`,
`settings.mcpServerTitle/Hint`, `settings.externalMcpTitle/Hint` — and
translates them across all 17 locales (ar, de, en, es-ES, fa, fr, hu, id,
ja, ko, pl, pt-BR, ru, tr, uk, zh-CN, zh-TW).
`SettingsDialog` now derives the header title/subtitle from the active
section (11 sections total) instead of a single hardcoded pair, so each
section renders an honest header.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(e2e): pin level: 3 on dialog heading lookups for Pets and Connectors
CI's Validate workspace job (#1479) failed two Playwright cases with the
strict-mode violation:
getByRole('dialog').getByRole('heading', { name: 'Pets' })
resolved to 2 elements:
1) <h2>Pets</h2>
2) <h3>Pets</h3>
Same root cause as the unit-test fix already in this PR: the dynamic
dialog `<h2>` now echoes the section's own `<h3>` because the dialog
header tracks the active section. Disambiguate to `level: 3` so each
assertion still pins the section heading specifically (which is what
the test intends to verify).
Audit of the rest of e2e/ for `dialog.getByRole('heading', ...)` —
settings-api-protocol.test.ts looks for "OpenAI API" / "Anthropic API"
section h3s which never appear in the dialog `<h2>` (always
"Execution & model"), so those stay safe.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(mcp): bind OAuth refresh to the issuing client and skip stale tokens
Persist the OAuth client context (token endpoint, client_id, client_secret,
issuer, redirect_uri, resource) alongside the bearer token so refresh hits
the same client the refresh_token was bound to (RFC 6749 §6). The previous
refresh path re-ran beginAuth with a dummy OOB redirect URI, which kept
getOrRegisterClient from finding the original DCR client and made
providers reject the refresh on the next chat turn. Refreshes now reuse
the persisted endpoint/client pair directly.
Also stop injecting expired access tokens at spawn time when refresh is
unavailable or fails. Pinning a stale Bearer made every Claude MCP call
401 while the prompt still treated the server as connected; on that path
we now skip the entry and let the UI surface a reconnect.
Generated-By: looper 0.6.1 (runner=fixer, agent=claude-code)
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
b06f26a5fd |
test: strengthen e2e PR coverage (#796)
* test: strengthen e2e PR coverage * fix: address e2e PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: address e2e PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * ci: cache Windows packaged smoke builds * test: fake additional agent runtimes * fix: address e2e PR feedback Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix: address e2e PR feedback Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix: address e2e PR feedback Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix: address e2e PR feedback Route tools-pack mac starts through a launch-time packaged config override so portable packaged smoke runs keep using the namespace runtime root that inspect and logs expect. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix: address e2e PR feedback Fall back to the packaged app's embedded config when the build output config is missing so installed mac starts still work. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix: align packaged mac PR smoke with tools-pack runtime mode Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix: address e2e PR feedback Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix: address e2e PR feedback Keep blake3-wasm out of the packaged mac daemon prebundle so the standalone runtime loads the Cloudflare asset hasher from node_modules instead of crashing in ESM. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix: address e2e PR feedback Skip the portable mac launch override when the bundled packaged config is missing so installed fallback app targets can still boot with packaged defaults. Add a regression test covering the missing-config start path. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(pack): remove duplicate mac prebundle dependency key |
||
|
|
8fee22d358 |
Fix stuck chat runs and unintended cancels (#896)
* Fix stuck chat runs and unintended cancels * Harden chat run stall watchdog |
||
|
|
c0c1f6555c |
add support for VP_HOME environment variable in agent resolution (#859)
* feat: add support for VP_HOME environment variable in agent resolution - Introduced a new .node-version file to specify Node.js version. - Enhanced agent resolution tests to include scenarios for VP_HOME, ensuring proper handling of Vite+ global installs. - Updated platform code to resolve user-scoped home directories, allowing for custom Vite+ installations to be prioritized. - Added tests to verify that the resolution logic correctly honors the VP_HOME environment variable and integrates with existing user toolchain paths. * feat: enhance VP_HOME support in sidecars and platform - Updated the PACKAGED_CHILD_ENV_ALLOWLIST to include VP_HOME for environment variable forwarding. - Exported functions resolvePackagedChildBaseEnv and resolvePackagedPathEnv for better accessibility in tests. - Added tests to validate VP_HOME handling in packaged child environments and ensure correct path resolution. - Adjusted wellKnownUserToolchainBins to prioritize VP_HOME/bin in the toolchain path resolution. |
||
|
|
e14b8092ea |
feat: add Orbit activity summaries (#681)
* feat: add Orbit activity summaries * fix(orbit): make runs navigable while agent continues * fix(web): widen minimum chat panel * feat: support Orbit template selection * fix(daemon): avoid bogus skill side-file preflight * fix(web): collapse orbit artifact project cards * fix(web): preserve orbit project card titles * fix: improve Orbit run daily briefing * fix: handle Orbit digest data failures * fix: load Orbit templates and connector tools reliably * fix: keep Orbit summary counts consistent Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: apply Orbit template skill context * fix: cache and curate connector tools for Orbit * fix: align Orbit defaults and connector discovery * fix: simplify Orbit template settings * fix: move connectors into settings * fix: compact connector settings catalog * fix: address Orbit PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: address Orbit PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: address Orbit PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: address Orbit PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: address Orbit PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: address Orbit PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: address Orbit PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: address Orbit PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: prevent connector action button from stretching into pill The icon-only connect/disconnect buttons in the embedded connectors catalog inherited min-width: 92px / 106px from the non-embedded pill rules, overriding the 24px square sizing and causing the buttons to overlap the card head text. Reset min-width to 0 in the embedded icon-only rule so the compact square layout holds. * fix(web): align live artifact file rows * fix: clean up Orbit connector settings lifecycle Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix: address Orbit review regressions Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * feat(web): localize Orbit and connector settings * feat(web): gate Orbit runs without connectors * feat(web): refine connector settings UX * feat(web): safeguard Composio key clearing * fix(web): refresh Composio tool badges * feat(web): show connector logos * feat(daemon): localize Orbit prompt window * fix(daemon): clarify blocked connector callback closes * test(daemon): harden flaky async probes * fix(web): align Indonesian connector locale keys * test(web): align connector browser props * fix(web): preserve explicit credential clears Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): time out Composio logo proxy fetches Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): localize Indonesian connector settings copy Translate the new connector settings strings in the Indonesian locale and lock them with a regression test so this surface no longer silently falls back to English. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): preserve discovered connector tools Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): preserve onboarding autosave completion Keep settings autosave from clearing onboarding completion after the close gesture, and expose the desktop main types from source so workspace validation can typecheck packaged imports without a prior desktop build. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): defer Composio catalog cache hydration Load persisted Composio catalog data only after the runtime data directory is configured so startup cannot read another namespace's cache. Add a regression test that exercises the module-load singleton path. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): treat discovery completion independently Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): preserve latest settings draft on close Use the latest persisted settings draft when the dialog closes so onboarding completion does not race a stale daemon sync and overwrite newer Orbit/template selections. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): avoid syncing draft Composio key on Orbit run Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): localize Orbit settings copy Translate the new Indonesian Orbit and autosave strings so the settings UI no longer falls back to English and the locale regression stays covered. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): prefer fresh connector catalog state Keep refetched connector status/auth data authoritative while retaining discovery-only tool metadata so the connectors UI stays consistent after refreshes. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): declare Indonesian locale fallback keys explicitly Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): inline Indonesian fallback strings for CI Replace the Indonesian locale's per-key English lookups with explicit strings so workspace typecheck no longer depends on brittle build-mode resolution in CI. Add a regression test that blocks those per-key English lookups from reappearing in the CI-sensitive fallback sections. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): restrict proxied connector logos to image MIME types Reject non-image upstream logo responses so the daemon never serves third-party HTML from its localhost origin. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * test(e2e): align settings dialog regressions Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): decouple Orbit runs from media sync failures Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): keep SPA catch-all export-compatible Disable dynamic catch-all params for the exported SPA shell so Next.js static builds can emit the root route again. Add a regression test covering the route config against the web export mode. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): preserve Orbit config and workspace routes Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): block SVG in connector logo proxy Reject SVG and other unsafe proxied logo responses so third-party logo content cannot execute under the daemon origin, while keeping raster logo fetches working and making rejected responses non-cacheable. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): fall back to static catalog for empty cache Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): disable Orbit run before connector gate resolves Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(desktop): export shipped desktop types Point the desktop ./main type export at the generated declaration so installed consumers resolve the published file set. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): restore persisted question form selections Render historical submitted answers directly so reloaded question forms keep their locked selections visible. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): retry forced media sync autosave Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): keep Composio logo timeout through body read Keep the Composio logo fetch timeout active until the response body is fully consumed so stalled body reads abort and clear the inflight cache entry. Add a regression test that proves a delayed body read times out and the next request can recover.\n\nGenerated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): refresh Orbit gate after connector auth Re-check connector availability when the settings window regains focus so Orbit unlocks as soon as a connector finishes authenticating in the same settings session. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): keep connector detail tool lists intact Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): ignore malformed Orbit summaries Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(e2e): stabilize design-system multi-select flow Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): cap Composio logo cache growth Bound the Composio logo cache with LRU eviction and expired-entry pruning so repeated untrusted logo requests cannot grow daemon memory without limit. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): bound proxied Composio logo payloads Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): align autosave settings tests Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): remove stray CSS conflict marker Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fixer: address PR #681 follow-up items Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): restore restart routes and connector flows * fix(web): keep SPA export route static * fix(web): stabilize chat scroll tests --------- Co-authored-by: lefarcen <935902669@qq.com> |
||
|
|
369d136d19 |
Add Docker Compose deployment workflow (#65)
* Add Docker Compose deployment workflow * Address Docker deployment review feedback Harden publishing inputs and temporary credential handling, and tighten Docker runtime defaults requested by the PR review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix Docker publish build in CI mode Set CI=true during the image build so pnpm prune can run non-interactively inside Docker. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix Docker runtime dependency layout Use pnpm deploy for the daemon package so the runtime image includes production dependencies where Node resolves them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Use legacy pnpm deploy in Docker build Allow pnpm v10 deploy to package the daemon workspace without requiring injected workspace packages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Align Docker runtime with Node 24 Use Node 24 for both build and runtime stages and update image verification for the workspace daemon dependency layout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Remove legacy OD_HOST Docker binding fallback Use OD_BIND_HOST as the single daemon bind-host setting for Docker deployment and origin validation. * Update Docker image verifier for daemon dist runtime Check the packaged daemon dist entrypoint and allow npm from the Node 24 runtime image while still rejecting build-only tools. * Allow private LAN browser origins for daemon * Share daemon origin validation helpers Move browser origin validation into a shared daemon module so tests exercise the production logic and cover the remaining private LAN edge cases. * Harden Docker Compose port exposure Bind the Compose deployment to localhost by default and pass the published port through to the daemon origin checks so host-port overrides remain same-origin. * Keep deployment hosts out of local-only no-origin checks Require an actual matching Origin before configured deployment origins can satisfy local-only daemon guards, preventing no-Origin remote clients from bypassing those checks. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: mrcfps <mrc@powerformer.com> Co-authored-by: lefarcen <935902669@qq.com> |
||
|
|
665e52b295 |
fix(daemon): pin OD_DATA_DIR in /api/mcp/install-info env so the macOS-packaged MCP server does not EPERM on .od/projects (#857)
* fix(daemon): pin OD_DATA_DIR in /api/mcp/install-info env so spawned MCP processes do not fall back to .od inside the macOS app bundle Reporter (#848) ran a packaged Open Design 0.5.0 on macOS and pointed Antigravity's MCP config at the bundle's daemon-cli.mjs. The MCP process is launched by the IDE outside the packaged app's environment, so it does not inherit OD_DATA_DIR. The daemon-cli import path runs mkdirSync('<cwd>/.od/projects') before dispatching to MCP mode, and <cwd> resolves to the read-only macOS app bundle, hitting EPERM. The /api/mcp/install-info endpoint already serializes env into every client snippet (Cursor, Claude Code, VS Code, Zed, Windsurf, Antigravity, Codex). Add OD_DATA_DIR: RUNTIME_DATA_DIR to that env so the snippet pins the daemon's resolved data root, and the spawned MCP process writes to the same directory the daemon already uses regardless of how the IDE was launched. Test added asserts env.OD_DATA_DIR is propagated. * refactor(daemon): extract buildMcpInstallPayload so the test asserts the production helper, not a fixture mirror Reviewer flagged that the previous test asserted env.OD_DATA_DIR on a copy of the handler's payload-construction logic, which would silently pass if the real handler ever diverged from the fixture. Move the env / args / buildHint shape into a pure exported helper (apps/daemon/src/mcp-install-info.ts), wire both server.ts and the test fixture through it, and drop the inline duplicates. The test now exercises the same code path that ships, so any regression in the env block (missing OD_DATA_DIR, wrong format, lost ELECTRON_RUN_AS_NODE) fails it. --------- Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
6de802ba70 |
feat(daemon): add critique interrupt endpoint + project-keyed run registry (Task 6.1) (#819)
Phase 6.1 of the Critique Theater rollout: a single new endpoint and the in-process registry that backs it. POST /api/projects/:projectId/critique/:runId/interrupt cascades an AbortController to the orchestrator that owns the spawned CLI so the parser can flush best-so-far state and emit critique.interrupted before the process exits. Backed by a new in-process run registry that the orchestrator wiring registers each run into before runOrchestrator is invoked, and unregisters in a finally block. The registry is keyed by (projectId, runId), not just runId. A request to interrupt project p1's runId cannot find or abort a registry handle that belongs to project p2 even if their ids ever collide. The HTTP handler also performs its own DB-row projectId check before calling the registry, so cross-project leakage is blocked at two layers. The endpoint is idempotent on already-interrupted rows: a client that lost the first response and retries observes 202 with prevStatus "interrupted" rather than a 409 conflict. Other terminal statuses (shipped, failed, timed_out, degraded, below_threshold, legacy) still return 409 because those runs reached their real terminal state on their own and an interrupt is no longer meaningful. Recovery path for stale running rows: when registry.interrupt returns false (the in-process registry has no AbortController for this projectId/runId pair) but the DB still says 'running', the endpoint marks the row 'interrupted' directly with recoveryReason='no_live_handle' and returns 202 with recovered=true. This window opens after a daemon restart in the gap before reconcileStaleRuns sees the row old enough. Without the recovery branch the endpoint would lie: 202 accepted, no child signaled, no critique.interrupted event, row stuck running. The new persistence helper markRunInterruptedRecovery mirrors the per-row write reconcileStaleRuns already does, gated on status='running' so a row that just transitioned terminal is not overwritten. Task 6.2 (rerun endpoint) is intentionally not in this PR. The earlier draft conflated row insertion between the handler and runOrchestrator (primary key collision) and did not actually start a new agent spawn. Rerun needs a real chat-run path with prior-art context, an artifact-id validator, and SQL LIKE escaping that the row lookup path is missing today; it is cleaner shipped as a follow-up than wedged into this PR. Tests: - critique-run-registry: 17 cases covering register, get, interrupt, unregister, list, plus the new (projectId, runId) composite key invariants (cross-project register, cross-project get/interrupt isolation, unregister keying). - critique-interrupt-endpoint: 17 cases covering 202 happy path, 404 on unknown run, 404 on cross-project run, 404 cross-project leak guard at the registry layer, 409 on terminal statuses, 202 idempotent retry on already-interrupted, stale-handle defense, 202 + recovered on a stale running row with no live handle, 400 on bad params. Incidental: apps/web/src/i18n/locales/id.ts was missing 18 fileViewer deploy/Cloudflare keys after upstream landed PR #805 (R2 release publishing). Without those keys the workspace web typecheck fails on the i18n Dict equality check, blocking CI on every PR. Added Indonesian translations for the missing keys to unblock. Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
2eae7da24b |
feat: support Cloudflare Pages custom domains (#851)
* Support Cloudflare Pages custom domains without hiding pages.dev fallback Keep the default Pages preview as the first public link while optional owned-zone binding provisions DNS and Pages custom-domain state in parallel. Constraint: Cloudflare deploys must use the existing direct-upload API path with no Wrangler dependency. Constraint: pages.dev must stay visible even while custom-domain verification is pending. Rejected: Vercel custom-domain support | outside requested Cloudflare-only scope. Rejected: overwriting arbitrary CNAME records | risks taking over user-managed DNS. Confidence: high Scope-risk: moderate Directive: Do not expose providerMetadata through public deploy contracts; keep custom-domain DNS ownership checks conservative. Tested: pnpm --dir apps/daemon exec vitest run -c vitest.config.ts tests/deploy.test.ts tests/deploy-routes.test.ts Tested: pnpm --filter @open-design/contracts build && pnpm --filter @open-design/contracts typecheck && pnpm --filter @open-design/contracts test Tested: pnpm --filter @open-design/web typecheck && pnpm --filter @open-design/web test -- providers/registry.test.ts components/FileViewer.test.tsx i18n/locales.test.ts Tested: pnpm i18n:check && pnpm guard && pnpm typecheck Tested: pnpm --filter @open-design/daemon build && pnpm --filter @open-design/web build && git diff --check Not-tested: real Cloudflare account/token/domain smoke test * Preserve Cloudflare fallback correctness under large accounts and races Constraint: Cloudflare Pages keeps pages.dev as the primary usable fallback while custom domains remain optional typed metadata. Rejected: Treating custom-domain DNS or binding failure as a top-level deployment failure | pages.dev can still be ready and usable. Confidence: high Scope-risk: moderate Directive: Keep custom-domain finality tied to Cloudflare Pages API active status plus URL reachability; do not expose providerMetadata. Tested: pnpm --dir apps/daemon exec vitest run -c vitest.config.ts tests/deploy.test.ts tests/deploy-routes.test.ts; pnpm --filter @open-design/web test -- components/FileViewer.test.tsx i18n/locales.test.ts providers/registry.test.ts; pnpm --filter @open-design/daemon typecheck; pnpm --filter @open-design/web typecheck; pnpm i18n:check; git diff --check; pnpm guard; pnpm typecheck; pnpm --filter @open-design/daemon build; pnpm --filter @open-design/web build Not-tested: Real Cloudflare token/account/zone smoke test. * Keep impeccable design notes local Constraint: .impeccable.md is local assistant/design context and should not be part of the PR diff. Rejected: Keeping the file tracked while adding it to .gitignore | tracked files are not ignored by Git. Confidence: high Scope-risk: narrow Directive: Keep .impeccable.md untracked and ignored; do not rely on it for required project documentation. Tested: git check-ignore -v .impeccable.md; git diff --check Not-tested: Full workspace tests not rerun for ignore-only metadata change. |
||
|
|
959bfaa817 |
fix(daemon): make MCP install snippet survive daemon port changes (#846)
* fix(daemon): make MCP install snippet survive daemon port changes `od mcp` now discovers the live daemon URL via the sidecar IPC status socket on every spawn, so the Settings -> MCP server snippet no longer bakes in `--daemon-url <port>`. Pasted client configs stay valid across daemon restarts even when the daemon binds an ephemeral port (tools-dev, packaged). Resolution order is --daemon-url > OD_DAEMON_URL > IPC discovery > http://127.0.0.1:7456 so explicit overrides still win for direct `od` launches. * fix(daemon): MCP snippet works in non-default namespaces and direct launches Propagate OD_SIDECAR_NAMESPACE / OD_SIDECAR_IPC_BASE into the snippet env so non-default namespace daemons stay reachable; the spawned MCP client does not inherit the daemon's env, so without this it would probe the default-namespace socket and miss. Restore --daemon-url in the snippet for direct `od --port X` launches that have no IPC socket. Reword `od mcp --help` so it does not imply live URL tracking; each new spawn rediscovers, but a running MCP server caches the URL until the client restarts. |
||
|
|
56bf6ee1b6 |
feat: agent-callable research command and /search (#615)
* feat: pre-generation research (Tavily) for grounded generation
Adds an optional pre-generation research step so the agent can produce
slides / prototypes / decks grounded in real sources instead of guessing.
User flow:
1. Settings -> Tavily Search -> paste API key (or set TAVILY_API_KEY).
2. Click the new Research button in the chat composer.
3. On send, the daemon runs a Tavily search, prepends the findings
as a <research_context> block ahead of the system prompt, and
spawns the agent. Research progress shows up as status pills in
the chat stream; the agent cites sources inline as [1]/[2]/...
Phase 1 surface:
- Single provider (Tavily), single depth ('shallow'), no LLM
synthesis pass (Tavily's `answer` is the summary).
- Composer toggle only; no popover / depth picker yet.
- Reuses the existing `status` SSE agent payload + StatusPill UI
so no new event variants or renderer code are needed.
Layers touched:
- contracts: ResearchOptions / Source / Findings DTOs;
ChatRequest.research; export from index.
- daemon: apps/daemon/src/research/{index,tavily}.ts orchestrator
+ provider; tavily added to MEDIA_PROVIDERS and ENV_KEYS; hook
in startChatRun before prompt assembly.
- web: ChatComposer toggle + ChatSendMeta; threaded through
ChatPane / ProjectView / streamViaDaemon into ChatRequest.
Side fix (required to land the feature, but useful on its own):
contracts internal relative imports lacked the `.js` suffix that
NodeNext module resolution requires. This was already breaking
`pnpm --filter @open-design/daemon typecheck` on main; without the
fix, none of the new research types were visible to the daemon.
All internal contracts imports now carry `.js`.
Spec: specs/current/research-feature.md (phases 2-4 outlined for
follow-up: composer popover, multi-provider, deep recursion, example
skills with research_recommends).
Verified:
- pnpm --filter @open-design/contracts typecheck/test
- pnpm --filter @open-design/daemon typecheck (the chokidar
project-watchers test is a pre-existing flake, unrelated)
- pnpm --filter @open-design/web typecheck
- node scripts/verify-media-models.mjs
* fix(daemon): clamp Tavily max_results to 20
Tavily's /search endpoint requires `max_results` in [0, 20]; sending a
larger value (e.g. when `research.depth: "deep"` resolves to 30) returns
400 and `runResearch` silently falls back to no-research. Clamp at the
provider boundary so Phase 2 depth tiers above 20 still produce results
instead of failing the request.
Generated-By: looper 0.6.1 (runner=fixer, agent=claude-code)
* Remove stale research merge leftovers
* Add agent-callable research search
* Fix Indonesian locale typecheck
* Fix research command invocation edge cases
* Harden slash search prompt expansion
* Honor research source caps in command contract
* Require search reports in design files
* Add research data provider settings
* Wire web research provider fallback order
* Update research provider fallback wording
* Revert "Update research provider fallback wording"
This reverts commit
|
||
|
|
988fd6db5e |
feat: import existing local folder as project (#597) (#624)
* feat(contracts): types for folder-import endpoint
Add ImportFolderRequest, ImportFolderResponse to the public contract
surface. Extend ProjectMetadata with a baseDir field — when set, the
project's files live at this absolute path instead of .od/projects/<id>/.
Stored as the realpath() result so symlinks cannot redirect later writes.
Refs nexu-io/open-design#597
* feat(daemon): support metadata.baseDir for folder-rooted projects
Add resolveProjectDir() and metadata-aware variants of listFiles,
readProjectFile, writeProjectFile, ensureProject so a project's files
can live under metadata.baseDir (the user's chosen folder) instead of
.od/projects/<id>/. metadata.baseDir is opt-in — projects without it
keep the existing .od/projects/<id>/ behavior unchanged.
When listFiles walks a baseDir-rooted project, it skips conventional
build / install dirs (node_modules, .git, dist, build, .next, .nuxt,
.turbo, .cache, .output, out, coverage, __pycache__, .venv, vendor,
target, .od, .tmp) so the file panel stays focused on design content
instead of being dominated by lockfiles and node_modules.
Add detectEntryFile() — best-effort lookup for index.html or any
.html at the folder root, used by the import endpoint to seed the
initial active tab.
Refs nexu-io/open-design#597
* feat(daemon): add POST /api/import/folder endpoint
Creates a project rooted at the submitted local folder. metadata.baseDir
points at that folder and OD reads / writes there directly — no copy,
no shadow tree, mirroring how Cursor / Claude Code / Aider behave. The
user owns the workspace and is responsible for their own version
control.
Safety:
- baseDir is canonicalized via fs.promises.realpath() at import time so
user-controlled symlinks can't redirect later writes. resolveSafe
enforces the bounds check against the literal stored path; without
realpath, a symlink (e.g. ~/sneaky → /etc) would let writeProjectFile
escape the project tree at every later call because the OS follows
the symlink at open() time.
- Post-realpath lstat ensures the canonical target is itself a real
directory (defense-in-depth).
- The data directory (RUNTIME_DATA_DIR) and its descendants are
refused after symlink resolution so a redirect into the daemon's
own state can't masquerade as a project import.
The web client wires this through state/projects.ts → App.tsx,
landing the user on the auto-detected entry file when present.
Refs nexu-io/open-design#597
* feat(desktop): expose native folder picker to renderer
Adds an Electron preload script that exposes window.electronAPI.pickFolder
via contextBridge. Wires dialog.showOpenDialog through ipcMain so the
web UI can open a native folder selector for project import. Browser-only
users fall back to a text input for the absolute path (handled in the
web layer); the picker stays an optional convenience on the desktop
binary.
ipcMain.handle() registers handlers in an internal map that is not
exposed via eventNames(), so the natural-looking guard
if (!ipcMain.eventNames().includes('dialog:pick-folder')) ipcMain.handle(...)
is always true. On a second createDesktopRuntime() call (dev hot-reload,
packaged-vs-electron mode swap) the body re-runs and ipcMain.handle()
throws 'Attempted to register a second handler'. Use removeHandler()
+ handle() unconditionally — removeHandler() is a documented no-op
when nothing is registered, making the pair idempotent.
Includes *.cts in the apps/desktop tsconfig so the preload script is
typechecked.
Refs nexu-io/open-design#597
* feat(web): add 'From existing folder' option to New Project
UI surface for the import flow:
- A new 'Open folder' affordance in NewProjectPanel that uses the
native picker on Electron (window.electronAPI.pickFolder) and falls
back to an absolute-path text input in the browser.
- importFolderProject() in state/projects.ts: typed wrapper around
POST /api/import/folder using @open-design/contracts types.
- App.tsx wires the response: prepend the new project to the list,
navigate to it, and select the auto-detected entry file as the
active tab.
Skill / design-system pickers from the existing prototype tab are
reused — folder import is a project-creation flow, not a separate
project type.
Refs nexu-io/open-design#597
* docs(architecture): document folder-import endpoint
Adds POST /api/import/folder to the daemon API table and a 'Folder
import' section explaining the single-mode design (direct read/write
in metadata.baseDir, mirroring Cursor / Claude Code / Aider), the
realpath() canonicalization, the RUNTIME_DATA_DIR refusal, and the
SKIP_DIRS list applied to listFiles for baseDir-rooted projects.
Refs nexu-io/open-design#597
* test(daemon): unit + integration tests for folder import
Two new files:
apps/daemon/tests/folder-import-projects.test.ts (13 unit tests):
- resolveProjectDir behavior under all metadata combinations,
including the fallback when baseDir is relative and the
isSafeId-bypass when baseDir is set
- detectEntryFile: index.html priority, .html fallback, null when
no html, no descent into subdirs
- listFiles with metadata.baseDir: walk, SKIP_DIRS hides node_modules
/ .git / dist, back-compat for projects without baseDir
apps/daemon/tests/folder-import-route.test.ts (10 integration tests):
- Happy path: baseDir stored in metadata, importedFrom='folder',
conversation created, entry file detected
- Error paths: missing baseDir, empty, relative, non-existent,
pointing at a file
- Security: realpath canonicalization (the symlink test was the one
that surfaced the original /var vs /private/var mismatch in
RUNTIME_DATA_DIR comparison on macOS)
- Security: a symlink that resolves into RUNTIME_DATA_DIR is rejected
after realpath, not before
Refs nexu-io/open-design#597
* fix(daemon): wire baseDir metadata into chat + deploy reads
Two bugs caught in Codex automated review of #624:
1. chat-route was passing the metadata object directly as the listFiles
opts argument: `listFiles(PROJECTS_DIR, projectId, chatMeta)`. The
listFiles contract reads opts.metadata, not opts itself, so this
silently fell back to .od/projects/<id>/ instead of the imported
folder. existingProjectFiles was empty for baseDir-rooted projects.
Wrap as `{ metadata: chatMeta }`.
2. deploy.ts read project files via readProjectFile without the
metadata third argument, so for baseDir-rooted projects the deploy
and preflight endpoints would look in .od/projects/<id>/ and fail
with file-not-found instead of reading the imported folder. Thread
options.metadata through buildDeployFilePlan → readProjectFile and
pass project?.metadata at the two server.ts callsites
(`POST /api/projects/:id/deploy` and the preflight endpoint).
Add a regression test that locks the listFiles contract: passing a
bare metadata object as opts must NOT scan baseDir — it must fall back
to the standard project dir, otherwise callers can leak the wrong
folder by mistake.
Refs nexu-io/open-design#597, #624 (Codex review)
* fix(daemon): ensure correct metadata handling in folder import
Addressed issues with metadata handling in folder import functionality. Updated the listFiles and readProjectFile methods to correctly utilize the metadata.baseDir, ensuring that project files are read from the intended directory. Added regression tests to verify that passing a bare metadata object does not inadvertently scan the baseDir, maintaining the integrity of project file access.
Refs nexu-io/open-design#597
* fix(daemon): security hardening from Codex review of #624
P1 findings from automated review:
1. POST /api/projects + PATCH /api/projects/:id rejected
client-supplied metadata.baseDir. baseDir is privileged: it lets a
project root inside the user's filesystem, and the realpath() +
RUNTIME_DATA_DIR reentry checks live only on /api/import/folder.
Allowing it on the generic create/patch path lets an attacker
smuggle e.g. /etc through and bypass every import-time guard.
Both endpoints now refuse a baseDir field with 400.
2. resolveSafeReal() helper: realpath()s each candidate path (or its
longest existing prefix for write paths) and re-validates against
realpath(projectRoot). The original resolveSafe() only did a
string-prefix check, which was fooled by symlinks *inside* a
baseDir-rooted project. A repo containing 'assets -> /Users/me/.ssh'
passed the literal prefix check but readFile() followed the link
at open() time. resolveSafeReal() is now used by readProjectFile,
writeProjectFile, and deleteProjectFile.
3. Multer chat-upload destination now resolves to metadata.baseDir for
imported folder projects via a module-level lookup wired to db at
startServer() boot. Previously attachments landed in
.od/projects/<id>/ even for baseDir projects, so the agent (which
runs with cwd=baseDir) couldn't open them.
P2 findings:
4. searchProjectFiles threads metadata through listFiles +
resolveProjectDir so /api/projects/:id/search hits the right tree.
5. buildProjectArchive + buildBatchArchive now accept metadata so
'Download .zip' works for imported folder projects.
6. Watcher subscribe() resolves to baseDir for imported projects so
live-reload SSE actually fires when the user edits files in their
own folder. Registry stays keyed by the canonical directory.
7. Template snapshotting reads source-project files with metadata
so a template can be saved from a baseDir-rooted source.
Tests:
- Regression: POST /api/projects with metadata.baseDir → 400.
- Regression: descendant symlink (assets/leak.txt -> /etc/hosts) is
refused on the raw read endpoint.
Refs nexu-io/open-design#597, #624 (Codex P1+P2 review)
* fix(daemon): close two regressions found in #624 review round 2
@mrcfps caught two more correctness gaps:
1. Archive root symlink escape — buildProjectArchive accepts an optional
?root=<subdir> param to scope the zip to a subdirectory. The path was
resolved with the string-only resolveSafe(), so a directory symlink
inside an imported folder (docs -> /Users/me/.ssh) passed the prefix
check and collectArchiveEntries() then walked outside the project
tree. Switch to the symlink-aware resolveSafeReal() — the same one
that already protects raw read/write/delete paths. The walker itself
already skips dirent symlinks via !isDirectory && !isFile, so
canonicalizing the root is the only missing piece.
2. PATCH metadata wiped baseDir — updateProject() replaces metadata
wholesale. The previous guard only blocked an explicit baseDir
change, but a normal patch that *omits* baseDir (a UI editing
linkedDirs only sends { metadata: { kind, linkedDirs } }) silently
detached imported projects from their folder root. Subsequent
reads/writes/watch/deploy fell back to .od/projects/<id>.
Re-stamp the immutable folder-import fields (baseDir, importedFrom='folder')
from the existing project record onto the incoming patch when the
project is imported. A patch that supplies a *different* baseDir
still gets rejected as before; a patch that supplies the *same*
baseDir is accepted as a no-op. A patch on a non-imported project
that tries to set baseDir is also still rejected (preserves the
POST /api/projects guard from the previous round).
Tests:
- archive endpoint: ?root=<symlink-to-/etc> → 400.
- patch endpoint: PATCH that omits baseDir on an imported project keeps
baseDir intact (project still resolves to the user's folder after).
Refs nexu-io/open-design#597, #624 (Codex P1 round 2)
* fix(web): add Indonesian deploy provider copy
---------
Co-authored-by: INFINITY <valentyn.sotov@trendarena.app>
Co-authored-by: Siri-Ray <2667192167@qq.com>
|
||
|
|
bef8203ad9 |
fix: expand Codex picker coverage (#757)
* fix: add newer Codex model choices * fix: expand Codex picker coverage --------- Co-authored-by: leprincep35700 <leprincep35700@users.noreply.github.com> |
||
|
|
09eb88f683 |
Add Cloudflare Pages artifact deployment
Adds Cloudflare Pages artifact deployment support. |
||
|
|
8630fd380a |
feat(daemon): close pi adapter parity gaps
Closes pi adapter parity gaps for image paths, extra allowed dirs, error events, and sendAgentEvent routing. |
||
|
|
cb92c93ae0 |
Migrate beta release publishing to R2 (#805)
* Prebundle standalone web packaged runtime * Harden mac standalone prebundle policy * Prebundle mac daemon packaged runtime * Prune mac Electron locales * Maximize mac release artifact compression * Publish beta mac artifacts to R2 * Use remote R2 uploads for beta releases * Fail fast on beta R2 access issues * Use S3-compatible uploads for beta R2 releases * Decouple beta versioning from GitHub releases * Remove legacy beta metadata source * Address release beta review notes |
||
|
|
a8418ac730 |
Fix Windows link code folder dialog (#698)
* Fix Windows link code folder dialog * Add Windows folder dialog coverage * Complete Indonesian locale copy |
||
|
|
6efac8887e |
Improve Windows beta packaging and installer flow (#768)
* Optimize Windows packaged web output * Fix packaged contracts runtime build * Optimize Windows packaged size pruning * Prune Windows root Next payload * Remove Windows bundled Node runtime * Prune Windows standalone duplicate Next * Add tools-pack cache foundation * Cache Windows packaged build layers * Cache Windows workspace builds * Cache Electron-ready Windows app * Split Windows tools-pack module * Cache Windows dir build outputs * Split Windows pack build modules * Document Windows NSIS smoke namespace limits * Move Windows NSIS smoke note to agents guide * Optimize Windows beta packaging * Bump packaged beta base version * Improve Windows installer namespace UX * Improve Windows tools-pack cache keys * Stabilize Windows beta cache version keys * Cache Windows workspace build outputs * Optimize windows release beta cache layers * Cache windows release dependencies * Trim windows release cache before save * Refresh windows tools-pack cache key * Improve windows installer preflight prompts * Fallback NSIS installer strings to English * Fix Windows installer cleanup and preflight * Improve Windows NSIS state logging * Fix system NSIS Persian language alias * Use long-path removal for Windows uninstall * Fix mac tools-pack tests on Windows * Address Windows packaging review feedback * Fix Windows installer cache namespace isolation * Include web output mode in Windows tarball cache key * Use unique Windows release cache save keys |
||
|
|
bb2015766a | feat: Critique Theater Phase 5 (panel prompt template + system composer wiring) | ||
|
|
25a3ffd298 |
fix(daemon): add legacy data dir migrator
Add a one-shot OD_LEGACY_DATA_DIR migrator so packaged Desktop users can recover 0.3.x repo .od data into the 0.4.x data root. The migrator stages payloads before promotion, refuses unsafe merges and symlinks, rolls back failed promotion or marker writes, and extends packaged daemon startup handling for long migrations while failing fast on daemon exits. Closes #710 |
||
|
|
9b501f12a5 |
Support overriding the Codex executable path (#755)
* Support overriding the Codex executable path * Replace save-as-template prompts with an in-app dialog * Seed local packaged app config from workspace * Fix packaged config and connection test overrides * Keep tools-pack mac config seeding self-contained * Require absolute CODEX_BIN overrides |