gestalt-mobile 0.9.0 → 0.11.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 +83 -14
- package/dist/client/assets/{index-DrULMr7l.css → index-Bkm5GDCs.css} +1 -1
- package/dist/client/assets/index-DviggEIS.js +19 -0
- package/dist/client/index.html +2 -2
- package/dist/server/server/app.js +52 -3
- package/dist/server/server/cli.js +6 -0
- package/dist/server/server/composition.js +212 -150
- package/dist/server/server/config.js +64 -2
- package/dist/server/server/features/auth/application/ports.js +8 -0
- package/dist/server/server/features/auth/devices/application.js +26 -0
- package/dist/server/server/features/auth/devices/list/endpoint.js +12 -0
- package/dist/server/server/features/auth/devices/rename/endpoint.js +37 -0
- package/dist/server/server/features/auth/devices/revoke/endpoint.js +48 -0
- package/dist/server/server/features/auth/domain/authorization.js +101 -0
- package/dist/server/server/features/auth/domain/device-nickname.js +13 -0
- package/dist/server/server/features/auth/domain/errors.js +7 -0
- package/dist/server/server/features/auth/domain/identifiers.js +45 -0
- package/dist/server/server/features/auth/enrollment-tickets/cancel/endpoint.js +14 -0
- package/dist/server/server/features/auth/enrollment-tickets/endpoint.js +17 -0
- package/dist/server/server/features/auth/enrollment-tickets/status/endpoint.js +13 -0
- package/dist/server/server/features/auth/login/options/endpoint.js +44 -0
- package/dist/server/server/features/auth/login/verify/endpoint.js +88 -0
- package/dist/server/server/features/auth/logout/endpoint.js +16 -0
- package/dist/server/server/features/auth/register/options/endpoint.js +34 -0
- package/dist/server/server/features/auth/register/verify/endpoint.js +56 -0
- package/dist/server/server/features/auth/status/endpoint.js +24 -0
- package/dist/server/server/features/sessions/get-history/endpoint.js +23 -1
- package/dist/server/server/features/sessions/restore-session/endpoint.js +24 -4
- package/dist/server/server/features/sessions/restore-session/use-case.js +20 -0
- package/dist/server/server/features/sessions/session-events/endpoint.js +7 -2
- package/dist/server/server/platform/auth/simple-webauthn-adapter.js +75 -0
- package/dist/server/server/platform/auth/sqlite-authorization-store.js +478 -0
- package/dist/server/server/platform/codex/json-rpc-client.js +36 -2
- package/dist/server/server/platform/codex/session-runtime.js +64 -22
- package/dist/server/server/platform/events/websocket-heartbeat.js +5 -1
- package/dist/server/server/platform/http/authorization-boundary.js +60 -0
- package/dist/server/server/platform/plans/filesystem-plan-status-source.js +30 -11
- package/dist/server/shared/contracts/plan-signal.js +17 -0
- package/package.json +8 -2
- package/dist/client/assets/index-D7-LoAAR.js +0 -12
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
6
|
import fastifyStatic from '@fastify/static';
|
|
7
|
+
import fastifyCookie from '@fastify/cookie';
|
|
7
8
|
import fastify, {} from 'fastify';
|
|
8
9
|
import { registerGetHealth } from './features/health/get-health/endpoint.js';
|
|
9
10
|
import { registerGetBootstrap } from './features/catalog/get-bootstrap/endpoint.js';
|
|
@@ -33,22 +34,60 @@ import { registerGetPlan } from './features/plans/get-plan/endpoint.js';
|
|
|
33
34
|
import { registerClosePlan } from './features/plans/close-plan/endpoint.js';
|
|
34
35
|
import { registerGetPlanMeasurement } from './features/plans/get-measurement/endpoint.js';
|
|
35
36
|
import { registerProblemHandler } from './platform/http/problem-handler.js';
|
|
37
|
+
import { authorizationSessionDevice, registerAuthorizationBoundary, } from './platform/http/authorization-boundary.js';
|
|
36
38
|
import { registerListAvailableSkills, } from './features/skills/list-available/endpoint.js';
|
|
37
39
|
import { registerListSkillProfiles, } from './features/skills/list-profiles/endpoint.js';
|
|
38
40
|
import { registerReplaceSkillProfile, } from './features/skills/replace-profile/endpoint.js';
|
|
39
41
|
import { registerDeleteSkillProfile, } from './features/skills/delete-profile/endpoint.js';
|
|
42
|
+
import { registerRegistrationOptions } from './features/auth/register/options/endpoint.js';
|
|
43
|
+
import { registerRegistrationVerification } from './features/auth/register/verify/endpoint.js';
|
|
44
|
+
import { registerLoginOptions } from './features/auth/login/options/endpoint.js';
|
|
45
|
+
import { registerLoginVerification } from './features/auth/login/verify/endpoint.js';
|
|
46
|
+
import { registerAuthStatus, registerDisabledAuthStatus, } from './features/auth/status/endpoint.js';
|
|
47
|
+
import { registerLogout } from './features/auth/logout/endpoint.js';
|
|
48
|
+
import { registerListAuthorizedDevices } from './features/auth/devices/list/endpoint.js';
|
|
49
|
+
import { registerRenameAuthorizedDevice } from './features/auth/devices/rename/endpoint.js';
|
|
50
|
+
import { registerRevokeAuthorizedDevice } from './features/auth/devices/revoke/endpoint.js';
|
|
51
|
+
import { registerCreateEnrollmentTicket } from './features/auth/enrollment-tickets/endpoint.js';
|
|
52
|
+
import { registerEnrollmentTicketStatus } from './features/auth/enrollment-tickets/status/endpoint.js';
|
|
53
|
+
import { registerCancelEnrollmentTicket } from './features/auth/enrollment-tickets/cancel/endpoint.js';
|
|
40
54
|
export async function buildApp(deps) {
|
|
41
55
|
const app = fastify({ logger: false });
|
|
56
|
+
await app.register(fastifyCookie);
|
|
42
57
|
if (deps.staticDir)
|
|
43
58
|
await app.register(fastifyStatic, { root: deps.staticDir });
|
|
59
|
+
if (deps.auth)
|
|
60
|
+
registerAuthorizationBoundary(app, {
|
|
61
|
+
repository: deps.auth.repository,
|
|
62
|
+
clock: deps.auth.clock,
|
|
63
|
+
publicOrigin: deps.auth.relyingParty.publicOrigin,
|
|
64
|
+
});
|
|
44
65
|
registerGetHealth(app, deps.health);
|
|
66
|
+
if (deps.auth) {
|
|
67
|
+
registerRegistrationOptions(app, deps.auth);
|
|
68
|
+
registerRegistrationVerification(app, deps.auth);
|
|
69
|
+
registerLoginOptions(app, deps.auth);
|
|
70
|
+
registerLoginVerification(app, deps.auth);
|
|
71
|
+
registerAuthStatus(app, deps.auth);
|
|
72
|
+
registerLogout(app, deps.auth);
|
|
73
|
+
registerListAuthorizedDevices(app, deps.auth);
|
|
74
|
+
registerRenameAuthorizedDevice(app, deps.auth);
|
|
75
|
+
registerRevokeAuthorizedDevice(app, deps.auth);
|
|
76
|
+
registerCreateEnrollmentTicket(app, deps.auth);
|
|
77
|
+
registerEnrollmentTicketStatus(app, deps.auth);
|
|
78
|
+
registerCancelEnrollmentTicket(app, deps.auth);
|
|
79
|
+
}
|
|
80
|
+
else if (deps.passkeyAuthDisabled)
|
|
81
|
+
registerDisabledAuthStatus(app);
|
|
45
82
|
if (deps.bootstrap)
|
|
46
83
|
registerGetBootstrap(app, deps.bootstrap);
|
|
47
84
|
if (deps.recentThreads)
|
|
48
85
|
registerListRecentThreads(app, {
|
|
49
86
|
...deps.recentThreads,
|
|
50
87
|
metadata: (threadId) => {
|
|
51
|
-
const session = deps.sessionRoutes
|
|
88
|
+
const session = deps.sessionRoutes
|
|
89
|
+
?.list?.()
|
|
90
|
+
.find((candidate) => candidate.threadId === threadId);
|
|
52
91
|
if (!session)
|
|
53
92
|
return null;
|
|
54
93
|
return {
|
|
@@ -56,7 +95,9 @@ export async function buildApp(deps) {
|
|
|
56
95
|
...(session.effectiveSkillSelection?.selectedProfileName === undefined
|
|
57
96
|
? {}
|
|
58
97
|
: { skillProfile: session.effectiveSkillSelection.selectedProfileName }),
|
|
59
|
-
...(session.lastOrgPlan === undefined
|
|
98
|
+
...(session.lastOrgPlan === undefined
|
|
99
|
+
? {}
|
|
100
|
+
: { orgPlanFilename: session.lastOrgPlan.filename }),
|
|
60
101
|
};
|
|
61
102
|
},
|
|
62
103
|
});
|
|
@@ -138,7 +179,15 @@ export async function buildApp(deps) {
|
|
|
138
179
|
});
|
|
139
180
|
}
|
|
140
181
|
if (deps.sessionEvents)
|
|
141
|
-
registerSessionEvents(app,
|
|
182
|
+
registerSessionEvents(app, {
|
|
183
|
+
...deps.sessionEvents,
|
|
184
|
+
...(deps.auth
|
|
185
|
+
? {
|
|
186
|
+
publicOrigin: deps.auth.relyingParty.publicOrigin,
|
|
187
|
+
authorized: (cookieHeader) => authorizationSessionDevice(cookieHeader, deps.auth) !== null,
|
|
188
|
+
}
|
|
189
|
+
: {}),
|
|
190
|
+
});
|
|
142
191
|
if (deps.planRoutes) {
|
|
143
192
|
registerGetPlan(app, deps.planRoutes);
|
|
144
193
|
registerClosePlan(app, deps.planRoutes);
|
|
@@ -21,6 +21,8 @@ Options:
|
|
|
21
21
|
--cwd <path> Workspace root (default: current directory)
|
|
22
22
|
--host <address> Listen address (default: 127.0.0.1)
|
|
23
23
|
--port <number> Listen port (default: 3000)
|
|
24
|
+
--public-origin <origin> Exact HTTPS browser origin for passkeys (localhost may use HTTP)
|
|
25
|
+
--disable-passkey-auth Disable passkey access control (unsafe on shared or network hosts)
|
|
24
26
|
--data-dir <path> Directory for persistent application data
|
|
25
27
|
--skills <profile> Use a global skill profile for every session
|
|
26
28
|
--skills list List saved global skill profiles without starting the server
|
|
@@ -135,6 +137,8 @@ export async function runCli(dependencies = {}) {
|
|
|
135
137
|
if (invocation.command === 'list')
|
|
136
138
|
return listSkillProfiles(skillProfiles, stdout, stderr);
|
|
137
139
|
const config = invocation.config;
|
|
140
|
+
if (!config.passkeyAuthEnabled)
|
|
141
|
+
stderr.write('WARNING: Passkey access control is disabled; anyone who can reach this server has full access.\n');
|
|
138
142
|
let explicitSkillProfile;
|
|
139
143
|
try {
|
|
140
144
|
explicitSkillProfile = config.skillsProfile
|
|
@@ -152,6 +156,8 @@ export async function runCli(dependencies = {}) {
|
|
|
152
156
|
const app = await (dependencies.compose ?? composeRelayApp)({
|
|
153
157
|
root: config.root,
|
|
154
158
|
dataDir: config.dataDir,
|
|
159
|
+
relyingParty: config.relyingParty,
|
|
160
|
+
passkeyAuthEnabled: config.passkeyAuthEnabled,
|
|
155
161
|
staticDir: packagedClientDir(moduleUrl),
|
|
156
162
|
profiles: new CodexProfileCatalog(),
|
|
157
163
|
installedCodexVersion: await (dependencies.probeCodexVersion ?? probeCodexVersion)(),
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
|
-
import { randomUUID } from 'node:crypto';
|
|
6
|
+
import { randomBytes, randomUUID } from 'node:crypto';
|
|
7
7
|
import { existsSync } from 'node:fs';
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
9
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
@@ -17,6 +17,9 @@ import { CodexSessionRuntime } from './platform/codex/session-runtime.js';
|
|
|
17
17
|
import { normalizeCodexNotification } from './platform/codex/normalizer.js';
|
|
18
18
|
import { migrate } from './platform/persistence/migrate.js';
|
|
19
19
|
import { openRelayDatabase } from './platform/persistence/sqlite.js';
|
|
20
|
+
import { SqliteAuthorizationStore } from './platform/auth/sqlite-authorization-store.js';
|
|
21
|
+
import { SimpleWebAuthnAdapter } from './platform/auth/simple-webauthn-adapter.js';
|
|
22
|
+
import { authorizationSessionId, authorizedDeviceId } from './features/auth/domain/identifiers.js';
|
|
20
23
|
import { SqliteSessionRepository } from './platform/persistence/sqlite-session-repository.js';
|
|
21
24
|
import { SqliteEventJournal } from './platform/persistence/sqlite-event-journal.js';
|
|
22
25
|
import { SqlitePendingInteractionStore } from './platform/persistence/sqlite-pending-interaction-store.js';
|
|
@@ -40,14 +43,30 @@ import { SupervisedPlanRegistry } from './features/plans/application/supervised-
|
|
|
40
43
|
import { FilesystemPlanStatusSource } from './platform/plans/filesystem-plan-status-source.js';
|
|
41
44
|
import { checkpointPlanMeasurement } from './platform/plans/plan-measurement-command.js';
|
|
42
45
|
import { PlanMeasurementRefresh } from './platform/plans/plan-measurement-refresh.js';
|
|
46
|
+
import { createRelyingPartyConfig, } from './config.js';
|
|
43
47
|
const generatedProtocolVersion = 'codex-cli 0.144.3';
|
|
44
48
|
export async function composeRelayApp(options) {
|
|
49
|
+
const passkeyAuthEnabled = options.passkeyAuthEnabled ?? true;
|
|
50
|
+
const relyingParty = createRelyingPartyConfig(options.relyingParty.publicOrigin);
|
|
51
|
+
if (relyingParty.rpId !== options.relyingParty.rpId ||
|
|
52
|
+
relyingParty.rpName !== options.relyingParty.rpName)
|
|
53
|
+
throw new Error('Invalid WebAuthn relying-party configuration');
|
|
54
|
+
const authorizationRandom = options.authorizationRandomBytes ?? randomBytes;
|
|
55
|
+
const ownerHandle = passkeyAuthEnabled ? authorizationRandom(32) : undefined;
|
|
56
|
+
if (ownerHandle && ownerHandle.length !== 32)
|
|
57
|
+
throw new Error('Authorization randomness must return exactly 32 bytes');
|
|
45
58
|
const root = resolve(options.root);
|
|
46
59
|
const databasePath = options.dataDir
|
|
47
60
|
? join(resolve(options.dataDir), 'relay.sqlite')
|
|
48
61
|
: resolveStateDatabasePath(root, process.env.XDG_STATE_HOME ?? join(homedir(), '.local', 'state'));
|
|
49
62
|
const database = openRelayDatabase(databasePath);
|
|
50
|
-
|
|
63
|
+
try {
|
|
64
|
+
migrate(database);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
database.close();
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
51
70
|
const sessions = new SqliteSessionRepository(database);
|
|
52
71
|
const journal = new SqliteEventJournal(database);
|
|
53
72
|
const interactions = new SqlitePendingInteractionStore(database);
|
|
@@ -158,164 +177,206 @@ export async function composeRelayApp(options) {
|
|
|
158
177
|
});
|
|
159
178
|
recoverExitedSession = (sessionId) => supervisor.recover(sessionId);
|
|
160
179
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
180
|
+
let authorization;
|
|
181
|
+
if (passkeyAuthEnabled) {
|
|
182
|
+
try {
|
|
183
|
+
authorization = new SqliteAuthorizationStore(options.homeDirectory ?? homedir(), relyingParty);
|
|
184
|
+
authorization.initializeOwner(ownerHandle);
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
database.close();
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
let app;
|
|
192
|
+
try {
|
|
193
|
+
app = await buildApp({
|
|
194
|
+
...(authorization ? { auth: {
|
|
195
|
+
repository: authorization,
|
|
196
|
+
clock: { now: options.authorizationClock ?? (() => new Date()) },
|
|
197
|
+
random: {
|
|
198
|
+
bytes: (length) => {
|
|
199
|
+
const value = authorizationRandom(length);
|
|
200
|
+
if (length !== 32 || value.length !== 32)
|
|
201
|
+
throw new Error('Authorization randomness must return exactly 32 bytes');
|
|
202
|
+
return value;
|
|
203
|
+
},
|
|
171
204
|
},
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
protocolCompatible: protocol.compatible,
|
|
197
|
-
},
|
|
198
|
-
sessionRoutes: {
|
|
199
|
-
createId: randomUUID,
|
|
200
|
-
now: () => new Date().toISOString(),
|
|
201
|
-
save: saveSession,
|
|
202
|
-
find: (id) => withPendingInteractions(sessions.find(id)),
|
|
203
|
-
list: () => sessions.list().map((session) => withPendingInteractions(session)),
|
|
204
|
-
workspaces,
|
|
205
|
-
profiles: options.profiles,
|
|
206
|
-
skillProfiles,
|
|
207
|
-
skillCatalog,
|
|
208
|
-
defaultSkillProfile: options.explicitSkillProfile,
|
|
209
|
-
activate: runtime
|
|
210
|
-
? async (session, settings) => runtime.start(session, new Date().toISOString(), settings)
|
|
211
|
-
: undefined,
|
|
212
|
-
startTurn: runtime
|
|
213
|
-
? async (session, text) => runtime.startTurn(session, text, new Date().toISOString())
|
|
214
|
-
: undefined,
|
|
215
|
-
onTurnStarted: (session) => planMeasurementRefresh?.refreshNow(session.id),
|
|
216
|
-
models,
|
|
217
|
-
readHistory: runtime ? (session) => runtime.readHistory(session) : undefined,
|
|
218
|
-
currentSequence: (sessionId) => journal.since(sessionId, 0).at(-1)?.sequence ?? 0,
|
|
219
|
-
interruptTurn: runtime
|
|
220
|
-
? (session, turnId) => runtime.interruptTurn(session, turnId)
|
|
221
|
-
: undefined,
|
|
222
|
-
restore: runtime
|
|
223
|
-
? (session) => runtime.restore(session, new Date().toISOString())
|
|
224
|
-
: undefined,
|
|
225
|
-
promoteRecent: runtime
|
|
226
|
-
? (thread) => promoteRecentThread(thread, {
|
|
227
|
-
createId: randomUUID,
|
|
228
|
-
now: () => new Date().toISOString(),
|
|
229
|
-
list: () => sessions.list(),
|
|
230
|
-
save: saveSession,
|
|
231
|
-
restore: (session) => runtime.restore(session, new Date().toISOString()),
|
|
232
|
-
})
|
|
233
|
-
: undefined,
|
|
234
|
-
release: (session) => RelaySession.rehydrate(session).release(new Date().toISOString()).snapshot,
|
|
235
|
-
remove: (id) => sessions.remove(id),
|
|
236
|
-
idempotency,
|
|
237
|
-
close: runtime
|
|
238
|
-
? (id) => {
|
|
239
|
-
planMeasurementRefresh?.stop(id);
|
|
240
|
-
return runtime.release(id);
|
|
241
|
-
}
|
|
242
|
-
: undefined,
|
|
243
|
-
replyInteraction: runtime
|
|
244
|
-
? (sessionId, requestId, value) => runtime.resolveServerRequest(sessionId, requestId, value)
|
|
245
|
-
: undefined,
|
|
246
|
-
interactionResolved: (sessionId, requestId, occurredAt) => {
|
|
247
|
-
events.publish(journal.append(sessionId, 'interaction.resolved', { requestId }, occurredAt));
|
|
205
|
+
identifiers: {
|
|
206
|
+
sessionId: () => {
|
|
207
|
+
const value = authorizationRandom(32);
|
|
208
|
+
if (value.length !== 32)
|
|
209
|
+
throw new Error('Authorization randomness must return exactly 32 bytes');
|
|
210
|
+
return authorizationSessionId(Buffer.from(value).toString('base64url'));
|
|
211
|
+
},
|
|
212
|
+
deviceId: () => authorizedDeviceId(randomUUID()),
|
|
213
|
+
},
|
|
214
|
+
webauthn: options.authorizationWebauthn ?? new SimpleWebAuthnAdapter(),
|
|
215
|
+
relyingParty,
|
|
216
|
+
} } : { passkeyAuthDisabled: true }),
|
|
217
|
+
health: {
|
|
218
|
+
async read() {
|
|
219
|
+
return {
|
|
220
|
+
status: protocol.compatible ? 'ok' : 'degraded',
|
|
221
|
+
version: '0.1.0',
|
|
222
|
+
codex: {
|
|
223
|
+
installedVersion: options.installedCodexVersion,
|
|
224
|
+
protocolVersion: generatedProtocolVersion,
|
|
225
|
+
compatible: protocol.compatible,
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
},
|
|
248
229
|
},
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
clear: (id) => supervisedPlans.clear(id),
|
|
260
|
-
closed: (id) => {
|
|
261
|
-
planMeasurementRefresh?.stop(id);
|
|
262
|
-
const occurredAt = new Date().toISOString();
|
|
263
|
-
events.publish(journal.append(id, 'plan.closed', {}, occurredAt));
|
|
230
|
+
skills: {
|
|
231
|
+
workspaces,
|
|
232
|
+
profiles: options.profiles,
|
|
233
|
+
catalog: editorSkillCatalog,
|
|
234
|
+
selections: skillProfiles,
|
|
235
|
+
listGlobalProfileNames: () => skillProfiles.listGlobalProfileNames(),
|
|
236
|
+
readGlobalProfile: (name) => skillProfiles.readGlobalProfile(name),
|
|
237
|
+
replaceGlobalProfile: (profile) => skillProfiles.replaceGlobalProfile(profile),
|
|
238
|
+
deleteGlobalProfile: (name) => skillProfiles.deleteGlobalProfile(name),
|
|
239
|
+
profilePath: (name) => skillProfiles.globalProfilePath(name),
|
|
264
240
|
},
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
throw new Error('CODEX_SESSION_NOT_RUNNING');
|
|
275
|
-
return runtime.readPlanMeasurement(session);
|
|
276
|
-
},
|
|
241
|
+
logger: console,
|
|
242
|
+
staticDir: options.staticDir,
|
|
243
|
+
recentThreads,
|
|
244
|
+
bootstrap: {
|
|
245
|
+
workspaces,
|
|
246
|
+
profiles: options.profiles,
|
|
247
|
+
models,
|
|
248
|
+
sessions: {
|
|
249
|
+
list: () => sessions.list().map((session) => withPendingInteractions(session)),
|
|
277
250
|
},
|
|
278
|
-
|
|
279
|
-
: {}),
|
|
280
|
-
interactions: {
|
|
281
|
-
resolve: (sessionId, requestId, resolvedAt) => interactions.resolve(sessionId, requestId, resolvedAt),
|
|
282
|
-
validate: (sessionId, requestId, value) => {
|
|
283
|
-
const interaction = interactions.find(sessionId, requestId);
|
|
284
|
-
if (!interaction)
|
|
285
|
-
return false;
|
|
286
|
-
if (interaction.kind === 'quiz')
|
|
287
|
-
return isValidQuizInteractionResponse(interaction.payload, value);
|
|
288
|
-
return isValidInteractionResponse(interaction.kind, value);
|
|
251
|
+
protocolCompatible: protocol.compatible,
|
|
289
252
|
},
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
253
|
+
sessionRoutes: {
|
|
254
|
+
createId: randomUUID,
|
|
255
|
+
now: () => new Date().toISOString(),
|
|
256
|
+
save: saveSession,
|
|
257
|
+
find: (id) => withPendingInteractions(sessions.find(id)),
|
|
258
|
+
list: () => sessions.list().map((session) => withPendingInteractions(session)),
|
|
259
|
+
workspaces,
|
|
260
|
+
profiles: options.profiles,
|
|
261
|
+
skillProfiles,
|
|
262
|
+
skillCatalog,
|
|
263
|
+
defaultSkillProfile: options.explicitSkillProfile,
|
|
264
|
+
activate: runtime
|
|
265
|
+
? async (session, settings) => runtime.start(session, new Date().toISOString(), settings)
|
|
266
|
+
: undefined,
|
|
267
|
+
startTurn: runtime
|
|
268
|
+
? async (session, text) => runtime.startTurn(session, text, new Date().toISOString())
|
|
269
|
+
: undefined,
|
|
270
|
+
onTurnStarted: (session) => planMeasurementRefresh?.refreshNow(session.id),
|
|
271
|
+
models,
|
|
272
|
+
readHistory: runtime ? (session) => runtime.readHistory(session) : undefined,
|
|
273
|
+
currentSequence: (sessionId) => journal.since(sessionId, 0).at(-1)?.sequence ?? 0,
|
|
274
|
+
interruptTurn: runtime
|
|
275
|
+
? (session, turnId) => runtime.interruptTurn(session, turnId)
|
|
276
|
+
: undefined,
|
|
277
|
+
restore: runtime
|
|
278
|
+
? (session) => runtime.restoreWithOutcome(session, new Date().toISOString())
|
|
279
|
+
: undefined,
|
|
280
|
+
promoteRecent: runtime
|
|
281
|
+
? (thread) => promoteRecentThread(thread, {
|
|
282
|
+
createId: randomUUID,
|
|
283
|
+
now: () => new Date().toISOString(),
|
|
284
|
+
list: () => sessions.list(),
|
|
285
|
+
save: saveSession,
|
|
286
|
+
restore: (session) => runtime.restore(session, new Date().toISOString()),
|
|
287
|
+
})
|
|
288
|
+
: undefined,
|
|
289
|
+
release: (session) => RelaySession.rehydrate(session).release(new Date().toISOString()).snapshot,
|
|
290
|
+
remove: (id) => sessions.remove(id),
|
|
291
|
+
idempotency,
|
|
292
|
+
close: runtime
|
|
293
|
+
? (id) => {
|
|
294
|
+
planMeasurementRefresh?.stop(id);
|
|
295
|
+
return runtime.release(id);
|
|
296
|
+
}
|
|
297
|
+
: undefined,
|
|
298
|
+
replyInteraction: runtime
|
|
299
|
+
? (sessionId, requestId, value) => runtime.resolveServerRequest(sessionId, requestId, value)
|
|
300
|
+
: undefined,
|
|
301
|
+
interactionResolved: (sessionId, requestId, occurredAt) => {
|
|
302
|
+
events.publish(journal.append(sessionId, 'interaction.resolved', { requestId }, occurredAt));
|
|
303
|
+
},
|
|
297
304
|
},
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
305
|
+
sessionEvents: {
|
|
306
|
+
exists: (id) => sessions.find(id) !== null,
|
|
307
|
+
since: (id, after) => journal.since(id, after),
|
|
308
|
+
subscribe: (id, listener) => events.subscribe(id, listener),
|
|
302
309
|
},
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
310
|
+
planRoutes: {
|
|
311
|
+
exists: (id) => sessions.find(id) !== null,
|
|
312
|
+
find: (id) => supervisedPlans.find(id),
|
|
313
|
+
removeStatus: (id) => planStatusSource.remove(id, supervisedPlans.identity(id) ?? undefined),
|
|
314
|
+
clear: (id) => supervisedPlans.clear(id),
|
|
315
|
+
closed: (id) => {
|
|
316
|
+
planMeasurementRefresh?.stop(id);
|
|
317
|
+
const occurredAt = new Date().toISOString();
|
|
318
|
+
events.publish(journal.append(id, 'plan.closed', {}, occurredAt));
|
|
319
|
+
},
|
|
306
320
|
},
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
321
|
+
...(runtime
|
|
322
|
+
? {
|
|
323
|
+
planMeasurementRoutes: {
|
|
324
|
+
exists: (id) => sessions.find(id) !== null,
|
|
325
|
+
authorize: (id, authorization) => runtime.authorizePlanMeasurement(id, authorization),
|
|
326
|
+
read: async (id) => {
|
|
327
|
+
const session = sessions.find(id);
|
|
328
|
+
if (!session)
|
|
329
|
+
throw new Error('CODEX_SESSION_NOT_RUNNING');
|
|
330
|
+
return runtime.readPlanMeasurement(session);
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
}
|
|
334
|
+
: {}),
|
|
335
|
+
interactions: {
|
|
336
|
+
resolve: (sessionId, requestId, resolvedAt) => interactions.resolve(sessionId, requestId, resolvedAt),
|
|
337
|
+
validate: (sessionId, requestId, value) => {
|
|
338
|
+
const interaction = interactions.find(sessionId, requestId);
|
|
339
|
+
if (!interaction)
|
|
340
|
+
return false;
|
|
341
|
+
if (interaction.kind === 'quiz')
|
|
342
|
+
return isValidQuizInteractionResponse(interaction.payload, value);
|
|
343
|
+
return isValidInteractionResponse(interaction.kind, value);
|
|
344
|
+
},
|
|
310
345
|
},
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
346
|
+
gitSummary: {
|
|
347
|
+
workspaces,
|
|
348
|
+
inspect: async (path) => {
|
|
349
|
+
const summary = await gitSummaries.inspect(path);
|
|
350
|
+
const fetchedAt = gitFetches.lastSuccessfulAt(path);
|
|
351
|
+
return { ...summary, fetchedAt: fetchedAt ? new Date(fetchedAt).toISOString() : null };
|
|
352
|
+
},
|
|
353
|
+
inspectForPush: inspectGit,
|
|
354
|
+
push: async (path, upstream) => {
|
|
355
|
+
await pushUpstream(path, upstream);
|
|
356
|
+
gitSummaries.invalidate(path);
|
|
357
|
+
},
|
|
358
|
+
refresh: async (path) => {
|
|
359
|
+
await gitFetches.refresh(path);
|
|
360
|
+
gitSummaries.invalidate(path);
|
|
361
|
+
},
|
|
362
|
+
pull: async (path) => {
|
|
363
|
+
await pullRebase(path);
|
|
364
|
+
gitSummaries.invalidate(path);
|
|
365
|
+
},
|
|
366
|
+
checkout: async (path, branch) => {
|
|
367
|
+
await checkoutBranch(path, branch);
|
|
368
|
+
gitSummaries.invalidate(path);
|
|
369
|
+
},
|
|
370
|
+
clone: cloneRepository,
|
|
371
|
+
idempotency,
|
|
314
372
|
},
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
authorization?.close();
|
|
377
|
+
database.close();
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
319
380
|
const restoreActiveSessions = async () => {
|
|
320
381
|
if (!runtime)
|
|
321
382
|
return;
|
|
@@ -341,6 +402,7 @@ export async function composeRelayApp(options) {
|
|
|
341
402
|
runtime?.stopAll();
|
|
342
403
|
planStatusSource.closeAll();
|
|
343
404
|
database.close();
|
|
405
|
+
authorization?.close();
|
|
344
406
|
});
|
|
345
407
|
return app;
|
|
346
408
|
}
|
|
@@ -4,16 +4,32 @@
|
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
6
|
import { resolve } from 'node:path';
|
|
7
|
+
import { isIP } from 'node:net';
|
|
7
8
|
export class CliUsageError extends Error {
|
|
8
9
|
exitCode = 2;
|
|
9
10
|
}
|
|
10
|
-
const optionNames = new Set([
|
|
11
|
+
const optionNames = new Set([
|
|
12
|
+
'--cwd',
|
|
13
|
+
'--host',
|
|
14
|
+
'--port',
|
|
15
|
+
'--data-dir',
|
|
16
|
+
'--public-origin',
|
|
17
|
+
'--skills',
|
|
18
|
+
]);
|
|
19
|
+
const flagNames = new Set(['--disable-passkey-auth']);
|
|
11
20
|
export function parseConfig(args, cwd = process.cwd()) {
|
|
12
21
|
const values = new Map();
|
|
22
|
+
const flags = new Set();
|
|
13
23
|
for (let index = 0; index < args.length; index += 1) {
|
|
14
24
|
const option = args[index];
|
|
15
25
|
if (!option.startsWith('--'))
|
|
16
26
|
throw new CliUsageError(`Unexpected argument: ${option}`);
|
|
27
|
+
if (flagNames.has(option)) {
|
|
28
|
+
if (flags.has(option))
|
|
29
|
+
throw new CliUsageError(`Duplicate option: ${option}`);
|
|
30
|
+
flags.add(option);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
17
33
|
if (!optionNames.has(option))
|
|
18
34
|
throw new CliUsageError(`Unknown option: ${option}`);
|
|
19
35
|
if (values.has(option))
|
|
@@ -28,11 +44,57 @@ export function parseConfig(args, cwd = process.cwd()) {
|
|
|
28
44
|
const port = Number(portValue);
|
|
29
45
|
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
30
46
|
throw new CliUsageError(`Invalid --port: ${portValue}`);
|
|
47
|
+
const host = values.get('--host') ?? '127.0.0.1';
|
|
48
|
+
const passkeyAuthEnabled = !flags.has('--disable-passkey-auth');
|
|
49
|
+
const publicOrigin = values.has('--public-origin')
|
|
50
|
+
? normalizePublicOrigin(values.get('--public-origin'))
|
|
51
|
+
: defaultPublicOrigin(host, port, passkeyAuthEnabled);
|
|
31
52
|
return {
|
|
32
|
-
host
|
|
53
|
+
host,
|
|
33
54
|
port,
|
|
55
|
+
passkeyAuthEnabled,
|
|
56
|
+
relyingParty: createRelyingPartyConfig(publicOrigin),
|
|
34
57
|
root: resolve(cwd, values.get('--cwd') ?? '.'),
|
|
35
58
|
dataDir: values.get('--data-dir'),
|
|
36
59
|
skillsProfile: values.get('--skills'),
|
|
37
60
|
};
|
|
38
61
|
}
|
|
62
|
+
export function normalizePublicOrigin(value) {
|
|
63
|
+
if (!/^[a-z][a-z\d+.-]*:\/\/[^/?#\s\\\\]+\/?$/i.test(value))
|
|
64
|
+
throw new CliUsageError('--public-origin must be a bare origin without a path, query, or fragment');
|
|
65
|
+
let url;
|
|
66
|
+
try {
|
|
67
|
+
url = new URL(value);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
throw new CliUsageError(`Invalid --public-origin: ${value}`);
|
|
71
|
+
}
|
|
72
|
+
if (url.username || url.password)
|
|
73
|
+
throw new CliUsageError('--public-origin must not include credentials');
|
|
74
|
+
if (url.pathname !== '/' || url.search || url.hash)
|
|
75
|
+
throw new CliUsageError('--public-origin must be a bare origin without a path, query, or fragment');
|
|
76
|
+
const hostname = url.hostname.replace(/^\[|\]$/g, '');
|
|
77
|
+
if (isIP(hostname))
|
|
78
|
+
throw new CliUsageError('--public-origin must use a hostname, not an IP address');
|
|
79
|
+
if (url.protocol === 'http:' && url.hostname === 'localhost')
|
|
80
|
+
return url.origin;
|
|
81
|
+
if (url.protocol !== 'https:')
|
|
82
|
+
throw new CliUsageError('--public-origin must use HTTPS, except for http://localhost');
|
|
83
|
+
return url.origin;
|
|
84
|
+
}
|
|
85
|
+
export function createRelyingPartyConfig(publicOrigin) {
|
|
86
|
+
const normalizedOrigin = normalizePublicOrigin(publicOrigin);
|
|
87
|
+
return {
|
|
88
|
+
publicOrigin: normalizedOrigin,
|
|
89
|
+
rpId: new URL(normalizedOrigin).hostname,
|
|
90
|
+
rpName: 'Gestalt Mobile',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function defaultPublicOrigin(host, port, passkeyAuthEnabled) {
|
|
94
|
+
if (passkeyAuthEnabled && !isLoopbackHost(host))
|
|
95
|
+
throw new CliUsageError('--public-origin is required when --host is not a loopback address');
|
|
96
|
+
return `http://localhost:${port}`;
|
|
97
|
+
}
|
|
98
|
+
function isLoopbackHost(host) {
|
|
99
|
+
return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]';
|
|
100
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
+
/** Expected proof failure; adapters must translate library-specific failures to this type. */
|
|
7
|
+
export class PasskeyVerificationError extends Error {
|
|
8
|
+
}
|