@k2b/cloud 0.29.1 → 0.31.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/package.json +4 -2
- package/src/_internal/app-presentation.ts +22 -1
- package/src/_internal/app-roles.ts +14 -0
- package/src/_internal/capabilities.ts +113 -5
- package/src/_internal/capability-sentences.ts +413 -0
- package/src/_internal/define-app.ts +24 -5
- package/src/_internal/registry-validation.ts +5 -0
- package/src/_internal/registry.ts +50 -6
- package/src/_internal/widget-declarations.ts +53 -0
- package/src/access/PermissionEditor.tsx +43 -24
- package/src/ai/app-skill-store.ts +298 -0
- package/src/ai/app-skills.ts +105 -0
- package/src/ai/approval-routes.ts +40 -4
- package/src/ai/approvals.ts +68 -10
- package/src/ai/browser.ts +10 -0
- package/src/ai/capabilities.ts +68 -9
- package/src/ai/chart-block.ts +380 -0
- package/src/ai/chat/action-sentences.ts +49 -0
- package/src/ai/chat/blocks.tsx +97 -40
- package/src/ai/chat/chart-block.tsx +79 -0
- package/src/ai/chat/live-turn.browser-harness.tsx +4 -1
- package/src/ai/chat/message-actions.tsx +3 -1
- package/src/ai/chat/message-utils.ts +3 -0
- package/src/ai/chat/messages.ts +26 -2
- package/src/ai/chat/tool-groups.ts +66 -12
- package/src/ai/chat/tool-names.ts +6 -2
- package/src/ai/chat/turn-layout.ts +10 -2
- package/src/ai/chat/turn-view.tsx +123 -36
- package/src/ai/chat/web-tools.tsx +95 -1
- package/src/ai/client/controller.ts +6 -3
- package/src/ai/code-runtime-tools.ts +109 -61
- package/src/ai/code-source-contracts.ts +25 -21
- package/src/ai/code-source-tools.ts +3 -1
- package/src/ai/default-tool-contracts.ts +198 -0
- package/src/ai/default-tools.ts +25 -1
- package/src/ai/executor.ts +189 -35
- package/src/ai/fetch-file-tool.ts +41 -4
- package/src/ai/firecrawl-tools.ts +87 -4
- package/src/ai/index.ts +17 -1
- package/src/ai/migrate.ts +172 -14
- package/src/ai/protocol.ts +22 -1
- package/src/ai/routes.ts +11 -0
- package/src/ai/runtime-tools.ts +1 -0
- package/src/ai/runtime.ts +46 -7
- package/src/ai/skill-content.ts +54 -0
- package/src/ai/skills-routes.ts +22 -13
- package/src/ai/skills.ts +177 -231
- package/src/ai/store.ts +66 -1
- package/src/ai/tool-audit.ts +11 -1
- package/src/ai/tool-sentences.ts +38 -0
- package/src/ai/tools.ts +91 -3
- package/src/ai/types.ts +47 -1
- package/src/ai/web-provenance.ts +103 -0
- package/src/api/admin-ai-skills.ts +35 -16
- package/src/api/capabilities.ts +3 -2
- package/src/api/fanout.ts +128 -0
- package/src/api/help.ts +8 -3
- package/src/api/mcp.ts +4 -1
- package/src/api/search.ts +31 -106
- package/src/api/widgets.ts +291 -101
- package/src/browser/CloudResourceSearch.browser-harness.tsx +1 -1
- package/src/browser/CloudResourceSearch.tsx +1 -1
- package/src/browser/command-shortcuts.ts +21 -6
- package/src/browser/ndjson.ts +34 -0
- package/src/browser/reload.ts +46 -0
- package/src/browser/search-stream.ts +4 -26
- package/src/browser/spotlight-position.ts +4 -1
- package/src/browser/widgets.ts +30 -0
- package/src/capabilities/executions.ts +3 -0
- package/src/capabilities/server.ts +20 -2
- package/src/cli/access.ts +48 -14
- package/src/cli/admin/ai-skills.ts +59 -39
- package/src/cli/commands.ts +20 -0
- package/src/cli/index.ts +16 -0
- package/src/cli/plugin.ts +30 -0
- package/src/contracts/announcement-cookie.ts +62 -0
- package/src/contracts/announcements.ts +14 -52
- package/src/contracts/app.ts +34 -2
- package/src/contracts/capabilities.ts +24 -0
- package/src/contracts/pwa-paths.ts +7 -2
- package/src/contracts/pwa.ts +2 -7
- package/src/contracts/registry.ts +10 -1
- package/src/contracts/shared.ts +5 -0
- package/src/contracts/widgets.ts +67 -2
- package/src/desktop/solid.tsx +6 -7
- package/src/index.ts +1 -0
- package/src/server/help.ts +8 -1
- package/src/server/index.ts +1 -0
- package/src/server/widget-request.ts +27 -0
- package/src/services/accounts/users.ts +15 -0
- package/src/services/approvals/index.ts +225 -0
- package/src/services/approvals/migrate.ts +33 -0
- package/src/services/help/index.ts +18 -5
- package/src/services/help/types.ts +3 -2
- package/src/services/identity/constants.ts +2 -0
- package/src/services/identity/key-ring.ts +123 -42
- package/src/services/identity/runtime-config.ts +13 -2
- package/src/shared/app-presentation.ts +11 -0
- package/src/shared/email-html.ts +43 -8
- package/src/ssr/AdminLayout.tsx +4 -7
- package/src/ssr/AppLaunchpad.island.tsx +1 -1
- package/src/ssr/AppLaunchpadPanel.tsx +12 -3
- package/src/ssr/GlobalAnnouncements.island.tsx +2 -2
- package/src/ssr/GlobalSearchDialog.tsx +1 -43
- package/src/ssr/GlobalSearchTrigger.island.tsx +12 -3
- package/src/ssr/Layout.tsx +38 -14
- package/src/ssr/LayoutFooter.tsx +1 -1
- package/src/ssr/LayoutHeader.tsx +17 -20
- package/src/ssr/LayoutHelp.tsx +1 -1
- package/src/ssr/LayoutHelpTrigger.island.tsx +1 -1
- package/src/ssr/LayoutRail.tsx +1 -2
- package/src/ssr/PageTrail.tsx +23 -0
- package/src/ssr/PageTrailRow.island.tsx +20 -0
- package/src/ssr/PageTrailRuntime.island.tsx +26 -0
- package/src/ssr/ProfilePreferences.island.tsx +7 -3
- package/src/ssr/PwaRuntime.island.tsx +1 -1
- package/src/ssr/app-navigation.ts +5 -7
- package/src/ssr/global-search-host.tsx +55 -0
- package/src/ssr/help.ts +3 -3
- package/src/ssr/islands/index.ts +1 -0
- package/src/ssr/layout-runtime.ts +60 -0
- package/src/ssr/mobile-menu-history.ts +1 -1
- package/src/ssr/page-trail-runtime.ts +313 -0
- package/src/ssr/page-trail.browser-harness.tsx +216 -0
- package/src/ssr/page-trail.ts +59 -0
- package/src/ssr/platform-messages.ts +4 -0
- package/src/ssr/rail-context.ts +4 -3
- package/src/ssr/runtime.ts +14 -1
- package/src/styles/effects.css +21 -4
- package/src/styles/global.css +35 -2
- package/src/styles/utilities-layout.css +24 -1
- package/src/styles/utilities-navigation.css +6 -18
- package/src/ai/code-mode-skill.ts +0 -114
- package/src/ai/data-analysis-skill.ts +0 -10
- package/src/ai/grids-skill.ts +0 -158
- package/src/ai/skill-seeds.ts +0 -560
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Hono, type MiddlewareHandler } from "hono";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
3
|
+
import { aiAppSkills } from "../ai/app-skill-store";
|
|
4
4
|
import { AiSkillInputError, AiSkillLastAdminError, AiSkillRevisionConflictError, aiSkills } from "../ai/skills";
|
|
5
5
|
import { AuthenticatedPrincipalSchema } from "../contracts/shared";
|
|
6
6
|
import { type AuthContext, auth, err, fail, ok, respond, v } from "../server";
|
|
@@ -18,34 +18,52 @@ const SkillAccessUpdateSchema = z.object({ permission: z.enum(["read", "write",
|
|
|
18
18
|
const notFound = (c: Parameters<typeof respond>[0], noun: string) => respond(c, fail(err.notFound(noun)));
|
|
19
19
|
const conflict = (c: Parameters<typeof respond>[0], error: Error) => respond(c, { ok: false, error: error.message, status: 409 as const });
|
|
20
20
|
|
|
21
|
+
const ConfirmedSchema = z.object({ confirmed: z.literal(true) }).strict();
|
|
22
|
+
|
|
23
|
+
const installAppSkill = async (c: Parameters<typeof respond>[0], install: () => Promise<boolean>) => {
|
|
24
|
+
try {
|
|
25
|
+
return (await install()) ? respond(c, ok({ installed: true })) : notFound(c, "App Skill");
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error instanceof AiSkillRevisionConflictError) return conflict(c, error);
|
|
28
|
+
if (error instanceof AiSkillInputError) return respond(c, fail(err.badInput(error.message)));
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
21
33
|
export const createAdminAiSkillsRoutes = (authenticate: MiddlewareHandler<AuthContext> = auth.requireRole("admin")) =>
|
|
22
34
|
new Hono<AuthContext>()
|
|
23
35
|
.use("*", authenticate)
|
|
24
36
|
.get("/", v("query", ListSkillsSchema), async (c) => {
|
|
25
37
|
const query = c.req.valid("query");
|
|
26
|
-
const [skills, summary] = await Promise.all([
|
|
38
|
+
const [skills, summary, appSkillIssues] = await Promise.all([
|
|
39
|
+
aiSkills.admin.list(query),
|
|
40
|
+
aiSkills.admin.summary({ search: query.search }),
|
|
41
|
+
aiAppSkills.appSkillIssues(),
|
|
42
|
+
]);
|
|
27
43
|
return respond(
|
|
28
44
|
c,
|
|
29
45
|
ok({
|
|
30
46
|
...skills,
|
|
31
47
|
items: skills.items.map((skill) => ({ ...skill, id: skill.shortId })),
|
|
32
48
|
summary,
|
|
49
|
+
appSkillIssues,
|
|
33
50
|
}),
|
|
34
51
|
);
|
|
35
52
|
})
|
|
36
|
-
.get("/
|
|
37
|
-
|
|
38
|
-
|
|
53
|
+
.get("/:skillId/app-version", async (c) => {
|
|
54
|
+
const skill = await aiSkills.admin.getByShortId(c.req.param("skillId")!);
|
|
55
|
+
if (!skill) return notFound(c, "Skill");
|
|
56
|
+
const version = await aiAppSkills.appVersion(skill.id);
|
|
57
|
+
return version ? respond(c, ok(version)) : notFound(c, "Skill");
|
|
58
|
+
})
|
|
39
59
|
.post(
|
|
40
|
-
"/:skillId/
|
|
60
|
+
"/:skillId/reset",
|
|
41
61
|
v(
|
|
42
62
|
"json",
|
|
43
63
|
z
|
|
44
64
|
.object({
|
|
45
|
-
templateId: z.string().min(1),
|
|
46
|
-
templateVersion: z.number().int().positive(),
|
|
47
65
|
expectedRevision: z.number().int().positive(),
|
|
48
|
-
|
|
66
|
+
expectedAppVersion: z.string().regex(/^[a-f0-9]{64}$/),
|
|
49
67
|
confirmed: z.literal(true),
|
|
50
68
|
})
|
|
51
69
|
.strict(),
|
|
@@ -54,22 +72,23 @@ export const createAdminAiSkillsRoutes = (authenticate: MiddlewareHandler<AuthCo
|
|
|
54
72
|
const skill = await aiSkills.admin.getByShortId(c.req.param("skillId")!);
|
|
55
73
|
if (!skill) return notFound(c, "Skill");
|
|
56
74
|
try {
|
|
57
|
-
|
|
75
|
+
const input = c.req.valid("json");
|
|
76
|
+
return (await aiAppSkills.reset(skill.id, input.expectedRevision, input.expectedAppVersion))
|
|
58
77
|
? respond(c, ok({ updated: true }))
|
|
59
78
|
: notFound(c, "Skill");
|
|
60
79
|
} catch (error) {
|
|
61
80
|
if (error instanceof AiSkillRevisionConflictError) return conflict(c, error);
|
|
62
81
|
if (error instanceof AiSkillInputError) return respond(c, fail(err.badInput(error.message)));
|
|
63
|
-
if (
|
|
64
|
-
typeof error === "object" &&
|
|
65
|
-
error !== null &&
|
|
66
|
-
(("code" in error && error.code === "23505") || ("errno" in error && error.errno === "23505"))
|
|
67
|
-
)
|
|
68
|
-
return conflict(c, new Error("A Skill with this name already exists."));
|
|
69
82
|
throw error;
|
|
70
83
|
}
|
|
71
84
|
},
|
|
72
85
|
)
|
|
86
|
+
.post("/apps/:appId/skills/:name/restore", v("json", ConfirmedSchema), (c) =>
|
|
87
|
+
installAppSkill(c, () => aiAppSkills.restore(c.req.param("appId")!, c.req.param("name")!)),
|
|
88
|
+
)
|
|
89
|
+
.post("/apps/:appId/skills/:name/adopt", v("json", ConfirmedSchema), (c) =>
|
|
90
|
+
installAppSkill(c, () => aiAppSkills.adopt(c.req.param("appId")!, c.req.param("name")!)),
|
|
91
|
+
)
|
|
73
92
|
.get("/:skillId/projects", async (c) => {
|
|
74
93
|
const skill = await aiSkills.admin.getByShortId(c.req.param("skillId")!);
|
|
75
94
|
if (!skill) return notFound(c, "Skill");
|
package/src/api/capabilities.ts
CHANGED
|
@@ -613,11 +613,12 @@ const runCapabilityDispatch = async (
|
|
|
613
613
|
: schemaValidator(`${operation.schemaHash}:result`, capabilityResultJsonSchema(operation.dataSchema));
|
|
614
614
|
const parsedResult = resultValidator?.safeParse(upstreamBody.data);
|
|
615
615
|
const parsedReview = params.review ? CapabilityActionReviewSchema.safeParse(upstreamBody.data) : null;
|
|
616
|
+
// A rememberable Action may leave out its scope for arguments that should ask every time; any other Action has none.
|
|
616
617
|
const reviewApprovalScopeIsValid =
|
|
617
618
|
!params.review ||
|
|
618
|
-
!("approval" in operation) ||
|
|
619
619
|
!parsedReview?.success ||
|
|
620
|
-
(
|
|
620
|
+
("approval" in operation && operation.approval === "rememberable") ||
|
|
621
|
+
parsedReview.data.approvalScope === undefined;
|
|
621
622
|
if (!resultValidator || !parsedResult?.success || !reviewApprovalScopeIsValid) {
|
|
622
623
|
if (actionWithoutRetrySafety) return settle(outcomeUnknown(), "uncertain");
|
|
623
624
|
const invalid = errorResponse("INVALID_APP_RESPONSE", messages.outsideResultSchema({ appId: params.appId }), 502);
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core's fan-out to many applications for one browser request, shared by Universal Search and dashboard widgets:
|
|
3
|
+
* bounded concurrency, one NDJSON line per application as soon as it finishes, and cancellation when the browser
|
|
4
|
+
* stops reading.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Media type a client sends in `Accept` and receives back for a stream of JSON lines. */
|
|
8
|
+
export const NDJSON_CONTENT_TYPE = "application/x-ndjson";
|
|
9
|
+
|
|
10
|
+
export const ndjsonStreamHeaders = {
|
|
11
|
+
"content-type": `${NDJSON_CONTENT_TYPE}; charset=utf-8`,
|
|
12
|
+
// Each line must reach the browser when it is written: no cache, no transformation, no proxy buffering.
|
|
13
|
+
"cache-control": "no-store, no-transform",
|
|
14
|
+
"x-accel-buffering": "no",
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
const encoder = new TextEncoder();
|
|
18
|
+
export const ndjsonLine = (line: unknown) => `${JSON.stringify(line)}\n`;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Starts `run` for every item with at most `concurrency` running at once, in item order. Each item's promise settles
|
|
22
|
+
* on its own, so a caller can act on fast items while slow ones still run. Aborting `signal` settles every item that
|
|
23
|
+
* has not finished as rejected and starts no further item.
|
|
24
|
+
*/
|
|
25
|
+
export const startBounded = <T, R>(
|
|
26
|
+
items: readonly T[],
|
|
27
|
+
concurrency: number,
|
|
28
|
+
run: (item: T, index: number) => Promise<R>,
|
|
29
|
+
signal?: AbortSignal,
|
|
30
|
+
) => {
|
|
31
|
+
const deferred = items.map(() => {
|
|
32
|
+
let settled = false;
|
|
33
|
+
let resolve!: (result: PromiseSettledResult<R>) => void;
|
|
34
|
+
const promise = new Promise<PromiseSettledResult<R>>((done) => {
|
|
35
|
+
resolve = (result) => {
|
|
36
|
+
if (settled) return;
|
|
37
|
+
settled = true;
|
|
38
|
+
done(result);
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
return { promise, resolve };
|
|
42
|
+
});
|
|
43
|
+
const abort = () => {
|
|
44
|
+
const reason = signal?.reason ?? new Error("Fan-out deadline exceeded");
|
|
45
|
+
for (const entry of deferred) entry.resolve({ status: "rejected", reason });
|
|
46
|
+
};
|
|
47
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
48
|
+
if (signal?.aborted) abort();
|
|
49
|
+
let next = 0;
|
|
50
|
+
void Promise.all(
|
|
51
|
+
Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
52
|
+
while (true) {
|
|
53
|
+
if (signal?.aborted) return;
|
|
54
|
+
const index = next;
|
|
55
|
+
next += 1;
|
|
56
|
+
const item = items[index];
|
|
57
|
+
if (item === undefined) return;
|
|
58
|
+
try {
|
|
59
|
+
deferred[index]?.resolve({
|
|
60
|
+
status: "fulfilled",
|
|
61
|
+
value: await run(item, index),
|
|
62
|
+
});
|
|
63
|
+
} catch (reason) {
|
|
64
|
+
deferred[index]?.resolve({ status: "rejected", reason });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}),
|
|
68
|
+
).then(() => signal?.removeEventListener("abort", abort));
|
|
69
|
+
return deferred.map((entry) => entry.promise);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** Waits for work that has already started, but no longer than `signal` allows. */
|
|
73
|
+
export const waitWithin = <T>(value: Promise<T>, signal: AbortSignal): Promise<T> =>
|
|
74
|
+
new Promise<T>((resolve, reject) => {
|
|
75
|
+
const aborted = () => reject(signal.reason);
|
|
76
|
+
// The work has already started; observe its rejection even if the client
|
|
77
|
+
// disconnected before we began waiting.
|
|
78
|
+
value.then(
|
|
79
|
+
(result) => {
|
|
80
|
+
signal.removeEventListener("abort", aborted);
|
|
81
|
+
resolve(result);
|
|
82
|
+
},
|
|
83
|
+
(error) => {
|
|
84
|
+
signal.removeEventListener("abort", aborted);
|
|
85
|
+
reject(error);
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
if (signal.aborted) return aborted();
|
|
89
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Answers with `first`, then one line per entry of `pending` in the order they finish, then `last(lines)`. A slow
|
|
94
|
+
* entry never holds back the others. When the client stops reading, `onCancel` runs so the caller can stop its work.
|
|
95
|
+
*/
|
|
96
|
+
export const ndjsonStream = <Line>(options: {
|
|
97
|
+
first: Line;
|
|
98
|
+
pending: readonly Promise<Line>[];
|
|
99
|
+
last: (lines: Line[]) => Line;
|
|
100
|
+
onCancel: () => void;
|
|
101
|
+
}): Response => {
|
|
102
|
+
let cancelled = false;
|
|
103
|
+
const body = new ReadableStream<Uint8Array>({
|
|
104
|
+
start(controller) {
|
|
105
|
+
const write = (line: Line) => {
|
|
106
|
+
if (!cancelled) controller.enqueue(encoder.encode(ndjsonLine(line)));
|
|
107
|
+
};
|
|
108
|
+
write(options.first);
|
|
109
|
+
const lines: Line[] = [];
|
|
110
|
+
void Promise.all(
|
|
111
|
+
options.pending.map((pending) =>
|
|
112
|
+
pending.then((line) => {
|
|
113
|
+
lines.push(line);
|
|
114
|
+
write(line);
|
|
115
|
+
}),
|
|
116
|
+
),
|
|
117
|
+
).then(() => {
|
|
118
|
+
write(options.last(lines));
|
|
119
|
+
if (!cancelled) controller.close();
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
cancel() {
|
|
123
|
+
cancelled = true;
|
|
124
|
+
options.onCancel();
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
return new Response(body, { headers: ndjsonStreamHeaders });
|
|
128
|
+
};
|
package/src/api/help.ts
CHANGED
|
@@ -13,21 +13,26 @@ export type HelpRouteDependencies = {
|
|
|
13
13
|
|
|
14
14
|
export const createHelpRoutes = (dependencies: HelpRouteDependencies = {}) => {
|
|
15
15
|
const reader = dependencies.help ?? createHelpReader;
|
|
16
|
+
// Help is for signed-in people; the reader then hides apps the viewer may not see, like missing ones.
|
|
17
|
+
// Scoped to Help's paths: Core mounts these routes at its API root, before MCP and the other APIs.
|
|
16
18
|
return new Hono<AuthContext>()
|
|
17
|
-
.use(dependencies.authenticate ?? auth.requireRole("
|
|
19
|
+
.use("/help/v1/*", dependencies.authenticate ?? auth.requireRole("authenticated"))
|
|
20
|
+
.use("/help/v1/*", auth.requireOAuthScope("read", "admin"))
|
|
18
21
|
.get("/help/v1/:appId/search", async (c) => {
|
|
19
22
|
const locale = getLocale(c);
|
|
20
23
|
const query = c.req.query("q")?.trim().slice(0, 200) ?? "";
|
|
21
24
|
const payload: HelpSearchPayload = {
|
|
22
25
|
locale,
|
|
23
|
-
ids: (await reader(locale).search({ query, appId: c.req.param("appId"), limit: 25 })).map(
|
|
26
|
+
ids: (await reader(locale, c.get("actor")).search({ query, appId: c.req.param("appId"), limit: 25 })).map(
|
|
27
|
+
(document) => document.documentId,
|
|
28
|
+
),
|
|
24
29
|
};
|
|
25
30
|
return c.json(payload);
|
|
26
31
|
})
|
|
27
32
|
.get("/help/v1/:appId/documents/:documentId", async (c) => {
|
|
28
33
|
const locale = getLocale(c);
|
|
29
34
|
const { t } = helpApiMessages.resolve([locale]);
|
|
30
|
-
const document = await reader(locale).read({ appId: c.req.param("appId"), documentId: c.req.param("documentId") });
|
|
35
|
+
const document = await reader(locale, c.get("actor")).read({ appId: c.req.param("appId"), documentId: c.req.param("documentId") });
|
|
31
36
|
if (!document) return c.json({ code: "HELP_NOT_FOUND", message: t.notFound }, 404);
|
|
32
37
|
const payload: HelpDocumentPayload = {
|
|
33
38
|
locale: document.locale,
|
package/src/api/mcp.ts
CHANGED
|
@@ -481,7 +481,10 @@ const createMcpServer = (
|
|
|
481
481
|
resolveLocale(request.headers, operatorDefault),
|
|
482
482
|
));
|
|
483
483
|
const helpReader = async () =>
|
|
484
|
-
(dependencies.help ?? ((locale) => createHelpReader(locale, { listApps: registry })))(
|
|
484
|
+
(dependencies.help ?? ((locale, viewer) => createHelpReader(locale, viewer, { listApps: registry })))(
|
|
485
|
+
await resolveHelpLocale(),
|
|
486
|
+
authority?.actor,
|
|
487
|
+
);
|
|
485
488
|
const hasScope = (scope: "read" | "write"): boolean =>
|
|
486
489
|
oauthScopes === null || oauthScopes.includes(scope) || oauthScopes.includes("admin");
|
|
487
490
|
const requireScope = (scope: "read" | "write"): void => {
|
package/src/api/search.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { searchInvocationOperation } from "../services/identity/invocation-opera
|
|
|
18
18
|
import { normalizeInvocationRequestId, signInvocationToken } from "../services/identity/invocation-token";
|
|
19
19
|
import { withActiveIdentitySigner } from "../services/identity/key-ring";
|
|
20
20
|
import { LOCALE_HEADER } from "../shared/locale";
|
|
21
|
+
import { ndjsonLine, ndjsonStream, ndjsonStreamHeaders, startBounded, waitWithin } from "./fanout";
|
|
21
22
|
import {
|
|
22
23
|
SEARCH_STREAM_CONTENT_TYPE,
|
|
23
24
|
type SearchItem,
|
|
@@ -85,53 +86,6 @@ type SearchRouteDependencies = {
|
|
|
85
86
|
withActiveSigner?: typeof withActiveIdentitySigner;
|
|
86
87
|
};
|
|
87
88
|
|
|
88
|
-
const startBounded = <T, R>(
|
|
89
|
-
items: readonly T[],
|
|
90
|
-
concurrency: number,
|
|
91
|
-
run: (item: T, index: number) => Promise<R>,
|
|
92
|
-
signal?: AbortSignal,
|
|
93
|
-
) => {
|
|
94
|
-
const deferred = items.map(() => {
|
|
95
|
-
let settled = false;
|
|
96
|
-
let resolve!: (result: PromiseSettledResult<R>) => void;
|
|
97
|
-
const promise = new Promise<PromiseSettledResult<R>>((done) => {
|
|
98
|
-
resolve = (result) => {
|
|
99
|
-
if (settled) return;
|
|
100
|
-
settled = true;
|
|
101
|
-
done(result);
|
|
102
|
-
};
|
|
103
|
-
});
|
|
104
|
-
return { promise, resolve };
|
|
105
|
-
});
|
|
106
|
-
const abort = () => {
|
|
107
|
-
const reason = signal?.reason ?? new Error("Search fan-out deadline exceeded");
|
|
108
|
-
for (const entry of deferred) entry.resolve({ status: "rejected", reason });
|
|
109
|
-
};
|
|
110
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
111
|
-
if (signal?.aborted) abort();
|
|
112
|
-
let next = 0;
|
|
113
|
-
void Promise.all(
|
|
114
|
-
Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
115
|
-
while (true) {
|
|
116
|
-
if (signal?.aborted) return;
|
|
117
|
-
const index = next;
|
|
118
|
-
next += 1;
|
|
119
|
-
const item = items[index];
|
|
120
|
-
if (item === undefined) return;
|
|
121
|
-
try {
|
|
122
|
-
deferred[index]?.resolve({
|
|
123
|
-
status: "fulfilled",
|
|
124
|
-
value: await run(item, index),
|
|
125
|
-
});
|
|
126
|
-
} catch (reason) {
|
|
127
|
-
deferred[index]?.resolve({ status: "rejected", reason });
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}),
|
|
131
|
-
).then(() => signal?.removeEventListener("abort", abort));
|
|
132
|
-
return deferred.map((entry) => entry.promise);
|
|
133
|
-
};
|
|
134
|
-
|
|
135
89
|
/** Highest app-provided priority first, then by title. */
|
|
136
90
|
const rankItems = (items: SearchItem[]) => items.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0) || a.title.localeCompare(b.title));
|
|
137
91
|
|
|
@@ -139,38 +93,8 @@ const rankItems = (items: SearchItem[]) => items.sort((a, b) => (b.priority ?? 0
|
|
|
139
93
|
const acceptsStream = (accept: string | undefined) =>
|
|
140
94
|
accept?.split(",").some((type) => type.split(";")[0]?.trim().toLowerCase() === SEARCH_STREAM_CONTENT_TYPE) ?? false;
|
|
141
95
|
|
|
142
|
-
const streamHeaders = {
|
|
143
|
-
"content-type": `${SEARCH_STREAM_CONTENT_TYPE}; charset=utf-8`,
|
|
144
|
-
// Each line must reach the browser when it is written: no cache, no transformation, no proxy buffering.
|
|
145
|
-
"cache-control": "no-store, no-transform",
|
|
146
|
-
"x-accel-buffering": "no",
|
|
147
|
-
} as const;
|
|
148
|
-
|
|
149
|
-
const encoder = new TextEncoder();
|
|
150
|
-
const lineText = (line: SearchStreamLine) => `${JSON.stringify(line)}\n`;
|
|
151
|
-
const encodeLine = (line: SearchStreamLine) => encoder.encode(lineText(line));
|
|
152
|
-
|
|
153
96
|
type AppOutcome = { appId: string; status: SearchProviderStatus; items: SearchItem[]; ms: number };
|
|
154
97
|
|
|
155
|
-
const waitWithin = <T>(value: Promise<T>, signal: AbortSignal): Promise<T> =>
|
|
156
|
-
new Promise<T>((resolve, reject) => {
|
|
157
|
-
const aborted = () => reject(signal.reason);
|
|
158
|
-
// The work has already started; observe its rejection even if the client
|
|
159
|
-
// disconnected before we began waiting.
|
|
160
|
-
value.then(
|
|
161
|
-
(result) => {
|
|
162
|
-
signal.removeEventListener("abort", aborted);
|
|
163
|
-
resolve(result);
|
|
164
|
-
},
|
|
165
|
-
(error) => {
|
|
166
|
-
signal.removeEventListener("abort", aborted);
|
|
167
|
-
reject(error);
|
|
168
|
-
},
|
|
169
|
-
);
|
|
170
|
-
if (signal.aborted) return aborted();
|
|
171
|
-
signal.addEventListener("abort", aborted, { once: true });
|
|
172
|
-
});
|
|
173
|
-
|
|
174
98
|
/**
|
|
175
99
|
* Creates the global search route.
|
|
176
100
|
* Discovers search providers from the capability registry and fetches results via HTTP,
|
|
@@ -271,10 +195,16 @@ export const createSearchRoutes = (dependencies: SearchRouteDependencies = {}) =
|
|
|
271
195
|
stream
|
|
272
196
|
? new Response(
|
|
273
197
|
[
|
|
274
|
-
|
|
275
|
-
|
|
198
|
+
ndjsonLine({
|
|
199
|
+
type: "start",
|
|
200
|
+
query: body.query,
|
|
201
|
+
apps,
|
|
202
|
+
providers: [],
|
|
203
|
+
...(body.unsupportedTags ? { unsupportedTags } : {}),
|
|
204
|
+
} satisfies SearchStreamLine),
|
|
205
|
+
ndjsonLine({ type: "done", status: "complete", count: 0 } satisfies SearchStreamLine),
|
|
276
206
|
].join(""),
|
|
277
|
-
{ headers:
|
|
207
|
+
{ headers: ndjsonStreamHeaders },
|
|
278
208
|
)
|
|
279
209
|
: c.json(body);
|
|
280
210
|
|
|
@@ -462,35 +392,30 @@ export const createSearchRoutes = (dependencies: SearchRouteDependencies = {}) =
|
|
|
462
392
|
const failure = (outcome: AppOutcome) => outcome.status === "timeout" || outcome.status === "error";
|
|
463
393
|
|
|
464
394
|
if (stream) {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
start(
|
|
468
|
-
|
|
469
|
-
|
|
395
|
+
// A slow app never holds back the others: each line is written when its app finishes.
|
|
396
|
+
return ndjsonStream<SearchStreamLine>({
|
|
397
|
+
first: { type: "start", query: query.q, apps, providers: appIds, ...(unsupportedTags.length > 0 ? { unsupportedTags } : {}) },
|
|
398
|
+
pending: outcomes.map((pending) =>
|
|
399
|
+
pending.then(
|
|
400
|
+
(outcome): SearchStreamLine => ({
|
|
401
|
+
type: "provider",
|
|
402
|
+
provider: outcome.appId,
|
|
403
|
+
status: outcome.status,
|
|
404
|
+
results: outcome.items,
|
|
405
|
+
ms: outcome.ms,
|
|
406
|
+
}),
|
|
407
|
+
),
|
|
408
|
+
),
|
|
409
|
+
last: (lines) => {
|
|
410
|
+
const providerLines = lines.filter((line) => line.type === "provider");
|
|
411
|
+
return {
|
|
412
|
+
type: "done",
|
|
413
|
+
status: providerLines.some((line) => line.status === "timeout" || line.status === "error") ? "partial" : "complete",
|
|
414
|
+
count: providerLines.reduce((count, line) => count + line.results.length, 0),
|
|
470
415
|
};
|
|
471
|
-
write({ type: "start", query: query.q, apps, providers: appIds, ...(unsupportedTags.length > 0 ? { unsupportedTags } : {}) });
|
|
472
|
-
let count = 0;
|
|
473
|
-
let partial = false;
|
|
474
|
-
// A slow app never holds back the others: each line is written when its app finishes.
|
|
475
|
-
void Promise.all(
|
|
476
|
-
outcomes.map((pending) =>
|
|
477
|
-
pending.then((outcome) => {
|
|
478
|
-
count += outcome.items.length;
|
|
479
|
-
partial ||= failure(outcome);
|
|
480
|
-
write({ type: "provider", provider: outcome.appId, status: outcome.status, results: outcome.items, ms: outcome.ms });
|
|
481
|
-
}),
|
|
482
|
-
),
|
|
483
|
-
).then(() => {
|
|
484
|
-
write({ type: "done", status: partial ? "partial" : "complete", count });
|
|
485
|
-
if (!cancelled) controller.close();
|
|
486
|
-
});
|
|
487
|
-
},
|
|
488
|
-
cancel() {
|
|
489
|
-
cancelled = true;
|
|
490
|
-
streamCancelled.abort();
|
|
491
416
|
},
|
|
417
|
+
onCancel: () => streamCancelled.abort(),
|
|
492
418
|
});
|
|
493
|
-
return new Response(body, { headers: streamHeaders });
|
|
494
419
|
}
|
|
495
420
|
|
|
496
421
|
const finished = await Promise.all(outcomes);
|