@enactprotocol/cli 2.1.7 → 2.1.10
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/dist/commands/auth/index.js +1 -1
- package/dist/commands/auth/index.js.map +1 -1
- package/dist/commands/index.d.ts +1 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +1 -1
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/info/index.d.ts +11 -0
- package/dist/commands/info/index.d.ts.map +1 -0
- package/dist/commands/info/index.js +232 -0
- package/dist/commands/info/index.js.map +1 -0
- package/dist/commands/init/index.d.ts.map +1 -1
- package/dist/commands/init/index.js +17 -14
- package/dist/commands/init/index.js.map +1 -1
- package/dist/commands/learn/index.d.ts +4 -0
- package/dist/commands/learn/index.d.ts.map +1 -1
- package/dist/commands/learn/index.js +159 -5
- package/dist/commands/learn/index.js.map +1 -1
- package/dist/commands/mcp/index.d.ts +20 -0
- package/dist/commands/mcp/index.d.ts.map +1 -0
- package/dist/commands/mcp/index.js +460 -0
- package/dist/commands/mcp/index.js.map +1 -0
- package/dist/commands/publish/index.d.ts.map +1 -1
- package/dist/commands/publish/index.js +14 -7
- package/dist/commands/publish/index.js.map +1 -1
- package/dist/commands/sign/index.d.ts +2 -1
- package/dist/commands/sign/index.d.ts.map +1 -1
- package/dist/commands/sign/index.js +75 -17
- package/dist/commands/sign/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/commands/auth/index.ts +1 -1
- package/src/commands/index.ts +1 -1
- package/src/commands/{get → info}/index.ts +103 -16
- package/src/commands/init/index.ts +17 -14
- package/src/commands/learn/index.ts +228 -5
- package/src/commands/publish/index.ts +14 -7
- package/src/commands/sign/index.ts +93 -18
- package/src/index.ts +3 -3
- package/tests/commands/{get.test.ts → info.test.ts} +35 -33
- package/tests/commands/init.test.ts +6 -6
- package/tests/commands/learn.test.ts +2 -2
- package/tests/e2e.test.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- /package/tests/fixtures/echo-tool/{enact.md → SKILL.md} +0 -0
|
@@ -26,7 +26,7 @@ const SUPABASE_ANON_KEY =
|
|
|
26
26
|
* Embedded templates (for single-binary compatibility)
|
|
27
27
|
*/
|
|
28
28
|
const TEMPLATES: Record<string, string> = {
|
|
29
|
-
"tool-
|
|
29
|
+
"tool-skill.md": `---
|
|
30
30
|
name: {{TOOL_NAME}}
|
|
31
31
|
description: A simple tool that echoes a greeting
|
|
32
32
|
version: 0.1.0
|
|
@@ -74,7 +74,7 @@ Edit this file to create your own tool:
|
|
|
74
74
|
|
|
75
75
|
"tool-agents.md": `# Enact Tool Development Guide
|
|
76
76
|
|
|
77
|
-
Enact tools are containerized, cryptographically-signed executables. Each tool is defined by
|
|
77
|
+
Enact tools are containerized, cryptographically-signed executables. Each tool is defined by a \`SKILL.md\` file (YAML frontmatter + Markdown docs).
|
|
78
78
|
|
|
79
79
|
## Quick Reference
|
|
80
80
|
|
|
@@ -85,7 +85,7 @@ Enact tools are containerized, cryptographically-signed executables. Each tool i
|
|
|
85
85
|
| Dry run | \`enact run ./ --args '{}' --dry-run\` |
|
|
86
86
|
| Sign & publish | \`enact sign ./ && enact publish ./\` |
|
|
87
87
|
|
|
88
|
-
##
|
|
88
|
+
## SKILL.md Structure
|
|
89
89
|
|
|
90
90
|
\`\`\`yaml
|
|
91
91
|
---
|
|
@@ -225,7 +225,7 @@ Tools run in a container with \`/work\` as the working directory. All source fil
|
|
|
225
225
|
|
|
226
226
|
## Secrets
|
|
227
227
|
|
|
228
|
-
Declare in \`
|
|
228
|
+
Declare in \`SKILL.md\`:
|
|
229
229
|
\`\`\`yaml
|
|
230
230
|
env:
|
|
231
231
|
API_KEY:
|
|
@@ -302,6 +302,7 @@ enact run ./path/to/tool --args '{}' # Run local tool
|
|
|
302
302
|
\`\`\`bash
|
|
303
303
|
enact search "pdf extraction" # Search registry
|
|
304
304
|
enact get author/category/tool # View tool info
|
|
305
|
+
enact learn author/category/tool # View tool documentation
|
|
305
306
|
enact install author/category/tool # Add to project (.enact/tools.json)
|
|
306
307
|
enact install author/category/tool --global # Add globally
|
|
307
308
|
enact list # List project tools
|
|
@@ -314,7 +315,7 @@ enact run tool --args '{}' | jq '.result'
|
|
|
314
315
|
\`\`\`
|
|
315
316
|
|
|
316
317
|
## Creating Local Tools
|
|
317
|
-
Create \`tools/<name>/
|
|
318
|
+
Create \`tools/<name>/SKILL.md\` with:
|
|
318
319
|
\`\`\`yaml
|
|
319
320
|
---
|
|
320
321
|
name: my-tool
|
|
@@ -345,6 +346,7 @@ This project uses Enact tools — containerized, signed executables you can run
|
|
|
345
346
|
\`\`\`bash
|
|
346
347
|
enact run <tool> --args '{"key": "value"}' # Run a tool
|
|
347
348
|
enact search "keyword" # Find tools
|
|
349
|
+
enact learn author/tool # View tool documentation
|
|
348
350
|
enact install author/tool # Install tool
|
|
349
351
|
enact list # List installed tools
|
|
350
352
|
\`\`\`
|
|
@@ -360,7 +362,7 @@ enact run tool --args '{}' | jq '.data'
|
|
|
360
362
|
\`\`\`
|
|
361
363
|
|
|
362
364
|
## Creating Tools
|
|
363
|
-
Create \`
|
|
365
|
+
Create \`SKILL.md\` in a directory:
|
|
364
366
|
\`\`\`yaml
|
|
365
367
|
---
|
|
366
368
|
name: namespace/category/tool
|
|
@@ -394,7 +396,7 @@ enact sign ./ && enact publish ./ # Publish
|
|
|
394
396
|
\`\`\`
|
|
395
397
|
|
|
396
398
|
## Secrets
|
|
397
|
-
Declare in
|
|
399
|
+
Declare in SKILL.md, set via CLI:
|
|
398
400
|
\`\`\`yaml
|
|
399
401
|
env:
|
|
400
402
|
API_KEY: # Declared but not set
|
|
@@ -543,9 +545,9 @@ async function initHandler(options: InitOptions, ctx: CommandContext): Promise<v
|
|
|
543
545
|
const isClaudeMode = options.claude;
|
|
544
546
|
// Default to agent mode if no flag specified
|
|
545
547
|
|
|
546
|
-
// Handle --tool mode: create
|
|
548
|
+
// Handle --tool mode: create SKILL.md + AGENTS.md for tool development
|
|
547
549
|
if (isToolMode) {
|
|
548
|
-
const manifestPath = join(targetDir, "
|
|
550
|
+
const manifestPath = join(targetDir, "SKILL.md");
|
|
549
551
|
const agentsPath = join(targetDir, "AGENTS.md");
|
|
550
552
|
|
|
551
553
|
if (existsSync(manifestPath) && !options.force) {
|
|
@@ -571,7 +573,7 @@ async function initHandler(options: InitOptions, ctx: CommandContext): Promise<v
|
|
|
571
573
|
|
|
572
574
|
// Load templates with placeholder replacement
|
|
573
575
|
const replacements = { TOOL_NAME: toolName };
|
|
574
|
-
const manifestContent = loadTemplate("tool-
|
|
576
|
+
const manifestContent = loadTemplate("tool-skill.md", replacements);
|
|
575
577
|
const agentsContent = loadTemplate("tool-agents.md", replacements);
|
|
576
578
|
|
|
577
579
|
// Ensure directory exists
|
|
@@ -579,7 +581,7 @@ async function initHandler(options: InitOptions, ctx: CommandContext): Promise<v
|
|
|
579
581
|
mkdirSync(targetDir, { recursive: true });
|
|
580
582
|
}
|
|
581
583
|
|
|
582
|
-
// Write
|
|
584
|
+
// Write SKILL.md
|
|
583
585
|
writeFileSync(manifestPath, manifestContent, "utf-8");
|
|
584
586
|
success(`Created tool manifest: ${manifestPath}`);
|
|
585
587
|
|
|
@@ -593,7 +595,7 @@ async function initHandler(options: InitOptions, ctx: CommandContext): Promise<v
|
|
|
593
595
|
|
|
594
596
|
info("");
|
|
595
597
|
info("Next steps:");
|
|
596
|
-
info(" 1. Edit
|
|
598
|
+
info(" 1. Edit SKILL.md to customize your tool");
|
|
597
599
|
info(" 2. Run 'enact run ./' to test your tool");
|
|
598
600
|
info(" 3. Run 'enact publish' to share your tool");
|
|
599
601
|
return;
|
|
@@ -637,7 +639,8 @@ async function initHandler(options: InitOptions, ctx: CommandContext): Promise<v
|
|
|
637
639
|
|
|
638
640
|
info("");
|
|
639
641
|
info("This file helps AI agents understand how to use Enact tools in your project.");
|
|
640
|
-
info("Run 'enact search <query>' to find tools, 'enact
|
|
642
|
+
info("Run 'enact search <query>' to find tools, 'enact learn <tool>' to view docs,");
|
|
643
|
+
info("and 'enact install <tool>' to add them.");
|
|
641
644
|
}
|
|
642
645
|
|
|
643
646
|
/**
|
|
@@ -649,7 +652,7 @@ export function configureInitCommand(program: Command): void {
|
|
|
649
652
|
.description("Initialize Enact in the current directory")
|
|
650
653
|
.option("-n, --name <name>", "Tool name (default: username/my-tool)")
|
|
651
654
|
.option("-f, --force", "Overwrite existing files")
|
|
652
|
-
.option("--tool", "Create a new Enact tool (
|
|
655
|
+
.option("--tool", "Create a new Enact tool (SKILL.md + AGENTS.md)")
|
|
653
656
|
.option("--agent", "Create AGENTS.md + .enact/tools.json (default)")
|
|
654
657
|
.option("--claude", "Create CLAUDE.md + .enact/tools.json")
|
|
655
658
|
.option("-v, --verbose", "Show detailed output")
|
|
@@ -3,16 +3,46 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Display the documentation (enact.md) for a tool.
|
|
5
5
|
* Fetches and displays the raw manifest content for easy reading.
|
|
6
|
+
*
|
|
7
|
+
* Security: For tools fetched from the registry, attestation checks are
|
|
8
|
+
* performed according to the trust policy. This prevents potentially
|
|
9
|
+
* malicious documentation from being displayed to LLMs or users.
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
|
-
import {
|
|
9
|
-
|
|
12
|
+
import {
|
|
13
|
+
type AttestationListResponse,
|
|
14
|
+
createApiClient,
|
|
15
|
+
getAttestationList,
|
|
16
|
+
getToolInfo,
|
|
17
|
+
getToolVersion,
|
|
18
|
+
verifyAllAttestations,
|
|
19
|
+
} from "@enactprotocol/api";
|
|
20
|
+
import {
|
|
21
|
+
getMinimumAttestations,
|
|
22
|
+
getTrustPolicy,
|
|
23
|
+
getTrustedAuditors,
|
|
24
|
+
loadConfig,
|
|
25
|
+
tryResolveTool,
|
|
26
|
+
} from "@enactprotocol/shared";
|
|
10
27
|
import type { Command } from "commander";
|
|
11
28
|
import type { CommandContext, GlobalOptions } from "../../types";
|
|
12
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
TrustError,
|
|
31
|
+
confirm,
|
|
32
|
+
dim,
|
|
33
|
+
error,
|
|
34
|
+
formatError,
|
|
35
|
+
header,
|
|
36
|
+
info,
|
|
37
|
+
json,
|
|
38
|
+
newline,
|
|
39
|
+
success,
|
|
40
|
+
symbols,
|
|
41
|
+
} from "../../utils";
|
|
13
42
|
|
|
14
43
|
interface LearnOptions extends GlobalOptions {
|
|
15
44
|
ver?: string;
|
|
45
|
+
local?: boolean;
|
|
16
46
|
}
|
|
17
47
|
|
|
18
48
|
/**
|
|
@@ -21,14 +51,76 @@ interface LearnOptions extends GlobalOptions {
|
|
|
21
51
|
async function learnHandler(
|
|
22
52
|
toolName: string,
|
|
23
53
|
options: LearnOptions,
|
|
24
|
-
|
|
54
|
+
ctx: CommandContext
|
|
25
55
|
): Promise<void> {
|
|
56
|
+
// First, try to resolve locally (project → user → cache)
|
|
57
|
+
// If the tool is already installed/cached, we trust it
|
|
58
|
+
const resolution = tryResolveTool(toolName, { startDir: ctx.cwd });
|
|
59
|
+
|
|
60
|
+
if (resolution) {
|
|
61
|
+
// Tool is installed locally - read documentation from the manifest file
|
|
62
|
+
if (resolution.manifestPath.endsWith(".md")) {
|
|
63
|
+
const { readFileSync } = await import("node:fs");
|
|
64
|
+
const content = readFileSync(resolution.manifestPath, "utf-8");
|
|
65
|
+
|
|
66
|
+
if (options.json) {
|
|
67
|
+
json({
|
|
68
|
+
name: toolName,
|
|
69
|
+
version: resolution.manifest.version,
|
|
70
|
+
documentation: content,
|
|
71
|
+
source: "local",
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
header(`${toolName}@${resolution.manifest.version ?? "local"}`);
|
|
77
|
+
dim("(installed locally)");
|
|
78
|
+
newline();
|
|
79
|
+
console.log(content);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Fallback for non-.md manifests
|
|
84
|
+
if (options.json) {
|
|
85
|
+
json({
|
|
86
|
+
name: toolName,
|
|
87
|
+
version: resolution.manifest.version,
|
|
88
|
+
documentation: resolution.manifest.doc ?? resolution.manifest.description ?? null,
|
|
89
|
+
source: "local",
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
header(`${toolName}@${resolution.manifest.version ?? "local"}`);
|
|
95
|
+
dim("(installed locally)");
|
|
96
|
+
newline();
|
|
97
|
+
console.log(
|
|
98
|
+
resolution.manifest.doc ?? resolution.manifest.description ?? "No documentation available."
|
|
99
|
+
);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// If --local flag is set, don't fetch from registry
|
|
104
|
+
if (options.local) {
|
|
105
|
+
error(`Tool not found locally: ${toolName}`);
|
|
106
|
+
dim("The tool is not installed. Remove --local to fetch from registry.");
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Tool not installed - fetch from registry with attestation checks
|
|
26
111
|
const config = loadConfig();
|
|
27
112
|
const registryUrl =
|
|
28
113
|
process.env.ENACT_REGISTRY_URL ??
|
|
29
114
|
config.registry?.url ??
|
|
30
115
|
"https://siikwkfgsmouioodghho.supabase.co/functions/v1";
|
|
31
|
-
|
|
116
|
+
|
|
117
|
+
// Get auth token - use user token if available, otherwise use anon key for public access
|
|
118
|
+
let authToken = config.registry?.authToken ?? process.env.ENACT_AUTH_TOKEN;
|
|
119
|
+
if (!authToken && registryUrl.includes("siikwkfgsmouioodghho.supabase.co")) {
|
|
120
|
+
authToken =
|
|
121
|
+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNpaWt3a2Znc21vdWlvb2RnaGhvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjQ2MTkzMzksImV4cCI6MjA4MDE5NTMzOX0.kxnx6-IPFhmGx6rzNx36vbyhFMFZKP_jFqaDbKnJ_E0";
|
|
122
|
+
}
|
|
123
|
+
|
|
32
124
|
const client = createApiClient({
|
|
33
125
|
baseUrl: registryUrl,
|
|
34
126
|
authToken: authToken,
|
|
@@ -42,14 +134,139 @@ async function learnHandler(
|
|
|
42
134
|
version = toolInfo.latestVersion;
|
|
43
135
|
}
|
|
44
136
|
|
|
137
|
+
if (!version) {
|
|
138
|
+
error(`No published versions for ${toolName}`);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
|
|
45
142
|
// Get the version info which includes rawManifest
|
|
46
143
|
const versionInfo = await getToolVersion(client, toolName, version);
|
|
47
144
|
|
|
145
|
+
// ========================================
|
|
146
|
+
// TRUST VERIFICATION - same as run command
|
|
147
|
+
// ========================================
|
|
148
|
+
const trustPolicy = getTrustPolicy();
|
|
149
|
+
const minimumAttestations = getMinimumAttestations();
|
|
150
|
+
const trustedAuditors = getTrustedAuditors();
|
|
151
|
+
|
|
152
|
+
// Fetch attestations from registry
|
|
153
|
+
const attestationsResponse: AttestationListResponse = await getAttestationList(
|
|
154
|
+
client,
|
|
155
|
+
toolName,
|
|
156
|
+
version
|
|
157
|
+
);
|
|
158
|
+
const attestations = attestationsResponse.attestations;
|
|
159
|
+
|
|
160
|
+
if (attestations.length === 0) {
|
|
161
|
+
// No attestations found
|
|
162
|
+
info(`${symbols.warning} Tool ${toolName}@${version} has no attestations.`);
|
|
163
|
+
|
|
164
|
+
if (trustPolicy === "require_attestation") {
|
|
165
|
+
throw new TrustError(
|
|
166
|
+
"Trust policy requires attestations. Cannot display documentation from unverified tools."
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
if (ctx.isInteractive && trustPolicy === "prompt") {
|
|
170
|
+
dim("Documentation from unverified tools may contain malicious content.");
|
|
171
|
+
const proceed = await confirm("View documentation from unverified tool?");
|
|
172
|
+
if (!proceed) {
|
|
173
|
+
info("Cancelled.");
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}
|
|
176
|
+
} else if (!ctx.isInteractive && trustPolicy === "prompt") {
|
|
177
|
+
throw new TrustError(
|
|
178
|
+
"Cannot display documentation from unverified tools in non-interactive mode."
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
// trustPolicy === "allow" - continue without prompting
|
|
182
|
+
} else {
|
|
183
|
+
// Verify attestations locally (never trust registry's verification status)
|
|
184
|
+
const verifiedAuditors = await verifyAllAttestations(
|
|
185
|
+
client,
|
|
186
|
+
toolName,
|
|
187
|
+
version,
|
|
188
|
+
versionInfo.bundle.hash ?? ""
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// Check verified auditors against trust config using provider:identity format
|
|
192
|
+
const trustedVerifiedAuditors = verifiedAuditors
|
|
193
|
+
.filter((auditor) => trustedAuditors.includes(auditor.providerIdentity))
|
|
194
|
+
.map((auditor) => auditor.providerIdentity);
|
|
195
|
+
|
|
196
|
+
if (trustedVerifiedAuditors.length > 0) {
|
|
197
|
+
// Check if we meet minimum attestations threshold
|
|
198
|
+
if (trustedVerifiedAuditors.length < minimumAttestations) {
|
|
199
|
+
info(
|
|
200
|
+
`${symbols.warning} Tool ${toolName}@${version} has ${trustedVerifiedAuditors.length} trusted attestation(s), but ${minimumAttestations} required.`
|
|
201
|
+
);
|
|
202
|
+
dim(`Trusted attestations: ${trustedVerifiedAuditors.join(", ")}`);
|
|
203
|
+
|
|
204
|
+
if (trustPolicy === "require_attestation") {
|
|
205
|
+
throw new TrustError(
|
|
206
|
+
`Trust policy requires at least ${minimumAttestations} attestation(s) from trusted identities.`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (ctx.isInteractive && trustPolicy === "prompt") {
|
|
210
|
+
const proceed = await confirm(
|
|
211
|
+
"View documentation with fewer attestations than required?"
|
|
212
|
+
);
|
|
213
|
+
if (!proceed) {
|
|
214
|
+
info("Cancelled.");
|
|
215
|
+
process.exit(0);
|
|
216
|
+
}
|
|
217
|
+
} else if (!ctx.isInteractive && trustPolicy === "prompt") {
|
|
218
|
+
throw new TrustError(
|
|
219
|
+
"Cannot display documentation without meeting minimum attestation requirement in non-interactive mode."
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
// trustPolicy === "allow" - continue without prompting
|
|
223
|
+
} else {
|
|
224
|
+
// Tool meets or exceeds minimum attestations
|
|
225
|
+
if (options.verbose) {
|
|
226
|
+
success(
|
|
227
|
+
`Tool verified by ${trustedVerifiedAuditors.length} trusted identity(ies): ${trustedVerifiedAuditors.join(", ")}`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
// Has attestations but none from trusted auditors
|
|
233
|
+
info(
|
|
234
|
+
`${symbols.warning} Tool ${toolName}@${version} has ${verifiedAuditors.length} attestation(s), but none from trusted auditors.`
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
if (trustPolicy === "require_attestation") {
|
|
238
|
+
dim(`Your trusted auditors: ${trustedAuditors.join(", ")}`);
|
|
239
|
+
dim(`Tool attested by: ${verifiedAuditors.map((a) => a.providerIdentity).join(", ")}`);
|
|
240
|
+
throw new TrustError(
|
|
241
|
+
"Trust policy requires attestations from trusted identities. Cannot display documentation."
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
if (ctx.isInteractive && trustPolicy === "prompt") {
|
|
245
|
+
dim(`Attested by: ${verifiedAuditors.map((a) => a.providerIdentity).join(", ")}`);
|
|
246
|
+
dim(`Your trusted auditors: ${trustedAuditors.join(", ")}`);
|
|
247
|
+
const proceed = await confirm("View documentation anyway?");
|
|
248
|
+
if (!proceed) {
|
|
249
|
+
info("Cancelled.");
|
|
250
|
+
process.exit(0);
|
|
251
|
+
}
|
|
252
|
+
} else if (!ctx.isInteractive && trustPolicy === "prompt") {
|
|
253
|
+
throw new TrustError(
|
|
254
|
+
"Cannot display documentation without trusted attestations in non-interactive mode."
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
// trustPolicy === "allow" - continue without prompting
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ========================================
|
|
262
|
+
// Display documentation (trust verified)
|
|
263
|
+
// ========================================
|
|
48
264
|
if (options.json) {
|
|
49
265
|
json({
|
|
50
266
|
name: toolName,
|
|
51
267
|
version: versionInfo.version,
|
|
52
268
|
documentation: versionInfo.rawManifest ?? null,
|
|
269
|
+
source: "registry",
|
|
53
270
|
});
|
|
54
271
|
return;
|
|
55
272
|
}
|
|
@@ -65,6 +282,10 @@ async function learnHandler(
|
|
|
65
282
|
newline();
|
|
66
283
|
console.log(versionInfo.rawManifest);
|
|
67
284
|
} catch (err) {
|
|
285
|
+
if (err instanceof TrustError) {
|
|
286
|
+
error(err.message);
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
68
289
|
if (err instanceof Error) {
|
|
69
290
|
if (err.message.includes("not_found") || err.message.includes("404")) {
|
|
70
291
|
error(`Tool not found: ${toolName}`);
|
|
@@ -88,7 +309,9 @@ export function configureLearnCommand(program: Command): void {
|
|
|
88
309
|
.command("learn <tool>")
|
|
89
310
|
.description("Display documentation (enact.md) for a tool")
|
|
90
311
|
.option("--ver <version>", "Show documentation for a specific version")
|
|
312
|
+
.option("--local", "Only show documentation for locally installed tools")
|
|
91
313
|
.option("--json", "Output as JSON")
|
|
314
|
+
.option("-v, --verbose", "Show detailed output")
|
|
92
315
|
.action(async (toolName: string, options: LearnOptions) => {
|
|
93
316
|
const ctx: CommandContext = {
|
|
94
317
|
cwd: process.cwd(),
|
|
@@ -130,9 +130,16 @@ async function createBundleFromDir(toolDir: string): Promise<Uint8Array> {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
/**
|
|
133
|
-
* Load the raw
|
|
133
|
+
* Load the raw markdown manifest file content (full documentation with frontmatter)
|
|
134
|
+
* Checks for SKILL.md first (preferred), then falls back to enact.md
|
|
134
135
|
*/
|
|
135
|
-
function
|
|
136
|
+
function loadRawManifest(toolDir: string): string | undefined {
|
|
137
|
+
// Check SKILL.md first (preferred format)
|
|
138
|
+
const skillMdPath = join(toolDir, "SKILL.md");
|
|
139
|
+
if (existsSync(skillMdPath)) {
|
|
140
|
+
return readFileSync(skillMdPath, "utf-8");
|
|
141
|
+
}
|
|
142
|
+
// Fall back to enact.md
|
|
136
143
|
const enactMdPath = join(toolDir, "enact.md");
|
|
137
144
|
if (existsSync(enactMdPath)) {
|
|
138
145
|
return readFileSync(enactMdPath, "utf-8");
|
|
@@ -299,10 +306,10 @@ async function publishHandler(
|
|
|
299
306
|
return;
|
|
300
307
|
}
|
|
301
308
|
|
|
302
|
-
// Load the full
|
|
303
|
-
const
|
|
304
|
-
if (
|
|
305
|
-
info("Found enact.md
|
|
309
|
+
// Load the full markdown manifest content (SKILL.md or enact.md)
|
|
310
|
+
const rawManifestContent = loadRawManifest(toolDir);
|
|
311
|
+
if (rawManifestContent) {
|
|
312
|
+
info("Found markdown documentation (SKILL.md or enact.md)");
|
|
306
313
|
}
|
|
307
314
|
|
|
308
315
|
// Create bundle
|
|
@@ -318,7 +325,7 @@ async function publishHandler(
|
|
|
318
325
|
name: toolName,
|
|
319
326
|
manifest: manifest as unknown as Record<string, unknown>,
|
|
320
327
|
bundle,
|
|
321
|
-
rawManifest:
|
|
328
|
+
rawManifest: rawManifestContent,
|
|
322
329
|
});
|
|
323
330
|
});
|
|
324
331
|
|
|
@@ -7,12 +7,18 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Supports both local paths and remote tool references:
|
|
9
9
|
* - Local: enact sign ./my-tool
|
|
10
|
-
* - Remote: enact sign author/tool
|
|
10
|
+
* - Remote: enact sign author/tool (prompts for version)
|
|
11
|
+
* - Remote: enact sign author/tool@1.0.0 (specific version)
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
14
15
|
import { dirname, join, resolve } from "node:path";
|
|
15
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
createApiClient,
|
|
18
|
+
getToolInfo,
|
|
19
|
+
getToolVersion,
|
|
20
|
+
submitAttestationToRegistry,
|
|
21
|
+
} from "@enactprotocol/api";
|
|
16
22
|
import { getSecret } from "@enactprotocol/secrets";
|
|
17
23
|
import {
|
|
18
24
|
addTrustedAuditor,
|
|
@@ -42,6 +48,7 @@ import {
|
|
|
42
48
|
json,
|
|
43
49
|
keyValue,
|
|
44
50
|
newline,
|
|
51
|
+
select,
|
|
45
52
|
success,
|
|
46
53
|
symbols,
|
|
47
54
|
warning,
|
|
@@ -63,26 +70,37 @@ interface SignOptions extends GlobalOptions {
|
|
|
63
70
|
const DEFAULT_BUNDLE_FILENAME = ".sigstore-bundle.json";
|
|
64
71
|
|
|
65
72
|
/**
|
|
66
|
-
* Parse a remote tool reference like "author/tool@1.0.0"
|
|
67
|
-
*
|
|
73
|
+
* Parse a remote tool reference like "author/tool@1.0.0" or "author/tool"
|
|
74
|
+
* Version is optional - if not provided, will prompt user to select
|
|
75
|
+
* Returns null if not a valid remote reference (i.e., looks like a local path)
|
|
68
76
|
*/
|
|
69
|
-
function parseRemoteToolRef(ref: string): { name: string; version: string } | null {
|
|
77
|
+
function parseRemoteToolRef(ref: string): { name: string; version: string | undefined } | null {
|
|
70
78
|
// Remote refs look like: author/tool@version or org/author/tool@version
|
|
71
|
-
// They don't start with . or / and contain
|
|
79
|
+
// They don't start with . or / and must contain at least one /
|
|
72
80
|
if (ref.startsWith(".") || ref.startsWith("/") || ref.startsWith("~")) {
|
|
73
81
|
return null;
|
|
74
82
|
}
|
|
75
83
|
|
|
84
|
+
// Must have at least one / in the name (author/tool format)
|
|
85
|
+
if (!ref.includes("/")) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
76
89
|
const atIndex = ref.lastIndexOf("@");
|
|
77
|
-
if (atIndex === -1
|
|
90
|
+
if (atIndex === -1) {
|
|
91
|
+
// No version specified - that's OK, we'll prompt for it
|
|
92
|
+
return { name: ref, version: undefined };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (atIndex === 0) {
|
|
78
96
|
return null;
|
|
79
97
|
}
|
|
80
98
|
|
|
81
99
|
const name = ref.substring(0, atIndex);
|
|
82
100
|
const version = ref.substring(atIndex + 1);
|
|
83
101
|
|
|
84
|
-
//
|
|
85
|
-
if (!
|
|
102
|
+
// Version after @ must not be empty
|
|
103
|
+
if (!version) {
|
|
86
104
|
return null;
|
|
87
105
|
}
|
|
88
106
|
|
|
@@ -276,9 +294,9 @@ function displayResult(
|
|
|
276
294
|
* Sign a remote tool from the registry
|
|
277
295
|
*/
|
|
278
296
|
async function signRemoteTool(
|
|
279
|
-
toolRef: { name: string; version: string },
|
|
297
|
+
toolRef: { name: string; version: string | undefined },
|
|
280
298
|
options: SignOptions,
|
|
281
|
-
|
|
299
|
+
ctx: CommandContext
|
|
282
300
|
): Promise<void> {
|
|
283
301
|
const config = loadConfig();
|
|
284
302
|
const registryUrl =
|
|
@@ -308,14 +326,71 @@ async function signRemoteTool(
|
|
|
308
326
|
newline();
|
|
309
327
|
}
|
|
310
328
|
|
|
329
|
+
// Resolve version - prompt if not provided
|
|
330
|
+
let targetVersion = toolRef.version;
|
|
331
|
+
|
|
332
|
+
if (!targetVersion) {
|
|
333
|
+
// Fetch tool info to get available versions
|
|
334
|
+
info(`Fetching versions for ${toolRef.name}...`);
|
|
335
|
+
|
|
336
|
+
let toolMetadata: Awaited<ReturnType<typeof getToolInfo>>;
|
|
337
|
+
try {
|
|
338
|
+
toolMetadata = await getToolInfo(client, toolRef.name);
|
|
339
|
+
} catch (err) {
|
|
340
|
+
error(`Tool not found: ${toolRef.name}`);
|
|
341
|
+
if (err instanceof Error) {
|
|
342
|
+
dim(` ${err.message}`);
|
|
343
|
+
}
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (toolMetadata.versions.length === 0) {
|
|
348
|
+
error(`No published versions found for ${toolRef.name}`);
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Filter out yanked versions for selection (unless there are no non-yanked versions)
|
|
353
|
+
const availableVersions = toolMetadata.versions.filter((v) => !v.yanked);
|
|
354
|
+
const versionsToShow = availableVersions.length > 0 ? availableVersions : toolMetadata.versions;
|
|
355
|
+
|
|
356
|
+
if (ctx.isInteractive) {
|
|
357
|
+
// Prompt user to select a version
|
|
358
|
+
newline();
|
|
359
|
+
const selectedVersion = await select(
|
|
360
|
+
"Select a version to sign:",
|
|
361
|
+
versionsToShow.map((v) => {
|
|
362
|
+
const option: { value: string; label: string; hint?: string } = {
|
|
363
|
+
value: v.version,
|
|
364
|
+
label: v.version + (v.version === toolMetadata.latestVersion ? " (latest)" : ""),
|
|
365
|
+
};
|
|
366
|
+
if (v.yanked) {
|
|
367
|
+
option.hint = "yanked";
|
|
368
|
+
}
|
|
369
|
+
return option;
|
|
370
|
+
})
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
if (!selectedVersion) {
|
|
374
|
+
info("Signing cancelled");
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
targetVersion = selectedVersion;
|
|
379
|
+
} else {
|
|
380
|
+
// Non-interactive: use latest version
|
|
381
|
+
targetVersion = toolMetadata.latestVersion;
|
|
382
|
+
info(`Using latest version: ${targetVersion}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
311
386
|
// Fetch tool info from registry
|
|
312
|
-
info(`Fetching ${toolRef.name}@${
|
|
387
|
+
info(`Fetching ${toolRef.name}@${targetVersion} from registry...`);
|
|
313
388
|
|
|
314
389
|
let toolInfo: Awaited<ReturnType<typeof getToolVersion>>;
|
|
315
390
|
try {
|
|
316
|
-
toolInfo = await getToolVersion(client, toolRef.name,
|
|
391
|
+
toolInfo = await getToolVersion(client, toolRef.name, targetVersion);
|
|
317
392
|
} catch (err) {
|
|
318
|
-
error(`Tool not found: ${toolRef.name}@${
|
|
393
|
+
error(`Tool not found: ${toolRef.name}@${targetVersion}`);
|
|
319
394
|
if (err instanceof Error) {
|
|
320
395
|
dim(` ${err.message}`);
|
|
321
396
|
}
|
|
@@ -357,7 +432,7 @@ async function signRemoteTool(
|
|
|
357
432
|
}
|
|
358
433
|
|
|
359
434
|
// Confirm signing
|
|
360
|
-
if (
|
|
435
|
+
if (ctx.isInteractive) {
|
|
361
436
|
newline();
|
|
362
437
|
const shouldSign = await confirm(
|
|
363
438
|
`Sign ${toolInfo.name}@${toolInfo.version} with your identity?`,
|
|
@@ -466,10 +541,10 @@ async function signRemoteTool(
|
|
|
466
541
|
}
|
|
467
542
|
|
|
468
543
|
// Prompt to add to trust list - extract issuer from bundle for correct identity format
|
|
469
|
-
if (
|
|
544
|
+
if (ctx.isInteractive && !options.json) {
|
|
470
545
|
const certificate = extractCertificateFromBundle(result.bundle);
|
|
471
546
|
const issuer = certificate?.identity?.issuer;
|
|
472
|
-
await promptAddToTrustList(attestationResult.auditor,
|
|
547
|
+
await promptAddToTrustList(attestationResult.auditor, ctx.isInteractive, issuer);
|
|
473
548
|
}
|
|
474
549
|
|
|
475
550
|
if (options.json) {
|
|
@@ -719,7 +794,7 @@ export function configureSignCommand(program: Command): void {
|
|
|
719
794
|
.description("Cryptographically sign a tool and submit attestation to registry")
|
|
720
795
|
.argument(
|
|
721
796
|
"<path>",
|
|
722
|
-
"Path to tool directory, manifest file, or remote tool (author/tool@version)"
|
|
797
|
+
"Path to tool directory, manifest file, or remote tool (author/tool or author/tool@version)"
|
|
723
798
|
)
|
|
724
799
|
.option("-i, --identity <email>", "Sign with specific identity (uses OAuth)")
|
|
725
800
|
.option("-o, --output <path>", "Output path for signature bundle (local only)")
|
package/src/index.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
configureConfigCommand,
|
|
16
16
|
configureEnvCommand,
|
|
17
17
|
configureExecCommand,
|
|
18
|
-
|
|
18
|
+
configureInfoCommand,
|
|
19
19
|
configureInitCommand,
|
|
20
20
|
configureInspectCommand,
|
|
21
21
|
configureInstallCommand,
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
} from "./commands";
|
|
34
34
|
import { error, formatError } from "./utils";
|
|
35
35
|
|
|
36
|
-
export const version = "2.1.
|
|
36
|
+
export const version = "2.1.10";
|
|
37
37
|
|
|
38
38
|
// Export types for external use
|
|
39
39
|
export type { GlobalOptions, CommandContext } from "./types";
|
|
@@ -63,7 +63,7 @@ async function main() {
|
|
|
63
63
|
|
|
64
64
|
// Registry commands (Phase 8)
|
|
65
65
|
configureSearchCommand(program);
|
|
66
|
-
|
|
66
|
+
configureInfoCommand(program);
|
|
67
67
|
configureLearnCommand(program);
|
|
68
68
|
configurePublishCommand(program);
|
|
69
69
|
configureAuthCommand(program);
|