@capybara-hq/kernel 0.1.0-rc.12 → 0.1.0-rc.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/dist/admin/admin.schemas.d.ts +13 -0
  2. package/dist/agents/agent-lookup.d.ts +5 -0
  3. package/dist/agents/agent-lookup.js +15 -0
  4. package/dist/background/background.module.js +2 -2
  5. package/dist/background/launch-lock.d.ts +1 -1
  6. package/dist/background/launch-lock.js +1 -1
  7. package/dist/background/poll.job.d.ts +2 -2
  8. package/dist/background/poll.job.js +10 -10
  9. package/dist/background/stop.d.ts +1 -1
  10. package/dist/background/stop.js +3 -3
  11. package/dist/background/store.d.ts +5 -5
  12. package/dist/background/store.js +5 -5
  13. package/dist/background/tasks.service.d.ts +6 -6
  14. package/dist/background/tasks.service.js +13 -13
  15. package/dist/catalog/marketplaces.service.d.ts +1 -1
  16. package/dist/channels/channels.module.js +2 -2
  17. package/dist/channels/inbound.d.ts +8 -8
  18. package/dist/channels/inbound.js +31 -31
  19. package/dist/channels/phrases.d.ts +27 -27
  20. package/dist/channels/phrases.js +27 -27
  21. package/dist/config/docs.js +1 -1
  22. package/dist/config/schema.d.ts +2 -2
  23. package/dist/config/schema.js +21 -21
  24. package/dist/conversations/conversations.controller.d.ts +11 -11
  25. package/dist/conversations/conversations.controller.js +15 -15
  26. package/dist/conversations/conversations.module.d.ts +1 -1
  27. package/dist/conversations/conversations.module.js +7 -7
  28. package/dist/conversations/conversations.schemas.js +1 -1
  29. package/dist/conversations/conversations.service.d.ts +14 -14
  30. package/dist/conversations/conversations.service.js +19 -19
  31. package/dist/conversations/{environment-status.d.ts → device-status.d.ts} +9 -9
  32. package/dist/conversations/{environment-status.js → device-status.js} +18 -18
  33. package/dist/conversations/event-log.d.ts +1 -1
  34. package/dist/conversations/event-log.js +1 -1
  35. package/dist/conversations/events.d.ts +1 -1
  36. package/dist/conversations/events.js +2 -2
  37. package/dist/ctx/create-ctx.d.ts +10 -1
  38. package/dist/ctx/create-ctx.js +86 -25
  39. package/dist/db/kernel-schema.d.ts +26 -23
  40. package/dist/{environments → devices}/access.d.ts +19 -19
  41. package/dist/{environments → devices}/access.js +28 -28
  42. package/dist/devices/conversation-devices.d.ts +75 -0
  43. package/dist/{environments/conversation-environments.js → devices/conversation-devices.js} +71 -72
  44. package/dist/devices/devices.controller.d.ts +36 -0
  45. package/dist/{environments/environments.controller.js → devices/devices.controller.js} +37 -37
  46. package/dist/devices/devices.module.d.ts +5 -13
  47. package/dist/devices/devices.module.js +16 -29
  48. package/dist/devices/devices.schemas.d.ts +3 -0
  49. package/dist/devices/devices.schemas.js +14 -0
  50. package/dist/{environments/environments.service.d.ts → devices/devices.service.d.ts} +70 -70
  51. package/dist/{environments/environments.service.js → devices/devices.service.js} +122 -122
  52. package/dist/devices/dto.d.ts +13 -54
  53. package/dist/devices/dto.js +7 -39
  54. package/dist/devices/label.d.ts +11 -0
  55. package/dist/{environments → devices}/label.js +7 -7
  56. package/dist/{environments → devices}/legacy-cleanup.job.d.ts +3 -3
  57. package/dist/{environments → devices}/legacy-cleanup.job.js +3 -3
  58. package/dist/errors.d.ts +25 -25
  59. package/dist/errors.js +25 -25
  60. package/dist/evals/recorded-tools.d.ts +2 -2
  61. package/dist/evals/recorded-tools.js +6 -6
  62. package/dist/extensions/admin-ops.js +1 -0
  63. package/dist/extensions/admin-view.js +2 -0
  64. package/dist/extensions/effective-config.d.ts +6 -0
  65. package/dist/extensions/effective-config.js +18 -7
  66. package/dist/extensions/host.d.ts +13 -2
  67. package/dist/extensions/host.js +24 -3
  68. package/dist/extensions/instance.d.ts +12 -1
  69. package/dist/extensions/instance.js +13 -0
  70. package/dist/extensions/permission-view.d.ts +10 -1
  71. package/dist/files/previewers.d.ts +1 -1
  72. package/dist/files/previewers.js +1 -1
  73. package/dist/http/api.module.js +10 -6
  74. package/dist/http/gateway-routes.d.ts +1 -1
  75. package/dist/http/gateway-routes.js +1 -1
  76. package/dist/identity/settings.d.ts +32 -0
  77. package/dist/identity/settings.js +26 -2
  78. package/dist/infra/config/kernel-config.service.js +4 -4
  79. package/dist/kernel.module.js +3 -3
  80. package/dist/{devices → local-devices}/device-auth.controller.d.ts +6 -6
  81. package/dist/{devices → local-devices}/device-auth.controller.js +5 -5
  82. package/dist/{devices → local-devices}/device-auth.d.ts +20 -20
  83. package/dist/{devices → local-devices}/device-auth.js +51 -53
  84. package/dist/local-devices/device-auth.module.d.ts +7 -0
  85. package/dist/{devices → local-devices}/device-auth.module.js +5 -5
  86. package/dist/{devices → local-devices}/device-routes.d.ts +3 -3
  87. package/dist/{devices → local-devices}/device-routes.js +2 -2
  88. package/dist/local-devices/dto.d.ts +66 -0
  89. package/dist/local-devices/dto.js +44 -0
  90. package/dist/{devices → local-devices}/keys.d.ts +11 -11
  91. package/dist/{devices → local-devices}/keys.js +11 -11
  92. package/dist/{devices/device-gateway.d.ts → local-devices/local-device-gateway.d.ts} +14 -14
  93. package/dist/{devices/device-gateway.js → local-devices/local-device-gateway.js} +44 -44
  94. package/dist/{devices/device-provider.d.ts → local-devices/local-device-provider.d.ts} +15 -15
  95. package/dist/{devices/device-provider.js → local-devices/local-device-provider.js} +23 -23
  96. package/dist/local-devices/local-devices.module.d.ts +16 -0
  97. package/dist/local-devices/local-devices.module.js +44 -0
  98. package/dist/{devices → local-devices}/machines.d.ts +26 -26
  99. package/dist/{devices → local-devices}/machines.js +71 -71
  100. package/dist/{devices → local-devices}/relay.d.ts +15 -15
  101. package/dist/{devices → local-devices}/relay.js +55 -55
  102. package/dist/{devices → local-devices}/targets.d.ts +8 -8
  103. package/dist/{devices → local-devices}/targets.js +14 -14
  104. package/dist/orgs/offboarding.d.ts +6 -6
  105. package/dist/orgs/offboarding.js +12 -12
  106. package/dist/orgs/settings.service.d.ts +28 -0
  107. package/dist/orgs/settings.service.js +20 -1
  108. package/dist/platform/exclusions.js +38 -3
  109. package/dist/platform/operation.js +6 -2
  110. package/dist/platform/ops/admin.ops.js +8 -2
  111. package/dist/platform/ops/conversations.ops.d.ts +3 -3
  112. package/dist/platform/ops/conversations.ops.js +9 -9
  113. package/dist/platform/ops/devices.ops.d.ts +26 -0
  114. package/dist/platform/ops/devices.ops.js +311 -0
  115. package/dist/platform/ops/org-settings.ops.js +2 -2
  116. package/dist/platform/ops/orgs.ops.js +1 -0
  117. package/dist/platform/ops/ui.ops.d.ts +8 -0
  118. package/dist/platform/ops/ui.ops.js +41 -0
  119. package/dist/platform/platform-ops.module.d.ts +3 -2
  120. package/dist/platform/platform-ops.module.js +8 -5
  121. package/dist/platform/route-schemas.js +6 -3
  122. package/dist/roles/gateway-role.d.ts +3 -3
  123. package/dist/roles/gateway-role.js +4 -4
  124. package/dist/roles/gateway.module.d.ts +1 -1
  125. package/dist/roles/gateway.module.js +5 -5
  126. package/dist/roles/worker.module.js +1 -1
  127. package/dist/runtime/context/build.d.ts +1 -1
  128. package/dist/runtime/context/build.js +6 -6
  129. package/dist/runtime/context/compaction.js +1 -1
  130. package/dist/runtime/context/{environment-paths.d.ts → device-paths.d.ts} +11 -11
  131. package/dist/runtime/context/{environment-paths.js → device-paths.js} +13 -13
  132. package/dist/runtime/maintenance.d.ts +1 -1
  133. package/dist/sandbox/jobs.d.ts +13 -13
  134. package/dist/sandbox/jobs.js +20 -20
  135. package/dist/sandbox/leases.d.ts +9 -9
  136. package/dist/sandbox/leases.js +14 -14
  137. package/dist/sandbox/providers.d.ts +8 -8
  138. package/dist/sandbox/providers.js +8 -8
  139. package/dist/sandbox/sandbox.module.d.ts +1 -1
  140. package/dist/sandbox/sandbox.module.js +1 -1
  141. package/dist/sandbox/sandbox.service.d.ts +76 -76
  142. package/dist/sandbox/sandbox.service.js +230 -230
  143. package/dist/tools/builtins/builtin-tools.d.ts +2 -2
  144. package/dist/tools/builtins/builtin-tools.js +5 -5
  145. package/dist/tools/builtins/platform.js +1 -1
  146. package/dist/tools/builtins/sandbox/bash.js +6 -6
  147. package/dist/tools/builtins/sandbox/common.d.ts +25 -25
  148. package/dist/tools/builtins/sandbox/common.js +51 -51
  149. package/dist/tools/builtins/sandbox/edit.js +2 -2
  150. package/dist/tools/builtins/sandbox/index.d.ts +1 -1
  151. package/dist/tools/builtins/sandbox/index.js +1 -1
  152. package/dist/tools/builtins/sandbox/task-output.d.ts +6 -6
  153. package/dist/tools/builtins/sandbox/task-output.js +10 -10
  154. package/dist/tools/builtins/sandbox/task-stop.js +2 -2
  155. package/dist/tools/builtins/sandbox/write.js +2 -2
  156. package/dist/tools/builtins/skill-load.d.ts +1 -1
  157. package/dist/tools/builtins/skill-load.js +2 -2
  158. package/dist/tools/pipeline.js +4 -3
  159. package/dist/tools/registry.d.ts +2 -2
  160. package/dist/tools/tools.module.js +3 -3
  161. package/dist/ui/admin-ui.controller.d.ts +56 -0
  162. package/dist/ui/admin-ui.controller.js +192 -0
  163. package/dist/ui/arrangement.d.ts +74 -0
  164. package/dist/ui/arrangement.js +98 -0
  165. package/dist/ui/embed-token.d.ts +2 -2
  166. package/dist/ui/embed-token.js +30 -5
  167. package/dist/ui/registry.d.ts +29 -14
  168. package/dist/ui/registry.js +31 -10
  169. package/dist/ui/slots.controller.d.ts +11 -0
  170. package/dist/ui/slots.controller.js +25 -0
  171. package/dist/ui/ui-stream.d.ts +20 -3
  172. package/dist/ui/ui-stream.js +25 -7
  173. package/dist/ui/ui.schemas.d.ts +66 -0
  174. package/dist/ui/ui.schemas.js +65 -0
  175. package/dist/ui/views.controller.d.ts +35 -49
  176. package/dist/ui/views.controller.js +48 -70
  177. package/dist/ui/views.d.ts +84 -18
  178. package/dist/ui/views.js +201 -38
  179. package/package.json +3 -3
  180. package/src/migrations/0105_devices_rename.sql +136 -0
  181. package/dist/devices/device-auth.module.d.ts +0 -7
  182. package/dist/environments/conversation-environments.d.ts +0 -75
  183. package/dist/environments/dto.d.ts +0 -25
  184. package/dist/environments/dto.js +0 -12
  185. package/dist/environments/environments.controller.d.ts +0 -36
  186. package/dist/environments/environments.module.d.ts +0 -8
  187. package/dist/environments/environments.module.js +0 -31
  188. package/dist/environments/environments.schemas.d.ts +0 -3
  189. package/dist/environments/environments.schemas.js +0 -14
  190. package/dist/environments/label.d.ts +0 -11
  191. package/dist/platform/ops/environments.ops.d.ts +0 -26
  192. package/dist/platform/ops/environments.ops.js +0 -311
@@ -1,7 +1,7 @@
1
1
  import { rootsOf, ToolError } from "@capybara-hq/plugin-sdk";
2
2
  import { decodeTail, tailCommand } from "../../../background/session.js";
3
3
  import { clip, display, mapSandboxError, sandboxBaseTimeoutMs, unavailableWhenNoSandbox, } from "./common.js";
4
- import { onTaskInstance, ownedTask, recheckEnvironment, statusLine, taskEnv, } from "./task-output.js";
4
+ import { onTaskInstance, ownedTask, recheckDevice, statusLine, taskEnv, } from "./task-output.js";
5
5
  /** The time limit of reading a stopped task's last lines. */
6
6
  const TAIL_EXEC_TIMEOUT_MS = 10_000;
7
7
  const SPEC = {
@@ -84,7 +84,7 @@ export function taskStopTool(deps) {
84
84
  const { task_id } = input;
85
85
  const env = await taskEnv(deps, ctx);
86
86
  const task = await ownedTask(env, ctx, task_id);
87
- await recheckEnvironment(env, ctx);
87
+ await recheckDevice(env, ctx);
88
88
  if (task.status !== "running") {
89
89
  return ended(env, task, "had already finished", ctx.signal);
90
90
  }
@@ -46,10 +46,10 @@ export function sandboxWriteTool(deps) {
46
46
  const data = new TextEncoder().encode(input.content);
47
47
  checkPathShape(input.file_path, "file_path");
48
48
  let path = "";
49
- const { value, notices } = await inSandbox(deps, ctx, ({ environmentId, h, roots, p, signal }) => {
49
+ const { value, notices } = await inSandbox(deps, ctx, ({ deviceId, h, roots, p, signal }) => {
50
50
  path = checkWritable(input.file_path, roots);
51
51
  // stat, read the previous content, write: one holder at a time per file
52
- return withPathLease(deps, { environmentId }, path, signal, async (signal) => {
52
+ return withPathLease(deps, { deviceId }, path, signal, async (signal) => {
53
53
  const st = await p.stat(h, path, signal);
54
54
  if (st?.isDir)
55
55
  throw new ToolError(isDirectory(path));
@@ -8,7 +8,7 @@ import { type SandboxToolDeps } from "./sandbox/common.js";
8
8
  * Builtin tool `Skill` (M5 T14, ruling M17): resolves the name against the latest skills catalog
9
9
  * snapshot in the log to an id and version, and returns that version's SKILL.md body and file list
10
10
  * (the result names the version). A skill with other files is copied whole into the conversation's
11
- * environment at `<skills root>/<name>@<digest>/` (the instance's skills root, `/skills` by default; the provider writes as root, commands read only; the
11
+ * device at `<skills root>/<name>@<digest>/` (the instance's skills root, `/skills` by default; the provider writes as root, commands read only; the
12
12
  * digest is {@link skillDigest} of the version's file manifest, so versions of one skill coexist in
13
13
  * the shared file system). Without a sandbox provider only the body is given, with a note that
14
14
  * scripts are unavailable.
@@ -14,7 +14,7 @@ import { clip, display, inSandbox, sandboxBaseTimeoutMs, sandboxConversationOf,
14
14
  * Builtin tool `Skill` (M5 T14, ruling M17): resolves the name against the latest skills catalog
15
15
  * snapshot in the log to an id and version, and returns that version's SKILL.md body and file list
16
16
  * (the result names the version). A skill with other files is copied whole into the conversation's
17
- * environment at `<skills root>/<name>@<digest>/` (the instance's skills root, `/skills` by default; the provider writes as root, commands read only; the
17
+ * device at `<skills root>/<name>@<digest>/` (the instance's skills root, `/skills` by default; the provider writes as root, commands read only; the
18
18
  * digest is {@link skillDigest} of the version's file manifest, so versions of one skill coexist in
19
19
  * the shared file system). Without a sandbox provider only the body is given, with a note that
20
20
  * scripts are unavailable.
@@ -49,7 +49,7 @@ const SPEC = {
49
49
  description: "Load a skill from the skills list: returns the skill's instructions " +
50
50
  "(its SKILL.md) and the list of its files. Call it before starting a task that matches a " +
51
51
  "skill's description, then follow the returned instructions. If the skill has scripts or other " +
52
- "files, they are copied read-only to <name>@<version-id>/ in the skills directory of this environment; run scripts " +
52
+ "files, they are copied read-only to <name>@<version-id>/ in the skills directory of this device; run scripts " +
53
53
  "there with an interpreter (sh, python3, node). Call it again when the skills list changes. Pass args when the task gives the skill arguments (they replace " +
54
54
  "$ARGUMENTS and $1, $2… in the instructions). Do not use it for skills that are not in the list.",
55
55
  inputSchema: {
@@ -207,9 +207,10 @@ let ToolPipeline = class ToolPipeline {
207
207
  },
208
208
  effective,
209
209
  switches,
210
- inlineViews: (plugin) => new Set((host?.current(plugin)?.manifest.ui ?? [])
211
- .filter((c) => c.place === "inline" && c.aliasOf === undefined)
212
- .map((c) => c.id)),
210
+ inlineViews: (plugin) => new Set((host?.current(plugin)?.ui ?? [])
211
+ .filter((r) => r.slot.id === "conversation.tool-result.card" &&
212
+ r.contribution.aliasOf === undefined)
213
+ .map((r) => r.contribution.id)),
213
214
  onToolsChanged: (orgId, connectorId) => {
214
215
  void drift?.onToolsChanged(orgId, connectorId);
215
216
  },
@@ -14,7 +14,7 @@ export { BUILTIN_TOOL_NAMES } from "./builtin-names.js";
14
14
  export { canonicalJson } from "./canonical-json.js";
15
15
  /**
16
16
  * What a tool's `unavailable` is asked with: the org, and for a run's listing the root
17
- * conversation (`conversationId`; the sandbox tools follow its environment, ruling R8), whether
17
+ * conversation (`conversationId`; the sandbox tools follow its device, ruling R8), whether
18
18
  * the run is an eval replay (`replay`: its tool calls are never executed), and a `memo` shared by
19
19
  * the tools of one listing (one answer per question per listing).
20
20
  */
@@ -346,7 +346,7 @@ export declare class ToolRegistry {
346
346
  forkChild?: boolean;
347
347
  /**
348
348
  * The root conversation of the run (a sub-agent's tree root): tools whose availability
349
- * depends on the conversation's environment (the sandbox tools) are listed by it.
349
+ * depends on the conversation's device (the sandbox tools) are listed by it.
350
350
  */
351
351
  sandboxConversationId?: string;
352
352
  /** An eval replay's listing: its calls are never executed (the sandbox tools are listed). */
@@ -9,7 +9,7 @@ import { BackgroundModule } from "../background/background.module.js";
9
9
  import { ConversationAccessModule } from "../conversations/access.js";
10
10
  import { AttachmentsModule } from "../conversations/attachments.module.js";
11
11
  import { EventLogModule } from "../conversations/event-log.js";
12
- import { EnvironmentsModule } from "../environments/environments.module.js";
12
+ import { DevicesModule } from "../devices/devices.module.js";
13
13
  import { ExtensionsModule } from "../extensions/extensions.module.js";
14
14
  import { QueueModule } from "../infra/queue/queue.module.js";
15
15
  import { ModelsModule } from "../models/models.module.js";
@@ -59,9 +59,9 @@ ToolsModule = __decorate([
59
59
  OrgLockModule,
60
60
  // the `Agent` tool's dispatch
61
61
  SubagentsModule,
62
- // the builtin sandbox and skill tools run in the conversation's environment
62
+ // the builtin sandbox and skill tools run in the conversation's device
63
63
  SandboxModule,
64
- EnvironmentsModule,
64
+ DevicesModule,
65
65
  // `Bash` with run_in_background records its task; TaskOutput / TaskStop finish and stop it
66
66
  BackgroundModule,
67
67
  // the conversation page's tool metadata reads only what the viewer may see
@@ -0,0 +1,56 @@
1
+ import type { UiActionResult, UiView } from "@capybara-hq/plugin-sdk";
2
+ import type { FastifyReply, FastifyRequest } from "fastify";
3
+ import type { z } from "zod";
4
+ import { ApiTokenService } from "../identity/api-tokens.js";
5
+ import { type Principal } from "../identity/principal.js";
6
+ import { SessionService } from "../identity/sessions.js";
7
+ import { EmbedTokens } from "./embed-token.js";
8
+ import { ContributionParams, PlatformActionBody, PlatformViewQuery } from "./ui.schemas.js";
9
+ import { OrgUiStreams } from "./ui-stream.js";
10
+ import { type UiArrangementSlot, type UiContributionItem, UiViews } from "./views.js";
11
+ /**
12
+ * Extension UI in the admin console (UI slots spec §7, platform scope): the `platform.page`
13
+ * contributions, for super admins only (`@SuperAdmin`, a person: service tokens are refused), with
14
+ * no org. A platform contribution needs its extension running (no org enablement applies) and is
15
+ * given the platform's deployment config; its render args carry `scope: "platform"`, `role:
16
+ * "superAdmin"` and no `orgId`.
17
+ * - `GET /api/v1/admin/ui/contributions` → `{ items }` (`platform.page` only, base order);
18
+ * - `GET /api/v1/admin/ui/arrangement` → `{ slots }` over every arrangeable slot, the platform's
19
+ * entries only, with every running extension's contributions (hidden ones marked);
20
+ * - `GET /api/v1/admin/ui/views/:plugin/:id?locale=` → `{ view }`;
21
+ * - `POST /api/v1/admin/ui/actions/:plugin/:id` (`{ actionId, payload?, answer?, locale? }`) →
22
+ * `{ toast }` | `{ view }`;
23
+ * - `POST /api/v1/admin/ui/embeds/:plugin/:id/token` → `{ token, src, expiresAt, expiresIn }`: a
24
+ * platform embed token (`scope: "platform"`, `role: "superAdmin"`, no `orgId`), which
25
+ * `ctx.ui.verifyEmbedToken` accepts only for a `platform.page` contribution while the account is
26
+ * still an active super admin;
27
+ * - `GET /api/v1/admin/ui/stream`: the platform refresh SSE (`event: ui`, topic `platform-ui`),
28
+ * super admin re-checked on the `runtime.sseRecheckMs` interval.
29
+ * A contribution in any other slot is 404 `ui.not_found`, the same as one that does not exist.
30
+ */
31
+ export declare class AdminUiController {
32
+ private readonly views;
33
+ private readonly streams;
34
+ private readonly sessions;
35
+ private readonly tokens;
36
+ private readonly embeds;
37
+ constructor(views: UiViews, streams: OrgUiStreams, sessions: SessionService, tokens: ApiTokenService, embeds: EmbedTokens);
38
+ contributions(principal: Principal): Promise<{
39
+ items: UiContributionItem[];
40
+ }>;
41
+ arrangement(principal: Principal): Promise<{
42
+ slots: UiArrangementSlot[];
43
+ }>;
44
+ view(principal: Principal, { plugin, id }: z.infer<typeof ContributionParams>, q: z.infer<typeof PlatformViewQuery>): Promise<{
45
+ view: UiView;
46
+ }>;
47
+ action(principal: Principal, { plugin, id }: z.infer<typeof ContributionParams>, body: z.infer<typeof PlatformActionBody>): Promise<UiActionResult>;
48
+ embedToken(principal: Principal, { plugin, id }: z.infer<typeof ContributionParams>): Promise<{
49
+ token: string;
50
+ src: string;
51
+ expiresAt: string;
52
+ expiresIn: number;
53
+ }>;
54
+ /** The platform refresh stream: written on the raw response (`reply.hijack()`) like the org one. */
55
+ stream(principal: Principal, req: FastifyRequest, reply: FastifyReply): Promise<void>;
56
+ }
@@ -0,0 +1,192 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
+ return function (target, key) { decorator(target, key, paramIndex); }
9
+ };
10
+ import { Controller, Get, HttpCode, Inject, Post, Req, Res } from "@nestjs/common";
11
+ import { ApiTokenService } from "../identity/api-tokens.js";
12
+ import { credentialCheck } from "../identity/credential-check.js";
13
+ import { CurrentPrincipal, requireUser, SuperAdmin, } from "../identity/principal.js";
14
+ import { SessionService } from "../identity/sessions.js";
15
+ import { createZodDto } from "../infra/validation/create-zod-dto.js";
16
+ import { ZBody, ZParam, ZQuery } from "../infra/validation/zod-params.js";
17
+ import { EMBED_TOKEN_TTL_SECONDS, EmbedTokens } from "./embed-token.js";
18
+ import { ContributionParams, PlatformActionBody, PlatformViewQuery } from "./ui.schemas.js";
19
+ import { OrgUiStreams } from "./ui-stream.js";
20
+ import { BEARER_RE, embedSrc } from "./views.controller.js";
21
+ import { UiViews } from "./views.js";
22
+ class AdminUiContributionParamsDto extends createZodDto(ContributionParams, "AdminUiContributionParams") {
23
+ }
24
+ class AdminUiViewQueryDto extends createZodDto(PlatformViewQuery, "AdminUiViewQuery") {
25
+ }
26
+ class AdminUiActionBodyDto extends createZodDto(PlatformActionBody, "AdminUiActionBody") {
27
+ }
28
+ /**
29
+ * Extension UI in the admin console (UI slots spec §7, platform scope): the `platform.page`
30
+ * contributions, for super admins only (`@SuperAdmin`, a person: service tokens are refused), with
31
+ * no org. A platform contribution needs its extension running (no org enablement applies) and is
32
+ * given the platform's deployment config; its render args carry `scope: "platform"`, `role:
33
+ * "superAdmin"` and no `orgId`.
34
+ * - `GET /api/v1/admin/ui/contributions` → `{ items }` (`platform.page` only, base order);
35
+ * - `GET /api/v1/admin/ui/arrangement` → `{ slots }` over every arrangeable slot, the platform's
36
+ * entries only, with every running extension's contributions (hidden ones marked);
37
+ * - `GET /api/v1/admin/ui/views/:plugin/:id?locale=` → `{ view }`;
38
+ * - `POST /api/v1/admin/ui/actions/:plugin/:id` (`{ actionId, payload?, answer?, locale? }`) →
39
+ * `{ toast }` | `{ view }`;
40
+ * - `POST /api/v1/admin/ui/embeds/:plugin/:id/token` → `{ token, src, expiresAt, expiresIn }`: a
41
+ * platform embed token (`scope: "platform"`, `role: "superAdmin"`, no `orgId`), which
42
+ * `ctx.ui.verifyEmbedToken` accepts only for a `platform.page` contribution while the account is
43
+ * still an active super admin;
44
+ * - `GET /api/v1/admin/ui/stream`: the platform refresh SSE (`event: ui`, topic `platform-ui`),
45
+ * super admin re-checked on the `runtime.sseRecheckMs` interval.
46
+ * A contribution in any other slot is 404 `ui.not_found`, the same as one that does not exist.
47
+ */
48
+ let AdminUiController = class AdminUiController {
49
+ views;
50
+ streams;
51
+ sessions;
52
+ tokens;
53
+ embeds;
54
+ constructor(views, streams, sessions, tokens, embeds) {
55
+ this.views = views;
56
+ this.streams = streams;
57
+ this.sessions = sessions;
58
+ this.tokens = tokens;
59
+ this.embeds = embeds;
60
+ }
61
+ async contributions(principal) {
62
+ requireUser(principal);
63
+ return { items: await this.views.platformContributions() };
64
+ }
65
+ async arrangement(principal) {
66
+ requireUser(principal);
67
+ return { slots: await this.views.platformArrangementView() };
68
+ }
69
+ async view(principal, { plugin, id }, q) {
70
+ const p = requireUser(principal);
71
+ const view = await this.views.render({
72
+ scope: "platform",
73
+ accountId: p.account.id,
74
+ plugin,
75
+ id,
76
+ locale: q.locale,
77
+ });
78
+ return { view };
79
+ }
80
+ async action(principal, { plugin, id }, body) {
81
+ const p = requireUser(principal);
82
+ return this.views.act({ scope: "platform", accountId: p.account.id, plugin, id, locale: body.locale }, {
83
+ actionId: body.actionId,
84
+ ...(body.payload !== undefined && { payload: body.payload }),
85
+ ...(body.answer !== undefined && { answer: body.answer }),
86
+ });
87
+ }
88
+ async embedToken(principal, { plugin, id }) {
89
+ const p = requireUser(principal);
90
+ const target = await this.views.embed({
91
+ scope: "platform",
92
+ accountId: p.account.id,
93
+ plugin,
94
+ id,
95
+ });
96
+ const { token, expiresAt } = this.embeds.issue({
97
+ plugin: target.plugin,
98
+ scope: "platform",
99
+ accountId: p.account.id,
100
+ role: "superAdmin",
101
+ contribution: target.contribution,
102
+ });
103
+ return {
104
+ token,
105
+ src: embedSrc(target.plugin, target.path),
106
+ expiresAt: expiresAt.toISOString(),
107
+ expiresIn: EMBED_TOKEN_TTL_SECONDS,
108
+ };
109
+ }
110
+ /** The platform refresh stream: written on the raw response (`reply.hijack()`) like the org one. */
111
+ async stream(principal, req, reply) {
112
+ const p = requireUser(principal);
113
+ const res = reply.raw;
114
+ const handle = await this.streams.open({
115
+ scope: "platform",
116
+ viewerAccountId: p.account.id,
117
+ credential: credentialCheck(p, BEARER_RE.exec(req.headers.authorization ?? "")?.[1], {
118
+ sessions: this.sessions,
119
+ tokens: this.tokens,
120
+ }),
121
+ }, {
122
+ write: (chunk) => {
123
+ res.write(chunk);
124
+ },
125
+ backlog: () => res.writableLength,
126
+ end: () => {
127
+ res.end();
128
+ },
129
+ abort: () => {
130
+ res.destroy();
131
+ },
132
+ });
133
+ reply.hijack();
134
+ res.on("close", () => handle.close());
135
+ if (res.destroyed || !res.socket || res.socket.destroyed) {
136
+ handle.close();
137
+ res.destroy();
138
+ return;
139
+ }
140
+ res.writeHead(200, {
141
+ "content-type": "text/event-stream; charset=utf-8",
142
+ "cache-control": "no-cache",
143
+ "x-accel-buffering": "no",
144
+ connection: "keep-alive",
145
+ });
146
+ res.flushHeaders();
147
+ handle.begin();
148
+ }
149
+ };
150
+ __decorate([
151
+ Get("contributions"),
152
+ __param(0, CurrentPrincipal())
153
+ ], AdminUiController.prototype, "contributions", null);
154
+ __decorate([
155
+ Get("arrangement"),
156
+ __param(0, CurrentPrincipal())
157
+ ], AdminUiController.prototype, "arrangement", null);
158
+ __decorate([
159
+ Get("views/:plugin/:id"),
160
+ __param(0, CurrentPrincipal()),
161
+ __param(1, ZParam(AdminUiContributionParamsDto)),
162
+ __param(2, ZQuery(AdminUiViewQueryDto))
163
+ ], AdminUiController.prototype, "view", null);
164
+ __decorate([
165
+ Post("actions/:plugin/:id"),
166
+ HttpCode(200),
167
+ __param(0, CurrentPrincipal()),
168
+ __param(1, ZParam(AdminUiContributionParamsDto)),
169
+ __param(2, ZBody(AdminUiActionBodyDto))
170
+ ], AdminUiController.prototype, "action", null);
171
+ __decorate([
172
+ Post("embeds/:plugin/:id/token"),
173
+ HttpCode(200),
174
+ __param(0, CurrentPrincipal()),
175
+ __param(1, ZParam(AdminUiContributionParamsDto))
176
+ ], AdminUiController.prototype, "embedToken", null);
177
+ __decorate([
178
+ Get("stream"),
179
+ __param(0, CurrentPrincipal()),
180
+ __param(1, Req()),
181
+ __param(2, Res())
182
+ ], AdminUiController.prototype, "stream", null);
183
+ AdminUiController = __decorate([
184
+ Controller("api/v1/admin/ui"),
185
+ SuperAdmin(),
186
+ __param(0, Inject(UiViews)),
187
+ __param(1, Inject(OrgUiStreams)),
188
+ __param(2, Inject(SessionService)),
189
+ __param(3, Inject(ApiTokenService)),
190
+ __param(4, Inject(EmbedTokens))
191
+ ], AdminUiController);
192
+ export { AdminUiController };
@@ -0,0 +1,74 @@
1
+ import { type UiSlotId } from "@capybara-hq/plugin-sdk";
2
+ import { z } from "zod";
3
+ import type { UiContributionItem } from "./views.js";
4
+ export declare const UiArrangementEntrySchema: z.ZodObject<{
5
+ hidden: z.ZodArray<z.ZodString>;
6
+ order: z.ZodArray<z.ZodString>;
7
+ }, z.core.$strict>;
8
+ export type UiArrangementEntry = z.infer<typeof UiArrangementEntrySchema>;
9
+ export declare const UiArrangementSchema: z.ZodRecord<z.ZodEnum<{
10
+ "agent.detail.section": "agent.detail.section";
11
+ "conversation.panel": "conversation.panel";
12
+ "conversation.properties.section": "conversation.properties.section";
13
+ "conversation.tool-result.card": "conversation.tool-result.card";
14
+ "file.preview": "file.preview";
15
+ "org.page": "org.page";
16
+ "org.settings.section": "org.settings.section";
17
+ "platform.page": "platform.page";
18
+ }> & z.core.$partial, z.ZodObject<{
19
+ hidden: z.ZodArray<z.ZodString>;
20
+ order: z.ZodArray<z.ZodString>;
21
+ }, z.core.$strict>>;
22
+ export type UiArrangement = z.infer<typeof UiArrangementSchema>;
23
+ /** The slots an org arranges: every arrangeable slot but the platform's own. */
24
+ export declare const ORG_ARRANGEABLE: readonly UiSlotId[];
25
+ /** An org's patch: a slot's entry replaces the stored one, null removes it. */
26
+ export declare const OrgUiArrangementPatchSchema: z.ZodRecord<z.ZodEnum<{
27
+ "agent.detail.section": "agent.detail.section";
28
+ "conversation.panel": "conversation.panel";
29
+ "conversation.properties.section": "conversation.properties.section";
30
+ "conversation.tool-result.card": "conversation.tool-result.card";
31
+ "file.preview": "file.preview";
32
+ "org.page": "org.page";
33
+ "org.settings.section": "org.settings.section";
34
+ "platform.page": "platform.page";
35
+ }> & z.core.$partial, z.ZodNullable<z.ZodObject<{
36
+ hidden: z.ZodArray<z.ZodString>;
37
+ order: z.ZodArray<z.ZodString>;
38
+ }, z.core.$strict>>>;
39
+ /** A deployment patch: like {@link OrgUiArrangementPatchSchema} over every arrangeable slot. */
40
+ export declare const UiArrangementPatchSchema: z.ZodRecord<z.ZodEnum<{
41
+ "agent.detail.section": "agent.detail.section";
42
+ "conversation.panel": "conversation.panel";
43
+ "conversation.properties.section": "conversation.properties.section";
44
+ "conversation.tool-result.card": "conversation.tool-result.card";
45
+ "file.preview": "file.preview";
46
+ "org.page": "org.page";
47
+ "org.settings.section": "org.settings.section";
48
+ "platform.page": "platform.page";
49
+ }> & z.core.$partial, z.ZodNullable<z.ZodObject<{
50
+ hidden: z.ZodArray<z.ZodString>;
51
+ order: z.ZodArray<z.ZodString>;
52
+ }, z.core.$strict>>>;
53
+ /** Merges a per-slot patch into the stored arrangement (null removes the slot's entry). */
54
+ export declare function mergeArrangement(current: UiArrangement | undefined, patch: Partial<Record<UiSlotId, UiArrangementEntry | null>>): UiArrangement;
55
+ /** The entry that applies to a slot: the org's if it arranged the slot, else the platform's. */
56
+ export declare function effectiveEntry(org: UiArrangement | undefined, platform: UiArrangement, slot: UiSlotId): {
57
+ entry: UiArrangementEntry;
58
+ source: "org" | "platform" | "none";
59
+ };
60
+ /**
61
+ * Orders the items of one slot (hidden ones stay in place): grouped by anchor in the slot's anchor
62
+ * order, also without an entry (the lists match what the page renders per anchor); within a group
63
+ * the keys of `entry.order` come first in that order, the rest in base order. Unknown keys are
64
+ * ignored, so an order never moves an item across anchors.
65
+ */
66
+ export declare function orderItems(items: UiContributionItem[], entry: UiArrangementEntry): UiContributionItem[];
67
+ /** {@link orderItems} without the hidden keys. */
68
+ export declare function arrange(items: UiContributionItem[], entry: UiArrangementEntry): UiContributionItem[];
69
+ /**
70
+ * Applies the per-slot entries to a mixed list: each slot's items are arranged and written back
71
+ * into the positions that slot occupied (a slot without an entry: its base order, grouped by
72
+ * anchor).
73
+ */
74
+ export declare function arrangeBySlot(items: UiContributionItem[], entryOf: (slot: UiSlotId) => UiArrangementEntry): UiContributionItem[];
@@ -0,0 +1,98 @@
1
+ import { UI_ARRANGEABLE_SLOTS, UI_CONTRIBUTION_KEY_RE, uiSlot, } from "@capybara-hq/plugin-sdk";
2
+ import { z } from "zod";
3
+ /**
4
+ * Arrangement of UI slots (UI slots spec §5): per slot, which contributions are hidden and in
5
+ * which order. "Hidden" means not listed: the access gate never reads an arrangement, so a hidden
6
+ * contribution's view, action and embed routes keep working for an allowed role.
7
+ */
8
+ const Key = z.string().regex(UI_CONTRIBUTION_KEY_RE);
9
+ const LIST_MAX = 200;
10
+ export const UiArrangementEntrySchema = z
11
+ .object({ hidden: z.array(Key).max(LIST_MAX), order: z.array(Key).max(LIST_MAX) })
12
+ .strict();
13
+ export const UiArrangementSchema = z.partialRecord(z.enum(UI_ARRANGEABLE_SLOTS), UiArrangementEntrySchema);
14
+ /** The slots an org arranges: every arrangeable slot but the platform's own. */
15
+ export const ORG_ARRANGEABLE = UI_ARRANGEABLE_SLOTS.filter((s) => s !== "platform.page");
16
+ /** An org's patch: a slot's entry replaces the stored one, null removes it. */
17
+ export const OrgUiArrangementPatchSchema = z.partialRecord(z.enum(ORG_ARRANGEABLE), UiArrangementEntrySchema.nullable());
18
+ /** A deployment patch: like {@link OrgUiArrangementPatchSchema} over every arrangeable slot. */
19
+ export const UiArrangementPatchSchema = z.partialRecord(z.enum(UI_ARRANGEABLE_SLOTS), UiArrangementEntrySchema.nullable());
20
+ /** Merges a per-slot patch into the stored arrangement (null removes the slot's entry). */
21
+ export function mergeArrangement(current, patch) {
22
+ const merged = { ...current };
23
+ for (const [slot, entry] of Object.entries(patch)) {
24
+ if (entry === null)
25
+ delete merged[slot];
26
+ else if (entry !== undefined)
27
+ merged[slot] = entry;
28
+ }
29
+ return merged;
30
+ }
31
+ const NONE = { hidden: [], order: [] };
32
+ /** The entry that applies to a slot: the org's if it arranged the slot, else the platform's. */
33
+ export function effectiveEntry(org, platform, slot) {
34
+ const o = org?.[slot];
35
+ if (o !== undefined)
36
+ return { entry: o, source: "org" };
37
+ const p = platform[slot];
38
+ if (p !== undefined)
39
+ return { entry: p, source: "platform" };
40
+ return { entry: NONE, source: "none" };
41
+ }
42
+ const keyOf = (i) => `${i.plugin}/${i.id}`;
43
+ /**
44
+ * Orders the items of one slot (hidden ones stay in place): grouped by anchor in the slot's anchor
45
+ * order, also without an entry (the lists match what the page renders per anchor); within a group
46
+ * the keys of `entry.order` come first in that order, the rest in base order. Unknown keys are
47
+ * ignored, so an order never moves an item across anchors.
48
+ */
49
+ export function orderItems(items, entry) {
50
+ const anchors = items[0] === undefined ? undefined : uiSlot(items[0].slot)?.anchors;
51
+ const groupOf = (i) => anchors === undefined ? 0 : Math.max(0, anchors.indexOf(i.after ?? anchors[0] ?? ""));
52
+ const rank = new Map(entry.order.map((k, n) => [k, n]));
53
+ return items
54
+ .map((item, base) => ({ item, base }))
55
+ .sort((a, b) => {
56
+ const g = groupOf(a.item) - groupOf(b.item);
57
+ if (g !== 0)
58
+ return g;
59
+ const ra = rank.get(keyOf(a.item));
60
+ const rb = rank.get(keyOf(b.item));
61
+ if (ra !== undefined && rb !== undefined)
62
+ return ra - rb;
63
+ if (ra !== undefined)
64
+ return -1;
65
+ if (rb !== undefined)
66
+ return 1;
67
+ return a.base - b.base;
68
+ })
69
+ .map((x) => x.item);
70
+ }
71
+ /** {@link orderItems} without the hidden keys. */
72
+ export function arrange(items, entry) {
73
+ const hidden = new Set(entry.hidden);
74
+ return orderItems(items, entry).filter((i) => !hidden.has(keyOf(i)));
75
+ }
76
+ /**
77
+ * Applies the per-slot entries to a mixed list: each slot's items are arranged and written back
78
+ * into the positions that slot occupied (a slot without an entry: its base order, grouped by
79
+ * anchor).
80
+ */
81
+ export function arrangeBySlot(items, entryOf) {
82
+ const bySlot = new Map();
83
+ items.forEach((i, n) => {
84
+ const at = bySlot.get(i.slot);
85
+ if (at)
86
+ at.push(n);
87
+ else
88
+ bySlot.set(i.slot, [n]);
89
+ });
90
+ const out = [...items];
91
+ for (const [slot, at] of bySlot) {
92
+ const arranged = arrange(at.map((n) => items[n]), entryOf(slot));
93
+ at.forEach((n, j) => {
94
+ out[n] = arranged[j];
95
+ });
96
+ }
97
+ return out.filter((i) => i !== undefined);
98
+ }
@@ -1,4 +1,4 @@
1
- import type { EmbedClaims } from "@capybara-hq/plugin-sdk";
1
+ import type { EmbedClaims, OrgEmbedClaims, PlatformEmbedClaims } from "@capybara-hq/plugin-sdk";
2
2
  import type { Keyring } from "../ctx/secrets.js";
3
3
  /** 嵌入令牌的有效期(秒)。网页在过期前 60 秒续期。 */
4
4
  export declare const EMBED_TOKEN_TTL_SECONDS = 300;
@@ -22,7 +22,7 @@ export declare class EmbedTokens {
22
22
  private readonly keys;
23
23
  constructor(keyring: Keyring, now?: () => number);
24
24
  /** 签发:`exp` = 现在 + {@link EMBED_TOKEN_TTL_SECONDS}。 */
25
- issue(c: Omit<EmbedClaims, "exp">): {
25
+ issue(c: Omit<OrgEmbedClaims, "exp"> | Omit<PlatformEmbedClaims, "exp">): {
26
26
  token: string;
27
27
  claims: EmbedClaims;
28
28
  expiresAt: Date;
@@ -24,10 +24,12 @@ const B64URL = /^[A-Za-z0-9_-]+$/;
24
24
  const PLUGIN_RE = /^[a-z][a-z0-9-]{1,40}$/;
25
25
  const CLAIM_KEYS = new Set([
26
26
  "plugin",
27
+ "scope",
27
28
  "orgId",
28
29
  "accountId",
29
30
  "role",
30
31
  "conversationId",
32
+ "agentId",
31
33
  "fileId",
32
34
  "contribution",
33
35
  "exp",
@@ -44,7 +46,11 @@ function decodeStrict(s) {
44
46
  const buf = Buffer.from(s, "base64url");
45
47
  return buf.toString("base64url") === s ? buf : null;
46
48
  }
47
- /** 逐字段核对声明:只认已知字段、类型正确,返回只含已知字段的新对象。 */
49
+ /**
50
+ * 逐字段核对声明:只认已知字段、类型正确,返回只含已知字段的新对象。The scope decides the shape:
51
+ * `"platform"` exactly when `orgId` is absent and `role` is `"superAdmin"`, with no
52
+ * `conversationId` / `agentId` / `fileId`; `"org"` needs an `orgId` and one of the org roles.
53
+ */
48
54
  function parseClaims(v) {
49
55
  if (typeof v !== "object" || v === null || Array.isArray(v))
50
56
  return null;
@@ -53,22 +59,41 @@ function parseClaims(v) {
53
59
  return null;
54
60
  if (typeof c.plugin !== "string" || !PLUGIN_RE.test(c.plugin))
55
61
  return null;
56
- if (!isId(c.orgId) || !isId(c.accountId) || !isId(c.contribution))
62
+ if (!isId(c.accountId) || !isId(c.contribution))
57
63
  return null;
58
- if (!ROLES.has(c.role))
64
+ if (typeof c.exp !== "number" || !Number.isSafeInteger(c.exp))
65
+ return null;
66
+ if (c.scope === "platform") {
67
+ if (c.orgId !== undefined || c.role !== "superAdmin")
68
+ return null;
69
+ if (c.conversationId !== undefined || c.agentId !== undefined || c.fileId !== undefined) {
70
+ return null;
71
+ }
72
+ return {
73
+ plugin: c.plugin,
74
+ scope: "platform",
75
+ accountId: c.accountId,
76
+ role: "superAdmin",
77
+ contribution: c.contribution,
78
+ exp: c.exp,
79
+ };
80
+ }
81
+ if (c.scope !== "org" || !isId(c.orgId) || !ROLES.has(c.role))
59
82
  return null;
60
83
  if (c.conversationId !== undefined && !isId(c.conversationId))
61
84
  return null;
62
- if (c.fileId !== undefined && (!isId(c.fileId) || c.conversationId === undefined))
85
+ if (c.agentId !== undefined && (!isId(c.agentId) || c.conversationId !== undefined))
63
86
  return null;
64
- if (typeof c.exp !== "number" || !Number.isSafeInteger(c.exp))
87
+ if (c.fileId !== undefined && (!isId(c.fileId) || c.conversationId === undefined))
65
88
  return null;
66
89
  return {
67
90
  plugin: c.plugin,
91
+ scope: "org",
68
92
  orgId: c.orgId,
69
93
  accountId: c.accountId,
70
94
  role: c.role,
71
95
  ...(c.conversationId !== undefined && { conversationId: c.conversationId }),
96
+ ...(c.agentId !== undefined && { agentId: c.agentId }),
72
97
  ...(c.fileId !== undefined && { fileId: c.fileId }),
73
98
  contribution: c.contribution,
74
99
  exp: c.exp,