@capybara-hq/kernel 0.1.0-rc.7 → 0.1.0-rc.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/background/finish.js +2 -1
- package/dist/background/launch.d.ts +13 -7
- package/dist/background/launch.js +15 -8
- package/dist/background/poll.job.d.ts +1 -1
- package/dist/background/poll.job.js +8 -3
- package/dist/background/session.d.ts +2 -2
- package/dist/background/session.js +3 -2
- package/dist/background/status.d.ts +1 -1
- package/dist/background/status.js +1 -1
- package/dist/background/stop.js +4 -3
- package/dist/background/store.d.ts +6 -1
- package/dist/background/store.js +5 -1
- package/dist/background/tasks.service.d.ts +1 -1
- package/dist/background/tasks.service.js +4 -4
- package/dist/catalog/marketplaces.service.d.ts +1 -1
- package/dist/config/docs.js +2 -0
- package/dist/config/schema.d.ts +7 -0
- package/dist/config/schema.js +45 -0
- package/dist/conversations/event-log.d.ts +4 -3
- package/dist/conversations/event-log.js +4 -3
- package/dist/conversations/events.d.ts +9 -0
- package/dist/conversations/events.js +14 -0
- package/dist/db/kernel-schema.d.ts +26 -1
- package/dist/devices/device-auth.controller.d.ts +42 -0
- package/dist/devices/device-auth.controller.js +116 -0
- package/dist/devices/device-auth.d.ts +119 -0
- package/dist/devices/device-auth.js +403 -0
- package/dist/devices/device-auth.module.d.ts +7 -0
- package/dist/devices/device-auth.module.js +25 -0
- package/dist/devices/device-gateway.d.ts +128 -0
- package/dist/devices/device-gateway.js +530 -0
- package/dist/devices/device-provider.d.ts +89 -0
- package/dist/devices/device-provider.js +151 -0
- package/dist/devices/device-routes.d.ts +36 -0
- package/dist/devices/device-routes.js +90 -0
- package/dist/devices/devices.module.d.ts +16 -0
- package/dist/devices/devices.module.js +44 -0
- package/dist/devices/dto.d.ts +66 -0
- package/dist/devices/dto.js +44 -0
- package/dist/devices/keys.d.ts +25 -0
- package/dist/devices/keys.js +25 -0
- package/dist/devices/machines.d.ts +106 -0
- package/dist/devices/machines.js +301 -0
- package/dist/devices/relay.d.ts +100 -0
- package/dist/devices/relay.js +373 -0
- package/dist/devices/targets.d.ts +28 -0
- package/dist/devices/targets.js +73 -0
- package/dist/environments/access.d.ts +11 -6
- package/dist/environments/access.js +23 -8
- package/dist/environments/conversation-environments.d.ts +20 -6
- package/dist/environments/conversation-environments.js +70 -9
- package/dist/environments/environments.controller.d.ts +4 -1
- package/dist/environments/environments.controller.js +15 -1
- package/dist/environments/environments.module.js +3 -1
- package/dist/environments/environments.service.d.ts +54 -7
- package/dist/environments/environments.service.js +104 -20
- package/dist/environments/legacy-cleanup.job.d.ts +1 -1
- package/dist/environments/legacy-cleanup.job.js +4 -4
- package/dist/errors.d.ts +24 -0
- package/dist/errors.js +24 -0
- package/dist/evals/recorded-tools.d.ts +9 -0
- package/dist/evals/recorded-tools.js +27 -0
- package/dist/http/api.module.js +4 -0
- package/dist/http/gateway-routes.d.ts +7 -3
- package/dist/http/gateway-routes.js +6 -3
- package/dist/identity/me.controller.d.ts +1 -3
- package/dist/identity/me.controller.js +1 -9
- package/dist/identity/me.schemas.d.ts +14 -0
- package/dist/identity/me.schemas.js +17 -0
- package/dist/infra/config/kernel-config.service.js +2 -0
- package/dist/infra/throttler/named-throttlers.d.ts +5 -0
- package/dist/infra/throttler/named-throttlers.js +38 -0
- package/dist/kernel.module.js +3 -0
- package/dist/lifecycle/kernel-lifecycle.js +5 -0
- package/dist/platform/exclusions.js +33 -10
- package/dist/platform/operation.js +1 -1
- package/dist/platform/ops/environments.ops.js +21 -3
- package/dist/platform/ops/platform.ops.d.ts +7 -2
- package/dist/platform/ops/platform.ops.js +50 -3
- package/dist/platform/route-schemas.js +3 -0
- package/dist/roles/gateway-role.d.ts +14 -3
- package/dist/roles/gateway-role.js +25 -5
- package/dist/roles/gateway.module.d.ts +2 -1
- package/dist/roles/gateway.module.js +6 -3
- package/dist/runtime/context/build.d.ts +2 -0
- package/dist/runtime/context/build.js +14 -0
- package/dist/runtime/context/compaction.js +1 -0
- package/dist/runtime/context/environment-paths.d.ts +40 -0
- package/dist/runtime/context/environment-paths.js +48 -0
- package/dist/runtime/model-call.js +2 -0
- package/dist/sandbox/providers.d.ts +29 -7
- package/dist/sandbox/providers.js +43 -8
- package/dist/sandbox/sandbox.service.d.ts +70 -18
- package/dist/sandbox/sandbox.service.js +176 -59
- package/dist/tools/builtins/sandbox/bash.d.ts +2 -2
- package/dist/tools/builtins/sandbox/bash.js +39 -27
- package/dist/tools/builtins/sandbox/common.d.ts +60 -25
- package/dist/tools/builtins/sandbox/common.js +121 -30
- package/dist/tools/builtins/sandbox/deliver.js +9 -6
- package/dist/tools/builtins/sandbox/edit.js +40 -37
- package/dist/tools/builtins/sandbox/glob.js +13 -8
- package/dist/tools/builtins/sandbox/grep.js +10 -6
- package/dist/tools/builtins/sandbox/index.d.ts +6 -4
- package/dist/tools/builtins/sandbox/index.js +6 -4
- package/dist/tools/builtins/sandbox/read.js +9 -6
- package/dist/tools/builtins/sandbox/task-output.d.ts +7 -3
- package/dist/tools/builtins/sandbox/task-output.js +14 -10
- package/dist/tools/builtins/sandbox/task-stop.js +3 -3
- package/dist/tools/builtins/sandbox/write.js +33 -30
- package/dist/tools/builtins/skill-load.d.ts +8 -5
- package/dist/tools/builtins/skill-load.js +21 -15
- package/dist/tools/builtins/skill-write.js +2 -2
- package/dist/tools/pipeline.js +2 -0
- package/dist/tools/registry.d.ts +21 -4
- package/dist/tools/registry.js +10 -2
- package/package.json +6 -3
- package/src/migrations/0093_connected_environments.sql +37 -0
- package/src/migrations/0094_environment_paths_idx.sql +7 -0
package/dist/errors.d.ts
CHANGED
|
@@ -254,6 +254,30 @@ export declare const ERROR_CODES: {
|
|
|
254
254
|
readonly "environment.name_taken": 409;
|
|
255
255
|
/** The person already has `environments.maxPersonal` live personal environments. */
|
|
256
256
|
readonly "environment.limit_reached": 409;
|
|
257
|
+
/**
|
|
258
|
+
* A connected environment's files are on its machine: they cannot be reset from the platform;
|
|
259
|
+
* run `capybara-device logout`, then `capybara-device login` into the environment, on the machine
|
|
260
|
+
* (ruling R24).
|
|
261
|
+
*/
|
|
262
|
+
readonly "environment.files_on_device": 409;
|
|
263
|
+
/** The environment's connected machine is not connected now (no replica holds its lease). */
|
|
264
|
+
readonly "environment.offline": 503;
|
|
265
|
+
/** The connected machine runs a device program too old for this kernel; update the program. */
|
|
266
|
+
readonly "environment.program_outdated": 426;
|
|
267
|
+
/** Device sign-in (RFC 8628 style): the person has not approved the code yet; keep polling. */
|
|
268
|
+
readonly "device.authorization_pending": 400;
|
|
269
|
+
/** Device sign-in: the program polls faster than the interval; add 5 seconds and keep polling. */
|
|
270
|
+
readonly "device.slow_down": 400;
|
|
271
|
+
/** Device sign-in: the code expired before it was approved; start over. */
|
|
272
|
+
readonly "device.expired": 400;
|
|
273
|
+
/** Device sign-in: the person denied the code. */
|
|
274
|
+
readonly "device.denied": 400;
|
|
275
|
+
/** Device sign-in: no pending code matches the one given. */
|
|
276
|
+
readonly "device.code_invalid": 404;
|
|
277
|
+
/** Device sign-in: the confirmation could not hand the credential to the program; start over. */
|
|
278
|
+
readonly "device.unavailable": 503;
|
|
279
|
+
/** The machine credential is unknown or revoked. */
|
|
280
|
+
readonly "device.credential_invalid": 401;
|
|
257
281
|
readonly "skill.not_found": 404;
|
|
258
282
|
/** Also a skill neither the model nor a person could use (phase D: both invocation flags off). */
|
|
259
283
|
readonly "skill.invalid_package": 400;
|
package/dist/errors.js
CHANGED
|
@@ -254,6 +254,30 @@ export const ERROR_CODES = {
|
|
|
254
254
|
"environment.name_taken": 409,
|
|
255
255
|
/** The person already has `environments.maxPersonal` live personal environments. */
|
|
256
256
|
"environment.limit_reached": 409,
|
|
257
|
+
/**
|
|
258
|
+
* A connected environment's files are on its machine: they cannot be reset from the platform;
|
|
259
|
+
* run `capybara-device logout`, then `capybara-device login` into the environment, on the machine
|
|
260
|
+
* (ruling R24).
|
|
261
|
+
*/
|
|
262
|
+
"environment.files_on_device": 409,
|
|
263
|
+
/** The environment's connected machine is not connected now (no replica holds its lease). */
|
|
264
|
+
"environment.offline": 503,
|
|
265
|
+
/** The connected machine runs a device program too old for this kernel; update the program. */
|
|
266
|
+
"environment.program_outdated": 426,
|
|
267
|
+
/** Device sign-in (RFC 8628 style): the person has not approved the code yet; keep polling. */
|
|
268
|
+
"device.authorization_pending": 400,
|
|
269
|
+
/** Device sign-in: the program polls faster than the interval; add 5 seconds and keep polling. */
|
|
270
|
+
"device.slow_down": 400,
|
|
271
|
+
/** Device sign-in: the code expired before it was approved; start over. */
|
|
272
|
+
"device.expired": 400,
|
|
273
|
+
/** Device sign-in: the person denied the code. */
|
|
274
|
+
"device.denied": 400,
|
|
275
|
+
/** Device sign-in: no pending code matches the one given. */
|
|
276
|
+
"device.code_invalid": 404,
|
|
277
|
+
/** Device sign-in: the confirmation could not hand the credential to the program; start over. */
|
|
278
|
+
"device.unavailable": 503,
|
|
279
|
+
/** The machine credential is unknown or revoked. */
|
|
280
|
+
"device.credential_invalid": 401,
|
|
257
281
|
"skill.not_found": 404,
|
|
258
282
|
/** Also a skill neither the model nor a person could use (phase D: both invocation flags off). */
|
|
259
283
|
"skill.invalid_package": 400,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SandboxRoots } from "@capybara-hq/plugin-sdk";
|
|
1
2
|
import type { EventLog } from "../conversations/event-log.js";
|
|
2
3
|
import { type LegacyScope } from "../tools/legacy-keys.js";
|
|
3
4
|
import { type ToolPipeline, type ToolStepContext } from "../tools/pipeline.js";
|
|
@@ -23,6 +24,14 @@ export interface RecordedCall {
|
|
|
23
24
|
specHash: string;
|
|
24
25
|
/** The recorded call used release-candidate names; `toolKey` and `inputKey` are already the current ones. */
|
|
25
26
|
legacy?: true;
|
|
27
|
+
/**
|
|
28
|
+
* The roots of the latest `environment_paths` event the original run had logged by this call's
|
|
29
|
+
* step (environments spec §13.1, ruling R22): set on every recorded call from the step where an
|
|
30
|
+
* event was logged onward. A replay that matches the call writes the event when the eval
|
|
31
|
+
* conversation's latest one differs, so a candidate whose first sandbox call lands in another
|
|
32
|
+
* step still gets the line, once.
|
|
33
|
+
*/
|
|
34
|
+
environmentPaths?: SandboxRoots;
|
|
26
35
|
}
|
|
27
36
|
/**
|
|
28
37
|
* The connectors and extensions an rc `<x>__<tool>` key can name, from positive evidence in the
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { environmentPathsChange } from "../runtime/context/environment-paths.js";
|
|
1
2
|
import { assistantIdemKey } from "../runtime/model-call.js";
|
|
2
3
|
import { MATCH_IGNORED_FIELDS } from "../tools/builtin-names.js";
|
|
3
4
|
import { isLegacyPrefixedKey, legacyToolInput, legacyToolKey, } from "../tools/legacy-keys.js";
|
|
@@ -137,11 +138,29 @@ export function recordedCalls(s) {
|
|
|
137
138
|
started.set(p.callId, p.specHash);
|
|
138
139
|
}
|
|
139
140
|
}
|
|
141
|
+
// an environment_paths event belongs to the step of the assistant message before it, and holds
|
|
142
|
+
// for that step's calls and every later step's (until the next one)
|
|
143
|
+
const pathsOf = new Map();
|
|
144
|
+
const assistants = own.filter((e) => e.type === "assistant_message").map((e) => e.seq);
|
|
145
|
+
let current;
|
|
146
|
+
let next = 0;
|
|
147
|
+
for (const [i, a] of assistants.entries()) {
|
|
148
|
+
const end = assistants[i + 1] ?? Number.POSITIVE_INFINITY;
|
|
149
|
+
for (; next < own.length && own[next].seq < end; next++) {
|
|
150
|
+
const e = own[next];
|
|
151
|
+
if (e.type === "environment_paths" && e.seq > a) {
|
|
152
|
+
current = e.payload.roots;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (current !== undefined)
|
|
156
|
+
pathsOf.set(a, current);
|
|
157
|
+
}
|
|
140
158
|
const scope = legacyScopeOf(s);
|
|
141
159
|
const out = [];
|
|
142
160
|
for (const e of own) {
|
|
143
161
|
if (e.type !== "assistant_message")
|
|
144
162
|
continue;
|
|
163
|
+
const paths = pathsOf.get(e.seq);
|
|
145
164
|
const content = e.payload.content;
|
|
146
165
|
for (const b of content) {
|
|
147
166
|
if (b.type !== "tool_call")
|
|
@@ -160,6 +179,7 @@ export function recordedCalls(s) {
|
|
|
160
179
|
...(r.loadedTools !== undefined && { loadedTools: r.loadedTools }),
|
|
161
180
|
...(r.contentTypes !== undefined && { contentTypes: r.contentTypes }),
|
|
162
181
|
specHash: started.get(call.id) ?? "",
|
|
182
|
+
...(paths !== undefined && { environmentPaths: paths }),
|
|
163
183
|
});
|
|
164
184
|
}
|
|
165
185
|
}
|
|
@@ -213,7 +233,14 @@ export class RecordedToolPipeline {
|
|
|
213
233
|
const rec = this.take(call.name, matchKey(call.name, call.input));
|
|
214
234
|
if (!rec)
|
|
215
235
|
this.misses++;
|
|
236
|
+
// the original run had told the model the instance's directories by this call: so does the
|
|
237
|
+
// replay, when the eval conversation's latest record differs (as the sandbox tools write it)
|
|
238
|
+
const paths = rec?.environmentPaths;
|
|
239
|
+
const change = paths === undefined
|
|
240
|
+
? null
|
|
241
|
+
: environmentPathsChange(await eventLog.latestOfType(ctx.conversationId, "environment_paths"), paths, ctx.runId);
|
|
216
242
|
await eventLog.append(ctx.conversationId, [
|
|
243
|
+
...(change ? [change] : []),
|
|
217
244
|
{
|
|
218
245
|
type: "tool_call_started",
|
|
219
246
|
runId: ctx.runId,
|
package/dist/http/api.module.js
CHANGED
|
@@ -30,6 +30,8 @@ import { ChannelConnectionsController } from "../channels/connections.controller
|
|
|
30
30
|
import { AttachmentsController } from "../conversations/attachments.controller.js";
|
|
31
31
|
import { ConversationsController } from "../conversations/conversations.controller.js";
|
|
32
32
|
import { ConversationsModule } from "../conversations/conversations.module.js";
|
|
33
|
+
import { DeviceAuthController } from "../devices/device-auth.controller.js";
|
|
34
|
+
import { DeviceAuthModule } from "../devices/device-auth.module.js";
|
|
33
35
|
import { EnvironmentsController } from "../environments/environments.controller.js";
|
|
34
36
|
import { EnvironmentsModule } from "../environments/environments.module.js";
|
|
35
37
|
import { EvalsModule } from "../evals/evals.module.js";
|
|
@@ -133,6 +135,7 @@ const CONTROLLERS = [
|
|
|
133
135
|
OrgOwnedPluginsController,
|
|
134
136
|
OrgComponentsController,
|
|
135
137
|
AdminCatalogRequestsController,
|
|
138
|
+
DeviceAuthController,
|
|
136
139
|
];
|
|
137
140
|
/**
|
|
138
141
|
* The api role (nest kernel spec §2): the kernel API (`/api/v1/*`), imported by `KernelModule` only
|
|
@@ -158,6 +161,7 @@ ApiModule = __decorate([
|
|
|
158
161
|
ConversationsModule,
|
|
159
162
|
BackgroundModule,
|
|
160
163
|
EnvironmentsModule,
|
|
164
|
+
DeviceAuthModule,
|
|
161
165
|
RuntimeModule,
|
|
162
166
|
ChannelsModule,
|
|
163
167
|
EvalsModule,
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import type { Logger } from "@capybara-hq/plugin-sdk";
|
|
2
2
|
import type { FastifyInstance } from "fastify";
|
|
3
3
|
import type { ChannelWebhooks } from "../channels/webhook.js";
|
|
4
|
+
import { type DeviceRoutesOptions } from "../devices/device-routes.js";
|
|
4
5
|
/** What the gateway role serves over HTTP besides health and plugin routes. */
|
|
5
6
|
export interface GatewayRoutes {
|
|
6
7
|
webhooks: Pick<ChannelWebhooks, "handle">;
|
|
7
8
|
/** `channels.webhookMaxBodyBytes`. */
|
|
8
9
|
webhookMaxBodyBytes: number;
|
|
10
|
+
/** The device connection route (`GET /device/connect`, environments spec §13.4). */
|
|
11
|
+
devices: DeviceRoutesOptions;
|
|
9
12
|
}
|
|
10
13
|
/**
|
|
11
|
-
* Registers the gateway role's HTTP routes (`serveGateway = roles.includes("gateway")`):
|
|
12
|
-
* channel webhook. These are raw Fastify routes, not a Nest
|
|
13
|
-
* CSRF check do not apply (P-4); each route does its own
|
|
14
|
+
* Registers the gateway role's HTTP routes (`serveGateway = roles.includes("gateway")`): the
|
|
15
|
+
* channel webhook and the device connection WebSocket. These are raw Fastify routes, not a Nest
|
|
16
|
+
* module, so the API's global guard and CSRF check do not apply (P-4); each route does its own
|
|
17
|
+
* verification (the channel plugin's signature check, the machine credential).
|
|
14
18
|
*/
|
|
15
19
|
export declare function registerGatewayRoutes(fastify: FastifyInstance, routes: GatewayRoutes, log: Pick<Logger, "error">): Promise<void>;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { registerChannelWebhookRoute } from "../channels/webhook.controller.js";
|
|
2
|
+
import { registerDeviceConnectRoute } from "../devices/device-routes.js";
|
|
2
3
|
/**
|
|
3
|
-
* Registers the gateway role's HTTP routes (`serveGateway = roles.includes("gateway")`):
|
|
4
|
-
* channel webhook. These are raw Fastify routes, not a Nest
|
|
5
|
-
* CSRF check do not apply (P-4); each route does its own
|
|
4
|
+
* Registers the gateway role's HTTP routes (`serveGateway = roles.includes("gateway")`): the
|
|
5
|
+
* channel webhook and the device connection WebSocket. These are raw Fastify routes, not a Nest
|
|
6
|
+
* module, so the API's global guard and CSRF check do not apply (P-4); each route does its own
|
|
7
|
+
* verification (the channel plugin's signature check, the machine credential).
|
|
6
8
|
*/
|
|
7
9
|
export async function registerGatewayRoutes(fastify, routes, log) {
|
|
8
10
|
await registerChannelWebhookRoute(fastify, {
|
|
@@ -10,4 +12,5 @@ export async function registerGatewayRoutes(fastify, routes, log) {
|
|
|
10
12
|
maxBodyBytes: routes.webhookMaxBodyBytes,
|
|
11
13
|
log,
|
|
12
14
|
});
|
|
15
|
+
await registerDeviceConnectRoute(fastify, routes.devices);
|
|
13
16
|
}
|
|
@@ -6,6 +6,7 @@ import { OrgsService } from "../orgs/orgs.service.js";
|
|
|
6
6
|
import type { OrgRole } from "../orgs/roles.js";
|
|
7
7
|
import { type Account, AccountsService } from "./accounts.js";
|
|
8
8
|
import { type ApiTokenRecord, ApiTokenService } from "./api-tokens.js";
|
|
9
|
+
import { type PreferencesSchema } from "./me.schemas.js";
|
|
9
10
|
import { type Principal } from "./principal.js";
|
|
10
11
|
import { SessionService } from "./sessions.js";
|
|
11
12
|
import { SettingsService } from "./settings.js";
|
|
@@ -17,9 +18,6 @@ declare const TokenSchema: z.ZodObject<{
|
|
|
17
18
|
name: z.ZodString;
|
|
18
19
|
expiresAt: z.ZodOptional<z.ZodPipe<z.ZodISODateTime, z.ZodTransform<Date, string>>>;
|
|
19
20
|
}, z.core.$strip>;
|
|
20
|
-
declare const PreferencesSchema: z.ZodObject<{
|
|
21
|
-
timeZone: z.ZodNullable<z.ZodString>;
|
|
22
|
-
}, z.core.$strict>;
|
|
23
21
|
declare const CurrentOrgSchema: z.ZodObject<{
|
|
24
22
|
orgId: z.ZodNullable<z.ZodString>;
|
|
25
23
|
}, z.core.$strip>;
|
|
@@ -15,10 +15,10 @@ import { KERNEL_LOGGER } from "../infra/tokens.js";
|
|
|
15
15
|
import { createZodDto } from "../infra/validation/create-zod-dto.js";
|
|
16
16
|
import { ZBody } from "../infra/validation/zod-params.js";
|
|
17
17
|
import { OrgsService } from "../orgs/orgs.service.js";
|
|
18
|
-
import { IanaTimeZone } from "../time-zone.js";
|
|
19
18
|
import { AccountsService } from "./accounts.js";
|
|
20
19
|
import { ApiTokenService, FutureIsoDate } from "./api-tokens.js";
|
|
21
20
|
import { MAX_PASSWORD_LENGTH } from "./login.js";
|
|
21
|
+
import { PreferencesBody } from "./me.schemas.js";
|
|
22
22
|
import { AllowPendingPasswordChange, CurrentPrincipal, requireUser, SessionOnly, } from "./principal.js";
|
|
23
23
|
import { SessionService } from "./sessions.js";
|
|
24
24
|
import { SettingsService } from "./settings.js";
|
|
@@ -36,14 +36,6 @@ const TokenSchema = z.object({
|
|
|
36
36
|
});
|
|
37
37
|
class PersonalTokenBody extends createZodDto(TokenSchema, "PersonalTokenBody") {
|
|
38
38
|
}
|
|
39
|
-
const PreferencesSchema = z
|
|
40
|
-
.object({
|
|
41
|
-
/** The person's time zone (IANA); null = follow the browser (run context spec §3.4). */
|
|
42
|
-
timeZone: IanaTimeZone.nullable(),
|
|
43
|
-
})
|
|
44
|
-
.strict();
|
|
45
|
-
class PreferencesBody extends createZodDto(PreferencesSchema, "PreferencesBody") {
|
|
46
|
-
}
|
|
47
39
|
const CurrentOrgSchema = z.object({ orgId: z.string().min(1).nullable() });
|
|
48
40
|
class CurrentOrgBody extends createZodDto(CurrentOrgSchema, "CurrentOrgBody") {
|
|
49
41
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { RouteSchemas } from "../platform/route-schemas.js";
|
|
3
|
+
/** `PUT /me/preferences`: the person's preferences (run context spec §3.4). */
|
|
4
|
+
export declare const PreferencesSchema: z.ZodObject<{
|
|
5
|
+
timeZone: z.ZodNullable<z.ZodString>;
|
|
6
|
+
}, z.core.$strict>;
|
|
7
|
+
declare const PreferencesBody_base: import("../infra/validation/create-zod-dto.js").ZodDtoClass<z.ZodObject<{
|
|
8
|
+
timeZone: z.ZodNullable<z.ZodString>;
|
|
9
|
+
}, z.core.$strict>>;
|
|
10
|
+
export declare class PreferencesBody extends PreferencesBody_base {
|
|
11
|
+
}
|
|
12
|
+
/** The `/me` routes the Platform operations mirror (`check:platform-ops` keys). */
|
|
13
|
+
export declare const ME_ROUTES: readonly RouteSchemas[];
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createZodDto } from "../infra/validation/create-zod-dto.js";
|
|
3
|
+
import { IanaTimeZone } from "../time-zone.js";
|
|
4
|
+
/** `PUT /me/preferences`: the person's preferences (run context spec §3.4). */
|
|
5
|
+
export const PreferencesSchema = z
|
|
6
|
+
.object({
|
|
7
|
+
/** The person's time zone (IANA); null = follow the browser (run context spec §3.4). */
|
|
8
|
+
timeZone: IanaTimeZone.nullable(),
|
|
9
|
+
})
|
|
10
|
+
.strict();
|
|
11
|
+
export class PreferencesBody extends createZodDto(PreferencesSchema, "PreferencesBody") {
|
|
12
|
+
}
|
|
13
|
+
/** The `/me` routes the Platform operations mirror (`check:platform-ops` keys). */
|
|
14
|
+
export const ME_ROUTES = [
|
|
15
|
+
{ route: "GET /api/v1/me/preferences" },
|
|
16
|
+
{ route: "PUT /api/v1/me/preferences", body: PreferencesSchema },
|
|
17
|
+
];
|
|
@@ -35,6 +35,8 @@ const HOT_APPLIERS = {
|
|
|
35
35
|
"sandbox.maxConcurrentCommands": () => { },
|
|
36
36
|
// The environments service reads the personal limit at every create.
|
|
37
37
|
"environments.maxPersonal": () => { },
|
|
38
|
+
// The device relay reads the reconnect grace at every disconnect and token response.
|
|
39
|
+
"environments.device.reconnectGraceSeconds": () => { },
|
|
38
40
|
// Sub-agent limits are read at every dispatch, child step and tool resolution.
|
|
39
41
|
"subagents.maxDepth": () => { },
|
|
40
42
|
"subagents.maxConcurrentPerConversation": () => { },
|
|
@@ -25,6 +25,11 @@ export interface NamedThrottler {
|
|
|
25
25
|
* | `PUT /api/v1/me/password` | `change-password`, `change-password-ip` |
|
|
26
26
|
* | `POST /api/v1/setup` | `setup-token`, `setup-ip` |
|
|
27
27
|
* | `POST /api/v1/invitations/:token/accept` | `invitation`, `invitation-ip` |
|
|
28
|
+
* | `GET /api/v1/device/pending/:userCode`, `POST /api/v1/device/confirm`, `POST /api/v1/device/deny` | `device-code-account` |
|
|
29
|
+
*
|
|
30
|
+
* `POST /api/v1/device/authorize` and `POST /api/v1/device/token` opt in to `device-authorize-ip`
|
|
31
|
+
* and `device-token-ip` with `@Throttled` (every request counts; neither checks a guessable
|
|
32
|
+
* credential: the device code is 32 random bytes).
|
|
28
33
|
*/
|
|
29
34
|
export declare const NAMED_THROTTLERS: readonly NamedThrottler[];
|
|
30
35
|
/** The storage key of a named throttler's counter for a tracker (shared by the guard and success paths). */
|
|
@@ -28,6 +28,11 @@ const LOGIN_MSG = "Too many failed login attempts; try again later";
|
|
|
28
28
|
* | `PUT /api/v1/me/password` | `change-password`, `change-password-ip` |
|
|
29
29
|
* | `POST /api/v1/setup` | `setup-token`, `setup-ip` |
|
|
30
30
|
* | `POST /api/v1/invitations/:token/accept` | `invitation`, `invitation-ip` |
|
|
31
|
+
* | `GET /api/v1/device/pending/:userCode`, `POST /api/v1/device/confirm`, `POST /api/v1/device/deny` | `device-code-account` |
|
|
32
|
+
*
|
|
33
|
+
* `POST /api/v1/device/authorize` and `POST /api/v1/device/token` opt in to `device-authorize-ip`
|
|
34
|
+
* and `device-token-ip` with `@Throttled` (every request counts; neither checks a guessable
|
|
35
|
+
* credential: the device code is 32 random bytes).
|
|
31
36
|
*/
|
|
32
37
|
export const NAMED_THROTTLERS = [
|
|
33
38
|
// (login, IP): 10; an attacker filling one login from one IP leaves the owner's other IPs alone (R25)
|
|
@@ -114,6 +119,39 @@ export const NAMED_THROTTLERS = [
|
|
|
114
119
|
message: "Too many invitation attempts; try again later",
|
|
115
120
|
getTracker: ip,
|
|
116
121
|
},
|
|
122
|
+
// device sign-in codes issued per client address (each one holds Redis state for its TTL): 20.
|
|
123
|
+
// Opted in with @Throttled, which counts every request before the handler: group and onSuccess
|
|
124
|
+
// are unused (they matter only to CredentialThrottler).
|
|
125
|
+
{
|
|
126
|
+
name: "device-authorize-ip",
|
|
127
|
+
group: "device-authorize",
|
|
128
|
+
onSuccess: "clear",
|
|
129
|
+
limit: 20,
|
|
130
|
+
ttl: CREDENTIAL_WINDOW_MS,
|
|
131
|
+
message: "Too many device sign-in attempts; try again later",
|
|
132
|
+
getTracker: ip,
|
|
133
|
+
},
|
|
134
|
+
// device token polls per client address (@Throttled like device-authorize-ip): 600, one login
|
|
135
|
+
// polls up to 120 times (600 s / 5 s), so several machines behind one NAT address fit
|
|
136
|
+
{
|
|
137
|
+
name: "device-token-ip",
|
|
138
|
+
group: "device-token",
|
|
139
|
+
onSuccess: "clear",
|
|
140
|
+
limit: 600,
|
|
141
|
+
ttl: CREDENTIAL_WINDOW_MS,
|
|
142
|
+
message: "Too many device sign-in polls; try again later",
|
|
143
|
+
getTracker: ip,
|
|
144
|
+
},
|
|
145
|
+
// user codes tried per signed-in person (guessing another machine's code): 10, refunded on success
|
|
146
|
+
{
|
|
147
|
+
name: "device-code-account",
|
|
148
|
+
group: "device-code",
|
|
149
|
+
onSuccess: "refund",
|
|
150
|
+
limit: 10,
|
|
151
|
+
ttl: CREDENTIAL_WINDOW_MS,
|
|
152
|
+
message: "Too many device code attempts; try again later",
|
|
153
|
+
getTracker: (req) => sha256(principalAccountId(req)),
|
|
154
|
+
},
|
|
117
155
|
];
|
|
118
156
|
/** The storage key of a named throttler's counter for a tracker (shared by the guard and success paths). */
|
|
119
157
|
export const throttlerKey = (name, tracker) => sha256(`${name}:${tracker}`);
|
package/dist/kernel.module.js
CHANGED
|
@@ -10,6 +10,7 @@ import { APP_INTERCEPTOR } from "@nestjs/core";
|
|
|
10
10
|
import { CatalogModule } from "./catalog/catalog.module.js";
|
|
11
11
|
import { ChannelsModule } from "./channels/channels.module.js";
|
|
12
12
|
import { TitlesModule } from "./conversations/titles.js";
|
|
13
|
+
import { DevicesModule } from "./devices/devices.module.js";
|
|
13
14
|
import { ExtensionsModule } from "./extensions/extensions.module.js";
|
|
14
15
|
import { ApiModule } from "./http/api.module.js";
|
|
15
16
|
import { ErrorFilterModule } from "./http/error-filter.module.js";
|
|
@@ -110,6 +111,8 @@ let KernelModule = KernelModule_1 = class KernelModule {
|
|
|
110
111
|
TitlesModule,
|
|
111
112
|
UsageReportModule,
|
|
112
113
|
ChannelsModule,
|
|
114
|
+
// the @device sandbox provider, registered on every role (any replica may relay to a machine)
|
|
115
|
+
DevicesModule,
|
|
113
116
|
// the Platform tool's operation registry and actor facts
|
|
114
117
|
PlatformModule,
|
|
115
118
|
// its operations (skills, agents, ...), registered at start on every role
|
|
@@ -62,6 +62,8 @@ export const STOP_BUDGET_MS = {
|
|
|
62
62
|
};
|
|
63
63
|
/** 停机时留给渠道长连接交还租约的最少时间(与 runs 阶段并行开始,runs 用尽预算时仍保底)。 */
|
|
64
64
|
const CHANNELS_STOP_MIN_MS = 2_000;
|
|
65
|
+
/** The time the device connections get to close and hand their leases back. */
|
|
66
|
+
const DEVICES_STOP_MS = 2_000;
|
|
65
67
|
/** runs 阶段里留给强制关闭 worker 的余量(预算的其余部分用于等运行到达步骤边界)。 */
|
|
66
68
|
const RUNS_FORCE_CLOSE_MS = 500;
|
|
67
69
|
const errMsg = (e) => (e instanceof Error ? e.message : String(e));
|
|
@@ -387,6 +389,9 @@ let KernelLifecycle = class KernelLifecycle {
|
|
|
387
389
|
this.unwatchConfig = undefined;
|
|
388
390
|
gateway?.unwatch();
|
|
389
391
|
await within("webhooks-unsubscribe", (gateway?.unsubscribe() ?? Promise.resolve()).catch(() => { }), 1_000);
|
|
392
|
+
// Device connections: closed (1001) and their leases handed back before the HTTP server
|
|
393
|
+
// closes, so the machines reconnect to another gateway replica at once
|
|
394
|
+
await within("devices", gateway?.stopDevices() ?? Promise.resolve(), DEVICES_STOP_MS);
|
|
390
395
|
// 不再接 reconcile(进行中的那次由宿主关闭收尾)
|
|
391
396
|
await within("plugin-lifecycle", this.extensionLifecycle.stop(), 1_000);
|
|
392
397
|
// http 阶段:等 listen 与关闭 HTTP 共用一个截止时间;listen 卡满预算时关闭只剩 0ms,直接强制断开
|
|
@@ -248,6 +248,39 @@ export const PLATFORM_EXCLUSIONS = [
|
|
|
248
248
|
reason: "Serves sandbox file bytes",
|
|
249
249
|
},
|
|
250
250
|
// environments (personal cloud environments, environments design phase 1)
|
|
251
|
+
// connected machines (environments design phase 2): device sign-in by the program and on the
|
|
252
|
+
// confirmation page, and disconnecting a machine. The machine's WebSocket (`GET /device/connect`)
|
|
253
|
+
// is a raw gateway-role route, not a Nest controller, so it is not listed (like the webhook)
|
|
254
|
+
{
|
|
255
|
+
route: "DELETE /api/v1/orgs/:orgId/environments/:id/machine",
|
|
256
|
+
category: "human-only",
|
|
257
|
+
reason: "Disconnects a person's own machine (revokes its credential); a trust decision taken on the environment's page",
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
route: "POST /api/v1/device/authorize",
|
|
261
|
+
category: "transport",
|
|
262
|
+
reason: "Issues a sign-in code pair to the capybara-device program, which has no credential yet",
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
route: "POST /api/v1/device/token",
|
|
266
|
+
category: "transport",
|
|
267
|
+
reason: "The capybara-device program polls for its machine credential",
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
route: "GET /api/v1/device/pending/:userCode",
|
|
271
|
+
category: "human-only",
|
|
272
|
+
reason: "Shows the machine behind a code the person read off their own machine",
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
route: "POST /api/v1/device/confirm",
|
|
276
|
+
category: "human-only",
|
|
277
|
+
reason: "Lets a machine run commands for the person; only the person, signed in on the web, confirms it",
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
route: "POST /api/v1/device/deny",
|
|
281
|
+
category: "human-only",
|
|
282
|
+
reason: "Refuses a machine's sign-in code on the confirmation page",
|
|
283
|
+
},
|
|
251
284
|
{
|
|
252
285
|
route: "GET /api/v1/orgs/:orgId/conversations/:id/tools",
|
|
253
286
|
category: "internal",
|
|
@@ -330,16 +363,6 @@ export const PLATFORM_EXCLUSIONS = [
|
|
|
330
363
|
category: "human-only",
|
|
331
364
|
reason: "Switches the web session's current org",
|
|
332
365
|
},
|
|
333
|
-
{
|
|
334
|
-
route: "GET /api/v1/me/preferences",
|
|
335
|
-
category: "internal",
|
|
336
|
-
reason: "The person's time zone setting the web's preferences page reads; the run context already gives the agent the zone in effect",
|
|
337
|
-
},
|
|
338
|
-
{
|
|
339
|
-
route: "PUT /api/v1/me/preferences",
|
|
340
|
-
category: "human-only",
|
|
341
|
-
reason: "A person's own time zone setting, chosen on their preferences page",
|
|
342
|
-
},
|
|
343
366
|
{
|
|
344
367
|
route: "PUT /api/v1/me/password",
|
|
345
368
|
category: "human-only",
|
|
@@ -12,7 +12,7 @@ export const operationDomain = (id) => id.slice(0, id.indexOf("."));
|
|
|
12
12
|
export const PLATFORM_DOMAINS = [
|
|
13
13
|
{
|
|
14
14
|
id: "platform",
|
|
15
|
-
description: "What this platform is, its tool families,
|
|
15
|
+
description: "What this platform is, its tool families, what you and this agent can do, and your own preferences (time zone).",
|
|
16
16
|
},
|
|
17
17
|
{ id: "skills", description: "The org's skill library: list, read, create, switch on or off." },
|
|
18
18
|
{
|
|
@@ -30,6 +30,13 @@ function stateOf(e) {
|
|
|
30
30
|
next: "Ask an org admin; use another environment meanwhile.",
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
+
if (e.filesLost && e.source === "device") {
|
|
34
|
+
return {
|
|
35
|
+
state: "files_lost",
|
|
36
|
+
reason: "Its directory is gone from the connected machine.",
|
|
37
|
+
next: "Ask the person to run capybara-device logout, then capybara-device login into this environment, on that machine.",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
33
40
|
if (e.filesLost) {
|
|
34
41
|
return {
|
|
35
42
|
state: "files_lost",
|
|
@@ -44,7 +51,14 @@ const row = (e) => ({
|
|
|
44
51
|
name: labelOf(e),
|
|
45
52
|
...stateOf(e),
|
|
46
53
|
isDefault: e.isDefault,
|
|
54
|
+
source: e.source,
|
|
47
55
|
provider: e.provider,
|
|
56
|
+
online: e.online,
|
|
57
|
+
machine: e.machine && {
|
|
58
|
+
name: e.machine.name,
|
|
59
|
+
os: e.machine.os,
|
|
60
|
+
lastSeenAt: e.machine.lastSeenAt,
|
|
61
|
+
},
|
|
48
62
|
lastUsedAt: e.instance?.lastUsedAt ?? null,
|
|
49
63
|
});
|
|
50
64
|
/**
|
|
@@ -95,7 +109,7 @@ let EnvironmentsOperations = class EnvironmentsOperations {
|
|
|
95
109
|
this.registry.register([
|
|
96
110
|
defineOperation({
|
|
97
111
|
id: "environments.list",
|
|
98
|
-
description: "List your own
|
|
112
|
+
description: "List your own environments in this org: one compact row each (name, state, whether it is the default, source: cloud or device for a connected machine, provider, whether a device one is online and its machine, last use), plus the default and last used ids and whether new cloud environments can be made. The default environment is shown as Personal.",
|
|
99
113
|
input: z.object({}).strict(),
|
|
100
114
|
example: {},
|
|
101
115
|
authz: { orgAction: "agents.use" },
|
|
@@ -113,7 +127,7 @@ let EnvironmentsOperations = class EnvironmentsOperations {
|
|
|
113
127
|
}),
|
|
114
128
|
defineOperation({
|
|
115
129
|
id: "environments.get",
|
|
116
|
-
description: "Read one of your environments with its provider, state, instance, limits and creation time. Takes the id from environments.list.",
|
|
130
|
+
description: "Read one of your environments with its source (cloud or device), provider, state, instance, connected machine and whether it is online, limits and creation time. Takes the id from environments.list.",
|
|
117
131
|
input: EnvironmentId,
|
|
118
132
|
example: { environmentId: "4e8a0c1d-6b2f-4a37-8d19-3c5e7f9a1b20" },
|
|
119
133
|
authz: { orgAction: "agents.use" },
|
|
@@ -208,7 +222,7 @@ let EnvironmentsOperations = class EnvironmentsOperations {
|
|
|
208
222
|
}),
|
|
209
223
|
defineOperation({
|
|
210
224
|
id: "environments.reset-files",
|
|
211
|
-
description: "Wipe one of your environments' files so it starts empty: ALL its files are destroyed (after running commands finish) and cannot be recovered; the next use creates a fresh workspace. Use it when an environment reports its files are lost. The person confirms every reset; it is never remembered. The environment this conversation works in cannot be wiped. Takes the id from environments.list.",
|
|
225
|
+
description: "Wipe one of your environments' files so it starts empty: ALL its files are destroyed (after running commands finish) and cannot be recovered; the next use creates a fresh workspace. Use it when an environment reports its files are lost (not for a connected machine's environment: its files are on the machine). The person confirms every reset; it is never remembered. The environment this conversation works in cannot be wiped. Takes the id from environments.list.",
|
|
212
226
|
input: EnvironmentId,
|
|
213
227
|
example: { environmentId: "4e8a0c1d-6b2f-4a37-8d19-3c5e7f9a1b20" },
|
|
214
228
|
authz: { orgAction: "agents.use" },
|
|
@@ -225,6 +239,10 @@ let EnvironmentsOperations = class EnvironmentsOperations {
|
|
|
225
239
|
code: "environment.current",
|
|
226
240
|
next: "This conversation works in it: ask the person to wipe it on the web, or wipe a different one.",
|
|
227
241
|
},
|
|
242
|
+
{
|
|
243
|
+
code: "environment.files_on_device",
|
|
244
|
+
next: "Its files are on its connected machine: ask the person to run capybara-device logout, then capybara-device login into this environment, on that machine.",
|
|
245
|
+
},
|
|
228
246
|
{
|
|
229
247
|
code: "environment.unavailable",
|
|
230
248
|
next: {
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { type OnModuleInit } from "@nestjs/common";
|
|
2
2
|
import { AgentsService } from "../../agents/agents.service.js";
|
|
3
|
+
import { AccountsService } from "../../identity/accounts.js";
|
|
3
4
|
import { OrgSettingsService } from "../../orgs/settings.service.js";
|
|
4
5
|
import { ToolRegistry } from "../../tools/registry.js";
|
|
5
6
|
import { PlatformActor } from "../actor.js";
|
|
6
7
|
import { PlatformOperations } from "../registry.js";
|
|
7
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* The `platform` domain: orientation and what this agent is (spec §7, synthetic), and the
|
|
10
|
+
* person's own preferences (`/me/preferences`).
|
|
11
|
+
*/
|
|
8
12
|
export declare class PlatformOperationsProvider implements OnModuleInit {
|
|
9
13
|
private readonly registry;
|
|
10
14
|
private readonly actor;
|
|
11
15
|
private readonly agents;
|
|
12
16
|
private readonly settings;
|
|
13
17
|
private readonly tools;
|
|
14
|
-
|
|
18
|
+
private readonly accounts;
|
|
19
|
+
constructor(registry: PlatformOperations, actor: PlatformActor, agents: AgentsService, settings: OrgSettingsService, tools: ToolRegistry, accounts: AccountsService);
|
|
15
20
|
/**
|
|
16
21
|
* The agent's tools by the keys the model sees, as the registry resolves them for a run (tools
|
|
17
22
|
* default-on spec §8): every tool the org offers that the definition does not turn off, builtins
|