@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 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
- # Temporary Holding Version
1
+ # @skaile/forge-agent-runtime
2
2
 
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.
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 };