@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
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# Plugin SDK
|
|
2
|
+
|
|
3
|
+
Create your own plugins for Conductor.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { Plugin, PluginTool } from '@useconductor/conductor/plugins';
|
|
9
|
+
|
|
10
|
+
export class MyPlugin implements Plugin {
|
|
11
|
+
name = 'my-plugin';
|
|
12
|
+
description = 'My custom plugin';
|
|
13
|
+
version = '1.0.0';
|
|
14
|
+
|
|
15
|
+
async initialize(conductor) {
|
|
16
|
+
// Setup - e.g., connect to API, load config
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
isConfigured(): boolean {
|
|
20
|
+
// Return true if plugin has required credentials
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getTools(): PluginTool[] {
|
|
25
|
+
return [
|
|
26
|
+
{
|
|
27
|
+
name: 'my_plugin_action',
|
|
28
|
+
description: 'Does something useful',
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
input: { type: 'string', description: 'Input description' }
|
|
33
|
+
},
|
|
34
|
+
required: ['input']
|
|
35
|
+
},
|
|
36
|
+
handler: async (args) => {
|
|
37
|
+
const result = await this.doSomething(args.input);
|
|
38
|
+
return { result };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Optional: proactive context for AI
|
|
45
|
+
async getContext() {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Full Example
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { Plugin, PluginTool, ToolContext } from '@useconductor/conductor/plugins';
|
|
55
|
+
|
|
56
|
+
export class GitHub IssuesPlugin implements Plugin {
|
|
57
|
+
name = 'github-issues';
|
|
58
|
+
description = 'Manage GitHub issues';
|
|
59
|
+
version = '1.0.0';
|
|
60
|
+
|
|
61
|
+
private apiKey?: string;
|
|
62
|
+
private owner?: string;
|
|
63
|
+
private repo?: string;
|
|
64
|
+
|
|
65
|
+
async initialize(conductor) {
|
|
66
|
+
const config = conductor.getConfig();
|
|
67
|
+
this.apiKey = await conductor.getKeychain().get('github', 'token');
|
|
68
|
+
|
|
69
|
+
const prefs = config.get('plugins.github-issues');
|
|
70
|
+
this.owner = prefs?.owner;
|
|
71
|
+
this.repo = prefs?.repo;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
isConfigured(): boolean {
|
|
75
|
+
return !!this.apiKey && !!this.owner && !!this.repo;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getTools(): PluginTool[] {
|
|
79
|
+
return [
|
|
80
|
+
{
|
|
81
|
+
name: 'github_issues_list',
|
|
82
|
+
description: 'List GitHub issues',
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: 'object',
|
|
85
|
+
properties: {
|
|
86
|
+
state: {
|
|
87
|
+
type: 'string',
|
|
88
|
+
enum: ['open', 'closed', 'all'],
|
|
89
|
+
description: 'Issue state'
|
|
90
|
+
},
|
|
91
|
+
limit: {
|
|
92
|
+
type: 'number',
|
|
93
|
+
description: 'Max issues to return'
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
handler: async (args, context) => {
|
|
98
|
+
return this.listIssues(args.state, args.limit);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'github_issues_create',
|
|
103
|
+
description: 'Create a GitHub issue',
|
|
104
|
+
inputSchema: {
|
|
105
|
+
type: 'object',
|
|
106
|
+
properties: {
|
|
107
|
+
title: { type: 'string' },
|
|
108
|
+
body: { type: 'string' },
|
|
109
|
+
labels: { type: 'array', items: { type: 'string' } }
|
|
110
|
+
},
|
|
111
|
+
required: ['title']
|
|
112
|
+
},
|
|
113
|
+
handler: async (args) => {
|
|
114
|
+
return this.createIssue(args.title, args.body, args.labels);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private async listIssues(state: string = 'open', limit: number = 10) {
|
|
121
|
+
// Implementation
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private async createIssue(title: string, body?: string, labels?: string[]) {
|
|
125
|
+
// Implementation
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Config Schema
|
|
131
|
+
|
|
132
|
+
For `conductor plugins setup <name>`:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
getConfigSchema() {
|
|
136
|
+
return {
|
|
137
|
+
fields: {
|
|
138
|
+
owner: {
|
|
139
|
+
label: 'GitHub Owner',
|
|
140
|
+
type: 'string',
|
|
141
|
+
required: true,
|
|
142
|
+
description: 'Organization or username'
|
|
143
|
+
},
|
|
144
|
+
repo: {
|
|
145
|
+
label: 'Repository',
|
|
146
|
+
type: 'string',
|
|
147
|
+
required: true
|
|
148
|
+
},
|
|
149
|
+
token: {
|
|
150
|
+
label: 'GitHub Token',
|
|
151
|
+
type: 'password',
|
|
152
|
+
secret: true,
|
|
153
|
+
description: 'Personal access token'
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Tool Handler Signature
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
handler: async (
|
|
164
|
+
args: Record<string, unknown>, // Parsed input
|
|
165
|
+
context: ToolContext // Execution context
|
|
166
|
+
) => {
|
|
167
|
+
// args = parsed and validated input
|
|
168
|
+
// context.conductor = Conductor instance
|
|
169
|
+
// context.user = user info (in multi-user mode)
|
|
170
|
+
|
|
171
|
+
return { /* result */ };
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Publishing
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
# Build
|
|
179
|
+
npm run build
|
|
180
|
+
|
|
181
|
+
# Publish to npm
|
|
182
|
+
npm publish
|
|
183
|
+
|
|
184
|
+
# Or submit to Conductor marketplace
|
|
185
|
+
conductor plugins publish ./dist
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Types
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
interface Plugin {
|
|
192
|
+
name: string;
|
|
193
|
+
description: string;
|
|
194
|
+
version: string;
|
|
195
|
+
|
|
196
|
+
initialize(conductor: Conductor): Promise<void>;
|
|
197
|
+
isConfigured(): boolean;
|
|
198
|
+
getTools(): PluginTool[];
|
|
199
|
+
|
|
200
|
+
// Optional
|
|
201
|
+
getConfigSchema?(): PluginConfigSchema;
|
|
202
|
+
getContext?(): Promise<string | null>;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
interface PluginTool {
|
|
206
|
+
name: string;
|
|
207
|
+
description: string;
|
|
208
|
+
inputSchema: object;
|
|
209
|
+
handler: (args: any, context: ToolContext) => Promise<any>;
|
|
210
|
+
requiresApproval?: boolean;
|
|
211
|
+
}
|
|
212
|
+
```
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@useconductor/conductor",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "The AI Tool Hub — One MCP server.
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "The AI Tool Hub — One MCP server. 255 tools. Every AI agent.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"conductor": "./dist/cli/index.js"
|
|
@@ -19,14 +19,16 @@
|
|
|
19
19
|
"format": "prettier --write \"src/**/*.ts\"",
|
|
20
20
|
"format:check": "prettier --check \"src/**/*.ts\"",
|
|
21
21
|
"typecheck": "tsc --noEmit",
|
|
22
|
-
"prepublishOnly": "npm run build"
|
|
22
|
+
"prepublishOnly": "npm run build",
|
|
23
|
+
"publish": "npm run build && npm publish",
|
|
24
|
+
"publish:beta": "npm run build && npm publish --tag beta"
|
|
23
25
|
},
|
|
24
26
|
"publishConfig": {
|
|
25
27
|
"access": "public",
|
|
26
28
|
"registry": "https://registry.npmjs.org/"
|
|
27
29
|
},
|
|
28
30
|
"engines": {
|
|
29
|
-
"node": ">=
|
|
31
|
+
"node": ">=20.12.0"
|
|
30
32
|
},
|
|
31
33
|
"keywords": [
|
|
32
34
|
"mcp",
|
|
@@ -44,8 +46,6 @@
|
|
|
44
46
|
"license": "Apache-2.0",
|
|
45
47
|
"type": "module",
|
|
46
48
|
"dependencies": {
|
|
47
|
-
"@anthropic-ai/sdk": "^0.74.0",
|
|
48
|
-
"@google/generative-ai": "^0.24.1",
|
|
49
49
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
50
50
|
"@octokit/rest": "^22.0.1",
|
|
51
51
|
"@slack/bolt": "^4.6.0",
|
|
@@ -59,7 +59,6 @@
|
|
|
59
59
|
"inquirer": "^13.2.5",
|
|
60
60
|
"mathjs": "^15.1.1",
|
|
61
61
|
"open": "^11.0.0",
|
|
62
|
-
"openai": "^6.22.0",
|
|
63
62
|
"ora": "^9.3.0",
|
|
64
63
|
"pino": "^10.3.1",
|
|
65
64
|
"pino-pretty": "^13.1.3",
|
|
@@ -84,7 +83,11 @@
|
|
|
84
83
|
"prettier": "^3.8.1",
|
|
85
84
|
"tsx": "^4.21.0",
|
|
86
85
|
"typescript": "^5.9.3",
|
|
87
|
-
"vitepress": "^1.6.4",
|
|
88
86
|
"vitest": "^4.1.2"
|
|
87
|
+
},
|
|
88
|
+
"optionalDependencies": {
|
|
89
|
+
"@anthropic-ai/sdk": "^0.86.1",
|
|
90
|
+
"@google/generative-ai": "^0.24.1",
|
|
91
|
+
"openai": "^6.34.0"
|
|
89
92
|
}
|
|
90
93
|
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* conductor audit — query and verify the tamper-evident audit log.
|
|
3
|
+
*
|
|
4
|
+
* Commands:
|
|
5
|
+
* conductor audit list — filter and display log entries
|
|
6
|
+
* conductor audit verify — verify SHA-256 chain integrity
|
|
7
|
+
* conductor audit tail — stream the log in real time
|
|
8
|
+
* conductor audit export — export entries to JSON or NDJSON
|
|
9
|
+
* conductor audit stats — show summary statistics
|
|
10
|
+
* conductor audit rotate — manually rotate the current log file
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'fs/promises';
|
|
14
|
+
import { createReadStream } from 'fs';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import readline from 'readline';
|
|
17
|
+
import { AuditLogger } from '../../core/audit.js';
|
|
18
|
+
import type { AuditEntry } from '../../core/audit.js';
|
|
19
|
+
import type { Conductor } from '../../core/conductor.js';
|
|
20
|
+
|
|
21
|
+
function getAuditDir(conductor: Conductor): string {
|
|
22
|
+
return path.join(conductor.getConfig().getConfigDir(), 'audit');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getAuditFile(conductor: Conductor): string {
|
|
26
|
+
return path.join(getAuditDir(conductor), 'audit.log');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Read all entries from all audit log files, newest files last. */
|
|
30
|
+
async function readAllEntries(conductor: Conductor): Promise<AuditEntry[]> {
|
|
31
|
+
const dir = getAuditDir(conductor);
|
|
32
|
+
const entries: AuditEntry[] = [];
|
|
33
|
+
|
|
34
|
+
let files: string[];
|
|
35
|
+
try {
|
|
36
|
+
files = await fs.readdir(dir);
|
|
37
|
+
} catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const logFiles = files.filter((f) => f.endsWith('.log')).sort();
|
|
42
|
+
|
|
43
|
+
for (const file of logFiles) {
|
|
44
|
+
const content = await fs.readFile(path.join(dir, file), 'utf-8').catch(() => '');
|
|
45
|
+
for (const line of content.split('\n').filter((l) => l.trim())) {
|
|
46
|
+
try {
|
|
47
|
+
entries.push(JSON.parse(line) as AuditEntry);
|
|
48
|
+
} catch {
|
|
49
|
+
// skip malformed lines
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return entries;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatEntry(e: AuditEntry): string {
|
|
58
|
+
const time = e.timestamp.replace('T', ' ').replace(/\.\d+Z$/, '');
|
|
59
|
+
const icon = e.result === 'success' ? '✓' : e.result === 'failure' ? '✗' : e.result === 'denied' ? '⊘' : '⏱';
|
|
60
|
+
return ` ${icon} ${time} ${e.actor.padEnd(12)} ${e.action.padEnd(16)} ${e.resource}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── list ──────────────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
export async function auditList(
|
|
66
|
+
conductor: Conductor,
|
|
67
|
+
opts: {
|
|
68
|
+
actor?: string;
|
|
69
|
+
action?: string;
|
|
70
|
+
tool?: string;
|
|
71
|
+
result?: string;
|
|
72
|
+
since?: string;
|
|
73
|
+
until?: string;
|
|
74
|
+
limit?: string;
|
|
75
|
+
json?: boolean;
|
|
76
|
+
},
|
|
77
|
+
): Promise<void> {
|
|
78
|
+
let entries = await readAllEntries(conductor);
|
|
79
|
+
|
|
80
|
+
if (opts.actor) entries = entries.filter((e) => e.actor === opts.actor);
|
|
81
|
+
if (opts.action) entries = entries.filter((e) => e.action === opts.action);
|
|
82
|
+
if (opts.tool) entries = entries.filter((e) => e.resource === opts.tool);
|
|
83
|
+
if (opts.result) entries = entries.filter((e) => e.result === opts.result);
|
|
84
|
+
if (opts.since) entries = entries.filter((e) => e.timestamp >= opts.since!);
|
|
85
|
+
if (opts.until) entries = entries.filter((e) => e.timestamp <= opts.until!);
|
|
86
|
+
|
|
87
|
+
const limit = opts.limit ? parseInt(opts.limit, 10) : 100;
|
|
88
|
+
entries = entries.slice(-limit);
|
|
89
|
+
|
|
90
|
+
if (entries.length === 0) {
|
|
91
|
+
console.log('\n No audit entries found.\n');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (opts.json) {
|
|
96
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log('');
|
|
101
|
+
console.log(` 📋 Audit Log (${entries.length} entries)\n`);
|
|
102
|
+
console.log(
|
|
103
|
+
` ${'RESULT'.padEnd(3)} ${'TIMESTAMP'.padEnd(19)} ${'ACTOR'.padEnd(12)} ${'ACTION'.padEnd(16)} RESOURCE`,
|
|
104
|
+
);
|
|
105
|
+
console.log(' ' + '─'.repeat(80));
|
|
106
|
+
for (const e of entries) {
|
|
107
|
+
console.log(formatEntry(e));
|
|
108
|
+
}
|
|
109
|
+
console.log('');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── verify ────────────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
export async function auditVerify(conductor: Conductor, opts: { json?: boolean }): Promise<void> {
|
|
115
|
+
const logger = new AuditLogger(conductor.getConfig().getConfigDir(), { flushIntervalMs: 50000 });
|
|
116
|
+
try {
|
|
117
|
+
const { valid, brokenAt } = await logger.verifyIntegrity();
|
|
118
|
+
|
|
119
|
+
if (opts.json) {
|
|
120
|
+
console.log(JSON.stringify({ valid, brokenAt }));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log('');
|
|
125
|
+
if (valid) {
|
|
126
|
+
console.log(' ✅ Audit log integrity verified — no tampering detected.\n');
|
|
127
|
+
} else {
|
|
128
|
+
console.log(` ❌ Integrity check FAILED — chain broken at: ${brokenAt}\n`);
|
|
129
|
+
console.log(' The audit log may have been tampered with. Contact your security team.\n');
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
} finally {
|
|
133
|
+
await logger.close();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── tail ──────────────────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
export async function auditTail(conductor: Conductor, opts: { json?: boolean; lines?: string }): Promise<void> {
|
|
140
|
+
const logFile = getAuditFile(conductor);
|
|
141
|
+
const initialLines = parseInt(opts.lines || '20', 10);
|
|
142
|
+
|
|
143
|
+
// Show last N lines from existing file
|
|
144
|
+
try {
|
|
145
|
+
const content = await fs.readFile(logFile, 'utf-8');
|
|
146
|
+
const lines = content.split('\n').filter((l) => l.trim());
|
|
147
|
+
const recent = lines.slice(-initialLines);
|
|
148
|
+
|
|
149
|
+
console.log('');
|
|
150
|
+
for (const line of recent) {
|
|
151
|
+
try {
|
|
152
|
+
const e = JSON.parse(line) as AuditEntry;
|
|
153
|
+
if (opts.json) {
|
|
154
|
+
console.log(JSON.stringify(e));
|
|
155
|
+
} else {
|
|
156
|
+
console.log(formatEntry(e));
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
// skip
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
console.log('\n No audit log found yet.\n');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Watch for new lines
|
|
167
|
+
console.log('\n Watching for new entries (Ctrl+C to stop)...\n');
|
|
168
|
+
|
|
169
|
+
let fileSize = 0;
|
|
170
|
+
try {
|
|
171
|
+
const stat = await fs.stat(logFile);
|
|
172
|
+
fileSize = stat.size;
|
|
173
|
+
} catch {
|
|
174
|
+
fileSize = 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const watcher = setInterval(async () => {
|
|
178
|
+
try {
|
|
179
|
+
const stat = await fs.stat(logFile);
|
|
180
|
+
if (stat.size > fileSize) {
|
|
181
|
+
const stream = createReadStream(logFile, { start: fileSize });
|
|
182
|
+
const rl = readline.createInterface({ input: stream });
|
|
183
|
+
rl.on('line', (line) => {
|
|
184
|
+
if (!line.trim()) return;
|
|
185
|
+
try {
|
|
186
|
+
const e = JSON.parse(line) as AuditEntry;
|
|
187
|
+
if (opts.json) {
|
|
188
|
+
console.log(JSON.stringify(e));
|
|
189
|
+
} else {
|
|
190
|
+
console.log(formatEntry(e));
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
// skip
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
fileSize = stat.size;
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
// file not yet created
|
|
200
|
+
}
|
|
201
|
+
}, 500);
|
|
202
|
+
|
|
203
|
+
process.on('SIGINT', () => {
|
|
204
|
+
clearInterval(watcher);
|
|
205
|
+
console.log('\n');
|
|
206
|
+
process.exit(0);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Keep alive
|
|
210
|
+
await new Promise(() => {});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── export ────────────────────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
export async function auditExport(
|
|
216
|
+
conductor: Conductor,
|
|
217
|
+
opts: {
|
|
218
|
+
output?: string;
|
|
219
|
+
format?: string;
|
|
220
|
+
since?: string;
|
|
221
|
+
until?: string;
|
|
222
|
+
},
|
|
223
|
+
): Promise<void> {
|
|
224
|
+
let entries = await readAllEntries(conductor);
|
|
225
|
+
|
|
226
|
+
if (opts.since) entries = entries.filter((e) => e.timestamp >= opts.since!);
|
|
227
|
+
if (opts.until) entries = entries.filter((e) => e.timestamp <= opts.until!);
|
|
228
|
+
|
|
229
|
+
const format = opts.format || 'json';
|
|
230
|
+
const output = opts.output || `-`;
|
|
231
|
+
|
|
232
|
+
let content: string;
|
|
233
|
+
if (format === 'ndjson') {
|
|
234
|
+
content = entries.map((e) => JSON.stringify(e)).join('\n') + '\n';
|
|
235
|
+
} else {
|
|
236
|
+
content = JSON.stringify(entries, null, 2) + '\n';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (output === '-') {
|
|
240
|
+
process.stdout.write(content);
|
|
241
|
+
} else {
|
|
242
|
+
await fs.writeFile(output, content, 'utf-8');
|
|
243
|
+
console.log(`\n ✅ Exported ${entries.length} entries to: ${output}\n`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ── stats ─────────────────────────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
export async function auditStats(conductor: Conductor, opts: { json?: boolean }): Promise<void> {
|
|
250
|
+
const entries = await readAllEntries(conductor);
|
|
251
|
+
|
|
252
|
+
if (entries.length === 0) {
|
|
253
|
+
console.log('\n No audit entries found.\n');
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const byAction: Record<string, number> = {};
|
|
258
|
+
const byActor: Record<string, number> = {};
|
|
259
|
+
const byResult: Record<string, number> = {};
|
|
260
|
+
|
|
261
|
+
for (const e of entries) {
|
|
262
|
+
byAction[e.action] = (byAction[e.action] || 0) + 1;
|
|
263
|
+
byActor[e.actor] = (byActor[e.actor] || 0) + 1;
|
|
264
|
+
byResult[e.result] = (byResult[e.result] || 0) + 1;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const stats = {
|
|
268
|
+
total: entries.length,
|
|
269
|
+
first: entries[0]?.timestamp,
|
|
270
|
+
last: entries[entries.length - 1]?.timestamp,
|
|
271
|
+
by_action: byAction,
|
|
272
|
+
by_actor: byActor,
|
|
273
|
+
by_result: byResult,
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
if (opts.json) {
|
|
277
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
console.log('');
|
|
282
|
+
console.log(` 📊 Audit Log Statistics\n`);
|
|
283
|
+
console.log(` Total entries: ${stats.total}`);
|
|
284
|
+
console.log(` First entry: ${stats.first}`);
|
|
285
|
+
console.log(` Last entry: ${stats.last}`);
|
|
286
|
+
console.log('');
|
|
287
|
+
console.log(' By result:');
|
|
288
|
+
for (const [k, v] of Object.entries(byResult).sort((a, b) => b[1] - a[1])) {
|
|
289
|
+
console.log(` ${k.padEnd(12)} ${v}`);
|
|
290
|
+
}
|
|
291
|
+
console.log('');
|
|
292
|
+
console.log(' By action (top 10):');
|
|
293
|
+
for (const [k, v] of Object.entries(byAction)
|
|
294
|
+
.sort((a, b) => b[1] - a[1])
|
|
295
|
+
.slice(0, 10)) {
|
|
296
|
+
console.log(` ${k.padEnd(20)} ${v}`);
|
|
297
|
+
}
|
|
298
|
+
console.log('');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── rotate ────────────────────────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
export async function auditRotate(conductor: Conductor): Promise<void> {
|
|
304
|
+
const logFile = getAuditFile(conductor);
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
const stat = await fs.stat(logFile);
|
|
308
|
+
if (stat.size === 0) {
|
|
309
|
+
console.log('\n Log file is empty — nothing to rotate.\n');
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const rotated = `${logFile}.${Date.now()}.bak`;
|
|
313
|
+
await fs.rename(logFile, rotated);
|
|
314
|
+
console.log(`\n ✅ Rotated audit log to: ${path.basename(rotated)}\n`);
|
|
315
|
+
} catch {
|
|
316
|
+
console.log('\n No audit log to rotate.\n');
|
|
317
|
+
}
|
|
318
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* conductor circuit — view and manage circuit breaker state.
|
|
3
|
+
*
|
|
4
|
+
* Commands:
|
|
5
|
+
* conductor circuit list — show state of all circuit breakers
|
|
6
|
+
* conductor circuit reset — reset a specific circuit to closed state
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Conductor } from '../../core/conductor.js';
|
|
10
|
+
|
|
11
|
+
export async function circuitList(conductor: Conductor, opts: { json?: boolean }): Promise<void> {
|
|
12
|
+
await conductor.initialize();
|
|
13
|
+
|
|
14
|
+
// Circuit breaker state is held in the running MCP server's memory.
|
|
15
|
+
// If we're not in the server process, we read the persisted health state
|
|
16
|
+
// from the health check endpoint instead.
|
|
17
|
+
try {
|
|
18
|
+
const { HealthChecker } = await import('../../core/health.js');
|
|
19
|
+
const checker = new HealthChecker();
|
|
20
|
+
const report = await checker.detailed('0');
|
|
21
|
+
|
|
22
|
+
const metrics = report.metrics;
|
|
23
|
+
|
|
24
|
+
if (opts.json) {
|
|
25
|
+
console.log(JSON.stringify({ open_circuits: metrics?.openCircuits ?? 0 }, null, 2));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log('');
|
|
30
|
+
console.log(' ⚡ Circuit Breaker Status\n');
|
|
31
|
+
|
|
32
|
+
if (!metrics || metrics.openCircuits === 0) {
|
|
33
|
+
console.log(' All circuits are CLOSED (healthy).\n');
|
|
34
|
+
} else {
|
|
35
|
+
console.log(
|
|
36
|
+
` ⚠️ ${metrics.openCircuits} circuit(s) OPEN — tools unavailable until recovery timeout expires.\n`,
|
|
37
|
+
);
|
|
38
|
+
console.log(' Run: conductor health --json for per-tool details.\n');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log(` Total tool calls: ${metrics?.totalToolCalls ?? 0}`);
|
|
42
|
+
console.log(` Failed calls: ${metrics?.failedToolCalls ?? 0}`);
|
|
43
|
+
console.log(` Avg latency: ${metrics?.avgLatencyMs ?? 0}ms`);
|
|
44
|
+
console.log('');
|
|
45
|
+
console.log(' To reset a specific circuit: conductor circuit reset <tool>\n');
|
|
46
|
+
} catch (e: unknown) {
|
|
47
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
48
|
+
console.error(` ❌ ${msg}\n`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function circuitReset(conductor: Conductor, tool: string): Promise<void> {
|
|
53
|
+
await conductor.initialize();
|
|
54
|
+
|
|
55
|
+
// Circuit breaker reset requires the server process. We emit a signal file
|
|
56
|
+
// that the running server will pick up, or print guidance if no server is running.
|
|
57
|
+
console.log('');
|
|
58
|
+
console.log(` ℹ️ Circuit breaker state lives in the running MCP server process.`);
|
|
59
|
+
console.log(` To reset "${tool}":`);
|
|
60
|
+
console.log(` 1. Restart the MCP server: conductor mcp start`);
|
|
61
|
+
console.log(` 2. Or wait for the recovery timeout to expire (default: 30s)`);
|
|
62
|
+
console.log('');
|
|
63
|
+
}
|