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.
- 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/app.js +22 -227
- package/dist/server/server/cli.js +3 -1
- package/dist/server/server/composition.js +25 -26
- package/dist/server/server/features/auth/application/ceremony-attempts.js +22 -0
- package/dist/server/server/features/auth/devices/revoke/endpoint.js +3 -1
- package/dist/server/server/features/auth/domain/errors.js +7 -0
- 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 +20 -0
- package/dist/server/server/features/auth/login/options/endpoint.js +22 -20
- package/dist/server/server/features/auth/login/verify/endpoint.js +6 -14
- package/dist/server/server/features/auth/logout/endpoint.js +2 -6
- package/dist/server/server/features/auth/register/options/endpoint.js +43 -11
- package/dist/server/server/features/auth/register/verify/endpoint.js +74 -18
- package/dist/server/server/features/auth/register-routes.js +41 -0
- package/dist/server/server/features/git/checkout-branch/endpoint.js +1 -1
- package/dist/server/server/features/git/clone-repository/endpoint.js +1 -1
- package/dist/server/server/features/git/register-routes.js +34 -0
- 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/plans/register-routes.js +22 -0
- 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/promote-recent-thread/endpoint.js +1 -1
- package/dist/server/server/features/sessions/register-routes.js +138 -0
- package/dist/server/server/features/sessions/respond-interaction/endpoint.js +1 -1
- package/dist/server/server/features/sessions/select-model/endpoint.js +1 -1
- package/dist/server/server/features/sessions/start-session/endpoint.js +5 -2
- package/dist/server/server/features/sessions/start-session/use-case.js +1 -1
- package/dist/server/server/features/sessions/start-turn/endpoint.js +2 -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/features/skills/register-routes.js +17 -0
- package/dist/server/server/features/skills/replace-profile/endpoint.js +20 -5
- package/dist/server/server/platform/auth/simple-webauthn-adapter.js +23 -3
- package/dist/server/server/platform/auth/sqlite-authorization-store.js +29 -6
- package/dist/server/server/platform/catalog/profile-command.js +8 -1
- package/dist/server/server/platform/codex/normalizer.js +50 -6
- package/dist/server/server/platform/codex/protocol-compatibility.js +19 -1
- package/dist/server/server/platform/codex/server-request.js +2 -0
- package/dist/server/server/platform/codex/session-runtime.js +235 -126
- package/dist/server/server/platform/codex/thread-plan-name.js +5 -2
- package/dist/server/server/platform/http/problem-handler.js +10 -0
- package/dist/server/server/platform/http/problem-reply.js +13 -0
- 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 +4 -1
- package/dist/client/assets/index-B7KwEjTE.js +0 -19
- package/dist/client/assets/index-CMs5ZzxJ.css +0 -1
- package/dist/server/server/features/plans/application/native-plan-alignment.js +0 -39
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
2
|
+
import { sendProblem } from '../../../../platform/http/problem-reply.js';
|
|
3
3
|
import { parseEnrollmentTicketId, passkeyCeremonyId } from '../../domain/identifiers.js';
|
|
4
|
+
import { CeremonyCapacityError } from '../../domain/errors.js';
|
|
5
|
+
import { setAuthCookie } from '../../http/cookies.js';
|
|
4
6
|
const requestSchema = z.object({ enrollmentTicket: z.string().min(1).optional() }).strict();
|
|
5
|
-
const optionsResponseSchema = z
|
|
7
|
+
const optionsResponseSchema = z
|
|
8
|
+
.object({ options: z.object({ challenge: z.string().min(1) }).passthrough() })
|
|
9
|
+
.strict();
|
|
6
10
|
const lifetimeMs = 10 * 60 * 1000;
|
|
7
11
|
export function registerRegistrationOptions(app, deps) {
|
|
8
|
-
app.post('/api/auth/register/options', async (request, reply) => {
|
|
12
|
+
app.post('/api/auth/register/options', { bodyLimit: 4 * 1024 }, async (request, reply) => {
|
|
13
|
+
if (!deps.ceremonyAttempts.allow(`register:${request.ip}`, deps.clock.now()))
|
|
14
|
+
return sendProblem(reply, 'REGISTRATION_NOT_AVAILABLE', 400, 'Registration could not be completed.');
|
|
9
15
|
const parsed = requestSchema.safeParse(request.body);
|
|
10
16
|
if (!parsed.success)
|
|
11
|
-
return reply
|
|
12
|
-
const ticket = parsed.data.enrollmentTicket === undefined
|
|
17
|
+
return sendProblem(reply, 'INVALID_REGISTRATION_REQUEST', 400, 'The registration request is invalid.');
|
|
18
|
+
const ticket = parsed.data.enrollmentTicket === undefined
|
|
19
|
+
? undefined
|
|
20
|
+
: parseEnrollmentTicketId(parsed.data.enrollmentTicket);
|
|
13
21
|
if (ticket === null)
|
|
14
|
-
return reply
|
|
22
|
+
return sendProblem(reply, 'INVALID_REGISTRATION_REQUEST', 400, 'The registration request is invalid.');
|
|
15
23
|
const owner = deps.repository.readOwner();
|
|
16
24
|
const devices = deps.repository.listAuthorizedDevices();
|
|
17
25
|
const now = deps.clock.now();
|
|
18
|
-
if (devices.length > 0 &&
|
|
19
|
-
|
|
26
|
+
if (devices.length > 0 &&
|
|
27
|
+
(!ticket || !deps.repository.ticketAvailable(ticket, now.toISOString())))
|
|
28
|
+
return sendProblem(reply, 'ENROLLMENT_NOT_AUTHORIZED', 403, 'Registration is not authorized.');
|
|
20
29
|
const ownerHandle = owner ? undefined : deps.random.bytes(32);
|
|
21
30
|
if (ownerHandle && ownerHandle.length !== 32)
|
|
22
31
|
throw new Error('AUTHORIZATION_RANDOMNESS_INVALID');
|
|
@@ -26,9 +35,32 @@ export function registerRegistrationOptions(app, deps) {
|
|
|
26
35
|
if (correlation.length !== 32 || challenge.length !== 32)
|
|
27
36
|
throw new Error('AUTHORIZATION_RANDOMNESS_INVALID');
|
|
28
37
|
const token = passkeyCeremonyId(Buffer.from(correlation).toString('base64url'));
|
|
29
|
-
const options = optionsResponseSchema.parse({
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
const options = optionsResponseSchema.parse({
|
|
39
|
+
options: await deps.webauthn.registrationOptions({
|
|
40
|
+
challenge,
|
|
41
|
+
rpId: deps.relyingParty.rpId,
|
|
42
|
+
userVerification: 'required',
|
|
43
|
+
rpName: deps.relyingParty.rpName,
|
|
44
|
+
userHandle: initialized.userHandle,
|
|
45
|
+
excludeCredentialIds: devices.map((device) => device.credentialId),
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
try {
|
|
49
|
+
deps.repository.saveCeremony(token, {
|
|
50
|
+
purpose: 'registration',
|
|
51
|
+
challenge,
|
|
52
|
+
expectedOrigin: deps.relyingParty.publicOrigin,
|
|
53
|
+
rpId: deps.relyingParty.rpId,
|
|
54
|
+
expiresAt: new Date(now.getTime() + lifetimeMs).toISOString(),
|
|
55
|
+
...(ticket ? { enrollmentTicket: ticket } : {}),
|
|
56
|
+
}, now.toISOString());
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (error instanceof CeremonyCapacityError)
|
|
60
|
+
return sendProblem(reply, 'REGISTRATION_NOT_AVAILABLE', 400, 'Registration could not be completed.');
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
setAuthCookie(reply, 'gestalt_mobile_registration', token, deps.relyingParty.publicOrigin);
|
|
32
64
|
return reply.send(options);
|
|
33
65
|
});
|
|
34
66
|
}
|
|
@@ -1,55 +1,111 @@
|
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
import { clearAuthCookie, setAuthCookie } from '../../http/cookies.js';
|
|
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();
|
|
13
32
|
const successSchema = z.object({ status: z.literal('authenticated') }).strict();
|
|
14
33
|
const sessionLifetimeMs = 30 * 24 * 60 * 60 * 1000;
|
|
15
34
|
export function registerRegistrationVerification(app, deps) {
|
|
16
|
-
app.post('/api/auth/register/verify', async (request, reply) => {
|
|
35
|
+
app.post('/api/auth/register/verify', { bodyLimit: 128 * 1024 }, async (request, reply) => {
|
|
17
36
|
const parsed = requestSchema.safeParse(request.body);
|
|
18
37
|
const token = request.cookies.gestalt_mobile_registration;
|
|
19
38
|
if (!parsed.success || typeof token !== 'string')
|
|
20
|
-
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.'));
|
|
21
43
|
let nickname;
|
|
22
44
|
try {
|
|
23
45
|
nickname = deviceNickname(parsed.data.nickname);
|
|
24
46
|
}
|
|
25
47
|
catch (error) {
|
|
26
48
|
if (error instanceof AuthorizationDomainError)
|
|
27
|
-
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.'));
|
|
28
53
|
throw error;
|
|
29
54
|
}
|
|
30
55
|
const now = deps.clock.now();
|
|
31
56
|
const ceremony = deps.repository.readCeremony(passkeyCeremonyId(token), now.toISOString());
|
|
32
57
|
if (!ceremony || ceremony.purpose !== 'registration')
|
|
33
|
-
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.'));
|
|
34
62
|
try {
|
|
35
|
-
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
|
+
});
|
|
36
69
|
if (!verified.userVerified)
|
|
37
70
|
throw new PasskeyVerificationError('PASSKEY_VERIFICATION_FAILED');
|
|
38
|
-
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
|
+
};
|
|
39
82
|
const session = deps.identifiers.sessionId();
|
|
40
83
|
if (Buffer.from(session, 'base64url').length !== 32)
|
|
41
84
|
throw new Error('AUTHORIZATION_RANDOMNESS_INVALID');
|
|
42
85
|
const expiresAt = new Date(now.getTime() + sessionLifetimeMs).toISOString();
|
|
43
|
-
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
|
+
});
|
|
44
92
|
if (outcome !== 'registered')
|
|
45
|
-
return reply
|
|
46
|
-
|
|
47
|
-
|
|
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.'));
|
|
99
|
+
setAuthCookie(reply, 'gestalt_mobile_session', session, deps.relyingParty.publicOrigin);
|
|
100
|
+
clearAuthCookie(reply, 'gestalt_mobile_registration', deps.relyingParty.publicOrigin);
|
|
48
101
|
return reply.code(201).send(successSchema.parse({ status: 'authenticated' }));
|
|
49
102
|
}
|
|
50
103
|
catch (error) {
|
|
51
104
|
if (error instanceof PasskeyVerificationError || error instanceof AuthorizationDomainError)
|
|
52
|
-
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.'));
|
|
53
109
|
throw error;
|
|
54
110
|
}
|
|
55
111
|
});
|
|
@@ -0,0 +1,41 @@
|
|
|
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 { ExpiringCeremonyAttemptGate } from './application/ceremony-attempts.js';
|
|
7
|
+
import { registerListAuthorizedDevices } from './devices/list/endpoint.js';
|
|
8
|
+
import { registerRenameAuthorizedDevice } from './devices/rename/endpoint.js';
|
|
9
|
+
import { registerRevokeAuthorizedDevice } from './devices/revoke/endpoint.js';
|
|
10
|
+
import { registerCancelEnrollmentTicket } from './enrollment-tickets/cancel/endpoint.js';
|
|
11
|
+
import { registerCreateEnrollmentTicket } from './enrollment-tickets/endpoint.js';
|
|
12
|
+
import { registerEnrollmentTicketStatus } from './enrollment-tickets/status/endpoint.js';
|
|
13
|
+
import { registerLoginOptions } from './login/options/endpoint.js';
|
|
14
|
+
import { registerLoginVerification } from './login/verify/endpoint.js';
|
|
15
|
+
import { registerLogout } from './logout/endpoint.js';
|
|
16
|
+
import { registerRegistrationOptions } from './register/options/endpoint.js';
|
|
17
|
+
import { registerRegistrationVerification } from './register/verify/endpoint.js';
|
|
18
|
+
import { registerAuthStatus, registerDisabledAuthStatus } from './status/endpoint.js';
|
|
19
|
+
/** Registers the complete passkey surface; authorization hooks are composed by app.ts first. */
|
|
20
|
+
export function registerAuthRoutes(app, deps) {
|
|
21
|
+
if (deps.auth) {
|
|
22
|
+
const auth = {
|
|
23
|
+
...deps.auth,
|
|
24
|
+
ceremonyAttempts: deps.auth.ceremonyAttempts ?? new ExpiringCeremonyAttemptGate(),
|
|
25
|
+
};
|
|
26
|
+
registerRegistrationOptions(app, auth);
|
|
27
|
+
registerRegistrationVerification(app, deps.auth);
|
|
28
|
+
registerLoginOptions(app, auth);
|
|
29
|
+
registerLoginVerification(app, deps.auth);
|
|
30
|
+
registerAuthStatus(app, deps.auth);
|
|
31
|
+
registerLogout(app, deps.auth);
|
|
32
|
+
registerListAuthorizedDevices(app, deps.auth);
|
|
33
|
+
registerRenameAuthorizedDevice(app, deps.auth);
|
|
34
|
+
registerRevokeAuthorizedDevice(app, deps.auth);
|
|
35
|
+
registerCreateEnrollmentTicket(app, deps.auth);
|
|
36
|
+
registerEnrollmentTicketStatus(app, deps.auth);
|
|
37
|
+
registerCancelEnrollmentTicket(app, deps.auth);
|
|
38
|
+
}
|
|
39
|
+
else if (deps.passkeyAuthDisabled)
|
|
40
|
+
registerDisabledAuthStatus(app);
|
|
41
|
+
}
|
|
@@ -7,7 +7,7 @@ import { z } from 'zod';
|
|
|
7
7
|
import { resolveRepositoryTarget } from '../application/repository-target.js';
|
|
8
8
|
const requestSchema = z.object({ branch: z.string().min(1) });
|
|
9
9
|
export function registerCheckoutBranch(app, deps) {
|
|
10
|
-
app.post('/api/git/repositories/:workspaceId/checkout', async (request, reply) => {
|
|
10
|
+
app.post('/api/git/repositories/:workspaceId/checkout', { bodyLimit: 8 * 1024 }, async (request, reply) => {
|
|
11
11
|
const target = await resolveRepositoryTarget(deps.workspaces, request.params.workspaceId);
|
|
12
12
|
if (!target.ok)
|
|
13
13
|
return reply.code(target.statusCode).send({ code: target.code });
|
|
@@ -10,7 +10,7 @@ const requestSchema = z.object({
|
|
|
10
10
|
workspaceId: z.string().trim().min(1),
|
|
11
11
|
});
|
|
12
12
|
export function registerCloneRepository(app, deps) {
|
|
13
|
-
app.post('/api/git/clone', async (request, reply) => {
|
|
13
|
+
app.post('/api/git/clone', { bodyLimit: 8 * 1024 }, async (request, reply) => {
|
|
14
14
|
const parsed = requestSchema.safeParse(request.body);
|
|
15
15
|
if (!parsed.success)
|
|
16
16
|
return reply.code(400).send({ code: 'INVALID_CLONE_ADDRESS' });
|
|
@@ -0,0 +1,34 @@
|
|
|
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 { registerCheckoutBranch } from './checkout-branch/endpoint.js';
|
|
7
|
+
import { registerCloneRepository } from './clone-repository/endpoint.js';
|
|
8
|
+
import { registerGetGitSummary } from './get-summary/endpoint.js';
|
|
9
|
+
import { registerPullRebase } from './pull-rebase/endpoint.js';
|
|
10
|
+
import { registerPushUpstream } from './push-upstream/endpoint.js';
|
|
11
|
+
import { registerRefreshGit } from './refresh/endpoint.js';
|
|
12
|
+
export function registerGitRoutes(app, deps) {
|
|
13
|
+
const git = deps.gitSummary;
|
|
14
|
+
if (!git)
|
|
15
|
+
return;
|
|
16
|
+
registerGetGitSummary(app, { workspaces: git.workspaces, inspect: git.inspect });
|
|
17
|
+
registerPushUpstream(app, {
|
|
18
|
+
workspaces: git.workspaces,
|
|
19
|
+
inspect: git.inspectForPush ?? git.inspect,
|
|
20
|
+
push: git.push,
|
|
21
|
+
idempotency: git.idempotency,
|
|
22
|
+
});
|
|
23
|
+
registerRefreshGit(app, {
|
|
24
|
+
workspaces: git.workspaces,
|
|
25
|
+
refresh: git.refresh,
|
|
26
|
+
idempotency: git.idempotency,
|
|
27
|
+
});
|
|
28
|
+
if (git.pull && git.checkout) {
|
|
29
|
+
registerPullRebase(app, { workspaces: git.workspaces, pull: git.pull });
|
|
30
|
+
registerCheckoutBranch(app, { workspaces: git.workspaces, checkout: git.checkout });
|
|
31
|
+
}
|
|
32
|
+
if (git.clone)
|
|
33
|
+
registerCloneRepository(app, { workspaces: git.workspaces, clone: git.clone });
|
|
34
|
+
}
|
|
@@ -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 {
|
|
@@ -0,0 +1,22 @@
|
|
|
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 { registerClosePlan } from './close-plan/endpoint.js';
|
|
7
|
+
import { registerGetPlanMeasurement } from './get-measurement/endpoint.js';
|
|
8
|
+
import { registerGetPlan } from './get-plan/endpoint.js';
|
|
9
|
+
import { registerGetWorkspacePlan } from './get-workspace-plan/endpoint.js';
|
|
10
|
+
import { registerListWorkspacePlans } from './list-workspace-plans/endpoint.js';
|
|
11
|
+
export function registerPlanRoutes(app, deps) {
|
|
12
|
+
if (deps.planRoutes) {
|
|
13
|
+
registerGetPlan(app, deps.planRoutes);
|
|
14
|
+
registerClosePlan(app, deps.planRoutes);
|
|
15
|
+
}
|
|
16
|
+
if (deps.workspacePlanRoutes) {
|
|
17
|
+
registerListWorkspacePlans(app, deps.workspacePlanRoutes);
|
|
18
|
+
registerGetWorkspacePlan(app, deps.workspacePlanRoutes);
|
|
19
|
+
}
|
|
20
|
+
if (deps.planMeasurementRoutes)
|
|
21
|
+
registerGetPlanMeasurement(app, deps.planMeasurementRoutes);
|
|
22
|
+
}
|
|
@@ -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
|
}
|
|
@@ -9,7 +9,7 @@ const requestSchema = z.object({
|
|
|
9
9
|
cwd: z.string().startsWith('/'),
|
|
10
10
|
});
|
|
11
11
|
export function registerPromoteRecentThread(app, deps) {
|
|
12
|
-
app.post('/api/sessions/recent-threads/open', async (request, reply) => {
|
|
12
|
+
app.post('/api/sessions/recent-threads/open', { bodyLimit: 8 * 1024 }, async (request, reply) => {
|
|
13
13
|
const parsed = requestSchema.safeParse(request.body);
|
|
14
14
|
if (!parsed.success)
|
|
15
15
|
return reply.code(400).send({ code: 'INVALID_RECENT_THREAD' });
|
|
@@ -0,0 +1,138 @@
|
|
|
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 { authorizationSessionDevice } from '../../platform/http/authorization-boundary.js';
|
|
7
|
+
import { stopSession } from './lifecycle/use-case.js';
|
|
8
|
+
import { registerForgetSession } from './forget-session/endpoint.js';
|
|
9
|
+
import { registerGetHistory } from './get-history/endpoint.js';
|
|
10
|
+
import { registerGetSession } from './get-session/endpoint.js';
|
|
11
|
+
import { registerInterruptTurn } from './interrupt-turn/endpoint.js';
|
|
12
|
+
import { registerListRecentThreads } from './list-recent-threads/endpoint.js';
|
|
13
|
+
import { registerListSessions } from './list-sessions/endpoint.js';
|
|
14
|
+
import { registerPromoteRecentThread } from './promote-recent-thread/endpoint.js';
|
|
15
|
+
import { registerReleaseSession } from './release-session/endpoint.js';
|
|
16
|
+
import { registerRestoreSession } from './restore-session/endpoint.js';
|
|
17
|
+
import { registerRespondInteraction } from './respond-interaction/endpoint.js';
|
|
18
|
+
import { registerSelectModel } from './select-model/endpoint.js';
|
|
19
|
+
import { registerSessionEvents } from './session-events/endpoint.js';
|
|
20
|
+
import { registerStartSession } from './start-session/endpoint.js';
|
|
21
|
+
import { registerStartTurn } from './start-turn/endpoint.js';
|
|
22
|
+
import { registerStopSession } from './stop-session/endpoint.js';
|
|
23
|
+
export function registerSessionRoutes(app, deps) {
|
|
24
|
+
const sessions = deps.sessionRoutes;
|
|
25
|
+
if (deps.recentThreads) {
|
|
26
|
+
registerListRecentThreads(app, {
|
|
27
|
+
...deps.recentThreads,
|
|
28
|
+
metadata: (threadId) => {
|
|
29
|
+
const session = sessions?.list?.().find((candidate) => candidate.threadId === threadId);
|
|
30
|
+
if (!session)
|
|
31
|
+
return null;
|
|
32
|
+
return {
|
|
33
|
+
...(session.model === undefined ? {} : { model: session.model }),
|
|
34
|
+
...(session.effectiveSkillSelection?.selectedProfileName === undefined
|
|
35
|
+
? {}
|
|
36
|
+
: { skillProfile: session.effectiveSkillSelection.selectedProfileName }),
|
|
37
|
+
...(session.lastOrgPlan === undefined
|
|
38
|
+
? {}
|
|
39
|
+
: { orgPlanFilename: session.lastOrgPlan.filename }),
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (sessions) {
|
|
45
|
+
registerStartSession(app, {
|
|
46
|
+
...sessions,
|
|
47
|
+
reportFailure: (operation, error) => deps.logger.error(`${operation} failed: ${safeErrorLabel(error)}`),
|
|
48
|
+
});
|
|
49
|
+
registerGetSession(app, sessions.find);
|
|
50
|
+
if (sessions.list)
|
|
51
|
+
registerListSessions(app, { list: sessions.list });
|
|
52
|
+
if (sessions.readHistory)
|
|
53
|
+
registerGetHistory(app, {
|
|
54
|
+
find: sessions.find,
|
|
55
|
+
read: sessions.readHistory,
|
|
56
|
+
currentSequence: sessions.currentSequence ?? (() => 0),
|
|
57
|
+
});
|
|
58
|
+
if (sessions.startTurn)
|
|
59
|
+
registerStartTurn(app, {
|
|
60
|
+
find: sessions.find,
|
|
61
|
+
start: sessions.startTurn,
|
|
62
|
+
save: sessions.save,
|
|
63
|
+
onStarted: sessions.onTurnStarted,
|
|
64
|
+
});
|
|
65
|
+
if (sessions.models)
|
|
66
|
+
registerSelectModel(app, {
|
|
67
|
+
find: sessions.find,
|
|
68
|
+
models: () => sessions.models.list(),
|
|
69
|
+
now: sessions.now,
|
|
70
|
+
save: sessions.save,
|
|
71
|
+
});
|
|
72
|
+
if (sessions.interruptTurn)
|
|
73
|
+
registerInterruptTurn(app, { find: sessions.find, interrupt: sessions.interruptTurn });
|
|
74
|
+
if (sessions.restore)
|
|
75
|
+
registerRestoreSession(app, {
|
|
76
|
+
find: sessions.find,
|
|
77
|
+
restore: sessions.restore,
|
|
78
|
+
save: sessions.save,
|
|
79
|
+
idempotency: sessions.idempotency,
|
|
80
|
+
});
|
|
81
|
+
if (deps.recentThreads && sessions.promoteRecent)
|
|
82
|
+
registerPromoteRecentThread(app, {
|
|
83
|
+
list: () => deps.recentThreads.list(),
|
|
84
|
+
promote: sessions.promoteRecent,
|
|
85
|
+
});
|
|
86
|
+
if (sessions.release && sessions.close)
|
|
87
|
+
registerReleaseSession(app, {
|
|
88
|
+
find: sessions.find,
|
|
89
|
+
release: sessions.release,
|
|
90
|
+
save: sessions.save,
|
|
91
|
+
close: sessions.close,
|
|
92
|
+
idempotency: sessions.idempotency,
|
|
93
|
+
});
|
|
94
|
+
if (sessions.remove && sessions.close)
|
|
95
|
+
registerForgetSession(app, {
|
|
96
|
+
find: sessions.find,
|
|
97
|
+
close: sessions.close,
|
|
98
|
+
remove: sessions.remove,
|
|
99
|
+
});
|
|
100
|
+
if (sessions.close)
|
|
101
|
+
registerStopSession(app, {
|
|
102
|
+
find: sessions.find,
|
|
103
|
+
stop: (session) => stopSession(session, sessions.now()),
|
|
104
|
+
save: sessions.save,
|
|
105
|
+
close: sessions.close,
|
|
106
|
+
idempotency: sessions.idempotency,
|
|
107
|
+
});
|
|
108
|
+
if (sessions.replyInteraction && deps.interactions)
|
|
109
|
+
registerRespondInteraction(app, {
|
|
110
|
+
exists: (id) => sessions.find(id) !== null,
|
|
111
|
+
resolve: (sessionId, requestId, resolvedAt) => deps.interactions.resolve(sessionId, requestId, resolvedAt),
|
|
112
|
+
validate: deps.interactions.validate,
|
|
113
|
+
reply: sessions.replyInteraction,
|
|
114
|
+
resolved: sessions.interactionResolved,
|
|
115
|
+
now: sessions.now,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (deps.sessionEvents)
|
|
119
|
+
registerSessionEvents(app, {
|
|
120
|
+
...deps.sessionEvents,
|
|
121
|
+
...(deps.auth
|
|
122
|
+
? {
|
|
123
|
+
publicOrigin: deps.auth.relyingParty.publicOrigin,
|
|
124
|
+
authorized: (cookieHeader) => authorizationSessionDevice(cookieHeader, deps.auth) !== null,
|
|
125
|
+
}
|
|
126
|
+
: {}),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function safeErrorLabel(error) {
|
|
130
|
+
if (typeof error === 'object' && error !== null && 'code' in error) {
|
|
131
|
+
const code = error.code;
|
|
132
|
+
if (typeof code === 'string' && /^[A-Z0-9_]+$/.test(code))
|
|
133
|
+
return code;
|
|
134
|
+
}
|
|
135
|
+
if (error instanceof Error && /^[A-Z0-9_]+$/.test(error.message))
|
|
136
|
+
return error.message;
|
|
137
|
+
return error instanceof Error ? error.name : 'UNKNOWN_ERROR';
|
|
138
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
6
|
export function registerRespondInteraction(app, deps) {
|
|
7
|
-
app.post('/api/sessions/:id/interactions/:requestId', async (request, reply) => {
|
|
7
|
+
app.post('/api/sessions/:id/interactions/:requestId', { bodyLimit: 64 * 1024 }, async (request, reply) => {
|
|
8
8
|
const { id, requestId } = request.params;
|
|
9
9
|
if (!deps.exists(id))
|
|
10
10
|
return reply.code(404).send({ code: 'SESSION_NOT_FOUND' });
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { RelaySession } from '../model/relay-session.js';
|
|
7
7
|
export function registerSelectModel(app, deps) {
|
|
8
|
-
app.post('/api/sessions/:id/model', async (request, reply) => {
|
|
8
|
+
app.post('/api/sessions/:id/model', { bodyLimit: 4 * 1024 }, async (request, reply) => {
|
|
9
9
|
const session = deps.find(request.params.id);
|
|
10
10
|
if (!session)
|
|
11
11
|
return reply.code(404).send({ code: 'SESSION_NOT_FOUND' });
|
|
@@ -9,7 +9,7 @@ import { startSession } from './use-case.js';
|
|
|
9
9
|
import { SkillProfileError } from '../../skills/model/errors.js';
|
|
10
10
|
import { problem } from '../../../platform/http/problem.js';
|
|
11
11
|
export function registerStartSession(app, deps) {
|
|
12
|
-
app.post('/api/sessions', async (request, reply) => {
|
|
12
|
+
app.post('/api/sessions', { bodyLimit: 4 * 1024 }, async (request, reply) => {
|
|
13
13
|
const key = idempotencyKey(request.headers);
|
|
14
14
|
const prior = key ? deps.idempotency?.get('start-session', key) : null;
|
|
15
15
|
if (prior)
|
|
@@ -31,7 +31,10 @@ export function registerStartSession(app, deps) {
|
|
|
31
31
|
catch (error) {
|
|
32
32
|
deps.reportFailure?.('start-session', error);
|
|
33
33
|
if (error instanceof SkillProfileError)
|
|
34
|
-
return reply
|
|
34
|
+
return reply
|
|
35
|
+
.code(400)
|
|
36
|
+
.type('application/problem+json')
|
|
37
|
+
.send(problem(error.code, 400, error.code === 'UNKNOWN_SKILL_PROFILE'
|
|
35
38
|
? 'The selected skill profile does not exist.'
|
|
36
39
|
: 'The selected skill profile is invalid.'));
|
|
37
40
|
throw error;
|
|
@@ -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) {
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
6
|
export function registerStartTurn(app, deps) {
|
|
7
|
-
|
|
7
|
+
// Text is capped at 100,000 characters; leave room for JSON encoding only.
|
|
8
|
+
app.post('/api/sessions/:id/turns', { bodyLimit: 128 * 1024 }, async (request, reply) => {
|
|
8
9
|
const session = deps.find(request.params.id);
|
|
9
10
|
if (!session)
|
|
10
11
|
return reply.code(404).send({ code: 'SESSION_NOT_FOUND' });
|
|
@@ -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
|
}
|