@agentproto/sandbox-box 0.2.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 +202 -0
- package/README.md +55 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.mjs +269 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# @agentproto/sandbox-box
|
|
2
|
+
|
|
3
|
+
ascii.dev Box `SandboxProvider` for `@agentproto/sandbox`. Boots a Box cloud
|
|
4
|
+
computer, installs an always-on `systemd` unit for the agentproto daemon, and
|
|
5
|
+
exposes its MCP endpoint as a URL — so `createSandboxAgentSessionHost` can
|
|
6
|
+
hand it straight to `connectDaemonAgentSessionHost` (`@agentproto/worktree`)
|
|
7
|
+
and run any `AgentStep` inside the sandbox, unchanged.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { createSandboxAgentSessionHost } from "@agentproto/sandbox"
|
|
13
|
+
import { boxSandboxProvider } from "@agentproto/sandbox-box"
|
|
14
|
+
import { runWorkflow } from "@agentproto/workflow-runtime"
|
|
15
|
+
import { worktreeAgentWorkflow } from "@agentproto/worktree"
|
|
16
|
+
|
|
17
|
+
const host = await createSandboxAgentSessionHost({
|
|
18
|
+
provider: boxSandboxProvider,
|
|
19
|
+
spec: { provider: "box", config: {} },
|
|
20
|
+
secrets: { slugs: ["OPENROUTER_API_KEY"] },
|
|
21
|
+
})
|
|
22
|
+
try {
|
|
23
|
+
await runWorkflow({ workflow: worktreeAgentWorkflow, input, agents: host })
|
|
24
|
+
} finally {
|
|
25
|
+
await host.stop()
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Requires `BOX_API_KEY` in the host process's environment.
|
|
30
|
+
|
|
31
|
+
## Notes
|
|
32
|
+
|
|
33
|
+
- Box's `host <port>` CLI exposes a port at
|
|
34
|
+
`https://<box-subdomain>-<port>.on.ascii.dev`; re-running it for the same
|
|
35
|
+
box+port returns the same URL, so this provider computes the hostname
|
|
36
|
+
directly from `Box.subdomain` (assigned once, persists across
|
|
37
|
+
stop/resume/fork) rather than parsing command output.
|
|
38
|
+
- Box's command API (`POST /boxes/{id}/commands`) is synchronous-only — no
|
|
39
|
+
`background` flag like e2b's. A bare `&`-backgrounded process would die
|
|
40
|
+
with the command's process group, so the daemon is installed as a
|
|
41
|
+
`systemd` unit (`Restart=always`) instead: `systemctl enable --now` hands
|
|
42
|
+
it to PID 1, which survives `box stop` (filesystem snapshot) + `box
|
|
43
|
+
resume` — cleaner than e2b, which has to re-issue its serve command after
|
|
44
|
+
every resume.
|
|
45
|
+
- systemd services don't inherit the invoking shell's environment, so
|
|
46
|
+
secrets resolved into the sandbox env (e.g. `OPENROUTER_API_KEY`) are
|
|
47
|
+
written to an `EnvironmentFile` (`/etc/agentproto/agentproto.env`, 0600)
|
|
48
|
+
the unit references.
|
|
49
|
+
- Box's own default TTL is 3600s (1 hour) auto-stop. This provider disables
|
|
50
|
+
it by default (`ttlSeconds: null`, same lesson as e2b's
|
|
51
|
+
`DEFAULT_SANDBOX_TIMEOUT_MS`) — set `config.ttlSeconds` to override.
|
|
52
|
+
|
|
53
|
+
## License
|
|
54
|
+
|
|
55
|
+
Apache-2.0 — see [LICENSE](./LICENSE).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { SandboxProvider } from '@agentproto/sandbox';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ascii.dev Box `SandboxProvider` — boots a Box cloud computer, ensures an
|
|
5
|
+
* always-on systemd unit for the agentproto daemon is installed and running
|
|
6
|
+
* on it, and exposes the daemon's `/mcp` endpoint via a stable Box hostname
|
|
7
|
+
* so `createSandboxAgentSessionHost` (`@agentproto/sandbox`) can hand it
|
|
8
|
+
* straight to `connectDaemonAgentSessionHost`.
|
|
9
|
+
*
|
|
10
|
+
* Box's `host <id> <port>` CLI exposes a port at
|
|
11
|
+
* `https://<box-subdomain>-<port>.on.ascii.dev`, and re-running it for the
|
|
12
|
+
* same box+port returns the same URL — so the hostname is computed directly
|
|
13
|
+
* from `Box.subdomain` (assigned once, persists across stop/resume/fork)
|
|
14
|
+
* rather than re-derived per call.
|
|
15
|
+
*
|
|
16
|
+
* Unlike e2b — whose per-command API can background a process that outlives
|
|
17
|
+
* the command (`background: true`) but whose default 60s command timeout
|
|
18
|
+
* SIGKILLs it anyway unless disabled — Box's command API
|
|
19
|
+
* (`POST /boxes/{id}/commands`) is synchronous-only: it waits for the
|
|
20
|
+
* process to exit and returns its output, with no background flag at all.
|
|
21
|
+
* A process started via a bare `&` would die with the command's process
|
|
22
|
+
* group. So the daemon is installed as a `systemd` unit (`Restart=always`)
|
|
23
|
+
* instead: `systemctl enable --now` hands it to PID 1, which is exactly
|
|
24
|
+
* what survives `box stop` (filesystem snapshot) + `box resume` — CLEANER
|
|
25
|
+
* than e2b, which has to re-issue its serve command after every resume.
|
|
26
|
+
*
|
|
27
|
+
* systemd services don't inherit the invoking shell's environment, so the
|
|
28
|
+
* secrets resolved into `opts.env` (e.g. `OPENROUTER_API_KEY`) are written to
|
|
29
|
+
* an `EnvironmentFile` the unit references — otherwise the long-lived daemon
|
|
30
|
+
* would boot with none of the credentials the agent step needs.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** `boxSandboxProvider.boot`/`.connect` — see module docs. */
|
|
34
|
+
declare const boxSandboxProvider: SandboxProvider;
|
|
35
|
+
|
|
36
|
+
export { boxSandboxProvider };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { BoxApi, Configuration } from '@asciidev/box-sdk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @agentproto/sandbox-box v0.1.0
|
|
5
|
+
* ascii.dev Box SandboxProvider for @agentproto/sandbox.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
var DEFAULT_PORT = 18790;
|
|
9
|
+
var DEFAULT_WORKSPACE = "/home/user";
|
|
10
|
+
var DEFAULT_TTL_SECONDS = null;
|
|
11
|
+
var HEALTH_PROBE_TIMEOUT_MS = 3e3;
|
|
12
|
+
var DAEMON_READY_TIMEOUT_MS = 3e4;
|
|
13
|
+
var POLL_INTERVAL_MS = 500;
|
|
14
|
+
var HEALTH_FETCH_TIMEOUT_MS = 5e3;
|
|
15
|
+
var UPDATE_CLI_TIMEOUT_SECONDS = 120;
|
|
16
|
+
var BOX_READY_TIMEOUT_MS = 12e4;
|
|
17
|
+
var READY_STATES = /* @__PURE__ */ new Set(["ready", "idle", "running"]);
|
|
18
|
+
var TERMINAL_BAD_STATES = /* @__PURE__ */ new Set(["archived", "archiving", "error"]);
|
|
19
|
+
var BOX_RETRY_ATTEMPTS = 5;
|
|
20
|
+
var BOX_RETRY_BASE_DELAY_MS = 1e3;
|
|
21
|
+
var ENV_FILE_DIR = "/etc/agentproto";
|
|
22
|
+
var ENV_FILE_PATH = `${ENV_FILE_DIR}/agentproto.env`;
|
|
23
|
+
var SYSTEMD_UNIT_PATH = "/etc/systemd/system/agentproto.service";
|
|
24
|
+
function readBoxConfig(spec) {
|
|
25
|
+
const config = spec.config ?? {};
|
|
26
|
+
return {
|
|
27
|
+
port: typeof config.port === "number" ? config.port : void 0,
|
|
28
|
+
workspace: typeof config.workspace === "string" ? config.workspace : void 0,
|
|
29
|
+
ttlSeconds: typeof config.ttlSeconds === "number" || config.ttlSeconds === null ? config.ttlSeconds : void 0,
|
|
30
|
+
healthProbeTimeoutMs: typeof config.healthProbeTimeoutMs === "number" ? config.healthProbeTimeoutMs : void 0,
|
|
31
|
+
daemonReadyTimeoutMs: typeof config.daemonReadyTimeoutMs === "number" ? config.daemonReadyTimeoutMs : void 0,
|
|
32
|
+
pollIntervalMs: typeof config.pollIntervalMs === "number" ? config.pollIntervalMs : void 0,
|
|
33
|
+
boxReadyTimeoutMs: typeof config.boxReadyTimeoutMs === "number" ? config.boxReadyTimeoutMs : void 0,
|
|
34
|
+
updateCliOnBoot: typeof config.updateCliOnBoot === "boolean" ? config.updateCliOnBoot : void 0,
|
|
35
|
+
cliVersion: typeof config.cliVersion === "string" ? config.cliVersion : void 0,
|
|
36
|
+
installPackages: Array.isArray(config.installPackages) ? config.installPackages.filter((p) => typeof p === "string" && p.length > 0) : void 0,
|
|
37
|
+
updateCliTimeoutSeconds: typeof config.updateCliTimeoutSeconds === "number" ? config.updateCliTimeoutSeconds : void 0,
|
|
38
|
+
setupCommands: Array.isArray(config.setupCommands) ? config.setupCommands.filter((c) => typeof c === "string" && c.length > 0) : void 0
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function makeBoxApi() {
|
|
42
|
+
return new BoxApi(
|
|
43
|
+
new Configuration({
|
|
44
|
+
basePath: process.env.BOX_API_BASE_URL ?? "https://ascii.dev/api/box/v1",
|
|
45
|
+
accessToken: process.env.BOX_API_KEY
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
function isTransientBoxError(err) {
|
|
50
|
+
if (err && typeof err === "object") {
|
|
51
|
+
const name = err.name;
|
|
52
|
+
if (name === "FetchError") return true;
|
|
53
|
+
if (name === "ResponseError") {
|
|
54
|
+
const status = err.response?.status;
|
|
55
|
+
return typeof status === "number" && (status === 429 || status >= 500);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
59
|
+
return /could not reach the box api|operation timed out|ETIMEDOUT|ECONNRESET|ECONNREFUSED|EAI_AGAIN|network|fetch failed/i.test(
|
|
60
|
+
message
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
async function withBoxRetry(fn, opts = {}) {
|
|
64
|
+
const attempts = opts.attempts ?? BOX_RETRY_ATTEMPTS;
|
|
65
|
+
const baseDelayMs = opts.baseDelayMs ?? BOX_RETRY_BASE_DELAY_MS;
|
|
66
|
+
for (let attempt = 1; ; attempt++) {
|
|
67
|
+
try {
|
|
68
|
+
return await fn();
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (attempt >= attempts || !isTransientBoxError(err)) throw err;
|
|
71
|
+
const maxDelay = baseDelayMs * 2 ** (attempt - 1);
|
|
72
|
+
const jitteredDelay = maxDelay === 0 ? 0 : maxDelay / 2 + Math.random() * (maxDelay / 2);
|
|
73
|
+
await new Promise((resolve) => setTimeout(resolve, jitteredDelay));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function waitUntilBoxReady(api, boxId, config) {
|
|
78
|
+
const timeoutMs = config.boxReadyTimeoutMs ?? BOX_READY_TIMEOUT_MS;
|
|
79
|
+
const pollIntervalMs = config.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
80
|
+
const deadline = Date.now() + timeoutMs;
|
|
81
|
+
for (; ; ) {
|
|
82
|
+
const { box } = await withBoxRetry(() => api.get({ boxId }));
|
|
83
|
+
if (READY_STATES.has(box.state) && box.subdomain) return box;
|
|
84
|
+
if (TERMINAL_BAD_STATES.has(box.state)) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`@agentproto/sandbox-box: box ${boxId} entered terminal state "${box.state}" while waiting to become ready.`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (Date.now() >= deadline) {
|
|
90
|
+
throw new Error(`@agentproto/sandbox-box: box ${boxId} did not become ready within ${timeoutMs}ms.`);
|
|
91
|
+
}
|
|
92
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function writeSystemdUnitCommand(host, port, workspace, env) {
|
|
96
|
+
const envLines = Object.entries(env).map(([key, value]) => `${key}=${value}`).join("\n");
|
|
97
|
+
const unit = [
|
|
98
|
+
"[Unit]",
|
|
99
|
+
"Description=agentproto daemon",
|
|
100
|
+
"After=network.target",
|
|
101
|
+
"",
|
|
102
|
+
"[Service]",
|
|
103
|
+
`EnvironmentFile=-${ENV_FILE_PATH}`,
|
|
104
|
+
`ExecStart=agentproto serve --port ${port} --bind 0.0.0.0 --workspace "${workspace}" --allow-origin https://${host}`,
|
|
105
|
+
"Restart=always",
|
|
106
|
+
"User=user",
|
|
107
|
+
"",
|
|
108
|
+
"[Install]",
|
|
109
|
+
"WantedBy=multi-user.target"
|
|
110
|
+
].join("\n");
|
|
111
|
+
return [
|
|
112
|
+
"set -e",
|
|
113
|
+
`sudo mkdir -p ${ENV_FILE_DIR}`,
|
|
114
|
+
`sudo tee ${ENV_FILE_PATH} > /dev/null <<'AGENTPROTO_ENV'
|
|
115
|
+
${envLines}
|
|
116
|
+
AGENTPROTO_ENV`,
|
|
117
|
+
`sudo chmod 600 ${ENV_FILE_PATH}`,
|
|
118
|
+
`sudo tee ${SYSTEMD_UNIT_PATH} > /dev/null <<'AGENTPROTO_UNIT'
|
|
119
|
+
${unit}
|
|
120
|
+
AGENTPROTO_UNIT`
|
|
121
|
+
].join("\n");
|
|
122
|
+
}
|
|
123
|
+
async function ensureDaemonHealthy(api, boxId, host, port, workspace, config, env, expose = "public") {
|
|
124
|
+
const healthUrl = `https://${host}/health`;
|
|
125
|
+
const healthProbeTimeoutMs = config.healthProbeTimeoutMs ?? HEALTH_PROBE_TIMEOUT_MS;
|
|
126
|
+
const daemonReadyTimeoutMs = config.daemonReadyTimeoutMs ?? DAEMON_READY_TIMEOUT_MS;
|
|
127
|
+
const pollIntervalMs = config.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
128
|
+
const alreadyUp = await probeHealth(healthUrl, healthProbeTimeoutMs, pollIntervalMs);
|
|
129
|
+
let token;
|
|
130
|
+
if (!alreadyUp) {
|
|
131
|
+
if (config.updateCliOnBoot ?? true) {
|
|
132
|
+
const extras = (config.installPackages ?? []).map((spec) => ` '${spec.replace(/'/g, "")}'`).join("");
|
|
133
|
+
const cli = `"@agentproto/cli@${config.cliVersion ?? "latest"}"`;
|
|
134
|
+
await api.command({
|
|
135
|
+
boxId,
|
|
136
|
+
commandRequest: {
|
|
137
|
+
command: `sudo npm i -g ${cli}${extras}`,
|
|
138
|
+
timeoutSeconds: config.updateCliTimeoutSeconds ?? UPDATE_CLI_TIMEOUT_SECONDS
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const hostCommand = await api.command({
|
|
143
|
+
boxId,
|
|
144
|
+
commandRequest: {
|
|
145
|
+
command: `box host ${boxId} ${port} ${expose === "private" ? "--private" : "--public"}`,
|
|
146
|
+
timeoutSeconds: 30
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
if (expose === "private") token = parseBoxHostToken(hostCommand.stdout);
|
|
150
|
+
await api.command({
|
|
151
|
+
boxId,
|
|
152
|
+
commandRequest: { command: writeSystemdUnitCommand(host, port, workspace, env), timeoutSeconds: 30 }
|
|
153
|
+
});
|
|
154
|
+
} else if (expose === "private") {
|
|
155
|
+
const hostCommand = await api.command({
|
|
156
|
+
boxId,
|
|
157
|
+
commandRequest: { command: `box host ${boxId} ${port} --private`, timeoutSeconds: 30 }
|
|
158
|
+
});
|
|
159
|
+
token = parseBoxHostToken(hostCommand.stdout);
|
|
160
|
+
}
|
|
161
|
+
for (const command of config.setupCommands ?? []) {
|
|
162
|
+
await api.command({ boxId, commandRequest: { command, timeoutSeconds: 60 } });
|
|
163
|
+
}
|
|
164
|
+
if (alreadyUp) return token;
|
|
165
|
+
await api.command({
|
|
166
|
+
boxId,
|
|
167
|
+
commandRequest: {
|
|
168
|
+
command: "sudo systemctl daemon-reload && sudo systemctl enable --now agentproto",
|
|
169
|
+
timeoutSeconds: 30
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
const ready = await probeHealth(healthUrl, daemonReadyTimeoutMs, pollIntervalMs);
|
|
173
|
+
if (!ready) {
|
|
174
|
+
await withBoxRetry(() => api.remove({ boxId }));
|
|
175
|
+
throw new Error(
|
|
176
|
+
`@agentproto/sandbox-box: agentproto daemon did not become healthy at ${healthUrl} within ${daemonReadyTimeoutMs}ms (box ${boxId}).`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
return token;
|
|
180
|
+
}
|
|
181
|
+
var PORT_AUTH_COOKIE = "_port_auth";
|
|
182
|
+
function parseBoxHostToken(stdout) {
|
|
183
|
+
const lines = stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
184
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
185
|
+
try {
|
|
186
|
+
const parsed = JSON.parse(lines[i]);
|
|
187
|
+
if (typeof parsed.url === "string") {
|
|
188
|
+
const token = new URL(parsed.url).searchParams.get("_token");
|
|
189
|
+
if (token) return token;
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const rawMatch = stdout.match(/[?&]_token=([A-Za-z0-9._-]+)/);
|
|
195
|
+
if (rawMatch?.[1]) return rawMatch[1];
|
|
196
|
+
throw new Error(
|
|
197
|
+
"@agentproto/sandbox-box: `box host --private` produced no `_token` in its output."
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
function toBootedSandbox(api, boxId, host, token) {
|
|
201
|
+
return {
|
|
202
|
+
mcpUrl: `https://${host}/mcp`,
|
|
203
|
+
sandboxId: boxId,
|
|
204
|
+
...token ? { token, authHeaders: { Cookie: `${PORT_AUTH_COOKIE}=${token}` } } : {},
|
|
205
|
+
async stop() {
|
|
206
|
+
await withBoxRetry(() => api.stop({ boxId })).catch(() => {
|
|
207
|
+
});
|
|
208
|
+
await withBoxRetry(() => api.remove({ boxId }));
|
|
209
|
+
},
|
|
210
|
+
async pause() {
|
|
211
|
+
await withBoxRetry(() => api.stop({ boxId }));
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
var boxSandboxProvider = {
|
|
216
|
+
async boot(spec, opts) {
|
|
217
|
+
const config = readBoxConfig(spec);
|
|
218
|
+
const port = config.port ?? DEFAULT_PORT;
|
|
219
|
+
const workspace = config.workspace ?? DEFAULT_WORKSPACE;
|
|
220
|
+
const api = makeBoxApi();
|
|
221
|
+
const created = await api.create({
|
|
222
|
+
createBoxRequest: {
|
|
223
|
+
ttlSeconds: config.ttlSeconds === void 0 ? DEFAULT_TTL_SECONDS : config.ttlSeconds,
|
|
224
|
+
env: opts.env
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
const boxId = created.box.id;
|
|
228
|
+
const box = await waitUntilBoxReady(api, boxId, config);
|
|
229
|
+
if (!box.subdomain) {
|
|
230
|
+
throw new Error(`@agentproto/sandbox-box: box ${boxId} has no assigned subdomain.`);
|
|
231
|
+
}
|
|
232
|
+
const host = `${box.subdomain}-${port}.on.ascii.dev`;
|
|
233
|
+
await ensureDaemonHealthy(api, boxId, host, port, workspace, config, opts.env);
|
|
234
|
+
return toBootedSandbox(api, boxId, host);
|
|
235
|
+
},
|
|
236
|
+
async connect(boxId, spec, opts) {
|
|
237
|
+
const config = readBoxConfig(spec);
|
|
238
|
+
const port = config.port ?? DEFAULT_PORT;
|
|
239
|
+
const workspace = config.workspace ?? DEFAULT_WORKSPACE;
|
|
240
|
+
const api = makeBoxApi();
|
|
241
|
+
await withBoxRetry(() => api.resume({ boxId, resumeRequest: {} }));
|
|
242
|
+
if (opts.keepAlive) {
|
|
243
|
+
await withBoxRetry(() => api.update({ boxId, updateBoxRequest: { ttlSeconds: null } }));
|
|
244
|
+
}
|
|
245
|
+
const box = await waitUntilBoxReady(api, boxId, config);
|
|
246
|
+
if (!box.subdomain) {
|
|
247
|
+
throw new Error(`@agentproto/sandbox-box: box ${boxId} has no assigned subdomain.`);
|
|
248
|
+
}
|
|
249
|
+
const host = `${box.subdomain}-${port}.on.ascii.dev`;
|
|
250
|
+
const token = await ensureDaemonHealthy(api, boxId, host, port, workspace, config, opts.env, opts.expose);
|
|
251
|
+
return toBootedSandbox(api, boxId, host, token);
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
async function probeHealth(url, timeoutMs, pollIntervalMs) {
|
|
255
|
+
const deadline = Date.now() + timeoutMs;
|
|
256
|
+
for (; ; ) {
|
|
257
|
+
try {
|
|
258
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(HEALTH_FETCH_TIMEOUT_MS) });
|
|
259
|
+
if (res.ok) return true;
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
262
|
+
if (Date.now() >= deadline) return false;
|
|
263
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export { boxSandboxProvider };
|
|
268
|
+
//# sourceMappingURL=index.mjs.map
|
|
269
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/provider.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * ascii.dev Box `SandboxProvider` — boots a Box cloud computer, ensures an\n * always-on systemd unit for the agentproto daemon is installed and running\n * on it, and exposes the daemon's `/mcp` endpoint via a stable Box hostname\n * so `createSandboxAgentSessionHost` (`@agentproto/sandbox`) can hand it\n * straight to `connectDaemonAgentSessionHost`.\n *\n * Box's `host <id> <port>` CLI exposes a port at\n * `https://<box-subdomain>-<port>.on.ascii.dev`, and re-running it for the\n * same box+port returns the same URL — so the hostname is computed directly\n * from `Box.subdomain` (assigned once, persists across stop/resume/fork)\n * rather than re-derived per call.\n *\n * Unlike e2b — whose per-command API can background a process that outlives\n * the command (`background: true`) but whose default 60s command timeout\n * SIGKILLs it anyway unless disabled — Box's command API\n * (`POST /boxes/{id}/commands`) is synchronous-only: it waits for the\n * process to exit and returns its output, with no background flag at all.\n * A process started via a bare `&` would die with the command's process\n * group. So the daemon is installed as a `systemd` unit (`Restart=always`)\n * instead: `systemctl enable --now` hands it to PID 1, which is exactly\n * what survives `box stop` (filesystem snapshot) + `box resume` — CLEANER\n * than e2b, which has to re-issue its serve command after every resume.\n *\n * systemd services don't inherit the invoking shell's environment, so the\n * secrets resolved into `opts.env` (e.g. `OPENROUTER_API_KEY`) are written to\n * an `EnvironmentFile` the unit references — otherwise the long-lived daemon\n * would boot with none of the credentials the agent step needs.\n */\n\nimport { BoxApi, Configuration, type Box } from \"@asciidev/box-sdk\"\nimport type { BootedSandbox, SandboxBootOpts, SandboxProvider, SandboxSpec } from \"@agentproto/sandbox\"\n\n/** `serve.ts`'s default port (`DEFAULT_MCP_URL` in `@agentproto/harness`). */\nconst DEFAULT_PORT = 18790\n/** Box's documented default login user / home directory. */\nconst DEFAULT_WORKSPACE = \"/home/user\"\n/**\n * Default Box lifetime. Box's own default TTL is 3600s (1 hour) —\n * survivable for short turns but the same class of mid-turn-reaper risk\n * e2b's 5-minute default caused (`DEFAULT_SANDBOX_TIMEOUT_MS` in\n * `@agentproto/sandbox-e2b`): boot + provisioning + a multi-minute agent\n * turn can cross the deadline. `null` disables Box's auto-stop entirely\n * (`box new --no-auto-stop` / `ttlSeconds: null`) — `config.ttlSeconds`\n * still overrides in either direction.\n */\nconst DEFAULT_TTL_SECONDS = null\nconst HEALTH_PROBE_TIMEOUT_MS = 3_000\nconst DAEMON_READY_TIMEOUT_MS = 30_000\nconst POLL_INTERVAL_MS = 500\n/** Fixed per-request HTTP timeout for health-probe fetches, deliberately\n * decoupled from `pollIntervalMs` (the delay BETWEEN probe attempts) — a\n * short poll cadence must not also force each individual fetch to time out\n * almost immediately on slow/congested networks. */\nconst HEALTH_FETCH_TIMEOUT_MS = 5_000\nconst UPDATE_CLI_TIMEOUT_SECONDS = 120\n/** How long to wait for a fresh/resumed box to reach a usable state and get\n * its subdomain assigned. Box is a fuller \"cloud computer\" than e2b's\n * Firecracker microVM, so boot can take noticeably longer than e2b's. */\nconst BOX_READY_TIMEOUT_MS = 120_000\n\nconst READY_STATES = new Set([\"ready\", \"idle\", \"running\"])\nconst TERMINAL_BAD_STATES = new Set([\"archived\", \"archiving\", \"error\"])\n\n/** Default retry policy for `withBoxRetry` — 5 attempts, 1s/2s/4s/8s backoff. */\nconst BOX_RETRY_ATTEMPTS = 5\nconst BOX_RETRY_BASE_DELAY_MS = 1_000\n\nconst ENV_FILE_DIR = \"/etc/agentproto\"\nconst ENV_FILE_PATH = `${ENV_FILE_DIR}/agentproto.env`\nconst SYSTEMD_UNIT_PATH = \"/etc/systemd/system/agentproto.service\"\n\ninterface BoxSandboxConfig {\n /** Port the agentproto daemon listens on inside the box. Default 18790. */\n port?: number\n /** `--workspace` passed to `agentproto serve`. Default `/home/user`. */\n workspace?: string\n /** Box lifetime cap in seconds, forwarded as `ttlSeconds` on create.\n * `null` disables Box's auto-stop. Defaults to `null` (see\n * DEFAULT_TTL_SECONDS above) — Box's own default is a 1-hour auto-stop. */\n ttlSeconds?: number | null\n /** How long to wait for the health probe before assuming the daemon isn't already up. */\n healthProbeTimeoutMs?: number\n /** How long to wait for the daemon to become healthy after starting it. */\n daemonReadyTimeoutMs?: number\n /** Delay between health-probe / box-state-poll attempts. */\n pollIntervalMs?: number\n /** How long to wait for the box itself to reach a ready state + get its subdomain assigned. */\n boxReadyTimeoutMs?: number\n /** Run `npm i -g @agentproto/cli@<cliVersion>` before installing the\n * systemd unit. Default true. Only runs when this provider is the one\n * starting the daemon (skipped when the health probe finds it already up). */\n updateCliOnBoot?: boolean\n /** CLI version to install on boot, pinned instead of a floating `@latest`.\n * Produces `@agentproto/cli@<version>`; when unset, falls back to `@latest`. */\n cliVersion?: string\n /** Extra npm package specs installed globally ALONGSIDE the CLI update\n * (single `npm i -g` invocation) — e.g. the adapter the caller intends to\n * spawn. Ignored when `updateCliOnBoot` is false or the daemon was\n * already up. */\n installPackages?: string[]\n /** Timeout (seconds) for the `npm i -g` update command. Default 120. */\n updateCliTimeoutSeconds?: number\n /** Extra shell commands host-executed inside the box AFTER the boot `npm i -g`\n * and systemd unit install, and BEFORE the daemon is started. Runs on\n * EVERY boot/connect, even when the health probe finds the daemon\n * already up — entries must be idempotent. */\n setupCommands?: string[]\n}\n\nfunction readBoxConfig(spec: SandboxSpec): BoxSandboxConfig {\n const config = (spec.config ?? {}) as Record<string, unknown>\n return {\n port: typeof config.port === \"number\" ? config.port : undefined,\n workspace: typeof config.workspace === \"string\" ? config.workspace : undefined,\n ttlSeconds:\n typeof config.ttlSeconds === \"number\" || config.ttlSeconds === null\n ? config.ttlSeconds\n : undefined,\n healthProbeTimeoutMs:\n typeof config.healthProbeTimeoutMs === \"number\" ? config.healthProbeTimeoutMs : undefined,\n daemonReadyTimeoutMs:\n typeof config.daemonReadyTimeoutMs === \"number\" ? config.daemonReadyTimeoutMs : undefined,\n pollIntervalMs: typeof config.pollIntervalMs === \"number\" ? config.pollIntervalMs : undefined,\n boxReadyTimeoutMs:\n typeof config.boxReadyTimeoutMs === \"number\" ? config.boxReadyTimeoutMs : undefined,\n updateCliOnBoot: typeof config.updateCliOnBoot === \"boolean\" ? config.updateCliOnBoot : undefined,\n cliVersion: typeof config.cliVersion === \"string\" ? config.cliVersion : undefined,\n installPackages: Array.isArray(config.installPackages)\n ? config.installPackages.filter((p): p is string => typeof p === \"string\" && p.length > 0)\n : undefined,\n updateCliTimeoutSeconds:\n typeof config.updateCliTimeoutSeconds === \"number\" ? config.updateCliTimeoutSeconds : undefined,\n setupCommands: Array.isArray(config.setupCommands)\n ? config.setupCommands.filter((c): c is string => typeof c === \"string\" && c.length > 0)\n : undefined,\n }\n}\n\n/** `process.env.BOX_API_KEY` read directly, exactly like e2b reads `E2B_API_KEY`. */\nfunction makeBoxApi(): BoxApi {\n return new BoxApi(\n new Configuration({\n basePath: process.env.BOX_API_BASE_URL ?? \"https://ascii.dev/api/box/v1\",\n accessToken: process.env.BOX_API_KEY,\n }),\n )\n}\n\n/** Matches ascii.dev control-plane failures known to be transient: network-level\n * errors (DNS/connect/timeout), the box-sdk's `FetchError` wrapper (thrown\n * when the underlying `fetch` itself rejects), and HTTP 429/5xx\n * `ResponseError`s. Everything else — 404 not-found, other 4xx, archived/\n * terminal-state errors — is terminal and must fail fast. */\nfunction isTransientBoxError(err: unknown): boolean {\n if (err && typeof err === \"object\") {\n const name = (err as { name?: unknown }).name\n if (name === \"FetchError\") return true\n if (name === \"ResponseError\") {\n const status = (err as { response?: { status?: unknown } }).response?.status\n return typeof status === \"number\" && (status === 429 || status >= 500)\n }\n }\n const message = err instanceof Error ? err.message : String(err)\n return /could not reach the box api|operation timed out|ETIMEDOUT|ECONNRESET|ECONNREFUSED|EAI_AGAIN|network|fetch failed/i.test(\n message,\n )\n}\n\ninterface BoxRetryOpts {\n /** Max attempts (first try + retries). Default `BOX_RETRY_ATTEMPTS` (5). */\n attempts?: number\n /** Base delay in ms before the first retry, doubling each subsequent\n * attempt (1s/2s/4s/8s at the default). Default `BOX_RETRY_BASE_DELAY_MS`. */\n baseDelayMs?: number\n}\n\n/**\n * Retries an ascii.dev control-plane call on transient failures, with\n * jittered exponential backoff. ascii.dev's control plane is observably\n * flaky (`could not reach the Box API at https://ascii.dev: ... operation\n * timed out`) — a first-attempt timeout on `stop`/`remove` otherwise leaves\n * a box running (billable) with nothing to retry it. `stop`/`remove`/`resume`\n * are idempotent on ascii.dev (stopping an already-stopping box, etc.), so\n * retrying them is safe; terminal errors (404, other 4xx) are NOT retried —\n * see `isTransientBoxError`.\n */\nexport async function withBoxRetry<T>(fn: () => Promise<T>, opts: BoxRetryOpts = {}): Promise<T> {\n const attempts = opts.attempts ?? BOX_RETRY_ATTEMPTS\n const baseDelayMs = opts.baseDelayMs ?? BOX_RETRY_BASE_DELAY_MS\n for (let attempt = 1; ; attempt++) {\n try {\n return await fn()\n } catch (err) {\n if (attempt >= attempts || !isTransientBoxError(err)) throw err\n const maxDelay = baseDelayMs * 2 ** (attempt - 1)\n const jitteredDelay = maxDelay === 0 ? 0 : maxDelay / 2 + Math.random() * (maxDelay / 2)\n await new Promise(resolve => setTimeout(resolve, jitteredDelay))\n }\n }\n}\n\n/** Poll `api.get` until the box reaches a ready state AND has its subdomain\n * assigned (needed to compute its public host), or throw on a terminal bad\n * state / timeout. */\nasync function waitUntilBoxReady(api: BoxApi, boxId: string, config: BoxSandboxConfig): Promise<Box> {\n const timeoutMs = config.boxReadyTimeoutMs ?? BOX_READY_TIMEOUT_MS\n const pollIntervalMs = config.pollIntervalMs ?? POLL_INTERVAL_MS\n const deadline = Date.now() + timeoutMs\n for (;;) {\n const { box } = await withBoxRetry(() => api.get({ boxId }))\n if (READY_STATES.has(box.state) && box.subdomain) return box\n if (TERMINAL_BAD_STATES.has(box.state)) {\n throw new Error(\n `@agentproto/sandbox-box: box ${boxId} entered terminal state \"${box.state}\" while waiting to become ready.`,\n )\n }\n if (Date.now() >= deadline) {\n throw new Error(`@agentproto/sandbox-box: box ${boxId} did not become ready within ${timeoutMs}ms.`)\n }\n await new Promise(resolve => setTimeout(resolve, pollIntervalMs))\n }\n}\n\n/** One shell command that writes the daemon's env file (0600, secrets from\n * `opts.env`) and its systemd unit (`Restart=always`, `ExecStart` bound to\n * this box's port/workspace/host) — prep only, does not start anything. */\nfunction writeSystemdUnitCommand(\n host: string,\n port: number,\n workspace: string,\n env: Record<string, string>,\n): string {\n const envLines = Object.entries(env)\n .map(([key, value]) => `${key}=${value}`)\n .join(\"\\n\")\n const unit = [\n \"[Unit]\",\n \"Description=agentproto daemon\",\n \"After=network.target\",\n \"\",\n \"[Service]\",\n `EnvironmentFile=-${ENV_FILE_PATH}`,\n `ExecStart=agentproto serve --port ${port} --bind 0.0.0.0 --workspace \"${workspace}\" --allow-origin https://${host}`,\n \"Restart=always\",\n \"User=user\",\n \"\",\n \"[Install]\",\n \"WantedBy=multi-user.target\",\n ].join(\"\\n\")\n\n // Heredoc terminators must be alone on their own line — joining these with\n // \" && \" (as every other multi-step command in this provider does) would\n // land the next command on the SAME line as the previous heredoc's closing\n // delimiter. bash then never recognizes that delimiter and swallows\n // everything after it (including the next heredoc) into the first\n // heredoc's body until EOF — silently no-op'ing the systemd unit write\n // while still reporting exitCode 0. Verified live: the daemon never\n // started because `/etc/systemd/system/agentproto.service` was never\n // written. Newlines keep each heredoc's terminator on its own line;\n // `set -e` preserves fail-fast semantics across the sequence.\n return [\n \"set -e\",\n `sudo mkdir -p ${ENV_FILE_DIR}`,\n `sudo tee ${ENV_FILE_PATH} > /dev/null <<'AGENTPROTO_ENV'\\n${envLines}\\nAGENTPROTO_ENV`,\n `sudo chmod 600 ${ENV_FILE_PATH}`,\n `sudo tee ${SYSTEMD_UNIT_PATH} > /dev/null <<'AGENTPROTO_UNIT'\\n${unit}\\nAGENTPROTO_UNIT`,\n ].join(\"\\n\")\n}\n\n/**\n * Probe health, and — if the daemon isn't already up — update the CLI,\n * expose the port, install the systemd unit, and start it, then re-probe.\n * Shared by `boot` (a fresh box's daemon has never been installed) and\n * `connect` (a resumed box's systemd-managed daemon SHOULD already be up,\n * but is re-verified rather than assumed). Throws (after deleting the box)\n * if the daemon never comes up.\n *\n * `expose` defaults to `\"public\"` — `boot`'s and the reuse path's existing\n * behaviour, completely unchanged. `\"private\"` (only ever passed by\n * `attachSandbox`, see `@agentproto/runtime`) additionally re-runs `box\n * host ... --private` even when the daemon is ALREADY up — a prior\n * boot/connect may have exposed the port publicly, and attach's whole\n * point is a fresh, token-gated URL — and returns the captured token.\n */\nasync function ensureDaemonHealthy(\n api: BoxApi,\n boxId: string,\n host: string,\n port: number,\n workspace: string,\n config: BoxSandboxConfig,\n env: Record<string, string>,\n expose: \"public\" | \"private\" = \"public\",\n): Promise<string | undefined> {\n const healthUrl = `https://${host}/health`\n const healthProbeTimeoutMs = config.healthProbeTimeoutMs ?? HEALTH_PROBE_TIMEOUT_MS\n const daemonReadyTimeoutMs = config.daemonReadyTimeoutMs ?? DAEMON_READY_TIMEOUT_MS\n const pollIntervalMs = config.pollIntervalMs ?? POLL_INTERVAL_MS\n\n const alreadyUp = await probeHealth(healthUrl, healthProbeTimeoutMs, pollIntervalMs)\n let token: string | undefined\n\n if (!alreadyUp) {\n if (config.updateCliOnBoot ?? true) {\n const extras = (config.installPackages ?? [])\n .map(spec => ` '${spec.replace(/'/g, \"\")}'`)\n .join(\"\")\n const cli = `\"@agentproto/cli@${config.cliVersion ?? \"latest\"}\"`\n await api.command({\n boxId,\n commandRequest: {\n command: `sudo npm i -g ${cli}${extras}`,\n timeoutSeconds: config.updateCliTimeoutSeconds ?? UPDATE_CLI_TIMEOUT_SECONDS,\n },\n })\n }\n\n // Exposes the port at https://<subdomain>-<port>.on.ascii.dev. Re-running\n // for the same box+port returns the same URL — safe on every (re-)boot.\n // `box host` requires its own <ID> argument even when run from inside\n // that same box — verified live (`box host <port> --public` alone fails\n // with \"the following required arguments were not provided: <PORT>\",\n // since `<port>` alone is parsed as `<ID>`). `--private` is the same\n // shape, token-gated: ascii.dev returns the bearer token in the\n // command's stdout (see `parseBoxHostToken`).\n const hostCommand = await api.command({\n boxId,\n commandRequest: {\n command: `box host ${boxId} ${port} ${expose === \"private\" ? \"--private\" : \"--public\"}`,\n timeoutSeconds: 30,\n },\n })\n if (expose === \"private\") token = parseBoxHostToken(hostCommand.stdout)\n\n await api.command({\n boxId,\n commandRequest: { command: writeSystemdUnitCommand(host, port, workspace, env), timeoutSeconds: 30 },\n })\n } else if (expose === \"private\") {\n const hostCommand = await api.command({\n boxId,\n commandRequest: { command: `box host ${boxId} ${port} --private`, timeoutSeconds: 30 },\n })\n token = parseBoxHostToken(hostCommand.stdout)\n }\n\n // Caller-declared provision hooks. Runs regardless of `alreadyUp` — a\n // caller-declared hook install must still land even when the daemon (and\n // therefore the systemd unit) was already running from a prior boot.\n for (const command of config.setupCommands ?? []) {\n await api.command({ boxId, commandRequest: { command, timeoutSeconds: 60 } })\n }\n\n if (alreadyUp) return token\n\n await api.command({\n boxId,\n commandRequest: {\n command: \"sudo systemctl daemon-reload && sudo systemctl enable --now agentproto\",\n timeoutSeconds: 30,\n },\n })\n\n const ready = await probeHealth(healthUrl, daemonReadyTimeoutMs, pollIntervalMs)\n if (!ready) {\n await withBoxRetry(() => api.remove({ boxId }))\n throw new Error(\n `@agentproto/sandbox-box: agentproto daemon did not become healthy at ${healthUrl} ` +\n `within ${daemonReadyTimeoutMs}ms (box ${boxId}).`,\n )\n }\n return token\n}\n\n/** Cookie name ascii.dev's private-hosting edge gates the port on — the\n * `_token` value is delivered to a client as `Cookie: <PORT_AUTH_COOKIE>=<token>`. */\nconst PORT_AUTH_COOKIE = \"_port_auth\"\n\n/**\n * Extract the access token from `box host ... --private`'s output.\n *\n * VERIFIED LIVE (this replaced a guess that took the last output line as a\n * bearer token — it wasn't). `box host <id> <port> --private` prints a JSON\n * object whose `url` carries the token as a `_token` query param, e.g.\n * `{\"access\":\"private\", ..., \"url\":\"https://<host>?_token=<64-hex>\"}`. Hitting\n * that URL 302-redirects and `Set-Cookie`s `_port_auth=<token>` (30-day TTL);\n * the port edge then gates on THAT COOKIE — a bearer header or a `?_token=`\n * query on a sub-path is ignored (403). So the token a client needs is the\n * `_token` hex, presented as a cookie (see `PORT_AUTH_COOKIE` / `authHeaders`),\n * NOT the whole JSON blob and NOT a bearer.\n *\n * Tolerates leading log/status lines by scanning for the last line that parses\n * as JSON with a `url`; falls back to a raw `_token=` match if the shape drifts.\n */\nfunction parseBoxHostToken(stdout: string): string {\n const lines = stdout\n .split(\"\\n\")\n .map(line => line.trim())\n .filter(line => line.length > 0)\n\n for (let i = lines.length - 1; i >= 0; i--) {\n try {\n const parsed = JSON.parse(lines[i]!) as { url?: unknown }\n if (typeof parsed.url === \"string\") {\n const token = new URL(parsed.url).searchParams.get(\"_token\")\n if (token) return token\n }\n } catch {\n // not JSON — fall through to the raw regex fallback below\n }\n }\n\n const rawMatch = stdout.match(/[?&]_token=([A-Za-z0-9._-]+)/)\n if (rawMatch?.[1]) return rawMatch[1]\n\n throw new Error(\n \"@agentproto/sandbox-box: `box host --private` produced no `_token` in its output.\",\n )\n}\n\n/** Wrap a ready box into the `BootedSandbox` shape common to `boot` and\n * `connect`. `pause()` maps to Box's `stop` (snapshot/archive, resumable);\n * `stop()` fully reaps the box. When gated (`token` present, attach path),\n * the caller authenticates with a `Cookie: _port_auth=<token>` — see\n * `parseBoxHostToken`. */\nfunction toBootedSandbox(api: BoxApi, boxId: string, host: string, token?: string): BootedSandbox {\n return {\n mcpUrl: `https://${host}/mcp`,\n sandboxId: boxId,\n ...(token ? { token, authHeaders: { Cookie: `${PORT_AUTH_COOKIE}=${token}` } } : {}),\n async stop(): Promise<void> {\n // VERIFIED LIVE: `api.remove` alone on a box that isn't actively\n // running acks `{\"status\":\"deleted\"}` but leaves it in state `idle`,\n // STILL counting against the account's `maxActiveBoxes` — a silent leak\n // (a fresh boot then fails the cap). Reaping needs `stop` (snapshot)\n // THEN `remove`; that pair archives it and frees the slot. `stop` is\n // best-effort — an already-stopped box 4xxs and we still want the\n // `remove` — so its failure must not abort the teardown.\n await withBoxRetry(() => api.stop({ boxId })).catch(() => {})\n await withBoxRetry(() => api.remove({ boxId }))\n },\n async pause(): Promise<void> {\n await withBoxRetry(() => api.stop({ boxId }))\n },\n }\n}\n\n/** `boxSandboxProvider.boot`/`.connect` — see module docs. */\nexport const boxSandboxProvider: SandboxProvider = {\n async boot(spec: SandboxSpec, opts: SandboxBootOpts): Promise<BootedSandbox> {\n const config = readBoxConfig(spec)\n const port = config.port ?? DEFAULT_PORT\n const workspace = config.workspace ?? DEFAULT_WORKSPACE\n const api = makeBoxApi()\n\n const created = await api.create({\n createBoxRequest: {\n ttlSeconds: config.ttlSeconds === undefined ? DEFAULT_TTL_SECONDS : config.ttlSeconds,\n env: opts.env,\n },\n })\n const boxId = created.box.id\n\n const box = await waitUntilBoxReady(api, boxId, config)\n if (!box.subdomain) {\n throw new Error(`@agentproto/sandbox-box: box ${boxId} has no assigned subdomain.`)\n }\n const host = `${box.subdomain}-${port}.on.ascii.dev`\n await ensureDaemonHealthy(api, boxId, host, port, workspace, config, opts.env)\n return toBootedSandbox(api, boxId, host)\n },\n\n async connect(boxId: string, spec: SandboxSpec, opts: SandboxBootOpts): Promise<BootedSandbox> {\n const config = readBoxConfig(spec)\n const port = config.port ?? DEFAULT_PORT\n const workspace = config.workspace ?? DEFAULT_WORKSPACE\n const api = makeBoxApi()\n\n // Box's no-auto-stop (the default, DEFAULT_TTL_SECONDS above) is sticky\n // across resumes, unlike e2b's timeout which resets and must be re-armed\n // here — `ResumeRequest` has no ttl field to re-arm even if it didn't.\n await withBoxRetry(() => api.resume({ boxId, resumeRequest: {} }))\n\n if (opts.keepAlive) {\n // `attachSandbox`'s always-on rendezvous model: defensively (re-)pin\n // this EXISTING box to no-auto-stop, in case it was created before\n // DEFAULT_TTL_SECONDS or with an explicit numeric ttlSeconds that\n // would otherwise still let the provider reap it.\n await withBoxRetry(() => api.update({ boxId, updateBoxRequest: { ttlSeconds: null } }))\n }\n\n const box = await waitUntilBoxReady(api, boxId, config)\n if (!box.subdomain) {\n throw new Error(`@agentproto/sandbox-box: box ${boxId} has no assigned subdomain.`)\n }\n const host = `${box.subdomain}-${port}.on.ascii.dev`\n const token = await ensureDaemonHealthy(api, boxId, host, port, workspace, config, opts.env, opts.expose)\n return toBootedSandbox(api, boxId, host, token)\n },\n}\n\n/** Poll `url` until it responds OK, or return false once `timeoutMs` elapses. */\nasync function probeHealth(url: string, timeoutMs: number, pollIntervalMs: number): Promise<boolean> {\n const deadline = Date.now() + timeoutMs\n for (;;) {\n try {\n const res = await fetch(url, { signal: AbortSignal.timeout(HEALTH_FETCH_TIMEOUT_MS) })\n if (res.ok) return true\n } catch {\n // not up yet — fall through to the deadline check\n }\n if (Date.now() >= deadline) return false\n await new Promise(resolve => setTimeout(resolve, pollIntervalMs))\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentproto/sandbox-box",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "@agentproto/sandbox-box — ascii.dev Box `SandboxProvider` for @agentproto/sandbox. Boots a Box cloud computer, installs an always-on systemd unit for the agentproto daemon, and exposes its MCP endpoint as a URL so any AgentSessionHost consumer (worktreeAgentWorkflow, worktree-agent CLI) can run a coding-agent step inside the sandbox.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agentproto",
|
|
7
|
+
"aip-36",
|
|
8
|
+
"sandbox",
|
|
9
|
+
"box",
|
|
10
|
+
"ascii.dev",
|
|
11
|
+
"open-standard",
|
|
12
|
+
"agentic"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://agentproto.sh/docs/aip-36",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/agentproto/ts",
|
|
18
|
+
"directory": "packages/sandbox-box"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/agentproto/ts/issues"
|
|
22
|
+
},
|
|
23
|
+
"license": "Apache-2.0",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "dist/index.mjs",
|
|
26
|
+
"module": "dist/index.mjs",
|
|
27
|
+
"types": "dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.mjs",
|
|
32
|
+
"default": "./dist/index.mjs"
|
|
33
|
+
},
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@asciidev/box-sdk": "^0.0.24",
|
|
46
|
+
"@agentproto/sandbox": "0.2.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^25.6.2",
|
|
50
|
+
"tsup": "^8.5.1",
|
|
51
|
+
"typescript": "^5.9.3",
|
|
52
|
+
"vitest": "^3.2.4",
|
|
53
|
+
"@agentproto/tooling": "0.1.0-alpha.0"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"dev": "tsup --watch",
|
|
57
|
+
"build": "tsup",
|
|
58
|
+
"clean": "rm -rf dist",
|
|
59
|
+
"check-types": "tsc --noEmit",
|
|
60
|
+
"test": "vitest run",
|
|
61
|
+
"test:watch": "vitest"
|
|
62
|
+
}
|
|
63
|
+
}
|