@enactprotocol/shared 2.2.4 → 2.3.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/README.md +1 -18
- package/dist/config.d.ts +12 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +32 -6
- package/dist/config.js.map +1 -1
- package/dist/execution/action-command.d.ts +131 -0
- package/dist/execution/action-command.d.ts.map +1 -0
- package/dist/execution/action-command.js +300 -0
- package/dist/execution/action-command.js.map +1 -0
- package/dist/execution/command.d.ts +8 -8
- package/dist/execution/command.js +6 -6
- package/dist/execution/index.d.ts +1 -0
- package/dist/execution/index.d.ts.map +1 -1
- package/dist/execution/index.js +2 -0
- package/dist/execution/index.js.map +1 -1
- package/dist/execution/types.d.ts +5 -2
- package/dist/execution/types.d.ts.map +1 -1
- package/dist/execution/types.js.map +1 -1
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -4
- package/dist/index.js.map +1 -1
- package/dist/manifest/actions-loader.d.ts +29 -0
- package/dist/manifest/actions-loader.d.ts.map +1 -0
- package/dist/manifest/actions-loader.js +34 -0
- package/dist/manifest/actions-loader.js.map +1 -0
- package/dist/manifest/actions-parser.d.ts +69 -0
- package/dist/manifest/actions-parser.d.ts.map +1 -0
- package/dist/manifest/actions-parser.js +265 -0
- package/dist/manifest/actions-parser.js.map +1 -0
- package/dist/manifest/index.d.ts +2 -0
- package/dist/manifest/index.d.ts.map +1 -1
- package/dist/manifest/index.js +4 -0
- package/dist/manifest/index.js.map +1 -1
- package/dist/manifest/loader.d.ts +7 -2
- package/dist/manifest/loader.d.ts.map +1 -1
- package/dist/manifest/loader.js +71 -4
- package/dist/manifest/loader.js.map +1 -1
- package/dist/manifest/parser.d.ts +1 -0
- package/dist/manifest/parser.d.ts.map +1 -1
- package/dist/manifest/parser.js +1 -0
- package/dist/manifest/parser.js.map +1 -1
- package/dist/manifest/scripts.d.ts +19 -0
- package/dist/manifest/scripts.d.ts.map +1 -0
- package/dist/manifest/scripts.js +102 -0
- package/dist/manifest/scripts.js.map +1 -0
- package/dist/manifest/validator.d.ts +1 -8
- package/dist/manifest/validator.d.ts.map +1 -1
- package/dist/manifest/validator.js +14 -13
- package/dist/manifest/validator.js.map +1 -1
- package/dist/mcp-registry.js +5 -5
- package/dist/mcp-registry.js.map +1 -1
- package/dist/paths.d.ts +9 -2
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +12 -3
- package/dist/paths.js.map +1 -1
- package/dist/registry.d.ts +3 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +5 -5
- package/dist/registry.js.map +1 -1
- package/dist/resolver.d.ts +55 -4
- package/dist/resolver.d.ts.map +1 -1
- package/dist/resolver.js +133 -75
- package/dist/resolver.js.map +1 -1
- package/dist/types/actions.d.ts +194 -0
- package/dist/types/actions.d.ts.map +1 -0
- package/dist/types/actions.js +32 -0
- package/dist/types/actions.js.map +1 -0
- package/dist/types/index.d.ts +3 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/manifest.d.ts +50 -5
- package/dist/types/manifest.d.ts.map +1 -1
- package/dist/types/manifest.js +10 -2
- package/dist/types/manifest.js.map +1 -1
- package/package.json +2 -2
- package/src/config.ts +48 -6
- package/src/execution/action-command.ts +417 -0
- package/src/execution/command.ts +8 -8
- package/src/execution/index.ts +17 -0
- package/src/execution/types.ts +13 -2
- package/src/index.ts +37 -0
- package/src/manifest/actions-loader.ts +49 -0
- package/src/manifest/index.ts +12 -0
- package/src/manifest/loader.ts +77 -4
- package/src/manifest/parser.ts +1 -0
- package/src/manifest/scripts.ts +116 -0
- package/src/manifest/validator.ts +15 -14
- package/src/mcp-registry.ts +5 -5
- package/src/paths.ts +13 -3
- package/src/registry.ts +5 -5
- package/src/resolver.ts +172 -77
- package/src/types/actions.ts +223 -0
- package/src/types/index.ts +11 -0
- package/src/types/manifest.ts +67 -6
- package/tests/action-command.test.ts +249 -0
- package/tests/config-normalization.test.ts +279 -0
- package/tests/config.test.ts +4 -1
- package/tests/effective-input-schema.test.ts +86 -0
- package/tests/fixtures/valid-tool.md +5 -12
- package/tests/fixtures/valid-tool.yaml +3 -10
- package/tests/hooks.test.ts +177 -0
- package/tests/manifest/loader.test.ts +34 -20
- package/tests/manifest/parser.test.ts +11 -15
- package/tests/manifest/validator.test.ts +7 -17
- package/tests/manifest-types.test.ts +9 -11
- package/tests/paths.test.ts +11 -4
- package/tests/registry.test.ts +12 -11
- package/tests/resolver.test.ts +11 -7
- package/tsconfig.tsbuildinfo +1 -1
package/src/types/manifest.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TypeScript types for Enact tool manifests
|
|
3
|
-
* These types define the structure of SKILL.md (and legacy enact.yaml/enact.md) frontmatter
|
|
3
|
+
* These types define the structure of SKILL.md (and skill.yaml/legacy enact.yaml/enact.md) frontmatter
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { JSONSchema7 } from "json-schema";
|
|
@@ -13,6 +13,8 @@ export interface EnvVariable {
|
|
|
13
13
|
description: string;
|
|
14
14
|
/** If true, stored in OS keyring; if false, stored in .env files */
|
|
15
15
|
secret?: boolean;
|
|
16
|
+
/** If true, must be set before execution (no default fallback) */
|
|
17
|
+
required?: boolean;
|
|
16
18
|
/** Default value if not set (only for non-secrets) */
|
|
17
19
|
default?: string;
|
|
18
20
|
}
|
|
@@ -50,6 +52,32 @@ export interface ToolAnnotations {
|
|
|
50
52
|
openWorldHint?: boolean;
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Lifecycle hooks for tool installation and management
|
|
57
|
+
*/
|
|
58
|
+
export interface ToolHooks {
|
|
59
|
+
/** Command(s) to run after the tool is installed/extracted (e.g., "npm install", "pip install -r requirements.txt") */
|
|
60
|
+
postinstall?: string | string[];
|
|
61
|
+
/** Build command(s) to run before execution (e.g., "pip install -r requirements.txt") */
|
|
62
|
+
build?: string | string[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Script definition — either a simple command string or an expanded object
|
|
67
|
+
*
|
|
68
|
+
* Simple form: "python main.py {{url}}"
|
|
69
|
+
* Expanded form: { command: "python main.py {{url}}", description: "Scrape a URL" }
|
|
70
|
+
*/
|
|
71
|
+
export type ScriptDefinition =
|
|
72
|
+
| string
|
|
73
|
+
| {
|
|
74
|
+
command: string;
|
|
75
|
+
description?: string;
|
|
76
|
+
inputSchema?: JSONSchema7;
|
|
77
|
+
outputSchema?: JSONSchema7;
|
|
78
|
+
annotations?: ToolAnnotations;
|
|
79
|
+
};
|
|
80
|
+
|
|
53
81
|
/**
|
|
54
82
|
* Resource requirements for tool execution
|
|
55
83
|
*/
|
|
@@ -76,7 +104,7 @@ export interface ToolExample {
|
|
|
76
104
|
|
|
77
105
|
/**
|
|
78
106
|
* Complete tool manifest structure
|
|
79
|
-
* This represents the YAML frontmatter in SKILL.md (or legacy enact.md/enact.yaml)
|
|
107
|
+
* This represents the YAML frontmatter in SKILL.md (or skill.yaml/legacy enact.md/enact.yaml)
|
|
80
108
|
*/
|
|
81
109
|
export interface ToolManifest {
|
|
82
110
|
// ==================== Required Fields ====================
|
|
@@ -115,9 +143,6 @@ export interface ToolManifest {
|
|
|
115
143
|
|
|
116
144
|
// ==================== Schema Fields ====================
|
|
117
145
|
|
|
118
|
-
/** JSON Schema defining input parameters */
|
|
119
|
-
inputSchema?: JSONSchema7;
|
|
120
|
-
|
|
121
146
|
/** JSON Schema defining output structure */
|
|
122
147
|
outputSchema?: JSONSchema7;
|
|
123
148
|
|
|
@@ -134,6 +159,28 @@ export interface ToolManifest {
|
|
|
134
159
|
/** Resource limits and requirements */
|
|
135
160
|
resources?: ResourceRequirements;
|
|
136
161
|
|
|
162
|
+
// ==================== Lifecycle Hooks ====================
|
|
163
|
+
|
|
164
|
+
/** Lifecycle hooks (e.g., postinstall build step) */
|
|
165
|
+
hooks?: ToolHooks;
|
|
166
|
+
|
|
167
|
+
// ==================== Scripts ====================
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Inline executable scripts (each becomes an MCP tool via colon syntax)
|
|
171
|
+
*
|
|
172
|
+
* Scripts replace the need for a separate ACTIONS.yaml file.
|
|
173
|
+
* Each script maps a name to a command with {{param}} template syntax.
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* scripts:
|
|
177
|
+
* scrape: python scripts/scrape.py {{url}}
|
|
178
|
+
* crawl:
|
|
179
|
+
* command: python scripts/crawl.py {{url}} {{depth}}
|
|
180
|
+
* description: Crawl a website to specified depth
|
|
181
|
+
*/
|
|
182
|
+
scripts?: Record<string, ScriptDefinition>;
|
|
183
|
+
|
|
137
184
|
// ==================== Agent Skills Spec Fields ====================
|
|
138
185
|
|
|
139
186
|
/** Environment requirements (intended product, system packages, network access, etc.) */
|
|
@@ -252,14 +299,28 @@ export interface ToolResolution {
|
|
|
252
299
|
manifestPath: string;
|
|
253
300
|
/** Tool version (if available) */
|
|
254
301
|
version?: string | undefined;
|
|
302
|
+
/** The resolved script (if a script was specified via colon syntax) */
|
|
303
|
+
action?: import("./actions").Action | undefined;
|
|
304
|
+
/** The requested script name (if specified via colon syntax) */
|
|
305
|
+
actionName?: string | undefined;
|
|
306
|
+
/** The scripts manifest (converted from inline scripts) */
|
|
307
|
+
actionsManifest?: import("./actions").ActionsManifest | undefined;
|
|
255
308
|
}
|
|
256
309
|
|
|
257
310
|
/**
|
|
258
311
|
* Supported manifest file names
|
|
259
312
|
* SKILL.md is the primary format (aligned with Anthropic Agent Skills)
|
|
313
|
+
* skill.yaml/yml is the package manifest
|
|
260
314
|
* enact.md/yaml/yml are supported for backwards compatibility
|
|
261
315
|
*/
|
|
262
|
-
export const MANIFEST_FILES = [
|
|
316
|
+
export const MANIFEST_FILES = [
|
|
317
|
+
"SKILL.md",
|
|
318
|
+
"skill.yaml",
|
|
319
|
+
"skill.yml",
|
|
320
|
+
"enact.md",
|
|
321
|
+
"enact.yaml",
|
|
322
|
+
"enact.yml",
|
|
323
|
+
] as const;
|
|
263
324
|
export type ManifestFileName = (typeof MANIFEST_FILES)[number];
|
|
264
325
|
|
|
265
326
|
/**
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the action command interpolation module.
|
|
3
|
+
*
|
|
4
|
+
* Covers {{param}} template parsing, interpolation, omission of optionals,
|
|
5
|
+
* and the prepareActionCommand entry point.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
import {
|
|
10
|
+
getActionCommandParams,
|
|
11
|
+
getMissingRequiredParams,
|
|
12
|
+
hasActionTemplates,
|
|
13
|
+
interpolateActionCommand,
|
|
14
|
+
parseActionArgument,
|
|
15
|
+
parseActionCommand,
|
|
16
|
+
prepareActionCommand,
|
|
17
|
+
} from "../src/execution/action-command";
|
|
18
|
+
|
|
19
|
+
describe("hasActionTemplates", () => {
|
|
20
|
+
test("returns true for string with {{param}}", () => {
|
|
21
|
+
expect(hasActionTemplates("hello {{name}}")).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("returns false for plain string", () => {
|
|
25
|
+
expect(hasActionTemplates("echo hello")).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("returns false for ${param} syntax", () => {
|
|
29
|
+
expect(hasActionTemplates("echo ${name}")).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("returns true for standalone {{param}}", () => {
|
|
33
|
+
expect(hasActionTemplates("{{url}}")).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("parseActionArgument", () => {
|
|
38
|
+
test("parses a literal argument", () => {
|
|
39
|
+
const result = parseActionArgument("echo");
|
|
40
|
+
expect(result.tokens).toHaveLength(1);
|
|
41
|
+
expect(result.tokens[0]?.type).toBe("literal");
|
|
42
|
+
expect(result.tokens[0]?.type === "literal" && result.tokens[0].value).toBe("echo");
|
|
43
|
+
expect(result.parameters).toHaveLength(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("parses a standalone parameter", () => {
|
|
47
|
+
const result = parseActionArgument("{{url}}");
|
|
48
|
+
expect(result.tokens).toHaveLength(1);
|
|
49
|
+
expect(result.tokens[0]?.type).toBe("parameter");
|
|
50
|
+
expect(result.tokens[0]?.type === "parameter" && result.tokens[0].name).toBe("url");
|
|
51
|
+
expect(result.parameters).toEqual(["url"]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("parses mixed literal and parameter", () => {
|
|
55
|
+
const result = parseActionArgument("--name={{name}}");
|
|
56
|
+
expect(result.tokens).toHaveLength(2);
|
|
57
|
+
expect(result.tokens[0]?.type).toBe("literal");
|
|
58
|
+
expect(result.tokens[1]?.type).toBe("parameter");
|
|
59
|
+
expect(result.parameters).toEqual(["name"]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("parses multiple parameters in one argument", () => {
|
|
63
|
+
const result = parseActionArgument("{{host}}:{{port}}");
|
|
64
|
+
expect(result.tokens).toHaveLength(3);
|
|
65
|
+
expect(result.parameters).toEqual(["host", "port"]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("trims whitespace in parameter names", () => {
|
|
69
|
+
const result = parseActionArgument("{{ name }}");
|
|
70
|
+
expect(result.parameters).toEqual(["name"]);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("parseActionCommand", () => {
|
|
75
|
+
test("parses command with no templates", () => {
|
|
76
|
+
const result = parseActionCommand(["echo", "hello", "world"]);
|
|
77
|
+
expect(result.allParameters).toHaveLength(0);
|
|
78
|
+
expect(result.arguments).toHaveLength(3);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("parses command with templates", () => {
|
|
82
|
+
const result = parseActionCommand(["python", "main.py", "{{url}}"]);
|
|
83
|
+
expect(result.allParameters).toEqual(["url"]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("deduplicates parameter names", () => {
|
|
87
|
+
const result = parseActionCommand(["echo", "{{name}}", "{{name}}"]);
|
|
88
|
+
expect(result.allParameters).toEqual(["name"]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("collects all unique parameters", () => {
|
|
92
|
+
const result = parseActionCommand(["cmd", "{{a}}", "{{b}}", "{{c}}"]);
|
|
93
|
+
expect(result.allParameters).toEqual(["a", "b", "c"]);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("interpolateActionCommand", () => {
|
|
98
|
+
test("replaces parameter with value", () => {
|
|
99
|
+
const result = interpolateActionCommand(["echo", "{{name}}"], { name: "Alice" });
|
|
100
|
+
expect(result).toEqual(["echo", "Alice"]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("preserves literal arguments", () => {
|
|
104
|
+
const result = interpolateActionCommand(["python", "main.py", "--verbose"], {});
|
|
105
|
+
expect(result).toEqual(["python", "main.py", "--verbose"]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("handles mixed literal and parameter", () => {
|
|
109
|
+
const result = interpolateActionCommand(["--output={{format}}"], { format: "json" });
|
|
110
|
+
expect(result).toEqual(["--output=json"]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("converts number to string", () => {
|
|
114
|
+
const result = interpolateActionCommand(["echo", "{{count}}"], { count: 42 });
|
|
115
|
+
expect(result).toEqual(["echo", "42"]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("converts boolean to string", () => {
|
|
119
|
+
const result = interpolateActionCommand(["echo", "{{flag}}"], { flag: true });
|
|
120
|
+
expect(result).toEqual(["echo", "true"]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("converts object to JSON string", () => {
|
|
124
|
+
const result = interpolateActionCommand(["echo", "{{data}}"], { data: { key: "value" } });
|
|
125
|
+
expect(result).toEqual(["echo", '{"key":"value"}']);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("omits argument for optional param with no value", () => {
|
|
129
|
+
const schema = {
|
|
130
|
+
type: "object" as const,
|
|
131
|
+
properties: {
|
|
132
|
+
name: { type: "string" as const },
|
|
133
|
+
verbose: { type: "boolean" as const },
|
|
134
|
+
},
|
|
135
|
+
required: ["name"],
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const result = interpolateActionCommand(
|
|
139
|
+
["echo", "{{name}}", "{{verbose}}"],
|
|
140
|
+
{ name: "Alice" },
|
|
141
|
+
{ inputSchema: schema }
|
|
142
|
+
);
|
|
143
|
+
// "verbose" is optional with no value → omitted
|
|
144
|
+
expect(result).toEqual(["echo", "Alice"]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("uses default value for optional param", () => {
|
|
148
|
+
const schema = {
|
|
149
|
+
type: "object" as const,
|
|
150
|
+
properties: {
|
|
151
|
+
format: { type: "string" as const, default: "text" },
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const result = interpolateActionCommand(["echo", "{{format}}"], {}, { inputSchema: schema });
|
|
156
|
+
expect(result).toEqual(["echo", "text"]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("does not split values with spaces into multiple args", () => {
|
|
160
|
+
const result = interpolateActionCommand(["echo", "{{msg}}"], {
|
|
161
|
+
msg: "hello world with spaces",
|
|
162
|
+
});
|
|
163
|
+
// Must remain a single argument — security property
|
|
164
|
+
expect(result).toEqual(["echo", "hello world with spaces"]);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("getMissingRequiredParams", () => {
|
|
169
|
+
test("returns empty when all required params provided", () => {
|
|
170
|
+
const schema = {
|
|
171
|
+
type: "object" as const,
|
|
172
|
+
required: ["name"],
|
|
173
|
+
properties: { name: { type: "string" as const } },
|
|
174
|
+
};
|
|
175
|
+
const missing = getMissingRequiredParams(["echo", "{{name}}"], { name: "hi" }, schema);
|
|
176
|
+
expect(missing).toHaveLength(0);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("returns missing required param names", () => {
|
|
180
|
+
const schema = {
|
|
181
|
+
type: "object" as const,
|
|
182
|
+
required: ["name", "age"],
|
|
183
|
+
properties: {
|
|
184
|
+
name: { type: "string" as const },
|
|
185
|
+
age: { type: "number" as const },
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
const missing = getMissingRequiredParams(
|
|
189
|
+
["echo", "{{name}}", "{{age}}"],
|
|
190
|
+
{ name: "Alice" },
|
|
191
|
+
schema
|
|
192
|
+
);
|
|
193
|
+
expect(missing).toEqual(["age"]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("does not report optional params as missing", () => {
|
|
197
|
+
const schema = {
|
|
198
|
+
type: "object" as const,
|
|
199
|
+
required: [],
|
|
200
|
+
properties: { verbose: { type: "boolean" as const } },
|
|
201
|
+
};
|
|
202
|
+
const missing = getMissingRequiredParams(["echo", "{{verbose}}"], {}, schema);
|
|
203
|
+
expect(missing).toHaveLength(0);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("getActionCommandParams", () => {
|
|
208
|
+
test("returns all parameter names from command", () => {
|
|
209
|
+
expect(getActionCommandParams(["cmd", "{{a}}", "{{b}}"])).toEqual(["a", "b"]);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("returns empty for command without templates", () => {
|
|
213
|
+
expect(getActionCommandParams(["echo", "hello"])).toEqual([]);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("prepareActionCommand", () => {
|
|
218
|
+
test("interpolates and returns command array", () => {
|
|
219
|
+
const schema = {
|
|
220
|
+
type: "object" as const,
|
|
221
|
+
required: ["url"],
|
|
222
|
+
properties: { url: { type: "string" as const } },
|
|
223
|
+
};
|
|
224
|
+
const result = prepareActionCommand(
|
|
225
|
+
["python", "main.py", "{{url}}"],
|
|
226
|
+
{ url: "https://example.com" },
|
|
227
|
+
schema
|
|
228
|
+
);
|
|
229
|
+
expect(result).toEqual(["python", "main.py", "https://example.com"]);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("throws when required parameters are missing", () => {
|
|
233
|
+
const schema = {
|
|
234
|
+
type: "object" as const,
|
|
235
|
+
required: ["url"],
|
|
236
|
+
properties: { url: { type: "string" as const } },
|
|
237
|
+
};
|
|
238
|
+
expect(() => prepareActionCommand(["python", "main.py", "{{url}}"], {}, schema)).toThrow(
|
|
239
|
+
"Missing required parameters: url"
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("works without schema (all params treated conservatively)", () => {
|
|
244
|
+
// Without schema, all params are treated as required
|
|
245
|
+
expect(() => prepareActionCommand(["echo", "{{name}}"], {})).toThrow(
|
|
246
|
+
"Missing required parameters: name"
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for config normalization — alias fields added in the README update.
|
|
3
|
+
*
|
|
4
|
+
* TrustConfig aliases: require_signatures → policy, trusted_publishers → auditors
|
|
5
|
+
* ExecutionConfig routing: default, fallback, trusted_scopes
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import yaml from "js-yaml";
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_CONFIG,
|
|
15
|
+
getTrustPolicy,
|
|
16
|
+
getTrustedIdentities,
|
|
17
|
+
loadConfig,
|
|
18
|
+
saveConfig,
|
|
19
|
+
} from "../src/config";
|
|
20
|
+
|
|
21
|
+
describe("config normalization", () => {
|
|
22
|
+
describe("TrustConfig aliases", () => {
|
|
23
|
+
test("require_signatures: true sets policy to require_attestation", () => {
|
|
24
|
+
const configPath = join(homedir(), ".enact", "config.yaml");
|
|
25
|
+
mkdirSync(join(homedir(), ".enact"), { recursive: true });
|
|
26
|
+
writeFileSync(
|
|
27
|
+
configPath,
|
|
28
|
+
yaml.dump({
|
|
29
|
+
trust: {
|
|
30
|
+
require_signatures: true,
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
"utf-8"
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const config = loadConfig();
|
|
37
|
+
expect(config.trust?.policy).toBe("require_attestation");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("require_signatures: false sets policy to allow", () => {
|
|
41
|
+
const configPath = join(homedir(), ".enact", "config.yaml");
|
|
42
|
+
writeFileSync(
|
|
43
|
+
configPath,
|
|
44
|
+
yaml.dump({
|
|
45
|
+
trust: {
|
|
46
|
+
require_signatures: false,
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
"utf-8"
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const config = loadConfig();
|
|
53
|
+
expect(config.trust?.policy).toBe("allow");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("explicit policy takes precedence over require_signatures", () => {
|
|
57
|
+
const configPath = join(homedir(), ".enact", "config.yaml");
|
|
58
|
+
writeFileSync(
|
|
59
|
+
configPath,
|
|
60
|
+
yaml.dump({
|
|
61
|
+
trust: {
|
|
62
|
+
policy: "prompt",
|
|
63
|
+
require_signatures: true,
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
"utf-8"
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const config = loadConfig();
|
|
70
|
+
// When policy is explicitly set, require_signatures should not override it
|
|
71
|
+
expect(config.trust?.policy).toBe("prompt");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("trusted_publishers merges into auditors", () => {
|
|
75
|
+
const configPath = join(homedir(), ".enact", "config.yaml");
|
|
76
|
+
writeFileSync(
|
|
77
|
+
configPath,
|
|
78
|
+
yaml.dump({
|
|
79
|
+
trust: {
|
|
80
|
+
auditors: ["github:existing-user"],
|
|
81
|
+
trusted_publishers: ["@my-org"],
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
"utf-8"
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const config = loadConfig();
|
|
88
|
+
expect(config.trust?.auditors).toContain("github:existing-user");
|
|
89
|
+
expect(config.trust?.auditors).toContain("@my-org");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("trusted_publishers does not create duplicates", () => {
|
|
93
|
+
const configPath = join(homedir(), ".enact", "config.yaml");
|
|
94
|
+
writeFileSync(
|
|
95
|
+
configPath,
|
|
96
|
+
yaml.dump({
|
|
97
|
+
trust: {
|
|
98
|
+
auditors: ["@my-org", "github:alice"],
|
|
99
|
+
trusted_publishers: ["@my-org"],
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
102
|
+
"utf-8"
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const config = loadConfig();
|
|
106
|
+
const orgCount = config.trust?.auditors?.filter((a) => a === "@my-org").length;
|
|
107
|
+
expect(orgCount).toBe(1);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("trusted_publishers works without existing auditors", () => {
|
|
111
|
+
const configPath = join(homedir(), ".enact", "config.yaml");
|
|
112
|
+
writeFileSync(
|
|
113
|
+
configPath,
|
|
114
|
+
yaml.dump({
|
|
115
|
+
trust: {
|
|
116
|
+
trusted_publishers: ["@my-org", "@other-org"],
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
"utf-8"
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const config = loadConfig();
|
|
123
|
+
// Should merge with default auditors
|
|
124
|
+
expect(config.trust?.auditors).toContain("@my-org");
|
|
125
|
+
expect(config.trust?.auditors).toContain("@other-org");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("getTrustPolicy respects require_signatures alias", () => {
|
|
129
|
+
const configPath = join(homedir(), ".enact", "config.yaml");
|
|
130
|
+
writeFileSync(
|
|
131
|
+
configPath,
|
|
132
|
+
yaml.dump({
|
|
133
|
+
trust: {
|
|
134
|
+
require_signatures: true,
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
"utf-8"
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
expect(getTrustPolicy()).toBe("require_attestation");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("getTrustedIdentities includes trusted_publishers", () => {
|
|
144
|
+
const configPath = join(homedir(), ".enact", "config.yaml");
|
|
145
|
+
writeFileSync(
|
|
146
|
+
configPath,
|
|
147
|
+
yaml.dump({
|
|
148
|
+
trust: {
|
|
149
|
+
auditors: ["github:alice"],
|
|
150
|
+
trusted_publishers: ["@acme"],
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
153
|
+
"utf-8"
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const identities = getTrustedIdentities();
|
|
157
|
+
expect(identities).toContain("github:alice");
|
|
158
|
+
expect(identities).toContain("@acme");
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("ExecutionConfig routing fields", () => {
|
|
163
|
+
test("default execution backend is preserved", () => {
|
|
164
|
+
const configPath = join(homedir(), ".enact", "config.yaml");
|
|
165
|
+
writeFileSync(
|
|
166
|
+
configPath,
|
|
167
|
+
yaml.dump({
|
|
168
|
+
execution: {
|
|
169
|
+
default: "docker",
|
|
170
|
+
},
|
|
171
|
+
}),
|
|
172
|
+
"utf-8"
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const config = loadConfig();
|
|
176
|
+
expect(config.execution?.default).toBe("docker");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("fallback execution backend is preserved", () => {
|
|
180
|
+
const configPath = join(homedir(), ".enact", "config.yaml");
|
|
181
|
+
writeFileSync(
|
|
182
|
+
configPath,
|
|
183
|
+
yaml.dump({
|
|
184
|
+
execution: {
|
|
185
|
+
default: "container",
|
|
186
|
+
fallback: "remote",
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
"utf-8"
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const config = loadConfig();
|
|
193
|
+
expect(config.execution?.default).toBe("container");
|
|
194
|
+
expect(config.execution?.fallback).toBe("remote");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("trusted_scopes is preserved as array", () => {
|
|
198
|
+
const configPath = join(homedir(), ".enact", "config.yaml");
|
|
199
|
+
writeFileSync(
|
|
200
|
+
configPath,
|
|
201
|
+
yaml.dump({
|
|
202
|
+
execution: {
|
|
203
|
+
default: "container",
|
|
204
|
+
fallback: "remote",
|
|
205
|
+
trusted_scopes: ["@my-org/*", "@internal/*"],
|
|
206
|
+
},
|
|
207
|
+
}),
|
|
208
|
+
"utf-8"
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const config = loadConfig();
|
|
212
|
+
expect(config.execution?.trusted_scopes).toEqual(["@my-org/*", "@internal/*"]);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("routing fields coexist with existing fields", () => {
|
|
216
|
+
const configPath = join(homedir(), ".enact", "config.yaml");
|
|
217
|
+
writeFileSync(
|
|
218
|
+
configPath,
|
|
219
|
+
yaml.dump({
|
|
220
|
+
execution: {
|
|
221
|
+
defaultTimeout: "1m",
|
|
222
|
+
verbose: true,
|
|
223
|
+
default: "docker",
|
|
224
|
+
fallback: "local",
|
|
225
|
+
trusted_scopes: ["@my-org/*"],
|
|
226
|
+
},
|
|
227
|
+
}),
|
|
228
|
+
"utf-8"
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const config = loadConfig();
|
|
232
|
+
expect(config.execution?.defaultTimeout).toBe("1m");
|
|
233
|
+
expect(config.execution?.verbose).toBe(true);
|
|
234
|
+
expect(config.execution?.default).toBe("docker");
|
|
235
|
+
expect(config.execution?.fallback).toBe("local");
|
|
236
|
+
expect(config.execution?.trusted_scopes).toEqual(["@my-org/*"]);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("missing routing fields default to undefined", () => {
|
|
240
|
+
saveConfig({ ...DEFAULT_CONFIG });
|
|
241
|
+
const config = loadConfig();
|
|
242
|
+
expect(config.execution?.default).toBeUndefined();
|
|
243
|
+
expect(config.execution?.fallback).toBeUndefined();
|
|
244
|
+
expect(config.execution?.trusted_scopes).toBeUndefined();
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe("full README-style config", () => {
|
|
249
|
+
test("parses complete README example correctly", () => {
|
|
250
|
+
const configPath = join(homedir(), ".enact", "config.yaml");
|
|
251
|
+
writeFileSync(
|
|
252
|
+
configPath,
|
|
253
|
+
yaml.dump({
|
|
254
|
+
trust: {
|
|
255
|
+
require_signatures: true,
|
|
256
|
+
trusted_publishers: ["@my-org"],
|
|
257
|
+
},
|
|
258
|
+
execution: {
|
|
259
|
+
default: "container",
|
|
260
|
+
fallback: "remote",
|
|
261
|
+
trusted_scopes: ["@my-org/*"],
|
|
262
|
+
},
|
|
263
|
+
}),
|
|
264
|
+
"utf-8"
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const config = loadConfig();
|
|
268
|
+
|
|
269
|
+
// Trust
|
|
270
|
+
expect(config.trust?.policy).toBe("require_attestation");
|
|
271
|
+
expect(config.trust?.auditors).toContain("@my-org");
|
|
272
|
+
|
|
273
|
+
// Execution
|
|
274
|
+
expect(config.execution?.default).toBe("container");
|
|
275
|
+
expect(config.execution?.fallback).toBe("remote");
|
|
276
|
+
expect(config.execution?.trusted_scopes).toEqual(["@my-org/*"]);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
});
|
package/tests/config.test.ts
CHANGED
|
@@ -534,7 +534,7 @@ describe("configuration manager", () => {
|
|
|
534
534
|
expect(existsSync(enactHome)).toBe(true);
|
|
535
535
|
});
|
|
536
536
|
|
|
537
|
-
test("creates ~/.
|
|
537
|
+
test("creates ~/.agent/skills/ directory", () => {
|
|
538
538
|
const enactHome = getEnactHome();
|
|
539
539
|
const cacheDir = getCacheDir();
|
|
540
540
|
|
|
@@ -542,6 +542,9 @@ describe("configuration manager", () => {
|
|
|
542
542
|
if (existsSync(enactHome)) {
|
|
543
543
|
rmSync(enactHome, { recursive: true, force: true });
|
|
544
544
|
}
|
|
545
|
+
if (existsSync(cacheDir)) {
|
|
546
|
+
rmSync(cacheDir, { recursive: true, force: true });
|
|
547
|
+
}
|
|
545
548
|
|
|
546
549
|
ensureGlobalSetup();
|
|
547
550
|
|