@1agh/maude 0.55.0 → 0.57.0

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 (85) hide show
  1. package/apps/studio/acp/bridge.ts +385 -26
  2. package/apps/studio/acp/index.ts +498 -102
  3. package/apps/studio/acp/running.ts +97 -0
  4. package/apps/studio/acp/transcript.ts +64 -0
  5. package/apps/studio/acp/write-scope.ts +459 -0
  6. package/apps/studio/bin/_smart-frames.mjs +187 -20
  7. package/apps/studio/bin/_smart-frames.test.mjs +59 -4
  8. package/apps/studio/bin/_transcribe.mjs +40 -3
  9. package/apps/studio/bin/smoke.sh +7 -1
  10. package/apps/studio/build.ts +28 -1
  11. package/apps/studio/client/app.jsx +78 -33
  12. package/apps/studio/client/panels/ChatPanel.jsx +19 -1
  13. package/apps/studio/client/panels/CloudBar.jsx +104 -6
  14. package/apps/studio/client/panels/GitPanel.jsx +38 -25
  15. package/apps/studio/client/panels/PermissionPrompt.jsx +146 -6
  16. package/apps/studio/client/panels/RepoBranchSwitcher.jsx +145 -6
  17. package/apps/studio/client/panels/SettingsPanel.jsx +239 -27
  18. package/apps/studio/client/panels/acp-runtime.js +96 -5
  19. package/apps/studio/client/styles/3-shell-maude.css +11 -2
  20. package/apps/studio/client/styles/4-components.css +72 -0
  21. package/apps/studio/client/styles/6-acp-chat.css +51 -0
  22. package/apps/studio/cloud/endpoints.ts +56 -3
  23. package/apps/studio/collab/persistence.ts +29 -2
  24. package/apps/studio/config.schema.json +3 -3
  25. package/apps/studio/context.ts +41 -0
  26. package/apps/studio/dist/client.bundle.js +1330 -1330
  27. package/apps/studio/dist/comment-mount.js +2 -2
  28. package/apps/studio/dist/styles.css +1 -1
  29. package/apps/studio/generation/gemma-models.ts +312 -14
  30. package/apps/studio/generation/prefs.ts +7 -2
  31. package/apps/studio/generation/runtime-probe.ts +50 -0
  32. package/apps/studio/generation/whisper-models.ts +124 -0
  33. package/apps/studio/hmr-broadcast.ts +67 -0
  34. package/apps/studio/http.ts +247 -111
  35. package/apps/studio/input-router.tsx +55 -2
  36. package/apps/studio/server.ts +22 -9
  37. package/apps/studio/sync/autocommit.ts +61 -2
  38. package/apps/studio/sync/cell-pairing.ts +174 -0
  39. package/apps/studio/sync/codec.ts +11 -5
  40. package/apps/studio/sync/connection-state.ts +116 -6
  41. package/apps/studio/sync/index.ts +288 -26
  42. package/apps/studio/sync/limits.ts +49 -0
  43. package/apps/studio/sync/loopback.ts +21 -0
  44. package/apps/studio/sync/presentation.ts +273 -0
  45. package/apps/studio/sync/projection.ts +47 -12
  46. package/apps/studio/sync/remote-docs.ts +191 -0
  47. package/apps/studio/sync/status.ts +4 -1
  48. package/apps/studio/sync/supervisor.ts +178 -0
  49. package/apps/studio/test/acp-activity-endpoint.test.ts +183 -0
  50. package/apps/studio/test/acp-branch-guard.test.ts +123 -0
  51. package/apps/studio/test/acp-bridge-lifetime.test.ts +369 -0
  52. package/apps/studio/test/acp-caps-bridge.test.ts +5 -0
  53. package/apps/studio/test/acp-commands.test.ts +5 -0
  54. package/apps/studio/test/acp-elicitation-bridge.test.ts +5 -0
  55. package/apps/studio/test/acp-permission-prompt.test.ts +175 -1
  56. package/apps/studio/test/acp-permission.test.ts +18 -2
  57. package/apps/studio/test/acp-session-allowed-tools.test.ts +62 -14
  58. package/apps/studio/test/acp-usage-bridge.test.ts +5 -0
  59. package/apps/studio/test/acp-write-gate.test.ts +323 -0
  60. package/apps/studio/test/acp-write-scope.test.ts +533 -0
  61. package/apps/studio/test/bundle-smoke.test.ts +35 -18
  62. package/apps/studio/test/canvas-origin-gate.test.ts +7 -0
  63. package/apps/studio/test/cloud-connect-note.test.ts +135 -0
  64. package/apps/studio/test/cloud-endpoints.test.ts +102 -0
  65. package/apps/studio/test/cloud-shell-surfaces.test.ts +5 -2
  66. package/apps/studio/test/csrf-write-guard.test.ts +19 -2
  67. package/apps/studio/test/fixtures/mock-acp-agent-slow.mjs +45 -0
  68. package/apps/studio/test/fixtures/mock-acp-agent-write.mjs +118 -0
  69. package/apps/studio/test/gemma-models.test.ts +245 -0
  70. package/apps/studio/test/hmr-broadcast.test.ts +57 -1
  71. package/apps/studio/test/input-router.test.ts +95 -0
  72. package/apps/studio/test/shared-doc-cell-pairing.test.ts +639 -0
  73. package/apps/studio/test/sync-autocommit.test.ts +47 -0
  74. package/apps/studio/test/sync-connect-honesty.test.ts +114 -0
  75. package/apps/studio/test/sync-connection-state.test.ts +45 -1
  76. package/apps/studio/test/sync-presentation.test.ts +185 -0
  77. package/apps/studio/test/sync-remote-docs.test.ts +171 -0
  78. package/apps/studio/test/sync-supervisor.test.ts +212 -0
  79. package/apps/studio/test/trusted-request-host.test.ts +66 -0
  80. package/apps/studio/test/whisper-setup.test.ts +97 -0
  81. package/apps/studio/whats-new.json +45 -0
  82. package/apps/studio/ws.ts +9 -1
  83. package/cli/commands/kg.mjs +9 -2
  84. package/package.json +8 -8
  85. package/plugins/design/dependencies.json +21 -3
@@ -880,6 +880,57 @@
880
880
  gap: var(--space-2);
881
881
  }
882
882
 
883
+ /* feature-acp-write-path-scope Task 4 — the out-of-project WRITE variant.
884
+ Distinguished from an ordinary permission card because the two ask
885
+ materially different questions: the ordinary one is "do you want this
886
+ tool to run", this one is "do you want a file written somewhere your
887
+ project's history stack cannot roll back". The danger tint is the whole
888
+ point — a card that looks identical to the routine one gets clicked
889
+ through at the same rate. */
890
+ .chat-perm--outside {
891
+ border-color: var(--danger, #d9534f);
892
+ }
893
+ .chat-perm--outside .chat-perm-hd b {
894
+ color: var(--danger, #d9534f);
895
+ }
896
+ .chat-perm-outside {
897
+ display: flex;
898
+ flex-direction: column;
899
+ gap: var(--space-1);
900
+ }
901
+ /* The resolved absolute paths. Monospaced + wrapping at any character: these
902
+ are the one string on the card the user actually has to READ, and a path
903
+ that overflows or ellipsizes is a path whose dangerous tail is hidden —
904
+ the same failure the .chat-perm-hd b clamp above exists to prevent, but
905
+ with the opposite fix, because here truncation is what would be unsafe. */
906
+ .chat-perm-paths {
907
+ margin: 0;
908
+ padding: 0;
909
+ list-style: none;
910
+ display: flex;
911
+ flex-direction: column;
912
+ gap: var(--space-1);
913
+ }
914
+ .chat-perm-path {
915
+ padding: var(--space-1) var(--space-2);
916
+ border-radius: var(--radius-xs);
917
+ background: var(--bg-1);
918
+ color: var(--fg-0);
919
+ font-family: var(--font-mono);
920
+ font-size: var(--type-xs);
921
+ overflow-wrap: anywhere;
922
+ }
923
+ .chat-perm-scope,
924
+ .chat-perm-note {
925
+ margin: 0;
926
+ font-size: var(--type-xs);
927
+ color: var(--fg-3);
928
+ }
929
+ .chat-perm-root {
930
+ font-family: var(--font-mono);
931
+ overflow-wrap: anywhere;
932
+ }
933
+
883
934
  /* Form-elicitation card (feature-acp-ask-user-question — AskUserQuestion +
884
935
  generic MCP form input). Same slot + shape as .chat-perm, mutually
885
936
  exclusive with it — see the mount site in ChatPanel.jsx. */
@@ -109,6 +109,19 @@ async function cloudFetch(
109
109
 
110
110
  interface Ctx {
111
111
  paths: { repoRoot: string; designRoot: string };
112
+ /**
113
+ * The live sync runtime's owner (server.ts → sync/supervisor.ts). Optional:
114
+ * unit tests and any non-serving embedder construct a Ctx without one, and a
115
+ * missing supervisor degrades to exactly the old behaviour — linked on disk,
116
+ * syncing after the next start.
117
+ */
118
+ syncControl?: {
119
+ restart(linkedHub?: {
120
+ url: string;
121
+ linkedAt: number;
122
+ syncTsx?: boolean;
123
+ }): Promise<{ syncing: boolean; canvases: number; reason?: string; detail?: string }>;
124
+ };
112
125
  }
113
126
 
114
127
  export function createCloudEndpoints(ctx: Ctx) {
@@ -225,7 +238,9 @@ export function createCloudEndpoints(ctx: Ctx) {
225
238
  /**
226
239
  * Attach THIS project to a cloud workspace: open → exchange at the cell →
227
240
  * store the hub credential + linkedHub, exactly as `maude design link`
228
- * would. The sync agent picks the link up on the next server start.
241
+ * would — and then START SYNCING (linkToWorkspace cycles the live runtime
242
+ * through ctx.syncControl). Connecting is the whole gesture; there is no
243
+ * second step for the person to discover.
229
244
  */
230
245
  async attach(projectId: string): Promise<CloudEndpointResult> {
231
246
  const file = readCloudFile();
@@ -380,9 +395,42 @@ export function createCloudEndpoints(ctx: Ctx) {
380
395
  } catch {
381
396
  /* absent/malformed → start minimal */
382
397
  }
383
- cfg.linkedHub = { url: norm, linkedAt: Date.now() };
398
+ const linkedHub: { url: string; linkedAt: number; syncTsx?: boolean } = {
399
+ url: norm,
400
+ linkedAt: Date.now(),
401
+ };
402
+ // Carry a project-wide TSX opt-out across the re-link. Only the RESTRICTIVE
403
+ // direction is carried (`false`, never `true`): somebody who turned canvas
404
+ // BODIES off (DDR-072/DDR-079) did so deliberately, and silently re-enabling
405
+ // them on the next Connect would start uploading source they opted out of.
406
+ // It never mattered while nothing synced until a restart; it does now.
407
+ const prior = (cfg as { linkedHub?: { syncTsx?: boolean } }).linkedHub;
408
+ if (prior?.syncTsx === false) linkedHub.syncTsx = false;
409
+ cfg.linkedHub = linkedHub;
384
410
  writeFileSync(cfgPath, `${JSON.stringify(cfg, null, 2)}\n`, 'utf8');
385
411
 
412
+ // START SYNCING — do not hand the person a "restart the server" note and
413
+ // call the job done. The runtime captures linkedHub once at boot, so this
414
+ // cycles it in place, with the value we JUST wrote (never re-read from the
415
+ // committed config file). Best-effort: a supervisor-less embedder, or a
416
+ // runtime that declines, still leaves a correctly linked project behind.
417
+ let sync: { syncing: boolean; canvases: number; reason?: string; detail?: string } = {
418
+ syncing: false,
419
+ canvases: 0,
420
+ reason: 'no-supervisor',
421
+ detail: 'Restart Maude to start syncing.',
422
+ };
423
+ try {
424
+ sync = (await ctx.syncControl?.restart(linkedHub)) ?? sync;
425
+ } catch (err) {
426
+ sync = {
427
+ syncing: false,
428
+ canvases: 0,
429
+ reason: 'error',
430
+ detail: `Linked, but syncing could not start: ${(err as Error).message}`,
431
+ };
432
+ }
433
+
386
434
  return {
387
435
  status: 200,
388
436
  json: {
@@ -390,7 +438,12 @@ export function createCloudEndpoints(ctx: Ctx) {
390
438
  url: norm,
391
439
  role: role ?? null,
392
440
  project: project ?? null,
393
- note: 'Linked. Restart the studio server to start syncing.',
441
+ sync,
442
+ // Kept for older clients (and the CLI) that read `note` — now it
443
+ // reports what actually happened instead of assigning homework.
444
+ note: sync.syncing
445
+ ? `Linked — syncing ${sync.canvases} canvas${sync.canvases === 1 ? '' : 'es'}.`
446
+ : `Linked. ${sync.detail ?? 'Restart Maude to start syncing.'}`,
394
447
  },
395
448
  };
396
449
  }
@@ -8,6 +8,9 @@ import * as Y from 'yjs';
8
8
 
9
9
  import type { Api } from '../api.ts';
10
10
  import type { Context } from '../context.ts';
11
+ // From the LEAF, never from `sync/codec.ts` — codec imports `Y_TYPES` from this
12
+ // file, so reaching for it here would close a cycle (see sync/limits.ts).
13
+ import { MAX_ANNOTATIONS_BYTES, MAX_COMMENTS_BYTES, withinByteCap } from '../sync/limits.ts';
11
14
  import { ensureStateDir, type RoomCallbacks } from './room.ts';
12
15
 
13
16
  /**
@@ -55,6 +58,26 @@ export interface PersistenceDeps {
55
58
  * skipped when the Y type is empty / unset — the JSON file stays whatever
56
59
  * the prior legacy write produced.
57
60
  */
61
+ /**
62
+ * DDR-064 pre-cutover checklist — cap the doc→disk lane for comments and
63
+ * annotations.
64
+ *
65
+ * The codec's `MAX_*_BYTES` guard the FILE→DOC direction. This is the other
66
+ * one, and until shared-doc it barely mattered: the room's doc was populated
67
+ * only by browsers on this machine. Under a shared doc it is populated by the
68
+ * hub, so an oversized array pushed by a peer (or by a hostile hub — DDR-054's
69
+ * threat model, §2d) would be materialized to this disk unbounded. Same ceiling
70
+ * as the import lane, so a value that could never be imported can never be
71
+ * written either.
72
+ *
73
+ * Refuses the WRITE, not the sync: the doc keeps the value and the peers keep
74
+ * converging. What is withheld is turning somebody else's blob into our disk.
75
+ * Shared with `sync/projection.ts`'s equivalent guard via `sync/limits.ts`.
76
+ */
77
+ function withinCap(slug: string, lane: string, value: string, max: number): boolean {
78
+ return withinByteCap(`collab/${slug}`, lane, Buffer.byteLength(value, 'utf8'), max);
79
+ }
80
+
58
81
  export function createPersistence(deps: PersistenceDeps): RoomCallbacks {
59
82
  const { ctx, api, fileForSlug } = deps;
60
83
  const stateDir = ensureStateDir(ctx.paths.designRoot);
@@ -135,14 +158,18 @@ export function createPersistence(deps: PersistenceDeps): RoomCallbacks {
135
158
  const list = arr.toArray() as Parameters<Api['saveCommentsForFile']>[1];
136
159
  if (list.length > 0) seenComments.add(slug);
137
160
  if (list.length > 0 || seenComments.has(slug)) {
138
- await api.saveCommentsForFile(file, list);
161
+ if (withinCap(slug, 'comments', JSON.stringify(list), MAX_COMMENTS_BYTES)) {
162
+ await api.saveCommentsForFile(file, list);
163
+ }
139
164
  }
140
165
 
141
166
  // Annotations — Y.Map.svg → annotations.svg file. Task 5.
142
167
  const map = doc.getMap<unknown>(Y_TYPES.annotations);
143
168
  const svg = map.get('svg');
144
169
  if (typeof svg === 'string' && svg) {
145
- await api.saveAnnotations(file, svg);
170
+ if (withinCap(slug, 'annotations', svg, MAX_ANNOTATIONS_BYTES)) {
171
+ await api.saveAnnotations(file, svg);
172
+ }
146
173
  }
147
174
  }
148
175
 
@@ -259,12 +259,12 @@
259
259
  },
260
260
  "transcription": {
261
261
  "type": "object",
262
- "description": "Speech-to-text / subtitle engine preference (Task 2.6, DDR-164). The engine is an EXPLICIT user choice, never an automatic fallback: a chosen-but-unavailable engine errors clearly and Maude never silently switches to another. `maude design transcribe --provider` overrides this per-call.",
262
+ "description": "Speech-to-text / subtitle engine preference (Task 2.6, DDR-164). The engine is never switched behind your back: a chosen-but-unavailable engine errors clearly, and `auto` — a mode you SELECT — resolves to the best engine this machine is set up for while stating which one it picked. `maude design transcribe --provider` overrides this per-call.",
263
263
  "properties": {
264
264
  "provider": {
265
265
  "type": "string",
266
- "enum": ["whisper", "elevenlabs", "groq"],
267
- "description": "Which engine transcribes when no --provider flag is given. `whisper` = local whisper.cpp (free, offline, no key); `elevenlabs` = ElevenLabs Scribe (cloud, key); `groq` = Groq Whisper (cloud, key). Absent ⇒ whisper.",
266
+ "enum": ["auto", "whisper", "elevenlabs", "groq"],
267
+ "description": "Which engine transcribes when no --provider flag is given. `auto` = use the cloud engine whose key is set (ElevenLabs Scribe first), else local whisper — the UI and the CLI both state what it resolved to; `whisper` = local whisper.cpp (free, offline, no key); `elevenlabs` = ElevenLabs Scribe (cloud, key, audio leaves the machine); `groq` = Groq Whisper (cloud, key). Absent ⇒ whisper.",
268
268
  "default": "whisper"
269
269
  },
270
270
  "model": {
@@ -182,6 +182,22 @@ export interface Context {
182
182
  * gates behavior starting in Phase B.
183
183
  */
184
184
  sharedDoc?: boolean;
185
+ /**
186
+ * The live sync runtime's owner, set by server.ts once it exists. Present so
187
+ * the cloud attach lane can START SYNCING the moment a person links a
188
+ * project, instead of leaving them holding a "restart the studio server"
189
+ * note with no button behind it. Structural type (not the SyncSupervisor
190
+ * import) so context.ts stays free of the sync module graph — tests and the
191
+ * non-serving entry points construct a Context without one.
192
+ */
193
+ syncControl?: {
194
+ restart(linkedHub?: LinkedHub): Promise<{
195
+ syncing: boolean;
196
+ canvases: number;
197
+ reason?: string;
198
+ detail?: string;
199
+ }>;
200
+ };
185
201
  }
186
202
 
187
203
  function resolveRepoRoot(): string {
@@ -364,6 +380,12 @@ export function reloadConfig(ctx: Context): boolean {
364
380
  // (syncTsx gating) drift out of step with the hub the socket is actually
365
381
  // attached to — and a poisoned config must never re-point sync without a
366
382
  // restart (DDR-149 fan-out review).
383
+ //
384
+ // This is a rule about the FILE, and it stays. The one path that may change
385
+ // the live link is `adoptLinkedHub` below: a person pressing Connect in
386
+ // trusted app chrome, whose hub credential this same process just stored.
387
+ // That path cycles the runtime itself (sync/supervisor.ts) instead of hoping
388
+ // a watcher notices.
367
389
  if (JSON.stringify(next.linkedHub) !== JSON.stringify(ctx.cfg.linkedHub)) {
368
390
  console.warn(' warn: linkedHub changed — not hot-reloadable, restart the server to apply.');
369
391
  if (ctx.cfg.linkedHub === undefined) delete next.linkedHub;
@@ -389,3 +411,22 @@ export function reloadConfig(ctx: Context): boolean {
389
411
  ctx.cfg.canvasGroups.find((g) => /system/i.test(g.path))?.path ?? 'system';
390
412
  return true;
391
413
  }
414
+
415
+ /**
416
+ * Adopt a link the person just authorized — the ONE way `cfg.linkedHub` may
417
+ * change on a running server.
418
+ *
419
+ * The value is passed BY VALUE from the attach lane (cloud/endpoints.ts), which
420
+ * exchanged it against the configured cloud address and stored the matching hub
421
+ * credential moments earlier. It is deliberately NOT re-read from
422
+ * `.design/config.json`: that file is committed, shared, and writable by
423
+ * anything else on the machine, and re-reading it here would hand the
424
+ * file-watcher's refused capability (reloadConfig above) back through a side
425
+ * door. What the person consented to is what gets applied.
426
+ *
427
+ * Callers must cycle the sync runtime afterwards — `createSyncSupervisor`
428
+ * (sync/supervisor.ts) does both in one step, and is the only intended caller.
429
+ */
430
+ export function adoptLinkedHub(ctx: Context, linkedHub: LinkedHub): void {
431
+ ctx.cfg.linkedHub = linkedHub;
432
+ }