gestalt-mobile 0.20.1 → 0.22.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/dist/client/assets/index-Db9p-xm2.css +1 -0
- package/dist/client/assets/index-NHzJXg1w.js +29 -0
- package/dist/client/index.html +2 -2
- package/dist/server/server/app.js +2 -0
- package/dist/server/server/composition.contracts.js +142 -6
- package/dist/server/server/composition.js +39 -8
- package/dist/server/server/features/agent-activity/model.js +5 -0
- package/dist/server/server/features/agent-activity/registry.js +12 -1
- package/dist/server/server/features/autopilot/application/service.js +53 -1
- package/dist/server/server/features/files/application/ports.js +6 -0
- package/dist/server/server/features/files/copy-entry/endpoint.js +48 -0
- package/dist/server/server/features/files/delete-entry/endpoint.js +23 -0
- package/dist/server/server/features/files/domain/relative-directory.js +15 -0
- package/dist/server/server/features/files/domain/workspace-directory.js +6 -0
- package/dist/server/server/features/files/list-directory/endpoint.js +49 -0
- package/dist/server/server/features/files/move-entry/endpoint.js +4 -0
- package/dist/server/server/features/files/register-routes.js +19 -0
- package/dist/server/server/features/files/upload-file/endpoint.js +35 -0
- package/dist/server/server/features/sessions/list-sessions/endpoint.js +10 -6
- package/dist/server/server/features/sessions/model/relay-session.js +19 -0
- package/dist/server/server/features/sessions/promote-recent-thread/use-case.js +13 -3
- package/dist/server/server/features/sessions/register-routes.js +1 -0
- package/dist/server/server/features/sessions/start-session/use-case.js +3 -5
- package/dist/server/server/platform/auth/sqlite-authorization-store.js +98 -205
- package/dist/server/server/platform/auth/sqlite.js +54 -0
- package/dist/server/server/platform/codex/activity-facts.js +4 -0
- package/dist/server/server/platform/codex/recent-thread-lister.js +37 -11
- package/dist/server/server/platform/codex/rollout-execution-policy.js +68 -0
- package/dist/server/server/platform/codex/session-runtime.js +76 -17
- package/dist/server/server/platform/filesystem/filesystem-workspace-files.js +559 -0
- package/dist/server/server/platform/persistence/migrate.js +5 -1
- package/dist/server/server/platform/persistence/sqlite-session-repository.js +11 -3
- package/package.json +1 -1
- package/dist/client/assets/index-CnXbn39K.js +0 -29
- package/dist/client/assets/index-DYERmevO.css +0 -1
|
@@ -0,0 +1,49 @@
|
|
|
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 { parseRelativeDirectory } from '../domain/relative-directory.js';
|
|
7
|
+
const defaultLimit = 250;
|
|
8
|
+
const maximumLimit = 500;
|
|
9
|
+
export function registerListDirectory(app, deps) {
|
|
10
|
+
app.get('/api/workspaces/:workspaceId/files', async (request, reply) => {
|
|
11
|
+
const query = request.query;
|
|
12
|
+
const directoryRaw = query.directory === undefined ? '' : query.directory;
|
|
13
|
+
const directory = typeof directoryRaw === 'string' ? parseRelativeDirectory(directoryRaw) : null;
|
|
14
|
+
const cursor = query.cursor === undefined ? undefined : query.cursor;
|
|
15
|
+
const limit = parseLimit(query.limit);
|
|
16
|
+
if (directory === null ||
|
|
17
|
+
(cursor !== undefined && (typeof cursor !== 'string' || cursor === '')) ||
|
|
18
|
+
!limit)
|
|
19
|
+
return reply.code(400).send({ code: 'INVALID_DIRECTORY_REQUEST' });
|
|
20
|
+
try {
|
|
21
|
+
const workspace = await deps.workspaces.resolve(request.params.workspaceId);
|
|
22
|
+
const result = await deps.files.list(workspace.realPath, { directory, cursor, limit });
|
|
23
|
+
if (result.kind === 'available')
|
|
24
|
+
return reply.send(result.page);
|
|
25
|
+
const mapped = {
|
|
26
|
+
missing: [404, 'DIRECTORY_NOT_FOUND'],
|
|
27
|
+
'not-directory': [409, 'DIRECTORY_NOT_DIRECTORY'],
|
|
28
|
+
unreadable: [403, 'DIRECTORY_UNREADABLE'],
|
|
29
|
+
'invalid-cursor': [400, 'INVALID_DIRECTORY_CURSOR'],
|
|
30
|
+
'stale-cursor': [409, 'STALE_DIRECTORY_CURSOR'],
|
|
31
|
+
};
|
|
32
|
+
const [status, code] = mapped[result.kind];
|
|
33
|
+
return reply.code(status).send({ code });
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error instanceof Error && error.message === 'WORKSPACE_NOT_FOUND')
|
|
37
|
+
return reply.code(404).send({ code: 'WORKSPACE_NOT_FOUND' });
|
|
38
|
+
return reply.code(503).send({ code: 'WORKSPACE_FILES_UNAVAILABLE' });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function parseLimit(value) {
|
|
43
|
+
if (value === undefined)
|
|
44
|
+
return defaultLimit;
|
|
45
|
+
if (typeof value !== 'string' || !/^(?:[1-9]\d*)$/.test(value))
|
|
46
|
+
return null;
|
|
47
|
+
const parsed = Number(value);
|
|
48
|
+
return parsed <= maximumLimit ? parsed : null;
|
|
49
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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 { registerListDirectory } from './list-directory/endpoint.js';
|
|
7
|
+
import { registerCopyEntry } from './copy-entry/endpoint.js';
|
|
8
|
+
import { registerMoveEntry } from './move-entry/endpoint.js';
|
|
9
|
+
import { registerUploadFile } from './upload-file/endpoint.js';
|
|
10
|
+
import { registerDeleteEntry } from './delete-entry/endpoint.js';
|
|
11
|
+
export function registerFileRoutes(app, deps) {
|
|
12
|
+
if (!deps.workspaceFileRoutes)
|
|
13
|
+
return;
|
|
14
|
+
registerListDirectory(app, deps.workspaceFileRoutes);
|
|
15
|
+
registerCopyEntry(app, deps.workspaceFileRoutes);
|
|
16
|
+
registerMoveEntry(app, deps.workspaceFileRoutes);
|
|
17
|
+
registerUploadFile(app, deps.workspaceFileRoutes);
|
|
18
|
+
registerDeleteEntry(app, deps.workspaceFileRoutes);
|
|
19
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { map } from '../copy-entry/endpoint.js';
|
|
2
|
+
export function registerUploadFile(app, deps) {
|
|
3
|
+
app.addContentTypeParser('application/octet-stream', { parseAs: 'buffer' }, (_request, body, done) => done(null, body));
|
|
4
|
+
app.put('/api/workspaces/:workspaceId/files/upload', { bodyLimit: 25 * 1024 * 1024 }, async (request, reply) => {
|
|
5
|
+
const q = request.query;
|
|
6
|
+
const conflict = q.conflict;
|
|
7
|
+
if (typeof q.directory !== 'string' ||
|
|
8
|
+
typeof q.filename !== 'string' ||
|
|
9
|
+
!['reject', 'replace', 'keep-both'].includes(String(conflict)))
|
|
10
|
+
return reply.code(400).send({ code: 'INVALID_UPLOAD_REQUEST' });
|
|
11
|
+
if (!deps.files.upload)
|
|
12
|
+
return reply.code(503).send({ code: 'WORKSPACE_FILES_UNAVAILABLE' });
|
|
13
|
+
try {
|
|
14
|
+
const workspace = await deps.workspaces.resolve(request.params.workspaceId);
|
|
15
|
+
if (!Buffer.isBuffer(request.body))
|
|
16
|
+
return reply.code(415).send({ code: 'INVALID_UPLOAD_CONTENT' });
|
|
17
|
+
const content = request.body;
|
|
18
|
+
return map(reply, await deps.files.upload(workspace.realPath, {
|
|
19
|
+
directory: q.directory,
|
|
20
|
+
filename: q.filename,
|
|
21
|
+
conflict: conflict,
|
|
22
|
+
content,
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return reply
|
|
27
|
+
.code(error instanceof Error && error.message === 'WORKSPACE_NOT_FOUND' ? 404 : 503)
|
|
28
|
+
.send({
|
|
29
|
+
code: error instanceof Error && error.message === 'WORKSPACE_NOT_FOUND'
|
|
30
|
+
? 'WORKSPACE_NOT_FOUND'
|
|
31
|
+
: 'WORKSPACE_FILES_UNAVAILABLE',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -5,10 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { buildResumeCommand } from '../application/resume-command.js';
|
|
7
7
|
export function registerListSessions(app, deps) {
|
|
8
|
-
app.get('/api/sessions', async () => deps.list().map((session) =>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
app.get('/api/sessions', async () => deps.list().map((session) => {
|
|
9
|
+
const plan = deps.plan?.(session.id) ?? null;
|
|
10
|
+
return {
|
|
11
|
+
...session,
|
|
12
|
+
...(deps.activity ? { agentActivity: deps.activity(session.id) } : {}),
|
|
13
|
+
...(deps.autopilot ? { autopilot: deps.autopilot(session.id) } : {}),
|
|
14
|
+
...(plan ? { plan } : {}),
|
|
15
|
+
resumeCommand: session.threadId ? buildResumeCommand(session) : null,
|
|
16
|
+
};
|
|
17
|
+
}));
|
|
14
18
|
}
|
|
@@ -14,6 +14,21 @@ export function isSessionReadable(session) {
|
|
|
14
14
|
export function relayOwnsWriter(session) {
|
|
15
15
|
return session.state === 'ready' || session.state === 'turnActive';
|
|
16
16
|
}
|
|
17
|
+
const sandboxes = new Set(['read-only', 'workspace-write', 'danger-full-access']);
|
|
18
|
+
const approvalPolicies = new Set(['untrusted', 'on-request', 'never']);
|
|
19
|
+
export function createSessionExecutionPolicy(input) {
|
|
20
|
+
if (input.sandbox !== undefined &&
|
|
21
|
+
(typeof input.sandbox !== 'string' || !sandboxes.has(input.sandbox)))
|
|
22
|
+
throw new DomainError('SESSION_EXECUTION_POLICY_INVALID');
|
|
23
|
+
const approvalPolicy = input.approvalPolicy ?? 'on-request';
|
|
24
|
+
if (typeof approvalPolicy !== 'string' ||
|
|
25
|
+
!approvalPolicies.has(approvalPolicy))
|
|
26
|
+
throw new DomainError('SESSION_EXECUTION_POLICY_INVALID');
|
|
27
|
+
return {
|
|
28
|
+
...(input.sandbox === undefined ? {} : { sandbox: input.sandbox }),
|
|
29
|
+
approvalPolicy: approvalPolicy,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
17
32
|
export function createEffectiveSkillSelection(input) {
|
|
18
33
|
return {
|
|
19
34
|
...(input.selectedProfileName === undefined
|
|
@@ -38,6 +53,7 @@ export class RelaySession {
|
|
|
38
53
|
...(input.model === undefined ? {} : { model: input.model }),
|
|
39
54
|
...(input.branch === undefined ? {} : { branch: input.branch }),
|
|
40
55
|
effectiveSkillSelection: createEffectiveSkillSelection(input.effectiveSkillSelection),
|
|
56
|
+
executionPolicy: createSessionExecutionPolicy(input),
|
|
41
57
|
threadId: null,
|
|
42
58
|
state: 'starting',
|
|
43
59
|
desiredState: 'active',
|
|
@@ -55,6 +71,9 @@ export class RelaySession {
|
|
|
55
71
|
workspaceId: workspaceId(input.workspaceId),
|
|
56
72
|
workspacePath: workspacePath(input.workspacePath),
|
|
57
73
|
profile: profileName(input.profile),
|
|
74
|
+
...(input.executionPolicy === undefined
|
|
75
|
+
? {}
|
|
76
|
+
: { executionPolicy: createSessionExecutionPolicy(input.executionPolicy) }),
|
|
58
77
|
threadId: threadId(input.threadId),
|
|
59
78
|
state: 'stopped',
|
|
60
79
|
desiredState: 'stopped',
|
|
@@ -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 { RelaySession } from '../model/relay-session.js';
|
|
6
|
+
import { RelaySession, } from '../model/relay-session.js';
|
|
7
7
|
export class RecentThreadHistoryUnavailable extends Error {
|
|
8
8
|
constructor() {
|
|
9
9
|
super('RECENT_THREAD_HISTORY_UNAVAILABLE');
|
|
@@ -28,15 +28,24 @@ export async function promoteRecentThread(thread, deps) {
|
|
|
28
28
|
async function promote(thread, deps) {
|
|
29
29
|
const existing = deps.list().find((session) => session.threadId === thread.id);
|
|
30
30
|
if (existing) {
|
|
31
|
+
const executionPolicy = existing.executionPolicy
|
|
32
|
+
? undefined
|
|
33
|
+
: await deps.executionPolicy?.(thread);
|
|
34
|
+
const recovered = executionPolicy
|
|
35
|
+
? { ...existing, executionPolicy, updatedAt: deps.now() }
|
|
36
|
+
: existing;
|
|
31
37
|
try {
|
|
32
|
-
await deps.read(
|
|
38
|
+
await deps.read(recovered);
|
|
33
39
|
}
|
|
34
40
|
catch {
|
|
35
41
|
throw new RecentThreadHistoryUnavailable();
|
|
36
42
|
}
|
|
37
|
-
|
|
43
|
+
if (recovered !== existing)
|
|
44
|
+
deps.save(recovered);
|
|
45
|
+
return recovered;
|
|
38
46
|
}
|
|
39
47
|
const now = deps.now();
|
|
48
|
+
const executionPolicy = await deps.executionPolicy?.(thread);
|
|
40
49
|
const imported = existing ??
|
|
41
50
|
RelaySession.fromExistingThread({
|
|
42
51
|
id: deps.createId(),
|
|
@@ -44,6 +53,7 @@ async function promote(thread, deps) {
|
|
|
44
53
|
workspacePath: thread.cwd,
|
|
45
54
|
profile: thread.profile,
|
|
46
55
|
threadId: thread.id,
|
|
56
|
+
...(executionPolicy === undefined ? {} : { executionPolicy }),
|
|
47
57
|
now,
|
|
48
58
|
}).snapshot;
|
|
49
59
|
// Validate first: an unavailable thread must never leave an imported stub.
|
|
@@ -44,17 +44,15 @@ export async function startSession(input, deps) {
|
|
|
44
44
|
profile: input.profile,
|
|
45
45
|
model,
|
|
46
46
|
...(branch ? { branch } : {}),
|
|
47
|
+
sandbox: input.sandbox,
|
|
48
|
+
approvalPolicy: input.approvalPolicy,
|
|
47
49
|
effectiveSkillSelection,
|
|
48
50
|
now: deps.now(),
|
|
49
51
|
}).snapshot;
|
|
50
52
|
deps.save(session);
|
|
51
53
|
if (!deps.activate)
|
|
52
54
|
return session;
|
|
53
|
-
const active = await deps.activate(session, {
|
|
54
|
-
model,
|
|
55
|
-
sandbox: input.sandbox,
|
|
56
|
-
approvalPolicy: input.approvalPolicy,
|
|
57
|
-
});
|
|
55
|
+
const active = await deps.activate(session, {});
|
|
58
56
|
deps.save(active);
|
|
59
57
|
return active;
|
|
60
58
|
}
|