wicked-crew 0.5.0 → 0.7.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.
- package/dist/api/audit.d.ts +70 -0
- package/dist/api/audit.d.ts.map +1 -0
- package/dist/api/audit.js +131 -0
- package/dist/api/audit.js.map +1 -0
- package/dist/api/auth.d.ts +192 -0
- package/dist/api/auth.d.ts.map +1 -0
- package/dist/api/auth.js +515 -0
- package/dist/api/auth.js.map +1 -0
- package/dist/api/gate-cache.d.ts +8 -0
- package/dist/api/gate-cache.d.ts.map +1 -1
- package/dist/api/gate-cache.js +10 -0
- package/dist/api/gate-cache.js.map +1 -1
- package/dist/api/guidance-index.d.ts +39 -0
- package/dist/api/guidance-index.d.ts.map +1 -0
- package/dist/api/guidance-index.js +67 -0
- package/dist/api/guidance-index.js.map +1 -0
- package/dist/api/open-path.d.ts +34 -0
- package/dist/api/open-path.d.ts.map +1 -0
- package/dist/api/open-path.js +101 -0
- package/dist/api/open-path.js.map +1 -0
- package/dist/api/retry-index.d.ts +30 -0
- package/dist/api/retry-index.d.ts.map +1 -0
- package/dist/api/retry-index.js +45 -0
- package/dist/api/retry-index.js.map +1 -0
- package/dist/api/routes.d.ts +164 -1
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +769 -21
- package/dist/api/routes.js.map +1 -1
- package/dist/api/run-files.d.ts +63 -0
- package/dist/api/run-files.d.ts.map +1 -0
- package/dist/api/run-files.js +271 -0
- package/dist/api/run-files.js.map +1 -0
- package/dist/api/seat-health.d.ts +55 -0
- package/dist/api/seat-health.d.ts.map +1 -0
- package/dist/api/seat-health.js +273 -0
- package/dist/api/seat-health.js.map +1 -0
- package/dist/api/seat-signin.d.ts +27 -0
- package/dist/api/seat-signin.d.ts.map +1 -0
- package/dist/api/seat-signin.js +143 -0
- package/dist/api/seat-signin.js.map +1 -0
- package/dist/api/server.d.ts +190 -3
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +365 -11
- package/dist/api/server.js.map +1 -1
- package/dist/api/stall-watchdog.d.ts +62 -0
- package/dist/api/stall-watchdog.d.ts.map +1 -0
- package/dist/api/stall-watchdog.js +138 -0
- package/dist/api/stall-watchdog.js.map +1 -0
- package/dist/cli/index.js +171 -12
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp.d.ts +14 -0
- package/dist/cli/mcp.d.ts.map +1 -0
- package/dist/cli/mcp.js +119 -0
- package/dist/cli/mcp.js.map +1 -0
- package/dist/core/adapter.d.ts +87 -10
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +347 -28
- package/dist/core/adapter.js.map +1 -1
- package/dist/core/bridge-reaper.d.ts +134 -0
- package/dist/core/bridge-reaper.d.ts.map +1 -0
- package/dist/core/bridge-reaper.js +286 -0
- package/dist/core/bridge-reaper.js.map +1 -0
- package/dist/core/deliver.d.ts +118 -0
- package/dist/core/deliver.d.ts.map +1 -0
- package/dist/core/deliver.js +241 -0
- package/dist/core/deliver.js.map +1 -0
- package/dist/core/deliverable-floor.d.ts +103 -0
- package/dist/core/deliverable-floor.d.ts.map +1 -0
- package/dist/core/deliverable-floor.js +173 -0
- package/dist/core/deliverable-floor.js.map +1 -0
- package/dist/core/exec.d.ts +2 -0
- package/dist/core/exec.d.ts.map +1 -1
- package/dist/core/exec.js.map +1 -1
- package/dist/core/types.d.ts +92 -352
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +14 -4
- package/dist/core/types.js.map +1 -1
- package/dist/interactive/bridge-pool.d.ts +99 -0
- package/dist/interactive/bridge-pool.d.ts.map +1 -0
- package/dist/interactive/bridge-pool.js +244 -0
- package/dist/interactive/bridge-pool.js.map +1 -0
- package/dist/interactive/bridge-root.d.ts +36 -0
- package/dist/interactive/bridge-root.d.ts.map +1 -0
- package/dist/interactive/bridge-root.js +48 -0
- package/dist/interactive/bridge-root.js.map +1 -0
- package/dist/interactive/chat-events.d.ts +207 -0
- package/dist/interactive/chat-events.d.ts.map +1 -0
- package/dist/interactive/chat-events.js +769 -0
- package/dist/interactive/chat-events.js.map +1 -0
- package/dist/interactive/demo-events.d.ts +283 -0
- package/dist/interactive/demo-events.d.ts.map +1 -0
- package/dist/interactive/demo-events.js +889 -0
- package/dist/interactive/demo-events.js.map +1 -0
- package/dist/interactive/draft-events.d.ts +224 -0
- package/dist/interactive/draft-events.d.ts.map +1 -0
- package/dist/interactive/draft-events.js +793 -0
- package/dist/interactive/draft-events.js.map +1 -0
- package/dist/interactive/edit-events.d.ts +194 -0
- package/dist/interactive/edit-events.d.ts.map +1 -0
- package/dist/interactive/edit-events.js +601 -0
- package/dist/interactive/edit-events.js.map +1 -0
- package/dist/interactive/ledger.d.ts +39 -0
- package/dist/interactive/ledger.d.ts.map +1 -0
- package/dist/interactive/ledger.js +93 -0
- package/dist/interactive/ledger.js.map +1 -0
- package/dist/interactive/proxy-routes.d.ts +39 -0
- package/dist/interactive/proxy-routes.d.ts.map +1 -0
- package/dist/interactive/proxy-routes.js +189 -0
- package/dist/interactive/proxy-routes.js.map +1 -0
- package/dist/interactive/repo-snapshot.d.ts +100 -0
- package/dist/interactive/repo-snapshot.d.ts.map +1 -0
- package/dist/interactive/repo-snapshot.js +289 -0
- package/dist/interactive/repo-snapshot.js.map +1 -0
- package/dist/interactive/ws-relay.d.ts +85 -0
- package/dist/interactive/ws-relay.d.ts.map +1 -0
- package/dist/interactive/ws-relay.js +191 -0
- package/dist/interactive/ws-relay.js.map +1 -0
- package/dist/projects/activity.d.ts +29 -0
- package/dist/projects/activity.d.ts.map +1 -0
- package/dist/projects/activity.js +172 -0
- package/dist/projects/activity.js.map +1 -0
- package/dist/projects/charter.d.ts +28 -0
- package/dist/projects/charter.d.ts.map +1 -0
- package/dist/projects/charter.js +53 -0
- package/dist/projects/charter.js.map +1 -0
- package/dist/projects/events.d.ts +55 -0
- package/dist/projects/events.d.ts.map +1 -0
- package/dist/projects/events.js +141 -0
- package/dist/projects/events.js.map +1 -0
- package/dist/projects/graph-paths.d.ts +92 -0
- package/dist/projects/graph-paths.d.ts.map +1 -0
- package/dist/projects/graph-paths.js +130 -0
- package/dist/projects/graph-paths.js.map +1 -0
- package/dist/projects/graph.d.ts +179 -0
- package/dist/projects/graph.d.ts.map +1 -0
- package/dist/projects/graph.js +775 -0
- package/dist/projects/graph.js.map +1 -0
- package/dist/projects/membership-index.d.ts +25 -0
- package/dist/projects/membership-index.d.ts.map +1 -0
- package/dist/projects/membership-index.js +46 -0
- package/dist/projects/membership-index.js.map +1 -0
- package/dist/projects/routes.d.ts +97 -0
- package/dist/projects/routes.d.ts.map +1 -0
- package/dist/projects/routes.js +510 -0
- package/dist/projects/routes.js.map +1 -0
- package/dist/projects/settings.d.ts +32 -0
- package/dist/projects/settings.d.ts.map +1 -0
- package/dist/projects/settings.js +64 -0
- package/dist/projects/settings.js.map +1 -0
- package/dist/qe/acceptance.d.ts +137 -0
- package/dist/qe/acceptance.d.ts.map +1 -0
- package/dist/qe/acceptance.js +249 -0
- package/dist/qe/acceptance.js.map +1 -0
- package/dist/qe/gate-events.d.ts +111 -0
- package/dist/qe/gate-events.d.ts.map +1 -0
- package/dist/qe/gate-events.js +168 -0
- package/dist/qe/gate-events.js.map +1 -0
- package/dist/qe/ledger.d.ts +100 -0
- package/dist/qe/ledger.d.ts.map +1 -0
- package/dist/qe/ledger.js +154 -0
- package/dist/qe/ledger.js.map +1 -0
- package/dist/studio/assets/index-8p8uwCxG.js +530 -0
- package/dist/studio/assets/index-D6S9zUtO.css +32 -0
- package/dist/studio/index.html +5 -3
- package/package.json +10 -4
- package/dist/studio/assets/index-DaaUU8Ep.css +0 -32
- package/dist/studio/assets/index-Fu5DRC00.js +0 -423
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Launch-scoped REPO SNAPSHOTS for grounding governed interactive runs (CREW-UX-8 v4).
|
|
3
|
+
*
|
|
4
|
+
* WHY a snapshot instead of the live repo: the interactive seams launch their governed runs
|
|
5
|
+
* UNBOUND — on a repoRef-bound run the worker's ACP tool-permission stream closes on the first
|
|
6
|
+
* call that needs a permission prompt and the session dies (wicked-core#293) — and an unbound
|
|
7
|
+
* worker's governance boundary is {sandbox, extraWriteRoots, ~/.claude/plugins}, so even READS
|
|
8
|
+
* of the live repo root are governance-denied (wicked-core#294; the v3 "hand the worker the
|
|
9
|
+
* root path to read" design rested on read evidence gathered under a *bound* boundary, which
|
|
10
|
+
* does not transfer). The one location an unbound worker can always read is the inbox the run
|
|
11
|
+
* already writes to — write roots are readable (wicked-core#259) — so grounding means putting
|
|
12
|
+
* a copy of the repo INSIDE the inbox, crew-side, BEFORE the launch, and naming that snapshot
|
|
13
|
+
* in the task.
|
|
14
|
+
*
|
|
15
|
+
* Mechanism, in preference order:
|
|
16
|
+
* 1. `git clone --depth 1 --no-hardlinks file://<root> <dest>` — a shallow LOCAL clone:
|
|
17
|
+
* one commit of history, `--no-hardlinks` so the snapshot never aliases the live object
|
|
18
|
+
* store. Because a clone checks out EVERY tracked path, the result is then swept: tracked
|
|
19
|
+
* `node_modules` trees and every symlink are removed (see below), and the swept snapshot
|
|
20
|
+
* is re-measured against the budget before it counts as usable.
|
|
21
|
+
* 2. A plain file copy that skips `.git`/`node_modules` AND symlinks — for roots that are not
|
|
22
|
+
* git repos (or where git itself is unavailable/failing).
|
|
23
|
+
*
|
|
24
|
+
* SYMLINKS NEVER SURVIVE into a snapshot, on either path (Copilot, crew#313): the snapshot
|
|
25
|
+
* lands inside the worker-visible inbox, so a repo symlink pointing OUTSIDE the repo would
|
|
26
|
+
* hand the worker readable reach beyond the governance boundary — and a link to a huge tree
|
|
27
|
+
* would bypass the size cap the preflight walk (which skips links) enforced. The copy filter
|
|
28
|
+
* refuses them and the post-clone sweep deletes the ones a clone checked out.
|
|
29
|
+
*
|
|
30
|
+
* The snapshot is CAPPED: a working tree whose (`.git`/`node_modules`/symlink-excluded) size
|
|
31
|
+
* exceeds the budget is not snapshotted at all — the caller degrades honestly to an ungrounded
|
|
32
|
+
* launch with a visible status note, never a truncated half-repo the worker would mistake for
|
|
33
|
+
* the whole. Snapshots are launch-scoped: the seams remove them when the run reaches a
|
|
34
|
+
* terminal state (finalize/failure folds), so the inbox never accretes dead clones.
|
|
35
|
+
*
|
|
36
|
+
* OVERLAP REFUSAL: a configured draft dir could place `dest` inside the live repo (or a repo
|
|
37
|
+
* could be registered AT the inbox). Clearing a stale dest with `rm -rf` before noticing that
|
|
38
|
+
* would DELETE live repository content — so source/dest containment is checked (realpath'd,
|
|
39
|
+
* both directions) BEFORE anything is removed, and any overlap refuses the snapshot outright.
|
|
40
|
+
* For the same reason THIS helper creates `dest`'s parent directory (after the check passes) —
|
|
41
|
+
* a caller that pre-created it would already have written into the live repo (Copilot round 2).
|
|
42
|
+
*
|
|
43
|
+
* Fully ASYNC on purpose: this runs inline in a bus handler whose process also feeds ~15s UI
|
|
44
|
+
* heartbeats — a synchronous clone/copy of a big repo would starve the event loop and the
|
|
45
|
+
* canvas would read frozen (Copilot, crew#313). All I/O is fs/promises; the clone goes through
|
|
46
|
+
* the daemon's capped exec chokepoint (`execCapped`, FINDING-016).
|
|
47
|
+
*/
|
|
48
|
+
import { cp, lstat, mkdir, readdir, realpath, rm, stat } from 'node:fs/promises';
|
|
49
|
+
import path, { basename, dirname, join, resolve } from 'node:path';
|
|
50
|
+
import { pathToFileURL } from 'node:url';
|
|
51
|
+
import { execCapped } from '../core/exec.js';
|
|
52
|
+
/** Default snapshot budget (~200MB). A repo whose working tree exceeds this is not
|
|
53
|
+
* snapshotted — the launch proceeds ungrounded, honestly narrated. */
|
|
54
|
+
export const REPO_SNAPSHOT_MAX_BYTES = 200 * 1024 * 1024;
|
|
55
|
+
/** Never snapshotted: git's object store (the clone brings its own shallow one; the copy
|
|
56
|
+
* fallback wants working files only) and dependency trees (huge, reproducible, ungrounding). */
|
|
57
|
+
const SNAPSHOT_SKIP = new Set(['.git', 'node_modules']);
|
|
58
|
+
/** `true` when the tree under `root` exceeds `cap` bytes, skipping {@link SNAPSHOT_SKIP} dirs
|
|
59
|
+
* and symlinks (a link to a huge tree — or out of the repo — must not distort the estimate;
|
|
60
|
+
* the copy filter and post-clone sweep exclude exactly the same set, so the preflight counts
|
|
61
|
+
* what a snapshot would actually hold), with an early exit the moment the cap is passed so a
|
|
62
|
+
* 10GB monorepo is never fully walked just to learn it is too big. */
|
|
63
|
+
async function treeExceeds(root, cap) {
|
|
64
|
+
let total = 0;
|
|
65
|
+
const stack = [root];
|
|
66
|
+
while (stack.length > 0) {
|
|
67
|
+
const dir = stack.pop();
|
|
68
|
+
let entries;
|
|
69
|
+
try {
|
|
70
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
continue; // unreadable subdir — the clone/copy will surface a real error if it matters
|
|
74
|
+
}
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
if (entry.isSymbolicLink())
|
|
77
|
+
continue;
|
|
78
|
+
if (entry.isDirectory()) {
|
|
79
|
+
if (!SNAPSHOT_SKIP.has(entry.name))
|
|
80
|
+
stack.push(join(dir, entry.name));
|
|
81
|
+
}
|
|
82
|
+
else if (entry.isFile()) {
|
|
83
|
+
try {
|
|
84
|
+
total += (await stat(join(dir, entry.name))).size;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// raced deletion — ignore
|
|
88
|
+
}
|
|
89
|
+
if (total > cap)
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
/** Post-clone sweep: delete every symlink (junctions included — boundary escape, cap bypass)
|
|
97
|
+
* and every {@link SNAPSHOT_SKIP} tree a `git clone` checked out (a clone materializes ALL
|
|
98
|
+
* tracked paths — tracked `node_modules` ignores the skip list otherwise). The clone's own
|
|
99
|
+
* top-level `.git` (its shallow object store) is the one deliberate survivor. */
|
|
100
|
+
async function sweepSnapshot(root) {
|
|
101
|
+
const stack = [root];
|
|
102
|
+
while (stack.length > 0) {
|
|
103
|
+
const dir = stack.pop();
|
|
104
|
+
let entries;
|
|
105
|
+
try {
|
|
106
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
const p = join(dir, entry.name);
|
|
113
|
+
if (entry.isSymbolicLink()) {
|
|
114
|
+
await rm(p, { recursive: true, force: true });
|
|
115
|
+
}
|
|
116
|
+
else if (entry.isDirectory()) {
|
|
117
|
+
if (dir === root && entry.name === '.git')
|
|
118
|
+
continue; // the shallow clone's own store
|
|
119
|
+
if (SNAPSHOT_SKIP.has(entry.name)) {
|
|
120
|
+
await rm(p, { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
stack.push(p);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** Resolve `p` through its nearest EXISTING ancestor's realpath (the path itself may not exist
|
|
130
|
+
* yet — a fresh dest usually does not), so symlinked parents cannot hide an overlap. Only
|
|
131
|
+
* ENOENT falls to the ancestor walk (the `api/open-path.ts` rule, Copilot round 2): any OTHER
|
|
132
|
+
* error (EACCES, ELOOP, …) means an EXISTING component could not be resolved — a lexical
|
|
133
|
+
* reconstruction there could miss an overlap through the unresolved link, so it THROWS and
|
|
134
|
+
* the caller's overlap-check catch fails closed (no snapshot, degrade). */
|
|
135
|
+
async function realpathNearest(p) {
|
|
136
|
+
let existing = resolve(p);
|
|
137
|
+
const tail = [];
|
|
138
|
+
// Walk up until something exists; dirname() at the root returns itself, which always exists.
|
|
139
|
+
for (;;) {
|
|
140
|
+
try {
|
|
141
|
+
const real = await realpath(existing);
|
|
142
|
+
return tail.length > 0 ? join(real, ...tail) : real;
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
if (err.code !== 'ENOENT')
|
|
146
|
+
throw err;
|
|
147
|
+
const parent = dirname(existing);
|
|
148
|
+
if (parent === existing)
|
|
149
|
+
return existing; // unreachable in practice — a root always resolves
|
|
150
|
+
tail.unshift(basename(existing));
|
|
151
|
+
existing = parent;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** Is `child` equal to `parent` or inside it? `relative()`-based (the `api/open-path.ts`
|
|
156
|
+
* containment pattern) — a plain `startsWith(base + sep)` never matches a ROOT base (`/`,
|
|
157
|
+
* `C:\`), whose spelling already ends in the separator (Copilot round 2). */
|
|
158
|
+
function contains(parent, child, p) {
|
|
159
|
+
const rel = p.relative(parent, child);
|
|
160
|
+
if (rel === '')
|
|
161
|
+
return true; // the same path
|
|
162
|
+
if (p.isAbsolute(rel))
|
|
163
|
+
return false; // different drive/tree entirely (win32)
|
|
164
|
+
return rel !== '..' && !rel.startsWith(`..${p.sep}`);
|
|
165
|
+
}
|
|
166
|
+
/** `true` when `a` and `b` name the same path or one contains the other — including when one
|
|
167
|
+
* of them is a filesystem root (`/`, a win32 drive root). Exported for its unit tests. */
|
|
168
|
+
export function pathsOverlap(a, b, p = path) {
|
|
169
|
+
return contains(a, b, p) || contains(b, a, p);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Materialize a snapshot of the repo at `rootPath` into `dest` (creating it), for a worker to
|
|
173
|
+
* READ as grounding context. Resolves `{ok: true}` when `dest` now holds a usable snapshot,
|
|
174
|
+
* `{ok: false, reason}` when it does not — see {@link SnapshotFailureReason}; the reason lets
|
|
175
|
+
* the caller narrate the RIGHT degrade (too large ≠ unreadable ≠ clone failed) — in which case
|
|
176
|
+
* any partial `dest` has been removed and the caller must launch UNGROUNDED (and say so on the
|
|
177
|
+
* thread). Never rejects: a grounding failure must degrade the launch, not kill it.
|
|
178
|
+
*
|
|
179
|
+
* The clone is the git-preferred path, so on a real repo the snapshot is HEAD's tracked
|
|
180
|
+
* content (uncommitted/untracked files are not included) minus swept symlinks/`node_modules`;
|
|
181
|
+
* the copy fallback takes the working tree as-is minus `.git`/`node_modules`/symlinks.
|
|
182
|
+
*/
|
|
183
|
+
export async function snapshotRepo(rootPath, dest, opts = {}) {
|
|
184
|
+
const log = opts.log ?? (() => { });
|
|
185
|
+
const maxBytes = opts.maxBytes ?? REPO_SNAPSHOT_MAX_BYTES;
|
|
186
|
+
try {
|
|
187
|
+
if (!(await stat(rootPath)).isDirectory()) {
|
|
188
|
+
log(`[repo-snapshot] repo root ${rootPath} is not a directory — no snapshot`);
|
|
189
|
+
return { ok: false, reason: 'root-unreadable' };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
log(`[repo-snapshot] repo root ${rootPath} does not exist — no snapshot`);
|
|
194
|
+
return { ok: false, reason: 'root-unreadable' };
|
|
195
|
+
}
|
|
196
|
+
// Overlap refusal comes BEFORE the stale-dest clear (Copilot, crew#313): with a configurable
|
|
197
|
+
// draft dir, `dest` can sit inside the registered repo (or vice versa) — clearing it first
|
|
198
|
+
// would `rm -rf` live repository content, and a copy could recurse into its own output.
|
|
199
|
+
try {
|
|
200
|
+
const realRoot = await realpath(rootPath);
|
|
201
|
+
const realDest = await realpathNearest(dest);
|
|
202
|
+
if (pathsOverlap(realRoot, realDest)) {
|
|
203
|
+
log(`[repo-snapshot] refusing snapshot: destination ${dest} (${realDest}) overlaps the repo root ${rootPath} (${realRoot})`);
|
|
204
|
+
return { ok: false, reason: 'dest-overlap' };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
log(`[repo-snapshot] could not resolve ${rootPath} / ${dest} for the overlap check: ${err instanceof Error ? err.message : String(err)} — no snapshot`);
|
|
209
|
+
return { ok: false, reason: 'root-unreadable' };
|
|
210
|
+
}
|
|
211
|
+
if (await treeExceeds(rootPath, maxBytes)) {
|
|
212
|
+
log(`[repo-snapshot] repo at ${rootPath} exceeds the ${maxBytes}-byte snapshot budget — no snapshot`);
|
|
213
|
+
return { ok: false, reason: 'too-large' };
|
|
214
|
+
}
|
|
215
|
+
// The snapshot's PARENT (the caller's per-run dir) is created HERE — only after containment
|
|
216
|
+
// was validated. The caller must never pre-create it: with an overlapping config that mkdir
|
|
217
|
+
// would write into the live repository before the refusal above could fire (Copilot round 2).
|
|
218
|
+
try {
|
|
219
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
log(`[repo-snapshot] could not create the snapshot parent for ${dest}: ${err instanceof Error ? err.message : String(err)} — no snapshot`);
|
|
223
|
+
return { ok: false, reason: 'copy-failed' };
|
|
224
|
+
}
|
|
225
|
+
// A stale snapshot (crashed prior run, replayed key) never survives into a new launch.
|
|
226
|
+
try {
|
|
227
|
+
await rm(dest, { recursive: true, force: true });
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
log(`[repo-snapshot] could not clear ${dest}: ${err instanceof Error ? err.message : String(err)}`);
|
|
231
|
+
return { ok: false, reason: 'dest-unclearable' };
|
|
232
|
+
}
|
|
233
|
+
// Preferred: shallow local clone. `file://` (not a plain path) is what makes --depth honored
|
|
234
|
+
// on a local source; pathToFileURL keeps the URL spelling right on Windows too.
|
|
235
|
+
try {
|
|
236
|
+
await execCapped('git', ['clone', '--depth', '1', '--no-hardlinks', pathToFileURL(rootPath).href, dest], {
|
|
237
|
+
timeout: 120_000,
|
|
238
|
+
windowsHide: true,
|
|
239
|
+
});
|
|
240
|
+
// The clone checked out EVERY tracked path — sweep what the snapshot contract excludes
|
|
241
|
+
// (tracked node_modules, all symlinks), then verify the SWEPT tree against the budget:
|
|
242
|
+
// the preflight measured the source with these exclusions, so a clean sweep normally
|
|
243
|
+
// passes, but the honest gate is what actually landed on disk.
|
|
244
|
+
await sweepSnapshot(dest);
|
|
245
|
+
if (await treeExceeds(dest, maxBytes)) {
|
|
246
|
+
log(`[repo-snapshot] clone of ${rootPath} exceeds the ${maxBytes}-byte budget after sweep — no snapshot`);
|
|
247
|
+
await rm(dest, { recursive: true, force: true }).catch(() => { });
|
|
248
|
+
return { ok: false, reason: 'too-large' };
|
|
249
|
+
}
|
|
250
|
+
return { ok: true };
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
log(`[repo-snapshot] git clone of ${rootPath} failed (${err instanceof Error ? err.message : String(err)}) — falling back to a file copy`);
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
await rm(dest, { recursive: true, force: true }); // whatever the failed clone left behind
|
|
257
|
+
await cp(rootPath, dest, {
|
|
258
|
+
recursive: true,
|
|
259
|
+
filter: async (src) => {
|
|
260
|
+
if (SNAPSHOT_SKIP.has(basename(src)))
|
|
261
|
+
return false;
|
|
262
|
+
try {
|
|
263
|
+
// Symlinks NEVER ride into the worker-visible inbox: a link out of the repo is a
|
|
264
|
+
// boundary escape, a link to a huge tree a cap bypass (Copilot, crew#313).
|
|
265
|
+
return !(await lstat(src)).isSymbolicLink();
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return false; // raced deletion / unreadable — leave it out
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
// The honest gate is what actually LANDED (same rule as the clone path's post-sweep
|
|
273
|
+
// re-measure): the preflight walked the SOURCE, and a file that grew between that walk and
|
|
274
|
+
// the copy would otherwise put an over-budget tree in the worker-visible inbox
|
|
275
|
+
// (Copilot round 2).
|
|
276
|
+
if (await treeExceeds(dest, maxBytes)) {
|
|
277
|
+
log(`[repo-snapshot] copy of ${rootPath} exceeds the ${maxBytes}-byte budget — no snapshot`);
|
|
278
|
+
await rm(dest, { recursive: true, force: true }).catch(() => { });
|
|
279
|
+
return { ok: false, reason: 'too-large' };
|
|
280
|
+
}
|
|
281
|
+
return { ok: true };
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
log(`[repo-snapshot] file copy of ${rootPath} failed too (${err instanceof Error ? err.message : String(err)}) — no snapshot`);
|
|
285
|
+
await rm(dest, { recursive: true, force: true }).catch(() => { }); // never leave a partial snapshot
|
|
286
|
+
return { ok: false, reason: 'copy-failed' };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
//# sourceMappingURL=repo-snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repo-snapshot.js","sourceRoot":"","sources":["../../src/interactive/repo-snapshot.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DES-MERGE-001 §5.4 / §6.1 (slice 3) — the `wicked.interactive.*` ↔ `/ws` relay.
|
|
3
|
+
*
|
|
4
|
+
* wicked-studio is absorbing wicked-interactive. The SPA already holds crew's `/ws` socket for
|
|
5
|
+
* CoreEvent frames; interactive's own progress travels on the wicked-bus. This module bridges the
|
|
6
|
+
* two so the skin needs exactly ONE socket — no second websocket, no new port.
|
|
7
|
+
*
|
|
8
|
+
* Two halves, one bus handle:
|
|
9
|
+
*
|
|
10
|
+
* 1. RELAY (bus → /ws). Subscribe to `wicked.interactive.**` and rebroadcast each event as
|
|
11
|
+
* `{ type: 'interactiveEvent', event: <the full bus event> }` — ONE envelope type, the original
|
|
12
|
+
* event nested whole, no field renaming, no project filter. Ordering is as received (the bus
|
|
13
|
+
* hands a batch to the handler in order and `broadcast` is synchronous), and there is no
|
|
14
|
+
* buffering beyond what the existing broadcast path already does.
|
|
15
|
+
*
|
|
16
|
+
* 2. EMIT (/ws client → bus). `POST /projects/:projectId/interactive-events` validates the body
|
|
17
|
+
* against a SERVER-SIDE whitelist ({@link EMITTABLE_TYPES}) and puts the event on the bus with
|
|
18
|
+
* interactive's own envelope conventions (domain `wicked-interactive`, producer `wi-crew`,
|
|
19
|
+
* `ts`-stamped payload — draft-events.ts is the precedent), so interactive's own subscribers
|
|
20
|
+
* see a UI-originated event exactly as they see a service-originated one.
|
|
21
|
+
*
|
|
22
|
+
* Deliberately DISTINCT from the `projectActivity` bridge in projects/events.ts, which reads the
|
|
23
|
+
* same filter: that one is project-FILTERED and reshapes the event into an activity frame for a
|
|
24
|
+
* project view. This one is unfiltered and verbatim, because the studio's document canvas must see
|
|
25
|
+
* the events of a doc that was never filed under a project. Separate plugin identity → separate
|
|
26
|
+
* durable cursor, so neither seam can strand the other.
|
|
27
|
+
*
|
|
28
|
+
* Posture mirrors every other bus seam here: LOUD-non-fatal. No wicked-bus, or a broken db → the
|
|
29
|
+
* factory logs once and returns null, the daemon boots without the relay, and the POST route
|
|
30
|
+
* answers 503 instead of pretending the emit landed.
|
|
31
|
+
*/
|
|
32
|
+
import type { FastifyInstance } from 'fastify';
|
|
33
|
+
import { z } from 'zod';
|
|
34
|
+
/** The ONE envelope type this relay puts on `/ws`. Consumers that don't know it ignore it — the
|
|
35
|
+
* CoreEvent contract is additive-safe (DES-STUDIO-001 §2.1). */
|
|
36
|
+
export declare const INTERACTIVE_EVENT_FRAME = "interactiveEvent";
|
|
37
|
+
/**
|
|
38
|
+
* Server-side whitelist of the event types a `/ws` client may emit through
|
|
39
|
+
* `POST /projects/:projectId/interactive-events`. A non-whitelisted type is a 400 that NAMES this
|
|
40
|
+
* array — the UI never has to guess. Extend here; nothing else needs to change.
|
|
41
|
+
*/
|
|
42
|
+
export declare const EMITTABLE_TYPES: readonly string[];
|
|
43
|
+
/** The request body: a whitelisted type plus that type's own payload. Strict — an unknown key is
|
|
44
|
+
* a 400, not a silently-dropped field. */
|
|
45
|
+
export declare const EmitInteractiveEventSchema: z.ZodObject<{
|
|
46
|
+
type: z.ZodString;
|
|
47
|
+
payload: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
48
|
+
}, "strict", z.ZodTypeAny, {
|
|
49
|
+
type: string;
|
|
50
|
+
payload?: Record<string, unknown> | undefined;
|
|
51
|
+
}, {
|
|
52
|
+
type: string;
|
|
53
|
+
payload?: Record<string, unknown> | undefined;
|
|
54
|
+
}>;
|
|
55
|
+
export interface InteractiveRelay {
|
|
56
|
+
/**
|
|
57
|
+
* Emit one interactive event onto the bus. Returns true on success (or on WB-002, a duplicate
|
|
58
|
+
* key — the emit already happened); logs and returns false otherwise. Never throws.
|
|
59
|
+
*/
|
|
60
|
+
emitInteractive(type: string, payload: Record<string, unknown>, idempotencyKey: string): boolean;
|
|
61
|
+
stop(): Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
export interface InteractiveRelayOptions {
|
|
64
|
+
/** Bus db path; omit for wicked-bus's own resolution (honors WICKED_BUS_DATA_DIR). */
|
|
65
|
+
dbPath?: string;
|
|
66
|
+
/** Poll cadence for the relay subscriber, ms (tests shorten it). */
|
|
67
|
+
pollIntervalMs?: number;
|
|
68
|
+
log?: (msg: string) => void;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Open the bus and arm both halves. Returns `null` (logged) when wicked-bus is not importable or
|
|
72
|
+
* the db cannot open — the caller degrades to a daemon whose `/ws` simply carries no interactive
|
|
73
|
+
* frames, exactly as before this slice.
|
|
74
|
+
*/
|
|
75
|
+
export declare function startInteractiveWsRelay(opts?: InteractiveRelayOptions): Promise<InteractiveRelay | null>;
|
|
76
|
+
/** Occurrence-unique, type-inclusive (DES-EXEC-001 rev0.2 #4a): two types sharing a key would
|
|
77
|
+
* silently drop one on the bus's UNIQUE constraint, and every UI emission is a new fact. */
|
|
78
|
+
export declare function interactiveEmitKey(projectId: string, type: string): string;
|
|
79
|
+
/**
|
|
80
|
+
* `POST /api/v1/projects/:projectId/interactive-events` — the UI-emittable direction (§6.1).
|
|
81
|
+
* Registered even when the relay is null, so the failure is an honest 503 rather than a 404 that
|
|
82
|
+
* reads like "this daemon doesn't have the feature".
|
|
83
|
+
*/
|
|
84
|
+
export declare function registerInteractiveEventRoutes(app: FastifyInstance, relay: InteractiveRelay | null): void;
|
|
85
|
+
//# sourceMappingURL=ws-relay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws-relay.d.ts","sourceRoot":"","sources":["../../src/interactive/ws-relay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAiBxB;iEACiE;AACjE,eAAO,MAAM,uBAAuB,qBAAqB,CAAC;AAE1D;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,SAAS,MAAM,EAG5C,CAAC;AAEF;2CAC2C;AAC3C,eAAO,MAAM,0BAA0B;;;;;;;;;EAK5B,CAAC;AAEZ,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC;IACjG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,MAAM,WAAW,uBAAuB;IACtC,sFAAsF;IACtF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC3C,IAAI,GAAE,uBAA4B,GACjC,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAgGlC;AAED;6FAC6F;AAC7F,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1E;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAC5C,GAAG,EAAE,eAAe,EACpB,KAAK,EAAE,gBAAgB,GAAG,IAAI,GAC7B,IAAI,CAmCN"}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DES-MERGE-001 §5.4 / §6.1 (slice 3) — the `wicked.interactive.*` ↔ `/ws` relay.
|
|
3
|
+
*
|
|
4
|
+
* wicked-studio is absorbing wicked-interactive. The SPA already holds crew's `/ws` socket for
|
|
5
|
+
* CoreEvent frames; interactive's own progress travels on the wicked-bus. This module bridges the
|
|
6
|
+
* two so the skin needs exactly ONE socket — no second websocket, no new port.
|
|
7
|
+
*
|
|
8
|
+
* Two halves, one bus handle:
|
|
9
|
+
*
|
|
10
|
+
* 1. RELAY (bus → /ws). Subscribe to `wicked.interactive.**` and rebroadcast each event as
|
|
11
|
+
* `{ type: 'interactiveEvent', event: <the full bus event> }` — ONE envelope type, the original
|
|
12
|
+
* event nested whole, no field renaming, no project filter. Ordering is as received (the bus
|
|
13
|
+
* hands a batch to the handler in order and `broadcast` is synchronous), and there is no
|
|
14
|
+
* buffering beyond what the existing broadcast path already does.
|
|
15
|
+
*
|
|
16
|
+
* 2. EMIT (/ws client → bus). `POST /projects/:projectId/interactive-events` validates the body
|
|
17
|
+
* against a SERVER-SIDE whitelist ({@link EMITTABLE_TYPES}) and puts the event on the bus with
|
|
18
|
+
* interactive's own envelope conventions (domain `wicked-interactive`, producer `wi-crew`,
|
|
19
|
+
* `ts`-stamped payload — draft-events.ts is the precedent), so interactive's own subscribers
|
|
20
|
+
* see a UI-originated event exactly as they see a service-originated one.
|
|
21
|
+
*
|
|
22
|
+
* Deliberately DISTINCT from the `projectActivity` bridge in projects/events.ts, which reads the
|
|
23
|
+
* same filter: that one is project-FILTERED and reshapes the event into an activity frame for a
|
|
24
|
+
* project view. This one is unfiltered and verbatim, because the studio's document canvas must see
|
|
25
|
+
* the events of a doc that was never filed under a project. Separate plugin identity → separate
|
|
26
|
+
* durable cursor, so neither seam can strand the other.
|
|
27
|
+
*
|
|
28
|
+
* Posture mirrors every other bus seam here: LOUD-non-fatal. No wicked-bus, or a broken db → the
|
|
29
|
+
* factory logs once and returns null, the daemon boots without the relay, and the POST route
|
|
30
|
+
* answers 503 instead of pretending the emit landed.
|
|
31
|
+
*/
|
|
32
|
+
import { randomUUID } from 'node:crypto';
|
|
33
|
+
import { z } from 'zod';
|
|
34
|
+
import { broadcast } from '../events/bus.js';
|
|
35
|
+
import { API_PREFIX } from '../api/api-prefix.js';
|
|
36
|
+
import { LOCAL_ACTOR } from '../api/auth.js';
|
|
37
|
+
import { INTERACTIVE_DOMAIN, INTERACTIVE_PRODUCER } from './draft-events.js';
|
|
38
|
+
const V = API_PREFIX;
|
|
39
|
+
/** Dedicated durable-cursor identity — NOT the projects bridge's, so the two seams that read
|
|
40
|
+
* `wicked.interactive.**` advance independent cursors. */
|
|
41
|
+
const RELAY_PLUGIN = 'wicked-crew-interactive-relay';
|
|
42
|
+
const RELAY_FILTER = 'wicked.interactive.**';
|
|
43
|
+
/** The scope guard, restated locally: the property "only interactive events are relayed" is this
|
|
44
|
+
* module's, not the bus glob's. */
|
|
45
|
+
const RELAY_TYPE_PREFIX = 'wicked.interactive.';
|
|
46
|
+
/** The ONE envelope type this relay puts on `/ws`. Consumers that don't know it ignore it — the
|
|
47
|
+
* CoreEvent contract is additive-safe (DES-STUDIO-001 §2.1). */
|
|
48
|
+
export const INTERACTIVE_EVENT_FRAME = 'interactiveEvent';
|
|
49
|
+
/**
|
|
50
|
+
* Server-side whitelist of the event types a `/ws` client may emit through
|
|
51
|
+
* `POST /projects/:projectId/interactive-events`. A non-whitelisted type is a 400 that NAMES this
|
|
52
|
+
* array — the UI never has to guess. Extend here; nothing else needs to change.
|
|
53
|
+
*/
|
|
54
|
+
export const EMITTABLE_TYPES = [
|
|
55
|
+
'wicked.interactive.feedback.submitted',
|
|
56
|
+
'wicked.interactive.status.requested',
|
|
57
|
+
];
|
|
58
|
+
/** The request body: a whitelisted type plus that type's own payload. Strict — an unknown key is
|
|
59
|
+
* a 400, not a silently-dropped field. */
|
|
60
|
+
export const EmitInteractiveEventSchema = z
|
|
61
|
+
.object({
|
|
62
|
+
type: z.string().min(1).max(128),
|
|
63
|
+
payload: z.record(z.string(), z.unknown()).optional(),
|
|
64
|
+
})
|
|
65
|
+
.strict();
|
|
66
|
+
/**
|
|
67
|
+
* Open the bus and arm both halves. Returns `null` (logged) when wicked-bus is not importable or
|
|
68
|
+
* the db cannot open — the caller degrades to a daemon whose `/ws` simply carries no interactive
|
|
69
|
+
* frames, exactly as before this slice.
|
|
70
|
+
*/
|
|
71
|
+
export async function startInteractiveWsRelay(opts = {}) {
|
|
72
|
+
const log = opts.log ?? (() => undefined);
|
|
73
|
+
let bus;
|
|
74
|
+
try {
|
|
75
|
+
bus = await import('wicked-bus');
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
log(`[interactive-relay] wicked-bus is not importable — the interactive /ws relay is disabled: ${err instanceof Error ? err.message : String(err)}`);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
let db;
|
|
82
|
+
let config;
|
|
83
|
+
try {
|
|
84
|
+
const override = opts.dbPath !== undefined ? { db_path: opts.dbPath } : {};
|
|
85
|
+
config = bus.loadConfig(override);
|
|
86
|
+
db = bus.openDb(override);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
log(`[interactive-relay] could not open the bus db${opts.dbPath !== undefined ? ` at ${opts.dbPath}` : ''} — the interactive /ws relay is disabled: ${err instanceof Error ? err.message : String(err)}`);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
// Half 1 — bus → /ws. maxRetries: 0 for the same reason the projects bridge uses it: a retry
|
|
93
|
+
// loop or a DLQ entry for a lost liveness frame is noise, and the durable feed is the record.
|
|
94
|
+
//
|
|
95
|
+
// This subscription also sees the events emitted by half 2 (they match the same filter). That
|
|
96
|
+
// is not a loop — the relay only broadcasts, never re-emits — and the echo is the UI's
|
|
97
|
+
// confirmation that its own emission actually landed on the bus.
|
|
98
|
+
let subscription = null;
|
|
99
|
+
try {
|
|
100
|
+
subscription = bus.subscribe({
|
|
101
|
+
db,
|
|
102
|
+
plugin: RELAY_PLUGIN,
|
|
103
|
+
filter: RELAY_FILTER,
|
|
104
|
+
cursor_init: 'latest',
|
|
105
|
+
pollIntervalMs: opts.pollIntervalMs ?? 2000,
|
|
106
|
+
maxRetries: 0,
|
|
107
|
+
handler: (event) => {
|
|
108
|
+
if (!event.event_type.startsWith(RELAY_TYPE_PREFIX))
|
|
109
|
+
return;
|
|
110
|
+
broadcast({ type: INTERACTIVE_EVENT_FRAME, event });
|
|
111
|
+
},
|
|
112
|
+
onError: (err, event) => {
|
|
113
|
+
log(`[interactive-relay] relay error on event ${String(event?.event_id ?? '?')}: ${err.message}`);
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
log(`[interactive-relay] could not arm the /ws relay subscriber (the emit direction still works): ${err instanceof Error ? err.message : String(err)}`);
|
|
119
|
+
}
|
|
120
|
+
// Half 2 — the emit surface the POST route calls after the whitelist check. The subdomain is
|
|
121
|
+
// the third segment of interactive's own type grammar (`wicked.interactive.<subdomain>.<verb>`).
|
|
122
|
+
function emitInteractive(type, payload, idempotencyKey) {
|
|
123
|
+
try {
|
|
124
|
+
bus.emit(db, config, {
|
|
125
|
+
event_type: type,
|
|
126
|
+
domain: INTERACTIVE_DOMAIN,
|
|
127
|
+
subdomain: type.split('.')[2] ?? 'status',
|
|
128
|
+
payload: { ts: new Date().toISOString(), ...payload },
|
|
129
|
+
producer_id: INTERACTIVE_PRODUCER,
|
|
130
|
+
idempotency_key: idempotencyKey,
|
|
131
|
+
});
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
const code = err.error;
|
|
136
|
+
if (code === 'WB-002')
|
|
137
|
+
return true; // duplicate key — the emit already happened
|
|
138
|
+
log(`[interactive-relay] emit ${type} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
emitInteractive,
|
|
144
|
+
stop: async () => {
|
|
145
|
+
if (subscription !== null)
|
|
146
|
+
await subscription.stop();
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/** Occurrence-unique, type-inclusive (DES-EXEC-001 rev0.2 #4a): two types sharing a key would
|
|
151
|
+
* silently drop one on the bus's UNIQUE constraint, and every UI emission is a new fact. */
|
|
152
|
+
export function interactiveEmitKey(projectId, type) {
|
|
153
|
+
return `crew:interactive.ui:${projectId}:${type}:${randomUUID()}`;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* `POST /api/v1/projects/:projectId/interactive-events` — the UI-emittable direction (§6.1).
|
|
157
|
+
* Registered even when the relay is null, so the failure is an honest 503 rather than a 404 that
|
|
158
|
+
* reads like "this daemon doesn't have the feature".
|
|
159
|
+
*/
|
|
160
|
+
export function registerInteractiveEventRoutes(app, relay) {
|
|
161
|
+
const actorOf = (req) => req.actor ?? LOCAL_ACTOR;
|
|
162
|
+
app.post(`${V}/projects/:projectId/interactive-events`, async (req, reply) => {
|
|
163
|
+
const { projectId } = req.params;
|
|
164
|
+
const parsed = EmitInteractiveEventSchema.safeParse(req.body);
|
|
165
|
+
if (!parsed.success) {
|
|
166
|
+
return reply.code(400).send({ error: 'Invalid request body', details: parsed.error.issues });
|
|
167
|
+
}
|
|
168
|
+
const { type } = parsed.data;
|
|
169
|
+
if (!EMITTABLE_TYPES.includes(type)) {
|
|
170
|
+
return reply.code(400).send({
|
|
171
|
+
error: `Event type '${type}' is not emittable from a client`,
|
|
172
|
+
allowed: EMITTABLE_TYPES,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (relay === null) {
|
|
176
|
+
return reply
|
|
177
|
+
.code(503)
|
|
178
|
+
.send({ error: 'the interactive bus seam is not armed on this daemon' });
|
|
179
|
+
}
|
|
180
|
+
// Server-side fields go LAST: `project_id` and `actor` are the daemon's to state, never the
|
|
181
|
+
// caller's (task #88 — event actors are not caller-supplied), so a payload that names either
|
|
182
|
+
// one cannot override them.
|
|
183
|
+
const idempotencyKey = interactiveEmitKey(projectId, type);
|
|
184
|
+
const emitted = relay.emitInteractive(type, { ...(parsed.data.payload ?? {}), project_id: projectId, actor: actorOf(req).id }, idempotencyKey);
|
|
185
|
+
if (!emitted) {
|
|
186
|
+
return reply.code(502).send({ error: `the bus refused the ${type} emit` });
|
|
187
|
+
}
|
|
188
|
+
return reply.code(202).send({ emitted: true, type, projectId, idempotencyKey });
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=ws-relay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws-relay.js","sourceRoot":"","sources":["../../src/interactive/ws-relay.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The project activity feed (DES-PROJECT-001 §5.2) — a READ-SIDE MERGE, not a new store.
|
|
3
|
+
*
|
|
4
|
+
* Two sources, one normalized shape:
|
|
5
|
+
* - CREW: the durable core-event log of the project's `crew.run`/`crew.chat` members
|
|
6
|
+
* (`adapter.runEvents` — the same log `/runs/:id/events` serves), entry id `crew:<run>:<seq>`.
|
|
7
|
+
* - INTERACTIVE: `wicked.interactive.*` bus events carrying this `project_id`, read directly
|
|
8
|
+
* from the bus SQLite READ-ONLY (`node:sqlite`, the `requirements.ts` precedent — wicked-bus
|
|
9
|
+
* exposes subscribe-cursors, not ad-hoc history queries, and a durable cursor per feed-read
|
|
10
|
+
* would turn a GET into a write), entry id `bus:<event_id>`.
|
|
11
|
+
*
|
|
12
|
+
* Newest-first, cursor on `(ts, id)` — an opaque `<ts>:<id>` token, base64url. The merge is
|
|
13
|
+
* recomputed per read; members are few and the log excludes high-volume frames, so the simple
|
|
14
|
+
* full-merge is the honest v1 (the ADR explicitly rejects a new store here).
|
|
15
|
+
*/
|
|
16
|
+
import type { CoreAdapter } from '../core/adapter.js';
|
|
17
|
+
import type { ActivityEntry } from '../core/types.js';
|
|
18
|
+
/** Opaque cursor: base64url of `<ts>:<id>`. */
|
|
19
|
+
export declare function encodeCursor(ts: number, id: string): string;
|
|
20
|
+
export declare function decodeCursor(cursor: string): {
|
|
21
|
+
ts: number;
|
|
22
|
+
id: string;
|
|
23
|
+
} | null;
|
|
24
|
+
/** Build one page of the merged feed. `refs` = the project's run/chat member refs. */
|
|
25
|
+
export declare function buildActivityPage(adapter: CoreAdapter, projectId: string, refs: string[], busDbPath: string | null, cursor: string | undefined, limit: number): Promise<{
|
|
26
|
+
entries: ActivityEntry[];
|
|
27
|
+
nextCursor: string | null;
|
|
28
|
+
}>;
|
|
29
|
+
//# sourceMappingURL=activity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"activity.d.ts","sourceRoot":"","sources":["../../src/projects/activity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAuDtD,+CAA+C;AAC/C,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAW9E;AAsFD,sFAAsF;AACtF,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EAAE,EACd,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC;IAAE,OAAO,EAAE,aAAa,EAAE,CAAC;IAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAgBlE"}
|