glovebox-core 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +7 -0
- package/README.md +272 -0
- package/dist/chunk-7EPHMP7S.js +43 -0
- package/dist/chunk-ACURXCF6.js +0 -0
- package/dist/chunk-LPP37JKX.js +24 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +422 -0
- package/dist/config.d.ts +88 -0
- package/dist/config.js +10 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +48 -0
- package/dist/protocol.d.ts +181 -0
- package/dist/protocol.js +1 -0
- package/dist/storage.d.ts +48 -0
- package/dist/storage.js +8 -0
- package/package.json +60 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2026 dterminal
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# glovebox-core
|
|
2
|
+
|
|
3
|
+
Authoring kit and `glovebox` build CLI for shipping a [Glove](https://github.com/porkytheblack/glove) agent as a sandboxed, network-addressable service. Wrap a built `Glove` runnable, run `glovebox build`, ship the resulting Dockerfile (or nixpacks bundle) to any container host.
|
|
4
|
+
|
|
5
|
+
> The package is named `glovebox-core` on npm because the unscoped `glovebox` slot is taken by another project. The CLI binary it installs is still called `glovebox`, and the value you import from it is still called `glovebox` — only the install name differs.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add glovebox-core glovebox-kit
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`glovebox-kit` is the in-container runtime; `glovebox build` bakes it into the generated server bundle, so it must resolve at install time. The `glovebox` binary is installed into your project's `node_modules/.bin`.
|
|
14
|
+
|
|
15
|
+
## What it does
|
|
16
|
+
|
|
17
|
+
A normal `Glove` agent runs in-process: tools, displays, and storage all live wherever the host process happens to live. Glovebox packages that same agent as a long-running container that exposes one authenticated WebSocket endpoint per session. The agent gets:
|
|
18
|
+
|
|
19
|
+
- A clean `/work`, `/input`, `/output` filesystem layout, owned by an unprivileged `glovebox` user.
|
|
20
|
+
- A pinned base image with the system tools the agent declares (ffmpeg, pandoc, Playwright, ...).
|
|
21
|
+
- A `FileRef`-based wire protocol so caller files cross the network as references (inline / URL / server-hosted / S3), never raw blobs over WS frames.
|
|
22
|
+
- An auto-injected `environment` skill, `workspace` skill, `/output` hook, and `/clear-workspace` hook.
|
|
23
|
+
|
|
24
|
+
The build CLI emits a Dockerfile, a `nixpacks.toml` (Railway-flavored alternative), an esbuild server bundle (~150 KB), a manifest, and a generated bearer key. No app code runs at build time besides the entry import that hands over the wrapped runnable.
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
### 1. Wrap a built runnable
|
|
29
|
+
|
|
30
|
+
`glovebox.wrap(runnable, config)` takes any object that satisfies `IGloveRunnable` (the result of `Glove.build()`) and returns an opaque `GloveboxApp`. The build CLI imports your entry file, reads `default` (or named `app`) from it, and consumes the wrap.
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// glovebox.ts — the entry the build CLI compiles
|
|
34
|
+
import { Glove } from "glove-core/glove"
|
|
35
|
+
import { Displaymanager } from "glove-core/display-manager"
|
|
36
|
+
import { SqliteStore } from "glove-core"
|
|
37
|
+
import { createAdapter } from "glove-core/models/providers"
|
|
38
|
+
import { z } from "zod"
|
|
39
|
+
import { execFile } from "node:child_process"
|
|
40
|
+
import { promisify } from "node:util"
|
|
41
|
+
import path from "node:path"
|
|
42
|
+
|
|
43
|
+
import { glovebox, rule, composite } from "glovebox-core"
|
|
44
|
+
|
|
45
|
+
const exec = promisify(execFile)
|
|
46
|
+
|
|
47
|
+
const agent = new Glove({
|
|
48
|
+
store: new SqliteStore({ dbPath: "/work/glove.db", sessionId: "trim" }),
|
|
49
|
+
model: createAdapter({ provider: "anthropic", model: "claude-sonnet-4-20250514" }),
|
|
50
|
+
displayManager: new Displaymanager(),
|
|
51
|
+
systemPrompt:
|
|
52
|
+
"You trim media files. Inputs land in /input, write trimmed files to /output.",
|
|
53
|
+
compaction_config: { compaction_instructions: "Summarize the conversation." },
|
|
54
|
+
serverMode: true,
|
|
55
|
+
})
|
|
56
|
+
.fold({
|
|
57
|
+
name: "trim",
|
|
58
|
+
description: "Trim a media file with ffmpeg.",
|
|
59
|
+
inputSchema: z.object({
|
|
60
|
+
file: z.string().describe("Filename in /input"),
|
|
61
|
+
start: z.string(),
|
|
62
|
+
duration: z.string(),
|
|
63
|
+
}),
|
|
64
|
+
async do(input) {
|
|
65
|
+
const out = path.join("/output", `trimmed-${input.file}`)
|
|
66
|
+
await exec("ffmpeg", [
|
|
67
|
+
"-y", "-i", path.join("/input", input.file),
|
|
68
|
+
"-ss", input.start, "-t", input.duration, "-c", "copy", out,
|
|
69
|
+
])
|
|
70
|
+
return { status: "success" as const, data: { out } }
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
.build()
|
|
74
|
+
|
|
75
|
+
export default glovebox.wrap(agent, {
|
|
76
|
+
name: "media-trimmer",
|
|
77
|
+
base: "glovebox/media",
|
|
78
|
+
packages: { apt: ["ffmpeg"] },
|
|
79
|
+
env: {
|
|
80
|
+
ANTHROPIC_API_KEY: { required: true, secret: true },
|
|
81
|
+
},
|
|
82
|
+
storage: {
|
|
83
|
+
inputs: composite([rule.url(), rule.inline()]),
|
|
84
|
+
outputs: composite([
|
|
85
|
+
rule.inline({ below: "1MB" }),
|
|
86
|
+
rule.localServer({ ttl: "1h" }),
|
|
87
|
+
]),
|
|
88
|
+
},
|
|
89
|
+
limits: { memory: "1Gi", timeout: "5m" },
|
|
90
|
+
})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 2. Build
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
glovebox build ./glovebox.ts
|
|
97
|
+
# → ✓ Resolved base image: ghcr.io/porkytheblack/glovebox/media:1.4
|
|
98
|
+
# ✓ Generated Dockerfile / nixpacks.toml / server bundle / auth key
|
|
99
|
+
# ✓ Wrote dist/
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`dist/` contains:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
dist/
|
|
106
|
+
├── Dockerfile # FROMs the resolved base, copies bundle
|
|
107
|
+
├── nixpacks.toml # Railway-style alternative
|
|
108
|
+
├── glovebox.json # manifest (env spec, fs layout, key fingerprint, storage policy)
|
|
109
|
+
├── glovebox.key # generated bearer (gitignored, 0600)
|
|
110
|
+
├── .env.example # filled from `env` config
|
|
111
|
+
└── server/
|
|
112
|
+
├── index.js # esbuild bundle (~150 KB)
|
|
113
|
+
├── package.json # only better-sqlite3 as runtime dep
|
|
114
|
+
└── glovebox.json # copy for runtime
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 3. Deploy
|
|
118
|
+
|
|
119
|
+
Anywhere that runs a container or honors nixpacks. The bearer key the build emitted is the one and only credential.
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
docker build -t my-trimmer ./dist
|
|
123
|
+
docker run -p 8080:8080 \
|
|
124
|
+
-e GLOVEBOX_KEY="$(cat ./dist/glovebox.key)" \
|
|
125
|
+
-e GLOVEBOX_PUBLIC_URL=https://trimmer.example.com \
|
|
126
|
+
-e ANTHROPIC_API_KEY=sk-ant-... \
|
|
127
|
+
my-trimmer
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Railway / Render / Fly: point the platform at `dist/nixpacks.toml`, set the same env vars, ship.
|
|
131
|
+
|
|
132
|
+
## Config reference
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
interface GloveboxConfig {
|
|
136
|
+
name?: string // defaults to "glovebox-app"
|
|
137
|
+
version?: string // defaults to "0.1.0"
|
|
138
|
+
base?: BaseImage // defaults to "glovebox/base"
|
|
139
|
+
packages?: { apt?: string[]; pip?: string[]; npm?: string[] }
|
|
140
|
+
fs?: Record<string, { path: string; writable: boolean }>
|
|
141
|
+
env?: Record<string, EnvVarSpec>
|
|
142
|
+
storage?: { inputs?: StoragePolicy; outputs?: StoragePolicy }
|
|
143
|
+
limits?: { cpu?: string; memory?: string; timeout?: string }
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Base images
|
|
148
|
+
|
|
149
|
+
| `base` | What's in it | Tag |
|
|
150
|
+
|--------|--------------|-----|
|
|
151
|
+
| `glovebox/base` | Node 20 + glovebox user + standard fs layout | `1.0` |
|
|
152
|
+
| `glovebox/media` | base + ffmpeg, imagemagick, sox, yt-dlp | `1.4` |
|
|
153
|
+
| `glovebox/docs` | base + pandoc, qpdf, pdftk-java, libreoffice headless | `1.2` |
|
|
154
|
+
| `glovebox/python` | base + uv + scientific stack | `1.3` |
|
|
155
|
+
| `glovebox/browser` | base + Playwright + Chromium | `1.1` |
|
|
156
|
+
|
|
157
|
+
Images are pulled from `ghcr.io/porkytheblack/glovebox/<name>:<tag>`. Override the registry at build time:
|
|
158
|
+
|
|
159
|
+
```sh
|
|
160
|
+
GLOVEBOX_REGISTRY=registry.my-corp.dev/glovebox glovebox build ./glovebox.ts
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
A custom `base` like `quay.io/me/img:tag` is passed through verbatim — the per-app Dockerfile then provisions the user, layout, and prebuilt `better-sqlite3` itself.
|
|
164
|
+
|
|
165
|
+
### Filesystem
|
|
166
|
+
|
|
167
|
+
Defaults — override per-mount via `fs`:
|
|
168
|
+
|
|
169
|
+
| Name | Path | Writable |
|
|
170
|
+
|------|------|----------|
|
|
171
|
+
| `work` | `/work` | yes |
|
|
172
|
+
| `input` | `/input` | no (mounted RO at runtime) |
|
|
173
|
+
| `output` | `/output` | yes (swept on `/clear-workspace`) |
|
|
174
|
+
|
|
175
|
+
The agent receives an environment block referencing these paths plus a `workspace` skill it can call to list current contents.
|
|
176
|
+
|
|
177
|
+
### Storage policy DSL
|
|
178
|
+
|
|
179
|
+
Inputs and outputs are independent ordered lists of `{ use, when }` rules. Earlier rules win. Build them with `rule.*` + `composite`:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import { rule, composite } from "glovebox-core"
|
|
183
|
+
|
|
184
|
+
storage: {
|
|
185
|
+
// Caller can pass URL refs the server fetches; otherwise inline base64.
|
|
186
|
+
inputs: composite([rule.url(), rule.inline()]),
|
|
187
|
+
|
|
188
|
+
// Outputs ≤ 1MB ride back inline; everything else is parked on the server
|
|
189
|
+
// for an hour and the client picks it up over the authenticated /files route.
|
|
190
|
+
outputs: composite([
|
|
191
|
+
rule.inline({ below: "1MB" }),
|
|
192
|
+
rule.localServer({ ttl: "1h" }),
|
|
193
|
+
]),
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
| Rule | Options | Notes |
|
|
198
|
+
|------|---------|-------|
|
|
199
|
+
| `rule.inline({ below?, above? })` | size bounds | base64 in the WS frame; fine for KB-scale |
|
|
200
|
+
| `rule.localServer({ ttl?, below?, above? })` | `ttl` defaults to `1h` | server-hosted via `GET /files/:id`; backed by sqlite + sweeper |
|
|
201
|
+
| `rule.url({ below?, above? })` | none | inputs only — read-only adapter for caller URLs |
|
|
202
|
+
| `rule.s3({ bucket, region?, prefix?, ... })` | bucket required | requires registering an `S3Storage` adapter in your wrap module's `adapters` export (see `glovebox-kit`) |
|
|
203
|
+
|
|
204
|
+
`composite([])` throws — every policy needs at least one rule. The kit additionally rejects an outputs policy at boot if it has no terminal rule (`always: true` or `default: true`) or if any referenced adapter isn't registered.
|
|
205
|
+
|
|
206
|
+
A per-prompt override is allowed on the wire (`outputs_policy` on `ClientPromptMessage`) and merges in front of the configured policy — useful when one specific call needs a different parking spot.
|
|
207
|
+
|
|
208
|
+
### Env vars
|
|
209
|
+
|
|
210
|
+
Declared variables show up in `.env.example` and are validated on container boot (`required: true` ones throw if unset). The runtime always reads:
|
|
211
|
+
|
|
212
|
+
| Variable | Required | Default | Meaning |
|
|
213
|
+
|----------|----------|---------|---------|
|
|
214
|
+
| `GLOVEBOX_KEY` | yes | — | Bearer key matching `key_fingerprint` in `glovebox.json` |
|
|
215
|
+
| `GLOVEBOX_PORT` | no | `8080` | HTTP/WS listen port |
|
|
216
|
+
| `GLOVEBOX_PUBLIC_URL` | no | `http://localhost:<port>` | Used to mint `server` FileRefs the client can reach |
|
|
217
|
+
|
|
218
|
+
### Limits
|
|
219
|
+
|
|
220
|
+
Surfaced verbatim through the `environment` skill so the agent can self-throttle. Glovebox does not enforce them — your container runtime does.
|
|
221
|
+
|
|
222
|
+
## Manifest (`glovebox.json`)
|
|
223
|
+
|
|
224
|
+
Static description of the deployed app. The kit verifies the bearer matches `key_fingerprint` on boot and rejects mismatches before opening the listener. The manifest is also copied into `server/` so the runtime resolves it via `import.meta.url`.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
interface Manifest {
|
|
228
|
+
name: string
|
|
229
|
+
version: string
|
|
230
|
+
base: string
|
|
231
|
+
fs: Record<string, { path: string; writable: boolean }>
|
|
232
|
+
env: Record<string, ManifestEnvVar>
|
|
233
|
+
limits?: { cpu?: string; memory?: string; timeout?: string }
|
|
234
|
+
key_fingerprint: string // sha256 prefix "abcd1234...wxyz"
|
|
235
|
+
storage_policy: { inputs: StoragePolicyEncoded; outputs: StoragePolicyEncoded }
|
|
236
|
+
packages: { apt?: string[]; pip?: string[]; npm?: string[] }
|
|
237
|
+
protocol_version: 1
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Wire protocol
|
|
242
|
+
|
|
243
|
+
One WebSocket per client session. Multiple prompts multiplex via `id`. The full type set lives at `glovebox/protocol`. Client → server: `prompt | abort | display_resolve | display_reject | ping`. Server → client: `event | display_push | display_clear | complete | error | pong`.
|
|
244
|
+
|
|
245
|
+
Files cross the wire as `FileRef` (`inline | url | server | s3 | gcs`) — never raw bytes. The server's storage adapter for the chosen kind is the one and only thing that touches the byte stream.
|
|
246
|
+
|
|
247
|
+
## CLI
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
glovebox build <entry> [--out <dir>] [--name <name>]
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
`<entry>` is the path to your wrap module. `--out` defaults to `<entry-dir>/dist`. `--name` overrides the manifest name without rebuilding the entry.
|
|
254
|
+
|
|
255
|
+
## Status
|
|
256
|
+
|
|
257
|
+
v1. Prompts within a session are serialized — Glove's `PromptMachine` is not safe to invoke concurrently, so the kit chains them. Bearer auth on the WebSocket upgrade; no JWT yet. GCS and Azure storage adapters are deferred to v2 along with multiplexed prompt execution, hot-reload of the wrap module, and the hosted glovebox.dev tier.
|
|
258
|
+
|
|
259
|
+
## Companion packages
|
|
260
|
+
|
|
261
|
+
- **[`glovebox-kit`](../glovebox-kit/README.md)** — the in-container runtime that the generated server bundle imports. Register custom storage adapters here.
|
|
262
|
+
- **[`glovebox-client`](../glovebox-client/README.md)** — client SDK for talking to a deployed glovebox.
|
|
263
|
+
|
|
264
|
+
## Documentation
|
|
265
|
+
|
|
266
|
+
- [Glovebox Guide](https://glove.dterminal.net/docs/glovebox)
|
|
267
|
+
- [Getting Started](https://glove.dterminal.net/docs/getting-started)
|
|
268
|
+
- [Full Documentation](https://glove.dterminal.net)
|
|
269
|
+
|
|
270
|
+
## License
|
|
271
|
+
|
|
272
|
+
MIT
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// src/storage.ts
|
|
2
|
+
function whenFromBounds(opts, fallbackDefault) {
|
|
3
|
+
const when = {};
|
|
4
|
+
if (opts?.above) when.sizeAbove = opts.above;
|
|
5
|
+
if (opts?.below) when.sizeBelow = opts.below;
|
|
6
|
+
if (!when.sizeAbove && !when.sizeBelow) {
|
|
7
|
+
if (fallbackDefault) when.default = true;
|
|
8
|
+
else when.always = true;
|
|
9
|
+
}
|
|
10
|
+
return when;
|
|
11
|
+
}
|
|
12
|
+
var rule = {
|
|
13
|
+
inline: (opts) => ({
|
|
14
|
+
use: { adapter: "inline" },
|
|
15
|
+
when: whenFromBounds(opts, true)
|
|
16
|
+
}),
|
|
17
|
+
localServer: (opts) => ({
|
|
18
|
+
use: { adapter: "localServer", options: { ttl: opts?.ttl ?? "1h" } },
|
|
19
|
+
when: whenFromBounds(opts, true)
|
|
20
|
+
}),
|
|
21
|
+
s3: (opts) => ({
|
|
22
|
+
use: {
|
|
23
|
+
adapter: "s3",
|
|
24
|
+
options: { bucket: opts.bucket, region: opts.region, prefix: opts.prefix }
|
|
25
|
+
},
|
|
26
|
+
when: whenFromBounds(opts, false)
|
|
27
|
+
}),
|
|
28
|
+
url: (opts) => ({
|
|
29
|
+
use: { adapter: "url" },
|
|
30
|
+
when: whenFromBounds(opts, false)
|
|
31
|
+
})
|
|
32
|
+
};
|
|
33
|
+
function composite(rules) {
|
|
34
|
+
if (rules.length === 0) {
|
|
35
|
+
throw new Error("composite() requires at least one rule");
|
|
36
|
+
}
|
|
37
|
+
return { rules };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export {
|
|
41
|
+
rule,
|
|
42
|
+
composite
|
|
43
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
var DEFAULT_FS = {
|
|
3
|
+
work: { path: "/work", writable: true },
|
|
4
|
+
input: { path: "/input", writable: false },
|
|
5
|
+
output: { path: "/output", writable: true }
|
|
6
|
+
};
|
|
7
|
+
var DEFAULT_INPUTS_POLICY = {
|
|
8
|
+
rules: [
|
|
9
|
+
{ use: { adapter: "url" }, when: { always: true } },
|
|
10
|
+
{ use: { adapter: "inline" }, when: { default: true } }
|
|
11
|
+
]
|
|
12
|
+
};
|
|
13
|
+
var DEFAULT_OUTPUTS_POLICY = {
|
|
14
|
+
rules: [
|
|
15
|
+
{ use: { adapter: "inline" }, when: { sizeBelow: "1MB" } },
|
|
16
|
+
{ use: { adapter: "localServer", options: { ttl: "1h" } }, when: { default: true } }
|
|
17
|
+
]
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
DEFAULT_FS,
|
|
22
|
+
DEFAULT_INPUTS_POLICY,
|
|
23
|
+
DEFAULT_OUTPUTS_POLICY
|
|
24
|
+
};
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import path3 from "path";
|
|
5
|
+
import process2 from "process";
|
|
6
|
+
|
|
7
|
+
// src/build/index.ts
|
|
8
|
+
import { mkdir as mkdir2, writeFile as writeFile3, rm } from "fs/promises";
|
|
9
|
+
import path2 from "path";
|
|
10
|
+
import { pathToFileURL } from "url";
|
|
11
|
+
|
|
12
|
+
// src/build/dockerfile.ts
|
|
13
|
+
var DEFAULT_BASE_IMAGE_REGISTRY = "ghcr.io/porkytheblack";
|
|
14
|
+
var KNOWN_BASE_TAGS = {
|
|
15
|
+
"glovebox/base": "1.0",
|
|
16
|
+
"glovebox/media": "1.4",
|
|
17
|
+
"glovebox/docs": "1.2",
|
|
18
|
+
"glovebox/python": "1.3",
|
|
19
|
+
"glovebox/browser": "1.1"
|
|
20
|
+
};
|
|
21
|
+
var STANDARD_GLOVEBOX_BASES = /* @__PURE__ */ new Set([
|
|
22
|
+
"glovebox/base",
|
|
23
|
+
"glovebox/media",
|
|
24
|
+
"glovebox/docs",
|
|
25
|
+
"glovebox/python",
|
|
26
|
+
"glovebox/browser"
|
|
27
|
+
]);
|
|
28
|
+
function resolveBaseImage(base) {
|
|
29
|
+
if (base.includes(":") || base.includes("/") && !base.startsWith("glovebox/")) {
|
|
30
|
+
return base;
|
|
31
|
+
}
|
|
32
|
+
const registry = (process.env.GLOVEBOX_REGISTRY ?? DEFAULT_BASE_IMAGE_REGISTRY).replace(/\/$/, "");
|
|
33
|
+
const tag = KNOWN_BASE_TAGS[base] ?? "latest";
|
|
34
|
+
return `${registry}/${base}:${tag}`;
|
|
35
|
+
}
|
|
36
|
+
function generateDockerfile(config) {
|
|
37
|
+
const baseImage = resolveBaseImage(config.base);
|
|
38
|
+
const isStandardBase = STANDARD_GLOVEBOX_BASES.has(config.base);
|
|
39
|
+
const apt = config.packages.apt ?? [];
|
|
40
|
+
const pip = config.packages.pip ?? [];
|
|
41
|
+
const npm = config.packages.npm ?? [];
|
|
42
|
+
const lines = [];
|
|
43
|
+
lines.push(`FROM ${baseImage} AS base`);
|
|
44
|
+
lines.push("");
|
|
45
|
+
const needsRoot = apt.length > 0 || pip.length > 0 || npm.length > 0;
|
|
46
|
+
if (needsRoot) {
|
|
47
|
+
lines.push("USER root");
|
|
48
|
+
lines.push("");
|
|
49
|
+
}
|
|
50
|
+
if (apt.length > 0) {
|
|
51
|
+
lines.push("RUN apt-get update && apt-get install -y --no-install-recommends \\");
|
|
52
|
+
lines.push(` ${apt.join(" \\\n ")} \\`);
|
|
53
|
+
lines.push(" && rm -rf /var/lib/apt/lists/*");
|
|
54
|
+
lines.push("");
|
|
55
|
+
}
|
|
56
|
+
if (pip.length > 0) {
|
|
57
|
+
lines.push(`RUN pip install --no-cache-dir --break-system-packages ${pip.join(" ")}`);
|
|
58
|
+
lines.push("");
|
|
59
|
+
}
|
|
60
|
+
if (npm.length > 0) {
|
|
61
|
+
lines.push(`RUN npm install -g ${npm.join(" ")}`);
|
|
62
|
+
lines.push("");
|
|
63
|
+
}
|
|
64
|
+
if (!isStandardBase) {
|
|
65
|
+
const mountLines = [];
|
|
66
|
+
for (const mount of Object.values(config.fs)) {
|
|
67
|
+
mountLines.push(`mkdir -p ${mount.path}`);
|
|
68
|
+
}
|
|
69
|
+
lines.push("RUN useradd -m -u 10001 glovebox || true \\");
|
|
70
|
+
lines.push(` && ${mountLines.join(" \\\n && ")} \\`);
|
|
71
|
+
const ownLines = [];
|
|
72
|
+
for (const mount of Object.values(config.fs)) {
|
|
73
|
+
if (mount.writable) {
|
|
74
|
+
ownLines.push(`chown glovebox:glovebox ${mount.path}`);
|
|
75
|
+
} else {
|
|
76
|
+
ownLines.push(`chown root:root ${mount.path} && chmod 555 ${mount.path}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
ownLines.push("mkdir -p /var/glovebox/files");
|
|
80
|
+
ownLines.push("chown -R glovebox:glovebox /var/glovebox");
|
|
81
|
+
lines.push(` && ${ownLines.join(" \\\n && ")}`);
|
|
82
|
+
lines.push("");
|
|
83
|
+
}
|
|
84
|
+
lines.push("COPY --chown=glovebox:glovebox server /opt/glovebox-server");
|
|
85
|
+
lines.push("WORKDIR /opt/glovebox-server");
|
|
86
|
+
if (isStandardBase) {
|
|
87
|
+
lines.push("RUN mkdir -p node_modules \\");
|
|
88
|
+
lines.push(" && ln -sfn /opt/glovebox-prebuilt/node_modules/better-sqlite3 node_modules/better-sqlite3");
|
|
89
|
+
} else {
|
|
90
|
+
lines.push("RUN npm install --omit=dev --no-package-lock --no-audit --no-fund");
|
|
91
|
+
}
|
|
92
|
+
lines.push("");
|
|
93
|
+
if (needsRoot) {
|
|
94
|
+
lines.push("USER glovebox");
|
|
95
|
+
lines.push("");
|
|
96
|
+
}
|
|
97
|
+
lines.push("EXPOSE 8080");
|
|
98
|
+
lines.push("ENV GLOVEBOX_PORT=8080");
|
|
99
|
+
lines.push('CMD ["node", "index.js"]');
|
|
100
|
+
lines.push("");
|
|
101
|
+
return lines.join("\n");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/build/key.ts
|
|
105
|
+
import { createHash, randomBytes } from "crypto";
|
|
106
|
+
import { existsSync } from "fs";
|
|
107
|
+
import { readFile, writeFile } from "fs/promises";
|
|
108
|
+
async function ensureAuthKey(keyPath) {
|
|
109
|
+
let key;
|
|
110
|
+
if (existsSync(keyPath)) {
|
|
111
|
+
key = (await readFile(keyPath, "utf8")).trim();
|
|
112
|
+
if (!key) {
|
|
113
|
+
key = generateKey();
|
|
114
|
+
await writeFile(keyPath, key + "\n", { mode: 384 });
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
key = generateKey();
|
|
118
|
+
await writeFile(keyPath, key + "\n", { mode: 384 });
|
|
119
|
+
}
|
|
120
|
+
return { key, fingerprint: fingerprintKey(key) };
|
|
121
|
+
}
|
|
122
|
+
function generateKey() {
|
|
123
|
+
return randomBytes(32).toString("base64url");
|
|
124
|
+
}
|
|
125
|
+
function fingerprintKey(key) {
|
|
126
|
+
const h = createHash("sha256").update(key).digest("hex");
|
|
127
|
+
return `${h.slice(0, 8)}...${h.slice(-4)}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/build/manifest.ts
|
|
131
|
+
function generateManifest(args) {
|
|
132
|
+
const { config, keyFingerprint } = args;
|
|
133
|
+
const env = {};
|
|
134
|
+
for (const [name, spec] of Object.entries(config.env)) {
|
|
135
|
+
env[name] = {
|
|
136
|
+
required: spec.required,
|
|
137
|
+
secret: spec.secret,
|
|
138
|
+
default: spec.default,
|
|
139
|
+
description: spec.description
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
name: config.name,
|
|
144
|
+
version: config.version,
|
|
145
|
+
base: config.base,
|
|
146
|
+
fs: config.fs,
|
|
147
|
+
env,
|
|
148
|
+
limits: config.limits,
|
|
149
|
+
key_fingerprint: keyFingerprint,
|
|
150
|
+
storage_policy: config.storage,
|
|
151
|
+
packages: config.packages,
|
|
152
|
+
protocol_version: 1
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function generateEnvExample(config) {
|
|
156
|
+
const lines = [];
|
|
157
|
+
lines.push("# Auth key for the glovebox server (generated by `glovebox build`)");
|
|
158
|
+
lines.push("GLOVEBOX_KEY=");
|
|
159
|
+
lines.push("");
|
|
160
|
+
lines.push("# Optional: override the listening port (default 8080)");
|
|
161
|
+
lines.push("# GLOVEBOX_PORT=8080");
|
|
162
|
+
lines.push("");
|
|
163
|
+
for (const [name, spec] of Object.entries(config.env)) {
|
|
164
|
+
if (spec.description) lines.push(`# ${spec.description}`);
|
|
165
|
+
if (spec.required) lines.push("# (required)");
|
|
166
|
+
if (spec.secret) lines.push("# (secret)");
|
|
167
|
+
lines.push(`${name}=${spec.default ?? ""}`);
|
|
168
|
+
lines.push("");
|
|
169
|
+
}
|
|
170
|
+
return lines.join("\n");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/build/nixpacks.ts
|
|
174
|
+
function generateNixpacks(config) {
|
|
175
|
+
const apt = config.packages.apt ?? [];
|
|
176
|
+
const pip = config.packages.pip ?? [];
|
|
177
|
+
const npm = config.packages.npm ?? [];
|
|
178
|
+
const nixPkgs = ["nodejs_20", ...apt];
|
|
179
|
+
if (pip.length > 0) {
|
|
180
|
+
nixPkgs.push("python311");
|
|
181
|
+
for (const p of pip) nixPkgs.push(`python311Packages.${p}`);
|
|
182
|
+
}
|
|
183
|
+
const lines = [];
|
|
184
|
+
lines.push("[phases.setup]");
|
|
185
|
+
lines.push(`nixPkgs = ${JSON.stringify(nixPkgs)}`);
|
|
186
|
+
lines.push("");
|
|
187
|
+
lines.push("[phases.install]");
|
|
188
|
+
lines.push(`cmds = ["cd server && npm install --omit=dev --no-package-lock"]`);
|
|
189
|
+
lines.push("");
|
|
190
|
+
if (npm.length > 0) {
|
|
191
|
+
lines.push("[phases.build]");
|
|
192
|
+
lines.push(`cmds = [${JSON.stringify(`npm install -g ${npm.join(" ")}`)}]`);
|
|
193
|
+
lines.push("");
|
|
194
|
+
}
|
|
195
|
+
lines.push("[start]");
|
|
196
|
+
lines.push(`cmd = "node server/index.js"`);
|
|
197
|
+
lines.push("");
|
|
198
|
+
lines.push("[variables]");
|
|
199
|
+
lines.push(`GLOVEBOX_PORT = "8080"`);
|
|
200
|
+
lines.push("");
|
|
201
|
+
return lines.join("\n");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/build/server-bundle.ts
|
|
205
|
+
import { existsSync as existsSync2 } from "fs";
|
|
206
|
+
import { mkdir, writeFile as writeFile2 } from "fs/promises";
|
|
207
|
+
import { createRequire } from "module";
|
|
208
|
+
import path from "path";
|
|
209
|
+
import { build as esbuild } from "esbuild";
|
|
210
|
+
var requireFromHere = createRequire(import.meta.url);
|
|
211
|
+
var NATIVE_EXTERNALS = ["better-sqlite3"];
|
|
212
|
+
var SYNTHETIC_ENTRY = (wrapEntry, kitEntry) => `import { startGlovebox } from ${JSON.stringify(kitEntry)}
|
|
213
|
+
import * as wrapModule from ${JSON.stringify(wrapEntry)}
|
|
214
|
+
|
|
215
|
+
const port = Number(process.env.GLOVEBOX_PORT ?? 8080)
|
|
216
|
+
const key = process.env.GLOVEBOX_KEY
|
|
217
|
+
if (!key) {
|
|
218
|
+
console.error("GLOVEBOX_KEY is required")
|
|
219
|
+
process.exit(1)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const app = wrapModule.default ?? wrapModule.app
|
|
223
|
+
if (!app || app.__glovebox !== 1) {
|
|
224
|
+
console.error("Wrap module did not default-export a GloveboxApp")
|
|
225
|
+
process.exit(1)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const adapters = typeof wrapModule.adapters === "function"
|
|
229
|
+
? await wrapModule.adapters()
|
|
230
|
+
: wrapModule.adapters
|
|
231
|
+
|
|
232
|
+
const publicBaseUrl = process.env.GLOVEBOX_PUBLIC_URL
|
|
233
|
+
|
|
234
|
+
await startGlovebox({
|
|
235
|
+
app,
|
|
236
|
+
port,
|
|
237
|
+
key,
|
|
238
|
+
manifestPath: new URL("./glovebox.json", import.meta.url).pathname,
|
|
239
|
+
adapters,
|
|
240
|
+
publicBaseUrl,
|
|
241
|
+
})
|
|
242
|
+
`;
|
|
243
|
+
var PACKAGE_JSON = (appName) => ({
|
|
244
|
+
name: `${appName}-server`,
|
|
245
|
+
version: "0.0.0",
|
|
246
|
+
private: true,
|
|
247
|
+
type: "module",
|
|
248
|
+
main: "index.js",
|
|
249
|
+
dependencies: {
|
|
250
|
+
"better-sqlite3": "^11.5.0"
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
async function emitServerBundle(args) {
|
|
254
|
+
const { wrapEntry, outDir, appName } = args;
|
|
255
|
+
if (!existsSync2(wrapEntry)) {
|
|
256
|
+
throw new Error(`Wrap entry not found: ${wrapEntry}`);
|
|
257
|
+
}
|
|
258
|
+
await mkdir(outDir, { recursive: true });
|
|
259
|
+
let kitEntry;
|
|
260
|
+
try {
|
|
261
|
+
kitEntry = requireFromHere.resolve("glovebox-kit");
|
|
262
|
+
} catch {
|
|
263
|
+
throw new Error(
|
|
264
|
+
"Could not resolve glovebox-kit from the glovebox install. Make sure glovebox-kit is installed (it's a dep of glovebox)."
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
const entryContents = SYNTHETIC_ENTRY(wrapEntry, kitEntry);
|
|
268
|
+
await esbuild({
|
|
269
|
+
stdin: {
|
|
270
|
+
contents: entryContents,
|
|
271
|
+
resolveDir: path.dirname(wrapEntry),
|
|
272
|
+
sourcefile: "synthetic-entry.ts",
|
|
273
|
+
loader: "ts"
|
|
274
|
+
},
|
|
275
|
+
outfile: path.join(outDir, "index.js"),
|
|
276
|
+
bundle: true,
|
|
277
|
+
platform: "node",
|
|
278
|
+
format: "esm",
|
|
279
|
+
target: "node20",
|
|
280
|
+
external: NATIVE_EXTERNALS,
|
|
281
|
+
// Mark the dynamic-import-only paths so esbuild doesn't choke if user's
|
|
282
|
+
// wrap module pulls in optional providers (anthropic, openai, bedrock).
|
|
283
|
+
logLevel: "error",
|
|
284
|
+
banner: {
|
|
285
|
+
// ESM in Node sometimes needs createRequire for transitive CJS modules.
|
|
286
|
+
js: `import { createRequire as __glb_createRequire } from "node:module";
|
|
287
|
+
const require = __glb_createRequire(import.meta.url);`
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
await writeFile2(
|
|
291
|
+
path.join(outDir, "package.json"),
|
|
292
|
+
JSON.stringify(PACKAGE_JSON(appName), null, 2) + "\n"
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/build/index.ts
|
|
297
|
+
async function build(args) {
|
|
298
|
+
const entry = path2.resolve(args.entry);
|
|
299
|
+
const entryDir = path2.dirname(entry);
|
|
300
|
+
const outDir = path2.resolve(args.outDir ?? path2.join(entryDir, "dist"));
|
|
301
|
+
const mod = await import(pathToFileURL(entry).href);
|
|
302
|
+
const app = mod.default ?? mod.app;
|
|
303
|
+
if (!app || app.__glovebox !== 1) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
`Entry ${entry} did not default-export a GloveboxApp. Did you call glovebox.wrap(...)?`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
const config = { ...app.config };
|
|
309
|
+
if (args.name) config.name = args.name;
|
|
310
|
+
await rm(outDir, { recursive: true, force: true });
|
|
311
|
+
await mkdir2(outDir, { recursive: true });
|
|
312
|
+
const keyPath = path2.join(outDir, "glovebox.key");
|
|
313
|
+
const { fingerprint } = await ensureAuthKey(keyPath);
|
|
314
|
+
const dockerfile = generateDockerfile(config);
|
|
315
|
+
const nixpacks = generateNixpacks(config);
|
|
316
|
+
const manifest = generateManifest({ config, keyFingerprint: fingerprint });
|
|
317
|
+
const envExample = generateEnvExample(config);
|
|
318
|
+
await writeFile3(path2.join(outDir, "Dockerfile"), dockerfile);
|
|
319
|
+
await writeFile3(path2.join(outDir, "nixpacks.toml"), nixpacks);
|
|
320
|
+
await writeFile3(path2.join(outDir, "glovebox.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
321
|
+
await writeFile3(path2.join(outDir, ".env.example"), envExample);
|
|
322
|
+
const serverDir = path2.join(outDir, "server");
|
|
323
|
+
await emitServerBundle({ wrapEntry: entry, outDir: serverDir, appName: config.name });
|
|
324
|
+
await writeFile3(
|
|
325
|
+
path2.join(serverDir, "glovebox.json"),
|
|
326
|
+
JSON.stringify(manifest, null, 2) + "\n"
|
|
327
|
+
);
|
|
328
|
+
return {
|
|
329
|
+
outDir,
|
|
330
|
+
baseImage: resolveBaseImage(config.base),
|
|
331
|
+
keyFingerprint: fingerprint,
|
|
332
|
+
packages: {
|
|
333
|
+
apt: config.packages.apt?.length ?? 0,
|
|
334
|
+
pip: config.packages.pip?.length ?? 0,
|
|
335
|
+
npm: config.packages.npm?.length ?? 0
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// src/cli.ts
|
|
341
|
+
function parseArgs(argv) {
|
|
342
|
+
const command = argv[0];
|
|
343
|
+
const positional = [];
|
|
344
|
+
const flags = {};
|
|
345
|
+
for (let i = 1; i < argv.length; i++) {
|
|
346
|
+
const a = argv[i];
|
|
347
|
+
if (a.startsWith("--")) {
|
|
348
|
+
const eq = a.indexOf("=");
|
|
349
|
+
if (eq !== -1) {
|
|
350
|
+
flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
351
|
+
} else {
|
|
352
|
+
const next = argv[i + 1];
|
|
353
|
+
if (next && !next.startsWith("--")) {
|
|
354
|
+
flags[a.slice(2)] = next;
|
|
355
|
+
i++;
|
|
356
|
+
} else {
|
|
357
|
+
flags[a.slice(2)] = true;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
} else {
|
|
361
|
+
positional.push(a);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return { command, positional, flags };
|
|
365
|
+
}
|
|
366
|
+
var HELP = `glovebox \u2014 build and ship sandboxed Glove agents
|
|
367
|
+
|
|
368
|
+
Usage:
|
|
369
|
+
glovebox build <entry> [--out <dir>] [--name <name>]
|
|
370
|
+
|
|
371
|
+
Commands:
|
|
372
|
+
build Compile a wrap module into a deployable artifact (Dockerfile,
|
|
373
|
+
nixpacks.toml, server bundle, manifest, key).
|
|
374
|
+
`;
|
|
375
|
+
async function main() {
|
|
376
|
+
const args = parseArgs(process2.argv.slice(2));
|
|
377
|
+
if (!args.command || args.command === "help" || args.flags.help) {
|
|
378
|
+
process2.stdout.write(HELP);
|
|
379
|
+
process2.exit(0);
|
|
380
|
+
}
|
|
381
|
+
if (args.command === "build") {
|
|
382
|
+
const entry = args.positional[0];
|
|
383
|
+
if (!entry) {
|
|
384
|
+
console.error("error: missing entry path");
|
|
385
|
+
console.error("usage: glovebox build <entry>");
|
|
386
|
+
process2.exit(1);
|
|
387
|
+
}
|
|
388
|
+
const outDir = typeof args.flags.out === "string" ? args.flags.out : void 0;
|
|
389
|
+
const name = typeof args.flags.name === "string" ? args.flags.name : void 0;
|
|
390
|
+
const result = await build({ entry, outDir, name });
|
|
391
|
+
const out = result.outDir;
|
|
392
|
+
const rel = path3.relative(process2.cwd(), out) || out;
|
|
393
|
+
process2.stdout.write(`\u2713 Resolved base image: ${result.baseImage}
|
|
394
|
+
`);
|
|
395
|
+
process2.stdout.write(
|
|
396
|
+
`\u2713 Resolved packages (${result.packages.apt} apt, ${result.packages.pip} pip, ${result.packages.npm} npm)
|
|
397
|
+
`
|
|
398
|
+
);
|
|
399
|
+
process2.stdout.write("\u2713 Generated Dockerfile\n");
|
|
400
|
+
process2.stdout.write("\u2713 Generated nixpacks.toml\n");
|
|
401
|
+
process2.stdout.write("\u2713 Generated server bundle\n");
|
|
402
|
+
process2.stdout.write(`\u2713 Generated auth key (fingerprint: ${result.keyFingerprint})
|
|
403
|
+
`);
|
|
404
|
+
process2.stdout.write(`\u2713 Wrote ${rel}/
|
|
405
|
+
|
|
406
|
+
`);
|
|
407
|
+
process2.stdout.write(`Next:
|
|
408
|
+
`);
|
|
409
|
+
process2.stdout.write(
|
|
410
|
+
` GLOVEBOX_KEY=$(cat ${rel}/glovebox.key) docker run -p 8080:8080 -e GLOVEBOX_KEY <image>
|
|
411
|
+
`
|
|
412
|
+
);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
console.error(`unknown command: ${args.command}`);
|
|
416
|
+
process2.stderr.write(HELP);
|
|
417
|
+
process2.exit(1);
|
|
418
|
+
}
|
|
419
|
+
main().catch((err) => {
|
|
420
|
+
console.error(err instanceof Error ? err.stack ?? err.message : err);
|
|
421
|
+
process2.exit(1);
|
|
422
|
+
});
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { StoragePolicyEncoded } from './protocol.js';
|
|
2
|
+
import 'glove-core/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wrap-time configuration for a Glovebox app.
|
|
6
|
+
*
|
|
7
|
+
* The developer hands one of these to `glovebox.wrap(runnable, config)` and
|
|
8
|
+
* the build CLI consumes it to emit a Dockerfile, nixpacks.toml, server
|
|
9
|
+
* bundle, manifest, and auth key.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
type BaseImage = "glovebox/base" | "glovebox/media" | "glovebox/docs" | "glovebox/python" | "glovebox/browser" | (string & {});
|
|
13
|
+
interface FsMount {
|
|
14
|
+
path: string;
|
|
15
|
+
writable: boolean;
|
|
16
|
+
}
|
|
17
|
+
interface EnvVarSpec {
|
|
18
|
+
required: boolean;
|
|
19
|
+
secret?: boolean;
|
|
20
|
+
default?: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
}
|
|
23
|
+
interface Limits {
|
|
24
|
+
cpu?: string;
|
|
25
|
+
memory?: string;
|
|
26
|
+
timeout?: string;
|
|
27
|
+
}
|
|
28
|
+
interface PackageSpec {
|
|
29
|
+
apt?: string[];
|
|
30
|
+
pip?: string[];
|
|
31
|
+
npm?: string[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Storage policy entry. Either passed as a typed `StoragePolicyEncoded` shape
|
|
35
|
+
* (post-encoding) or as a fluent `StorageRule[]` constructed with the
|
|
36
|
+
* `rule.*` helpers from `glovebox/storage`.
|
|
37
|
+
*/
|
|
38
|
+
type StoragePolicy = StoragePolicyEncoded | {
|
|
39
|
+
__rules: StoragePolicyEncoded["rules"];
|
|
40
|
+
};
|
|
41
|
+
interface GloveboxConfig {
|
|
42
|
+
/** Display name for the app, defaults to the package directory name. */
|
|
43
|
+
name?: string;
|
|
44
|
+
/** Semver. Defaults to "0.1.0". */
|
|
45
|
+
version?: string;
|
|
46
|
+
/** Base image to extend. Defaults to "glovebox/base". */
|
|
47
|
+
base?: BaseImage;
|
|
48
|
+
/** Extra system or language packages to install on top of the base. */
|
|
49
|
+
packages?: PackageSpec;
|
|
50
|
+
/** Filesystem mounts inside the container. */
|
|
51
|
+
fs?: Record<string, FsMount>;
|
|
52
|
+
/** Declared environment variables. The runtime validates these on boot. */
|
|
53
|
+
env?: Record<string, EnvVarSpec>;
|
|
54
|
+
/** Storage policies for inputs (client → server) and outputs (server → client). */
|
|
55
|
+
storage?: {
|
|
56
|
+
inputs?: StoragePolicy;
|
|
57
|
+
outputs?: StoragePolicy;
|
|
58
|
+
};
|
|
59
|
+
/** Resource limits and timeout. Surfaced to the agent via the `environment` skill. */
|
|
60
|
+
limits?: Limits;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Opaque marker type returned by `glovebox.wrap`. The runtime introspects this
|
|
64
|
+
* to discover the runnable and the resolved config.
|
|
65
|
+
*/
|
|
66
|
+
interface GloveboxApp {
|
|
67
|
+
readonly __glovebox: 1;
|
|
68
|
+
readonly runnable: unknown;
|
|
69
|
+
readonly config: ResolvedGloveboxConfig;
|
|
70
|
+
}
|
|
71
|
+
interface ResolvedGloveboxConfig {
|
|
72
|
+
name: string;
|
|
73
|
+
version: string;
|
|
74
|
+
base: string;
|
|
75
|
+
packages: PackageSpec;
|
|
76
|
+
fs: Record<string, FsMount>;
|
|
77
|
+
env: Record<string, EnvVarSpec>;
|
|
78
|
+
storage: {
|
|
79
|
+
inputs: StoragePolicyEncoded;
|
|
80
|
+
outputs: StoragePolicyEncoded;
|
|
81
|
+
};
|
|
82
|
+
limits: Limits;
|
|
83
|
+
}
|
|
84
|
+
declare const DEFAULT_FS: Record<string, FsMount>;
|
|
85
|
+
declare const DEFAULT_INPUTS_POLICY: StoragePolicyEncoded;
|
|
86
|
+
declare const DEFAULT_OUTPUTS_POLICY: StoragePolicyEncoded;
|
|
87
|
+
|
|
88
|
+
export { type BaseImage, DEFAULT_FS, DEFAULT_INPUTS_POLICY, DEFAULT_OUTPUTS_POLICY, type EnvVarSpec, type FsMount, type GloveboxApp, type GloveboxConfig, type Limits, type PackageSpec, type ResolvedGloveboxConfig, type StoragePolicy };
|
package/dist/config.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { GloveboxConfig, GloveboxApp } from './config.js';
|
|
2
|
+
export { BaseImage, DEFAULT_FS, DEFAULT_INPUTS_POLICY, DEFAULT_OUTPUTS_POLICY, EnvVarSpec, FsMount, Limits, PackageSpec, ResolvedGloveboxConfig, StoragePolicy } from './config.js';
|
|
3
|
+
export { ClientAbortMessage, ClientDisplayRejectMessage, ClientDisplayResolveMessage, ClientMessage, ClientPingMessage, ClientPromptMessage, FileRef, FileRefGcs, FileRefInline, FileRefS3, FileRefServer, FileRefUrl, Manifest, ManifestEnvVar, OutputsPolicyOverride, ServerCompleteMessage, ServerDisplayClearMessage, ServerDisplayPushMessage, ServerErrorMessage, ServerEventMessage, ServerMessage, ServerPongMessage, StoragePolicyEncoded, SubscriberEventType, WireSlot } from './protocol.js';
|
|
4
|
+
export { InlineOptions, LocalServerOptions, S3Options, UrlOptions, composite, rule } from './storage.js';
|
|
5
|
+
import 'glove-core/core';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Authoring entry point for Glovebox apps.
|
|
9
|
+
*
|
|
10
|
+
* import { glovebox, rule, composite } from "glovebox-core"
|
|
11
|
+
* import { agent } from "./my-agent"
|
|
12
|
+
*
|
|
13
|
+
* export default glovebox.wrap(agent, {
|
|
14
|
+
* base: "glovebox/media",
|
|
15
|
+
* packages: { apt: ["ffmpeg"] },
|
|
16
|
+
* storage: {
|
|
17
|
+
* outputs: composite([rule.inline({ below: "1MB" }), rule.localServer({ ttl: "1h" })]),
|
|
18
|
+
* },
|
|
19
|
+
* })
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Wrap a built Glove runnable into a deployable Glovebox app.
|
|
24
|
+
*
|
|
25
|
+
* The returned object is opaque from the developer's perspective. At runtime
|
|
26
|
+
* (inside the container), `glovebox-kit` reads it to discover the runnable
|
|
27
|
+
* and the resolved config, then injects glovebox-flavored skills, hooks, and
|
|
28
|
+
* mentions on top.
|
|
29
|
+
*/
|
|
30
|
+
declare function wrap<R>(runnable: R, config?: GloveboxConfig): GloveboxApp;
|
|
31
|
+
declare const glovebox: {
|
|
32
|
+
wrap: typeof wrap;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export { GloveboxApp, GloveboxConfig, glovebox };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_FS,
|
|
3
|
+
DEFAULT_INPUTS_POLICY,
|
|
4
|
+
DEFAULT_OUTPUTS_POLICY
|
|
5
|
+
} from "./chunk-LPP37JKX.js";
|
|
6
|
+
import "./chunk-ACURXCF6.js";
|
|
7
|
+
import {
|
|
8
|
+
composite,
|
|
9
|
+
rule
|
|
10
|
+
} from "./chunk-7EPHMP7S.js";
|
|
11
|
+
|
|
12
|
+
// src/index.ts
|
|
13
|
+
function resolvePolicy(p, fallback) {
|
|
14
|
+
if (!p) return fallback;
|
|
15
|
+
if ("__rules" in p) return { rules: p.__rules };
|
|
16
|
+
return p;
|
|
17
|
+
}
|
|
18
|
+
function resolve(config) {
|
|
19
|
+
return {
|
|
20
|
+
name: config.name ?? "glovebox-app",
|
|
21
|
+
version: config.version ?? "0.1.0",
|
|
22
|
+
base: config.base ?? "glovebox/base",
|
|
23
|
+
packages: config.packages ?? {},
|
|
24
|
+
fs: config.fs ?? DEFAULT_FS,
|
|
25
|
+
env: config.env ?? {},
|
|
26
|
+
storage: {
|
|
27
|
+
inputs: resolvePolicy(config.storage?.inputs, DEFAULT_INPUTS_POLICY),
|
|
28
|
+
outputs: resolvePolicy(config.storage?.outputs, DEFAULT_OUTPUTS_POLICY)
|
|
29
|
+
},
|
|
30
|
+
limits: config.limits ?? {}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function wrap(runnable, config = {}) {
|
|
34
|
+
return {
|
|
35
|
+
__glovebox: 1,
|
|
36
|
+
runnable,
|
|
37
|
+
config: resolve(config)
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
var glovebox = { wrap };
|
|
41
|
+
export {
|
|
42
|
+
DEFAULT_FS,
|
|
43
|
+
DEFAULT_INPUTS_POLICY,
|
|
44
|
+
DEFAULT_OUTPUTS_POLICY,
|
|
45
|
+
composite,
|
|
46
|
+
glovebox,
|
|
47
|
+
rule
|
|
48
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { SubscriberEvent } from 'glove-core/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wire protocol shared by glovebox, glovebox-kit, and glovebox-client.
|
|
5
|
+
*
|
|
6
|
+
* One WebSocket per client session. Authenticated on upgrade with
|
|
7
|
+
* `Authorization: Bearer <key>`. Multiple prompts multiplexed by `id`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
type FileRefInline = {
|
|
11
|
+
kind: "inline";
|
|
12
|
+
name: string;
|
|
13
|
+
mime: string;
|
|
14
|
+
/** base64-encoded bytes */
|
|
15
|
+
data: string;
|
|
16
|
+
};
|
|
17
|
+
type FileRefUrl = {
|
|
18
|
+
kind: "url";
|
|
19
|
+
name: string;
|
|
20
|
+
mime?: string;
|
|
21
|
+
url: string;
|
|
22
|
+
headers?: Record<string, string>;
|
|
23
|
+
};
|
|
24
|
+
type FileRefServer = {
|
|
25
|
+
kind: "server";
|
|
26
|
+
name: string;
|
|
27
|
+
mime: string;
|
|
28
|
+
size: number;
|
|
29
|
+
id: string;
|
|
30
|
+
url: string;
|
|
31
|
+
};
|
|
32
|
+
type FileRefS3 = {
|
|
33
|
+
kind: "s3";
|
|
34
|
+
name: string;
|
|
35
|
+
mime?: string;
|
|
36
|
+
bucket: string;
|
|
37
|
+
key: string;
|
|
38
|
+
region?: string;
|
|
39
|
+
};
|
|
40
|
+
type FileRefGcs = {
|
|
41
|
+
kind: "gcs";
|
|
42
|
+
name: string;
|
|
43
|
+
mime?: string;
|
|
44
|
+
bucket: string;
|
|
45
|
+
object: string;
|
|
46
|
+
};
|
|
47
|
+
type FileRef = FileRefInline | FileRefUrl | FileRefServer | FileRefS3 | FileRefGcs;
|
|
48
|
+
/**
|
|
49
|
+
* Wire-side name of a subscriber event. Derived directly from glove-core's
|
|
50
|
+
* `SubscriberEvent` discriminated union so new events added there flow into
|
|
51
|
+
* the wire protocol automatically — no hand-maintained mirror to drift.
|
|
52
|
+
*/
|
|
53
|
+
type SubscriberEventType = SubscriberEvent["type"];
|
|
54
|
+
interface WireSlot<I = unknown> {
|
|
55
|
+
id: string;
|
|
56
|
+
renderer: string;
|
|
57
|
+
input: I;
|
|
58
|
+
}
|
|
59
|
+
type ClientPromptMessage = {
|
|
60
|
+
type: "prompt";
|
|
61
|
+
id: string;
|
|
62
|
+
text: string;
|
|
63
|
+
inputs?: Record<string, FileRef>;
|
|
64
|
+
/** Optional per-request override for output storage policy. */
|
|
65
|
+
outputs_policy?: OutputsPolicyOverride;
|
|
66
|
+
};
|
|
67
|
+
type ClientAbortMessage = {
|
|
68
|
+
type: "abort";
|
|
69
|
+
id: string;
|
|
70
|
+
};
|
|
71
|
+
type ClientDisplayResolveMessage = {
|
|
72
|
+
type: "display_resolve";
|
|
73
|
+
slot_id: string;
|
|
74
|
+
value: unknown;
|
|
75
|
+
};
|
|
76
|
+
type ClientDisplayRejectMessage = {
|
|
77
|
+
type: "display_reject";
|
|
78
|
+
slot_id: string;
|
|
79
|
+
error: unknown;
|
|
80
|
+
};
|
|
81
|
+
type ClientPingMessage = {
|
|
82
|
+
type: "ping";
|
|
83
|
+
ts: number;
|
|
84
|
+
};
|
|
85
|
+
type ClientMessage = ClientPromptMessage | ClientAbortMessage | ClientDisplayResolveMessage | ClientDisplayRejectMessage | ClientPingMessage;
|
|
86
|
+
type ServerEventMessage = {
|
|
87
|
+
type: "event";
|
|
88
|
+
id: string;
|
|
89
|
+
event_type: SubscriberEventType;
|
|
90
|
+
data: unknown;
|
|
91
|
+
};
|
|
92
|
+
type ServerDisplayPushMessage = {
|
|
93
|
+
type: "display_push";
|
|
94
|
+
slot: WireSlot;
|
|
95
|
+
};
|
|
96
|
+
type ServerDisplayClearMessage = {
|
|
97
|
+
type: "display_clear";
|
|
98
|
+
slot_id: string;
|
|
99
|
+
};
|
|
100
|
+
type ServerCompleteMessage = {
|
|
101
|
+
type: "complete";
|
|
102
|
+
id: string;
|
|
103
|
+
message: string;
|
|
104
|
+
outputs: Record<string, FileRef>;
|
|
105
|
+
};
|
|
106
|
+
type ServerErrorMessage = {
|
|
107
|
+
type: "error";
|
|
108
|
+
id: string;
|
|
109
|
+
error: {
|
|
110
|
+
code: string;
|
|
111
|
+
message: string;
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
type ServerPongMessage = {
|
|
115
|
+
type: "pong";
|
|
116
|
+
ts: number;
|
|
117
|
+
};
|
|
118
|
+
type ServerMessage = ServerEventMessage | ServerDisplayPushMessage | ServerDisplayClearMessage | ServerCompleteMessage | ServerErrorMessage | ServerPongMessage;
|
|
119
|
+
type OutputsPolicyOverride = {
|
|
120
|
+
/** Force inline below this size. */
|
|
121
|
+
inline_below?: string;
|
|
122
|
+
/** Direct uploads to this S3 bucket. Caller must trust server with creds. */
|
|
123
|
+
s3?: {
|
|
124
|
+
bucket: string;
|
|
125
|
+
region?: string;
|
|
126
|
+
prefix?: string;
|
|
127
|
+
};
|
|
128
|
+
/** Time-to-live for the local server adapter. */
|
|
129
|
+
server_ttl?: string;
|
|
130
|
+
};
|
|
131
|
+
interface ManifestEnvVar {
|
|
132
|
+
required: boolean;
|
|
133
|
+
secret?: boolean;
|
|
134
|
+
default?: string;
|
|
135
|
+
description?: string;
|
|
136
|
+
}
|
|
137
|
+
interface Manifest {
|
|
138
|
+
name: string;
|
|
139
|
+
version: string;
|
|
140
|
+
base: string;
|
|
141
|
+
fs: Record<string, {
|
|
142
|
+
path: string;
|
|
143
|
+
writable: boolean;
|
|
144
|
+
}>;
|
|
145
|
+
env: Record<string, ManifestEnvVar>;
|
|
146
|
+
limits?: {
|
|
147
|
+
cpu?: string;
|
|
148
|
+
memory?: string;
|
|
149
|
+
timeout?: string;
|
|
150
|
+
};
|
|
151
|
+
/** SHA-256 prefix of the auth key (8 chars + ... + 4 chars formatting on display). */
|
|
152
|
+
key_fingerprint: string;
|
|
153
|
+
storage_policy: {
|
|
154
|
+
inputs: StoragePolicyEncoded;
|
|
155
|
+
outputs: StoragePolicyEncoded;
|
|
156
|
+
};
|
|
157
|
+
packages: {
|
|
158
|
+
apt?: string[];
|
|
159
|
+
pip?: string[];
|
|
160
|
+
npm?: string[];
|
|
161
|
+
};
|
|
162
|
+
/** Glovebox protocol version. */
|
|
163
|
+
protocol_version: 1;
|
|
164
|
+
}
|
|
165
|
+
/** Wire-encoded storage policy. Adapters are referenced by name. */
|
|
166
|
+
type StoragePolicyEncoded = {
|
|
167
|
+
rules: Array<{
|
|
168
|
+
use: {
|
|
169
|
+
adapter: string;
|
|
170
|
+
options?: Record<string, unknown>;
|
|
171
|
+
};
|
|
172
|
+
when: {
|
|
173
|
+
sizeAbove?: string;
|
|
174
|
+
sizeBelow?: string;
|
|
175
|
+
always?: boolean;
|
|
176
|
+
default?: boolean;
|
|
177
|
+
};
|
|
178
|
+
}>;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export type { ClientAbortMessage, ClientDisplayRejectMessage, ClientDisplayResolveMessage, ClientMessage, ClientPingMessage, ClientPromptMessage, FileRef, FileRefGcs, FileRefInline, FileRefS3, FileRefServer, FileRefUrl, Manifest, ManifestEnvVar, OutputsPolicyOverride, ServerCompleteMessage, ServerDisplayClearMessage, ServerDisplayPushMessage, ServerErrorMessage, ServerEventMessage, ServerMessage, ServerPongMessage, StoragePolicyEncoded, SubscriberEventType, WireSlot };
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./chunk-ACURXCF6.js";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { StoragePolicyEncoded } from './protocol.js';
|
|
2
|
+
import 'glove-core/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wrap-time storage policy DSL.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
*
|
|
9
|
+
* storage: {
|
|
10
|
+
* inputs: composite([rule.url(), rule.s3({ bucket: "in" })]),
|
|
11
|
+
* outputs: composite([
|
|
12
|
+
* rule.inline({ below: "1MB" }),
|
|
13
|
+
* rule.localServer({ ttl: "1h" }),
|
|
14
|
+
* ]),
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
type Rule = StoragePolicyEncoded["rules"][number];
|
|
19
|
+
interface InlineOptions {
|
|
20
|
+
below?: string;
|
|
21
|
+
above?: string;
|
|
22
|
+
}
|
|
23
|
+
interface LocalServerOptions {
|
|
24
|
+
ttl?: string;
|
|
25
|
+
below?: string;
|
|
26
|
+
above?: string;
|
|
27
|
+
}
|
|
28
|
+
interface S3Options {
|
|
29
|
+
bucket: string;
|
|
30
|
+
region?: string;
|
|
31
|
+
prefix?: string;
|
|
32
|
+
below?: string;
|
|
33
|
+
above?: string;
|
|
34
|
+
}
|
|
35
|
+
interface UrlOptions {
|
|
36
|
+
below?: string;
|
|
37
|
+
above?: string;
|
|
38
|
+
}
|
|
39
|
+
declare const rule: {
|
|
40
|
+
inline: (opts?: InlineOptions) => Rule;
|
|
41
|
+
localServer: (opts?: LocalServerOptions) => Rule;
|
|
42
|
+
s3: (opts: S3Options) => Rule;
|
|
43
|
+
url: (opts?: UrlOptions) => Rule;
|
|
44
|
+
};
|
|
45
|
+
/** Combine ordered rules into a storage policy. Earlier rules take priority. */
|
|
46
|
+
declare function composite(rules: Rule[]): StoragePolicyEncoded;
|
|
47
|
+
|
|
48
|
+
export { type InlineOptions, type LocalServerOptions, type S3Options, type UrlOptions, composite, rule };
|
package/dist/storage.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "glovebox-core",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Authoring kit and build CLI for sandboxed Glove agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/porkytheblack/glove.git",
|
|
10
|
+
"directory": "packages/glovebox"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/porkytheblack/glove",
|
|
13
|
+
"bugs": "https://github.com/porkytheblack/glove/issues",
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"bin": {
|
|
18
|
+
"glovebox": "./dist/cli.js"
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./protocol": {
|
|
27
|
+
"types": "./dist/protocol.d.ts",
|
|
28
|
+
"import": "./dist/protocol.js",
|
|
29
|
+
"default": "./dist/protocol.js"
|
|
30
|
+
},
|
|
31
|
+
"./storage": {
|
|
32
|
+
"types": "./dist/storage.d.ts",
|
|
33
|
+
"import": "./dist/storage.js",
|
|
34
|
+
"default": "./dist/storage.js"
|
|
35
|
+
},
|
|
36
|
+
"./config": {
|
|
37
|
+
"types": "./dist/config.d.ts",
|
|
38
|
+
"import": "./dist/config.js",
|
|
39
|
+
"default": "./dist/config.js"
|
|
40
|
+
},
|
|
41
|
+
"./package.json": "./package.json"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist"
|
|
45
|
+
],
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"esbuild": "^0.27.3",
|
|
48
|
+
"glove-core": "3.0.0",
|
|
49
|
+
"glovebox-kit": "0.5.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^25.2.3",
|
|
53
|
+
"tsup": "^8.5.1",
|
|
54
|
+
"typescript": "^5.9.3"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsup",
|
|
58
|
+
"typecheck": "tsc --noEmit"
|
|
59
|
+
}
|
|
60
|
+
}
|