@mamund/grail 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +328 -2
- package/affordanceModel.js +23 -0
- package/affordanceRegistry.js +14 -0
- package/bindings/binding.js +229 -0
- package/bindings/httpBinding.js +173 -0
- package/bindings/nodeBinding.js +25 -0
- package/bindings/stdioBinding.js +58 -0
- package/cli/grail-cli.js +565 -0
- package/client.js +77 -0
- package/config-loader/loadEnvironment.js +71 -0
- package/docs/ENVIRONMENT.md +381 -0
- package/docs/grail-trust-model.md +665 -0
- package/examples/cli-tour/README.md +346 -0
- package/examples/cli-tour/capabilities/farewell.js +3 -0
- package/examples/cli-tour/capabilities/hello.js +3 -0
- package/examples/cli-tour/config/goal.json +3 -0
- package/examples/cli-tour/config/inputs.json +3 -0
- package/examples/cli-tour/config/observations.json +23 -0
- package/examples/cli-tour/config/registry.json +48 -0
- package/examples/cli-tour/config/worldstate.json +4 -0
- package/examples/cli-tour/g-loop.sh +7 -0
- package/examples/cli-tour/inputs/jane.json +3 -0
- package/examples/cli-tour/inputs/mike.json +3 -0
- package/examples/cli-tour/inputs/ruth.json +3 -0
- package/grail.js +59 -0
- package/images/grail-release-banner.png +0 -0
- package/index.js +2 -0
- package/observationStore.js +110 -0
- package/package.json +57 -4
- package/schemas/goal.schema.json +14 -0
- package/schemas/inputs.schema.json +6 -0
- package/schemas/observations.schema.json +75 -0
- package/schemas/registry.schema.json +285 -0
- package/schemas/worldstate.schema.json +7 -0
- package/server.js +201 -0
- package/utils/loadJSON.js +33 -0
- package/utils/validateWithSchema.js +42 -0
- package/worldState.js +36 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 amundsen-com, Inc.
|
|
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,329 @@
|
|
|
1
|
-
|
|
1
|
+
<img src="./images/grail-release-banner.png" />
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
GRAIL is a runtime for pursuing a declared goal within a bounded environment of available capabilities.
|
|
4
|
+
|
|
5
|
+
A GRAIL world describes the conditions that matter, the capabilities that can change those conditions, and the effects produced when those capabilities succeed. The runtime works from the goal and the current world state to determine what condition needs to change and which available affordance can change it.
|
|
6
|
+
|
|
7
|
+
The environment defines the possibilities. GRAIL pursues the goal without requiring a predefined workflow or execution path.
|
|
8
|
+
|
|
9
|
+
> Define the environment, not the path.
|
|
10
|
+
|
|
11
|
+
## Status
|
|
12
|
+
|
|
13
|
+
GRAIL is currently being prepared for beta release. The core runtime, configuration validation, Node/HTTP/stdio bindings, programmatic API, and initial CLI are working. Interfaces and package details may still change before the beta is declared stable.
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
Install and run the most recent beta edition as follows:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install -g @mamund/grail@beta
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
During development, install the repository and expose the local CLI:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install
|
|
27
|
+
npm link
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Create a runnable GRAIL world:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
grail init my-world
|
|
34
|
+
cd my-world
|
|
35
|
+
grail validate
|
|
36
|
+
grail run
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`grail init` creates a complete example using a small Node capability. A successful run pursues the generated `greetingCreated` goal and executes the capability needed to establish it.
|
|
40
|
+
|
|
41
|
+
## A GRAIL world
|
|
42
|
+
|
|
43
|
+
The generated world has this structure:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
my-world/
|
|
47
|
+
├── capabilities/
|
|
48
|
+
│ └── hello.mjs
|
|
49
|
+
└── config/
|
|
50
|
+
├── goal.json
|
|
51
|
+
├── inputs.json
|
|
52
|
+
├── registry.json
|
|
53
|
+
└── worldstate.json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The four configuration documents have distinct responsibilities:
|
|
57
|
+
|
|
58
|
+
- `goal.json` declares the condition GRAIL is trying to establish.
|
|
59
|
+
- `worldstate.json` records the conditions that currently hold in the world.
|
|
60
|
+
- `inputs.json` supplies initial data available to capabilities.
|
|
61
|
+
- `registry.json` describes available affordances, their preconditions and effects, their inputs, and their executable bindings.
|
|
62
|
+
|
|
63
|
+
Executable domain behavior lives in capabilities. GRAIL remains opaque to the application domain.
|
|
64
|
+
|
|
65
|
+
## How pursuit works
|
|
66
|
+
|
|
67
|
+
Suppose the goal is:
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"goal": "greetingCreated"
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
and the initial world contains:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"greetingCreated": false
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The registry can expose an affordance whose effect is `greetingCreated`. GRAIL can select that affordance, resolve its inputs, invoke its bound capability, and apply the declared effect when execution succeeds.
|
|
84
|
+
|
|
85
|
+
Conceptually:
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
goal
|
|
89
|
+
↓
|
|
90
|
+
inspect current world state
|
|
91
|
+
↓
|
|
92
|
+
select an unresolved condition
|
|
93
|
+
↓
|
|
94
|
+
select an affordance that can establish it
|
|
95
|
+
↓
|
|
96
|
+
resolve inputs and execute its capability
|
|
97
|
+
↓
|
|
98
|
+
apply effects on success
|
|
99
|
+
↓
|
|
100
|
+
continue until the goal is reached or cannot be resolved
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The exact traversal is not encoded as a workflow. It emerges from the current conditions and the affordances available in the world.
|
|
104
|
+
|
|
105
|
+
## CLI
|
|
106
|
+
|
|
107
|
+
### `grail init`
|
|
108
|
+
|
|
109
|
+
Create a complete runnable world:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
grail init my-world
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
GRAIL refuses to overwrite an existing target directory.
|
|
116
|
+
|
|
117
|
+
### `grail validate`
|
|
118
|
+
|
|
119
|
+
Load and validate a GRAIL environment without executing it:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
grail validate
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The default configuration directory is `./config`. A different directory can be supplied with:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
grail validate --config ./path/to/config
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### `grail show`
|
|
132
|
+
|
|
133
|
+
Inspect a validated GRAIL world without executing capabilities:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
grail show
|
|
137
|
+
grail show registry
|
|
138
|
+
grail show worldstate
|
|
139
|
+
grail show inputs
|
|
140
|
+
grail show goal
|
|
141
|
+
grail show registry --config ./my-world/config
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Without a target, `show` prints all four configuration documents as a single JSON object. With a target, it prints that document as formatted JSON. The entire environment must validate first, even when only one document is requested. `show` displays configured initial values, not the worldstate or observations from a previous run, and does not write files.
|
|
145
|
+
|
|
146
|
+
### `grail run`
|
|
147
|
+
|
|
148
|
+
Pursue the goal declared by an environment:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
grail run
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
or:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
grail run --config ./path/to/config
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The CLI writes execution observations to `observations.json` in the selected configuration directory. Each `grail run` starts an independent pursuit and overwrites this file rather than appending to prior runs. Copy or rename the file before another run if you need to retain its execution record. The configured `worldstate.json` is not modified.
|
|
161
|
+
|
|
162
|
+
The goal and inputs declared by the environment can be overridden for a single invocation. A goal override takes precedence over `goal.json`:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
grail run --config ./config --goal anotherGoal
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Inputs can be supplied as an inline JSON object:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
grail run --config ./config --inputs '{"name":"Mike"}'
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
or loaded from a JSON file:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
grail run --config ./config --inputs-file ./cases/mike.json
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
To inspect results rather than the default execution trace, use `--output`:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
grail run --output summary
|
|
184
|
+
grail run --output json
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`summary` prints the goal, success or failure, capability invocation count, and captured outputs. `json` prints the complete pursuit result (`goal`, `reached`, `worldstate`, and `observations`) as valid JSON suitable for tools such as `jq`. Routine runtime trace messages are suppressed in these modes; errors still appear on stderr. Omitting `--output` preserves the existing trace. The option accepts both `--output json` and `--output=json`. Invalid modes exit with code `2`.
|
|
188
|
+
|
|
189
|
+
Input overrides replace the contents of `inputs.json` for that invocation; they are not merged with it. `--inputs` and `--inputs-file` are mutually exclusive. Neither goal nor input overrides modify the environment files on disk.
|
|
190
|
+
|
|
191
|
+
Relative binding paths are resolved from the GRAIL world root—the parent directory of the selected configuration directory—not from the shell's current working directory. This allows a world to be run from another directory without changing the paths declared by its bindings. Paths supplied with `--inputs-file`, however, are resolved relative to the caller's current working directory.
|
|
192
|
+
|
|
193
|
+
General CLI information is available with:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
grail --help
|
|
197
|
+
grail --version
|
|
198
|
+
grail init --help
|
|
199
|
+
grail validate --help
|
|
200
|
+
grail run --help
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Exit codes
|
|
204
|
+
|
|
205
|
+
GRAIL uses process exit codes so CLI commands can be used reliably from shell scripts and other automation:
|
|
206
|
+
|
|
207
|
+
| Code | Meaning |
|
|
208
|
+
| ---: | --- |
|
|
209
|
+
| `0` | The command completed successfully. For `grail run`, the goal was reached. |
|
|
210
|
+
| `1` | GRAIL ran, but the pursuit or capability execution failed. |
|
|
211
|
+
| `2` | The command, configuration, or environment was invalid. |
|
|
212
|
+
|
|
213
|
+
For example, a caller can vary the inputs while GRAIL performs one independent pursuit for each invocation:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
for input in ./cases/*.json
|
|
217
|
+
do
|
|
218
|
+
grail run --config ./config --inputs-file "$input" || break
|
|
219
|
+
done
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Iteration remains the responsibility of the caller; GRAIL pursues the selected goal once per invocation.
|
|
223
|
+
|
|
224
|
+
### Errors
|
|
225
|
+
|
|
226
|
+
CLI errors identify the kind of failure and provide relevant context. Typical messages include:
|
|
227
|
+
|
|
228
|
+
```text
|
|
229
|
+
GRAIL: Configuration error: ...
|
|
230
|
+
GRAIL: Execution failed while pursuing goal "...": ...
|
|
231
|
+
GRAIL: Pursuit failed: goal "..." cannot be resolved with the available capabilities.
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Configuration and invocation errors exit with code `2`. Execution and pursuit failures exit with code `1`. Expected user errors are reported as concise CLI messages rather than uncaught stack traces.
|
|
235
|
+
|
|
236
|
+
## Command-line Tour
|
|
237
|
+
|
|
238
|
+
For a hands-on introduction to GRAIL, see [A Tour of GRAIL at the Command Line](./examples/cli-tour/README.md).
|
|
239
|
+
|
|
240
|
+
The tour uses a complete example world to explore:
|
|
241
|
+
|
|
242
|
+
- running and validating a GRAIL world
|
|
243
|
+
- overriding inputs from the command line or a file
|
|
244
|
+
- running a world from different filesystem locations
|
|
245
|
+
- pursuing different goals in the same world
|
|
246
|
+
- recognizing an unresolvable goal
|
|
247
|
+
- using exit codes and shell loops to compose GRAIL with other tools
|
|
248
|
+
|
|
249
|
+
The tour is designed to be run directly from the command line and includes all required capabilities, configuration, and sample inputs.
|
|
250
|
+
|
|
251
|
+
## Programmatic API
|
|
252
|
+
|
|
253
|
+
GRAIL can also be loaded as a module by another application or front end.
|
|
254
|
+
|
|
255
|
+
Load a filesystem-based environment and pursue its goal:
|
|
256
|
+
|
|
257
|
+
```javascript
|
|
258
|
+
import { Grail, loadEnvironment } from '@mamund/grail';
|
|
259
|
+
|
|
260
|
+
const environment = loadEnvironment('./config');
|
|
261
|
+
|
|
262
|
+
const grail = new Grail({
|
|
263
|
+
registry: environment.registry,
|
|
264
|
+
worldstate: environment.worldstate,
|
|
265
|
+
inputs: environment.inputs
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const result = await grail.pursue(environment.goal);
|
|
269
|
+
|
|
270
|
+
console.log(result.reached);
|
|
271
|
+
console.log(result.worldstate);
|
|
272
|
+
console.log(result.observations);
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The `Grail` runtime itself does not require filesystem-backed observations. Observations are retained in memory by default. A caller can provide an `observationPath` when file persistence is wanted.
|
|
276
|
+
|
|
277
|
+
`pursue()` returns the pursued goal, whether it was reached, the resulting world state, and the observations collected during execution.
|
|
278
|
+
|
|
279
|
+
## Bindings
|
|
280
|
+
|
|
281
|
+
An affordance may be connected to an executable capability through a binding. The current runtime supports three protocols:
|
|
282
|
+
|
|
283
|
+
| Protocol | Purpose |
|
|
284
|
+
| --- | --- |
|
|
285
|
+
| `node` | Invoke a local JavaScript module and exported function. |
|
|
286
|
+
| `http` | Invoke a capability through HTTP. |
|
|
287
|
+
| `stdio` | Launch a local process, send JSON inputs on stdin, and consume JSON output from stdout. |
|
|
288
|
+
|
|
289
|
+
Bindings are adapters between GRAIL affordances and capability implementations. Goal pursuit does not depend on the implementation technology behind a capability.
|
|
290
|
+
|
|
291
|
+
## Execution results and observations
|
|
292
|
+
|
|
293
|
+
Bound capabilities reduce execution to GRAIL's runtime result model. Successful execution permits declared effects to be applied; failed execution does not.
|
|
294
|
+
|
|
295
|
+
Observations retain execution evidence such as invocation details, responses, extracted outputs, and the resulting execution status. This allows GRAIL to keep its runtime contract small while preserving useful diagnostic information.
|
|
296
|
+
|
|
297
|
+
## Validation and tests
|
|
298
|
+
|
|
299
|
+
Run the current automated test suite with:
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
npm test
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
The suite covers end-to-end goal pursuit through the public API, stdio execution and output handling, world-relative path resolution, goal and input invocation overrides, and CLI behavior including configuration errors, execution failures, pursuit failures, messages, and exit codes.
|
|
306
|
+
|
|
307
|
+
Tests run as independent suites with start/pass/fail reporting and a 30-second timeout per suite. The CLI regression tests are organized under `test/cli/` into commands, show, output, overrides, and errors. Run an individual suite directly, for example:
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
node test/cli/output.js
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Each CLI subprocess also has a 15-second timeout so a hanging command fails with a useful error rather than blocking the entire suite.
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
## Design notes
|
|
317
|
+
|
|
318
|
+
Two documents in `docs/` provide additional architectural context:
|
|
319
|
+
|
|
320
|
+
- [What Is Not in the Agent Must Be in the Environment](./docs/ENVIRONMENT.md) discusses GRAIL's environment-first approach to bounded autonomy.
|
|
321
|
+
- [GRAIL Trust Model](./docs/grail-trust-model.md) describes the runtime's trust boundaries and the responsibilities retained by capabilities and the surrounding environment.
|
|
322
|
+
|
|
323
|
+
**Security and trust**: GRAIL executes capabilities defined by a trusted world. Only run reviewed configurations and bindings. Capabilities are responsible for authentication, authorization, and domain security. Observations may contain sensitive data, and the beta runtime does not enforce execution timeouts. See the GRAIL Trust Model for execution boundaries and operational limitations.
|
|
324
|
+
|
|
325
|
+
## Beta scope
|
|
326
|
+
|
|
327
|
+
The beta is centered on a small runtime and CLI for composing and executing GRAIL worlds. Current work is focused on hardening the public module boundary, CLI behavior, path semantics, error handling, package metadata, documentation, and regression coverage.
|
|
328
|
+
|
|
329
|
+
More advanced selection strategies, additional tooling, richer authoring experiences, and other execution features can build on this foundation without changing the basic model of goal pursuit through conditions, affordances, effects, and bound capabilities.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// affordanceModel.js
|
|
2
|
+
|
|
3
|
+
export class Affordance {
|
|
4
|
+
constructor({
|
|
5
|
+
id,
|
|
6
|
+
action,
|
|
7
|
+
type,
|
|
8
|
+
enabled,
|
|
9
|
+
preconditions = [],
|
|
10
|
+
inputs = [],
|
|
11
|
+
effects = [],
|
|
12
|
+
binding
|
|
13
|
+
}) {
|
|
14
|
+
this.id = id;
|
|
15
|
+
this.action = action;
|
|
16
|
+
this.type = type;
|
|
17
|
+
this.enabled = enabled;
|
|
18
|
+
this.preconditions = preconditions;
|
|
19
|
+
this.inputs = inputs;
|
|
20
|
+
this.effects = effects;
|
|
21
|
+
this.binding = binding;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// affordanceRegistry.js (loads from file at runtime)
|
|
2
|
+
|
|
3
|
+
import { Affordance } from './affordanceModel.js';
|
|
4
|
+
|
|
5
|
+
export function loadAffordanceRegistry(registryData) {
|
|
6
|
+
const registry = {};
|
|
7
|
+
|
|
8
|
+
for (let key in registryData) {
|
|
9
|
+
registry[key] = new Affordance(registryData[key]);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return registry;
|
|
13
|
+
}
|
|
14
|
+
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { executeHttpBinding } from "./httpBinding.js";
|
|
2
|
+
import { executeNodeBinding } from "./nodeBinding.js";
|
|
3
|
+
import { executeStdioBinding } from "./stdioBinding.js";
|
|
4
|
+
|
|
5
|
+
export async function executeBinding(binding, inputs, baseDir = process.cwd()) {
|
|
6
|
+
switch (binding.protocol) {
|
|
7
|
+
case "http":
|
|
8
|
+
return executeHttp(binding, inputs);
|
|
9
|
+
|
|
10
|
+
case "node":
|
|
11
|
+
return executeNode(binding, inputs, baseDir);
|
|
12
|
+
|
|
13
|
+
case "stdio":
|
|
14
|
+
return executeStdio(binding, inputs, baseDir);
|
|
15
|
+
|
|
16
|
+
default:
|
|
17
|
+
throw new Error(`Unsupported binding protocol: ${binding.protocol}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function executeHttp(binding, inputs) {
|
|
22
|
+
const interaction = await executeHttpBinding(binding, inputs);
|
|
23
|
+
const outputs = extractHttpOutputs(binding.outputs, interaction.response);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
ok: interaction.response.ok,
|
|
27
|
+
description: `${binding.method} ${binding.url}`,
|
|
28
|
+
summary: interaction.response.error?.type === "network"
|
|
29
|
+
? `network error (${interaction.response.error.message})`
|
|
30
|
+
: `HTTP ${interaction.response.status}`,
|
|
31
|
+
invocation: {
|
|
32
|
+
request: interaction.request
|
|
33
|
+
},
|
|
34
|
+
response: {
|
|
35
|
+
status: interaction.response.status,
|
|
36
|
+
headers: interaction.response.headers,
|
|
37
|
+
body: interaction.response.body
|
|
38
|
+
},
|
|
39
|
+
outputs
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function executeNode(binding, inputs, baseDir) {
|
|
44
|
+
try {
|
|
45
|
+
const interaction = await executeNodeBinding(binding, inputs, baseDir);
|
|
46
|
+
const outputs = extractNodeOutputs(binding.outputs, interaction.result);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
ok: true,
|
|
50
|
+
description: `${binding.module} :: ${binding.function}`,
|
|
51
|
+
summary: `${binding.module} :: ${binding.function}`,
|
|
52
|
+
invocation: {
|
|
53
|
+
module: interaction.module,
|
|
54
|
+
function: interaction.function,
|
|
55
|
+
inputs: interaction.inputs
|
|
56
|
+
},
|
|
57
|
+
response: {
|
|
58
|
+
result: interaction.result
|
|
59
|
+
},
|
|
60
|
+
outputs
|
|
61
|
+
};
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
description: `${binding.module} :: ${binding.function}`,
|
|
66
|
+
summary: error.message,
|
|
67
|
+
invocation: {
|
|
68
|
+
module: binding.module,
|
|
69
|
+
function: binding.function,
|
|
70
|
+
inputs
|
|
71
|
+
},
|
|
72
|
+
response: {
|
|
73
|
+
error: error.message
|
|
74
|
+
},
|
|
75
|
+
outputs: {}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function executeStdio(binding, inputs, baseDir) {
|
|
81
|
+
try {
|
|
82
|
+
const interaction = await executeStdioBinding(binding, inputs, baseDir);
|
|
83
|
+
|
|
84
|
+
const ok =
|
|
85
|
+
interaction.exitCode === 0 &&
|
|
86
|
+
!interaction.error;
|
|
87
|
+
|
|
88
|
+
const outputs = ok
|
|
89
|
+
? extractStdioOutputs(binding.outputs, interaction.stdout)
|
|
90
|
+
: {};
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
ok,
|
|
94
|
+
description: `${binding.command} ${(binding.args || []).join(" ")}`,
|
|
95
|
+
summary: ok
|
|
96
|
+
? `${binding.command} ${(binding.args || []).join(" ")}`
|
|
97
|
+
: interaction.error,
|
|
98
|
+
invocation: {
|
|
99
|
+
command: interaction.command,
|
|
100
|
+
args: interaction.args,
|
|
101
|
+
inputs: interaction.inputs
|
|
102
|
+
},
|
|
103
|
+
response: {
|
|
104
|
+
exitCode: interaction.exitCode,
|
|
105
|
+
stdout: interaction.stdout,
|
|
106
|
+
stderr: interaction.stderr,
|
|
107
|
+
...(interaction.error && { error: interaction.error })
|
|
108
|
+
},
|
|
109
|
+
outputs
|
|
110
|
+
};
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
description: `${binding.command} ${(binding.args || []).join(" ")}`,
|
|
115
|
+
summary: error.message,
|
|
116
|
+
invocation: {
|
|
117
|
+
command: binding.command,
|
|
118
|
+
args: binding.args || [],
|
|
119
|
+
inputs
|
|
120
|
+
},
|
|
121
|
+
response: {
|
|
122
|
+
error: error.message
|
|
123
|
+
},
|
|
124
|
+
outputs: {}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function extractHttpOutputs(outputDefinitions, response) {
|
|
130
|
+
const outputs = {};
|
|
131
|
+
|
|
132
|
+
if (!outputDefinitions) {
|
|
133
|
+
return outputs;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const [outputName, definition] of Object.entries(outputDefinitions)) {
|
|
137
|
+
switch (definition.from) {
|
|
138
|
+
case "body": {
|
|
139
|
+
const extracted = readPath(response.body, definition.path);
|
|
140
|
+
if (extracted.found) {
|
|
141
|
+
outputs[outputName] = extracted.value;
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
case "header": {
|
|
147
|
+
const headerName = definition.name.toLowerCase();
|
|
148
|
+
|
|
149
|
+
for (const [name, value] of Object.entries(response.headers)) {
|
|
150
|
+
if (name.toLowerCase() === headerName) {
|
|
151
|
+
outputs[outputName] = value;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
case "status":
|
|
159
|
+
outputs[outputName] = response.status;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return outputs;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function extractNodeOutputs(outputDefinitions, result) {
|
|
168
|
+
const outputs = {};
|
|
169
|
+
|
|
170
|
+
if (!outputDefinitions) {
|
|
171
|
+
return outputs;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const [outputName, definition] of Object.entries(outputDefinitions)) {
|
|
175
|
+
if (definition.from !== "result") {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const extracted = readPath(result, definition.path);
|
|
180
|
+
|
|
181
|
+
if (extracted.found) {
|
|
182
|
+
outputs[outputName] = extracted.value;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return outputs;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function readPath(value, path) {
|
|
190
|
+
const segments = path.split(".");
|
|
191
|
+
let current = value;
|
|
192
|
+
|
|
193
|
+
for (const segment of segments) {
|
|
194
|
+
if (
|
|
195
|
+
current === null ||
|
|
196
|
+
current === undefined ||
|
|
197
|
+
(typeof current !== "object" && !Array.isArray(current)) ||
|
|
198
|
+
!(segment in current)
|
|
199
|
+
) {
|
|
200
|
+
return { found: false };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
current = current[segment];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { found: true, value: current };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function extractStdioOutputs(outputDefinitions, stdout) {
|
|
210
|
+
const outputs = {};
|
|
211
|
+
|
|
212
|
+
if (!outputDefinitions) {
|
|
213
|
+
return outputs;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (const [outputName, definition] of Object.entries(outputDefinitions)) {
|
|
217
|
+
if (definition.from !== "stdout") {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const extracted = readPath(stdout, definition.path);
|
|
222
|
+
|
|
223
|
+
if (extracted.found) {
|
|
224
|
+
outputs[outputName] = extracted.value;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return outputs;
|
|
229
|
+
}
|