@gmickel/gno 1.12.4 → 1.13.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/README.md +57 -30
- package/assets/skill/SKILL.md +5 -0
- package/assets/skill/cli-reference.md +16 -6
- package/assets/skill/mcp-reference.md +22 -3
- package/package.json +2 -1
- package/src/app/constants.ts +43 -10
- package/src/app/index-name.ts +127 -0
- package/src/cli/commands/doctor-activation.ts +151 -0
- package/src/cli/commands/doctor.ts +41 -16
- package/src/cli/commands/get.ts +18 -0
- package/src/cli/commands/mcp/atomic-config-write.ts +118 -0
- package/src/cli/commands/mcp/config-discovery.ts +42 -0
- package/src/cli/commands/mcp/config-editors.ts +432 -0
- package/src/cli/commands/mcp/config.ts +63 -160
- package/src/cli/commands/mcp/install.ts +75 -37
- package/src/cli/commands/mcp/paths.ts +141 -136
- package/src/cli/commands/mcp/server-entry.ts +66 -0
- package/src/cli/commands/mcp/status.ts +189 -57
- package/src/cli/commands/mcp/target-display.ts +30 -0
- package/src/cli/commands/mcp/uninstall.ts +29 -31
- package/src/cli/commands/mcp/yaml-config-editor.ts +257 -0
- package/src/cli/commands/mcp/yaml-layout-scanner.ts +447 -0
- package/src/cli/commands/multi-get.ts +31 -6
- package/src/cli/commands/status.ts +107 -11
- package/src/cli/program.ts +66 -20
- package/src/core/activation-connector-health.ts +19 -0
- package/src/core/activation-probe-plan.ts +321 -0
- package/src/core/activation-probe.ts +138 -0
- package/src/core/activation-receipt-store.ts +39 -0
- package/src/core/activation-status.ts +513 -0
- package/src/core/activation-verifier.ts +416 -0
- package/src/core/connector-environment.ts +68 -0
- package/src/core/connector-policy.ts +233 -0
- package/src/core/connector-verification-target.ts +150 -0
- package/src/core/connector-verifier.ts +497 -0
- package/src/core/indexed-reference.ts +33 -8
- package/src/core/runtime-entrypoint.ts +24 -0
- package/src/mcp/activation-verification-mode.ts +4 -0
- package/src/mcp/server.ts +9 -2
- package/src/sdk/client.ts +7 -0
- package/src/sdk/types.ts +1 -0
- package/src/serve/activation-health.ts +91 -0
- package/src/serve/background-runtime.ts +11 -1
- package/src/serve/connectors.ts +164 -19
- package/src/serve/public/components/BootstrapStatus.tsx +94 -1
- package/src/serve/public/components/FirstRunWizard.tsx +13 -51
- package/src/serve/public/components/HealthCenter.tsx +8 -2
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/Connectors.tsx +216 -55
- package/src/serve/public/pages/Dashboard.tsx +1 -0
- package/src/serve/routes/api.ts +152 -8
- package/src/serve/server.ts +44 -9
- package/src/serve/status-model.ts +4 -0
- package/src/serve/status.ts +79 -35
- package/src/store/activation-receipts.ts +390 -0
- package/src/store/index.ts +8 -0
- package/src/store/migrations/012-activation-receipts.ts +38 -0
- package/src/store/migrations/013-fts-sync-marker.ts +39 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +313 -53
- package/src/store/types.ts +118 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AlertCircleIcon,
|
|
2
3
|
BotIcon,
|
|
3
4
|
CheckCircle2Icon,
|
|
4
5
|
CpuIcon,
|
|
5
6
|
Loader2Icon,
|
|
6
7
|
PlugZapIcon,
|
|
8
|
+
SearchCheckIcon,
|
|
7
9
|
} from "lucide-react";
|
|
8
10
|
import { useCallback, useEffect, useState } from "react";
|
|
9
11
|
|
|
@@ -40,12 +42,35 @@ interface ConnectorStatus {
|
|
|
40
42
|
|
|
41
43
|
interface ConnectorsResponse {
|
|
42
44
|
connectors: ConnectorStatus[];
|
|
45
|
+
collections: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface ConnectorVerificationResponse {
|
|
49
|
+
verification: {
|
|
50
|
+
collection: string;
|
|
51
|
+
lexicalReady: boolean;
|
|
52
|
+
connectorReady: boolean;
|
|
53
|
+
generatedAt: string;
|
|
54
|
+
stages: {
|
|
55
|
+
connector: {
|
|
56
|
+
status: "passed" | "pending" | "failed" | "skipped";
|
|
57
|
+
code?: string;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
remediation: string | null;
|
|
43
62
|
}
|
|
44
63
|
|
|
45
64
|
export default function Connectors({ navigate }: PageProps) {
|
|
46
65
|
const [connectors, setConnectors] = useState<ConnectorStatus[]>([]);
|
|
66
|
+
const [collections, setCollections] = useState<string[]>([]);
|
|
67
|
+
const [selectedCollection, setSelectedCollection] = useState("");
|
|
47
68
|
const [error, setError] = useState<string | null>(null);
|
|
48
69
|
const [installingId, setInstallingId] = useState<string | null>(null);
|
|
70
|
+
const [verifyingId, setVerifyingId] = useState<string | null>(null);
|
|
71
|
+
const [verificationById, setVerificationById] = useState<
|
|
72
|
+
Record<string, ConnectorVerificationResponse>
|
|
73
|
+
>({});
|
|
49
74
|
|
|
50
75
|
const loadConnectors = useCallback(async () => {
|
|
51
76
|
const { data, error: err } =
|
|
@@ -56,6 +81,11 @@ export default function Connectors({ navigate }: PageProps) {
|
|
|
56
81
|
}
|
|
57
82
|
|
|
58
83
|
setConnectors(data?.connectors ?? []);
|
|
84
|
+
const nextCollections = data?.collections ?? [];
|
|
85
|
+
setCollections(nextCollections);
|
|
86
|
+
setSelectedCollection((current) =>
|
|
87
|
+
nextCollections.includes(current) ? current : (nextCollections[0] ?? "")
|
|
88
|
+
);
|
|
59
89
|
setError(null);
|
|
60
90
|
}, []);
|
|
61
91
|
|
|
@@ -83,6 +113,37 @@ export default function Connectors({ navigate }: PageProps) {
|
|
|
83
113
|
await loadConnectors();
|
|
84
114
|
};
|
|
85
115
|
|
|
116
|
+
const handleVerify = async (connectorId: string) => {
|
|
117
|
+
if (!selectedCollection) {
|
|
118
|
+
setError("Add and index a collection before verifying retrieval.");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
setVerifyingId(connectorId);
|
|
123
|
+
setError(null);
|
|
124
|
+
const { data, error: err } = await apiFetch<ConnectorVerificationResponse>(
|
|
125
|
+
"/api/connectors/verify",
|
|
126
|
+
{
|
|
127
|
+
method: "POST",
|
|
128
|
+
body: JSON.stringify({
|
|
129
|
+
connectorId,
|
|
130
|
+
collection: selectedCollection,
|
|
131
|
+
}),
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
setVerifyingId(null);
|
|
135
|
+
|
|
136
|
+
if (err || !data) {
|
|
137
|
+
setError(err ?? "Connector verification could not be completed.");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
setVerificationById((current) => ({
|
|
142
|
+
...current,
|
|
143
|
+
[connectorId]: data,
|
|
144
|
+
}));
|
|
145
|
+
};
|
|
146
|
+
|
|
86
147
|
return (
|
|
87
148
|
<div className="min-h-screen">
|
|
88
149
|
<main className="mx-auto max-w-6xl p-8">
|
|
@@ -103,9 +164,29 @@ export default function Connectors({ navigate }: PageProps) {
|
|
|
103
164
|
opt-in instead of something the app quietly enables for you.
|
|
104
165
|
</p>
|
|
105
166
|
</div>
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
|
|
167
|
+
<div className="flex flex-col items-end gap-3">
|
|
168
|
+
<Button onClick={() => navigate("/")} variant="outline">
|
|
169
|
+
Back to Dashboard
|
|
170
|
+
</Button>
|
|
171
|
+
{collections.length > 0 && (
|
|
172
|
+
<label className="flex items-center gap-2 font-mono text-muted-foreground text-xs">
|
|
173
|
+
Proof collection
|
|
174
|
+
<select
|
|
175
|
+
className="cursor-pointer rounded border border-border/60 bg-background px-2 py-1.5 text-foreground outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
|
176
|
+
onChange={(event) =>
|
|
177
|
+
setSelectedCollection(event.currentTarget.value)
|
|
178
|
+
}
|
|
179
|
+
value={selectedCollection}
|
|
180
|
+
>
|
|
181
|
+
{collections.map((collection) => (
|
|
182
|
+
<option key={collection} value={collection}>
|
|
183
|
+
{collection}
|
|
184
|
+
</option>
|
|
185
|
+
))}
|
|
186
|
+
</select>
|
|
187
|
+
</label>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
109
190
|
</div>
|
|
110
191
|
|
|
111
192
|
{error && (
|
|
@@ -115,62 +196,142 @@ export default function Connectors({ navigate }: PageProps) {
|
|
|
115
196
|
)}
|
|
116
197
|
|
|
117
198
|
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
|
118
|
-
{connectors.map((connector) =>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
<
|
|
125
|
-
|
|
126
|
-
|
|
199
|
+
{connectors.map((connector) => {
|
|
200
|
+
const verification = verificationById[connector.id];
|
|
201
|
+
return (
|
|
202
|
+
<Card className="border-border/60 bg-card/70" key={connector.id}>
|
|
203
|
+
<CardHeader className="space-y-3 pb-3">
|
|
204
|
+
<div className="flex items-center justify-between gap-3">
|
|
205
|
+
<div className="flex items-center gap-2">
|
|
206
|
+
<BotIcon className="size-4 text-primary" />
|
|
207
|
+
<CardTitle className="text-base">
|
|
208
|
+
{connector.appName}
|
|
209
|
+
</CardTitle>
|
|
210
|
+
</div>
|
|
211
|
+
{connector.installed ? (
|
|
212
|
+
<CheckCircle2Icon className="size-4 text-green-500" />
|
|
213
|
+
) : (
|
|
214
|
+
<CpuIcon className="size-4 text-muted-foreground" />
|
|
215
|
+
)}
|
|
127
216
|
</div>
|
|
128
|
-
{connector.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
217
|
+
<CardDescription>{connector.summary}</CardDescription>
|
|
218
|
+
</CardHeader>
|
|
219
|
+
<CardContent className="space-y-4 text-sm">
|
|
220
|
+
<div className="rounded-lg border border-border/50 bg-background/70 p-3">
|
|
221
|
+
<div className="mb-1 font-medium">
|
|
222
|
+
{connector.mode.label}
|
|
223
|
+
</div>
|
|
224
|
+
<p className="text-muted-foreground">
|
|
225
|
+
{connector.mode.detail}
|
|
226
|
+
</p>
|
|
227
|
+
</div>
|
|
228
|
+
<div className="space-y-1">
|
|
229
|
+
<div>
|
|
230
|
+
<span className="font-medium">Install type:</span>{" "}
|
|
231
|
+
{connector.installKind.toUpperCase()}
|
|
232
|
+
</div>
|
|
233
|
+
<div>
|
|
234
|
+
<span className="font-medium">Scope:</span>{" "}
|
|
235
|
+
{connector.scope}
|
|
236
|
+
</div>
|
|
237
|
+
<div className="font-mono text-muted-foreground text-xs">
|
|
238
|
+
{connector.path}
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
{connector.error && (
|
|
242
|
+
<p className="text-destructive text-sm">
|
|
243
|
+
{connector.error}
|
|
244
|
+
</p>
|
|
132
245
|
)}
|
|
133
|
-
</div>
|
|
134
|
-
<CardDescription>{connector.summary}</CardDescription>
|
|
135
|
-
</CardHeader>
|
|
136
|
-
<CardContent className="space-y-4 text-sm">
|
|
137
|
-
<div className="rounded-lg border border-border/50 bg-background/70 p-3">
|
|
138
|
-
<div className="mb-1 font-medium">{connector.mode.label}</div>
|
|
139
246
|
<p className="text-muted-foreground">
|
|
140
|
-
{connector.
|
|
247
|
+
{connector.nextAction}
|
|
141
248
|
</p>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
249
|
+
{connector.installed && connector.installKind === "skill" && (
|
|
250
|
+
<div className="rounded border border-secondary/30 bg-secondary/10 p-3">
|
|
251
|
+
<div className="flex items-center gap-2 font-medium text-secondary text-xs">
|
|
252
|
+
<AlertCircleIcon className="size-3.5" />
|
|
253
|
+
Runtime verification unavailable
|
|
254
|
+
</div>
|
|
255
|
+
<p className="mt-1 text-muted-foreground text-xs">
|
|
256
|
+
target_runtime_unverifiable — this client exposes no
|
|
257
|
+
safe read-only runtime hook. The skill file is
|
|
258
|
+
installed, but GNO cannot claim the agent used it.
|
|
259
|
+
</p>
|
|
260
|
+
</div>
|
|
261
|
+
)}
|
|
262
|
+
{connector.installed && connector.installKind === "mcp" && (
|
|
263
|
+
<div className="space-y-3 border-border/50 border-t pt-4">
|
|
264
|
+
<div className="flex items-start gap-2">
|
|
265
|
+
<SearchCheckIcon className="mt-0.5 size-4 text-primary" />
|
|
266
|
+
<div>
|
|
267
|
+
<p className="font-medium text-sm">Retrieval proof</p>
|
|
268
|
+
<p className="text-muted-foreground text-xs">
|
|
269
|
+
Starts this configured MCP once and checks a real,
|
|
270
|
+
collection-scoped result. Runs only when requested.
|
|
271
|
+
</p>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
<Button
|
|
275
|
+
className="w-full"
|
|
276
|
+
disabled={!selectedCollection || verifyingId !== null}
|
|
277
|
+
onClick={() => void handleVerify(connector.id)}
|
|
278
|
+
size="sm"
|
|
279
|
+
variant="secondary"
|
|
280
|
+
>
|
|
281
|
+
{verifyingId === connector.id ? (
|
|
282
|
+
<Loader2Icon className="mr-2 size-4 animate-spin" />
|
|
283
|
+
) : (
|
|
284
|
+
<SearchCheckIcon className="mr-2 size-4" />
|
|
285
|
+
)}
|
|
286
|
+
Verify retrieval
|
|
287
|
+
</Button>
|
|
288
|
+
{collections.length === 0 && (
|
|
289
|
+
<p className="text-muted-foreground text-xs">
|
|
290
|
+
Add and index a collection before verifying retrieval.
|
|
291
|
+
</p>
|
|
292
|
+
)}
|
|
293
|
+
{verification && (
|
|
294
|
+
<div
|
|
295
|
+
aria-live="polite"
|
|
296
|
+
className={
|
|
297
|
+
verification.verification.stages.connector
|
|
298
|
+
.status === "passed"
|
|
299
|
+
? "rounded border border-primary/30 bg-primary/10 p-3 text-xs"
|
|
300
|
+
: "rounded border border-secondary/30 bg-secondary/10 p-3 text-xs"
|
|
301
|
+
}
|
|
302
|
+
>
|
|
303
|
+
<p className="font-mono text-foreground uppercase tracking-wide">
|
|
304
|
+
{verification.verification.stages.connector.status}
|
|
305
|
+
{verification.verification.stages.connector.code
|
|
306
|
+
? ` / ${verification.verification.stages.connector.code}`
|
|
307
|
+
: ""}
|
|
308
|
+
</p>
|
|
309
|
+
<p className="mt-1 text-muted-foreground">
|
|
310
|
+
Collection: {verification.verification.collection}
|
|
311
|
+
</p>
|
|
312
|
+
<p className="mt-2 text-foreground">
|
|
313
|
+
{verification.remediation ??
|
|
314
|
+
"Retrieval returned the expected indexed source. No action needed."}
|
|
315
|
+
</p>
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
318
|
+
</div>
|
|
319
|
+
)}
|
|
320
|
+
<Button
|
|
321
|
+
className="w-full"
|
|
322
|
+
onClick={() => void handleInstall(connector.id)}
|
|
323
|
+
size="sm"
|
|
324
|
+
variant={connector.installed ? "outline" : "default"}
|
|
325
|
+
>
|
|
326
|
+
{installingId === connector.id ? (
|
|
327
|
+
<Loader2Icon className="mr-2 size-4 animate-spin" />
|
|
328
|
+
) : null}
|
|
329
|
+
{connector.installed ? "Reinstall" : "Install"}
|
|
330
|
+
</Button>
|
|
331
|
+
</CardContent>
|
|
332
|
+
</Card>
|
|
333
|
+
);
|
|
334
|
+
})}
|
|
174
335
|
</div>
|
|
175
336
|
</main>
|
|
176
337
|
</div>
|
|
@@ -520,6 +520,7 @@ export default function Dashboard({ navigate }: PageProps) {
|
|
|
520
520
|
{status && (
|
|
521
521
|
<div className="mb-10">
|
|
522
522
|
<BootstrapStatus
|
|
523
|
+
activation={status.activation}
|
|
523
524
|
bootstrap={status.bootstrap}
|
|
524
525
|
onDownloadModels={() => void handleDownloadModels()}
|
|
525
526
|
/>
|
package/src/serve/routes/api.ts
CHANGED
|
@@ -23,7 +23,12 @@ import type {
|
|
|
23
23
|
SearchOptions,
|
|
24
24
|
} from "../../pipeline/types";
|
|
25
25
|
import type { SqliteAdapter } from "../../store/sqlite/adapter";
|
|
26
|
-
import type {
|
|
26
|
+
import type {
|
|
27
|
+
ActivationVerificationReceipt,
|
|
28
|
+
DocumentRow,
|
|
29
|
+
StorePort,
|
|
30
|
+
StoreResult,
|
|
31
|
+
} from "../../store/types";
|
|
27
32
|
import type { DocumentEventBus } from "../doc-events";
|
|
28
33
|
import type { EmbedScheduler } from "../embed-scheduler";
|
|
29
34
|
import type { StartJobError } from "../jobs";
|
|
@@ -47,6 +52,10 @@ import {
|
|
|
47
52
|
type PublicCaptureInput,
|
|
48
53
|
} from "../../core/capture";
|
|
49
54
|
import { writeCapturePlanFile } from "../../core/capture-write";
|
|
55
|
+
import {
|
|
56
|
+
type ConnectorVerificationCode,
|
|
57
|
+
getConnectorVerificationRemediation,
|
|
58
|
+
} from "../../core/connector-verifier";
|
|
50
59
|
import {
|
|
51
60
|
buildEditableCopyContent,
|
|
52
61
|
deriveEditableCopyRelPath,
|
|
@@ -116,7 +125,11 @@ import {
|
|
|
116
125
|
import { exportPublishArtifact } from "../../publish/export-service";
|
|
117
126
|
import { buildBrowseTree, normalizeBrowsePath } from "../browse-tree";
|
|
118
127
|
import { applyConfigChange, applyConfigChangeTyped } from "../config-sync";
|
|
119
|
-
import {
|
|
128
|
+
import {
|
|
129
|
+
getConnectorStatuses,
|
|
130
|
+
installConnector,
|
|
131
|
+
verifyInstalledConnector,
|
|
132
|
+
} from "../connectors";
|
|
120
133
|
import {
|
|
121
134
|
downloadState,
|
|
122
135
|
reloadServerContext,
|
|
@@ -153,6 +166,27 @@ export interface InstallConnectorRequestBody {
|
|
|
153
166
|
reinstall?: boolean;
|
|
154
167
|
}
|
|
155
168
|
|
|
169
|
+
export interface VerifyConnectorRequestBody {
|
|
170
|
+
connectorId: string;
|
|
171
|
+
collection: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
interface ConnectorRouteDeps {
|
|
175
|
+
getStatuses: typeof getConnectorStatuses;
|
|
176
|
+
verify: (
|
|
177
|
+
id: string,
|
|
178
|
+
store: StorePort,
|
|
179
|
+
collection: string,
|
|
180
|
+
options?: { force?: boolean },
|
|
181
|
+
overrides?: { cwd?: string; homeDir?: string }
|
|
182
|
+
) => Promise<StoreResult<ActivationVerificationReceipt>>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const DEFAULT_CONNECTOR_ROUTE_DEPS: ConnectorRouteDeps = {
|
|
186
|
+
getStatuses: getConnectorStatuses,
|
|
187
|
+
verify: verifyInstalledConnector,
|
|
188
|
+
};
|
|
189
|
+
|
|
156
190
|
export interface SearchRequestBody {
|
|
157
191
|
query: string;
|
|
158
192
|
// Only BM25 supported in web UI (vector/hybrid require LLM deps)
|
|
@@ -4272,18 +4306,120 @@ export function handleEmbedStatus(scheduler: EmbedScheduler | null): Response {
|
|
|
4272
4306
|
});
|
|
4273
4307
|
}
|
|
4274
4308
|
|
|
4275
|
-
export async function handleConnectors(
|
|
4276
|
-
|
|
4277
|
-
homeDir?: string
|
|
4278
|
-
|
|
4309
|
+
export async function handleConnectors(
|
|
4310
|
+
config: Config,
|
|
4311
|
+
overrides?: { cwd?: string; homeDir?: string },
|
|
4312
|
+
deps: ConnectorRouteDeps = DEFAULT_CONNECTOR_ROUTE_DEPS
|
|
4313
|
+
): Promise<Response> {
|
|
4279
4314
|
return jsonResponse({
|
|
4280
|
-
connectors: await
|
|
4315
|
+
connectors: await deps.getStatuses(overrides),
|
|
4316
|
+
collections: config.collections
|
|
4317
|
+
.map(({ name }) => name)
|
|
4318
|
+
.sort((left, right) => left.localeCompare(right)),
|
|
4281
4319
|
});
|
|
4282
4320
|
}
|
|
4283
4321
|
|
|
4322
|
+
/**
|
|
4323
|
+
* Explicitly run one collection-scoped connector retrieval proof.
|
|
4324
|
+
* Passive connector/status routes never call this handler.
|
|
4325
|
+
*/
|
|
4326
|
+
export async function handleVerifyConnector(
|
|
4327
|
+
config: Config,
|
|
4328
|
+
store: StorePort,
|
|
4329
|
+
req: Request,
|
|
4330
|
+
overrides?: { cwd?: string; homeDir?: string },
|
|
4331
|
+
deps: ConnectorRouteDeps = DEFAULT_CONNECTOR_ROUTE_DEPS
|
|
4332
|
+
): Promise<Response> {
|
|
4333
|
+
let body: unknown;
|
|
4334
|
+
try {
|
|
4335
|
+
body = await req.json();
|
|
4336
|
+
} catch {
|
|
4337
|
+
return errorResponse("VALIDATION", "Invalid JSON body");
|
|
4338
|
+
}
|
|
4339
|
+
|
|
4340
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
4341
|
+
return errorResponse("VALIDATION", "Request body must be an object");
|
|
4342
|
+
}
|
|
4343
|
+
const fields = Object.keys(body);
|
|
4344
|
+
const allowedFields = new Set(["connectorId", "collection"]);
|
|
4345
|
+
if (fields.some((field) => !allowedFields.has(field))) {
|
|
4346
|
+
return errorResponse("VALIDATION", "Request body has unknown fields");
|
|
4347
|
+
}
|
|
4348
|
+
|
|
4349
|
+
const { connectorId, collection } = body as Record<string, unknown>;
|
|
4350
|
+
if (
|
|
4351
|
+
typeof connectorId !== "string" ||
|
|
4352
|
+
connectorId.length === 0 ||
|
|
4353
|
+
connectorId.length > 128
|
|
4354
|
+
) {
|
|
4355
|
+
return errorResponse("VALIDATION", "Missing or invalid connectorId");
|
|
4356
|
+
}
|
|
4357
|
+
if (
|
|
4358
|
+
typeof collection !== "string" ||
|
|
4359
|
+
collection.length === 0 ||
|
|
4360
|
+
collection.length > 64
|
|
4361
|
+
) {
|
|
4362
|
+
return errorResponse("VALIDATION", "Missing or invalid collection");
|
|
4363
|
+
}
|
|
4364
|
+
if (!config.collections.some(({ name }) => name === collection)) {
|
|
4365
|
+
return errorResponse("VALIDATION", "Unknown collection");
|
|
4366
|
+
}
|
|
4367
|
+
|
|
4368
|
+
try {
|
|
4369
|
+
const statuses = await deps.getStatuses(overrides);
|
|
4370
|
+
const connector = statuses.find(({ id }) => id === connectorId);
|
|
4371
|
+
if (!connector) {
|
|
4372
|
+
return errorResponse("VALIDATION", "Unknown connectorId");
|
|
4373
|
+
}
|
|
4374
|
+
|
|
4375
|
+
const result = await deps.verify(
|
|
4376
|
+
connectorId,
|
|
4377
|
+
store,
|
|
4378
|
+
collection,
|
|
4379
|
+
{ force: true },
|
|
4380
|
+
overrides
|
|
4381
|
+
);
|
|
4382
|
+
if (!result.ok) {
|
|
4383
|
+
return errorResponse(
|
|
4384
|
+
"CONNECTOR_VERIFICATION_FAILED",
|
|
4385
|
+
"Connector verification could not be completed",
|
|
4386
|
+
500
|
|
4387
|
+
);
|
|
4388
|
+
}
|
|
4389
|
+
|
|
4390
|
+
const code = result.value.stages.connector.code;
|
|
4391
|
+
return jsonResponse({
|
|
4392
|
+
verification: {
|
|
4393
|
+
collection: result.value.collection,
|
|
4394
|
+
lexicalReady: result.value.ready,
|
|
4395
|
+
connectorReady: result.value.stages.connector.status === "passed",
|
|
4396
|
+
generatedAt: result.value.generatedAt,
|
|
4397
|
+
stages: { connector: result.value.stages.connector },
|
|
4398
|
+
},
|
|
4399
|
+
remediation: code
|
|
4400
|
+
? getConnectorVerificationRemediation(
|
|
4401
|
+
code as ConnectorVerificationCode,
|
|
4402
|
+
connector.appName
|
|
4403
|
+
)
|
|
4404
|
+
: null,
|
|
4405
|
+
});
|
|
4406
|
+
} catch {
|
|
4407
|
+
return errorResponse(
|
|
4408
|
+
"CONNECTOR_VERIFICATION_FAILED",
|
|
4409
|
+
"Connector verification could not be completed",
|
|
4410
|
+
500
|
|
4411
|
+
);
|
|
4412
|
+
}
|
|
4413
|
+
}
|
|
4414
|
+
|
|
4284
4415
|
export async function handleInstallConnector(
|
|
4285
4416
|
req: Request,
|
|
4286
|
-
overrides?: {
|
|
4417
|
+
overrides?: {
|
|
4418
|
+
cwd?: string;
|
|
4419
|
+
homeDir?: string;
|
|
4420
|
+
indexName?: string;
|
|
4421
|
+
configPath?: string;
|
|
4422
|
+
}
|
|
4287
4423
|
): Promise<Response> {
|
|
4288
4424
|
let body: InstallConnectorRequestBody;
|
|
4289
4425
|
try {
|
|
@@ -4391,6 +4527,14 @@ export async function routeApi(
|
|
|
4391
4527
|
return handleCollections(config);
|
|
4392
4528
|
}
|
|
4393
4529
|
|
|
4530
|
+
if (path === "/api/connectors" && req.method === "GET") {
|
|
4531
|
+
return handleConnectors(config);
|
|
4532
|
+
}
|
|
4533
|
+
|
|
4534
|
+
if (path === "/api/connectors/verify" && req.method === "POST") {
|
|
4535
|
+
return handleVerifyConnector(config, store, req);
|
|
4536
|
+
}
|
|
4537
|
+
|
|
4394
4538
|
if (path === "/api/publish/export" && req.method === "POST") {
|
|
4395
4539
|
return handlePublishExport(config, store, req);
|
|
4396
4540
|
}
|
package/src/serve/server.ts
CHANGED
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
handleTrashDoc,
|
|
59
59
|
handleUpdateCollection,
|
|
60
60
|
handleUpdateDoc,
|
|
61
|
+
handleVerifyConnector,
|
|
61
62
|
} from "./routes/api";
|
|
62
63
|
import { handleGraph, handleGraphQuery } from "./routes/graph";
|
|
63
64
|
import {
|
|
@@ -81,6 +82,13 @@ export interface ServeResult {
|
|
|
81
82
|
error?: string;
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
interface StartServerDependencies {
|
|
86
|
+
startBackgroundRuntime?: typeof startBackgroundRuntime;
|
|
87
|
+
serve?: typeof Bun.serve;
|
|
88
|
+
handleInstallConnector?: typeof handleInstallConnector;
|
|
89
|
+
waitForShutdown?: (signal: AbortSignal) => Promise<void>;
|
|
90
|
+
}
|
|
91
|
+
|
|
84
92
|
// Hostname parsing helpers - preserved for future fetch handler use
|
|
85
93
|
// function parseHostname(host: string): string { ... }
|
|
86
94
|
// function isLoopback(hostname: string): boolean { ... }
|
|
@@ -135,11 +143,14 @@ function withSecurityHeaders(response: Response, isDev: boolean): Response {
|
|
|
135
143
|
* Opens DB once, closes on SIGINT/SIGTERM.
|
|
136
144
|
*/
|
|
137
145
|
export async function startServer(
|
|
138
|
-
options: ServeOptions = {}
|
|
146
|
+
options: ServeOptions = {},
|
|
147
|
+
dependencies: StartServerDependencies = {}
|
|
139
148
|
): Promise<ServeResult> {
|
|
140
149
|
const port = options.port ?? 3000;
|
|
141
150
|
const isDev = process.env.NODE_ENV !== "production";
|
|
142
|
-
const runtimeResult = await
|
|
151
|
+
const runtimeResult = await (
|
|
152
|
+
dependencies.startBackgroundRuntime ?? startBackgroundRuntime
|
|
153
|
+
)({
|
|
143
154
|
configPath: options.configPath,
|
|
144
155
|
index: options.index,
|
|
145
156
|
requireCollections: false,
|
|
@@ -171,7 +182,7 @@ export async function startServer(
|
|
|
171
182
|
// Start server with try/catch for port-in-use etc.
|
|
172
183
|
let server: ReturnType<typeof Bun.serve>;
|
|
173
184
|
try {
|
|
174
|
-
server = Bun.serve({
|
|
185
|
+
server = (dependencies.serve ?? Bun.serve)({
|
|
175
186
|
port,
|
|
176
187
|
hostname: "127.0.0.1", // Loopback only - no LAN exposure
|
|
177
188
|
|
|
@@ -216,7 +227,11 @@ export async function startServer(
|
|
|
216
227
|
},
|
|
217
228
|
},
|
|
218
229
|
"/api/connectors": {
|
|
219
|
-
GET: async () =>
|
|
230
|
+
GET: async () =>
|
|
231
|
+
withSecurityHeaders(
|
|
232
|
+
await handleConnectors(ctxHolder.config),
|
|
233
|
+
isDev
|
|
234
|
+
),
|
|
220
235
|
},
|
|
221
236
|
"/api/connectors/install": {
|
|
222
237
|
POST: async (req: Request) => {
|
|
@@ -224,7 +239,23 @@ export async function startServer(
|
|
|
224
239
|
return withSecurityHeaders(forbiddenResponse(), isDev);
|
|
225
240
|
}
|
|
226
241
|
return withSecurityHeaders(
|
|
227
|
-
await
|
|
242
|
+
await (
|
|
243
|
+
dependencies.handleInstallConnector ?? handleInstallConnector
|
|
244
|
+
)(req, {
|
|
245
|
+
indexName: options.index,
|
|
246
|
+
configPath: runtime.actualConfigPath,
|
|
247
|
+
}),
|
|
248
|
+
isDev
|
|
249
|
+
);
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
"/api/connectors/verify": {
|
|
253
|
+
POST: async (req: Request) => {
|
|
254
|
+
if (!isRequestAllowed(req, port)) {
|
|
255
|
+
return withSecurityHeaders(forbiddenResponse(), isDev);
|
|
256
|
+
}
|
|
257
|
+
return withSecurityHeaders(
|
|
258
|
+
await handleVerifyConnector(ctxHolder.config, store, req),
|
|
228
259
|
isDev
|
|
229
260
|
);
|
|
230
261
|
},
|
|
@@ -695,11 +726,15 @@ export async function startServer(
|
|
|
695
726
|
console.log("Press Ctrl+C to stop");
|
|
696
727
|
|
|
697
728
|
// Block until shutdown signal
|
|
698
|
-
|
|
699
|
-
shutdownController.signal
|
|
700
|
-
|
|
729
|
+
if (dependencies.waitForShutdown) {
|
|
730
|
+
await dependencies.waitForShutdown(shutdownController.signal);
|
|
731
|
+
} else {
|
|
732
|
+
await new Promise<void>((resolve) => {
|
|
733
|
+
shutdownController.signal.addEventListener("abort", () => resolve(), {
|
|
734
|
+
once: true,
|
|
735
|
+
});
|
|
701
736
|
});
|
|
702
|
-
}
|
|
737
|
+
}
|
|
703
738
|
|
|
704
739
|
removeShutdownHandlers();
|
|
705
740
|
try {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ActivationStatus } from "../core/activation-status";
|
|
2
|
+
|
|
1
3
|
export type HealthCheckStatus = "ok" | "warn" | "error";
|
|
2
4
|
|
|
3
5
|
export type HealthActionKind =
|
|
@@ -29,6 +31,7 @@ export interface OnboardingStep {
|
|
|
29
31
|
title: string;
|
|
30
32
|
status: OnboardingStepStatus;
|
|
31
33
|
detail: string;
|
|
34
|
+
action?: "add-collection" | "download-models" | "sync" | "embed";
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
export interface OnboardingState {
|
|
@@ -142,6 +145,7 @@ export interface AppStatusResponse {
|
|
|
142
145
|
hybrid: boolean;
|
|
143
146
|
answer: boolean;
|
|
144
147
|
};
|
|
148
|
+
activation: ActivationStatus;
|
|
145
149
|
onboarding: OnboardingState;
|
|
146
150
|
health: HealthCenterState;
|
|
147
151
|
background: BackgroundServiceState;
|