@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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +328 -2
  3. package/affordanceModel.js +23 -0
  4. package/affordanceRegistry.js +14 -0
  5. package/bindings/binding.js +229 -0
  6. package/bindings/httpBinding.js +173 -0
  7. package/bindings/nodeBinding.js +25 -0
  8. package/bindings/stdioBinding.js +58 -0
  9. package/cli/grail-cli.js +565 -0
  10. package/client.js +77 -0
  11. package/config-loader/loadEnvironment.js +71 -0
  12. package/docs/ENVIRONMENT.md +381 -0
  13. package/docs/grail-trust-model.md +665 -0
  14. package/examples/cli-tour/README.md +346 -0
  15. package/examples/cli-tour/capabilities/farewell.js +3 -0
  16. package/examples/cli-tour/capabilities/hello.js +3 -0
  17. package/examples/cli-tour/config/goal.json +3 -0
  18. package/examples/cli-tour/config/inputs.json +3 -0
  19. package/examples/cli-tour/config/observations.json +23 -0
  20. package/examples/cli-tour/config/registry.json +48 -0
  21. package/examples/cli-tour/config/worldstate.json +4 -0
  22. package/examples/cli-tour/g-loop.sh +7 -0
  23. package/examples/cli-tour/inputs/jane.json +3 -0
  24. package/examples/cli-tour/inputs/mike.json +3 -0
  25. package/examples/cli-tour/inputs/ruth.json +3 -0
  26. package/grail.js +59 -0
  27. package/images/grail-release-banner.png +0 -0
  28. package/index.js +2 -0
  29. package/observationStore.js +110 -0
  30. package/package.json +57 -4
  31. package/schemas/goal.schema.json +14 -0
  32. package/schemas/inputs.schema.json +6 -0
  33. package/schemas/observations.schema.json +75 -0
  34. package/schemas/registry.schema.json +285 -0
  35. package/schemas/worldstate.schema.json +7 -0
  36. package/server.js +201 -0
  37. package/utils/loadJSON.js +33 -0
  38. package/utils/validateWithSchema.js +42 -0
  39. package/worldState.js +36 -0
@@ -0,0 +1,173 @@
1
+ // httpBinding.js
2
+ // Generic HTTP binding executor.
3
+ // Maps resolved GRAIL inputs to HTTP path, query, header, and body locations.
4
+ // Returns captured request/response facts to the GRAIL runtime.
5
+
6
+ export async function executeHttpBinding(binding, inputs) {
7
+ const parameters = binding.parameters;
8
+
9
+ if (!parameters) {
10
+ return executeRequest(binding, {
11
+ url: binding.url,
12
+ headers: {},
13
+ body: { ...inputs }
14
+ });
15
+ }
16
+
17
+ let url = binding.url;
18
+ const headers = {};
19
+ const body = {};
20
+ const query = new URLSearchParams();
21
+
22
+ for (const [inputName, parameter] of Object.entries(parameters)) {
23
+ if (!(inputName in inputs)) {
24
+ throw new Error(`Missing binding input: ${inputName}`);
25
+ }
26
+
27
+ const value = inputs[inputName];
28
+ const httpName = parameter.name || inputName;
29
+
30
+ switch (parameter.in) {
31
+ case "path": {
32
+ const token = `{${httpName}}`;
33
+
34
+ if (!url.includes(token)) {
35
+ throw new Error(
36
+ `Path parameter '${inputName}' expects placeholder '${token}' in binding URL`
37
+ );
38
+ }
39
+
40
+ url = url.replaceAll(token, encodeURIComponent(String(value)));
41
+ break;
42
+ }
43
+
44
+ case "query":
45
+ appendQueryValue(query, httpName, value);
46
+ break;
47
+
48
+ case "header":
49
+ headers[httpName] = String(value);
50
+ break;
51
+
52
+ case "body":
53
+ body[httpName] = value;
54
+ break;
55
+
56
+ default:
57
+ throw new Error(
58
+ `Unsupported HTTP parameter location '${parameter.in}' for input '${inputName}'`
59
+ );
60
+ }
61
+ }
62
+
63
+ const queryString = query.toString();
64
+ if (queryString) {
65
+ url += `${url.includes("?") ? "&" : "?"}${queryString}`;
66
+ }
67
+
68
+ return executeRequest(binding, {
69
+ url,
70
+ headers,
71
+ body
72
+ });
73
+ }
74
+
75
+ function appendQueryValue(query, name, value) {
76
+ if (Array.isArray(value)) {
77
+ for (const item of value) {
78
+ query.append(name, String(item));
79
+ }
80
+ return;
81
+ }
82
+
83
+ query.append(name, String(value));
84
+ }
85
+
86
+ async function executeRequest(binding, request) {
87
+ const contentType = binding.contentType || "application/json";
88
+ const headers = { ...request.headers };
89
+ const hasBody = Object.keys(request.body).length > 0;
90
+ let requestBody;
91
+
92
+ if (hasBody) {
93
+ headers["Content-Type"] = contentType;
94
+
95
+ if (contentType === "application/x-www-form-urlencoded") {
96
+ requestBody = new URLSearchParams(request.body).toString();
97
+ } else {
98
+ requestBody = JSON.stringify(request.body);
99
+ }
100
+ }
101
+
102
+ const options = {
103
+ method: binding.method,
104
+ headers
105
+ };
106
+
107
+ if (hasBody) {
108
+ options.body = requestBody;
109
+ }
110
+
111
+ // const response = await fetch(request.url, options);
112
+ let response;
113
+
114
+ try {
115
+ response = await fetch(request.url, options);
116
+ } catch (error) {
117
+ return {
118
+ request: {
119
+ method: binding.method,
120
+ url: request.url,
121
+ headers,
122
+ body: hasBody ? request.body : null
123
+ },
124
+ response: {
125
+ ok: false,
126
+ status: null,
127
+ headers: {},
128
+ body: null,
129
+ error: {
130
+ type: "network",
131
+ message: error.message
132
+ }
133
+ }
134
+ };
135
+ }
136
+
137
+ const responseHeaders = Object.fromEntries(response.headers.entries());
138
+ const responseBody = await readResponseBody(response);
139
+
140
+ return {
141
+ request: {
142
+ method: binding.method,
143
+ url: request.url,
144
+ headers,
145
+ body: hasBody ? request.body : null
146
+ },
147
+ response: {
148
+ ok: response.ok,
149
+ status: response.status,
150
+ headers: responseHeaders,
151
+ body: responseBody
152
+ }
153
+ };
154
+ }
155
+
156
+ async function readResponseBody(response) {
157
+ const contentType = response.headers.get("content-type") || "";
158
+ const text = await response.text();
159
+
160
+ if (text === "") {
161
+ return null;
162
+ }
163
+
164
+ if (contentType.includes("application/json")) {
165
+ try {
166
+ return JSON.parse(text);
167
+ } catch {
168
+ return text;
169
+ }
170
+ }
171
+
172
+ return text;
173
+ }
@@ -0,0 +1,25 @@
1
+ import path from "node:path";
2
+ import { pathToFileURL } from "node:url";
3
+
4
+ export async function executeNodeBinding(binding, inputs, baseDir = process.cwd()) {
5
+ const modulePath = path.resolve(baseDir, binding.module);
6
+ const moduleUrl = pathToFileURL(modulePath).href;
7
+
8
+ const loaded = await import(moduleUrl);
9
+ const fn = loaded[binding.function];
10
+
11
+ if (typeof fn !== "function") {
12
+ throw new Error(
13
+ `Node binding function not found: ${binding.function} in ${binding.module}`
14
+ );
15
+ }
16
+
17
+ const result = await fn(inputs);
18
+
19
+ return {
20
+ module: binding.module,
21
+ function: binding.function,
22
+ inputs,
23
+ result
24
+ };
25
+ }
@@ -0,0 +1,58 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ export function executeStdioBinding(binding, inputs, baseDir = process.cwd()) {
4
+ return new Promise((resolve, reject) => {
5
+ const args = binding.args || [];
6
+
7
+ const child = spawn(binding.command, args, {
8
+ cwd: baseDir,
9
+ stdio: ["pipe", "pipe", "pipe"]
10
+ });
11
+
12
+ let stdout = "";
13
+ let stderr = "";
14
+
15
+ child.stdout.on("data", (data) => {
16
+ stdout += data.toString();
17
+ });
18
+
19
+ child.stderr.on("data", (data) => {
20
+ stderr += data.toString();
21
+ });
22
+
23
+ child.on("error", (error) => {
24
+ reject(error);
25
+ });
26
+
27
+ child.on("close", (exitCode) => {
28
+ let parsedStdout = null;
29
+ let parseError = null;
30
+
31
+ if (stdout.trim()) {
32
+ try {
33
+ parsedStdout = JSON.parse(stdout);
34
+ } catch (error) {
35
+ parseError = `Invalid JSON on stdout: ${error.message}`;
36
+ }
37
+ } else {
38
+ parseError = "No JSON returned on stdout";
39
+ }
40
+
41
+ resolve({
42
+ command: binding.command,
43
+ args,
44
+ inputs,
45
+ exitCode,
46
+ stdout: parsedStdout ?? stdout,
47
+ stderr,
48
+ error:
49
+ exitCode !== 0
50
+ ? stderr.trim() || `Process exited with code ${exitCode}`
51
+ : parseError
52
+ });
53
+ });
54
+
55
+ child.stdin.write(JSON.stringify(inputs));
56
+ child.stdin.end();
57
+ });
58
+ }