@roj-ai/standalone-server 0.1.36 → 0.2.2
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/main.js +1 -4
- package/dist/main.js.map +1 -1
- package/dist/platform-api.d.ts +26 -5
- package/dist/platform-api.d.ts.map +1 -1
- package/dist/platform-api.js +331 -55
- package/dist/platform-api.js.map +1 -1
- package/dist/preview-proxy.d.ts +28 -3
- package/dist/preview-proxy.d.ts.map +1 -1
- package/dist/preview-proxy.js +69 -16
- package/dist/preview-proxy.js.map +1 -1
- package/dist/server.d.ts +5 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +83 -37
- package/dist/server.js.map +1 -1
- package/dist/user-config-loader.d.ts.map +1 -1
- package/dist/user-config-loader.js +2 -1
- package/dist/user-config-loader.js.map +1 -1
- package/package.json +9 -7
- package/src/main.ts +1 -4
- package/src/platform-api.ts +445 -111
- package/src/preview-proxy.ts +86 -18
- package/src/server.ts +82 -39
- package/src/user-config-loader.ts +2 -1
package/dist/main.js
CHANGED
|
@@ -26,10 +26,7 @@ async function main() {
|
|
|
26
26
|
console.error(error instanceof Error ? error.message : String(error));
|
|
27
27
|
process.exit(1);
|
|
28
28
|
}
|
|
29
|
-
await startStandaloneServer(
|
|
30
|
-
presets: userConfig.presets,
|
|
31
|
-
localResources: userConfig.localResources,
|
|
32
|
-
});
|
|
29
|
+
await startStandaloneServer(userConfig);
|
|
33
30
|
}
|
|
34
31
|
main().catch((error) => {
|
|
35
32
|
console.error('Fatal error:', error);
|
package/dist/main.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AACA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAExD,KAAK,UAAU,IAAI;IAClB,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,eAAe,CAAA;IACrD,MAAM,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,UAAU,CAAC,CAAA;IAE7D,IAAI,UAAsD,CAAA;IAC1D,IAAI,CAAC;QACJ,UAAU,GAAG,MAAM,cAAc,CAAC,kBAAkB,CAAC,CAAA;QACrD,OAAO,CAAC,GAAG,CAAC,uBAAuB,kBAAkB,EAAE,CAAC,CAAA;QACxD,OAAO,CAAC,GAAG,CAAC,cAAc,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACzE,IAAI,UAAU,CAAC,cAAc,EAAE,MAAM,EAAE,CAAC;YACvC,OAAO,CAAC,GAAG,CAAC,sBAAsB,UAAU,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAC3F,CAAC;IACF,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,OAAO,CAAC,KAAK,CAAC,8BAA8B,kBAAkB,GAAG,CAAC,CAAA;QAClE,OAAO,CAAC,KAAK,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;QACrE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAChB,CAAC;IAED,MAAM,qBAAqB,CAAC
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AACA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAExD,KAAK,UAAU,IAAI;IAClB,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,eAAe,CAAA;IACrD,MAAM,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,UAAU,CAAC,CAAA;IAE7D,IAAI,UAAsD,CAAA;IAC1D,IAAI,CAAC;QACJ,UAAU,GAAG,MAAM,cAAc,CAAC,kBAAkB,CAAC,CAAA;QACrD,OAAO,CAAC,GAAG,CAAC,uBAAuB,kBAAkB,EAAE,CAAC,CAAA;QACxD,OAAO,CAAC,GAAG,CAAC,cAAc,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACzE,IAAI,UAAU,CAAC,cAAc,EAAE,MAAM,EAAE,CAAC;YACvC,OAAO,CAAC,GAAG,CAAC,sBAAsB,UAAU,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAC3F,CAAC;IACF,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,OAAO,CAAC,KAAK,CAAC,8BAA8B,kBAAkB,GAAG,CAAC,CAAA;QAClE,OAAO,CAAC,KAAK,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;QACrE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAChB,CAAC;IAED,MAAM,qBAAqB,CAAC,UAAU,CAAC,CAAA;AACxC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACtB,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,KAAK,CAAC,CAAA;IACpC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AAChB,CAAC,CAAC,CAAA"}
|
package/dist/platform-api.d.ts
CHANGED
|
@@ -13,23 +13,44 @@
|
|
|
13
13
|
* - sessions.publish — publishing requires the CF platform
|
|
14
14
|
* - instances.archive — no-op; shutdown the server instead
|
|
15
15
|
*/
|
|
16
|
-
import type { Logger, Preset,
|
|
16
|
+
import type { Logger, Preset, Result, Session } from '@roj-ai/sdk';
|
|
17
|
+
import { SessionId } from '@roj-ai/sdk';
|
|
17
18
|
import { Hono } from 'hono';
|
|
18
|
-
import type { GitInstanceFs } from './git-instance-fs.js';
|
|
19
19
|
import type { InstanceState } from './instance.js';
|
|
20
20
|
import type { LocalRegistry } from './local-registry.js';
|
|
21
|
+
interface RpcError {
|
|
22
|
+
type: string;
|
|
23
|
+
message: string;
|
|
24
|
+
httpStatus: number;
|
|
25
|
+
}
|
|
26
|
+
interface PlatformSessionManager {
|
|
27
|
+
callManagerMethod(method: string, input: unknown): Promise<Result<unknown, RpcError>>;
|
|
28
|
+
callPluginMethod(sessionId: SessionId, method: string, input: unknown): Promise<Result<unknown, RpcError>>;
|
|
29
|
+
withSessionLease(sessionId: SessionId, reason: string, operation: (session: Pick<Session, 'close'>) => Promise<Result<void, RpcError>>): Promise<Result<void, RpcError>>;
|
|
30
|
+
getStats(): Promise<{
|
|
31
|
+
sessions: Array<{
|
|
32
|
+
id: SessionId;
|
|
33
|
+
presetId: string;
|
|
34
|
+
status: string;
|
|
35
|
+
}>;
|
|
36
|
+
}>;
|
|
37
|
+
}
|
|
38
|
+
interface SessionGitFs {
|
|
39
|
+
addSessionWorktree(instanceId: string, sessionId: string): Promise<string>;
|
|
40
|
+
removeSessionWorktree(instanceId: string, sessionId: string): Promise<void>;
|
|
41
|
+
}
|
|
21
42
|
interface Deps {
|
|
22
43
|
instance: InstanceState;
|
|
23
|
-
sessionManager:
|
|
44
|
+
sessionManager: PlatformSessionManager;
|
|
24
45
|
logger: Logger;
|
|
25
46
|
presets: Preset[];
|
|
26
47
|
registry: LocalRegistry;
|
|
27
48
|
/** Per-instance bare repo + per-session worktree manager. */
|
|
28
|
-
gitFs:
|
|
49
|
+
gitFs: SessionGitFs;
|
|
29
50
|
/** HMAC secret for signing download tokens (`sessionFiles.createDownloadUrl`). */
|
|
30
51
|
tokenSecret: string;
|
|
31
52
|
/** Externally-reachable base URL (e.g. `http://localhost:8765`) used when minting download URLs. */
|
|
32
|
-
|
|
53
|
+
getPublicBaseUrl(): string;
|
|
33
54
|
}
|
|
34
55
|
export declare function createPlatformApi(deps: Deps): Hono;
|
|
35
56
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform-api.d.ts","sourceRoot":"","sources":["../src/platform-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;
|
|
1
|
+
{"version":3,"file":"platform-api.d.ts","sourceRoot":"","sources":["../src/platform-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAClE,OAAO,EAAE,SAAS,EAAyB,MAAM,aAAa,CAAA;AAG9D,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAC3B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAClD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAGxD,UAAU,QAAQ;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;CAClB;AAED,UAAU,sBAAsB;IAC/B,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAA;IACrF,gBAAgB,CAAC,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAA;IAC1G,gBAAgB,CACf,SAAS,EAAE,SAAS,EACpB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,GAC7E,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAA;IAClC,QAAQ,IAAI,OAAO,CAAC;QACnB,QAAQ,EAAE,KAAK,CAAC;YAAE,EAAE,EAAE,SAAS,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KACpE,CAAC,CAAA;CACF;AAED,UAAU,YAAY;IACrB,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAC1E,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3E;AAED,UAAU,IAAI;IACb,QAAQ,EAAE,aAAa,CAAA;IACvB,cAAc,EAAE,sBAAsB,CAAA;IACtC,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,QAAQ,EAAE,aAAa,CAAA;IACvB,6DAA6D;IAC7D,KAAK,EAAE,YAAY,CAAA;IACnB,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAA;IACnB,oGAAoG;IACpG,gBAAgB,IAAI,MAAM,CAAA;CAC1B;AAmLD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CA6BlD"}
|
package/dist/platform-api.js
CHANGED
|
@@ -13,46 +13,261 @@
|
|
|
13
13
|
* - sessions.publish — publishing requires the CF platform
|
|
14
14
|
* - instances.archive — no-op; shutdown the server instead
|
|
15
15
|
*/
|
|
16
|
-
import { SessionId } from '@roj-ai/sdk';
|
|
16
|
+
import { SessionId, sessionMetadataSchema } from '@roj-ai/sdk';
|
|
17
|
+
import z from 'zod/v4';
|
|
17
18
|
import { randomUUID } from 'node:crypto';
|
|
18
19
|
import { Hono } from 'hono';
|
|
19
20
|
import { signFileToken } from './signed-token.js';
|
|
21
|
+
const RpcCallSchema = z.strictObject({
|
|
22
|
+
method: z.string(),
|
|
23
|
+
input: z.unknown().optional(),
|
|
24
|
+
});
|
|
25
|
+
const RpcEnvelopeSchema = z.union([
|
|
26
|
+
RpcCallSchema,
|
|
27
|
+
z.strictObject({ batch: z.array(RpcCallSchema) }),
|
|
28
|
+
]);
|
|
29
|
+
const AutoCreateSessionSchema = z.strictObject({
|
|
30
|
+
presetId: z.string().min(1),
|
|
31
|
+
blocking: z.boolean().optional(),
|
|
32
|
+
initialPrompt: z.string().optional(),
|
|
33
|
+
resourceIds: z.array(z.string()).optional(),
|
|
34
|
+
fileIds: z.array(z.string()).optional(),
|
|
35
|
+
});
|
|
36
|
+
const inputSchemas = {
|
|
37
|
+
'instances.create': z.strictObject({
|
|
38
|
+
templateSlug: z.string(),
|
|
39
|
+
bundleSlug: z.string().optional(),
|
|
40
|
+
bundleRevisionId: z.string().optional(),
|
|
41
|
+
name: z.string(),
|
|
42
|
+
vcsType: z.enum(['github', 'gitLocal', 'none']).optional(),
|
|
43
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
44
|
+
autoCreateSession: AutoCreateSessionSchema.optional(),
|
|
45
|
+
}),
|
|
46
|
+
'instances.get': z.strictObject({ instanceId: z.string() }),
|
|
47
|
+
'instances.list': z.strictObject({
|
|
48
|
+
limit: z.number().optional(),
|
|
49
|
+
offset: z.number().optional(),
|
|
50
|
+
}),
|
|
51
|
+
'instances.status': z.strictObject({ instanceId: z.string() }),
|
|
52
|
+
'instances.archive': z.strictObject({ instanceId: z.string() }),
|
|
53
|
+
'sessions.create': z.strictObject({
|
|
54
|
+
instanceId: z.string(),
|
|
55
|
+
presetId: z.string().min(1),
|
|
56
|
+
blocking: z.boolean().optional(),
|
|
57
|
+
origin: z.string().optional(),
|
|
58
|
+
expiresIn: z.number().optional(),
|
|
59
|
+
initialPrompt: z.string().optional(),
|
|
60
|
+
}),
|
|
61
|
+
'sessions.list': z.strictObject({ instanceId: z.string() }),
|
|
62
|
+
'tokens.create': z.strictObject({
|
|
63
|
+
instanceId: z.string(),
|
|
64
|
+
origin: z.string().optional(),
|
|
65
|
+
expiresIn: z.number().optional(),
|
|
66
|
+
meta: z.record(z.string(), z.unknown()).optional(),
|
|
67
|
+
}),
|
|
68
|
+
'resources.create': z.strictObject({
|
|
69
|
+
slug: z.string(),
|
|
70
|
+
name: z.string().optional(),
|
|
71
|
+
description: z.string().optional(),
|
|
72
|
+
fileId: z.string(),
|
|
73
|
+
label: z.string().optional(),
|
|
74
|
+
}),
|
|
75
|
+
'resources.addRevision': z.strictObject({
|
|
76
|
+
resourceId: z.string().optional(),
|
|
77
|
+
resourceSlug: z.string().optional(),
|
|
78
|
+
fileId: z.string(),
|
|
79
|
+
label: z.string().optional(),
|
|
80
|
+
}),
|
|
81
|
+
'resources.get': z.strictObject({
|
|
82
|
+
resourceId: z.string().optional(),
|
|
83
|
+
resourceSlug: z.string().optional(),
|
|
84
|
+
}),
|
|
85
|
+
'resources.list': z.strictObject({
|
|
86
|
+
limit: z.number().optional(),
|
|
87
|
+
offset: z.number().optional(),
|
|
88
|
+
}),
|
|
89
|
+
'resources.delete': z.strictObject({ resourceId: z.string() }),
|
|
90
|
+
'sessionFiles.createDownloadUrl': z.strictObject({
|
|
91
|
+
instanceId: z.string(),
|
|
92
|
+
sessionId: z.string(),
|
|
93
|
+
scope: z.enum(['workspace', 'session']),
|
|
94
|
+
path: z.string(),
|
|
95
|
+
ttlSeconds: z.number().optional(),
|
|
96
|
+
}),
|
|
97
|
+
};
|
|
98
|
+
const InstanceSummaryOutputSchema = z.strictObject({
|
|
99
|
+
instanceId: z.string(),
|
|
100
|
+
name: z.string(),
|
|
101
|
+
status: z.string(),
|
|
102
|
+
templateSlug: z.string(),
|
|
103
|
+
bundleSlug: z.string(),
|
|
104
|
+
bundleRevisionId: z.string(),
|
|
105
|
+
vcsType: z.string(),
|
|
106
|
+
metadata: z.record(z.string(), z.unknown()).nullable(),
|
|
107
|
+
createdAt: z.string(),
|
|
108
|
+
});
|
|
109
|
+
const SessionSummaryOutputSchema = z.strictObject({
|
|
110
|
+
id: z.string(),
|
|
111
|
+
presetId: z.string().nullable(),
|
|
112
|
+
status: z.string(),
|
|
113
|
+
createdAt: z.string(),
|
|
114
|
+
});
|
|
115
|
+
const ResourceOutputSchema = z.strictObject({
|
|
116
|
+
id: z.string(),
|
|
117
|
+
slug: z.string(),
|
|
118
|
+
name: z.string().nullable(),
|
|
119
|
+
description: z.string().nullable(),
|
|
120
|
+
latestRevision: z.strictObject({
|
|
121
|
+
id: z.string(),
|
|
122
|
+
label: z.string().nullable(),
|
|
123
|
+
file: z.strictObject({
|
|
124
|
+
id: z.string(),
|
|
125
|
+
filename: z.string(),
|
|
126
|
+
mimeType: z.string(),
|
|
127
|
+
size: z.number(),
|
|
128
|
+
}),
|
|
129
|
+
createdAt: z.string(),
|
|
130
|
+
}).nullable(),
|
|
131
|
+
createdAt: z.string(),
|
|
132
|
+
});
|
|
133
|
+
const outputSchemas = {
|
|
134
|
+
'instances.create': z.strictObject({
|
|
135
|
+
instanceId: z.string(),
|
|
136
|
+
status: z.enum(['created', 'initializing', 'ready']),
|
|
137
|
+
sessionId: z.string().optional(),
|
|
138
|
+
wsToken: z.string().optional(),
|
|
139
|
+
}),
|
|
140
|
+
'instances.get': InstanceSummaryOutputSchema,
|
|
141
|
+
'instances.list': z.strictObject({
|
|
142
|
+
instances: z.array(InstanceSummaryOutputSchema),
|
|
143
|
+
total: z.number(),
|
|
144
|
+
}),
|
|
145
|
+
'instances.status': z.strictObject({
|
|
146
|
+
instanceId: z.string(),
|
|
147
|
+
status: z.string(),
|
|
148
|
+
sandbox: z.strictObject({
|
|
149
|
+
state: z.enum(['stopped', 'starting', 'running', 'pausing', 'paused', 'failed']),
|
|
150
|
+
e2bId: z.string().optional(),
|
|
151
|
+
lastActivityAt: z.string().optional(),
|
|
152
|
+
versions: z.strictObject({
|
|
153
|
+
sdk: z.string(),
|
|
154
|
+
runtime: z.strictObject({ name: z.string(), version: z.string() }).nullable(),
|
|
155
|
+
}).optional(),
|
|
156
|
+
}).nullable(),
|
|
157
|
+
sessions: z.array(SessionSummaryOutputSchema),
|
|
158
|
+
lifecycleEvents: z.array(z.strictObject({
|
|
159
|
+
event: z.string(),
|
|
160
|
+
detail: z.string().optional(),
|
|
161
|
+
createdAt: z.string(),
|
|
162
|
+
})),
|
|
163
|
+
serviceUrls: z.array(z.strictObject({
|
|
164
|
+
code: z.string(),
|
|
165
|
+
sessionId: z.string().nullable(),
|
|
166
|
+
serviceType: z.string().nullable(),
|
|
167
|
+
port: z.number(),
|
|
168
|
+
})),
|
|
169
|
+
}),
|
|
170
|
+
'instances.archive': z.strictObject({ ok: z.boolean() }),
|
|
171
|
+
'sessions.create': z.strictObject({
|
|
172
|
+
sessionId: z.string(),
|
|
173
|
+
status: z.enum(['creating', 'active']),
|
|
174
|
+
wsToken: z.string().optional(),
|
|
175
|
+
}),
|
|
176
|
+
'sessions.list': z.strictObject({ sessions: z.array(SessionSummaryOutputSchema) }),
|
|
177
|
+
'tokens.create': z.strictObject({ token: z.string(), expiresAt: z.string() }),
|
|
178
|
+
'resources.create': z.strictObject({ resourceId: z.string(), revisionId: z.string() }),
|
|
179
|
+
'resources.addRevision': z.strictObject({
|
|
180
|
+
revisionId: z.string(),
|
|
181
|
+
noop: z.boolean().optional(),
|
|
182
|
+
}),
|
|
183
|
+
'resources.get': ResourceOutputSchema,
|
|
184
|
+
'resources.list': z.strictObject({ resources: z.array(ResourceOutputSchema) }),
|
|
185
|
+
'resources.delete': z.strictObject({ ok: z.boolean() }),
|
|
186
|
+
'sessionFiles.createDownloadUrl': z.strictObject({ url: z.string(), expiresAt: z.string() }),
|
|
187
|
+
};
|
|
20
188
|
export function createPlatformApi(deps) {
|
|
21
189
|
const app = new Hono();
|
|
22
190
|
app.post('/rpc', async (c) => {
|
|
23
|
-
|
|
24
|
-
|
|
191
|
+
let rawBody;
|
|
192
|
+
try {
|
|
193
|
+
rawBody = await c.req.json();
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return c.json({ ok: false, error: { type: 'invalid_request', message: 'Invalid JSON body' } }, 400);
|
|
197
|
+
}
|
|
198
|
+
const parsedBody = RpcEnvelopeSchema.safeParse(rawBody);
|
|
199
|
+
if (!parsedBody.success) {
|
|
200
|
+
return c.json({ ok: false, error: { type: 'invalid_request', message: parsedBody.error.message } }, 400);
|
|
201
|
+
}
|
|
202
|
+
const body = parsedBody.data;
|
|
203
|
+
if ('batch' in body) {
|
|
25
204
|
const results = [];
|
|
26
205
|
for (const call of body.batch) {
|
|
27
206
|
results.push(await dispatch(deps, call.method, call.input));
|
|
28
207
|
}
|
|
29
208
|
return c.json({ results });
|
|
30
209
|
}
|
|
31
|
-
if (typeof body.method !== 'string') {
|
|
32
|
-
return c.json({ ok: false, error: { type: 'invalid_request', message: 'Missing method' } }, 400);
|
|
33
|
-
}
|
|
34
210
|
const result = await dispatch(deps, body.method, body.input);
|
|
35
211
|
return c.json(result);
|
|
36
212
|
});
|
|
37
213
|
return app;
|
|
38
214
|
}
|
|
39
215
|
async function dispatch(deps, method, input) {
|
|
40
|
-
const handler = handlers[method];
|
|
41
|
-
if (!handler) {
|
|
42
|
-
return { ok: false, error: { type: 'method_not_found', message: `Method not supported in standalone: ${method}` } };
|
|
43
|
-
}
|
|
44
216
|
try {
|
|
45
|
-
const value = await
|
|
217
|
+
const value = await callHandler(deps, method, input ?? {});
|
|
46
218
|
return { ok: true, value };
|
|
47
219
|
}
|
|
48
220
|
catch (err) {
|
|
221
|
+
if (err instanceof UnsupportedMethodError) {
|
|
222
|
+
return { ok: false, error: { type: 'method_not_found', message: err.message } };
|
|
223
|
+
}
|
|
49
224
|
const message = err instanceof Error ? err.message : String(err);
|
|
50
225
|
deps.logger.error(`Platform RPC handler failed: ${method}`, err instanceof Error ? err : new Error(message));
|
|
51
226
|
return { ok: false, error: { type: 'handler_error', message } };
|
|
52
227
|
}
|
|
53
228
|
}
|
|
54
|
-
|
|
55
|
-
|
|
229
|
+
async function callHandler(deps, method, input) {
|
|
230
|
+
switch (method) {
|
|
231
|
+
case 'instances.create':
|
|
232
|
+
return outputSchemas['instances.create'].parse(await handlers['instances.create'](deps, inputSchemas['instances.create'].parse(input)));
|
|
233
|
+
case 'instances.get':
|
|
234
|
+
return outputSchemas['instances.get'].parse(await handlers['instances.get'](deps, inputSchemas['instances.get'].parse(input)));
|
|
235
|
+
case 'instances.list':
|
|
236
|
+
return outputSchemas['instances.list'].parse(await handlers['instances.list'](deps, inputSchemas['instances.list'].parse(input)));
|
|
237
|
+
case 'instances.status':
|
|
238
|
+
return outputSchemas['instances.status'].parse(await handlers['instances.status'](deps, inputSchemas['instances.status'].parse(input)));
|
|
239
|
+
case 'instances.archive':
|
|
240
|
+
return outputSchemas['instances.archive'].parse(await handlers['instances.archive'](deps, inputSchemas['instances.archive'].parse(input)));
|
|
241
|
+
case 'sessions.create':
|
|
242
|
+
return outputSchemas['sessions.create'].parse(await handlers['sessions.create'](deps, inputSchemas['sessions.create'].parse(input)));
|
|
243
|
+
case 'sessions.list':
|
|
244
|
+
return outputSchemas['sessions.list'].parse(await handlers['sessions.list'](deps, inputSchemas['sessions.list'].parse(input)));
|
|
245
|
+
case 'tokens.create':
|
|
246
|
+
return outputSchemas['tokens.create'].parse(await handlers['tokens.create'](deps, inputSchemas['tokens.create'].parse(input)));
|
|
247
|
+
case 'resources.create':
|
|
248
|
+
return outputSchemas['resources.create'].parse(await handlers['resources.create'](deps, inputSchemas['resources.create'].parse(input)));
|
|
249
|
+
case 'resources.addRevision':
|
|
250
|
+
return outputSchemas['resources.addRevision'].parse(await handlers['resources.addRevision'](deps, inputSchemas['resources.addRevision'].parse(input)));
|
|
251
|
+
case 'resources.get':
|
|
252
|
+
return outputSchemas['resources.get'].parse(await handlers['resources.get'](deps, inputSchemas['resources.get'].parse(input)));
|
|
253
|
+
case 'resources.list':
|
|
254
|
+
return outputSchemas['resources.list'].parse(await handlers['resources.list'](deps, inputSchemas['resources.list'].parse(input)));
|
|
255
|
+
case 'resources.delete':
|
|
256
|
+
return outputSchemas['resources.delete'].parse(await handlers['resources.delete'](deps, inputSchemas['resources.delete'].parse(input)));
|
|
257
|
+
case 'sessionFiles.createDownloadUrl':
|
|
258
|
+
return outputSchemas['sessionFiles.createDownloadUrl'].parse(await handlers['sessionFiles.createDownloadUrl'](deps, inputSchemas['sessionFiles.createDownloadUrl'].parse(input)));
|
|
259
|
+
default:
|
|
260
|
+
throw new UnsupportedMethodError(method);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
class UnsupportedMethodError extends Error {
|
|
264
|
+
method;
|
|
265
|
+
constructor(method) {
|
|
266
|
+
super(`Method not supported in standalone: ${method}`);
|
|
267
|
+
this.method = method;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// Creates a session via the SDK session manager, injects selected local files,
|
|
56
271
|
// then (if set) pushes `initialPrompt` as a user-chat message — order matters:
|
|
57
272
|
// resources must land in the workspace before the agent's first inference so
|
|
58
273
|
// the agent sees a non-empty workspace. Mirrors roj-platform's project-init
|
|
@@ -70,14 +285,18 @@ async function startSession(deps, input) {
|
|
|
70
285
|
workspaceDir,
|
|
71
286
|
});
|
|
72
287
|
if (!created.ok) {
|
|
73
|
-
|
|
74
|
-
// orphaned dir. Best-effort — failure here is logged inside removeSessionWorktree.
|
|
75
|
-
await deps.gitFs.removeSessionWorktree(deps.instance.id, sessionId);
|
|
288
|
+
await removeWorktreeAfterFailure(deps, sessionId);
|
|
76
289
|
throw new Error(created.error.message);
|
|
77
290
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
291
|
+
try {
|
|
292
|
+
const selections = resolveSessionFiles(deps, input.presetId, input.resourceIds, input.fileIds);
|
|
293
|
+
for (const selection of selections) {
|
|
294
|
+
await injectRegistryFile(deps, sessionId, selection);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
await rollbackCreatedSession(deps, sessionId);
|
|
299
|
+
throw error;
|
|
81
300
|
}
|
|
82
301
|
if (input.initialPrompt) {
|
|
83
302
|
const sent = await deps.sessionManager.callPluginMethod(SessionId(sessionId), 'user-chat.sendMessage', {
|
|
@@ -90,23 +309,32 @@ async function startSession(deps, input) {
|
|
|
90
309
|
});
|
|
91
310
|
}
|
|
92
311
|
}
|
|
93
|
-
|
|
312
|
+
// Creation is synchronous here — there is no provisioning step to wait on,
|
|
313
|
+
// so the session is active by the time this returns. The contract's other
|
|
314
|
+
// value, 'creating', belongs to the CF platform.
|
|
315
|
+
return { sessionId, status: 'active' };
|
|
94
316
|
}
|
|
95
317
|
// Resolution mirrors roj-platform's project-init.ts:206:
|
|
96
|
-
// 1. explicit
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
function
|
|
100
|
-
|
|
101
|
-
|
|
318
|
+
// 1. explicit resourceIds, followed by explicit fileIds.
|
|
319
|
+
// 2. preset.defaultResourceSlugs only when no explicit IDs were supplied.
|
|
320
|
+
// Repeated selections of the same registry file are injected once.
|
|
321
|
+
function resolveSessionFiles(deps, presetId, inputResourceIds, inputFileIds) {
|
|
322
|
+
const hasExplicitFiles = (inputResourceIds?.length ?? 0) > 0 || (inputFileIds?.length ?? 0) > 0;
|
|
323
|
+
const resolved = [];
|
|
324
|
+
const seenFileIds = new Set();
|
|
325
|
+
if (hasExplicitFiles) {
|
|
102
326
|
const unmatched = [];
|
|
103
|
-
for (const id of inputResourceIds) {
|
|
327
|
+
for (const id of inputResourceIds ?? []) {
|
|
104
328
|
const resource = deps.registry.getResource({ resourceId: id }) ?? deps.registry.getResource({ resourceSlug: id });
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
107
|
-
matched.push(resolved);
|
|
108
|
-
else
|
|
329
|
+
const selection = toResolvedFile(resource);
|
|
330
|
+
if (!selection) {
|
|
109
331
|
unmatched.push(id);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (seenFileIds.has(selection.fileId))
|
|
335
|
+
continue;
|
|
336
|
+
seenFileIds.add(selection.fileId);
|
|
337
|
+
resolved.push(selection);
|
|
110
338
|
}
|
|
111
339
|
if (unmatched.length > 0) {
|
|
112
340
|
deps.logger.warn('Some input resourceIds did not match the local registry; ignoring', {
|
|
@@ -114,58 +342,92 @@ function resolveSessionResources(deps, presetId, inputResourceIds) {
|
|
|
114
342
|
availableSlugs: deps.registry.listResources().map(r => r.slug),
|
|
115
343
|
});
|
|
116
344
|
}
|
|
117
|
-
|
|
118
|
-
|
|
345
|
+
for (const fileId of inputFileIds ?? []) {
|
|
346
|
+
if (seenFileIds.has(fileId))
|
|
347
|
+
continue;
|
|
348
|
+
seenFileIds.add(fileId);
|
|
349
|
+
resolved.push({ fileId });
|
|
350
|
+
}
|
|
351
|
+
return resolved;
|
|
119
352
|
}
|
|
120
353
|
const preset = deps.presets.find(p => p.id === presetId);
|
|
121
354
|
const slugs = preset?.defaultResourceSlugs ?? [];
|
|
122
|
-
const resolved = [];
|
|
123
355
|
const missing = [];
|
|
124
356
|
for (const slug of slugs) {
|
|
125
|
-
const
|
|
126
|
-
if (
|
|
127
|
-
resolved.push(r);
|
|
128
|
-
else
|
|
357
|
+
const selection = toResolvedFile(deps.registry.getResource({ resourceSlug: slug }));
|
|
358
|
+
if (!selection) {
|
|
129
359
|
missing.push(slug);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (seenFileIds.has(selection.fileId))
|
|
363
|
+
continue;
|
|
364
|
+
seenFileIds.add(selection.fileId);
|
|
365
|
+
resolved.push(selection);
|
|
130
366
|
}
|
|
131
367
|
if (missing.length > 0) {
|
|
132
368
|
deps.logger.warn("Preset declares defaultResourceSlugs not present in the local registry; those slots will be skipped", { presetId, missing, availableSlugs: deps.registry.listResources().map(r => r.slug) });
|
|
133
369
|
}
|
|
134
370
|
return resolved;
|
|
135
371
|
}
|
|
136
|
-
function
|
|
372
|
+
function toResolvedFile(resource) {
|
|
137
373
|
if (!resource || !resource.latestRevision)
|
|
138
374
|
return null;
|
|
139
375
|
const file = resource.latestRevision.file;
|
|
140
376
|
return {
|
|
141
|
-
resourceId: resource.id,
|
|
142
|
-
slug: resource.slug,
|
|
143
|
-
name: resource.name,
|
|
144
377
|
fileId: file.id,
|
|
145
|
-
|
|
146
|
-
mimeType: file.mimeType,
|
|
378
|
+
metadata: { slug: resource.slug, name: resource.name ?? resource.slug },
|
|
147
379
|
};
|
|
148
380
|
}
|
|
149
|
-
async function
|
|
150
|
-
const file = await deps.registry.readFileById(
|
|
381
|
+
async function injectRegistryFile(deps, sessionId, selection) {
|
|
382
|
+
const file = await deps.registry.readFileById(selection.fileId);
|
|
151
383
|
if (!file) {
|
|
152
|
-
|
|
384
|
+
if (selection.metadata?.slug) {
|
|
385
|
+
throw new Error(`Registry file not found for resource ${selection.metadata.slug} (fileId=${selection.fileId})`);
|
|
386
|
+
}
|
|
387
|
+
deps.logger.warn('Selected fileId did not match the local registry; ignoring', {
|
|
388
|
+
fileId: selection.fileId,
|
|
389
|
+
});
|
|
390
|
+
return;
|
|
153
391
|
}
|
|
154
392
|
const result = await deps.sessionManager.callPluginMethod(SessionId(sessionId), 'resources.inject', {
|
|
155
393
|
sessionId,
|
|
156
|
-
filename:
|
|
157
|
-
mimeType:
|
|
394
|
+
filename: file.meta.filename,
|
|
395
|
+
mimeType: file.meta.mimeType,
|
|
158
396
|
size: file.buffer.length,
|
|
159
397
|
fileBuffer: file.buffer,
|
|
160
|
-
metadata:
|
|
398
|
+
metadata: selection.metadata,
|
|
161
399
|
});
|
|
162
400
|
if (!result.ok) {
|
|
163
401
|
deps.logger.error('Registry resource injection failed', undefined, {
|
|
164
402
|
sessionId,
|
|
165
|
-
|
|
403
|
+
fileId: selection.fileId,
|
|
166
404
|
error: result.error,
|
|
167
405
|
});
|
|
168
|
-
throw new Error(`Failed to inject
|
|
406
|
+
throw new Error(`Failed to inject registry file '${selection.fileId}': ${result.error.message}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
async function rollbackCreatedSession(deps, sessionId) {
|
|
410
|
+
let closeAttempted = false;
|
|
411
|
+
try {
|
|
412
|
+
const closeResult = await deps.sessionManager.withSessionLease(SessionId(sessionId), 'standalone:rollback', async (session) => {
|
|
413
|
+
closeAttempted = true;
|
|
414
|
+
return session.close();
|
|
415
|
+
});
|
|
416
|
+
if (!closeResult.ok) {
|
|
417
|
+
deps.logger.error(closeAttempted ? 'Failed to close session during rollback' : 'Failed to load session for rollback', undefined, { sessionId, error: closeResult.error });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
deps.logger.error(closeAttempted ? 'Failed to close session during rollback' : 'Failed to load session for rollback', error instanceof Error ? error : new Error(String(error)), { sessionId });
|
|
422
|
+
}
|
|
423
|
+
await removeWorktreeAfterFailure(deps, sessionId);
|
|
424
|
+
}
|
|
425
|
+
async function removeWorktreeAfterFailure(deps, sessionId) {
|
|
426
|
+
try {
|
|
427
|
+
await deps.gitFs.removeSessionWorktree(deps.instance.id, sessionId);
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
deps.logger.error('Failed to remove session worktree during rollback', error instanceof Error ? error : new Error(String(error)), { sessionId });
|
|
169
431
|
}
|
|
170
432
|
}
|
|
171
433
|
const handlers = {
|
|
@@ -179,6 +441,7 @@ const handlers = {
|
|
|
179
441
|
presetId: input.autoCreateSession.presetId,
|
|
180
442
|
initialPrompt: input.autoCreateSession.initialPrompt,
|
|
181
443
|
resourceIds: input.autoCreateSession.resourceIds,
|
|
444
|
+
fileIds: input.autoCreateSession.fileIds,
|
|
182
445
|
});
|
|
183
446
|
sessionId = result.sessionId;
|
|
184
447
|
}
|
|
@@ -215,9 +478,22 @@ const handlers = {
|
|
|
215
478
|
const result = await sessionManager.callManagerMethod('sessions.list', {});
|
|
216
479
|
if (!result.ok)
|
|
217
480
|
throw new Error(result.error.message);
|
|
218
|
-
|
|
481
|
+
// callManagerMethod is typed Result<unknown>, so the plugin's output schema
|
|
482
|
+
// does not reach us — validate here rather than assert.
|
|
483
|
+
const listed = z.object({ sessions: z.array(sessionMetadataSchema) }).parse(result.value);
|
|
484
|
+
return {
|
|
485
|
+
sessions: listed.sessions.map(s => ({
|
|
486
|
+
id: String(s.sessionId),
|
|
487
|
+
presetId: s.presetId,
|
|
488
|
+
status: s.status,
|
|
489
|
+
createdAt: new Date(s.createdAt).toISOString(),
|
|
490
|
+
})),
|
|
491
|
+
};
|
|
219
492
|
},
|
|
220
|
-
|
|
493
|
+
// Standalone has no auth: the token is empty and never checked. expiresAt is
|
|
494
|
+
// still part of the contract, so hand back a real timestamp rather than
|
|
495
|
+
// omitting the field and hoping no caller reads it.
|
|
496
|
+
'tokens.create': async () => ({ token: '', expiresAt: new Date(Date.now() + 3600_000).toISOString() }),
|
|
221
497
|
'resources.create': async (deps, input) => deps.registry.createResource(input),
|
|
222
498
|
'resources.addRevision': async (deps, input) => deps.registry.addRevision(input),
|
|
223
499
|
'resources.get': async (deps, input) => {
|
|
@@ -250,7 +526,7 @@ const handlers = {
|
|
|
250
526
|
.split('/')
|
|
251
527
|
.map(seg => encodeURIComponent(seg))
|
|
252
528
|
.join('/');
|
|
253
|
-
const url = `${deps.
|
|
529
|
+
const url = `${deps.getPublicBaseUrl()}/api/v1/instances/${input.instanceId}/sessions/${input.sessionId}/files/${input.scope}/${encodedPath}?token=${encodeURIComponent(token)}`;
|
|
254
530
|
return { url, expiresAt: new Date(expiresAt).toISOString() };
|
|
255
531
|
},
|
|
256
532
|
};
|