@skaile/forge-agent-runtime 1.0.0 → 1.1.1
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/README.md +58 -0
- 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 +586 -0
- package/dist/index.d.mts +6 -124
- package/dist/index.d.ts +6 -124
- package/dist/index.mjs +11 -535
- 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 +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.1
|
|
4
|
+
|
|
5
|
+
- `app-runner-host`: a run whose `skaile serve` child exits, or whose connection is found dead on a command (`410`), is torn down immediately: credentials erased, scratch directory removed, listener closed, later requests `404`. The final error event is still delivered to `onEvent`. Before, the run stayed registered until its lease expired.
|
|
6
|
+
- `POST /runs` answers `400` when `model` does not name an enabled `llm` provider of the snapshot (`<providerId>/<modelId>`), before any directory or process is created. Before it surfaced as an opaque `503` after startup.
|
|
7
|
+
- Native tools stay disabled through `tools.allowed: ["WebFetch"]`, because the Workspaces schema has no explicit empty form; a test now asserts that `buildOmpToolArgs` turns the written agent definition into `--no-tools`.
|
|
8
|
+
- Internal: the registry shape is validated once (`requireRegistryShape`) and `StartRunRequest.registry` is typed; routing uses one method table; the provider snapshot throws a domain error that the host maps to `400` instead of depending on the HTTP request module.
|
|
9
|
+
|
|
10
|
+
## 1.1.0
|
|
11
|
+
|
|
12
|
+
- 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.
|
|
13
|
+
- 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.
|
|
14
|
+
- 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`.
|
|
15
|
+
- 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.
|
|
16
|
+
- Requires `@skaile/workspaces` ^6.11.0 (`tools.host_allowed`).
|
|
17
|
+
|
|
3
18
|
## 1.0.0
|
|
4
19
|
|
|
5
20
|
- 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`.
|
package/README.md
CHANGED
|
@@ -40,6 +40,64 @@ must keep the child private:
|
|
|
40
40
|
environment at startup, so agent processes never see it) and the client
|
|
41
41
|
transport presents it. Absent: no authentication, as before.
|
|
42
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, including a `model` whose `<providerId>` is not an enabled `llm` entry of the `registry` (refused before any directory or process exists), `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`) or the run's process has ended, `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. A run whose child process exits (or whose connection is found dead) is torn down at once: credentials erased, scratch removed, later requests answer `404`.
|
|
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
|
+
|
|
43
101
|
## Migrating from `@skaile/forge-common/server`
|
|
44
102
|
|
|
45
103
|
Replace the import specifier; names are unchanged.
|
|
@@ -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 };
|