@ijfw/memory-server 1.4.0 → 1.4.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 +67 -0
- package/package.json +1 -1
- package/src/.registry-meta-key.pem +3 -0
- package/src/active-extension-writer.js +30 -4
- package/src/dashboard-client.html +210 -1
- package/src/dashboard-server.js +243 -0
- package/src/dispatch/extension.js +234 -1
- package/src/extension-installer.js +39 -0
- package/src/extension-permission-check.mjs +79 -0
- package/src/extension-registry.js +619 -0
- package/src/extension-signer.js +165 -0
- package/src/memory-feedback.js +194 -10
- package/src/runtime-mediator.js +30 -1
|
@@ -29,7 +29,19 @@ import {
|
|
|
29
29
|
addTrustedPublisher,
|
|
30
30
|
removeTrustedPublisher,
|
|
31
31
|
readTrustedPublishers,
|
|
32
|
+
loadPublisherKeypair,
|
|
33
|
+
signRotationToken,
|
|
34
|
+
verifyRotationToken,
|
|
32
35
|
} from '../extension-signer.js';
|
|
36
|
+
import {
|
|
37
|
+
refreshTrustFromRegistry,
|
|
38
|
+
readCachedRegistry,
|
|
39
|
+
verifyRegistry,
|
|
40
|
+
keygenMeta,
|
|
41
|
+
signRegistry,
|
|
42
|
+
verifyRegistryFile,
|
|
43
|
+
DEFAULT_REGISTRY_URL,
|
|
44
|
+
} from '../extension-registry.js';
|
|
33
45
|
import {
|
|
34
46
|
deployExtensionSkillsToPlatforms,
|
|
35
47
|
deployExtensionToAgentsMd,
|
|
@@ -355,6 +367,220 @@ async function deployOneExtension({ scope, name, extDir, projectRoot, result })
|
|
|
355
367
|
}
|
|
356
368
|
}
|
|
357
369
|
|
|
370
|
+
// === B6: Registry commands ================================================
|
|
371
|
+
|
|
372
|
+
async function cmdTrustRegistry({ args }) {
|
|
373
|
+
const url = args.trim() || DEFAULT_REGISTRY_URL;
|
|
374
|
+
try {
|
|
375
|
+
const r = await refreshTrustFromRegistry(url);
|
|
376
|
+
if (!r.ok) return { ok: false, command: 'trust-registry', error: r.error };
|
|
377
|
+
const lines = [];
|
|
378
|
+
if (r.fromCache) {
|
|
379
|
+
lines.push('(loaded from cache — network unavailable)');
|
|
380
|
+
} else if (r.diff) {
|
|
381
|
+
for (const kid of r.diff.added) lines.push(`+ added: ${kid}`);
|
|
382
|
+
for (const kid of r.diff.removed) lines.push(`- removed (revoked): ${kid}`);
|
|
383
|
+
for (const kid of r.diff.unchanged) lines.push(` unchanged: ${kid}`);
|
|
384
|
+
for (const kid of r.diff.rejected) lines.push(`! rejected: ${kid}`);
|
|
385
|
+
if (lines.length === 0) lines.push('registry applied — no changes');
|
|
386
|
+
}
|
|
387
|
+
for (const w of (r.warnings || [])) lines.push(`[warn] ${w}`);
|
|
388
|
+
return { ok: true, command: 'trust-registry', result: { url, diff: r.diff, fromCache: r.fromCache, lines } };
|
|
389
|
+
} catch (err) {
|
|
390
|
+
return { ok: false, command: 'trust-registry', error: err.message };
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function cmdRegistryStatus() {
|
|
395
|
+
try {
|
|
396
|
+
const cached = await readCachedRegistry();
|
|
397
|
+
if (!cached.registry) {
|
|
398
|
+
return { ok: true, command: 'registry-status', result: { cached: false, message: 'no cached registry' } };
|
|
399
|
+
}
|
|
400
|
+
const ageMs = cached.cachedAt ? Date.now() - cached.cachedAt : null;
|
|
401
|
+
const ageHours = ageMs !== null ? (ageMs / 3600000).toFixed(1) : null;
|
|
402
|
+
const body = JSON.stringify(cached.registry);
|
|
403
|
+
const sizeBytes = Buffer.byteLength(body, 'utf8');
|
|
404
|
+
const sigStatus = verifyRegistry(body);
|
|
405
|
+
return {
|
|
406
|
+
ok: true,
|
|
407
|
+
command: 'registry-status',
|
|
408
|
+
result: {
|
|
409
|
+
cached: true,
|
|
410
|
+
stale: cached.stale,
|
|
411
|
+
cached_at: cached.cachedAt ? new Date(cached.cachedAt).toISOString() : null,
|
|
412
|
+
age_hours: ageHours,
|
|
413
|
+
size_bytes: sizeBytes,
|
|
414
|
+
signature_valid: sigStatus.valid,
|
|
415
|
+
signature_reason: sigStatus.reason,
|
|
416
|
+
publisher_count: Object.keys(cached.registry.publishers || {}).length,
|
|
417
|
+
revoked_count: (cached.registry.revoked || []).length,
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
} catch (err) {
|
|
421
|
+
return { ok: false, command: 'registry-status', error: err.message };
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function cmdKeygenMeta({ args }) {
|
|
426
|
+
const author = args.trim();
|
|
427
|
+
if (!author) return { ok: false, command: 'keygen-meta', error: 'missing author name; usage: keygen-meta <author>' };
|
|
428
|
+
try {
|
|
429
|
+
const r = await keygenMeta(author);
|
|
430
|
+
return {
|
|
431
|
+
ok: true,
|
|
432
|
+
command: 'keygen-meta',
|
|
433
|
+
result: { keyId: r.keyId, publicKey: r.publicKey, dir: r.dir },
|
|
434
|
+
};
|
|
435
|
+
} catch (err) {
|
|
436
|
+
return { ok: false, command: 'keygen-meta', error: err.message };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function cmdSignRegistry({ args }) {
|
|
441
|
+
const registryPath = args.trim();
|
|
442
|
+
if (!registryPath) return { ok: false, command: 'sign-registry', error: 'missing path; usage: sign-registry <path>' };
|
|
443
|
+
try {
|
|
444
|
+
const r = await signRegistry(registryPath);
|
|
445
|
+
return r.ok
|
|
446
|
+
? { ok: true, command: 'sign-registry', result: { path: registryPath } }
|
|
447
|
+
: { ok: false, command: 'sign-registry', error: r.error };
|
|
448
|
+
} catch (err) {
|
|
449
|
+
return { ok: false, command: 'sign-registry', error: err.message };
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function cmdVerifyRegistry({ args }) {
|
|
454
|
+
const registryPath = args.trim();
|
|
455
|
+
if (!registryPath) return { ok: false, command: 'verify-registry', error: 'missing path; usage: verify-registry <path>' };
|
|
456
|
+
try {
|
|
457
|
+
const r = await verifyRegistryFile(registryPath);
|
|
458
|
+
return {
|
|
459
|
+
ok: r.ok,
|
|
460
|
+
command: 'verify-registry',
|
|
461
|
+
result: { path: registryPath, valid: r.valid, reason: r.reason },
|
|
462
|
+
};
|
|
463
|
+
} catch (err) {
|
|
464
|
+
return { ok: false, command: 'verify-registry', error: err.message };
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// === B8: Key rotation + revocation =========================================
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* rotate-keys <oldKeyId> <newKeyId> [--out <file>]
|
|
472
|
+
*
|
|
473
|
+
* Loads both keypairs from ~/.ijfw/keys/<keyId>/, produces a rotation token
|
|
474
|
+
* signed by the old private key, writes JSON to --out or stdout.
|
|
475
|
+
*/
|
|
476
|
+
async function cmdRotateKeys({ args }) {
|
|
477
|
+
const tokens = String(args || '').split(/\s+/).filter(Boolean);
|
|
478
|
+
|
|
479
|
+
// Extract --out flag
|
|
480
|
+
let outFile = null;
|
|
481
|
+
const keep = [];
|
|
482
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
483
|
+
if (tokens[i] === '--out' && tokens[i + 1]) {
|
|
484
|
+
outFile = tokens[i + 1];
|
|
485
|
+
i++;
|
|
486
|
+
} else {
|
|
487
|
+
keep.push(tokens[i]);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const [oldKeyId, newKeyId] = keep;
|
|
492
|
+
if (!oldKeyId || !newKeyId) {
|
|
493
|
+
return { ok: false, command: 'rotate-keys', error: 'usage: rotate-keys <oldKeyId> <newKeyId> [--out <file>]' };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const oldKp = await loadPublisherKeypair(oldKeyId);
|
|
497
|
+
if (!oldKp) return { ok: false, command: 'rotate-keys', error: `old keypair not found: ${oldKeyId}` };
|
|
498
|
+
|
|
499
|
+
const newKp = await loadPublisherKeypair(newKeyId);
|
|
500
|
+
if (!newKp) return { ok: false, command: 'rotate-keys', error: `new keypair not found: ${newKeyId}` };
|
|
501
|
+
|
|
502
|
+
let token;
|
|
503
|
+
try {
|
|
504
|
+
token = signRotationToken(oldKp.privateKey, newKp.publicKey);
|
|
505
|
+
} catch (err) {
|
|
506
|
+
return { ok: false, command: 'rotate-keys', error: `sign failed: ${err.message}` };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const json = JSON.stringify(token, null, 2) + '\n';
|
|
510
|
+
|
|
511
|
+
if (outFile) {
|
|
512
|
+
try {
|
|
513
|
+
await fs.writeFile(path.resolve(outFile), json, 'utf8');
|
|
514
|
+
} catch (err) {
|
|
515
|
+
return { ok: false, command: 'rotate-keys', error: `write failed: ${err.message}` };
|
|
516
|
+
}
|
|
517
|
+
return { ok: true, command: 'rotate-keys', result: { token, out: path.resolve(outFile) } };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return { ok: true, command: 'rotate-keys', result: { token } };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* verify-rotation-token <file>
|
|
525
|
+
*
|
|
526
|
+
* Reads a rotation token JSON, looks up the old public key from the local
|
|
527
|
+
* trusted-publishers store (or ~/.ijfw/keys/<oldKeyId>/public.pem as fallback),
|
|
528
|
+
* calls verifyRotationToken, prints verdict.
|
|
529
|
+
*/
|
|
530
|
+
async function cmdVerifyRotationToken({ args }) {
|
|
531
|
+
const filePath = String(args || '').trim();
|
|
532
|
+
if (!filePath) {
|
|
533
|
+
return { ok: false, command: 'verify-rotation-token', error: 'usage: verify-rotation-token <file>' };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
let raw;
|
|
537
|
+
try {
|
|
538
|
+
raw = await fs.readFile(path.resolve(filePath), 'utf8');
|
|
539
|
+
} catch (err) {
|
|
540
|
+
return { ok: false, command: 'verify-rotation-token', error: `read failed: ${err.message}` };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
let token;
|
|
544
|
+
try {
|
|
545
|
+
token = JSON.parse(raw);
|
|
546
|
+
} catch (err) {
|
|
547
|
+
return { ok: false, command: 'verify-rotation-token', error: `JSON parse failed: ${err.message}` };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const oldKeyId = token && token.old_key_id;
|
|
551
|
+
if (!oldKeyId) {
|
|
552
|
+
return { ok: false, command: 'verify-rotation-token', error: 'token missing old_key_id' };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Look up old public key: trusted-publishers store first, then key-dir fallback.
|
|
556
|
+
let oldPublicKey = null;
|
|
557
|
+
const store = await readTrustedPublishers();
|
|
558
|
+
const entry = store.publishers && store.publishers[oldKeyId];
|
|
559
|
+
if (entry && entry.publicKey) {
|
|
560
|
+
oldPublicKey = entry.publicKey;
|
|
561
|
+
} else {
|
|
562
|
+
// Fallback: key may be on disk even if not in trust store (already revoked).
|
|
563
|
+
const kp = await loadPublisherKeypair(oldKeyId);
|
|
564
|
+
if (kp) oldPublicKey = kp.publicKey;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (!oldPublicKey) {
|
|
568
|
+
return { ok: false, command: 'verify-rotation-token', error: `old public key not found for keyId: ${oldKeyId}` };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const verdict = verifyRotationToken(token, oldPublicKey);
|
|
572
|
+
return {
|
|
573
|
+
ok: verdict.valid,
|
|
574
|
+
command: 'verify-rotation-token',
|
|
575
|
+
result: {
|
|
576
|
+
valid: verdict.valid,
|
|
577
|
+
reason: verdict.reason,
|
|
578
|
+
old_key_id: token.old_key_id,
|
|
579
|
+
new_key_id: token.new_key_id,
|
|
580
|
+
},
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
358
584
|
async function cmdActivate({ args, projectRoot }) {
|
|
359
585
|
const name = args && args.trim();
|
|
360
586
|
if (!name) return { ok: false, command: 'activate', error: 'missing extension name; usage: activate <name>' };
|
|
@@ -394,11 +620,18 @@ export async function extensionDispatch({ command, args = '', projectRoot }) {
|
|
|
394
620
|
case 'trusted': return cmdTrusted(ctx);
|
|
395
621
|
case 'activate': return cmdActivate(ctx);
|
|
396
622
|
case 'deactivate': return cmdDeactivate(ctx);
|
|
623
|
+
case 'trust-registry': return cmdTrustRegistry(ctx);
|
|
624
|
+
case 'registry-status': return cmdRegistryStatus(ctx);
|
|
625
|
+
case 'keygen-meta': return cmdKeygenMeta(ctx);
|
|
626
|
+
case 'sign-registry': return cmdSignRegistry(ctx);
|
|
627
|
+
case 'verify-registry': return cmdVerifyRegistry(ctx);
|
|
628
|
+
case 'rotate-keys': return cmdRotateKeys(ctx);
|
|
629
|
+
case 'verify-rotation-token': return cmdVerifyRotationToken(ctx);
|
|
397
630
|
default:
|
|
398
631
|
return {
|
|
399
632
|
ok: false,
|
|
400
633
|
command,
|
|
401
|
-
error: `unknown extension command: ${command}. Supported: add | list | remove | audit | deploy-lazy | keygen | trust | untrust | trusted | activate | deactivate`,
|
|
634
|
+
error: `unknown extension command: ${command}. Supported: add | list | remove | audit | deploy-lazy | keygen | trust | untrust | trusted | activate | deactivate | trust-registry | registry-status | keygen-meta | sign-registry | verify-registry | rotate-keys | verify-rotation-token`,
|
|
402
635
|
};
|
|
403
636
|
}
|
|
404
637
|
}
|
|
@@ -36,6 +36,7 @@ import { randomBytes } from 'node:crypto';
|
|
|
36
36
|
import { spawn } from 'node:child_process';
|
|
37
37
|
import { get as httpsGet } from 'node:https';
|
|
38
38
|
import { pipeline } from 'node:stream/promises';
|
|
39
|
+
import { createInterface } from 'node:readline';
|
|
39
40
|
|
|
40
41
|
import {
|
|
41
42
|
validateExtensionManifest,
|
|
@@ -79,6 +80,35 @@ const EXTENSION_NAME_PATTERN = /^(@[a-z0-9-]+\/)?[a-z][a-z0-9-]*$/;
|
|
|
79
80
|
// Verdicts considered acceptable for a normal install (3/3 lenses).
|
|
80
81
|
const ACCEPTABLE_VERDICTS = new Set(['PASS', 'CONDITIONAL']);
|
|
81
82
|
|
|
83
|
+
// --- helpers: TTY confirmation ---------------------------------------------
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Prompt the user to confirm an untrusted publisher by typing the last 8 chars
|
|
87
|
+
* of the keyId. Returns true on match, false on mismatch or EOF.
|
|
88
|
+
* Only called when process.stdin.isTTY === true.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} keyId
|
|
91
|
+
* @returns {Promise<boolean>}
|
|
92
|
+
*/
|
|
93
|
+
export async function promptUntrustedConfirmation(keyId) {
|
|
94
|
+
const expected = keyId.slice(-8);
|
|
95
|
+
return new Promise((resolve) => {
|
|
96
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
97
|
+
const prompt =
|
|
98
|
+
`⚠️ Extension is signed by publisher keyId ${keyId} but ${keyId} is not in your trusted publishers store.\n` +
|
|
99
|
+
` Type the LAST 8 CHARS of the keyId (lowercase hex) to confirm: `;
|
|
100
|
+
let answered = false;
|
|
101
|
+
rl.question(prompt, (line) => {
|
|
102
|
+
answered = true;
|
|
103
|
+
rl.close();
|
|
104
|
+
resolve(line.trim() === expected);
|
|
105
|
+
});
|
|
106
|
+
rl.once('close', () => {
|
|
107
|
+
if (!answered) resolve(false); // EOF / ctrl-D
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
82
112
|
// --- helpers: source resolution -------------------------------------------
|
|
83
113
|
|
|
84
114
|
/**
|
|
@@ -763,6 +793,15 @@ export async function installExtension(source, opts = {}) {
|
|
|
763
793
|
errors: [`signature: verify failed: ${sigCheck.reason}${kidHint}`],
|
|
764
794
|
};
|
|
765
795
|
}
|
|
796
|
+
// B11: when stdin is a TTY, require the user to confirm by typing the
|
|
797
|
+
// last 8 chars of the keyId. Non-TTY (CI / scripted) falls through to
|
|
798
|
+
// the stderr-warn path unchanged — no regression for automation.
|
|
799
|
+
if (process.stdin.isTTY === true && sigCheck.publisherKeyId) {
|
|
800
|
+
const confirmed = await promptUntrustedConfirmation(sigCheck.publisherKeyId);
|
|
801
|
+
if (!confirmed) {
|
|
802
|
+
return { ok: false, errors: ['signature: untrusted confirmation cancelled'] };
|
|
803
|
+
}
|
|
804
|
+
}
|
|
766
805
|
process.stderr.write(
|
|
767
806
|
`[ijfw] extension-installer: signature unverified for ${manifest.name}: ${sigCheck.reason}\n`,
|
|
768
807
|
);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// IJFW v1.4.1 B7 -- shared extension permission checker.
|
|
3
|
+
//
|
|
4
|
+
// Reads ~/.ijfw/state/active-extension.json. Accepts a JSON payload on stdin
|
|
5
|
+
// with shape { hook_event_name, tool_name, tool_input } (Claude/Codex shape --
|
|
6
|
+
// callers reshape platform-specific payloads before piping here).
|
|
7
|
+
//
|
|
8
|
+
// Exit 0 = allow. Exit 1 = deny (stderr message emitted).
|
|
9
|
+
// With no active-extension.json: always exit 0 (backwards-compat invariant).
|
|
10
|
+
//
|
|
11
|
+
// Also appends one JSON line per check to ~/.ijfw/state/permission-events.jsonl
|
|
12
|
+
// (best-effort: failures are swallowed so logging never breaks tool dispatch).
|
|
13
|
+
|
|
14
|
+
import { readFile, appendFile, mkdir } from 'node:fs/promises';
|
|
15
|
+
import { homedir } from 'node:os';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
|
|
18
|
+
async function emitEvent(home, extensionName, toolName, allowed, reason) {
|
|
19
|
+
try {
|
|
20
|
+
const stateDir = join(home, '.ijfw', 'state');
|
|
21
|
+
await mkdir(stateDir, { recursive: true });
|
|
22
|
+
const event = { ts: new Date().toISOString(), extension: extensionName, tool: toolName, allowed };
|
|
23
|
+
if (reason) event.reason = reason;
|
|
24
|
+
await appendFile(join(stateDir, 'permission-events.jsonl'), JSON.stringify(event) + '\n', 'utf8');
|
|
25
|
+
} catch {
|
|
26
|
+
// Best-effort: swallow all errors.
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const home = process.env.HOME || process.env.USERPROFILE || homedir();
|
|
31
|
+
const stateFile = join(home, '.ijfw', 'state', 'active-extension.json');
|
|
32
|
+
|
|
33
|
+
let active;
|
|
34
|
+
try {
|
|
35
|
+
const raw = await readFile(stateFile, 'utf8');
|
|
36
|
+
active = JSON.parse(raw);
|
|
37
|
+
if (!active || typeof active !== 'object' || !active.name || !active.permissions) {
|
|
38
|
+
process.stderr.write('ijfw extension permission check: malformed active-extension state\n');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
} catch (err) {
|
|
42
|
+
if (err.code === 'ENOENT') process.exit(0); // no active extension -- allow
|
|
43
|
+
process.stderr.write(`ijfw extension permission check: ${err.message}\n`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const chunks = [];
|
|
48
|
+
for await (const c of process.stdin) chunks.push(c);
|
|
49
|
+
const payload_str = chunks.join('');
|
|
50
|
+
if (!payload_str.trim()) process.exit(0);
|
|
51
|
+
|
|
52
|
+
let req;
|
|
53
|
+
try { req = JSON.parse(payload_str); } catch { process.exit(0); }
|
|
54
|
+
|
|
55
|
+
const tool = req.tool_name || '';
|
|
56
|
+
const writes = new Set(active.permissions.writes || []);
|
|
57
|
+
const reads = new Set(active.permissions.reads || []);
|
|
58
|
+
const writeTools = new Set(['Edit', 'Write', 'NotebookEdit', 'Bash']);
|
|
59
|
+
const readTools = new Set(['Read', 'Glob', 'Grep', 'LS', 'NotebookRead', 'WebFetch', 'WebSearch']);
|
|
60
|
+
|
|
61
|
+
const has = (set, want) =>
|
|
62
|
+
set.has(want) ||
|
|
63
|
+
set.has('*') ||
|
|
64
|
+
[...set].some((p) => p.endsWith(':*') && want.startsWith(p.slice(0, -1)));
|
|
65
|
+
|
|
66
|
+
if (writeTools.has(tool) && !has(writes, `tool:${tool.toLowerCase()}`) && !has(writes, 'tool:*')) {
|
|
67
|
+
const reason = `not in permissions.writes`;
|
|
68
|
+
process.stderr.write(`extension "${active.name}" not permitted to use ${tool} (declare tool:${tool.toLowerCase()} in permissions.writes)\n`);
|
|
69
|
+
await emitEvent(home, active.name, tool, false, reason);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
if (readTools.has(tool) && !has(reads, `tool:${tool.toLowerCase()}`) && !has(reads, 'tool:*')) {
|
|
73
|
+
const reason = `not in permissions.reads`;
|
|
74
|
+
process.stderr.write(`extension "${active.name}" not permitted to use ${tool} (declare tool:${tool.toLowerCase()} in permissions.reads)\n`);
|
|
75
|
+
await emitEvent(home, active.name, tool, false, reason);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
await emitEvent(home, active.name, tool, true);
|
|
79
|
+
process.exit(0);
|