@stim-cli/agent-driver 0.0.0 → 1.22.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 +61 -1
- package/dist/index.d.mts +147 -0
- package/dist/index.mjs +253 -0
- package/package.json +27 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Janic Duplessis
|
|
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,63 @@
|
|
|
1
1
|
# @stim-cli/agent-driver
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The contract stim-server uses to start the tool a client's coding agent drives
|
|
4
|
+
hosted apps with. stim-server ships the first-party drivers as dependencies and
|
|
5
|
+
selects one with the hosting Mac's `hosting.agentDriver` setting:
|
|
6
|
+
[`@stim-cli/agent-driver-agent-device`](../agent-driver-agent-device/README.md).
|
|
7
|
+
|
|
8
|
+
Drivers are machine scope. stim-server resolves them from its own
|
|
9
|
+
dependencies, never from a project.
|
|
10
|
+
|
|
11
|
+
## Writing a driver
|
|
12
|
+
|
|
13
|
+
A driver package exports:
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
export const apiVersion = 1;
|
|
17
|
+
export const id = 'my-tool';
|
|
18
|
+
export function createAgentDriver({ env, stateDir, claimRoot, device }) {
|
|
19
|
+
return driver;
|
|
20
|
+
}
|
|
21
|
+
export const client = { configFile: 'my-tool-remote.json', access };
|
|
22
|
+
export const actions = { read };
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`createAgentDriver` returns a `HostedAgentDriver` named `id`. stim-server
|
|
26
|
+
authenticates the client, its tailnet node and the session before it calls
|
|
27
|
+
`forward`; the driver confines every forwarded request to the one app or
|
|
28
|
+
device it issued the grant for, and refuses with `AgentDriverUnavailable`
|
|
29
|
+
when it cannot. `device` names the hosted simulator or emulator when the driver
|
|
30
|
+
serves one hosted device session; without it the driver serves hosted macOS
|
|
31
|
+
apps.
|
|
32
|
+
|
|
33
|
+
`client` is optional and runs on the Mac that placed the app. `access` turns a
|
|
34
|
+
grant into the contents of a mode 0600 file and a command that names that file.
|
|
35
|
+
The command never carries the token.
|
|
36
|
+
|
|
37
|
+
`actions` is optional. `read` returns the driver's recorded agent actions in a
|
|
38
|
+
workspace or archive directory as `agent` log records.
|
|
39
|
+
|
|
40
|
+
`loadAgentDriverModule` and `loadAgentDriver` never throw. A failed or slow
|
|
41
|
+
import, a module with another `apiVersion` or `id`, or a factory that throws
|
|
42
|
+
resolves to `{ unavailable }` with a one-line reason.
|
|
43
|
+
|
|
44
|
+
## Contract tests
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
import { runAgentDriverContract } from '@stim-cli/agent-driver';
|
|
48
|
+
|
|
49
|
+
const results = await runAgentDriverContract({
|
|
50
|
+
id: 'my-tool',
|
|
51
|
+
load: () => import('my-tool-driver'),
|
|
52
|
+
unavailableInput: { env: { PATH: '' }, stateDir, claimRoot },
|
|
53
|
+
app: { client: 'client', session, bundleId: 'dev.example.app', pid: 4242 },
|
|
54
|
+
clientGrant,
|
|
55
|
+
});
|
|
56
|
+
for (const result of results) {
|
|
57
|
+
if (!result.passed) throw new Error(`${result.name}: ${result.error}`);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The checks cover the module shape, idempotent `stop` and `revoke`, refusals
|
|
62
|
+
before `start` and without a grant, `AgentDriverUnavailable` when the tool is
|
|
63
|
+
missing, and a client file that keeps the token out of the command.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { HostedAgentIssuedGrant, NdjsonRecord } from "@stim-cli/core/state";
|
|
3
|
+
//#region driver.d.ts
|
|
4
|
+
declare const AGENT_DRIVER_API_VERSION: 1;
|
|
5
|
+
declare const AGENT_DRIVER_LOAD_TIMEOUT_MS = 1e4;
|
|
6
|
+
type HostedAgentDevice = {
|
|
7
|
+
session: string;
|
|
8
|
+
} & ({
|
|
9
|
+
udid: string;
|
|
10
|
+
serial?: never;
|
|
11
|
+
avdName?: never;
|
|
12
|
+
} | {
|
|
13
|
+
serial: string;
|
|
14
|
+
avdName: string;
|
|
15
|
+
udid?: never;
|
|
16
|
+
});
|
|
17
|
+
/** One installed hosted app and its process or owned device. */
|
|
18
|
+
type HostedAgentApp = {
|
|
19
|
+
client: string;
|
|
20
|
+
session: string;
|
|
21
|
+
bundleId: string;
|
|
22
|
+
} & ({
|
|
23
|
+
pid: number;
|
|
24
|
+
udid?: never;
|
|
25
|
+
serial?: never;
|
|
26
|
+
avdName?: never;
|
|
27
|
+
} | (HostedAgentDevice & {
|
|
28
|
+
pid?: never;
|
|
29
|
+
}));
|
|
30
|
+
/**
|
|
31
|
+
* The host side of one driving tool. stim-server's `HostedAgentHost` calls it; a driver never decides which
|
|
32
|
+
* client or session may use it.
|
|
33
|
+
*/
|
|
34
|
+
interface HostedAgentDriver {
|
|
35
|
+
/** The module's `id`. */
|
|
36
|
+
readonly name: string;
|
|
37
|
+
/** Starts the host daemon under an ownership claim whose child is the daemon; idempotent. */
|
|
38
|
+
start(): Promise<void>;
|
|
39
|
+
/** Stops the daemon with identity-checked signals and releases its claim; resolves only once it is gone. */
|
|
40
|
+
stop(): Promise<void>;
|
|
41
|
+
/** Issues a grant scoped to exactly one hosted app. Refuses with `AgentDriverUnavailable` when it cannot scope. */
|
|
42
|
+
issue(app: HostedAgentApp): Promise<HostedAgentIssuedGrant>;
|
|
43
|
+
/** Invalidates a session's grant and lease. */
|
|
44
|
+
revoke(session: string): Promise<void>;
|
|
45
|
+
/** Forwards one request the host already authenticated to the loopback daemon. */
|
|
46
|
+
forward(session: string, request: IncomingMessage, response: ServerResponse): void;
|
|
47
|
+
/** Calls `listener` once for each daemon run that exits without `stop()`; a driver keeps one listener. */
|
|
48
|
+
onExit(listener: () => void): void;
|
|
49
|
+
}
|
|
50
|
+
/** The driver cannot run or cannot scope an app; the message is safe to show a client. */
|
|
51
|
+
declare class AgentDriverUnavailable extends Error {}
|
|
52
|
+
declare const AGENT_ROUTE_PREFIX = "/device-host/agent/";
|
|
53
|
+
declare function agentRoute(session: string): string;
|
|
54
|
+
/** A 256-bit bearer secret in the alphabet `parseHostedAgentGrant` accepts. */
|
|
55
|
+
declare function newAgentToken(): string;
|
|
56
|
+
/**
|
|
57
|
+
* What stim-server hands a driver factory. `stateDir` and `claimRoot` are the driver's own, under the server
|
|
58
|
+
* home for hosted macOS apps or under the session's area for one hosted device, which `device` then names.
|
|
59
|
+
*/
|
|
60
|
+
interface AgentDriverInput {
|
|
61
|
+
env: NodeJS.ProcessEnv;
|
|
62
|
+
stateDir: string;
|
|
63
|
+
claimRoot: string;
|
|
64
|
+
device?: HostedAgentDevice;
|
|
65
|
+
}
|
|
66
|
+
interface AgentClientAccessInput {
|
|
67
|
+
/** A grant naming this driver, as the client parsed it. */
|
|
68
|
+
grant: HostedAgentIssuedGrant;
|
|
69
|
+
/** `https://<host>[:<port>]` followed by the grant's path. */
|
|
70
|
+
baseUrl: string;
|
|
71
|
+
/** Where the client writes `config`, with mode 0600. */
|
|
72
|
+
file: string;
|
|
73
|
+
platform: "macos" | "ios" | "android";
|
|
74
|
+
}
|
|
75
|
+
/** The client's file holding the grant's secret, and the command line that addresses the hosted app through it. */
|
|
76
|
+
interface AgentClientAccess {
|
|
77
|
+
config: string;
|
|
78
|
+
command: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The client side of a driver, run by the Stim CLI on the Mac that placed the app. `access` returns null for a
|
|
82
|
+
* grant it cannot use on `platform`; `command` must name `file` and never carry the token.
|
|
83
|
+
*/
|
|
84
|
+
interface AgentDriverClient {
|
|
85
|
+
/** The file name the client writes in the placement's state directory. */
|
|
86
|
+
configFile: string;
|
|
87
|
+
access(input: AgentClientAccessInput): AgentClientAccess | null;
|
|
88
|
+
}
|
|
89
|
+
/** Reads the driver's recorded agent actions from a workspace or archive directory, as `agent` log records. */
|
|
90
|
+
interface AgentDriverActions {
|
|
91
|
+
read(input: {
|
|
92
|
+
workspaceDir: string;
|
|
93
|
+
}): NdjsonRecord[];
|
|
94
|
+
}
|
|
95
|
+
/** The module shape a driver package exports. The loader rejects any other `apiVersion`. */
|
|
96
|
+
interface AgentDriverModule {
|
|
97
|
+
apiVersion: typeof AGENT_DRIVER_API_VERSION;
|
|
98
|
+
id: string;
|
|
99
|
+
createAgentDriver(input: AgentDriverInput): HostedAgentDriver;
|
|
100
|
+
client?: AgentDriverClient;
|
|
101
|
+
actions?: AgentDriverActions;
|
|
102
|
+
}
|
|
103
|
+
type LoadAgentDriverModuleResult = {
|
|
104
|
+
module: AgentDriverModule;
|
|
105
|
+
} | {
|
|
106
|
+
unavailable: string;
|
|
107
|
+
};
|
|
108
|
+
type LoadAgentDriverResult = {
|
|
109
|
+
driver: HostedAgentDriver;
|
|
110
|
+
} | {
|
|
111
|
+
unavailable: string;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Imports a driver module and checks its shape. Never throws: a failed or slow import, or a module that is not
|
|
115
|
+
* a version 1 driver named `id`, resolves to `{ unavailable }` with a one-line reason.
|
|
116
|
+
*/
|
|
117
|
+
declare function loadAgentDriverModule(id: string, load: () => Promise<unknown>, timeoutMs?: number): Promise<LoadAgentDriverModuleResult>;
|
|
118
|
+
/** Loads a driver module and creates its driver. Never throws; see `loadAgentDriverModule`. */
|
|
119
|
+
declare function loadAgentDriver(id: string, load: () => Promise<unknown>, input: AgentDriverInput, timeoutMs?: number): Promise<LoadAgentDriverResult>;
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region contract.d.ts
|
|
122
|
+
interface AgentDriverContractResult {
|
|
123
|
+
name: string;
|
|
124
|
+
passed: boolean;
|
|
125
|
+
error?: string;
|
|
126
|
+
}
|
|
127
|
+
interface AgentDriverContractOptions {
|
|
128
|
+
/** Imports the driver module, for example `() => import('@stim-cli/agent-driver-x')`. */
|
|
129
|
+
load: () => Promise<unknown>;
|
|
130
|
+
id: string;
|
|
131
|
+
/** Factory input for a host whose driving tool is not installed. */
|
|
132
|
+
unavailableInput: AgentDriverInput;
|
|
133
|
+
/** An app the driver would scope a grant to. */
|
|
134
|
+
app: HostedAgentApp;
|
|
135
|
+
/** A grant this driver issues, for the client checks. Required when the module has a client capability. */
|
|
136
|
+
clientGrant?: HostedAgentIssuedGrant;
|
|
137
|
+
checkTimeoutMs?: number;
|
|
138
|
+
}
|
|
139
|
+
declare const AGENT_DRIVER_CONTRACT_CHECK_TIMEOUT_MS = 1e4;
|
|
140
|
+
/**
|
|
141
|
+
* Loads a driver module the way stim-server does and runs the host-independent checks every driver must pass:
|
|
142
|
+
* shape, idempotent stop and revoke, refusals before start and without a grant, `AgentDriverUnavailable` when
|
|
143
|
+
* its tool is missing, and, with a client capability, a client file that keeps the token out of the command.
|
|
144
|
+
*/
|
|
145
|
+
declare function runAgentDriverContract(options: AgentDriverContractOptions): Promise<AgentDriverContractResult[]>;
|
|
146
|
+
//#endregion
|
|
147
|
+
export { AGENT_DRIVER_API_VERSION, AGENT_DRIVER_CONTRACT_CHECK_TIMEOUT_MS, AGENT_DRIVER_LOAD_TIMEOUT_MS, AGENT_ROUTE_PREFIX, AgentClientAccess, AgentClientAccessInput, AgentDriverActions, AgentDriverClient, AgentDriverContractOptions, AgentDriverContractResult, AgentDriverInput, AgentDriverModule, AgentDriverUnavailable, HostedAgentApp, HostedAgentDevice, HostedAgentDriver, LoadAgentDriverModuleResult, LoadAgentDriverResult, agentRoute, loadAgentDriver, loadAgentDriverModule, newAgentToken, runAgentDriverContract };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { createServer, request } from "node:http";
|
|
3
|
+
//#region driver.ts
|
|
4
|
+
const AGENT_DRIVER_API_VERSION = 1;
|
|
5
|
+
const AGENT_DRIVER_LOAD_TIMEOUT_MS = 1e4;
|
|
6
|
+
/** The driver cannot run or cannot scope an app; the message is safe to show a client. */
|
|
7
|
+
var AgentDriverUnavailable = class extends Error {};
|
|
8
|
+
const AGENT_ROUTE_PREFIX = "/device-host/agent/";
|
|
9
|
+
function agentRoute(session) {
|
|
10
|
+
return `${AGENT_ROUTE_PREFIX}${session}/`;
|
|
11
|
+
}
|
|
12
|
+
/** A 256-bit bearer secret in the alphabet `parseHostedAgentGrant` accepts. */
|
|
13
|
+
function newAgentToken() {
|
|
14
|
+
return randomBytes(32).toString("base64url");
|
|
15
|
+
}
|
|
16
|
+
function isRecord(value) {
|
|
17
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
function reason(error) {
|
|
20
|
+
return (error instanceof Error ? error.message : String(error)).replace(/\n[\s\S]*$/, "").trim() || "unknown error";
|
|
21
|
+
}
|
|
22
|
+
function moduleError(candidate, id) {
|
|
23
|
+
if (!isRecord(candidate) || candidate.apiVersion !== 1) return `expected apiVersion 1, found ${JSON.stringify(isRecord(candidate) ? candidate.apiVersion : candidate)}`;
|
|
24
|
+
if (candidate.id !== id) return `expected id ${JSON.stringify(id)}, found ${JSON.stringify(candidate.id)}`;
|
|
25
|
+
if (typeof candidate.createAgentDriver !== "function") return "the module does not export createAgentDriver()";
|
|
26
|
+
const client = candidate.client;
|
|
27
|
+
if (client !== void 0 && (!isRecord(client) || typeof client.configFile !== "string" || typeof client.access !== "function")) return "the client capability must name configFile and implement access()";
|
|
28
|
+
const actions = candidate.actions;
|
|
29
|
+
if (actions !== void 0 && (!isRecord(actions) || typeof actions.read !== "function")) return "the actions capability must implement read()";
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
function driverError(driver, id) {
|
|
33
|
+
if (!isRecord(driver)) return "createAgentDriver() did not return a driver object";
|
|
34
|
+
if (driver.name !== id) return `the driver is named ${JSON.stringify(driver.name)}, not ${JSON.stringify(id)}`;
|
|
35
|
+
for (const method of [
|
|
36
|
+
"start",
|
|
37
|
+
"stop",
|
|
38
|
+
"issue",
|
|
39
|
+
"revoke",
|
|
40
|
+
"forward",
|
|
41
|
+
"onExit"
|
|
42
|
+
]) if (typeof driver[method] !== "function") return `the driver does not implement ${method}()`;
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Imports a driver module and checks its shape. Never throws: a failed or slow import, or a module that is not
|
|
47
|
+
* a version 1 driver named `id`, resolves to `{ unavailable }` with a one-line reason.
|
|
48
|
+
*/
|
|
49
|
+
async function loadAgentDriverModule(id, load, timeoutMs = AGENT_DRIVER_LOAD_TIMEOUT_MS) {
|
|
50
|
+
let timer;
|
|
51
|
+
const expired = new Promise((resolve) => {
|
|
52
|
+
timer = setTimeout(() => resolve({ unavailable: `the module did not load within ${timeoutMs}ms` }), timeoutMs);
|
|
53
|
+
});
|
|
54
|
+
const loaded = Promise.resolve().then(load).then((namespace) => {
|
|
55
|
+
const candidate = isRecord(namespace) && !("apiVersion" in namespace) && "default" in namespace ? namespace.default : namespace;
|
|
56
|
+
const invalid = moduleError(candidate, id);
|
|
57
|
+
return invalid ? { unavailable: invalid } : { module: candidate };
|
|
58
|
+
}).catch((error) => ({ unavailable: reason(error) }));
|
|
59
|
+
try {
|
|
60
|
+
return await Promise.race([loaded, expired]);
|
|
61
|
+
} finally {
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Loads a driver module and creates its driver. Never throws; see `loadAgentDriverModule`. */
|
|
66
|
+
async function loadAgentDriver(id, load, input, timeoutMs) {
|
|
67
|
+
const loaded = await loadAgentDriverModule(id, load, timeoutMs);
|
|
68
|
+
if ("unavailable" in loaded) return loaded;
|
|
69
|
+
let driver;
|
|
70
|
+
let invalid;
|
|
71
|
+
try {
|
|
72
|
+
driver = loaded.module.createAgentDriver(input);
|
|
73
|
+
if (driver instanceof Promise) {
|
|
74
|
+
driver.catch(() => void 0);
|
|
75
|
+
return { unavailable: "createAgentDriver() must return the driver, not a promise" };
|
|
76
|
+
}
|
|
77
|
+
invalid = driverError(driver, id);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return { unavailable: `createAgentDriver() failed: ${reason(error)}` };
|
|
80
|
+
}
|
|
81
|
+
return invalid ? { unavailable: invalid } : { driver };
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region contract.ts
|
|
85
|
+
const AGENT_DRIVER_CONTRACT_CHECK_TIMEOUT_MS = 1e4;
|
|
86
|
+
const UNKNOWN_SESSION = "00000000-0000-4000-8000-000000000000";
|
|
87
|
+
function assert(condition, message) {
|
|
88
|
+
if (!condition) throw new Error(message);
|
|
89
|
+
}
|
|
90
|
+
async function rejects(call) {
|
|
91
|
+
try {
|
|
92
|
+
await call();
|
|
93
|
+
} catch (error) {
|
|
94
|
+
return error;
|
|
95
|
+
}
|
|
96
|
+
throw new Error("the call resolved");
|
|
97
|
+
}
|
|
98
|
+
function forwardStatus(driver, session, timeoutMs) {
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
const server = createServer((request, response) => {
|
|
101
|
+
try {
|
|
102
|
+
driver.forward(session, request, response);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
finish(() => reject(error));
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
const timer = setTimeout(() => finish(() => reject(/* @__PURE__ */ new Error(`forward() did not answer within ${timeoutMs}ms`))), timeoutMs);
|
|
108
|
+
let done = false;
|
|
109
|
+
function finish(settle) {
|
|
110
|
+
if (done) return;
|
|
111
|
+
done = true;
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
server.closeAllConnections();
|
|
114
|
+
server.close(() => settle());
|
|
115
|
+
}
|
|
116
|
+
server.once("error", (error) => finish(() => reject(error)));
|
|
117
|
+
server.listen(0, "127.0.0.1", () => {
|
|
118
|
+
const address = server.address();
|
|
119
|
+
const port = address && typeof address === "object" ? address.port : 0;
|
|
120
|
+
const request$1 = request({
|
|
121
|
+
host: "127.0.0.1",
|
|
122
|
+
port,
|
|
123
|
+
method: "POST",
|
|
124
|
+
path: `${agentRoute(session)}rpc`
|
|
125
|
+
}, (response) => {
|
|
126
|
+
response.resume();
|
|
127
|
+
response.once("end", () => finish(() => resolve(response.statusCode ?? 0)));
|
|
128
|
+
});
|
|
129
|
+
request$1.once("error", (error) => finish(() => reject(error)));
|
|
130
|
+
request$1.end("{}");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
async function withDeadline(work, timeoutMs) {
|
|
135
|
+
let timer;
|
|
136
|
+
try {
|
|
137
|
+
return await Promise.race([work, new Promise((_, reject) => {
|
|
138
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`the check did not finish within ${timeoutMs}ms`)), timeoutMs);
|
|
139
|
+
})]);
|
|
140
|
+
} finally {
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function checks(module, options) {
|
|
145
|
+
const fresh = () => module.createAgentDriver(options.unavailableInput);
|
|
146
|
+
const list = [
|
|
147
|
+
{
|
|
148
|
+
name: "the driver carries the module id",
|
|
149
|
+
run: async () => {
|
|
150
|
+
assert(fresh().name === options.id, `the driver is named ${JSON.stringify(fresh().name)}`);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: "stop before start resolves",
|
|
155
|
+
run: async () => {
|
|
156
|
+
await fresh().stop();
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: "revoking an unknown session resolves",
|
|
161
|
+
run: async () => {
|
|
162
|
+
await fresh().revoke(UNKNOWN_SESSION);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: "issuing before start refuses",
|
|
167
|
+
run: async () => {
|
|
168
|
+
await rejects(() => fresh().issue(options.app));
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: "forwarding without a grant answers with an error status",
|
|
173
|
+
run: async () => {
|
|
174
|
+
const status = await forwardStatus(fresh(), UNKNOWN_SESSION, options.checkTimeoutMs ?? 1e4);
|
|
175
|
+
assert(status >= 400, `the forward answered ${status}`);
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "start refuses with AgentDriverUnavailable when the tool is missing, then stops",
|
|
180
|
+
run: async () => {
|
|
181
|
+
const driver = fresh();
|
|
182
|
+
const error = await rejects(() => driver.start());
|
|
183
|
+
assert(error instanceof AgentDriverUnavailable, `start failed with ${String(error)}`);
|
|
184
|
+
await driver.stop();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
];
|
|
188
|
+
const client = module.client;
|
|
189
|
+
if (client) {
|
|
190
|
+
const grant = options.clientGrant;
|
|
191
|
+
const input = (granted) => ({
|
|
192
|
+
grant: granted,
|
|
193
|
+
baseUrl: `https://host.example.ts.net${granted.path}`,
|
|
194
|
+
file: `/tmp/stim-contract/${client.configFile}`,
|
|
195
|
+
platform: options.app.udid !== void 0 ? "ios" : options.app.serial !== void 0 ? "android" : "macos"
|
|
196
|
+
});
|
|
197
|
+
list.push({
|
|
198
|
+
name: "client access names its file and keeps the token out of the command",
|
|
199
|
+
run: async () => {
|
|
200
|
+
assert(grant, "clientGrant is required for a module with a client capability");
|
|
201
|
+
const access = client.access(input(grant));
|
|
202
|
+
assert(access, "the client refused its own grant");
|
|
203
|
+
assert(access.command.includes(input(grant).file), "the command does not name the config file");
|
|
204
|
+
assert(!access.command.includes(grant.token), "the command carries the token");
|
|
205
|
+
}
|
|
206
|
+
}, {
|
|
207
|
+
name: "client access refuses a grant for another driver",
|
|
208
|
+
run: async () => {
|
|
209
|
+
assert(grant, "clientGrant is required for a module with a client capability");
|
|
210
|
+
const other = {
|
|
211
|
+
...grant,
|
|
212
|
+
driver: `${options.id}-other`,
|
|
213
|
+
token: newAgentToken()
|
|
214
|
+
};
|
|
215
|
+
assert(client.access(input(other)) === null, "accepted");
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return list;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Loads a driver module the way stim-server does and runs the host-independent checks every driver must pass:
|
|
223
|
+
* shape, idempotent stop and revoke, refusals before start and without a grant, `AgentDriverUnavailable` when
|
|
224
|
+
* its tool is missing, and, with a client capability, a client file that keeps the token out of the command.
|
|
225
|
+
*/
|
|
226
|
+
async function runAgentDriverContract(options) {
|
|
227
|
+
const loaded = await loadAgentDriverModule(options.id, options.load);
|
|
228
|
+
if ("unavailable" in loaded) return [{
|
|
229
|
+
name: "the module loads",
|
|
230
|
+
passed: false,
|
|
231
|
+
error: loaded.unavailable
|
|
232
|
+
}];
|
|
233
|
+
const results = [{
|
|
234
|
+
name: "the module loads",
|
|
235
|
+
passed: true
|
|
236
|
+
}];
|
|
237
|
+
for (const check of checks(loaded.module, options)) try {
|
|
238
|
+
await withDeadline(check.run(), options.checkTimeoutMs ?? 1e4);
|
|
239
|
+
results.push({
|
|
240
|
+
name: check.name,
|
|
241
|
+
passed: true
|
|
242
|
+
});
|
|
243
|
+
} catch (error) {
|
|
244
|
+
results.push({
|
|
245
|
+
name: check.name,
|
|
246
|
+
passed: false,
|
|
247
|
+
error: error instanceof Error ? error.message : String(error)
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return results;
|
|
251
|
+
}
|
|
252
|
+
//#endregion
|
|
253
|
+
export { AGENT_DRIVER_API_VERSION, AGENT_DRIVER_CONTRACT_CHECK_TIMEOUT_MS, AGENT_DRIVER_LOAD_TIMEOUT_MS, AGENT_ROUTE_PREFIX, AgentDriverUnavailable, agentRoute, loadAgentDriver, loadAgentDriverModule, newAgentToken, runAgentDriverContract };
|
package/package.json
CHANGED
|
@@ -1,11 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stim-cli/agent-driver",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.22.0",
|
|
4
|
+
"description": "Hosted agent driver contract for stim-server.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/appandflow/stim.git",
|
|
9
9
|
"directory": "packages/agent-driver"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "dist/index.mjs",
|
|
18
|
+
"types": "dist/index.d.mts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.mts",
|
|
22
|
+
"module-sync": "./dist/index.mjs",
|
|
23
|
+
"default": "./dist/index.mjs"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@stim-cli/core": "^1.22.0"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=22.12.0"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsdown"
|
|
10
34
|
}
|
|
11
|
-
}
|
|
35
|
+
}
|