@useconductor/conductor 1.0.0 → 1.0.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/.github/README.md +374 -7
- package/.github/workflows/ci.yml +3 -1
- package/.github/workflows/claude-code-review.yml +1 -15
- package/.github/workflows/publish.yml +43 -0
- package/README.md +290 -121
- package/dist/cli/commands/audit.d.ts +40 -0
- package/dist/cli/commands/audit.d.ts.map +1 -0
- package/dist/cli/commands/audit.js +272 -0
- package/dist/cli/commands/audit.js.map +1 -0
- package/dist/cli/commands/circuit.d.ts +13 -0
- package/dist/cli/commands/circuit.d.ts.map +1 -0
- package/dist/cli/commands/circuit.js +53 -0
- package/dist/cli/commands/circuit.js.map +1 -0
- package/dist/cli/commands/config.d.ts +31 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +152 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/init.d.ts +5 -8
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +86 -123
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/marketplace.js +1 -1
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/commands/onboard.js +33 -11
- package/dist/cli/commands/onboard.js.map +1 -1
- package/dist/cli/commands/release.d.ts.map +1 -1
- package/dist/cli/commands/release.js +1 -1
- package/dist/cli/commands/release.js.map +1 -1
- package/dist/cli/index.js +146 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/core/audit.d.ts.map +1 -1
- package/dist/core/audit.js +5 -2
- package/dist/core/audit.js.map +1 -1
- package/dist/core/conductor.d.ts.map +1 -1
- package/dist/core/conductor.js +12 -0
- package/dist/core/conductor.js.map +1 -1
- package/dist/core/config.d.ts +3 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +46 -2
- package/dist/core/config.js.map +1 -1
- package/dist/core/database.d.ts +3 -0
- package/dist/core/database.d.ts.map +1 -1
- package/dist/core/database.js +26 -0
- package/dist/core/database.js.map +1 -1
- package/dist/core/encryption.d.ts +34 -0
- package/dist/core/encryption.d.ts.map +1 -0
- package/dist/core/encryption.js +96 -0
- package/dist/core/encryption.js.map +1 -0
- package/dist/core/zero-config.d.ts.map +1 -1
- package/dist/core/zero-config.js +1 -4
- package/dist/core/zero-config.js.map +1 -1
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +112 -16
- package/dist/dashboard/server.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +30 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/plugins/builtin/aws.d.ts +31 -0
- package/dist/plugins/builtin/aws.d.ts.map +1 -0
- package/dist/plugins/builtin/aws.js +149 -0
- package/dist/plugins/builtin/aws.js.map +1 -0
- package/dist/plugins/builtin/database.d.ts +1 -0
- package/dist/plugins/builtin/database.d.ts.map +1 -1
- package/dist/plugins/builtin/database.js +26 -1
- package/dist/plugins/builtin/database.js.map +1 -1
- package/dist/plugins/builtin/docker.d.ts +4 -0
- package/dist/plugins/builtin/docker.d.ts.map +1 -1
- package/dist/plugins/builtin/docker.js +20 -1
- package/dist/plugins/builtin/docker.js.map +1 -1
- package/dist/plugins/builtin/gcp.d.ts +28 -0
- package/dist/plugins/builtin/gcp.d.ts.map +1 -0
- package/dist/plugins/builtin/gcp.js +135 -0
- package/dist/plugins/builtin/gcp.js.map +1 -0
- package/dist/plugins/builtin/index.d.ts.map +1 -1
- package/dist/plugins/builtin/index.js +4 -0
- package/dist/plugins/builtin/index.js.map +1 -1
- package/dist/plugins/builtin/jira.d.ts.map +1 -1
- package/dist/plugins/builtin/jira.js +4 -2
- package/dist/plugins/builtin/jira.js.map +1 -1
- package/dist/plugins/builtin/linear.js +1 -1
- package/dist/plugins/builtin/linear.js.map +1 -1
- package/dist/plugins/builtin/shell.js +1 -1
- package/dist/plugins/builtin/shell.js.map +1 -1
- package/dist/plugins/builtin/slack.d.ts +1 -0
- package/dist/plugins/builtin/slack.d.ts.map +1 -1
- package/dist/plugins/builtin/slack.js +9 -1
- package/dist/plugins/builtin/slack.js.map +1 -1
- package/dist/plugins/builtin/spotify.js +1 -1
- package/dist/plugins/builtin/spotify.js.map +1 -1
- package/dist/plugins/builtin/vercel.d.ts.map +1 -1
- package/dist/plugins/builtin/vercel.js +3 -1
- package/dist/plugins/builtin/vercel.js.map +1 -1
- package/dist/security/sso.d.ts +37 -0
- package/dist/security/sso.d.ts.map +1 -0
- package/dist/security/sso.js +92 -0
- package/dist/security/sso.js.map +1 -0
- package/docs/deployment.md +201 -0
- package/docs/plugin-sdk.md +212 -0
- package/package.json +11 -8
- package/src/cli/commands/audit.ts +318 -0
- package/src/cli/commands/circuit.ts +63 -0
- package/src/cli/commands/config.ts +176 -0
- package/src/cli/commands/init.ts +87 -145
- package/src/cli/commands/marketplace.ts +1 -1
- package/src/cli/commands/onboard.ts +33 -11
- package/src/cli/commands/release.ts +13 -6
- package/src/cli/index.ts +165 -11
- package/src/core/audit.ts +5 -2
- package/src/core/conductor.ts +11 -0
- package/src/core/config.ts +47 -2
- package/src/core/database.ts +32 -0
- package/src/core/encryption.ts +110 -0
- package/src/core/zero-config.ts +1 -5
- package/src/dashboard/server.ts +135 -16
- package/src/mcp/server.ts +40 -2
- package/src/plugins/builtin/aws.ts +162 -0
- package/src/plugins/builtin/database.ts +19 -1
- package/src/plugins/builtin/docker.ts +17 -1
- package/src/plugins/builtin/gcp.ts +145 -0
- package/src/plugins/builtin/index.ts +4 -0
- package/src/plugins/builtin/jira.ts +23 -19
- package/src/plugins/builtin/linear.ts +1 -1
- package/src/plugins/builtin/shell.ts +1 -1
- package/src/plugins/builtin/slack.ts +6 -1
- package/src/plugins/builtin/spotify.ts +1 -1
- package/src/plugins/builtin/vercel.ts +3 -1
- package/src/security/sso.ts +124 -0
- package/tests/audit.test.ts +185 -0
- package/tests/circuit-breaker.test.ts +125 -0
- package/tests/docker.test.ts +244 -39
- package/tests/errors.test.ts +122 -0
- package/tests/github.test.ts.skip +392 -0
- package/tests/jira.test.ts +310 -0
- package/tests/linear.test.ts +366 -0
- package/tests/mcp.test.ts.skip +243 -0
- package/tests/notion.test.ts +257 -0
- package/tests/retry.test.ts +104 -0
- package/tests/shell.test.ts +262 -30
- package/tests/slack.test.ts +250 -0
- package/tests/stripe.test.ts +272 -0
- package/tests/validation.test.ts +173 -0
- package/tests/vercel.test.ts +368 -0
- package/tests/zero-config.test.ts +566 -0
- package/C.png +0 -0
- package/tests/mcp.test.ts +0 -14
|
@@ -52,9 +52,16 @@ async function run(cmd: string, args: string[], cwd?: string): Promise<string> {
|
|
|
52
52
|
|
|
53
53
|
function bumpVersion(current: string, bump: 'patch' | 'minor' | 'major'): string {
|
|
54
54
|
const parts = current.replace(/^v/, '').split('.').map(Number);
|
|
55
|
-
if (bump === 'major') {
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
if (bump === 'major') {
|
|
56
|
+
parts[0]++;
|
|
57
|
+
parts[1] = 0;
|
|
58
|
+
parts[2] = 0;
|
|
59
|
+
} else if (bump === 'minor') {
|
|
60
|
+
parts[1]++;
|
|
61
|
+
parts[2] = 0;
|
|
62
|
+
} else {
|
|
63
|
+
parts[2]++;
|
|
64
|
+
}
|
|
58
65
|
return parts.join('.');
|
|
59
66
|
}
|
|
60
67
|
|
|
@@ -168,7 +175,7 @@ export async function release(): Promise<void> {
|
|
|
168
175
|
try {
|
|
169
176
|
await run('npx', ['vitest', 'run', '--reporter=dot']);
|
|
170
177
|
ok();
|
|
171
|
-
} catch
|
|
178
|
+
} catch {
|
|
172
179
|
const { skipTests } = await inquirer.prompt<{ skipTests: boolean }>([
|
|
173
180
|
{
|
|
174
181
|
type: 'confirm',
|
|
@@ -241,8 +248,8 @@ export async function release(): Promise<void> {
|
|
|
241
248
|
console.log('');
|
|
242
249
|
console.log(
|
|
243
250
|
chalk.bold.white(` ✓ Published `) +
|
|
244
|
-
|
|
245
|
-
|
|
251
|
+
chalk.hex('#FF8C00')(`${pkg.name}@${newVersion}`) +
|
|
252
|
+
chalk.bold.white(` to npm`),
|
|
246
253
|
);
|
|
247
254
|
console.log('');
|
|
248
255
|
console.log(chalk.dim(` npm i -g ${pkg.name}`));
|
package/src/cli/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ const _require = createRequire(import.meta.url);
|
|
|
10
10
|
const { version: pkgVersion } = _require('../../package.json') as { version: string };
|
|
11
11
|
|
|
12
12
|
const program = new Command();
|
|
13
|
-
const conductor = new Conductor();
|
|
13
|
+
const conductor = new Conductor(undefined, { quiet: true });
|
|
14
14
|
|
|
15
15
|
program
|
|
16
16
|
.name('conductor')
|
|
@@ -161,6 +161,15 @@ function registerPluginCommands(parent: Command, cmdName: string): void {
|
|
|
161
161
|
const { onboard } = await import('./commands/onboard.js');
|
|
162
162
|
await onboard(conductor);
|
|
163
163
|
});
|
|
164
|
+
|
|
165
|
+
cmd
|
|
166
|
+
.command('create')
|
|
167
|
+
.argument('<name>', 'Plugin name')
|
|
168
|
+
.description('Scaffold a new plugin with tests')
|
|
169
|
+
.action(async (name: string) => {
|
|
170
|
+
const { pluginCreate } = await import('./commands/plugin-create.js');
|
|
171
|
+
await pluginCreate(name);
|
|
172
|
+
});
|
|
164
173
|
}
|
|
165
174
|
|
|
166
175
|
registerPluginCommands(program, 'plugins');
|
|
@@ -374,16 +383,6 @@ program
|
|
|
374
383
|
await doctor(conductor);
|
|
375
384
|
});
|
|
376
385
|
|
|
377
|
-
// ── Plugin Create ─────────────────────────────────────────────────────
|
|
378
|
-
program
|
|
379
|
-
.command('plugin create')
|
|
380
|
-
.argument('<name>', 'Plugin name')
|
|
381
|
-
.description('Scaffold a new plugin with tests')
|
|
382
|
-
.action(async (name: string) => {
|
|
383
|
-
const { pluginCreate } = await import('./commands/plugin-create.js');
|
|
384
|
-
await pluginCreate(name);
|
|
385
|
-
});
|
|
386
|
-
|
|
387
386
|
// ── Health ────────────────────────────────────────────────────────────
|
|
388
387
|
program
|
|
389
388
|
.command('health')
|
|
@@ -446,5 +445,160 @@ program
|
|
|
446
445
|
await release();
|
|
447
446
|
});
|
|
448
447
|
|
|
448
|
+
// ── Config ────────────────────────────────────────────────────────────────────
|
|
449
|
+
const config = program.command('config').description('Read and write configuration');
|
|
450
|
+
|
|
451
|
+
config
|
|
452
|
+
.command('list')
|
|
453
|
+
.description('Show all configuration keys and values')
|
|
454
|
+
.option('--json', 'Output as JSON')
|
|
455
|
+
.option('--show-secrets', 'Include secret values (use with care)')
|
|
456
|
+
.action(async (opts: { json?: boolean; showSecrets?: boolean }) => {
|
|
457
|
+
const { configList } = await import('./commands/config.js');
|
|
458
|
+
await configList(conductor, { json: opts.json, show_secrets: opts.showSecrets });
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
config
|
|
462
|
+
.command('get')
|
|
463
|
+
.argument('<key>', 'Dot-separated config key, e.g. ai.provider')
|
|
464
|
+
.description('Get a configuration value')
|
|
465
|
+
.option('--json', 'Output as JSON')
|
|
466
|
+
.action(async (key: string, opts: { json?: boolean }) => {
|
|
467
|
+
const { configGet } = await import('./commands/config.js');
|
|
468
|
+
await configGet(conductor, key, opts);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
config
|
|
472
|
+
.command('set')
|
|
473
|
+
.argument('<key>', 'Dot-separated config key')
|
|
474
|
+
.argument('<value>', 'Value to set (JSON or string)')
|
|
475
|
+
.description('Set a configuration value')
|
|
476
|
+
.action(async (key: string, value: string) => {
|
|
477
|
+
const { configSet } = await import('./commands/config.js');
|
|
478
|
+
await configSet(conductor, key, value);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
config
|
|
482
|
+
.command('path')
|
|
483
|
+
.description('Print the config file path')
|
|
484
|
+
.action(async () => {
|
|
485
|
+
const { configPath } = await import('./commands/config.js');
|
|
486
|
+
await configPath(conductor);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
config
|
|
490
|
+
.command('export')
|
|
491
|
+
.description('Export configuration as JSON')
|
|
492
|
+
.option('-o, --output <file>', 'Output file (default: stdout)')
|
|
493
|
+
.action(async (opts: { output?: string }) => {
|
|
494
|
+
const { configExport } = await import('./commands/config.js');
|
|
495
|
+
await configExport(conductor, { output: opts.output });
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
config
|
|
499
|
+
.command('reset')
|
|
500
|
+
.description('Reset configuration to defaults')
|
|
501
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
502
|
+
.action(async (opts: { yes?: boolean }) => {
|
|
503
|
+
const { configReset } = await import('./commands/config.js');
|
|
504
|
+
await configReset(conductor, opts);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
config
|
|
508
|
+
.command('validate')
|
|
509
|
+
.description('Validate configuration structure')
|
|
510
|
+
.action(async () => {
|
|
511
|
+
const { configValidate } = await import('./commands/config.js');
|
|
512
|
+
await configValidate(conductor);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// ── Audit ─────────────────────────────────────────────────────────────────────
|
|
516
|
+
const audit = program.command('audit').description('Query and verify the tamper-evident audit log');
|
|
517
|
+
|
|
518
|
+
audit
|
|
519
|
+
.command('list')
|
|
520
|
+
.description('List audit log entries')
|
|
521
|
+
.option('--actor <actor>', 'Filter by actor')
|
|
522
|
+
.option('--action <action>', 'Filter by action (tool_call, config_set, auth_login, ...)')
|
|
523
|
+
.option('--tool <tool>', 'Filter by tool/resource name')
|
|
524
|
+
.option('--result <result>', 'Filter by result (success, failure, denied, timeout)')
|
|
525
|
+
.option('--since <iso>', 'Show entries after this ISO timestamp')
|
|
526
|
+
.option('--until <iso>', 'Show entries before this ISO timestamp')
|
|
527
|
+
.option('-n, --limit <n>', 'Max entries to show', '100')
|
|
528
|
+
.option('--json', 'Output as JSON')
|
|
529
|
+
.action(async (opts) => {
|
|
530
|
+
const { auditList } = await import('./commands/audit.js');
|
|
531
|
+
await auditList(conductor, opts);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
audit
|
|
535
|
+
.command('verify')
|
|
536
|
+
.description('Verify SHA-256 chain integrity — detect tampering')
|
|
537
|
+
.option('--json', 'Output as JSON')
|
|
538
|
+
.action(async (opts: { json?: boolean }) => {
|
|
539
|
+
const { auditVerify } = await import('./commands/audit.js');
|
|
540
|
+
await auditVerify(conductor, opts);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
audit
|
|
544
|
+
.command('tail')
|
|
545
|
+
.description('Stream the audit log in real time')
|
|
546
|
+
.option('-n, --lines <n>', 'Initial lines to show', '20')
|
|
547
|
+
.option('--json', 'Output as NDJSON')
|
|
548
|
+
.action(async (opts: { lines?: string; json?: boolean }) => {
|
|
549
|
+
const { auditTail } = await import('./commands/audit.js');
|
|
550
|
+
await auditTail(conductor, opts);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
audit
|
|
554
|
+
.command('export')
|
|
555
|
+
.description('Export audit log entries to a file')
|
|
556
|
+
.option('-o, --output <file>', 'Output file path')
|
|
557
|
+
.option('--format <fmt>', 'Output format: json or ndjson', 'json')
|
|
558
|
+
.option('--since <iso>', 'Export entries after this ISO timestamp')
|
|
559
|
+
.option('--until <iso>', 'Export entries before this ISO timestamp')
|
|
560
|
+
.action(async (opts) => {
|
|
561
|
+
const { auditExport } = await import('./commands/audit.js');
|
|
562
|
+
await auditExport(conductor, opts);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
audit
|
|
566
|
+
.command('stats')
|
|
567
|
+
.description('Show audit log summary statistics')
|
|
568
|
+
.option('--json', 'Output as JSON')
|
|
569
|
+
.action(async (opts: { json?: boolean }) => {
|
|
570
|
+
const { auditStats } = await import('./commands/audit.js');
|
|
571
|
+
await auditStats(conductor, opts);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
audit
|
|
575
|
+
.command('rotate')
|
|
576
|
+
.description('Manually rotate the current audit log file')
|
|
577
|
+
.action(async () => {
|
|
578
|
+
const { auditRotate } = await import('./commands/audit.js');
|
|
579
|
+
await auditRotate(conductor);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// ── Circuit ───────────────────────────────────────────────────────────────────
|
|
583
|
+
const circuit = program.command('circuit').description('View and manage circuit breaker state');
|
|
584
|
+
|
|
585
|
+
circuit
|
|
586
|
+
.command('list')
|
|
587
|
+
.description('Show state of all circuit breakers')
|
|
588
|
+
.option('--json', 'Output as JSON')
|
|
589
|
+
.action(async (opts: { json?: boolean }) => {
|
|
590
|
+
const { circuitList } = await import('./commands/circuit.js');
|
|
591
|
+
await circuitList(conductor, opts);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
circuit
|
|
595
|
+
.command('reset')
|
|
596
|
+
.argument('<tool>', 'Tool name to reset (e.g. shell.exec)')
|
|
597
|
+
.description('Reset a circuit breaker to closed state')
|
|
598
|
+
.action(async (tool: string) => {
|
|
599
|
+
const { circuitReset } = await import('./commands/circuit.js');
|
|
600
|
+
await circuitReset(conductor, tool);
|
|
601
|
+
});
|
|
602
|
+
|
|
449
603
|
// ── Run ──────────────────────────────────────────────────────────────
|
|
450
604
|
program.parse();
|
package/src/core/audit.ts
CHANGED
|
@@ -200,10 +200,13 @@ export class AuditLogger {
|
|
|
200
200
|
for (const line of lines) {
|
|
201
201
|
const entry = JSON.parse(line) as AuditEntry;
|
|
202
202
|
|
|
203
|
-
// Reconstruct what the hash should be
|
|
203
|
+
// Reconstruct what the hash should be — must match log() exactly:
|
|
204
|
+
// log() hashes: sha256(lastHash + JSON.stringify({...entryFields, timestamp, previousHash: ''}))
|
|
205
|
+
// where entryFields = {actor, action, resource, result, metadata} (no hash field)
|
|
206
|
+
const { hash: _hash, previousHash: _prev, ...entryFields } = entry;
|
|
204
207
|
const expectedHash = crypto
|
|
205
208
|
.createHash('sha256')
|
|
206
|
-
.update(prevHash + JSON.stringify({ ...
|
|
209
|
+
.update(prevHash + JSON.stringify({ ...entryFields, previousHash: '' }))
|
|
207
210
|
.digest('hex');
|
|
208
211
|
|
|
209
212
|
if (entry.hash !== expectedHash) {
|
package/src/core/conductor.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from 'path';
|
|
1
2
|
import { ConfigManager } from './config.js';
|
|
2
3
|
import { DatabaseManager } from './database.js';
|
|
3
4
|
import { PluginManager } from '../plugins/manager.js';
|
|
@@ -39,6 +40,16 @@ export class Conductor {
|
|
|
39
40
|
await this.config.initialize();
|
|
40
41
|
await this.db.initialize();
|
|
41
42
|
await this.plugins.loadBuiltins();
|
|
43
|
+
|
|
44
|
+
// Reset audit log on fresh init (avoid tampered chain issues)
|
|
45
|
+
const { default: fs } = await import('fs/promises');
|
|
46
|
+
const auditLog = path.join(this.config.getConfigDir(), 'audit', 'audit.log');
|
|
47
|
+
try {
|
|
48
|
+
const stat = await fs.stat(auditLog);
|
|
49
|
+
if (stat.size === 0) await fs.writeFile(auditLog, '', 'utf-8');
|
|
50
|
+
} catch {
|
|
51
|
+
// First run
|
|
52
|
+
}
|
|
42
53
|
|
|
43
54
|
this.initialized = true;
|
|
44
55
|
|
package/src/core/config.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
|
+
import { EncryptionManager } from './encryption.js';
|
|
4
5
|
|
|
5
6
|
export interface ConductorConfig {
|
|
6
7
|
user?: {
|
|
@@ -59,11 +60,13 @@ export class ConfigManager {
|
|
|
59
60
|
private configDir: string;
|
|
60
61
|
private configPath: string;
|
|
61
62
|
private config: ConductorConfig;
|
|
63
|
+
private encryption: EncryptionManager;
|
|
62
64
|
|
|
63
65
|
constructor(customPath?: string) {
|
|
64
66
|
this.configDir = customPath || path.join(homedir(), '.conductor');
|
|
65
67
|
this.configPath = path.join(this.configDir, 'config.json');
|
|
66
68
|
this.config = {};
|
|
69
|
+
this.encryption = new EncryptionManager(this.configDir);
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
async initialize(): Promise<void> {
|
|
@@ -80,10 +83,51 @@ export class ConfigManager {
|
|
|
80
83
|
await this.load();
|
|
81
84
|
}
|
|
82
85
|
|
|
86
|
+
// Encrypt sensitive values before saving
|
|
87
|
+
private async encryptSensitive(obj: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
88
|
+
const SENSITIVE_KEYS = ['key', 'secret', 'token', 'password', 'api_key', 'access_token'];
|
|
89
|
+
const result: Record<string, unknown> = {};
|
|
90
|
+
|
|
91
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
92
|
+
if (typeof value === 'string' && SENSITIVE_KEYS.some((k) => key.toLowerCase().includes(k))) {
|
|
93
|
+
result[key] = await this.encryption.encrypt(value);
|
|
94
|
+
} else if (typeof value === 'object') {
|
|
95
|
+
result[key] = await this.encryptSensitive(value as Record<string, unknown>);
|
|
96
|
+
} else {
|
|
97
|
+
result[key] = value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Decrypt sensitive values after loading
|
|
105
|
+
private async decryptSensitive(obj: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
106
|
+
const SENSITIVE_KEYS = ['key', 'secret', 'token', 'password', 'api_key', 'access_token'];
|
|
107
|
+
const result: Record<string, unknown> = {};
|
|
108
|
+
|
|
109
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
110
|
+
if (typeof value === 'string' && SENSITIVE_KEYS.some((k) => key.toLowerCase().includes(k))) {
|
|
111
|
+
try {
|
|
112
|
+
result[key] = await this.encryption.decrypt(value);
|
|
113
|
+
} catch {
|
|
114
|
+
result[key] = value; // Not encrypted, use as-is
|
|
115
|
+
}
|
|
116
|
+
} else if (typeof value === 'object') {
|
|
117
|
+
result[key] = await this.decryptSensitive(value as Record<string, unknown>);
|
|
118
|
+
} else {
|
|
119
|
+
result[key] = value;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
83
126
|
async load(): Promise<void> {
|
|
84
127
|
try {
|
|
85
128
|
const data = await fs.readFile(this.configPath, 'utf-8');
|
|
86
|
-
|
|
129
|
+
const parsed = JSON.parse(data);
|
|
130
|
+
this.config = await this.decryptSensitive(parsed) as ConductorConfig;
|
|
87
131
|
} catch {
|
|
88
132
|
// Config doesn't exist yet — use defaults
|
|
89
133
|
this.config = {
|
|
@@ -106,8 +150,9 @@ export class ConfigManager {
|
|
|
106
150
|
}
|
|
107
151
|
|
|
108
152
|
async save(): Promise<void> {
|
|
153
|
+
const encrypted = await this.encryptSensitive(this.config as unknown as Record<string, unknown>);
|
|
109
154
|
const tmp = this.configPath + '.tmp';
|
|
110
|
-
await fs.writeFile(tmp, JSON.stringify(
|
|
155
|
+
await fs.writeFile(tmp, JSON.stringify(encrypted, null, 2), 'utf-8');
|
|
111
156
|
await fs.rename(tmp, this.configPath);
|
|
112
157
|
}
|
|
113
158
|
|
package/src/core/database.ts
CHANGED
|
@@ -92,6 +92,38 @@ export class DatabaseManager {
|
|
|
92
92
|
this.db = new SQL.Database();
|
|
93
93
|
await this.createTables();
|
|
94
94
|
}
|
|
95
|
+
|
|
96
|
+
// Always run migrations — safe to run on both new and existing databases
|
|
97
|
+
this.runMigrations();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Current schema version — bump this whenever tables change. */
|
|
101
|
+
private static readonly SCHEMA_VERSION = 1;
|
|
102
|
+
|
|
103
|
+
private runMigrations(): void {
|
|
104
|
+
if (!this.db) throw new Error('Database not initialized');
|
|
105
|
+
|
|
106
|
+
// Ensure schema_version table exists
|
|
107
|
+
this.db.run(`
|
|
108
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
109
|
+
version INTEGER NOT NULL,
|
|
110
|
+
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
111
|
+
)
|
|
112
|
+
`);
|
|
113
|
+
|
|
114
|
+
const stmt = this.db.prepare('SELECT MAX(version) AS v FROM schema_version');
|
|
115
|
+
stmt.step();
|
|
116
|
+
const row = stmt.getAsObject() as { v: number | null };
|
|
117
|
+
stmt.free();
|
|
118
|
+
const current = row.v ?? 0;
|
|
119
|
+
|
|
120
|
+
if (current < 1) {
|
|
121
|
+
// Migration 1: initial schema (all existing tables)
|
|
122
|
+
this.db.run(`INSERT INTO schema_version (version) VALUES (1)`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Future migrations go here:
|
|
126
|
+
// if (current < 2) { this.db.run(`ALTER TABLE ...`); this.db.run(`INSERT INTO schema_version (version) VALUES (2)`); }
|
|
95
127
|
}
|
|
96
128
|
|
|
97
129
|
private async createTables(): Promise<void> {
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encryption at rest for sensitive config data.
|
|
3
|
+
* Uses AES-256-GCM with a machine-derived key.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import crypto from 'crypto';
|
|
7
|
+
import fs from 'fs/promises';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import os from 'os';
|
|
10
|
+
|
|
11
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
12
|
+
const KEY_LENGTH = 32;
|
|
13
|
+
const IV_LENGTH = 12;
|
|
14
|
+
const AUTH_TAG_LENGTH = 16;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Derive an encryption key from machine-specific data.
|
|
18
|
+
* The key is stored in the config directory and tied to the machine.
|
|
19
|
+
*/
|
|
20
|
+
export class EncryptionManager {
|
|
21
|
+
private keyPath: string;
|
|
22
|
+
private key: Buffer | null = null;
|
|
23
|
+
|
|
24
|
+
constructor(configDir: string) {
|
|
25
|
+
this.keyPath = path.join(configDir, '.key');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get or create the encryption key.
|
|
30
|
+
*/
|
|
31
|
+
private async getKey(): Promise<Buffer> {
|
|
32
|
+
if (this.key) return this.key;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const existing = await fs.readFile(this.keyPath);
|
|
36
|
+
if (existing.length === KEY_LENGTH) {
|
|
37
|
+
this.key = existing;
|
|
38
|
+
return this.key;
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
/* key doesn't exist - generate new one */
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Generate new key
|
|
45
|
+
this.key = crypto.randomBytes(KEY_LENGTH);
|
|
46
|
+
await fs.writeFile(this.keyPath, this.key, { mode: 0o600 });
|
|
47
|
+
return this.key;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Encrypt data using AES-256-GCM.
|
|
52
|
+
*/
|
|
53
|
+
async encrypt(plaintext: string): Promise<string> {
|
|
54
|
+
const key = await this.getKey();
|
|
55
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
56
|
+
|
|
57
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
58
|
+
const encrypted = Buffer.concat([
|
|
59
|
+
cipher.update(plaintext, 'utf8'),
|
|
60
|
+
cipher.final(),
|
|
61
|
+
]);
|
|
62
|
+
const authTag = cipher.getAuthTag();
|
|
63
|
+
|
|
64
|
+
// Format: iv + authTag + ciphertext (all base64)
|
|
65
|
+
const result = Buffer.concat([iv, authTag, encrypted]);
|
|
66
|
+
return result.toString('base64');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Decrypt data using AES-256-GCM.
|
|
71
|
+
*/
|
|
72
|
+
async decrypt(ciphertext: string): Promise<string> {
|
|
73
|
+
const key = await this.getKey();
|
|
74
|
+
const data = Buffer.from(ciphertext, 'base64');
|
|
75
|
+
|
|
76
|
+
const iv = data.subarray(0, IV_LENGTH);
|
|
77
|
+
const authTag = data.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);
|
|
78
|
+
const encrypted = data.subarray(IV_LENGTH + AUTH_TAG_LENGTH);
|
|
79
|
+
|
|
80
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
81
|
+
decipher.setAuthTag(authTag);
|
|
82
|
+
|
|
83
|
+
const decrypted = Buffer.concat([
|
|
84
|
+
decipher.update(encrypted),
|
|
85
|
+
decipher.final(),
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
return decrypted.toString('utf8');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check if encryption has been initialized.
|
|
93
|
+
*/
|
|
94
|
+
async isInitialized(): Promise<boolean> {
|
|
95
|
+
try {
|
|
96
|
+
await fs.access(this.keyPath);
|
|
97
|
+
return true;
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Encrypt a value if it looks sensitive.
|
|
106
|
+
*/
|
|
107
|
+
export function looksEncrypted(value: string): boolean {
|
|
108
|
+
// Base64 strings that look like encryption output
|
|
109
|
+
return /^[A-Za-z0-9+/]{40,}={0,2}$/.test(value);
|
|
110
|
+
}
|
package/src/core/zero-config.ts
CHANGED
|
@@ -32,12 +32,8 @@ export const ZERO_CONFIG_PLUGINS = [
|
|
|
32
32
|
|
|
33
33
|
// Infrastructure - local tools
|
|
34
34
|
'shell', // Safe shell commands (whitelist)
|
|
35
|
-
'docker', // Local Docker (if installed)
|
|
36
35
|
|
|
37
|
-
//
|
|
38
|
-
'github', // Public repos, user info
|
|
39
|
-
|
|
40
|
-
// Weather - uses free Open-Meteo API
|
|
36
|
+
// Weather - uses free Open-Meteo API (no key required)
|
|
41
37
|
'weather', // Weather/current forecasts
|
|
42
38
|
|
|
43
39
|
// File system tools are built into Shell plugin
|