jac-scale 0.1.1__py3-none-any.whl → 0.1.3__py3-none-any.whl
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.
- jac_scale/abstractions/config/app_config.jac +5 -2
- jac_scale/context.jac +2 -1
- jac_scale/factories/storage_factory.jac +75 -0
- jac_scale/google_sso_provider.jac +85 -0
- jac_scale/impl/context.impl.jac +3 -0
- jac_scale/impl/serve.impl.jac +82 -234
- jac_scale/impl/user_manager.impl.jac +349 -0
- jac_scale/memory_hierarchy.jac +3 -1
- jac_scale/plugin.jac +46 -3
- jac_scale/plugin_config.jac +27 -0
- jac_scale/serve.jac +3 -12
- jac_scale/sso_provider.jac +72 -0
- jac_scale/targets/kubernetes/kubernetes_config.jac +9 -15
- jac_scale/targets/kubernetes/kubernetes_target.jac +174 -15
- jac_scale/tests/fixtures/scale-feats/components/Button.cl.jac +32 -0
- jac_scale/tests/fixtures/scale-feats/main.jac +147 -0
- jac_scale/tests/fixtures/test_api.jac +29 -0
- jac_scale/tests/fixtures/test_restspec.jac +37 -0
- jac_scale/tests/test_deploy_k8s.py +2 -1
- jac_scale/tests/test_examples.py +180 -5
- jac_scale/tests/test_hooks.py +39 -0
- jac_scale/tests/test_restspec.py +192 -0
- jac_scale/tests/test_serve.py +54 -0
- jac_scale/tests/test_sso.py +273 -284
- jac_scale/tests/test_storage.py +274 -0
- jac_scale/user_manager.jac +49 -0
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.3.dist-info}/METADATA +9 -2
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.3.dist-info}/RECORD +31 -20
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.3.dist-info}/WHEEL +1 -1
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.3.dist-info}/entry_points.txt +0 -0
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
impl JacScaleUserManager.postinit -> None {
|
|
2
|
+
super.postinit();
|
|
3
|
+
# Create SSO accounts table for tracking linked external identities
|
|
4
|
+
self._ensure_connection();
|
|
5
|
+
self._conn.execute(
|
|
6
|
+
"""
|
|
7
|
+
CREATE TABLE IF NOT EXISTS sso_accounts (
|
|
8
|
+
user_id TEXT NOT NULL,
|
|
9
|
+
platform TEXT NOT NULL,
|
|
10
|
+
external_id TEXT NOT NULL,
|
|
11
|
+
email TEXT,
|
|
12
|
+
linked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
13
|
+
PRIMARY KEY (platform, external_id),
|
|
14
|
+
FOREIGN KEY (user_id) REFERENCES users(username) ON DELETE CASCADE
|
|
15
|
+
)
|
|
16
|
+
"""
|
|
17
|
+
);
|
|
18
|
+
# Create index for faster lookups by user_id
|
|
19
|
+
self._conn.execute(
|
|
20
|
+
"CREATE INDEX IF NOT EXISTS idx_sso_accounts_user_id ON sso_accounts(user_id)"
|
|
21
|
+
);
|
|
22
|
+
self._conn.commit();
|
|
23
|
+
# Load SSO config
|
|
24
|
+
sso_config = get_scale_config().get_sso_config();
|
|
25
|
+
for platform in Platforms {
|
|
26
|
+
key = platform.lower();
|
|
27
|
+
platform_config = sso_config.get(key, {});
|
|
28
|
+
|
|
29
|
+
client_id = platform_config.get('client_id', '');
|
|
30
|
+
client_secret = platform_config.get('client_secret', '');
|
|
31
|
+
|
|
32
|
+
if not client_id or not client_secret {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
self.SUPPORTED_PLATFORMS[platform.value] = {
|
|
37
|
+
"client_id": client_id,
|
|
38
|
+
"client_secret": client_secret
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
impl JacScaleUserManager.create_jwt_token(username: str) -> str {
|
|
44
|
+
now = datetime.now(UTC);
|
|
45
|
+
payload: dict[(str, Any)] = {
|
|
46
|
+
'username': username,
|
|
47
|
+
'exp': (now + timedelta(days=JWT_EXP_DELTA_DAYS)),
|
|
48
|
+
'iat': now.timestamp()
|
|
49
|
+
};
|
|
50
|
+
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
impl JacScaleUserManager.validate_jwt_token(token: str) -> (str | None) {
|
|
54
|
+
try {
|
|
55
|
+
decoded = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM]);
|
|
56
|
+
return decoded['username'];
|
|
57
|
+
} except Exception {
|
|
58
|
+
return None;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
impl JacScaleUserManager.refresh_jwt_token(token: str) -> (str | None) {
|
|
63
|
+
try {
|
|
64
|
+
decoded = jwt.decode(
|
|
65
|
+
token, JWT_SECRET, algorithms=[JWT_ALGORITHM], options={"verify_exp": True}
|
|
66
|
+
);
|
|
67
|
+
username = decoded.get('username');
|
|
68
|
+
|
|
69
|
+
if not username {
|
|
70
|
+
return None;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return self.create_jwt_token(username);
|
|
74
|
+
} except Exception {
|
|
75
|
+
return None;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
impl JacScaleUserManager.validate_token(token: str) -> (str | None) {
|
|
80
|
+
return self.validate_jwt_token(token);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
impl JacScaleUserManager.get_sso(platform: str, operation: str) -> (SSOProvider | None) {
|
|
84
|
+
if (platform not in self.SUPPORTED_PLATFORMS) {
|
|
85
|
+
return None;
|
|
86
|
+
}
|
|
87
|
+
credentials = self.SUPPORTED_PLATFORMS[platform];
|
|
88
|
+
redirect_uri = f"{SSO_HOST}/{platform}/{operation}/callback";
|
|
89
|
+
if (platform == Platforms.GOOGLE.value) {
|
|
90
|
+
import from jac_scale.google_sso_provider { GoogleSSOProvider }
|
|
91
|
+
return GoogleSSOProvider(
|
|
92
|
+
client_id=credentials['client_id'],
|
|
93
|
+
client_secret=credentials['client_secret'],
|
|
94
|
+
redirect_uri=redirect_uri,
|
|
95
|
+
allow_insecure_http=True
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return None;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
impl JacScaleUserManager.sso_initiate(
|
|
102
|
+
platform: str, operation: str
|
|
103
|
+
) -> (Response | TransportResponse) {
|
|
104
|
+
import from jaclang.runtimelib.server { JsonValue }
|
|
105
|
+
if (platform not in [p.value for p in Platforms]) {
|
|
106
|
+
return TransportResponse.fail(
|
|
107
|
+
code='INVALID_PLATFORM',
|
|
108
|
+
message=f"Invalid platform '{platform}'. Supported platforms: {', '.join(
|
|
109
|
+
[p.value for p in Platforms]
|
|
110
|
+
)}",
|
|
111
|
+
meta=Meta(extra={'http_status': 400})
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (platform not in self.SUPPORTED_PLATFORMS) {
|
|
115
|
+
return TransportResponse.fail(
|
|
116
|
+
code='SSO_NOT_CONFIGURED',
|
|
117
|
+
message=f"SSO for platform '{platform}' is not configured. Please set SSO_{platform.upper()}_CLIENT_ID and SSO_{platform.upper()}_CLIENT_SECRET environment variables.",
|
|
118
|
+
meta=Meta(extra={'http_status': 501})
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
if (operation not in [o.value for o in Operations]) {
|
|
122
|
+
return TransportResponse.fail(
|
|
123
|
+
code='INVALID_OPERATION',
|
|
124
|
+
message=f"Invalid operation '{operation}'. Must be 'login' or 'register'",
|
|
125
|
+
meta=Meta(extra={'http_status': 400})
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
sso = self.get_sso(platform, operation);
|
|
129
|
+
if not sso {
|
|
130
|
+
return TransportResponse.fail(
|
|
131
|
+
code='SSO_INIT_FAILED',
|
|
132
|
+
message=f"Failed to initialize SSO for platform '{platform}'",
|
|
133
|
+
meta=Meta(extra={'http_status': 500})
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return await sso.initiate_auth(operation);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
impl JacScaleUserManager.sso_callback(
|
|
140
|
+
request: Request, platform: str, operation: str
|
|
141
|
+
) -> TransportResponse {
|
|
142
|
+
import from jaclang.runtimelib.server { JsonValue }
|
|
143
|
+
if (platform not in [p.value for p in Platforms]) {
|
|
144
|
+
return TransportResponse.fail(
|
|
145
|
+
code='INVALID_PLATFORM',
|
|
146
|
+
message=f"Invalid platform '{platform}'. Supported platforms: {', '.join(
|
|
147
|
+
[p.value for p in Platforms]
|
|
148
|
+
)}",
|
|
149
|
+
meta=Meta(extra={'http_status': 400})
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
if (platform not in self.SUPPORTED_PLATFORMS) {
|
|
153
|
+
return TransportResponse.fail(
|
|
154
|
+
code='SSO_NOT_CONFIGURED',
|
|
155
|
+
message=f"SSO for platform '{platform}' is not configured. Please set SSO_{platform.upper()}_CLIENT_ID and SSO_{platform.upper()}_CLIENT_SECRET environment variables.",
|
|
156
|
+
meta=Meta(extra={'http_status': 501})
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
if (operation not in [o.value for o in Operations]) {
|
|
160
|
+
return TransportResponse.fail(
|
|
161
|
+
code='INVALID_OPERATION',
|
|
162
|
+
message=f"Invalid operation '{operation}'. Must be 'login' or 'register'",
|
|
163
|
+
meta=Meta(extra={'http_status': 400})
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
sso = self.get_sso(platform, operation);
|
|
167
|
+
if not sso {
|
|
168
|
+
return TransportResponse.fail(
|
|
169
|
+
code='SSO_INIT_FAILED',
|
|
170
|
+
message=f"Failed to initialize SSO for platform '{platform}'",
|
|
171
|
+
meta=Meta(extra={'http_status': 500})
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
user_info = await sso.handle_callback(request);
|
|
176
|
+
email = user_info.email;
|
|
177
|
+
if not email {
|
|
178
|
+
return TransportResponse.fail(
|
|
179
|
+
code='EMAIL_MISSING',
|
|
180
|
+
message=f"Email not provided by {platform}",
|
|
181
|
+
meta=Meta(extra={'http_status': 400})
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
if (operation == Operations.LOGIN.value) {
|
|
185
|
+
user = self.get_user(email);
|
|
186
|
+
if not user {
|
|
187
|
+
return TransportResponse.fail(
|
|
188
|
+
code='USER_NOT_FOUND',
|
|
189
|
+
message='User not found. Please register first.',
|
|
190
|
+
meta=Meta(extra={'http_status': 404})
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
token = self.create_jwt_token(email);
|
|
194
|
+
return TransportResponse.success(
|
|
195
|
+
data={
|
|
196
|
+
'message': 'Login successful',
|
|
197
|
+
'email': email,
|
|
198
|
+
'token': token,
|
|
199
|
+
'platform': platform,
|
|
200
|
+
'user': dict[(str, JsonValue)](user)
|
|
201
|
+
},
|
|
202
|
+
meta=Meta(extra={'http_status': 200})
|
|
203
|
+
);
|
|
204
|
+
} elif (operation == Operations.REGISTER.value) {
|
|
205
|
+
existing_user = self.get_user(email);
|
|
206
|
+
if existing_user {
|
|
207
|
+
return TransportResponse.fail(
|
|
208
|
+
code='USER_EXISTS',
|
|
209
|
+
message='User already exists. Please login instead.',
|
|
210
|
+
meta=Meta(extra={'http_status': 400})
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
random_password = generate_random_password();
|
|
214
|
+
result = self.create_user(email, random_password);
|
|
215
|
+
if ('error' in result) {
|
|
216
|
+
return TransportResponse.fail(
|
|
217
|
+
code='USER_CREATION_FAILED',
|
|
218
|
+
message=result.get('error', 'User creation failed'),
|
|
219
|
+
meta=Meta(extra={'http_status': 400})
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
token = self.create_jwt_token(email);
|
|
223
|
+
result['token'] = token;
|
|
224
|
+
result['platform'] = platform;
|
|
225
|
+
return TransportResponse.success(
|
|
226
|
+
data=result, meta=Meta(extra={'http_status': 201})
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
} except Exception as e {
|
|
230
|
+
return TransportResponse.fail(
|
|
231
|
+
code='AUTHENTICATION_FAILED',
|
|
232
|
+
message=f"Authentication failed: {str(e)}",
|
|
233
|
+
meta=Meta(extra={'http_status': 500})
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
return TransportResponse.fail(
|
|
237
|
+
code='UNKNOWN_ERROR',
|
|
238
|
+
message='An unknown error occurred',
|
|
239
|
+
meta=Meta(extra={'http_status': 500})
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
# SSO Account Linking Methods
|
|
244
|
+
"""Link an SSO account to a user.
|
|
245
|
+
|
|
246
|
+
This allows users to have multiple SSO providers linked to their account.
|
|
247
|
+
"""
|
|
248
|
+
impl JacScaleUserManager.link_sso_account(
|
|
249
|
+
user_id: str, platform: str, external_id: str, email: str
|
|
250
|
+
) -> dict[str, str] {
|
|
251
|
+
self._ensure_connection();
|
|
252
|
+
# Check if this SSO account is already linked to another user
|
|
253
|
+
cursor = self._conn.execute(
|
|
254
|
+
"SELECT user_id FROM sso_accounts WHERE platform = ? AND external_id = ?",
|
|
255
|
+
(platform, external_id)
|
|
256
|
+
);
|
|
257
|
+
existing = cursor.fetchone();
|
|
258
|
+
if existing {
|
|
259
|
+
if existing[0] == user_id {
|
|
260
|
+
return {'message': 'SSO account already linked to this user'};
|
|
261
|
+
}
|
|
262
|
+
return {'error': 'This SSO account is already linked to another user'};
|
|
263
|
+
}
|
|
264
|
+
# Link the SSO account
|
|
265
|
+
self._conn.execute(
|
|
266
|
+
"""
|
|
267
|
+
INSERT INTO sso_accounts (user_id, platform, external_id, email)
|
|
268
|
+
VALUES (?, ?, ?, ?)
|
|
269
|
+
""",
|
|
270
|
+
(user_id, platform, external_id, email)
|
|
271
|
+
);
|
|
272
|
+
self._conn.commit();
|
|
273
|
+
return {
|
|
274
|
+
'message': 'SSO account linked successfully',
|
|
275
|
+
'user_id': user_id,
|
|
276
|
+
'platform': platform
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
"""Unlink an SSO account from a user."""
|
|
281
|
+
impl JacScaleUserManager.unlink_sso_account(
|
|
282
|
+
user_id: str, platform: str
|
|
283
|
+
) -> dict[str, str] {
|
|
284
|
+
self._ensure_connection();
|
|
285
|
+
cursor = self._conn.execute(
|
|
286
|
+
"DELETE FROM sso_accounts WHERE user_id = ? AND platform = ?",
|
|
287
|
+
(user_id, platform)
|
|
288
|
+
);
|
|
289
|
+
self._conn.commit();
|
|
290
|
+
if cursor.rowcount == 0 {
|
|
291
|
+
return {'error': 'SSO account not found'};
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
'message': 'SSO account unlinked successfully',
|
|
295
|
+
'user_id': user_id,
|
|
296
|
+
'platform': platform
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
"""Get all SSO accounts linked to a user."""
|
|
301
|
+
impl JacScaleUserManager.get_sso_accounts(user_id: str) -> list[dict[str, str]] {
|
|
302
|
+
self._ensure_connection();
|
|
303
|
+
cursor = self._conn.execute(
|
|
304
|
+
"""
|
|
305
|
+
SELECT platform, external_id, email, linked_at
|
|
306
|
+
FROM sso_accounts
|
|
307
|
+
WHERE user_id = ?
|
|
308
|
+
ORDER BY linked_at DESC
|
|
309
|
+
""",
|
|
310
|
+
(user_id, )
|
|
311
|
+
);
|
|
312
|
+
accounts = [];
|
|
313
|
+
for row in cursor.fetchall() {
|
|
314
|
+
accounts.append(
|
|
315
|
+
{
|
|
316
|
+
'platform': row[0],
|
|
317
|
+
'external_id': row[1],
|
|
318
|
+
'email': row[2],
|
|
319
|
+
'linked_at': row[3]
|
|
320
|
+
}
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
return accounts;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
"""Find a user by their SSO account credentials.
|
|
327
|
+
|
|
328
|
+
This is used during SSO login to find the user associated with
|
|
329
|
+
an external SSO identity.
|
|
330
|
+
"""
|
|
331
|
+
impl JacScaleUserManager.get_user_by_sso(
|
|
332
|
+
platform: str, external_id: str
|
|
333
|
+
) -> (dict[str, str] | None) {
|
|
334
|
+
self._ensure_connection();
|
|
335
|
+
cursor = self._conn.execute(
|
|
336
|
+
"""
|
|
337
|
+
SELECT sa.user_id, u.token, u.root_id
|
|
338
|
+
FROM sso_accounts sa
|
|
339
|
+
JOIN users u ON sa.user_id = u.username
|
|
340
|
+
WHERE sa.platform = ? AND sa.external_id = ?
|
|
341
|
+
""",
|
|
342
|
+
(platform, external_id)
|
|
343
|
+
);
|
|
344
|
+
row = cursor.fetchone();
|
|
345
|
+
if not row {
|
|
346
|
+
return None;
|
|
347
|
+
}
|
|
348
|
+
return {'email': row[0], 'token': row[1], 'root_id': row[2]};
|
|
349
|
+
}
|
jac_scale/memory_hierarchy.jac
CHANGED
|
@@ -74,7 +74,9 @@ MongoDB persistence backend - implements PersistentMemory for durable L3 storage
|
|
|
74
74
|
Replaces SqliteMemory when MongoDB is available.
|
|
75
75
|
"""
|
|
76
76
|
obj MongoBackend(PersistentMemory) {
|
|
77
|
-
has client:
|
|
77
|
+
has client: MongoClient | None = None,
|
|
78
|
+
db: Any by postinit,
|
|
79
|
+
collection: Any by postinit,
|
|
78
80
|
db_name: str = 'jac_db',
|
|
79
81
|
collection_name: str = 'anchors',
|
|
80
82
|
mongo_url: str = _db_config['mongodb_uri'];
|
jac_scale/plugin.jac
CHANGED
|
@@ -4,6 +4,7 @@ import pathlib;
|
|
|
4
4
|
import from dotenv { load_dotenv }
|
|
5
5
|
import from jaclang.cli.registry { get_registry }
|
|
6
6
|
import from jaclang.cli.command { Arg, ArgKind, CommandPriority, HookContext }
|
|
7
|
+
import from jaclang.cli.console { console }
|
|
7
8
|
import from jaclang.pycore.runtime { hookimpl, plugin_manager }
|
|
8
9
|
import from jaclang.runtimelib.context { ExecutionContext }
|
|
9
10
|
import from jaclang.runtimelib.server { JacAPIServer as JacServer }
|
|
@@ -15,6 +16,9 @@ import from .factories.deployment_factory { DeploymentTargetFactory }
|
|
|
15
16
|
import from .factories.registry_factory { ImageRegistryFactory }
|
|
16
17
|
import from .factories.utility_factory { UtilityFactory }
|
|
17
18
|
import from .abstractions.config.app_config { AppConfig }
|
|
19
|
+
import from .user_manager { JacScaleUserManager }
|
|
20
|
+
import from jaclang.runtimelib.server { UserManager }
|
|
21
|
+
import from jaclang.runtimelib.storage { Storage }
|
|
18
22
|
|
|
19
23
|
"""Pre-hook for jac start command to handle --scale flag."""
|
|
20
24
|
def _scale_pre_hook(context: HookContext) -> None {
|
|
@@ -23,6 +27,7 @@ def _scale_pre_hook(context: HookContext) -> None {
|
|
|
23
27
|
# Handle deployment instead of local server
|
|
24
28
|
filename = context.get_arg("filename");
|
|
25
29
|
build = context.get_arg("build", False);
|
|
30
|
+
experimental = context.get_arg("experimental", False);
|
|
26
31
|
target = context.get_arg("target", "kubernetes");
|
|
27
32
|
registry = context.get_arg("registry", "dockerhub");
|
|
28
33
|
if not os.path.exists(filename) {
|
|
@@ -57,15 +62,27 @@ def _scale_pre_hook(context: HookContext) -> None {
|
|
|
57
62
|
}
|
|
58
63
|
# Create app config
|
|
59
64
|
app_config = AppConfig(
|
|
60
|
-
code_folder=code_folder,
|
|
65
|
+
code_folder=code_folder,
|
|
66
|
+
file_name=base_file_path,
|
|
67
|
+
build=build,
|
|
68
|
+
experimental=experimental
|
|
61
69
|
);
|
|
70
|
+
if experimental {
|
|
71
|
+
console.print(
|
|
72
|
+
"Installing Jaseci packages from repository (experimental mode)..."
|
|
73
|
+
);
|
|
74
|
+
} else {
|
|
75
|
+
console.print("Installing Jaseci packages from PyPI...");
|
|
76
|
+
}
|
|
62
77
|
# Deploy
|
|
63
78
|
result = deployment_target.deploy(app_config);
|
|
64
79
|
if not result.success {
|
|
65
80
|
raise RuntimeError(result.message or "Deployment failed") ;
|
|
66
81
|
}
|
|
67
82
|
if result.service_url {
|
|
68
|
-
print(
|
|
83
|
+
console.print(
|
|
84
|
+
f"Deployment complete! Service available at: {result.service_url}"
|
|
85
|
+
);
|
|
69
86
|
}
|
|
70
87
|
# Cancel normal start execution since we handled it
|
|
71
88
|
context.set_data("cancel_execution", True);
|
|
@@ -99,6 +116,13 @@ class JacCmd {
|
|
|
99
116
|
help="Build and push Docker image (with --scale)",
|
|
100
117
|
short="b"
|
|
101
118
|
),
|
|
119
|
+
Arg.create(
|
|
120
|
+
"experimental",
|
|
121
|
+
typ=bool,
|
|
122
|
+
default=False,
|
|
123
|
+
help="Use experimental mode (install from repo instead of PyPI)",
|
|
124
|
+
short="e"
|
|
125
|
+
),
|
|
102
126
|
Arg.create(
|
|
103
127
|
"target",
|
|
104
128
|
typ=str,
|
|
@@ -166,7 +190,9 @@ class JacCmd {
|
|
|
166
190
|
app_name = os.getenv('APP_NAME') or target_config.get('app_name', 'jaseci');
|
|
167
191
|
deployment_target.destroy(app_name);
|
|
168
192
|
|
|
169
|
-
print(
|
|
193
|
+
console.print(
|
|
194
|
+
f"Successfully destroyed deployment '{app_name}' from {target}"
|
|
195
|
+
);
|
|
170
196
|
return 0;
|
|
171
197
|
}
|
|
172
198
|
}
|
|
@@ -197,6 +223,23 @@ class JacScalePlugin {
|
|
|
197
223
|
static def get_api_server_class -> type {
|
|
198
224
|
return JacAPIServer;
|
|
199
225
|
}
|
|
226
|
+
|
|
227
|
+
"""Provide jac-scale's UserManager."""
|
|
228
|
+
@hookimpl
|
|
229
|
+
static def get_user_manager(base_path: str) -> UserManager {
|
|
230
|
+
return JacScaleUserManager(base_path=base_path);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
"""Provide jac-scale's storage backend.
|
|
234
|
+
|
|
235
|
+
This overrides the core store() to use jac-scale's StorageFactory,
|
|
236
|
+
which supports cloud backends (S3, GCS, Azure) via configuration.
|
|
237
|
+
"""
|
|
238
|
+
@hookimpl
|
|
239
|
+
static def store(base_path: str = "./storage", create_dirs: bool = True) -> Storage {
|
|
240
|
+
import from .factories.storage_factory { StorageFactory }
|
|
241
|
+
return StorageFactory.get_default(base_path, create_dirs);
|
|
242
|
+
}
|
|
200
243
|
}
|
|
201
244
|
|
|
202
245
|
# Pluggy's varnames() puts parameters with defaults into kwargnames, not argnames.
|
jac_scale/plugin_config.jac
CHANGED
|
@@ -149,6 +149,33 @@ class JacScalePluginConfig {
|
|
|
149
149
|
"type": "bool",
|
|
150
150
|
"default": True,
|
|
151
151
|
"description": "Enable Redis deployment in Kubernetes"
|
|
152
|
+
},
|
|
153
|
+
"plugin_versions": {
|
|
154
|
+
"type": "dict",
|
|
155
|
+
"default": {},
|
|
156
|
+
"description": "Package versions for PyPI installation (default mode). Use 'latest' or specific version.",
|
|
157
|
+
"nested": {
|
|
158
|
+
"jaclang": {
|
|
159
|
+
"type": "string",
|
|
160
|
+
"default": "latest",
|
|
161
|
+
"description": "jaclang package version"
|
|
162
|
+
},
|
|
163
|
+
"jac_scale": {
|
|
164
|
+
"type": "string",
|
|
165
|
+
"default": "latest",
|
|
166
|
+
"description": "jac-scale package version"
|
|
167
|
+
},
|
|
168
|
+
"jac_client": {
|
|
169
|
+
"type": "string",
|
|
170
|
+
"default": "latest",
|
|
171
|
+
"description": "jac-client package version"
|
|
172
|
+
},
|
|
173
|
+
"jac_byllm": {
|
|
174
|
+
"type": "string",
|
|
175
|
+
"default": "latest",
|
|
176
|
+
"description": "jac-byllm package version (use 'none' to skip)"
|
|
177
|
+
}
|
|
178
|
+
}
|
|
152
179
|
}
|
|
153
180
|
}
|
|
154
181
|
},
|
jac_scale/serve.jac
CHANGED
|
@@ -24,6 +24,9 @@ import from enum { StrEnum }
|
|
|
24
24
|
import from fastapi_sso.sso.google { GoogleSSO }
|
|
25
25
|
import from jac_scale.utils { generate_random_password }
|
|
26
26
|
import from jac_scale.config_loader { get_scale_config }
|
|
27
|
+
import from typing { AsyncGenerator }
|
|
28
|
+
import from inspect { isgenerator }
|
|
29
|
+
import from fastapi.responses { StreamingResponse }
|
|
27
30
|
|
|
28
31
|
# Load configuration from jac.toml with env var overrides
|
|
29
32
|
glob _jwt_config = get_scale_config().get_jwt_config(),
|
|
@@ -43,9 +46,6 @@ obj JacAPIServer(JServer) {
|
|
|
43
46
|
has _hmr_pending: bool = False,
|
|
44
47
|
_hot_reloader: Any | None = None;
|
|
45
48
|
|
|
46
|
-
static def create_jwt_token(username: str) -> str;
|
|
47
|
-
static def validate_jwt_token(token: str) -> (str | None);
|
|
48
|
-
static def refresh_jwt_token(token: str) -> (str | None);
|
|
49
49
|
def postinit -> None;
|
|
50
50
|
def login(username: str, password: str) -> TransportResponse;
|
|
51
51
|
def register_login_endpoint -> None;
|
|
@@ -66,15 +66,6 @@ obj JacAPIServer(JServer) {
|
|
|
66
66
|
def refresh_token(token: (str | None) = None) -> TransportResponse;
|
|
67
67
|
def register_create_user_endpoint -> None;
|
|
68
68
|
def register_refresh_token_endpoint -> None;
|
|
69
|
-
def get_sso(platform: str, operation: str) -> (GoogleSSO | None);
|
|
70
|
-
async def sso_initiate(
|
|
71
|
-
platform: str, operation: str
|
|
72
|
-
) -> (Response | TransportResponse);
|
|
73
|
-
|
|
74
|
-
async def sso_callback(
|
|
75
|
-
request: Request, platform: str, operation: str
|
|
76
|
-
) -> TransportResponse;
|
|
77
|
-
|
|
78
69
|
def register_sso_endpoints -> None;
|
|
79
70
|
def create_walker_callback(
|
|
80
71
|
walker_name: str, has_node_param: bool = False
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""SSO Provider Abstraction for jac-scale.
|
|
2
|
+
|
|
3
|
+
This module defines the abstract interface for SSO providers, enabling
|
|
4
|
+
easy addition of new authentication vendors (Google, Microsoft, GitHub, SAML, etc.).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import from typing { Any }
|
|
8
|
+
import from fastapi { Request, Response }
|
|
9
|
+
import from dataclasses { dataclass }
|
|
10
|
+
|
|
11
|
+
"""Standardized user information from SSO providers."""
|
|
12
|
+
@dataclass
|
|
13
|
+
class SSOUserInfo {
|
|
14
|
+
has email: str,
|
|
15
|
+
external_id: str,
|
|
16
|
+
platform: str,
|
|
17
|
+
display_name: (str | None) = None;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
"""Abstract base class for SSO providers.
|
|
21
|
+
|
|
22
|
+
All SSO provider implementations must inherit from this class and implement
|
|
23
|
+
the required methods. This abstraction enables:
|
|
24
|
+
|
|
25
|
+
1. Consistent interface across all SSO vendors
|
|
26
|
+
2. Easy addition of new providers (just implement this interface)
|
|
27
|
+
3. Testability through mock implementations
|
|
28
|
+
4. Standardized user information format
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
To add a new SSO provider (e.g., Microsoft):
|
|
32
|
+
|
|
33
|
+
```jac
|
|
34
|
+
obj MicrosoftSSOProvider(SSOProvider) {
|
|
35
|
+
# Implement the three required methods
|
|
36
|
+
async def initiate_auth(operation: str) -> Response;
|
|
37
|
+
async def handle_callback(request: Request) -> SSOUserInfo;
|
|
38
|
+
def get_platform_name() -> str;
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
"""
|
|
42
|
+
obj SSOProvider {
|
|
43
|
+
"""Initialize SSO authentication flow.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
operation: The operation type ('login' or 'register')
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Response object (typically a redirect to the SSO provider's auth page)
|
|
50
|
+
"""
|
|
51
|
+
async def initiate_auth(operation: str) -> Response;
|
|
52
|
+
|
|
53
|
+
"""Handle the OAuth callback from the SSO provider.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
request: The FastAPI request object containing the OAuth callback data
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
SSOUserInfo: Standardized user information from the provider
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
Exception: If authentication fails or user info cannot be retrieved
|
|
63
|
+
"""
|
|
64
|
+
async def handle_callback(request: Request) -> SSOUserInfo;
|
|
65
|
+
|
|
66
|
+
"""Get the platform identifier for this provider.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
str: Platform name (e.g., 'google', 'microsoft', 'github')
|
|
70
|
+
"""
|
|
71
|
+
def get_platform_name -> str;
|
|
72
|
+
}
|
|
@@ -34,12 +34,12 @@ class KubernetesConfig(BaseConfig) {
|
|
|
34
34
|
app_mount_path: str = '/app',
|
|
35
35
|
code_mount_path: str = '/code',
|
|
36
36
|
workspace_path: str = '/code/workspace',
|
|
37
|
-
# Runtime environment (defaults to official Jaseci repo)
|
|
38
37
|
jaseci_repo_url: str = 'https://github.com/jaseci-labs/jaseci.git',
|
|
39
38
|
jaseci_branch: str = 'main',
|
|
40
39
|
jaseci_commit: (str | None) = None,
|
|
41
40
|
install_jaseci: bool = True,
|
|
42
|
-
additional_packages: list[str] = []
|
|
41
|
+
additional_packages: list[str] = [],
|
|
42
|
+
plugin_versions: dict[str, str] = {};
|
|
43
43
|
|
|
44
44
|
def init(
|
|
45
45
|
self: KubernetesConfig,
|
|
@@ -75,12 +75,8 @@ class KubernetesConfig(BaseConfig) {
|
|
|
75
75
|
jaseci_branch: str = 'main',
|
|
76
76
|
jaseci_commit: (str | None) = None,
|
|
77
77
|
install_jaseci: bool = True,
|
|
78
|
-
additional_packages: list[str] = []
|
|
79
|
-
|
|
80
|
-
# Storage configuration
|
|
81
|
-
# Timing configuration
|
|
82
|
-
# Paths
|
|
83
|
-
# Runtime environment
|
|
78
|
+
additional_packages: list[str] = [],
|
|
79
|
+
plugin_versions: dict[str, str] = {}
|
|
84
80
|
) -> None {
|
|
85
81
|
self.app_name = app_name;
|
|
86
82
|
self.namespace = namespace;
|
|
@@ -121,6 +117,7 @@ class KubernetesConfig(BaseConfig) {
|
|
|
121
117
|
self.jaseci_commit = jaseci_commit;
|
|
122
118
|
self.install_jaseci = install_jaseci;
|
|
123
119
|
self.additional_packages = additional_packages;
|
|
120
|
+
self.plugin_versions = plugin_versions;
|
|
124
121
|
}
|
|
125
122
|
|
|
126
123
|
override def to_dict(self: KubernetesConfig) -> dict[str, Any] {
|
|
@@ -162,7 +159,8 @@ class KubernetesConfig(BaseConfig) {
|
|
|
162
159
|
'jaseci_branch': self.jaseci_branch,
|
|
163
160
|
'jaseci_commit': self.jaseci_commit,
|
|
164
161
|
'install_jaseci': self.install_jaseci,
|
|
165
|
-
'additional_packages': self.additional_packages
|
|
162
|
+
'additional_packages': self.additional_packages,
|
|
163
|
+
'plugin_versions': self.plugin_versions
|
|
166
164
|
}
|
|
167
165
|
);
|
|
168
166
|
return base;
|
|
@@ -204,12 +202,8 @@ class KubernetesConfig(BaseConfig) {
|
|
|
204
202
|
jaseci_branch=config.get('jaseci_branch', 'main'),
|
|
205
203
|
jaseci_commit=config.get('jaseci_commit'),
|
|
206
204
|
install_jaseci=config.get('install_jaseci', True),
|
|
207
|
-
additional_packages=config.get('additional_packages', [])
|
|
208
|
-
|
|
209
|
-
# Storage configuration
|
|
210
|
-
# Timing configuration
|
|
211
|
-
# Paths
|
|
212
|
-
# Runtime environment
|
|
205
|
+
additional_packages=config.get('additional_packages', []),
|
|
206
|
+
plugin_versions=config.get('plugin_versions', {})
|
|
213
207
|
);
|
|
214
208
|
}
|
|
215
209
|
}
|