@kortix/sandbox 0.4.1 → 0.4.3

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.
@@ -0,0 +1,279 @@
1
+ # Freestyle Serverless Deployments - Full API Reference
2
+
3
+ Complete reference for `freestyle.serverless.deployments.create()`.
4
+
5
+ ## Import
6
+
7
+ ```javascript
8
+ import { freestyle, readFiles } from "freestyle-sandboxes"; // readFiles for local file deploys
9
+ ```
10
+
11
+ Requires `FREESTYLE_API_KEY` environment variable.
12
+
13
+ ## Source Types (exactly one required)
14
+
15
+ ### Git Repository
16
+
17
+ ```typescript
18
+ {
19
+ repo: "https://github.com/user/repo", // public URL, authenticated URL, or freestyle repo ID
20
+ branch: "main", // optional, defaults to default branch
21
+ rootPath: "./apps/web", // optional, for monorepos
22
+ }
23
+ ```
24
+
25
+ Private repos: use `https://user:token@github.com/user/repo.git` or a Freestyle Git repo ID.
26
+
27
+ ### Inline Code
28
+
29
+ ```typescript
30
+ {
31
+ code: `
32
+ import express from 'express';
33
+ const app = express();
34
+ app.get('/', (req, res) => res.send('Hello'));
35
+ app.listen(3000);
36
+ `,
37
+ nodeModules: { // required with code deploys
38
+ express: "^4.18.2",
39
+ },
40
+ }
41
+ ```
42
+
43
+ ### Local Files (readFiles)
44
+
45
+ ```javascript
46
+ import { readFiles } from "freestyle-sandboxes"; // NOT from /utils
47
+
48
+ const files = await readFiles("./dist"); // reads dir, excludes node_modules, base64-encodes
49
+ {
50
+ files,
51
+ entrypointPath: "server.js", // required for file deploys - MUST be a Node.js server, not an HTML file
52
+ }
53
+ ```
54
+
55
+ `readFiles` automatically excludes `node_modules/` and handles binary files. The `entrypointPath` MUST point to a Node.js server file (e.g., Express/Hono) — pointing it at an HTML file will not serve sub-assets.
56
+
57
+ ### Tar URL
58
+
59
+ ```typescript
60
+ {
61
+ tarUrl: "https://s3.example.com/signed-url/app.tar.gz",
62
+ }
63
+ ```
64
+
65
+ Useful when source is hosted externally (S3, GCS, etc).
66
+
67
+ ## Options
68
+
69
+ ### domains (required)
70
+
71
+ ```typescript
72
+ domains: ["my-app.style.dev"] // free *.style.dev subdomain
73
+ domains: ["app.style.dev", "app.yourdomain.com"] // multiple domains
74
+ ```
75
+
76
+ Any unclaimed `*.style.dev` subdomain works instantly with SSL. Custom domains need verification first.
77
+
78
+ ### build
79
+
80
+ ```typescript
81
+ build: true // auto-detect framework, run build
82
+ build: {
83
+ command: "npm run build", // custom build command
84
+ outDir: "dist", // build output directory
85
+ envVars: { // build-time env vars (NOT available at runtime)
86
+ NEXT_PUBLIC_API_URL: "https://api.example.com",
87
+ NODE_ENV: "production",
88
+ },
89
+ }
90
+ ```
91
+
92
+ Default: `false` (deploy files as-is). Set `true` for Next.js, Vite, and other frameworks requiring a build step. Freestyle auto-detects Next.js, Vite, and Expo.
93
+
94
+ ### entrypoint
95
+
96
+ ```typescript
97
+ entrypoint: "server.js" // main file of your application
98
+ ```
99
+
100
+ Auto-detected for Next.js, Vite, Expo. Specify manually for custom setups.
101
+
102
+ ### envVars
103
+
104
+ ```typescript
105
+ envVars: {
106
+ API_KEY: "secret-value", // available at RUNTIME only
107
+ DATABASE_URL: "postgres://...",
108
+ }
109
+ ```
110
+
111
+ NOT available at build time. For build-time vars, use `build.envVars`. Env vars are tied to deployments; to change them, create a new deployment.
112
+
113
+ ### nodeModules
114
+
115
+ ```typescript
116
+ nodeModules: {
117
+ express: "^4.18.2",
118
+ cors: "^2.8.5",
119
+ }
120
+ ```
121
+
122
+ Only needed for `code` deploys. For git/file deploys, include your lockfile (`package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, or `bun.lock`) and Freestyle installs from it.
123
+
124
+ ### timeoutMs
125
+
126
+ ```typescript
127
+ timeoutMs: 60000 // milliseconds, idle timeout before scale-down
128
+ ```
129
+
130
+ Timeout is from last TCP packet (not HTTP request), so WebSockets stay alive as long as you ping faster than the timeout.
131
+
132
+ ### networkPermissions
133
+
134
+ ```typescript
135
+ networkPermissions: [
136
+ { action: "allow", domain: "api.stripe.com", behavior: "exact" },
137
+ { action: "allow", domain: ".*\\.amazonaws\\.com", behavior: "regex" },
138
+ ]
139
+ ```
140
+
141
+ ### await
142
+
143
+ ```typescript
144
+ await: false // return immediately with deploymentId for polling
145
+ ```
146
+
147
+ Default: `true` (waits for deployment to build and propagate). Set `false` to return immediately.
148
+
149
+ ```typescript
150
+ // Polling pattern
151
+ const { deploymentId } = await freestyle.serverless.deployments.create({
152
+ ...,
153
+ await: false,
154
+ });
155
+ const status = await freestyle.serverless.deployments.get({ deploymentId });
156
+ ```
157
+
158
+ ### waitForRollout
159
+
160
+ ```typescript
161
+ waitForRollout: true // wait until deployment is fully serving traffic
162
+ ```
163
+
164
+ ### staticOnly
165
+
166
+ ```typescript
167
+ staticOnly: true,
168
+ publicDir: "dist", // directory with static files
169
+ cleanUrls: true, // /about.html becomes /about
170
+ ```
171
+
172
+ Serves files directly without a server entrypoint. For pure static sites.
173
+
174
+ ### headers
175
+
176
+ ```typescript
177
+ headers: [
178
+ {
179
+ source: "^/assets/.*$",
180
+ headers: [{ key: "Cache-Control", value: "max-age=31536000, immutable" }],
181
+ },
182
+ ]
183
+ ```
184
+
185
+ ### redirects
186
+
187
+ ```typescript
188
+ redirects: [
189
+ { source: "^/old-page$", destination: "/new-page", permanent: true },
190
+ ]
191
+ ```
192
+
193
+ ## Return Value
194
+
195
+ ```typescript
196
+ const { deployment, domains } = await freestyle.serverless.deployments.create({...});
197
+
198
+ deployment.deploymentId // string - unique deployment ID
199
+ domains // string[] - live URLs
200
+
201
+ // With await: false
202
+ const { deploymentId } = await freestyle.serverless.deployments.create({..., await: false});
203
+ ```
204
+
205
+ ## Custom Domains
206
+
207
+ ### Step 1: Verify ownership
208
+
209
+ ```typescript
210
+ const { record, instructions } = await freestyle.domains.verifications.create({
211
+ domain: "example.com",
212
+ });
213
+ // Add TXT record: _freestyle_custom_hostname.example.com → <verification-code>
214
+ ```
215
+
216
+ ### Step 2: Complete verification
217
+
218
+ ```typescript
219
+ await freestyle.domains.verifications.complete({ domain: "example.com" });
220
+ ```
221
+
222
+ ### Step 3: Configure DNS
223
+
224
+ Point domain to Freestyle:
225
+ - **APEX** (`example.com`): A record → `35.235.84.134`
226
+ - **Subdomain** (`app.example.com`): A record for `app` → `35.235.84.134`
227
+ - **Wildcard** (`*.example.com`): A record for `*` → `35.235.84.134`
228
+
229
+ ### Step 4: Deploy
230
+
231
+ ```typescript
232
+ await freestyle.serverless.deployments.create({
233
+ ...,
234
+ domains: ["example.com"],
235
+ });
236
+ ```
237
+
238
+ ## Framework Notes
239
+
240
+ ### Next.js
241
+
242
+ Requires in `next.config.mjs`:
243
+ ```javascript
244
+ const nextConfig = {
245
+ output: "standalone", // required
246
+ images: { unoptimized: true }, // required (no Sharp on Freestyle)
247
+ };
248
+ export default nextConfig;
249
+ ```
250
+
251
+ For local file deploys, after `npm run build`:
252
+ ```bash
253
+ cp -r public .next/standalone/public
254
+ cp -r .next/static .next/standalone/.next/static
255
+ cp package-lock.json .next/standalone/package-lock.json
256
+ ```
257
+ Then `readFiles(".next/standalone")` with `entrypointPath: "server.js"`.
258
+
259
+ ### Vite
260
+
261
+ Auto-detected when deploying from git with `build: true`. For local file deploys, use an Express static server:
262
+
263
+ ```javascript
264
+ import express from 'express';
265
+ import { fileURLToPath } from 'url';
266
+ import { dirname, join } from 'path';
267
+
268
+ const __dirname = dirname(fileURLToPath(import.meta.url));
269
+ const app = express();
270
+ app.use(express.static(__dirname));
271
+ app.get('*', (req, res) => res.sendFile(join(__dirname, 'index.html'))); // SPA fallback
272
+ app.listen(3000);
273
+ ```
274
+
275
+ > **WARNING**: Do NOT use `Deno.serve()` or Hono's `app.fire()`. Freestyle runs Node.js. Use `app.listen(3000)` (Express) or `serve({ fetch: app.fetch, port: 3000 })` from `@hono/node-server`.
276
+
277
+ ### Static Sites
278
+
279
+ Static sites MUST have an Express/Hono Node.js server entrypoint. Use the same Express pattern as Vite above.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kortix/sandbox",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Kortix sandbox runtime — kortix-master, opencode config/agents/skills, and dependencies",
5
5
  "private": false,
6
6
  "scripts": {
@@ -1,9 +1,16 @@
1
1
  const fs = require('fs');
2
+ const { execSync } = require('child_process');
3
+
4
+ // Auto-detect npm global modules directory
5
+ const npmGlobalRoot = execSync('npm root -g').toString().trim();
6
+ console.log('npm global root:', npmGlobalRoot);
7
+
8
+ const agentBrowserDir = npmGlobalRoot + '/agent-browser/dist';
2
9
 
3
10
  // ── Patch 1: handleLaunch env var fallbacks ─────────────────────────────────
4
11
  // The Rust CLI sends a launch command to the Node.js daemon but doesn't
5
12
  // include executablePath/args/profile in the JSON. We inject env var fallbacks.
6
- const actionsFile = '/usr/lib/node_modules/agent-browser/dist/actions.js';
13
+ const actionsFile = agentBrowserDir + '/actions.js';
7
14
  let actions = fs.readFileSync(actionsFile, 'utf8');
8
15
 
9
16
  const oldLaunch = [
@@ -44,7 +51,7 @@ console.log('PATCH 2 SKIPPED - upstream already allows localhost origins');
44
51
  // When AGENT_BROWSER_STREAM_PORT is set globally, ALL sessions try to bind
45
52
  // that port. Named sessions crash with EADDRINUSE. Fix: only the default
46
53
  // session uses the env var port; named sessions use the hash-based port.
47
- const daemonFile = '/usr/lib/node_modules/agent-browser/dist/daemon.js';
54
+ const daemonFile = agentBrowserDir + '/daemon.js';
48
55
  let daemon = fs.readFileSync(daemonFile, 'utf8');
49
56
 
50
57
  const oldStreamPort = `const streamPort = options?.streamPort ??
@@ -85,7 +92,7 @@ console.log('PATCH 4 OK - auto-launch profile only for default session');
85
92
  // When using launchPersistentContext (profile mode), this.browser is null.
86
93
  // newTab() checks `!this.browser` and throws "Browser not launched".
87
94
  // Fix: also check this.contexts.length > 0 as an alternative.
88
- const browserFile = '/usr/lib/node_modules/agent-browser/dist/browser.js';
95
+ const browserFile = agentBrowserDir + '/browser.js';
89
96
  let browser = fs.readFileSync(browserFile, 'utf8');
90
97
 
91
98
  const oldNewTabCheck = "if (!this.browser || this.contexts.length === 0) {\n throw new Error('Browser not launched');\n }\n // Invalidate CDP session since we're switching to a new page";
@@ -1,37 +0,0 @@
1
- #!/usr/bin/with-contenv bash
2
- # KORTIX Presentation Viewer — s6 service
3
- #
4
- # Watches for presentations in /workspace/presentations/ and serves
5
- # the most recently updated one on port 3210.
6
- # If no presentation exists yet, waits until one appears.
7
-
8
- PRES_ROOT="/workspace/presentations"
9
- VIEWER="/opt/opencode/skills/KORTIX-presentation-viewer/serve.ts"
10
- PORT=3210
11
-
12
- wait_for_presentation() {
13
- while true; do
14
- if [ -d "$PRES_ROOT" ]; then
15
- LATEST=$(find "$PRES_ROOT" -maxdepth 2 -name "metadata.json" -newer /tmp/.pv-last-check 2>/dev/null | head -1)
16
- if [ -z "$LATEST" ]; then
17
- LATEST=$(find "$PRES_ROOT" -maxdepth 2 -name "metadata.json" 2>/dev/null | head -1)
18
- fi
19
- if [ -n "$LATEST" ]; then
20
- dirname "$LATEST"
21
- return 0
22
- fi
23
- fi
24
- sleep 3
25
- done
26
- }
27
-
28
- touch /tmp/.pv-last-check
29
-
30
- echo "[KORTIX-pv] Waiting for a presentation in $PRES_ROOT ..."
31
- PRES_DIR=$(wait_for_presentation)
32
- echo "[KORTIX-pv] Serving: $PRES_DIR on port $PORT"
33
-
34
- touch /tmp/.pv-last-check
35
-
36
- exec s6-setuidgid abc \
37
- /usr/local/bin/bun run "$VIEWER" "$PRES_DIR"
@@ -1,48 +0,0 @@
1
- #!/usr/bin/with-contenv bash
2
- # Agent Browser Viewer — s6 service
3
- #
4
- # Serves the browser viewer UI on port 9224.
5
- # - GET / → viewer HTML
6
- # - GET /sessions → JSON list of active sessions + stream ports
7
-
8
- echo "[agent-browser-viewer] Starting viewer on port 9224"
9
-
10
- exec s6-setuidgid abc \
11
- node -e '
12
- const http = require("http");
13
- const fs = require("fs");
14
- const path = require("path");
15
-
16
- const html = fs.readFileSync("/opt/agent-browser-viewer/index.html");
17
- const socketDir = process.env.AGENT_BROWSER_SOCKET_DIR || "/workspace/.agent-browser";
18
-
19
- function getSessions() {
20
- try {
21
- const files = fs.readdirSync(socketDir);
22
- const sessions = [];
23
- for (const f of files) {
24
- if (f.endsWith(".stream")) {
25
- const name = f.replace(".stream", "");
26
- const port = parseInt(fs.readFileSync(path.join(socketDir, f), "utf8").trim(), 10);
27
- const hasSock = files.includes(name + ".sock");
28
- if (port > 0 && hasSock) {
29
- sessions.push({ name, port });
30
- }
31
- }
32
- }
33
- return sessions;
34
- } catch(e) { return []; }
35
- }
36
-
37
- http.createServer((req, res) => {
38
- if (req.url === "/sessions") {
39
- res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-cache" });
40
- res.end(JSON.stringify(getSessions()));
41
- } else {
42
- res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-cache" });
43
- res.end(html);
44
- }
45
- }).listen(9224, "0.0.0.0", () => {
46
- console.log("[agent-browser-viewer] Ready at http://0.0.0.0:9224");
47
- });
48
- '
@@ -1,16 +0,0 @@
1
- #!/usr/bin/with-contenv bash
2
- # Kortix Master — s6 service
3
- #
4
- # Reverse proxy that sits in front of OpenCode on port 8000.
5
- # All external traffic enters through Kortix Master, which proxies
6
- # to the OpenCode API server on localhost:4096.
7
-
8
- echo "[kortix-master] Starting on port 8000, proxying to OpenCode at localhost:4096"
9
-
10
- exec s6-setuidgid abc \
11
- env HOME=/workspace \
12
- KORTIX_MASTER_PORT=8000 \
13
- OPENCODE_HOST=localhost \
14
- OPENCODE_PORT=4096 \
15
- PATH="/opt/bun/bin:/usr/local/bin:/usr/bin:/bin" \
16
- /opt/bun/bin/bun run /opt/kortix-master/src/index.ts
@@ -1,22 +0,0 @@
1
- #!/usr/bin/with-contenv bash
2
- # lss-sync — File-watcher daemon that keeps the semantic search index in sync.
3
- #
4
- # Uses watchdog (inotify/FSEvents) to detect file changes in real time and
5
- # triggers re-indexing with debounce. Changes are picked up within seconds.
6
- #
7
- # Watches:
8
- # /workspace — User files and projects
9
- # /workspace/.kortix — Agent memory
10
-
11
- exec s6-setuidgid abc \
12
- env HOME=/workspace \
13
- OPENAI_API_KEY="${OPENAI_API_KEY}" \
14
- LSS_DIR=/workspace/.lss \
15
- PATH="/lsiopy/bin:/usr/local/bin:/usr/bin:/bin" \
16
- lss-sync \
17
- --watch /workspace \
18
- --exclude node_modules \
19
- --exclude .git \
20
- --exclude __pycache__ \
21
- --startup-delay 15 \
22
- --debounce 2
@@ -1,25 +0,0 @@
1
- #!/usr/bin/with-contenv bash
2
- # OpenCode API Server — s6 service
3
- #
4
- # Runs `opencode serve` on port 4096 (internal only, proxied by kortix-master).
5
- # This is the headless API server, distinct from `opencode web` (the Web UI).
6
-
7
- # Wait a moment for filesystem to settle
8
- sleep 3
9
-
10
- cd /workspace
11
-
12
- # Ensure OpenCode data dirs exist and are owned by abc (guards against
13
- # Daytona race conditions where /workspace ownership may shift after startup.sh)
14
- mkdir -p /workspace/.local/share/opencode/log \
15
- /workspace/.local/share/opencode/storage \
16
- /workspace/.local/share/opencode/snapshot
17
- chown -R abc:abc /workspace/.local/share/opencode
18
-
19
- echo "[opencode-serve] Starting OpenCode API server on port 4096"
20
-
21
- exec s6-setuidgid abc \
22
- env HOME=/workspace \
23
- OPENCODE_CONFIG_DIR=/opt/opencode \
24
- PATH="/opt/bun/bin:/usr/local/bin:/usr/bin:/bin" \
25
- opencode serve --port 4096 --hostname 0.0.0.0
@@ -1,21 +0,0 @@
1
- #!/usr/bin/with-contenv bash
2
- # OpenCode Web UI — serves the web interface on port 3111
3
- #
4
- # Runs `opencode web` headless so it's accessible via the browser at
5
- # http://localhost:3111 (mapped from the container).
6
-
7
- cd /workspace
8
-
9
- # Ensure OpenCode data dirs exist and are owned by abc
10
- mkdir -p /workspace/.local/share/opencode/log \
11
- /workspace/.local/share/opencode/storage \
12
- /workspace/.local/share/opencode/snapshot
13
- chown -R abc:abc /workspace/.local/share/opencode
14
-
15
- exec s6-setuidgid abc \
16
- env HOME=/workspace \
17
- OPENCODE_CONFIG_DIR=/opt/opencode \
18
- OPENCODE_SERVER_USERNAME= \
19
- OPENCODE_SERVER_PASSWORD= \
20
- PATH="/opt/bun/bin:/usr/local/bin:/usr/bin:/bin" \
21
- opencode web --port 3111 --hostname 0.0.0.0