@capybara-hq/kernel 0.1.0-rc.21 → 0.1.0-rc.22

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 (242) hide show
  1. package/dist/agents/admin-agents.d.ts +16 -3
  2. package/dist/agents/admin-agents.js +65 -7
  3. package/dist/agents/agents.schemas.d.ts +24 -6
  4. package/dist/agents/agents.service.d.ts +4 -3
  5. package/dist/agents/agents.service.js +39 -26
  6. package/dist/agents/definition.d.ts +44 -6
  7. package/dist/agents/definition.js +86 -18
  8. package/dist/agents/from-template.d.ts +6 -3
  9. package/dist/agents/from-template.js +57 -44
  10. package/dist/agents/org-templates.js +97 -41
  11. package/dist/agents/templates.d.ts +6 -0
  12. package/dist/agents/templates.js +15 -1
  13. package/dist/catalog/admin-catalog.controller.d.ts +2 -4
  14. package/dist/catalog/admin-catalog.controller.js +2 -6
  15. package/dist/catalog/catalog.module.js +3 -0
  16. package/dist/catalog/claude/report.d.ts +9 -2
  17. package/dist/catalog/claude/report.js +1 -0
  18. package/dist/catalog/claude/translate.js +1 -1
  19. package/dist/catalog/component-promotion.js +1 -2
  20. package/dist/catalog/component-state.d.ts +13 -1
  21. package/dist/catalog/component-state.js +14 -5
  22. package/dist/catalog/marketplaces.service.d.ts +1 -1
  23. package/dist/catalog/marketplaces.service.js +2 -1
  24. package/dist/catalog/org-catalog.controller.d.ts +13 -3
  25. package/dist/catalog/org-catalog.controller.js +26 -4
  26. package/dist/catalog/org-catalog.schemas.d.ts +13 -0
  27. package/dist/catalog/org-catalog.schemas.js +34 -0
  28. package/dist/catalog/org-plugin-uploads.controller.js +0 -1
  29. package/dist/catalog/org-plugin-uploads.d.ts +0 -1
  30. package/dist/catalog/org-plugin-uploads.js +0 -3
  31. package/dist/catalog/org-plugin-uploads.schemas.d.ts +0 -1
  32. package/dist/catalog/org-plugin-uploads.schemas.js +0 -2
  33. package/dist/catalog/platform-components.d.ts +0 -1
  34. package/dist/catalog/platform-components.js +3 -8
  35. package/dist/catalog/plugin-import.d.ts +0 -8
  36. package/dist/catalog/plugin-import.js +4 -35
  37. package/dist/catalog/plugin-install.d.ts +2 -2
  38. package/dist/catalog/plugin-install.js +2 -2
  39. package/dist/catalog/plugin-package.d.ts +7 -0
  40. package/dist/catalog/plugin-package.js +27 -1
  41. package/dist/catalog/plugin-upgrade.d.ts +5 -6
  42. package/dist/catalog/plugin-upgrade.js +5 -23
  43. package/dist/catalog/requests.service.d.ts +66 -10
  44. package/dist/catalog/requests.service.js +196 -15
  45. package/dist/channels/actions.d.ts +2 -0
  46. package/dist/channels/actions.js +19 -4
  47. package/dist/channels/cards.d.ts +16 -2
  48. package/dist/channels/cards.js +40 -7
  49. package/dist/channels/channels.module.js +3 -0
  50. package/dist/channels/outbound.d.ts +22 -1
  51. package/dist/channels/outbound.js +116 -7
  52. package/dist/channels/phrases.d.ts +25 -0
  53. package/dist/channels/phrases.js +19 -0
  54. package/dist/channels/threads.d.ts +12 -0
  55. package/dist/channels/threads.js +34 -0
  56. package/dist/config/schema.d.ts +23 -1
  57. package/dist/config/schema.js +105 -5
  58. package/dist/conversations/conversations.service.d.ts +42 -9
  59. package/dist/conversations/conversations.service.js +57 -25
  60. package/dist/conversations/events.d.ts +106 -11
  61. package/dist/conversations/events.js +121 -18
  62. package/dist/conversations/message-context.d.ts +6 -3
  63. package/dist/conversations/message-context.js +14 -10
  64. package/dist/conversations/queue.d.ts +15 -2
  65. package/dist/conversations/queue.js +22 -3
  66. package/dist/conversations/queue.service.d.ts +12 -6
  67. package/dist/conversations/queue.service.js +36 -13
  68. package/dist/db/kernel-schema.d.ts +105 -3
  69. package/dist/devices/conversation-devices.d.ts +8 -1
  70. package/dist/devices/conversation-devices.js +8 -0
  71. package/dist/devices/devices.controller.d.ts +21 -4
  72. package/dist/devices/devices.controller.js +46 -8
  73. package/dist/devices/devices.schemas.d.ts +9 -1
  74. package/dist/devices/devices.schemas.js +11 -3
  75. package/dist/devices/devices.service.d.ts +176 -4
  76. package/dist/devices/devices.service.js +341 -6
  77. package/dist/devices/dto.d.ts +56 -3
  78. package/dist/devices/dto.js +51 -1
  79. package/dist/devices/public-suffix.d.ts +6 -0
  80. package/dist/devices/public-suffix.js +79 -0
  81. package/dist/errors.d.ts +46 -5
  82. package/dist/errors.js +46 -5
  83. package/dist/evals/reconstruct.d.ts +18 -13
  84. package/dist/evals/reconstruct.js +50 -36
  85. package/dist/evals/recorded-tools.d.ts +7 -7
  86. package/dist/evals/recorded-tools.js +4 -3
  87. package/dist/evals/trajectory.d.ts +6 -1
  88. package/dist/evals/trajectory.js +7 -0
  89. package/dist/extensions/package.js +2 -2
  90. package/dist/http/api.module.js +5 -0
  91. package/dist/inbox/inbox.module.js +2 -1
  92. package/dist/inbox/inbox.service.d.ts +19 -4
  93. package/dist/inbox/inbox.service.js +14 -3
  94. package/dist/infra/config/kernel-config.service.js +11 -0
  95. package/dist/infra/events/kernel-events.d.ts +2 -1
  96. package/dist/local-devices/device-policy.service.d.ts +65 -0
  97. package/dist/local-devices/device-policy.service.js +217 -0
  98. package/dist/local-devices/local-device-gateway.js +4 -1
  99. package/dist/local-devices/local-device-provider.d.ts +40 -3
  100. package/dist/local-devices/local-device-provider.js +109 -7
  101. package/dist/local-devices/local-devices.module.d.ts +2 -1
  102. package/dist/local-devices/local-devices.module.js +19 -3
  103. package/dist/local-devices/machines.d.ts +14 -3
  104. package/dist/local-devices/machines.js +41 -3
  105. package/dist/local-devices/network-grants.d.ts +104 -0
  106. package/dist/local-devices/network-grants.js +288 -0
  107. package/dist/local-devices/policy-summary.d.ts +20 -0
  108. package/dist/local-devices/policy-summary.js +63 -0
  109. package/dist/orgs/orgs.module.js +4 -10
  110. package/dist/orgs/roles.d.ts +1 -1
  111. package/dist/orgs/roles.js +4 -0
  112. package/dist/orgs/settings.controller.js +2 -1
  113. package/dist/orgs/settings.service.d.ts +164 -12
  114. package/dist/orgs/settings.service.js +212 -24
  115. package/dist/platform/exclusions.js +11 -1
  116. package/dist/platform/operation.js +4 -0
  117. package/dist/platform/ops/admin-items.ops.js +1 -1
  118. package/dist/platform/ops/agent-skills.ops.js +2 -2
  119. package/dist/platform/ops/agent-templates.ops.js +2 -2
  120. package/dist/platform/ops/agents.ops.js +11 -15
  121. package/dist/platform/ops/devices.ops.js +3 -1
  122. package/dist/platform/ops/inbox.ops.js +19 -0
  123. package/dist/platform/ops/org-settings.ops.js +14 -5
  124. package/dist/platform/ops/platform.ops.d.ts +9 -1
  125. package/dist/platform/ops/platform.ops.js +34 -12
  126. package/dist/platform/ops/requests.ops.js +3 -2
  127. package/dist/platform/ops/schedules.ops.d.ts +21 -0
  128. package/dist/platform/ops/schedules.ops.js +470 -0
  129. package/dist/platform/ops/skills.ops.js +3 -3
  130. package/dist/platform/platform-ops.module.d.ts +2 -1
  131. package/dist/platform/platform-ops.module.js +5 -0
  132. package/dist/platform/route-schemas.js +3 -0
  133. package/dist/roles/worker.module.js +3 -0
  134. package/dist/runtime/context/build.js +13 -4
  135. package/dist/runtime/context/compaction.js +6 -3
  136. package/dist/runtime/context/device-paths.d.ts +43 -21
  137. package/dist/runtime/context/device-paths.js +104 -21
  138. package/dist/runtime/context/run-context.d.ts +9 -0
  139. package/dist/runtime/context/run-context.js +8 -0
  140. package/dist/runtime/context/skills.d.ts +14 -1
  141. package/dist/runtime/context/skills.js +36 -5
  142. package/dist/runtime/maintenance.d.ts +4 -2
  143. package/dist/runtime/maintenance.js +16 -5
  144. package/dist/runtime/run-finish.d.ts +6 -1
  145. package/dist/runtime/run-finish.js +16 -2
  146. package/dist/runtime/run-queue.js +21 -2
  147. package/dist/runtime/run-worker.js +14 -33
  148. package/dist/runtime/runtime.module.js +3 -0
  149. package/dist/runtime/subagents/definition.d.ts +32 -14
  150. package/dist/runtime/subagents/definition.js +106 -109
  151. package/dist/runtime/subagents/dispatch.js +4 -3
  152. package/dist/runtime/subagents/preamble.d.ts +2 -1
  153. package/dist/runtime/subagents/preamble.js +2 -1
  154. package/dist/runtime/waits.js +14 -4
  155. package/dist/sandbox/sandbox.service.d.ts +120 -10
  156. package/dist/sandbox/sandbox.service.js +335 -28
  157. package/dist/sandbox/tiers.d.ts +86 -0
  158. package/dist/sandbox/tiers.js +96 -0
  159. package/dist/schedules/effective-settings.d.ts +16 -0
  160. package/dist/schedules/effective-settings.js +17 -0
  161. package/dist/schedules/errors.d.ts +8 -0
  162. package/dist/schedules/errors.js +8 -0
  163. package/dist/schedules/fire.d.ts +118 -0
  164. package/dist/schedules/fire.js +625 -0
  165. package/dist/schedules/grant-rules.d.ts +94 -0
  166. package/dist/schedules/grant-rules.js +126 -0
  167. package/dist/schedules/job-ids.d.ts +32 -0
  168. package/dist/schedules/job-ids.js +27 -0
  169. package/dist/schedules/pause.d.ts +9 -0
  170. package/dist/schedules/pause.js +33 -0
  171. package/dist/schedules/run-tracker.d.ts +81 -0
  172. package/dist/schedules/run-tracker.js +333 -0
  173. package/dist/schedules/scan.job.d.ts +64 -0
  174. package/dist/schedules/scan.job.js +303 -0
  175. package/dist/schedules/schedule-math.d.ts +31 -0
  176. package/dist/schedules/schedule-math.js +223 -0
  177. package/dist/schedules/scheduled-tasks.service.d.ts +351 -0
  178. package/dist/schedules/scheduled-tasks.service.js +1193 -0
  179. package/dist/schedules/schedules.controller.d.ts +68 -0
  180. package/dist/schedules/schedules.controller.js +242 -0
  181. package/dist/schedules/schedules.module.d.ts +16 -0
  182. package/dist/schedules/schedules.module.js +65 -0
  183. package/dist/schedules/schedules.schemas.d.ts +316 -0
  184. package/dist/schedules/schedules.schemas.js +186 -0
  185. package/dist/schedules/store.d.ts +81 -0
  186. package/dist/schedules/store.js +390 -0
  187. package/dist/schedules/store.module.d.ts +6 -0
  188. package/dist/schedules/store.module.js +21 -0
  189. package/dist/schedules/types.d.ts +108 -0
  190. package/dist/schedules/types.js +1 -0
  191. package/dist/schedules/unattended.d.ts +117 -0
  192. package/dist/schedules/unattended.js +228 -0
  193. package/dist/setup/oauth-token-probe.js +8 -4
  194. package/dist/skills/agent-skills.service.d.ts +4 -8
  195. package/dist/skills/agent-skills.service.js +18 -53
  196. package/dist/skills/catalog.d.ts +65 -62
  197. package/dist/skills/catalog.js +109 -206
  198. package/dist/skills/conversation-catalog.d.ts +3 -3
  199. package/dist/skills/conversation-catalog.js +5 -12
  200. package/dist/skills/invoke.d.ts +2 -2
  201. package/dist/skills/skills.controller.d.ts +3 -1
  202. package/dist/skills/skills.service.d.ts +38 -37
  203. package/dist/skills/skills.service.js +168 -107
  204. package/dist/skills/usable.d.ts +57 -8
  205. package/dist/skills/usable.js +172 -38
  206. package/dist/tools/approvals.controller.d.ts +4 -2
  207. package/dist/tools/approvals.controller.js +17 -2
  208. package/dist/tools/approvals.schemas.d.ts +1 -0
  209. package/dist/tools/approvals.schemas.js +3 -1
  210. package/dist/tools/approvals.service.d.ts +182 -7
  211. package/dist/tools/approvals.service.js +597 -13
  212. package/dist/tools/builtin-names.d.ts +25 -4
  213. package/dist/tools/builtin-names.js +45 -4
  214. package/dist/tools/builtins/builtin-tools.d.ts +8 -0
  215. package/dist/tools/builtins/builtin-tools.js +23 -2
  216. package/dist/tools/builtins/sandbox/bash.js +16 -7
  217. package/dist/tools/builtins/sandbox/common.d.ts +1 -1
  218. package/dist/tools/builtins/sandbox/common.js +33 -10
  219. package/dist/tools/builtins/sandbox/edit.js +7 -3
  220. package/dist/tools/builtins/sandbox/file-state.d.ts +31 -1
  221. package/dist/tools/builtins/sandbox/file-state.js +33 -3
  222. package/dist/tools/builtins/sandbox/write.js +8 -4
  223. package/dist/tools/builtins/schedules.d.ts +107 -0
  224. package/dist/tools/builtins/schedules.js +789 -0
  225. package/dist/tools/elicitation.d.ts +2 -0
  226. package/dist/tools/elicitation.js +1 -1
  227. package/dist/tools/fixed-ask.d.ts +7 -0
  228. package/dist/tools/fixed-ask.js +9 -0
  229. package/dist/tools/network-asks.d.ts +109 -0
  230. package/dist/tools/network-asks.js +221 -0
  231. package/dist/tools/pipeline.d.ts +53 -6
  232. package/dist/tools/pipeline.js +202 -13
  233. package/dist/tools/policy.d.ts +21 -8
  234. package/dist/tools/policy.js +63 -9
  235. package/dist/tools/registry.d.ts +10 -1
  236. package/dist/tools/tools.module.js +12 -0
  237. package/package.json +2 -2
  238. package/src/migrations/0113_scheduled_tasks.sql +108 -0
  239. package/src/migrations/0114_network_asks.sql +23 -0
  240. package/src/migrations/0115_network_requests.sql +28 -0
  241. package/src/migrations/0116_skills_default_on.sql +37 -0
  242. package/src/migrations/0117_sandbox_tiers.sql +11 -0
@@ -1,9 +1,11 @@
1
- import { type SandboxLimits } from "@capybara-hq/plugin-sdk";
1
+ import { type SandboxLimits, type SandboxTier } from "@capybara-hq/plugin-sdk";
2
2
  import { z } from "zod";
3
3
  import type { KernelSchemaDb } from "../db/kernel-schema.js";
4
4
  import { KernelConfigService } from "../infra/config/kernel-config.service.js";
5
5
  import { KernelDb } from "../infra/database/kernel-db.js";
6
6
  import { TransactionHost } from "../infra/database/transaction-host.js";
7
+ import type { KernelLogger } from "../log.js";
8
+ import { type EffectiveSandboxPolicy } from "../sandbox/tiers.js";
7
9
  import { OrgLock } from "./org-lock.js";
8
10
  /**
9
11
  * 组织设置(`capybara.orgs.settings` jsonb)。读取宽松:缺失或非法的字段按默认值,未知键保留不动
@@ -14,7 +16,10 @@ import { OrgLock } from "./org-lock.js";
14
16
  * `Read` / `Glob` / `Grep` 外都需要审批(执行时实时读取)。
15
17
  * - `wakeOnBackgroundTask`:组织对 `runtime.wakeOnBackgroundTask` 的覆盖(缺省跟随部署),见 {@link effectiveWakeOnBackgroundTask}。
16
18
  * - `sandboxEgress`:组织的沙箱出站(Ruling M19 / D6),缺省跟随部署的 `sandbox.limits.egress` + `allowHosts`。
17
- * 见 {@link effectiveSandboxEgress}。存量值非法时按 `none` 读(宁可断网,不悄悄放宽到部署默认)。
19
+ * See `baseSandboxEgress` (sandbox/tiers.ts). 存量值非法时按 `none` 读(宁可断网,不悄悄放宽到部署默认)。
20
+ * - `sandboxTier`: the org's sandbox tier ceiling, default and unsandboxed switch (sandbox dev tiers
21
+ * spec §4.1), each absent = follow the platform (`sandbox.tiers`); never above the platform. An
22
+ * invalid stored value reads as `{ max: "strict" }` (narrower, never wider).
18
23
  * - `platformTool`: the `Platform` builtin for the whole org (Platform tool spec §3): `on`,
19
24
  * `read-only` (writes listed as unavailable) or `off` (default `on`).
20
25
  * - `timeZone`: the org's default time zone (IANA; absent = UTC) for senders without one of their
@@ -38,6 +43,25 @@ export declare const SandboxEgressSettingSchema: z.ZodObject<{
38
43
  allowHosts: z.ZodOptional<z.ZodArray<z.ZodString>>;
39
44
  }, z.core.$strict>;
40
45
  export type SandboxEgressSetting = z.infer<typeof SandboxEgressSettingSchema>;
46
+ /**
47
+ * The org's sandbox tier setting (sandbox dev tiers spec §4.1): `max` the highest tier its devices
48
+ * may reach, `default` the tier of a device that chose none, `allowUnsandboxed` false forbids
49
+ * unsandboxed local devices. Each absent = follow the platform's `sandbox.tiers`.
50
+ */
51
+ export declare const SandboxTierSettingSchema: z.ZodObject<{
52
+ max: z.ZodOptional<z.ZodEnum<{
53
+ dev: "dev";
54
+ open: "open";
55
+ strict: "strict";
56
+ }>>;
57
+ default: z.ZodOptional<z.ZodEnum<{
58
+ dev: "dev";
59
+ open: "open";
60
+ strict: "strict";
61
+ }>>;
62
+ allowUnsandboxed: z.ZodOptional<z.ZodBoolean>;
63
+ }, z.core.$strict>;
64
+ export type SandboxTierSetting = z.infer<typeof SandboxTierSettingSchema>;
41
65
  /**
42
66
  * The org's theme setting (themes spec §7): `id` a theme on in the org (a `platform_themes` id),
43
67
  * null = follow the platform's default; `personal: "locked"` hides the personal picker in the org
@@ -53,6 +77,45 @@ export declare const OrgThemeSettingSchema: z.ZodObject<{
53
77
  export type OrgThemeSetting = z.infer<typeof OrgThemeSettingSchema>;
54
78
  /** The theme setting of an org that has none: follow the platform, personal picks allowed. */
55
79
  export declare const DEFAULT_ORG_THEME: OrgThemeSetting;
80
+ /**
81
+ * Org settings of scheduled tasks (spec §9); every field reads leniently (invalid = default,
82
+ * except `forbiddenTools`, which keeps its valid entries).
83
+ */
84
+ export declare const ScheduledTasksSettingsSchema: z.ZodObject<{
85
+ enabled: z.ZodCatch<z.ZodDefault<z.ZodBoolean>>;
86
+ minIntervalMinutes: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
87
+ maxTasksPerMember: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
88
+ maxTasksPerOrg: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
89
+ maxRunsPerHour: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
90
+ defaultExpiryDays: z.ZodCatch<z.ZodDefault<z.ZodNullable<z.ZodNumber>>>;
91
+ catchUpWindowMinutes: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
92
+ unattendedAsk: z.ZodCatch<z.ZodDefault<z.ZodEnum<{
93
+ fail: "fail";
94
+ wait: "wait";
95
+ }>>>;
96
+ pauseAfterFailures: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
97
+ forbiddenTools: z.ZodCatch<z.ZodPreprocess<z.ZodDefault<z.ZodArray<z.ZodString>>, unknown>>;
98
+ }, z.core.$strip>;
99
+ export type ScheduledTasksSettings = z.infer<typeof ScheduledTasksSettingsSchema>;
100
+ /**
101
+ * The PATCH form: a partial merge into the group (fields left out keep their stored value; see
102
+ * `OrgSettingsService.update`). A field given replaces that field whole, `forbiddenTools` too.
103
+ */
104
+ export declare const ScheduledTasksPatchSchema: z.ZodObject<{
105
+ enabled: z.ZodOptional<z.ZodBoolean>;
106
+ minIntervalMinutes: z.ZodOptional<z.ZodNumber>;
107
+ maxTasksPerMember: z.ZodOptional<z.ZodNumber>;
108
+ maxTasksPerOrg: z.ZodOptional<z.ZodNumber>;
109
+ maxRunsPerHour: z.ZodOptional<z.ZodNumber>;
110
+ defaultExpiryDays: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
111
+ catchUpWindowMinutes: z.ZodOptional<z.ZodNumber>;
112
+ unattendedAsk: z.ZodOptional<z.ZodEnum<{
113
+ fail: "fail";
114
+ wait: "wait";
115
+ }>>;
116
+ pauseAfterFailures: z.ZodOptional<z.ZodNumber>;
117
+ forbiddenTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
118
+ }, z.core.$strict>;
56
119
  export declare const OrgSettingsSchema: z.ZodObject<{
57
120
  toolSearch: z.ZodCatch<z.ZodDefault<z.ZodEnum<{
58
121
  off: "off";
@@ -78,6 +141,19 @@ export declare const OrgSettingsSchema: z.ZodObject<{
78
141
  }>;
79
142
  allowHosts: z.ZodOptional<z.ZodArray<z.ZodString>>;
80
143
  }, z.core.$strict>>>;
144
+ sandboxTier: z.ZodCatch<z.ZodOptional<z.ZodObject<{
145
+ max: z.ZodOptional<z.ZodEnum<{
146
+ dev: "dev";
147
+ open: "open";
148
+ strict: "strict";
149
+ }>>;
150
+ default: z.ZodOptional<z.ZodEnum<{
151
+ dev: "dev";
152
+ open: "open";
153
+ strict: "strict";
154
+ }>>;
155
+ allowUnsandboxed: z.ZodOptional<z.ZodBoolean>;
156
+ }, z.core.$strict>>>;
81
157
  wakeOnBackgroundTask: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
82
158
  timeZone: z.ZodCatch<z.ZodOptional<z.ZodString>>;
83
159
  theme: z.ZodCatch<z.ZodOptional<z.ZodObject<{
@@ -88,6 +164,21 @@ export declare const OrgSettingsSchema: z.ZodObject<{
88
164
  }>;
89
165
  }, z.core.$strict>>>;
90
166
  uiArrangement: z.ZodCatch<z.ZodOptional<z.ZodType<Partial<Record<"agent.detail.section" | "conversation.composer.frame" | "conversation.empty" | "conversation.panel" | "conversation.properties.section" | "conversation.tool-result.card" | "file.preview" | "org.page" | "org.settings.section" | "platform.page" | "shell.brand" | "shell.overlay", import("../ui/arrangement.js").UiArrangementEntry>>, unknown, z.core.$ZodTypeInternals<Partial<Record<"agent.detail.section" | "conversation.composer.frame" | "conversation.empty" | "conversation.panel" | "conversation.properties.section" | "conversation.tool-result.card" | "file.preview" | "org.page" | "org.settings.section" | "platform.page" | "shell.brand" | "shell.overlay", import("../ui/arrangement.js").UiArrangementEntry>>, unknown>>>>;
167
+ scheduledTasks: z.ZodCatch<z.ZodPrefault<z.ZodObject<{
168
+ enabled: z.ZodCatch<z.ZodDefault<z.ZodBoolean>>;
169
+ minIntervalMinutes: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
170
+ maxTasksPerMember: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
171
+ maxTasksPerOrg: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
172
+ maxRunsPerHour: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
173
+ defaultExpiryDays: z.ZodCatch<z.ZodDefault<z.ZodNullable<z.ZodNumber>>>;
174
+ catchUpWindowMinutes: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
175
+ unattendedAsk: z.ZodCatch<z.ZodDefault<z.ZodEnum<{
176
+ fail: "fail";
177
+ wait: "wait";
178
+ }>>>;
179
+ pauseAfterFailures: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
180
+ forbiddenTools: z.ZodCatch<z.ZodPreprocess<z.ZodDefault<z.ZodArray<z.ZodString>>, unknown>>;
181
+ }, z.core.$strip>>>;
91
182
  }, z.core.$strip>;
92
183
  export type OrgSettings = z.infer<typeof OrgSettingsSchema>;
93
184
  /** PATCH 体:只改给出的字段;未知字段 → 400。 */
@@ -117,6 +208,19 @@ export declare const OrgSettingsPatchSchema: z.ZodObject<{
117
208
  }>;
118
209
  allowHosts: z.ZodOptional<z.ZodArray<z.ZodString>>;
119
210
  }, z.core.$strict>>>;
211
+ sandboxTier: z.ZodOptional<z.ZodNullable<z.ZodObject<{
212
+ max: z.ZodOptional<z.ZodEnum<{
213
+ dev: "dev";
214
+ open: "open";
215
+ strict: "strict";
216
+ }>>;
217
+ default: z.ZodOptional<z.ZodEnum<{
218
+ dev: "dev";
219
+ open: "open";
220
+ strict: "strict";
221
+ }>>;
222
+ allowUnsandboxed: z.ZodOptional<z.ZodBoolean>;
223
+ }, z.core.$strict>>>;
120
224
  timeZone: z.ZodOptional<z.ZodNullable<z.ZodString>>;
121
225
  theme: z.ZodOptional<z.ZodNullable<z.ZodObject<{
122
226
  id: z.ZodNullable<z.ZodString>;
@@ -126,15 +230,23 @@ export declare const OrgSettingsPatchSchema: z.ZodObject<{
126
230
  }>;
127
231
  }, z.core.$strict>>>;
128
232
  uiArrangement: z.ZodOptional<z.ZodType<Partial<Record<"agent.detail.section" | "conversation.composer.frame" | "conversation.empty" | "conversation.panel" | "conversation.properties.section" | "conversation.tool-result.card" | "file.preview" | "org.page" | "org.settings.section" | "platform.page" | "shell.brand" | "shell.overlay", import("../ui/arrangement.js").UiArrangementEntry | null>>, unknown, z.core.$ZodTypeInternals<Partial<Record<"agent.detail.section" | "conversation.composer.frame" | "conversation.empty" | "conversation.panel" | "conversation.properties.section" | "conversation.tool-result.card" | "file.preview" | "org.page" | "org.settings.section" | "platform.page" | "shell.brand" | "shell.overlay", import("../ui/arrangement.js").UiArrangementEntry | null>>, unknown>>>;
233
+ scheduledTasks: z.ZodOptional<z.ZodObject<{
234
+ enabled: z.ZodOptional<z.ZodBoolean>;
235
+ minIntervalMinutes: z.ZodOptional<z.ZodNumber>;
236
+ maxTasksPerMember: z.ZodOptional<z.ZodNumber>;
237
+ maxTasksPerOrg: z.ZodOptional<z.ZodNumber>;
238
+ maxRunsPerHour: z.ZodOptional<z.ZodNumber>;
239
+ defaultExpiryDays: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
240
+ catchUpWindowMinutes: z.ZodOptional<z.ZodNumber>;
241
+ unattendedAsk: z.ZodOptional<z.ZodEnum<{
242
+ fail: "fail";
243
+ wait: "wait";
244
+ }>>;
245
+ pauseAfterFailures: z.ZodOptional<z.ZodNumber>;
246
+ forbiddenTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
247
+ }, z.core.$strict>>;
129
248
  }, z.core.$strict>;
130
249
  export type OrgSettingsPatch = z.infer<typeof OrgSettingsPatchSchema>;
131
- /** 部署的沙箱出站(`sandbox.limits.egress` + `allowHosts`)。 */
132
- export type DeploymentEgress = Pick<SandboxLimits, "egress" | "allowHosts">;
133
- /**
134
- * 组织实际生效的沙箱出站:没有组织设置 → 部署默认;`none` → 只有回环;`allowlist` → 组织的白名单
135
- * (省略时用部署的),但部署是 `none` 时仍为 `none`(组织永远不比部署放得更开,部署改成 none 后存量设置也随之收紧)。
136
- */
137
- export declare function effectiveSandboxEgress(deployment: DeploymentEgress, org: SandboxEgressSetting | undefined): DeploymentEgress;
138
250
  /** Whether a finished background task wakes an idle conversation: the org's setting, else the deployment's. */
139
251
  export declare function effectiveWakeOnBackgroundTask(deployment: boolean, org: OrgSettings["wakeOnBackgroundTask"]): boolean;
140
252
  /**
@@ -154,14 +266,23 @@ export interface OrgSandboxLimits {
154
266
  /** 设置页展示用的部署默认值(`GET` 一并返回)。 */
155
267
  export interface OrgSettingsDeployment {
156
268
  sandboxEgress: {
157
- mode: SandboxLimits["egress"];
269
+ mode: "none" | "allowlist";
158
270
  allowHosts: string[];
159
271
  };
272
+ /** The platform's tiers (`sandbox.tiers`) and the dev hosts the dev tier adds. */
273
+ sandboxTiers: {
274
+ max: SandboxTier;
275
+ default: SandboxTier;
276
+ allowUnsandboxed: boolean;
277
+ devHosts: string[];
278
+ };
160
279
  }
161
280
  /** 把存储的 jsonb 解析为设置(宽松,见 {@link OrgSettingsSchema})。 */
162
281
  export declare function parseOrgSettings(raw: unknown): OrgSettings;
163
282
  /** 读组织设置(每次实时读 PG,多副本下改动立即生效);组织不存在或已删除按默认值。 */
164
283
  export declare function readOrgSettings(db: KernelSchemaDb, orgId: string): Promise<OrgSettings>;
284
+ /** Whether a stored settings object holds a `sandboxTier` this version reads as `{ max: "strict" }`. */
285
+ export declare function invalidStoredSandboxTier(raw: unknown): boolean;
165
286
  /** 组织设置服务:读取与修改(修改需 `org.update`,锁内实时复核角色,写审计)。 */
166
287
  export declare class OrgSettingsService {
167
288
  private readonly db;
@@ -169,9 +290,12 @@ export declare class OrgSettingsService {
169
290
  private readonly orgLock;
170
291
  /** The deployment config (`sandboxEgress` may not exceed the deployment's). */
171
292
  private readonly config;
293
+ private readonly log?;
172
294
  constructor(db: KernelDb, txHost: TransactionHost, orgLock: OrgLock,
173
295
  /** The deployment config (`sandboxEgress` may not exceed the deployment's). */
174
- config: Pick<KernelConfigService, "get">);
296
+ config: Pick<KernelConfigService, "get">, log?: KernelLogger | undefined);
297
+ /** Parses stored settings, warning when an invalid `sandboxTier` narrows to `{ max: "strict" }`. */
298
+ private parseStored;
175
299
  /** 部署默认值(设置页展示「跟随部署」时的实际取值)。 */
176
300
  deployment(): OrgSettingsDeployment;
177
301
  /** 组织实际生效的沙箱 cpu / 内存(只读;由超级管理员在部署设置里覆盖)。 */
@@ -181,6 +305,34 @@ export declare class OrgSettingsService {
181
305
  check(patch: OrgSettingsPatch): void;
182
306
  update(orgId: string, patch: OrgSettingsPatch, actorAccountId: string, actorTokenId?: string,
183
307
  /** Merged into the audit detail (the Platform tool's `via: "agent"`). */
184
- auditDetail?: Record<string, unknown>): Promise<OrgSettings>;
308
+ auditDetail?: Record<string, unknown>,
309
+ /**
310
+ * `onlyNarrows` (the Platform tool, API tokens): refuse a `sandboxTier` or `sandboxEgress`
311
+ * change that widens (see {@link assertOnlyNarrows}); only a person in a session widens.
312
+ */
313
+ opts?: {
314
+ onlyNarrows?: boolean;
315
+ }): Promise<OrgSettings>;
316
+ /** The effective sandbox policy for the org's settings and a device's own tier (null = none). */
317
+ sandboxPolicy(settings: OrgSettings, deviceTier: SandboxTier | null): EffectiveSandboxPolicy;
318
+ /**
319
+ * Refuses a patch that widens the org's sandbox (sandbox dev tiers spec §2, §7.2: only a person
320
+ * widens it, never the Platform tool or an API token):
321
+ * - `sandboxTier`: the effective ceiling, default or unsandboxed switch rises, or a stored cap
322
+ * loosens (an absent `max` / `default` follows the platform, so it ranks above every tier and
323
+ * clearing a cap that equals today's platform value is a widening; an explicit
324
+ * `allowUnsandboxed: false` stays false). A cleared `default` under a new `max` no higher than
325
+ * the old default is no loosening: the default never exceeds the ceiling. The value is
326
+ * replaced whole, so the refusal names the fields the patch would clear;
327
+ * - `sandboxEgress`: the base list grows: from none to anything else, or to hosts (the
328
+ * deployment's when the org gives none) not all in the current list.
329
+ */
330
+ assertOnlyNarrows(before: OrgSettings, patch: OrgSettingsPatch): void;
185
331
  private updateIn;
186
332
  }
333
+ /**
334
+ * The one {@link OrgSettingsService}: the orgs module re-exports it, and the catalog imports it
335
+ * directly (approving a network request writes `sandboxEgress`) without a cycle through OrgsModule.
336
+ */
337
+ export declare class OrgSettingsModule {
338
+ }
@@ -7,8 +7,8 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  var __param = (this && this.__param) || function (paramIndex, decorator) {
8
8
  return function (target, key) { decorator(target, key, paramIndex); }
9
9
  };
10
- import { validateAllowHost } from "@capybara-hq/plugin-sdk";
11
- import { Inject, Injectable } from "@nestjs/common";
10
+ import { SANDBOX_TIERS, tierAtMost, validateAllowHost, } from "@capybara-hq/plugin-sdk";
11
+ import { Inject, Injectable, Module, Optional } from "@nestjs/common";
12
12
  import { z } from "zod";
13
13
  import { ApiError } from "../errors.js";
14
14
  import { audit } from "../identity/audit.js";
@@ -17,10 +17,12 @@ import { KernelConfigService } from "../infra/config/kernel-config.service.js";
17
17
  import { KernelDb } from "../infra/database/kernel-db.js";
18
18
  import { TransactionHost } from "../infra/database/transaction-host.js";
19
19
  import { Transactional } from "../infra/database/transactional.decorator.js";
20
+ import { KERNEL_LOGGER } from "../infra/tokens.js";
21
+ import { effectiveSandboxPolicy } from "../sandbox/tiers.js";
20
22
  import { assertThemeOnIn } from "../themes/theme-state.js";
21
23
  import { IanaTimeZone } from "../time-zone.js";
22
24
  import { mergeArrangement, OrgUiArrangementPatchSchema, UiArrangementSchema, } from "../ui/arrangement.js";
23
- import { OrgLock } from "./org-lock.js";
25
+ import { OrgLock, OrgLockModule } from "./org-lock.js";
24
26
  /**
25
27
  * 组织设置(`capybara.orgs.settings` jsonb)。读取宽松:缺失或非法的字段按默认值,未知键保留不动
26
28
  * (更新时与存量合并),新版本加字段不需要迁移。
@@ -30,7 +32,10 @@ import { OrgLock } from "./org-lock.js";
30
32
  * `Read` / `Glob` / `Grep` 外都需要审批(执行时实时读取)。
31
33
  * - `wakeOnBackgroundTask`:组织对 `runtime.wakeOnBackgroundTask` 的覆盖(缺省跟随部署),见 {@link effectiveWakeOnBackgroundTask}。
32
34
  * - `sandboxEgress`:组织的沙箱出站(Ruling M19 / D6),缺省跟随部署的 `sandbox.limits.egress` + `allowHosts`。
33
- * 见 {@link effectiveSandboxEgress}。存量值非法时按 `none` 读(宁可断网,不悄悄放宽到部署默认)。
35
+ * See `baseSandboxEgress` (sandbox/tiers.ts). 存量值非法时按 `none` 读(宁可断网,不悄悄放宽到部署默认)。
36
+ * - `sandboxTier`: the org's sandbox tier ceiling, default and unsandboxed switch (sandbox dev tiers
37
+ * spec §4.1), each absent = follow the platform (`sandbox.tiers`); never above the platform. An
38
+ * invalid stored value reads as `{ max: "strict" }` (narrower, never wider).
34
39
  * - `platformTool`: the `Platform` builtin for the whole org (Platform tool spec §3): `on`,
35
40
  * `read-only` (writes listed as unavailable) or `off` (default `on`).
36
41
  * - `timeZone`: the org's default time zone (IANA; absent = UTC) for senders without one of their
@@ -65,6 +70,19 @@ export const SandboxEgressSettingSchema = z
65
70
  .optional(),
66
71
  })
67
72
  .strict();
73
+ const TierSchema = z.enum(["strict", "dev", "open"]);
74
+ /**
75
+ * The org's sandbox tier setting (sandbox dev tiers spec §4.1): `max` the highest tier its devices
76
+ * may reach, `default` the tier of a device that chose none, `allowUnsandboxed` false forbids
77
+ * unsandboxed local devices. Each absent = follow the platform's `sandbox.tiers`.
78
+ */
79
+ export const SandboxTierSettingSchema = z
80
+ .object({
81
+ max: TierSchema.optional(),
82
+ default: TierSchema.optional(),
83
+ allowUnsandboxed: z.boolean().optional(),
84
+ })
85
+ .strict();
68
86
  /**
69
87
  * The org's theme setting (themes spec §7): `id` a theme on in the org (a `platform_themes` id),
70
88
  * null = follow the platform's default; `personal: "locked"` hides the personal picker in the org
@@ -78,12 +96,56 @@ export const OrgThemeSettingSchema = z
78
96
  .strict();
79
97
  /** The theme setting of an org that has none: follow the platform, personal picks allowed. */
80
98
  export const DEFAULT_ORG_THEME = { id: null, personal: "allowed" };
99
+ const posInt = (max, def) => z.number().int().min(1).max(max).default(def).catch(def);
100
+ /** A stored `forbiddenTools` entry worth keeping: a tool key of the length the PATCH accepts. */
101
+ const isForbiddenToolEntry = (v) => typeof v === "string" && v.length >= 1 && v.length <= 200;
102
+ /**
103
+ * Org settings of scheduled tasks (spec §9); every field reads leniently (invalid = default,
104
+ * except `forbiddenTools`, which keeps its valid entries).
105
+ */
106
+ export const ScheduledTasksSettingsSchema = z.object({
107
+ enabled: z.boolean().default(true).catch(true),
108
+ minIntervalMinutes: posInt(1_000_000, 60),
109
+ maxTasksPerMember: posInt(1_000_000, 20),
110
+ maxTasksPerOrg: posInt(1_000_000, 200),
111
+ maxRunsPerHour: posInt(1_000_000, 60),
112
+ defaultExpiryDays: z.number().int().min(1).max(36_500).nullable().default(null).catch(null),
113
+ catchUpWindowMinutes: posInt(1_000_000, 60),
114
+ unattendedAsk: z.enum(["fail", "wait"]).default("fail").catch("fail"),
115
+ pauseAfterFailures: posInt(1000, 5),
116
+ // fail-safe: an invalid entry is dropped, the valid ones stay forbidden (never reset to []);
117
+ // past the PATCH's 500 the list is truncated
118
+ forbiddenTools: z
119
+ .preprocess((v) => (Array.isArray(v) ? v.filter(isForbiddenToolEntry).slice(0, 500) : v), z.array(z.string()).max(500).default([]))
120
+ .catch([]),
121
+ });
122
+ /**
123
+ * The PATCH form: a partial merge into the group (fields left out keep their stored value; see
124
+ * `OrgSettingsService.update`). A field given replaces that field whole, `forbiddenTools` too.
125
+ */
126
+ export const ScheduledTasksPatchSchema = z
127
+ .object({
128
+ enabled: z.boolean(),
129
+ minIntervalMinutes: z.number().int().min(1).max(1_000_000),
130
+ maxTasksPerMember: z.number().int().min(1).max(1_000_000),
131
+ maxTasksPerOrg: z.number().int().min(1).max(1_000_000),
132
+ maxRunsPerHour: z.number().int().min(1).max(1_000_000),
133
+ defaultExpiryDays: z.number().int().min(1).max(36_500).nullable(),
134
+ catchUpWindowMinutes: z.number().int().min(1).max(1_000_000),
135
+ unattendedAsk: z.enum(["fail", "wait"]),
136
+ pauseAfterFailures: z.number().int().min(1).max(1000),
137
+ forbiddenTools: z.array(z.string().min(1).max(200)).max(500),
138
+ })
139
+ .partial()
140
+ .strict();
81
141
  export const OrgSettingsSchema = z.object({
82
142
  toolSearch: z.enum(["on", "off"]).default("on").catch("on"),
83
143
  locale: z.enum(["zh", "en"]).default("en").catch("en"),
84
144
  sandboxToolPolicy: z.enum(["allow", "ask"]).default("allow").catch("allow"),
85
145
  platformTool: z.enum(["on", "read-only", "off"]).default("on").catch("on"),
86
146
  sandboxEgress: SandboxEgressSettingSchema.optional().catch({ mode: "none" }),
147
+ /** The org's sandbox tiers (sandbox dev tiers spec §4.1); absent follows the platform. */
148
+ sandboxTier: SandboxTierSettingSchema.optional().catch({ max: "strict" }),
87
149
  /** Overrides `runtime.wakeOnBackgroundTask`; absent follows the deployment. */
88
150
  wakeOnBackgroundTask: z.boolean().optional().catch(undefined),
89
151
  /** The org's default time zone (run context spec §3.3); absent = UTC. */
@@ -92,6 +154,8 @@ export const OrgSettingsSchema = z.object({
92
154
  theme: OrgThemeSettingSchema.optional().catch(undefined),
93
155
  /** Hidden and ordered extension contributions per slot; absent = follow the platform. */
94
156
  uiArrangement: UiArrangementSchema.optional().catch(undefined),
157
+ /** Scheduled tasks (spec §9); the platform bounds clamp it, see `effectiveScheduleSettings`. */
158
+ scheduledTasks: ScheduledTasksSettingsSchema.prefault({}).catch(ScheduledTasksSettingsSchema.parse({})),
95
159
  });
96
160
  /** PATCH 体:只改给出的字段;未知字段 → 400。 */
97
161
  export const OrgSettingsPatchSchema = z
@@ -104,6 +168,11 @@ export const OrgSettingsPatchSchema = z
104
168
  wakeOnBackgroundTask: z.boolean().nullable().optional(),
105
169
  /** null 清除(恢复跟随部署默认)。 */
106
170
  sandboxEgress: SandboxEgressSettingSchema.nullable().optional(),
171
+ /**
172
+ * The org's sandbox tiers, replaced whole (fields left out follow the platform); never above
173
+ * the platform (`org.sandbox_tier_not_allowed`). null clears it.
174
+ */
175
+ sandboxTier: SandboxTierSettingSchema.nullable().optional(),
107
176
  /** The org's default time zone (IANA); null clears it (UTC). */
108
177
  timeZone: IanaTimeZone.nullable().optional(),
109
178
  /**
@@ -116,19 +185,10 @@ export const OrgSettingsPatchSchema = z
116
185
  * null removes a slot's entry (it follows the platform again). `platform.page` is refused.
117
186
  */
118
187
  uiArrangement: OrgUiArrangementPatchSchema.optional(),
188
+ /** Fields to change in the scheduled-tasks group; the rest keep their value. */
189
+ scheduledTasks: ScheduledTasksPatchSchema.optional(),
119
190
  })
120
191
  .strict();
121
- /**
122
- * 组织实际生效的沙箱出站:没有组织设置 → 部署默认;`none` → 只有回环;`allowlist` → 组织的白名单
123
- * (省略时用部署的),但部署是 `none` 时仍为 `none`(组织永远不比部署放得更开,部署改成 none 后存量设置也随之收紧)。
124
- */
125
- export function effectiveSandboxEgress(deployment, org) {
126
- if (org === undefined)
127
- return { egress: deployment.egress, allowHosts: [...deployment.allowHosts] };
128
- if (org.mode === "none" || deployment.egress === "none")
129
- return { egress: "none", allowHosts: [] };
130
- return { egress: "allowlist", allowHosts: [...(org.allowHosts ?? deployment.allowHosts)] };
131
- }
132
192
  /** Whether a finished background task wakes an idle conversation: the org's setting, else the deployment's. */
133
193
  export function effectiveWakeOnBackgroundTask(deployment, org) {
134
194
  return org ?? deployment;
@@ -162,25 +222,50 @@ export async function readOrgSettings(db, orgId) {
162
222
  .executeTakeFirst();
163
223
  return parseOrgSettings(row?.settings);
164
224
  }
225
+ const sandboxTierNotAllowed = (message) => new ApiError("org.sandbox_tier_not_allowed", message);
226
+ /** Whether a stored settings object holds a `sandboxTier` this version reads as `{ max: "strict" }`. */
227
+ export function invalidStoredSandboxTier(raw) {
228
+ const v = asObject(raw).sandboxTier;
229
+ return v !== undefined && !SandboxTierSettingSchema.safeParse(v).success;
230
+ }
231
+ /** A setting's tier for intent comparison: absent (follow the platform) ranks above every tier. */
232
+ const intentRank = (t) => t === undefined ? SANDBOX_TIERS.length : SANDBOX_TIERS.indexOf(t);
165
233
  /** 组织设置服务:读取与修改(修改需 `org.update`,锁内实时复核角色,写审计)。 */
166
234
  let OrgSettingsService = class OrgSettingsService {
167
235
  db;
168
236
  txHost;
169
237
  orgLock;
170
238
  config;
239
+ log;
171
240
  constructor(db, txHost, orgLock,
172
241
  /** The deployment config (`sandboxEgress` may not exceed the deployment's). */
173
- config) {
242
+ config, log) {
174
243
  this.db = db;
175
244
  this.txHost = txHost;
176
245
  this.orgLock = orgLock;
177
246
  this.config = config;
247
+ this.log = log;
248
+ }
249
+ /** Parses stored settings, warning when an invalid `sandboxTier` narrows to `{ max: "strict" }`. */
250
+ parseStored(orgId, raw) {
251
+ if (invalidStoredSandboxTier(raw)) {
252
+ this.log?.warn("org settings: stored sandboxTier is invalid; it reads as max strict", {
253
+ orgId,
254
+ });
255
+ }
256
+ return parseOrgSettings(raw);
178
257
  }
179
258
  /** 部署默认值(设置页展示「跟随部署」时的实际取值)。 */
180
259
  deployment() {
181
- const lim = this.config.get().sandbox.limits;
260
+ const { limits: lim, tiers } = this.config.get().sandbox;
182
261
  return {
183
262
  sandboxEgress: { mode: lim.egress, allowHosts: [...lim.allowHosts] },
263
+ sandboxTiers: {
264
+ max: tiers.max,
265
+ default: tiers.default,
266
+ allowUnsandboxed: tiers.allowUnsandboxed,
267
+ devHosts: [...lim.devHosts],
268
+ },
184
269
  };
185
270
  }
186
271
  /** 组织实际生效的沙箱 cpu / 内存(只读;由超级管理员在部署设置里覆盖)。 */
@@ -202,7 +287,7 @@ let OrgSettingsService = class OrgSettingsService {
202
287
  .executeTakeFirst();
203
288
  if (!row)
204
289
  throw new ApiError("org.not_found", "Organization not found");
205
- return parseOrgSettings(row.settings);
290
+ return this.parseStored(orgId, row.settings);
206
291
  }
207
292
  /** The refusals of {@link update} that need no read of the org (the Platform tool's precheck). */
208
293
  check(patch) {
@@ -210,14 +295,83 @@ let OrgSettingsService = class OrgSettingsService {
210
295
  this.deployment().sandboxEgress.mode === "none") {
211
296
  throw new ApiError("org.sandbox_egress_not_allowed", "This deployment disables sandbox network access (sandbox.limits.egress is none); an organization can only set sandboxEgress to none");
212
297
  }
298
+ const t = patch.sandboxTier;
299
+ if (t != null) {
300
+ const platform = this.config.get().sandbox.tiers;
301
+ if (t.max !== undefined && !tierAtMost(t.max, platform.max)) {
302
+ throw sandboxTierNotAllowed(`The platform allows sandbox tiers up to ${platform.max}; sandboxTier.max cannot be ${t.max}`);
303
+ }
304
+ const max = t.max ?? platform.max;
305
+ if (t.default !== undefined && !tierAtMost(t.default, max)) {
306
+ throw sandboxTierNotAllowed(`sandboxTier.default (${t.default}) cannot be above the organization's highest tier (${max})`);
307
+ }
308
+ if (t.allowUnsandboxed === true && !platform.allowUnsandboxed) {
309
+ throw sandboxTierNotAllowed("The platform does not allow unsandboxed devices (sandbox.tiers.allowUnsandboxed is false); sandboxTier.allowUnsandboxed cannot be true");
310
+ }
311
+ }
213
312
  }
214
313
  async update(orgId, patch, actorAccountId, actorTokenId,
215
314
  /** Merged into the audit detail (the Platform tool's `via: "agent"`). */
216
- auditDetail) {
315
+ auditDetail,
316
+ /**
317
+ * `onlyNarrows` (the Platform tool, API tokens): refuse a `sandboxTier` or `sandboxEgress`
318
+ * change that widens (see {@link assertOnlyNarrows}); only a person in a session widens.
319
+ */
320
+ opts) {
217
321
  this.check(patch);
218
- return this.updateIn(orgId, patch, actorAccountId, actorTokenId, auditDetail);
322
+ return this.updateIn(orgId, patch, actorAccountId, actorTokenId, auditDetail, opts);
219
323
  }
220
- async updateIn(orgId, patch, actorAccountId, actorTokenId, auditDetail) {
324
+ /** The effective sandbox policy for the org's settings and a device's own tier (null = none). */
325
+ sandboxPolicy(settings, deviceTier) {
326
+ return effectiveSandboxPolicy(this.config.get().sandbox, settings, deviceTier);
327
+ }
328
+ /**
329
+ * Refuses a patch that widens the org's sandbox (sandbox dev tiers spec §2, §7.2: only a person
330
+ * widens it, never the Platform tool or an API token):
331
+ * - `sandboxTier`: the effective ceiling, default or unsandboxed switch rises, or a stored cap
332
+ * loosens (an absent `max` / `default` follows the platform, so it ranks above every tier and
333
+ * clearing a cap that equals today's platform value is a widening; an explicit
334
+ * `allowUnsandboxed: false` stays false). A cleared `default` under a new `max` no higher than
335
+ * the old default is no loosening: the default never exceeds the ceiling. The value is
336
+ * replaced whole, so the refusal names the fields the patch would clear;
337
+ * - `sandboxEgress`: the base list grows: from none to anything else, or to hosts (the
338
+ * deployment's when the org gives none) not all in the current list.
339
+ */
340
+ assertOnlyNarrows(before, patch) {
341
+ if (patch.sandboxTier !== undefined) {
342
+ const next = patch.sandboxTier ?? undefined;
343
+ const prev = before.sandboxTier;
344
+ const b = this.sandboxPolicy(before, null);
345
+ const a = this.sandboxPolicy({ ...before, sandboxTier: next }, null);
346
+ // a cleared default follows the platform, capped by the ceiling: `max` bounds it then
347
+ const nextDefault = next?.default === undefined && next?.max !== undefined ? next.max : next?.default;
348
+ if (!tierAtMost(a.ceiling, b.ceiling) ||
349
+ !tierAtMost(a.defaultTier, b.defaultTier) ||
350
+ (a.allowUnsandboxed && !b.allowUnsandboxed) ||
351
+ intentRank(next?.max) > intentRank(prev?.max) ||
352
+ intentRank(nextDefault) > intentRank(prev?.default) ||
353
+ (prev?.allowUnsandboxed === false && next?.allowUnsandboxed !== false)) {
354
+ const cleared = ["max", "default", "allowUnsandboxed"].filter((f) => prev?.[f] !== undefined && next?.[f] === undefined);
355
+ throw sandboxTierNotAllowed("Raising the sandbox level needs a person in the organization settings." +
356
+ (cleared.length === 0
357
+ ? ""
358
+ : ` sandboxTier is replaced whole: this change clears ${cleared.join(", ")}` +
359
+ " (which would then follow the platform); give the values to keep again."));
360
+ }
361
+ }
362
+ if (patch.sandboxEgress !== undefined) {
363
+ const deploymentHosts = this.config.get().sandbox.limits.allowHosts;
364
+ const prev = before.sandboxEgress;
365
+ const next = patch.sandboxEgress ?? undefined;
366
+ const hostsOf = (e) => e?.allowHosts ?? deploymentHosts;
367
+ const widens = next?.mode !== "none" &&
368
+ (prev?.mode === "none" || !hostsOf(next).every((h) => hostsOf(prev).includes(h)));
369
+ if (widens) {
370
+ throw new ApiError("org.sandbox_egress_not_allowed", "Widening the sandbox network needs a person in the organization settings.");
371
+ }
372
+ }
373
+ }
374
+ async updateIn(orgId, patch, actorAccountId, actorTokenId, auditDetail, opts) {
221
375
  await this.orgLock.lockAndAuthorize(orgId, actorAccountId, "org.update");
222
376
  const row = await this.db
223
377
  .selectFrom("orgs")
@@ -225,14 +379,26 @@ let OrgSettingsService = class OrgSettingsService {
225
379
  .where("id", "=", orgId)
226
380
  .executeTakeFirstOrThrow();
227
381
  const stored = asObject(row.settings);
228
- const before = parseOrgSettings(stored);
382
+ const before = this.parseStored(orgId, stored);
383
+ // again under the org lock: a hot config reload may have lowered the platform since
384
+ this.check(patch);
385
+ // under the org lock, so the comparison is with the value the write replaces
386
+ if (opts?.onlyNarrows)
387
+ this.assertOnlyNarrows(before, patch);
229
388
  const changes = Object.fromEntries(Object.entries(patch)
230
389
  .filter(([, v]) => v !== undefined)
231
390
  // null clears sandboxEgress / wakeOnBackgroundTask: follow the deployment default again
232
391
  .map(([k, v]) => [k, v === null ? undefined : v]));
233
392
  const arrangementPatch = patch.uiArrangement;
234
393
  delete changes.uiArrangement;
394
+ delete changes.scheduledTasks;
235
395
  const after = { ...before, ...changes };
396
+ if (patch.scheduledTasks !== undefined) {
397
+ after.scheduledTasks = ScheduledTasksSettingsSchema.parse({
398
+ ...before.scheduledTasks,
399
+ ...Object.fromEntries(Object.entries(patch.scheduledTasks).filter(([, v]) => v !== undefined)),
400
+ });
401
+ }
236
402
  if (arrangementPatch !== undefined) {
237
403
  after.uiArrangement = mergeArrangement(before.uiArrangement, arrangementPatch);
238
404
  if (Object.keys(after.uiArrangement).length === 0)
@@ -240,6 +406,12 @@ let OrgSettingsService = class OrgSettingsService {
240
406
  }
241
407
  if (after.sandboxEgress === undefined)
242
408
  delete after.sandboxEgress;
409
+ // an empty sandboxTier is the same as none: follow the platform
410
+ if (after.sandboxTier !== undefined && Object.keys(after.sandboxTier).length === 0) {
411
+ delete after.sandboxTier;
412
+ }
413
+ if (after.sandboxTier === undefined)
414
+ delete after.sandboxTier;
243
415
  if (after.wakeOnBackgroundTask === undefined)
244
416
  delete after.wakeOnBackgroundTask;
245
417
  if (after.timeZone === undefined)
@@ -256,7 +428,7 @@ let OrgSettingsService = class OrgSettingsService {
256
428
  await assertThemeOnIn(this.db.db, orgId, patch.theme.id);
257
429
  }
258
430
  // 与存量合并:保留本版本不认识的键(清除的 sandboxEgress 不留在存量里)
259
- const { sandboxEgress: _old, wakeOnBackgroundTask: _oldWake, timeZone: _oldZone, theme: _oldTheme, uiArrangement: _oldArrangement, ...rest } = stored;
431
+ const { sandboxEgress: _old, sandboxTier: _oldTier, wakeOnBackgroundTask: _oldWake, timeZone: _oldZone, theme: _oldTheme, uiArrangement: _oldArrangement, scheduledTasks: _oldScheduled, ...rest } = stored;
260
432
  await this.db
261
433
  .updateTable("orgs")
262
434
  .set({ settings: JSON.stringify({ ...rest, ...after }) })
@@ -281,6 +453,22 @@ OrgSettingsService = __decorate([
281
453
  __param(0, Inject(KernelDb)),
282
454
  __param(1, Inject(TransactionHost)),
283
455
  __param(2, Inject(OrgLock)),
284
- __param(3, Inject(KernelConfigService))
456
+ __param(3, Inject(KernelConfigService)),
457
+ __param(4, Optional()),
458
+ __param(4, Inject(KERNEL_LOGGER))
285
459
  ], OrgSettingsService);
286
460
  export { OrgSettingsService };
461
+ /**
462
+ * The one {@link OrgSettingsService}: the orgs module re-exports it, and the catalog imports it
463
+ * directly (approving a network request writes `sandboxEgress`) without a cycle through OrgsModule.
464
+ */
465
+ let OrgSettingsModule = class OrgSettingsModule {
466
+ };
467
+ OrgSettingsModule = __decorate([
468
+ Module({
469
+ imports: [OrgLockModule],
470
+ providers: [OrgSettingsService],
471
+ exports: [OrgSettingsService],
472
+ })
473
+ ], OrgSettingsModule);
474
+ export { OrgSettingsModule };