gestalt-mobile 0.13.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 (54) 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/cli.js +3 -1
  24. package/dist/server/server/composition.js +25 -21
  25. package/dist/server/server/features/auth/devices/revoke/endpoint.js +3 -1
  26. package/dist/server/server/features/auth/enrollment-tickets/endpoint.js +7 -2
  27. package/dist/server/server/features/auth/enrollment-tickets/status/endpoint.js +3 -1
  28. package/dist/server/server/features/auth/http/cookies.js +12 -2
  29. package/dist/server/server/features/auth/register/verify/endpoint.js +70 -15
  30. package/dist/server/server/features/plans/application/measurement-snapshot.js +1 -1
  31. package/dist/server/server/features/plans/get-measurement/endpoint.js +1 -3
  32. package/dist/server/server/features/sessions/interaction/kind.js +7 -1
  33. package/dist/server/server/features/sessions/list-recent-threads/endpoint.js +3 -1
  34. package/dist/server/server/features/sessions/model/relay-session.js +3 -1
  35. package/dist/server/server/features/sessions/start-session/use-case.js +1 -1
  36. package/dist/server/server/features/skills/delete-profile/endpoint.js +12 -3
  37. package/dist/server/server/features/skills/list-available/endpoint.js +24 -6
  38. package/dist/server/server/features/skills/list-profiles/endpoint.js +29 -5
  39. package/dist/server/server/features/skills/model/skill-profile.js +23 -3
  40. package/dist/server/server/platform/auth/simple-webauthn-adapter.js +23 -3
  41. package/dist/server/server/platform/catalog/profile-command.js +8 -1
  42. package/dist/server/server/platform/codex/normalizer.js +22 -4
  43. package/dist/server/server/platform/codex/session-runtime.js +52 -11
  44. package/dist/server/server/platform/codex/thread-plan-name.js +5 -2
  45. package/dist/server/server/platform/http/problem-reply.js +4 -1
  46. package/dist/server/server/platform/plans/filesystem-plan-status-source.js +11 -6
  47. package/dist/server/server/platform/plans/filesystem-workspace-plan-catalog.js +4 -1
  48. package/dist/server/server/platform/plans/plan-measurement-command.js +4 -1
  49. package/dist/server/server/platform/skills/codex-skill-catalog.js +29 -7
  50. package/dist/server/server/platform/skills/filesystem-skill-profile-store.js +5 -1
  51. package/dist/server/shared/contracts/quiz.js +30 -9
  52. package/package.json +3 -1
  53. package/dist/client/assets/index-CMs5ZzxJ.css +0 -1
  54. package/dist/client/assets/index-D2sT9ZHY.js +0 -19
@@ -9,9 +9,10 @@ SPDX-License-Identifier: AGPL-3.0-or-later
9
9
  <head>
10
10
  <meta charset="UTF-8" />
11
11
  <meta name="viewport" content="width=device-width, initial-scale=1" />
12
+ <meta name="color-scheme" content="light dark" />
12
13
  <title>Codex Relay</title>
13
- <script type="module" crossorigin src="/assets/index-D2sT9ZHY.js"></script>
14
- <link rel="stylesheet" crossorigin href="/assets/index-CMs5ZzxJ.css">
14
+ <script type="module" crossorigin src="/assets/index-tMyOolAm.js"></script>
15
+ <link rel="stylesheet" crossorigin href="/assets/index-C5qFcFUg.css">
15
16
  </head>
16
17
  <body>
17
18
  <div id="app"></div>
@@ -99,7 +99,9 @@ async function listSkillProfiles(store, stdout, stderr) {
99
99
  if (!profile)
100
100
  continue;
101
101
  stdout.write(`${profile.name}\n ${store.globalProfilePath(name)}\n`);
102
- for (const skill of [...profile.skills].filter((entry) => entry.enabled).sort((a, b) => a.name.localeCompare(b.name) || a.path.localeCompare(b.path)))
102
+ for (const skill of [...profile.skills]
103
+ .filter((entry) => entry.enabled)
104
+ .sort((a, b) => a.name.localeCompare(b.name) || a.path.localeCompare(b.path)))
103
105
  stdout.write(` ${skill.name}\t${skill.path}\n`);
104
106
  }
105
107
  catch (error) {
@@ -44,7 +44,7 @@ import { FilesystemPlanStatusSource } from './platform/plans/filesystem-plan-sta
44
44
  import { FilesystemWorkspacePlanCatalog } from './platform/plans/filesystem-workspace-plan-catalog.js';
45
45
  import { checkpointPlanMeasurement } from './platform/plans/plan-measurement-command.js';
46
46
  import { PlanMeasurementRefresh } from './platform/plans/plan-measurement-refresh.js';
47
- import { createRelyingPartyConfig, } from './config.js';
47
+ import { createRelyingPartyConfig } from './config.js';
48
48
  const generatedProtocolVersion = 'codex-cli 0.144.3';
49
49
  export async function composeRelayApp(options) {
50
50
  const passkeyAuthEnabled = options.passkeyAuthEnabled ?? true;
@@ -194,29 +194,33 @@ export async function composeRelayApp(options) {
194
194
  let app;
195
195
  try {
196
196
  app = await buildApp({
197
- ...(authorization ? { auth: {
198
- repository: authorization,
199
- clock: { now: options.authorizationClock ?? (() => new Date()) },
200
- random: {
201
- bytes: (length) => {
202
- const value = authorizationRandom(length);
203
- if (length !== 32 || value.length !== 32)
204
- throw new Error('Authorization randomness must return exactly 32 bytes');
205
- return value;
197
+ ...(authorization
198
+ ? {
199
+ auth: {
200
+ repository: authorization,
201
+ clock: { now: options.authorizationClock ?? (() => new Date()) },
202
+ random: {
203
+ bytes: (length) => {
204
+ const value = authorizationRandom(length);
205
+ if (length !== 32 || value.length !== 32)
206
+ throw new Error('Authorization randomness must return exactly 32 bytes');
207
+ return value;
208
+ },
206
209
  },
207
- },
208
- identifiers: {
209
- sessionId: () => {
210
- const value = authorizationRandom(32);
211
- if (value.length !== 32)
212
- throw new Error('Authorization randomness must return exactly 32 bytes');
213
- return authorizationSessionId(Buffer.from(value).toString('base64url'));
210
+ identifiers: {
211
+ sessionId: () => {
212
+ const value = authorizationRandom(32);
213
+ if (value.length !== 32)
214
+ throw new Error('Authorization randomness must return exactly 32 bytes');
215
+ return authorizationSessionId(Buffer.from(value).toString('base64url'));
216
+ },
217
+ deviceId: () => authorizedDeviceId(randomUUID()),
214
218
  },
215
- deviceId: () => authorizedDeviceId(randomUUID()),
219
+ webauthn: options.authorizationWebauthn ?? new SimpleWebAuthnAdapter(),
220
+ relyingParty,
216
221
  },
217
- webauthn: options.authorizationWebauthn ?? new SimpleWebAuthnAdapter(),
218
- relyingParty,
219
- } } : { passkeyAuthDisabled: true }),
222
+ }
223
+ : { passkeyAuthDisabled: true }),
220
224
  health: {
221
225
  async read() {
222
226
  return {
@@ -28,7 +28,9 @@ export function registerRevokeAuthorizedDevice(app, deps) {
28
28
  return unavailable(reply, 404, 'DEVICE_NOT_AVAILABLE');
29
29
  if (current === id)
30
30
  reply.clearCookie('gestalt_mobile_session', {
31
- path: '/', httpOnly: true, sameSite: 'strict',
31
+ path: '/',
32
+ httpOnly: true,
33
+ sameSite: 'strict',
32
34
  secure: deps.relyingParty.publicOrigin.startsWith('https://'),
33
35
  });
34
36
  return reply.code(204).send();
@@ -4,7 +4,8 @@ export function registerCreateEnrollmentTicket(app, deps) {
4
4
  app.post('/api/auth/enrollment-tickets', async (request, reply) => {
5
5
  const rawSession = request.cookies.gestalt_mobile_session;
6
6
  const session = parseAuthorizationSessionId(rawSession);
7
- if (session === null || deps.repository.sessionDevice(session, deps.clock.now().toISOString()) === null)
7
+ if (session === null ||
8
+ deps.repository.sessionDevice(session, deps.clock.now().toISOString()) === null)
8
9
  return reply.code(401).send();
9
10
  const entropy = deps.random.bytes(32);
10
11
  if (entropy.length < 32)
@@ -12,6 +13,10 @@ export function registerCreateEnrollmentTicket(app, deps) {
12
13
  const ticket = enrollmentTicketId(Buffer.from(entropy).toString('base64url'));
13
14
  const expiresAt = new Date(deps.clock.now().getTime() + lifetimeMs).toISOString();
14
15
  deps.repository.issueEnrollmentTicket(ticket, session, expiresAt);
15
- return reply.code(201).send({ ticket, url: `${deps.relyingParty.publicOrigin.replace(/\/$/, '')}/#enroll=${ticket}`, expiresAt });
16
+ return reply.code(201).send({
17
+ ticket,
18
+ url: `${deps.relyingParty.publicOrigin.replace(/\/$/, '')}/#enroll=${ticket}`,
19
+ expiresAt,
20
+ });
16
21
  });
17
22
  }
@@ -1,6 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import { parseAuthorizationSessionId } from '../../domain/identifiers.js';
3
- const responseSchema = z.object({ status: z.enum(['none', 'pending', 'used', 'expired']) }).strict();
3
+ const responseSchema = z
4
+ .object({ status: z.enum(['none', 'pending', 'used', 'expired']) })
5
+ .strict();
4
6
  export function registerEnrollmentTicketStatus(app, deps) {
5
7
  app.get('/api/auth/enrollment-tickets/current', async (request, reply) => {
6
8
  const rawSession = request.cookies.gestalt_mobile_session;
@@ -1,6 +1,16 @@
1
- const seconds = { gestalt_mobile_login: 600, gestalt_mobile_registration: 600, gestalt_mobile_session: 30 * 24 * 60 * 60 };
1
+ const seconds = {
2
+ gestalt_mobile_login: 600,
3
+ gestalt_mobile_registration: 600,
4
+ gestalt_mobile_session: 30 * 24 * 60 * 60,
5
+ };
2
6
  function options(name, origin) {
3
- return { httpOnly: true, sameSite: 'strict', path: '/', secure: origin.startsWith('https://'), maxAge: seconds[name] };
7
+ return {
8
+ httpOnly: true,
9
+ sameSite: 'strict',
10
+ path: '/',
11
+ secure: origin.startsWith('https://'),
12
+ maxAge: seconds[name],
13
+ };
4
14
  }
5
15
  export function setAuthCookie(reply, name, value, origin) {
6
16
  reply.setCookie(name, value, options(name, origin));
@@ -1,16 +1,34 @@
1
1
  import { z } from 'zod';
2
2
  import { problem } from '../../../../platform/http/problem.js';
3
- import { PasskeyVerificationError } from '../../application/ports.js';
3
+ import { PasskeyVerificationError, } from '../../application/ports.js';
4
4
  import { deviceNickname } from '../../domain/device-nickname.js';
5
5
  import { passkeyCeremonyId } from '../../domain/identifiers.js';
6
6
  import { AuthorizationDomainError } from '../../domain/errors.js';
7
7
  import { clearAuthCookie, setAuthCookie } from '../../http/cookies.js';
8
- const registrationResponseSchema = z.object({
9
- id: z.string().min(1), rawId: z.string().min(1), type: z.literal('public-key'),
10
- response: z.object({ clientDataJSON: z.string().min(1), attestationObject: z.string().min(1), authenticatorData: z.string().min(1).optional(), transports: z.array(z.enum(['ble', 'hybrid', 'internal', 'nfc', 'usb', 'cable', 'smart-card'])).optional(), publicKeyAlgorithm: z.number().int().optional(), publicKey: z.string().min(1).optional() }).strict(),
11
- authenticatorAttachment: z.enum(['platform', 'cross-platform']).optional(), clientExtensionResults: z.record(z.string(), z.unknown()),
12
- }).strict();
13
- const requestSchema = z.object({ response: registrationResponseSchema, nickname: z.string() }).strict();
8
+ const registrationResponseSchema = z
9
+ .object({
10
+ id: z.string().min(1),
11
+ rawId: z.string().min(1),
12
+ type: z.literal('public-key'),
13
+ response: z
14
+ .object({
15
+ clientDataJSON: z.string().min(1),
16
+ attestationObject: z.string().min(1),
17
+ authenticatorData: z.string().min(1).optional(),
18
+ transports: z
19
+ .array(z.enum(['ble', 'hybrid', 'internal', 'nfc', 'usb', 'cable', 'smart-card']))
20
+ .optional(),
21
+ publicKeyAlgorithm: z.number().int().optional(),
22
+ publicKey: z.string().min(1).optional(),
23
+ })
24
+ .strict(),
25
+ authenticatorAttachment: z.enum(['platform', 'cross-platform']).optional(),
26
+ clientExtensionResults: z.record(z.string(), z.unknown()),
27
+ })
28
+ .strict();
29
+ const requestSchema = z
30
+ .object({ response: registrationResponseSchema, nickname: z.string() })
31
+ .strict();
14
32
  const successSchema = z.object({ status: z.literal('authenticated') }).strict();
15
33
  const sessionLifetimeMs = 30 * 24 * 60 * 60 * 1000;
16
34
  export function registerRegistrationVerification(app, deps) {
@@ -18,39 +36,76 @@ export function registerRegistrationVerification(app, deps) {
18
36
  const parsed = requestSchema.safeParse(request.body);
19
37
  const token = request.cookies.gestalt_mobile_registration;
20
38
  if (!parsed.success || typeof token !== 'string')
21
- return reply.code(400).type('application/problem+json').send(problem('INVALID_REGISTRATION_REQUEST', 400, 'The registration request is invalid.'));
39
+ return reply
40
+ .code(400)
41
+ .type('application/problem+json')
42
+ .send(problem('INVALID_REGISTRATION_REQUEST', 400, 'The registration request is invalid.'));
22
43
  let nickname;
23
44
  try {
24
45
  nickname = deviceNickname(parsed.data.nickname);
25
46
  }
26
47
  catch (error) {
27
48
  if (error instanceof AuthorizationDomainError)
28
- return reply.code(400).type('application/problem+json').send(problem('INVALID_DEVICE_NICKNAME', 400, 'The device nickname is invalid.'));
49
+ return reply
50
+ .code(400)
51
+ .type('application/problem+json')
52
+ .send(problem('INVALID_DEVICE_NICKNAME', 400, 'The device nickname is invalid.'));
29
53
  throw error;
30
54
  }
31
55
  const now = deps.clock.now();
32
56
  const ceremony = deps.repository.readCeremony(passkeyCeremonyId(token), now.toISOString());
33
57
  if (!ceremony || ceremony.purpose !== 'registration')
34
- return reply.code(400).type('application/problem+json').send(problem('REGISTRATION_NOT_AVAILABLE', 400, 'Registration could not be completed.'));
58
+ return reply
59
+ .code(400)
60
+ .type('application/problem+json')
61
+ .send(problem('REGISTRATION_NOT_AVAILABLE', 400, 'Registration could not be completed.'));
35
62
  try {
36
- const verified = await deps.webauthn.verifyRegistration({ response: parsed.data.response, challenge: ceremony.challenge, expectedOrigin: ceremony.expectedOrigin, rpId: ceremony.rpId });
63
+ const verified = await deps.webauthn.verifyRegistration({
64
+ response: parsed.data.response,
65
+ challenge: ceremony.challenge,
66
+ expectedOrigin: ceremony.expectedOrigin,
67
+ rpId: ceremony.rpId,
68
+ });
37
69
  if (!verified.userVerified)
38
70
  throw new PasskeyVerificationError('PASSKEY_VERIFICATION_FAILED');
39
- const device = { id: deps.identifiers.deviceId(), credentialId: verified.credentialId, publicKey: verified.publicKey, counter: verified.counter, transports: verified.transports, deviceType: verified.deviceType, backedUp: verified.backedUp, nickname, createdAt: now.toISOString() };
71
+ const device = {
72
+ id: deps.identifiers.deviceId(),
73
+ credentialId: verified.credentialId,
74
+ publicKey: verified.publicKey,
75
+ counter: verified.counter,
76
+ transports: verified.transports,
77
+ deviceType: verified.deviceType,
78
+ backedUp: verified.backedUp,
79
+ nickname,
80
+ createdAt: now.toISOString(),
81
+ };
40
82
  const session = deps.identifiers.sessionId();
41
83
  if (Buffer.from(session, 'base64url').length !== 32)
42
84
  throw new Error('AUTHORIZATION_RANDOMNESS_INVALID');
43
85
  const expiresAt = new Date(now.getTime() + sessionLifetimeMs).toISOString();
44
- const outcome = deps.repository.completeRegistration({ ceremony: passkeyCeremonyId(token), now: now.toISOString(), device, session: { id: session, deviceId: device.id, expiresAt } });
86
+ const outcome = deps.repository.completeRegistration({
87
+ ceremony: passkeyCeremonyId(token),
88
+ now: now.toISOString(),
89
+ device,
90
+ session: { id: session, deviceId: device.id, expiresAt },
91
+ });
45
92
  if (outcome !== 'registered')
46
- return reply.code(outcome === 'bootstrapAlreadyClaimed' ? 409 : 400).type('application/problem+json').send(problem(outcome === 'bootstrapAlreadyClaimed' ? 'BOOTSTRAP_ALREADY_CLAIMED' : 'REGISTRATION_NOT_AVAILABLE', outcome === 'bootstrapAlreadyClaimed' ? 409 : 400, 'Registration could not be completed.'));
93
+ return reply
94
+ .code(outcome === 'bootstrapAlreadyClaimed' ? 409 : 400)
95
+ .type('application/problem+json')
96
+ .send(problem(outcome === 'bootstrapAlreadyClaimed'
97
+ ? 'BOOTSTRAP_ALREADY_CLAIMED'
98
+ : 'REGISTRATION_NOT_AVAILABLE', outcome === 'bootstrapAlreadyClaimed' ? 409 : 400, 'Registration could not be completed.'));
47
99
  setAuthCookie(reply, 'gestalt_mobile_session', session, deps.relyingParty.publicOrigin);
48
100
  clearAuthCookie(reply, 'gestalt_mobile_registration', deps.relyingParty.publicOrigin);
49
101
  return reply.code(201).send(successSchema.parse({ status: 'authenticated' }));
50
102
  }
51
103
  catch (error) {
52
104
  if (error instanceof PasskeyVerificationError || error instanceof AuthorizationDomainError)
53
- return reply.code(400).type('application/problem+json').send(problem('REGISTRATION_VERIFICATION_FAILED', 400, 'Registration could not be completed.'));
105
+ return reply
106
+ .code(400)
107
+ .type('application/problem+json')
108
+ .send(problem('REGISTRATION_VERIFICATION_FAILED', 400, 'Registration could not be completed.'));
54
109
  throw error;
55
110
  }
56
111
  });
@@ -19,7 +19,7 @@ export function weeklyRateLimitWindow(windows) {
19
19
  Number.isFinite(window.usedPercent) &&
20
20
  window.usedPercent >= 0 &&
21
21
  window.usedPercent <= 100);
22
- return valid.reduce((weekly, window) => (!weekly || window.durationSeconds > weekly.durationSeconds ? window : weekly), null);
22
+ return valid.reduce((weekly, window) => !weekly || window.durationSeconds > weekly.durationSeconds ? window : weekly, null);
23
23
  }
24
24
  export function weeklyRemainingPercent(windows) {
25
25
  const weekly = weeklyRateLimitWindow(windows);
@@ -6,9 +6,7 @@
6
6
  export function registerGetPlanMeasurement(app, deps) {
7
7
  app.get('/api/sessions/:id/plan-measurement', async (request, reply) => {
8
8
  const id = request.params.id;
9
- const authorization = typeof request.headers.authorization === 'string'
10
- ? request.headers.authorization
11
- : undefined;
9
+ const authorization = typeof request.headers.authorization === 'string' ? request.headers.authorization : undefined;
12
10
  if (!deps.exists(id) || !deps.authorize(id, authorization))
13
11
  return reply.code(404).send({ code: 'SESSION_NOT_FOUND' });
14
12
  try {
@@ -4,5 +4,11 @@
4
4
  * SPDX-License-Identifier: AGPL-3.0-or-later
5
5
  */
6
6
  export function isInteractionKind(value) {
7
- return ['commandApproval', 'fileChangeApproval', 'permissionsApproval', 'userInput', 'quiz'].includes(value);
7
+ return [
8
+ 'commandApproval',
9
+ 'fileChangeApproval',
10
+ 'permissionsApproval',
11
+ 'userInput',
12
+ 'quiz',
13
+ ].includes(value);
8
14
  }
@@ -15,7 +15,9 @@ export function registerListRecentThreads(app, deps) {
15
15
  recencyAt,
16
16
  ...(metadata?.model === undefined ? {} : { model: metadata.model }),
17
17
  ...(metadata?.skillProfile === undefined ? {} : { skillProfile: metadata.skillProfile }),
18
- ...(metadata?.orgPlanFilename === undefined ? {} : { orgPlanFilename: metadata.orgPlanFilename }),
18
+ ...(metadata?.orgPlanFilename === undefined
19
+ ? {}
20
+ : { orgPlanFilename: metadata.orgPlanFilename }),
19
21
  resumeCommand: buildResumeCommand({ profile, threadId: id, workspacePath: cwd }),
20
22
  };
21
23
  });
@@ -136,7 +136,9 @@ function copy(snapshot) {
136
136
  ...snapshot,
137
137
  ...(snapshot.effectiveSkillSelection === undefined
138
138
  ? {}
139
- : { effectiveSkillSelection: createEffectiveSkillSelection(snapshot.effectiveSkillSelection) }),
139
+ : {
140
+ effectiveSkillSelection: createEffectiveSkillSelection(snapshot.effectiveSkillSelection),
141
+ }),
140
142
  pendingInteractions: snapshot.pendingInteractions.map((item) => ({ ...item })),
141
143
  };
142
144
  }
@@ -4,7 +4,7 @@
4
4
  * SPDX-License-Identifier: AGPL-3.0-or-later
5
5
  */
6
6
  import { RelaySession } from '../model/relay-session.js';
7
- import { DEFAULT_SESSION_MODEL, } from '../application/start-settings.js';
7
+ import { DEFAULT_SESSION_MODEL } from '../application/start-settings.js';
8
8
  import { applySkillSelectionSnapshot, } from '../../skills/model/skill-profile.js';
9
9
  import { SkillProfileError } from '../../skills/model/errors.js';
10
10
  export async function startSession(input, deps) {
@@ -12,13 +12,22 @@ export function registerDeleteSkillProfile(app, deps) {
12
12
  try {
13
13
  const name = normalizeSkillProfileName(request.params.name);
14
14
  if (!(await deps.deleteGlobalProfile(name)))
15
- return reply.code(404).type('application/problem+json').send(problem('SKILL_PROFILE_NOT_FOUND', 404, 'The skill profile was not found.'));
15
+ return reply
16
+ .code(404)
17
+ .type('application/problem+json')
18
+ .send(problem('SKILL_PROFILE_NOT_FOUND', 404, 'The skill profile was not found.'));
16
19
  return reply.code(204).send();
17
20
  }
18
21
  catch (error) {
19
22
  if (error instanceof SkillProfileError)
20
- return reply.code(400).type('application/problem+json').send(problem('INVALID_SKILL_PROFILE', 400, 'The skill profile could not be deleted.'));
21
- return reply.code(500).type('application/problem+json').send(problem('SKILL_PROFILE_PERSISTENCE_FAILED', 500, 'The skill profile could not be deleted.', true));
23
+ return reply
24
+ .code(400)
25
+ .type('application/problem+json')
26
+ .send(problem('INVALID_SKILL_PROFILE', 400, 'The skill profile could not be deleted.'));
27
+ return reply
28
+ .code(500)
29
+ .type('application/problem+json')
30
+ .send(problem('SKILL_PROFILE_PERSISTENCE_FAILED', 500, 'The skill profile could not be deleted.', true));
22
31
  }
23
32
  });
24
33
  }
@@ -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) {
@@ -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
  }
@@ -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
  }