@roj-ai/standalone-server 0.1.11 → 0.1.13
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/files-upload-route.d.ts +36 -0
- package/dist/files-upload-route.d.ts.map +1 -0
- package/dist/files-upload-route.js +50 -0
- package/dist/files-upload-route.js.map +1 -0
- package/dist/git-instance-fs.d.ts +60 -0
- package/dist/git-instance-fs.d.ts.map +1 -0
- package/dist/git-instance-fs.js +164 -0
- package/dist/git-instance-fs.js.map +1 -0
- package/dist/instance.d.ts +6 -0
- package/dist/instance.d.ts.map +1 -1
- package/dist/instance.js +5 -0
- package/dist/instance.js.map +1 -1
- package/dist/local-registry.d.ts +109 -0
- package/dist/local-registry.d.ts.map +1 -0
- package/dist/local-registry.js +252 -0
- package/dist/local-registry.js.map +1 -0
- package/dist/main.js +7 -1
- package/dist/main.js.map +1 -1
- package/dist/platform-api.d.ts +11 -1
- package/dist/platform-api.d.ts.map +1 -1
- package/dist/platform-api.js +176 -13
- package/dist/platform-api.js.map +1 -1
- package/dist/server.d.ts +7 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +31 -1
- package/dist/server.js.map +1 -1
- package/dist/session-file-route.d.ts +24 -0
- package/dist/session-file-route.d.ts.map +1 -0
- package/dist/session-file-route.js +72 -0
- package/dist/session-file-route.js.map +1 -0
- package/dist/signed-token.d.ts +25 -0
- package/dist/signed-token.d.ts.map +1 -0
- package/dist/signed-token.js +50 -0
- package/dist/signed-token.js.map +1 -0
- package/dist/user-config-loader.d.ts.map +1 -1
- package/dist/user-config-loader.js +37 -1
- package/dist/user-config-loader.js.map +1 -1
- package/package.json +5 -5
- package/src/files-upload-route.ts +64 -0
- package/src/git-instance-fs.ts +187 -0
- package/src/instance.ts +12 -1
- package/src/local-registry.ts +334 -0
- package/src/main.ts +7 -1
- package/src/platform-api.ts +251 -13
- package/src/server.ts +47 -2
- package/src/session-file-route.ts +99 -0
- package/src/signed-token.ts +68 -0
- package/src/user-config-loader.ts +41 -2
package/dist/server.js
CHANGED
|
@@ -18,9 +18,14 @@ import { createBunPlatform } from '@roj-ai/sdk/bun-platform';
|
|
|
18
18
|
import { createBunWebSocketHandlers } from '@roj-ai/transport/bun';
|
|
19
19
|
import { Hono } from 'hono';
|
|
20
20
|
import { cors } from 'hono/cors';
|
|
21
|
+
import { createFilesUploadRoute } from './files-upload-route.js';
|
|
22
|
+
import { GitInstanceFs } from './git-instance-fs.js';
|
|
21
23
|
import { createInstance } from './instance.js';
|
|
24
|
+
import { LocalRegistry } from './local-registry.js';
|
|
22
25
|
import { createPlatformApi } from './platform-api.js';
|
|
23
26
|
import { proxyPreview } from './preview-proxy.js';
|
|
27
|
+
import { createSessionFileRoute } from './session-file-route.js';
|
|
28
|
+
import { generateTokenSecret } from './signed-token.js';
|
|
24
29
|
export async function startStandaloneServer(options) {
|
|
25
30
|
const envConfig = loadConfig();
|
|
26
31
|
const config = options.config ? { ...envConfig, ...options.config } : envConfig;
|
|
@@ -54,13 +59,35 @@ export async function startStandaloneServer(options) {
|
|
|
54
59
|
...services,
|
|
55
60
|
sessionRuntime: sessionManager,
|
|
56
61
|
});
|
|
57
|
-
const
|
|
62
|
+
const tokenSecret = generateTokenSecret();
|
|
63
|
+
const publicHost = config.host === '0.0.0.0' ? 'localhost' : config.host;
|
|
64
|
+
const publicBaseUrl = `http://${publicHost}:${config.port}`;
|
|
65
|
+
const registry = new LocalRegistry(config.dataPath, logger);
|
|
66
|
+
await registry.init();
|
|
67
|
+
if (options.localResources?.length) {
|
|
68
|
+
await registry.bootstrapLocalResources(options.localResources);
|
|
69
|
+
}
|
|
70
|
+
const gitFs = new GitInstanceFs(config.dataPath, logger);
|
|
71
|
+
await gitFs.initInstance(instance.id);
|
|
72
|
+
const platformApp = createPlatformApi({
|
|
73
|
+
instance,
|
|
74
|
+
sessionManager,
|
|
75
|
+
logger,
|
|
76
|
+
presets,
|
|
77
|
+
registry,
|
|
78
|
+
gitFs,
|
|
79
|
+
tokenSecret,
|
|
80
|
+
publicBaseUrl,
|
|
81
|
+
});
|
|
58
82
|
const outerApp = new Hono();
|
|
59
83
|
// Reflect the request origin and allow credentials — the SPA uses
|
|
60
84
|
// `credentials: 'include'` on its RPC fetches, which requires a specific
|
|
61
85
|
// origin header (not `*`) plus `Access-Control-Allow-Credentials: true`.
|
|
62
86
|
outerApp.use('*', cors({ origin: (origin) => origin ?? '*', credentials: true }));
|
|
63
87
|
outerApp.get('/health', (c) => c.json({ status: 'ok', timestamp: Date.now() }));
|
|
88
|
+
// Multipart file upload — must be registered BEFORE the /api/v1 RPC mount
|
|
89
|
+
// so it isn't treated as JSON RPC.
|
|
90
|
+
outerApp.post('/api/v1/files/upload', createFilesUploadRoute({ registry }));
|
|
64
91
|
outerApp.route('/api/v1', platformApp);
|
|
65
92
|
// Path-based preview proxy (must come BEFORE the catch-all instance route)
|
|
66
93
|
outerApp.all('/api/v1/instances/:id/preview/*', (c) => {
|
|
@@ -68,6 +95,9 @@ export async function startStandaloneServer(options) {
|
|
|
68
95
|
const prefix = `/api/v1/instances/${id}/preview/`;
|
|
69
96
|
return proxyPreview(c.req.raw, prefix, sessionManager, logger);
|
|
70
97
|
});
|
|
98
|
+
// Token-authenticated download URL minted by `sessionFiles.createDownloadUrl`.
|
|
99
|
+
// Must come BEFORE the agent catch-all so the token gate runs.
|
|
100
|
+
outerApp.get('/api/v1/instances/:id/sessions/:sid/files/:scope{workspace|session}/*', createSessionFileRoute({ tokenSecret, agentApp, logger }));
|
|
71
101
|
// Noop auth-exchange (platform has real cookies; standalone is open)
|
|
72
102
|
outerApp.post('/api/v1/instances/:id/exchange', (c) => c.json({ ok: true }));
|
|
73
103
|
// Instance-scoped agent routes — strip the instance prefix and delegate to createApp
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,SAAS,EAAE,wBAAwB,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAC7F,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAA;AAC1D,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AACnF,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAA;AAClE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAC3B,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,cAAc,EAAsB,MAAM,eAAe,CAAA;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token-authenticated session file download route.
|
|
3
|
+
*
|
|
4
|
+
* GET /api/v1/instances/:id/sessions/:sid/files/:scope/{path}?token=...
|
|
5
|
+
*
|
|
6
|
+
* Validates the HMAC token minted by `sessionFiles.createDownloadUrl` against
|
|
7
|
+
* the URL's (instanceId, sessionId, scope, path), then proxies to the SDK's
|
|
8
|
+
* existing `/sessions/:sid/workspace/*` or `/sessions/:sid/files/*` route on
|
|
9
|
+
* the agent app — same shape as roj-platform's `session-file-route.ts`, just
|
|
10
|
+
* served in-process.
|
|
11
|
+
*/
|
|
12
|
+
import type { Logger } from '@roj-ai/sdk';
|
|
13
|
+
import type { Context } from 'hono';
|
|
14
|
+
interface AgentAppLike {
|
|
15
|
+
fetch: (req: Request) => Response | Promise<Response>;
|
|
16
|
+
}
|
|
17
|
+
interface Deps {
|
|
18
|
+
tokenSecret: string;
|
|
19
|
+
agentApp: AgentAppLike;
|
|
20
|
+
logger: Logger;
|
|
21
|
+
}
|
|
22
|
+
export declare function createSessionFileRoute(deps: Deps): (c: Context) => Promise<Response>;
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=session-file-route.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-file-route.d.ts","sourceRoot":"","sources":["../src/session-file-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAMnC,UAAU,YAAY;IACrB,KAAK,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CACrD;AAED,UAAU,IAAI;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,YAAY,CAAA;IACtB,MAAM,EAAE,MAAM,CAAA;CACd;AAOD,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,IAAI,IAClC,GAAG,OAAO,uBA2DxB"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token-authenticated session file download route.
|
|
3
|
+
*
|
|
4
|
+
* GET /api/v1/instances/:id/sessions/:sid/files/:scope/{path}?token=...
|
|
5
|
+
*
|
|
6
|
+
* Validates the HMAC token minted by `sessionFiles.createDownloadUrl` against
|
|
7
|
+
* the URL's (instanceId, sessionId, scope, path), then proxies to the SDK's
|
|
8
|
+
* existing `/sessions/:sid/workspace/*` or `/sessions/:sid/files/*` route on
|
|
9
|
+
* the agent app — same shape as roj-platform's `session-file-route.ts`, just
|
|
10
|
+
* served in-process.
|
|
11
|
+
*/
|
|
12
|
+
import { verifyFileToken } from './signed-token.js';
|
|
13
|
+
const SCOPE_TO_SDK_PREFIX = {
|
|
14
|
+
workspace: 'workspace',
|
|
15
|
+
session: 'files',
|
|
16
|
+
};
|
|
17
|
+
export function createSessionFileRoute(deps) {
|
|
18
|
+
return async (c) => {
|
|
19
|
+
const instanceId = c.req.param('id');
|
|
20
|
+
const sessionId = c.req.param('sid');
|
|
21
|
+
const scope = c.req.param('scope');
|
|
22
|
+
const url = new URL(c.req.url);
|
|
23
|
+
const tokenStr = url.searchParams.get('token');
|
|
24
|
+
if (!tokenStr) {
|
|
25
|
+
return c.json({ error: { type: 'unauthorized', message: 'Missing token' } }, 401);
|
|
26
|
+
}
|
|
27
|
+
const verified = verifyFileToken(deps.tokenSecret, tokenStr);
|
|
28
|
+
if (!verified.ok) {
|
|
29
|
+
return c.json({ error: { type: 'unauthorized', message: `Invalid token (${verified.error})` } }, 401);
|
|
30
|
+
}
|
|
31
|
+
// Recover the file path the token was signed for. Hono `*` capture is
|
|
32
|
+
// available via the URL pathname; decode each segment back to its
|
|
33
|
+
// original form so the comparison against the signed payload is exact.
|
|
34
|
+
const pathPrefix = `/api/v1/instances/${instanceId}/sessions/${sessionId}/files/${scope}/`;
|
|
35
|
+
if (!url.pathname.startsWith(pathPrefix)) {
|
|
36
|
+
return c.json({ error: { type: 'invalid_request', message: 'Path mismatch' } }, 400);
|
|
37
|
+
}
|
|
38
|
+
const requestedPath = url.pathname
|
|
39
|
+
.slice(pathPrefix.length)
|
|
40
|
+
.split('/')
|
|
41
|
+
.map(seg => decodeURIComponent(seg))
|
|
42
|
+
.join('/');
|
|
43
|
+
const p = verified.payload;
|
|
44
|
+
if (p.instanceId !== instanceId ||
|
|
45
|
+
p.sessionId !== sessionId ||
|
|
46
|
+
p.scope !== scope ||
|
|
47
|
+
p.path !== requestedPath) {
|
|
48
|
+
return c.json({ error: { type: 'unauthorized', message: 'Token does not match requested file' } }, 401);
|
|
49
|
+
}
|
|
50
|
+
// Proxy to the SDK's underlying file route. Note the path/scope mapping:
|
|
51
|
+
// public `workspace` → SDK `/sessions/:sid/workspace/*`,
|
|
52
|
+
// public `session` → SDK `/sessions/:sid/files/*`.
|
|
53
|
+
const sdkPrefix = SCOPE_TO_SDK_PREFIX[scope];
|
|
54
|
+
const innerUrl = `${url.origin}/sessions/${sessionId}/${sdkPrefix}/${encodePathSegments(requestedPath)}`;
|
|
55
|
+
try {
|
|
56
|
+
return await deps.agentApp.fetch(new Request(innerUrl, { method: 'GET' }));
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
deps.logger.error('Session file proxy failed', err instanceof Error ? err : new Error(String(err)), {
|
|
60
|
+
instanceId,
|
|
61
|
+
sessionId,
|
|
62
|
+
scope,
|
|
63
|
+
path: requestedPath,
|
|
64
|
+
});
|
|
65
|
+
return c.json({ error: { type: 'proxy_error', message: 'File proxy failed' } }, 502);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function encodePathSegments(path) {
|
|
70
|
+
return path.split('/').map(seg => encodeURIComponent(seg)).join('/');
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=session-file-route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-file-route.js","sourceRoot":"","sources":["../src/session-file-route.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny HMAC-signed download token used by `sessionFiles.createDownloadUrl`.
|
|
3
|
+
*
|
|
4
|
+
* The token binds (instanceId, sessionId, scope, path, expiresAt) so a leaked
|
|
5
|
+
* URL can only fetch the exact file it was minted for, and only briefly. Same
|
|
6
|
+
* shape as roj-platform's `session-file-token.ts`, just pruned for local use.
|
|
7
|
+
*/
|
|
8
|
+
export interface SignedFilePayload {
|
|
9
|
+
instanceId: string;
|
|
10
|
+
sessionId: string;
|
|
11
|
+
scope: 'workspace' | 'session';
|
|
12
|
+
path: string;
|
|
13
|
+
expiresAt: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function generateTokenSecret(): string;
|
|
16
|
+
export declare function signFileToken(secret: string, payload: SignedFilePayload): string;
|
|
17
|
+
export type VerifyResult = {
|
|
18
|
+
ok: true;
|
|
19
|
+
payload: SignedFilePayload;
|
|
20
|
+
} | {
|
|
21
|
+
ok: false;
|
|
22
|
+
error: 'malformed' | 'bad_signature' | 'expired';
|
|
23
|
+
};
|
|
24
|
+
export declare function verifyFileToken(secret: string, token: string): VerifyResult;
|
|
25
|
+
//# sourceMappingURL=signed-token.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signed-token.d.ts","sourceRoot":"","sources":["../src/signed-token.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,MAAM,WAAW,iBAAiB;IACjC,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,WAAW,GAAG,SAAS,CAAA;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;CACjB;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAIhF;AAED,MAAM,MAAM,YAAY,GACrB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,iBAAiB,CAAA;CAAE,GACxC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,WAAW,GAAG,eAAe,GAAG,SAAS,CAAA;CAAE,CAAA;AAElE,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,CAsB3E"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny HMAC-signed download token used by `sessionFiles.createDownloadUrl`.
|
|
3
|
+
*
|
|
4
|
+
* The token binds (instanceId, sessionId, scope, path, expiresAt) so a leaked
|
|
5
|
+
* URL can only fetch the exact file it was minted for, and only briefly. Same
|
|
6
|
+
* shape as roj-platform's `session-file-token.ts`, just pruned for local use.
|
|
7
|
+
*/
|
|
8
|
+
import { randomBytes, createHmac, timingSafeEqual } from 'node:crypto';
|
|
9
|
+
export function generateTokenSecret() {
|
|
10
|
+
return randomBytes(32).toString('hex');
|
|
11
|
+
}
|
|
12
|
+
export function signFileToken(secret, payload) {
|
|
13
|
+
const body = base64UrlEncode(Buffer.from(JSON.stringify(payload), 'utf8'));
|
|
14
|
+
const sig = base64UrlEncode(hmac(secret, body));
|
|
15
|
+
return `${body}.${sig}`;
|
|
16
|
+
}
|
|
17
|
+
export function verifyFileToken(secret, token) {
|
|
18
|
+
const dot = token.indexOf('.');
|
|
19
|
+
if (dot < 0)
|
|
20
|
+
return { ok: false, error: 'malformed' };
|
|
21
|
+
const body = token.slice(0, dot);
|
|
22
|
+
const sigEncoded = token.slice(dot + 1);
|
|
23
|
+
const expectedSig = hmac(secret, body);
|
|
24
|
+
const givenSig = base64UrlDecode(sigEncoded);
|
|
25
|
+
if (givenSig.length !== expectedSig.length || !timingSafeEqual(givenSig, expectedSig)) {
|
|
26
|
+
return { ok: false, error: 'bad_signature' };
|
|
27
|
+
}
|
|
28
|
+
let payload;
|
|
29
|
+
try {
|
|
30
|
+
payload = JSON.parse(base64UrlDecode(body).toString('utf8'));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return { ok: false, error: 'malformed' };
|
|
34
|
+
}
|
|
35
|
+
if (typeof payload.expiresAt !== 'number' || payload.expiresAt < Date.now()) {
|
|
36
|
+
return { ok: false, error: 'expired' };
|
|
37
|
+
}
|
|
38
|
+
return { ok: true, payload };
|
|
39
|
+
}
|
|
40
|
+
function hmac(secret, body) {
|
|
41
|
+
return createHmac('sha256', secret).update(body).digest();
|
|
42
|
+
}
|
|
43
|
+
function base64UrlEncode(buf) {
|
|
44
|
+
return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
45
|
+
}
|
|
46
|
+
function base64UrlDecode(s) {
|
|
47
|
+
const pad = '='.repeat((4 - (s.length % 4)) % 4);
|
|
48
|
+
return Buffer.from(s.replace(/-/g, '+').replace(/_/g, '/') + pad, 'base64');
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=signed-token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signed-token.js","sourceRoot":"","sources":["../src/signed-token.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user-config-loader.d.ts","sourceRoot":"","sources":["../src/user-config-loader.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"user-config-loader.d.ts","sourceRoot":"","sources":["../src/user-config-loader.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAyB,SAAS,EAAE,MAAM,aAAa,CAAA;AAKnE,wBAAsB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CA8D3E"}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { validatePreset } from '@roj-ai/sdk';
|
|
8
8
|
import { existsSync } from 'node:fs';
|
|
9
|
-
import { resolve } from 'node:path';
|
|
9
|
+
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
10
10
|
export async function loadUserConfig(configPath) {
|
|
11
11
|
const absolutePath = resolve(process.cwd(), configPath);
|
|
12
12
|
if (!existsSync(absolutePath)) {
|
|
@@ -50,10 +50,46 @@ export async function loadUserConfig(configPath) {
|
|
|
50
50
|
if (duplicates.length > 0) {
|
|
51
51
|
throw new Error(`Duplicate preset IDs: ${[...new Set(duplicates)].join(', ')}`);
|
|
52
52
|
}
|
|
53
|
+
const configDir = dirname(absolutePath);
|
|
54
|
+
const localResources = parseLocalResources(typedConfig.localResources, configDir, absolutePath);
|
|
53
55
|
return {
|
|
54
56
|
presets,
|
|
55
57
|
sandboxed: typedConfig.sandboxed,
|
|
56
58
|
snapshotter: typedConfig.snapshotter,
|
|
59
|
+
localResources,
|
|
57
60
|
};
|
|
58
61
|
}
|
|
62
|
+
function parseLocalResources(raw, configDir, configPath) {
|
|
63
|
+
if (raw === undefined)
|
|
64
|
+
return undefined;
|
|
65
|
+
if (!Array.isArray(raw)) {
|
|
66
|
+
throw new Error(`'localResources' must be an array: ${configPath}`);
|
|
67
|
+
}
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
return raw.map((entry, i) => {
|
|
70
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
71
|
+
throw new Error(`localResources[${i}] must be an object: ${configPath}`);
|
|
72
|
+
}
|
|
73
|
+
const r = entry;
|
|
74
|
+
if (typeof r.slug !== 'string' || !r.slug) {
|
|
75
|
+
throw new Error(`localResources[${i}] missing required 'slug': ${configPath}`);
|
|
76
|
+
}
|
|
77
|
+
if (typeof r.path !== 'string' || !r.path) {
|
|
78
|
+
throw new Error(`localResources[${i}] missing required 'path': ${configPath}`);
|
|
79
|
+
}
|
|
80
|
+
if (seen.has(r.slug)) {
|
|
81
|
+
throw new Error(`Duplicate localResources slug '${r.slug}': ${configPath}`);
|
|
82
|
+
}
|
|
83
|
+
seen.add(r.slug);
|
|
84
|
+
const resolvedPath = isAbsolute(r.path) ? r.path : resolve(configDir, r.path);
|
|
85
|
+
if (!existsSync(resolvedPath)) {
|
|
86
|
+
throw new Error(`localResources[${i}] file not found: ${resolvedPath} (config: ${configPath})`);
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
slug: r.slug,
|
|
90
|
+
path: resolvedPath,
|
|
91
|
+
name: typeof r.name === 'string' ? r.name : undefined,
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
}
|
|
59
95
|
//# sourceMappingURL=user-config-loader.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user-config-loader.js","sourceRoot":"","sources":["../src/user-config-loader.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"user-config-loader.js","sourceRoot":"","sources":["../src/user-config-loader.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roj-ai/standalone-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
"type-check": "tsc --noEmit"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@roj-ai/client": "^0.1.
|
|
37
|
-
"@roj-ai/sdk": "^0.1.
|
|
38
|
-
"@roj-ai/shared": "^0.1.
|
|
39
|
-
"@roj-ai/transport": "^0.1.
|
|
36
|
+
"@roj-ai/client": "^0.1.13",
|
|
37
|
+
"@roj-ai/sdk": "^0.1.13",
|
|
38
|
+
"@roj-ai/shared": "^0.1.13",
|
|
39
|
+
"@roj-ai/transport": "^0.1.13",
|
|
40
40
|
"hono": "^4.10.7"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/v1/files/upload — multipart endpoint matching the `@roj-ai/client/platform.files.upload`
|
|
3
|
+
* two-phase contract:
|
|
4
|
+
*
|
|
5
|
+
* 1. Preflight with `contentHash`, `filename`, `mimeType` only. If we already have
|
|
6
|
+
* the bytes (sha-256 match), respond `{ ok, fileId, ..., deduped: true }`.
|
|
7
|
+
* Otherwise respond 409 `{ error: 'file-required' }`.
|
|
8
|
+
* 2. Retry with the same fields plus a `file` blob. We persist the bytes,
|
|
9
|
+
* record the file, and respond `{ ok, fileId, ..., deduped: false }`.
|
|
10
|
+
*
|
|
11
|
+
* No bearer auth (standalone is open) — but we accept and ignore an
|
|
12
|
+
* `Authorization: Bearer …` header so client code copy-pasted from the platform
|
|
13
|
+
* keeps working.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Context } from 'hono'
|
|
17
|
+
import type { LocalRegistry } from './local-registry.js'
|
|
18
|
+
|
|
19
|
+
interface Deps {
|
|
20
|
+
registry: LocalRegistry
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createFilesUploadRoute(deps: Deps) {
|
|
24
|
+
return async (c: Context) => {
|
|
25
|
+
let form: FormData
|
|
26
|
+
try {
|
|
27
|
+
form = await c.req.formData()
|
|
28
|
+
} catch {
|
|
29
|
+
return c.json({ ok: false, error: 'invalid_multipart' }, 400)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const contentHash = form.get('contentHash')
|
|
33
|
+
const filename = form.get('filename')
|
|
34
|
+
const mimeType = form.get('mimeType')
|
|
35
|
+
if (typeof contentHash !== 'string' || typeof filename !== 'string' || typeof mimeType !== 'string') {
|
|
36
|
+
return c.json(
|
|
37
|
+
{ ok: false, error: 'invalid_form' },
|
|
38
|
+
400,
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const file = form.get('file')
|
|
43
|
+
const buffer = file && typeof file !== 'string' && 'arrayBuffer' in file
|
|
44
|
+
? Buffer.from(await file.arrayBuffer())
|
|
45
|
+
: undefined
|
|
46
|
+
|
|
47
|
+
const result = await deps.registry.uploadFile({ buffer, filename, mimeType, contentHash })
|
|
48
|
+
|
|
49
|
+
if ('error' in result) {
|
|
50
|
+
// Preflight miss — client retries with body.
|
|
51
|
+
return c.json({ ok: false, error: result.error }, 409)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return c.json({
|
|
55
|
+
ok: true,
|
|
56
|
+
fileId: result.fileId,
|
|
57
|
+
filename: result.filename,
|
|
58
|
+
mimeType: result.mimeType,
|
|
59
|
+
size: result.size,
|
|
60
|
+
r2Key: '',
|
|
61
|
+
deduped: result.deduped,
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-instance git layout for the standalone server.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors what `roj-platform` does inside an E2B sandbox: each instance owns
|
|
5
|
+
* a bare repo, each session owns a worktree branched from `main`. The SDK
|
|
6
|
+
* receives the worktree path as `workspaceDir` on `sessions.create` and treats
|
|
7
|
+
* it as an opaque directory — it doesn't know about git.
|
|
8
|
+
*
|
|
9
|
+
* {dataPath}/instances/{instanceId}/repo.git/ ← bare, branch `main`
|
|
10
|
+
* {dataPath}/instances/{instanceId}/sessions/{sid}/ ← worktree, branch `session/{sid}`
|
|
11
|
+
*
|
|
12
|
+
* Why a bare repo + worktrees instead of a plain dir per session:
|
|
13
|
+
* - Parallel sessions on one instance need independent commit history.
|
|
14
|
+
* - `git worktree` shares object storage, so the cost is mostly metadata.
|
|
15
|
+
* - Mental model matches platform — agent `git status` / `git log` behave
|
|
16
|
+
* identically against standalone and against E2B.
|
|
17
|
+
*
|
|
18
|
+
* Idempotent: `initInstance` skips if `repo.git` already exists,
|
|
19
|
+
* `addSessionWorktree` skips if the target worktree is already registered.
|
|
20
|
+
* That keeps `roj-standalone` restart-safe — bare repos and worktrees on disk
|
|
21
|
+
* persist across process restarts the same way `LocalRegistry` does.
|
|
22
|
+
*
|
|
23
|
+
* No auto-commit. Resources extracted via `resources.inject` land in the
|
|
24
|
+
* worktree as untracked files; whether to commit them is the agent's call,
|
|
25
|
+
* matching platform behavior.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import type { Logger } from '@roj-ai/sdk'
|
|
29
|
+
import { execFile } from 'node:child_process'
|
|
30
|
+
import { existsSync } from 'node:fs'
|
|
31
|
+
import { mkdir, rm } from 'node:fs/promises'
|
|
32
|
+
import { join } from 'node:path'
|
|
33
|
+
import { promisify } from 'node:util'
|
|
34
|
+
|
|
35
|
+
const execFileAsync = promisify(execFile)
|
|
36
|
+
|
|
37
|
+
// Used for the empty initial commit on `main`. Real commits the agent makes
|
|
38
|
+
// will pick up the host's git config; this just keeps `commit-tree` from
|
|
39
|
+
// failing on machines without `user.name`/`user.email` set.
|
|
40
|
+
const BOOTSTRAP_GIT_ENV = {
|
|
41
|
+
GIT_AUTHOR_NAME: 'roj-standalone',
|
|
42
|
+
GIT_AUTHOR_EMAIL: 'standalone@roj.local',
|
|
43
|
+
GIT_COMMITTER_NAME: 'roj-standalone',
|
|
44
|
+
GIT_COMMITTER_EMAIL: 'standalone@roj.local',
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class GitInstanceFs {
|
|
48
|
+
constructor(
|
|
49
|
+
private readonly dataPath: string,
|
|
50
|
+
private readonly logger: Logger,
|
|
51
|
+
) {}
|
|
52
|
+
|
|
53
|
+
instanceDir(instanceId: string): string {
|
|
54
|
+
return join(this.dataPath, 'instances', instanceId)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
repoDir(instanceId: string): string {
|
|
58
|
+
return join(this.instanceDir(instanceId), 'repo.git')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
worktreeDir(instanceId: string, sessionId: string): string {
|
|
62
|
+
return join(this.instanceDir(instanceId), 'sessions', sessionId)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Idempotently initialize the bare repo for an instance with an empty
|
|
67
|
+
* initial commit on `main`. The commit exists so subsequent
|
|
68
|
+
* `git worktree add -b session/{sid}` calls have something to branch from.
|
|
69
|
+
*/
|
|
70
|
+
async initInstance(instanceId: string): Promise<void> {
|
|
71
|
+
const repoDir = this.repoDir(instanceId)
|
|
72
|
+
if (existsSync(repoDir)) {
|
|
73
|
+
this.logger.debug('Instance bare repo already exists, skipping init', { instanceId })
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
await mkdir(this.instanceDir(instanceId), { recursive: true })
|
|
78
|
+
await this.git(['init', '--bare', repoDir])
|
|
79
|
+
// `git init --bare` may default to `master` on older git; normalize so
|
|
80
|
+
// the worktree branch starts from a known parent ref.
|
|
81
|
+
await this.git(['symbolic-ref', 'HEAD', 'refs/heads/main'], { cwd: repoDir })
|
|
82
|
+
|
|
83
|
+
const tree = (await this.git(['mktree'], { cwd: repoDir, input: '' })).trim()
|
|
84
|
+
const commit = (await this.git(
|
|
85
|
+
['commit-tree', tree, '-m', 'Initial commit'],
|
|
86
|
+
{ cwd: repoDir, env: BOOTSTRAP_GIT_ENV },
|
|
87
|
+
)).trim()
|
|
88
|
+
await this.git(['update-ref', 'refs/heads/main', commit], { cwd: repoDir })
|
|
89
|
+
|
|
90
|
+
this.logger.info('Initialized instance bare repo', { instanceId, repoDir })
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Idempotently add a worktree for `sessionId` on branch `session/{sid}`,
|
|
95
|
+
* branching from `main`. Throws if the target dir exists but is not a valid
|
|
96
|
+
* worktree of this instance's bare repo (unrecoverable conflict).
|
|
97
|
+
*/
|
|
98
|
+
async addSessionWorktree(instanceId: string, sessionId: string): Promise<string> {
|
|
99
|
+
const repoDir = this.repoDir(instanceId)
|
|
100
|
+
const worktreePath = this.worktreeDir(instanceId, sessionId)
|
|
101
|
+
|
|
102
|
+
if (!existsSync(repoDir)) {
|
|
103
|
+
throw new Error(`Cannot add session worktree: instance bare repo missing at ${repoDir}`)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (existsSync(worktreePath)) {
|
|
107
|
+
if (await this.isLinkedWorktree(repoDir, worktreePath)) {
|
|
108
|
+
this.logger.debug('Session worktree already exists, skipping add', { instanceId, sessionId })
|
|
109
|
+
return worktreePath
|
|
110
|
+
}
|
|
111
|
+
throw new Error(`Path exists but is not a registered worktree of ${repoDir}: ${worktreePath}`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await mkdir(join(this.instanceDir(instanceId), 'sessions'), { recursive: true })
|
|
115
|
+
await this.git(
|
|
116
|
+
['worktree', 'add', '-b', `session/${sessionId}`, worktreePath, 'main'],
|
|
117
|
+
{ cwd: repoDir },
|
|
118
|
+
)
|
|
119
|
+
this.logger.info('Added session worktree', { instanceId, sessionId, worktreePath })
|
|
120
|
+
return worktreePath
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Remove a session's worktree (filesystem + bookkeeping). Silently no-ops
|
|
125
|
+
* if the worktree was never registered.
|
|
126
|
+
*/
|
|
127
|
+
async removeSessionWorktree(instanceId: string, sessionId: string): Promise<void> {
|
|
128
|
+
const repoDir = this.repoDir(instanceId)
|
|
129
|
+
const worktreePath = this.worktreeDir(instanceId, sessionId)
|
|
130
|
+
|
|
131
|
+
if (!existsSync(repoDir) || !existsSync(worktreePath)) return
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
await this.git(['worktree', 'remove', '--force', worktreePath], { cwd: repoDir })
|
|
135
|
+
} catch (err) {
|
|
136
|
+
// worktree may already be detached; fall back to filesystem rm + prune.
|
|
137
|
+
this.logger.warn('git worktree remove failed, falling back to rm + prune', {
|
|
138
|
+
instanceId,
|
|
139
|
+
sessionId,
|
|
140
|
+
error: err instanceof Error ? err.message : String(err),
|
|
141
|
+
})
|
|
142
|
+
await rm(worktreePath, { recursive: true, force: true })
|
|
143
|
+
await this.git(['worktree', 'prune'], { cwd: repoDir }).catch(() => {})
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Tear down everything for an instance: every worktree, then the
|
|
149
|
+
* bare repo + on-disk dir. Safe to call when nothing exists.
|
|
150
|
+
*/
|
|
151
|
+
async removeInstance(instanceId: string): Promise<void> {
|
|
152
|
+
const dir = this.instanceDir(instanceId)
|
|
153
|
+
if (!existsSync(dir)) return
|
|
154
|
+
await rm(dir, { recursive: true, force: true })
|
|
155
|
+
this.logger.info('Removed instance directory', { instanceId, dir })
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private async isLinkedWorktree(repoDir: string, worktreePath: string): Promise<boolean> {
|
|
159
|
+
try {
|
|
160
|
+
const stdout = await this.git(['worktree', 'list', '--porcelain'], { cwd: repoDir })
|
|
161
|
+
// `worktree` lines look like: `worktree /abs/path` (one per registration).
|
|
162
|
+
return stdout
|
|
163
|
+
.split('\n')
|
|
164
|
+
.some((line) => line.startsWith('worktree ') && line.slice('worktree '.length) === worktreePath)
|
|
165
|
+
} catch {
|
|
166
|
+
return false
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private async git(
|
|
171
|
+
args: string[],
|
|
172
|
+
opts: { cwd?: string; input?: string; env?: Record<string, string> } = {},
|
|
173
|
+
): Promise<string> {
|
|
174
|
+
const env = opts.env ? { ...process.env, ...opts.env } : process.env
|
|
175
|
+
const child = execFileAsync('git', args, { cwd: opts.cwd, env })
|
|
176
|
+
if (opts.input !== undefined) {
|
|
177
|
+
child.child.stdin?.end(opts.input)
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const { stdout } = await child
|
|
181
|
+
return stdout
|
|
182
|
+
} catch (err) {
|
|
183
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
184
|
+
throw new Error(`git ${args.join(' ')} failed: ${message}`)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|