@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.
- package/dist/entrypoint.js +1530 -256
- package/dist/index.cjs +1517 -232
- package/dist/index.d.cts +1359 -212
- package/dist/index.d.ts +1359 -212
- package/dist/index.js +1519 -234
- package/drizzle/0010_sandbox_profiles.sql +5 -0
- package/drizzle/0011_entity_permissions.sql +100 -0
- package/drizzle/0012_horton_user_manage_permission.sql +25 -0
- package/drizzle/0013_worker_user_manage_permission.sql +25 -0
- package/drizzle/meta/_journal.json +28 -0
- package/package.json +7 -7
- package/src/db/schema.ts +200 -0
- package/src/electric-agents-types.ts +147 -2
- package/src/entity-bridge-manager.ts +57 -6
- package/src/entity-manager.ts +411 -62
- package/src/entity-projector.ts +79 -17
- package/src/entity-registry.ts +681 -5
- package/src/index.ts +11 -0
- package/src/permissions.ts +239 -0
- package/src/routing/context.ts +2 -0
- package/src/routing/durable-streams-router.ts +125 -4
- package/src/routing/electric-proxy-router.ts +4 -0
- package/src/routing/entities-router.ts +362 -20
- package/src/routing/entity-types-router.ts +244 -15
- package/src/routing/hooks.ts +1 -0
- package/src/routing/observations-router.ts +2 -1
- package/src/routing/runners-router.ts +10 -0
- package/src/routing/sandbox.ts +173 -0
- package/src/runtime.ts +34 -0
- package/src/sandbox-choice-schema.ts +28 -0
- package/src/scheduler.ts +2 -0
- package/src/server.ts +5 -0
- package/src/stream-client.ts +17 -1
- package/src/utils/server-utils.ts +192 -12
- package/src/wake-registry.ts +30 -11
|
@@ -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.
|
|
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.
|
|
41
|
-
"@durable-streams/state": "^0.
|
|
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.
|
|
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.
|
|
69
|
-
"@electric-ax/agents-server-conformance-tests": "0.1.
|
|
70
|
-
"@electric-ax/agents-server-ui": "0.4.
|
|
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
|
|
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
|