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.
- package/README.md +42 -11
- package/dist/client/assets/index-C5qFcFUg.css +1 -0
- package/dist/client/assets/index-tMyOolAm.js +24 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-ext-400-normal-Bh0R7Dhr.woff +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-ext-400-normal-C_uLvvQ5.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/client/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/client/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-ByoDsISC.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/client/assets/syne-greek-600-normal-BJs4kvyH.woff +0 -0
- package/dist/client/assets/syne-greek-600-normal-BpFII5Ja.woff2 +0 -0
- package/dist/client/assets/syne-latin-600-normal-BiwQbQXw.woff2 +0 -0
- package/dist/client/assets/syne-latin-600-normal-dRu9QuIh.woff +0 -0
- package/dist/client/assets/syne-latin-ext-600-normal-DEzBMW8B.woff +0 -0
- package/dist/client/assets/syne-latin-ext-600-normal-ZnizrDKU.woff2 +0 -0
- package/dist/client/index.html +3 -2
- package/dist/server/server/cli.js +3 -1
- package/dist/server/server/composition.js +25 -21
- package/dist/server/server/features/auth/devices/revoke/endpoint.js +3 -1
- package/dist/server/server/features/auth/enrollment-tickets/endpoint.js +7 -2
- package/dist/server/server/features/auth/enrollment-tickets/status/endpoint.js +3 -1
- package/dist/server/server/features/auth/http/cookies.js +12 -2
- package/dist/server/server/features/auth/register/verify/endpoint.js +70 -15
- package/dist/server/server/features/plans/application/measurement-snapshot.js +1 -1
- package/dist/server/server/features/plans/get-measurement/endpoint.js +1 -3
- package/dist/server/server/features/sessions/interaction/kind.js +7 -1
- package/dist/server/server/features/sessions/list-recent-threads/endpoint.js +3 -1
- package/dist/server/server/features/sessions/model/relay-session.js +3 -1
- package/dist/server/server/features/sessions/start-session/use-case.js +1 -1
- package/dist/server/server/features/skills/delete-profile/endpoint.js +12 -3
- package/dist/server/server/features/skills/list-available/endpoint.js +24 -6
- package/dist/server/server/features/skills/list-profiles/endpoint.js +29 -5
- package/dist/server/server/features/skills/model/skill-profile.js +23 -3
- package/dist/server/server/platform/auth/simple-webauthn-adapter.js +23 -3
- package/dist/server/server/platform/catalog/profile-command.js +8 -1
- package/dist/server/server/platform/codex/normalizer.js +22 -4
- package/dist/server/server/platform/codex/session-runtime.js +52 -11
- package/dist/server/server/platform/codex/thread-plan-name.js +5 -2
- package/dist/server/server/platform/http/problem-reply.js +4 -1
- package/dist/server/server/platform/plans/filesystem-plan-status-source.js +11 -6
- package/dist/server/server/platform/plans/filesystem-workspace-plan-catalog.js +4 -1
- package/dist/server/server/platform/plans/plan-measurement-command.js +4 -1
- package/dist/server/server/platform/skills/codex-skill-catalog.js +29 -7
- package/dist/server/server/platform/skills/filesystem-skill-profile-store.js +5 -1
- package/dist/server/shared/contracts/quiz.js +30 -9
- package/package.json +3 -1
- package/dist/client/assets/index-CMs5ZzxJ.css +0 -1
- package/dist/client/assets/index-D2sT9ZHY.js +0 -19
|
Binary file
|
|
Binary file
|
package/dist/client/index.html
CHANGED
|
@@ -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-
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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]
|
|
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
|
|
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
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
219
|
+
webauthn: options.authorizationWebauthn ?? new SimpleWebAuthnAdapter(),
|
|
220
|
+
relyingParty,
|
|
216
221
|
},
|
|
217
|
-
|
|
218
|
-
|
|
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: '/',
|
|
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 ||
|
|
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({
|
|
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
|
|
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 = {
|
|
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 {
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
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
|
|
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
|
|
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({
|
|
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 = {
|
|
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({
|
|
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
|
|
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
|
|
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) =>
|
|
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 [
|
|
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
|
|
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
|
-
: {
|
|
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
|
|
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
|
|
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
|
|
21
|
-
|
|
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
|
|
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
|
|
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 &&
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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
|
-
? {
|
|
16
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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: {
|
|
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({
|
|
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 {
|
|
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
|
-
: {
|
|
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
|
}
|