@hachej/boring-execution 0.0.0-stage → 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 +21 -0
- package/README.md +97 -2
- package/dist/aws-code-interpreter.d.ts +149 -0
- package/dist/aws-code-interpreter.js +453 -0
- package/dist/bounded-error.d.ts +8 -0
- package/dist/bounded-error.js +12 -0
- package/dist/contracts.d.ts +46 -0
- package/dist/contracts.js +1 -0
- package/dist/fs-readers.d.ts +7 -0
- package/dist/fs-readers.js +72 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/native.d.ts +8 -0
- package/dist/native.js +1 -0
- package/dist/remote-files-handler.d.ts +21 -0
- package/dist/remote-files-handler.js +215 -0
- package/dist/remote-files-io.d.ts +8 -0
- package/dist/remote-files-io.js +46 -0
- package/dist/remote-files-positional.d.ts +12 -0
- package/dist/remote-files-positional.js +208 -0
- package/dist/remote-files-protocol.d.ts +342 -0
- package/dist/remote-files-protocol.js +113 -0
- package/dist/remote-files-watch.d.ts +29 -0
- package/dist/remote-files-watch.js +154 -0
- package/dist/remote-files.d.ts +17 -0
- package/dist/remote-files.js +238 -0
- package/dist/remote-shell-handler.d.ts +25 -0
- package/dist/remote-shell-handler.js +131 -0
- package/dist/remote-shell-protocol.d.ts +50 -0
- package/dist/remote-shell-protocol.js +163 -0
- package/dist/remote-shell-window.d.ts +14 -0
- package/dist/remote-shell-window.js +82 -0
- package/dist/remote-shell.d.ts +15 -0
- package/dist/remote-shell.js +127 -0
- package/dist/virtual-filesystem.d.ts +67 -0
- package/dist/virtual-filesystem.js +218 -0
- package/dist/virtual-git.d.ts +60 -0
- package/dist/virtual-git.js +116 -0
- package/dist/virtual-sqlite.d.ts +42 -0
- package/dist/virtual-sqlite.js +50 -0
- package/dist/virtual.d.ts +34 -0
- package/dist/virtual.js +277 -0
- package/package.json +94 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Julien Hurault
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,98 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @hachej/boring-execution
|
|
2
|
+
|
|
3
|
+
Optional working environments use Pi's public `FileSystem`, `Shell` and `ExecutionEnv` contracts. `/native` re-exports those contracts. `/contracts` adds acquisition and lease identity without requiring recovery, a shell or a Boring resource reference.
|
|
4
|
+
|
|
5
|
+
## Disposable virtual workspace
|
|
6
|
+
|
|
7
|
+
`/virtual` provides `createVirtualWorkspace`. Install the exact optional peer `just-bash@3.6.0` alongside the declared native peers. Seed absolute paths, acquire a native environment with an absolute cwd, or create a public upstream Bash instance.
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import { createVirtualWorkspace } from '@hachej/boring-execution/virtual';
|
|
11
|
+
import { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';
|
|
12
|
+
|
|
13
|
+
const workspace = createVirtualWorkspace({
|
|
14
|
+
providerId: 'scratch',
|
|
15
|
+
files: { '/repo/note.txt': 'hello\n' },
|
|
16
|
+
});
|
|
17
|
+
const lease = await workspace.acquire({
|
|
18
|
+
operationId: 'open-scratch', input: { cwd: '/repo' },
|
|
19
|
+
}, BACKGROUND_CONTEXT);
|
|
20
|
+
console.log(await workspace.createBash({ cwd: '/repo' }).exec('cat note.txt'));
|
|
21
|
+
await lease.release(BACKGROUND_CONTEXT);
|
|
22
|
+
workspace.dispose();
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
One upstream `InMemoryFs` stores all bytes. Public mutation adapters resolve aliases consistently for native access, Bash and Git. Leases share namespace identity and have independent cwd and cleanup. Releasing a lease closes only its native facade. Disposing the workspace prevents new acquisitions/Bash instances and closes native facades. Already returned raw filesystem and Bash handles remain trusted host-owned handles; disposal does not revoke them or terminate their commands.
|
|
26
|
+
|
|
27
|
+
Native read/write/append, directory access, symlinks and exclusive temporary files are exercised by public tests. Native rename, truncate and durable flush return `not_supported`. The backing refuses move and recursive directory copy. These refusals precede mutation. Hard links refuse with ENOTSUP. Virtual Bash refuses hard-link arguments before stock `ln -f` can remove an existing destination; symbolic links retain the stock command context. Concurrent structural changes to paths remain unqualified; this disposable adapter is not an authoritative publication provider or a POSIX filesystem qualification.
|
|
28
|
+
|
|
29
|
+
The native shell runs Pi's stock bash tool with buffered output: `onOutput` receives the combined stdout and stderr once when the command ends (never as a live stream), spill thresholds are accepted but nothing is spilled to a file, and `timeout` (seconds) aborts the command and returns `timeout`. Use public Bash `exec` for separate buffered stdout/stderr. Host Bash options select upstream limits and explicit network/custom commands. No native executable fallback or network transport is added. Host callbacks are trusted code.
|
|
30
|
+
|
|
31
|
+
Errors are bounded in one place (`boundedMessage` in `src/bounded-error.ts`, also used by the AWS Code Interpreter environment below): every error the native environment, the `git` command or a `fs` backing passes on toward a tool result is its message only, with stack-frame lines dropped even when a backing put them in the message, and at most `MAX_ERROR_CHARS` (2,000, exported from `/virtual`) characters. The whole error stays on `cause` for host logs. Command output itself is bounded by Pi's bash tool (its tail, 50 KB or 2,000 lines).
|
|
32
|
+
|
|
33
|
+
### Python (opt-in, Node only)
|
|
34
|
+
|
|
35
|
+
`createVirtualWorkspace({ providerId, python: true })` adds `python3` and `python` to every shell of the workspace (the native `exec`'s included): just-bash's CPython 3.13 compiled to WebAssembly, with the standard library, reading and writing the same files as the shell. It is off by default and is the only way to turn it on (shell options cannot). Limits: Node only, so it throws a clear `TypeError` in a browser or its workers; the runtime (about 10 MB, shipped inside `just-bash`) loads on the first call (about a second here; later calls about half a second); its memory is not contained by the shell's execution limits, so a script can grow the host process; there is no pip, package installation or network. A Python traceback is the script's own output and passes through unchanged.
|
|
36
|
+
|
|
37
|
+
## Optional local Git
|
|
38
|
+
|
|
39
|
+
`/virtual-git` requires `isomorphic-git@1.42.6` and `@hachej/boring-files/git`. `createVirtualGitFs(workspace.filesystem)` supplies its public promise filesystem. The host creates one `createGitRepository({ fs, directory, author, authorize })`, then calls `installVirtualGitCommand({ bash, repository })`. Installation verifies the actual public `bash.fs` identity. Each command requires the selected root cwd. Native tools receive the same repository instance.
|
|
40
|
+
|
|
41
|
+
The command supports local `init`, matrix `status`, single-path `add`, index-only `rm --cached`, `commit -m`, bounded `log`, `branch`, `checkout`, and binary-content JSON `diff` or `diff --cached`. Other argument forms return exit 126. [The files guide](../files/README.md) owns authorization, serialization and diff semantics. No remote transport, durable recovery or publication transaction is claimed. Raw `.git` writes remain subject to the selected filesystem's host policy.
|
|
42
|
+
|
|
43
|
+
Run `node examples/virtual-workspace.mjs` after building for a fictional shared native/Bash/Git example. `test/packages/execution.test.mjs` exercises public output and actual native ToolTasks. `npm run test:virtual-consumer` installs the tarball and exact registry dependency closure outside the repository, checks declarations and repeats those tests. Set `npm_config_cache` to a writable cache containing the pinned archives.
|
|
44
|
+
|
|
45
|
+
## Borrowed remote Shell
|
|
46
|
+
|
|
47
|
+
`/remote-shell` exports paired `createRemoteShellHandler` and `createRemoteShellLease` Fetch adapters over native Pi `Shell`. This subpath needs only the pinned native peers. It does not load virtual Bash, Git, Boring files or an agent implementation. The host authenticates each POST, enforces origin/CSRF policy, selects an already acquired workspace, and provides its native Shell, Context, exact identity, revocation signal and command authorization callback. Authorization must return `true` and runs before execution and terminal disclosure. Abort the revocation signal when access or workspace identity changes, including during silent commands.
|
|
48
|
+
|
|
49
|
+
The client takes that expected identity, a fixed HTTP(S) endpoint and an explicit authenticated Fetch function. The transport must honor `redirect: 'error'` and must not retry command requests. Both ends bind provider, instance, incarnation and selected view. The server checks identity before effects. The client checks identity before delivering output, validates ordered request-bound frames and requires exactly one terminal result followed by EOF. A lost or malformed acknowledgement returns native `ExecutionError('unknown')`; the client never replays the command. A request identifier correlates frames, not a durable idempotency or recovery record.
|
|
50
|
+
|
|
51
|
+
Command strings, cwd, environment and native exit/error results pass through (protocol version 2). Output callbacks receive live combined text and the caller's Context. Pi's `window` option is forwarded to the worker's native Shell, and `skipped` counts on its output chunks are relayed in the output frames (validated, never invented), The handler also applies the window itself (`remote-shell-window.ts`): it keeps the retained tail, flushes no faster than `minIntervalMs`, and reports what it dropped as merged `skipped` counts, so a worker that ignores the window (Pi's Node environment) still transfers only the tail. Counts a windowing worker already reports are merged, not recomputed. Without `window`, every chunk is delivered. Exit-only calls do not request output frames. Positive fractional timeout values use native **seconds**, with the provider responsible for its additional limits. Hosts explicitly qualify timeout and spill support; unsupported requested features refuse before execution. Spill paths remain in the remote workspace and need a separately authorized file capability to retrieve them. No dummy filesystem is supplied.
|
|
52
|
+
|
|
53
|
+
Transport defaults are 64 KiB per request, 1 MiB per frame and 4 MiB of queued server frames. Oversized output or an overfull queue interrupts observation rather than reporting truncated success. These bounds do not cap native output production or Fetch/provider peak memory. Host native execution limits remain necessary. Local cancellation, callback failure or lease release requests remote cancellation and reports unconfirmed termination; prior effects can remain. Each borrowed lease owns only its active transports, and never calls the shared provider's cleanup.
|
|
54
|
+
|
|
55
|
+
Run `node examples/remote-shell.mjs` for an actual native command through in-process Request/Response streaming. `npm run test:remote-shell-consumer` installs only the execution tarball and pinned native registry closure, checks strict declarations and repeats public native tests without the virtual/Git/Boring UI/agent peers. The handler reads request bodies through `@hachej/boring-files/request-guard`, so install `@hachej/boring-files` (an optional peer) with this subpath; the consumer installs its archive. This establishes the paired Fetch protocol and selected native behavior. Socket deployment, vendor provisioning/reattachment, durable command admission/recovery, independent file grants, protected builds and process-tree termination remain separate qualifications.
|
|
56
|
+
|
|
57
|
+
## Borrowed remote FileSystem
|
|
58
|
+
|
|
59
|
+
`/remote-files` exports `createRemoteFileSystemHandler` and `createRemoteFileSystemLease` over native Pi `FileSystem`. Install the exact optional peer `zod@4.6.5` with the declared native peers. This entry does not require a shell, virtual Bash, Git, Boring files, UI or an agent implementation.
|
|
60
|
+
|
|
61
|
+
The host authenticates each POST and selects an already acquired workspace. Its `bindFileSystem(cwd, context)` returns a `WorkspaceLease<FileSystem>` over that namespace with the requested cwd. The adapter checks the actual lease identity, native filesystem ID and cwd before execution and disclosure. Bind distinct facades for concurrent incompatible cwd values. Binding must not provision or silently recreate a workspace. The handler releases each bound lease after its operation, or after closing its native line reader. It never invokes shared provider cleanup.
|
|
62
|
+
|
|
63
|
+
`authorize(call, cwd)` receives a discriminated native method/argument tuple and must return literal `true`. The handler checks authorization before and after each operation and stream read. Abort the host revocation signal when authorization or workspace identity changes, including while a reader is idle. Authentication, origin/CSRF policy, path and symlink confinement, deadlines and provider limits remain host responsibilities. Native temporary-file operations retain provider semantics, which can place files outside cwd. A cwd check alone is not a path grant.
|
|
64
|
+
|
|
65
|
+
The client captures cwd and arguments for each invocation. Native path, binary, metadata, text and mutation results pass through validated request-bound envelopes. Native errors retain their code, message and optional path. A lost, malformed or post-dispatch cancelled acknowledgement returns `FileError('unknown')` and can follow prior effects. There is no automatic retry, rollback or durable deduplication. The request ID correlates the response only. Authoritative published resources use their separate conditional publication contract.
|
|
66
|
+
|
|
67
|
+
`openTextLineReader` retains one streamed native reader. Frames preserve strict LF splitting and each line's `terminated` flag. A cancelled `readLine` wait leaves its pending frame available for the next call. Cancelling the original open context after successful opening does not close the reader. Concurrent reads refuse. Explicit close or lease release cancels local observation; server cleanup closes the reader before releasing its facade. Client close does not wait for a remote cleanup acknowledgement or prove remote termination.
|
|
68
|
+
|
|
69
|
+
Pi's `openBinaryReader` and `openDirReader` keep a handle open, which a stateless request cannot. The client therefore turns each reader call into one request-bound call (`binaryInfo`, `readRange`, `scanLines`, `readDirPage`) that opens the worker's own native reader, answers a bounded range, line scan or directory page, and closes it. `scanLines` runs on the worker, so locating lines in a large file does not move the file; `read` is split into responses that fit the response limit, and a range above the worker's limit is refused. The client records the opened file's `size` and `mtimeMs` and sends them with every call; the worker checks them before and after the read and fails with `FileError('invalid')` when the file changed (Pi's `FileInfo` has no inode, so a same-size, same-mtime replacement cannot be told apart). Directory pages continue by entry offset over the native reader and are checked against the directory's `mtimeMs`; a directory that changed between pages fails instead of skipping or repeating entries. Native open errors (`not_found`, `is_directory`, `not_directory`, `invalid` for `noFollow` links) keep their codes. These calls pass through `authorize` like every other method. Only environments without positional reads (the in-memory virtual file system) use `snapshotReaders` in `fs-readers.ts`.
|
|
70
|
+
|
|
71
|
+
Known deviations from Pi's reader contract: a reader holds no handle, so renaming or replacing the file after `openBinaryReader` makes later calls fail (`not_found` or `invalid`) instead of reading the originally opened file; the path is resolved to an absolute path once at open, so a later `cwd` change does not affect an open reader; concurrent `next()` calls on one directory reader are refused with `invalid`; an empty not-done directory page is refused (`unknown`) rather than looped on; directory paging costs O(offset) per page on the worker.
|
|
72
|
+
|
|
73
|
+
`watch` is one streamed request: the worker's native `watch` feeds a bounded queue (a full queue, or a frame over the limit, becomes Pi's `overflow`), each change is authorized again before disclosure, and `mode` comes from the native watcher. A terminal `error` is kept apart from the bounded event queue (a full queue collapses events to `overflow`, never the error) and an oversized error has its text bounded; a stream that ends without an `error` change is reported to the listener as an `error` change. An `error` change ends the stream; revocation, a lost stream or lease release deliver one `error` change; `close` (or aborting the stream) closes the native watcher, and no change is delivered after `close` resolves. Establishing the watch fails with the worker's native error, so an environment without native watching reports its own `not_supported`.
|
|
74
|
+
|
|
75
|
+
Defaults are 1 MiB per request, 4 MiB per normal JSON response and 1 MiB per stream frame. Binary values travel as canonical base64 text (protocol version 3; older peers are refused, so upgrade both ends together). Server backpressure queues at most one frame and reads on demand, with a read possibly in flight. These limits bound accepted transport data, not native reader allocation, JSON expansion, Fetch allocation or provider production. Oversized data refuses instead of reporting truncated success.
|
|
76
|
+
|
|
77
|
+
Run `node examples/remote-files.mjs` after building to write and stream a fictional native file without a shell capability. `npm run test:remote-files-consumer` installs the execution and files tarballs (this subpath reads bodies through `@hachej/boring-files/request-guard`) and pinned registry dependencies, checks strict declarations and exercises actual native files, a native ToolTask and reader lifetime races. Its pinned `buffer` fixture dependency keeps undici's bare type import inside the isolated installation when the temporary directory has a parent workspace. In-process Fetch evidence does not qualify deployed sockets, vendor lifecycle, acquisition recovery, protected builds or complete exposure policy.
|
|
78
|
+
|
|
79
|
+
## Virtual repository saved in SQLite
|
|
80
|
+
|
|
81
|
+
`createVirtualWorkspace({ providerId, fs })` keeps the files in a Pi `FileSystem` instead of memory, for example the SQLite workspace backend `openSqliteFileSystem` of `@hachej/boring-files/sqlite-filesystem`: the shell, isomorphic-git and the native environment then work on the very rows a workspace provider over the same `fs` reads and writes (one view, no copy). That backend has regular files and directories only: `ln` is refused, `chmod` changes nothing (files report 0644, directories 0755) and `touch` sets the modification time to now.
|
|
82
|
+
|
|
83
|
+
`@hachej/boring-execution/virtual-sqlite` is a git repository on such a file system: `openVirtualRepository({ fs, root?, seed?, seedMessage?, author?, context, providerId? })` returns `{ root, env, repository, close() }`. `fs` is the SQLite file system over `openBrowserSqliteConnection` (the private, unpublished `@hachej/boring-browser/sqlite`, monorepo-only; supply your own browser `SqliteConnection` otherwise) in a browser tab, or over `openNodeConnection` (`@hachej/boring-files/sqlite`) in Node. It composes the pieces above and adds no file or shell API of its own (BORING-PI-6): read, list, write and run commands through `env`, Pi's native `ExecutionEnv`, whose `exec` shell has the `git` command bound to `repository` (`@hachej/boring-files/git`) on the same files (the stock virtual environment creates a shell without it). Every write lands in SQLite as it happens; there is nothing to save, and `close()` only releases the workspace. When `root` has no `.git` yet it is filled from `seed` (paths relative to `root`) and committed; otherwise the files and history are the stored ones.
|
|
84
|
+
|
|
85
|
+
## AWS Code Interpreter environment
|
|
86
|
+
|
|
87
|
+
`@hachej/boring-execution/aws-code-interpreter` needs the exact optional peer `@aws-sdk/client-bedrock-agentcore@3.1146.0`; the host passes its own `BedrockAgentCoreClient` (region, credentials, retries). It makes no AWS call except the session start, the command tools and `stop()`, and creates no AWS resource. The recipe that uses it is [examples/aws](../../examples/aws/README.md); the decisions are in [HOST-RECIPE-AWS.md](../../docs/architecture/HOST-RECIPE-AWS.md).
|
|
88
|
+
|
|
89
|
+
`createCodeInterpreterEnv({ client, codeInterpreterIdentifier, session, id, mount, cwd?, umask?, pollIntervalMs?, renew?, rotateMarginSeconds? })` returns `{ env, owned, sessionId(), lost(), renew(), stop(context) }`. `env` is one native Pi `ExecutionEnv` over one folder that both this host and the Code Interpreter session mount (an EFS access point): `mount.path` is where the session sees it (`/mnt/<name>`), `mount.root` where this host does. Tool paths are session paths, so commands and file tools name the same file.
|
|
90
|
+
|
|
91
|
+
- **Commands** run through `startCommandExecution`, then `getTask` every `pollIntervalMs` (500 ms by default) until a terminal status. The cwd, environment (`env`, or `env -i` for `inheritEnv: false`) and `umask` (default `002`) are prefixed to the command, which runs under `bash -c`. `onOutput` receives the new part of stdout, then of stderr, after each poll; spill thresholds are accepted and nothing is spilled. `timeout` (seconds) and an aborted context call `stopTask` and return `timeout`/`aborted`, termination unconfirmed. A `failed` status with an exit code is a normal result.
|
|
92
|
+
- **Files** run on this host's mount through Pi's `NodeExecutionEnv`: no bytes cross the API. A path outside `mount.path`, or one whose resolution (symbolic links included, dangling ones followed by hand) leaves `mount.root`, returns `FileError('permission_denied')`; operations that do not follow a final link (`fileInfo`, `exists`, `remove`, `renameFile`) check its directory. Paths in results and errors are session paths. `createTempDir`/`createTempFile` return `not_supported` (a temporary file here would be invisible to the session). The check and the use are separate steps: a link changed in between by a concurrent command is not caught.
|
|
93
|
+
- **Session**: `{ sessionId }` borrows a running session, which `stop()` never ends; `{ start: { name?, sessionTimeoutSeconds?, filesystemConfigurations? } }` starts one on the first command and `stop()` ends it once. `env.cleanup()` never stops a session. When the service reports the session gone (`ResourceNotFoundException`), the running and every later command return `shell_unavailable` and `lost()` is true; files stay. `renew: 'auto'` (default for started sessions; borrowed ones are always manual) starts a new session on the NEXT command after a loss: the failing command still returns `shell_unavailable` and is never replayed, since it may have had side effects. Before a command, a session older than `sessionTimeoutSeconds - rotateMarginSeconds` (default 60 s, at most half the time to live) is replaced for NEW commands. Rotation never stops the old session: a command stays on the session it started in (an `unknown` outcome after a transport failure stays unknown; nothing kills or replays it) and the old session lingers until its own `sessionTimeoutSeconds`, so hosts that care about idle sessions set a shorter timeout. Starts are single-flight (commands arriving during a rotation share the one new session); a failed Start leaves commands on the current session and backs off. `stop()` stops the current session and every session this env started and has not confirmed stopped, and waits for them; a failed stop stays tracked, rejects, and is retried by the next `stop()`. `renew: 'manual'` keeps the earlier behaviour: only `renew()` (started sessions only) lets the next command start a new session. Loss is `ResourceNotFoundException` plus the names and message patterns in the exported `SESSION_ENDED_ERRORS` (for example a `ValidationException` about a terminated session): those extra shapes are UNVERIFIED against real AgentCore (open question), and `AccessDeniedException` is never loss. Service and file errors map to `ExecutionError`/`FileError` with messages bounded by the same `boundedMessage` (no stack frames, at most `MAX_ERROR_CHARS`).
|
|
94
|
+
|
|
95
|
+
`efsUserLayout({ userId, uid, gid?, runtimeMountPath?, runtimeAccessPointRoot?, interpreterMountPath? })` is pure: for a user id (`[a-z0-9][a-z0-9_-]{0,63}`, never a path) and a host-allocated uid it returns the access point to create (root `/boring/users/<id>`, POSIX user `uid:1000`, setgid `2770`, a `boring:user` tag), the session mount (`/mnt/workspace`), this host's folder (`/mnt/efs/users/<id>`), the state directory outside it (`/mnt/efs/state/<id>`, with `journal.sqlite` and `harnessFile(key)`), the namespace id and the `filesystemConfigurations` entry.
|
|
96
|
+
|
|
97
|
+
`test/packages/execution.test.mjs` drives the real SDK client against `examples/aws/fake-code-interpreter.mjs` (the wire format, without AWS): shared files, output, exit codes, cwd and environment, timeout and abort through `stopTask`, a lost session (auto renewal mid-turn and under concurrent commands, rotation before the time to live without stopping the old session, a stop that fails once and is retried, manual mode, borrowed sessions), borrowed versus owned stop, and two users who never see each other's folders (`..`, absolute paths, links and dangling links made by a command). Nothing has run against the real service; see the open questions in HOST-RECIPE-AWS.md.
|
|
2
98
|
|
|
3
|
-
This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { BedrockAgentCoreClient, ToolsFileSystemConfiguration } from '@aws-sdk/client-bedrock-agentcore';
|
|
2
|
+
import type { Context } from '@earendil-works/chord';
|
|
3
|
+
import type { ExecutionEnv } from '@earendil-works/pi-durable/env';
|
|
4
|
+
/** What the adapter sends: the host's own `BedrockAgentCoreClient` (it owns region, credentials and retries). */
|
|
5
|
+
export type CodeInterpreterClient = Pick<BedrockAgentCoreClient, 'send'>;
|
|
6
|
+
/** Options of a session the adapter starts on the first command, and stops only through `stop()`. */
|
|
7
|
+
export interface CodeInterpreterStart {
|
|
8
|
+
readonly name?: string;
|
|
9
|
+
/** Session time to live in seconds: AgentCore default 900, maximum 28 800. */
|
|
10
|
+
readonly sessionTimeoutSeconds?: number;
|
|
11
|
+
/** The per-user EFS access point (`efsUserLayout(...).filesystemConfiguration(...)`); mounts at session start. */
|
|
12
|
+
readonly filesystemConfigurations?: readonly ToolsFileSystemConfiguration[];
|
|
13
|
+
}
|
|
14
|
+
export interface CodeInterpreterEnvOptions {
|
|
15
|
+
readonly client: CodeInterpreterClient;
|
|
16
|
+
/** A custom Code Interpreter in VPC network mode (an EFS mount needs it). */
|
|
17
|
+
readonly codeInterpreterIdentifier: string;
|
|
18
|
+
/** Borrow a running session (`{ sessionId }`: never stopped here) or own one (`{ start }`: started on the first command). */
|
|
19
|
+
readonly session: {
|
|
20
|
+
readonly sessionId: string;
|
|
21
|
+
} | {
|
|
22
|
+
readonly start: CodeInterpreterStart;
|
|
23
|
+
};
|
|
24
|
+
/** The file namespace id: equal ids see the same files (for example `efsUserLayout(...).namespaceId`). */
|
|
25
|
+
readonly id: string;
|
|
26
|
+
/**
|
|
27
|
+
* The one folder both sides see: `path` is where the interpreter session mounts it (`/mnt/<name>`), `root` is the same
|
|
28
|
+
* folder on this host (the runtime's own mount of the file system). Tool paths are interpreter paths; file operations
|
|
29
|
+
* run on `root`, refusing any path or symbolic link that leaves it.
|
|
30
|
+
*/
|
|
31
|
+
readonly mount: {
|
|
32
|
+
readonly path: string;
|
|
33
|
+
readonly root: string;
|
|
34
|
+
};
|
|
35
|
+
/** Working directory, an interpreter path inside `mount.path` (default `mount.path`). */
|
|
36
|
+
readonly cwd?: string;
|
|
37
|
+
/** Prefixed to every command: `umask 002` keeps files group-writable for the runtime (see efsUserLayout). */
|
|
38
|
+
readonly umask?: string;
|
|
39
|
+
/** Delay between `getTask` polls while a command runs (default 500 ms). */
|
|
40
|
+
readonly pollIntervalMs?: number;
|
|
41
|
+
/**
|
|
42
|
+
* What happens after a started session is lost or nearly expired. `'auto'` (default for `{ start }`): the failing
|
|
43
|
+
* command still returns `shell_unavailable` (it is never replayed: it may have had side effects) and the NEXT command
|
|
44
|
+
* starts a new session; a session older than `sessionTimeoutSeconds - rotateMarginSeconds` is replaced before the next
|
|
45
|
+
* command. Rotation only switches NEW commands: a command stays on the session it started in, and the old session is
|
|
46
|
+
* never stopped by rotation (it lingers until its own `sessionTimeoutSeconds`; a host that cares sets a shorter one).
|
|
47
|
+
* `'manual'`: only `renew()` starts a new session. A borrowed session is always manual.
|
|
48
|
+
*/
|
|
49
|
+
readonly renew?: 'auto' | 'manual';
|
|
50
|
+
/** Seconds before the session time to live at which `'auto'` rotates (default 60; at most half the time to live). */
|
|
51
|
+
readonly rotateMarginSeconds?: number;
|
|
52
|
+
}
|
|
53
|
+
export interface CodeInterpreterEnv {
|
|
54
|
+
/** Pi's native environment: files on the shared mount, commands in the interpreter session. */
|
|
55
|
+
readonly env: ExecutionEnv;
|
|
56
|
+
/** Whether `stop()` ends the session (a started session) or leaves it to its owner (a borrowed one). */
|
|
57
|
+
readonly owned: boolean;
|
|
58
|
+
/** The session commands run in, once known. */
|
|
59
|
+
sessionId(): string | undefined;
|
|
60
|
+
/** True once the service reported the session gone (expired or stopped); commands then fail with `shell_unavailable`. */
|
|
61
|
+
lost(): boolean;
|
|
62
|
+
/** A started session only: after a loss, the next command starts a new session. Returns false for a borrowed session. */
|
|
63
|
+
renew(): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Stops every session this env started and has not confirmed stopped (the current one and any rotated away from),
|
|
66
|
+
* and waits for them. A failed stop stays tracked, rejects, and is retried by the next `stop()`. A borrowed session is
|
|
67
|
+
* left running. `env.cleanup` never stops a session.
|
|
68
|
+
*/
|
|
69
|
+
stop(context: Context): Promise<void>;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Error names and messages, besides `ResourceNotFoundException`, taken to mean "the session ended".
|
|
73
|
+
* UNVERIFIED against the real AgentCore service (open question in HOST-RECIPE-AWS.md): they are the likely shapes of an
|
|
74
|
+
* expired or terminated session. `AccessDeniedException` is deliberately NOT here: it is a permissions problem and
|
|
75
|
+
* renewing would not fix it. Matching needs both a listed name and a message pattern.
|
|
76
|
+
*/
|
|
77
|
+
export declare const SESSION_ENDED_ERRORS: readonly {
|
|
78
|
+
readonly name: string;
|
|
79
|
+
readonly message: RegExp;
|
|
80
|
+
}[];
|
|
81
|
+
/**
|
|
82
|
+
* A native Pi `ExecutionEnv` over an AgentCore Code Interpreter session whose sandbox mounts the same folder this host
|
|
83
|
+
* mounts. Commands run in the session through the task API (`startCommandExecution`, `getTask`, `stopTask`); file
|
|
84
|
+
* operations run on this host's mount of the folder, so the agent's file tools, the workspace provider and the commands
|
|
85
|
+
* see the same bytes without copying them through the API. Nothing here creates AWS resources or credentials.
|
|
86
|
+
*/
|
|
87
|
+
export declare function createCodeInterpreterEnv(options: CodeInterpreterEnvOptions): CodeInterpreterEnv;
|
|
88
|
+
export interface EfsUserLayoutOptions {
|
|
89
|
+
/** The host's user id: lowercase letters, digits, `-` and `_`, at most 64 characters (never a path). */
|
|
90
|
+
readonly userId: string;
|
|
91
|
+
/** The user's POSIX uid, allocated by the host (1001..2147483646, unique per user). */
|
|
92
|
+
readonly uid: number;
|
|
93
|
+
/** The group shared by the runtime and every user's access point (default 1000): files stay writable by both. */
|
|
94
|
+
readonly gid?: number;
|
|
95
|
+
/** Where this host mounts the runtime's access point (default `/mnt/efs`; AgentCore Runtime requires `/mnt/<name>`). */
|
|
96
|
+
readonly runtimeMountPath?: string;
|
|
97
|
+
/** The runtime access point's root directory in the file system (default `/boring`): user folders are below it. */
|
|
98
|
+
readonly runtimeAccessPointRoot?: string;
|
|
99
|
+
/** Where each interpreter session mounts the user's access point (default `/mnt/workspace`). */
|
|
100
|
+
readonly interpreterMountPath?: string;
|
|
101
|
+
}
|
|
102
|
+
export interface EfsUserLayout {
|
|
103
|
+
readonly userId: string;
|
|
104
|
+
/** The file namespace id for `createCodeInterpreterEnv`. */
|
|
105
|
+
readonly namespaceId: string;
|
|
106
|
+
/** The user's access point, as `elasticfilesystem:CreateAccessPoint` takes it (the host creates it once per user). */
|
|
107
|
+
readonly accessPoint: {
|
|
108
|
+
readonly rootDirectory: string;
|
|
109
|
+
readonly posixUser: {
|
|
110
|
+
readonly uid: number;
|
|
111
|
+
readonly gid: number;
|
|
112
|
+
};
|
|
113
|
+
readonly creationInfo: {
|
|
114
|
+
readonly ownerUid: number;
|
|
115
|
+
readonly ownerGid: number;
|
|
116
|
+
readonly permissions: string;
|
|
117
|
+
};
|
|
118
|
+
readonly tags: readonly {
|
|
119
|
+
readonly key: string;
|
|
120
|
+
readonly value: string;
|
|
121
|
+
}[];
|
|
122
|
+
};
|
|
123
|
+
/** The interpreter's view: the user's folder is the whole mount. */
|
|
124
|
+
readonly interpreter: {
|
|
125
|
+
readonly mountPath: string;
|
|
126
|
+
readonly cwd: string;
|
|
127
|
+
};
|
|
128
|
+
/** The runtime's view of the same folder, and the user's state directory outside it (harness and journal SQLite files). */
|
|
129
|
+
readonly runtime: {
|
|
130
|
+
readonly root: string;
|
|
131
|
+
readonly state: string;
|
|
132
|
+
readonly journal: string;
|
|
133
|
+
};
|
|
134
|
+
/** The single-writer harness file of one conversation key (`[a-z0-9_-]{1,64}`). */
|
|
135
|
+
harnessFile(conversationKey: string): string;
|
|
136
|
+
/** `filesystemConfigurations` entry for StartCodeInterpreterSession. */
|
|
137
|
+
filesystemConfiguration(arns: {
|
|
138
|
+
readonly accessPointArn: string;
|
|
139
|
+
readonly fileSystemArn: string;
|
|
140
|
+
}): ToolsFileSystemConfiguration;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* One EFS file system, one folder per user below the runtime's access point (root `/boring`, mounted at `/mnt/efs`):
|
|
144
|
+
* `users/<id>` (the interpreter's whole view, through the user's own access point rooted there) and `state/<id>` (the
|
|
145
|
+
* runtime's SQLite files, which no interpreter can reach). The runtime confines itself to the user's folders; the
|
|
146
|
+
* interpreter is confined by the access point itself.
|
|
147
|
+
*/
|
|
148
|
+
export declare function efsUserLayout(options: EfsUserLayoutOptions): EfsUserLayout;
|
|
149
|
+
//# sourceMappingURL=aws-code-interpreter.d.ts.map
|