@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 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;QAC3B,OAAO,EAAE,UAAU,CAAC,OAAO;QAC3B,cAAc,EAAE,UAAU,CAAC,cAAc;KACzC,CAAC,CAAA;AACH,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"}
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"}
@@ -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, SessionManager } from '@roj-ai/sdk';
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: 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: GitInstanceFs;
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
- publicBaseUrl: string;
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;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAGjE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAC3B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAClD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAGxD,UAAU,IAAI;IACb,QAAQ,EAAE,aAAa,CAAA;IACvB,cAAc,EAAE,cAAc,CAAA;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,QAAQ,EAAE,aAAa,CAAA;IACvB,6DAA6D;IAC7D,KAAK,EAAE,aAAa,CAAA;IACpB,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAA;IACnB,oGAAoG;IACpG,aAAa,EAAE,MAAM,CAAA;CACrB;AAQD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAuBlD"}
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"}
@@ -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
- const body = await c.req.json().catch(() => ({}));
24
- if (Array.isArray(body.batch)) {
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 handler(deps, input ?? {});
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
- // Shared by `instances.create.autoCreateSession` and `sessions.create`. Creates
55
- // a session via the SDK session manager, injects any matching local resources,
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
- // Roll back the worktree we just created so a retry isn't blocked by an
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
- const resources = resolveSessionResources(deps, input.presetId, input.resourceIds);
79
- for (const resource of resources) {
80
- await injectRegistryResource(deps, sessionId, resource);
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
- return { sessionId };
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 input.resourceIds — matched against the local registry by ID
97
- // first, then by slug (since local-dev callers often pass slugs as ids).
98
- // 2. fallback to preset.defaultResourceSlugs (looked up by slug).
99
- function resolveSessionResources(deps, presetId, inputResourceIds) {
100
- if (inputResourceIds && inputResourceIds.length > 0) {
101
- const matched = [];
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 resolved = toResolvedResource(resource);
106
- if (resolved)
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
- if (matched.length > 0)
118
- return matched;
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 r = toResolvedResource(deps.registry.getResource({ resourceSlug: slug }));
126
- if (r)
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 toResolvedResource(resource) {
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
- filename: file.filename,
146
- mimeType: file.mimeType,
378
+ metadata: { slug: resource.slug, name: resource.name ?? resource.slug },
147
379
  };
148
380
  }
149
- async function injectRegistryResource(deps, sessionId, resource) {
150
- const file = await deps.registry.readFileById(resource.fileId);
381
+ async function injectRegistryFile(deps, sessionId, selection) {
382
+ const file = await deps.registry.readFileById(selection.fileId);
151
383
  if (!file) {
152
- throw new Error(`Registry file not found for resource ${resource.slug} (fileId=${resource.fileId})`);
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: resource.filename,
157
- mimeType: resource.mimeType,
394
+ filename: file.meta.filename,
395
+ mimeType: file.meta.mimeType,
158
396
  size: file.buffer.length,
159
397
  fileBuffer: file.buffer,
160
- metadata: { slug: resource.slug, name: resource.name ?? resource.slug },
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
- slug: resource.slug,
403
+ fileId: selection.fileId,
166
404
  error: result.error,
167
405
  });
168
- throw new Error(`Failed to inject resource '${resource.slug}': ${result.error.message ?? result.error.type}`);
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
- return result.value;
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
- 'tokens.create': async () => ({ token: '' }),
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.publicBaseUrl}/api/v1/instances/${input.instanceId}/sessions/${input.sessionId}/files/${input.scope}/${encodedPath}?token=${encodeURIComponent(token)}`;
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
  };