@skaile/forge-agent-runtime 0.0.0-stage → 1.1.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/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +123 -2
- package/dist/app-runner-host/index.d.mts +61 -0
- package/dist/app-runner-host/index.d.ts +61 -0
- package/dist/app-runner-host/index.mjs +565 -0
- package/dist/index.d.mts +475 -0
- package/dist/index.d.ts +475 -0
- package/dist/index.mjs +357 -0
- package/dist/shared/forge-agent-runtime.B1LZr0Sl.mjs +538 -0
- package/dist/shared/forge-agent-runtime.DlNAoPjs.d.mts +122 -0
- package/dist/shared/forge-agent-runtime.DlNAoPjs.d.ts +122 -0
- package/package.json +55 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
- New entry `@skaile/forge-agent-runtime/app-runner-host`: `createAppRunnerHost({ rootDir, authenticate, onEvent, appTools })`, an authenticated HTTP host that runs one isolated `omp` agent per request for an app that deploys it privately next to itself. Node-only; it imports no Nuxt, database or settings UI code, and the package root is unchanged.
|
|
6
|
+
- Run lifecycle over HTTP: `POST /runs`, `POST /runs/:id/commands` (the Workspaces commands `prompt`, `cancel`, `reply`, `capability_register`, `capability_deregister`, `capability_result` only), `PATCH /runs/:id/lease`, idempotent `DELETE /runs/:id`. `authenticate` runs before anything is read from a request. An expired lease cancels the turn; a failed `onEvent` terminates the run without replay.
|
|
7
|
+
- Each run has its own scratch workspace, HOME and `.skaile/providers.yml` snapshot, its own `skaile serve` child bound to `127.0.0.1` with a fresh WebSocket token, and an environment of exactly `PATH`, `LANG`, `HOME`, `SKAILE_HOME`, `TMPDIR`, the canonical `SKAILE_AIP_<ID>_KEY` provider keys, `APP_MCP_AUTHORIZATION` and `SKAILE_WS_AUTH_TOKEN`.
|
|
8
|
+
- The child runs harness `omp` with native tools disabled. Its only tools are the host tools `mcp__<mcpName>__*` from one generated app MCP endpoint and the trusted `browserActionNames`, enforced through `tools.host_allowed`. No agent instances and no other MCP servers are declared.
|
|
9
|
+
- Requires `@skaile/workspaces` ^6.11.0 (`tools.host_allowed`).
|
|
10
|
+
|
|
11
|
+
## 1.0.0
|
|
12
|
+
|
|
13
|
+
- Initial release. Extracted from `@skaile/forge-common` 10.x without behaviour change: `AgentProcessManager`, `ProcessManagerAdapter`, the `AgentAdapter` contract, the harness registry, `BaseAgentManager`, `JsonlMessageStore`, `EventEmitterSubscriberTransport` and `FanoutTransport`.
|
|
14
|
+
- `AgentProcessConfig` and `AgentAdapterConfig` gain `host` and `wsAuthToken`. `host` becomes `--host` for `skaile serve` and the client dial address. `wsAuthToken` becomes `SKAILE_WS_AUTH_TOKEN` in the child environment and the client transport's `auth`. Both default to the previous behaviour when unset.
|
|
15
|
+
- Credentials are resolved through `@skaile/forge-settings/server`; the package re-exports no credential helper.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matthias Ortegro
|
|
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,124 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @skaile/forge-agent-runtime
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The agent process lifecycle Skaile forge apps run on. It spawns a `skaile serve`
|
|
4
|
+
subprocess per session, connects to it over WebSocket, and exposes the result
|
|
5
|
+
through the `AgentAdapter` contract, a harness registry and a session manager
|
|
6
|
+
base class.
|
|
7
|
+
|
|
8
|
+
Node-only. The package imports no Nuxt, database, settings UI or Directus code.
|
|
9
|
+
Credential helpers stay in `@skaile/forge-settings/server`; this package calls
|
|
10
|
+
`registryCredentialEnv` from there to deliver provider credentials to the child
|
|
11
|
+
and re-exports nothing from it.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
bun add @skaile/forge-agent-runtime @skaile/forge-settings @skaile/workspaces
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Entry point
|
|
20
|
+
|
|
21
|
+
`@skaile/forge-agent-runtime`
|
|
22
|
+
|
|
23
|
+
| Export | Purpose |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `AgentProcessManager`, `AgentProcessConfig` | Spawn `skaile serve`, connect, send prompt/cancel, shut down. Emits `event:<id>` and `error:<id>`. |
|
|
26
|
+
| `ProcessManagerAdapter`, `AgentAdapter`, `AgentAdapterConfig`, `AgentCommand`, `AgentEvent`, `AgentStatus` | One adapter per session behind the canonical contract. |
|
|
27
|
+
| `createAdapterForHarness`, `registerHarness`, `listRegisteredHarnesses` | Harness name → adapter factory (`omp`, `claude-sdk`, fallback `omp`). |
|
|
28
|
+
| `BaseAgentManager`, `ManagedSession` | Session lifecycle base class. A host implements `buildProcessKey`, `buildAdapterConfig`, `getMessagesDir`, `onSessionCreated` and optionally `onEvent`. |
|
|
29
|
+
| `JsonlMessageStore`, `EventEmitterSubscriberTransport`, `FanoutTransport` | Session persistence and SSE fan-out used by `BaseAgentManager`. |
|
|
30
|
+
|
|
31
|
+
## Private runner options
|
|
32
|
+
|
|
33
|
+
`AgentProcessConfig` and `AgentAdapterConfig` accept two options for hosts that
|
|
34
|
+
must keep the child private:
|
|
35
|
+
|
|
36
|
+
- `host` — address `skaile serve` binds (`--host`) and the client dials. Absent:
|
|
37
|
+
the runtime's default bind and a client dial on `127.0.0.1`.
|
|
38
|
+
- `wsAuthToken` — bearer token the child's WebSocket requires. It reaches the
|
|
39
|
+
child as `SKAILE_WS_AUTH_TOKEN` (the runtime deletes it from its own
|
|
40
|
+
environment at startup, so agent processes never see it) and the client
|
|
41
|
+
transport presents it. Absent: no authentication, as before.
|
|
42
|
+
|
|
43
|
+
## App runner host
|
|
44
|
+
|
|
45
|
+
`@skaile/forge-agent-runtime/app-runner-host` is an HTTP host an app deploys
|
|
46
|
+
privately next to itself. Each request starts one isolated `omp` agent whose only
|
|
47
|
+
tools are the app's own MCP endpoint and a fixed set of browser actions. It
|
|
48
|
+
imports no Nuxt, database or settings UI code.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { createServer } from "node:http";
|
|
52
|
+
import { createAppRunnerHost } from "@skaile/forge-agent-runtime/app-runner-host";
|
|
53
|
+
|
|
54
|
+
const host = createAppRunnerHost({
|
|
55
|
+
rootDir: "/var/lib/runner/runs", // private; one directory per run
|
|
56
|
+
authenticate: async (req) => req.headers.authorization === `Bearer ${process.env.RUNNER_TOKEN}`,
|
|
57
|
+
onEvent: async (runId, event) => {
|
|
58
|
+
// deliver to the app; a rejection terminates the run, events are never replayed
|
|
59
|
+
},
|
|
60
|
+
appTools: {
|
|
61
|
+
mcpName: "planner", // tools reach the model as mcp__planner__*
|
|
62
|
+
mcpUrl: "http://app:3000/mcp",
|
|
63
|
+
browserActionNames: ["app.ui.open_training"],
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
createServer((req, res) => void host.handler(req, res)).listen(8081, "0.0.0.0");
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`appTools` is startup configuration. A request can never name a tool or a
|
|
70
|
+
destination. `host.close()` terminates every run.
|
|
71
|
+
|
|
72
|
+
| Request | Behaviour |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `POST /runs` | Body `{ id, model, registry, credentialEnv, systemPrompt, mcpAuthorization, expiresAt }`. `id` is a UUID, `expiresAt` an ISO-8601 time or epoch milliseconds in the future, `mcpAuthorization` the full `Authorization` header value for the app MCP endpoint. `400` invalid, `409` id already active, `503` the runtime did not start, `201` started. |
|
|
75
|
+
| `POST /runs/:id/commands` | One Workspaces command: `prompt`, `cancel`, `reply`, `capability_register`, `capability_deregister` or `capability_result`. `400` any other type or a malformed field, `404` no such run, `410` lease expired (except `cancel`), `202` sent. `capability_register` accepts only the declared `browserActionNames`. |
|
|
76
|
+
| `PATCH /runs/:id/lease` | Body `{ expiresAt }`. `400` not in the future, `404` no such run, `410` already expired. |
|
|
77
|
+
| `DELETE /runs/:id` | Idempotent `204`. Stops the process and its listener, erases the run's credentials and removes its scratch directory. |
|
|
78
|
+
|
|
79
|
+
`authenticate` is called first. `false` answers `401` and a throw `500`; the body
|
|
80
|
+
is never read in either case. When a lease expires the turn is cancelled, further
|
|
81
|
+
commands answer `410`, and the run is torn down a few seconds later.
|
|
82
|
+
|
|
83
|
+
What a run is:
|
|
84
|
+
|
|
85
|
+
- **Process.** One `ProcessManagerAdapter` and one `skaile serve --harness omp` child per run, bound to
|
|
86
|
+
`127.0.0.1`, requiring a fresh random WebSocket token. The event subscription exists before the first command.
|
|
87
|
+
- **Files.** `<rootDir>/<id>/` holds the workspace (`skaile.yaml`, `agent/agent.yaml`, `.skaile/providers.yml`),
|
|
88
|
+
`home/`, `skaile-home/` and `tmp/`. No secret is written to any of them.
|
|
89
|
+
- **Environment.** Exactly `PATH`, `LANG`, `HOME`, `SKAILE_HOME`, `TMPDIR`, one `SKAILE_AIP_<ID>_KEY` per
|
|
90
|
+
provider, `APP_MCP_AUTHORIZATION` and `SKAILE_WS_AUTH_TOKEN`.
|
|
91
|
+
- **Providers.** Only enabled `llm` entries are kept. Each credential must be an `env:NAME` reference; its value is
|
|
92
|
+
read from `credentialEnv` and handed to the child under the canonical `SKAILE_AIP_<ID>_KEY`, and the written
|
|
93
|
+
reference is rewritten to that name. `cmd:`, `keyring:` and `platform:` references, and `${NAME}` interpolation in
|
|
94
|
+
`base_url`, `headers` and `query_params`, are refused with `400`.
|
|
95
|
+
- **Tools.** Native omp tools are disabled. The host tool allowlist is `mcp__<mcpName>__*` plus the browser action
|
|
96
|
+
names; the app MCP endpoint is the only MCP server and no agent instances are declared. This needs
|
|
97
|
+
`@skaile/workspaces` 6.11 (`tools.host_allowed`).
|
|
98
|
+
|
|
99
|
+
A throwaway proof against the real `omp` binary is in `scripts/proof-app-runner-host.ts`.
|
|
100
|
+
|
|
101
|
+
## Migrating from `@skaile/forge-common/server`
|
|
102
|
+
|
|
103
|
+
Replace the import specifier; names are unchanged.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
// before
|
|
107
|
+
import { AgentProcessManager, BaseAgentManager, JsonlMessageStore } from "@skaile/forge-common/server";
|
|
108
|
+
// after
|
|
109
|
+
import { AgentProcessManager, BaseAgentManager, JsonlMessageStore } from "@skaile/forge-agent-runtime";
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`AgentProcessConfig` is no longer exported from `@skaile/forge-common/types`.
|
|
113
|
+
|
|
114
|
+
## Development
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
bun install
|
|
118
|
+
bun run build
|
|
119
|
+
bun run typecheck
|
|
120
|
+
bun run test
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Releases publish from a `v*` tag through `.github/workflows/publish.yml` (npm
|
|
124
|
+
trusted publishing).
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { AgentEvent } from '../index.mjs';
|
|
3
|
+
import '@skaile/forge-settings/server';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Trusted startup configuration naming what a run may reach. It is never taken
|
|
7
|
+
* from a request: a caller of the host can start and steer runs, it cannot widen
|
|
8
|
+
* what they are allowed to call.
|
|
9
|
+
*/
|
|
10
|
+
interface AppRunnerTools {
|
|
11
|
+
/**
|
|
12
|
+
* Name of the one generated MCP server. Its tools are offered to the model as
|
|
13
|
+
* the host tools `mcp__<mcpName>__<tool>`. Letters, digits, `_` and `-`; no `__`.
|
|
14
|
+
*/
|
|
15
|
+
mcpName: string;
|
|
16
|
+
/** `http(s)` URL of the app's MCP endpoint. */
|
|
17
|
+
mcpUrl: string;
|
|
18
|
+
/**
|
|
19
|
+
* Exact names of the browser-side actions a client may register with
|
|
20
|
+
* `capability_register`. Wildcards are not accepted here.
|
|
21
|
+
*/
|
|
22
|
+
browserActionNames: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
interface AppRunnerHostOptions {
|
|
25
|
+
/** Private directory each run's scratch workspace is created under. */
|
|
26
|
+
rootDir: string;
|
|
27
|
+
/**
|
|
28
|
+
* Called for every request before anything is read from it. `false` answers
|
|
29
|
+
* 401; a throw answers 500. Neither parses the body.
|
|
30
|
+
*/
|
|
31
|
+
authenticate(req: IncomingMessage): Promise<boolean>;
|
|
32
|
+
/**
|
|
33
|
+
* Receives every agent event of a run, in order, one at a time. A rejection
|
|
34
|
+
* terminates the run: events are never replayed.
|
|
35
|
+
*/
|
|
36
|
+
onEvent(runId: string, event: AgentEvent): Promise<void>;
|
|
37
|
+
appTools: AppRunnerTools;
|
|
38
|
+
}
|
|
39
|
+
interface AppRunnerHost {
|
|
40
|
+
/**
|
|
41
|
+
* Request handler for `POST /runs`, `POST /runs/:id/commands`,
|
|
42
|
+
* `PATCH /runs/:id/lease` and `DELETE /runs/:id`. `req.url` is matched from the
|
|
43
|
+
* host root, so a caller that mounts the handler under a prefix strips it first.
|
|
44
|
+
*/
|
|
45
|
+
handler(req: IncomingMessage, res: ServerResponse): Promise<void>;
|
|
46
|
+
/** Terminates every run, erases its credentials and scratch, and refuses new requests. */
|
|
47
|
+
close(): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create the isolated app runner host.
|
|
52
|
+
*
|
|
53
|
+
* One run is one `skaile serve` child (through {@link ProcessManagerAdapter}) with
|
|
54
|
+
* its own scratch workspace, HOME and provider snapshot. The child binds loopback
|
|
55
|
+
* and requires a fresh WebSocket token. Its only tools are the app MCP endpoint
|
|
56
|
+
* and the trusted browser actions, as host tools.
|
|
57
|
+
*/
|
|
58
|
+
declare function createAppRunnerHost(options: AppRunnerHostOptions): AppRunnerHost;
|
|
59
|
+
|
|
60
|
+
export { createAppRunnerHost };
|
|
61
|
+
export type { AppRunnerHost, AppRunnerHostOptions, AppRunnerTools };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { AgentEvent } from '../index.js';
|
|
3
|
+
import '@skaile/forge-settings/server';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Trusted startup configuration naming what a run may reach. It is never taken
|
|
7
|
+
* from a request: a caller of the host can start and steer runs, it cannot widen
|
|
8
|
+
* what they are allowed to call.
|
|
9
|
+
*/
|
|
10
|
+
interface AppRunnerTools {
|
|
11
|
+
/**
|
|
12
|
+
* Name of the one generated MCP server. Its tools are offered to the model as
|
|
13
|
+
* the host tools `mcp__<mcpName>__<tool>`. Letters, digits, `_` and `-`; no `__`.
|
|
14
|
+
*/
|
|
15
|
+
mcpName: string;
|
|
16
|
+
/** `http(s)` URL of the app's MCP endpoint. */
|
|
17
|
+
mcpUrl: string;
|
|
18
|
+
/**
|
|
19
|
+
* Exact names of the browser-side actions a client may register with
|
|
20
|
+
* `capability_register`. Wildcards are not accepted here.
|
|
21
|
+
*/
|
|
22
|
+
browserActionNames: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
interface AppRunnerHostOptions {
|
|
25
|
+
/** Private directory each run's scratch workspace is created under. */
|
|
26
|
+
rootDir: string;
|
|
27
|
+
/**
|
|
28
|
+
* Called for every request before anything is read from it. `false` answers
|
|
29
|
+
* 401; a throw answers 500. Neither parses the body.
|
|
30
|
+
*/
|
|
31
|
+
authenticate(req: IncomingMessage): Promise<boolean>;
|
|
32
|
+
/**
|
|
33
|
+
* Receives every agent event of a run, in order, one at a time. A rejection
|
|
34
|
+
* terminates the run: events are never replayed.
|
|
35
|
+
*/
|
|
36
|
+
onEvent(runId: string, event: AgentEvent): Promise<void>;
|
|
37
|
+
appTools: AppRunnerTools;
|
|
38
|
+
}
|
|
39
|
+
interface AppRunnerHost {
|
|
40
|
+
/**
|
|
41
|
+
* Request handler for `POST /runs`, `POST /runs/:id/commands`,
|
|
42
|
+
* `PATCH /runs/:id/lease` and `DELETE /runs/:id`. `req.url` is matched from the
|
|
43
|
+
* host root, so a caller that mounts the handler under a prefix strips it first.
|
|
44
|
+
*/
|
|
45
|
+
handler(req: IncomingMessage, res: ServerResponse): Promise<void>;
|
|
46
|
+
/** Terminates every run, erases its credentials and scratch, and refuses new requests. */
|
|
47
|
+
close(): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create the isolated app runner host.
|
|
52
|
+
*
|
|
53
|
+
* One run is one `skaile serve` child (through {@link ProcessManagerAdapter}) with
|
|
54
|
+
* its own scratch workspace, HOME and provider snapshot. The child binds loopback
|
|
55
|
+
* and requires a fresh WebSocket token. Its only tools are the app MCP endpoint
|
|
56
|
+
* and the trusted browser actions, as host tools.
|
|
57
|
+
*/
|
|
58
|
+
declare function createAppRunnerHost(options: AppRunnerHostOptions): AppRunnerHost;
|
|
59
|
+
|
|
60
|
+
export { createAppRunnerHost };
|
|
61
|
+
export type { AppRunnerHost, AppRunnerHostOptions, AppRunnerTools };
|