@synapsor/runner 1.6.2 → 1.6.3
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/CHANGELOG.md +77 -2
- package/README.md +33 -19
- package/SECURITY.md +10 -2
- package/THREAT_MODEL.md +27 -4
- package/dist/authoring.mjs +53 -3
- package/dist/cli.d.ts +19 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/local-ui.d.ts +85 -1
- package/dist/local-ui.d.ts.map +1 -1
- package/dist/runner.mjs +36107 -22949
- package/dist/runtime.mjs +3581 -237
- package/dist/shadow.mjs +2423 -65
- package/docs/README.md +22 -2
- package/docs/agent-guided-setup.md +239 -0
- package/docs/approval-roles-and-operator-identity.md +353 -0
- package/docs/auto-boundary-and-scoped-explore.md +32 -6
- package/docs/capability-authoring.md +19 -0
- package/docs/current-scope.md +20 -1
- package/docs/dsl-json-parity.md +75 -0
- package/docs/dsl-reference.md +10 -0
- package/docs/fresh-developer-usability.md +155 -80
- package/docs/getting-started-own-database.md +5 -4
- package/docs/guarded-crud-writeback.md +19 -0
- package/docs/guided-onboarding.md +331 -0
- package/docs/human-attention-notifications.md +367 -0
- package/docs/limitations.md +19 -3
- package/docs/local-mode.md +16 -2
- package/docs/migrating-to-synapsor-spec.md +38 -0
- package/docs/production.md +39 -0
- package/docs/release-notes.md +66 -2
- package/docs/runner-config-reference.md +132 -0
- package/docs/running-a-runner-fleet.md +16 -1
- package/docs/security-boundary.md +24 -0
- package/docs/store-lifecycle.md +20 -1
- package/docs/supervised-automatic-apply.md +267 -0
- package/docs/troubleshooting-first-run.md +35 -0
- package/examples/fitflow-guided-onboarding/README.md +43 -0
- package/examples/fitflow-guided-onboarding/app/page.tsx +8 -0
- package/examples/fitflow-guided-onboarding/docker-compose.yml +16 -0
- package/examples/fitflow-guided-onboarding/package.json +18 -0
- package/examples/fitflow-guided-onboarding/prisma/schema.prisma +98 -0
- package/examples/fitflow-guided-onboarding/seed/postgres.sql +401 -0
- package/examples/operator-oidc/issuer.mjs +188 -0
- package/examples/support-plan-credit/README.md +6 -2
- package/package.json +5 -3
- package/schemas/schema-candidate-review.schema.json +42 -0
- package/schemas/synapsor.runner.schema.json +227 -4
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
DO $$
|
|
2
|
+
BEGIN
|
|
3
|
+
CREATE ROLE fitflow_analytics_reader LOGIN PASSWORD 'fitflow_analytics_reader_password';
|
|
4
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
5
|
+
END
|
|
6
|
+
$$;
|
|
7
|
+
|
|
8
|
+
DO $$
|
|
9
|
+
BEGIN
|
|
10
|
+
CREATE ROLE fitflow_trainer_reader LOGIN PASSWORD 'fitflow_trainer_reader_password';
|
|
11
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
12
|
+
END
|
|
13
|
+
$$;
|
|
14
|
+
|
|
15
|
+
DO $$
|
|
16
|
+
BEGIN
|
|
17
|
+
CREATE ROLE fitflow_writer LOGIN PASSWORD 'fitflow_writer_password';
|
|
18
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
19
|
+
END
|
|
20
|
+
$$;
|
|
21
|
+
|
|
22
|
+
DO $$
|
|
23
|
+
BEGIN
|
|
24
|
+
CREATE ROLE fitflow_setup LOGIN PASSWORD 'fitflow_setup_password';
|
|
25
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
26
|
+
END
|
|
27
|
+
$$;
|
|
28
|
+
|
|
29
|
+
ALTER ROLE fitflow_analytics_reader SET default_transaction_read_only = on;
|
|
30
|
+
ALTER ROLE fitflow_trainer_reader SET default_transaction_read_only = on;
|
|
31
|
+
|
|
32
|
+
CREATE TABLE public.organizations (
|
|
33
|
+
id text PRIMARY KEY,
|
|
34
|
+
name text NOT NULL
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
CREATE TABLE public.locations (
|
|
38
|
+
id text PRIMARY KEY,
|
|
39
|
+
organization_id text NOT NULL REFERENCES public.organizations(id) ON DELETE RESTRICT,
|
|
40
|
+
name text NOT NULL,
|
|
41
|
+
region text NOT NULL CHECK (region IN ('central', 'east', 'north', 'south', 'west')),
|
|
42
|
+
UNIQUE (organization_id, name)
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
CREATE TABLE public.trainers (
|
|
46
|
+
id text PRIMARY KEY,
|
|
47
|
+
organization_id text NOT NULL REFERENCES public.organizations(id) ON DELETE RESTRICT,
|
|
48
|
+
display_name text NOT NULL
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
CREATE TABLE public.members (
|
|
52
|
+
id text PRIMARY KEY,
|
|
53
|
+
organization_id text NOT NULL REFERENCES public.organizations(id) ON DELETE RESTRICT,
|
|
54
|
+
location_id text NOT NULL REFERENCES public.locations(id) ON DELETE RESTRICT,
|
|
55
|
+
assigned_trainer_id text NOT NULL REFERENCES public.trainers(id) ON DELETE RESTRICT,
|
|
56
|
+
membership_status text NOT NULL CHECK (membership_status IN ('active', 'frozen', 'cancelled')),
|
|
57
|
+
membership_tier text NOT NULL CHECK (membership_tier IN ('basic', 'plus', 'elite')),
|
|
58
|
+
loyalty_balance integer NOT NULL CHECK (loyalty_balance BETWEEN 0 AND 10000),
|
|
59
|
+
version integer NOT NULL DEFAULT 1 CHECK (version >= 1),
|
|
60
|
+
payment_method text NOT NULL,
|
|
61
|
+
home_address text NOT NULL,
|
|
62
|
+
medical_waiver_notes text NOT NULL
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
CREATE INDEX members_trusted_scope_idx
|
|
66
|
+
ON public.members(organization_id, assigned_trainer_id);
|
|
67
|
+
|
|
68
|
+
CREATE TABLE public.classes (
|
|
69
|
+
id text PRIMARY KEY,
|
|
70
|
+
organization_id text NOT NULL REFERENCES public.organizations(id) ON DELETE RESTRICT,
|
|
71
|
+
location_id text NOT NULL REFERENCES public.locations(id) ON DELETE RESTRICT,
|
|
72
|
+
trainer_id text NOT NULL REFERENCES public.trainers(id) ON DELETE RESTRICT,
|
|
73
|
+
class_type text NOT NULL CHECK (class_type IN ('cycling', 'strength', 'yoga')),
|
|
74
|
+
starts_at timestamptz NOT NULL
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
CREATE TABLE public.check_ins (
|
|
78
|
+
id text PRIMARY KEY,
|
|
79
|
+
organization_id text NOT NULL REFERENCES public.organizations(id) ON DELETE RESTRICT,
|
|
80
|
+
location_id text NOT NULL REFERENCES public.locations(id) ON DELETE RESTRICT,
|
|
81
|
+
member_id text NOT NULL REFERENCES public.members(id) ON DELETE RESTRICT,
|
|
82
|
+
class_id text NOT NULL REFERENCES public.classes(id) ON DELETE RESTRICT,
|
|
83
|
+
outcome text NOT NULL CHECK (outcome IN ('attended', 'late_cancel', 'no_show')),
|
|
84
|
+
checked_in_at timestamptz NOT NULL
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
CREATE INDEX check_ins_tenant_time_idx
|
|
88
|
+
ON public.check_ins(organization_id, checked_in_at);
|
|
89
|
+
|
|
90
|
+
-- A realistic application has much more schema than the first agent pack
|
|
91
|
+
-- needs. These metadata-only subsystems exercise whole-application review
|
|
92
|
+
-- without adding irrelevant rows to the onboarding result.
|
|
93
|
+
DO $$
|
|
94
|
+
DECLARE
|
|
95
|
+
relation_name text;
|
|
96
|
+
BEGIN
|
|
97
|
+
FOREACH relation_name IN ARRAY ARRAY[
|
|
98
|
+
'plans',
|
|
99
|
+
'membership_events',
|
|
100
|
+
'membership_freezes',
|
|
101
|
+
'membership_cancellations',
|
|
102
|
+
'class_bookings',
|
|
103
|
+
'class_waitlists',
|
|
104
|
+
'attendance_adjustments',
|
|
105
|
+
'trainer_certifications',
|
|
106
|
+
'trainer_schedules',
|
|
107
|
+
'staff_users',
|
|
108
|
+
'staff_roles',
|
|
109
|
+
'location_hours',
|
|
110
|
+
'rooms',
|
|
111
|
+
'equipment',
|
|
112
|
+
'equipment_maintenance',
|
|
113
|
+
'workout_programs',
|
|
114
|
+
'workout_exercises',
|
|
115
|
+
'member_goals',
|
|
116
|
+
'body_metrics',
|
|
117
|
+
'health_flags',
|
|
118
|
+
'waivers',
|
|
119
|
+
'billing_accounts',
|
|
120
|
+
'payment_methods',
|
|
121
|
+
'invoices',
|
|
122
|
+
'invoice_items',
|
|
123
|
+
'payments',
|
|
124
|
+
'refunds',
|
|
125
|
+
'promo_codes',
|
|
126
|
+
'member_promotions',
|
|
127
|
+
'leads',
|
|
128
|
+
'referrals',
|
|
129
|
+
'campaigns',
|
|
130
|
+
'notification_preferences',
|
|
131
|
+
'support_tickets'
|
|
132
|
+
]
|
|
133
|
+
LOOP
|
|
134
|
+
EXECUTE format(
|
|
135
|
+
'CREATE TABLE public.%I (
|
|
136
|
+
id text PRIMARY KEY,
|
|
137
|
+
organization_id text NOT NULL REFERENCES public.organizations(id) ON DELETE RESTRICT,
|
|
138
|
+
status text NOT NULL,
|
|
139
|
+
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
|
140
|
+
)',
|
|
141
|
+
relation_name
|
|
142
|
+
);
|
|
143
|
+
END LOOP;
|
|
144
|
+
END
|
|
145
|
+
$$;
|
|
146
|
+
|
|
147
|
+
INSERT INTO public.organizations (id, name) VALUES
|
|
148
|
+
('org-fitflow', 'FitFlow'),
|
|
149
|
+
('org-other', 'Other Fitness');
|
|
150
|
+
|
|
151
|
+
INSERT INTO public.locations (id, organization_id, name, region) VALUES
|
|
152
|
+
('loc-downtown', 'org-fitflow', 'Downtown', 'central'),
|
|
153
|
+
('loc-east', 'org-fitflow', 'Eastside', 'east'),
|
|
154
|
+
('loc-north', 'org-fitflow', 'North Loop', 'north'),
|
|
155
|
+
('loc-other', 'org-other', 'Other Downtown', 'west');
|
|
156
|
+
|
|
157
|
+
INSERT INTO public.trainers (id, organization_id, display_name) VALUES
|
|
158
|
+
('trainer-alex', 'org-fitflow', 'Alex'),
|
|
159
|
+
('trainer-jordan', 'org-fitflow', 'Jordan'),
|
|
160
|
+
('trainer-other', 'org-other', 'Other Trainer');
|
|
161
|
+
|
|
162
|
+
WITH member_rows AS (
|
|
163
|
+
SELECT
|
|
164
|
+
item,
|
|
165
|
+
CASE
|
|
166
|
+
WHEN item <= 12 THEN 'loc-downtown'
|
|
167
|
+
WHEN item <= 22 THEN 'loc-east'
|
|
168
|
+
ELSE 'loc-north'
|
|
169
|
+
END AS location_id,
|
|
170
|
+
CASE WHEN item % 2 = 0 THEN 'trainer-alex' ELSE 'trainer-jordan' END AS trainer_id,
|
|
171
|
+
CASE WHEN item % 7 = 0 THEN 'frozen' ELSE 'active' END AS status,
|
|
172
|
+
CASE WHEN item % 3 = 0 THEN 'elite' WHEN item % 2 = 0 THEN 'plus' ELSE 'basic' END AS tier
|
|
173
|
+
FROM generate_series(1, 30) AS item
|
|
174
|
+
)
|
|
175
|
+
INSERT INTO public.members (
|
|
176
|
+
id,
|
|
177
|
+
organization_id,
|
|
178
|
+
location_id,
|
|
179
|
+
assigned_trainer_id,
|
|
180
|
+
membership_status,
|
|
181
|
+
membership_tier,
|
|
182
|
+
loyalty_balance,
|
|
183
|
+
version,
|
|
184
|
+
payment_method,
|
|
185
|
+
home_address,
|
|
186
|
+
medical_waiver_notes
|
|
187
|
+
)
|
|
188
|
+
SELECT
|
|
189
|
+
'member-' || lpad(item::text, 3, '0'),
|
|
190
|
+
'org-fitflow',
|
|
191
|
+
location_id,
|
|
192
|
+
trainer_id,
|
|
193
|
+
status,
|
|
194
|
+
tier,
|
|
195
|
+
item * 10,
|
|
196
|
+
1,
|
|
197
|
+
'synthetic-card-token-' || item,
|
|
198
|
+
item || ' Synthetic Street',
|
|
199
|
+
'synthetic private medical note ' || item
|
|
200
|
+
FROM member_rows;
|
|
201
|
+
|
|
202
|
+
INSERT INTO public.members (
|
|
203
|
+
id,
|
|
204
|
+
organization_id,
|
|
205
|
+
location_id,
|
|
206
|
+
assigned_trainer_id,
|
|
207
|
+
membership_status,
|
|
208
|
+
membership_tier,
|
|
209
|
+
loyalty_balance,
|
|
210
|
+
version,
|
|
211
|
+
payment_method,
|
|
212
|
+
home_address,
|
|
213
|
+
medical_waiver_notes
|
|
214
|
+
) VALUES (
|
|
215
|
+
'other-member-001',
|
|
216
|
+
'org-other',
|
|
217
|
+
'loc-other',
|
|
218
|
+
'trainer-other',
|
|
219
|
+
'active',
|
|
220
|
+
'elite',
|
|
221
|
+
9999,
|
|
222
|
+
1,
|
|
223
|
+
'other-secret-payment',
|
|
224
|
+
'Other Tenant Address',
|
|
225
|
+
'other tenant private medical note'
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
INSERT INTO public.classes (id, organization_id, location_id, trainer_id, class_type, starts_at) VALUES
|
|
229
|
+
('class-downtown-w1', 'org-fitflow', 'loc-downtown', 'trainer-alex', 'strength', '2026-07-06T17:00:00Z'),
|
|
230
|
+
('class-downtown-w2', 'org-fitflow', 'loc-downtown', 'trainer-alex', 'cycling', '2026-07-13T17:00:00Z'),
|
|
231
|
+
('class-east-w1', 'org-fitflow', 'loc-east', 'trainer-jordan', 'yoga', '2026-07-06T18:00:00Z'),
|
|
232
|
+
('class-north-w2', 'org-fitflow', 'loc-north', 'trainer-jordan', 'strength', '2026-07-13T18:00:00Z'),
|
|
233
|
+
('class-other', 'org-other', 'loc-other', 'trainer-other', 'cycling', '2026-07-13T19:00:00Z');
|
|
234
|
+
|
|
235
|
+
WITH groups(prefix, organization_id, location_id, class_id, outcome, checked_in_at, first_member, group_size) AS (
|
|
236
|
+
VALUES
|
|
237
|
+
('downtown-w1', 'org-fitflow', 'loc-downtown', 'class-downtown-w1', 'attended', '2026-07-06T17:00:00Z'::timestamptz, 1, 6),
|
|
238
|
+
('downtown-w2', 'org-fitflow', 'loc-downtown', 'class-downtown-w2', 'attended', '2026-07-13T17:00:00Z'::timestamptz, 1, 9),
|
|
239
|
+
('east-w1', 'org-fitflow', 'loc-east', 'class-east-w1', 'late_cancel', '2026-07-06T18:00:00Z'::timestamptz, 13, 5),
|
|
240
|
+
('north-w2', 'org-fitflow', 'loc-north', 'class-north-w2', 'no_show', '2026-07-13T18:00:00Z'::timestamptz, 23, 2)
|
|
241
|
+
)
|
|
242
|
+
INSERT INTO public.check_ins (
|
|
243
|
+
id,
|
|
244
|
+
organization_id,
|
|
245
|
+
location_id,
|
|
246
|
+
member_id,
|
|
247
|
+
class_id,
|
|
248
|
+
outcome,
|
|
249
|
+
checked_in_at
|
|
250
|
+
)
|
|
251
|
+
SELECT
|
|
252
|
+
'checkin-' || prefix || '-' || item,
|
|
253
|
+
organization_id,
|
|
254
|
+
location_id,
|
|
255
|
+
'member-' || lpad((first_member + item - 1)::text, 3, '0'),
|
|
256
|
+
class_id,
|
|
257
|
+
outcome,
|
|
258
|
+
checked_in_at
|
|
259
|
+
FROM groups
|
|
260
|
+
CROSS JOIN LATERAL generate_series(1, group_size) AS item;
|
|
261
|
+
|
|
262
|
+
INSERT INTO public.check_ins (
|
|
263
|
+
id,
|
|
264
|
+
organization_id,
|
|
265
|
+
location_id,
|
|
266
|
+
member_id,
|
|
267
|
+
class_id,
|
|
268
|
+
outcome,
|
|
269
|
+
checked_in_at
|
|
270
|
+
)
|
|
271
|
+
SELECT
|
|
272
|
+
'other-checkin-' || item,
|
|
273
|
+
'org-other',
|
|
274
|
+
'loc-other',
|
|
275
|
+
'other-member-001',
|
|
276
|
+
'class-other',
|
|
277
|
+
'attended',
|
|
278
|
+
'2026-07-13T19:00:00Z'::timestamptz
|
|
279
|
+
FROM generate_series(1, 8) AS item;
|
|
280
|
+
|
|
281
|
+
ALTER TABLE public.organizations ENABLE ROW LEVEL SECURITY;
|
|
282
|
+
ALTER TABLE public.organizations FORCE ROW LEVEL SECURITY;
|
|
283
|
+
ALTER TABLE public.locations ENABLE ROW LEVEL SECURITY;
|
|
284
|
+
ALTER TABLE public.locations FORCE ROW LEVEL SECURITY;
|
|
285
|
+
ALTER TABLE public.trainers ENABLE ROW LEVEL SECURITY;
|
|
286
|
+
ALTER TABLE public.trainers FORCE ROW LEVEL SECURITY;
|
|
287
|
+
ALTER TABLE public.members ENABLE ROW LEVEL SECURITY;
|
|
288
|
+
ALTER TABLE public.members FORCE ROW LEVEL SECURITY;
|
|
289
|
+
ALTER TABLE public.classes ENABLE ROW LEVEL SECURITY;
|
|
290
|
+
ALTER TABLE public.classes FORCE ROW LEVEL SECURITY;
|
|
291
|
+
ALTER TABLE public.check_ins ENABLE ROW LEVEL SECURITY;
|
|
292
|
+
ALTER TABLE public.check_ins FORCE ROW LEVEL SECURITY;
|
|
293
|
+
|
|
294
|
+
CREATE POLICY organizations_org_read ON public.organizations
|
|
295
|
+
FOR SELECT TO fitflow_analytics_reader, fitflow_trainer_reader, fitflow_writer
|
|
296
|
+
USING (id = current_setting('app.tenant_id', true));
|
|
297
|
+
|
|
298
|
+
CREATE POLICY locations_org_read ON public.locations
|
|
299
|
+
FOR SELECT TO fitflow_analytics_reader, fitflow_trainer_reader, fitflow_writer
|
|
300
|
+
USING (organization_id = current_setting('app.tenant_id', true));
|
|
301
|
+
|
|
302
|
+
CREATE POLICY trainers_org_read ON public.trainers
|
|
303
|
+
FOR SELECT TO fitflow_analytics_reader, fitflow_trainer_reader, fitflow_writer
|
|
304
|
+
USING (organization_id = current_setting('app.tenant_id', true));
|
|
305
|
+
|
|
306
|
+
CREATE POLICY members_analytics_read ON public.members
|
|
307
|
+
FOR SELECT TO fitflow_analytics_reader
|
|
308
|
+
USING (organization_id = current_setting('app.tenant_id', true));
|
|
309
|
+
|
|
310
|
+
CREATE POLICY members_trainer_read ON public.members
|
|
311
|
+
FOR SELECT TO fitflow_trainer_reader, fitflow_writer
|
|
312
|
+
USING (
|
|
313
|
+
organization_id = current_setting('app.tenant_id', true)
|
|
314
|
+
AND assigned_trainer_id = current_setting('app.principal', true)
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
CREATE POLICY members_guarded_update ON public.members
|
|
318
|
+
FOR UPDATE TO fitflow_writer
|
|
319
|
+
USING (
|
|
320
|
+
organization_id = current_setting('app.tenant_id', true)
|
|
321
|
+
AND assigned_trainer_id = current_setting('app.principal', true)
|
|
322
|
+
)
|
|
323
|
+
WITH CHECK (
|
|
324
|
+
organization_id = current_setting('app.tenant_id', true)
|
|
325
|
+
AND assigned_trainer_id = current_setting('app.principal', true)
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
CREATE POLICY classes_org_read ON public.classes
|
|
329
|
+
FOR SELECT TO fitflow_analytics_reader, fitflow_trainer_reader, fitflow_writer
|
|
330
|
+
USING (organization_id = current_setting('app.tenant_id', true));
|
|
331
|
+
|
|
332
|
+
CREATE POLICY check_ins_org_read ON public.check_ins
|
|
333
|
+
FOR SELECT TO fitflow_analytics_reader
|
|
334
|
+
USING (organization_id = current_setting('app.tenant_id', true));
|
|
335
|
+
|
|
336
|
+
DO $$
|
|
337
|
+
DECLARE
|
|
338
|
+
relation_name text;
|
|
339
|
+
BEGIN
|
|
340
|
+
FOREACH relation_name IN ARRAY ARRAY[
|
|
341
|
+
'plans',
|
|
342
|
+
'membership_events',
|
|
343
|
+
'membership_freezes',
|
|
344
|
+
'membership_cancellations',
|
|
345
|
+
'class_bookings',
|
|
346
|
+
'class_waitlists',
|
|
347
|
+
'attendance_adjustments',
|
|
348
|
+
'trainer_certifications',
|
|
349
|
+
'trainer_schedules',
|
|
350
|
+
'staff_users',
|
|
351
|
+
'staff_roles',
|
|
352
|
+
'location_hours',
|
|
353
|
+
'rooms',
|
|
354
|
+
'equipment',
|
|
355
|
+
'equipment_maintenance',
|
|
356
|
+
'workout_programs',
|
|
357
|
+
'workout_exercises',
|
|
358
|
+
'member_goals',
|
|
359
|
+
'body_metrics',
|
|
360
|
+
'health_flags',
|
|
361
|
+
'waivers',
|
|
362
|
+
'billing_accounts',
|
|
363
|
+
'payment_methods',
|
|
364
|
+
'invoices',
|
|
365
|
+
'invoice_items',
|
|
366
|
+
'payments',
|
|
367
|
+
'refunds',
|
|
368
|
+
'promo_codes',
|
|
369
|
+
'member_promotions',
|
|
370
|
+
'leads',
|
|
371
|
+
'referrals',
|
|
372
|
+
'campaigns',
|
|
373
|
+
'notification_preferences',
|
|
374
|
+
'support_tickets'
|
|
375
|
+
]
|
|
376
|
+
LOOP
|
|
377
|
+
EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', relation_name);
|
|
378
|
+
EXECUTE format('ALTER TABLE public.%I FORCE ROW LEVEL SECURITY', relation_name);
|
|
379
|
+
EXECUTE format(
|
|
380
|
+
'CREATE POLICY %I ON public.%I
|
|
381
|
+
FOR SELECT TO fitflow_analytics_reader
|
|
382
|
+
USING (organization_id = current_setting(''app.tenant_id'', true))',
|
|
383
|
+
relation_name || '_org_read',
|
|
384
|
+
relation_name
|
|
385
|
+
);
|
|
386
|
+
EXECUTE format('GRANT SELECT ON public.%I TO fitflow_analytics_reader', relation_name);
|
|
387
|
+
END LOOP;
|
|
388
|
+
END
|
|
389
|
+
$$;
|
|
390
|
+
|
|
391
|
+
GRANT CONNECT ON DATABASE fitflow TO fitflow_analytics_reader, fitflow_trainer_reader, fitflow_writer, fitflow_setup;
|
|
392
|
+
GRANT USAGE ON SCHEMA public TO fitflow_analytics_reader, fitflow_trainer_reader, fitflow_writer, fitflow_setup;
|
|
393
|
+
GRANT SELECT ON public.organizations, public.locations, public.trainers, public.members, public.classes, public.check_ins TO fitflow_analytics_reader;
|
|
394
|
+
GRANT SELECT ON public.organizations, public.locations, public.trainers, public.members, public.classes TO fitflow_trainer_reader;
|
|
395
|
+
GRANT SELECT, UPDATE (membership_status, loyalty_balance, version) ON public.members TO fitflow_writer;
|
|
396
|
+
GRANT CREATE ON SCHEMA public TO fitflow_setup;
|
|
397
|
+
|
|
398
|
+
CREATE TABLE public.synapsor_fixture_ready (
|
|
399
|
+
initialized_at timestamptz NOT NULL
|
|
400
|
+
);
|
|
401
|
+
INSERT INTO public.synapsor_fixture_ready (initialized_at) VALUES (clock_timestamp());
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import http from "node:http";
|
|
5
|
+
|
|
6
|
+
const issuer = process.env.SYNAPSOR_EXAMPLE_OIDC_ISSUER
|
|
7
|
+
?? "https://identity.example.test/oidc";
|
|
8
|
+
const audience = process.env.SYNAPSOR_EXAMPLE_OIDC_AUDIENCE
|
|
9
|
+
?? "synapsor-operators";
|
|
10
|
+
const host = "127.0.0.1";
|
|
11
|
+
const requestedPort = Number(process.env.SYNAPSOR_EXAMPLE_OIDC_PORT ?? "0");
|
|
12
|
+
|
|
13
|
+
const primary = rsaSigner("fitflow-key-1");
|
|
14
|
+
const rotated = rsaSigner("fitflow-key-2");
|
|
15
|
+
const untrusted = rsaSigner("untrusted-key");
|
|
16
|
+
let rotationEnabled = false;
|
|
17
|
+
|
|
18
|
+
const tokenCases = {
|
|
19
|
+
reviewer: () => token(primary, {
|
|
20
|
+
sub: "reviewer@example.test",
|
|
21
|
+
groups: ["membership_reviewer"],
|
|
22
|
+
}),
|
|
23
|
+
applier: () => token(rotated, {
|
|
24
|
+
sub: "writeback@example.test",
|
|
25
|
+
groups: ["writeback_operator"],
|
|
26
|
+
}),
|
|
27
|
+
missing_role: () => token(primary, {
|
|
28
|
+
sub: "trainer@example.test",
|
|
29
|
+
groups: ["trainer"],
|
|
30
|
+
}),
|
|
31
|
+
similar_role: () => token(primary, {
|
|
32
|
+
sub: "reviewer-backup@example.test",
|
|
33
|
+
groups: ["membership_reviewer_backup"],
|
|
34
|
+
}),
|
|
35
|
+
bad_signature: () => token(untrusted, {
|
|
36
|
+
sub: "attacker@example.test",
|
|
37
|
+
groups: ["membership_reviewer"],
|
|
38
|
+
}, { kid: primary.kid }),
|
|
39
|
+
unknown_key: () => token(untrusted, {
|
|
40
|
+
sub: "unknown-key@example.test",
|
|
41
|
+
groups: ["membership_reviewer"],
|
|
42
|
+
}),
|
|
43
|
+
expired: () => token(primary, {
|
|
44
|
+
sub: "expired@example.test",
|
|
45
|
+
groups: ["membership_reviewer"],
|
|
46
|
+
}, { expiresAt: epoch() - 60 }),
|
|
47
|
+
not_yet_valid: () => token(primary, {
|
|
48
|
+
sub: "future@example.test",
|
|
49
|
+
groups: ["membership_reviewer"],
|
|
50
|
+
}, { notBefore: epoch() + 600 }),
|
|
51
|
+
wrong_issuer: () => token(primary, {
|
|
52
|
+
sub: "wrong-issuer@example.test",
|
|
53
|
+
groups: ["membership_reviewer"],
|
|
54
|
+
iss: "https://wrong.example.test/oidc",
|
|
55
|
+
}),
|
|
56
|
+
wrong_audience: () => token(primary, {
|
|
57
|
+
sub: "wrong-audience@example.test",
|
|
58
|
+
groups: ["membership_reviewer"],
|
|
59
|
+
aud: "another-service",
|
|
60
|
+
}),
|
|
61
|
+
unsafe_subject: () => token(primary, {
|
|
62
|
+
sub: "unsafe subject with spaces",
|
|
63
|
+
groups: ["membership_reviewer"],
|
|
64
|
+
}),
|
|
65
|
+
malformed_roles: () => token(primary, {
|
|
66
|
+
sub: "malformed-roles@example.test",
|
|
67
|
+
groups: { membership_reviewer: true },
|
|
68
|
+
}),
|
|
69
|
+
missing_expiry: () => token(primary, {
|
|
70
|
+
sub: "missing-expiry@example.test",
|
|
71
|
+
groups: ["membership_reviewer"],
|
|
72
|
+
}, { omitExpiry: true }),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const server = http.createServer((request, response) => {
|
|
76
|
+
const url = new URL(request.url ?? "/", `http://${host}`);
|
|
77
|
+
response.setHeader("cache-control", "no-store");
|
|
78
|
+
|
|
79
|
+
if (request.method === "GET" && url.pathname === "/jwks") {
|
|
80
|
+
return json(response, 200, {
|
|
81
|
+
keys: rotationEnabled ? [primary.publicJwk, rotated.publicJwk] : [primary.publicJwk],
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (request.method === "POST" && url.pathname === "/rotate") {
|
|
86
|
+
rotationEnabled = true;
|
|
87
|
+
return json(response, 200, {
|
|
88
|
+
ok: true,
|
|
89
|
+
active_kids: [primary.kid, rotated.kid],
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const match = request.method === "GET"
|
|
94
|
+
? url.pathname.match(/^\/token\/([a-z_]+)$/)
|
|
95
|
+
: undefined;
|
|
96
|
+
const mint = match ? tokenCases[match[1]] : undefined;
|
|
97
|
+
if (mint) {
|
|
98
|
+
return json(response, 200, {
|
|
99
|
+
access_token: mint(),
|
|
100
|
+
token_type: "Bearer",
|
|
101
|
+
expires_in: 300,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return json(response, 404, { error: "not_found" });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
server.listen(requestedPort, host, () => {
|
|
109
|
+
const address = server.address();
|
|
110
|
+
if (!address || typeof address === "string") {
|
|
111
|
+
process.stderr.write("OIDC fixture did not bind a TCP port.\n");
|
|
112
|
+
process.exitCode = 1;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
process.stdout.write(`${JSON.stringify({
|
|
116
|
+
fixture: "synapsor.operator-oidc.v1",
|
|
117
|
+
base_url: `http://${host}:${address.port}`,
|
|
118
|
+
jwks_url: `http://${host}:${address.port}/jwks`,
|
|
119
|
+
issuer,
|
|
120
|
+
audience,
|
|
121
|
+
approval_role: "membership_reviewer",
|
|
122
|
+
apply_role: "writeback_operator",
|
|
123
|
+
group_mapping: {
|
|
124
|
+
"example-membership-reviewers": "membership_reviewer",
|
|
125
|
+
"example-writeback-operators": "writeback_operator",
|
|
126
|
+
},
|
|
127
|
+
})}\n`);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
131
|
+
process.on(signal, () => {
|
|
132
|
+
server.close(() => process.exit(0));
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function rsaSigner(kid) {
|
|
137
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", {
|
|
138
|
+
modulusLength: 2048,
|
|
139
|
+
publicExponent: 0x10001,
|
|
140
|
+
});
|
|
141
|
+
return {
|
|
142
|
+
kid,
|
|
143
|
+
privateKey,
|
|
144
|
+
publicJwk: {
|
|
145
|
+
...publicKey.export({ format: "jwk" }),
|
|
146
|
+
kid,
|
|
147
|
+
alg: "RS256",
|
|
148
|
+
use: "sig",
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function token(signer, claims, options = {}) {
|
|
154
|
+
const now = epoch();
|
|
155
|
+
const header = {
|
|
156
|
+
alg: "RS256",
|
|
157
|
+
kid: options.kid ?? signer.kid,
|
|
158
|
+
typ: "JWT",
|
|
159
|
+
};
|
|
160
|
+
const payload = {
|
|
161
|
+
iss: issuer,
|
|
162
|
+
aud: audience,
|
|
163
|
+
iat: now,
|
|
164
|
+
nbf: options.notBefore ?? now - 1,
|
|
165
|
+
...(!options.omitExpiry ? { exp: options.expiresAt ?? now + 300 } : {}),
|
|
166
|
+
...claims,
|
|
167
|
+
};
|
|
168
|
+
const signingInput = `${encodeJson(header)}.${encodeJson(payload)}`;
|
|
169
|
+
const signature = crypto.sign("RSA-SHA256", Buffer.from(signingInput), signer.privateKey);
|
|
170
|
+
return `${signingInput}.${signature.toString("base64url")}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function encodeJson(value) {
|
|
174
|
+
return Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function epoch() {
|
|
178
|
+
return Math.floor(Date.now() / 1000);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function json(response, status, value) {
|
|
182
|
+
const body = JSON.stringify(value);
|
|
183
|
+
response.writeHead(status, {
|
|
184
|
+
"content-type": "application/json",
|
|
185
|
+
"content-length": Buffer.byteLength(body),
|
|
186
|
+
});
|
|
187
|
+
response.end(body);
|
|
188
|
+
}
|
|
@@ -298,8 +298,12 @@ To disable local policy approval without changing the contract:
|
|
|
298
298
|
}
|
|
299
299
|
```
|
|
300
300
|
|
|
301
|
-
|
|
302
|
-
|
|
301
|
+
In this example, auto-approval records an approval row and audit event with
|
|
302
|
+
actor `policy:<policy_name>`; apply remains manual. Runner does not start
|
|
303
|
+
automatic execution merely because a contract uses `AUTO APPROVE`. A separately
|
|
304
|
+
trusted supervised worker can apply an eligible proposal only when the contract
|
|
305
|
+
and deployment independently opt into the exact active digest. See
|
|
306
|
+
[`docs/supervised-automatic-apply.md`](../../docs/supervised-automatic-apply.md).
|
|
303
307
|
|
|
304
308
|
After reviewing the ceilings and running `doctor`, an operator can drain the
|
|
305
309
|
approved queue without letting one stale-row conflict abort the remaining jobs:
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synapsor/runner",
|
|
3
|
-
"version": "1.6.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.6.3",
|
|
4
|
+
"description": "Open-source MCP runtime for governed Postgres/MySQL access with reviewed capabilities, proposals, guarded writes, receipts, and replay.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -41,10 +41,12 @@
|
|
|
41
41
|
"examples/dangerous-mcp-tools.json",
|
|
42
42
|
"examples/app-owned-writeback/**",
|
|
43
43
|
"examples/auto-boundary-churn/**",
|
|
44
|
+
"examples/fitflow-guided-onboarding/**",
|
|
44
45
|
"examples/claude-desktop-postgres/**",
|
|
45
46
|
"examples/cursor-postgres/**",
|
|
46
47
|
"examples/mcp-postgres-billing-app-handler/**",
|
|
47
48
|
"examples/mysql-refund-agent/**",
|
|
49
|
+
"examples/operator-oidc/**",
|
|
48
50
|
"examples/openai-agents-http/**",
|
|
49
51
|
"examples/openai-agents-stdio/**",
|
|
50
52
|
"examples/raw-sql-vs-synapsor/**",
|
|
@@ -85,7 +87,7 @@
|
|
|
85
87
|
},
|
|
86
88
|
"dependencies": {
|
|
87
89
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
88
|
-
"@synapsor/spec": "^1.
|
|
90
|
+
"@synapsor/spec": "^1.6.0",
|
|
89
91
|
"jose": "6.2.3",
|
|
90
92
|
"mysql2": "^3.11.0",
|
|
91
93
|
"pdf-lib": "1.17.1",
|
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
"potential_principal_fields",
|
|
79
79
|
"potential_conflict_fields",
|
|
80
80
|
"potentially_sensitive_fields",
|
|
81
|
+
"field_classifications",
|
|
81
82
|
"suggested_kept_out_fields",
|
|
82
83
|
"suggested_visible_fields",
|
|
83
84
|
"possible_actions",
|
|
@@ -106,6 +107,47 @@
|
|
|
106
107
|
"potentially_sensitive_fields": {
|
|
107
108
|
"$ref": "#/$defs/stringArray"
|
|
108
109
|
},
|
|
110
|
+
"field_classifications": {
|
|
111
|
+
"type": "array",
|
|
112
|
+
"items": {
|
|
113
|
+
"type": "object",
|
|
114
|
+
"additionalProperties": false,
|
|
115
|
+
"required": [
|
|
116
|
+
"field",
|
|
117
|
+
"state",
|
|
118
|
+
"reason_codes",
|
|
119
|
+
"reasons",
|
|
120
|
+
"evidence_source"
|
|
121
|
+
],
|
|
122
|
+
"properties": {
|
|
123
|
+
"field": {
|
|
124
|
+
"type": "string"
|
|
125
|
+
},
|
|
126
|
+
"state": {
|
|
127
|
+
"enum": [
|
|
128
|
+
"high_confidence_sensitive",
|
|
129
|
+
"unresolved_free_text",
|
|
130
|
+
"structurally_low_risk"
|
|
131
|
+
]
|
|
132
|
+
},
|
|
133
|
+
"reason_codes": {
|
|
134
|
+
"$ref": "#/$defs/stringArray"
|
|
135
|
+
},
|
|
136
|
+
"reasons": {
|
|
137
|
+
"$ref": "#/$defs/stringArray"
|
|
138
|
+
},
|
|
139
|
+
"evidence_source": {
|
|
140
|
+
"enum": [
|
|
141
|
+
"database",
|
|
142
|
+
"prisma",
|
|
143
|
+
"drizzle",
|
|
144
|
+
"openapi",
|
|
145
|
+
"synapsor"
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
},
|
|
109
151
|
"suggested_kept_out_fields": {
|
|
110
152
|
"$ref": "#/$defs/stringArray"
|
|
111
153
|
},
|