gestalt-mobile 0.12.0 → 0.14.0

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.
Files changed (79) hide show
  1. package/README.md +42 -11
  2. package/dist/client/assets/index-C5qFcFUg.css +1 -0
  3. package/dist/client/assets/index-tMyOolAm.js +24 -0
  4. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  5. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  6. package/dist/client/assets/jetbrains-mono-cyrillic-ext-400-normal-Bh0R7Dhr.woff +0 -0
  7. package/dist/client/assets/jetbrains-mono-cyrillic-ext-400-normal-C_uLvvQ5.woff2 +0 -0
  8. package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  9. package/dist/client/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  10. package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  11. package/dist/client/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  12. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  13. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  14. package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-ByoDsISC.woff2 +0 -0
  15. package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  16. package/dist/client/assets/syne-greek-600-normal-BJs4kvyH.woff +0 -0
  17. package/dist/client/assets/syne-greek-600-normal-BpFII5Ja.woff2 +0 -0
  18. package/dist/client/assets/syne-latin-600-normal-BiwQbQXw.woff2 +0 -0
  19. package/dist/client/assets/syne-latin-600-normal-dRu9QuIh.woff +0 -0
  20. package/dist/client/assets/syne-latin-ext-600-normal-DEzBMW8B.woff +0 -0
  21. package/dist/client/assets/syne-latin-ext-600-normal-ZnizrDKU.woff2 +0 -0
  22. package/dist/client/index.html +3 -2
  23. package/dist/server/server/app.js +22 -227
  24. package/dist/server/server/cli.js +3 -1
  25. package/dist/server/server/composition.js +25 -26
  26. package/dist/server/server/features/auth/application/ceremony-attempts.js +22 -0
  27. package/dist/server/server/features/auth/devices/revoke/endpoint.js +3 -1
  28. package/dist/server/server/features/auth/domain/errors.js +7 -0
  29. package/dist/server/server/features/auth/enrollment-tickets/endpoint.js +7 -2
  30. package/dist/server/server/features/auth/enrollment-tickets/status/endpoint.js +3 -1
  31. package/dist/server/server/features/auth/http/cookies.js +20 -0
  32. package/dist/server/server/features/auth/login/options/endpoint.js +22 -20
  33. package/dist/server/server/features/auth/login/verify/endpoint.js +6 -14
  34. package/dist/server/server/features/auth/logout/endpoint.js +2 -6
  35. package/dist/server/server/features/auth/register/options/endpoint.js +43 -11
  36. package/dist/server/server/features/auth/register/verify/endpoint.js +74 -18
  37. package/dist/server/server/features/auth/register-routes.js +41 -0
  38. package/dist/server/server/features/git/checkout-branch/endpoint.js +1 -1
  39. package/dist/server/server/features/git/clone-repository/endpoint.js +1 -1
  40. package/dist/server/server/features/git/register-routes.js +34 -0
  41. package/dist/server/server/features/plans/application/measurement-snapshot.js +1 -1
  42. package/dist/server/server/features/plans/get-measurement/endpoint.js +1 -3
  43. package/dist/server/server/features/plans/register-routes.js +22 -0
  44. package/dist/server/server/features/sessions/interaction/kind.js +7 -1
  45. package/dist/server/server/features/sessions/list-recent-threads/endpoint.js +3 -1
  46. package/dist/server/server/features/sessions/model/relay-session.js +3 -1
  47. package/dist/server/server/features/sessions/promote-recent-thread/endpoint.js +1 -1
  48. package/dist/server/server/features/sessions/register-routes.js +138 -0
  49. package/dist/server/server/features/sessions/respond-interaction/endpoint.js +1 -1
  50. package/dist/server/server/features/sessions/select-model/endpoint.js +1 -1
  51. package/dist/server/server/features/sessions/start-session/endpoint.js +5 -2
  52. package/dist/server/server/features/sessions/start-session/use-case.js +1 -1
  53. package/dist/server/server/features/sessions/start-turn/endpoint.js +2 -1
  54. package/dist/server/server/features/skills/delete-profile/endpoint.js +12 -3
  55. package/dist/server/server/features/skills/list-available/endpoint.js +24 -6
  56. package/dist/server/server/features/skills/list-profiles/endpoint.js +29 -5
  57. package/dist/server/server/features/skills/model/skill-profile.js +23 -3
  58. package/dist/server/server/features/skills/register-routes.js +17 -0
  59. package/dist/server/server/features/skills/replace-profile/endpoint.js +20 -5
  60. package/dist/server/server/platform/auth/simple-webauthn-adapter.js +23 -3
  61. package/dist/server/server/platform/auth/sqlite-authorization-store.js +29 -6
  62. package/dist/server/server/platform/catalog/profile-command.js +8 -1
  63. package/dist/server/server/platform/codex/normalizer.js +50 -6
  64. package/dist/server/server/platform/codex/protocol-compatibility.js +19 -1
  65. package/dist/server/server/platform/codex/server-request.js +2 -0
  66. package/dist/server/server/platform/codex/session-runtime.js +235 -126
  67. package/dist/server/server/platform/codex/thread-plan-name.js +5 -2
  68. package/dist/server/server/platform/http/problem-handler.js +10 -0
  69. package/dist/server/server/platform/http/problem-reply.js +13 -0
  70. package/dist/server/server/platform/plans/filesystem-plan-status-source.js +11 -6
  71. package/dist/server/server/platform/plans/filesystem-workspace-plan-catalog.js +4 -1
  72. package/dist/server/server/platform/plans/plan-measurement-command.js +4 -1
  73. package/dist/server/server/platform/skills/codex-skill-catalog.js +29 -7
  74. package/dist/server/server/platform/skills/filesystem-skill-profile-store.js +5 -1
  75. package/dist/server/shared/contracts/quiz.js +30 -9
  76. package/package.json +4 -1
  77. package/dist/client/assets/index-B7KwEjTE.js +0 -19
  78. package/dist/client/assets/index-CMs5ZzxJ.css +0 -1
  79. package/dist/server/server/features/plans/application/native-plan-alignment.js +0 -39
@@ -7,13 +7,22 @@ import { z } from 'zod';
7
7
  import { applySkillSelectionSnapshot } from '../model/skill-profile.js';
8
8
  import { SkillProfileError } from '../model/errors.js';
9
9
  import { problem } from '../../../platform/http/problem.js';
10
- const querySchema = z.object({ workspaceId: z.string().min(1), profile: z.string().min(1), refresh: z.enum(['true']).optional() }).strict();
10
+ const querySchema = z
11
+ .object({
12
+ workspaceId: z.string().min(1),
13
+ profile: z.string().min(1),
14
+ refresh: z.enum(['true']).optional(),
15
+ })
16
+ .strict();
11
17
  /** Register the workspace-scoped skill discovery REPR slice. */
12
18
  export function registerListAvailableSkills(app, deps) {
13
19
  app.get('/api/skills', async (request, reply) => {
14
20
  const parsed = querySchema.safeParse(request.query);
15
21
  if (!parsed.success)
16
- return reply.code(400).type('application/problem+json').send(problem('INVALID_SKILL_REQUEST', 400, 'workspaceId and profile are required.'));
22
+ return reply
23
+ .code(400)
24
+ .type('application/problem+json')
25
+ .send(problem('INVALID_SKILL_REQUEST', 400, 'workspaceId and profile are required.'));
17
26
  try {
18
27
  const [workspace, profile] = await Promise.all([
19
28
  deps.workspaces.resolve(parsed.data.workspaceId),
@@ -52,10 +61,19 @@ export function registerListAvailableSkills(app, deps) {
52
61
  };
53
62
  }
54
63
  catch (error) {
55
- if (error instanceof Error && (error.message === 'WORKSPACE_NOT_FOUND' || error.message === 'PROFILE_NOT_FOUND'))
56
- return reply.code(404).type('application/problem+json').send(problem(error.message, 404, 'The requested catalog entry was not found.'));
57
- const code = error instanceof SkillProfileError && error.code === 'INVALID_SKILL_PROFILE' ? 'INVALID_SKILL_PROFILE' : 'SKILL_DISCOVERY_FAILED';
58
- return reply.code(502).type('application/problem+json').send(problem(code, 502, 'Skill discovery could not be completed.', true));
64
+ if (error instanceof Error &&
65
+ (error.message === 'WORKSPACE_NOT_FOUND' || error.message === 'PROFILE_NOT_FOUND'))
66
+ return reply
67
+ .code(404)
68
+ .type('application/problem+json')
69
+ .send(problem(error.message, 404, 'The requested catalog entry was not found.'));
70
+ const code = error instanceof SkillProfileError && error.code === 'INVALID_SKILL_PROFILE'
71
+ ? 'INVALID_SKILL_PROFILE'
72
+ : 'SKILL_DISCOVERY_FAILED';
73
+ return reply
74
+ .code(502)
75
+ .type('application/problem+json')
76
+ .send(problem(code, 502, 'Skill discovery could not be completed.', true));
59
77
  }
60
78
  });
61
79
  }
@@ -8,21 +8,45 @@ export function registerListSkillProfiles(app, deps) {
8
8
  app.get('/api/skill-profiles', async (_request, reply) => {
9
9
  try {
10
10
  const names = await deps.listGlobalProfileNames();
11
- const profiles = await Promise.all(names.sort((a, b) => a.localeCompare(b)).map(async (name) => {
11
+ const profiles = await Promise.all(names
12
+ .sort((a, b) => a.localeCompare(b))
13
+ .map(async (name) => {
12
14
  try {
13
15
  const profile = await deps.readGlobalProfile(name);
14
16
  return profile
15
- ? { name: profile.name, version: profile.version, path: deps.profilePath(name), skills: profile.skills }
16
- : { name, path: deps.profilePath(name), error: { code: 'INVALID_SKILL_PROFILE', message: 'Profile disappeared while reading.' } };
17
+ ? {
18
+ name: profile.name,
19
+ version: profile.version,
20
+ path: deps.profilePath(name),
21
+ skills: profile.skills,
22
+ }
23
+ : {
24
+ name,
25
+ path: deps.profilePath(name),
26
+ error: {
27
+ code: 'INVALID_SKILL_PROFILE',
28
+ message: 'Profile disappeared while reading.',
29
+ },
30
+ };
17
31
  }
18
32
  catch (error) {
19
- return { name, path: deps.profilePath(name), error: { code: 'INVALID_SKILL_PROFILE', message: error instanceof Error ? error.message : 'Invalid profile.' } };
33
+ return {
34
+ name,
35
+ path: deps.profilePath(name),
36
+ error: {
37
+ code: 'INVALID_SKILL_PROFILE',
38
+ message: error instanceof Error ? error.message : 'Invalid profile.',
39
+ },
40
+ };
20
41
  }
21
42
  }));
22
43
  return { profiles };
23
44
  }
24
45
  catch {
25
- return reply.code(500).type('application/problem+json').send(problem('SKILL_PROFILE_PERSISTENCE_FAILED', 500, 'Skill profiles could not be read.', true));
46
+ return reply
47
+ .code(500)
48
+ .type('application/problem+json')
49
+ .send(problem('SKILL_PROFILE_PERSISTENCE_FAILED', 500, 'Skill profiles could not be read.', true));
26
50
  }
27
51
  });
28
52
  }
@@ -37,7 +37,20 @@ export const availableSkillSchema = z
37
37
  defaultPrompt: z.string().optional(),
38
38
  })
39
39
  .optional(),
40
- dependencies: z.object({ tools: z.array(z.object({ type: z.string(), value: z.string(), description: z.string().optional(), transport: z.string().optional(), command: z.string().optional(), url: z.string().optional() })).optional() }).optional(),
40
+ dependencies: z
41
+ .object({
42
+ tools: z
43
+ .array(z.object({
44
+ type: z.string(),
45
+ value: z.string(),
46
+ description: z.string().optional(),
47
+ transport: z.string().optional(),
48
+ command: z.string().optional(),
49
+ url: z.string().optional(),
50
+ }))
51
+ .optional(),
52
+ })
53
+ .optional(),
41
54
  scope: z.string().optional(),
42
55
  })
43
56
  .strict();
@@ -53,7 +66,10 @@ const profileDocumentSchema = z
53
66
  * is deliberately narrow so a profile name can never become a path traversal.
54
67
  */
55
68
  export function normalizeSkillProfileName(value) {
56
- const normalized = value.trim().toLowerCase().replace(/[\s_]+/g, '-');
69
+ const normalized = value
70
+ .trim()
71
+ .toLowerCase()
72
+ .replace(/[\s_]+/g, '-');
57
73
  if (!profileNamePattern.test(normalized)) {
58
74
  throw new SkillProfileError('INVALID_SKILL_PROFILE', 'Invalid skill profile name.');
59
75
  }
@@ -136,7 +152,11 @@ export function compileSkillOverride(input) {
136
152
  }
137
153
  /** Construct the single profile contract shared by global and project YAML files. */
138
154
  export function createSkillProfile(input) {
139
- return { version: 1, name: normalizeSkillProfileName(input.name), skills: createSkillSelection(input.skills) };
155
+ return {
156
+ version: 1,
157
+ name: normalizeSkillProfileName(input.name),
158
+ skills: createSkillSelection(input.skills),
159
+ };
140
160
  }
141
161
  /** Parse the version-1 YAML document without performing filesystem access. */
142
162
  export function parseSkillProfileYaml(source) {
@@ -0,0 +1,17 @@
1
+ /*
2
+ * Copyright (C) 2026 Dyne.org foundation
3
+ * Designed by Denis Roio <jaromil@dyne.org>
4
+ * SPDX-License-Identifier: AGPL-3.0-or-later
5
+ */
6
+ import { registerDeleteSkillProfile } from './delete-profile/endpoint.js';
7
+ import { registerListAvailableSkills } from './list-available/endpoint.js';
8
+ import { registerListSkillProfiles } from './list-profiles/endpoint.js';
9
+ import { registerReplaceSkillProfile } from './replace-profile/endpoint.js';
10
+ export function registerSkillRoutes(app, deps) {
11
+ if (!deps.skills)
12
+ return;
13
+ registerListAvailableSkills(app, deps.skills);
14
+ registerListSkillProfiles(app, deps.skills);
15
+ registerReplaceSkillProfile(app, deps.skills);
16
+ registerDeleteSkillProfile(app, deps.skills);
17
+ }
@@ -7,14 +7,23 @@ import { z } from 'zod';
7
7
  import { createSkillProfile, normalizeSkillProfileName } from '../model/skill-profile.js';
8
8
  import { SkillProfileError } from '../model/errors.js';
9
9
  import { problem } from '../../../platform/http/problem.js';
10
- const requestSchema = z.object({ version: z.literal(1), name: z.string(), skills: z.array(z.object({ name: z.string(), path: z.string(), enabled: z.boolean() }).strict()) }).strict();
10
+ const requestSchema = z
11
+ .object({
12
+ version: z.literal(1),
13
+ name: z.string(),
14
+ skills: z.array(z.object({ name: z.string(), path: z.string(), enabled: z.boolean() }).strict()),
15
+ })
16
+ .strict();
11
17
  export function registerReplaceSkillProfile(app, deps) {
12
- app.put('/api/skill-profiles/:name', async (request, reply) => {
18
+ app.put('/api/skill-profiles/:name', { bodyLimit: 64 * 1024 }, async (request, reply) => {
13
19
  const body = requestSchema.safeParse(request.body);
14
20
  try {
15
21
  const name = normalizeSkillProfileName(request.params.name);
16
22
  if (!body.success || normalizeSkillProfileName(body.data.name) !== name)
17
- return reply.code(400).type('application/problem+json').send(problem('INVALID_SKILL_PROFILE', 400, 'The route and body profile names must match.'));
23
+ return reply
24
+ .code(400)
25
+ .type('application/problem+json')
26
+ .send(problem('INVALID_SKILL_PROFILE', 400, 'The route and body profile names must match.'));
18
27
  const profile = createSkillProfile(body.data);
19
28
  const existed = await deps.readGlobalProfile(name);
20
29
  await deps.replaceGlobalProfile(profile);
@@ -22,8 +31,14 @@ export function registerReplaceSkillProfile(app, deps) {
22
31
  }
23
32
  catch (error) {
24
33
  if (error instanceof SkillProfileError)
25
- return reply.code(400).type('application/problem+json').send(problem('INVALID_SKILL_PROFILE', 400, 'The skill profile could not be stored.'));
26
- return reply.code(500).type('application/problem+json').send(problem('SKILL_PROFILE_PERSISTENCE_FAILED', 500, 'The skill profile could not be stored.', true));
34
+ return reply
35
+ .code(400)
36
+ .type('application/problem+json')
37
+ .send(problem('INVALID_SKILL_PROFILE', 400, 'The skill profile could not be stored.'));
38
+ return reply
39
+ .code(500)
40
+ .type('application/problem+json')
41
+ .send(problem('SKILL_PROFILE_PERSISTENCE_FAILED', 500, 'The skill profile could not be stored.', true));
27
42
  }
28
43
  });
29
44
  }
@@ -19,7 +19,11 @@ export class SimpleWebAuthnAdapter {
19
19
  challenge: new Uint8Array(input.challenge),
20
20
  attestationType: 'none',
21
21
  excludeCredentials: input.excludeCredentialIds.map((id) => ({ id })),
22
- authenticatorSelection: { residentKey: 'required', requireResidentKey: true, userVerification: 'required' },
22
+ authenticatorSelection: {
23
+ residentKey: 'required',
24
+ requireResidentKey: true,
25
+ userVerification: 'required',
26
+ },
23
27
  supportedAlgorithmIDs: [-7, -257],
24
28
  });
25
29
  }
@@ -63,13 +67,29 @@ export class SimpleWebAuthnAdapter {
63
67
  const value = input;
64
68
  let result;
65
69
  try {
66
- result = await verifyAuthenticationResponse({ response: value.response, expectedChallenge: Buffer.from(value.challenge).toString('base64url'), expectedOrigin: value.expectedOrigin, expectedRPID: value.rpId, requireUserVerification: true, credential: { id: value.credential.id, publicKey: new Uint8Array(value.credential.publicKey), counter: value.credential.counter, transports: [...value.credential.transports] } });
70
+ result = await verifyAuthenticationResponse({
71
+ response: value.response,
72
+ expectedChallenge: Buffer.from(value.challenge).toString('base64url'),
73
+ expectedOrigin: value.expectedOrigin,
74
+ expectedRPID: value.rpId,
75
+ requireUserVerification: true,
76
+ credential: {
77
+ id: value.credential.id,
78
+ publicKey: new Uint8Array(value.credential.publicKey),
79
+ counter: value.credential.counter,
80
+ transports: [...value.credential.transports],
81
+ },
82
+ });
67
83
  }
68
84
  catch {
69
85
  throw new PasskeyVerificationError('PASSKEY_VERIFICATION_FAILED');
70
86
  }
71
87
  if (!result.verified || !result.authenticationInfo?.userVerified)
72
88
  throw new PasskeyVerificationError('PASSKEY_VERIFICATION_FAILED');
73
- return { credentialId: webAuthnCredentialId(value.response.id), counter: result.authenticationInfo.newCounter, userVerified: result.authenticationInfo.userVerified };
89
+ return {
90
+ credentialId: webAuthnCredentialId(value.response.id),
91
+ counter: result.authenticationInfo.newCounter,
92
+ userVerified: result.authenticationInfo.userVerified,
93
+ };
74
94
  }
75
95
  }
@@ -9,6 +9,7 @@ import { dirname, join, resolve } from 'node:path';
9
9
  import { DatabaseSync } from 'node:sqlite';
10
10
  import { authorizedDeviceId, localOwnerId, passkeyCeremonyId, webAuthnCredentialId, } from '../../features/auth/domain/identifiers.js';
11
11
  import { deviceNickname } from '../../features/auth/domain/device-nickname.js';
12
+ import { CeremonyCapacityError } from '../../features/auth/domain/errors.js';
12
13
  const OWNER_ID = localOwnerId('local-owner');
13
14
  const SCHEMA_VERSION = 1;
14
15
  const SQLITE_BUSY_TIMEOUT_MS = 5_000;
@@ -234,10 +235,28 @@ CREATE TABLE IF NOT EXISTS auth_sessions (token_hash TEXT PRIMARY KEY, device_id
234
235
  throw error;
235
236
  }
236
237
  }
237
- saveCeremony(token, ceremony) {
238
- this.db
239
- .prepare('INSERT INTO auth_ceremonies (token_hash, purpose, challenge, expected_origin, rp_id, expires_at, ticket_hash) VALUES (?, ?, ?, ?, ?, ?, ?)')
240
- .run(hash(token), ceremony.purpose, ceremony.challenge, ceremony.expectedOrigin, ceremony.rpId, ceremony.expiresAt, ceremony.enrollmentTicket ? hash(ceremony.enrollmentTicket) : null);
238
+ saveCeremony(token, ceremony, now) {
239
+ this.db.exec('BEGIN IMMEDIATE');
240
+ try {
241
+ // Admission and cleanup share one immediate transaction, so separate
242
+ // processes cannot both observe the same remaining slot.
243
+ this.db.prepare('DELETE FROM auth_ceremonies WHERE consumed_at IS NOT NULL').run();
244
+ if (now)
245
+ this.db.prepare('DELETE FROM auth_ceremonies WHERE expires_at <= ?').run(now);
246
+ const active = this.db
247
+ .prepare('SELECT count(*) AS count FROM auth_ceremonies WHERE consumed_at IS NULL')
248
+ .get();
249
+ if (active.count >= 64)
250
+ throw new CeremonyCapacityError('Ceremony capacity reached');
251
+ this.db
252
+ .prepare('INSERT INTO auth_ceremonies (token_hash, purpose, challenge, expected_origin, rp_id, expires_at, ticket_hash) VALUES (?, ?, ?, ?, ?, ?, ?)')
253
+ .run(hash(token), ceremony.purpose, ceremony.challenge, ceremony.expectedOrigin, ceremony.rpId, ceremony.expiresAt, ceremony.enrollmentTicket ? hash(ceremony.enrollmentTicket) : null);
254
+ this.db.exec('COMMIT');
255
+ }
256
+ catch (error) {
257
+ this.db.exec('ROLLBACK');
258
+ throw error;
259
+ }
241
260
  }
242
261
  consumeCeremony(token, now) {
243
262
  this.db.exec('BEGIN IMMEDIATE');
@@ -286,8 +305,12 @@ CREATE TABLE IF NOT EXISTS auth_sessions (token_hash TEXT PRIMARY KEY, device_id
286
305
  this.db.exec('BEGIN IMMEDIATE');
287
306
  try {
288
307
  const creator = hash(creatorSession);
289
- this.db.prepare('DELETE FROM auth_tickets WHERE creator_session_hash = ? AND consumed_at IS NULL').run(creator);
290
- this.db.prepare('INSERT INTO auth_tickets (token_hash, expires_at, consumed_at, creator_session_hash) VALUES (?, ?, NULL, ?)').run(hash(token), expiresAt, creator);
308
+ this.db
309
+ .prepare('DELETE FROM auth_tickets WHERE creator_session_hash = ? AND consumed_at IS NULL')
310
+ .run(creator);
311
+ this.db
312
+ .prepare('INSERT INTO auth_tickets (token_hash, expires_at, consumed_at, creator_session_hash) VALUES (?, ?, NULL, ?)')
313
+ .run(hash(token), expiresAt, creator);
291
314
  this.db.exec('COMMIT');
292
315
  }
293
316
  catch (error) {
@@ -18,5 +18,12 @@ export function profileAppServerCommand(_profile, availability = localAvailabili
18
18
  : { command: 'codex', args: ['app-server', '--stdio'] };
19
19
  return skillsConfig === undefined
20
20
  ? base
21
- : { ...base, args: [...base.args, '--config', `skills.config = [${skillsConfig.map((entry) => `{ path = ${JSON.stringify(entry.path)}, enabled = ${entry.enabled} }`).join(', ')}]`] };
21
+ : {
22
+ ...base,
23
+ args: [
24
+ ...base.args,
25
+ '--config',
26
+ `skills.config = [${skillsConfig.map((entry) => `{ path = ${JSON.stringify(entry.path)}, enabled = ${entry.enabled} }`).join(', ')}]`,
27
+ ],
28
+ };
22
29
  }
@@ -5,29 +5,68 @@
5
5
  */
6
6
  import { relative } from 'node:path';
7
7
  export function normalizeCodexNotification(sessionId, sequence, occurredAt, notification, workspacePath) {
8
- if (notification.method === 'item/agentMessage/delta')
8
+ const decoded = decodeNotification(notification);
9
+ if (!decoded)
10
+ return null;
11
+ if (decoded.method === 'item/agentMessage/delta')
9
12
  return {
10
13
  sessionId,
11
14
  sequence,
12
15
  occurredAt,
13
16
  type: 'agentMessageDelta',
14
- payload: { text: notification.params?.delta ?? '' },
17
+ payload: { text: decoded.params.delta },
15
18
  };
16
- if (notification.method === 'turn/completed')
19
+ if (decoded.method === 'turn/completed')
17
20
  return {
18
21
  sessionId,
19
22
  sequence,
20
23
  occurredAt,
21
24
  type: 'turnCompleted',
22
- payload: notification.params ?? {},
25
+ payload: decoded.params,
23
26
  };
24
- if (notification.method === 'item/started' || notification.method === 'item/completed') {
25
- const activity = safeActivity(notification.params?.item, workspacePath);
27
+ if (decoded.method === 'item/started' || decoded.method === 'item/completed') {
28
+ const activity = safeActivity(decoded.params.item, workspacePath);
26
29
  if (activity)
27
30
  return { sessionId, sequence, occurredAt, type: 'activity.updated', payload: activity };
28
31
  }
29
32
  return null;
30
33
  }
34
+ /** Strictly decode only consumed notification shapes; unknown/future methods stay isolated. */
35
+ export function decodeNotification(input) {
36
+ if (input.method === 'item/agentMessage/delta') {
37
+ const params = record(input.params);
38
+ return typeof params?.delta === 'string' && params.delta.length <= 64_000
39
+ ? { method: input.method, params: { delta: params.delta } }
40
+ : null;
41
+ }
42
+ if (input.method === 'turn/completed') {
43
+ const params = record(input.params);
44
+ const turn = record(params?.turn);
45
+ if (!turn || typeof turn.id !== 'string' || turn.id.length > 256)
46
+ return null;
47
+ return {
48
+ method: input.method,
49
+ params: {
50
+ ...(typeof params?.threadId === 'string' && params.threadId.length <= 256
51
+ ? { threadId: params.threadId }
52
+ : {}),
53
+ turn: {
54
+ id: turn.id,
55
+ ...(typeof turn.status === 'string' && turn.status.length <= 64
56
+ ? { status: turn.status }
57
+ : {}),
58
+ },
59
+ },
60
+ };
61
+ }
62
+ if (input.method === 'item/started' || input.method === 'item/completed') {
63
+ const params = record(input.params);
64
+ return params && 'item' in params
65
+ ? { method: input.method, params: { item: params.item } }
66
+ : null;
67
+ }
68
+ return null;
69
+ }
31
70
  function safeActivity(item, workspacePath) {
32
71
  if (!item || typeof item !== 'object')
33
72
  return null;
@@ -79,3 +118,8 @@ function reasoningSummary(parts) {
79
118
  return [];
80
119
  });
81
120
  }
121
+ function record(value) {
122
+ return value && typeof value === 'object' && !Array.isArray(value)
123
+ ? value
124
+ : null;
125
+ }
@@ -6,7 +6,25 @@
6
6
  export function protocolCompatibility(installedVersion, protocolVersion) {
7
7
  if (!installedVersion)
8
8
  return { compatible: false, code: 'CODEX_NOT_FOUND' };
9
- if (installedVersion !== protocolVersion)
9
+ const installed = parseCoreVersion(installedVersion);
10
+ const expected = protocolVersion ? parseCoreVersion(protocolVersion) : undefined;
11
+ // 0.x is pre-1.0: the minor is the core wire contract. Patch releases and
12
+ // newer compatible minors are accepted only within a small forward window;
13
+ // optional APIs are negotiated independently at runtime.
14
+ if (!installed || !expected || !coreCompatible(installed, expected))
10
15
  return { compatible: false, code: 'CODEX_PROTOCOL_MISMATCH' };
11
16
  return { compatible: true };
12
17
  }
18
+ function parseCoreVersion(value) {
19
+ const match = /\b(\d+)\.(\d+)(?:\.(\d+))?\b/.exec(value);
20
+ if (!match)
21
+ return undefined;
22
+ return { major: Number(match[1]), minor: Number(match[2]) };
23
+ }
24
+ function coreCompatible(installed, expected) {
25
+ if (installed.major !== expected.major)
26
+ return false;
27
+ if (installed.major !== 0)
28
+ return installed.minor === expected.minor;
29
+ return installed.minor >= expected.minor && installed.minor <= expected.minor + 4;
30
+ }
@@ -5,6 +5,8 @@
5
5
  */
6
6
  import { GESTALT_QUIZ_TOOL_NAME, parseQuiz } from '../../../shared/contracts/quiz.js';
7
7
  export function toPendingInteraction(input) {
8
+ if (!Number.isSafeInteger(input.id) || !isRecord(input.params))
9
+ return null;
8
10
  if (input.method === 'item/tool/call') {
9
11
  if (!isRecord(input.params) || input.params.tool !== GESTALT_QUIZ_TOOL_NAME)
10
12
  return null;