mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
125 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0edbf38171 |
feat(plugins): add specVersion and version fields to plugin and marketplace schemas
- Introduced `specVersion` and `version` fields to the plugin and marketplace schemas, ensuring better versioning and compatibility tracking. - Updated various components and functions to handle the new fields, including database migrations, plugin snapshots, and marketplace management. - Enhanced tests to validate the presence and correctness of the new fields in plugin manifests and marketplace entries. - Improved documentation to reflect the changes in schema requirements and provide guidance on the new versioning system. This update strengthens the plugin ecosystem by providing clear versioning, enhancing the reliability and maintainability of plugins and marketplaces. |
||
|
|
8b2d48a258 |
feat(daemon, web): enhance plugin preview handling and add new templates
- Introduced logic to assemble example slides with a companion template when the declared entry is missing, improving the user experience for plugin previews. - Updated the server logic to handle special cases for `example-slides.html`, ensuring proper fallback to `template.html` when applicable. - Enhanced tests to verify the new preview assembly functionality and ensure correct rendering of fallback content. - Added new HTML and Markdown examples for various skills, including a magazine article layout and a Twitter share card, expanding the available templates for users. This update significantly improves the plugin preview experience, providing users with more robust and visually appealing fallback options. |
||
|
|
9e196d34af |
feat(daemon, web): enhance plugin sharing workflows and UI components
- Updated the plugin sharing prompts to utilize local daemon endpoints for publishing to GitHub and contributing to Open Design, streamlining the user experience. - Refactored the `PluginsView` and `PluginShareMenu` components to support new sharing functionalities, including confirmation modals and improved link handling. - Enhanced the CSS styles for the plugin share confirmation modal and related UI elements for better visual consistency. - Added tests to verify the functionality of the new sharing workflows and ensure proper integration within the existing plugin management system. This update significantly improves the plugin sharing experience, making it easier for users to publish and contribute their plugins effectively. |
||
|
|
c36609c47d |
feat(daemon, web): implement plugin sharing project creation and enhance CLI functionality
- Added new flags for conversation, message, agent, and model in the CLI to support enhanced plugin sharing features. - Introduced a new API endpoint for creating share projects for plugins, allowing users to publish to GitHub or contribute to Open Design. - Updated the UI components to facilitate the new sharing functionalities, including prompts for user input during the sharing process. - Enhanced the project management system to handle new plugin share actions, improving user interaction and experience. - Added tests to ensure the reliability of the new sharing features and their integration within the existing plugin management system. This update significantly enhances the plugin ecosystem by enabling users to share their creations more effectively and streamline collaboration. |
||
|
|
5f71968f61 |
feat(daemon, web): implement plugin sharing features for GitHub and Open Design contributions
- Added new API endpoints for publishing plugins to GitHub and contributing to Open Design, enhancing the plugin sharing capabilities. - Introduced functions for handling plugin sharing actions, including `publishGeneratedPluginToGitHub` and `contributeGeneratedPluginToOpenDesign`. - Updated the `DesignFilesPanel` and `FileWorkspace` components to support new sharing functionalities, allowing users to publish or contribute plugins directly from the interface. - Enhanced the UI with new buttons for publishing and contributing plugins, improving user interaction and experience. - Added tests to ensure the reliability of the new sharing features and their integration within the existing plugin management system. This update significantly improves the plugin ecosystem by enabling users to share their creations with the community and streamline collaboration. |
||
|
|
6f818d971d |
feat(daemon, web): implement plugin folder installation and enhance atom worker registry
- Added a new API endpoint for installing plugins from specified folder paths, improving the plugin management experience. - Introduced functions for normalizing and validating project plugin folder paths, ensuring robust error handling. - Implemented a registry for built-in atom workers, allowing for dynamic signal aggregation during pipeline execution. - Enhanced the `runStageWithRegistry` function to support multiple atom workers, merging their outputs with pessimistic logic. - Updated the UI components to display plugin folder candidates and facilitate user interactions for plugin installation. - Added tests for the new atom worker registry and plugin folder installation features, ensuring reliability and correctness. This update significantly enhances the plugin installation process and the overall functionality of the atom worker system, providing users with better tools for managing plugins and their interactions. |
||
|
|
ed2cbe171b |
feat(daemon, web): implement media generation scenario and enhance plugin handling
- Introduced a new `od-media-generation` scenario plugin for handling image, video, and audio projects, providing a default pipeline for media generation. - Updated the `collectBundledScenarios` function to deduplicate scenarios and prefer canonical IDs for task kinds, improving plugin routing. - Enhanced the `PluginsView` and `HomeHero` components to better display community and user-installed plugins, improving user experience. - Refactored tests to accommodate the new media generation scenario and ensure proper functionality across plugin types. This update significantly enhances the media handling capabilities and overall plugin management experience, making it easier for users to work with various media projects. |
||
|
|
13d5598b0c |
feat(web, daemon): enhance plugin import functionality and UI components
- Added support for uploading plugins via zip files and folders, improving the plugin import process. - Introduced a new `PluginImportModal` for a streamlined user experience when importing plugins. - Updated the `PluginsView` to include disabled states for unfinished plugin areas, enhancing clarity for users. - Refactored various components to utilize the new `resolvePluginQueryFallback` function for improved localization handling. - Enhanced CSS styles for better visual feedback and responsiveness in the plugin import interface. This update significantly improves the plugin management experience, making it easier for users to import and manage plugins effectively. |
||
|
|
443aea72c5 |
feat(daemon, web): enhance plugin handling and UI integration
- Introduced a new plugin upload mechanism with file size limits and memory storage, allowing users to upload plugins directly. - Implemented fallback logic for plugin application, ensuring projects can be created without explicit plugin requests. - Enhanced the UI to support plugin selection and integration, including a new `PluginsView` component for managing plugins. - Updated various components to utilize localized text for plugin queries, improving user experience across different languages. - Added tests for new plugin functionalities and local skill loading, ensuring reliability and correctness. This update significantly improves the plugin management experience, providing users with better tools for plugin integration and interaction. |
||
|
|
244e8b7981 |
feat(daemon): enhance plugin preview handling and add fallback mechanisms
- Implemented a new `collectPluginPreviewCandidates` function to gather potential HTML assets for plugins, improving the robustness of the preview endpoint. - Introduced `discoverPluginHtmlAssets` to scan common directories for HTML files, ensuring that plugins with missing declared entries can still provide a valid preview. - Updated the `/api/plugins/:id/preview` route to utilize the new candidate collection and discovery functions, enhancing the user experience by preventing blank tiles in the gallery. - Added comprehensive tests for the new fallback logic to ensure reliability and correctness in various scenarios. This update significantly improves the plugin preview functionality, ensuring users have access to valid previews even when manifest entries are outdated or missing. |
||
|
|
1bdf765cf2 |
feat(daemon): enrich API responses with surface specs and add new flags
- Implemented `--schema` flag for `od ui show` to return only the JSON Schema of the surface. - Enhanced the response of `GET /api/runs/:runId/genui/:surfaceId` to include the surface spec from the AppliedPluginSnapshot. - Introduced new flags for daemon and library commands to improve command handling and parsing. - Added tests for the new functionality, ensuring proper behavior of the enriched responses and flag handling. This change supports headless interactions by allowing code agents to inspect surface contracts before responding. |
||
|
|
2a24043c32
|
feat(plugins): od plugin events purge admin escape hatch (Phase 4)
Plan NN1.
apps/daemon/src/plugins/events.ts ships a public buffer reset
distinct from the test-only `__resetPluginEventBufferForTests`:
purgePluginEventBuffer() \u2192 PurgePluginEventBufferResult
{
purged: <count discarded>,
firstId: <id of first discarded entry, or null>,
lastId: <id of last discarded entry, or null>,
preNextId: <buffer's nextId pre-purge, for audit>,
}
The pre-purge stats let an operator confirm what they discarded;
preNextId surfaces 'did we lose a window of events between an
external export and the purge'.
apps/daemon/src/server.ts: new loopback-only POST
/api/plugins/events/purge route (requireLocalDaemonRequest).
CLI: `od plugin events purge --confirm [--json]`. Refuses to
run without --confirm so a stray invocation never drops audit
data accidentally. Output:
[events purge] dropped 47 events (id range: 1 \u2192 47; preNextId=48)
Daemon tests: 1818 \u2192 1822 (+4 cases on plugins-events-purge:
zero-shape on empty buffer, full-buffer purge resets state +
records id range, post-purge nextId restarts at 1, subscribers
are cleared on purge — a new subscriber starts fresh).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
20ef3f52b3
|
feat(daemon): od daemon db verify SQLite integrity check (Phase 5)
Plan LL1.
apps/daemon/src/storage/db-inspect.ts gains a pure helper:
verifySqliteIntegrity({ db, quick? })
\u2192 DbIntegrityReport { ok, mode, issues[], elapsedMs, generatedAt }
Wraps two SQLite PRAGMAs:
integrity_check (or quick_check when quick=true) \u2014 verifies
each table + index page is internally consistent; quick skips
the index-content check for ~10x speedup on big DBs.
foreign_key_check \u2014 walks every FK and reports rows that
reference a missing parent. Only meaningful when foreign_keys
PRAGMA is enabled (which the daemon does in openDatabase).
Issues come back tagged kind='integrity' | 'foreign_key' so a
consumer can route alerts differently.
apps/daemon/src/server.ts: new POST /api/daemon/db/verify
(loopback-only via requireLocalDaemonRequest) accepts ?quick=1.
CLI: `od daemon db verify [--quick] [--json]`. Exit 0 on
ok=true, 4 on any issue. Operator one-liner:
od daemon db verify --quick
\u2192 [db verify] mode=quick_check ok=true issues=0 3ms
Daemon tests: 1808 \u2192 1813 (+5 cases on storage-db-verify:
healthy fresh DB ok, --quick mode tag, FK violation detection
(via foreign_keys=OFF + insert + foreign_keys=ON dance),
elapsedMs / generatedAt, populated DB still ok).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
a43f34f00c
|
feat(plugins): od plugin events snapshot/stats + tail filters (Phase 4)
Plan KK1 + KK2 + KK3.
KK1: `od plugin events snapshot` non-SSE one-shot read.
New GET /api/plugins/events/snapshot returns
{ events, count, generatedAt }. Supports the same ?since=<id>
trim as the SSE route. Useful for dashboards that don't want
to hold a long-lived connection.
KK2: `od plugin events stats` rollup helper.
New summarisePluginEvents(events) pure helper +
GET /api/plugins/events/stats route. Counts by kind +
byPluginId (skipping empty plugin ids from marketplace
events) + oldest/newest timestamps + id range. CLI
pretty-prints the rollup with sorted-key counts so output
is byte-deterministic.
KK3: --kind / --plugin-id filter flags on tail / snapshot.
Both subcommands accept the same filter knobs. tail filters
client-side post-render (so the SSE backlog still arrives
but the renderer drops non-matching entries); snapshot does
the same client-side filter on the JSON response. Lets ops
write 'show me only plugin.trust-changed for slack-bot' as
a one-liner.
CLI summary: helpers in cli.ts now reuse formatCounts /
formatTimestamp from §3.GG1 for deterministic output across
status-style commands.
Daemon tests: 1803 \u2192 1808 (+5 cases on plugins-events-stats:
zero-shape on empty buffer, byKind aggregation, byPluginId
skipping empty ids, oldest+newest+id-range, roll-up over a
pre-filtered slice respects the input order).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
c9669339de
|
feat(plugins): more event producer hooks (Phase 4)
Plan JJ1 — extends §3.II1 ring buffer with three new producer
hooks + distinguishes upgrade-vs-install in the install path.
installPlugin opts gain `eventKind?: 'installed' | 'upgraded'`
Default 'installed' (back-compat). The upgrade route now
passes 'upgraded' so the live tail shows
'plugin.upgraded' instead of 'plugin.installed' when the
operation came through POST /api/plugins/:id/upgrade.
POST /api/plugins/:id/trust now emits 'plugin.trust-changed'
with { action, capabilities, total } so security audits can
track grant / revoke operations from the live tail.
POST /api/applied-plugins/prune now emits 'plugin.snapshot-pruned'
when result.removed > 0, with { removed, before? } so ops can
track GC churn across daemon uptime.
POST /api/marketplaces/:id/refresh now emits
'plugin.marketplace-refreshed' with { marketplaceId } so the
catalog refresh cadence is visible.
Each producer hook is wrapped in a try/catch and never blocks the
underlying mutation if the event ring buffer ever throws.
Daemon tests: 1797 \u2192 1803 (+6 cases on plugins-events-producers:
installFromLocalFolder emits plugin.installed, installPlugin with
eventKind='upgraded' emits plugin.upgraded instead, default
back-compat eventKind, uninstallPlugin emits plugin.uninstalled,
uninstall event guard pins the (removed || removedFolder)
predicate, install \u2192 upgrade sequence shows distinct kinds in
order).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
16ac3b549d
|
feat(plugins): plugin event ring buffer + SSE tail (Phase 4)
Plan II1.
apps/daemon/src/plugins/events.ts ships a small in-memory FIFO
ring buffer for plugin lifecycle events. Producers call
recordPluginEvent({ kind, pluginId, details? }); consumers either
pull pluginEventSnapshot(since?) for a one-shot read or call
subscribePluginEvents(cb) for a live feed (returns an unsubscribe
callback).
The buffer is capped at 1000 entries — older events fall off the
head. State resets on daemon restart (events survive the run,
not the boot).
PluginEventKind union covers the lifecycle vocabulary:
plugin.installed / .upgraded / .uninstalled
plugin.trust-changed / .applied
plugin.snapshot-pruned / .marketplace-refreshed
Producer hooks:
installer.installFromLocalFolder() emits 'plugin.installed' on
success with { version, sourceKind, source, trust, warnings }.
installer.uninstallPlugin() emits 'plugin.uninstalled' on
successful row removal.
(Apply / snapshot-prune / trust-changed hooks are wiring in
incrementally in subsequent slices; the producer surface is
documented in events.ts.)
apps/daemon/src/server.ts: new GET /api/plugins/events SSE route.
On open: emits the buffered backlog as 'event: backlog' entries,
then forwards every newly-recorded event as 'event: plugin' with
the same shape. Optional ?since=<id> trims the backlog.
CLI: `od plugin events tail [-f] [--since <id>] [--json]`.
Default mode drains the backlog + idles 200ms before exiting;
-f / --follow keeps the connection open and prints live events.
JSON mode emits one event per line for easy piping into jq.
Daemon tests: 1787 \u2192 1797 (+10 cases on plugins-events-buffer:
monotonic id + epoch ms, multi-record id sequence, optional
details field, full snapshot vs. since filter, snapshot copy
semantics, subscriber forwarding, unsubscribe stops forwarding,
listener-exception isolation, 1000-cap eviction).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
b3e8246f85
|
feat(plugins,daemon): doctor --strict + db vacuum (Phase 4 + 5)
Plan HH1 + HH2.
HH1: `od plugin doctor --strict` flag promotes doctor warnings
to errors. Useful in CI when any drift in resolved-context refs
or atom warnings should fail the build. JSON output includes
{ strict, passed }; exit code 4 distinguishes 'strict failed' from
'doctor errors' (exit 1). Verify config grows a `strict: true`
knob that propagates through verifyPlugin() — committing
.od-verify.json with strict:true gives plugins a one-line CI
'no warnings allowed' policy.
HH2: `od daemon db vacuum`. New POST /api/daemon/db/vacuum route
runs SQLite VACUUM, reports before/after sizes + reclaimed bytes
+ elapsed ms. Useful after large delete batches (snapshot prune,
plugin uninstall) shrink rows but leave space allocated to the
file. Loopback-only via requireLocalDaemonRequest (matches the
existing applied-plugins/export route shape — DB compaction is
operator-only).
CLI:
od plugin doctor <id> --strict
od daemon db vacuum
Daemon tests: 1784 \u2192 1787 (+3 cases on plugins-verify
strict-mode: default-pass on warnings, strict-fail on warnings,
strict-still-pass on zero issues). Existing plugins-verify
warnings-pass test stays correct because warnings-only doctor
keeps doctor.ok=true (the strict path explicitly opts in).
Existing plugins-canon / plugins-doctor tests stay byte-equal.
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
d2ce5325f8
|
feat(daemon): od daemon db status SQLite inventory (Phase 5)
Plan GG1.
apps/daemon/src/storage/db-inspect.ts ships a pure helper:
inspectSqliteDatabase({ db, file }) \u2192 DaemonDbStatusReport
{
kind: 'sqlite',
location: <abs path>,
sizeBytes: <primary + WAL + SHM>,
schemaVersion: <user_version PRAGMA>,
tables: [{ name, rowCount }, ...],
generatedAt: <epoch ms>,
}
User tables only (sqlite_* / better_sqlite3_* excluded). Tables
walk in lexicographic order so the report is byte-deterministic.
Each table's row count is computed via a parameterised query
behind an identifier sanitiser ([A-Za-z_][A-Za-z0-9_]*) that
rejects malformed names; a corrupted view doesn't crash the
whole inspection — its row count just falls back to 0.
apps/daemon/src/server.ts: new GET /api/daemon/db wires the
inspector against the live DB handle + RUNTIME_DATA_DIR-relative
file path.
CLI: `od daemon db status [--json]`. Pretty-prints two columns
(table name padded to longest; row count). Helps ops sanity-check
deployments + compare expected-vs-actual table rosters across
daemon upgrades.
Daemon tests: 1776 \u2192 1784 (+8 cases on storage-db-inspect:
kind+location reporting, schemaVersion from PRAGMA, fresh DB
defaults to 0, system tables excluded, lexicographic ordering,
WAL companion size summed, generatedAt timestamp, empty DB
non-crash).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
85729b8000
|
feat(plugins): od plugin stats inventory health report (Phase 4)
Plan DD1.
apps/daemon/src/plugins/stats.ts ships two pure helpers:
pluginInventoryStats(plugins) \u2192 PluginInventoryStats
{ total, bySourceKind, byTrust, byTaskKind,
withElevatedCapabilities, bundled, thirdParty,
lastInstalledAt, lastUpdatedAt }
snapshotInventoryStats(rows) \u2192 SnapshotInventoryStats
{ total, byStatus, withProject, withRun,
oldestAppliedAt, newestAppliedAt }
Elevated-capability detection covers fs:write, subprocess, bash,
network, and any 'connector:*' grant — the §5.3 set that lets a
plugin mutate state outside the project cwd.
apps/daemon/src/server.ts: new GET /api/plugins/stats route reads
installed_plugins + applied_plugin_snapshots and returns
{ plugins, snapshots, generatedAt } (snapshot read uses raw SQL
since this is the only place outside snapshots.ts that needs the
status / linkage flat).
CLI: `od plugin stats [--json]`. Pretty-print mode shows two
sections (Plugins / Snapshots) with sorted-key counts so the
output is byte-deterministic. printPluginHelp() updated.
Daemon tests: 1744 \u2192 1753 (+9 cases on plugins-stats:
empty-roster zero-shape, sourceKind / trust / taskKind counts,
elevated-capability detection (excludes prompt:inject; counts
fs:write / network / connector:* / subprocess / bash),
bundled vs. third-party split, newest installedAt / updatedAt
extraction; snapshot zero-shape + status / project / run /
oldest+newest applied tally).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
501fdf9e3a
|
feat(plugins): od plugin canon <snapshotId> show prompt block (Phase 4)
Plan CC1.
apps/daemon/src/server.ts: new `GET /api/applied-plugins/:snapshotId/canon`
returns the canonical `## Active plugin` block this snapshot
splices into the system prompt. Two response modes:
default \u2192 { snapshotId, pluginId, block }
Accept: text/plain \u2192 raw block body for shell pipes
Powered by the same renderPluginBlock() composeSystemPrompt() uses,
so the route output is byte-equal to what the agent reads. Useful
for:
- debugging 'why is the agent ignoring my plugin' (read what's
actually injected),
- locking byte-equality regression fixtures against the daemon's
renderPluginBlock() output,
- eyeballing what the prompt block looks like before applying.
CLI: `od plugin canon <snapshotId> [--json]`. Default output is
plain text suitable for piping; --json wraps the block in
{ snapshotId, pluginId, block }. printPluginHelp() updated.
Daemon tests: 1737 \u2192 1744 (+7 cases on plugins-canon: id +
version when title absent, pluginTitle wins when present, plugin
description appended, sorted alphabetic inputs block, byte-equal
output across calls with same snapshot (replay invariance check),
inputs block omitted when no inputs, query echoed as stylized
brief).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
f68d0148fd
|
feat(plugins): od atoms info <id> + Phase 6/7/8 catalog promotion
Plan AA2.
Two slices:
1. Catalog drift fix. apps/daemon/src/plugins/atoms.ts had
nine atoms still tagged status='planned' even though their
daemon impls landed across §3.N1-N4 / §3.O2-O5 / §3.P1-P2 /
§3.Q2 / §3.S1. Promotes them to 'implemented' + adds the
missing 'build-test' entry that was shipped without a
matching catalog row. Net result: every atom in the catalog
is now status='implemented'.
Updated entries:
code-import planned \u2192 implemented
design-extract planned \u2192 implemented
figma-extract planned \u2192 implemented
token-map planned \u2192 implemented
rewrite-plan planned \u2192 implemented
patch-edit planned \u2192 implemented
diff-review planned \u2192 implemented
handoff planned \u2192 implemented
build-test (new) \u2192 implemented
2. `od atoms info <id>` CLI + matching daemon route. New
GET /api/atoms/:id returns the catalog row plus the bundled
SKILL.md body (when one exists at
plugins/_official/atoms/<id>/SKILL.md), so a user can read
what the atom does + the prompt fragment that drives it from
one CLI invocation.
CLI behaviour:
od atoms info code-import # human-formatted
od atoms info code-import --json # raw JSON
Daemon tests: 1716 \u2192 1728 (+12 cases on plugins-atoms-info:
9 atoms confirmed promoted to 'implemented', build-test
catalog presence + matching task-kind, zero remaining 'planned'
atoms invariant, every atom has a non-empty taskKinds[]).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
bae57da565
|
feat(plugins): od plugin upgrade <id> re-install from recorded source (Phase 4)
Plan Z2.
apps/daemon/src/server.ts: new `POST /api/plugins/:id/upgrade`
route that streams the same SSE shape as
`POST /api/plugins/install`. Internally:
1. Loads the InstalledPluginRecord by id.
2. Rejects with 409 / code='bundled-plugin' when
sourceKind='bundled' (bundled plugins ship with the daemon
image and the bundled boot walker re-registers them every
boot — letting an operator 'upgrade' would silently overwrite
the daemon's authoritative copy).
3. Rejects with 409 / code='missing-source' when the recorded
source string is empty (manual SQLite tampering escape hatch).
4. Otherwise re-runs installPlugin() against the recorded
`plugin.source`. installPlugin() already routes by source
prefix (github: / https://*.tar.gz / local folder), so the
upgrade replays whichever byte path the original install
used.
CLI: `od plugin upgrade <id>`.
- Same SSE consumer as install, with [upgrade] prefixed lines.
- Exit 0 on success, 1 on installer error, 2 on usage error.
printPluginHelp() updated.
Daemon tests: 1702 \u2192 1705 (+3 cases on plugins-upgrade:
re-install bumps the registry version, re-running with the same
source on disk is idempotent, on-disk manifest stays in sync with
the SQLite row across an upgrade).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
b7f0fc0d96
|
feat(daemon): flip OD_BUNDLED_ATOM_PROMPTS default to ON (Phase 4)
Plan V1 / spec §23.4.
The bundled SKILL.md fragment library now covers every Phase 6/7/8
atom impl shipped this changeset (build-test, code-import,
design-extract, figma-extract, token-map, rewrite-plan, patch-edit,
diff-review, handoff). The §3.M3-M5 audit-block on flipping the
default-active flag is therefore lifted.
The new policy:
default: ON (activeStageBlocks render)
OD_BUNDLED_ATOM_PROMPTS unset ON
OD_BUNDLED_ATOM_PROMPTS='' ON
OD_BUNDLED_ATOM_PROMPTS=1 ON
OD_BUNDLED_ATOM_PROMPTS=0 OFF (explicit opt-out for
snapshot replay against
pre-§3.V1 daemons + regression
bisects that need byte-equal
pre-flip prompts)
OD_BUNDLED_ATOM_PROMPTS=* ON (forward-compat)
Effect: a run with a plugin snapshot whose pipeline carries any
of the bundled atoms now ALWAYS gets the matching SKILL.md fragments
spliced in as 'Active stage' blocks ahead of the rest of the prompt,
without operators having to set the env var. Runs without a
snapshot keep byte-equal prompts (the snapshotId guard short-
circuits the build).
Daemon tests: 1661 \u2192 1666 (+5 cases on
plugins-bundled-atom-prompts-default: ON when unset / empty / '1'
/ unknown values; OFF only when explicit '0').
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
a1bfac34f6
|
feat(plugins): wire diff-review GenUI response \u2192 review/decision.json (Phase 8)
Plan R1 / spec §10.3 / §21.5.
When the user (or agent) responds to the auto-derived
`__auto_diff_review_<stageId>` choice surface, the daemon now
immediately persists the decision into the run's project cwd as
`<cwd>/review/decision.json` so the next pipeline stage (handoff,
typically) sees the user's choice without an extra agent turn.
apps/daemon/src/plugins/atoms/diff-review-genui-bridge.ts
- isDiffReviewSurfaceId(id) — owns the
'__auto_diff_review_' prefix; future renames flow from one
constant.
- parseDiffReviewGenuiResponse(value) — strict JSON validator
coercing the surface payload into runDiffReview's
{ decision, accepted_files?, rejected_files?, reason? }
shape. Rejects non-object payloads + unknown decisions.
- applyDiffReviewDecisionToCwd({ cwd, value, reviewer })
— end-to-end glue: parses, calls runDiffReview(), returns
structured ok/error so the caller can decide what to surface.
apps/daemon/src/server.ts POST /api/runs/:runId/genui/:surfaceId/respond
now becomes async and, when isDiffReviewSurfaceId() matches, looks
up the run's projectId via design.runs.get(), resolves the project
cwd via resolveProjectDir(), and calls
applyDiffReviewDecisionToCwd() with reviewer='user' (or 'agent' /
'auto' when respondedBy says so).
Best-effort wiring:
- Failures are caught and surfaced on the response payload as
`diffReviewBridge: { ok: false, error }` so the agent can
retry without the GenUI respond contract regressing.
- No-project runs return diffReviewBridge: { ok: false, error:
'run is not linked to a project' }.
Daemon tests: 1618 → 1629 (+11 cases on
plugins-diff-review-genui-bridge: prefix detection, payload
parsing happy + sad paths (non-object, malformed decision,
non-string file lists, optional reason forwarding), end-to-end
applyDiffReviewDecisionToCwd writes review/decision.json on
accept, agent reviewer + reason forwarding, partial-missing-file
rejection, malformed value does NOT touch disk).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
694289295a
|
feat(plugins): wire bundled-scenario pipeline fallback through apply (spec §23.3.3)
Plan O1 / spec §23.3.3.
When a plugin omits `od.pipeline`, applyPlugin() now consults the
bundled scenario plugins registered by the daemon's bundled boot
walker (apps/daemon/src/plugins/bundled.ts) and copies the matching
scenario's pipeline verbatim into the snapshot. Match key:
`od.taskKind` (defaulting to 'new-generation' when absent).
Pure-side (packages/plugin-runtime):
- new resolveAppliedPipeline({ manifest, scenarios }) →
{ pipeline, source: 'declared' | 'scenario' | 'none', scenarioId? }
- RegistryView gains optional `scenarios?: ScenarioRegistryEntry[]`
so the daemon registry view propagates the lookup table without
coupling the pure resolver to SQLite.
Daemon-side (apps/daemon):
- applyPlugin() calls resolveAppliedPipeline and stores the result on
BOTH `ApplyResult.pipeline` and `AppliedPluginSnapshot.pipeline`
so a replay reproduces the same stages without re-consulting the
scenario registry.
- server.loadPluginRegistryView() now collects bundled scenarios
from `installed_plugins` (source_kind='bundled', od.kind='scenario',
non-empty pipeline). Third-party scenarios are intentionally NOT
eligible — only rows seeded by the bundled boot walker.
Invariants preserved:
- Apply stays pure: scenarios are read once at the registry-view
construction site and passed in; apply never opens a DB itself.
- Scenario plugins themselves never fall back to themselves
(resolveAppliedPipeline returns 'none' when od.kind='scenario').
- manifestSourceDigest is unaffected by the fallback (digest is over
manifest + inputs + resolvedContextRefs); replay invariance held.
Tests: +15 (plugin-runtime: 7 cases on resolveAppliedPipeline;
daemon: 8 cases on plugins-scenario-fallback covering declared
pipeline win, scenario fallback by taskKind, default to
new-generation, scenario plugins don't recurse, no scenarios →
pipeline=undefined, manifestSourceDigest stability across applies,
DB collector round-trip).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
d491b3b7da
|
feat(plugins): wire activeStageBlocks under OD_BUNDLED_ATOM_PROMPTS=1
Plan M2 / spec §23.4 / §23.3.2 patch 2.
Closes the runtime half of the §23.3.2 lift. When
OD_BUNDLED_ATOM_PROMPTS=1 is set AND the run carries an
appliedPluginSnapshotId whose snapshot has a non-empty
od.pipeline.stages[*], composeDaemonSystemPrompt now:
1. Fetches the snapshot from applied_plugin_snapshots.
2. For each stage, calls loadAtomBodies(db, stage.atoms ?? []).
3. Calls renderActiveStageBlock({ stageId, bodies }) for each
stage; skips blocks that come back empty (no bundled atom
resolved).
4. Threads the resulting array as composeSystemPrompt's
activeStageBlocks, which the composer splices in immediately
after the block.
Default behaviour (flag unset OR no snapshot OR no pipeline) is
byte-equal to today's prompt. Errors during atom-body loading
surface as a single console.warn and never block the run; the
composer falls through to the inline DISCOVERY_AND_PHILOSOPHY
constants.
The run's appliedPluginSnapshotId is forwarded from the in-memory
run object set up by POST /api/runs (plan §3.A1). Non-plugin runs
pass undefined and skip the entire branch.
Daemon tests stay at 1519/1519 (the new branch is opt-in; existing
tests don't set the env flag, so the default path is exercised
unchanged. The activeStageBlocks composer field already has 3
unit tests from §3.L2.)
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
61931faef9
|
feat(plugins): plugin-bundled component surface (sandboxed iframe)
Plan L3 / spec §10.3.5 / §9.2.
Web: GenUISurfaceRenderer now renders a sandboxed iframe when the
surface declares a component path. Communication is one-way: the
iframe posts `{ kind: 'genui:respond', surfaceId, value }` envelopes
back to the parent via window.postMessage; the parent forwards to
onAnswered. The iframe carries `sandbox="allow-scripts"` only — no
allow-same-origin / forms / popups / downloads, exactly as spec §9.2
mandates.
PendingSurface gains `componentPluginId`, supplied by the host (from
the run's AppliedPluginSnapshot.pluginId). Surfaces missing that
field render an inline error so a misconfigured host fails loudly.
Daemon: GET /api/plugins/:id/asset/* serves arbitrary files inside an
installed plugin's fsPath under the §9.2 preview CSP:
Content-Security-Policy: default-src 'none'; img-src 'self' data: blob:;
media-src 'self' data: blob:;
style-src 'self' 'unsafe-inline';
script-src 'self' 'unsafe-inline';
connect-src 'none'; frame-ancestors 'self'
X-Content-Type-Options: nosniff
Three guards: unknown plugin id → 404, traversal segments / absolute
paths → 400, escape-via-resolved-path → 400. Content-Type maps
common asset extensions; everything else falls back to
application/octet-stream so the browser's nosniff respects it.
Daemon tests: 1499 → 1503 (+4 cases on plugins-asset-route covering
404 / traversal-rejection / 200 with CSP headers / asset-not-found).
Web typecheck clean; jsdom tests stay at 586/586 (the iframe path is
DOM-shape-locked and has no callable surface to drive without a
real browser).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
bef9add981
|
feat(plugins): bound-API-token guard + bearer middleware (Phase 5)
Plan K1 / spec §15.7 / §16 Phase 5.
The daemon now ships a two-part hosted-mode safety floor:
1. startServer() refuses to start when OD_BIND_HOST is set to a
non-loopback address and OD_API_TOKEN is unset. Loopback hosts
(127.0.0.1 / ::1 / localhost) keep the existing zero-config
desktop / dev experience. The error message points the operator
at `openssl rand -hex 32` so the next attempt succeeds in one
step.
2. When OD_API_TOKEN is set, every /api/* request must carry an
`Authorization: Bearer <OD_API_TOKEN>` header. Three escape
hatches:
- Loopback peers (req.socket.remoteAddress matches
isLoopbackPeerAddress) skip the check — the desktop UI / local
CLI never need a bearer.
- The /api/health, /api/version, and /api/daemon/status probes
remain open so monitoring + cloud orchestrators (k8s readiness,
Compose healthcheck) work without touching secrets.
- Mismatched / missing headers return 401 API_TOKEN_REQUIRED.
This closes a previously documented Phase 5 hazard: an operator
spinning up the daemon with OD_BIND_HOST=0.0.0.0 (the docker-compose
default) without a token would have published an unsecured API.
Daemon tests: 1486 → 1490 (+4 cases on api-token-guard:
non-loopback bind without token rejected, public host with token
boots, loopback callers still pass without bearer, probes stay open).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
0631f04a00
|
feat(plugins): @open-design/agui-adapter package + GET /api/runs/:id/agui
Plan J1 + J2 / spec §10.3.5 / Phase 4. New workspace package: packages/agui-adapter/. Pure-TS bidirectional bridge between OD's native PersistedAgentEvent / GenUIEvent / PluginPipelineStageEvent union and the AG-UI canonical event protocol (https://github.com/CopilotKit/CopilotKit). - src/types.ts — AGUIEvent discriminated union (agent.message, tool_call, state_update, ui.surface_requested, ui.surface_responded, run.lifecycle). - src/encode.ts — encodeOdEventForAgui(event, ctx): maps every OD native event onto the canonical shape; drops events the encoder can't translate so external AG-UI clients always see a clean stream. - tests/encode.test.ts (9 cases) covers message_chunk, tool_call, run_started, end → started/completed/failed/ cancelled, pipeline_stage_started/completed, genui_surface_request/response/timeout, genui_state_synced, and the unknown-event drop. apps/daemon/src/server.ts mounts GET /api/runs/:id/agui: - 404 for unknown run ids. - Replays the run's recorded events through the encoder on subscribe (so a reconnecting client with Last-Event-ID picks up exactly the AG-UI events it missed). - Subscribes to future events via a thin adapter client wrapper that routes through the existing run.clients fan-out, so the encoder runs lazily on each broadcast (no double event buffering). Daemon depends on @open-design/agui-adapter; the package builds clean and ships pure ESM. v1 plugins consume CopilotKit / agent-protocol clients without modification — the adapter ships independently from daemon main, so upstream protocol revs do not couple to the daemon release cadence (per spec §10.3.5 Phase 4 contract). Tests: agui-adapter 9/9, daemon 1481 → 1482 (+1 case on agui-route). Co-authored-by: Tom Huang <1043269994@qq.com> |
||
|
|
2848327199
|
feat(plugins): bundled atom plugins + boot walker (Phase 4 / spec §23)
Plan I3 / spec §23.3.5 + §23.3.4.
Lays the entry slice for spec §23 self-bootstrapping: the daemon's
own behavioural content moves out of system.ts hard-coded constants
and into per-atom SKILL.md files under plugins/_official/atoms/.
New ground:
- plugins/_official/atoms/ ships four canonical atom SKILL.md +
open-design.json pairs (discovery-question-form, todo-write,
direction-picker, critique-theater). Each pair is a fully valid
plugin that the existing local-folder installer can pick up; the
manifest declares od.kind='atom' so the future doctor warns when a
pipeline references the matching id but the bundled folder is
missing.
- apps/daemon/src/plugins/bundled.ts owns the boot walker:
registerBundledPlugins({ db, bundledRoot }) walks one level of
subdirectories (atoms/, scenarios/, bundles/) plus direct
plugins, registers every match into installed_plugins under
source_kind='bundled' / trust='bundled', and is idempotent across
daemon restarts (upserts on every boot).
- defaultBundledRoot(workspaceRoot) returns
<workspaceRoot>/plugins/_official; server.ts boot calls
registerBundledPlugins(..., defaultBundledRoot(PROJECT_ROOT)) and
prints a one-line summary. ENOENT is silent — running the daemon
outside the dev tree just skips this step.
The full §23 lift (system.ts prompt fragments → SKILL.md bodies +
plugin.kind='atom' → composeSystemPrompt resolving each stage's
atoms[] through the registry) stays open. This commit is the
substrate slice: bundled plugins are now first-class
installed_plugins rows under the new tier, doctor / list / info /
apply all pick them up, and a future PR that wires composeSystemPrompt
to read SKILL.md bodies has zero registration work to do.
Daemon tests: 1476 → 1481 (+5 cases on plugins-bundled covering
tiered + direct layouts, idempotent reruns, ENOENT, miss-skips).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
deb74d91c4
|
feat(plugins): wire pipeline runner into POST /api/runs (e2e-3 full contract)
Plan I1 + I4 / spec §10.1 / plan §8 e2e-3. Lifts e2e-3 from entry-slice to the full §8 contract: 'first ND-JSON event has kind=pipeline_stage_started'. POST /api/runs gains a synchronous firePipelineForRun() call that runs immediately after design.runs.create() + linkSnapshotToRun(), BEFORE design.runs.start() schedules the agent. The first pipeline_stage_started event lands on the run's SSE buffer ahead of any message_chunk the agent emits. The remaining stage timeline (stage_started/completed for stages 2..N, plus the run_devloop_iterations audit row per stage) fires asynchronously while the agent runs. The stage runner is a stub that returns converged signals (critique.score=4, preview.ok=true, user.confirmed=true) so a non-loop pipeline walks through every stage in O(stages) time. Loop stages still respect OD_MAX_DEVLOOP_ITERATIONS. Phase 4's atom migration into plugins/_official/atoms/<atom>/SKILL.md will swap the stub for a real per-stage worker that drives the agent. Errors inside the pipeline runner emit pipeline_stage_failed onto the run stream and never block the agent process — a buggy plugin manifest can never deadlock a chat run. apps/daemon/tests/plugins-headless-run.test.ts gains a second case that installs a fixture plugin with two declared stages, creates a project + run, then reads the SSE event stream and asserts the first observed stage event is pipeline_stage_started. The fixture grants pipeline:* via the resolver's grantCaps[] field so the restricted-trust capability gate stays in force for non-pipeline runs. Daemon tests: 1475 → 1476. Co-authored-by: Tom Huang <1043269994@qq.com> |
||
|
|
712c18dbc2
|
feat(cli): od atoms / skills / design-systems / craft / status / version + marketplace search
Plan H2 + H3 + H4 / spec §12.2 / §11.7.
Closes the CLI parity remainder: every UI feature reachable via
/api/* now has a matching CLI verb.
od atoms list List first-party atoms.
od atoms show <id> Print one atom (status + taskKinds).
od skills list / show <id> Wraps GET /api/skills{,/:id}.
od design-systems list / show <id> Wraps GET /api/design-systems{,/:id}.
od craft list / show <id> NEW endpoints GET /api/craft{,/:id}
walk the craft/ directory and
emit { id, label, bytes }.
od status Alias of `od daemon status`.
od version Wraps GET /api/version; --json
emits the structured shape.
od marketplace search "<query>" New verb. Walks every configured
[--tag <tag>] marketplace's plugins[] and
matches by substring on
name + description + tags.
The HTTP additions are GET /api/craft (catalog summary) and
GET /api/craft/:id (raw markdown body), both safe-slug-validated.
Daemon tests: 1472 → 1475 (+3 cases on craft-route covering catalog
shape, malformed slug rejection, miss-as-404).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
04784013e6
|
feat(plugins): od plugin scaffold + od plugin export (Phase 4 author tooling)
Plan G1 + G2 / spec §14.
Two new author-side CLI verbs that close the spec §14 'one repo, every
catalog' loop without leaving the terminal.
apps/daemon/src/plugins/scaffold.ts owns the scaffold path:
od plugin scaffold --id <id> [--title] [--description]
[--task-kind <new-generation|code-migration|
figma-migration|tune-collab>]
[--mode <m>] [--scenario <s>]
[--out <dir>] [--with-claude-plugin]
Writes <out>/<id>/{SKILL.md, open-design.json, README.md} (plus the
optional .claude-plugin/plugin.json for cross-catalog publishing).
Refuses to overwrite an existing scaffold; rejects unsafe ids.
apps/daemon/src/plugins/export.ts + POST /api/applied-plugins/export
own the export path. Three targets:
od → SKILL.md + open-design.json + provenance README
claude-plugin → SKILL.md + .claude-plugin/plugin.json + README
agent-skill → SKILL.md + README only (every catalog accepts this)
od plugin export <projectId> --as od|claude-plugin|agent-skill --out <dir>
od plugin export --snapshot-id <id> --as <target> --out <dir>
The exporter pulls SKILL.md straight off the installed plugin's fsPath
when available and otherwise synthesises one from the snapshot's
frozen plugin metadata, so a publishable folder is reproducible even
after od plugin update / uninstall rotates the live source. The
generated open-design.json carries a provenance block recording
snapshot id + manifestSourceDigest so a downstream catalog viewer can
reverse-resolve the originating run.
The HTTP route is loopback-only (requireLocalDaemonRequest) because
it writes to the host filesystem; the CLI is the canonical caller.
Daemon tests: 1455 → 1465 (+10): plugins-scaffold (5 cases) +
plugins-export (5 cases).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
9927707361
|
feat(plugins): od plugin install <name> resolves through marketplaces
Plan §3.F3 / spec §7.2 + §6 / §16 Phase 3. `resolvePluginInMarketplaces(db, name)` walks every configured marketplace in registration order and returns the first matching plugin entry. The matched entry carries: - the canonical source string (github:owner/repo, https://…tar.gz) that the existing installer backends already understand - the marketplace trust tier and id for the audit trail - the optional description for surface display POST /api/plugins/install detects bare-name sources (anything that isn't a local path / github: / https://) and re-routes through `resolvePluginInMarketplaces` before handing off to the installer. On miss the route returns 404 plugin-not-found with structured error data, so the CLI's structuredHttpFailure helper maps it to exit 65. `od plugin install <name>` (no flag) now works end-to-end. The existing `./folder`, `github:…` and `https://*.tar.gz` shapes are unchanged. Note: per spec §9, marketplace trust does NOT propagate to the installed plugin: a plugin pulled from a 'trusted' marketplace still defaults to its source-kind tier. Trust elevation goes through `od plugin trust` / the Phase 3 trust UI. Daemon tests: 1450 → 1454 (added 4 cases to plugins-marketplaces: canonical-source resolution, case insensitivity, miss → null, registration-order tie-break). Co-authored-by: Tom Huang <1043269994@qq.com> |
||
|
|
1ae977e066
|
feat(cli): od daemon start/status/stop + new HTTP daemon-lifecycle routes
Plan §6 Phase 1.5 / spec §11.7.
The desktop default `od` (no subcommand) keeps its current behaviour
for back-compat. New explicit lifecycle entry:
od daemon start [--headless] [--serve-web] [--port <n>] [--host <addr>]
[--no-open] [--namespace <ns>]
--headless / --no-open / --serve-web all imply 'do not auto-open the
browser', so a code agent / CI runner / Docker entry point can launch
the daemon without electron. The port + host flags fall through to
OD_PORT / OD_BIND_HOST when unset, matching every other surface.
od daemon status [--json] [--daemon-url <url>]
od daemon stop [--daemon-url <url>]
These two CLI verbs hit the new HTTP routes:
GET /api/daemon/status → { ok, version, bindHost, port, pid,
dataDir, mediaConfigDir, namespace,
installedPlugins, shuttingDown }
POST /api/daemon/shutdown → 202; emits SIGTERM so the existing
graceful close path runs unchanged.
requireLocalDaemonRequest enforces the
same loopback / origin check used by
composio config writes (no public
shutdown surface).
This makes `od daemon start --headless --port 17456` followed by
`od plugin install ./fixtures/sample-plugin` and the rest of the
§12.5 walkthrough reachable without electron — exactly the substrate
e2e-3 needs.
Daemon tests: 1448 → 1450 (added daemon-lifecycle with 2 cases:
status snapshot shape; shutdown rejects non-loopback callers).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
0df9176c2f
|
feat(plugins): marketplace registry + od marketplace CLI verbs
Plan §3.B4 / spec §6 / §7.2 / §16 Phase 3 entry slice.
The storage half of federated catalogs:
- New apps/daemon/src/plugins/marketplaces.ts owns add / list / get /
refresh / remove / set-trust over the pre-existing plugin_marketplaces
SQLite table. Uses parseMarketplace from @open-design/plugin-runtime so
validation matches the schema in docs/schemas/open-design.marketplace.v1.json.
Default trust tier is 'restricted' (discovery only) per spec §9.
- HTTP routes:
* GET /api/marketplaces
* POST /api/marketplaces (body { url, trust? })
* GET /api/marketplaces/:id
* DELETE /api/marketplaces/:id
* POST /api/marketplaces/:id/refresh
* POST /api/marketplaces/:id/trust (body { trust })
* GET /api/marketplaces/:id/plugins (catalog passthrough)
- CLI verbs: `od marketplace add/list/info/refresh/remove/trust`.
--trust accepts 'trusted', 'restricted', or 'official'.
Phase 3 follow-up wires `od plugin install <name>` resolution + the trust
UI on top; this commit is the data-layer half so the CLI can already
register and inspect catalogs.
Daemon tests: 1438 → 1443 (added plugins-marketplaces with 5 cases).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
b9d40094b5
|
feat(plugins): github tarball + https archive install sources
Plan §3.A6 / spec §7.2.
The installer gains a top-level dispatcher `installPlugin` that picks
the right backend off the source string:
- ./folder, /abs/path → installFromLocalFolder (existing behavior)
- github:owner/repo[@ref][/subpath]
→ fetch codeload tarball, extract via tar.x,
optionally chroot into a subpath, then re-use
the local backend for copy / persist
- https://*.tar.gz, *.tgz
→ same archive backend, recorded as source_kind=url
Hard guards inherited from spec §7.2:
- Symbolic / hard link entries inside the archive abort the install with
a clean error.
- Path-traversal segments (..) abort.
- Total extracted size is measured against maxBytes (50 MiB default) and
rejected if exceeded.
- Local-folder backend now preserves the recorded source / source_kind
when called from the archive backend so installed_plugins records
accurate provenance.
POST /api/plugins/install accepts the new shapes; the SSE event stream
shape is unchanged. The fetcher is pluggable so tests don't need network.
apps/daemon/package.json adds tar@^7 + @types/tar@^6.
Daemon tests: 1429 → 1434 (added plugins-installer-archive with 5 cases).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
39820be28b
|
feat(plugins): snapshot GC worker + od plugin run shorthand
Plan §3.A5 (snapshot GC) + §3.B3 (od plugin run shorthand).
- New apps/daemon/src/plugins/gc.ts wraps pruneExpiredSnapshots() in a
periodic worker driven by OD_SNAPSHOT_GC_INTERVAL_MS. Daemon boot
starts the worker and runs an immediate sweep so a fresh daemon does
not wait the full interval before reaping pre-existing expired rows.
Setting the interval to 0 disables the worker entirely.
- New HTTP endpoints:
* GET /api/applied-plugins → list all snapshots
* GET /api/projects/:projectId/applied-plugins → list snapshots for
one project
* POST /api/applied-plugins/prune { before? } → operator escape hatch
that calls pruneExpiredSnapshots() with the optional cutoff so a
hosted operator can force-delete unreferenced rows older than ts.
- New CLI verbs:
* od plugin snapshots list [--project <id>]
* od plugin snapshots prune [--before <unix-ms>]
* od plugin run <id> --project <projectId> [--inputs <json>]
[--grant-caps a,b] (apply + run start shorthand). Capability gate
failures map to exit 66; missing-input to exit 67.
- ConnectorApiErrorCode gains CONNECTOR_NOT_GRANTED so the §3.A3 gate
rejection can use the canonical sender.
Daemon tests: 1425 → 1429 (added plugins-snapshot-gc with 4 cases).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
44ea30c986
|
feat(plugins): trust mutation + connector tool-token gate
Plan §3.A2 (trust mutation) and §3.A3 (connector capability gate).
- New POST /api/plugins/:id/trust accepts { capabilities[], action: 'grant' | 'revoke' }
and unions the spec §5.3 vocabulary into installed_plugins.capabilities_granted.
Unknown / malformed strings come back as HTTP 400 with the rejected list so the
CLI can surface exit-2 usage advice. The implicit prompt:inject floor is
preserved on revoke so a plugin never falls below the spec minimum.
- trust.ts gains validateCapabilityList / grantCapabilities / revokeCapabilities.
These are the only writers of installed_plugins.capabilities_granted outside
of install.
- od plugin trust calls the new endpoint instead of printing the deferred
Phase 3 stub. --revoke flag and --grant-caps flag added; PLUGIN_BOOLEAN_FLAGS
/ PLUGIN_STRING_FLAGS extended.
- Tool-token grants now optionally carry pluginSnapshotId / pluginTrust /
pluginCapabilitiesGranted. The mint site in startChatRun resolves these
off the run's appliedPluginSnapshotId so the connector execute route can
re-validate per-call without re-reading SQLite.
- /api/tools/connectors/execute calls a new pure helper
checkConnectorAccess(grant, connectorId). Trusted / bundled implicitly
carry connector:*; restricted plugins must list connector:<id> (or the
coarse connector capability) — otherwise 403 CONNECTOR_NOT_GRANTED.
Daemon test suite: 1417 / 1417 (added plugins-trust + plugins-tool-token-gate).
Co-authored-by: Tom Huang <1043269994@qq.com>
|
||
|
|
aa3f857653
|
feat(plugins): wire snapshot resolver into POST /api/projects and /api/runs
Adds the snapshot side-effect bridge between the pure applyPlugin() and the daemon's existing project / run create paths (plan §3.A1, spec §11.5). - New apps/daemon/src/plugins/resolve-snapshot.ts owns resolution: explicit appliedPluginSnapshotId or pluginId in the request body produces a persisted AppliedPluginSnapshot, validates capability gates, and pins the row to the new project / conversation / run. Missing-input maps to HTTP 422 (exit 67), capabilities-required to HTTP 409 (exit 66), not-found to HTTP 404 (exit 65), stale to HTTP 409 (exit 72). - snapshots.ts gains linkSnapshotToProject / linkSnapshotToConversation / pruneExpiredSnapshots. Snapshot becomes referenced (expires_at = NULL) the moment any of those links land, per PB2 reproducibility-first. - runs.ts persists appliedPluginSnapshotId / pluginId on the in-memory run object and surfaces them on statusBody(). - POST /api/runs and POST /api/projects gained pluginId / pluginInputs / appliedPluginSnapshotId / grantCaps fields. Legacy bodies behave unchanged. - /api/plugins/:id/apply gains a grantCaps[] union for ephemeral capability grants scoped to the snapshot. - /api/plugins/:id/doctor and /api/plugins/:id/apply share a new loadPluginRegistryView() helper instead of inlining the catalog walk. - genui/index.ts disambiguates the respondSurface re-export so the public registry version stays canonical and the store-only writer is reached via respondSurfaceRow. Daemon test suite stays green (1405 / 1405). Co-authored-by: Tom Huang <1043269994@qq.com> |
||
|
|
89be57b2c4 |
feat(genui): introduce GenUI surface management and event handling
- Added a new GenUI module for managing user interface surfaces, including creation, response handling, and state synchronization. - Implemented API endpoints for listing and responding to GenUI surfaces associated with runs and projects. - Introduced event types and payload helpers for GenUI surface events, enhancing the interaction model for headless operations. - Established a persistent state writer for GenUI surfaces, ensuring reliable data management and retrieval. - Enhanced the plugin system to support auto-derived OAuth prompts for required connectors, improving user experience during plugin application. |
||
|
|
4c7cd5d9f2 |
feat(plugins): introduce plugin system with installation and management capabilities
- Added support for a new plugin system, allowing users to install, uninstall, and manage plugins through the daemon. - Implemented API endpoints for listing installed plugins, retrieving plugin details, and applying plugins with input validation. - Introduced a plugin doctor feature to validate plugin manifests and check for issues before application. - Established a plugin persistence layer with SQLite migrations for managing installed plugins and their metadata. - Enhanced the CLI with commands for plugin operations, improving user interaction with the plugin ecosystem. |
||
|
|
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>
|
||
|
|
109722de3a
|
feat(desktop): export artifacts directly to PDF (#532)
* feat(desktop): export artifacts directly to PDF * fix(desktop): PDF 내보내기 기본 여백 제거 |
||
|
|
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) |
||
|
|
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>
|