@frockbot/cloudflare 0.7.294 → 0.7.296

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.
@@ -121,9 +121,13 @@ the local Worker, and every generated config overrides it from the profile.
121
121
 
122
122
  Some fields keep a placeholder rather than nothing: wrangler's validator refuses a `services` entry with no target and a `vectorize` entry with no index even in a config it never deploys, so the app Worker's two services and its Vectorize binding, and the admin portal's one service, all say `named-by-deployment-config`. Nothing reads those values — `wrangler dev --env development` and the `e2e` harness resolve their own environments, and the generator writes the deployment's own names. A bucket or database name is left out entirely, because wrangler does not ask for one.
123
123
 
124
- `adminEmails` is in the schema and in no generated config. It is the
125
- `FROCKBOT_ADMIN_EMAILS` secret the installer sets; the hosted deployment already
126
- carries it as a repository secret, which is why `hosted.json` omits it.
124
+ `adminEmails` and `adminUserIds` are in the schema and in no generated config.
125
+ They are the `FROCKBOT_ADMIN_EMAILS` and `FROCKBOT_ADMIN_USER_IDS` secrets the
126
+ installer sets; the hosted deployment already carries the first as a repository
127
+ secret, which is why `hosted.json` omits it. An email makes an admin only once
128
+ the auth Package's provider verified it, so a deployment whose people have no
129
+ email names its admins by User id; `frockbot-deployment-config secrets` carries
130
+ `FROCKBOT_ADMIN_USER_IDS` like any other optional secret.
127
131
 
128
132
  ## Brand
129
133
 
@@ -181,13 +181,23 @@ export const DEPLOYMENT_PROFILE_SCHEMA_V1 = {
181
181
  },
182
182
  adminEmails: {
183
183
  description:
184
- "The deployment's admins: they bypass admission, and a debug Turn may act as one of them. The debug surface itself is gated on `DEBUG_TOKEN`. The installer sets it as the Worker secret `FROCKBOT_ADMIN_EMAILS`; no generated config carries it, which is why the hosted profile leaves it to the repository secret it already has.",
184
+ "The deployment's admins: they bypass admission, and a debug Turn may act as one of them. The debug surface itself is gated on `DEBUG_TOKEN`. Only a verified email counts. The installer sets it as the Worker secret `FROCKBOT_ADMIN_EMAILS`; no generated config carries it, which is why the hosted profile leaves it to the repository secret it already has.",
185
185
  type: "array",
186
186
  minItems: 1,
187
187
  items: {
188
188
  $ref: "#/$defs/email",
189
189
  },
190
190
  },
191
+ adminUserIds: {
192
+ description:
193
+ "Admins by User id, beside `adminEmails`, for a deployment whose people have no verified email: an id here is an admin exactly as a verified email on `adminEmails` is. The installer sets it as the Worker secret `FROCKBOT_ADMIN_USER_IDS`; no generated config carries it.",
194
+ type: "array",
195
+ minItems: 1,
196
+ items: {
197
+ type: "string",
198
+ pattern: "^[^,\\s]+$",
199
+ },
200
+ },
191
201
  d1DatabaseId: {
192
202
  description:
193
203
  "The `AUTH_DB` database. Required by the `better-auth` Package unless the deploy supplies it with `--d1-database-id`, which is how a disposable stage names a database it creates in the same job.",
@@ -164,13 +164,22 @@
164
164
  }
165
165
  },
166
166
  "adminEmails": {
167
- "description": "The deployment's admins: they bypass admission, and a debug Turn may act as one of them. The debug surface itself is gated on `DEBUG_TOKEN`. The installer sets it as the Worker secret `FROCKBOT_ADMIN_EMAILS`; no generated config carries it, which is why the hosted profile leaves it to the repository secret it already has.",
167
+ "description": "The deployment's admins: they bypass admission, and a debug Turn may act as one of them. The debug surface itself is gated on `DEBUG_TOKEN`. Only a verified email counts. The installer sets it as the Worker secret `FROCKBOT_ADMIN_EMAILS`; no generated config carries it, which is why the hosted profile leaves it to the repository secret it already has.",
168
168
  "type": "array",
169
169
  "minItems": 1,
170
170
  "items": {
171
171
  "$ref": "#/$defs/email"
172
172
  }
173
173
  },
174
+ "adminUserIds": {
175
+ "description": "Admins by User id, beside `adminEmails`, for a deployment whose people have no verified email: an id here is an admin exactly as a verified email on `adminEmails` is. The installer sets it as the Worker secret `FROCKBOT_ADMIN_USER_IDS`; no generated config carries it.",
176
+ "type": "array",
177
+ "minItems": 1,
178
+ "items": {
179
+ "type": "string",
180
+ "pattern": "^[^,\\s]+$"
181
+ }
182
+ },
174
183
  "d1DatabaseId": {
175
184
  "description": "The `AUTH_DB` database. Required by the `better-auth` Package unless the deploy supplies it with `--d1-database-id`, which is how a disposable stage names a database it creates in the same job.",
176
185
  "type": "string",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frockbot/cloudflare",
3
- "version": "0.7.294",
3
+ "version": "0.7.296",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,12 +23,12 @@
23
23
  "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.access.json && tsc --noEmit -p test/tsconfig.json && tsc --noEmit -p e2e/tsconfig.json"
24
24
  },
25
25
  "dependencies": {
26
- "@frockbot/app": "0.7.294",
27
- "@frockbot/applets": "0.7.294",
28
- "@frockbot/computer": "0.7.294",
29
- "@frockbot/core": "0.7.294",
30
- "@frockbot/frock-compose": "0.7.294",
31
- "@frockbot/providers": "0.7.294",
26
+ "@frockbot/app": "0.7.296",
27
+ "@frockbot/applets": "0.7.296",
28
+ "@frockbot/computer": "0.7.296",
29
+ "@frockbot/core": "0.7.296",
30
+ "@frockbot/frock-compose": "0.7.296",
31
+ "@frockbot/providers": "0.7.296",
32
32
  "@rive-app/flutter-native-wasm": "44.0.0",
33
33
  "agents": "0.24.0",
34
34
  "ai": "^7.0.107",
@@ -13,14 +13,38 @@ export interface GatewayIdentityV1 {
13
13
  mode: "better-auth" | "development";
14
14
  }
15
15
 
16
+ /**
17
+ * The deployment's administrators by User id, as `FROCKBOT_ADMIN_USER_IDS`
18
+ * spells them: comma-separated and trimmed. A User id is opaque, so it is
19
+ * compared exactly. It is how a deployment whose people have no email names
20
+ * an admin.
21
+ */
22
+ export function adminUserIdsV1(value: string | undefined): ReadonlySet<string> {
23
+ return new Set(
24
+ (value ?? "")
25
+ .split(",")
26
+ .map((id) => id.trim())
27
+ .filter((id) => id.length > 0),
28
+ );
29
+ }
30
+
31
+ /** The two allowlists, as the deployment's secrets carry them. */
32
+ export interface DeploymentAdminsV1 {
33
+ emails?: string;
34
+ userIds?: string;
35
+ }
36
+
16
37
  export function isDeploymentAdminV1(
17
38
  identity: GatewayIdentityV1,
18
- configuredEmails: string | undefined,
39
+ configured: DeploymentAdminsV1,
19
40
  ): boolean {
20
41
  if (identity.mode === "development" && identity.id === DEVELOPMENT_USER_ID) {
21
42
  return true;
22
43
  }
23
- const emails = adminEmailsV1(configuredEmails);
44
+ const userIds = adminUserIdsV1(configured.userIds);
45
+ // A development id is whatever `?as_user=` said, so only a signed-in one counts.
46
+ if (identity.mode === "better-auth" && userIds.has(identity.id)) return true;
47
+ const emails = adminEmailsV1(configured.emails);
24
48
  if (
25
49
  identity.emailVerified === true &&
26
50
  identity.email &&
@@ -28,5 +52,7 @@ export function isDeploymentAdminV1(
28
52
  ) {
29
53
  return true;
30
54
  }
31
- return identity.mode === "development" && emails.size === 0;
55
+ return (
56
+ identity.mode === "development" && emails.size === 0 && userIds.size === 0
57
+ );
32
58
  }
package/src/bot-state.ts CHANGED
@@ -231,6 +231,7 @@ import {
231
231
  type ClientRunLookupQueryV1,
232
232
  type ClientRunStopCommandV1,
233
233
  } from "@frockbot/app/shell/run-protocol";
234
+ import { decodeWorkLogQueryV1 } from "@frockbot/app/shell/work-log";
234
235
  import {
235
236
  decodeBotUnreadCommandV1,
236
237
  type BotUnreadCommandV1,
@@ -3806,6 +3807,22 @@ export class BotState
3806
3807
  return shell.listRuns(request.query as ClientRunListQueryV1);
3807
3808
  }
3808
3809
 
3810
+ /** The Work log: a page of this Bot's Turns, every step of each. */
3811
+ async workLog(input: unknown) {
3812
+ const request = decodeRpcEnvelopeV1(input, {
3813
+ userId: rpcIdentifier,
3814
+ botId: rpcBotId,
3815
+ query: rpcDecoded(decodeWorkLogQueryV1),
3816
+ });
3817
+ const identity = {
3818
+ userId: request.userId as string,
3819
+ botId: request.botId as string,
3820
+ };
3821
+ const { shell } = await this.materialized(identity);
3822
+ await shell.validateIdentity(identity);
3823
+ return shell.workLog(request.query);
3824
+ }
3825
+
3809
3826
  /**
3810
3827
  * The operator snapshot behind `/api/debug`. Bot-scoped like every other
3811
3828
  * Bot RPC — the debug token authorizes the *caller*, it does not widen what
package/src/contracts.ts CHANGED
@@ -20,6 +20,7 @@ import type {
20
20
  SettingsFrame,
21
21
  ConnectionsFrame,
22
22
  SettingsChangeCommand,
23
+ WorkLogPage,
23
24
  } from "@frockbot/core/protocol-schemas";
24
25
  import {
25
26
  type AuthPackageV1,
@@ -28,6 +29,7 @@ import {
28
29
  type SessionEvent,
29
30
  } from "@frockbot/core/contracts";
30
31
  import type { MachineResultDeliveryV1 } from "@frockbot/app/machine/delivery";
32
+ import type { WorkLogQueryV1 } from "@frockbot/app/shell/work-log";
31
33
  import type { DebugGatewaySurface } from "./debug.js";
32
34
  import type { BotTurnCommandRequestV1 } from "./durable-rpc.js";
33
35
  import type {
@@ -275,6 +277,12 @@ export interface UserBotStateBinding {
275
277
  botId: string;
276
278
  query: ClientRunListQueryV1;
277
279
  }): Promise<ClientRunListV1>;
280
+ /** Everything the Bot did, a page of Turns at a time. */
281
+ workLog(input: {
282
+ schemaVersion: 1;
283
+ botId: string;
284
+ query: WorkLogQueryV1;
285
+ }): Promise<WorkLogPage>;
278
286
  lookupRun(input: {
279
287
  schemaVersion: 1;
280
288
  botId: string;
@@ -785,8 +793,9 @@ export interface GatewayDependencies {
785
793
  admitAccount(
786
794
  identity: AdmissionIdentityV1,
787
795
  ): Promise<AccountAdmissionDecisionV1>;
788
- /** Raw deployment secret; only the derived `isAdmin` boolean reaches clients. */
796
+ /** Raw deployment secrets; only the derived `isAdmin` boolean reaches clients. */
789
797
  adminEmails?: string;
798
+ adminUserIds?: string;
790
799
  applicationHashFor(userId: string): Promise<string>;
791
800
  botStateFor(userId: string): UserBotStateBinding;
792
801
  userConfigurationFor(userId: string): UserConfigurationBinding;
package/src/gateway.ts CHANGED
@@ -435,7 +435,10 @@ async function identifyRequest(
435
435
  emailVerified: session?.user.emailVerified === true,
436
436
  mode: development.userId ? "development" : "better-auth",
437
437
  },
438
- dependencies.adminEmails,
438
+ {
439
+ emails: dependencies.adminEmails,
440
+ userIds: dependencies.adminUserIds,
441
+ },
439
442
  );
440
443
  // Every authenticated request asks, so pausing an account takes effect on
441
444
  // its next request rather than at its next sign-in. An admin is never
package/src/index.ts CHANGED
@@ -59,6 +59,11 @@ import {
59
59
  type ClientRunStopCommandV1,
60
60
  type ClientRunStopReceiptV1,
61
61
  } from "@frockbot/app/shell/run-protocol";
62
+ import {
63
+ decodeWorkLogQueryV1,
64
+ type WorkLogQueryV1,
65
+ } from "@frockbot/app/shell/work-log";
66
+ import type { WorkLogPage } from "@frockbot/core/protocol-schemas";
62
67
  import {
63
68
  decodeBotNotificationDirectoryViewV1,
64
69
  decodeBotUnreadDirectoryViewV1,
@@ -163,6 +168,7 @@ import {
163
168
  import {
164
169
  DEVELOPMENT_USER_ID,
165
170
  isDeploymentAdminV1,
171
+ type DeploymentAdminsV1,
166
172
  } from "./admin-identities.js";
167
173
  import type { DebugGatewaySurface } from "./debug.js";
168
174
  import type { BotDebugQueryV1 } from "@frockbot/app/shell/debug-protocol";
@@ -369,6 +375,8 @@ interface Env {
369
375
  MACHINE_TOKEN_SECRET?: string;
370
376
  ALLOW_DEVELOPMENT_AUTH?: string;
371
377
  FROCKBOT_ADMIN_EMAILS?: string;
378
+ /** Admins by User id, for a deployment whose people have no email. */
379
+ FROCKBOT_ADMIN_USER_IDS?: string;
372
380
  ALLOWED_CLIENT_ORIGINS?: string;
373
381
  /** Authorizes `/api/debug/*`. Absent disables the surface entirely. */
374
382
  DEBUG_TOKEN?: string;
@@ -432,7 +440,7 @@ function debugSurface(env: Env): DebugGatewaySurface {
432
440
  (await getAgentByName(env.VOICE_ASSISTANTS, userId)).debugSnapshot(),
433
441
  isAdminUser: async (userId) => {
434
442
  // The auth Package's store is the durable identity source. The path's
435
- // User id is never trusted on its own: it must resolve to a stored email,
443
+ // User id is never trusted on its own: it must resolve to a stored identity,
436
444
  // and that identity is evaluated by the same allowlist policy as the
437
445
  // signed-in gateway. A Package that stores nothing resolves nothing, so
438
446
  // the one write on this surface is refused rather than granted on the
@@ -447,7 +455,7 @@ function debugSurface(env: Env): DebugGatewaySurface {
447
455
  emailVerified: identity.emailVerified,
448
456
  mode: "better-auth",
449
457
  },
450
- env.FROCKBOT_ADMIN_EMAILS,
458
+ deploymentAdmins(env),
451
459
  )
452
460
  );
453
461
  },
@@ -501,6 +509,7 @@ interface BotStateRpc extends BotConfigurationBinding {
501
509
  command: BotTurnCommandRequestV1<OwnedBotTurnCommand>,
502
510
  ): Promise<{ schemaVersion: 1; runId: string }>;
503
511
  listRuns(query: ClientRunListQueryV1): Promise<ClientRunListV1>;
512
+ workLog(query: WorkLogQueryV1): Promise<WorkLogPage>;
504
513
  debugSnapshot(query: BotDebugQueryV1): Promise<unknown>;
505
514
  lookupRun(query: ClientRunLookupQueryV1): Promise<ClientRunLookupV1>;
506
515
  runQuestions(query: ClientRunLookupQueryV1): Promise<ClientRunQuestionsV1>;
@@ -688,6 +697,7 @@ function botStateStub(env: Env, userId: string, botId: string): BotStateRpc {
688
697
  admitRun: (command) => rpc.admitRun(botTurnRpcV1(command)),
689
698
  listRuns: (query) =>
690
699
  rpc.listRuns({ schemaVersion: 1, userId, botId, query }),
700
+ workLog: (query) => rpc.workLog({ schemaVersion: 1, userId, botId, query }),
691
701
  debugSnapshot: (query) =>
692
702
  rpc.debugSnapshot({ schemaVersion: 1, userId, botId, query }),
693
703
  lookupRun: (query) =>
@@ -841,6 +851,13 @@ async function admitAccount(
841
851
  );
842
852
  }
843
853
 
854
+ function deploymentAdmins(env: Env): DeploymentAdminsV1 {
855
+ return {
856
+ emails: env.FROCKBOT_ADMIN_EMAILS,
857
+ userIds: env.FROCKBOT_ADMIN_USER_IDS,
858
+ };
859
+ }
860
+
844
861
  async function storedAdmissionIdentity(
845
862
  env: Env,
846
863
  userId: string,
@@ -860,7 +877,7 @@ async function storedAdmissionIdentity(
860
877
  emailVerified: identity.emailVerified,
861
878
  mode: "better-auth",
862
879
  },
863
- env.FROCKBOT_ADMIN_EMAILS,
880
+ deploymentAdmins(env),
864
881
  ),
865
882
  };
866
883
  }
@@ -930,7 +947,8 @@ async function mayCreateIdentity(
930
947
  emailVerified: candidate.emailVerified,
931
948
  mode: "better-auth",
932
949
  },
933
- env.FROCKBOT_ADMIN_EMAILS,
950
+ // No User exists yet, so only the email list can name one.
951
+ { emails: env.FROCKBOT_ADMIN_EMAILS },
934
952
  )
935
953
  ) {
936
954
  return true;
@@ -1285,6 +1303,18 @@ export class UserBotState extends WorkerEntrypoint<Env, UserScopedProps> {
1285
1303
  ).listRuns(request.query as ClientRunListQueryV1);
1286
1304
  }
1287
1305
 
1306
+ async workLog(input: unknown): Promise<WorkLogPage> {
1307
+ const request = decodeRpcEnvelopeV1(input, {
1308
+ botId: rpcBotId,
1309
+ query: rpcDecoded(decodeWorkLogQueryV1),
1310
+ });
1311
+ return botStateStub(
1312
+ this.env,
1313
+ this.ctx.props.userId,
1314
+ request.botId as string,
1315
+ ).workLog(request.query as WorkLogQueryV1);
1316
+ }
1317
+
1288
1318
  async lookupRun(input: unknown): Promise<ClientRunLookupV1> {
1289
1319
  const request = decodeUserBotRunLookupRpcV1(input);
1290
1320
  return botStateStub(
@@ -2754,6 +2784,9 @@ export default {
2754
2784
  ...(env.FROCKBOT_ADMIN_EMAILS
2755
2785
  ? { adminEmails: env.FROCKBOT_ADMIN_EMAILS }
2756
2786
  : {}),
2787
+ ...(env.FROCKBOT_ADMIN_USER_IDS
2788
+ ? { adminUserIds: env.FROCKBOT_ADMIN_USER_IDS }
2789
+ : {}),
2757
2790
  applicationHashFor: async () => env.DEFAULT_APPLICATION_HASH,
2758
2791
  botStateFor: (userId) =>
2759
2792
  runtimeExports.UserBotState({ props: { userId } }),
@@ -221,6 +221,12 @@ export const OPTIONAL_PRODUCTION_SECRETS_V1: readonly OptionalProductionSecretV1
221
221
  degraded:
222
222
  "nobody can administer beta access, so admission stays closed and only accounts already granted access can sign in",
223
223
  },
224
+ {
225
+ name: "FROCKBOT_ADMIN_USER_IDS",
226
+ why: "Admins by User id, beside `FROCKBOT_ADMIN_EMAILS`, for a deployment whose people have no verified email.",
227
+ degraded:
228
+ "only a verified email on `FROCKBOT_ADMIN_EMAILS` makes an admin",
229
+ },
224
230
  {
225
231
  name: "DEBUG_TOKEN",
226
232
  why: "Authorizes the read-only `/api/debug` operator surface.",
@@ -29,6 +29,10 @@ import {
29
29
  type ClientTurnRefusalV1,
30
30
  } from "@frockbot/app/shell/run-protocol";
31
31
  import { decodeApprovalDecisionCommandV1 } from "@frockbot/app/shell/approvals";
32
+ import {
33
+ decodeWorkLogQueryV1,
34
+ type WorkLogQueryV1,
35
+ } from "@frockbot/app/shell/work-log";
32
36
  import {
33
37
  decodeSecretSubmitCommandV1,
34
38
  isSecretRequestIdV1,
@@ -726,6 +730,7 @@ function createUserApplicationRoute() {
726
730
  }
727
731
 
728
732
  const skillsMatch = url.pathname.match(/^\/api\/bots\/([^/]+)\/skills$/);
733
+ const workLogMatch = url.pathname.match(/^\/api\/bots\/([^/]+)\/work-log$/);
729
734
  const workspaceFileMatch = url.pathname.match(
730
735
  /^\/api\/bots\/([^/]+)\/workspace\/file$/,
731
736
  );
@@ -744,6 +749,7 @@ function createUserApplicationRoute() {
744
749
  );
745
750
  if (
746
751
  !skillsMatch &&
752
+ !workLogMatch &&
747
753
  !workspaceFileMatch &&
748
754
  !turnMatch &&
749
755
  !lookupMatch &&
@@ -757,6 +763,7 @@ function createUserApplicationRoute() {
757
763
  try {
758
764
  const matched =
759
765
  skillsMatch ??
766
+ workLogMatch ??
760
767
  workspaceFileMatch ??
761
768
  turnMatch ??
762
769
  lookupMatch ??
@@ -776,7 +783,7 @@ function createUserApplicationRoute() {
776
783
  env,
777
784
  botId,
778
785
  request.method === "GET" &&
779
- Boolean(turnMatch || lookupMatch || questionsMatch),
786
+ Boolean(turnMatch || lookupMatch || questionsMatch || workLogMatch),
780
787
  );
781
788
  if (missingBot) return missingBot;
782
789
  // An installed client older than protocol 3 refuses a Run it does not
@@ -800,6 +807,37 @@ function createUserApplicationRoute() {
800
807
  }
801
808
  }
802
809
 
810
+ if (workLogMatch) {
811
+ if (request.method !== "GET") return jsonError(405, "method not allowed");
812
+ let query: WorkLogQueryV1;
813
+ try {
814
+ const keys = [...url.searchParams.keys()];
815
+ if (
816
+ keys.some((key) => key !== "before") ||
817
+ url.searchParams.getAll("before").length > 1
818
+ ) {
819
+ throw new Error("work log query is invalid");
820
+ }
821
+ const before = url.searchParams.get("before");
822
+ query = decodeWorkLogQueryV1({
823
+ schemaVersion: 1,
824
+ ...(before === null ? {} : { before }),
825
+ });
826
+ } catch (error) {
827
+ return jsonError(
828
+ 400,
829
+ error instanceof Error ? error.message : "invalid work log page",
830
+ );
831
+ }
832
+ try {
833
+ return Response.json(
834
+ await env.BOT_STATE.workLog({ schemaVersion: 1, botId, query }),
835
+ );
836
+ } catch (error) {
837
+ return botFailure(error, "work log failed");
838
+ }
839
+ }
840
+
803
841
  if (workspaceFileMatch) {
804
842
  // Read-only, and the durable root and path arrive as one encoded
805
843
  // `WorkspacePathV1` so the route cannot assemble a root the decoder