@electric-ax/agents-server 0.4.15 → 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 +1176 -232
- package/dist/index.cjs +1179 -226
- package/dist/index.d.cts +1146 -167
- package/dist/index.d.ts +1146 -167
- package/dist/index.js +1181 -228
- 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 +21 -0
- package/package.json +7 -7
- package/src/db/schema.ts +198 -0
- package/src/electric-agents-types.ts +76 -2
- package/src/entity-bridge-manager.ts +57 -6
- package/src/entity-manager.ts +78 -60
- package/src/entity-projector.ts +76 -17
- package/src/entity-registry.ts +608 -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 +344 -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/runtime.ts +34 -0
- package/src/scheduler.ts +2 -0
- package/src/server.ts +5 -0
- package/src/utils/server-utils.ts +191 -11
- package/src/wake-registry.ts +8 -0
|
@@ -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
|
+
);
|
|
@@ -78,6 +78,27 @@
|
|
|
78
78
|
"when": 1779062400000,
|
|
79
79
|
"tag": "0010_sandbox_profiles",
|
|
80
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
|
|
81
102
|
}
|
|
82
103
|
]
|
|
83
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
|
@@ -72,6 +72,197 @@ export const entities = pgTable(
|
|
|
72
72
|
]
|
|
73
73
|
)
|
|
74
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
|
+
|
|
75
266
|
export const users = pgTable(
|
|
76
267
|
`users`,
|
|
77
268
|
{
|
|
@@ -436,6 +627,8 @@ export const entityBridges = pgTable(
|
|
|
436
627
|
sourceRef: text(`source_ref`).notNull(),
|
|
437
628
|
tags: jsonb(`tags`).notNull(),
|
|
438
629
|
streamUrl: text(`stream_url`).notNull(),
|
|
630
|
+
principalUrl: text(`principal_url`),
|
|
631
|
+
principalKind: text(`principal_kind`),
|
|
439
632
|
shapeHandle: text(`shape_handle`),
|
|
440
633
|
shapeOffset: text(`shape_offset`),
|
|
441
634
|
lastObserverActivityAt: timestamp(`last_observer_activity_at`, {
|
|
@@ -453,6 +646,11 @@ export const entityBridges = pgTable(
|
|
|
453
646
|
(table) => [
|
|
454
647
|
primaryKey({ columns: [table.tenantId, table.sourceRef] }),
|
|
455
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
|
+
),
|
|
456
654
|
]
|
|
457
655
|
)
|
|
458
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`,
|
|
@@ -356,7 +428,6 @@ export interface ElectricAgentsEntity {
|
|
|
356
428
|
status: EntityStatus
|
|
357
429
|
streams: {
|
|
358
430
|
main: string
|
|
359
|
-
error: string
|
|
360
431
|
}
|
|
361
432
|
subscription_id: string
|
|
362
433
|
dispatch_policy?: DispatchPolicy
|
|
@@ -385,7 +456,7 @@ export interface PublicElectricAgentsEntity {
|
|
|
385
456
|
url: string
|
|
386
457
|
type: string
|
|
387
458
|
status: EntityStatus
|
|
388
|
-
streams: { main: string
|
|
459
|
+
streams: { main: string }
|
|
389
460
|
dispatch_policy?: DispatchPolicy
|
|
390
461
|
tags: Record<string, string>
|
|
391
462
|
spawn_args?: Record<string, unknown>
|
|
@@ -443,6 +514,7 @@ export interface RegisterEntityTypeRequest {
|
|
|
443
514
|
state_schemas?: Record<string, Record<string, unknown>>
|
|
444
515
|
serve_endpoint?: string
|
|
445
516
|
default_dispatch_policy?: DispatchPolicy
|
|
517
|
+
permission_grants?: Array<EntityTypePermissionGrantInput>
|
|
446
518
|
}
|
|
447
519
|
|
|
448
520
|
export interface TypedSpawnRequest {
|
|
@@ -477,6 +549,8 @@ export interface TypedSpawnRequest {
|
|
|
477
549
|
|
|
478
550
|
export interface SendRequest {
|
|
479
551
|
from?: string
|
|
552
|
+
from_principal?: string
|
|
553
|
+
from_agent?: string
|
|
480
554
|
payload?: unknown
|
|
481
555
|
key?: string
|
|
482
556
|
type?: string
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
assertTags,
|
|
4
4
|
buildTagsIndex,
|
|
5
5
|
getEntitiesStreamPath,
|
|
6
|
+
hashString,
|
|
6
7
|
normalizeTags,
|
|
7
8
|
sourceRefForTags,
|
|
8
9
|
} from '@electric-ax/agents-runtime'
|
|
@@ -13,7 +14,9 @@ import {
|
|
|
13
14
|
} from '@electric-sql/client'
|
|
14
15
|
import { serverLog } from './utils/log.js'
|
|
15
16
|
import { electricUrlWithPath } from './utils/electric-url.js'
|
|
17
|
+
import { buildReadableEntitiesWhere } from './utils/server-utils.js'
|
|
16
18
|
import { DEFAULT_TENANT_ID } from './tenant.js'
|
|
19
|
+
import { isBuiltInSystemPrincipalUrl } from './principal.js'
|
|
17
20
|
import type { EntityBridgeRow, PostgresRegistry } from './entity-registry.js'
|
|
18
21
|
import type { StreamClient } from './stream-client.js'
|
|
19
22
|
import type {
|
|
@@ -30,7 +33,11 @@ import type {
|
|
|
30
33
|
export interface EntityBridgeCoordinator {
|
|
31
34
|
start(): Promise<void>
|
|
32
35
|
stop(): Promise<void>
|
|
33
|
-
register(
|
|
36
|
+
register(
|
|
37
|
+
tagsInput: unknown,
|
|
38
|
+
principalUrl: string,
|
|
39
|
+
principalKind: string
|
|
40
|
+
): Promise<{
|
|
34
41
|
sourceRef: string
|
|
35
42
|
streamUrl: string
|
|
36
43
|
}>
|
|
@@ -115,19 +122,44 @@ function sqlStringLiteral(value: string): string {
|
|
|
115
122
|
|
|
116
123
|
function buildTenantTagsWhereClause(
|
|
117
124
|
tenantId: string,
|
|
118
|
-
tags: EntityTags
|
|
125
|
+
tags: EntityTags,
|
|
126
|
+
principalUrl?: string,
|
|
127
|
+
principalKind?: string,
|
|
128
|
+
permissionBypass?: boolean
|
|
119
129
|
): string {
|
|
120
|
-
|
|
130
|
+
const readableWhere =
|
|
131
|
+
principalUrl && principalKind
|
|
132
|
+
? buildReadableEntitiesWhere({
|
|
133
|
+
tenantId,
|
|
134
|
+
principalUrl,
|
|
135
|
+
principalKind,
|
|
136
|
+
permissionBypass,
|
|
137
|
+
})
|
|
138
|
+
: `tenant_id = ${sqlStringLiteral(tenantId)} AND FALSE`
|
|
139
|
+
return `${readableWhere} AND (${buildTagsWhereClause(tags)})`
|
|
121
140
|
}
|
|
122
141
|
|
|
123
142
|
function shapeEntityKey(message: ChangeMessage<EntityShapeRow>): string {
|
|
124
143
|
return message.value.url
|
|
125
144
|
}
|
|
126
145
|
|
|
146
|
+
function principalScopedSourceRef(
|
|
147
|
+
tagSourceRef: string,
|
|
148
|
+
principalUrl: string,
|
|
149
|
+
principalKind: string
|
|
150
|
+
): string {
|
|
151
|
+
return `${tagSourceRef}-${hashString(
|
|
152
|
+
JSON.stringify({ principalKind, principalUrl })
|
|
153
|
+
)}`
|
|
154
|
+
}
|
|
155
|
+
|
|
127
156
|
class EntityBridge {
|
|
128
157
|
readonly sourceRef: string
|
|
129
158
|
readonly tags: EntityTags
|
|
130
159
|
readonly streamUrl: string
|
|
160
|
+
private readonly principalUrl?: string
|
|
161
|
+
private readonly principalKind?: string
|
|
162
|
+
private readonly permissionBypass: boolean
|
|
131
163
|
|
|
132
164
|
private currentMembers = new Map<string, EntityMembershipRow>()
|
|
133
165
|
private producer: IdempotentProducer | null = null
|
|
@@ -152,6 +184,9 @@ class EntityBridge {
|
|
|
152
184
|
this.sourceRef = row.sourceRef
|
|
153
185
|
this.tags = normalizeTags(row.tags)
|
|
154
186
|
this.streamUrl = row.streamUrl
|
|
187
|
+
this.principalUrl = row.principalUrl
|
|
188
|
+
this.principalKind = row.principalKind
|
|
189
|
+
this.permissionBypass = isBuiltInSystemPrincipalUrl(row.principalUrl)
|
|
155
190
|
this.initialShapeHandle = row.shapeHandle
|
|
156
191
|
this.initialShapeOffset = row.shapeOffset
|
|
157
192
|
}
|
|
@@ -316,7 +351,13 @@ class EntityBridge {
|
|
|
316
351
|
url: electricUrlWithPath(this.electricUrl, `/v1/shape`).toString(),
|
|
317
352
|
params: {
|
|
318
353
|
table: `entities`,
|
|
319
|
-
where: buildTenantTagsWhereClause(
|
|
354
|
+
where: buildTenantTagsWhereClause(
|
|
355
|
+
this.tenantId,
|
|
356
|
+
this.tags,
|
|
357
|
+
this.principalUrl,
|
|
358
|
+
this.principalKind,
|
|
359
|
+
this.permissionBypass
|
|
360
|
+
),
|
|
320
361
|
...(this.electricSecret ? { secret: this.electricSecret } : {}),
|
|
321
362
|
columns: [...ENTITY_SHAPE_COLUMNS],
|
|
322
363
|
replica: `full`,
|
|
@@ -564,7 +605,11 @@ export class EntityBridgeManager implements EntityBridgeCoordinator {
|
|
|
564
605
|
)
|
|
565
606
|
}
|
|
566
607
|
|
|
567
|
-
async register(
|
|
608
|
+
async register(
|
|
609
|
+
tagsInput: unknown,
|
|
610
|
+
principalUrl: string,
|
|
611
|
+
principalKind: string
|
|
612
|
+
): Promise<{
|
|
568
613
|
sourceRef: string
|
|
569
614
|
streamUrl: string
|
|
570
615
|
}> {
|
|
@@ -573,13 +618,19 @@ export class EntityBridgeManager implements EntityBridgeCoordinator {
|
|
|
573
618
|
}
|
|
574
619
|
|
|
575
620
|
const tags = normalizeTags(assertTags(tagsInput))
|
|
576
|
-
const sourceRef =
|
|
621
|
+
const sourceRef = principalScopedSourceRef(
|
|
622
|
+
sourceRefForTags(tags),
|
|
623
|
+
principalUrl,
|
|
624
|
+
principalKind
|
|
625
|
+
)
|
|
577
626
|
const streamUrl = getEntitiesStreamPath(sourceRef)
|
|
578
627
|
|
|
579
628
|
const row = await this.registry.upsertEntityBridge({
|
|
580
629
|
sourceRef,
|
|
581
630
|
tags,
|
|
582
631
|
streamUrl,
|
|
632
|
+
principalUrl,
|
|
633
|
+
principalKind,
|
|
583
634
|
})
|
|
584
635
|
await this.registry.touchEntityBridge(sourceRef)
|
|
585
636
|
await this.ensureBridge(row)
|