mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
22 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f4c5d22f22
|
fix(daemon): confine sandbox project roots and host discovery (#3243)
* fix(daemon): confine sandbox project and host discovery * fix(daemon): resolve sandbox data dir for toolchain discovery * fix(daemon): resolve sandbox data dir for agent env * fix(daemon): fail fast for sandbox imported folders * test(daemon): assert sandbox imported folder rejection * fix(daemon): keep sandbox import guard at run start * fix(daemon): reject sandbox imported project file roots * fix(daemon): preserve imported project detail roots * test(daemon): expect sandbox profiles to stay scoped * fix(daemon): bypass proxies for agent tool callbacks * test(daemon): isolate media policy route memory extraction * fix(daemon): keep loopback no-proxy scoped to sandbox |
||
|
|
c7f55388a9
|
fix: ignore generated project dirs before watching (#2838) | ||
|
|
c80acfefeb
|
fix(daemon,web): block pitch-deck placeholder publishes and unbreak framework decks (#2384)
Two preview-time bugs surfaced ahead of 0.8.0: 1. Pitch-deck example (#2215): the official html-ppt-pitch-deck prompt asked the agent to confirm three facts first, but the manifest had no structured `od.inputs`, so the platform's required-input gate had no fields to enforce and the run could publish HTML that still contained unresolved fundraising placeholders (`Name to confirm`, `$X.XM`, `Replace this panel with`, ...). Add structured required inputs to the manifest and a daemon-side publication guard that rejects HTML/deck artifact writes whose body still contains those placeholders. Scope is the file-write boundary only (no assistant-text scanning), so the guard cannot trip on the agent's chat prose mid-clarification. 2. Framework deck preview off-screen: `injectDeckBridge` injected `place-content: center !important` on `.deck-shell` for every deck-mode srcdoc, which forced the framework's `display: grid` shell to re-center its implicit track. The framework's `fit()` already centers a `transform-origin: top left` stage with an explicit `translate(tx, ty)` that assumes the stage's natural layout position is (0, 0); the two centerings stacked and the scaled stage landed ~1000px off-screen, so the preview showed a sliver of slide content in the top-left with the rest black. Skip the override when the framework's `id="deck-stage"` marker is in the doc, and drop the dead `display: grid; place-items: center` from the deck framework template so future drift can't re-introduce the same stack. |
||
|
|
dc4d8cdddb
|
fix(daemon): reconcile missing artifact manifests (#2068)
* fix(daemon): reconcile missing artifact manifests Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): preserve reconciled artifact mtimes Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
7975514b3d
|
Add normal artifact creation via MCP and CLI (#2057) | ||
|
|
140a4e1ff6
|
Improve responsive preview and design handoff outputs (#1224)
* feat: improve responsive design handoff * feat: refine cross-platform design outputs Changelog:\n- Add auto-fit responsive preview behavior for tablet/mobile frames.\n- Add landing page and OS widgets metadata options with project header chips.\n- Strengthen prompt contracts for modern breakpoints, app-specific modules, CJX-ready UX, and final product surfaces.\n- Require cross-platform outputs to use separate platform files instead of tabbed demo selectors.\n- Add DESIGN-MANIFEST.json plus richer handoff guidance to daemon/client exports.\n- Update archive/export tests for manifest and responsive viewport matrix. * feat: enforce screen-file design outputs Changelog:\n- Enforce screen-file-first generation for landing pages, app screens, platform surfaces, and OS widgets.\n- Update design handoff and manifest exports so coding tools map each screen file to separate routes/surfaces.\n- Strengthen minimal-brief visual guidance to avoid monochrome or unstyled design outputs. * fix: address responsive handoff review feedback * fix: address handoff review blockers * fix: preserve proxy auth and normalized export entry * fix: narrow frame wrapper filter to directory paths only * fix: make artifact save failure banner generic --------- Co-authored-by: Huy Hoàng <macos@MacBook-Pro-Hoang.local> |
||
|
|
8962088c75
|
feat(daemon): guard against agent-emitted stub artifact regressions (#1171)
* feat(daemon): guard against agent-emitted stub artifact regressions
When an agent emits an <artifact> block whose body is a placeholder
("see other-file.html in this project", a bare filename string, a tiny
fallback page) instead of the full document, the daemon writes the
placeholder to disk verbatim. Users see a 25-500 byte HTML file where
their previous version had tens of kilobytes of real markup.
Add a structural regression guard in writeProjectFile: before writing
an html/deck artifact whose manifest carries metadata.identifier, scan
the project dir for prior siblings matching <identifier>(-\d+)?\.html?
and compare sizes. If the new body is below minRetainedRatio (default
0.2) of the largest prior sibling >= minPriorBytes (default 4096),
flag a regression. Three modes via env:
- OD_ARTIFACT_STUB_GUARD=warn (default) writes the file and attaches
stubGuardWarning to the response so the frontend can surface it.
- OD_ARTIFACT_STUB_GUARD=reject throws ArtifactRegressionError before
fs.writeFile; the route returns 422 ARTIFACT_REGRESSION with the
prior sibling's name and size in error.details.
- OD_ARTIFACT_STUB_GUARD=off skips the guard entirely.
Cross-agent by design: anchored on size delta + identifier match,
no agent-specific stub-phrase regex, so works for any agent backend
behind the agent-adapter abstraction.
The body-then-manifest write order pre-dates this change; the reject
path throws before fs.writeFile so rejections never leave a partial
state behind.
24 unit + 8 HTTP tests cover happy paths, all three modes, deck kind,
.htm extension sibling detection, ratio=1 edge case, and verify
rejected writes leave neither the html nor its manifest sidecar on
disk.
* fix(stub-guard): close same-name, nested-dir, and non-slug bypasses
Code review on PR #1171 (lefarcen, Codex, mrcfps) found three holes
where the stub guard could be silently bypassed. All three are now
closed with HTTP test coverage.
Same-name overwrite (lefarcen P1): the writer's prior-sibling scan
deliberately skipped the file at safeName, but for an in-session
overwrite (persistArtifact reuses the same fileName when
savedArtifactRef.current matches) that file is the prior content,
not the new entry. Drop the exclude-by-name filter; the current
on-disk size at scan time is always the prior because the overwrite
happens after this check.
Subdirectory scoping (Codex/mrcfps P2): writeProjectFile creates
parent directories for nested paths like reports/overview.html, but
the guard only scanned the project root. Pass path.dirname(target)
as scanDir so nested artifacts are evaluated against their real
sibling set.
Non-slug identifier (Codex/lefarcen/mrcfps P2): the web's
persistArtifact slugifies the filename basename but stores the raw
identifier in the manifest, so an identifier like "Landing Page"
yields filename landing-page.html with metadata.identifier="Landing
Page". Build the sibling regex from both the raw identifier and a
slugified variant (mirroring the frontend's slugifier) so either
form matches the same priors.
Also surface warn-mode warnings in the web UI: ProjectView now
checks file.stubGuardWarning after writeProjectTextFile and renders
the warning via setError. Reject-mode 422 surfacing requires
restructuring writeProjectTextFile's return contract and is
deferred.
API change inside the daemon: evaluateArtifactStubGuard /
findPriorArtifactSiblings drop excludeSafeName and rename projectDir
to scanDir. Tests updated.
Tests: 4 new HTTP cases (same-name overwrite preserves prior body,
nested subdir rejects, slug-form match rejects, plus the existing
warn/off/deck/.htm cases) and 1 new unit case (slug-form sibling
match). 44 tests pass.
* fix(stub-guard): empty-slug fallback + reject-mode UI surface
Round 3 review on PR #1171 (lefarcen, mrcfps) found two remaining
holes after
|
||
|
|
b1d440d2bd
|
refactor(daemon): split route registration (#1043)
* spec * refactor(daemon): split server route registrars * refactor(daemon): group route registrar dependencies * refactor(daemon): move remaining domain routes out of server * update doc * revert spec * fix daemon route context contract Generated-By: looper 0.5.6 (runner=fixer, agent=opencode) * fix media task persistence Generated-By: looper 0.5.6 (runner=fixer, agent=opencode) * fix: restore daemon route registrations * fix: restore static resource mutation origin checks |
||
|
|
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> |
||
|
|
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 |
||
|
|
c2facb0e02
|
fix: serve python files as text (#947)
Co-authored-by: leprincep35700 <leprincep35700@users.noreply.github.com> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
c3d9136a0c
|
Add live artifacts and Composio connector catalog (#381)
* docs: add live artifacts implementation spec * docs: align live artifacts implementation plan * Ralph iteration 1: work in progress * Ralph iteration 2: work in progress * Ralph iteration 3: work in progress * Ralph iteration 4: work in progress * Ralph iteration 5: work in progress * Ralph iteration 6: work in progress * Ralph iteration 7: work in progress * Ralph iteration 8: work in progress * Ralph iteration 9: work in progress * Ralph iteration 10: work in progress * Ralph iteration 11: work in progress * Ralph iteration 12: work in progress * Ralph iteration 13: work in progress * Ralph iteration 14: work in progress * Ralph iteration 15: work in progress * Ralph iteration 16: work in progress * Ralph iteration 17: work in progress * Ralph iteration 18: work in progress * Ralph iteration 19: work in progress * Ralph iteration 20: work in progress * Ralph iteration 21: work in progress * Ralph iteration 22: work in progress * Ralph iteration 23: work in progress * Ralph iteration 24: work in progress * Ralph iteration 25: work in progress * Ralph iteration 26: work in progress * Ralph iteration 27: work in progress * Ralph iteration 28: work in progress * Ralph iteration 29: work in progress * Ralph iteration 30: work in progress * Ralph iteration 31: work in progress * Ralph iteration 32: work in progress * Ralph iteration 33: work in progress * Ralph iteration 34: work in progress * Ralph iteration 35: work in progress * Ralph iteration 36: work in progress * Ralph iteration 37: work in progress * Ralph iteration 38: work in progress * Ralph iteration 39: work in progress * Ralph iteration 40: work in progress * Ralph iteration 41: work in progress * Ralph iteration 42: work in progress * Ralph iteration 43: work in progress * Ralph iteration 44: work in progress * Ralph iteration 45: work in progress * Ralph iteration 46: work in progress * Ralph iteration 47: work in progress * Ralph iteration 48: work in progress * Ralph iteration 49: work in progress * Ralph iteration 50: work in progress * Ralph iteration 51: work in progress * Ralph iteration 52: work in progress * Ralph iteration 53: work in progress * Ralph iteration 54: work in progress * Ralph iteration 55: work in progress * Ralph iteration 56: work in progress * Ralph iteration 57: work in progress * Ralph iteration 58: work in progress * Ralph iteration 59: work in progress * Ralph iteration 60: work in progress * Ralph iteration 61: work in progress * Ralph iteration 62: work in progress * Ralph iteration 63: work in progress * Ralph iteration 64: work in progress * Ralph iteration 65: work in progress * Ralph iteration 1: work in progress * Ralph iteration 2: work in progress * Ralph iteration 3: work in progress * Ralph iteration 4: work in progress * Ralph iteration 5: work in progress * Ralph iteration 6: work in progress * Ralph iteration 8: work in progress * Ralph iteration 9: work in progress * Ralph iteration 17: work in progress * Add Composio-backed connectors * Add Composio-backed connector catalog * Fix connector callback flow * Update live artifact connector refresh * Fix live artifact refresh updates * Improve live artifact viewer toolbar * Refine live artifact source tabs * Expand Composio connector catalog * Improve Composio connector browsing * Fix artifact refresh source safety checks Generated-By: looper 0.4.1 (runner=fixer, agent=opencode) * Fix live artifacts PR feedback Generated-By: looper 0.5.0 (runner=fixer, agent=opencode) * Fix live artifact preview CORS validation Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode) * Fix connector OAuth IPv6 loopback hosts Allow bracketed IPv6 loopback Host headers when deriving connector OAuth callback URLs so IPv6-bound daemons can complete connection flow. Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode) * Preserve live artifact refresh permissions Respect explicit refresh permission choices during live artifact create and update flows so revoked connector sources remain gated. Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode) * Fix live artifact preview cache freshness Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode) * Fix live artifact refresh validation Guard manual refreshes with local daemon checks and reject daemon_tool sources without a toolName before refresh execution. Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode) * Fix Composio credential invalidation Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode) * Fix live artifact CORS methods Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode) * Fix workspace validation Restore media config test isolation under Vitest setup data-dir overrides and add the missing French live artifact display copy so the workspace test suite stays aligned.\n\nGenerated-By: looper 0.5.2 (runner=fixer, agent=opencode) * Fix connector safety filtering Keep agent-preview connector listings aligned with execution safety policy and prune stale Composio OAuth state records before they accumulate. Generated-By: looper 0.5.2 (runner=fixer, agent=opencode) * Fix agent runtime cleanup Generated-By: looper 0.5.2 (runner=fixer, agent=opencode) * Fix live artifact daemon access Validate local-only live artifact routes against the peer socket address and pass daemon-resolved CLI paths to ACP MCP descriptors.\n\nGenerated-By: looper 0.5.2 (runner=fixer, agent=opencode) * Fix connector run limit pruning Evict stale connector rate-limit buckets so long-lived daemon processes do not retain per-run entries indefinitely.\n\nGenerated-By: looper 0.5.2 (runner=fixer, agent=opencode) * Fix connector compact schemas Generated-By: looper 0.5.2 (runner=fixer, agent=opencode) * Improve connector connection feedback * Adjust connector gate positioning * Fix live artifact refresh commits Avoid marking refresh candidates failed after snapshot or state persistence errors by deferring live artifact mutations until the durable refresh metadata is written. Also align connector OAuth callback host validation with daemon loopback handling.\n\nGenerated-By: looper 0.5.4 (runner=fixer, agent=opencode) * Improve connector search relevance * fix(daemon): harden connector connection state Require loopback daemon validation before connector connect side effects and only clear provider-owned connector statuses during credential reset. Generated-By: looper 0.5.4 (runner=fixer, agent=opencode) * fix(daemon): guard connector disconnect route Require local daemon request validation before connector disconnect side effects. Generated-By: looper 0.5.4 (runner=fixer, agent=opencode) * fix(daemon): guard composio config updates Generated-By: looper 0.5.4 (runner=fixer, agent=opencode) * fix(daemon): dispatch live artifacts mcp first Route the live-artifacts MCP server before the generic MCP CLI so od mcp live-artifacts starts the dedicated server instead of failing generic argument parsing.\n\nGenerated-By: looper 0.5.4 (runner=fixer, agent=opencode) * fix(daemon): handle integer connector schemas Allow JSON Schema integer connector inputs while preserving fractional-value validation so generated connector tool schemas accept valid page sizes and limits. Generated-By: looper 0.5.4 (runner=fixer, agent=opencode) * fix: align live artifact refresh error codes Generated-By: looper 0.5.4 (runner=fixer, agent=opencode) * Fix live artifact connector refresh flow * Update live artifact design cards * Add beta badge to live artifact form * Remove live artifact tile model * Fix live artifact refresh sync * Fix live artifact MCP refresh durability Generated-By: looper 0.5.4 (runner=fixer, agent=opencode) * Fix live artifact refresh safety Enforce persisted refresh opt-out and connector auto-read gating before refresh sources execute. Generated-By: looper 0.5.5 (runner=fixer, agent=opencode) |
||
|
|
bc04d61903
|
feat(design-files): add batch ZIP download with multi-select (#405)
* feat(design-files): add batch ZIP download with multi-select Add checkbox-based multi-select to the Design Files panel and a new POST /api/projects/:id/archive/batch endpoint that zips selected files using the project name as the archive filename. * fix(i18n): add missing batch-download keys to uk locale The upstream main branch has a uk.ts locale that didn't exist in the fork. Without these keys the web typecheck fails against the full locale set. * fix(design-files): harden batch archive and prune stale selections - Use lstat to reject symlinks, dotfiles, and .artifact.json sidecars in buildBatchArchive (mirror listFiles/collectFiles allowlist) - Reject invalid names explicitly instead of silently skipping - Prune stale filenames from selected set on files/projectId change * fix(daemon): tighten batch archive allowlist with segment-level checks - Check every path segment for hidden directories, not just basename - Walk intermediate directories with lstat to reject symlinks at any level - Fail-fast on any ineligible file instead of silently skipping |
||
|
|
33c3b94b42
|
feat(daemon): add od mcp - expose Open Design as an MCP server (#399)
* feat(daemon): add `od mcp` subcommand for stdio MCP server
Lets a coding agent in a different repo (Claude Code, Cursor, Zed)
pull files from a locally-running OD project over the Model Context
Protocol — no export/import zip dance.
The MCP server is a thin stdio process that proxies read-only tool
calls to the daemon's existing HTTP API; no daemon-side changes
required. Exposes 8 tools:
list_projects, get_project,
list_files, get_file,
list_skills, get_skill,
list_design_systems, get_design_system
Wired exactly like `od media`: a hoisted flag set, a SUBCOMMAND_MAP
entry, a thin handler that resolves OD_DAEMON_URL and hands off to
src/mcp.ts. Tool dispatch is a switch over the tool name; each branch
fetches the matching daemon route and surfaces the response as MCP
text content. Binary mimes return a clear error pending phase-2
support.
Lifecycle gotcha worth flagging: Server.connect(transport) only
*starts* the stdio reader; the promise resolves immediately. Without
holding the function awaiting until transport/stdin close, cli.ts's
top-level process.exit(0) kills the server before the first request
arrives. The fix in src/mcp.ts holds until onclose / stdin EOF.
Wire-up example for a consuming repo:
{
"mcpServers": {
"open-design": {
"command": "od",
"args": ["mcp"],
"env": { "OD_DAEMON_URL": "http://127.0.0.1:7456" }
}
}
}
New dep: @modelcontextprotocol/sdk (MIT, official Anthropic SDK).
* feat(daemon): add MCP server instructions for zero-shot LLM context
Hand the consuming LLM a system-prompt-style overview of the OD
workflow so it picks the right tool without prompt-engineering on
the user's side. Mentions get_artifact and project-name resolution
ahead of their actual implementation; both ship in the same batch.
* feat(daemon): resolve MCP project args by UUID, name, or substring
Lets a consuming agent say `project: "recaptr"` instead of pasting a
UUID. Match order: exact id → exact name (case-insensitive) →
slug-normalized name (strips trailing " (N)", normalizes whitespace) →
substring (errors if multiple). UUID inputs short-circuit and never
hit the daemon.
* feat(daemon): surface entryFile and kind on MCP get_project response
Promote metadata.entryFile and metadata.kind to top-level fields so
consumers (including get_artifact in this branch) can find the entry
without digging through nested metadata blobs.
* feat(daemon): add MCP get_artifact tool for bundle retrieval
A design rarely lives in a single file. get_artifact pulls the entry
HTML/JSX plus every sibling it references (tokens CSS, JSX modules,
imported components) in one call, so a consuming agent doesn't need
to parse HTML and round-trip per file.
Three modes:
auto (default): BFS over relative <script src>, <link href>,
<img src>, <source/video src>, JSX import/from, CSS url(), with
depth cap 3 and a visited set. CDN, data:, mailto:, anchors, and
paths containing .. are skipped.
all: every textual file in the project (mirror of /archive
minus binaries).
shallow: just the entry file (same as get_file).
Output is a structured JSON blob with name/mime/size/content per
file and the project's manifest metadata at the top.
* feat(daemon): add /api/projects/:id/search route + MCP search_files
Server-side substring search across textual project files. Returns
file, 1-indexed line, and snippet, capped at 1000 matches. Exposed
through the MCP layer as search_files(project, query, pattern?, max?).
Treats the query as a literal substring (regex chars escaped) to
avoid catastrophic-backtracking attacks from LLM-supplied input.
Honors the project dir's existing path-safety guards via listFiles.
* feat(daemon): add since= filter to /files route + MCP list_files arg
Lets a consumer poll for "what's changed since I last looked" without
re-walking every file. Daemon-side: parse since= as ms, filter
listFiles output by mtime. MCP-side: forward as URL query.
* feat(daemon): expose skills and design systems as MCP resources
Catalog reads are stable reference material — they fit MCP's
resources surface (LLM-passive) better than tools (LLM-active).
Skills and design systems each become resources at
od://skills/<id>/SKILL.md and od://design-systems/<id>/DESIGN.md;
existing list_skills / get_skill / list_design_systems /
get_design_system tools remain as fallbacks for clients that don't
handle resources cleanly.
* fix(daemon): tighten MCP correctness in get_artifact and resources
Several silent-failure paths and minor footguns the first pass missed:
- get_artifact auto: the entry's own fetch now raises a clear
error instead of returning files: []. Previously a typo in
`entry:` looked like an empty project.
- get_artifact: invalid `include` value returns a clear error
listing the valid modes instead of silently behaving as auto.
- get_artifact all: includes binary files as metadata stubs to
match auto's behavior. Both modes are now strict supersets of
shallow.
- extractRelativeRefs: gate JS-only patterns (import/from/require/
dynamic-import) by file mime/extension so prose in markdown or
HTML doesn't generate spurious 404 round-trips on words like
"imported from 'X'".
- extractRelativeRefs: cover <iframe>, <audio>, srcset, and
CSS @import — common in real OD output.
- resources/list descriptions are collapsed to a single line
(newlines + repeated whitespace -> one space) so MCP UIs that
don't normalize whitespace render cleanly.
- fetchProjectFile: 0-byte binary files no longer report size: null
due to falsy short-circuit on Number(content-length).
* perf(daemon): cache MCP project list for 5s in resolveProjectId
A typical agent session calls list_files/get_file/get_artifact several
times in a row, each with a project name argument. Each previously
re-fetched /api/projects. Cache the list in module scope with a 5s
TTL so back-to-back lookups are local; renames in the OD UI still
propagate within a few seconds.
* feat(daemon): MCP UX polish — tool order, annotations, get_artifact maxBytes
Three changes well-behaved MCP clients pick up automatically:
- Tool ordering. list_projects + get_artifact are now first; LLMs
that weight earlier entries surface the bundle path before
per-file fetching. Catalog tools (list_skills, get_skill,
list_design_systems, get_design_system) sit at the bottom; they
are also exposed as MCP resources.
- readOnlyHint / idempotentHint / openWorldHint annotations on
every tool so clients can skip confirmation prompts on safe
tools and let the LLM know re-running is fine. Per-tool `title`
annotations give clients a friendlier display name than the
snake_case tool id.
- get_artifact gains a `maxBytes` arg (default 1.5MB). Once the
accumulated textual content crosses the cap, remaining files
are dropped and `truncated: true` is set on the bundle so the
consumer knows to use list_files / get_file for the rest.
* feat(daemon): expose user's active OD project/file via MCP
The "what file are you on?" round-trip the agent had to do every
session is now answered automatically. Three pieces:
- Daemon: in-memory active-context slot with 5-minute TTL.
POST /api/active sets {projectId, fileName}; GET /api/active
returns the current value enriched with projectName, or
{active:false} when the slot is empty/stale. Cleared on
daemon restart.
- Web: a small useEffect in App.tsx posts the active project +
file to the daemon on every route change. Best-effort fire-
and-forget; a missing daemon doesn't surface an error.
- MCP: get_active_context tool (no args) and a matching MCP
resource at od://focus/active. The tool is listed second,
right after list_projects, so an LLM picks it up before
asking for ids. Server instructions tell the model to call
it FIRST when the user says "this file" / "the design I have
open" / "what I'm looking at."
End to end: user opens a project in OD, agent in another repo
calls get_active_context() → gets {projectName: "recaptr",
fileName: "recaptr-onboarding-4.html"}, then immediately calls
get_artifact(project: "recaptr") with no further user input.
* feat(daemon): make MCP project arg optional, fall back to active OD context
get_artifact, get_project, get_file, search_files, and list_files now
accept project as optional. When omitted, the MCP resolves project
from /api/active so an agent in another repo can call
search_files({ query: "Polaroid" })
without first asking the user "which project?". get_file and
get_artifact also default their path/entry to the active file, so
get_file({}) returns whatever the user is currently looking at.
The implicit path stamps `usedActiveContext` on JSON responses (or a
separate `[od:active-context …]` content block on get_file) so the
agent can see exactly which project/file got chosen. Explicit
project args pass through with zero added overhead.
Cuts the common case from two MCP round trips
(get_active_context → search_files) to one. Server instructions and
get_active_context's own description are updated to point at the
new default.
* fix(daemon): require same-origin for /api/active POST and GET
The active-context endpoint was added without isLocalSameOrigin
guard. Since the daemon binds 0.0.0.0 by default, a LAN peer could
GET it to learn what file the user has open, or POST it to redirect
the MCP fallback to a project of their choice. Same-origin only is
the right scope: the web app proxies its requests through Next.js
on the daemon port, and the MCP runs over loopback in-process, so
both legitimate callers pass.
Pattern matches the existing /api/app-config etc. guards.
* feat(daemon): add /api/mcp/install-info for cross-platform install snippets
The Settings -> MCP server panel needs absolute paths to node and
the daemon's built cli.js so it can render snippets that work on a
fresh source clone (where `od` is not on PATH) and dodge the
/usr/bin/od octal-dump tool that ships on macOS/Linux and would
otherwise shadow ours.
Endpoint returns:
- command: process.execPath (the node binary running the daemon)
- args: [<absolute path to dist/cli.js>, "mcp"]
- daemonUrl: http://127.0.0.1:<port>
- platform: process.platform (so the panel can localize ~/.cursor
vs %USERPROFILE%\.cursor and Cmd vs Ctrl shortcuts)
- cliExists / nodeExists: existsSync checks on both binaries
- buildHint: human-readable build/reinstall instructions when
either path is missing
isLocalSameOrigin guard same as /api/active. Cached for 5s because
the panel may re-fetch on every open and the paths cannot change
without a daemon restart.
Test file covers the happy path, cross-origin rejection, two
allowed-Origin variants, and the cache by counting fresh resolves
across rapid calls. 5/5 pass.
* refactor(daemon): tighten MCP surface, trim descriptions, polish copy
Three intertwined cleanups that all live in mcp.ts + cli.ts:
1. Drop catalog tools from MCP. list_skills / get_skill /
list_design_systems / get_design_system are removed. The audience
is a coding agent in a separate repo consuming Open Design's
output; it cannot run skills (those are recipes Open Design uses
to generate) and design-system DESIGN.md is reference material
that already ships as an MCP resource. Keeping the catalog as
tools cost ~350 token-overhead per turn for capabilities the
agent could not act on. Tool count: 11 -> 7.
2. Trim tool descriptions. The active-context fallback explanation
was repeated in 5 separate tool descriptions; hoisted into
PROJECT_ARG and explained once in the server `instructions`
block instead. Saves ~150-200 tokens per tools/list response.
3. User-facing branding pass. Tool titles, tool descriptions,
resource names, error messages, comments, and `od mcp --help`
now consistently use "Open Design" rather than "OD". Internal
abbreviation `OD` is retained only inside the server
instructions block where it is introduced inline as "Open Design
(OD)" for compactness across multi-paragraph guidance.
Em dashes replaced with hyphens throughout, per project style.
* feat(web): add MCP server install panel in Settings
New "MCP server" section in the Settings dialog, surfacing
copy-paste install snippets for the major MCP-compatible coding
agents (Claude Code, Cursor, VS Code, Antigravity, Zed, Windsurf).
Highlights:
- In-brand custom dropdown (reuses the existing .ds-picker
pattern from the design-system / prompt-template pickers, click
outside / Escape to close, chevron animates) instead of a
native <select>.
- Per-client snippet that uses absolute paths to node + cli.js
fetched from /api/mcp/install-info on mount, so it works even
when `od` is not on PATH.
- Cursor gets a one-click "Install in Cursor" deeplink
(cursor://anysphere.cursor-deeplink/mcp/install) that pops an
approval dialog and writes the config for the user. UTF-8-safe
base64 so paths with accented characters do not throw.
- Per-OS path hints (~/.cursor on POSIX, %USERPROFILE%\.cursor
on Windows) and keyboard shortcuts (Cmd vs Ctrl).
- Build-required warning card when cli.js or the node binary
does not exist on disk; deeplink button disables in that state.
- Prominent "restart your client to pick up the new server"
callout below the snippet, with per-client guidance.
- Capability list ("what your agent can do") instead of a tool-
name dump, so non-developer designers can also tell what is
possible without reading MCP docs.
README adds a short "Use Open Design from your coding agent"
section that points at the panel and summarizes the per-client
flow (one-click for Cursor, JSON merge elsewhere). Read-only by
design; the daemon must be running locally.
* docs(readme): align MCP server section with the Settings panel
The "Use Open Design from your coding agent" section had drifted
from what the panel actually emits and lists.
- Add Antigravity to the supported-client list (previously missing).
- Drop the "(GitHub Copilot)" parenthetical from VS Code so the
label matches the panel.
- Fix the Claude Code line: we no longer emit a single
`claude mcp add ...` shell command. The snippet is JSON; the
panel additionally suggests `claude mcp add-json` as the safer
way to apply it instead of hand-editing ~/.claude.json.
- Swap the "find the Polaroid section" example for two more
universal phrases ("build this in my app", "match these
styles") that match what the panel surfaces.
- Add a one-line "restart or reload your client after install"
note - this was prominent in the panel and absent from the
README.
- Trim the /usr/bin/od octal-dump aside; it was technical detail
that did not earn its space at the README intro level.
* feat(web): add Codex CLI to the MCP server install panel
Codex is a first-class supported coding agent (listed alongside
Claude Code, Cursor, etc. in the README's PATH-detected agent
table) but the install panel was missing it.
Codex stores MCP server config at ~/.codex/config.toml (TOML, not
JSON) under an `[mcp_servers.<name>]` table, and the same file is
shared between the Codex CLI and the Codex IDE extension - so one
install covers both. Added a 7th client entry that emits the right
TOML snippet, expanded the snippet-lang union to include 'toml'
(behaves like 'json' for whitespace handling, just a different
syntax-highlight hint).
For our minimal payload (just command + args), JSON.stringify
happens to produce valid TOML literal values since TOML basic
strings use the same double-quote escape rules as JSON, and TOML
inline arrays match JSON array syntax. No new TOML serializer
needed.
README updated to list Codex among the supported clients.
Schema verified against https://developers.openai.com/codex/mcp.
* fix(daemon): accept any loopback origin in same-origin guard
The previous port-pinned check required the request's Origin to match
either the daemon's own port or OD_WEB_PORT. tools-dev does not pass
OD_WEB_PORT to the daemon process, so any browser POST to /api/active
proxied through the dev web (port 17573 etc.) was rejected with 403,
and get_active_context always returned {active: false}.
Relax to a loopback-prefix match: any http://127.0.0.1:*,
http://localhost:*, or http://[::1]:* origin passes regardless of
port. Cross-origin (https://evil.com) is still rejected. The
trade-off is that another local web app on a different loopback port
could now CSRF the daemon; same-origin checks are inherently a CSRF
defense, not a network ACL.
* fix(web): make Claude Code MCP snippet a real copyable one-liner
claude mcp add-json open-design '<json>' takes only the inner
server-config object, not the full {"mcpServers": ...} wrapper, and
rejected the wrapped shape with "Invalid configuration: : Invalid
input". Pass only the inner config, and inline the JSON into the
command itself so the snippet is a real one-liner the user can copy
and paste, no template substitution.
* test(daemon): drop loopback-prefix assertions superseded by upstream origin policy
The two proxy-flow allow tests were added in
|
||
|
|
0851df379e
|
fix(daemon): serve .jsx / .tsx with JS-family MIME so browser loaders accept them (#340)
Multi-file React prototypes loaded via Babel-standalone (`<script type="text/babel" src="…">`) fetch each `.jsx` / `.tsx` source over HTTP. The daemon's `EXT_MIME` map in `apps/daemon/src/projects.ts` served `.jsx` as `application/octet-stream` (no entry → fallback) and `.tsx` as `text/typescript`, neither of which is a recognized JavaScript MIME. Strict CSPs and browser module loaders reject both, which is one of the failure modes flagged in #336 for the Open-button render path. Map both to `text/javascript; charset=utf-8`, matching the existing treatment of `.js` / `.mjs` / `.cjs`. `.ts` is intentionally left as `text/typescript` — it has no browser-execution path here; tooling that reads `.ts` as TypeScript source is the load-bearing consumer. Tests in `apps/daemon/tests/project-classifiers.test.ts` were updated to pin the new mappings and document the rationale. Refs #336. |
||
|
|
30f8036c9a
|
fix(web): make share-menu "Download as .zip" return the actual project tree (#341)
* fix(web): download project tree as zip from share menu The "Download as .zip" share action previously produced a single-file ZIP of the rendered HTML srcdoc. Add a daemon archive endpoint that bundles the on-disk project tree (scoped to the active top-level directory when applicable) and have FileViewer call it, falling back to the in-memory single-file ZIP on failure. UTF-8 filenames are preserved via RFC 5987 Content-Disposition. * fix(daemon,web): address PR #341 review — diagnostics, comments, more tests - Stat the archive root up-front so a missing/non-directory target surfaces a clear ENOENT/ENOTDIR ("does not exist" vs "is empty"), instead of being swallowed by the recursive walk and reported as empty. Distinguishes a deleted project from one with no archivable files for on-call diagnostics. - Document the DEFLATE level-6 choice in the archive builder so future maintainers don't have to guess at the speed/ratio trade-off. - Add a daemon test that the baseName preserves a multi-byte UTF-8 directory name (café-design), covering the path that feeds RFC 5987 filename* on the server. - Add a daemon test that a missing archive root surfaces ENOENT with a "does not exist" message, distinct from the empty-directory case. - Export archiveRootFromFilePath and archiveFilenameFrom for testing, and add web unit tests covering the Content-Disposition fallback chain (UTF-8 filename* → legacy quoted filename= → root slug → title slug, plus malformed-encoding fall-through). - Replace the CJK example string in code comments and tests with a Latin-extended example (café-design) so the codebase stays English while still exercising multi-byte UTF-8 handling. |
||
|
|
b9d8bd36b9
|
fix(daemon): treat .py files as previewable code in Design Files (#261)
Python files in Design Files cannot be previewed inline today: the side panel falls back to BinaryViewer and users can only download or open externally, even though `.py` is plain text and reasonable to inspect in-app. Root cause: `kindFor()` in `apps/daemon/src/projects.ts` (line 278) classifies files into coarse buckets the frontend uses to pick a viewer. The `'code'` branch lists `.js / .mjs / .cjs / .ts / .tsx / .json / .css` but not `.py`, so Python files fall through to `'binary'`. The frontend already renders the `'code'` bucket via `TextViewer` in `apps/web/src/components/FileViewer.tsx`, so once a file lands in the `'code'` bucket it gets the existing read-only code preview for free. Fix: add `'.py'` to the `'code'` extension list. No frontend change needed — `TextViewer` handles it on receipt. Scope: this PR keeps the change minimal to match the issue (`.py` only). Other commonly-requested text-like extensions (`.yaml / .yml / .toml / .sh`, plus other code languages) can be added in a follow-up if maintainers want that surface area. Closes #61 |
||
|
|
3f266103b0
|
feat(media): port generation workflow onto main (#12)
Co-authored-by: Elian <elian@EliandeMacBook-Pro.local> |
||
|
|
132adac3bb
|
fix(daemon): preserve non-ASCII filenames on multipart upload (#166)
* fix(daemon): preserve non-ASCII filenames on multipart upload
multer@1 hands callers latin1-decoded multipart filenames, and
sanitizeName() then collapses every non-ASCII character to '_'. A
Chinese DOCX uploaded as 测试文档.docx therefore landed on disk as
____.docx, while the response shipped a latin1-mangled originalName
back to the client. The chat composer compared that to the UTF-8
File.name from the picker, missed the match, and reported "some
files could not be stored" even though the bytes were already on
disk.
Add decodeMultipartFilename() that round-trip-checks latin1->utf8
so genuine latin1 names aren't corrupted, and switch sanitizeName()
to keep \p{L}\p{N} (Unicode letters/digits) while still stripping
path separators, control characters, and Windows-reserved
punctuation. Apply the decoder to all three multer storage filename
callbacks so /api/upload, /api/import/claude-design, and
/api/projects/:id/upload all return UTF-8 originalNames.
Closes #144
* fix(daemon): address review feedback on filename decoder
Three changes from review:
1. decodeMultipartFilename now bails out early when any code point in
the input exceeds 0xFF. multer can hand us an already-decoded UTF-8
string when the client uses the RFC 5987 `filename*` parameter, and
the previous round-trip-only check would corrupt those names — for
example, a properly decoded `测试文档.docx` could be re-mangled into
`KՇc.docx` because the latin1-encoded bytes happened to form a
valid UTF-8 sequence. Code points above 0xFF can never appear in a
genuine latin1-decoding-of-utf8 input, so they're an unambiguous
signal that no further decoding should run.
2. decodeMultipartFilename now handles null/undefined defensively (it
was relying on multer always populating originalname; this just
makes the helper safer to reuse from other call sites).
3. The inline sanitiser regex in the `upload` and `importUpload`
multer instances is replaced with a direct `sanitizeName()` call,
matching what `projectUpload` already did. This keeps a single
source of truth for the allowed character set so future tweaks
only have to land in one place.
Adds two more tests to the existing sanitize-name suite covering the
above-0xFF early return and the null/undefined inputs (13/13 in that
file, 58/58 across the daemon).
|
||
|
|
c6d11018a0
|
Refresh desktop integration control plane (#123)
* feat(dev): add desktop tools-dev control plane * refactor(sidecar): split Open Design contracts Move Open Design-specific sidecar protocol definitions into @open-design/contracts so sidecar and platform can remain descriptor-driven primitives. * refactor(daemon): organize package sources Keep daemon app code, tests, and sidecar entrypoints in separate package directories so each layer can be built and verified independently. * chore(repo): streamline maintenance entrypoints Centralize agent guidance by directory and reduce root command chains while preserving the existing build scope. * docs: translate agent guidance to English * fix(sidecar): tolerate stale IPC sockets Remove stale Unix socket files only after confirming no listener is active, so tools-dev can restart after unclean shutdowns. |
Renamed from apps/daemon/projects.ts (Browse further)