@electric-ax/agents-server 0.4.14 → 0.4.16

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.
@@ -0,0 +1,5 @@
1
+ ALTER TABLE runners
2
+ ADD COLUMN sandbox_profiles jsonb NOT NULL DEFAULT '[]'::jsonb;
3
+ --> statement-breakpoint
4
+ ALTER TABLE entities
5
+ ADD COLUMN sandbox jsonb;
@@ -0,0 +1,100 @@
1
+ CREATE TABLE "entity_type_permission_grants" (
2
+ "id" bigserial PRIMARY KEY NOT NULL,
3
+ "tenant_id" text DEFAULT 'default' NOT NULL,
4
+ "entity_type" text NOT NULL,
5
+ "permission" text NOT NULL,
6
+ "subject_kind" text NOT NULL,
7
+ "subject_value" text NOT NULL,
8
+ "created_by" text,
9
+ "expires_at" timestamp with time zone,
10
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
11
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
12
+ CONSTRAINT "chk_type_permission_grants_permission" CHECK ("entity_type_permission_grants"."permission" IN ('spawn', 'manage')),
13
+ CONSTRAINT "chk_type_permission_grants_subject_kind" CHECK ("entity_type_permission_grants"."subject_kind" IN ('principal', 'principal_kind'))
14
+ );
15
+ --> statement-breakpoint
16
+ CREATE TABLE "entity_lineage" (
17
+ "tenant_id" text DEFAULT 'default' NOT NULL,
18
+ "ancestor_url" text NOT NULL,
19
+ "descendant_url" text NOT NULL,
20
+ "depth" integer NOT NULL,
21
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
22
+ CONSTRAINT "entity_lineage_pkey" PRIMARY KEY ("tenant_id", "ancestor_url", "descendant_url"),
23
+ CONSTRAINT "chk_entity_lineage_depth" CHECK ("entity_lineage"."depth" >= 0)
24
+ );
25
+ --> statement-breakpoint
26
+ CREATE TABLE "entity_permission_grants" (
27
+ "id" bigserial PRIMARY KEY NOT NULL,
28
+ "tenant_id" text DEFAULT 'default' NOT NULL,
29
+ "entity_url" text NOT NULL,
30
+ "permission" text NOT NULL,
31
+ "subject_kind" text NOT NULL,
32
+ "subject_value" text NOT NULL,
33
+ "propagation" text DEFAULT 'self' NOT NULL,
34
+ "copy_to_children" boolean DEFAULT false NOT NULL,
35
+ "created_by" text,
36
+ "expires_at" timestamp with time zone,
37
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
38
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
39
+ CONSTRAINT "chk_entity_permission_grants_permission" CHECK ("entity_permission_grants"."permission" IN ('read', 'write', 'delete', 'signal', 'fork', 'schedule', 'spawn', 'manage')),
40
+ CONSTRAINT "chk_entity_permission_grants_subject_kind" CHECK ("entity_permission_grants"."subject_kind" IN ('principal', 'principal_kind')),
41
+ CONSTRAINT "chk_entity_permission_grants_propagation" CHECK ("entity_permission_grants"."propagation" IN ('self', 'descendants'))
42
+ );
43
+ --> statement-breakpoint
44
+ CREATE TABLE "entity_effective_permissions" (
45
+ "id" bigserial PRIMARY KEY NOT NULL,
46
+ "tenant_id" text DEFAULT 'default' NOT NULL,
47
+ "entity_url" text NOT NULL,
48
+ "source_entity_url" text NOT NULL,
49
+ "source_grant_id" bigint NOT NULL,
50
+ "permission" text NOT NULL,
51
+ "subject_kind" text NOT NULL,
52
+ "subject_value" text NOT NULL,
53
+ "expires_at" timestamp with time zone,
54
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
55
+ CONSTRAINT "uq_entity_effective_permission" UNIQUE ("tenant_id", "entity_url", "source_grant_id"),
56
+ CONSTRAINT "chk_entity_effective_permissions_permission" CHECK ("entity_effective_permissions"."permission" IN ('read', 'write', 'delete', 'signal', 'fork', 'schedule', 'spawn', 'manage')),
57
+ CONSTRAINT "chk_entity_effective_permissions_subject_kind" CHECK ("entity_effective_permissions"."subject_kind" IN ('principal', 'principal_kind'))
58
+ );
59
+ --> statement-breakpoint
60
+ CREATE TABLE "shared_state_links" (
61
+ "tenant_id" text DEFAULT 'default' NOT NULL,
62
+ "shared_state_id" text NOT NULL,
63
+ "owner_entity_url" text NOT NULL,
64
+ "manifest_key" text NOT NULL,
65
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
66
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
67
+ CONSTRAINT "shared_state_links_pkey" PRIMARY KEY ("tenant_id", "owner_entity_url", "manifest_key")
68
+ );
69
+ --> statement-breakpoint
70
+ CREATE INDEX "idx_type_permission_grants_lookup" ON "entity_type_permission_grants" USING btree ("tenant_id", "entity_type", "permission", "subject_kind", "subject_value");
71
+ --> statement-breakpoint
72
+ CREATE INDEX "idx_type_permission_grants_expiry" ON "entity_type_permission_grants" USING btree ("tenant_id", "expires_at");
73
+ --> statement-breakpoint
74
+ CREATE INDEX "idx_entity_lineage_descendant" ON "entity_lineage" USING btree ("tenant_id", "descendant_url");
75
+ --> statement-breakpoint
76
+ CREATE INDEX "idx_entity_permission_grants_entity" ON "entity_permission_grants" USING btree ("tenant_id", "entity_url");
77
+ --> statement-breakpoint
78
+ CREATE INDEX "idx_entity_permission_grants_subject" ON "entity_permission_grants" USING btree ("tenant_id", "permission", "subject_kind", "subject_value");
79
+ --> statement-breakpoint
80
+ CREATE INDEX "idx_entity_permission_grants_expiry" ON "entity_permission_grants" USING btree ("tenant_id", "expires_at");
81
+ --> statement-breakpoint
82
+ CREATE INDEX "idx_entity_effective_permissions_lookup" ON "entity_effective_permissions" USING btree ("tenant_id", "permission", "subject_kind", "subject_value", "entity_url");
83
+ --> statement-breakpoint
84
+ CREATE INDEX "idx_entity_effective_permissions_entity" ON "entity_effective_permissions" USING btree ("tenant_id", "entity_url");
85
+ --> statement-breakpoint
86
+ CREATE INDEX "idx_entity_effective_permissions_expiry" ON "entity_effective_permissions" USING btree ("tenant_id", "expires_at");
87
+ --> statement-breakpoint
88
+ CREATE INDEX "idx_shared_state_links_shared_state" ON "shared_state_links" USING btree ("tenant_id", "shared_state_id");
89
+ --> statement-breakpoint
90
+ CREATE INDEX "idx_shared_state_links_owner" ON "shared_state_links" USING btree ("tenant_id", "owner_entity_url");
91
+ --> statement-breakpoint
92
+ -- Pre-permission entity bridge rows do not carry principal attribution. Drop them
93
+ -- so observation bridges are rebuilt with principal_url/principal_kind scoping.
94
+ DELETE FROM "entity_bridges";
95
+ --> statement-breakpoint
96
+ ALTER TABLE "entity_bridges" ADD COLUMN "principal_url" text;
97
+ --> statement-breakpoint
98
+ ALTER TABLE "entity_bridges" ADD COLUMN "principal_kind" text;
99
+ --> statement-breakpoint
100
+ CREATE INDEX "idx_entity_bridges_principal" ON "entity_bridges" USING btree ("tenant_id", "principal_kind", "principal_url");
@@ -0,0 +1,25 @@
1
+ INSERT INTO "entity_type_permission_grants" (
2
+ "tenant_id",
3
+ "entity_type",
4
+ "permission",
5
+ "subject_kind",
6
+ "subject_value"
7
+ )
8
+ SELECT
9
+ "entity_types"."tenant_id",
10
+ "entity_types"."name",
11
+ 'manage',
12
+ 'principal_kind',
13
+ 'user'
14
+ FROM "entity_types"
15
+ WHERE "entity_types"."name" = 'horton'
16
+ AND NOT EXISTS (
17
+ SELECT 1
18
+ FROM "entity_type_permission_grants"
19
+ WHERE "entity_type_permission_grants"."tenant_id" = "entity_types"."tenant_id"
20
+ AND "entity_type_permission_grants"."entity_type" = "entity_types"."name"
21
+ AND "entity_type_permission_grants"."permission" = 'manage'
22
+ AND "entity_type_permission_grants"."subject_kind" = 'principal_kind'
23
+ AND "entity_type_permission_grants"."subject_value" = 'user'
24
+ AND "entity_type_permission_grants"."expires_at" IS NULL
25
+ );
@@ -0,0 +1,25 @@
1
+ INSERT INTO "entity_type_permission_grants" (
2
+ "tenant_id",
3
+ "entity_type",
4
+ "permission",
5
+ "subject_kind",
6
+ "subject_value"
7
+ )
8
+ SELECT
9
+ "entity_types"."tenant_id",
10
+ "entity_types"."name",
11
+ 'manage',
12
+ 'principal_kind',
13
+ 'user'
14
+ FROM "entity_types"
15
+ WHERE "entity_types"."name" = 'worker'
16
+ AND NOT EXISTS (
17
+ SELECT 1
18
+ FROM "entity_type_permission_grants"
19
+ WHERE "entity_type_permission_grants"."tenant_id" = "entity_types"."tenant_id"
20
+ AND "entity_type_permission_grants"."entity_type" = "entity_types"."name"
21
+ AND "entity_type_permission_grants"."permission" = 'manage'
22
+ AND "entity_type_permission_grants"."subject_kind" = 'principal_kind'
23
+ AND "entity_type_permission_grants"."subject_value" = 'user'
24
+ AND "entity_type_permission_grants"."expires_at" IS NULL
25
+ );
@@ -71,6 +71,34 @@
71
71
  "when": 1778540000000,
72
72
  "tag": "0009_entity_signal_statuses",
73
73
  "breakpoints": true
74
+ },
75
+ {
76
+ "idx": 10,
77
+ "version": "7",
78
+ "when": 1779062400000,
79
+ "tag": "0010_sandbox_profiles",
80
+ "breakpoints": true
81
+ },
82
+ {
83
+ "idx": 11,
84
+ "version": "7",
85
+ "when": 1779050000000,
86
+ "tag": "0011_entity_permissions",
87
+ "breakpoints": true
88
+ },
89
+ {
90
+ "idx": 12,
91
+ "version": "7",
92
+ "when": 1780584581000,
93
+ "tag": "0012_horton_user_manage_permission",
94
+ "breakpoints": true
95
+ },
96
+ {
97
+ "idx": 13,
98
+ "version": "7",
99
+ "when": 1780588695000,
100
+ "tag": "0013_worker_user_manage_permission",
101
+ "breakpoints": true
74
102
  }
75
103
  ]
76
104
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electric-ax/agents-server",
3
- "version": "0.4.14",
3
+ "version": "0.4.16",
4
4
  "description": "Electric Agents entity runtime server",
5
5
  "author": "Durable Stream contributors",
6
6
  "bin": {
@@ -37,8 +37,8 @@
37
37
  "dependencies": {
38
38
  "@anthropic-ai/sdk": "^0.78.0",
39
39
  "@durable-streams/client": "^0.2.6",
40
- "@durable-streams/server": "^0.3.5",
41
- "@durable-streams/state": "^0.2.9",
40
+ "@durable-streams/server": "^0.3.7",
41
+ "@durable-streams/state": "^0.3.1",
42
42
  "@electric-sql/client": "^1.5.20",
43
43
  "@mariozechner/pi-agent-core": "^0.70.2",
44
44
  "@opentelemetry/api": "^1.9.1",
@@ -54,7 +54,7 @@
54
54
  "pino-pretty": "^13.0.0",
55
55
  "postgres": "^3.4.0",
56
56
  "undici": "^7.24.7",
57
- "@electric-ax/agents-runtime": "0.3.7"
57
+ "@electric-ax/agents-runtime": "0.3.9"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "^22.19.15",
@@ -65,9 +65,9 @@
65
65
  "tsx": "^4.19.0",
66
66
  "typescript": "^5.0.0",
67
67
  "vitest": "^4.1.0",
68
- "@electric-ax/agents": "0.4.11",
69
- "@electric-ax/agents-server-conformance-tests": "0.1.9",
70
- "@electric-ax/agents-server-ui": "0.4.14"
68
+ "@electric-ax/agents": "0.4.13",
69
+ "@electric-ax/agents-server-conformance-tests": "0.1.11",
70
+ "@electric-ax/agents-server-ui": "0.4.16"
71
71
  },
72
72
  "files": [
73
73
  "dist",
package/src/db/schema.ts CHANGED
@@ -49,6 +49,7 @@ export const entities = pgTable(
49
49
  .notNull()
50
50
  .default(sql`'{}'::text[]`),
51
51
  spawnArgs: jsonb(`spawn_args`).default({}),
52
+ sandbox: jsonb(`sandbox`),
52
53
  parent: text(`parent`),
53
54
  createdBy: text(`created_by`),
54
55
  typeRevision: integer(`type_revision`),
@@ -71,6 +72,197 @@ export const entities = pgTable(
71
72
  ]
72
73
  )
73
74
 
75
+ export const entityTypePermissionGrants = pgTable(
76
+ `entity_type_permission_grants`,
77
+ {
78
+ id: bigserial(`id`, { mode: `number` }).primaryKey(),
79
+ tenantId: text(`tenant_id`).notNull().default(`default`),
80
+ entityType: text(`entity_type`).notNull(),
81
+ permission: text(`permission`).notNull(),
82
+ subjectKind: text(`subject_kind`).notNull(),
83
+ subjectValue: text(`subject_value`).notNull(),
84
+ createdBy: text(`created_by`),
85
+ expiresAt: timestamp(`expires_at`, { withTimezone: true }),
86
+ createdAt: timestamp(`created_at`, { withTimezone: true })
87
+ .notNull()
88
+ .defaultNow(),
89
+ updatedAt: timestamp(`updated_at`, { withTimezone: true })
90
+ .notNull()
91
+ .defaultNow(),
92
+ },
93
+ (table) => [
94
+ index(`idx_type_permission_grants_lookup`).on(
95
+ table.tenantId,
96
+ table.entityType,
97
+ table.permission,
98
+ table.subjectKind,
99
+ table.subjectValue
100
+ ),
101
+ index(`idx_type_permission_grants_expiry`).on(
102
+ table.tenantId,
103
+ table.expiresAt
104
+ ),
105
+ check(
106
+ `chk_type_permission_grants_permission`,
107
+ sql`${table.permission} IN ('spawn', 'manage')`
108
+ ),
109
+ check(
110
+ `chk_type_permission_grants_subject_kind`,
111
+ sql`${table.subjectKind} IN ('principal', 'principal_kind')`
112
+ ),
113
+ ]
114
+ )
115
+
116
+ export const entityLineage = pgTable(
117
+ `entity_lineage`,
118
+ {
119
+ tenantId: text(`tenant_id`).notNull().default(`default`),
120
+ ancestorUrl: text(`ancestor_url`).notNull(),
121
+ descendantUrl: text(`descendant_url`).notNull(),
122
+ depth: integer(`depth`).notNull(),
123
+ createdAt: timestamp(`created_at`, { withTimezone: true })
124
+ .notNull()
125
+ .defaultNow(),
126
+ },
127
+ (table) => [
128
+ primaryKey({
129
+ columns: [table.tenantId, table.ancestorUrl, table.descendantUrl],
130
+ }),
131
+ index(`idx_entity_lineage_descendant`).on(
132
+ table.tenantId,
133
+ table.descendantUrl
134
+ ),
135
+ check(`chk_entity_lineage_depth`, sql`${table.depth} >= 0`),
136
+ ]
137
+ )
138
+
139
+ export const entityPermissionGrants = pgTable(
140
+ `entity_permission_grants`,
141
+ {
142
+ id: bigserial(`id`, { mode: `number` }).primaryKey(),
143
+ tenantId: text(`tenant_id`).notNull().default(`default`),
144
+ entityUrl: text(`entity_url`).notNull(),
145
+ permission: text(`permission`).notNull(),
146
+ subjectKind: text(`subject_kind`).notNull(),
147
+ subjectValue: text(`subject_value`).notNull(),
148
+ propagation: text(`propagation`).notNull().default(`self`),
149
+ copyToChildren: boolean(`copy_to_children`).notNull().default(false),
150
+ createdBy: text(`created_by`),
151
+ expiresAt: timestamp(`expires_at`, { withTimezone: true }),
152
+ createdAt: timestamp(`created_at`, { withTimezone: true })
153
+ .notNull()
154
+ .defaultNow(),
155
+ updatedAt: timestamp(`updated_at`, { withTimezone: true })
156
+ .notNull()
157
+ .defaultNow(),
158
+ },
159
+ (table) => [
160
+ index(`idx_entity_permission_grants_entity`).on(
161
+ table.tenantId,
162
+ table.entityUrl
163
+ ),
164
+ index(`idx_entity_permission_grants_subject`).on(
165
+ table.tenantId,
166
+ table.permission,
167
+ table.subjectKind,
168
+ table.subjectValue
169
+ ),
170
+ index(`idx_entity_permission_grants_expiry`).on(
171
+ table.tenantId,
172
+ table.expiresAt
173
+ ),
174
+ check(
175
+ `chk_entity_permission_grants_permission`,
176
+ sql`${table.permission} IN ('read', 'write', 'delete', 'signal', 'fork', 'schedule', 'spawn', 'manage')`
177
+ ),
178
+ check(
179
+ `chk_entity_permission_grants_subject_kind`,
180
+ sql`${table.subjectKind} IN ('principal', 'principal_kind')`
181
+ ),
182
+ check(
183
+ `chk_entity_permission_grants_propagation`,
184
+ sql`${table.propagation} IN ('self', 'descendants')`
185
+ ),
186
+ ]
187
+ )
188
+
189
+ export const entityEffectivePermissions = pgTable(
190
+ `entity_effective_permissions`,
191
+ {
192
+ id: bigserial(`id`, { mode: `number` }).primaryKey(),
193
+ tenantId: text(`tenant_id`).notNull().default(`default`),
194
+ entityUrl: text(`entity_url`).notNull(),
195
+ sourceEntityUrl: text(`source_entity_url`).notNull(),
196
+ sourceGrantId: bigint(`source_grant_id`, { mode: `number` }).notNull(),
197
+ permission: text(`permission`).notNull(),
198
+ subjectKind: text(`subject_kind`).notNull(),
199
+ subjectValue: text(`subject_value`).notNull(),
200
+ expiresAt: timestamp(`expires_at`, { withTimezone: true }),
201
+ createdAt: timestamp(`created_at`, { withTimezone: true })
202
+ .notNull()
203
+ .defaultNow(),
204
+ },
205
+ (table) => [
206
+ unique(`uq_entity_effective_permission`).on(
207
+ table.tenantId,
208
+ table.entityUrl,
209
+ table.sourceGrantId
210
+ ),
211
+ index(`idx_entity_effective_permissions_lookup`).on(
212
+ table.tenantId,
213
+ table.permission,
214
+ table.subjectKind,
215
+ table.subjectValue,
216
+ table.entityUrl
217
+ ),
218
+ index(`idx_entity_effective_permissions_entity`).on(
219
+ table.tenantId,
220
+ table.entityUrl
221
+ ),
222
+ index(`idx_entity_effective_permissions_expiry`).on(
223
+ table.tenantId,
224
+ table.expiresAt
225
+ ),
226
+ check(
227
+ `chk_entity_effective_permissions_permission`,
228
+ sql`${table.permission} IN ('read', 'write', 'delete', 'signal', 'fork', 'schedule', 'spawn', 'manage')`
229
+ ),
230
+ check(
231
+ `chk_entity_effective_permissions_subject_kind`,
232
+ sql`${table.subjectKind} IN ('principal', 'principal_kind')`
233
+ ),
234
+ ]
235
+ )
236
+
237
+ export const sharedStateLinks = pgTable(
238
+ `shared_state_links`,
239
+ {
240
+ tenantId: text(`tenant_id`).notNull().default(`default`),
241
+ sharedStateId: text(`shared_state_id`).notNull(),
242
+ ownerEntityUrl: text(`owner_entity_url`).notNull(),
243
+ manifestKey: text(`manifest_key`).notNull(),
244
+ createdAt: timestamp(`created_at`, { withTimezone: true })
245
+ .notNull()
246
+ .defaultNow(),
247
+ updatedAt: timestamp(`updated_at`, { withTimezone: true })
248
+ .notNull()
249
+ .defaultNow(),
250
+ },
251
+ (table) => [
252
+ primaryKey({
253
+ columns: [table.tenantId, table.ownerEntityUrl, table.manifestKey],
254
+ }),
255
+ index(`idx_shared_state_links_shared_state`).on(
256
+ table.tenantId,
257
+ table.sharedStateId
258
+ ),
259
+ index(`idx_shared_state_links_owner`).on(
260
+ table.tenantId,
261
+ table.ownerEntityUrl
262
+ ),
263
+ ]
264
+ )
265
+
74
266
  export const users = pgTable(
75
267
  `users`,
76
268
  {
@@ -111,6 +303,7 @@ export const runners = pgTable(
111
303
  kind: text(`kind`).notNull().default(`local`),
112
304
  adminStatus: text(`admin_status`).notNull().default(`enabled`),
113
305
  wakeStream: text(`wake_stream`).notNull(),
306
+ sandboxProfiles: jsonb(`sandbox_profiles`).notNull().default([]),
114
307
  createdAt: timestamp(`created_at`, { withTimezone: true })
115
308
  .notNull()
116
309
  .defaultNow(),
@@ -434,6 +627,8 @@ export const entityBridges = pgTable(
434
627
  sourceRef: text(`source_ref`).notNull(),
435
628
  tags: jsonb(`tags`).notNull(),
436
629
  streamUrl: text(`stream_url`).notNull(),
630
+ principalUrl: text(`principal_url`),
631
+ principalKind: text(`principal_kind`),
437
632
  shapeHandle: text(`shape_handle`),
438
633
  shapeOffset: text(`shape_offset`),
439
634
  lastObserverActivityAt: timestamp(`last_observer_activity_at`, {
@@ -451,6 +646,11 @@ export const entityBridges = pgTable(
451
646
  (table) => [
452
647
  primaryKey({ columns: [table.tenantId, table.sourceRef] }),
453
648
  unique(`uq_entity_bridges_stream_url`).on(table.tenantId, table.streamUrl),
649
+ index(`idx_entity_bridges_principal`).on(
650
+ table.tenantId,
651
+ table.principalKind,
652
+ table.principalUrl
653
+ ),
454
654
  ]
455
655
  )
456
656
 
@@ -67,6 +67,78 @@ export type RunnerKind = `local` | `cloud-worker` | `sandbox` | `ci` | `server`
67
67
  export type RunnerAdminStatus = `enabled` | `disabled`
68
68
  export type RunnerLiveness = `online` | `offline`
69
69
 
70
+ export type PermissionSubjectKind = `principal` | `principal_kind`
71
+ export type PermissionSubject = {
72
+ subject_kind: PermissionSubjectKind
73
+ subject_value: string
74
+ }
75
+ export type EntityPermission =
76
+ | `read`
77
+ | `write`
78
+ | `delete`
79
+ | `signal`
80
+ | `fork`
81
+ | `schedule`
82
+ | `spawn`
83
+ | `manage`
84
+ export type EntityTypePermission = `spawn` | `manage`
85
+ export type EntityPermissionPropagation = `self` | `descendants`
86
+
87
+ export interface EntityPermissionGrant extends PermissionSubject {
88
+ id: number
89
+ entity_url: string
90
+ permission: EntityPermission
91
+ propagation: EntityPermissionPropagation
92
+ copy_to_children: boolean
93
+ created_by?: string
94
+ expires_at?: string
95
+ created_at: string
96
+ updated_at: string
97
+ }
98
+
99
+ export interface EntityTypePermissionGrant extends PermissionSubject {
100
+ id: number
101
+ entity_type: string
102
+ permission: EntityTypePermission
103
+ created_by?: string
104
+ expires_at?: string
105
+ created_at: string
106
+ updated_at: string
107
+ }
108
+
109
+ export interface EntityTypePermissionGrantInput extends PermissionSubject {
110
+ permission: EntityTypePermission
111
+ expires_at?: string
112
+ }
113
+
114
+ export type AuthorizationResource =
115
+ | { kind: `entity`; entity: ElectricAgentsEntity }
116
+ | { kind: `entity_type`; entityType: ElectricAgentsEntityType }
117
+ | { kind: `entity_type_registration`; entityTypeName: string }
118
+ | {
119
+ kind: `shared_state`
120
+ sharedStateId: string
121
+ linkedEntityUrls: Array<string>
122
+ }
123
+
124
+ export type AuthorizationDecision = {
125
+ decision: `allow` | `deny`
126
+ expires_at?: string
127
+ }
128
+
129
+ export type AuthorizeRequest = (input: {
130
+ tenant: string
131
+ principal: Principal
132
+ verb: EntityPermission | EntityTypePermission
133
+ resource: AuthorizationResource
134
+ request?: {
135
+ method: string
136
+ url: string
137
+ headers: Record<string, string>
138
+ }
139
+ builtInAllowed: boolean
140
+ }) => Promise<AuthorizationDecision> | AuthorizationDecision
141
+
70
142
  const VALID_RUNNER_KINDS = new Set<string>([
71
143
  `local`,
72
144
  `cloud-worker`,
@@ -130,6 +202,19 @@ export interface RunnerActiveClaim {
130
202
  leaseExpiresAt?: string
131
203
  }
132
204
 
205
+ export interface SandboxProfileAdvertisement {
206
+ name: string
207
+ label: string
208
+ description?: string
209
+ /**
210
+ * True for off-host (remote-provider) profiles, reachable from any runner.
211
+ * Absent/false means the sandbox is host-local, so a shared sandbox on this
212
+ * profile requires its collaborators to be pinned to a single runner. Set
213
+ * by the runtime per profile (see SandboxProfile.remote).
214
+ */
215
+ remote?: boolean
216
+ }
217
+
133
218
  export interface ElectricAgentsRunner {
134
219
  id: string
135
220
  owner_principal: string
@@ -143,6 +228,7 @@ export interface ElectricAgentsRunner {
143
228
  wake_stream: string
144
229
  wake_stream_offset?: string
145
230
  diagnostics?: Record<string, unknown>
231
+ sandbox_profiles: Array<SandboxProfileAdvertisement>
146
232
  created_at: string
147
233
  updated_at: string
148
234
  }
@@ -295,19 +381,67 @@ export function expectedSignalStatus(
295
381
  }
296
382
  }
297
383
 
384
+ /**
385
+ * Resolved sandbox selection stored on an entity and replayed to the runtime at
386
+ * wake. Only an explicit / inherited cross-entity `key` is persisted here;
387
+ * `scope`-derived keys are computed at wake time (and so left unstored, keeping
388
+ * the co-location guard keyed on genuine cross-entity sharing). `persistent`
389
+ * defaults by scope at wake time when unset.
390
+ */
391
+ export interface EntitySandboxSelection {
392
+ profile: string
393
+ key?: string
394
+ scope?: `entity` | `wake`
395
+ persistent?: boolean
396
+ /**
397
+ * Whether the entity owns the sandbox (create + govern teardown) or only
398
+ * attaches to an owner's. Stored as `false` for an attacher (e.g. an
399
+ * `inherit` spawn); omitted ⇒ owner (the default).
400
+ */
401
+ owner?: boolean
402
+ }
403
+
404
+ /**
405
+ * Spawn-time sandbox CHOICE — the request input, before resolution. Resolved
406
+ * into an {@link EntitySandboxSelection} by the spawn path. The wire schema for
407
+ * this shape lives in `sandbox-choice-schema.ts` (mirrors how `DispatchPolicy`
408
+ * pairs with `dispatch-policy-schema.ts`).
409
+ */
410
+ export interface SandboxChoice {
411
+ /** Profile name advertised by the target runner. */
412
+ profile?: string
413
+ /** Explicit cross-entity key to join (or start) a shared sandbox. */
414
+ key?: string
415
+ /** Identity scope when no explicit `key`: per-entity (default) or per-wake. */
416
+ scope?: `entity` | `wake`
417
+ /** Idle-teardown durability; defaults by scope when unset. */
418
+ persistent?: boolean
419
+ /** Whether this entity owns the sandbox (default) or only attaches to one. */
420
+ owner?: boolean
421
+ /** Reuse the parent entity's resolved sandbox (attach-only). */
422
+ inherit?: boolean
423
+ }
424
+
298
425
  export interface ElectricAgentsEntity {
299
426
  url: string
300
427
  type: string
301
428
  status: EntityStatus
302
429
  streams: {
303
430
  main: string
304
- error: string
305
431
  }
306
432
  subscription_id: string
307
433
  dispatch_policy?: DispatchPolicy
308
434
  write_token: string
309
435
  tags: Record<string, string>
310
436
  spawn_args?: Record<string, unknown>
437
+ /**
438
+ * Resolved sandbox selection. An explicit `key` lets entities collaborate on
439
+ * one workspace and is the only key form persisted (it's cross-entity, so the
440
+ * co-location guard applies); a `scope` ('entity' default / 'wake') instead
441
+ * derives the key at wake time, so it's left unstored. `persistent` chooses
442
+ * idle durability.
443
+ */
444
+ sandbox?: EntitySandboxSelection
311
445
  parent?: string
312
446
  type_revision?: number
313
447
  inbox_schemas?: Record<string, Record<string, unknown>>
@@ -322,10 +456,11 @@ export interface PublicElectricAgentsEntity {
322
456
  url: string
323
457
  type: string
324
458
  status: EntityStatus
325
- streams: { main: string; error: string }
459
+ streams: { main: string }
326
460
  dispatch_policy?: DispatchPolicy
327
461
  tags: Record<string, string>
328
462
  spawn_args?: Record<string, unknown>
463
+ sandbox?: EntitySandboxSelection
329
464
  parent?: string
330
465
  created_by?: string
331
466
  created_at: number
@@ -350,6 +485,7 @@ export function toPublicEntity(
350
485
  dispatch_policy: entity.dispatch_policy,
351
486
  tags: entity.tags,
352
487
  spawn_args: entity.spawn_args,
488
+ sandbox: entity.sandbox,
353
489
  parent: entity.parent,
354
490
  created_by: entity.created_by,
355
491
  created_at: entity.created_at,
@@ -378,6 +514,7 @@ export interface RegisterEntityTypeRequest {
378
514
  state_schemas?: Record<string, Record<string, unknown>>
379
515
  serve_endpoint?: string
380
516
  default_dispatch_policy?: DispatchPolicy
517
+ permission_grants?: Array<EntityTypePermissionGrantInput>
381
518
  }
382
519
 
383
520
  export interface TypedSpawnRequest {
@@ -386,6 +523,12 @@ export interface TypedSpawnRequest {
386
523
  tags?: Record<string, string>
387
524
  parent?: string
388
525
  dispatch_policy?: DispatchPolicy
526
+ /**
527
+ * Sandbox selection: `profile` for a sandbox (optionally with `scope` /
528
+ * `persistent`), `key` to join (or start) an explicit shared one, or
529
+ * `inherit: true` to reuse the parent's resolved sandbox.
530
+ */
531
+ sandbox?: SandboxChoice
389
532
  initialMessage?: unknown
390
533
  created_by?: string
391
534
  wake?: {
@@ -406,6 +549,8 @@ export interface TypedSpawnRequest {
406
549
 
407
550
  export interface SendRequest {
408
551
  from?: string
552
+ from_principal?: string
553
+ from_agent?: string
409
554
  payload?: unknown
410
555
  key?: string
411
556
  type?: string