@enactprotocol/cli 2.0.4 → 2.0.5
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/get/index.js +4 -4
- package/dist/commands/get/index.js.map +1 -1
- package/dist/commands/run/index.d.ts.map +1 -1
- package/dist/commands/run/index.js +8 -1
- package/dist/commands/run/index.js.map +1 -1
- package/dist/commands/search/index.d.ts.map +1 -1
- package/dist/commands/search/index.js +22 -1
- package/dist/commands/search/index.js.map +1 -1
- package/dist/commands/sign/index.d.ts.map +1 -1
- package/dist/commands/sign/index.js +33 -17
- package/dist/commands/sign/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -4
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/commands/get/index.ts +5 -5
- package/src/commands/run/index.ts +10 -1
- package/src/commands/search/index.ts +30 -1
- package/src/commands/sign/index.ts +48 -20
- package/src/index.ts +2 -4
- package/tests/commands/get.test.ts +8 -6
- package/tests/commands/sign.test.ts +59 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -32,6 +32,7 @@ interface SearchOptions extends GlobalOptions {
|
|
|
32
32
|
tags?: string;
|
|
33
33
|
limit?: string;
|
|
34
34
|
offset?: string;
|
|
35
|
+
threshold?: string;
|
|
35
36
|
local?: boolean;
|
|
36
37
|
global?: boolean;
|
|
37
38
|
}
|
|
@@ -229,7 +230,15 @@ async function searchHandler(
|
|
|
229
230
|
process.env.ENACT_REGISTRY_URL ??
|
|
230
231
|
config.registry?.url ??
|
|
231
232
|
"https://siikwkfgsmouioodghho.supabase.co/functions/v1";
|
|
232
|
-
|
|
233
|
+
|
|
234
|
+
// Get auth token - use user token if available, otherwise use anon key for public access
|
|
235
|
+
let authToken = config.registry?.authToken ?? process.env.ENACT_AUTH_TOKEN;
|
|
236
|
+
if (!authToken && registryUrl.includes("siikwkfgsmouioodghho.supabase.co")) {
|
|
237
|
+
// Use the official Supabase anon key for unauthenticated access
|
|
238
|
+
authToken =
|
|
239
|
+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNpaWt3a2Znc21vdWlvb2RnaGhvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjQ2MTkzMzksImV4cCI6MjA4MDE5NTMzOX0.kxnx6-IPFhmGx6rzNx36vbyhFMFZKP_jFqaDbKnJ_E0";
|
|
240
|
+
}
|
|
241
|
+
|
|
233
242
|
const client = createApiClient({
|
|
234
243
|
baseUrl: registryUrl,
|
|
235
244
|
authToken: authToken,
|
|
@@ -237,12 +246,16 @@ async function searchHandler(
|
|
|
237
246
|
|
|
238
247
|
const limit = options.limit ? Number.parseInt(options.limit, 10) : 20;
|
|
239
248
|
const offset = options.offset ? Number.parseInt(options.offset, 10) : 0;
|
|
249
|
+
const threshold = options.threshold ? Number.parseFloat(options.threshold) : undefined;
|
|
240
250
|
|
|
241
251
|
if (ctx.options.verbose) {
|
|
242
252
|
info(`Searching for: "${query}"`);
|
|
243
253
|
if (options.tags) {
|
|
244
254
|
info(`Tags: ${options.tags}`);
|
|
245
255
|
}
|
|
256
|
+
if (threshold !== undefined) {
|
|
257
|
+
info(`Similarity threshold: ${threshold}`);
|
|
258
|
+
}
|
|
246
259
|
}
|
|
247
260
|
|
|
248
261
|
try {
|
|
@@ -251,8 +264,16 @@ async function searchHandler(
|
|
|
251
264
|
tags: options.tags,
|
|
252
265
|
limit,
|
|
253
266
|
offset,
|
|
267
|
+
threshold,
|
|
254
268
|
});
|
|
255
269
|
|
|
270
|
+
// Show search type in verbose mode
|
|
271
|
+
if (ctx.options.verbose && response.searchType) {
|
|
272
|
+
const searchTypeLabel =
|
|
273
|
+
response.searchType === "hybrid" ? "semantic + text (hybrid)" : "text only (no OpenAI key)";
|
|
274
|
+
info(`Search mode: ${searchTypeLabel}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
256
277
|
// JSON output
|
|
257
278
|
if (options.json) {
|
|
258
279
|
json({
|
|
@@ -262,6 +283,7 @@ async function searchHandler(
|
|
|
262
283
|
limit: response.limit,
|
|
263
284
|
offset: response.offset,
|
|
264
285
|
hasMore: response.hasMore,
|
|
286
|
+
searchType: response.searchType,
|
|
265
287
|
});
|
|
266
288
|
return;
|
|
267
289
|
}
|
|
@@ -269,6 +291,9 @@ async function searchHandler(
|
|
|
269
291
|
// No results
|
|
270
292
|
if (response.results.length === 0) {
|
|
271
293
|
info(`No tools found matching "${query}"`);
|
|
294
|
+
if (response.searchType === "text") {
|
|
295
|
+
dim("Note: Semantic search unavailable (OpenAI key not configured on server)");
|
|
296
|
+
}
|
|
272
297
|
dim("Try a different search term or remove tag filters");
|
|
273
298
|
return;
|
|
274
299
|
}
|
|
@@ -348,6 +373,10 @@ export function configureSearchCommand(program: Command): void {
|
|
|
348
373
|
.option("-t, --tags <tags>", "Filter by tags (comma-separated, registry only)")
|
|
349
374
|
.option("-l, --limit <number>", "Maximum results to return (default: 20, registry only)")
|
|
350
375
|
.option("-o, --offset <number>", "Pagination offset (default: 0, registry only)")
|
|
376
|
+
.option(
|
|
377
|
+
"--threshold <number>",
|
|
378
|
+
"Similarity threshold for semantic search (0.0-1.0, default: 0.1)"
|
|
379
|
+
)
|
|
351
380
|
.option("-v, --verbose", "Show detailed output")
|
|
352
381
|
.option("--json", "Output as JSON")
|
|
353
382
|
.action(async (query: string, options: SearchOptions) => {
|
|
@@ -12,11 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { dirname, join, resolve } from "node:path";
|
|
15
|
-
import {
|
|
16
|
-
createApiClient,
|
|
17
|
-
getToolVersion,
|
|
18
|
-
submitAttestation as submitAttestationToRegistry,
|
|
19
|
-
} from "@enactprotocol/api";
|
|
15
|
+
import { createApiClient, getToolVersion, submitAttestationToRegistry } from "@enactprotocol/api";
|
|
20
16
|
import { getSecret } from "@enactprotocol/secrets";
|
|
21
17
|
import {
|
|
22
18
|
addTrustedAuditor,
|
|
@@ -31,6 +27,7 @@ import {
|
|
|
31
27
|
type EnactToolAttestationOptions,
|
|
32
28
|
type SigstoreBundle,
|
|
33
29
|
createEnactToolStatement,
|
|
30
|
+
extractCertificateFromBundle,
|
|
34
31
|
signAttestation,
|
|
35
32
|
} from "@enactprotocol/trust";
|
|
36
33
|
import type { Command } from "commander";
|
|
@@ -160,7 +157,8 @@ function displayDryRun(
|
|
|
160
157
|
*/
|
|
161
158
|
async function promptAddToTrustList(
|
|
162
159
|
auditorEmail: string,
|
|
163
|
-
isInteractive: boolean
|
|
160
|
+
isInteractive: boolean,
|
|
161
|
+
issuer?: string
|
|
164
162
|
): Promise<boolean> {
|
|
165
163
|
if (!isInteractive) {
|
|
166
164
|
return false;
|
|
@@ -168,7 +166,8 @@ async function promptAddToTrustList(
|
|
|
168
166
|
|
|
169
167
|
try {
|
|
170
168
|
// Convert email to provider:identity format (e.g., github:alice)
|
|
171
|
-
|
|
169
|
+
// Pass the issuer so we can correctly determine the provider
|
|
170
|
+
const providerIdentity = emailToProviderIdentity(auditorEmail, issuer);
|
|
172
171
|
|
|
173
172
|
// Check if already in local trust list
|
|
174
173
|
const trustedAuditors = getTrustedAuditors();
|
|
@@ -387,11 +386,12 @@ async function signRemoteTool(
|
|
|
387
386
|
const attestationResult = await withSpinner(
|
|
388
387
|
"Submitting attestation to registry...",
|
|
389
388
|
async () => {
|
|
390
|
-
return await submitAttestationToRegistry(
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
389
|
+
return await submitAttestationToRegistry(
|
|
390
|
+
client,
|
|
391
|
+
toolInfo.name,
|
|
392
|
+
toolInfo.version,
|
|
393
|
+
result.bundle as unknown as Record<string, unknown>
|
|
394
|
+
);
|
|
395
395
|
}
|
|
396
396
|
);
|
|
397
397
|
|
|
@@ -402,9 +402,11 @@ async function signRemoteTool(
|
|
|
402
402
|
keyValue("Rekor log index", String(attestationResult.rekorLogIndex));
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
-
// Prompt to add to trust list
|
|
405
|
+
// Prompt to add to trust list - extract issuer from bundle for correct identity format
|
|
406
406
|
if (_ctx.isInteractive && !options.json) {
|
|
407
|
-
|
|
407
|
+
const certificate = extractCertificateFromBundle(result.bundle);
|
|
408
|
+
const issuer = certificate?.identity?.issuer;
|
|
409
|
+
await promptAddToTrustList(attestationResult.auditor, _ctx.isInteractive, issuer);
|
|
408
410
|
}
|
|
409
411
|
|
|
410
412
|
if (options.json) {
|
|
@@ -446,6 +448,28 @@ async function signLocalTool(
|
|
|
446
448
|
|
|
447
449
|
const manifest = loaded.manifest;
|
|
448
450
|
|
|
451
|
+
// Warn about local signing workflow - attestation hash won't match published bundle
|
|
452
|
+
if (_ctx.isInteractive && !options.dryRun) {
|
|
453
|
+
newline();
|
|
454
|
+
warning("Local signing creates an attestation for the manifest content hash.");
|
|
455
|
+
dim("If you plan to publish this tool, the published bundle will have a different hash.");
|
|
456
|
+
dim("The attestation won't match and verification will fail.");
|
|
457
|
+
newline();
|
|
458
|
+
info("Recommended workflow:");
|
|
459
|
+
dim(` 1. ${colors.command(`enact publish ${pathArg}`)} # Publish first`);
|
|
460
|
+
dim(
|
|
461
|
+
` 2. ${colors.command(`enact sign ${manifest.name}@${manifest.version ?? "1.0.0"}`)} # Then sign the published version`
|
|
462
|
+
);
|
|
463
|
+
newline();
|
|
464
|
+
|
|
465
|
+
const shouldContinue = await confirm("Continue with local signing anyway?", false);
|
|
466
|
+
if (!shouldContinue) {
|
|
467
|
+
info("Signing cancelled. Use the recommended workflow above.");
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
newline();
|
|
471
|
+
}
|
|
472
|
+
|
|
449
473
|
// Validate manifest
|
|
450
474
|
const validation = validateManifest(manifest);
|
|
451
475
|
if (!validation.valid && validation.errors) {
|
|
@@ -562,11 +586,12 @@ async function signLocalTool(
|
|
|
562
586
|
"Submitting attestation to registry...",
|
|
563
587
|
async () => {
|
|
564
588
|
// Submit the Sigstore bundle directly (v2 API)
|
|
565
|
-
return await submitAttestationToRegistry(
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
589
|
+
return await submitAttestationToRegistry(
|
|
590
|
+
client,
|
|
591
|
+
manifest.name,
|
|
592
|
+
manifest.version ?? "1.0.0",
|
|
593
|
+
result.bundle as unknown as Record<string, unknown>
|
|
594
|
+
);
|
|
570
595
|
}
|
|
571
596
|
);
|
|
572
597
|
|
|
@@ -576,8 +601,11 @@ async function signLocalTool(
|
|
|
576
601
|
};
|
|
577
602
|
|
|
578
603
|
// Prompt to add auditor to trust list (if interactive and not in JSON mode)
|
|
604
|
+
// Extract issuer from bundle for correct identity format
|
|
579
605
|
if (!options.json && _ctx.isInteractive) {
|
|
580
|
-
|
|
606
|
+
const certificate = extractCertificateFromBundle(result.bundle);
|
|
607
|
+
const issuer = certificate?.identity?.issuer;
|
|
608
|
+
await promptAddToTrustList(attestationResult.auditor, _ctx.isInteractive, issuer);
|
|
581
609
|
}
|
|
582
610
|
} catch (err) {
|
|
583
611
|
warning("Failed to submit attestation to registry");
|
package/src/index.ts
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
} from "./commands";
|
|
33
33
|
import { error, formatError } from "./utils";
|
|
34
34
|
|
|
35
|
-
export const version = "2.0.
|
|
35
|
+
export const version = "2.0.5";
|
|
36
36
|
|
|
37
37
|
// Export types for external use
|
|
38
38
|
export type { GlobalOptions, CommandContext } from "./types";
|
|
@@ -47,9 +47,7 @@ async function main() {
|
|
|
47
47
|
program
|
|
48
48
|
.name("enact")
|
|
49
49
|
.description("Enact - Verified, portable protocol for AI-executable tools")
|
|
50
|
-
.version(version)
|
|
51
|
-
.option("--json", "Output as JSON")
|
|
52
|
-
.option("-v, --verbose", "Enable verbose output");
|
|
50
|
+
.version(version);
|
|
53
51
|
|
|
54
52
|
// Configure all commands
|
|
55
53
|
configureSetupCommand(program);
|
|
@@ -42,24 +42,26 @@ describe("get command", () => {
|
|
|
42
42
|
expect(args[0]?.name()).toBe("tool");
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
test("has --
|
|
45
|
+
test("has --ver option for specifying version", () => {
|
|
46
46
|
const program = new Command();
|
|
47
47
|
configureGetCommand(program);
|
|
48
48
|
|
|
49
49
|
const getCmd = program.commands.find((cmd) => cmd.name() === "get");
|
|
50
50
|
const opts = getCmd?.options ?? [];
|
|
51
|
-
const
|
|
52
|
-
expect(
|
|
51
|
+
const verOpt = opts.find((o) => o.long === "--ver");
|
|
52
|
+
expect(verOpt).toBeDefined();
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
test("has -v short option for version", () => {
|
|
55
|
+
test("has -v short option for verbose (not version)", () => {
|
|
56
56
|
const program = new Command();
|
|
57
57
|
configureGetCommand(program);
|
|
58
58
|
|
|
59
59
|
const getCmd = program.commands.find((cmd) => cmd.name() === "get");
|
|
60
60
|
const opts = getCmd?.options ?? [];
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
// -v is for verbose, not version (--ver is for version)
|
|
62
|
+
const verboseOpt = opts.find((o) => o.short === "-v");
|
|
63
|
+
expect(verboseOpt).toBeDefined();
|
|
64
|
+
expect(verboseOpt?.long).toBe("--verbose");
|
|
63
65
|
});
|
|
64
66
|
|
|
65
67
|
test("has --json option", () => {
|
|
@@ -161,4 +161,63 @@ command: "echo hello"
|
|
|
161
161
|
expect(predicateType).toContain("tool");
|
|
162
162
|
});
|
|
163
163
|
});
|
|
164
|
+
|
|
165
|
+
describe("remote tool reference parsing", () => {
|
|
166
|
+
test("parses simple remote tool reference with version", () => {
|
|
167
|
+
const ref = "alice/greeter@1.2.0";
|
|
168
|
+
const atIndex = ref.lastIndexOf("@");
|
|
169
|
+
const name = ref.slice(0, atIndex);
|
|
170
|
+
const version = ref.slice(atIndex + 1);
|
|
171
|
+
|
|
172
|
+
expect(name).toBe("alice/greeter");
|
|
173
|
+
expect(version).toBe("1.2.0");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("parses namespaced tool reference with version", () => {
|
|
177
|
+
const ref = "org/utils/greeter@2.0.0";
|
|
178
|
+
const atIndex = ref.lastIndexOf("@");
|
|
179
|
+
const name = ref.slice(0, atIndex);
|
|
180
|
+
const version = ref.slice(atIndex + 1);
|
|
181
|
+
|
|
182
|
+
expect(name).toBe("org/utils/greeter");
|
|
183
|
+
expect(version).toBe("2.0.0");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("detects remote vs local tool reference", () => {
|
|
187
|
+
const isRemoteToolRef = (path: string): boolean => {
|
|
188
|
+
// Remote refs contain @ for version and don't start with . or /
|
|
189
|
+
return (
|
|
190
|
+
!path.startsWith(".") && !path.startsWith("/") && path.includes("@") && path.includes("/")
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
expect(isRemoteToolRef("alice/greeter@1.0.0")).toBe(true);
|
|
195
|
+
expect(isRemoteToolRef("./local-tool")).toBe(false);
|
|
196
|
+
expect(isRemoteToolRef("/absolute/path/tool")).toBe(false);
|
|
197
|
+
expect(isRemoteToolRef("examples/hello-python")).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("requires version in remote tool reference", () => {
|
|
201
|
+
const hasVersion = (ref: string): boolean => {
|
|
202
|
+
return ref.includes("@") && ref.lastIndexOf("@") > 0;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
expect(hasVersion("alice/greeter@1.0.0")).toBe(true);
|
|
206
|
+
expect(hasVersion("alice/greeter")).toBe(false);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("validates semver version format", () => {
|
|
210
|
+
const isValidSemver = (version: string): boolean => {
|
|
211
|
+
const semverRegex = /^\d+\.\d+\.\d+(-[\w.]+)?(\+[\w.]+)?$/;
|
|
212
|
+
return semverRegex.test(version);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
expect(isValidSemver("1.0.0")).toBe(true);
|
|
216
|
+
expect(isValidSemver("2.1.3")).toBe(true);
|
|
217
|
+
expect(isValidSemver("1.0.0-alpha")).toBe(true);
|
|
218
|
+
expect(isValidSemver("1.0.0-beta.1")).toBe(true);
|
|
219
|
+
expect(isValidSemver("invalid")).toBe(false);
|
|
220
|
+
expect(isValidSemver("1.0")).toBe(false);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
164
223
|
});
|