@enactprotocol/cli 1.2.13 → 2.0.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 (73) hide show
  1. package/README.md +88 -0
  2. package/package.json +34 -38
  3. package/src/commands/auth/index.ts +940 -0
  4. package/src/commands/cache/index.ts +361 -0
  5. package/src/commands/config/README.md +239 -0
  6. package/src/commands/config/index.ts +164 -0
  7. package/src/commands/env/README.md +197 -0
  8. package/src/commands/env/index.ts +392 -0
  9. package/src/commands/exec/README.md +110 -0
  10. package/src/commands/exec/index.ts +195 -0
  11. package/src/commands/get/index.ts +198 -0
  12. package/src/commands/index.ts +30 -0
  13. package/src/commands/inspect/index.ts +264 -0
  14. package/src/commands/install/README.md +146 -0
  15. package/src/commands/install/index.ts +682 -0
  16. package/src/commands/list/README.md +115 -0
  17. package/src/commands/list/index.ts +138 -0
  18. package/src/commands/publish/index.ts +350 -0
  19. package/src/commands/report/index.ts +366 -0
  20. package/src/commands/run/README.md +124 -0
  21. package/src/commands/run/index.ts +686 -0
  22. package/src/commands/search/index.ts +368 -0
  23. package/src/commands/setup/index.ts +274 -0
  24. package/src/commands/sign/index.ts +652 -0
  25. package/src/commands/trust/README.md +214 -0
  26. package/src/commands/trust/index.ts +453 -0
  27. package/src/commands/unyank/index.ts +107 -0
  28. package/src/commands/yank/index.ts +143 -0
  29. package/src/index.ts +96 -0
  30. package/src/types.ts +81 -0
  31. package/src/utils/errors.ts +409 -0
  32. package/src/utils/exit-codes.ts +159 -0
  33. package/src/utils/ignore.ts +147 -0
  34. package/src/utils/index.ts +107 -0
  35. package/src/utils/output.ts +242 -0
  36. package/src/utils/spinner.ts +214 -0
  37. package/tests/commands/auth.test.ts +217 -0
  38. package/tests/commands/cache.test.ts +286 -0
  39. package/tests/commands/config.test.ts +277 -0
  40. package/tests/commands/env.test.ts +293 -0
  41. package/tests/commands/exec.test.ts +112 -0
  42. package/tests/commands/get.test.ts +179 -0
  43. package/tests/commands/inspect.test.ts +201 -0
  44. package/tests/commands/install-integration.test.ts +343 -0
  45. package/tests/commands/install.test.ts +288 -0
  46. package/tests/commands/list.test.ts +160 -0
  47. package/tests/commands/publish.test.ts +186 -0
  48. package/tests/commands/report.test.ts +194 -0
  49. package/tests/commands/run.test.ts +231 -0
  50. package/tests/commands/search.test.ts +131 -0
  51. package/tests/commands/sign.test.ts +164 -0
  52. package/tests/commands/trust.test.ts +236 -0
  53. package/tests/commands/unyank.test.ts +114 -0
  54. package/tests/commands/yank.test.ts +154 -0
  55. package/tests/e2e.test.ts +554 -0
  56. package/tests/fixtures/calculator/enact.yaml +34 -0
  57. package/tests/fixtures/echo-tool/enact.md +31 -0
  58. package/tests/fixtures/env-tool/enact.yaml +19 -0
  59. package/tests/fixtures/greeter/enact.yaml +18 -0
  60. package/tests/fixtures/invalid-tool/enact.yaml +4 -0
  61. package/tests/index.test.ts +8 -0
  62. package/tests/types.test.ts +84 -0
  63. package/tests/utils/errors.test.ts +303 -0
  64. package/tests/utils/exit-codes.test.ts +189 -0
  65. package/tests/utils/ignore.test.ts +461 -0
  66. package/tests/utils/output.test.ts +126 -0
  67. package/tsconfig.json +17 -0
  68. package/tsconfig.tsbuildinfo +1 -0
  69. package/dist/index.js +0 -231612
  70. package/dist/index.js.bak +0 -231611
  71. package/dist/web/static/app.js +0 -663
  72. package/dist/web/static/index.html +0 -117
  73. package/dist/web/static/style.css +0 -291
@@ -0,0 +1,366 @@
1
+ /**
2
+ * enact report command
3
+ *
4
+ * Report security vulnerabilities or issues with a tool.
5
+ * Creates a signed attestation with result "failed" and submits to the registry.
6
+ */
7
+
8
+ import { createApiClient, getToolVersion, submitAttestation } from "@enactprotocol/api";
9
+ import { getSecret } from "@enactprotocol/secrets";
10
+ import { loadConfig } from "@enactprotocol/shared";
11
+ import {
12
+ type EnactAuditAttestationOptions,
13
+ createEnactAuditStatement,
14
+ signAttestation,
15
+ } from "@enactprotocol/trust";
16
+ import type { Command } from "commander";
17
+ import type { CommandContext, GlobalOptions } from "../../types";
18
+ import {
19
+ colors,
20
+ dim,
21
+ error,
22
+ formatError,
23
+ info,
24
+ json,
25
+ keyValue,
26
+ newline,
27
+ success,
28
+ warning,
29
+ withSpinner,
30
+ } from "../../utils";
31
+
32
+ /** Auth namespace for token storage */
33
+ const AUTH_NAMESPACE = "enact:auth";
34
+ const ACCESS_TOKEN_KEY = "access_token";
35
+
36
+ interface ReportOptions extends GlobalOptions {
37
+ reason: string;
38
+ severity?: "critical" | "high" | "medium" | "low";
39
+ category?: "security" | "malware" | "quality" | "license" | "other";
40
+ dryRun?: boolean;
41
+ local?: boolean;
42
+ }
43
+
44
+ /** Valid severity levels */
45
+ const SEVERITY_LEVELS = ["critical", "high", "medium", "low"] as const;
46
+
47
+ /** Valid categories */
48
+ const CATEGORIES = ["security", "malware", "quality", "license", "other"] as const;
49
+
50
+ /**
51
+ * Parse tool@version format
52
+ */
53
+ function parseToolVersion(toolArg: string): { name: string; version: string | undefined } {
54
+ const atIndex = toolArg.lastIndexOf("@");
55
+
56
+ // Check if @ is part of the tool name (like @scope/package) or version separator
57
+ if (atIndex <= 0 || toolArg.startsWith("@")) {
58
+ // Could be @scope/package or @scope/package@version
59
+ const scopedMatch = toolArg.match(/^(@[^/]+\/[^@]+)(?:@(.+))?$/);
60
+ if (scopedMatch) {
61
+ return {
62
+ name: scopedMatch[1] ?? toolArg,
63
+ version: scopedMatch[2],
64
+ };
65
+ }
66
+ return { name: toolArg, version: undefined };
67
+ }
68
+
69
+ return {
70
+ name: toolArg.slice(0, atIndex),
71
+ version: toolArg.slice(atIndex + 1),
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Display report preview (dry run)
77
+ */
78
+ function displayDryRun(tool: string, version: string | undefined, options: ReportOptions): void {
79
+ newline();
80
+ info(colors.bold("Dry Run Preview - Report Submission"));
81
+ newline();
82
+
83
+ keyValue("Tool", tool);
84
+ if (version) {
85
+ keyValue("Version", version);
86
+ }
87
+ keyValue("Reason", options.reason);
88
+ keyValue("Severity", options.severity ?? "medium");
89
+ keyValue("Category", options.category ?? "other");
90
+ keyValue("Submit to registry", options.local ? "No (local only)" : "Yes");
91
+ newline();
92
+
93
+ info("Actions that would be performed:");
94
+ dim(" 1. Authenticate via OIDC (browser-based OAuth flow)");
95
+ dim(" 2. Create audit attestation with result 'failed'");
96
+ dim(" 3. Request signing certificate from Fulcio");
97
+ dim(" 4. Sign attestation with ephemeral keypair");
98
+ dim(" 5. Log signature to Rekor transparency log");
99
+ if (!options.local) {
100
+ dim(" 6. Submit signed report to Enact registry");
101
+ }
102
+ newline();
103
+
104
+ warning("Note: False reports may result in account suspension.");
105
+ dim("Your identity will be cryptographically bound to this report.");
106
+ }
107
+
108
+ /**
109
+ * Display report result
110
+ */
111
+ function displayResult(
112
+ tool: string,
113
+ version: string | undefined,
114
+ options: ReportOptions,
115
+ registryResult?: { auditor: string; rekorLogIndex: number | undefined }
116
+ ): void {
117
+ if (options.json) {
118
+ json({
119
+ success: true,
120
+ tool,
121
+ version: version ?? "latest",
122
+ reason: options.reason,
123
+ severity: options.severity ?? "medium",
124
+ category: options.category ?? "other",
125
+ registry: registryResult
126
+ ? {
127
+ submitted: true,
128
+ auditor: registryResult.auditor,
129
+ rekorLogIndex: registryResult.rekorLogIndex,
130
+ }
131
+ : { submitted: false },
132
+ });
133
+ return;
134
+ }
135
+
136
+ newline();
137
+ success(`Report submitted for ${tool}${version ? `@${version}` : ""}`);
138
+ newline();
139
+
140
+ keyValue("Severity", options.severity ?? "medium");
141
+ keyValue("Category", options.category ?? "other");
142
+
143
+ if (registryResult) {
144
+ keyValue("Auditor identity", registryResult.auditor);
145
+ if (registryResult.rekorLogIndex !== undefined) {
146
+ keyValue("Rekor log index", String(registryResult.rekorLogIndex));
147
+ }
148
+ }
149
+
150
+ newline();
151
+
152
+ info("What happens next:");
153
+ dim(" • Your signed report is recorded on the Rekor transparency log");
154
+ dim(" • Registry moderators will review your report");
155
+ dim(" • The tool publisher will be notified");
156
+ dim(" • You may be contacted for additional information");
157
+ newline();
158
+
159
+ warning("Note: Your identity is cryptographically bound to this report.");
160
+ warning("False reports may result in account suspension.");
161
+ }
162
+
163
+ /**
164
+ * Report command handler
165
+ */
166
+ async function reportHandler(
167
+ toolArg: string,
168
+ options: ReportOptions,
169
+ _ctx: CommandContext
170
+ ): Promise<void> {
171
+ // Parse tool@version
172
+ const { name: toolName, version: parsedVersion } = parseToolVersion(toolArg);
173
+
174
+ // Validate required options
175
+ if (!options.reason || options.reason.trim().length === 0) {
176
+ error("--reason is required. Please provide a description of the issue.");
177
+ process.exit(1);
178
+ }
179
+
180
+ // Validate severity if provided
181
+ if (options.severity && !SEVERITY_LEVELS.includes(options.severity)) {
182
+ error(`Invalid severity. Must be one of: ${SEVERITY_LEVELS.join(", ")}`);
183
+ process.exit(1);
184
+ }
185
+
186
+ // Validate category if provided
187
+ if (options.category && !CATEGORIES.includes(options.category)) {
188
+ error(`Invalid category. Must be one of: ${CATEGORIES.join(", ")}`);
189
+ process.exit(1);
190
+ }
191
+
192
+ // Dry run mode
193
+ if (options.dryRun) {
194
+ displayDryRun(toolName, parsedVersion, options);
195
+ return;
196
+ }
197
+
198
+ // Create API client to fetch tool info
199
+ const config = loadConfig();
200
+ const registryUrl = config.registry?.url ?? "https://registry.enact.tools";
201
+ const client = createApiClient({ baseUrl: registryUrl });
202
+
203
+ // Get version to report - either specified or latest
204
+ let version = parsedVersion;
205
+ if (!version) {
206
+ try {
207
+ const toolInfo = await withSpinner(
208
+ `Fetching ${toolName} info...`,
209
+ async () => getToolVersion(client, toolName, "latest"),
210
+ `Found ${toolName}`
211
+ );
212
+ version = toolInfo.version;
213
+ } catch (err) {
214
+ error(`Failed to find tool ${toolName}: ${formatError(err)}`);
215
+ process.exit(1);
216
+ }
217
+ }
218
+
219
+ // Create the audit notes with severity and category
220
+ const notes = `[${(options.category ?? "other").toUpperCase()}] [${(options.severity ?? "medium").toUpperCase()}] ${options.reason}`;
221
+
222
+ // Create audit statement with result "failed"
223
+ const auditOptions: EnactAuditAttestationOptions = {
224
+ toolName,
225
+ toolVersion: version,
226
+ auditor: "unknown", // Will be filled by OIDC
227
+ result: "failed",
228
+ timestamp: new Date(),
229
+ notes,
230
+ };
231
+
232
+ // Create a simple manifest placeholder for the subject hash
233
+ // In a full implementation, we would fetch the actual manifest from registry
234
+ const manifestPlaceholder = JSON.stringify({
235
+ name: toolName,
236
+ version,
237
+ reported: true,
238
+ });
239
+
240
+ const statement = createEnactAuditStatement(manifestPlaceholder, auditOptions);
241
+
242
+ if (options.verbose) {
243
+ info("Created report attestation statement:");
244
+ dim(JSON.stringify(statement, null, 2));
245
+ newline();
246
+ }
247
+
248
+ // Sign the attestation using Sigstore
249
+ info("Starting OIDC signing flow...");
250
+ dim("A browser window will open for authentication.");
251
+ newline();
252
+
253
+ const result = await withSpinner("Signing report...", async () => {
254
+ try {
255
+ return await signAttestation(statement as unknown as Record<string, unknown>, {
256
+ timeout: 120000, // 2 minutes for OIDC flow
257
+ });
258
+ } catch (err) {
259
+ if (err instanceof Error) {
260
+ if (err.message.includes("OIDC") || err.message.includes("token")) {
261
+ throw new Error(
262
+ `OIDC authentication failed: ${err.message}\nMake sure you complete the browser authentication flow.`
263
+ );
264
+ }
265
+ if (err.message.includes("Fulcio") || err.message.includes("certificate")) {
266
+ throw new Error(
267
+ `Certificate issuance failed: ${err.message}\nThis may be a temporary issue with the Sigstore infrastructure.`
268
+ );
269
+ }
270
+ if (err.message.includes("Rekor") || err.message.includes("transparency")) {
271
+ throw new Error(
272
+ `Transparency log failed: ${err.message}\nThis may be a temporary issue with the Sigstore infrastructure.`
273
+ );
274
+ }
275
+ }
276
+ throw err;
277
+ }
278
+ });
279
+
280
+ // Submit attestation to registry (unless --local)
281
+ let registryResult: { auditor: string; rekorLogIndex: number | undefined } | undefined;
282
+
283
+ if (!options.local) {
284
+ // Check for auth token from keyring
285
+ const authToken = await getSecret(AUTH_NAMESPACE, ACCESS_TOKEN_KEY);
286
+
287
+ if (!authToken) {
288
+ warning("Not authenticated with registry - report not submitted");
289
+ dim("Run 'enact auth login' to authenticate, then report again");
290
+ dim("Your report is still recorded on the Rekor transparency log.");
291
+ } else {
292
+ client.setAuthToken(authToken);
293
+
294
+ try {
295
+ const attestationResult = await withSpinner(
296
+ "Submitting report to registry...",
297
+ async () => {
298
+ return await submitAttestation(client, {
299
+ name: toolName,
300
+ version: version!,
301
+ sigstoreBundle: result.bundle as unknown as Record<string, unknown>,
302
+ });
303
+ }
304
+ );
305
+
306
+ registryResult = {
307
+ auditor: attestationResult.auditor,
308
+ rekorLogIndex: attestationResult.rekorLogIndex,
309
+ };
310
+ } catch (err) {
311
+ warning("Failed to submit report to registry");
312
+ if (err instanceof Error) {
313
+ dim(` ${err.message}`);
314
+ }
315
+ dim("Your report is still recorded on the Rekor transparency log.");
316
+ }
317
+ }
318
+ } else {
319
+ dim("Report signed locally only (--local flag)");
320
+ dim("The signature is recorded on the Rekor transparency log.");
321
+ }
322
+
323
+ // Display result
324
+ displayResult(toolName, version, options, registryResult);
325
+ }
326
+
327
+ /**
328
+ * Configure the report command
329
+ */
330
+ export function configureReportCommand(program: Command): void {
331
+ program
332
+ .command("report")
333
+ .description(
334
+ "Report security vulnerabilities or issues with a tool (creates signed attestation)"
335
+ )
336
+ .argument("<tool>", "Tool to report (name or name@version)")
337
+ .requiredOption("-r, --reason <description>", "Issue description (required)")
338
+ .option("-s, --severity <level>", "Severity level: critical, high, medium, low", "medium")
339
+ .option(
340
+ "-c, --category <type>",
341
+ "Issue type: security, malware, quality, license, other",
342
+ "other"
343
+ )
344
+ .option("--dry-run", "Show what would be submitted without submitting")
345
+ .option("--local", "Sign locally only, do not submit to registry")
346
+ .option("-v, --verbose", "Show detailed output")
347
+ .option("--json", "Output result as JSON")
348
+ .action(async (toolArg: string, options: ReportOptions) => {
349
+ const ctx: CommandContext = {
350
+ cwd: process.cwd(),
351
+ options,
352
+ isCI: Boolean(process.env.CI),
353
+ isInteractive: process.stdout.isTTY ?? false,
354
+ };
355
+
356
+ try {
357
+ await reportHandler(toolArg, options, ctx);
358
+ } catch (err) {
359
+ error(formatError(err));
360
+ if (options.verbose && err instanceof Error && err.stack) {
361
+ dim(err.stack);
362
+ }
363
+ process.exit(1);
364
+ }
365
+ });
366
+ }
@@ -0,0 +1,124 @@
1
+ # enact run
2
+
3
+ Execute a tool with its manifest-defined command in a containerized environment.
4
+
5
+ ## Synopsis
6
+
7
+ ```bash
8
+ enact run <tool> [options]
9
+ ```
10
+
11
+ ## Description
12
+
13
+ The `run` command executes a tool using the command defined in its manifest (`enact.yaml` or `enact.md`). The tool runs in an isolated container environment with:
14
+
15
+ - Input validation against the tool's JSON Schema
16
+ - Automatic secret resolution from the OS keyring
17
+ - Environment variable injection from `.env` files
18
+ - Shell-safe parameter interpolation
19
+
20
+ ## Arguments
21
+
22
+ | Argument | Description |
23
+ |----------|-------------|
24
+ | `<tool>` | Tool to run. Can be a tool name (`alice/utils/greeter`), a path (`./my-tool`), or `.` for the current directory |
25
+
26
+ ## Options
27
+
28
+ | Option | Description |
29
+ |--------|-------------|
30
+ | `-a, --args <json>` | Input arguments as a JSON object |
31
+ | `-i, --input <key=value>` | Input arguments as key=value pairs (can be repeated) |
32
+ | `-t, --timeout <duration>` | Execution timeout (e.g., `30s`, `5m`, `1h`) |
33
+ | `--no-cache` | Disable container caching |
34
+ | `--local` | Only resolve from local sources |
35
+ | `--dry-run` | Show what would be executed without running |
36
+ | `-v, --verbose` | Show detailed output including stderr and timing |
37
+ | `--json` | Output result as JSON |
38
+
39
+ ## Examples
40
+
41
+ ### Basic execution
42
+
43
+ ```bash
44
+ # Run a tool with JSON arguments
45
+ enact run alice/utils/greeter --args '{"name":"World"}'
46
+
47
+ # Run a tool with key=value arguments
48
+ enact run alice/utils/greeter --input name=World
49
+
50
+ # Run a tool from current directory
51
+ enact run . --args '{"input":"test.txt"}'
52
+ ```
53
+
54
+ ### Advanced options
55
+
56
+ ```bash
57
+ # Run with timeout
58
+ enact run slow-tool --args '{}' --timeout 5m
59
+
60
+ # Dry run to preview execution
61
+ enact run alice/utils/greeter --args '{"name":"World"}' --dry-run
62
+
63
+ # Get JSON output for scripting
64
+ enact run alice/utils/greeter --args '{"name":"World"}' --json
65
+
66
+ # Verbose mode for debugging
67
+ enact run alice/utils/greeter --args '{"name":"World"}' --verbose
68
+ ```
69
+
70
+ ### Multiple inputs
71
+
72
+ ```bash
73
+ # Mix JSON and key=value
74
+ enact run my-tool --args '{"config":{"debug":true}}' --input file=input.txt
75
+
76
+ # Multiple key=value pairs
77
+ enact run my-tool --input name=test --input count=5 --input enabled=true
78
+ ```
79
+
80
+ ## Input Resolution
81
+
82
+ Inputs are resolved in the following priority order:
83
+
84
+ 1. `--input` key=value pairs (highest priority)
85
+ 2. `--args` JSON object
86
+ 3. Default values from the manifest's `inputSchema`
87
+
88
+ ## Environment Resolution
89
+
90
+ The command automatically resolves environment variables:
91
+
92
+ 1. **Secrets** (env vars with `secret: true` in manifest):
93
+ - Resolved from OS keyring using namespace inheritance
94
+ - Never written to disk or shown in output
95
+
96
+ 2. **Environment variables** (env vars with `secret: false`):
97
+ - Resolved from local `.enact/.env` (project)
98
+ - Then from `~/.enact/.env` (global)
99
+ - Then from manifest defaults
100
+
101
+ ## Dry Run Output
102
+
103
+ When using `--dry-run`, the command shows:
104
+
105
+ - Tool name and version
106
+ - Container image that would be used
107
+ - All input parameters
108
+ - Environment variables (secrets masked as `***`)
109
+ - The interpolated command
110
+
111
+ ## Exit Codes
112
+
113
+ | Code | Description |
114
+ |------|-------------|
115
+ | `0` | Successful execution |
116
+ | `1` | Execution failed or error |
117
+ | `2` | Invalid arguments |
118
+ | `3` | Tool not found |
119
+
120
+ ## See Also
121
+
122
+ - [enact exec](../exec/README.md) - Execute arbitrary commands in a tool's container
123
+ - [enact install](../install/README.md) - Install tools
124
+ - [enact env](../env/README.md) - Manage environment variables and secrets