rivet-design 0.11.5 → 0.11.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/mcp/agent-variants/SessionStore.d.ts +39 -9
  2. package/dist/mcp/agent-variants/SessionStore.d.ts.map +1 -1
  3. package/dist/mcp/agent-variants/SessionStore.js +36 -16
  4. package/dist/mcp/agent-variants/SessionStore.js.map +1 -1
  5. package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts +1 -1
  6. package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts.map +1 -1
  7. package/dist/mcp/agent-variants/WorktreeOrchestrator.js +111 -63
  8. package/dist/mcp/agent-variants/WorktreeOrchestrator.js.map +1 -1
  9. package/dist/mcp/agent-variants/contracts.d.ts +18 -2
  10. package/dist/mcp/agent-variants/contracts.d.ts.map +1 -1
  11. package/dist/mcp/agent-variants/contracts.js +6 -3
  12. package/dist/mcp/agent-variants/contracts.js.map +1 -1
  13. package/dist/mcp/agent-variants/createZeroToOneTool.d.ts.map +1 -1
  14. package/dist/mcp/agent-variants/createZeroToOneTool.js +26 -1
  15. package/dist/mcp/agent-variants/createZeroToOneTool.js.map +1 -1
  16. package/dist/mcp/agent-variants/designCritique.d.ts.map +1 -1
  17. package/dist/mcp/agent-variants/designCritique.js +57 -0
  18. package/dist/mcp/agent-variants/designCritique.js.map +1 -1
  19. package/dist/mcp/agent-variants/tools.d.ts +7 -0
  20. package/dist/mcp/agent-variants/tools.d.ts.map +1 -1
  21. package/dist/mcp/agent-variants/tools.js +40 -4
  22. package/dist/mcp/agent-variants/tools.js.map +1 -1
  23. package/dist/mcp/agent-variants/variantContext.d.ts +1 -0
  24. package/dist/mcp/agent-variants/variantContext.d.ts.map +1 -1
  25. package/dist/mcp/agent-variants/variantContext.js +3 -3
  26. package/dist/mcp/agent-variants/variantContext.js.map +1 -1
  27. package/dist/mcp/hostedServer.d.ts.map +1 -1
  28. package/dist/mcp/hostedServer.js +8 -1
  29. package/dist/mcp/hostedServer.js.map +1 -1
  30. package/dist/mcp/instructions.d.ts +16 -0
  31. package/dist/mcp/instructions.d.ts.map +1 -0
  32. package/dist/mcp/instructions.js +18 -0
  33. package/dist/mcp/instructions.js.map +1 -0
  34. package/dist/mcp/server.d.ts +9 -0
  35. package/dist/mcp/server.d.ts.map +1 -1
  36. package/dist/mcp/server.js +28 -5
  37. package/dist/mcp/server.js.map +1 -1
  38. package/dist/services/createAgentVariantsOrchestrator.d.ts +8 -0
  39. package/dist/services/createAgentVariantsOrchestrator.d.ts.map +1 -1
  40. package/dist/services/createAgentVariantsOrchestrator.js +18 -8
  41. package/dist/services/createAgentVariantsOrchestrator.js.map +1 -1
  42. package/package.json +1 -1
  43. package/src/ui/dist/assets/main-CtRINpOq.css +1 -0
  44. package/src/ui/dist/assets/main-DM4pFbJk.js +641 -0
  45. package/src/ui/dist/index.html +2 -2
  46. package/src/ui/dist/assets/main-E5hevKg2.css +0 -1
  47. package/src/ui/dist/assets/main-SvoqOpMt.js +0 -641
@@ -42,7 +42,7 @@ const DESIGN_CONTEXT_VIEW_SEGMENT = 'view';
42
42
  // fast and legibly instead. Per-git-op hangs are already caught by simple-git's
43
43
  // block timeout in WorktreeManager; this is the cross-op backstop. Override via
44
44
  // env for unusually large repos.
45
- const DEFAULT_PROVISION_TIMEOUT_MS = 180_000;
45
+ const DEFAULT_PROVISION_TIMEOUT_MS = 540_000;
46
46
  const PROVISION_TIMEOUT_MS = (() => {
47
47
  const raw = process.env.RIVET_VARIANTS_PROVISION_TIMEOUT_MS;
48
48
  const parsed = raw ? Number.parseInt(raw, 10) : NaN;
@@ -188,10 +188,18 @@ function copyAssetIntoWorktree(worktreePath, entry, assetSourceRoot) {
188
188
  if (!path_1.default.isAbsolute(assetSourceRoot)) {
189
189
  throw new errors_1.AgentVariantsError('RUNTIME_VALIDATION_FAILED', `assetSourceRoot must be an absolute path, got '${assetSourceRoot}'`);
190
190
  }
191
- if (!path_1.default.isAbsolute(entry.source)) {
192
- throw new errors_1.AgentVariantsError('RUNTIME_VALIDATION_FAILED', `assetPlan.source must be an absolute path, got '${entry.source}'`);
193
- }
194
- const ext = path_1.default.extname(entry.source).toLowerCase();
191
+ // A relative source is resolved against the approved asset root. The
192
+ // agent-facing protocol requires the source to live under that root but not
193
+ // to be absolute, and the root path is internal to Rivet (a staged context
194
+ // bundle) so a relative name such as 'avatar.glb' is the natural, safe
195
+ // form. Every downstream guard (symlink, extension, within-root, sensitive
196
+ // segments) runs against this resolved absolute path, so the resolution
197
+ // cannot widen what is copyable: a relative `..` escape still fails the
198
+ // within-root check below.
199
+ const absoluteSource = path_1.default.isAbsolute(entry.source)
200
+ ? entry.source
201
+ : path_1.default.resolve(assetSourceRoot, entry.source);
202
+ const ext = path_1.default.extname(absoluteSource).toLowerCase();
195
203
  if (!ALLOWED_ASSET_EXTENSIONS.has(ext)) {
196
204
  throw new errors_1.AgentVariantsError('RUNTIME_VALIDATION_FAILED', `assetPlan.source extension '${ext || '(none)'}' is not on the allowlist (got '${entry.source}'). Only inert media/font/document assets may be copied.`);
197
205
  }
@@ -201,7 +209,7 @@ function copyAssetIntoWorktree(worktreePath, entry, assetSourceRoot) {
201
209
  // assetPlan entry is to name a concrete file.
202
210
  let lstat;
203
211
  try {
204
- lstat = fs_1.default.lstatSync(entry.source);
212
+ lstat = fs_1.default.lstatSync(absoluteSource);
205
213
  }
206
214
  catch {
207
215
  throw new errors_1.AgentVariantsError('RUNTIME_VALIDATION_FAILED', `assetPlan.source not found on disk: ${entry.source}`);
@@ -217,7 +225,7 @@ function copyAssetIntoWorktree(worktreePath, entry, assetSourceRoot) {
217
225
  // file kind on the resolved path.
218
226
  let resolvedSource;
219
227
  try {
220
- resolvedSource = fs_1.default.realpathSync(entry.source);
228
+ resolvedSource = fs_1.default.realpathSync(absoluteSource);
221
229
  }
222
230
  catch {
223
231
  throw new errors_1.AgentVariantsError('RUNTIME_VALIDATION_FAILED', `assetPlan.source could not be resolved: ${entry.source}`);
@@ -233,21 +241,28 @@ function copyAssetIntoWorktree(worktreePath, entry, assetSourceRoot) {
233
241
  if (!resolvedStat.isFile()) {
234
242
  throw new errors_1.AgentVariantsError('RUNTIME_VALIDATION_FAILED', `assetPlan.source resolved target must be a regular file: ${entry.source}`);
235
243
  }
236
- // Symlinked parent directory defense: even though we rejected a symlink
237
- // leaf and confirmed the resolved file is regular, an intermediate dir
238
- // could have been a symlink that quietly forwards into a sensitive
239
- // ancestor (e.g. `<asset-root>/avatar -> ~/.ssh`). Cross-check that
240
- // NEITHER the user-supplied path NOR its realpath traverses a known
241
- // sensitive segment such as `.ssh`, `.aws`, `credentials`, etc. Also
242
- // re-verify the extension on the resolved path so a `.glb` symlink
243
- // chain cannot smuggle in a `.json` realpath.
244
- if (hasSensitivePathSegment(entry.source) ||
245
- hasSensitivePathSegment(resolvedSource)) {
246
- throw new errors_1.AgentVariantsError('RUNTIME_VALIDATION_FAILED', `assetPlan.source traverses a sensitive directory and is refused: ${entry.source}`);
247
- }
244
+ // Containment is the security boundary and must be checked before any
245
+ // sensitive-segment scan. The approved asset root is system-chosen and
246
+ // trusted (a staged context bundle), and its realpath can legitimately
247
+ // pass through a "sensitive" segment that is NOT agent-controlled — on
248
+ // macOS `fs.realpathSync(os.tmpdir())` resolves to `/private/var/folders/...`,
249
+ // so the temp-staged root contains `private`. A symlinked intermediate dir
250
+ // that forwards into a sensitive ancestor (e.g. `<asset-root>/avatar -> ~/.ssh`)
251
+ // pushes the realpath OUTSIDE the root and is rejected here.
248
252
  if (!isPathWithinRoot(resolvedSource, resolvedAssetSourceRoot)) {
249
253
  throw new errors_1.AgentVariantsError('RUNTIME_VALIDATION_FAILED', `assetPlan.source must stay inside the approved asset root: ${entry.source}`);
250
254
  }
255
+ // Sensitive-segment defense, scoped to the agent-controlled portion BELOW
256
+ // the trusted root. Only this relative remainder is chosen by the agent's
257
+ // source plan, so a sensitive segment it introduces under the root (a real
258
+ // `<asset-root>/.ssh/key.glb`, or a within-root symlink that realpaths into
259
+ // one) is still refused, while the trusted root prefix is never scanned.
260
+ // `resolvedSource` is the fully symlink-resolved real path, so the scan
261
+ // covers what the file actually resolves to, not just its literal name.
262
+ const relativeResolvedSource = path_1.default.relative(resolvedAssetSourceRoot, resolvedSource);
263
+ if (hasSensitivePathSegment(relativeResolvedSource)) {
264
+ throw new errors_1.AgentVariantsError('RUNTIME_VALIDATION_FAILED', `assetPlan.source traverses a sensitive directory and is refused: ${entry.source}`);
265
+ }
251
266
  const resolvedExt = path_1.default.extname(resolvedSource).toLowerCase();
252
267
  if (!ALLOWED_ASSET_EXTENSIONS.has(resolvedExt)) {
253
268
  throw new errors_1.AgentVariantsError('RUNTIME_VALIDATION_FAILED', `assetPlan.source resolved extension '${resolvedExt || '(none)'}' is not on the allowlist (resolved from '${entry.source}').`);
@@ -913,49 +928,16 @@ class AgentVariantsOrchestrator {
913
928
  return undefined;
914
929
  if (!requestedPath || requestedPath.length === 0)
915
930
  return undefined;
916
- // Express has already URL-decoded `req.params.assetPath` for us calling
917
- // decodeURIComponent again would mangle filenames that legitimately
918
- // contain `%` (e.g. `100%25.png` on disk `100%.png` after the second
919
- // pass, 404). Use the path as Express delivered it.
920
- //
921
- // `record.assetBase` is already a realpath'd, within-workspace directory
922
- // (see `resolveStaticPreviewAssetBase`). Resolve the requested target
923
- // through `realpathSync` too — without this, an attacker who manages to
924
- // plant a symlink under the base (e.g. via a follow-up Write tool call)
925
- // could pivot outside the sandbox.
926
- const target = path_1.default.resolve(record.assetBase, '.' + path_1.default.sep + requestedPath);
927
- if (target !== record.assetBase &&
928
- !target.startsWith(record.assetBase + path_1.default.sep)) {
929
- return undefined;
930
- }
931
- let realTarget;
932
- try {
933
- realTarget = fs_1.default.realpathSync(target);
934
- }
935
- catch {
936
- return undefined;
937
- }
938
- if (realTarget !== record.assetBase &&
939
- !realTarget.startsWith(record.assetBase + path_1.default.sep)) {
940
- return undefined;
941
- }
942
- try {
943
- const stat = fs_1.default.statSync(realTarget);
944
- if (!stat.isFile())
945
- return undefined;
946
- // Hardlinks aren't dereferenced by `realpath` — they ARE the file from
947
- // a path-resolution perspective, so the realpath check above can't
948
- // detect them. An agent could `ln /path/to/.env assetBase/x.glb`
949
- // (hardlink, not symlink) and the resolver would happily serve the
950
- // secret. Reject anything with multiple links — legitimate
951
- // agent-generated assets always have nlink === 1.
952
- if (stat.nlink !== 1)
953
- return undefined;
954
- return realTarget;
955
- }
956
- catch {
957
- return undefined;
958
- }
931
+ // Resolve against the variant's own directory first so any asset the agent
932
+ // actually wrote/modified wins. Only when the worktree clone lacks the file
933
+ // do we fall back to the host project root that recovers git-ignored or
934
+ // binary assets the worktree provisioning couldn't carry over, which the
935
+ // original preview already serves (see `assetFallbackBase`). Both roots are
936
+ // held to the identical traversal/realpath/hardlink sandbox.
937
+ return (resolveAssetWithinBase(record.assetBase, requestedPath) ??
938
+ (record.assetFallbackBase
939
+ ? resolveAssetWithinBase(record.assetFallbackBase, requestedPath)
940
+ : undefined));
959
941
  }
960
942
  getStaticPreviewByBriefId(sessionId, briefId) {
961
943
  const resources = this.resources.get(sessionId);
@@ -2565,7 +2547,12 @@ class AgentVariantsOrchestrator {
2565
2547
  overall: qa.dimensionScores?.overall ?? null,
2566
2548
  });
2567
2549
  this.emitChange();
2568
- return result;
2550
+ // Carry the requeue signal + critique so the tool layer can tell the agent
2551
+ // to re-lease and regenerate. The store result is an opaque
2552
+ // `waiting_for_results` (the variant is pending again); on its own that is
2553
+ // indistinguishable from a sibling still in flight, so the agent would
2554
+ // otherwise abandon the variant and the session would stall at ready:0.
2555
+ return { ...result, requeuedForRegeneration: true, qa };
2569
2556
  }
2570
2557
  async startVariantDevServer(args) {
2571
2558
  if (args.record.port !== undefined && args.record.devServerProcess) {
@@ -2992,6 +2979,22 @@ class AgentVariantsOrchestrator {
2992
2979
  // sandbox symlink-normalization is skipped.
2993
2980
  dir = projectCwd;
2994
2981
  }
2982
+ // Host project root, used as a read-only fallback for assets the worktree
2983
+ // clone is missing (git-ignored or binary files that `git worktree add` +
2984
+ // text-patch + untracked-copy can't carry). Without this the original
2985
+ // preview shows such an asset while the variant 404s. Realpath'd so the
2986
+ // sandbox containment check in `resolveAssetWithinBase` lines up.
2987
+ let assetFallbackBase;
2988
+ try {
2989
+ const hostRealRoot = fs_1.default.realpathSync(env.projectPath);
2990
+ // Skip the fallback if it would collapse onto the worktree root (can't
2991
+ // happen in practice, but keeps the two roots genuinely distinct).
2992
+ if (hostRealRoot !== dir)
2993
+ assetFallbackBase = hostRealRoot;
2994
+ }
2995
+ catch {
2996
+ // No host root resolvable — serve worktree assets only.
2997
+ }
2995
2998
  resources.staticPreviews.set(workItemId, {
2996
2999
  workItemId,
2997
3000
  briefId: input.briefId,
@@ -2999,6 +3002,7 @@ class AgentVariantsOrchestrator {
2999
3002
  // Existing-mode static projects are real multi-file sites — assume
3000
3003
  // sibling assets so the preview route resolves relative URLs.
3001
3004
  hasAssets: true,
3005
+ ...(assetFallbackBase ? { assetFallbackBase } : {}),
3002
3006
  });
3003
3007
  log.info(`Variant ${workItemId} served as a static preview (existing/static, no dev server; root ${projectCwd})`);
3004
3008
  return true;
@@ -3840,6 +3844,50 @@ function isStrictlyInside(child, parent) {
3840
3844
  const p = path_1.default.resolve(parent);
3841
3845
  return c !== p && c.startsWith(p + path_1.default.sep);
3842
3846
  }
3847
+ /**
3848
+ * Resolve `requestedPath` to an absolute, sandboxed file under `base`, or
3849
+ * `undefined` if it escapes the base, doesn't exist, isn't a regular file, or
3850
+ * is a hardlink. Shared by `resolveStaticPreviewAssetPath` across the variant's
3851
+ * own directory and the host-root fallback so both get identical containment.
3852
+ *
3853
+ * `base` is expected to be realpath'd by the caller. The requested target is
3854
+ * realpath'd here too — without it, a symlink planted under the base (e.g. via
3855
+ * a follow-up Write tool call) could pivot outside the sandbox. `requestedPath`
3856
+ * is used as Express delivered it (already URL-decoded); decoding again would
3857
+ * mangle filenames that legitimately contain `%` (e.g. `100%25.png` on disk).
3858
+ */
3859
+ function resolveAssetWithinBase(base, requestedPath) {
3860
+ const target = path_1.default.resolve(base, '.' + path_1.default.sep + requestedPath);
3861
+ if (target !== base && !target.startsWith(base + path_1.default.sep)) {
3862
+ return undefined;
3863
+ }
3864
+ let realTarget;
3865
+ try {
3866
+ realTarget = fs_1.default.realpathSync(target);
3867
+ }
3868
+ catch {
3869
+ return undefined;
3870
+ }
3871
+ if (realTarget !== base && !realTarget.startsWith(base + path_1.default.sep)) {
3872
+ return undefined;
3873
+ }
3874
+ try {
3875
+ const stat = fs_1.default.statSync(realTarget);
3876
+ if (!stat.isFile())
3877
+ return undefined;
3878
+ // Hardlinks aren't dereferenced by `realpath` — they ARE the file from a
3879
+ // path-resolution perspective, so the realpath check above can't detect
3880
+ // them. An agent could `ln /path/to/.env base/x.glb` (hardlink, not
3881
+ // symlink) and the resolver would happily serve the secret. Reject
3882
+ // anything with multiple links — legitimate assets always have nlink === 1.
3883
+ if (stat.nlink !== 1)
3884
+ return undefined;
3885
+ return realTarget;
3886
+ }
3887
+ catch {
3888
+ return undefined;
3889
+ }
3890
+ }
3843
3891
  /**
3844
3892
  * Sandbox the agent's `output.assetBase` to a path inside the session
3845
3893
  * workspace. Accepts either an absolute path (used as-is after the