@capybara-hq/kernel 0.1.0-rc.8 → 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/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 -0
- package/dist/platform/ops/environments.ops.js +21 -3
- 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 +5 -2
- 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
|
}
|
|
@@ -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",
|
|
@@ -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,13 +1,15 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@capybara-hq/plugin-sdk";
|
|
2
2
|
import { ChannelSupervisor } from "../channels/supervisor.js";
|
|
3
3
|
import { ChannelWebhooks } from "../channels/webhook.js";
|
|
4
|
+
import { DeviceGateway } from "../devices/device-gateway.js";
|
|
4
5
|
import { ExtensionHost } from "../extensions/host.js";
|
|
5
6
|
import type { GatewayRoutes } from "../http/gateway-routes.js";
|
|
6
7
|
import { KernelConfigService } from "../infra/config/kernel-config.service.js";
|
|
7
8
|
/**
|
|
8
9
|
* The gateway role's parts, driven by `KernelLifecycle` at fixed points of its start order and
|
|
9
10
|
* stop phases: the channel long-connection supervisor (leader election and share) and the webhook
|
|
10
|
-
* inbound (its raw Fastify route, {@link routes}, and its runtime cache's invalidation)
|
|
11
|
+
* inbound (its raw Fastify route, {@link routes}, and its runtime cache's invalidation), and the
|
|
12
|
+
* device connections ({@link DeviceGateway}, `GET /device/connect`). Provided
|
|
11
13
|
* only by `GatewayModule`, so a process without the gateway role has none of them.
|
|
12
14
|
*/
|
|
13
15
|
export declare class GatewayRole {
|
|
@@ -16,6 +18,7 @@ export declare class GatewayRole {
|
|
|
16
18
|
private readonly bus;
|
|
17
19
|
private readonly supervisor;
|
|
18
20
|
private readonly webhooks;
|
|
21
|
+
private readonly devices;
|
|
19
22
|
/** The supervisor's subscription to extension instance changes. */
|
|
20
23
|
private unwatchPlugins;
|
|
21
24
|
private unwatchWebhooks;
|
|
@@ -23,8 +26,11 @@ export declare class GatewayRole {
|
|
|
23
26
|
private unsubscribeWebhooks;
|
|
24
27
|
/** The supervisor was started: the runs phase stops it. */
|
|
25
28
|
private supervisorStarted;
|
|
26
|
-
constructor(config: KernelConfigService, host: ExtensionHost, bus: Pick<ExtensionContext["bus"], "subscribe">, supervisor: ChannelSupervisor, webhooks: ChannelWebhooks);
|
|
27
|
-
/**
|
|
29
|
+
constructor(config: KernelConfigService, host: ExtensionHost, bus: Pick<ExtensionContext["bus"], "subscribe">, supervisor: ChannelSupervisor, webhooks: ChannelWebhooks, devices: DeviceGateway);
|
|
30
|
+
/**
|
|
31
|
+
* The gateway role's HTTP routes (the channel webhook, the device connection), registered when
|
|
32
|
+
* the app is built.
|
|
33
|
+
*/
|
|
28
34
|
routes(): GatewayRoutes;
|
|
29
35
|
/**
|
|
30
36
|
* Starts the supervisor and the webhook cache invalidation (after the extension load).
|
|
@@ -35,6 +41,11 @@ export declare class GatewayRole {
|
|
|
35
41
|
unwatch(): void;
|
|
36
42
|
/** Ends the webhook cache's `channel.changed` subscription. */
|
|
37
43
|
unsubscribe(): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Closes the device connections (1001) and hands their leases back, before the HTTP server
|
|
46
|
+
* closes (the machines reconnect to another gateway replica).
|
|
47
|
+
*/
|
|
48
|
+
stopDevices(): Promise<void>;
|
|
38
49
|
/**
|
|
39
50
|
* The runs phase: the supervisor aborts its connects and hands back its leases (undefined: never
|
|
40
51
|
* started).
|
|
@@ -11,13 +11,15 @@ import { Inject, Injectable } from "@nestjs/common";
|
|
|
11
11
|
import { CHANNEL_CHANGED_TOPIC } from "../channels/connections.service.js";
|
|
12
12
|
import { ChannelSupervisor } from "../channels/supervisor.js";
|
|
13
13
|
import { ChannelWebhooks } from "../channels/webhook.js";
|
|
14
|
+
import { DeviceGateway } from "../devices/device-gateway.js";
|
|
14
15
|
import { ExtensionHost } from "../extensions/host.js";
|
|
15
16
|
import { KernelConfigService } from "../infra/config/kernel-config.service.js";
|
|
16
17
|
import { KERNEL_BUS } from "../infra/tokens.js";
|
|
17
18
|
/**
|
|
18
19
|
* The gateway role's parts, driven by `KernelLifecycle` at fixed points of its start order and
|
|
19
20
|
* stop phases: the channel long-connection supervisor (leader election and share) and the webhook
|
|
20
|
-
* inbound (its raw Fastify route, {@link routes}, and its runtime cache's invalidation)
|
|
21
|
+
* inbound (its raw Fastify route, {@link routes}, and its runtime cache's invalidation), and the
|
|
22
|
+
* device connections ({@link DeviceGateway}, `GET /device/connect`). Provided
|
|
21
23
|
* only by `GatewayModule`, so a process without the gateway role has none of them.
|
|
22
24
|
*/
|
|
23
25
|
let GatewayRole = class GatewayRole {
|
|
@@ -26,6 +28,7 @@ let GatewayRole = class GatewayRole {
|
|
|
26
28
|
bus;
|
|
27
29
|
supervisor;
|
|
28
30
|
webhooks;
|
|
31
|
+
devices;
|
|
29
32
|
/** The supervisor's subscription to extension instance changes. */
|
|
30
33
|
unwatchPlugins;
|
|
31
34
|
unwatchWebhooks;
|
|
@@ -33,18 +36,27 @@ let GatewayRole = class GatewayRole {
|
|
|
33
36
|
unsubscribeWebhooks;
|
|
34
37
|
/** The supervisor was started: the runs phase stops it. */
|
|
35
38
|
supervisorStarted = false;
|
|
36
|
-
constructor(config, host, bus, supervisor, webhooks) {
|
|
39
|
+
constructor(config, host, bus, supervisor, webhooks, devices) {
|
|
37
40
|
this.config = config;
|
|
38
41
|
this.host = host;
|
|
39
42
|
this.bus = bus;
|
|
40
43
|
this.supervisor = supervisor;
|
|
41
44
|
this.webhooks = webhooks;
|
|
45
|
+
this.devices = devices;
|
|
42
46
|
}
|
|
43
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* The gateway role's HTTP routes (the channel webhook, the device connection), registered when
|
|
49
|
+
* the app is built.
|
|
50
|
+
*/
|
|
44
51
|
routes() {
|
|
52
|
+
const config = this.config.get();
|
|
45
53
|
return {
|
|
46
54
|
webhooks: this.webhooks,
|
|
47
|
-
webhookMaxBodyBytes:
|
|
55
|
+
webhookMaxBodyBytes: config.channels.webhookMaxBodyBytes,
|
|
56
|
+
devices: {
|
|
57
|
+
gateway: this.devices,
|
|
58
|
+
maxFrameBytes: config.environments.device.maxFrameBytes,
|
|
59
|
+
},
|
|
48
60
|
};
|
|
49
61
|
}
|
|
50
62
|
/**
|
|
@@ -79,6 +91,13 @@ let GatewayRole = class GatewayRole {
|
|
|
79
91
|
unsubscribe() {
|
|
80
92
|
return this.unsubscribeWebhooks?.() ?? Promise.resolve();
|
|
81
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Closes the device connections (1001) and hands their leases back, before the HTTP server
|
|
96
|
+
* closes (the machines reconnect to another gateway replica).
|
|
97
|
+
*/
|
|
98
|
+
stopDevices() {
|
|
99
|
+
return this.devices.stop();
|
|
100
|
+
}
|
|
82
101
|
/**
|
|
83
102
|
* The runs phase: the supervisor aborts its connects and hands back its leases (undefined: never
|
|
84
103
|
* started).
|
|
@@ -93,6 +112,7 @@ GatewayRole = __decorate([
|
|
|
93
112
|
__param(1, Inject(ExtensionHost)),
|
|
94
113
|
__param(2, Inject(KERNEL_BUS)),
|
|
95
114
|
__param(3, Inject(ChannelSupervisor)),
|
|
96
|
-
__param(4, Inject(ChannelWebhooks))
|
|
115
|
+
__param(4, Inject(ChannelWebhooks)),
|
|
116
|
+
__param(5, Inject(DeviceGateway))
|
|
97
117
|
], GatewayRole);
|
|
98
118
|
export { GatewayRole };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The gateway role (nest kernel spec §2), imported by `KernelModule` only when `roles` includes
|
|
3
3
|
* `gateway`: the channel long-connection supervisor, the webhook inbound (served as a raw Fastify
|
|
4
|
-
* route, `http/gateway-routes.ts`)
|
|
4
|
+
* route, `http/gateway-routes.ts`), the device connections ({@link DeviceGateway}, `GET
|
|
5
|
+
* /device/connect`) and {@link GatewayRole}, which `KernelLifecycle` drives. It runs
|
|
5
6
|
* no cron and no queue consumer (those are the worker role's).
|
|
6
7
|
*/
|
|
7
8
|
export declare class GatewayModule {
|
|
@@ -8,20 +8,23 @@ import { Module } from "@nestjs/common";
|
|
|
8
8
|
import { ChannelsModule } from "../channels/channels.module.js";
|
|
9
9
|
import { ChannelSupervisor } from "../channels/supervisor.js";
|
|
10
10
|
import { ChannelWebhooks } from "../channels/webhook.js";
|
|
11
|
+
import { DeviceGateway } from "../devices/device-gateway.js";
|
|
12
|
+
import { DevicesModule } from "../devices/devices.module.js";
|
|
11
13
|
import { ExtensionsModule } from "../extensions/extensions.module.js";
|
|
12
14
|
import { GatewayRole } from "./gateway-role.js";
|
|
13
15
|
/**
|
|
14
16
|
* The gateway role (nest kernel spec §2), imported by `KernelModule` only when `roles` includes
|
|
15
17
|
* `gateway`: the channel long-connection supervisor, the webhook inbound (served as a raw Fastify
|
|
16
|
-
* route, `http/gateway-routes.ts`)
|
|
18
|
+
* route, `http/gateway-routes.ts`), the device connections ({@link DeviceGateway}, `GET
|
|
19
|
+
* /device/connect`) and {@link GatewayRole}, which `KernelLifecycle` drives. It runs
|
|
17
20
|
* no cron and no queue consumer (those are the worker role's).
|
|
18
21
|
*/
|
|
19
22
|
let GatewayModule = class GatewayModule {
|
|
20
23
|
};
|
|
21
24
|
GatewayModule = __decorate([
|
|
22
25
|
Module({
|
|
23
|
-
imports: [ChannelsModule, ExtensionsModule],
|
|
24
|
-
providers: [ChannelSupervisor, ChannelWebhooks, GatewayRole],
|
|
26
|
+
imports: [ChannelsModule, ExtensionsModule, DevicesModule],
|
|
27
|
+
providers: [ChannelSupervisor, ChannelWebhooks, DeviceGateway, GatewayRole],
|
|
25
28
|
exports: [GatewayRole],
|
|
26
29
|
})
|
|
27
30
|
], GatewayModule);
|
|
@@ -233,6 +233,8 @@ export declare function summaryText(summary: string): string;
|
|
|
233
233
|
* `user_message` / `assistant_message`(content 原样,含 opaque;aborted 的也回放;媒体引用按
|
|
234
234
|
* {@link resolveMedia} 换成 base64 或文字占位)/ `reminder`
|
|
235
235
|
* (`role: "system"`,放在它所在的位置)/ `tools_changed` (`role: "system"`, the text of {@link toolsChangedText}, at its position) /
|
|
236
|
+
* `environment_paths` (`role: "system"` at its position, `environmentPathsText`; covered by the
|
|
237
|
+
* summary: the latest one follows the summary) /
|
|
236
238
|
* `run_context` (`role: "system"` at its position, `runContextText`; covered by the summary: the
|
|
237
239
|
* current context follows the summary whole; a user message's sender line by `senderLines`) /
|
|
238
240
|
* `background_task_finished`(`role: "system"`, the text of
|