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,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable handoff ledger shared by crew's wicked-interactive seams (task #86, Phase 7c).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from the draft leg (draft-events.ts, PR #241) when the structural-edit leg arrived,
|
|
5
|
+
* because both seams need the same replay-dedup discipline with DIFFERENT key grammars:
|
|
6
|
+
*
|
|
7
|
+
* - the DRAFT leg keys by document id — one first draft per document lifetime;
|
|
8
|
+
* - the EDIT leg keys by `<doc>:v<version>` — one governed edit per HANDOFF, because the same
|
|
9
|
+
* document legitimately produces many structural handoffs over its life (versioned targeting).
|
|
10
|
+
*
|
|
11
|
+
* The ledger itself is key-agnostic: a durable map `key → HandoffLedgerEntry`, JSON on disk,
|
|
12
|
+
* written atomically (tmp + rename). The bus cursor alone cannot carry this: at-least-once
|
|
13
|
+
* delivery means the same trigger can arrive again after a crash between handling and cursor
|
|
14
|
+
* advance, and in-memory state dies with the process. The ledger is the system of record for
|
|
15
|
+
* "this handoff was answered"; each seam's in-flight map is just the live half.
|
|
16
|
+
*/
|
|
17
|
+
/** One ledger row — the lifecycle of a handoff a seam answered. */
|
|
18
|
+
export interface HandoffLedgerEntry {
|
|
19
|
+
runId: string;
|
|
20
|
+
launchedAt: string;
|
|
21
|
+
/** Set once the closing event (draft.completed / edit.completed) was emitted. */
|
|
22
|
+
emittedAt?: string;
|
|
23
|
+
/** Set when the governed run ended without a usable result; kept so a redelivered trigger
|
|
24
|
+
* does not silently relaunch a run an operator should look at first. */
|
|
25
|
+
failedAt?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare class InteractiveHandoffLedger {
|
|
28
|
+
private readonly path;
|
|
29
|
+
private entries;
|
|
30
|
+
constructor(path: string);
|
|
31
|
+
has(key: string): boolean;
|
|
32
|
+
get(key: string): HandoffLedgerEntry | undefined;
|
|
33
|
+
recordLaunch(key: string, runId: string): void;
|
|
34
|
+
recordEmitted(key: string): void;
|
|
35
|
+
recordFailure(key: string): void;
|
|
36
|
+
size(): number;
|
|
37
|
+
private persist;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=ledger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../../src/interactive/ledger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,mEAAmE;AACnE,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,iFAAiF;IACjF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;6EACyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAOD,qBAAa,wBAAwB;IACnC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,OAAO,CAAqC;gBAExC,IAAI,EAAE,MAAM;IAuCxB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAIzB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS;IAIhD,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAK9C,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAQhC,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAQhC,IAAI,IAAI,MAAM;IAId,OAAO,CAAC,OAAO;CAMhB"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable handoff ledger shared by crew's wicked-interactive seams (task #86, Phase 7c).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from the draft leg (draft-events.ts, PR #241) when the structural-edit leg arrived,
|
|
5
|
+
* because both seams need the same replay-dedup discipline with DIFFERENT key grammars:
|
|
6
|
+
*
|
|
7
|
+
* - the DRAFT leg keys by document id — one first draft per document lifetime;
|
|
8
|
+
* - the EDIT leg keys by `<doc>:v<version>` — one governed edit per HANDOFF, because the same
|
|
9
|
+
* document legitimately produces many structural handoffs over its life (versioned targeting).
|
|
10
|
+
*
|
|
11
|
+
* The ledger itself is key-agnostic: a durable map `key → HandoffLedgerEntry`, JSON on disk,
|
|
12
|
+
* written atomically (tmp + rename). The bus cursor alone cannot carry this: at-least-once
|
|
13
|
+
* delivery means the same trigger can arrive again after a crash between handling and cursor
|
|
14
|
+
* advance, and in-memory state dies with the process. The ledger is the system of record for
|
|
15
|
+
* "this handoff was answered"; each seam's in-flight map is just the live half.
|
|
16
|
+
*/
|
|
17
|
+
import { mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
18
|
+
import { dirname } from 'node:path';
|
|
19
|
+
export class InteractiveHandoffLedger {
|
|
20
|
+
path;
|
|
21
|
+
entries;
|
|
22
|
+
constructor(path) {
|
|
23
|
+
this.path = path;
|
|
24
|
+
this.entries = {};
|
|
25
|
+
try {
|
|
26
|
+
// The persisted top-level field stays `docs` for wire-compat with ledgers the draft leg
|
|
27
|
+
// already wrote to operator machines (~/.wicked-crew/interactive-draft-ledger.json).
|
|
28
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
29
|
+
if (parsed && typeof parsed === 'object' && parsed.docs && typeof parsed.docs === 'object') {
|
|
30
|
+
for (const [key, entry] of Object.entries(parsed.docs)) {
|
|
31
|
+
// Per-row validation: one malformed row (null, primitive, missing/empty fields) costs
|
|
32
|
+
// that row alone — letting it throw into the outer catch would blank the WHOLE ledger
|
|
33
|
+
// and defeat replay-dedup for every handoff already answered. A kept row must satisfy
|
|
34
|
+
// the full HandoffLedgerEntry contract (non-empty runId + launchedAt strings).
|
|
35
|
+
if (typeof entry !== 'object' ||
|
|
36
|
+
entry === null ||
|
|
37
|
+
typeof entry.runId !== 'string' ||
|
|
38
|
+
entry.runId.length === 0 ||
|
|
39
|
+
typeof entry.launchedAt !== 'string') {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const { draftEmittedAt, ...rest } = entry;
|
|
43
|
+
this.entries[key] = {
|
|
44
|
+
...rest,
|
|
45
|
+
// Migrate the draft leg's original field name in place.
|
|
46
|
+
...(rest.emittedAt === undefined && draftEmittedAt !== undefined
|
|
47
|
+
? { emittedAt: draftEmittedAt }
|
|
48
|
+
: {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Missing or malformed ledger — start empty. Malformed is deliberately NOT fatal: a
|
|
55
|
+
// corrupt ledger must not stop the daemon, and the worst case is one duplicate closing
|
|
56
|
+
// emit, which the deterministic bus idempotency key still dedupes.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
has(key) {
|
|
60
|
+
return Object.prototype.hasOwnProperty.call(this.entries, key);
|
|
61
|
+
}
|
|
62
|
+
get(key) {
|
|
63
|
+
return this.entries[key];
|
|
64
|
+
}
|
|
65
|
+
recordLaunch(key, runId) {
|
|
66
|
+
this.entries[key] = { runId, launchedAt: new Date().toISOString() };
|
|
67
|
+
this.persist();
|
|
68
|
+
}
|
|
69
|
+
recordEmitted(key) {
|
|
70
|
+
const entry = this.entries[key];
|
|
71
|
+
if (entry) {
|
|
72
|
+
entry.emittedAt = new Date().toISOString();
|
|
73
|
+
this.persist();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
recordFailure(key) {
|
|
77
|
+
const entry = this.entries[key];
|
|
78
|
+
if (entry) {
|
|
79
|
+
entry.failedAt = new Date().toISOString();
|
|
80
|
+
this.persist();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
size() {
|
|
84
|
+
return Object.keys(this.entries).length;
|
|
85
|
+
}
|
|
86
|
+
persist() {
|
|
87
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
88
|
+
const tmp = `${this.path}.tmp-${process.pid}`;
|
|
89
|
+
writeFileSync(tmp, JSON.stringify({ docs: this.entries }, null, 2), 'utf8');
|
|
90
|
+
renameSync(tmp, this.path);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=ledger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ledger.js","sourceRoot":"","sources":["../../src/interactive/ledger.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/api/v1/projects/:projectId/interactive/*` — crew reverse-proxies the wicked-interactive
|
|
3
|
+
* bridge (DES-MERGE-001 §5.3, §7.2; slice 1).
|
|
4
|
+
*
|
|
5
|
+
* PURE TRANSPORT. Nothing user-visible moves in this slice: what changes is that interactive's
|
|
6
|
+
* HTTP surface becomes reachable through crew's OWN origin, so the studio client never learns a
|
|
7
|
+
* second origin, never needs CORS, and never carries a bridge port literal — ADR-0022's dynamic
|
|
8
|
+
* port is honored rather than worked around, and the bridge stops being browser-reachable at all.
|
|
9
|
+
*
|
|
10
|
+
* The path encodes the project (§7.2) because the ROOT is a per-project setting: the same proxy
|
|
11
|
+
* mount serves N interactive instances, one bridge per resolved root.
|
|
12
|
+
*
|
|
13
|
+
* Two things are deliberately hand-rolled over `node:http` rather than delegated to `fetch`:
|
|
14
|
+
*
|
|
15
|
+
* 1. UNBUFFERED STREAMING BOTH WAYS. Interactive's generation surface is SSE; a proxy that
|
|
16
|
+
* buffers turns a live narration into a single blob delivered at stream close. Piping raw
|
|
17
|
+
* sockets keeps every chunk flowing as it arrives, in both directions.
|
|
18
|
+
* 2. VERBATIM FORWARDING. The path remainder and query string are taken from `req.raw.url`,
|
|
19
|
+
* not from Fastify's decoded wildcard param, so percent-encoding survives the hop intact.
|
|
20
|
+
*/
|
|
21
|
+
import type { FastifyInstance } from 'fastify';
|
|
22
|
+
import type { CoreAdapter } from '../core/adapter.js';
|
|
23
|
+
import type { ProjectSettingsStore } from '../projects/settings.js';
|
|
24
|
+
import { InteractiveBridgePool, type LiveBridge } from './bridge-pool.js';
|
|
25
|
+
/**
|
|
26
|
+
* Point a `Location` back at the proxy. The bridge answers on its own origin and knows nothing
|
|
27
|
+
* about the mount prefix, so both spellings it can emit have to be re-anchored: an absolute URL
|
|
28
|
+
* on the bridge origin, and a root-relative path. Anything else (a foreign origin, a relative
|
|
29
|
+
* path) is left exactly as-is — rewriting those would be inventing a redirect target.
|
|
30
|
+
*/
|
|
31
|
+
export declare function rewriteLocation(location: string, bridge: LiveBridge, prefix: string): string;
|
|
32
|
+
export interface InteractiveProxyDeps {
|
|
33
|
+
settings: ProjectSettingsStore;
|
|
34
|
+
pool: InteractiveBridgePool;
|
|
35
|
+
env?: NodeJS.ProcessEnv;
|
|
36
|
+
log?: (msg: string) => void;
|
|
37
|
+
}
|
|
38
|
+
export declare function registerInteractiveProxy(app: FastifyInstance, adapter: CoreAdapter, deps: InteractiveProxyDeps): void;
|
|
39
|
+
//# sourceMappingURL=proxy-routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy-routes.d.ts","sourceRoot":"","sources":["../../src/interactive/proxy-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAG7E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAGtD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAEpE,OAAO,EAA0B,qBAAqB,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AA4BlG;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAO5F;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,IAAI,EAAE,qBAAqB,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,eAAe,EAAE,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,oBAAoB,GAAG,IAAI,CAiErH"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/api/v1/projects/:projectId/interactive/*` — crew reverse-proxies the wicked-interactive
|
|
3
|
+
* bridge (DES-MERGE-001 §5.3, §7.2; slice 1).
|
|
4
|
+
*
|
|
5
|
+
* PURE TRANSPORT. Nothing user-visible moves in this slice: what changes is that interactive's
|
|
6
|
+
* HTTP surface becomes reachable through crew's OWN origin, so the studio client never learns a
|
|
7
|
+
* second origin, never needs CORS, and never carries a bridge port literal — ADR-0022's dynamic
|
|
8
|
+
* port is honored rather than worked around, and the bridge stops being browser-reachable at all.
|
|
9
|
+
*
|
|
10
|
+
* The path encodes the project (§7.2) because the ROOT is a per-project setting: the same proxy
|
|
11
|
+
* mount serves N interactive instances, one bridge per resolved root.
|
|
12
|
+
*
|
|
13
|
+
* Two things are deliberately hand-rolled over `node:http` rather than delegated to `fetch`:
|
|
14
|
+
*
|
|
15
|
+
* 1. UNBUFFERED STREAMING BOTH WAYS. Interactive's generation surface is SSE; a proxy that
|
|
16
|
+
* buffers turns a live narration into a single blob delivered at stream close. Piping raw
|
|
17
|
+
* sockets keeps every chunk flowing as it arrives, in both directions.
|
|
18
|
+
* 2. VERBATIM FORWARDING. The path remainder and query string are taken from `req.raw.url`,
|
|
19
|
+
* not from Fastify's decoded wildcard param, so percent-encoding survives the hop intact.
|
|
20
|
+
*/
|
|
21
|
+
import { request as httpRequest } from 'node:http';
|
|
22
|
+
import { API_PREFIX } from '../api/api-prefix.js';
|
|
23
|
+
import { ProjectsUnsupportedError } from '../core/adapter.js';
|
|
24
|
+
import { DEFAULT_PROJECT_ID } from '../projects/routes.js';
|
|
25
|
+
import { resolveInteractiveRoot } from './bridge-root.js';
|
|
26
|
+
import { BridgeUnavailableError } from './bridge-pool.js';
|
|
27
|
+
/** Per-hop headers that must never be forwarded across a proxy (RFC 9110 §7.6.1). */
|
|
28
|
+
const HOP_BY_HOP = new Set([
|
|
29
|
+
'connection',
|
|
30
|
+
'keep-alive',
|
|
31
|
+
'proxy-authenticate',
|
|
32
|
+
'proxy-authorization',
|
|
33
|
+
'te',
|
|
34
|
+
'trailer',
|
|
35
|
+
'transfer-encoding',
|
|
36
|
+
'upgrade',
|
|
37
|
+
]);
|
|
38
|
+
function forwardableRequestHeaders(headers, bridge) {
|
|
39
|
+
const out = {};
|
|
40
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
41
|
+
if (HOP_BY_HOP.has(name) || value === undefined)
|
|
42
|
+
continue;
|
|
43
|
+
// Crew's bearer token is CREW's credential. The bridge is a local process with no auth of
|
|
44
|
+
// its own and no use for it; forwarding it would hand a third-party child crew's ambient
|
|
45
|
+
// authority for free. One auth path (§5.3) means crew terminates auth, it does not relay it.
|
|
46
|
+
if (name === 'authorization')
|
|
47
|
+
continue;
|
|
48
|
+
out[name] = value;
|
|
49
|
+
}
|
|
50
|
+
out['host'] = `${bridge.host}:${bridge.port}`;
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Point a `Location` back at the proxy. The bridge answers on its own origin and knows nothing
|
|
55
|
+
* about the mount prefix, so both spellings it can emit have to be re-anchored: an absolute URL
|
|
56
|
+
* on the bridge origin, and a root-relative path. Anything else (a foreign origin, a relative
|
|
57
|
+
* path) is left exactly as-is — rewriting those would be inventing a redirect target.
|
|
58
|
+
*/
|
|
59
|
+
export function rewriteLocation(location, bridge, prefix) {
|
|
60
|
+
for (const host of new Set([bridge.host, 'localhost', '127.0.0.1'])) {
|
|
61
|
+
const origin = `http://${host}:${bridge.port}`;
|
|
62
|
+
if (location.startsWith(origin))
|
|
63
|
+
return `${prefix}${location.slice(origin.length)}`;
|
|
64
|
+
}
|
|
65
|
+
if (location.startsWith('/'))
|
|
66
|
+
return `${prefix}${location}`;
|
|
67
|
+
return location;
|
|
68
|
+
}
|
|
69
|
+
export function registerInteractiveProxy(app, adapter, deps) {
|
|
70
|
+
const { settings, pool } = deps;
|
|
71
|
+
const env = deps.env ?? process.env;
|
|
72
|
+
// Strips exactly the mount prefix off the RAW url, leaving the remainder + query untouched.
|
|
73
|
+
const prefixRe = new RegExp(`^${API_PREFIX}/projects/[^/?#]+/interactive`);
|
|
74
|
+
/** The resolved docs root for a project, or null when no such project exists. */
|
|
75
|
+
async function rootFor(projectId) {
|
|
76
|
+
// `default` is SYNTHESIZED by the route layer (DES-PROJECT-001 §7) — the engine has no row
|
|
77
|
+
// for it, so an existence check there would 404 the one project every operator starts with.
|
|
78
|
+
if (projectId !== DEFAULT_PROJECT_ID) {
|
|
79
|
+
try {
|
|
80
|
+
if ((await adapter.projectGet(projectId)) === null)
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
// A pre-0.6.0 engine has no project surface at all; the shared default root is still
|
|
85
|
+
// a truthful answer for the only project such a deployment can have.
|
|
86
|
+
if (!(err instanceof ProjectsUnsupportedError))
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return resolveInteractiveRoot(settings.get(projectId), env);
|
|
91
|
+
}
|
|
92
|
+
// Encapsulated so the raw-body parser below applies to the PROXY ONLY. The root instance
|
|
93
|
+
// installs a JSON parser that buffers and parses the body — correct for every other route,
|
|
94
|
+
// fatal here: a parsed body is a consumed stream, and an SSE POST would arrive at the bridge
|
|
95
|
+
// only once the client finished sending.
|
|
96
|
+
void app.register(async (scope) => {
|
|
97
|
+
scope.removeAllContentTypeParsers();
|
|
98
|
+
scope.addContentTypeParser('*', (_req, payload, done) => {
|
|
99
|
+
done(null, payload);
|
|
100
|
+
});
|
|
101
|
+
scope.all(`${API_PREFIX}/projects/:projectId/interactive/*`, async (req, reply) => {
|
|
102
|
+
const { projectId } = req.params;
|
|
103
|
+
const prefix = `${API_PREFIX}/projects/${encodeURIComponent(projectId)}/interactive`;
|
|
104
|
+
const root = await rootFor(projectId);
|
|
105
|
+
if (root === null)
|
|
106
|
+
return reply.code(404).send({ error: `Project ${projectId} not found` });
|
|
107
|
+
let bridge;
|
|
108
|
+
try {
|
|
109
|
+
bridge = await pool.ensure(root);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
return unavailable(reply, err, deps.log);
|
|
113
|
+
}
|
|
114
|
+
const target = (req.raw.url ?? '').replace(prefixRe, '') || '/';
|
|
115
|
+
try {
|
|
116
|
+
await forward(req, reply, bridge, target, prefix);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
// The cached bridge died between the pid check and the connect (an operator killed it,
|
|
120
|
+
// a crash). Invalidate and let `ensure` restart it — ONE retry, so a genuinely broken
|
|
121
|
+
// bridge fails fast to a 503 instead of looping.
|
|
122
|
+
if (!isConnectionRefused(err) || reply.raw.headersSent)
|
|
123
|
+
throw err;
|
|
124
|
+
pool.invalidate(root);
|
|
125
|
+
try {
|
|
126
|
+
bridge = await pool.ensure(root);
|
|
127
|
+
}
|
|
128
|
+
catch (startErr) {
|
|
129
|
+
return unavailable(reply, startErr, deps.log);
|
|
130
|
+
}
|
|
131
|
+
await forward(req, reply, bridge, target, prefix);
|
|
132
|
+
}
|
|
133
|
+
return reply;
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function isConnectionRefused(err) {
|
|
138
|
+
const code = err?.code;
|
|
139
|
+
return code === 'ECONNREFUSED' || code === 'ECONNRESET' || code === 'EHOSTUNREACH';
|
|
140
|
+
}
|
|
141
|
+
/** §5.6's failure shape: a machine-readable code plus a command the operator can actually run. */
|
|
142
|
+
function unavailable(reply, err, log) {
|
|
143
|
+
if (!(err instanceof BridgeUnavailableError))
|
|
144
|
+
throw err;
|
|
145
|
+
log?.(`interactive bridge unavailable: ${err.message}`);
|
|
146
|
+
return reply.code(503).send({ code: 'bridge_unavailable', hint: err.hint });
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* One proxied exchange, socket to socket. `reply.hijack()` hands us the raw response before
|
|
150
|
+
* Fastify can serialize or buffer anything, which is what lets an SSE chunk reach the browser
|
|
151
|
+
* the moment the bridge emits it rather than at stream close.
|
|
152
|
+
*/
|
|
153
|
+
function forward(req, reply, bridge, target, prefix) {
|
|
154
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
155
|
+
const upstream = httpRequest({
|
|
156
|
+
host: bridge.host,
|
|
157
|
+
port: bridge.port,
|
|
158
|
+
method: req.method,
|
|
159
|
+
path: target,
|
|
160
|
+
headers: forwardableRequestHeaders(req.headers, bridge),
|
|
161
|
+
}, (res) => {
|
|
162
|
+
reply.hijack();
|
|
163
|
+
const headers = {};
|
|
164
|
+
for (const [name, value] of Object.entries(res.headers)) {
|
|
165
|
+
if (HOP_BY_HOP.has(name) || value === undefined)
|
|
166
|
+
continue;
|
|
167
|
+
headers[name] = name === 'location' && typeof value === 'string' ? rewriteLocation(value, bridge, prefix) : value;
|
|
168
|
+
}
|
|
169
|
+
reply.raw.writeHead(res.statusCode ?? 502, headers);
|
|
170
|
+
// No `pipeline` and no buffering layer: `pipe` forwards each chunk as it lands, and
|
|
171
|
+
// Node flushes it because we never set a highWaterMark barrier in between.
|
|
172
|
+
res.pipe(reply.raw);
|
|
173
|
+
res.on('end', () => resolvePromise());
|
|
174
|
+
res.on('error', (err) => {
|
|
175
|
+
reply.raw.destroy();
|
|
176
|
+
rejectPromise(err);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
upstream.on('error', (err) => {
|
|
180
|
+
if (reply.raw.headersSent)
|
|
181
|
+
reply.raw.destroy();
|
|
182
|
+
rejectPromise(err);
|
|
183
|
+
});
|
|
184
|
+
// If the client hangs up mid-stream (closing an SSE tab), stop pulling from the bridge.
|
|
185
|
+
reply.raw.on('close', () => upstream.destroy());
|
|
186
|
+
req.raw.pipe(upstream);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=proxy-routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy-routes.js","sourceRoot":"","sources":["../../src/interactive/proxy-routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,100 @@
|
|
|
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 path from 'node:path';
|
|
49
|
+
/** Default snapshot budget (~200MB). A repo whose working tree exceeds this is not
|
|
50
|
+
* snapshotted — the launch proceeds ungrounded, honestly narrated. */
|
|
51
|
+
export declare const REPO_SNAPSHOT_MAX_BYTES: number;
|
|
52
|
+
/** Why a snapshot was refused — each maps to a DIFFERENT operator-facing degrade message in
|
|
53
|
+
* the calling seam, so "too large" is never claimed about a repo that was merely unreadable
|
|
54
|
+
* (Copilot, crew#313). */
|
|
55
|
+
export type SnapshotFailureReason =
|
|
56
|
+
/** `rootPath` does not exist or is not a directory. */
|
|
57
|
+
'root-unreadable'
|
|
58
|
+
/** The tree (preflight) — or the swept clone (post-check) — exceeds the byte budget. */
|
|
59
|
+
| 'too-large'
|
|
60
|
+
/** `rootPath` and `dest` overlap (either direction) — clearing/cloning would eat live data. */
|
|
61
|
+
| 'dest-overlap'
|
|
62
|
+
/** The stale `dest` could not be cleared. */
|
|
63
|
+
| 'dest-unclearable'
|
|
64
|
+
/** Both the git clone and the file-copy fallback failed. */
|
|
65
|
+
| 'copy-failed';
|
|
66
|
+
/** The honest outcome of {@link snapshotRepo}: usable snapshot, or the reason there is none. */
|
|
67
|
+
export type SnapshotResult = {
|
|
68
|
+
ok: true;
|
|
69
|
+
} | {
|
|
70
|
+
ok: false;
|
|
71
|
+
reason: SnapshotFailureReason;
|
|
72
|
+
};
|
|
73
|
+
/** The subset of `node:path` {@link pathsOverlap} needs — injectable so the win32 drive-root
|
|
74
|
+
* behavior is unit-testable on any host (`path.win32`). */
|
|
75
|
+
type PathImpl = Pick<typeof path, 'relative' | 'isAbsolute' | 'sep'>;
|
|
76
|
+
/** `true` when `a` and `b` name the same path or one contains the other — including when one
|
|
77
|
+
* of them is a filesystem root (`/`, a win32 drive root). Exported for its unit tests. */
|
|
78
|
+
export declare function pathsOverlap(a: string, b: string, p?: PathImpl): boolean;
|
|
79
|
+
/** Options for {@link snapshotRepo}. */
|
|
80
|
+
export interface SnapshotRepoOptions {
|
|
81
|
+
/** Size budget in bytes (default {@link REPO_SNAPSHOT_MAX_BYTES}; tests shrink it). */
|
|
82
|
+
maxBytes?: number | undefined;
|
|
83
|
+
/** Diagnostics sink — every degrade path logs its real reason here. */
|
|
84
|
+
log?: ((message: string) => void) | undefined;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Materialize a snapshot of the repo at `rootPath` into `dest` (creating it), for a worker to
|
|
88
|
+
* READ as grounding context. Resolves `{ok: true}` when `dest` now holds a usable snapshot,
|
|
89
|
+
* `{ok: false, reason}` when it does not — see {@link SnapshotFailureReason}; the reason lets
|
|
90
|
+
* the caller narrate the RIGHT degrade (too large ≠ unreadable ≠ clone failed) — in which case
|
|
91
|
+
* any partial `dest` has been removed and the caller must launch UNGROUNDED (and say so on the
|
|
92
|
+
* thread). Never rejects: a grounding failure must degrade the launch, not kill it.
|
|
93
|
+
*
|
|
94
|
+
* The clone is the git-preferred path, so on a real repo the snapshot is HEAD's tracked
|
|
95
|
+
* content (uncommitted/untracked files are not included) minus swept symlinks/`node_modules`;
|
|
96
|
+
* the copy fallback takes the working tree as-is minus `.git`/`node_modules`/symlinks.
|
|
97
|
+
*/
|
|
98
|
+
export declare function snapshotRepo(rootPath: string, dest: string, opts?: SnapshotRepoOptions): Promise<SnapshotResult>;
|
|
99
|
+
export {};
|
|
100
|
+
//# sourceMappingURL=repo-snapshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repo-snapshot.d.ts","sourceRoot":"","sources":["../../src/interactive/repo-snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAGH,OAAO,IAA0C,MAAM,WAAW,CAAC;AAInE;uEACuE;AACvE,eAAO,MAAM,uBAAuB,QAAoB,CAAC;AAMzD;;2BAE2B;AAC3B,MAAM,MAAM,qBAAqB;AAC/B,uDAAuD;AACrD,iBAAiB;AACnB,wFAAwF;GACtF,WAAW;AACb,+FAA+F;GAC7F,cAAc;AAChB,6CAA6C;GAC3C,kBAAkB;AACpB,4DAA4D;GAC1D,aAAa,CAAC;AAElB,gGAAgG;AAChG,MAAM,MAAM,cAAc,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,qBAAqB,CAAA;CAAE,CAAC;AAyFzF;4DAC4D;AAC5D,KAAK,QAAQ,GAAG,IAAI,CAAC,OAAO,IAAI,EAAE,UAAU,GAAG,YAAY,GAAG,KAAK,CAAC,CAAC;AAYrE;2FAC2F;AAC3F,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,GAAE,QAAe,GAAG,OAAO,CAE9E;AAED,wCAAwC;AACxC,MAAM,WAAW,mBAAmB;IAClC,uFAAuF;IACvF,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,uEAAuE;IACvE,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;CAC/C;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,YAAY,CAChC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,mBAAwB,GAC7B,OAAO,CAAC,cAAc,CAAC,CA0HzB"}
|